@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
@@ -3,6 +3,7 @@
3
3
  * Hides complexity of choosing and managing sync backends.
4
4
  */
5
5
  import { BaseAgent } from "../agents/base/base";
6
+ import { TraceAll } from "../util/Trace";
6
7
  import { AgentSyncFs } from "./AgentSyncFs";
7
8
  import { AgentSyncKnowhowWeb } from "./AgentSyncKnowhowWeb";
8
9
 
@@ -11,12 +12,13 @@ export interface SyncerOptions {
11
12
  prompt: string;
12
13
  /** If provided → use web sync (unless syncFs is set) */
13
14
  messageId?: string;
15
+ /** Existing remote Knowhow task UUID. Never inferred from the local taskId. */
16
+ remoteTaskId?: string;
14
17
  /** Force fs sync even if messageId is provided */
15
18
  syncFs?: boolean;
16
19
  /**
17
- * Push the agent's work to a remote Knowhow task. When set (and there is no
18
- * messageId), the `taskId` is treated as the remote task ID to attach to and
19
- * push updates to, rather than creating a new remote task.
20
+ * Push the agent's work to the existing `remoteTaskId`. The local taskId is
21
+ * always a filesystem identity and is never used as a remote identifier.
20
22
  */
21
23
  syncRemote?: boolean;
22
24
  /** Agent name to persist in metadata.json */
@@ -44,8 +46,9 @@ export interface AgentSyncer {
44
46
  * Decision logic:
45
47
  * - Always sets up AgentSyncFs (primary local syncer)
46
48
  * - If messageId is present AND syncFs is not forced → creates a new remote task via AgentSyncKnowhowWeb
47
- * - If syncRemote is set (and no messageId) → attaches to the remote task identified by taskId
49
+ * - If syncRemote and a valid remoteTaskId are set → attaches to that UUID
48
50
  */
51
+ @TraceAll()
49
52
  export class SyncerService implements AgentSyncer {
50
53
  private fsSync: AgentSyncFs;
51
54
  private webSync: AgentSyncKnowhowWeb;
@@ -75,14 +78,16 @@ export class SyncerService implements AgentSyncer {
75
78
 
76
79
  // Determine whether to ATTACH to an already-existing remote task and push
77
80
  // local work to it (no creation needed). This is the "--sync-remote" path:
78
- // the caller wants to push work to the remote task identified by taskId.
81
+ // the caller wants to push work to an explicitly identified remote task.
79
82
  // We only auto-attach when there is NO messageId (messageId always creates
80
83
  // a fresh remote task above).
81
84
  //
82
85
  // This is orthogonal to fs sync: we can push to the remote task AND keep
83
86
  // the local .knowhow/processes/agents/<id>/ files in sync at the same time.
84
87
  const shouldAttachExisting =
85
- !!options.syncRemote && !options.messageId && !!options.taskId;
88
+ !!options.syncRemote &&
89
+ !options.messageId &&
90
+ isUuid(options.remoteTaskId);
86
91
 
87
92
  // Always create fs sync task first
88
93
  console.log(
@@ -104,6 +109,7 @@ export class SyncerService implements AgentSyncer {
104
109
 
105
110
  if (knowhowTaskId) {
106
111
  this.useWebSync = true;
112
+ await this.fsSync.setRemoteIdentity(knowhowTaskId, options.messageId);
107
113
  this.createdTaskId = knowhowTaskId;
108
114
  console.log(`🌐 Web sync task created: ${knowhowTaskId}`);
109
115
  }
@@ -112,9 +118,10 @@ export class SyncerService implements AgentSyncer {
112
118
  // Attach live web sync to an existing remote task (push updates to it).
113
119
  if (!shouldUseWebSync && shouldAttachExisting) {
114
120
  this.useWebSync = true;
115
- this.createdTaskId = options.taskId;
121
+ await this.fsSync.setRemoteIdentity(options.remoteTaskId!);
122
+ this.createdTaskId = options.remoteTaskId;
116
123
  console.log(
117
- `🌐 Attaching web sync to existing remote task: ${options.taskId}`
124
+ `🌐 Attaching web sync to existing remote task: ${options.remoteTaskId}`
118
125
  );
119
126
  }
120
127
 
@@ -174,3 +181,11 @@ export class SyncerService implements AgentSyncer {
174
181
  return this.useWebSync ? this.createdTaskId : undefined;
175
182
  }
176
183
  }
184
+
185
+ /** Remote task IDs are UUIDs; local filesystem IDs are timestamped slugs. */
186
+ function isUuid(value: string | undefined): value is string {
187
+ return !!value &&
188
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
189
+ value
190
+ );
191
+ }
@@ -2,10 +2,12 @@
2
2
  * Task Registry Service - Manages active agent tasks
3
3
  */
4
4
  import { TaskInfo } from "../chat/types";
5
+ import { TraceAll } from "../util/Trace";
5
6
 
6
7
  /**
7
8
  * TaskRegistry manages the in-memory registry of active agent tasks
8
9
  */
10
+ @TraceAll()
9
11
  export class TaskRegistry {
10
12
  private tasks = new Map<string, TaskInfo>();
11
13
 
@@ -1,4 +1,5 @@
1
1
  import { ChatCompletionTool } from "openai/resources/chat";
2
+ import { TraceAll } from "../util/Trace";
2
3
  import { replaceEscapedNewLines, restoreEscapedNewLines } from "../utils";
3
4
  import { includedTools } from "../agents/tools/list";
4
5
  import { AgentService } from "./AgentService";
@@ -61,6 +62,7 @@ export interface ToolContext {
61
62
  * larger context bloat from having every tool defined in system prompt
62
63
  *
63
64
  */
65
+ @TraceAll()
64
66
  export class ToolsService {
65
67
  private context: ToolContext = {};
66
68
 
@@ -133,6 +135,16 @@ export class ToolsService {
133
135
  return this.tools.map((tool) => tool.function.name);
134
136
  }
135
137
 
138
+ /**
139
+ * Return every registered function implementation. Unlike getToolNames(),
140
+ * this is not a prompt-visibility list; lazy tool services may intentionally
141
+ * hide definitions from the model while keeping their functions callable by
142
+ * trusted programmatic consumers such as the script runtime.
143
+ */
144
+ getFunctionNames(): string[] {
145
+ return Object.keys(this.functions).filter((name) => typeof this.functions[name] === "function");
146
+ }
147
+
136
148
  getTool(name: string): Tool {
137
149
  return this.tools.find(
138
150
  (tool) =>
@@ -16,6 +16,7 @@ import { TaskRegistry } from "./TaskRegistry";
16
16
  import { MediaProcessorService } from "./MediaProcessorService";
17
17
  import { BehaviorsService } from "./BehaviorsService";
18
18
  import { RuntimeReloadService } from "./RuntimeReloadService";
19
+ import { ExtensionsService } from "./ExtensionsService";
19
20
 
20
21
  import { ConversionService } from "./conversion/ConversionService";
21
22
  import { TracingService } from "./TracingService";
@@ -45,12 +46,14 @@ export { TracingService } from "./TracingService";
45
46
  export type { TracerImpl, SpanHandle } from "./TracingService";
46
47
  export * from "./conversion";
47
48
  export * from "./RuntimeReloadService";
49
+ export * from "./ExtensionsService";
48
50
  export { BehaviorsService } from "./BehaviorsService";
49
51
 
50
52
  let Singletons = {} as {
51
53
  Tools: ToolsService;
52
54
  Events: EventService;
53
55
  Agents: AgentService;
56
+ Tracing: typeof TracingService;
54
57
  Embeddings: EmbeddingsService;
55
58
  Flags: FlagsService;
56
59
  Mcp: McpService;
@@ -63,6 +66,7 @@ let Singletons = {} as {
63
66
  Conversion: ConversionService;
64
67
  Behaviors: BehaviorsService;
65
68
  RuntimeReload: RuntimeReloadService;
69
+ Extensions: ExtensionsService;
66
70
  };
67
71
 
68
72
  export const services = (): typeof Singletons => {
@@ -83,6 +87,7 @@ export const services = (): typeof Singletons => {
83
87
  Singletons = {
84
88
  Agents,
85
89
  AwsS3: new S3Service(),
90
+ Tracing: TracingService,
86
91
  Clients,
87
92
  Docker: new DockerService(),
88
93
  Events,
@@ -96,6 +101,7 @@ export const services = (): typeof Singletons => {
96
101
  knowhowApiClient: new KnowhowSimpleClient(),
97
102
  Behaviors,
98
103
  RuntimeReload: new RuntimeReloadService(),
104
+ Extensions: new ExtensionsService(),
99
105
  };
100
106
 
101
107
  Singletons.Tools.setContext({
@@ -207,6 +207,11 @@ export class ModulesService {
207
207
  }
208
208
  this._loadedModules.push({ modulePath, resolvedPath, module: importedModule, params: initParams });
209
209
 
210
+ context.Extensions?.removeOwner(modulePath);
211
+ for (const extension of importedModule.extensions ?? []) {
212
+ context.Extensions?.register(modulePath, extension);
213
+ }
214
+
210
215
  // Only register tools/agents/plugins/clients if the relevant services
211
216
  // are available in context (they may not be during early CLI command registration)
212
217
  if (context.Agents) {
@@ -253,6 +258,7 @@ export class ModulesService {
253
258
  // Clear first so repeated shutdown/reload attempts cannot destroy twice.
254
259
  this._loadedModules = [];
255
260
  for (const { modulePath, resolvedPath, module: mod, params } of toDestroy) {
261
+ params.context?.Extensions?.removeOwner(modulePath);
256
262
  if (typeof mod.destroy === "function") {
257
263
  try {
258
264
  await mod.destroy(params);
@@ -284,9 +290,12 @@ export class ModulesService {
284
290
 
285
291
  // Load global-config modules resolving against the global install's
286
292
  // node_modules (~/.knowhow/node_modules) first.
293
+ // Pass a merged config so that keys defined in the global config (e.g.
294
+ // `tracing`) are visible to globally-declared modules during init().
295
+ // Local config keys take precedence so that local overrides still work.
287
296
  if (globalModules.length > 0) {
288
297
  await this.loadModulesFrom(
289
- { ...config, modules: globalModules },
298
+ { ...globalConfig, ...config, modules: globalModules },
290
299
  context,
291
300
  globalResolvePaths()
292
301
  );
@@ -15,6 +15,7 @@ import { EventService } from "../EventService";
15
15
  import { ConversionService } from "../conversion/ConversionService";
16
16
  import { BehaviorsService } from "../BehaviorsService";
17
17
  import { ComputerUseService } from "./computerUse";
18
+ import { ExtensionsService, ModuleExtension } from "../ExtensionsService";
18
19
 
19
20
  import { TracingService as TracingServiceType } from "../TracingService";
20
21
  /*
@@ -66,6 +67,7 @@ export interface ModuleContext {
66
67
  Program?: Command;
67
68
  Behaviors?: BehaviorsService;
68
69
  ComputerUse?: ComputerUseService;
70
+ Extensions?: ExtensionsService;
69
71
  [key: string]: any;
70
72
  Tracing?: typeof TracingServiceType;
71
73
  }
@@ -98,6 +100,8 @@ export interface KnowhowModule {
98
100
  */
99
101
  destroy?: (params: InitParams) => Promise<void>;
100
102
  commands: ModuleChatCommand[];
103
+ /** Optional capabilities consumed by core or third-party services. */
104
+ extensions?: ModuleExtension[];
101
105
  tools: ModuleTool[];
102
106
  agents: ModuleAgent[];
103
107
  plugins: ModulePlugin[];
package/src/types.ts CHANGED
@@ -75,6 +75,8 @@ export type Config = {
75
75
  promptsDir: string;
76
76
  lintCommands?: { [fileExtension: string]: string };
77
77
  orgId?: string;
78
+ /** Private Ed25519 identity selected with `knowhow login --identity`. */
79
+ cliIdentityPath?: string;
78
80
  syncRemote?: boolean;
79
81
  micCommand?: string;
80
82
  defaultMic?: string;
package/src/util/Trace.ts CHANGED
@@ -141,7 +141,7 @@ export interface TraceAllOptions {
141
141
  export function TraceAll(options: TraceAllOptions = {}) {
142
142
  const skip = new Set(options.skip ?? []);
143
143
 
144
- return function <T extends { new (...args: unknown[]): object }>(
144
+ return function <T extends abstract new (...args: unknown[]) => object>(
145
145
  constructor: T
146
146
  ): T {
147
147
  const className = constructor.name;
package/src/utils/http.ts CHANGED
@@ -21,6 +21,15 @@ export class HttpError extends Error {
21
21
  }
22
22
  }
23
23
 
24
+ /**
25
+ * Optional, non-enumerable hook carried by an authenticated headers object.
26
+ * The hook may replace an expired credential before one safe 401 retry.
27
+ */
28
+ export const HTTP_UNAUTHORIZED_HANDLER = Symbol("httpUnauthorizedHandler");
29
+ export type RefreshableHeaders = Record<string, string> & {
30
+ [HTTP_UNAUTHORIZED_HANDLER]?: () => Promise<void>;
31
+ };
32
+
24
33
  async function parseBody<T>(response: Response, responseType?: string): Promise<T> {
25
34
  if (responseType === "arraybuffer") {
26
35
  return (await response.arrayBuffer()) as unknown as T;
@@ -41,13 +50,14 @@ async function request<T = any>(
41
50
  url: string,
42
51
  options: {
43
52
  method?: string;
44
- headers?: Record<string, string>;
53
+ headers?: RefreshableHeaders;
45
54
  body?: any;
46
55
  responseType?: "json" | "arraybuffer" | "stream" | "text";
47
56
  params?: Record<string, any>;
48
57
  /** Timeout in milliseconds. Default: 30000 (30s). Use 0 to disable. */
49
58
  timeout?: number;
50
- } = {}
59
+ } = {},
60
+ mayRefresh = true
51
61
  ): Promise<HttpResponse<T>> {
52
62
  const { method = "GET", headers = {}, body, responseType, params, timeout = 30000 } = options;
53
63
 
@@ -102,6 +112,12 @@ async function request<T = any>(
102
112
  }
103
113
 
104
114
  if (!response.ok) {
115
+ const refresh = headers[HTTP_UNAUTHORIZED_HANDLER];
116
+ if (response.status === 401 && mayRefresh && refresh) {
117
+ await refresh();
118
+ return request<T>(url, options, false);
119
+ }
120
+
105
121
  const text = await response.text().catch(() => "");
106
122
  let parsedBody: any;
107
123
  try {
@@ -0,0 +1,53 @@
1
+ import { rollbackAgentInteractions } from "../../../src/agents/historyRollback";
2
+ import { Message } from "../../../src/clients/types";
3
+
4
+ const message = (role: Message["role"], content: string): Message => ({ role, content });
5
+
6
+ describe("rollbackAgentInteractions", () => {
7
+ const threads: Message[][] = [[
8
+ message("user", "request"),
9
+ message("assistant", "first"),
10
+ message("tool", "first result"),
11
+ message("assistant", "second"),
12
+ message("tool", "second result"),
13
+ ]];
14
+
15
+ it("clones history without changing it when rollback is zero", () => {
16
+ const result = rollbackAgentInteractions(threads, 0);
17
+ expect(result).toEqual(threads);
18
+ expect(result).not.toBe(threads);
19
+ expect(result[0]).not.toBe(threads[0]);
20
+ expect(result[0][0]).not.toBe(threads[0][0]);
21
+ });
22
+
23
+ it("removes the newest assistant and its following tool results", () => {
24
+ expect(rollbackAgentInteractions(threads, 1)).toEqual([threads[0].slice(0, 3)]);
25
+ });
26
+
27
+ it("can remove multiple interactions", () => {
28
+ expect(rollbackAgentInteractions(threads, 2)).toEqual([[threads[0][0]]]);
29
+ });
30
+
31
+ it("removes a trailing assistant without tools", () => {
32
+ const input = [[message("user", "request"), message("assistant", "tail")]];
33
+ expect(rollbackAgentInteractions(input, 1)).toEqual([[input[0][0]]]);
34
+ });
35
+
36
+ it.each([-1, 1.5])("rejects invalid rollback %p", (rollback) => {
37
+ expect(() => rollbackAgentInteractions(threads, rollback)).toThrow(
38
+ "rollback must be a non-negative integer"
39
+ );
40
+ });
41
+
42
+ it("rejects rollback beyond available interactions", () => {
43
+ expect(() => rollbackAgentInteractions(threads, 3)).toThrow(
44
+ "only has 2 assistant interaction(s)"
45
+ );
46
+ });
47
+
48
+ it("does not mutate input history", () => {
49
+ const before = JSON.stringify(threads);
50
+ rollbackAgentInteractions(threads, 1);
51
+ expect(JSON.stringify(threads)).toBe(before);
52
+ });
53
+ });
@@ -0,0 +1,102 @@
1
+ const post = jest.fn();
2
+ const publicKeyBase64 = Buffer.alloc(32, 7).toString("base64");
3
+ const keyPairExists = jest.fn(() => true);
4
+ const loadKeyPair = jest.fn(() => ({ publicKeyBase64 }));
5
+ const signMessage = jest.fn(() => "server-message-signature");
6
+
7
+ jest.mock("../../../src/utils/http", () => ({
8
+ __esModule: true,
9
+ default: {
10
+ post,
11
+ isHttpError: (error: unknown) =>
12
+ typeof error === "object" && error !== null && "status" in error,
13
+ },
14
+ }));
15
+
16
+ jest.mock("../../../src/auth/keyManager", () => ({
17
+ keyPairExists,
18
+ loadKeyPair,
19
+ signMessage,
20
+ }));
21
+
22
+ jest.mock("../../../src/auth/browserLogin", () => ({
23
+ getCliUserAgent: () => "test-agent",
24
+ }));
25
+
26
+ import {
27
+ authenticateWithKey,
28
+ exchangePublicKeyJwt,
29
+ hasKeyPair,
30
+ registerPublicKey,
31
+ } from "../../../src/auth/keyAuth";
32
+
33
+ describe("keyAuth", () => {
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ keyPairExists.mockReturnValue(true);
37
+ });
38
+
39
+ it("signs the exact server challenge for the selected key, org, and environment", async () => {
40
+ post
41
+ .mockResolvedValueOnce({
42
+ data: {
43
+ challengeId: "challenge_1",
44
+ message: "exact message from server",
45
+ expiresAt: "2099-01-01T00:00:00.000Z",
46
+ },
47
+ status: 201,
48
+ })
49
+ .mockResolvedValueOnce({ data: { jwt: "renewed-jwt" }, status: 200 });
50
+
51
+ expect(hasKeyPair("/keys/work")).toBe(true);
52
+ await expect(
53
+ exchangePublicKeyJwt("org-1", "https://work.example", "/keys/work")
54
+ ).resolves.toBe("renewed-jwt");
55
+
56
+ expect(loadKeyPair).toHaveBeenCalledWith("/keys/work");
57
+ expect(signMessage).toHaveBeenCalledWith("exact message from server", "/keys/work");
58
+ expect(post.mock.calls).toEqual([
59
+ [
60
+ "https://work.example/api/public-keys/challenge",
61
+ { publicKey: publicKeyBase64, orgId: "org-1" },
62
+ { headers: { "User-Agent": "test-agent" } },
63
+ ],
64
+ [
65
+ "https://work.example/api/public-keys/authenticate",
66
+ { challengeId: "challenge_1", signature: "server-message-signature" },
67
+ { headers: { "User-Agent": "test-agent" } },
68
+ ],
69
+ ]);
70
+ });
71
+
72
+ it("falls back when the key is unavailable without signing a challenge", async () => {
73
+ post.mockRejectedValueOnce({ status: 401 });
74
+
75
+ await expect(
76
+ authenticateWithKey("org-1", "https://work.example", "/keys/work")
77
+ ).resolves.toBe(false);
78
+
79
+ expect(post).toHaveBeenCalledTimes(1);
80
+ expect(signMessage).not.toHaveBeenCalled();
81
+ });
82
+
83
+ it("registers only server-owned Ed25519 key fields in the selected environment", async () => {
84
+ post.mockResolvedValueOnce({ data: {}, status: 201 });
85
+
86
+ await registerPublicKey("browser-jwt", publicKeyBase64, "https://dev.example");
87
+
88
+ expect(post).toHaveBeenCalledWith(
89
+ "https://dev.example/api/public-keys/register",
90
+ {
91
+ publicKey: publicKeyBase64,
92
+ label: expect.any(String),
93
+ },
94
+ {
95
+ headers: {
96
+ Authorization: "Bearer browser-jwt",
97
+ "User-Agent": "test-agent",
98
+ },
99
+ }
100
+ );
101
+ });
102
+ });
@@ -0,0 +1,67 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import {
6
+ generateKeyPair,
7
+ loadKeyPair,
8
+ signMessage,
9
+ } from "../../../src/auth/keyManager";
10
+
11
+ describe("keyManager", () => {
12
+ let directory: string;
13
+ let identity: string;
14
+
15
+ beforeEach(() => {
16
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-key-manager-"));
17
+ identity = path.join(directory, "identity");
18
+ });
19
+
20
+ afterEach(() => {
21
+ fs.rmSync(directory, { recursive: true, force: true });
22
+ });
23
+
24
+ it("creates an Ed25519 identity with an OpenSSH-compatible public key", () => {
25
+ const generated = generateKeyPair(identity);
26
+ const publicKeyLine = fs.readFileSync(`${identity}.pub`, "utf8").trim();
27
+
28
+ expect(publicKeyLine).toMatch(/^ssh-ed25519 [A-Za-z0-9+/]+=* knowhow-cli$/);
29
+ expect(fs.statSync(identity).mode % 0o1000).toBe(0o600);
30
+ expect(loadKeyPair(identity)).toEqual(generated);
31
+
32
+ const challengeMessage = "exact server-provided challenge";
33
+ const signature = signMessage(challengeMessage, identity);
34
+ const publicKey = crypto.createPublicKey(fs.readFileSync(identity));
35
+ expect(
36
+ crypto.verify(
37
+ null,
38
+ Buffer.from(challengeMessage),
39
+ publicKey,
40
+ Buffer.from(signature, "base64")
41
+ )
42
+ ).toBe(true);
43
+ });
44
+
45
+ it("loads legacy raw-base64 public keys but rejects non-Ed25519 identities", () => {
46
+ const generated = generateKeyPair(identity);
47
+ fs.writeFileSync(`${identity}.pub`, generated.publicKeyBase64);
48
+ expect(loadKeyPair(identity).publicKeyBase64).toBe(generated.publicKeyBase64);
49
+
50
+ const rsaIdentity = path.join(directory, "rsa");
51
+ const rsa = crypto.generateKeyPairSync("rsa", {
52
+ modulusLength: 2048,
53
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
54
+ publicKeyEncoding: { type: "spki", format: "pem" },
55
+ });
56
+ fs.writeFileSync(rsaIdentity, rsa.privateKey);
57
+ fs.writeFileSync(`${rsaIdentity}.pub`, generated.publicKeyBase64);
58
+
59
+ expect(() => loadKeyPair(rsaIdentity)).toThrow("must be an Ed25519 private key");
60
+ });
61
+
62
+ it("does not overwrite a partial identity", () => {
63
+ fs.writeFileSync(identity, "existing key");
64
+ expect(() => generateKeyPair(identity)).toThrow("a key file already exists");
65
+ expect(fs.readFileSync(identity, "utf8")).toBe("existing key");
66
+ });
67
+ });
@@ -0,0 +1,71 @@
1
+ import { ToolCommandModule } from "../../../src/chat/modules/ToolCommandModule";
2
+ import { LazyToolsService } from "../../../src/services/LazyToolsService";
3
+ import { ChatService } from "../../../src/chat/types";
4
+
5
+ function makeHarness() {
6
+ const tools = new LazyToolsService();
7
+ tools.addTools([{
8
+ type: "function",
9
+ function: {
10
+ name: "hiddenEcho",
11
+ description: "echo",
12
+ parameters: { type: "object", properties: { value: { type: "string" } } },
13
+ },
14
+ }]);
15
+ tools.setFunction("hiddenEcho", ({ value }: { value: string }) => ({ value }));
16
+
17
+ const context: any = { plugins: [], chatHistory: [], activeAgentTaskId: "task-1" };
18
+ const service = {
19
+ getTools: () => tools,
20
+ getContext: () => context,
21
+ registerCommand: jest.fn(),
22
+ registerMode: jest.fn(),
23
+ } as unknown as ChatService;
24
+ const module = new ToolCommandModule();
25
+ return module.initialize(service).then(() => ({
26
+ command: module.getCommands()[0],
27
+ tools,
28
+ }));
29
+ }
30
+
31
+ describe("ToolCommandModule", () => {
32
+ let log: jest.SpyInstance;
33
+ let error: jest.SpyInstance;
34
+
35
+ beforeEach(() => {
36
+ log = jest.spyOn(console, "log").mockImplementation();
37
+ error = jest.spyOn(console, "error").mockImplementation();
38
+ });
39
+
40
+ afterEach(() => jest.restoreAllMocks());
41
+
42
+ test("executes compact JSON against a hidden lazy tool", async () => {
43
+ const { command, tools } = await makeHarness();
44
+ expect(tools.getToolNames()).not.toContain("hiddenEcho");
45
+
46
+ await command.handler(['{"name":"hiddenEcho","arguments":{"value":"hello world"}}']);
47
+
48
+ expect(log).toHaveBeenCalledWith('{\n "value": "hello world"\n}');
49
+ expect(error).not.toHaveBeenCalled();
50
+ });
51
+
52
+ test("accepts an OpenAI-style tool call", async () => {
53
+ const { command } = await makeHarness();
54
+ await command.handler([
55
+ '{"id":"call-1","function":{"name":"hiddenEcho","arguments":{"value":"ok"}}}',
56
+ ]);
57
+ expect(log).toHaveBeenCalledWith('{\n "value": "ok"\n}');
58
+ });
59
+
60
+ test("prints usage and actionable errors", async () => {
61
+ const { command } = await makeHarness();
62
+ await command.handler([]);
63
+ expect(log).toHaveBeenCalledWith(expect.stringContaining("Usage: /tool"));
64
+
65
+ await command.handler(["not-json"]);
66
+ expect(error).toHaveBeenCalledWith(expect.stringContaining("Invalid tool call JSON"));
67
+
68
+ await command.handler(['{"name":"missing","arguments":{}}']);
69
+ expect(error).toHaveBeenCalledWith(expect.stringContaining("Tool execution failed"));
70
+ });
71
+ });
@@ -0,0 +1,37 @@
1
+ import { parseHistoryCommandArgs } from "../../../src/chat/modules/SessionsModule";
2
+
3
+ describe("parseHistoryCommandArgs", () => {
4
+ it("parses an id and rollback in either order", () => {
5
+ expect(parseHistoryCommandArgs(["task-1", "--rollback", "2"])).toEqual({
6
+ id: "task-1", rollback: 2, showAll: false,
7
+ });
8
+ expect(parseHistoryCommandArgs(["--rollback", "1", "task-1"])).toEqual({
9
+ id: "task-1", rollback: 1, showAll: false,
10
+ });
11
+ });
12
+
13
+ it("supports interactive --all", () => {
14
+ expect(parseHistoryCommandArgs(["--all"])).toEqual({
15
+ id: undefined, rollback: 0, showAll: true,
16
+ });
17
+ });
18
+
19
+ it.each([
20
+ [["--rollback"]],
21
+ [["--rollback", "-1"]],
22
+ [["--rollback", "1.5"]],
23
+ ])(
24
+ "rejects invalid rollback arguments: %p",
25
+ (args) => {
26
+ expect(() => parseHistoryCommandArgs(args)).toThrow(
27
+ "--rollback must be a non-negative integer"
28
+ );
29
+ }
30
+ );
31
+
32
+ it("rejects unexpected arguments", () => {
33
+ expect(() => parseHistoryCommandArgs(["one", "two"])).toThrow(
34
+ "Unexpected argument: two"
35
+ );
36
+ });
37
+ });