@datename/fastcode-termux 0.1.6 → 0.1.8-beta.0

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 (303) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +67 -16
  3. package/dist/agent/approval-json.d.ts +43 -0
  4. package/dist/agent/approval-json.js +69 -0
  5. package/dist/agent/approval-native.d.ts +3 -0
  6. package/dist/agent/approval-native.js +165 -0
  7. package/dist/agent/approval-store.d.ts +16 -0
  8. package/dist/agent/approval-store.js +21 -0
  9. package/dist/agent/approval.d.ts +83 -0
  10. package/dist/agent/approval.js +42 -0
  11. package/dist/agent/compaction.d.ts +41 -0
  12. package/dist/agent/compaction.js +270 -0
  13. package/dist/agent/context.d.ts +29 -0
  14. package/dist/agent/context.js +62 -0
  15. package/dist/agent/guards.d.ts +24 -16
  16. package/dist/agent/guards.js +1167 -18
  17. package/dist/agent/ignore.d.ts +11 -0
  18. package/dist/agent/ignore.js +129 -0
  19. package/dist/agent/policy/defaults.d.ts +16 -0
  20. package/dist/agent/policy/defaults.js +17 -0
  21. package/dist/agent/policy/egress.d.ts +32 -0
  22. package/dist/agent/policy/egress.js +280 -0
  23. package/dist/agent/process-tree.d.ts +22 -0
  24. package/dist/agent/process-tree.js +102 -0
  25. package/dist/agent/protocol-mode.d.ts +6 -0
  26. package/dist/agent/protocol-mode.js +51 -0
  27. package/dist/agent/protocol.d.ts +20 -1
  28. package/dist/agent/protocol.js +169 -39
  29. package/dist/agent/registry.d.ts +19 -0
  30. package/dist/agent/registry.js +247 -0
  31. package/dist/agent/runner.d.ts +127 -0
  32. package/dist/agent/runner.js +391 -0
  33. package/dist/agent/shell-ast.d.ts +11 -0
  34. package/dist/agent/shell-ast.js +204 -0
  35. package/dist/agent/shell-session.d.ts +51 -0
  36. package/dist/agent/shell-session.js +611 -0
  37. package/dist/agent/stream.d.ts +29 -0
  38. package/dist/agent/stream.js +103 -0
  39. package/dist/agent/tool-schemas.d.ts +2 -0
  40. package/dist/agent/tool-schemas.js +82 -0
  41. package/dist/agent/tools/append_file.d.ts +2 -0
  42. package/dist/agent/tools/append_file.js +28 -0
  43. package/dist/agent/tools/apply_patch.d.ts +10 -0
  44. package/dist/agent/tools/apply_patch.js +92 -0
  45. package/dist/agent/tools/copy_file.d.ts +2 -0
  46. package/dist/agent/tools/copy_file.js +53 -0
  47. package/dist/agent/tools/delete_file.d.ts +2 -0
  48. package/dist/agent/tools/delete_file.js +43 -0
  49. package/dist/agent/tools/grep.d.ts +2 -0
  50. package/dist/agent/tools/grep.js +54 -0
  51. package/dist/agent/tools/index.d.ts +2 -0
  52. package/dist/agent/tools/index.js +32 -0
  53. package/dist/agent/tools/list_files.d.ts +2 -0
  54. package/dist/agent/tools/list_files.js +17 -0
  55. package/dist/agent/tools/make_dir.d.ts +2 -0
  56. package/dist/agent/tools/make_dir.js +26 -0
  57. package/dist/agent/tools/move_path.d.ts +9 -0
  58. package/dist/agent/tools/move_path.js +73 -0
  59. package/dist/agent/tools/paths.d.ts +7 -0
  60. package/dist/agent/tools/paths.js +84 -0
  61. package/dist/agent/tools/pwd.d.ts +2 -0
  62. package/dist/agent/tools/pwd.js +10 -0
  63. package/dist/agent/tools/read_file.d.ts +2 -0
  64. package/dist/agent/tools/read_file.js +81 -0
  65. package/dist/agent/tools/read_many.d.ts +2 -0
  66. package/dist/agent/tools/read_many.js +43 -0
  67. package/dist/agent/tools/readers.d.ts +58 -0
  68. package/dist/agent/tools/readers.js +426 -0
  69. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  70. package/dist/agent/tools/replace_in_file.js +63 -0
  71. package/dist/agent/tools/shell.d.ts +2 -0
  72. package/dist/agent/tools/shell.js +48 -0
  73. package/dist/agent/tools/stat_path.d.ts +2 -0
  74. package/dist/agent/tools/stat_path.js +31 -0
  75. package/dist/agent/tools/types.d.ts +65 -0
  76. package/dist/agent/tools/types.js +1 -0
  77. package/dist/agent/tools/validate.d.ts +19 -0
  78. package/dist/agent/tools/validate.js +92 -0
  79. package/dist/agent/tools/write_file.d.ts +2 -0
  80. package/dist/agent/tools/write_file.js +28 -0
  81. package/dist/agent/tools.d.ts +3 -1
  82. package/dist/agent/tools.js +2 -377
  83. package/dist/app.d.ts +2 -1
  84. package/dist/app.js +846 -173
  85. package/dist/cli-main.js +363 -0
  86. package/dist/cli.js +444 -333
  87. package/dist/commands/about.js +1 -1
  88. package/dist/commands/attach.d.ts +9 -0
  89. package/dist/commands/attach.js +15 -0
  90. package/dist/commands/bg.d.ts +7 -0
  91. package/dist/commands/bg.js +9 -0
  92. package/dist/commands/cancel.d.ts +10 -0
  93. package/dist/commands/cancel.js +10 -0
  94. package/dist/commands/clear.d.ts +1 -0
  95. package/dist/commands/clear.js +1 -1
  96. package/dist/commands/compact.d.ts +22 -0
  97. package/dist/commands/compact.js +34 -0
  98. package/dist/commands/context.d.ts +7 -0
  99. package/dist/commands/context.js +25 -0
  100. package/dist/commands/edit.d.ts +10 -0
  101. package/dist/commands/edit.js +36 -0
  102. package/dist/commands/effort.d.ts +25 -0
  103. package/dist/commands/effort.js +48 -0
  104. package/dist/commands/expand.d.ts +10 -0
  105. package/dist/commands/expand.js +8 -0
  106. package/dist/commands/index.js +24 -0
  107. package/dist/commands/jobs.d.ts +7 -0
  108. package/dist/commands/jobs.js +17 -0
  109. package/dist/commands/kill.d.ts +7 -0
  110. package/dist/commands/kill.js +13 -0
  111. package/dist/commands/login.d.ts +6 -4
  112. package/dist/commands/login.js +24 -22
  113. package/dist/commands/logout.js +8 -16
  114. package/dist/commands/model.d.ts +1 -0
  115. package/dist/commands/model.js +10 -22
  116. package/dist/commands/provider.d.ts +1 -2
  117. package/dist/commands/provider.js +4 -11
  118. package/dist/commands/skills.js +34 -4
  119. package/dist/commands/steer.d.ts +11 -0
  120. package/dist/commands/steer.js +13 -0
  121. package/dist/commands/threads.d.ts +9 -0
  122. package/dist/commands/threads.js +26 -0
  123. package/dist/commands/whoami.js +6 -5
  124. package/dist/config/store.d.ts +53 -12
  125. package/dist/config/store.js +163 -73
  126. package/dist/config/trust.d.ts +30 -8
  127. package/dist/config/trust.js +139 -14
  128. package/dist/headless.d.ts +7 -0
  129. package/dist/headless.js +213 -0
  130. package/dist/index.js +19 -3
  131. package/dist/lib/debug.d.ts +2 -0
  132. package/dist/lib/debug.js +22 -0
  133. package/dist/lib/diagnostics.d.ts +48 -4
  134. package/dist/lib/diagnostics.js +90 -8
  135. package/dist/lib/highlight-subset.d.ts +21 -0
  136. package/dist/lib/highlight-subset.js +88 -0
  137. package/dist/lib/http.d.ts +11 -1
  138. package/dist/lib/http.js +62 -4
  139. package/dist/lib/lifecycle.d.ts +5 -0
  140. package/dist/lib/lifecycle.js +62 -0
  141. package/dist/lib/markdown.js +22 -4
  142. package/dist/lib/model-meta.d.ts +19 -0
  143. package/dist/lib/model-meta.js +113 -0
  144. package/dist/lib/native-tools.d.ts +12 -0
  145. package/dist/lib/native-tools.js +21 -0
  146. package/dist/lib/net.d.ts +33 -0
  147. package/dist/lib/net.js +168 -0
  148. package/dist/patch/apply.d.ts +15 -0
  149. package/dist/patch/apply.js +172 -0
  150. package/dist/patch/parse.d.ts +29 -0
  151. package/dist/patch/parse.js +112 -0
  152. package/dist/patch/plan.d.ts +49 -0
  153. package/dist/patch/plan.js +273 -0
  154. package/dist/providers/anthropic.d.ts +1 -0
  155. package/dist/providers/anthropic.js +12 -0
  156. package/dist/providers/custom.d.ts +9 -0
  157. package/dist/providers/deepseek.d.ts +10 -0
  158. package/dist/providers/deepseek.js +3 -2
  159. package/dist/providers/google.d.ts +9 -0
  160. package/dist/providers/groq.d.ts +10 -0
  161. package/dist/providers/groq.js +2 -1
  162. package/dist/providers/index.d.ts +10 -201
  163. package/dist/providers/index.js +30 -0
  164. package/dist/providers/mistral.d.ts +10 -0
  165. package/dist/providers/mistral.js +2 -1
  166. package/dist/providers/moonshot.d.ts +10 -0
  167. package/dist/providers/moonshot.js +2 -1
  168. package/dist/providers/ollama.d.ts +9 -0
  169. package/dist/providers/openai.d.ts +10 -0
  170. package/dist/providers/openai.js +2 -1
  171. package/dist/providers/openaiCompatible.d.ts +33 -2
  172. package/dist/providers/openaiCompatible.js +78 -16
  173. package/dist/providers/openrouter.d.ts +10 -0
  174. package/dist/providers/openrouter.js +2 -1
  175. package/dist/providers/qwen.d.ts +10 -0
  176. package/dist/providers/qwen.js +2 -1
  177. package/dist/providers/spec.d.ts +12 -0
  178. package/dist/providers/spec.js +29 -0
  179. package/dist/providers/xai.d.ts +10 -0
  180. package/dist/providers/xai.js +2 -1
  181. package/dist/providers/zhipu.d.ts +10 -0
  182. package/dist/providers/zhipu.js +2 -1
  183. package/dist/session/log.d.ts +49 -14
  184. package/dist/session/log.js +236 -11
  185. package/dist/session/projection.d.ts +76 -0
  186. package/dist/session/projection.js +526 -0
  187. package/dist/skills/manager.d.ts +15 -0
  188. package/dist/skills/manager.js +135 -18
  189. package/dist/types.d.ts +75 -1
  190. package/dist/ui/Message.d.ts +1 -7
  191. package/dist/ui/Message.js +13 -2
  192. package/dist/ui/Picker.d.ts +8 -0
  193. package/dist/ui/Picker.js +24 -0
  194. package/dist/ui/Prompt.d.ts +1 -2
  195. package/dist/ui/Prompt.js +2 -2
  196. package/dist/ui/Spinner.js +11 -5
  197. package/dist/ui/StatusBar.d.ts +2 -1
  198. package/dist/ui/StatusBar.js +6 -5
  199. package/package.json +3 -2
  200. package/src/agent/approval-json.ts +103 -0
  201. package/src/agent/approval-native.ts +168 -0
  202. package/src/agent/approval-store.ts +38 -0
  203. package/src/agent/approval.ts +94 -0
  204. package/src/agent/compaction.ts +321 -0
  205. package/src/agent/context.ts +97 -0
  206. package/src/agent/guards.ts +1239 -19
  207. package/src/agent/ignore.ts +147 -0
  208. package/src/agent/policy/defaults.ts +34 -0
  209. package/src/agent/policy/egress.ts +335 -0
  210. package/src/agent/process-tree.ts +114 -0
  211. package/src/agent/protocol-mode.ts +59 -0
  212. package/src/agent/protocol.ts +191 -40
  213. package/src/agent/registry.ts +270 -0
  214. package/src/agent/runner.ts +486 -0
  215. package/src/agent/shell-ast.ts +227 -0
  216. package/src/agent/shell-session.ts +673 -0
  217. package/src/agent/stream.ts +143 -0
  218. package/src/agent/tool-schemas.ts +86 -0
  219. package/src/agent/tools/append_file.ts +30 -0
  220. package/src/agent/tools/apply_patch.ts +95 -0
  221. package/src/agent/tools/copy_file.ts +56 -0
  222. package/src/agent/tools/delete_file.ts +45 -0
  223. package/src/agent/tools/grep.ts +57 -0
  224. package/src/agent/tools/index.ts +34 -0
  225. package/src/agent/tools/list_files.ts +19 -0
  226. package/src/agent/tools/make_dir.ts +28 -0
  227. package/src/agent/tools/move_path.ts +83 -0
  228. package/src/agent/tools/paths.ts +92 -0
  229. package/src/agent/tools/pwd.ts +12 -0
  230. package/src/agent/tools/read_file.ts +84 -0
  231. package/src/agent/tools/read_many.ts +45 -0
  232. package/src/agent/tools/readers.ts +491 -0
  233. package/src/agent/tools/replace_in_file.ts +66 -0
  234. package/src/agent/tools/shell.ts +53 -0
  235. package/src/agent/tools/stat_path.ts +33 -0
  236. package/src/agent/tools/types.ts +84 -0
  237. package/src/agent/tools/validate.ts +104 -0
  238. package/src/agent/tools/write_file.ts +30 -0
  239. package/src/agent/tools.ts +3 -377
  240. package/src/app.ts +853 -170
  241. package/src/{cli.ts → cli-main.ts} +64 -9
  242. package/src/commands/about.ts +1 -1
  243. package/src/commands/attach.ts +16 -0
  244. package/src/commands/bg.ts +9 -0
  245. package/src/commands/cancel.ts +10 -0
  246. package/src/commands/clear.ts +1 -1
  247. package/src/commands/compact.ts +35 -0
  248. package/src/commands/context.ts +26 -0
  249. package/src/commands/edit.ts +37 -0
  250. package/src/commands/effort.ts +49 -0
  251. package/src/commands/expand.ts +8 -0
  252. package/src/commands/index.ts +24 -0
  253. package/src/commands/jobs.ts +18 -0
  254. package/src/commands/kill.ts +14 -0
  255. package/src/commands/login.ts +24 -22
  256. package/src/commands/logout.ts +8 -16
  257. package/src/commands/model.ts +10 -22
  258. package/src/commands/provider.ts +4 -11
  259. package/src/commands/skills.ts +34 -4
  260. package/src/commands/steer.ts +13 -0
  261. package/src/commands/threads.ts +28 -0
  262. package/src/commands/whoami.ts +6 -5
  263. package/src/config/store.ts +168 -77
  264. package/src/config/trust.ts +181 -22
  265. package/src/headless.ts +225 -0
  266. package/src/index.ts +19 -3
  267. package/src/lib/debug.ts +24 -0
  268. package/src/lib/diagnostics.ts +90 -8
  269. package/src/lib/highlight-subset.ts +93 -0
  270. package/src/lib/http.ts +66 -4
  271. package/src/lib/lifecycle.ts +69 -0
  272. package/src/lib/markdown.ts +22 -4
  273. package/src/lib/model-meta.ts +138 -0
  274. package/src/lib/native-tools.ts +32 -0
  275. package/src/lib/net.ts +204 -0
  276. package/src/patch/apply.ts +194 -0
  277. package/src/patch/parse.ts +131 -0
  278. package/src/patch/plan.ts +317 -0
  279. package/src/providers/anthropic.ts +13 -1
  280. package/src/providers/deepseek.ts +3 -2
  281. package/src/providers/groq.ts +2 -1
  282. package/src/providers/index.ts +32 -1
  283. package/src/providers/mistral.ts +2 -1
  284. package/src/providers/moonshot.ts +2 -1
  285. package/src/providers/openai.ts +2 -1
  286. package/src/providers/openaiCompatible.ts +104 -19
  287. package/src/providers/openrouter.ts +2 -1
  288. package/src/providers/qwen.ts +2 -1
  289. package/src/providers/spec.ts +42 -0
  290. package/src/providers/xai.ts +2 -1
  291. package/src/providers/zhipu.ts +2 -1
  292. package/src/session/log.ts +311 -29
  293. package/src/session/projection.ts +621 -0
  294. package/src/skills/manager.ts +140 -18
  295. package/src/types/highlight-shims.d.ts +14 -0
  296. package/src/types.ts +79 -2
  297. package/src/ui/Message.ts +13 -2
  298. package/src/ui/Picker.ts +24 -0
  299. package/src/ui/Prompt.ts +2 -2
  300. package/src/ui/Spinner.ts +11 -5
  301. package/src/ui/StatusBar.ts +6 -5
  302. package/tsconfig.json +1 -0
  303. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { installSkill, uninstallSkill, listInstalledSkills, searchRegistry, getSkill, } from "../skills/manager.js";
