@tyvm/knowhow 0.0.148 โ 0.0.150
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 +18 -9
- package/src/agents/tools/startAgentTask.ts +5 -2
- package/src/auth/keyAuth.ts +10 -4
- package/src/auth/registerIdentity.ts +67 -0
- package/src/chat/modules/AgentModule.ts +39 -7
- package/src/clients/index.ts +34 -1
- package/src/commands/agent.ts +1 -0
- package/src/commands/agents.ts +4 -1
- package/src/commands/misc.ts +17 -2
- package/src/commands/processes.ts +116 -6
- package/src/hashes.ts +2 -0
- package/src/processes/ProcessManager.ts +2 -1
- package/src/processes/managedProcessShim.ts +17 -5
- 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 +42 -0
- package/src/services/modules/computerUse.ts +3 -1
- 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/hashes.test.ts +23 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +14 -9
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.js +2 -2
- package/ts_build/src/agents/tools/startAgentTask.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/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/clients/index.js +19 -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.js +1 -1
- 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.js +125 -6
- package/ts_build/src/commands/processes.js.map +1 -1
- package/ts_build/src/hashes.js +1 -0
- package/ts_build/src/hashes.js.map +1 -1
- package/ts_build/src/processes/ProcessManager.js +2 -1
- package/ts_build/src/processes/ProcessManager.js.map +1 -1
- package/ts_build/src/processes/managedProcessShim.js +13 -7
- package/ts_build/src/processes/managedProcessShim.js.map +1 -1
- 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.d.ts +1 -0
- package/ts_build/src/services/AgentSyncFs.js +34 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/modules/computerUse.d.ts +3 -1
- 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/hashes.test.d.ts +1 -0
- package/ts_build/tests/unit/hashes.test.js +25 -0
- package/ts_build/tests/unit/hashes.test.js.map +1 -0
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -1670,8 +1670,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1670
1670
|
const response = await this.createAgentCompletion({
|
|
1671
1671
|
model,
|
|
1672
1672
|
messages: taskBreakdownMessages,
|
|
1673
|
-
|
|
1674
|
-
tool_choice: "auto",
|
|
1673
|
+
tool_choice: "none",
|
|
1675
1674
|
long_ttl_cache: this.runTime() > 300_000,
|
|
1676
1675
|
...(this.reasoningEffort !== undefined && {
|
|
1677
1676
|
reasoning_effort: this.reasoningEffort,
|
|
@@ -1681,9 +1680,17 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1681
1680
|
}),
|
|
1682
1681
|
});
|
|
1683
1682
|
|
|
1684
|
-
|
|
1683
|
+
// The model may return null content (e.g. when it responds with tool calls
|
|
1684
|
+
// instead of text). Guard against storing a literal null so that template
|
|
1685
|
+
// strings don't render "null".
|
|
1686
|
+
const breakdownContent = response.choices
|
|
1687
|
+
.map((c) => c.message.content)
|
|
1688
|
+
.filter((s): s is string => typeof s === "string" && s.trim().length > 0)
|
|
1689
|
+
.join("\n\n");
|
|
1690
|
+
|
|
1691
|
+
this.taskBreakdown = breakdownContent || "";
|
|
1685
1692
|
this.log(`task breakdown cost: ${response.usd_cost}`);
|
|
1686
|
-
return this.taskBreakdown;
|
|
1693
|
+
return this.taskBreakdown || undefined;
|
|
1687
1694
|
}
|
|
1688
1695
|
|
|
1689
1696
|
async compressMessages(messages: Message[]) {
|
|
@@ -1713,7 +1720,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1713
1720
|
3. Next Steps - what we're about to do next to continue the user's original request.
|
|
1714
1721
|
4. Tasks remaining - what tasks are left from the initial task breakdown.
|
|
1715
1722
|
|
|
1716
|
-
Our initial task breakdown: ${this.taskBreakdown}
|
|
1723
|
+
${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
|
|
1717
1724
|
|
|
1718
1725
|
This summary will replace the older history. The latest agent interaction will remain verbatim:
|
|
1719
1726
|
|
|
@@ -1729,20 +1736,22 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1729
1736
|
|
|
1730
1737
|
const response = await this.createAgentCompletion({
|
|
1731
1738
|
messages: compressMessagesPayload,
|
|
1739
|
+
tool_choice: "none",
|
|
1732
1740
|
});
|
|
1733
1741
|
|
|
1734
|
-
const summaries = response.choices
|
|
1742
|
+
const summaries = response.choices
|
|
1743
|
+
.map((c) => c.message.content)
|
|
1744
|
+
.filter((s): s is string => typeof s === "string" && s.trim().length > 0);
|
|
1735
1745
|
this.summaries.push(...summaries);
|
|
1736
1746
|
|
|
1737
1747
|
const startMessages = [
|
|
1738
1748
|
{
|
|
1739
1749
|
role: "user",
|
|
1740
1750
|
content: `
|
|
1741
|
-
Initial task breakdown:
|
|
1742
|
-
${this.taskBreakdown}
|
|
1751
|
+
${this.taskBreakdown ? `Initial task breakdown:\n ${this.taskBreakdown}` : "(No task breakdown available โ summarize what you know from context above)"}
|
|
1743
1752
|
|
|
1744
1753
|
We have just compressed the conversation to save memory:
|
|
1745
|
-
${JSON.stringify(summaries)}
|
|
1754
|
+
${summaries.length > 0 ? JSON.stringify(summaries) : "(summary unavailable โ please continue from the task breakdown above)"}
|
|
1746
1755
|
|
|
1747
1756
|
Please continue the task from where we left off
|
|
1748
1757
|
`,
|
|
@@ -152,7 +152,9 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<Star
|
|
|
152
152
|
// Default filesystem synchronization ON unless the caller explicitly opts out
|
|
153
153
|
// (syncFs: false) or is using a messageId-based web sync. This ensures spawned
|
|
154
154
|
// subagents always appear in `knowhow agents list` and can be attached/tailed.
|
|
155
|
-
|
|
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);
|
|
156
158
|
|
|
157
159
|
// Use provided taskId if given, otherwise generate one from the prompt
|
|
158
160
|
const taskId = providedTaskId ?? generateTaskId(prompt);
|
|
@@ -175,7 +177,8 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<Star
|
|
|
175
177
|
|
|
176
178
|
if (messageId) {
|
|
177
179
|
args.push("--message-id", messageId);
|
|
178
|
-
}
|
|
180
|
+
}
|
|
181
|
+
if (useSyncFs) {
|
|
179
182
|
args.push("--sync-fs");
|
|
180
183
|
}
|
|
181
184
|
// When syncRemote is requested, pass it through so the spawned agent pushes
|
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
|
+
}
|
|
@@ -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(
|
package/src/clients/index.ts
CHANGED
|
@@ -544,14 +544,17 @@ export class AIClient {
|
|
|
544
544
|
}
|
|
545
545
|
|
|
546
546
|
// Handle the case when model prefix is gpt-5 and the provider is knowhow, and the actual model is openai/gpt-5
|
|
547
|
+
// Also handles deep prefixes like "accounts/fireworks/models/kimi-k3" matched by "kimi-k3"
|
|
547
548
|
const inferredFound = models.find((m) => {
|
|
548
549
|
const split = m.split("/");
|
|
549
550
|
if (split.length < 2) return false;
|
|
550
551
|
const inferredModel = split.slice(1).join("/");
|
|
552
|
+
const lastSegment = split[split.length - 1];
|
|
551
553
|
return (
|
|
552
554
|
m === modelPrefix ||
|
|
553
555
|
inferredModel === modelPrefix ||
|
|
554
|
-
inferredModel.startsWith(modelPrefix)
|
|
556
|
+
inferredModel.startsWith(modelPrefix) ||
|
|
557
|
+
lastSegment === modelPrefix
|
|
555
558
|
);
|
|
556
559
|
});
|
|
557
560
|
if (inferredFound) {
|
|
@@ -626,6 +629,36 @@ export class AIClient {
|
|
|
626
629
|
return { provider, model };
|
|
627
630
|
}
|
|
628
631
|
|
|
632
|
+
// When an explicit provider is given and the model contains slashes (e.g.
|
|
633
|
+
// provider="knowhow", model="anthropic/claude-sonnet-4-6"), check if the
|
|
634
|
+
// explicit provider has a model that matches by ignoring the leading
|
|
635
|
+
// provider prefix segments. Also handle deep prefixes like
|
|
636
|
+
// "accounts/fireworks/models/kimi-k3" โ search for "kimi-k3".
|
|
637
|
+
if (provider && model) {
|
|
638
|
+
const providerModels = this.clientModels[provider] ?? [];
|
|
639
|
+
if (model.includes("/")) {
|
|
640
|
+
// Model has slashes: try progressively stripping leading prefix segments
|
|
641
|
+
const modelParts = model.split("/");
|
|
642
|
+
for (let i = 1; i < modelParts.length; i++) {
|
|
643
|
+
const suffix = modelParts.slice(i).join("/");
|
|
644
|
+
// Exact match within the explicit provider
|
|
645
|
+
if (this.providerHasModel(provider, suffix)) {
|
|
646
|
+
return { provider, model: suffix };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// For any model (with or without slashes): check if a registered model's
|
|
651
|
+
// last path segment matches the requested model name.
|
|
652
|
+
// e.g. provider="fireworks", model="kimi-k3" matches
|
|
653
|
+
// "accounts/fireworks/models/kimi-k3"
|
|
654
|
+
const lastSegmentMatch = providerModels.find(
|
|
655
|
+
(m) => m.split("/").pop() === model
|
|
656
|
+
);
|
|
657
|
+
if (lastSegmentMatch) {
|
|
658
|
+
return { provider, model: lastSegmentMatch };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
629
662
|
// If an explicit provider was given, don't fall through to fuzzy cross-provider
|
|
630
663
|
// search โ that would silently pick a completely different provider (e.g. nvidia
|
|
631
664
|
// instead of fireworks). Just pass through and let the API accept/reject the model.
|
package/src/commands/agent.ts
CHANGED
|
@@ -193,6 +193,7 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
|
|
|
193
193
|
|
|
194
194
|
const { taskCompleted } = await agentModule.setupAgent({
|
|
195
195
|
...options,
|
|
196
|
+
agentName: options.agentName ?? config.chat?.defaultAgent ?? "Patcher",
|
|
196
197
|
input,
|
|
197
198
|
maxTimeLimit: parseInt(options.maxTimeLimit, 10),
|
|
198
199
|
maxSpendLimit: parseFloat(options.maxSpendLimit),
|
package/src/commands/agents.ts
CHANGED
|
@@ -292,12 +292,15 @@ function buildRows(includeAll: boolean): Row[] {
|
|
|
292
292
|
* - A numeric index string ("1", "2", โฆ) โ 1-based row from `agents list`
|
|
293
293
|
* - An exact task ID
|
|
294
294
|
* - A partial task ID (substring match)
|
|
295
|
+
*
|
|
296
|
+
* NOTE: numeric index always uses the same row set as `agents list` (includeAll=false)
|
|
297
|
+
* so that index numbers are consistent across all subcommands.
|
|
295
298
|
*/
|
|
296
299
|
function resolveTaskId(value: string, includeAll = false): string | null {
|
|
297
300
|
const n = parseInt(value, 10);
|
|
298
301
|
// Only treat as a numeric index if it's a small number (โค 9999) โ large numbers are timestamp-based task IDs
|
|
299
302
|
if (!isNaN(n) && String(n) === value.trim() && n <= 9999) {
|
|
300
|
-
const rows = buildRows(
|
|
303
|
+
const rows = buildRows(false);
|
|
301
304
|
const row = rows[n - 1];
|
|
302
305
|
return row ? row.taskId : null;
|
|
303
306
|
}
|
package/src/commands/misc.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { embed, upload, download, purge } from "../index";
|
|
|
6
6
|
import { generate, buildWaves, normalizeInputPattern, GenerateOptions } from "../generate";
|
|
7
7
|
import { init } from "../config";
|
|
8
8
|
import { login } from "../login";
|
|
9
|
+
import { registerIdentity } from "../auth/registerIdentity";
|
|
9
10
|
import { KnowhowSimpleClient } from "../services/KnowhowClient";
|
|
10
11
|
import { startChat } from "../chat";
|
|
11
12
|
|
|
@@ -24,8 +25,22 @@ export function addLoginCommand(program: Command): void {
|
|
|
24
25
|
.description("Login to knowhow")
|
|
25
26
|
.option("--jwt", "Use manual JWT input instead of browser login")
|
|
26
27
|
.option("-i, --identity <path>", "Ed25519 private key to use for public-key login")
|
|
27
|
-
.
|
|
28
|
-
|
|
28
|
+
.option("--register-identity", "Register an Ed25519 identity using the JWT in .knowhow/.jwt")
|
|
29
|
+
.option("--api-url <url>", "API base URL override (falls back to KNOWHOW_API_URL env)")
|
|
30
|
+
.action(async (options: {
|
|
31
|
+
jwt?: boolean;
|
|
32
|
+
identity?: string;
|
|
33
|
+
registerIdentity?: boolean;
|
|
34
|
+
apiUrl?: string;
|
|
35
|
+
}) => {
|
|
36
|
+
if (options.registerIdentity) {
|
|
37
|
+
await registerIdentity({
|
|
38
|
+
identityPath: options.identity,
|
|
39
|
+
apiUrl: options.apiUrl,
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
await login(options.jwt, options.identity);
|
|
43
|
+
}
|
|
29
44
|
});
|
|
30
45
|
}
|
|
31
46
|
|
|
@@ -8,8 +8,23 @@ import {
|
|
|
8
8
|
import { runManagedProcessShim } from "../processes/managedProcessShim";
|
|
9
9
|
|
|
10
10
|
const terminal = new Set(["exited", "failed", "stopped"]);
|
|
11
|
-
function resolveProcess(value:
|
|
11
|
+
function resolveProcess(value?: string, index?: string, options: any = {}): ManagedProcess {
|
|
12
12
|
const root = getProcessesDir();
|
|
13
|
+
if (index !== undefined) {
|
|
14
|
+
const parsed = Number(index);
|
|
15
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
16
|
+
throw new Error(`Invalid process index '${index}'. Use the index shown by 'knowhow processes list'.`);
|
|
17
|
+
}
|
|
18
|
+
const items = filtered(options);
|
|
19
|
+
const item = items[parsed];
|
|
20
|
+
if (!item) {
|
|
21
|
+
throw new Error(`Managed process index ${parsed} not found. Run 'knowhow processes list${options.all ? " --all" : ""}' to see valid indices.`);
|
|
22
|
+
}
|
|
23
|
+
return new ManagedProcess(item.id, item.directory);
|
|
24
|
+
}
|
|
25
|
+
if (!value) {
|
|
26
|
+
throw new Error("Provide a process id, or use -i <index> from 'knowhow processes list'.");
|
|
27
|
+
}
|
|
13
28
|
const matches = listManagedProcesses(root).filter((p) => p.id === value || p.id.includes(value));
|
|
14
29
|
if (!matches.length) throw new Error(`Managed process not found: ${value}`);
|
|
15
30
|
const exact = matches.find((p) => p.id === value);
|
|
@@ -30,13 +45,55 @@ function filtered(options: any): ManagedProcessInfo[] {
|
|
|
30
45
|
return true;
|
|
31
46
|
});
|
|
32
47
|
}
|
|
48
|
+
function truncate(value: string, width: number): string {
|
|
49
|
+
if (value.length <= width) return value;
|
|
50
|
+
if (width <= 1) return "โฆ";
|
|
51
|
+
return `${value.slice(0, width - 1)}โฆ`;
|
|
52
|
+
}
|
|
53
|
+
function truncateMiddle(value: string, width: number): string {
|
|
54
|
+
if (value.length <= width) return value;
|
|
55
|
+
if (width <= 1) return "โฆ";
|
|
56
|
+
const left = Math.ceil((width - 1) / 2);
|
|
57
|
+
return `${value.slice(0, left)}โฆ${value.slice(value.length - (width - left - 1))}`;
|
|
58
|
+
}
|
|
59
|
+
function printProcessTable(rows: Array<Record<string, string | number>>): void {
|
|
60
|
+
const terminalWidth = process.stdout.columns || 120;
|
|
61
|
+
const available = Math.max(80, Math.min(terminalWidth, 180));
|
|
62
|
+
const widths = {
|
|
63
|
+
index: Math.max(1, String(rows.length - 1).length),
|
|
64
|
+
state: Math.max(5, ...rows.map((row) => String(row.state).length)),
|
|
65
|
+
pid: Math.max(3, ...rows.map((row) => String(row.pid).length)),
|
|
66
|
+
background: 2,
|
|
67
|
+
id: Math.min(38, Math.max(12, ...rows.map((row) => String(row.id).length))),
|
|
68
|
+
parent: Math.min(20, Math.max(6, ...rows.map((row) => String(row.parent).length))),
|
|
69
|
+
};
|
|
70
|
+
const fixedWidth = widths.index + widths.state + widths.pid + widths.background
|
|
71
|
+
+ widths.id + widths.parent + 12;
|
|
72
|
+
const commandWidth = Math.max(20, available - fixedWidth);
|
|
73
|
+
const header = ["#".padEnd(widths.index), "STATE".padEnd(widths.state),
|
|
74
|
+
"PID".padEnd(widths.pid), "BG".padEnd(widths.background),
|
|
75
|
+
"ID".padEnd(widths.id), "PARENT".padEnd(widths.parent), "COMMAND"];
|
|
76
|
+
console.log(header.join(" "));
|
|
77
|
+
console.log("โ".repeat(Math.min(available, fixedWidth + commandWidth)));
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
console.log([
|
|
80
|
+
String(row.index).padEnd(widths.index),
|
|
81
|
+
String(row.state).padEnd(widths.state),
|
|
82
|
+
String(row.pid).padEnd(widths.pid),
|
|
83
|
+
String(row.background).padEnd(widths.background),
|
|
84
|
+
truncateMiddle(String(row.id), widths.id).padEnd(widths.id),
|
|
85
|
+
truncateMiddle(String(row.parent), widths.parent).padEnd(widths.parent),
|
|
86
|
+
truncate(String(row.command).replace(/\s+/g, " "), commandWidth),
|
|
87
|
+
].join(" "));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
33
90
|
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",
|
|
91
|
+
const rows = filtered(options).map(({ id, status }, index) => ({
|
|
92
|
+
index, id, state: status.state, pid: status.pid || "-", background: status.background ? "yes" : "no",
|
|
36
93
|
parent: status.parentProcessId || "-", command: [status.command, ...status.args].join(" "),
|
|
37
94
|
}));
|
|
38
95
|
if (options.json) console.log(JSON.stringify(rows, null, 2));
|
|
39
|
-
else if (rows.length)
|
|
96
|
+
else if (rows.length) printProcessTable(rows);
|
|
40
97
|
else console.log("No managed processes.");
|
|
41
98
|
}
|
|
42
99
|
function addFilters(command: Command): Command {
|
|
@@ -71,6 +128,19 @@ async function followFiles(files: string[], fromEnd = true): Promise<void> {
|
|
|
71
128
|
});
|
|
72
129
|
}
|
|
73
130
|
|
|
131
|
+
function parseMaxAge(value: string): number {
|
|
132
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*([smhd]?)$/i);
|
|
133
|
+
if (!match) throw new Error(`Invalid max-age format: "${value}". Use e.g. 30m, 6h, 7d, 3600s.`);
|
|
134
|
+
const n = parseFloat(match[1]);
|
|
135
|
+
switch ((match[2] || "h").toLowerCase()) {
|
|
136
|
+
case "s": return n * 1000;
|
|
137
|
+
case "m": return n * 60 * 1000;
|
|
138
|
+
case "h": return n * 60 * 60 * 1000;
|
|
139
|
+
case "d": return n * 24 * 60 * 60 * 1000;
|
|
140
|
+
default: throw new Error(`Unknown unit in max-age: "${match[2]}"`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
74
144
|
export function addProcessesCommand(program: Command): void {
|
|
75
145
|
const processes = addFilters(program.command("processes").description("inspect and control managed processes"));
|
|
76
146
|
processes.action((rawOptions, command) => printList(commandOptions(rawOptions, command)));
|
|
@@ -80,11 +150,19 @@ export function addProcessesCommand(program: Command): void {
|
|
|
80
150
|
processes.command("run").description("internal managed process shim").requiredOption("--config <path>")
|
|
81
151
|
.action(async ({ config }) => runManagedProcessShim(path.resolve(config)));
|
|
82
152
|
|
|
83
|
-
|
|
153
|
+
// Avoid Commander's built-in help exit here. The optional tracing module
|
|
154
|
+
// flushes asynchronously on process.exit, while Commander expects it never
|
|
155
|
+
// to return, which can otherwise produce a spurious "unknown option" error.
|
|
156
|
+
addFilters(processes.command("logs [id]").alias("tail"))
|
|
157
|
+
.helpOption(false)
|
|
158
|
+
.option("-h, --help", "display help for command")
|
|
159
|
+
.option("-i, --index <number>", "select by the zero-based index shown by processes list")
|
|
160
|
+
.option("-n, --lines <number>", "lines to show", "100")
|
|
84
161
|
.option("-f, --follow", "follow output").option("--stderr", "show stderr instead of stdout")
|
|
85
162
|
.option("--both", "show stdout and stderr").action(async (id, rawOptions, command) => {
|
|
86
163
|
const options = commandOptions(rawOptions, command);
|
|
87
|
-
|
|
164
|
+
if (options.help) { command.outputHelp(); return; }
|
|
165
|
+
const managed = resolveProcess(id, options.index, options);
|
|
88
166
|
const files = options.both ? [managed.stdoutPath, managed.stderrPath] : [options.stderr ? managed.stderrPath : managed.stdoutPath];
|
|
89
167
|
for (const file of files) process.stdout.write(tailText(file, Math.max(0, Number(options.lines) || 100)));
|
|
90
168
|
if (options.follow) await followFiles(files);
|
|
@@ -135,4 +213,36 @@ export function addProcessesCommand(program: Command): void {
|
|
|
135
213
|
});
|
|
136
214
|
render(null, "");
|
|
137
215
|
});
|
|
216
|
+
|
|
217
|
+
processes.command("cleanup")
|
|
218
|
+
.description("remove process directories older than max-age (default 24h)")
|
|
219
|
+
.option("--max-age <duration>", "max age before removal, e.g. 30m, 6h, 7d (default: 24h)", "24h")
|
|
220
|
+
.option("--dry-run", "show what would be removed without deleting")
|
|
221
|
+
.action(async (rawOptions) => {
|
|
222
|
+
const options = typeof rawOptions?.optsWithGlobals === "function" ? rawOptions.optsWithGlobals() : rawOptions;
|
|
223
|
+
const maxAgeMs = parseMaxAge(options.maxAge || "24h");
|
|
224
|
+
const processesDir = getProcessesDir();
|
|
225
|
+
if (!fs.existsSync(processesDir)) { console.log("No processes directory found."); return; }
|
|
226
|
+
const entries = fs.readdirSync(processesDir, { withFileTypes: true });
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
let removed = 0;
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
if (!entry.isDirectory()) continue;
|
|
231
|
+
const dirPath = path.join(processesDir, entry.name);
|
|
232
|
+
const statusPath = path.join(dirPath, "status.json");
|
|
233
|
+
if (!fs.existsSync(statusPath)) continue;
|
|
234
|
+
const stats = fs.statSync(dirPath);
|
|
235
|
+
const age = now - stats.mtimeMs;
|
|
236
|
+
if (age > maxAgeMs) {
|
|
237
|
+
if (options.dryRun) {
|
|
238
|
+
console.log(`[dry-run] would remove: ${entry.name} (age: ${Math.round(age / 60000)}m)`);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(`๐งน Removing: ${entry.name} (age: ${Math.round(age / 60000)}m)`);
|
|
241
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
removed++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
console.log(`${options.dryRun ? "[dry-run] " : ""}${removed} process director${removed === 1 ? "y" : "ies"} ${options.dryRun ? "would be" : ""} removed.`);
|
|
247
|
+
});
|
|
138
248
|
}
|
package/src/hashes.ts
CHANGED
|
@@ -19,11 +19,13 @@ export async function getHashes() {
|
|
|
19
19
|
/**
|
|
20
20
|
* Atomically save hashes to disk โ writes to a temp file then renames,
|
|
21
21
|
* preventing concurrent writes from producing corrupted/truncated JSON.
|
|
22
|
+
* The hash directory is optional state and may not exist in a fresh checkout.
|
|
22
23
|
*/
|
|
23
24
|
export async function saveHashes(hashes: any) {
|
|
24
25
|
const target = ".knowhow/.hashes.json";
|
|
25
26
|
const tmp = `${target}.tmp.${process.pid}`;
|
|
26
27
|
try {
|
|
28
|
+
fs.mkdirSync(".knowhow", { recursive: true });
|
|
27
29
|
fs.writeFileSync(tmp, JSON.stringify(hashes, null, 2));
|
|
28
30
|
fs.renameSync(tmp, target);
|
|
29
31
|
} catch (err) {
|
|
@@ -34,8 +34,9 @@ export function listManagedProcesses(processesDir = getProcessesDir()): ManagedP
|
|
|
34
34
|
.sort((a, b) => b.status.startedAt.localeCompare(a.status.startedAt));
|
|
35
35
|
}
|
|
36
36
|
function makeId(command: string): string {
|
|
37
|
+
const ts = Date.now();
|
|
37
38
|
const name = path.basename(command).replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 28) || "process";
|
|
38
|
-
return `${
|
|
39
|
+
return `${ts}-${name}-${randomBytes(3).toString("hex")}`;
|
|
39
40
|
}
|
|
40
41
|
function waitForFile(file: string, timeoutMs: number): Promise<void> {
|
|
41
42
|
const started = Date.now();
|
|
@@ -70,12 +70,11 @@ export async function runManagedProcessShim(configPath: string): Promise<void> {
|
|
|
70
70
|
fs.writeFileSync(path.join(dir, "pid"), child.pid ? `${child.pid}\n` : "");
|
|
71
71
|
writeStatus();
|
|
72
72
|
|
|
73
|
-
let fifoFd: number | undefined;
|
|
74
73
|
let fifoStream: fs.ReadStream | undefined;
|
|
75
74
|
if (process.platform !== "win32") {
|
|
76
75
|
// 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:
|
|
76
|
+
const fifoFd = fs.openSync(fifoPath, fs.constants.O_RDWR);
|
|
77
|
+
fifoStream = fs.createReadStream("", { fd: fifoFd, autoClose: true });
|
|
79
78
|
fifoStream.on("data", (chunk) => {
|
|
80
79
|
fs.appendFileSync(inputLogPath, chunk);
|
|
81
80
|
if (child.stdin?.writable) child.stdin.write(chunk);
|
|
@@ -110,6 +109,19 @@ export async function runManagedProcessShim(configPath: string): Promise<void> {
|
|
|
110
109
|
});
|
|
111
110
|
});
|
|
112
111
|
if (parentTimer) clearInterval(parentTimer);
|
|
113
|
-
fifoStream
|
|
114
|
-
|
|
112
|
+
if (fifoStream && !fifoStream.closed) {
|
|
113
|
+
// A FIFO read cannot always be cancelled by closing its descriptor (notably
|
|
114
|
+
// on macOS). Wake the pending read before asking ReadStream to close it;
|
|
115
|
+
// otherwise each completed command can leave its shim wedged in close(2).
|
|
116
|
+
const wakeFd = fs.openSync(fifoPath, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK);
|
|
117
|
+
try {
|
|
118
|
+
fs.writeSync(wakeFd, Buffer.from([0]));
|
|
119
|
+
} finally {
|
|
120
|
+
fs.closeSync(wakeFd);
|
|
121
|
+
}
|
|
122
|
+
await new Promise<void>((resolve) => {
|
|
123
|
+
fifoStream!.once("close", resolve);
|
|
124
|
+
fifoStream!.destroy();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
115
127
|
}
|