praxis-agent 0.59.0 → 0.60.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/README.md +11 -4
- package/dist/application/background-bash-manager.d.ts +4 -0
- package/dist/application/background-bash-manager.js +95 -3
- package/dist/cli/interactive.js +3 -0
- package/dist/cli/protocol.js +15 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/mcp/claude-mcp-tools.js +22 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,8 +126,11 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
126
126
|
composer grammar, compact stable tool rows, responsive density,
|
|
127
127
|
terminal-native background, and a minimal composer/status row. Successful
|
|
128
128
|
background Bash completion bursts collapse in normal reading, while
|
|
129
|
-
failed/stopped notifications remain detailed.
|
|
130
|
-
|
|
129
|
+
failed/stopped notifications remain detailed.
|
|
130
|
+
Prompt-like background Bash output that remains unchanged for 50 seconds
|
|
131
|
+
raises one warning and model follow-up without stopping or reclassifying the
|
|
132
|
+
running task; silent and ordinary output remain quiet.
|
|
133
|
+
Interactive surfaces share the same presentation across terminals, with English
|
|
131
134
|
permission/configuration choices and a taught `❯` / Up/Down / Enter / Esc
|
|
132
135
|
interaction grammar. While a regular turn is active, the composer remains
|
|
133
136
|
editable: Enter steers at the next safe continuation boundary, Tab or
|
|
@@ -232,8 +235,12 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
232
235
|
imports, memory, skills, commands, agents, hooks, settings, MCP servers,
|
|
233
236
|
plugins, and append-only `praxis.transcript` JSONL sessions under `~/.praxis`,
|
|
234
237
|
with bounded MCP connection, discovery, and tool operations plus safe
|
|
235
|
-
disconnect recovery that never replays an already-dispatched call.
|
|
236
|
-
|
|
238
|
+
disconnect recovery that never replays an already-dispatched call. Stdio MCP
|
|
239
|
+
environment grants expand defined `${NAME}` references at launch without
|
|
240
|
+
exposing unrelated ambient credentials or shell-startup variables; reconnect
|
|
241
|
+
retains the derived grants, while reload derives them again from the current
|
|
242
|
+
environment. Default tool selection defers `mcp__*` schemas behind a
|
|
243
|
+
turn-scoped `ToolSearch`;
|
|
237
244
|
each query activates at most eight deterministic matches for the next model
|
|
238
245
|
request, and published MCP tool descriptions are capped at 2,048 Unicode
|
|
239
246
|
code points. Text-only MCP results above 100,000 UTF-8 bytes are redacted
|
|
@@ -18,6 +18,7 @@ export interface BackgroundBashManagerOptions {
|
|
|
18
18
|
stateRoot: string;
|
|
19
19
|
maxOutputBytes?: number;
|
|
20
20
|
eventSink?: RuntimeEventSink;
|
|
21
|
+
stallWatchdogMs?: number;
|
|
21
22
|
}
|
|
22
23
|
export interface BackgroundBashLaunchInput {
|
|
23
24
|
command: string;
|
|
@@ -40,6 +41,7 @@ export declare class BackgroundBashManager {
|
|
|
40
41
|
private readonly runner;
|
|
41
42
|
private readonly outputRoot;
|
|
42
43
|
private readonly sessionStateRoot;
|
|
44
|
+
private readonly stallWatchdogMs;
|
|
43
45
|
constructor(options: BackgroundBashManagerOptions);
|
|
44
46
|
has(taskId: string): boolean;
|
|
45
47
|
snapshots(): Promise<readonly BackgroundBashSnapshot[]>;
|
|
@@ -56,6 +58,8 @@ export declare class BackgroundBashManager {
|
|
|
56
58
|
stop(taskId: string): Promise<BackgroundBashToolResult>;
|
|
57
59
|
notifications(waitForRunning: boolean): Promise<string[]>;
|
|
58
60
|
private run;
|
|
61
|
+
private clearWatchdog;
|
|
62
|
+
private rearmWatchdog;
|
|
59
63
|
private emitNotification;
|
|
60
64
|
private complete;
|
|
61
65
|
private outputResult;
|
|
@@ -53,14 +53,48 @@ function escapeXml(value) {
|
|
|
53
53
|
.replaceAll('"', '"')
|
|
54
54
|
.replaceAll("'", ''');
|
|
55
55
|
}
|
|
56
|
+
function isPromptTail(output) {
|
|
57
|
+
const tail = output
|
|
58
|
+
.slice(-1024)
|
|
59
|
+
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'gu'), '');
|
|
60
|
+
const lines = tail.split(/\r?\n/u);
|
|
61
|
+
const line = [...lines].reverse().find((item) => item.trim() !== '');
|
|
62
|
+
if (!line)
|
|
63
|
+
return false;
|
|
64
|
+
const value = line.trim().replace(/[\t ]+$/u, '');
|
|
65
|
+
if (/\?\s*(?:\[[^\]]{1,20}\]|\([^)]{1,20}\))$/u.test(value))
|
|
66
|
+
return true;
|
|
67
|
+
if (/^(?:password|passphrase|username)\s*:\s*$/iu.test(value) ||
|
|
68
|
+
/^(?:enter|input|select|selection|choice|confirm)\b[^\n:]{0,100}:\s*$/iu.test(value))
|
|
69
|
+
return true;
|
|
70
|
+
return /^(?:press|hit)\s+(?:enter|return|any key)(?:\s+to\b[^\n]*)?[.!?:…]?$/iu.test(value);
|
|
71
|
+
}
|
|
72
|
+
function watchdogNotification(task, summary) {
|
|
73
|
+
return `<task-notification>\n<task-id>${escapeXml(task.taskId)}</task-id>\n<tool-use-id>${escapeXml(task.toolUseId)}</tool-use-id>\n<output-file>${escapeXml(task.outputFile)}</output-file>\n<summary>${escapeXml(summary)}</summary>\n</task-notification>\nLast output:\n${task.output}\n\nThe command is likely blocked on an interactive prompt. Stop this task and re-run with piped input (e.g., \`echo y | command\`) or a non-interactive flag if one exists.`;
|
|
74
|
+
}
|
|
75
|
+
function createWatchdogWake() {
|
|
76
|
+
let resolveWake;
|
|
77
|
+
const promise = new Promise((resolve) => {
|
|
78
|
+
resolveWake = resolve;
|
|
79
|
+
});
|
|
80
|
+
return { promise, resolve: resolveWake };
|
|
81
|
+
}
|
|
56
82
|
export class BackgroundBashManager {
|
|
57
83
|
options;
|
|
58
84
|
tasks = new Map();
|
|
59
85
|
runner;
|
|
60
86
|
outputRoot;
|
|
61
87
|
sessionStateRoot;
|
|
88
|
+
stallWatchdogMs;
|
|
62
89
|
constructor(options) {
|
|
63
90
|
this.options = options;
|
|
91
|
+
if (options.stallWatchdogMs !== undefined &&
|
|
92
|
+
(!Number.isFinite(options.stallWatchdogMs) ||
|
|
93
|
+
!Number.isInteger(options.stallWatchdogMs) ||
|
|
94
|
+
options.stallWatchdogMs <= 0)) {
|
|
95
|
+
throw new RangeError('stallWatchdogMs must be a positive integer');
|
|
96
|
+
}
|
|
97
|
+
this.stallWatchdogMs = options.stallWatchdogMs ?? 50_000;
|
|
64
98
|
this.runner = new BoundedProcessRunner({
|
|
65
99
|
cwd: options.cwd,
|
|
66
100
|
maxOutputBytes: options.maxOutputBytes ?? 128 * 1024,
|
|
@@ -88,6 +122,7 @@ export class BackgroundBashManager {
|
|
|
88
122
|
const outputFile = resolve(this.outputRoot, `${id}.output`);
|
|
89
123
|
await writeFile(outputFile, '', { mode: 0o600 });
|
|
90
124
|
const controller = new AbortController();
|
|
125
|
+
const watchdogWake = createWatchdogWake();
|
|
91
126
|
const task = {
|
|
92
127
|
taskId: id,
|
|
93
128
|
command: input.command,
|
|
@@ -108,6 +143,11 @@ export class BackgroundBashManager {
|
|
|
108
143
|
parentAbort: () => controller.abort(),
|
|
109
144
|
}
|
|
110
145
|
: {}),
|
|
146
|
+
watchdogTimer: null,
|
|
147
|
+
watchdogFired: false,
|
|
148
|
+
pendingWatchdogMessage: null,
|
|
149
|
+
watchdogWake: watchdogWake.promise,
|
|
150
|
+
watchdogWakeResolve: watchdogWake.resolve,
|
|
111
151
|
};
|
|
112
152
|
if (task.parentSignal && task.parentAbort) {
|
|
113
153
|
if (task.parentSignal.aborted)
|
|
@@ -192,12 +232,25 @@ export class BackgroundBashManager {
|
|
|
192
232
|
async notifications(waitForRunning) {
|
|
193
233
|
await this.hydratePersistedTasks();
|
|
194
234
|
if (waitForRunning) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
235
|
+
const hasPendingWatchdog = [...this.tasks.values()].some(({ pendingWatchdogMessage }) => pendingWatchdogMessage !== null);
|
|
236
|
+
const running = [...this.tasks.values()].filter(({ status }) => status === 'running');
|
|
237
|
+
const completions = Promise.all(running.map(({ completion }) => completion));
|
|
238
|
+
const wakes = running
|
|
239
|
+
.filter((task) => !task.watchdogFired)
|
|
240
|
+
.map(({ watchdogWake }) => watchdogWake);
|
|
241
|
+
if (!hasPendingWatchdog && wakes.length > 0)
|
|
242
|
+
await Promise.race([completions, ...wakes]);
|
|
243
|
+
else if (!hasPendingWatchdog)
|
|
244
|
+
await completions;
|
|
198
245
|
}
|
|
199
246
|
const messages = [];
|
|
200
247
|
for (const task of this.tasks.values()) {
|
|
248
|
+
if (task.pendingWatchdogMessage) {
|
|
249
|
+
const pendingWatchdogMessage = task.pendingWatchdogMessage;
|
|
250
|
+
task.pendingWatchdogMessage = null;
|
|
251
|
+
if (!task.notified && task.status !== 'stopped')
|
|
252
|
+
messages.push(pendingWatchdogMessage);
|
|
253
|
+
}
|
|
201
254
|
if (task.status === 'running' ||
|
|
202
255
|
task.status === 'stopped' ||
|
|
203
256
|
task.notified) {
|
|
@@ -219,6 +272,7 @@ export class BackgroundBashManager {
|
|
|
219
272
|
signal: task.controller.signal,
|
|
220
273
|
onOutput: async (output) => {
|
|
221
274
|
task.output = output;
|
|
275
|
+
this.rearmWatchdog(task);
|
|
222
276
|
await writeFile(task.outputFile, output, { mode: 0o600 });
|
|
223
277
|
},
|
|
224
278
|
});
|
|
@@ -244,12 +298,45 @@ export class BackgroundBashManager {
|
|
|
244
298
|
});
|
|
245
299
|
}
|
|
246
300
|
finally {
|
|
301
|
+
this.clearWatchdog(task);
|
|
247
302
|
this.emitNotification(task);
|
|
248
303
|
if (task.parentSignal && task.parentAbort) {
|
|
249
304
|
task.parentSignal.removeEventListener('abort', task.parentAbort);
|
|
250
305
|
}
|
|
251
306
|
}
|
|
252
307
|
}
|
|
308
|
+
clearWatchdog(task) {
|
|
309
|
+
if (task.watchdogTimer !== null) {
|
|
310
|
+
clearTimeout(task.watchdogTimer);
|
|
311
|
+
task.watchdogTimer = null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
rearmWatchdog(task) {
|
|
315
|
+
this.clearWatchdog(task);
|
|
316
|
+
if (task.status !== 'running' ||
|
|
317
|
+
task.watchdogFired ||
|
|
318
|
+
!isPromptTail(task.output))
|
|
319
|
+
return;
|
|
320
|
+
task.watchdogTimer = setTimeout(() => {
|
|
321
|
+
task.watchdogTimer = null;
|
|
322
|
+
if (task.status !== 'running' ||
|
|
323
|
+
task.watchdogFired ||
|
|
324
|
+
!isPromptTail(task.output))
|
|
325
|
+
return;
|
|
326
|
+
task.watchdogFired = true;
|
|
327
|
+
const summary = `Background command "${task.description}" appears to be waiting for interactive input`;
|
|
328
|
+
task.pendingWatchdogMessage = watchdogNotification(task, summary);
|
|
329
|
+
this.options.eventSink?.({
|
|
330
|
+
type: 'task-input-waiting',
|
|
331
|
+
taskId: task.taskId,
|
|
332
|
+
toolUseId: task.toolUseId,
|
|
333
|
+
outputFile: task.outputFile,
|
|
334
|
+
summary,
|
|
335
|
+
});
|
|
336
|
+
task.watchdogWakeResolve?.();
|
|
337
|
+
task.watchdogWakeResolve = null;
|
|
338
|
+
}, this.stallWatchdogMs);
|
|
339
|
+
}
|
|
253
340
|
emitNotification(task) {
|
|
254
341
|
if (task.status === 'running')
|
|
255
342
|
return;
|
|
@@ -330,6 +417,11 @@ export class BackgroundBashManager {
|
|
|
330
417
|
completion: Promise.resolve(),
|
|
331
418
|
startedAt: state.startedAt ?? Math.floor(metadata.mtimeMs),
|
|
332
419
|
durationMs: state.durationMs ?? null,
|
|
420
|
+
watchdogTimer: null,
|
|
421
|
+
watchdogFired: false,
|
|
422
|
+
pendingWatchdogMessage: null,
|
|
423
|
+
watchdogWake: createWatchdogWake().promise,
|
|
424
|
+
watchdogWakeResolve: null,
|
|
333
425
|
};
|
|
334
426
|
this.tasks.set(taskId, task);
|
|
335
427
|
return task;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -1968,6 +1968,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1968
1968
|
},
|
|
1969
1969
|
});
|
|
1970
1970
|
break;
|
|
1971
|
+
case 'task-input-waiting':
|
|
1972
|
+
append({ kind: 'warning', text: event.summary });
|
|
1973
|
+
break;
|
|
1971
1974
|
case 'compact-boundary':
|
|
1972
1975
|
append({
|
|
1973
1976
|
kind: 'notice',
|
package/dist/cli/protocol.js
CHANGED
|
@@ -2206,6 +2206,21 @@ export class StreamJsonOutput {
|
|
|
2206
2206
|
});
|
|
2207
2207
|
return;
|
|
2208
2208
|
}
|
|
2209
|
+
if (event.type === 'task-input-waiting') {
|
|
2210
|
+
this.write({
|
|
2211
|
+
type: 'system',
|
|
2212
|
+
subtype: 'task_notification',
|
|
2213
|
+
task_id: event.taskId,
|
|
2214
|
+
...(event.toolUseId === undefined
|
|
2215
|
+
? {}
|
|
2216
|
+
: { tool_use_id: event.toolUseId }),
|
|
2217
|
+
output_file: event.outputFile,
|
|
2218
|
+
summary: event.summary,
|
|
2219
|
+
uuid: randomUUID(),
|
|
2220
|
+
session_id: this.sessionId,
|
|
2221
|
+
});
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2209
2224
|
if (event.type === 'session-state-changed') {
|
|
2210
2225
|
this.writeSessionState(event.state);
|
|
2211
2226
|
return;
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -275,6 +275,12 @@ export type RuntimeEvent = {
|
|
|
275
275
|
toolUses: number;
|
|
276
276
|
durationMs: number;
|
|
277
277
|
};
|
|
278
|
+
} | {
|
|
279
|
+
type: 'task-input-waiting';
|
|
280
|
+
taskId: string;
|
|
281
|
+
toolUseId?: string;
|
|
282
|
+
outputFile: string;
|
|
283
|
+
summary: string;
|
|
278
284
|
} | {
|
|
279
285
|
type: 'session-state-changed';
|
|
280
286
|
state: 'idle' | 'running' | 'requires_action';
|
|
@@ -257,12 +257,12 @@ export function validateClaudeMcpConfiguration(resources) {
|
|
|
257
257
|
});
|
|
258
258
|
return { servers, warnings };
|
|
259
259
|
}
|
|
260
|
-
async function transport(serverName, config, cwd, configRoot) {
|
|
260
|
+
async function transport(serverName, config, cwd, configRoot, environment = process.env) {
|
|
261
261
|
if (config.type === 'stdio') {
|
|
262
262
|
return new StdioClientTransport({
|
|
263
263
|
command: config.command,
|
|
264
264
|
args: config.args,
|
|
265
|
-
env: sanitizeChildEnvironment(config.env),
|
|
265
|
+
env: sanitizeChildEnvironment(config.env, environment),
|
|
266
266
|
cwd: config.cwd ?? cwd,
|
|
267
267
|
stderr: 'pipe',
|
|
268
268
|
});
|
|
@@ -280,6 +280,21 @@ async function transport(serverName, config, cwd, configRoot) {
|
|
|
280
280
|
...(authProvider ? { authProvider } : {}),
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
|
+
function deriveMcpServerConfig(config, environment) {
|
|
284
|
+
if (config.type !== 'stdio')
|
|
285
|
+
return config;
|
|
286
|
+
const env = Object.fromEntries(Object.entries(config.env).map(([name, value]) => [
|
|
287
|
+
name,
|
|
288
|
+
value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (match, variable) => {
|
|
289
|
+
const replacement = environment[variable];
|
|
290
|
+
return Object.prototype.hasOwnProperty.call(environment, variable) &&
|
|
291
|
+
typeof replacement === 'string'
|
|
292
|
+
? replacement
|
|
293
|
+
: match;
|
|
294
|
+
}),
|
|
295
|
+
]));
|
|
296
|
+
return { ...config, env };
|
|
297
|
+
}
|
|
283
298
|
function toolContent(result, sensitiveValues) {
|
|
284
299
|
if (!Array.isArray(result.content)) {
|
|
285
300
|
throw new Error('MCP tool result content must be an array');
|
|
@@ -883,7 +898,6 @@ export class ClaudeMcpToolRegistry {
|
|
|
883
898
|
warn(`MCP server ${server.name} unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
884
899
|
continue;
|
|
885
900
|
}
|
|
886
|
-
this.serverSensitiveValues.set(server.name, configSensitiveValues(config, server.sensitiveValues ?? [], this.options.environment ?? process.env));
|
|
887
901
|
await this.connectServer(server.name, config, server.sensitiveValues);
|
|
888
902
|
}
|
|
889
903
|
this.publishPrompts();
|
|
@@ -1091,7 +1105,10 @@ export class ClaudeMcpToolRegistry {
|
|
|
1091
1105
|
async connectServer(serverName, config, additionalSensitiveValues = [], expectedGeneration = this.generation) {
|
|
1092
1106
|
this.assertOpenGeneration(expectedGeneration);
|
|
1093
1107
|
this.statuses.set(serverName, { name: serverName, status: 'failed' });
|
|
1094
|
-
const
|
|
1108
|
+
const environment = this.options.environment ?? process.env;
|
|
1109
|
+
const launchConfig = deriveMcpServerConfig(config, environment);
|
|
1110
|
+
const sensitiveValues = configSensitiveValues(launchConfig, additionalSensitiveValues, environment);
|
|
1111
|
+
this.serverSensitiveValues.set(serverName, sensitiveValues);
|
|
1095
1112
|
const session = new McpServerSession({
|
|
1096
1113
|
serverName,
|
|
1097
1114
|
connectionTimeoutMs: this.timeouts.connectionTimeoutMs,
|
|
@@ -1099,7 +1116,7 @@ export class ClaudeMcpToolRegistry {
|
|
|
1099
1116
|
...(this.options.signal ? { lifetimeSignal: this.options.signal } : {}),
|
|
1100
1117
|
createTransport: async () => {
|
|
1101
1118
|
const configRoot = this.options.configRoot ?? join(homedir(), '.praxis');
|
|
1102
|
-
return (await transport(serverName,
|
|
1119
|
+
return (await transport(serverName, launchConfig, this.options.cwd, configRoot, environment));
|
|
1103
1120
|
},
|
|
1104
1121
|
configureClient: (client) => {
|
|
1105
1122
|
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|