1
+ import { installSkill, uninstallSkill, listInstalledSkills, searchRegistry, getSkill, rebaselineSkills, resolveRegistry, } from "../skills/manager.js";
2
2
  export const skillsCommand = {
3
3
  name: "skills",
4
4
  description: "Manage Agent Skills (download from skills.sh and toggle).",
@@ -17,15 +17,20 @@ export const skillsCommand = {
17
17
  const enabled = state.enabledSkills.includes(s.name) ? "● " : " ";
18
18
  lines.push(`${enabled}${s.name} — ${truncate(s.description, 80)}`);
19
19
  lines.push(` source: ${s.source}`);
20
+ if (s.integrity !== "ok")
21
+ lines.push(` integrity: ${s.integrity === "drift" ? "DRIFT (disabled; run /skills verify " + s.name + " --confirm)" : "unverified (run /skills verify " + s.name + " --confirm)"}`);
20
22
  }
21
23
  lines.push("");
22
24
  lines.push("Toggle with `/skills enable <name>` or `/skills disable <name>`.");
23
25
  return { message: lines.join("\n") };
24
26
  }
25
27
  if (sub === "search") {
28
+ const registry = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], state.skillsRegistry);
29
+ if (!registry)
30
+ return { message: "No skills registry configured. Set one with `/skills registry <url>` or FASTCODE_SKILLS_REGISTRY." };
26
31
  if (rest.length === 0)
