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:
Harivansh Rathi 2026-03-26 16:22:52 -04:00
parent f93fe7d1a0
commit 67168d8289
356 changed files with 2249 additions and 10223 deletions

4
.gitignore vendored
View file

@ -23,13 +23,13 @@ packages/*/dist-firefox/
.npm/
coverage/
.nyc_output/
.companion_config/
.clanker_config/
tui-debug.log
compaction-results/
.opencode/
syntax.jsonl
out.jsonl
companion-*.html
clanker-*.html
out.html
packages/coding-agent/binaries/
todo.md

View file

@ -71,29 +71,29 @@ When closing issues via commit:
- 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
## 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
# 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
tmux send-keys -t companion-test "cd /Users/badlogic/workspaces/companion-mono && ./companion-test.sh" Enter
# Start clanker from source
tmux send-keys -t clanker-test "cd /Users/badlogic/workspaces/clanker-agent && ./clanker-test.sh" Enter
# 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
tmux send-keys -t companion-test "your prompt here" Enter
tmux send-keys -t clanker-test "your prompt here" Enter
# Send special keys
tmux send-keys -t companion-test Escape
tmux send-keys -t companion-test C-o # ctrl+o
tmux send-keys -t clanker-test Escape
tmux send-keys -t clanker-test C-o # ctrl+o
# Cleanup
tmux kill-session -t companion-test
tmux kill-session -t clanker-test
```
## Style
@ -127,8 +127,8 @@ Use these sections under `## [Unreleased]`:
### Attribution
- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/badlogic/companion-mono/issues/123))`
- **External contributions**: `Added feature X ([#456](https://github.com/badlogic/companion-mono/pull/456) by [@username](https://github.com/username))`
- **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/clanker-agent/pull/456) by [@username](https://github.com/username))`
## Adding a New LLM Provider (packages/ai)

View file

@ -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.
@ -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.
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
@ -35,7 +35,7 @@ If you're adding a new provider to `packages/ai`, see `AGENTS.md` for required t
## 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?

View file

@ -1,33 +1,33 @@
<p align="center">
<a href="https://shittycodingagent.ai">
<img src="https://shittycodingagent.ai/logo.svg" alt="companion logo" width="128">
<a href="https://clanker.dev">
<img src="https://clanker.dev/logo.svg" alt="clanker logo" width="128">
</a>
</p>
<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://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 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 />
<a href="https://exe.dev"><img src="packages/coding-agent/docs/images/exy.png" alt="Exy mascot" width="48" /><br />exe.dev</a>
</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
| Package | Description |
| ---------------------------------------------------------- | ---------------------------------------------------------------- |
| **[@mariozechner/companion-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/companion-coding-agent](packages/coding-agent)** | Interactive coding agent CLI |
| **[@mariozechner/companion-tui](packages/tui)** | Terminal UI library with differential rendering |
| **[@mariozechner/companion-web-ui](packages/web-ui)** | Web components for AI chat interfaces |
| **[@mariozechner/clanker-ai](packages/ai)** | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
| **[@mariozechner/clanker-agent-core](packages/agent)** | Agent runtime with tool calling and state management |
| **[@mariozechner/clanker-coding-agent](packages/coding-agent)** | Interactive coding agent CLI |
| **[@mariozechner/clanker-tui](packages/tui)** | Terminal UI library with differential rendering |
| **[@mariozechner/clanker-web-ui](packages/web-ui)** | Web components for AI chat interfaces |
## 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.
```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):
```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:
- Downloads the latest release (or falls back to source when needed),
- writes `~/.local/bin/companion` launcher,
- populates `~/.companion/agent/settings.json` with package list,
- writes `~/.local/bin/clanker` launcher,
- populates `~/.clanker/agent/settings.json` with package list,
- 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:
```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.
@ -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.
```bash
COMPANION_FALLBACK_TO_SOURCE=0 \
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/co-mono/main/public-install.sh | bash -s -- --daemon --start
CLANKER_FALLBACK_TO_SOURCE=0 \
curl -fsSL https://raw.githubusercontent.com/harivansh-afk/clanker-agent/main/public-install.sh | bash -s -- --daemon --start
```
`public-install.sh` options:
```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)
```bash
git clone https://github.com/getcompanion-ai/co-mono.git
cd co-mono
git clone https://github.com/harivansh-afk/clanker-agent.git
cd clanker-agent
./install.sh
```
Run:
```bash
./companion
./clanker
```
Run in background with extensions active:
```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
[Unit]
Description=companion daemon
Description=clanker daemon
After=network-online.target
[Service]
Type=simple
Environment=CO_MONO_AGENT_DIR=%h/.companion/agent
Environment=COMPANION_CODING_AGENT_DIR=%h/.companion/agent
ExecStart=/absolute/path/to/repo/companion daemon
Environment=CLANKER_MONO_AGENT_DIR=%h/.clanker/agent
Environment=CLANKER_CODING_AGENT_DIR=%h/.clanker/agent
ExecStart=/absolute/path/to/repo/clanker daemon
Restart=always
RestartSec=5
@ -121,7 +121,7 @@ Then enable:
```bash
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.
@ -140,7 +140,7 @@ npm install # Install all dependencies
npm run build # Build all packages
npm run check # Lint, format, and type check
./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.

View file

@ -1,7 +1,7 @@
{
"folders": [
{
"name": "companion",
"name": "clanker-agent",
"path": "."
},
{

View file

@ -24,12 +24,12 @@ need npm
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"
npm install
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"
BUILD_FAILED=0
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."
fi
LAUNCHER="$ROOT_DIR/companion"
LAUNCHER="$ROOT_DIR/clanker"
cat > "$LAUNCHER" <<'EOF'
#!/usr/bin/env bash
@ -54,8 +54,8 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -x "$ROOT_DIR/packages/coding-agent/dist/companion" ]]; then
exec "$ROOT_DIR/packages/coding-agent/dist/companion" "$@"
if [[ -x "$ROOT_DIR/packages/coding-agent/dist/clanker" ]]; then
exec "$ROOT_DIR/packages/coding-agent/dist/clanker" "$@"
fi
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" "$@"
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
EOF
chmod +x "$LAUNCHER"
log "Created launcher: $LAUNCHER"
log "Run with: ./companion"
log "Run with: ./clanker"

7975
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
{
"name": "companion",
"name": "clanker-agent",
"private": true,
"type": "module",
"homepage": "https://github.com/getcompanion-ai/co-mono#readme",
"homepage": "https://github.com/harivansh-afk/clanker-agent#readme",
"bugs": {
"url": "https://github.com/getcompanion-ai/co-mono/issues"
"url": "https://github.com/harivansh-afk/clanker-agent/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/getcompanion-ai/co-mono.git"
"url": "git+https://github.com/harivansh-afk/clanker-agent.git"
},
"workspaces": [
"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:tsc": "cd packages/ai && npm run dev:tsc",
"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",
"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",
@ -49,7 +49,7 @@
"version": "0.0.3",
"dependencies": {
"@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"
},
"overrides": {

View file

@ -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
```bash
npm install @mariozechner/companion-agent-core
npm install @mariozechner/clanker-agent-core
```
## Quick Start
```typescript
import { Agent } from "@mariozechner/companion-agent-core";
import { getModel } from "@mariozechner/companion-ai";
import { Agent } from "@mariozechner/clanker-agent-core";
import { getModel } from "@mariozechner/clanker-ai";
const agent = new Agent({
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:
```typescript
declare module "@mariozechner/companion-agent-core" {
declare module "@mariozechner/clanker-agent-core" {
interface CustomAgentMessages {
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:
```typescript
import { Agent, streamProxy } from "@mariozechner/companion-agent-core";
import { Agent, streamProxy } from "@mariozechner/clanker-agent-core";
const agent = new Agent({
streamFn: (model, context, options) =>
@ -395,7 +395,7 @@ const agent = new Agent({
For direct control without the Agent class:
```typescript
import { agentLoop, agentLoopContinue } from "@mariozechner/companion-agent-core";
import { agentLoop, agentLoopContinue } from "@mariozechner/clanker-agent-core";
const context: AgentContext = {
systemPrompt: "You are helpful.",

View file

@ -1,5 +1,5 @@
{
"name": "@mariozechner/companion-agent-core",
"name": "@harivansh-afk/clanker-agent-core",
"version": "0.56.2",
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module",
@ -17,7 +17,7 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/companion-ai": "^0.56.2"
"@harivansh-afk/clanker-ai": "^0.56.2"
},
"keywords": [
"ai",
@ -30,7 +30,7 @@
"license": "MIT",
"repository": {
"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"
},
"engines": {

View file

@ -10,7 +10,7 @@ import {
streamSimple,
type ToolResultMessage,
validateToolArguments,
} from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
import type {
AgentContext,
AgentEvent,

View file

@ -12,7 +12,7 @@ import {
type TextContent,
type ThinkingBudgets,
type Transport,
} from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
import { agentLoop, agentLoopContinue } from "./agent-loop.js";
import type {
AgentContext,

View file

@ -14,7 +14,7 @@ import {
type SimpleStreamOptions,
type StopReason,
type ToolCall,
} from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
// Create stream class matching ProxyMessageEventStream
class ProxyMessageEventStream extends EventStream<

View file

@ -8,7 +8,7 @@ import type {
TextContent,
Tool,
ToolResultMessage,
} from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
import type { Static, TSchema } from "@sinclair/typebox";
/** Stream function - can return sync or Promise for async config lookup */

