@tyvm/knowhow 0.0.157 → 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.
- package/README.md +17 -1
- package/package.json +1 -1
- 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 +1 -1
- 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
|
@@ -11,6 +11,10 @@ import { ModelPricing, ModelCatalogEntry, completions, embeddings } from "./type
|
|
|
11
11
|
export const FireworksModels = {
|
|
12
12
|
// Moonshot AI
|
|
13
13
|
KimiK3: "accounts/fireworks/models/kimi-k3",
|
|
14
|
+
|
|
15
|
+
// Meta
|
|
16
|
+
MuseGlimmer30b: "accounts/fireworks/models/muse-glimmer-30b",
|
|
17
|
+
|
|
14
18
|
KimiK2p7Code: "accounts/fireworks/models/kimi-k2p7-code",
|
|
15
19
|
KimiK2_6: "accounts/fireworks/models/kimi-k2-6",
|
|
16
20
|
KimiK2_5: "accounts/fireworks/models/kimi-k2-5",
|
|
@@ -22,6 +26,13 @@ export const FireworksModels = {
|
|
|
22
26
|
|
|
23
27
|
// Qwen
|
|
24
28
|
Qwen3p7Plus: "accounts/fireworks/models/qwen3p7-plus",
|
|
29
|
+
Qwen3p8Max: "accounts/fireworks/models/qwen3p8-2p4t-a95b",
|
|
30
|
+
|
|
31
|
+
// NVIDIA
|
|
32
|
+
NemotronLightning3p530bA3b: "accounts/fireworks/models/nemotron-lightning-3p5-30b-a3b",
|
|
33
|
+
|
|
34
|
+
// Thinking Machines Lab
|
|
35
|
+
Inkling: "accounts/fireworks/models/inkling",
|
|
25
36
|
|
|
26
37
|
// Deprecated Qwen Models
|
|
27
38
|
Qwen3_6Plus: "accounts/fireworks/models/qwen3-6-plus",
|
|
@@ -56,6 +67,10 @@ export const FireworksTextPricing: Record<string, ModelPricing> = {
|
|
|
56
67
|
// Moonshot AI
|
|
57
68
|
[FireworksModels.KimiK3]: { input: 3.0, cache_hit: 0.30, output: 15.0 },
|
|
58
69
|
[FireworksModels.KimiK2p7Code]: { input: 0.95, cache_hit: 0.19, output: 4.0 },
|
|
70
|
+
|
|
71
|
+
// Meta
|
|
72
|
+
[FireworksModels.MuseGlimmer30b]: { input: 0.35, cache_hit: 0.04, output: 1.50 },
|
|
73
|
+
|
|
59
74
|
[FireworksModels.KimiK2_6]: { input: 0.95, cache_hit: 0.16, output: 4.0 },
|
|
60
75
|
[FireworksModels.KimiK2_5]: { input: 0.60, cache_hit: 0.10, output: 3.0 },
|
|
61
76
|
|
|
@@ -67,6 +82,14 @@ export const FireworksTextPricing: Record<string, ModelPricing> = {
|
|
|
67
82
|
// Qwen
|
|
68
83
|
[FireworksModels.Qwen3p7Plus]: { input: 0.40, cache_hit: 0.08, output: 1.60 },
|
|
69
84
|
|
|
85
|
+
[FireworksModels.Qwen3p8Max]: { input: 2.00, cache_hit: 0.25, output: 6.00 },
|
|
86
|
+
|
|
87
|
+
// NVIDIA
|
|
88
|
+
[FireworksModels.NemotronLightning3p530bA3b]: { input: 0.05, cache_hit: 0.01, output: 0.20 },
|
|
89
|
+
|
|
90
|
+
// Thinking Machines Lab
|
|
91
|
+
[FireworksModels.Inkling]: { input: 1.00, cache_hit: 0.17, output: 4.05 },
|
|
92
|
+
|
|
70
93
|
// Deprecated QWEN Models
|
|
71
94
|
[FireworksModels.Qwen3_6Plus]: { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true },
|
|
72
95
|
[FireworksModels.Qwen3p6Plus]: { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true },
|
|
@@ -81,8 +104,8 @@ export const FireworksTextPricing: Record<string, ModelPricing> = {
|
|
|
81
104
|
[FireworksModels.Glm4_7]: { input: 0.60, cache_hit: 0.30, output: 2.20, deprecated: true },
|
|
82
105
|
|
|
83
106
|
// DeepSeek AI
|
|
84
|
-
[FireworksModels.DeepseekV4Pro]: { input: 1.
|
|
85
|
-
[FireworksModels.DeepseekV4Flash]: { input: 0.
|
|
107
|
+
[FireworksModels.DeepseekV4Pro]: { input: 1.32, cache_hit: 0.044, output: 3.96 },
|
|
108
|
+
[FireworksModels.DeepseekV4Flash]: { input: 0.22, cache_hit: 0.007, output: 0.66 },
|
|
86
109
|
[FireworksModels.DeepseekV3_2]: { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
|
|
87
110
|
[FireworksModels.DeepseekV3_1]: { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
|
|
88
111
|
|
|
@@ -111,12 +134,19 @@ export const FireworksActiveTextModels: string[] = [
|
|
|
111
134
|
FireworksModels.KimiK2p7Code,
|
|
112
135
|
FireworksModels.KimiK2_6,
|
|
113
136
|
FireworksModels.KimiK2_5,
|
|
137
|
+
// Meta
|
|
138
|
+
FireworksModels.MuseGlimmer30b,
|
|
114
139
|
// MiniMax
|
|
115
140
|
FireworksModels.MinimaxM3,
|
|
116
141
|
FireworksModels.MinimaxM2_7,
|
|
117
142
|
FireworksModels.MinimaxM2_5,
|
|
118
143
|
// Qwen
|
|
119
144
|
FireworksModels.Qwen3p7Plus,
|
|
145
|
+
FireworksModels.Qwen3p8Max,
|
|
146
|
+
// NVIDIA
|
|
147
|
+
FireworksModels.NemotronLightning3p530bA3b,
|
|
148
|
+
// Thinking Machines Lab
|
|
149
|
+
FireworksModels.Inkling,
|
|
120
150
|
// Z.ai
|
|
121
151
|
FireworksModels.Glm5p2,
|
|
122
152
|
FireworksModels.Glm5p1,
|
package/src/commands/mcp.ts
CHANGED
package/src/commands/misc.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig, updateConfig } from "../config";
|
|
3
|
+
import {
|
|
4
|
+
createRemote,
|
|
5
|
+
DEFAULT_API_URL,
|
|
6
|
+
DEFAULT_REMOTE_NAME,
|
|
7
|
+
getConfiguredRemotes,
|
|
8
|
+
registerRemote,
|
|
9
|
+
} from "../remotes";
|
|
10
|
+
|
|
11
|
+
export function addRemoteCommand(program: Command): void {
|
|
12
|
+
const command = program
|
|
13
|
+
.command("remote")
|
|
14
|
+
.description("Manage Knowhow API remotes");
|
|
15
|
+
|
|
16
|
+
command
|
|
17
|
+
.command("add")
|
|
18
|
+
.description("Register a Knowhow-compatible backend")
|
|
19
|
+
.argument("<name>", "Remote name")
|
|
20
|
+
.argument("<api-url>", "API base URL")
|
|
21
|
+
.action(async (name: string, apiUrl: string) => {
|
|
22
|
+
const config = await getConfig();
|
|
23
|
+
if (config.remotes?.[name]) {
|
|
24
|
+
throw new Error(`Remote '${name}' already exists.`);
|
|
25
|
+
}
|
|
26
|
+
const remote = createRemote(name, apiUrl);
|
|
27
|
+
registerRemote(config, remote);
|
|
28
|
+
await updateConfig(config);
|
|
29
|
+
console.log(`Added remote '${name}' (${remote.apiUrl}).`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
command
|
|
33
|
+
.command("use")
|
|
34
|
+
.description("Select the remote used by Knowhow commands in this configuration")
|
|
35
|
+
.argument("<name>", "Remote name")
|
|
36
|
+
.action(async (name: string) => {
|
|
37
|
+
const config = await getConfig();
|
|
38
|
+
const remote = getConfiguredRemotes(config)[name] ??
|
|
39
|
+
(name === DEFAULT_REMOTE_NAME ? createRemote(DEFAULT_REMOTE_NAME, DEFAULT_API_URL) : undefined);
|
|
40
|
+
if (!remote) {
|
|
41
|
+
throw new Error(`Unknown remote '${name}'. Add it with: knowhow remote add ${name} <api-url>`);
|
|
42
|
+
}
|
|
43
|
+
registerRemote(config, remote);
|
|
44
|
+
config.activeRemote = name;
|
|
45
|
+
await updateConfig(config);
|
|
46
|
+
console.log(`Using remote '${name}' (${remote.apiUrl}).`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
command
|
|
50
|
+
.command("list")
|
|
51
|
+
.alias("ls")
|
|
52
|
+
.description("List configured remotes")
|
|
53
|
+
.action(async () => {
|
|
54
|
+
const config = await getConfig();
|
|
55
|
+
const remotes = { ...getConfiguredRemotes(config) };
|
|
56
|
+
if (!remotes[DEFAULT_REMOTE_NAME]) {
|
|
57
|
+
remotes[DEFAULT_REMOTE_NAME] = createRemote(DEFAULT_REMOTE_NAME, DEFAULT_API_URL);
|
|
58
|
+
}
|
|
59
|
+
for (const [name, remote] of Object.entries(remotes)) {
|
|
60
|
+
const marker = (config.activeRemote ?? DEFAULT_REMOTE_NAME) === name ? "*" : " ";
|
|
61
|
+
console.log(`${marker} ${name}\t${remote.apiUrl}\t${remote.jwtPath}${remote.orgId ? `\t${remote.orgId}` : ""}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
command
|
|
66
|
+
.command("get-url")
|
|
67
|
+
.description("Print a remote API URL")
|
|
68
|
+
.argument("[name]", "Remote name")
|
|
69
|
+
.action(async (name?: string) => {
|
|
70
|
+
const config = await getConfig();
|
|
71
|
+
const selectedName = name ?? config.activeRemote ?? DEFAULT_REMOTE_NAME;
|
|
72
|
+
const remote = getConfiguredRemotes(config)[selectedName] ??
|
|
73
|
+
(selectedName === DEFAULT_REMOTE_NAME ? createRemote(DEFAULT_REMOTE_NAME, DEFAULT_API_URL) : undefined);
|
|
74
|
+
if (!remote) throw new Error(`Unknown remote '${selectedName}'.`);
|
|
75
|
+
console.log(remote.apiUrl);
|
|
76
|
+
});
|
|
77
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -4,7 +4,6 @@ import * as os from "os";
|
|
|
4
4
|
import gitignoreToGlob from "gitignore-to-glob";
|
|
5
5
|
import { Prompts } from "./prompts";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
-
import { KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
8
7
|
import {
|
|
9
8
|
Config,
|
|
10
9
|
SourceRef,
|
|
@@ -113,6 +112,15 @@ const defaultConfig = {
|
|
|
113
112
|
},
|
|
114
113
|
},
|
|
115
114
|
|
|
115
|
+
activeRemote: "origin",
|
|
116
|
+
remotes: {
|
|
117
|
+
origin: {
|
|
118
|
+
name: "origin",
|
|
119
|
+
apiUrl: "https://api.knowhow.tyvm.ai",
|
|
120
|
+
jwtPath: ".knowhow/.jwt",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
|
|
116
124
|
syncRemote: false,
|
|
117
125
|
} as Config;
|
|
118
126
|
|
package/src/login.ts
CHANGED
|
@@ -8,6 +8,16 @@ import { KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
|
8
8
|
import { BrowserLoginService } from "./auth/browserLogin";
|
|
9
9
|
import { authenticateWithKey, hasKeyPair, registerPublicKey } from "./auth/keyAuth";
|
|
10
10
|
import { getDefaultPrivateKeyPath, getOrCreatePublicKey } from "./auth/keyManager";
|
|
11
|
+
import { storeJwtForApi } from "./auth/jwtStore";
|
|
12
|
+
import { getOrgIdForApi, setOrgIdForApi } from "./auth/environmentAuth";
|
|
13
|
+
import {
|
|
14
|
+
createRemote,
|
|
15
|
+
findRemoteForApi,
|
|
16
|
+
inferRemoteName,
|
|
17
|
+
registerRemote,
|
|
18
|
+
resolveJwtPath,
|
|
19
|
+
} from "./remotes";
|
|
20
|
+
import { KnowhowRemote } from "./types";
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
23
|
* Log in to Knowhow.
|
|
@@ -23,37 +33,31 @@ import { getDefaultPrivateKeyPath, getOrCreatePublicKey } from "./auth/keyManage
|
|
|
23
33
|
* @param jwtFlag True when --jwt flag is passed.
|
|
24
34
|
* @param identityPath Optional path to a specific private key file (--identity flag).
|
|
25
35
|
*/
|
|
26
|
-
export async function login(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
export async function login(
|
|
37
|
+
jwtFlag?: boolean,
|
|
38
|
+
identityPath?: string,
|
|
39
|
+
apiUrl: string = KNOWHOW_API_URL,
|
|
40
|
+
remoteName?: string
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const config = await getConfig();
|
|
43
|
+
const remote = findRemoteForApi(config, apiUrl) ??
|
|
44
|
+
createRemote(remoteName ?? inferRemoteName(apiUrl), apiUrl);
|
|
30
45
|
|
|
31
46
|
if (jwtFlag) {
|
|
32
47
|
const jwt = await ask("Enter your JWT: ");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const jwtFile = path.join(configDir, ".jwt");
|
|
36
|
-
|
|
37
|
-
if (!fs.existsSync(configDir)) {
|
|
38
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
39
|
-
}
|
|
40
|
-
fs.writeFileSync(jwtFile, jwt);
|
|
41
|
-
fs.chmodSync(jwtFile, 0o600);
|
|
42
|
-
console.log("JWT updated successfully.");
|
|
48
|
+
const jwtFile = storeJwtForApi(jwt, remote.apiUrl, remote.jwtPath);
|
|
49
|
+
console.log(`JWT updated successfully in ${path.relative(process.cwd(), jwtFile)}.`);
|
|
43
50
|
} else {
|
|
44
51
|
const selectedIdentityPath = identityPath ?? getDefaultPrivateKeyPath();
|
|
45
|
-
const
|
|
46
|
-
const orgId = config.orgId;
|
|
52
|
+
const orgId = getOrgIdForApi(config, remote.apiUrl);
|
|
47
53
|
|
|
48
|
-
// A local key is not proof of registration. Let the selected environment
|
|
49
|
-
// check it, but only when an organization is available to bind the attempt.
|
|
50
54
|
if (orgId && hasKeyPair(selectedIdentityPath)) {
|
|
51
55
|
console.log("Found CLI identity — authenticating with public key...");
|
|
52
56
|
try {
|
|
53
|
-
const success = await authenticateWithKey(orgId,
|
|
57
|
+
const success = await authenticateWithKey(orgId, remote.apiUrl, selectedIdentityPath);
|
|
54
58
|
if (success) {
|
|
55
59
|
console.log("✅ Successfully authenticated via public key!");
|
|
56
|
-
return await postLoginConfigUpdate(selectedIdentityPath);
|
|
60
|
+
return await postLoginConfigUpdate(remote, selectedIdentityPath);
|
|
57
61
|
}
|
|
58
62
|
console.warn("Key authentication returned no JWT, falling back to browser login...");
|
|
59
63
|
} catch (error: unknown) {
|
|
@@ -62,30 +66,31 @@ export async function login(jwtFlag?: boolean, identityPath?: string): Promise<v
|
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
await doBrowserLogin(selectedIdentityPath);
|
|
69
|
+
await doBrowserLogin(remote, selectedIdentityPath, orgId);
|
|
67
70
|
identityPath = selectedIdentityPath;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
await postLoginConfigUpdate(identityPath);
|
|
73
|
+
await postLoginConfigUpdate(remote, identityPath);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
/** Complete browser PKCE, then best-effort bootstrap the selected identity. */
|
|
74
|
-
async function doBrowserLogin(
|
|
77
|
+
async function doBrowserLogin(
|
|
78
|
+
remote: KnowhowRemote,
|
|
79
|
+
identityPath?: string,
|
|
80
|
+
orgId?: string
|
|
81
|
+
): Promise<void> {
|
|
75
82
|
console.log("Starting browser-based authentication...");
|
|
76
83
|
try {
|
|
77
|
-
const
|
|
78
|
-
const existingOrgId = existingConfig?.orgId;
|
|
79
|
-
const browserLoginService = new BrowserLoginService(undefined, existingOrgId);
|
|
84
|
+
const browserLoginService = new BrowserLoginService(remote.apiUrl, orgId);
|
|
80
85
|
await browserLoginService.login();
|
|
81
86
|
console.log("✅ Successfully authenticated via browser!");
|
|
82
87
|
|
|
83
88
|
try {
|
|
84
89
|
const keyPair = getOrCreatePublicKey(identityPath);
|
|
85
90
|
await registerPublicKey(
|
|
86
|
-
await loadJwt(),
|
|
91
|
+
await loadJwt(remote),
|
|
87
92
|
keyPair.publicKeyBase64,
|
|
88
|
-
|
|
93
|
+
remote.apiUrl
|
|
89
94
|
);
|
|
90
95
|
console.log(`Registered CLI identity ${keyPair.fingerprint}`);
|
|
91
96
|
} catch (registrationError: unknown) {
|
|
@@ -114,10 +119,13 @@ function isTemporaryIdentityPath(identityPath: string): boolean {
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
/** After any successful login, update the local config with the current user/org. */
|
|
117
|
-
async function postLoginConfigUpdate(
|
|
122
|
+
async function postLoginConfigUpdate(
|
|
123
|
+
remote: KnowhowRemote,
|
|
124
|
+
identityPath?: string
|
|
125
|
+
): Promise<void> {
|
|
118
126
|
try {
|
|
119
|
-
const storedJwt = await loadJwt();
|
|
120
|
-
const { user, currentOrg } = await checkJwt(storedJwt);
|
|
127
|
+
const storedJwt = await loadJwt(remote);
|
|
128
|
+
const { user, currentOrg } = await checkJwt(storedJwt, remote.apiUrl);
|
|
121
129
|
const orgId = currentOrg?.organizationId;
|
|
122
130
|
|
|
123
131
|
console.log(
|
|
@@ -137,8 +145,10 @@ async function postLoginConfigUpdate(identityPath?: string): Promise<void> {
|
|
|
137
145
|
config.modelProviders.push({ provider: "knowhow" });
|
|
138
146
|
}
|
|
139
147
|
|
|
148
|
+
registerRemote(config, orgId ? { ...remote, orgId } : remote);
|
|
149
|
+
config.activeRemote = remote.name;
|
|
140
150
|
if (orgId) {
|
|
141
|
-
config.
|
|
151
|
+
setOrgIdForApi(config, remote.apiUrl, orgId);
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
if (identityPath && !isTemporaryIdentityPath(identityPath)) {
|
|
@@ -164,23 +174,25 @@ async function postLoginConfigUpdate(identityPath?: string): Promise<void> {
|
|
|
164
174
|
}
|
|
165
175
|
}
|
|
166
176
|
|
|
167
|
-
export async function loadJwt(): Promise<string> {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const jwt = fs.readFileSync(jwtFile, "utf-8").trim();
|
|
177
|
+
export async function loadJwt(remote?: KnowhowRemote): Promise<string> {
|
|
178
|
+
const selectedRemote = remote ??
|
|
179
|
+
findRemoteForApi(await getConfig(), KNOWHOW_API_URL) ??
|
|
180
|
+
createRemote(inferRemoteName(KNOWHOW_API_URL), KNOWHOW_API_URL);
|
|
181
|
+
const jwtFile = resolveJwtPath(selectedRemote.jwtPath);
|
|
182
|
+
const jwt = fs.existsSync(jwtFile) ? fs.readFileSync(jwtFile, "utf8").trim() : "";
|
|
174
183
|
|
|
175
184
|
if (!jwt) {
|
|
185
|
+
if (!fs.existsSync(jwtFile)) {
|
|
186
|
+
throw new Error(`Error: JWT file not found: ${jwtFile}`);
|
|
187
|
+
}
|
|
176
188
|
throw new Error("Error: JWT is empty. Re-login with knowhow login --jwt.");
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
return jwt;
|
|
180
192
|
}
|
|
181
193
|
|
|
182
|
-
export async function checkJwt(storedJwt: string) {
|
|
183
|
-
const response = await http.get(`${
|
|
194
|
+
export async function checkJwt(storedJwt: string, apiUrl: string = KNOWHOW_API_URL) {
|
|
195
|
+
const response = await http.get(`${apiUrl}/api/users/me`, {
|
|
184
196
|
headers: {
|
|
185
197
|
Authorization: `Bearer ${storedJwt}`,
|
|
186
198
|
},
|
package/src/remotes.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Config, KnowhowRemote } from "./types";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_REMOTE_NAME = "origin";
|
|
6
|
+
export const DEFAULT_API_URL = "https://api.knowhow.tyvm.ai";
|
|
7
|
+
|
|
8
|
+
export function normalizeApiUrl(apiUrl: string): string {
|
|
9
|
+
const trimmed = apiUrl.trim().replace(/\/+$/, "");
|
|
10
|
+
const url = new URL(trimmed);
|
|
11
|
+
if (!/^https?:$/.test(url.protocol)) {
|
|
12
|
+
throw new Error(`Remote URL must use http or https: ${apiUrl}`);
|
|
13
|
+
}
|
|
14
|
+
return url.toString().replace(/\/$/, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function defaultJwtPath(apiUrl: string): string {
|
|
18
|
+
const normalized = normalizeApiUrl(apiUrl);
|
|
19
|
+
if (normalized === DEFAULT_API_URL) return ".knowhow/.jwt";
|
|
20
|
+
const { hostname, port } = new URL(normalized);
|
|
21
|
+
let suffix: string;
|
|
22
|
+
if (hostname === "localhost" || hostname === "::1" || hostname.startsWith("127.")) {
|
|
23
|
+
suffix = "local";
|
|
24
|
+
} else if (hostname === "api.dev.knowhow.tyvm.ai" || hostname.includes(".dev.")) {
|
|
25
|
+
suffix = "dev";
|
|
26
|
+
} else {
|
|
27
|
+
suffix = `${hostname}${port ? `-${port}` : ""}`
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9.-]+/g, "-")
|
|
30
|
+
.replace(/^-+|-+$/g, "");
|
|
31
|
+
}
|
|
32
|
+
return `.knowhow/.jwt.${suffix}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createRemote(name: string, apiUrl: string): KnowhowRemote {
|
|
36
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
37
|
+
throw new Error("Remote names may only contain letters, numbers, '.', '_' and '-'.");
|
|
38
|
+
}
|
|
39
|
+
const normalizedUrl = normalizeApiUrl(apiUrl);
|
|
40
|
+
return { name, apiUrl: normalizedUrl, jwtPath: defaultJwtPath(normalizedUrl) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getConfiguredRemotes(config: Config): Record<string, KnowhowRemote> {
|
|
44
|
+
return config.remotes ?? {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function findRemoteForApi(config: Config, apiUrl: string): KnowhowRemote | undefined {
|
|
48
|
+
const normalizedUrl = normalizeApiUrl(apiUrl);
|
|
49
|
+
return Object.values(getConfiguredRemotes(config)).find(
|
|
50
|
+
(remote) => normalizeApiUrl(remote.apiUrl) === normalizedUrl
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function findRemoteForApiSync(apiUrl: string): KnowhowRemote | undefined {
|
|
55
|
+
try {
|
|
56
|
+
const config = JSON.parse(
|
|
57
|
+
fs.readFileSync(path.join(process.cwd(), ".knowhow", "knowhow.json"), "utf8")
|
|
58
|
+
) as Config;
|
|
59
|
+
return findRemoteForApi(config, apiUrl);
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getActiveRemote(config: Config): KnowhowRemote {
|
|
66
|
+
const envUrl = process.env.KNOWHOW_API_URL;
|
|
67
|
+
if (envUrl) {
|
|
68
|
+
return findRemoteForApi(config, envUrl) ?? createRemote(inferRemoteName(envUrl), envUrl);
|
|
69
|
+
}
|
|
70
|
+
const name = config.activeRemote ?? DEFAULT_REMOTE_NAME;
|
|
71
|
+
return config.remotes?.[name] ?? createRemote(DEFAULT_REMOTE_NAME, DEFAULT_API_URL);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getActiveRemoteSync(): KnowhowRemote {
|
|
75
|
+
try {
|
|
76
|
+
const config = JSON.parse(
|
|
77
|
+
fs.readFileSync(path.join(process.cwd(), ".knowhow", "knowhow.json"), "utf8")
|
|
78
|
+
) as Config;
|
|
79
|
+
return getActiveRemote(config);
|
|
80
|
+
} catch {
|
|
81
|
+
const apiUrl = process.env.KNOWHOW_API_URL ?? DEFAULT_API_URL;
|
|
82
|
+
return createRemote(inferRemoteName(apiUrl), apiUrl);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function registerRemote(config: Config, remote: KnowhowRemote): void {
|
|
87
|
+
const migratedRemotes: Record<string, KnowhowRemote> = {};
|
|
88
|
+
for (const [apiUrl, orgId] of Object.entries(config.orgIds ?? {})) {
|
|
89
|
+
const name = inferRemoteName(apiUrl);
|
|
90
|
+
migratedRemotes[name] = {
|
|
91
|
+
...createRemote(name, apiUrl),
|
|
92
|
+
orgId,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
config.remotes = { ...migratedRemotes, ...(config.remotes ?? {}), [remote.name]: remote };
|
|
96
|
+
delete config.orgIds;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function inferRemoteName(apiUrl: string): string {
|
|
100
|
+
const normalized = normalizeApiUrl(apiUrl);
|
|
101
|
+
if (normalized === DEFAULT_API_URL) return DEFAULT_REMOTE_NAME;
|
|
102
|
+
const { hostname } = new URL(normalized);
|
|
103
|
+
if (hostname === "localhost" || hostname === "::1" || hostname.startsWith("127.")) return "local";
|
|
104
|
+
if (hostname === "api.dev.knowhow.tyvm.ai" || hostname.includes(".dev.")) return "dev";
|
|
105
|
+
return hostname.toLowerCase().replace(/[^a-z0-9.-]+/g, "-");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveJwtPath(jwtPath: string): string {
|
|
109
|
+
return path.resolve(process.cwd(), jwtPath);
|
|
110
|
+
}
|
|
@@ -5,8 +5,10 @@ import fs from "fs";
|
|
|
5
5
|
import { getConfigSync } from "../config";
|
|
6
6
|
import { exchangeAvailablePublicKeyJwt, storeJwt } from "../auth/keyAuth";
|
|
7
7
|
import { keyPairExists } from "../auth/keyManager";
|
|
8
|
+
import { getOrgIdForApi } from "../auth/environmentAuth";
|
|
9
|
+
import { loadJwtFromDisk } from "../auth/jwtStore";
|
|
10
|
+
import { getActiveRemoteSync } from "../remotes";
|
|
8
11
|
import { Message } from "../clients/types";
|
|
9
|
-
import path from "path";
|
|
10
12
|
import {
|
|
11
13
|
CompletionOptions,
|
|
12
14
|
CompletionResponse,
|
|
@@ -125,7 +127,7 @@ export interface GitCredentialResponse {
|
|
|
125
127
|
expiresAt: string | null;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
export function loadKnowhowJwt(): string {
|
|
130
|
+
export function loadKnowhowJwt(apiUrl: string = KNOWHOW_API_URL): string {
|
|
129
131
|
// Check env var first — this is how cloud workers/CI runners get auth
|
|
130
132
|
// (injected via KNOWHOW_JWT env var in the runner's .env file by GithubRunnerService).
|
|
131
133
|
// This avoids depending on a potentially stale .knowhow/.jwt file from the snapshot.
|
|
@@ -133,17 +135,10 @@ export function loadKnowhowJwt(): string {
|
|
|
133
135
|
return process.env.KNOWHOW_JWT;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
|
|
137
|
-
if (!fs.existsSync(jwtFile)) {
|
|
138
|
-
return "";
|
|
139
|
-
}
|
|
140
|
-
const jwt = fs.readFileSync(jwtFile, "utf-8").trim();
|
|
141
|
-
|
|
142
|
-
return jwt;
|
|
138
|
+
return loadJwtFromDisk(apiUrl);
|
|
143
139
|
}
|
|
144
140
|
|
|
145
|
-
export const KNOWHOW_API_URL =
|
|
146
|
-
process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai";
|
|
141
|
+
export const KNOWHOW_API_URL = getActiveRemoteSync().apiUrl;
|
|
147
142
|
|
|
148
143
|
@TraceAll()
|
|
149
144
|
export class KnowhowSimpleClient {
|
|
@@ -159,7 +154,7 @@ export class KnowhowSimpleClient {
|
|
|
159
154
|
// Explicit tokens (including KNOWHOW_JWT worker credentials) have no
|
|
160
155
|
// corresponding local identity and must never trigger interactive renewal.
|
|
161
156
|
this.canRefreshJwt = jwt === undefined && !process.env.KNOWHOW_JWT;
|
|
162
|
-
this.jwt = jwt ?? loadKnowhowJwt();
|
|
157
|
+
this.jwt = jwt ?? loadKnowhowJwt(this.baseUrl);
|
|
163
158
|
this.setJwt(this.jwt);
|
|
164
159
|
if (this.canRefreshJwt) {
|
|
165
160
|
Object.defineProperty(this.headers, HTTP_UNAUTHORIZED_HANDLER, {
|
|
@@ -175,7 +170,7 @@ export class KnowhowSimpleClient {
|
|
|
175
170
|
* Reload the JWT from disk (useful after login refreshes the token).
|
|
176
171
|
*/
|
|
177
172
|
refreshJwt() {
|
|
178
|
-
const freshJwt = loadKnowhowJwt();
|
|
173
|
+
const freshJwt = loadKnowhowJwt(this.baseUrl);
|
|
179
174
|
if (freshJwt) {
|
|
180
175
|
this.setJwt(freshJwt);
|
|
181
176
|
this.jwtValidated = false;
|
|
@@ -206,12 +201,13 @@ export class KnowhowSimpleClient {
|
|
|
206
201
|
this.refreshPromise = (async () => {
|
|
207
202
|
const config = getConfigSync();
|
|
208
203
|
const identityPath = config.cliIdentityPath;
|
|
209
|
-
|
|
204
|
+
const orgId = getOrgIdForApi(config, this.baseUrl);
|
|
205
|
+
if (!orgId || (!keyPairExists(identityPath) && !keyPairExists())) {
|
|
210
206
|
throw new Error("JWT expired and the CLI identity is unavailable. Please run `knowhow login` again.");
|
|
211
207
|
}
|
|
212
208
|
try {
|
|
213
|
-
const freshJwt = await exchangeAvailablePublicKeyJwt(
|
|
214
|
-
storeJwt(freshJwt);
|
|
209
|
+
const freshJwt = await exchangeAvailablePublicKeyJwt(orgId, this.baseUrl, identityPath);
|
|
210
|
+
storeJwt(freshJwt, this.baseUrl);
|
|
215
211
|
this.setJwt(freshJwt);
|
|
216
212
|
this.jwtValidated = true;
|
|
217
213
|
} catch (error) {
|