@tyvm/knowhow 0.0.157 → 0.0.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/package.json +2 -2
- package/src/agents/base/base.ts +161 -34
- package/src/auth/browserLogin.ts +2 -13
- package/src/auth/environmentAuth.ts +51 -0
- package/src/auth/jwtStore.ts +70 -0
- package/src/auth/keyAuth.ts +4 -15
- package/src/auth/registerIdentity.ts +8 -8
- package/src/cli.ts +2 -0
- package/src/clients/index.ts +25 -5
- package/src/clients/pricing/fireworks.ts +32 -2
- package/src/commands/mcp.ts +1 -0
- package/src/commands/misc.ts +1 -1
- package/src/commands/remote.ts +77 -0
- package/src/config.ts +9 -1
- package/src/login.ts +54 -42
- package/src/remotes.ts +110 -0
- package/src/services/KnowhowClient.ts +12 -16
- package/src/services/Mcp.ts +94 -7
- package/src/services/McpWebsocketTransport.ts +14 -11
- package/src/types.ts +12 -0
- package/tests/unit/McpWebsocketTransport.test.ts +56 -0
- package/tests/unit/auth/jwtStore.test.ts +55 -0
- package/tests/unit/auth/registerIdentity.test.ts +8 -2
- package/tests/unit/commands/remote.test.ts +79 -0
- package/tests/unit/login.test.ts +64 -3
- package/tests/unit/services/KnowhowClient.test.ts +2 -2
- package/tests/unit/services/Mcp.test.ts +2 -2
- package/ts_build/package.json +2 -2
- package/ts_build/src/agents/base/base.d.ts +8 -3
- package/ts_build/src/agents/base/base.js +98 -22
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/auth/browserLogin.js +2 -7
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/auth/environmentAuth.d.ts +5 -0
- package/ts_build/src/auth/environmentAuth.js +48 -0
- package/ts_build/src/auth/environmentAuth.js.map +1 -0
- package/ts_build/src/auth/jwtStore.d.ts +4 -0
- package/ts_build/src/auth/jwtStore.js +65 -0
- package/ts_build/src/auth/jwtStore.js.map +1 -0
- package/ts_build/src/auth/keyAuth.d.ts +1 -1
- package/ts_build/src/auth/keyAuth.js +4 -16
- package/ts_build/src/auth/keyAuth.js.map +1 -1
- package/ts_build/src/auth/registerIdentity.js +6 -7
- package/ts_build/src/auth/registerIdentity.js.map +1 -1
- package/ts_build/src/cli.js +2 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/index.js +19 -5
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.d.ts +4 -0
- package/ts_build/src/clients/pricing/fireworks.js +14 -2
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/commands/mcp.js.map +1 -1
- package/ts_build/src/commands/misc.js +1 -1
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/remote.d.ts +2 -0
- package/ts_build/src/commands/remote.js +70 -0
- package/ts_build/src/commands/remote.js.map +1 -0
- package/ts_build/src/config.js +8 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/login.d.ts +4 -3
- package/ts_build/src/login.js +34 -35
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/remotes.d.ts +14 -0
- package/ts_build/src/remotes.js +117 -0
- package/ts_build/src/remotes.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +12 -14
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/Mcp.d.ts +1 -1
- package/ts_build/src/services/Mcp.js +68 -7
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/services/McpWebsocketTransport.d.ts +1 -0
- package/ts_build/src/services/McpWebsocketTransport.js +11 -6
- package/ts_build/src/services/McpWebsocketTransport.js.map +1 -1
- package/ts_build/src/types.d.ts +13 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/tests/unit/McpWebsocketTransport.test.d.ts +1 -0
- package/ts_build/tests/unit/McpWebsocketTransport.test.js +47 -0
- package/ts_build/tests/unit/McpWebsocketTransport.test.js.map +1 -0
- package/ts_build/tests/unit/auth/jwtStore.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/jwtStore.test.js +46 -0
- package/ts_build/tests/unit/auth/jwtStore.test.js.map +1 -0
- package/ts_build/tests/unit/auth/registerIdentity.test.js +8 -2
- package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -1
- package/ts_build/tests/unit/commands/remote.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/remote.test.js +70 -0
- package/ts_build/tests/unit/commands/remote.test.js.map +1 -0
- package/ts_build/tests/unit/login.test.js +44 -3
- package/ts_build/tests/unit/login.test.js.map +1 -1
- package/ts_build/tests/unit/services/KnowhowClient.test.js +2 -2
- package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -1
- package/ts_build/tests/unit/services/Mcp.test.js +2 -2
- 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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.159",
|
|
4
4
|
"description": "ai cli with plugins and agents",
|
|
5
5
|
"main": "ts_build/src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@inquirer/editor": "^4.2.18",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.13.3",
|
|
60
60
|
"@simplewebauthn/server": "^13.3.0",
|
|
61
|
-
"@tyvm/knowhow-tunnel": "0.0.
|
|
61
|
+
"@tyvm/knowhow-tunnel": "0.0.6",
|
|
62
62
|
"commander": "^14.0.0",
|
|
63
63
|
"diff": "^5.2.0",
|
|
64
64
|
"express": "^4.19.2",
|
package/src/agents/base/base.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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 (
|
|
1285
|
-
|
|
1346
|
+
if (
|
|
1347
|
+
!appliedCompaction &&
|
|
1348
|
+
!this._compactionPromise &&
|
|
1349
|
+
(overThreshold || this._forceCompact)
|
|
1350
|
+
) {
|
|
1286
1351
|
this.log(
|
|
1287
1352
|
this._forceCompact
|
|
1288
|
-
? `
|
|
1289
|
-
: `
|
|
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
|
-
|
|
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)) {
|
|
@@ -1683,7 +1740,61 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1683
1740
|
return [...content, ...finalAnswer].join("\n\n");
|
|
1684
1741
|
}
|
|
1685
1742
|
|
|
1686
|
-
|
|
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) {
|
|
1687
1798
|
if (this.taskBreakdown) {
|
|
1688
1799
|
return this.taskBreakdown;
|
|
1689
1800
|
}
|
|
@@ -1708,10 +1819,13 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1708
1819
|
},
|
|
1709
1820
|
] as Message[];
|
|
1710
1821
|
|
|
1711
|
-
const response = await this.createAgentCompletion(
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1822
|
+
const response = await this.createAgentCompletion(
|
|
1823
|
+
{
|
|
1824
|
+
messages: taskBreakdownMessages,
|
|
1825
|
+
tool_choice: "none",
|
|
1826
|
+
},
|
|
1827
|
+
{ updateLastPromptTokens: false }
|
|
1828
|
+
);
|
|
1715
1829
|
|
|
1716
1830
|
const breakdownContent = this.extractContentFromMessages(
|
|
1717
1831
|
response.choices?.map((c) => c.message)
|
|
@@ -1726,12 +1840,18 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1726
1840
|
);
|
|
1727
1841
|
}
|
|
1728
1842
|
|
|
1729
|
-
|
|
1843
|
+
if (cacheResult) {
|
|
1844
|
+
this.taskBreakdown = breakdownContent;
|
|
1845
|
+
}
|
|
1730
1846
|
this.log(`task breakdown cost: ${response.usd_cost}`);
|
|
1731
|
-
return
|
|
1847
|
+
return breakdownContent;
|
|
1732
1848
|
}
|
|
1733
1849
|
|
|
1734
|
-
async compressMessages(
|
|
1850
|
+
async compressMessages(
|
|
1851
|
+
messages: Message[],
|
|
1852
|
+
onSummary?: (summary: string) => void,
|
|
1853
|
+
taskBreakdown = this.taskBreakdown
|
|
1854
|
+
) {
|
|
1735
1855
|
// Preserve the latest agent interaction exactly. Starting the resumed thread
|
|
1736
1856
|
// at the final assistant message keeps its tool calls, every tool response,
|
|
1737
1857
|
// and any subsequent user messages together as one protocol-valid unit.
|
|
@@ -1757,8 +1877,8 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1757
1877
|
4. Tasks remaining - what tasks are left from the initial task breakdown.
|
|
1758
1878
|
|
|
1759
1879
|
${
|
|
1760
|
-
|
|
1761
|
-
? `Our initial task breakdown: ${
|
|
1880
|
+
taskBreakdown
|
|
1881
|
+
? `Our initial task breakdown: ${taskBreakdown}`
|
|
1762
1882
|
: ""
|
|
1763
1883
|
}
|
|
1764
1884
|
|
|
@@ -1774,10 +1894,13 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1774
1894
|
},
|
|
1775
1895
|
] as Message[];
|
|
1776
1896
|
|
|
1777
|
-
const response = await this.createAgentCompletion(
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1897
|
+
const response = await this.createAgentCompletion(
|
|
1898
|
+
{
|
|
1899
|
+
messages: compressMessagesPayload,
|
|
1900
|
+
tool_choice: "none",
|
|
1901
|
+
},
|
|
1902
|
+
{ updateLastPromptTokens: false }
|
|
1903
|
+
);
|
|
1781
1904
|
|
|
1782
1905
|
const summary = this.extractContentFromMessages(
|
|
1783
1906
|
response.choices?.map((c) => c.message)
|
|
@@ -1788,15 +1911,19 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1788
1911
|
throw new Error("Compaction summary request returned no text content");
|
|
1789
1912
|
}
|
|
1790
1913
|
|
|
1791
|
-
|
|
1914
|
+
if (onSummary) {
|
|
1915
|
+
onSummary(summary);
|
|
1916
|
+
} else {
|
|
1917
|
+
this.summaries.push(summary);
|
|
1918
|
+
}
|
|
1792
1919
|
|
|
1793
1920
|
const startMessages = [
|
|
1794
1921
|
{
|
|
1795
1922
|
role: "user",
|
|
1796
1923
|
content: `
|
|
1797
1924
|
${
|
|
1798
|
-
|
|
1799
|
-
? `Initial task breakdown:\n ${
|
|
1925
|
+
taskBreakdown
|
|
1926
|
+
? `Initial task breakdown:\n ${taskBreakdown}`
|
|
1800
1927
|
: "(No task breakdown available — summarize what you know from context above)"
|
|
1801
1928
|
}
|
|
1802
1929
|
|
package/src/auth/browserLogin.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/auth/keyAuth.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
40
|
-
*
|
|
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 =
|
|
47
|
-
|
|
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
|
|
62
|
+
setOrgIdForApi(config, apiUrl, orgId);
|
|
63
63
|
config.cliIdentityPath = path.resolve(options.identityPath ?? keyPair.privateKeyPath);
|
|
64
64
|
await updateConfig(config);
|
|
65
65
|
|
package/src/cli.ts
CHANGED
|
@@ -63,6 +63,7 @@ import { addAgentsCommand } from "./commands/agents";
|
|
|
63
63
|
import { addHashCommand } from "./commands/hash";
|
|
64
64
|
import { addSandboxCommand } from "./commands/sandbox";
|
|
65
65
|
import { addProcessesCommand } from "./commands/processes";
|
|
66
|
+
import { addRemoteCommand } from "./commands/remote";
|
|
66
67
|
|
|
67
68
|
// Handle unhandled promise rejections gracefully — particularly from MCP SDK
|
|
68
69
|
// which fires errors via event emitters that can bypass Promise.allSettled.
|
|
@@ -106,6 +107,7 @@ async function main() {
|
|
|
106
107
|
// Register all commands
|
|
107
108
|
addInitCommand(program);
|
|
108
109
|
addLoginCommand(program);
|
|
110
|
+
addRemoteCommand(program);
|
|
109
111
|
addUpdateCommand(program);
|
|
110
112
|
addGenerateCommand(program);
|
|
111
113
|
addEmbedCommands(program);
|
package/src/clients/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { ModelProvider } from "../types";
|
|
|
31
31
|
import { getConfig, getConfigSync } from "../config";
|
|
32
32
|
import { loadKnowhowJwt, KNOWHOW_API_URL } from "../services/KnowhowClient";
|
|
33
33
|
import { keyPairExists } from "../auth/keyManager";
|
|
34
|
+
import { getOrgIdForApi } from "../auth/environmentAuth";
|
|
34
35
|
import { ContextLimits } from "./contextLimits";
|
|
35
36
|
import { OpenAiTextPricing } from "./pricing/openai";
|
|
36
37
|
import { AnthropicTextPricing } from "./pricing/anthropic";
|
|
@@ -112,8 +113,9 @@ const BUILT_IN_PROVIDER_REGISTRY: Record<string, ProviderRegistryEntry> = {
|
|
|
112
113
|
createClient: (entry: ModelProvider) => {
|
|
113
114
|
if (!loadKnowhowJwt()) {
|
|
114
115
|
const config = getConfigSync();
|
|
116
|
+
const orgId = getOrgIdForApi(config, KNOWHOW_API_URL);
|
|
115
117
|
if (
|
|
116
|
-
!
|
|
118
|
+
!orgId ||
|
|
117
119
|
(!keyPairExists(config.cliIdentityPath) && !keyPairExists())
|
|
118
120
|
)
|
|
119
121
|
return null;
|
|
@@ -1030,7 +1032,9 @@ export class AIClient {
|
|
|
1030
1032
|
/**
|
|
1031
1033
|
* Returns the context window limit (in tokens) for a given model.
|
|
1032
1034
|
* Delegates to the registered client's getContextLimit() if available.
|
|
1033
|
-
* Falls back to the global ContextLimits table
|
|
1035
|
+
* Falls back to the global ContextLimits table, stripping any leading
|
|
1036
|
+
* provider prefix (e.g. "openai/gpt-5.6-sol" → "gpt-5.6-sol") so that
|
|
1037
|
+
* proxied providers like "knowhow" resolve correctly.
|
|
1034
1038
|
*/
|
|
1035
1039
|
getContextLimit(
|
|
1036
1040
|
provider: string,
|
|
@@ -1040,9 +1044,25 @@ export class AIClient {
|
|
|
1040
1044
|
if (client?.getContextLimit) {
|
|
1041
1045
|
return client.getContextLimit(model);
|
|
1042
1046
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1047
|
+
// Try the model as-is first, then strip leading "provider/" prefixes
|
|
1048
|
+
// (e.g. "openai/gpt-5.6-sol" used with the "knowhow" proxy provider).
|
|
1049
|
+
const candidates = [model];
|
|
1050
|
+
if (model.includes("/")) {
|
|
1051
|
+
const parts = model.split("/");
|
|
1052
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1053
|
+
candidates.push(parts.slice(i).join("/"));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
for (const candidate of candidates) {
|
|
1057
|
+
const contextLimit = ContextLimits[candidate];
|
|
1058
|
+
if (contextLimit !== undefined) {
|
|
1059
|
+
const pricing = OpenAiTextPricing[candidate];
|
|
1060
|
+
const threshold =
|
|
1061
|
+
pricing && "input_gt_200k" in pricing ? 200_000 : contextLimit;
|
|
1062
|
+
return { contextLimit, threshold };
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return undefined;
|
|
1046
1066
|
}
|
|
1047
1067
|
|
|
1048
1068
|
/**
|