@datename/fastcode-termux 0.1.6-beta → 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 (165) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +50 -17
  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 +33 -0
  10. package/dist/agent/approval.js +40 -5
  11. package/dist/agent/compaction.d.ts +2 -0
  12. package/dist/agent/compaction.js +60 -17
  13. package/dist/agent/guards.d.ts +16 -16
  14. package/dist/agent/guards.js +978 -24
  15. package/dist/agent/policy/defaults.d.ts +16 -0
  16. package/dist/agent/policy/defaults.js +17 -0
  17. package/dist/agent/policy/egress.d.ts +32 -0
  18. package/dist/agent/policy/egress.js +280 -0
  19. package/dist/agent/process-tree.d.ts +20 -0
  20. package/dist/agent/process-tree.js +81 -10
  21. package/dist/agent/protocol.d.ts +20 -1
  22. package/dist/agent/protocol.js +165 -38
  23. package/dist/agent/registry.d.ts +3 -1
  24. package/dist/agent/registry.js +168 -11
  25. package/dist/agent/runner.d.ts +21 -0
  26. package/dist/agent/runner.js +44 -5
  27. package/dist/agent/shell-ast.d.ts +11 -0
  28. package/dist/agent/shell-ast.js +204 -0
  29. package/dist/agent/shell-session.d.ts +14 -0
  30. package/dist/agent/shell-session.js +251 -40
  31. package/dist/agent/tool-schemas.js +26 -7
  32. package/dist/agent/tools/append_file.js +3 -1
  33. package/dist/agent/tools/apply_patch.js +44 -4
  34. package/dist/agent/tools/copy_file.js +26 -3
  35. package/dist/agent/tools/delete_file.js +22 -3
  36. package/dist/agent/tools/grep.js +35 -4
  37. package/dist/agent/tools/make_dir.js +3 -1
  38. package/dist/agent/tools/move_path.d.ts +7 -0
  39. package/dist/agent/tools/move_path.js +31 -5
  40. package/dist/agent/tools/read_file.js +63 -3
  41. package/dist/agent/tools/read_many.js +21 -3
  42. package/dist/agent/tools/readers.d.ts +53 -1
  43. package/dist/agent/tools/readers.js +236 -26
  44. package/dist/agent/tools/replace_in_file.js +38 -5
  45. package/dist/agent/tools/shell.js +2 -2
  46. package/dist/agent/tools/types.d.ts +26 -0
  47. package/dist/agent/tools/validate.d.ts +15 -2
  48. package/dist/agent/tools/validate.js +66 -3
  49. package/dist/agent/tools/write_file.js +3 -1
  50. package/dist/app.d.ts +2 -1
  51. package/dist/app.js +526 -85
  52. package/dist/cli-main.js +363 -0
  53. package/dist/cli.js +397 -337
  54. package/dist/commands/about.js +1 -1
  55. package/dist/commands/clear.d.ts +1 -0
  56. package/dist/commands/clear.js +1 -1
  57. package/dist/commands/compact.d.ts +12 -0
  58. package/dist/commands/compact.js +14 -2
  59. package/dist/commands/context.js +5 -4
  60. package/dist/commands/index.js +2 -0
  61. package/dist/commands/jobs.js +3 -2
  62. package/dist/commands/kill.js +3 -1
  63. package/dist/commands/login.d.ts +2 -2
  64. package/dist/commands/login.js +15 -15
  65. package/dist/commands/model.d.ts +1 -13
  66. package/dist/commands/model.js +6 -38
  67. package/dist/commands/provider.d.ts +1 -15
  68. package/dist/commands/provider.js +4 -22
  69. package/dist/commands/skills.js +34 -4
  70. package/dist/commands/threads.d.ts +9 -0
  71. package/dist/commands/threads.js +26 -0
  72. package/dist/config/store.d.ts +21 -0
  73. package/dist/config/store.js +47 -9
  74. package/dist/config/trust.d.ts +9 -0
  75. package/dist/config/trust.js +41 -2
  76. package/dist/headless.d.ts +7 -0
  77. package/dist/headless.js +213 -0
  78. package/dist/index.js +19 -3
  79. package/dist/lib/debug.d.ts +2 -0
  80. package/dist/lib/debug.js +22 -0
  81. package/dist/lib/diagnostics.d.ts +19 -0
  82. package/dist/lib/diagnostics.js +32 -6
  83. package/dist/lib/highlight-subset.d.ts +21 -0
  84. package/dist/lib/highlight-subset.js +88 -0
  85. package/dist/lib/lifecycle.d.ts +5 -0
  86. package/dist/lib/lifecycle.js +62 -0
  87. package/dist/lib/markdown.js +22 -4
  88. package/dist/providers/deepseek.js +1 -1
  89. package/dist/providers/openaiCompatible.d.ts +24 -2
  90. package/dist/providers/openaiCompatible.js +20 -6
  91. package/dist/providers/spec.d.ts +12 -0
  92. package/dist/providers/spec.js +29 -0
  93. package/dist/session/log.d.ts +14 -2
  94. package/dist/session/log.js +56 -27
  95. package/dist/session/projection.d.ts +34 -1
  96. package/dist/session/projection.js +315 -46
  97. package/dist/skills/manager.d.ts +15 -0
  98. package/dist/skills/manager.js +135 -18
  99. package/dist/types.d.ts +15 -0
  100. package/dist/ui/StatusBar.js +1 -1
  101. package/package.json +3 -2
  102. package/src/agent/approval-json.ts +103 -0
  103. package/src/agent/approval-native.ts +168 -0
  104. package/src/agent/approval-store.ts +38 -0
  105. package/src/agent/approval.ts +58 -11
  106. package/src/agent/compaction.ts +62 -17
  107. package/src/agent/guards.ts +1049 -25
  108. package/src/agent/policy/defaults.ts +34 -0
  109. package/src/agent/policy/egress.ts +335 -0
  110. package/src/agent/process-tree.ts +105 -23
  111. package/src/agent/protocol.ts +187 -39
  112. package/src/agent/registry.ts +186 -14
  113. package/src/agent/runner.ts +50 -7
  114. package/src/agent/shell-ast.ts +227 -0
  115. package/src/agent/shell-session.ts +265 -41
  116. package/src/agent/tool-schemas.ts +26 -7
  117. package/src/agent/tools/append_file.ts +3 -1
  118. package/src/agent/tools/apply_patch.ts +45 -4
  119. package/src/agent/tools/copy_file.ts +27 -3
  120. package/src/agent/tools/delete_file.ts +22 -3
  121. package/src/agent/tools/grep.ts +36 -4
  122. package/src/agent/tools/make_dir.ts +3 -1
  123. package/src/agent/tools/move_path.ts +38 -5
  124. package/src/agent/tools/read_file.ts +64 -3
  125. package/src/agent/tools/read_many.ts +21 -3
  126. package/src/agent/tools/readers.ts +289 -26
  127. package/src/agent/tools/replace_in_file.ts +39 -5
  128. package/src/agent/tools/shell.ts +2 -2
  129. package/src/agent/tools/types.ts +43 -1
  130. package/src/agent/tools/validate.ts +73 -4
  131. package/src/agent/tools/write_file.ts +3 -1
  132. package/src/app.ts +533 -82
  133. package/src/{cli.ts → cli-main.ts} +34 -6
  134. package/src/commands/about.ts +1 -1
  135. package/src/commands/clear.ts +1 -1
  136. package/src/commands/compact.ts +14 -2
  137. package/src/commands/context.ts +5 -4
  138. package/src/commands/index.ts +2 -0
  139. package/src/commands/jobs.ts +3 -2
  140. package/src/commands/kill.ts +3 -1
  141. package/src/commands/login.ts +15 -15
  142. package/src/commands/model.ts +6 -38
  143. package/src/commands/provider.ts +4 -22
  144. package/src/commands/skills.ts +34 -4
  145. package/src/commands/threads.ts +28 -0
  146. package/src/config/store.ts +47 -9
  147. package/src/config/trust.ts +45 -2
  148. package/src/headless.ts +225 -0
  149. package/src/index.ts +19 -3
  150. package/src/lib/debug.ts +24 -0
  151. package/src/lib/diagnostics.ts +32 -6
  152. package/src/lib/highlight-subset.ts +93 -0
  153. package/src/lib/lifecycle.ts +69 -0
  154. package/src/lib/markdown.ts +22 -4
  155. package/src/providers/deepseek.ts +1 -1
  156. package/src/providers/openaiCompatible.ts +47 -10
  157. package/src/providers/spec.ts +42 -0
  158. package/src/session/log.ts +70 -27
  159. package/src/session/projection.ts +360 -47
  160. package/src/skills/manager.ts +140 -18
  161. package/src/types/highlight-shims.d.ts +14 -0
  162. package/src/types.ts +16 -1
  163. package/src/ui/StatusBar.ts +1 -1
  164. package/tsconfig.json +1 -0
  165. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
