praxis-agent 0.11.0 → 0.13.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.
Files changed (31) hide show
  1. package/README.md +13 -2
  2. package/dist/application/subagent-service.d.ts +1 -1
  3. package/dist/application/subagent-service.js +33 -2
  4. package/dist/cli/interactive.d.ts +6 -1
  5. package/dist/cli/interactive.js +197 -5
  6. package/dist/cli/tui/claude-command-inventory.d.ts +35 -19
  7. package/dist/cli/tui/claude-command-inventory.js +98 -18
  8. package/dist/cli/tui/release-notes.d.ts +10 -0
  9. package/dist/cli/tui/release-notes.js +60 -0
  10. package/dist/cli/tui/sandbox-dashboard.d.ts +10 -0
  11. package/dist/cli/tui/sandbox-dashboard.js +78 -0
  12. package/dist/cli/tui/sandbox-settings.d.ts +32 -0
  13. package/dist/cli/tui/sandbox-settings.js +146 -0
  14. package/dist/cli/tui/slash-commands.js +10 -0
  15. package/dist/cli/tui/status-line.d.ts +88 -0
  16. package/dist/cli/tui/status-line.js +234 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +73 -3
  19. package/dist/extensions/claude-extensions.d.ts +1 -0
  20. package/dist/extensions/claude-extensions.js +55 -3
  21. package/dist/permissions/bash-path-safety.d.ts +2 -0
  22. package/dist/permissions/bash-path-safety.js +7 -0
  23. package/dist/permissions/claude-permission-resolver.d.ts +11 -0
  24. package/dist/permissions/claude-permission-resolver.js +15 -0
  25. package/dist/sandbox/claude-sandbox-runtime.d.ts +54 -0
  26. package/dist/sandbox/claude-sandbox-runtime.js +175 -0
  27. package/dist/sandbox/claude-sandbox-settings.d.ts +23 -0
  28. package/dist/sandbox/claude-sandbox-settings.js +286 -0
  29. package/dist/tools/local-tools.d.ts +18 -0
  30. package/dist/tools/local-tools.js +47 -8
  31. package/package.json +2 -1
