@teammates/cli 0.3.1 → 0.3.3

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/cli.js CHANGED
@@ -7,390 +7,27 @@
7
7
  * teammates --adapter codex Use a specific agent adapter
8
8
  * teammates --dir <path> Override .teammates/ location
9
9
  */
10
- import { exec as execCb, } from "node:child_process";
10
+ import { exec as execCb, execSync, spawn } from "node:child_process";
11
11
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
13
13
  import { tmpdir } from "node:os";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { createInterface } from "node:readline";
16
- import { App, ChatView, Control, concat, esc, Interview, pen, renderMarkdown, StyledText, stripAnsi, } from "@teammates/consolonia";
16
+ import { App, ChatView, concat, esc, Interview, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
17
17
  import chalk from "chalk";
18
18
  import ora from "ora";
19
19
  import { syncRecallIndex } from "./adapter.js";
20
- import { CliProxyAdapter, PRESETS } from "./adapters/cli-proxy.js";
21
- import { EchoAdapter } from "./adapters/echo.js";
20
+ import { AnimatedBanner } from "./banner.js";
21
+ import { findTeammatesDir, PKG_VERSION, parseCliArgs, printUsage, resolveAdapter, } from "./cli-args.js";
22
22
  import { findAtMention, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
23
23
  import { buildWisdomPrompt, compactEpisodic } from "./compact.js";
24
24
  import { PromptInput } from "./console/prompt-input.js";
25
25
  import { buildTitle } from "./console/startup.js";
26
26
  import { buildImportAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
27
27
  import { Orchestrator } from "./orchestrator.js";
28
- import { colorToHex, theme } from "./theme.js";
29
- // ─── Version ─────────────────────────────────────────────────────────
30
- const PKG_VERSION = (() => {
31
- try {
32
- const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
33
- return pkg.version ?? "0.0.0";
34
- }
35
- catch {
36
- return "0.0.0";
37
- }
38
- })();
39
- // ─── Argument parsing ────────────────────────────────────────────────
40
- const args = process.argv.slice(2);
41
- function getFlag(name) {
42
- const idx = args.indexOf(`--${name}`);
43
- if (idx >= 0) {
44
- args.splice(idx, 1);
45
- return true;
46
- }
47
- return false;
48
- }
49
- function getOption(name) {
50
- const idx = args.indexOf(`--${name}`);
51
- if (idx >= 0 && idx + 1 < args.length) {
52
- const val = args[idx + 1];
53
- args.splice(idx, 2);
54
- return val;
55
- }
56
- return undefined;
57
- }
58
- const showHelp = getFlag("help");
59
- const modelOverride = getOption("model");
60
- const dirOverride = getOption("dir");
61
- // First remaining positional arg is the agent name (default: echo)
62
- const adapterName = args.shift() ?? "echo";
63
- // Everything left passes through to the agent CLI
64
- const agentPassthrough = [...args];
65
- args.length = 0;
66
- // ─── Helpers ─────────────────────────────────────────────────────────
67
- async function findTeammatesDir() {
68
- if (dirOverride)
69
- return resolve(dirOverride);
70
- let dir = process.cwd();
71
- while (true) {
72
- const candidate = join(dir, ".teammates");
73
- try {
74
- const s = await stat(candidate);
75
- if (s.isDirectory())
76
- return candidate;
77
- }
78
- catch {
79
- /* keep looking */
80
- }
81
- const parent = resolve(dir, "..");
82
- if (parent === dir)
83
- break;
84
- dir = parent;
85
- }
86
- return null;
87
- }
88
- async function resolveAdapter(name) {
89
- if (name === "echo")
90
- return new EchoAdapter();
91
- // GitHub Copilot SDK adapter — lazy-loaded to avoid pulling in
92
- // @github/copilot-sdk (and vscode-jsonrpc) when not needed.
93
- if (name === "copilot") {
94
- const { CopilotAdapter } = await import("./adapters/copilot.js");
95
- return new CopilotAdapter({
96
- model: modelOverride,
97
- });
98
- }
99
- // All other adapters go through the CLI proxy
100
- if (PRESETS[name]) {
101
- return new CliProxyAdapter({
102
- preset: name,
103
- model: modelOverride,
104
- extraFlags: agentPassthrough,
105
- });
106
- }
107
- const available = ["echo", "copilot", ...Object.keys(PRESETS)].join(", ");
108
- console.error(chalk.red(`Unknown adapter: ${name}`));
109
- console.error(`Available adapters: ${available}`);
110
- process.exit(1);
111
- }
112
- // WordwheelItem is now DropdownItem from @teammates/consolonia
113
- // ── Themed pen shortcuts ────────────────────────────────────────────
114
- //
115
- // Thin wrappers that read from the active theme() at call time, so
116
- // every styled span picks up the current palette automatically.
117
- const tp = {
118
- accent: (s) => pen.fg(theme().accent)(s),
119
- accentBright: (s) => pen.fg(theme().accentBright)(s),
120
- accentDim: (s) => pen.fg(theme().accentDim)(s),
121
- text: (s) => pen.fg(theme().text)(s),
122
- muted: (s) => pen.fg(theme().textMuted)(s),
123
- dim: (s) => pen.fg(theme().textDim)(s),
124
- success: (s) => pen.fg(theme().success)(s),
125
- warning: (s) => pen.fg(theme().warning)(s),
126
- error: (s) => pen.fg(theme().error)(s),
127
- info: (s) => pen.fg(theme().info)(s),
128
- bold: (s) => pen.bold.fg(theme().text)(s),
129
- };
130
- /**
131
- * Custom banner widget that plays a reveal animation inside the
132
- * consolonia rendering loop (alternate screen already active).
133
- *
134
- * Phases:
135
- * 1. Reveal "teammates" letter by letter in block font
136
- * 2. Collapse to "TM" + stats panel
137
- * 3. Fade in teammate roster
138
- * 4. Fade in command reference
139
- */
140
- class AnimatedBanner extends Control {
141
- _lines = [];
142
- _info;
143
- _phase = "idle";
144
- _inner;
145
- _timer = null;
146
- _onDirty = null;
147
- // Spelling state
148
- _word = "teammates";
149
- _charIndex = 0;
150
- _builtTop = "";
151
- _builtBot = "";
152
- _versionStr = ` v${PKG_VERSION}`;
153
- _versionIndex = 0;
154
- // Roster/command reveal state
155
- _revealIndex = 0;
156
- /** When true, the animation pauses after roster reveal (before commands). */
157
- _held = false;
158
- // The final lines (built once, revealed progressively)
159
- _finalLines = [];
160
- // Line index where roster starts and commands start
161
- _rosterStart = 0;
162
- _commandsStart = 0;
163
- static GLYPHS = {
164
- t: ["▀█▀", " █ "],
165
- e: ["█▀▀", "██▄"],
166
- a: ["▄▀█", "█▀█"],
167
- m: ["█▀▄▀█", "█ ▀ █"],
168
- s: ["█▀", "▄█"],
169
- };
170
- constructor(info) {
171
- super();
172
- this._info = info;
173
- this._inner = new StyledText({ lines: [], wrap: true });
174
- this.addChild(this._inner);
175
- this._buildFinalLines();
176
- }
177
- /** Set a callback that fires when the banner needs a re-render. */
178
- set onDirty(fn) {
179
- this._onDirty = fn;
180
- }
181
- /** Start the animation sequence. */
182
- start() {
183
- this._phase = "spelling";
184
- this._charIndex = 0;
185
- this._builtTop = "";
186
- this._builtBot = "";
187
- this._tick();
188
- }
189
- _buildFinalLines() {
190
- const info = this._info;
191
- const [tmTop, tmBot] = buildTitle("tm");
192
- const tmPad = " ".repeat(tmTop.length);
193
- const gap = " ";
194
- const lines = [];
195
- // TM logo row 1 + adapter info
196
- 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}`)));
197
- // TM logo row 2 + cwd
198
- lines.push(concat(tp.accent(tmBot), tp.muted(gap + info.cwd)));
199
- // Recall status (bundled as dependency)
200
- lines.push(concat(tp.text(tmPad + gap), tp.success("● "), tp.success("recall"), tp.muted(" bundled")));
201
- // blank
202
- lines.push("");
203
- this._rosterStart = lines.length;
204
- // Teammate roster
205
- for (const t of info.teammates) {
206
- lines.push(concat(tp.accent(" ● "), tp.accent(`@${t.name}`.padEnd(14)), tp.muted(t.role)));
207
- }
208
- // blank
209
- lines.push("");
210
- this._commandsStart = lines.length;
211
- // Command reference (must match printBanner normal-mode layout)
212
- const col1 = [
213
- ["@mention", "assign to teammate"],
214
- ["text", "auto-route task"],
215
- ["[image]", "drag & drop images"],
216
- ];
217
- const col2 = [
218
- ["/status", "teammates & queue"],
219
- ["/compact", "compact memory"],
220
- ["/retro", "run retrospective"],
221
- ];
222
- const col3 = [
223
- ["/copy", "copy session text"],
224
- ["/help", "all commands"],
225
- ["/exit", "exit session"],
226
- ];
227
- for (let i = 0; i < col1.length; i++) {
228
- 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])));
229
- }
230
- this._finalLines = lines;
231
- }
232
- _tick() {
233
- switch (this._phase) {
234
- case "spelling": {
235
- const ch = this._word[this._charIndex];
236
- const g = AnimatedBanner.GLYPHS[ch];
237
- if (g) {
238
- if (this._builtTop.length > 0) {
239
- this._builtTop += " ";
240
- this._builtBot += " ";
241
- }
242
- this._builtTop += g[0];
243
- this._builtBot += g[1];
244
- }
245
- this._lines = [
246
- concat(tp.accent(this._builtTop)),
247
- concat(tp.accent(this._builtBot)),
248
- ];
249
- this._apply();
250
- this._charIndex++;
251
- if (this._charIndex >= this._word.length) {
252
- this._phase = "version";
253
- this._versionIndex = 0;
254
- this._schedule(60);
255
- }
256
- else {
257
- this._schedule(60);
258
- }
259
- break;
260
- }
261
- case "version": {
262
- // Type out version string character by character on the bottom row
263
- this._versionIndex++;
264
- const partial = this._versionStr.slice(0, this._versionIndex);
265
- this._lines = [
266
- concat(tp.accent(this._builtTop)),
267
- concat(tp.accent(this._builtBot), tp.muted(partial)),
268
- ];
269
- this._apply();
270
- if (this._versionIndex >= this._versionStr.length) {
271
- this._phase = "pause";
272
- this._schedule(600);
273
- }
274
- else {
275
- this._schedule(60);
276
- }
277
- break;
278
- }
279
- case "pause": {
280
- // Brief pause before transitioning to compact view
281
- this._phase = "compact";
282
- this._schedule(800);
283
- break;
284
- }
285
- case "compact": {
286
- // Switch to TM + stats — show first 4 lines of final
287
- this._lines = this._finalLines.slice(0, 4);
288
- this._apply();
289
- this._phase = "roster";
290
- this._revealIndex = 0;
291
- this._schedule(80);
292
- break;
293
- }
294
- case "roster": {
295
- // Reveal roster lines one at a time
296
- const end = this._rosterStart + this._revealIndex + 1;
297
- this._lines = [
298
- ...this._finalLines.slice(0, this._rosterStart),
299
- ...this._finalLines.slice(this._rosterStart, end),
300
- ];
301
- this._apply();
302
- this._revealIndex++;
303
- const rosterCount = this._commandsStart - 1 - this._rosterStart; // -1 for blank line
304
- if (this._revealIndex >= rosterCount) {
305
- if (this._held) {
306
- // Pause here until releaseHold() is called
307
- this._phase = "roster-held";
308
- }
309
- else {
310
- this._phase = "commands";
311
- this._revealIndex = 0;
312
- this._schedule(80);
313
- }
314
- }
315
- else {
316
- this._schedule(40);
317
- }
318
- break;
319
- }
320
- case "commands": {
321
- // Add the blank line between roster and commands, then reveal commands
322
- const rosterEnd = this._commandsStart; // includes the blank line
323
- const cmdEnd = this._commandsStart + this._revealIndex + 1;
324
- this._lines = [
325
- ...this._finalLines.slice(0, rosterEnd),
326
- ...this._finalLines.slice(this._commandsStart, cmdEnd),
327
- ];
328
- this._apply();
329
- this._revealIndex++;
330
- const cmdCount = this._finalLines.length - this._commandsStart;
331
- if (this._revealIndex >= cmdCount) {
332
- this._phase = "done";
333
- }
334
- else {
335
- this._schedule(30);
336
- }
337
- break;
338
- }
339
- }
340
- }
341
- _apply() {
342
- this._inner.lines = this._lines;
343
- this.invalidate();
344
- if (this._onDirty)
345
- this._onDirty();
346
- }
347
- _schedule(ms) {
348
- this._timer = setTimeout(() => {
349
- this._timer = null;
350
- this._tick();
351
- }, ms);
352
- }
353
- /**
354
- * Hold the animation — it will pause after the roster phase and
355
- * not reveal the command reference until releaseHold() is called.
356
- */
357
- hold() {
358
- this._held = true;
359
- }
360
- /**
361
- * Release the hold and continue to the commands phase.
362
- * If the animation already reached the hold point, it resumes immediately.
363
- */
364
- releaseHold() {
365
- this._held = false;
366
- // If we're waiting at the hold point, resume
367
- if (this._phase === "roster-held") {
368
- this._phase = "commands";
369
- this._revealIndex = 0;
370
- this._schedule(80);
371
- }
372
- }
373
- /** Cancel any pending animation timer. */
374
- dispose() {
375
- if (this._timer) {
376
- clearTimeout(this._timer);
377
- this._timer = null;
378
- }
379
- }
380
- // ── Layout delegation ───────────────────────────────────────────
381
- measure(constraint) {
382
- const size = this._inner.measure(constraint);
383
- this.desiredSize = size;
384
- return size;
385
- }
386
- arrange(rect) {
387
- this.bounds = rect;
388
- this._inner.arrange(rect);
389
- }
390
- render(ctx) {
391
- this._inner.render(ctx);
392
- }
393
- }
28
+ import { colorToHex, theme, tp } from "./theme.js";
29
+ // ─── Parsed CLI arguments ────────────────────────────────────────────
30
+ const cliArgs = parseCliArgs();
394
31
  // ─── REPL ────────────────────────────────────────────────────────────
395
32
  class TeammatesREPL {
396
33
  orchestrator;
@@ -453,6 +90,8 @@ class TeammatesREPL {
453
90
  /** Quoted reply text to expand on next submit. */
454
91
  _pendingQuotedReply = null;
455
92
  defaultFooter = null; // cached default footer content
93
+ /** Cached service statuses for banner + /configure. */
94
+ serviceStatuses = [];
456
95
  // ── Animated status tracker ─────────────────────────────────────
457
96
  activeTasks = new Map();
458
97
  statusTimer = null;
@@ -1136,14 +775,14 @@ class TeammatesREPL {
1136
775
  const changes = proposals
1137
776
  .map((p) => `- **Proposal ${p.index}: ${p.title}**\n - Section: ${p.section}\n - Before: ${p.before}\n - After: ${p.after}`)
1138
777
  .join("\n\n");
1139
- const applyPrompt = `The user approved the following SOUL.md changes from your retrospective. Apply them now.
1140
-
1141
- **Edit your SOUL.md file** (\`.teammates/${teammate}/SOUL.md\`) to incorporate these changes:
1142
-
1143
- ${changes}
1144
-
1145
- After editing SOUL.md, record a brief summary of the retro outcome in your daily log: which proposals were approved and what changed.
1146
-
778
+ const applyPrompt = `The user approved the following SOUL.md changes from your retrospective. Apply them now.
779
+
780
+ **Edit your SOUL.md file** (\`.teammates/${teammate}/SOUL.md\`) to incorporate these changes:
781
+
782
+ ${changes}
783
+
784
+ After editing SOUL.md, record a brief summary of the retro outcome in your daily log: which proposals were approved and what changed.
785
+
1147
786
  Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily log.`;
1148
787
  this.taskQueue.push({ type: "agent", teammate, task: applyPrompt });
1149
788
  this.feedLine(concat(tp.muted(" Queued SOUL.md update for "), tp.accent(`@${teammate}`)));
@@ -1676,6 +1315,22 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1676
1315
  };
1677
1316
  /** Build param-completion items for the current line, if any. */
1678
1317
  getParamItems(cmdName, argsBefore, partial) {
1318
+ // Service name completion for /configure
1319
+ if (cmdName === "configure" || cmdName === "config") {
1320
+ const completedArgs = argsBefore.trim()
1321
+ ? argsBefore.trim().split(/\s+/).length
1322
+ : 0;
1323
+ if (completedArgs > 0)
1324
+ return [];
1325
+ const lower = partial.toLowerCase();
1326
+ return TeammatesREPL.CONFIGURABLE_SERVICES
1327
+ .filter((s) => s.startsWith(lower))
1328
+ .map((s) => ({
1329
+ label: s,
1330
+ description: `configure ${s}`,
1331
+ completion: `/${cmdName} ${s} `,
1332
+ }));
1333
+ }
1679
1334
  const positions = TeammatesREPL.TEAMMATE_ARG_POSITIONS[cmdName];
1680
1335
  if (!positions)
1681
1336
  return [];
@@ -1899,8 +1554,11 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1899
1554
  }
1900
1555
  // ─── Lifecycle ────────────────────────────────────────────────────
1901
1556
  async start() {
1902
- let teammatesDir = await findTeammatesDir();
1903
- const adapter = await resolveAdapter(this.adapterName);
1557
+ let teammatesDir = await findTeammatesDir(cliArgs.dirOverride);
1558
+ const adapter = await resolveAdapter(this.adapterName, {
1559
+ modelOverride: cliArgs.modelOverride,
1560
+ agentPassthrough: cliArgs.agentPassthrough,
1561
+ });
1904
1562
  this.adapter = adapter;
1905
1563
  // No .teammates/ found — offer onboarding or solo mode
1906
1564
  if (!teammatesDir) {
@@ -1993,6 +1651,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
1993
1651
  return currentValue;
1994
1652
  },
1995
1653
  });
1654
+ // ── Detect service statuses ────────────────────────────────────────
1655
+ this.serviceStatuses = this.detectServices();
1996
1656
  // ── Build animated banner for ChatView ─────────────────────────────
1997
1657
  const names = this.orchestrator.listTeammates();
1998
1658
  const reg = this.orchestrator.getRegistry();
@@ -2004,6 +1664,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2004
1664
  const t = reg.get(name);
2005
1665
  return { name, role: t?.role ?? "" };
2006
1666
  }),
1667
+ services: this.serviceStatuses,
2007
1668
  });
