@tyvm/knowhow 0.0.147 → 0.0.149
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 +45 -32
- package/src/agents/tools/execCommand.ts +102 -320
- package/src/agents/tools/list.ts +2 -2
- package/src/agents/tools/startAgentTask.ts +55 -27
- package/src/ai.ts +4 -1
- package/src/auth/keyAuth.ts +10 -4
- package/src/auth/registerIdentity.ts +67 -0
- package/src/chat/CliChatService.ts +23 -2
- package/src/chat/modules/AgentModule.ts +39 -7
- 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 +36 -1
- package/src/commands/agent.ts +1 -0
- package/src/commands/agents.ts +57 -9
- package/src/commands/misc.ts +17 -2
- package/src/commands/processes.ts +203 -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 +127 -0
- package/src/processors/Base64ImageDetector.ts +28 -4
- package/src/processors/JsonCompressor.ts +14 -0
- package/src/processors/TokenCompressor.ts +17 -14
- package/src/services/AgentService.ts +2 -2
- 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/computerUse.ts +3 -1
- package/src/services/modules/index.ts +6 -0
- package/src/services/modules/types.ts +4 -0
- package/tests/clients/AIClient.test.ts +98 -15
- package/tests/processes/managedProcessShim.test.ts +59 -0
- package/tests/processors/Base64ImageDetector.test.ts +21 -0
- package/tests/processors/TokenCompressor.test.ts +18 -0
- package/tests/unit/auth/registerIdentity.test.ts +93 -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 +30 -21
- 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 +37 -21
- 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/keyAuth.js +10 -3
- package/ts_build/src/auth/keyAuth.js.map +1 -1
- package/ts_build/src/auth/registerIdentity.d.ts +7 -0
- package/ts_build/src/auth/registerIdentity.js +62 -0
- package/ts_build/src/auth/registerIdentity.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 +2 -0
- package/ts_build/src/chat/modules/AgentModule.js +27 -7
- package/ts_build/src/chat/modules/AgentModule.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 +23 -1
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agent.js +1 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +1 -0
- package/ts_build/src/commands/agents.js +43 -8
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/misc.js +12 -1
- 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 +282 -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 +167 -0
- package/ts_build/src/processes/managedProcessShim.js.map +1 -0
- package/ts_build/src/processors/Base64ImageDetector.d.ts +1 -0
- package/ts_build/src/processors/Base64ImageDetector.js +19 -2
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/processors/JsonCompressor.js +7 -0
- package/ts_build/src/processors/JsonCompressor.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.d.ts +1 -1
- package/ts_build/src/processors/TokenCompressor.js +9 -8
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +1 -1
- package/ts_build/src/services/AgentService.js.map +1 -1
- 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/computerUse.d.ts +3 -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/clients/AIClient.test.js +73 -11
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/processes/managedProcessShim.test.d.ts +1 -0
- package/ts_build/tests/processes/managedProcessShim.test.js +92 -0
- package/ts_build/tests/processes/managedProcessShim.test.js.map +1 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js +18 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +15 -0
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/unit/auth/registerIdentity.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/registerIdentity.test.js +86 -0
- package/ts_build/tests/unit/auth/registerIdentity.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/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,
|
|
@@ -138,7 +152,9 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
138
152
|
// Default filesystem synchronization ON unless the caller explicitly opts out
|
|
139
153
|
// (syncFs: false) or is using a messageId-based web sync. This ensures spawned
|
|
140
154
|
// subagents always appear in `knowhow agents list` and can be attached/tailed.
|
|
141
|
-
|
|
155
|
+
// If the caller explicitly passes syncFs: true, honor it even when a messageId
|
|
156
|
+
// is also provided (messageId handles remote sync; syncFs handles local fs sync).
|
|
157
|
+
const useSyncFs = syncFs === true ? true : (syncFs !== false && !messageId);
|
|
142
158
|
|
|
143
159
|
// Use provided taskId if given, otherwise generate one from the prompt
|
|
144
160
|
const taskId = providedTaskId ?? generateTaskId(prompt);
|
|
@@ -161,7 +177,8 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
161
177
|
|
|
162
178
|
if (messageId) {
|
|
163
179
|
args.push("--message-id", messageId);
|
|
164
|
-
}
|
|
180
|
+
}
|
|
181
|
+
if (useSyncFs) {
|
|
165
182
|
args.push("--sync-fs");
|
|
166
183
|
}
|
|
167
184
|
// When syncRemote is requested, pass it through so the spawned agent pushes
|
|
@@ -235,43 +252,54 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
235
252
|
child.stdin!.write(promptWithCaps, "utf8");
|
|
236
253
|
child.stdin!.end();
|
|
237
254
|
|
|
238
|
-
return new Promise<
|
|
255
|
+
return new Promise<StartAgentTaskResult>((resolve) => {
|
|
239
256
|
let settled = false;
|
|
240
|
-
|
|
257
|
+
let exited = false;
|
|
258
|
+
const done = (result: StartAgentTaskResult) => {
|
|
241
259
|
if (settled) return;
|
|
242
260
|
settled = true;
|
|
243
261
|
try { fs.closeSync(fd); } catch {}
|
|
244
|
-
resolve(
|
|
262
|
+
resolve(result);
|
|
245
263
|
};
|
|
246
264
|
|
|
247
265
|
child.once("error", (e) => {
|
|
248
|
-
done(
|
|
266
|
+
done({
|
|
267
|
+
success: false, status: "failed", taskId, pid, syncFs: useSyncFs,
|
|
268
|
+
syncReady: false, agentDir: useSyncFs ? agentTaskDir : undefined,
|
|
269
|
+
logPath, parentTaskId, error: String(e),
|
|
270
|
+
});
|
|
249
271
|
});
|
|
250
272
|
|
|
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
273
|
child.once("exit", (code) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
exited = true;
|
|
275
|
+
done({
|
|
276
|
+
success: code === 0, status: code === 0 ? "completed" : "failed",
|
|
277
|
+
taskId, pid, syncFs: useSyncFs,
|
|
278
|
+
syncReady: useSyncFs && fs.existsSync(path.join(agentTaskDir, "metadata.json")),
|
|
279
|
+
agentDir: useSyncFs ? agentTaskDir : undefined, logPath, parentTaskId,
|
|
280
|
+
exitCode: code, ...(code === 0 ? {} : { error: `Agent exited with code ${code}` }),
|
|
281
|
+
});
|
|
262
282
|
});
|
|
263
283
|
|
|
264
284
|
if (!waitForCompletion) {
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
285
|
+
// A spawn is not the same as an initialized fs-sync agent. Poll briefly
|
|
286
|
+
// so callers can distinguish those states without parsing log prose.
|
|
287
|
+
const deadline = Date.now() + (useSyncFs ? 5000 : 100);
|
|
288
|
+
const acknowledge = () => {
|
|
289
|
+
if (settled || exited) return;
|
|
290
|
+
const syncReady = useSyncFs && fs.existsSync(path.join(agentTaskDir, "metadata.json"));
|
|
291
|
+
if (!useSyncFs || syncReady || Date.now() >= deadline) {
|
|
292
|
+
try { child.unref(); } catch {}
|
|
293
|
+
done({
|
|
294
|
+
success: true, status: "started", taskId, pid, syncFs: useSyncFs,
|
|
295
|
+
syncReady, agentDir: useSyncFs ? agentTaskDir : undefined,
|
|
296
|
+
logPath, parentTaskId,
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
setTimeout(acknowledge, 100);
|
|
301
|
+
};
|
|
302
|
+
setTimeout(acknowledge, 50);
|
|
275
303
|
}
|
|
276
304
|
});
|
|
277
305
|
}
|
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) {
|
package/src/auth/keyAuth.ts
CHANGED
|
@@ -81,7 +81,7 @@ export async function authenticateWithKey(
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/** Register an Ed25519
|
|
84
|
+
/** Register an Ed25519 identity; the backend derives its scope from the source JWT. */
|
|
85
85
|
export async function registerPublicKey(
|
|
86
86
|
jwt: string,
|
|
87
87
|
publicKeyBase64: string,
|
|
@@ -109,7 +109,13 @@ export async function registerPublicKey(
|
|
|
109
109
|
|
|
110
110
|
export function storeJwt(jwt: string): void {
|
|
111
111
|
const directory = path.dirname(JWT_FILE_PATH);
|
|
112
|
-
|
|
113
|
-
fs.
|
|
114
|
-
|
|
112
|
+
const temporaryPath = `${JWT_FILE_PATH}.${process.pid}.tmp`;
|
|
113
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(temporaryPath, jwt, { mode: 0o600, flag: "wx" });
|
|
116
|
+
fs.renameSync(temporaryPath, JWT_FILE_PATH);
|
|
117
|
+
fs.chmodSync(JWT_FILE_PATH, 0o600);
|
|
118
|
+
} finally {
|
|
119
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
120
|
+
}
|
|
115
121
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getOrCreatePublicKey } from "./keyManager";
|
|
4
|
+
import { registerPublicKey, exchangePublicKeyJwt, storeJwt } from "./keyAuth";
|
|
5
|
+
import { getConfig, updateConfig } from "../config";
|
|
6
|
+
|
|
7
|
+
export interface RegisterIdentityOptions {
|
|
8
|
+
/** Optional Ed25519 private-key path. Defaults to ~/.knowhow/keys/id_ed25519. */
|
|
9
|
+
identityPath?: string;
|
|
10
|
+
/** API base URL. Defaults to KNOWHOW_API_URL or the production public API. */
|
|
11
|
+
apiUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Decode an unverified JWT payload for local routing information only. */
|
|
15
|
+
export function decodeJwtPayload(jwt: string): Record<string, unknown> {
|
|
16
|
+
const parts = jwt.split(".");
|
|
17
|
+
if (parts.length !== 3) throw new Error("Invalid JWT format: expected 3 parts");
|
|
18
|
+
const base64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
19
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
20
|
+
try {
|
|
21
|
+
const payload: unknown = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
22
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error();
|
|
23
|
+
return payload as Record<string, unknown>;
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error("Invalid JWT payload: could not parse JSON");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function extractOrgIdFromJwt(jwt: string): string {
|
|
30
|
+
const payload = decodeJwtPayload(jwt);
|
|
31
|
+
const orgId = payload["org"] ?? payload["orgId"] ?? payload["org_id"] ?? payload["organizationId"];
|
|
32
|
+
if (typeof orgId !== "string" || !orgId) {
|
|
33
|
+
throw new Error("JWT does not contain an organization claim (expected org, orgId, org_id, or organizationId)");
|
|
34
|
+
}
|
|
35
|
+
return orgId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a CLI identity using the JWT already stored in the current project's
|
|
40
|
+
* .knowhow/.jwt, then replace that JWT with one obtained via key authentication.
|
|
41
|
+
* This is intentionally generic: authorization scope is derived by the backend
|
|
42
|
+
* from the verified source JWT.
|
|
43
|
+
*/
|
|
44
|
+
export async function registerIdentity(options: RegisterIdentityOptions = {}): Promise<void> {
|
|
45
|
+
const apiUrl = options.apiUrl ?? process.env.KNOWHOW_API_URL ?? "https://api.knowhow.tyvm.ai";
|
|
46
|
+
const jwtPath = path.join(process.cwd(), ".knowhow", ".jwt");
|
|
47
|
+
if (!fs.existsSync(jwtPath)) throw new Error(`JWT file not found: ${jwtPath}`);
|
|
48
|
+
const sourceJwt = fs.readFileSync(jwtPath, "utf8").trim();
|
|
49
|
+
if (!sourceJwt) throw new Error(`JWT file is empty: ${jwtPath}`);
|
|
50
|
+
|
|
51
|
+
const orgId = extractOrgIdFromJwt(sourceJwt);
|
|
52
|
+
const keyPair = getOrCreatePublicKey(options.identityPath);
|
|
53
|
+
await registerPublicKey(sourceJwt, keyPair.publicKeyBase64, apiUrl);
|
|
54
|
+
const identityJwt = await exchangePublicKeyJwt(orgId, apiUrl, options.identityPath);
|
|
55
|
+
storeJwt(identityJwt);
|
|
56
|
+
|
|
57
|
+
const config = await getConfig();
|
|
58
|
+
if (!config.modelProviders) config.modelProviders = [];
|
|
59
|
+
if (!config.modelProviders.some((provider: { provider: string }) => provider.provider === "knowhow")) {
|
|
60
|
+
config.modelProviders.push({ provider: "knowhow" });
|
|
61
|
+
}
|
|
62
|
+
config.orgId = orgId;
|
|
63
|
+
config.cliIdentityPath = path.resolve(options.identityPath ?? keyPair.privateKeyPath);
|
|
64
|
+
await updateConfig(config);
|
|
65
|
+
|
|
66
|
+
console.log(`✅ Identity registered. Identity: ${keyPair.fingerprint}, Org: ${orgId}`);
|
|
67
|
+
}
|
|
@@ -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) {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
TaskRegistry,
|
|
9
9
|
SyncedAgentWatcher,
|
|
10
10
|
SyncerService,
|
|
11
|
+
AgentService,
|
|
11
12
|
} from "../../services/index";
|
|
12
13
|
import { AttachableAgent } from "../../services/SyncedAgentWatcher";
|
|
13
14
|
import * as fs from "fs";
|
|
@@ -77,10 +78,15 @@ export class AgentModule extends BaseChatModule {
|
|
|
77
78
|
| { toolCall?: string; toolUsed?: string; agentSay?: string; done: string }
|
|
78
79
|
| undefined;
|
|
79
80
|
|
|
81
|
+
private agentService: AgentService;
|
|
82
|
+
private eventService: EventService;
|
|
83
|
+
|
|
80
84
|
constructor() {
|
|
81
85
|
super();
|
|
82
86
|
this.taskRegistry = new TaskRegistry();
|
|
83
87
|
this.sessionManager = new SessionManager();
|
|
88
|
+
this.agentService = services().Agents;
|
|
89
|
+
this.eventService = services().Events;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
@@ -327,7 +333,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
327
333
|
await super.initialize(service);
|
|
328
334
|
|
|
329
335
|
// Set up plugin log event handler - use setListener so re-init doesn't double-subscribe
|
|
330
|
-
const Events =
|
|
336
|
+
const Events = this.eventService;
|
|
331
337
|
Events.setListener(
|
|
332
338
|
{ key: "agentModule:pluginLog", event: Events.eventTypes.pluginLog },
|
|
333
339
|
(logEvent: any) => {
|
|
@@ -405,7 +411,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
405
411
|
// Set selected agent in context and enable agent mode
|
|
406
412
|
if (context) {
|
|
407
413
|
// Create a temporary agent instance to read its default model/provider
|
|
408
|
-
const agentContext =
|
|
414
|
+
const agentContext = this.agentService.getAgentContext();
|
|
409
415
|
const tempAgent = createAgent(
|
|
410
416
|
agentName as AgentName,
|
|
411
417
|
agentContext
|
|
@@ -427,6 +433,25 @@ export class AgentModule extends BaseChatModule {
|
|
|
427
433
|
this.chatService.setMode("agent");
|
|
428
434
|
}
|
|
429
435
|
|
|
436
|
+
console.log(
|
|
437
|
+
`Agent mode enabled. Selected agent: ${agentName}. Type your task to get started.`
|
|
438
|
+
);
|
|
439
|
+
} else if (this.agentService.listAgents().includes(agentName)) {
|
|
440
|
+
// Config agent from knowhow.json
|
|
441
|
+
if (context) {
|
|
442
|
+
const configAgent = this.agentService.getAgent<BaseAgent>(agentName);
|
|
443
|
+
context.selectedAgent = configAgent;
|
|
444
|
+
context.agentMode = true;
|
|
445
|
+
context.currentAgent = agentName;
|
|
446
|
+
const clientInfo = await configAgent.clientService.getClient(
|
|
447
|
+
undefined,
|
|
448
|
+
configAgent.getModel()
|
|
449
|
+
);
|
|
450
|
+
context.currentModel = clientInfo.model;
|
|
451
|
+
context.currentProvider = clientInfo.provider;
|
|
452
|
+
|
|
453
|
+
this.chatService.setMode("agent");
|
|
454
|
+
}
|
|
430
455
|
console.log(
|
|
431
456
|
`Agent mode enabled. Selected agent: ${agentName}. Type your task to get started.`
|
|
432
457
|
);
|
|
@@ -601,12 +626,16 @@ export class AgentModule extends BaseChatModule {
|
|
|
601
626
|
*/
|
|
602
627
|
async handleAgentsCommand(args: string[]): Promise<void> {
|
|
603
628
|
try {
|
|
604
|
-
const
|
|
629
|
+
const builtinNames = Object.keys(agentConstructors);
|
|
630
|
+
const configAgentNames = this.agentService.listAgents();
|
|
631
|
+
// Merge, deduplicate, preserving built-ins first
|
|
632
|
+
const agentNames = [...new Set([...builtinNames, ...configAgentNames])];
|
|
605
633
|
|
|
606
634
|
if (agentNames.length > 0) {
|
|
607
635
|
console.log("\nAvailable agents:");
|
|
608
636
|
agentNames.forEach((name) => {
|
|
609
|
-
|
|
637
|
+
const isConfig = !builtinNames.includes(name);
|
|
638
|
+
console.log(` - ${name}${isConfig ? " (config)" : ""}`);
|
|
610
639
|
});
|
|
611
640
|
console.log("─".repeat(80), "\n");
|
|
612
641
|
|
|
@@ -700,11 +729,14 @@ export class AgentModule extends BaseChatModule {
|
|
|
700
729
|
const agentName = session.agentName || context.currentAgent;
|
|
701
730
|
const previousAgentMode = context?.agentMode;
|
|
702
731
|
|
|
703
|
-
if (!agentName
|
|
732
|
+
if (!agentName) {
|
|
704
733
|
console.error(`Agent ${agentName} not found.`);
|
|
705
734
|
return;
|
|
706
735
|
}
|
|
707
736
|
|
|
737
|
+
// setupAgent resolves both built-in agents and custom ConfigAgents. Do
|
|
738
|
+
// not reject config-defined agents here before it gets that opportunity.
|
|
739
|
+
|
|
708
740
|
// Start agent with Knowhow task context and restored message history
|
|
709
741
|
const remoteTaskId = asRemoteTaskId(session.knowhowTaskId);
|
|
710
742
|
const { agent, taskId } = await this.setupAgent({
|
|
@@ -817,7 +849,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
817
849
|
enabledTools?: string[];
|
|
818
850
|
}) {
|
|
819
851
|
const { input, chatHistory = [], agentName } = options;
|
|
820
|
-
const agentContext =
|
|
852
|
+
const agentContext = this.agentService.getAgentContext();
|
|
821
853
|
|
|
822
854
|
// Resolve agent: built-in agents first, then fall back to ConfigAgents from knowhow.json
|
|
823
855
|
let agent: BaseAgent;
|
|
@@ -826,7 +858,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
826
858
|
} else {
|
|
827
859
|
// Try to find a ConfigAgent registered via loadAgentsFromConfig (from knowhow.json agents[])
|
|
828
860
|
try {
|
|
829
|
-
const configAgent =
|
|
861
|
+
const configAgent = this.agentService.getAgent(options.agentName);
|
|
830
862
|
agent = configAgent as unknown as BaseAgent;
|
|
831
863
|
} catch {
|
|
832
864
|
throw new Error(
|
|
@@ -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();
|