@tyvm/knowhow 0.0.148 → 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.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +18 -9
  3. package/src/agents/tools/startAgentTask.ts +5 -2
  4. package/src/auth/keyAuth.ts +10 -4
  5. package/src/auth/registerIdentity.ts +67 -0
  6. package/src/chat/modules/AgentModule.ts +39 -7
  7. package/src/clients/index.ts +34 -1
  8. package/src/commands/agent.ts +1 -0
  9. package/src/commands/agents.ts +4 -1
  10. package/src/commands/misc.ts +17 -2
  11. package/src/commands/processes.ts +71 -6
  12. package/src/processes/managedProcessShim.ts +17 -5
  13. package/src/processors/Base64ImageDetector.ts +28 -4
  14. package/src/processors/JsonCompressor.ts +14 -0
  15. package/src/processors/TokenCompressor.ts +17 -14
  16. package/src/services/AgentService.ts +2 -2
  17. package/src/services/modules/computerUse.ts +3 -1
  18. package/tests/clients/AIClient.test.ts +98 -15
  19. package/tests/processes/managedProcessShim.test.ts +59 -0
  20. package/tests/processors/Base64ImageDetector.test.ts +21 -0
  21. package/tests/processors/TokenCompressor.test.ts +18 -0
  22. package/tests/unit/auth/registerIdentity.test.ts +93 -0
  23. package/ts_build/package.json +1 -1
  24. package/ts_build/src/agents/base/base.js +14 -9
  25. package/ts_build/src/agents/base/base.js.map +1 -1
  26. package/ts_build/src/agents/tools/startAgentTask.js +2 -2
  27. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  28. package/ts_build/src/auth/keyAuth.js +10 -3
  29. package/ts_build/src/auth/keyAuth.js.map +1 -1
  30. package/ts_build/src/auth/registerIdentity.d.ts +7 -0
  31. package/ts_build/src/auth/registerIdentity.js +62 -0
  32. package/ts_build/src/auth/registerIdentity.js.map +1 -0
  33. package/ts_build/src/chat/modules/AgentModule.d.ts +2 -0
  34. package/ts_build/src/chat/modules/AgentModule.js +27 -7
  35. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  36. package/ts_build/src/clients/index.js +19 -1
  37. package/ts_build/src/clients/index.js.map +1 -1
  38. package/ts_build/src/commands/agent.js +1 -0
  39. package/ts_build/src/commands/agent.js.map +1 -1
  40. package/ts_build/src/commands/agents.js +1 -1
  41. package/ts_build/src/commands/agents.js.map +1 -1
  42. package/ts_build/src/commands/misc.js +12 -1
  43. package/ts_build/src/commands/misc.js.map +1 -1
  44. package/ts_build/src/commands/processes.js +75 -6
  45. package/ts_build/src/commands/processes.js.map +1 -1
  46. package/ts_build/src/processes/managedProcessShim.js +13 -7
  47. package/ts_build/src/processes/managedProcessShim.js.map +1 -1
  48. package/ts_build/src/processors/Base64ImageDetector.d.ts +1 -0
  49. package/ts_build/src/processors/Base64ImageDetector.js +19 -2
  50. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  51. package/ts_build/src/processors/JsonCompressor.js +7 -0
  52. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  53. package/ts_build/src/processors/TokenCompressor.d.ts +1 -1
  54. package/ts_build/src/processors/TokenCompressor.js +9 -8
  55. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  56. package/ts_build/src/services/AgentService.d.ts +1 -1
  57. package/ts_build/src/services/AgentService.js.map +1 -1
  58. package/ts_build/src/services/modules/computerUse.d.ts +3 -1
  59. package/ts_build/tests/clients/AIClient.test.js +73 -11
  60. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  61. package/ts_build/tests/processes/managedProcessShim.test.d.ts +1 -0
  62. package/ts_build/tests/processes/managedProcessShim.test.js +92 -0
  63. package/ts_build/tests/processes/managedProcessShim.test.js.map +1 -0
  64. package/ts_build/tests/processors/Base64ImageDetector.test.js +18 -0
  65. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  66. package/ts_build/tests/processors/TokenCompressor.test.js +15 -0
  67. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  68. package/ts_build/tests/unit/auth/registerIdentity.test.d.ts +1 -0
  69. package/ts_build/tests/unit/auth/registerIdentity.test.js +86 -0
  70. package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -0