27
32
  return { message: "Usage: /skills search <query>" };
28
- const hits = await searchRegistry(rest.join(" "), state.skillsRegistry);
33
+ const hits = await searchRegistry(rest.join(" "), registry);
29
34
  if (hits.length === 0) {
30
35
  return { message: `No skills found for "${rest.join(" ")}".` };
31
36
  }
@@ -66,6 +71,10 @@ export const skillsCommand = {
66
71
  const skill = getSkill(rest[0]);
67
72
  if (!skill)
68
73
  return { message: `Skill "${rest[0]}" is not installed.` };
74
+ if (skill.integrity !== "ok") {
75
+ const reason = skill.integrity === "drift" ? "content drift detected" : "no integrity baseline";
76
+ return { message: `Skill "${skill.name}" is disabled (${reason}). Run /skills verify ${skill.name} --confirm to re-baseline.` };
77
+ }
69
78
  actions.enableSkill(skill.name);
70
79
  return { message: `Enabled skill "${skill.name}".` };
71
80
  }
@@ -84,9 +93,30 @@ export const skillsCommand = {
84
93
  const head = `# ${skill.name}\n_${skill.description}_\n\nSource: ${skill.source}\n\n---\n\n`;
85
94
  return { message: head + skill.body };
86
95
  }
96
+ if (sub === "verify") {
97
+ const confirm = rest.includes("--confirm") || rest.includes("--yes");
98
+ const all = rest.includes("--all");
99
+ const names = rest.filter((entry) => !entry.startsWith("-"));
100
+ if (!all && names.length === 0)
101
+ return { message: "Usage: /skills verify <name> --confirm (or /skills verify --all --confirm)" };
102
+ if (!confirm) {
103
+ return { message: `Re-baselining ${all ? "all skills" : names.join(", ")} trusts the current files as the new baseline. ` +
104
+ "Re-run the same command with --confirm to proceed." };
105
+ }
106
+ const results = rebaselineSkills(all ? undefined : names);
107
+ if (results.length === 0)
108
+ return { message: "No matching installed skills to verify." };
109
+ const lines = ["Skill integrity re-baselined:"];
110
+ for (const result of results)
111
+ lines.push(result.ok ? ` • ${result.name}: locked` : ` • ${result.name}: FAILED — ${result.warning ?? "unknown error"}`);
112
+ return { message: lines.join("\n") };
113
+ }
87
114
  if (sub === "registry") {
88
115
  if (rest.length === 0) {
89
- return { message: `Skills registry: ${state.skillsRegistry}` };
116
+ const registry = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], state.skillsRegistry);
117
+ return { message: registry
118
+ ? `Skills registry: ${registry}`
119
+ : "No skills registry configured. Set one with `/skills registry <url>` or FASTCODE_SKILLS_REGISTRY." };
90
120
  }
