praxis-agent 0.20.4 → 0.20.7

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.
@@ -0,0 +1,4434 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { access, copyFile, mkdir, readFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { homedir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { ClaudeSessionService, } from './application/session-service.js';
10
+ import { ClaudeCostStateStore } from './persistence/claude-cost-state-store.js';
11
+ import { agentColorMessage, parseAgentColorInput, } from './compatibility/claude/agent-color.js';
12
+ import { ClaudeConditionalRuleResolver, ClaudeContextAssembler, } from './compatibility/claude/context.js';
13
+ import { loadClaudeDynamicContext } from './compatibility/claude/dynamic-context.js';
14
+ import { createClaudePrSessionFilter, filterClaudePrLinkedSessions, } from './compatibility/claude/pr-links.js';
15
+ import { isClaudeSessionId, resolveClaudePaths, } from './compatibility/claude/paths.js';
16
+ import { loadClaudeContextResources, loadClaudeSettings, loadClaudeSharedResources, resolveClaudeProjectIdentity, resolveClaudeProjectMemoryDirectory, } from './compatibility/claude/shared-resources.js';
17
+ import { AgentRunCancelledError, } from './core/runtime.js';
18
+ import { persistTuiPermissionUpdates } from './cli/tui/permission-settings.js';
19
+ import { loadClaudeReleaseNotes } from './cli/tui/release-notes.js';
20
+ import { canonicalClaudeCostModelName, formatCostSummary, } from './cli/tui/cost-summary.js';
21
+ import { projectTuiHooks, } from './cli/tui/hook-settings.js';
22
+ import { DEFAULT_CLI_CONTROLS, resolveCliControls } from './cli/controls.js';
23
+ import { applyRuntimeSettingDefaults, loadRuntimeSettings, runtimeSettingsSystemPrompt, } from './cli/tui/runtime-settings.js';
24
+ import { createCliDebugSink } from './cli/debug.js';
25
+ import { ClaudePermissionResolver, } from './permissions/claude-permission-resolver.js';
26
+ import { createClaudeModelAutoClassifier, defaultClaudeAutoModeConfig, loadClaudeAutoModeConfig, } from './permissions/claude-auto-classifier.js';
27
+ import { ClaudeExtensionPermissionResolver, ClaudeExtensionToolRegistry, } from './extensions/claude-extension-tools.js';
28
+ import { ClaudeExtensionCatalog } from './extensions/claude-extensions.js';
29
+ import { ClaudeHookRunner } from './hooks/claude-hooks.js';
30
+ import { ClaudeMcpToolRegistry, } from './mcp/claude-mcp-tools.js';
31
+ import { ClaudeMcpManagement, filterDisabledMcpResources, mcpScope, } from './mcp/claude-mcp-management.js';
32
+ import { authenticateMcpServer, ClaudeMcpOAuthStore, mcpOAuthServerIdentity, readMcpClientSecret, } from './mcp/claude-mcp-oauth.js';
33
+ import { servePraxisMcpStdio } from './mcp/praxis-mcp-server.js';
34
+ import { VERIFIED_CLAUDE_SCHEMA_VERSION } from './compatibility/claude/schema.js';
35
+ import { detectInstalledClaudeVersion } from './platform/claude-version.js';
36
+ import { redactSensitiveText, sensitiveEnvironmentValues, } from './platform/sensitive-data.js';
37
+ import { AnthropicCompatibleProvider } from './providers/anthropic-compatible.js';
38
+ import { FallbackModelProvider } from './providers/fallback-provider.js';
39
+ import { OpenAICompatibleProvider } from './providers/openai-compatible.js';
40
+ import { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
41
+ import { ModelPricingRegistry, usageCostUsd } from './core/usage.js';
42
+ import { LocalToolRegistry } from './tools/local-tools.js';
43
+ import { ClaudeLspToolManager } from './tools/claude-lsp-tool.js';
44
+ import { ClaudeInteractiveToolManager, } from './tools/claude-interactive-tools.js';
45
+ import { claudeBackgroundTaskParent } from './application/background-bash-manager.js';
46
+ import { runTopLevelAgentWorker, TopLevelAgentManager, } from './application/top-level-agent-manager.js';
47
+ import { FilteredToolRegistry } from './tools/filtered-tool-registry.js';
48
+ import { WebToolRegistry } from './tools/web.js';
49
+ import { WorkspaceContext } from './application/session-worktree.js';
50
+ import { launchTmuxWorktree } from './platform/tmux-worktree.js';
51
+ import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
52
+ import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
53
+ import { createErrorResult, createSuccessResult, isHeadlessCostCommand, matchHeadlessColorCommand, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
54
+ import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
55
+ import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
56
+ import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
57
+ import { CLAUDE_PLUGIN_PRUNE_HELP, CLAUDE_PLUGIN_TAG_HELP, executeClaudePluginPrune, planClaudePluginPrune, tagClaudePlugin, } from './plugins/claude-plugin-maintenance.js';
58
+ import { formatDoctorReport, runDoctor } from './maintenance/doctor.js';
59
+ import { runSelfUpdate, } from './maintenance/self-update.js';
60
+ import { executeClaudeProjectPurge, planClaudeProjectPurge, } from './application/claude-project-purge.js';
61
+ const VERSION = createRequire(import.meta.url)('../package.json').version;
62
+ function fileResourceBaseUrl(environment, providerEnvironment) {
63
+ return (environment.PRAXIS_FILES_BASE_URL ??
64
+ providerEnvironment?.baseUrl ??
65
+ environment.PRAXIS_BASE_URL ??
66
+ (environment.PRAXIS_PROVIDER === 'anthropic'
67
+ ? 'https://api.anthropic.com/v1'
68
+ : 'https://api.openai.com/v1'));
69
+ }
70
+ function fileResourceHeaders(environment, providerEnvironment, credential) {
71
+ const anthropic = providerEnvironment?.provider === 'anthropic';
72
+ const bearer = Boolean(environment.PRAXIS_FILES_BEARER_TOKEN);
73
+ const authentication = bearer
74
+ ? { Authorization: `Bearer ${credential}` }
75
+ : anthropic
76
+ ? { 'x-api-key': credential }
77
+ : { Authorization: `Bearer ${credential}` };
78
+ return {
79
+ ...authentication,
80
+ ...(anthropic
81
+ ? {
82
+ 'anthropic-version': '2023-06-01',
83
+ 'anthropic-beta': `files-api-2025-04-14${bearer ? ',oauth-2025-04-20' : ''}`,
84
+ }
85
+ : {}),
86
+ };
87
+ }
88
+ function createProviderForModel(apiKey, providerEnvironment, context, controls, explicitThinkingControls) {
89
+ return (selectedModel) => {
90
+ const providerOptions = {
91
+ apiKey,
92
+ model: selectedModel,
93
+ baseUrl: providerEnvironment.baseUrl,
94
+ ...('contextWindowTokens' in context
95
+ ? { contextWindowTokens: context.contextWindowTokens }
96
+ : {}),
97
+ };
98
+ return providerEnvironment.provider === 'anthropic'
99
+ ? new AnthropicCompatibleProvider({
100
+ ...providerOptions,
101
+ thinking: {
102
+ mode: controls.thinking ?? 'enabled',
103
+ ...(controls.maxThinkingTokens === undefined
104
+ ? {}
105
+ : { maxTokens: controls.maxThinkingTokens }),
106
+ },
107
+ ...('maxOutputTokens' in providerEnvironment
108
+ ? { maxOutputTokens: providerEnvironment.maxOutputTokens }
109
+ : {}),
110
+ ...('anthropicVersion' in providerEnvironment
111
+ ? { anthropicVersion: providerEnvironment.anthropicVersion }
112
+ : {}),
113
+ ...('webSearch' in providerEnvironment
114
+ ? { webSearch: providerEnvironment.webSearch }
115
+ : {}),
116
+ })
117
+ : new OpenAICompatibleProvider({
118
+ ...providerOptions,
119
+ ...(explicitThinkingControls.thinking === undefined &&
120
+ explicitThinkingControls.maxThinkingTokens === undefined
121
+ ? {}
122
+ : {
123
+ thinking: {
124
+ mode: controls.thinking ?? 'enabled',
125
+ ...(controls.maxThinkingTokens === undefined
126
+ ? {}
127
+ : { maxTokens: controls.maxThinkingTokens }),
128
+ },
129
+ }),
130
+ });
131
+ };
132
+ }
133
+ const HELP = `Praxis — local-first general agent
134
+
135
+ Usage:
136
+ praxis
137
+ praxis [options] [prompt]
138
+ praxis -p [options] [prompt]
139
+ praxis --resume [session-id|title|search] [options] [prompt]
140
+ praxis run [options] <prompt>
141
+ praxis resume [options] <session-id> <prompt>
142
+ praxis fork [--json] <session-id>
143
+ praxis sessions [--json]
144
+ praxis inspect [--json] <session-id>
145
+ praxis export [--json] <session-id>
146
+ praxis --bg [options] <prompt>
147
+ praxis agents [--json] [--all] [--cwd <path>]
148
+ praxis attach <agent-id>
149
+ praxis logs <agent-id>
150
+ praxis stop <agent-id>
151
+ praxis mcp <list|get|add|add-json|remove|reset-project-choices|login|logout|serve> ...
152
+ praxis auto-mode <config|defaults|critique>
153
+ praxis plugin|plugins <details|list|install|uninstall|enable|disable|update|init|prune|tag|validate|marketplace> ...
154
+ praxis doctor [--json]
155
+ praxis install [--force] [stable|latest|version]
156
+ praxis update|upgrade
157
+ praxis project purge [options] [path]
158
+
159
+ Options:
160
+ -p, --print Print response and exit
161
+ --bg, --background Run as a persistent background agent
162
+ -r, --resume [session] Resume by ID/title, or select interactively
163
+ --from-pr [number-or-url] Resume a session linked to a GitHub PR
164
+ -c, --continue Continue latest session in this directory
165
+ --fork-session Fork when resuming or continuing
166
+ --resume-session-at <message-id> Resume at an active conversation message
167
+ --session-id <uuid> Use an explicit ID for a new session
168
+ -n, --name <name> Set session display name
169
+ --model <model> Select model for this session
170
+ --effort <level> low, medium, high, xhigh, or max
171
+ --thinking <mode> enabled, adaptive, or disabled
172
+ --max-thinking-tokens <tokens> Cap extended-thinking tokens
173
+ --fallback-model <models> Comma-separated print-mode fallbacks
174
+ --json-schema <schema> Print-mode JSON Schema for structured output
175
+ --max-budget-usd <amount> Maximum print-mode API spend
176
+ --prefill <text> Accepted as a Claude 2.1.208-compatible no-op
177
+ --prompt-suggestions [value] Enable prompt suggestions. In print/SDK mode, emits a
178
+ prompt_suggestion message after each turn with a predicted
179
+ next user prompt (choices: "true", "false", "1", "0", "yes",
180
+ "no", "on", "off", preset: "true")
181
+ --scope <scope> MCP scope: local, project, or user
182
+ --no-browser Print MCP OAuth URL without opening a browser
183
+ --no-session-persistence Keep print-mode session in memory only
184
+ --agent <name> Select a shared agent definition
185
+ --max-turns <turns> Limit print-mode model round trips
186
+ --betas <betas...> Include Anthropic beta headers
187
+ --brief Enable SendUserMessage communication
188
+ --ax-screen-reader Render flat screen-reader output
189
+ -d, --debug [filter] Enable debug mode with optional category filtering
190
+ (e.g., "api,hooks" or "!1p,!file")
191
+ --debug-file <path> Write runtime diagnostics to a file
192
+ --file <specs...> Download file resources at startup (file_id:relative_path)
193
+ --agents <json> Define inline agents for this session
194
+ --mcp-config <configs...> Load MCP server JSON files or objects
195
+ --strict-mcp-config Ignore configured MCP servers
196
+ --disable-slash-commands Disable skills and slash commands
197
+ --settings <file-or-json> Load additional settings
198
+ --setting-sources <sources> user, project, local, or an empty list
199
+ --safe-mode Disable shared customizations
200
+ --bare Use only explicitly supplied context
201
+ --system-prompt <prompt> Set system prompt
202
+ --append-system-prompt <prompt> Append system prompt
203
+ --exclude-dynamic-system-prompt-sections
204
+ Move cwd, environment, memory path, and git status into first user message
205
+ (default system prompt only; ignored with --system-prompt)
206
+ --add-dir <directories...> Allow access to additional directories
207
+ --plugin-dir <path> Load a local plugin directory or .zip for this session (repeatable)
208
+ --plugin-url <url> Load a plugin .zip URL for this session (repeatable)
209
+ --tools <tools...> Select available tools; empty disables all
210
+ --allowedTools, --allowed-tools <tools...>
211
+ Add permission allow rules
212
+ --disallowedTools, --disallowed-tools <tools...>
213
+ Add permission deny rules
214
+ --permission-mode <mode> Set permission behavior
215
+ --dangerously-skip-permissions Bypass checks except explicit deny rules
216
+ --allow-dangerously-skip-permissions
217
+ Allow bypass mode without enabling it
218
+ --input-format <format> text (default) or stream-json
219
+ --output-format <format> text (default), json, or stream-json
220
+ --include-partial-messages Emit stream_event records
221
+ --include-hook-events Emit hook_started/progress/response records
222
+ --replay-user-messages Echo stream-json user records
223
+ --retry-interrupted-tools Approve prepared interrupted tools
224
+ --verbose Required for stream-json output
225
+ --json Legacy Praxis runtime NDJSON output
226
+ -h, --help Show help
227
+ -v, --version Show version
228
+ -w, --worktree [name] Start in an isolated Git worktree
229
+ --tmux[=classic] Launch worktree in an iTerm2 native pane when available; classic forces tmux
230
+
231
+ Provider environment:
232
+ PRAXIS_PROVIDER=openai|anthropic, PRAXIS_API_KEY, PRAXIS_MODEL
233
+ PRAXIS_BASE_URL, PRAXIS_MAX_OUTPUT_TOKENS, PRAXIS_ANTHROPIC_VERSION
234
+ PRAXIS_ANTHROPIC_WEB_SEARCH=true|false
235
+ PRAXIS_CONTEXT_WINDOW_TOKENS, PRAXIS_CONTEXT_RESERVE_TOKENS
236
+ `;
237
+ const AGENTS_HELP = `Usage: praxis agents [options]
238
+
239
+ Manage background agents
240
+
241
+ Options:
242
+ --add-dir <directory> Additional directory to allow tool
243
+ access to in dispatched sessions
244
+ (repeatable)
245
+ --agent <agent> Default agent for sessions dispatched
246
+ from agent view. Overrides the 'agent'
247
+ setting.
248
+ --all With --json: include completed sessions
249
+ (the full agent view list)
250
+ --allow-dangerously-skip-permissions Make bypass-permissions mode available
251
+ to dispatched sessions without
252
+ defaulting to it
253
+ --cwd <path> Show only background sessions started
254
+ under <path>
255
+ --dangerously-skip-permissions Alias for --permission-mode
256
+ bypassPermissions
257
+ --effort <level> Default effort level for sessions
258
+ dispatched from agent view
259
+ -h, --help Display help for command
260
+ --json Print active sessions as a JSON array
261
+ and exit (for scripting; does not
262
+ require a TTY)
263
+ --mcp-config <config> MCP server configuration to apply to
264
+ dispatched sessions (repeatable)
265
+ --model <model> Default model for sessions dispatched
266
+ from agent view
267
+ --permission-mode <mode> Default permission mode for sessions
268
+ dispatched from agent view
269
+ --plugin-dir <path> Load plugins from specified directory
270
+ for the agent view and dispatched
271
+ sessions (repeatable)
272
+ --setting-sources <sources> Comma-separated list of setting sources
273
+ to load (user, project, local).
274
+ --settings <file-or-json> Settings file or JSON string to apply to
275
+ the agent view and dispatched sessions
276
+ --strict-mcp-config Only use MCP servers from --mcp-config
277
+ in dispatched sessions
278
+ `;
279
+ const MCP_HELP = `Usage: praxis mcp [options] [command]
280
+
281
+ Configure and manage Model Context Protocol servers.
282
+
283
+ Options:
284
+ -h, --help Display help for command
285
+
286
+ Commands:
287
+ list [options] List configured MCP servers
288
+ get [options] <name> Get an MCP server configuration
289
+ add [options] <name> <commandOrUrl> [args...] Add an MCP server
290
+ add-json [options] <name> <json> Add an MCP server from JSON
291
+ remove [options] <name> Remove an MCP server
292
+ reset-project-choices Reset project MCP approvals
293
+ login [options] <name> Authenticate with an MCP server
294
+ logout [options] <name> Clear MCP OAuth credentials
295
+ serve [options] Start Praxis MCP server over stdio
296
+ `;
297
+ const MCP_LIST_HELP = `Usage: praxis mcp list [options]
298
+
299
+ List configured MCP servers. With --scope, list only servers in that scope.
300
+
301
+ Options:
302
+ --scope <scope> Configuration scope: local, project, or user
303
+ --json Print a machine-readable mcp-list result
304
+ -h, --help Display help for command
305
+ `;
306
+ const MCP_GET_HELP = `Usage: praxis mcp get [options] <name>
307
+
308
+ Print configuration for one MCP server.
309
+
310
+ Options:
311
+ --scope <scope> Read from local, project, or user scope
312
+ --json Print a machine-readable mcp-server result
313
+ -h, --help Display help for command
314
+ `;
315
+ const MCP_ADD_HELP = `Usage: praxis mcp add [options] <name> <commandOrUrl> [args...]
316
+
317
+ Add an MCP server to Praxis.
318
+
319
+ Examples:
320
+ # Add HTTP server:
321
+ praxis mcp add --transport http sentry https://mcp.sentry.dev/mcp
322
+
323
+ # Add HTTP server with headers:
324
+ praxis mcp add --transport http corridor https://app.corridor.dev/api/mcp --header "Authorization: Bearer ..."
325
+
326
+ # Add stdio server with environment variables:
327
+ praxis mcp add my-server -e API_KEY=xxx -- npx my-mcp-server
328
+
329
+ # Add stdio server with subprocess flags:
330
+ praxis mcp add my-server -- my-command --some-flag arg1
331
+
332
+ Options:
333
+ --callback-port <port> Fixed port for OAuth callback (for servers requiring pre-registered redirect URIs)
334
+ --client-id <clientId> OAuth client ID for HTTP/SSE servers
335
+ --client-secret Prompt for OAuth client secret (or set MCP_CLIENT_SECRET env var)
336
+ -e, --env <env...> Set environment variables (e.g. -e KEY=value)
337
+ -H, --header <header...> Set WebSocket headers (e.g. -H "X-Api-Key: abc123" -H "X-Custom: value")
338
+ -h, --help Display help for command
339
+ -s, --scope <scope> Configuration scope (local, user, or project) (default: "local")
340
+ -t, --transport <transport> Transport type (stdio, sse, http). Defaults to stdio if not specified.
341
+ `;
342
+ const MCP_ADD_JSON_HELP = `Usage: praxis mcp add-json [options] <name> <json>
343
+
344
+ Add an MCP server from a JSON configuration object. Supported configurations
345
+ include stdio and HTTP server records accepted by Praxis.
346
+
347
+ Options:
348
+ --client-secret Prompt for OAuth client secret (or set MCP_CLIENT_SECRET env var)
349
+ -s, --scope <scope> Configuration scope: local, project, or user (default: local)
350
+ --json Print a machine-readable mcp-added result
351
+ -h, --help Display help for command
352
+ `;
353
+ const MCP_REMOVE_HELP = `Usage: praxis mcp remove [options] <name>
354
+
355
+ Remove an MCP server. Without --scope, Praxis resolves the configured server
356
+ across available scopes.
357
+
358
+ Options:
359
+ -s, --scope <scope> Remove from local, project, or user scope
360
+ --json Print a machine-readable mcp-removed result
361
+ -h, --help Display help for command
362
+ `;
363
+ const MCP_RESET_PROJECT_CHOICES_HELP = `Usage: praxis mcp reset-project-choices [options]
364
+
365
+ Reset approved and rejected project-scoped MCP server choices.
366
+
367
+ Options:
368
+ --json Print a machine-readable confirmation
369
+ -h, --help Display help for command
370
+ `;
371
+ const MCP_LOGIN_HELP = `Usage: praxis mcp login [options] <name>
372
+
373
+ Authenticate with an OAuth-enabled MCP server.
374
+
375
+ Options:
376
+ --no-browser Print authorization URL instead of opening a browser
377
+ --scope <scope> Read server from local, project, or user scope
378
+ -h, --help Display help for command
379
+ `;
380
+ const MCP_LOGOUT_HELP = `Usage: praxis mcp logout [options] <name>
381
+
382
+ Clear stored OAuth credentials for an MCP server.
383
+
384
+ Options:
385
+ --scope <scope> Read server from local, project, or user scope
386
+ -h, --help Display help for command
387
+ `;
388
+ const MCP_SERVE_HELP = `Usage: praxis mcp serve [options]
389
+
390
+ Start the Praxis MCP server over stdio.
391
+
392
+ Options:
393
+ -d, --debug Enable MCP server debug logging
394
+ --verbose Enable verbose MCP server output
395
+ -h, --help Display help for command
396
+ `;
397
+ const MCP_ACTION_HELP = {
398
+ list: MCP_LIST_HELP,
399
+ get: MCP_GET_HELP,
400
+ add: MCP_ADD_HELP,
401
+ 'add-json': MCP_ADD_JSON_HELP,
402
+ remove: MCP_REMOVE_HELP,
403
+ 'reset-project-choices': MCP_RESET_PROJECT_CHOICES_HELP,
404
+ login: MCP_LOGIN_HELP,
405
+ logout: MCP_LOGOUT_HELP,
406
+ serve: MCP_SERVE_HELP,
407
+ };
408
+ const PLUGIN_HELP = `Usage: praxis plugin|plugins [options] [command]
409
+
410
+ Manage Praxis-compatible Claude plugins and marketplaces.
411
+
412
+ Options:
413
+ --json Print machine-readable command output when supported
414
+ --scope <scope> Plugin configuration scope: local, project, or user
415
+ -h, --help Display help for command
416
+
417
+ Commands:
418
+ details <name> Show component inventory and projected token cost
419
+ list [options] List installed or available plugins
420
+ install|i [options] <source> Install a local plugin, URL, or plugin@marketplace
421
+ uninstall|remove [options] <id> Uninstall a plugin
422
+ enable [options] <id> Enable a plugin
423
+ disable [options] [id] Disable one plugin, or all with --all
424
+ update [options] <id> Update a plugin
425
+ init|new [options] <name> Scaffold ~/.claude/skills/<name>
426
+ prune|autoremove [options] Remove unused auto-installed dependencies
427
+ tag [options] [path] Create a validated plugin release tag
428
+ validate [options] <path> Validate a plugin or marketplace manifest
429
+ eval [options] [target] Evaluate plugin behavior
430
+ marketplace [command] Manage configured marketplaces
431
+ `;
432
+ const PLUGIN_DETAILS_HELP = `Usage: praxis plugin details <name>
433
+
434
+ Show a plugin's component inventory and projected token cost.
435
+
436
+ Options:
437
+ -h, --help Display help for command
438
+ `;
439
+ const PLUGIN_LIST_HELP = `Usage: praxis plugin list [options]
440
+
441
+ List installed native and local plugins with enabled and validation status.
442
+
443
+ Options:
444
+ --available List marketplace plugins that are not installed
445
+ --json Print plugin records as JSON (default output is JSON as well)
446
+ -h, --help Display help for command
447
+ `;
448
+ const PLUGIN_INSTALL_HELP = `Usage: praxis plugin install [options] <path-or-url-or-plugin@marketplace>
449
+
450
+ Install a plugin from a local directory, URL, or configured marketplace. A
451
+ marketplace plugin identifier uses the form plugin@marketplace.
452
+
453
+ Options:
454
+ --config <key=value> Set userConfig; use server.key=value for MCPB (repeatable)
455
+ -s, --scope <scope> Install native marketplace plugin at local, project, or user scope (default: user)
456
+ --json Print a machine-readable plugin-installed result
457
+ -h, --help Display help for command
458
+ `;
459
+ const PLUGIN_UNINSTALL_HELP = `Usage: praxis plugin uninstall [options] <name-or-plugin@marketplace>
460
+
461
+ Uninstall a local plugin or a native marketplace plugin.
462
+
463
+ Options:
464
+ --keep-data Preserve the plugin's persistent data directory
465
+ --prune Also remove unused auto-installed dependencies
466
+ -s, --scope <scope> Select native plugin scope: local, project, or user
467
+ -y, --yes Skip the --prune confirmation prompt
468
+ --json Print a machine-readable plugin-uninstalled result
469
+ -h, --help Display help for command
470
+ `;
471
+ const PLUGIN_ENABLE_HELP = `Usage: praxis plugin enable [options] <name-or-plugin@marketplace>
472
+
473
+ Enable a disabled local plugin or native marketplace plugin.
474
+
475
+ Options:
476
+ -s, --scope <scope> Select native plugin scope: local, project, or user
477
+ --json Print a machine-readable plugin-enabled result
478
+ -h, --help Display help for command
479
+ `;
480
+ const PLUGIN_DISABLE_HELP = `Usage: praxis plugin disable [options] [name-or-plugin@marketplace]
481
+
482
+ Disable an enabled local plugin or native marketplace plugin.
483
+
484
+ Options:
485
+ -a, --all Disable all enabled plugins
486
+ -s, --scope <scope> Select native plugin scope: local, project, or user
487
+ --json Print a machine-readable plugin-disabled result
488
+ -h, --help Display help for command
489
+ `;
490
+ const PLUGIN_UPDATE_HELP = `Usage: praxis plugin update [options] <name-or-plugin@marketplace>
491
+
492
+ Update a local plugin or native marketplace plugin from its configured source.
493
+
494
+ Options:
495
+ -s, --scope <scope> Select native plugin scope: local, project, or user
496
+ --json Print a machine-readable plugin-updated result
497
+ -h, --help Display help for command
498
+ `;
499
+ const PLUGIN_INIT_HELP = `Usage: praxis plugin init|new [options] <name>
500
+
501
+ Scaffold a plugin at ~/.claude/skills/<name>. The legacy
502
+ plugin init <directory> <name> form remains available without native options.
503
+
504
+ Options:
505
+ --author <name> Author name (default: git config user.name)
506
+ --author-email <email> Author email (default: git config user.email)
507
+ --description <text> Manifest description
508
+ -f, --force Overwrite an existing .claude-plugin at the target
509
+ --with <components...> Also scaffold: skills, agents, hooks, mcp, lsp, output-style, channel
510
+ -h, --help Display help for command
511
+ `;
512
+ const PLUGIN_VALIDATE_HELP = `Usage: praxis plugin validate <directory>
513
+
514
+ Validate a plugin manifest and report its parsed metadata.
515
+
516
+ Options:
517
+ --strict Treat validation warnings as errors
518
+ --json Print a machine-readable plugin-valid result
519
+ -h, --help Display help for command
520
+ `;
521
+ const PLUGIN_ACTION_HELP = {
522
+ details: PLUGIN_DETAILS_HELP,
523
+ list: PLUGIN_LIST_HELP,
524
+ install: PLUGIN_INSTALL_HELP,
525
+ i: PLUGIN_INSTALL_HELP,
526
+ uninstall: PLUGIN_UNINSTALL_HELP,
527
+ remove: PLUGIN_UNINSTALL_HELP,
528
+ enable: PLUGIN_ENABLE_HELP,
529
+ disable: PLUGIN_DISABLE_HELP,
530
+ update: PLUGIN_UPDATE_HELP,
531
+ init: PLUGIN_INIT_HELP,
532
+ new: PLUGIN_INIT_HELP,
533
+ prune: CLAUDE_PLUGIN_PRUNE_HELP,
534
+ autoremove: CLAUDE_PLUGIN_PRUNE_HELP,
535
+ tag: CLAUDE_PLUGIN_TAG_HELP,
536
+ validate: PLUGIN_VALIDATE_HELP,
537
+ eval: PLUGIN_EVAL_HELP,
538
+ };
539
+ const PLUGIN_MARKETPLACE_HELP = `Usage: praxis plugin marketplace [options] [command]
540
+
541
+ Manage configured plugin marketplaces.
542
+
543
+ Options:
544
+ --scope <scope> Scope for marketplace additions: local, project, or user (default: user)
545
+ --json Print machine-readable command output when supported
546
+ -h, --help Display help for command
547
+
548
+ Commands:
549
+ list List configured marketplaces
550
+ add <source> Add marketplace from a local path or URL
551
+ remove|rm <name> Remove a configured marketplace
552
+ update [name] Update one marketplace, or all when name is omitted
553
+ `;
554
+ const PLUGIN_MARKETPLACE_LIST_HELP = `Usage: praxis plugin marketplace list [options]
555
+
556
+ List configured marketplaces.
557
+
558
+ Options:
559
+ --json Print marketplace records as JSON (default output is JSON as well)
560
+ -h, --help Display help for command
561
+ `;
562
+ const PLUGIN_MARKETPLACE_ADD_HELP = `Usage: praxis plugin marketplace add [options] <source>
563
+
564
+ Add a marketplace from a local path, URL, or supported repository source.
565
+
566
+ Options:
567
+ --scope <scope> Configuration scope: local, project, or user (default: user)
568
+ --sparse <paths...> Limit a Git checkout to specific directories
569
+ --json Print a machine-readable plugin-marketplace-added result
570
+ -h, --help Display help for command
571
+ `;
572
+ const PLUGIN_MARKETPLACE_REMOVE_HELP = `Usage: praxis plugin marketplace remove [options] <name>
573
+
574
+ Remove a configured marketplace.
575
+
576
+ Options:
577
+ --scope <scope> Select marketplace scope: local, project, or user
578
+ --json Print a machine-readable plugin-marketplace-removed result
579
+ -h, --help Display help for command
580
+ `;
581
+ const PLUGIN_MARKETPLACE_UPDATE_HELP = `Usage: praxis plugin marketplace update [options] [name]
582
+
583
+ Update one marketplace, or all configured marketplaces when name is omitted.
584
+
585
+ Options:
586
+ --json Print a machine-readable plugin-marketplace-updated result
587
+ -h, --help Display help for command
588
+ `;
589
+ const PLUGIN_MARKETPLACE_ACTION_HELP = {
590
+ list: PLUGIN_MARKETPLACE_LIST_HELP,
591
+ add: PLUGIN_MARKETPLACE_ADD_HELP,
592
+ remove: PLUGIN_MARKETPLACE_REMOVE_HELP,
593
+ rm: PLUGIN_MARKETPLACE_REMOVE_HELP,
594
+ update: PLUGIN_MARKETPLACE_UPDATE_HELP,
595
+ };
596
+ const AUTO_MODE_HELP = `Usage: praxis auto-mode [options] [command]
597
+
598
+ Inspect auto mode classifier configuration and critique custom rules.
599
+
600
+ Options:
601
+ -h, --help Display help for command
602
+
603
+ Commands:
604
+ config Print effective auto mode config as JSON
605
+ defaults Print default environment, allow, soft_deny, and hard_deny rules as JSON
606
+ critique [options] Get provider-backed feedback on custom auto mode rules
607
+ `;
608
+ const AUTO_MODE_CONFIG_HELP = `Usage: praxis auto-mode config
609
+
610
+ Print effective auto mode configuration as JSON. Settings values override
611
+ defaults; omitted rule lists retain their default values.
612
+
613
+ Options:
614
+ -h, --help Display help for command
615
+ `;
616
+ const AUTO_MODE_DEFAULTS_HELP = `Usage: praxis auto-mode defaults [options]
617
+
618
+ Print default auto mode environment, allow, soft_deny, and hard_deny rules as
619
+ JSON.
620
+
621
+ Options:
622
+ --label <prefix> Show only rules whose label starts with this prefix (case-insensitive)
623
+ -h, --help Display help for command
624
+ `;
625
+ const AUTO_MODE_CRITIQUE_HELP = `Usage: praxis auto-mode critique [options]
626
+
627
+ Get provider-backed feedback on custom auto mode rules. With no custom rule
628
+ lists configured, Praxis prints guidance without creating a provider request.
629
+
630
+ Options:
631
+ --model <model> Override model used for critique (default: PRAXIS_MODEL)
632
+ -h, --help Display help for command
633
+ `;
634
+ const AUTO_MODE_ACTION_HELP = {
635
+ config: AUTO_MODE_CONFIG_HELP,
636
+ defaults: AUTO_MODE_DEFAULTS_HELP,
637
+ critique: AUTO_MODE_CRITIQUE_HELP,
638
+ };
639
+ const PROJECT_HELP = `Usage: praxis project [options] [command]
640
+
641
+ Manage Praxis-compatible Claude project state.
642
+
643
+ Options:
644
+ -h, --help Display help for command
645
+
646
+ Commands:
647
+ purge [options] [path] Delete Claude project state for a path (default: current project)
648
+ `;
649
+ const DOCTOR_HELP = `Usage: praxis doctor [options]
650
+
651
+ Check Praxis installation and local single-user configuration health.
652
+
653
+ Options:
654
+ --json Output a machine-readable report
655
+ -h, --help Show help
656
+ `;
657
+ const PROJECT_PURGE_HELP = `Usage: praxis project purge [options] [path]
658
+
659
+ Delete all Claude Code state for a project (transcripts, tasks, file history,
660
+ config entry)
661
+
662
+ Options:
663
+ --all Purge state for every project (mutually exclusive with [path])
664
+ --dry-run List what would be deleted without deleting anything
665
+ -i, --interactive Prompt for each item before deleting
666
+ --json Print purge plan and result as JSON
667
+ -y, --yes Skip confirmation prompt
668
+ -h, --help Show help
669
+
670
+ Default: [path] is current project; Praxis requests confirmation unless --yes
671
+ is supplied.
672
+ `;
673
+ const INSTALL_HELP = `Usage: praxis install [options] [target]
674
+
675
+ Install Praxis global npm package. Target may be stable, latest, next, beta,
676
+ canary, or an exact semantic version.
677
+
678
+ Options:
679
+ --force Force installation even if already installed
680
+ --json Output a machine-readable result
681
+ -h, --help Show help
682
+ `;
683
+ const UPDATE_HELP = `Usage: praxis update|upgrade [options]
684
+
685
+ Install latest Praxis global npm package.
686
+
687
+ Options:
688
+ --json Output a machine-readable result
689
+ -h, --help Show help
690
+ `;
691
+ export { parseContextEnvironment, parseProviderEnvironment };
692
+ const consoleIO = {
693
+ stdout: (message) => process.stdout.write(message),
694
+ stderr: (message) => process.stderr.write(message),
695
+ isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
696
+ readStdinLines: () => process.stdin,
697
+ };
698
+ 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, }) => {
699
+ const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
700
+ const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
701
+ const claudeVersion = VERIFIED_CLAUDE_SCHEMA_VERSION;
702
+ const cwd = requestedCwd ?? process.cwd();
703
+ const workspace = new WorkspaceContext(cwd);
704
+ const configuredRoot = runtimeEnvironment.CLAUDE_CONFIG_DIR || undefined;
705
+ const configRoot = resolve(requestedConfigRoot ?? configuredRoot ?? resolve(homedir(), '.claude'));
706
+ const claudeStatePath = requestedConfigRoot || configuredRoot
707
+ ? join(configRoot, '.claude.json')
708
+ : resolve(homedir(), '.claude.json');
709
+ const runtimeSettings = controls.safeMode || controls.bare
710
+ ? undefined
711
+ : await loadRuntimeSettings({ configRoot, statePath: claudeStatePath });
712
+ const runtimeSettingsPrompt = runtimeSettingsSystemPrompt(runtimeSettings);
713
+ const cli = await resolveCliControls({
714
+ ...applyRuntimeSettingDefaults(controls, runtimeSettings),
715
+ ...(interactiveModel === undefined ? {} : { model: interactiveModel }),
716
+ ...(interactiveEffort === undefined ? {} : { effort: interactiveEffort }),
717
+ ...(interactivePermissionMode === undefined
718
+ ? {}
719
+ : { permissionMode: interactivePermissionMode }),
720
+ }, cwd);
721
+ const effectiveAppendSystemPrompt = [
722
+ runtimeSettingsPrompt,
723
+ cli.appendSystemPrompt,
724
+ ]
725
+ .filter((value) => value !== undefined)
726
+ .join('\n');
727
+ const debug = cli.debug !== undefined || cli.debugFile !== undefined
728
+ ? createCliDebugSink(eventSink, {
729
+ cwd,
730
+ ...(cli.debug === undefined ? {} : { filter: cli.debug }),
731
+ ...(cli.debugFile === undefined ? {} : { file: cli.debugFile }),
732
+ ...(cli.debugFile === undefined
733
+ ? { stderr: (message) => process.stderr.write(message) }
734
+ : {}),
735
+ })
736
+ : undefined;
737
+ const runtimeEventSink = debug?.eventSink ?? eventSink;
738
+ let provider;
739
+ let providerForModel;
740
+ let providerForMainModel;
741
+ const context = parseContextEnvironment(runtimeEnvironment);
742
+ const apiKey = runtimeEnvironment.PRAXIS_API_KEY;
743
+ const model = cli.model ?? runtimeEnvironment.PRAXIS_MODEL;
744
+ const providerEnvironment = apiKey && model
745
+ ? parseProviderEnvironment(runtimeEnvironment)
746
+ : cli.fileResources.length > 0
747
+ ? parseProviderEnvironment(runtimeEnvironment)
748
+ : undefined;
749
+ const fileCredential = runtimeEnvironment.PRAXIS_FILES_BEARER_TOKEN ??
750
+ runtimeEnvironment.PRAXIS_FILES_API_KEY ??
751
+ apiKey;
752
+ if (cli.fileResources.length > 0 && !fileCredential) {
753
+ throw new Error('--file requires PRAXIS_FILES_BEARER_TOKEN, PRAXIS_FILES_API_KEY, or PRAXIS_API_KEY');
754
+ }
755
+ if (requireProvider && (!apiKey || !model)) {
756
+ throw new Error('PRAXIS_API_KEY and a model (--model or PRAXIS_MODEL) are required');
757
+ }
758
+ if (!hooksOnly && apiKey && model) {
759
+ if (!providerEnvironment) {
760
+ throw new Error('Provider environment is unavailable');
761
+ }
762
+ providerForModel = createProviderForModel(apiKey, providerEnvironment, context, cli, controls);
763
+ const createProvider = providerForModel;
764
+ providerForMainModel = (primaryModel) => {
765
+ const models = [primaryModel, ...(cli.fallbackModels ?? [])].filter((candidate, index, all) => all.indexOf(candidate) === index);
766
+ const providers = models.map((candidate) => createProvider(candidate));
767
+ const selected = providers[0];
768
+ if (!selected)
769
+ throw new Error('A primary model is required');
770
+ return providers.length > 1
771
+ ? new FallbackModelProvider({ providers })
772
+ : selected;
773
+ };
774
+ provider = providerForMainModel(model);
775
+ }
776
+ const costStateStore = new ClaudeCostStateStore({
777
+ statePath: claudeStatePath,
778
+ projectIdentity: await resolveClaudeProjectIdentity({ cwd }),
779
+ sidecarPath: join(configRoot, 'praxis', 'unknown-cost-sidecar.json'),
780
+ });
781
+ const options = {
782
+ configRoot,
783
+ cwd,
784
+ claudeVersion,
785
+ eventSink: runtimeEventSink,
786
+ sessionPersistence: cli.sessionPersistence,
787
+ costStateStore,
788
+ explicitModel: interactiveModel !== undefined || controls.model !== undefined,
789
+ explicitSystemPrompt: cli.systemPrompt !== undefined,
790
+ agentInitialPromptHandledExternally: interactive,
791
+ agentSystemPromptOverridesExplicit: interactive,
792
+ effort: cli.effort ?? 'high',
793
+ ...(cli.maxTurns === undefined ? {} : { maxModelTurns: cli.maxTurns }),
794
+ ...(cli.brief && !cli.disallowedTools.includes('SendUserMessage')
795
+ ? { brief: true }
796
+ : {}),
797
+ ...(cli.betas.length ? { betas: cli.betas } : {}),
798
+ ...(cli.jsonSchema ? { structuredOutputSchema: cli.jsonSchema } : {}),
799
+ ...(cli.maxBudgetUsd === undefined
800
+ ? {}
801
+ : { maxBudgetUsd: cli.maxBudgetUsd }),
802
+ pricing: ModelPricingRegistry.fromEnvironment(runtimeEnvironment.PRAXIS_PRICING_JSON),
803
+ collectMetrics: true,
804
+ ...(sessionKind === undefined ? {} : { sessionKind }),
805
+ workspace,
806
+ ...(cli.worktreeRequested
807
+ ? {
808
+ initialWorktree: true,
809
+ ...(cli.worktreeName === undefined
810
+ ? {}
811
+ : { initialWorktreeName: cli.worktreeName }),
812
+ }
813
+ : {}),
814
+ ...(cli.fileResources.length > 0
815
+ ? {
816
+ fileResources: cli.fileResources,
817
+ fileResourceConfig: {
818
+ cwd,
819
+ apiKey: fileCredential ?? '',
820
+ baseUrl: runtimeEnvironment.PRAXIS_FILES_BASE_URL ??
821
+ fileResourceBaseUrl(runtimeEnvironment, providerEnvironment),
822
+ headers: fileResourceHeaders(runtimeEnvironment, providerEnvironment, fileCredential ?? ''),
823
+ },
824
+ }
825
+ : {}),
826
+ };
827
+ const hookConfiguration = async () => {
828
+ const [settings, pluginResources] = await Promise.all([
829
+ loadClaudeSettings({
830
+ configRoot,
831
+ cwd,
832
+ ...(cli.bare
833
+ ? { settingSources: [] }
834
+ : cli.settingSources === undefined
835
+ ? {}
836
+ : { settingSources: cli.settingSources }),
837
+ }),
838
+ loadClaudePlugins({
839
+ configRoot,
840
+ cwd,
841
+ pluginDirectories: cli.pluginDirectories,
842
+ pluginUrls: cli.pluginUrls,
843
+ strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
844
+ loadInstalled: !cli.safeMode && !cli.bare,
845
+ readOnlyHooks: true,
846
+ environment: runtimeEnvironment,
847
+ }),
848
+ ]);
849
+ return projectTuiHooks([
850
+ ...settings,
851
+ ...pluginResources.settings,
852
+ ...(cli.additionalSettings ? [cli.additionalSettings] : []),
853
+ ]);
854
+ };
855
+ if (hooksOnly) {
856
+ const service = new ClaudeSessionService(options);
857
+ return Object.assign(service, { hookConfiguration });
858
+ }
859
+ if (!provider && !exposeToolRegistry) {
860
+ const service = new ClaudeSessionService(options);
861
+ if (!interactive)
862
+ return service;
863
+ return Object.assign(service, { hookConfiguration });
864
+ }
865
+ const toolProvider = provider ?? {
866
+ model: 'praxis/provider',
867
+ capabilities: {
868
+ streaming: true,
869
+ usage: true,
870
+ tools: true,
871
+ images: true,
872
+ documents: true,
873
+ webSearch: true,
874
+ },
875
+ complete: () => ({
876
+ [Symbol.asyncIterator]: () => ({
877
+ next: async () => {
878
+ throw new Error('A model provider is required to execute model-backed tools');
879
+ },
880
+ }),
881
+ }),
882
+ };
883
+ const hostedToolProvider = exposeToolRegistry && !toolProvider.capabilities.webSearch
884
+ ? {
885
+ ...(toolProvider.model === undefined
886
+ ? {}
887
+ : { model: toolProvider.model }),
888
+ capabilities: { ...toolProvider.capabilities, webSearch: true },
889
+ complete: (request) => toolProvider.complete(request),
890
+ }
891
+ : toolProvider;
892
+ const automaticSettingSources = cli.safeMode || cli.bare ? [] : cli.settingSources;
893
+ const settings = [
894
+ ...(await loadClaudeSettings({
895
+ configRoot,
896
+ cwd,
897
+ ...(cli.bare
898
+ ? { settingSources: [] }
899
+ : cli.settingSources === undefined
900
+ ? {}
901
+ : { settingSources: cli.settingSources }),
902
+ })),
903
+ ...(cli.additionalSettings ? [cli.additionalSettings] : []),
904
+ ];
905
+ const loadedResources = await loadClaudeSharedResources({
906
+ configRoot,
907
+ cwd,
908
+ claudeStatePath,
909
+ ...(automaticSettingSources === undefined
910
+ ? {}
911
+ : { settingSources: automaticSettingSources }),
912
+ });
913
+ const pluginResources = await loadClaudePlugins({
914
+ configRoot,
915
+ cwd,
916
+ pluginDirectories: cli.pluginDirectories,
917
+ pluginUrls: cli.pluginUrls,
918
+ strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
919
+ loadInstalled: !cli.safeMode && !cli.bare,
920
+ environment: runtimeEnvironment,
921
+ });
922
+ for (const plugin of pluginResources.plugins) {
923
+ for (const error of plugin.errors) {
924
+ runtimeEventSink({
925
+ type: 'warning',
926
+ message: `Plugin ${plugin.name} could not be loaded: ${error}`,
927
+ });
928
+ }
929
+ }
930
+ settings.push(...pluginResources.settings);
931
+ const configuredAgent = [...settings]
932
+ .reverse()
933
+ .map((resource) => resource.value &&
934
+ typeof resource.value === 'object' &&
935
+ !Array.isArray(resource.value)
936
+ ? resource.value.agent
937
+ : undefined)
938
+ .find((value) => typeof value === 'string' && value.length > 0);
939
+ const selectedMainAgent = agent ?? configuredAgent;
940
+ const resources = {
941
+ ...loadedResources,
942
+ commands: [...loadedResources.commands, ...pluginResources.commands],
943
+ skills: [...loadedResources.skills, ...pluginResources.skills],
944
+ agents: [
945
+ ...loadedResources.agents,
946
+ ...pluginResources.agents,
947
+ ...cli.inlineAgents,
948
+ ],
949
+ settings: [
950
+ ...loadedResources.settings,
951
+ ...pluginResources.settings,
952
+ ...(cli.additionalSettings ? [cli.additionalSettings] : []),
953
+ ],
954
+ mcp: cli.strictMcpConfig
955
+ ? cli.mcpResources
956
+ : [...loadedResources.mcp, ...pluginResources.mcp, ...cli.mcpResources],
957
+ };
958
+ const extensions = new ClaudeExtensionCatalog(resources, {
959
+ disableSlashCommands: cli.disableSlashCommands,
960
+ });
961
+ const memoryDirectory = cli.safeMode || cli.bare
962
+ ? undefined
963
+ : await resolveClaudeProjectMemoryDirectory({ configRoot, cwd });
964
+ if (memoryDirectory)
965
+ await mkdir(memoryDirectory, { recursive: true });
966
+ const loadContextResources = (runtimeCwd = workspace.cwd()) => loadClaudeContextResources({
967
+ configRoot,
968
+ cwd: runtimeCwd,
969
+ ...(automaticSettingSources === undefined
970
+ ? {}
971
+ : { settingSources: automaticSettingSources }),
972
+ });
973
+ const exposePlanDirectory = interactive &&
974
+ askUser !== undefined &&
975
+ approvePlan !== undefined &&
976
+ ['EnterPlanMode', 'ExitPlanMode'].some((name) => (cli.tools === undefined ||
977
+ cli.tools.includes('default') ||
978
+ cli.tools.includes(name)) &&
979
+ !cli.disallowedTools.includes(name));
980
+ const initialAdditionalDirectories = [
981
+ ...cli.additionalDirectories,
982
+ ...(exposePlanDirectory ? [resolve(configRoot, 'plans')] : []),
983
+ ];
984
+ const sandboxSettings = loadClaudeSandboxSettings({
985
+ resources: settings.filter((resource) => resource.plugin !== true),
986
+ cwd: workspace.cwd(),
987
+ originalCwd: sandboxOriginalCwd ?? workspace.cwd(),
988
+ configRoot,
989
+ homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
990
+ additionalDirectories: initialAdditionalDirectories,
991
+ tempDirectory: claudeSandboxTempDirectory(sandboxEnvironment),
992
+ });
993
+ await claudeSandboxRuntime.initialize(sandboxSettings);
994
+ const sandboxUnavailableReason = claudeSandboxRuntime.unavailableReason();
995
+ if (sandboxUnavailableReason && !sandboxSettings.failIfUnavailable) {
996
+ runtimeEventSink({ type: 'warning', message: sandboxUnavailableReason });
997
+ }
998
+ const permissionAdditionalDirectories = [
999
+ ...new Set([
1000
+ ...initialAdditionalDirectories,
1001
+ ...controls.addDirectories.map((directory) => resolve(cwd, directory)),
1002
+ ...(memoryDirectory ? [memoryDirectory] : []),
1003
+ ]),
1004
+ ];
1005
+ const initialAdditionalReadDirectories = [claudeBackgroundTaskParent(cwd)];
1006
+ const permissionResolverForMode = (permissionMode) => new ClaudeExtensionPermissionResolver(new ClaudePermissionResolver({
1007
+ cwd,
1008
+ cwdProvider: () => workspace.cwd(),
1009
+ configRoot,
1010
+ settings,
1011
+ allowedTools: cli.allowedTools,
1012
+ disallowedTools: cli.disallowedTools,
1013
+ additionalDirectories: permissionAdditionalDirectories,
1014
+ additionalReadDirectories: initialAdditionalReadDirectories,
1015
+ sandbox: claudeSandboxRuntime,
1016
+ permissionMode,
1017
+ ...(isSessionActionApproved ? { isSessionActionApproved } : {}),
1018
+ ...(permissionMode === 'auto'
1019
+ ? {
1020
+ autoClassifier: createClaudeModelAutoClassifier(hostedToolProvider),
1021
+ }
1022
+ : {}),
1023
+ }), extensions);
1024
+ const permissions = permissionResolverForMode(cli.dangerouslySkipPermissions ? 'bypassPermissions' : cli.permissionMode);
1025
+ const localTools = new LocalToolRegistry({
1026
+ cwd,
1027
+ cwdProvider: () => workspace.cwd(),
1028
+ enableReportFindings: exposeToolRegistry,
1029
+ ...(memoryDirectory ? { sharedMemoryDirectory: memoryDirectory } : {}),
1030
+ additionalDirectories: initialAdditionalDirectories,
1031
+ additionalReadDirectories: initialAdditionalReadDirectories,
1032
+ sandbox: claudeSandboxRuntime,
1033
+ homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
1034
+ configRoot,
1035
+ ...(environment ? { environment } : {}),
1036
+ });
1037
+ const runtimeMcpResources = async (candidates) => {
1038
+ const management = new ClaudeMcpManagement({
1039
+ configRoot,
1040
+ cwd: workspace.cwd(),
1041
+ });
1042
+ return filterDisabledMcpResources(candidates, await management.disabled());
1043
+ };
1044
+ const mcpTools = await ClaudeMcpToolRegistry.connect({
1045
+ base: cli.bare
1046
+ ? localTools
1047
+ : new WebToolRegistry({
1048
+ base: localTools,
1049
+ provider: hostedToolProvider,
1050
+ }),
1051
+ resources: await runtimeMcpResources(resources.mcp),
1052
+ reloadResources: async () => {
1053
+ if (cli.strictMcpConfig)
1054
+ return runtimeMcpResources(cli.mcpResources);
1055
+ const refreshed = await loadClaudeSharedResources({
1056
+ configRoot,
1057
+ cwd: workspace.cwd(),
1058
+ claudeStatePath,
1059
+ ...(automaticSettingSources === undefined
1060
+ ? {}
1061
+ : { settingSources: automaticSettingSources }),
1062
+ });
1063
+ return runtimeMcpResources([
1064
+ ...refreshed.mcp,
1065
+ ...pluginResources.mcp,
1066
+ ...cli.mcpResources,
1067
+ ]);
1068
+ },
1069
+ cwd,
1070
+ configRoot,
1071
+ onWarning: (message) => runtimeEventSink({ type: 'warning', message }),
1072
+ onPromptsChanged: (prompts) => extensions.setMcpPrompts(prompts),
1073
+ authenticateServer: async (name) => {
1074
+ const record = await new ClaudeMcpManagement({
1075
+ configRoot,
1076
+ cwd: workspace.cwd(),
1077
+ }).get(name);
1078
+ const server = mcpOAuthServerIdentity(record.name, record.config);
1079
+ const oauth = mcpOauthOptions(record.config);
1080
+ const clientSecret = oauth.clientId
1081
+ ? await new ClaudeMcpOAuthStore({ configRoot }).readClientSecret(server)
1082
+ : undefined;
1083
+ await authenticateMcpServer({
1084
+ configRoot,
1085
+ server,
1086
+ ...oauth,
1087
+ ...(clientSecret ? { clientSecret } : {}),
1088
+ noBrowser: false,
1089
+ write: (message) => process.stdout.write(message),
1090
+ });
1091
+ },
1092
+ ...(onElicitation ? { onElicitation } : {}),
1093
+ eventSink: runtimeEventSink,
1094
+ ...(signal ? { signal } : {}),
1095
+ });
1096
+ let lspTools;
1097
+ try {
1098
+ const permissionApprover = cli.permissionPromptTool
1099
+ ? mcpTools.permissionPrompt(cli.permissionPromptTool)
1100
+ : approveTool;
1101
+ const extensionTools = new ClaudeExtensionToolRegistry(mcpTools, extensions);
1102
+ const lspEnabled = interactive &&
1103
+ !cli.safeMode &&
1104
+ !cli.bare &&
1105
+ pluginResources.lsp.length > 0;
1106
+ lspTools = lspEnabled
1107
+ ? new ClaudeLspToolManager({
1108
+ servers: pluginResources.lsp,
1109
+ cwdProvider: () => workspace.cwd(),
1110
+ roots: () => [
1111
+ workspace.cwd(),
1112
+ ...cli.additionalDirectories,
1113
+ ...(memoryDirectory ? [memoryDirectory] : []),
1114
+ ],
1115
+ environment: runtimeEnvironment,
1116
+ })
1117
+ : undefined;
1118
+ const extensionAndLspTools = lspTools
1119
+ ? lspTools.registry(extensionTools)
1120
+ : extensionTools;
1121
+ const agentToolNames = ['Agent', 'SendMessage'];
1122
+ const taskToolNames = [
1123
+ 'TaskCreate',
1124
+ 'TaskGet',
1125
+ 'TaskList',
1126
+ 'TaskOutput',
1127
+ 'TaskStop',
1128
+ 'TaskUpdate',
1129
+ ];
1130
+ const scheduledToolNames = [
1131
+ 'CronCreate',
1132
+ 'CronDelete',
1133
+ 'CronList',
1134
+ 'ScheduleWakeup',
1135
+ ];
1136
+ const workflowToolNames = ['Workflow'];
1137
+ const worktreeToolNames = ['EnterWorktree', 'ExitWorktree'];
1138
+ const interactiveToolNames = [
1139
+ 'AskUserQuestion',
1140
+ 'EnterPlanMode',
1141
+ 'ExitPlanMode',
1142
+ ];
1143
+ const selectedAgentTools = agentToolNames.filter((name) => (cli.tools === undefined ||
1144
+ cli.tools.includes('default') ||
1145
+ cli.tools.includes(name)) &&
1146
+ !cli.disallowedTools.includes(name));
1147
+ const selectedTaskTools = taskToolNames.filter((name) => cli.sessionPersistence &&
1148
+ !cli.bare &&
1149
+ (cli.tools === undefined ||
1150
+ cli.tools.includes('default') ||
1151
+ cli.tools.includes(name)) &&
1152
+ !cli.disallowedTools.includes(name));
1153
+ const selectedScheduledTools = scheduledToolNames.filter((name) => !cli.bare &&
1154
+ (cli.tools === undefined ||
1155
+ cli.tools.includes('default') ||
1156
+ cli.tools.includes(name)) &&
1157
+ !cli.disallowedTools.includes(name));
1158
+ const selectedWorkflowTools = workflowToolNames.filter((name) => (runtimeSettings?.workflows ?? true) &&
1159
+ cli.sessionPersistence &&
1160
+ !cli.bare &&
1161
+ (cli.tools === undefined ||
1162
+ cli.tools.includes('default') ||
1163
+ cli.tools.includes(name)) &&
1164
+ !cli.disallowedTools.includes(name));
1165
+ const selectedWorktreeTools = worktreeToolNames.filter((name) => !cli.bare &&
1166
+ (cli.tools === undefined ||
1167
+ cli.tools.includes('default') ||
1168
+ cli.tools.includes(name)) &&
1169
+ !cli.disallowedTools.includes(name));
1170
+ const selectedInteractiveTools = interactiveToolNames.filter((name) => interactive &&
1171
+ (cli.tools === undefined ||
1172
+ cli.tools.includes('default') ||
1173
+ cli.tools.includes(name)) &&
1174
+ !cli.disallowedTools.includes(name));
1175
+ const enableBackgroundBash = cli.sessionPersistence &&
1176
+ !cli.bare &&
1177
+ (cli.tools === undefined ||
1178
+ cli.tools.includes('default') ||
1179
+ cli.tools.includes('Bash')) &&
1180
+ !cli.disallowedTools.includes('Bash');
1181
+ const selectedTaskRuntimeTools = [
1182
+ ...selectedTaskTools,
1183
+ ...(enableBackgroundBash ? ['Bash'] : []),
1184
+ ];
1185
+ const routedSubagentTools = [
1186
+ ...selectedAgentTools,
1187
+ ...selectedTaskTools.filter((name) => name === 'TaskOutput' || name === 'TaskStop'),
1188
+ ];
1189
+ const selectedBaseTools = cli.tools?.filter((name) => !agentToolNames.includes(name) &&
1190
+ !taskToolNames.includes(name) &&
1191
+ !scheduledToolNames.includes(name) &&
1192
+ !workflowToolNames.includes(name) &&
1193
+ !worktreeToolNames.includes(name) &&
1194
+ !interactiveToolNames.includes(name) &&
1195
+ (!cli.bare || (name !== 'WebFetch' && name !== 'WebSearch')));
1196
+ const filteredTools = new FilteredToolRegistry(extensionAndLspTools, {
1197
+ ...(cli.tools === undefined
1198
+ ? cli.bare
1199
+ ? { tools: ['Bash', 'Edit', 'Read'] }
1200
+ : {}
1201
+ : { tools: selectedBaseTools ?? [] }),
1202
+ disallowedTools: cli.disallowedTools,
1203
+ });
1204
+ const enableSubagents = !cli.bare && selectedAgentTools.length > 0;
1205
+ const hooks = cli.safeMode || cli.bare
1206
+ ? undefined
1207
+ : new ClaudeHookRunner({
1208
+ settings,
1209
+ cwd,
1210
+ onEvent: (event) => runtimeEventSink({ type: 'hook', event }),
1211
+ });
1212
+ const interactiveTools = selectedInteractiveTools.length > 0 && askUser && approvePlan
1213
+ ? new ClaudeInteractiveToolManager({
1214
+ configRoot,
1215
+ initialMode: cli.dangerouslySkipPermissions
1216
+ ? 'bypassPermissions'
1217
+ : cli.permissionMode,
1218
+ enabledTools: selectedInteractiveTools,
1219
+ callbacks: { askUser, approvePlan },
1220
+ permissionResolverForMode,
1221
+ settings: {
1222
+ useAutoModeDuringPlan: runtimeSettings?.useAutoModeDuringPlan ?? true,
1223
+ },
1224
+ })
1225
+ : undefined;
1226
+ const service = new ClaudeSessionService({
1227
+ ...options,
1228
+ provider: hostedToolProvider,
1229
+ ...(providerForModel ? { providerForModel } : {}),
1230
+ ...(providerForMainModel ? { providerForMainModel } : {}),
1231
+ tools: filteredTools,
1232
+ mcp: mcpTools,
1233
+ permissions,
1234
+ permissionResolverForMode,
1235
+ permissionMode: cli.dangerouslySkipPermissions
1236
+ ? 'bypassPermissions'
1237
+ : cli.permissionMode,
1238
+ persistPermissionUpdates: (updates) => persistTuiPermissionUpdates({
1239
+ cwd: workspace.cwd(),
1240
+ configRoot,
1241
+ updates,
1242
+ }),
1243
+ extensions,
1244
+ enableSubagents,
1245
+ subagentToolNames: routedSubagentTools,
1246
+ taskToolNames: selectedTaskRuntimeTools,
1247
+ scheduledToolNames: selectedScheduledTools,
1248
+ enableDynamicWakeups: interactive,
1249
+ enableWorkflows: selectedWorkflowTools.length > 0,
1250
+ emitToolUseSummaries,
1251
+ enableWorktrees: cli.worktreeRequested || selectedWorktreeTools.length > 0,
1252
+ worktreeToolNames: selectedWorktreeTools,
1253
+ ...(runtimeSettings
1254
+ ? { worktreeBaseRef: runtimeSettings.worktreeBaseRef }
1255
+ : {}),
1256
+ ...(interactiveTools ? { interactiveTools } : {}),
1257
+ ...(hooks ? { hooks } : {}),
1258
+ ...(selectedMainAgent ? { agent: selectedMainAgent } : {}),
1259
+ contextAssembler: new ClaudeContextAssembler({
1260
+ loadResources: loadContextResources,
1261
+ loadDynamicContext: (runtimeCwd = workspace.cwd()) => loadClaudeDynamicContext({
1262
+ cwd: runtimeCwd,
1263
+ ...(memoryDirectory ? { memoryDirectory } : {}),
1264
+ }),
1265
+ excludeDynamicSystemPromptSections: cli.excludeDynamicSystemPromptSections,
1266
+ ...(cli.systemPrompt === undefined
1267
+ ? {}
1268
+ : { systemPrompt: cli.systemPrompt }),
1269
+ ...(effectiveAppendSystemPrompt
1270
+ ? { appendSystemPrompt: effectiveAppendSystemPrompt }
1271
+ : {}),
1272
+ }),
1273
+ conditionalRuleResolver: new ClaudeConditionalRuleResolver({
1274
+ loadResources: loadContextResources,
1275
+ }),
1276
+ ...('contextReserveTokens' in context
1277
+ ? { contextReserveTokens: context.contextReserveTokens }
1278
+ : {}),
1279
+ ...(permissionApprover ? { approveTool: permissionApprover } : {}),
1280
+ ...(approveRecovery ? { approveRecovery } : {}),
1281
+ fileCheckpointing: runtimeEnvironment.CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING ===
1282
+ 'true' || runtimeSettings?.checkpoints === true,
1283
+ autoCompact: runtimeSettings?.autoCompact ?? true,
1284
+ fileRewindRoots: [
1285
+ ...cli.additionalDirectories,
1286
+ ...(memoryDirectory ? [memoryDirectory] : []),
1287
+ ],
1288
+ });
1289
+ const toolNames = [
1290
+ ...filteredTools.definitions().map((definition) => definition.name),
1291
+ ...(cli.brief && !cli.disallowedTools.includes('SendUserMessage')
1292
+ ? ['SendUserMessage']
1293
+ : []),
1294
+ ...selectedTaskTools,
1295
+ ...selectedScheduledTools,
1296
+ ...selectedWorkflowTools,
1297
+ ...selectedWorktreeTools,
1298
+ ...selectedInteractiveTools,
1299
+ ...(enableSubagents ? selectedAgentTools : []),
1300
+ ];
1301
+ const runtimeInfo = {
1302
+ cwd: workspace.cwd(),
1303
+ model: provider?.model ?? runtimeEnvironment.PRAXIS_MODEL ?? 'unknown',
1304
+ ...(provider?.capabilities.contextWindowTokens === undefined
1305
+ ? {}
1306
+ : { contextWindowTokens: provider.capabilities.contextWindowTokens }),
1307
+ ...(provider?.capabilities.maxOutputTokens === undefined
1308
+ ? {}
1309
+ : { maxOutputTokens: provider.capabilities.maxOutputTokens }),
1310
+ tools: toolNames,
1311
+ mcpServers: mcpTools.serverStatuses(),
1312
+ permissionMode: cli.dangerouslySkipPermissions
1313
+ ? 'bypassPermissions'
1314
+ : cli.permissionMode,
1315
+ slashCommands: extensions
1316
+ .modelInvocableSkills()
1317
+ .map((definition) => definition.name)
1318
+ .concat(extensions.mcpPromptNames()),
1319
+ agents: extensions.agentNames(),
1320
+ skills: extensions
1321
+ .modelInvocableSkills()
1322
+ .map((definition) => definition.name),
1323
+ plugins: pluginResources.plugins
1324
+ .filter((plugin) => plugin.enabled && plugin.errors.length === 0)
1325
+ .map((plugin) => ({ name: plugin.name, path: plugin.path })),
1326
+ claudeCodeVersion: claudeVersion,
1327
+ };
1328
+ const toolRegistry = exposeToolRegistry
1329
+ ? service.createHostedToolRegistry(randomUUID())
1330
+ : filteredTools;
1331
+ let pendingResumeSessionAt = cli.resumeSessionAt;
1332
+ return {
1333
+ toolRegistry,
1334
+ run: (prompt, signal, sessionId, name, images, documents) => service.run(prompt, signal, sessionId, name ?? cli.name, images, documents),
1335
+ resume: (sessionId, prompt, signal, name, images, documents) => {
1336
+ const resumeSessionAt = pendingResumeSessionAt;
1337
+ pendingResumeSessionAt = undefined;
1338
+ return service.resume(sessionId, prompt, signal, name ?? cli.name, images, documents, resumeSessionAt);
1339
+ },
1340
+ runShell: (command, signal, sessionId, name) => service.runShell(command, signal, sessionId, name ?? cli.name),
1341
+ resumeShell: (sessionId, command, signal, name) => {
1342
+ const resumeSessionAt = pendingResumeSessionAt;
1343
+ pendingResumeSessionAt = undefined;
1344
+ return service.resumeShell(sessionId, command, signal, name ?? cli.name, resumeSessionAt);
1345
+ },
1346
+ fork: (sessionId, targetSessionId, requestedResumeSessionAt) => {
1347
+ const resumeSessionAt = requestedResumeSessionAt ?? pendingResumeSessionAt;
1348
+ pendingResumeSessionAt = undefined;
1349
+ return service.fork(sessionId, targetSessionId, resumeSessionAt);
1350
+ },
1351
+ ensureFork: (sessionId, targetSessionId, checkpoint) => service.ensureFork(sessionId, targetSessionId, checkpoint),
1352
+ setPermissionMode: (sessionId, permissionMode) => service.setPermissionMode(sessionId, permissionMode),
1353
+ rewindFiles: (sessionId, userMessageId) => service.rewindFiles(sessionId, userMessageId),
1354
+ rewindPoints: (sessionId) => service.rewindPoints(sessionId),
1355
+ changeCwd: (sessionId, cwd) => service.changeCwd(sessionId, cwd),
1356
+ recordCdUsage: (sessionId) => service.recordCdUsage(sessionId),
1357
+ approveRecentlyDenied: (sessionId, display) => service.approveRecentlyDenied(sessionId, display),
1358
+ retryRecentlyDenied: (sessionId, display, retrySignal) => service.retryRecentlyDenied(sessionId, display, retrySignal),
1359
+ answerSideQuestion: (sessionId, question, sideSignal, onDelta, permissionMode) => service.answerSideQuestion(sessionId, question, sideSignal, onDelta, permissionMode),
1360
+ recordBtwUsage: (sessionId, permissionMode) => service.recordBtwUsage(sessionId, permissionMode),
1361
+ recordColorUsage: (sessionId, selection, display, permissionMode, options) => service.recordColorUsage(sessionId, selection, display, permissionMode, options),
1362
+ agentColor: (sessionId) => service.readEffectiveAgentColor(sessionId),
1363
+ recordBackgroundUsage: (sessionId, permissionMode) => service.recordBackgroundUsage(sessionId, permissionMode),
1364
+ recordBackgroundLaunch: (sessionId) => service.recordBackgroundLaunch(sessionId),
1365
+ forkSideQuestion: (sessionId, question, sideSignal) => service.forkSideQuestion(sessionId, question, sideSignal),
1366
+ lifecycle: async (trigger, lifecycleOptions = {}) => {
1367
+ if (!hooks)
1368
+ return;
1369
+ const sessionId = lifecycleOptions.sessionId ?? randomUUID();
1370
+ const runtimeCwd = workspace.cwd();
1371
+ const hookSession = {
1372
+ session_id: sessionId,
1373
+ transcript_path: resolveClaudePaths({
1374
+ cwd: runtimeCwd,
1375
+ sessionId,
1376
+ configDir: configRoot,
1377
+ }).sessionFile,
1378
+ cwd: runtimeCwd,
1379
+ permission_mode: cli.dangerouslySkipPermissions
1380
+ ? 'bypassPermissions'
1381
+ : cli.permissionMode,
1382
+ };
1383
+ const setup = await hooks.run({ ...hookSession, hook_event_name: 'Setup', trigger }, trigger, signal);
1384
+ if (setup.blockedReason) {
1385
+ throw new Error(`Setup hook error: ${setup.blockedReason}`);
1386
+ }
1387
+ if (!lifecycleOptions.sessionStart)
1388
+ return;
1389
+ const startup = await hooks.run({
1390
+ ...hookSession,
1391
+ hook_event_name: 'SessionStart',
1392
+ source: 'startup',
1393
+ }, 'startup', signal);
1394
+ if (startup.blockedReason) {
1395
+ throw new Error(`SessionStart hook error: ${startup.blockedReason}`);
1396
+ }
1397
+ },
1398
+ sessions: () => service.sessions(),
1399
+ costSnapshot: (sessionId) => service.costSnapshot(sessionId),
1400
+ slashCommands: () => extensions.slashCommandDefinitions().map((definition) => ({
1401
+ name: definition.name,
1402
+ description: definition.description,
1403
+ source: definition.builtin === true ? 'builtin' : definition.kind,
1404
+ ...(definition.progressMessage === undefined
1405
+ ? {}
1406
+ : { progressMessage: definition.progressMessage }),
1407
+ })),
1408
+ hookConfiguration: async () => projectTuiHooks(settings),
1409
+ mcpInspect: () => service.mcpInspect(),
1410
+ mcpReconnect: (name) => service.mcpReconnect(name),
1411
+ mcpAuthenticate: (name) => service.mcpAuthenticate(name),
1412
+ mcpReload: () => service.mcpReload(),
1413
+ mcpTools: (name) => service.mcpTools(name),
1414
+ taskSnapshots: (sessionId) => service.taskSnapshots(sessionId),
1415
+ stopTask: (sessionId, taskId) => service.stopTask(sessionId, taskId),
1416
+ agentDefinitions: () => extensions.agentDefinitions(),
1417
+ initialAgentPrompt: () => selectedMainAgent
1418
+ ? extensions.agent(selectedMainAgent)?.initialPrompt
1419
+ : undefined,
1420
+ inspect: (sessionId) => service.inspect(sessionId),
1421
+ export: (sessionId) => service.export(sessionId),
1422
+ transcript: (sessionId) => service.transcript(sessionId, pendingResumeSessionAt),
1423
+ compact: (sessionId, signal, selection) => service.compact(sessionId, signal, selection),
1424
+ rename: (sessionId, name) => service.rename(sessionId, name),
1425
+ sessionNameSuggestion: (sessionId, signal) => service.sessionNameSuggestion(sessionId, signal),
1426
+ nextScheduledPrompt: (signal) => service.nextScheduledPrompt(signal),
1427
+ close: async () => {
1428
+ let failure;
1429
+ try {
1430
+ await service.close();
1431
+ }
1432
+ catch (error) {
1433
+ failure = error;
1434
+ }
1435
+ try {
1436
+ await mcpTools.close();
1437
+ }
1438
+ catch (error) {
1439
+ failure ??= error;
1440
+ }
1441
+ try {
1442
+ await lspTools?.close();
1443
+ }
1444
+ catch (error) {
1445
+ failure ??= error;
1446
+ }
1447
+ await debug?.close();
1448
+ if (failure !== undefined)
1449
+ throw failure;
1450
+ },
1451
+ runtimeInfo: () => {
1452
+ // Drop the static capability snapshot so a fallback-routed provider
1453
+ // never reports stale context/output limits, then re-derive them from
1454
+ // the currently active provider.
1455
+ const staticInfo = {
1456
+ ...runtimeInfo,
1457
+ cwd: workspace.cwd(),
1458
+ model: service.model() ??
1459
+ provider?.model ??
1460
+ runtimeEnvironment.PRAXIS_MODEL ??
1461
+ 'unknown',
1462
+ };
1463
+ delete staticInfo.contextWindowTokens;
1464
+ delete staticInfo.maxOutputTokens;
1465
+ return {
1466
+ ...staticInfo,
1467
+ ...(provider?.capabilities.contextWindowTokens === undefined
1468
+ ? {}
1469
+ : {
1470
+ contextWindowTokens: provider.capabilities.contextWindowTokens,
1471
+ }),
1472
+ ...(provider?.capabilities.maxOutputTokens === undefined
1473
+ ? {}
1474
+ : { maxOutputTokens: provider.capabilities.maxOutputTokens }),
1475
+ };
1476
+ },
1477
+ promptSuggestion: (sessionId, suggestionSignal) => service.promptSuggestion(sessionId, suggestionSignal),
1478
+ };
1479
+ }
1480
+ catch (error) {
1481
+ try {
1482
+ await lspTools?.close();
1483
+ }
1484
+ catch {
1485
+ // Preserve the service-construction failure as the primary error.
1486
+ }
1487
+ try {
1488
+ await mcpTools.close();
1489
+ }
1490
+ catch {
1491
+ // Preserve the service-construction failure as the primary error.
1492
+ }
1493
+ finally {
1494
+ await debug?.close();
1495
+ }
1496
+ throw error;
1497
+ }
1498
+ };
1499
+ const createDefaultAutoModeCritic = async ({ model }) => {
1500
+ const apiKey = process.env.PRAXIS_API_KEY;
1501
+ const selectedModel = model ?? process.env.PRAXIS_MODEL;
1502
+ if (!apiKey || !selectedModel) {
1503
+ throw new Error('PRAXIS_API_KEY and a model (--model or PRAXIS_MODEL) are required');
1504
+ }
1505
+ return createProviderForModel(apiKey, parseProviderEnvironment(process.env), parseContextEnvironment(process.env), {}, {})(selectedModel);
1506
+ };
1507
+ const defaultPluginEvalRuntimeFactory = {
1508
+ create: async (options) => {
1509
+ let turns = 0;
1510
+ let historySessionId;
1511
+ if (options.historyFile) {
1512
+ const source = await readFile(options.historyFile, 'utf8');
1513
+ for (const line of source.split(/\r?\n/u)) {
1514
+ if (!line.trim())
1515
+ continue;
1516
+ let entry;
1517
+ try {
1518
+ entry = JSON.parse(line);
1519
+ }
1520
+ catch (error) {
1521
+ throw new Error(`Invalid history_file JSONL: ${options.historyFile}`, {
1522
+ cause: error,
1523
+ });
1524
+ }
1525
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
1526
+ throw new Error(`Invalid history_file entry: ${options.historyFile}`);
1527
+ const candidate = entry.sessionId;
1528
+ if (typeof candidate === 'string' && isClaudeSessionId(candidate)) {
1529
+ historySessionId ??= candidate;
1530
+ }
1531
+ }
1532
+ if (!historySessionId)
1533
+ throw new Error('history_file must contain a Claude sessionId');
1534
+ const sessionFile = resolveClaudePaths({
1535
+ cwd: options.cwd,
1536
+ sessionId: historySessionId,
1537
+ configDir: options.configRoot,
1538
+ }).sessionFile;
1539
+ await mkdir(dirname(sessionFile), { recursive: true });
1540
+ await copyFile(options.historyFile, sessionFile);
1541
+ }
1542
+ const service = await createDefaultService({
1543
+ eventSink: (event) => {
1544
+ if (event.type === 'state' && event.state === 'awaiting-model')
1545
+ turns += 1;
1546
+ options.eventSink(event);
1547
+ },
1548
+ requireProvider: true,
1549
+ cwd: options.cwd,
1550
+ configRoot: options.configRoot,
1551
+ environment: {
1552
+ ...options.env,
1553
+ HOME: options.home,
1554
+ USERPROFILE: options.home,
1555
+ },
1556
+ providerEnvironment: process.env,
1557
+ isSessionActionApproved: (call) => options.allowedTools.includes(call.name),
1558
+ controls: {
1559
+ ...DEFAULT_CLI_CONTROLS,
1560
+ sessionPersistence: false,
1561
+ maxTurns: options.maxTurns,
1562
+ pluginDirectories: [...options.pluginDirectories],
1563
+ addDirectories: [...options.addDirs],
1564
+ allowedTools: [...options.allowedTools],
1565
+ disallowedTools: [],
1566
+ tools: [...options.allowedTools],
1567
+ permissionMode: 'dontAsk',
1568
+ ...(options.model ? { model: options.model } : {}),
1569
+ ...(options.appendSystemPrompt
1570
+ ? { appendSystemPrompt: options.appendSystemPrompt }
1571
+ : {}),
1572
+ },
1573
+ });
1574
+ return {
1575
+ run: async (prompt, signal) => {
1576
+ const result = historySessionId
1577
+ ? await service.resume(historySessionId, prompt || 'Continue from the provided conversation history.', signal)
1578
+ : await service.run(prompt, signal);
1579
+ return {
1580
+ text: result.text,
1581
+ turns,
1582
+ ...(result.costUsd === undefined
1583
+ ? {}
1584
+ : { costUsd: result.costUsd }),
1585
+ };
1586
+ },
1587
+ close: () => service.close?.() ?? Promise.resolve(),
1588
+ };
1589
+ },
1590
+ };
1591
+ const defaultPluginEvalJudge = {
1592
+ vote: async ({ criteria, focus, baseline, model, signal }) => {
1593
+ const environment = process.env;
1594
+ const apiKey = environment.PRAXIS_API_KEY;
1595
+ if (!apiKey)
1596
+ throw new Error('PRAXIS_API_KEY is required for paid eval graders');
1597
+ const provider = createProviderForModel(apiKey, parseProviderEnvironment(environment), parseContextEnvironment(environment), {}, {})(model);
1598
+ const prompt = `You are an eval judge. Return only JSON matching {"passed":boolean,"explanation":string}.
1599
+
1600
+ Criteria:
1601
+ ${criteria}
1602
+
1603
+ Candidate:
1604
+ ${focus}${baseline === undefined ? '' : `\n\nBaseline:\n${baseline}`}`;
1605
+ let text = '';
1606
+ let usage;
1607
+ for await (const event of provider.complete({
1608
+ messages: [{ role: 'user', content: prompt }],
1609
+ ...(signal ? { signal } : {}),
1610
+ })) {
1611
+ if (event.type === 'text-delta')
1612
+ text += event.delta;
1613
+ else if (event.type === 'usage')
1614
+ usage = event.usage;
1615
+ }
1616
+ const object = /\{[\s\S]*\}/u.exec(text)?.[0];
1617
+ if (!object)
1618
+ throw new Error('Eval judge returned no JSON object');
1619
+ let parsed;
1620
+ try {
1621
+ parsed = JSON.parse(object);
1622
+ }
1623
+ catch (error) {
1624
+ throw new Error('Eval judge returned invalid JSON', { cause: error });
1625
+ }
1626
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1627
+ throw new Error('Eval judge returned an invalid result');
1628
+ const result = parsed;
1629
+ if (typeof result.passed !== 'boolean' ||
1630
+ (result.explanation !== undefined &&
1631
+ typeof result.explanation !== 'string'))
1632
+ throw new Error('Eval judge result must contain passed and explanation');
1633
+ if (!usage)
1634
+ throw new Error('Eval judge provider returned no usage');
1635
+ const pricing = ModelPricingRegistry.fromEnvironment(environment.PRAXIS_PRICING_JSON).resolve(model);
1636
+ if (!pricing)
1637
+ throw new Error(`No pricing configured for eval judge model ${model}`);
1638
+ return {
1639
+ passed: result.passed,
1640
+ ...(typeof result.explanation === 'string'
1641
+ ? { explanation: result.explanation }
1642
+ : {}),
1643
+ costUsd: usageCostUsd(usage, pricing),
1644
+ };
1645
+ },
1646
+ };
1647
+ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta.url)) {
1648
+ const dependencies = {
1649
+ createService: createDefaultService,
1650
+ createAutoModeCritic: createDefaultAutoModeCritic,
1651
+ pluginEval: {
1652
+ runtimeFactory: defaultPluginEvalRuntimeFactory,
1653
+ judge: defaultPluginEvalJudge,
1654
+ },
1655
+ cliPath: entrypoint,
1656
+ runInteractive: async ({ agent, controls, initialPrompt, resume, signal, }) => {
1657
+ const { runInteractive } = await import('./cli/interactive.js');
1658
+ const interactiveControls = controls ?? DEFAULT_CLI_CONTROLS;
1659
+ const initialAdditionalDirectories = interactiveControls.addDirectories.map((directory) => realpathSync(resolve(process.cwd(), directory)));
1660
+ return runInteractive({
1661
+ factory: {
1662
+ createService: ({ additionalDirectories, cwd, ...options }) => createDefaultService({
1663
+ ...options,
1664
+ ...(cwd === undefined ? {} : { cwd }),
1665
+ sandboxOriginalCwd: process.cwd(),
1666
+ ...(agent === undefined ? {} : { agent }),
1667
+ controls: {
1668
+ ...interactiveControls,
1669
+ addDirectories: additionalDirectories ?? interactiveControls.addDirectories,
1670
+ },
1671
+ interactive: true,
1672
+ }),
1673
+ scheduledPrompts: Boolean(process.env.PRAXIS_API_KEY &&
1674
+ (interactiveControls.model ?? process.env.PRAXIS_MODEL)),
1675
+ },
1676
+ ...(signal ? { signal } : {}),
1677
+ ...(initialPrompt === undefined ? {} : { initialPrompt }),
1678
+ ...(controls?.axScreenReader ? { axScreenReader: true } : {}),
1679
+ ...(controls?.allowDangerouslySkipPermissions
1680
+ ? { allowDangerouslySkipPermissions: true }
1681
+ : {}),
1682
+ additionalDirectories: initialAdditionalDirectories,
1683
+ ...(interactiveControls.settingSources === undefined
1684
+ ? {}
1685
+ : { settingSources: interactiveControls.settingSources }),
1686
+ display: {
1687
+ version: VERSION,
1688
+ cwd: process.cwd(),
1689
+ ...(controls?.model
1690
+ ? { model: controls.model }
1691
+ : process.env.PRAXIS_MODEL
1692
+ ? { model: process.env.PRAXIS_MODEL }
1693
+ : {}),
1694
+ effort: controls?.effort ?? 'high',
1695
+ permissionMode: controls?.dangerouslySkipPermissions
1696
+ ? 'bypassPermissions'
1697
+ : (controls?.permissionMode ?? 'default'),
1698
+ },
1699
+ onBackground: (request) => requireTopLevelAgentManager(dependencies).launch({
1700
+ prompt: request.prompt,
1701
+ initialDetail: request.detail,
1702
+ sourceSessionId: request.sourceSessionId,
1703
+ sourceCheckpoint: request.sourceCheckpoint,
1704
+ cwd: request.cwd,
1705
+ deferInitialTurn: true,
1706
+ argv: agentDashboardWorkerArgv({
1707
+ ...interactiveControls,
1708
+ agent,
1709
+ }),
1710
+ }),
1711
+ onBackgrounded: (result) => {
1712
+ process.stdout.write(backgroundLaunchMessage(result.id));
1713
+ },
1714
+ ...(resume === undefined ? {} : { resume }),
1715
+ ...(resume?.fromPr !== undefined
1716
+ ? {
1717
+ sessionFilter: createClaudePrSessionFilter(resume.fromPr),
1718
+ requireSession: true,
1719
+ }
1720
+ : resume?.sessionSelector === undefined
1721
+ ? {}
1722
+ : {
1723
+ sessionFilter: createResumeSessionFilter(resume.sessionSelector),
1724
+ requireSession: true,
1725
+ missingSessionMessage: isClaudeSessionId(resume.sessionSelector)
1726
+ ? `No conversation found with session ID: ${resume.sessionSelector}`
1727
+ : `No conversation found matching: ${resume.sessionSelector}`,
1728
+ }),
1729
+ ...(resume?.requireSession ? { requireSession: true } : {}),
1730
+ });
1731
+ },
1732
+ runAgentsDashboard: async ({ manager, defaults, signal }) => {
1733
+ const { runAgentsDashboard } = await import('./cli/agents-dashboard.js');
1734
+ return runAgentsDashboard({
1735
+ manager,
1736
+ defaults,
1737
+ ...(signal ? { signal } : {}),
1738
+ });
1739
+ },
1740
+ topLevelAgents: new TopLevelAgentManager({
1741
+ configRoot: resolve(process.env.CLAUDE_CONFIG_DIR ?? resolve(homedir(), '.claude')),
1742
+ cwd: process.cwd(),
1743
+ cliPath: entrypoint,
1744
+ version: VERSION,
1745
+ }),
1746
+ launchTmux: launchTmuxWorktree,
1747
+ selfUpdate: runSelfUpdate,
1748
+ };
1749
+ return dependencies;
1750
+ }
1751
+ export async function createBackgroundWorkerRuntime(workerSink, dispatch, createService = createDefaultService) {
1752
+ const invocation = parseCliInvocation(dispatch.argv);
1753
+ return createService({
1754
+ eventSink: workerSink,
1755
+ requireProvider: true,
1756
+ ...(invocation.retryInterruptedTools
1757
+ ? { approveRecovery: () => true }
1758
+ : {}),
1759
+ ...(invocation.agent ? { agent: invocation.agent } : {}),
1760
+ controls: invocation,
1761
+ sessionKind: 'bg',
1762
+ });
1763
+ }
1764
+ async function runBackgroundWorker(id) {
1765
+ const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? resolve(homedir(), '.claude'));
1766
+ await runTopLevelAgentWorker({
1767
+ configRoot,
1768
+ id,
1769
+ createRuntime: (workerSink, dispatch) => createBackgroundWorkerRuntime(workerSink, dispatch),
1770
+ });
1771
+ }
1772
+ function writeJson(io, value) {
1773
+ io.stdout(`${JSON.stringify(value)}\n`);
1774
+ }
1775
+ function formatSessionIssue(issue) {
1776
+ return issue
1777
+ ? `line ${issue.lineNumber}, byte ${issue.byteOffset}: ${issue.message}`
1778
+ : '';
1779
+ }
1780
+ function selectPrLinkedSession(sessions, selector) {
1781
+ const matches = filterClaudePrLinkedSessions(sessions, selector);
1782
+ const label = selector === true ? 'a pull request' : `PR ${selector}`;
1783
+ if (matches.length === 0) {
1784
+ throw new Error(`No conversation linked to ${label} in this project`);
1785
+ }
1786
+ if (matches.length > 1) {
1787
+ throw new Error(`Multiple conversations are linked to ${label}; use --resume with one of: ${matches
1788
+ .map((session) => session.sessionId)
1789
+ .join(', ')}`);
1790
+ }
1791
+ return matches[0];
1792
+ }
1793
+ function selectImplicitResumeSession(sessions, fromPr) {
1794
+ if (fromPr !== undefined)
1795
+ return selectPrLinkedSession(sessions, fromPr);
1796
+ const latest = sessions[0];
1797
+ if (!latest)
1798
+ throw new Error('No conversation found to continue');
1799
+ return latest;
1800
+ }
1801
+ function missingResumeSelectorMessage(invocation) {
1802
+ if (invocation.print) {
1803
+ return '--resume requires a valid session ID or session title when used with --print. Usage: praxis -p --resume <session-id|title>';
1804
+ }
1805
+ if (invocation.background) {
1806
+ return '--resume requires a valid session ID or session title when used with --background';
1807
+ }
1808
+ return '--resume requires a valid session ID or session title outside an interactive terminal';
1809
+ }
1810
+ function selectResumeSession(sessions, selector, invocation) {
1811
+ const normalized = selector.toLowerCase();
1812
+ const idMatch = sessions.find((session) => session.sessionId.toLowerCase() === normalized);
1813
+ if (idMatch)
1814
+ return idMatch;
1815
+ if (isClaudeSessionId(selector)) {
1816
+ throw new Error(`No conversation found with session ID: ${selector}`);
1817
+ }
1818
+ const titleMatches = sessions.filter((session) => session.name?.toLowerCase() === normalized);
1819
+ if (titleMatches.length === 1)
1820
+ return titleMatches[0];
1821
+ if (titleMatches.length > 1) {
1822
+ throw new Error(`--resume "${selector}" matches ${titleMatches.length} sessions. Pass one of these session IDs to disambiguate:\n${titleMatches
1823
+ .map((session) => ` ${session.sessionId} (modified ${session.updatedAt})`)
1824
+ .join('\n')}`);
1825
+ }
1826
+ throw new Error(`${missingResumeSelectorMessage(invocation)}. Provided value "${selector}" is not a UUID and does not match any session title.`);
1827
+ }
1828
+ function createResumeSessionFilter(selector) {
1829
+ const normalized = selector.toLowerCase();
1830
+ if (isClaudeSessionId(selector)) {
1831
+ return (session) => session.sessionId.toLowerCase() === normalized;
1832
+ }
1833
+ return (session) => [session.name, session.lastPrompt, session.sessionId].some((value) => value?.toLowerCase().includes(normalized));
1834
+ }
1835
+ function mcpOutput(io, invocation, value) {
1836
+ if (invocation.legacyJson || invocation.outputFormat !== 'text') {
1837
+ writeJson(io, value);
1838
+ }
1839
+ else if (typeof value === 'string') {
1840
+ io.stdout(`${value}\n`);
1841
+ }
1842
+ else {
1843
+ io.stdout(`${JSON.stringify(value)}\n`);
1844
+ }
1845
+ }
1846
+ function mcpEnvironment(values) {
1847
+ const environment = {};
1848
+ for (const value of values) {
1849
+ const [name, ...parts] = value.split('=');
1850
+ if (!name || parts.length === 0) {
1851
+ throw new Error(`Invalid environment variable format: ${value}, environment variables should be added as: -e KEY1=value1 -e KEY2=value2`);
1852
+ }
1853
+ environment[name] = parts.join('=');
1854
+ }
1855
+ return environment;
1856
+ }
1857
+ function mcpHeaders(values) {
1858
+ if (values.length === 0)
1859
+ return undefined;
1860
+ const headers = {};
1861
+ for (const value of values) {
1862
+ const separator = value.indexOf(':');
1863
+ if (separator < 0) {
1864
+ throw new Error(`Invalid header format: "${value}". Expected format: "Header-Name: value"`);
1865
+ }
1866
+ const name = value.slice(0, separator).trim();
1867
+ if (!name) {
1868
+ throw new Error(`Invalid header: "${value}". Header name cannot be empty.`);
1869
+ }
1870
+ headers[name] = value.slice(separator + 1).trim();
1871
+ }
1872
+ return headers;
1873
+ }
1874
+ function mcpCallbackPort(value) {
1875
+ if (value === undefined)
1876
+ return undefined;
1877
+ const port = Number.parseInt(value, 10);
1878
+ if (!port)
1879
+ return undefined;
1880
+ if (port < 0)
1881
+ throw new Error('Invalid configuration: : Invalid input');
1882
+ return port;
1883
+ }
1884
+ function mcpOauthOptions(config) {
1885
+ const oauth = config.oauth;
1886
+ if (!oauth || typeof oauth !== 'object' || Array.isArray(oauth))
1887
+ return {};
1888
+ const values = oauth;
1889
+ return {
1890
+ ...(typeof values.clientId === 'string'
1891
+ ? { clientId: values.clientId }
1892
+ : {}),
1893
+ ...(typeof values.callbackPort === 'number'
1894
+ ? { callbackPort: values.callbackPort }
1895
+ : {}),
1896
+ };
1897
+ }
1898
+ function mcpConfigPath(record) {
1899
+ return record.scope === 'local'
1900
+ ? `${record.path} [project: ${process.cwd()}]`
1901
+ : record.path;
1902
+ }
1903
+ function redactMcpHeaders(headers) {
1904
+ return Object.fromEntries(Object.entries(headers).map(([name, value]) => [
1905
+ name,
1906
+ /authorization|api[-_]?key|token|secret|cookie/iu.test(name)
1907
+ ? '[REDACTED]'
1908
+ : value,
1909
+ ]));
1910
+ }
1911
+ function isLikelyMcpUrl(value) {
1912
+ return (value.startsWith('http://') ||
1913
+ value.startsWith('https://') ||
1914
+ value.startsWith('localhost') ||
1915
+ value.endsWith('/sse') ||
1916
+ value.endsWith('/mcp'));
1917
+ }
1918
+ function mcpAddedOutput(io, invocation, record, transport, commandOrUrl, args, headers) {
1919
+ if (invocation.legacyJson || invocation.outputFormat !== 'text') {
1920
+ mcpOutput(io, invocation, {
1921
+ type: 'mcp-added',
1922
+ server: mcpRecordJson(record),
1923
+ });
1924
+ return;
1925
+ }
1926
+ if (transport === 'stdio') {
1927
+ io.stdout(`Added stdio MCP server ${record.name} with command: ${commandOrUrl} ${args.join(' ')} to ${record.scope} config\n`);
1928
+ }
1929
+ else {
1930
+ io.stdout(`Added ${transport.toUpperCase()} MCP server ${record.name} with URL: ${commandOrUrl} to ${record.scope} config\n`);
1931
+ if (headers) {
1932
+ io.stdout(`Headers: ${JSON.stringify(redactMcpHeaders(headers), null, 2)}\n`);
1933
+ }
1934
+ }
1935
+ io.stdout(`File modified: ${mcpConfigPath(record)}\n`);
1936
+ }
1937
+ async function existingMcpServer(management, name, scope) {
1938
+ try {
1939
+ return await management.get(name, scope);
1940
+ }
1941
+ catch (error) {
1942
+ if (error instanceof Error &&
1943
+ error.message === `MCP server not found: ${name}`) {
1944
+ return undefined;
1945
+ }
1946
+ throw error;
1947
+ }
1948
+ }
1949
+ function mcpRecordJson(record) {
1950
+ return {
1951
+ name: record.name,
1952
+ scope: record.scope,
1953
+ path: record.path,
1954
+ config: record.config,
1955
+ };
1956
+ }
1957
+ function autoModeJson(config) {
1958
+ return {
1959
+ allow: config.allow,
1960
+ soft_deny: config.softDeny,
1961
+ hard_deny: config.hardDeny,
1962
+ environment: config.environment,
1963
+ classifyAllShell: config.classifyAllShell,
1964
+ };
1965
+ }
1966
+ function filterAutoModeRules(config, label) {
1967
+ if (label === undefined)
1968
+ return config;
1969
+ const prefix = label.toLowerCase();
1970
+ const filter = (rules) => rules.filter((rule) => rule.toLowerCase().startsWith(prefix));
1971
+ return {
1972
+ ...config,
1973
+ allow: filter(config.allow),
1974
+ soft_deny: filter(config.soft_deny),
1975
+ hard_deny: filter(config.hard_deny),
1976
+ environment: filter(config.environment),
1977
+ };
1978
+ }
1979
+ function hasCustomAutoModeRules(settings) {
1980
+ return settings.some((setting) => {
1981
+ if (!setting.value ||
1982
+ typeof setting.value !== 'object' ||
1983
+ Array.isArray(setting.value)) {
1984
+ return false;
1985
+ }
1986
+ const autoMode = setting.value.autoMode;
1987
+ if (!autoMode || typeof autoMode !== 'object' || Array.isArray(autoMode)) {
1988
+ return false;
1989
+ }
1990
+ const rules = autoMode;
1991
+ return ['allow', 'soft_deny', 'hard_deny', 'environment'].some((key) => Array.isArray(rules[key]) && rules[key].length > 0);
1992
+ });
1993
+ }
1994
+ const AUTO_MODE_CRITIQUE_SYSTEM = `Praxis auto-mode critique. Review custom auto mode rules for ambiguity, coverage gaps, conflicts, risk, and actionable wording. Return concise Markdown only; do not claim to have changed settings.`;
1995
+ const NO_CUSTOM_AUTO_MODE_RULES = `No custom auto mode rules found.
1996
+
1997
+ Add rules to your settings file under autoMode.{allow, soft_deny, hard_deny,
1998
+ environment}.
1999
+ Run \`praxis auto-mode defaults\` to see the default rules for reference.
2000
+ `;
2001
+ async function executeAutoModeCommand(args, invocation, io, dependencies, signal) {
2002
+ const action = args[1];
2003
+ if (!action || action === 'help') {
2004
+ io.stdout(AUTO_MODE_HELP);
2005
+ return 0;
2006
+ }
2007
+ if (invocation.autoModeLabel !== undefined && action !== 'defaults') {
2008
+ throw new Error('--label is only valid with auto-mode defaults');
2009
+ }
2010
+ if (args.length !== 2) {
2011
+ throw new Error(`auto-mode ${action} takes no operands`);
2012
+ }
2013
+ if (action === 'defaults') {
2014
+ const output = filterAutoModeRules(autoModeJson(defaultClaudeAutoModeConfig()), invocation.autoModeLabel);
2015
+ if (invocation.legacyJson || invocation.outputFormat !== 'text')
2016
+ writeJson(io, output);
2017
+ else
2018
+ io.stdout(`${JSON.stringify(output)}\n`);
2019
+ return 0;
2020
+ }
2021
+ if (action === 'critique') {
2022
+ const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? resolve(homedir(), '.claude'));
2023
+ const settings = await loadClaudeSettings({
2024
+ configRoot,
2025
+ cwd: process.cwd(),
2026
+ });
2027
+ if (!hasCustomAutoModeRules(settings)) {
2028
+ io.stdout(NO_CUSTOM_AUTO_MODE_RULES);
2029
+ return 0;
2030
+ }
2031
+ const config = autoModeJson(loadClaudeAutoModeConfig(settings));
2032
+ io.stdout('Analyzing your auto mode rules…\n\n');
2033
+ const provider = await (dependencies.createAutoModeCritic ?? createDefaultAutoModeCritic)({
2034
+ ...(invocation.model === undefined ? {} : { model: invocation.model }),
2035
+ });
2036
+ let critique = '';
2037
+ for await (const event of provider.complete({
2038
+ messages: [
2039
+ { role: 'system', content: AUTO_MODE_CRITIQUE_SYSTEM },
2040
+ {
2041
+ role: 'user',
2042
+ content: `Review this effective auto mode configuration:\n\n${JSON.stringify(config, null, 2)}`,
2043
+ },
2044
+ ],
2045
+ thinking: { mode: 'disabled' },
2046
+ ...(signal ? { signal } : {}),
2047
+ })) {
2048
+ if (event.type !== 'text-delta')
2049
+ continue;
2050
+ critique += event.delta;
2051
+ io.stdout(event.delta);
2052
+ }
2053
+ if (!critique.trim()) {
2054
+ io.stdout('No critique was generated. Please try again.\n');
2055
+ }
2056
+ else if (!critique.endsWith('\n')) {
2057
+ io.stdout('\n');
2058
+ }
2059
+ return 0;
2060
+ }
2061
+ if (action === 'config') {
2062
+ const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? resolve(homedir(), '.claude'));
2063
+ const settings = await loadClaudeSettings({
2064
+ configRoot,
2065
+ cwd: process.cwd(),
2066
+ });
2067
+ const output = autoModeJson(loadClaudeAutoModeConfig(settings));
2068
+ if (invocation.legacyJson || invocation.outputFormat !== 'text')
2069
+ writeJson(io, output);
2070
+ else
2071
+ io.stdout(`${JSON.stringify(output)}\n`);
2072
+ return 0;
2073
+ }
2074
+ throw new Error(`Unknown auto-mode command: ${action}`);
2075
+ }
2076
+ async function executeDoctorCommand(args, invocation, io) {
2077
+ if (args.length !== 1)
2078
+ throw new Error('doctor takes no operands');
2079
+ if (invocation.outputFormat !== 'text' &&
2080
+ invocation.outputFormat !== 'json' &&
2081
+ !invocation.legacyJson) {
2082
+ throw new Error('doctor output format must be text or json');
2083
+ }
2084
+ const configuredRoot = process.env.CLAUDE_CONFIG_DIR || undefined;
2085
+ const configRoot = resolve(configuredRoot ?? resolve(homedir(), '.claude'));
2086
+ const claudeStatePath = configuredRoot
2087
+ ? join(configRoot, '.claude.json')
2088
+ : resolve(homedir(), '.claude.json');
2089
+ const runtimeSettings = await loadRuntimeSettings({
2090
+ configRoot,
2091
+ statePath: claudeStatePath,
2092
+ });
2093
+ const report = await runDoctor({
2094
+ version: VERSION,
2095
+ executablePath: fileURLToPath(import.meta.url),
2096
+ nodeExecutablePath: process.execPath,
2097
+ nodeVersion: process.version,
2098
+ configRoot,
2099
+ claudeStatePath,
2100
+ cwd: process.cwd(),
2101
+ environment: process.env,
2102
+ autoUpdateChannel: runtimeSettings.autoUpdatesChannel,
2103
+ ...(process.argv[1] === undefined
2104
+ ? {}
2105
+ : { invokedBinaryPath: process.argv[1] }),
2106
+ });
2107
+ if (invocation.legacyJson || invocation.outputFormat === 'json') {
2108
+ writeJson(io, report);
2109
+ }
2110
+ else {
2111
+ io.stdout(formatDoctorReport(report));
2112
+ }
2113
+ return report.ok ? 0 : 1;
2114
+ }
2115
+ async function executeSelfUpdateCommand(argv, io, dependencies) {
2116
+ const command = argv[0];
2117
+ const values = argv.slice(1);
2118
+ if (values.includes('--help') || values.includes('-h')) {
2119
+ io.stdout(command === 'install' ? INSTALL_HELP : UPDATE_HELP);
2120
+ return 0;
2121
+ }
2122
+ const json = values.includes('--json');
2123
+ const operands = values.filter((value) => value !== '--json');
2124
+ if (command === 'update' || command === 'upgrade') {
2125
+ if (operands.length > 0) {
2126
+ throw new Error(`${command} takes no operands`);
2127
+ }
2128
+ const runtimeSettings = await loadRuntimeSettings();
2129
+ const result = await dependencies.selfUpdate?.({
2130
+ operation: 'update',
2131
+ ...(runtimeSettings.autoUpdatesChannel === 'latest'
2132
+ ? {}
2133
+ : { target: runtimeSettings.autoUpdatesChannel }),
2134
+ });
2135
+ if (!result)
2136
+ throw new Error('Self-update unavailable');
2137
+ if (json)
2138
+ writeJson(io, result);
2139
+ else
2140
+ io.stdout(`Praxis update completed: ${result.output}\n`);
2141
+ return 0;
2142
+ }
2143
+ let force = false;
2144
+ const targets = [];
2145
+ for (const value of operands) {
2146
+ if (value === '--force')
2147
+ force = true;
2148
+ else
2149
+ targets.push(value);
2150
+ }
2151
+ if (targets.length > 1)
2152
+ throw new Error('install accepts at most one target');
2153
+ const result = await dependencies.selfUpdate?.({
2154
+ operation: 'install',
2155
+ force,
2156
+ ...(targets[0] === undefined ? {} : { target: targets[0] }),
2157
+ });
2158
+ if (!result)
2159
+ throw new Error('Self-update unavailable');
2160
+ if (json)
2161
+ writeJson(io, result);
2162
+ else
2163
+ io.stdout(`Praxis install completed: ${result.output}\n`);
2164
+ return 0;
2165
+ }
2166
+ async function executeProjectPurgeCommand(argv, io) {
2167
+ let all = false;
2168
+ let dryRun = false;
2169
+ let interactive = false;
2170
+ let yes = false;
2171
+ let help = false;
2172
+ let json = false;
2173
+ const passthrough = [];
2174
+ let path;
2175
+ let optionsEnded = false;
2176
+ for (let index = 2; index < argv.length; index += 1) {
2177
+ const value = argv[index];
2178
+ if (value === undefined)
2179
+ continue;
2180
+ if (!optionsEnded && value === '--') {
2181
+ optionsEnded = true;
2182
+ continue;
2183
+ }
2184
+ if (value === '--all') {
2185
+ all = true;
2186
+ continue;
2187
+ }
2188
+ if (value === '--dry-run') {
2189
+ dryRun = true;
2190
+ continue;
2191
+ }
2192
+ if (value === '-i' || value === '--interactive') {
2193
+ interactive = true;
2194
+ continue;
2195
+ }
2196
+ if (value === '-y' || value === '--yes') {
2197
+ yes = true;
2198
+ continue;
2199
+ }
2200
+ if (value === '--json') {
2201
+ json = true;
2202
+ continue;
2203
+ }
2204
+ if (value === '-h' || value === '--help') {
2205
+ help = true;
2206
+ continue;
2207
+ }
2208
+ if (!optionsEnded && value.startsWith('-')) {
2209
+ passthrough.push(value);
2210
+ continue;
2211
+ }
2212
+ if (path !== undefined)
2213
+ throw new Error('project purge accepts one path');
2214
+ path = value;
2215
+ }
2216
+ if (help) {
2217
+ io.stdout(PROJECT_PURGE_HELP);
2218
+ return 0;
2219
+ }
2220
+ if (passthrough.length > 0) {
2221
+ throw new Error(`Unknown project purge option: ${passthrough[0]}`);
2222
+ }
2223
+ if (all && path !== undefined) {
2224
+ throw new Error('Cannot specify both a path and --all');
2225
+ }
2226
+ if (interactive && yes) {
2227
+ throw new Error('--interactive cannot be combined with --yes');
2228
+ }
2229
+ const configuredRoot = process.env.CLAUDE_CONFIG_DIR || undefined;
2230
+ const configRoot = resolve(configuredRoot ?? resolve(homedir(), '.claude'));
2231
+ const statePath = configuredRoot
2232
+ ? join(configRoot, '.claude.json')
2233
+ : resolve(homedir(), '.claude.json');
2234
+ const plan = await planClaudeProjectPurge({
2235
+ cwd: process.cwd(),
2236
+ ...(path === undefined ? {} : { path }),
2237
+ ...(all ? { all: true } : {}),
2238
+ configRoot,
2239
+ statePath,
2240
+ });
2241
+ const jsonValue = (result) => ({
2242
+ type: 'project-purge',
2243
+ plan,
2244
+ result: {
2245
+ ...result,
2246
+ failures: result.failures.map((failure) => ({
2247
+ item: failure.item,
2248
+ error: redactSensitiveText(failure.error.message, sensitiveEnvironmentValues(process.env)),
2249
+ })),
2250
+ },
2251
+ });
2252
+ if (dryRun) {
2253
+ const result = await executeClaudeProjectPurge(plan, { dryRun: true });
2254
+ if (json)
2255
+ writeJson(io, jsonValue(result));
2256
+ else {
2257
+ io.stdout(`${plan.items.length} item(s) would be deleted for ${plan.mode === 'all' ? 'all projects' : plan.targetPath}\n`);
2258
+ for (const item of plan.items)
2259
+ io.stdout(` ${item.kind}\t${item.path}\n`);
2260
+ }
2261
+ return 0;
2262
+ }
2263
+ if (plan.items.length === 0) {
2264
+ if (json) {
2265
+ writeJson(io, jsonValue({
2266
+ dryRun: false,
2267
+ aborted: false,
2268
+ deleted: [],
2269
+ skipped: [],
2270
+ failures: [],
2271
+ }));
2272
+ }
2273
+ else
2274
+ io.stdout('No project state found.\n');
2275
+ return 0;
2276
+ }
2277
+ let inputIterator;
2278
+ if (!yes) {
2279
+ const input = io.readStdinLines?.();
2280
+ if (!input)
2281
+ throw new Error('project purge requires --yes without stdin');
2282
+ inputIterator = input[Symbol.asyncIterator]();
2283
+ }
2284
+ const nextAnswer = async () => {
2285
+ const next = await inputIterator?.next();
2286
+ return typeof next?.value === 'string'
2287
+ ? next.value.trim().toLowerCase()
2288
+ : next?.value instanceof Uint8Array
2289
+ ? Buffer.from(next.value).toString('utf8').trim().toLowerCase()
2290
+ : '';
2291
+ };
2292
+ let confirmed = yes;
2293
+ if (!yes && !interactive) {
2294
+ io.stderr(`Delete ${plan.items.length} item(s) for ${plan.mode === 'all' ? 'all projects' : plan.targetPath}? [y/N] `);
2295
+ confirmed = ['y', 'yes'].includes(await nextAnswer());
2296
+ if (!confirmed) {
2297
+ io.stderr('Purge cancelled.\n');
2298
+ return 1;
2299
+ }
2300
+ }
2301
+ const selectItem = interactive
2302
+ ? async (item) => {
2303
+ io.stderr(`Delete ${item.description} (${item.path})? [y/N/a/q] `);
2304
+ const answer = await nextAnswer();
2305
+ if (answer === 'a' || answer === 'all')
2306
+ return 'delete-all';
2307
+ if (answer === 'q' || answer === 'quit')
2308
+ return 'abort';
2309
+ return answer === 'y' || answer === 'yes' ? 'delete' : 'skip';
2310
+ }
2311
+ : undefined;
2312
+ const result = await executeClaudeProjectPurge(plan, {
2313
+ ...(selectItem ? { selectItem } : {}),
2314
+ });
2315
+ if (json) {
2316
+ writeJson(io, jsonValue(result));
2317
+ }
2318
+ else {
2319
+ for (const item of result.deleted)
2320
+ io.stdout(`deleted\t${item.path}\n`);
2321
+ for (const item of result.skipped)
2322
+ io.stdout(`skipped\t${item.path}\n`);
2323
+ for (const failure of result.failures) {
2324
+ io.stderr(`failed\t${failure.item.path}\t${failure.error.message}\n`);
2325
+ }
2326
+ if (result.aborted)
2327
+ io.stderr('Purge cancelled.\n');
2328
+ }
2329
+ return result.failures.length === 0 && !result.aborted ? 0 : 1;
2330
+ }
2331
+ function pluginOutput(io, invocation, value) {
2332
+ if (invocation.legacyJson || invocation.outputFormat !== 'text') {
2333
+ writeJson(io, value);
2334
+ }
2335
+ else if (typeof value === 'string') {
2336
+ io.stdout(`${value}\n`);
2337
+ }
2338
+ else {
2339
+ io.stdout(`${JSON.stringify(value)}\n`);
2340
+ }
2341
+ }
2342
+ function pluginPruneSummary(plan) {
2343
+ const count = plan.candidates.length;
2344
+ return `${count} auto-installed plugin${count === 1 ? '' : 's'} no longer needed at ${plan.scope} scope:\n${plan.candidates.map((plugin) => ` ${plugin.id} (${plugin.version})`).join('\n')}\n`;
2345
+ }
2346
+ async function pluginConfirmation(io) {
2347
+ const input = io.readStdinLines?.();
2348
+ if (!input)
2349
+ return false;
2350
+ const next = await input[Symbol.asyncIterator]().next();
2351
+ const value = typeof next.value === 'string'
2352
+ ? next.value
2353
+ : next.value instanceof Uint8Array
2354
+ ? Buffer.from(next.value).toString('utf8')
2355
+ : '';
2356
+ return ['y', 'yes'].includes(value.trim().toLowerCase());
2357
+ }
2358
+ async function executePluginPruneFlow(configRoot, cwd, scope, invocation, io, uninstalledName) {
2359
+ const structured = invocation.legacyJson || invocation.outputFormat !== 'text';
2360
+ const structuredOutput = (status, value = {}) => {
2361
+ pluginOutput(io, invocation, {
2362
+ type: uninstalledName === undefined
2363
+ ? 'plugin-pruned'
2364
+ : 'plugin-uninstalled-and-pruned',
2365
+ ...(uninstalledName === undefined ? {} : { name: uninstalledName }),
2366
+ scope,
2367
+ status,
2368
+ ...value,
2369
+ });
2370
+ };
2371
+ const plan = await planClaudePluginPrune(configRoot, cwd, scope);
2372
+ if (plan.failedPluginIds) {
2373
+ const message = `Skipped — cannot determine orphans: ${plan.failedPluginIds.join(', ')} failed to load. Fix or uninstall, then retry.`;
2374
+ if (structured) {
2375
+ structuredOutput('skipped', {
2376
+ message,
2377
+ failedPluginIds: plan.failedPluginIds,
2378
+ });
2379
+ }
2380
+ else
2381
+ io.stdout(`${message}\n`);
2382
+ return 0;
2383
+ }
2384
+ if (plan.candidates.length === 0) {
2385
+ const message = plan.autoCount === 0
2386
+ ? `Nothing to prune (no auto-installed plugins at ${scope} scope).`
2387
+ : `Nothing to prune (${plan.autoCount} auto-installed plugin${plan.autoCount === 1 ? '' : 's'} at ${scope} scope, all still needed).`;
2388
+ if (structured) {
2389
+ structuredOutput('complete', {
2390
+ dryRun: invocation.pluginDryRun,
2391
+ removed: [],
2392
+ });
2393
+ }
2394
+ else
2395
+ io.stdout(`${message}\n`);
2396
+ return 0;
2397
+ }
2398
+ if (structured) {
2399
+ if (invocation.pluginDryRun) {
2400
+ structuredOutput('complete', {
2401
+ dryRun: true,
2402
+ removed: [],
2403
+ candidates: plan.candidates,
2404
+ });
2405
+ return 0;
2406
+ }
2407
+ }
2408
+ else {
2409
+ io.stdout(pluginPruneSummary(plan));
2410
+ if (invocation.pluginDryRun) {
2411
+ io.stdout('(dry run — nothing removed)\n');
2412
+ return 0;
2413
+ }
2414
+ }
2415
+ if (!invocation.pluginYes && io.isTTY !== true) {
2416
+ if (structured) {
2417
+ structuredOutput('confirmation-required', {
2418
+ dryRun: false,
2419
+ removed: [],
2420
+ candidates: plan.candidates,
2421
+ });
2422
+ }
2423
+ else {
2424
+ io.stdout('Not a TTY — run `praxis plugin prune -y` to remove.\n');
2425
+ }
2426
+ return 0;
2427
+ }
2428
+ if (!invocation.pluginYes) {
2429
+ if (structured)
2430
+ io.stderr('Remove? [y/N] ');
2431
+ else
2432
+ io.stdout('Remove? [y/N] ');
2433
+ if (!(await pluginConfirmation(io))) {
2434
+ if (structured) {
2435
+ structuredOutput('cancelled', {
2436
+ dryRun: false,
2437
+ removed: [],
2438
+ candidates: plan.candidates,
2439
+ });
2440
+ }
2441
+ else
2442
+ io.stdout('Cancelled.\n');
2443
+ return 0;
2444
+ }
2445
+ }
2446
+ const removed = await executeClaudePluginPrune(configRoot, cwd, plan);
2447
+ if (structured) {
2448
+ structuredOutput('complete', {
2449
+ dryRun: false,
2450
+ removed,
2451
+ });
2452
+ }
2453
+ else {
2454
+ io.stdout(`Removed ${removed.length} auto-installed plugin${removed.length === 1 ? '' : 's'}: ${removed.map((plugin) => plugin.id.split('@')[0]).join(', ')}\n`);
2455
+ }
2456
+ return 0;
2457
+ }
2458
+ async function executePluginTag(path, invocation, io) {
2459
+ try {
2460
+ const result = await tagClaudePlugin({
2461
+ path,
2462
+ ...(invocation.pluginDryRun ? { dryRun: true } : {}),
2463
+ ...(invocation.pluginForce ? { force: true } : {}),
2464
+ ...(invocation.pluginMessage === undefined
2465
+ ? {}
2466
+ : { message: invocation.pluginMessage }),
2467
+ ...(invocation.pluginPush ? { push: true } : {}),
2468
+ ...(invocation.pluginRemote === undefined
2469
+ ? {}
2470
+ : { remote: invocation.pluginRemote }),
2471
+ });
2472
+ if (invocation.legacyJson || invocation.outputFormat !== 'text') {
2473
+ pluginOutput(io, invocation, { type: 'plugin-tagged', ...result });
2474
+ return 0;
2475
+ }
2476
+ for (const warning of result.warnings)
2477
+ io.stdout(`⚠ ${warning}\n`);
2478
+ io.stdout(`Plugin: ${result.name}\nVersion: ${result.version} (from plugin.json)\n${result.marketplaceEntry === undefined ? '' : `Marketplace entry: plugins[${result.marketplaceEntry.index}] in ${result.marketplaceEntry.path}${result.marketplaceEntry.version === undefined ? '' : ` (version: ${result.marketplaceEntry.version})`}\n`}Tag: ${result.tag}\n\n`);
2479
+ const forceFlag = result.force ? ' --force' : '';
2480
+ if (result.dryRun) {
2481
+ io.stdout(`✔ Dry run — would create tag ${result.tag} at HEAD in ${result.repository}\n git -C ${result.repository} tag${forceFlag} -a ${result.tag} -m ${JSON.stringify(result.message)}\n git -C ${result.repository} push${forceFlag} ${result.remote} refs/tags/${result.tag}\n`);
2482
+ }
2483
+ else {
2484
+ io.stdout(`✔ Created tag ${result.tag}\n`);
2485
+ io.stdout(result.pushed
2486
+ ? `✔ Pushed to ${result.remote}\n`
2487
+ : ` Push with: git -C ${result.repository} push${forceFlag} ${result.remote} refs/tags/${result.tag}\n`);
2488
+ }
2489
+ return 0;
2490
+ }
2491
+ catch (error) {
2492
+ io.stdout(`✘ ${error instanceof Error ? error.message : String(error)}\n`);
2493
+ return 1;
2494
+ }
2495
+ }
2496
+ function isClaudeMarketplacePluginId(value) {
2497
+ const separator = value.lastIndexOf('@');
2498
+ return (separator > 0 &&
2499
+ separator < value.length - 1 &&
2500
+ !value.includes('/') &&
2501
+ !value.includes('\\'));
2502
+ }
2503
+ function formatPluginDetails(details) {
2504
+ const { plugin, components, tokenEstimate } = details;
2505
+ const lines = [
2506
+ `${plugin.name}${plugin.version ? ` ${plugin.version}` : ''}`,
2507
+ ...(plugin.description ? [` ${plugin.description}`] : []),
2508
+ ` Source: ${plugin.source}`,
2509
+ '',
2510
+ 'Component inventory',
2511
+ ];
2512
+ const groups = [
2513
+ ['Skills', [...components.skills, ...components.commands].sort(), ''],
2514
+ ['Agents', components.agents, ''],
2515
+ ['Hooks', components.hooks, ' (harness-only — no model context cost)'],
2516
+ [
2517
+ 'MCP servers',
2518
+ components.mcpServers,
2519
+ ' (tool schemas resolved at runtime; not counted)',
2520
+ ],
2521
+ [
2522
+ 'LSP servers',
2523
+ components.lspServers,
2524
+ ' (out-of-process tooling; no model context cost)',
2525
+ ],
2526
+ ];
2527
+ for (const [label, values, annotation] of groups) {
2528
+ lines.push(` ${label} (${values.length})${values.length === 0 ? '' : ` ${values.join(', ')}`}${values.length > 0 ? annotation : ''}`);
2529
+ }
2530
+ lines.push('', 'Projected token cost', ` Always-on: ~${tokenEstimate.alwaysOn.toLocaleString()} tok added to every session`);
2531
+ const textComponents = details.componentCosts;
2532
+ if (textComponents.length === 0)
2533
+ return lines.join('\n');
2534
+ lines.push('', 'Per-component (rounded)', ' component always-on on-invoke');
2535
+ const width = Math.max(9, ...textComponents.map((component) => component.name.length));
2536
+ for (const component of textComponents) {
2537
+ lines.push(` ${component.name.padEnd(width)} ${formatPluginTokenEstimate(component.alwaysOn).padStart(9)} ${formatPluginTokenEstimate(component.onInvoke).padStart(9)}`);
2538
+ }
2539
+ lines.push('', ' On-invoke cost is paid each time a skill or agent fires.', ' Token counts are estimates and may differ from actual usage.');
2540
+ return lines.join('\n');
2541
+ }
2542
+ function formatPluginTokenEstimate(value) {
2543
+ if (value < 20)
2544
+ return '< 20';
2545
+ return `~${Math.round(value / 10) * 10}`;
2546
+ }
2547
+ async function pluginDetailsForName(configRoot, cwd, name) {
2548
+ const [native, local] = await Promise.all([
2549
+ listNativePluginRecords(configRoot, cwd),
2550
+ readPluginRegistry(configRoot),
2551
+ ]);
2552
+ const entries = [...native, ...local];
2553
+ const entry = entries.find((candidate) => candidate.name === name) ??
2554
+ entries.find((candidate) => candidate.name.startsWith(`${name}@`));
2555
+ if (!entry) {
2556
+ throw new Error(`Plugin "${name}" not found. Run \`praxis plugin list\` to see installed plugins.`);
2557
+ }
2558
+ return describeClaudePlugin(entry.path, entry.source);
2559
+ }
2560
+ async function executePluginCommand(args, invocation, io) {
2561
+ const requestedAction = args[1];
2562
+ const action = requestedAction === 'i'
2563
+ ? 'install'
2564
+ : requestedAction === 'new'
2565
+ ? 'init'
2566
+ : requestedAction === 'remove'
2567
+ ? 'uninstall'
2568
+ : requestedAction;
2569
+ const cwd = process.cwd();
2570
+ const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? resolve(homedir(), '.claude'));
2571
+ const requestedScope = invocation.mcpScope;
2572
+ const installScope = requestedScope ?? 'user';
2573
+ if (!action || action === 'help') {
2574
+ io.stdout(PLUGIN_HELP);
2575
+ return 0;
2576
+ }
2577
+ if (invocation.pluginAvailable && action !== 'list') {
2578
+ throw new Error('--available is only valid with plugin list');
2579
+ }
2580
+ if (invocation.pluginAll || invocation.agentsAll) {
2581
+ if (action !== 'disable') {
2582
+ throw new Error('--all is only valid with plugin disable');
2583
+ }
2584
+ }
2585
+ if ((invocation.pluginKeepData || invocation.pluginPrune) &&
2586
+ action !== 'uninstall') {
2587
+ throw new Error('--keep-data and --prune are only valid with plugin uninstall');
2588
+ }
2589
+ if (invocation.pluginYes &&
2590
+ !['uninstall', 'prune', 'autoremove'].includes(action)) {
2591
+ throw new Error('--yes is only valid with plugin uninstall or prune');
2592
+ }
2593
+ if (invocation.pluginStrict && action !== 'validate') {
2594
+ throw new Error('--strict is only valid with plugin validate');
2595
+ }
2596
+ if (invocation.pluginConfig.length > 0 && action !== 'install') {
2597
+ throw new Error('--config is only valid with plugin install');
2598
+ }
2599
+ if ((invocation.pluginAuthor !== undefined ||
2600
+ invocation.pluginAuthorEmail !== undefined ||
2601
+ invocation.pluginDescription !== undefined ||
2602
+ invocation.pluginWith.length > 0) &&
2603
+ action !== 'init') {
2604
+ throw new Error('--author, --author-email, --description, and --with are only valid with plugin init');
2605
+ }
2606
+ if (invocation.pluginForce && !['init', 'tag'].includes(action)) {
2607
+ throw new Error('--force is only valid with plugin init or tag');
2608
+ }
2609
+ if (invocation.pluginDryRun &&
2610
+ !['prune', 'autoremove', 'tag'].includes(action)) {
2611
+ throw new Error('--dry-run is only valid with plugin prune or tag');
2612
+ }
2613
+ if ((invocation.pluginMessage !== undefined ||
2614
+ invocation.pluginPush ||
2615
+ invocation.pluginRemote !== undefined) &&
2616
+ action !== 'tag') {
2617
+ throw new Error('--message, --push, and --remote are only valid with plugin tag');
2618
+ }
2619
+ if (requestedScope !== undefined &&
2620
+ ![
2621
+ 'install',
2622
+ 'uninstall',
2623
+ 'enable',
2624
+ 'disable',
2625
+ 'update',
2626
+ 'marketplace',
2627
+ 'prune',
2628
+ 'autoremove',
2629
+ ].includes(action)) {
2630
+ throw new Error(`--scope is not valid with plugin ${action}`);
2631
+ }
2632
+ if (action === 'marketplace') {
2633
+ const requestedMarketplaceAction = args[2];
2634
+ const marketplaceAction = requestedMarketplaceAction === 'rm'
2635
+ ? 'remove'
2636
+ : requestedMarketplaceAction;
2637
+ if (!marketplaceAction || marketplaceAction === 'help') {
2638
+ io.stdout(PLUGIN_MARKETPLACE_HELP);
2639
+ return 0;
2640
+ }
2641
+ if (marketplaceAction === 'list') {
2642
+ if (args.length !== 3)
2643
+ throw new Error('plugin marketplace list takes no operands');
2644
+ if (requestedScope !== undefined) {
2645
+ throw new Error('--scope is not valid with plugin marketplace list');
2646
+ }
2647
+ pluginOutput(io, invocation, await readClaudeKnownMarketplaces(configRoot));
2648
+ return 0;
2649
+ }
2650
+ if (marketplaceAction === 'add') {
2651
+ if (args.length !== 4)
2652
+ throw new Error('plugin marketplace add requires a source');
2653
+ pluginOutput(io, invocation, {
2654
+ type: 'plugin-marketplace-added',
2655
+ marketplace: await addClaudeMarketplace(configRoot, cwd, args[3], installScope, invocation.pluginSparsePaths),
2656
+ });
2657
+ return 0;
2658
+ }
2659
+ if (marketplaceAction === 'remove') {
2660
+ if (args.length !== 4)
2661
+ throw new Error('plugin marketplace remove requires a name');
2662
+ await removeClaudeMarketplace(configRoot, cwd, args[3], requestedScope);
2663
+ pluginOutput(io, invocation, {
2664
+ type: 'plugin-marketplace-removed',
2665
+ name: args[3],
2666
+ });
2667
+ return 0;
2668
+ }
2669
+ if (marketplaceAction === 'update') {
2670
+ if (args.length > 4)
2671
+ throw new Error('plugin marketplace update accepts at most one name');
2672
+ if (requestedScope !== undefined) {
2673
+ throw new Error('--scope is not valid with plugin marketplace update');
2674
+ }
2675
+ pluginOutput(io, invocation, {
2676
+ type: 'plugin-marketplace-updated',
2677
+ marketplaces: await updateClaudeMarketplace(configRoot, args[3]),
2678
+ });
2679
+ return 0;
2680
+ }
2681
+ throw new Error(`Unknown plugin marketplace command: ${marketplaceAction}`);
2682
+ }
2683
+ if (action === 'list') {
2684
+ if (args.length !== 2)
2685
+ throw new Error('plugin list takes no operands');
2686
+ const registry = await readPluginRegistry(configRoot);
2687
+ const native = await listNativePluginRecords(configRoot, cwd);
2688
+ const installed = await Promise.all([...native, ...registry].map(async (entry) => {
2689
+ try {
2690
+ const record = await validateClaudePlugin(entry.path);
2691
+ return {
2692
+ ...entry,
2693
+ status: entry.enabled ? 'enabled' : 'disabled',
2694
+ valid: true,
2695
+ version: record.version ?? entry.version,
2696
+ };
2697
+ }
2698
+ catch (error) {
2699
+ return {
2700
+ ...entry,
2701
+ status: entry.enabled ? 'enabled' : 'disabled',
2702
+ valid: false,
2703
+ error: error instanceof Error ? error.message : String(error),
2704
+ };
2705
+ }
2706
+ }));
2707
+ const includeAvailable = invocation.pluginAvailable;
2708
+ pluginOutput(io, invocation, includeAvailable
2709
+ ? {
2710
+ installed,
2711
+ available: await listClaudeMarketplaceAvailablePlugins(configRoot, cwd),
2712
+ }
2713
+ : installed);
2714
+ return 0;
2715
+ }
2716
+ if (action === 'details') {
2717
+ if (args.length !== 3)
2718
+ throw new Error('plugin details requires a plugin name');
2719
+ const details = await pluginDetailsForName(configRoot, cwd, args[2]);
2720
+ pluginOutput(io, invocation, invocation.legacyJson || invocation.outputFormat !== 'text'
2721
+ ? details
2722
+ : formatPluginDetails(details));
2723
+ return 0;
2724
+ }
2725
+ if (action === 'install') {
2726
+ if (args.length !== 3)
2727
+ throw new Error('plugin install requires a plugin path, URL, or plugin@marketplace id');
2728
+ const source = args[2];
2729
+ if (isClaudeMarketplacePluginId(source)) {
2730
+ const plugin = await installClaudeMarketplacePlugin(configRoot, cwd, source, installScope);
2731
+ const config = await saveClaudePluginConfig(configRoot, cwd, installScope, plugin.id, plugin.installPath, invocation.pluginConfig);
2732
+ pluginOutput(io, invocation, {
2733
+ type: 'plugin-installed',
2734
+ plugin,
2735
+ ...(config.warnings.length === 0 ? {} : { warnings: config.warnings }),
2736
+ });
2737
+ return 0;
2738
+ }
2739
+ if (requestedScope !== undefined) {
2740
+ throw new Error('--scope is only supported when installing plugin@marketplace');
2741
+ }
2742
+ const plugin = await installClaudePlugin(configRoot, source);
2743
+ const config = await saveClaudePluginConfig(configRoot, cwd, installScope, plugin.name, plugin.path, invocation.pluginConfig);
2744
+ pluginOutput(io, invocation, {
2745
+ type: 'plugin-installed',
2746
+ plugin,
2747
+ ...(config.warnings.length === 0 ? {} : { warnings: config.warnings }),
2748
+ });
2749
+ return 0;
2750
+ }
2751
+ if (action === 'uninstall') {
2752
+ if (args.length !== 3)
2753
+ throw new Error('plugin uninstall requires a plugin name');
2754
+ const name = args[2];
2755
+ if (isClaudeMarketplacePluginId(name))
2756
+ await uninstallNativePlugin(configRoot, cwd, name, installScope, !invocation.pluginKeepData);
2757
+ else
2758
+ await uninstallClaudePlugin(configRoot, name, !invocation.pluginKeepData);
2759
+ if (!invocation.pluginPrune) {
2760
+ pluginOutput(io, invocation, { type: 'plugin-uninstalled', name });
2761
+ return 0;
2762
+ }
2763
+ const structured = invocation.legacyJson || invocation.outputFormat !== 'text';
2764
+ if (!structured) {
2765
+ pluginOutput(io, invocation, { type: 'plugin-uninstalled', name });
2766
+ }
2767
+ return executePluginPruneFlow(configRoot, cwd, installScope, invocation, io, structured ? name : undefined);
2768
+ }
2769
+ if (action === 'enable' || action === 'disable') {
2770
+ if (action === 'disable' && invocation.pluginAll) {
2771
+ if (args.length !== 2) {
2772
+ throw new Error('plugin disable --all takes no plugin name');
2773
+ }
2774
+ if (requestedScope !== undefined) {
2775
+ throw new Error('--scope cannot be used with plugin disable --all');
2776
+ }
2777
+ const native = await disableAllNativePlugins(configRoot, cwd);
2778
+ const local = (await readPluginRegistry(configRoot)).filter((plugin) => plugin.enabled);
2779
+ const disabledLocal = await Promise.all(local.map((plugin) => setClaudePluginEnabled(configRoot, plugin.name, false)));
2780
+ pluginOutput(io, invocation, {
2781
+ type: 'plugins-disabled',
2782
+ plugins: [...native, ...disabledLocal],
2783
+ });
2784
+ return 0;
2785
+ }
2786
+ if (args.length !== 3)
2787
+ throw new Error(`plugin ${action} requires a plugin name`);
2788
+ const name = args[2];
2789
+ const plugin = isClaudeMarketplacePluginId(name)
2790
+ ? await setNativePluginEnabled(configRoot, cwd, name, action === 'enable', requestedScope)
2791
+ : await setClaudePluginEnabled(configRoot, name, action === 'enable');
2792
+ pluginOutput(io, invocation, {
2793
+ type: action === 'enable' ? 'plugin-enabled' : 'plugin-disabled',
2794
+ plugin,
2795
+ });
2796
+ return 0;
2797
+ }
2798
+ if (action === 'update') {
2799
+ if (args.length !== 3)
2800
+ throw new Error('plugin update requires a plugin name');
2801
+ const name = args[2];
2802
+ pluginOutput(io, invocation, {
2803
+ type: 'plugin-updated',
2804
+ plugin: isClaudeMarketplacePluginId(name)
2805
+ ? await updateNativePlugin(configRoot, cwd, name, installScope)
2806
+ : await updateClaudePlugin(configRoot, name),
2807
+ });
2808
+ return 0;
2809
+ }
2810
+ if (action === 'prune' || action === 'autoremove') {
2811
+ if (args.length !== 2)
2812
+ throw new Error(`plugin ${action} takes no operands`);
2813
+ return executePluginPruneFlow(configRoot, cwd, installScope, invocation, io);
2814
+ }
2815
+ if (action === 'tag') {
2816
+ if (args.length > 3)
2817
+ throw new Error('plugin tag accepts at most one path');
2818
+ return executePluginTag(args[2] ?? cwd, invocation, io);
2819
+ }
2820
+ if (action === 'validate') {
2821
+ if (args.length !== 3)
2822
+ throw new Error('plugin validate requires a plugin directory');
2823
+ const path = resolve(args[2]);
2824
+ let marketplace = false;
2825
+ try {
2826
+ await access(join(path, '.claude-plugin', 'marketplace.json'));
2827
+ marketplace = true;
2828
+ }
2829
+ catch {
2830
+ // A plugin manifest remains the default validation target.
2831
+ }
2832
+ pluginOutput(io, invocation, {
2833
+ type: 'plugin-valid',
2834
+ ...(marketplace
2835
+ ? {
2836
+ marketplace: await validateClaudeMarketplace(path, {
2837
+ strict: invocation.pluginStrict,
2838
+ }),
2839
+ }
2840
+ : {
2841
+ plugin: await validateClaudePlugin(path, {
2842
+ strict: invocation.pluginStrict,
2843
+ }),
2844
+ }),
2845
+ });
2846
+ return 0;
2847
+ }
2848
+ if (action === 'init') {
2849
+ if (args.length < 3 || args.length > 4) {
2850
+ throw new Error('plugin init requires a name');
2851
+ }
2852
+ const nativeOptionsUsed = invocation.pluginAuthor !== undefined ||
2853
+ invocation.pluginAuthorEmail !== undefined ||
2854
+ invocation.pluginDescription !== undefined ||
2855
+ invocation.pluginForce ||
2856
+ invocation.pluginWith.length > 0;
2857
+ if (args.length === 4 && nativeOptionsUsed) {
2858
+ throw new Error('plugin init native syntax accepts one name; use plugin init <directory> <name> without native init options for legacy scaffolding');
2859
+ }
2860
+ const options = {
2861
+ ...(invocation.pluginAuthor === undefined
2862
+ ? {}
2863
+ : { author: invocation.pluginAuthor }),
2864
+ ...(invocation.pluginAuthorEmail === undefined
2865
+ ? {}
2866
+ : { authorEmail: invocation.pluginAuthorEmail }),
2867
+ ...(invocation.pluginDescription === undefined
2868
+ ? {}
2869
+ : { description: invocation.pluginDescription }),
2870
+ ...(invocation.pluginForce ? { force: true } : {}),
2871
+ ...(invocation.pluginWith.length === 0
2872
+ ? {}
2873
+ : {
2874
+ with: invocation.pluginWith,
2875
+ }),
2876
+ };
2877
+ const nativeLayout = args.length === 3;
2878
+ const path = nativeLayout
2879
+ ? join(configRoot, 'skills', args[2])
2880
+ : args[2];
2881
+ const name = nativeLayout ? args[2] : args[3];
2882
+ const plugin = await initClaudePlugin(path, name, {
2883
+ ...options,
2884
+ nativeLayout,
2885
+ });
2886
+ pluginOutput(io, invocation, {
2887
+ type: 'plugin-initialized',
2888
+ path: resolve(path),
2889
+ ...(name ? { name } : {}),
2890
+ plugin,
2891
+ });
2892
+ return 0;
2893
+ }
2894
+ throw new Error(`Unknown plugin command: ${action}`);
2895
+ }
2896
+ async function executeMcpCommand(args, invocation, io, dependencies, signal) {
2897
+ const action = args[1];
2898
+ if (!action || action === 'help') {
2899
+ io.stdout(MCP_HELP);
2900
+ return 0;
2901
+ }
2902
+ const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'));
2903
+ const management = new ClaudeMcpManagement({
2904
+ configRoot,
2905
+ cwd: process.cwd(),
2906
+ });
2907
+ const scope = invocation.mcpScope ? mcpScope(invocation.mcpScope) : undefined;
2908
+ if (action === 'login') {
2909
+ if (args.length !== 3)
2910
+ throw new Error('mcp login requires a server name');
2911
+ const record = await management.get(args[2], scope);
2912
+ const server = mcpOAuthServerIdentity(record.name, record.config);
2913
+ const oauth = mcpOauthOptions(record.config);
2914
+ const clientSecret = oauth.clientId
2915
+ ? await new ClaudeMcpOAuthStore({ configRoot }).readClientSecret(server)
2916
+ : undefined;
2917
+ await (dependencies.mcpAuthenticate ?? authenticateMcpServer)({
2918
+ configRoot,
2919
+ server,
2920
+ ...oauth,
2921
+ ...(clientSecret ? { clientSecret } : {}),
2922
+ noBrowser: invocation.mcpNoBrowser,
2923
+ write: (message) => io.stdout(message),
2924
+ });
2925
+ io.stdout(`Authenticated with "${record.name}". Its tools are now available in Praxis.\n`);
2926
+ return 0;
2927
+ }
2928
+ if (action === 'logout') {
2929
+ if (args.length !== 3)
2930
+ throw new Error('mcp logout requires a server name');
2931
+ const record = await management.get(args[2], scope);
2932
+ const server = mcpOAuthServerIdentity(record.name, record.config);
2933
+ await new ClaudeMcpOAuthStore({ configRoot }).clear(server);
2934
+ io.stdout(`Signed out of "${record.name}". Run \`praxis mcp login ${record.name}\` to authenticate again.\n`);
2935
+ return 0;
2936
+ }
2937
+ if (action === 'serve') {
2938
+ if (args.length !== 2)
2939
+ throw new Error('mcp serve takes no operands');
2940
+ if (scope)
2941
+ throw new Error('--scope is not valid with mcp serve');
2942
+ await (dependencies.mcpServe ?? servePraxisMcpStdio)({
2943
+ cwd: process.cwd(),
2944
+ debug: invocation.mcpDebug,
2945
+ verbose: invocation.verbose,
2946
+ ...(signal ? { signal } : {}),
2947
+ writeError: (message) => io.stderr(message),
2948
+ createToolRegistry: async () => {
2949
+ const service = await dependencies.createService({
2950
+ eventSink: () => undefined,
2951
+ requireProvider: false,
2952
+ exposeToolRegistry: true,
2953
+ approveTool: async () => true,
2954
+ controls: invocation,
2955
+ ...(signal ? { signal } : {}),
2956
+ });
2957
+ const registry = service.toolRegistry;
2958
+ if (!registry) {
2959
+ await service.close?.();
2960
+ throw new Error('MCP tool registry is unavailable');
2961
+ }
2962
+ return {
2963
+ definitions: registry.definitions.bind(registry),
2964
+ prepare: registry.prepare.bind(registry),
2965
+ execute: registry.execute.bind(registry),
2966
+ close: async () => service.close?.(),
2967
+ };
2968
+ },
2969
+ createAgentService: ({ agent, model, permissionMode, worktree, eventSink: agentEventSink, }) => dependencies.createService({
2970
+ eventSink: agentEventSink,
2971
+ requireProvider: true,
2972
+ approveTool: async () => true,
2973
+ ...(agent ? { agent } : {}),
2974
+ controls: {
2975
+ ...invocation,
2976
+ ...(model ? { model } : {}),
2977
+ ...(permissionMode ? { permissionMode } : {}),
2978
+ ...(worktree ? { worktreeRequested: true } : {}),
2979
+ },
2980
+ ...(signal ? { signal } : {}),
2981
+ }),
2982
+ });
2983
+ return 0;
2984
+ }
2985
+ if (action === 'list') {
2986
+ if (args.length !== 2)
2987
+ throw new Error('mcp list takes no operands');
2988
+ const servers = await management.list(scope);
2989
+ if (invocation.legacyJson || invocation.outputFormat !== 'text') {
2990
+ mcpOutput(io, invocation, {
2991
+ type: 'mcp-list',
2992
+ servers: servers.map(mcpRecordJson),
2993
+ });
2994
+ }
2995
+ else {
2996
+ for (const server of servers) {
2997
+ io.stdout(`${server.name}\t${server.scope}\t${JSON.stringify(server.config)}\n`);
2998
+ }
2999
+ }
3000
+ return 0;
3001
+ }
3002
+ if (action === 'get') {
3003
+ if (args.length !== 3)
3004
+ throw new Error('mcp get requires a server name');
3005
+ const server = await management.get(args[2], scope);
3006
+ mcpOutput(io, invocation, {
3007
+ type: 'mcp-server',
3008
+ server: mcpRecordJson(server),
3009
+ });
3010
+ return 0;
3011
+ }
3012
+ if (action === 'add-json') {
3013
+ if (args.length !== 4)
3014
+ throw new Error('mcp add-json requires name and JSON');
3015
+ let config;
3016
+ try {
3017
+ config = JSON.parse(args[3]);
3018
+ }
3019
+ catch (error) {
3020
+ throw new Error('mcp add-json requires valid JSON', { cause: error });
3021
+ }
3022
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
3023
+ throw new Error('mcp add-json server config must be an object');
3024
+ }
3025
+ const name = args[2];
3026
+ const serverConfig = config;
3027
+ const oauth = serverConfig.oauth;
3028
+ const hasClientId = oauth !== null &&
3029
+ typeof oauth === 'object' &&
3030
+ !Array.isArray(oauth) &&
3031
+ typeof oauth.clientId === 'string' &&
3032
+ oauth.clientId !== '';
3033
+ const clientSecret = invocation.mcpClientSecret && hasClientId
3034
+ ? await readMcpClientSecret()
3035
+ : undefined;
3036
+ const targetScope = scope ?? 'local';
3037
+ const previous = clientSecret
3038
+ ? await existingMcpServer(management, name, targetScope)
3039
+ : undefined;
3040
+ const server = await management.add(name, serverConfig, targetScope);
3041
+ if (clientSecret) {
3042
+ try {
3043
+ await new ClaudeMcpOAuthStore({ configRoot }).saveClientSecret(mcpOAuthServerIdentity(name, serverConfig), clientSecret);
3044
+ }
3045
+ catch (error) {
3046
+ try {
3047
+ if (previous)
3048
+ await management.add(name, previous.config, targetScope);
3049
+ else
3050
+ await management.remove(name, targetScope);
3051
+ }
3052
+ catch {
3053
+ throw new Error('MCP client secret could not be stored and configuration rollback failed', { cause: error });
3054
+ }
3055
+ throw error;
3056
+ }
3057
+ }
3058
+ mcpOutput(io, invocation, {
3059
+ type: 'mcp-added',
3060
+ server: mcpRecordJson(server),
3061
+ });
3062
+ return 0;
3063
+ }
3064
+ if (action === 'add') {
3065
+ if (args.length < 3)
3066
+ throw new Error("missing required argument 'name'");
3067
+ if (args.length < 4)
3068
+ throw new Error("missing required argument 'commandOrUrl'");
3069
+ const name = args[2];
3070
+ const commandOrUrl = args[3];
3071
+ const transport = invocation.mcpTransport ?? 'stdio';
3072
+ const commandArgs = args.slice(4);
3073
+ if (transport === 'stdio') {
3074
+ if (invocation.mcpClientId ||
3075
+ invocation.mcpClientSecret ||
3076
+ invocation.mcpCallbackPort) {
3077
+ io.stderr('Warning: --client-id, --client-secret, and --callback-port are only supported for HTTP/SSE transports and will be ignored for stdio.\n');
3078
+ }
3079
+ if (!invocation.mcpTransport && isLikelyMcpUrl(commandOrUrl)) {
3080
+ io.stderr(`\nWarning: The command "${commandOrUrl}" looks like a URL, but is being interpreted as a stdio server as --transport was not specified.\n`);
3081
+ io.stderr(`If this is an HTTP server, use: praxis mcp add --transport http ${name} ${commandOrUrl}\n`);
3082
+ io.stderr(`If this is an SSE server, use: praxis mcp add --transport sse ${name} ${commandOrUrl}\n`);
3083
+ }
3084
+ const server = await management.add(name, {
3085
+ type: 'stdio',
3086
+ command: commandOrUrl,
3087
+ args: commandArgs,
3088
+ env: mcpEnvironment(invocation.mcpEnv),
3089
+ }, scope ?? 'local');
3090
+ mcpAddedOutput(io, invocation, server, transport, commandOrUrl, commandArgs, undefined);
3091
+ return 0;
3092
+ }
3093
+ const headers = mcpHeaders(invocation.mcpHeaders);
3094
+ const callbackPort = mcpCallbackPort(invocation.mcpCallbackPort);
3095
+ const oauth = invocation.mcpClientId || callbackPort
3096
+ ? {
3097
+ ...(invocation.mcpClientId
3098
+ ? { clientId: invocation.mcpClientId }
3099
+ : {}),
3100
+ ...(callbackPort ? { callbackPort } : {}),
3101
+ }
3102
+ : undefined;
3103
+ const clientSecret = invocation.mcpClientSecret && invocation.mcpClientId
3104
+ ? await readMcpClientSecret()
3105
+ : undefined;
3106
+ const config = {
3107
+ type: transport,
3108
+ url: commandOrUrl,
3109
+ ...(headers ? { headers } : {}),
3110
+ ...(oauth ? { oauth } : {}),
3111
+ };
3112
+ const targetScope = scope ?? 'local';
3113
+ const previous = clientSecret
3114
+ ? await existingMcpServer(management, name, targetScope)
3115
+ : undefined;
3116
+ const server = await management.add(name, config, targetScope);
3117
+ if (clientSecret) {
3118
+ try {
3119
+ await new ClaudeMcpOAuthStore({ configRoot }).saveClientSecret(mcpOAuthServerIdentity(name, config), clientSecret);
3120
+ }
3121
+ catch (error) {
3122
+ try {
3123
+ if (previous) {
3124
+ await management.add(name, previous.config, targetScope);
3125
+ }
3126
+ else {
3127
+ await management.remove(name, targetScope);
3128
+ }
3129
+ }
3130
+ catch {
3131
+ throw new Error('MCP client secret could not be stored and configuration rollback failed', { cause: error });
3132
+ }
3133
+ throw error;
3134
+ }
3135
+ }
3136
+ mcpAddedOutput(io, invocation, server, transport, commandOrUrl, commandArgs, headers);
3137
+ return 0;
3138
+ }
3139
+ if (action === 'remove') {
3140
+ if (args.length !== 3)
3141
+ throw new Error('mcp remove requires a server name');
3142
+ const server = await management.remove(args[2], scope);
3143
+ mcpOutput(io, invocation, {
3144
+ type: 'mcp-removed',
3145
+ server: mcpRecordJson(server),
3146
+ });
3147
+ return 0;
3148
+ }
3149
+ if (action === 'reset-project-choices') {
3150
+ if (args.length !== 2) {
3151
+ throw new Error('mcp reset-project-choices takes no operands');
3152
+ }
3153
+ await management.resetProjectChoices();
3154
+ mcpOutput(io, invocation, 'Reset project MCP choices');
3155
+ return 0;
3156
+ }
3157
+ throw new Error(`Unknown mcp command: ${action}`);
3158
+ }
3159
+ function eventSink(io, outputFormat, legacyJson = false) {
3160
+ const sensitiveValues = sensitiveEnvironmentValues(process.env);
3161
+ if (legacyJson) {
3162
+ return (event) => writeJson(io, event.type === 'warning' || event.type === 'failed'
3163
+ ? {
3164
+ ...event,
3165
+ message: redactSensitiveText(event.message, sensitiveValues),
3166
+ }
3167
+ : event);
3168
+ }
3169
+ if (outputFormat !== 'text')
3170
+ return () => undefined;
3171
+ return (event) => {
3172
+ if (event.type === 'text-delta')
3173
+ io.stdout(event.delta);
3174
+ if (event.type === 'user-message')
3175
+ io.stdout(`\n${event.message}\n`);
3176
+ if (event.type === 'warning') {
3177
+ io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveValues)}\n`);
3178
+ }
3179
+ };
3180
+ }
3181
+ function requireValue(value, label) {
3182
+ if (!value)
3183
+ throw new Error(`${label} is required`);
3184
+ return value;
3185
+ }
3186
+ function promptFrom(values) {
3187
+ const prompt = values.join(' ').trim();
3188
+ if (prompt.length === 0)
3189
+ throw new Error('Prompt is required');
3190
+ return prompt;
3191
+ }
3192
+ function backgroundWorkerArgv(argv) {
3193
+ const filtered = [];
3194
+ let optionsEnded = false;
3195
+ for (let index = 0; index < argv.length; index += 1) {
3196
+ const value = argv[index];
3197
+ if (value === undefined)
3198
+ continue;
3199
+ if (!optionsEnded && value === '--') {
3200
+ optionsEnded = true;
3201
+ filtered.push(value);
3202
+ continue;
3203
+ }
3204
+ if (!optionsEnded && (value === '--bg' || value === '--background')) {
3205
+ continue;
3206
+ }
3207
+ if (!optionsEnded && value === '--session-id') {
3208
+ index += 1;
3209
+ continue;
3210
+ }
3211
+ if (!optionsEnded && value.startsWith('--session-id='))
3212
+ continue;
3213
+ filtered.push(value);
3214
+ }
3215
+ return filtered;
3216
+ }
3217
+ function agentDashboardWorkerArgv(invocation) {
3218
+ const argv = [];
3219
+ if (invocation.model !== undefined)
3220
+ argv.push('--model', invocation.model);
3221
+ if (invocation.effort !== undefined)
3222
+ argv.push('--effort', invocation.effort);
3223
+ if (invocation.permissionMode !== 'default') {
3224
+ argv.push('--permission-mode', invocation.permissionMode);
3225
+ }
3226
+ if (invocation.dangerouslySkipPermissions) {
3227
+ argv.push('--dangerously-skip-permissions');
3228
+ }
3229
+ if (invocation.allowDangerouslySkipPermissions) {
3230
+ argv.push('--allow-dangerously-skip-permissions');
3231
+ }
3232
+ if (invocation.agent !== undefined)
3233
+ argv.push('--agent', invocation.agent);
3234
+ for (const directory of invocation.addDirectories) {
3235
+ argv.push('--add-dir', directory);
3236
+ }
3237
+ for (const config of invocation.mcpConfigs) {
3238
+ argv.push('--mcp-config', config);
3239
+ }
3240
+ if (invocation.strictMcpConfig)
3241
+ argv.push('--strict-mcp-config');
3242
+ if (invocation.settings !== undefined) {
3243
+ argv.push('--settings', invocation.settings);
3244
+ }
3245
+ if (invocation.settingSources !== undefined) {
3246
+ argv.push('--setting-sources', invocation.settingSources.join(','));
3247
+ }
3248
+ for (const directory of invocation.pluginDirectories) {
3249
+ argv.push('--plugin-dir', directory);
3250
+ }
3251
+ return argv;
3252
+ }
3253
+ function backgroundLaunchMessage(id) {
3254
+ return `backgrounded · ${id}\n praxis agents list sessions\n praxis attach ${id} open in this terminal\n praxis logs ${id} show recent output\n praxis stop ${id} stop this session\n`;
3255
+ }
3256
+ function requireTopLevelAgentManager(dependencies) {
3257
+ if (!dependencies.topLevelAgents) {
3258
+ throw new Error('Top-level agent manager unavailable');
3259
+ }
3260
+ return dependencies.topLevelAgents;
3261
+ }
3262
+ function assertAgentsOptionAllowlist(argv) {
3263
+ const valued = new Set([
3264
+ '--add-dir',
3265
+ '--agent',
3266
+ '--cwd',
3267
+ '--effort',
3268
+ '--mcp-config',
3269
+ '--model',
3270
+ '--permission-mode',
3271
+ '--plugin-dir',
3272
+ '--setting-sources',
3273
+ '--settings',
3274
+ ]);
3275
+ const flags = new Set([
3276
+ '--all',
3277
+ '--allow-dangerously-skip-permissions',
3278
+ '--dangerously-skip-permissions',
3279
+ '--json',
3280
+ '--strict-mcp-config',
3281
+ ]);
3282
+ for (let index = 0; index < argv.length; index += 1) {
3283
+ const value = argv[index];
3284
+ if (value === 'agents')
3285
+ continue;
3286
+ if (value === '--')
3287
+ throw new Error('Unexpected operand for agents');
3288
+ const option = value?.split('=', 1)[0];
3289
+ if (option === '-h' || option === '--help')
3290
+ continue;
3291
+ if (option && flags.has(option)) {
3292
+ if (value.includes('='))
3293
+ throw new Error(`Option does not take a value: ${option}`);
3294
+ continue;
3295
+ }
3296
+ if (option && valued.has(option)) {
3297
+ if (!value.includes('=')) {
3298
+ index += 1;
3299
+ if (argv[index] === undefined)
3300
+ throw new Error(`Missing value for ${option}`);
3301
+ }
3302
+ continue;
3303
+ }
3304
+ if (value?.startsWith('-'))
3305
+ throw new Error(`${value} is not valid with agents`);
3306
+ throw new Error(`Unexpected operand for agents: ${value}`);
3307
+ }
3308
+ }
3309
+ function isCancellation(error, signal) {
3310
+ return (error instanceof AgentRunCancelledError ||
3311
+ signal?.aborted === true ||
3312
+ (error instanceof DOMException && error.name === 'AbortError'));
3313
+ }
3314
+ function emptyCostSnapshot(sessionId) {
3315
+ return {
3316
+ sessionId,
3317
+ totalCostUsd: 0,
3318
+ apiDurationMs: 0,
3319
+ apiDurationWithoutRetriesMs: 0,
3320
+ toolDurationMs: 0,
3321
+ wallDurationMs: 0,
3322
+ linesAdded: 0,
3323
+ linesRemoved: 0,
3324
+ modelUsage: {},
3325
+ hasUnknownModelCost: false,
3326
+ };
3327
+ }
3328
+ /**
3329
+ * Computes the validated nonnegative delta between a current cost snapshot and
3330
+ * a process-local baseline. The baseline represents restored historic state or
3331
+ * the empty state before this process touched the session, so the delta shows
3332
+ * only same-process accumulation. Counter regression and incompatible model
3333
+ * state are rejected explicitly rather than clamped.
3334
+ */
3335
+ function costSummaryDelta(current, baseline) {
3336
+ const base = baseline ?? emptyCostSnapshot(current.sessionId);
3337
+ const assertAtLeast = (label, value, floor) => {
3338
+ if (value < floor) {
3339
+ throw new Error(`Cost state regression: ${label} fell below the process baseline`);
3340
+ }
3341
+ return value - floor;
3342
+ };
3343
+ const totalCostUsd = assertAtLeast('total cost', current.totalCostUsd, base.totalCostUsd);
3344
+ const apiDurationMs = assertAtLeast('API duration', current.apiDurationMs, base.apiDurationMs);
3345
+ const wallDurationMs = assertAtLeast('wall duration', current.wallDurationMs, base.wallDurationMs);
3346
+ const linesAdded = assertAtLeast('lines added', current.linesAdded, base.linesAdded);
3347
+ const linesRemoved = assertAtLeast('lines removed', current.linesRemoved, base.linesRemoved);
3348
+ const modelUsage = [];
3349
+ for (const [model, usage] of Object.entries(current.modelUsage)) {
3350
+ const baseUsage = base.modelUsage[model];
3351
+ if (baseUsage === undefined) {
3352
+ modelUsage.push({
3353
+ model,
3354
+ canonicalName: canonicalClaudeCostModelName(model),
3355
+ inputTokens: usage.inputTokens,
3356
+ outputTokens: usage.outputTokens,
3357
+ cacheReadInputTokens: usage.cacheReadInputTokens,
3358
+ cacheCreationInputTokens: usage.cacheCreationInputTokens,
3359
+ webSearchRequests: usage.webSearchRequests,
3360
+ costUsd: usage.costUsd,
3361
+ });
3362
+ continue;
3363
+ }
3364
+ const inputTokens = usage.inputTokens - baseUsage.inputTokens;
3365
+ const outputTokens = usage.outputTokens - baseUsage.outputTokens;
3366
+ const cacheReadInputTokens = usage.cacheReadInputTokens - baseUsage.cacheReadInputTokens;
3367
+ const cacheCreationInputTokens = usage.cacheCreationInputTokens - baseUsage.cacheCreationInputTokens;
3368
+ const webSearchRequests = usage.webSearchRequests - baseUsage.webSearchRequests;
3369
+ const costUsd = usage.costUsd - baseUsage.costUsd;
3370
+ if (inputTokens < 0 ||
3371
+ outputTokens < 0 ||
3372
+ cacheReadInputTokens < 0 ||
3373
+ cacheCreationInputTokens < 0 ||
3374
+ webSearchRequests < 0 ||
3375
+ costUsd < 0) {
3376
+ throw new Error(`Cost state regression for model ${model}: counters fell below the process baseline`);
3377
+ }
3378
+ if (inputTokens === 0 &&
3379
+ outputTokens === 0 &&
3380
+ cacheReadInputTokens === 0 &&
3381
+ cacheCreationInputTokens === 0 &&
3382
+ webSearchRequests === 0 &&
3383
+ costUsd === 0) {
3384
+ continue;
3385
+ }
3386
+ modelUsage.push({
3387
+ model,
3388
+ canonicalName: canonicalClaudeCostModelName(model),
3389
+ inputTokens,
3390
+ outputTokens,
3391
+ cacheReadInputTokens,
3392
+ cacheCreationInputTokens,
3393
+ webSearchRequests,
3394
+ costUsd,
3395
+ });
3396
+ }
3397
+ for (const model of Object.keys(base.modelUsage)) {
3398
+ if (current.modelUsage[model] === undefined) {
3399
+ throw new Error(`Cost state regression for model ${model}: model disappeared from the session tracker`);
3400
+ }
3401
+ }
3402
+ return {
3403
+ totalCostUsd,
3404
+ apiDurationMs,
3405
+ wallDurationMs,
3406
+ linesAdded,
3407
+ linesRemoved,
3408
+ hasUnknownModelCost: current.hasUnknownModelCost && !base.hasUnknownModelCost,
3409
+ modelUsage,
3410
+ };
3411
+ }
3412
+ function helpActionAt(argv, commandIndex, actions) {
3413
+ for (let index = commandIndex + 1; index < argv.length; index += 1) {
3414
+ const value = argv[index];
3415
+ if (value === undefined || value === '--')
3416
+ return undefined;
3417
+ if (actions.includes(value))
3418
+ return { value, index };
3419
+ }
3420
+ return undefined;
3421
+ }
3422
+ function pluginActionHelp(action, marketplaceAction) {
3423
+ if (action === 'marketplace') {
3424
+ return (PLUGIN_MARKETPLACE_ACTION_HELP[marketplaceAction ?? ''] ??
3425
+ PLUGIN_MARKETPLACE_HELP);
3426
+ }
3427
+ return PLUGIN_ACTION_HELP[action ?? ''] ?? PLUGIN_HELP;
3428
+ }
3429
+ function printCommandHelp(argv, io) {
3430
+ const hasHelpFlag = argv.includes('--help') || argv.includes('-h');
3431
+ const commandIndex = argv.findIndex((value) => ['agents', 'mcp', 'plugin', 'plugins', 'auto-mode', 'project'].includes(value));
3432
+ if (commandIndex < 0)
3433
+ return false;
3434
+ const command = argv[commandIndex];
3435
+ if (command === 'agents') {
3436
+ if (!hasHelpFlag)
3437
+ return false;
3438
+ io.stdout(AGENTS_HELP);
3439
+ return true;
3440
+ }
3441
+ if (command === 'mcp') {
3442
+ const action = helpActionAt(argv, commandIndex, [
3443
+ 'help',
3444
+ ...Object.keys(MCP_ACTION_HELP),
3445
+ ]);
3446
+ if (!hasHelpFlag && action?.value !== 'help')
3447
+ return false;
3448
+ const target = action?.value === 'help'
3449
+ ? helpActionAt(argv, action.index, Object.keys(MCP_ACTION_HELP))
3450
+ : action;
3451
+ io.stdout(MCP_ACTION_HELP[target?.value ?? ''] ?? MCP_HELP);
3452
+ return true;
3453
+ }
3454
+ if (command === 'plugin' || command === 'plugins') {
3455
+ const initialAction = helpActionAt(argv, commandIndex, [
3456
+ 'help',
3457
+ 'marketplace',
3458
+ ...Object.keys(PLUGIN_ACTION_HELP),
3459
+ ]);
3460
+ let requestedHelp = hasHelpFlag || initialAction?.value === 'help';
3461
+ if (!requestedHelp && initialAction?.value !== 'marketplace') {
3462
+ return false;
3463
+ }
3464
+ const action = initialAction?.value === 'help'
3465
+ ? helpActionAt(argv, initialAction.index, [
3466
+ 'marketplace',
3467
+ ...Object.keys(PLUGIN_ACTION_HELP),
3468
+ ])
3469
+ : initialAction;
3470
+ if (action?.value === 'marketplace') {
3471
+ const initialMarketplaceAction = helpActionAt(argv, action.index, [
3472
+ 'help',
3473
+ ...Object.keys(PLUGIN_MARKETPLACE_ACTION_HELP),
3474
+ ]);
3475
+ const marketplaceAction = initialMarketplaceAction?.value === 'help'
3476
+ ? helpActionAt(argv, initialMarketplaceAction.index, Object.keys(PLUGIN_MARKETPLACE_ACTION_HELP))
3477
+ : initialMarketplaceAction;
3478
+ requestedHelp ||= initialMarketplaceAction?.value === 'help';
3479
+ if (!requestedHelp)
3480
+ return false;
3481
+ io.stdout(pluginActionHelp(action.value, marketplaceAction?.value));
3482
+ return true;
3483
+ }
3484
+ if (!requestedHelp)
3485
+ return false;
3486
+ io.stdout(pluginActionHelp(action?.value));
3487
+ return true;
3488
+ }
3489
+ if (command === 'auto-mode') {
3490
+ const action = helpActionAt(argv, commandIndex, [
3491
+ 'help',
3492
+ ...Object.keys(AUTO_MODE_ACTION_HELP),
3493
+ ]);
3494
+ if (!hasHelpFlag && action?.value !== 'help')
3495
+ return false;
3496
+ const target = action?.value === 'help'
3497
+ ? helpActionAt(argv, action.index, Object.keys(AUTO_MODE_ACTION_HELP))
3498
+ : action;
3499
+ io.stdout(AUTO_MODE_ACTION_HELP[target?.value ?? ''] ?? AUTO_MODE_HELP);
3500
+ return true;
3501
+ }
3502
+ if (command === 'project') {
3503
+ const action = helpActionAt(argv, commandIndex, ['help', 'purge']);
3504
+ if (!hasHelpFlag && action?.value !== 'help')
3505
+ return false;
3506
+ const target = action?.value === 'help'
3507
+ ? helpActionAt(argv, action.index, ['purge'])
3508
+ : action;
3509
+ io.stdout(target?.value === 'purge' ? PROJECT_PURGE_HELP : PROJECT_HELP);
3510
+ return true;
3511
+ }
3512
+ return false;
3513
+ }
3514
+ async function execute(argv, io, dependencies, signal) {
3515
+ if (argv[0] === '__background-worker') {
3516
+ await runBackgroundWorker(requireValue(argv[1], 'Agent ID'));
3517
+ return 0;
3518
+ }
3519
+ if (argv.length === 0 && io.isTTY && dependencies.runInteractive) {
3520
+ return dependencies.runInteractive(signal ? { signal } : {});
3521
+ }
3522
+ if ((argv[0] === 'plugin' || argv[0] === 'plugins') && argv[1] === 'eval') {
3523
+ if (!dependencies.pluginEval)
3524
+ throw new Error('Plugin eval unavailable');
3525
+ const pluginEvalDependencies = dependencies.pluginEval.claudeVersion
3526
+ ? dependencies.pluginEval
3527
+ : {
3528
+ ...dependencies.pluginEval,
3529
+ claudeVersion: await detectInstalledClaudeVersion(),
3530
+ };
3531
+ return executeClaudePluginEvalCommand(argv.slice(2), io, pluginEvalDependencies, signal);
3532
+ }
3533
+ if (printCommandHelp(argv, io))
3534
+ return 0;
3535
+ if (argv[0] === 'doctor' &&
3536
+ (argv.includes('--help') || argv.includes('-h'))) {
3537
+ io.stdout(DOCTOR_HELP);
3538
+ return 0;
3539
+ }
3540
+ if (argv[0] === 'mcp' &&
3541
+ argv.includes('add') &&
3542
+ (argv.includes('--help') || argv.includes('-h'))) {
3543
+ io.stdout(MCP_ADD_HELP);
3544
+ return 0;
3545
+ }
3546
+ if (argv[0] === 'project' && argv[1] === 'purge') {
3547
+ return executeProjectPurgeCommand(argv, io);
3548
+ }
3549
+ if (['install', 'update', 'upgrade'].includes(argv[0] ?? '')) {
3550
+ return executeSelfUpdateCommand(argv, io, dependencies);
3551
+ }
3552
+ if (argv[0]?.startsWith('-') &&
3553
+ argv[argv.indexOf('project') + 1] === 'purge') {
3554
+ const commandIndex = argv.indexOf('project');
3555
+ return executeProjectPurgeCommand([
3556
+ 'project',
3557
+ 'purge',
3558
+ ...argv.slice(0, commandIndex),
3559
+ ...argv.slice(commandIndex + 2),
3560
+ ], io);
3561
+ }
3562
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
3563
+ io.stdout(HELP);
3564
+ return 0;
3565
+ }
3566
+ if (argv.includes('--version') || argv.includes('-v')) {
3567
+ io.stdout(`${VERSION}\n`);
3568
+ return 0;
3569
+ }
3570
+ const invocation = parseCliInvocation(argv);
3571
+ const { agent, args, outputFormat, inputFormat, includePartialMessages } = invocation;
3572
+ const command = args[0];
3573
+ const knownCommand = [
3574
+ 'run',
3575
+ 'resume',
3576
+ 'fork',
3577
+ 'sessions',
3578
+ 'inspect',
3579
+ 'export',
3580
+ 'agents',
3581
+ 'attach',
3582
+ 'logs',
3583
+ 'stop',
3584
+ 'mcp',
3585
+ 'auto-mode',
3586
+ 'plugin',
3587
+ 'plugins',
3588
+ 'doctor',
3589
+ 'install',
3590
+ 'update',
3591
+ 'upgrade',
3592
+ 'project',
3593
+ ].includes(command ?? '');
3594
+ if (args[0] === 'agents')
3595
+ assertAgentsOptionAllowlist(argv);
3596
+ const interactiveResume = invocation.resumeSelector !== undefined &&
3597
+ args.length === (typeof invocation.resumeSelector === 'string' ? 2 : 1);
3598
+ const interactivePromptArgs = command === 'resume' && invocation.resumeSelector !== undefined
3599
+ ? args.slice(2)
3600
+ : !knownCommand
3601
+ ? args
3602
+ : [];
3603
+ const interactivePrompt = interactivePromptArgs.length > 0
3604
+ ? promptFrom(interactivePromptArgs)
3605
+ : undefined;
3606
+ if ((invocation.fallbackModels !== undefined ||
3607
+ invocation.jsonSchema !== undefined ||
3608
+ invocation.maxBudgetUsd !== undefined) &&
3609
+ !invocation.print) {
3610
+ throw new Error('--fallback-model, --json-schema, and --max-budget-usd require --print');
3611
+ }
3612
+ if (invocation.tmux) {
3613
+ if (!dependencies.launchTmux)
3614
+ throw new Error('tmux launcher unavailable');
3615
+ const launched = await dependencies.launchTmux({
3616
+ argv,
3617
+ cwd: process.cwd(),
3618
+ cliPath: dependencies.cliPath ?? fileURLToPath(import.meta.url),
3619
+ ...(invocation.worktreeName === undefined
3620
+ ? {}
3621
+ : { worktreeName: invocation.worktreeName }),
3622
+ mode: invocation.tmux,
3623
+ attach: Boolean(io.isTTY),
3624
+ });
3625
+ if (!io.isTTY)
3626
+ io.stdout(launched.kind === 'iterm'
3627
+ ? `Started iTerm2 pane ${launched.sessionName}\n`
3628
+ : `Started tmux session ${launched.sessionName}\n`);
3629
+ return 0;
3630
+ }
3631
+ if (invocation.rewindFiles !== undefined && args.length > 2) {
3632
+ throw new Error('--rewind-files is a standalone operation and cannot be used with a prompt');
3633
+ }
3634
+ if (io.isTTY &&
3635
+ dependencies.runInteractive &&
3636
+ (args.length === 0 ||
3637
+ interactiveResume ||
3638
+ interactivePrompt !== undefined) &&
3639
+ !invocation.print &&
3640
+ !invocation.background &&
3641
+ !invocation.initOnly &&
3642
+ inputFormat === 'text' &&
3643
+ outputFormat === 'text') {
3644
+ return dependencies.runInteractive({
3645
+ ...(agent === undefined ? {} : { agent }),
3646
+ controls: invocation,
3647
+ ...(interactivePrompt === undefined
3648
+ ? {}
3649
+ : { initialPrompt: interactivePrompt }),
3650
+ resume: {
3651
+ ...(typeof invocation.resumeSelector === 'string'
3652
+ ? {
3653
+ sessionSelector: invocation.resumeSelector,
3654
+ ...(isClaudeSessionId(invocation.resumeSelector)
3655
+ ? { sessionId: invocation.resumeSelector }
3656
+ : {}),
3657
+ }
3658
+ : {}),
3659
+ ...(invocation.resumeSelector === undefined
3660
+ ? {}
3661
+ : { requireSession: true }),
3662
+ ...(invocation.fromPr === undefined
3663
+ ? {}
3664
+ : { fromPr: invocation.fromPr }),
3665
+ ...(invocation.forkSession ? { forkSession: true } : {}),
3666
+ ...(invocation.sessionId === undefined
3667
+ ? {}
3668
+ : { forkSessionId: invocation.sessionId }),
3669
+ ...(invocation.retryInterruptedTools
3670
+ ? { retryInterruptedTools: true }
3671
+ : {}),
3672
+ },
3673
+ ...(signal ? { signal } : {}),
3674
+ });
3675
+ }
3676
+ if (invocation.initOnly) {
3677
+ const lifecycleService = await dependencies.createService({
3678
+ eventSink: () => undefined,
3679
+ requireProvider: false,
3680
+ exposeToolRegistry: true,
3681
+ ...(signal ? { signal } : {}),
3682
+ controls: invocation,
3683
+ });
3684
+ try {
3685
+ await lifecycleService.lifecycle?.('init', {
3686
+ sessionStart: true,
3687
+ ...(invocation.sessionId === undefined
3688
+ ? {}
3689
+ : { sessionId: invocation.sessionId }),
3690
+ });
3691
+ return 0;
3692
+ }
3693
+ finally {
3694
+ await lifecycleService.close?.();
3695
+ }
3696
+ }
3697
+ const { retryInterruptedTools } = invocation;
3698
+ if (command === 'resume' && args[1] === undefined) {
3699
+ if (invocation.resumeSelector === true) {
3700
+ throw new Error(missingResumeSelectorMessage(invocation));
3701
+ }
3702
+ requireValue(args[1], 'Session ID');
3703
+ }
3704
+ if (retryInterruptedTools &&
3705
+ command !== 'resume' &&
3706
+ invocation.fromPr === undefined) {
3707
+ throw new Error('--retry-interrupted-tools is only valid with resume or --from-pr');
3708
+ }
3709
+ if (agent &&
3710
+ knownCommand &&
3711
+ !['run', 'resume', 'agents'].includes(command ?? 'run')) {
3712
+ throw new Error('--agent is only valid with run or resume');
3713
+ }
3714
+ if ((invocation.agentsAll || invocation.agentsCwd !== undefined) &&
3715
+ command !== 'agents') {
3716
+ throw new Error('--all and --cwd are only valid with agents');
3717
+ }
3718
+ if (invocation.mcpScope !== undefined && command !== 'mcp') {
3719
+ if (command !== 'plugin' && command !== 'plugins') {
3720
+ throw new Error('--scope is only valid with mcp or plugin commands');
3721
+ }
3722
+ }
3723
+ const mcpAction = command === 'mcp' ? args[1] : undefined;
3724
+ if ((invocation.mcpTransport !== undefined ||
3725
+ invocation.mcpEnv.length > 0 ||
3726
+ invocation.mcpHeaders.length > 0 ||
3727
+ invocation.mcpCallbackPort !== undefined ||
3728
+ invocation.mcpClientId !== undefined) &&
3729
+ mcpAction !== 'add') {
3730
+ throw new Error('--transport, --env, --header, --callback-port, and --client-id are only valid with mcp add');
3731
+ }
3732
+ if (invocation.mcpClientSecret &&
3733
+ mcpAction !== 'add' &&
3734
+ mcpAction !== 'add-json') {
3735
+ throw new Error('--client-secret is only valid with mcp add or add-json');
3736
+ }
3737
+ if (invocation.mcpNoBrowser && mcpAction !== 'login') {
3738
+ throw new Error('--no-browser is only valid with mcp login');
3739
+ }
3740
+ if (invocation.mcpDebug && mcpAction !== 'serve') {
3741
+ throw new Error('--debug is only valid with mcp serve');
3742
+ }
3743
+ if (command === 'mcp') {
3744
+ return executeMcpCommand(args, invocation, io, dependencies, signal);
3745
+ }
3746
+ if (command === 'auto-mode') {
3747
+ return executeAutoModeCommand(args, invocation, io, dependencies, signal);
3748
+ }
3749
+ if (invocation.autoModeLabel !== undefined &&
3750
+ !(command === 'auto-mode' && args[1] === 'defaults')) {
3751
+ throw new Error('--label is only valid with auto-mode defaults');
3752
+ }
3753
+ if (invocation.pluginSparsePaths.length > 0 &&
3754
+ !((command === 'plugin' || command === 'plugins') &&
3755
+ args[1] === 'marketplace' &&
3756
+ args[2] === 'add')) {
3757
+ throw new Error('--sparse is only valid with plugin marketplace add');
3758
+ }
3759
+ if (command === 'plugin' || command === 'plugins') {
3760
+ return executePluginCommand(command === 'plugins' ? ['plugin', ...args.slice(1)] : args, invocation, io);
3761
+ }
3762
+ if (command === 'doctor') {
3763
+ return executeDoctorCommand(args, invocation, io);
3764
+ }
3765
+ const expectedOperands = command === 'sessions' || command === 'agents' ? 1 : 2;
3766
+ if ([
3767
+ 'sessions',
3768
+ 'fork',
3769
+ 'inspect',
3770
+ 'export',
3771
+ 'agents',
3772
+ 'attach',
3773
+ 'logs',
3774
+ 'stop',
3775
+ ].includes(command ?? '') &&
3776
+ args.length > expectedOperands) {
3777
+ throw new Error(`Unexpected operand for ${command}: ${args[expectedOperands]}`);
3778
+ }
3779
+ if (command === 'agents') {
3780
+ if (invocation.legacyJson) {
3781
+ const agents = await requireTopLevelAgentManager(dependencies).list({
3782
+ ...(invocation.agentsCwd === undefined
3783
+ ? {}
3784
+ : { cwd: invocation.agentsCwd }),
3785
+ all: invocation.agentsAll,
3786
+ });
3787
+ writeJson(io, agents);
3788
+ return 0;
3789
+ }
3790
+ if (!io.isTTY) {
3791
+ throw new Error("'praxis agents' requires an interactive terminal (stdout is not a TTY) — use 'praxis agents --json' for a machine-readable listing.");
3792
+ }
3793
+ const manager = requireTopLevelAgentManager(dependencies);
3794
+ if (!dependencies.runAgentsDashboard) {
3795
+ throw new Error('Agents dashboard unavailable');
3796
+ }
3797
+ return dependencies.runAgentsDashboard({
3798
+ manager,
3799
+ defaults: {
3800
+ argv: agentDashboardWorkerArgv(invocation),
3801
+ ...(invocation.agentsCwd === undefined
3802
+ ? {}
3803
+ : { cwd: invocation.agentsCwd }),
3804
+ },
3805
+ ...(signal ? { signal } : {}),
3806
+ });
3807
+ }
3808
+ if (command === 'logs') {
3809
+ io.stdout(await requireTopLevelAgentManager(dependencies).logs(requireValue(args[1], 'Agent ID')));
3810
+ return 0;
3811
+ }
3812
+ if (command === 'stop') {
3813
+ const id = requireValue(args[1], 'Agent ID');
3814
+ await requireTopLevelAgentManager(dependencies).stop(id);
3815
+ io.stdout(`stopped ${id}\n`);
3816
+ return 0;
3817
+ }
3818
+ if (command === 'attach') {
3819
+ const input = io.readStdinLines?.();
3820
+ if (!input)
3821
+ throw new Error('attach requires stdin support');
3822
+ await requireTopLevelAgentManager(dependencies).attach(requireValue(args[1], 'Agent ID'), input, (text) => io.stdout(text), signal);
3823
+ return 0;
3824
+ }
3825
+ if (invocation.background) {
3826
+ if (!invocation.sessionPersistence) {
3827
+ throw new Error('--bg requires session persistence');
3828
+ }
3829
+ if (inputFormat !== 'text' || outputFormat !== 'text') {
3830
+ throw new Error('--bg only supports text input and output');
3831
+ }
3832
+ if (command && knownCommand && command !== 'run' && command !== 'resume') {
3833
+ throw new Error(`--bg cannot be combined with ${command}`);
3834
+ }
3835
+ const prompt = promptFrom(command === 'resume'
3836
+ ? args.slice(2)
3837
+ : command === 'run'
3838
+ ? args.slice(1)
3839
+ : args);
3840
+ const explicitResumeSelector = command === 'resume' ? requireValue(args[1], 'Session ID') : undefined;
3841
+ let resumeSessionId;
3842
+ let usedExplicitSessionId = false;
3843
+ if (explicitResumeSelector !== undefined ||
3844
+ invocation.continueSession ||
3845
+ invocation.fromPr !== undefined) {
3846
+ const sessionService = await dependencies.createService({
3847
+ eventSink: () => undefined,
3848
+ requireProvider: false,
3849
+ controls: invocation,
3850
+ });
3851
+ try {
3852
+ const sessions = await sessionService.sessions();
3853
+ if (explicitResumeSelector !== undefined) {
3854
+ resumeSessionId = selectResumeSession(sessions, explicitResumeSelector, invocation).sessionId;
3855
+ }
3856
+ else {
3857
+ resumeSessionId = selectImplicitResumeSession(sessions, invocation.fromPr).sessionId;
3858
+ }
3859
+ if (invocation.forkSession) {
3860
+ resumeSessionId = (await sessionService.fork(resumeSessionId, invocation.sessionId)).sessionId;
3861
+ usedExplicitSessionId = invocation.sessionId !== undefined;
3862
+ }
3863
+ }
3864
+ finally {
3865
+ await sessionService.close?.();
3866
+ }
3867
+ }
3868
+ if (invocation.sessionId && !usedExplicitSessionId) {
3869
+ io.stderr('warning: --bg manages the session id; ignoring --session-id (use --resume <id> to continue an existing session)\n');
3870
+ }
3871
+ const launched = await requireTopLevelAgentManager(dependencies).launch({
3872
+ prompt,
3873
+ argv: backgroundWorkerArgv(argv),
3874
+ ...(resumeSessionId === undefined ? {} : { resumeSessionId }),
3875
+ });
3876
+ io.stdout(backgroundLaunchMessage(launched.id));
3877
+ return 0;
3878
+ }
3879
+ let streamOutput;
3880
+ let jsonModelTurns = 0;
3881
+ const pendingEvents = [];
3882
+ let streamIterator;
3883
+ let firstStreamMessage;
3884
+ let streamInputExhausted = false;
3885
+ const queuedStreamUsers = [];
3886
+ const earlyControlResponses = new Map();
3887
+ let currentTurnAbort;
3888
+ if (inputFormat === 'stream-json' && !invocation.legacyJson) {
3889
+ const input = io.readStdinLines?.();
3890
+ if (!input)
3891
+ throw new Error('stream-json input requires stdin support');
3892
+ streamIterator = readStreamJsonMessages(input);
3893
+ }
3894
+ const receiveStreamControl = (message) => {
3895
+ if (!('type' in message)) {
3896
+ queuedStreamUsers.push(message);
3897
+ return;
3898
+ }
3899
+ if (message.type === 'control_request') {
3900
+ if (message.request.subtype === 'interrupt')
3901
+ currentTurnAbort?.abort();
3902
+ return;
3903
+ }
3904
+ if (message.type === 'control_response') {
3905
+ earlyControlResponses.set(message.response.request_id, message);
3906
+ return;
3907
+ }
3908
+ if (message.type === 'control_cancel_request') {
3909
+ earlyControlResponses.set(message.request_id, {
3910
+ type: 'control_response',
3911
+ response: {
3912
+ subtype: 'error',
3913
+ request_id: message.request_id,
3914
+ error: 'Control request cancelled',
3915
+ },
3916
+ });
3917
+ }
3918
+ };
3919
+ const nextStreamMessage = async () => {
3920
+ if (!streamIterator)
3921
+ return null;
3922
+ const next = await streamIterator.next();
3923
+ return next.done ? null : next.value;
3924
+ };
3925
+ const nextStreamUser = async () => {
3926
+ const queued = queuedStreamUsers.shift();
3927
+ if (queued)
3928
+ return queued;
3929
+ for (;;) {
3930
+ const message = await nextStreamMessage();
3931
+ if (!message)
3932
+ return null;
3933
+ if (!('type' in message))
3934
+ return message;
3935
+ receiveStreamControl(message);
3936
+ }
3937
+ };
3938
+ const awaitControlResponse = async (requestId) => {
3939
+ const early = earlyControlResponses.get(requestId);
3940
+ if (early) {
3941
+ earlyControlResponses.delete(requestId);
3942
+ return early;
3943
+ }
3944
+ for (;;) {
3945
+ const message = await nextStreamMessage();
3946
+ if (!message)
3947
+ return null;
3948
+ if (!('type' in message)) {
3949
+ queuedStreamUsers.push(message);
3950
+ continue;
3951
+ }
3952
+ if (message.type === 'control_response') {
3953
+ if (message.response.request_id === requestId)
3954
+ return message;
3955
+ earlyControlResponses.set(message.response.request_id, message);
3956
+ continue;
3957
+ }
3958
+ if (message.type === 'control_cancel_request' &&
3959
+ message.request_id === requestId) {
3960
+ return {
3961
+ type: 'control_response',
3962
+ response: {
3963
+ subtype: 'error',
3964
+ request_id: requestId,
3965
+ error: 'Control request cancelled',
3966
+ },
3967
+ };
3968
+ }
3969
+ receiveStreamControl(message);
3970
+ if (currentTurnAbort?.signal.aborted) {
3971
+ return {
3972
+ type: 'control_response',
3973
+ response: {
3974
+ subtype: 'error',
3975
+ request_id: requestId,
3976
+ error: 'Control request interrupted',
3977
+ },
3978
+ };
3979
+ }
3980
+ }
3981
+ };
3982
+ const approveStreamTool = async (call) => {
3983
+ if (!streamOutput || !streamIterator)
3984
+ return false;
3985
+ const requestId = randomUUID();
3986
+ streamOutput.controlRequest({
3987
+ request_id: requestId,
3988
+ request: {
3989
+ subtype: 'can_use_tool',
3990
+ tool_name: call.name,
3991
+ input: call.input,
3992
+ tool_use_id: call.id,
3993
+ },
3994
+ });
3995
+ const response = await awaitControlResponse(requestId);
3996
+ if (!response || response.response.subtype === 'error')
3997
+ return false;
3998
+ const result = response.response.response;
3999
+ if (!result || result.behavior !== 'allow') {
4000
+ if (result?.interrupt === true)
4001
+ currentTurnAbort?.abort();
4002
+ return false;
4003
+ }
4004
+ return true;
4005
+ };
4006
+ const respondStreamElicitation = async (request) => {
4007
+ if (!streamOutput || !streamIterator)
4008
+ return { action: 'decline' };
4009
+ const requestId = randomUUID();
4010
+ streamOutput.controlRequest({
4011
+ request_id: requestId,
4012
+ request: {
4013
+ subtype: 'elicitation',
4014
+ mcp_server_name: request.serverName,
4015
+ message: request.message,
4016
+ ...(request.mode === undefined ? {} : { mode: request.mode }),
4017
+ ...(request.url === undefined ? {} : { url: request.url }),
4018
+ ...(request.elicitationId === undefined
4019
+ ? {}
4020
+ : { elicitation_id: request.elicitationId }),
4021
+ ...(request.requestedSchema === undefined
4022
+ ? {}
4023
+ : { requested_schema: request.requestedSchema }),
4024
+ },
4025
+ });
4026
+ const response = await awaitControlResponse(requestId);
4027
+ if (!response || response.response.subtype === 'error')
4028
+ return { action: 'decline' };
4029
+ const result = response.response.response;
4030
+ if (!result ||
4031
+ !['accept', 'decline', 'cancel'].includes(String(result.action)))
4032
+ return { action: 'decline' };
4033
+ return {
4034
+ action: result.action,
4035
+ ...(result.content && typeof result.content === 'object'
4036
+ ? {
4037
+ content: result.content,
4038
+ }
4039
+ : {}),
4040
+ };
4041
+ };
4042
+ const headlessPromptArgs = command === 'resume' ? args.slice(2) : knownCommand ? args.slice(1) : args;
4043
+ const headlessPrompt = inputFormat === 'text' && headlessPromptArgs.length > 0
4044
+ ? promptFrom(headlessPromptArgs)
4045
+ : undefined;
4046
+ if (headlessPrompt === '/release-notes' && !invocation.disableSlashCommands) {
4047
+ const startedAt = Date.now();
4048
+ const sessionId = invocation.sessionId ?? randomUUID();
4049
+ const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'));
4050
+ const text = dependencies.loadReleaseNotes
4051
+ ? await dependencies.loadReleaseNotes(configRoot)
4052
+ : await loadClaudeReleaseNotes({ configRoot });
4053
+ const result = {
4054
+ sessionId,
4055
+ text,
4056
+ usage: { inputTokens: 0, outputTokens: 0 },
4057
+ };
4058
+ const info = {
4059
+ cwd: process.cwd(),
4060
+ model: invocation.model ?? process.env.PRAXIS_MODEL ?? 'unknown',
4061
+ tools: [],
4062
+ mcpServers: [],
4063
+ permissionMode: invocation.permissionMode,
4064
+ slashCommands: ['release-notes'],
4065
+ agents: [],
4066
+ skills: [],
4067
+ claudeCodeVersion: '2.1.208',
4068
+ };
4069
+ if (outputFormat === 'stream-json') {
4070
+ const output = new StreamJsonOutput((value) => writeJson(io, value), info, sessionId, includePartialMessages, invocation.includeHookEvents);
4071
+ output.init();
4072
+ output.sink({ type: 'text-delta', delta: text });
4073
+ output.sink({ type: 'usage', usage: result.usage });
4074
+ output.result(result, startedAt);
4075
+ }
4076
+ else if (outputFormat === 'json' || invocation.legacyJson) {
4077
+ writeJson(io, createSuccessResult(result, info, startedAt, 0));
4078
+ }
4079
+ else {
4080
+ io.stdout(`${text}\n`);
4081
+ }
4082
+ return 0;
4083
+ }
4084
+ const headlessTurnReached = invocation.rewindFiles === undefined &&
4085
+ !['sessions', 'fork', 'inspect', 'export'].includes(command ?? 'run');
4086
+ if (streamIterator && headlessTurnReached) {
4087
+ const first = await nextStreamUser();
4088
+ if (first)
4089
+ firstStreamMessage = first;
4090
+ else
4091
+ streamInputExhausted = true;
4092
+ }
4093
+ const firstHeadlessPrompt = firstStreamMessage?.prompt ?? headlessPrompt;
4094
+ const firstTurnIsLocalColor = !invocation.disableSlashCommands &&
4095
+ firstHeadlessPrompt !== undefined &&
4096
+ matchHeadlessColorCommand(firstHeadlessPrompt) !== undefined;
4097
+ const firstTurnIsLocalCost = !invocation.disableSlashCommands &&
4098
+ firstHeadlessPrompt !== undefined &&
4099
+ isHeadlessCostCommand(firstHeadlessPrompt);
4100
+ const service = await dependencies.createService({
4101
+ eventSink: outputFormat === 'stream-json' && !invocation.legacyJson
4102
+ ? (event) => {
4103
+ const safeEvent = event.type === 'warning' || event.type === 'failed'
4104
+ ? {
4105
+ ...event,
4106
+ message: redactSensitiveText(event.message, sensitiveEnvironmentValues(process.env)),
4107
+ }
4108
+ : event;
4109
+ if (streamOutput)
4110
+ streamOutput.sink(safeEvent);
4111
+ else
4112
+ pendingEvents.push(safeEvent);
4113
+ }
4114
+ : outputFormat === 'json'
4115
+ ? (event) => {
4116
+ if (event.type === 'state' && event.state === 'awaiting-model') {
4117
+ jsonModelTurns += 1;
4118
+ }
4119
+ if (event.type === 'warning') {
4120
+ io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveEnvironmentValues(process.env))}\n`);
4121
+ }
4122
+ }
4123
+ : eventSink(io, outputFormat, invocation.legacyJson),
4124
+ requireProvider: !streamInputExhausted &&
4125
+ !firstTurnIsLocalColor &&
4126
+ !firstTurnIsLocalCost &&
4127
+ headlessTurnReached,
4128
+ ...(retryInterruptedTools ? { approveRecovery: () => true } : {}),
4129
+ ...(streamIterator ? { approveTool: approveStreamTool } : {}),
4130
+ ...(streamIterator ? { onElicitation: respondStreamElicitation } : {}),
4131
+ ...(streamIterator ? { emitToolUseSummaries: true } : {}),
4132
+ ...(signal ? { signal } : {}),
4133
+ ...(agent ? { agent } : {}),
4134
+ controls: invocation,
4135
+ });
4136
+ const costBaselines = new Map();
4137
+ const ensureCostBaseline = async (sessionId) => {
4138
+ if (!service.costSnapshot || costBaselines.has(sessionId))
4139
+ return;
4140
+ costBaselines.set(sessionId, await service.costSnapshot(sessionId));
4141
+ };
4142
+ try {
4143
+ const lifecycleTrigger = invocation.init
4144
+ ? 'init'
4145
+ : invocation.maintenance
4146
+ ? 'maintenance'
4147
+ : undefined;
4148
+ if (lifecycleTrigger !== undefined) {
4149
+ await service.lifecycle?.(lifecycleTrigger, {
4150
+ ...(invocation.sessionId === undefined
4151
+ ? {}
4152
+ : { sessionId: invocation.sessionId }),
4153
+ });
4154
+ }
4155
+ if (invocation.rewindFiles !== undefined) {
4156
+ const sessionId = requireValue(args[1], 'Session ID');
4157
+ if (!service.rewindFiles)
4158
+ throw new Error('File rewinding is unavailable');
4159
+ await service.rewindFiles(sessionId, invocation.rewindFiles);
4160
+ io.stdout(`Files rewound to state at message ${invocation.rewindFiles}\n`);
4161
+ return 0;
4162
+ }
4163
+ if (command === 'sessions') {
4164
+ const sessions = await service.sessions();
4165
+ if (outputFormat === 'json' || invocation.legacyJson) {
4166
+ writeJson(io, { type: 'sessions', sessions });
4167
+ }
4168
+ else if (outputFormat === 'stream-json') {
4169
+ for (const session of sessions)
4170
+ writeJson(io, { type: 'session', session });
4171
+ }
4172
+ else {
4173
+ for (const session of sessions) {
4174
+ io.stdout(`${session.sessionId}\t${session.updatedAt}\t${session.lastPrompt ?? ''}\t${session.status}\t${formatSessionIssue(session.issue)}\n`);
4175
+ }
4176
+ }
4177
+ return 0;
4178
+ }
4179
+ if (command === 'fork') {
4180
+ const result = await service.fork(requireValue(args[1], 'Session ID'));
4181
+ if (outputFormat !== 'text')
4182
+ writeJson(io, { type: 'forked', ...result });
4183
+ else
4184
+ io.stdout(`${result.sessionId}\n`);
4185
+ return 0;
4186
+ }
4187
+ if (command === 'inspect') {
4188
+ const session = await service.inspect(requireValue(args[1], 'Session ID'));
4189
+ if (outputFormat !== 'text')
4190
+ writeJson(io, { type: 'session', session });
4191
+ else {
4192
+ io.stdout(`${session.sessionId}\t${session.status}\t${session.writeMode}\t${session.updatedAt}\t${session.entryCount}\t${session.byteLength}\t${session.newlineTerminated}\t${session.lastPrompt ?? ''}\t${formatSessionIssue(session.issue)}\n`);
4193
+ }
4194
+ return 0;
4195
+ }
4196
+ if (command === 'export') {
4197
+ const sessionId = requireValue(args[1], 'Session ID');
4198
+ const transcript = await service.export(sessionId);
4199
+ if (outputFormat !== 'text') {
4200
+ writeJson(io, {
4201
+ type: 'session-export',
4202
+ sessionId,
4203
+ encoding: 'base64',
4204
+ transcript: transcript.toString('base64'),
4205
+ });
4206
+ }
4207
+ else
4208
+ io.stdout(transcript);
4209
+ return 0;
4210
+ }
4211
+ const runtimeInfo = service.runtimeInfo?.() ?? {
4212
+ cwd: process.cwd(),
4213
+ model: process.env.PRAXIS_MODEL ?? 'unknown',
4214
+ tools: [],
4215
+ mcpServers: [],
4216
+ permissionMode: 'default',
4217
+ slashCommands: [],
4218
+ agents: [],
4219
+ skills: [],
4220
+ claudeCodeVersion: 'unknown',
4221
+ };
4222
+ const explicitResumeSelector = command === 'resume' ? requireValue(args[1], 'Session ID') : undefined;
4223
+ let existingSessionId;
4224
+ if (explicitResumeSelector !== undefined ||
4225
+ invocation.continueSession ||
4226
+ invocation.fromPr !== undefined) {
4227
+ const sessions = await service.sessions();
4228
+ existingSessionId =
4229
+ explicitResumeSelector === undefined
4230
+ ? selectImplicitResumeSession(sessions, invocation.fromPr).sessionId
4231
+ : selectResumeSession(sessions, explicitResumeSelector, invocation)
4232
+ .sessionId;
4233
+ }
4234
+ if (existingSessionId && invocation.forkSession) {
4235
+ existingSessionId = (await service.fork(existingSessionId, invocation.sessionId)).sessionId;
4236
+ }
4237
+ let activeSessionId = existingSessionId ?? invocation.sessionId ?? randomUUID();
4238
+ if (outputFormat === 'stream-json' && !invocation.legacyJson) {
4239
+ streamOutput = new StreamJsonOutput((value) => writeJson(io, value), runtimeInfo, activeSessionId, includePartialMessages, invocation.includeHookEvents);
4240
+ }
4241
+ if (streamInputExhausted)
4242
+ return 0;
4243
+ const initialPrompt = firstStreamMessage?.prompt ??
4244
+ promptFrom(command === 'resume'
4245
+ ? args.slice(2)
4246
+ : knownCommand
4247
+ ? args.slice(1)
4248
+ : args);
4249
+ let isFirstTurn = true;
4250
+ const runTurn = async (prompt, streamMessage) => {
4251
+ const startedAt = Date.now();
4252
+ const turnAbort = new AbortController();
4253
+ const forwardAbort = () => turnAbort.abort();
4254
+ if (signal?.aborted)
4255
+ turnAbort.abort();
4256
+ signal?.addEventListener('abort', forwardAbort, { once: true });
4257
+ currentTurnAbort = streamIterator ? turnAbort : undefined;
4258
+ const runSignal = streamIterator ? turnAbort.signal : signal;
4259
+ await ensureCostBaseline(activeSessionId);
4260
+ if (streamOutput) {
4261
+ streamOutput.init();
4262
+ if (isFirstTurn) {
4263
+ for (const event of pendingEvents)
4264
+ streamOutput.sink(event);
4265
+ }
4266
+ if (invocation.replayUserMessages && streamMessage)
4267
+ streamOutput.replayUser(streamMessage.message);
4268
+ }
4269
+ let result;
4270
+ let colorArgs;
4271
+ let costTurn = false;
4272
+ try {
4273
+ costTurn =
4274
+ !invocation.disableSlashCommands && isHeadlessCostCommand(prompt);
4275
+ colorArgs = invocation.disableSlashCommands
4276
+ ? undefined
4277
+ : matchHeadlessColorCommand(prompt);
4278
+ if (costTurn) {
4279
+ if (!service.costSnapshot) {
4280
+ throw new Error('Session cost is unavailable.');
4281
+ }
4282
+ const current = await service.costSnapshot(activeSessionId);
4283
+ const delta = costSummaryDelta(current, costBaselines.get(activeSessionId));
4284
+ const modelUsageEntries = delta.modelUsage.map((entry) => [
4285
+ entry.model,
4286
+ {
4287
+ inputTokens: entry.inputTokens,
4288
+ outputTokens: entry.outputTokens,
4289
+ ...(entry.cacheReadInputTokens === undefined
4290
+ ? {}
4291
+ : { cacheReadInputTokens: entry.cacheReadInputTokens }),
4292
+ ...(entry.cacheCreationInputTokens === undefined
4293
+ ? {}
4294
+ : { cacheCreationInputTokens: entry.cacheCreationInputTokens }),
4295
+ ...(entry.webSearchRequests === undefined
4296
+ ? {}
4297
+ : { webSearchRequests: entry.webSearchRequests }),
4298
+ },
4299
+ ]);
4300
+ result = {
4301
+ sessionId: activeSessionId,
4302
+ text: formatCostSummary(delta),
4303
+ usage: { inputTokens: 0, outputTokens: 0 },
4304
+ durationApiMs: 0,
4305
+ costUsd: delta.totalCostUsd,
4306
+ modelUsage: Object.fromEntries(modelUsageEntries),
4307
+ modelCostUsd: Object.fromEntries(delta.modelUsage.map((entry) => [entry.model, entry.costUsd])),
4308
+ };
4309
+ }
4310
+ else if (colorArgs !== undefined) {
4311
+ if (!service.recordColorUsage) {
4312
+ throw new Error('Session color is unavailable.');
4313
+ }
4314
+ const selection = parseAgentColorInput(colorArgs);
4315
+ const localSessionId = await service.recordColorUsage(activeSessionId, selection, prompt, invocation.permissionMode, isFirstTurn && existingSessionId === undefined
4316
+ ? { createSession: true }
4317
+ : undefined);
4318
+ result = {
4319
+ sessionId: localSessionId,
4320
+ text: agentColorMessage(selection),
4321
+ usage: { inputTokens: 0, outputTokens: 0 },
4322
+ durationApiMs: 0,
4323
+ costUsd: 0,
4324
+ modelUsage: {},
4325
+ };
4326
+ }
4327
+ else {
4328
+ result =
4329
+ existingSessionId !== undefined || !isFirstTurn
4330
+ ? runSignal
4331
+ ? await service.resume(activeSessionId, prompt, runSignal, undefined, streamMessage?.images, streamMessage?.documents)
4332
+ : await service.resume(activeSessionId, prompt, undefined, undefined, streamMessage?.images, streamMessage?.documents)
4333
+ : runSignal
4334
+ ? await service.run(prompt, runSignal, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents)
4335
+ : await service.run(prompt, undefined, activeSessionId, undefined, streamMessage?.images, streamMessage?.documents);
4336
+ }
4337
+ }
4338
+ catch (error) {
4339
+ if (isCancellation(error, turnAbort.signal)) {
4340
+ if (currentTurnAbort === turnAbort)
4341
+ currentTurnAbort = undefined;
4342
+ signal?.removeEventListener('abort', forwardAbort);
4343
+ throw error;
4344
+ }
4345
+ const message = redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveEnvironmentValues(process.env));
4346
+ if (streamOutput)
4347
+ streamOutput.error(message, startedAt);
4348
+ else if (outputFormat === 'json') {
4349
+ writeJson(io, createErrorResult(message, activeSessionId, startedAt, jsonModelTurns));
4350
+ }
4351
+ else {
4352
+ if (currentTurnAbort === turnAbort)
4353
+ currentTurnAbort = undefined;
4354
+ signal?.removeEventListener('abort', forwardAbort);
4355
+ throw error;
4356
+ }
4357
+ if (currentTurnAbort === turnAbort)
4358
+ currentTurnAbort = undefined;
4359
+ signal?.removeEventListener('abort', forwardAbort);
4360
+ return false;
4361
+ }
4362
+ activeSessionId = result.sessionId;
4363
+ await ensureCostBaseline(activeSessionId);
4364
+ const localCommand = colorArgs !== undefined || costTurn;
4365
+ if (streamOutput) {
4366
+ if (localCommand) {
4367
+ streamOutput.syntheticAssistant(result.text);
4368
+ }
4369
+ streamOutput.result(result, startedAt);
4370
+ }
4371
+ else if (outputFormat === 'json') {
4372
+ const resultRuntimeInfo = service.runtimeInfo?.() ?? runtimeInfo;
4373
+ writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, localCommand ? 0 : Math.max(1, jsonModelTurns)));
4374
+ }
4375
+ else if (outputFormat !== 'text')
4376
+ writeJson(io, { type: 'result', ...result });
4377
+ else
4378
+ io.stdout(localCommand ? `${result.text}\n` : '\n');
4379
+ if (streamOutput && invocation.promptSuggestions) {
4380
+ try {
4381
+ const suggestion = await service.promptSuggestion?.(activeSessionId, runSignal);
4382
+ if (suggestion)
4383
+ streamOutput.promptSuggestion(suggestion);
4384
+ }
4385
+ catch {
4386
+ // Prompt suggestions are auxiliary and must not change turn success.
4387
+ }
4388
+ }
4389
+ isFirstTurn = false;
4390
+ if (currentTurnAbort === turnAbort)
4391
+ currentTurnAbort = undefined;
4392
+ signal?.removeEventListener('abort', forwardAbort);
4393
+ return true;
4394
+ };
4395
+ if (!(await runTurn(initialPrompt, firstStreamMessage)))
4396
+ return 1;
4397
+ if (streamIterator) {
4398
+ for (;;) {
4399
+ const message = await nextStreamUser();
4400
+ if (!message)
4401
+ break;
4402
+ if (!(await runTurn(message.prompt, message)))
4403
+ return 1;
4404
+ }
4405
+ }
4406
+ return 0;
4407
+ }
4408
+ finally {
4409
+ try {
4410
+ await service.close?.();
4411
+ }
4412
+ catch (error) {
4413
+ io.stderr(`Warning: ${redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveEnvironmentValues(process.env))}\n`);
4414
+ }
4415
+ }
4416
+ }
4417
+ export async function run(argv, io = consoleIO, dependencies = createDefaultDependencies(), signal) {
4418
+ try {
4419
+ return await execute(argv, io, dependencies, signal);
4420
+ }
4421
+ catch (error) {
4422
+ if (isCancellation(error, signal)) {
4423
+ io.stderr('Praxis run cancelled.\n');
4424
+ return 130;
4425
+ }
4426
+ const message = redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveEnvironmentValues(process.env));
4427
+ if (argv.includes('--json'))
4428
+ writeJson(io, { type: 'error', message });
4429
+ else
4430
+ io.stderr(`${message}\n`);
4431
+ return 1;
4432
+ }
4433
+ }
4434
+ //# sourceMappingURL=cli-runtime.js.map