@tyvm/knowhow 0.0.83 → 0.0.84

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 (210) hide show
  1. package/package.json +4 -2
  2. package/src/agents/base/base.ts +72 -62
  3. package/src/agents/index.ts +30 -14
  4. package/src/agents/tools/startAgentTask.ts +3 -1
  5. package/src/chat/CliChatService.ts +20 -4
  6. package/src/chat/modules/AgentModule.ts +399 -357
  7. package/src/chat/modules/CustomCommandsModule.ts +0 -1
  8. package/src/chat/modules/InternalChatModule.ts +18 -2
  9. package/src/chat/modules/RendererModule.ts +109 -0
  10. package/src/chat/modules/SessionsModule.ts +854 -0
  11. package/src/chat/modules/SetupModule.ts +6 -8
  12. package/src/chat/modules/index.ts +1 -0
  13. package/src/chat/renderer/CompactRenderer.ts +209 -0
  14. package/src/chat/renderer/ConsoleRenderer.ts +141 -0
  15. package/src/chat/renderer/FancyRenderer.ts +421 -0
  16. package/src/chat/renderer/index.ts +5 -0
  17. package/src/chat/renderer/loadRenderer.ts +314 -0
  18. package/src/chat/renderer/messagesToRenderEvents.ts +96 -0
  19. package/src/chat/renderer/types.ts +88 -0
  20. package/src/chat/types.ts +5 -0
  21. package/src/chat.ts +69 -5
  22. package/src/cli.ts +24 -5
  23. package/src/config.ts +15 -0
  24. package/src/plugins/AgentsMdPlugin.ts +1 -1
  25. package/src/plugins/GitPlugin.ts +20 -20
  26. package/src/plugins/PluginBase.ts +11 -0
  27. package/src/plugins/SkillsPlugin.ts +150 -0
  28. package/src/plugins/asana.ts +4 -4
  29. package/src/plugins/embedding.ts +3 -5
  30. package/src/plugins/exec.ts +3 -3
  31. package/src/plugins/figma.ts +3 -7
  32. package/src/plugins/github.ts +18 -29
  33. package/src/plugins/jira.ts +2 -2
  34. package/src/plugins/language.ts +4 -4
  35. package/src/plugins/linear.ts +4 -4
  36. package/src/plugins/notion.ts +6 -8
  37. package/src/plugins/plugins.ts +29 -3
  38. package/src/plugins/url.ts +2 -2
  39. package/src/plugins/vim.ts +4 -3
  40. package/src/services/AgentService.ts +17 -0
  41. package/src/services/AgentSyncFs.ts +3 -0
  42. package/src/services/EventService.ts +168 -27
  43. package/src/services/KnowhowClient.ts +1 -0
  44. package/src/services/SessionManager.ts +51 -1
  45. package/src/services/SyncedAgentWatcher.ts +397 -0
  46. package/src/services/SyncerService.ts +147 -0
  47. package/src/services/index.ts +2 -0
  48. package/src/services/modules/index.ts +14 -3
  49. package/src/types.ts +25 -0
  50. package/src/worker.ts +80 -2
  51. package/src/workers/auth/PasskeySetup.ts +185 -0
  52. package/src/workers/auth/WorkerPasskeyAuth.ts +190 -0
  53. package/src/workers/auth/types.ts +58 -0
  54. package/src/workers/tools/getChallenge.ts +33 -0
  55. package/src/workers/tools/index.ts +8 -0
  56. package/src/workers/tools/lock.ts +31 -0
  57. package/src/workers/tools/unlock.ts +116 -0
  58. package/tests/unit/modules/moduleLoading.test.ts +226 -0
  59. package/tests/unit/plugins/pluginLoading.test.ts +151 -0
  60. package/ts_build/package.json +4 -2
  61. package/ts_build/src/agents/base/base.d.ts +4 -3
  62. package/ts_build/src/agents/base/base.js +54 -30
  63. package/ts_build/src/agents/base/base.js.map +1 -1
  64. package/ts_build/src/agents/index.d.ts +3 -0
  65. package/ts_build/src/agents/index.js +21 -11
  66. package/ts_build/src/agents/index.js.map +1 -1
  67. package/ts_build/src/agents/tools/startAgentTask.js +2 -1
  68. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  69. package/ts_build/src/chat/CliChatService.js +16 -5
  70. package/ts_build/src/chat/CliChatService.js.map +1 -1
  71. package/ts_build/src/chat/modules/AgentModule.d.ts +34 -17
  72. package/ts_build/src/chat/modules/AgentModule.js +248 -258
  73. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  74. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  75. package/ts_build/src/chat/modules/InternalChatModule.d.ts +3 -0
  76. package/ts_build/src/chat/modules/InternalChatModule.js +16 -1
  77. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  78. package/ts_build/src/chat/modules/RendererModule.d.ts +16 -0
  79. package/ts_build/src/chat/modules/RendererModule.js +76 -0
  80. package/ts_build/src/chat/modules/RendererModule.js.map +1 -0
  81. package/ts_build/src/chat/modules/SessionsModule.d.ts +33 -0
  82. package/ts_build/src/chat/modules/SessionsModule.js +582 -0
  83. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -0
  84. package/ts_build/src/chat/modules/SetupModule.d.ts +3 -3
  85. package/ts_build/src/chat/modules/SetupModule.js +4 -6
  86. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  87. package/ts_build/src/chat/modules/index.d.ts +1 -0
  88. package/ts_build/src/chat/modules/index.js +3 -1
  89. package/ts_build/src/chat/modules/index.js.map +1 -1
  90. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +23 -0
  91. package/ts_build/src/chat/renderer/CompactRenderer.js +167 -0
  92. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -0
  93. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +22 -0
  94. package/ts_build/src/chat/renderer/ConsoleRenderer.js +110 -0
  95. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -0
  96. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +23 -0
  97. package/ts_build/src/chat/renderer/FancyRenderer.js +328 -0
  98. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -0
  99. package/ts_build/src/chat/renderer/index.d.ts +5 -0
  100. package/ts_build/src/chat/renderer/index.js +29 -0
  101. package/ts_build/src/chat/renderer/index.js.map +1 -0
  102. package/ts_build/src/chat/renderer/loadRenderer.d.ts +4 -0
  103. package/ts_build/src/chat/renderer/loadRenderer.js +246 -0
  104. package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -0
  105. package/ts_build/src/chat/renderer/messagesToRenderEvents.d.ts +15 -0
  106. package/ts_build/src/chat/renderer/messagesToRenderEvents.js +72 -0
  107. package/ts_build/src/chat/renderer/messagesToRenderEvents.js.map +1 -0
  108. package/ts_build/src/chat/renderer/types.d.ts +75 -0
  109. package/ts_build/src/chat/renderer/types.js +3 -0
  110. package/ts_build/src/chat/renderer/types.js.map +1 -0
  111. package/ts_build/src/chat/types.d.ts +5 -0
  112. package/ts_build/src/chat.js +46 -4
  113. package/ts_build/src/chat.js.map +1 -1
  114. package/ts_build/src/cli.js +18 -5
  115. package/ts_build/src/cli.js.map +1 -1
  116. package/ts_build/src/config.d.ts +1 -0
  117. package/ts_build/src/config.js +17 -1
  118. package/ts_build/src/config.js.map +1 -1
  119. package/ts_build/src/plugins/AgentsMdPlugin.js +1 -1
  120. package/ts_build/src/plugins/AgentsMdPlugin.js.map +1 -1
  121. package/ts_build/src/plugins/GitPlugin.js +20 -20
  122. package/ts_build/src/plugins/GitPlugin.js.map +1 -1
  123. package/ts_build/src/plugins/PluginBase.d.ts +1 -0
  124. package/ts_build/src/plugins/PluginBase.js +13 -0
  125. package/ts_build/src/plugins/PluginBase.js.map +1 -1
  126. package/ts_build/src/plugins/SkillsPlugin.d.ts +13 -0
  127. package/ts_build/src/plugins/SkillsPlugin.js +149 -0
  128. package/ts_build/src/plugins/SkillsPlugin.js.map +1 -0
  129. package/ts_build/src/plugins/asana.js +4 -4
  130. package/ts_build/src/plugins/asana.js.map +1 -1
  131. package/ts_build/src/plugins/embedding.js +3 -3
  132. package/ts_build/src/plugins/embedding.js.map +1 -1
  133. package/ts_build/src/plugins/exec.js +3 -3
  134. package/ts_build/src/plugins/exec.js.map +1 -1
  135. package/ts_build/src/plugins/figma.js +3 -3
  136. package/ts_build/src/plugins/figma.js.map +1 -1
  137. package/ts_build/src/plugins/github.js +18 -18
  138. package/ts_build/src/plugins/github.js.map +1 -1
  139. package/ts_build/src/plugins/jira.js +2 -2
  140. package/ts_build/src/plugins/jira.js.map +1 -1
  141. package/ts_build/src/plugins/language.js +4 -4
  142. package/ts_build/src/plugins/language.js.map +1 -1
  143. package/ts_build/src/plugins/linear.js +4 -4
  144. package/ts_build/src/plugins/linear.js.map +1 -1
  145. package/ts_build/src/plugins/notion.js +6 -6
  146. package/ts_build/src/plugins/notion.js.map +1 -1
  147. package/ts_build/src/plugins/plugins.d.ts +3 -0
  148. package/ts_build/src/plugins/plugins.js +18 -3
  149. package/ts_build/src/plugins/plugins.js.map +1 -1
  150. package/ts_build/src/plugins/url.js +2 -2
  151. package/ts_build/src/plugins/url.js.map +1 -1
  152. package/ts_build/src/plugins/vim.js +2 -2
  153. package/ts_build/src/plugins/vim.js.map +1 -1
  154. package/ts_build/src/services/AgentService.d.ts +3 -0
  155. package/ts_build/src/services/AgentService.js +7 -0
  156. package/ts_build/src/services/AgentService.js.map +1 -1
  157. package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
  158. package/ts_build/src/services/AgentSyncFs.js +2 -0
  159. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  160. package/ts_build/src/services/EventService.d.ts +25 -2
  161. package/ts_build/src/services/EventService.js +92 -14
  162. package/ts_build/src/services/EventService.js.map +1 -1
  163. package/ts_build/src/services/KnowhowClient.d.ts +1 -0
  164. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  165. package/ts_build/src/services/SessionManager.d.ts +6 -0
  166. package/ts_build/src/services/SessionManager.js +39 -1
  167. package/ts_build/src/services/SessionManager.js.map +1 -1
  168. package/ts_build/src/services/SyncedAgentWatcher.d.ts +101 -0
  169. package/ts_build/src/services/SyncedAgentWatcher.js +312 -0
  170. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -0
  171. package/ts_build/src/services/SyncerService.d.ts +30 -0
  172. package/ts_build/src/services/SyncerService.js +72 -0
  173. package/ts_build/src/services/SyncerService.js.map +1 -0
  174. package/ts_build/src/services/index.d.ts +2 -0
  175. package/ts_build/src/services/index.js +2 -0
  176. package/ts_build/src/services/index.js.map +1 -1
  177. package/ts_build/src/services/modules/index.js +10 -2
  178. package/ts_build/src/services/modules/index.js.map +1 -1
  179. package/ts_build/src/types.d.ts +19 -0
  180. package/ts_build/src/types.js.map +1 -1
  181. package/ts_build/src/worker.d.ts +2 -0
  182. package/ts_build/src/worker.js +59 -4
  183. package/ts_build/src/worker.js.map +1 -1
  184. package/ts_build/src/workers/auth/PasskeySetup.d.ts +10 -0
  185. package/ts_build/src/workers/auth/PasskeySetup.js +131 -0
  186. package/ts_build/src/workers/auth/PasskeySetup.js.map +1 -0
  187. package/ts_build/src/workers/auth/WorkerPasskeyAuth.d.ts +35 -0
  188. package/ts_build/src/workers/auth/WorkerPasskeyAuth.js +129 -0
  189. package/ts_build/src/workers/auth/WorkerPasskeyAuth.js.map +1 -0
  190. package/ts_build/src/workers/auth/types.d.ts +36 -0
  191. package/ts_build/src/workers/auth/types.js +3 -0
  192. package/ts_build/src/workers/auth/types.js.map +1 -0
  193. package/ts_build/src/workers/tools/getChallenge.d.ts +9 -0
  194. package/ts_build/src/workers/tools/getChallenge.js +27 -0
  195. package/ts_build/src/workers/tools/getChallenge.js.map +1 -0
  196. package/ts_build/src/workers/tools/index.d.ts +6 -0
  197. package/ts_build/src/workers/tools/index.js +10 -0
  198. package/ts_build/src/workers/tools/index.js.map +1 -1
  199. package/ts_build/src/workers/tools/lock.d.ts +9 -0
  200. package/ts_build/src/workers/tools/lock.js +27 -0
  201. package/ts_build/src/workers/tools/lock.js.map +1 -0
  202. package/ts_build/src/workers/tools/unlock.d.ts +18 -0
  203. package/ts_build/src/workers/tools/unlock.js +78 -0
  204. package/ts_build/src/workers/tools/unlock.js.map +1 -0
  205. package/ts_build/tests/unit/modules/moduleLoading.test.d.ts +1 -0
  206. package/ts_build/tests/unit/modules/moduleLoading.test.js +187 -0
  207. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -0
  208. package/ts_build/tests/unit/plugins/pluginLoading.test.d.ts +1 -0
  209. package/ts_build/tests/unit/plugins/pluginLoading.test.js +123 -0
  210. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -0
