@thegitai/cli 1.0.0-beta.16 → 1.0.0-beta.18
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/dist/bin/ai.js +92 -11
- package/dist/src/agent-mode.js +4 -0
- package/dist/src/api/chat.js +43 -8
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/executor.js +4 -4
- package/dist/src/help-text.js +10 -0
- package/dist/src/tool-executor.js +132 -17
- package/dist/src/tools/index.js +4 -0
- package/dist/src/tools/run-command.js +81 -12
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/ui/repl.js +239 -3
- package/dist/src/ui/tui/build-frame.js +99 -5
- package/dist/src/ui/tui/shell-input.js +33 -0
- package/package.json +5 -5
package/dist/src/executor.js
CHANGED
|
@@ -459,7 +459,7 @@ function shouldDropOutputLine(line, rootDir, baseDir) {
|
|
|
459
459
|
function isLsDirectoryHeader(line) {
|
|
460
460
|
return /^\.?\/?.+:$/.test(line) && !line.includes(' ');
|
|
461
461
|
}
|
|
462
|
-
function sanitizeCommandText(command, text, rootDir) {
|
|
462
|
+
export function sanitizeCommandText(command, text, rootDir) {
|
|
463
463
|
if (!text)
|
|
464
464
|
return '';
|
|
465
465
|
const lines = text.split('\n');
|
|
@@ -534,7 +534,7 @@ export function cancelActiveCommand() {
|
|
|
534
534
|
cancelled: true,
|
|
535
535
|
});
|
|
536
536
|
}
|
|
537
|
-
function terminateChild(child, signal) {
|
|
537
|
+
export function terminateChild(child, signal) {
|
|
538
538
|
if (!child?.pid)
|
|
539
539
|
return;
|
|
540
540
|
if (process.platform === 'win32') {
|
|
@@ -591,7 +591,7 @@ export function getBlockedCommandReason(command, hasTimeout, rootDir) {
|
|
|
591
591
|
}
|
|
592
592
|
return null;
|
|
593
593
|
}
|
|
594
|
-
function commandUsesSudo(command) {
|
|
594
|
+
export function commandUsesSudo(command) {
|
|
595
595
|
return /\bsudo\b/.test(getUnquotedShellText(command));
|
|
596
596
|
}
|
|
597
597
|
export function sudoPromptFromTail(text) {
|
|
@@ -619,7 +619,7 @@ function redactSecrets(text, secrets) {
|
|
|
619
619
|
}
|
|
620
620
|
return redacted;
|
|
621
621
|
}
|
|
622
|
-
function buildCommandEnv(cwd) {
|
|
622
|
+
export function buildCommandEnv(cwd) {
|
|
623
623
|
const venvBin = detectVenvBin(cwd);
|
|
624
624
|
const envPath = buildCommandPath(process.env.PATH, venvBin ? [venvBin] : []);
|
|
625
625
|
return {
|
package/dist/src/help-text.js
CHANGED
|
@@ -73,6 +73,10 @@ const HELP_MARKDOWN = [
|
|
|
73
73
|
'- `/model` — list supported models and pick one',
|
|
74
74
|
'- `/model <id>` — switch the active model without clearing history',
|
|
75
75
|
'- `/resume` — resume a saved session for this repo',
|
|
76
|
+
'- `/jobs` — open the background jobs picker (Enter expands/collapses,',
|
|
77
|
+
' k kills the selected job)',
|
|
78
|
+
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
79
|
+
'- `/jobs kill <id>` — kill one background job',
|
|
76
80
|
'- `/clear` — clear the current conversation history',
|
|
77
81
|
'- `/exit` — quit the session',
|
|
78
82
|
'',
|
|
@@ -85,6 +89,12 @@ const HELP_MARKDOWN = [
|
|
|
85
89
|
' command and keeps the password masked and local.',
|
|
86
90
|
'- `-y` / `--yes` at startup auto-approves every shell command and file',
|
|
87
91
|
' edit for the whole session — use with care.',
|
|
92
|
+
'- Long-running commands (e.g. dev servers) can run as managed background',
|
|
93
|
+
' jobs after the same approval as any other command. Background output',
|
|
94
|
+
' stays quiet once the model has responded; the footer shows only a compact',
|
|
95
|
+
' shell-running indicator, `/jobs` lists, inspects, and kills jobs, and',
|
|
96
|
+
' killed jobs disappear immediately. Every job is killed when the session',
|
|
97
|
+
' ends.',
|
|
88
98
|
'- File and shell operations are confined to the target repo root.',
|
|
89
99
|
'- Sensitive directories (`.git`, `node_modules`, build output) are',
|
|
90
100
|
' never indexed.',
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { drainBackgroundJobNotifications, getBackgroundJob, } from './background-jobs.js';
|
|
1
2
|
import { canStoreEditSnapshot, isEditToolName, isGitWorkTree, MAX_EDIT_JOURNAL_RECORDS, operationFromSnapshots, readFileEditSnapshot, } from './edit-journal.js';
|
|
2
3
|
import { clearEditFailure, collectCommandMutations, captureMutationBaseline, ensureActiveCheckpoint, recordEditFailure, recordSessionEdit, rememberCheckpointFiles, } from './session-safety.js';
|
|
3
4
|
import { buildAgentModeToolBlockedResult, } from './agent-mode.js';
|
|
4
5
|
import { dispatchTool } from './tools/index.js';
|
|
6
|
+
import { syncIndexFromDisk } from './project-index.js';
|
|
5
7
|
import { PATH_REPAIRING_EDIT_TOOLS, repairFilePath } from './tools/path-suggest.js';
|
|
6
8
|
import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './tools/shell-diagnostics.js';
|
|
7
9
|
const EDIT_FILE_PATH_ARG_ALIASES = [
|
|
@@ -12,6 +14,7 @@ const EDIT_FILE_PATH_ARG_ALIASES = [
|
|
|
12
14
|
'file',
|
|
13
15
|
'filename',
|
|
14
16
|
];
|
|
17
|
+
const backgroundCommandTrackers = new Map();
|
|
15
18
|
function toolCallSummary(call) {
|
|
16
19
|
const args = call.args && typeof call.args === 'object' ? call.args : {};
|
|
17
20
|
if (call.name === 'run_command') {
|
|
@@ -20,6 +23,10 @@ function toolCallSummary(call) {
|
|
|
20
23
|
if (call.name === 'run_node_script') {
|
|
21
24
|
return String(args.script ?? '').trim().slice(0, 120);
|
|
22
25
|
}
|
|
26
|
+
if (call.name === 'shell_job_output' ||
|
|
27
|
+
call.name === 'shell_job_kill') {
|
|
28
|
+
return String(args.job_id ?? '').trim();
|
|
29
|
+
}
|
|
23
30
|
const filePath = getEditToolFilePath(call);
|
|
24
31
|
if (filePath)
|
|
25
32
|
return filePath;
|
|
@@ -53,6 +60,84 @@ function editToolWritesSeparateOutput(call) {
|
|
|
53
60
|
const output = args.outputPath ?? args.output_path;
|
|
54
61
|
return typeof output === 'string' && output.trim().length > 0;
|
|
55
62
|
}
|
|
63
|
+
async function collectTrackedCommandMutations({ session, projectIndex, result, tracker, toolName, toolCallId, turnId, }) {
|
|
64
|
+
const checkpoint = ensureActiveCheckpoint(session.clientState.safety, turnId);
|
|
65
|
+
const records = collectCommandMutations({
|
|
66
|
+
state: session.clientState.safety,
|
|
67
|
+
rootDir: session.rootDir,
|
|
68
|
+
tracker,
|
|
69
|
+
toolName,
|
|
70
|
+
toolCallId,
|
|
71
|
+
turnId,
|
|
72
|
+
checkpointId: checkpoint.id,
|
|
73
|
+
});
|
|
74
|
+
if (!records.length)
|
|
75
|
+
return;
|
|
76
|
+
invalidateShellDiagnosticsCache(session.rootDir);
|
|
77
|
+
rememberCheckpointFiles(session.clientState.safety, session.rootDir, records.map((record) => record.filePath), turnId);
|
|
78
|
+
const repoSync = await syncIndexFromDisk(projectIndex);
|
|
79
|
+
result.repoSync = repoSync;
|
|
80
|
+
result.sessionEdits = records.map((record) => ({
|
|
81
|
+
id: record.id,
|
|
82
|
+
filePath: record.filePath,
|
|
83
|
+
operation: record.operation,
|
|
84
|
+
beforeHash: record.beforeHash,
|
|
85
|
+
afterHash: record.afterHash,
|
|
86
|
+
}));
|
|
87
|
+
result.diagnostics = runShellDiagnostics(session.rootDir);
|
|
88
|
+
}
|
|
89
|
+
export async function collectBackgroundJobUiKillMutations({ session, projectIndex, jobId, result, }) {
|
|
90
|
+
const normalizedJobId = String(jobId ?? result.snapshot?.id ?? '').trim();
|
|
91
|
+
if (!normalizedJobId || !result.ok)
|
|
92
|
+
return;
|
|
93
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
94
|
+
if (!tracked)
|
|
95
|
+
return;
|
|
96
|
+
const mutationResult = result;
|
|
97
|
+
await collectTrackedCommandMutations({
|
|
98
|
+
session,
|
|
99
|
+
projectIndex,
|
|
100
|
+
result: mutationResult,
|
|
101
|
+
tracker: tracked.tracker,
|
|
102
|
+
toolName: tracked.toolName,
|
|
103
|
+
toolCallId: tracked.toolCallId,
|
|
104
|
+
turnId: tracked.turnId,
|
|
105
|
+
});
|
|
106
|
+
if (result.snapshot?.status === 'running') {
|
|
107
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export async function collectBackgroundJobUiOutputMutations({ session, projectIndex, jobId, }) {
|
|
114
|
+
const normalizedJobId = String(jobId ?? '').trim();
|
|
115
|
+
if (!normalizedJobId)
|
|
116
|
+
return;
|
|
117
|
+
const tracked = backgroundCommandTrackers.get(normalizedJobId);
|
|
118
|
+
if (!tracked)
|
|
119
|
+
return;
|
|
120
|
+
const snapshot = getBackgroundJob(normalizedJobId, {
|
|
121
|
+
sessionId: session.sessionId,
|
|
122
|
+
});
|
|
123
|
+
if (!snapshot)
|
|
124
|
+
return;
|
|
125
|
+
await collectTrackedCommandMutations({
|
|
126
|
+
session,
|
|
127
|
+
projectIndex,
|
|
128
|
+
result: {},
|
|
129
|
+
tracker: tracked.tracker,
|
|
130
|
+
toolName: tracked.toolName,
|
|
131
|
+
toolCallId: tracked.toolCallId,
|
|
132
|
+
turnId: tracked.turnId,
|
|
133
|
+
});
|
|
134
|
+
if (snapshot.status === 'running') {
|
|
135
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
backgroundCommandTrackers.delete(normalizedJobId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
56
141
|
function recordAssistantEdit(session, call, result, before) {
|
|
57
142
|
if (!before || !isEditToolName(call.name))
|
|
58
143
|
return;
|
|
@@ -146,6 +231,7 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
146
231
|
: null;
|
|
147
232
|
const context = {
|
|
148
233
|
rootDir: session.rootDir,
|
|
234
|
+
sessionId: session.sessionId,
|
|
149
235
|
projectIndex: toolContext.projectIndex,
|
|
150
236
|
autoYes: session.autoYes,
|
|
151
237
|
confirmCommand: session.confirmCommand,
|
|
@@ -167,28 +253,57 @@ export async function executeLocalToolCall(toolContext, session, call) {
|
|
|
167
253
|
};
|
|
168
254
|
const result = await dispatchTool(context, call);
|
|
169
255
|
recordAssistantEdit(session, call, result, beforeEditSnapshot);
|
|
170
|
-
if (result &&
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
256
|
+
if (result && typeof result === 'object' && commandTracker) {
|
|
257
|
+
await collectTrackedCommandMutations({
|
|
258
|
+
session,
|
|
259
|
+
projectIndex: toolContext.projectIndex,
|
|
260
|
+
result,
|
|
175
261
|
tracker: commandTracker,
|
|
176
262
|
toolName: call.name,
|
|
177
263
|
toolCallId: call.id,
|
|
178
264
|
turnId: session.turnState.id,
|
|
179
|
-
checkpointId: checkpoint.id,
|
|
180
265
|
});
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
result.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
191
|
-
|
|
266
|
+
if (call.name === 'run_command' &&
|
|
267
|
+
result.backgrounded === true &&
|
|
268
|
+
result.status === 'running' &&
|
|
269
|
+
result.jobId) {
|
|
270
|
+
backgroundCommandTrackers.set(String(result.jobId), {
|
|
271
|
+
tracker: captureMutationBaseline(session.rootDir),
|
|
272
|
+
toolName: call.name,
|
|
273
|
+
toolCallId: call.id,
|
|
274
|
+
turnId: session.turnState.id,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (result &&
|
|
279
|
+
typeof result === 'object' &&
|
|
280
|
+
(call.name === 'shell_job_output' || call.name === 'shell_job_kill')) {
|
|
281
|
+
const jobId = String(result.jobId ?? call.args?.job_id ?? '').trim();
|
|
282
|
+
const tracked = backgroundCommandTrackers.get(jobId);
|
|
283
|
+
if (tracked) {
|
|
284
|
+
await collectTrackedCommandMutations({
|
|
285
|
+
session,
|
|
286
|
+
projectIndex: toolContext.projectIndex,
|
|
287
|
+
result,
|
|
288
|
+
tracker: tracked.tracker,
|
|
289
|
+
toolName: tracked.toolName,
|
|
290
|
+
toolCallId: tracked.toolCallId,
|
|
291
|
+
turnId: tracked.turnId,
|
|
292
|
+
});
|
|
293
|
+
if (result.status === 'running') {
|
|
294
|
+
tracked.tracker = captureMutationBaseline(session.rootDir);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
backgroundCommandTrackers.delete(jobId);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (result && typeof result === 'object') {
|
|
302
|
+
const backgroundJobUpdate = drainBackgroundJobNotifications({
|
|
303
|
+
sessionId: session.sessionId,
|
|
304
|
+
});
|
|
305
|
+
if (backgroundJobUpdate) {
|
|
306
|
+
result.backgroundJobUpdate = backgroundJobUpdate;
|
|
192
307
|
}
|
|
193
308
|
}
|
|
194
309
|
session.onToolEvent?.({ call, result });
|
package/dist/src/tools/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shellJobOutput } from './shell-job-output.js';
|
|
1
2
|
import { deleteFile } from './delete-file.js';
|
|
2
3
|
import { findSymbol } from './find-symbol.js';
|
|
3
4
|
import { getDiagnostics } from './get-diagnostics.js';
|
|
@@ -17,6 +18,7 @@ import { runNodeScript } from './run-node-script.js';
|
|
|
17
18
|
import { restoreFilesToCheckpoint, restoreToCheckpoint, } from './restore-checkpoint.js';
|
|
18
19
|
import { searchCode } from './search-code.js';
|
|
19
20
|
import { getSignatureHelp } from './signature-help.js';
|
|
21
|
+
import { shellJobKill } from './shell-job-kill.js';
|
|
20
22
|
import { strReplace } from './str-replace.js';
|
|
21
23
|
import { undoEdit } from './undo-edit.js';
|
|
22
24
|
import { writeFile } from './write-file.js';
|
|
@@ -44,6 +46,8 @@ export const TOOL_MAP = {
|
|
|
44
46
|
undo_edit: undoEdit,
|
|
45
47
|
run_command: runShellCommand,
|
|
46
48
|
run_node_script: runNodeScript,
|
|
49
|
+
shell_job_output: shellJobOutput,
|
|
50
|
+
shell_job_kill: shellJobKill,
|
|
47
51
|
};
|
|
48
52
|
function invalidToolCall(error) {
|
|
49
53
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from '../colors.js';
|
|
2
|
+
import { startBackgroundJob } from '../background-jobs.js';
|
|
2
3
|
import { getBlockedCommandReason, runCommand, } from '../executor.js';
|
|
3
4
|
import { syncIndexFromDisk } from '../project-index.js';
|
|
4
5
|
import { isTuiMode } from '../runtime-mode.js';
|
|
@@ -12,9 +13,10 @@ export async function runShellCommand(context, args) {
|
|
|
12
13
|
if (!command) {
|
|
13
14
|
return { ok: false, error: 'command is required' };
|
|
14
15
|
}
|
|
16
|
+
const runInBackground = args.background === true;
|
|
15
17
|
const hasTimeout = typeof args.timeout_ms === 'number' && args.timeout_ms > 0;
|
|
16
18
|
const repoHint = buildNestedGitHint(rootDir, command);
|
|
17
|
-
const blockedReason = getBlockedCommandReason(command, hasTimeout, rootDir);
|
|
19
|
+
const blockedReason = getBlockedCommandReason(command, hasTimeout || runInBackground, rootDir);
|
|
18
20
|
if (blockedReason) {
|
|
19
21
|
const error = blockedReason;
|
|
20
22
|
if (!isTuiMode())
|
|
@@ -37,7 +39,9 @@ export async function runShellCommand(context, args) {
|
|
|
37
39
|
error: 'confirmCommand is required when autoYes is false',
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
|
-
const approved = await confirmCommand(
|
|
42
|
+
const approved = await confirmCommand(runInBackground
|
|
43
|
+
? `${command}\n\nRuns as a managed background job until it exits or is killed.`
|
|
44
|
+
: command);
|
|
41
45
|
if (!approved) {
|
|
42
46
|
if (!isTuiMode())
|
|
43
47
|
console.log(chalk.dim(` ⏭ Skipped: ${command}`));
|
|
@@ -49,6 +53,9 @@ export async function runShellCommand(context, args) {
|
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
}
|
|
56
|
+
if (runInBackground) {
|
|
57
|
+
return runBackgroundCommand(context, command, args.timeout_ms, repoHint);
|
|
58
|
+
}
|
|
52
59
|
const result = await runCommand(command, rootDir, {
|
|
53
60
|
requestSudoPassword,
|
|
54
61
|
timeout: typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? args.timeout_ms : undefined,
|
|
@@ -67,17 +74,9 @@ export async function runShellCommand(context, args) {
|
|
|
67
74
|
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
68
75
|
onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
69
76
|
}
|
|
70
|
-
|
|
71
|
-
? redactConnectionStringCredentials(result.output)
|
|
77
|
+
const output = typeof result.output === 'string'
|
|
78
|
+
? boundCommandOutput(redactConnectionStringCredentials(result.output))
|
|
72
79
|
: result.output;
|
|
73
|
-
if (typeof output === 'string' && output.length > MAX_OUTPUT_CHARS) {
|
|
74
|
-
const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.2);
|
|
75
|
-
const tailSize = MAX_OUTPUT_CHARS - headSize;
|
|
76
|
-
output =
|
|
77
|
-
output.slice(0, headSize) +
|
|
78
|
-
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
79
|
-
output.slice(-tailSize);
|
|
80
|
-
}
|
|
81
80
|
return {
|
|
82
81
|
ok: result.exitCode === 0,
|
|
83
82
|
command,
|
|
@@ -90,3 +89,73 @@ export async function runShellCommand(context, args) {
|
|
|
90
89
|
repoHint,
|
|
91
90
|
};
|
|
92
91
|
}
|
|
92
|
+
export function boundCommandOutput(output) {
|
|
93
|
+
if (output.length <= MAX_OUTPUT_CHARS)
|
|
94
|
+
return output;
|
|
95
|
+
const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.2);
|
|
96
|
+
const tailSize = MAX_OUTPUT_CHARS - headSize;
|
|
97
|
+
return (output.slice(0, headSize) +
|
|
98
|
+
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
99
|
+
output.slice(-tailSize));
|
|
100
|
+
}
|
|
101
|
+
async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
|
|
102
|
+
const { rootDir, projectIndex, onStatus } = context;
|
|
103
|
+
const started = await startBackgroundJob(command, rootDir, {
|
|
104
|
+
startupWaitMs: typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : undefined,
|
|
105
|
+
sessionId: context.sessionId,
|
|
106
|
+
});
|
|
107
|
+
if (!started.ok || !started.snapshot) {
|
|
108
|
+
if (!isTuiMode())
|
|
109
|
+
console.log(chalk.red(`\n ✖ ${started.error}`));
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
blocked: true,
|
|
113
|
+
command,
|
|
114
|
+
error: started.error ?? 'Background job failed to start.',
|
|
115
|
+
repoHint,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const snapshot = started.snapshot;
|
|
119
|
+
const repoSync = projectIndex.initialized
|
|
120
|
+
? await syncIndexFromDisk(projectIndex)
|
|
121
|
+
: {
|
|
122
|
+
added: 0,
|
|
123
|
+
modified: 0,
|
|
124
|
+
removed: 0,
|
|
125
|
+
indexedChunks: 0,
|
|
126
|
+
retrievalTokensUsed: 0,
|
|
127
|
+
};
|
|
128
|
+
invalidateShellDiagnosticsCache(rootDir);
|
|
129
|
+
if (repoSync.added || repoSync.modified || repoSync.removed) {
|
|
130
|
+
onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
|
|
131
|
+
}
|
|
132
|
+
const output = boundCommandOutput(redactConnectionStringCredentials(started.startupOutput ?? '').trim());
|
|
133
|
+
if (snapshot.status === 'running') {
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
backgrounded: true,
|
|
137
|
+
command,
|
|
138
|
+
jobId: snapshot.id,
|
|
139
|
+
status: 'running',
|
|
140
|
+
pid: snapshot.pid,
|
|
141
|
+
output,
|
|
142
|
+
note: `Background job ${snapshot.id} is running. Use shell_job_output to poll status and new output, and shell_job_kill to stop it.`,
|
|
143
|
+
repoSync,
|
|
144
|
+
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
145
|
+
repoHint,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ok: snapshot.exitCode === 0,
|
|
150
|
+
backgrounded: true,
|
|
151
|
+
command,
|
|
152
|
+
jobId: snapshot.id,
|
|
153
|
+
status: snapshot.status,
|
|
154
|
+
exitCode: snapshot.exitCode,
|
|
155
|
+
output,
|
|
156
|
+
note: `Background job ${snapshot.id} finished during the startup window.`,
|
|
157
|
+
repoSync,
|
|
158
|
+
retrievalTokensUsed: repoSync.retrievalTokensUsed,
|
|
159
|
+
repoHint,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { killBackgroundJob } from '../background-jobs.js';
|
|
2
|
+
import { redactConnectionStringCredentials } from '../secret-preview.js';
|
|
3
|
+
const MAX_JOB_TOOL_OUTPUT_CHARS = 64 * 1024;
|
|
4
|
+
function boundJobToolOutput(output) {
|
|
5
|
+
if (output.length <= MAX_JOB_TOOL_OUTPUT_CHARS)
|
|
6
|
+
return output;
|
|
7
|
+
const headSize = Math.floor(MAX_JOB_TOOL_OUTPUT_CHARS * 0.2);
|
|
8
|
+
const tailSize = MAX_JOB_TOOL_OUTPUT_CHARS - headSize;
|
|
9
|
+
return (output.slice(0, headSize) +
|
|
10
|
+
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
11
|
+
output.slice(-tailSize));
|
|
12
|
+
}
|
|
13
|
+
export async function shellJobKill(context, args) {
|
|
14
|
+
const jobId = String(args.job_id ?? '').trim();
|
|
15
|
+
if (!jobId) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
error: 'job_id is required',
|
|
19
|
+
failureCategory: 'missing_required_argument',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const result = await killBackgroundJob(jobId, {
|
|
23
|
+
sessionId: context.sessionId,
|
|
24
|
+
});
|
|
25
|
+
if (!result.ok || !result.snapshot) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
error: result.error ?? 'Background job lookup failed.',
|
|
29
|
+
failureCategory: 'not_found',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const snapshot = result.snapshot;
|
|
33
|
+
let finalOutput = redactConnectionStringCredentials(result.finalOutput ?? '');
|
|
34
|
+
if (result.droppedChars) {
|
|
35
|
+
finalOutput = `... (${result.droppedChars} chars of older output dropped) ...\n${finalOutput}`;
|
|
36
|
+
}
|
|
37
|
+
finalOutput = boundJobToolOutput(finalOutput);
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
jobId: snapshot.id,
|
|
41
|
+
command: snapshot.command,
|
|
42
|
+
status: snapshot.status,
|
|
43
|
+
exitCode: snapshot.exitCode,
|
|
44
|
+
alreadyFinished: result.alreadyFinished === true,
|
|
45
|
+
ranMs: (snapshot.endedAt ?? Date.now()) - snapshot.startedAt,
|
|
46
|
+
finalOutput,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MAX_JOB_WAIT_MS, readBackgroundJobOutput, } from '../background-jobs.js';
|
|
2
|
+
import { redactConnectionStringCredentials } from '../secret-preview.js';
|
|
3
|
+
const MAX_JOB_TOOL_OUTPUT_CHARS = 64 * 1024;
|
|
4
|
+
function boundJobToolOutput(output) {
|
|
5
|
+
if (output.length <= MAX_JOB_TOOL_OUTPUT_CHARS)
|
|
6
|
+
return output;
|
|
7
|
+
const headSize = Math.floor(MAX_JOB_TOOL_OUTPUT_CHARS * 0.2);
|
|
8
|
+
const tailSize = MAX_JOB_TOOL_OUTPUT_CHARS - headSize;
|
|
9
|
+
return (output.slice(0, headSize) +
|
|
10
|
+
`\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
|
|
11
|
+
output.slice(-tailSize));
|
|
12
|
+
}
|
|
13
|
+
export async function shellJobOutput(context, args) {
|
|
14
|
+
const jobId = String(args.job_id ?? '').trim();
|
|
15
|
+
if (!jobId) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
error: 'job_id is required',
|
|
19
|
+
failureCategory: 'missing_required_argument',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const waitMs = typeof args.wait_ms === 'number' && args.wait_ms > 0
|
|
23
|
+
? Math.min(args.wait_ms, MAX_JOB_WAIT_MS)
|
|
24
|
+
: 0;
|
|
25
|
+
const result = await readBackgroundJobOutput(jobId, {
|
|
26
|
+
waitMs,
|
|
27
|
+
sessionId: context.sessionId,
|
|
28
|
+
});
|
|
29
|
+
if (!result.ok || !result.snapshot) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
error: result.error ?? 'Background job lookup failed.',
|
|
33
|
+
failureCategory: 'not_found',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const snapshot = result.snapshot;
|
|
37
|
+
let newOutput = redactConnectionStringCredentials(result.newOutput ?? '');
|
|
38
|
+
if (result.droppedChars) {
|
|
39
|
+
newOutput = `... (${result.droppedChars} chars of older output dropped) ...\n${newOutput}`;
|
|
40
|
+
}
|
|
41
|
+
newOutput = boundJobToolOutput(newOutput);
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
jobId: snapshot.id,
|
|
45
|
+
command: snapshot.command,
|
|
46
|
+
status: snapshot.status,
|
|
47
|
+
exitCode: snapshot.exitCode,
|
|
48
|
+
elapsedMs: (snapshot.endedAt ?? Date.now()) - snapshot.startedAt,
|
|
49
|
+
newOutput,
|
|
50
|
+
};
|
|
51
|
+
}
|