@tyvm/knowhow 0.0.156 → 0.0.158

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 (155) hide show
  1. package/README.md +17 -1
  2. package/package.json +1 -1
  3. package/src/agents/base/base.ts +176 -39
  4. package/src/auth/browserLogin.ts +2 -13
  5. package/src/auth/environmentAuth.ts +51 -0
  6. package/src/auth/jwtStore.ts +70 -0
  7. package/src/auth/keyAuth.ts +4 -15
  8. package/src/auth/registerIdentity.ts +8 -8
  9. package/src/chat/modules/AgentModule.ts +3 -3
  10. package/src/chat/modules/RemoteSyncModule.ts +45 -17
  11. package/src/cli.ts +2 -0
  12. package/src/clients/anthropic.ts +156 -79
  13. package/src/clients/attribution.ts +53 -0
  14. package/src/clients/http.ts +211 -84
  15. package/src/clients/index.ts +45 -13
  16. package/src/clients/openai.ts +441 -220
  17. package/src/clients/openrouter.ts +10 -3
  18. package/src/clients/pricing/fireworks.ts +32 -2
  19. package/src/clients/pricing/openai.ts +15 -1
  20. package/src/clients/pricing/types.ts +15 -0
  21. package/src/clients/responseMetadata.ts +221 -0
  22. package/src/clients/types.ts +49 -4
  23. package/src/commands/mcp.ts +1 -0
  24. package/src/commands/misc.ts +1 -1
  25. package/src/commands/remote.ts +77 -0
  26. package/src/config.ts +9 -1
  27. package/src/login.ts +54 -42
  28. package/src/remotes.ts +110 -0
  29. package/src/services/KnowhowClient.ts +12 -16
  30. package/src/services/Mcp.ts +94 -7
  31. package/src/services/McpWebsocketTransport.ts +14 -11
  32. package/src/types.ts +12 -0
  33. package/tests/unit/McpWebsocketTransport.test.ts +56 -0
  34. package/tests/unit/auth/jwtStore.test.ts +55 -0
  35. package/tests/unit/auth/registerIdentity.test.ts +8 -2
  36. package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
  37. package/tests/unit/clients/attribution.test.ts +185 -0
  38. package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
  39. package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
  40. package/tests/unit/clients/responseMetadata.test.ts +123 -0
  41. package/tests/unit/commands/remote.test.ts +79 -0
  42. package/tests/unit/login.test.ts +64 -3
  43. package/tests/unit/services/KnowhowClient.test.ts +2 -2
  44. package/tests/unit/services/Mcp.test.ts +2 -2
  45. package/ts_build/package.json +1 -1
  46. package/ts_build/src/agents/base/base.d.ts +8 -3
  47. package/ts_build/src/agents/base/base.js +112 -26
  48. package/ts_build/src/agents/base/base.js.map +1 -1
  49. package/ts_build/src/auth/browserLogin.js +2 -7
  50. package/ts_build/src/auth/browserLogin.js.map +1 -1
  51. package/ts_build/src/auth/environmentAuth.d.ts +5 -0
  52. package/ts_build/src/auth/environmentAuth.js +48 -0
  53. package/ts_build/src/auth/environmentAuth.js.map +1 -0
  54. package/ts_build/src/auth/jwtStore.d.ts +4 -0
  55. package/ts_build/src/auth/jwtStore.js +65 -0
  56. package/ts_build/src/auth/jwtStore.js.map +1 -0
  57. package/ts_build/src/auth/keyAuth.d.ts +1 -1
  58. package/ts_build/src/auth/keyAuth.js +4 -16
  59. package/ts_build/src/auth/keyAuth.js.map +1 -1
  60. package/ts_build/src/auth/registerIdentity.js +6 -7
  61. package/ts_build/src/auth/registerIdentity.js.map +1 -1
  62. package/ts_build/src/chat/modules/AgentModule.js +3 -3
  63. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  64. package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
  65. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
  66. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  67. package/ts_build/src/cli.js +2 -0
  68. package/ts_build/src/cli.js.map +1 -1
  69. package/ts_build/src/clients/anthropic.d.ts +1 -0
  70. package/ts_build/src/clients/anthropic.js +83 -38
  71. package/ts_build/src/clients/anthropic.js.map +1 -1
  72. package/ts_build/src/clients/attribution.d.ts +2 -0
  73. package/ts_build/src/clients/attribution.js +32 -0
  74. package/ts_build/src/clients/attribution.js.map +1 -0
  75. package/ts_build/src/clients/http.js +100 -20
  76. package/ts_build/src/clients/http.js.map +1 -1
  77. package/ts_build/src/clients/index.d.ts +2 -1
  78. package/ts_build/src/clients/index.js +24 -6
  79. package/ts_build/src/clients/index.js.map +1 -1
  80. package/ts_build/src/clients/openai.d.ts +2 -1
  81. package/ts_build/src/clients/openai.js +225 -99
  82. package/ts_build/src/clients/openai.js.map +1 -1
  83. package/ts_build/src/clients/openrouter.d.ts +2 -2
  84. package/ts_build/src/clients/openrouter.js +6 -2
  85. package/ts_build/src/clients/openrouter.js.map +1 -1
  86. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -0
  87. package/ts_build/src/clients/pricing/fireworks.js +14 -2
  88. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  89. package/ts_build/src/clients/pricing/openai.js +15 -1
  90. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  91. package/ts_build/src/clients/pricing/types.d.ts +6 -0
  92. package/ts_build/src/clients/pricing/types.js.map +1 -1
  93. package/ts_build/src/clients/responseMetadata.d.ts +15 -0
  94. package/ts_build/src/clients/responseMetadata.js +141 -0
  95. package/ts_build/src/clients/responseMetadata.js.map +1 -0
  96. package/ts_build/src/clients/types.d.ts +18 -1
  97. package/ts_build/src/clients/types.js +2 -1
  98. package/ts_build/src/clients/types.js.map +1 -1
  99. package/ts_build/src/commands/mcp.js.map +1 -1
  100. package/ts_build/src/commands/misc.js +1 -1
  101. package/ts_build/src/commands/misc.js.map +1 -1
  102. package/ts_build/src/commands/remote.d.ts +2 -0
  103. package/ts_build/src/commands/remote.js +70 -0
  104. package/ts_build/src/commands/remote.js.map +1 -0
  105. package/ts_build/src/config.js +8 -0
  106. package/ts_build/src/config.js.map +1 -1
  107. package/ts_build/src/login.d.ts +4 -3
  108. package/ts_build/src/login.js +34 -35
  109. package/ts_build/src/login.js.map +1 -1
  110. package/ts_build/src/remotes.d.ts +14 -0
  111. package/ts_build/src/remotes.js +117 -0
  112. package/ts_build/src/remotes.js.map +1 -0
  113. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  114. package/ts_build/src/services/KnowhowClient.js +12 -14
  115. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  116. package/ts_build/src/services/Mcp.d.ts +1 -1
  117. package/ts_build/src/services/Mcp.js +68 -7
  118. package/ts_build/src/services/Mcp.js.map +1 -1
  119. package/ts_build/src/services/McpWebsocketTransport.d.ts +1 -0
  120. package/ts_build/src/services/McpWebsocketTransport.js +11 -6
  121. package/ts_build/src/services/McpWebsocketTransport.js.map +1 -1
  122. package/ts_build/src/types.d.ts +13 -0
  123. package/ts_build/src/types.js.map +1 -1
  124. package/ts_build/tests/unit/McpWebsocketTransport.test.d.ts +1 -0
  125. package/ts_build/tests/unit/McpWebsocketTransport.test.js +47 -0
  126. package/ts_build/tests/unit/McpWebsocketTransport.test.js.map +1 -0
  127. package/ts_build/tests/unit/auth/jwtStore.test.d.ts +1 -0
  128. package/ts_build/tests/unit/auth/jwtStore.test.js +46 -0
  129. package/ts_build/tests/unit/auth/jwtStore.test.js.map +1 -0
  130. package/ts_build/tests/unit/auth/registerIdentity.test.js +8 -2
  131. package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -1
  132. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
  133. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
  134. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
  135. package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
  136. package/ts_build/tests/unit/clients/attribution.test.js +153 -0
  137. package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
  138. package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
  139. package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
  140. package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
  141. package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
  142. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
  143. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
  144. package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
  145. package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
  146. package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
  147. package/ts_build/tests/unit/commands/remote.test.d.ts +1 -0
  148. package/ts_build/tests/unit/commands/remote.test.js +70 -0
  149. package/ts_build/tests/unit/commands/remote.test.js.map +1 -0
  150. package/ts_build/tests/unit/login.test.js +44 -3
  151. package/ts_build/tests/unit/login.test.js.map +1 -1
  152. package/ts_build/tests/unit/services/KnowhowClient.test.js +2 -2
  153. package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -1
  154. package/ts_build/tests/unit/services/Mcp.test.js +2 -2
  155. package/ts_build/tests/unit/services/Mcp.test.js.map +1 -1
