@tyvm/knowhow 0.0.154 → 0.0.156

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 (93) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +44 -26
  3. package/src/agents/base/prompt.ts +3 -2
  4. package/src/agents/tools/index.ts +1 -0
  5. package/src/agents/tools/list.ts +2 -0
  6. package/src/agents/tools/loadImageAsBase64.ts +83 -0
  7. package/src/auth/keyAuth.ts +40 -8
  8. package/src/chat/modules/AgentModule.ts +98 -42
  9. package/src/clients/index.ts +7 -4
  10. package/src/clients/openai.ts +37 -4
  11. package/src/commands/services.ts +23 -18
  12. package/src/login.ts +22 -6
  13. package/src/processors/Base64ImageDetector.ts +12 -11
  14. package/src/services/KnowhowClient.ts +6 -5
  15. package/src/services/watchers/FsSyncer.ts +40 -2
  16. package/src/services/watchers/RemoteSyncer.ts +40 -2
  17. package/src/tunnel.ts +29 -13
  18. package/src/worker.ts +62 -12
  19. package/tests/clients/reasoningEffort.test.ts +57 -0
  20. package/tests/processors/Base64ImageDetector.test.ts +32 -0
  21. package/tests/unit/agents/tools/loadImageAsBase64.test.ts +44 -0
  22. package/tests/unit/auth/keyAuth.test.ts +52 -2
  23. package/tests/unit/login.test.ts +39 -1
  24. package/tests/unit/services/KnowhowClient.test.ts +84 -0
  25. package/tests/unit/services/Mcp.test.ts +56 -0
  26. package/tests/unit/services/SyncedAgentWatcher.test.ts +89 -0
  27. package/tests/unit/tunnel.test.ts +51 -0
  28. package/ts_build/package.json +1 -1
  29. package/ts_build/src/agents/base/base.d.ts +1 -0
  30. package/ts_build/src/agents/base/base.js +27 -22
  31. package/ts_build/src/agents/base/base.js.map +1 -1
  32. package/ts_build/src/agents/base/prompt.d.ts +1 -1
  33. package/ts_build/src/agents/base/prompt.js +3 -2
  34. package/ts_build/src/agents/base/prompt.js.map +1 -1
  35. package/ts_build/src/agents/tools/index.d.ts +1 -0
  36. package/ts_build/src/agents/tools/index.js +1 -0
  37. package/ts_build/src/agents/tools/index.js.map +1 -1
  38. package/ts_build/src/agents/tools/list.js +2 -0
  39. package/ts_build/src/agents/tools/list.js.map +1 -1
  40. package/ts_build/src/agents/tools/loadImageAsBase64.d.ts +3 -0
  41. package/ts_build/src/agents/tools/loadImageAsBase64.js +79 -0
  42. package/ts_build/src/agents/tools/loadImageAsBase64.js.map +1 -0
  43. package/ts_build/src/auth/keyAuth.d.ts +1 -0
  44. package/ts_build/src/auth/keyAuth.js +24 -7
  45. package/ts_build/src/auth/keyAuth.js.map +1 -1
  46. package/ts_build/src/chat/modules/AgentModule.d.ts +3 -0
  47. package/ts_build/src/chat/modules/AgentModule.js +74 -34
  48. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  49. package/ts_build/src/clients/index.js +7 -4
  50. package/ts_build/src/clients/index.js.map +1 -1
  51. package/ts_build/src/clients/openai.js +29 -4
  52. package/ts_build/src/clients/openai.js.map +1 -1
  53. package/ts_build/src/commands/services.js +15 -8
  54. package/ts_build/src/commands/services.js.map +1 -1
  55. package/ts_build/src/login.js +19 -5
  56. package/ts_build/src/login.js.map +1 -1
  57. package/ts_build/src/processors/Base64ImageDetector.js +7 -3
  58. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  59. package/ts_build/src/services/KnowhowClient.js +5 -3
  60. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  61. package/ts_build/src/services/watchers/FsSyncer.d.ts +4 -0
  62. package/ts_build/src/services/watchers/FsSyncer.js +36 -2
  63. package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
  64. package/ts_build/src/services/watchers/RemoteSyncer.d.ts +4 -0
  65. package/ts_build/src/services/watchers/RemoteSyncer.js +36 -2
  66. package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
  67. package/ts_build/src/tunnel.js +19 -8
  68. package/ts_build/src/tunnel.js.map +1 -1
  69. package/ts_build/src/worker.js +47 -10
  70. package/ts_build/src/worker.js.map +1 -1
  71. package/ts_build/tests/clients/reasoningEffort.test.js +47 -0
  72. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -1
  73. package/ts_build/tests/processors/Base64ImageDetector.test.js +28 -0
  74. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  75. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.d.ts +1 -0
  76. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js +67 -0
  77. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js.map +1 -0
  78. package/ts_build/tests/unit/auth/keyAuth.test.js +43 -2
  79. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -1
  80. package/ts_build/tests/unit/login.test.js +29 -1
  81. package/ts_build/tests/unit/login.test.js.map +1 -1
  82. package/ts_build/tests/unit/services/KnowhowClient.test.d.ts +1 -0
  83. package/ts_build/tests/unit/services/KnowhowClient.test.js +69 -0
  84. package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -0
  85. package/ts_build/tests/unit/services/Mcp.test.d.ts +1 -0
  86. package/ts_build/tests/unit/services/Mcp.test.js +45 -0
  87. package/ts_build/tests/unit/services/Mcp.test.js.map +1 -0
  88. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.d.ts +1 -0
  89. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js +78 -0
  90. package/ts_build/tests/unit/services/SyncedAgentWatcher.test.js.map +1 -0
  91. package/ts_build/tests/unit/tunnel.test.d.ts +1 -0
  92. package/ts_build/tests/unit/tunnel.test.js +47 -0
  93. package/ts_build/tests/unit/tunnel.test.js.map +1 -0