91
121
  // Mutate via AppActions so the in-memory React state stays in sync —
92
122
  // direct saveConfig() would be silently overwritten by the next state
@@ -95,7 +125,7 @@ export const skillsCommand = {
95
125
  return { message: `Skills registry set to ${rest[0]}.` };
96
126
  }
97
127
  return {
98
- message: "Unknown subcommand. Try: list, search, install, uninstall, enable, disable, info, registry.",
128
+ message: "Unknown subcommand. Try: list, search, install, uninstall, enable, disable, info, verify, registry.",
99
129
  };
100
130
  },
101
131
  };
@@ -0,0 +1,13 @@
1
+ export const steerCommand = {
2
+ name: "steer",
3
+ description: "Add guidance to the next request of an active run (/steer <text>)",
4
+ run({ args, state, actions }) {
5
+ const text = args.join(" ").trim();
6
+ if (!text)
7
+ return { message: "Usage: /steer <guidance>" };
8
+ if (!state.busy)
9
+ return { message: "No active run. Just send the guidance as the next message." };
10
+ actions.steer(text);
11
+ return { message: "Steering queued for the next request." };
12
+ },
13
+ };
@@ -0,0 +1,28 @@
1
+ import type { CommandContext } from "../types.js";
2
+ import { DEFAULT_SHELL_POOL_SIZE, MAX_SHELL_POOL_SIZE, shellPoolSize } from "../agent/shell-session.js";
3
+
4
+ export const threadsCommand = {
5
+ name: "threads",
6
+ aliases: ["pool"],
7
+ description: `Show or set resident shell concurrency (default ${DEFAULT_SHELL_POOL_SIZE}, max ${MAX_SHELL_POOL_SIZE}). Usage: /threads [1-${MAX_SHELL_POOL_SIZE}]`,
8
+ run({ args, actions }: CommandContext) {
9
+ const before = shellPoolSize();
10
+ if (args.length === 0) {
11
+ return {
12
+ message: `Shell concurrency: ${before} resident slot(s) (default ${DEFAULT_SHELL_POOL_SIZE}, max ${MAX_SHELL_POOL_SIZE}). ` +
13
+ `Usage: /threads <1-${MAX_SHELL_POOL_SIZE}>.`,
14
+ };
15
+ }
16
+ const requested = Number(args[0]);
17
+ if (!Number.isFinite(requested)) {
18
+ return { message: `Invalid thread count "${args[0]}". Use /threads <1-${MAX_SHELL_POOL_SIZE}>.` };
19
+ }
20
+ actions.setShellPoolSize(requested);
21
+ const applied = shellPoolSize();
22
+ const clamped = applied !== Math.floor(requested) ? " (clamped)" : "";
23
+ return {
24
+ message: `Shell concurrency: ${before} → ${applied}${clamped} (default ${DEFAULT_SHELL_POOL_SIZE}, max ${MAX_SHELL_POOL_SIZE}). ` +
25
+ "Running jobs keep their slots.",
26
+ };
27
+ },
28
+ };
@@ -10,19 +10,20 @@ 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}):`);
13
+ lines.push(`Stored keys (${credentials.length}, provider-wide):`);
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 && c.model === state.model ? "● " : " ";
18
- lines.push(`${marker}${c.provider}::${c.model} ${maskKey(c.key)}`);
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}`);
19
20
  }