package/README.md CHANGED
@@ -49,7 +49,23 @@ If you want to paste a token manually:
49
49
  knowhow login --jwt
50
50
  ```
51
51
 
52
- After login, Knowhow stores your JWT in `.knowhow/.jwt` and updates `knowhow.json` so models can route through the Knowhow proxy.
52
+ Authentication belongs to a named remote. `origin` is available by default and
53
+ points to the production API. To use another compatible backend:
54
+
55
+ ```bash
56
+ knowhow remote add dev https://api.dev.knowhow.tyvm.ai
57
+ knowhow remote use dev
58
+ knowhow login
59
+ ```
60
+
61
+ Use `knowhow remote list` to show remotes and `knowhow remote get-url` to show
62
+ the selected API. Each entry in `.knowhow/knowhow.json` stores its name, API URL,
63
+ organization ID, and JWT path. Tokens remain independent: production uses
64
+ `.knowhow/.jwt`, development uses `.knowhow/.jwt.dev`, localhost uses
65
+ `.knowhow/.jwt.local`, and arbitrary hosts receive a host-specific filename.
66
+
67
+ `KNOWHOW_API_URL` remains an explicit per-process override, which is useful when
68
+ you only want to select a remote for one terminal command.
53
69
 
54
70
  ### First steps after setup
55
71
  Start the interactive chat:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.156",
3
+ "version": "0.0.158",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -49,6 +49,13 @@ type CachedChatCompletion = { messages: ChatCompletion["messages"] } & Partial<
49
49
  Omit<ChatCompletion, "messages">
50
50
  >;
51
51
 
52
+ interface CompletedCompaction {
53
+ compressionEnd: number;
54
+ originalPrefix: Message[];
55
+ replacementPrefix: Message[];
56
+ summary: string;
57
+ }
58
+
52
59
  /**
53
60
  * A source is anything that can (a) start producing data, (b) hand each datum to
54
61
  * a callback, and (c) be torn down. This is the unified contract behind
@@ -128,6 +135,12 @@ export abstract class BaseAgent implements IAgent {
128
135
  // regardless of whether the token threshold has been reached.
129
136
  protected _forceCompact = false;
130
137
 
138
+ // Compaction is intentionally detached from the main agent loop. A failed
139
+ // attempt is cleared so a later turn can retry without ending the task.
140
+ private _compactionPromise: Promise<void> | null = null;
141
+ private _completedCompaction: CompletedCompaction | null = null;
142
+ private _compactionGeneration = 0;
143
+
131
144
  // Interrupt support: resolves the currently awaited tool call or completion
132
145
  private _interruptResolve: (() => void) | null = null;
133
146
  // A monotonically-increasing token identifying the active interruptible window.
@@ -287,6 +300,10 @@ export abstract class BaseAgent implements IAgent {
287
300
  }
288
301
 
289
302
  newTask(taskId?: string) {
303
+ // Invalidate any detached compaction still finishing for the previous task.
304
+ this._compactionGeneration++;
305
+ this._compactionPromise = null;
306
+ this._completedCompaction = null;
290
307
  this.currentThread = 0;
291
308
  this.threads = [];
292
309
  this.taskBreakdown = "";
@@ -571,7 +588,11 @@ export abstract class BaseAgent implements IAgent {
571
588
  return this.totalCostUsd;
572
589
  }
573
590
 
574
- adjustTokenUsage(usage: any, messages?: Message[]) {
591
+ adjustTokenUsage(
592
+ usage: any,
593
+ messages?: Message[],
594
+ updateLastPromptTokens = true
595
+ ) {
575
596
  if (!usage) return;
576
597
  // Support both OpenAI-style (prompt_tokens/completion_tokens) and Anthropic-style (input_tokens/output_tokens)
577
598
  const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0;
@@ -593,7 +614,7 @@ export abstract class BaseAgent implements IAgent {
593
614
  // (i.e. when there are input tokens) so a usage-less interrupt stub doesn't
594
615
  // reset it to 0.
595
616
  const promptTokens = inputTokens + cacheReadTokens + cacheWriteTokens;
596
- if (promptTokens > 0) {
617
+ if (updateLastPromptTokens && promptTokens > 0) {
597
618
  this.lastPromptTokens = promptTokens;
598
619
  }
599
620
 
@@ -641,9 +662,10 @@ export abstract class BaseAgent implements IAgent {
641
662
  params: CachedChatCompletion,
642
663
  options: {
643
664
  interruptValue?: CompletionResponse;
665
+ updateLastPromptTokens?: boolean;
644
666
  } = {}
645
667
  ): Promise<CompletionResponse> {
646
- const { interruptValue } = options;
668
+ const { interruptValue, updateLastPromptTokens = true } = options;
647
669
 
648
670
  // If you change the tools, or any of this, you bust cache
649
671
  // only safe to change messages
@@ -668,7 +690,11 @@ export abstract class BaseAgent implements IAgent {
668
690
  : await callPromise;
669
691
 
670
692
  this.adjustTotalCostUsd(response?.usd_cost);
671
- this.adjustTokenUsage(response?.usage, params.messages);
693
+ this.adjustTokenUsage(
694
+ response?.usage,
695
+ params.messages,
696
+ updateLastPromptTokens
697
+ );
672
698
 
673
699
  return response;
674
700
  }
@@ -1276,28 +1302,59 @@ export abstract class BaseAgent implements IAgent {
1276
1302
  // prompt token count from the last completion response over the
1277
1303
  // whitespace-based estimate — so it compares real tokens against the
1278
1304
  // real (token-based) compress threshold.
1305
+ const completedCompaction = this._completedCompaction;
1306
+ this._completedCompaction = null;
1307
+ let appliedCompaction = false;
1308
+
1309
+ if (completedCompaction) {
1310
+ const currentPrefix = messages.slice(
1311
+ 0,
1312
+ completedCompaction.compressionEnd
1313
+ );
1314
+ const prefixIsUnchanged = currentPrefix.every(
1315
+ (message, index) =>
1316
+ message === completedCompaction.originalPrefix[index] ||
1317
+ JSON.stringify(message) ===
1318
+ JSON.stringify(completedCompaction.originalPrefix[index])
1319
+ );
1320
+
1321
+ if (
1322
+ currentPrefix.length === completedCompaction.compressionEnd &&
1323
+ prefixIsUnchanged
1324
+ ) {
1325
+ messages = [
1326
+ ...completedCompaction.replacementPrefix,
1327
+ ...messages.slice(completedCompaction.compressionEnd),
1328
+ ];
1329
+ this.summaries.push(completedCompaction.summary);
1330
+ this.startNewThread(messages);
1331
+ this.lastPromptTokens = 0;
1332
+ appliedCompaction = true;
1333
+ } else {
1334
+ this.log(
1335
+ "Discarding completed compaction because its source prefix changed; a later turn will retry",
1336
+ "warn"
1337
+ );
1338
+ }
1339
+ }
1340
+
1279
1341
  const contextTokens = this.getContextTokenCount(messages);
1280
1342
  const overThreshold =
1281
1343
  contextTokens > this.getCompressThreshold() &&
1282
1344
  messages.length > this.compressMinMessages;
1283
1345
 
1284
- if (overThreshold || this._forceCompact) {
1285
- const taskBreakdown = await this.getTaskBreakdown(messages);
1346
+ if (
1347
+ !appliedCompaction &&
1348
+ !this._compactionPromise &&
1349
+ (overThreshold || this._forceCompact)
1350
+ ) {
1286
1351
  this.log(
1287
1352
  this._forceCompact
1288
- ? `Compacting messages (manual /compact request): ${contextTokens} tokens`
1289
- : `Compressing messages: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`
1353
+ ? `Starting background compaction (manual /compact request): ${contextTokens} tokens`
1354
+ : `Starting background compaction: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`
1290
1355
  );
1291
1356
  this._forceCompact = false;
1292
- messages = await this.compressMessages(messages);
1293
- this.startNewThread(messages);
1294
- // Reset the real-token tracker. compressMessages() ran a completion over
1295
- // the older history, so lastPromptTokens reflects the compression input
1296
- // rather than the newly reconstructed thread. Zeroing it makes
1297
- // getContextTokenCount() estimate the new thread until the next regular
1298
- // completion reports fresh usage, preventing an immediate false
1299
- // re-compaction on the following iteration.
1300
- this.lastPromptTokens = 0;
1357
+ this.startBackgroundCompaction(messages);
1301
1358
  }
1302
1359
 
1303
1360
  if (["assistant", "tool"].includes(messages[messages.length - 1].role)) {
@@ -1307,10 +1364,19 @@ export abstract class BaseAgent implements IAgent {
1307
1364
  this.logStatus();
1308
1365
 
1309
1366
  const continuation = `<Workflow>