package/LICENSE CHANGED
@@ -1,6 +1,10 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 SimpleLittleDev
4
+ Copyright (c) 2026 FastCode-Termux contributors
5
+
6
+ This project is a fix and Termux adaptation based on SimpleLittleDev/FastCode,
7
+ originally licensed under MIT.
4
8
 
5
9
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
10
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -16,18 +16,23 @@
16
16
  </p>
17
17
 
18
18
  ```bash
19
- npm install -g @datename/fastcode-termux
19
+ npm install -g @datename/fastcode-termux@0.1.8-beta.0
20
20
  fastcode
21
21
  ```
22
22
 
23
+ > Registry check (2026-11-09): npm still reports `latest=0.1.6` / `beta=0.1.6-beta`. If `@0.1.8-beta.0` is not resolvable yet, use the repo/portable build or `@0.1.6-beta`.
24
+
23
25
  Short alias included:
24
26
 
25
27
  ```bash
26
28
  fc
27
29
  ```
28
30
 
29
- This tree is **0.1.6-beta**. When the beta is published to npm, install it with
30
- `npm install -g @datename/fastcode-termux@0.1.6-beta`; until then use the repo/portable build.
31
+ This tree is **0.1.8-beta.0**, based on tag `v0.1.7` plus the 2026-11-09
32
+ fixes (shell safety, `/threads`, exit protection, approval feedback,
33
+ compaction/projection, TUI effort, license). It is built and packaged locally
34
+ but not published. The npm registry still reports `latest=0.1.6` /
35
+ `beta=0.1.6-beta` (checked 2026-11-09); `release/` contains 0.1.8-beta.0 assets.
31
36
 
