praxis-agent 0.20.7 → 0.20.8

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.
@@ -19,11 +19,16 @@ export interface ClaudeQuestionResult {
19
19
  notes?: string;
20
20
  }>>;
21
21
  }
22
+ export interface ClaudeAllowedPrompt {
23
+ tool: 'Bash';
24
+ prompt: string;
25
+ }
22
26
  export interface ClaudePlanApprovalRequest {
23
27
  action: 'exit';
24
28
  planPath: string;
25
29
  plan?: string;
26
30
  previousMode: ClaudePermissionMode;
31
+ allowedPrompts?: readonly ClaudeAllowedPrompt[];
27
32
  }
28
33
  export type ClaudePlanApprovalResult = {
29
34
  behavior: 'allow';
@@ -67,6 +72,7 @@ export declare class ClaudeInteractiveToolManager {
67
72
  private planPath;
68
73
  private resolver;
69
74
  private resolve;
75
+ private applyRetainedPrompts;
70
76
  private resolvePlanFile;
71
77
  private isPlanFileForState;
72
78
  enter(sessionId: string, call: ModelToolCall): Promise<ToolExecutionResult>;
@@ -83,7 +83,7 @@ const EXIT_PLAN_MODE = {
83
83
  type: 'object',
84
84
  properties: {
85
85
  allowedPrompts: {
86
- description: 'Deprecated: no longer used.',
86
+ description: 'Bash commands to pre-approve once the plan is approved. Each entry must declare tool "Bash" and a non-empty prompt string.',
87
87
  type: 'array',
88
88
  items: {
89
89
  type: 'object',
@@ -164,6 +164,24 @@ function questionsFrom(input) {
164
164
  };
165
165
  });
166
166
  }
167
+ function allowedPromptsFrom(input) {
168
+ if (input.allowedPrompts === undefined)
169
+ return [];
170
+ if (!Array.isArray(input.allowedPrompts)) {
171
+ throw new Error('allowedPrompts must be an array');
172
+ }
173
+ return input.allowedPrompts.map((rawEntry, index) => {
174
+ const entry = object(rawEntry, `allowedPrompts[${index}]`);
175
+ strictKeys(entry, ['tool', 'prompt'], `allowedPrompts[${index}]`);
176
+ if (entry.tool !== 'Bash') {
177
+ throw new Error(`allowedPrompts[${index}].tool must be "Bash"`);
178
+ }
179
+ return {
180
+ tool: 'Bash',
181
+ prompt: nonEmptyString(entry.prompt, `allowedPrompts[${index}].prompt`),
182
+ };
183
+ });
184
+ }
167
185
  class ClaudeInteractiveToolRegistry {
168
186
  base;
169
187
  manager;
@@ -318,7 +336,7 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
318
336
  }
319
337
  return resolver;
320
338
  }
321
- resolve(sessionId, call, context) {
339
+ async resolve(sessionId, call, context) {
322
340
  const state = this.state(sessionId);
323
341
  if (this.enabledNames.has(call.name))
324
342
  return { behavior: 'allow' };
@@ -327,7 +345,18 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
327
345
  typeof call.input.file_path === 'string') {
328
346
  return this.resolvePlanFile(state, call, context);
329
347
  }
330
- return this.resolver(this.planPermissionMode(sessionId)).resolve(call, context);
348
+ const decision = await this.resolver(this.planPermissionMode(sessionId)).resolve(call, context);
349
+ return this.applyRetainedPrompts(state, call, decision);
350
+ }
351
+ applyRetainedPrompts(state, call, decision) {
352
+ if (decision.behavior !== 'ask' || call.name !== 'Bash')
353
+ return decision;
354
+ if (typeof call.input.command !== 'string')
355
+ return decision;
356
+ const retained = state.allowedPrompts ?? [];
357
+ return retained.some(({ prompt }) => prompt === call.input.command)
358
+ ? { behavior: 'allow' }
359
+ : decision;
331
360
  }
332
361
  async resolvePlanFile(state, call, context) {
333
362
  const requestedPath = String(call.input.file_path);
@@ -378,6 +407,7 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
378
407
  isError: true,
379
408
  };
380
409
  }
410
+ const allowedPrompts = allowedPromptsFrom(call.input);
381
411
  let plan;
382
412
  try {
383
413
  plan = await readFile(state.planPath, 'utf8');
@@ -398,9 +428,11 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
398
428
  action: 'exit',
399
429
  planPath: state.planPath,
400
430
  previousMode: state.previousMode,
431
+ allowedPrompts,
401
432
  ...(plan === undefined ? {} : { plan }),
402
433
  }, signal);
403
434
  if (approval.behavior === 'deny') {
435
+ delete state.allowedPrompts;
404
436
  return {
405
437
  content: approval.feedback
406
438
  ? `User declined the plan with feedback: ${approval.feedback}\n\nRemain in plan mode and revise it.`
@@ -409,6 +441,7 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
409
441
  };
410
442
  }
411
443
  state.mode = approval.permissionMode;
444
+ state.allowedPrompts = allowedPrompts;
412
445
  this.transitions.set(call.id, state.mode);
413
446
  return {
414
447
  content: plan?.trim()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.20.7",
3
+ "version": "0.20.8",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",