@tyvm/knowhow 0.0.130 → 0.0.132

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.
Files changed (97) hide show
  1. package/package.json +1 -1
  2. package/src/agents/configurable/ConfigAgent.ts +0 -1
  3. package/src/agents/tools/startAgentTask.ts +17 -13
  4. package/src/ai.ts +55 -24
  5. package/src/chat/CliChatService.ts +7 -1
  6. package/src/chat/modules/AgentModule.ts +72 -14
  7. package/src/chat/renderer/PlainRenderer.ts +218 -0
  8. package/src/chat/renderer/index.ts +1 -0
  9. package/src/chat/renderer/loadRenderer.ts +2 -0
  10. package/src/chat.ts +2 -2
  11. package/src/cli.ts +23 -0
  12. package/src/clients/anthropic.ts +9 -1
  13. package/src/clients/fireworks.ts +10 -1
  14. package/src/clients/index.ts +6 -3
  15. package/src/clients/meta.ts +9 -0
  16. package/src/clients/pricing/anthropic.ts +11 -0
  17. package/src/clients/pricing/fireworks.ts +12 -0
  18. package/src/commands/agent.ts +53 -20
  19. package/src/commands/agents.ts +556 -0
  20. package/src/commands/hash.ts +170 -0
  21. package/src/commands/misc.ts +157 -3
  22. package/src/commands/skills.ts +36 -0
  23. package/src/config.ts +54 -2
  24. package/src/generate.ts +507 -0
  25. package/src/index.ts +32 -216
  26. package/src/services/BehaviorsService.ts +32 -0
  27. package/src/services/SkillsService.ts +198 -0
  28. package/src/types.ts +36 -4
  29. package/ts_build/package.json +1 -1
  30. package/ts_build/src/agents/configurable/ConfigAgent.js.map +1 -1
  31. package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -2
  32. package/ts_build/src/agents/tools/startAgentTask.js +13 -12
  33. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  34. package/ts_build/src/ai.d.ts +10 -4
  35. package/ts_build/src/ai.js +27 -25
  36. package/ts_build/src/ai.js.map +1 -1
  37. package/ts_build/src/chat/CliChatService.d.ts +1 -1
  38. package/ts_build/src/chat/CliChatService.js +4 -1
  39. package/ts_build/src/chat/CliChatService.js.map +1 -1
  40. package/ts_build/src/chat/modules/AgentModule.js +61 -4
  41. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  42. package/ts_build/src/chat/renderer/PlainRenderer.d.ts +28 -0
  43. package/ts_build/src/chat/renderer/PlainRenderer.js +169 -0
  44. package/ts_build/src/chat/renderer/PlainRenderer.js.map +1 -0
  45. package/ts_build/src/chat/renderer/index.d.ts +1 -0
  46. package/ts_build/src/chat/renderer/index.js +3 -1
  47. package/ts_build/src/chat/renderer/index.js.map +1 -1
  48. package/ts_build/src/chat/renderer/loadRenderer.js +2 -0
  49. package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -1
  50. package/ts_build/src/chat.d.ts +1 -1
  51. package/ts_build/src/chat.js +2 -2
  52. package/ts_build/src/chat.js.map +1 -1
  53. package/ts_build/src/cli.js +16 -0
  54. package/ts_build/src/cli.js.map +1 -1
  55. package/ts_build/src/clients/anthropic.js +4 -1
  56. package/ts_build/src/clients/anthropic.js.map +1 -1
  57. package/ts_build/src/clients/fireworks.js +7 -0
  58. package/ts_build/src/clients/fireworks.js.map +1 -1
  59. package/ts_build/src/clients/index.js +5 -1
  60. package/ts_build/src/clients/index.js.map +1 -1
  61. package/ts_build/src/clients/meta.d.ts +2 -0
  62. package/ts_build/src/clients/meta.js +5 -0
  63. package/ts_build/src/clients/meta.js.map +1 -1
  64. package/ts_build/src/clients/pricing/anthropic.d.ts +3 -0
  65. package/ts_build/src/clients/pricing/anthropic.js +6 -0
  66. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  67. package/ts_build/src/clients/pricing/fireworks.d.ts +1 -0
  68. package/ts_build/src/clients/pricing/fireworks.js +5 -1
  69. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  70. package/ts_build/src/commands/agent.js +44 -17
  71. package/ts_build/src/commands/agent.js.map +1 -1
  72. package/ts_build/src/commands/agents.d.ts +2 -0
  73. package/ts_build/src/commands/agents.js +460 -0
  74. package/ts_build/src/commands/agents.js.map +1 -0
  75. package/ts_build/src/commands/hash.d.ts +2 -0
  76. package/ts_build/src/commands/hash.js +150 -0
  77. package/ts_build/src/commands/hash.js.map +1 -0
  78. package/ts_build/src/commands/misc.js +108 -2
  79. package/ts_build/src/commands/misc.js.map +1 -1
  80. package/ts_build/src/commands/skills.js +24 -0
  81. package/ts_build/src/commands/skills.js.map +1 -1
  82. package/ts_build/src/config.js +37 -1
  83. package/ts_build/src/config.js.map +1 -1
  84. package/ts_build/src/generate.d.ts +16 -0
  85. package/ts_build/src/generate.js +304 -0
  86. package/ts_build/src/generate.js.map +1 -0
  87. package/ts_build/src/index.d.ts +1 -3
  88. package/ts_build/src/index.js +9 -118
  89. package/ts_build/src/index.js.map +1 -1
  90. package/ts_build/src/services/BehaviorsService.d.ts +2 -0
  91. package/ts_build/src/services/BehaviorsService.js +24 -0
  92. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  93. package/ts_build/src/services/SkillsService.d.ts +7 -0
  94. package/ts_build/src/services/SkillsService.js +163 -0
  95. package/ts_build/src/services/SkillsService.js.map +1 -1
  96. package/ts_build/src/types.d.ts +14 -4
  97. package/ts_build/src/types.js.map +1 -1
