mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 05:02:07 +00:00
chore: rebrand companion-os to clanker-agent
- Rename all package names from companion-* to clanker-* - Update npm scopes from @mariozechner to @harivansh-afk - Rename config directories .companion -> .clanker - Rename environment variables COMPANION_* -> CLANKER_* - Update all documentation, README files, and install scripts - Rename package directories (companion-channels, companion-grind, companion-teams) - Update GitHub URLs to harivansh-afk/clanker-agent - Preserve full git history from companion-cloud monorepo
This commit is contained in:
parent
f93fe7d1a0
commit
67168d8289
356 changed files with 2249 additions and 10223 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -23,13 +23,13 @@ packages/*/dist-firefox/
|
||||||
.npm/
|
.npm/
|
||||||
coverage/
|
coverage/
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
.companion_config/
|
.clanker_config/
|
||||||
tui-debug.log
|
tui-debug.log
|
||||||
compaction-results/
|
compaction-results/
|
||||||
.opencode/
|
.opencode/
|
||||||
syntax.jsonl
|
syntax.jsonl
|
||||||
out.jsonl
|
out.jsonl
|
||||||
companion-*.html
|
clanker-*.html
|
||||||
out.html
|
out.html
|
||||||
packages/coding-agent/binaries/
|
packages/coding-agent/binaries/
|
||||||
todo.md
|
todo.md
|
||||||
|
|
|
||||||
24
AGENTS.md
24
AGENTS.md
|
|
@ -71,29 +71,29 @@ When closing issues via commit:
|
||||||
- GitHub CLI for issues/PRs
|
- GitHub CLI for issues/PRs
|
||||||
- Add package labels to issues/PRs: pkg:agent, pkg:ai, pkg:coding-agent, pkg:mom, pkg:pods, pkg:tui, pkg:web-ui
|
- Add package labels to issues/PRs: pkg:agent, pkg:ai, pkg:coding-agent, pkg:mom, pkg:pods, pkg:tui, pkg:web-ui
|
||||||
|
|
||||||
## Testing companion Interactive Mode with tmux
|
## Testing clanker Interactive Mode with tmux
|
||||||
|
|
||||||
To test companion's TUI in a controlled terminal environment:
|
To test clanker's TUI in a controlled terminal environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create tmux session with specific dimensions
|
# Create tmux session with specific dimensions
|
||||||
tmux new-session -d -s companion-test -x 80 -y 24
|
tmux new-session -d -s clanker-test -x 80 -y 24
|
||||||
|
|
||||||
# Start companion from source
|
# Start clanker from source
|
||||||
tmux send-keys -t companion-test "cd /Users/badlogic/workspaces/companion-mono && ./companion-test.sh" Enter
|
tmux send-keys -t clanker-test "cd /Users/badlogic/workspaces/clanker-agent && ./clanker-test.sh" Enter
|
||||||
|
|
||||||
# Wait for startup, then capture output
|
# Wait for startup, then capture output
|
||||||
sleep 3 && tmux capture-pane -t companion-test -p
|
sleep 3 && tmux capture-pane -t clanker-test -p
|
||||||
|
|
||||||
# Send input
|
# Send input
|
||||||
tmux send-keys -t companion-test "your prompt here" Enter
|
tmux send-keys -t clanker-test "your prompt here" Enter
|
||||||
|
|
||||||
# Send special keys
|
# Send special keys
|
||||||
tmux send-keys -t companion-test Escape
|
tmux send-keys -t clanker-test Escape
|
||||||
tmux send-keys -t companion-test C-o # ctrl+o
|
tmux send-keys -t clanker-test C-o # ctrl+o
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
tmux kill-session -t companion-test
|
tmux kill-session -t clanker-test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Style
|
## Style
|
||||||
|
|
@ -127,8 +127,8 @@ Use these sections under `## [Unreleased]`:
|
||||||
|
|
||||||
### Attribution
|
### Attribution
|
||||||
|
|
||||||
- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/badlogic/companion-mono/issues/123))`
|
- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/badlogic/clanker-agent/issues/123))`
|
||||||
- **External contributions**: `Added feature X ([#456](https://github.com/badlogic/companion-mono/pull/456) by [@username](https://github.com/username))`
|
- **External contributions**: `Added feature X ([#456](https://github.com/badlogic/clanker-agent/pull/456) by [@username](https://github.com/username))`
|
||||||
|
|
||||||
## Adding a New LLM Provider (packages/ai)
|
## Adding a New LLM Provider (packages/ai)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Contributing to companion
|
# Contributing to clanker-agent
|
||||||
|
|
||||||
Thanks for wanting to contribute! This guide exists to save both of us time.
|
Thanks for wanting to contribute! This guide exists to save both of us time.
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ Thanks for wanting to contribute! This guide exists to save both of us time.
|
||||||
|
|
||||||
Using AI to write code is fine. You can gain understanding by interrogating an agent with access to the codebase until you grasp all edge cases and effects of your changes. What's not fine is submitting agent-generated slop without that understanding.
|
Using AI to write code is fine. You can gain understanding by interrogating an agent with access to the codebase until you grasp all edge cases and effects of your changes. What's not fine is submitting agent-generated slop without that understanding.
|
||||||
|
|
||||||
If you use an agent, run it from the `companion` root directory so it picks up `AGENTS.md` automatically. Your agent must follow the rules and guidelines in that file.
|
If you use an agent, run it from the `clanker` root directory so it picks up `AGENTS.md` automatically. Your agent must follow the rules and guidelines in that file.
|
||||||
|
|
||||||
## First-Time Contributors
|
## First-Time Contributors
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ If you're adding a new provider to `packages/ai`, see `AGENTS.md` for required t
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
companion's core is minimal. If your feature doesn't belong in the core, it should be an extension. PRs that bloat the core will likely be rejected.
|
clanker's core is minimal. If your feature doesn't belong in the core, it should be an extension. PRs that bloat the core will likely be rejected.
|
||||||
|
|
||||||
## Questions?
|
## Questions?
|
||||||
|
|
||||||
|
|
|
||||||
64
README.md
64
README.md
|
|
@ -1,33 +1,33 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://shittycodingagent.ai">
|
<a href="https://clanker.dev">
|
||||||
<img src="https://shittycodingagent.ai/logo.svg" alt="companion logo" width="128">
|
<img src="https://clanker.dev/logo.svg" alt="clanker logo" width="128">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.com/invite/3cU7Bz4UPx"><img alt="Discord" src="https://img.shields.io/badge/discord-community-5865F2?style=flat-square&logo=discord&logoColor=white" /></a>
|
<a href="https://discord.com/invite/3cU7Bz4UPx"><img alt="Discord" src="https://img.shields.io/badge/discord-community-5865F2?style=flat-square&logo=discord&logoColor=white" /></a>
|
||||||
<a href="https://github.com/getcompanion-ai/co-mono/actions/workflows/ci.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/getcompanion-ai/co-mono/ci.yml?style=flat-square&branch=main" /></a>
|
<a href="https://github.com/harivansh-afk/clanker-agent/actions/workflows/ci.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/harivansh-afk/clanker-agent/ci.yml?style=flat-square&branch=main" /></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://companion.dev">companion.dev</a> domain graciously donated by
|
<a href="https://clanker.dev">clanker.dev</a> domain graciously donated by
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<a href="https://exe.dev"><img src="packages/coding-agent/docs/images/exy.png" alt="Exy mascot" width="48" /><br />exe.dev</a>
|
<a href="https://exe.dev"><img src="packages/coding-agent/docs/images/exy.png" alt="Exy mascot" width="48" /><br />exe.dev</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# companion
|
# clanker-agent
|
||||||
|
|
||||||
> **Looking for the companion coding agent?** See **[packages/coding-agent](packages/coding-agent)** for installation and usage.
|
> **Looking for the clanker coding agent?** See **[packages/coding-agent](packages/coding-agent)** for installation and usage.
|
||||||
|
|
||||||
Tools for building AI agents and running the companion coding agent.
|
Tools for building AI agents and running the clanker coding agent.
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
| Package | Description |
|
| Package | Description |
|
||||||
| ---------------------------------------------------------- | ---------------------------------------------------------------- |
|
| ---------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||||
| **[@mariozechner/companion-ai](packages/ai)** | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
| **[@mariozechner/clanker-ai](packages/ai)** | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||||
| **[@mariozechner/companion-agent-core](packages/agent)** | Agent runtime with tool calling and state management |
|
| **[@mariozechner/clanker-agent-core](packages/agent)** | Agent runtime with tool calling and state management |
|
||||||
| **[@mariozechner/companion-coding-agent](packages/coding-agent)** | Interactive coding agent CLI |
|
| **[@mariozechner/clanker-coding-agent](packages/coding-agent)** | Interactive coding agent CLI |
|
||||||
| **[@mariozechner/companion-tui](packages/tui)** | Terminal UI library with differential rendering |
|
| **[@mariozechner/clanker-tui](packages/tui)** | Terminal UI library with differential rendering |
|
||||||
| **[@mariozechner/companion-web-ui](packages/web-ui)** | Web components for AI chat interfaces |
|
| **[@mariozechner/clanker-web-ui](packages/web-ui)** | Web components for AI chat interfaces |
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
@ -40,27 +40,27 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [AGENTS.m
|
||||||
Use this for users on production machines where you don't want to expose source.
|
Use this for users on production machines where you don't want to expose source.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/harivansh-afk/clanker-agent/main/public-install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Install everything and keep it always-on (recommended for new devices):
|
Install everything and keep it always-on (recommended for new devices):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash -s -- --daemon --start
|
curl -fsSL https://raw.githubusercontent.com/harivansh-afk/clanker-agent/main/public-install.sh | bash -s -- --daemon --start
|
||||||
```
|
```
|
||||||
|
|
||||||
This installer:
|
This installer:
|
||||||
|
|
||||||
- Downloads the latest release (or falls back to source when needed),
|
- Downloads the latest release (or falls back to source when needed),
|
||||||
- writes `~/.local/bin/companion` launcher,
|
- writes `~/.local/bin/clanker` launcher,
|
||||||
- populates `~/.companion/agent/settings.json` with package list,
|
- populates `~/.clanker/agent/settings.json` with package list,
|
||||||
- installs packages (if `npm` is available),
|
- installs packages (if `npm` is available),
|
||||||
- and can install a user service for `companion daemon` so it stays alive (`systemd` on Linux, `launchd` on macOS).
|
- and can install a user service for `clanker daemon` so it stays alive (`systemd` on Linux, `launchd` on macOS).
|
||||||
|
|
||||||
Preinstalled package sources are:
|
Preinstalled package sources are:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
["npm:@e9n/companion-channels", "npm:companion-teams"]
|
["npm:@e9n/clanker-channels", "npm:clanker-teams"]
|
||||||
```
|
```
|
||||||
|
|
||||||
If `npm` is available, it also installs these packages during install.
|
If `npm` is available, it also installs these packages during install.
|
||||||
|
|
@ -68,48 +68,48 @@ If `npm` is available, it also installs these packages during install.
|
||||||
If no release asset is found, the installer falls back to source.
|
If no release asset is found, the installer falls back to source.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
COMPANION_FALLBACK_TO_SOURCE=0 \
|
CLANKER_FALLBACK_TO_SOURCE=0 \
|
||||||
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash -s -- --daemon --start
|
curl -fsSL https://raw.githubusercontent.com/harivansh-afk/clanker-agent/main/public-install.sh | bash -s -- --daemon --start
|
||||||
```
|
```
|
||||||
|
|
||||||
`public-install.sh` options:
|
`public-install.sh` options:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash -s -- --help
|
curl -fsSL https://raw.githubusercontent.com/harivansh-afk/clanker-agent/main/public-install.sh | bash -s -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local (source)
|
### Local (source)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/getcompanion-ai/co-mono.git
|
git clone https://github.com/harivansh-afk/clanker-agent.git
|
||||||
cd co-mono
|
cd clanker-agent
|
||||||
./install.sh
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./companion
|
./clanker
|
||||||
```
|
```
|
||||||
|
|
||||||
Run in background with extensions active:
|
Run in background with extensions active:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./companion daemon
|
./clanker daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
For a user systemd setup, create `~/.config/systemd/user/companion.service` with:
|
For a user systemd setup, create `~/.config/systemd/user/clanker.service` with:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=companion daemon
|
Description=clanker daemon
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
Environment=CO_MONO_AGENT_DIR=%h/.companion/agent
|
Environment=CLANKER_MONO_AGENT_DIR=%h/.clanker/agent
|
||||||
Environment=COMPANION_CODING_AGENT_DIR=%h/.companion/agent
|
Environment=CLANKER_CODING_AGENT_DIR=%h/.clanker/agent
|
||||||
ExecStart=/absolute/path/to/repo/companion daemon
|
ExecStart=/absolute/path/to/repo/clanker daemon
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ Then enable:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable --now companion
|
systemctl --user enable --now clanker
|
||||||
```
|
```
|
||||||
|
|
||||||
On macOS, `public-install.sh --daemon --start` now provisions a per-user `launchd` agent automatically.
|
On macOS, `public-install.sh --daemon --start` now provisions a per-user `launchd` agent automatically.
|
||||||
|
|
@ -140,7 +140,7 @@ npm install # Install all dependencies
|
||||||
npm run build # Build all packages
|
npm run build # Build all packages
|
||||||
npm run check # Lint, format, and type check
|
npm run check # Lint, format, and type check
|
||||||
./test.sh # Run tests (skips LLM-dependent tests without API keys)
|
./test.sh # Run tests (skips LLM-dependent tests without API keys)
|
||||||
./companion-test.sh # Run companion from sources (must be run from repo root)
|
./clanker-test.sh # Run clanker from sources (must be run from repo root)
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** `npm run check` requires `npm run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.
|
> **Note:** `npm run check` requires `npm run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"name": "companion",
|
"name": "clanker-agent",
|
||||||
"path": "."
|
"path": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
14
install.sh
14
install.sh
|
|
@ -24,12 +24,12 @@ need npm
|
||||||
|
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
if [[ "${COMPANION_SKIP_INSTALL:-${CO_MONO_SKIP_INSTALL:-0}}" != "1" ]]; then
|
if [[ "${CLANKER_SKIP_INSTALL:-${CLANKER_MONO_SKIP_INSTALL:-0}}" != "1" ]]; then
|
||||||
log "Installing workspace dependencies"
|
log "Installing workspace dependencies"
|
||||||
npm install
|
npm install
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${COMPANION_SKIP_BUILD:-${CO_MONO_SKIP_BUILD:-0}}" != "1" ]]; then
|
if [[ "${CLANKER_SKIP_BUILD:-${CLANKER_MONO_SKIP_BUILD:-0}}" != "1" ]]; then
|
||||||
log "Building core packages"
|
log "Building core packages"
|
||||||
BUILD_FAILED=0
|
BUILD_FAILED=0
|
||||||
for pkg in packages/tui packages/ai packages/agent packages/coding-agent; do
|
for pkg in packages/tui packages/ai packages/agent packages/coding-agent; do
|
||||||
|
|
@ -46,7 +46,7 @@ if [[ "$BUILD_FAILED" == "1" ]] && [[ ! -f "$ROOT_DIR/packages/coding-agent/src/
|
||||||
fail "No usable coding-agent CLI source found for source launch fallback."
|
fail "No usable coding-agent CLI source found for source launch fallback."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
LAUNCHER="$ROOT_DIR/companion"
|
LAUNCHER="$ROOT_DIR/clanker"
|
||||||
cat > "$LAUNCHER" <<'EOF'
|
cat > "$LAUNCHER" <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
|
@ -54,8 +54,8 @@ set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
if [[ -x "$ROOT_DIR/packages/coding-agent/dist/companion" ]]; then
|
if [[ -x "$ROOT_DIR/packages/coding-agent/dist/clanker" ]]; then
|
||||||
exec "$ROOT_DIR/packages/coding-agent/dist/companion" "$@"
|
exec "$ROOT_DIR/packages/coding-agent/dist/clanker" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "$ROOT_DIR/packages/coding-agent/dist/cli.js" ]]; then
|
if [[ -f "$ROOT_DIR/packages/coding-agent/dist/cli.js" ]]; then
|
||||||
|
|
@ -66,10 +66,10 @@ if [[ -x "$ROOT_DIR/node_modules/.bin/tsx" ]] && [[ -f "$ROOT_DIR/packages/codin
|
||||||
exec "$ROOT_DIR/node_modules/.bin/tsx" "$ROOT_DIR/packages/coding-agent/src/cli.ts" "$@"
|
exec "$ROOT_DIR/node_modules/.bin/tsx" "$ROOT_DIR/packages/coding-agent/src/cli.ts" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "ERROR: no runnable companion binary found and tsx fallback is unavailable." >&2
|
echo "ERROR: no runnable clanker binary found and tsx fallback is unavailable." >&2
|
||||||
exit 1
|
exit 1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x "$LAUNCHER"
|
chmod +x "$LAUNCHER"
|
||||||
log "Created launcher: $LAUNCHER"
|
log "Created launcher: $LAUNCHER"
|
||||||
log "Run with: ./companion"
|
log "Run with: ./clanker"
|
||||||
|
|
|
||||||
7975
package-lock.json
generated
7975
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "companion",
|
"name": "clanker-agent",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://github.com/getcompanion-ai/co-mono#readme",
|
"homepage": "https://github.com/harivansh-afk/clanker-agent#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/getcompanion-ai/co-mono/issues"
|
"url": "https://github.com/harivansh-afk/clanker-agent/issues"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/getcompanion-ai/co-mono.git"
|
"url": "git+https://github.com/harivansh-afk/clanker-agent.git"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
"dev": "concurrently --names \"ai,agent,coding-agent,tui\" --prefix-colors \"cyan,yellow,red,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/tui && npm run dev\"",
|
"dev": "concurrently --names \"ai,agent,coding-agent,tui\" --prefix-colors \"cyan,yellow,red,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/tui && npm run dev\"",
|
||||||
"dev:tsc": "cd packages/ai && npm run dev:tsc",
|
"dev:tsc": "cd packages/ai && npm run dev:tsc",
|
||||||
"check": "biome lint --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke",
|
"check": "biome lint --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke",
|
||||||
"check:browser-smoke": "sh -c 'esbuild scripts/browser-smoke-entry.ts --bundle --platform=browser --format=esm --log-limit=0 --outfile=/tmp/companion-browser-smoke.js > /tmp/companion-browser-smoke-errors.log 2>&1 || { echo \"Browser smoke check failed. See /tmp/companion-browser-smoke-errors.log\"; exit 1; }'",
|
"check:browser-smoke": "sh -c 'esbuild scripts/browser-smoke-entry.ts --bundle --platform=browser --format=esm --log-limit=0 --outfile=/tmp/clanker-browser-smoke.js > /tmp/clanker-browser-smoke-errors.log 2>&1 || { echo \"Browser smoke check failed. See /tmp/clanker-browser-smoke-errors.log\"; exit 1; }'",
|
||||||
"test": "npm run test --workspaces --if-present",
|
"test": "npm run test --workspaces --if-present",
|
||||||
"version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",
|
"version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",
|
||||||
"version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",
|
"version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/jiti": "^2.6.5",
|
"@mariozechner/jiti": "^2.6.5",
|
||||||
"@mariozechner/companion-coding-agent": "^0.30.2",
|
"@harivansh-afk/clanker-coding-agent": "^0.30.2",
|
||||||
"get-east-asian-width": "^1.4.0"
|
"get-east-asian-width": "^1.4.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
# @mariozechner/companion-agent-core
|
# @mariozechner/clanker-agent-core
|
||||||
|
|
||||||
Stateful agent with tool execution and event streaming. Built on `@mariozechner/companion-ai`.
|
Stateful agent with tool execution and event streaming. Built on `@mariozechner/clanker-ai`.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @mariozechner/companion-agent-core
|
npm install @mariozechner/clanker-agent-core
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Agent } from "@mariozechner/companion-agent-core";
|
import { Agent } from "@mariozechner/clanker-agent-core";
|
||||||
import { getModel } from "@mariozechner/companion-ai";
|
import { getModel } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
initialState: {
|
initialState: {
|
||||||
|
|
@ -298,7 +298,7 @@ Follow-up messages are checked only when there are no more tool calls and no ste
|
||||||
Extend `AgentMessage` via declaration merging:
|
Extend `AgentMessage` via declaration merging:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
declare module "@mariozechner/companion-agent-core" {
|
declare module "@mariozechner/clanker-agent-core" {
|
||||||
interface CustomAgentMessages {
|
interface CustomAgentMessages {
|
||||||
notification: { role: "notification"; text: string; timestamp: number };
|
notification: { role: "notification"; text: string; timestamp: number };
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +378,7 @@ Thrown errors are caught by the agent and reported to the LLM as tool errors wit
|
||||||
For browser apps that proxy through a backend:
|
For browser apps that proxy through a backend:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Agent, streamProxy } from "@mariozechner/companion-agent-core";
|
import { Agent, streamProxy } from "@mariozechner/clanker-agent-core";
|
||||||
|
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: (model, context, options) =>
|
streamFn: (model, context, options) =>
|
||||||
|
|
@ -395,7 +395,7 @@ const agent = new Agent({
|
||||||
For direct control without the Agent class:
|
For direct control without the Agent class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { agentLoop, agentLoopContinue } from "@mariozechner/companion-agent-core";
|
import { agentLoop, agentLoopContinue } from "@mariozechner/clanker-agent-core";
|
||||||
|
|
||||||
const context: AgentContext = {
|
const context: AgentContext = {
|
||||||
systemPrompt: "You are helpful.",
|
systemPrompt: "You are helpful.",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/companion-agent-core",
|
"name": "@harivansh-afk/clanker-agent-core",
|
||||||
"version": "0.56.2",
|
"version": "0.56.2",
|
||||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"prepublishOnly": "npm run clean && npm run build"
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/companion-ai": "^0.56.2"
|
"@harivansh-afk/clanker-ai": "^0.56.2"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ai",
|
"ai",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/getcompanion-ai/co-mono.git",
|
"url": "git+https://github.com/harivansh-afk/clanker-agent.git",
|
||||||
"directory": "packages/agent"
|
"directory": "packages/agent"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
streamSimple,
|
streamSimple,
|
||||||
type ToolResultMessage,
|
type ToolResultMessage,
|
||||||
validateToolArguments,
|
validateToolArguments,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
import type {
|
import type {
|
||||||
AgentContext,
|
AgentContext,
|
||||||
AgentEvent,
|
AgentEvent,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
type TextContent,
|
type TextContent,
|
||||||
type ThinkingBudgets,
|
type ThinkingBudgets,
|
||||||
type Transport,
|
type Transport,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
import { agentLoop, agentLoopContinue } from "./agent-loop.js";
|
import { agentLoop, agentLoopContinue } from "./agent-loop.js";
|
||||||
import type {
|
import type {
|
||||||
AgentContext,
|
AgentContext,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
type SimpleStreamOptions,
|
type SimpleStreamOptions,
|
||||||
type StopReason,
|
type StopReason,
|
||||||
type ToolCall,
|
type ToolCall,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Create stream class matching ProxyMessageEventStream
|
// Create stream class matching ProxyMessageEventStream
|
||||||
class ProxyMessageEventStream extends EventStream<
|
class ProxyMessageEventStream extends EventStream<
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
TextContent,
|
TextContent,
|
||||||
Tool,
|
Tool,
|
||||||
ToolResultMessage,
|
ToolResultMessage,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
import type { Static, TSchema } from "@sinclair/typebox";
|
import type { Static, TSchema } from "@sinclair/typebox";
|
||||||
|
|
||||||
/** Stream function - can return sync or Promise for async config lookup */
|
/** Stream function - can return sync or Promise for async config lookup */
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
type Message,
|
type Message,
|
||||||
type Model,
|
type Model,
|
||||||
type UserMessage,
|
type UserMessage,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { agentLoop, agentLoopContinue } from "../src/agent-loop.js";
|
import { agentLoop, agentLoopContinue } from "../src/agent-loop.js";
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
type AssistantMessageEvent,
|
type AssistantMessageEvent,
|
||||||
EventStream,
|
EventStream,
|
||||||
getModel,
|
getModel,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { Agent } from "../src/index.js";
|
import { Agent } from "../src/index.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
*
|
*
|
||||||
* You can run this test suite with:
|
* You can run this test suite with:
|
||||||
* ```bash
|
* ```bash
|
||||||
* $ AWS_REGION=us-east-1 BEDROCK_EXTENSIVE_MODEL_TEST=1 AWS_PROFILE=companion npm test -- ./test/bedrock-models.test.ts
|
* $ AWS_REGION=us-east-1 BEDROCK_EXTENSIVE_MODEL_TEST=1 AWS_PROFILE=clanker npm test -- ./test/bedrock-models.test.ts
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* ## Known Issues by Category
|
* ## Known Issues by Category
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
* 5. **Invalid Signature Format**: Model validates signature format (Anthropic newer models).
|
* 5. **Invalid Signature Format**: Model validates signature format (Anthropic newer models).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AssistantMessage } from "@mariozechner/companion-ai";
|
import type { AssistantMessage } from "@mariozechner/clanker-ai";
|
||||||
import { getModels } from "@mariozechner/companion-ai";
|
import { getModels } from "@mariozechner/clanker-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { Agent } from "../src/index.js";
|
import { Agent } from "../src/index.js";
|
||||||
import { hasBedrockCredentials } from "./bedrock-utils.js";
|
import { hasBedrockCredentials } from "./bedrock-utils.js";
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import type {
|
||||||
Model,
|
Model,
|
||||||
ToolResultMessage,
|
ToolResultMessage,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
import { getModel } from "@mariozechner/companion-ai";
|
import { getModel } from "@mariozechner/clanker-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { Agent } from "../src/index.js";
|
import { Agent } from "../src/index.js";
|
||||||
import { hasBedrockCredentials } from "./bedrock-utils.js";
|
import { hasBedrockCredentials } from "./bedrock-utils.js";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# @mariozechner/companion-ai
|
# @mariozechner/clanker-ai
|
||||||
|
|
||||||
Unified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session.
|
Unified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session.
|
||||||
|
|
||||||
|
|
@ -72,10 +72,10 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @mariozechner/companion-ai
|
npm install @mariozechner/clanker-ai
|
||||||
```
|
```
|
||||||
|
|
||||||
TypeBox exports are re-exported from `@mariozechner/companion-ai`: `Type`, `Static`, and `TSchema`.
|
TypeBox exports are re-exported from `@mariozechner/clanker-ai`: `Type`, `Static`, and `TSchema`.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ import {
|
||||||
Context,
|
Context,
|
||||||
Tool,
|
Tool,
|
||||||
StringEnum,
|
StringEnum,
|
||||||
} from "@mariozechner/companion-ai";
|
} from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Fully typed with auto-complete support for both providers and models
|
// Fully typed with auto-complete support for both providers and models
|
||||||
const model = getModel("openai", "gpt-4o-mini");
|
const model = getModel("openai", "gpt-4o-mini");
|
||||||
|
|
@ -223,7 +223,7 @@ Tools enable LLMs to interact with external systems. This library uses TypeBox s
|
||||||
### Defining Tools
|
### Defining Tools
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Type, Tool, StringEnum } from "@mariozechner/companion-ai";
|
import { Type, Tool, StringEnum } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Define tool parameters with TypeBox
|
// Define tool parameters with TypeBox
|
||||||
const weatherTool: Tool = {
|
const weatherTool: Tool = {
|
||||||
|
|
@ -356,7 +356,7 @@ When using `agentLoop`, tool arguments are automatically validated against your
|
||||||
When implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:
|
When implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { stream, validateToolCall, Tool } from "@mariozechner/companion-ai";
|
import { stream, validateToolCall, Tool } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
const tools: Tool[] = [weatherTool, calculatorTool];
|
const tools: Tool[] = [weatherTool, calculatorTool];
|
||||||
const s = stream(model, { messages, tools });
|
const s = stream(model, { messages, tools });
|
||||||
|
|
@ -410,7 +410,7 @@ Models with vision capabilities can process images. You can check if a model sup
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { getModel, complete } from "@mariozechner/companion-ai";
|
import { getModel, complete } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
const model = getModel("openai", "gpt-4o-mini");
|
const model = getModel("openai", "gpt-4o-mini");
|
||||||
|
|
||||||
|
|
@ -449,7 +449,7 @@ Many models support thinking/reasoning capabilities where they can show their in
|
||||||
### Unified Interface (streamSimple/completeSimple)
|
### Unified Interface (streamSimple/completeSimple)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getModel, streamSimple, completeSimple } from "@mariozechner/companion-ai";
|
import { getModel, streamSimple, completeSimple } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Many models across providers support thinking/reasoning
|
// Many models across providers support thinking/reasoning
|
||||||
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||||
|
|
@ -491,7 +491,7 @@ for (const block of response.content) {
|
||||||
For fine-grained control, use the provider-specific options:
|
For fine-grained control, use the provider-specific options:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getModel, complete } from "@mariozechner/companion-ai";
|
import { getModel, complete } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// OpenAI Reasoning (o1, o3, gpt-5)
|
// OpenAI Reasoning (o1, o3, gpt-5)
|
||||||
const openaiModel = getModel("openai", "gpt-5-mini");
|
const openaiModel = getModel("openai", "gpt-5-mini");
|
||||||
|
|
@ -578,7 +578,7 @@ if (message.stopReason === "error" || message.stopReason === "aborted") {
|
||||||
The abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:
|
The abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getModel, stream } from "@mariozechner/companion-ai";
|
import { getModel, stream } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
const model = getModel("openai", "gpt-4o-mini");
|
const model = getModel("openai", "gpt-4o-mini");
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -682,7 +682,7 @@ A **provider** offers models through a specific API. For example:
|
||||||
### Querying Providers and Models
|
### Querying Providers and Models
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getProviders, getModels, getModel } from "@mariozechner/companion-ai";
|
import { getProviders, getModels, getModel } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Get all available providers
|
// Get all available providers
|
||||||
const providers = getProviders();
|
const providers = getProviders();
|
||||||
|
|
@ -708,7 +708,7 @@ console.log(`Using ${model.name} via ${model.api} API`);
|
||||||
You can create custom models for local inference servers or custom endpoints:
|
You can create custom models for local inference servers or custom endpoints:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Model, stream } from "@mariozechner/companion-ai";
|
import { Model, stream } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Example: Ollama using OpenAI-compatible API
|
// Example: Ollama using OpenAI-compatible API
|
||||||
const ollamaModel: Model<"openai-completions"> = {
|
const ollamaModel: Model<"openai-completions"> = {
|
||||||
|
|
@ -802,7 +802,7 @@ If `compat` is not set, the library falls back to URL-based detection. If `compa
|
||||||
Models are typed by their API, which keeps the model metadata accurate. Provider-specific option types are enforced when you call the provider functions directly. The generic `stream` and `complete` functions accept `StreamOptions` with additional provider fields.
|
Models are typed by their API, which keeps the model metadata accurate. Provider-specific option types are enforced when you call the provider functions directly. The generic `stream` and `complete` functions accept `StreamOptions` with additional provider fields.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { streamAnthropic, type AnthropicOptions } from "@mariozechner/companion-ai";
|
import { streamAnthropic, type AnthropicOptions } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// TypeScript knows this is an Anthropic model
|
// TypeScript knows this is an Anthropic model
|
||||||
const claude = getModel("anthropic", "claude-sonnet-4-20250514");
|
const claude = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||||
|
|
@ -831,7 +831,7 @@ When messages from one provider are sent to a different provider, the library au
|
||||||
### Example: Multi-Provider Conversation
|
### Example: Multi-Provider Conversation
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getModel, complete, Context } from "@mariozechner/companion-ai";
|
import { getModel, complete, Context } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Start with Claude
|
// Start with Claude
|
||||||
const claude = getModel("anthropic", "claude-sonnet-4-20250514");
|
const claude = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||||
|
|
@ -884,7 +884,7 @@ This enables flexible workflows where you can:
|
||||||
The `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:
|
The `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Context, getModel, complete } from "@mariozechner/companion-ai";
|
import { Context, getModel, complete } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Create and use a context
|
// Create and use a context
|
||||||
const context: Context = {
|
const context: Context = {
|
||||||
|
|
@ -922,7 +922,7 @@ const continuation = await complete(newModel, restored);
|
||||||
The library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers:
|
The library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getModel, complete } from "@mariozechner/companion-ai";
|
import { getModel, complete } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// API key must be passed explicitly in browser
|
// API key must be passed explicitly in browser
|
||||||
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
||||||
|
|
@ -943,7 +943,7 @@ const response = await complete(
|
||||||
### Browser Compatibility Notes
|
### Browser Compatibility Notes
|
||||||
|
|
||||||
- Amazon Bedrock (`bedrock-converse-stream`) is not supported in browser environments.
|
- Amazon Bedrock (`bedrock-converse-stream`) is not supported in browser environments.
|
||||||
- OAuth login flows are not supported in browser environments. Use the `@mariozechner/companion-ai/oauth` entry point in Node.js.
|
- OAuth login flows are not supported in browser environments. Use the `@mariozechner/clanker-ai/oauth` entry point in Node.js.
|
||||||
- In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime.
|
- In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime.
|
||||||
- Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app.
|
- Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app.
|
||||||
|
|
||||||
|
|
@ -985,17 +985,17 @@ const response = await complete(model, context, {
|
||||||
|
|
||||||
#### Antigravity Version Override
|
#### Antigravity Version Override
|
||||||
|
|
||||||
Set `COMPANION_AI_ANTIGRAVITY_VERSION` to override the Antigravity User-Agent version when Google updates their requirements:
|
Set `CLANKER_AI_ANTIGRAVITY_VERSION` to override the Antigravity User-Agent version when Google updates their requirements:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export COMPANION_AI_ANTIGRAVITY_VERSION="1.23.0"
|
export CLANKER_AI_ANTIGRAVITY_VERSION="1.23.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Cache Retention
|
#### Cache Retention
|
||||||
|
|
||||||
Set `COMPANION_CACHE_RETENTION=long` to extend prompt cache retention:
|
Set `CLANKER_CACHE_RETENTION=long` to extend prompt cache retention:
|
||||||
|
|
||||||
| Provider | Default | With `COMPANION_CACHE_RETENTION=long` |
|
| Provider | Default | With `CLANKER_CACHE_RETENTION=long` |
|
||||||
| --------- | --------- | ------------------------------ |
|
| --------- | --------- | ------------------------------ |
|
||||||
| Anthropic | 5 minutes | 1 hour |
|
| Anthropic | 5 minutes | 1 hour |
|
||||||
| OpenAI | in-memory | 24 hours |
|
| OpenAI | in-memory | 24 hours |
|
||||||
|
|
@ -1007,7 +1007,7 @@ This only affects direct API calls to `api.anthropic.com` and `api.openai.com`.
|
||||||
### Checking Environment Variables
|
### Checking Environment Variables
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getEnvApiKey } from "@mariozechner/companion-ai";
|
import { getEnvApiKey } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
// Check if an API key is set in environment variables
|
// Check if an API key is set in environment variables
|
||||||
const key = getEnvApiKey("openai"); // checks OPENAI_API_KEY
|
const key = getEnvApiKey("openai"); // checks OPENAI_API_KEY
|
||||||
|
|
@ -1047,7 +1047,7 @@ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getModel, complete } from "@mariozechner/companion-ai";
|
import { getModel, complete } from "@mariozechner/clanker-ai";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const model = getModel("google-vertex", "gemini-2.5-flash");
|
const model = getModel("google-vertex", "gemini-2.5-flash");
|
||||||
|
|
@ -1068,16 +1068,16 @@ Official docs: [Application Default Credentials](https://cloud.google.com/docs/a
|
||||||
The quickest way to authenticate:
|
The quickest way to authenticate:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx @mariozechner/companion-ai login # interactive provider selection
|
npx @mariozechner/clanker-ai login # interactive provider selection
|
||||||
npx @mariozechner/companion-ai login anthropic # login to specific provider
|
npx @mariozechner/clanker-ai login anthropic # login to specific provider
|
||||||
npx @mariozechner/companion-ai list # list available providers
|
npx @mariozechner/clanker-ai list # list available providers
|
||||||
```
|
```
|
||||||
|
|
||||||
Credentials are saved to `auth.json` in the current directory.
|
Credentials are saved to `auth.json` in the current directory.
|
||||||
|
|
||||||
### Programmatic OAuth
|
### Programmatic OAuth
|
||||||
|
|
||||||
The library provides login and token refresh functions via the `@mariozechner/companion-ai/oauth` entry point. Credential storage is the caller's responsibility.
|
The library provides login and token refresh functions via the `@mariozechner/clanker-ai/oauth` entry point. Credential storage is the caller's responsibility.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {
|
import {
|
||||||
|
|
@ -1095,13 +1095,13 @@ import {
|
||||||
// Types
|
// Types
|
||||||
type OAuthProvider, // 'anthropic' | 'openai-codex' | 'github-copilot' | 'google-gemini-cli' | 'google-antigravity'
|
type OAuthProvider, // 'anthropic' | 'openai-codex' | 'github-copilot' | 'google-gemini-cli' | 'google-antigravity'
|
||||||
type OAuthCredentials,
|
type OAuthCredentials,
|
||||||
} from "@mariozechner/companion-ai/oauth";
|
} from "@mariozechner/clanker-ai/oauth";
|
||||||
```
|
```
|
||||||
|
|
||||||
### Login Flow Example
|
### Login Flow Example
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { loginGitHubCopilot } from "@mariozechner/companion-ai/oauth";
|
import { loginGitHubCopilot } from "@mariozechner/clanker-ai/oauth";
|
||||||
import { writeFileSync } from "fs";
|
import { writeFileSync } from "fs";
|
||||||
|
|
||||||
const credentials = await loginGitHubCopilot({
|
const credentials = await loginGitHubCopilot({
|
||||||
|
|
@ -1125,8 +1125,8 @@ writeFileSync("auth.json", JSON.stringify(auth, null, 2));
|
||||||
Use `getOAuthApiKey()` to get an API key, automatically refreshing if expired:
|
Use `getOAuthApiKey()` to get an API key, automatically refreshing if expired:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getModel, complete } from "@mariozechner/companion-ai";
|
import { getModel, complete } from "@mariozechner/clanker-ai";
|
||||||
import { getOAuthApiKey } from "@mariozechner/companion-ai/oauth";
|
import { getOAuthApiKey } from "@mariozechner/clanker-ai/oauth";
|
||||||
import { readFileSync, writeFileSync } from "fs";
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
// Load your stored credentials
|
// Load your stored credentials
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/companion-ai",
|
"name": "@harivansh-afk/clanker-ai",
|
||||||
"version": "0.56.2",
|
"version": "0.56.2",
|
||||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"companion-ai": "./dist/cli.js"
|
"clanker-ai": "./dist/cli.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/getcompanion-ai/co-mono.git",
|
"url": "git+https://github.com/harivansh-afk/clanker-agent.git",
|
||||||
"directory": "packages/ai"
|
"directory": "packages/ai"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ async function main(): Promise<void> {
|
||||||
const providerList = PROVIDERS.map(
|
const providerList = PROVIDERS.map(
|
||||||
(p) => ` ${p.id.padEnd(20)} ${p.name}`,
|
(p) => ` ${p.id.padEnd(20)} ${p.name}`,
|
||||||
).join("\n");
|
).join("\n");
|
||||||
console.log(`Usage: npx @mariozechner/companion-ai <command> [provider]
|
console.log(`Usage: npx @mariozechner/clanker-ai <command> [provider]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
login [provider] Login to an OAuth provider
|
login [provider] Login to an OAuth provider
|
||||||
|
|
@ -88,9 +88,9 @@ Providers:
|
||||||
${providerList}
|
${providerList}
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
npx @mariozechner/companion-ai login # interactive provider selection
|
npx @mariozechner/clanker-ai login # interactive provider selection
|
||||||
npx @mariozechner/companion-ai login anthropic # login to specific provider
|
npx @mariozechner/clanker-ai login anthropic # login to specific provider
|
||||||
npx @mariozechner/companion-ai list # list providers
|
npx @mariozechner/clanker-ai list # list providers
|
||||||
`);
|
`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +131,7 @@ Examples:
|
||||||
if (!PROVIDERS.some((p) => p.id === provider)) {
|
if (!PROVIDERS.some((p) => p.id === provider)) {
|
||||||
console.error(`Unknown provider: ${provider}`);
|
console.error(`Unknown provider: ${provider}`);
|
||||||
console.error(
|
console.error(
|
||||||
`Use 'npx @mariozechner/companion-ai list' to see available providers`,
|
`Use 'npx @mariozechner/clanker-ai list' to see available providers`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`Unknown command: ${command}`);
|
console.error(`Unknown command: ${command}`);
|
||||||
console.error(`Use 'npx @mariozechner/companion-ai --help' for usage`);
|
console.error(`Use 'npx @mariozechner/clanker-ai --help' for usage`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -514,7 +514,7 @@ function mapThinkingLevelToEffort(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve cache retention preference.
|
* Resolve cache retention preference.
|
||||||
* Defaults to "short" and uses COMPANION_CACHE_RETENTION for backward compatibility.
|
* Defaults to "short" and uses CLANKER_CACHE_RETENTION for backward compatibility.
|
||||||
*/
|
*/
|
||||||
function resolveCacheRetention(
|
function resolveCacheRetention(
|
||||||
cacheRetention?: CacheRetention,
|
cacheRetention?: CacheRetention,
|
||||||
|
|
@ -524,7 +524,7 @@ function resolveCacheRetention(
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
typeof process !== "undefined" &&
|
typeof process !== "undefined" &&
|
||||||
process.env.COMPANION_CACHE_RETENTION === "long"
|
process.env.CLANKER_CACHE_RETENTION === "long"
|
||||||
) {
|
) {
|
||||||
return "long";
|
return "long";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve cache retention preference.
|
* Resolve cache retention preference.
|
||||||
* Defaults to "short" and uses COMPANION_CACHE_RETENTION for backward compatibility.
|
* Defaults to "short" and uses CLANKER_CACHE_RETENTION for backward compatibility.
|
||||||
*/
|
*/
|
||||||
function resolveCacheRetention(
|
function resolveCacheRetention(
|
||||||
cacheRetention?: CacheRetention,
|
cacheRetention?: CacheRetention,
|
||||||
|
|
@ -50,7 +50,7 @@ function resolveCacheRetention(
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
typeof process !== "undefined" &&
|
typeof process !== "undefined" &&
|
||||||
process.env.COMPANION_CACHE_RETENTION === "long"
|
process.env.CLANKER_CACHE_RETENTION === "long"
|
||||||
) {
|
) {
|
||||||
return "long";
|
return "long";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ const DEFAULT_ANTIGRAVITY_VERSION = "1.18.3";
|
||||||
|
|
||||||
function getAntigravityHeaders() {
|
function getAntigravityHeaders() {
|
||||||
const version =
|
const version =
|
||||||
process.env.COMPANION_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION;
|
process.env.CLANKER_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION;
|
||||||
return {
|
return {
|
||||||
"User-Agent": `antigravity/${version} darwin/arm64`,
|
"User-Agent": `antigravity/${version} darwin/arm64`,
|
||||||
};
|
};
|
||||||
|
|
@ -1040,8 +1040,8 @@ export function buildRequest(
|
||||||
model: model.id,
|
model: model.id,
|
||||||
request,
|
request,
|
||||||
...(isAntigravity ? { requestType: "agent" } : {}),
|
...(isAntigravity ? { requestType: "agent" } : {}),
|
||||||
userAgent: isAntigravity ? "antigravity" : "companion-coding-agent",
|
userAgent: isAntigravity ? "antigravity" : "clanker-coding-agent",
|
||||||
requestId: `${isAntigravity ? "agent" : "companion"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
requestId: `${isAntigravity ? "agent" : "clanker"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -997,10 +997,10 @@ function buildHeaders(
|
||||||
headers.set("Authorization", `Bearer ${token}`);
|
headers.set("Authorization", `Bearer ${token}`);
|
||||||
headers.set("chatgpt-account-id", accountId);
|
headers.set("chatgpt-account-id", accountId);
|
||||||
headers.set("OpenAI-Beta", "responses=experimental");
|
headers.set("OpenAI-Beta", "responses=experimental");
|
||||||
headers.set("originator", "companion");
|
headers.set("originator", "clanker");
|
||||||
const userAgent = _os
|
const userAgent = _os
|
||||||
? `companion (${_os.platform()} ${_os.release()}; ${_os.arch()})`
|
? `clanker (${_os.platform()} ${_os.release()}; ${_os.arch()})`
|
||||||
: "companion (browser)";
|
: "clanker (browser)";
|
||||||
headers.set("User-Agent", userAgent);
|
headers.set("User-Agent", userAgent);
|
||||||
headers.set("accept", "text/event-stream");
|
headers.set("accept", "text/event-stream");
|
||||||
headers.set("content-type", "application/json");
|
headers.set("content-type", "application/json");
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const OPENAI_TOOL_CALL_PROVIDERS = new Set([
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve cache retention preference.
|
* Resolve cache retention preference.
|
||||||
* Defaults to "short" and uses COMPANION_CACHE_RETENTION for backward compatibility.
|
* Defaults to "short" and uses CLANKER_CACHE_RETENTION for backward compatibility.
|
||||||
*/
|
*/
|
||||||
function resolveCacheRetention(
|
function resolveCacheRetention(
|
||||||
cacheRetention?: CacheRetention,
|
cacheRetention?: CacheRetention,
|
||||||
|
|
@ -43,7 +43,7 @@ function resolveCacheRetention(
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
typeof process !== "undefined" &&
|
typeof process !== "undefined" &&
|
||||||
process.env.COMPANION_CACHE_RETENTION === "long"
|
process.env.CLANKER_CACHE_RETENTION === "long"
|
||||||
) {
|
) {
|
||||||
return "long";
|
return "long";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,7 @@ export interface OpenAICompletionsCompat {
|
||||||
supportsDeveloperRole?: boolean;
|
supportsDeveloperRole?: boolean;
|
||||||
/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */
|
/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */
|
||||||
supportsReasoningEffort?: boolean;
|
supportsReasoningEffort?: boolean;
|
||||||
/** Optional mapping from companion-ai reasoning levels to provider/model-specific `reasoning_effort` values. */
|
/** Optional mapping from clanker-ai reasoning levels to provider/model-specific `reasoning_effort` values. */
|
||||||
reasoningEffortMap?: Partial<Record<ThinkingLevel, string>>;
|
reasoningEffortMap?: Partial<Record<ThinkingLevel, string>>;
|
||||||
/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
|
/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
|
||||||
supportsUsageInStreaming?: boolean;
|
supportsUsageInStreaming?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createAuthorizationFlow(
|
async function createAuthorizationFlow(
|
||||||
originator: string = "companion",
|
originator: string = "clanker",
|
||||||
): Promise<{ verifier: string; state: string; url: string }> {
|
): Promise<{ verifier: string; state: string; url: string }> {
|
||||||
const { verifier, challenge } = await generatePKCE();
|
const { verifier, challenge } = await generatePKCE();
|
||||||
const state = createState();
|
const state = createState();
|
||||||
|
|
@ -337,7 +337,7 @@ function getAccountId(accessToken: string): string | null {
|
||||||
* @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.
|
* @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.
|
||||||
* Races with browser callback - whichever completes first wins.
|
* Races with browser callback - whichever completes first wins.
|
||||||
* Useful for showing paste input immediately alongside browser flow.
|
* Useful for showing paste input immediately alongside browser flow.
|
||||||
* @param options.originator - OAuth originator parameter (defaults to "companion")
|
* @param options.originator - OAuth originator parameter (defaults to "clanker")
|
||||||
*/
|
*/
|
||||||
export async function loginOpenAICodex(options: {
|
export async function loginOpenAICodex(options: {
|
||||||
onAuth: (info: { url: string; instructions?: string }) => void;
|
onAuth: (info: { url: string; instructions?: string }) => void;
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ describe.skipIf(!oauthToken)("Anthropic OAuth tool name normalization", () => {
|
||||||
expect(toolCallName).toBe("todowrite");
|
expect(toolCallName).toBe("todowrite");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle companion's built-in tools (read, write, edit, bash)", async () => {
|
it("should handle clanker's built-in tools (read, write, edit, bash)", async () => {
|
||||||
// Companion's tools use lowercase names, CC uses PascalCase
|
// Clanker's tools use lowercase names, CC uses PascalCase
|
||||||
const readTool: Tool = {
|
const readTool: Tool = {
|
||||||
name: "read",
|
name: "read",
|
||||||
description: "Read a file",
|
description: "Read a file",
|
||||||
|
|
@ -116,7 +116,7 @@ describe.skipIf(!oauthToken)("Anthropic OAuth tool name normalization", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should NOT map find to Glob - find is not a CC tool name", async () => {
|
it("should NOT map find to Glob - find is not a CC tool name", async () => {
|
||||||
// Companion has a "find" tool, CC has "Glob" - these are DIFFERENT tools
|
// Clanker has a "find" tool, CC has "Glob" - these are DIFFERENT tools
|
||||||
// The old code incorrectly mapped find -> Glob, which broke the round-trip
|
// The old code incorrectly mapped find -> Glob, which broke the round-trip
|
||||||
// because there's no tool named "glob" in context.tools
|
// because there's no tool named "glob" in context.tools
|
||||||
const findTool: Tool = {
|
const findTool: Tool = {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@ import { getModel } from "../src/models.js";
|
||||||
import { stream } from "../src/stream.js";
|
import { stream } from "../src/stream.js";
|
||||||
import type { Context } from "../src/types.js";
|
import type { Context } from "../src/types.js";
|
||||||
|
|
||||||
describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
|
describe("Cache Retention (CLANKER_CACHE_RETENTION)", () => {
|
||||||
const originalEnv = process.env.COMPANION_CACHE_RETENTION;
|
const originalEnv = process.env.CLANKER_CACHE_RETENTION;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete process.env.COMPANION_CACHE_RETENTION;
|
delete process.env.CLANKER_CACHE_RETENTION;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (originalEnv !== undefined) {
|
if (originalEnv !== undefined) {
|
||||||
process.env.COMPANION_CACHE_RETENTION = originalEnv;
|
process.env.CLANKER_CACHE_RETENTION = originalEnv;
|
||||||
} else {
|
} else {
|
||||||
delete process.env.COMPANION_CACHE_RETENTION;
|
delete process.env.CLANKER_CACHE_RETENTION;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
|
||||||
|
|
||||||
describe("Anthropic Provider", () => {
|
describe("Anthropic Provider", () => {
|
||||||
it.skipIf(!process.env.ANTHROPIC_API_KEY)(
|
it.skipIf(!process.env.ANTHROPIC_API_KEY)(
|
||||||
"should use default cache TTL (no ttl field) when COMPANION_CACHE_RETENTION is not set",
|
"should use default cache TTL (no ttl field) when CLANKER_CACHE_RETENTION is not set",
|
||||||
async () => {
|
async () => {
|
||||||
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
||||||
let capturedPayload: any = null;
|
let capturedPayload: any = null;
|
||||||
|
|
@ -51,9 +51,9 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it.skipIf(!process.env.ANTHROPIC_API_KEY)(
|
it.skipIf(!process.env.ANTHROPIC_API_KEY)(
|
||||||
"should use 1h cache TTL when COMPANION_CACHE_RETENTION=long",
|
"should use 1h cache TTL when CLANKER_CACHE_RETENTION=long",
|
||||||
async () => {
|
async () => {
|
||||||
process.env.COMPANION_CACHE_RETENTION = "long";
|
process.env.CLANKER_CACHE_RETENTION = "long";
|
||||||
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
|
||||||
let capturedPayload: any = null;
|
let capturedPayload: any = null;
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it("should not add ttl when baseUrl is not api.anthropic.com", async () => {
|
it("should not add ttl when baseUrl is not api.anthropic.com", async () => {
|
||||||
process.env.COMPANION_CACHE_RETENTION = "long";
|
process.env.CLANKER_CACHE_RETENTION = "long";
|
||||||
|
|
||||||
// Create a model with a different baseUrl (simulating a proxy)
|
// Create a model with a different baseUrl (simulating a proxy)
|
||||||
const baseModel = getModel("anthropic", "claude-3-5-haiku-20241022");
|
const baseModel = getModel("anthropic", "claude-3-5-haiku-20241022");
|
||||||
|
|
@ -210,7 +210,7 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
|
||||||
|
|
||||||
describe("OpenAI Responses Provider", () => {
|
describe("OpenAI Responses Provider", () => {
|
||||||
it.skipIf(!process.env.OPENAI_API_KEY)(
|
it.skipIf(!process.env.OPENAI_API_KEY)(
|
||||||
"should not set prompt_cache_retention when COMPANION_CACHE_RETENTION is not set",
|
"should not set prompt_cache_retention when CLANKER_CACHE_RETENTION is not set",
|
||||||
async () => {
|
async () => {
|
||||||
const model = getModel("openai", "gpt-4o-mini");
|
const model = getModel("openai", "gpt-4o-mini");
|
||||||
let capturedPayload: any = null;
|
let capturedPayload: any = null;
|
||||||
|
|
@ -232,9 +232,9 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it.skipIf(!process.env.OPENAI_API_KEY)(
|
it.skipIf(!process.env.OPENAI_API_KEY)(
|
||||||
"should set prompt_cache_retention to 24h when COMPANION_CACHE_RETENTION=long",
|
"should set prompt_cache_retention to 24h when CLANKER_CACHE_RETENTION=long",
|
||||||
async () => {
|
async () => {
|
||||||
process.env.COMPANION_CACHE_RETENTION = "long";
|
process.env.CLANKER_CACHE_RETENTION = "long";
|
||||||
const model = getModel("openai", "gpt-4o-mini");
|
const model = getModel("openai", "gpt-4o-mini");
|
||||||
let capturedPayload: any = null;
|
let capturedPayload: any = null;
|
||||||
|
|
||||||
|
|
@ -255,7 +255,7 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it("should not set prompt_cache_retention when baseUrl is not api.openai.com", async () => {
|
it("should not set prompt_cache_retention when baseUrl is not api.openai.com", async () => {
|
||||||
process.env.COMPANION_CACHE_RETENTION = "long";
|
process.env.CLANKER_CACHE_RETENTION = "long";
|
||||||
|
|
||||||
// Create a model with a different baseUrl (simulating a proxy)
|
// Create a model with a different baseUrl (simulating a proxy)
|
||||||
const baseModel = getModel("openai", "gpt-4o-mini");
|
const baseModel = getModel("openai", "gpt-4o-mini");
|
||||||
|
|
|
||||||
|
|
@ -683,7 +683,7 @@ describe("Context overflow error handling", () => {
|
||||||
|
|
||||||
// Check if ollama is installed and local LLM tests are enabled
|
// Check if ollama is installed and local LLM tests are enabled
|
||||||
let ollamaInstalled = false;
|
let ollamaInstalled = false;
|
||||||
if (!process.env.COMPANION_NO_LOCAL_LLM) {
|
if (!process.env.CLANKER_NO_LOCAL_LLM) {
|
||||||
try {
|
try {
|
||||||
execSync("which ollama", { stdio: "ignore" });
|
execSync("which ollama", { stdio: "ignore" });
|
||||||
ollamaInstalled = true;
|
ollamaInstalled = true;
|
||||||
|
|
@ -785,7 +785,7 @@ describe("Context overflow error handling", () => {
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
let lmStudioRunning = false;
|
let lmStudioRunning = false;
|
||||||
if (!process.env.COMPANION_NO_LOCAL_LLM) {
|
if (!process.env.CLANKER_NO_LOCAL_LLM) {
|
||||||
try {
|
try {
|
||||||
execSync(
|
execSync(
|
||||||
"curl -s --max-time 1 http://localhost:1234/v1/models > /dev/null",
|
"curl -s --max-time 1 http://localhost:1234/v1/models > /dev/null",
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ function dumpFailurePayload(params: {
|
||||||
payload?: unknown;
|
payload?: unknown;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
}): void {
|
}): void {
|
||||||
const filename = `/tmp/companion-handoff-${params.label}-${Date.now()}.json`;
|
const filename = `/tmp/clanker-handoff-${params.label}-${Date.now()}.json`;
|
||||||
const body = {
|
const body = {
|
||||||
label: params.label,
|
label: params.label,
|
||||||
error: params.error,
|
error: params.error,
|
||||||
|
|
|
||||||
|
|
@ -765,7 +765,7 @@ describe("AI Providers Empty Message Tests", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// OAuth-based providers (credentials from ~/.companion/agent/oauth.json)
|
// OAuth-based providers (credentials from ~/.clanker/agent/oauth.json)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
describe("Anthropic OAuth Provider Empty Messages", () => {
|
describe("Anthropic OAuth Provider Empty Messages", () => {
|
||||||
|
|
|
||||||
|
|
@ -476,7 +476,7 @@ describe("Tool Results with Images", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// OAuth-based providers (credentials from ~/.companion/agent/oauth.json)
|
// OAuth-based providers (credentials from ~/.clanker/agent/oauth.json)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
describe("Anthropic OAuth Provider (claude-sonnet-4-5)", () => {
|
describe("Anthropic OAuth Provider (claude-sonnet-4-5)", () => {
|
||||||
|
|
@ -584,7 +584,7 @@ describe("Tool Results with Images", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/** These two don't work, the model simply won't call the tool, works in companion
|
/** These two don't work, the model simply won't call the tool, works in clanker
|
||||||
it.skipIf(!antigravityToken)(
|
it.skipIf(!antigravityToken)(
|
||||||
"claude-sonnet-4-5 - should handle tool result with only image",
|
"claude-sonnet-4-5 - should handle tool result with only image",
|
||||||
{ retry: 3, timeout: 30000 },
|
{ retry: 3, timeout: 30000 },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Test helper for resolving API keys from ~/.companion/agent/auth.json
|
* Test helper for resolving API keys from ~/.clanker/agent/auth.json
|
||||||
*
|
*
|
||||||
* Supports both API key and OAuth credentials.
|
* Supports both API key and OAuth credentials.
|
||||||
* OAuth tokens are automatically refreshed if expired and saved back to auth.json.
|
* OAuth tokens are automatically refreshed if expired and saved back to auth.json.
|
||||||
|
|
@ -20,7 +20,7 @@ import type {
|
||||||
OAuthProvider,
|
OAuthProvider,
|
||||||
} from "../src/utils/oauth/types.js";
|
} from "../src/utils/oauth/types.js";
|
||||||
|
|
||||||
const AUTH_PATH = join(homedir(), ".companion", "agent", "auth.json");
|
const AUTH_PATH = join(homedir(), ".clanker", "agent", "auth.json");
|
||||||
|
|
||||||
type ApiKeyCredential = {
|
type ApiKeyCredential = {
|
||||||
type: "api_key";
|
type: "api_key";
|
||||||
|
|
@ -57,7 +57,7 @@ function saveAuthStorage(storage: AuthStorage): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve API key for a provider from ~/.companion/agent/auth.json
|
* Resolve API key for a provider from ~/.clanker/agent/auth.json
|
||||||
*
|
*
|
||||||
* For API key credentials, returns the key directly.
|
* For API key credentials, returns the key directly.
|
||||||
* For OAuth credentials, returns the access token (refreshing if expired and saving back).
|
* For OAuth credentials, returns the access token (refreshing if expired and saving back).
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,22 @@ import { streamOpenAICodexResponses } from "../src/providers/openai-codex-respon
|
||||||
import type { Context, Model } from "../src/types.js";
|
import type { Context, Model } from "../src/types.js";
|
||||||
|
|
||||||
const originalFetch = global.fetch;
|
const originalFetch = global.fetch;
|
||||||
const originalAgentDir = process.env.COMPANION_CODING_AGENT_DIR;
|
const originalAgentDir = process.env.CLANKER_CODING_AGENT_DIR;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
global.fetch = originalFetch;
|
global.fetch = originalFetch;
|
||||||
if (originalAgentDir === undefined) {
|
if (originalAgentDir === undefined) {
|
||||||
delete process.env.COMPANION_CODING_AGENT_DIR;
|
delete process.env.CLANKER_CODING_AGENT_DIR;
|
||||||
} else {
|
} else {
|
||||||
process.env.COMPANION_CODING_AGENT_DIR = originalAgentDir;
|
process.env.CLANKER_CODING_AGENT_DIR = originalAgentDir;
|
||||||
}
|
}
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("openai-codex streaming", () => {
|
describe("openai-codex streaming", () => {
|
||||||
it("streams SSE responses into AssistantMessageEventStream", async () => {
|
it("streams SSE responses into AssistantMessageEventStream", async () => {
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
|
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
|
||||||
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
|
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
|
||||||
|
|
||||||
const payload = Buffer.from(
|
const payload = Buffer.from(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
@ -95,7 +95,7 @@ describe("openai-codex streaming", () => {
|
||||||
expect(headers?.get("Authorization")).toBe(`Bearer ${token}`);
|
expect(headers?.get("Authorization")).toBe(`Bearer ${token}`);
|
||||||
expect(headers?.get("chatgpt-account-id")).toBe("acc_test");
|
expect(headers?.get("chatgpt-account-id")).toBe("acc_test");
|
||||||
expect(headers?.get("OpenAI-Beta")).toBe("responses=experimental");
|
expect(headers?.get("OpenAI-Beta")).toBe("responses=experimental");
|
||||||
expect(headers?.get("originator")).toBe("companion");
|
expect(headers?.get("originator")).toBe("clanker");
|
||||||
expect(headers?.get("accept")).toBe("text/event-stream");
|
expect(headers?.get("accept")).toBe("text/event-stream");
|
||||||
expect(headers?.has("x-api-key")).toBe(false);
|
expect(headers?.has("x-api-key")).toBe(false);
|
||||||
return new Response(stream, {
|
return new Response(stream, {
|
||||||
|
|
@ -149,8 +149,8 @@ describe("openai-codex streaming", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets conversation_id/session_id headers and prompt_cache_key when sessionId is provided", async () => {
|
it("sets conversation_id/session_id headers and prompt_cache_key when sessionId is provided", async () => {
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
|
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
|
||||||
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
|
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
|
||||||
|
|
||||||
const payload = Buffer.from(
|
const payload = Buffer.from(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
@ -272,8 +272,8 @@ describe("openai-codex streaming", () => {
|
||||||
it.each(["gpt-5.3-codex", "gpt-5.4"])(
|
it.each(["gpt-5.3-codex", "gpt-5.4"])(
|
||||||
"clamps %s minimal reasoning effort to low",
|
"clamps %s minimal reasoning effort to low",
|
||||||
async (modelId) => {
|
async (modelId) => {
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
|
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
|
||||||
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
|
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
|
||||||
|
|
||||||
const payload = Buffer.from(
|
const payload = Buffer.from(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
@ -393,8 +393,8 @@ describe("openai-codex streaming", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it("does not set conversation_id/session_id headers when sessionId is not provided", async () => {
|
it("does not set conversation_id/session_id headers when sessionId is not provided", async () => {
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
|
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
|
||||||
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
|
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
|
||||||
|
|
||||||
const payload = Buffer.from(
|
const payload = Buffer.from(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -1048,7 +1048,7 @@ describe("Generate E2E Tests", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// OAuth-based providers (credentials from ~/.companion/agent/oauth.json)
|
// OAuth-based providers (credentials from ~/.clanker/agent/oauth.json)
|
||||||
// Tokens are resolved at module level (see oauthTokens above)
|
// Tokens are resolved at module level (see oauthTokens above)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
|
@ -1800,7 +1800,7 @@ describe("Generate E2E Tests", () => {
|
||||||
|
|
||||||
// Check if ollama is installed and local LLM tests are enabled
|
// Check if ollama is installed and local LLM tests are enabled
|
||||||
let ollamaInstalled = false;
|
let ollamaInstalled = false;
|
||||||
if (!process.env.COMPANION_NO_LOCAL_LLM) {
|
if (!process.env.CLANKER_NO_LOCAL_LLM) {
|
||||||
try {
|
try {
|
||||||
execSync("which ollama", { stdio: "ignore" });
|
execSync("which ollama", { stdio: "ignore" });
|
||||||
ollamaInstalled = true;
|
ollamaInstalled = true;
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ describe("Token Statistics on Abort", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// OAuth-based providers (credentials from ~/.companion/agent/oauth.json)
|
// OAuth-based providers (credentials from ~/.clanker/agent/oauth.json)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
describe("Anthropic OAuth Provider", () => {
|
describe("Anthropic OAuth Provider", () => {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* OpenAI Responses API generates IDs in format: {call_id}|{id}
|
* OpenAI Responses API generates IDs in format: {call_id}|{id}
|
||||||
* where {id} can be 400+ chars with special characters (+, /, =).
|
* where {id} can be 400+ chars with special characters (+, /, =).
|
||||||
*
|
*
|
||||||
* Regression test for: https://github.com/badlogic/companion-mono/issues/1022
|
* Regression test for: https://github.com/badlogic/clanker-mono/issues/1022
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,7 @@ describe("Tool Call Without Result Tests", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// OAuth-based providers (credentials from ~/.companion/agent/oauth.json)
|
// OAuth-based providers (credentials from ~/.clanker/agent/oauth.json)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
describe("Anthropic OAuth Provider", () => {
|
describe("Anthropic OAuth Provider", () => {
|
||||||
|
|
|
||||||
|
|
@ -472,7 +472,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// OAuth-based providers (credentials from ~/.companion/agent/oauth.json)
|
// OAuth-based providers (credentials from ~/.clanker/agent/oauth.json)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
describe("Anthropic OAuth Provider Unicode Handling", () => {
|
describe("Anthropic OAuth Provider Unicode Handling", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# @e9n/companion-channels
|
# @e9n/clanker-channels
|
||||||
|
|
||||||
Two-way channel extension for [companion](https://github.com/espennilsen/companion) — route messages between agents and Telegram, Slack, webhooks, or custom adapters.
|
Two-way channel extension for [clanker](https://github.com/espennilsen/clanker) — route messages between agents and Telegram, Slack, webhooks, or custom adapters.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
@ -13,11 +13,11 @@ Two-way channel extension for [companion](https://github.com/espennilsen/compani
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Add to `~/.companion/agent/settings.json` or `.companion/settings.json`:
|
Add to `~/.clanker/agent/settings.json` or `.clanker/settings.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"companion-channels": {
|
"clanker-channels": {
|
||||||
"adapters": {
|
"adapters": {
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"type": "telegram",
|
"type": "telegram",
|
||||||
|
|
@ -81,7 +81,7 @@ Use `"env:VAR_NAME"` to reference environment variables. Project settings overri
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
companion install npm:@e9n/companion-channels
|
clanker install npm:@e9n/clanker-channels
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "@e9n/companion-channels",
|
"name": "@harivansh-afk/clanker-channels",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-way channel extension for companion — route messages between agents and Telegram, webhooks, and custom adapters",
|
"description": "Two-way channel extension for clanker - route messages between agents and Telegram, webhooks, and custom adapters",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"companion-package"
|
"clanker-package"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Espen Nilsen <hi@e9n.dev>",
|
"author": "Espen Nilsen <hi@e9n.dev>",
|
||||||
"companion": {
|
"clanker": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"./src/index.ts"
|
"./src/index.ts"
|
||||||
]
|
]
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mariozechner/companion-ai": "*",
|
"@harivansh-afk/clanker-ai": "*",
|
||||||
"@mariozechner/companion-coding-agent": "*",
|
"@harivansh-afk/clanker-coding-agent": "*",
|
||||||
"@sinclair/typebox": "*"
|
"@sinclair/typebox": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/espennilsen/companion.git",
|
"url": "git+https://github.com/harivansh-afk/clanker-agent.git",
|
||||||
"directory": "extensions/companion-channels"
|
"directory": "packages/clanker-channels"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Built-in Slack adapter (bidirectional).
|
* clanker-channels — Built-in Slack adapter (bidirectional).
|
||||||
*
|
*
|
||||||
* Outgoing: Slack Web API chat.postMessage.
|
* Outgoing: Slack Web API chat.postMessage.
|
||||||
* Incoming: Socket Mode (WebSocket) for events + slash commands.
|
* Incoming: Socket Mode (WebSocket) for events + slash commands.
|
||||||
|
|
@ -14,13 +14,13 @@
|
||||||
* - Channel allowlisting (optional)
|
* - Channel allowlisting (optional)
|
||||||
*
|
*
|
||||||
* Requires:
|
* Requires:
|
||||||
* - App-level token (xapp-...) for Socket Mode — in settings under companion-channels.slack.appToken
|
* - App-level token (xapp-...) for Socket Mode — in settings under clanker-channels.slack.appToken
|
||||||
* - Bot token (xoxb-...) for Web API — in settings under companion-channels.slack.botToken
|
* - Bot token (xoxb-...) for Web API — in settings under clanker-channels.slack.botToken
|
||||||
* - Socket Mode enabled in app settings
|
* - Socket Mode enabled in app settings
|
||||||
*
|
*
|
||||||
* Config in ~/.companion/agent/settings.json:
|
* Config in ~/.clanker/agent/settings.json:
|
||||||
* {
|
* {
|
||||||
* "companion-channels": {
|
* "clanker-channels": {
|
||||||
* "adapters": {
|
* "adapters": {
|
||||||
* "slack": {
|
* "slack": {
|
||||||
* "type": "slack",
|
* "type": "slack",
|
||||||
|
|
@ -95,7 +95,7 @@ export function createSlackAdapter(
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
log?: SlackAdapterLogger,
|
log?: SlackAdapterLogger,
|
||||||
): ChannelAdapter {
|
): ChannelAdapter {
|
||||||
// Tokens live in settings under companion-channels.slack (not in the adapter config block)
|
// Tokens live in settings under clanker-channels.slack (not in the adapter config block)
|
||||||
const appToken =
|
const appToken =
|
||||||
(cwd ? (getChannelSetting(cwd, "slack.appToken") as string) : null) ??
|
(cwd ? (getChannelSetting(cwd, "slack.appToken") as string) : null) ??
|
||||||
(config.appToken as string);
|
(config.appToken as string);
|
||||||
|
|
@ -109,11 +109,11 @@ export function createSlackAdapter(
|
||||||
|
|
||||||
if (!appToken)
|
if (!appToken)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Slack adapter requires appToken (xapp-...) in settings under companion-channels.slack.appToken",
|
"Slack adapter requires appToken (xapp-...) in settings under clanker-channels.slack.appToken",
|
||||||
);
|
);
|
||||||
if (!botToken)
|
if (!botToken)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Slack adapter requires botToken (xoxb-...) in settings under companion-channels.slack.botToken",
|
"Slack adapter requires botToken (xoxb-...) in settings under clanker-channels.slack.botToken",
|
||||||
);
|
);
|
||||||
|
|
||||||
let socketClient: SocketModeClient | null = null;
|
let socketClient: SocketModeClient | null = null;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Built-in Telegram adapter (bidirectional).
|
* clanker-channels — Built-in Telegram adapter (bidirectional).
|
||||||
*
|
*
|
||||||
* Outgoing: Telegram Bot API sendMessage.
|
* Outgoing: Telegram Bot API sendMessage.
|
||||||
* Incoming: Long-polling via getUpdates.
|
* Incoming: Long-polling via getUpdates.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* - File size validation (1MB for docs/photos, 10MB for voice/audio)
|
* - File size validation (1MB for docs/photos, 10MB for voice/audio)
|
||||||
* - MIME type filtering (text-like files only for documents)
|
* - MIME type filtering (text-like files only for documents)
|
||||||
*
|
*
|
||||||
* Config (in settings.json under companion-channels.adapters.telegram):
|
* Config (in settings.json under clanker-channels.adapters.telegram):
|
||||||
* {
|
* {
|
||||||
* "type": "telegram",
|
* "type": "telegram",
|
||||||
* "botToken": "your-telegram-bot-token",
|
* "botToken": "your-telegram-bot-token",
|
||||||
|
|
@ -173,7 +173,7 @@ export function createTelegramAdapter(config: AdapterConfig): ChannelAdapter {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
transcriberError = err.message ?? "Unknown transcription config error";
|
transcriberError = err.message ?? "Unknown transcription config error";
|
||||||
console.error(
|
console.error(
|
||||||
`[companion-channels] Transcription config error: ${transcriberError}`,
|
`[clanker-channels] Transcription config error: ${transcriberError}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +259,7 @@ export function createTelegramAdapter(config: AdapterConfig): ChannelAdapter {
|
||||||
path.extname(info.result.file_path) ||
|
path.extname(info.result.file_path) ||
|
||||||
path.extname(suggestedName || "") ||
|
path.extname(suggestedName || "") ||
|
||||||
"";
|
"";
|
||||||
const tmpDir = path.join(os.tmpdir(), "companion-channels");
|
const tmpDir = path.join(os.tmpdir(), "clanker-channels");
|
||||||
fs.mkdirSync(tmpDir, { recursive: true });
|
fs.mkdirSync(tmpDir, { recursive: true });
|
||||||
const localPath = path.join(
|
const localPath = path.join(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Pluggable audio transcription.
|
* clanker-channels — Pluggable audio transcription.
|
||||||
*
|
*
|
||||||
* Supports three providers:
|
* Supports three providers:
|
||||||
* - "apple" — macOS SFSpeechRecognizer (free, offline, no API key)
|
* - "apple" — macOS SFSpeechRecognizer (free, offline, no API key)
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Built-in webhook adapter.
|
* clanker-channels — Built-in webhook adapter.
|
||||||
*
|
*
|
||||||
* POSTs message as JSON. The recipient field is the webhook URL.
|
* POSTs message as JSON. The recipient field is the webhook URL.
|
||||||
*
|
*
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Chat bridge.
|
* clanker-channels — Chat bridge.
|
||||||
*
|
*
|
||||||
* Listens for incoming messages (channel:receive), serializes per sender,
|
* Listens for incoming messages (channel:receive), serializes per sender,
|
||||||
* routes prompts into the live companion gateway runtime, and sends responses
|
* routes prompts into the live clanker gateway runtime, and sends responses
|
||||||
* back via the same adapter. Each sender gets their own FIFO queue.
|
* back via the same adapter. Each sender gets their own FIFO queue.
|
||||||
* Multiple senders run concurrently up to maxConcurrent.
|
* Multiple senders run concurrently up to maxConcurrent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import type { ImageContent } from "@mariozechner/companion-ai";
|
import type { ImageContent } from "@mariozechner/clanker-ai";
|
||||||
import {
|
import {
|
||||||
type EventBus,
|
type EventBus,
|
||||||
getActiveGatewayRuntime,
|
getActiveGatewayRuntime,
|
||||||
} from "@mariozechner/companion-coding-agent";
|
} from "@mariozechner/clanker-coding-agent";
|
||||||
import type { ChannelRegistry } from "../registry.js";
|
import type { ChannelRegistry } from "../registry.js";
|
||||||
import type {
|
import type {
|
||||||
BridgeConfig,
|
BridgeConfig,
|
||||||
|
|
@ -73,7 +73,7 @@ export class ChatBridge {
|
||||||
if (!getActiveGatewayRuntime()) {
|
if (!getActiveGatewayRuntime()) {
|
||||||
this.log(
|
this.log(
|
||||||
"bridge-unavailable",
|
"bridge-unavailable",
|
||||||
{ reason: "no active companion gateway runtime" },
|
{ reason: "no active clanker gateway runtime" },
|
||||||
"WARN",
|
"WARN",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -195,7 +195,7 @@ export class ChatBridge {
|
||||||
this.sendReply(
|
this.sendReply(
|
||||||
prompt.adapter,
|
prompt.adapter,
|
||||||
prompt.sender,
|
prompt.sender,
|
||||||
"❌ companion gateway is not running.",
|
"❌ clanker gateway is not running.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Bot command handler.
|
* clanker-channels — Bot command handler.
|
||||||
*
|
*
|
||||||
* Detects messages starting with / and handles them without routing
|
* Detects messages starting with / and handles them without routing
|
||||||
* to the agent. Provides built-in commands and a registry for custom ones.
|
* to the agent. Provides built-in commands and a registry for custom ones.
|
||||||
|
|
@ -74,7 +74,7 @@ registerCommand({
|
||||||
name: "start",
|
name: "start",
|
||||||
description: "Welcome message",
|
description: "Welcome message",
|
||||||
handler: () =>
|
handler: () =>
|
||||||
"👋 Hi! I'm your companion assistant.\n\n" +
|
"👋 Hi! I'm your clanker assistant.\n\n" +
|
||||||
"Send me a message and I'll process it. Use /help to see available commands.",
|
"Send me a message and I'll process it. Use /help to see available commands.",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Persistent RPC session runner.
|
* clanker-channels — Persistent RPC session runner.
|
||||||
*
|
*
|
||||||
* Maintains a long-lived `companion --mode rpc` subprocess per sender,
|
* Maintains a long-lived `clanker --mode rpc` subprocess per sender,
|
||||||
* enabling persistent conversation context across messages.
|
* enabling persistent conversation context across messages.
|
||||||
* Falls back to stateless runner if RPC fails to start.
|
* Falls back to stateless runner if RPC fails to start.
|
||||||
*
|
*
|
||||||
|
|
@ -33,7 +33,7 @@ interface PendingRequest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A persistent RPC session for a single sender.
|
* A persistent RPC session for a single sender.
|
||||||
* Wraps a `companion --mode rpc` subprocess.
|
* Wraps a `clanker --mode rpc` subprocess.
|
||||||
*/
|
*/
|
||||||
export class RpcSession {
|
export class RpcSession {
|
||||||
private child: ChildProcess | null = null;
|
private child: ChildProcess | null = null;
|
||||||
|
|
@ -63,7 +63,7 @@ export class RpcSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.child = spawn("companion", args, {
|
this.child = spawn("clanker", args, {
|
||||||
cwd: this.options.cwd,
|
cwd: this.options.cwd,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
env: { ...process.env },
|
env: { ...process.env },
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Subprocess runner for the chat bridge.
|
* clanker-channels — Subprocess runner for the chat bridge.
|
||||||
*
|
*
|
||||||
* Spawns `companion -p --no-session [@files...] <prompt>` to process a single prompt.
|
* Spawns `clanker -p --no-session [@files...] <prompt>` to process a single prompt.
|
||||||
* Supports file attachments (images, documents) via the @file syntax.
|
* Supports file attachments (images, documents) via the @file syntax.
|
||||||
* Same pattern as companion-cron and companion-heartbeat.
|
* Same pattern as clanker-cron and clanker-heartbeat.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ChildProcess, spawn } from "node:child_process";
|
import { type ChildProcess, spawn } from "node:child_process";
|
||||||
|
|
@ -49,7 +49,7 @@ export function runPrompt(options: RunOptions): Promise<RunResult> {
|
||||||
|
|
||||||
let child: ChildProcess;
|
let child: ChildProcess;
|
||||||
try {
|
try {
|
||||||
child = spawn("companion", args, {
|
child = spawn("clanker", args, {
|
||||||
cwd,
|
cwd,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
env: { ...process.env },
|
env: { ...process.env },
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Typing indicator manager.
|
* clanker-channels — Typing indicator manager.
|
||||||
*
|
*
|
||||||
* Sends periodic typing chat actions via the adapter's sendTyping method.
|
* Sends periodic typing chat actions via the adapter's sendTyping method.
|
||||||
* Telegram typing indicators expire after ~5s, so we refresh every 4s.
|
* Telegram typing indicators expire after ~5s, so we refresh every 4s.
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Config from companion SettingsManager.
|
* clanker-channels — Config from clanker SettingsManager.
|
||||||
*
|
*
|
||||||
* Reads the "companion-channels" key from settings via SettingsManager,
|
* Reads the "clanker-channels" key from settings via SettingsManager,
|
||||||
* which merges global (~/.companion/agent/settings.json) and project
|
* which merges global (~/.clanker/agent/settings.json) and project
|
||||||
* (.companion/settings.json) configs automatically.
|
* (.clanker/settings.json) configs automatically.
|
||||||
*
|
*
|
||||||
* Example settings.json:
|
* Example settings.json:
|
||||||
* {
|
* {
|
||||||
* "companion-channels": {
|
* "clanker-channels": {
|
||||||
* "adapters": {
|
* "adapters": {
|
||||||
* "telegram": {
|
* "telegram": {
|
||||||
* "type": "telegram",
|
* "type": "telegram",
|
||||||
|
|
@ -28,10 +28,10 @@
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getAgentDir, SettingsManager } from "@mariozechner/companion-coding-agent";
|
import { getAgentDir, SettingsManager } from "@mariozechner/clanker-coding-agent";
|
||||||
import type { ChannelConfig } from "./types.js";
|
import type { ChannelConfig } from "./types.js";
|
||||||
|
|
||||||
const SETTINGS_KEY = "companion-channels";
|
const SETTINGS_KEY = "clanker-channels";
|
||||||
|
|
||||||
export function loadConfig(cwd: string): ChannelConfig {
|
export function loadConfig(cwd: string): ChannelConfig {
|
||||||
const agentDir = getAgentDir();
|
const agentDir = getAgentDir();
|
||||||
|
|
@ -62,10 +62,10 @@ export function loadConfig(cwd: string): ChannelConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a setting from the "companion-channels" config by dotted key path.
|
* Read a setting from the "clanker-channels" config by dotted key path.
|
||||||
* Useful for adapter-specific secrets that shouldn't live in the adapter config block.
|
* Useful for adapter-specific secrets that shouldn't live in the adapter config block.
|
||||||
*
|
*
|
||||||
* Example: getChannelSetting(cwd, "slack.appToken") reads companion-channels.slack.appToken
|
* Example: getChannelSetting(cwd, "slack.appToken") reads clanker-channels.slack.appToken
|
||||||
*/
|
*/
|
||||||
export function getChannelSetting(cwd: string, keyPath: string): unknown {
|
export function getChannelSetting(cwd: string, keyPath: string): unknown {
|
||||||
const agentDir = getAgentDir();
|
const agentDir = getAgentDir();
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Event API registration.
|
* clanker-channels — Event API registration.
|
||||||
*
|
*
|
||||||
* Events emitted:
|
* Events emitted:
|
||||||
* channel:receive — incoming message from an external adapter
|
* channel:receive — incoming message from an external adapter
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* bridge:* — chat bridge lifecycle events
|
* bridge:* — chat bridge lifecycle events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/companion-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/clanker-coding-agent";
|
||||||
import type { ChatBridge } from "./bridge/bridge.js";
|
import type { ChatBridge } from "./bridge/bridge.js";
|
||||||
import type { ChannelRegistry } from "./registry.js";
|
import type { ChannelRegistry } from "./registry.js";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -31,13 +31,13 @@ export function setBridge(bridge: ChatBridge | null): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerChannelEvents(
|
export function registerChannelEvents(
|
||||||
companion: ExtensionAPI,
|
clanker: ExtensionAPI,
|
||||||
registry: ChannelRegistry,
|
registry: ChannelRegistry,
|
||||||
): void {
|
): void {
|
||||||
// ── Incoming messages → channel:receive (+ bridge) ──────
|
// ── Incoming messages → channel:receive (+ bridge) ──────
|
||||||
|
|
||||||
registry.setOnIncoming((message: IncomingMessage) => {
|
registry.setOnIncoming((message: IncomingMessage) => {
|
||||||
companion.events.emit("channel:receive", message);
|
clanker.events.emit("channel:receive", message);
|
||||||
|
|
||||||
// Route to bridge if active
|
// Route to bridge if active
|
||||||
if (activeBridge?.isActive()) {
|
if (activeBridge?.isActive()) {
|
||||||
|
|
@ -47,7 +47,7 @@ export function registerChannelEvents(
|
||||||
|
|
||||||
// ── Auto-route cron job output ──────────────────────────
|
// ── Auto-route cron job output ──────────────────────────
|
||||||
|
|
||||||
companion.events.on("cron:job_complete", (raw: unknown) => {
|
clanker.events.on("cron:job_complete", (raw: unknown) => {
|
||||||
const event = raw as {
|
const event = raw as {
|
||||||
job: { name: string; channel: string; prompt: string };
|
job: { name: string; channel: string; prompt: string };
|
||||||
response?: string;
|
response?: string;
|
||||||
|
|
@ -74,7 +74,7 @@ export function registerChannelEvents(
|
||||||
|
|
||||||
// ── channel:send — deliver a message ─────────────────────
|
// ── channel:send — deliver a message ─────────────────────
|
||||||
|
|
||||||
companion.events.on("channel:send", (raw: unknown) => {
|
clanker.events.on("channel:send", (raw: unknown) => {
|
||||||
const data = raw as ChannelMessage & {
|
const data = raw as ChannelMessage & {
|
||||||
callback?: (result: { ok: boolean; error?: string }) => void;
|
callback?: (result: { ok: boolean; error?: string }) => void;
|
||||||
};
|
};
|
||||||
|
|
@ -83,7 +83,7 @@ export function registerChannelEvents(
|
||||||
|
|
||||||
// ── channel:register — add a custom adapter ──────────────
|
// ── channel:register — add a custom adapter ──────────────
|
||||||
|
|
||||||
companion.events.on("channel:register", (raw: unknown) => {
|
clanker.events.on("channel:register", (raw: unknown) => {
|
||||||
const data = raw as {
|
const data = raw as {
|
||||||
name: string;
|
name: string;
|
||||||
adapter: ChannelAdapter;
|
adapter: ChannelAdapter;
|
||||||
|
|
@ -99,14 +99,14 @@ export function registerChannelEvents(
|
||||||
|
|
||||||
// ── channel:remove — remove an adapter ───────────────────
|
// ── channel:remove — remove an adapter ───────────────────
|
||||||
|
|
||||||
companion.events.on("channel:remove", (raw: unknown) => {
|
clanker.events.on("channel:remove", (raw: unknown) => {
|
||||||
const data = raw as { name: string; callback?: (ok: boolean) => void };
|
const data = raw as { name: string; callback?: (ok: boolean) => void };
|
||||||
data.callback?.(registry.unregister(data.name));
|
data.callback?.(registry.unregister(data.name));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── channel:list — list adapters + routes ────────────────
|
// ── channel:list — list adapters + routes ────────────────
|
||||||
|
|
||||||
companion.events.on("channel:list", (raw: unknown) => {
|
clanker.events.on("channel:list", (raw: unknown) => {
|
||||||
const data = raw as {
|
const data = raw as {
|
||||||
callback?: (items: ReturnType<ChannelRegistry["list"]>) => void;
|
callback?: (items: ReturnType<ChannelRegistry["list"]>) => void;
|
||||||
};
|
};
|
||||||
|
|
@ -115,7 +115,7 @@ export function registerChannelEvents(
|
||||||
|
|
||||||
// ── channel:test — send a test ping ──────────────────────
|
// ── channel:test — send a test ping ──────────────────────
|
||||||
|
|
||||||
companion.events.on("channel:test", (raw: unknown) => {
|
clanker.events.on("channel:test", (raw: unknown) => {
|
||||||
const data = raw as {
|
const data = raw as {
|
||||||
adapter: string;
|
adapter: string;
|
||||||
recipient: string;
|
recipient: string;
|
||||||
|
|
@ -125,7 +125,7 @@ export function registerChannelEvents(
|
||||||
.send({
|
.send({
|
||||||
adapter: data.adapter,
|
adapter: data.adapter,
|
||||||
recipient: data.recipient ?? "",
|
recipient: data.recipient ?? "",
|
||||||
text: `🏓 companion-channels test — ${new Date().toISOString()}`,
|
text: `🏓 clanker-channels test — ${new Date().toISOString()}`,
|
||||||
source: "channel:test",
|
source: "channel:test",
|
||||||
})
|
})
|
||||||
.then((r) => data.callback?.(r));
|
.then((r) => data.callback?.(r));
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Two-way channel extension for companion.
|
* clanker-channels — Two-way channel extension for clanker.
|
||||||
*
|
*
|
||||||
* Routes messages between agents and external services
|
* Routes messages between agents and external services
|
||||||
* (Telegram, webhooks, custom adapters).
|
* (Telegram, webhooks, custom adapters).
|
||||||
*
|
*
|
||||||
* Built-in adapters: telegram (bidirectional), webhook (outgoing)
|
* Built-in adapters: telegram (bidirectional), webhook (outgoing)
|
||||||
* Custom adapters: register via companion.events.emit("channel:register", ...)
|
* Custom adapters: register via clanker.events.emit("channel:register", ...)
|
||||||
*
|
*
|
||||||
* Chat bridge: when enabled, incoming messages are routed to the agent
|
* Chat bridge: when enabled, incoming messages are routed to the agent
|
||||||
* as isolated subprocess prompts and responses are sent back. Enable via:
|
* as isolated subprocess prompts and responses are sent back. Enable via:
|
||||||
* - --chat-bridge flag
|
* - --chat-bridge flag
|
||||||
* - /chat-bridge on command
|
* - /chat-bridge on command
|
||||||
* - settings.json: { "companion-channels": { "bridge": { "enabled": true } } }
|
* - settings.json: { "clanker-channels": { "bridge": { "enabled": true } } }
|
||||||
*
|
*
|
||||||
* Config in settings.json under "companion-channels":
|
* Config in settings.json under "clanker-channels":
|
||||||
* {
|
* {
|
||||||
* "companion-channels": {
|
* "clanker-channels": {
|
||||||
* "adapters": {
|
* "adapters": {
|
||||||
* "telegram": { "type": "telegram", "botToken": "your-telegram-bot-token", "polling": true }
|
* "telegram": { "type": "telegram", "botToken": "your-telegram-bot-token", "polling": true }
|
||||||
* },
|
* },
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/companion-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/clanker-coding-agent";
|
||||||
import { ChatBridge } from "./bridge/bridge.js";
|
import { ChatBridge } from "./bridge/bridge.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { registerChannelEvents, setBridge } from "./events.js";
|
import { registerChannelEvents, setBridge } from "./events.js";
|
||||||
|
|
@ -42,15 +42,15 @@ import { createLogger } from "./logger.js";
|
||||||
import { ChannelRegistry } from "./registry.js";
|
import { ChannelRegistry } from "./registry.js";
|
||||||
import { registerChannelTool } from "./tool.js";
|
import { registerChannelTool } from "./tool.js";
|
||||||
|
|
||||||
export default function (companion: ExtensionAPI) {
|
export default function (clanker: ExtensionAPI) {
|
||||||
const log = createLogger(companion);
|
const log = createLogger(clanker);
|
||||||
const registry = new ChannelRegistry();
|
const registry = new ChannelRegistry();
|
||||||
registry.setLogger(log);
|
registry.setLogger(log);
|
||||||
let bridge: ChatBridge | null = null;
|
let bridge: ChatBridge | null = null;
|
||||||
|
|
||||||
// ── Flag: --chat-bridge ───────────────────────────────────
|
// ── Flag: --chat-bridge ───────────────────────────────────
|
||||||
|
|
||||||
companion.registerFlag("chat-bridge", {
|
clanker.registerFlag("chat-bridge", {
|
||||||
description:
|
description:
|
||||||
"Enable the chat bridge on startup (incoming messages → agent → reply)",
|
"Enable the chat bridge on startup (incoming messages → agent → reply)",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
|
|
@ -59,17 +59,17 @@ export default function (companion: ExtensionAPI) {
|
||||||
|
|
||||||
// ── Event API + cron integration ──────────────────────────
|
// ── Event API + cron integration ──────────────────────────
|
||||||
|
|
||||||
registerChannelEvents(companion, registry);
|
registerChannelEvents(clanker, registry);
|
||||||
|
|
||||||
// ── Lifecycle ─────────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────────
|
||||||
|
|
||||||
companion.on("session_start", async (_event, ctx) => {
|
clanker.on("session_start", async (_event, ctx) => {
|
||||||
const config = loadConfig(ctx.cwd);
|
const config = loadConfig(ctx.cwd);
|
||||||
await registry.loadConfig(config, ctx.cwd);
|
await registry.loadConfig(config, ctx.cwd);
|
||||||
|
|
||||||
const errors = registry.getErrors();
|
const errors = registry.getErrors();
|
||||||
for (const err of errors) {
|
for (const err of errors) {
|
||||||
ctx.ui.notify(`companion-channels: ${err.adapter}: ${err.error}`, "warning");
|
ctx.ui.notify(`clanker-channels: ${err.adapter}: ${err.error}`, "warning");
|
||||||
log("adapter-error", { adapter: err.adapter, error: err.error }, "ERROR");
|
log("adapter-error", { adapter: err.adapter, error: err.error }, "ERROR");
|
||||||
}
|
}
|
||||||
log("init", {
|
log("init", {
|
||||||
|
|
@ -84,22 +84,22 @@ export default function (companion: ExtensionAPI) {
|
||||||
.getErrors()
|
.getErrors()
|
||||||
.filter((e) => e.error.startsWith("Failed to start"));
|
.filter((e) => e.error.startsWith("Failed to start"));
|
||||||
for (const err of startErrors) {
|
for (const err of startErrors) {
|
||||||
ctx.ui.notify(`companion-channels: ${err.adapter}: ${err.error}`, "warning");
|
ctx.ui.notify(`clanker-channels: ${err.adapter}: ${err.error}`, "warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize bridge
|
// Initialize bridge
|
||||||
bridge = new ChatBridge(config.bridge, ctx.cwd, registry, companion.events, log);
|
bridge = new ChatBridge(config.bridge, ctx.cwd, registry, clanker.events, log);
|
||||||
setBridge(bridge);
|
setBridge(bridge);
|
||||||
|
|
||||||
const flagEnabled = companion.getFlag("--chat-bridge");
|
const flagEnabled = clanker.getFlag("--chat-bridge");
|
||||||
if (flagEnabled || config.bridge?.enabled) {
|
if (flagEnabled || config.bridge?.enabled) {
|
||||||
bridge.start();
|
bridge.start();
|
||||||
log("bridge-start", {});
|
log("bridge-start", {});
|
||||||
ctx.ui.notify("companion-channels: Chat bridge started", "info");
|
ctx.ui.notify("clanker-channels: Chat bridge started", "info");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.on("session_shutdown", async () => {
|
clanker.on("session_shutdown", async () => {
|
||||||
if (bridge?.isActive()) log("bridge-stop", {});
|
if (bridge?.isActive()) log("bridge-stop", {});
|
||||||
bridge?.stop();
|
bridge?.stop();
|
||||||
setBridge(null);
|
setBridge(null);
|
||||||
|
|
@ -108,7 +108,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
|
|
||||||
// ── Command: /chat-bridge ─────────────────────────────────
|
// ── Command: /chat-bridge ─────────────────────────────────
|
||||||
|
|
||||||
companion.registerCommand("chat-bridge", {
|
clanker.registerCommand("chat-bridge", {
|
||||||
description: "Manage chat bridge: /chat-bridge [on|off|status]",
|
description: "Manage chat bridge: /chat-bridge [on|off|status]",
|
||||||
getArgumentCompletions: (prefix: string) => {
|
getArgumentCompletions: (prefix: string) => {
|
||||||
return ["on", "off", "status"]
|
return ["on", "off", "status"]
|
||||||
|
|
@ -164,5 +164,5 @@ export default function (companion: ExtensionAPI) {
|
||||||
|
|
||||||
// ── LLM tool ──────────────────────────────────────────────
|
// ── LLM tool ──────────────────────────────────────────────
|
||||||
|
|
||||||
registerChannelTool(companion, registry);
|
registerChannelTool(clanker, registry);
|
||||||
}
|
}
|
||||||
8
packages/clanker-channels/src/logger.ts
Normal file
8
packages/clanker-channels/src/logger.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { ExtensionAPI } from "@mariozechner/clanker-coding-agent";
|
||||||
|
|
||||||
|
const CHANNEL = "channels";
|
||||||
|
|
||||||
|
export function createLogger(clanker: ExtensionAPI) {
|
||||||
|
return (event: string, data: unknown, level = "INFO") =>
|
||||||
|
clanker.events.emit("log", { channel: CHANNEL, event, level, data });
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Adapter registry + route resolution.
|
* clanker-channels — Adapter registry + route resolution.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — LLM tool registration.
|
* clanker-channels — LLM tool registration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { StringEnum } from "@mariozechner/companion-ai";
|
import { StringEnum } from "@mariozechner/clanker-ai";
|
||||||
import type { ExtensionAPI } from "@mariozechner/companion-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/clanker-coding-agent";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import type { ChannelRegistry } from "./registry.js";
|
import type { ChannelRegistry } from "./registry.js";
|
||||||
|
|
||||||
|
|
@ -16,10 +16,10 @@ interface ChannelToolParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerChannelTool(
|
export function registerChannelTool(
|
||||||
companion: ExtensionAPI,
|
clanker: ExtensionAPI,
|
||||||
registry: ChannelRegistry,
|
registry: ChannelRegistry,
|
||||||
): void {
|
): void {
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "notify",
|
name: "notify",
|
||||||
label: "Channel",
|
label: "Channel",
|
||||||
description:
|
description:
|
||||||
|
|
@ -57,7 +57,7 @@ export function registerChannelTool(
|
||||||
const items = registry.list();
|
const items = registry.list();
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
result =
|
result =
|
||||||
'No adapters configured. Add "companion-channels" to your settings.json.';
|
'No adapters configured. Add "clanker-channels" to your settings.json.';
|
||||||
} else {
|
} else {
|
||||||
const lines = items.map((i) =>
|
const lines = items.map((i) =>
|
||||||
i.type === "route"
|
i.type === "route"
|
||||||
|
|
@ -92,7 +92,7 @@ export function registerChannelTool(
|
||||||
const r = await registry.send({
|
const r = await registry.send({
|
||||||
adapter: params.adapter,
|
adapter: params.adapter,
|
||||||
recipient: params.recipient ?? "",
|
recipient: params.recipient ?? "",
|
||||||
text: `🏓 companion-channels test — ${new Date().toISOString()}`,
|
text: `🏓 clanker-channels test — ${new Date().toISOString()}`,
|
||||||
source: "channel:test",
|
source: "channel:test",
|
||||||
});
|
});
|
||||||
result = r.ok
|
result = r.ok
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* companion-channels — Shared types.
|
* clanker-channels — Shared types.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ── Channel message ─────────────────────────────────────────────
|
// ── Channel message ─────────────────────────────────────────────
|
||||||
|
|
@ -17,7 +17,7 @@ export interface ChannelMessage {
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Incoming message (from external → companion) ───────────────────────
|
// ── Incoming message (from external → clanker) ───────────────────────
|
||||||
|
|
||||||
export interface IncomingAttachment {
|
export interface IncomingAttachment {
|
||||||
/** Attachment type */
|
/** Attachment type */
|
||||||
|
|
@ -90,7 +90,7 @@ export interface ChannelAdapter {
|
||||||
sendTyping?(recipient: string): Promise<void>;
|
sendTyping?(recipient: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Config (lives under "companion-channels" key in companion settings.json) ──
|
// ── Config (lives under "clanker-channels" key in clanker settings.json) ──
|
||||||
|
|
||||||
export interface AdapterConfig {
|
export interface AdapterConfig {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -103,8 +103,8 @@ export interface BridgeConfig {
|
||||||
/**
|
/**
|
||||||
* Default session mode (default: "persistent").
|
* Default session mode (default: "persistent").
|
||||||
*
|
*
|
||||||
* - "persistent" — long-lived `companion --mode rpc` subprocess with conversation memory
|
* - "persistent" — long-lived `clanker --mode rpc` subprocess with conversation memory
|
||||||
* - "stateless" — isolated `companion -p --no-session` subprocess per message (no memory)
|
* - "stateless" — isolated `clanker -p --no-session` subprocess per message (no memory)
|
||||||
*
|
*
|
||||||
* Can be overridden per sender via `sessionRules`.
|
* Can be overridden per sender via `sessionRules`.
|
||||||
*/
|
*/
|
||||||
|
|
@ -144,7 +144,7 @@ export interface BridgeConfig {
|
||||||
* extensions that crash or conflict, e.g. webserver port collisions).
|
* extensions that crash or conflict, e.g. webserver port collisions).
|
||||||
* List only the extensions the bridge agent actually needs.
|
* List only the extensions the bridge agent actually needs.
|
||||||
*
|
*
|
||||||
* Example: ["/Users/you/Dev/companion/extensions/companion-vault/src/index.ts"]
|
* Example: ["/Users/you/Dev/clanker/extensions/clanker-vault/src/index.ts"]
|
||||||
*/
|
*/
|
||||||
extensions?: string[];
|
extensions?: string[];
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# companion-grind
|
# clanker-grind
|
||||||
|
|
||||||
Explicit grind mode for companion.
|
Explicit grind mode for clanker.
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
|
||||||
- Auto-activates only when the user uses explicit grind cues in a prompt
|
- Auto-activates only when the user uses explicit grind cues in a prompt
|
||||||
- Persists run state in session custom entries
|
- Persists run state in session custom entries
|
||||||
- Continues work on a heartbeat while running in `companion daemon`
|
- Continues work on a heartbeat while running in `clanker daemon`
|
||||||
- Pauses automatically when the user sends a normal prompt
|
- Pauses automatically when the user sends a normal prompt
|
||||||
|
|
||||||
Example prompts:
|
Example prompts:
|
||||||
|
|
@ -26,7 +26,7 @@ Settings:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"companion-grind": {
|
"clanker-grind": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"pollIntervalMs": 30000,
|
"pollIntervalMs": 30000,
|
||||||
"cueMode": "explicit-only",
|
"cueMode": "explicit-only",
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "companion-grind",
|
"name": "clanker-grind",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Explicit grind mode for companion with durable follow-up continuation in daemon mode",
|
"description": "Explicit grind mode for clanker with durable follow-up continuation in daemon mode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"companion-package"
|
"clanker-package"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Mario Zechner",
|
"author": "Mario Zechner",
|
||||||
|
|
@ -15,14 +15,14 @@
|
||||||
"package.json",
|
"package.json",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"companion": {
|
"clanker": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"./src/index.ts"
|
"./src/index.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mariozechner/companion-agent-core": "*",
|
"@harivansh-afk/clanker-agent-core": "*",
|
||||||
"@mariozechner/companion-coding-agent": "*"
|
"@harivansh-afk/clanker-coding-agent": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getAgentDir, SettingsManager } from "@mariozechner/companion-coding-agent";
|
import { getAgentDir, SettingsManager } from "@mariozechner/clanker-coding-agent";
|
||||||
import { DEFAULT_POLL_INTERVAL_MS, GRIND_SETTINGS_KEY, type GrindConfig } from "./types.js";
|
import { DEFAULT_POLL_INTERVAL_MS, GRIND_SETTINGS_KEY, type GrindConfig } from "./types.js";
|
||||||
|
|
||||||
const DEFAULT_CUE_PATTERNS = [
|
const DEFAULT_CUE_PATTERNS = [
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { AgentMessage } from "@mariozechner/companion-agent-core";
|
import type { AgentMessage } from "@mariozechner/clanker-agent-core";
|
||||||
import type { ExtensionAPI, ExtensionContext, RegisteredCommand } from "@mariozechner/companion-coding-agent";
|
import type { ExtensionAPI, ExtensionContext, RegisteredCommand } from "@mariozechner/clanker-coding-agent";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { parseAutoActivation, parseGrindStatus, parseStopCondition } from "./parser.js";
|
import { parseAutoActivation, parseGrindStatus, parseStopCondition } from "./parser.js";
|
||||||
import { buildContinuationPrompt, buildRepairPrompt, buildSystemPromptAddon } from "./prompts.js";
|
import { buildContinuationPrompt, buildRepairPrompt, buildSystemPromptAddon } from "./prompts.js";
|
||||||
|
|
@ -13,19 +13,19 @@ import {
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
function isDaemonRuntime(): boolean {
|
function isDaemonRuntime(): boolean {
|
||||||
if (process.env.COMPANION_GRIND_FORCE_DAEMON === "1") {
|
if (process.env.CLANKER_GRIND_FORCE_DAEMON === "1") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (process.env.COMPANION_GRIND_FORCE_DAEMON === "0") {
|
if (process.env.CLANKER_GRIND_FORCE_DAEMON === "0") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
process.argv.includes("daemon") ||
|
process.argv.includes("daemon") ||
|
||||||
process.argv.includes("gateway") ||
|
process.argv.includes("gateway") ||
|
||||||
Boolean(process.env.COMPANION_GATEWAY_BIND) ||
|
Boolean(process.env.CLANKER_GATEWAY_BIND) ||
|
||||||
Boolean(process.env.COMPANION_GATEWAY_PORT) ||
|
Boolean(process.env.CLANKER_GATEWAY_PORT) ||
|
||||||
Boolean(process.env.COMPANION_GATEWAY_TOKEN)
|
Boolean(process.env.CLANKER_GATEWAY_TOKEN)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,21 +57,21 @@ function readState(ctx: ExtensionContext): GrindRunState | null {
|
||||||
return getLatestRunState(ctx.sessionManager.getEntries());
|
return getLatestRunState(ctx.sessionManager.getEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistState(companion: ExtensionAPI, ctx: ExtensionContext, state: GrindRunState): GrindRunState {
|
function persistState(clanker: ExtensionAPI, ctx: ExtensionContext, state: GrindRunState): GrindRunState {
|
||||||
companion.appendEntry(GRIND_STATE_ENTRY_TYPE, state);
|
clanker.appendEntry(GRIND_STATE_ENTRY_TYPE, state);
|
||||||
if (ctx.hasUI) {
|
if (ctx.hasUI) {
|
||||||
ctx.ui.setStatus("companion-grind", state.status === "active" ? "GRIND" : state.status.toUpperCase());
|
ctx.ui.setStatus("clanker-grind", state.status === "active" ? "GRIND" : state.status.toUpperCase());
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearUiStatus(ctx: ExtensionContext): void {
|
function clearUiStatus(ctx: ExtensionContext): void {
|
||||||
if (ctx.hasUI) {
|
if (ctx.hasUI) {
|
||||||
ctx.ui.setStatus("companion-grind", "");
|
ctx.ui.setStatus("clanker-grind", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeExpireRun(companion: ExtensionAPI, ctx: ExtensionContext, state: GrindRunState): GrindRunState | null {
|
function maybeExpireRun(clanker: ExtensionAPI, ctx: ExtensionContext, state: GrindRunState): GrindRunState | null {
|
||||||
if (!state.deadlineAt) {
|
if (!state.deadlineAt) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +85,7 @@ function maybeExpireRun(companion: ExtensionAPI, ctx: ExtensionContext, state: G
|
||||||
lastNextAction: null,
|
lastNextAction: null,
|
||||||
pendingRepair: false,
|
pendingRepair: false,
|
||||||
});
|
});
|
||||||
persistState(companion, ctx, expired);
|
persistState(clanker, ctx, expired);
|
||||||
note(ctx, "Grind mode stopped: deadline reached.");
|
note(ctx, "Grind mode stopped: deadline reached.");
|
||||||
return expired;
|
return expired;
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +164,7 @@ function parseStartCommandArgs(args: string): {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRun(
|
function startRun(
|
||||||
companion: ExtensionAPI,
|
clanker: ExtensionAPI,
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
config: GrindConfig,
|
config: GrindConfig,
|
||||||
input: {
|
input: {
|
||||||
|
|
@ -181,17 +181,17 @@ function startRun(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.requireDaemon && !isDaemonRuntime()) {
|
if (config.requireDaemon && !isDaemonRuntime()) {
|
||||||
note(ctx, "Durable grind mode requires `companion daemon`.");
|
note(ctx, "Durable grind mode requires `clanker daemon`.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextState = createRunState(input);
|
const nextState = createRunState(input);
|
||||||
persistState(companion, ctx, nextState);
|
persistState(clanker, ctx, nextState);
|
||||||
note(ctx, "Grind mode activated.");
|
note(ctx, "Grind mode activated.");
|
||||||
return nextState;
|
return nextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function grind(companion: ExtensionAPI) {
|
export default function grind(clanker: ExtensionAPI) {
|
||||||
let config: GrindConfig | null = null;
|
let config: GrindConfig | null = null;
|
||||||
let state: GrindRunState | null = null;
|
let state: GrindRunState | null = null;
|
||||||
let heartbeat: NodeJS.Timeout | null = null;
|
let heartbeat: NodeJS.Timeout | null = null;
|
||||||
|
|
@ -221,18 +221,18 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expired = maybeExpireRun(companion, ctx, state);
|
const expired = maybeExpireRun(clanker, ctx, state);
|
||||||
state = expired;
|
state = expired;
|
||||||
if (!state || state.status !== "active") {
|
if (!state || state.status !== "active") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.pendingRepair) {
|
if (state.pendingRepair) {
|
||||||
companion.sendUserMessage(buildRepairPrompt(state), {
|
clanker.sendUserMessage(buildRepairPrompt(state), {
|
||||||
deliverAs: "followUp",
|
deliverAs: "followUp",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
companion.sendUserMessage(buildContinuationPrompt(state), {
|
clanker.sendUserMessage(buildContinuationPrompt(state), {
|
||||||
deliverAs: "followUp",
|
deliverAs: "followUp",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +241,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerCommand = (name: string, command: Omit<RegisteredCommand, "name">) => {
|
const registerCommand = (name: string, command: Omit<RegisteredCommand, "name">) => {
|
||||||
companion.registerCommand(name, command);
|
clanker.registerCommand(name, command);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerCommand("grind", {
|
registerCommand("grind", {
|
||||||
|
|
@ -268,7 +268,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
? parseStopCondition(`until ${parsed.until}`)
|
? parseStopCondition(`until ${parsed.until}`)
|
||||||
: { deadlineAt: null, completionCriterion: null };
|
: { deadlineAt: null, completionCriterion: null };
|
||||||
|
|
||||||
const nextState = startRun(companion, ctx, currentConfig, {
|
const nextState = startRun(clanker, ctx, currentConfig, {
|
||||||
activation: "command",
|
activation: "command",
|
||||||
goal: parsed.goal,
|
goal: parsed.goal,
|
||||||
sourcePrompt: parsed.goal,
|
sourcePrompt: parsed.goal,
|
||||||
|
|
@ -277,7 +277,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
});
|
});
|
||||||
state = nextState;
|
state = nextState;
|
||||||
if (state) {
|
if (state) {
|
||||||
companion.sendUserMessage(parsed.goal, { deliverAs: "followUp" });
|
clanker.sendUserMessage(parsed.goal, { deliverAs: "followUp" });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -312,7 +312,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
note(ctx, "No grind run to pause.");
|
note(ctx, "No grind run to pause.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state = persistState(companion, ctx, withStatus(state, "paused", { pendingRepair: false }));
|
state = persistState(clanker, ctx, withStatus(state, "paused", { pendingRepair: false }));
|
||||||
note(ctx, "Grind mode paused.");
|
note(ctx, "Grind mode paused.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -326,12 +326,12 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentConfig.requireDaemon && !isDaemonRuntime()) {
|
if (currentConfig.requireDaemon && !isDaemonRuntime()) {
|
||||||
note(ctx, "Durable grind mode requires `companion daemon`.");
|
note(ctx, "Durable grind mode requires `clanker daemon`.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state = persistState(companion, ctx, withStatus(state, "active"));
|
state = persistState(clanker, ctx, withStatus(state, "active"));
|
||||||
note(ctx, "Grind mode resumed.");
|
note(ctx, "Grind mode resumed.");
|
||||||
companion.sendUserMessage(buildContinuationPrompt(state), {
|
clanker.sendUserMessage(buildContinuationPrompt(state), {
|
||||||
deliverAs: "followUp",
|
deliverAs: "followUp",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -346,7 +346,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state = persistState(
|
state = persistState(
|
||||||
companion,
|
clanker,
|
||||||
ctx,
|
ctx,
|
||||||
withStatus(state, "stopped", {
|
withStatus(state, "stopped", {
|
||||||
pendingRepair: false,
|
pendingRepair: false,
|
||||||
|
|
@ -362,22 +362,22 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.on("session_start", async (_event, ctx) => {
|
clanker.on("session_start", async (_event, ctx) => {
|
||||||
config = loadConfig(ctx.cwd);
|
config = loadConfig(ctx.cwd);
|
||||||
state = readState(ctx);
|
state = readState(ctx);
|
||||||
if (state && ctx.hasUI) {
|
if (state && ctx.hasUI) {
|
||||||
ctx.ui.setStatus("companion-grind", state.status === "active" ? "GRIND" : state.status.toUpperCase());
|
ctx.ui.setStatus("clanker-grind", state.status === "active" ? "GRIND" : state.status.toUpperCase());
|
||||||
}
|
}
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
ensureHeartbeat(ctx);
|
ensureHeartbeat(ctx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.on("session_shutdown", async () => {
|
clanker.on("session_shutdown", async () => {
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.on("input", async (event, ctx) => {
|
clanker.on("input", async (event, ctx) => {
|
||||||
const currentConfig = getConfig(ctx.cwd);
|
const currentConfig = getConfig(ctx.cwd);
|
||||||
if (!currentConfig.enabled || event.source === "extension") {
|
if (!currentConfig.enabled || event.source === "extension") {
|
||||||
return { action: "continue" } as const;
|
return { action: "continue" } as const;
|
||||||
|
|
@ -389,7 +389,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
const activation = parseAutoActivation(event.text, currentConfig.cuePatterns);
|
const activation = parseAutoActivation(event.text, currentConfig.cuePatterns);
|
||||||
if (!activation) {
|
if (!activation) {
|
||||||
if (currentConfig.userIntervention === "pause") {
|
if (currentConfig.userIntervention === "pause") {
|
||||||
state = persistState(companion, ctx, withStatus(currentState, "paused", { pendingRepair: false }));
|
state = persistState(clanker, ctx, withStatus(currentState, "paused", { pendingRepair: false }));
|
||||||
note(ctx, "Grind mode paused for manual input.");
|
note(ctx, "Grind mode paused for manual input.");
|
||||||
}
|
}
|
||||||
return { action: "continue" } as const;
|
return { action: "continue" } as const;
|
||||||
|
|
@ -401,7 +401,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
return { action: "continue" } as const;
|
return { action: "continue" } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
state = startRun(companion, ctx, currentConfig, {
|
state = startRun(clanker, ctx, currentConfig, {
|
||||||
activation: "explicit",
|
activation: "explicit",
|
||||||
goal: event.text,
|
goal: event.text,
|
||||||
sourcePrompt: event.text,
|
sourcePrompt: event.text,
|
||||||
|
|
@ -412,13 +412,13 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
return { action: "continue" } as const;
|
return { action: "continue" } as const;
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.on("before_agent_start", async (event, ctx) => {
|
clanker.on("before_agent_start", async (event, ctx) => {
|
||||||
state = state ?? readState(ctx);
|
state = state ?? readState(ctx);
|
||||||
if (!state || state.status !== "active") {
|
if (!state || state.status !== "active") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expired = maybeExpireRun(companion, ctx, state);
|
const expired = maybeExpireRun(clanker, ctx, state);
|
||||||
state = expired;
|
state = expired;
|
||||||
if (!state || state.status !== "active") {
|
if (!state || state.status !== "active") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -429,13 +429,13 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.on("turn_end", async (event, ctx) => {
|
clanker.on("turn_end", async (event, ctx) => {
|
||||||
state = state ?? readState(ctx);
|
state = state ?? readState(ctx);
|
||||||
if (!state || state.status !== "active") {
|
if (!state || state.status !== "active") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expired = maybeExpireRun(companion, ctx, state);
|
const expired = maybeExpireRun(clanker, ctx, state);
|
||||||
state = expired;
|
state = expired;
|
||||||
if (!state || state.status !== "active") {
|
if (!state || state.status !== "active") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -447,7 +447,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
if (state.consecutiveParseFailures + 1 >= MAX_PARSE_FAILURES) {
|
if (state.consecutiveParseFailures + 1 >= MAX_PARSE_FAILURES) {
|
||||||
state = persistState(
|
state = persistState(
|
||||||
companion,
|
clanker,
|
||||||
ctx,
|
ctx,
|
||||||
withStatus(state, "blocked", {
|
withStatus(state, "blocked", {
|
||||||
pendingRepair: false,
|
pendingRepair: false,
|
||||||
|
|
@ -460,7 +460,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state = persistState(companion, ctx, {
|
state = persistState(clanker, ctx, {
|
||||||
...state,
|
...state,
|
||||||
pendingRepair: true,
|
pendingRepair: true,
|
||||||
consecutiveParseFailures: state.consecutiveParseFailures + 1,
|
consecutiveParseFailures: state.consecutiveParseFailures + 1,
|
||||||
|
|
@ -469,7 +469,7 @@ export default function grind(companion: ExtensionAPI) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state = persistState(companion, ctx, withLoopStatus(state, parsed));
|
state = persistState(clanker, ctx, withLoopStatus(state, parsed));
|
||||||
if (state.status !== "active") {
|
if (state.status !== "active") {
|
||||||
note(ctx, `Grind mode ${state.status}.`);
|
note(ctx, `Grind mode ${state.status}.`);
|
||||||
if (state.status !== "paused") {
|
if (state.status !== "paused") {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { SessionEntry } from "@mariozechner/companion-coding-agent";
|
import type { SessionEntry } from "@mariozechner/clanker-coding-agent";
|
||||||
import {
|
import {
|
||||||
GRIND_STATE_ENTRY_TYPE,
|
GRIND_STATE_ENTRY_TYPE,
|
||||||
type GrindActivation,
|
type GrindActivation,
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export const GRIND_SETTINGS_KEY = "companion-grind";
|
export const GRIND_SETTINGS_KEY = "clanker-grind";
|
||||||
export const GRIND_STATE_ENTRY_TYPE = "companion-grind/state";
|
export const GRIND_STATE_ENTRY_TYPE = "clanker-grind/state";
|
||||||
export const DEFAULT_COMPLETION_CRITERION = "finish the requested task";
|
export const DEFAULT_COMPLETION_CRITERION = "finish the requested task";
|
||||||
export const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
export const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
||||||
export const MAX_PARSE_FAILURES = 2;
|
export const MAX_PARSE_FAILURES = 2;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { detectCue, parseAutoActivation, parseGrindStatus, parseStopCondition } from "../src/parser.js";
|
import { detectCue, parseAutoActivation, parseGrindStatus, parseStopCondition } from "../src/parser.js";
|
||||||
|
|
||||||
describe("companion-grind parser", () => {
|
describe("clanker-grind parser", () => {
|
||||||
const now = new Date(2026, 2, 9, 9, 0, 0);
|
const now = new Date(2026, 2, 9, 9, 0, 0);
|
||||||
const cues = ["don't stop", "keep going", "run until"];
|
const cues = ["don't stop", "keep going", "run until"];
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { SessionEntry } from "@mariozechner/companion-coding-agent";
|
import type { SessionEntry } from "@mariozechner/clanker-coding-agent";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { createRunState, getLatestRunState, withLoopStatus, withStatus } from "../src/state.js";
|
import { createRunState, getLatestRunState, withLoopStatus, withStatus } from "../src/state.js";
|
||||||
import { GRIND_STATE_ENTRY_TYPE } from "../src/types.js";
|
import { GRIND_STATE_ENTRY_TYPE } from "../src/types.js";
|
||||||
|
|
||||||
describe("companion-grind state", () => {
|
describe("clanker-grind state", () => {
|
||||||
it("creates active run state", () => {
|
it("creates active run state", () => {
|
||||||
const state = createRunState({
|
const state = createRunState({
|
||||||
activation: "explicit",
|
activation: "explicit",
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.companion
|
.clanker
|
||||||
dist
|
dist
|
||||||
*.log
|
*.log
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# companion-teams: Agent Guide 🤖
|
# clanker-teams: Agent Guide 🤖
|
||||||
|
|
||||||
This guide explains how `companion-teams` transforms your single companion agent into a coordinated team of specialists. It covers the roles, capabilities, and coordination patterns available to you as the **Team Lead**.
|
This guide explains how `clanker-teams` transforms your single clanker agent into a coordinated team of specialists. It covers the roles, capabilities, and coordination patterns available to you as the **Team Lead**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎭 The Two Roles
|
## 🎭 The Two Roles
|
||||||
|
|
||||||
In a `companion-teams` environment, there are two distinct types of agents:
|
In a `clanker-teams` environment, there are two distinct types of agents:
|
||||||
|
|
||||||
### 1. The Team Lead (You)
|
### 1. The Team Lead (You)
|
||||||
|
|
||||||
|
|
@ -87,13 +87,13 @@ Use this for refactoring or security work.
|
||||||
|
|
||||||
Use automated hooks to ensure standards.
|
Use automated hooks to ensure standards.
|
||||||
|
|
||||||
1. Define a script at `.companion/team-hooks/task_completed.sh`.
|
1. Define a script at `.clanker/team-hooks/task_completed.sh`.
|
||||||
2. When any teammate marks a task as `completed`, the hook runs (e.g., runs `npm test`).
|
2. When any teammate marks a task as `completed`, the hook runs (e.g., runs `npm test`).
|
||||||
3. If the hook fails, you'll know the work isn't ready.
|
3. If the hook fails, you'll know the work isn't ready.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛑 When to Use companion-teams
|
## 🛑 When to Use clanker-teams
|
||||||
|
|
||||||
- **Complex Projects**: Tasks that involve multiple files and logic layers.
|
- **Complex Projects**: Tasks that involve multiple files and logic layers.
|
||||||
- **Research & Execution**: One agent researches while another implements.
|
- **Research & Execution**: One agent researches while another implements.
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
# companion-teams 🚀
|
# clanker-teams 🚀
|
||||||
|
|
||||||
**companion-teams** turns your single companion agent into a coordinated software engineering team. It allows you to spawn multiple "Teammate" agents in separate terminal panes that work autonomously, communicate with each other, and manage a shared task board-all mediated through tmux, Zellij, iTerm2, or WezTerm.
|
**clanker-teams** turns your single clanker agent into a coordinated software engineering team. It allows you to spawn multiple "Teammate" agents in separate terminal panes that work autonomously, communicate with each other, and manage a shared task board-all mediated through tmux, Zellij, iTerm2, or WezTerm.
|
||||||
|
|
||||||
### 🖥️ companion-teams in Action
|
### 🖥️ clanker-teams in Action
|
||||||
|
|
||||||
| iTerm2 | tmux | Zellij |
|
| iTerm2 | tmux | Zellij |
|
||||||
| :----------------------------------------------------------------------------------: | :----------------------------------------------------------------------------: | :----------------------------------------------------------------------------------: |
|
| :----------------------------------------------------------------------------------: | :----------------------------------------------------------------------------: | :----------------------------------------------------------------------------------: |
|
||||||
| <a href="iTerm2.png"><img src="iTerm2.png" width="300" alt="companion-teams in iTerm2"></a> | <a href="tmux.png"><img src="tmux.png" width="300" alt="companion-teams in tmux"></a> | <a href="zellij.png"><img src="zellij.png" width="300" alt="companion-teams in Zellij"></a> |
|
| <a href="iTerm2.png"><img src="iTerm2.png" width="300" alt="clanker-teams in iTerm2"></a> | <a href="tmux.png"><img src="tmux.png" width="300" alt="clanker-teams in tmux"></a> | <a href="zellij.png"><img src="zellij.png" width="300" alt="clanker-teams in Zellij"></a> |
|
||||||
|
|
||||||
_Also works with **WezTerm** (cross-platform support)_
|
_Also works with **WezTerm** (cross-platform support)_
|
||||||
|
|
||||||
## 🛠 Installation
|
## 🛠 Installation
|
||||||
|
|
||||||
Open your companion terminal and type:
|
Open your clanker terminal and type:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
companion install npm:companion-teams
|
clanker install npm:clanker-teams
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
@ -95,9 +95,9 @@ companion install npm:companion-teams
|
||||||
> **You:** "Spawn a teammate named 'architect-bot' using 'gpt-4o' with 'high' thinking level for deep reasoning."
|
> **You:** "Spawn a teammate named 'architect-bot' using 'gpt-4o' with 'high' thinking level for deep reasoning."
|
||||||
|
|
||||||
**Smart Model Resolution:**
|
**Smart Model Resolution:**
|
||||||
When you specify a model name without a provider (e.g., `gemini-2.5-flash`), companion-teams automatically:
|
When you specify a model name without a provider (e.g., `gemini-2.5-flash`), clanker-teams automatically:
|
||||||
|
|
||||||
- Queries available models from `companion --list-models`
|
- Queries available models from `clanker --list-models`
|
||||||
- Prioritizes **OAuth/subscription providers** (cheaper/free) over API-key providers:
|
- Prioritizes **OAuth/subscription providers** (cheaper/free) over API-key providers:
|
||||||
- `google-gemini-cli` (OAuth) is preferred over `google` (API key)
|
- `google-gemini-cli` (OAuth) is preferred over `google` (API key)
|
||||||
- `github-copilot`, `kimi-sub` are preferred over their API-key equivalents
|
- `github-copilot`, `kimi-sub` are preferred over their API-key equivalents
|
||||||
|
|
@ -131,7 +131,7 @@ Teammates in `planning` mode will use `task_submit_plan`. As the lead, review th
|
||||||
|
|
||||||
## 🪟 Terminal Requirements
|
## 🪟 Terminal Requirements
|
||||||
|
|
||||||
To show multiple agents on one screen, **companion-teams** requires a way to manage terminal panes. It supports **tmux**, **Zellij**, **iTerm2**, and **WezTerm**.
|
To show multiple agents on one screen, **clanker-teams** requires a way to manage terminal panes. It supports **tmux**, **Zellij**, **iTerm2**, and **WezTerm**.
|
||||||
|
|
||||||
### Option 1: tmux (Recommended)
|
### Option 1: tmux (Recommended)
|
||||||
|
|
||||||
|
|
@ -144,16 +144,16 @@ How to run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tmux # Start tmux session
|
tmux # Start tmux session
|
||||||
companion # Start companion inside tmux
|
clanker # Start clanker inside tmux
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 2: Zellij
|
### Option 2: Zellij
|
||||||
|
|
||||||
Simply start `companion` inside a Zellij session. **companion-teams** will detect it via the `ZELLIJ` environment variable and use `zellij run` to spawn teammates in new panes.
|
Simply start `clanker` inside a Zellij session. **clanker-teams** will detect it via the `ZELLIJ` environment variable and use `zellij run` to spawn teammates in new panes.
|
||||||
|
|
||||||
### Option 3: iTerm2 (macOS)
|
### Option 3: iTerm2 (macOS)
|
||||||
|
|
||||||
If you are using **iTerm2** on macOS and are _not_ inside tmux or Zellij, **companion-teams** can manage your team in two ways:
|
If you are using **iTerm2** on macOS and are _not_ inside tmux or Zellij, **clanker-teams** can manage your team in two ways:
|
||||||
|
|
||||||
1. **Panes (Default)**: Automatically split your current window into an optimized layout.
|
1. **Panes (Default)**: Automatically split your current window into an optimized layout.
|
||||||
2. **Windows**: Create true separate OS windows for each agent.
|
2. **Windows**: Create true separate OS windows for each agent.
|
||||||
|
|
@ -174,14 +174,14 @@ How to run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wezterm # Start WezTerm
|
wezterm # Start WezTerm
|
||||||
companion # Start companion inside WezTerm
|
clanker # Start clanker inside WezTerm
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📜 Credits & Attribution
|
## 📜 Credits & Attribution
|
||||||
|
|
||||||
This project is a port of the excellent [claude-code-teams-mcp](https://github.com/cs50victor/claude-code-teams-mcp) by [cs50victor](https://github.com/cs50victor).
|
This project is a port of the excellent [claude-code-teams-mcp](https://github.com/cs50victor/claude-code-teams-mcp) by [cs50victor](https://github.com/cs50victor).
|
||||||
|
|
||||||
We have adapted the original MCP coordination protocol to work natively as a **companion package**, adding features like auto-starting teammates, balanced vertical UI layouts, automatic inbox polling, plan approval mode, broadcast messaging, and quality gate hooks.
|
We have adapted the original MCP coordination protocol to work natively as a **clanker package**, adding features like auto-starting teammates, balanced vertical UI layouts, automatic inbox polling, plan approval mode, broadcast messaging, and quality gate hooks.
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
WezTerm was not creating the correct panel layout for companion-teams. The desired layout is:
|
WezTerm was not creating the correct panel layout for clanker-teams. The desired layout is:
|
||||||
|
|
||||||
- **Main controller panel** on the LEFT (takes 70% width)
|
- **Main controller panel** on the LEFT (takes 70% width)
|
||||||
- **Teammate panels** stacked on the RIGHT (takes 30% width, divided vertically)
|
- **Teammate panels** stacked on the RIGHT (takes 30% width, divided vertically)
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Successfully added support for **WezTerm** terminal emulator to companion-teams, bringing the total number of supported terminals to **4**:
|
Successfully added support for **WezTerm** terminal emulator to clanker-teams, bringing the total number of supported terminals to **4**:
|
||||||
|
|
||||||
- tmux (multiplexer)
|
- tmux (multiplexer)
|
||||||
- Zellij (multiplexer)
|
- Zellij (multiplexer)
|
||||||
|
|
@ -89,7 +89,7 @@ Total: **46 tests passing**, 0 failures
|
||||||
|
|
||||||
- ✅ CLI-based pane management (`wezterm cli split-pane`)
|
- ✅ CLI-based pane management (`wezterm cli split-pane`)
|
||||||
- ✅ Auto-layout: left split for first pane (30%), bottom splits for subsequent (50%)
|
- ✅ Auto-layout: left split for first pane (30%), bottom splits for subsequent (50%)
|
||||||
- ✅ Environment variable filtering (only `COMPANION_*` prefixed)
|
- ✅ Environment variable filtering (only `CLANKER_*` prefixed)
|
||||||
- ✅ Graceful error handling
|
- ✅ Graceful error handling
|
||||||
- ✅ Pane killing via Ctrl-C
|
- ✅ Pane killing via Ctrl-C
|
||||||
- ✅ Tab title setting
|
- ✅ Tab title setting
|
||||||
|
|
@ -102,7 +102,7 @@ WezTerm is cross-platform:
|
||||||
- Linux ✅
|
- Linux ✅
|
||||||
- Windows ✅
|
- Windows ✅
|
||||||
|
|
||||||
This means companion-teams now works out-of-the-box on **more platforms** without requiring multiplexers like tmux or Zellij.
|
This means clanker-teams now works out-of-the-box on **more platforms** without requiring multiplexers like tmux or Zellij.
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# companion-teams Usage Guide
|
# clanker-teams Usage Guide
|
||||||
|
|
||||||
This guide provides detailed examples, patterns, and best practices for using companion-teams.
|
This guide provides detailed examples, patterns, and best practices for using clanker-teams.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
|
|
@ -22,10 +22,10 @@ First, make sure you're inside a tmux session, Zellij session, or iTerm2:
|
||||||
tmux # or zellij, or just use iTerm2
|
tmux # or zellij, or just use iTerm2
|
||||||
```
|
```
|
||||||
|
|
||||||
Then start companion:
|
Then start clanker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
companion
|
clanker
|
||||||
```
|
```
|
||||||
|
|
||||||
Create your first team:
|
Create your first team:
|
||||||
|
|
@ -66,7 +66,7 @@ Approve or reject:
|
||||||
|
|
||||||
### 3. Testing with Automated Hooks
|
### 3. Testing with Automated Hooks
|
||||||
|
|
||||||
Create a hook script at `.companion/team-hooks/task_completed.sh`:
|
Create a hook script at `.clanker/team-hooks/task_completed.sh`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
@ -124,11 +124,11 @@ Hooks are shell scripts that run automatically at specific events. Currently sup
|
||||||
|
|
||||||
### Hook Location
|
### Hook Location
|
||||||
|
|
||||||
Hooks should be placed in `.companion/team-hooks/` in your project directory:
|
Hooks should be placed in `.clanker/team-hooks/` in your project directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
your-project/
|
your-project/
|
||||||
├── .companion/
|
├── .clanker/
|
||||||
│ └── team-hooks/
|
│ └── team-hooks/
|
||||||
│ └── task_completed.sh
|
│ └── task_completed.sh
|
||||||
```
|
```
|
||||||
|
|
@ -161,7 +161,7 @@ Example payload:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# .companion/team-hooks/task_completed.sh
|
# .clanker/team-hooks/task_completed.sh
|
||||||
|
|
||||||
TASK_DATA="$1"
|
TASK_DATA="$1"
|
||||||
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
|
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
|
||||||
|
|
@ -174,7 +174,7 @@ npm test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# .companion/team-hooks/task_completed.sh
|
# .clanker/team-hooks/task_completed.sh
|
||||||
|
|
||||||
TASK_DATA="$1"
|
TASK_DATA="$1"
|
||||||
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
|
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
|
||||||
|
|
@ -189,7 +189,7 @@ curl -X POST -H 'Content-type: application/json' \
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# .companion/team-hooks/task_completed.sh
|
# .clanker/team-hooks/task_completed.sh
|
||||||
|
|
||||||
TASK_DATA="$1"
|
TASK_DATA="$1"
|
||||||
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
|
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
|
||||||
|
|
@ -304,15 +304,15 @@ This helps you catch blockers early and provide feedback.
|
||||||
|
|
||||||
**Problem**: tmux panes don't close when killing teammates.
|
**Problem**: tmux panes don't close when killing teammates.
|
||||||
|
|
||||||
**Solution**: Make sure you started companion inside a tmux session. If you started companion outside tmux, it won't work properly.
|
**Solution**: Make sure you started clanker inside a tmux session. If you started clanker outside tmux, it won't work properly.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Correct way
|
# Correct way
|
||||||
tmux
|
tmux
|
||||||
companion
|
clanker
|
||||||
|
|
||||||
# Incorrect way
|
# Incorrect way
|
||||||
companion # Then try to use tmux commands
|
clanker # Then try to use tmux commands
|
||||||
```
|
```
|
||||||
|
|
||||||
### Hook Not Running
|
### Hook Not Running
|
||||||
|
|
@ -321,30 +321,30 @@ companion # Then try to use tmux commands
|
||||||
|
|
||||||
**Checklist**:
|
**Checklist**:
|
||||||
|
|
||||||
1. File exists at `.companion/team-hooks/task_completed.sh`
|
1. File exists at `.clanker/team-hooks/task_completed.sh`
|
||||||
2. File is executable: `chmod +x .companion/team-hooks/task_completed.sh`
|
2. File is executable: `chmod +x .clanker/team-hooks/task_completed.sh`
|
||||||
3. Shebang line is present: `#!/bin/bash`
|
3. Shebang line is present: `#!/bin/bash`
|
||||||
4. Test manually: `.companion/team-hooks/task_completed.sh '{"test":"data"}'`
|
4. Test manually: `.clanker/team-hooks/task_completed.sh '{"test":"data"}'`
|
||||||
|
|
||||||
### Model Errors
|
### Model Errors
|
||||||
|
|
||||||
**Problem**: "Model not found" or similar errors.
|
**Problem**: "Model not found" or similar errors.
|
||||||
|
|
||||||
**Solution**: Check the model name is correct and available in your companion config. Some model names vary between providers:
|
**Solution**: Check the model name is correct and available in your clanker config. Some model names vary between providers:
|
||||||
|
|
||||||
- `gpt-4o` - OpenAI
|
- `gpt-4o` - OpenAI
|
||||||
- `haiku` - Anthropic (usually `claude-3-5-haiku`)
|
- `haiku` - Anthropic (usually `claude-3-5-haiku`)
|
||||||
- `glm-4.7` - Zhipu AI
|
- `glm-4.7` - Zhipu AI
|
||||||
|
|
||||||
Check your companion config for available models.
|
Check your clanker config for available models.
|
||||||
|
|
||||||
### Data Location
|
### Data Location
|
||||||
|
|
||||||
All team data is stored in:
|
All team data is stored in:
|
||||||
|
|
||||||
- `~/.companion/teams/<team-name>/` - Team configuration, member list
|
- `~/.clanker/teams/<team-name>/` - Team configuration, member list
|
||||||
- `~/.companion/tasks/<team-name>/` - Task files
|
- `~/.clanker/tasks/<team-name>/` - Task files
|
||||||
- `~/.companion/messages/<team-name>/` - Message history
|
- `~/.clanker/messages/<team-name>/` - Message history
|
||||||
|
|
||||||
You can manually inspect these JSON files to debug issues.
|
You can manually inspect these JSON files to debug issues.
|
||||||
|
|
||||||
|
|
@ -386,9 +386,9 @@ To remove all team data:
|
||||||
> "Shut down the team named 'my-team'"
|
> "Shut down the team named 'my-team'"
|
||||||
|
|
||||||
# Then delete data directory
|
# Then delete data directory
|
||||||
rm -rf ~/.companion/teams/my-team/
|
rm -rf ~/.clanker/teams/my-team/
|
||||||
rm -rf ~/.companion/tasks/my-team/
|
rm -rf ~/.clanker/tasks/my-team/
|
||||||
rm -rf ~/.companion/messages/my-team/
|
rm -rf ~/.clanker/messages/my-team/
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use the delete command:
|
Or use the delete command:
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
# companion-teams Core Features Implementation Plan
|
# clanker-teams Core Features Implementation Plan
|
||||||
|
|
||||||
> **REQUIRED SUB-SKILL:** Use the executing-plans skill to implement this plan task-by-task.
|
> **REQUIRED SUB-SKILL:** Use the executing-plans skill to implement this plan task-by-task.
|
||||||
|
|
||||||
**Goal:** Implement Plan Approval Mode, Broadcast Messaging, and Quality Gate Hooks for the `companion-teams` repository to achieve functional parity with Claude Code Agent Teams.
|
**Goal:** Implement Plan Approval Mode, Broadcast Messaging, and Quality Gate Hooks for the `clanker-teams` repository to achieve functional parity with Claude Code Agent Teams.
|
||||||
|
|
||||||
**Architecture:**
|
**Architecture:**
|
||||||
|
|
||||||
- **Plan Approval**: Add a `planning` status to `TaskFile.status`. Create `task_submit_plan` and `task_evaluate_plan` tools. Lead can approve/reject.
|
- **Plan Approval**: Add a `planning` status to `TaskFile.status`. Create `task_submit_plan` and `task_evaluate_plan` tools. Lead can approve/reject.
|
||||||
- **Broadcast Messaging**: Add a `broadcast_message` tool that iterates through the team roster in `config.json` and sends messages to all active members.
|
- **Broadcast Messaging**: Add a `broadcast_message` tool that iterates through the team roster in `config.json` and sends messages to all active members.
|
||||||
- **Quality Gate Hooks**: Introduce a simple hook system that triggers on `task_update` (specifically when status becomes `completed`). For now, it will look for a `.companion/team-hooks/task_completed.sh` or similar.
|
- **Quality Gate Hooks**: Introduce a simple hook system that triggers on `task_update` (specifically when status becomes `completed`). For now, it will look for a `.clanker/team-hooks/task_completed.sh` or similar.
|
||||||
|
|
||||||
**Tech Stack:** Node.js, TypeScript, Vitest
|
**Tech Stack:** Node.js, TypeScript, Vitest
|
||||||
|
|
||||||
|
|
@ -231,7 +231,7 @@ export function runHook(
|
||||||
): boolean {
|
): boolean {
|
||||||
const hookPath = path.join(
|
const hookPath = path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
".companion",
|
".clanker",
|
||||||
"team-hooks",
|
"team-hooks",
|
||||||
`${hookName}.sh`,
|
`${hookName}.sh`,
|
||||||
);
|
);
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# companion-teams Tool Reference
|
# clanker-teams Tool Reference
|
||||||
|
|
||||||
Complete documentation of all tools, parameters, and automated behavior.
|
Complete documentation of all tools, parameters, and automated behavior.
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ Launch a new agent into a terminal pane with a role and instructions.
|
||||||
|
|
||||||
**Model Options**:
|
**Model Options**:
|
||||||
|
|
||||||
- Any model available in your companion configuration
|
- Any model available in your clanker configuration
|
||||||
- Common models: `gpt-4o`, `haiku` (Anthropic), `glm-4.7`, `glm-5` (Zhipu AI)
|
- Common models: `gpt-4o`, `haiku` (Anthropic), `glm-4.7`, `glm-5` (Zhipu AI)
|
||||||
|
|
||||||
**Thinking Levels**:
|
**Thinking Levels**:
|
||||||
|
|
@ -298,7 +298,7 @@ task_update({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: When status changes to `completed`, any hook script at `.companion/team-hooks/task_completed.sh` will automatically run.
|
**Note**: When status changes to `completed`, any hook script at `.clanker/team-hooks/task_completed.sh` will automatically run.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -477,9 +477,9 @@ This ensures teammates stay responsive to new tasks, messages, and task reassign
|
||||||
|
|
||||||
### Automated Hooks
|
### Automated Hooks
|
||||||
|
|
||||||
When a task's status changes to `completed`, companion-teams automatically executes:
|
When a task's status changes to `completed`, clanker-teams automatically executes:
|
||||||
|
|
||||||
`.companion/team-hooks/task_completed.sh`
|
`.clanker/team-hooks/task_completed.sh`
|
||||||
|
|
||||||
The hook receives the task data as a JSON string as the first argument.
|
The hook receives the task data as a JSON string as the first argument.
|
||||||
|
|
||||||
|
|
@ -536,10 +536,10 @@ Task is removed from the active task list. Still preserved in data history.
|
||||||
|
|
||||||
### Data Storage
|
### Data Storage
|
||||||
|
|
||||||
All companion-teams data is stored in your home directory under `~/.companion/`:
|
All clanker-teams data is stored in your home directory under `~/.clanker/`:
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.companion/
|
~/.clanker/
|
||||||
├── teams/
|
├── teams/
|
||||||
│ └── <team-name>/
|
│ └── <team-name>/
|
||||||
│ └── config.json # Team configuration and member list
|
│ └── config.json # Team configuration and member list
|
||||||
|
|
@ -618,11 +618,11 @@ All companion-teams data is stored in your home directory under `~/.companion/`:
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
companion-teams respects the following environment variables:
|
clanker-teams respects the following environment variables:
|
||||||
|
|
||||||
- `ZELLIJ`: Automatically detected when running inside Zellij. Enables Zellij pane management.
|
- `ZELLIJ`: Automatically detected when running inside Zellij. Enables Zellij pane management.
|
||||||
- `TMUX`: Automatically detected when running inside tmux. Enables tmux pane management.
|
- `TMUX`: Automatically detected when running inside tmux. Enables tmux pane management.
|
||||||
- `COMPANION_DEFAULT_THINKING_LEVEL`: Default thinking level for spawned teammates if not specified (`off`, `minimal`, `low`, `medium`, `high`).
|
- `CLANKER_DEFAULT_THINKING_LEVEL`: Default thinking level for spawned teammates if not specified (`off`, `minimal`, `low`, `medium`, `high`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -630,19 +630,19 @@ companion-teams respects the following environment variables:
|
||||||
|
|
||||||
### tmux Detection
|
### tmux Detection
|
||||||
|
|
||||||
If the `TMUX` environment variable is set, companion-teams uses `tmux split-window` to create panes.
|
If the `TMUX` environment variable is set, clanker-teams uses `tmux split-window` to create panes.
|
||||||
|
|
||||||
**Layout**: Large lead pane on the left, teammates stacked on the right.
|
**Layout**: Large lead pane on the left, teammates stacked on the right.
|
||||||
|
|
||||||
### Zellij Detection
|
### Zellij Detection
|
||||||
|
|
||||||
If the `ZELLIJ` environment variable is set, companion-teams uses `zellij run` to create panes.
|
If the `ZELLIJ` environment variable is set, clanker-teams uses `zellij run` to create panes.
|
||||||
|
|
||||||
**Layout**: Same as tmux - large lead pane on left, teammates on right.
|
**Layout**: Same as tmux - large lead pane on left, teammates on right.
|
||||||
|
|
||||||
### iTerm2 Detection
|
### iTerm2 Detection
|
||||||
|
|
||||||
If neither tmux nor Zellij is detected, and you're on macOS with iTerm2, companion-teams uses AppleScript to split the window.
|
If neither tmux nor Zellij is detected, and you're on macOS with iTerm2, clanker-teams uses AppleScript to split the window.
|
||||||
|
|
||||||
**Layout**: Same as tmux/Zellij - large lead pane on left, teammates on right.
|
**Layout**: Same as tmux/Zellij - large lead pane on left, teammates on right.
|
||||||
|
|
||||||
|
|
@ -658,12 +658,12 @@ If neither tmux nor Zellij is detected, and you're on macOS with iTerm2, compani
|
||||||
|
|
||||||
### Lock Files
|
### Lock Files
|
||||||
|
|
||||||
companion-teams uses lock files to prevent concurrent modifications:
|
clanker-teams uses lock files to prevent concurrent modifications:
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.companion/teams/<team-name>/.lock
|
~/.clanker/teams/<team-name>/.lock
|
||||||
~/.companion/tasks/<team-name>/.lock
|
~/.clanker/tasks/<team-name>/.lock
|
||||||
~/.companion/messages/<team-name>/.lock
|
~/.clanker/messages/<team-name>/.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
If a lock file is stale (process no longer running), it's automatically removed after 60 seconds.
|
If a lock file is stale (process no longer running), it's automatically removed after 60 seconds.
|
||||||
|
|
@ -678,7 +678,7 @@ If a lock file persists beyond 60 seconds, it's automatically cleaned up. For ma
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Remove stale lock
|
# Remove stale lock
|
||||||
rm ~/.companion/teams/my-team/.lock
|
rm ~/.clanker/teams/my-team/.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -699,5 +699,5 @@ Messages are stored as JSON. For teams with extensive message history, consider
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Archive old messages
|
# Archive old messages
|
||||||
mv ~/.companion/messages/my-team/ ~/.companion/messages-archive/my-team-2024-02-22/
|
mv ~/.clanker/messages/my-team/ ~/.clanker/messages-archive/my-team-2024-02-22/
|
||||||
```
|
```
|
||||||
|
|
@ -269,7 +269,7 @@ While not tested in this research, iTerm2 is known to have:
|
||||||
|
|
||||||
## Recommendations
|
## Recommendations
|
||||||
|
|
||||||
### For the companion-teams Project
|
### For the clanker-teams Project
|
||||||
|
|
||||||
**Primary Recommendation:**
|
**Primary Recommendation:**
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ Prompt:
|
||||||
Test the shell-based hook system. First, create a hook script, then mark a task as completed.
|
Test the shell-based hook system. First, create a hook script, then mark a task as completed.
|
||||||
|
|
||||||
Prompt:
|
Prompt:
|
||||||
"Create a shell script at '.companion/team-hooks/task_completed.sh' that echoes the task ID and status to a file called 'hook_results.txt'. Then, mark task #1 as 'completed' and verify that 'hook_results.txt' has been created."
|
"Create a shell script at '.clanker/team-hooks/task_completed.sh' that echoes the task ID and status to a file called 'hook_results.txt'. Then, mark task #1 as 'completed' and verify that 'hook_results.txt' has been created."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -31,10 +31,10 @@ Prompt:
|
||||||
|
|
||||||
### 4. Test Environment Variable Propagation
|
### 4. Test Environment Variable Propagation
|
||||||
|
|
||||||
Verify that the COMPANION_DEFAULT_THINKING_LEVEL environment variable is correctly set for each spawned process.
|
Verify that the CLANKER_DEFAULT_THINKING_LEVEL environment variable is correctly set for each spawned process.
|
||||||
|
|
||||||
Prompt (run in terminal):
|
Prompt (run in terminal):
|
||||||
"Run 'ps aux | grep COMPANION_DEFAULT_THINKING_LEVEL' to check that the environment variables were passed to the spawned teammate processes."
|
"Run 'ps aux | grep CLANKER_DEFAULT_THINKING_LEVEL' to check that the environment variables were passed to the spawned teammate processes."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ Prompt (run in terminal):
|
||||||
Create tasks appropriate for each teammate's thinking level.
|
Create tasks appropriate for each teammate's thinking level.
|
||||||
|
|
||||||
Prompt:
|
Prompt:
|
||||||
"Create a task for DeepThinker: 'Analyze the companion-teams codebase architecture and suggest improvements for scalability'. Set it to in_progress.
|
"Create a task for DeepThinker: 'Analyze the clanker-teams codebase architecture and suggest improvements for scalability'. Set it to in_progress.
|
||||||
Create a task for FastWorker: 'List all TypeScript files in the src directory'. Set it to in_progress."
|
Create a task for FastWorker: 'List all TypeScript files in the src directory'. Set it to in_progress."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
## Executive Summary
|
## Executive Summary
|
||||||
|
|
||||||
After researching VS Code and Cursor integrated terminal capabilities, **I recommend AGAINST implementing direct VS Code/Cursor terminal support for companion-teams at this time**. The fundamental issue is that VS Code does not provide a command-line API for spawning or managing terminal panes from within an integrated terminal. While a VS Code extension could theoretically provide this functionality, it would require users to install an additional extension and would not work "out of the box" like the current tmux/Zellij/iTerm2 solutions.
|
After researching VS Code and Cursor integrated terminal capabilities, **I recommend AGAINST implementing direct VS Code/Cursor terminal support for clanker-teams at this time**. The fundamental issue is that VS Code does not provide a command-line API for spawning or managing terminal panes from within an integrated terminal. While a VS Code extension could theoretically provide this functionality, it would require users to install an additional extension and would not work "out of the box" like the current tmux/Zellij/iTerm2 solutions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Research Scope
|
## Research Scope
|
||||||
|
|
||||||
This document investigates whether companion-teams can work with VS Code and Cursor integrated terminals, specifically:
|
This document investigates whether clanker-teams can work with VS Code and Cursor integrated terminals, specifically:
|
||||||
|
|
||||||
1. Detecting when running inside VS Code/Cursor integrated terminal
|
1. Detecting when running inside VS Code/Cursor integrated terminal
|
||||||
2. Programmatically creating new terminal instances
|
2. Programmatically creating new terminal instances
|
||||||
|
|
@ -163,8 +163,8 @@ Extensions can register custom terminal profiles:
|
||||||
"terminal": {
|
"terminal": {
|
||||||
"profiles": [
|
"profiles": [
|
||||||
{
|
{
|
||||||
"title": "Companion Teams Terminal",
|
"title": "Clanker Teams Terminal",
|
||||||
"id": "companion-teams-terminal"
|
"id": "clanker-teams-terminal"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -172,10 +172,10 @@ Extensions can register custom terminal profiles:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register provider
|
// Register provider
|
||||||
vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
|
vscode.window.registerTerminalProfileProvider('clanker-teams-terminal', {
|
||||||
provideTerminalProfile(token) {
|
provideTerminalProfile(token) {
|
||||||
return {
|
return {
|
||||||
name: "Companion Teams Agent",
|
name: "Clanker Teams Agent",
|
||||||
shellPath: "bash",
|
shellPath: "bash",
|
||||||
cwd: "/project/path"
|
cwd: "/project/path"
|
||||||
};
|
};
|
||||||
|
|
@ -248,17 +248,17 @@ vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
|
||||||
|
|
||||||
### ⚠️ Approach 4: VS Code Extension (Partial Solution)
|
### ⚠️ Approach 4: VS Code Extension (Partial Solution)
|
||||||
|
|
||||||
**Investigated**: Create a VS Code extension that companion-teams can communicate with
|
**Investigated**: Create a VS Code extension that clanker-teams can communicate with
|
||||||
|
|
||||||
**Feasible Design**:
|
**Feasible Design**:
|
||||||
|
|
||||||
1. companion-teams detects VS Code environment (`TERM_PROGRAM=vscode`)
|
1. clanker-teams detects VS Code environment (`TERM_PROGRAM=vscode`)
|
||||||
2. companion-teams spawns child processes that communicate with the extension
|
2. clanker-teams spawns child processes that communicate with the extension
|
||||||
3. Extension receives requests and creates terminals via VS Code API
|
3. Extension receives requests and creates terminals via VS Code API
|
||||||
|
|
||||||
**Communication Mechanisms**:
|
**Communication Mechanisms**:
|
||||||
|
|
||||||
- **Local WebSocket server**: Extension starts server, companion-teams connects
|
- **Local WebSocket server**: Extension starts server, clanker-teams connects
|
||||||
- **Named pipes/Unix domain sockets**: On Linux/macOS
|
- **Named pipes/Unix domain sockets**: On Linux/macOS
|
||||||
- **File system polling**: Write request files, extension reads them
|
- **File system polling**: Write request files, extension reads them
|
||||||
- **Local HTTP server**: Easier cross-platform
|
- **Local HTTP server**: Easier cross-platform
|
||||||
|
|
@ -267,15 +267,15 @@ vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐
|
┌─────────────┐
|
||||||
│ companion-teams │ ← Running in integrated terminal
|
│ clanker-teams │ ← Running in integrated terminal
|
||||||
│ (node.js) │
|
│ (node.js) │
|
||||||
└──────┬──────┘
|
└──────┬──────┘
|
||||||
│
|
│
|
||||||
│ 1. HTTP POST /create-terminal
|
│ 1. HTTP POST /create-terminal
|
||||||
│ { name: "agent-1", cwd: "/path", command: "companion ..." }
|
│ { name: "agent-1", cwd: "/path", command: "clanker ..." }
|
||||||
↓
|
↓
|
||||||
┌───────────────────────────┐
|
┌───────────────────────────┐
|
||||||
│ companion-teams VS Code Extension │ ← Running in extension host
|
│ clanker-teams VS Code Extension │ ← Running in extension host
|
||||||
│ (TypeScript) │
|
│ (TypeScript) │
|
||||||
└───────┬───────────────────┘
|
└───────┬───────────────────┘
|
||||||
│
|
│
|
||||||
|
|
@ -283,7 +283,7 @@ vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
|
||||||
↓
|
↓
|
||||||
┌───────────────────────────┐
|
┌───────────────────────────┐
|
||||||
│ VS Code Terminal Pane │ ← New terminal created
|
│ VS Code Terminal Pane │ ← New terminal created
|
||||||
│ (running companion) │
|
│ (running clanker) │
|
||||||
└───────────────────────────┘
|
└───────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -298,7 +298,7 @@ vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
|
||||||
|
|
||||||
- ❌ Users must install extension (additional dependency)
|
- ❌ Users must install extension (additional dependency)
|
||||||
- ❌ Extension adds ~5-10MB to install
|
- ❌ Extension adds ~5-10MB to install
|
||||||
- ❌ Extension must be maintained alongside companion-teams
|
- ❌ Extension must be maintained alongside clanker-teams
|
||||||
- ❌ Extension adds startup overhead
|
- ❌ Extension adds startup overhead
|
||||||
- ❌ Extension permissions/security concerns
|
- ❌ Extension permissions/security concerns
|
||||||
- ❌ Not "plug and play" like tmux/Zellij
|
- ❌ Not "plug and play" like tmux/Zellij
|
||||||
|
|
@ -307,7 +307,7 @@ vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Comparison with Existing companion-teams Adapters
|
## 6. Comparison with Existing clanker-teams Adapters
|
||||||
|
|
||||||
| Feature | tmux | Zellij | iTerm2 | VS Code (CLI) | VS Code (Extension) |
|
| Feature | tmux | Zellij | iTerm2 | VS Code (CLI) | VS Code (Extension) |
|
||||||
| ----------------- | ------------------------ | ------------------------- | ------------------------ | --------------------- | ----------------------- |
|
| ----------------- | ------------------------ | ------------------------- | ------------------------ | --------------------- | ----------------------- |
|
||||||
|
|
@ -407,7 +407,7 @@ The fundamental blocker: **VS Code provides no command-line or shell interface f
|
||||||
1. **No native CLI support**: VS Code provides no command-line API for terminal management
|
1. **No native CLI support**: VS Code provides no command-line API for terminal management
|
||||||
2. **Extension required**: Would require users to install and configure an extension
|
2. **Extension required**: Would require users to install and configure an extension
|
||||||
3. **User friction**: Adds setup complexity vs. "just use tmux"
|
3. **User friction**: Adds setup complexity vs. "just use tmux"
|
||||||
4. **Maintenance burden**: Extension must be maintained alongside companion-teams
|
4. **Maintenance burden**: Extension must be maintained alongside clanker-teams
|
||||||
5. **Limited benefit**: Users can simply run `tmux` inside VS Code integrated terminal
|
5. **Limited benefit**: Users can simply run `tmux` inside VS Code integrated terminal
|
||||||
6. **Alternative exists**: tmux/Zellij work perfectly fine inside VS Code terminals
|
6. **Alternative exists**: tmux/Zellij work perfectly fine inside VS Code terminals
|
||||||
|
|
||||||
|
|
@ -417,9 +417,9 @@ The fundamental blocker: **VS Code provides no command-line or shell interface f
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Option 1: Run tmux inside VS Code integrated terminal
|
# Option 1: Run tmux inside VS Code integrated terminal
|
||||||
tmux new -s companion-teams
|
tmux new -s clanker-teams
|
||||||
companion create-team my-team
|
clanker create-team my-team
|
||||||
companion spawn-teammate ...
|
clanker spawn-teammate ...
|
||||||
|
|
||||||
# Option 2: Start tmux from terminal, then open VS Code
|
# Option 2: Start tmux from terminal, then open VS Code
|
||||||
tmux new -s my-session
|
tmux new -s my-session
|
||||||
|
|
@ -445,27 +445,27 @@ If there's strong user demand for native VS Code integration:
|
||||||
#### Architecture
|
#### Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
1. companion-teams detects VS Code (TERM_PROGRAM=vscode)
|
1. clanker-teams detects VS Code (TERM_PROGRAM=vscode)
|
||||||
|
|
||||||
2. companion-teams spawns a lightweight HTTP server
|
2. clanker-teams spawns a lightweight HTTP server
|
||||||
- Port: Random free port (e.g., 34567)
|
- Port: Random free port (e.g., 34567)
|
||||||
- Endpoint: POST /create-terminal
|
- Endpoint: POST /create-terminal
|
||||||
- Payload: { name, cwd, command, env }
|
- Payload: { name, cwd, command, env }
|
||||||
|
|
||||||
3. User installs "companion-teams" VS Code extension
|
3. User installs "clanker-teams" VS Code extension
|
||||||
- Extension starts HTTP client on activation
|
- Extension starts HTTP client on activation
|
||||||
- Finds companion-teams server port via shared file or env var
|
- Finds clanker-teams server port via shared file or env var
|
||||||
|
|
||||||
4. Extension receives create-terminal requests
|
4. Extension receives create-terminal requests
|
||||||
- Calls vscode.window.createTerminal()
|
- Calls vscode.window.createTerminal()
|
||||||
- Returns terminal ID
|
- Returns terminal ID
|
||||||
|
|
||||||
5. companion-teams tracks terminal IDs via extension responses
|
5. clanker-teams tracks terminal IDs via extension responses
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Implementation Sketch
|
#### Implementation Sketch
|
||||||
|
|
||||||
**companion-teams (TypeScript)**:
|
**clanker-teams (TypeScript)**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class VSCodeAdapter implements TerminalAdapter {
|
class VSCodeAdapter implements TerminalAdapter {
|
||||||
|
|
@ -482,7 +482,7 @@ class VSCodeAdapter implements TerminalAdapter {
|
||||||
// Write request file
|
// Write request file
|
||||||
const requestId = uuidv4();
|
const requestId = uuidv4();
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
`/tmp/companion-teams-request-${requestId}.json`,
|
`/tmp/clanker-teams-request-${requestId}.json`,
|
||||||
JSON.stringify({ ...options, requestId }),
|
JSON.stringify({ ...options, requestId }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -514,7 +514,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
|
|
||||||
// Watch for request files
|
// Watch for request files
|
||||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
const watcher = vscode.workspace.createFileSystemWatcher(
|
||||||
"/tmp/companion-teams-request-*.json",
|
"/tmp/clanker-teams-request-*.json",
|
||||||
);
|
);
|
||||||
|
|
||||||
watcher.onDidChange(async (uri) => {
|
watcher.onDidChange(async (uri) => {
|
||||||
|
|
@ -564,7 +564,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
|
|
||||||
### Could We Detect Existing Terminal Tabs?
|
### Could We Detect Existing Terminal Tabs?
|
||||||
|
|
||||||
**Investigated**: Can companion-teams detect existing VS Code terminal tabs and use them?
|
**Investigated**: Can clanker-teams detect existing VS Code terminal tabs and use them?
|
||||||
|
|
||||||
**Findings**:
|
**Findings**:
|
||||||
|
|
||||||
|
|
@ -639,9 +639,9 @@ export class VSCodeAdapter implements TerminalAdapter {
|
||||||
spawn(options: SpawnOptions): string {
|
spawn(options: SpawnOptions): string {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"VS Code integrated terminals do not support spawning " +
|
"VS Code integrated terminals do not support spawning " +
|
||||||
"new terminals from command line. Please run companion-teams " +
|
"new terminals from command line. Please run clanker-teams " +
|
||||||
"inside tmux, Zellij, or iTerm2 for terminal management. " +
|
"inside tmux, Zellij, or iTerm2 for terminal management. " +
|
||||||
"Alternatively, install the companion-teams VS Code extension " +
|
"Alternatively, install the clanker-teams VS Code extension " +
|
||||||
"(if implemented).",
|
"(if implemented).",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -665,22 +665,22 @@ export class VSCodeAdapter implements TerminalAdapter {
|
||||||
```
|
```
|
||||||
❌ Cannot spawn terminal in VS Code integrated terminal
|
❌ Cannot spawn terminal in VS Code integrated terminal
|
||||||
|
|
||||||
companion-teams requires a terminal multiplexer to create multiple panes.
|
clanker-teams requires a terminal multiplexer to create multiple panes.
|
||||||
|
|
||||||
For VS Code users, we recommend one of these options:
|
For VS Code users, we recommend one of these options:
|
||||||
|
|
||||||
Option 1: Run tmux inside VS Code integrated terminal
|
Option 1: Run tmux inside VS Code integrated terminal
|
||||||
┌────────────────────────────────────────┐
|
┌────────────────────────────────────────┐
|
||||||
│ $ tmux new -s companion-teams │
|
│ $ tmux new -s clanker-teams │
|
||||||
│ $ companion create-team my-team │
|
│ $ clanker create-team my-team │
|
||||||
│ $ companion spawn-teammate security-bot ... │
|
│ $ clanker spawn-teammate security-bot ... │
|
||||||
└────────────────────────────────────────┘
|
└────────────────────────────────────────┘
|
||||||
|
|
||||||
Option 2: Open VS Code from tmux session
|
Option 2: Open VS Code from tmux session
|
||||||
┌────────────────────────────────────────┐
|
┌────────────────────────────────────────┐
|
||||||
│ $ tmux new -s my-session │
|
│ $ tmux new -s my-session │
|
||||||
│ $ code . │
|
│ $ code . │
|
||||||
│ $ companion create-team my-team │
|
│ $ clanker create-team my-team │
|
||||||
└────────────────────────────────────────┘
|
└────────────────────────────────────────┘
|
||||||
|
|
||||||
Option 3: Use a terminal with multiplexer support
|
Option 3: Use a terminal with multiplexer support
|
||||||
|
|
@ -690,7 +690,7 @@ Option 3: Use a terminal with multiplexer support
|
||||||
│ • Zellij - Install: cargo install ... │
|
│ • Zellij - Install: cargo install ... │
|
||||||
└────────────────────────────────────────┘
|
└────────────────────────────────────────┘
|
||||||
|
|
||||||
Learn more: https://github.com/your-org/companion-teams#terminal-support
|
Learn more: https://github.com/your-org/clanker-teams#terminal-support
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -717,7 +717,7 @@ For VS Code/Cursor users, recommend:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Option 1: Run tmux inside VS Code (simplest)
|
# Option 1: Run tmux inside VS Code (simplest)
|
||||||
tmux new -s companion-teams
|
tmux new -s clanker-teams
|
||||||
|
|
||||||
# Option 2: Start tmux first, then open VS Code
|
# Option 2: Start tmux first, then open VS Code
|
||||||
tmux new -s dev
|
tmux new -s dev
|
||||||
|
|
@ -726,19 +726,19 @@ code .
|
||||||
|
|
||||||
### Documentation Update
|
### Documentation Update
|
||||||
|
|
||||||
Add to companion-teams README.md:
|
Add to clanker-teams README.md:
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
## Using companion-teams with VS Code or Cursor
|
## Using clanker-teams with VS Code or Cursor
|
||||||
|
|
||||||
companion-teams works great with VS Code and Cursor! Simply run tmux
|
clanker-teams works great with VS Code and Cursor! Simply run tmux
|
||||||
or Zellij inside the integrated terminal:
|
or Zellij inside the integrated terminal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start tmux in VS Code integrated terminal
|
# Start tmux in VS Code integrated terminal
|
||||||
$ tmux new -s companion-teams
|
$ tmux new -s clanker-teams
|
||||||
$ companion create-team my-team
|
$ clanker create-team my-team
|
||||||
$ companion spawn-teammate security-bot "Scan for vulnerabilities"
|
$ clanker spawn-teammate security-bot "Scan for vulnerabilities"
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
|
|
@ -898,12 +898,12 @@ process.env.TERM_PROGRAM === 'iTerm.app'
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Step 1: Start tmux
|
# Step 1: Start tmux
|
||||||
tmux new -s companion-teams
|
tmux new -s clanker-teams
|
||||||
|
|
||||||
# Step 2: Use companion-teams
|
# Step 2: Use clanker-teams
|
||||||
companion create-team my-team
|
clanker create-team my-team
|
||||||
companion spawn-teammate frontend-dev
|
clanker spawn-teammate frontend-dev
|
||||||
companion spawn-teammate backend-dev
|
clanker spawn-teammate backend-dev
|
||||||
|
|
||||||
# Step 3: Enjoy multi-pane coordination
|
# Step 3: Enjoy multi-pane coordination
|
||||||
┌──────────────────┬──────────────────┬──────────────────┐
|
┌──────────────────┬──────────────────┬──────────────────┐
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { ExtensionAPI } from "@mariozechner/companion-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/clanker-coding-agent";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { StringEnum } from "@mariozechner/companion-ai";
|
import { StringEnum } from "@mariozechner/clanker-ai";
|
||||||
import * as paths from "../src/utils/paths";
|
import * as paths from "../src/utils/paths";
|
||||||
import * as teams from "../src/utils/teams";
|
import * as teams from "../src/utils/teams";
|
||||||
import * as tasks from "../src/utils/tasks";
|
import * as tasks from "../src/utils/tasks";
|
||||||
|
|
@ -19,7 +19,7 @@ let modelsCacheTime = 0;
|
||||||
const MODELS_CACHE_TTL = 60000; // 1 minute
|
const MODELS_CACHE_TTL = 60000; // 1 minute
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query available models from companion --list-models
|
* Query available models from clanker --list-models
|
||||||
*/
|
*/
|
||||||
function getAvailableModels(): Array<{ provider: string; model: string }> {
|
function getAvailableModels(): Array<{ provider: string; model: string }> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -28,7 +28,7 @@ function getAvailableModels(): Array<{ provider: string; model: string }> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = spawnSync("companion", ["--list-models"], {
|
const result = spawnSync("clanker", ["--list-models"], {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
@ -142,14 +142,14 @@ function resolveModelWithProvider(modelName: string): string | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (companion: ExtensionAPI) {
|
export default function (clanker: ExtensionAPI) {
|
||||||
const isTeammate = !!process.env.COMPANION_AGENT_NAME;
|
const isTeammate = !!process.env.CLANKER_AGENT_NAME;
|
||||||
const agentName = process.env.COMPANION_AGENT_NAME || "team-lead";
|
const agentName = process.env.CLANKER_AGENT_NAME || "team-lead";
|
||||||
const teamName = process.env.COMPANION_TEAM_NAME;
|
const teamName = process.env.CLANKER_TEAM_NAME;
|
||||||
|
|
||||||
const terminal = getTerminalAdapter();
|
const terminal = getTerminalAdapter();
|
||||||
|
|
||||||
companion.on("session_start", async (_event, ctx) => {
|
clanker.on("session_start", async (_event, ctx) => {
|
||||||
paths.ensureDirs();
|
paths.ensureDirs();
|
||||||
if (isTeammate) {
|
if (isTeammate) {
|
||||||
if (teamName) {
|
if (teamName) {
|
||||||
|
|
@ -157,7 +157,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
fs.writeFileSync(pidFile, process.pid.toString());
|
fs.writeFileSync(pidFile, process.pid.toString());
|
||||||
}
|
}
|
||||||
ctx.ui.notify(`Teammate: ${agentName} (Team: ${teamName})`, "info");
|
ctx.ui.notify(`Teammate: ${agentName} (Team: ${teamName})`, "info");
|
||||||
ctx.ui.setStatus("00-companion-teams", `[${agentName.toUpperCase()}]`);
|
ctx.ui.setStatus("00-clanker-teams", `[${agentName.toUpperCase()}]`);
|
||||||
|
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
|
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
|
||||||
|
|
@ -172,7 +172,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
companion.sendUserMessage(
|
clanker.sendUserMessage(
|
||||||
`I am starting my work as '${agentName}' on team '${teamName}'. Checking my inbox for instructions...`,
|
`I am starting my work as '${agentName}' on team '${teamName}'. Checking my inbox for instructions...`,
|
||||||
);
|
);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
@ -186,18 +186,18 @@ export default function (companion: ExtensionAPI) {
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
if (unread.length > 0) {
|
if (unread.length > 0) {
|
||||||
companion.sendUserMessage(
|
clanker.sendUserMessage(
|
||||||
`I have ${unread.length} new message(s) in my inbox. Reading them now...`,
|
`I have ${unread.length} new message(s) in my inbox. Reading them now...`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
} else if (teamName) {
|
} else if (teamName) {
|
||||||
ctx.ui.setStatus("companion-teams", `Lead @ ${teamName}`);
|
ctx.ui.setStatus("clanker-teams", `Lead @ ${teamName}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.on("turn_start", async (_event, ctx) => {
|
clanker.on("turn_start", async (_event, ctx) => {
|
||||||
if (isTeammate) {
|
if (isTeammate) {
|
||||||
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
|
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
|
||||||
if ((ctx.ui as any).setTitle) (ctx.ui as any).setTitle(fullTitle);
|
if ((ctx.ui as any).setTitle) (ctx.ui as any).setTitle(fullTitle);
|
||||||
|
|
@ -206,7 +206,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
});
|
});
|
||||||
|
|
||||||
let firstTurn = true;
|
let firstTurn = true;
|
||||||
companion.on("before_agent_start", async (event, ctx) => {
|
clanker.on("before_agent_start", async (event, ctx) => {
|
||||||
if (isTeammate && firstTurn) {
|
if (isTeammate && firstTurn) {
|
||||||
firstTurn = false;
|
firstTurn = false;
|
||||||
|
|
||||||
|
|
@ -259,7 +259,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tools
|
// Tools
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "team_create",
|
name: "team_create",
|
||||||
label: "Create Team",
|
label: "Create Team",
|
||||||
description: "Create a new agent team.",
|
description: "Create a new agent team.",
|
||||||
|
|
@ -290,7 +290,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "spawn_teammate",
|
name: "spawn_teammate",
|
||||||
label: "Spawn Teammate",
|
label: "Spawn Teammate",
|
||||||
description: "Spawn a new teammate in a terminal pane or separate window.",
|
description: "Spawn a new teammate in a terminal pane or separate window.",
|
||||||
|
|
@ -324,7 +324,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
// Resolve model to provider/model format
|
// Resolve model to provider/model format
|
||||||
if (chosenModel) {
|
if (chosenModel) {
|
||||||
if (!chosenModel.includes("/")) {
|
if (!chosenModel.includes("/")) {
|
||||||
// Try to resolve using available models from companion --list-models
|
// Try to resolve using available models from clanker --list-models
|
||||||
const resolved = resolveModelWithProvider(chosenModel);
|
const resolved = resolveModelWithProvider(chosenModel);
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
chosenModel = resolved;
|
chosenModel = resolved;
|
||||||
|
|
@ -371,7 +371,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
"Initial prompt",
|
"Initial prompt",
|
||||||
);
|
);
|
||||||
|
|
||||||
const piBinary = "companion";
|
const piBinary = "clanker";
|
||||||
let piCmd = piBinary;
|
let piCmd = piBinary;
|
||||||
|
|
||||||
if (chosenModel) {
|
if (chosenModel) {
|
||||||
|
|
@ -387,8 +387,8 @@ export default function (companion: ExtensionAPI) {
|
||||||
|
|
||||||
const env: Record<string, string> = {
|
const env: Record<string, string> = {
|
||||||
...process.env,
|
...process.env,
|
||||||
COMPANION_TEAM_NAME: safeTeamName,
|
CLANKER_TEAM_NAME: safeTeamName,
|
||||||
COMPANION_AGENT_NAME: safeName,
|
CLANKER_AGENT_NAME: safeName,
|
||||||
};
|
};
|
||||||
|
|
||||||
let terminalId = "";
|
let terminalId = "";
|
||||||
|
|
@ -452,7 +452,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "spawn_lead_window",
|
name: "spawn_lead_window",
|
||||||
label: "Spawn Lead Window",
|
label: "Spawn Lead Window",
|
||||||
description: "Open the team lead in a separate OS window.",
|
description: "Open the team lead in a separate OS window.",
|
||||||
|
|
@ -469,7 +469,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
|
|
||||||
const teamConfig = await teams.readConfig(safeTeamName);
|
const teamConfig = await teams.readConfig(safeTeamName);
|
||||||
const cwd = params.cwd || process.cwd();
|
const cwd = params.cwd || process.cwd();
|
||||||
const piBinary = "companion";
|
const piBinary = "clanker";
|
||||||
let piCmd = piBinary;
|
let piCmd = piBinary;
|
||||||
if (teamConfig.defaultModel) {
|
if (teamConfig.defaultModel) {
|
||||||
// Use the combined --model provider/model format
|
// Use the combined --model provider/model format
|
||||||
|
|
@ -478,8 +478,8 @@ export default function (companion: ExtensionAPI) {
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
COMPANION_TEAM_NAME: safeTeamName,
|
CLANKER_TEAM_NAME: safeTeamName,
|
||||||
COMPANION_AGENT_NAME: "team-lead",
|
CLANKER_AGENT_NAME: "team-lead",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const windowId = terminal.spawnWindow({
|
const windowId = terminal.spawnWindow({
|
||||||
|
|
@ -500,7 +500,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "send_message",
|
name: "send_message",
|
||||||
label: "Send Message",
|
label: "Send Message",
|
||||||
description: "Send a message to a teammate.",
|
description: "Send a message to a teammate.",
|
||||||
|
|
@ -527,7 +527,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "broadcast_message",
|
name: "broadcast_message",
|
||||||
label: "Broadcast Message",
|
label: "Broadcast Message",
|
||||||
description: "Broadcast a message to all team members except the sender.",
|
description: "Broadcast a message to all team members except the sender.",
|
||||||
|
|
@ -554,7 +554,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "read_inbox",
|
name: "read_inbox",
|
||||||
label: "Read Inbox",
|
label: "Read Inbox",
|
||||||
description: "Read messages from an agent's inbox.",
|
description: "Read messages from an agent's inbox.",
|
||||||
|
|
@ -581,7 +581,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "task_create",
|
name: "task_create",
|
||||||
label: "Create Task",
|
label: "Create Task",
|
||||||
description: "Create a new team task.",
|
description: "Create a new team task.",
|
||||||
|
|
@ -603,7 +603,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "task_submit_plan",
|
name: "task_submit_plan",
|
||||||
label: "Submit Plan",
|
label: "Submit Plan",
|
||||||
description: "Submit a plan for a task, updating its status to 'planning'.",
|
description: "Submit a plan for a task, updating its status to 'planning'.",
|
||||||
|
|
@ -627,7 +627,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "task_evaluate_plan",
|
name: "task_evaluate_plan",
|
||||||
label: "Evaluate Plan",
|
label: "Evaluate Plan",
|
||||||
description: "Evaluate a submitted plan for a task.",
|
description: "Evaluate a submitted plan for a task.",
|
||||||
|
|
@ -658,7 +658,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "task_list",
|
name: "task_list",
|
||||||
label: "List Tasks",
|
label: "List Tasks",
|
||||||
description: "List all tasks for a team.",
|
description: "List all tasks for a team.",
|
||||||
|
|
@ -674,7 +674,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "task_update",
|
name: "task_update",
|
||||||
label: "Update Task",
|
label: "Update Task",
|
||||||
description: "Update a task's status or owner.",
|
description: "Update a task's status or owner.",
|
||||||
|
|
@ -704,7 +704,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "team_shutdown",
|
name: "team_shutdown",
|
||||||
label: "Shutdown Team",
|
label: "Shutdown Team",
|
||||||
description: "Shutdown the entire team and close all panes/windows.",
|
description: "Shutdown the entire team and close all panes/windows.",
|
||||||
|
|
@ -732,7 +732,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "task_read",
|
name: "task_read",
|
||||||
label: "Read Task",
|
label: "Read Task",
|
||||||
description: "Read details of a specific task.",
|
description: "Read details of a specific task.",
|
||||||
|
|
@ -749,7 +749,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "check_teammate",
|
name: "check_teammate",
|
||||||
label: "Check Teammate",
|
label: "Check Teammate",
|
||||||
description: "Check a single teammate's status.",
|
description: "Check a single teammate's status.",
|
||||||
|
|
@ -789,7 +789,7 @@ export default function (companion: ExtensionAPI) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
companion.registerTool({
|
clanker.registerTool({
|
||||||
name: "process_shutdown_approved",
|
name: "process_shutdown_approved",
|
||||||
label: "Process Shutdown Approved",
|
label: "Process Shutdown Approved",
|
||||||
description: "Process a teammate's shutdown.",
|
description: "Process a teammate's shutdown.",
|
||||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
|
@ -1,15 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "companion-teams",
|
"name": "clanker-teams",
|
||||||
"version": "0.8.6",
|
"version": "0.8.6",
|
||||||
"description": "Agent teams for companion, ported from claude-code-teams-mcp",
|
"description": "Agent teams for clanker, ported from claude-code-teams-mcp",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/burggraf/companion-teams.git"
|
"url": "git+https://github.com/harivansh-afk/clanker-agent.git",
|
||||||
|
"directory": "packages/clanker-teams"
|
||||||
},
|
},
|
||||||
"author": "Mark Burggraf",
|
"author": "Mark Burggraf",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"companion-package"
|
"clanker-package"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
|
|
@ -26,11 +27,11 @@
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mariozechner/companion-coding-agent": "*",
|
"@harivansh-afk/clanker-coding-agent": "*",
|
||||||
"@sinclair/typebox": "*"
|
"@sinclair/typebox": "*"
|
||||||
},
|
},
|
||||||
"companion": {
|
"clanker": {
|
||||||
"image": "https://raw.githubusercontent.com/burggraf/companion-teams/main/companion-team-in-action.png",
|
"image": "https://raw.githubusercontent.com/burggraf/clanker-teams/main/clanker-team-in-action.png",
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"extensions/index.ts"
|
"extensions/index.ts"
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
|
@ -26,7 +26,7 @@ export class CmuxAdapter implements TerminalAdapter {
|
||||||
|
|
||||||
// Construct the command with environment variables
|
// Construct the command with environment variables
|
||||||
const envPrefix = Object.entries(options.env)
|
const envPrefix = Object.entries(options.env)
|
||||||
.filter(([k]) => k.startsWith("COMPANION_"))
|
.filter(([k]) => k.startsWith("CLANKER_"))
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
|
@ -124,7 +124,7 @@ export class CmuxAdapter implements TerminalAdapter {
|
||||||
// Wait a bit for the window to be ready?
|
// Wait a bit for the window to be ready?
|
||||||
|
|
||||||
const envPrefix = Object.entries(options.env)
|
const envPrefix = Object.entries(options.env)
|
||||||
.filter(([k]) => k.startsWith("COMPANION_"))
|
.filter(([k]) => k.startsWith("CLANKER_"))
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue