subagent-cli 0.1.0

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 ADDED
@@ -0,0 +1,1647 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/env.ts
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ function loadEnv() {
7
+ const toLoad = [];
8
+ let dir = process.cwd();
9
+ while (true) {
10
+ const envFile = path.join(dir, ".env");
11
+ if (fs.existsSync(envFile)) {
12
+ toLoad.push(envFile);
13
+ break;
14
+ }
15
+ const parent = path.dirname(dir);
16
+ if (parent === dir) break;
17
+ dir = parent;
18
+ }
19
+ const home = process.env.HOME || process.env.USERPROFILE;
20
+ if (home) {
21
+ const globalEnv = path.join(home, ".sa", ".env");
22
+ if (fs.existsSync(globalEnv)) {
23
+ toLoad.push(globalEnv);
24
+ }
25
+ }
26
+ for (const envFile of toLoad) {
27
+ parseEnvFile(envFile);
28
+ }
29
+ }
30
+ function parseEnvFile(filePath) {
31
+ const content = fs.readFileSync(filePath, "utf-8");
32
+ for (const line of content.split("\n")) {
33
+ const trimmed = line.trim();
34
+ if (!trimmed || trimmed.startsWith("#")) continue;
35
+ const eqIdx = trimmed.indexOf("=");
36
+ if (eqIdx === -1) continue;
37
+ const key = trimmed.slice(0, eqIdx).trim();
38
+ let value = trimmed.slice(eqIdx + 1).trim();
39
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
40
+ value = value.slice(1, -1);
41
+ }
42
+ if (!(key in process.env)) {
43
+ process.env[key] = value;
44
+ }
45
+ }
46
+ }
47
+
48
+ // src/config.ts
49
+ import * as fs2 from "fs";
50
+ import * as path2 from "path";
51
+ import * as os from "os";
52
+ var SA_DIR = ".sa";
53
+ var CONFIG_NAME = "config.json";
54
+ var GLOBAL_DIR = path2.join(os.homedir(), SA_DIR);
55
+ var DEFAULT_MODEL = "anthropic:claude-sonnet-4-6";
56
+ var MEMORY_SYSTEM_PROMPT = `
57
+ ## Memory
58
+
59
+ You have persistent memory via write_memory and read_memory tools.
60
+
61
+ When you learn something useful during a conversation, save it with write_memory so future sessions benefit:
62
+ - User preferences (coding style, tools, workflow)
63
+ - Project conventions (naming, structure, patterns)
64
+ - Solutions to problems (so you don't re-solve them)
65
+ - Corrections the user made (so you don't repeat mistakes)
66
+
67
+ Use scope "project" for project-specific knowledge, "global" for cross-project preferences.
68
+ Memory auto-loads into your system prompt on startup.
69
+ `;
70
+ function getProjectDir() {
71
+ return path2.join(process.cwd(), SA_DIR);
72
+ }
73
+ function getGlobalDir() {
74
+ return GLOBAL_DIR;
75
+ }
76
+ function ensureDir(dir) {
77
+ if (!fs2.existsSync(dir)) {
78
+ fs2.mkdirSync(dir, { recursive: true });
79
+ }
80
+ }
81
+ function loadJsonFile(filePath) {
82
+ try {
83
+ if (fs2.existsSync(filePath)) {
84
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
85
+ }
86
+ } catch {
87
+ }
88
+ return {};
89
+ }
90
+ function loadConfig() {
91
+ const global = loadJsonFile(path2.join(GLOBAL_DIR, CONFIG_NAME));
92
+ const project = loadJsonFile(path2.join(getProjectDir(), CONFIG_NAME));
93
+ return { ...global, ...project };
94
+ }
95
+ function parseFlags(argv) {
96
+ const flags = {};
97
+ const positional = [];
98
+ for (let i = 0; i < argv.length; i++) {
99
+ const arg = argv[i];
100
+ if (arg === "--help" || arg === "-h") {
101
+ flags.help = true;
102
+ } else if (arg === "--version" || arg === "-v") {
103
+ flags.version = true;
104
+ } else if (arg === "--print" || arg === "-p") {
105
+ flags.print = true;
106
+ } else if (arg === "--debug") {
107
+ flags.debug = true;
108
+ } else if (arg === "--resume" || arg === "-r") {
109
+ const next = argv[i + 1];
110
+ if (next && !next.startsWith("-")) {
111
+ flags.resume = next;
112
+ i++;
113
+ } else {
114
+ flags.resume = true;
115
+ }
116
+ } else if (arg === "--model" || arg === "-m") {
117
+ flags.model = argv[++i];
118
+ } else if (arg === "--thinking") {
119
+ flags.thinkingLevel = argv[++i];
120
+ } else if (arg === "--sms") {
121
+ flags.sms = true;
122
+ } else if (arg === "sessions") {
123
+ flags.sessions = true;
124
+ } else if (!arg.startsWith("-")) {
125
+ positional.push(arg);
126
+ }
127
+ }
128
+ if (positional.length > 0) {
129
+ flags.prompt = positional.join(" ");
130
+ if (flags.print === void 0) {
131
+ flags.print = true;
132
+ }
133
+ }
134
+ return flags;
135
+ }
136
+ function resolveModelName(config, flags) {
137
+ return flags.model ?? config.model ?? DEFAULT_MODEL;
138
+ }
139
+ function configToAgentOptions(config, flags) {
140
+ const memoryPaths = [];
141
+ const projectMemory = path2.join(getProjectDir(), "memory.md");
142
+ if (fs2.existsSync(projectMemory)) {
143
+ memoryPaths.push(projectMemory);
144
+ }
145
+ const globalMemory = path2.join(GLOBAL_DIR, "memory.md");
146
+ if (fs2.existsSync(globalMemory)) {
147
+ memoryPaths.push(globalMemory);
148
+ }
149
+ let systemPrompt = MEMORY_SYSTEM_PROMPT;
150
+ if (config.systemPrompt) {
151
+ systemPrompt = config.systemPrompt + "\n" + systemPrompt;
152
+ }
153
+ return {
154
+ model: flags.model ?? config.model,
155
+ systemPrompt,
156
+ warpGrep: config.warpGrep ?? true,
157
+ fastApply: config.fastApply ?? true,
158
+ compact: config.compact ?? false,
159
+ githubSearch: config.githubSearch ?? true,
160
+ thinkingLevel: flags.thinkingLevel ?? config.thinkingLevel ?? "medium",
161
+ subagents: config.subagents,
162
+ memory: memoryPaths.length > 0 ? memoryPaths : void 0,
163
+ debug: flags.debug ?? config.debug
164
+ };
165
+ }
166
+
167
+ // src/modes/print.ts
168
+ import { createAgent } from "subagent";
169
+
170
+ // src/tools/memory.ts
171
+ import * as fs3 from "fs";
172
+ import * as path3 from "path";
173
+ import { Type } from "@sinclair/typebox";
174
+ var MAX_MEMORY_SIZE = 8 * 1024;
175
+ function textResult(text) {
176
+ return { content: [{ type: "text", text }], details: {} };
177
+ }
178
+ function getMemoryPath(scope) {
179
+ const dir = scope === "global" ? getGlobalDir() : getProjectDir();
180
+ ensureDir(dir);
181
+ return path3.join(dir, "memory.md");
182
+ }
183
+ function readMemoryFile(filePath) {
184
+ try {
185
+ if (fs3.existsSync(filePath)) {
186
+ return fs3.readFileSync(filePath, "utf-8");
187
+ }
188
+ } catch {
189
+ }
190
+ return "";
191
+ }
192
+ var writeMemoryTool = {
193
+ name: "write_memory",
194
+ label: "Write Memory",
195
+ description: `Save a learning, pattern, or preference to persistent memory so future sessions benefit.
196
+
197
+ Use this when you discover:
198
+ - User preferences (coding style, tools, workflow)
199
+ - Project conventions (naming, structure, patterns)
200
+ - Solutions to problems you solved (so you don't re-solve them)
201
+ - Corrections the user made (so you don't repeat mistakes)
202
+
203
+ Scope "project" saves to .sa/memory.md (this project only).
204
+ Scope "global" saves to ~/.sa/memory.md (all projects).`,
205
+ parameters: Type.Object({
206
+ key: Type.String({ description: "Short identifier for this memory entry (e.g., 'test-command', 'prefer-bun')" }),
207
+ content: Type.String({ description: "The memory content to save. Be concise but complete." }),
208
+ scope: Type.Union([Type.Literal("project"), Type.Literal("global")], {
209
+ description: "Where to save: 'project' for project-specific, 'global' for cross-project",
210
+ default: "project"
211
+ })
212
+ }),
213
+ execute: async (_toolCallId, params) => {
214
+ const { key, content, scope } = params;
215
+ const memoryPath = getMemoryPath(scope);
216
+ let existing = readMemoryFile(memoryPath);
217
+ const keyHeader = `## ${key}`;
218
+ const keyRegex = new RegExp(`## ${escapeRegex(key)}\\n[\\s\\S]*?(?=\\n## |$)`, "m");
219
+ const newEntry = `## ${key}
220
+ ${content}
221
+ `;
222
+ if (existing.match(keyRegex)) {
223
+ existing = existing.replace(keyRegex, newEntry.trim());
224
+ } else {
225
+ existing = existing.trim() + (existing.trim() ? "\n\n" : "") + newEntry;
226
+ }
227
+ if (Buffer.byteLength(existing, "utf-8") > MAX_MEMORY_SIZE) {
228
+ return textResult(`Memory file would exceed ${MAX_MEMORY_SIZE / 1024}KB limit. Remove old entries first with write_memory using a shorter content, or use read_memory to see what's stored.`);
229
+ }
230
+ fs3.writeFileSync(memoryPath, existing.trim() + "\n", "utf-8");
231
+ return textResult(`Saved "${key}" to ${scope} memory (${memoryPath})`);
232
+ }
233
+ };
234
+ var readMemoryTool = {
235
+ name: "read_memory",
236
+ label: "Read Memory",
237
+ description: "Read your persistent memory files to recall learnings from previous sessions.",
238
+ parameters: Type.Object({
239
+ scope: Type.Optional(
240
+ Type.Union([Type.Literal("project"), Type.Literal("global"), Type.Literal("all")], {
241
+ description: "Which memory to read. Default: 'all'",
242
+ default: "all"
243
+ })
244
+ )
245
+ }),
246
+ execute: async (_toolCallId, params) => {
247
+ const scope = params.scope ?? "all";
248
+ const sections = [];
249
+ if (scope === "project" || scope === "all") {
250
+ const projectPath = path3.join(getProjectDir(), "memory.md");
251
+ const content = readMemoryFile(projectPath);
252
+ if (content.trim()) {
253
+ sections.push(`# Project Memory (.sa/memory.md)
254
+
255
+ ${content.trim()}`);
256
+ } else {
257
+ sections.push("# Project Memory\n\n(empty)");
258
+ }
259
+ }
260
+ if (scope === "global" || scope === "all") {
261
+ const globalPath = path3.join(getGlobalDir(), "memory.md");
262
+ const content = readMemoryFile(globalPath);
263
+ if (content.trim()) {
264
+ sections.push(`# Global Memory (~/.sa/memory.md)
265
+
266
+ ${content.trim()}`);
267
+ } else {
268
+ sections.push("# Global Memory\n\n(empty)");
269
+ }
270
+ }
271
+ return textResult(sections.join("\n\n---\n\n"));
272
+ }
273
+ };
274
+ function escapeRegex(s) {
275
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
276
+ }
277
+
278
+ // src/components/theme.ts
279
+ var ESC = "\x1B[";
280
+ var color = {
281
+ reset: `${ESC}0m`,
282
+ bold: `${ESC}1m`,
283
+ dim: `${ESC}2m`,
284
+ italic: `${ESC}3m`,
285
+ underline: `${ESC}4m`,
286
+ // Foreground
287
+ black: `${ESC}30m`,
288
+ red: `${ESC}31m`,
289
+ green: `${ESC}32m`,
290
+ yellow: `${ESC}33m`,
291
+ blue: `${ESC}34m`,
292
+ magenta: `${ESC}35m`,
293
+ cyan: `${ESC}36m`,
294
+ white: `${ESC}37m`,
295
+ // Bright foreground
296
+ gray: `${ESC}90m`,
297
+ brightRed: `${ESC}91m`,
298
+ brightGreen: `${ESC}92m`,
299
+ brightYellow: `${ESC}93m`,
300
+ brightBlue: `${ESC}94m`,
301
+ brightMagenta: `${ESC}95m`,
302
+ brightCyan: `${ESC}96m`,
303
+ brightWhite: `${ESC}97m`,
304
+ // 256-color (for subtle tones)
305
+ fg256: (n) => `${ESC}38;5;${n}m`,
306
+ bg256: (n) => `${ESC}48;5;${n}m`
307
+ };
308
+ var theme = {
309
+ agent: color.brightCyan,
310
+ user: color.brightWhite,
311
+ tool: color.yellow,
312
+ toolResult: color.dim,
313
+ error: color.red,
314
+ success: color.green,
315
+ thinking: color.gray,
316
+ dim: color.dim,
317
+ accent: color.blue,
318
+ label: color.gray,
319
+ reset: color.reset,
320
+ bold: color.bold
321
+ };
322
+ function styled(style, text) {
323
+ return `${style}${text}${color.reset}`;
324
+ }
325
+ var box = {
326
+ topLeft: "\u256D",
327
+ topRight: "\u256E",
328
+ bottomLeft: "\u2570",
329
+ bottomRight: "\u256F",
330
+ horizontal: "\u2500",
331
+ vertical: "\u2502",
332
+ teeRight: "\u251C",
333
+ teeLeft: "\u2524",
334
+ dot: "\u25CF",
335
+ arrow: "\u2192",
336
+ spinner: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
337
+ };
338
+
339
+ // src/components/team-panel.ts
340
+ var TEAMMATE_COLORS = [
341
+ color.brightCyan,
342
+ color.brightMagenta,
343
+ color.brightYellow,
344
+ color.brightGreen,
345
+ color.brightBlue,
346
+ color.brightRed,
347
+ color.cyan,
348
+ color.magenta,
349
+ color.yellow,
350
+ color.green
351
+ ];
352
+ var colorAssignments = /* @__PURE__ */ new Map();
353
+ var nextColorIndex = 0;
354
+ function getTeammateColor(name) {
355
+ let assigned = colorAssignments.get(name);
356
+ if (!assigned) {
357
+ assigned = TEAMMATE_COLORS[nextColorIndex % TEAMMATE_COLORS.length];
358
+ colorAssignments.set(name, assigned);
359
+ nextColorIndex++;
360
+ }
361
+ return assigned;
362
+ }
363
+ var activeTasks = /* @__PURE__ */ new Map();
364
+ var agentStats = /* @__PURE__ */ new Map();
365
+ function trackTaskStart(toolCallId, agentName, prompt) {
366
+ activeTasks.set(toolCallId, { toolCallId, agentName, prompt, startTime: Date.now() });
367
+ }
368
+ function trackTaskEnd(toolCallId) {
369
+ const task = activeTasks.get(toolCallId);
370
+ if (!task) return null;
371
+ activeTasks.delete(toolCallId);
372
+ const elapsed = Date.now() - task.startTime;
373
+ const existing = agentStats.get(task.agentName);
374
+ if (existing) {
375
+ existing.taskCount++;
376
+ existing.totalTimeMs += elapsed;
377
+ } else {
378
+ agentStats.set(task.agentName, { taskCount: 1, totalTimeMs: elapsed });
379
+ }
380
+ return { agentName: task.agentName, elapsed };
381
+ }
382
+ function hasActiveTasks() {
383
+ return activeTasks.size > 0;
384
+ }
385
+ var activeMessages = /* @__PURE__ */ new Map();
386
+ function trackMessageStart(toolCallId, to, text) {
387
+ activeMessages.set(toolCallId, { toolCallId, to, text, startTime: Date.now() });
388
+ }
389
+ function trackMessageEnd(toolCallId, delivered) {
390
+ const msg = activeMessages.get(toolCallId);
391
+ if (!msg) return null;
392
+ activeMessages.delete(toolCallId);
393
+ return { to: msg.to, text: msg.text, elapsed: Date.now() - msg.startTime, delivered };
394
+ }
395
+ function renderTaskCompletion(agentName, elapsed, isError) {
396
+ const c = getTeammateColor(agentName);
397
+ const timeStr = formatTime(elapsed);
398
+ const status = isError ? styled(theme.error, "\u2717") : styled(theme.success, "\u2713");
399
+ return ` ${status} ${styled(c, agentName)} ${styled(theme.dim, isError ? "failed" : "done")} ${styled(theme.dim, timeStr)}`;
400
+ }
401
+ function renderMessageFlow(to, text, delivered, elapsed) {
402
+ const toColor = getTeammateColor(to);
403
+ const summary = text.length > 50 ? text.slice(0, 47) + "..." : text;
404
+ const arrow = `${styled(theme.dim, "\u2500\u2500")}${styled(toColor, "\u2192")}`;
405
+ const status = delivered ? styled(theme.success, "\u2713") : styled(theme.error, "\u2717");
406
+ const timeStr = formatTime(elapsed);
407
+ return ` \u2709 ${styled(theme.dim, "agent")} ${arrow} ${styled(toColor, to)} ${styled(theme.dim, `"${summary}"`)} ${status} ${styled(theme.dim, timeStr)}`;
408
+ }
409
+ function renderInboxBadge(to) {
410
+ const toColor = getTeammateColor(to);
411
+ return ` \u{1F4EC} ${styled(toColor, to)} ${styled(theme.dim, "new message received")}`;
412
+ }
413
+ function renderTaskGraph(spinnerFrame2) {
414
+ if (activeTasks.size === 0) return [];
415
+ const tasks = Array.from(activeTasks.values());
416
+ const lines = [];
417
+ for (let i = 0; i < tasks.length; i++) {
418
+ const t = tasks[i];
419
+ const c = getTeammateColor(t.agentName);
420
+ const elapsed = Date.now() - t.startTime;
421
+ const timeStr = formatTime(elapsed);
422
+ const spinnerChar = styled(c, box.spinner[spinnerFrame2 % box.spinner.length]);
423
+ const prompt = t.prompt.length > 40 ? t.prompt.slice(0, 37) + "..." : t.prompt;
424
+ const connector = i === tasks.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
425
+ const name = t.agentName.padEnd(14);
426
+ lines.push(
427
+ ` ${styled(theme.dim, connector)} ${spinnerChar} ${styled(c, name)} ${styled(theme.dim, `"${prompt}"`)} ${styled(theme.dim, timeStr)}`
428
+ );
429
+ }
430
+ return lines;
431
+ }
432
+ function renderAgentSummary() {
433
+ if (agentStats.size === 0) return "";
434
+ const entries = Array.from(agentStats.entries()).sort((a, b) => b[1].totalTimeMs - a[1].totalTimeMs);
435
+ const lines = [];
436
+ lines.push(` ${styled(theme.dim, `\u250C agents ${"\u2500".repeat(37)}\u2510`)}`);
437
+ for (const [name, stats] of entries) {
438
+ const c = getTeammateColor(name);
439
+ const taskLabel = stats.taskCount === 1 ? "1 task" : `${stats.taskCount} tasks`;
440
+ const timeStr = formatTime(stats.totalTimeMs);
441
+ lines.push(
442
+ ` ${styled(theme.dim, "\u2502")} ${styled(c, box.dot)} ${styled(c, name.padEnd(14))} ${styled(theme.dim, taskLabel.padEnd(10))} ${styled(theme.dim, timeStr.padStart(7))} ${styled(theme.dim, "\u2502")}`
443
+ );
444
+ }
445
+ lines.push(` ${styled(theme.dim, `\u2514${"\u2500".repeat(44)}\u2518`)}`);
446
+ return lines.join("\n");
447
+ }
448
+ function renderTeamStatus(team) {
449
+ if (!team) return "";
450
+ const lines = [];
451
+ lines.push(styled(theme.label, ` team: ${team.name}`));
452
+ for (const [name, handle] of team.teammates) {
453
+ const c = getTeammateColor(name);
454
+ const status = handle.abortController.signal.aborted ? styled(theme.dim, "done") : styled(color.green, "active");
455
+ lines.push(` ${styled(c, box.dot)} ${styled(c, name)} ${status}`);
456
+ }
457
+ return lines.join("\n");
458
+ }
459
+ function formatTime(ms) {
460
+ if (ms >= 6e4) return `${(ms / 6e4).toFixed(1)}m`;
461
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(1)}s`;
462
+ return `${ms}ms`;
463
+ }
464
+
465
+ // src/components/tool-panel.ts
466
+ var TEAM_TOOLS = /* @__PURE__ */ new Set(["task", "send_message"]);
467
+ var activeTools = /* @__PURE__ */ new Map();
468
+ var spinnerFrame = 0;
469
+ var renderedLineCount = 0;
470
+ function clearLiveRender() {
471
+ if (renderedLineCount <= 0) return;
472
+ let seq = "";
473
+ for (let i = 0; i < renderedLineCount; i++) {
474
+ seq += "\x1B[2K";
475
+ if (i < renderedLineCount - 1) {
476
+ seq += "\x1B[A";
477
+ }
478
+ }
479
+ seq += "\r";
480
+ process.stderr.write(seq);
481
+ renderedLineCount = 0;
482
+ }
483
+ function writeLiveLines(lines) {
484
+ clearLiveRender();
485
+ if (lines.length === 0) return;
486
+ process.stderr.write(lines.join("\n") + "\r");
487
+ renderedLineCount = lines.length;
488
+ }
489
+ function toolStart(toolCallId, name, args) {
490
+ activeTools.set(toolCallId, { toolCallId, name, args, startTime: Date.now() });
491
+ if (name === "task" && args?.agent) {
492
+ trackTaskStart(toolCallId, args.agent, args.prompt ?? "");
493
+ } else if (name === "send_message" && args?.to) {
494
+ trackMessageStart(toolCallId, args.to, args.text ?? "");
495
+ }
496
+ }
497
+ function toolEnd(toolCallId, _result, isError) {
498
+ const tool = activeTools.get(toolCallId);
499
+ activeTools.delete(toolCallId);
500
+ if (!tool) return;
501
+ clearLiveRender();
502
+ if (tool.name === "task") {
503
+ const tracked = trackTaskEnd(toolCallId);
504
+ if (tracked) {
505
+ process.stderr.write(renderTaskCompletion(tracked.agentName, tracked.elapsed, isError) + "\n");
506
+ }
507
+ return;
508
+ }
509
+ if (tool.name === "send_message") {
510
+ const tracked = trackMessageEnd(toolCallId, !isError);
511
+ if (tracked) {
512
+ process.stderr.write(renderMessageFlow(tracked.to, tracked.text, tracked.delivered, tracked.elapsed) + "\n");
513
+ if (tracked.delivered) {
514
+ process.stderr.write(renderInboxBadge(tracked.to) + "\n");
515
+ }
516
+ }
517
+ return;
518
+ }
519
+ const elapsed = Date.now() - tool.startTime;
520
+ const timeStr = formatTime2(elapsed);
521
+ const status = isError ? styled(theme.error, "\u2717") : styled(theme.success, "\u2713");
522
+ const label = formatToolLabel(tool.name, tool.args);
523
+ const nameColor = getToolNameColor(tool.name, tool.args);
524
+ process.stderr.write(` ${status} ${styled(nameColor, tool.name)} ${styled(theme.dim, label)} ${styled(theme.dim, timeStr)}
525
+ `);
526
+ }
527
+ function updateLivePanel() {
528
+ spinnerFrame = (spinnerFrame + 1) % box.spinner.length;
529
+ const lines = [];
530
+ if (hasActiveTasks()) {
531
+ lines.push(...renderTaskGraph(spinnerFrame));
532
+ }
533
+ const nonTeamTools = Array.from(activeTools.values()).filter((t) => !TEAM_TOOLS.has(t.name));
534
+ for (const t of nonTeamTools) {
535
+ const elapsed = Date.now() - t.startTime;
536
+ const timeStr = formatTime2(elapsed);
537
+ const label = formatToolLabel(t.name, t.args);
538
+ const spinnerColor = getToolNameColor(t.name, t.args);
539
+ const spinner = styled(spinnerColor, box.spinner[spinnerFrame]);
540
+ lines.push(` ${spinner} ${styled(spinnerColor, t.name)} ${styled(theme.dim, label)} ${styled(theme.dim, timeStr)}`);
541
+ }
542
+ writeLiveLines(lines);
543
+ }
544
+ function hasActiveWork() {
545
+ return activeTools.size > 0;
546
+ }
547
+ function getToolNameColor(name, args) {
548
+ if (name === "task" && args?.agent) return getTeammateColor(args.agent);
549
+ if (name === "send_message" && args?.to) return getTeammateColor(args.to);
550
+ return theme.tool;
551
+ }
552
+ function formatToolLabel(name, args) {
553
+ if (!args) return "";
554
+ switch (name) {
555
+ case "read_file":
556
+ return truncate(args.path ?? args.file_path ?? "", 50);
557
+ case "write_file":
558
+ return truncate(args.path ?? args.file_path ?? "", 50);
559
+ case "edit_file":
560
+ return truncate(args.path ?? args.file_path ?? "", 50);
561
+ case "execute":
562
+ return truncate(args.command ?? "", 60);
563
+ case "glob":
564
+ return truncate(args.pattern ?? "", 50);
565
+ case "grep":
566
+ return truncate(`"${args.pattern ?? ""}" ${args.path ?? ""}`, 60);
567
+ case "ls":
568
+ return truncate(args.path ?? "", 50);
569
+ case "warp_grep":
570
+ return truncate(`"${args.query ?? ""}"`, 50);
571
+ case "fast_apply":
572
+ return truncate(args.file_path ?? args.path ?? "", 50);
573
+ case "write_memory":
574
+ return truncate(`${args.scope ?? "project"}:${args.key ?? ""}`, 40);
575
+ default:
576
+ for (const v of Object.values(args)) {
577
+ if (typeof v === "string" && v.length > 0) {
578
+ return truncate(v, 50);
579
+ }
580
+ }
581
+ return "";
582
+ }
583
+ }
584
+ function truncate(s, max) {
585
+ if (s.length <= max) return s;
586
+ return s.slice(0, max - 1) + "\u2026";
587
+ }
588
+ function formatTime2(ms) {
589
+ if (ms >= 6e4) return `${(ms / 6e4).toFixed(1)}m`;
590
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(1)}s`;
591
+ return `${ms}ms`;
592
+ }
593
+
594
+ // src/components/chat.ts
595
+ function writeTextDelta(delta) {
596
+ process.stdout.write(delta);
597
+ }
598
+ function writeThinkingDelta(delta) {
599
+ process.stderr.write(styled(theme.thinking, delta));
600
+ }
601
+ function endThinking() {
602
+ process.stderr.write(color.reset + "\n");
603
+ }
604
+ function endAssistantMessage() {
605
+ process.stdout.write("\n");
606
+ }
607
+ function writeError(message) {
608
+ process.stderr.write(`${styled(theme.error, "error")}: ${message}
609
+ `);
610
+ }
611
+ function writeInfo(message) {
612
+ process.stderr.write(`${styled(theme.dim, message)}
613
+ `);
614
+ }
615
+ function writeBanner(model, sessionId) {
616
+ process.stderr.write(
617
+ `${styled(theme.accent, "sa")} ${styled(theme.dim, model)} ${styled(theme.dim, `session:${sessionId}`)}
618
+ ${styled(theme.dim, "Type a message to start. Ctrl+C to exit. /help for commands.")}
619
+
620
+ `
621
+ );
622
+ }
623
+
624
+ // src/session.ts
625
+ import * as fs4 from "fs";
626
+ import * as path4 from "path";
627
+ function getSessionsDir() {
628
+ const dir = path4.join(getProjectDir(), "sessions");
629
+ ensureDir(dir);
630
+ return dir;
631
+ }
632
+ function generateSessionId() {
633
+ const now = /* @__PURE__ */ new Date();
634
+ const date = now.toISOString().slice(0, 10);
635
+ const time = now.toISOString().slice(11, 19).replace(/:/g, "");
636
+ const rand = Math.random().toString(36).slice(2, 6);
637
+ return `${date}_${time}_${rand}`;
638
+ }
639
+ function appendEvent(sessionId, event) {
640
+ const sessionsDir = getSessionsDir();
641
+ const filePath = path4.join(sessionsDir, `${sessionId}.jsonl`);
642
+ fs4.appendFileSync(filePath, JSON.stringify(event) + "\n", "utf-8");
643
+ }
644
+ function loadSession(sessionId) {
645
+ const filePath = path4.join(getSessionsDir(), `${sessionId}.jsonl`);
646
+ if (!fs4.existsSync(filePath)) return [];
647
+ return fs4.readFileSync(filePath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
648
+ }
649
+ function listSessions() {
650
+ const dir = getSessionsDir();
651
+ if (!fs4.existsSync(dir)) return [];
652
+ return fs4.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
653
+ const filePath = path4.join(dir, f);
654
+ const stat = fs4.statSync(filePath);
655
+ const content = fs4.readFileSync(filePath, "utf-8").trim();
656
+ const lines = content.split("\n").filter(Boolean);
657
+ const firstUser = lines.find((l) => {
658
+ try {
659
+ return JSON.parse(l).type === "user";
660
+ } catch {
661
+ return false;
662
+ }
663
+ });
664
+ let preview = "(empty)";
665
+ if (firstUser) {
666
+ try {
667
+ const parsed = JSON.parse(firstUser);
668
+ preview = (parsed.content ?? "").slice(0, 80);
669
+ if ((parsed.content ?? "").length > 80) preview += "...";
670
+ } catch {
671
+ }
672
+ }
673
+ return {
674
+ id: f.replace(".jsonl", ""),
675
+ created: stat.birthtimeMs,
676
+ lastActive: stat.mtimeMs,
677
+ lines: lines.length,
678
+ preview
679
+ };
680
+ }).sort((a, b) => b.lastActive - a.lastActive);
681
+ }
682
+ function getLatestSessionId() {
683
+ const sessions = listSessions();
684
+ return sessions.length > 0 ? sessions[0].id : null;
685
+ }
686
+ function formatSessionList(sessions) {
687
+ if (sessions.length === 0) return "No sessions found.";
688
+ const lines = sessions.map((s) => {
689
+ const age = formatAge(Date.now() - s.lastActive);
690
+ return ` ${s.id} ${age} ago (${s.lines} events) ${s.preview}`;
691
+ });
692
+ return `Sessions:
693
+ ${lines.join("\n")}`;
694
+ }
695
+ function formatAge(ms) {
696
+ const mins = Math.floor(ms / 6e4);
697
+ if (mins < 60) return `${mins}m`;
698
+ const hours = Math.floor(mins / 60);
699
+ if (hours < 24) return `${hours}h`;
700
+ const days = Math.floor(hours / 24);
701
+ return `${days}d`;
702
+ }
703
+
704
+ // src/components/status-bar.ts
705
+ function renderStatusBar(state) {
706
+ const cols = process.stdout.columns || 80;
707
+ const model = styled(theme.accent, state.model);
708
+ const tokens = state.totalTokens > 0 ? styled(theme.dim, ` ${formatTokens(state.totalTokens)} tokens`) : "";
709
+ const cost = state.totalCost > 0 ? styled(theme.dim, ` $${state.totalCost.toFixed(4)}`) : "";
710
+ const streaming = state.isStreaming ? styled(color.yellow, " \u25CF") : styled(color.green, " \u25CF");
711
+ return `${streaming} ${model}${tokens}${cost}`;
712
+ }
713
+ function formatTokens(n) {
714
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
715
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
716
+ return `${n}`;
717
+ }
718
+
719
+ // src/modes/print.ts
720
+ async function runPrintMode(prompt, agentOptions, modelName) {
721
+ const sessionId = generateSessionId();
722
+ const tools = [...agentOptions.tools ?? [], writeMemoryTool, readMemoryTool];
723
+ let agent;
724
+ try {
725
+ agent = await createAgent({ ...agentOptions, tools });
726
+ } catch (err) {
727
+ writeError(err.message ?? String(err));
728
+ process.exit(1);
729
+ }
730
+ let totalTokens = 0;
731
+ let totalCost = 0;
732
+ let isInText = false;
733
+ let isInThinking = false;
734
+ let assistantText = "";
735
+ appendEvent(sessionId, { type: "user", content: prompt, timestamp: Date.now() });
736
+ agent.subscribe((event) => {
737
+ switch (event.type) {
738
+ case "message_update": {
739
+ const e = event.assistantMessageEvent;
740
+ if (e.type === "text_delta") {
741
+ if (isInThinking) {
742
+ isInThinking = false;
743
+ endThinking();
744
+ }
745
+ if (!isInText) {
746
+ clearLiveRender();
747
+ }
748
+ isInText = true;
749
+ writeTextDelta(e.delta);
750
+ assistantText += e.delta;
751
+ } else if (e.type === "thinking_delta") {
752
+ isInThinking = true;
753
+ writeThinkingDelta(e.delta);
754
+ } else if (e.type === "thinking_end") {
755
+ if (isInThinking) {
756
+ isInThinking = false;
757
+ endThinking();
758
+ }
759
+ } else if (e.type === "error") {
760
+ const errMsg = e.error?.errorMessage ?? "Unknown streaming error";
761
+ writeError(errMsg);
762
+ }
763
+ break;
764
+ }
765
+ case "message_end": {
766
+ const msg = event.message;
767
+ if (msg?.role === "assistant" && msg?.stopReason === "error") {
768
+ const errMsg = msg.errorMessage ?? "API error (no details)";
769
+ writeError(errMsg);
770
+ }
771
+ break;
772
+ }
773
+ case "turn_end": {
774
+ if (isInText) {
775
+ endAssistantMessage();
776
+ isInText = false;
777
+ }
778
+ const msg = event.message;
779
+ if (msg?.usage) {
780
+ totalTokens += msg.usage.totalTokens ?? 0;
781
+ totalCost += msg.usage.cost?.total ?? 0;
782
+ }
783
+ if (assistantText) {
784
+ appendEvent(sessionId, { type: "assistant", content: assistantText, timestamp: Date.now() });
785
+ assistantText = "";
786
+ }
787
+ break;
788
+ }
789
+ case "tool_execution_start": {
790
+ toolStart(event.toolCallId, event.toolName, event.args);
791
+ appendEvent(sessionId, {
792
+ type: "tool_call",
793
+ name: event.toolName,
794
+ args: event.args,
795
+ timestamp: Date.now()
796
+ });
797
+ break;
798
+ }
799
+ case "tool_execution_end": {
800
+ toolEnd(event.toolCallId, event.result, event.isError);
801
+ const resultText = extractResultText(event.result);
802
+ appendEvent(sessionId, {
803
+ type: "tool_result",
804
+ name: event.toolName,
805
+ result: resultText.slice(0, 500),
806
+ isError: event.isError,
807
+ timestamp: Date.now()
808
+ });
809
+ break;
810
+ }
811
+ case "agent_end": {
812
+ const bar = renderStatusBar({
813
+ model: modelName,
814
+ totalTokens,
815
+ totalCost,
816
+ sessionId,
817
+ isStreaming: false
818
+ });
819
+ process.stderr.write(`
820
+ ${bar}
821
+ `);
822
+ const agentSummary = renderAgentSummary();
823
+ if (agentSummary) {
824
+ process.stderr.write(`${agentSummary}
825
+ `);
826
+ }
827
+ break;
828
+ }
829
+ }
830
+ });
831
+ try {
832
+ await agent.prompt(prompt);
833
+ await agent.waitForIdle();
834
+ } catch (err) {
835
+ if (err.name === "AbortError") {
836
+ process.stderr.write(`
837
+ ${styled(theme.dim, "(aborted)")}
838
+ `);
839
+ } else {
840
+ writeError(err.message ?? String(err));
841
+ }
842
+ } finally {
843
+ agent.destroy();
844
+ }
845
+ }
846
+ function extractResultText(result) {
847
+ if (!result) return "";
848
+ if (typeof result === "string") return result;
849
+ if (result.content && Array.isArray(result.content)) {
850
+ return result.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
851
+ }
852
+ return JSON.stringify(result).slice(0, 500);
853
+ }
854
+
855
+ // src/modes/interactive.ts
856
+ import { createAgent as createAgent2 } from "subagent";
857
+
858
+ // src/components/input.ts
859
+ import * as readline from "readline";
860
+ var HISTORY_MAX = 200;
861
+ var history = [];
862
+ var rl = null;
863
+ function createInput(opts) {
864
+ rl = readline.createInterface({
865
+ input: process.stdin,
866
+ output: process.stderr,
867
+ prompt: styled(color.brightWhite + color.bold, "> "),
868
+ historySize: HISTORY_MAX,
869
+ terminal: true
870
+ });
871
+ rl.on("line", (line) => {
872
+ const trimmed = line.trim();
873
+ if (!trimmed) {
874
+ rl?.prompt();
875
+ return;
876
+ }
877
+ history.push(trimmed);
878
+ opts.onSubmit(trimmed);
879
+ });
880
+ rl.on("close", () => {
881
+ opts.onAbort();
882
+ });
883
+ rl.on("SIGINT", () => {
884
+ opts.onAbort();
885
+ });
886
+ return {
887
+ prompt: () => {
888
+ rl?.prompt();
889
+ },
890
+ close: () => {
891
+ rl?.close();
892
+ rl = null;
893
+ }
894
+ };
895
+ }
896
+ function pauseInput() {
897
+ if (rl) {
898
+ process.stderr.write("\r\x1B[K");
899
+ }
900
+ }
901
+
902
+ // src/modes/interactive.ts
903
+ async function runInteractiveMode(agentOptions, modelName, resumeSessionId) {
904
+ const sessionId = resumeSessionId ?? generateSessionId();
905
+ const model = modelName;
906
+ const tools = [...agentOptions.tools ?? [], writeMemoryTool, readMemoryTool];
907
+ let agent;
908
+ try {
909
+ agent = await createAgent2({ ...agentOptions, tools });
910
+ } catch (err) {
911
+ writeError(err.message ?? String(err));
912
+ process.exit(1);
913
+ }
914
+ let totalTokens = 0;
915
+ let totalCost = 0;
916
+ let isInText = false;
917
+ let isInThinking = false;
918
+ let isWorking = false;
919
+ let assistantText = "";
920
+ let spinnerInterval = null;
921
+ if (resumeSessionId) {
922
+ const events = loadSession(resumeSessionId);
923
+ if (events.length > 0) {
924
+ writeInfo(`Resuming session ${resumeSessionId} (${events.length} events)`);
925
+ for (const event of events) {
926
+ if (event.type === "user" && event.content) {
927
+ agent.followUp({
928
+ role: "user",
929
+ content: [{ type: "text", text: event.content }],
930
+ timestamp: event.timestamp
931
+ });
932
+ }
933
+ }
934
+ }
935
+ }
936
+ writeBanner(model, sessionId);
937
+ agent.subscribe((event) => {
938
+ switch (event.type) {
939
+ case "message_update": {
940
+ const e = event.assistantMessageEvent;
941
+ if (e.type === "text_delta") {
942
+ if (isInThinking) {
943
+ isInThinking = false;
944
+ endThinking();
945
+ }
946
+ if (!isInText) {
947
+ clearLiveRender();
948
+ }
949
+ isInText = true;
950
+ writeTextDelta(e.delta);
951
+ assistantText += e.delta;
952
+ } else if (e.type === "thinking_delta") {
953
+ if (!isInThinking) {
954
+ clearLiveRender();
955
+ process.stderr.write(styled(theme.thinking, "thinking... "));
956
+ isInThinking = true;
957
+ }
958
+ writeThinkingDelta(e.delta);
959
+ } else if (e.type === "thinking_end") {
960
+ if (isInThinking) {
961
+ isInThinking = false;
962
+ endThinking();
963
+ }
964
+ }
965
+ break;
966
+ }
967
+ case "turn_end": {
968
+ if (isInText) {
969
+ endAssistantMessage();
970
+ isInText = false;
971
+ }
972
+ const msg = event.message;
973
+ if (msg?.usage) {
974
+ totalTokens += msg.usage.totalTokens ?? 0;
975
+ totalCost += msg.usage.cost?.total ?? 0;
976
+ }
977
+ if (assistantText) {
978
+ appendEvent(sessionId, { type: "assistant", content: assistantText, timestamp: Date.now() });
979
+ assistantText = "";
980
+ }
981
+ break;
982
+ }
983
+ case "tool_execution_start": {
984
+ toolStart(event.toolCallId, event.toolName, event.args);
985
+ appendEvent(sessionId, {
986
+ type: "tool_call",
987
+ name: event.toolName,
988
+ args: event.args,
989
+ timestamp: Date.now()
990
+ });
991
+ break;
992
+ }
993
+ case "tool_execution_end": {
994
+ toolEnd(event.toolCallId, event.result, event.isError);
995
+ const resultText = extractResultText2(event.result);
996
+ appendEvent(sessionId, {
997
+ type: "tool_result",
998
+ name: event.toolName,
999
+ result: resultText.slice(0, 500),
1000
+ isError: event.isError,
1001
+ timestamp: Date.now()
1002
+ });
1003
+ break;
1004
+ }
1005
+ case "agent_end": {
1006
+ isWorking = false;
1007
+ stopSpinner();
1008
+ const bar = renderStatusBar({
1009
+ model,
1010
+ totalTokens,
1011
+ totalCost,
1012
+ sessionId,
1013
+ isStreaming: false
1014
+ });
1015
+ process.stderr.write(`${bar}
1016
+ `);
1017
+ const agentSummary = renderAgentSummary();
1018
+ if (agentSummary) {
1019
+ process.stderr.write(`${agentSummary}
1020
+ `);
1021
+ }
1022
+ const teamStatus = renderTeamStatus(agent.team);
1023
+ if (teamStatus) {
1024
+ process.stderr.write(`${teamStatus}
1025
+ `);
1026
+ }
1027
+ process.stderr.write("\n");
1028
+ input.prompt();
1029
+ break;
1030
+ }
1031
+ case "agent_start": {
1032
+ isWorking = true;
1033
+ startSpinner();
1034
+ break;
1035
+ }
1036
+ }
1037
+ });
1038
+ function startSpinner() {
1039
+ if (spinnerInterval) return;
1040
+ spinnerInterval = setInterval(() => {
1041
+ if (hasActiveWork()) {
1042
+ updateLivePanel();
1043
+ }
1044
+ }, 100);
1045
+ }
1046
+ function stopSpinner() {
1047
+ if (spinnerInterval) {
1048
+ clearInterval(spinnerInterval);
1049
+ spinnerInterval = null;
1050
+ clearLiveRender();
1051
+ }
1052
+ }
1053
+ function handleSlashCommand(input2) {
1054
+ const [cmd, ...rest] = input2.slice(1).split(/\s+/);
1055
+ const arg = rest.join(" ");
1056
+ switch (cmd) {
1057
+ case "help":
1058
+ writeInfo(`Commands:
1059
+ /help Show this help
1060
+ /sessions List saved sessions
1061
+ /status Show current status
1062
+ /clear Clear conversation
1063
+ /compact Force context compaction
1064
+ /memory Show loaded memories
1065
+ /quit, /exit Exit sa`);
1066
+ return true;
1067
+ case "sessions":
1068
+ process.stderr.write(formatSessionList(listSessions()) + "\n");
1069
+ return true;
1070
+ case "status": {
1071
+ const bar = renderStatusBar({
1072
+ model,
1073
+ totalTokens,
1074
+ totalCost,
1075
+ sessionId,
1076
+ isStreaming: isWorking
1077
+ });
1078
+ process.stderr.write(`${bar}
1079
+ `);
1080
+ const teamStatus = renderTeamStatus(agent.team);
1081
+ if (teamStatus) process.stderr.write(`${teamStatus}
1082
+ `);
1083
+ return true;
1084
+ }
1085
+ case "clear":
1086
+ agent.reset();
1087
+ totalTokens = 0;
1088
+ totalCost = 0;
1089
+ writeInfo("Conversation cleared.");
1090
+ return true;
1091
+ case "memory":
1092
+ writeInfo("Memory is auto-loaded into system prompt from:");
1093
+ writeInfo(" Project: .sa/memory.md");
1094
+ writeInfo(" Global: ~/.sa/memory.md");
1095
+ writeInfo("The agent can write to memory with write_memory tool.");
1096
+ return true;
1097
+ case "quit":
1098
+ case "exit":
1099
+ cleanup();
1100
+ return true;
1101
+ default:
1102
+ writeError(`Unknown command: /${cmd}. Type /help for available commands.`);
1103
+ return true;
1104
+ }
1105
+ }
1106
+ const input = createInput({
1107
+ onSubmit: async (text) => {
1108
+ if (text.startsWith("/")) {
1109
+ const handled = handleSlashCommand(text);
1110
+ if (handled) {
1111
+ input.prompt();
1112
+ return;
1113
+ }
1114
+ }
1115
+ if (isWorking) {
1116
+ writeInfo("(steering agent...)");
1117
+ agent.steer({
1118
+ role: "user",
1119
+ content: [{ type: "text", text }],
1120
+ timestamp: Date.now()
1121
+ });
1122
+ return;
1123
+ }
1124
+ pauseInput();
1125
+ appendEvent(sessionId, { type: "user", content: text, timestamp: Date.now() });
1126
+ try {
1127
+ await agent.prompt(text);
1128
+ await agent.waitForIdle();
1129
+ } catch (err) {
1130
+ if (err.name === "AbortError") {
1131
+ process.stderr.write(`
1132
+ ${styled(theme.dim, "(aborted)")}
1133
+ `);
1134
+ } else {
1135
+ writeError(err.message ?? String(err));
1136
+ }
1137
+ input.prompt();
1138
+ }
1139
+ },
1140
+ onAbort: () => {
1141
+ if (isWorking) {
1142
+ agent.abort();
1143
+ isWorking = false;
1144
+ stopSpinner();
1145
+ process.stderr.write(`
1146
+ ${styled(theme.dim, "(aborted)")}
1147
+ `);
1148
+ input.prompt();
1149
+ } else {
1150
+ cleanup();
1151
+ }
1152
+ }
1153
+ });
1154
+ function cleanup() {
1155
+ stopSpinner();
1156
+ agent.destroy();
1157
+ input.close();
1158
+ process.stderr.write(`
1159
+ ${styled(theme.dim, `Session saved: ${sessionId}`)}
1160
+ `);
1161
+ process.exit(0);
1162
+ }
1163
+ process.on("SIGTERM", cleanup);
1164
+ input.prompt();
1165
+ }
1166
+ function extractResultText2(result) {
1167
+ if (!result) return "";
1168
+ if (typeof result === "string") return result;
1169
+ if (result.content && Array.isArray(result.content)) {
1170
+ return result.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
1171
+ }
1172
+ return JSON.stringify(result).slice(0, 500);
1173
+ }
1174
+
1175
+ // src/modes/sms.ts
1176
+ import { createAgent as createAgent3 } from "subagent";
1177
+
1178
+ // src/sms/relay-client.ts
1179
+ import WebSocket from "ws";
1180
+ function connectRelay(opts) {
1181
+ let ws = null;
1182
+ let closed = false;
1183
+ let reconnectAttempt = 0;
1184
+ let reconnectTimer = null;
1185
+ let pingInterval = null;
1186
+ const MAX_RECONNECT_DELAY = 3e4;
1187
+ const RECONNECT_BUDGET_MS = 10 * 60 * 1e3;
1188
+ let firstConnectTime = Date.now();
1189
+ function connect() {
1190
+ if (closed) return;
1191
+ const wsUrl = `${opts.apiBaseUrl}/v1/sms/ws?session=${encodeURIComponent(opts.sessionId)}&code=${encodeURIComponent(opts.pairingCode)}`;
1192
+ ws = new WebSocket(wsUrl);
1193
+ ws.on("open", () => {
1194
+ reconnectAttempt = 0;
1195
+ firstConnectTime = Date.now();
1196
+ pingInterval = setInterval(() => {
1197
+ if (ws?.readyState === WebSocket.OPEN) {
1198
+ ws.send(JSON.stringify({ type: "ping" }));
1199
+ }
1200
+ }, 3e4);
1201
+ });
1202
+ ws.on("message", (data) => {
1203
+ let msg;
1204
+ try {
1205
+ msg = JSON.parse(data.toString());
1206
+ } catch {
1207
+ return;
1208
+ }
1209
+ switch (msg.type) {
1210
+ case "prompt":
1211
+ opts.onPrompt(msg.body, msg.from);
1212
+ break;
1213
+ case "paired":
1214
+ opts.onPaired(msg.phone);
1215
+ break;
1216
+ case "pong":
1217
+ break;
1218
+ case "error":
1219
+ opts.onError(msg.message);
1220
+ break;
1221
+ }
1222
+ });
1223
+ ws.on("close", (code) => {
1224
+ clearPing();
1225
+ if (closed) return;
1226
+ if (code === 1e3) {
1227
+ opts.onDisconnect();
1228
+ return;
1229
+ }
1230
+ attemptReconnect();
1231
+ });
1232
+ ws.on("error", (err) => {
1233
+ if (!closed) {
1234
+ console.error(`[relay] WebSocket error: ${err.message}`);
1235
+ }
1236
+ });
1237
+ }
1238
+ function attemptReconnect() {
1239
+ if (closed) return;
1240
+ if (Date.now() - firstConnectTime > RECONNECT_BUDGET_MS) {
1241
+ opts.onError("Reconnect budget exhausted");
1242
+ opts.onDisconnect();
1243
+ return;
1244
+ }
1245
+ const delay = Math.min(1e3 * 2 ** reconnectAttempt, MAX_RECONNECT_DELAY);
1246
+ reconnectAttempt++;
1247
+ reconnectTimer = setTimeout(connect, delay);
1248
+ }
1249
+ function clearPing() {
1250
+ if (pingInterval) {
1251
+ clearInterval(pingInterval);
1252
+ pingInterval = null;
1253
+ }
1254
+ }
1255
+ function send(msg) {
1256
+ if (ws?.readyState === WebSocket.OPEN) {
1257
+ ws.send(JSON.stringify(msg));
1258
+ }
1259
+ }
1260
+ connect();
1261
+ return {
1262
+ sendResponse(to, body) {
1263
+ send({ type: "response", body, to });
1264
+ },
1265
+ sendStatus(status) {
1266
+ send({ type: "status", status });
1267
+ },
1268
+ close() {
1269
+ closed = true;
1270
+ clearPing();
1271
+ if (reconnectTimer) clearTimeout(reconnectTimer);
1272
+ if (ws) {
1273
+ try {
1274
+ ws.close(1e3, "Client closing");
1275
+ } catch {
1276
+ }
1277
+ }
1278
+ }
1279
+ };
1280
+ }
1281
+
1282
+ // src/modes/sms.ts
1283
+ import crypto from "crypto";
1284
+ var SMS_API_BASE = "wss://api.subagents.com";
1285
+ var TWILIO_PHONE = process.env.TWILIO_PHONE_NUMBER || "+18556242904";
1286
+ var MAX_SMS_LEN = 1500;
1287
+ function generatePairingCode() {
1288
+ return String(crypto.randomInt(1e3, 9999));
1289
+ }
1290
+ function truncateForSms(text) {
1291
+ if (text.length <= MAX_SMS_LEN) return text;
1292
+ return text.slice(0, MAX_SMS_LEN - 30) + '\n\n[text "/full" for complete response]';
1293
+ }
1294
+ async function runSmsMode(agentOptions, modelName) {
1295
+ const sessionId = generateSessionId();
1296
+ const pairingCode = generatePairingCode();
1297
+ const model = modelName;
1298
+ const tools = [...agentOptions.tools ?? [], writeMemoryTool, readMemoryTool];
1299
+ let agent;
1300
+ try {
1301
+ agent = await createAgent3({ ...agentOptions, tools });
1302
+ } catch (err) {
1303
+ writeError(err.message ?? String(err));
1304
+ process.exit(1);
1305
+ }
1306
+ let totalTokens = 0;
1307
+ let totalCost = 0;
1308
+ let isInText = false;
1309
+ let isInThinking = false;
1310
+ let isWorking = false;
1311
+ let assistantText = "";
1312
+ let lastFullResponse = "";
1313
+ let pairedPhone = "";
1314
+ let spinnerInterval = null;
1315
+ let relay;
1316
+ process.stderr.write("\n");
1317
+ process.stderr.write(styled(color.bold, ` sa `) + styled(theme.dim, `SMS Remote Control
1318
+ `));
1319
+ process.stderr.write(styled(theme.dim, ` Model: `) + styled(color.brightCyan, model) + "\n");
1320
+ process.stderr.write(styled(theme.dim, ` Session: `) + sessionId + "\n");
1321
+ process.stderr.write("\n");
1322
+ process.stderr.write(styled(color.brightYellow, ` Text "${pairingCode}" to ${TWILIO_PHONE} to connect
1323
+ `));
1324
+ process.stderr.write(styled(theme.dim, ` Ctrl+C to stop
1325
+ `));
1326
+ process.stderr.write("\n");
1327
+ agent.subscribe((event) => {
1328
+ switch (event.type) {
1329
+ case "message_update": {
1330
+ const e = event.assistantMessageEvent;
1331
+ if (e.type === "text_delta") {
1332
+ if (isInThinking) {
1333
+ isInThinking = false;
1334
+ endThinking();
1335
+ }
1336
+ isInText = true;
1337
+ writeTextDelta(e.delta);
1338
+ assistantText += e.delta;
1339
+ } else if (e.type === "thinking_delta") {
1340
+ if (!isInThinking) {
1341
+ process.stderr.write(styled(theme.thinking, "thinking... "));
1342
+ isInThinking = true;
1343
+ }
1344
+ writeThinkingDelta(e.delta);
1345
+ } else if (e.type === "thinking_end") {
1346
+ if (isInThinking) {
1347
+ isInThinking = false;
1348
+ endThinking();
1349
+ }
1350
+ }
1351
+ break;
1352
+ }
1353
+ case "turn_end": {
1354
+ if (isInText) {
1355
+ endAssistantMessage();
1356
+ isInText = false;
1357
+ }
1358
+ const msg = event.message;
1359
+ if (msg?.usage) {
1360
+ totalTokens += msg.usage.totalTokens ?? 0;
1361
+ totalCost += msg.usage.cost?.total ?? 0;
1362
+ }
1363
+ if (assistantText) {
1364
+ appendEvent(sessionId, { type: "assistant", content: assistantText, timestamp: Date.now() });
1365
+ }
1366
+ break;
1367
+ }
1368
+ case "tool_execution_start": {
1369
+ toolStart(event.toolCallId, event.toolName, event.args);
1370
+ appendEvent(sessionId, {
1371
+ type: "tool_call",
1372
+ name: event.toolName,
1373
+ args: event.args,
1374
+ timestamp: Date.now()
1375
+ });
1376
+ break;
1377
+ }
1378
+ case "tool_execution_end": {
1379
+ toolEnd(event.toolCallId, event.result, event.isError);
1380
+ break;
1381
+ }
1382
+ case "agent_end": {
1383
+ isWorking = false;
1384
+ stopSpinner();
1385
+ lastFullResponse = assistantText;
1386
+ if (pairedPhone && assistantText) {
1387
+ relay.sendResponse(pairedPhone, truncateForSms(assistantText));
1388
+ }
1389
+ assistantText = "";
1390
+ const bar = renderStatusBar({
1391
+ model,
1392
+ totalTokens,
1393
+ totalCost,
1394
+ sessionId,
1395
+ isStreaming: false
1396
+ });
1397
+ process.stderr.write(`${bar}
1398
+
1399
+ `);
1400
+ relay.sendStatus("idle");
1401
+ break;
1402
+ }
1403
+ case "agent_start": {
1404
+ isWorking = true;
1405
+ startSpinner();
1406
+ relay.sendStatus("working");
1407
+ break;
1408
+ }
1409
+ }
1410
+ });
1411
+ function startSpinner() {
1412
+ if (spinnerInterval) return;
1413
+ spinnerInterval = setInterval(() => {
1414
+ if (hasActiveWork()) {
1415
+ updateLivePanel();
1416
+ }
1417
+ }, 100);
1418
+ }
1419
+ function stopSpinner() {
1420
+ if (spinnerInterval) {
1421
+ clearInterval(spinnerInterval);
1422
+ spinnerInterval = null;
1423
+ clearLiveRender();
1424
+ }
1425
+ }
1426
+ function handleSmsCommand(text) {
1427
+ const cmd = text.toLowerCase().trim();
1428
+ if (cmd === "/full") {
1429
+ if (lastFullResponse && pairedPhone) {
1430
+ relay.sendResponse(pairedPhone, lastFullResponse);
1431
+ } else if (pairedPhone) {
1432
+ relay.sendResponse(pairedPhone, "No previous response to show.");
1433
+ }
1434
+ return true;
1435
+ }
1436
+ if (cmd === "/status") {
1437
+ if (pairedPhone) {
1438
+ const status = isWorking ? "Working..." : "Idle";
1439
+ relay.sendResponse(pairedPhone, `Status: ${status}
1440
+ Tokens: ${totalTokens}
1441
+ Cost: $${totalCost.toFixed(4)}`);
1442
+ }
1443
+ return true;
1444
+ }
1445
+ if (cmd === "/clear") {
1446
+ agent.reset();
1447
+ totalTokens = 0;
1448
+ totalCost = 0;
1449
+ lastFullResponse = "";
1450
+ if (pairedPhone) relay.sendResponse(pairedPhone, "Conversation cleared.");
1451
+ writeInfo("Conversation cleared (via SMS).");
1452
+ return true;
1453
+ }
1454
+ if (cmd === "/quit") {
1455
+ if (pairedPhone) relay.sendResponse(pairedPhone, "Session ended.");
1456
+ cleanup();
1457
+ return true;
1458
+ }
1459
+ return false;
1460
+ }
1461
+ relay = connectRelay({
1462
+ sessionId,
1463
+ pairingCode,
1464
+ apiBaseUrl: SMS_API_BASE,
1465
+ onPrompt: async (body, from) => {
1466
+ process.stderr.write(styled(color.brightCyan, `
1467
+ [SMS from ${from}] `) + body + "\n\n");
1468
+ appendEvent(sessionId, { type: "user", content: `[SMS] ${body}`, timestamp: Date.now() });
1469
+ if (body.startsWith("/")) {
1470
+ if (handleSmsCommand(body)) return;
1471
+ }
1472
+ if (isWorking) {
1473
+ writeInfo("(steering agent via SMS...)");
1474
+ agent.steer({
1475
+ role: "user",
1476
+ content: [{ type: "text", text: body }],
1477
+ timestamp: Date.now()
1478
+ });
1479
+ return;
1480
+ }
1481
+ try {
1482
+ await agent.prompt(body);
1483
+ await agent.waitForIdle();
1484
+ } catch (err) {
1485
+ if (err.name === "AbortError") {
1486
+ process.stderr.write(`
1487
+ ${styled(theme.dim, "(aborted)")}
1488
+ `);
1489
+ } else {
1490
+ writeError(err.message ?? String(err));
1491
+ if (pairedPhone) relay.sendResponse(pairedPhone, `Error: ${err.message}`);
1492
+ }
1493
+ }
1494
+ },
1495
+ onPaired: (phone) => {
1496
+ pairedPhone = phone;
1497
+ process.stderr.write(styled(color.brightGreen, ` Paired with ${phone}
1498
+
1499
+ `));
1500
+ },
1501
+ onError: (message) => {
1502
+ writeError(`Relay: ${message}`);
1503
+ },
1504
+ onDisconnect: () => {
1505
+ writeInfo("Relay disconnected.");
1506
+ cleanup();
1507
+ }
1508
+ });
1509
+ function cleanup() {
1510
+ stopSpinner();
1511
+ relay.close();
1512
+ agent.destroy();
1513
+ process.stderr.write(`
1514
+ ${styled(theme.dim, `Session saved: ${sessionId}`)}
1515
+ `);
1516
+ process.exit(0);
1517
+ }
1518
+ process.on("SIGINT", cleanup);
1519
+ process.on("SIGTERM", cleanup);
1520
+ process.stderr.write(styled(theme.dim, " Waiting for SMS connection...\n"));
1521
+ }
1522
+
1523
+ // src/main.ts
1524
+ var VERSION = "0.1.0";
1525
+ var HELP = `${styled(color.brightCyan, "sa")} \u2014 fluid, subagent-centric coding agent
1526
+
1527
+ ${styled(color.bold, "Usage:")}
1528
+ sa Interactive mode (TUI)
1529
+ sa "fix the login bug" Run prompt and exit (print mode)
1530
+ sa --sms SMS remote control (text prompts from phone)
1531
+ sa --resume Resume last session
1532
+ sa sessions List saved sessions
1533
+
1534
+ ${styled(color.bold, "Options:")}
1535
+ -m, --model <model> Model (e.g., anthropic:claude-sonnet-4-6)
1536
+ -p, --print Force print mode (non-interactive)
1537
+ -r, --resume [id] Resume a session (latest if no id)
1538
+ --sms SMS remote control mode
1539
+ --thinking <level> Thinking level: off, minimal, low, medium, high
1540
+ --debug Enable debug logging
1541
+ -h, --help Show this help
1542
+ -v, --version Show version
1543
+
1544
+ ${styled(color.bold, "Interactive commands:")}
1545
+ /help Show available commands
1546
+ /sessions List sessions
1547
+ /status Show tokens, cost, model
1548
+ /clear Clear conversation
1549
+ /quit Exit
1550
+
1551
+ ${styled(color.bold, "Memory:")}
1552
+ The agent saves learnings to .sa/memory.md (project) or ~/.sa/memory.md (global).
1553
+ Memory auto-loads on startup so the agent improves across sessions.
1554
+
1555
+ ${styled(color.bold, "Config:")}
1556
+ ~/.sa/config.json Global config
1557
+ .sa/config.json Project config (overrides global)
1558
+
1559
+ ${styled(color.bold, "Setup:")}
1560
+ Set your API key: echo 'ANTHROPIC_API_KEY=sk-...' >> ~/.sa/.env
1561
+ `;
1562
+ async function main(argv) {
1563
+ const flags = parseFlags(argv);
1564
+ if (flags.help) {
1565
+ process.stderr.write(HELP);
1566
+ process.exit(0);
1567
+ }
1568
+ if (flags.version) {
1569
+ process.stderr.write(`sa v${VERSION}
1570
+ `);
1571
+ process.exit(0);
1572
+ }
1573
+ if (flags.sessions) {
1574
+ process.stderr.write(formatSessionList(listSessions()) + "\n");
1575
+ process.exit(0);
1576
+ }
1577
+ const config = loadConfig();
1578
+ const agentOptions = configToAgentOptions(config, flags);
1579
+ const modelName = resolveModelName(config, flags);
1580
+ if (flags.sms) {
1581
+ await runSmsMode(agentOptions, modelName);
1582
+ return;
1583
+ }
1584
+ if (flags.resume) {
1585
+ let sessionId;
1586
+ if (typeof flags.resume === "string") {
1587
+ sessionId = flags.resume;
1588
+ } else {
1589
+ sessionId = getLatestSessionId() ?? void 0;
1590
+ if (!sessionId) {
1591
+ writeError("No sessions found to resume.");
1592
+ process.exit(1);
1593
+ }
1594
+ }
1595
+ await runInteractiveMode(agentOptions, modelName, sessionId);
1596
+ return;
1597
+ }
1598
+ if (flags.prompt) {
1599
+ await runPrintMode(flags.prompt, agentOptions, modelName);
1600
+ return;
1601
+ }
1602
+ if (!process.stdin.isTTY) {
1603
+ const chunks = [];
1604
+ for await (const chunk of process.stdin) {
1605
+ chunks.push(chunk);
1606
+ }
1607
+ const input = Buffer.concat(chunks).toString("utf-8").trim();
1608
+ if (input) {
1609
+ await runPrintMode(input, agentOptions, modelName);
1610
+ return;
1611
+ }
1612
+ }
1613
+ await runInteractiveMode(agentOptions, modelName);
1614
+ }
1615
+
1616
+ // src/cli.ts
1617
+ loadEnv();
1618
+ process.on("uncaughtException", (err) => {
1619
+ const msg = err.message ?? String(err);
1620
+ if (msg.includes("No API key")) {
1621
+ process.stderr.write(
1622
+ "\x1B[31merror\x1B[0m: No API key found. Set ANTHROPIC_API_KEY in:\n - Environment variable\n - .env file in project directory\n - ~/.sa/.env for global config\n"
1623
+ );
1624
+ } else {
1625
+ process.stderr.write(`\x1B[31merror\x1B[0m: ${msg}
1626
+ `);
1627
+ }
1628
+ process.exit(1);
1629
+ });
1630
+ process.on("unhandledRejection", (err) => {
1631
+ const msg = err?.message ?? String(err);
1632
+ if (msg.includes("No API key")) {
1633
+ process.stderr.write(
1634
+ "\x1B[31merror\x1B[0m: No API key found. Set ANTHROPIC_API_KEY in:\n - Environment variable\n - .env file in project directory\n - ~/.sa/.env for global config\n"
1635
+ );
1636
+ } else {
1637
+ process.stderr.write(`\x1B[31merror\x1B[0m: ${msg}
1638
+ `);
1639
+ }
1640
+ process.exit(1);
1641
+ });
1642
+ main(process.argv.slice(2)).catch((err) => {
1643
+ process.stderr.write(`\x1B[31merror\x1B[0m: ${err.message ?? err}
1644
+ `);
1645
+ process.exit(1);
1646
+ });
1647
+ //# sourceMappingURL=cli.js.map