@datename/fastcode-termux 0.1.6-beta → 0.1.6

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 (246) hide show
  1. package/README.md +6 -24
  2. package/dist/agent/guards.d.ts +0 -8
  3. package/dist/agent/guards.js +0 -195
  4. package/dist/agent/protocol.js +1 -4
  5. package/dist/agent/tools.d.ts +1 -3
  6. package/dist/agent/tools.js +377 -2
  7. package/dist/app.js +103 -335
  8. package/dist/cli.js +311 -362
  9. package/dist/commands/index.js +0 -22
  10. package/dist/commands/login.d.ts +2 -4
  11. package/dist/commands/login.js +10 -12
  12. package/dist/commands/logout.js +16 -8
  13. package/dist/commands/model.d.ts +0 -13
  14. package/dist/commands/model.js +22 -42
  15. package/dist/commands/provider.d.ts +0 -13
  16. package/dist/commands/provider.js +9 -20
  17. package/dist/commands/whoami.js +5 -6
  18. package/dist/config/store.d.ts +12 -32
  19. package/dist/config/store.js +65 -117
  20. package/dist/config/trust.d.ts +8 -21
  21. package/dist/config/trust.js +13 -99
  22. package/dist/lib/diagnostics.d.ts +4 -29
  23. package/dist/lib/diagnostics.js +4 -60
  24. package/dist/lib/http.d.ts +1 -11
  25. package/dist/lib/http.js +4 -62
  26. package/dist/providers/anthropic.d.ts +0 -1
  27. package/dist/providers/anthropic.js +0 -12
  28. package/dist/providers/custom.d.ts +0 -9
  29. package/dist/providers/deepseek.d.ts +0 -10
  30. package/dist/providers/deepseek.js +1 -2
  31. package/dist/providers/google.d.ts +0 -9
  32. package/dist/providers/groq.d.ts +0 -10
  33. package/dist/providers/groq.js +1 -2
  34. package/dist/providers/index.d.ts +201 -10
  35. package/dist/providers/index.js +0 -30
  36. package/dist/providers/mistral.d.ts +0 -10
  37. package/dist/providers/mistral.js +1 -2
  38. package/dist/providers/moonshot.d.ts +0 -10
  39. package/dist/providers/moonshot.js +1 -2
  40. package/dist/providers/ollama.d.ts +0 -9
  41. package/dist/providers/openai.d.ts +0 -10
  42. package/dist/providers/openai.js +1 -2
  43. package/dist/providers/openaiCompatible.d.ts +0 -9
  44. package/dist/providers/openaiCompatible.js +10 -58
  45. package/dist/providers/openrouter.d.ts +0 -10
  46. package/dist/providers/openrouter.js +1 -2
  47. package/dist/providers/qwen.d.ts +0 -10
  48. package/dist/providers/qwen.js +1 -2
  49. package/dist/providers/xai.d.ts +0 -10
  50. package/dist/providers/xai.js +1 -2
  51. package/dist/providers/zhipu.d.ts +0 -10
  52. package/dist/providers/zhipu.js +1 -2
  53. package/dist/session/log.d.ts +12 -35
  54. package/dist/session/log.js +10 -206
  55. package/dist/types.d.ts +1 -60
  56. package/dist/ui/Message.d.ts +7 -1
  57. package/dist/ui/Message.js +2 -13
  58. package/dist/ui/Prompt.d.ts +2 -1
  59. package/dist/ui/Prompt.js +2 -2
  60. package/dist/ui/Spinner.js +5 -11
  61. package/dist/ui/StatusBar.d.ts +1 -2
  62. package/dist/ui/StatusBar.js +5 -6
  63. package/package.json +1 -1
  64. package/src/agent/guards.ts +0 -196
  65. package/src/agent/protocol.ts +1 -4
  66. package/src/agent/tools.ts +377 -3
  67. package/src/app.ts +103 -335
  68. package/src/cli.ts +3 -30
  69. package/src/commands/index.ts +0 -22
  70. package/src/commands/login.ts +10 -12
  71. package/src/commands/logout.ts +16 -8
  72. package/src/commands/model.ts +22 -42
  73. package/src/commands/provider.ts +9 -20
  74. package/src/commands/whoami.ts +5 -6
  75. package/src/config/store.ts +69 -122
  76. package/src/config/trust.ts +21 -137
  77. package/src/lib/diagnostics.ts +4 -60
  78. package/src/lib/http.ts +4 -66
  79. package/src/providers/anthropic.ts +1 -13
  80. package/src/providers/deepseek.ts +1 -2
  81. package/src/providers/groq.ts +1 -2
  82. package/src/providers/index.ts +1 -32
  83. package/src/providers/mistral.ts +1 -2
  84. package/src/providers/moonshot.ts +1 -2
  85. package/src/providers/openai.ts +1 -2
  86. package/src/providers/openaiCompatible.ts +10 -58
  87. package/src/providers/openrouter.ts +1 -2
  88. package/src/providers/qwen.ts +1 -2
  89. package/src/providers/xai.ts +1 -2
  90. package/src/providers/zhipu.ts +1 -2
  91. package/src/session/log.ts +28 -267
  92. package/src/types.ts +1 -63
  93. package/src/ui/Message.ts +2 -13
  94. package/src/ui/Prompt.ts +2 -2
  95. package/src/ui/Spinner.ts +5 -11
  96. package/src/ui/StatusBar.ts +5 -6
  97. package/dist/agent/approval.d.ts +0 -50
  98. package/dist/agent/approval.js +0 -7
  99. package/dist/agent/compaction.d.ts +0 -39
  100. package/dist/agent/compaction.js +0 -227
  101. package/dist/agent/context.d.ts +0 -29
  102. package/dist/agent/context.js +0 -62
  103. package/dist/agent/ignore.d.ts +0 -11
  104. package/dist/agent/ignore.js +0 -129
  105. package/dist/agent/process-tree.d.ts +0 -2
  106. package/dist/agent/process-tree.js +0 -31
  107. package/dist/agent/protocol-mode.d.ts +0 -6
  108. package/dist/agent/protocol-mode.js +0 -51
  109. package/dist/agent/registry.d.ts +0 -17
  110. package/dist/agent/registry.js +0 -90
  111. package/dist/agent/runner.d.ts +0 -106
  112. package/dist/agent/runner.js +0 -352
  113. package/dist/agent/shell-session.d.ts +0 -37
  114. package/dist/agent/shell-session.js +0 -400
  115. package/dist/agent/stream.d.ts +0 -29
  116. package/dist/agent/stream.js +0 -103
  117. package/dist/agent/tool-schemas.d.ts +0 -2
  118. package/dist/agent/tool-schemas.js +0 -63
  119. package/dist/agent/tools/append_file.d.ts +0 -2
  120. package/dist/agent/tools/append_file.js +0 -26
  121. package/dist/agent/tools/apply_patch.d.ts +0 -10
  122. package/dist/agent/tools/apply_patch.js +0 -52
  123. package/dist/agent/tools/copy_file.d.ts +0 -2
  124. package/dist/agent/tools/copy_file.js +0 -30
  125. package/dist/agent/tools/delete_file.d.ts +0 -2
  126. package/dist/agent/tools/delete_file.js +0 -24
  127. package/dist/agent/tools/grep.d.ts +0 -2
  128. package/dist/agent/tools/grep.js +0 -23
  129. package/dist/agent/tools/index.d.ts +0 -2
  130. package/dist/agent/tools/index.js +0 -32
  131. package/dist/agent/tools/list_files.d.ts +0 -2
  132. package/dist/agent/tools/list_files.js +0 -17
  133. package/dist/agent/tools/make_dir.d.ts +0 -2
  134. package/dist/agent/tools/make_dir.js +0 -24
  135. package/dist/agent/tools/move_path.d.ts +0 -2
  136. package/dist/agent/tools/move_path.js +0 -47
  137. package/dist/agent/tools/paths.d.ts +0 -7
  138. package/dist/agent/tools/paths.js +0 -84
  139. package/dist/agent/tools/pwd.d.ts +0 -2
  140. package/dist/agent/tools/pwd.js +0 -10
  141. package/dist/agent/tools/read_file.d.ts +0 -2
  142. package/dist/agent/tools/read_file.js +0 -21
  143. package/dist/agent/tools/read_many.d.ts +0 -2
  144. package/dist/agent/tools/read_many.js +0 -25
  145. package/dist/agent/tools/readers.d.ts +0 -6
  146. package/dist/agent/tools/readers.js +0 -216
  147. package/dist/agent/tools/replace_in_file.d.ts +0 -2
  148. package/dist/agent/tools/replace_in_file.js +0 -30
  149. package/dist/agent/tools/shell.d.ts +0 -2
  150. package/dist/agent/tools/shell.js +0 -48
  151. package/dist/agent/tools/stat_path.d.ts +0 -2
  152. package/dist/agent/tools/stat_path.js +0 -31
  153. package/dist/agent/tools/types.d.ts +0 -39
  154. package/dist/agent/tools/types.js +0 -1
  155. package/dist/agent/tools/validate.d.ts +0 -6
  156. package/dist/agent/tools/validate.js +0 -29
  157. package/dist/agent/tools/write_file.d.ts +0 -2
  158. package/dist/agent/tools/write_file.js +0 -26
  159. package/dist/commands/attach.d.ts +0 -9
  160. package/dist/commands/attach.js +0 -15
  161. package/dist/commands/bg.d.ts +0 -7
  162. package/dist/commands/bg.js +0 -9
  163. package/dist/commands/cancel.d.ts +0 -10
  164. package/dist/commands/cancel.js +0 -10
  165. package/dist/commands/compact.d.ts +0 -10
  166. package/dist/commands/compact.js +0 -22
  167. package/dist/commands/context.d.ts +0 -7
  168. package/dist/commands/context.js +0 -24
  169. package/dist/commands/edit.d.ts +0 -10
  170. package/dist/commands/edit.js +0 -36
  171. package/dist/commands/effort.d.ts +0 -25
  172. package/dist/commands/effort.js +0 -48
  173. package/dist/commands/expand.d.ts +0 -10
  174. package/dist/commands/expand.js +0 -8
  175. package/dist/commands/jobs.d.ts +0 -7
  176. package/dist/commands/jobs.js +0 -16
  177. package/dist/commands/kill.d.ts +0 -7
  178. package/dist/commands/kill.js +0 -11
  179. package/dist/commands/steer.d.ts +0 -11
  180. package/dist/commands/steer.js +0 -13
  181. package/dist/lib/model-meta.d.ts +0 -19
  182. package/dist/lib/model-meta.js +0 -113
  183. package/dist/lib/native-tools.d.ts +0 -12
  184. package/dist/lib/native-tools.js +0 -21
  185. package/dist/lib/net.d.ts +0 -33
  186. package/dist/lib/net.js +0 -168
  187. package/dist/patch/apply.d.ts +0 -15
  188. package/dist/patch/apply.js +0 -172
  189. package/dist/patch/parse.d.ts +0 -29
  190. package/dist/patch/parse.js +0 -112
  191. package/dist/patch/plan.d.ts +0 -49
  192. package/dist/patch/plan.js +0 -273
  193. package/dist/session/projection.d.ts +0 -43
  194. package/dist/session/projection.js +0 -257
  195. package/dist/ui/Picker.d.ts +0 -8
  196. package/dist/ui/Picker.js +0 -24
  197. package/src/agent/approval.ts +0 -47
  198. package/src/agent/compaction.ts +0 -276
  199. package/src/agent/context.ts +0 -97
  200. package/src/agent/ignore.ts +0 -147
  201. package/src/agent/process-tree.ts +0 -32
  202. package/src/agent/protocol-mode.ts +0 -59
  203. package/src/agent/registry.ts +0 -98
  204. package/src/agent/runner.ts +0 -443
  205. package/src/agent/shell-session.ts +0 -449
  206. package/src/agent/stream.ts +0 -143
  207. package/src/agent/tool-schemas.ts +0 -67
  208. package/src/agent/tools/append_file.ts +0 -28
  209. package/src/agent/tools/apply_patch.ts +0 -54
  210. package/src/agent/tools/copy_file.ts +0 -32
  211. package/src/agent/tools/delete_file.ts +0 -26
  212. package/src/agent/tools/grep.ts +0 -25
  213. package/src/agent/tools/index.ts +0 -34
  214. package/src/agent/tools/list_files.ts +0 -19
  215. package/src/agent/tools/make_dir.ts +0 -26
  216. package/src/agent/tools/move_path.ts +0 -50
  217. package/src/agent/tools/paths.ts +0 -92
  218. package/src/agent/tools/pwd.ts +0 -12
  219. package/src/agent/tools/read_file.ts +0 -23
  220. package/src/agent/tools/read_many.ts +0 -27
  221. package/src/agent/tools/readers.ts +0 -228
  222. package/src/agent/tools/replace_in_file.ts +0 -32
  223. package/src/agent/tools/shell.ts +0 -53
  224. package/src/agent/tools/stat_path.ts +0 -33
  225. package/src/agent/tools/types.ts +0 -42
  226. package/src/agent/tools/validate.ts +0 -35
  227. package/src/agent/tools/write_file.ts +0 -28
  228. package/src/commands/attach.ts +0 -16
  229. package/src/commands/bg.ts +0 -9
  230. package/src/commands/cancel.ts +0 -10
  231. package/src/commands/compact.ts +0 -23
  232. package/src/commands/context.ts +0 -25
  233. package/src/commands/edit.ts +0 -37
  234. package/src/commands/effort.ts +0 -49
  235. package/src/commands/expand.ts +0 -8
  236. package/src/commands/jobs.ts +0 -17
  237. package/src/commands/kill.ts +0 -12
  238. package/src/commands/steer.ts +0 -13
  239. package/src/lib/model-meta.ts +0 -138
  240. package/src/lib/native-tools.ts +0 -32
  241. package/src/lib/net.ts +0 -204
  242. package/src/patch/apply.ts +0 -194
  243. package/src/patch/parse.ts +0 -131
  244. package/src/patch/plan.ts +0 -317
  245. package/src/session/projection.ts +0 -308
  246. package/src/ui/Picker.ts +0 -24
