@datename/fastcode-termux 0.0.0-stage → 0.1.6-beta

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 (347) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -2
  3. package/bin/fastcode.mjs +59 -0
  4. package/dist/agent/approval.d.ts +50 -0
  5. package/dist/agent/approval.js +7 -0
  6. package/dist/agent/compaction.d.ts +39 -0
  7. package/dist/agent/compaction.js +227 -0
  8. package/dist/agent/context.d.ts +29 -0
  9. package/dist/agent/context.js +62 -0
  10. package/dist/agent/guards.d.ts +43 -0
  11. package/dist/agent/guards.js +269 -0
  12. package/dist/agent/ignore.d.ts +11 -0
  13. package/dist/agent/ignore.js +129 -0
  14. package/dist/agent/limits.d.ts +24 -0
  15. package/dist/agent/limits.js +71 -0
  16. package/dist/agent/process-tree.d.ts +2 -0
  17. package/dist/agent/process-tree.js +31 -0
  18. package/dist/agent/protocol-mode.d.ts +6 -0
  19. package/dist/agent/protocol-mode.js +51 -0
  20. package/dist/agent/protocol.d.ts +4 -0
  21. package/dist/agent/protocol.js +129 -0
  22. package/dist/agent/registry.d.ts +17 -0
  23. package/dist/agent/registry.js +90 -0
  24. package/dist/agent/runner.d.ts +106 -0
  25. package/dist/agent/runner.js +352 -0
  26. package/dist/agent/shell-session.d.ts +37 -0
  27. package/dist/agent/shell-session.js +400 -0
  28. package/dist/agent/stream.d.ts +29 -0
  29. package/dist/agent/stream.js +103 -0
  30. package/dist/agent/tool-schemas.d.ts +2 -0
  31. package/dist/agent/tool-schemas.js +63 -0
  32. package/dist/agent/tools/append_file.d.ts +2 -0
  33. package/dist/agent/tools/append_file.js +26 -0
  34. package/dist/agent/tools/apply_patch.d.ts +10 -0
  35. package/dist/agent/tools/apply_patch.js +52 -0
  36. package/dist/agent/tools/copy_file.d.ts +2 -0
  37. package/dist/agent/tools/copy_file.js +30 -0
  38. package/dist/agent/tools/delete_file.d.ts +2 -0
  39. package/dist/agent/tools/delete_file.js +24 -0
  40. package/dist/agent/tools/grep.d.ts +2 -0
  41. package/dist/agent/tools/grep.js +23 -0
  42. package/dist/agent/tools/index.d.ts +2 -0
  43. package/dist/agent/tools/index.js +32 -0
  44. package/dist/agent/tools/list_files.d.ts +2 -0
  45. package/dist/agent/tools/list_files.js +17 -0
  46. package/dist/agent/tools/make_dir.d.ts +2 -0
  47. package/dist/agent/tools/make_dir.js +24 -0
  48. package/dist/agent/tools/move_path.d.ts +2 -0
  49. package/dist/agent/tools/move_path.js +47 -0
  50. package/dist/agent/tools/paths.d.ts +7 -0
  51. package/dist/agent/tools/paths.js +84 -0
  52. package/dist/agent/tools/pwd.d.ts +2 -0
  53. package/dist/agent/tools/pwd.js +10 -0
  54. package/dist/agent/tools/read_file.d.ts +2 -0
  55. package/dist/agent/tools/read_file.js +21 -0
  56. package/dist/agent/tools/read_many.d.ts +2 -0
  57. package/dist/agent/tools/read_many.js +25 -0
  58. package/dist/agent/tools/readers.d.ts +6 -0
  59. package/dist/agent/tools/readers.js +216 -0
  60. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  61. package/dist/agent/tools/replace_in_file.js +30 -0
  62. package/dist/agent/tools/shell.d.ts +2 -0
  63. package/dist/agent/tools/shell.js +48 -0
  64. package/dist/agent/tools/stat_path.d.ts +2 -0
  65. package/dist/agent/tools/stat_path.js +31 -0
  66. package/dist/agent/tools/types.d.ts +39 -0
  67. package/dist/agent/tools/types.js +1 -0
  68. package/dist/agent/tools/validate.d.ts +6 -0
  69. package/dist/agent/tools/validate.js +29 -0
  70. package/dist/agent/tools/write_file.d.ts +2 -0
  71. package/dist/agent/tools/write_file.js +26 -0
  72. package/dist/agent/tools.d.ts +3 -0
  73. package/dist/agent/tools.js +2 -0
  74. package/dist/app.d.ts +4 -0
  75. package/dist/app.js +708 -0
  76. package/dist/cli.d.ts +1 -0
  77. package/dist/cli.js +569 -0
  78. package/dist/commands/about.d.ts +8 -0
  79. package/dist/commands/about.js +16 -0
  80. package/dist/commands/agent.d.ts +11 -0
  81. package/dist/commands/agent.js +18 -0
  82. package/dist/commands/attach.d.ts +9 -0
  83. package/dist/commands/attach.js +15 -0
  84. package/dist/commands/bg.d.ts +7 -0
  85. package/dist/commands/bg.js +9 -0
  86. package/dist/commands/cancel.d.ts +10 -0
  87. package/dist/commands/cancel.js +10 -0
  88. package/dist/commands/clear.d.ts +11 -0
  89. package/dist/commands/clear.js +9 -0
  90. package/dist/commands/compact.d.ts +10 -0
  91. package/dist/commands/compact.js +22 -0
  92. package/dist/commands/config.d.ts +7 -0
  93. package/dist/commands/config.js +24 -0
  94. package/dist/commands/context.d.ts +7 -0
  95. package/dist/commands/context.js +24 -0
  96. package/dist/commands/cost.d.ts +9 -0
  97. package/dist/commands/cost.js +13 -0
  98. package/dist/commands/cwd.d.ts +7 -0
  99. package/dist/commands/cwd.js +7 -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/exit.d.ts +9 -0
  105. package/dist/commands/exit.js +8 -0
  106. package/dist/commands/expand.d.ts +10 -0
  107. package/dist/commands/expand.js +8 -0
  108. package/dist/commands/help.d.ts +6 -0
  109. package/dist/commands/help.js +18 -0
  110. package/dist/commands/index.d.ts +9 -0
  111. package/dist/commands/index.js +80 -0
  112. package/dist/commands/jobs.d.ts +7 -0
  113. package/dist/commands/jobs.js +16 -0
  114. package/dist/commands/kill.d.ts +7 -0
  115. package/dist/commands/kill.js +11 -0
  116. package/dist/commands/login.d.ts +18 -0
  117. package/dist/commands/login.js +63 -0
  118. package/dist/commands/logout.d.ts +9 -0
  119. package/dist/commands/logout.js +45 -0
  120. package/dist/commands/model.d.ts +24 -0
  121. package/dist/commands/model.js +50 -0
  122. package/dist/commands/plugin.d.ts +11 -0
  123. package/dist/commands/plugin.js +74 -0
  124. package/dist/commands/provider.d.ts +24 -0
  125. package/dist/commands/provider.js +39 -0
  126. package/dist/commands/reset.d.ts +9 -0
  127. package/dist/commands/reset.js +13 -0
  128. package/dist/commands/skills.d.ts +11 -0
  129. package/dist/commands/skills.js +106 -0
  130. package/dist/commands/steer.d.ts +11 -0
  131. package/dist/commands/steer.js +13 -0
  132. package/dist/commands/system.d.ts +11 -0
  133. package/dist/commands/system.js +20 -0
  134. package/dist/commands/theme.d.ts +11 -0
  135. package/dist/commands/theme.js +22 -0
  136. package/dist/commands/tools.d.ts +7 -0
  137. package/dist/commands/tools.js +29 -0
  138. package/dist/commands/whoami.d.ts +9 -0
  139. package/dist/commands/whoami.js +52 -0
  140. package/dist/config/paths.d.ts +4 -0
  141. package/dist/config/paths.js +59 -0
  142. package/dist/config/store.d.ts +89 -0
  143. package/dist/config/store.js +260 -0
  144. package/dist/config/themes.d.ts +73 -0
  145. package/dist/config/themes.js +77 -0
  146. package/dist/config/trust.d.ts +22 -0
  147. package/dist/config/trust.js +153 -0
  148. package/dist/index.d.ts +1 -0
  149. package/dist/index.js +31 -0
  150. package/dist/lib/diagnostics.d.ts +66 -0
  151. package/dist/lib/diagnostics.js +195 -0
  152. package/dist/lib/http.d.ts +23 -0
  153. package/dist/lib/http.js +128 -0
  154. package/dist/lib/markdown.d.ts +1 -0
  155. package/dist/lib/markdown.js +32 -0
  156. package/dist/lib/model-meta.d.ts +19 -0
  157. package/dist/lib/model-meta.js +113 -0
  158. package/dist/lib/native-tools.d.ts +12 -0
  159. package/dist/lib/native-tools.js +21 -0
  160. package/dist/lib/net.d.ts +33 -0
  161. package/dist/lib/net.js +168 -0
  162. package/dist/lib/pkg.d.ts +5 -0
  163. package/dist/lib/pkg.js +27 -0
  164. package/dist/lib/platform.d.ts +7 -0
  165. package/dist/lib/platform.js +57 -0
  166. package/dist/lib/startup.d.ts +6 -0
  167. package/dist/lib/startup.js +25 -0
  168. package/dist/lib/uiMode.d.ts +2 -0
  169. package/dist/lib/uiMode.js +7 -0
  170. package/dist/patch/apply.d.ts +15 -0
  171. package/dist/patch/apply.js +172 -0
  172. package/dist/patch/parse.d.ts +29 -0
  173. package/dist/patch/parse.js +112 -0
  174. package/dist/patch/plan.d.ts +49 -0
  175. package/dist/patch/plan.js +273 -0
  176. package/dist/plugins/manager.d.ts +23 -0
  177. package/dist/plugins/manager.js +101 -0
  178. package/dist/providers/anthropic.d.ts +22 -0
  179. package/dist/providers/anthropic.js +119 -0
  180. package/dist/providers/custom.d.ts +33 -0
  181. package/dist/providers/custom.js +25 -0
  182. package/dist/providers/deepseek.d.ts +34 -0
  183. package/dist/providers/deepseek.js +16 -0
  184. package/dist/providers/google.d.ts +33 -0
  185. package/dist/providers/google.js +19 -0
  186. package/dist/providers/groq.d.ts +34 -0
  187. package/dist/providers/groq.js +21 -0
  188. package/dist/providers/index.d.ts +10 -0
  189. package/dist/providers/index.js +67 -0
  190. package/dist/providers/mistral.d.ts +34 -0
  191. package/dist/providers/mistral.js +20 -0
  192. package/dist/providers/moonshot.d.ts +34 -0
  193. package/dist/providers/moonshot.js +23 -0
  194. package/dist/providers/ollama.d.ts +33 -0
  195. package/dist/providers/ollama.js +22 -0
  196. package/dist/providers/openai.d.ts +34 -0
  197. package/dist/providers/openai.js +26 -0
  198. package/dist/providers/openaiCompatible.d.ts +26 -0
  199. package/dist/providers/openaiCompatible.js +158 -0
  200. package/dist/providers/openrouter.d.ts +34 -0
  201. package/dist/providers/openrouter.js +27 -0
  202. package/dist/providers/qwen.d.ts +34 -0
  203. package/dist/providers/qwen.js +24 -0
  204. package/dist/providers/xai.d.ts +34 -0
  205. package/dist/providers/xai.js +19 -0
  206. package/dist/providers/zhipu.d.ts +34 -0
  207. package/dist/providers/zhipu.js +24 -0
  208. package/dist/session/log.d.ts +52 -0
  209. package/dist/session/log.js +331 -0
  210. package/dist/session/projection.d.ts +43 -0
  211. package/dist/session/projection.js +257 -0
  212. package/dist/skills/manager.d.ts +18 -0
  213. package/dist/skills/manager.js +201 -0
  214. package/dist/skills/parser.d.ts +18 -0
  215. package/dist/skills/parser.js +65 -0
  216. package/dist/types.d.ts +205 -0
  217. package/dist/types.js +1 -0
  218. package/dist/ui/Banner.d.ts +5 -0
  219. package/dist/ui/Banner.js +65 -0
  220. package/dist/ui/Message.d.ts +1 -0
  221. package/dist/ui/Message.js +24 -0
  222. package/dist/ui/Picker.d.ts +8 -0
  223. package/dist/ui/Picker.js +24 -0
  224. package/dist/ui/Prompt.d.ts +9 -0
  225. package/dist/ui/Prompt.js +38 -0
  226. package/dist/ui/Spinner.d.ts +5 -0
  227. package/dist/ui/Spinner.js +39 -0
  228. package/dist/ui/StatusBar.d.ts +11 -0
  229. package/dist/ui/StatusBar.js +20 -0
  230. package/dist/ui/TrustGate.d.ts +5 -0
  231. package/dist/ui/TrustGate.js +26 -0
  232. package/package.json +96 -4
  233. package/src/agent/approval.ts +47 -0
  234. package/src/agent/compaction.ts +276 -0
  235. package/src/agent/context.ts +97 -0
  236. package/src/agent/guards.ts +270 -0
  237. package/src/agent/ignore.ts +147 -0
  238. package/src/agent/limits.ts +71 -0
  239. package/src/agent/process-tree.ts +32 -0
  240. package/src/agent/protocol-mode.ts +59 -0
  241. package/src/agent/protocol.ts +129 -0
  242. package/src/agent/registry.ts +98 -0
  243. package/src/agent/runner.ts +443 -0
  244. package/src/agent/shell-session.ts +449 -0
  245. package/src/agent/stream.ts +143 -0
  246. package/src/agent/tool-schemas.ts +67 -0
  247. package/src/agent/tools/append_file.ts +28 -0
  248. package/src/agent/tools/apply_patch.ts +54 -0
  249. package/src/agent/tools/copy_file.ts +32 -0
  250. package/src/agent/tools/delete_file.ts +26 -0
  251. package/src/agent/tools/grep.ts +25 -0
  252. package/src/agent/tools/index.ts +34 -0
  253. package/src/agent/tools/list_files.ts +19 -0
  254. package/src/agent/tools/make_dir.ts +26 -0
  255. package/src/agent/tools/move_path.ts +50 -0
  256. package/src/agent/tools/paths.ts +92 -0
  257. package/src/agent/tools/pwd.ts +12 -0
  258. package/src/agent/tools/read_file.ts +23 -0
  259. package/src/agent/tools/read_many.ts +27 -0
  260. package/src/agent/tools/readers.ts +228 -0
  261. package/src/agent/tools/replace_in_file.ts +32 -0
  262. package/src/agent/tools/shell.ts +53 -0
  263. package/src/agent/tools/stat_path.ts +33 -0
  264. package/src/agent/tools/types.ts +42 -0
  265. package/src/agent/tools/validate.ts +35 -0
  266. package/src/agent/tools/write_file.ts +28 -0
  267. package/src/agent/tools.ts +3 -0
  268. package/src/app.ts +708 -0
  269. package/src/cli.ts +335 -0
  270. package/src/commands/about.ts +16 -0
  271. package/src/commands/agent.ts +18 -0
  272. package/src/commands/attach.ts +16 -0
  273. package/src/commands/bg.ts +9 -0
  274. package/src/commands/cancel.ts +10 -0
  275. package/src/commands/clear.ts +9 -0
  276. package/src/commands/compact.ts +23 -0
  277. package/src/commands/config.ts +24 -0
  278. package/src/commands/context.ts +25 -0
  279. package/src/commands/cost.ts +13 -0
  280. package/src/commands/cwd.ts +7 -0
  281. package/src/commands/edit.ts +37 -0
  282. package/src/commands/effort.ts +49 -0
  283. package/src/commands/exit.ts +8 -0
  284. package/src/commands/expand.ts +8 -0
  285. package/src/commands/help.ts +18 -0
  286. package/src/commands/index.ts +80 -0
  287. package/src/commands/jobs.ts +17 -0
  288. package/src/commands/kill.ts +12 -0
  289. package/src/commands/login.ts +63 -0
  290. package/src/commands/logout.ts +45 -0
  291. package/src/commands/model.ts +50 -0
  292. package/src/commands/plugin.ts +74 -0
  293. package/src/commands/provider.ts +39 -0
  294. package/src/commands/reset.ts +13 -0
  295. package/src/commands/skills.ts +106 -0
  296. package/src/commands/steer.ts +13 -0
  297. package/src/commands/system.ts +20 -0
  298. package/src/commands/theme.ts +22 -0
  299. package/src/commands/tools.ts +29 -0
  300. package/src/commands/whoami.ts +52 -0
  301. package/src/config/paths.ts +59 -0
  302. package/src/config/store.ts +293 -0
  303. package/src/config/themes.ts +77 -0
  304. package/src/config/trust.ts +183 -0
  305. package/src/index.ts +31 -0
  306. package/src/lib/diagnostics.ts +195 -0
  307. package/src/lib/http.ts +132 -0
  308. package/src/lib/markdown.ts +32 -0
  309. package/src/lib/model-meta.ts +138 -0
  310. package/src/lib/native-tools.ts +32 -0
  311. package/src/lib/net.ts +204 -0
  312. package/src/lib/pkg.ts +35 -0
  313. package/src/lib/platform.ts +57 -0
  314. package/src/lib/startup.ts +25 -0
  315. package/src/lib/uiMode.ts +7 -0
  316. package/src/patch/apply.ts +194 -0
  317. package/src/patch/parse.ts +131 -0
  318. package/src/patch/plan.ts +317 -0
  319. package/src/plugins/manager.ts +101 -0
  320. package/src/providers/anthropic.ts +119 -0
  321. package/src/providers/custom.ts +25 -0
  322. package/src/providers/deepseek.ts +16 -0
  323. package/src/providers/google.ts +19 -0
  324. package/src/providers/groq.ts +21 -0
  325. package/src/providers/index.ts +68 -0
  326. package/src/providers/mistral.ts +20 -0
  327. package/src/providers/moonshot.ts +23 -0
  328. package/src/providers/ollama.ts +22 -0
  329. package/src/providers/openai.ts +26 -0
  330. package/src/providers/openaiCompatible.ts +158 -0
  331. package/src/providers/openrouter.ts +27 -0
  332. package/src/providers/qwen.ts +24 -0
  333. package/src/providers/xai.ts +19 -0
  334. package/src/providers/zhipu.ts +24 -0
  335. package/src/session/log.ts +374 -0
  336. package/src/session/projection.ts +308 -0
  337. package/src/skills/manager.ts +201 -0
  338. package/src/skills/parser.ts +65 -0
  339. package/src/types.ts +205 -0
  340. package/src/ui/Banner.ts +68 -0
  341. package/src/ui/Message.ts +24 -0
  342. package/src/ui/Picker.ts +24 -0
  343. package/src/ui/Prompt.ts +38 -0
  344. package/src/ui/Spinner.ts +39 -0
  345. package/src/ui/StatusBar.ts +20 -0
  346. package/src/ui/TrustGate.ts +26 -0
  347. package/tsconfig.json +22 -0
