@teammates/cli 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/adapter.js CHANGED
@@ -125,66 +125,66 @@ export function buildTeammatePrompt(teammate, taskPrompt, options) {
125
125
  // ── Session state ────────────────────────────────────────────────
126
126
  if (options?.sessionFile) {
127
127
  parts.push("## Session State\n");
128
- parts.push(`Your session file is at: \`${options.sessionFile}\`
129
-
130
- **Before returning your result**, append a brief entry to this file with:
131
- - What you did
132
- - Key decisions made
133
- - Files changed
134
- - Anything the next task should know
135
-
136
- This is how you maintain continuity across tasks. Always read it, always update it.
128
+ parts.push(`Your session file is at: \`${options.sessionFile}\`
129
+
130
+ **Before returning your result**, append a brief entry to this file with:
131
+ - What you did
132
+ - Key decisions made
133
+ - Files changed
134
+ - Anything the next task should know
135
+
136
+ This is how you maintain continuity across tasks. Always read it, always update it.
137
137
  `);
138
138
  parts.push("\n---\n");
139
139
  }
140
140
  // ── Memory updates ─────────────────────────────────────────────────
141
141
  const today = new Date().toISOString().slice(0, 10);
142
142
  parts.push("## Memory Updates\n");
143
- parts.push(`**Before returning your result**, update your memory files:
144
-
145
- 1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist.
146
- - What you did
147
- - Key decisions made
148
- - Files changed
149
- - Anything the next task should know
150
-
151
- 2. **Typed memories** — If you learned something durable (a decision, pattern, feedback, or reference), create a typed memory file at \`.teammates/${teammate.name}/memory/<type>_<topic>.md\` with frontmatter (\`name\`, \`description\`, \`type\`). Update existing memory files if the topic already has one.
152
-
153
- 3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.
154
-
155
- These files are your persistent memory. Without them, your next session starts from scratch.
143
+ parts.push(`**Before returning your result**, update your memory files:
144
+
145
+ 1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist.
146
+ - What you did
147
+ - Key decisions made
148
+ - Files changed
149
+ - Anything the next task should know
150
+
151
+ 2. **Typed memories** — If you learned something durable (a decision, pattern, feedback, or reference), create a typed memory file at \`.teammates/${teammate.name}/memory/<type>_<topic>.md\` with frontmatter (\`name\`, \`description\`, \`type\`). Update existing memory files if the topic already has one.
152
+
153
+ 3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.
154
+
155
+ These files are your persistent memory. Without them, your next session starts from scratch.
156
156
  `);
157
157
  parts.push("\n---\n");
158
158
  // ── Output protocol ───────────────────────────────────────────────
159
159
  parts.push("## Output Protocol (CRITICAL)\n");
160
- parts.push(`**Your #1 job is to produce a visible text response.** Session updates and memory writes are secondary — they support continuity but are not the deliverable. The user sees ONLY your text output. If you update files but return no text, the user sees an empty message and your work is invisible.
161
-
162
- Format your response as:
163
-
164
- \`\`\`
165
- TO: user
166
- # <Subject line>
167
-
168
- <Body — full markdown response>
169
- \`\`\`
170
-
171
- **Handoffs:** To hand off work to a teammate, include a fenced handoff block anywhere in your response:
172
-
173
- \`\`\`
174
- \`\`\`handoff
175
- @<teammate>
176
- <task description — what you need them to do, with full context>
177
- \`\`\`
178
- \`\`\`
179
-
180
- **Rules:**
181
- - **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.
182
- - The \`# Subject\` line is REQUIRED — it becomes the message title.
183
- - Always write a substantive body. Never return just the subject.
184
- - Use markdown: headings, lists, code blocks, bold, etc.
185
- - Do as much work as you can before handing off.
186
- - Only hand off to teammates listed in "Your Team" above.
187
- - The handoff block can appear anywhere in your response — it will be detected automatically.
160
+ parts.push(`**Your #1 job is to produce a visible text response.** Session updates and memory writes are secondary — they support continuity but are not the deliverable. The user sees ONLY your text output. If you update files but return no text, the user sees an empty message and your work is invisible.
161
+
162
+ Format your response as:
163
+
164
+ \`\`\`
165
+ TO: user
166
+ # <Subject line>
167
+
168
+ <Body — full markdown response>
169
+ \`\`\`
170
+
171
+ **Handoffs:** To hand off work to a teammate, include a fenced handoff block anywhere in your response:
172
+
173
+ \`\`\`
174
+ \`\`\`handoff
175
+ @<teammate>
176
+ <task description — what you need them to do, with full context>
177
+ \`\`\`
178
+ \`\`\`
179
+
180
+ **Rules:**
181
+ - **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.
182
+ - The \`# Subject\` line is REQUIRED — it becomes the message title.
183
+ - Always write a substantive body. Never return just the subject.
184
+ - Use markdown: headings, lists, code blocks, bold, etc.
185
+ - Do as much work as you can before handing off.
186
+ - Only hand off to teammates listed in "Your Team" above.
187
+ - The handoff block can appear anywhere in your response — it will be detected automatically.
188
188
  `);
189
189
  parts.push("\n---\n");
190
190
  // ── Current date/time ────────────────────────────────────────────
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Animated startup banner for @teammates/cli.
3
+ */
4
+ import { type Constraint, Control, type DrawingContext, type Rect, type Size } from "@teammates/consolonia";
5
+ export type ServiceStatus = "bundled" | "missing" | "not-configured" | "configured";
6
+ export interface ServiceInfo {
7
+ name: string;
8
+ status: ServiceStatus;
9
+ }
10
+ export interface BannerInfo {
11
+ adapterName: string;
12
+ teammateCount: number;
13
+ cwd: string;
14
+ teammates: {
15
+ name: string;
16
+ role: string;
17
+ }[];
18
+ services: ServiceInfo[];
19
+ }
20
+ /**
21
+ * Custom banner widget that plays a reveal animation inside the
22
+ * consolonia rendering loop (alternate screen already active).
23
+ *
24
+ * Phases:
25
+ * 1. Reveal "teammates" letter by letter in block font
26
+ * 2. Collapse to "TM" + stats panel
27
+ * 3. Fade in teammate roster
28
+ * 4. Fade in command reference
29
+ */
30
+ export declare class AnimatedBanner extends Control {
31
+ private _lines;
32
+ private _info;
33
+ private _phase;
34
+ private _inner;
35
+ private _timer;
36
+ private _onDirty;
37
+ private _word;
38
+ private _charIndex;
39
+ private _builtTop;
40
+ private _builtBot;
41
+ private _versionStr;
42
+ private _versionIndex;
43
+ private _revealIndex;
44
+ /** When true, the animation pauses after roster reveal (before commands). */
45
+ private _held;
46
+ private _finalLines;
47
+ private _rosterStart;
48
+ private _commandsStart;
49
+ private static GLYPHS;
50
+ constructor(info: BannerInfo);
51
+ /** Set a callback that fires when the banner needs a re-render. */
52
+ set onDirty(fn: () => void);
53
+ /** Start the animation sequence. */
54
+ start(): void;
55
+ private _buildFinalLines;
56
+ private _tick;
57
+ private _apply;
58
+ private _schedule;
59
+ /**
60
+ * Hold the animation — it will pause after the roster phase and
61
+ * not reveal the command reference until releaseHold() is called.
62
+ */
63
+ hold(): void;
64
+ /**
65
+ * Release the hold and continue to the commands phase.
66
+ * If the animation already reached the hold point, it resumes immediately.
67
+ */
68
+ releaseHold(): void;
69
+ /** Cancel any pending animation timer. */
70
+ dispose(): void;
71
+ measure(constraint: Constraint): Size;
72
+ arrange(rect: Rect): void;
73
+ render(ctx: DrawingContext): void;
74
+ }
package/dist/banner.js ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Animated startup banner for @teammates/cli.
3
+ */
4
+ import { Control, concat, StyledText, } from "@teammates/consolonia";
5
+ import { PKG_VERSION } from "./cli-args.js";
6
+ import { buildTitle } from "./console/startup.js";
7
+ import { tp } from "./theme.js";
8
+ // ─── Animated banner widget ─────────────────────────────────────────
9
+ /**
10
+ * Custom banner widget that plays a reveal animation inside the
11
+ * consolonia rendering loop (alternate screen already active).
12
+ *
13
+ * Phases:
14
+ * 1. Reveal "teammates" letter by letter in block font
15
+ * 2. Collapse to "TM" + stats panel
16
+ * 3. Fade in teammate roster
17
+ * 4. Fade in command reference
18
+ */
19
+ export class AnimatedBanner extends Control {
20
+ _lines = [];
21
+ _info;
22
+ _phase = "idle";
23
+ _inner;
24
+ _timer = null;
25
+ _onDirty = null;
26
+ // Spelling state
27
+ _word = "teammates";
28
+ _charIndex = 0;
29
+ _builtTop = "";
30
+ _builtBot = "";
31
+ _versionStr = ` v${PKG_VERSION}`;
32
+ _versionIndex = 0;
33
+ // Roster/command reveal state
34
+ _revealIndex = 0;
35
+ /** When true, the animation pauses after roster reveal (before commands). */
36
+ _held = false;
37
+ // The final lines (built once, revealed progressively)
38
+ _finalLines = [];
39
+ // Line index where roster starts and commands start
40
+ _rosterStart = 0;
41
+ _commandsStart = 0;
42
+ static GLYPHS = {
43
+ t: ["▀█▀", " █ "],
44
+ e: ["█▀▀", "██▄"],
45
+ a: ["▄▀█", "█▀█"],
46
+ m: ["█▀▄▀█", "█ ▀ █"],
47
+ s: ["█▀", "▄█"],
48
+ };
49
+ constructor(info) {
50
+ super();
51
+ this._info = info;
52
+ this._inner = new StyledText({ lines: [], wrap: true });
53
+ this.addChild(this._inner);
54
+ this._buildFinalLines();
55
+ }
56
+ /** Set a callback that fires when the banner needs a re-render. */
57
+ set onDirty(fn) {
58
+ this._onDirty = fn;
59
+ }
60
+ /** Start the animation sequence. */
61
+ start() {
62
+ this._phase = "spelling";
63
+ this._charIndex = 0;
64
+ this._builtTop = "";
65
+ this._builtBot = "";
66
+ this._tick();
67
+ }
68
+ _buildFinalLines() {
69
+ const info = this._info;
70
+ const [tmTop, tmBot] = buildTitle("tm");
71
+ const tmPad = " ".repeat(tmTop.length);
72
+ const gap = " ";
73
+ const lines = [];
74
+ // TM logo row 1 + adapter info
75
+ lines.push(concat(tp.accent(tmTop), tp.text(gap + info.adapterName), tp.muted(` · ${info.teammateCount} teammate${info.teammateCount === 1 ? "" : "s"}`), tp.muted(` · v${PKG_VERSION}`)));
76
+ // TM logo row 2 + cwd
77
+ lines.push(concat(tp.accent(tmBot), tp.muted(gap + info.cwd)));
78
+ // Service status rows
79
+ for (const svc of info.services) {
80
+ const isBundledOrConfigured = svc.status === "bundled" || svc.status === "configured";
81
+ const icon = isBundledOrConfigured ? "● " : svc.status === "not-configured" ? "◐ " : "○ ";
82
+ const color = isBundledOrConfigured ? tp.success : tp.warning;
83
+ const label = svc.status === "bundled"
84
+ ? "bundled"
85
+ : svc.status === "configured"
86
+ ? "configured"
87
+ : svc.status === "not-configured"
88
+ ? `not configured — /configure ${svc.name.toLowerCase()}`
89
+ : `missing — /configure ${svc.name.toLowerCase()}`;
90
+ lines.push(concat(tp.text(tmPad + gap), color(icon), color(svc.name), tp.muted(` ${label}`)));
91
+ }
92
+ // blank
93
+ lines.push("");
94
+ this._rosterStart = lines.length;
95
+ // Teammate roster
96
+ for (const t of info.teammates) {
97
+ lines.push(concat(tp.accent(" ● "), tp.accent(`@${t.name}`.padEnd(14)), tp.muted(t.role)));
98
+ }
99
+ // blank
100
+ lines.push("");
101
+ this._commandsStart = lines.length;
102
+ // Command reference (must match printBanner normal-mode layout)
103
+ const col1 = [
104
+ ["@mention", "assign to teammate"],
105
+ ["text", "auto-route task"],
106
+ ["[image]", "drag & drop images"],
107
+ ];
108
+ const col2 = [
109
+ ["/status", "teammates & queue"],
110
+ ["/compact", "compact memory"],
111
+ ["/retro", "run retrospective"],
112
+ ];
113
+ const col3 = [
114
+ ["/copy", "copy session text"],
115
+ ["/help", "all commands"],
116
+ ["/exit", "exit session"],
117
+ ];
118
+ for (let i = 0; i < col1.length; i++) {
119
+ lines.push(concat(tp.accent(` ${col1[i][0].padEnd(12)}`), tp.muted(col1[i][1].padEnd(22)), tp.accent(col2[i][0].padEnd(12)), tp.muted(col2[i][1].padEnd(22)), tp.accent(col3[i][0].padEnd(12)), tp.muted(col3[i][1])));
120
+ }
121
+ this._finalLines = lines;
122
+ }
123
+ _tick() {
124
+ switch (this._phase) {
125
+ case "spelling": {
126
+ const ch = this._word[this._charIndex];
127
+ const g = AnimatedBanner.GLYPHS[ch];
128
+ if (g) {
129
+ if (this._builtTop.length > 0) {
130
+ this._builtTop += " ";
131
+ this._builtBot += " ";
132
+ }
133
+ this._builtTop += g[0];
134
+ this._builtBot += g[1];
135
+ }
136
+ this._lines = [
137
+ concat(tp.accent(this._builtTop)),
138
+ concat(tp.accent(this._builtBot)),
139
+ ];
140
+ this._apply();
141
+ this._charIndex++;
142
+ if (this._charIndex >= this._word.length) {
143
+ this._phase = "version";
144
+ this._versionIndex = 0;
145
+ this._schedule(60);
146
+ }
147
+ else {
148
+ this._schedule(60);
149
+ }
150
+ break;
151
+ }
152
+ case "version": {
153
+ // Type out version string character by character on the bottom row
154
+ this._versionIndex++;
155
+ const partial = this._versionStr.slice(0, this._versionIndex);
156
+ this._lines = [
157
+ concat(tp.accent(this._builtTop)),
158
+ concat(tp.accent(this._builtBot), tp.muted(partial)),
159
+ ];
160
+ this._apply();
161
+ if (this._versionIndex >= this._versionStr.length) {
162
+ this._phase = "pause";
163
+ this._schedule(600);
164
+ }
165
+ else {
166
+ this._schedule(60);
167
+ }
168
+ break;
169
+ }
170
+ case "pause": {
171
+ // Brief pause before transitioning to compact view
172
+ this._phase = "compact";
173
+ this._schedule(800);
174
+ break;
175
+ }
176
+ case "compact": {
177
+ // Switch to TM + stats — show first 4 lines of final
178
+ this._lines = this._finalLines.slice(0, 4);
179
+ this._apply();
180
+ this._phase = "roster";
181
+ this._revealIndex = 0;
182
+ this._schedule(80);
183
+ break;
184
+ }
185
+ case "roster": {
186
+ // Reveal roster lines one at a time
187
+ const end = this._rosterStart + this._revealIndex + 1;
188
+ this._lines = [
189
+ ...this._finalLines.slice(0, this._rosterStart),
190
+ ...this._finalLines.slice(this._rosterStart, end),
191
+ ];
192
+ this._apply();
193
+ this._revealIndex++;
194
+ const rosterCount = this._commandsStart - 1 - this._rosterStart; // -1 for blank line
195
+ if (this._revealIndex >= rosterCount) {
196
+ if (this._held) {
197
+ // Pause here until releaseHold() is called
198
+ this._phase = "roster-held";
199
+ }
200
+ else {
201
+ this._phase = "commands";
202
+ this._revealIndex = 0;
203
+ this._schedule(80);
204
+ }
205
+ }
206
+ else {
207
+ this._schedule(40);
208
+ }
209
+ break;
210
+ }
211
+ case "commands": {
212
+ // Add the blank line between roster and commands, then reveal commands
213
+ const rosterEnd = this._commandsStart; // includes the blank line
214
+ const cmdEnd = this._commandsStart + this._revealIndex + 1;
215
+ this._lines = [
216
+ ...this._finalLines.slice(0, rosterEnd),
217
+ ...this._finalLines.slice(this._commandsStart, cmdEnd),
218
+ ];
219
+ this._apply();
220
+ this._revealIndex++;
221
+ const cmdCount = this._finalLines.length - this._commandsStart;
222
+ if (this._revealIndex >= cmdCount) {
223
+ this._phase = "done";
224
+ }
225
+ else {
226
+ this._schedule(30);
227
+ }
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ _apply() {
233
+ this._inner.lines = this._lines;
234
+ this.invalidate();
235
+ if (this._onDirty)
236
+ this._onDirty();
237
+ }
238
+ _schedule(ms) {
239
+ this._timer = setTimeout(() => {
240
+ this._timer = null;
241
+ this._tick();
242
+ }, ms);
243
+ }
244
+ /**
245
+ * Hold the animation — it will pause after the roster phase and
246
+ * not reveal the command reference until releaseHold() is called.
247
+ */
248
+ hold() {
249
+ this._held = true;
250
+ }
251
+ /**
252
+ * Release the hold and continue to the commands phase.
253
+ * If the animation already reached the hold point, it resumes immediately.
254
+ */
255
+ releaseHold() {
256
+ this._held = false;
257
+ // If we're waiting at the hold point, resume
258
+ if (this._phase === "roster-held") {
259
+ this._phase = "commands";
260
+ this._revealIndex = 0;
261
+ this._schedule(80);
262
+ }
263
+ }
264
+ /** Cancel any pending animation timer. */
265
+ dispose() {
266
+ if (this._timer) {
267
+ clearTimeout(this._timer);
268
+ this._timer = null;
269
+ }
270
+ }
271
+ // ── Layout delegation ───────────────────────────────────────────
272
+ measure(constraint) {
273
+ const size = this._inner.measure(constraint);
274
+ this.desiredSize = size;
275
+ return size;
276
+ }
277
+ arrange(rect) {
278
+ this.bounds = rect;
279
+ this._inner.arrange(rect);
280
+ }
281
+ render(ctx) {
282
+ this._inner.render(ctx);
283
+ }
284
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * CLI argument parsing, version, and startup helpers for @teammates/cli.
3
+ */
4
+ import type { AgentAdapter } from "./adapter.js";
5
+ export declare const PKG_VERSION: string;
6
+ export interface CliArgs {
7
+ showHelp: boolean;
8
+ modelOverride: string | undefined;
9
+ dirOverride: string | undefined;
10
+ adapterName: string;
11
+ agentPassthrough: string[];
12
+ }
13
+ export declare function parseCliArgs(argv?: string[]): CliArgs;
14
+ export declare function findTeammatesDir(dirOverride: string | undefined): Promise<string | null>;
15
+ export declare function resolveAdapter(name: string, opts?: {
16
+ modelOverride?: string;
17
+ agentPassthrough?: string[];
18
+ }): Promise<AgentAdapter>;
19
+ export declare function printUsage(): void;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * CLI argument parsing, version, and startup helpers for @teammates/cli.
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+ import { stat } from "node:fs/promises";
6
+ import { join, resolve } from "node:path";
7
+ import chalk from "chalk";
8
+ import { CliProxyAdapter, PRESETS } from "./adapters/cli-proxy.js";
9
+ import { EchoAdapter } from "./adapters/echo.js";
10
+ // ─── Version ─────────────────────────────────────────────────────────
11
+ export const PKG_VERSION = (() => {
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
14
+ return pkg.version ?? "0.0.0";
15
+ }
16
+ catch {
17
+ return "0.0.0";
18
+ }
19
+ })();
20
+ export function parseCliArgs(argv = process.argv.slice(2)) {
21
+ const args = [...argv];
22
+ function getFlag(name) {
23
+ const idx = args.indexOf(`--${name}`);
24
+ if (idx >= 0) {
25
+ args.splice(idx, 1);
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ function getOption(name) {
31
+ const idx = args.indexOf(`--${name}`);
32
+ if (idx >= 0 && idx + 1 < args.length) {
33
+ const val = args[idx + 1];
34
+ args.splice(idx, 2);
35
+ return val;
36
+ }
37
+ return undefined;
38
+ }
39
+ const showHelp = getFlag("help");
40
+ const modelOverride = getOption("model");
41
+ const dirOverride = getOption("dir");
42
+ const adapterName = args.shift() ?? "echo";
43
+ const agentPassthrough = [...args];
44
+ return {
45
+ showHelp,
46
+ modelOverride,
47
+ dirOverride,
48
+ adapterName,
49
+ agentPassthrough,
50
+ };
51
+ }
52
+ // ─── Helpers ─────────────────────────────────────────────────────────
53
+ export async function findTeammatesDir(dirOverride) {
54
+ if (dirOverride)
55
+ return resolve(dirOverride);
56
+ let dir = process.cwd();
57
+ while (true) {
58
+ const candidate = join(dir, ".teammates");
59
+ try {
60
+ const s = await stat(candidate);
61
+ if (s.isDirectory())
62
+ return candidate;
63
+ }
64
+ catch {
65
+ /* keep looking */
66
+ }
67
+ const parent = resolve(dir, "..");
68
+ if (parent === dir)
69
+ break;
70
+ dir = parent;
71
+ }
72
+ return null;
73
+ }
74
+ export async function resolveAdapter(name, opts = {}) {
75
+ if (name === "echo")
76
+ return new EchoAdapter();
77
+ // GitHub Copilot SDK adapter — lazy-loaded to avoid pulling in
78
+ // @github/copilot-sdk (and vscode-jsonrpc) when not needed.
79
+ if (name === "copilot") {
80
+ const { CopilotAdapter } = await import("./adapters/copilot.js");
81
+ return new CopilotAdapter({
82
+ model: opts.modelOverride,
83
+ });
84
+ }
85
+ // All other adapters go through the CLI proxy
86
+ if (PRESETS[name]) {
87
+ return new CliProxyAdapter({
88
+ preset: name,
89
+ model: opts.modelOverride,
90
+ extraFlags: opts.agentPassthrough,
91
+ });
92
+ }
93
+ const available = ["echo", "copilot", ...Object.keys(PRESETS)].join(", ");
94
+ console.error(chalk.red(`Unknown adapter: ${name}`));
95
+ console.error(`Available adapters: ${available}`);
96
+ process.exit(1);
97
+ }
98
+ // ─── Usage ───────────────────────────────────────────────────────────
99
+ export function printUsage() {
100
+ console.log(`
101
+ ${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
102
+
103
+ ${chalk.bold("Usage:")}
104
+ teammates <agent> Launch session with an agent
105
+ teammates claude Use Claude Code
106
+ teammates codex Use OpenAI Codex
107
+ teammates aider Use Aider
108
+
109
+ ${chalk.bold("Options:")}
110
+ --model <model> Override the agent model
111
+ --dir <path> Override .teammates/ location
112
+
113
+ ${chalk.bold("Agents:")}
114
+ claude Claude Code CLI (requires 'claude' on PATH)
115
+ codex OpenAI Codex CLI (requires 'codex' on PATH)
116
+ aider Aider CLI (requires 'aider' on PATH)
117
+ echo Test adapter — echoes prompts (no external agent)
118
+
119
+ ${chalk.bold("In-session:")}
120
+ @teammate <task> Assign directly via @mention
121
+ <text> Auto-route to the best teammate
122
+ /status Session overview
123
+ /help All commands
124
+ `.trim());
125
+ }