@tyvm/knowhow 0.0.146 → 0.0.148

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 (271) hide show
  1. package/autodoc/cli-reference.md +36 -7
  2. package/bin/knowhow.js +8 -6
  3. package/package.json +1 -1
  4. package/src/agents/base/base.ts +51 -23
  5. package/src/agents/historyRollback.ts +36 -0
  6. package/src/agents/tools/execCommand.ts +102 -320
  7. package/src/agents/tools/list.ts +2 -2
  8. package/src/agents/tools/startAgentTask.ts +91 -34
  9. package/src/ai.ts +4 -1
  10. package/src/auth/browserLogin.ts +9 -3
  11. package/src/auth/keyAuth.ts +115 -0
  12. package/src/auth/keyManager.ts +166 -0
  13. package/src/chat/CliChatService.ts +23 -2
  14. package/src/chat/modules/AgentModule.ts +114 -33
  15. package/src/chat/modules/AskModule.ts +2 -0
  16. package/src/chat/modules/BaseChatModule.ts +0 -1
  17. package/src/chat/modules/CustomCommandsModule.ts +2 -0
  18. package/src/chat/modules/InternalChatModule.ts +7 -0
  19. package/src/chat/modules/RemoteSyncModule.ts +4 -0
  20. package/src/chat/modules/RendererModule.ts +2 -0
  21. package/src/chat/modules/SearchModule.ts +2 -0
  22. package/src/chat/modules/SessionsModule.ts +103 -34
  23. package/src/chat/modules/SetupModule.ts +2 -0
  24. package/src/chat/modules/ShellCommandModule.ts +2 -0
  25. package/src/chat/modules/SystemModule.ts +2 -0
  26. package/src/chat/modules/ToolCommandModule.ts +102 -0
  27. package/src/chat/modules/index.ts +1 -0
  28. package/src/chat/types.ts +3 -1
  29. package/src/cli.ts +10 -3
  30. package/src/clients/http.ts +67 -7
  31. package/src/clients/index.ts +4 -0
  32. package/src/commands/agent.ts +1 -38
  33. package/src/commands/agents.ts +455 -60
  34. package/src/commands/misc.ts +3 -2
  35. package/src/commands/processes.ts +138 -0
  36. package/src/commands/services.ts +23 -1
  37. package/src/index.ts +1 -0
  38. package/src/login.ts +89 -28
  39. package/src/processes/ProcessManager.ts +95 -0
  40. package/src/processes/managedProcessShim.ts +115 -0
  41. package/src/services/AgentService.ts +2 -0
  42. package/src/services/AgentSyncFs.ts +77 -1
  43. package/src/services/AgentSyncKnowhowWeb.ts +22 -4
  44. package/src/services/BehaviorsService.ts +2 -0
  45. package/src/services/DockerService.ts +2 -0
  46. package/src/services/EmbeddingService.ts +2 -0
  47. package/src/services/EmbeddingsService.ts +3 -0
  48. package/src/services/ExtensionsService.ts +25 -0
  49. package/src/services/KnowhowClient.ts +68 -7
  50. package/src/services/LazyToolsService.ts +24 -20
  51. package/src/services/Mcp.ts +2 -0
  52. package/src/services/McpServer.ts +2 -0
  53. package/src/services/MessageProcessor.ts +2 -0
  54. package/src/services/MinimalToolsService.ts +2 -0
  55. package/src/services/SessionManager.ts +5 -0
  56. package/src/services/SkillsService.ts +2 -0
  57. package/src/services/SyncedAgentWatcher.ts +2 -0
  58. package/src/services/SyncerService.ts +23 -8
  59. package/src/services/TaskRegistry.ts +2 -0
  60. package/src/services/Tools.ts +12 -0
  61. package/src/services/index.ts +6 -0
  62. package/src/services/modules/index.ts +10 -1
  63. package/src/services/modules/types.ts +4 -0
  64. package/src/types.ts +2 -0
  65. package/src/util/Trace.ts +1 -1
  66. package/src/utils/http.ts +18 -2
  67. package/tests/unit/agents/historyRollback.test.ts +53 -0
  68. package/tests/unit/auth/keyAuth.test.ts +102 -0
  69. package/tests/unit/auth/keyManager.test.ts +67 -0
  70. package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
  71. package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
  72. package/tests/unit/commands/agents.test.ts +111 -0
  73. package/tests/unit/login.test.ts +119 -0
  74. package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
  75. package/tests/unit/services/LazyToolsService.test.ts +53 -0
  76. package/tests/unit/services/SessionManager.test.ts +43 -0
  77. package/tests/unit/services/SyncerService.test.ts +45 -0
  78. package/tests/unit/util/Trace.test.ts +119 -0
  79. package/tests/unit/utils/http.test.ts +43 -0
  80. package/ts_build/package.json +1 -1
  81. package/ts_build/src/agents/base/base.d.ts +2 -1
  82. package/ts_build/src/agents/base/base.js +47 -14
  83. package/ts_build/src/agents/base/base.js.map +1 -1
  84. package/ts_build/src/agents/historyRollback.d.ts +2 -0
  85. package/ts_build/src/agents/historyRollback.js +22 -0
  86. package/ts_build/src/agents/historyRollback.js.map +1 -0
  87. package/ts_build/src/agents/tools/execCommand.d.ts +4 -1
  88. package/ts_build/src/agents/tools/execCommand.js +100 -223
  89. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  90. package/ts_build/src/agents/tools/list.js +2 -2
  91. package/ts_build/src/agents/tools/list.js.map +1 -1
  92. package/ts_build/src/agents/tools/startAgentTask.d.ts +16 -1
  93. package/ts_build/src/agents/tools/startAgentTask.js +71 -28
  94. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  95. package/ts_build/src/ai.js +2 -1
  96. package/ts_build/src/ai.js.map +1 -1
  97. package/ts_build/src/auth/browserLogin.d.ts +1 -0
  98. package/ts_build/src/auth/browserLogin.js +5 -2
  99. package/ts_build/src/auth/browserLogin.js.map +1 -1
  100. package/ts_build/src/auth/keyAuth.d.ts +5 -0
  101. package/ts_build/src/auth/keyAuth.js +79 -0
  102. package/ts_build/src/auth/keyAuth.js.map +1 -0
  103. package/ts_build/src/auth/keyManager.d.ts +14 -0
  104. package/ts_build/src/auth/keyManager.js +145 -0
  105. package/ts_build/src/auth/keyManager.js.map +1 -0
  106. package/ts_build/src/chat/CliChatService.d.ts +6 -0
  107. package/ts_build/src/chat/CliChatService.js +17 -2
  108. package/ts_build/src/chat/CliChatService.js.map +1 -1
  109. package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
  110. package/ts_build/src/chat/modules/AgentModule.js +83 -24
  111. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  112. package/ts_build/src/chat/modules/AskModule.js +12 -2
  113. package/ts_build/src/chat/modules/AskModule.js.map +1 -1
  114. package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
  115. package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
  116. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  117. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  118. package/ts_build/src/chat/modules/InternalChatModule.js +21 -2
  119. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  120. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
  121. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  122. package/ts_build/src/chat/modules/RendererModule.js +17 -2
  123. package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
  124. package/ts_build/src/chat/modules/SearchModule.js +12 -2
  125. package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
  126. package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
  127. package/ts_build/src/chat/modules/SessionsModule.js +113 -18
  128. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  129. package/ts_build/src/chat/modules/SetupModule.js +17 -2
  130. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  131. package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
  132. package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
  133. package/ts_build/src/chat/modules/SystemModule.js +12 -2
  134. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  135. package/ts_build/src/chat/modules/ToolCommandModule.d.ts +10 -0
  136. package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
  137. package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
  138. package/ts_build/src/chat/modules/index.d.ts +1 -0
  139. package/ts_build/src/chat/modules/index.js +3 -1
  140. package/ts_build/src/chat/modules/index.js.map +1 -1
  141. package/ts_build/src/chat/types.d.ts +3 -1
  142. package/ts_build/src/cli.js +7 -3
  143. package/ts_build/src/cli.js.map +1 -1
  144. package/ts_build/src/clients/http.d.ts +4 -0
  145. package/ts_build/src/clients/http.js +68 -6
  146. package/ts_build/src/clients/http.js.map +1 -1
  147. package/ts_build/src/clients/index.js +23 -4
  148. package/ts_build/src/clients/index.js.map +1 -1
  149. package/ts_build/src/commands/agent.js +1 -21
  150. package/ts_build/src/commands/agent.js.map +1 -1
  151. package/ts_build/src/commands/agents.d.ts +2 -1
  152. package/ts_build/src/commands/agents.js +216 -23
  153. package/ts_build/src/commands/agents.js.map +1 -1
  154. package/ts_build/src/commands/misc.js +3 -2
  155. package/ts_build/src/commands/misc.js.map +1 -1
  156. package/ts_build/src/commands/processes.d.ts +2 -0
  157. package/ts_build/src/commands/processes.js +213 -0
  158. package/ts_build/src/commands/processes.js.map +1 -0
  159. package/ts_build/src/commands/services.js +11 -1
  160. package/ts_build/src/commands/services.js.map +1 -1
  161. package/ts_build/src/index.d.ts +1 -0
  162. package/ts_build/src/index.js +1 -0
  163. package/ts_build/src/index.js.map +1 -1
  164. package/ts_build/src/login.d.ts +1 -1
  165. package/ts_build/src/login.js +53 -15
  166. package/ts_build/src/login.js.map +1 -1
  167. package/ts_build/src/processes/ProcessManager.d.ts +51 -0
  168. package/ts_build/src/processes/ProcessManager.js +160 -0
  169. package/ts_build/src/processes/ProcessManager.js.map +1 -0
  170. package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
  171. package/ts_build/src/processes/managedProcessShim.js +161 -0
  172. package/ts_build/src/processes/managedProcessShim.js.map +1 -0
  173. package/ts_build/src/services/AgentService.js +18 -2
  174. package/ts_build/src/services/AgentService.js.map +1 -1
  175. package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
  176. package/ts_build/src/services/AgentSyncFs.js +68 -16
  177. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  178. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
  179. package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
  180. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  181. package/ts_build/src/services/BehaviorsService.js +12 -2
  182. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  183. package/ts_build/src/services/DockerService.js +20 -8
  184. package/ts_build/src/services/DockerService.js.map +1 -1
  185. package/ts_build/src/services/EmbeddingService.js +16 -2
  186. package/ts_build/src/services/EmbeddingService.js.map +1 -1
  187. package/ts_build/src/services/EmbeddingsService.js +12 -2
  188. package/ts_build/src/services/EmbeddingsService.js.map +1 -1
  189. package/ts_build/src/services/ExtensionsService.d.ts +10 -0
  190. package/ts_build/src/services/ExtensionsService.js +17 -0
  191. package/ts_build/src/services/ExtensionsService.js.map +1 -0
  192. package/ts_build/src/services/KnowhowClient.d.ts +7 -2
  193. package/ts_build/src/services/KnowhowClient.js +108 -10
  194. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  195. package/ts_build/src/services/LazyToolsService.d.ts +2 -0
  196. package/ts_build/src/services/LazyToolsService.js +26 -9
  197. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  198. package/ts_build/src/services/Mcp.js +12 -2
  199. package/ts_build/src/services/Mcp.js.map +1 -1
  200. package/ts_build/src/services/McpServer.js +17 -2
  201. package/ts_build/src/services/McpServer.js.map +1 -1
  202. package/ts_build/src/services/MessageProcessor.js +16 -2
  203. package/ts_build/src/services/MessageProcessor.js.map +1 -1
  204. package/ts_build/src/services/MinimalToolsService.js +16 -2
  205. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  206. package/ts_build/src/services/SessionManager.js +19 -2
  207. package/ts_build/src/services/SessionManager.js.map +1 -1
  208. package/ts_build/src/services/SkillsService.js +16 -2
  209. package/ts_build/src/services/SkillsService.js.map +1 -1
  210. package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
  211. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  212. package/ts_build/src/services/SyncerService.d.ts +1 -0
  213. package/ts_build/src/services/SyncerService.js +27 -5
  214. package/ts_build/src/services/SyncerService.js.map +1 -1
  215. package/ts_build/src/services/TaskRegistry.js +12 -2
  216. package/ts_build/src/services/TaskRegistry.js.map +1 -1
  217. package/ts_build/src/services/Tools.d.ts +1 -0
  218. package/ts_build/src/services/Tools.js +19 -2
  219. package/ts_build/src/services/Tools.js.map +1 -1
  220. package/ts_build/src/services/index.d.ts +5 -0
  221. package/ts_build/src/services/index.js +7 -2
  222. package/ts_build/src/services/index.js.map +1 -1
  223. package/ts_build/src/services/modules/index.d.ts +4 -1
  224. package/ts_build/src/services/modules/index.js +6 -1
  225. package/ts_build/src/services/modules/index.js.map +1 -1
  226. package/ts_build/src/services/modules/types.d.ts +3 -0
  227. package/ts_build/src/types.d.ts +1 -0
  228. package/ts_build/src/types.js.map +1 -1
  229. package/ts_build/src/util/Trace.d.ts +1 -3
  230. package/ts_build/src/utils/http.d.ts +6 -2
  231. package/ts_build/src/utils/http.js +8 -2
  232. package/ts_build/src/utils/http.js.map +1 -1
  233. package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
  234. package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
  235. package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
  236. package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
  237. package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
  238. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
  239. package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
  240. package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
  241. package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
  242. package/ts_build/tests/unit/chat/ToolCommandModule.test.d.ts +1 -0
  243. package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
  244. package/ts_build/tests/unit/chat/ToolCommandModule.test.js.map +1 -0
  245. package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
  246. package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
  247. package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
  248. package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
  249. package/ts_build/tests/unit/commands/agents.test.js +119 -0
  250. package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
  251. package/ts_build/tests/unit/login.test.d.ts +1 -0
  252. package/ts_build/tests/unit/login.test.js +97 -0
  253. package/ts_build/tests/unit/login.test.js.map +1 -0
  254. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
  255. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
  256. package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
  257. package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
  258. package/ts_build/tests/unit/services/LazyToolsService.test.js +47 -0
  259. package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
  260. package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
  261. package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
  262. package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
  263. package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
  264. package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
  265. package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
  266. package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
  267. package/ts_build/tests/unit/util/Trace.test.js +107 -0
  268. package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
  269. package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
  270. package/ts_build/tests/unit/utils/http.test.js +69 -0
  271. package/ts_build/tests/unit/utils/http.test.js.map +1 -0
