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

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(" ");

View file

@ -49,7 +49,7 @@ export class Iterm2Adapter implements TerminalAdapter {
spawn(options: SpawnOptions): string {
const envStr = Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");
@ -186,7 +186,7 @@ end tell`;
*/
spawnWindow(options: SpawnOptions): string {
const envStr = Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`)
.join(" ");

View file

@ -60,8 +60,8 @@ describe("TmuxAdapter", () => {
adapter.spawn({
name: "worker",
cwd: "/tmp/project",
command: "companion",
env: { COMPANION_TEAM_NAME: "demo", COMPANION_AGENT_NAME: "worker" },
command: "clanker",
env: { CLANKER_TEAM_NAME: "demo", CLANKER_AGENT_NAME: "worker" },
}),
).toBe("%1");
@ -71,7 +71,7 @@ describe("TmuxAdapter", () => {
"new-session",
"-d",
"-s",
"companion-teams-demo",
"clanker-teams-demo",
]),
);
});
@ -92,14 +92,14 @@ describe("TmuxAdapter", () => {
adapter.spawn({
name: "worker",
cwd: "/tmp/project",
command: "companion",
env: { COMPANION_TEAM_NAME: "demo", COMPANION_AGENT_NAME: "worker" },
command: "clanker",
env: { CLANKER_TEAM_NAME: "demo", CLANKER_AGENT_NAME: "worker" },
}),
).toBe("%2");
expect(mockExecCommand).toHaveBeenCalledWith(
"tmux",
expect.arrayContaining(["split-window", "-t", "companion-teams-demo:0"]),
expect.arrayContaining(["split-window", "-t", "clanker-teams-demo:0"]),
);
});

View file

@ -25,12 +25,12 @@ export class TmuxAdapter implements TerminalAdapter {
spawn(options: SpawnOptions): string {
const envArgs = Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`);
let targetWindow: string | null = null;
if (!process.env.TMUX) {
const sessionName = `companion-teams-${options.env.COMPANION_TEAM_NAME || "default"}`;
const sessionName = `clanker-teams-${options.env.CLANKER_TEAM_NAME || "default"}`;
targetWindow = `${sessionName}:0`;
const hasSession = execCommand("tmux", [
"has-session",

View file

@ -61,8 +61,8 @@ describe("WezTermAdapter", () => {
const result = adapter.spawn({
name: "test-agent",
cwd: "/home/user/project",
command: "companion --agent test",
env: { COMPANION_AGENT_ID: "test-123" },
command: "clanker --agent test",
env: { CLANKER_AGENT_ID: "test-123" },
});
expect(result).toBe("wezterm_1");
@ -101,7 +101,7 @@ describe("WezTermAdapter", () => {
const result = adapter.spawn({
name: "agent2",
cwd: "/home/user/project",
command: "companion",
command: "clanker",
env: {},
});

View file

@ -87,7 +87,7 @@ export class WezTermAdapter implements TerminalAdapter {
const panes = this.getPanes();
const envArgs = Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`);
let weztermArgs: string[];
@ -211,7 +211,7 @@ export class WezTermAdapter implements TerminalAdapter {
}
const envArgs = Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`);
// Format window title as "teamName: agentName" if teamName is provided

View file

@ -30,7 +30,7 @@ export class ZellijAdapter implements TerminalAdapter {
"--",
"env",
...Object.entries(options.env)
.filter(([k]) => k.startsWith("COMPANION_"))
.filter(([k]) => k.startsWith("CLANKER_"))
.map(([k, v]) => `${k}=${v}`),
"sh",
"-c",

View file

@ -4,7 +4,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { runHook } from "./hooks";
describe("runHook", () => {
const hooksDir = path.join(process.cwd(), ".companion", "team-hooks");
const hooksDir = path.join(process.cwd(), ".clanker", "team-hooks");
beforeAll(() => {
if (!fs.existsSync(hooksDir)) {

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