@code-yeongyu/senpi 2026.10.5 → 2026.10.7
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/CHANGELOG.md +46 -0
- package/dist/bundle/chunks/{anthropic-messages-G5236QNK.js → anthropic-messages-4MNFPQ66.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-3Q5G4CAF.js → app-server-command-SO5RJ2N7.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-NTA2HGG4.js → azure-openai-responses-A5OEHSJM.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-ECDLITEY.js → chunk-6EH7QXOG.js} +2 -2
- package/dist/bundle/chunks/{chunk-JVSOW4QU.js → chunk-7A3WRAKP.js} +1 -1
- package/dist/bundle/chunks/chunk-7SMFF2QJ.js +14 -0
- package/dist/bundle/chunks/{chunk-WTJU5B2C.js → chunk-BE66QJ3Z.js} +1 -1
- package/dist/bundle/chunks/{chunk-6JXPZACL.js → chunk-CM5QTRC2.js} +1 -1
- package/dist/bundle/chunks/{chunk-UHRXXTFZ.js → chunk-D2W3MXRF.js} +2 -2
- package/dist/bundle/chunks/{chunk-U7CI2JGX.js → chunk-DDEAZX22.js} +1 -1
- package/dist/bundle/chunks/{chunk-UXIQK5NQ.js → chunk-ESSQUSRC.js} +1 -1
- package/dist/bundle/chunks/{chunk-EEOKVSNF.js → chunk-EUU6W5B3.js} +180 -176
- package/dist/bundle/chunks/{chunk-KLBKUBDV.js → chunk-GQ3YLXVZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-HKG7XL4H.js → chunk-HY4AOLON.js} +1 -1
- package/dist/bundle/chunks/{chunk-2WV426OJ.js → chunk-JJNIU2A2.js} +8 -4
- package/dist/bundle/chunks/{chunk-22DAHAHO.js → chunk-LUWWM7GA.js} +2 -2
- package/dist/bundle/chunks/{chunk-CTQU2XNX.js → chunk-MLNJNJFE.js} +1 -1
- package/dist/bundle/chunks/{chunk-BPX6QWPD.js → chunk-PELNNWGO.js} +1 -1
- package/dist/bundle/chunks/{chunk-BBK6SSYT.js → chunk-TAV6QZFM.js} +1 -1
- package/dist/bundle/chunks/{chunk-QUROHJLP.js → chunk-TFO5JGLR.js} +1 -1
- package/dist/bundle/chunks/chunk-VJJ53ORU.js +2 -0
- package/dist/bundle/chunks/{chunk-XYDZ3LIE.js → chunk-YI7SPFDN.js} +2 -2
- package/dist/bundle/chunks/chunk-ZJHRDQCH.js +2 -0
- package/dist/bundle/chunks/cli-main-XBQN73O2.js +3 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-5ZUHUKMU.js → cloudflare-workers-ai-system-one-TE3OEJEL.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-OTMG6ZXI.js → google-generative-ai-JBC53AMK.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-2LXS3ICC.js → google-vertex-46ODNPM2.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-SHZJ3QWQ.js → help-fast-path-FUMH5NSA.js} +1 -1
- package/dist/bundle/chunks/host-command-YXQAXNKK.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-7OVWGVIZ.js → host-lifecycle-L4XQIATP.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/interactive-mode-ROO5W56P.js +2 -0
- package/dist/bundle/chunks/{llama-cpp-classify-U5XAMKZE.js → llama-cpp-classify-AD6TTKNL.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-34WOINP4.js → multi-session-host-5WTKAQJM.js} +5 -5
- package/dist/bundle/chunks/{openai-codex-responses-WNTYIPFZ.js → openai-codex-responses-Q4OWIK7Y.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-VSH22FCN.js → openai-completions-6HU7GIGD.js} +1 -1
- package/dist/bundle/chunks/{openai-images-XBTUJXPR.js → openai-images-M73YANTR.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-WLBHCFYL.js → openai-responses-S4QYZXF3.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-3LVJZY3G.js → openrouter-images-PCJFX2KO.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-2QLVGQSC.js → package-manager-cli-NCD5FQGV.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-JUCEXHWU.js → rotation-stream-7E37KOCX.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-MWURKCP2.js +2 -0
- package/dist/bundle/chunks/{schedule-command-QTYSUYO2.js → schedule-command-UTXB465F.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-J6RHZXWB.js +3 -0
- package/dist/bundle/chunks/{session-picker-HX4LOJZ7.js → session-picker-VTTRFGE7.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +376 -368
- package/dist/bundle/chunks/{typesafe-system-one-FNI3UASE.js → typesafe-system-one-TF23RUVG.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +12 -0
- package/dist/core/agent-session.d.ts +18 -0
- package/dist/core/agent-session.js +55 -19
- package/dist/core/dynamic-prompt/handoff.d.ts +4 -2
- package/dist/core/dynamic-prompt/handoff.js +1 -1
- package/dist/core/extensions/builtin/diff.js +14 -9
- package/dist/core/extensions/builtin/files.js +6 -2
- package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +5 -1
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.d.ts +8 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +26 -2
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-render.js +82 -11
- package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +9 -1
- package/dist/core/extensions/builtin/permission-system/auto-credentials.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/auto-credentials.js +17 -0
- package/dist/core/extensions/builtin/permission-system/auto-paths.d.ts +14 -0
- package/dist/core/extensions/builtin/permission-system/auto-paths.js +93 -0
- package/dist/core/extensions/builtin/permission-system/auto-policy.d.ts +20 -0
- package/dist/core/extensions/builtin/permission-system/auto-policy.js +126 -0
- package/dist/core/extensions/builtin/permission-system/auto-program-rules.d.ts +6 -0
- package/dist/core/extensions/builtin/permission-system/auto-program-rules.js +139 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.d.ts +29 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-grammar.js +74 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-segments.d.ts +13 -0
- package/dist/core/extensions/builtin/permission-system/auto-shell-segments.js +23 -0
- package/dist/core/extensions/builtin/permission-system/cli.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/cli.js +9 -10
- package/dist/core/extensions/builtin/permission-system/config.d.ts +4 -1
- package/dist/core/extensions/builtin/permission-system/config.js +13 -1
- package/dist/core/extensions/builtin/permission-system/index.js +20 -12
- package/dist/core/extensions/builtin/permission-system/non-interactive.d.ts +11 -6
- package/dist/core/extensions/builtin/permission-system/non-interactive.js +8 -33
- package/dist/core/extensions/builtin/permission-system/service.d.ts +20 -1
- package/dist/core/extensions/builtin/permission-system/service.js +43 -7
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +1 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +4 -3
- package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +16 -3
- package/dist/core/extensions/eval-handle-host.d.ts +82 -0
- package/dist/core/extensions/eval-handle-host.js +28 -0
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/runner.js +4 -0
- package/dist/core/extensions/types.d.ts +15 -0
- package/dist/core/retry-fallback/usage-limit.js +5 -1
- package/dist/core/tools/diff-render.d.ts +4 -0
- package/dist/core/tools/diff-render.js +13 -0
- package/dist/core/tools/renderers/edit.js +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/main.js +12 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +55 -11
- package/dist/modes/interactive/provider-error-presentation.d.ts +25 -0
- package/dist/modes/interactive/provider-error-presentation.js +42 -0
- package/dist/modes/interactive/session-control-registry.js +2 -0
- package/dist/modes/interactive/theme/dark.json +5 -5
- package/dist/modes/interactive/theme/light.json +5 -5
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/host-daemon-registration.d.ts +9 -0
- package/dist/modes/rpc/host-daemon-registration.js +2 -4
- package/dist/modes/rpc/host-ensure-start.js +2 -0
- package/dist/modes/rpc/host-handoff-unregistered.d.ts +24 -0
- package/dist/modes/rpc/host-handoff-unregistered.js +50 -0
- package/dist/modes/rpc/host-handoff.d.ts +7 -2
- package/dist/modes/rpc/host-handoff.js +7 -3
- package/dist/modes/rpc/host-legacy.d.ts +3 -0
- package/dist/modes/rpc/host-legacy.js +10 -5
- package/dist/modes/rpc/host-probe.d.ts +12 -0
- package/dist/modes/rpc/host-probe.js +53 -5
- package/dist/modes/rpc/host-runner.js +1 -1
- package/dist/modes/rpc/host-successor.d.ts +5 -4
- package/dist/modes/rpc/host-successor.js +11 -4
- package/dist/modes/rpc/rpc-client.d.ts +3 -0
- package/dist/modes/rpc/rpc-input-validation.d.ts +7 -0
- package/dist/modes/rpc/rpc-input-validation.js +37 -0
- package/dist/modes/rpc/rpc-types.d.ts +9 -0
- package/dist/modes/rpc/session-command-activity.d.ts +2 -0
- package/dist/modes/rpc/session-command-activity.js +25 -0
- package/dist/modes/rpc/session-command-router.js +11 -4
- package/dist/modes/rpc/session-registry-attach.js +1 -0
- package/dist/modes/rpc/session-registry-types.d.ts +7 -0
- package/dist/modes/rpc/session-registry-types.js +11 -0
- package/dist/modes/rpc/session-registry.js +6 -3
- package/dist/modes/rpc/session-sweep.d.ts +12 -2
- package/dist/modes/rpc/session-sweep.js +36 -3
- package/dist/modes/rpc/session-teardown.d.ts +2 -0
- package/dist/modes/rpc/session-teardown.js +1 -0
- package/dist/modes/rpc/worker-session-registry.js +5 -1
- package/docs/rpc.md +49 -7
- package/docs/settings.md +3 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-K3BXFPWZ.js +0 -2
- package/dist/bundle/chunks/chunk-UUAKEDVS.js +0 -14
- package/dist/bundle/chunks/cli-main-VZWSUHFB.js +0 -3
- package/dist/bundle/chunks/host-command-7JIGGOWR.js +0 -2
- package/dist/bundle/chunks/interactive-mode-JA7AHYYL.js +0 -2
- package/dist/bundle/chunks/rpc-mode-QEBSN2OH.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-UGQIP6ZN.js +0 -3
|
@@ -3,6 +3,9 @@ import { normalizeApplyPatchArguments } from "./params.js";
|
|
|
3
3
|
import { renderPatchLine } from "./preview-format.js";
|
|
4
4
|
import { StreamingPatchParser } from "./streaming-parser.js";
|
|
5
5
|
import { extractPatchedPaths } from "./text.js";
|
|
6
|
+
// Tail-window the streaming body so a long patch never takes over the viewport: a sticky
|
|
7
|
+
// per-file header plus the last few lines of each file, with a "+N lines above" indicator.
|
|
8
|
+
const STREAMING_TAIL_LINES_PER_FILE = 12;
|
|
6
9
|
function hunkOperation(hunk) {
|
|
7
10
|
if (hunk.type === "add")
|
|
8
11
|
return "Added";
|
|
@@ -27,8 +30,44 @@ function hunkDiffLines(hunk) {
|
|
|
27
30
|
...chunk.newLines.map((line) => ` + ${line}`),
|
|
28
31
|
]);
|
|
29
32
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
33
|
+
function hunkChangeCounts(hunk) {
|
|
34
|
+
if (hunk.type === "add")
|
|
35
|
+
return { added: hunk.content.split("\n").filter(Boolean).length, removed: 0 };
|
|
36
|
+
if (hunk.type === "delete")
|
|
37
|
+
return { added: 0, removed: 0 };
|
|
38
|
+
// addedCount/removedCount are tracked by prefix at parse time, so context lines are excluded
|
|
39
|
+
// and a + line whose text equals a context line still counts as an addition (no set-membership
|
|
40
|
+
// or positional undercount).
|
|
41
|
+
let added = 0;
|
|
42
|
+
let removed = 0;
|
|
43
|
+
for (const chunk of hunk.chunks) {
|
|
44
|
+
added += chunk.addedCount;
|
|
45
|
+
removed += chunk.removedCount;
|
|
46
|
+
}
|
|
47
|
+
return { added, removed };
|
|
48
|
+
}
|
|
49
|
+
// One bounded block per file: sticky header with net counts, then the last N diff lines
|
|
50
|
+
// with a "+N lines above" marker when the file outgrew the window.
|
|
51
|
+
function formatStreamingHunks(hunks, partialLine) {
|
|
52
|
+
const blocks = [];
|
|
53
|
+
for (const hunk of hunks) {
|
|
54
|
+
const { added, removed } = hunkChangeCounts(hunk);
|
|
55
|
+
const counts = added + removed > 0 ? ` (+${added} -${removed})` : "";
|
|
56
|
+
const header = `• ${hunkOperation(hunk)} ${hunkPath(hunk)}${counts}`;
|
|
57
|
+
const lines = hunkDiffLines(hunk);
|
|
58
|
+
if (lines.length <= STREAMING_TAIL_LINES_PER_FILE) {
|
|
59
|
+
blocks.push([header, ...lines].join("\n"));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const hidden = lines.length - STREAMING_TAIL_LINES_PER_FILE;
|
|
63
|
+
const tail = lines.slice(-STREAMING_TAIL_LINES_PER_FILE);
|
|
64
|
+
blocks.push([header, ` … (+${hidden} lines above)`, ...tail].join("\n"));
|
|
65
|
+
}
|
|
66
|
+
// The in-flight, not-yet-newline-terminated line renders dimmed as the last row.
|
|
67
|
+
const partial = partialLine.trim();
|
|
68
|
+
if (partial.length > 0)
|
|
69
|
+
blocks.push(` ${partial}`);
|
|
70
|
+
return blocks.join("\n");
|
|
32
71
|
}
|
|
33
72
|
function updateStreamingState(input, state) {
|
|
34
73
|
if (!state.streamingParser || !input.startsWith(state.streamingInput ?? "")) {
|
|
@@ -39,8 +78,14 @@ function updateStreamingState(input, state) {
|
|
|
39
78
|
}
|
|
40
79
|
const previousInput = state.streamingInput ?? "";
|
|
41
80
|
const delta = input.slice(previousInput.length);
|
|
81
|
+
// A zero-length delta (same render pass re-run with no new text) must not re-parse or re-render.
|
|
82
|
+
if (delta.length === 0)
|
|
83
|
+
return state.streamingHunks ?? [];
|
|
42
84
|
try {
|
|
43
|
-
|
|
85
|
+
// Parse the delta, then render the parser's live hunk list rather than the deep clone
|
|
86
|
+
// pushDelta returns; the per-delta structuredClone over every hunk is the O(n^2) cost.
|
|
87
|
+
state.streamingParser.pushDelta(delta);
|
|
88
|
+
state.streamingHunks = state.streamingParser.getLiveHunks?.() ?? state.streamingHunks;
|
|
44
89
|
state.streamingInput = input;
|
|
45
90
|
state.streamingError = undefined;
|
|
46
91
|
}
|
|
@@ -49,30 +94,56 @@ function updateStreamingState(input, state) {
|
|
|
49
94
|
}
|
|
50
95
|
return state.streamingHunks ?? [];
|
|
51
96
|
}
|
|
52
|
-
function renderBox(title, body, theme) {
|
|
97
|
+
function renderBox(title, body, theme, dimLastLine) {
|
|
53
98
|
const component = new Container();
|
|
54
99
|
const box = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
|
|
55
100
|
box.addChild(new Text(theme.fg("toolTitle", theme.bold(title)), 0, 0));
|
|
56
101
|
box.addChild(new Spacer(1));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.map((line) =>
|
|
102
|
+
const lines = body.split("\n");
|
|
103
|
+
box.addChild(new Text(lines
|
|
104
|
+
.map((line, index) => dimLastLine && index === lines.length - 1
|
|
105
|
+
? theme.fg("toolDiffContext", line)
|
|
106
|
+
: renderPatchLine(line, theme))
|
|
60
107
|
.join("\n"), 0, 0));
|
|
61
108
|
component.addChild(box);
|
|
62
109
|
return component;
|
|
63
110
|
}
|
|
111
|
+
// A no-change delta keeps the already-rendered body; the component renders it on demand so the
|
|
112
|
+
// box persists across a redraw without a rebuild.
|
|
113
|
+
class MemoizedStreamingBox extends Container {
|
|
114
|
+
constructor(body, hasPartial, theme) {
|
|
115
|
+
super();
|
|
116
|
+
this.body = body;
|
|
117
|
+
this.hasPartial = hasPartial;
|
|
118
|
+
this.theme = theme;
|
|
119
|
+
}
|
|
120
|
+
render(width) {
|
|
121
|
+
this.built ??= renderBox("Applying patch", this.body, this.theme, this.hasPartial);
|
|
122
|
+
return this.built.render(width);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
64
125
|
export function renderStreamingPatchCall(args, theme, state) {
|
|
65
126
|
const input = normalizeApplyPatchArguments(args).input;
|
|
66
127
|
if (!input)
|
|
67
128
|
return undefined;
|
|
68
129
|
const hunks = updateStreamingState(input, state);
|
|
69
130
|
if (state.streamingError)
|
|
70
|
-
return renderBox("Invalid patch stream", state.streamingError, theme);
|
|
71
|
-
if (hunks.length > 0)
|
|
72
|
-
|
|
131
|
+
return renderBox("Invalid patch stream", state.streamingError, theme, false);
|
|
132
|
+
if (hunks.length > 0) {
|
|
133
|
+
const partialLine = state.streamingParser?.getPartialLine?.() ?? "";
|
|
134
|
+
const body = formatStreamingHunks(hunks, partialLine);
|
|
135
|
+
const hasPartial = partialLine.trim().length > 0;
|
|
136
|
+
// A no-change delta keeps the already-rendered box instead of rebuilding it; the box must
|
|
137
|
+
// still render (returning undefined here would blank the preview on any redraw).
|
|
138
|
+
if (body === state.streamingLastRenderKey && state.streamingLastRenderKey !== undefined) {
|
|
139
|
+
return new MemoizedStreamingBox(body, hasPartial, theme);
|
|
140
|
+
}
|
|
141
|
+
state.streamingLastRenderKey = body;
|
|
142
|
+
return renderBox("Applying patch", body, theme, hasPartial);
|
|
143
|
+
}
|
|
73
144
|
const paths = extractPatchedPaths(input);
|
|
74
145
|
if (paths.length === 0)
|
|
75
146
|
return undefined;
|
|
76
|
-
return renderBox("Applying patch", paths.map((filePath) => `• ${filePath}`).join("\n"), theme);
|
|
147
|
+
return renderBox("Applying patch", paths.map((filePath) => `• ${filePath}`).join("\n"), theme, false);
|
|
77
148
|
}
|
|
78
149
|
//# sourceMappingURL=streaming-render.js.map
|
|
@@ -20,6 +20,10 @@ export type PatchChunk = {
|
|
|
20
20
|
newLines: string[];
|
|
21
21
|
contextLineIndices: ContextLineIndex[];
|
|
22
22
|
isEndOfFile: boolean;
|
|
23
|
+
/** Real added/removed line counts by prefix, tracked at parse time (context lines excluded,
|
|
24
|
+
* so a `+` line whose text equals a context line still counts as an addition). */
|
|
25
|
+
addedCount: number;
|
|
26
|
+
removedCount: number;
|
|
23
27
|
};
|
|
24
28
|
export type ApplyPatchWireMode = "freeform" | "json" | "none";
|
|
25
29
|
export type ApplyPatchToolVariant = Exclude<ApplyPatchWireMode, "none">;
|
|
@@ -111,9 +115,13 @@ export type ApplyPatchRenderState = {
|
|
|
111
115
|
streamingInput?: string;
|
|
112
116
|
streamingParser?: {
|
|
113
117
|
pushDelta: (delta: string) => ParsedPatch[];
|
|
118
|
+
getLiveHunks?: () => readonly ParsedPatch[];
|
|
119
|
+
getPartialLine?: () => string;
|
|
114
120
|
};
|
|
115
|
-
streamingHunks?: ParsedPatch[];
|
|
121
|
+
streamingHunks?: readonly ParsedPatch[];
|
|
116
122
|
streamingError?: string;
|
|
123
|
+
/** Last rendered body fingerprint, so a delta that changes nothing skips the rebuild. */
|
|
124
|
+
streamingLastRenderKey?: string;
|
|
117
125
|
};
|
|
118
126
|
export type ApplyPatchToolDefinition = ToolDefinition<typeof APPLY_PATCH_PARAMS, ApplyPatchToolDetails | undefined, ApplyPatchRenderState>;
|
|
119
127
|
export type ApplyPatchExtensionAPI = Pick<ExtensionAPI, "on" | "getActiveTools" | "getAllTools" | "setActiveTools"> & {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const CREDENTIAL_PATH_PATTERNS = [
|
|
2
|
+
/(^|[/\\=])\.(ssh|aws|gnupg|kube|docker|azure|gcloud)([/\\]|$)/i,
|
|
3
|
+
/(^|[/\\=])\.config[/\\](gh|gcloud|hub|op)([/\\]|$)/i,
|
|
4
|
+
/(^|[/\\=])\.(netrc|npmrc|pypirc|pgpass|git-credentials|htpasswd|vault-token|envrc|terraformrc)$/i,
|
|
5
|
+
/(^|[/\\=])\.env(\.[^/\\]*)?$/i,
|
|
6
|
+
/(^|[/\\=])id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i,
|
|
7
|
+
/\.(pem|key|p12|pfx|jks|keystore|kdbx|ppk|asc|gpg)$/i,
|
|
8
|
+
/(^|[/\\=])\.?credentials(\.json|\.toml|\.tfrc\.json)?$/i,
|
|
9
|
+
/(^|[/\\=])auth\.json$/i,
|
|
10
|
+
/(^|[/\\=])(\.[a-z]+_history|fish_history)$/i,
|
|
11
|
+
/(^|[/\\=])\.m2[/\\]settings\.xml$/i,
|
|
12
|
+
/(^|[/\\])Library[/\\]Keychains([/\\]|$)/i,
|
|
13
|
+
];
|
|
14
|
+
export function isCredentialPath(value) {
|
|
15
|
+
return CREDENTIAL_PATH_PATTERNS.some((pattern) => pattern.test(value));
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=auto-credentials.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type TargetKind = "file" | "directory" | "missing" | "other";
|
|
2
|
+
/** Whether something, even a dangling symlink, exists at `target` itself; git looks at the name. */
|
|
3
|
+
export declare function existsAtName(target: string): boolean;
|
|
4
|
+
/** Whether `cwd` is a root `auto` approves anything in (see `projectRoot`). */
|
|
5
|
+
export declare function isProjectSession(cwd: string): boolean;
|
|
6
|
+
/** What is at `target` after following every symlink, without opening anything. */
|
|
7
|
+
export declare function targetKind(target: string): TargetKind;
|
|
8
|
+
/**
|
|
9
|
+
* Whether `auto` may touch the absolute path `target` that a tool resolved: after following every
|
|
10
|
+
* symlink it is inside the project root, no component below the root is hidden (other than a
|
|
11
|
+
* short list of repository and formatter files), and nothing along it is credential-shaped.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isApprovableTarget(target: string, cwd: string): boolean;
|
|
14
|
+
//# sourceMappingURL=auto-paths.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { realpathWithoutOpenStrict } from "../../../../utils/paths.js";
|
|
5
|
+
import { isCredentialPath } from "./auto-credentials.js";
|
|
6
|
+
/** Hidden project entries `auto` may touch: repository metadata and formatter/linter configs. */
|
|
7
|
+
const SAFE_HIDDEN_NAMES = new Set([
|
|
8
|
+
".github",
|
|
9
|
+
".gitignore",
|
|
10
|
+
".gitattributes",
|
|
11
|
+
".editorconfig",
|
|
12
|
+
".nvmrc",
|
|
13
|
+
".node-version",
|
|
14
|
+
".prettierrc",
|
|
15
|
+
".prettierignore",
|
|
16
|
+
".eslintrc",
|
|
17
|
+
".eslintignore",
|
|
18
|
+
".stylelintrc",
|
|
19
|
+
".markdownlint.json",
|
|
20
|
+
]);
|
|
21
|
+
const physicalPath = (target) => {
|
|
22
|
+
try {
|
|
23
|
+
const resolved = realpathWithoutOpenStrict(target);
|
|
24
|
+
return resolved.split(path.sep).includes("..") ? undefined : resolved;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The project root `auto` works in, or undefined when the session root is `/`, the home directory, any
|
|
32
|
+
* ancestor of it (`/Users`, `/home`), or inside a hidden directory (`~/.config`): from there login
|
|
33
|
+
* items, app stores and tool configs would count as project files.
|
|
34
|
+
*/
|
|
35
|
+
function projectRoot(cwd) {
|
|
36
|
+
const root = physicalPath(path.resolve(cwd));
|
|
37
|
+
const home = physicalPath(os.homedir());
|
|
38
|
+
if (root === undefined || home === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
const homeFromRoot = path.relative(root, home);
|
|
41
|
+
const rootContainsHome = homeFromRoot === "" || (!homeFromRoot.startsWith("..") && !path.isAbsolute(homeFromRoot));
|
|
42
|
+
const hiddenRoot = root.split(path.sep).some((segment) => segment.startsWith("."));
|
|
43
|
+
return root === path.parse(root).root || rootContainsHome || hiddenRoot ? undefined : root;
|
|
44
|
+
}
|
|
45
|
+
/** Whether something, even a dangling symlink, exists at `target` itself; git looks at the name. */
|
|
46
|
+
export function existsAtName(target) {
|
|
47
|
+
try {
|
|
48
|
+
return lstatSync(target, { throwIfNoEntry: false }) !== undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// A name the filesystem refuses (too long, a loop, no permission) is not a plain ref either:
|
|
52
|
+
// report it as existing so the caller checks it as a path, and that check asks.
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Whether `cwd` is a root `auto` approves anything in (see `projectRoot`). */
|
|
57
|
+
export function isProjectSession(cwd) {
|
|
58
|
+
return projectRoot(cwd) !== undefined;
|
|
59
|
+
}
|
|
60
|
+
/** What is at `target` after following every symlink, without opening anything. */
|
|
61
|
+
export function targetKind(target) {
|
|
62
|
+
const physical = physicalPath(target);
|
|
63
|
+
if (physical === undefined)
|
|
64
|
+
return "other";
|
|
65
|
+
try {
|
|
66
|
+
const stats = lstatSync(physical);
|
|
67
|
+
return stats.isFile() ? "file" : stats.isDirectory() ? "directory" : "other";
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return error.code === "ENOENT" ? "missing" : "other";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether `auto` may touch the absolute path `target` that a tool resolved: after following every
|
|
75
|
+
* symlink it is inside the project root, no component below the root is hidden (other than a
|
|
76
|
+
* short list of repository and formatter files), and nothing along it is credential-shaped.
|
|
77
|
+
*/
|
|
78
|
+
export function isApprovableTarget(target, cwd) {
|
|
79
|
+
const root = projectRoot(cwd);
|
|
80
|
+
const physical = physicalPath(target);
|
|
81
|
+
if (root === undefined || physical === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
const below = path.relative(root, physical);
|
|
84
|
+
if (below === "")
|
|
85
|
+
return true;
|
|
86
|
+
if (below.startsWith("..") || path.isAbsolute(below))
|
|
87
|
+
return false;
|
|
88
|
+
if (below.split(path.sep).some((segment) => segment.startsWith(".") && !SAFE_HIDDEN_NAMES.has(segment))) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return !isCredentialPath(physical) && !isCredentialPath(target);
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=auto-paths.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PermissionRequest } from "./parsers.ts";
|
|
2
|
+
export type AutoCommandVerdict = "allow" | "ask";
|
|
3
|
+
export interface AutoDecision {
|
|
4
|
+
/** Approve an ask that comes only from the preset's own rule (a user's rules still win). */
|
|
5
|
+
readonly approveBlanketAsk: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Judges a shell command for the `auto` preset: every simple command must be a listed read-only
|
|
9
|
+
* program run in the project root, whose every word the program grammar classifies, and every
|
|
10
|
+
* path word must name an approvable project file or directory. Anything else asks.
|
|
11
|
+
*/
|
|
12
|
+
export declare function judgeAutoCommand(command: string, cwd: string): AutoCommandVerdict;
|
|
13
|
+
/**
|
|
14
|
+
* The `auto` preset's allowlist (decision table on senpi#2614). Each decision is made on the exact
|
|
15
|
+
* target the tool will open, obtained from the tool's own resolver (`resolveReadPathAsync` for `read`,
|
|
16
|
+
* `resolveToCwd` for `write`, `edit`, `ls`, `find` and `grep`, `parsePatch` + `resolvePatchPath` for `apply_patch`).
|
|
17
|
+
* Every other call keeps the preset's ask.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decideAuto(toolName: string, input: Record<string, unknown>, request: PermissionRequest, cwd: string): Promise<AutoDecision>;
|
|
20
|
+
//# sourceMappingURL=auto-policy.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { resolveReadPathAsync, resolveToCwd } from "../../../tools/path-utils.js";
|
|
3
|
+
import { normalizeApplyPatchArguments } from "../gpt-apply-patch/params.js";
|
|
4
|
+
import { parsePatch } from "../gpt-apply-patch/parser.js";
|
|
5
|
+
import { resolvePatchPath } from "../gpt-apply-patch/workspace.js";
|
|
6
|
+
import { isCredentialPath } from "./auto-credentials.js";
|
|
7
|
+
import { existsAtName, isApprovableTarget, isProjectSession, targetKind } from "./auto-paths.js";
|
|
8
|
+
import { PROGRAM_RULES } from "./auto-program-rules.js";
|
|
9
|
+
import { splitShellSegments } from "./auto-shell-segments.js";
|
|
10
|
+
const WRITE_TOOLS = new Set(["write", "edit"]);
|
|
11
|
+
const LIST_TOOLS = new Set(["ls", "find"]);
|
|
12
|
+
const NO = { approveBlanketAsk: false };
|
|
13
|
+
const YES = { approveBlanketAsk: true };
|
|
14
|
+
const approvedTarget = (target, cwd, kinds) => isApprovableTarget(target, cwd) && kinds.includes(targetKind(target));
|
|
15
|
+
function shellWordAllowed(entry, cwd) {
|
|
16
|
+
if (entry.role === "text")
|
|
17
|
+
return true;
|
|
18
|
+
const text = entry.word.text;
|
|
19
|
+
if (text === "" || text === "-" || text.split("/").includes(".."))
|
|
20
|
+
return false;
|
|
21
|
+
const target = path.resolve(cwd, text);
|
|
22
|
+
if (entry.role === "read-file")
|
|
23
|
+
return approvedTarget(target, cwd, ["file"]);
|
|
24
|
+
if (entry.role === "list")
|
|
25
|
+
return approvedTarget(target, cwd, ["file", "directory"]);
|
|
26
|
+
if (entry.role === "ref-or-path") {
|
|
27
|
+
if (isCredentialPath(target))
|
|
28
|
+
return false;
|
|
29
|
+
return !existsAtName(target) || approvedTarget(target, cwd, ["file", "directory"]);
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Judges a shell command for the `auto` preset: every simple command must be a listed read-only
|
|
35
|
+
* program run in the project root, whose every word the program grammar classifies, and every
|
|
36
|
+
* path word must name an approvable project file or directory. Anything else asks.
|
|
37
|
+
*/
|
|
38
|
+
export function judgeAutoCommand(command, cwd) {
|
|
39
|
+
const segments = splitShellSegments(command);
|
|
40
|
+
if (!segments || segments.length === 0)
|
|
41
|
+
return "ask";
|
|
42
|
+
for (const segment of segments) {
|
|
43
|
+
const [program, ...args] = segment;
|
|
44
|
+
if (!program || program.text.includes("/") || program.text.includes("="))
|
|
45
|
+
return "ask";
|
|
46
|
+
const classified = PROGRAM_RULES.get(program.text)?.(args);
|
|
47
|
+
if (classified === undefined || !classified.every((entry) => shellWordAllowed(entry, cwd)))
|
|
48
|
+
return "ask";
|
|
49
|
+
}
|
|
50
|
+
return "allow";
|
|
51
|
+
}
|
|
52
|
+
const stringPaths = (value) => {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return ["."];
|
|
55
|
+
if (typeof value === "string")
|
|
56
|
+
return [value];
|
|
57
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string"))
|
|
58
|
+
return value;
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
/** The paths an `apply_patch` call will write, resolved by the patch tool's own parser and resolver. */
|
|
62
|
+
function patchTargets(input, cwd) {
|
|
63
|
+
const { input: patchText } = normalizeApplyPatchArguments(input);
|
|
64
|
+
if (!patchText)
|
|
65
|
+
return undefined;
|
|
66
|
+
try {
|
|
67
|
+
const hunks = parsePatch(patchText);
|
|
68
|
+
// A delete asks, the same as `rm` in the shell.
|
|
69
|
+
if (hunks.length === 0 || hunks.some((hunk) => hunk.type === "delete"))
|
|
70
|
+
return undefined;
|
|
71
|
+
return hunks.flatMap((hunk) => {
|
|
72
|
+
const targets = [resolvePatchPath(cwd, hunk.filePath)];
|
|
73
|
+
if (hunk.type === "update" && hunk.movePath)
|
|
74
|
+
targets.push(resolvePatchPath(cwd, hunk.movePath));
|
|
75
|
+
return targets;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The `auto` preset's allowlist (decision table on senpi#2614). Each decision is made on the exact
|
|
84
|
+
* target the tool will open, obtained from the tool's own resolver (`resolveReadPathAsync` for `read`,
|
|
85
|
+
* `resolveToCwd` for `write`, `edit`, `ls`, `find` and `grep`, `parsePatch` + `resolvePatchPath` for `apply_patch`).
|
|
86
|
+
* Every other call keeps the preset's ask.
|
|
87
|
+
*/
|
|
88
|
+
export async function decideAuto(toolName, input, request, cwd) {
|
|
89
|
+
if (!isProjectSession(cwd))
|
|
90
|
+
return NO;
|
|
91
|
+
if (toolName === "bash" || (toolName === "monitor" && typeof input.command === "string")) {
|
|
92
|
+
if (request.permission !== "bash" || typeof input.command !== "string")
|
|
93
|
+
return NO;
|
|
94
|
+
return judgeAutoCommand(input.command, cwd) === "allow" ? YES : NO;
|
|
95
|
+
}
|
|
96
|
+
if (toolName === "read") {
|
|
97
|
+
const raw = input.path ?? input.file_path;
|
|
98
|
+
if (typeof raw !== "string")
|
|
99
|
+
return NO;
|
|
100
|
+
return approvedTarget(await resolveReadPathAsync(raw, cwd), cwd, ["file", "missing"]) ? YES : NO;
|
|
101
|
+
}
|
|
102
|
+
if (toolName === "grep") {
|
|
103
|
+
const raws = stringPaths(input.path);
|
|
104
|
+
if (raws?.length === 0)
|
|
105
|
+
return NO;
|
|
106
|
+
return raws?.every((raw) => approvedTarget(resolveToCwd(raw, cwd), cwd, ["file"])) ? YES : NO;
|
|
107
|
+
}
|
|
108
|
+
if (LIST_TOOLS.has(toolName)) {
|
|
109
|
+
const raw = input.path ?? ".";
|
|
110
|
+
if (typeof raw !== "string")
|
|
111
|
+
return NO;
|
|
112
|
+
return approvedTarget(resolveToCwd(raw || ".", cwd), cwd, ["directory"]) ? YES : NO;
|
|
113
|
+
}
|
|
114
|
+
if (WRITE_TOOLS.has(toolName)) {
|
|
115
|
+
const raw = input.path ?? input.file_path;
|
|
116
|
+
if (typeof raw !== "string")
|
|
117
|
+
return NO;
|
|
118
|
+
return approvedTarget(resolveToCwd(raw, cwd), cwd, ["file", "missing"]) ? YES : NO;
|
|
119
|
+
}
|
|
120
|
+
if (toolName === "apply_patch") {
|
|
121
|
+
const targets = patchTargets(input, cwd);
|
|
122
|
+
return targets?.every((target) => approvedTarget(target, cwd, ["file", "missing"])) ? YES : NO;
|
|
123
|
+
}
|
|
124
|
+
return NO;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=auto-policy.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ClassifiedWord } from "./auto-shell-grammar.ts";
|
|
2
|
+
import type { ShellWord } from "./auto-shell-segments.ts";
|
|
3
|
+
/** Classifies a command's words, or undefined when the program or any word is not understood. */
|
|
4
|
+
export type ProgramRule = (args: readonly ShellWord[]) => ClassifiedWord[] | undefined;
|
|
5
|
+
export declare const PROGRAM_RULES: ReadonlyMap<string, ProgramRule>;
|
|
6
|
+
//# sourceMappingURL=auto-program-rules.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { classifyWords } from "./auto-shell-grammar.js";
|
|
2
|
+
const all = (role) => () => role;
|
|
3
|
+
const firstIs = (first, rest) => (index) => index === 0 ? first : rest;
|
|
4
|
+
const flags = (names, role = true) => Object.fromEntries(names.split(" ").map((name) => [name, role]));
|
|
5
|
+
const spec = (value) => (args) => classifyWords(args, value);
|
|
6
|
+
/**
|
|
7
|
+
* grep and rg: the first operand is the pattern unless a pattern came from `-e`/`--regexp`, in
|
|
8
|
+
* which case every operand is a file to read.
|
|
9
|
+
*/
|
|
10
|
+
const searchSpec = (value) => (args) => {
|
|
11
|
+
const patternFromFlag = args.some((word) => word.text === "--regexp" || word.text.startsWith("--regexp=") || /^-[^-]*e/.test(word.text));
|
|
12
|
+
return classifyWords(args, patternFromFlag ? { ...value, operand: all("read-file"), minOperands: 1 } : value);
|
|
13
|
+
};
|
|
14
|
+
const READ_FILES = { flags: {}, operand: all("read-file"), minOperands: 1 };
|
|
15
|
+
const SPECS = [
|
|
16
|
+
["pwd", spec({ flags: {}, operand: all("text"), maxOperands: 0 })],
|
|
17
|
+
["true", spec({ flags: {}, operand: all("text"), maxOperands: 0 })],
|
|
18
|
+
["echo", spec({ flags: flags("-n -e -E"), operand: all("text") })],
|
|
19
|
+
["which", spec({ flags: flags("-a"), operand: all("text"), minOperands: 1 })],
|
|
20
|
+
[
|
|
21
|
+
"ls",
|
|
22
|
+
spec({
|
|
23
|
+
flags: flags("-a -A -l -h -1 -R -t -S -r -d -F -p --all --almost-all --human-readable --recursive"),
|
|
24
|
+
operand: all("list"),
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
["cat", spec({ ...READ_FILES, flags: flags("-n -b -s -A -e -t -v") })],
|
|
28
|
+
["head", spec({ ...READ_FILES, flags: { ...flags("-n -c --lines --bytes", "text"), ...flags("-q -v --quiet") } })],
|
|
29
|
+
["tail", spec({ ...READ_FILES, flags: { ...flags("-n -c --lines --bytes", "text"), ...flags("-q -v --quiet") } })],
|
|
30
|
+
["wc", spec({ ...READ_FILES, flags: flags("-l -w -c -m -L --lines --words --bytes --chars") })],
|
|
31
|
+
[
|
|
32
|
+
"diff",
|
|
33
|
+
spec({
|
|
34
|
+
...READ_FILES,
|
|
35
|
+
flags: { ...flags("-u -q -s -w -b -B -i --brief"), ...flags("-U --unified", "text") },
|
|
36
|
+
minOperands: 2,
|
|
37
|
+
maxOperands: 2,
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
["stat", spec({ flags: flags("-L"), operand: all("list"), minOperands: 1 })],
|
|
41
|
+
["file", spec({ flags: flags("-b -i -L --brief --mime"), operand: all("read-file"), minOperands: 1 })],
|
|
42
|
+
[
|
|
43
|
+
"cut",
|
|
44
|
+
spec({
|
|
45
|
+
...READ_FILES,
|
|
46
|
+
flags: {
|
|
47
|
+
...flags("-d -f -c -b --delimiter --fields --characters --bytes", "text"),
|
|
48
|
+
...flags("-s --only-delimited"),
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"grep",
|
|
54
|
+
searchSpec({
|
|
55
|
+
flags: {
|
|
56
|
+
...flags("-i -n -c -l -L -v -w -x -o -q -s -H -h -F -E --ignore-case --line-number --count --fixed-strings --extended-regexp --files-with-matches"),
|
|
57
|
+
...flags("-e -m -A -B -C --regexp --max-count --after-context --before-context --context", "text"),
|
|
58
|
+
},
|
|
59
|
+
operand: firstIs("text", "read-file"),
|
|
60
|
+
minOperands: 2,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"rg",
|
|
65
|
+
searchSpec({
|
|
66
|
+
flags: {
|
|
67
|
+
...flags("-i -n -c -l -v -w -x -o -F -S -N --ignore-case --line-number --count --fixed-strings --smart-case --files-with-matches --no-heading"),
|
|
68
|
+
...flags("-e -m -A -B -C --regexp --max-count --after-context --before-context --context", "text"),
|
|
69
|
+
},
|
|
70
|
+
operand: firstIs("text", "read-file"),
|
|
71
|
+
minOperands: 2,
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
];
|
|
75
|
+
const GIT_REF = /^[A-Za-z0-9_][A-Za-z0-9_.@-]*$/;
|
|
76
|
+
/**
|
|
77
|
+
* A git operand that is not shaped like a ref is a path; a ref-shaped one (`HEAD`, `main`, `notes`)
|
|
78
|
+
* is still a path when something exists at that name, which the judge checks.
|
|
79
|
+
*/
|
|
80
|
+
const gitOperand = (_index, _count, text) => GIT_REF.test(text) && !text.includes("..") ? "ref-or-path" : "list";
|
|
81
|
+
const GIT_READ_SUBCOMMANDS = {
|
|
82
|
+
status: { flags: flags("-s -b -u --short --branch --porcelain --untracked-files"), operand: all("list") },
|
|
83
|
+
diff: {
|
|
84
|
+
flags: flags("--stat --cached --staged --name-only --name-status --numstat --no-color --color -w"),
|
|
85
|
+
operand: gitOperand,
|
|
86
|
+
},
|
|
87
|
+
log: {
|
|
88
|
+
flags: {
|
|
89
|
+
...flags("--oneline --graph --decorate --stat --no-color --all --reverse --name-only --name-status"),
|
|
90
|
+
...flags("-n --max-count --since --until --author --format --pretty", "text"),
|
|
91
|
+
},
|
|
92
|
+
operand: gitOperand,
|
|
93
|
+
},
|
|
94
|
+
"rev-parse": { flags: flags("--abbrev-ref --short --show-toplevel --verify"), operand: gitOperand },
|
|
95
|
+
"ls-files": {
|
|
96
|
+
flags: flags("-m -o -d -s --modified --others --deleted --stage --exclude-standard"),
|
|
97
|
+
operand: all("list"),
|
|
98
|
+
},
|
|
99
|
+
blame: {
|
|
100
|
+
flags: { ...flags("-w -s --porcelain"), ...flags("-L", "text") },
|
|
101
|
+
operand: all("read-file"),
|
|
102
|
+
minOperands: 1,
|
|
103
|
+
},
|
|
104
|
+
branch: {
|
|
105
|
+
flags: flags("-a -r -v -vv --all --remotes --verbose --list --show-current"),
|
|
106
|
+
operand: all("text"),
|
|
107
|
+
maxOperands: 0,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const SUMMARY_ONLY = new Set(["--stat", "--name-only", "--name-status", "--numstat"]);
|
|
111
|
+
/**
|
|
112
|
+
* Read-only git subcommands only; any global option (`-c`, `-C`, `--git-dir`, a pager) asks.
|
|
113
|
+
* `diff` prints file contents, which can include a tracked secret, so it passes only in a summary
|
|
114
|
+
* form; `show` is not listed (`git show <blob id>` prints any tracked file), no operand may name an
|
|
115
|
+
* object path or a full object id, and an operand that is not a plain ref is checked as a path.
|
|
116
|
+
*/
|
|
117
|
+
const gitRule = (args) => {
|
|
118
|
+
const [sub, ...rest] = args;
|
|
119
|
+
if (sub === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
const subSpec = GIT_READ_SUBCOMMANDS[sub.text];
|
|
122
|
+
// A flag's value (`-n 1000`) is not an object id; every other word is checked.
|
|
123
|
+
const isFlagValue = (index) => {
|
|
124
|
+
const previous = rest[index - 1]?.text;
|
|
125
|
+
return previous !== undefined && typeof subSpec?.flags[previous] === "string";
|
|
126
|
+
};
|
|
127
|
+
if (subSpec === undefined ||
|
|
128
|
+
rest.some((word, index) => word.text.includes(":") || (!isFlagValue(index) && /^[0-9a-f]{4,64}$/i.test(word.text)))) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
if (sub.text === "diff" && !rest.some((word) => SUMMARY_ONLY.has(word.text)))
|
|
132
|
+
return undefined;
|
|
133
|
+
return classifyWords(rest, subSpec);
|
|
134
|
+
};
|
|
135
|
+
export const PROGRAM_RULES = new Map([
|
|
136
|
+
...SPECS,
|
|
137
|
+
["git", gitRule],
|
|
138
|
+
]);
|
|
139
|
+
//# sourceMappingURL=auto-program-rules.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ShellWord } from "./auto-shell-segments.ts";
|
|
2
|
+
/**
|
|
3
|
+
* What a word means to the program, so the judge knows how to check it: `read-file` is read from
|
|
4
|
+
* (must be a regular project file), `list` only has its name shown, `text` is never used as a path
|
|
5
|
+
* (a pattern, a count, a format), and `ref-or-path` is a git revision unless something exists at
|
|
6
|
+
* that name, in which case it is checked like `list` (git reads it as a path).
|
|
7
|
+
*/
|
|
8
|
+
export type WordRole = "read-file" | "list" | "text" | "ref-or-path";
|
|
9
|
+
export interface ClassifiedWord {
|
|
10
|
+
readonly role: WordRole;
|
|
11
|
+
readonly word: ShellWord;
|
|
12
|
+
}
|
|
13
|
+
export interface ProgramSpec {
|
|
14
|
+
/** Every flag the program may take; `true` = no value, a role = takes one value. */
|
|
15
|
+
readonly flags: Readonly<Record<string, true | WordRole>>;
|
|
16
|
+
/** The role of operand `index` out of `count` operands, given its text. */
|
|
17
|
+
readonly operand: (index: number, count: number, text: string) => WordRole;
|
|
18
|
+
readonly minOperands?: number;
|
|
19
|
+
readonly maxOperands?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Classifies every word of one simple command against `spec`, or returns undefined when any word
|
|
23
|
+
* is not understood: an unknown flag (short or long), a flag in a form the spec does not list, a
|
|
24
|
+
* value-taking flag with no value, or an operand count out of range. Short flags may be
|
|
25
|
+
* clustered (`-la`), and a value-taking short flag takes the rest of its word as the value
|
|
26
|
+
* (`-o/x`, `-ro/x`) or else the next word; a long flag takes `--name=value` or `--name value`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function classifyWords(words: readonly ShellWord[], spec: ProgramSpec): ClassifiedWord[] | undefined;
|
|
29
|
+
//# sourceMappingURL=auto-shell-grammar.d.ts.map
|