@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.
Files changed (94) hide show
  1. package/README.md +17 -1
  2. package/package.json +1 -1
  3. package/src/agents/base/base.ts +161 -34
  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/cli.ts +2 -0
  10. package/src/clients/index.ts +25 -5
  11. package/src/clients/pricing/fireworks.ts +32 -2
  12. package/src/commands/mcp.ts +1 -0
  13. package/src/commands/misc.ts +1 -1
  14. package/src/commands/remote.ts +77 -0
  15. package/src/config.ts +9 -1
  16. package/src/login.ts +54 -42
  17. package/src/remotes.ts +110 -0
  18. package/src/services/KnowhowClient.ts +12 -16
  19. package/src/services/Mcp.ts +94 -7
  20. package/src/services/McpWebsocketTransport.ts +14 -11
  21. package/src/types.ts +12 -0
  22. package/tests/unit/McpWebsocketTransport.test.ts +56 -0
  23. package/tests/unit/auth/jwtStore.test.ts +55 -0
  24. package/tests/unit/auth/registerIdentity.test.ts +8 -2
  25. package/tests/unit/commands/remote.test.ts +79 -0
  26. package/tests/unit/login.test.ts +64 -3
  27. package/tests/unit/services/KnowhowClient.test.ts +2 -2
  28. package/tests/unit/services/Mcp.test.ts +2 -2
  29. package/ts_build/package.json +1 -1
  30. package/ts_build/src/agents/base/base.d.ts +8 -3
  31. package/ts_build/src/agents/base/base.js +98 -22
  32. package/ts_build/src/agents/base/base.js.map +1 -1
  33. package/ts_build/src/auth/browserLogin.js +2 -7
  34. package/ts_build/src/auth/browserLogin.js.map +1 -1
  35. package/ts_build/src/auth/environmentAuth.d.ts +5 -0
  36. package/ts_build/src/auth/environmentAuth.js +48 -0
  37. package/ts_build/src/auth/environmentAuth.js.map +1 -0
  38. package/ts_build/src/auth/jwtStore.d.ts +4 -0
  39. package/ts_build/src/auth/jwtStore.js +65 -0
  40. package/ts_build/src/auth/jwtStore.js.map +1 -0
  41. package/ts_build/src/auth/keyAuth.d.ts +1 -1
  42. package/ts_build/src/auth/keyAuth.js +4 -16
  43. package/ts_build/src/auth/keyAuth.js.map +1 -1
  44. package/ts_build/src/auth/registerIdentity.js +6 -7
  45. package/ts_build/src/auth/registerIdentity.js.map +1 -1
  46. package/ts_build/src/cli.js +2 -0
  47. package/ts_build/src/cli.js.map +1 -1
  48. package/ts_build/src/clients/index.js +19 -5
  49. package/ts_build/src/clients/index.js.map +1 -1
  50. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -0
  51. package/ts_build/src/clients/pricing/fireworks.js +14 -2
  52. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  53. package/ts_build/src/commands/mcp.js.map +1 -1
  54. package/ts_build/src/commands/misc.js +1 -1
  55. package/ts_build/src/commands/misc.js.map +1 -1
  56. package/ts_build/src/commands/remote.d.ts +2 -0
  57. package/ts_build/src/commands/remote.js +70 -0
  58. package/ts_build/src/commands/remote.js.map +1 -0
  59. package/ts_build/src/config.js +8 -0
  60. package/ts_build/src/config.js.map +1 -1
  61. package/ts_build/src/login.d.ts +4 -3
  62. package/ts_build/src/login.js +34 -35
  63. package/ts_build/src/login.js.map +1 -1
  64. package/ts_build/src/remotes.d.ts +14 -0
  65. package/ts_build/src/remotes.js +117 -0
  66. package/ts_build/src/remotes.js.map +1 -0
  67. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  68. package/ts_build/src/services/KnowhowClient.js +12 -14
  69. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  70. package/ts_build/src/services/Mcp.d.ts +1 -1
  71. package/ts_build/src/services/Mcp.js +68 -7
  72. package/ts_build/src/services/Mcp.js.map +1 -1
  73. package/ts_build/src/services/McpWebsocketTransport.d.ts +1 -0
  74. package/ts_build/src/services/McpWebsocketTransport.js +11 -6
  75. package/ts_build/src/services/McpWebsocketTransport.js.map +1 -1
  76. package/ts_build/src/types.d.ts +13 -0
  77. package/ts_build/src/types.js.map +1 -1
  78. package/ts_build/tests/unit/McpWebsocketTransport.test.d.ts +1 -0
  79. package/ts_build/tests/unit/McpWebsocketTransport.test.js +47 -0
  80. package/ts_build/tests/unit/McpWebsocketTransport.test.js.map +1 -0
  81. package/ts_build/tests/unit/auth/jwtStore.test.d.ts +1 -0
  82. package/ts_build/tests/unit/auth/jwtStore.test.js +46 -0
  83. package/ts_build/tests/unit/auth/jwtStore.test.js.map +1 -0
  84. package/ts_build/tests/unit/auth/registerIdentity.test.js +8 -2
  85. package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -1
  86. package/ts_build/tests/unit/commands/remote.test.d.ts +1 -0
  87. package/ts_build/tests/unit/commands/remote.test.js +70 -0
  88. package/ts_build/tests/unit/commands/remote.test.js.map +1 -0
  89. package/ts_build/tests/unit/login.test.js +44 -3
  90. package/ts_build/tests/unit/login.test.js.map +1 -1
  91. package/ts_build/tests/unit/services/KnowhowClient.test.js +2 -2
  92. package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -1
  93. package/ts_build/tests/unit/services/Mcp.test.js +2 -2
  94. 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.74, cache_hit: 0.15, output: 3.48 },
85
- [FireworksModels.DeepseekV4Flash]: { input: 0.14, cache_hit: 0.03, output: 0.28 },
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,
@@ -273,6 +273,7 @@ export function addMcpCommands(program: Command): void {
273
273
  await listRemoteSecrets();
274
274
  }
275
275
  });
276
+
276
277
  }
277
278
 
278
279
  // ──────────────────────────────────────────────────────────────────────────────
@@ -63,7 +63,7 @@ export function addLoginCommand(program: Command): void {
63
63
  return;
64
64
  }
65
65
 
66
- await login(options.jwt, options.identity);
66
+ await login(options.jwt, options.identity, options.apiUrl);
67
67
  });
68
68
  }
69
69
 
@@ -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(jwtFlag?: boolean, identityPath?: string): Promise<void> {
27
- if (!KNOWHOW_API_URL) {
28
- throw new Error("Error: KNOWHOW_API_URL environment variable not set.");
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
- const configDir = path.join(process.cwd(), ".knowhow");
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 config = await getConfig();
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, KNOWHOW_API_URL, selectedIdentityPath);
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
- // Fall back when no org/key is configured or when key auth is unavailable.
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(identityPath?: string): Promise<void> {
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 existingConfig = await getConfig();
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
- KNOWHOW_API_URL
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(identityPath?: string): Promise<void> {
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.orgId = orgId;
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 jwtFile = path.join(process.cwd(), ".knowhow", ".jwt");
169
- if (!fs.existsSync(jwtFile)) {
170
- throw new Error("Error: JWT file not found.");
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(`${KNOWHOW_API_URL}/api/users/me`, {
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
- const jwtFile = path.join(process.cwd(), ".knowhow", ".jwt");
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
- if (!config.orgId || (!keyPairExists(identityPath) && !keyPairExists())) {
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(config.orgId, this.baseUrl, identityPath);
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) {