@@ -0,0 +1,556 @@
1
+ /**
2
+ * `knowhow agents` — CLI commands for inspecting running / recent agent tasks.
3
+ *
4
+ * Subcommands:
5
+ * knowhow agents list — list all known agent task dirs + status + last-modified
6
+ * knowhow agents status <taskId> — one-line summary (status, last tool, cost, elapsed)
7
+ * knowhow agents tail <taskId> — print last N messages; without -f behaves like read-only attach
8
+ * knowhow agents attach <taskId> — open the full knowhow chat interface attached to the agent
9
+ *
10
+ * Both `status` and `tail` accept:
11
+ * - A full task ID
12
+ * - A partial task ID (substring match)
13
+ * - `-i <number>` / `--index <number>` to select by row number from `agents list`
14
+ *
15
+ * Data source: .knowhow/processes/agents/<taskId>/metadata.json (written by AgentSyncFs)
16
+ * and .knowhow/chats/sessions/<taskId>.json (written by SessionManager).
17
+ */
18
+
19
+ import { Command } from "commander";
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+ import { startChat } from "../chat";
23
+
24
+ const AGENTS_DIR = path.join(".knowhow", "processes", "agents");
25
+ const SESSIONS_DIR = path.join(".knowhow", "chats", "sessions");
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function readJson(filePath: string): any | null {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function agentTaskIds(): string[] {
40
+ if (!fs.existsSync(AGENTS_DIR)) return [];
41
+ return fs
42
+ .readdirSync(AGENTS_DIR, { withFileTypes: true })
43
+ .filter((e) => e.isDirectory())
44
+ .map((e) => e.name);
45
+ }
46
+
47
+ function loadMeta(taskId: string): any | null {
48
+ return readJson(path.join(AGENTS_DIR, taskId, "metadata.json"));
49
+ }
50
+
51
+ function loadStatus(taskId: string): string {
52
+ const f = path.join(AGENTS_DIR, taskId, "status.txt");
53
+ try {
54
+ return fs.readFileSync(f, "utf-8").trim();
55
+ } catch {
56
+ return "unknown";
57
+ }
58
+ }
59
+
60
+ /** Get the canonical status — meta.status is live/preferred; status.txt is fallback */
61
+ function getStatus(taskId: string, meta: any | null): string {
62
+ if (meta?.status) return meta.status;
63
+ const fromFile = loadStatus(taskId);
64
+ return fromFile ?? "unknown";
65
+ }
66
+
67
+ function elapsedStr(startIso: string | undefined, endIso?: string): string {
68
+ if (!startIso) return "?";
69
+ const start = new Date(startIso).getTime();
70
+ const end = endIso ? new Date(endIso).getTime() : Date.now();
71
+ const ms = end - start;
72
+ if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
73
+ if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
74
+ return `${Math.floor(ms / 3_600_000)}h ${Math.floor((ms % 3_600_000) / 60_000)}m`;
75
+ }
76
+
77
+ function shortId(taskId: string, maxLen = 52): string {
78
+ return taskId.length > maxLen ? taskId.slice(0, maxLen) + "…" : taskId;
79
+ }
80
+
81
+ /** Extract the last assistant message text from a thread */
82
+ function lastAssistantMessage(thread: any[]): string | undefined {
83
+ for (let i = thread.length - 1; i >= 0; i--) {
84
+ const msg = thread[i];
85
+ if (msg.role === "assistant") {
86
+ if (typeof msg.content === "string" && msg.content.trim()) {
87
+ return msg.content.trim();
88
+ }
89
+ if (Array.isArray(msg.content)) {
90
+ const text = msg.content
91
+ .filter((c: any) => c.type === "text")
92
+ .map((c: any) => c.text)
93
+ .join("\n")
94
+ .trim();
95
+ if (text) return text;
96
+ }
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ /** Extract last tool call name from a thread */
103
+ function lastToolCall(thread: any[]): string | undefined {
104
+ for (let i = thread.length - 1; i >= 0; i--) {
105
+ const msg = thread[i];
106
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
107
+ const last = msg.tool_calls[msg.tool_calls.length - 1];
108
+ return last?.function?.name;
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ /** Pretty-print a single message from a thread */
115
+ function formatMessage(msg: any, index: number): string {
116
+ const role = (msg.role as string).padEnd(9);
117
+ let content = "";
118
+
119
+ if (typeof msg.content === "string") {
120
+ content = msg.content.trim().slice(0, 300);
121
+ if (msg.content.length > 300) content += "…";
122
+ } else if (Array.isArray(msg.content)) {
123
+ content = msg.content
124
+ .filter((c: any) => c.type === "text")
125
+ .map((c: any) => (c.text as string).slice(0, 200))
126
+ .join(" | ");
127
+ }
128
+
129
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
130
+ const tools = msg.tool_calls.map((tc: any) => {
131
+ const name = tc?.function?.name ?? "?";
132
+ let args = "";
133
+ try {
134
+ const parsed = JSON.parse(tc?.function?.arguments ?? "{}");
135
+ const entries = Object.entries(parsed).slice(0, 2);
136
+ args = entries.map(([k, v]) => `${k}=${String(v).slice(0, 40)}`).join(", ");
137
+ } catch {
138
+ args = (tc?.function?.arguments ?? "").slice(0, 80);
139
+ }
140
+ return `[TOOL] ${name}(${args})`;
141
+ });
142
+ content = content ? content + "\n " + tools.join("\n ") : tools.join("\n ");
143
+ }
144
+
145
+ if (msg.role === "tool") {
146
+ const result = typeof msg.content === "string" ? msg.content.slice(0, 200) : JSON.stringify(msg.content).slice(0, 200);
147
+ content = `[RESULT] ${result}${(typeof msg.content === "string" ? msg.content : "").length > 200 ? "…" : ""}`;
148
+ }
149
+
150
+ return ` #${String(index).padStart(3)} [${role}] ${content}`;
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Row building (shared between list display and index resolution)
155
+ // ---------------------------------------------------------------------------
156
+
157
+ type Row = {
158
+ taskId: string; // full task ID (not truncated)
159
+ displayId: string; // truncated for display
160
+ agentName: string;
161
+ status: string;
162
+ cost: string;
163
+ elapsed: string;
164
+ lastUpdate: string;
165
+ };
166
+
167
+ function buildRows(includeAll: boolean): Row[] {
168
+ const taskIds = agentTaskIds();
169
+ const rows: Row[] = [];
170
+
171
+ for (const taskId of taskIds) {
172
+ const meta = loadMeta(taskId);
173
+ const status = meta?.status ?? loadStatus(taskId);
174
+
175
+ if (!includeAll && (status === "completed" || status === "killed")) {
176
+ const lastUpdate = meta?.lastUpdate ?? "";
177
+ if (lastUpdate) {
178
+ const age = Date.now() - new Date(lastUpdate).getTime();
179
+ if (age > 3_600_000) continue;
180
+ } else {
181
+ continue;
182
+ }
183
+ }
184
+
185
+ const cost = meta?.totalCostUsd != null ? `$${(meta.totalCostUsd as number).toFixed(3)}` : "";
186
+ const elapsed = elapsedStr(meta?.startTime, meta?.status === "completed" ? meta?.lastUpdate : undefined);
187
+ const lastUpdate = meta?.lastUpdate ? new Date(meta.lastUpdate).toLocaleString() : "?";
188
+
189
+ rows.push({
190
+ taskId,
191
+ displayId: shortId(taskId),
192
+ agentName: meta?.agentName ?? "?",
193
+ status: status ?? "unknown",
194
+ cost,
195
+ elapsed,
196
+ lastUpdate,
197
+ });
198
+ }
199
+
200
+ return rows;
201
+ }
202
+
203
+ /**
204
+ * Resolve a task identifier to a real task ID.
205
+ * Accepts:
206
+ * - A numeric index string ("1", "2", …) — 1-based row from `agents list`
207
+ * - An exact task ID
208
+ * - A partial task ID (substring match)
209
+ */
210
+ function resolveTaskId(value: string, includeAll = false): string | null {
211
+ const n = parseInt(value, 10);
212
+ // Only treat as a numeric index if it's a small number (≤ 9999) — large numbers are timestamp-based task IDs
213
+ if (!isNaN(n) && String(n) === value.trim() && n <= 9999) {
214
+ const rows = buildRows(includeAll);
215
+ const row = rows[n - 1];
216
+ return row ? row.taskId : null;
217
+ }
218
+
219
+ const all = agentTaskIds();
220
+ if (all.includes(value)) return value;
221
+ return all.find((id) => id.includes(value)) ?? null;
222
+ }
223
+
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Command handlers
227
+ // ---------------------------------------------------------------------------
228
+
229
+ async function cmdList(opts: { all: boolean; json: boolean }): Promise<void> {
230
+ const rows = buildRows(opts.all);
231
+
232
+ if (rows.length === 0) {
233
+ if (agentTaskIds().length === 0) {
234
+ console.log("No agent task directories found under", AGENTS_DIR);
235
+ } else {
236
+ console.log("No active agent tasks. Use --all to include completed tasks.");
237
+ }
238
+ return;
239
+ }
240
+
241
+ if (opts.json) {
242
+ console.log(JSON.stringify(rows.map(({ taskId, agentName, status, cost, elapsed, lastUpdate }) => ({
243
+ taskId, agentName, status, cost, elapsed, lastUpdate,
244
+ })), null, 2));
245
+ return;
246
+ }
247
+
248
+ // Table output
249
+ const COL_IDX = 4;
250
+ const COL_ID = 55;
251
+ const COL_AGENT = 14;
252
+ const COL_STATUS = 12;
253
+ const COL_COST = 8;
254
+ const COL_ELAPSED = 10;
255
+
256
+ const header =
257
+ "#".padEnd(COL_IDX) +
258
+ "TASK ID".padEnd(COL_ID) +
259
+ "AGENT".padEnd(COL_AGENT) +
260
+ "STATUS".padEnd(COL_STATUS) +
261
+ "COST".padEnd(COL_COST) +
262
+ "ELAPSED".padEnd(COL_ELAPSED) +
263
+ "LAST UPDATE";
264
+
265
+ console.log("\n" + header);
266
+ console.log("─".repeat(120));
267
+
268
+ rows.forEach((row, idx) => {
269
+ process.stdout.write(
270
+ String(idx + 1).padEnd(COL_IDX) +
271
+ row.displayId.padEnd(COL_ID) +
272
+ row.agentName.padEnd(COL_AGENT) +
273
+ row.status.padEnd(COL_STATUS) +
274
+ row.cost.padEnd(COL_COST) +
275
+ row.elapsed.padEnd(COL_ELAPSED) +
276
+ row.lastUpdate +
277
+ "\n"
278
+ );
279
+ });
280
+ console.log("\nTip: use -i <#> with tail/status/attach to select by row number, e.g. knowhow agents attach -i 1\n");
281
+ }
282
+
283
+
284
+
285
+ async function cmdStatus(taskIdOrIndex: string | undefined, opts: { index?: string }): Promise<void> {
286
+ const value = opts.index ?? taskIdOrIndex;
287
+ if (!value) {
288
+ console.error("Provide a task ID or use -i <number> to select by index.");
289
+ process.exit(1);
290
+ }
291
+
292
+ const resolvedId = resolveTaskId(value);
293
+ if (!resolvedId) {
294
+ console.error(`Task not found: ${value}`);
295
+ const rows = buildRows(true);
296
+ if (rows.length > 0) {
297
+ console.error("\nAvailable tasks:");
298
+ rows.forEach((r, i) => console.error(` ${i + 1}. ${r.displayId}`));
299
+ }
300
+ process.exit(1);
301
+ }
302
+
303
+ if (resolvedId !== value) {
304
+ console.log(`Resolved: ${resolvedId}`);
305
+ }
306
+
307
+ const meta = loadMeta(resolvedId!);
308
+ const status = getStatus(resolvedId!, meta);
309
+
310
+ console.log("\n📋 Agent Status");
311
+ console.log("─".repeat(60));
312
+ console.log(`Task ID: ${resolvedId}`);
313
+ console.log(`Agent: ${meta?.agentName ?? "?"}`);
314
+ console.log(`Status: ${status}`);
315
+ console.log(`Cost: ${meta?.totalCostUsd != null ? "$" + (meta.totalCostUsd as number).toFixed(4) : "?"}`);
316
+ console.log(`Started: ${meta?.startTime ? new Date(meta.startTime).toLocaleString() : "?"}`);
317
+ console.log(`Last update:${meta?.lastUpdate ? " " + new Date(meta.lastUpdate).toLocaleString() : " ?"}`);
318
+ console.log(`Elapsed: ${elapsedStr(meta?.startTime)}`);
319
+
320
+ const threads: any[][] = meta?.threads ?? [];
321
+ if (threads.length > 0) {
322
+ const lastThread = threads[threads.length - 1];
323
+ const msgCount = lastThread.length;
324
+ const lastTool = lastToolCall(lastThread);
325
+ const lastMsg = lastAssistantMessage(lastThread);
326
+
327
+ console.log(`Messages: ${msgCount} in last thread`);
328
+ if (lastTool) {
329
+ console.log(`Last tool: ${lastTool}`);
330
+ }
331
+ if (lastMsg) {
332
+ const preview = lastMsg.slice(0, 200);
333
+ console.log(`Last msg: ${preview}${lastMsg.length > 200 ? "…" : ""}`);
334
+ }
335
+ }
336
+
337
+ if (meta?.result) {
338
+ const result = String(meta.result).slice(0, 300);
339
+ console.log(`\nResult:\n ${result}`);
340
+ }
341
+
342
+ console.log("─".repeat(60));
343
+ }
344
+
345
+ async function cmdTail(taskIdOrIndex: string | undefined, opts: { count: number; raw: boolean; follow: boolean; index?: string }): Promise<void> {
346
+ const value = opts.index ?? taskIdOrIndex;
347
+ if (!value) {
348
+ console.error("Provide a task ID or use -i <number> to select by index.");
349
+ process.exit(1);
350
+ }
351
+
352
+ const resolvedId = resolveTaskId(value);
353
+ if (!resolvedId) {
354
+ console.error(`Task not found: ${value}`);
355
+ const rows = buildRows(true);
356
+ if (rows.length > 0) {
357
+ console.error("\nAvailable tasks:");
358
+ rows.forEach((r, i) => console.error(` ${i + 1}. ${r.displayId}`));
359
+ }
360
+ process.exit(1);
361
+ return;
362
+ }
363
+
364
+ if (resolvedId !== value) {
365
+ console.log(`Resolved: ${resolvedId}`);
366
+ }
367
+
368
+ const meta = loadMeta(resolvedId);
369
+ if (!meta) {
370
+ console.error(`No metadata found for task: ${resolvedId}`);
371
+ process.exit(1);
372
+ }
373
+
374
+ const threads: any[][] = meta.threads ?? [];
375
+ if (threads.length === 0) {
376
+ console.log("No thread data found in metadata.json (agent may not have started yet).");
377
+ return;
378
+ }
379
+
380
+ const lastThread = threads[threads.length - 1];
381
+
382
+ if (opts.raw) {
383
+ const slice = lastThread.slice(-opts.count);
384
+ console.log(JSON.stringify(slice, null, 2));
385
+ if (!opts.follow) return;
386
+ }
387
+
388
+ const slice = lastThread.slice(-opts.count);
389
+ const startIndex = lastThread.length - slice.length;
390
+
391
+ console.log(`\n📜 Last ${slice.length} messages for task: ${resolvedId}`);
392
+ console.log(` Agent: ${meta.agentName} | Status: ${meta.status ?? loadStatus(resolvedId)} | Total messages: ${lastThread.length}`);
393
+ console.log("─".repeat(80));
394
+
395
+ for (let i = 0; i < slice.length; i++) {
396
+ console.log(formatMessage(slice[i], startIndex + i));
397
+ }
398
+
399
+ console.log("─".repeat(80));
400
+
401
+ const cost = meta.totalCostUsd != null ? `$${(meta.totalCostUsd as number).toFixed(4)}` : "?";
402
+ const elapsed = elapsedStr(meta.startTime);
403
+ const status = getStatus(resolvedId, meta);
404
+ console.log(`Cost: ${cost} | Elapsed: ${elapsed} | Last update: ${meta.lastUpdate ? new Date(meta.lastUpdate).toLocaleString() : "?"}`);
405
+
406
+ const isDone = status === "completed" || status === "failed" || status === "killed";
407
+
408
+ if (opts.follow) {
409
+ if (isDone) {
410
+ console.log(`\n✅ Agent already ${status}. Nothing to follow.\n`);
411
+ } else {
412
+ await followTask(resolvedId, lastThread.length);
413
+ }
414
+ } else if (!isDone) {
415
+ // Without -f, if agent is still running, hint that they can follow
416
+ console.log(`\n⏳ Agent is ${status}. Use -f to follow live output.\n`);
417
+ } else {
418
+ console.log("");
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Poll metadata.json every second, printing any new messages as they arrive.
424
+ * Exits when the agent status becomes completed/failed/killed.
425
+ */
426
+ async function followTask(resolvedId: string, seenCount: number): Promise<void> {
427
+ console.log(`\n👁 Following agent output… (Ctrl+C to stop)\n`);
428
+
429
+ // Check upfront — if already done, exit immediately
430
+ const initialMeta = loadMeta(resolvedId);
431
+ const initialStatus = getStatus(resolvedId, initialMeta);
432
+ if (initialStatus === "completed" || initialStatus === "failed" || initialStatus === "killed") {
433
+ console.log(`✅ Agent already ${initialStatus}. Nothing to follow.\n`);
434
+ return;
435
+ }
436
+
437
+ let seen = seenCount;
438
+
439
+ const interval = setInterval(() => {
440
+ const meta = loadMeta(resolvedId);
441
+ if (!meta) return;
442
+
443
+ const threads: any[][] = meta.threads ?? [];
444
+ if (threads.length === 0) return;
445
+ const thread = threads[threads.length - 1];
446
+
447
+ if (thread.length > seen) {
448
+ for (let i = seen; i < thread.length; i++) {
449
+ console.log(formatMessage(thread[i], i));
450
+ }
451
+ seen = thread.length;
452
+ }
453
+ const status = getStatus(resolvedId, meta);
454
+ if (status === "completed" || status === "failed" || status === "killed") {
455
+ const cost = meta.totalCostUsd != null ? `$${(meta.totalCostUsd as number).toFixed(4)}` : "?";
456
+ console.log(`\n─────────────────────────────────────────────────────────────────────────────────`);
457
+ console.log(`✅ Agent ${status}. Cost: ${cost} | Elapsed: ${elapsedStr(meta.startTime, meta.lastUpdate)}`);
458
+ clearInterval(interval);
459
+ process.exit(0);
460
+ }
461
+ }, 1000);
462
+
463
+ // Keep process alive
464
+ await new Promise<void>(() => {});
465
+ }
466
+
467
+ async function cmdAttach(taskIdOrIndex: string | undefined, opts: { index?: string }): Promise<void> {
468
+ const value = opts.index ?? taskIdOrIndex;
469
+ if (!value) {
470
+ console.error("Provide a task ID or use -i <number> to select by index.");
471
+ process.exit(1);
472
+ }
473
+
474
+ const resolvedId = resolveTaskId(value);
475
+ if (!resolvedId) {
476
+ console.error(`Task not found: ${value}`);
477
+ const rows = buildRows(true);
478
+ if (rows.length > 0) {
479
+ console.error("\nAvailable tasks:");
480
+ rows.forEach((r, i) => console.error(` ${i + 1}. ${r.displayId}`));
481
+ }
482
+ process.exit(1);
483
+ return;
484
+ }
485
+
486
+ // Delegate to the full knowhow chat interface.
487
+ // This gives the complete interactive experience: /logs, /poke, /kill,
488
+ // /detach — exactly the same as `knowhow chat` then typing the command.
489
+ const { setupServices } = await import("./services");
490
+ await setupServices();
491
+
492
+ // Always use /attach — if the user wants to resume they can do so from chat
493
+ await startChat(`/attach ${resolvedId}`);
494
+ }
495
+
496
+
497
+
498
+ // ---------------------------------------------------------------------------
499
+ // Command registration
500
+ // ---------------------------------------------------------------------------
501
+
502
+ export function addAgentsCommand(program: Command): void {
503
+ const agents = program
504
+ .command("agents")
505
+ .description("Inspect running and recent agent tasks");
506
+
507
+ agents
508
+ .command("list")
509
+ .description(
510
+ "List known agent task directories with their status and cost. " +
511
+ "Shows only active/recent tasks by default; use --all to include completed."
512
+ )
513
+ .option("--all", "Include completed and killed tasks", false)
514
+ .option("--json", "Output as JSON", false)
515
+ .action(async (opts) => {
516
+ await cmdList(opts);
517
+ });
518
+
519
+ agents
520
+ .command("status [taskId]")
521
+ .description(
522
+ "Show a summary for a specific agent task: status, last tool, cost, elapsed. " +
523
+ "Accepts a full task ID, partial substring, or -i <number> for row index from 'agents list'."
524
+ )
525
+ .option("-i, --index <number>", "Select task by row number from 'agents list'")
526
+ .action(async (taskId: string | undefined, opts: { index?: string }) => {
527
+ await cmdStatus(taskId, opts);
528
+ });
529
+
530
+ agents
531
+ .command("tail [taskId]")
532
+ .description(
533
+ "Print the last N messages from an agent's persisted thread. " +
534
+ "Accepts a full task ID, partial substring, or -i <number> for row index from 'agents list'. " +
535
+ "Use -f to follow live output."
536
+ )
537
+ .option("-i, --index <number>", "Select task by row number from 'agents list'")
538
+ .option("-n, --count <number>", "Number of messages to show", (v) => parseInt(v, 10), 20)
539
+ .option("--raw", "Output raw JSON of messages", false)
540
+ .option("-f, --follow", "Follow live output (poll for new messages)", false)
541
+ .action(async (taskId: string | undefined, opts: { count: number; raw: boolean; follow: boolean; index?: string }) => {
542
+ await cmdTail(taskId, opts);
543
+ });
544
+
545
+ agents
546
+ .command("attach [taskId]")
547
+ .description(
548
+ "Attach to a running agent and follow its live output. " +
549
+ "Shows recent history then streams new messages as they arrive. " +
550
+ "Accepts a full task ID, partial substring, or -i <number> for row index from 'agents list'."
551
+ )
552
+ .option("-i, --index <number>", "Select task by row number from 'agents list'")
553
+ .action(async (taskId: string | undefined, opts: { index?: string }) => {
554
+ await cmdAttach(taskId, opts);
555
+ });
556
+ }