@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
|
@@ -12,10 +12,11 @@ import { loadConfig, saveConfig, resolveProviderKey } from "./config/store.js";
|
|
|
12
12
|
import { getPaths } from "./config/paths.js";
|
|
13
13
|
import { PROVIDERS, getProvider } from "./providers/index.js";
|
|
14
14
|
import { collectDiagnostics, formatDiagnostics } from "./lib/diagnostics.js";
|
|
15
|
+
import { probeConnectivity } from "./lib/http.js";
|
|
15
16
|
import { existsSync } from "node:fs";
|
|
16
17
|
import { latestSessionFile, listSessions, projectionPath, readSession, resolveSessionFile } from "./session/log.js";
|
|
17
18
|
import { readProjection, sessionMetaFromFile } from "./session/projection.js";
|
|
18
|
-
import { listTrusted, trustProject, untrustProject } from "./config/trust.js";
|
|
19
|
+
import { forgetToolApproval, listToolApprovals, listTrusted, trustProject, untrustProject } from "./config/trust.js";
|
|
19
20
|
const NL = String.fromCharCode(10);
|
|
20
21
|
const HELP = [
|
|
21
22
|
"",
|
|
@@ -39,7 +40,8 @@ const HELP = [
|
|
|
39
40
|
" Trust (first-run safety)",
|
|
40
41
|
" $ fastcode trust [dir] Trust a folder (enables agent tools)",
|
|
41
42
|
" $ fastcode untrust [dir] Revoke trust for a folder",
|
|
42
|
-
" $ fastcode trusted List trusted folders",
|
|
43
|
+
" $ fastcode trusted List trusted folders and remembered approvals",
|
|
44
|
+
" $ fastcode trust --forget <key> Revoke one remembered approval",
|
|
43
45
|
"",
|
|
44
46
|
" Skills & maintenance",
|
|
45
47
|
" $ fastcode skills list Print installed skills",
|
|
@@ -47,6 +49,7 @@ const HELP = [
|
|
|
47
49
|
" $ fastcode skills uninstall <id> Remove an installed skill",
|
|
48
50
|
" $ fastcode skills search <q> Search the registry",
|
|
49
51
|
" $ fastcode doctor Environment diagnostics (Termux/mobile)",
|
|
52
|
+
" $ fastcode doctor --net Also probe registry connectivity",
|
|
50
53
|
" $ fastcode update Check for a newer release",
|
|
51
54
|
"",
|
|
52
55
|
" Options",
|
|
@@ -63,16 +66,26 @@ const HELP = [
|
|
|
63
66
|
" DASHSCOPE_API_KEY) or from /login inside the TUI.",
|
|
64
67
|
" Override the data directory with FASTCODE_HOME.",
|
|
65
68
|
"",
|
|
69
|
+
" Approval UI: --ui tty|native|plain|json (default tty)",
|
|
70
|
+
"",
|
|
66
71
|
].join(NL);
|
|
67
72
|
export async function parseCli(): Promise<any> {
|
|
68
73
|
const cli = meow(HELP, {
|
|
69
74
|
importMeta: import.meta,
|
|
70
75
|
flags: {
|
|
71
76
|
version: { type: "boolean", shortFlag: "v" },
|
|
77
|
+
ui: { type: "string", default: "tty" },
|
|
78
|
+
forget: { type: "string" },
|
|
79
|
+
net: { type: "boolean", default: false },
|
|
72
80
|
},
|
|
73
81
|
autoHelp: true,
|
|
74
82
|
autoVersion: true,
|
|
75
83
|
});
|
|
84
|
+
const uiMode = String(cli.flags.ui ?? "tty").toLowerCase();
|
|
85
|
+
if (!["tty", "native", "plain", "json"].includes(uiMode)) {
|
|
86
|
+
console.error("Unknown --ui mode: " + uiMode + " (use tty, native, plain, or json)");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
76
89
|
// First positional dictates the sub-command. Anything else is a prompt.
|
|
77
90
|
const [first, ...rest] = cli.input;
|
|
78
91
|
if (first === "provider") {
|
|
@@ -214,12 +227,22 @@ export async function parseCli(): Promise<any> {
|
|
|
214
227
|
if (first === "trust" || first === "untrust" || first === "trusted") {
|
|
215
228
|
if (first === "trusted") {
|
|
216
229
|
const all = listTrusted();
|
|
217
|
-
if (all.length === 0)
|
|
230
|
+
if (all.length === 0)
|
|
218
231
|
console.log("No trusted folders yet. Run: fastcode trust");
|
|
219
|
-
return { done: true };
|
|
220
|
-
}
|
|
221
232
|
for (const entry of all)
|
|
222
233
|
console.log(" " + entry.path + " (" + entry.addedAt + ")");
|
|
234
|
+
const approvals = listToolApprovals();
|
|
235
|
+
if (approvals.length > 0) {
|
|
236
|
+
console.log("");
|
|
237
|
+
console.log("Remembered tool approvals:");
|
|
238
|
+
for (const entry of approvals)
|
|
239
|
+
console.log(" " + entry.key + " catalog=" + entry.catalogHash);
|
|
240
|
+
}
|
|
241
|
+
return { done: true };
|
|
242
|
+
}
|
|
243
|
+
if (first === "trust" && typeof cli.flags.forget === "string") {
|
|
244
|
+
const key = String(cli.flags.forget);
|
|
245
|
+
console.log(forgetToolApproval(key) ? "Forgot approval: " + key : "No such approval: " + key);
|
|
223
246
|
return { done: true };
|
|
224
247
|
}
|
|
225
248
|
const dir = rest[0] ?? process.cwd();
|
|
@@ -285,6 +308,11 @@ export async function parseCli(): Promise<any> {
|
|
|
285
308
|
}
|
|
286
309
|
if (first === "doctor") {
|
|
287
310
|
console.log(formatDiagnostics(collectDiagnostics()));
|
|
311
|
+
if (cli.flags.net === true) {
|
|
312
|
+
const url = "https://registry.npmjs.org/";
|
|
313
|
+
const reachable = await probeConnectivity(url);
|
|
314
|
+
console.log(` network ${reachable ? "reachable" : "unreachable"} (${url})`);
|
|
315
|
+
}
|
|
288
316
|
return { done: true };
|
|
289
317
|
}
|
|
290
318
|
if (first === "update") {
|
|
@@ -293,7 +321,7 @@ export async function parseCli(): Promise<any> {
|
|
|
293
321
|
}
|
|
294
322
|
// No sub-command → treat all positionals as the initial prompt.
|
|
295
323
|
const prompt = cli.input.length > 0 ? cli.input.join(" ") : undefined;
|
|
296
|
-
return prompt ? { prompt, done: false } : { done: false };
|
|
324
|
+
return prompt ? { prompt, done: false, ui: uiMode } : { done: false, ui: uiMode };
|
|
297
325
|
}
|
|
298
326
|
function compareVersions(a, b) {
|
|
299
327
|
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
package/src/commands/about.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const aboutCommand = {
|
|
|
9
9
|
`Platform: ${platformLabel()} • Node ${process.version}`,
|
|
10
10
|
``,
|
|
11
11
|
`GitHub: https://github.com/dateName/fastcode-termux`,
|
|
12
|
-
`Skills:
|
|
12
|
+
`Skills: configure a registry with /skills registry <url>`,
|
|
13
13
|
];
|
|
14
14
|
return { message: lines.join("\n") };
|
|
15
15
|
},
|
package/src/commands/clear.ts
CHANGED
package/src/commands/compact.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { compactMessages,
|
|
1
|
+
import { compactMessages, compactionConfigFromEnv } from "../agent/compaction.js";
|
|
2
2
|
import { getModelMeta } from "../lib/model-meta.js";
|
|
3
3
|
|
|
4
4
|
export const compactCommand = {
|
|
5
5
|
name: "compact",
|
|
6
6
|
description: "Force local context compaction now",
|
|
7
7
|
run({ state, actions }) {
|
|
8
|
+
const config = compactionConfigFromEnv();
|
|
9
|
+
if (!config.enabled)
|
|
10
|
+
return { message: "Compaction is disabled by FASTCODE_COMPACTION." };
|
|
8
11
|
const meta = getModelMeta(state.providerId, state.model);
|
|
9
|
-
const result = compactMessages(state.history, { ...
|
|
12
|
+
const result = compactMessages(state.history, { ...config, localThresholdRatio: 0 }, {
|
|
10
13
|
contextWindow: meta.contextWindow,
|
|
11
14
|
providerId: state.providerId,
|
|
12
15
|
model: state.model,
|
|
@@ -18,6 +21,15 @@ export const compactCommand = {
|
|
|
18
21
|
return {
|
|
19
22
|
message: `[context compacted] ${result.beforeTokens} → ${result.afterTokens} tokens (${result.droppedMessages} messages summarized).` +
|
|
20
23
|
(result.summaryFile ? `\nSummary: ${result.summaryFile}` : ""),
|
|
24
|
+
compacted: {
|
|
25
|
+
summaryRef: result.summaryFile,
|
|
26
|
+
source: "local",
|
|
27
|
+
beforeTokens: result.beforeTokens,
|
|
28
|
+
afterTokens: result.afterTokens,
|
|
29
|
+
droppedMessages: result.droppedMessages,
|
|
30
|
+
keepMessages: result.keptMessages,
|
|
31
|
+
reason: "manual",
|
|
32
|
+
},
|
|
21
33
|
};
|
|
22
34
|
},
|
|
23
35
|
};
|
package/src/commands/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compactionConfigFromEnv, heapExceeded, localThresholdTokens } from "../agent/compaction.js";
|
|
2
2
|
import { estMessagesTokens, getModelMeta } from "../lib/model-meta.js";
|
|
3
3
|
|
|
4
4
|
export const contextCommand = {
|
|
@@ -6,18 +6,19 @@ export const contextCommand = {
|
|
|
6
6
|
description: "Show estimated context tokens, compaction threshold, and heap usage",
|
|
7
7
|
run(ctx) {
|
|
8
8
|
const meta = getModelMeta(ctx.state.providerId, ctx.state.model);
|
|
9
|
+
const config = compactionConfigFromEnv();
|
|
9
10
|
const tokens = estMessagesTokens(ctx.state.history, {
|
|
10
11
|
providerId: ctx.state.providerId,
|
|
11
12
|
model: ctx.state.model,
|
|
12
13
|
});
|
|
13
|
-
const threshold = localThresholdTokens(
|
|
14
|
+
const threshold = localThresholdTokens(config, meta.contextWindow);
|
|
14
15
|
const percent = meta.contextWindow > 0 ? (tokens / meta.contextWindow) * 100 : 0;
|
|
15
16
|
const heapMb = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
16
17
|
return {
|
|
17
18
|
message: [
|
|
18
19
|
`Context: ~${tokens.toLocaleString()} / ${meta.contextWindow.toLocaleString()} tokens (${percent.toFixed(1)}%)`,
|
|
19
|
-
`Compaction threshold: ${threshold.toLocaleString()} tokens (${Math.round(
|
|
20
|
-
`Heap: ${heapMb.toFixed(1)} MB / ${
|
|
20
|
+
`Compaction threshold: ${threshold.toLocaleString()} tokens (${Math.round(config.localThresholdRatio * 100)}%)${config.enabled ? "" : " [disabled]"}`,
|
|
21
|
+
`Heap: ${heapMb.toFixed(1)} MB / ${config.heapLimitMb} MB${heapExceeded(config) ? " [forced compaction]" : ""}`,
|
|
21
22
|
`Messages: ${ctx.state.history.length} · chars/token: ~${meta.charsPerToken}`,
|
|
22
23
|
].join("\n"),
|
|
23
24
|
};
|
package/src/commands/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { expandCommand } from "./expand.js";
|
|
|
28
28
|
import { attachCommand } from "./attach.js";
|
|
29
29
|
import { bgCommand } from "./bg.js";
|
|
30
30
|
import { editCommand } from "./edit.js";
|
|
31
|
+
import { threadsCommand } from "./threads.js";
|
|
31
32
|
export const COMMANDS = [
|
|
32
33
|
helpCommand,
|
|
33
34
|
agentCommand,
|
|
@@ -59,6 +60,7 @@ export const COMMANDS = [
|
|
|
59
60
|
attachCommand,
|
|
60
61
|
bgCommand,
|
|
61
62
|
editCommand,
|
|
63
|
+
threadsCommand,
|
|
62
64
|
];
|
|
63
65
|
export function findCommand(name) {
|
|
64
66
|
const lower = name.toLowerCase();
|
package/src/commands/jobs.ts
CHANGED
|
@@ -9,9 +9,10 @@ export const jobsCommand = {
|
|
|
9
9
|
return { message: "No shell sessions. Long commands return a session id automatically." };
|
|
10
10
|
const lines = jobs.map((job) => {
|
|
11
11
|
const exit = job.exitCode === undefined || job.exitCode === null ? "" : ` exit=${job.exitCode}`;
|
|
12
|
+
const age = Math.max(0, Math.round((Date.now() - job.startedAt) / 1000));
|
|
12
13
|
const command = job.command.length > 80 ? job.command.slice(0, 77) + "…" : job.command;
|
|
13
|
-
return `${job.id} ${job.status}${exit} ${(job.bytes / 1024).toFixed(1)}KB ${command}`;
|
|
14
|
+
return `${job.id} ${job.status}${exit} ${age}s ${(job.bytes / 1024).toFixed(1)}KB ${command}`;
|
|
14
15
|
});
|
|
15
|
-
return { message: ["Shell sessions:", ...lines, "", "Kill one with: /kill <id>"].join("\n") };
|
|
16
|
+
return { message: ["Shell sessions:", ...lines, "", "Kill one with: /kill <id>", "Completed sessions are pruned after 10 minutes; resident pool cap is 2."].join("\n") };
|
|
16
17
|
},
|
|
17
18
|
};
|
package/src/commands/kill.ts
CHANGED
|
@@ -7,6 +7,8 @@ export const killCommand = {
|
|
|
7
7
|
const id = ctx.args[0];
|
|
8
8
|
if (!id)
|
|
9
9
|
return { message: "Usage: /kill <session-id> (see /jobs)" };
|
|
10
|
-
return { message: killShellSession(id)
|
|
10
|
+
return { message: killShellSession(id)
|
|
11
|
+
? `Killed ${id} (process group killed; setsid descendants may escape).`
|
|
12
|
+
: `No running session named ${id}.` };
|
|
11
13
|
},
|
|
12
14
|
};
|
package/src/commands/login.ts
CHANGED
|
@@ -21,13 +21,25 @@ export function saveScopedKey(providerId, providerLabel, model, key) {
|
|
|
21
21
|
`Remove with /logout ${providerId}`,
|
|
22
22
|
].join("\n");
|
|
23
23
|
}
|
|
24
|
+
export function maskLoginEcho(args) {
|
|
25
|
+
const parts = ["/login", args[0] ?? ""];
|
|
26
|
+
if (args.length === 2) {
|
|
27
|
+
parts.push("********");
|
|
28
|
+
}
|
|
29
|
+
else if (args.length >= 3) {
|
|
30
|
+
parts.push(args[1]);
|
|
31
|
+
for (let i = 2; i < args.length; i += 1)
|
|
32
|
+
parts.push("********");
|
|
33
|
+
}
|
|
34
|
+
return parts.filter((part) => part !== "").join(" ");
|
|
35
|
+
}
|
|
24
36
|
export const loginCommand = {
|
|
25
37
|
name: "login",
|
|
26
38
|
description: "Bind a provider-wide API key. Usage: /login [provider] [model] [key]",
|
|
27
|
-
run({ args,
|
|
39
|
+
run({ args, actions }) {
|
|
28
40
|
if (args.length === 0) {
|
|
29
|
-
actions.startLogin({});
|
|
30
|
-
return { message:
|
|
41
|
+
actions.startLogin({ flow: "login" });
|
|
42
|
+
return { message: "Configure a provider: choose a provider and model, then enter its API key." };
|
|
31
43
|
}
|
|
32
44
|
const provider = getProvider(args[0]);
|
|
33
45
|
if (!provider) {
|
|
@@ -48,16 +60,4 @@ export const loginCommand = {
|
|
|
48
60
|
const key = args.slice(2).join(" ");
|
|
49
61
|
return { message: saveScopedKey(provider.id, provider.label, model, key) };
|
|
50
62
|
},
|
|
51
|
-
};
|
|
52
|
-
function introText(state) {
|
|
53
|
-
const env = getProvider(state.providerId)?.authEnv?.[0] ?? "PROVIDER_API_KEY";
|
|
54
|
-
return [
|
|
55
|
-
"Bind a provider-wide API key (reused by every model of that provider).",
|
|
56
|
-
"",
|
|
57
|
-
` current provider: ${state.providerId}`,
|
|
58
|
-
"",
|
|
59
|
-
"FastCode will ask for the provider, then optionally a model name, then the key.",
|
|
60
|
-
" one-liner: /login <provider> <model> <key>",
|
|
61
|
-
` or set: export ${env}=<key> (provider-wide, any model)`,
|
|
62
|
-
].join("\n");
|
|
63
63
|
}
|
package/src/commands/model.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { getProviderOrThrow
|
|
2
|
-
|
|
1
|
+
import { getProviderOrThrow } from "../providers/index.js";
|
|
2
|
+
export function isValidModelId(value) {
|
|
3
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,199}$/.test(value);
|
|
4
|
+
}
|
|
3
5
|
export const modelCommand = {
|
|
4
6
|
name: "model",
|
|
5
7
|
description: "Pick a model interactively, or switch with /model <id>",
|
|
@@ -10,41 +12,7 @@ export const modelCommand = {
|
|
|
10
12
|
actions.setModel(id);
|
|
11
13
|
return { message: `Model set to ${id}.` };
|
|
12
14
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const models = dedupe(fetched);
|
|
16
|
-
if (models.length === 0) {
|
|
17
|
-
return { message: `No models available from ${provider.label}.` };
|
|
18
|
-
}
|
|
19
|
-
const online = await probeProvider(provider, key);
|
|
20
|
-
const list = models.slice(0, 50);
|
|
21
|
-
return {
|
|
22
|
-
message: `Models for ${provider.label}: ${list.length}${online ? "" : " (offline — showing the built-in list)"}.\n` +
|
|
23
|
-
"Use ↑/↓, a number, or Enter to select.",
|
|
24
|
-
picker: {
|
|
25
|
-
title: `Select model · ${provider.label}${online ? "" : " (offline)"}`,
|
|
26
|
-
current: state.model,
|
|
27
|
-
items: list.map((m) => ({
|
|
28
|
-
value: m.id,
|
|
29
|
-
label: m.id,
|
|
30
|
-
hint: m.label ? `— ${m.label}` : undefined,
|
|
31
|
-
})),
|
|
32
|
-
onSelect: (id) => {
|
|
33
|
-
actions.setModel(id);
|
|
34
|
-
actions.pushSystemMessage(`Model set to ${id}.`);
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
};
|
|
15
|
+
actions.startLogin({ flow: "model", provider: provider.id, model: state.model });
|
|
16
|
+
return { message: `Configure ${provider.label}: choose a model, then enter its API key.` };
|
|
38
17
|
},
|
|
39
18
|
};
|
|
40
|
-
function dedupe(models) {
|
|
41
|
-
const seen = new Set();
|
|
42
|
-
const out = [];
|
|
43
|
-
for (const model of models) {
|
|
44
|
-
if (!model?.id || seen.has(model.id))
|
|
45
|
-
continue;
|
|
46
|
-
seen.add(model.id);
|
|
47
|
-
out.push(model);
|
|
48
|
-
}
|
|
49
|
-
return out;
|
|
50
|
-
}
|
package/src/commands/provider.ts
CHANGED
|
@@ -1,29 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getProvider } from "../providers/index.js";
|
|
2
2
|
export const providerCommand = {
|
|
3
3
|
name: "provider",
|
|
4
4
|
description: "List or switch the active LLM provider. Usage: /provider [id]",
|
|
5
|
-
run({ args, actions
|
|
5
|
+
run({ args, actions }) {
|
|
6
6
|
if (args.length === 0) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
picker: {
|
|
10
|
-
title: "Select provider",
|
|
11
|
-
current: state.providerId,
|
|
12
|
-
items: PROVIDERS.map((p) => ({
|
|
13
|
-
value: p.id,
|
|
14
|
-
label: `${p.id.padEnd(12)} ${p.label}`,
|
|
15
|
-
hint: p.authEnv.length === 0 ? "(no key needed)" : `[${p.authEnv.join(", ")}]`,
|
|
16
|
-
})),
|
|
17
|
-
onSelect: (id) => {
|
|
18
|
-
const next = getProvider(id);
|
|
19
|
-
if (!next)
|
|
20
|
-
return;
|
|
21
|
-
actions.setProvider(next.id);
|
|
22
|
-
actions.setModel(next.defaultModel);
|
|
23
|
-
actions.pushSystemMessage(`Provider set to ${next.label}. Default model: ${next.defaultModel}.`);
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
};
|
|
7
|
+
actions.startLogin({ flow: "provider" });
|
|
8
|
+
return { message: "Configure a provider: choose a provider and model, then enter its API key." };
|
|
27
9
|
}
|
|
28
10
|
const target = args[0];
|
|
29
11
|
const next = getProvider(target);
|
package/src/commands/skills.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { installSkill, uninstallSkill, listInstalledSkills, searchRegistry, getSkill, } from "../skills/manager.js";
|
|
1
|
+
import { installSkill, uninstallSkill, listInstalledSkills, searchRegistry, getSkill, rebaselineSkills, resolveRegistry, } from "../skills/manager.js";
|
|
2
2
|
export const skillsCommand = {
|
|
3
3
|
name: "skills",
|
|
4
4
|
description: "Manage Agent Skills (download from skills.sh and toggle).",
|
|
@@ -17,15 +17,20 @@ export const skillsCommand = {
|
|
|
17
17
|
const enabled = state.enabledSkills.includes(s.name) ? "● " : " ";
|
|
18
18
|
lines.push(`${enabled}${s.name} — ${truncate(s.description, 80)}`);
|
|
19
19
|
lines.push(` source: ${s.source}`);
|
|
20
|
+
if (s.integrity !== "ok")
|
|
21
|
+
lines.push(` integrity: ${s.integrity === "drift" ? "DRIFT (disabled; run /skills verify " + s.name + " --confirm)" : "unverified (run /skills verify " + s.name + " --confirm)"}`);
|
|
20
22
|
}
|
|
21
23
|
lines.push("");
|
|
22
24
|
lines.push("Toggle with `/skills enable <name>` or `/skills disable <name>`.");
|
|
23
25
|
return { message: lines.join("\n") };
|
|
24
26
|
}
|
|
25
27
|
if (sub === "search") {
|
|
28
|
+
const registry = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], state.skillsRegistry);
|
|
29
|
+
if (!registry)
|
|
30
|
+
return { message: "No skills registry configured. Set one with `/skills registry <url>` or FASTCODE_SKILLS_REGISTRY." };
|
|
26
31
|
if (rest.length === 0)
|
|
27
32
|
return { message: "Usage: /skills search <query>" };
|
|
28
|
-
const hits = await searchRegistry(rest.join(" "),
|
|
33
|
+
const hits = await searchRegistry(rest.join(" "), registry);
|
|
29
34
|
if (hits.length === 0) {
|
|
30
35
|
return { message: `No skills found for "${rest.join(" ")}".` };
|
|
31
36
|
}
|
|
@@ -66,6 +71,10 @@ export const skillsCommand = {
|
|
|
66
71
|
const skill = getSkill(rest[0]);
|
|
67
72
|
if (!skill)
|
|
68
73
|
return { message: `Skill "${rest[0]}" is not installed.` };
|
|
74
|
+
if (skill.integrity !== "ok") {
|
|
75
|
+
const reason = skill.integrity === "drift" ? "content drift detected" : "no integrity baseline";
|
|
76
|
+
return { message: `Skill "${skill.name}" is disabled (${reason}). Run /skills verify ${skill.name} --confirm to re-baseline.` };
|
|
77
|
+
}
|
|
69
78
|
actions.enableSkill(skill.name);
|
|
70
79
|
return { message: `Enabled skill "${skill.name}".` };
|
|
71
80
|
}
|
|
@@ -84,9 +93,30 @@ export const skillsCommand = {
|
|
|
84
93
|
const head = `# ${skill.name}\n_${skill.description}_\n\nSource: ${skill.source}\n\n---\n\n`;
|
|
85
94
|
return { message: head + skill.body };
|
|
86
95
|
}
|
|
96
|
+
if (sub === "verify") {
|
|
97
|
+
const confirm = rest.includes("--confirm") || rest.includes("--yes");
|
|
98
|
+
const all = rest.includes("--all");
|
|
99
|
+
const names = rest.filter((entry) => !entry.startsWith("-"));
|
|
100
|
+
if (!all && names.length === 0)
|
|
101
|
+
return { message: "Usage: /skills verify <name> --confirm (or /skills verify --all --confirm)" };
|
|
102
|
+
if (!confirm) {
|
|
103
|
+
return { message: `Re-baselining ${all ? "all skills" : names.join(", ")} trusts the current files as the new baseline. ` +
|
|
104
|
+
"Re-run the same command with --confirm to proceed." };
|
|
105
|
+
}
|
|
106
|
+
const results = rebaselineSkills(all ? undefined : names);
|
|
107
|
+
if (results.length === 0)
|
|
108
|
+
return { message: "No matching installed skills to verify." };
|
|
109
|
+
const lines = ["Skill integrity re-baselined:"];
|
|
110
|
+
for (const result of results)
|
|
111
|
+
lines.push(result.ok ? ` • ${result.name}: locked` : ` • ${result.name}: FAILED — ${result.warning ?? "unknown error"}`);
|
|
112
|
+
return { message: lines.join("\n") };
|
|
113
|
+
}
|
|
87
114
|
if (sub === "registry") {
|
|
88
115
|
if (rest.length === 0) {
|
|
89
|
-
|
|
116
|
+
const registry = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], state.skillsRegistry);
|
|
117
|
+
return { message: registry
|
|
118
|
+
? `Skills registry: ${registry}`
|
|
119
|
+
: "No skills registry configured. Set one with `/skills registry <url>` or FASTCODE_SKILLS_REGISTRY." };
|
|
90
120
|
}
|
|
91
121
|
// Mutate via AppActions so the in-memory React state stays in sync —
|
|
92
122
|
// direct saveConfig() would be silently overwritten by the next state
|
|
@@ -95,7 +125,7 @@ export const skillsCommand = {
|
|
|
95
125
|
return { message: `Skills registry set to ${rest[0]}.` };
|
|
96
126
|
}
|
|
97
127
|
return {
|
|
98
|
-
message: "Unknown subcommand. Try: list, search, install, uninstall, enable, disable, info, registry.",
|
|
128
|
+
message: "Unknown subcommand. Try: list, search, install, uninstall, enable, disable, info, verify, registry.",
|
|
99
129
|
};
|
|
100
130
|
},
|
|
101
131
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CommandContext } from "../types.js";
|
|
2
|
+
import { DEFAULT_SHELL_POOL_SIZE, MAX_SHELL_POOL_SIZE, shellPoolSize } from "../agent/shell-session.js";
|
|
3
|
+
|
|
4
|
+
export const threadsCommand = {
|
|
5
|
+
name: "threads",
|
|
6
|
+
aliases: ["pool"],
|
|
7
|
+
description: `Show or set resident shell concurrency (default ${DEFAULT_SHELL_POOL_SIZE}, max ${MAX_SHELL_POOL_SIZE}). Usage: /threads [1-${MAX_SHELL_POOL_SIZE}]`,
|
|
8
|
+
run({ args, actions }: CommandContext) {
|
|
9
|
+
const before = shellPoolSize();
|
|
10
|
+
if (args.length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
message: `Shell concurrency: ${before} resident slot(s) (default ${DEFAULT_SHELL_POOL_SIZE}, max ${MAX_SHELL_POOL_SIZE}). ` +
|
|
13
|
+
`Usage: /threads <1-${MAX_SHELL_POOL_SIZE}>.`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const requested = Number(args[0]);
|
|
17
|
+
if (!Number.isFinite(requested)) {
|
|
18
|
+
return { message: `Invalid thread count "${args[0]}". Use /threads <1-${MAX_SHELL_POOL_SIZE}>.` };
|
|
19
|
+
}
|
|
20
|
+
actions.setShellPoolSize(requested);
|
|
21
|
+
const applied = shellPoolSize();
|
|
22
|
+
const clamped = applied !== Math.floor(requested) ? " (clamped)" : "";
|
|
23
|
+
return {
|
|
24
|
+
message: `Shell concurrency: ${before} → ${applied}${clamped} (default ${DEFAULT_SHELL_POOL_SIZE}, max ${MAX_SHELL_POOL_SIZE}). ` +
|
|
25
|
+
"Running jobs keep their slots.",
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
package/src/config/store.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* reuses the same key. The old "provider::model" (v2) and provider-only (v1)
|
|
11
11
|
* formats are read and migrated to the provider-scoped v3 layout on first load.
|
|
12
12
|
*/
|
|
13
|
-
import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync } from "node:fs";
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync, statSync } from "node:fs";
|
|
14
14
|
import { join, dirname } from "node:path";
|
|
15
15
|
import { getPaths, lockdown, ensureDir } from "./paths.js";
|
|
16
16
|
import { DEFAULT_THEME } from "./themes.js";
|
|
17
|
+
import { DEFAULT_EGRESS_CONFIG } from "../agent/policy/defaults.js";
|
|
17
18
|
export const DEFAULT_CONFIG = {
|
|
18
19
|
providerId: "anthropic",
|
|
19
20
|
model: "claude-sonnet-5.5",
|
|
@@ -25,7 +26,7 @@ export const DEFAULT_CONFIG = {
|
|
|
25
26
|
"or unified diffs the user can apply.",
|
|
26
27
|
enabledSkills: [],
|
|
27
28
|
enabledPlugins: [],
|
|
28
|
-
skillsRegistry: "
|
|
29
|
+
skillsRegistry: "",
|
|
29
30
|
lifetimeCostUsd: 0,
|
|
30
31
|
/**
|
|
31
32
|
* Extra directories the file tools may read and write outside the project
|
|
@@ -41,10 +42,17 @@ export const DEFAULT_CONFIG = {
|
|
|
41
42
|
contextBudgetChars: 400_000,
|
|
42
43
|
/** Thinking depth: auto = send nothing, provider default. */
|
|
43
44
|
reasoningEffort: "auto",
|
|
45
|
+
/** Resident shell concurrency: safe default 2, max 6. `/threads` changes it. */
|
|
46
|
+
shellPoolSize: 2,
|
|
44
47
|
/** Cached per-provider/model capabilities (native vs pseudo tool calling). */
|
|
45
48
|
capabilities: {
|
|
46
49
|
toolCalling: {},
|
|
47
50
|
},
|
|
51
|
+
/**
|
|
52
|
+
* Application-layer egress policy. Default is audit-only; set
|
|
53
|
+
* enabled:true + mode:"block" to enforce. See docs/CONFIGURATION.md.
|
|
54
|
+
*/
|
|
55
|
+
egress: { ...DEFAULT_EGRESS_CONFIG },
|
|
48
56
|
};
|
|
49
57
|
export interface StoredCredential {
|
|
50
58
|
provider: string;
|
|
@@ -73,21 +81,51 @@ export type KeyLookup = {
|
|
|
73
81
|
} | {
|
|
74
82
|
status: "missing";
|
|
75
83
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!existsSync(config))
|
|
79
|
-
return { ...DEFAULT_CONFIG };
|
|
84
|
+
let configCache: { path: string; fingerprint: string; value: any } | undefined;
|
|
85
|
+
function configFingerprint(file: string): string {
|
|
80
86
|
try {
|
|
81
|
-
const
|
|
82
|
-
|
|
87
|
+
const info = statSync(file);
|
|
88
|
+
// mtime + size are required; ctime adds resilience on filesystems with
|
|
89
|
+
// coarse mtime resolution. stat() is much cheaper than a full read on
|
|
90
|
+
// Termux shared storage.
|
|
91
|
+
return `${info.mtimeMs}:${info.size}:${info.ctimeMs}`;
|
|
83
92
|
}
|
|
84
93
|
catch {
|
|
85
|
-
return
|
|
94
|
+
return "missing";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Drop the cached config so the next loadConfig() re-stats and re-reads. */
|
|
98
|
+
export function invalidateConfigCache(): void {
|
|
99
|
+
configCache = undefined;
|
|
100
|
+
}
|
|
101
|
+
export function configCacheStatus(): { cached: boolean; fingerprint?: string } {
|
|
102
|
+
return configCache ? { cached: true, fingerprint: configCache.fingerprint } : { cached: false };
|
|
103
|
+
}
|
|
104
|
+
export function loadConfig() {
|
|
105
|
+
const { config } = getPaths();
|
|
106
|
+
const fingerprint = configFingerprint(config);
|
|
107
|
+
if (configCache && configCache.path === config && configCache.fingerprint === fingerprint)
|
|
108
|
+
return { ...configCache.value };
|
|
109
|
+
let value;
|
|
110
|
+
if (!existsSync(config)) {
|
|
111
|
+
value = { ...DEFAULT_CONFIG };
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
try {
|
|
115
|
+
const raw = JSON.parse(readFileSync(config, "utf8"));
|
|
116
|
+
value = { ...DEFAULT_CONFIG, ...raw };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
value = { ...DEFAULT_CONFIG };
|
|
120
|
+
}
|
|
86
121
|
}
|
|
122
|
+
configCache = { path: config, fingerprint, value };
|
|
123
|
+
return { ...value };
|
|
87
124
|
}
|
|
88
125
|
export function saveConfig(cfg) {
|
|
89
126
|
const { config } = getPaths();
|
|
90
127
|
atomicWrite(config, JSON.stringify(cfg, null, 2));
|
|
128
|
+
invalidateConfigCache();
|
|
91
129
|
}
|
|
92
130
|
/** Composite id used by the legacy v2 format; kept for API compatibility. */
|
|
93
131
|
export function scopeId(providerId, model) {
|
package/src/config/trust.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* FASTCODE_TRUST_ALL=1 marks every directory trusted (scripts, CI).
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { ensureDir, getPaths } from "./paths.js";
|
|
13
13
|
|
|
@@ -136,7 +136,20 @@ export function loadTrust(): TrustStore {
|
|
|
136
136
|
function saveTrust(state: TrustStore): void {
|
|
137
137
|
const paths = getPaths();
|
|
138
138
|
ensureDir(paths.root);
|
|
139
|
-
|
|
139
|
+
const tmp = paths.trust + "." + process.pid + ".tmp";
|
|
140
|
+
try {
|
|
141
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
142
|
+
renameSync(tmp, paths.trust);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
try {
|
|
146
|
+
rmSync(tmp, { force: true });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// best effort
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
140
153
|
try {
|
|
141
154
|
const info = statSync(paths.trust);
|
|
142
155
|
cache = { file: paths.trust, key: `${info.mtimeMs}:${info.size}`, state };
|
|
@@ -181,3 +194,33 @@ export function untrustProject(dir: string): boolean {
|
|
|
181
194
|
export function listTrusted(): Array<{ path: string; addedAt: string }> {
|
|
182
195
|
return Object.entries(loadTrust().paths).map(([path, entry]) => ({ path, addedAt: entry.grantedAt }));
|
|
183
196
|
}
|
|
197
|
+
|
|
198
|
+
/** Check a tool/fingerprint approval bound to the current tool catalog. */
|
|
199
|
+
export function isToolApproved(key: string, catalogHash: string): boolean {
|
|
200
|
+
const entry = loadTrust().tools[key];
|
|
201
|
+
return Boolean(entry && entry.catalogHash === catalogHash);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function rememberToolApproval(key: string, catalogHash: string): void {
|
|
205
|
+
const state = loadTrust();
|
|
206
|
+
const at = new Date().toISOString();
|
|
207
|
+
state.tools[key] = { grantedAt: at, lastUsed: at, catalogHash };
|
|
208
|
+
saveTrust(state);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function forgetToolApproval(key: string): boolean {
|
|
212
|
+
const state = loadTrust();
|
|
213
|
+
if (!state.tools[key])
|
|
214
|
+
return false;
|
|
215
|
+
delete state.tools[key];
|
|
216
|
+
saveTrust(state);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function listToolApprovals(): Array<{ key: string; grantedAt: string; catalogHash: string }> {
|
|
221
|
+
return Object.entries(loadTrust().tools).map(([key, entry]) => ({
|
|
222
|
+
key,
|
|
223
|
+
grantedAt: entry.grantedAt,
|
|
224
|
+
catalogHash: entry.catalogHash,
|
|
225
|
+
}));
|
|
226
|
+
}
|