1310
- Task terminates after you call on of these tools: ${JSON.stringify(
1367
+ This is an automated runtime instruction, not a new user message.
1368
+ The original user request remains active. Do not say that the user's
1369
+ message was empty, and do not ask the user to repeat the request.
1370
+
1371
+ Your previous response did not terminate the task because it did not
1372
+ call one of the required tools: ${JSON.stringify(
1311
1373
  this.requiredToolNames
1312
- )}.\n
1313
- User likely only sees output from the required tool call.
1374
+ )}.
1375
+ If your previous response was intended as the final response, call the
1376
+ appropriate required tool now with that response. Otherwise, continue
1377
+ working on the original request and call an appropriate required tool
1378
+ when the task is complete.
1379
+
1314
1380
  <TaskStatus>${statusMessage}</TaskStatus>
1315
1381
  </Workflow>`;
1316
1382
 
@@ -1674,13 +1740,67 @@ export abstract class BaseAgent implements IAgent {
1674
1740
  return [...content, ...finalAnswer].join("\n\n");
1675
1741
  }
1676
1742
 
1677
- async getTaskBreakdown(messages: Message[]) {
1743
+ private startBackgroundCompaction(messages: Message[]): void {
1744
+ const snapshot = messages.slice();
1745
+ let resumeIndex = snapshot.length - 1;
1746
+ while (resumeIndex >= 0 && snapshot[resumeIndex].role !== "assistant") {
1747
+ resumeIndex--;
1748
+ }
1749
+ const compressionEnd =
1750
+ resumeIndex === -1 ? snapshot.length : resumeIndex;
1751
+ const generation = this._compactionGeneration;
1752
+
1753
+ const promise = (async () => {
1754
+ const taskBreakdown = await this.getTaskBreakdown(snapshot, false);
1755
+ if (generation !== this._compactionGeneration) return;
1756
+ this.taskBreakdown = taskBreakdown;
1757
+ let summary = "";
1758
+ const compactedSnapshot = await this.compressMessages(
1759
+ snapshot,
1760
+ (generatedSummary) => {
1761
+ summary = generatedSummary;
1762
+ },
1763
+ taskBreakdown
1764
+ );
1765
+ if (generation !== this._compactionGeneration) return;
1766
+
1767
+ const preservedSnapshotLength = snapshot.length - compressionEnd;
1768
+ this._completedCompaction = {
1769
+ compressionEnd,
1770
+ originalPrefix: snapshot.slice(0, compressionEnd),
1771
+ replacementPrefix: compactedSnapshot.slice(
1772
+ 0,
1773
+ compactedSnapshot.length - preservedSnapshotLength
1774
+ ),
1775
+ summary,
1776
+ };
1777
+ this.log("Background compaction completed; it will be applied next turn");
1778
+ })();
1779
+
1780
+ this._compactionPromise = promise;
1781
+ promise
1782
+ .catch((error) => {
1783
+ if (generation === this._compactionGeneration) {
1784
+ this.log(
1785
+ `Background compaction failed; a later turn will retry: ${error}`,
1786
+ "warn"
1787
+ );
1788
+ }
1789
+ })
1790
+ .finally(() => {
1791
+ if (this._compactionPromise === promise) {
1792
+ this._compactionPromise = null;
1793
+ }
1794
+ });
1795
+ }
1796
+
1797
+ async getTaskBreakdown(messages: Message[], cacheResult = true) {
1678
1798
  if (this.taskBreakdown) {
1679
1799
  return this.taskBreakdown;
1680
1800
  }
1681
1801
 
1682
1802
  const taskPrompt = `
