@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/dist/skills/manager.js
CHANGED
|
@@ -5,21 +5,122 @@
|
|
|
5
5
|
* a SKILL.md file plus optional resource files. FastCode supports three
|
|
6
6
|
* install sources:
|
|
7
7
|
*
|
|
8
|
-
* 1.
|
|
9
|
-
* `/skills install pdf` → resolves via the registry
|
|
10
|
-
* `/skills install owner/repo/skill` → fetches raw SKILL.md
|
|
11
|
-
* 2. Direct https URLs ending in SKILL.md.
|
|
8
|
+
* 1. A registry configured via FASTCODE_SKILLS_REGISTRY or skillsRegistry
|
|
9
|
+
* `/skills install pdf` → resolves via the configured registry
|
|
10
|
+
* `/skills install owner/repo/skill` → fetches raw SKILL.md from the registry
|
|
11
|
+
* 2. Direct https URLs ending in SKILL.md (no registry required).
|
|
12
12
|
* 3. Local paths (just symlinks into ~/.fastcode/skills/).
|
|
13
13
|
*
|
|
14
14
|
* Installed skills can be enabled per-session and are then stitched into
|
|
15
15
|
* the system prompt at chat time, exactly like Claude does internally.
|
|
16
16
|
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
17
18
|
import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, mkdirSync, } from "node:fs";
|
|
18
19
|
import { join } from "node:path";
|
|
19
20
|
import { getPaths } from "../config/paths.js";
|
|
21
|
+
import { invalidateConfigCache } from "../config/store.js";
|
|
20
22
|
import { fetchJson } from "../lib/http.js";
|
|
21
23
|
import { parseSkill } from "./parser.js";
|
|
22
|
-
const
|
|
24
|
+
const SOURCE_FILE = ".fastcode-source";
|
|
25
|
+
const INTEGRITY_VERSION = 1;
|
|
26
|
+
const REGISTRY_REQUIRED = "No skills registry configured. Set one with `/skills registry <url>` or FASTCODE_SKILLS_REGISTRY.";
|
|
27
|
+
/**
|
|
28
|
+
* Pick the skills registry without treating an empty environment variable as
|
|
29
|
+
* "configured". `??` would keep `""` and make env overrides silently break;
|
|
30
|
+
* explicit null/empty checks keep `""` and `" "` on the config path. There is
|
|
31
|
+
* intentionally no built-in default registry.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveRegistry(envValue, configValue) {
|
|
34
|
+
const env = typeof envValue === "string" && envValue.trim() ? envValue.trim() : "";
|
|
35
|
+
const configured = typeof configValue === "string" && configValue.trim() ? configValue.trim() : "";
|
|
36
|
+
return (env || configured).replace(/\/+$/, "");
|
|
37
|
+
}
|
|
38
|
+
function requireRegistry(registry) {
|
|
39
|
+
if (!registry)
|
|
40
|
+
throw new Error(REGISTRY_REQUIRED);
|
|
41
|
+
return registry;
|
|
42
|
+
}
|
|
43
|
+
export function hashSkillDirectory(folder) {
|
|
44
|
+
const entries = skillFiles(folder).sort();
|
|
45
|
+
return createHash("sha256").update(entries.join("\n")).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function skillFiles(folder, prefix = "") {
|
|
48
|
+
const out = [];
|
|
49
|
+
let items;
|
|
50
|
+
try {
|
|
51
|
+
items = readdirSync(folder, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
if (item.name === SOURCE_FILE)
|
|
58
|
+
continue;
|
|
59
|
+
const full = join(folder, item.name);
|
|
60
|
+
const rel = prefix ? prefix + "/" + item.name : item.name;
|
|
61
|
+
if (item.isDirectory()) {
|
|
62
|
+
out.push(...skillFiles(full, rel));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!item.isFile())
|
|
66
|
+
continue;
|
|
67
|
+
const digest = createHash("sha256").update(readFileSync(full)).digest("hex");
|
|
68
|
+
out.push(rel + "\0" + digest);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function readSourceMeta(folder) {
|
|
73
|
+
const sourcePath = join(folder, SOURCE_FILE);
|
|
74
|
+
if (!existsSync(sourcePath))
|
|
75
|
+
return { source: "local", legacy: true };
|
|
76
|
+
const raw = readFileSync(sourcePath, "utf8").trim();
|
|
77
|
+
if (raw.startsWith("{")) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
return {
|
|
81
|
+
source: typeof parsed.source === "string" ? parsed.source : "local",
|
|
82
|
+
sha256: typeof parsed.sha256 === "string" ? parsed.sha256 : undefined,
|
|
83
|
+
legacy: typeof parsed.sha256 !== "string",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Fall through and treat the file as a legacy plain-text source.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { source: raw || "local", legacy: true };
|
|
91
|
+
}
|
|
92
|
+
function writeSourceMeta(folder, source) {
|
|
93
|
+
const sha256 = hashSkillDirectory(folder);
|
|
94
|
+
const payload = {
|
|
95
|
+
version: INTEGRITY_VERSION,
|
|
96
|
+
algorithm: "sha256",
|
|
97
|
+
source,
|
|
98
|
+
sha256,
|
|
99
|
+
};
|
|
100
|
+
writeFileSync(join(folder, SOURCE_FILE), JSON.stringify(payload, null, 2));
|
|
101
|
+
return sha256;
|
|
102
|
+
}
|
|
103
|
+
export function rebaselineSkills(names) {
|
|
104
|
+
const installed = listInstalledSkills();
|
|
105
|
+
const wanted = names && names.length > 0 ? new Set(names) : undefined;
|
|
106
|
+
const results = [];
|
|
107
|
+
let changed = false;
|
|
108
|
+
for (const skill of installed) {
|
|
109
|
+
if (wanted && !wanted.has(skill.name) && !wanted.has(basename(skill.path)))
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
writeSourceMeta(skill.path, skill.source === "local" ? "local" : skill.source);
|
|
113
|
+
results.push({ name: skill.name, ok: true });
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
results.push({ name: skill.name, ok: false, warning: err?.message ?? String(err) });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (changed)
|
|
121
|
+
invalidateConfigCache();
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
23
124
|
/** List skills currently installed under ~/.fastcode/skills. */
|
|
24
125
|
export function listInstalledSkills() {
|
|
25
126
|
const { skills } = getPaths();
|
|
@@ -35,16 +136,20 @@ export function listInstalledSkills() {
|
|
|
35
136
|
continue;
|
|
36
137
|
const raw = readFileSync(md, "utf8");
|
|
37
138
|
const parsed = parseSkill(raw, dir.name);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
139
|
+
const meta = readSourceMeta(folder);
|
|
140
|
+
const actualHash = hashSkillDirectory(folder);
|
|
141
|
+
const integrity = !meta.sha256
|
|
142
|
+
? "unverified"
|
|
143
|
+
: actualHash === meta.sha256 ? "ok" : "drift";
|
|
42
144
|
out.push({
|
|
43
145
|
name: parsed.name || dir.name,
|
|
44
146
|
description: parsed.description,
|
|
45
|
-
source,
|
|
147
|
+
source: meta.source,
|
|
46
148
|
path: folder,
|
|
47
149
|
body: parsed.body,
|
|
150
|
+
integrity,
|
|
151
|
+
sha256: actualHash,
|
|
152
|
+
lockedHash: meta.sha256,
|
|
48
153
|
});
|
|
49
154
|
}
|
|
50
155
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -56,7 +161,7 @@ export function getSkill(name) {
|
|
|
56
161
|
/** Install a skill. Returns the parsed manifest on success. */
|
|
57
162
|
export async function installSkill(spec, opts = {}) {
|
|
58
163
|
const { skills } = getPaths();
|
|
59
|
-
const registry = (opts.registry
|
|
164
|
+
const registry = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], opts.registry);
|
|
60
165
|
let parsed;
|
|
61
166
|
let source;
|
|
62
167
|
let folderName;
|
|
@@ -68,7 +173,8 @@ export async function installSkill(spec, opts = {}) {
|
|
|
68
173
|
}
|
|
69
174
|
else if (looksLikeRepoPath(spec)) {
|
|
70
175
|
// owner/repo or owner/repo/skill — fetch via registry's raw endpoint.
|
|
71
|
-
const
|
|
176
|
+
const base = requireRegistry(registry);
|
|
177
|
+
const url = `${base}/${spec}?format=md`;
|
|
72
178
|
const body = await fetchText(url);
|
|
73
179
|
parsed = parseSkill(body, parts(spec).pop() ?? spec);
|
|
74
180
|
source = url;
|
|
@@ -76,11 +182,12 @@ export async function installSkill(spec, opts = {}) {
|
|
|
76
182
|
}
|
|
77
183
|
else {
|
|
78
184
|
// Single-token name → search the registry.
|
|
79
|
-
const
|
|
185
|
+
const base = requireRegistry(registry);
|
|
186
|
+
const hits = await searchRegistry(spec, base);
|
|
80
187
|
const hit = hits[0];
|
|
81
188
|
if (!hit)
|
|
82
|
-
throw new Error(`No skill found for "${spec}" in ${
|
|
83
|
-
const url = hit.fetchUrl ?? `${
|
|
189
|
+
throw new Error(`No skill found for "${spec}" in ${base}`);
|
|
190
|
+
const url = hit.fetchUrl ?? `${base}/${hit.slug}?format=md`;
|
|
84
191
|
const body = await fetchText(url);
|
|
85
192
|
parsed = parseSkill(body, hit.name);
|
|
86
193
|
source = url;
|
|
@@ -94,13 +201,14 @@ export async function installSkill(spec, opts = {}) {
|
|
|
94
201
|
rmSync(targetDir, { recursive: true, force: true });
|
|
95
202
|
mkdirSync(targetDir, { recursive: true });
|
|
96
203
|
writeFileSync(join(targetDir, "SKILL.md"), reassemble(parsed));
|
|
97
|
-
|
|
204
|
+
writeSourceMeta(targetDir, source);
|
|
98
205
|
return {
|
|
99
206
|
name: parsed.name || folderName,
|
|
100
207
|
description: parsed.description,
|
|
101
208
|
source,
|
|
102
209
|
path: targetDir,
|
|
103
210
|
body: parsed.body,
|
|
211
|
+
integrity: "ok",
|
|
104
212
|
};
|
|
105
213
|
}
|
|
106
214
|
/** Remove an installed skill by name or folder. */
|
|
@@ -125,8 +233,10 @@ export function uninstallSkill(name) {
|
|
|
125
233
|
return false;
|
|
126
234
|
}
|
|
127
235
|
/** Search the registry for skills matching a query. */
|
|
128
|
-
export async function searchRegistry(query, registry
|
|
129
|
-
const base =
|
|
236
|
+
export async function searchRegistry(query, registry) {
|
|
237
|
+
const base = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], registry);
|
|
238
|
+
if (!base)
|
|
239
|
+
return [];
|
|
130
240
|
try {
|
|
131
241
|
const json = await fetchJson(`${base}/api/v1/skills/search?q=${encodeURIComponent(query)}`);
|
|
132
242
|
return json.data ?? [];
|
|
@@ -145,6 +255,13 @@ export function buildSkillsPrompt(enabled) {
|
|
|
145
255
|
const skill = installed.find((s) => s.name === name || basename(s.path) === name);
|
|
146
256
|
if (!skill)
|
|
147
257
|
continue;
|
|
258
|
+
if (skill.integrity !== "ok") {
|
|
259
|
+
const reason = skill.integrity === "drift"
|
|
260
|
+
? "content drift detected"
|
|
261
|
+
: "no integrity baseline (legacy install)";
|
|
262
|
+
process.stderr.write(`skill ${skill.name} ${reason}; disabled. Run /skills verify ${skill.name} --confirm to re-baseline.\n`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
148
265
|
chunks.push(`<skill name="${skill.name}">\n${skill.body.trim()}\n</skill>`);
|
|
149
266
|
}
|
|
150
267
|
if (chunks.length === 0)
|
package/dist/types.d.ts
CHANGED
|
@@ -121,6 +121,15 @@ export interface SlashCommand {
|
|
|
121
121
|
/** Execute the command. Return text to render, or undefined for no output. */
|
|
122
122
|
run(ctx: CommandContext): Promise<CommandResult> | CommandResult;
|
|
123
123
|
}
|
|
124
|
+
export interface CommandCompaction {
|
|
125
|
+
summaryRef?: string;
|
|
126
|
+
source: "local" | "server" | "none";
|
|
127
|
+
beforeTokens: number;
|
|
128
|
+
afterTokens: number;
|
|
129
|
+
droppedMessages: number;
|
|
130
|
+
keepMessages: number;
|
|
131
|
+
reason?: "threshold" | "heap" | "manual";
|
|
132
|
+
}
|
|
124
133
|
export interface CommandResult {
|
|
125
134
|
/** Text rendered as a system message in the transcript. */
|
|
126
135
|
message?: string;
|
|
@@ -130,6 +139,10 @@ export interface CommandResult {
|
|
|
130
139
|
quit?: boolean;
|
|
131
140
|
/** Open an interactive picker after the command runs. */
|
|
132
141
|
picker?: PickerRequest;
|
|
142
|
+
/** Manual /compact result; the app appends the matching compacted fact. */
|
|
143
|
+
compacted?: CommandCompaction;
|
|
144
|
+
/** /clear result; the app appends a cleared fact so resume does not resurrect history. */
|
|
145
|
+
cleared?: boolean;
|
|
133
146
|
}
|
|
134
147
|
export interface CommandContext {
|
|
135
148
|
args: string[];
|
|
@@ -163,6 +176,7 @@ export interface AppActions {
|
|
|
163
176
|
setSystemPrompt(prompt: string): void;
|
|
164
177
|
setSkillsRegistry(url: string): void;
|
|
165
178
|
setReasoningEffort(effort: ReasoningEffort): void;
|
|
179
|
+
setShellPoolSize(size: number): void;
|
|
166
180
|
enableSkill(name: string): void;
|
|
167
181
|
disableSkill(name: string): void;
|
|
168
182
|
enablePlugin(name: string): void;
|
|
@@ -178,6 +192,7 @@ export interface AppActions {
|
|
|
178
192
|
startLogin(options: {
|
|
179
193
|
provider?: string;
|
|
180
194
|
model?: string;
|
|
195
|
+
flow?: "provider" | "model" | "login";
|
|
181
196
|
}): void;
|
|
182
197
|
}
|
|
183
198
|
export interface SkillManifest {
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -9,7 +9,7 @@ export const StatusBar = ({ theme, providerId, model, inputTokens, outputTokens,
|
|
|
9
9
|
const providerModel = `${providerId}/${model}`;
|
|
10
10
|
const label = compact ? truncate(providerModel, Math.max(18, columns - 28)) : providerModel;
|
|
11
11
|
const effort = reasoningEffort && reasoningEffort !== "auto" ? `effort:${reasoningEffort}` : "";
|
|
12
|
-
return (_jsxs(Box, { flexDirection: compact ? "column" : "row", justifyContent: "space-between", borderStyle: "round", borderColor: theme.border, paddingX: 1, children: [_jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: "\u25CF" }), " ", label, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), inputTokens, "\u2191 ", outputTokens, "\u2193 ", cost, effort ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), effort] })) : null, !compact ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), "skills: ", enabledSkillCount, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), platformLabel()] })) : null] }), _jsx(Text, { color: theme.muted, children: compact
|
|
12
|
+
return (_jsxs(Box, { flexDirection: compact ? "column" : "row", justifyContent: "space-between", borderStyle: "round", borderColor: theme.border, paddingX: 1, children: [_jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: "\u25CF" }), " ", label, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), inputTokens, "\u2191 ", outputTokens, "\u2193 ", cost, !compact && effort ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), effort] })) : null, !compact ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), "skills: ", enabledSkillCount, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), platformLabel()] })) : null] }), _jsx(Text, { color: theme.muted, children: compact
|
|
13
13
|
? `${effort ? effort + " \u00B7 " : ""}${platformLabel()} \u00B7 skills:${enabledSkillCount} \u00B7 ${hint ?? "/help \u00B7 esc \u00B7 ctrl+c"}`
|
|
14
14
|
: hint ?? "/help \u00B7 esc cancel \u00B7 ctrl+c quit" })] }));
|
|
15
15
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datename/fastcode-termux",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8-beta.0",
|
|
4
4
|
"description": "FastCode — a blazing-fast TUI coding assistant. Multi-LLM, skills.sh skills, slash commands. Works on Termux, Linux, Windows, and macOS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"node": ">=18.17"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc -p tsconfig.json && node scripts/build.mjs",
|
|
24
|
+
"build": "tsc -p tsconfig.build.json && node scripts/build.mjs",
|
|
25
25
|
"build:bundle": "node scripts/build.mjs",
|
|
26
26
|
"dev": "tsx src/index.ts",
|
|
27
27
|
"start": "node bin/fastcode.mjs",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"url": "https://github.com/dateName/fastcode-termux/issues"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
+
"bash-parser": "^0.5.0",
|
|
68
69
|
"boxen": "^7.1.1",
|
|
69
70
|
"chalk": "^5.3.0",
|
|
70
71
|
"cli-highlight": "^2.1.11",
|
|
@@ -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
|
+
}
|