@@ -0,0 +1,234 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { spawn } from 'node:child_process';
3
+ import { watchFile, unwatchFile } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { Box, Text } from 'ink';
6
+ import { useCallback, useEffect, useRef, useState } from 'react';
7
+ import { isClaudeSessionId, resolveClaudePaths, sanitizeClaudeProjectPath, } from '../../compatibility/claude/paths.js';
8
+ import { loadClaudeSettings } from '../../compatibility/claude/shared-resources.js';
9
+ import { commandShell, commandShellArguments, } from '../../platform/command-shell.js';
10
+ function record(value) {
11
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
12
+ ? value
13
+ : null;
14
+ }
15
+ export async function loadClaudeStatusLineSetting(options) {
16
+ const resources = await loadClaudeSettings(options);
17
+ const merged = Object.assign({}, ...resources.map((resource) => record(resource.value) ?? {}));
18
+ const value = record(merged.statusLine);
19
+ const setting = value?.type === 'command' && typeof value.command === 'string'
20
+ ? {
21
+ type: 'command',
22
+ command: value.command,
23
+ ...(typeof value.padding === 'number' &&
24
+ Number.isFinite(value.padding)
25
+ ? { padding: value.padding }
26
+ : {}),
27
+ }
28
+ : undefined;
29
+ return {
30
+ ...(setting ? { setting } : {}),
31
+ disabled: merged.disableAllHooks === true,
32
+ };
33
+ }
34
+ export async function executeClaudeStatusLine(setting, input, options = {
35
+ cwd: process.cwd(),
36
+ }) {
37
+ const timeout = AbortSignal.timeout(options.timeoutMs ?? 5000);
38
+ const signal = options.signal
39
+ ? AbortSignal.any([options.signal, timeout])
40
+ : timeout;
41
+ return new Promise((resolve) => {
42
+ let settled = false;
43
+ let stdout = '';
44
+ const finish = (value) => {
45
+ if (settled)
46
+ return;
47
+ settled = true;
48
+ resolve(value);
49
+ };
50
+ let child;
51
+ try {
52
+ child = spawn(commandShell(), commandShellArguments(setting.command), {
53
+ cwd: options.cwd,
54
+ detached: process.platform !== 'win32',
55
+ env: process.env,
56
+ stdio: ['pipe', 'pipe', 'ignore'],
57
+ });
58
+ }
59
+ catch {
60
+ finish();
61
+ return;
62
+ }
63
+ child.stdout.setEncoding('utf8');
64
+ const terminate = () => {
65
+ if (child.pid === undefined)
66
+ return;
67
+ try {
68
+ if (process.platform === 'win32')
69
+ child.kill('SIGKILL');
70
+ else
71
+ process.kill(-child.pid, 'SIGKILL');
72
+ }
73
+ catch (error) {
74
+ if (error.code !== 'ESRCH')
75
+ finish();
76
+ }
77
+ };
78
+ if (signal.aborted)
79
+ terminate();
80
+ else
81
+ signal.addEventListener('abort', terminate, { once: true });
82
+ child.stdout.on('data', (chunk) => {
83
+ if (stdout.length <= 1024 * 1024)
84
+ stdout += chunk;
85
+ });
86
+ child.on('error', () => {
87
+ signal.removeEventListener('abort', terminate);
88
+ finish();
89
+ });
90
+ child.on('close', (code) => {
91
+ signal.removeEventListener('abort', terminate);
92
+ if (code !== 0 || signal.aborted) {
93
+ finish();
94
+ return;
95
+ }
96
+ const output = stdout
97
+ .trim()
98
+ .split(/\r?\n/u)
99
+ .map((line) => line.trim())
100
+ .filter(Boolean)
101
+ .join('\n');
102
+ finish(output || undefined);
103
+ });
104
+ child.stdin.on('error', () => undefined);
105
+ child.stdin.end(JSON.stringify(input));
106
+ });
107
+ }
108
+ export function createClaudeStatusLineInput(options) {
109
+ const usage = options.usage;
110
+ const contextInputTokens = usage
111
+ ? usage.inputTokens +
112
+ (usage.cacheReadInputTokens ?? 0) +
113
+ (usage.cacheCreationInputTokens ?? 0)
114
+ : null;
115
+ const usedTokens = contextInputTokens === null || usage === undefined
116
+ ? null
117
+ : contextInputTokens + usage.outputTokens;
118
+ const contextWindowSize = options.contextWindowTokens ?? 0;
119
+ const usedPercentage = contextInputTokens === null || contextWindowSize <= 0
120
+ ? null
121
+ : Math.min(100, Math.max(0, Math.round((contextInputTokens / contextWindowSize) * 100)));
122
+ const model = options.model ?? 'default';
123
+ const transcriptPath = isClaudeSessionId(options.sessionId)
124
+ ? resolveClaudePaths({
125
+ configDir: options.configRoot,
126
+ cwd: options.cwd,
127
+ sessionId: options.sessionId,
128
+ }).sessionFile
129
+ : resolve(options.configRoot, 'projects', sanitizeClaudeProjectPath(options.cwd), `${options.sessionId}.jsonl`);
130
+ return {
131
+ session_id: options.sessionId,
132
+ ...(options.sessionName ? { session_name: options.sessionName } : {}),
133
+ transcript_path: transcriptPath,
134
+ cwd: options.cwd,
135
+ ...(options.permissionMode
136
+ ? { permission_mode: options.permissionMode }
137
+ : {}),
138
+ model: { id: model, display_name: model },
139
+ workspace: {
140
+ current_dir: options.cwd,
141
+ project_dir: options.projectDir,
142
+ added_dirs: options.additionalDirectories,
143
+ },
144
+ version: options.version,
145
+ output_style: { name: options.outputStyle },
146
+ cost: {
147
+ total_cost_usd: options.costUsd ?? 0,
148
+ total_duration_ms: 0,
149
+ total_api_duration_ms: 0,
150
+ total_lines_added: 0,
151
+ total_lines_removed: 0,
152
+ },
153
+ context_window: {
154
+ total_input_tokens: usage?.inputTokens ?? 0,
155
+ total_output_tokens: usage?.outputTokens ?? 0,
156
+ context_window_size: contextWindowSize,
157
+ current_usage: usage
158
+ ? {
159
+ input_tokens: usage.inputTokens,
160
+ output_tokens: usage.outputTokens,
161
+ cache_creation_input_tokens: usage.cacheCreationInputTokens ?? 0,
162
+ cache_read_input_tokens: usage.cacheReadInputTokens ?? 0,
163
+ }
164
+ : null,
165
+ used_percentage: usedPercentage,
166
+ remaining_percentage: usedPercentage === null ? null : 100 - usedPercentage,
167
+ },
168
+ exceeds_200k_tokens: usedTokens !== null && usedTokens > 200_000,
169
+ ...(options.vimMode ? { vim: { mode: options.vimMode } } : {}),
170
+ };
171
+ }
172
+ export function StatusLine({ configRoot, cwd, input, refreshKey, settingSources, }) {
173
+ const [text, setText] = useState();
174
+ const [padding, setPadding] = useState(0);
175
+ const timer = useRef(undefined);
176
+ const controller = useRef(undefined);
177
+ const latestInput = useRef(input);
178
+ latestInput.current = input;
179
+ const update = useCallback(async () => {
180
+ controller.current?.abort();
181
+ const current = new AbortController();
182
+ controller.current = current;
183
+ try {
184
+ const loaded = await loadClaudeStatusLineSetting({
185
+ configRoot,
186
+ cwd,
187
+ ...(settingSources === undefined ? {} : { settingSources }),
188
+ });
189
+ if (current.signal.aborted)
190
+ return;
191
+ setPadding(loaded.setting?.padding ?? 0);
192
+ if (loaded.disabled || !loaded.setting) {
193
+ setText(undefined);
194
+ return;
195
+ }
196
+ const result = await executeClaudeStatusLine(loaded.setting, latestInput.current, { cwd, signal: current.signal });
197
+ if (!current.signal.aborted)
198
+ setText(result);
199
+ }
200
+ catch {
201
+ if (!current.signal.aborted)
202
+ setText(undefined);
203
+ }
204
+ }, [configRoot, cwd, settingSources]);
205
+ const schedule = useCallback(() => {
206
+ if (timer.current)
207
+ clearTimeout(timer.current);
208
+ timer.current = setTimeout(() => void update(), 300);
209
+ }, [update]);
210
+ useEffect(() => {
211
+ schedule();
212
+ }, [refreshKey, schedule]);
213
+ useEffect(() => {
214
+ const paths = [
215
+ join(configRoot, 'settings.json'),
216
+ join(cwd, '.claude', 'settings.json'),
217
+ join(cwd, '.claude', 'settings.local.json'),
218
+ ];
219
+ const changed = () => schedule();
220
+ for (const path of paths)
221
+ watchFile(path, { interval: 500 }, changed);
222
+ return () => {
223
+ for (const path of paths)
224
+ unwatchFile(path, changed);
225
+ controller.current?.abort();
226
+ if (timer.current)
227
+ clearTimeout(timer.current);
228
+ };
229
+ }, [configRoot, cwd, schedule]);
230
+ if (!text)
231
+ return null;
232
+ return (_jsx(Box, { paddingX: padding, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: text }) }));
233
+ }
234
+ //# sourceMappingURL=status-line.js.map
package/dist/cli.d.ts CHANGED
@@ -96,6 +96,7 @@ interface TopLevelAgentCommands {
96
96
  attach(id: string, input: AsyncIterable<string | Uint8Array>, output: (text: string) => void, signal?: AbortSignal): Promise<void>;
97
97
  }