32
37
  ---
33
38
 
@@ -83,7 +88,7 @@ npm link
83
88
  ### Global install
84
89
 
85
90
  ```bash
86
- npm install -g @datename/fastcode-termux
91
+ npm install -g @datename/fastcode-termux@0.1.8-beta.0
87
92
  fastcode
88
93
  ```
89
94
 
@@ -136,7 +141,7 @@ fastcode
136
141
  | `google` | `gemini-1.5-flash` | `GEMINI_API_KEY` or `GOOGLE_API_KEY` |
137
142
  | `openrouter` | `anthropic/claude-3.5-sonnet` | `OPENROUTER_API_KEY` |
138
143
  | `groq` | `llama-3.3-70b-versatile` | `GROQ_API_KEY` |
139
- | `deepseek` | `deepseek-chat` | `DEEPSEEK_API_KEY` |
144
+ | `deepseek` | `deepseek-flash` | `DEEPSEEK_API_KEY` |
140
145
  | `mistral` | `mistral-small-latest` | `MISTRAL_API_KEY` |
141
146
  | `ollama` | `llama3.2` | Local, no API key required |
142
147
 
@@ -240,8 +245,29 @@ Installed plugins live under:
240
245
 
241
246
  ---
242
247
 
248
+ ## ⌨️ CLI Commands
249
+
250
+ These subcommands do not start a conversation:
251
+
252
+ | Command | Action |
253
+ | --- | --- |
254
+ | `fastcode` / `fastcode "prompt"` | open TUI / open with an initial prompt |
255
+ | `fastcode --version` / `-v` / `--help` / `-h` | version / all commands |
256
+ | `fastcode provider [id]` / `model [id]` | list or set default provider / model |
257
+ | `fastcode config` / `config path` | config + paths / config.json path only |
258
+ | `fastcode sessions` / `sessions show <id>` / `resume [id\|latest]` | list / inspect / resume sessions |
259
+ | `fastcode trust [dir]` / `untrust [dir]` / `trusted` / `trust --forget <key>` | trust management |
260
+ | `fastcode skills list\|install <id>\|uninstall <id>\|search <q>` | skill management |
261
+ | `fastcode doctor` / `doctor --net` | diagnostics / probe registry |
262
+ | `fastcode update` | check for updates |
263
+ | `--ui tty\|native\|plain\|json` | approval/render backend |
264
+
265
+ ---
266
+
243
267
  ## 🎮 Slash Commands
244
268
 
269
+ These run in the TUI without sending a model request:
270
+
245
271
  | Command | Action |
246
272
  | --- | --- |
247
273
  | `/help` | Show command list |
@@ -260,7 +286,7 @@ Installed plugins live under:
260
286
  | `/system [text]` | Show or update system prompt |
261
287
  | `/whoami` | Show configured provider keys |
262
288
  | `/config` | Print config and paths |
263
- | `/clear` | Clear conversation |
289
+ | `/clear` (`cls`) | Clear conversation |
264
290
  | `/reset` | Reset config |
265
291
  | `/about` | Show version and platform |
266
292
  | `/context` | Token estimate, compaction threshold, heap |
@@ -268,16 +294,17 @@ Installed plugins live under:
268
294
  | `/cancel` | Abort the active run (mobile alternative to Esc) |
269
295
  | `/steer <text>` | Guide the next request of an active run |
270
296
  | `/jobs` / `/attach <id>` / `/kill <id>` | Background shell sessions |
297
+ | `/threads [1-6]` | Show or set resident shell concurrency (default 2, max 6) |
271
298
  | `/expand` | Toggle full/collapsed tool output |
272
299
  | `/edit` | Edit the prompt in `$EDITOR` (works around Ink IME bugs) |
273
300
  | `/bg` | Explain background shell behaviour |
274
- | `/exit` | Quit |
301
+ | `/exit` | Abort the active run, kill resident shells, then quit |
275
302
 