@@ -45,6 +45,22 @@ export class TokenCompressor implements JsonCompressorStorage {
45
45
  // JSON compression handler
46
46
  private jsonCompressor: JsonCompressor;
47
47
 
48
+
49
+ /**
50
+ * Tool names whose responses should never be compressed.
51
+ * These are compression/retrieval tools themselves - compressing their output
52
+ * would prevent the agent from seeing the uncompressed data it just retrieved.
53
+ */
54
+ static DO_NOT_COMPRESS: ReadonlySet<string> = new Set([
55
+ "expandTokens",
56
+ "grepToolResponse",
57
+ "jqToolResponse",
58
+ "tailToolResponse",
59
+ "listStoredToolResponses",
60
+ "loadImageAsBase64" // need a proper way to do this
61
+ ]);
62
+
63
+
48
64
  constructor(toolsService?: ToolsService) {
49
65
  this.jsonCompressor = new JsonCompressor(
50
66
  this,
@@ -290,19 +306,6 @@ export class TokenCompressor implements JsonCompressorStorage {
290
306
  }
291
307
  }
292
308
 
293
- /**
294
- * Tool names whose responses should never be compressed.
295
- * These are compression/retrieval tools themselves - compressing their output
296
- * would prevent the agent from seeing the uncompressed data it just retrieved.
297
- */
298
- static readonly COMPRESSION_TOOL_NAMES: ReadonlySet<string> = new Set([
299
- "expandTokens",
300
- "grepToolResponse",
301
- "jqToolResponse",
302
- "tailToolResponse",
303
- "listStoredToolResponses",
304
- ]);
305
-
306
309
  createProcessor(
307
310
  filterFn?: (msg: Message) => boolean
308
311
  ): MessageProcessorFunction {
@@ -344,7 +347,7 @@ export class TokenCompressor implements JsonCompressorStorage {
344
347
  if (
345
348
  message.role === "tool" &&
346
349
  message.name &&
347
- TokenCompressor.COMPRESSION_TOOL_NAMES.has(message.name)
350
+ TokenCompressor.DO_NOT_COMPRESS.has(message.name)
348
351
  ) {
349
352
  continue;
350
353
  }
@@ -76,14 +76,14 @@ export class AgentService {
76
76
  return this.agentContext ?? { Tools: this.tools, Events: this.events };
77
77
  }
78
78
 
79
- public getAgent(name: string): IAgent {
79
+ public getAgent<T extends IAgent = IAgent>(name: string): T {
80
80
  const agent = this.agents.get(name);
81
81
  if (!agent) {
82
82
  throw new Error(
83
83
  `Agent ${name} not found. Options are: ${this.listAgents()}`
84
84
  );
85
85
  }
86
- return agent;
86
+ return agent as T;
87
87
  }
88
88
 
89
89
  public listAgents(): string[] {
@@ -26,13 +26,15 @@ export interface Region extends Point, Size {}
26
26
  export type MouseButton = "left" | "right" | "middle";
27
27
 
28
28
  export interface OverlayPrimitive {
29
- kind: "rect" | "circle" | "line" | "point";
29
+ kind: "rect" | "circle" | "line" | "point" | "text";
30
30
  x: number;
31
31
  y: number;
32
32
  width?: number;
33
33
  height?: number;
34
34
  x2?: number;
35
35
  y2?: number;
36
+ text?: string;
37
+ fontSize?: number;
36
38
  color?: string;
37
39
  lineWidth?: number;
38
40
  }
@@ -187,6 +187,98 @@ describe("AIClient", () => {
187
187
  expect(result.model).toBe("unknown-model");
188
188
  });
189
189
 
190
+ // ── New tests: explicit provider + slashed model prefix stripping ────────
191
+
192
+ it("knowhow provider: resolves 'anthropic/claude-sonnet-4-6' to model stored as 'anthropic/claude-sonnet-4-6'", () => {
193
+ aiClient.registerClient("knowhow", new FakeClient());
194
+ aiClient.registerModels("knowhow", [
195
+ "anthropic/claude-sonnet-4-6",
196
+ "openai/gpt-4",
197
+ "google/gemini-pro",
198
+ ]);
199
+
200
+ // Exact match: provider=knowhow, model=anthropic/claude-sonnet-4-6
201
+ const result = aiClient.detectProviderModel("knowhow", "anthropic/claude-sonnet-4-6");
202
+ expect(result.provider).toBe("knowhow");
203
+ expect(result.model).toBe("anthropic/claude-sonnet-4-6");
204
+ });
205
+
206
+ it("knowhow provider: resolves bare 'claude-sonnet-4-6' to 'anthropic/claude-sonnet-4-6' stored on knowhow", () => {
207
+ aiClient.registerClient("knowhow", new FakeClient());
208
+ aiClient.registerModels("knowhow", [
209
+ "anthropic/claude-sonnet-4-6",
210
+ "openai/gpt-4",
211
+ ]);
212
+
213
+ // Bare model name — should resolve by stripping the provider prefix from
214
+ // the registered model and matching on the trailing segment
215
+ const result = aiClient.detectProviderModel("knowhow", "claude-sonnet-4-6");
216
+ expect(result.provider).toBe("knowhow");
217
+ expect(result.model).toBe("anthropic/claude-sonnet-4-6");
218
+ });
219
+
220
+ it("fireworks provider: resolves 'kimi-k3' to 'accounts/fireworks/models/kimi-k3'", () => {
221
+ aiClient.registerClient("fireworks", new FakeClient());
222
+ aiClient.registerModels("fireworks", [
223
+ "accounts/fireworks/models/kimi-k3",
224
+ "accounts/fireworks/models/kimi-k2p7-code",
225
+ "accounts/fireworks/models/minimax-m3",
226
+ "accounts/fireworks/models/deepseek-v3-2",
227
+ ]);
228
+
229
+ const result = aiClient.detectProviderModel("fireworks", "kimi-k3");
230
+ expect(result.provider).toBe("fireworks");
231
+ expect(result.model).toBe("accounts/fireworks/models/kimi-k3");
232
+ });
233
+
234
+ it("fireworks provider: resolves 'accounts/fireworks/models/kimi-k3' (full path) correctly", () => {
235
+ aiClient.registerClient("fireworks", new FakeClient());
236
+ aiClient.registerModels("fireworks", [
237
+ "accounts/fireworks/models/kimi-k3",
238
+ "accounts/fireworks/models/deepseek-v4-pro",
239
+ ]);
240
+
241
+ // Full path exact match
242
+ const result = aiClient.detectProviderModel("fireworks", "accounts/fireworks/models/kimi-k3");
243
+ expect(result.provider).toBe("fireworks");
244
+ expect(result.model).toBe("accounts/fireworks/models/kimi-k3");
245
+ });
246
+
247
+ it("explicit provider: does NOT leak to a different provider when stripping slashes", () => {
248
+ aiClient.registerClient("knowhow", new FakeClient());
249
+ aiClient.registerModels("knowhow", ["anthropic/claude-sonnet-4-6"]);
250
+
251
+ // 'fake' provider doesn't have this model — should not find it on knowhow
252
+ const result = aiClient.detectProviderModel("fake", "claude-sonnet-4-6");
253
+ // Falls back: provider=fake, model=claude-sonnet-4-6 (no cross-provider leak)
254
+ expect(result.provider).toBe("fake");
255
+ expect(result.model).toBe("claude-sonnet-4-6");
256
+ });
257
+
258
+ it("empty provider: finds 'claude-sonnet-4-6' cross-provider on knowhow (stored as 'anthropic/claude-sonnet-4-6')", () => {
259
+ aiClient.registerClient("knowhow", new FakeClient());
260
+ aiClient.registerModels("knowhow", [
261
+ "anthropic/claude-sonnet-4-6",
262
+ "openai/gpt-4",
263
+ ]);
264
+
265
+ const result = aiClient.detectProviderModel("", "claude-sonnet-4-6");
266
+ expect(result.provider).toBe("knowhow");
267
+ expect(result.model).toBe("anthropic/claude-sonnet-4-6");
268
+ });
269
+
270
+ it("empty provider: finds 'kimi-k3' cross-provider on fireworks (stored as 'accounts/fireworks/models/kimi-k3')", () => {
271
+ aiClient.registerClient("fireworks", new FakeClient());
272
+ aiClient.registerModels("fireworks", [
273
+ "accounts/fireworks/models/kimi-k3",
274
+ "accounts/fireworks/models/deepseek-v4-pro",
275
+ ]);
276
+
277
+ const result = aiClient.detectProviderModel("", "kimi-k3");
278
+ expect(result.provider).toBe("fireworks");
279
+ expect(result.model).toBe("accounts/fireworks/models/kimi-k3");
280
+ });
281
+
190
282
  it("should detect real provider when model exists", () => {
191
283
  // Register an anthropic provider explicitly so we don't rely on env vars
192
284
  aiClient.registerClient("anthropic", new FakeClient());
@@ -442,14 +534,14 @@ describe("AIClient", () => {
442
534
  });
443
535
 
444
536
  it("should handle empty provider and model strings", () => {
445
- // Empty strings should return default OpenAI client with gpt-5
446
- const result = aiClient.getClient("", "");
447
- expect(result.provider.length).toBeGreaterThan(0);
448
- expect(result.model.length).toBeGreaterThan(0);
537
+ // Empty provider + empty model: detectProviderModel returns the fallback (empty strings)
538
+ // getClient("", "") throws because "" is not a registered provider
539
+ expect(() => aiClient.getClient("", "")).toThrow();
449
540
 
450
541
  const detection = aiClient.detectProviderModel("", "");
451
- expect(detection?.provider?.length).toBeGreaterThan(0);
452
- expect(detection?.model?.length).toBeGreaterThan(0);
542
+ // Falls back to original values — both empty
543
+ expect(detection?.provider).toBe("");
544
+ expect(detection?.model).toBe("");
453
545
  });
454
546
 
455
547
  it("should handle malformed model formats", () => {
@@ -469,15 +561,6 @@ describe("AIClient", () => {
469
561
  expect(detection).toBeDefined();
470
562
  expect(detection?.provider).toBeDefined();
471
563
  expect(detection?.model).toBeDefined();
472
- // For malformed inputs that can't be parsed, should fallback to defaults
473
- if (
474
- input === "provider/" ||
475
- input === "///" ||
476
- input === "provider/model/"
477
- ) {
478
- expect(detection?.provider?.length).toBeGreaterThan(0);
479
- expect(detection?.model?.length).toBeGreaterThan(0);
480
- }
481
564
  });
482
565
  });
483
566
 
@@ -0,0 +1,59 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+
6
+ describe("managed process shim", () => {
7
+ const testOnPosix = process.platform === "win32" ? test.skip : test;
8
+
9
+ testOnPosix("exits after a short command instead of leaking the shim process", async () => {
10
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-shim-test-"));
11
+ const configPath = path.join(directory, "launch.json");
12
+ const shimModule = path.resolve(__dirname, "../../src/processes/managedProcessShim");
13
+ fs.writeFileSync(configPath, JSON.stringify({
14
+ id: "short-command",
15
+ command: process.execPath,
16
+ args: ["-e", "process.exit(0)"],
17
+ cwd: directory,
18
+ env: {},
19
+ background: true,
20
+ parentProcessId: null,
21
+ parentPid: null,
22
+ shell: false,
23
+ processesDir: path.dirname(directory),
24
+ }));
25
+
26
+ const script = `require(${JSON.stringify(shimModule)})`
27
+ + `.runManagedProcessShim(${JSON.stringify(configPath)})`
28
+ + `.catch(error => { console.error(error); process.exitCode = 1; });`;
29
+ const shim = spawn(process.execPath, ["-r", "ts-node/register/transpile-only", "-e", script], {
30
+ cwd: path.resolve(__dirname, "../.."),
31
+ stdio: ["ignore", "ignore", "pipe"],
32
+ });
33
+ let stderr = "";
34
+ shim.stderr!.setEncoding("utf8");
35
+ shim.stderr!.on("data", (chunk) => { stderr += chunk; });
36
+
37
+ try {
38
+ const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
39
+ const timer = setTimeout(() => {
40
+ shim.kill("SIGKILL");
41
+ reject(new Error(`Shim PID ${shim.pid} did not exit after its command completed`));
42
+ }, 10000);
43
+ shim.once("exit", (code, signal) => {
44
+ clearTimeout(timer);
45
+ resolve({ code, signal });
46
+ });
47
+ });
48
+
49
+ expect(stderr).toBe("");
50
+ expect(result).toEqual({ code: 0, signal: null });
51
+ const status = JSON.parse(fs.readFileSync(path.join(directory, "status.json"), "utf8"));
52
+ expect(status).toMatchObject({ shimPid: shim.pid, state: "exited", exitCode: 0 });
53
+ expect(() => process.kill(shim.pid!, 0)).toThrow();
54
+ } finally {
55
+ if (shim.exitCode === null && shim.signalCode === null) shim.kill("SIGKILL");
56
+ fs.rmSync(directory, { recursive: true, force: true });
57
+ }
58
+ });
59
+ });
@@ -220,6 +220,27 @@ describe("Base64ImageDetector", () => {
220
220
  expect(content[0].image_url.url).toBe(imageDataUrl);
221
221
  });
222
222
 
223
+ it("should not promote a compressed image URL placeholder to image content", () => {
224
+ const toolResponseJson = JSON.stringify({
225
+ type: "image_url",
226
+ image_url: {
227
+ url: "[COMPRESSED_JSON_PROPERTY - 24614 tokens]\nKey: compressed_image_url",
228
+ detail: "high",
229
+ },
230
+ });
231
+ const modifiedMessages: Message[] = [
232
+ {
233
+ role: "tool",
234
+ content: toolResponseJson,
235
+ tool_call_id: "call_compressed_image",
236
+ },
237
+ ];
238
+
239
+ detector.createProcessor()([], modifiedMessages);
240
+
241
+ expect(modifiedMessages[0].content).toBe(toolResponseJson);
242
+ });
243
+
223
244
  it("should process tool messages with plain base64 string", () => {
224
245
  const validPngBase64 =
225
246
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
@@ -415,6 +415,24 @@ describe("TokenCompressor", () => {
415
415
  });
416
416
  });
417
417
 
418
+ describe("image content compression", () => {
419
+ it("keeps image URLs intact when expanded", () => {
420
+ const imageUrl = `data:image/png;base64,${"A".repeat(20_000)}`;
421
+ const content = JSON.stringify({
422
+ type: "image_url",
423
+ image_url: { url: imageUrl, detail: "high" },
424
+ });
425
+
426
+ const compressed = tokenCompressor.compressContent(content);
427
+ const key = compressed.match(/Key:\s*(\S+)/)?.[1];
428
+
429
+ expect(key).toBeDefined();
430
+ const expanded = tokenCompressor.retrieveFullString(key!);
431
+ expect(expanded).toBe(content);
432
+ expect(expanded).not.toContain("[COMPRESSED_JSON_PROPERTY");
433
+ });
434
+ });
435
+
418
436
  describe("tool function integration", () => {
419
437
  it("should register expandTokens function correctly", () => {
420
438
  const toolsServiceCalls = mockToolsService.addFunctions.mock.calls;
@@ -0,0 +1,93 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ const registerPublicKey = jest.fn();
6
+ const exchangePublicKeyJwt = jest.fn();
7
+ const storeJwt = jest.fn();
8
+ const getOrCreatePublicKey = jest.fn();
9
+ const getConfig = jest.fn();
10
+ const updateConfig = jest.fn();
11
+
12
+ jest.mock("../../../src/auth/keyAuth", () => ({ registerPublicKey, exchangePublicKeyJwt, storeJwt }));
13
+ jest.mock("../../../src/auth/keyManager", () => ({ getOrCreatePublicKey }));
14
+ jest.mock("../../../src/config", () => ({ getConfig, updateConfig }));
15
+
16
+ import { decodeJwtPayload, extractOrgIdFromJwt, registerIdentity } from "../../../src/auth/registerIdentity";
17
+
18
+ function makeJwt(payload: Record<string, unknown>): string {
19
+ return `${Buffer.from("{}").toString("base64url")}.${Buffer.from(JSON.stringify(payload)).toString("base64url")}.sig`;
20
+ }
21
+
22
+ describe("registerIdentity", () => {
23
+ const originalCwd = process.cwd();
24
+ let workDir: string;
25
+ const sourceJwt = makeJwt({ org: "org-test" });
26
+ const publicKeyBase64 = Buffer.alloc(32, 5).toString("base64");
27
+
28
+ beforeEach(() => {
29
+ jest.clearAllMocks();
30
+ jest.spyOn(console, "log").mockImplementation(() => undefined);
31
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-register-identity-"));
32
+ fs.mkdirSync(path.join(workDir, ".knowhow"), { recursive: true });
33
+ fs.writeFileSync(path.join(workDir, ".knowhow", ".jwt"), sourceJwt);
34
+ process.chdir(workDir);
35
+ getOrCreatePublicKey.mockReturnValue({
36
+ publicKeyBase64,
37
+ fingerprint: "SHA256:test",
38
+ privateKeyPath: path.join(workDir, ".knowhow", "keys", "id_ed25519"),
39
+ });
40
+ registerPublicKey.mockResolvedValue(undefined);
41
+ exchangePublicKeyJwt.mockResolvedValue("identity.jwt.token");
42
+ getConfig.mockResolvedValue({ modelProviders: [] });
43
+ updateConfig.mockResolvedValue(undefined);
44
+ });
45
+
46
+ afterEach(() => {
47
+ process.chdir(originalCwd);
48
+ fs.rmSync(workDir, { recursive: true, force: true });
49
+ jest.restoreAllMocks();
50
+ });
51
+
52
+ it("registers from the project JWT and stores the exchanged JWT", async () => {
53
+ await registerIdentity({ apiUrl: "https://api.example" });
54
+ expect(registerPublicKey).toHaveBeenCalledWith(sourceJwt, publicKeyBase64, "https://api.example");
55
+ expect(exchangePublicKeyJwt).toHaveBeenCalledWith("org-test", "https://api.example", undefined);
56
+ expect(storeJwt).toHaveBeenCalledWith("identity.jwt.token");
57
+ expect(updateConfig).toHaveBeenCalledWith(expect.objectContaining({
58
+ orgId: "org-test",
59
+ modelProviders: [{ provider: "knowhow" }],
60
+ cliIdentityPath: expect.stringContaining("id_ed25519"),
61
+ }));
62
+ });
63
+
64
+ it("uses an explicitly selected identity", async () => {
65
+ await registerIdentity({ identityPath: "/keys/id", apiUrl: "https://api.example" });
66
+ expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/id");
67
+ expect(exchangePublicKeyJwt).toHaveBeenCalledWith("org-test", "https://api.example", "/keys/id");
68
+ });
69
+
70
+ it("requires a nonempty project JWT", async () => {
71
+ fs.writeFileSync(path.join(workDir, ".knowhow", ".jwt"), " ");
72
+ await expect(registerIdentity()).rejects.toThrow("JWT file is empty");
73
+ expect(registerPublicKey).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it("does not replace the JWT when registration fails", async () => {
77
+ registerPublicKey.mockRejectedValue(new Error("registration failed"));
78
+ await expect(registerIdentity()).rejects.toThrow("registration failed");
79
+ expect(storeJwt).not.toHaveBeenCalled();
80
+ });
81
+ });
82
+
83
+ describe("JWT organization extraction", () => {
84
+ it("decodes payload and accepts canonical org", () => {
85
+ expect(decodeJwtPayload(makeJwt({ org: "one" }))).toMatchObject({ org: "one" });
86
+ expect(extractOrgIdFromJwt(makeJwt({ org: "one" }))).toBe("one");
87
+ });
88
+
89
+ it("rejects malformed and organization-less JWTs", () => {
90
+ expect(() => decodeJwtPayload("invalid")).toThrow("Invalid JWT format");
91
+ expect(() => extractOrgIdFromJwt(makeJwt({ sub: "user" }))).toThrow("organization claim");
92
+ });
93
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.148",
3
+ "version": "0.0.149",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1039,8 +1039,7 @@ let BaseAgent = class BaseAgent {
1039
1039
  const response = await this.createAgentCompletion({
1040
1040
  model,
1041
1041
  messages: taskBreakdownMessages,
1042
- tools: this.getEnabledTools(),
1043
- tool_choice: "auto",
1042
+ tool_choice: "none",
1044
1043
  long_ttl_cache: this.runTime() > 300_000,
1045
1044
  ...(this.reasoningEffort !== undefined && {
1046
1045
  reasoning_effort: this.reasoningEffort,
@@ -1049,9 +1048,13 @@ let BaseAgent = class BaseAgent {
1049
1048
  reasoning_summary: this.summarizeReasoning,
1050
1049
  }),
1051
1050
  });
1052
- this.taskBreakdown = response.choices[0].message.content;
1051
+ const breakdownContent = response.choices
1052
+ .map((c) => c.message.content)
1053
+ .filter((s) => typeof s === "string" && s.trim().length > 0)
1054
+ .join("\n\n");
1055
+ this.taskBreakdown = breakdownContent || "";
1053
1056
  this.log(`task breakdown cost: ${response.usd_cost}`);
1054
- return this.taskBreakdown;
1057
+ return this.taskBreakdown || undefined;
1055
1058
  }
1056
1059
  async compressMessages(messages) {
1057
1060
  let resumeIndex = messages.length - 1;
@@ -1072,7 +1075,7 @@ let BaseAgent = class BaseAgent {
1072
1075
  3. Next Steps - what we're about to do next to continue the user's original request.
1073
1076
  4. Tasks remaining - what tasks are left from the initial task breakdown.
1074
1077
 
1075
- Our initial task breakdown: ${this.taskBreakdown}
1078
+ ${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
1076
1079
 
1077
1080
  This summary will replace the older history. The latest agent interaction will remain verbatim:
1078
1081
 
@@ -1086,18 +1089,20 @@ let BaseAgent = class BaseAgent {
1086
1089
  ];
1087
1090
  const response = await this.createAgentCompletion({
1088
1091
  messages: compressMessagesPayload,
1092
+ tool_choice: "none",
1089
1093
  });
1090
- const summaries = response.choices.map((c) => c.message.content);
1094
+ const summaries = response.choices
1095
+ .map((c) => c.message.content)
1096
+ .filter((s) => typeof s === "string" && s.trim().length > 0);
1091
1097
  this.summaries.push(...summaries);
1092
1098
  const startMessages = [
1093
1099
  {
1094
1100
  role: "user",
1095
1101
  content: `
1096
- Initial task breakdown:
1097
- ${this.taskBreakdown}
1102
+ ${this.taskBreakdown ? `Initial task breakdown:\n ${this.taskBreakdown}` : "(No task breakdown available — summarize what you know from context above)"}
1098
1103
 
1099
1104
  We have just compressed the conversation to save memory:
1100
- ${JSON.stringify(summaries)}
1105
+ ${summaries.length > 0 ? JSON.stringify(summaries) : "(summary unavailable — please continue from the task breakdown above)"}
1101
1106
 
1102
1107
  Please continue the task from where we left off
1103
1108
  `,