mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 05:02:07 +00:00
chore: rebrand companion-os to clanker-agent
- Rename all package names from companion-* to clanker-* - Update npm scopes from @mariozechner to @harivansh-afk - Rename config directories .companion -> .clanker - Rename environment variables COMPANION_* -> CLANKER_* - Update all documentation, README files, and install scripts - Rename package directories (companion-channels, companion-grind, companion-teams) - Update GitHub URLs to harivansh-afk/clanker-agent - Preserve full git history from companion-cloud monorepo
This commit is contained in:
parent
f93fe7d1a0
commit
67168d8289
356 changed files with 2249 additions and 10223 deletions
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
streamSimple,
|
||||
type ToolResultMessage,
|
||||
validateToolArguments,
|
||||
} from "@mariozechner/companion-ai";
|
||||
} from "@mariozechner/clanker-ai";
|
||||
import type {
|
||||
AgentContext,
|
||||
AgentEvent,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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.",
|
||||
});
|
||||
|
||||
|
|
@ -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 },
|
||||
|
|
@ -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 },
|
||||
|
|
@ -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.
|
||||
|
|
@ -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();
|
||||
|
|
@ -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));
|
||||
|
|
@ -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 = createLogger(clanker);
|
||||
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);
|
||||
}
|
||||
8
packages/clanker-channels/src/logger.ts
Normal file
8
packages/clanker-channels/src/logger.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { ExtensionAPI } from "@mariozechner/clanker-coding-agent";
|
||||
|
||||
const CHANNEL = "channels";
|
||||
|
||||
export function createLogger(clanker: ExtensionAPI) {
|
||||
return (event: string, data: unknown, level = "INFO") =>
|
||||
clanker.events.emit("log", { channel: CHANNEL, event, level, data });
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* companion-channels — Adapter registry + route resolution.
|
||||
* clanker-channels — Adapter registry + route resolution.
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
|
@ -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
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
@ -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",
|
||||
|
|
@ -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 = [
|
||||
|
|
@ -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") {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"];
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
.companion
|
||||
.clanker
|
||||
dist
|
||||
*.log
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
# companion-teams: Agent Guide 🤖
|
||||
# clanker-teams: Agent Guide 🤖
|
||||
|
||||
This guide explains how `companion-teams` transforms your single companion agent into a coordinated team of specialists. It covers the roles, capabilities, and coordination patterns available to you as the **Team Lead**.
|
||||
This guide explains how `clanker-teams` transforms your single clanker agent into a coordinated team of specialists. It covers the roles, capabilities, and coordination patterns available to you as the **Team Lead**.
|
||||
|
||||
---
|
||||
|
||||
## 🎭 The Two Roles
|
||||
|
||||
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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
@ -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`,
|
||||
);
|
||||
|
|
@ -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/
|
||||
```
|
||||
|
|
@ -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:**
|
||||
|
||||
|
|
@ -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."
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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."
|
||||
|
||||
---
|
||||
|
|
@ -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
|
||||
┌──────────────────┬──────────────────┬──────────────────┐
|
||||
|
|
@ -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.",
|
||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"name": "companion-teams",
|
||||
"name": "clanker-teams",
|
||||
"version": "0.8.6",
|
||||
"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"
|
||||
],
|
||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
|
@ -26,7 +26,7 @@ export class CmuxAdapter implements TerminalAdapter {
|
|||
|
||||
// Construct the command with environment variables
|
||||
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(" ");
|
||||
|
||||
|
|
@ -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(" ");
|
||||
|
||||
|
|
@ -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"]),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
@ -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: {},
|
||||
});
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue