@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.
- package/autodoc/cli-reference.md +36 -7
- package/bin/knowhow.js +8 -6
- package/package.json +1 -1
- package/src/agents/base/base.ts +51 -23
- package/src/agents/historyRollback.ts +36 -0
- package/src/agents/tools/execCommand.ts +102 -320
- package/src/agents/tools/list.ts +2 -2
- package/src/agents/tools/startAgentTask.ts +91 -34
- package/src/ai.ts +4 -1
- package/src/auth/browserLogin.ts +9 -3
- package/src/auth/keyAuth.ts +115 -0
- package/src/auth/keyManager.ts +166 -0
- package/src/chat/CliChatService.ts +23 -2
- package/src/chat/modules/AgentModule.ts +114 -33
- package/src/chat/modules/AskModule.ts +2 -0
- package/src/chat/modules/BaseChatModule.ts +0 -1
- package/src/chat/modules/CustomCommandsModule.ts +2 -0
- package/src/chat/modules/InternalChatModule.ts +7 -0
- package/src/chat/modules/RemoteSyncModule.ts +4 -0
- package/src/chat/modules/RendererModule.ts +2 -0
- package/src/chat/modules/SearchModule.ts +2 -0
- package/src/chat/modules/SessionsModule.ts +103 -34
- package/src/chat/modules/SetupModule.ts +2 -0
- package/src/chat/modules/ShellCommandModule.ts +2 -0
- package/src/chat/modules/SystemModule.ts +2 -0
- package/src/chat/modules/ToolCommandModule.ts +102 -0
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/types.ts +3 -1
- package/src/cli.ts +10 -3
- package/src/clients/http.ts +67 -7
- package/src/clients/index.ts +4 -0
- package/src/commands/agent.ts +1 -38
- package/src/commands/agents.ts +455 -60
- package/src/commands/misc.ts +3 -2
- package/src/commands/processes.ts +138 -0
- package/src/commands/services.ts +23 -1
- package/src/index.ts +1 -0
- package/src/login.ts +89 -28
- package/src/processes/ProcessManager.ts +95 -0
- package/src/processes/managedProcessShim.ts +115 -0
- package/src/services/AgentService.ts +2 -0
- package/src/services/AgentSyncFs.ts +77 -1
- package/src/services/AgentSyncKnowhowWeb.ts +22 -4
- package/src/services/BehaviorsService.ts +2 -0
- package/src/services/DockerService.ts +2 -0
- package/src/services/EmbeddingService.ts +2 -0
- package/src/services/EmbeddingsService.ts +3 -0
- package/src/services/ExtensionsService.ts +25 -0
- package/src/services/KnowhowClient.ts +68 -7
- package/src/services/LazyToolsService.ts +24 -20
- package/src/services/Mcp.ts +2 -0
- package/src/services/McpServer.ts +2 -0
- package/src/services/MessageProcessor.ts +2 -0
- package/src/services/MinimalToolsService.ts +2 -0
- package/src/services/SessionManager.ts +5 -0
- package/src/services/SkillsService.ts +2 -0
- package/src/services/SyncedAgentWatcher.ts +2 -0
- package/src/services/SyncerService.ts +23 -8
- package/src/services/TaskRegistry.ts +2 -0
- package/src/services/Tools.ts +12 -0
- package/src/services/index.ts +6 -0
- package/src/services/modules/index.ts +10 -1
- package/src/services/modules/types.ts +4 -0
- package/src/types.ts +2 -0
- package/src/util/Trace.ts +1 -1
- package/src/utils/http.ts +18 -2
- package/tests/unit/agents/historyRollback.test.ts +53 -0
- package/tests/unit/auth/keyAuth.test.ts +102 -0
- package/tests/unit/auth/keyManager.test.ts +67 -0
- package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
- package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
- package/tests/unit/commands/agents.test.ts +111 -0
- package/tests/unit/login.test.ts +119 -0
- package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
- package/tests/unit/services/LazyToolsService.test.ts +53 -0
- package/tests/unit/services/SessionManager.test.ts +43 -0
- package/tests/unit/services/SyncerService.test.ts +45 -0
- package/tests/unit/util/Trace.test.ts +119 -0
- package/tests/unit/utils/http.test.ts +43 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +2 -1
- package/ts_build/src/agents/base/base.js +47 -14
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/historyRollback.d.ts +2 -0
- package/ts_build/src/agents/historyRollback.js +22 -0
- package/ts_build/src/agents/historyRollback.js.map +1 -0
- package/ts_build/src/agents/tools/execCommand.d.ts +4 -1
- package/ts_build/src/agents/tools/execCommand.js +100 -223
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +2 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.d.ts +16 -1
- package/ts_build/src/agents/tools/startAgentTask.js +71 -28
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.js +2 -1
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/auth/browserLogin.d.ts +1 -0
- package/ts_build/src/auth/browserLogin.js +5 -2
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/auth/keyAuth.d.ts +5 -0
- package/ts_build/src/auth/keyAuth.js +79 -0
- package/ts_build/src/auth/keyAuth.js.map +1 -0
- package/ts_build/src/auth/keyManager.d.ts +14 -0
- package/ts_build/src/auth/keyManager.js +145 -0
- package/ts_build/src/auth/keyManager.js.map +1 -0
- package/ts_build/src/chat/CliChatService.d.ts +6 -0
- package/ts_build/src/chat/CliChatService.js +17 -2
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
- package/ts_build/src/chat/modules/AgentModule.js +83 -24
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/AskModule.js +12 -2
- package/ts_build/src/chat/modules/AskModule.js.map +1 -1
- package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
- package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
- package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +21 -2
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/chat/modules/RendererModule.js +17 -2
- package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
- package/ts_build/src/chat/modules/SearchModule.js +12 -2
- package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
- package/ts_build/src/chat/modules/SessionsModule.js +113 -18
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/modules/SetupModule.js +17 -2
- package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
- package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
- package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
- package/ts_build/src/chat/modules/SystemModule.js +12 -2
- package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
- package/ts_build/src/chat/modules/ToolCommandModule.d.ts +10 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
- package/ts_build/src/chat/modules/index.d.ts +1 -0
- package/ts_build/src/chat/modules/index.js +3 -1
- package/ts_build/src/chat/modules/index.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +3 -1
- package/ts_build/src/cli.js +7 -3
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/http.d.ts +4 -0
- package/ts_build/src/clients/http.js +68 -6
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.js +23 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agent.js +1 -21
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +2 -1
- package/ts_build/src/commands/agents.js +216 -23
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/misc.js +3 -2
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/processes.d.ts +2 -0
- package/ts_build/src/commands/processes.js +213 -0
- package/ts_build/src/commands/processes.js.map +1 -0
- package/ts_build/src/commands/services.js +11 -1
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/index.d.ts +1 -0
- package/ts_build/src/index.js +1 -0
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/login.d.ts +1 -1
- package/ts_build/src/login.js +53 -15
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/processes/ProcessManager.d.ts +51 -0
- package/ts_build/src/processes/ProcessManager.js +160 -0
- package/ts_build/src/processes/ProcessManager.js.map +1 -0
- package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
- package/ts_build/src/processes/managedProcessShim.js +161 -0
- package/ts_build/src/processes/managedProcessShim.js.map +1 -0
- package/ts_build/src/services/AgentService.js +18 -2
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
- package/ts_build/src/services/AgentSyncFs.js +68 -16
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
- package/ts_build/src/services/BehaviorsService.js +12 -2
- package/ts_build/src/services/BehaviorsService.js.map +1 -1
- package/ts_build/src/services/DockerService.js +20 -8
- package/ts_build/src/services/DockerService.js.map +1 -1
- package/ts_build/src/services/EmbeddingService.js +16 -2
- package/ts_build/src/services/EmbeddingService.js.map +1 -1
- package/ts_build/src/services/EmbeddingsService.js +12 -2
- package/ts_build/src/services/EmbeddingsService.js.map +1 -1
- package/ts_build/src/services/ExtensionsService.d.ts +10 -0
- package/ts_build/src/services/ExtensionsService.js +17 -0
- package/ts_build/src/services/ExtensionsService.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +7 -2
- package/ts_build/src/services/KnowhowClient.js +108 -10
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +2 -0
- package/ts_build/src/services/LazyToolsService.js +26 -9
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Mcp.js +12 -2
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/services/McpServer.js +17 -2
- package/ts_build/src/services/McpServer.js.map +1 -1
- package/ts_build/src/services/MessageProcessor.js +16 -2
- package/ts_build/src/services/MessageProcessor.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.js +16 -2
- package/ts_build/src/services/MinimalToolsService.js.map +1 -1
- package/ts_build/src/services/SessionManager.js +19 -2
- package/ts_build/src/services/SessionManager.js.map +1 -1
- package/ts_build/src/services/SkillsService.js +16 -2
- package/ts_build/src/services/SkillsService.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/SyncerService.d.ts +1 -0
- package/ts_build/src/services/SyncerService.js +27 -5
- package/ts_build/src/services/SyncerService.js.map +1 -1
- package/ts_build/src/services/TaskRegistry.js +12 -2
- package/ts_build/src/services/TaskRegistry.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +1 -0
- package/ts_build/src/services/Tools.js +19 -2
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +5 -0
- package/ts_build/src/services/index.js +7 -2
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +4 -1
- package/ts_build/src/services/modules/index.js +6 -1
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +3 -0
- package/ts_build/src/types.d.ts +1 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/util/Trace.d.ts +1 -3
- package/ts_build/src/utils/http.d.ts +6 -2
- package/ts_build/src/utils/http.js +8 -2
- package/ts_build/src/utils/http.js.map +1 -1
- package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
- package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
- package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
- package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.d.ts +1 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.js.map +1 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
- package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/agents.test.js +119 -0
- package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
- package/ts_build/tests/unit/login.test.d.ts +1 -0
- package/ts_build/tests/unit/login.test.js +97 -0
- package/ts_build/tests/unit/login.test.js.map +1 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js +47 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
- package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
- package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
- package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
- package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
- package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
- package/ts_build/tests/unit/util/Trace.test.js +107 -0
- package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
- package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
- package/ts_build/tests/unit/utils/http.test.js +69 -0
- 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
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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 &&
|
|
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.
|
|
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.
|
|
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
|
|
package/src/services/Tools.ts
CHANGED
|
@@ -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) =>
|
package/src/services/index.ts
CHANGED
|
@@ -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
|
|
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?:
|
|
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
|
+
});
|