20
21
  const legacy = listLegacy();
21
22
  if (legacy.length > 0) {
22
23
  lines.push("");
23
- lines.push("Unbound legacy keys (NOT used — re-bind them to a model):");
24
+ lines.push("Legacy keys (could not be migrated — re-save with /login):");
24
25
  for (const l of legacy)
25
- lines.push(` ${l.provider} ${maskKey(l.key)} → /login ${l.provider} <model> <key>`);
26
+ lines.push(` ${l.provider} ${maskKey(l.key)}`);
26
27
  }
27
28
  const envLines = [];
28
29
  for (const p of PROVIDERS) {
@@ -5,17 +5,16 @@
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 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.
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.
14
12
  */
15
- import { readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
13
+ import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync, statSync } from "node:fs";
16
14
  import { join, dirname } from "node:path";
17
15
  import { getPaths, lockdown, ensureDir } from "./paths.js";
18
16
  import { DEFAULT_THEME } from "./themes.js";
17
+ import { DEFAULT_EGRESS_CONFIG } from "../agent/policy/defaults.js";
19
18
  export const DEFAULT_CONFIG = {
20
19
  providerId: "anthropic",
21
20
  model: "claude-sonnet-5.5",
@@ -27,22 +26,49 @@ export const DEFAULT_CONFIG = {
27
26
  "or unified diffs the user can apply.",
28
27
  enabledSkills: [],
29
28
  enabledPlugins: [],
30
- skillsRegistry: "https://skillshub.wtf",
29
+ skillsRegistry: "",
31
30
  lifetimeCostUsd: 0,
31
+ /**
32
+ * Extra directories the file tools may read and write outside the project
33
+ * root. Relative entries are resolved against the project root; `~/` is
34
+ * expanded against the user's home directory.
35
+ */
36
+ sharedDirs: [],
37
+ /** Maximum agent tool turns before the loop asks the user to continue. */
38
+ maxAgentTurns: 24,
39
+ /** Abort a stream that receives no data for this long (ms). */
40
+ requestIdleTimeoutMs: 60_000,
41
+ /** Approximate character budget before history compaction kicks in. */
42
+ contextBudgetChars: 400_000,
43
+ /** Thinking depth: auto = send nothing, provider default. */
44
+ reasoningEffort: "auto",
45
+ /** Resident shell concurrency: safe default 2, max 6. `/threads` changes it. */
46
+ shellPoolSize: 2,
47
+ /** Cached per-provider/model capabilities (native vs pseudo tool calling). */
48
+ capabilities: {
49
+ toolCalling: {},
50
+ },
51
+ /**
52
+ * Application-layer egress policy. Default is audit-only; set
53
+ * enabled:true + mode:"block" to enforce. See docs/CONFIGURATION.md.
54
+ */
55
+ egress: { ...DEFAULT_EGRESS_CONFIG },
32
56
  };
33
57
  export interface StoredCredential {
34
58
  provider: string;
35
- model: string;
59
+ /** Model the key was originally saved for (display only; keys are provider-wide). */
60
+ model?: string;
36
61
  key: string;
37
62
  addedAt: string;
38
63
  }
39
64
  export interface AuthStore {
40
65
  version: number;
41
- /** Scoped credentials, keyed by `provider::model`. */
66
+ /** Provider-scoped credentials, keyed by provider id. */
42
67
  keys: Record<string, StoredCredential>;
43
- /** Old provider-only keys we refuse to use until the user re-binds them. */
68
+ /** Old provider-only keys that could not be migrated (kept for diagnostics). */
44
69
  legacy: Record<string, string>;
45
70
  }
71
+ export const AUTH_VERSION = 3;
46
72
  export type KeyLookup = {
47
73
  status: "ok";
48
74
  key: string;
@@ -55,74 +81,143 @@ export type KeyLookup = {
55
81
  } | {
56
82
  status: "missing";
57
83
  };
58
- export function loadConfig() {
59
- const { config } = getPaths();
60
- if (!existsSync(config))
61
- return { ...DEFAULT_CONFIG };
84
+ let configCache: { path: string; fingerprint: string; value: any } | undefined;
85
+ function configFingerprint(file: string): string {
62
86
  try {
63
- const raw = JSON.parse(readFileSync(config, "utf8"));
64
- return { ...DEFAULT_CONFIG, ...raw };
87
+ const info = statSync(file);
88
+ // mtime + size are required; ctime adds resilience on filesystems with
89
+ // coarse mtime resolution. stat() is much cheaper than a full read on
90
+ // Termux shared storage.
91
+ return `${info.mtimeMs}:${info.size}:${info.ctimeMs}`;
65
92
  }
66
93
  catch {
67
- return { ...DEFAULT_CONFIG };
94
+ return "missing";
95
+ }
96
+ }
97
+ /** Drop the cached config so the next loadConfig() re-stats and re-reads. */
98
+ export function invalidateConfigCache(): void {
99
+ configCache = undefined;
100
+ }
101
+ export function configCacheStatus(): { cached: boolean; fingerprint?: string } {
102
+ return configCache ? { cached: true, fingerprint: configCache.fingerprint } : { cached: false };
103
+ }
104
+ export function loadConfig() {
105
+ const { config } = getPaths();
106
+ const fingerprint = configFingerprint(config);
107
+ if (configCache && configCache.path === config && configCache.fingerprint === fingerprint)
108
+ return { ...configCache.value };
109
+ let value;
110
+ if (!existsSync(config)) {
111
+ value = { ...DEFAULT_CONFIG };
68
112
  }
113
+ else {
114
+ try {
115
+ const raw = JSON.parse(readFileSync(config, "utf8"));
116
+ value = { ...DEFAULT_CONFIG, ...raw };
117
+ }
118
+ catch {
119
+ value = { ...DEFAULT_CONFIG };
120
+ }
121
+ }
122
+ configCache = { path: config, fingerprint, value };
123
+ return { ...value };
69
124
  }
70
125
  export function saveConfig(cfg) {
71
126
  const { config } = getPaths();
72
127
  atomicWrite(config, JSON.stringify(cfg, null, 2));
128
+ invalidateConfigCache();
73
129
  }
74
- /** Composite scope id for a provider + model credential. */
130
+ /** Composite id used by the legacy v2 format; kept for API compatibility. */
75
131
  export function scopeId(providerId, model) {
76
132
  return `${providerId}::${model}`;
77
133
  }
134
+ /** Pick the most recently saved credential when several legacy scopes map to one provider. */
135
+ function keepNewest(keys, credential) {
136
+ const existing = keys[credential.provider];
137
+ if (!existing) {
138
+ keys[credential.provider] = credential;
139
+ return;
140
+ }
141
+ const previous = Date.parse(existing.addedAt || "") || 0;
142
+ const next = Date.parse(credential.addedAt || "") || 0;
143
+ if (next > previous)
144
+ keys[credential.provider] = credential;
145
+ }
146
+ let migrationChecked = false;
78
147
  export function loadAuth(): AuthStore {
79
148
  const { auth } = getPaths();
80
- const empty: AuthStore = { version: 2, keys: {}, legacy: {} };
149
+ const empty: AuthStore = { version: AUTH_VERSION, keys: {}, legacy: {} };
81
150
  if (!existsSync(auth))
82
151
  return empty;
152
+ let raw: any;
83
153
  try {
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
- }
154
+ raw = JSON.parse(readFileSync(auth, "utf8"));
155
+ }
156
+ catch {
157
+ return empty;
158
+ }
159
+ if (!raw || typeof raw !== "object")
160
+ return empty;
161
+ const keys: Record<string, StoredCredential> = {};
162
+ const legacy: Record<string, string> = {};
163
+ let migrated = false;
164
+ const source = raw.keys && typeof raw.keys === "object" ? raw.keys : {};
165
+ for (const [id, value] of Object.entries<any>(source)) {
166
+ if (value && typeof value === "object" && typeof value.key === "string" && value.key.trim()) {
167
+ const [idProvider, idModel] = id.split("::");
168
+ const provider = typeof value.provider === "string" && value.provider ? value.provider : idProvider || id;
169
+ const model = typeof value.model === "string" ? value.model : idModel || "";
170
+ if (id.includes("::") || raw.version !== AUTH_VERSION)
171
+ migrated = true;
172
+ keepNewest(keys, {
173
+ provider,
174
+ model,
175
+ key: value.key,
176
+ addedAt: typeof value.addedAt === "string" ? value.addedAt : "",
177
+ });
103
178
  }
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;
179
+ else if (typeof value === "string" && value.trim()) {
180
+ // v1: keys were plain provider -> key strings.
181
+ keepNewest(keys, { provider: id, key: value, addedAt: "" });
182
+ migrated = true;
183
+ }
184
+ }
185
+ if (raw.legacy && typeof raw.legacy === "object") {
186
+ for (const [provider, value] of Object.entries<any>(raw.legacy)) {
187
+ if (typeof value === "string" && value.trim()) {
188
+ // Provider-only keys are exactly what v3 wants: migrate instead
189
+ // of leaving them unusable.
190
+ keepNewest(keys, { provider, key: value, addedAt: "" });
191
+ migrated = true;
108
192
  }
109
193
  }
110
- return { version: 2, keys, legacy };
111
194
  }
112
- catch {
113
- return empty;
195
+ if (migrated && !migrationChecked) {
196
+ try {
197
+ // Keep the pre-migration file so a user with several old model
198
+ // keys can recover a credential we had to collapse.
199
+ const backup = auth + ".v2.bak";
200
+ if (!existsSync(backup))
201
+ copyFileSync(auth, backup);
202
+ writeAuth({ version: AUTH_VERSION, keys, legacy });
203
+ }
204
+ catch {
205
+ // Read-only home (or locked file): keep the migrated view in memory.
206
+ }
114
207
  }
208
+ migrationChecked = true;
209
+ return { version: AUTH_VERSION, keys, legacy };
115
210
  }
116
- export function saveAuth(store: AuthStore) {
211
+ function writeAuth(store: AuthStore) {
117
212
  const { auth } = getPaths();
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));
213
+ atomicWrite(auth, JSON.stringify({ version: AUTH_VERSION, keys: store.keys }, null, 2));
122
214
  lockdown(auth);
123
215
  }
124
- export function getCredential(providerId, model): StoredCredential | undefined {
125
- return loadAuth().keys[scopeId(providerId, model)];
216
+ export function saveAuth(store: AuthStore) {
217
+ writeAuth(store);
218
+ }
219
+ export function getCredential(providerId, _model?): StoredCredential | undefined {
220
+ return loadAuth().keys[providerId];
126
221
  }
127
222
  export function listCredentials(): StoredCredential[] {
128
223
  return Object.values(loadAuth().keys);
@@ -136,38 +231,35 @@ export function listLegacy(): Array<{
136
231
  export function getLegacy(providerId): string | undefined {
137
232
  return loadAuth().legacy[providerId];
138
233
  }
139
- /** Save/replace the credential for one provider+model scope (drops the legacy key). */
234
+ /** Save/replace the provider-wide credential (drops any legacy key). */
140
235
  export function setCredential(providerId, model, key): StoredCredential {
141
236
  const store = loadAuth();
142
- const credential = {
237
+ const credential: StoredCredential = {
143
238
  provider: providerId,
144
239
  model,
145
240
  key: key.trim(),
146
241
  addedAt: new Date().toISOString(),
147
242
  };
148
- store.keys[scopeId(providerId, model)] = credential;
243
+ store.keys[providerId] = credential;
149
244
  delete store.legacy[providerId];
150
245
  saveAuth(store);
151
246
  return credential;
152
247
  }
153
- export function removeCredential(providerId, model): boolean {
248
+ export function removeCredential(providerId, _model?): boolean {
154
249
  const store = loadAuth();
155
- const id = scopeId(providerId, model);
156
- if (!store.keys[id])
250
+ if (!store.keys[providerId])
157
251
  return false;
158
- delete store.keys[id];
252
+ delete store.keys[providerId];
159
253
  saveAuth(store);
160
254
  return true;
161
255
  }
162
- /** Remove every credential for a provider (all models) plus its legacy key. */
256
+ /** Remove the provider-wide credential plus any legacy leftovers. */
163
257
  export function removeCredentialsForProvider(providerId): number {
164
258
  const store = loadAuth();
165
259
  let removed = 0;
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
- }
260
+ if (store.keys[providerId]) {
261
+ delete store.keys[providerId];
262
+ removed += 1;
171
263
  }
172
264
  if (store.legacy[providerId]) {
173
265
  delete store.legacy[providerId];
@@ -187,16 +279,16 @@ export function maskKey(key) {
187
279
  return value.slice(0, 3) + "****" + value.slice(-4);
188
280
  }
189
281
  /**
190
- * Resolve the key to use for a specific provider+model:
191
- * 1. the scoped credential in auth.json (exact provider+model match), then
282
+ * Resolve the key to use for a provider:
283
+ * 1. the provider-wide credential in auth.json,
192
284
  * 2. the provider's environment variable (provider-scoped, any model), and
193
- * 3. report an "unbound" legacy key instead of using it silently.
285
+ * 3. a legacy provider-only key that somehow escaped migration.
194
286
  */
195
- export function resolveApiKey(providerId, model, envVars): KeyLookup {
287
+ export function resolveApiKey(providerId, _model, envVars): KeyLookup {
196
288
  const store = loadAuth();
197
- const scoped = store.keys[scopeId(providerId, model)];
198
- if (scoped && scoped.key)
199
- return { status: "ok", key: scoped.key, source: "file" };
289
+ const credential = store.keys[providerId];
290
+ if (credential && credential.key)
291
+ return { status: "ok", key: credential.key, source: "file" };
200
292
  for (const envVar of envVars ?? []) {
201
293
  const value = process.env[envVar];
202
294
  if (value && value.trim())
@@ -213,10 +305,9 @@ export function resolveProviderKey(providerId, envVars): {
213
305
  envVar?: string;
214
306
  } {
215
307
  const store = loadAuth();
216
- for (const credential of Object.values(store.keys)) {
217
- if (credential.provider === providerId && credential.key)
218
- return { key: credential.key, source: "file" };
219
- }
308
+ const credential = store.keys[providerId];
309
+ if (credential && credential.key)
310
+ return { key: credential.key, source: "file" };
220
311
  for (const envVar of envVars ?? []) {
221
312
  const value = process.env[envVar];
222
313
  if (value && value.trim())