@datename/fastcode-termux 0.0.0-stage → 0.1.6-beta
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 +21 -0
- package/README.md +418 -2
- package/bin/fastcode.mjs +59 -0
- package/dist/agent/approval.d.ts +50 -0
- package/dist/agent/approval.js +7 -0
- package/dist/agent/compaction.d.ts +39 -0
- package/dist/agent/compaction.js +227 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +43 -0
- package/dist/agent/guards.js +269 -0
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- package/dist/agent/limits.d.ts +24 -0
- package/dist/agent/limits.js +71 -0
- package/dist/agent/process-tree.d.ts +2 -0
- package/dist/agent/process-tree.js +31 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +4 -0
- package/dist/agent/protocol.js +129 -0
- package/dist/agent/registry.d.ts +17 -0
- package/dist/agent/registry.js +90 -0
- package/dist/agent/runner.d.ts +106 -0
- package/dist/agent/runner.js +352 -0
- package/dist/agent/shell-session.d.ts +37 -0
- package/dist/agent/shell-session.js +400 -0
- package/dist/agent/stream.d.ts +29 -0
- package/dist/agent/stream.js +103 -0
- package/dist/agent/tool-schemas.d.ts +2 -0
- package/dist/agent/tool-schemas.js +63 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +26 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +52 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +30 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +24 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +23 -0
- package/dist/agent/tools/index.d.ts +2 -0
- package/dist/agent/tools/index.js +32 -0
- package/dist/agent/tools/list_files.d.ts +2 -0
- package/dist/agent/tools/list_files.js +17 -0
- package/dist/agent/tools/make_dir.d.ts +2 -0
- package/dist/agent/tools/make_dir.js +24 -0
- package/dist/agent/tools/move_path.d.ts +2 -0
- package/dist/agent/tools/move_path.js +47 -0
- package/dist/agent/tools/paths.d.ts +7 -0
- package/dist/agent/tools/paths.js +84 -0
- package/dist/agent/tools/pwd.d.ts +2 -0
- package/dist/agent/tools/pwd.js +10 -0
- package/dist/agent/tools/read_file.d.ts +2 -0
- package/dist/agent/tools/read_file.js +21 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +25 -0
- package/dist/agent/tools/readers.d.ts +6 -0
- package/dist/agent/tools/readers.js +216 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +30 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +48 -0
- package/dist/agent/tools/stat_path.d.ts +2 -0
- package/dist/agent/tools/stat_path.js +31 -0
- package/dist/agent/tools/types.d.ts +39 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +6 -0
- package/dist/agent/tools/validate.js +29 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +26 -0
- package/dist/agent/tools.d.ts +3 -0
- package/dist/agent/tools.js +2 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +708 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +569 -0
- package/dist/commands/about.d.ts +8 -0
- package/dist/commands/about.js +16 -0
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.js +18 -0
- package/dist/commands/attach.d.ts +9 -0
- package/dist/commands/attach.js +15 -0
- package/dist/commands/bg.d.ts +7 -0
- package/dist/commands/bg.js +9 -0
- package/dist/commands/cancel.d.ts +10 -0
- package/dist/commands/cancel.js +10 -0
- package/dist/commands/clear.d.ts +11 -0
- package/dist/commands/clear.js +9 -0
- package/dist/commands/compact.d.ts +10 -0
- package/dist/commands/compact.js +22 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +24 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +24 -0
- package/dist/commands/cost.d.ts +9 -0
- package/dist/commands/cost.js +13 -0
- package/dist/commands/cwd.d.ts +7 -0
- package/dist/commands/cwd.js +7 -0
- package/dist/commands/edit.d.ts +10 -0
- package/dist/commands/edit.js +36 -0
- package/dist/commands/effort.d.ts +25 -0
- package/dist/commands/effort.js +48 -0
- package/dist/commands/exit.d.ts +9 -0
- package/dist/commands/exit.js +8 -0
- package/dist/commands/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/help.d.ts +6 -0
- package/dist/commands/help.js +18 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +80 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +16 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +11 -0
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.js +63 -0
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +45 -0
- package/dist/commands/model.d.ts +24 -0
- package/dist/commands/model.js +50 -0
- package/dist/commands/plugin.d.ts +11 -0
- package/dist/commands/plugin.js +74 -0
- package/dist/commands/provider.d.ts +24 -0
- package/dist/commands/provider.js +39 -0
- package/dist/commands/reset.d.ts +9 -0
- package/dist/commands/reset.js +13 -0
- package/dist/commands/skills.d.ts +11 -0
- package/dist/commands/skills.js +106 -0
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/system.d.ts +11 -0
- package/dist/commands/system.js +20 -0
- package/dist/commands/theme.d.ts +11 -0
- package/dist/commands/theme.js +22 -0
- package/dist/commands/tools.d.ts +7 -0
- package/dist/commands/tools.js +29 -0
- package/dist/commands/whoami.d.ts +9 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +59 -0
- package/dist/config/store.d.ts +89 -0
- package/dist/config/store.js +260 -0
- package/dist/config/themes.d.ts +73 -0
- package/dist/config/themes.js +77 -0
- package/dist/config/trust.d.ts +22 -0
- package/dist/config/trust.js +153 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -0
- package/dist/lib/diagnostics.d.ts +66 -0
- package/dist/lib/diagnostics.js +195 -0
- package/dist/lib/http.d.ts +23 -0
- package/dist/lib/http.js +128 -0
- package/dist/lib/markdown.d.ts +1 -0
- package/dist/lib/markdown.js +32 -0
- package/dist/lib/model-meta.d.ts +19 -0
- package/dist/lib/model-meta.js +113 -0
- package/dist/lib/native-tools.d.ts +12 -0
- package/dist/lib/native-tools.js +21 -0
- package/dist/lib/net.d.ts +33 -0
- package/dist/lib/net.js +168 -0
- package/dist/lib/pkg.d.ts +5 -0
- package/dist/lib/pkg.js +27 -0
- package/dist/lib/platform.d.ts +7 -0
- package/dist/lib/platform.js +57 -0
- package/dist/lib/startup.d.ts +6 -0
- package/dist/lib/startup.js +25 -0
- package/dist/lib/uiMode.d.ts +2 -0
- package/dist/lib/uiMode.js +7 -0
- package/dist/patch/apply.d.ts +15 -0
- package/dist/patch/apply.js +172 -0
- package/dist/patch/parse.d.ts +29 -0
- package/dist/patch/parse.js +112 -0
- package/dist/patch/plan.d.ts +49 -0
- package/dist/patch/plan.js +273 -0
- package/dist/plugins/manager.d.ts +23 -0
- package/dist/plugins/manager.js +101 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +119 -0
- package/dist/providers/custom.d.ts +33 -0
- package/dist/providers/custom.js +25 -0
- package/dist/providers/deepseek.d.ts +34 -0
- package/dist/providers/deepseek.js +16 -0
- package/dist/providers/google.d.ts +33 -0
- package/dist/providers/google.js +19 -0
- package/dist/providers/groq.d.ts +34 -0
- package/dist/providers/groq.js +21 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +67 -0
- package/dist/providers/mistral.d.ts +34 -0
- package/dist/providers/mistral.js +20 -0
- package/dist/providers/moonshot.d.ts +34 -0
- package/dist/providers/moonshot.js +23 -0
- package/dist/providers/ollama.d.ts +33 -0
- package/dist/providers/ollama.js +22 -0
- package/dist/providers/openai.d.ts +34 -0
- package/dist/providers/openai.js +26 -0
- package/dist/providers/openaiCompatible.d.ts +26 -0
- package/dist/providers/openaiCompatible.js +158 -0
- package/dist/providers/openrouter.d.ts +34 -0
- package/dist/providers/openrouter.js +27 -0
- package/dist/providers/qwen.d.ts +34 -0
- package/dist/providers/qwen.js +24 -0
- package/dist/providers/xai.d.ts +34 -0
- package/dist/providers/xai.js +19 -0
- package/dist/providers/zhipu.d.ts +34 -0
- package/dist/providers/zhipu.js +24 -0
- package/dist/session/log.d.ts +52 -0
- package/dist/session/log.js +331 -0
- package/dist/session/projection.d.ts +43 -0
- package/dist/session/projection.js +257 -0
- package/dist/skills/manager.d.ts +18 -0
- package/dist/skills/manager.js +201 -0
- package/dist/skills/parser.d.ts +18 -0
- package/dist/skills/parser.js +65 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.js +1 -0
- package/dist/ui/Banner.d.ts +5 -0
- package/dist/ui/Banner.js +65 -0
- package/dist/ui/Message.d.ts +1 -0
- package/dist/ui/Message.js +24 -0
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +9 -0
- package/dist/ui/Prompt.js +38 -0
- package/dist/ui/Spinner.d.ts +5 -0
- package/dist/ui/Spinner.js +39 -0
- package/dist/ui/StatusBar.d.ts +11 -0
- package/dist/ui/StatusBar.js +20 -0
- package/dist/ui/TrustGate.d.ts +5 -0
- package/dist/ui/TrustGate.js +26 -0
- package/package.json +96 -4
- package/src/agent/approval.ts +47 -0
- package/src/agent/compaction.ts +276 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +270 -0
- package/src/agent/ignore.ts +147 -0
- package/src/agent/limits.ts +71 -0
- package/src/agent/process-tree.ts +32 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +129 -0
- package/src/agent/registry.ts +98 -0
- package/src/agent/runner.ts +443 -0
- package/src/agent/shell-session.ts +449 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +67 -0
- package/src/agent/tools/append_file.ts +28 -0
- package/src/agent/tools/apply_patch.ts +54 -0
- package/src/agent/tools/copy_file.ts +32 -0
- package/src/agent/tools/delete_file.ts +26 -0
- package/src/agent/tools/grep.ts +25 -0
- package/src/agent/tools/index.ts +34 -0
- package/src/agent/tools/list_files.ts +19 -0
- package/src/agent/tools/make_dir.ts +26 -0
- package/src/agent/tools/move_path.ts +50 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +23 -0
- package/src/agent/tools/read_many.ts +27 -0
- package/src/agent/tools/readers.ts +228 -0
- package/src/agent/tools/replace_in_file.ts +32 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +42 -0
- package/src/agent/tools/validate.ts +35 -0
- package/src/agent/tools/write_file.ts +28 -0
- package/src/agent/tools.ts +3 -0
- package/src/app.ts +708 -0
- package/src/cli.ts +335 -0
- package/src/commands/about.ts +16 -0
- package/src/commands/agent.ts +18 -0
- package/src/commands/attach.ts +16 -0
- package/src/commands/bg.ts +9 -0
- package/src/commands/cancel.ts +10 -0
- package/src/commands/clear.ts +9 -0
- package/src/commands/compact.ts +23 -0
- package/src/commands/config.ts +24 -0
- package/src/commands/context.ts +25 -0
- package/src/commands/cost.ts +13 -0
- package/src/commands/cwd.ts +7 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/exit.ts +8 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/help.ts +18 -0
- package/src/commands/index.ts +80 -0
- package/src/commands/jobs.ts +17 -0
- package/src/commands/kill.ts +12 -0
- package/src/commands/login.ts +63 -0
- package/src/commands/logout.ts +45 -0
- package/src/commands/model.ts +50 -0
- package/src/commands/plugin.ts +74 -0
- package/src/commands/provider.ts +39 -0
- package/src/commands/reset.ts +13 -0
- package/src/commands/skills.ts +106 -0
- package/src/commands/steer.ts +13 -0
- package/src/commands/system.ts +20 -0
- package/src/commands/theme.ts +22 -0
- package/src/commands/tools.ts +29 -0
- package/src/commands/whoami.ts +52 -0
- package/src/config/paths.ts +59 -0
- package/src/config/store.ts +293 -0
- package/src/config/themes.ts +77 -0
- package/src/config/trust.ts +183 -0
- package/src/index.ts +31 -0
- package/src/lib/diagnostics.ts +195 -0
- package/src/lib/http.ts +132 -0
- package/src/lib/markdown.ts +32 -0
- package/src/lib/model-meta.ts +138 -0
- package/src/lib/native-tools.ts +32 -0
- package/src/lib/net.ts +204 -0
- package/src/lib/pkg.ts +35 -0
- package/src/lib/platform.ts +57 -0
- package/src/lib/startup.ts +25 -0
- package/src/lib/uiMode.ts +7 -0
- package/src/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/plugins/manager.ts +101 -0
- package/src/providers/anthropic.ts +119 -0
- package/src/providers/custom.ts +25 -0
- package/src/providers/deepseek.ts +16 -0
- package/src/providers/google.ts +19 -0
- package/src/providers/groq.ts +21 -0
- package/src/providers/index.ts +68 -0
- package/src/providers/mistral.ts +20 -0
- package/src/providers/moonshot.ts +23 -0
- package/src/providers/ollama.ts +22 -0
- package/src/providers/openai.ts +26 -0
- package/src/providers/openaiCompatible.ts +158 -0
- package/src/providers/openrouter.ts +27 -0
- package/src/providers/qwen.ts +24 -0
- package/src/providers/xai.ts +19 -0
- package/src/providers/zhipu.ts +24 -0
- package/src/session/log.ts +374 -0
- package/src/session/projection.ts +308 -0
- package/src/skills/manager.ts +201 -0
- package/src/skills/parser.ts +65 -0
- package/src/types.ts +205 -0
- package/src/ui/Banner.ts +68 -0
- package/src/ui/Message.ts +24 -0
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +38 -0
- package/src/ui/Spinner.ts +39 -0
- package/src/ui/StatusBar.ts +20 -0
- package/src/ui/TrustGate.ts +26 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { estMessagesTokens, estTokens } from "../lib/model-meta.js";
|
|
4
|
+
import { getPaths } from "../config/paths.js";
|
|
5
|
+
import { parseToolCalls, stripToolCalls } from "./protocol.js";
|
|
6
|
+
export const DEFAULT_COMPACTION_CONFIG = {
|
|
7
|
+
enabled: true,
|
|
8
|
+
localThresholdRatio: 0.7,
|
|
9
|
+
keepTurns: 6,
|
|
10
|
+
serverCompactionAware: false,
|
|
11
|
+
clampAboveServerByTokens: 8192,
|
|
12
|
+
maxSummaryTokens: 1500,
|
|
13
|
+
heapLimitMb: 220,
|
|
14
|
+
};
|
|
15
|
+
export function compactionConfigFromEnv() {
|
|
16
|
+
const config = { ...DEFAULT_COMPACTION_CONFIG };
|
|
17
|
+
const disabled = (process.env["FASTCODE_COMPACTION"] ?? "").trim().toLowerCase();
|
|
18
|
+
if (["0", "false", "no", "off"].includes(disabled))
|
|
19
|
+
config.enabled = false;
|
|
20
|
+
const heap = Number(process.env["FASTCODE_HEAP_LIMIT_MB"]);
|
|
21
|
+
if (Number.isFinite(heap) && heap > 0)
|
|
22
|
+
config.heapLimitMb = heap;
|
|
23
|
+
const server = (process.env["FASTCODE_SERVER_COMPACTION"] ?? "").trim().toLowerCase();
|
|
24
|
+
if (["1", "true", "yes", "on"].includes(server))
|
|
25
|
+
config.serverCompactionAware = true;
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
export function localThresholdTokens(config, contextWindow) {
|
|
29
|
+
const local = Math.floor(Math.max(1, contextWindow) * config.localThresholdRatio);
|
|
30
|
+
if (config.serverCompactionAware && typeof config.serverCompactThreshold === "number" && config.serverCompactThreshold > 0) {
|
|
31
|
+
return Math.max(local, config.serverCompactThreshold + config.clampAboveServerByTokens);
|
|
32
|
+
}
|
|
33
|
+
return local;
|
|
34
|
+
}
|
|
35
|
+
export function heapExceeded(config) {
|
|
36
|
+
const used = process.memoryUsage().heapUsed;
|
|
37
|
+
return used > Math.max(0, config.heapLimitMb) * 1024 * 1024;
|
|
38
|
+
}
|
|
39
|
+
export function shouldCompact(messages, config, options) {
|
|
40
|
+
if (!config.enabled)
|
|
41
|
+
return false;
|
|
42
|
+
if (options.force || heapExceeded(config))
|
|
43
|
+
return true;
|
|
44
|
+
return messageTokens(messages, options) > localThresholdTokens(config, options.contextWindow);
|
|
45
|
+
}
|
|
46
|
+
export function compactMessages(messages, config = DEFAULT_COMPACTION_CONFIG, options) {
|
|
47
|
+
const beforeTokens = messageTokens(messages, options);
|
|
48
|
+
const beforeChars = messageChars(messages);
|
|
49
|
+
const heap = heapExceeded(config);
|
|
50
|
+
const noop = {
|
|
51
|
+
messages,
|
|
52
|
+
compacted: false,
|
|
53
|
+
source: "none",
|
|
54
|
+
beforeTokens,
|
|
55
|
+
afterTokens: beforeTokens,
|
|
56
|
+
beforeChars,
|
|
57
|
+
afterChars: beforeChars,
|
|
58
|
+
droppedMessages: 0,
|
|
59
|
+
keptMessages: messages.length,
|
|
60
|
+
};
|
|
61
|
+
if (!config.enabled)
|
|
62
|
+
return noop;
|
|
63
|
+
const threshold = localThresholdTokens(config, options.contextWindow);
|
|
64
|
+
if (!options.force && !heap && beforeTokens <= threshold)
|
|
65
|
+
return noop;
|
|
66
|
+
const reason = heap && beforeTokens <= threshold ? "heap" : "threshold";
|
|
67
|
+
if (config.serverCompactionAware && options.serverOpaqueState) {
|
|
68
|
+
return {
|
|
69
|
+
...noop,
|
|
70
|
+
compacted: true,
|
|
71
|
+
reason,
|
|
72
|
+
source: "server",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const headCount = messages[0]?.role === "user" ? 1 : 0;
|
|
76
|
+
const keep = Math.max(2, Math.floor(config.keepTurns * 2));
|
|
77
|
+
const tailStart = Math.max(headCount, messages.length - keep);
|
|
78
|
+
if (tailStart <= headCount)
|
|
79
|
+
return noop;
|
|
80
|
+
const middle = messages.slice(headCount, tailStart);
|
|
81
|
+
const summary = buildSummary(middle, config, options);
|
|
82
|
+
const summaryFile = writeSummaryFile(summary);
|
|
83
|
+
const marker = {
|
|
84
|
+
role: "system",
|
|
85
|
+
content: `[context compacted: ${middle.length} earlier messages were summarized to stay within the context window.` +
|
|
86
|
+
`${summaryFile ? ` Summary file: ${summaryFile}.` : ""} Recent turns are kept verbatim.` +
|
|
87
|
+
" If an earlier detail is missing, re-read that file with a tool.]\n\n" +
|
|
88
|
+
summary,
|
|
89
|
+
};
|
|
90
|
+
const next = [...messages.slice(0, headCount), marker, ...messages.slice(tailStart)];
|
|
91
|
+
return {
|
|
92
|
+
messages: next,
|
|
93
|
+
compacted: true,
|
|
94
|
+
reason,
|
|
95
|
+
source: "local",
|
|
96
|
+
beforeTokens,
|
|
97
|
+
afterTokens: messageTokens(next, options),
|
|
98
|
+
beforeChars,
|
|
99
|
+
afterChars: messageChars(next),
|
|
100
|
+
droppedMessages: middle.length,
|
|
101
|
+
keptMessages: next.length - headCount - 1,
|
|
102
|
+
summary,
|
|
103
|
+
summaryFile,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function messageTokens(messages, options) {
|
|
107
|
+
return estMessagesTokens(messages, { providerId: options.providerId, model: options.model });
|
|
108
|
+
}
|
|
109
|
+
function messageChars(messages) {
|
|
110
|
+
return messages.reduce((sum, message) => sum + (message.content?.length ?? 0), 0);
|
|
111
|
+
}
|
|
112
|
+
export function buildSummary(messages, config, options) {
|
|
113
|
+
const requests = [];
|
|
114
|
+
const actions = [];
|
|
115
|
+
const outcomes = [];
|
|
116
|
+
const notes = [];
|
|
117
|
+
const files = new Set();
|
|
118
|
+
for (const message of messages) {
|
|
119
|
+
if (!message.content)
|
|
120
|
+
continue;
|
|
121
|
+
if (message.role === "user") {
|
|
122
|
+
if (message.content.startsWith("FastCode executed the requested local tools.")) {
|
|
123
|
+
for (const line of message.content.split(/\r?\n/)) {
|
|
124
|
+
if (/error|failed|not found|denied|timed out/i.test(line))
|
|
125
|
+
outcomes.push(clip(line, 240));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
requests.push(clip(message.content, 300));
|
|
130
|
+
}
|
|
131
|
+
collectFiles(message.content, files);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (message.role === "assistant") {
|
|
135
|
+
const calls = parseToolCalls(message.content);
|
|
136
|
+
for (const call of calls)
|
|
137
|
+
actions.push(describeCall(call));
|
|
138
|
+
const visible = stripToolCalls(message.content);
|
|
139
|
+
if (visible)
|
|
140
|
+
notes.push(clip(visible, 300));
|
|
141
|
+
collectFiles(message.content, files);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const lines = message.content.split(/\r?\n/);
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
if (/error|failed|not found|denied|timed out/i.test(line))
|
|
147
|
+
outcomes.push(clip(line, 240));
|
|
148
|
+
}
|
|
149
|
+
collectFiles(message.content, files);
|
|
150
|
+
}
|
|
151
|
+
const sections = [
|
|
152
|
+
"# Session summary",
|
|
153
|
+
"",
|
|
154
|
+
"## Requests",
|
|
155
|
+
...limitLines(requests, 8).map((line) => `- ${line}`),
|
|
156
|
+
"",
|
|
157
|
+
"## Actions",
|
|
158
|
+
...limitLines(actions, 20).map((line) => `- ${line}`),
|
|
159
|
+
"",
|
|
160
|
+
"## Outcomes",
|
|
161
|
+
...limitLines(outcomes, 10).map((line) => `- ${line}`),
|
|
162
|
+
"",
|
|
163
|
+
"## Files touched",
|
|
164
|
+
...(files.size > 0 ? Array.from(files).slice(0, 40).map((file) => `- ${file}`) : ["- (none recorded)"]),
|
|
165
|
+
"",
|
|
166
|
+
"## Notes",
|
|
167
|
+
...limitLines(notes, 10).map((line) => `- ${line}`),
|
|
168
|
+
];
|
|
169
|
+
const budget = Math.max(200, Math.floor(config.maxSummaryTokens));
|
|
170
|
+
const kept = [];
|
|
171
|
+
let used = 0;
|
|
172
|
+
for (const line of sections) {
|
|
173
|
+
const cost = estTokens(line + "\n", options);
|
|
174
|
+
if (used + cost > budget) {
|
|
175
|
+
kept.push("[... summary truncated ...]");
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
kept.push(line);
|
|
179
|
+
used += cost;
|
|
180
|
+
}
|
|
181
|
+
return kept.join("\n").trimEnd() + "\n";
|
|
182
|
+
}
|
|
183
|
+
function describeCall(call) {
|
|
184
|
+
const tool = String(call?.tool ?? "unknown");
|
|
185
|
+
if (tool === "shell" && typeof call.command === "string")
|
|
186
|
+
return `shell: ${clip(call.command, 200)}`;
|
|
187
|
+
if (typeof call.path === "string")
|
|
188
|
+
return `${tool}: ${call.path}`;
|
|
189
|
+
if (typeof call.from === "string" || typeof call.to === "string")
|
|
190
|
+
return `${tool}: ${call.from ?? "?"} -> ${call.to ?? "?"}`;
|
|
191
|
+
if (typeof call.pattern === "string")
|
|
192
|
+
return `${tool}: /${clip(call.pattern, 120)}/`;
|
|
193
|
+
return tool;
|
|
194
|
+
}
|
|
195
|
+
function collectFiles(text, files) {
|
|
196
|
+
const matches = text.match(/[A-Za-z0-9_@./\\-]+\.[A-Za-z0-9]{1,8}\b/g);
|
|
197
|
+
if (!matches)
|
|
198
|
+
return;
|
|
199
|
+
for (const match of matches) {
|
|
200
|
+
if (match.length <= 3 || match.length > 240)
|
|
201
|
+
continue;
|
|
202
|
+
if (/^\d+(\.\d+)+$/.test(match))
|
|
203
|
+
continue;
|
|
204
|
+
files.add(match);
|
|
205
|
+
if (files.size >= 200)
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function limitLines(lines, max) {
|
|
210
|
+
return lines.slice(0, max);
|
|
211
|
+
}
|
|
212
|
+
function clip(text, max) {
|
|
213
|
+
const oneLine = String(text).replace(/\s+/g, " ").trim();
|
|
214
|
+
return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
|
|
215
|
+
}
|
|
216
|
+
function writeSummaryFile(summary) {
|
|
217
|
+
try {
|
|
218
|
+
const dir = getPaths().sessions;
|
|
219
|
+
mkdirSync(dir, { recursive: true });
|
|
220
|
+
const file = join(dir, `summary-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.md`);
|
|
221
|
+
writeFileSync(file, summary, "utf8");
|
|
222
|
+
return file;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context compaction for long agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* The chat loop keeps appending tool results to the conversation, so a long
|
|
5
|
+
* investigation can grow past the model's window and fail with a provider 400.
|
|
6
|
+
* Instead of silently truncating the tail (which loses the current task), we
|
|
7
|
+
* keep the first user request and the most recent turns, and shrink the middle
|
|
8
|
+
* into short summaries with an explicit marker.
|
|
9
|
+
*
|
|
10
|
+
* Token counts are approximated at 4 characters per token. That is deliberately
|
|
11
|
+
* crude: it must work for every provider without a tokenizer dependency.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChatMessage } from "../types.js";
|
|
14
|
+
export interface CompactOptions {
|
|
15
|
+
/** Character budget for the whole history (default 400k ≈ 100k tokens). */
|
|
16
|
+
maxChars?: number;
|
|
17
|
+
/** Number of trailing messages to keep verbatim. */
|
|
18
|
+
keepRecent?: number;
|
|
19
|
+
/** Maximum characters kept from one old tool result. */
|
|
20
|
+
toolSummaryChars?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface CompactResult {
|
|
23
|
+
messages: ChatMessage[];
|
|
24
|
+
compacted: boolean;
|
|
25
|
+
beforeChars: number;
|
|
26
|
+
afterChars: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function historyChars(messages: ChatMessage[]): number;
|
|
29
|
+
export declare function compactHistory(messages: ChatMessage[], options?: CompactOptions): CompactResult;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const TOOL_MARKER = "FastCode executed the requested local tools.";
|
|
2
|
+
export function historyChars(messages) {
|
|
3
|
+
return messages.reduce((sum, message) => sum + (message.content?.length ?? 0), 0);
|
|
4
|
+
}
|
|
5
|
+
export function compactHistory(messages, options = {}) {
|
|
6
|
+
const beforeChars = historyChars(messages);
|
|
7
|
+
const maxChars = positive(options.maxChars, 400_000);
|
|
8
|
+
const keepRecent = Math.max(2, Math.floor(positive(options.keepRecent, 6)));
|
|
9
|
+
const toolSummaryChars = Math.max(200, Math.floor(positive(options.toolSummaryChars, 2_000)));
|
|
10
|
+
if (beforeChars <= maxChars || messages.length <= keepRecent + 1) {
|
|
11
|
+
return { messages, compacted: false, beforeChars, afterChars: beforeChars };
|
|
12
|
+
}
|
|
13
|
+
// Keep the original request (usually the first message) plus the newest
|
|
14
|
+
// turns. Everything between is summarized, never dropped without a marker.
|
|
15
|
+
const head = messages[0]?.role === "user" ? [messages[0]] : [];
|
|
16
|
+
const tailStart = Math.max(head.length, messages.length - keepRecent);
|
|
17
|
+
const middle = messages.slice(head.length, tailStart);
|
|
18
|
+
const summarized = middle.map((message) => ({
|
|
19
|
+
role: message.role,
|
|
20
|
+
content: summarizeMessage(message.content, toolSummaryChars),
|
|
21
|
+
}));
|
|
22
|
+
const marker = {
|
|
23
|
+
role: "system",
|
|
24
|
+
content: `[context compacted: ${middle.length} earlier message(s) were summarized to stay within the context window. ` +
|
|
25
|
+
`Recent tool results are kept in full. If an earlier detail is missing, re-read that file with a tool.]`,
|
|
26
|
+
};
|
|
27
|
+
const next = [...head, marker, ...summarized, ...messages.slice(tailStart)];
|
|
28
|
+
return {
|
|
29
|
+
messages: next,
|
|
30
|
+
compacted: true,
|
|
31
|
+
beforeChars,
|
|
32
|
+
afterChars: historyChars(next),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function summarizeMessage(content, limit) {
|
|
36
|
+
if (!content)
|
|
37
|
+
return content;
|
|
38
|
+
if (content.includes(TOOL_MARKER))
|
|
39
|
+
return summarizeToolMessage(content, limit);
|
|
40
|
+
if (content.length <= limit)
|
|
41
|
+
return content;
|
|
42
|
+
const head = content.slice(0, Math.floor(limit * 0.6));
|
|
43
|
+
const tail = content.slice(-Math.floor(limit * 0.3));
|
|
44
|
+
return `${head}\n[… ${content.length - head.length - tail.length} chars summarized …]\n${tail}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Tool results have the shape:
|
|
48
|
+
* FastCode executed ...\n\nTool result 1 (ok)\ncall: {...}\noutput:\n```...```
|
|
49
|
+
* Keep the call and the start/end of each output so file paths and errors
|
|
50
|
+
* survive compaction.
|
|
51
|
+
*/
|
|
52
|
+
function summarizeToolMessage(content, limit) {
|
|
53
|
+
if (content.length <= limit)
|
|
54
|
+
return content;
|
|
55
|
+
const keep = Math.max(120, Math.floor((limit - 240) / 2));
|
|
56
|
+
const head = content.slice(0, keep);
|
|
57
|
+
const tail = content.slice(-keep);
|
|
58
|
+
return `${head}\n[… ${content.length - keep * 2} chars of tool output summarized; re-run the tool if you need the middle …]\n${tail}`;
|
|
59
|
+
}
|
|
60
|
+
function positive(value, fallback) {
|
|
61
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
62
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dangerous-command guard.
|
|
3
|
+
*
|
|
4
|
+
* Blocks destructive shell patterns before execution. Word-boundary escapes
|
|
5
|
+
* are written as character classes so the intent is identical without
|
|
6
|
+
* boundary syntax.
|
|
7
|
+
*
|
|
8
|
+
* Overrides:
|
|
9
|
+
* FASTCODE_ALLOW_DANGEROUS=1 bypass the deny list (reported in output)
|
|
10
|
+
* FASTCODE_SHELL_ALLOWLIST=a;b only commands matching these regexes run
|
|
11
|
+
* FASTCODE_SHELL_DENY_EXTRA=x;y extra regexes to deny
|
|
12
|
+
* FASTCODE_RESTRICT_TO_WORKSPACE=1 also reject ../ and .. escapes
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_DENY_PATTERNS: string[];
|
|
15
|
+
export declare const FORK_BOMB_MARKERS: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Decide whether a shell command may run.
|
|
18
|
+
* Returns { allow: true } or { allow: false, output: explanation }.
|
|
19
|
+
*/
|
|
20
|
+
export declare function evaluateShellCommand(command: any): {
|
|
21
|
+
allow: boolean;
|
|
22
|
+
output: any;
|
|
23
|
+
bypassed: boolean;
|
|
24
|
+
pattern?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
allow: boolean;
|
|
27
|
+
pattern: string;
|
|
28
|
+
output: string;
|
|
29
|
+
bypassed?: undefined;
|
|
30
|
+
} | {
|
|
31
|
+
allow: boolean;
|
|
32
|
+
output: string;
|
|
33
|
+
bypassed?: undefined;
|
|
34
|
+
pattern?: undefined;
|
|
35
|
+
};
|
|
36
|
+
export declare const READ_ONLY_TOOLS: Set<string>;
|
|
37
|
+
export declare function splitCommandSegments(command: any): any[];
|
|
38
|
+
export declare function isReadOnlyCommand(command: any): boolean;
|
|
39
|
+
export declare function isReadOnlyToolCall(call: any): boolean;
|
|
40
|
+
export declare function loopExemptTools(): Set<string>;
|
|
41
|
+
export declare function isLoopExempt(tool: any): boolean;
|
|
42
|
+
export declare function stableStringify(value: any): any;
|
|
43
|
+
export declare function toolCallFingerprint(call: any, cwd?: string): string;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { normalize, resolve } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Dangerous-command guard.
|
|
4
|
+
*
|
|
5
|
+
* Blocks destructive shell patterns before execution. Word-boundary escapes
|
|
6
|
+
* are written as character classes so the intent is identical without
|
|
7
|
+
* boundary syntax.
|
|
8
|
+
*
|
|
9
|
+
* Overrides:
|
|
10
|
+
* FASTCODE_ALLOW_DANGEROUS=1 bypass the deny list (reported in output)
|
|
11
|
+
* FASTCODE_SHELL_ALLOWLIST=a;b only commands matching these regexes run
|
|
12
|
+
* FASTCODE_SHELL_DENY_EXTRA=x;y extra regexes to deny
|
|
13
|
+
* FASTCODE_RESTRICT_TO_WORKSPACE=1 also reject ../ and .. escapes
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_DENY_PATTERNS = [
|
|
16
|
+
"(^|[^a-z])rm +-[rf]{1,2}([^a-z]|$)",
|
|
17
|
+
"(^|[^a-z])del +/[fq]([^a-z]|$)",
|
|
18
|
+
"(^|[^a-z])rmdir +/s([^a-z]|$)",
|
|
19
|
+
"(^|[^a-z-])(format|mkfs|diskpart)([^a-z]|$)",
|
|
20
|
+
"(^|[^a-z])dd +if=",
|
|
21
|
+
"> */dev/sd",
|
|
22
|
+
"(^|[^a-z])(shutdown|reboot|poweroff)([^a-z]|$)",
|
|
23
|
+
];
|
|
24
|
+
export const FORK_BOMB_MARKERS = [":(){", ":|:&"];
|
|
25
|
+
function enabled(name) {
|
|
26
|
+
const value = process.env[name];
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
function splitList(value) {
|
|
32
|
+
return (value ?? "").split(/[;,]/).map((entry) => entry.trim()).filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
function matches(lower, pattern) {
|
|
35
|
+
try {
|
|
36
|
+
return new RegExp(pattern).test(lower);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Decide whether a shell command may run.
|
|
44
|
+
* Returns { allow: true } or { allow: false, output: explanation }.
|
|
45
|
+
*/
|
|
46
|
+
export function evaluateShellCommand(command) {
|
|
47
|
+
const cmd = String(command ?? "").trim();
|
|
48
|
+
const lower = cmd.toLowerCase();
|
|
49
|
+
if (enabled("FASTCODE_ALLOW_DANGEROUS"))
|
|
50
|
+
return { allow: true, output: undefined, bypassed: true };
|
|
51
|
+
const deny = DEFAULT_DENY_PATTERNS.concat(splitList(process.env["FASTCODE_SHELL_DENY_EXTRA"]));
|
|
52
|
+
for (const pattern of deny) {
|
|
53
|
+
if (matches(lower, pattern)) {
|
|
54
|
+
return {
|
|
55
|
+
allow: false,
|
|
56
|
+
pattern,
|
|
57
|
+
output: "Error: blocked by the safety guard (dangerous pattern: " + pattern + "). " +
|
|
58
|
+
"Set FASTCODE_ALLOW_DANGEROUS=1 to override.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const marker of FORK_BOMB_MARKERS) {
|
|
63
|
+
if (lower.includes(marker))
|
|
64
|
+
return { allow: false, output: "Error: blocked by the safety guard (fork bomb pattern)." };
|
|
65
|
+
}
|
|
66
|
+
const allowlist = splitList(process.env["FASTCODE_SHELL_ALLOWLIST"]);
|
|
67
|
+
if (allowlist.length > 0 && !allowlist.some((pattern) => matches(lower, pattern)))
|
|
68
|
+
return { allow: false, output: "Error: blocked by the safety guard (not in FASTCODE_SHELL_ALLOWLIST)." };
|
|
69
|
+
if (enabled("FASTCODE_RESTRICT_TO_WORKSPACE")) {
|
|
70
|
+
const backslash = String.fromCharCode(92);
|
|
71
|
+
if (cmd.includes("../") || cmd.includes(".." + backslash))
|
|
72
|
+
return { allow: false, output: "Error: blocked by the safety guard (path traversal)." };
|
|
73
|
+
}
|
|
74
|
+
return { allow: true, output: undefined };
|
|
75
|
+
}
|
|
76
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
77
|
+
"pwd", "read_file", "read_many", "list_files", "grep", "stat_path",
|
|
78
|
+
]);
|
|
79
|
+
const READ_ONLY_COMMANDS = new Set([
|
|
80
|
+
"pwd", "ls", "dir", "cat", "head", "tail", "wc", "stat", "file", "du", "df",
|
|
81
|
+
"grep", "rg", "egrep", "fgrep", "which", "whereis", "where", "whoami", "id",
|
|
82
|
+
"groups", "uname", "date", "uptime", "printenv", "echo", "printf",
|
|
83
|
+
"true", "false", "basename", "dirname", "realpath", "readlink", "tree",
|
|
84
|
+
"more", "less", "jq", "cut", "tr",
|
|
85
|
+
]);
|
|
86
|
+
const READ_ONLY_PREFIXES = new Set([
|
|
87
|
+
"git status", "git diff", "git log", "git show", "git rev-parse",
|
|
88
|
+
"git ls-files", "git blame", "git grep", "git describe", "git merge-base",
|
|
89
|
+
"git cat-file", "git rev-list", "git shortlog", "git whatchanged",
|
|
90
|
+
"npm ls", "npm list", "npm view", "npm outdated", "npm explain", "npm why",
|
|
91
|
+
"pnpm list", "pnpm ls", "yarn list", "yarn info",
|
|
92
|
+
"cargo tree", "go list", "pip list", "pip show",
|
|
93
|
+
"docker ps", "docker images", "docker logs",
|
|
94
|
+
]);
|
|
95
|
+
const NON_READ_ONLY_LEADERS = new Set([
|
|
96
|
+
"cd", "sudo", "doas", "env", "xargs", "time", "command", "eval", "exec",
|
|
97
|
+
"source", ".", "sh", "bash", "zsh", "fish", "powershell", "cmd",
|
|
98
|
+
]);
|
|
99
|
+
function scanCommand(command, onChar) {
|
|
100
|
+
let quote = "";
|
|
101
|
+
let escaped = false;
|
|
102
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
103
|
+
const char = command[i];
|
|
104
|
+
if (escaped) {
|
|
105
|
+
escaped = false;
|
|
106
|
+
onChar(char, i, quote);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === "\\") {
|
|
110
|
+
escaped = true;
|
|
111
|
+
onChar(char, i, quote);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (quote) {
|
|
115
|
+
if (char === quote)
|
|
116
|
+
quote = "";
|
|
117
|
+
onChar(char, i, quote);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === '"' || char === "'") {
|
|
121
|
+
quote = char;
|
|
122
|
+
onChar(char, i, quote);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
onChar(char, i, quote);
|
|
126
|
+
}
|
|
127
|
+
return quote === "";
|
|
128
|
+
}
|
|
129
|
+
function hasWriteRedirection(command) {
|
|
130
|
+
let found = false;
|
|
131
|
+
const balanced = scanCommand(command, (char, index, quote) => {
|
|
132
|
+
if (quote || char !== ">")
|
|
133
|
+
return;
|
|
134
|
+
const previous = command[index - 1];
|
|
135
|
+
if (previous === "=")
|
|
136
|
+
return;
|
|
137
|
+
found = true;
|
|
138
|
+
});
|
|
139
|
+
return !balanced || found;
|
|
140
|
+
}
|
|
141
|
+
function hasCommandSubstitution(command) {
|
|
142
|
+
let found = false;
|
|
143
|
+
const balanced = scanCommand(command, (char, index, quote) => {
|
|
144
|
+
if (quote)
|
|
145
|
+
return;
|
|
146
|
+
if (char === "`" || (char === "$" && command[index + 1] === "("))
|
|
147
|
+
found = true;
|
|
148
|
+
});
|
|
149
|
+
return !balanced || found;
|
|
150
|
+
}
|
|
151
|
+
export function splitCommandSegments(command) {
|
|
152
|
+
const parts = [];
|
|
153
|
+
let current = "";
|
|
154
|
+
let quote = "";
|
|
155
|
+
let escaped = false;
|
|
156
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
157
|
+
const char = command[i];
|
|
158
|
+
if (escaped) {
|
|
159
|
+
current += char;
|
|
160
|
+
escaped = false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === "\\") {
|
|
164
|
+
current += char;
|
|
165
|
+
escaped = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (quote) {
|
|
169
|
+
current += char;
|
|
170
|
+
if (char === quote)
|
|
171
|
+
quote = "";
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (char === '"' || char === "'") {
|
|
175
|
+
quote = char;
|
|
176
|
+
current += char;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const next = command[i + 1];
|
|
180
|
+
if ((char === "&" && next === "&") || (char === "|" && next === "|")) {
|
|
181
|
+
parts.push(current);
|
|
182
|
+
current = "";
|
|
183
|
+
i += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (char === ";" || char === "|" || char === "&" || char === "\n") {
|
|
187
|
+
parts.push(current);
|
|
188
|
+
current = "";
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
current += char;
|
|
192
|
+
}
|
|
193
|
+
parts.push(current);
|
|
194
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
195
|
+
}
|
|
196
|
+
function isReadOnlySegment(segment) {
|
|
197
|
+
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
198
|
+
if (tokens.length === 0)
|
|
199
|
+
return true;
|
|
200
|
+
const first = tokens[0].toLowerCase();
|
|
201
|
+
const second = (tokens[1] ?? "").toLowerCase();
|
|
202
|
+
if (NON_READ_ONLY_LEADERS.has(first))
|
|
203
|
+
return false;
|
|
204
|
+
if (READ_ONLY_PREFIXES.has(first + " " + second))
|
|
205
|
+
return true;
|
|
206
|
+
if (READ_ONLY_COMMANDS.has(first))
|
|
207
|
+
return true;
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
export function isReadOnlyCommand(command) {
|
|
211
|
+
const cmd = String(command ?? "").trim();
|
|
212
|
+
if (!cmd)
|
|
213
|
+
return false;
|
|
214
|
+
if (hasWriteRedirection(cmd) || hasCommandSubstitution(cmd))
|
|
215
|
+
return false;
|
|
216
|
+
const segments = splitCommandSegments(cmd);
|
|
217
|
+
if (segments.length === 0)
|
|
218
|
+
return false;
|
|
219
|
+
return segments.every(isReadOnlySegment);
|
|
220
|
+
}
|
|
221
|
+
export function isReadOnlyToolCall(call) {
|
|
222
|
+
const tool = typeof call?.tool === "string" ? call.tool : "";
|
|
223
|
+
if (tool === "shell")
|
|
224
|
+
return isReadOnlyCommand(call.command);
|
|
225
|
+
return READ_ONLY_TOOLS.has(tool);
|
|
226
|
+
}
|
|
227
|
+
export function loopExemptTools() {
|
|
228
|
+
const raw = process.env["FASTCODE_LOOP_EXEMPT"] ?? "";
|
|
229
|
+
return new Set(raw.split(",").map((entry) => entry.trim()).filter(Boolean));
|
|
230
|
+
}
|
|
231
|
+
export function isLoopExempt(tool) {
|
|
232
|
+
return loopExemptTools().has(String(tool ?? ""));
|
|
233
|
+
}
|
|
234
|
+
function stableValue(value) {
|
|
235
|
+
if (value === undefined)
|
|
236
|
+
return "null";
|
|
237
|
+
if (value === null || typeof value !== "object")
|
|
238
|
+
return JSON.stringify(value);
|
|
239
|
+
if (Array.isArray(value))
|
|
240
|
+
return "[" + value.map((entry) => stableValue(entry)).join(",") + "]";
|
|
241
|
+
const keys = Object.keys(value).sort().filter((key) => value[key] !== undefined);
|
|
242
|
+
return "{" + keys.map((key) => JSON.stringify(key) + ":" + stableValue(value[key])).join(",") + "}";
|
|
243
|
+
}
|
|
244
|
+
export function stableStringify(value) {
|
|
245
|
+
return stableValue(value);
|
|
246
|
+
}
|
|
247
|
+
function normalizeFingerprintPath(value, cwd) {
|
|
248
|
+
let normalized = normalize(resolve(cwd, value));
|
|
249
|
+
if (normalized.length > 1)
|
|
250
|
+
normalized = normalized.replace(/[\\/]+$/, "");
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
function fingerprintValue(key, value, cwd) {
|
|
254
|
+
if (typeof value === "string" && (key === "path" || key === "from" || key === "to" || key === "cwd"))
|
|
255
|
+
return normalizeFingerprintPath(value, cwd);
|
|
256
|
+
if (key === "paths" && Array.isArray(value))
|
|
257
|
+
return value.map((entry) => typeof entry === "string" ? normalizeFingerprintPath(entry, cwd) : entry);
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
export function toolCallFingerprint(call, cwd = process.cwd()) {
|
|
261
|
+
const tool = typeof call?.tool === "string" ? call.tool : "unknown";
|
|
262
|
+
const args = {};
|
|
263
|
+
for (const key of Object.keys(call ?? {}).sort()) {
|
|
264
|
+
if (key === "tool" || key === "timeoutMs")
|
|
265
|
+
continue;
|
|
266
|
+
args[key] = fingerprintValue(key, call[key], cwd);
|
|
267
|
+
}
|
|
268
|
+
return tool + ":" + stableStringify(args);
|
|
269
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface IgnoreRule {
|
|
2
|
+
regex: RegExp;
|
|
3
|
+
negate: boolean;
|
|
4
|
+
dirOnly: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface IgnoreMatcher {
|
|
7
|
+
ignored(name: string, fullPath: string, isDir: boolean): boolean;
|
|
8
|
+
isBinary(name: string): boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function isBinaryName(name: string): boolean;
|
|
11
|
+
export declare function createIgnore(root: string): IgnoreMatcher;
|