@@ -0,0 +1,854 @@
1
+ /**
2
+ * Sessions Chat Module - Handles session/attachment related commands
3
+ * Extracted from AgentModule to keep AgentModule focused on agent running logic.
4
+ *
5
+ * Command semantics:
6
+ * /attach <taskId> - Attach to a RUNNING session (in-memory, filesystem, or web).
7
+ * If the task is completed, the user is told to use /resume instead.
8
+ * /resume <taskId> - Resume a COMPLETED/saved session with optional additional context.
9
+ * /sessions - List sessions that can be attached to or resumed.
10
+ * /logs [N] - Show the last N messages from the currently attached agent.
11
+ */
12
+ import { BaseChatModule } from "./BaseChatModule";
13
+ import { ChatCommand, ChatMode, ChatContext } from "../types";
14
+ import { AgentModule } from "./AgentModule";
15
+ import {
16
+ FsSyncedAgentWatcher,
17
+ WebSyncedAgentWatcher,
18
+ WatcherBackedAgent,
19
+ } from "../../services/index";
20
+ import { TaskInfo, ChatSession } from "../types";
21
+ import { agents } from "../../agents";
22
+ import { KnowhowSimpleClient } from "../../services/KnowhowClient";
23
+ import { messagesToRenderEvents } from "../renderer/messagesToRenderEvents";
24
+ import { Marked } from "../../utils/index";
25
+ import * as fs from "fs";
26
+ import * as path from "path";
27
+
28
+ export class SessionsModule extends BaseChatModule {
29
+ name = "sessions";
30
+ description = "Session and attachment management";
31
+
32
+ private agentModule: AgentModule;
33
+
34
+ constructor(agentModule: AgentModule) {
35
+ super();
36
+ this.agentModule = agentModule;
37
+ }
38
+
39
+ getCommands(): ChatCommand[] {
40
+ return [
41
+ {
42
+ name: "attach",
43
+ description:
44
+ "Attach to a RUNNING session. Use --completed to also see completed sessions.",
45
+ handler: this.handleAttachCommand.bind(this),
46
+ },
47
+ {
48
+ name: "resume",
49
+ description:
50
+ "Resume a completed/saved session with optional additional context",
51
+ handler: this.handleResumeCommand.bind(this),
52
+ },
53
+ {
54
+ name: "sessions",
55
+ description:
56
+ "List running sessions. Use --completed to also show completed/saved sessions.",
57
+ handler: this.handleSessionsCommand.bind(this),
58
+ },
59
+ {
60
+ name: "logs",
61
+ description: "Show recent messages from attached agent [N=20]",
62
+ handler: this.handleLogsCommand.bind(this),
63
+ modes: ["agent:attached"],
64
+ },
65
+ ];
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+ // /logs [N]
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+
72
+ async handleLogsCommand(args: string[]): Promise<void> {
73
+ const count = parseInt(args[0] || "20", 10) || 20;
74
+ const activeSyncedWatcher = this.agentModule.getActiveSyncedWatcher();
75
+ const renderer = this.agentModule.getRenderer();
76
+ const taskRegistry = this.agentModule.getTaskRegistry();
77
+
78
+ try {
79
+ // Prefer the synced watcher (fs or web attach)
80
+ if (activeSyncedWatcher) {
81
+ const threads = await activeSyncedWatcher.getThreads();
82
+ const lastThread = threads[threads.length - 1] || [];
83
+ const events = messagesToRenderEvents(
84
+ lastThread,
85
+ activeSyncedWatcher.taskId,
86
+ activeSyncedWatcher.agentName
87
+ );
88
+ renderer.logMessages(events, count);
89
+ return;
90
+ }
91
+
92
+ // Fall back to in-process task
93
+ const activeTaskId = renderer.getActiveTaskId();
94
+ if (activeTaskId && taskRegistry.has(activeTaskId)) {
95
+ const taskInfo = taskRegistry.get(activeTaskId);
96
+ const agent = taskInfo?.agent;
97
+ if (agent) {
98
+ const threads = agent.getThreads();
99
+ const lastThread = threads[threads.length - 1] || [];
100
+ const events = messagesToRenderEvents(
101
+ lastThread,
102
+ activeTaskId,
103
+ agent.name
104
+ );
105
+ renderer.logMessages(events, count);
106
+ return;
107
+ }
108
+ }
109
+
110
+ console.log(
111
+ "No active agent to show logs for. Use /attach <taskId> to attach to an agent first."
112
+ );
113
+ } catch (error) {
114
+ console.error("Error showing logs:", error);
115
+ }
116
+ }
117
+
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+ // Numbered selection helper
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Shows a prompt and accepts a number (1-based index) to select from a list of IDs.
124
+ * Returns the resolved ID, or undefined if cancelled.
125
+ */
126
+ private async selectByNumber(
127
+ prompt: string,
128
+ allIds: string[]
129
+ ): Promise<string | undefined> {
130
+ const numbers = allIds.map((_, i) => String(i + 1));
131
+ const input = await this.chatService?.getInput(prompt, numbers);
132
+ if (!input || !input.trim()) return undefined;
133
+ const idx = parseInt(input.trim(), 10);
134
+ if (isNaN(idx) || idx < 1 || idx > allIds.length) return undefined;
135
+ return allIds[idx - 1];
136
+ }
137
+
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ // /attach [taskId]
140
+ // Attaches to a RUNNING session only. Completed sessions → suggest /resume.
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+
143
+ async handleAttachCommand(args: string[]): Promise<void> {
144
+ const taskRegistry = this.agentModule.getTaskRegistry();
145
+ const sessionManager = this.agentModule.getSessionManager();
146
+ const showCompleted = args.includes("--completed");
147
+ const filteredArgs = args.filter((a) => a !== "--completed");
148
+
149
+ if (filteredArgs.length === 0) {
150
+ // Build list of running-only sessions for interactive selection
151
+ const runningTasks = taskRegistry.getAll();
152
+ const fsAgents = await this.getFsAgents(runningTasks);
153
+ // If --completed flag, also fetch saved/completed sessions
154
+ const savedSessions = showCompleted
155
+ ? sessionManager.listAvailableSessions()
156
+ : [];
157
+
158
+ if (
159
+ runningTasks.length === 0 &&
160
+ fsAgents.length === 0 &&
161
+ savedSessions.length === 0
162
+ ) {
163
+ console.log(
164
+ "No running sessions found to attach to.\n" +
165
+ "Use /attach --completed to also see completed sessions.\n" +
166
+ "Use /resume <taskId> to resume a completed session."
167
+ );
168
+ return;
169
+ }
170
+
171
+ if (showCompleted) {
172
+ this.logSessionsCompact(runningTasks, savedSessions, fsAgents);
173
+ } else {
174
+ this.printRunningTable(runningTasks, fsAgents);
175
+ }
176
+
177
+ const allIds = [
178
+ ...runningTasks.map((t) => t.taskId),
179
+ ...fsAgents.map((a) => a.taskId),
180
+ ...(showCompleted ? savedSessions.map((s) => s.sessionId) : []),
181
+ ];
182
+
183
+ const selectedId = await this.selectByNumber(
184
+ showCompleted
185
+ ? "Enter number to attach/resume (or press Enter to cancel): "
186
+ : "Enter number to attach to (or press Enter to cancel): ",
187
+ allIds
188
+ );
189
+
190
+ if (selectedId) {
191
+ const trimmed = selectedId;
192
+ const isCompleted =
193
+ showCompleted && savedSessions.some((s) => s.sessionId === trimmed);
194
+ if (isCompleted) {
195
+ await this.resumeById(trimmed);
196
+ } else {
197
+ await this.attachById(trimmed);
198
+ }
199
+ }
200
+ return;
201
+ }
202
+
203
+ const taskId = filteredArgs[0];
204
+ await this.attachById(taskId);
205
+ }
206
+
207
+ /**
208
+ * Core attach logic — only attaches to RUNNING sessions.
209
+ * Completed/saved sessions get a helpful message pointing to /resume.
210
+ */
211
+ private async attachById(id: string): Promise<void> {
212
+ const taskRegistry = this.agentModule.getTaskRegistry();
213
+ const sessionManager = this.agentModule.getSessionManager();
214
+
215
+ // ── Case 1: in-memory running task ──────────────────────────────────────
216
+ if (taskRegistry.has(id)) {
217
+ const taskInfo = taskRegistry.get(id)!;
218
+ const renderer = this.agentModule.getRenderer();
219
+ const context = this.chatService?.getContext();
220
+ const allAgents = agents();
221
+ const selectedAgent = allAgents[taskInfo.agentName];
222
+
223
+ if (context && selectedAgent) {
224
+ context.selectedAgent = selectedAgent;
225
+ context.agentMode = true;
226
+ context.currentAgent = taskInfo.agentName;
227
+ context.activeAgentTaskId = id;
228
+ context.currentModel = selectedAgent.getModel();
229
+ context.currentProvider = selectedAgent.getProvider();
230
+ }
231
+ this.agentModule.setActiveAgentTaskId(id);
232
+ renderer.setActiveTaskId(id);
233
+ if (this.chatService) this.chatService.setMode("agent:attached");
234
+
235
+ console.log(`🔄 Attached to running task: ${id}`);
236
+ console.log(` Agent : ${taskInfo.agentName}`);
237
+ console.log(` Task : ${taskInfo.initialInput}`);
238
+ console.log(` Status: ${taskInfo.status}`);
239
+ console.log(
240
+ ` Type /logs to see recent messages, or /detach to detach.`
241
+ );
242
+ return;
243
+ }
244
+
245
+ // ── Case 2: filesystem agent directory ──────────────────────────────────
246
+ const fsAgentPath = path.join(".knowhow", "processes", "agents", id);
247
+ if (fs.existsSync(fsAgentPath)) {
248
+ // Read status — only attach if running
249
+ const status = this.readFsAgentStatus(fsAgentPath);
250
+ if (status === "completed") {
251
+ console.log(
252
+ `⚠️ Task ${id} is completed.\n` +
253
+ ` Use /resume ${id} to resume it with additional context.`
254
+ );
255
+ return;
256
+ }
257
+ await this.attachToFsAgent(id);
258
+ return;
259
+ }
260
+
261
+ // ── Case 3: saved session (completed) ───────────────────────────────────
262
+ try {
263
+ const session = sessionManager.loadSession(id);
264
+ if (session) {
265
+ if (session.status === "completed") {
266
+ console.log(
267
+ `⚠️ Session ${id} is completed.\n` +
268
+ ` Use /resume ${id} to resume it with additional context.`
269
+ );
270
+ } else {
271
+ // Session exists but is not yet completed — treat as attach via fs watcher
272
+ // (the agent may be running in another process)
273
+ const fsPath = path.join(".knowhow", "processes", "agents", id);
274
+ if (fs.existsSync(fsPath)) {
275
+ await this.attachToFsAgent(id);
276
+ } else {
277
+ // Try web as last resort
278
+ await this.attachToWebAgent(id);
279
+ }
280
+ }
281
+ return;
282
+ }
283
+ } catch {
284
+ // session not on disk, continue
285
+ }
286
+
287
+ // ── Case 4: web task ────────────────────────────────────────────────────
288
+ try {
289
+ await this.attachToWebAgent(id);
290
+ return;
291
+ } catch {
292
+ // not found on web
293
+ }
294
+
295
+ console.log(
296
+ `Session/Task "${id}" not found among running tasks, filesystem agents, or web.\n` +
297
+ `Use /sessions to see all known sessions.`
298
+ );
299
+ }
300
+
301
+ // ─────────────────────────────────────────────────────────────────────────────
302
+ // /resume [taskId]
303
+ // Resumes a completed/saved session with optional additional context.
304
+ // ─────────────────────────────────────────────────────────────────────────────
305
+
306
+ async handleResumeCommand(args: string[]): Promise<void> {
307
+ const sessionManager = this.agentModule.getSessionManager();
308
+
309
+ if (args.length === 0) {
310
+ // Interactive: show saved sessions for selection
311
+ const savedSessions = sessionManager.listAvailableSessions();
312
+ if (savedSessions.length === 0) {
313
+ console.log("No saved sessions found to resume.");
314
+ return;
315
+ }
316
+
317
+ this.printSavedSessionsTable(savedSessions);
318
+
319
+ const allIds = savedSessions.map((s) => s.sessionId);
320
+ const selectedId = await this.selectByNumber(
321
+ "Enter number to resume (or press Enter to cancel): ",
322
+ allIds
323
+ );
324
+
325
+ if (selectedId) {
326
+ await this.resumeById(selectedId);
327
+ }
328
+ return;
329
+ }
330
+
331
+ await this.resumeById(args[0]);
332
+ }
333
+
334
+ private async resumeById(id: string): Promise<void> {
335
+ const sessionManager = this.agentModule.getSessionManager();
336
+
337
+ // Check saved sessions first
338
+ try {
339
+ const session = sessionManager.loadSession(id);
340
+ if (session) {
341
+ console.log(`\n📋 Session found: ${id}`);
342
+ console.log(` Agent : ${session.agentName}`);
343
+ console.log(` Task : ${session.initialInput}`);
344
+ console.log(` Status : ${session.status}`);
345
+
346
+ const additionalContext = await this.chatService?.getInput(
347
+ "Add any additional context for resuming this session (or press Enter to skip): "
348
+ );
349
+ await this.agentModule.resumeSession(
350
+ id,
351
+ additionalContext?.trim() || undefined
352
+ );
353
+ return;
354
+ }
355
+ } catch {
356
+ // not found as a saved session
357
+ }
358
+
359
+ // Check filesystem agent (may have metadata with threads)
360
+ const fsAgentPath = path.join(".knowhow", "processes", "agents", id);
361
+ if (fs.existsSync(fsAgentPath)) {
362
+ console.log(
363
+ `⚠️ Task ${id} exists in the filesystem but has no saved session.\n` +
364
+ ` Use /attach ${id} if it is still running.`
365
+ );
366
+ return;
367
+ }
368
+
369
+ console.log(
370
+ `Session "${id}" not found. Use /sessions to list available sessions.`
371
+ );
372
+ }
373
+
374
+ // ─────────────────────────────────────────────────────────────────────────────
375
+ // /sessions [--all] [--csv]
376
+ // Shows all sessions: running ones can be /attach'd, saved ones can be /resume'd
377
+ // ─────────────────────────────────────────────────────────────────────────────
378
+
379
+ async handleSessionsCommand(args: string[]): Promise<void> {
380
+ try {
381
+ const showCompleted =
382
+ args.includes("--completed") || args.includes("--all");
383
+ const showCsv = args.includes("--csv");
384
+
385
+ const taskRegistry = this.agentModule.getTaskRegistry();
386
+ const sessionManager = this.agentModule.getSessionManager();
387
+ const runningTasks = taskRegistry.getAll();
388
+ const fsAgents = await this.getFsAgents(runningTasks);
389
+ // Only include saved/completed sessions when --completed (or --all) is passed
390
+ const savedSessions = showCompleted
391
+ ? sessionManager.listAvailableSessions()
392
+ : [];
393
+
394
+ if (showCompleted) {
395
+ // Show running + completed together
396
+ await this.logSessionTable(true, showCsv, true);
397
+ } else {
398
+ // Only show running tasks + fs agents
399
+ if (runningTasks.length === 0 && fsAgents.length === 0) {
400
+ console.log(
401
+ "No running sessions. Use /sessions --completed to also see completed sessions."
402
+ );
403
+ } else if (showCsv) {
404
+ this.logSessionsCsv(runningTasks, [], fsAgents);
405
+ } else {
406
+ this.logSessionsCompact(runningTasks, [], fsAgents);
407
+ }
408
+ }
409
+
410
+ // Interactive selection — running → attach, completed → resume
411
+ const allIds = [
412
+ ...runningTasks.map((t) => t.taskId),
413
+ ...fsAgents.map((a) => a.taskId),
414
+ ...savedSessions.map((s) => s.sessionId),
415
+ ];
416
+
417
+ if (allIds.length > 0) {
418
+ const selectedId = await this.selectByNumber(
419
+ showCompleted
420
+ ? "Enter number to attach/resume (or press Enter to skip): "
421
+ : "Enter number to attach to (or press Enter to skip): ",
422
+ allIds
423
+ );
424
+
425
+ if (selectedId) {
426
+ const isRunning =
427
+ taskRegistry.has(selectedId) ||
428
+ fsAgents.some((a) => a.taskId === selectedId);
429
+
430
+ if (isRunning) {
431
+ await this.attachById(selectedId);
432
+ } else {
433
+ await this.resumeById(selectedId);
434
+ }
435
+ }
436
+ }
437
+ } catch (error) {
438
+ console.error("Error listing sessions and tasks:", error);
439
+ }
440
+ }
441
+
442
+ // ─────────────────────────────────────────────────────────────────────────────
443
+ // Table rendering helpers
444
+ // ─────────────────────────────────────────────────────────────────────────────
445
+
446
+ /**
447
+ * Main session table — shows running, saved, and fs agents.
448
+ */
449
+ async logSessionTable(
450
+ all: boolean = false,
451
+ csv: boolean = false,
452
+ includeFs: boolean = false
453
+ ) {
454
+ const taskRegistry = this.agentModule.getTaskRegistry();
455
+ const sessionManager = this.agentModule.getSessionManager();
456
+ const runningTasks = taskRegistry.getAll();
457
+ let savedSessions = sessionManager.listAvailableSessions();
458
+
459
+ if (!all) {
460
+ savedSessions = savedSessions.filter(
461
+ (s) => s.startTime >= this.agentModule.getProcessStartTime()
462
+ );
463
+ const filteredTasks = runningTasks.filter(
464
+ (t) => t.startTime >= this.agentModule.getProcessStartTime()
465
+ );
466
+
467
+ const fsAgents = includeFs ? await this.getFsAgents(runningTasks) : [];
468
+ if (
469
+ filteredTasks.length === 0 &&
470
+ savedSessions.length === 0 &&
471
+ fsAgents.length === 0
472
+ ) {
473
+ console.log(
474
+ "No sessions from this process run. Use --all to see all historical sessions."
475
+ );
476
+ return;
477
+ }
478
+
479
+ if (csv) {
480
+ this.logSessionsCsv(filteredTasks, savedSessions, fsAgents);
481
+ } else {
482
+ this.logSessionsCompact(filteredTasks, savedSessions, fsAgents);
483
+ }
484
+ return;
485
+ }
486
+
487
+ const fsAgents = includeFs
488
+ ? await this.getFsAgentsIncludingCompleted(runningTasks)
489
+ : [];
490
+ if (csv) {
491
+ this.logSessionsCsv(runningTasks, savedSessions, fsAgents);
492
+ } else {
493
+ this.logSessionsCompact(runningTasks, savedSessions, fsAgents);
494
+ }
495
+ }
496
+
497
+ /** Compact table of ONLY running tasks + fs agents (for /attach interactive) */
498
+ private printRunningTable(
499
+ runningTasks: TaskInfo[],
500
+ fsAgents: {
501
+ taskId: string;
502
+ agentName: string;
503
+ status: string;
504
+ totalCostUsd?: number;
505
+ }[]
506
+ ): void {
507
+ const rows = [
508
+ ...runningTasks.map((t) => ({
509
+ id: t.taskId,
510
+ agent: t.agentName,
511
+ status: t.status,
512
+ cost: `$${t.totalCost.toFixed(3)}`,
513
+ type: "running",
514
+ })),
515
+ ...fsAgents.map((a) => ({
516
+ id: a.taskId,
517
+ agent: a.agentName,
518
+ status: a.status,
519
+ cost: a.totalCostUsd != null ? `$${a.totalCostUsd.toFixed(3)}` : "n/a",
520
+ type: "fs",
521
+ })),
522
+ ];
523
+
524
+ console.log("\n🏃 Running sessions (attach-able):");
525
+ console.log("─".repeat(86));
526
+ console.log(
527
+ "#".padEnd(5) +
528
+ "taskId".padEnd(40) +
529
+ "agent".padEnd(14) +
530
+ "status".padEnd(12) +
531
+ "cost"
532
+ );
533
+ console.log("─".repeat(86));
534
+ for (const r of rows) {
535
+ const num = String(rows.indexOf(r) + 1).padEnd(5);
536
+ const shortId = r.id.length > 38 ? r.id.substring(0, 35) + "..." : r.id;
537
+ console.log(
538
+ num +
539
+ shortId.padEnd(40) +
540
+ r.agent.padEnd(14) +
541
+ r.status.padEnd(12) +
542
+ r.cost
543
+ );
544
+ }
545
+ console.log("─".repeat(86));
546
+ }
547
+
548
+ /** Compact table of ONLY saved sessions (for /resume interactive) */
549
+ private printSavedSessionsTable(savedSessions: ChatSession[]): void {
550
+ console.log("\n💾 Saved sessions (resumable):");
551
+ console.log("─".repeat(86));
552
+ console.log(
553
+ "#".padEnd(5) +
554
+ "taskId".padEnd(40) +
555
+ "agent".padEnd(14) +
556
+ "status".padEnd(12) +
557
+ "cost"
558
+ );
559
+ console.log("─".repeat(86));
560
+ for (let i = 0; i < savedSessions.length; i++) {
561
+ const s = savedSessions[i];
562
+ const num = String(i + 1).padEnd(5);
563
+ const shortId =
564
+ s.sessionId.length > 38
565
+ ? s.sessionId.substring(0, 35) + "..."
566
+ : s.sessionId;
567
+ const cost = s.totalCost ? `$${s.totalCost.toFixed(3)}` : "$0.000";
568
+ console.log(
569
+ num +
570
+ shortId.padEnd(40) +
571
+ s.agentName.padEnd(14) +
572
+ s.status.padEnd(12) +
573
+ cost
574
+ );
575
+ }
576
+ console.log("─".repeat(86));
577
+ }
578
+
579
+ /**
580
+ * Full compact list: running + saved + fs agents, with type labels.
581
+ */
582
+ private logSessionsCompact(
583
+ runningTasks: TaskInfo[],
584
+ savedSessions: ChatSession[],
585
+ fsAgents: {
586
+ taskId: string;
587
+ agentName: string;
588
+ status: string;
589
+ totalCostUsd?: number;
590
+ }[] = []
591
+ ): void {
592
+ const runningTaskIds = new Set(runningTasks.map((t) => t.taskId));
593
+ const savedIds = new Set(savedSessions.map((s) => s.sessionId));
594
+ const dedupedSaved = savedSessions.filter(
595
+ (s) => !runningTaskIds.has(s.sessionId)
596
+ );
597
+ const allKnownIds = new Set([...runningTaskIds, ...savedIds]);
598
+ const dedupedFs = fsAgents.filter((a) => !allKnownIds.has(a.taskId));
599
+
600
+ const rows = [
601
+ ...runningTasks.map((t) => ({
602
+ id: t.taskId,
603
+ agent: t.agentName,
604
+ status: t.status,
605
+ cost: `$${t.totalCost.toFixed(3)}`,
606
+ type: "running",
607
+ action: "/attach",
608
+ })),
609
+ ...dedupedFs.map((a) => ({
610
+ id: a.taskId,
611
+ agent: a.agentName,
612
+ status: a.status,
613
+ cost: a.totalCostUsd != null ? `$${a.totalCostUsd.toFixed(3)}` : "n/a",
614
+ type: "fs",
615
+ action: a.status === "completed" ? "/resume" : "/attach",
616
+ })),
617
+ ...dedupedSaved.map((s) => ({
618
+ id: s.sessionId,
619
+ agent: s.agentName,
620
+ status: s.status,
621
+ cost: s.totalCost ? `$${s.totalCost.toFixed(3)}` : "$0.000",
622
+ type: "saved",
623
+ action: "/resume",
624
+ })),
625
+ ];
626
+
627
+ if (rows.length === 0) {
628
+ console.log("No sessions found.");
629
+ return;
630
+ }
631
+
632
+ console.log("\n📋 Sessions:");
633
+ console.log("─".repeat(109));
634
+ console.log(
635
+ "#".padEnd(5) +
636
+ "taskId".padEnd(40) +
637
+ "agent".padEnd(14) +
638
+ "status".padEnd(12) +
639
+ "type".padEnd(10) +
640
+ "cost".padEnd(12) +
641
+ "action"
642
+ );
643
+ console.log("─".repeat(109));
644
+ for (let i = 0; i < rows.length; i++) {
645
+ const r = rows[i];
646
+ const num = String(i + 1).padEnd(5);
647
+ const shortId = r.id.length > 38 ? r.id.substring(0, 35) + "..." : r.id;
648
+ console.log(
649
+ num +
650
+ shortId.padEnd(40) +
651
+ r.agent.padEnd(14) +
652
+ r.status.padEnd(12) +
653
+ r.type.padEnd(10) +
654
+ r.cost.padEnd(12) +
655
+ r.action
656
+ );
657
+ }
658
+ console.log("─".repeat(109));
659
+ }
660
+
661
+ /**
662
+ * CSV output for sessions.
663
+ */
664
+ private logSessionsCsv(
665
+ runningTasks: TaskInfo[],
666
+ savedSessions: ChatSession[],
667
+ fsAgents: {
668
+ taskId: string;
669
+ agentName: string;
670
+ status: string;
671
+ totalCostUsd?: number;
672
+ }[] = []
673
+ ): void {
674
+ const lines = ["taskId,agent,status,type,cost,startTime,initialInput"];
675
+ const runningTaskIds = new Set(runningTasks.map((t) => t.taskId));
676
+ const dedupedSaved = savedSessions.filter(
677
+ (s) => !runningTaskIds.has(s.sessionId)
678
+ );
679
+
680
+ for (const t of runningTasks) {
681
+ const input = (t.initialInput || "")
682
+ .replace(/,/g, ";")
683
+ .replace(/\n/g, " ");
684
+ lines.push(
685
+ `${t.taskId},${t.agentName},${t.status},running,${
686
+ t.totalCost?.toFixed(3) || "0.000"
687
+ },${t.startTime},"${input}"`
688
+ );
689
+ }
690
+ for (const s of dedupedSaved) {
691
+ const input = (s.initialInput || "")
692
+ .replace(/,/g, ";")
693
+ .replace(/\n/g, " ");
694
+ lines.push(
695
+ `${s.sessionId},${s.agentName},${s.status},saved,${
696
+ s.totalCost?.toFixed(3) || "0.000"
697
+ },${s.startTime},"${input}"`
698
+ );
699
+ }
700
+ const allKnownIds = new Set([
701
+ ...runningTaskIds,
702
+ ...savedSessions.map((s) => s.sessionId),
703
+ ]);
704
+ for (const a of fsAgents) {
705
+ if (!allKnownIds.has(a.taskId)) {
706
+ lines.push(
707
+ `${a.taskId},${a.agentName},${a.status},fs,${
708
+ a.totalCostUsd != null ? a.totalCostUsd.toFixed(3) : "n/a"
709
+ },n/a,""`
710
+ );
711
+ }
712
+ }
713
+ console.log(lines.join("\n"));
714
+ }
715
+
716
+ // ─────────────────────────────────────────────────────────────────────────────
717
+ // Low-level attach helpers
718
+ // ─────────────────────────────────────────────────────────────────────────────
719
+
720
+ private async attachToFsAgent(taskId: string): Promise<void> {
721
+ const existingWatcher = this.agentModule.getActiveSyncedWatcher();
722
+ if (existingWatcher) {
723
+ existingWatcher.stopWatching();
724
+ this.agentModule.setActiveSyncedWatcher(undefined);
725
+ }
726
+
727
+ const watcher = new FsSyncedAgentWatcher();
728
+ await watcher.startWatching(taskId);
729
+ this.agentModule.setActiveSyncedWatcher(watcher);
730
+
731
+ // Wire rendering via AgentModule utility (handles cleanup on detach)
732
+ this.agentModule.wireAgentRendering(taskId, watcher.agentEvents, watcher.eventTypes, watcher.agentName);
733
+ watcher.agentEvents.once(watcher.eventTypes.done, (output) => {
734
+ console.log(Marked.parse(output));
735
+ });
736
+
737
+ const context = this.chatService?.getContext();
738
+ if (context) context.activeAgentTaskId = taskId;
739
+
740
+ console.log(`📁 Attached to filesystem agent: ${taskId}`);
741
+
742
+ // Enter interactive loop — this sets mode to "agent:attached" and blocks until detach/done/kill
743
+ const fsWatcherAgent = new WatcherBackedAgent(watcher);
744
+ await this.agentModule.attachedAgentChatLoop(taskId, fsWatcherAgent);
745
+ }
746
+
747
+ private async attachToWebAgent(taskId: string): Promise<void> {
748
+ const client = new KnowhowSimpleClient();
749
+ // Verify the task exists — throws if not found
750
+ const details = await client.getTaskDetails(taskId);
751
+
752
+ // Check if it's already completed on the web
753
+ const webStatus = details?.data?.status;
754
+ if (webStatus === "completed" || webStatus === "killed") {
755
+ console.log(
756
+ `⚠️ Web task ${taskId} has status: ${webStatus}.\n` +
757
+ ` Use /resume ${taskId} to resume it with additional context.`
758
+ );
759
+ return;
760
+ }
761
+
762
+ const existingWatcher = this.agentModule.getActiveSyncedWatcher();
763
+ if (existingWatcher) {
764
+ existingWatcher.stopWatching();
765
+ this.agentModule.setActiveSyncedWatcher(undefined);
766
+ }
767
+
768
+ const watcher = new WebSyncedAgentWatcher(client);
769
+ await watcher.startWatching(taskId);
770
+ this.agentModule.setActiveSyncedWatcher(watcher);
771
+
772
+ // Wire rendering via AgentModule utility (handles cleanup on detach)
773
+ this.agentModule.wireAgentRendering(taskId, watcher.agentEvents, watcher.eventTypes, watcher.agentName);
774
+
775
+ const context = this.chatService?.getContext();
776
+ if (context) context.activeAgentTaskId = taskId;
777
+
778
+ console.log(`🌐 Attached to web agent: ${taskId}`);
779
+
780
+ // Enter interactive loop — this sets mode to "agent:attached" and blocks until detach/done/kill
781
+ const webWatcherAgent = new WatcherBackedAgent(watcher);
782
+ await this.agentModule.attachedAgentChatLoop(taskId, webWatcherAgent);
783
+ }
784
+
785
+ // ─────────────────────────────────────────────────────────────────────────────
786
+ // Utility helpers
787
+ // ─────────────────────────────────────────────────────────────────────────────
788
+
789
+ private readFsAgentStatus(agentPath: string): string {
790
+ try {
791
+ const statusPath = path.join(agentPath, "status.txt");
792
+ if (fs.existsSync(statusPath)) {
793
+ return fs.readFileSync(statusPath, "utf8").trim();
794
+ }
795
+ // Fall back to metadata.json
796
+ const metaPath = path.join(agentPath, "metadata.json");
797
+ if (fs.existsSync(metaPath)) {
798
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
799
+ return meta.status || "unknown";
800
+ }
801
+ } catch {
802
+ // ignore
803
+ }
804
+ return "unknown";
805
+ }
806
+
807
+ private async getFsAgents(runningTasks: TaskInfo[]): Promise<
808
+ {
809
+ taskId: string;
810
+ agentName: string;
811
+ status: string;
812
+ totalCostUsd?: number;
813
+ }[]
814
+ > {
815
+ const sessionManager = this.agentModule.getSessionManager();
816
+ const registeredIds = new Set(runningTasks.map((t) => t.taskId));
817
+ return sessionManager.discoverFsAgents(registeredIds);
818
+ }
819
+
820
+ private async getFsAgentsIncludingCompleted(
821
+ runningTasks: TaskInfo[]
822
+ ): Promise<
823
+ {
824
+ taskId: string;
825
+ agentName: string;
826
+ status: string;
827
+ totalCostUsd?: number;
828
+ }[]
829
+ > {
830
+ const sessionManager = this.agentModule.getSessionManager();
831
+ const registeredIds = new Set(runningTasks.map((t) => t.taskId));
832
+ return sessionManager.discoverFsAgents(registeredIds, true);
833
+ }
834
+
835
+ // ─────────────────────────────────────────────────────────────────────────────
836
+ // Public helpers used by CLI (src/cli.ts)
837
+ // ─────────────────────────────────────────────────────────────────────────────
838
+
839
+ public async listAvailableSessions(): Promise<ChatSession[]> {
840
+ return this.agentModule.getSessionManager().listAvailableSessions();
841
+ }
842
+
843
+ public async listSessionsAndTasks(): Promise<{
844
+ runningTasks: TaskInfo[];
845
+ savedSessions: ChatSession[];
846
+ }> {
847
+ const taskRegistry = this.agentModule.getTaskRegistry();
848
+ const sessionManager = this.agentModule.getSessionManager();
849
+ return {
850
+ runningTasks: taskRegistry.getAll(),
851
+ savedSessions: sessionManager.listAvailableSessions(),
852
+ };
853
+ }
854
+ }