@@ -0,0 +1,317 @@
1
+ import { closeSync, constants as fsConstants, lstatSync, openSync, readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { sandboxPath } from "../agent/tools/paths.js";
4
+ import { parsePatch, type PatchHunk, type PatchOp } from "./parse.js";
5
+
6
+ export interface PlannedFile {
7
+ kind: "add" | "delete" | "update";
8
+ relPath: string;
9
+ path: string;
10
+ moveRel?: string;
11
+ moveTo?: string;
12
+ existed: boolean;
13
+ origMode: number;
14
+ eol: "\n" | "\r\n";
15
+ oldContent?: string;
16
+ newContent?: string;
17
+ diff: string;
18
+ changedLines: number;
19
+ }
20
+
21
+ export interface PatchPlan {
22
+ files: PlannedFile[];
23
+ warnings: string[];
24
+ }
25
+
26
+ export type PlanResult = { ok: true; plan: PatchPlan } | { ok: false; output: string };
27
+
28
+ export type PatchReview =
29
+ | { kind: "allow" }
30
+ | { kind: "deny"; reason: string }
31
+ | { kind: "pending"; reason: string; changedLines: number };
32
+
33
+ export function planPatch(patch: string, cwd: string): PlanResult {
34
+ const parsed = parsePatch(patch);
35
+ if (parsed.ok === false)
36
+ return { ok: false, output: parsed.output };
37
+ const files: PlannedFile[] = [];
38
+ const warnings: string[] = [];
39
+ for (const op of parsed.ops) {
40
+ const planned = planOp(op, cwd, warnings);
41
+ if (planned.ok === false)
42
+ return { ok: false, output: planned.output };
43
+ files.push(planned.file);
44
+ }
45
+ return { ok: true, plan: { files, warnings } };
46
+ }
47
+
48
+ function planOp(op: PatchOp, cwd: string, warnings: string[]): { ok: true; file: PlannedFile } | { ok: false; output: string } {
49
+ try {
50
+ const path = sandboxPath(cwd, op.path);
51
+ if (op.kind === "add") {
52
+ if (lexists(path))
53
+ return { ok: false, output: `Add File target already exists: ${op.path}` };
54
+ const newContent = op.lines.length > 0 ? op.lines.join("\n") + "\n" : "";
55
+ return {
56
+ ok: true,
57
+ file: {
58
+ kind: "add",
59
+ relPath: op.path,
60
+ path,
61
+ existed: false,
62
+ origMode: 0o644,
63
+ eol: "\n",
64
+ newContent,
65
+ diff: makeDiff(null, op.path, "", newContent, "add"),
66
+ changedLines: newContent.split("\n").filter(Boolean).length,
67
+ },
68
+ };
69
+ }
70
+ if (!lexists(path))
71
+ return { ok: false, output: `file not found: ${op.path}` };
72
+ const info = lstatSync(path);
73
+ if (info.isSymbolicLink())
74
+ return { ok: false, output: `refusing to patch a symbolic link: ${op.path}` };
75
+ if (!info.isFile())
76
+ return { ok: false, output: `not a regular file: ${op.path}` };
77
+ const oldContent = readNoFollow(path);
78
+ if (op.kind === "delete") {
79
+ return {
80
+ ok: true,
81
+ file: {
82
+ kind: "delete",
83
+ relPath: op.path,
84
+ path,
85
+ existed: true,
86
+ origMode: info.mode & 0o777,
87
+ eol: detectEol(oldContent),
88
+ oldContent,
89
+ diff: makeDiff(op.path, null, oldContent, "", "delete"),
90
+ changedLines: oldContent.split(/\r?\n/).length,
91
+ },
92
+ };
93
+ }
94
+ const eol = detectEol(oldContent);
95
+ if (eol === "\n" && /\r\n/.test(oldContent))
96
+ warnings.push(`mixed line endings in ${op.path}; writing LF`);
97
+ let newContent = oldContent;
98
+ if (op.hunks.length > 0) {
99
+ const applied = applyHunks(oldContent, op.hunks);
100
+ if (applied.ok === false)
101
+ return applied;
102
+ newContent = applied.content;
103
+ if (eol === "\r\n")
104
+ newContent = newContent.replace(/\n/g, "\r\n");
105
+ }
106
+ let moveTo: string | undefined;
107
+ let moveRel: string | undefined;
108
+ if (op.moveTo) {
109
+ moveTo = sandboxPath(cwd, op.moveTo);
110
+ if (lexists(moveTo))
111
+ return { ok: false, output: `Move target already exists: ${op.moveTo}` };
112
+ moveRel = op.moveTo;
113
+ }
114
+ const diff = makeDiff(op.path, op.moveTo ?? op.path, oldContent, newContent, "update");
115
+ return {
116
+ ok: true,
117
+ file: {
118
+ kind: "update",
119
+ relPath: op.path,
120
+ path,
121
+ moveRel,
122
+ moveTo,
123
+ existed: true,
124
+ origMode: info.mode & 0o777,
125
+ eol,
126
+ oldContent,
127
+ newContent,
128
+ diff,
129
+ changedLines: countChangedLines(oldContent, newContent),
130
+ },
131
+ };
132
+ }
133
+ catch (err: any) {
134
+ return { ok: false, output: err?.message ?? String(err) };
135
+ }
136
+ }
137
+
138
+ export function reviewPatchPlan(plan: PatchPlan): PatchReview {
139
+ let changed = 0;
140
+ for (const file of plan.files) {
141
+ changed += file.changedLines;
142
+ if (isHardDenied(file.path))
143
+ return { kind: "deny", reason: `patch targets a protected path: ${file.relPath}` };
144
+ if (isSensitivePath(file.path))
145
+ return { kind: "pending", reason: `patch targets a sensitive path: ${file.relPath}`, changedLines: changed };
146
+ }
147
+ if (changed > 200)
148
+ return { kind: "pending", reason: `patch changes ${changed} lines (limit 200)`, changedLines: changed };
149
+ return { kind: "allow" };
150
+ }
151
+
152
+ function isHardDenied(path: string): boolean {
153
+ const normalized = path.replace(/\\/g, "/").toLowerCase();
154
+ const segments = normalized.split("/");
155
+ if (segments.includes(".ssh") || segments.includes(".gnupg"))
156
+ return true;
157
+ if (normalized.includes("/.git/hooks/"))
158
+ return true;
159
+ if (normalized.includes("/android/data/"))
160
+ return true;
161
+ const name = basename(normalized);
162
+ return name === "auth.json" || name === "trust.json";
163
+ }
164
+
165
+ function isSensitivePath(path: string): boolean {
166
+ const normalized = path.replace(/\\/g, "/").toLowerCase();
167
+ const name = basename(normalized);
168
+ return name === ".env" || name.startsWith(".env.") || /[.](pem|key|p12|pfx)$/.test(name) || normalized.includes("/.git/hooks/");
169
+ }
170
+
171
+ function lexists(path: string): boolean {
172
+ try {
173
+ lstatSync(path);
174
+ return true;
175
+ }
176
+ catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function readNoFollow(path: string): string {
182
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0);
183
+ const fd = openSync(path, flags);
184
+ try {
185
+ return readFileSync(fd, "utf8");
186
+ }
187
+ finally {
188
+ closeSync(fd);
189
+ }
190
+ }
191
+
192
+ export function detectEol(content: string): "\n" | "\r\n" {
193
+ const crlf = (content.match(/\r\n/g) ?? []).length;
194
+ const lf = (content.match(/\n/g) ?? []).length;
195
+ return crlf > 0 && crlf === lf ? "\r\n" : "\n";
196
+ }
197
+
198
+ type ApplyHunksResult = { ok: true; content: string } | { ok: false; output: string };
199
+
200
+ export function applyHunks(oldContent: string, hunks: PatchHunk[]): ApplyHunksResult {
201
+ const lines = oldContent.split(/\r?\n/);
202
+ let changed = false;
203
+ for (const hunk of hunks) {
204
+ const needle = hunk.lines.filter((line) => line.type !== "+").map((line) => line.text);
205
+ const replacement = hunk.lines.filter((line) => line.type !== "-").map((line) => line.text);
206
+ if (needle.length === 0) {
207
+ const at = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
208
+ lines.splice(at, 0, ...replacement);
209
+ changed = true;
210
+ continue;
211
+ }
212
+ const verdict = findHunk(lines, needle);
213
+ if (verdict.ok === false)
214
+ return verdict;
215
+ lines.splice(verdict.index, needle.length, ...replacement);
216
+ changed = true;
217
+ }
218
+ if (!changed)
219
+ return { ok: false, output: "patch made no changes" };
220
+ return { ok: true, content: lines.join("\n") };
221
+ }
222
+
223
+ function findHunk(lines: string[], needle: string[]): { ok: true; index: number } | { ok: false; output: string } {
224
+ for (let level = 0; level < 3; level += 1) {
225
+ const matches = matchAll(lines, needle, level);
226
+ if (matches.length === 0)
227
+ continue;
228
+ if (matches.length > 1) {
229
+ return {
230
+ ok: false,
231
+ output: `ambiguous patch context (${matches.length} candidates at lines ${matches.map((index) => index + 1).join(", ")}); add surrounding context`,
232
+ };
233
+ }
234
+ return { ok: true, index: matches[0] };
235
+ }
236
+ return { ok: false, output: `patch context not found: ${JSON.stringify(needle.slice(0, 4))}` };
237
+ }
238
+
239
+ function matchAll(lines: string[], needle: string[], level: number): number[] {
240
+ const found: number[] = [];
241
+ const wanted = needle.map((line) => normalizeLine(line, level));
242
+ for (let i = 0; i + needle.length <= lines.length; i += 1) {
243
+ let match = true;
244
+ for (let j = 0; j < needle.length; j += 1) {
245
+ if (normalizeLine(lines[i + j], level) !== wanted[j]) {
246
+ match = false;
247
+ break;
248
+ }
249
+ }
250
+ if (match)
251
+ found.push(i);
252
+ }
253
+ return found;
254
+ }
255
+
256
+ function normalizeLine(line: string, level: number): string {
257
+ if (level === 0)
258
+ return line;
259
+ if (level === 1)
260
+ return line.replace(/\s+$/, "");
261
+ return line.replace(/\s+/g, " ").trim();
262
+ }
263
+
264
+ export function makeDiff(oldPath: string | null, newPath: string | null, oldContent: string, newContent: string, kind: "add" | "delete" | "update"): string {
265
+ const oldLines = splitDiffLines(oldContent);
266
+ const newLines = splitDiffLines(newContent);
267
+ let prefix = 0;
268
+ while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix])
269
+ prefix += 1;
270
+ let suffix = 0;
271
+ while (suffix < oldLines.length - prefix && suffix < newLines.length - prefix &&
272
+ oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix])
273
+ suffix += 1;
274
+ const oldMiddle = oldLines.slice(prefix, oldLines.length - suffix);
275
+ const newMiddle = newLines.slice(prefix, newLines.length - suffix);
276
+ const oldStart = prefix + 1;
277
+ const newStart = prefix + 1;
278
+ const header = kind === "add"
279
+ ? `--- /dev/null\n+++ b/${newPath ?? ""}`
280
+ : kind === "delete"
281
+ ? `--- a/${oldPath ?? ""}\n+++ /dev/null`
282
+ : `--- a/${oldPath ?? ""}\n+++ b/${newPath ?? oldPath ?? ""}`;
283
+ const body = [
284
+ `@@ -${oldStart},${oldMiddle.length} +${newStart},${newMiddle.length} @@`,
285
+ ...oldMiddle.map((line) => "-" + line),
286
+ ...newMiddle.map((line) => "+" + line),
287
+ ];
288
+ return [header, ...body].join("\n");
289
+ }
290
+
291
+ function splitDiffLines(content: string): string[] {
292
+ if (content === "")
293
+ return [];
294
+ const lines = content.split(/\r?\n/);
295
+ if (lines.length > 0 && lines[lines.length - 1] === "")
296
+ lines.pop();
297
+ return lines;
298
+ }
299
+
300
+ function countChangedLines(oldContent: string, newContent: string): number {
301
+ const oldLines = oldContent.split(/\r?\n/);
302
+ const newLines = newContent.split(/\r?\n/);
303
+ let changed = 0;
304
+ let i = 0;
305
+ let j = 0;
306
+ while (i < oldLines.length && j < newLines.length) {
307
+ if (oldLines[i] === newLines[j]) {
308
+ i += 1;
309
+ j += 1;
310
+ continue;
311
+ }
312
+ changed += 1;
313
+ i += 1;
314
+ j += 1;
315
+ }
316
+ return changed + Math.abs(oldLines.length - newLines.length);
317
+ }
@@ -0,0 +1,101 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { getPaths } from "../config/paths.js";
4
+ export function listInstalledPlugins() {
5
+ const { plugins } = getPaths();
6
+ if (!existsSync(plugins))
7
+ return [];
8
+ return readdirSync(plugins, { withFileTypes: true })
9
+ .filter((entry) => entry.isDirectory())
10
+ .map((entry) => loadPlugin(join(plugins, entry.name), entry.name))
11
+ .filter((plugin) => plugin !== undefined)
12
+ .sort((a, b) => a.name.localeCompare(b.name));
13
+ }
14
+ export function getPlugin(name) {
15
+ return listInstalledPlugins().find((p) => p.name === name || basename(p.path) === name);
16
+ }
17
+ export function importPlugin(source, name) {
18
+ const { plugins } = getPaths();
19
+ const src = resolve(source);
20
+ if (!existsSync(src))
21
+ throw new Error(`Plugin source does not exist: ${source}`);
22
+ if (!statSync(src).isDirectory())
23
+ throw new Error("Plugin source must be a directory");
24
+ const folder = slugify(name ?? basename(src));
25
+ const target = join(plugins, folder);
26
+ if (existsSync(target))
27
+ rmSync(target, { recursive: true, force: true });
28
+ mkdirSync(target, { recursive: true });
29
+ cpSync(src, target, { recursive: true });
30
+ writeFileSync(join(target, ".fastcode-source"), src);
31
+ const loaded = loadPlugin(target, folder);
32
+ if (!loaded)
33
+ throw new Error("Plugin must include PLUGIN.md, README.md, or at least one SKILL.md");
34
+ return loaded;
35
+ }
36
+ export function removePlugin(name) {
37
+ const plugin = getPlugin(name);
38
+ if (!plugin)
39
+ return false;
40
+ rmSync(plugin.path, { recursive: true, force: true });
41
+ return true;
42
+ }
43
+ export function buildPluginsPrompt(enabled) {
44
+ if (enabled.length === 0)
45
+ return "";
46
+ const plugins = enabled.map(getPlugin).filter((p) => p !== undefined);
47
+ if (plugins.length === 0)
48
+ return "";
49
+ const chunks = plugins.map((plugin) => {
50
+ const skills = plugin.skills
51
+ .map((skill) => `<plugin-skill name="${skill.name}">\n${skill.body.trim()}\n</plugin-skill>`)
52
+ .join("\n\n");
53
+ return `<plugin name="${plugin.name}">\n${plugin.body.trim()}\n${skills ? `\n\n${skills}` : ""}\n</plugin>`;
54
+ });
55
+ return [
56
+ "",
57
+ "## Active plugins",
58
+ "The following plugins are always-active behavior packs. Follow them when relevant:",
59
+ "",
60
+ chunks.join("\n\n"),
61
+ ].join("\n");
62
+ }
63
+ function loadPlugin(path, fallbackName) {
64
+ const pluginMd = firstExisting([join(path, "PLUGIN.md"), join(path, "README.md")]);
65
+ const skillsDir = join(path, "skills");
66
+ const rootSkill = join(path, "SKILL.md");
67
+ const skills = [];
68
+ if (existsSync(rootSkill))
69
+ skills.push({ name: "root", body: readFileSync(rootSkill, "utf8") });
70
+ if (existsSync(skillsDir)) {
71
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
72
+ if (!entry.isDirectory())
73
+ continue;
74
+ const skillPath = join(skillsDir, entry.name, "SKILL.md");
75
+ if (existsSync(skillPath))
76
+ skills.push({ name: entry.name, body: readFileSync(skillPath, "utf8") });
77
+ }
78
+ }
79
+ if (!pluginMd && skills.length === 0)
80
+ return undefined;
81
+ const body = pluginMd ? readFileSync(pluginMd, "utf8") : `Plugin ${fallbackName}`;
82
+ return {
83
+ name: fallbackName,
84
+ description: firstParagraph(body) || `${fallbackName} plugin`,
85
+ path,
86
+ body,
87
+ skills,
88
+ };
89
+ }
90
+ function firstExisting(paths) {
91
+ return paths.find((path) => existsSync(path));
92
+ }
93
+ function firstParagraph(markdown) {
94
+ return markdown
95
+ .split(/\n\s*\n/)
96
+ .map((part) => part.replace(/^#+\s*/, "").trim())
97
+ .find(Boolean);
98
+ }
99
+ function slugify(value) {
100
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "plugin";
101
+ }
@@ -0,0 +1,119 @@
1
+ import { streamSSE } from "../lib/http.js";
2
+ const STATIC_MODELS = [
3
+ { id: "claude-sonnet-5.5", label: "Claude Sonnet 5.5", contextWindow: 1000000, pricing: { inputUsdPerM: 3, outputUsdPerM: 15 } },
4
+ { id: "claude-opus-5.5", label: "Claude Opus 5.5", contextWindow: 1000000, pricing: { inputUsdPerM: 15, outputUsdPerM: 75 } },
5
+ { id: "claude-haiku-5.5", label: "Claude Haiku 5.5", contextWindow: 200000, pricing: { inputUsdPerM: 0.8, outputUsdPerM: 4 } },
6
+ { id: "claude-sonnet-5", label: "Claude Sonnet 5" },
7
+ { id: "claude-opus-5", label: "Claude Opus 5" },
8
+ { id: "claude-sonnet-4.6", label: "Claude Sonnet 4.6" },
9
+ { id: "claude-opus-4.6", label: "Claude Opus 4.6" },
10
+ { id: "claude-haiku-4.5", label: "Claude Haiku 4.5" },
11
+ ];
12
+ const BASE = "https://api.anthropic.com/v1";
13
+ const THINKING_BUDGETS = { low: 1024, medium: 4096, high: 8192, max: 16000 };
14
+ export const anthropicProvider = {
15
+ id: "anthropic",
16
+ label: "Anthropic (Claude)",
17
+ authEnv: ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY"],
18
+ baseUrl: BASE,
19
+ defaultModel: "claude-sonnet-5.5",
20
+ supportsReasoning() {
21
+ return true;
22
+ },
23
+ async listModels(apiKey) {
24
+ // Query the live model list; fall back to the built-in list offline.
25
+ if (apiKey) {
26
+ try {
27
+ const res = await fetch(BASE + "/models", {
28
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
29
+ });
30
+ if (res.ok) {
31
+ const json: any = await res.json();
32
+ const ids = (json.data ?? [])
33
+ .map((m) => m.id)
34
+ .filter((id) => typeof id === "string" && id.length > 0);
35
+ if (ids.length > 0) {
36
+ const known = new Map(STATIC_MODELS.map((m) => [m.id, m]));
37
+ return ids.map((id) => known.get(id) ?? { id });
38
+ }
39
+ }
40
+ }
41
+ catch {
42
+ // network/auth issue → fall through to the static list
43
+ }
44
+ }
45
+ return STATIC_MODELS;
46
+ },
47
+ async *stream(req, apiKey) {
48
+ if (!apiKey) {
49
+ throw new Error("Missing ANTHROPIC_API_KEY. Bind one with /login anthropic <model> <key>, or set the env var.");
50
+ }
51
+ const messages = req.messages.map((m) => ({ role: m.role, content: m.content }));
52
+ const body: any = {
53
+ model: req.model,
54
+ messages,
55
+ ...(req.system ? { system: req.system } : {}),
56
+ stream: true,
57
+ max_tokens: req.maxOutputTokens ?? 4096,
58
+ ...(req.temperature !== undefined ? { temperature: req.temperature } : {}),
59
+ };
60
+ if (req.reasoningEffort && req.reasoningEffort !== "auto") {
61
+ const budget = THINKING_BUDGETS[req.reasoningEffort] ?? THINKING_BUDGETS.medium;
62
+ body.thinking = { type: "enabled", budget_tokens: budget };
63
+ // max_tokens must exceed the thinking budget.
64
+ body.max_tokens = Math.max(body.max_tokens, budget + 1024);
65
+ // Extended thinking is incompatible with a custom temperature.
66
+ delete body.temperature;
67
+ }
68
+ const res = await fetch(BASE + "/messages", {
69
+ method: "POST",
70
+ headers: {
71
+ "content-type": "application/json",
72
+ "x-api-key": apiKey,
73
+ "anthropic-version": "2023-06-01",
74
+ accept: "text/event-stream",
75
+ },
76
+ body: JSON.stringify(body),
77
+ signal: req.signal,
78
+ });
79
+ let usageInput = 0;
80
+ let usageOutput = 0;
81
+ for await (const ev of streamSSE(res)) {
82
+ let data;
83
+ try {
84
+ data = JSON.parse(ev.data);
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ const t = data.type;
90
+ if (t === "content_block_delta") {
91
+ const delta = data.delta?.text;
92
+ if (delta)
93
+ yield { delta, done: false };
94
+ }
95
+ else if (t === "message_start") {
96
+ const u = data.message?.usage;
97
+ if (u?.input_tokens)
98
+ usageInput = u.input_tokens;
99
+ }
100
+ else if (t === "message_delta") {
101
+ const u = data.usage;
102
+ if (u?.output_tokens)
103
+ usageOutput = u.output_tokens;
104
+ }
105
+ else if (t === "message_stop") {
106
+ break;
107
+ }
108
+ }
109
+ const cost = priceFor(req.model, usageInput, usageOutput);
110
+ yield { delta: "", done: true, usage: { inputTokens: usageInput, outputTokens: usageOutput, costUsd: cost } };
111
+ },
112
+ };
113
+ function priceFor(modelId, input, output) {
114
+ const m = STATIC_MODELS.find((x) => x.id === modelId);
115
+ const p = m?.pricing;
116
+ if (!p?.inputUsdPerM || !p.outputUsdPerM)
117
+ return undefined;
118
+ return (input / 1000000) * p.inputUsdPerM + (output / 1000000) * p.outputUsdPerM;
119
+ }
@@ -0,0 +1,25 @@
1
+ import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
2
+ function baseUrl() {
3
+ return (process.env["FASTCODE_BASE_URL"] ?? "").trim().replace(/[/]+$/, "");
4
+ }
5
+ function defaultModel() {
6
+ return (process.env["FASTCODE_MODEL"] ?? "").trim() || "gpt-4o-mini";
7
+ }
8
+ function requireBaseUrl() {
9
+ const base = baseUrl();
10
+ if (!base) {
11
+ throw new Error("FASTCODE_BASE_URL is not set. Example: export FASTCODE_BASE_URL=https://api.example.com/v1");
12
+ }
13
+ return base;
14
+ }
15
+ export const customProvider = {
16
+ id: "custom",
17
+ label: "Custom (OpenAI-compatible)",
18
+ authEnv: [],
19
+ get baseUrl() {
20
+ return baseUrl();
21
+ },
22
+ defaultModel: defaultModel(),
23
+ listModels: () => listModelsHttp({ baseUrl: requireBaseUrl(), apiKey: process.env["FASTCODE_API_KEY"], staticModels: [] }),
24
+ stream: (req) => streamChatCompletion(req, { baseUrl: requireBaseUrl(), apiKey: process.env["FASTCODE_API_KEY"], staticModels: [] }),
25
+ };
@@ -0,0 +1,16 @@
1
+ import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
2
+ const BASE = "https://api.deepseek.com/v1";
3
+ const STATIC = [
4
+ { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
5
+ { id: "deepseek-flash", label: "DeepSeek Flash" },
6
+ ];
7
+ export const deepseekProvider = {
8
+ id: "deepseek",
9
+ label: "DeepSeek",
10
+ authEnv: ["DEEPSEEK_API_KEY"],
11
+ baseUrl: BASE,
12
+ defaultModel: "deepseek-v4-pro",
13
+ listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
14
+ stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, reasoning: "openai" }),
15
+ supportsReasoning: () => true,
16
+ };
@@ -0,0 +1,19 @@
1
+ import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
2
+ const BASE = "https://generativelanguage.googleapis.com/v1beta/openai";
3
+ const STATIC = [
4
+ { id: "gemini-3.8-flash", label: "Gemini 3.8 Flash" },
5
+ { id: "gemini-3.7-flash", label: "Gemini 3.7 Flash" },
6
+ { id: "gemini-3.5-flash", label: "Gemini 3.5 Flash" },
7
+ { id: "gemini-3.5-flash-lite", label: "Gemini 3.5 Flash Lite" },
8
+ { id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite" },
9
+ { id: "gemini-4-argon", label: "Gemini 4 Argon" },
10
+ ];
11
+ export const googleProvider = {
12
+ id: "google",
13
+ label: "Google Gemini",
14
+ authEnv: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
15
+ baseUrl: BASE,
16
+ defaultModel: "gemini-3.8-flash",
17
+ listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
18
+ stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
19
+ };
@@ -0,0 +1,21 @@
1
+ import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
2
+ const BASE = "https://api.groq.com/openai/v1";
3
+ const STATIC = [
4
+ { id: "llama-3.3-70b-versatile", label: "Llama 3.3 70B" },
5
+ { id: "meta-llama/llama-4-maverick-17b-128e-instruct", label: "Llama 4 Maverick" },
6
+ { id: "meta-llama/llama-4-scout-17b-16e-instruct", label: "Llama 4 Scout" },
7
+ { id: "moonshotai/kimi-k2-instruct", label: "Kimi K2" },
8
+ { id: "openai/gpt-oss-120b", label: "GPT-OSS 120B" },
9
+ { id: "qwen/qwen3-32b", label: "Qwen3 32B" },
10
+ { id: "llama-3.1-8b-instant", label: "Llama 3.1 8B instant" },
11
+ ];
12
+ export const groqProvider = {
13
+ id: "groq",
14
+ label: "Groq",
15
+ authEnv: ["GROQ_API_KEY"],
16
+ baseUrl: BASE,
17
+ defaultModel: "llama-3.3-70b-versatile",
18
+ listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
19
+ stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, reasoning: "openai" }),
20
+ supportsReasoning: () => true,
21
+ };