View file

@ -5,7 +5,7 @@ import {
type Message,
type Model,
type UserMessage,
} from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
import { Type } from "@sinclair/typebox";
import { describe, expect, it } from "vitest";
import { agentLoop, agentLoopContinue } from "../src/agent-loop.js";

View file

@ -3,7 +3,7 @@ import {
type AssistantMessageEvent,
EventStream,
getModel,
} from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
import { describe, expect, it } from "vitest";
import { Agent } from "../src/index.js";

View file

@ -9,7 +9,7 @@
*
* You can run this test suite with:
* ```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
@ -21,8 +21,8 @@
* 5. **Invalid Signature Format**: Model validates signature format (Anthropic newer models).
*/
import type { AssistantMessage } from "@mariozechner/companion-ai";
import { getModels } from "@mariozechner/companion-ai";
import type { AssistantMessage } from "@mariozechner/clanker-ai";
import { getModels } from "@mariozechner/clanker-ai";
import { describe, expect, it } from "vitest";
import { Agent } from "../src/index.js";
import { hasBedrockCredentials } from "./bedrock-utils.js";

View file

@ -3,8 +3,8 @@ import type {
Model,
ToolResultMessage,
UserMessage,
} from "@mariozechner/companion-ai";
import { getModel } from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
import { getModel } from "@mariozechner/clanker-ai";
import { describe, expect, it } from "vitest";
import { Agent } from "../src/index.js";
import { hasBedrockCredentials } from "./bedrock-utils.js";

View file

@ -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.
@ -72,10 +72,10 @@ Unified LLM API with automatic model discovery, provider configuration, token an
## Installation
```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
@ -88,7 +88,7 @@ import {
Context,
Tool,
StringEnum,
} from "@mariozechner/companion-ai";
} from "@mariozechner/clanker-ai";
// Fully typed with auto-complete support for both providers and models
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
```typescript
import { Type, Tool, StringEnum } from "@mariozechner/companion-ai";
import { Type, Tool, StringEnum } from "@mariozechner/clanker-ai";
// Define tool parameters with TypeBox
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:
```typescript
import { stream, validateToolCall, Tool } from "@mariozechner/companion-ai";
import { stream, validateToolCall, Tool } from "@mariozechner/clanker-ai";
const tools: Tool[] = [weatherTool, calculatorTool];
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
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");
@ -449,7 +449,7 @@ Many models support thinking/reasoning capabilities where they can show their in
### Unified Interface (streamSimple/completeSimple)
```typescript
import { getModel, streamSimple, completeSimple } from "@mariozechner/companion-ai";
import { getModel, streamSimple, completeSimple } from "@mariozechner/clanker-ai";
// Many models across providers support thinking/reasoning
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:
```typescript
import { getModel, complete } from "@mariozechner/companion-ai";
import { getModel, complete } from "@mariozechner/clanker-ai";
// OpenAI Reasoning (o1, o3, gpt-5)
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'`:
```typescript
import { getModel, stream } from "@mariozechner/companion-ai";
import { getModel, stream } from "@mariozechner/clanker-ai";
const model = getModel("openai", "gpt-4o-mini");
const controller = new AbortController();
@ -682,7 +682,7 @@ A **provider** offers models through a specific API. For example:
### Querying Providers and Models
```typescript
import { getProviders, getModels, getModel } from "@mariozechner/companion-ai";
import { getProviders, getModels, getModel } from "@mariozechner/clanker-ai";
// Get all available providers
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:
```typescript
import { Model, stream } from "@mariozechner/companion-ai";
import { Model, stream } from "@mariozechner/clanker-ai";
// Example: Ollama using OpenAI-compatible API
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.
```typescript
import { streamAnthropic, type AnthropicOptions } from "@mariozechner/companion-ai";
import { streamAnthropic, type AnthropicOptions } from "@mariozechner/clanker-ai";
// TypeScript knows this is an Anthropic model
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
```typescript
import { getModel, complete, Context } from "@mariozechner/companion-ai";
import { getModel, complete, Context } from "@mariozechner/clanker-ai";
// Start with Claude
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:
```typescript
import { Context, getModel, complete } from "@mariozechner/companion-ai";
import { Context, getModel, complete } from "@mariozechner/clanker-ai";
// Create and use a 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:
```typescript
import { getModel, complete } from "@mariozechner/companion-ai";
import { getModel, complete } from "@mariozechner/clanker-ai";
// API key must be passed explicitly in browser
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
@ -943,7 +943,7 @@ const response = await complete(
### Browser Compatibility Notes
- 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.
- 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
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
export COMPANION_AI_ANTIGRAVITY_VERSION="1.23.0"
export CLANKER_AI_ANTIGRAVITY_VERSION="1.23.0"
```
#### 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 |
| 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
```typescript
import { getEnvApiKey } from "@mariozechner/companion-ai";
import { getEnvApiKey } from "@mariozechner/clanker-ai";
// Check if an API key is set in environment variables
const key = getEnvApiKey("openai"); // checks OPENAI_API_KEY
@ -1047,7 +1047,7 @@ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
```
```typescript
import { getModel, complete } from "@mariozechner/companion-ai";
import { getModel, complete } from "@mariozechner/clanker-ai";
(async () => {
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:
```bash
npx @mariozechner/companion-ai login # interactive provider selection
npx @mariozechner/companion-ai login anthropic # login to specific provider
npx @mariozechner/companion-ai list # list available providers
npx @mariozechner/clanker-ai login # interactive provider selection
npx @mariozechner/clanker-ai login anthropic # login to specific provider
npx @mariozechner/clanker-ai list # list available providers
```
Credentials are saved to `auth.json` in the current directory.
### 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
import {
@ -1095,13 +1095,13 @@ import {
// Types
type OAuthProvider, // 'anthropic' | 'openai-codex' | 'github-copilot' | 'google-gemini-cli' | 'google-antigravity'
type OAuthCredentials,
} from "@mariozechner/companion-ai/oauth";
} from "@mariozechner/clanker-ai/oauth";
```
### Login Flow Example
```typescript
import { loginGitHubCopilot } from "@mariozechner/companion-ai/oauth";
import { loginGitHubCopilot } from "@mariozechner/clanker-ai/oauth";
import { writeFileSync } from "fs";
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:
```typescript
import { getModel, complete } from "@mariozechner/companion-ai";
import { getOAuthApiKey } from "@mariozechner/companion-ai/oauth";
import { getModel, complete } from "@mariozechner/clanker-ai";
import { getOAuthApiKey } from "@mariozechner/clanker-ai/oauth";
import { readFileSync, writeFileSync } from "fs";
// Load your stored credentials

View file

