@tyvm/knowhow 0.0.155 → 0.0.157

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 (116) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +15 -5
  3. package/src/agents/tools/index.ts +1 -0
  4. package/src/agents/tools/list.ts +2 -0
  5. package/src/agents/tools/loadImageAsBase64.ts +83 -0
  6. package/src/auth/keyAuth.ts +5 -5
  7. package/src/chat/modules/AgentModule.ts +3 -3
  8. package/src/chat/modules/RemoteSyncModule.ts +45 -17
  9. package/src/clients/anthropic.ts +156 -79
  10. package/src/clients/attribution.ts +53 -0
  11. package/src/clients/http.ts +211 -84
  12. package/src/clients/index.ts +21 -9
  13. package/src/clients/openai.ts +441 -220
  14. package/src/clients/openrouter.ts +10 -3
  15. package/src/clients/pricing/openai.ts +15 -1
  16. package/src/clients/pricing/types.ts +15 -0
  17. package/src/clients/responseMetadata.ts +221 -0
  18. package/src/clients/types.ts +49 -4
  19. package/src/commands/services.ts +23 -18
  20. package/src/login.ts +22 -6
  21. package/src/processors/Base64ImageDetector.ts +12 -11
  22. package/src/tunnel.ts +13 -3
  23. package/tests/processors/Base64ImageDetector.test.ts +32 -0
  24. package/tests/unit/agents/tools/loadImageAsBase64.test.ts +44 -0
  25. package/tests/unit/auth/keyAuth.test.ts +21 -0
  26. package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
  27. package/tests/unit/clients/attribution.test.ts +185 -0
  28. package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
  29. package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
  30. package/tests/unit/clients/responseMetadata.test.ts +123 -0
  31. package/tests/unit/login.test.ts +39 -1
  32. package/tests/unit/services/Mcp.test.ts +56 -0
  33. package/tests/unit/tunnel.test.ts +51 -0
  34. package/ts_build/package.json +1 -1
  35. package/ts_build/src/agents/base/base.js +14 -4
  36. package/ts_build/src/agents/base/base.js.map +1 -1
  37. package/ts_build/src/agents/tools/index.d.ts +1 -0
  38. package/ts_build/src/agents/tools/index.js +1 -0
  39. package/ts_build/src/agents/tools/index.js.map +1 -1
  40. package/ts_build/src/agents/tools/list.js +2 -0
  41. package/ts_build/src/agents/tools/list.js.map +1 -1
  42. package/ts_build/src/agents/tools/loadImageAsBase64.d.ts +3 -0
  43. package/ts_build/src/agents/tools/loadImageAsBase64.js +79 -0
  44. package/ts_build/src/agents/tools/loadImageAsBase64.js.map +1 -0
  45. package/ts_build/src/auth/keyAuth.js +5 -5
  46. package/ts_build/src/auth/keyAuth.js.map +1 -1
  47. package/ts_build/src/chat/modules/AgentModule.js +3 -3
  48. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  49. package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
  50. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
  51. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  52. package/ts_build/src/clients/anthropic.d.ts +1 -0
  53. package/ts_build/src/clients/anthropic.js +83 -38
  54. package/ts_build/src/clients/anthropic.js.map +1 -1
  55. package/ts_build/src/clients/attribution.d.ts +2 -0
  56. package/ts_build/src/clients/attribution.js +32 -0
  57. package/ts_build/src/clients/attribution.js.map +1 -0
  58. package/ts_build/src/clients/http.js +100 -20
  59. package/ts_build/src/clients/http.js.map +1 -1
  60. package/ts_build/src/clients/index.d.ts +2 -1
  61. package/ts_build/src/clients/index.js +6 -2
  62. package/ts_build/src/clients/index.js.map +1 -1
  63. package/ts_build/src/clients/openai.d.ts +2 -1
  64. package/ts_build/src/clients/openai.js +225 -99
  65. package/ts_build/src/clients/openai.js.map +1 -1
  66. package/ts_build/src/clients/openrouter.d.ts +2 -2
  67. package/ts_build/src/clients/openrouter.js +6 -2
  68. package/ts_build/src/clients/openrouter.js.map +1 -1
  69. package/ts_build/src/clients/pricing/openai.js +15 -1
  70. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  71. package/ts_build/src/clients/pricing/types.d.ts +6 -0
  72. package/ts_build/src/clients/pricing/types.js.map +1 -1
  73. package/ts_build/src/clients/responseMetadata.d.ts +15 -0
  74. package/ts_build/src/clients/responseMetadata.js +141 -0
  75. package/ts_build/src/clients/responseMetadata.js.map +1 -0
  76. package/ts_build/src/clients/types.d.ts +18 -1
  77. package/ts_build/src/clients/types.js +2 -1
  78. package/ts_build/src/clients/types.js.map +1 -1
  79. package/ts_build/src/commands/services.js +15 -8
  80. package/ts_build/src/commands/services.js.map +1 -1
  81. package/ts_build/src/login.js +19 -5
  82. package/ts_build/src/login.js.map +1 -1
  83. package/ts_build/src/processors/Base64ImageDetector.js +7 -3
  84. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  85. package/ts_build/src/tunnel.js +9 -1
  86. package/ts_build/src/tunnel.js.map +1 -1
  87. package/ts_build/tests/processors/Base64ImageDetector.test.js +28 -0
  88. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  89. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.d.ts +1 -0
  90. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js +67 -0
  91. package/ts_build/tests/unit/agents/tools/loadImageAsBase64.test.js.map +1 -0
  92. package/ts_build/tests/unit/auth/keyAuth.test.js +21 -0
  93. package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -1
  94. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
  95. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
  96. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
  97. package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
  98. package/ts_build/tests/unit/clients/attribution.test.js +153 -0
  99. package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
  100. package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
  101. package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
  102. package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
  103. package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
  104. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
  105. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
  106. package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
  107. package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
  108. package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
  109. package/ts_build/tests/unit/login.test.js +29 -1
  110. package/ts_build/tests/unit/login.test.js.map +1 -1
  111. package/ts_build/tests/unit/services/Mcp.test.d.ts +1 -0
  112. package/ts_build/tests/unit/services/Mcp.test.js +45 -0
  113. package/ts_build/tests/unit/services/Mcp.test.js.map +1 -0
  114. package/ts_build/tests/unit/tunnel.test.d.ts +1 -0
  115. package/ts_build/tests/unit/tunnel.test.js +47 -0
  116. package/ts_build/tests/unit/tunnel.test.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.155",