98
98
  export interface CliDependencies extends InteractiveServiceFactory {
99
+ loadReleaseNotes?(configRoot: string): Promise<string>;
99
100
  createService(options: {
100
101
  eventSink: RuntimeEventSink;
101
102
  requireProvider: boolean;
@@ -114,6 +115,7 @@ export interface CliDependencies extends InteractiveServiceFactory {
114
115
  exposeToolRegistry?: boolean;
115
116
  emitToolUseSummaries?: boolean;
116
117
  cwd?: string;
118
+ sandboxOriginalCwd?: string;
117
119
  configRoot?: string;
118
120
  environment?: Readonly<Record<string, string>>;
119
121
  providerEnvironment?: NodeJS.ProcessEnv;
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { isClaudeSessionId, resolveClaudePaths, } from './compatibility/claude/p
14
14
  import { loadClaudeContextResources, loadClaudeSettings, loadClaudeSharedResources, resolveClaudeProjectMemoryDirectory, } from './compatibility/claude/shared-resources.js';
15
15
  import { AgentRunCancelledError, } from './core/runtime.js';
16
16
  import { persistTuiPermissionUpdates } from './cli/tui/permission-settings.js';
17
+ import { loadClaudeReleaseNotes } from './cli/tui/release-notes.js';
17
18
  import { projectTuiHooks, } from './cli/tui/hook-settings.js';
18
19
  import { DEFAULT_CLI_CONTROLS, resolveCliControls } from './cli/controls.js';
19
20
  import { applyRuntimeSettingDefaults, loadRuntimeSettings, runtimeSettingsSystemPrompt, } from './cli/tui/runtime-settings.js';
@@ -43,6 +44,8 @@ import { FilteredToolRegistry } from './tools/filtered-tool-registry.js';
43
44
  import { WebToolRegistry } from './tools/web.js';
44
45
  import { WorkspaceContext } from './application/session-worktree.js';
45
46
  import { launchTmuxWorktree } from './platform/tmux-worktree.js';
47
+ import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
48
+ import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
46
49
  import { createErrorResult, createSuccessResult, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
47
50
  import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
48
51
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
@@ -688,8 +691,9 @@ const consoleIO = {
688
691
  isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
689
692
  readStdinLines: () => process.stdin,
690
693
  };
691
- const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
694
+ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
692
695
  const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
696
+ const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
693
697
  const claudeVersion = await detectInstalledClaudeVersion();
694
698
  const cwd = requestedCwd ?? process.cwd();
695
699
  const workspace = new WorkspaceContext(cwd);
@@ -948,9 +952,26 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
948
952
  ...cli.additionalDirectories,
949
953
  ...(exposePlanDirectory ? [resolve(configRoot, 'plans')] : []),
950
954
  ];
955
+ const sandboxSettings = loadClaudeSandboxSettings({
956
+ resources: settings.filter((resource) => resource.plugin !== true),
957
+ cwd: workspace.cwd(),
958
+ originalCwd: sandboxOriginalCwd ?? workspace.cwd(),
959
+ configRoot,
960
+ homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
961
+ additionalDirectories: initialAdditionalDirectories,
962
+ tempDirectory: claudeSandboxTempDirectory(sandboxEnvironment),
963
+ });
964
+ await claudeSandboxRuntime.initialize(sandboxSettings);
965
+ const sandboxUnavailableReason = claudeSandboxRuntime.unavailableReason();
966
+ if (sandboxUnavailableReason && !sandboxSettings.failIfUnavailable) {
967
+ runtimeEventSink({ type: 'warning', message: sandboxUnavailableReason });
968
+ }
951
969
  const permissionAdditionalDirectories = [
952
- ...initialAdditionalDirectories,
953
- ...(memoryDirectory ? [memoryDirectory] : []),
970
+ ...new Set([
971
+ ...initialAdditionalDirectories,
972
+ ...controls.addDirectories.map((directory) => resolve(cwd, directory)),
973
+ ...(memoryDirectory ? [memoryDirectory] : []),
974
+ ]),
954
975
  ];
955
976
  const initialAdditionalReadDirectories = [claudeBackgroundTaskParent(cwd)];
956
977
  const permissionResolverForMode = (permissionMode) => new ClaudeExtensionPermissionResolver(new ClaudePermissionResolver({
@@ -962,6 +983,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
962
983
  disallowedTools: cli.disallowedTools,
963
984
  additionalDirectories: permissionAdditionalDirectories,
964
985
  additionalReadDirectories: initialAdditionalReadDirectories,
986
+ sandbox: claudeSandboxRuntime,
965
987
  permissionMode,
966
988
  ...(isSessionActionApproved ? { isSessionActionApproved } : {}),
967
989
  ...(permissionMode === 'auto'
@@ -978,6 +1000,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
978
1000
  ...(memoryDirectory ? { sharedMemoryDirectory: memoryDirectory } : {}),
979
1001
  additionalDirectories: initialAdditionalDirectories,
980
1002
  additionalReadDirectories: initialAdditionalReadDirectories,
1003
+ sandbox: claudeSandboxRuntime,
981
1004
  ...(environment ? { environment } : {}),
982
1005
  });
983
1006
  const runtimeMcpResources = async (candidates) => {
@@ -1463,6 +1486,7 @@ const defaultPluginEvalRuntimeFactory = {
1463
1486
  USERPROFILE: options.home,
1464
1487
  },
1465
1488
  providerEnvironment: process.env,
1489
+ isSessionActionApproved: (call) => options.allowedTools.includes(call.name),
1466
1490
  controls: {
1467
1491
  ...DEFAULT_CLI_CONTROLS,
1468
1492
  sessionPersistence: false,
@@ -1568,6 +1592,7 @@ const defaultDependencies = {
1568
1592
  createService: ({ additionalDirectories, cwd, ...options }) => createDefaultService({
1569
1593
  ...options,
1570
1594
  ...(cwd === undefined ? {} : { cwd }),
1595
+ sandboxOriginalCwd: process.cwd(),
1571
1596
  ...(agent === undefined ? {} : { agent }),
1572
1597
  controls: {
1573
1598
  ...interactiveControls,
@@ -1585,6 +1610,9 @@ const defaultDependencies = {
1585
1610
  ? { allowDangerouslySkipPermissions: true }
1586
1611
  : {}),
1587
1612
  additionalDirectories: initialAdditionalDirectories,
1613
+ ...(interactiveControls.settingSources === undefined
1614
+ ? {}
1615
+ : { settingSources: interactiveControls.settingSources }),
1588
1616
  display: {
1589
1617
  version: VERSION,
1590
1618
  cwd: process.cwd(),
@@ -3832,6 +3860,48 @@ async function execute(argv, io, dependencies, signal) {
3832
3860
  : {}),
3833
3861
  };
3834
3862
  };
3863
+ const headlessPromptArgs = command === 'resume' ? args.slice(2) : knownCommand ? args.slice(1) : args;
3864
+ const headlessPrompt = inputFormat === 'text' && headlessPromptArgs.length > 0
3865
+ ? promptFrom(headlessPromptArgs)
3866
+ : undefined;
3867
+ if (headlessPrompt === '/release-notes' && !invocation.disableSlashCommands) {
3868
+ const startedAt = Date.now();
3869
+ const sessionId = invocation.sessionId ?? randomUUID();
3870
+ const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'));
3871
+ const text = dependencies.loadReleaseNotes
3872
+ ? await dependencies.loadReleaseNotes(configRoot)
3873
+ : await loadClaudeReleaseNotes({ configRoot });
3874
+ const result = {
3875
+ sessionId,
3876
+ text,
3877
+ usage: { inputTokens: 0, outputTokens: 0 },
3878
+ };
3879
+ const info = {
3880
+ cwd: process.cwd(),
3881
+ model: invocation.model ?? process.env.PRAXIS_MODEL ?? 'unknown',
3882
+ tools: [],
3883
+ mcpServers: [],
3884
+ permissionMode: invocation.permissionMode,
3885
+ slashCommands: ['release-notes'],
3886
+ agents: [],
3887
+ skills: [],
3888
+ claudeCodeVersion: '2.1.208',
3889
+ };
3890
+ if (outputFormat === 'stream-json') {
3891
+ const output = new StreamJsonOutput((value) => writeJson(io, value), info, sessionId, includePartialMessages, invocation.includeHookEvents);
3892
+ output.init();
3893
+ output.sink({ type: 'text-delta', delta: text });
3894
+ output.sink({ type: 'usage', usage: result.usage });
3895
+ output.result(result, startedAt);
3896
+ }
3897
+ else if (outputFormat === 'json' || invocation.legacyJson) {
3898
+ writeJson(io, createSuccessResult(result, info, startedAt, 0));
3899
+ }
3900
+ else {
3901
+ io.stdout(`${text}\n`);
3902
+ }
3903
+ return 0;
3904
+ }
3835
3905
  const service = await dependencies.createService({
3836
3906
  eventSink: outputFormat === 'stream-json' && !invocation.legacyJson
3837
3907
  ? (event) => {
@@ -28,6 +28,7 @@ export interface ClaudePromptExpansionMessage {
28
28
  contentBlocks?: readonly ModelContentBlock[];
29
29
  images?: readonly ModelImage[];
30
30
  }
31
+ export declare const BUILTIN_STATUSLINE_AGENT_PATH = "/__praxis_builtin__/agents/statusline-setup.md";
31
32
  export declare function validateClaudeExtensions(resources: readonly ClaudeTextResource[]): void;
32
33
  export declare class ClaudeExtensionCatalog {
33
34
  private readonly commands;
@@ -34,6 +34,49 @@ Call CronCreate with the derived cron, the parsed prompt verbatim, and recurring
34
34
  Input:
35
35
  $ARGUMENTS`,
36
36
  };
37
+ const BUILTIN_STATUSLINE_COMMAND = {
38
+ path: '/__praxis_builtin__/commands/statusline.md',
39
+ scope: 'user',
40
+ content: '',
41
+ kind: 'command',
42
+ name: 'statusline',
43
+ description: "Set up Claude Code's status line UI",
44
+ modelInvocable: true,
45
+ permissionSafe: true,
46
+ body: `Create an Agent with subagent_type "statusline-setup" and the prompt "$ARGUMENTS"`,
47
+ };
48
+ export const BUILTIN_STATUSLINE_AGENT_PATH = '/__praxis_builtin__/agents/statusline-setup.md';
49
+ const BUILTIN_STATUSLINE_AGENT = {
50
+ path: BUILTIN_STATUSLINE_AGENT_PATH,
51
+ scope: 'user',
52
+ content: '',
53
+ kind: 'agent',
54
+ name: 'statusline-setup',
55
+ description: "Configure the user's Claude Code status line setting.",
56
+ modelInvocable: false,
57
+ permissionSafe: false,
58
+ body: `You configure the statusLine command in the user's shared Claude Code settings.
59
+
60
+ Importing a shell prompt:
61
+ 1. Inspect shell configuration in this order: ~/.zshrc, ~/.bashrc, ~/.bash_profile, then ~/.profile.
62
+ 2. Find an exported or unexported quoted PS1 assignment. Preserve the prompt's information and colors.
63
+ 3. Translate prompt escapes into commands: user to $(whoami), short/full host to $(hostname -s)/$(hostname), working directory to $(pwd), basename to $(basename "$(pwd)"), and time/date escapes to the corresponding date command. Preserve newlines and literal prompt counters.
64
+ 4. Emit ANSI sequences with printf because the result is rendered by a terminal in dimmed colors.
65
+ 5. Remove a trailing prompt marker such as "$" or ">" from the displayed status line.
66
+ 6. If no PS1 exists and the request gives no other design, ask what information should be displayed.
67
+
68
+ The status command receives one JSON document on stdin. It contains session_id, optional session_name, transcript_path, cwd, model.id and model.display_name, workspace.current_dir/project_dir/added_dirs, version, output_style.name, cost totals, context_window totals/current_usage/used_percentage/remaining_percentage, exceeds_200k_tokens, and optional vim, agent, and worktree objects. Read stdin once and reuse it when extracting multiple values. Typical jq selectors include .model.display_name, .workspace.current_dir, .output_style.name, .context_window.remaining_percentage, and .context_window.used_percentage. Optional values must be tested before printing them.
69
+
70
+ Preserve every unrelated setting and update ~/.claude/settings.json with this shape:
71
+ {
72
+ "statusLine": {
73
+ "type": "command",
74
+ "command": "the command"
75
+ }
76
+ }
77
+
78
+ Resolve a symlinked settings file and edit its target. Complex implementations may live in a script under ~/.claude and be referenced by the setting. Git commands in that script must avoid optional locks. Report the exact configuration and script path, if any. End by telling the parent agent that future status-line changes must use the statusline-setup agent and that the user can request further changes.`,
79
+ };
37
80
  const GENERAL_PURPOSE_AGENT = {
38
81
  name: 'general-purpose',
39
82
  description: 'General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks.',
@@ -175,7 +218,10 @@ export class ClaudeExtensionCatalog {
175
218
  this.disableSlashCommands = options.disableSlashCommands === true;
176
219
  this.commands = options.disableSlashCommands
177
220
  ? new Map()
178
- : new Map([['loop', BUILTIN_LOOP]]);
221
+ : new Map([
222
+ ['loop', BUILTIN_LOOP],
223
+ ['statusline', BUILTIN_STATUSLINE_COMMAND],
224
+ ]);
179
225
  if (!options.disableSlashCommands) {
180
226
  for (const [name, command] of indexed('command', resources.commands)) {
181
227
  this.commands.set(name, command);
@@ -184,7 +230,10 @@ export class ClaudeExtensionCatalog {
184
230
  this.skills = options.disableSlashCommands
185
231
  ? new Map()
186
232
  : indexed('skill', resources.skills);
187
- this.agents = indexed('agent', resources.agents);
233
+ this.agents = new Map([
234
+ ['statusline-setup', BUILTIN_STATUSLINE_AGENT],
235
+ ...indexed('agent', resources.agents),
236
+ ]);
188
237
  }
189
238
  setMcpPrompts(prompts) {
190
239
  this.mcpPrompts.clear();
@@ -208,6 +257,9 @@ export class ClaudeExtensionCatalog {
208
257
  if (!definition) {
209
258
  return { userMessages: [prompt] };
210
259
  }
260
+ const invocationArguments = definition === BUILTIN_STATUSLINE_COMMAND && argumentsText.length === 0
261
+ ? 'Configure my statusLine from my shell PS1 configuration'
262
+ : argumentsText;
211
263
  return {
212
264
  userMessages: [
213
265
  [
@@ -217,7 +269,7 @@ export class ClaudeExtensionCatalog {
217
269
  ? [`<command-args>${argumentsText}</command-args>`]
218
270
  : []),
219
271
  ].join('\n'),
220
- renderInvocation(definition, argumentsText),
272
+ renderInvocation(definition, invocationArguments),
221
273
  ],
222
274
  };
223
275
  }
@@ -1,3 +1,4 @@
1
+ import type { FsWriteRestrictionConfig } from '@anthropic-ai/sandbox-runtime';
1
2
  import type { PermissionUpdate } from '../core/runtime.js';
2
3
  type FileOperation = 'read' | 'write' | 'create';
3
4
  type RuleOutcome = 'allow' | 'deny' | null;
@@ -8,6 +9,7 @@ export interface BashPathSafetyOptions {
8
9
  writeRoots: readonly string[];
9
10
  internalEditableRoots?: readonly string[];
10
11
  internalReadableRoots?: readonly string[];
12
+ sandboxWriteConfig?: FsWriteRestrictionConfig;
11
13
  permissionMode: 'acceptEdits' | 'auto' | 'bypassPermissions' | 'manual' | 'dontAsk' | 'plan' | 'default';
12
14
  fileRule?: (operation: FileOperation, absolutePath: string) => RuleOutcome;
13
15
  platform?: NodeJS.Platform;
@@ -477,6 +477,13 @@ function pathFailure(rawPath, operation, options, dangerous = false) {
477
477
  pathIsInsideRoots(absolutePath, options.internalReadableRoots ?? [])) {
478
478
  return { safe: true };
479
479
  }
480
+ if (operation !== 'read' &&
481
+ !inRoot &&
482
+ options.sandboxWriteConfig &&
483
+ !pathIsInsideRoots(absolutePath, options.sandboxWriteConfig.denyWithinAllow) &&
484
+ pathIsInsideRoots(absolutePath, options.sandboxWriteConfig.allowOnly)) {
485
+ return { safe: true };
486
+ }
480
487
  if (rules.length > 0 && rules.every((rule) => rule === 'allow')) {
481
488
  return { safe: true };
482
489
  }
@@ -1,3 +1,4 @@
1
+ import type { FsWriteRestrictionConfig } from '@anthropic-ai/sandbox-runtime';
1
2
  import type { ClaudeJsonResource } from '../compatibility/claude/shared-resources.js';
2
3
  import type { ModelToolCall, PermissionDecision, PermissionResolutionContext, PermissionResolver } from '../core/runtime.js';
3
4
  import { type ClaudeAutoClassifier, type ClaudeAutoModeConfig } from './claude-auto-classifier.js';
@@ -15,6 +16,15 @@ export interface ClaudePermissionResolverOptions {
15
16
  isSessionActionApproved?: (call: ModelToolCall) => boolean;
16
17
  additionalDirectories?: readonly string[];
17
18
  additionalReadDirectories?: readonly string[];
19
+ sandbox?: ClaudePermissionSandbox;
20
+ }
21
+ export interface ClaudePermissionSandbox {
22
+ autoAllowsBash(): boolean;
23
+ shouldUseSandbox(input: {
24
+ command: string;
25
+ dangerouslyDisableSandbox?: boolean;
26
+ }): boolean;
27
+ getFsWriteConfig(): FsWriteRestrictionConfig | undefined;
18
28
  }
19
29
  export type ClaudePermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'manual' | 'dontAsk' | 'plan' | 'default';
20
30
  export declare function claudePermissionActionKey(call: ModelToolCall): string;
@@ -32,6 +42,7 @@ export declare class ClaudePermissionResolver implements PermissionResolver {
32
42
  private readonly isSessionActionApproved;
33
43
  private readonly additionalDirectories;
34
44
  private readonly additionalReadDirectories;
45
+ private readonly sandbox;
35
46
  constructor(options: ClaudePermissionResolverOptions);
36
47
  resolve(call: ModelToolCall, context?: PermissionResolutionContext): Promise<PermissionDecision>;
37
48
  private askDecision;
@@ -317,6 +317,7 @@ export class ClaudePermissionResolver {
317
317
  isSessionActionApproved;
318
318
  additionalDirectories;
319
319
  additionalReadDirectories;
320
+ sandbox;
320
321
  constructor(options) {
321
322
  this.cwd = resolve(options.cwd);
322
323
  this.cwdProvider = options.cwdProvider;
@@ -336,6 +337,7 @@ export class ClaudePermissionResolver {
336
337
  this.isSessionActionApproved = options.isSessionActionApproved;
337
338
  this.additionalDirectories = (options.additionalDirectories ?? []).map((directory) => resolve(directory));
338
339
  this.additionalReadDirectories = (options.additionalReadDirectories ?? []).map((directory) => resolve(directory));
340
+ this.sandbox = options.sandbox;
339
341
  if (this.permissionMode === 'auto') {
340
342
  if (!this.autoClassifier) {
341
343
  throw new Error('Permission mode auto requires a classifier');
@@ -444,7 +446,19 @@ export class ClaudePermissionResolver {
444
446
  .find((rule) => rule !== undefined);
445
447
  if (asked)
446
448
  return this.askDecision(call, cwd, permissionMode, context, 'rule', undefined, true);
449
+ if (command &&
450
+ call.name === 'Bash' &&
451
+ this.sandbox?.autoAllowsBash() &&
452
+ this.sandbox.shouldUseSandbox({
453
+ command,
454
+ ...(call.input.dangerouslyDisableSandbox === true
455
+ ? { dangerouslyDisableSandbox: true }
456
+ : {}),
457
+ })) {
458
+ return annotatePermissionDecision({ behavior: 'allow' }, 'default');
459
+ }
447
460
  if (command && call.name === 'Bash') {
461
+ const sandboxWriteConfig = this.sandbox?.getFsWriteConfig();
448
462
  const writeRoots = [
449
463
  cwd,
450
464
  ...effectiveAdditionalDirectories(updates, this.additionalDirectories).map((directory) => resolve(cwd, directory)),
@@ -458,6 +472,7 @@ export class ClaudePermissionResolver {
458
472
  readRoots: [...writeRoots, ...this.additionalReadDirectories],
459
473
  internalEditableRoots,
460
474
  internalReadableRoots,
475
+ ...(sandboxWriteConfig ? { sandboxWriteConfig } : {}),
461
476
  permissionMode,
462
477
  fileRule: (operation, absolutePath) => {
463
478
  const candidate = {
@@ -0,0 +1,54 @@
1
+ import { type FsWriteRestrictionConfig, type SandboxDependencyCheck, type SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime';
2
+ import type { ClaudeSandboxSettings } from './claude-sandbox-settings.js';
3
+ export type ClaudeSandboxPlatform = 'macos' | 'linux' | 'windows' | 'wsl';
4
+ export interface SandboxBackend {
5
+ initialize(config: SandboxRuntimeConfig): Promise<void>;
6
+ isSupportedPlatform(): boolean;
7
+ checkDependencies(ripgrep?: {
8
+ command: string;
9
+ args?: string[];
10
+ }): SandboxDependencyCheck;
11
+ wrapWithSandbox(command: string, shell?: string, config?: Partial<SandboxRuntimeConfig>, signal?: AbortSignal, options?: {
12
+ commandId?: string;
13
+ commandText?: string;
14
+ }): Promise<string>;
15
+ getFsWriteConfig(): FsWriteRestrictionConfig;
16
+ updateConfig(config: SandboxRuntimeConfig): void;
17
+ cleanupAfterCommand(): void;
18
+ annotateStderrWithSandboxFailures(commandId: string, stderr: string): string;
19
+ reset(): Promise<void>;
20
+ }
21
+ export interface SandboxCommandInput {
22
+ command: string;
23
+ dangerouslyDisableSandbox?: boolean;
24
+ }
25
+ export declare class ClaudeSandboxRuntime {
26
+ private readonly backend;
27
+ private readonly platform;
28
+ private active;
29
+ private initialized;
30
+ private initializationError;
31
+ private settings;
32
+ constructor(backend?: SandboxBackend, platform?: () => ClaudeSandboxPlatform);
33
+ initialize(settings: ClaudeSandboxSettings): Promise<void>;
34
+ isActive(): boolean;
35
+ isEnabledInSettings(): boolean;
36
+ isSupportedPlatform(): boolean;
37
+ platformName(): ClaudeSandboxPlatform;
38
+ dependencyCheck(settings?: ClaudeSandboxSettings): SandboxDependencyCheck;
39
+ autoAllowsBash(): boolean;
40
+ unavailableReason(settings?: ClaudeSandboxSettings): string | undefined;
41
+ shouldUseSandbox(input: SandboxCommandInput): boolean;
42
+ wrapCommand(input: SandboxCommandInput, options?: {
43
+ shell?: string;
44
+ signal?: AbortSignal;
45
+ commandId?: string;
46
+ }): Promise<string>;
47
+ annotateStderr(commandId: string, stderr: string): string;
48
+ cleanupAfterCommand(): void;
49
+ getFsWriteConfig(): FsWriteRestrictionConfig | undefined;
50
+ reset(): Promise<void>;
51
+ private requireSettings;
52
+ }
53
+ export declare const claudeSandboxRuntime: ClaudeSandboxRuntime;
54
+ //# sourceMappingURL=claude-sandbox-runtime.d.ts.map