@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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import {
|
|
5
|
+
getProcessesDir, listManagedProcesses, ManagedProcess,
|
|
6
|
+
ManagedProcessInfo, processDirectory,
|
|
7
|
+
} from "../processes/ProcessManager";
|
|
8
|
+
import { runManagedProcessShim } from "../processes/managedProcessShim";
|
|
9
|
+
|
|
10
|
+
const terminal = new Set(["exited", "failed", "stopped"]);
|
|
11
|
+
function resolveProcess(value: string): ManagedProcess {
|
|
12
|
+
const root = getProcessesDir();
|
|
13
|
+
const matches = listManagedProcesses(root).filter((p) => p.id === value || p.id.includes(value));
|
|
14
|
+
if (!matches.length) throw new Error(`Managed process not found: ${value}`);
|
|
15
|
+
const exact = matches.find((p) => p.id === value);
|
|
16
|
+
if (!exact && matches.length > 1) throw new Error(`Ambiguous process id '${value}': ${matches.map((p) => p.id).join(", ")}`);
|
|
17
|
+
const item = exact || matches[0];
|
|
18
|
+
return new ManagedProcess(item.id, item.directory);
|
|
19
|
+
}
|
|
20
|
+
function commandOptions(options: any, command?: Command): any {
|
|
21
|
+
const fromOptions = typeof options?.optsWithGlobals === "function" ? options.optsWithGlobals() : options || {};
|
|
22
|
+
const fromCommand = typeof command?.optsWithGlobals === "function" ? command.optsWithGlobals() : {};
|
|
23
|
+
return { ...fromOptions, ...fromCommand };
|
|
24
|
+
}
|
|
25
|
+
function filtered(options: any): ManagedProcessInfo[] {
|
|
26
|
+
return listManagedProcesses().filter((item) => {
|
|
27
|
+
if (!options.all && terminal.has(item.status.state)) return false;
|
|
28
|
+
if (options.parent && item.status.parentProcessId !== options.parent) return false;
|
|
29
|
+
if (options.background !== undefined && item.status.background !== options.background) return false;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function printList(options: any): void {
|
|
34
|
+
const rows = filtered(options).map(({ id, status }) => ({
|
|
35
|
+
id, state: status.state, pid: status.pid || "-", background: status.background ? "yes" : "no",
|
|
36
|
+
parent: status.parentProcessId || "-", command: [status.command, ...status.args].join(" "),
|
|
37
|
+
}));
|
|
38
|
+
if (options.json) console.log(JSON.stringify(rows, null, 2));
|
|
39
|
+
else if (rows.length) console.table(rows);
|
|
40
|
+
else console.log("No managed processes.");
|
|
41
|
+
}
|
|
42
|
+
function addFilters(command: Command): Command {
|
|
43
|
+
return command.option("-a, --all", "include completed processes")
|
|
44
|
+
.option("--parent <id>", "filter by parent process/task id")
|
|
45
|
+
.option("--background", "show only background processes")
|
|
46
|
+
.option("--json", "emit JSON");
|
|
47
|
+
}
|
|
48
|
+
function tailText(file: string, lines: number): string {
|
|
49
|
+
if (!fs.existsSync(file)) return "";
|
|
50
|
+
return fs.readFileSync(file, "utf8").split("\n").slice(-lines - 1).join("\n");
|
|
51
|
+
}
|
|
52
|
+
async function followFiles(files: string[], fromEnd = true): Promise<void> {
|
|
53
|
+
const offsets = new Map<string, number>();
|
|
54
|
+
for (const file of files) offsets.set(file, fromEnd && fs.existsSync(file) ? fs.statSync(file).size : 0);
|
|
55
|
+
await new Promise<void>((resolve) => {
|
|
56
|
+
const poll = setInterval(() => {
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
if (!fs.existsSync(file)) continue;
|
|
59
|
+
const size = fs.statSync(file).size; const offset = offsets.get(file) || 0;
|
|
60
|
+
if (size < offset) offsets.set(file, 0);
|
|
61
|
+
if (size > (offsets.get(file) || 0)) {
|
|
62
|
+
const start = offsets.get(file) || 0;
|
|
63
|
+
const fd = fs.openSync(file, "r"); const data = Buffer.alloc(size - start);
|
|
64
|
+
fs.readSync(fd, data, 0, data.length, start); fs.closeSync(fd);
|
|
65
|
+
process.stdout.write(data); offsets.set(file, size);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, 150);
|
|
69
|
+
const done = () => { clearInterval(poll); resolve(); };
|
|
70
|
+
process.once("SIGINT", done); process.once("SIGTERM", done);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function addProcessesCommand(program: Command): void {
|
|
75
|
+
const processes = addFilters(program.command("processes").description("inspect and control managed processes"));
|
|
76
|
+
processes.action((rawOptions, command) => printList(commandOptions(rawOptions, command)));
|
|
77
|
+
addFilters(processes.command("list").description("list managed processes"))
|
|
78
|
+
.action((rawOptions, command) => printList(commandOptions(rawOptions, command)));
|
|
79
|
+
|
|
80
|
+
processes.command("run").description("internal managed process shim").requiredOption("--config <path>")
|
|
81
|
+
.action(async ({ config }) => runManagedProcessShim(path.resolve(config)));
|
|
82
|
+
|
|
83
|
+
processes.command("logs <id>").alias("tail").option("-n, --lines <number>", "lines to show", "100")
|
|
84
|
+
.option("-f, --follow", "follow output").option("--stderr", "show stderr instead of stdout")
|
|
85
|
+
.option("--both", "show stdout and stderr").action(async (id, rawOptions, command) => {
|
|
86
|
+
const options = commandOptions(rawOptions, command);
|
|
87
|
+
const managed = resolveProcess(id);
|
|
88
|
+
const files = options.both ? [managed.stdoutPath, managed.stderrPath] : [options.stderr ? managed.stderrPath : managed.stdoutPath];
|
|
89
|
+
for (const file of files) process.stdout.write(tailText(file, Math.max(0, Number(options.lines) || 100)));
|
|
90
|
+
if (options.follow) await followFiles(files);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
processes.command("send <id> [input...]").option("-n, --no-newline", "do not append newline")
|
|
94
|
+
.action(async (id, input: string[], rawOptions, command) => {
|
|
95
|
+
const options = commandOptions(rawOptions, command);
|
|
96
|
+
const managed = resolveProcess(id);
|
|
97
|
+
await managed.write((input || []).join(" ") + (options.newline ? "\n" : ""));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
processes.command("signal <id> [signal]").action((id, signal = "SIGTERM") => resolveProcess(id).signal(signal.toUpperCase() as NodeJS.Signals));
|
|
101
|
+
processes.command("stop <id>").action((id) => resolveProcess(id).signal("SIGTERM"));
|
|
102
|
+
processes.command("kill <id>").action((id) => resolveProcess(id).signal("SIGKILL"));
|
|
103
|
+
|
|
104
|
+
processes.command("attach <id>").description("attach terminal (Ctrl-] to detach)").action(async (id) => {
|
|
105
|
+
const managed = resolveProcess(id); let detached = false;
|
|
106
|
+
const raw = !!process.stdin.isTTY; if (raw) process.stdin.setRawMode(true);
|
|
107
|
+
process.stdin.resume();
|
|
108
|
+
const onData = async (data: Buffer) => {
|
|
109
|
+
if (data.includes(0x1d)) { detached = true; return; }
|
|
110
|
+
try { await managed.write(data.toString()); } catch (error) { console.error(String(error)); detached = true; }
|
|
111
|
+
};
|
|
112
|
+
process.stdin.on("data", onData);
|
|
113
|
+
const offsets = [managed.stdoutPath, managed.stderrPath].map((f) => fs.existsSync(f) ? fs.statSync(f).size : 0);
|
|
114
|
+
await new Promise<void>((resolve) => {
|
|
115
|
+
const timer = setInterval(() => {
|
|
116
|
+
[managed.stdoutPath, managed.stderrPath].forEach((file, i) => {
|
|
117
|
+
if (!fs.existsSync(file)) return; const size = fs.statSync(file).size;
|
|
118
|
+
if (size > offsets[i]) { const fd = fs.openSync(file, "r"); const b = Buffer.alloc(size - offsets[i]); fs.readSync(fd, b, 0, b.length, offsets[i]); fs.closeSync(fd); process.stdout.write(b); offsets[i] = size; }
|
|
119
|
+
});
|
|
120
|
+
if (detached || terminal.has(managed.status.state)) { clearInterval(timer); resolve(); }
|
|
121
|
+
}, 100);
|
|
122
|
+
});
|
|
123
|
+
process.stdin.off("data", onData); process.stdin.pause(); if (raw) process.stdin.setRawMode(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
processes.command("tree").option("-a, --all", "include completed processes").action((rawOptions, command) => {
|
|
127
|
+
const options = commandOptions(rawOptions, command);
|
|
128
|
+
const items = listManagedProcesses().filter((p) => options.all || !terminal.has(p.status.state));
|
|
129
|
+
const ids = new Set(items.map((p) => p.id));
|
|
130
|
+
const children = new Map<string | null, ManagedProcessInfo[]>();
|
|
131
|
+
for (const item of items) { const parent = item.status.parentProcessId && ids.has(item.status.parentProcessId) ? item.status.parentProcessId : null; children.set(parent, [...(children.get(parent) || []), item]); }
|
|
132
|
+
const render = (parent: string | null, prefix: string) => (children.get(parent) || []).forEach((item, index, array) => {
|
|
133
|
+
const last = index === array.length - 1; console.log(`${prefix}${last ? "└─" : "├─"}${item.id} [${item.status.state}] ${item.status.command}`);
|
|
134
|
+
render(item.id, prefix + (last ? " " : "│ "));
|
|
135
|
+
});
|
|
136
|
+
render(null, "");
|
|
137
|
+
});
|
|
138
|
+
}
|
package/src/commands/services.ts
CHANGED
|
@@ -3,7 +3,8 @@ import * as allTools from "../agents/tools";
|
|
|
3
3
|
import { LazyToolsService, services, MinimalToolsService, TracingService } from "../services";
|
|
4
4
|
import { agents } from "../agents";
|
|
5
5
|
import { ModulesService } from "../services/modules";
|
|
6
|
-
import { getConfig } from "../config";
|
|
6
|
+
import { getConfig, getConfigSync } from "../config";
|
|
7
|
+
import { authenticateWithKey } from "../auth/keyAuth";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Shared service setup used by commands that need full services (chat, agent, worker, etc.)
|
|
@@ -20,6 +21,7 @@ export async function setupServices() {
|
|
|
20
21
|
MediaProcessor,
|
|
21
22
|
Behaviors,
|
|
22
23
|
RuntimeReload,
|
|
24
|
+
Extensions,
|
|
23
25
|
} = services();
|
|
24
26
|
|
|
25
27
|
await Plugins.refreshConfiguredState();
|
|
@@ -76,6 +78,25 @@ export async function setupServices() {
|
|
|
76
78
|
`⚠ Some MCP servers failed to connect (continuing without them): ${msg}`
|
|
77
79
|
);
|
|
78
80
|
}
|
|
81
|
+
// Attempt silent JWT refresh via SSH identity before registering models.
|
|
82
|
+
// This handles the case where the JWT has expired but the user has a valid
|
|
83
|
+
// SSH identity registered — they shouldn't need to run `knowhow login` again
|
|
84
|
+
// unless they want to switch organizations.
|
|
85
|
+
try {
|
|
86
|
+
const startupConfig = getConfigSync();
|
|
87
|
+
if (startupConfig.orgId && !process.env.KNOWHOW_JWT) {
|
|
88
|
+
await authenticateWithKey(
|
|
89
|
+
startupConfig.orgId,
|
|
90
|
+
process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai",
|
|
91
|
+
startupConfig.cliIdentityPath
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} catch (_err) {
|
|
95
|
+
// Non-fatal: if SSH refresh fails (no key, revoked, network issue), we
|
|
96
|
+
// continue with the existing JWT (or no JWT). The user will see model
|
|
97
|
+
// warnings if no valid JWT is available.
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
console.log("Connecting to clients...");
|
|
80
101
|
await Clients.registerConfiguredModels();
|
|
81
102
|
console.log("✓ Services are set up and ready to go!");
|
|
@@ -92,6 +113,7 @@ export async function setupServices() {
|
|
|
92
113
|
Behaviors,
|
|
93
114
|
Events,
|
|
94
115
|
Tracing: TracingService,
|
|
116
|
+
Extensions,
|
|
95
117
|
};
|
|
96
118
|
|
|
97
119
|
// Call destroy() on all modules when the process is shutting down so they
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ export * as embeddings from "./embeddings";
|
|
|
65
65
|
export * as types from "./types";
|
|
66
66
|
export * as processors from "./processors";
|
|
67
67
|
export * as ai from "./ai";
|
|
68
|
+
export * from "./processes/ProcessManager";
|
|
68
69
|
|
|
69
70
|
// Export module system types for external modules
|
|
70
71
|
export * from "./services/modules/types";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
|
|
6
|
+
export type ManagedProcessState = "starting" | "running" | "exited" | "failed" | "stopped";
|
|
7
|
+
export interface ManagedProcessStatus {
|
|
8
|
+
id: string; pid: number | null; shimPid: number | null; state: ManagedProcessState;
|
|
9
|
+
command: string; args: string[]; cwd: string; parentProcessId: string | null;
|
|
10
|
+
parentPid: number | null; background: boolean; startedAt: string; endedAt: string | null;
|
|
11
|
+
exitCode: number | null; signal: string | null; error?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SpawnManagedOptions {
|
|
14
|
+
id?: string; cwd?: string; env?: NodeJS.ProcessEnv; background?: boolean;
|
|
15
|
+
parentProcessId?: string; parentPid?: number; shell?: boolean; processesDir?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ManagedProcessInfo { id: string; directory: string; status: ManagedProcessStatus; }
|
|
18
|
+
|
|
19
|
+
export function getProcessesDir(cwd = process.cwd()): string {
|
|
20
|
+
return process.env.KNOWHOW_PROCESSES_DIR || path.join(cwd, ".knowhow", "processes");
|
|
21
|
+
}
|
|
22
|
+
export function processDirectory(id: string, processesDir = getProcessesDir()): string {
|
|
23
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(id)) throw new Error(`Invalid process id: ${id}`);
|
|
24
|
+
return path.join(processesDir, id);
|
|
25
|
+
}
|
|
26
|
+
export function readManagedStatus(id: string, processesDir = getProcessesDir()): ManagedProcessStatus {
|
|
27
|
+
return JSON.parse(fs.readFileSync(path.join(processDirectory(id, processesDir), "status.json"), "utf8"));
|
|
28
|
+
}
|
|
29
|
+
export function listManagedProcesses(processesDir = getProcessesDir()): ManagedProcessInfo[] {
|
|
30
|
+
if (!fs.existsSync(processesDir)) return [];
|
|
31
|
+
return fs.readdirSync(processesDir, { withFileTypes: true })
|
|
32
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(processesDir, entry.name, "status.json")))
|
|
33
|
+
.flatMap((entry) => { try { return [{ id: entry.name, directory: path.join(processesDir, entry.name), status: readManagedStatus(entry.name, processesDir) }]; } catch { return []; } })
|
|
34
|
+
.sort((a, b) => b.status.startedAt.localeCompare(a.status.startedAt));
|
|
35
|
+
}
|
|
36
|
+
function makeId(command: string): string {
|
|
37
|
+
const name = path.basename(command).replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 28) || "process";
|
|
38
|
+
return `${name}-${Date.now()}-${randomBytes(3).toString("hex")}`;
|
|
39
|
+
}
|
|
40
|
+
function waitForFile(file: string, timeoutMs: number): Promise<void> {
|
|
41
|
+
const started = Date.now();
|
|
42
|
+
return new Promise((resolve, reject) => { const check = () => {
|
|
43
|
+
if (fs.existsSync(file)) return resolve();
|
|
44
|
+
if (Date.now() - started >= timeoutMs) return reject(new Error(`Managed process did not initialize: ${file}`));
|
|
45
|
+
setTimeout(check, 20);
|
|
46
|
+
}; check(); });
|
|
47
|
+
}
|
|
48
|
+
export class ManagedProcess {
|
|
49
|
+
constructor(public readonly id: string, public readonly directory: string) {}
|
|
50
|
+
get status(): ManagedProcessStatus { return readManagedStatus(this.id, path.dirname(this.directory)); }
|
|
51
|
+
get stdoutPath(): string { return path.join(this.directory, "stdout"); }
|
|
52
|
+
get stderrPath(): string { return path.join(this.directory, "stderr"); }
|
|
53
|
+
get inputLogPath(): string { return path.join(this.directory, "input.log"); }
|
|
54
|
+
get stdinPath(): string { return path.join(this.directory, "stdin"); }
|
|
55
|
+
async write(input: string): Promise<void> {
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
await fs.promises.appendFile(this.stdinPath, input);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// A non-blocking open fails promptly with ENXIO if the shim is gone instead
|
|
61
|
+
// of leaving callers hung forever waiting for a FIFO reader.
|
|
62
|
+
const handle = await fs.promises.open(this.stdinPath, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK);
|
|
63
|
+
try { await handle.writeFile(input); } finally { await handle.close(); }
|
|
64
|
+
}
|
|
65
|
+
signal(signal: NodeJS.Signals = "SIGTERM"): void {
|
|
66
|
+
const status = this.status;
|
|
67
|
+
if (!status.pid) throw new Error(`Process ${this.id} has no active pid`);
|
|
68
|
+
try { process.kill(-status.pid, signal); } catch { process.kill(status.pid, signal); }
|
|
69
|
+
}
|
|
70
|
+
async wait(timeoutMs?: number): Promise<ManagedProcessStatus> {
|
|
71
|
+
const started = Date.now();
|
|
72
|
+
while (true) {
|
|
73
|
+
const status = this.status;
|
|
74
|
+
if (!["starting", "running"].includes(status.state)) return status;
|
|
75
|
+
if (timeoutMs !== undefined && Date.now() - started >= timeoutMs) throw new Error(`Timed out waiting for managed process ${this.id} after ${timeoutMs}ms`);
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function spawnManaged(command: string, args: string[] = [], options: SpawnManagedOptions = {}): Promise<ManagedProcess> {
|
|
81
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
82
|
+
const processesDir = options.processesDir || getProcessesDir(cwd);
|
|
83
|
+
const id = options.id || makeId(command);
|
|
84
|
+
const directory = processDirectory(id, processesDir);
|
|
85
|
+
fs.mkdirSync(processesDir, { recursive: true });
|
|
86
|
+
fs.mkdirSync(directory);
|
|
87
|
+
const configPath = path.join(directory, "launch.json");
|
|
88
|
+
fs.writeFileSync(configPath, JSON.stringify({ id, command, args, cwd, env: options.env || {}, background: options.background === true, parentProcessId: options.parentProcessId || null, parentPid: options.parentPid ?? process.pid, shell: options.shell === true, processesDir }, null, 2));
|
|
89
|
+
const cliPath = path.resolve(__dirname, "..", "cli.js");
|
|
90
|
+
const shim = spawn(process.execPath, [cliPath, "processes", "run", "--config", configPath], { cwd, detached: true, stdio: "ignore", env: process.env });
|
|
91
|
+
shim.unref();
|
|
92
|
+
try { await waitForFile(path.join(directory, "status.json"), 5000); }
|
|
93
|
+
catch (error) { try { fs.rmSync(directory, { recursive: true, force: true }); } catch {} throw error; }
|
|
94
|
+
return new ManagedProcess(id, directory);
|
|
95
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { spawn, spawnSync, ChildProcess } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { ManagedProcessStatus } from "./ProcessManager";
|
|
5
|
+
|
|
6
|
+
interface LaunchConfig {
|
|
7
|
+
id: string; command: string; args: string[]; cwd: string; env: NodeJS.ProcessEnv;
|
|
8
|
+
background: boolean; parentProcessId: string | null; parentPid: number | null;
|
|
9
|
+
shell: boolean; processesDir: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function atomicJson(file: string, value: unknown): void {
|
|
13
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
14
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2));
|
|
15
|
+
fs.renameSync(tmp, file);
|
|
16
|
+
}
|
|
17
|
+
function alive(pid: number): boolean {
|
|
18
|
+
try { process.kill(pid, 0); return true; } catch (error: any) { return error?.code === "EPERM"; }
|
|
19
|
+
}
|
|
20
|
+
function terminateGroup(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
21
|
+
if (!child.pid) return;
|
|
22
|
+
try { process.kill(-child.pid, signal); } catch { try { child.kill(signal); } catch {} }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runManagedProcessShim(configPath: string): Promise<void> {
|
|
26
|
+
const config: LaunchConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
27
|
+
const dir = path.dirname(configPath);
|
|
28
|
+
const startedAt = new Date().toISOString();
|
|
29
|
+
const statusPath = path.join(dir, "status.json");
|
|
30
|
+
const stdoutPath = path.join(dir, "stdout");
|
|
31
|
+
const stderrPath = path.join(dir, "stderr");
|
|
32
|
+
const inputLogPath = path.join(dir, "input.log");
|
|
33
|
+
const fifoPath = path.join(dir, "stdin");
|
|
34
|
+
fs.closeSync(fs.openSync(stdoutPath, "a"));
|
|
35
|
+
fs.closeSync(fs.openSync(stderrPath, "a"));
|
|
36
|
+
fs.closeSync(fs.openSync(inputLogPath, "a"));
|
|
37
|
+
|
|
38
|
+
if (process.platform !== "win32") {
|
|
39
|
+
const result = spawnSync("mkfifo", [fifoPath]);
|
|
40
|
+
if (result.status !== 0 && !fs.existsSync(fifoPath)) throw new Error(`Unable to create stdin FIFO: ${result.stderr?.toString()}`);
|
|
41
|
+
} else {
|
|
42
|
+
// Windows has no POSIX FIFO. Keep the same path as an append-only inbox fallback.
|
|
43
|
+
fs.closeSync(fs.openSync(fifoPath, "a"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let status: ManagedProcessStatus = {
|
|
47
|
+
id: config.id, pid: null, shimPid: process.pid, state: "starting", command: config.command,
|
|
48
|
+
args: config.args, cwd: config.cwd, parentProcessId: config.parentProcessId,
|
|
49
|
+
parentPid: config.parentPid, background: config.background, startedAt, endedAt: null,
|
|
50
|
+
exitCode: null, signal: null,
|
|
51
|
+
};
|
|
52
|
+
const writeStatus = () => atomicJson(statusPath, status);
|
|
53
|
+
atomicJson(path.join(dir, "metadata.json"), {
|
|
54
|
+
id: config.id, command: config.command, args: config.args, cwd: config.cwd,
|
|
55
|
+
parentProcessId: config.parentProcessId, parentPid: config.parentPid,
|
|
56
|
+
background: config.background, createdAt: startedAt, shimPid: process.pid,
|
|
57
|
+
});
|
|
58
|
+
fs.writeFileSync(path.join(dir, "pid"), "");
|
|
59
|
+
writeStatus();
|
|
60
|
+
|
|
61
|
+
const stdoutFd = fs.openSync(stdoutPath, "a");
|
|
62
|
+
const stderrFd = fs.openSync(stderrPath, "a");
|
|
63
|
+
const child = spawn(config.command, config.args, {
|
|
64
|
+
cwd: config.cwd, shell: config.shell, detached: process.platform !== "win32",
|
|
65
|
+
stdio: ["pipe", stdoutFd, stderrFd],
|
|
66
|
+
env: { ...process.env, ...config.env, KNOWHOW_MANAGED_PROCESS_ID: config.id, KNOWHOW_MANAGED_PROCESS_DIR: dir },
|
|
67
|
+
});
|
|
68
|
+
fs.closeSync(stdoutFd); fs.closeSync(stderrFd);
|
|
69
|
+
status = { ...status, pid: child.pid || null, state: "running" };
|
|
70
|
+
fs.writeFileSync(path.join(dir, "pid"), child.pid ? `${child.pid}\n` : "");
|
|
71
|
+
writeStatus();
|
|
72
|
+
|
|
73
|
+
let fifoFd: number | undefined;
|
|
74
|
+
let fifoStream: fs.ReadStream | undefined;
|
|
75
|
+
if (process.platform !== "win32") {
|
|
76
|
+
// O_RDWR prevents transient writers disconnecting from delivering EOF to the shim.
|
|
77
|
+
fifoFd = fs.openSync(fifoPath, fs.constants.O_RDWR);
|
|
78
|
+
fifoStream = fs.createReadStream("", { fd: fifoFd, autoClose: false });
|
|
79
|
+
fifoStream.on("data", (chunk) => {
|
|
80
|
+
fs.appendFileSync(inputLogPath, chunk);
|
|
81
|
+
if (child.stdin?.writable) child.stdin.write(chunk);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let stoppingForParent = false;
|
|
86
|
+
const parentTimer = !config.background && config.parentPid
|
|
87
|
+
? setInterval(() => {
|
|
88
|
+
if (!alive(config.parentPid!)) {
|
|
89
|
+
stoppingForParent = true;
|
|
90
|
+
terminateGroup(child, "SIGTERM");
|
|
91
|
+
setTimeout(() => { if (alive(child.pid || -1)) terminateGroup(child, "SIGKILL"); }, 3000).unref();
|
|
92
|
+
}
|
|
93
|
+
}, 500)
|
|
94
|
+
: undefined;
|
|
95
|
+
parentTimer?.unref();
|
|
96
|
+
|
|
97
|
+
const forward = (signal: NodeJS.Signals) => terminateGroup(child, signal);
|
|
98
|
+
process.once("SIGINT", () => forward("SIGINT"));
|
|
99
|
+
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
100
|
+
process.once("SIGHUP", () => forward("SIGHUP"));
|
|
101
|
+
|
|
102
|
+
await new Promise<void>((resolve) => {
|
|
103
|
+
child.once("error", (error) => {
|
|
104
|
+
status = { ...status, state: "failed", error: error.message, endedAt: new Date().toISOString() };
|
|
105
|
+
writeStatus(); resolve();
|
|
106
|
+
});
|
|
107
|
+
child.once("exit", (code, signal) => {
|
|
108
|
+
status = { ...status, state: stoppingForParent || signal ? "stopped" : code === 0 ? "exited" : "failed", endedAt: new Date().toISOString(), exitCode: code, signal };
|
|
109
|
+
writeStatus(); resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
if (parentTimer) clearInterval(parentTimer);
|
|
113
|
+
fifoStream?.destroy();
|
|
114
|
+
if (fifoFd !== undefined) try { fs.closeSync(fifoFd); } catch {}
|
|
115
|
+
}
|
|
@@ -124,7 +124,18 @@ export class AgentSyncFs {
|
|
|
124
124
|
|
|
125
125
|
try {
|
|
126
126
|
const metadataPath = path.join(this.taskPath, "metadata.json");
|
|
127
|
-
|
|
127
|
+
// Thread/status updates are partial snapshots. Merge them so stable
|
|
128
|
+
// orchestration identity written by createTask (taskId, pid and parent)
|
|
129
|
+
// is never discarded by a later update.
|
|
130
|
+
let existing: Record<string, unknown> = {};
|
|
131
|
+
try {
|
|
132
|
+
existing = JSON.parse(await fs.readFile(metadataPath, "utf8"));
|
|
133
|
+
} catch { /* first write or an interrupted legacy write */ }
|
|
134
|
+
await fs.writeFile(
|
|
135
|
+
metadataPath,
|
|
136
|
+
JSON.stringify({ ...existing, ...data }, null, 2),
|
|
137
|
+
"utf8"
|
|
138
|
+
);
|
|
128
139
|
} catch (error) {
|
|
129
140
|
console.error(`❌ Failed to write metadata:`, error);
|
|
130
141
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ModuleExtension {
|
|
2
|
+
type: string;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface RegisteredExtension extends ModuleExtension {
|
|
7
|
+
owner: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Process-local registry for optional, consumer-defined module capabilities. */
|
|
11
|
+
export class ExtensionsService {
|
|
12
|
+
private extensions: RegisteredExtension[] = [];
|
|
13
|
+
|
|
14
|
+
register(owner: string, extension: ModuleExtension): void {
|
|
15
|
+
this.extensions.push({ ...extension, owner });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
list<T extends ModuleExtension>(type: T["type"]): readonly T[] {
|
|
19
|
+
return this.extensions.filter((item) => item.type === type) as unknown as readonly T[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
removeOwner(owner: string): void {
|
|
23
|
+
this.extensions = this.extensions.filter((item) => item.owner !== owner);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -82,6 +82,15 @@ export class LazyToolsService extends ToolsService {
|
|
|
82
82
|
return this.tools; // Returns filtered subset
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Definitions remain resolvable even when omitted from the model prompt.
|
|
86
|
+
getTool(name: string): Tool {
|
|
87
|
+
return this.allTools.find(
|
|
88
|
+
(tool) =>
|
|
89
|
+
name &&
|
|
90
|
+
(tool.function.name === name || tool.function.name.endsWith(name))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
/** Rebuild the lazy catalog while preserving enable/disable preferences. */
|
|
86
95
|
resetTools(): void {
|
|
87
96
|
this.allTools = [];
|
|
@@ -155,32 +164,18 @@ export class LazyToolsService extends ToolsService {
|
|
|
155
164
|
};
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
//
|
|
167
|
+
// Prompt visibility is not authorization. Registered functions remain
|
|
168
|
+
// callable without changing which definitions are sent to the model.
|
|
159
169
|
async callTool(
|
|
160
170
|
toolCall: ToolCall,
|
|
161
171
|
enabledTools?: string[],
|
|
162
172
|
callContext?: ToolCallContext
|
|
163
173
|
) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// enabling it via listAvailableTools/enableTools.
|
|
169
|
-
const isCurrentlyEnabled = this.tools.some(
|
|
170
|
-
(t) => t.function.name === functionName
|
|
174
|
+
return super.callTool(
|
|
175
|
+
toolCall,
|
|
176
|
+
enabledTools ?? this.getFunctionNames(),
|
|
177
|
+
callContext
|
|
171
178
|
);
|
|
172
|
-
const existsInAll = this.allTools.some(
|
|
173
|
-
(t) => t.function.name === functionName
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
if (!isCurrentlyEnabled && existsInAll) {
|
|
177
|
-
// Auto-enable by adding the tool name as an exact pattern
|
|
178
|
-
this.enableTools([functionName]);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Always use the current enabled tool names after any auto-enable above,
|
|
182
|
-
// so the base class check sees the freshly-enabled tool in the allowed list.
|
|
183
|
-
return super.callTool(toolCall, this.getToolNames(), callContext);
|
|
184
179
|
}
|
|
185
180
|
|
|
186
181
|
// Internal: Update visible tools based on patterns
|
package/src/services/Tools.ts
CHANGED
|
@@ -135,6 +135,16 @@ export class ToolsService {
|
|
|
135
135
|
return this.tools.map((tool) => tool.function.name);
|
|
136
136
|
}
|
|
137
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
|
+
|
|
138
148
|
getTool(name: string): Tool {
|
|
139
149
|
return this.tools.find(
|
|
140
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,6 +46,7 @@ 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 {
|
|
@@ -64,6 +66,7 @@ let Singletons = {} as {
|
|
|
64
66
|
Conversion: ConversionService;
|
|
65
67
|
Behaviors: BehaviorsService;
|
|
66
68
|
RuntimeReload: RuntimeReloadService;
|
|
69
|
+
Extensions: ExtensionsService;
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
export const services = (): typeof Singletons => {
|
|
@@ -98,6 +101,7 @@ export const services = (): typeof Singletons => {
|
|
|
98
101
|
knowhowApiClient: new KnowhowSimpleClient(),
|
|
99
102
|
Behaviors,
|
|
100
103
|
RuntimeReload: new RuntimeReloadService(),
|
|
104
|
+
Extensions: new ExtensionsService(),
|
|
101
105
|
};
|
|
102
106
|
|
|
103
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);
|
|
@@ -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[];
|