2008
1669
  // ── Create ChatView and Consolonia App ────────────────────────────
2009
1670
  const t = theme();
@@ -2402,7 +2063,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2402
2063
  this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
2403
2064
  this.feedLine(concat(tp.text(` ${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
2404
2065
  this.feedLine(` ${process.cwd()}`);
2405
- this.feedLine(concat(tp.success(" ● recall"), tp.muted(" bundled")));
2066
+ // Service status rows
2067
+ for (const svc of this.serviceStatuses) {
2068
+ const ok = svc.status === "bundled" || svc.status === "configured";
2069
+ const icon = ok ? "● " : svc.status === "not-configured" ? "◐ " : "○ ";
2070
+ const color = ok ? tp.success : tp.warning;
2071
+ const label = svc.status === "bundled"
2072
+ ? "bundled"
2073
+ : svc.status === "configured"
2074
+ ? "configured"
2075
+ : svc.status === "not-configured"
2076
+ ? `not configured — /configure ${svc.name.toLowerCase()}`
2077
+ : `missing — /configure ${svc.name.toLowerCase()}`;
2078
+ this.feedLine(concat(tp.text(" "), color(icon), color(svc.name), tp.muted(` ${label}`)));
2079
+ }
2406
2080
  // Roster
2407
2081
  this.feedLine();
2408
2082
  for (const name of teammates) {
@@ -2455,6 +2129,200 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2455
2129
  this.feedLine();
2456
2130
  this.refreshView();
2457
2131
  }
2132
+ // ─── Service detection ────────────────────────────────────────────
2133
+ detectGitHub() {
2134
+ try {
2135
+ execSync("gh --version", { stdio: "pipe" });
2136
+ }
2137
+ catch {
2138
+ return "missing";
2139
+ }
2140
+ try {
2141
+ execSync("gh auth status", { stdio: "pipe" });
2142
+ return "configured";
2143
+ }
2144
+ catch {
2145
+ return "not-configured";
2146
+ }
2147
+ }
2148
+ detectServices() {
2149
+ return [
2150
+ { name: "recall", status: "bundled" },
2151
+ { name: "GitHub", status: this.detectGitHub() },
2152
+ ];
2153
+ }
2154
+ // ─── /configure command ─────────────────────────────────────────
2155
+ static CONFIGURABLE_SERVICES = ["github"];
2156
+ async cmdConfigure(argsStr) {
2157
+ const serviceName = argsStr.trim().toLowerCase();
2158
+ if (!serviceName) {
2159
+ // Show status table
2160
+ this.feedLine();
2161
+ this.feedLine(tp.bold(" Services:"));
2162
+ for (const svc of this.serviceStatuses) {
2163
+ const ok = svc.status === "bundled" || svc.status === "configured";
2164
+ const icon = ok ? "● " : svc.status === "not-configured" ? "◐ " : "○ ";
2165
+ const color = ok ? tp.success : tp.warning;
2166
+ const label = svc.status === "bundled"
2167
+ ? "bundled"
2168
+ : svc.status === "configured"
2169
+ ? "configured"
2170
+ : svc.status === "not-configured"
2171
+ ? "not configured"
2172
+ : "missing";
2173
+ this.feedLine(concat(tp.text(" "), color(icon), color(svc.name.padEnd(12)), tp.muted(label)));
2174
+ }
2175
+ this.feedLine();
2176
+ this.feedLine(tp.muted(" Use /configure [service] to set up a service"));
2177
+ this.feedLine();
2178
+ this.refreshView();
2179
+ return;
2180
+ }
2181
+ if (serviceName === "github") {
2182
+ await this.configureGitHub();
2183
+ }
2184
+ else {
2185
+ this.feedLine(tp.warning(` Unknown service: ${serviceName}`));
2186
+ this.feedLine(tp.muted(` Available: ${TeammatesREPL.CONFIGURABLE_SERVICES.join(", ")}`));
2187
+ this.refreshView();
2188
+ }
2189
+ }
2190
+ async configureGitHub() {
2191
+ // Step 1: Check if gh is installed
2192
+ let ghInstalled = false;
2193
+ try {
2194
+ execSync("gh --version", { stdio: "pipe" });
2195
+ ghInstalled = true;
2196
+ }
2197
+ catch {
2198
+ // not installed
2199
+ }
2200
+ if (!ghInstalled) {
2201
+ this.feedLine();
2202
+ this.feedLine(tp.warning(" GitHub CLI is not installed."));
2203
+ this.feedLine();
2204
+ const plat = process.platform;
2205
+ let installCmd;
2206
+ let installLabel;
2207
+ if (plat === "win32") {
2208
+ installCmd = "winget install --id GitHub.cli";
2209
+ installLabel = "winget install --id GitHub.cli";
2210
+ }
2211
+ else if (plat === "darwin") {
2212
+ installCmd = "brew install gh";
2213
+ installLabel = "brew install gh";
2214
+ }
2215
+ else {
2216
+ installCmd = "sudo apt install gh";
2217
+ installLabel = "sudo apt install gh (or see https://cli.github.com)";
2218
+ }
2219
+ this.feedLine(tp.text(` Install: ${installLabel}`));
2220
+ this.feedLine();
2221
+ // Ask user
2222
+ const answer = await this.askInput("Run install command? [Y/n] ");
2223
+ if (answer.toLowerCase() === "n") {
2224
+ this.feedLine(tp.muted(" Skipped. Install manually and re-run /configure github"));
2225
+ this.refreshView();
2226
+ return;
2227
+ }
2228
+ // Spawn install in a visible subprocess
2229
+ this.feedLine(tp.muted(` Running: ${installCmd}`));
2230
+ this.refreshView();
2231
+ const installSuccess = await new Promise((res) => {
2232
+ const parts = installCmd.split(" ");
2233
+ const child = spawn(parts[0], parts.slice(1), {
2234
+ stdio: "inherit",
2235
+ shell: true,
2236
+ });
2237
+ child.on("error", () => res(false));
2238
+ child.on("exit", (code) => res(code === 0));
2239
+ });
2240
+ if (!installSuccess) {
2241
+ this.feedLine(tp.error(" Install failed. Please install manually from https://cli.github.com"));
2242
+ this.refreshView();
2243
+ return;
2244
+ }
2245
+ // Re-check
2246
+ try {
2247
+ execSync("gh --version", { stdio: "pipe" });
2248
+ ghInstalled = true;
2249
+ this.feedLine(tp.success(" ✓ GitHub CLI installed"));
2250
+ }
2251
+ catch {
2252
+ this.feedLine(tp.error(" GitHub CLI still not found after install. You may need to restart your terminal."));
2253
+ this.refreshView();
2254
+ return;
2255
+ }
2256
+ }
2257
+ else {
2258
+ this.feedLine();
2259
+ this.feedLine(tp.success(" ✓ GitHub CLI installed"));
2260
+ }
2261
+ // Step 2: Check auth
2262
+ let authed = false;
2263
+ try {
2264
+ execSync("gh auth status", { stdio: "pipe" });
2265
+ authed = true;
2266
+ }
2267
+ catch {
2268
+ // not authenticated
2269
+ }
2270
+ if (!authed) {
2271
+ this.feedLine(tp.muted(" Authentication needed — this will open your browser for GitHub OAuth."));
2272
+ this.feedLine();
2273
+ const answer = await this.askInput("Start authentication? [Y/n] ");
2274
+ if (answer.toLowerCase() === "n") {
2275
+ this.feedLine(tp.muted(" Skipped. Run /configure github when ready."));
2276
+ this.refreshView();
2277
+ this.updateServiceStatus("GitHub", "not-configured");
2278
+ return;
2279
+ }
2280
+ this.feedLine(tp.muted(" Starting auth flow..."));
2281
+ this.refreshView();
2282
+ const authSuccess = await new Promise((res) => {
2283
+ const child = spawn("gh", ["auth", "login", "--web", "--git-protocol", "https"], {
2284
+ stdio: "inherit",
2285
+ shell: true,
2286
+ });
2287
+ child.on("error", () => res(false));
2288
+ child.on("exit", (code) => res(code === 0));
2289
+ });
2290
+ if (!authSuccess) {
2291
+ this.feedLine(tp.error(" Authentication failed. Try again with /configure github"));
2292
+ this.refreshView();
2293
+ this.updateServiceStatus("GitHub", "not-configured");
2294
+ return;
2295
+ }
2296
+ // Verify
2297
+ try {
2298
+ execSync("gh auth status", { stdio: "pipe" });
2299
+ authed = true;
2300
+ }
2301
+ catch {
2302
+ this.feedLine(tp.error(" Authentication could not be verified. Try again with /configure github"));
2303
+ this.refreshView();
2304
+ this.updateServiceStatus("GitHub", "not-configured");
2305
+ return;
2306
+ }
2307
+ }
2308
+ // Get username for confirmation
2309
+ let username = "";
2310
+ try {
2311
+ username = execSync("gh api user --jq .login", { stdio: "pipe", encoding: "utf-8" }).trim();
2312
+ }
2313
+ catch {
2314
+ // non-critical
2315
+ }
2316
+ this.feedLine(tp.success(` ✓ GitHub configured${username ? ` — authenticated as @${username}` : ""}`));
2317
+ this.feedLine();
2318
+ this.refreshView();
2319
+ this.updateServiceStatus("GitHub", "configured");
2320
+ }
2321
+ updateServiceStatus(name, status) {
2322
+ const svc = this.serviceStatuses.find((s) => s.name === name);
2323
+ if (svc)
2324
+ svc.status = status;
2325
+ }
2458
2326
  registerCommands() {
2459
2327
  const cmds = [
2460
2328
  {
@@ -2541,6 +2409,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2541
2409
  description: "Show current theme colors",
2542
2410
  run: () => this.cmdTheme(),
2543
2411
  },
2412
+ {
2413
+ name: "configure",
2414
+ aliases: ["config"],
2415
+ usage: "/configure [service]",
2416
+ description: "Configure external services (github)",
2417
+ run: (args) => this.cmdConfigure(args),
2418
+ },
2544
2419
  {
2545
2420
  name: "exit",
2546
2421
  aliases: ["q", "quit"],
@@ -3254,34 +3129,34 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
3254
3129
  this.refreshView();
3255
3130
  return;
3256
3131
  }
3257
- const retroPrompt = `Run a structured self-retrospective. Review your SOUL.md, WISDOM.md, your last 2-3 weekly summaries (or last 7 daily logs if no weeklies exist), and any typed memories in your memory/ folder.
3258
-
3259
- Produce a response with these four sections:
3260
-
3261
- ## 1. What's Working
3262
- Things you do well, based on evidence from recent work. Patterns worth reinforcing or codifying into wisdom. Cite specific examples from daily logs or memories.
3263
-
3264
- ## 2. What's Not Working
3265
- Friction, recurring issues, or patterns that aren't serving the project. Be specific — cite examples from daily logs or memories if possible.
3266
-
3267
- ## 3. Proposed SOUL.md Changes
3268
- The core output. Each proposal is a **specific edit** to your SOUL.md. Use this exact format for each proposal:
3269
-
3270
- **Proposal N: <short title>**
3271
- - **Section:** <which SOUL.md section to change, e.g. Boundaries, Core Principles, Ownership>
3272
- - **Before:** <the current text to replace, or "(new entry)" if adding>
3273
- - **After:** <the exact replacement text>
3274
- - **Why:** <evidence from recent work justifying the change>
3275
-
3276
- Only propose changes to your own SOUL.md. If a change affects shared files, note that it needs a handoff.
3277
-
3278
- ## 4. Questions for the Team
3279
- Issues that can't be resolved unilaterally — they need input from other teammates or the user.
3280
-
3281
- **Rules:**
3282
- - This is a self-review of YOUR work. Do not evaluate other teammates.
3283
- - Evidence over opinion — cite specific examples.
3284
- - No busywork — if everything is working well, say "all good, no changes." That's a valid outcome.
3132
+ const retroPrompt = `Run a structured self-retrospective. Review your SOUL.md, WISDOM.md, your last 2-3 weekly summaries (or last 7 daily logs if no weeklies exist), and any typed memories in your memory/ folder.
3133
+
3134
+ Produce a response with these four sections:
3135
+
3136
+ ## 1. What's Working
3137
+ Things you do well, based on evidence from recent work. Patterns worth reinforcing or codifying into wisdom. Cite specific examples from daily logs or memories.
3138
+
3139
+ ## 2. What's Not Working
3140
+ Friction, recurring issues, or patterns that aren't serving the project. Be specific — cite examples from daily logs or memories if possible.
3141
+
3142
+ ## 3. Proposed SOUL.md Changes
3143
+ The core output. Each proposal is a **specific edit** to your SOUL.md. Use this exact format for each proposal:
3144
+
3145
+ **Proposal N: <short title>**
3146
+ - **Section:** <which SOUL.md section to change, e.g. Boundaries, Core Principles, Ownership>
3147
+ - **Before:** <the current text to replace, or "(new entry)" if adding>
3148
+ - **After:** <the exact replacement text>
3149
+ - **Why:** <evidence from recent work justifying the change>
3150
+
3151
+ Only propose changes to your own SOUL.md. If a change affects shared files, note that it needs a handoff.
3152
+
3153
+ ## 4. Questions for the Team
3154
+ Issues that can't be resolved unilaterally — they need input from other teammates or the user.
3155
+
3156
+ **Rules:**
3157
+ - This is a self-review of YOUR work. Do not evaluate other teammates.
3158
+ - Evidence over opinion — cite specific examples.
3159
+ - No busywork — if everything is working well, say "all good, no changes." That's a valid outcome.
3285
3160
  - Number each proposal (Proposal 1, Proposal 2, etc.) so the user can approve or reject individually.`;
3286
3161
  const label = targets.length > 1
3287
3162
  ? targets.map((n) => `@${n}`).join(", ")
@@ -3628,41 +3503,13 @@ Issues that can't be resolved unilaterally — they need input from other teamma
3628
3503
  this.refreshView();
3629
3504
  }
3630
3505
  }
3631
- // ─── Usage (non-interactive) ─────────────────────────────────────────
3632
- function printUsage() {
3633
- console.log(`
3634
- ${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
3635
-
3636
- ${chalk.bold("Usage:")}
3637
- teammates <agent> Launch session with an agent
3638
- teammates claude Use Claude Code
3639
- teammates codex Use OpenAI Codex
3640
- teammates aider Use Aider
3641
-
3642
- ${chalk.bold("Options:")}
3643
- --model <model> Override the agent model
3644
- --dir <path> Override .teammates/ location
3645
-
3646
- ${chalk.bold("Agents:")}
3647
- claude Claude Code CLI (requires 'claude' on PATH)
3648
- codex OpenAI Codex CLI (requires 'codex' on PATH)
3649
- aider Aider CLI (requires 'aider' on PATH)
3650
- echo Test adapter — echoes prompts (no external agent)
3651
-
3652
- ${chalk.bold("In-session:")}
3653
- @teammate <task> Assign directly via @mention
3654
- <text> Auto-route to the best teammate
3655
- /status Session overview
3656
- /help All commands
3657
- `.trim());
3658
- }
3659
3506
  // ─── Main ────────────────────────────────────────────────────────────
3660
3507
  async function main() {
3661
- if (showHelp) {
3508
+ if (cliArgs.showHelp) {
3662
3509
  printUsage();
3663
3510
  process.exit(0);
3664
3511
  }
3665
- const repl = new TeammatesREPL(adapterName);
3512
+ const repl = new TeammatesREPL(cliArgs.adapterName);
3666
3513
  await repl.start();
3667
3514
  }
3668
3515
  main().catch((err) => {