package/src/cli.ts CHANGED
@@ -12,9 +12,7 @@ import { loadConfig, saveConfig, resolveProviderKey } from "./config/store.js";
12
12
  import { getPaths } from "./config/paths.js";
13
13
  import { PROVIDERS, getProvider } from "./providers/index.js";
14
14
  import { collectDiagnostics, formatDiagnostics } from "./lib/diagnostics.js";
15
- import { existsSync } from "node:fs";
16
- import { latestSessionFile, listSessions, projectionPath, readSession, resolveSessionFile } from "./session/log.js";
17
- import { readProjection, sessionMetaFromFile } from "./session/projection.js";
15
+ import { latestSessionFile, listSessions, readSession, resolveSessionFile } from "./session/log.js";
18
16
  import { listTrusted, trustProject, untrustProject } from "./config/trust.js";
19
17
  const NL = String.fromCharCode(10);
20
18
  const HELP = [
@@ -158,26 +156,13 @@ export async function parseCli(): Promise<any> {
158
156
  return { done: true };
159
157
  }
160
158
  if (rest[0] === "show") {
161
- const idOrFile = rest[1] ?? all[0].id;
162
- const entries = readSession(idOrFile);
159
+ const entries = readSession(rest[1] ?? all[0].id);
163
160
  for (const entry of entries) {
164
161
  if (entry.type === "message")
165
162
  console.log("[" + entry.role + "] " + entry.content);
166
- else if (entry.type === "user" || entry.type === "assistant_raw")
167
- console.log("[" + entry.type + "] " + String(entry.content ?? "").slice(0, 300));
168
163
  else
169
164
  console.log("[" + entry.type + "]");
170
165
  }
171
- const file = resolveSessionFile(idOrFile);
172
- const projection = readProjection(file);
173
- console.log("");
174
- if (projection) {
175
- console.log("[projection] " + projectionPath(file));
176
- console.log(" messages: " + projection.messages.length + " · activeFiles: " + projection.activeFiles.length + " · lastFactSeq: " + projection.lastFactSeq);
177
- }
178
- else {
179
- console.log("[projection] (none)");
180
- }
181
166
  return { done: true };
182
167
  }
183
168
  console.log("Sessions (newest first):");
@@ -192,22 +177,10 @@ export async function parseCli(): Promise<any> {
192
177
  if (first === "resume") {
193
178
  const target = rest[0];
194
179
  const file = !target || target === "latest" ? latestSessionFile() : resolveSessionFile(target);
195
- if (!file || !existsSync(file)) {
180
+ if (!file) {
196
181
  console.error("No saved session to resume. Start one with: fastcode");
197
182
  process.exit(1);
198
183
  }
199
- const meta = sessionMetaFromFile(file);
200
- if (meta.cwd) {
201
- if (existsSync(meta.cwd)) {
202
- process.chdir(meta.cwd);
203
- console.log("Working directory: " + meta.cwd);
204
- }
205
- else {
206
- console.log("Session directory no longer exists: " + meta.cwd + " (staying in " + process.cwd() + ")");
207
- }
208
- }
209
- if (meta.providerId)
210
- console.log("Session model: " + meta.providerId + "::" + meta.model);
211
184
  console.log("Resuming " + file);
212
185
  return { done: false, resume: file };
213
186
  }
@@ -7,7 +7,6 @@ import { clearCommand } from "./clear.js";
7
7
  import { exitCommand } from "./exit.js";
8
8
  import { modelCommand } from "./model.js";
9
9
  import { providerCommand } from "./provider.js";
10
- import { effortCommand } from "./effort.js";
11
10
  import { loginCommand } from "./login.js";
12
11
  import { logoutCommand } from "./logout.js";
13
12
  import { skillsCommand } from "./skills.js";
@@ -18,16 +17,6 @@ import { systemCommand } from "./system.js";
18
17
  import { aboutCommand } from "./about.js";
19
18
  import { configCommand } from "./config.js";
20
19
  import { whoamiCommand } from "./whoami.js";
21
- import { contextCommand } from "./context.js";
22
- import { jobsCommand } from "./jobs.js";
23
- import { killCommand } from "./kill.js";
24
- import { cancelCommand } from "./cancel.js";
25
- import { steerCommand } from "./steer.js";
26
- import { compactCommand } from "./compact.js";
27
- import { expandCommand } from "./expand.js";
28
- import { attachCommand } from "./attach.js";
29
- import { bgCommand } from "./bg.js";
30
- import { editCommand } from "./edit.js";
31
20
  export const COMMANDS = [
32
21
  helpCommand,
33
22
  agentCommand,
@@ -38,7 +27,6 @@ export const COMMANDS = [
38
27
  exitCommand,
39
28
  modelCommand,
40
29
  providerCommand,
41
- effortCommand,
42
30
  loginCommand,
43
31
  logoutCommand,
44
32
  skillsCommand,
@@ -49,16 +37,6 @@ export const COMMANDS = [
49
37
  aboutCommand,
50
38
  configCommand,
51
39
  whoamiCommand,
52
- contextCommand,
53
- jobsCommand,
54
- killCommand,
55
- cancelCommand,
56
- steerCommand,
57
- compactCommand,
58
- expandCommand,
59
- attachCommand,
60
- bgCommand,
61
- editCommand,
62
40
  ];
63
41
  export function findCommand(name) {
64
42
  const lower = name.toLowerCase();
@@ -2,10 +2,8 @@ import { getProvider, PROVIDERS } from "../providers/index.js";
2
2
  import { maskKey, setCredential } from "../config/store.js";
3
3
  import { getPaths } from "../config/paths.js";
4
4
  /**
5
- * Save a provider-wide key and describe it for the terminal. `model` is kept
6
- * for the `/login provider model key` one-liner and only recorded as the
7
- * credential's origin — the key itself is reused by every model of that
8
- * provider. Shared by the `/login` command and the guided flow in the app.
5
+ * Save a key scoped to one provider + model and describe it for the terminal.
6
+ * Shared by the `/login` command and the guided flow in the app.
9
7
  */
10
8
  export function saveScopedKey(providerId, providerLabel, model, key) {
11
9
  const trimmed = key.trim();
@@ -14,16 +12,16 @@ export function saveScopedKey(providerId, providerLabel, model, key) {
14
12
  return [
15
13
  "Saved API key",
16
14
  ` provider ${providerLabel} (${providerId})`,
17
- ` scope all ${providerId} models (saved from ${model || "unknown"})`,
15
+ ` model ${model}`,
18
16
  ` key ${maskKey(trimmed)}`,
19
17
  ` stored ${paths.auth} (mode 0600)`,
20
18
  "",
21
- `Remove with /logout ${providerId}`,
19
+ `Remove with /logout ${providerId} ${model}`,
22
20
  ].join("\n");
23
21
  }
24
22
  export const loginCommand = {
25
23
  name: "login",
26
- description: "Bind a provider-wide API key. Usage: /login [provider] [model] [key]",
24
+ description: "Bind an API key to a provider + model. Usage: /login [provider] [model] [key]",
27
25
  run({ args, state, actions }) {
28
26
  if (args.length === 0) {
29
27
  actions.startLogin({});
@@ -38,12 +36,12 @@ export const loginCommand = {
38
36
  }
39
37
  if (args.length === 1) {
40
38
  actions.startLogin({ provider: provider.id });
41
- return { message: `Provider: ${provider.label} (${provider.id}).\nThe key will apply to every model of this provider.` };
39
+ return { message: `Provider: ${provider.label} (${provider.id}).\nNow pick the model this key is for.` };
42
40
  }
43
41
  const model = args[1];
44
42
  if (args.length === 2) {
45
43
  actions.startLogin({ provider: provider.id, model });
46
- return { message: `Provider: ${provider.id} · saved from model: ${model}.\nNow paste the API key (input is masked).` };
44
+ return { message: `Provider: ${provider.id} · model: ${model}.\nNow paste the API key (input is masked).` };
47
45
  }
48
46
  const key = args.slice(2).join(" ");
49
47
  return { message: saveScopedKey(provider.id, provider.label, model, key) };
@@ -52,11 +50,11 @@ export const loginCommand = {
52
50
  function introText(state) {
53
51
  const env = getProvider(state.providerId)?.authEnv?.[0] ?? "PROVIDER_API_KEY";
54
52
  return [
55
- "Bind a provider-wide API key (reused by every model of that provider).",
53
+ "Bind an API key to a specific provider + model.",
56
54
  "",
57
- ` current provider: ${state.providerId}`,
55
+ ` current scope: ${state.providerId} · ${state.model}`,
58
56
  "",
59
- "FastCode will ask for the provider, then optionally a model name, then the key.",
57
+ "FastCode will ask for the provider, then the model, then the key.",
60
58
  " one-liner: /login <provider> <model> <key>",
61
59
  ` or set: export ${env}=<key> (provider-wide, any model)`,
62
60
  ].join("\n");
@@ -8,11 +8,11 @@ export const logoutCommand = {
8
8
  const credentials = listCredentials();
9
9
  if (credentials.length === 0)
10
10
  return { message: "No stored keys. Use /login to add one." };
11
- const lines = ["Stored keys (provider-wide):"];
11
+ const lines = ["Stored keys (provider :: model):"];
12
12
  for (const c of credentials)
13
- lines.push(` ${c.provider} ${maskKey(c.key)}`);
13
+ lines.push(` ${c.provider}::${c.model} ${maskKey(c.key)}`);
14
14
  lines.push("");
15
- lines.push("Remove: /logout <provider>");
15
+ lines.push("Remove: /logout <provider> <model>");
16
16
  lines.push(" /logout <provider> --all");
17
17
  return { message: lines.join("\n") };
18
18
  }
@@ -29,17 +29,25 @@ export const logoutCommand = {
29
29
  };
30
30
  }
31
31
  if (args.length >= 2) {
32
- const ok = removeCredential(provider.id);
32
+ const model = args[1];
33
+ const ok = removeCredential(provider.id, model);
33
34
  return {
34
35
  message: ok
35
- ? `Removed the stored key for ${provider.id} (applies to all its models).`
36
- : `No stored key for ${provider.id}.`,
36
+ ? `Removed stored key for ${provider.id}::${model}.`
37
+ : `No stored key for ${provider.id}::${model}.`,
37
38
  };
38
39
  }
39
40
  const forProvider = listCredentials().filter((c) => c.provider === provider.id);
40
41
  if (forProvider.length === 0)
41
42
  return { message: `No stored key for ${provider.label}.` };
42
- removeCredential(provider.id);
43
- return { message: `Removed the stored key for ${provider.label} (all models).` };
43
+ if (forProvider.length === 1) {
44
+ removeCredential(forProvider[0].provider, forProvider[0].model);
45
+ return { message: `Removed stored key for ${forProvider[0].provider}::${forProvider[0].model}.` };
46
+ }
47
+ const lines = [`${provider.label} has ${forProvider.length} stored keys — pick one to remove:`];
48
+ for (const c of forProvider)
49
+ lines.push(` /logout ${c.provider} ${c.model} (${maskKey(c.key)})`);
50
+ lines.push(` /logout ${provider.id} --all (remove all)`);
51
+ return { message: lines.join("\n") };
44
52
  },
45
53
  };
@@ -1,50 +1,30 @@
1
- import { getProviderOrThrow, probeProvider } from "../providers/index.js";
1
+ import { getProviderOrThrow } from "../providers/index.js";
2
2
  import { resolveProviderKey } from "../config/store.js";
3
3
  export const modelCommand = {
4
4
  name: "model",
5
- description: "Pick a model interactively, or switch with /model <id>",
5
+ description: "List models for the current provider, or switch model. Usage: /model [id]",
6
6
  async run({ args, state, actions }) {
7
7
  const provider = getProviderOrThrow(state.providerId);
8
- if (args.length > 0) {
9
- const id = args.join(" ");
10
- actions.setModel(id);
11
- return { message: `Model set to ${id}.` };
8
+ if (args.length === 0) {
9
+ const { key } = resolveProviderKey(provider.id, provider.authEnv);
10
+ const models = await provider.listModels(key);
11
+ if (models.length === 0) {
12
+ return { message: `No models available from ${provider.label}.` };
13
+ }
14
+ const lines = [`Models for ${provider.label}:`];
15
+ for (const m of models.slice(0, 50)) {
16
+ const marker = m.id === state.model ? "● " : " ";
17
+ const label = m.label ? ` — ${m.label}` : "";
18
+ lines.push(`${marker}${m.id}${label}`);
19
+ }
20
+ if (models.length > 50)
21
+ lines.push(` … and ${models.length - 50} more.`);
22
+ lines.push("");
23
+ lines.push("Switch with `/model <id>`.");
24
+ return { message: lines.join("\n") };
12
25
  }
13
- const { key } = resolveProviderKey(provider.id, provider.authEnv);
14
- const fetched = await provider.listModels(key).catch(() => []);
15
- const models = dedupe(fetched);
16
- if (models.length === 0) {
17
- return { message: `No models available from ${provider.label}.` };
18
- }
19
- const online = await probeProvider(provider, key);
20
- const list = models.slice(0, 50);
21
- return {
22
- message: `Models for ${provider.label}: ${list.length}${online ? "" : " (offline — showing the built-in list)"}.\n` +
23
- "Use ↑/↓, a number, or Enter to select.",
24
- picker: {
25
- title: `Select model · ${provider.label}${online ? "" : " (offline)"}`,
26
- current: state.model,
27
- items: list.map((m) => ({
28
- value: m.id,
29
- label: m.id,
30
- hint: m.label ? `— ${m.label}` : undefined,
31
- })),
32
- onSelect: (id) => {
33
- actions.setModel(id);
34
- actions.pushSystemMessage(`Model set to ${id}.`);
35
- },
36
- },
37
- };
26
+ const id = args.join(" ");
27
+ actions.setModel(id);
28
+ return { message: `Model set to ${id}.` };
38
29
  },
39
30
  };
40
- function dedupe(models) {
41
- const seen = new Set();
42
- const out = [];
43
- for (const model of models) {
44
- if (!model?.id || seen.has(model.id))
45
- continue;
46
- seen.add(model.id);
47
- out.push(model);
48
- }
49
- return out;
50
- }
@@ -4,26 +4,15 @@ export const providerCommand = {
4
4
  description: "List or switch the active LLM provider. Usage: /provider [id]",
5
5
  run({ args, actions, state }) {
6
6
  if (args.length === 0) {
7
- return {
8
- message: "Select a provider (↑/↓ or a number, Enter to confirm).",
9
- picker: {
10
- title: "Select provider",
11
- current: state.providerId,
12
- items: PROVIDERS.map((p) => ({
13
- value: p.id,
14
- label: `${p.id.padEnd(12)} ${p.label}`,
15
- hint: p.authEnv.length === 0 ? "(no key needed)" : `[${p.authEnv.join(", ")}]`,
16
- })),
17
- onSelect: (id) => {
18
- const next = getProvider(id);
19
- if (!next)
20
- return;
21
- actions.setProvider(next.id);
22
- actions.setModel(next.defaultModel);
23
- actions.pushSystemMessage(`Provider set to ${next.label}. Default model: ${next.defaultModel}.`);
24
- },
25
- },
26
- };
7
+ const lines = ["Available providers:"];
8
+ for (const p of PROVIDERS) {
9
+ const marker = p.id === state.providerId ? "● " : " ";
10
+ const auth = p.authEnv.length === 0 ? "(no key needed)" : `[${p.authEnv.join(", ")}]`;
11
+ lines.push(`${marker}${p.id.padEnd(12)} ${p.label} ${auth}`);
12
+ }
13
+ lines.push("");
14
+ lines.push("Switch with `/provider <id>` (e.g. `/provider openrouter`).");
15
+ return { message: lines.join("\n") };
27
16
  }
28
17
  const target = args[0];
29
18
  const next = getProvider(target);
@@ -10,20 +10,19 @@ export const whoamiCommand = {
10
10
  lines.push(" " + describeLookup(current, resolveApiKey(current.id, state.model, current.authEnv)));
11
11
  const credentials = listCredentials();
12
12
  lines.push("");
13
- lines.push(`Stored keys (${credentials.length}, provider-wide):`);
13
+ lines.push(`Stored keys (${credentials.length}):`);
14
14
  if (credentials.length === 0)
15
15
  lines.push(" (none) — add one with /login");
16
16
  for (const c of credentials) {
17
- const marker = c.provider === state.providerId ? "● " : " ";
18
- const origin = c.model ? ` (originally saved for ${c.model})` : "";
19
- lines.push(`${marker}${c.provider} ${maskKey(c.key)}${origin}`);
17
+ const marker = c.provider === state.providerId && c.model === state.model ? "● " : " ";
18
+ lines.push(`${marker}${c.provider}::${c.model} ${maskKey(c.key)}`);
20
19
  }
21
20
  const legacy = listLegacy();
22
21
  if (legacy.length > 0) {
23
22
  lines.push("");
24
- lines.push("Legacy keys (could not be migrated — re-save with /login):");
23
+ lines.push("Unbound legacy keys (NOT used — re-bind them to a model):");
25
24
  for (const l of legacy)
26
- lines.push(` ${l.provider} ${maskKey(l.key)}`);
25
+ lines.push(` ${l.provider} ${maskKey(l.key)} → /login ${l.provider} <model> <key>`);
27
26
  }
28
27
  const envLines = [];
29
28
  for (const p of PROVIDERS) {
@@ -5,12 +5,14 @@
5
5
  * edit it from `nano ~/.fastcode/config.json` if needed. Atomic writes prevent
6
6
  * partial files on Ctrl+C.
7
7
  *
8
- * API keys are scoped to a provider, not to a model. auth.json stores one
9
- * credential per provider id, so switching between models of the same provider
10
- * reuses the same key. The old "provider::model" (v2) and provider-only (v1)
11
- * formats are read and migrated to the provider-scoped v3 layout on first load.
8
+ * API keys are NOT a single global string: each one is scoped to a concrete
9
+ * "provider + model" pair. auth.json stores one credential per
10
+ * `<provider>::<model>` scope, and a stored key is only used when the current
11
+ * provider AND model match that scope. The old provider-only format is read as
12
+ * "legacy": it is shown back to the user but never used silently, because a
13
+ * provider-only key does not tell us which model it was meant for.
12
14
  */
13
- import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync } from "node:fs";
15
+ import { readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
14
16
  import { join, dirname } from "node:path";
15
17
  import { getPaths, lockdown, ensureDir } from "./paths.js";
16
18
  import { DEFAULT_THEME } from "./themes.js";
@@ -27,40 +29,20 @@ export const DEFAULT_CONFIG = {
27
29
  enabledPlugins: [],
28
30
  skillsRegistry: "https://skillshub.wtf",
29
31
  lifetimeCostUsd: 0,
30
- /**
31
- * Extra directories the file tools may read and write outside the project
32
- * root. Relative entries are resolved against the project root; `~/` is
33
- * expanded against the user's home directory.
34
- */
35
- sharedDirs: [],
36
- /** Maximum agent tool turns before the loop asks the user to continue. */
37
- maxAgentTurns: 24,
38
- /** Abort a stream that receives no data for this long (ms). */
39
- requestIdleTimeoutMs: 60_000,
40
- /** Approximate character budget before history compaction kicks in. */
41
- contextBudgetChars: 400_000,
42
- /** Thinking depth: auto = send nothing, provider default. */
43
- reasoningEffort: "auto",
44
- /** Cached per-provider/model capabilities (native vs pseudo tool calling). */
45
- capabilities: {
46
- toolCalling: {},
47
- },
48
32
  };
49
33
  export interface StoredCredential {
50
34
  provider: string;
51
- /** Model the key was originally saved for (display only; keys are provider-wide). */
52
- model?: string;
35
+ model: string;
53
36
  key: string;
54
37
  addedAt: string;
55
38
  }
56
39
  export interface AuthStore {
57
40
  version: number;
58
- /** Provider-scoped credentials, keyed by provider id. */
41
+ /** Scoped credentials, keyed by `provider::model`. */
59
42
  keys: Record<string, StoredCredential>;
60
- /** Old provider-only keys that could not be migrated (kept for diagnostics). */
43
+ /** Old provider-only keys we refuse to use until the user re-binds them. */
61
44
  legacy: Record<string, string>;
62
45
  }
63
- export const AUTH_VERSION = 3;
64
46
  export type KeyLookup = {
65
47
  status: "ok";
66
48
  key: string;
@@ -89,97 +71,58 @@ export function saveConfig(cfg) {
89
71
  const { config } = getPaths();
90
72
  atomicWrite(config, JSON.stringify(cfg, null, 2));
91
73
  }
92
- /** Composite id used by the legacy v2 format; kept for API compatibility. */
74
+ /** Composite scope id for a provider + model credential. */
93
75
  export function scopeId(providerId, model) {
94
76
  return `${providerId}::${model}`;
95
77
  }
96
- /** Pick the most recently saved credential when several legacy scopes map to one provider. */
97
- function keepNewest(keys, credential) {
98
- const existing = keys[credential.provider];
99
- if (!existing) {
100
- keys[credential.provider] = credential;
101
- return;
102
- }
103
- const previous = Date.parse(existing.addedAt || "") || 0;
104
- const next = Date.parse(credential.addedAt || "") || 0;
105
- if (next > previous)
106
- keys[credential.provider] = credential;
107
- }
108
- let migrationChecked = false;
109
78
  export function loadAuth(): AuthStore {
110
79
  const { auth } = getPaths();
111
- const empty: AuthStore = { version: AUTH_VERSION, keys: {}, legacy: {} };
80
+ const empty: AuthStore = { version: 2, keys: {}, legacy: {} };
112
81
  if (!existsSync(auth))
113
82
  return empty;
114
- let raw: any;
115
83
  try {
116
- raw = JSON.parse(readFileSync(auth, "utf8"));
117
- }
118
- catch {
119
- return empty;
120
- }
121
- if (!raw || typeof raw !== "object")
122
- return empty;
123
- const keys: Record<string, StoredCredential> = {};
124
- const legacy: Record<string, string> = {};
125
- let migrated = false;
126
- const source = raw.keys && typeof raw.keys === "object" ? raw.keys : {};
127
- for (const [id, value] of Object.entries<any>(source)) {
128
- if (value && typeof value === "object" && typeof value.key === "string" && value.key.trim()) {
129
- const [idProvider, idModel] = id.split("::");
130
- const provider = typeof value.provider === "string" && value.provider ? value.provider : idProvider || id;
131
- const model = typeof value.model === "string" ? value.model : idModel || "";
132
- if (id.includes("::") || raw.version !== AUTH_VERSION)
133
- migrated = true;
134
- keepNewest(keys, {
135
- provider,
136
- model,
137
- key: value.key,
138
- addedAt: typeof value.addedAt === "string" ? value.addedAt : "",
139
- });
140
- }
141
- else if (typeof value === "string" && value.trim()) {
142
- // v1: keys were plain provider -> key strings.
143
- keepNewest(keys, { provider: id, key: value, addedAt: "" });
144
- migrated = true;
84
+ const raw = JSON.parse(readFileSync(auth, "utf8"));
85
+ const keys: Record<string, StoredCredential> = {};
86
+ const legacy: Record<string, string> = {};
87
+ const source = raw && typeof raw === "object" ? (raw.keys ?? {}) : {};
88
+ for (const [id, value] of Object.entries<any>(source)) {
89
+ if (value && typeof value === "object" && typeof value.key === "string") {
90
+ const [idProvider, idModel] = id.split("::");
91
+ const provider = value.provider || idProvider || id;
92
+ const model = value.model || idModel || "";
93
+ keys[scopeId(provider, model)] = {
94
+ provider,
95
+ model,
96
+ key: value.key,
97
+ addedAt: value.addedAt ?? "",
98
+ };
99
+ }
100
+ else if (typeof value === "string" && value.trim()) {
101
+ legacy[id] = value;
102
+ }
145
103
  }
146
- }
147
- if (raw.legacy && typeof raw.legacy === "object") {
148
- for (const [provider, value] of Object.entries<any>(raw.legacy)) {
149
- if (typeof value === "string" && value.trim()) {
150
- // Provider-only keys are exactly what v3 wants: migrate instead
151
- // of leaving them unusable.
152
- keepNewest(keys, { provider, key: value, addedAt: "" });
153
- migrated = true;
104
+ if (raw && typeof raw === "object" && raw.legacy && typeof raw.legacy === "object") {
105
+ for (const [id, value] of Object.entries<any>(raw.legacy)) {
106
+ if (typeof value === "string" && value.trim())
107
+ legacy[id] = value;
154
108
  }
155
109
  }
110
+ return { version: 2, keys, legacy };
156
111
  }
157
- if (migrated && !migrationChecked) {
158
- try {
159
- // Keep the pre-migration file so a user with several old model
160
- // keys can recover a credential we had to collapse.
161
- const backup = auth + ".v2.bak";
162
- if (!existsSync(backup))
163
- copyFileSync(auth, backup);
164
- writeAuth({ version: AUTH_VERSION, keys, legacy });
165
- }
166
- catch {
167
- // Read-only home (or locked file): keep the migrated view in memory.
168
- }
112
+ catch {
113
+ return empty;
169
114
  }
170
- migrationChecked = true;
171
- return { version: AUTH_VERSION, keys, legacy };
172
115
  }
173
- function writeAuth(store: AuthStore) {
116
+ export function saveAuth(store: AuthStore) {
174
117
  const { auth } = getPaths();
175
- atomicWrite(auth, JSON.stringify({ version: AUTH_VERSION, keys: store.keys }, null, 2));
118
+ const payload: any = { version: 2, keys: store.keys };
119
+ if (Object.keys(store.legacy).length > 0)
120
+ payload.legacy = store.legacy;
121
+ atomicWrite(auth, JSON.stringify(payload, null, 2));
176
122
  lockdown(auth);
177
123
  }
178
- export function saveAuth(store: AuthStore) {
179
- writeAuth(store);
180
- }
181
- export function getCredential(providerId, _model?): StoredCredential | undefined {
182
- return loadAuth().keys[providerId];
124
+ export function getCredential(providerId, model): StoredCredential | undefined {
125
+ return loadAuth().keys[scopeId(providerId, model)];
183
126
  }
184
127
  export function listCredentials(): StoredCredential[] {
185
128
  return Object.values(loadAuth().keys);
@@ -193,35 +136,38 @@ export function listLegacy(): Array<{
193
136
  export function getLegacy(providerId): string | undefined {
194
137
  return loadAuth().legacy[providerId];
195
138
  }
196
- /** Save/replace the provider-wide credential (drops any legacy key). */
139
+ /** Save/replace the credential for one provider+model scope (drops the legacy key). */
197
140
  export function setCredential(providerId, model, key): StoredCredential {
198
141
  const store = loadAuth();
199
- const credential: StoredCredential = {
142
+ const credential = {
200
143
  provider: providerId,
201
144
  model,
202
145
  key: key.trim(),
203
146
  addedAt: new Date().toISOString(),
204
147
  };
205
- store.keys[providerId] = credential;
148
+ store.keys[scopeId(providerId, model)] = credential;
206
149
  delete store.legacy[providerId];
207
150
  saveAuth(store);
208
151
  return credential;
209
152
  }
210
- export function removeCredential(providerId, _model?): boolean {
153
+ export function removeCredential(providerId, model): boolean {
211
154
  const store = loadAuth();
212
- if (!store.keys[providerId])
155
+ const id = scopeId(providerId, model);
156
+ if (!store.keys[id])
213
157
  return false;
214
- delete store.keys[providerId];
158
+ delete store.keys[id];
215
159
  saveAuth(store);
216
160
  return true;
217
161
  }
218
- /** Remove the provider-wide credential plus any legacy leftovers. */
162
+ /** Remove every credential for a provider (all models) plus its legacy key. */
219
163
  export function removeCredentialsForProvider(providerId): number {
220
164
  const store = loadAuth();
221
165
  let removed = 0;
222
- if (store.keys[providerId]) {
223
- delete store.keys[providerId];
224
- removed += 1;
166
+ for (const id of Object.keys(store.keys)) {
167
+ if (store.keys[id].provider === providerId) {
168
+ delete store.keys[id];
169
+ removed += 1;
170
+ }
225
171
  }
226
172
  if (store.legacy[providerId]) {
227
173
  delete store.legacy[providerId];
@@ -241,16 +187,16 @@ export function maskKey(key) {
241
187
  return value.slice(0, 3) + "****" + value.slice(-4);
242
188
  }
243
189
  /**
244
- * Resolve the key to use for a provider:
245
- * 1. the provider-wide credential in auth.json,
190
+ * Resolve the key to use for a specific provider+model:
191
+ * 1. the scoped credential in auth.json (exact provider+model match), then
246
192
  * 2. the provider's environment variable (provider-scoped, any model), and
247
- * 3. a legacy provider-only key that somehow escaped migration.
193
+ * 3. report an "unbound" legacy key instead of using it silently.
248
194
  */
249
- export function resolveApiKey(providerId, _model, envVars): KeyLookup {
195
+ export function resolveApiKey(providerId, model, envVars): KeyLookup {
250
196
  const store = loadAuth();
251
- const credential = store.keys[providerId];
252
- if (credential && credential.key)
253
- return { status: "ok", key: credential.key, source: "file" };
197
+ const scoped = store.keys[scopeId(providerId, model)];
198
+ if (scoped && scoped.key)
199
+ return { status: "ok", key: scoped.key, source: "file" };
254
200
  for (const envVar of envVars ?? []) {
255
201
  const value = process.env[envVar];
256
202
  if (value && value.trim())
@@ -267,9 +213,10 @@ export function resolveProviderKey(providerId, envVars): {
267
213
  envVar?: string;
268
214
  } {
269
215
  const store = loadAuth();
270
- const credential = store.keys[providerId];
271
- if (credential && credential.key)
272
- return { key: credential.key, source: "file" };
216
+ for (const credential of Object.values(store.keys)) {
217
+ if (credential.provider === providerId && credential.key)
218
+ return { key: credential.key, source: "file" };
219
+ }
273
220
  for (const envVar of envVars ?? []) {
274
221
  const value = process.env[envVar];
275
222
  if (value && value.trim())