1683
- Analyze all previous messages.
1803
+ TASK INTERRUPT. CONTEXT COMPACTION NEEDED. Analyze all previous messages.
1684
1804
 
1685
1805
  Generate a detailed task breakdown for this conversation, include a section for the following:
1686
1806
  1. Task List
@@ -1699,10 +1819,13 @@ export abstract class BaseAgent implements IAgent {
1699
1819
  },
1700
1820
  ] as Message[];
1701
1821
 
1702
- const response = await this.createAgentCompletion({
1703
- messages: taskBreakdownMessages,
1704
- tool_choice: "none",
1705
- });
1822
+ const response = await this.createAgentCompletion(
1823
+ {
1824
+ messages: taskBreakdownMessages,
1825
+ tool_choice: "none",
1826
+ },
1827
+ { updateLastPromptTokens: false }
1828
+ );
1706
1829
 
1707
1830
  const breakdownContent = this.extractContentFromMessages(
1708
1831
  response.choices?.map((c) => c.message)
@@ -1717,12 +1840,18 @@ export abstract class BaseAgent implements IAgent {
1717
1840
  );
1718
1841
  }
1719
1842
 
1720
- this.taskBreakdown = breakdownContent;
1843
+ if (cacheResult) {
1844
+ this.taskBreakdown = breakdownContent;
1845
+ }
1721
1846
  this.log(`task breakdown cost: ${response.usd_cost}`);