3
+ "version": "0.0.157",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1307,10 +1307,19 @@ export abstract class BaseAgent implements IAgent {
1307
1307
  this.logStatus();
1308
1308
 
1309
1309
  const continuation = `<Workflow>
1310
- Task terminates after you call on of these tools: ${JSON.stringify(
1310
+ This is an automated runtime instruction, not a new user message.
1311
+ The original user request remains active. Do not say that the user's
1312
+ message was empty, and do not ask the user to repeat the request.
1313
+
1314
+ Your previous response did not terminate the task because it did not
1315
+ call one of the required tools: ${JSON.stringify(
1311
1316
  this.requiredToolNames
1312
- )}.\n
1313
- User likely only sees output from the required tool call.
1317
+ )}.
1318
+ If your previous response was intended as the final response, call the
1319
+ appropriate required tool now with that response. Otherwise, continue
1320
+ working on the original request and call an appropriate required tool
1321
+ when the task is complete.
1322
+
1314
1323
  <TaskStatus>${statusMessage}</TaskStatus>
1315
1324
  </Workflow>`;
1316
1325
 
@@ -1680,7 +1689,7 @@ export abstract class BaseAgent implements IAgent {
1680
1689
  }
1681
1690
 
1682
1691
  const taskPrompt = `
1683
- Analyze all previous messages.
1692
+ TASK INTERRUPT. CONTEXT COMPACTION NEEDED. Analyze all previous messages.
1684
1693
 
1685
1694
  Generate a detailed task breakdown for this conversation, include a section for the following:
1686
1695
  1. Task List
@@ -1737,7 +1746,8 @@ export abstract class BaseAgent implements IAgent {
1737
1746
  this.log(
1738
1747
  `Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`
1739
1748
  );
1740
- const toCompressPrompt = `We are compressing our conversation to save memory.
1749
+ const toCompressPrompt = `TASK INTERRUPT. CONTEXT COMPACTION NEEDED.
1750
+ We are compressing our conversation to save memory.
1741
1751
  Please summarize the conversation so far, so that we may continue the original task with a smaller context
1742
1752
 
1743
1753
  Include the following sections:
@@ -13,6 +13,7 @@ export * from "./readFile";
13
13
  export * from "./scanFile";
14
14
  export * from "./textSearch";
15
15
  export * from "./visionTool";
16
+ export * from "./loadImageAsBase64";
16
17
  export * from "./writeFile";
17
18
  export * from "./fileSearch";
18
19
  export * from "./language";
@@ -20,6 +20,7 @@ import {
20
20
  stopObservingDefinition,
21
21
  } from "./observe";
22
22
  import { skillsToolDefinitions } from "./skills";
23
+ import { loadImageAsBase64Definition } from "./loadImageAsBase64";
23
24
 
24
25
  function getPluginNames(): string {
25
26
  try {
@@ -720,6 +721,7 @@ export const includedTools = [
720
721
  },
721
722
  },
722
723
  },
724
+ loadImageAsBase64Definition,
723
725
  googleSearchDefinition,
724
726
  startAgentTaskDefinition,
725
727
  replyToParentDefinition,
@@ -0,0 +1,83 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Tool } from "../../clients/types";
4
+
5
+ const MAX_IMAGE_BYTES = 15 * 1024 * 1024;
6
+
7
+ const MIME_TYPES: Record<string, string> = {
8
+ png: "image/png",
9
+ jpg: "image/jpeg",
10
+ jpeg: "image/jpeg",
11
+ gif: "image/gif",
12
+ webp: "image/webp",
13
+ bmp: "image/bmp",
14
+ svg: "image/svg+xml",
15
+ avif: "image/avif",
16
+ ico: "image/x-icon",
17
+ };
18
+
19
+ function detectMimeType(buffer: Buffer, fallback: string): string {
20
+ if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
21
+ return "image/jpeg";
22
+ }
23
+ if (buffer.length >= 4 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
24
+ return "image/png";
25
+ }
26
+ if (buffer.length >= 4 && buffer.subarray(0, 4).toString("ascii") === "GIF8") {
27
+ return "image/gif";
28
+ }
29
+ if (
30
+ buffer.length >= 12 &&
31
+ buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
32
+ buffer.subarray(8, 12).toString("ascii") === "WEBP"
33
+ ) {
34
+ return "image/webp";
35
+ }
36
+ return fallback;
37
+ }
38
+
39
+ /** Load a reasonably sized image from the worker filesystem for UI/AI preview. */
40
+ export async function loadImageAsBase64(
41
+ filePath: string,
42
+ detail: "auto" | "low" | "high" = "auto"
43
+ ): Promise<string> {
44
+ const stats = await fs.promises.stat(filePath).catch(() => null);
45
+ if (!stats) throw new Error(`File not found: ${filePath}`);
46
+ if (!stats.isFile()) throw new Error(`Path is not a file: ${filePath}`);
47
+ if (stats.size > MAX_IMAGE_BYTES) {
48
+ throw new Error(`Image is too large to preview (${Math.ceil(stats.size / 1024 / 1024)} MB; maximum 15 MB)`);
49
+ }
50
+
51
+ const extension = path.extname(filePath).slice(1).toLowerCase();
52
+ const extensionMimeType = MIME_TYPES[extension];
53
+ if (!extensionMimeType) {
54
+ throw new Error(`Unsupported image format: ${extension || "unknown"}`);
55
+ }
56
+
57
+ const image = await fs.promises.readFile(filePath);
58
+ const mimeType = detectMimeType(image, extensionMimeType);
59
+ return JSON.stringify({
60
+ type: "image_url",
61
+ image_url: {
62
+ url: `data:${mimeType};base64,${image.toString("base64")}`,
63
+ detail,
64
+ },
65
+ });
66
+ }
67
+
68
+ export const loadImageAsBase64Definition: Tool = {
69
+ type: "function",
70
+ function: {
71
+ name: "loadImageAsBase64",
72
+ description: "Load an image from the worker filesystem and return it as a base64 data URL for previewing.",
73
+ parameters: {
74
+ type: "object",
75
+ positional: true,
76
+ properties: {
77
+ filePath: { type: "string", description: "Absolute or workspace-relative image path" },
78
+ detail: { type: "string", enum: ["auto", "low", "high"], description: "Image detail hint" },
79
+ },
80
+ required: ["filePath"],
81
+ },
82
+ },
83
+ };
@@ -10,7 +10,6 @@ import {
10
10
  signMessage,
11
11
  } from "./keyManager";
12
12
 
13
- const JWT_FILE_PATH = path.join(process.cwd(), ".knowhow", ".jwt");
14
13
  const DEFAULT_API_URL = process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
15
14
 
16
15
  /**
@@ -140,13 +139,14 @@ export async function registerPublicKey(
140
139
  }
141
140
 
142
141
  export function storeJwt(jwt: string): void {
143
- const directory = path.dirname(JWT_FILE_PATH);
144
- const temporaryPath = `${JWT_FILE_PATH}.${process.pid}.tmp`;
142
+ const jwtFilePath = path.join(process.cwd(), ".knowhow", ".jwt");
143
+ const directory = path.dirname(jwtFilePath);
144
+ const temporaryPath = `${jwtFilePath}.${process.pid}.tmp`;
145
145
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
146
146
  try {
147
147
  fs.writeFileSync(temporaryPath, jwt, { mode: 0o600, flag: "wx" });
148
- fs.renameSync(temporaryPath, JWT_FILE_PATH);
149
- fs.chmodSync(JWT_FILE_PATH, 0o600);
148
+ fs.renameSync(temporaryPath, jwtFilePath);
149
+ fs.chmodSync(jwtFilePath, 0o600);
150
150
  } finally {
151
151
  fs.rmSync(temporaryPath, { force: true });
152
152
  }
@@ -810,7 +810,7 @@ export class AgentModule extends BaseChatModule {
810
810
  // Match new interactive threads: local-only sessions get a fresh remote
811
811
  // task when auto-sync is enabled.
812
812
  if (!remoteTaskId && this.remoteSyncModule?.isAutoSyncEnabled()) {
813
- await this.remoteSyncModule.syncTask(taskId);
813
+ this.remoteSyncModule.syncTaskInBackground(taskId);
814
814
  }
815
815
 
816
816
  // After resume finishes, revert to normal chat (non-agent mode) so the
@@ -1471,7 +1471,7 @@ export class AgentModule extends BaseChatModule {
1471
1471
  !remoteTaskId &&
1472
1472
  this.remoteSyncModule?.isAutoSyncEnabled()
1473
1473
  ) {
1474
- await this.remoteSyncModule.syncTask(result.taskId);
1474
+ this.remoteSyncModule.syncTaskInBackground(result.taskId);
1475
1475
  }
1476
1476
 
1477
1477
  await this.attachedAgentChatLoop(
@@ -1566,7 +1566,7 @@ export class AgentModule extends BaseChatModule {
1566
1566
 
1567
1567
  // If auto-sync is enabled, push this task to the remote KnowHow app
1568
1568
  if (this.remoteSyncModule?.isAutoSyncEnabled()) {
1569
- await this.remoteSyncModule.syncTask(taskId);
1569
+ this.remoteSyncModule.syncTaskInBackground(taskId);
1570
1570
  }
1571
1571
 
1572
1572
  await this.attachedAgentChatLoop(taskId, agent, formattedPrompt);
@@ -14,6 +14,7 @@ import { AgentSyncFs } from "../../services/AgentSyncFs";
14
14
  import { AgentModule } from "./AgentModule";
15
15
  import { TaskInfo } from "../types";
16
16
  import { getConfig, updateConfig } from "../../config";
17
+ import { withRetry } from "../../clients/withRetry";
17
18
 
18
19
  @TraceAll()
19
20
  export class RemoteSyncModule extends BaseChatModule {
@@ -44,6 +45,22 @@ export class RemoteSyncModule extends BaseChatModule {
44
45
  this.client = new KnowhowSimpleClient(KNOWHOW_API_URL);
45
46
  }
46
47
 
48
+ /**
49
+ * Start an automatic sync without delaying agent execution. Manual syncs use
50
+ * syncTask directly so the command still waits for and reports its result.
51
+ */
52
+ public syncTaskInBackground(taskId: string): void {
53
+ void this.syncTask(taskId).catch((error: unknown) => {
54
+ // syncTask handles expected API failures itself. Keep this final guard so
55
+ // an unexpected failure can never become an unhandled rejection.
56
+ console.error(
57
+ `āŒ Unexpected remote sync failure: ${
58
+ error instanceof Error ? error.message : String(error)
59
+ }`
60
+ );
61
+ });
62
+ }
63
+
47
64
  /**
48
65
  * On initialize, read config to set initial autoSync state.
49
66
  */
@@ -157,11 +174,15 @@ export class RemoteSyncModule extends BaseChatModule {
157
174
  }
158
175
 
159
176
  console.log("šŸ“” Creating remote session for this terminal session...");
160
- const result = await this.client.createSessionPlaceholder({
161
- title: title.slice(0, 80) || "CLI Session",
162
- workerId: this.workerId,
163
- metadata: { source: "cli", createdAt: new Date().toISOString() },
164
- });
177
+ const result = await withRetry(
178
+ () =>
179
+ this.client.createSessionPlaceholder({
180
+ title: title.slice(0, 80) || "CLI Session",
181
+ workerId: this.workerId,
182
+ metadata: { source: "cli", createdAt: new Date().toISOString() },
183
+ }),
184
+ { maxRetries: 3, backoffMs: 500 }
185
+ );
165
186
 
166
187
  this.remoteSessionId = result.sessionId;
167
188
  console.log(`āœ… Remote session created: ${result.sessionId}`);
@@ -177,12 +198,16 @@ export class RemoteSyncModule extends BaseChatModule {
177
198
  agentName?: string,
178
199
  modelName?: string
179
200
  ): Promise<{ messageId: string; taskId: string | undefined }> {
180
- const result = await this.client.createMessagePlaceholder(sessionId, {
181
- content: prompt,
182
- agentName,
183
- modelName,
184
- metadata: { source: "cli", createdAt: new Date().toISOString() },
185
- });
201
+ const result = await withRetry(
202
+ () =>
203
+ this.client.createMessagePlaceholder(sessionId, {
204
+ content: prompt,
205
+ agentName,
206
+ modelName,
207
+ metadata: { source: "cli", createdAt: new Date().toISOString() },
208
+ }),
209
+ { maxRetries: 3, backoffMs: 500 }
210
+ );
186
211
  return { messageId: result.messageId, taskId: result.taskId };
187
212
  }
188
213
 
@@ -273,7 +298,10 @@ export class RemoteSyncModule extends BaseChatModule {
273
298
  try {
274
299
  // Step 1: Ensure remote session exists
275
300
  console.log("\nšŸ” Checking KnowHow API credentials...");
276
- const me = await this.client.me();
301
+ const me = await withRetry(() => this.client.me(), {
302
+ maxRetries: 3,
303
+ backoffMs: 500,
304
+ });
277
305
  console.log(`āœ… Authenticated as ${me.data?.email || "unknown"}`);
278
306
 
279
307
  const title =
@@ -341,13 +369,13 @@ export class RemoteSyncModule extends BaseChatModule {
341
369
 
342
370
  this.syncedMessageCount++;
343
371
 
344
- const baseUrl = KNOWHOW_API_URL.replace("/api", "").replace(
345
- "api.",
346
- ""
347
- );
372
+ const chatUrl = new URL(KNOWHOW_API_URL);
373
+ chatUrl.hostname = chatUrl.hostname.replace(/^api\./, "");
374
+ chatUrl.pathname = chatUrl.pathname.replace(/\/api\/?$/, "");
375
+ chatUrl.pathname = `${chatUrl.pathname.replace(/\/$/, "")}/chat/${sessionId}`;
348
376
  console.log(
349
377
  `\nšŸ’¾ Local session updated with remote IDs.\n` +
350
- `🌐 View your task at: ${baseUrl}/chat/${sessionId}`
378
+ `🌐 View your task at: ${chatUrl.toString()}`
351
379
  );
352
380
  } catch (error: any) {
353
381
  console.error(