@@ -0,0 +1,44 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import {
5
+ loadImageAsBase64,
6
+ loadImageAsBase64Definition,
7
+ } from "../../../../src/agents/tools/loadImageAsBase64";
8
+ import { Base64ImageProcessor } from "../../../../src/processors/Base64ImageDetector";
9
+ import { ToolsService } from "../../../../src/services/Tools";
10
+
11
+ describe("loadImageAsBase64 tool", () => {
12
+ it("returns an image data URL with a MIME type detected from its bytes", async () => {
13
+ const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "knowhow-image-"));
14
+ const filePath = path.join(directory, "incorrectly-named.jpg");
15
+
16
+ try {
17
+ await fs.promises.writeFile(filePath, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
18
+ const result = JSON.parse(await loadImageAsBase64(filePath));
19
+
20
+ expect(result).toMatchObject({
21
+ type: "image_url",
22
+ image_url: { detail: "auto" },
23
+ });
24
+ expect(result.image_url.url).toMatch(/^data:image\/png;base64,/);
25
+ } finally {
26
+ await fs.promises.rm(directory, { recursive: true, force: true });
27
+ }
28
+ });
29
+
30
+ it("can also be registered by the image message processor without duplication", () => {
31
+ const toolsService = new ToolsService();
32
+ toolsService.defineTools(
33
+ [loadImageAsBase64Definition],
34
+ { loadImageAsBase64 }
35
+ );
36
+
37
+ new Base64ImageProcessor(toolsService);
38
+
39
+ expect(
40
+ toolsService.getToolNames().filter((name) => name === "loadImageAsBase64")
41
+ ).toHaveLength(1);
42
+ expect(typeof toolsService.getFunction("loadImageAsBase64")).toBe("function");
43
+ });
44
+ });
@@ -14,6 +14,7 @@ jest.mock("../../../src/utils/http", () => ({
14
14
  }));
15
15
 