@@ -8,6 +8,8 @@ export interface StartAgentTaskParams {
8
8
  syncFs?: boolean;
9
9
  taskId?: string;
10
10
  resume?: boolean;
11
+ forkTaskId?: string;
12
+ rollback?: number;
11
13
  prompt: string;
12
14
  /**
13
15
  * Push this agent's work to a remote Knowhow task identified by taskId.
@@ -32,6 +34,20 @@ export interface StartAgentTaskParams {
32
34
  _ctx?: { caller?: any; taskId?: string; [key: string]: any };
33
35
  }
34
36
 
37
+ export interface StartAgentTaskResult {
38
+ success: boolean;
39
+ status: "started" | "completed" | "failed";
40
+ taskId: string;
41
+ pid?: number;
42
+ syncFs: boolean;
43
+ syncReady: boolean;
44
+ agentDir?: string;
45
+ logPath: string;
46
+ parentTaskId?: string;
47
+ exitCode?: number | null;
48
+ error?: string;
49
+ }
50
+
35
51
  const PROCESSES_DIR = path.join(process.cwd(), ".knowhow", "processes");
36
52
  const AGENTS_DIR = path.join(process.cwd(), ".knowhow", "processes", "agents");
37
53
 
@@ -98,12 +114,14 @@ function subagentCapabilitiesNote(): string {
98
114
  * .knowhow/processes/agents/{taskId}/input.txt
99
115
  * The agent will pick up the new content and process it as a new message.
100
116
  */
101
- export async function startAgentTask(params: StartAgentTaskParams): Promise<string> {
117
+ export async function startAgentTask(params: StartAgentTaskParams): Promise<StartAgentTaskResult> {
102
118
  const {
103
119
  messageId,
104
120
  prompt,
105
121
  taskId: providedTaskId,
106
122
  resume,
123
+ forkTaskId,
124
+ rollback = 0,
107
125
  syncFs,
108
126
  syncRemote,
109
127
  provider,
@@ -118,6 +136,18 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
118
136
  if (!prompt) {
119
137
  throw new Error("prompt is required to create a chat task");
120
138
  }
139
+ if (resume && forkTaskId) {
140
+ throw new Error("resume and forkTaskId are mutually exclusive");
141
+ }
142
+ if (resume && !providedTaskId) {
143
+ throw new Error("taskId is required when resuming");
144
+ }
145
+ if (!Number.isInteger(rollback) || rollback < 0) {
146
+ throw new Error("rollback must be a non-negative integer");
147
+ }
148
+ if (rollback > 0 && !resume && !forkTaskId) {
149
+ throw new Error("rollback requires resume or forkTaskId");
150
+ }
121
151
 
122
152
  // Default filesystem synchronization ON unless the caller explicitly opts out
123
153
  // (syncFs: false) or is using a messageId-based web sync. This ensures spawned
@@ -134,7 +164,14 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
134
164
  explicitParentTaskId ?? _ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId;
135
165
 
136
166
  // Build args array (no shell escaping needed - args are passed directly)
137
- const args: string[] = ["agent"];
167
+ const isHistoryRun = !!resume || !!forkTaskId;
168
+ const args: string[] = resume
169
+ ? ["agents", "resume", providedTaskId!]
170
+ : forkTaskId
171
+ ? ["agents", "fork", forkTaskId]
172
+ : ["agent"];
173
+
174
+ if (forkTaskId) args.push("--task-id", taskId);
138
175
 
139
176
  if (messageId) {
140
177
  args.push("--message-id", messageId);
@@ -144,11 +181,11 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
144
181
  // When syncRemote is requested, pass it through so the spawned agent pushes
145
182
  // its work to the remote task identified by --task-id rather than staying
146
183
  // local-only.
147
- if (syncRemote) {
184
+ if (syncRemote && !isHistoryRun) {
148
185
  args.push("--sync-remote");
149
186
  }
150
187
 
151
- if (useSyncFs || providedTaskId) {
188
+ if (!isHistoryRun && (useSyncFs || providedTaskId)) {
152
189
  // Pass --task-id whenever we have a known taskId (syncFs or explicit taskId)
153
190
  args.push("--task-id", taskId);
154
191
  }
@@ -165,20 +202,19 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
165
202
  args.push("--agent-name", agentName);
166
203
  }
167
204
 
168
- if (maxTimeLimit !== undefined) {
205
+ if (maxTimeLimit !== undefined && !isHistoryRun) {
169
206
  args.push("--max-time-limit", String(maxTimeLimit));
170
207
  }
171
208
 
172
- if (maxSpendLimit !== undefined) {
209
+ if (maxSpendLimit !== undefined && !isHistoryRun) {
173
210
  args.push("--max-spend-limit", String(maxSpendLimit));
174
211
  }
175
- if (parentTaskId) {
212
+ if (parentTaskId && !isHistoryRun) {
176
213
  // Tell the child who spawned it so it can report back to the parent.
177
214
  args.push("--parent-task-id", parentTaskId);
178
215
  }
179
- if (resume) {
180
- // --resume is a boolean flag; task ID is already passed via --task-id above
181
- args.push("--resume");
216
+ if (rollback > 0) {
217
+ args.push("--rollback", String(rollback));
182
218
  }
183
219
 
184
220
  const timeoutMs = maxTimeLimit ? maxTimeLimit * 60 * 1000 : 60 * 60 * 1000;
@@ -213,43 +249,54 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
213
249
  child.stdin!.write(promptWithCaps, "utf8");
214
250
  child.stdin!.end();
215
251
 
216
- return new Promise<string>((resolve) => {
252
+ return new Promise<StartAgentTaskResult>((resolve) => {
217
253
  let settled = false;
218
- const done = (msg: string) => {
254
+ let exited = false;
255
+ const done = (result: StartAgentTaskResult) => {
219
256
  if (settled) return;
220
257
  settled = true;
221
258
  try { fs.closeSync(fd); } catch {}
222
- resolve(msg);
259
+ resolve(result);
223
260
  };
224
261
 
225
262
  child.once("error", (e) => {
226
- done(`Failed to start agent: ${String(e)}\nLogs: ${logPath}`);
263
+ done({
264
+ success: false, status: "failed", taskId, pid, syncFs: useSyncFs,
265
+ syncReady: false, agentDir: useSyncFs ? agentTaskDir : undefined,
266
+ logPath, parentTaskId, error: String(e),
267
+ });
227
268
  });
228
269
 
229
- const syncFsNote = useSyncFs
230
- ? `\nTask ID: ${taskId}\nAgent dir: ${agentTaskDir}\n` +
231
- `To send agent messages, write to: ${agentTaskDir}/input.txt\n` +
232
- `To check status, read: ${agentTaskDir}/status.txt\n`
233
- : "";
234
-
235
270
  child.once("exit", (code) => {
236
- done(
237
- `Agent finished with exit code ${code}.\nLogs: ${logPath}\n` +
238
- syncFsNote
239
- );
271
+ exited = true;
272
+ done({
273
+ success: code === 0, status: code === 0 ? "completed" : "failed",
274
+ taskId, pid, syncFs: useSyncFs,
275
+ syncReady: useSyncFs && fs.existsSync(path.join(agentTaskDir, "metadata.json")),
276
+ agentDir: useSyncFs ? agentTaskDir : undefined, logPath, parentTaskId,
277
+ exitCode: code, ...(code === 0 ? {} : { error: `Agent exited with code ${code}` }),
278
+ });
240
279
  });
241
280
 
242
281
  if (!waitForCompletion) {
243
- // Give the agent 5 seconds to finish before detaching (fire-and-forget mode)
244
- const detachTime = 5 * 1000;
245
- setTimeout(() => {
246
- try { child.unref(); } catch {}
247
- done(
248
- `Agent started (pid=${pid}), running in background.\n` +
249
- `Logs: ${logPath}\n` +
250
- syncFsNote
251
- );
252
- }, detachTime);
282
+ // A spawn is not the same as an initialized fs-sync agent. Poll briefly
283
+ // so callers can distinguish those states without parsing log prose.
284
+ const deadline = Date.now() + (useSyncFs ? 5000 : 100);
285
+ const acknowledge = () => {
286
+ if (settled || exited) return;
287
+ const syncReady = useSyncFs && fs.existsSync(path.join(agentTaskDir, "metadata.json"));
288
+ if (!useSyncFs || syncReady || Date.now() >= deadline) {
289
+ try { child.unref(); } catch {}
290
+ done({
291
+ success: true, status: "started", taskId, pid, syncFs: useSyncFs,
292
+ syncReady, agentDir: useSyncFs ? agentTaskDir : undefined,
293
+ logPath, parentTaskId,
294
+ });
295
+ return;
296
+ }
297
+ setTimeout(acknowledge, 100);
298
+ };
299
+ setTimeout(acknowledge, 50);
253
300
  }
254
301
  });
255
302
  }
@@ -320,6 +367,16 @@ export const startAgentTaskDefinition: Tool = {
320
367
  description:
321
368
  "Resume a previously started task from where it left off. Must be used together with taskId which identifies the task to resume.",
322
369
  },
370
+ forkTaskId: {
371
+ type: "string",
372
+ description:
373
+ "Fork this existing task into a new task. The source remains unchanged; taskId optionally selects the new task ID.",
374
+ },
375
+ rollback: {
376
+ type: "number",
377
+ description:
378
+ "Discard this many newest agent interactions before resuming or forking. Must be a non-negative integer.",
379
+ },
323
380
  parentTaskId: {
324
381
  type: "string",
325
382
  description:
package/src/ai.ts CHANGED
@@ -65,13 +65,16 @@ export async function singlePrompt(
65
65
  // This ensures generate agent runs produce readable output and are observable
66
66
  // via `knowhow agents list/tail/status`, matching `knowhow agent --input` behaviour.
67
67
  const { startAgentTask } = await import("./agents/tools/startAgentTask");
68
- return startAgentTask({
68
+ const result = await startAgentTask({
69
69
  agentName: agent,
70
70
  prompt: userPrompt,
71
71
  model: model || undefined,
72
72
  waitForCompletion: true,
73
73
  ...(agentOptions ?? {}),
74
74
  });
75
+ // singlePrompt is a text API; keep generation pipelines compatible while
76
+ // the tool itself exposes a structured orchestration result.
77
+ return JSON.stringify(result);
75
78
  }
76
79
 
77
80
  if (!model) {
@@ -1,6 +1,7 @@
1
1
  import http from "../utils/http";
2
2
  import { exec } from "child_process";
3
3
  import { promisify } from "util";
4
+ import crypto from "crypto";
4
5
  import * as os from "os";
5
6
  import * as fs from "fs";
6
7
  import * as path from "path";
@@ -28,6 +29,8 @@ interface RetrieveTokenResponse {
28
29
 
29
30
  export class BrowserLoginService {
30
31
  private baseUrl: string;
32
+ /** PKCE verifier — generated once per login attempt, never sent in a URL */
33
+ private verifier: string = crypto.randomBytes(32).toString("hex");
31
34
 
32
35
  constructor(baseUrl: string = KNOWHOW_API_URL, private orgId?: string) {
33
36
  if (!baseUrl) {
@@ -100,9 +103,10 @@ export class BrowserLoginService {
100
103
  spinner.stop();
101
104
  spinner.start("Authentication successful! Retrieving token");
102
105
 
103
- // Step 4: Retrieve JWT token
106
+ // Step 4: Redeem session using PKCE verifier (never sent in URL)
104
107
  const tokenResponse = await http.post(
105
- `${this.baseUrl}/api/cli-login/session/${sessionData.sessionId}/token`
108
+ `${this.baseUrl}/api/cli-login/session/${sessionData.sessionId}/redeem`,
109
+ { verifier: this.verifier }
106
110
  );
107
111
 
108
112
  const tokenData = tokenResponse.data as RetrieveTokenResponse;
@@ -170,9 +174,11 @@ export class BrowserLoginService {
170
174
  */
171
175
  private async createSession(): Promise<CreateSessionResponse> {
172
176
  try {
177
+ // Compute the PKCE challenge (sha256 of verifier) to store server-side
178
+ const challenge = crypto.createHash("sha256").update(this.verifier, "utf8").digest("hex");
173
179
  const response = await http.post<CreateSessionResponse>(
174
180
  `${this.baseUrl}/api/cli-login/session`,
175
- {},
181
+ { challenge },
176
182
  { headers: { "User-Agent": getCliUserAgent() } }
177
183
  );
178
184
  return response.data;
@@ -0,0 +1,115 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import http from "../utils/http";
5
+ import { getCliUserAgent } from "./browserLogin";
6
+ import { keyPairExists, loadKeyPair, signMessage } from "./keyManager";
7
+
8
+ const JWT_FILE_PATH = path.join(process.cwd(), ".knowhow", ".jwt");
9
+ const DEFAULT_API_URL = process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
10
+
11
+ /**
12
+ * This is intentionally based only on the selected identity. Registration is
13
+ * server-side and scoped to its environment/user/org; a local `.registered`
14
+ * marker cannot safely represent that state. An unknown or revoked key is
15
+ * cheaply rejected by the selected API and login then falls back to PKCE.
16
+ */
17
+ export function hasKeyPair(privateKeyPath?: string): boolean {
18
+ return keyPairExists(privateKeyPath);
19
+ }
20
+
21
+ /** Exchange a registered Ed25519 identity for a new short-lived JWT. */
22
+ export async function exchangePublicKeyJwt(
23
+ orgId: string,
24
+ apiUrl: string = DEFAULT_API_URL,
25
+ privateKeyPath?: string
26
+ ): Promise<string> {
27
+ if (!keyPairExists(privateKeyPath)) {
28
+ throw new Error("CLI identity is unavailable");
29
+ }
30
+
31
+ const keyPair = loadKeyPair(privateKeyPath);
32
+ const headers = { "User-Agent": getCliUserAgent() };
33
+ const challengeResponse = await http.post<{
34
+ challengeId: string;
35
+ message: string;
36
+ expiresAt: string;
37
+ }>(
38
+ `${apiUrl}/api/public-keys/challenge`,
39
+ { publicKey: keyPair.publicKeyBase64, orgId },
40
+ { headers }
41
+ );
42
+ const { challengeId, message } = challengeResponse.data;
43
+ if (
44
+ typeof challengeId !== "string" ||
45
+ challengeId.length === 0 ||
46
+ challengeId.length > 128 ||
47
+ !/^[A-Za-z0-9_-]+$/.test(challengeId) ||
48
+ typeof message !== "string" ||
49
+ message.length === 0
50
+ ) {
51
+ throw new Error("Public-key challenge response is invalid");
52
+ }
53
+
54
+ const response = await http.post<{ jwt: string }>(
55
+ `${apiUrl}/api/public-keys/authenticate`,
56
+ { challengeId, signature: signMessage(message, privateKeyPath) },
57
+ { headers }
58
+ );
59
+ if (!response.data.jwt) {
60
+ throw new Error("Public-key authentication returned no JWT");
61
+ }
62
+ return response.data.jwt;
63
+ }
64
+
65
+ export async function authenticateWithKey(
66
+ orgId: string,
67
+ apiUrl: string = DEFAULT_API_URL,
68
+ privateKeyPath?: string
69
+ ): Promise<boolean> {
70
+ if (!keyPairExists(privateKeyPath)) return false;
71
+
72
+ try {
73
+ storeJwt(await exchangePublicKeyJwt(orgId, apiUrl, privateKeyPath));
74
+ return true;
75
+ } catch (error: unknown) {
76
+ // The identity is not registered for this environment/org (or was revoked).
77
+ if (http.isHttpError(error) && (error.status === 401 || error.status === 404)) {
78
+ return false;
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /** Register an Ed25519 CLI-auth key using the JWT obtained by browser PKCE. */
85
+ export async function registerPublicKey(
86
+ jwt: string,
87
+ publicKeyBase64: string,
88
+ apiUrl: string = DEFAULT_API_URL
89
+ ): Promise<void> {
90
+ const rawPublicKey = Buffer.from(publicKeyBase64, "base64");
91
+ if (rawPublicKey.length !== 32 || rawPublicKey.toString("base64") !== publicKeyBase64) {
92
+ throw new Error("CLI authentication requires a 32-byte Ed25519 public key");
93
+ }
94
+
95
+ await http.post(
96
+ `${apiUrl}/api/public-keys/register`,
97
+ {
98
+ publicKey: publicKeyBase64,
99
+ label: os.hostname(),
100
+ },
101
+ {
102
+ headers: {
103
+ Authorization: `Bearer ${jwt}`,
104
+ "User-Agent": getCliUserAgent(),
105
+ },
106
+ }
107
+ );
108
+ }
109
+
110
+ export function storeJwt(jwt: string): void {
111
+ const directory = path.dirname(JWT_FILE_PATH);
112
+ fs.mkdirSync(directory, { recursive: true });
113
+ fs.writeFileSync(JWT_FILE_PATH, jwt, { mode: 0o600 });
114
+ fs.chmodSync(JWT_FILE_PATH, 0o600);
115
+ }
@@ -0,0 +1,166 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+
6
+ const ED25519_KEY_TYPE = "ssh-ed25519";
7
+ const ED25519_RAW_KEY_LENGTH = 32;
8
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
9
+
10
+ export interface KeyPair {
11
+ privateKeyPath: string;
12
+ publicKeyPath: string;
13
+ /** Raw 32-byte Ed25519 key, base64 encoded for the authentication API. */
14
+ publicKeyBase64: string;
15
+ /** OpenSSH SHA-256 fingerprint (hashes the SSH wire-format public key). */
16
+ fingerprint: string;
17
+ }
18
+
19
+ export function getKeyDir(): string {
20
+ return path.join(os.homedir(), ".knowhow", "keys");
21
+ }
22
+
23
+ export function getDefaultPrivateKeyPath(): string {
24
+ return path.join(getKeyDir(), "id_ed25519");
25
+ }
26
+
27
+ export function getDefaultPublicKeyPath(): string {
28
+ return `${getDefaultPrivateKeyPath()}.pub`;
29
+ }
30
+
31
+ export function keyPairExists(privateKeyPath?: string): boolean {
32
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
33
+ return fs.existsSync(privatePath) && fs.existsSync(`${privatePath}.pub`);
34
+ }
35
+
36
+ /** Generate a CLI-only Ed25519 key and an OpenSSH-compatible public-key file. */
37
+ export function generateKeyPair(privateKeyPath?: string): KeyPair {
38
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
39
+ const publicPath = `${privatePath}.pub`;
40
+
41
+ fs.mkdirSync(path.dirname(privatePath), { recursive: true, mode: 0o700 });
42
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519", {
43
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
44
+ publicKeyEncoding: { type: "spki", format: "der" },
45
+ });
46
+ const rawPublicKey = extractRawPublicKey(publicKey);
47
+
48
+ // Refuse to overwrite either half of an identity. Partial identities should be
49
+ // repaired explicitly rather than silently replacing a user's key.
50
+ if (fs.existsSync(privatePath) || fs.existsSync(publicPath)) {
51
+ throw new Error(`Cannot generate identity: a key file already exists at ${privatePath} or ${publicPath}`);
52
+ }
53
+
54
+ fs.writeFileSync(privatePath, privateKey, { mode: 0o600, flag: "wx" });
55
+ try {
56
+ fs.writeFileSync(publicPath, formatOpenSshPublicKey(rawPublicKey), {
57
+ mode: 0o644,
58
+ flag: "wx",
59
+ });
60
+ } catch (error) {
61
+ fs.rmSync(privatePath, { force: true });
62
+ throw error;
63
+ }
64
+
65
+ return keyPairMetadata(privatePath, rawPublicKey);
66
+ }
67
+
68
+ /** Load and validate an Ed25519 identity. Legacy raw-base64 .pub files remain supported. */
69
+ export function loadKeyPair(privateKeyPath?: string): KeyPair {
70
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
71
+ const publicPath = `${privatePath}.pub`;
72
+ if (!fs.existsSync(privatePath)) throw new Error(`Private key not found at: ${privatePath}`);
73
+ if (!fs.existsSync(publicPath)) throw new Error(`Public key not found at: ${publicPath}`);
74
+
75
+ const privateKey = readEd25519PrivateKey(privatePath);
76
+ const publicKey = parsePublicKey(fs.readFileSync(publicPath, "utf8"));
77
+ const derivedPublicKey = extractRawPublicKey(
78
+ crypto.createPublicKey(privateKey).export({ type: "spki", format: "der" })
79
+ );
80
+ if (!crypto.timingSafeEqual(publicKey, derivedPublicKey)) {
81
+ throw new Error(`Public key does not match private key: ${publicPath}`);
82
+ }
83
+
84
+ return keyPairMetadata(privatePath, publicKey);
85
+ }
86
+
87
+ export function getOrCreatePublicKey(privateKeyPath?: string): KeyPair {
88
+ return keyPairExists(privateKeyPath)
89
+ ? loadKeyPair(privateKeyPath)
90
+ : generateKeyPair(privateKeyPath);
91
+ }
92
+
93
+ export function signMessage(message: string, privateKeyPath?: string): string {
94
+ const privatePath = privateKeyPath ?? getDefaultPrivateKeyPath();
95
+ if (!fs.existsSync(privatePath)) throw new Error(`Private key not found at: ${privatePath}`);
96
+ return crypto.sign(null, Buffer.from(message, "utf8"), readEd25519PrivateKey(privatePath)).toString("base64");
97
+ }
98
+
99
+ function readEd25519PrivateKey(privatePath: string): crypto.KeyObject {
100
+ let key: crypto.KeyObject;
101
+ try {
102
+ key = crypto.createPrivateKey(fs.readFileSync(privatePath));
103
+ } catch {
104
+ throw new Error(`Identity at ${privatePath} is not a supported PEM Ed25519 private key`);
105
+ }
106
+ if (key.asymmetricKeyType !== "ed25519") {
107
+ throw new Error(`Identity at ${privatePath} must be an Ed25519 private key`);
108
+ }
109
+ return key;
110
+ }
111
+
112
+ function extractRawPublicKey(spkiDer: Buffer): Buffer {
113
+ if (
114
+ spkiDer.length !== ED25519_SPKI_PREFIX.length + ED25519_RAW_KEY_LENGTH ||
115
+ !spkiDer.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
116
+ ) {
117
+ throw new Error("Invalid Ed25519 public key");
118
+ }
119
+ return spkiDer.subarray(ED25519_SPKI_PREFIX.length);
120
+ }
121
+
122
+ function sshPublicKeyBlob(rawPublicKey: Buffer): Buffer {
123
+ const type = Buffer.from(ED25519_KEY_TYPE, "ascii");
124
+ const typeLength = Buffer.alloc(4);
125
+ const keyLength = Buffer.alloc(4);
126
+ typeLength.writeUInt32BE(type.length);
127
+ keyLength.writeUInt32BE(rawPublicKey.length);
128
+ return Buffer.concat([typeLength, type, keyLength, rawPublicKey]);
129
+ }
130
+
131
+ function formatOpenSshPublicKey(rawPublicKey: Buffer): string {
132
+ return `${ED25519_KEY_TYPE} ${sshPublicKeyBlob(rawPublicKey).toString("base64")} knowhow-cli\n`;
133
+ }
134
+
135
+ function parsePublicKey(value: string): Buffer {
136
+ const trimmed = value.trim();
137
+ if (trimmed.startsWith(`${ED25519_KEY_TYPE} `)) {
138
+ const parts = trimmed.split(/\s+/);
139
+ const blob = Buffer.from(parts[1] ?? "", "base64");
140
+ const expected = sshPublicKeyBlob(blob.subarray(blob.length - ED25519_RAW_KEY_LENGTH));
141
+ if (blob.length !== expected.length || !blob.equals(expected)) {
142
+ throw new Error("Invalid OpenSSH Ed25519 public key");
143
+ }
144
+ return blob.subarray(blob.length - ED25519_RAW_KEY_LENGTH);
145
+ }
146
+
147
+ // Compatibility with key files written by early CLI builds.
148
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(trimmed)) {
149
+ throw new Error("Public key must be an OpenSSH Ed25519 key");
150
+ }
151
+ const raw = Buffer.from(trimmed, "base64");
152
+ if (raw.length !== ED25519_RAW_KEY_LENGTH || raw.toString("base64") !== trimmed) {
153
+ throw new Error("Ed25519 public key must contain exactly 32 bytes");
154
+ }
155
+ return raw;
156
+ }
157
+
158
+ function keyPairMetadata(privatePath: string, rawPublicKey: Buffer): KeyPair {
159
+ const hash = crypto.createHash("sha256").update(sshPublicKeyBlob(rawPublicKey)).digest("base64").replace(/=+$/, "");
160
+ return {
161
+ privateKeyPath: privatePath,
162
+ publicKeyPath: `${privatePath}.pub`,
163
+ publicKeyBase64: rawPublicKey.toString("base64"),
164
+ fingerprint: `SHA256:${hash}`,
165
+ };
166
+ }
@@ -20,6 +20,12 @@ import fs from "fs";
20
20
  import path from "path";
21
21
  import { services } from "../services";
22
22
  import { logger } from "../logger";
23
+ import { ModuleExtension } from "../services/ExtensionsService";
24
+
25
+ export interface ChatExtension extends ModuleExtension {
26
+ type: "chat";
27
+ commands: ChatCommand[];
28
+ }
23
29
 
24
30
  export class CliChatService implements ChatService {
25
31
  private context: ChatContext;
@@ -59,6 +65,7 @@ export class CliChatService implements ChatService {
59
65
  }
60
66
  },
61
67
  });
68
+ this.refreshExtensions();
62
69
  this.loadInputHistory();
63
70
 
64
71
  // Set up callback to add entries to inputHistory immediately when user presses Enter
@@ -151,7 +158,12 @@ export class CliChatService implements ChatService {
151
158
  }
152
159
 
153
160
  registerCommand(command: ChatCommand): void {
154
- this.commands.push(command);
161
+ const existing = this.commands.findIndex((item) => item.name === command.name);
162
+ if (existing >= 0) {
163
+ this.commands[existing] = command;
164
+ } else {
165
+ this.commands.push(command);
166
+ }
155
167
  }
156
168
 
157
169
  registerMode(mode: ChatMode): void {
@@ -163,9 +175,17 @@ export class CliChatService implements ChatService {
163
175
  }
164
176
 
165
177
  getCommands(): ChatCommand[] {
178
+ this.refreshExtensions();
166
179
  return this.commands;
167
180
  }
168
181
 
182
+ /** Re-read the registry so services created before module setup still work. */
183
+ refreshExtensions(): void {
184
+ for (const extension of services().Extensions.list<ChatExtension>("chat")) {
185
+ for (const command of extension.commands ?? []) this.registerCommand(command);
186
+ }
187
+ }
188
+
169
189
  /**
170
190
  * Get commands available in the current mode
171
191
  */
@@ -176,6 +196,7 @@ export class CliChatService implements ChatService {
176
196
  }
177
197
 
178
198
  getCommandsForActiveModes(): ChatCommand[] {
199
+ this.refreshExtensions();
179
200
  const activeModes = this.modes
180
201
  .filter((mode) => mode.active)
181
202
  .map((mode) => mode.name);
@@ -216,7 +237,7 @@ export class CliChatService implements ChatService {
216
237
  const command = availableCommands.find((cmd) => cmd.name === commandName);
217
238
 
218
239
  if (command) {
219
- const result = await command.handler(args);
240
+ const result = await command.handler(args, this);
220
241
 
221
242
  // If handler returns a CommandResult and it's not handled, pass to modules
222
243
  if (result && typeof result === "object" && "handled" in result) {