@datename/fastcode-termux 0.1.6 → 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 +67 -16
- 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 +83 -0
- package/dist/agent/approval.js +42 -0
- package/dist/agent/compaction.d.ts +41 -0
- package/dist/agent/compaction.js +270 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +24 -16
- package/dist/agent/guards.js +1167 -18
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- 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 +22 -0
- package/dist/agent/process-tree.js +102 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +169 -39
- package/dist/agent/registry.d.ts +19 -0
- package/dist/agent/registry.js +247 -0
- package/dist/agent/runner.d.ts +127 -0
- package/dist/agent/runner.js +391 -0
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +51 -0
- package/dist/agent/shell-session.js +611 -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 +82 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +28 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +92 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +53 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +43 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +54 -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 +26 -0
- package/dist/agent/tools/move_path.d.ts +9 -0
- package/dist/agent/tools/move_path.js +73 -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 +81 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +43 -0
- package/dist/agent/tools/readers.d.ts +58 -0
- package/dist/agent/tools/readers.js +426 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +63 -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 +65 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +19 -0
- package/dist/agent/tools/validate.js +92 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +28 -0
- package/dist/agent/tools.d.ts +3 -1
- package/dist/agent/tools.js +2 -377
- package/dist/app.d.ts +2 -1
- package/dist/app.js +846 -173
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +444 -333
- package/dist/commands/about.js +1 -1
- 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 +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +22 -0
- package/dist/commands/compact.js +34 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +25 -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/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/index.js +24 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +17 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +13 -0
- package/dist/commands/login.d.ts +6 -4
- package/dist/commands/login.js +24 -22
- package/dist/commands/logout.js +8 -16
- package/dist/commands/model.d.ts +1 -0
- package/dist/commands/model.js +10 -22
- package/dist/commands/provider.d.ts +1 -2
- package/dist/commands/provider.js +4 -11
- package/dist/commands/skills.js +34 -4
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/commands/whoami.js +6 -5
- package/dist/config/store.d.ts +53 -12
- package/dist/config/store.js +163 -73
- package/dist/config/trust.d.ts +30 -8
- package/dist/config/trust.js +139 -14
- 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 +48 -4
- package/dist/lib/diagnostics.js +90 -8
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/http.d.ts +11 -1
- package/dist/lib/http.js +62 -4
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- 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/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/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/custom.d.ts +9 -0
- package/dist/providers/deepseek.d.ts +10 -0
- package/dist/providers/deepseek.js +3 -2
- package/dist/providers/google.d.ts +9 -0
- package/dist/providers/groq.d.ts +10 -0
- package/dist/providers/groq.js +2 -1
- package/dist/providers/index.d.ts +10 -201
- package/dist/providers/index.js +30 -0
- package/dist/providers/mistral.d.ts +10 -0
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/moonshot.d.ts +10 -0
- package/dist/providers/moonshot.js +2 -1
- package/dist/providers/ollama.d.ts +9 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +33 -2
- package/dist/providers/openaiCompatible.js +78 -16
- package/dist/providers/openrouter.d.ts +10 -0
- package/dist/providers/openrouter.js +2 -1
- package/dist/providers/qwen.d.ts +10 -0
- package/dist/providers/qwen.js +2 -1
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/providers/xai.d.ts +10 -0
- package/dist/providers/xai.js +2 -1
- package/dist/providers/zhipu.d.ts +10 -0
- package/dist/providers/zhipu.js +2 -1
- package/dist/session/log.d.ts +49 -14
- package/dist/session/log.js +236 -11
- package/dist/session/projection.d.ts +76 -0
- package/dist/session/projection.js +526 -0
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +75 -1
- package/dist/ui/Message.d.ts +1 -7
- package/dist/ui/Message.js +13 -2
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +1 -2
- package/dist/ui/Prompt.js +2 -2
- package/dist/ui/Spinner.js +11 -5
- package/dist/ui/StatusBar.d.ts +2 -1
- package/dist/ui/StatusBar.js +6 -5
- 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 +94 -0
- package/src/agent/compaction.ts +321 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +1239 -19
- package/src/agent/ignore.ts +147 -0
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +114 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +191 -40
- package/src/agent/registry.ts +270 -0
- package/src/agent/runner.ts +486 -0
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +673 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +86 -0
- package/src/agent/tools/append_file.ts +30 -0
- package/src/agent/tools/apply_patch.ts +95 -0
- package/src/agent/tools/copy_file.ts +56 -0
- package/src/agent/tools/delete_file.ts +45 -0
- package/src/agent/tools/grep.ts +57 -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 +28 -0
- package/src/agent/tools/move_path.ts +83 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +84 -0
- package/src/agent/tools/read_many.ts +45 -0
- package/src/agent/tools/readers.ts +491 -0
- package/src/agent/tools/replace_in_file.ts +66 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +84 -0
- package/src/agent/tools/validate.ts +104 -0
- package/src/agent/tools/write_file.ts +30 -0
- package/src/agent/tools.ts +3 -377
- package/src/app.ts +853 -170
- package/src/{cli.ts → cli-main.ts} +64 -9
- package/src/commands/about.ts +1 -1
- 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 +1 -1
- package/src/commands/compact.ts +35 -0
- package/src/commands/context.ts +26 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/jobs.ts +18 -0
- package/src/commands/kill.ts +14 -0
- package/src/commands/login.ts +24 -22
- package/src/commands/logout.ts +8 -16
- package/src/commands/model.ts +10 -22
- package/src/commands/provider.ts +4 -11
- package/src/commands/skills.ts +34 -4
- package/src/commands/steer.ts +13 -0
- package/src/commands/threads.ts +28 -0
- package/src/commands/whoami.ts +6 -5
- package/src/config/store.ts +168 -77
- package/src/config/trust.ts +181 -22
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +90 -8
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/http.ts +66 -4
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- 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/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/providers/anthropic.ts +13 -1
- package/src/providers/deepseek.ts +3 -2
- package/src/providers/groq.ts +2 -1
- package/src/providers/index.ts +32 -1
- package/src/providers/mistral.ts +2 -1
- package/src/providers/moonshot.ts +2 -1
- package/src/providers/openai.ts +2 -1
- package/src/providers/openaiCompatible.ts +104 -19
- package/src/providers/openrouter.ts +2 -1
- package/src/providers/qwen.ts +2 -1
- package/src/providers/spec.ts +42 -0
- package/src/providers/xai.ts +2 -1
- package/src/providers/zhipu.ts +2 -1
- package/src/session/log.ts +311 -29
- package/src/session/projection.ts +621 -0
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +79 -2
- package/src/ui/Message.ts +13 -2
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +2 -2
- package/src/ui/Spinner.ts +11 -5
- package/src/ui/StatusBar.ts +6 -5
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { estMessagesTokens, estTokens, getModelMeta } from "../lib/model-meta.js";
|
|
4
|
+
import { compactMessages, DEFAULT_COMPACTION_CONFIG } from "./compaction.js";
|
|
5
|
+
import { isLoopExempt, isReadOnlyToolCall, toolCallFingerprint } from "./guards.js";
|
|
6
|
+
import { isNativeUnsupportedError } from "./protocol-mode.js";
|
|
7
|
+
import { formatToolResults, parseToolCalls, stripToolCalls } from "./protocol.js";
|
|
8
|
+
import { runStream } from "./stream.js";
|
|
9
|
+
import { runAgentTool } from "./tools.js";
|
|
10
|
+
export const LOOP_WARNING = "[loop detection] You appear to be repeating the same tool call. " +
|
|
11
|
+
"Do not repeat it again: inspect the result you already have, or change approach.";
|
|
12
|
+
/**
|
|
13
|
+
* Sliding-window loop detector.
|
|
14
|
+
*
|
|
15
|
+
* Semantics (fixed by FC-004, do not re-interpret):
|
|
16
|
+
* - hits count the last `window` tool calls, not turns;
|
|
17
|
+
* - read-only tools use a higher threshold than side-effecting tools (D-02);
|
|
18
|
+
* - the first threshold hit yields a warning and resets that fingerprint's count;
|
|
19
|
+
* - only a second full accumulation yields a terminal error;
|
|
20
|
+
* - the table lives for one run (turn), so a new user task starts clean;
|
|
21
|
+
* - fingerprints are normalized per tool call (absolute paths, timeoutMs removed).
|
|
22
|
+
*/
|
|
23
|
+
class LoopState {
|
|
24
|
+
entries = new Map();
|
|
25
|
+
calls = 0;
|
|
26
|
+
readOnlyThreshold;
|
|
27
|
+
sideEffectThreshold;
|
|
28
|
+
window;
|
|
29
|
+
exemptReadOnly;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.readOnlyThreshold = positive(options.readOnlyThreshold, 6);
|
|
32
|
+
this.sideEffectThreshold = positive(options.sideEffectThreshold, 3);
|
|
33
|
+
this.window = positive(options.window, 5);
|
|
34
|
+
this.exemptReadOnly = options.exemptReadOnly === true;
|
|
35
|
+
}
|
|
36
|
+
check(fingerprint, readOnly) {
|
|
37
|
+
if (readOnly && this.exemptReadOnly)
|
|
38
|
+
return { action: "ok", occurrences: 0 };
|
|
39
|
+
this.calls += 1;
|
|
40
|
+
const index = this.calls;
|
|
41
|
+
let entry = this.entries.get(fingerprint);
|
|
42
|
+
if (!entry) {
|
|
43
|
+
entry = { warned: false, hits: [] };
|
|
44
|
+
this.entries.set(fingerprint, entry);
|
|
45
|
+
}
|
|
46
|
+
entry.hits = entry.hits.filter((hit) => index - hit < this.window);
|
|
47
|
+
entry.hits.push(index);
|
|
48
|
+
const threshold = readOnly ? this.readOnlyThreshold : this.sideEffectThreshold;
|
|
49
|
+
if (entry.hits.length < threshold)
|
|
50
|
+
return { action: "ok", occurrences: entry.hits.length };
|
|
51
|
+
const occurrences = entry.hits.length;
|
|
52
|
+
if (!entry.warned) {
|
|
53
|
+
entry.warned = true;
|
|
54
|
+
entry.hits = [];
|
|
55
|
+
return { action: "warn", occurrences };
|
|
56
|
+
}
|
|
57
|
+
return { action: "stop", occurrences };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function* runAgent(options) {
|
|
61
|
+
const provider = options.provider;
|
|
62
|
+
const model = options.model;
|
|
63
|
+
const cwd = options.cwd ?? process.cwd();
|
|
64
|
+
const signal = options.signal;
|
|
65
|
+
const maxTurns = Math.max(1, Math.floor(positive(options.maxTurns, 8)));
|
|
66
|
+
const tokenBudget = optionalPositive(options.tokenBudget);
|
|
67
|
+
const costBudget = optionalPositive(options.costBudgetUsd);
|
|
68
|
+
const loop = new LoopState(options.loopDetection);
|
|
69
|
+
let history = [...(options.messages ?? []), { role: "user", content: options.prompt }];
|
|
70
|
+
let currentText = "";
|
|
71
|
+
let lastVisible = "";
|
|
72
|
+
let consumedInput = 0;
|
|
73
|
+
let spentCost = 0;
|
|
74
|
+
let stopReason = "complete";
|
|
75
|
+
let errorMessage = "";
|
|
76
|
+
let naturalFinish = false;
|
|
77
|
+
let toolCallSeq = 0;
|
|
78
|
+
let protocol = options.protocol ?? "legacy";
|
|
79
|
+
let fellBack = false;
|
|
80
|
+
const activeFiles = new Map();
|
|
81
|
+
for (let turn = 0; turn < maxTurns; turn += 1) {
|
|
82
|
+
if (signal?.aborted) {
|
|
83
|
+
stopReason = "aborted";
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
for (const steering of options.takeSteering?.() ?? []) {
|
|
87
|
+
history = [...history, { role: "system", content: "[user steering] " + steering }];
|
|
88
|
+
yield { type: "notice", text: "Steering applied to the next request." };
|
|
89
|
+
}
|
|
90
|
+
const contextWindow = options.contextWindow ?? getModelMeta(provider.id, model).contextWindow;
|
|
91
|
+
const compacted = compactMessages(history, options.compaction ?? DEFAULT_COMPACTION_CONFIG, {
|
|
92
|
+
contextWindow,
|
|
93
|
+
providerId: provider.id,
|
|
94
|
+
model,
|
|
95
|
+
serverOpaqueState: options.serverOpaqueState,
|
|
96
|
+
});
|
|
97
|
+
if (compacted.compacted) {
|
|
98
|
+
history = compacted.messages;
|
|
99
|
+
yield {
|
|
100
|
+
type: "compaction",
|
|
101
|
+
beforeChars: compacted.beforeChars,
|
|
102
|
+
afterChars: compacted.afterChars,
|
|
103
|
+
beforeTokens: compacted.beforeTokens,
|
|
104
|
+
afterTokens: compacted.afterTokens,
|
|
105
|
+
droppedMessages: compacted.droppedMessages,
|
|
106
|
+
keptMessages: compacted.keptMessages,
|
|
107
|
+
reason: compacted.reason ?? "threshold",
|
|
108
|
+
source: compacted.source === "server" ? "server" : "local",
|
|
109
|
+
summaryFile: compacted.summaryFile,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const promptTokens = estMessagesTokens(history, { providerId: provider.id, model }) +
|
|
113
|
+
estTokens(options.system ?? "", { providerId: provider.id, model });
|
|
114
|
+
if (tokenBudget !== undefined && consumedInput + promptTokens > tokenBudget) {
|
|
115
|
+
stopReason = "token_budget";
|
|
116
|
+
errorMessage = `Token budget exhausted (${consumedInput + promptTokens} > ${tokenBudget} estimated input tokens). Partial output is kept.`;
|
|
117
|
+
yield { type: "error", kind: "token_budget", message: errorMessage, text: lastVisible, used: consumedInput + promptTokens };
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (costBudget !== undefined && spentCost >= costBudget) {
|
|
121
|
+
stopReason = "cost_budget";
|
|
122
|
+
errorMessage = `Cost budget exhausted ($${spentCost.toFixed(4)} of $${costBudget}). Partial output is kept.`;
|
|
123
|
+
yield { type: "error", kind: "cost_budget", message: errorMessage, text: lastVisible };
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
currentText = "";
|
|
127
|
+
let resetNotice = "";
|
|
128
|
+
let streamed;
|
|
129
|
+
try {
|
|
130
|
+
streamed = await runStream(provider, {
|
|
131
|
+
model,
|
|
132
|
+
messages: history,
|
|
133
|
+
system: options.system,
|
|
134
|
+
reasoningEffort: options.reasoningEffort,
|
|
135
|
+
...(protocol === "native" && options.tools && options.tools.length > 0 ? { tools: options.tools } : {}),
|
|
136
|
+
}, options.apiKey, {
|
|
137
|
+
signal,
|
|
138
|
+
idleTimeoutMs: options.idleTimeoutMs,
|
|
139
|
+
onDelta: (delta, text) => {
|
|
140
|
+
currentText = text;
|
|
141
|
+
options.onDelta?.(delta, text);
|
|
142
|
+
},
|
|
143
|
+
onReset: () => {
|
|
144
|
+
resetNotice = "Retried stream returned different content; the earlier partial output was discarded and replaced by the new stream.";
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (err?.name === "AbortError" || signal?.aborted) {
|
|
150
|
+
stopReason = "aborted";
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
errorMessage = String(err?.message ?? err);
|
|
154
|
+
if (protocol === "native" && !fellBack && isNativeUnsupportedError(errorMessage)) {
|
|
155
|
+
fellBack = true;
|
|
156
|
+
protocol = "legacy";
|
|
157
|
+
options.onProtocolFallback?.(`Native tools were rejected (${errorMessage.slice(0, 160)}); retrying with the pseudo protocol.`);
|
|
158
|
+
turn -= 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
stopReason = "stream_error";
|
|
162
|
+
const partial = stripToolCalls(String(err?.partialText ?? "")).trim();
|
|
163
|
+
if (partial)
|
|
164
|
+
lastVisible = partial;
|
|
165
|
+
yield { type: "error", kind: "stream_error", message: errorMessage, text: partial || undefined };
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (signal?.aborted) {
|
|
169
|
+
stopReason = "aborted";
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (resetNotice)
|
|
173
|
+
yield { type: "notice", text: resetNotice };
|
|
174
|
+
if (streamed.retried > 0)
|
|
175
|
+
yield { type: "notice", text: `Network stream was interrupted; retried ${streamed.retried} time(s).` };
|
|
176
|
+
if (streamed.usage) {
|
|
177
|
+
consumedInput += streamed.usage.inputTokens ?? promptTokens;
|
|
178
|
+
spentCost += streamed.usage.costUsd ?? 0;
|
|
179
|
+
yield { type: "usage", usage: streamed.usage };
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
consumedInput += promptTokens;
|
|
183
|
+
}
|
|
184
|
+
if (streamed.incomplete) {
|
|
185
|
+
const partial = stripToolCalls(streamed.text).trim();
|
|
186
|
+
if (partial)
|
|
187
|
+
lastVisible = partial;
|
|
188
|
+
stopReason = "incomplete";
|
|
189
|
+
errorMessage = `Connection ended before the response finished${streamed.retried > 0 ? ` (retried ${streamed.retried}×)` : ""}. ` +
|
|
190
|
+
"Partial output is kept — type 继续 to continue.";
|
|
191
|
+
yield { type: "notice", text: errorMessage };
|
|
192
|
+
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
const visibleText = stripToolCalls(streamed.text);
|
|
196
|
+
if (visibleText)
|
|
197
|
+
lastVisible = visibleText;
|
|
198
|
+
const nativeCalls = protocol === "native" && options.allowTools !== false ? (streamed.toolCalls ?? []) : [];
|
|
199
|
+
if (nativeCalls.length > 0) {
|
|
200
|
+
yield { type: "assistant_raw", text: streamed.text, toolCalls: nativeCalls };
|
|
201
|
+
if (visibleText)
|
|
202
|
+
yield { type: "assistant_text", text: visibleText };
|
|
203
|
+
history = [...history, { role: "assistant", content: streamed.text, toolCalls: nativeCalls }];
|
|
204
|
+
for (const native of nativeCalls) {
|
|
205
|
+
if (signal?.aborted) {
|
|
206
|
+
stopReason = "aborted";
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
const id = native.id || `tc_${++toolCallSeq}`;
|
|
210
|
+
let call;
|
|
211
|
+
let parseError = "";
|
|
212
|
+
try {
|
|
213
|
+
const args = native.arguments ? JSON.parse(native.arguments) : {};
|
|
214
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
215
|
+
throw new Error("arguments must be a JSON object");
|
|
216
|
+
call = { ...args, tool: native.name };
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
parseError = `invalid arguments for ${native.name}: ${err?.message ?? String(err)}`;
|
|
220
|
+
call = { tool: native.name };
|
|
221
|
+
}
|
|
222
|
+
const fingerprint = toolCallFingerprint(call, cwd);
|
|
223
|
+
if (!isLoopExempt(call.tool)) {
|
|
224
|
+
const verdict = loop.check(fingerprint, isReadOnlyToolCall(call));
|
|
225
|
+
if (verdict.action === "warn") {
|
|
226
|
+
yield { type: "loop_detected", fingerprint, occurrences: verdict.occurrences, warned: false };
|
|
227
|
+
history = [...history, { role: "system", content: LOOP_WARNING }];
|
|
228
|
+
}
|
|
229
|
+
else if (verdict.action === "stop") {
|
|
230
|
+
stopReason = "loop_detected";
|
|
231
|
+
errorMessage = `Loop detected: the same tool call repeated ${verdict.occurrences} times. Stopped before executing it again.`;
|
|
232
|
+
yield { type: "error", kind: "loop_detected", message: errorMessage, text: lastVisible };
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
yield { type: "tool_call", id, call, native: true };
|
|
237
|
+
const result = parseError
|
|
238
|
+
? { ok: false, output: parseError }
|
|
239
|
+
: await runAgentTool(call, cwd, { signal, turn: turn + 1, requestApproval: options.requestApproval });
|
|
240
|
+
trackSideEffects(activeFiles, result.sideEffects);
|
|
241
|
+
if (result.egress)
|
|
242
|
+
yield { type: "egress_denied", ...result.egress };
|
|
243
|
+
yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), ...toolResultMeta(result) };
|
|
244
|
+
history = [...history, { role: "tool", toolCallId: id, content: String(result.output ?? "") }];
|
|
245
|
+
}
|
|
246
|
+
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
247
|
+
if (stopReason === "complete")
|
|
248
|
+
options.onNativeOk?.();
|
|
249
|
+
if (stopReason !== "complete")
|
|
250
|
+
break;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const parsed = options.allowTools === false
|
|
254
|
+
? { calls: [], consumedEnd: 0, errors: [] }
|
|
255
|
+
: parseToolCalls(streamed.text);
|
|
256
|
+
const calls = parsed.calls;
|
|
257
|
+
yield { type: "assistant_raw", text: streamed.text };
|
|
258
|
+
if (calls.length === 0) {
|
|
259
|
+
if (parsed.errors.length > 0) {
|
|
260
|
+
// Never let a broken tool-call block end the turn silently: the
|
|
261
|
+
// model would wait forever for a tool result. Inject a visible
|
|
262
|
+
// tool error and continue so it can re-emit valid markup.
|
|
263
|
+
const id = `tc_${++toolCallSeq}`;
|
|
264
|
+
const call = { tool: "protocol_error" };
|
|
265
|
+
const output = "Previous output contained invalid tool-call markup. " +
|
|
266
|
+
"Re-emit a complete <fastcode_tool>{\"tool\":...}</fastcode_tool> block.\n" +
|
|
267
|
+
parsed.errors.join("\n");
|
|
268
|
+
yield { type: "tool_call", id, call };
|
|
269
|
+
yield { type: "tool_result", id, call, ok: false, output, code: "INVALID_ARGS", hint: "Re-emit a complete tool call." };
|
|
270
|
+
history = [...history, { role: "user", content: formatToolResults([{ call, ok: false, output }]) }];
|
|
271
|
+
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
lastVisible = visibleText || streamed.text;
|
|
275
|
+
naturalFinish = true;
|
|
276
|
+
stopReason = "complete";
|
|
277
|
+
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
history = [...history, { role: "assistant", content: streamed.text }];
|
|
281
|
+
if (visibleText)
|
|
282
|
+
yield { type: "assistant_text", text: visibleText };
|
|
283
|
+
const results = [];
|
|
284
|
+
for (const call of calls) {
|
|
285
|
+
if (signal?.aborted) {
|
|
286
|
+
stopReason = "aborted";
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
const fingerprint = toolCallFingerprint(call, cwd);
|
|
290
|
+
if (!isLoopExempt(call.tool)) {
|
|
291
|
+
const verdict = loop.check(fingerprint, isReadOnlyToolCall(call));
|
|
292
|
+
if (verdict.action === "warn") {
|
|
293
|
+
yield { type: "loop_detected", fingerprint, occurrences: verdict.occurrences, warned: false };
|
|
294
|
+
history = [...history, { role: "system", content: LOOP_WARNING }];
|
|
295
|
+
}
|
|
296
|
+
else if (verdict.action === "stop") {
|
|
297
|
+
stopReason = "loop_detected";
|
|
298
|
+
errorMessage = `Loop detected: the same tool call repeated ${verdict.occurrences} times. Stopped before executing it again.`;
|
|
299
|
+
yield { type: "error", kind: "loop_detected", message: errorMessage, text: lastVisible };
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const id = `tc_${++toolCallSeq}`;
|
|
304
|
+
yield { type: "tool_call", id, call };
|
|
305
|
+
const result = await runAgentTool(call, cwd, { signal, turn: turn + 1, requestApproval: options.requestApproval });
|
|
306
|
+
results.push({ call, ok: Boolean(result.ok), output: String(result.output ?? ""), sideEffects: result.sideEffects });
|
|
307
|
+
trackSideEffects(activeFiles, result.sideEffects);
|
|
308
|
+
if (result.egress)
|
|
309
|
+
yield { type: "egress_denied", ...result.egress };
|
|
310
|
+
yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), ...toolResultMeta(result) };
|
|
311
|
+
}
|
|
312
|
+
if (results.length > 0)
|
|
313
|
+
history = [...history, { role: "user", content: formatToolResults(results) }];
|
|
314
|
+
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
315
|
+
if (stopReason !== "complete")
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
if (stopReason === "stream_error") {
|
|
319
|
+
const partial = lastVisible;
|
|
320
|
+
if (partial)
|
|
321
|
+
history = [...history, { role: "assistant", content: partial }];
|
|
322
|
+
yield { type: "done", history, finalText: partial, stopReason };
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (stopReason === "complete" && !naturalFinish)
|
|
326
|
+
stopReason = "max_turns";
|
|
327
|
+
let finalText = "";
|
|
328
|
+
if (stopReason === "aborted") {
|
|
329
|
+
const partial = stripToolCalls(currentText).trim() || lastVisible;
|
|
330
|
+
finalText = (partial ? partial + "\n\n" : "") + "[用户中断]";
|
|
331
|
+
yield { type: "error", kind: "aborted", message: "Cancelled.", text: partial };
|
|
332
|
+
}
|
|
333
|
+
else if (stopReason === "token_budget" || stopReason === "cost_budget" || stopReason === "loop_detected" || stopReason === "incomplete") {
|
|
334
|
+
finalText = lastVisible || errorMessage;
|
|
335
|
+
}
|
|
336
|
+
else if (stopReason === "max_turns") {
|
|
337
|
+
finalText = `Reached the maximum of ${maxTurns} agent tool turns. The work so far is kept in this session — type 继续 to keep going.`;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
finalText = lastVisible;
|
|
341
|
+
}
|
|
342
|
+
if (finalText)
|
|
343
|
+
history = [...history, { role: "assistant", content: finalText }];
|
|
344
|
+
yield { type: "done", history, finalText, stopReason };
|
|
345
|
+
}
|
|
346
|
+
function positive(value, fallback) {
|
|
347
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
348
|
+
}
|
|
349
|
+
function optionalPositive(value) {
|
|
350
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
351
|
+
}
|
|
352
|
+
function toolResultMeta(result) {
|
|
353
|
+
return {
|
|
354
|
+
sideEffects: result.sideEffects,
|
|
355
|
+
...(result.code ? { code: result.code } : {}),
|
|
356
|
+
...(result.hint ? { hint: result.hint } : {}),
|
|
357
|
+
...(result.truncated !== undefined ? { truncated: result.truncated } : {}),
|
|
358
|
+
...(result.totalLines !== undefined ? { totalLines: result.totalLines } : {}),
|
|
359
|
+
...(result.returnedLines !== undefined ? { returnedLines: result.returnedLines } : {}),
|
|
360
|
+
...(result.totalBytes !== undefined ? { totalBytes: result.totalBytes } : {}),
|
|
361
|
+
...(result.returnedBytes !== undefined ? { returnedBytes: result.returnedBytes } : {}),
|
|
362
|
+
...(result.filesRead !== undefined ? { filesRead: result.filesRead } : {}),
|
|
363
|
+
...(result.filesSkipped !== undefined ? { filesSkipped: result.filesSkipped } : {}),
|
|
364
|
+
...(result.matches !== undefined ? { matches: result.matches } : {}),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function trackSideEffects(activeFiles, sideEffects) {
|
|
368
|
+
if (!sideEffects)
|
|
369
|
+
return;
|
|
370
|
+
for (const file of [...(sideEffects.touched ?? []), ...(sideEffects.created ?? [])]) {
|
|
371
|
+
const meta = describeActiveFile(file);
|
|
372
|
+
if (meta)
|
|
373
|
+
activeFiles.set(file, meta);
|
|
374
|
+
}
|
|
375
|
+
for (const file of sideEffects.removed ?? [])
|
|
376
|
+
activeFiles.delete(file);
|
|
377
|
+
}
|
|
378
|
+
function describeActiveFile(file) {
|
|
379
|
+
try {
|
|
380
|
+
const info = statSync(file);
|
|
381
|
+
if (!info.isFile())
|
|
382
|
+
return undefined;
|
|
383
|
+
const sha1_8 = info.size > 8 * 1024 * 1024
|
|
384
|
+
? "large"
|
|
385
|
+
: createHash("sha1").update(readFileSync(file)).digest("hex").slice(0, 8);
|
|
386
|
+
return { path: file, sha1_8, mtime: info.mtime.toISOString() };
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ShellAstHard {
|
|
2
|
+
code: string;
|
|
3
|
+
target: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
alternatives: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ShellAstAnalysis {
|
|
8
|
+
derived: string[];
|
|
9
|
+
hard?: ShellAstHard;
|
|
10
|
+
}
|
|
11
|
+
export declare function analyzeShellAst(command: string): ShellAstAnalysis;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Limited bash AST second opinion for Guardrails.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately not a replacement for the shape matcher. It only
|
|
5
|
+
* extracts the three entry classes we can safely re-feed into the existing
|
|
6
|
+
* classifier:
|
|
7
|
+
*
|
|
8
|
+
* 1. shell `-c` literal strings
|
|
9
|
+
* 2. `find ... -exec ... ;/+`
|
|
10
|
+
* 3. eval / source / `.` literal command strings
|
|
11
|
+
*
|
|
12
|
+
* plus one same-scope assignment (`X=rm; $X -rf /`). It only reports Hard
|
|
13
|
+
* Block upgrades; parse failures return no findings so the shape matcher's
|
|
14
|
+
* result is never downgraded.
|
|
15
|
+
*/
|
|
16
|
+
import parse from "bash-parser";
|
|
17
|
+
const MAX_DEPTH = 4;
|
|
18
|
+
export function analyzeShellAst(command) {
|
|
19
|
+
return analyze(command, 0);
|
|
20
|
+
}
|
|
21
|
+
function analyze(command, depth) {
|
|
22
|
+
if (depth > MAX_DEPTH)
|
|
23
|
+
return { derived: [] };
|
|
24
|
+
let ast;
|
|
25
|
+
try {
|
|
26
|
+
ast = parse(command, { mode: "bash" });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { derived: [] };
|
|
30
|
+
}
|
|
31
|
+
const derived = [];
|
|
32
|
+
let hard;
|
|
33
|
+
const scope = new Map();
|
|
34
|
+
const addDerived = (value) => {
|
|
35
|
+
const trimmed = value.trim();
|
|
36
|
+
if (trimmed && !derived.includes(trimmed))
|
|
37
|
+
derived.push(trimmed);
|
|
38
|
+
};
|
|
39
|
+
const addHard = (finding) => {
|
|
40
|
+
if (!hard)
|
|
41
|
+
hard = finding;
|
|
42
|
+
};
|
|
43
|
+
const wordText = (word) => typeof word?.text === "string" ? word.text : "";
|
|
44
|
+
const wordIsDynamic = (word) => {
|
|
45
|
+
if (Array.isArray(word?.expansion) && word.expansion.length > 0)
|
|
46
|
+
return true;
|
|
47
|
+
const text = wordText(word);
|
|
48
|
+
return /\$|`/.test(text);
|
|
49
|
+
};
|
|
50
|
+
const words = (value) => Array.isArray(value) ? value : [];
|
|
51
|
+
const walkWord = (word) => {
|
|
52
|
+
for (const expansion of words(word?.expansion)) {
|
|
53
|
+
if (expansion?.type === "CommandExpansion") {
|
|
54
|
+
if (typeof expansion.command === "string")
|
|
55
|
+
addDerived(expansion.command);
|
|
56
|
+
if (expansion.commandAST)
|
|
57
|
+
walkNode(expansion.commandAST, scope, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const walkList = (commands, assignments, level) => {
|
|
62
|
+
for (const node of commands)
|
|
63
|
+
walkNode(node, assignments, level);
|
|
64
|
+
};
|
|
65
|
+
const handleCommand = (node, assignments) => {
|
|
66
|
+
const prefix = words(node?.prefix);
|
|
67
|
+
for (const assignment of prefix) {
|
|
68
|
+
const text = wordText(assignment);
|
|
69
|
+
const eq = text.indexOf("=");
|
|
70
|
+
if (eq <= 0)
|
|
71
|
+
continue;
|
|
72
|
+
const name = text.slice(0, eq);
|
|
73
|
+
const value = text.slice(eq + 1);
|
|
74
|
+
// One same-scope assignment, literal values only. Nested command
|
|
75
|
+
// substitution / variable values are intentionally not tracked.
|
|
76
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && value && !/[$`]/.test(value))
|
|
77
|
+
assignments.set(name, value);
|
|
78
|
+
}
|
|
79
|
+
const nameWord = node?.name;
|
|
80
|
+
const name = wordText(nameWord);
|
|
81
|
+
const suffix = words(node?.suffix);
|
|
82
|
+
const parameter = words(nameWord?.expansion).find((entry) => entry?.type === "ParameterExpansion")?.parameter;
|
|
83
|
+
if (parameter && assignments.has(parameter) && (name === `$${parameter}` || name === `\${${parameter}}`)) {
|
|
84
|
+
const suffixText = suffix.map(wordText);
|
|
85
|
+
// bash-parser strips double quotes; a suffix that looks like a
|
|
86
|
+
// redirect/operator may be a quoted literal argument. Do not turn
|
|
87
|
+
// it into a false Hard Block.
|
|
88
|
+
if (suffixText.every((text) => !/[<>|;&]/.test(text))) {
|
|
89
|
+
const resolved = `${assignments.get(parameter)} ${suffixText.join(" ")}`.trim();
|
|
90
|
+
if (resolved)
|
|
91
|
+
addDerived(resolved);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (name === "eval" || name === "source" || name === ".") {
|
|
95
|
+
if (suffix.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
if (suffix.some(wordIsDynamic)) {
|
|
98
|
+
addHard({
|
|
99
|
+
code: "dynamic_execution",
|
|
100
|
+
target: name,
|
|
101
|
+
reason: "non-literal-eval-source",
|
|
102
|
+
alternatives: [
|
|
103
|
+
"Use a literal, reviewed command string; dynamic eval/source cannot be audited.",
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
addDerived(suffix.map(wordText).join(" "));
|
|
109
|
+
}
|
|
110
|
+
if (SHELL_COMMAND_NAMES.has(name)) {
|
|
111
|
+
const cIndex = suffix.findIndex((word) => /^-[a-z]*c$/i.test(wordText(word)));
|
|
112
|
+
if (cIndex >= 0 && suffix[cIndex + 1]) {
|
|
113
|
+
const script = suffix[cIndex + 1];
|
|
114
|
+
if (wordIsDynamic(script)) {
|
|
115
|
+
addHard({
|
|
116
|
+
code: "dynamic_execution",
|
|
117
|
+
target: `${name} -c`,
|
|
118
|
+
reason: "non-literal-shell-command",
|
|
119
|
+
alternatives: [
|
|
120
|
+
"Pass a literal, reviewed script string instead of a variable/command substitution.",
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
addDerived(wordText(script));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (name === "env") {
|
|
129
|
+
const start = suffix.findIndex((word) => {
|
|
130
|
+
const value = wordText(word);
|
|
131
|
+
return value && !value.startsWith("-") && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(value);
|
|
132
|
+
});
|
|
133
|
+
if (start >= 0)
|
|
134
|
+
addDerived(suffix.slice(start).map(wordText).join(" "));
|
|
135
|
+
}
|
|
136
|
+
if (name === "find") {
|
|
137
|
+
const execIndex = suffix.findIndex((word) => {
|
|
138
|
+
const value = wordText(word);
|
|
139
|
+
return value === "-exec" || value === "-execdir";
|
|
140
|
+
});
|
|
141
|
+
if (execIndex >= 0) {
|
|
142
|
+
const inner = [];
|
|
143
|
+
for (const word of suffix.slice(execIndex + 1)) {
|
|
144
|
+
const value = wordText(word);
|
|
145
|
+
if (value === ";" || value === "+")
|
|
146
|
+
break;
|
|
147
|
+
inner.push(value);
|
|
148
|
+
}
|
|
149
|
+
const executable = (inner[0] ?? "").replace(/\\/g, "/").split("/").pop()?.toLowerCase() ?? "";
|
|
150
|
+
if (inner.length > 0 && INSPECT_EXECUTABLES.has(executable))
|
|
151
|
+
addDerived(inner.join(" "));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const word of words(node?.prefix))
|
|
155
|
+
walkWord(word);
|
|
156
|
+
for (const word of words(node?.suffix))
|
|
157
|
+
walkWord(word);
|
|
158
|
+
};
|
|
159
|
+
const walkNode = (node, assignments, level) => {
|
|
160
|
+
if (!node || typeof node !== "object" || level > MAX_DEPTH)
|
|
161
|
+
return;
|
|
162
|
+
if (Array.isArray(node)) {
|
|
163
|
+
for (const item of node)
|
|
164
|
+
walkNode(item, assignments, level);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
switch (node.type) {
|
|
168
|
+
case "Command":
|
|
169
|
+
handleCommand(node, assignments);
|
|
170
|
+
return;
|
|
171
|
+
case "Script":
|
|
172
|
+
case "CompoundList":
|
|
173
|
+
walkList(words(node.commands), new Map(assignments), level);
|
|
174
|
+
return;
|
|
175
|
+
case "Pipeline":
|
|
176
|
+
walkList(words(node.commands), assignments, level);
|
|
177
|
+
return;
|
|
178
|
+
case "LogicalExpression":
|
|
179
|
+
walkNode(node.left, assignments, level);
|
|
180
|
+
walkNode(node.right, assignments, level);
|
|
181
|
+
return;
|
|
182
|
+
case "Subshell":
|
|
183
|
+
walkNode(node.list, new Map(assignments), level + 1);
|
|
184
|
+
return;
|
|
185
|
+
case "CommandExpansion":
|
|
186
|
+
walkNode(node.commandAST, assignments, level + 1);
|
|
187
|
+
return;
|
|
188
|
+
default:
|
|
189
|
+
for (const value of Object.values(node))
|
|
190
|
+
walkNode(value, assignments, level);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
walkNode(ast, scope, depth);
|
|
194
|
+
return hard ? { derived, hard } : { derived };
|
|
195
|
+
}
|
|
196
|
+
const SHELL_COMMAND_NAMES = new Set(["sh", "bash", "zsh", "dash", "ksh", "fish"]);
|
|
197
|
+
const INSPECT_EXECUTABLES = new Set([
|
|
198
|
+
"sh", "bash", "zsh", "dash", "ksh", "fish", "eval", "source", ".",
|
|
199
|
+
"rm", "rmdir", "rd", "del", "erase", "unlink", "shred", "truncate",
|
|
200
|
+
"git", "dd", "mkfs", "mke2fs", "fdisk", "diskpart", "format",
|
|
201
|
+
"docker", "kubectl", "chmod", "chown", "chgrp", "shutdown", "reboot",
|
|
202
|
+
"poweroff", "halt", "systemctl", "find", "curl", "wget", "base64",
|
|
203
|
+
"nc", "ncat", "netcat", "socat", "mv", "cp", "install", "tar", "rsync",
|
|
204
|
+
]);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type ShellJobStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
|
2
|
+
export interface ShellExecResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
status: ShellJobStatus;
|
|
5
|
+
output: string;
|
|
6
|
+
exitCode?: number | null;
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
nextPollMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ShellJobView {
|
|
11
|
+
id: string;
|
|
12
|
+
command: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
status: ShellJobStatus;
|
|
15
|
+
exitCode?: number | null;
|
|
16
|
+
bytes: number;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
pid?: number;
|
|
19
|
+
/** Lifecycle visibility for /jobs and regression tests. */
|
|
20
|
+
timeoutArmed?: boolean;
|
|
21
|
+
foregroundAttached?: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface ExecOptions {
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
turn?: number;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
backgroundAfterMs?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const DEFAULT_SHELL_POOL_SIZE = 2;
|
|
30
|
+
export declare const MAX_SHELL_POOL_SIZE = 6;
|
|
31
|
+
export declare function executeShellCommand(command: string, cwd: string, options?: ExecOptions): Promise<ShellExecResult>;
|
|
32
|
+
export declare function pollShellSession(id: string, waitMs?: number): Promise<ShellExecResult | undefined>;
|
|
33
|
+
export declare function killShellSession(id: string): boolean;
|
|
34
|
+
export declare function listShellSessions(): ShellJobView[];
|
|
35
|
+
export declare function shutdownShells(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Exit-safe stop: send SIGTERM, wait briefly for close, then force SIGKILL.
|
|
38
|
+
* Use this when the process is about to exit and must not leave children.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shutdownShellsAsync(graceMs?: number): Promise<void>;
|
|
41
|
+
export declare function shellPoolSize(): number;
|
|
42
|
+
/** Clamp and apply the resident shell concurrency (1..6, default 2). */
|
|
43
|
+
export declare function setShellPoolSize(size: number): number;
|
|
44
|
+
export declare function shellPoolStats(): {
|
|
45
|
+
spawns: number;
|
|
46
|
+
active: number;
|
|
47
|
+
pool: number;
|
|
48
|
+
maxPool: number;
|
|
49
|
+
maxSpawnPerTurn: number;
|
|
50
|
+
};
|
|
51
|
+
export {};
|