@tyvm/knowhow 0.0.147 → 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/package.json +1 -1
- package/src/agents/base/base.ts +27 -23
- package/src/agents/tools/execCommand.ts +102 -320
- package/src/agents/tools/list.ts +2 -2
- package/src/agents/tools/startAgentTask.ts +50 -25
- package/src/ai.ts +4 -1
- package/src/chat/CliChatService.ts +23 -2
- package/src/chat/modules/InternalChatModule.ts +5 -0
- package/src/chat/modules/ToolCommandModule.ts +102 -0
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/types.ts +1 -1
- package/src/cli.ts +9 -2
- package/src/clients/http.ts +67 -7
- package/src/clients/index.ts +2 -0
- package/src/commands/agents.ts +53 -8
- package/src/commands/processes.ts +138 -0
- package/src/commands/services.ts +23 -1
- package/src/index.ts +1 -0
- package/src/processes/ProcessManager.ts +95 -0
- package/src/processes/managedProcessShim.ts +115 -0
- package/src/services/AgentSyncFs.ts +12 -1
- package/src/services/ExtensionsService.ts +25 -0
- package/src/services/LazyToolsService.ts +15 -20
- package/src/services/Tools.ts +10 -0
- package/src/services/index.ts +4 -0
- package/src/services/modules/index.ts +6 -0
- package/src/services/modules/types.ts +4 -0
- package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
- package/tests/unit/commands/agents.test.ts +44 -1
- package/tests/unit/services/LazyToolsService.test.ts +24 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +1 -1
- package/ts_build/src/agents/base/base.js +16 -12
- package/ts_build/src/agents/base/base.js.map +1 -1
- 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 +14 -1
- package/ts_build/src/agents/tools/startAgentTask.js +35 -19
- 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/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/InternalChatModule.d.ts +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +5 -0
- package/ts_build/src/chat/modules/InternalChatModule.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 +1 -1
- package/ts_build/src/cli.js +6 -2
- 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 +4 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +1 -0
- package/ts_build/src/commands/agents.js +42 -7
- package/ts_build/src/commands/agents.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/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/AgentSyncFs.js +6 -1
- package/ts_build/src/services/AgentSyncFs.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/LazyToolsService.d.ts +1 -0
- package/ts_build/src/services/LazyToolsService.js +5 -7
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +1 -0
- package/ts_build/src/services/Tools.js +3 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +3 -0
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +2 -0
- package/ts_build/src/services/modules/index.js +5 -0
- 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/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/commands/agents.test.js +28 -0
- package/ts_build/tests/unit/commands/agents.test.js.map +1 -1
- package/ts_build/tests/unit/services/LazyToolsService.test.js +20 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -1
|
@@ -34,6 +34,20 @@ export interface StartAgentTaskParams {
|
|
|
34
34
|
_ctx?: { caller?: any; taskId?: string; [key: string]: any };
|
|
35
35
|
}
|
|
36
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
|
+
|
|
37
51
|
const PROCESSES_DIR = path.join(process.cwd(), ".knowhow", "processes");
|
|
38
52
|
const AGENTS_DIR = path.join(process.cwd(), ".knowhow", "processes", "agents");
|
|
39
53
|
|
|
@@ -100,7 +114,7 @@ function subagentCapabilitiesNote(): string {
|
|
|
100
114
|
* .knowhow/processes/agents/{taskId}/input.txt
|
|
101
115
|
* The agent will pick up the new content and process it as a new message.
|
|
102
116
|
*/
|
|
103
|
-
export async function startAgentTask(params: StartAgentTaskParams): Promise<
|
|
117
|
+
export async function startAgentTask(params: StartAgentTaskParams): Promise<StartAgentTaskResult> {
|
|
104
118
|
const {
|
|
105
119
|
messageId,
|
|
106
120
|
prompt,
|
|
@@ -235,43 +249,54 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
235
249
|
child.stdin!.write(promptWithCaps, "utf8");
|
|
236
250
|
child.stdin!.end();
|
|
237
251
|
|
|
238
|
-
return new Promise<
|
|
252
|
+
return new Promise<StartAgentTaskResult>((resolve) => {
|
|
239
253
|
let settled = false;
|
|
240
|
-
|
|
254
|
+
let exited = false;
|
|
255
|
+
const done = (result: StartAgentTaskResult) => {
|
|
241
256
|
if (settled) return;
|
|
242
257
|
settled = true;
|
|
243
258
|
try { fs.closeSync(fd); } catch {}
|
|
244
|
-
resolve(
|
|
259
|
+
resolve(result);
|
|
245
260
|
};
|
|
246
261
|
|
|
247
262
|
child.once("error", (e) => {
|
|
248
|
-
done(
|
|
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
|
+
});
|
|
249
268
|
});
|
|
250
269
|
|
|
251
|
-
const syncFsNote = useSyncFs
|
|
252
|
-
? `\nTask ID: ${taskId}\nAgent dir: ${agentTaskDir}\n` +
|
|
253
|
-
`To send agent messages, write to: ${agentTaskDir}/input.txt\n` +
|
|
254
|
-
`To check status, read: ${agentTaskDir}/status.txt\n`
|
|
255
|
-
: "";
|
|
256
|
-
|
|
257
270
|
child.once("exit", (code) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
+
});
|
|
262
279
|
});
|
|
263
280
|
|
|
264
281
|
if (!waitForCompletion) {
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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);
|
|
275
300
|
}
|
|
276
301
|
});
|
|
277
302
|
}
|
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
|
-
|
|
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) {
|
|
@@ -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.
|
|
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) {
|
|
@@ -10,6 +10,7 @@ import { SystemModule } from "./SystemModule";
|
|
|
10
10
|
import { SetupModule } from "./SetupModule";
|
|
11
11
|
import { CustomCommandsModule } from "./CustomCommandsModule";
|
|
12
12
|
import { ShellCommandModule } from "./ShellCommandModule";
|
|
13
|
+
import { ToolCommandModule } from "./ToolCommandModule";
|
|
13
14
|
import { RendererModule } from "./RendererModule";
|
|
14
15
|
import { SessionsModule } from "./SessionsModule";
|
|
15
16
|
import { RemoteSyncModule } from "./RemoteSyncModule";
|
|
@@ -32,6 +33,7 @@ export class InternalChatModule implements ChatModule {
|
|
|
32
33
|
private setupModule: SetupModule;
|
|
33
34
|
private customCommandsModule = new CustomCommandsModule();
|
|
34
35
|
private shellCommandModule = new ShellCommandModule();
|
|
36
|
+
private toolCommandModule = new ToolCommandModule();
|
|
35
37
|
private rendererModule: RendererModule;
|
|
36
38
|
private remoteSyncModule: RemoteSyncModule;
|
|
37
39
|
|
|
@@ -61,6 +63,7 @@ export class InternalChatModule implements ChatModule {
|
|
|
61
63
|
await this.customCommandsModule.initialize(chatService);
|
|
62
64
|
await this.remoteSyncModule.initialize(chatService);
|
|
63
65
|
await this.shellCommandModule.initialize(chatService);
|
|
66
|
+
await this.toolCommandModule.initialize(chatService);
|
|
64
67
|
await this.clipboardImageModule.initialize(chatService);
|
|
65
68
|
|
|
66
69
|
// Register our own commands (exit and multi) - not duplicated by BaseChatModule
|
|
@@ -93,6 +96,7 @@ export class InternalChatModule implements ChatModule {
|
|
|
93
96
|
...this.setupModule.getCommands(),
|
|
94
97
|
...this.customCommandsModule.getCommands(),
|
|
95
98
|
...this.shellCommandModule.getCommands(),
|
|
99
|
+
...this.toolCommandModule.getCommands(),
|
|
96
100
|
...this.rendererModule.getCommands(),
|
|
97
101
|
...this.remoteSyncModule.getCommands(),
|
|
98
102
|
{
|
|
@@ -120,6 +124,7 @@ export class InternalChatModule implements ChatModule {
|
|
|
120
124
|
...this.setupModule.getModes(),
|
|
121
125
|
...this.customCommandsModule.getModes(),
|
|
122
126
|
...this.shellCommandModule.getModes(),
|
|
127
|
+
...this.toolCommandModule.getModes(),
|
|
123
128
|
...this.rendererModule.getModes(),
|
|
124
129
|
];
|
|
125
130
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ToolCall } from "../../clients/types";
|
|
2
|
+
import { services } from "../../services";
|
|
3
|
+
import { BaseChatModule } from "./BaseChatModule";
|
|
4
|
+
import { ChatCommand, ChatContext, CommandResult } from "../types";
|
|
5
|
+
|
|
6
|
+
/** Execute registered tools directly from interactive chat. */
|
|
7
|
+
export class ToolCommandModule extends BaseChatModule {
|
|
8
|
+
name = "tool-command";
|
|
9
|
+
description = "Execute a tool with /tool";
|
|
10
|
+
|
|
11
|
+
public getCommands(): ChatCommand[] {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
name: "tool",
|
|
15
|
+
description: "Execute a tool call from JSON",
|
|
16
|
+
handler: (args: string[]) => this.handleToolCommand(args),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async handleToolCommand(args: string[]): Promise<CommandResult> {
|
|
22
|
+
const input = args.join(" ").trim();
|
|
23
|
+
if (!input) {
|
|
24
|
+
console.log('Usage: /tool {"name":"toolName","arguments":{"key":"value"}}');
|
|
25
|
+
return { handled: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let toolCall: ToolCall;
|
|
29
|
+
try {
|
|
30
|
+
toolCall = this.parseToolCall(input);
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
console.error(`Invalid tool call JSON: ${error.message}`);
|
|
33
|
+
return { handled: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tools = this.chatService.getTools() || services().Tools;
|
|
37
|
+
if (!tools) {
|
|
38
|
+
console.error("Tool execution failed: no tool service is available");
|
|
39
|
+
return { handled: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const context = this.chatService.getContext();
|
|
44
|
+
const result = await tools.callTool(
|
|
45
|
+
toolCall,
|
|
46
|
+
tools.getFunctionNames(),
|
|
47
|
+
{
|
|
48
|
+
caller: context.selectedAgent,
|
|
49
|
+
taskId: context.activeAgentTaskId,
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const toolError = result.toolMessages?.find(
|
|
54
|
+
(message: any) => message.name === "error"
|
|
55
|
+
);
|
|
56
|
+
if (toolError) {
|
|
57
|
+
console.error(`Tool execution failed: ${toolError.content}`);
|
|
58
|
+
} else {
|
|
59
|
+
const output = result.functionResp;
|
|
60
|
+
console.log(
|
|
61
|
+
typeof output === "object"
|
|
62
|
+
? JSON.stringify(output, null, 2)
|
|
63
|
+
: String(output)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
console.error(`Tool execution failed: ${error.message || String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { handled: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private parseToolCall(input: string): ToolCall {
|
|
74
|
+
const parsed = JSON.parse(input);
|
|
75
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
76
|
+
throw new Error("expected a JSON object");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fn = parsed.function || parsed;
|
|
80
|
+
if (!fn || typeof fn.name !== "string" || !fn.name.trim()) {
|
|
81
|
+
throw new Error('expected a non-empty "name" (or "function.name")');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const args = fn.arguments === undefined ? {} : fn.arguments;
|
|
85
|
+
if (typeof args !== "string" && (args === null || typeof args !== "object")) {
|
|
86
|
+
throw new Error('"arguments" must be a JSON object, array, or JSON string');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
id: typeof parsed.id === "string" ? parsed.id : `chat_tool_${Date.now()}`,
|
|
91
|
+
type: "function",
|
|
92
|
+
function: {
|
|
93
|
+
name: fn.name,
|
|
94
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async handleInput(input: string, context: ChatContext): Promise<boolean> {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -4,4 +4,5 @@ export { AgentModule } from './AgentModule';
|
|
|
4
4
|
export { VoiceModule } from './VoiceModule';
|
|
5
5
|
export { InternalChatModule } from './InternalChatModule';
|
|
6
6
|
export { ShellCommandModule } from './ShellCommandModule';
|
|
7
|
+
export { ToolCommandModule } from './ToolCommandModule';
|
|
7
8
|
export { RendererModule } from './RendererModule';
|
package/src/chat/types.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface CommandResult {
|
|
|
45
45
|
export interface ChatCommand {
|
|
46
46
|
name: string;
|
|
47
47
|
description: string;
|
|
48
|
-
handler: (args: string[]) => Promise<void | CommandResult>;
|
|
48
|
+
handler: (args: string[], chatService?: ChatService) => Promise<void | CommandResult>;
|
|
49
49
|
modes?: string[]; // Commands can specify which modes they're available in
|
|
50
50
|
}
|
|
51
51
|
|
package/src/cli.ts
CHANGED
|
@@ -62,6 +62,7 @@ import { addSkillsCommand } from "./commands/skills";
|
|
|
62
62
|
import { addAgentsCommand } from "./commands/agents";
|
|
63
63
|
import { addHashCommand } from "./commands/hash";
|
|
64
64
|
import { addSandboxCommand } from "./commands/sandbox";
|
|
65
|
+
import { addProcessesCommand } from "./commands/processes";
|
|
65
66
|
|
|
66
67
|
// Handle unhandled promise rejections gracefully — particularly from MCP SDK
|
|
67
68
|
// which fires errors via event emitters that can bypass Promise.allSettled.
|
|
@@ -131,15 +132,21 @@ async function main() {
|
|
|
131
132
|
addAgentsCommand(program, getChatService);
|
|
132
133
|
addHashCommand(program);
|
|
133
134
|
addSandboxCommand(program);
|
|
135
|
+
addProcessesCommand(program);
|
|
134
136
|
|
|
135
137
|
// Load global modules early (before parse) so they can register CLI subcommands.
|
|
136
138
|
// We pass only the Program in context — no services are spun up at this stage.
|
|
137
139
|
// Each module's command action is responsible for calling setupServices() as needed.
|
|
138
140
|
try {
|
|
139
141
|
const globalConfig = await getGlobalConfig();
|
|
142
|
+
const globalModules = [...new Set(globalConfig.modules || [])];
|
|
143
|
+
const localModules = [...new Set(config.modules || [])];
|
|
144
|
+
// Local modules take precedence: if a module appears in both global and
|
|
145
|
+
// local configs, use the local version so it resolves against local
|
|
146
|
+
// node_modules first. This prevents duplicate command registration errors.
|
|
140
147
|
const allModulePaths = [
|
|
141
|
-
...(
|
|
142
|
-
...
|
|
148
|
+
...globalModules.filter((m) => !localModules.includes(m)),
|
|
149
|
+
...localModules,
|
|
143
150
|
];
|
|
144
151
|
if (allModulePaths.length) {
|
|
145
152
|
const earlyModulesService = new ModulesService();
|
package/src/clients/http.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import http from "../utils/http";
|
|
1
|
+
import http, { HTTP_UNAUTHORIZED_HANDLER, RefreshableHeaders } from "../utils/http";
|
|
2
2
|
import {
|
|
3
3
|
GenericClient,
|
|
4
4
|
CompletionOptions,
|
|
@@ -17,16 +17,72 @@ export interface HttpClientOptions {
|
|
|
17
17
|
extra_body?: Record<string, any>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Optional callback that, when set, is called whenever a 401 is received
|
|
22
|
+
* (reactive refresh) or whenever `needsRefresh()` returns true before a
|
|
23
|
+
* request (proactive refresh). Override this in a subclass or set it
|
|
24
|
+
* directly on an instance:
|
|
25
|
+
*
|
|
26
|
+
* client.refreshToken = async () => { const newJwt = await ...; client.setJwt(newJwt); };
|
|
27
|
+
*/
|
|
28
|
+
export type RefreshTokenFn = () => Promise<void>;
|
|
29
|
+
|
|
20
30
|
export class HttpClient implements GenericClient {
|
|
21
31
|
/** Timeout in milliseconds for HTTP requests. Default: 30000 (30s). Use 0 to disable. */
|
|
22
32
|
private timeout: number;
|
|
23
|
-
private headers:
|
|
33
|
+
private headers: RefreshableHeaders;
|
|
24
34
|
private extra_body: Record<string, any>;
|
|
25
35
|
/** Optional pricing table: model id → per-million-token prices */
|
|
26
36
|
private pricingMap: Record<string, ModelPricing> = {};
|
|
27
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Override this to provide automatic token refresh. Called reactively on
|
|
40
|
+
* 401 responses (via HTTP_UNAUTHORIZED_HANDLER) and proactively before
|
|
41
|
+
* each request when `needsRefresh()` returns true.
|
|
42
|
+
*
|
|
43
|
+
* The implementation should obtain a new token and call `this.setJwt()`
|
|
44
|
+
* (or `this.setKey()`) before returning.
|
|
45
|
+
*/
|
|
46
|
+
refreshToken: RefreshTokenFn | undefined = undefined;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Override this to provide proactive expiry detection. When it returns
|
|
50
|
+
* `true`, `refreshToken()` is called before the next outgoing request.
|
|
51
|
+
* The default implementation decodes the JWT `exp` claim (< 5 min window).
|
|
52
|
+
*/
|
|
53
|
+
needsRefresh(): boolean {
|
|
54
|
+
const authHeader = (this.headers as Record<string, string>)["Authorization"];
|
|
55
|
+
if (!authHeader) return false;
|
|
56
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
|
|
57
|
+
try {
|
|
58
|
+
const parts = token.split(".");
|
|
59
|
+
if (parts.length !== 3) return false;
|
|
60
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
61
|
+
return typeof payload.exp === "number" && payload.exp * 1000 - Date.now() < 5 * 60 * 1000;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Call before each outgoing request to proactively refresh an expiring
|
|
69
|
+
* token. No-ops when no `refreshToken` hook is set.
|
|
70
|
+
*/
|
|
71
|
+
private async refreshIfNeeded(): Promise<void> {
|
|
72
|
+
if (this.refreshToken && this.needsRefresh()) {
|
|
73
|
+
await this.refreshToken();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
28
77
|
constructor(private baseUrl: string, options: HttpClientOptions = {}) {
|
|
29
78
|
this.headers = options.headers ?? {};
|
|
79
|
+
// Wire the reactive 401 handler so the low-level http util can retry once
|
|
80
|
+
// after a token refresh without callers needing to do anything extra.
|
|
81
|
+
Object.defineProperty(this.headers, HTTP_UNAUTHORIZED_HANDLER, {
|
|
82
|
+
enumerable: false,
|
|
83
|
+
configurable: true,
|
|
84
|
+
get: () => this.refreshToken ? () => this.refreshToken!() : undefined,
|
|
85
|
+
});
|
|
30
86
|
this.timeout = options.timeout ?? 30000;
|
|
31
87
|
this.extra_body = options.extra_body ?? {};
|
|
32
88
|
}
|
|
@@ -69,10 +125,8 @@ export class HttpClient implements GenericClient {
|
|
|
69
125
|
}
|
|
70
126
|
|
|
71
127
|
setJwt(jwt: string) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Authorization: `Bearer ${jwt}`,
|
|
75
|
-
};
|
|
128
|
+
// Mutate in-place to preserve the HTTP_UNAUTHORIZED_HANDLER symbol property.
|
|
129
|
+
this.headers.Authorization = `Bearer ${jwt}`;
|
|
76
130
|
}
|
|
77
131
|
|
|
78
132
|
setBaseUrl(baseUrl: string) {
|
|
@@ -163,7 +217,8 @@ export class HttpClient implements GenericClient {
|
|
|
163
217
|
if (options.timeout !== undefined) this.timeout = options.timeout;
|
|
164
218
|
if (options.extra_body !== undefined) this.extra_body = options.extra_body;
|
|
165
219
|
if (options.headers) {
|
|
166
|
-
|
|
220
|
+
// Mutate in-place to preserve the HTTP_UNAUTHORIZED_HANDLER symbol property.
|
|
221
|
+
Object.assign(this.headers, options.headers);
|
|
167
222
|
}
|
|
168
223
|
}
|
|
169
224
|
|
|
@@ -183,6 +238,7 @@ export class HttpClient implements GenericClient {
|
|
|
183
238
|
async createChatCompletion(
|
|
184
239
|
options: CompletionOptions
|
|
185
240
|
): Promise<CompletionResponse> {
|
|
241
|
+
await this.refreshIfNeeded();
|
|
186
242
|
return this.withRetry(async () => {
|
|
187
243
|
const body = {
|
|
188
244
|
...options,
|
|
@@ -232,6 +288,7 @@ export class HttpClient implements GenericClient {
|
|
|
232
288
|
async *createChatCompletionStream(
|
|
233
289
|
options: CompletionOptions
|
|
234
290
|
): AsyncGenerator<StreamChunk> {
|
|
291
|
+
await this.refreshIfNeeded();
|
|
235
292
|
const body = {
|
|
236
293
|
...options,
|
|
237
294
|
model: options.model,
|
|
@@ -316,6 +373,7 @@ export class HttpClient implements GenericClient {
|
|
|
316
373
|
options: CompletionOptions,
|
|
317
374
|
store = false
|
|
318
375
|
): Promise<CompletionResponse> {
|
|
376
|
+
await this.refreshIfNeeded();
|
|
319
377
|
return this.withRetry(async () => {
|
|
320
378
|
// Extract system messages as instructions
|
|
321
379
|
const systemMessages = options.messages.filter((m) => m.role === "system");
|
|
@@ -484,6 +542,7 @@ export class HttpClient implements GenericClient {
|
|
|
484
542
|
}
|
|
485
543
|
|
|
486
544
|
async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
|
|
545
|
+
await this.refreshIfNeeded();
|
|
487
546
|
return this.withRetry(async () => {
|
|
488
547
|
const response = await http.post(
|
|
489
548
|
`${this.baseUrl}/v1/embeddings`,
|
|
@@ -511,6 +570,7 @@ export class HttpClient implements GenericClient {
|
|
|
511
570
|
}
|
|
512
571
|
|
|
513
572
|
async getModels(type = "all") {
|
|
573
|
+
await this.refreshIfNeeded();
|
|
514
574
|
return this.withRetry(async () => {
|
|
515
575
|
const response = await http.get(`${this.baseUrl}/v1/models?type=${type}`, {
|
|
516
576
|
headers: this.headers as Record<string, string>,
|
package/src/clients/index.ts
CHANGED
|
@@ -528,12 +528,14 @@ export class AIClient {
|
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
providerHasModel(provider: string, model: string): boolean {
|
|
531
|
+
if (!provider) return false;
|
|
531
532
|
const models = this.clientModels[provider];
|
|
532
533
|
if (!models) return false;
|
|
533
534
|
return models.includes(model);
|
|
534
535
|
}
|
|
535
536
|
|
|
536
537
|
findModel(modelPrefix: string) {
|
|
538
|
+
if (!modelPrefix) return undefined;
|
|
537
539
|
for (const provider of Object.keys(this.clientModels)) {
|
|
538
540
|
const models = this.clientModels[provider] as string[];
|
|
539
541
|
const foundModel = models.find((m) => m.startsWith(modelPrefix));
|
package/src/commands/agents.ts
CHANGED
|
@@ -35,6 +35,18 @@ import { getConfig } from "../config";
|
|
|
35
35
|
const AGENTS_DIR = path.join(".knowhow", "processes", "agents");
|
|
36
36
|
const SESSIONS_DIR = path.join(".knowhow", "chats", "sessions");
|
|
37
37
|
|
|
38
|
+
const TERMINAL_STATUSES = new Set([
|
|
39
|
+
"completed",
|
|
40
|
+
"failed",
|
|
41
|
+
"killed",
|
|
42
|
+
"error",
|
|
43
|
+
"done",
|
|
44
|
+
]);
|
|
45
|
+
const ACTIVE_STATUSES = new Set(["running", "paused"]);
|
|
46
|
+
// Older task records did not persist a pid. After this grace period, report
|
|
47
|
+
// them as unverifiable rather than continuing to claim they are running.
|
|
48
|
+
const UNVERIFIED_TASK_GRACE_MS = 5 * 60 * 1000;
|
|
49
|
+
|
|
38
50
|
// ---------------------------------------------------------------------------
|
|
39
51
|
// Helpers
|
|
40
52
|
// ---------------------------------------------------------------------------
|
|
@@ -68,11 +80,42 @@ function loadStatus(taskId: string): string {
|
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
function pidAlive(pid: number): boolean {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
// EPERM means the process exists but cannot be signalled by this user.
|
|
89
|
+
return error?.code === "EPERM";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the effective status, reconciling persisted state with process liveness.
|
|
95
|
+
* An agent can be terminated before AgentSyncFs gets a chance to finalize its
|
|
96
|
+
* files; those records must not remain "running" forever.
|
|
97
|
+
*/
|
|
98
|
+
export function getStatus(taskId: string, meta: any | null): string {
|
|
99
|
+
const persisted = meta?.status || loadStatus(taskId) || "unknown";
|
|
100
|
+
if (TERMINAL_STATUSES.has(persisted)) return persisted;
|
|
101
|
+
if (meta?.inProgress === false) return "completed";
|
|
102
|
+
if (!ACTIVE_STATUSES.has(persisted)) return persisted;
|
|
103
|
+
|
|
104
|
+
const pid = meta?.pid ?? meta?.processId;
|
|
105
|
+
if (typeof pid === "number") {
|
|
106
|
+
return pidAlive(pid) ? persisted : "failed";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lastKnownTime = meta?.lastUpdate ?? meta?.startTime;
|
|
110
|
+
const lastKnownMs = lastKnownTime ? new Date(lastKnownTime).getTime() : NaN;
|
|
111
|
+
if (
|
|
112
|
+
!Number.isFinite(lastKnownMs) ||
|
|
113
|
+
Date.now() - lastKnownMs > UNVERIFIED_TASK_GRACE_MS
|
|
114
|
+
) {
|
|
115
|
+
return "stale";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return persisted;
|
|
76
119
|
}
|
|
77
120
|
|
|
78
121
|
function elapsedStr(startIso: string | undefined, endIso?: string): string {
|
|
@@ -203,9 +246,9 @@ function buildRows(includeAll: boolean): Row[] {
|
|
|
203
246
|
|
|
204
247
|
for (const taskId of taskIds) {
|
|
205
248
|
const meta = loadMeta(taskId);
|
|
206
|
-
const status =
|
|
249
|
+
const status = getStatus(taskId, meta);
|
|
207
250
|
|
|
208
|
-
if (!includeAll && (status
|
|
251
|
+
if (!includeAll && (TERMINAL_STATUSES.has(status) || status === "stale")) {
|
|
209
252
|
const lastUpdate = meta?.lastUpdate ?? "";
|
|
210
253
|
if (lastUpdate) {
|
|
211
254
|
const age = Date.now() - new Date(lastUpdate).getTime();
|
|
@@ -221,7 +264,9 @@ function buildRows(includeAll: boolean): Row[] {
|
|
|
221
264
|
: "";
|
|
222
265
|
const elapsed = elapsedStr(
|
|
223
266
|
meta?.startTime,
|
|
224
|
-
|
|
267
|
+
TERMINAL_STATUSES.has(status) || status === "stale"
|
|
268
|
+
? meta?.lastUpdate
|
|
269
|
+
: undefined
|
|
225
270
|
);
|
|
226
271
|
const lastUpdate = meta?.lastUpdate
|
|
227
272
|
? new Date(meta.lastUpdate).toLocaleString()
|