276
303
  Keyboard controls:
277
304
 
278
305
  ```text
279
- Esc cancel streaming
280
- Ctrl+C press twice to quit
306
+ Esc cancel streaming; partial output is kept
307
+ Ctrl+C during a run: first cancels, second quits; idle: press twice to quit
281
308
  ```
282
309
 
283
310
  ---
@@ -308,10 +335,12 @@ Install Node.js first:
308
335
 
309
336
  ```bash
310
337
  pkg install nodejs
311
- npm install -g @datename/fastcode-termux
338
+ npm install -g @datename/fastcode-termux@0.1.8-beta.0
312
339
  fastcode
313
340
  ```
314
341
 
342
+ > If npm does not resolve `@0.1.8-beta.0` yet, install the portable bundle or use `@0.1.6-beta`.
343
+
315
344
  If the command is not found, check your npm global binary path:
316
345
 
317
346
  ```bash
@@ -393,19 +422,21 @@ npm publish --access public
393
422
 
394
423
  ---
395
424
 
396
- ## Safety, sessions and limits (0.1.6-beta)
425
+ ## Safety, sessions and limits (0.1.8-beta.0)
397
426
 
398
427
  | Feature | Behaviour | Override |
399
428
  | --- | --- | --- |
400
429
  | Project trust | First run in a folder asks before agent tools load; the list lives in ~/.fastcode/trusted.json | fastcode trust / untrust / trusted, FASTCODE_TRUST_ALL=1 |
401
- | Dangerous-command guard | rm -rf, del /f, rmdir /s, format / mkfs / diskpart, dd if=, writes to /dev/sd, shutdown / reboot / poweroff and fork bombs are refused before execution | FASTCODE_ALLOW_DANGEROUS=1, FASTCODE_SHELL_ALLOWLIST, FASTCODE_SHELL_DENY_EXTRA |
430
+ | Dangerous-command guard | Hard Block for destructive/system/sensitive-file commands and credential exfil; `.env` writes/deletes/moves, critical credential reads and egress are non-overridable | FASTCODE_ALLOW_DANGEROUS=1 only for Soft Gate |
431
+ | Egress policy | application-layer tool/payload/output policy; default audit-only, `enabled:true + mode:"block"` returns `EGRESS_DENY` | `config.egress`: enabled, mode, denyPaths, denyGlobs, denyContentMarkers, maxScanBytes |
402
432
  | Unified tool-output cap | Output is truncated while it is produced, not sliced afterwards (default 30000 characters) | FASTCODE_TOOL_OUTPUT_LIMIT |
403
433
  | Session log | Append-only JSONL in ~/.fastcode/sessions, resumable after a crash (cache cap 256MB by default) | fastcode sessions, fastcode resume, FASTCODE_SESSION_LOG=0, FASTCODE_SESSION_CACHE_MB |
404
- | Checkpoints/projection | `*.projection.json` (<20KB) with tool/model/cwd fingerprint; unpaired tool calls are completed on resume | FASTCODE_FSYNC, FASTCODE_SESSION_LOG |
434
+ | Checkpoints/projection | `*.projection.json` (~200KB cap) with tool/model/cwd fingerprint; tool payloads folded to edit facts, dropped-message markers, summary markers protected; unpaired tool calls completed on resume | FASTCODE_FSYNC, FASTCODE_SESSION_LOG |
435
+ | Exit save point | `/exit` aborts the run, stops shells, then writes `session_end{reason,compaction}` + projection and best-effort fsyncs both; busy runs require a second `/exit` within 5s; Esc/Ctrl+C keep partial output | `/exit` (`/quit`, `/q`), double Ctrl+C |
405
436
  | Patch transaction | parse → plan → apply; CRLF/mode kept; symlinks refused; move never overwrites | `/expand` for the full diff |
406
- | Process tree | abort or `/kill` kills children/grandchildren | POSIX process group, Windows `taskkill /T` |
407
- | Compaction | token threshold or heap pressure; summary written to `summary-*.md` | FASTCODE_COMPACTION=0, FASTCODE_HEAP_LIMIT_MB |
408
- | Background shell | Commands over ~2s return a session id; poll incrementally | `/jobs`, `/attach`, `/kill`, FASTCODE_MAX_SPAWN_PER_TURN |
437
+ | Process tree | abort or `/kill` kills children/grandchildren; `/exit` uses SIGTERM → wait → SIGKILL for resident shells | POSIX process group, Windows `taskkill /T` |
438
+ | Compaction | token/heap threshold; Notes/Outcomes/Files first; summary written to `summary-*.md`; `/compact` records a `compacted` fact; old summaries cleaned | FASTCODE_COMPACTION=0, FASTCODE_COMPACTION_RATIO, FASTCODE_COMPACTION_KEEP_TURNS, FASTCODE_COMPACTION_SUMMARY_TOKENS, FASTCODE_HEAP_LIMIT_MB |
439
+ | Background shell | Commands over ~2s return a session id; poll incrementally. `/threads` caps the **resident** pool (default 2, max 6); one-shot commands use a separate `FASTCODE_MAX_SPAWN_PER_TURN` budget and are not globally capped | `/jobs`, `/attach`, `/kill`, `/threads`, FASTCODE_MAX_SPAWN_PER_TURN |
409
440
  | Approval hook | Sensitive/large patches need approval; without a resolver they are denied (fail closed) | pending UI not implemented yet |
410
441
 
411
442
  ## 🙏 Credits
@@ -416,4 +447,6 @@ npm publish --access public
416
447
 
417
448
  ## 📜 License
418
449
 
419
- MIT © SimpleLittleDev
450
+ MIT. Original FastCode © 2026 SimpleLittleDev; this fix/Termux adaptation © 2026
451
+ FastCode-Termux contributors. Based on `SimpleLittleDev/FastCode`
452
+ (`@simplelittledev/fastcode` 0.1.3), originally MIT. See `LICENSE`.
@@ -0,0 +1,43 @@
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, ApprovalRequest } from "./approval.js";
9
+ export declare const GATE_PROTOCOL_VERSION = 1;
10
+ export interface GateAskEvent {
11
+ event: "gate:ask";
12
+ version: number;
13
+ id: string;
14
+ tier: number;
15
+ action: string;
16
+ reason: string;
17
+ prompt: string;
18
+ source: string;
19
+ targets: string[];
20
+ diffstat: {
21
+ files: number;
22
+ lines: number;
23
+ };
24
+ options: Array<{
25
+ id: string;
26
+ label: string;
27
+ kind: string;
28
+ default?: boolean;
29
+ }>;
30
+ default: string;
31
+ require_input?: string;
32
+ }
33
+ export declare function encodeGateAsk(request: ApprovalRequest): GateAskEvent;
34
+ export interface GateDecisionResult {
35
+ decision: ApprovalDecision;
36
+ error?: string;
37
+ }
38
+ /**
39
+ * Accept only a well-formed decision for this exact request id and one of its
40
+ * stable option ids. Malformed JSON, wrong ids, unknown options and missing
41
+ * required input all fail closed.
42
+ */
43
+ export declare function parseGateDecision(request: ApprovalRequest, line: string): GateDecisionResult;
@@ -0,0 +1,69 @@
1
+ import { decisionForOption } from "./approval.js";
2
+ export const GATE_PROTOCOL_VERSION = 1;
3
+ function optionId(option, index) {
4
+ return option.id ?? `${option.kind}-${index}`;
5
+ }
6
+ function targetsFor(request) {
7
+ const args = request.args ?? {};
8
+ const values = [args.path, args.from, args.to, args.cwd, request.preview?.kind === "diff" ? "diff" : undefined];
9
+ return values.filter((value) => typeof value === "string" && value.length > 0);
10
+ }
11
+ export function encodeGateAsk(request) {
12
+ const options = request.options.map((option, index) => ({
13
+ id: optionId(option, index),
14
+ label: option.label,
15
+ kind: option.kind,
16
+ ...(index === (request.defaultIndex ?? 0) ? { default: true } : {}),
17
+ }));
18
+ const defaultIndex = Math.max(0, Math.min(request.defaultIndex ?? 0, options.length - 1));
19
+ const text = request.preview?.kind === "diff" ? request.preview.text : "";
20
+ const feedbackOption = request.options.find((option) => option.kind === "feedback");
21
+ const requireInput = request.freeInput?.prompt ?? feedbackOption?.prompt;
22
+ return {
23
+ event: "gate:ask",
24
+ version: GATE_PROTOCOL_VERSION,
25
+ id: request.id,
26
+ tier: request.tier ?? 2,
27
+ action: request.tool,
28
+ reason: request.reason,
29
+ prompt: request.prompt ?? "Approval required",
30
+ source: request.source ?? "system",
31
+ targets: targetsFor(request),
32
+ diffstat: { files: text ? text.split(/\r?\n/).filter(Boolean).length : 0, lines: text ? text.split(/\r?\n/).length : 0 },
33
+ options,
34
+ default: options[defaultIndex]?.id ?? "",
35
+ ...(requireInput ? { require_input: requireInput } : {}),
36
+ };
37
+ }
38
+ /**
39
+ * Accept only a well-formed decision for this exact request id and one of its
40
+ * stable option ids. Malformed JSON, wrong ids, unknown options and missing
41
+ * required input all fail closed.
42
+ */
43
+ export function parseGateDecision(request, line) {
44
+ let payload;
45
+ try {
46
+ payload = JSON.parse(line);
47
+ }
48
+ catch {
49
+ return { decision: { kind: "deny", reason: "malformed JSON decision" }, error: "malformed JSON" };
50
+ }
51
+ if (!payload || typeof payload !== "object" || payload.version !== GATE_PROTOCOL_VERSION)
52
+ return { decision: { kind: "deny", reason: "unsupported gate protocol version" }, error: "version" };
53
+ if (payload.event !== "gate:decision")
54
+ return { decision: { kind: "deny", reason: "unexpected gate event" }, error: "event" };
55
+ if (payload.id !== request.id)
56
+ return { decision: { kind: "deny", reason: "decision id does not match the pending request" }, error: "id" };
57
+ if (request.freeInput?.enabled && (typeof payload.input !== "string" || payload.input.trim() === ""))
58
+ return { decision: { kind: "deny", reason: "required free input is missing" }, error: "input" };
59
+ const option = request.options.find((entry, index) => optionId(entry, index) === payload.option);
60
+ if (!option)
61
+ return { decision: { kind: "deny", reason: "unknown approval option id" }, error: "option" };
62
+ if (option.kind === "feedback") {
63
+ const text = typeof payload.input === "string" ? payload.input.trim() : "";
64
+ if (!text)
65
+ return { decision: { kind: "deny", reason: "feedback input is required" }, error: "feedback" };
66
+ return { decision: { kind: "feedback", text } };
67
+ }
68
+ return { decision: decisionForOption(option) };
69
+ }
@@ -0,0 +1,3 @@
1
+ import { type ApprovalDecision, type ApprovalRequest } from "./approval.js";
2
+ export declare function nativeDecisionFromPayload(request: ApprovalRequest, payload: any): ApprovalDecision | undefined;
3
+ export declare function nativeApproval(request: ApprovalRequest, signal?: AbortSignal): Promise<ApprovalDecision | undefined>;
@@ -0,0 +1,165 @@
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 } from "./approval.js";
10
+ export function nativeDecisionFromPayload(request, payload) {
11
+ if (payload?.code === -1)
12
+ return { kind: "deny", reason: "termux-dialog cancelled" };
13
+ if (payload?.code !== 0 || !Number.isInteger(payload.index) ||
14
+ payload.index < 0 || payload.index >= request.options.length)
15
+ return { kind: "deny", reason: "termux-dialog returned an invalid selection" };
16
+ return decisionForOption(request.options[payload.index]);
17
+ }
18
+ async function nativeFeedback(prompt, signal, timeoutMs = 60_000) {
19
+ if (signal?.aborted)
20
+ return undefined;
21
+ return await new Promise((resolve) => {
22
+ let settled = false;
23
+ let child;
24
+ let timer;
25
+ const finish = (value) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ if (timer)
30
+ clearTimeout(timer);
31
+ signal?.removeEventListener("abort", onAbort);
32
+ resolve(value);
33
+ };
34
+ const onAbort = () => {
35
+ try {
36
+ child?.kill("SIGKILL");
37
+ }
38
+ catch {
39
+ // Already gone.
40
+ }
41
+ finish(undefined);
42
+ };
43
+ signal?.addEventListener("abort", onAbort, { once: true });
44
+ child = spawn("termux-dialog", ["text", "-t", prompt], {
45
+ stdio: ["ignore", "pipe", "ignore"],
46
+ windowsHide: true,
47
+ });
48
+ let stdout = "";
49
+ child.stdout?.setEncoding("utf8");
50
+ child.stdout?.on("data", (chunk) => {
51
+ stdout += chunk;
52
+ });
53
+ child.on("error", () => finish(undefined));
54
+ child.on("close", () => {
55
+ let payload;
56
+ try {
57
+ payload = JSON.parse(stdout.trim());
58
+ }
59
+ catch {
60
+ finish(undefined);
61
+ return;
62
+ }
63
+ if (payload?.code === 0 && typeof payload.text === "string")
64
+ finish(payload.text);
65
+ else
66
+ finish(undefined);
67
+ });
68
+ timer = setTimeout(() => {
69
+ try {
70
+ child?.kill("SIGKILL");
71
+ }
72
+ catch {
73
+ // Already gone.
74
+ }
75
+ finish(undefined);
76
+ }, Math.max(1000, timeoutMs));
77
+ timer.unref?.();
78
+ });
79
+ }
80
+ export async function nativeApproval(request, signal) {
81
+ if (signal?.aborted)
82
+ return { kind: "deny", reason: "approval cancelled" };
83
+ const labels = request.options.map((option) => option.label);
84
+ const title = request.prompt ?? `Approval: ${request.tool}`;
85
+ return await new Promise((resolve) => {
86
+ let settled = false;
87
+ let child;
88
+ let timer;
89
+ const finish = (value) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ if (timer)
94
+ clearTimeout(timer);
95
+ signal?.removeEventListener("abort", onAbort);
96
+ resolve(value);
97
+ };
98
+ const onAbort = () => {
99
+ try {
100
+ child?.kill("SIGKILL");
101
+ }
102
+ catch {
103
+ // Already gone.
104
+ }
105
+ finish({ kind: "deny", reason: "approval cancelled" });
106
+ };
107
+ signal?.addEventListener("abort", onAbort, { once: true });
108
+ child = spawn("termux-dialog", ["sheet", "-v", labels.join(","), "-t", title], {
109
+ stdio: ["ignore", "pipe", "ignore"],
110
+ windowsHide: true,
111
+ });
112
+ let stdout = "";
113
+ child.stdout?.setEncoding("utf8");
114
+ child.stdout?.on("data", (chunk) => {
115
+ stdout += chunk;
116
+ });
117
+ child.on("error", () => finish(undefined));
118
+ child.on("close", async () => {
119
+ let payload;
120
+ try {
121
+ payload = JSON.parse(stdout.trim());
122
+ }
123
+ catch {
124
+ finish(undefined);
125
+ return;
126
+ }
127
+ const selected = payload?.code === 0 && Number.isInteger(payload.index)
128
+ ? request.options[payload.index]
129
+ : undefined;
130
+ if (selected?.kind === "feedback") {
131
+ if (timer) {
132
+ clearTimeout(timer);
133
+ timer = undefined;
134
+ }
135
+ const prompt = selected.prompt || "Why should this be denied?";
136
+ const text = await nativeFeedback(prompt, signal, request.timeoutMs);
137
+ if (signal?.aborted) {
138
+ finish({ kind: "deny", reason: "approval cancelled" });
139
+ return;
140
+ }
141
+ if (typeof text !== "string") {
142
+ // Text dialog unavailable/cancelled: fall back to the TUI prompt.
143
+ finish(undefined);
144
+ return;
145
+ }
146
+ const feedback = text.trim();
147
+ finish(feedback
148
+ ? { kind: "feedback", text: feedback }
149
+ : { kind: "deny", reason: "feedback was empty (fail closed)" });
150
+ return;
151
+ }
152
+ finish(nativeDecisionFromPayload(request, payload));
153
+ });
154
+ timer = setTimeout(() => {
155
+ try {
156
+ child?.kill("SIGKILL");
157
+ }
158
+ catch {
159
+ // Already gone.
160
+ }
161
+ finish(undefined);
162
+ }, Math.max(1000, request.timeoutMs ?? 60_000));
163
+ timer.unref?.();
164
+ });
165
+ }
@@ -0,0 +1,16 @@
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
+ export declare function approvalKey(tool: string, call: ToolCall, cwd: string): string;
14
+ export declare function isPreApproved(key: string, catalogHash: string): boolean;
15
+ export declare function rememberApproval(key: string, catalogHash: string, scope: ApprovalScope): void;
16
+ export declare function clearSessionApprovals(): void;
@@ -0,0 +1,21 @@
1
+ import { toolCallFingerprint } from "./guards.js";
2
+ import { isToolApproved, rememberToolApproval } from "../config/trust.js";
3
+ const sessionApprovals = new Set();
4
+ export function approvalKey(tool, call, cwd) {
5
+ return `${tool}:${toolCallFingerprint(call, cwd)}`;
6
+ }
7
+ export function isPreApproved(key, catalogHash) {
8
+ return sessionApprovals.has(`${catalogHash}:${key}`) || isToolApproved(key, catalogHash);
9
+ }
10
+ export function rememberApproval(key, catalogHash, scope) {
11
+ if (scope === "once")
12
+ return;
13
+ if (scope === "session") {
14
+ sessionApprovals.add(`${catalogHash}:${key}`);
15
+ return;
16
+ }
17
+ rememberToolApproval(key, catalogHash);
18
+ }
19
+ export function clearSessionApprovals() {
20
+ sessionApprovals.clear();
21
+ }
@@ -13,17 +13,27 @@ export type ApprovalPreview = {
13
13
  };
14
14
  export type ApprovalOption = {
15
15
  kind: "allow";
16
+ id?: string;
16
17
  scope: ApprovalScope;
17
18
  label: string;
18
19
  } | {
19
20
  kind: "deny";
21
+ id?: string;
20
22
  label: string;
21
23
  } | {
22
24
  kind: "choose";
25
+ id?: string;
23
26
  label: string;
24
27
  value: string;
28
+ meta?: {
29
+ recommended?: boolean;
30
+ risk?: string;
31
+ files?: number;
32
+ reason?: string;
33
+ };
25
34
  } | {
26
35
  kind: "feedback";
36
+ id?: string;
27
37
  label: string;
28
38
  prompt: string;
29
39
  };
@@ -34,6 +44,16 @@ export interface ApprovalRequest {
34
44
  reason: ApprovalReason;
35
45
  options: ApprovalOption[];
36
46
  preview?: ApprovalPreview;
47
+ tier?: 0 | 1 | 2 | 3;
48
+ prompt?: string;
49
+ source?: "system" | "ai" | "free";
50
+ defaultIndex?: number;
51
+ timeoutMs?: number;
52
+ freeInput?: {
53
+ enabled: true;
54
+ prompt: string;
55
+ hint?: string;
56
+ };
37
57
  }
38
58
  export type ApprovalDecision = {
39
59
  kind: "allow";
@@ -42,9 +62,22 @@ export type ApprovalDecision = {
42
62
  kind: "deny";
43
63
  reason?: string;
44
64
  feedback?: string;
65
+ code?: string;
66
+ target?: string;
45
67
  } | {
46
68
  kind: "pending";
47
69
  request: ApprovalRequest;
70
+ } | {
71
+ kind: "choose";
72
+ value: string;
73
+ input?: string;
74
+ } | {
75
+ kind: "feedback";
76
+ text: string;
48
77
  };
49
78
  export type ApprovalResolver = (request: ApprovalRequest) => ApprovalDecision | Promise<ApprovalDecision>;
50
79
  export declare const DEFAULT_ALLOW_OPTIONS: ApprovalOption[];
80
+ export declare function approvalRequestId(): string;
81
+ export declare function approvalOptionForDecision(request: ApprovalRequest, decision: ApprovalDecision): ApprovalOption | undefined;
82
+ export declare function parseApprovalChoice(request: ApprovalRequest, text: string): ApprovalOption | undefined;
83
+ export declare function decisionForOption(option: ApprovalOption, input?: string): ApprovalDecision;
@@ -1,7 +1,42 @@
1
1
  export const DEFAULT_ALLOW_OPTIONS = [
2
- { kind: "allow", scope: "once", label: "Allow once" },
3
- { kind: "allow", scope: "session", label: "Allow for this session" },
4
- { kind: "allow", scope: "always", label: "Always allow" },
5
- { kind: "deny", label: "Deny" },
6
- { kind: "feedback", label: "Deny with feedback", prompt: "Why should this be denied?" },
2
+ { kind: "allow", id: "allow-once", scope: "once", label: "Allow once" },
3
+ { kind: "allow", id: "allow-session", scope: "session", label: "Allow for this session" },
4
+ { kind: "allow", id: "allow-always", scope: "always", label: "Always allow" },
5
+ { kind: "deny", id: "deny", label: "Deny" },
6
+ { kind: "feedback", id: "feedback", label: "Deny with feedback", prompt: "Why should this be denied?" },
7
7
  ];
8
+ export function approvalRequestId() {
9
+ return "ap_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
10
+ }
11
+ export function approvalOptionForDecision(request, decision) {
12
+ if (decision.kind === "allow")
13
+ return request.options.find((option) => option.kind === "allow" && option.scope === decision.scope);
14
+ if (decision.kind === "deny")
15
+ return request.options.find((option) => option.kind === "deny");
16
+ if (decision.kind === "choose")
17
+ return request.options.find((option) => option.kind === "choose" && option.value === decision.value);
18
+ if (decision.kind === "feedback")
19
+ return request.options.find((option) => option.kind === "feedback");
20
+ return undefined;
21
+ }
22
+ export function parseApprovalChoice(request, text) {
23
+ const index = Number(String(text ?? "").trim()) - 1;
24
+ if (!Number.isInteger(index) || index < 0 || index >= request.options.length)
25
+ return undefined;
26
+ return request.options[index];
27
+ }
28
+ export function decisionForOption(option, input) {
29
+ if (option.kind === "allow")
30
+ return { kind: "allow", scope: option.scope };
31
+ if (option.kind === "choose")
32
+ return { kind: "choose", value: option.value };
33
+ if (option.kind === "feedback") {
34
+ const text = typeof input === "string" ? input.trim() : "";
35
+ // The option's prompt is the question, not the user's answer. Without an
36
+ // actual answer, fail closed instead of sending the prompt back to the model.
37
+ return text
38
+ ? { kind: "feedback", text }
39
+ : { kind: "deny", reason: "feedback was required but not provided (fail closed)" };
40
+ }
41
+ return { kind: "deny", reason: "denied by user" };
42
+ }
@@ -37,3 +37,5 @@ export declare function heapExceeded(config: CompactionConfig): boolean;
37
37
  export declare function shouldCompact(messages: ChatMessage[], config: CompactionConfig, options: CompactionOptions): boolean;
38
38
  export declare function compactMessages(messages: ChatMessage[], config: CompactionConfig, options: CompactionOptions): CompactionResult;
39
39
  export declare function buildSummary(messages: ChatMessage[], config: CompactionConfig, options: Pick<CompactionOptions, "providerId" | "model">): string;
40
+ /** Delete a summary file only when it is one of our summary-*.md artifacts. */
41
+ export declare function removeSummaryFile(file?: string): void;