@ -1,5 +1,5 @@
{
"name": "@mariozechner/companion-ai",
"name": "@harivansh-afk/clanker-ai",
"version": "0.56.2",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
@ -20,7 +20,7 @@
}
},
"bin": {
"companion-ai": "./dist/cli.js"
"clanker-ai": "./dist/cli.js"
},
"files": [
"dist",
@ -66,7 +66,7 @@
"license": "MIT",
"repository": {
"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"
},
"engines": {

View file

@ -78,7 +78,7 @@ async function main(): Promise<void> {
const providerList = PROVIDERS.map(
(p) => ` ${p.id.padEnd(20)} ${p.name}`,
).join("\n");
console.log(`Usage: npx @mariozechner/companion-ai <command> [provider]
console.log(`Usage: npx @mariozechner/clanker-ai <command> [provider]
Commands:
login [provider] Login to an OAuth provider
@ -88,9 +88,9 @@ Providers:
${providerList}
Examples:
npx @mariozechner/companion-ai login # interactive provider selection
npx @mariozechner/companion-ai login anthropic # login to specific provider
npx @mariozechner/companion-ai list # list providers
npx @mariozechner/clanker-ai login # interactive provider selection
npx @mariozechner/clanker-ai login anthropic # login to specific provider
npx @mariozechner/clanker-ai list # list providers
`);
return;
}
@ -131,7 +131,7 @@ Examples:
if (!PROVIDERS.some((p) => p.id === provider)) {
console.error(`Unknown provider: ${provider}`);
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);
}
@ -142,7 +142,7 @@ Examples:
}
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);
}

View file

@ -514,7 +514,7 @@ function mapThinkingLevelToEffort(
/**
* 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(
cacheRetention?: CacheRetention,
@ -524,7 +524,7 @@ function resolveCacheRetention(
}
if (
typeof process !== "undefined" &&
process.env.COMPANION_CACHE_RETENTION === "long"
process.env.CLANKER_CACHE_RETENTION === "long"
) {
return "long";
}

View file

@ -40,7 +40,7 @@ import { transformMessages } from "./transform-messages.js";
/**
* 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(
cacheRetention?: CacheRetention,
@ -50,7 +50,7 @@ function resolveCacheRetention(
}
if (
typeof process !== "undefined" &&
process.env.COMPANION_CACHE_RETENTION === "long"
process.env.CLANKER_CACHE_RETENTION === "long"
) {
return "long";
}

View file

@ -88,7 +88,7 @@ const DEFAULT_ANTIGRAVITY_VERSION = "1.18.3";
function getAntigravityHeaders() {
const version =
process.env.COMPANION_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION;
process.env.CLANKER_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION;
return {
"User-Agent": `antigravity/${version} darwin/arm64`,
};
@ -1040,8 +1040,8 @@ export function buildRequest(
model: model.id,
request,
...(isAntigravity ? { requestType: "agent" } : {}),
userAgent: isAntigravity ? "antigravity" : "companion-coding-agent",
requestId: `${isAntigravity ? "agent" : "companion"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
userAgent: isAntigravity ? "antigravity" : "clanker-coding-agent",
requestId: `${isAntigravity ? "agent" : "clanker"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
};
}

View file

@ -997,10 +997,10 @@ function buildHeaders(
headers.set("Authorization", `Bearer ${token}`);
headers.set("chatgpt-account-id", accountId);
headers.set("OpenAI-Beta", "responses=experimental");
headers.set("originator", "companion");
headers.set("originator", "clanker");
const userAgent = _os
? `companion (${_os.platform()} ${_os.release()}; ${_os.arch()})`
: "companion (browser)";
? `clanker (${_os.platform()} ${_os.release()}; ${_os.arch()})`
: "clanker (browser)";
headers.set("User-Agent", userAgent);
headers.set("accept", "text/event-stream");
headers.set("content-type", "application/json");

View file

@ -33,7 +33,7 @@ const OPENAI_TOOL_CALL_PROVIDERS = new Set([
/**
* 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(
cacheRetention?: CacheRetention,
@ -43,7 +43,7 @@ function resolveCacheRetention(
}
if (
typeof process !== "undefined" &&
process.env.COMPANION_CACHE_RETENTION === "long"
process.env.CLANKER_CACHE_RETENTION === "long"
) {
return "long";
}

View file

@ -283,7 +283,7 @@ export interface OpenAICompletionsCompat {
supportsDeveloperRole?: boolean;
/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */
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>>;
/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
supportsUsageInStreaming?: boolean;

View file

@ -216,7 +216,7 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
}
async function createAuthorizationFlow(
originator: string = "companion",
originator: string = "clanker",
): Promise<{ verifier: string; state: string; url: string }> {
const { verifier, challenge } = await generatePKCE();
const state = createState();
@ -337,7 +337,7 @@ function getAccountId(accessToken: string): string | null {
* @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.
* Races with browser callback - whichever completes first wins.
* 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: {
onAuth: (info: { url: string; instructions?: string }) => void;

View file

@ -71,8 +71,8 @@ describe.skipIf(!oauthToken)("Anthropic OAuth tool name normalization", () => {
expect(toolCallName).toBe("todowrite");
});
it("should handle companion's built-in tools (read, write, edit, bash)", async () => {
// Companion's tools use lowercase names, CC uses PascalCase
it("should handle clanker's built-in tools (read, write, edit, bash)", async () => {
// Clanker's tools use lowercase names, CC uses PascalCase
const readTool: Tool = {
name: "read",
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 () => {
// 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
// because there's no tool named "glob" in context.tools
const findTool: Tool = {

View file

@ -3,18 +3,18 @@ import { getModel } from "../src/models.js";
import { stream } from "../src/stream.js";
import type { Context } from "../src/types.js";
describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
const originalEnv = process.env.COMPANION_CACHE_RETENTION;
describe("Cache Retention (CLANKER_CACHE_RETENTION)", () => {
const originalEnv = process.env.CLANKER_CACHE_RETENTION;
beforeEach(() => {
delete process.env.COMPANION_CACHE_RETENTION;
delete process.env.CLANKER_CACHE_RETENTION;
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env.COMPANION_CACHE_RETENTION = originalEnv;
process.env.CLANKER_CACHE_RETENTION = originalEnv;
} 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", () => {
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 () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
let capturedPayload: any = null;
@ -51,9 +51,9 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
);
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 () => {
process.env.COMPANION_CACHE_RETENTION = "long";
process.env.CLANKER_CACHE_RETENTION = "long";
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
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 () => {
process.env.COMPANION_CACHE_RETENTION = "long";
process.env.CLANKER_CACHE_RETENTION = "long";
// Create a model with a different baseUrl (simulating a proxy)
const baseModel = getModel("anthropic", "claude-3-5-haiku-20241022");
@ -210,7 +210,7 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
describe("OpenAI Responses Provider", () => {
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 () => {
const model = getModel("openai", "gpt-4o-mini");
let capturedPayload: any = null;
@ -232,9 +232,9 @@ describe("Cache Retention (COMPANION_CACHE_RETENTION)", () => {
);
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 () => {
process.env.COMPANION_CACHE_RETENTION = "long";
process.env.CLANKER_CACHE_RETENTION = "long";
const model = getModel("openai", "gpt-4o-mini");
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 () => {
process.env.COMPANION_CACHE_RETENTION = "long";
process.env.CLANKER_CACHE_RETENTION = "long";
// Create a model with a different baseUrl (simulating a proxy)
const baseModel = getModel("openai", "gpt-4o-mini");

View file

@ -683,7 +683,7 @@ describe("Context overflow error handling", () => {
// Check if ollama is installed and local LLM tests are enabled
let ollamaInstalled = false;
if (!process.env.COMPANION_NO_LOCAL_LLM) {
if (!process.env.CLANKER_NO_LOCAL_LLM) {
try {
execSync("which ollama", { stdio: "ignore" });
ollamaInstalled = true;
@ -785,7 +785,7 @@ describe("Context overflow error handling", () => {
// =============================================================================
let lmStudioRunning = false;
if (!process.env.COMPANION_NO_LOCAL_LLM) {
if (!process.env.CLANKER_NO_LOCAL_LLM) {
try {
execSync(
"curl -s --max-time 1 http://localhost:1234/v1/models > /dev/null",

View file

@ -227,7 +227,7 @@ function dumpFailurePayload(params: {
payload?: unknown;
messages: Message[];
}): void {
const filename = `/tmp/companion-handoff-${params.label}-${Date.now()}.json`;
const filename = `/tmp/clanker-handoff-${params.label}-${Date.now()}.json`;
const body = {
label: params.label,
error: params.error,

View file

@ -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", () => {

View file

@ -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)", () => {
@ -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)(
"claude-sonnet-4-5 - should handle tool result with only image",
{ retry: 3, timeout: 30000 },

View file

@ -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.
* OAuth tokens are automatically refreshed if expired and saved back to auth.json.
@ -20,7 +20,7 @@ import type {
OAuthProvider,
} 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: "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 OAuth credentials, returns the access token (refreshing if expired and saving back).

View file

@ -6,22 +6,22 @@ import { streamOpenAICodexResponses } from "../src/providers/openai-codex-respon
import type { Context, Model } from "../src/types.js";
const originalFetch = global.fetch;
const originalAgentDir = process.env.COMPANION_CODING_AGENT_DIR;
const originalAgentDir = process.env.CLANKER_CODING_AGENT_DIR;
afterEach(() => {
global.fetch = originalFetch;
if (originalAgentDir === undefined) {
delete process.env.COMPANION_CODING_AGENT_DIR;
delete process.env.CLANKER_CODING_AGENT_DIR;
} else {
process.env.COMPANION_CODING_AGENT_DIR = originalAgentDir;
process.env.CLANKER_CODING_AGENT_DIR = originalAgentDir;
}
vi.restoreAllMocks();
});
describe("openai-codex streaming", () => {
it("streams SSE responses into AssistantMessageEventStream", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
const payload = Buffer.from(
JSON.stringify({
@ -95,7 +95,7 @@ describe("openai-codex streaming", () => {
expect(headers?.get("Authorization")).toBe(`Bearer ${token}`);
expect(headers?.get("chatgpt-account-id")).toBe("acc_test");
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?.has("x-api-key")).toBe(false);
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 () => {
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
const payload = Buffer.from(
JSON.stringify({
@ -272,8 +272,8 @@ describe("openai-codex streaming", () => {
it.each(["gpt-5.3-codex", "gpt-5.4"])(
"clamps %s minimal reasoning effort to low",
async (modelId) => {
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
const payload = Buffer.from(
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 () => {
const tempDir = mkdtempSync(join(tmpdir(), "companion-codex-stream-"));
process.env.COMPANION_CODING_AGENT_DIR = tempDir;
const tempDir = mkdtempSync(join(tmpdir(), "clanker-codex-stream-"));
process.env.CLANKER_CODING_AGENT_DIR = tempDir;
const payload = Buffer.from(
JSON.stringify({

View file

@ -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)
// =========================================================================
@ -1800,7 +1800,7 @@ describe("Generate E2E Tests", () => {
// Check if ollama is installed and local LLM tests are enabled
let ollamaInstalled = false;
if (!process.env.COMPANION_NO_LOCAL_LLM) {
if (!process.env.CLANKER_NO_LOCAL_LLM) {
try {
execSync("which ollama", { stdio: "ignore" });
ollamaInstalled = true;

View file

@ -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", () => {

View file

@ -7,7 +7,7 @@
* OpenAI Responses API generates IDs in format: {call_id}|{id}
* 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";

View file

@ -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", () => {

View file

@ -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", () => {

View file

@ -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
@ -13,11 +13,11 @@ Two-way channel extension for [companion](https://github.com/espennilsen/compani
## Settings
Add to `~/.companion/agent/settings.json` or `.companion/settings.json`:
Add to `~/.clanker/agent/settings.json` or `.clanker/settings.json`:
```json
{
"companion-channels": {
"clanker-channels": {
"adapters": {
"telegram": {
"type": "telegram",
@ -81,7 +81,7 @@ Use `"env:VAR_NAME"` to reference environment variables. Project settings overri
## Install
```bash
companion install npm:@e9n/companion-channels
clanker install npm:@e9n/clanker-channels
```
## License

View file

@ -1,14 +1,14 @@
{
"name": "@e9n/companion-channels",
"name": "@harivansh-afk/clanker-channels",
"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",
"keywords": [
"companion-package"
"clanker-package"
],
"license": "MIT",
"author": "Espen Nilsen <hi@e9n.dev>",
"companion": {
"clanker": {
"extensions": [
"./src/index.ts"
]
@ -18,8 +18,8 @@
"typescript": "^5.0.0"
},
"peerDependencies": {
"@mariozechner/companion-ai": "*",
"@mariozechner/companion-coding-agent": "*",
"@harivansh-afk/clanker-ai": "*",
"@harivansh-afk/clanker-coding-agent": "*",
"@sinclair/typebox": "*"
},
"dependencies": {
@ -34,7 +34,7 @@
],
"repository": {
"type": "git",
"url": "git+https://github.com/espennilsen/companion.git",
"directory": "extensions/companion-channels"
"url": "git+https://github.com/harivansh-afk/clanker-agent.git",
"directory": "packages/clanker-channels"
}
}

View file

@ -1,5 +1,5 @@
/**
* companion-channels Built-in Slack adapter (bidirectional).
* clanker-channels Built-in Slack adapter (bidirectional).
*
* Outgoing: Slack Web API chat.postMessage.
* Incoming: Socket Mode (WebSocket) for events + slash commands.
@ -14,13 +14,13 @@
* - Channel allowlisting (optional)
*
* Requires:
* - App-level token (xapp-...) for Socket Mode in settings under companion-channels.slack.appToken
* - Bot token (xoxb-...) for Web API in settings under companion-channels.slack.botToken
* - App-level token (xapp-...) for Socket Mode in settings under clanker-channels.slack.appToken
* - Bot token (xoxb-...) for Web API in settings under clanker-channels.slack.botToken
* - Socket Mode enabled in app settings
*
* Config in ~/.companion/agent/settings.json:
* Config in ~/.clanker/agent/settings.json:
* {
* "companion-channels": {
* "clanker-channels": {
* "adapters": {
* "slack": {
* "type": "slack",
@ -95,7 +95,7 @@ export function createSlackAdapter(
cwd?: string,
log?: SlackAdapterLogger,
): 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 =
(cwd ? (getChannelSetting(cwd, "slack.appToken") as string) : null) ??
(config.appToken as string);
@ -109,11 +109,11 @@ export function createSlackAdapter(
if (!appToken)
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)
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;

View file

@ -1,5 +1,5 @@
/**
* companion-channels Built-in Telegram adapter (bidirectional).
* clanker-channels Built-in Telegram adapter (bidirectional).
*
* Outgoing: Telegram Bot API sendMessage.
* Incoming: Long-polling via getUpdates.
@ -14,7 +14,7 @@
* - File size validation (1MB for docs/photos, 10MB for voice/audio)
* - 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",
* "botToken": "your-telegram-bot-token",
@ -173,7 +173,7 @@ export function createTelegramAdapter(config: AdapterConfig): ChannelAdapter {
} catch (err: any) {
transcriberError = err.message ?? "Unknown transcription config 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(suggestedName || "") ||
"";
const tmpDir = path.join(os.tmpdir(), "companion-channels");
const tmpDir = path.join(os.tmpdir(), "clanker-channels");
fs.mkdirSync(tmpDir, { recursive: true });
const localPath = path.join(
tmpDir,

View file

@ -1,5 +1,5 @@
/**
* companion-channels Pluggable audio transcription.
* clanker-channels Pluggable audio transcription.
*
* Supports three providers:
* - "apple" macOS SFSpeechRecognizer (free, offline, no API key)

View file

@ -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.
*

View file

@ -1,18 +1,18 @@
/**
* companion-channels Chat bridge.
* clanker-channels Chat bridge.
*
* 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.
* Multiple senders run concurrently up to maxConcurrent.
*/
import { readFileSync } from "node:fs";
import type { ImageContent } from "@mariozechner/companion-ai";
import type { ImageContent } from "@mariozechner/clanker-ai";
import {
type EventBus,
getActiveGatewayRuntime,
} from "@mariozechner/companion-coding-agent";
} from "@mariozechner/clanker-coding-agent";
import type { ChannelRegistry } from "../registry.js";
import type {
BridgeConfig,
@ -73,7 +73,7 @@ export class ChatBridge {
if (!getActiveGatewayRuntime()) {
this.log(
"bridge-unavailable",
{ reason: "no active companion gateway runtime" },
{ reason: "no active clanker gateway runtime" },
"WARN",
);
return;
@ -195,7 +195,7 @@ export class ChatBridge {
this.sendReply(
prompt.adapter,
prompt.sender,
"❌ companion gateway is not running.",
"❌ clanker gateway is not running.",
);
return;
}

View file

@ -1,5 +1,5 @@
/**
* companion-channels Bot command handler.
* clanker-channels Bot command handler.
*
* Detects messages starting with / and handles them without routing
* to the agent. Provides built-in commands and a registry for custom ones.
@ -74,7 +74,7 @@ registerCommand({
name: "start",
description: "Welcome message",
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.",
});

View file

@ -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.
* Falls back to stateless runner if RPC fails to start.
*
@ -33,7 +33,7 @@ interface PendingRequest {
/**
* A persistent RPC session for a single sender.
* Wraps a `companion --mode rpc` subprocess.
* Wraps a `clanker --mode rpc` subprocess.
*/
export class RpcSession {
private child: ChildProcess | null = null;
@ -63,7 +63,7 @@ export class RpcSession {
}
try {
this.child = spawn("companion", args, {
this.child = spawn("clanker", args, {
cwd: this.options.cwd,
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env },

View file

@ -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.
* Same pattern as companion-cron and companion-heartbeat.
* Same pattern as clanker-cron and clanker-heartbeat.
*/
import { type ChildProcess, spawn } from "node:child_process";
@ -49,7 +49,7 @@ export function runPrompt(options: RunOptions): Promise<RunResult> {
let child: ChildProcess;
try {
child = spawn("companion", args, {
child = spawn("clanker", args, {
cwd,
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },

View file

@ -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.
* Telegram typing indicators expire after ~5s, so we refresh every 4s.

View file

@ -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,
* which merges global (~/.companion/agent/settings.json) and project
* (.companion/settings.json) configs automatically.
* Reads the "clanker-channels" key from settings via SettingsManager,
* which merges global (~/.clanker/agent/settings.json) and project
* (.clanker/settings.json) configs automatically.
*
* Example settings.json:
* {
* "companion-channels": {
* "clanker-channels": {
* "adapters": {
* "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";
const SETTINGS_KEY = "companion-channels";
const SETTINGS_KEY = "clanker-channels";
export function loadConfig(cwd: string): ChannelConfig {
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.
*
* 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 {
const agentDir = getAgentDir();

View file

@ -1,5 +1,5 @@
/**
* companion-channels Event API registration.
* clanker-channels Event API registration.
*
* Events emitted:
* channel:receive incoming message from an external adapter
@ -14,7 +14,7 @@
* 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 { ChannelRegistry } from "./registry.js";
import type {
@ -31,13 +31,13 @@ export function setBridge(bridge: ChatBridge | null): void {
}
export function registerChannelEvents(
companion: ExtensionAPI,
clanker: ExtensionAPI,
registry: ChannelRegistry,
): void {
// ── Incoming messages → channel:receive (+ bridge) ──────
registry.setOnIncoming((message: IncomingMessage) => {
companion.events.emit("channel:receive", message);
clanker.events.emit("channel:receive", message);
// Route to bridge if active
if (activeBridge?.isActive()) {
@ -47,7 +47,7 @@ export function registerChannelEvents(
// ── 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 {
job: { name: string; channel: string; prompt: string };
response?: string;
@ -74,7 +74,7 @@ export function registerChannelEvents(
// ── channel:send — deliver a message ─────────────────────
companion.events.on("channel:send", (raw: unknown) => {
clanker.events.on("channel:send", (raw: unknown) => {
const data = raw as ChannelMessage & {
callback?: (result: { ok: boolean; error?: string }) => void;
};
@ -83,7 +83,7 @@ export function registerChannelEvents(
// ── channel:register — add a custom adapter ──────────────
companion.events.on("channel:register", (raw: unknown) => {
clanker.events.on("channel:register", (raw: unknown) => {
const data = raw as {
name: string;
adapter: ChannelAdapter;
@ -99,14 +99,14 @@ export function registerChannelEvents(
// ── 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 };
data.callback?.(registry.unregister(data.name));
});
// ── channel:list — list adapters + routes ────────────────
companion.events.on("channel:list", (raw: unknown) => {
clanker.events.on("channel:list", (raw: unknown) => {
const data = raw as {
callback?: (items: ReturnType<ChannelRegistry["list"]>) => void;
};
@ -115,7 +115,7 @@ export function registerChannelEvents(
// ── channel:test — send a test ping ──────────────────────
companion.events.on("channel:test", (raw: unknown) => {
clanker.events.on("channel:test", (raw: unknown) => {
const data = raw as {
adapter: string;
recipient: string;
@ -125,7 +125,7 @@ export function registerChannelEvents(
.send({
adapter: data.adapter,
recipient: data.recipient ?? "",
text: `🏓 companion-channels test — ${new Date().toISOString()}`,
text: `🏓 clanker-channels test — ${new Date().toISOString()}`,
source: "channel:test",
})
.then((r) => data.callback?.(r));

View file

@ -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
* (Telegram, webhooks, custom adapters).
*
* 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
* as isolated subprocess prompts and responses are sent back. Enable via:
* - --chat-bridge flag
* - /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": {
* "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 { loadConfig } from "./config.js";
import { registerChannelEvents, setBridge } from "./events.js";
@ -42,15 +42,15 @@ import { createLogger } from "./logger.js";
import { ChannelRegistry } from "./registry.js";
import { registerChannelTool } from "./tool.js";
export default function (companion: ExtensionAPI) {
const log = createLogger(companion);
export default function (clanker: ExtensionAPI) {
const log = createLoggerclanker;
const registry = new ChannelRegistry();
registry.setLogger(log);
let bridge: ChatBridge | null = null;
// ── Flag: --chat-bridge ───────────────────────────────────
companion.registerFlag("chat-bridge", {
clanker.registerFlag("chat-bridge", {
description:
"Enable the chat bridge on startup (incoming messages → agent → reply)",
type: "boolean",
@ -59,17 +59,17 @@ export default function (companion: ExtensionAPI) {
// ── Event API + cron integration ──────────────────────────
registerChannelEvents(companion, registry);
registerChannelEvents(clanker, registry);
// ── Lifecycle ─────────────────────────────────────────────
companion.on("session_start", async (_event, ctx) => {
clanker.on("session_start", async (_event, ctx) => {
const config = loadConfig(ctx.cwd);
await registry.loadConfig(config, ctx.cwd);
const errors = registry.getErrors();
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("init", {
@ -84,22 +84,22 @@ export default function (companion: ExtensionAPI) {
.getErrors()
.filter((e) => e.error.startsWith("Failed to start"));
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
bridge = new ChatBridge(config.bridge, ctx.cwd, registry, companion.events, log);
bridge = new ChatBridge(config.bridge, ctx.cwd, registry, clanker.events, log);
setBridge(bridge);
const flagEnabled = companion.getFlag("--chat-bridge");
const flagEnabled = clanker.getFlag("--chat-bridge");
if (flagEnabled || config.bridge?.enabled) {
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", {});
bridge?.stop();
setBridge(null);
@ -108,7 +108,7 @@ export default function (companion: ExtensionAPI) {
// ── Command: /chat-bridge ─────────────────────────────────
companion.registerCommand("chat-bridge", {
clanker.registerCommand("chat-bridge", {
description: "Manage chat bridge: /chat-bridge [on|off|status]",
getArgumentCompletions: (prefix: string) => {
return ["on", "off", "status"]
@ -164,5 +164,5 @@ export default function (companion: ExtensionAPI) {
// ── LLM tool ──────────────────────────────────────────────
registerChannelTool(companion, registry);
registerChannelTool(clanker, registry);
}

View 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 });
}

View file

@ -1,5 +1,5 @@
/**
* companion-channels Adapter registry + route resolution.
* clanker-channels Adapter registry + route resolution.
*/
import type {

View file

@ -1,9 +1,9 @@
/**
* companion-channels LLM tool registration.
* clanker-channels LLM tool registration.
*/
import { StringEnum } from "@mariozechner/companion-ai";
import type { ExtensionAPI } from "@mariozechner/companion-coding-agent";
import { StringEnum } from "@mariozechner/clanker-ai";
import type { ExtensionAPI } from "@mariozechner/clanker-coding-agent";
import { Type } from "@sinclair/typebox";
import type { ChannelRegistry } from "./registry.js";
@ -16,10 +16,10 @@ interface ChannelToolParams {
}
export function registerChannelTool(
companion: ExtensionAPI,
clanker: ExtensionAPI,
registry: ChannelRegistry,
): void {
companion.registerTool({
clanker.registerTool({
name: "notify",
label: "Channel",
description:
@ -57,7 +57,7 @@ export function registerChannelTool(
const items = registry.list();
if (items.length === 0) {
result =
'No adapters configured. Add "companion-channels" to your settings.json.';
'No adapters configured. Add "clanker-channels" to your settings.json.';
} else {
const lines = items.map((i) =>
i.type === "route"
@ -92,7 +92,7 @@ export function registerChannelTool(
const r = await registry.send({
adapter: params.adapter,
recipient: params.recipient ?? "",
text: `🏓 companion-channels test — ${new Date().toISOString()}`,
text: `🏓 clanker-channels test — ${new Date().toISOString()}`,
source: "channel:test",
});
result = r.ok

View file

@ -1,5 +1,5 @@
/**
* companion-channels Shared types.
* clanker-channels Shared types.
*/
// ── Channel message ─────────────────────────────────────────────
@ -17,7 +17,7 @@ export interface ChannelMessage {
metadata?: Record<string, unknown>;
}
// ── Incoming message (from external → companion) ───────────────────────
// ── Incoming message (from external → clanker) ───────────────────────
export interface IncomingAttachment {
/** Attachment type */
@ -90,7 +90,7 @@ export interface ChannelAdapter {
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 {
type: string;
@ -103,8 +103,8 @@ export interface BridgeConfig {
/**
* Default session mode (default: "persistent").
*
* - "persistent" long-lived `companion --mode rpc` subprocess with conversation memory
* - "stateless" isolated `companion -p --no-session` subprocess per message (no memory)
* - "persistent" long-lived `clanker --mode rpc` subprocess with conversation memory
* - "stateless" isolated `clanker -p --no-session` subprocess per message (no memory)
*
* Can be overridden per sender via `sessionRules`.
*/
@ -144,7 +144,7 @@ export interface BridgeConfig {
* extensions that crash or conflict, e.g. webserver port collisions).
* 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[];
}

View file

@ -1,12 +1,12 @@
# companion-grind
# clanker-grind
Explicit grind mode for companion.
Explicit grind mode for clanker.
Features:
- Auto-activates only when the user uses explicit grind cues in a prompt
- 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
Example prompts:
@ -26,7 +26,7 @@ Settings:
```json
{
"companion-grind": {
"clanker-grind": {
"enabled": true,
"pollIntervalMs": 30000,
"cueMode": "explicit-only",

View file

@ -1,10 +1,10 @@
{
"name": "companion-grind",
"name": "clanker-grind",
"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",
"keywords": [
"companion-package"
"clanker-package"
],
"license": "MIT",
"author": "Mario Zechner",
@ -15,14 +15,14 @@
"package.json",
"README.md"
],
"companion": {
"clanker": {
"extensions": [
"./src/index.ts"
]
},
"peerDependencies": {
"@mariozechner/companion-agent-core": "*",
"@mariozechner/companion-coding-agent": "*"
"@harivansh-afk/clanker-agent-core": "*",
"@harivansh-afk/clanker-coding-agent": "*"
},
"devDependencies": {
"@types/node": "^24.3.0",

View file

@ -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";
const DEFAULT_CUE_PATTERNS = [

View file

@ -1,5 +1,5 @@
import type { AgentMessage } from "@mariozechner/companion-agent-core";
import type { ExtensionAPI, ExtensionContext, RegisteredCommand } from "@mariozechner/companion-coding-agent";
import type { AgentMessage } from "@mariozechner/clanker-agent-core";
import type { ExtensionAPI, ExtensionContext, RegisteredCommand } from "@mariozechner/clanker-coding-agent";
import { loadConfig } from "./config.js";
import { parseAutoActivation, parseGrindStatus, parseStopCondition } from "./parser.js";
import { buildContinuationPrompt, buildRepairPrompt, buildSystemPromptAddon } from "./prompts.js";
@ -13,19 +13,19 @@ import {
} from "./types.js";
function isDaemonRuntime(): boolean {
if (process.env.COMPANION_GRIND_FORCE_DAEMON === "1") {
if (process.env.CLANKER_GRIND_FORCE_DAEMON === "1") {
return true;
}
if (process.env.COMPANION_GRIND_FORCE_DAEMON === "0") {
if (process.env.CLANKER_GRIND_FORCE_DAEMON === "0") {
return false;
}
return (
process.argv.includes("daemon") ||
process.argv.includes("gateway") ||
Boolean(process.env.COMPANION_GATEWAY_BIND) ||
Boolean(process.env.COMPANION_GATEWAY_PORT) ||
Boolean(process.env.COMPANION_GATEWAY_TOKEN)
Boolean(process.env.CLANKER_GATEWAY_BIND) ||
Boolean(process.env.CLANKER_GATEWAY_PORT) ||
Boolean(process.env.CLANKER_GATEWAY_TOKEN)
);
}
@ -57,21 +57,21 @@ function readState(ctx: ExtensionContext): GrindRunState | null {
return getLatestRunState(ctx.sessionManager.getEntries());
}
function persistState(companion: ExtensionAPI, ctx: ExtensionContext, state: GrindRunState): GrindRunState {
companion.appendEntry(GRIND_STATE_ENTRY_TYPE, state);
function persistState(clanker: ExtensionAPI, ctx: ExtensionContext, state: GrindRunState): GrindRunState {
clanker.appendEntry(GRIND_STATE_ENTRY_TYPE, state);
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;
}
function clearUiStatus(ctx: ExtensionContext): void {
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) {
return state;
}
@ -85,7 +85,7 @@ function maybeExpireRun(companion: ExtensionAPI, ctx: ExtensionContext, state: G
lastNextAction: null,
pendingRepair: false,
});
persistState(companion, ctx, expired);
persistState(clanker, ctx, expired);
note(ctx, "Grind mode stopped: deadline reached.");
return expired;
}
@ -164,7 +164,7 @@ function parseStartCommandArgs(args: string): {
}
function startRun(
companion: ExtensionAPI,
clanker: ExtensionAPI,
ctx: ExtensionContext,
config: GrindConfig,
input: {
@ -181,17 +181,17 @@ function startRun(
}
if (config.requireDaemon && !isDaemonRuntime()) {
note(ctx, "Durable grind mode requires `companion daemon`.");
note(ctx, "Durable grind mode requires `clanker daemon`.");
return null;
}
const nextState = createRunState(input);
persistState(companion, ctx, nextState);
persistState(clanker, ctx, nextState);
note(ctx, "Grind mode activated.");
return nextState;
}
export default function grind(companion: ExtensionAPI) {
export default function grind(clanker: ExtensionAPI) {
let config: GrindConfig | null = null;
let state: GrindRunState | null = null;
let heartbeat: NodeJS.Timeout | null = null;
@ -221,18 +221,18 @@ export default function grind(companion: ExtensionAPI) {
return;
}
const expired = maybeExpireRun(companion, ctx, state);
const expired = maybeExpireRun(clanker, ctx, state);
state = expired;
if (!state || state.status !== "active") {
return;
}
if (state.pendingRepair) {
companion.sendUserMessage(buildRepairPrompt(state), {
clanker.sendUserMessage(buildRepairPrompt(state), {
deliverAs: "followUp",
});
} else {
companion.sendUserMessage(buildContinuationPrompt(state), {
clanker.sendUserMessage(buildContinuationPrompt(state), {
deliverAs: "followUp",
});
}
@ -241,7 +241,7 @@ export default function grind(companion: ExtensionAPI) {
};
const registerCommand = (name: string, command: Omit<RegisteredCommand, "name">) => {
companion.registerCommand(name, command);
clanker.registerCommand(name, command);
};
registerCommand("grind", {
@ -268,7 +268,7 @@ export default function grind(companion: ExtensionAPI) {
? parseStopCondition(`until ${parsed.until}`)
: { deadlineAt: null, completionCriterion: null };
const nextState = startRun(companion, ctx, currentConfig, {
const nextState = startRun(clanker, ctx, currentConfig, {
activation: "command",
goal: parsed.goal,
sourcePrompt: parsed.goal,
@ -277,7 +277,7 @@ export default function grind(companion: ExtensionAPI) {
});
state = nextState;
if (state) {
companion.sendUserMessage(parsed.goal, { deliverAs: "followUp" });
clanker.sendUserMessage(parsed.goal, { deliverAs: "followUp" });
}
return;
}
@ -312,7 +312,7 @@ export default function grind(companion: ExtensionAPI) {
note(ctx, "No grind run to pause.");
return;
}
state = persistState(companion, ctx, withStatus(state, "paused", { pendingRepair: false }));
state = persistState(clanker, ctx, withStatus(state, "paused", { pendingRepair: false }));
note(ctx, "Grind mode paused.");
return;
}
@ -326,12 +326,12 @@ export default function grind(companion: ExtensionAPI) {
return;
}
if (currentConfig.requireDaemon && !isDaemonRuntime()) {
note(ctx, "Durable grind mode requires `companion daemon`.");
note(ctx, "Durable grind mode requires `clanker daemon`.");
return;
}
state = persistState(companion, ctx, withStatus(state, "active"));
state = persistState(clanker, ctx, withStatus(state, "active"));
note(ctx, "Grind mode resumed.");
companion.sendUserMessage(buildContinuationPrompt(state), {
clanker.sendUserMessage(buildContinuationPrompt(state), {
deliverAs: "followUp",
});
return;
@ -346,7 +346,7 @@ export default function grind(companion: ExtensionAPI) {
return;
}
state = persistState(
companion,
clanker,
ctx,
withStatus(state, "stopped", {
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);
state = readState(ctx);
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) {
ensureHeartbeat(ctx);
}
});
companion.on("session_shutdown", async () => {
clanker.on("session_shutdown", async () => {
stopHeartbeat();
});
companion.on("input", async (event, ctx) => {
clanker.on("input", async (event, ctx) => {
const currentConfig = getConfig(ctx.cwd);
if (!currentConfig.enabled || event.source === "extension") {
return { action: "continue" } as const;
@ -389,7 +389,7 @@ export default function grind(companion: ExtensionAPI) {
const activation = parseAutoActivation(event.text, currentConfig.cuePatterns);
if (!activation) {
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.");
}
return { action: "continue" } as const;
@ -401,7 +401,7 @@ export default function grind(companion: ExtensionAPI) {
return { action: "continue" } as const;
}
state = startRun(companion, ctx, currentConfig, {
state = startRun(clanker, ctx, currentConfig, {
activation: "explicit",
goal: event.text,
sourcePrompt: event.text,
@ -412,13 +412,13 @@ export default function grind(companion: ExtensionAPI) {
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);
if (!state || state.status !== "active") {
return;
}
const expired = maybeExpireRun(companion, ctx, state);
const expired = maybeExpireRun(clanker, ctx, state);
state = expired;
if (!state || state.status !== "active") {
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);
if (!state || state.status !== "active") {
return;
}
const expired = maybeExpireRun(companion, ctx, state);
const expired = maybeExpireRun(clanker, ctx, state);
state = expired;
if (!state || state.status !== "active") {
return;
@ -447,7 +447,7 @@ export default function grind(companion: ExtensionAPI) {
if (!parsed) {
if (state.consecutiveParseFailures + 1 >= MAX_PARSE_FAILURES) {
state = persistState(
companion,
clanker,
ctx,
withStatus(state, "blocked", {
pendingRepair: false,
@ -460,7 +460,7 @@ export default function grind(companion: ExtensionAPI) {
return;
}
state = persistState(companion, ctx, {
state = persistState(clanker, ctx, {
...state,
pendingRepair: true,
consecutiveParseFailures: state.consecutiveParseFailures + 1,
@ -469,7 +469,7 @@ export default function grind(companion: ExtensionAPI) {
return;
}
state = persistState(companion, ctx, withLoopStatus(state, parsed));
state = persistState(clanker, ctx, withLoopStatus(state, parsed));
if (state.status !== "active") {
note(ctx, `Grind mode ${state.status}.`);
if (state.status !== "paused") {

View file

@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import type { SessionEntry } from "@mariozechner/companion-coding-agent";
import type { SessionEntry } from "@mariozechner/clanker-coding-agent";
import {
GRIND_STATE_ENTRY_TYPE,
type GrindActivation,

View file

@ -1,5 +1,5 @@
export const GRIND_SETTINGS_KEY = "companion-grind";
export const GRIND_STATE_ENTRY_TYPE = "companion-grind/state";
export const GRIND_SETTINGS_KEY = "clanker-grind";
export const GRIND_STATE_ENTRY_TYPE = "clanker-grind/state";
export const DEFAULT_COMPLETION_CRITERION = "finish the requested task";
export const DEFAULT_POLL_INTERVAL_MS = 30_000;
export const MAX_PARSE_FAILURES = 2;

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
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 cues = ["don't stop", "keep going", "run until"];

View file

@ -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 { createRunState, getLatestRunState, withLoopStatus, withStatus } from "../src/state.js";
import { GRIND_STATE_ENTRY_TYPE } from "../src/types.js";
describe("companion-grind state", () => {
describe("clanker-grind state", () => {
it("creates active run state", () => {
const state = createRunState({
activation: "explicit",

View file

@ -1,5 +1,5 @@
node_modules
.DS_Store
.companion
.clanker
dist
*.log

View file

@ -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
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)
@ -87,13 +87,13 @@ Use this for refactoring or security work.
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`).
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.
- **Research & Execution**: One agent researches while another implements.

View file

@ -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 |
| :----------------------------------------------------------------------------------: | :----------------------------------------------------------------------------: | :----------------------------------------------------------------------------------: |
| <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)_
## 🛠 Installation
Open your companion terminal and type:
Open your clanker terminal and type:
```bash
companion install npm:companion-teams
clanker install npm:clanker-teams
```
## 🚀 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."
**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:
- `google-gemini-cli` (OAuth) is preferred over `google` (API key)
- `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
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)
@ -144,16 +144,16 @@ How to run:
```bash
tmux # Start tmux session
companion # Start companion inside tmux
clanker # Start clanker inside tmux
```
### 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)
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.
2. **Windows**: Create true separate OS windows for each agent.
@ -174,14 +174,14 @@ How to run:
```bash
wezterm # Start WezTerm
companion # Start companion inside WezTerm
clanker # Start clanker inside WezTerm
```
## 📜 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).
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

View file

@ -2,7 +2,7 @@
## 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)
- **Teammate panels** stacked on the RIGHT (takes 30% width, divided vertically)

View file

@ -2,7 +2,7 @@
## 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)
- Zellij (multiplexer)
@ -89,7 +89,7 @@ Total: **46 tests passing**, 0 failures
- ✅ CLI-based pane management (`wezterm cli split-pane`)
- ✅ 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
- ✅ Pane killing via Ctrl-C
- ✅ Tab title setting
@ -102,7 +102,7 @@ WezTerm is cross-platform:
- Linux ✅
- 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

View file

@ -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
@ -22,10 +22,10 @@ First, make sure you're inside a tmux session, Zellij session, or iTerm2:
tmux # or zellij, or just use iTerm2
```
Then start companion:
Then start clanker:
```bash
companion
clanker
```
Create your first team:
@ -66,7 +66,7 @@ Approve or reject:
### 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
#!/bin/bash
@ -124,11 +124,11 @@ Hooks are shell scripts that run automatically at specific events. Currently sup
### 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/
├── .companion/
├── .clanker/
│ └── team-hooks/
│ └── task_completed.sh
```
@ -161,7 +161,7 @@ Example payload:
```bash
#!/bin/bash
# .companion/team-hooks/task_completed.sh
# .clanker/team-hooks/task_completed.sh
TASK_DATA="$1"
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
@ -174,7 +174,7 @@ npm test
```bash
#!/bin/bash
# .companion/team-hooks/task_completed.sh
# .clanker/team-hooks/task_completed.sh
TASK_DATA="$1"
SUBJECT=$(echo "$TASK_DATA" | jq -r '.subject')
@ -189,7 +189,7 @@ curl -X POST -H 'Content-type: application/json' \
```bash
#!/bin/bash
# .companion/team-hooks/task_completed.sh
# .clanker/team-hooks/task_completed.sh
TASK_DATA="$1"
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.
**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
# Correct way
tmux
companion
clanker
# Incorrect way
companion # Then try to use tmux commands
clanker # Then try to use tmux commands
```
### Hook Not Running
@ -321,30 +321,30 @@ companion # Then try to use tmux commands
**Checklist**:
1. File exists at `.companion/team-hooks/task_completed.sh`
2. File is executable: `chmod +x .companion/team-hooks/task_completed.sh`
1. File exists at `.clanker/team-hooks/task_completed.sh`
2. File is executable: `chmod +x .clanker/team-hooks/task_completed.sh`
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
**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
- `haiku` - Anthropic (usually `claude-3-5-haiku`)
- `glm-4.7` - Zhipu AI
Check your companion config for available models.
Check your clanker config for available models.
### Data Location
All team data is stored in:
- `~/.companion/teams/<team-name>/` - Team configuration, member list
- `~/.companion/tasks/<team-name>/` - Task files
- `~/.companion/messages/<team-name>/` - Message history
- `~/.clanker/teams/<team-name>/` - Team configuration, member list
- `~/.clanker/tasks/<team-name>/` - Task files
- `~/.clanker/messages/<team-name>/` - Message history
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'"
# Then delete data directory
rm -rf ~/.companion/teams/my-team/
rm -rf ~/.companion/tasks/my-team/
rm -rf ~/.companion/messages/my-team/
rm -rf ~/.clanker/teams/my-team/
rm -rf ~/.clanker/tasks/my-team/
rm -rf ~/.clanker/messages/my-team/
```
Or use the delete command:

View file

@ -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.
**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:**
- **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.
- **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
@ -231,7 +231,7 @@ export function runHook(
): boolean {
const hookPath = path.join(
process.cwd(),
".companion",
".clanker",
"team-hooks",
`${hookName}.sh`,
);

View file

@ -1,4 +1,4 @@
# companion-teams Tool Reference
# clanker-teams Tool Reference
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**:
- 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)
**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
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.
@ -536,10 +536,10 @@ Task is removed from the active task list. Still preserved in data history.
### 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/
│ └── <team-name>/
│ └── 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
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.
- `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
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.
### 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.
### 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.
@ -658,12 +658,12 @@ If neither tmux nor Zellij is detected, and you're on macOS with iTerm2, compani
### 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
~/.companion/tasks/<team-name>/.lock
~/.companion/messages/<team-name>/.lock
~/.clanker/teams/<team-name>/.lock
~/.clanker/tasks/<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.
@ -678,7 +678,7 @@ If a lock file persists beyond 60 seconds, it's automatically cleaned up. For ma
```bash
# 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
# 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/
```

View file

@ -269,7 +269,7 @@ While not tested in this research, iTerm2 is known to have:
## Recommendations
### For the companion-teams Project
### For the clanker-teams Project
**Primary Recommendation:**

View file

@ -39,7 +39,7 @@ Prompt:
Test the shell-based hook system. First, create a hook script, then mark a task as completed.
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."
---

View file

@ -31,10 +31,10 @@ Prompt:
### 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):
"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.
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."
---

View file

@ -2,13 +2,13 @@
## 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
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
2. Programmatically creating new terminal instances
@ -163,8 +163,8 @@ Extensions can register custom terminal profiles:
"terminal": {
"profiles": [
{
"title": "Companion Teams Terminal",
"id": "companion-teams-terminal"
"title": "Clanker Teams Terminal",
"id": "clanker-teams-terminal"
}
]
}
@ -172,10 +172,10 @@ Extensions can register custom terminal profiles:
}
// Register provider
vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
vscode.window.registerTerminalProfileProvider('clanker-teams-terminal', {
provideTerminalProfile(token) {
return {
name: "Companion Teams Agent",
name: "Clanker Teams Agent",
shellPath: "bash",
cwd: "/project/path"
};
@ -248,17 +248,17 @@ vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
### ⚠️ 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**:
1. companion-teams detects VS Code environment (`TERM_PROGRAM=vscode`)
2. companion-teams spawns child processes that communicate with the extension
1. clanker-teams detects VS Code environment (`TERM_PROGRAM=vscode`)
2. clanker-teams spawns child processes that communicate with the extension
3. Extension receives requests and creates terminals via VS Code API
**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
- **File system polling**: Write request files, extension reads them
- **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) │
└──────┬──────┘
│ 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) │
└───────┬───────────────────┘
@ -283,7 +283,7 @@ vscode.window.registerTerminalProfileProvider('companion-teams-terminal', {
┌───────────────────────────┐
│ 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)
- ❌ 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 permissions/security concerns
- ❌ 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) |
| ----------------- | ------------------------ | ------------------------- | ------------------------ | --------------------- | ----------------------- |
@ -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
2. **Extension required**: Would require users to install and configure an extension
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
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
# Option 1: Run tmux inside VS Code integrated terminal
tmux new -s companion-teams
companion create-team my-team
companion spawn-teammate ...
tmux new -s clanker-teams
clanker create-team my-team
clanker spawn-teammate ...
# Option 2: Start tmux from terminal, then open VS Code
tmux new -s my-session
@ -445,27 +445,27 @@ If there's strong user demand for native VS Code integration:
#### 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)
- Endpoint: POST /create-terminal
- 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
- 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
- Calls vscode.window.createTerminal()
- Returns terminal ID
5. companion-teams tracks terminal IDs via extension responses
5. clanker-teams tracks terminal IDs via extension responses
```
#### Implementation Sketch
**companion-teams (TypeScript)**:
**clanker-teams (TypeScript)**:
```typescript
class VSCodeAdapter implements TerminalAdapter {
@ -482,7 +482,7 @@ class VSCodeAdapter implements TerminalAdapter {
// Write request file
const requestId = uuidv4();
await fs.writeFile(
`/tmp/companion-teams-request-${requestId}.json`,
`/tmp/clanker-teams-request-${requestId}.json`,
JSON.stringify({ ...options, requestId }),
);
@ -514,7 +514,7 @@ export function activate(context: vscode.ExtensionContext) {
// Watch for request files
const watcher = vscode.workspace.createFileSystemWatcher(
"/tmp/companion-teams-request-*.json",
"/tmp/clanker-teams-request-*.json",
);
watcher.onDidChange(async (uri) => {
@ -564,7 +564,7 @@ export function activate(context: vscode.ExtensionContext) {
### 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**:
@ -639,9 +639,9 @@ export class VSCodeAdapter implements TerminalAdapter {
spawn(options: SpawnOptions): string {
throw new Error(
"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. " +
"Alternatively, install the companion-teams VS Code extension " +
"Alternatively, install the clanker-teams VS Code extension " +
"(if implemented).",
);
}
@ -665,22 +665,22 @@ export class VSCodeAdapter implements TerminalAdapter {
```
❌ 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:
Option 1: Run tmux inside VS Code integrated terminal
┌────────────────────────────────────────┐
│ $ tmux new -s companion-teams │
│ $ companion create-team my-team │
│ $ companion spawn-teammate security-bot ... │
│ $ tmux new -s clanker-teams │
│ $ clanker create-team my-team │
│ $ clanker spawn-teammate security-bot ... │
└────────────────────────────────────────┘
Option 2: Open VS Code from tmux session
┌────────────────────────────────────────┐
│ $ tmux new -s my-session │
│ $ code . │
│ $ companion create-team my-team │
│ $ clanker create-team my-team │
└────────────────────────────────────────┘
Option 3: Use a terminal with multiplexer support
@ -690,7 +690,7 @@ Option 3: Use a terminal with multiplexer support
│ • 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
# 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
tmux new -s dev
@ -726,19 +726,19 @@ code .
### Documentation Update
Add to companion-teams README.md:
Add to clanker-teams README.md:
````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:
```bash
# Start tmux in VS Code integrated terminal
$ tmux new -s companion-teams
$ companion create-team my-team
$ companion spawn-teammate security-bot "Scan for vulnerabilities"
$ tmux new -s clanker-teams
$ clanker create-team my-team
$ clanker spawn-teammate security-bot "Scan for vulnerabilities"
```
````
@ -898,12 +898,12 @@ process.env.TERM_PROGRAM === 'iTerm.app'
```bash
# Step 1: Start tmux
tmux new -s companion-teams
tmux new -s clanker-teams
# Step 2: Use companion-teams
companion create-team my-team
companion spawn-teammate frontend-dev
companion spawn-teammate backend-dev
# Step 2: Use clanker-teams
clanker create-team my-team
clanker spawn-teammate frontend-dev
clanker spawn-teammate backend-dev
# Step 3: Enjoy multi-pane coordination
┌──────────────────┬──────────────────┬──────────────────┐

View file

@ -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 { StringEnum } from "@mariozechner/companion-ai";
import { StringEnum } from "@mariozechner/clanker-ai";
import * as paths from "../src/utils/paths";
import * as teams from "../src/utils/teams";
import * as tasks from "../src/utils/tasks";
@ -19,7 +19,7 @@ let modelsCacheTime = 0;
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 }> {
const now = Date.now();
@ -28,7 +28,7 @@ function getAvailableModels(): Array<{ provider: string; model: string }> {
}
try {
const result = spawnSync("companion", ["--list-models"], {
const result = spawnSync("clanker", ["--list-models"], {
encoding: "utf-8",
timeout: 10000,
});
@ -142,14 +142,14 @@ function resolveModelWithProvider(modelName: string): string | null {
return null;
}
export default function (companion: ExtensionAPI) {
const isTeammate = !!process.env.COMPANION_AGENT_NAME;
const agentName = process.env.COMPANION_AGENT_NAME || "team-lead";
const teamName = process.env.COMPANION_TEAM_NAME;
export default function (clanker: ExtensionAPI) {
const isTeammate = !!process.env.CLANKER_AGENT_NAME;
const agentName = process.env.CLANKER_AGENT_NAME || "team-lead";
const teamName = process.env.CLANKER_TEAM_NAME;
const terminal = getTerminalAdapter();
companion.on("session_start", async (_event, ctx) => {
clanker.on("session_start", async (_event, ctx) => {
paths.ensureDirs();
if (isTeammate) {
if (teamName) {
@ -157,7 +157,7 @@ export default function (companion: ExtensionAPI) {
fs.writeFileSync(pidFile, process.pid.toString());
}
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) {
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
@ -172,7 +172,7 @@ export default function (companion: ExtensionAPI) {
}
setTimeout(() => {
companion.sendUserMessage(
clanker.sendUserMessage(
`I am starting my work as '${agentName}' on team '${teamName}'. Checking my inbox for instructions...`,
);
}, 1000);
@ -186,18 +186,18 @@ export default function (companion: ExtensionAPI) {
false,
);
if (unread.length > 0) {
companion.sendUserMessage(
clanker.sendUserMessage(
`I have ${unread.length} new message(s) in my inbox. Reading them now...`,
);
}
}
}, 30000);
} 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) {
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
if ((ctx.ui as any).setTitle) (ctx.ui as any).setTitle(fullTitle);
@ -206,7 +206,7 @@ export default function (companion: ExtensionAPI) {
});
let firstTurn = true;
companion.on("before_agent_start", async (event, ctx) => {
clanker.on("before_agent_start", async (event, ctx) => {
if (isTeammate && firstTurn) {
firstTurn = false;
@ -259,7 +259,7 @@ export default function (companion: ExtensionAPI) {
}
// Tools
companion.registerTool({
clanker.registerTool({
name: "team_create",
label: "Create Team",
description: "Create a new agent team.",
@ -290,7 +290,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "spawn_teammate",
label: "Spawn Teammate",
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
if (chosenModel) {
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);
if (resolved) {
chosenModel = resolved;
@ -371,7 +371,7 @@ export default function (companion: ExtensionAPI) {
"Initial prompt",
);
const piBinary = "companion";
const piBinary = "clanker";
let piCmd = piBinary;
if (chosenModel) {
@ -387,8 +387,8 @@ export default function (companion: ExtensionAPI) {
const env: Record<string, string> = {
...process.env,
COMPANION_TEAM_NAME: safeTeamName,
COMPANION_AGENT_NAME: safeName,
CLANKER_TEAM_NAME: safeTeamName,
CLANKER_AGENT_NAME: safeName,
};
let terminalId = "";
@ -452,7 +452,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "spawn_lead_window",
label: "Spawn Lead 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 cwd = params.cwd || process.cwd();
const piBinary = "companion";
const piBinary = "clanker";
let piCmd = piBinary;
if (teamConfig.defaultModel) {
// Use the combined --model provider/model format
@ -478,8 +478,8 @@ export default function (companion: ExtensionAPI) {
const env = {
...process.env,
COMPANION_TEAM_NAME: safeTeamName,
COMPANION_AGENT_NAME: "team-lead",
CLANKER_TEAM_NAME: safeTeamName,
CLANKER_AGENT_NAME: "team-lead",
};
try {
const windowId = terminal.spawnWindow({
@ -500,7 +500,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "send_message",
label: "Send Message",
description: "Send a message to a teammate.",
@ -527,7 +527,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "broadcast_message",
label: "Broadcast Message",
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",
label: "Read 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",
label: "Create Task",
description: "Create a new team task.",
@ -603,7 +603,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "task_submit_plan",
label: "Submit Plan",
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",
label: "Evaluate Plan",
description: "Evaluate a submitted plan for a task.",
@ -658,7 +658,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "task_list",
label: "List Tasks",
description: "List all tasks for a team.",
@ -674,7 +674,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "task_update",
label: "Update Task",
description: "Update a task's status or owner.",
@ -704,7 +704,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "team_shutdown",
label: "Shutdown Team",
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",
label: "Read Task",
description: "Read details of a specific task.",
@ -749,7 +749,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "check_teammate",
label: "Check Teammate",
description: "Check a single teammate's status.",
@ -789,7 +789,7 @@ export default function (companion: ExtensionAPI) {
},
});
companion.registerTool({
clanker.registerTool({
name: "process_shutdown_approved",
label: "Process Shutdown Approved",
description: "Process a teammate's shutdown.",

View file

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before After
Before After

View file

@ -1,15 +1,16 @@
{
"name": "companion-teams",
"name": "clanker-teams",
"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": {
"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",
"license": "MIT",
"keywords": [
"companion-package"
"clanker-package"
],
"scripts": {
"test": "vitest run"
@ -26,11 +27,11 @@
"uuid": "^11.1.0"
},
"peerDependencies": {
"@mariozechner/companion-coding-agent": "*",
"@harivansh-afk/clanker-coding-agent": "*",
"@sinclair/typebox": "*"
},
"companion": {
"image": "https://raw.githubusercontent.com/burggraf/companion-teams/main/companion-team-in-action.png",
"clanker": {
"image": "https://raw.githubusercontent.com/burggraf/clanker-teams/main/clanker-team-in-action.png",
"extensions": [
"extensions/index.ts"
],

View file

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before After
Before After

View file

@ -26,7 +26,7 @@ export class CmuxAdapter implements TerminalAdapter {
// Construct the command with environment variables
const envPrefix = Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");
@ -124,7 +124,7 @@ export class CmuxAdapter implements TerminalAdapter {
// Wait a bit for the window to be ready?
const envPrefix = Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");

Some files were not shown because too many files have changed in this diff Show more