@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
@@ -7,11 +7,30 @@
7
7
  *
8
8
  * FASTCODE_TRUST_ALL=1 marks every directory trusted (scripts, CI).
9
9
  */
10
- import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
10
+ import { existsSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
12
  import { ensureDir, getPaths } from "./paths.js";
13
+
13
14
  const TRUTHY = ["1", "true", "yes", "on"];
14
- export function normalizeProjectPath(dir) {
15
+
16
+ export interface TrustPathEntry {
17
+ grantedAt: string;
18
+ lastUsed: string;
19
+ }
20
+
21
+ export interface TrustToolEntry extends TrustPathEntry {
22
+ catalogHash: string;
23
+ }
24
+
25
+ export interface TrustStore {
26
+ version: 1;
27
+ tools: Record<string, TrustToolEntry>;
28
+ paths: Record<string, TrustPathEntry>;
29
+ }
30
+
31
+ let cache: { file: string; key: string; state: TrustStore } | undefined;
32
+
33
+ export function normalizeProjectPath(dir: string): string {
15
34
  const absolute = resolve(dir);
16
35
  try {
17
36
  return realpathSync(absolute);
@@ -20,48 +39,188 @@ export function normalizeProjectPath(dir) {
20
39
  return absolute;
21
40
  }
22
41
  }
23
- export function loadTrust() {
42
+
43
+ function emptyStore(): TrustStore {
44
+ return { version: 1, tools: {}, paths: {} };
45
+ }
46
+
47
+ function normalizePaths(raw: any): Record<string, TrustPathEntry> {
48
+ const paths: Record<string, TrustPathEntry> = {};
49
+ if (!raw || typeof raw !== "object")
50
+ return paths;
51
+ for (const [key, value] of Object.entries<any>(raw)) {
52
+ if (!key || !value || typeof value !== "object")
53
+ continue;
54
+ paths[key] = {
55
+ grantedAt: typeof value.grantedAt === "string" ? value.grantedAt : "",
56
+ lastUsed: typeof value.lastUsed === "string" ? value.lastUsed : "",
57
+ };
58
+ }
59
+ return paths;
60
+ }
61
+
62
+ function normalizeTools(raw: any): Record<string, TrustToolEntry> {
63
+ const tools: Record<string, TrustToolEntry> = {};
64
+ if (!raw || typeof raw !== "object")
65
+ return tools;
66
+ for (const [key, value] of Object.entries<any>(raw)) {
67
+ if (!key || !value || typeof value !== "object" || typeof value.catalogHash !== "string")
68
+ continue;
69
+ tools[key] = {
70
+ grantedAt: typeof value.grantedAt === "string" ? value.grantedAt : "",
71
+ lastUsed: typeof value.lastUsed === "string" ? value.lastUsed : "",
72
+ catalogHash: value.catalogHash,
73
+ };
74
+ }
75
+ return tools;
76
+ }
77
+
78
+ function migrateLegacy(raw: any): TrustStore | undefined {
79
+ if (!raw || typeof raw !== "object" || !Array.isArray(raw.trusted))
80
+ return undefined;
81
+ const store = emptyStore();
82
+ for (const entry of raw.trusted) {
83
+ if (!entry || typeof entry.path !== "string" || !entry.path)
84
+ continue;
85
+ const at = typeof entry.addedAt === "string" ? entry.addedAt : new Date().toISOString();
86
+ store.paths[entry.path] = { grantedAt: at, lastUsed: at };
87
+ }
88
+ return store;
89
+ }
90
+
91
+ function readStore(file: string): TrustStore {
92
+ if (!existsSync(file))
93
+ return emptyStore();
94
+ try {
95
+ const raw = JSON.parse(readFileSync(file, "utf8"));
96
+ if (raw && raw.version === 1) {
97
+ return {
98
+ version: 1,
99
+ tools: normalizeTools(raw.tools),
100
+ paths: normalizePaths(raw.paths),
101
+ };
102
+ }
103
+ const migrated = migrateLegacy(raw);
104
+ if (!migrated)
105
+ return emptyStore();
106
+ try {
107
+ writeFileSync(file, JSON.stringify(migrated, null, 2), "utf8");
108
+ }
109
+ catch {
110
+ // A read-only home still gets the migrated view in memory.
111
+ }
112
+ return migrated;
113
+ }
114
+ catch {
115
+ return emptyStore();
116
+ }
117
+ }
118
+
119
+ export function loadTrust(): TrustStore {
24
120
  const { trust } = getPaths();
25
- if (!existsSync(trust))
26
- return { trusted: [] };
121
+ let key = "missing";
27
122
  try {
28
- const raw = JSON.parse(readFileSync(trust, "utf8"));
29
- return { trusted: Array.isArray(raw.trusted) ? raw.trusted.filter((entry) => entry && typeof entry.path === "string") : [] };
123
+ const info = statSync(trust);
124
+ key = `${info.mtimeMs}:${info.size}`;
30
125
  }
31
126
  catch {
32
- return { trusted: [] };
127
+ // Missing file: cache the empty store until it appears.
33
128
  }
129
+ if (cache && cache.file === trust && cache.key === key)
130
+ return cache.state;
131
+ const state = readStore(trust);
132
+ cache = { file: trust, key, state };
133
+ return state;
34
134
  }
35
- function saveTrust(state) {
135
+
136
+ function saveTrust(state: TrustStore): void {
36
137
  const paths = getPaths();
37
138
  ensureDir(paths.root);
38
- writeFileSync(paths.trust, JSON.stringify(state, null, 2), "utf8");
139
+ const tmp = paths.trust + "." + process.pid + ".tmp";
140
+ try {
141
+ writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
142
+ renameSync(tmp, paths.trust);
143
+ }
144
+ catch (err) {
145
+ try {
146
+ rmSync(tmp, { force: true });
147
+ }
148
+ catch {
149
+ // best effort
150
+ }
151
+ throw err;
152
+ }
153
+ try {
154
+ const info = statSync(paths.trust);
155
+ cache = { file: paths.trust, key: `${info.mtimeMs}:${info.size}`, state };
156
+ }
157
+ catch {
158
+ cache = undefined;
159
+ }
39
160
  }
40
- export function trustAllEnabled() {
161
+
162
+ export function trustAllEnabled(): boolean {
41
163
  return TRUTHY.includes((process.env["FASTCODE_TRUST_ALL"] ?? "").trim().toLowerCase());
42
164
  }
43
- export function isTrusted(dir) {
165
+
166
+ export function isTrusted(dir: string): boolean {
44
167
  if (trustAllEnabled())
45
168
  return true;
46
169
  const target = normalizeProjectPath(dir);
47
- return loadTrust().trusted.some((entry) => entry.path === target);
170
+ return Boolean(loadTrust().paths[target]);
48
171
  }
49
- export function trustProject(dir) {
172
+
173
+ export function trustProject(dir: string): string {
50
174
  const target = normalizeProjectPath(dir);
51
175
  const state = loadTrust();
52
- if (!state.trusted.some((entry) => entry.path === target)) {
53
- state.trusted.push({ path: target, addedAt: new Date().toISOString() });
176
+ if (!state.paths[target]) {
177
+ const at = new Date().toISOString();
178
+ state.paths[target] = { grantedAt: at, lastUsed: at };
54
179
  saveTrust(state);
55
180
  }
56
181
  return target;
57
182
  }
58
- export function untrustProject(dir) {
183
+
184
+ export function untrustProject(dir: string): boolean {
59
185
  const target = normalizeProjectPath(dir);
60
186
  const state = loadTrust();
61
- const next = state.trusted.filter((entry) => entry.path !== target);
62
- saveTrust({ trusted: next });
63
- return state.trusted.length !== next.length;
187
+ if (!state.paths[target])
188
+ return false;
189
+ delete state.paths[target];
190
+ saveTrust(state);
191
+ return true;
192
+ }
193
+
194
+ export function listTrusted(): Array<{ path: string; addedAt: string }> {
195
+ return Object.entries(loadTrust().paths).map(([path, entry]) => ({ path, addedAt: entry.grantedAt }));
196
+ }
197
+
198
+ /** Check a tool/fingerprint approval bound to the current tool catalog. */
199
+ export function isToolApproved(key: string, catalogHash: string): boolean {
200
+ const entry = loadTrust().tools[key];
201
+ return Boolean(entry && entry.catalogHash === catalogHash);
202
+ }
203
+
204
+ export function rememberToolApproval(key: string, catalogHash: string): void {
205
+ const state = loadTrust();
206
+ const at = new Date().toISOString();
207
+ state.tools[key] = { grantedAt: at, lastUsed: at, catalogHash };
208
+ saveTrust(state);
209
+ }
210
+
211
+ export function forgetToolApproval(key: string): boolean {
212
+ const state = loadTrust();
213
+ if (!state.tools[key])
214
+ return false;
215
+ delete state.tools[key];
216
+ saveTrust(state);
217
+ return true;
64
218
  }
65
- export function listTrusted() {
66
- return loadTrust().trusted;
219
+
220
+ export function listToolApprovals(): Array<{ key: string; grantedAt: string; catalogHash: string }> {
221
+ return Object.entries(loadTrust().tools).map(([key, entry]) => ({
222
+ key,
223
+ grantedAt: entry.grantedAt,
224
+ catalogHash: entry.catalogHash,
225
+ }));
67
226
  }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Non-Ink headless runner for `--ui json` / non-TTY `--ui plain`.
3
+ *
4
+ * stdout is reserved for the protocol/answer; diagnostics go to stderr.
5
+ */
6
+ import readline from "node:readline";
7
+ import { AGENT_SYSTEM_PROMPT } from "./agent/protocol.js";
8
+ import { compactionConfigFromEnv } from "./agent/compaction.js";
9
+ import { resolveToolProtocol } from "./agent/protocol-mode.js";
10
+ import { runAgent } from "./agent/runner.js";
11
+ import { setShellPoolSize } from "./agent/shell-session.js";
12
+ import { toolSchemas } from "./agent/tool-schemas.js";
13
+ import { encodeGateAsk, parseGateDecision } from "./agent/approval-json.js";
14
+ import { decisionForOption, parseApprovalChoice, type ApprovalDecision, type ApprovalRequest } from "./agent/approval.js";
15
+ import { buildPluginsPrompt } from "./plugins/manager.js";
16
+ import { buildSkillsPrompt } from "./skills/manager.js";
17
+ import { loadConfig, resolveApiKey } from "./config/store.js";
18
+ import { getProviderOrThrow } from "./providers/index.js";
19
+ import { resumeSession } from "./session/projection.js";
20
+
21
+ interface HeadlessOptions {
22
+ prompt: string;
23
+ ui: "json" | "plain";
24
+ resume?: string;
25
+ }
26
+
27
+ let eventSeq = 0;
28
+
29
+ function writeEvent(value: Record<string, any>): void {
30
+ eventSeq += 1;
31
+ process.stdout.write(JSON.stringify({ v: 1, seq: eventSeq, ts: new Date().toISOString(), ...value }) + "\n");
32
+ }
33
+
34
+ function optionalBudget(name: string): number | undefined {
35
+ const value = Number(process.env[name]);
36
+ return Number.isFinite(value) && value > 0 ? value : undefined;
37
+ }
38
+
39
+ export async function runHeadless(options: HeadlessOptions): Promise<void> {
40
+ const config = loadConfig();
41
+ setShellPoolSize(config.shellPoolSize);
42
+ const provider = getProviderOrThrow(config.providerId);
43
+ let apiKey: string | undefined;
44
+ if (provider.authEnv.length > 0) {
45
+ const lookup = resolveApiKey(provider.id, config.model, provider.authEnv);
46
+ if (lookup.status !== "ok")
47
+ throw new Error(`No usable API key for ${provider.id}.`);
48
+ apiKey = lookup.key;
49
+ }
50
+ const history = options.resume ? resumeSession(options.resume, process.cwd()).messages : [];
51
+ const system = [
52
+ config.systemPrompt,
53
+ buildSkillsPrompt(config.enabledSkills),
54
+ buildPluginsPrompt(config.enabledPlugins),
55
+ config.agentMode ? AGENT_SYSTEM_PROMPT : "",
56
+ ].filter((part) => part.trim()).join("\n\n");
57
+ const protocol = config.agentMode ? resolveToolProtocol(config, config.providerId, config.model) : "legacy";
58
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
59
+ const queued: string[] = [];
60
+ const waiters: Array<(line: string | undefined) => void> = [];
61
+ let eof = false;
62
+ rl.on("line", (line) => {
63
+ const waiter = waiters.shift();
64
+ if (waiter)
65
+ waiter(line);
66
+ else
67
+ queued.push(line);
68
+ });
69
+ rl.on("close", () => {
70
+ eof = true;
71
+ while (waiters.length > 0)
72
+ waiters.shift()!(undefined);
73
+ });
74
+ const nextLine = (timeoutMs: number): Promise<string | undefined> => {
75
+ if (queued.length > 0)
76
+ return Promise.resolve(queued.shift());
77
+ if (eof)
78
+ return Promise.resolve(undefined);
79
+ return new Promise((resolve) => {
80
+ const waiter = (line: string | undefined) => {
81
+ clearTimeout(timer);
82
+ resolve(line);
83
+ };
84
+ const timer = setTimeout(() => {
85
+ const index = waiters.indexOf(waiter);
86
+ if (index >= 0)
87
+ waiters.splice(index, 1);
88
+ resolve(undefined);
89
+ }, Math.max(1000, timeoutMs));
90
+ timer.unref?.();
91
+ waiters.push(waiter);
92
+ });
93
+ };
94
+
95
+ const requestApproval = async (request: ApprovalRequest): Promise<ApprovalDecision> => {
96
+ if (options.ui === "json") {
97
+ writeEvent(encodeGateAsk(request));
98
+ const line = await nextLine(request.timeoutMs ?? 60_000);
99
+ if (line === undefined)
100
+ return { kind: "deny", reason: "approval timeout/EOF" };
101
+ const parsed = parseGateDecision(request, line);
102
+ if (parsed.error) {
103
+ writeEvent({ event: "gate:error", version: 1, id: request.id, error: parsed.error });
104
+ return parsed.decision;
105
+ }
106
+ return parsed.decision;
107
+ }
108
+ process.stderr.write(`${request.prompt ?? "Approval required"}\n`);
109
+ request.options.forEach((option, index) => process.stderr.write(`${index + 1}. ${option.label}\n`));
110
+ process.stderr.write(`Choose 1-${request.options.length}: `);
111
+ const line = await nextLine(request.timeoutMs ?? 60_000);
112
+ const option = line === undefined ? undefined : parseApprovalChoice(request, line);
113
+ if (!option)
114
+ return { kind: "deny", reason: "approval cancelled/invalid/timeout" };
115
+ if (option.kind === "feedback") {
116
+ process.stderr.write(`${option.prompt || "Why should this be denied?"}\n> `);
117
+ const feedbackLine = await nextLine(request.timeoutMs ?? 60_000);
118
+ const feedback = feedbackLine?.trim();
119
+ return feedback
120
+ ? { kind: "feedback", text: feedback }
121
+ : { kind: "deny", reason: "feedback cancelled/empty (fail closed)" };
122
+ }
123
+ return decisionForOption(option);
124
+ };
125
+
126
+ const abort = new AbortController();
127
+ const onSigint = () => abort.abort();
128
+ const onTerminate = () => abort.abort();
129
+ process.once("SIGINT", onSigint);
130
+ process.once("SIGTERM", onTerminate);
131
+ process.once("SIGHUP", onTerminate);
132
+ try {
133
+ for await (const event of runAgent({
134
+ provider,
135
+ apiKey,
136
+ model: config.model,
137
+ messages: history,
138
+ prompt: options.prompt,
139
+ system,
140
+ maxTurns: config.agentMode ? Math.max(1, Math.floor(Number(config.maxAgentTurns) || 24)) : 1,
141
+ allowTools: config.agentMode,
142
+ tokenBudget: optionalBudget("FASTCODE_TOKEN_BUDGET"),
143
+ costBudgetUsd: optionalBudget("FASTCODE_COST_BUDGET_USD"),
144
+ signal: abort.signal,
145
+ cwd: process.cwd(),
146
+ idleTimeoutMs: config.requestIdleTimeoutMs,
147
+ compaction: compactionConfigFromEnv(),
148
+ reasoningEffort: config.reasoningEffort,
149
+ protocol,
150
+ tools: protocol === "native" ? toolSchemas() : undefined,
151
+ requestApproval,
152
+ })) {
153
+ if (options.ui === "json") {
154
+ if (event.type === "assistant_text")
155
+ writeEvent({ event: "assistant", text: event.text });
156
+ else if (event.type === "tool_call")
157
+ writeEvent({
158
+ event: "tool_call",
159
+ id: event.id,
160
+ tool: event.call.tool,
161
+ args: event.call,
162
+ ...(event.native ? { native: true } : {}),
163
+ });
164
+ else if (event.type === "egress_denied")
165
+ writeEvent({
166
+ event: "egress_denied",
167
+ outlet: event.outlet,
168
+ ...(event.target ? { target: event.target } : {}),
169
+ rule: event.rule,
170
+ mode: event.mode,
171
+ would_block: event.wouldBlock,
172
+ code: event.code,
173
+ reason: event.reason,
174
+ });
175
+ else if (event.type === "tool_result")
176
+ writeEvent({
177
+ event: "tool_result",
178
+ id: event.id,
179
+ tool: event.call.tool,
180
+ ok: event.ok,
181
+ output: event.output,
182
+ ...(event.sideEffects ? { side_effects: event.sideEffects } : {}),
183
+ ...(event.code ? { code: event.code } : {}),
184
+ ...(event.hint ? { hint: event.hint } : {}),
185
+ ...(event.truncated !== undefined ? { truncated: event.truncated } : {}),
186
+ ...(event.totalLines !== undefined ? { total_lines: event.totalLines } : {}),
187
+ ...(event.returnedLines !== undefined ? { returned_lines: event.returnedLines } : {}),
188
+ ...(event.totalBytes !== undefined ? { total_bytes: event.totalBytes } : {}),
189
+ ...(event.returnedBytes !== undefined ? { returned_bytes: event.returnedBytes } : {}),
190
+ ...(event.filesRead !== undefined ? { files_read: event.filesRead } : {}),
191
+ ...(event.filesSkipped !== undefined ? { files_skipped: event.filesSkipped } : {}),
192
+ ...(event.matches !== undefined ? { matches: event.matches } : {}),
193
+ });
194
+ else if (event.type === "usage")
195
+ writeEvent({
196
+ event: "usage",
197
+ input_tokens: event.usage.inputTokens ?? 0,
198
+ output_tokens: event.usage.outputTokens ?? 0,
199
+ cost_usd: event.usage.costUsd ?? 0,
200
+ });
201
+ else if (event.type === "notice")
202
+ writeEvent({ event: "notice", text: event.text });
203
+ else if (event.type === "error")
204
+ writeEvent({ event: "error", kind: event.kind, code: event.kind, message: event.message });
205
+ else if (event.type === "done")
206
+ writeEvent({ event: "done", stopReason: event.stopReason, finalText: event.finalText });
207
+ }
208
+ else if (event.type === "assistant_text") {
209
+ process.stdout.write(event.text);
210
+ }
211
+ else if (event.type === "notice") {
212
+ process.stderr.write(`[fastcode] ${event.text}\n`);
213
+ }
214
+ else if (event.type === "done" && event.finalText) {
215
+ process.stdout.write(event.finalText + "\n");
216
+ }
217
+ }
218
+ }
219
+ finally {
220
+ process.removeListener("SIGINT", onSigint);
221
+ process.removeListener("SIGTERM", onTerminate);
222
+ process.removeListener("SIGHUP", onTerminate);
223
+ rl.close();
224
+ }
225
+ }
package/src/index.ts CHANGED
@@ -5,13 +5,29 @@
5
5
  * Ink-based TUI. We keep this file tiny so the slow imports (Ink, React)
6
6
  * are not loaded for `fastcode skills list`.
7
7
  */
8
- import { parseCli } from "./cli.js";
8
+ import { parseCli } from "./cli-main.js";
9
+ import { installProcessGuards } from "./lib/lifecycle.js";
9
10
  async function main() {
11
+ installProcessGuards();
10
12
  const cli = await parseCli();
11
13
  if (cli.done)
12
14
  return;
15
+ if (cli.ui === "json") {
16
+ if (!cli.prompt) {
17
+ console.error("--ui json requires a prompt: fastcode --ui json \"<prompt>\"");
18
+ process.exit(1);
19
+ }
20
+ const { runHeadless } = await import("./headless.js");
21
+ await runHeadless({ prompt: cli.prompt, ui: "json", resume: cli.resume });
22
+ return;
23
+ }
13
24
  if (!process.stdout.isTTY) {
14
- console.error("FastCode TUI requires a TTY. Pipe-mode prompts aren't supported yet — try `fastcode \"<prompt>\"` from a real terminal.");
25
+ if (cli.ui === "plain" && cli.prompt) {
26
+ const { runHeadless } = await import("./headless.js");
27
+ await runHeadless({ prompt: cli.prompt, ui: "plain", resume: cli.resume });
28
+ return;
29
+ }
30
+ console.error("FastCode TUI requires a TTY. Use `fastcode --ui plain|json \"<prompt>\"` for pipe mode.");
15
31
  process.exit(1);
16
32
  }
17
33
  // Lazy-load Ink so subcommands don't pay the React boot cost.
@@ -23,7 +39,7 @@ async function main() {
23
39
  // React.createElement avoids needing a separate .tsx loader for this file.
24
40
  // exitOnCtrlC: false lets the app own Ctrl+C so the documented
25
41
  // "press Ctrl+C twice to quit" behavior actually works.
26
- render(React.createElement(App, { initialPrompt: cli.prompt, resume: cli.resume }), { exitOnCtrlC: false });
42
+ render(React.createElement(App, { initialPrompt: cli.prompt, resume: cli.resume, ui: cli.ui ?? "tty" }), { exitOnCtrlC: false });
27
43
  }
28
44
  main().catch((err) => {
29
45
  console.error(err);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Minimal debug logging.
3
+ *
4
+ * There is no logging framework in this project: normal output goes through
5
+ * Ink and lifecycle warnings go straight to stderr. `FASTCODE_DEBUG=1` opts
6
+ * into low-volume stderr diagnostics (for example timeout clamping or config
7
+ * cache invalidation) without changing the default experience.
8
+ */
9
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
10
+
11
+ export function debugEnabled(): boolean {
12
+ return TRUTHY.has((process.env["FASTCODE_DEBUG"] ?? "").trim().toLowerCase());
13
+ }
14
+
15
+ export function debugLog(scope: string, message: string): void {
16
+ if (!debugEnabled())
17
+ return;
18
+ try {
19
+ process.stderr.write(`[fastcode:${scope}] ${message}\n`);
20
+ }
21
+ catch {
22
+ // Debug logging must never break the operation it observes.
23
+ }
24
+ }