16
16
  jest.mock("../../../src/auth/keyManager", () => ({
17
+ getDefaultPrivateKeyPath: () => "/keys/default",
17
18
  keyPairExists,
18
19
  loadKeyPair,
19
20
  signMessage,
@@ -23,11 +24,16 @@ jest.mock("../../../src/auth/browserLogin", () => ({
23
24
  getCliUserAgent: () => "test-agent",
24
25
  }));
25
26
 
27
+ import fs from "fs";
28
+ import os from "os";
29
+ import path from "path";
26
30
  import {
27
31
  authenticateWithKey,
32
+ exchangeAvailablePublicKeyJwt,
28
33
  exchangePublicKeyJwt,
29
34
  hasKeyPair,
30
35
  registerPublicKey,
36
+ storeJwt,
31
37
  } from "../../../src/auth/keyAuth";
32
38
 
33
39
  describe("keyAuth", () => {
@@ -69,17 +75,61 @@ describe("keyAuth", () => {
69
75
  ]);
70
76
  });
71
77
 
78
+ it("uses the registered default identity when a configured identity is rejected", async () => {
79
+ post
80
+ .mockRejectedValueOnce({ status: 401 })
81
+ .mockResolvedValueOnce({
82
+ data: {
83
+ challengeId: "challenge_2",
84
+ message: "default key challenge",
85
+ expiresAt: "2099-01-01T00:00:00.000Z",
86
+ },
87
+ status: 201,
88
+ })
89
+ .mockResolvedValueOnce({ data: { jwt: "default-key-jwt" }, status: 200 });
90
+
91
+ await expect(
92
+ exchangeAvailablePublicKeyJwt("org-1", "https://work.example", "/keys/stale")
93
+ ).resolves.toBe("default-key-jwt");
94
+
95
+ expect(loadKeyPair.mock.calls).toEqual([
96
+ ["/keys/stale"],
97
+ ["/keys/default"],
98
+ ]);
99
+ expect(signMessage).toHaveBeenCalledWith(
100
+ "default key challenge",
101
+ "/keys/default"
102
+ );
103
+ });
104
+
72
105
  it("falls back when the key is unavailable without signing a challenge", async () => {
73
- post.mockRejectedValueOnce({ status: 401 });
106
+ post.mockRejectedValue({ status: 401 });
74
107
 
75
108
  await expect(
76
109
  authenticateWithKey("org-1", "https://work.example", "/keys/work")
77
110
  ).resolves.toBe(false);
78
111
 
79
- expect(post).toHaveBeenCalledTimes(1);
112
+ expect(post).toHaveBeenCalledTimes(2);
80
113
  expect(signMessage).not.toHaveBeenCalled();
81
114
  });
82
115
 
116
+ it("stores a refreshed JWT in the active isolated project", () => {
117
+ const originalCwd = process.cwd();
118
+ const temporaryProject = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-key-auth-"));
119
+ try {
120
+ process.chdir(temporaryProject);
121
+
122
+ storeJwt("fresh-project-jwt");
123
+
124
+ const jwtPath = path.join(temporaryProject, ".knowhow", ".jwt");
125
+ expect(fs.readFileSync(jwtPath, "utf8")).toBe("fresh-project-jwt");
126
+ expect(fs.statSync(jwtPath).mode & 0o777).toBe(0o600);
127
+ } finally {
128
+ process.chdir(originalCwd);
129
+ fs.rmSync(temporaryProject, { recursive: true, force: true });
130
+ }
131
+ });
132
+
83
133
  it("registers only server-owned Ed25519 key fields in the selected environment", async () => {
84
134
  post.mockResolvedValueOnce({ data: {}, status: 201 });
85
135
 
@@ -26,7 +26,10 @@ jest.mock("../../src/auth/keyAuth", () => ({
26
26
  registerPublicKey,
27
27
  }));
28
28
 
29
- jest.mock("../../src/auth/keyManager", () => ({ getOrCreatePublicKey }));
29
+ jest.mock("../../src/auth/keyManager", () => ({
30
+ getDefaultPrivateKeyPath: () => "/keys/default",
31
+ getOrCreatePublicKey,
32
+ }));
30
33
  jest.mock("../../src/config", () => ({ getConfig, updateConfig }));
31
34
  jest.mock("../../src/utils", () => ({ ask: jest.fn() }));
32
35
 
@@ -88,6 +91,41 @@ describe("login public-key bootstrap", () => {
88
91
  );
89
92
  });
90
93
 
94
+ it("does not persist a temporary identity path into project configuration", async () => {
95
+ const temporaryIdentity = path.join(os.tmpdir(), "knowhow-login-test-key");
96
+ getConfig.mockResolvedValue({
97
+ orgId: "org-1",
98
+ cliIdentityPath: "/tmp/knowhow-csrf-e2e-key",
99
+ modelProviders: [],
100
+ });
101
+ registerPublicKey.mockResolvedValueOnce(undefined);
102
+
103
+ await expect(login(false, temporaryIdentity)).resolves.toBeUndefined();
104
+
105
+ expect(getOrCreatePublicKey).toHaveBeenCalledWith(temporaryIdentity);
106
+ expect(updateConfig).toHaveBeenCalledTimes(1);
107
+ expect(updateConfig.mock.calls[0][0]).toEqual(
108
+ expect.not.objectContaining({
109
+ cliIdentityPath: expect.any(String),
110
+ })
111
+ );
112
+ });
113
+
114
+ it("persists the default identity selected during browser bootstrap", async () => {
115
+ registerPublicKey.mockResolvedValueOnce(undefined);
116
+
117
+ await expect(login(false)).resolves.toBeUndefined();
118
+
119
+ expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/default");
120
+ expect(updateConfig).toHaveBeenCalledWith(
121
+ expect.objectContaining({
122
+ orgId: "org-1",
123
+ cliIdentityPath: "/keys/default",
124
+ modelProviders: [{ provider: "knowhow" }],
125
+ })
126
+ );
127
+ });
128
+
91
129
  it("safely falls back to browser PKCE when the selected key cannot authenticate", async () => {
92
130
  hasKeyPair.mockReturnValue(true);
93
131
  getConfig.mockResolvedValue({ orgId: "org-1", modelProviders: [] });
@@ -0,0 +1,84 @@
1
+ import fs from "fs";
2
+
3
+ const mockGet = jest.fn();
4
+ const mockExchangePublicKeyJwt = jest.fn();
5
+ const mockStoreJwt = jest.fn();
6
+
7
+ jest.mock("../../../src/utils/http", () => {
8
+ const actual = jest.requireActual("../../../src/utils/http");
9
+ const mockedHttp = {
10
+ get: (...args: unknown[]) => mockGet(...args),
11
+ };
12
+ return {
13
+ __esModule: true,
14
+ ...actual,
15
+ http: mockedHttp,
16
+ default: mockedHttp,
17
+ };
18
+ });
19
+
20
+ jest.mock("../../../src/config", () => ({
21
+ getConfigSync: jest.fn().mockReturnValue({
22
+ orgId: "org-1",
23
+ cliIdentityPath: "/tmp/identity",
24
+ }),
25
+ }));
26
+
27
+ jest.mock("../../../src/auth/keyManager", () => ({
28
+ keyPairExists: jest.fn().mockReturnValue(true),
29
+ }));
30
+
31
+ jest.mock("../../../src/auth/keyAuth", () => ({
32
+ exchangeAvailablePublicKeyJwt: (...args: unknown[]) => mockExchangePublicKeyJwt(...args),
33
+ storeJwt: (...args: unknown[]) => mockStoreJwt(...args),
34
+ }));
35
+
36
+ import { KnowhowSimpleClient } from "../../../src/services/KnowhowClient";
37
+
38
+ describe("KnowhowSimpleClient identity renewal", () => {
39
+ afterEach(() => {
40
+ jest.restoreAllMocks();
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ it("obtains a JWT from the registered identity before loading models when no JWT exists", async () => {
45
+ jest.spyOn(fs, "existsSync").mockReturnValue(false);
46
+ mockExchangePublicKeyJwt.mockResolvedValue("fresh-jwt");
47
+ mockGet.mockResolvedValueOnce({ data: { data: [] } });
48
+
49
+ const client = new KnowhowSimpleClient("https://api.example.test");
50
+ await client.getModels();
51
+
52
+ expect(mockExchangePublicKeyJwt).toHaveBeenCalledWith(
53
+ "org-1",
54
+ "https://api.example.test",
55
+ "/tmp/identity"
56
+ );
57
+ expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
58
+ expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
59
+ });
60
+
61
+ it("replaces an expired JWT with one signed by the registered identity before loading models", async () => {
62
+ const expiredJwt = [
63
+ Buffer.from('{"alg":"none"}').toString("base64url"),
64
+ Buffer.from('{"exp":0}').toString("base64url"),
65
+ "old-signature",
66
+ ].join(".");
67
+ jest.spyOn(fs, "existsSync").mockReturnValue(true);
68
+ jest.spyOn(fs, "readFileSync").mockReturnValue(expiredJwt);
69
+ mockExchangePublicKeyJwt.mockResolvedValue("fresh-jwt");
70
+ mockGet.mockResolvedValueOnce({ data: { data: [] } });
71
+
72
+ const client = new KnowhowSimpleClient("https://api.example.test");
73
+ await client.getModels();
74
+
75
+ expect(mockExchangePublicKeyJwt).toHaveBeenCalledWith(
76
+ "org-1",
77
+ "https://api.example.test",
78
+ "/tmp/identity"
79
+ );
80
+ expect(mockStoreJwt).toHaveBeenCalledWith("fresh-jwt");
81
+ expect(mockGet).toHaveBeenCalledTimes(1);
82
+ expect(mockGet.mock.calls[0][1].headers.Authorization).toBe("Bearer fresh-jwt");
83
+ });
84
+ });
@@ -0,0 +1,56 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ interface CapturedTransportOptions {
6
+ requestInit?: { headers?: Record<string, string> };
7
+ }
8
+
9
+ let capturedOptions: CapturedTransportOptions | undefined;
10
+
11
+ jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
12
+ StreamableHTTPClientTransport: jest.fn().mockImplementation(
13
+ (_url: URL, options: unknown) => {
14
+ capturedOptions = options as CapturedTransportOptions;
15
+ return { close: jest.fn() };
16
+ }
17
+ ),
18
+ }));
19
+
20
+ import { McpService } from "../../../src/services/Mcp";
21
+
22
+ describe("McpService JWT file transport", () => {
23
+ let temporaryDirectory: string;
24
+ let tokenFile: string;
25
+
26
+ beforeEach(() => {
27
+ capturedOptions = undefined;
28
+ temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-mcp-"));
29
+ tokenFile = path.join(temporaryDirectory, ".jwt");
30
+ });
31
+
32
+ afterEach(() => {
33
+ fs.rmSync(temporaryDirectory, { recursive: true, force: true });
34
+ });
35
+
36
+ it("reads the refreshed JWT when the transport is created", () => {
37
+ fs.writeFileSync(tokenFile, "expired-jwt");
38
+ fs.writeFileSync(tokenFile, "fresh-jwt");
39
+
40
+ new McpService().createTransport({
41
+ name: "authenticated-remote",
42
+ url: "https://api.example.test/mcp",
43
+ authorization_token_file: tokenFile,
44
+ });
45
+
46
+ expect(capturedOptions).toEqual(
47
+ expect.objectContaining({
48
+ requestInit: {
49
+ headers: expect.objectContaining({
50
+ Authorization: "Bearer fresh-jwt",
51
+ }),
52
+ },
53
+ })
54
+ );
55
+ });
56
+ });
@@ -0,0 +1,89 @@
1
+ import { FsSyncedAgentWatcher } from "../../../src/services/watchers/FsSyncer";
2
+ import { WebSyncedAgentWatcher } from "../../../src/services/watchers/RemoteSyncer";
3
+
4
+ const retained = { role: "assistant", content: "before compaction" };
5
+ const oldThread = [
6
+ { role: "system", content: "instructions" },
7
+ { role: "user", content: "task" },
8
+ retained,
9
+ ];
10
+ const compactedThread = [
11
+ { role: "system", content: "instructions" },
12
+ { role: "user", content: "compressed summary" },
13
+ retained,
14
+ { role: "assistant", content: "after compaction" },
15
+ ];
16
+
17
+ function seedCursor(watcher: any): void {
18
+ watcher.taskId = "task-1";
19
+ watcher.agentName = "Developer";
20
+ watcher.lastThreadIndex = 0;
21
+ watcher.lastThreadLength = oldThread.length;
22
+ watcher.lastMessageJson = JSON.stringify(retained);
23
+ }
24
+
25
+ describe("synced agent watcher thread rollover", () => {
26
+ it("continues rendering after filesystem agent compaction", async () => {
27
+ const watcher: any = new FsSyncedAgentWatcher();
28
+ seedCursor(watcher);
29
+ watcher.readMetadata = jest.fn().mockResolvedValue({
30
+ threads: [oldThread, compactedThread],
31
+ status: "running",
32
+ });
33
+
34
+ const messages: string[] = [];
35
+ watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
36
+ messages.push(data.message);
37
+ });
38
+
39
+ await watcher.onMetadataChanged();
40
+
41
+ expect(messages).toEqual(["after compaction"]);
42
+ expect(watcher.lastThreadIndex).toBe(1);
43
+ expect(watcher.lastThreadLength).toBe(compactedThread.length);
44
+ });
45
+
46
+ it("continues rendering after web agent compaction", async () => {
47
+ const client = {
48
+ getTaskDetails: jest.fn().mockResolvedValue({
49
+ data: {
50
+ threads: [oldThread, compactedThread],
51
+ status: "running",
52
+ },
53
+ }),
54
+ };
55
+ const watcher: any = new WebSyncedAgentWatcher(client as any);
56
+ seedCursor(watcher);
57
+
58
+ const messages: string[] = [];
59
+ watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
60
+ messages.push(data.message);
61
+ });
62
+
63
+ await watcher.onPoll();
64
+
65
+ expect(messages).toEqual(["after compaction"]);
66
+ expect(watcher.lastThreadIndex).toBe(1);
67
+ expect(watcher.lastThreadLength).toBe(compactedThread.length);
68
+ });
69
+
70
+ it("resets a cursor when a thread shrinks without changing index", async () => {
71
+ const watcher: any = new FsSyncedAgentWatcher();
72
+ seedCursor(watcher);
73
+ watcher.lastThreadLength = 10;
74
+ watcher.readMetadata = jest.fn().mockResolvedValue({
75
+ threads: [compactedThread],
76
+ status: "running",
77
+ });
78
+
79
+ const messages: string[] = [];
80
+ watcher.agentEvents.on(watcher.eventTypes.agentSay, (data: any) => {
81
+ messages.push(data.message);
82
+ });
83
+
84
+ await watcher.onMetadataChanged();
85
+
86
+ expect(messages).toEqual(["after compaction"]);
87
+ expect(watcher.lastThreadLength).toBe(compactedThread.length);
88
+ });
89
+ });
@@ -0,0 +1,51 @@
1
+ import { extractTunnelDomain } from "../../src/tunnel";
2
+
3
+ describe("extractTunnelDomain", () => {
4
+ const originalTunnelWorkerDomain = process.env.TUNNEL_WORKER_DOMAIN;
5
+
6
+ beforeEach(() => {
7
+ delete process.env.TUNNEL_WORKER_DOMAIN;
8
+ });
9
+
10
+ afterAll(() => {
11
+ if (originalTunnelWorkerDomain === undefined) {
12
+ delete process.env.TUNNEL_WORKER_DOMAIN;
13
+ } else {
14
+ process.env.TUNNEL_WORKER_DOMAIN = originalTunnelWorkerDomain;
15
+ }
16
+ });
17
+
18
+ it("uses the local tunnel proxy for a localhost API URL", () => {
19
+ expect(extractTunnelDomain("http://localhost:4000")).toEqual({
20
+ domain: "worker.localhost:4000",
21
+ useHttps: false,
22
+ });
23
+ });
24
+
25
+ it("uses the local tunnel proxy for loopback API URLs", () => {
26
+ expect(extractTunnelDomain("http://127.0.0.1:4000")).toEqual({
27
+ domain: "worker.localhost:4000",
28
+ useHttps: false,
29
+ });
30
+ });
31
+
32
+ it("continues to select dev and production tunnel domains", () => {
33
+ expect(extractTunnelDomain("https://api.dev.knowhow.tyvm.ai")).toEqual({
34
+ domain: "worker.dev.tyvm-apps.com",
35
+ useHttps: true,
36
+ });
37
+ expect(extractTunnelDomain("https://api.knowhow.tyvm.ai")).toEqual({
38
+ domain: "worker.tyvm-apps.com",
39
+ useHttps: true,
40
+ });
41
+ });
42
+
43
+ it("allows an explicit tunnel domain to override localhost detection", () => {
44
+ process.env.TUNNEL_WORKER_DOMAIN = "worker.override.example";
45
+
46
+ expect(extractTunnelDomain("http://localhost:4000")).toEqual({
47
+ domain: "worker.override.example",
48
+ useHttps: false,
49
+ });
50
+ });
51
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.154",
3
+ "version": "0.0.156",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -191,6 +191,7 @@ export declare abstract class BaseAgent implements IAgent {
191
191
  role: string;
192
192
  content: string;
193
193
  } | null;
194
+ extractContentFromMessages(messages: Message[]): string;
194
195
  getTaskBreakdown(messages: Message[]): Promise<string>;
195
196
  compressMessages(messages: Message[]): Promise<Message[]>;
196
197
  }
@@ -1017,6 +1017,17 @@ let BaseAgent = class BaseAgent {
1017
1017
  }
1018
1018
  return null;
1019
1019
  }
1020
+ extractContentFromMessages(messages) {
1021
+ const content = messages
1022
+ .filter((m) => typeof m.content === "string" && m.content.trim())
1023
+ .map((m) => m.content);
1024
+ const finalAnswer = messages
1025
+ .filter((m) => m.tool_calls?.length)
1026
+ .flatMap((t) => t.tool_calls)
1027
+ .filter((t) => t.function.name === "finalAnswer")
1028
+ .map((t) => t.function.arguments);
1029
+ return [...content, ...finalAnswer].join("\n\n");
1030
+ }
1020
1031
  async getTaskBreakdown(messages) {
1021
1032
  if (this.taskBreakdown) {
1022
1033
  return this.taskBreakdown;
@@ -1039,22 +1050,12 @@ let BaseAgent = class BaseAgent {
1039
1050
  },
1040
1051
  ];
1041
1052
  const response = await this.createAgentCompletion({
1042
- model,
1043
1053
  messages: taskBreakdownMessages,
1044
1054
  tool_choice: "none",
1045
- long_ttl_cache: this.runTime() > 300_000,
1046
- ...(this.reasoningEffort !== undefined && {
1047
- reasoning_effort: this.reasoningEffort,
1048
- }),
1049
- ...(this.summarizeReasoning !== undefined && {
1050
- reasoning_summary: this.summarizeReasoning,
1051
- }),
1052
1055
  });
1053
- const breakdownContent = response.choices
1054
- .map((c) => c.message.content)
1055
- .filter((s) => typeof s === "string" && s.trim().length > 0)
1056
- .join("\n\n");
1056
+ const breakdownContent = this.extractContentFromMessages(response.choices?.map((c) => c.message));
1057
1057
  if (!breakdownContent) {
1058
+ console.log(JSON.stringify(response.choices, null, 2));
1058
1059
  throw new Error("Compaction task-breakdown request returned no text content");
1059
1060
  }
1060
1061
  this.taskBreakdown = breakdownContent;
@@ -1063,8 +1064,7 @@ let BaseAgent = class BaseAgent {
1063
1064
  }
1064
1065
  async compressMessages(messages) {
1065
1066
  let resumeIndex = messages.length - 1;
1066
- while (resumeIndex >= 0 &&
1067
- messages[resumeIndex].role !== "assistant") {
1067
+ while (resumeIndex >= 0 && messages[resumeIndex].role !== "assistant") {
1068
1068
  resumeIndex--;
1069
1069
  }
1070
1070
  const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
@@ -1080,7 +1080,9 @@ let BaseAgent = class BaseAgent {
1080
1080
  3. Next Steps - what we're about to do next to continue the user's original request.
1081
1081
  4. Tasks remaining - what tasks are left from the initial task breakdown.
1082
1082
 
1083
- ${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
1083
+ ${this.taskBreakdown
1084
+ ? `Our initial task breakdown: ${this.taskBreakdown}`
1085
+ : ""}
1084
1086
 
1085
1087
  This summary will replace the older history. The latest agent interaction will remain verbatim:
1086
1088
 
@@ -1096,21 +1098,24 @@ let BaseAgent = class BaseAgent {
1096
1098
  messages: compressMessagesPayload,
1097
1099
  tool_choice: "none",
1098
1100
  });
1099
- const summaries = response.choices
1100
- .map((c) => c.message.content)
1101
- .filter((s) => typeof s === "string" && s.trim().length > 0);
1102
- if (summaries.length === 0) {
1101
+ const summary = this.extractContentFromMessages(response.choices?.map((c) => c.message));
1102
+ if (summary.length === 0) {
1103
+ console.log(JSON.stringify(response.choices, null, 2));
1103
1104
  throw new Error("Compaction summary request returned no text content");
1104
1105
  }
1105
- this.summaries.push(...summaries);
1106
+ this.summaries.push(summary);
1106
1107
  const startMessages = [
1107
1108
  {
1108
1109
  role: "user",
1109
1110
  content: `
1110
- ${this.taskBreakdown ? `Initial task breakdown:\n ${this.taskBreakdown}` : "(No task breakdown available — summarize what you know from context above)"}
1111
+ ${this.taskBreakdown
1112
+ ? `Initial task breakdown:\n ${this.taskBreakdown}`
1113
+ : "(No task breakdown available — summarize what you know from context above)"}
1111
1114
 
1112
1115
  We have just compressed the conversation to save memory:
1113
- ${summaries.length > 0 ? JSON.stringify(summaries) : "(summary unavailable — please continue from the task breakdown above)"}
1116
+ ${summary.length > 0
1117
+ ? JSON.stringify(summary)
1118
+ : "(summary unavailable — please continue from the task breakdown above)"}
1114
1119
 
1115
1120
  Please continue the task from where we left off
1116
1121
  `,