@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
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Stable NDJSON gate protocol for GUI/JSON clients.
3
+ *
4
+ * This module is pure: it validates and encodes one request/decision at a
5
+ * time. A headless transport can consume it without leaking TUI output to
6
+ * stdout. Hard Block requests never reach this code.
7
+ */
8
+ import type { ApprovalDecision, ApprovalOption, ApprovalRequest } from "./approval.js";
9
+ import { decisionForOption } from "./approval.js";
10
+
11
+ export const GATE_PROTOCOL_VERSION = 1;
12
+
13
+ export interface GateAskEvent {
14
+ event: "gate:ask";
15
+ version: number;
16
+ id: string;
17
+ tier: number;
18
+ action: string;
19
+ reason: string;
20
+ prompt: string;
21
+ source: string;
22
+ targets: string[];
23
+ diffstat: { files: number; lines: number };
24
+ options: Array<{ id: string; label: string; kind: string; default?: boolean }>;
25
+ default: string;
26
+ require_input?: string;
27
+ }
28
+
29
+ function optionId(option: ApprovalOption, index: number): string {
30
+ return option.id ?? `${option.kind}-${index}`;
31
+ }
32
+
33
+ function targetsFor(request: ApprovalRequest): string[] {
34
+ const args: any = request.args ?? {};
35
+ const values = [args.path, args.from, args.to, args.cwd, request.preview?.kind === "diff" ? "diff" : undefined];
36
+ return values.filter((value): value is string => typeof value === "string" && value.length > 0);
37
+ }
38
+
39
+ export function encodeGateAsk(request: ApprovalRequest): GateAskEvent {
40
+ const options = request.options.map((option, index) => ({
41
+ id: optionId(option, index),
42
+ label: option.label,
43
+ kind: option.kind,
44
+ ...(index === (request.defaultIndex ?? 0) ? { default: true } : {}),
45
+ }));
46
+ const defaultIndex = Math.max(0, Math.min(request.defaultIndex ?? 0, options.length - 1));
47
+ const text = request.preview?.kind === "diff" ? request.preview.text : "";
48
+ const feedbackOption = request.options.find((option) => option.kind === "feedback");
49
+ const requireInput = request.freeInput?.prompt ?? feedbackOption?.prompt;
50
+ return {
51
+ event: "gate:ask",
52
+ version: GATE_PROTOCOL_VERSION,
53
+ id: request.id,
54
+ tier: request.tier ?? 2,
55
+ action: request.tool,
56
+ reason: request.reason,
57
+ prompt: request.prompt ?? "Approval required",
58
+ source: request.source ?? "system",
59
+ targets: targetsFor(request),
60
+ diffstat: { files: text ? text.split(/\r?\n/).filter(Boolean).length : 0, lines: text ? text.split(/\r?\n/).length : 0 },
61
+ options,
62
+ default: options[defaultIndex]?.id ?? "",
63
+ ...(requireInput ? { require_input: requireInput } : {}),
64
+ };
65
+ }
66
+
67
+ export interface GateDecisionResult {
68
+ decision: ApprovalDecision;
69
+ error?: string;
70
+ }
71
+
72
+ /**
73
+ * Accept only a well-formed decision for this exact request id and one of its
74
+ * stable option ids. Malformed JSON, wrong ids, unknown options and missing
75
+ * required input all fail closed.
76
+ */
77
+ export function parseGateDecision(request: ApprovalRequest, line: string): GateDecisionResult {
78
+ let payload: any;
79
+ try {
80
+ payload = JSON.parse(line);
81
+ }
82
+ catch {
83
+ return { decision: { kind: "deny", reason: "malformed JSON decision" }, error: "malformed JSON" };
84
+ }
85
+ if (!payload || typeof payload !== "object" || payload.version !== GATE_PROTOCOL_VERSION)
86
+ return { decision: { kind: "deny", reason: "unsupported gate protocol version" }, error: "version" };
87
+ if (payload.event !== "gate:decision")
88
+ return { decision: { kind: "deny", reason: "unexpected gate event" }, error: "event" };
89
+ if (payload.id !== request.id)
90
+ return { decision: { kind: "deny", reason: "decision id does not match the pending request" }, error: "id" };
91
+ if (request.freeInput?.enabled && (typeof payload.input !== "string" || payload.input.trim() === ""))
92
+ return { decision: { kind: "deny", reason: "required free input is missing" }, error: "input" };
93
+ const option = request.options.find((entry, index) => optionId(entry, index) === payload.option);
94
+ if (!option)
95
+ return { decision: { kind: "deny", reason: "unknown approval option id" }, error: "option" };
96
+ if (option.kind === "feedback") {
97
+ const text = typeof payload.input === "string" ? payload.input.trim() : "";
98
+ if (!text)
99
+ return { decision: { kind: "deny", reason: "feedback input is required" }, error: "feedback" };
100
+ return { decision: { kind: "feedback", text } };
101
+ }
102
+ return { decision: decisionForOption(option) };
103
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Termux:API approval sheet.
3
+ *
4
+ * Returns undefined when termux-dialog is missing/times out/crashes so the
5
+ * caller can fall back to the TUI. A real cancellation returns a deny.
6
+ * The command is built with spawn args (no shell interpolation).
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ import { decisionForOption, type ApprovalDecision, type ApprovalRequest } from "./approval.js";
10
+
11
+ export function nativeDecisionFromPayload(request: ApprovalRequest, payload: any): ApprovalDecision | undefined {
12
+ if (payload?.code === -1)
13
+ return { kind: "deny", reason: "termux-dialog cancelled" };
14
+ if (payload?.code !== 0 || !Number.isInteger(payload.index) ||
15
+ payload.index < 0 || payload.index >= request.options.length)
16
+ return { kind: "deny", reason: "termux-dialog returned an invalid selection" };
17
+ return decisionForOption(request.options[payload.index]);
18
+ }
19
+
20
+ async function nativeFeedback(prompt: string, signal?: AbortSignal, timeoutMs = 60_000): Promise<string | undefined> {
21
+ if (signal?.aborted)
22
+ return undefined;
23
+ return await new Promise((resolve) => {
24
+ let settled = false;
25
+ let child: ReturnType<typeof spawn> | undefined;
26
+ let timer: ReturnType<typeof setTimeout> | undefined;
27
+ const finish = (value: string | undefined) => {
28
+ if (settled)
29
+ return;
30
+ settled = true;
31
+ if (timer)
32
+ clearTimeout(timer);
33
+ signal?.removeEventListener("abort", onAbort);
34
+ resolve(value);
35
+ };
36
+ const onAbort = () => {
37
+ try {
38
+ child?.kill("SIGKILL");
39
+ }
40
+ catch {
41
+ // Already gone.
42
+ }
43
+ finish(undefined);
44
+ };
45
+ signal?.addEventListener("abort", onAbort, { once: true });
46
+ child = spawn("termux-dialog", ["text", "-t", prompt], {
47
+ stdio: ["ignore", "pipe", "ignore"],
48
+ windowsHide: true,
49
+ });
50
+ let stdout = "";
51
+ child.stdout?.setEncoding("utf8");
52
+ child.stdout?.on("data", (chunk: string) => {
53
+ stdout += chunk;
54
+ });
55
+ child.on("error", () => finish(undefined));
56
+ child.on("close", () => {
57
+ let payload: any;
58
+ try {
59
+ payload = JSON.parse(stdout.trim());
60
+ }
61
+ catch {
62
+ finish(undefined);
63
+ return;
64
+ }
65
+ if (payload?.code === 0 && typeof payload.text === "string")
66
+ finish(payload.text);
67
+ else
68
+ finish(undefined);
69
+ });
70
+ timer = setTimeout(() => {
71
+ try {
72
+ child?.kill("SIGKILL");
73
+ }
74
+ catch {
75
+ // Already gone.
76
+ }
77
+ finish(undefined);
78
+ }, Math.max(1000, timeoutMs));
79
+ timer.unref?.();
80
+ });
81
+ }
82
+
83
+ export async function nativeApproval(request: ApprovalRequest, signal?: AbortSignal): Promise<ApprovalDecision | undefined> {
84
+ if (signal?.aborted)
85
+ return { kind: "deny", reason: "approval cancelled" };
86
+ const labels = request.options.map((option) => option.label);
87
+ const title = request.prompt ?? `Approval: ${request.tool}`;
88
+ return await new Promise((resolve) => {
89
+ let settled = false;
90
+ let child: ReturnType<typeof spawn> | undefined;
91
+ let timer: ReturnType<typeof setTimeout> | undefined;
92
+ const finish = (value: ApprovalDecision | undefined) => {
93
+ if (settled)
94
+ return;
95
+ settled = true;
96
+ if (timer)
97
+ clearTimeout(timer);
98
+ signal?.removeEventListener("abort", onAbort);
99
+ resolve(value);
100
+ };
101
+ const onAbort = () => {
102
+ try {
103
+ child?.kill("SIGKILL");
104
+ }
105
+ catch {
106
+ // Already gone.
107
+ }
108
+ finish({ kind: "deny", reason: "approval cancelled" });
109
+ };
110
+ signal?.addEventListener("abort", onAbort, { once: true });
111
+ child = spawn("termux-dialog", ["sheet", "-v", labels.join(","), "-t", title], {
112
+ stdio: ["ignore", "pipe", "ignore"],
113
+ windowsHide: true,
114
+ });
115
+ let stdout = "";
116
+ child.stdout?.setEncoding("utf8");
117
+ child.stdout?.on("data", (chunk: string) => {
118
+ stdout += chunk;
119
+ });
120
+ child.on("error", () => finish(undefined));
121
+ child.on("close", async () => {
122
+ let payload: any;
123
+ try {
124
+ payload = JSON.parse(stdout.trim());
125
+ }
126
+ catch {
127
+ finish(undefined);
128
+ return;
129
+ }
130
+ const selected = payload?.code === 0 && Number.isInteger(payload.index)
131
+ ? request.options[payload.index]
132
+ : undefined;
133
+ if (selected?.kind === "feedback") {
134
+ if (timer) {
135
+ clearTimeout(timer);
136
+ timer = undefined;
137
+ }
138
+ const prompt = selected.prompt || "Why should this be denied?";
139
+ const text = await nativeFeedback(prompt, signal, request.timeoutMs);
140
+ if (signal?.aborted) {
141
+ finish({ kind: "deny", reason: "approval cancelled" });
142
+ return;
143
+ }
144
+ if (typeof text !== "string") {
145
+ // Text dialog unavailable/cancelled: fall back to the TUI prompt.
146
+ finish(undefined);
147
+ return;
148
+ }
149
+ const feedback = text.trim();
150
+ finish(feedback
151
+ ? { kind: "feedback", text: feedback }
152
+ : { kind: "deny", reason: "feedback was empty (fail closed)" });
153
+ return;
154
+ }
155
+ finish(nativeDecisionFromPayload(request, payload));
156
+ });
157
+ timer = setTimeout(() => {
158
+ try {
159
+ child?.kill("SIGKILL");
160
+ }
161
+ catch {
162
+ // Already gone.
163
+ }
164
+ finish(undefined);
165
+ }, Math.max(1000, request.timeoutMs ?? 60_000));
166
+ timer.unref?.();
167
+ });
168
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Approval scope storage.
3
+ *
4
+ * - once: no persistence
5
+ * - session: in-memory, implicitly cleared when the process exits
6
+ * - always: ~/.fastcode/trusted.json `tools` entry bound to catalogHash
7
+ *
8
+ * Keys include the normalized tool-call fingerprint so "always" cannot grant
9
+ * every future shell command from one approval.
10
+ */
11
+ import type { ApprovalScope } from "./approval.js";
12
+ import type { ToolCall } from "./tools/types.js";
13
+ import { toolCallFingerprint } from "./guards.js";
14
+ import { isToolApproved, rememberToolApproval } from "../config/trust.js";
15
+
16
+ const sessionApprovals = new Set<string>();
17
+
18
+ export function approvalKey(tool: string, call: ToolCall, cwd: string): string {
19
+ return `${tool}:${toolCallFingerprint(call, cwd)}`;
20
+ }
21
+
22
+ export function isPreApproved(key: string, catalogHash: string): boolean {
23
+ return sessionApprovals.has(`${catalogHash}:${key}`) || isToolApproved(key, catalogHash);
24
+ }
25
+
26
+ export function rememberApproval(key: string, catalogHash: string, scope: ApprovalScope): void {
27
+ if (scope === "once")
28
+ return;
29
+ if (scope === "session") {
30
+ sessionApprovals.add(`${catalogHash}:${key}`);
31
+ return;
32
+ }
33
+ rememberToolApproval(key, catalogHash);
34
+ }
35
+
36
+ export function clearSessionApprovals(): void {
37
+ sessionApprovals.clear();
38
+ }
@@ -0,0 +1,94 @@
1
+ export type ApprovalScope = "once" | "session" | "always";
2
+
3
+ export type ApprovalReason =
4
+ | "trust"
5
+ | "sensitive_path"
6
+ | "ambiguous_match"
7
+ | "destructive"
8
+ | "large_diff";
9
+
10
+ export interface ChoiceItem {
11
+ label: string;
12
+ value: string;
13
+ }
14
+
15
+ export type ApprovalPreview =
16
+ | { kind: "diff"; text: string }
17
+ | { kind: "choices"; items: ChoiceItem[] };
18
+
19
+ export type ApprovalOption =
20
+ | { kind: "allow"; id?: string; scope: ApprovalScope; label: string }
21
+ | { kind: "deny"; id?: string; label: string }
22
+ | { kind: "choose"; id?: string; label: string; value: string; meta?: { recommended?: boolean; risk?: string; files?: number; reason?: string } }
23
+ | { kind: "feedback"; id?: string; label: string; prompt: string };
24
+
25
+ export interface ApprovalRequest {
26
+ id: string;
27
+ tool: string;
28
+ args: unknown;
29
+ reason: ApprovalReason;
30
+ options: ApprovalOption[];
31
+ preview?: ApprovalPreview;
32
+ tier?: 0 | 1 | 2 | 3;
33
+ prompt?: string;
34
+ source?: "system" | "ai" | "free";
35
+ defaultIndex?: number;
36
+ timeoutMs?: number;
37
+ freeInput?: { enabled: true; prompt: string; hint?: string };
38
+ }
39
+
40
+ export type ApprovalDecision =
41
+ | { kind: "allow"; scope: ApprovalScope }
42
+ | { kind: "deny"; reason?: string; feedback?: string; code?: string; target?: string }
43
+ | { kind: "pending"; request: ApprovalRequest }
44
+ | { kind: "choose"; value: string; input?: string }
45
+ | { kind: "feedback"; text: string };
46
+
47
+ export type ApprovalResolver = (request: ApprovalRequest) => ApprovalDecision | Promise<ApprovalDecision>;
48
+
49
+ export const DEFAULT_ALLOW_OPTIONS: ApprovalOption[] = [
50
+ { kind: "allow", id: "allow-once", scope: "once", label: "Allow once" },
51
+ { kind: "allow", id: "allow-session", scope: "session", label: "Allow for this session" },
52
+ { kind: "allow", id: "allow-always", scope: "always", label: "Always allow" },
53
+ { kind: "deny", id: "deny", label: "Deny" },
54
+ { kind: "feedback", id: "feedback", label: "Deny with feedback", prompt: "Why should this be denied?" },
55
+ ];
56
+
57
+ export function approvalRequestId(): string {
58
+ return "ap_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
59
+ }
60
+
61
+ export function approvalOptionForDecision(request: ApprovalRequest, decision: ApprovalDecision): ApprovalOption | undefined {
62
+ if (decision.kind === "allow")
63
+ return request.options.find((option) => option.kind === "allow" && option.scope === decision.scope);
64
+ if (decision.kind === "deny")
65
+ return request.options.find((option) => option.kind === "deny");
66
+ if (decision.kind === "choose")
67
+ return request.options.find((option) => option.kind === "choose" && option.value === decision.value);
68
+ if (decision.kind === "feedback")
69
+ return request.options.find((option) => option.kind === "feedback");
70
+ return undefined;
71
+ }
72
+
73
+ export function parseApprovalChoice(request: ApprovalRequest, text: string): ApprovalOption | undefined {
74
+ const index = Number(String(text ?? "").trim()) - 1;
75
+ if (!Number.isInteger(index) || index < 0 || index >= request.options.length)
76
+ return undefined;
77
+ return request.options[index];
78
+ }
79
+
80
+ export function decisionForOption(option: ApprovalOption, input?: string): ApprovalDecision {
81
+ if (option.kind === "allow")
82
+ return { kind: "allow", scope: option.scope };
83
+ if (option.kind === "choose")
84
+ return { kind: "choose", value: option.value };
85
+ if (option.kind === "feedback") {
86
+ const text = typeof input === "string" ? input.trim() : "";
87
+ // The option's prompt is the question, not the user's answer. Without an
88
+ // actual answer, fail closed instead of sending the prompt back to the model.
89
+ return text
90
+ ? { kind: "feedback", text }
91
+ : { kind: "deny", reason: "feedback was required but not provided (fail closed)" };
92
+ }
93
+ return { kind: "deny", reason: "denied by user" };
94
+ }