tinker-agent 1.0.65
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/README.md +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
3
|
+
import {
|
|
4
|
+
ShellTaskManager,
|
|
5
|
+
type ShellTaskHandle,
|
|
6
|
+
type ShellTaskInspection,
|
|
7
|
+
type ShellTaskSnapshot,
|
|
8
|
+
} from "./bash-task";
|
|
9
|
+
import { isWorkspaceLocalCwd, type CwdState } from "./cwd-state";
|
|
10
|
+
import { buildOutputSnapshotFromText } from "./task-output-snapshot";
|
|
11
|
+
import { defineToolExecutor } from "./types";
|
|
12
|
+
import type { TaskOutputSnapshot } from "./task-output";
|
|
13
|
+
import type { BashRawResult, ToolExecutionContext, ToolExecutor } from "./types";
|
|
14
|
+
|
|
15
|
+
type BashArgs = {
|
|
16
|
+
command: string;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
description?: string;
|
|
19
|
+
run_in_background?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type BashToolOptions = {
|
|
23
|
+
workspaceRoot: string;
|
|
24
|
+
cwdState: CwdState;
|
|
25
|
+
taskManager: ShellTaskManager;
|
|
26
|
+
defaultTimeoutMs?: number;
|
|
27
|
+
maxTimeoutMs?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const defaultForegroundTimeoutMs = 5_000;
|
|
31
|
+
const defaultMaxForegroundTimeoutMs = 600_000;
|
|
32
|
+
|
|
33
|
+
export function createBashToolExecutor(options: BashToolOptions): ToolExecutor {
|
|
34
|
+
const maxTimeoutMs =
|
|
35
|
+
options.maxTimeoutMs ??
|
|
36
|
+
parsePositiveInteger(
|
|
37
|
+
process.env.TINKER_BASH_MAX_TIMEOUT_MS,
|
|
38
|
+
defaultMaxForegroundTimeoutMs,
|
|
39
|
+
);
|
|
40
|
+
const defaultTimeoutMs =
|
|
41
|
+
options.defaultTimeoutMs ??
|
|
42
|
+
Math.min(
|
|
43
|
+
parsePositiveInteger(
|
|
44
|
+
process.env.TINKER_BASH_DEFAULT_TIMEOUT_MS,
|
|
45
|
+
defaultForegroundTimeoutMs,
|
|
46
|
+
),
|
|
47
|
+
maxTimeoutMs,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return defineToolExecutor("bash", {
|
|
51
|
+
definition: {
|
|
52
|
+
name: "Bash",
|
|
53
|
+
description: "Run a shell command in the local workspace.",
|
|
54
|
+
parameters: {
|
|
55
|
+
type: "object",
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
properties: {
|
|
58
|
+
command: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "The shell command to execute.",
|
|
61
|
+
},
|
|
62
|
+
timeout: {
|
|
63
|
+
type: "integer",
|
|
64
|
+
minimum: 1,
|
|
65
|
+
maximum: maxTimeoutMs,
|
|
66
|
+
description: "Optional foreground timeout in milliseconds.",
|
|
67
|
+
},
|
|
68
|
+
description: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Clear 5-10 word description of what this command does.",
|
|
71
|
+
},
|
|
72
|
+
run_in_background: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
description: "Run the command in the background and return immediately.",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["command"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
async execute(args, call, context: ToolExecutionContext): Promise<BashRawResult> {
|
|
81
|
+
throwIfTurnCancelled(context.signal);
|
|
82
|
+
const parsed = parseBashArgs(args, maxTimeoutMs);
|
|
83
|
+
|
|
84
|
+
if (!parsed.ok) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
command: "",
|
|
88
|
+
taskId: "",
|
|
89
|
+
sessionId: call.sessionId,
|
|
90
|
+
status: "failed",
|
|
91
|
+
cwd: options.cwdState.cwd,
|
|
92
|
+
outputFilePath: "",
|
|
93
|
+
outputBytes: 0,
|
|
94
|
+
outputLines: 0,
|
|
95
|
+
preview: "",
|
|
96
|
+
truncated: false,
|
|
97
|
+
error: parsed.error,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const input = parsed.value;
|
|
102
|
+
const foregroundTimeoutMs = input.timeout ?? defaultTimeoutMs;
|
|
103
|
+
throwIfTurnCancelled(context.signal);
|
|
104
|
+
const task = await options.taskManager.start({
|
|
105
|
+
command: input.command,
|
|
106
|
+
description: input.description ?? input.command,
|
|
107
|
+
origin: call,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (input.run_in_background === true) {
|
|
111
|
+
// Starting and publishing an explicit background task is one commit
|
|
112
|
+
// boundary. Cancellation is observed after its result is recorded.
|
|
113
|
+
await options.taskManager.markBackgrounded(task.taskId, "requested");
|
|
114
|
+
const inspection = requireTaskInspection(options.taskManager, task.taskId);
|
|
115
|
+
if (inspection.task.status !== "running") {
|
|
116
|
+
return buildCompletedResult(inspection.task, inspection.output);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return buildRunningResult({
|
|
120
|
+
inspection,
|
|
121
|
+
backgrounded: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const waitResult = await waitForTask(task, foregroundTimeoutMs, context.signal);
|
|
126
|
+
if (waitResult.type === "cancelled") {
|
|
127
|
+
await options.taskManager.cancelForegroundTask(task.taskId);
|
|
128
|
+
throw cancellationError(context.signal);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (waitResult.type === "timeout") {
|
|
132
|
+
// Timeout wins ownership. Marking the task backgrounded and returning
|
|
133
|
+
// its task ID is an uninterrupted commit boundary.
|
|
134
|
+
await options.taskManager.markBackgrounded(task.taskId, "foreground_timeout");
|
|
135
|
+
const inspection = requireTaskInspection(options.taskManager, task.taskId);
|
|
136
|
+
if (inspection.task.status !== "running") {
|
|
137
|
+
return buildCompletedResult(inspection.task, inspection.output);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return buildRunningResult({
|
|
141
|
+
inspection,
|
|
142
|
+
timedOut: true,
|
|
143
|
+
backgrounded: true,
|
|
144
|
+
backgroundedDueToTimeout: true,
|
|
145
|
+
timeoutMs: foregroundTimeoutMs,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const inspection = requireTaskInspection(options.taskManager, task.taskId);
|
|
150
|
+
const raw = await buildCompletedResult(waitResult.task, inspection.output);
|
|
151
|
+
updateCwdStateAfterForegroundCommand({
|
|
152
|
+
raw,
|
|
153
|
+
task: waitResult.task,
|
|
154
|
+
cwdState: options.cwdState,
|
|
155
|
+
workspaceRoot: options.workspaceRoot,
|
|
156
|
+
});
|
|
157
|
+
return raw;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function parseBashArgs(
|
|
163
|
+
args: unknown,
|
|
164
|
+
maxTimeoutMs = defaultMaxForegroundTimeoutMs,
|
|
165
|
+
): { ok: true; value: BashArgs } | { ok: false; error: string } {
|
|
166
|
+
if (!isRecord(args)) {
|
|
167
|
+
return { ok: false, error: "Bash arguments must be an object." };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof args.command !== "string" || args.command.trim() === "") {
|
|
171
|
+
return { ok: false, error: "Bash.command must be a non-empty string." };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const timeout = parseOptionalTimeout(args.timeout, maxTimeoutMs);
|
|
175
|
+
if (!timeout.ok) {
|
|
176
|
+
return timeout;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (args.description !== undefined && typeof args.description !== "string") {
|
|
180
|
+
return { ok: false, error: "Bash.description must be a string." };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
args.run_in_background !== undefined &&
|
|
185
|
+
typeof args.run_in_background !== "boolean"
|
|
186
|
+
) {
|
|
187
|
+
return { ok: false, error: "Bash.run_in_background must be a boolean." };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
value: {
|
|
193
|
+
command: args.command,
|
|
194
|
+
timeout: timeout.value,
|
|
195
|
+
description:
|
|
196
|
+
args.description === undefined || args.description.trim() === ""
|
|
197
|
+
? undefined
|
|
198
|
+
: args.description,
|
|
199
|
+
run_in_background: args.run_in_background,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function interpretCommandResult(input: {
|
|
205
|
+
command: string;
|
|
206
|
+
exitCode?: number;
|
|
207
|
+
status: BashRawResult["status"];
|
|
208
|
+
}): { ok: boolean; status: BashRawResult["status"]; interpretation?: string } {
|
|
209
|
+
if (input.status === "running") {
|
|
210
|
+
return { ok: true, status: "running" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (input.status === "killed") {
|
|
214
|
+
return { ok: false, status: "killed" };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const exitCode = input.exitCode ?? 1;
|
|
218
|
+
if (exitCode === 0) {
|
|
219
|
+
return { ok: true, status: "completed" };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const commandName = lastPipelineCommandName(input.command);
|
|
223
|
+
const interpretation = interpretInformationalExitCode(commandName, exitCode);
|
|
224
|
+
if (interpretation !== undefined) {
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
status: "completed",
|
|
228
|
+
interpretation,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { ok: false, status: "failed" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
type ForegroundWaitResult =
|
|
236
|
+
| { type: "completed"; task: ShellTaskSnapshot }
|
|
237
|
+
| { type: "timeout" }
|
|
238
|
+
| { type: "cancelled" };
|
|
239
|
+
|
|
240
|
+
async function waitForTask(
|
|
241
|
+
task: ShellTaskHandle,
|
|
242
|
+
timeoutMs: number,
|
|
243
|
+
signal: AbortSignal,
|
|
244
|
+
): Promise<ForegroundWaitResult> {
|
|
245
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
246
|
+
let onAbort: (() => void) | undefined;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
return await Promise.race([
|
|
250
|
+
task.completion.then(
|
|
251
|
+
(snapshot): ForegroundWaitResult => ({
|
|
252
|
+
type: "completed",
|
|
253
|
+
task: snapshot,
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
256
|
+
new Promise<ForegroundWaitResult>((resolve) => {
|
|
257
|
+
timeout = setTimeout(() => resolve({ type: "timeout" }), timeoutMs);
|
|
258
|
+
}),
|
|
259
|
+
new Promise<ForegroundWaitResult>((resolve) => {
|
|
260
|
+
onAbort = () => resolve({ type: "cancelled" });
|
|
261
|
+
if (signal.aborted) {
|
|
262
|
+
onAbort();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
267
|
+
}),
|
|
268
|
+
]);
|
|
269
|
+
} finally {
|
|
270
|
+
if (timeout !== undefined) {
|
|
271
|
+
clearTimeout(timeout);
|
|
272
|
+
}
|
|
273
|
+
if (onAbort !== undefined) {
|
|
274
|
+
signal.removeEventListener("abort", onAbort);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildRunningResult(input: {
|
|
280
|
+
inspection: ShellTaskInspection;
|
|
281
|
+
timedOut?: boolean;
|
|
282
|
+
backgrounded?: boolean;
|
|
283
|
+
backgroundedDueToTimeout?: boolean;
|
|
284
|
+
timeoutMs?: number;
|
|
285
|
+
}): BashRawResult {
|
|
286
|
+
const { task, output } = input.inspection;
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
ok: true,
|
|
290
|
+
command: task.command,
|
|
291
|
+
taskId: task.taskId,
|
|
292
|
+
sessionId: task.origin.sessionId,
|
|
293
|
+
status: "running",
|
|
294
|
+
cwd: task.cwd,
|
|
295
|
+
outputFilePath: task.outputFilePath,
|
|
296
|
+
outputBytes: output.outputBytes,
|
|
297
|
+
outputLines: output.outputLines,
|
|
298
|
+
preview: output.preview,
|
|
299
|
+
truncated: output.truncated,
|
|
300
|
+
omittedLines: output.omittedLines,
|
|
301
|
+
timedOut: input.timedOut,
|
|
302
|
+
timeoutMs: input.timeoutMs,
|
|
303
|
+
backgrounded: input.backgrounded,
|
|
304
|
+
backgroundedDueToTimeout: input.backgroundedDueToTimeout,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function buildCompletedResult(
|
|
309
|
+
task: ShellTaskSnapshot,
|
|
310
|
+
fallbackOutput: TaskOutputSnapshot,
|
|
311
|
+
): Promise<BashRawResult> {
|
|
312
|
+
if (task.status === "running" || task.status === "stopping") {
|
|
313
|
+
throw new Error(`Bash task ${task.taskId} completed with status=${task.status}.`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const snapshot = await snapshotCompletedOutput(task, fallbackOutput);
|
|
317
|
+
const interpretation = interpretCommandResult({
|
|
318
|
+
command: task.command,
|
|
319
|
+
exitCode: task.exitCode,
|
|
320
|
+
status: task.status,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
ok: interpretation.ok,
|
|
325
|
+
command: task.command,
|
|
326
|
+
taskId: task.taskId,
|
|
327
|
+
sessionId: task.origin.sessionId,
|
|
328
|
+
status: interpretation.status,
|
|
329
|
+
exitCode: task.exitCode,
|
|
330
|
+
signal: task.signal,
|
|
331
|
+
cwd: task.cwd,
|
|
332
|
+
outputFilePath: task.outputFilePath,
|
|
333
|
+
outputBytes: snapshot.outputBytes,
|
|
334
|
+
outputLines: snapshot.outputLines,
|
|
335
|
+
preview: snapshot.preview,
|
|
336
|
+
truncated: snapshot.truncated,
|
|
337
|
+
omittedLines: snapshot.omittedLines,
|
|
338
|
+
returnCodeInterpretation: interpretation.interpretation,
|
|
339
|
+
error: task.error,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function snapshotCompletedOutput(
|
|
344
|
+
task: ShellTaskSnapshot,
|
|
345
|
+
fallback: TaskOutputSnapshot,
|
|
346
|
+
): Promise<TaskOutputSnapshot> {
|
|
347
|
+
try {
|
|
348
|
+
const content = await readFile(task.outputFilePath);
|
|
349
|
+
return buildOutputSnapshotFromText(content);
|
|
350
|
+
} catch {
|
|
351
|
+
return fallback;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function updateCwdStateAfterForegroundCommand(input: {
|
|
356
|
+
raw: BashRawResult;
|
|
357
|
+
task: ShellTaskSnapshot;
|
|
358
|
+
cwdState: CwdState;
|
|
359
|
+
workspaceRoot: string;
|
|
360
|
+
}): void {
|
|
361
|
+
if (
|
|
362
|
+
input.raw.ok &&
|
|
363
|
+
input.raw.status === "completed" &&
|
|
364
|
+
isWorkspaceLocalCwd(input.workspaceRoot, input.task.cwd)
|
|
365
|
+
) {
|
|
366
|
+
input.cwdState.cwd = input.task.cwd;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function requireTaskInspection(
|
|
371
|
+
taskManager: ShellTaskManager,
|
|
372
|
+
taskId: string,
|
|
373
|
+
): ShellTaskInspection {
|
|
374
|
+
const inspection = taskManager.inspectTask(taskId);
|
|
375
|
+
if (inspection === undefined) {
|
|
376
|
+
throw new Error(`Bash task disappeared from task manager: ${taskId}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return inspection;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function parseOptionalTimeout(
|
|
383
|
+
value: unknown,
|
|
384
|
+
maxTimeoutMs: number,
|
|
385
|
+
): { ok: true; value?: number } | { ok: false; error: string } {
|
|
386
|
+
if (value === undefined) {
|
|
387
|
+
return { ok: true };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!Number.isInteger(value) || typeof value !== "number" || value < 1) {
|
|
391
|
+
return { ok: false, error: "Bash.timeout must be a positive integer." };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (value > maxTimeoutMs) {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
error: `Bash.timeout must be less than or equal to ${maxTimeoutMs}.`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { ok: true, value };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
|
405
|
+
if (value === undefined) {
|
|
406
|
+
return fallback;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const parsed = Number(value);
|
|
410
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function lastPipelineCommandName(command: string): string | undefined {
|
|
414
|
+
const segments = command.split("|");
|
|
415
|
+
const lastSegment = segments.at(-1)?.trim();
|
|
416
|
+
if (lastSegment === undefined || lastSegment === "") {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const firstToken = lastSegment.match(/^[\s([{]*([^\s;&|)]+)/)?.[1];
|
|
421
|
+
if (firstToken === undefined) {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return firstToken.split("/").at(-1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function interpretInformationalExitCode(
|
|
429
|
+
commandName: string | undefined,
|
|
430
|
+
exitCode: number,
|
|
431
|
+
): string | undefined {
|
|
432
|
+
if (exitCode !== 1 || commandName === undefined) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const interpretations = new Map<string, string>([
|
|
437
|
+
["grep", "No matches found."],
|
|
438
|
+
["rg", "No matches found."],
|
|
439
|
+
["find", "Some directories were inaccessible."],
|
|
440
|
+
["diff", "Files differ."],
|
|
441
|
+
["test", "Condition is false."],
|
|
442
|
+
["[", "Condition is false."],
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
return interpretations.get(commandName);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
449
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
450
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export type CwdState = {
|
|
5
|
+
cwd: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createCwdState(workspaceRoot: string): CwdState {
|
|
9
|
+
return {
|
|
10
|
+
cwd: path.resolve(workspaceRoot),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isWorkspaceLocalCwd(workspaceRoot: string, cwd: string): boolean {
|
|
15
|
+
try {
|
|
16
|
+
const root = realpathSync(path.resolve(workspaceRoot));
|
|
17
|
+
const candidate = realpathSync(path.resolve(cwd));
|
|
18
|
+
return candidate === root || candidate.startsWith(root + path.sep);
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|