@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.
- package/LICENSE +4 -0
- package/README.md +50 -17
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +33 -0
- package/dist/agent/approval.js +40 -5
- package/dist/agent/compaction.d.ts +2 -0
- package/dist/agent/compaction.js +60 -17
- package/dist/agent/guards.d.ts +16 -16
- package/dist/agent/guards.js +978 -24
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +20 -0
- package/dist/agent/process-tree.js +81 -10
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +165 -38
- package/dist/agent/registry.d.ts +3 -1
- package/dist/agent/registry.js +168 -11
- package/dist/agent/runner.d.ts +21 -0
- package/dist/agent/runner.js +44 -5
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +14 -0
- package/dist/agent/shell-session.js +251 -40
- package/dist/agent/tool-schemas.js +26 -7
- package/dist/agent/tools/append_file.js +3 -1
- package/dist/agent/tools/apply_patch.js +44 -4
- package/dist/agent/tools/copy_file.js +26 -3
- package/dist/agent/tools/delete_file.js +22 -3
- package/dist/agent/tools/grep.js +35 -4
- package/dist/agent/tools/make_dir.js +3 -1
- package/dist/agent/tools/move_path.d.ts +7 -0
- package/dist/agent/tools/move_path.js +31 -5
- package/dist/agent/tools/read_file.js +63 -3
- package/dist/agent/tools/read_many.js +21 -3
- package/dist/agent/tools/readers.d.ts +53 -1
- package/dist/agent/tools/readers.js +236 -26
- package/dist/agent/tools/replace_in_file.js +38 -5
- package/dist/agent/tools/shell.js +2 -2
- package/dist/agent/tools/types.d.ts +26 -0
- package/dist/agent/tools/validate.d.ts +15 -2
- package/dist/agent/tools/validate.js +66 -3
- package/dist/agent/tools/write_file.js +3 -1
- package/dist/app.d.ts +2 -1
- package/dist/app.js +526 -85
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +397 -337
- package/dist/commands/about.js +1 -1
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +12 -0
- package/dist/commands/compact.js +14 -2
- package/dist/commands/context.js +5 -4
- package/dist/commands/index.js +2 -0
- package/dist/commands/jobs.js +3 -2
- package/dist/commands/kill.js +3 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.js +15 -15
- package/dist/commands/model.d.ts +1 -13
- package/dist/commands/model.js +6 -38
- package/dist/commands/provider.d.ts +1 -15
- package/dist/commands/provider.js +4 -22
- package/dist/commands/skills.js +34 -4
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/config/store.d.ts +21 -0
- package/dist/config/store.js +47 -9
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +41 -2
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +19 -0
- package/dist/lib/diagnostics.js +32 -6
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/openaiCompatible.d.ts +24 -2
- package/dist/providers/openaiCompatible.js +20 -6
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/session/log.d.ts +14 -2
- package/dist/session/log.js +56 -27
- package/dist/session/projection.d.ts +34 -1
- package/dist/session/projection.js +315 -46
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +15 -0
- package/dist/ui/StatusBar.js +1 -1
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +58 -11
- package/src/agent/compaction.ts +62 -17
- package/src/agent/guards.ts +1049 -25
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +105 -23
- package/src/agent/protocol.ts +187 -39
- package/src/agent/registry.ts +186 -14
- package/src/agent/runner.ts +50 -7
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +265 -41
- package/src/agent/tool-schemas.ts +26 -7
- package/src/agent/tools/append_file.ts +3 -1
- package/src/agent/tools/apply_patch.ts +45 -4
- package/src/agent/tools/copy_file.ts +27 -3
- package/src/agent/tools/delete_file.ts +22 -3
- package/src/agent/tools/grep.ts +36 -4
- package/src/agent/tools/make_dir.ts +3 -1
- package/src/agent/tools/move_path.ts +38 -5
- package/src/agent/tools/read_file.ts +64 -3
- package/src/agent/tools/read_many.ts +21 -3
- package/src/agent/tools/readers.ts +289 -26
- package/src/agent/tools/replace_in_file.ts +39 -5
- package/src/agent/tools/shell.ts +2 -2
- package/src/agent/tools/types.ts +43 -1
- package/src/agent/tools/validate.ts +73 -4
- package/src/agent/tools/write_file.ts +3 -1
- package/src/app.ts +533 -82
- package/src/{cli.ts → cli-main.ts} +34 -6
- package/src/commands/about.ts +1 -1
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +14 -2
- package/src/commands/context.ts +5 -4
- package/src/commands/index.ts +2 -0
- package/src/commands/jobs.ts +3 -2
- package/src/commands/kill.ts +3 -1
- package/src/commands/login.ts +15 -15
- package/src/commands/model.ts +6 -38
- package/src/commands/provider.ts +4 -22
- package/src/commands/skills.ts +34 -4
- package/src/commands/threads.ts +28 -0
- package/src/config/store.ts +47 -9
- package/src/config/trust.ts +45 -2
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +32 -6
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/providers/deepseek.ts +1 -1
- package/src/providers/openaiCompatible.ts +47 -10
- package/src/providers/spec.ts +42 -0
- package/src/session/log.ts +70 -27
- package/src/session/projection.ts +360 -47
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +16 -1
- package/src/ui/StatusBar.ts +1 -1
- package/tsconfig.json +1 -0
- /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.
|
|
30
|
-
|
|
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-
|
|
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` |
|
|
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.
|
|
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 |
|
|
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` (
|
|
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
|
|
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
|
+
}
|
package/dist/agent/approval.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent/approval.js
CHANGED
|
@@ -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;
|