1722
- return this.taskBreakdown;
1847
+ return breakdownContent;
1723
1848
  }
1724
1849
 
1725
- async compressMessages(messages: Message[]) {
1850
+ async compressMessages(
1851
+ messages: Message[],
1852
+ onSummary?: (summary: string) => void,
1853
+ taskBreakdown = this.taskBreakdown
1854
+ ) {
1726
1855
  // Preserve the latest agent interaction exactly. Starting the resumed thread
1727
1856
  // at the final assistant message keeps its tool calls, every tool response,
1728
1857
  // and any subsequent user messages together as one protocol-valid unit.
@@ -1737,7 +1866,8 @@ export abstract class BaseAgent implements IAgent {
1737
1866
  this.log(
1738
1867
  `Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`
1739
1868
  );
1740
- const toCompressPrompt = `We are compressing our conversation to save memory.
1869
+ const toCompressPrompt = `TASK INTERRUPT. CONTEXT COMPACTION NEEDED.
1870
+ We are compressing our conversation to save memory.
1741
1871
  Please summarize the conversation so far, so that we may continue the original task with a smaller context
1742
1872
 
1743
1873
  Include the following sections:
@@ -1747,8 +1877,8 @@ export abstract class BaseAgent implements IAgent {
1747
1877
  4. Tasks remaining - what tasks are left from the initial task breakdown.
1748
1878
 
1749
1879
  ${
1750
- this.taskBreakdown
1751
- ? `Our initial task breakdown: ${this.taskBreakdown}`
1880
+ taskBreakdown
1881
+ ? `Our initial task breakdown: ${taskBreakdown}`
1752
1882
  : ""
1753
1883
  }
1754
1884
 
@@ -1764,10 +1894,13 @@ export abstract class BaseAgent implements IAgent {
1764
1894
  },
1765
1895
  ] as Message[];
1766
1896
 
1767
- const response = await this.createAgentCompletion({
1768
- messages: compressMessagesPayload,
1769
- tool_choice: "none",
1770
- });
1897
+ const response = await this.createAgentCompletion(
1898
+ {
1899
+ messages: compressMessagesPayload,
1900
+ tool_choice: "none",
1901
+ },
1902
+ { updateLastPromptTokens: false }
1903
+ );
1771
1904
 
1772
1905
  const summary = this.extractContentFromMessages(
1773
1906
  response.choices?.map((c) => c.message)
@@ -1778,15 +1911,19 @@ export abstract class BaseAgent implements IAgent {
1778
1911
  throw new Error("Compaction summary request returned no text content");
1779
1912
  }
1780
1913
 
1781
- this.summaries.push(summary);
1914
+ if (onSummary) {
1915
+ onSummary(summary);
1916
+ } else {
1917
+ this.summaries.push(summary);
1918
+ }
1782
1919
 
1783
1920
  const startMessages = [
1784
1921
  {
1785
1922
  role: "user",
1786
1923
  content: `
1787
1924
  ${
1788
- this.taskBreakdown
1789
- ? `Initial task breakdown:\n ${this.taskBreakdown}`
1925
+ taskBreakdown
1926
+ ? `Initial task breakdown:\n ${taskBreakdown}`
1790
1927
  : "(No task breakdown available — summarize what you know from context above)"
1791
1928
  }
1792
1929
 
@@ -8,6 +8,7 @@ import * as path from "path";
8
8
  import { KNOWHOW_API_URL } from "../services/KnowhowClient";
9
9
  import { Spinner } from "./spinner";
10
10
  import { BrowserLoginError } from "./errors";
11
+ import { storeJwtForApi } from "./jwtStore";
11
12
 
12
13
  // TypeScript interfaces for the CLI Login API
13
14
 
@@ -199,19 +200,7 @@ export class BrowserLoginService {
199
200
  * Securely stores the JWT token to the file system
200
201
  */
201
202
  private async storeJwt(jwt: string): Promise<void> {
202
- const configDir = `${process.cwd()}/.knowhow`;
203
- const jwtFile = `${configDir}/.jwt`;
204
-
205
- // Ensure directory exists
206
- if (!fs.existsSync(configDir)) {
207
- fs.mkdirSync(configDir, { recursive: true });
208
- }
209
-
210
- // Write JWT to file
211
- fs.writeFileSync(jwtFile, jwt, { mode: 0o600 });
212
-
213
- // Ensure file has correct permissions (readable only by owner)
214
- fs.chmodSync(jwtFile, 0o600);
203
+ storeJwtForApi(jwt, this.baseUrl);
215
204
  }
216
205
 
217
206
  /**
@@ -0,0 +1,51 @@
1
+ import { Config } from "../types";
2
+ import { loadJwtFromDisk } from "./jwtStore";
3
+ import { createRemote, findRemoteForApi, registerRemote } from "../remotes";
4
+
5
+ /** Stable config key for authentication state owned by one API deployment. */
6
+ export function getApiEnvironmentKey(apiUrl: string): string {
7
+ try {
8
+ const url = new URL(apiUrl);
9
+ return `${url.protocol}//${url.host}`.toLowerCase();
10
+ } catch {
11
+ return apiUrl.replace(/\/+$/, "").toLowerCase();
12
+ }
13
+ }
14
+
15
+ /** Read an organization claim without trusting the JWT for authorization. */
16
+ export function getOrgIdFromJwt(jwt: string): string | undefined {
17
+ try {
18
+ const payload = JSON.parse(
19
+ Buffer.from(jwt.split(".")[1] ?? "", "base64url").toString("utf8")
20
+ ) as Record<string, unknown>;
21
+ const orgId = payload.org ?? payload.orgId ?? payload.org_id ?? payload.organizationId;
22
+ return typeof orgId === "string" && orgId ? orgId : undefined;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ /** Resolve the org for the selected API without leaking another environment's org. */
29
+ export function getOrgIdForApi(config: Config, apiUrl: string): string | undefined {
30
+ const remote = findRemoteForApi(config, apiUrl);
31
+ if (remote?.orgId) return remote.orgId;
32
+
33
+ // Read old orgIds only until this API has been represented as a remote.
34
+ const environmentKey = getApiEnvironmentKey(apiUrl);
35
+ const configured = config.orgIds?.[environmentKey];
36
+ if (configured) return configured;
37
+
38
+ const tokenOrgId = getOrgIdFromJwt(loadJwtFromDisk(apiUrl));
39
+ if (tokenOrgId) return tokenOrgId;
40
+
41
+ return config.remotes || (config.orgIds && Object.keys(config.orgIds).length > 0)
42
+ ? undefined
43
+ : config.orgId;
44
+ }
45
+
46
+ /** Record authentication state on the remote that owns it. */
47
+ export function setOrgIdForApi(config: Config, apiUrl: string, orgId: string): void {
48
+ const remote = findRemoteForApi(config, apiUrl) ?? createRemote("origin", apiUrl);
49
+ registerRemote(config, { ...remote, orgId });
50
+ delete config.orgId;
51
+ }
@@ -0,0 +1,70 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { findRemoteForApiSync, resolveJwtPath } from "../remotes";
4
+
5
+ const PRODUCTION_API_HOST = "api.knowhow.tyvm.ai";
6
+
7
+ /** Return a readable, environment-specific JWT filename for an API URL. */
8
+ export function getJwtFileName(apiUrl: string): string {
9
+ try {
10
+ const { hostname, port } = new URL(apiUrl);
11
+ const normalizedHost = hostname.toLowerCase();
12
+
13
+ if (normalizedHost === PRODUCTION_API_HOST) return ".jwt";
14
+ if (
15
+ normalizedHost === "localhost" ||
16
+ normalizedHost === "::1" ||
17
+ normalizedHost.startsWith("127.")
18
+ ) {
19
+ return ".jwt.local";
20
+ }
21
+ if (normalizedHost === "api.dev.knowhow.tyvm.ai" || normalizedHost.includes(".dev.")) {
22
+ return ".jwt.dev";
23
+ }
24
+
25
+ const environment = `${normalizedHost}${port ? `-${port}` : ""}`
26
+ .replace(/[^a-z0-9.-]+/g, "-")
27
+ .replace(/^-+|-+$/g, "");
28
+ return environment ? `.jwt.${environment}` : ".jwt";
29
+ } catch {
30
+ return ".jwt";
31
+ }
32
+ }
33
+
34
+ export function getJwtFilePath(
35
+ apiUrl: string,
36
+ configDir = path.join(process.cwd(), ".knowhow")
37
+ ): string {
38
+ return path.join(configDir, getJwtFileName(apiUrl));
39
+ }
40
+
41
+ /**
42
+ * Load the JWT selected for an environment. The optional legacy path provides
43
+ * the directory for configs that historically pointed at `.jwt`.
44
+ */
45
+ export function loadJwtFromDisk(apiUrl: string, legacyPath?: string): string {
46
+ const basePath = legacyPath ?? path.join(process.cwd(), ".knowhow", ".jwt");
47
+ const configuredPath = findRemoteForApiSync(apiUrl)?.jwtPath;
48
+ const jwtPath = configuredPath
49
+ ? resolveJwtPath(configuredPath)
50
+ : path.join(path.dirname(basePath), getJwtFileName(apiUrl));
51
+ return fs.existsSync(jwtPath)
52
+ ? fs.readFileSync(jwtPath, "utf8").trim()
53
+ : "";
54
+ }
55
+
56
+ /** Atomically store a JWT in the file selected for the API environment. */
57
+ export function storeJwtForApi(jwt: string, apiUrl: string, configuredPath?: string): string {
58
+ const jwtFilePath = configuredPath ? resolveJwtPath(configuredPath) : getJwtFilePath(apiUrl);
59
+ const directory = path.dirname(jwtFilePath);
60
+ const temporaryPath = `${jwtFilePath}.${process.pid}.tmp`;
61
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
62
+ try {
63
+ fs.writeFileSync(temporaryPath, jwt, { mode: 0o600, flag: "wx" });
64
+ fs.renameSync(temporaryPath, jwtFilePath);
65
+ fs.chmodSync(jwtFilePath, 0o600);
66
+ } finally {
67
+ fs.rmSync(temporaryPath, { force: true });
68
+ }
69
+ return jwtFilePath;
70
+ }
@@ -1,8 +1,7 @@
1
- import fs from "fs";
2
1
  import os from "os";
3
- import path from "path";
4
2
  import http from "../utils/http";
5
3
  import { getCliUserAgent } from "./browserLogin";
4
+ import { storeJwtForApi } from "./jwtStore";
6
5
  import {
7
6
  getDefaultPrivateKeyPath,
8
7
  keyPairExists,
@@ -101,7 +100,7 @@ export async function authenticateWithKey(
101
100
  if (!keyPairExists(privateKeyPath) && !keyPairExists()) return false;
102
101
 
103
102
  try {
104
- storeJwt(await exchangeAvailablePublicKeyJwt(orgId, apiUrl, privateKeyPath));
103
+ storeJwt(await exchangeAvailablePublicKeyJwt(orgId, apiUrl, privateKeyPath), apiUrl);
105
104
  return true;
106
105
  } catch (error: unknown) {
107
106
  // The identity is not registered for this environment/org (or was revoked).
@@ -138,16 +137,6 @@ export async function registerPublicKey(
138
137
  );
139
138
  }
140
139
 
141
- export function storeJwt(jwt: string): void {
142
- const jwtFilePath = path.join(process.cwd(), ".knowhow", ".jwt");
143
- const directory = path.dirname(jwtFilePath);
144
- const temporaryPath = `${jwtFilePath}.${process.pid}.tmp`;
145
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
146
- try {
147
- fs.writeFileSync(temporaryPath, jwt, { mode: 0o600, flag: "wx" });
148
- fs.renameSync(temporaryPath, jwtFilePath);
149
- fs.chmodSync(jwtFilePath, 0o600);
150
- } finally {
151
- fs.rmSync(temporaryPath, { force: true });
152
- }
140
+ export function storeJwt(jwt: string, apiUrl: string = DEFAULT_API_URL): void {
141
+ storeJwtForApi(jwt, apiUrl);
153
142
  }
@@ -1,8 +1,9 @@
1
- import fs from "fs";
2
1
  import path from "path";
3
2
  import { getOrCreatePublicKey } from "./keyManager";
4
3
  import { registerPublicKey, exchangePublicKeyJwt, storeJwt } from "./keyAuth";
5
4
  import { getConfig, updateConfig } from "../config";
5
+ import { getJwtFilePath, loadJwtFromDisk } from "./jwtStore";
6
+ import { setOrgIdForApi } from "./environmentAuth";
6
7
 
7
8
  export interface RegisterIdentityOptions {
8
9
  /** Optional Ed25519 private-key path. Defaults to ~/.knowhow/keys/id_ed25519. */
@@ -36,30 +37,29 @@ export function extractOrgIdFromJwt(jwt: string): string {
36
37
  }
37
38
 
38
39
  /**
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.
40
+ * Register a CLI identity using the JWT stored for the selected API, then
41
+ * replace that JWT with one obtained via key authentication.
41
42
  * This is intentionally generic: authorization scope is derived by the backend
42
43
  * from the verified source JWT.
43
44
  */
44
45
  export async function registerIdentity(options: RegisterIdentityOptions = {}): Promise<void> {
45
46
  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();
47
+ const jwtPath = getJwtFilePath(apiUrl);
48
+ const sourceJwt = loadJwtFromDisk(apiUrl);
49
49
  if (!sourceJwt) throw new Error(`JWT file is empty: ${jwtPath}`);
50
50
 
51
51
  const orgId = extractOrgIdFromJwt(sourceJwt);
52
52
  const keyPair = getOrCreatePublicKey(options.identityPath);
53
53
  await registerPublicKey(sourceJwt, keyPair.publicKeyBase64, apiUrl);
54
54
  const identityJwt = await exchangePublicKeyJwt(orgId, apiUrl, options.identityPath);
55
- storeJwt(identityJwt);
55
+ storeJwt(identityJwt, apiUrl);
56
56
 
57
57
  const config = await getConfig();
58
58
  if (!config.modelProviders) config.modelProviders = [];
59
59
  if (!config.modelProviders.some((provider: { provider: string }) => provider.provider === "knowhow")) {
60
60
  config.modelProviders.push({ provider: "knowhow" });
61
61
  }
62
- config.orgId = orgId;
62
+ setOrgIdForApi(config, apiUrl, orgId);
63
63
  config.cliIdentityPath = path.resolve(options.identityPath ?? keyPair.privateKeyPath);
64
64
  await updateConfig(config);
65
65
 
@@ -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);