praxis-agent 0.19.0 → 0.20.1

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.
@@ -14,7 +14,7 @@ import type { AgentColorName } from '../compatibility/claude/agent-color.js';
14
14
  import type { ClaudeResourceScope } from '../compatibility/claude/shared-resources.js';
15
15
  import type { TuiHookConfiguration } from './tui/hook-settings.js';
16
16
  import { type TuiSlashCommand } from './tui/slash-commands.js';
17
- import { type DoctorReport } from '../maintenance/doctor.js';
17
+ import { type DoctorProgressListener, type DoctorReport } from '../maintenance/doctor.js';
18
18
  import { type TuiAgentEntry, type TuiFileEntry } from './tui/file-picker.js';
19
19
  import { type TuiEditorOptions, type TuiEditorResult } from './tui/external-editor.js';
20
20
  import { type TuiClipboardContent } from './tui/clipboard.js';
@@ -135,7 +135,7 @@ interface InteractiveAppProps {
135
135
  allowDangerouslySkipPermissions?: boolean;
136
136
  additionalDirectories?: readonly string[];
137
137
  diffLoader?: () => Promise<TuiDiffSnapshot>;
138
- doctorLoader?: () => Promise<DoctorReport>;
138
+ doctorLoader?: (onProgress?: DoctorProgressListener) => Promise<DoctorReport>;
139
139
  fileLoader?: () => Promise<readonly TuiFileEntry[]>;
140
140
  externalEditor?: (prompt: string, options: TuiEditorOptions) => Promise<TuiEditorResult>;
141
141
  keybindingsConfigRoot?: string;
@@ -20,7 +20,7 @@ import { addTuiPermissionRule, loadTuiPermissionRules, removeTuiPermissionRule,
20
20
  import { createRecentlyDeniedStore, } from './tui/recently-denied.js';
21
21
  import { agentColorMessage, parseAgentColorInput, } from '../compatibility/claude/agent-color.js';
22
22
  import { filterTuiSlashCommands, mergeTuiSlashCommands, slashCommandQuery, } from './tui/slash-commands.js';
23
- import { runDoctor } from '../maintenance/doctor.js';
23
+ import { runDoctor, } from '../maintenance/doctor.js';
24
24
  import { canonicalClaudeCostModelName, formatCostSummary, } from './tui/cost-summary.js';
25
25
  import { createComposerEditor, deleteComposerBackward, deleteComposerForward, deleteComposerToEnd, deleteComposerToStart, deleteComposerWordBackward, insertComposerText, moveComposerCursor, moveComposerCursorByWord, } from './tui/composer-editor.js';
26
26
  import { applyMentionReference, fileReferenceAtCursor, filterTuiMentionEntries, loadTuiFileEntries, } from './tui/file-picker.js';
@@ -274,7 +274,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
274
274
  const runtimeGitignoreRef = useRef(true);
275
275
  const loadDiffSnapshot = useMemo(() => diffLoader ?? (() => loadGitDiff(runtimeCwd)), [diffLoader, runtimeCwd]);
276
276
  const loadDoctorReport = useMemo(() => doctorLoader ??
277
- (async () => {
277
+ (async (onProgress) => {
278
278
  const configuredRoot = process.env.CLAUDE_CONFIG_DIR || undefined;
279
279
  const configRoot = resolve(configuredRoot ?? resolve(homedir(), '.claude'));
280
280
  const claudeStatePath = configuredRoot
@@ -295,6 +295,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
295
295
  ...(process.argv[1] === undefined
296
296
  ? {}
297
297
  : { invokedBinaryPath: process.argv[1] }),
298
+ ...(onProgress === undefined ? {} : { onProgress }),
298
299
  });
299
300
  }), [doctorLoader, runtimeCwd, display.version, configTarget]);
300
301
  const loadFiles = useMemo(() => fileLoader ??
@@ -1849,7 +1850,18 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
1849
1850
  const loading = (async () => {
1850
1851
  setBusy(true);
1851
1852
  try {
1852
- const report = await loadDoctorReport();
1853
+ const report = await loadDoctorReport((progress) => {
1854
+ const current = menuRef.current;
1855
+ if (current?.kind === 'doctor' && current.generation === generation) {
1856
+ updateMenu({
1857
+ kind: 'doctor',
1858
+ generation,
1859
+ loading: false,
1860
+ report: progress,
1861
+ error: null,
1862
+ });
1863
+ }
1864
+ });
1853
1865
  const current = menuRef.current;
1854
1866
  if (current?.kind === 'doctor' && current.generation === generation) {
1855
1867
  updateMenu({
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
- import type { DoctorCheck, DoctorReport } from '../../maintenance/doctor.js';
2
+ import type { DoctorCheck, DoctorProgressReport, DoctorReport } from '../../maintenance/doctor.js';
3
3
  export interface DoctorWarningGroup {
4
4
  heading: string;
5
5
  checks: readonly DoctorCheck[];
6
6
  }
7
- export declare function projectDoctorWarningGroups(report: DoctorReport): readonly DoctorWarningGroup[];
7
+ export declare function projectDoctorWarningGroups(report: DoctorReport | DoctorProgressReport): readonly DoctorWarningGroup[];
8
8
  export declare function DoctorDashboard({ loading, report, error, width, screenReader, }: {
9
9
  loading: boolean;
10
- report: DoctorReport | null;
10
+ report: DoctorReport | DoctorProgressReport | null;
11
11
  error: string | null;
12
12
  width: number;
13
13
  screenReader: boolean;
@@ -77,6 +77,6 @@ export function DoctorDashboard({ loading, report, error, width, screenReader, }
77
77
  if (report === null)
78
78
  return null;
79
79
  const { diagnostic, updates } = report;
80
- return (_jsxs(Box, { flexDirection: "column", width: Math.min(100, width), children: [_jsx(Text, { bold: true, children: " Doctor:" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Diagnostics" }), _jsx(Text, { children: `Currently running: Praxis ${diagnostic.version} (${diagnostic.installationType})` }), diagnostic.packageManager !== null && (_jsx(Text, { children: `Package manager: ${diagnostic.packageManager}` })), _jsx(Text, { children: `Path: ${diagnostic.installationPath}` }), _jsx(Text, { children: `Invoked: ${diagnostic.invokedBinary}` }), _jsx(Text, { children: `Config install method: ${diagnostic.configInstallMethod}` }), diagnostic.search.working ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: `Search: OK (${diagnostic.search.mode})` }), diagnostic.search.systemPath !== null && (_jsx(Text, { children: `└ ${diagnostic.search.systemPath}` }))] })) : (_jsx(Text, { children: "Search: unavailable" })), diagnostic.multipleInstallations.length > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Multiple installations found" }), diagnostic.multipleInstallations.map((path) => (_jsx(Text, { children: `- ${path}` }, path)))] })), diagnostic.recommendation !== null && (_jsx(Text, { children: `Recommendation: ${diagnostic.recommendation}` })), diagnostic.warnings.map((warning) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: warning.issue }), _jsx(Text, { children: `└ Fix: ${warning.fix}` })] }, warning.issue))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Updates" }), _jsx(Text, { children: `Auto-updates: ${updates.autoUpdates}` }), updates.hasUpdatePermissions !== null && (_jsx(Text, { children: `Update permissions: ${updates.hasUpdatePermissions ? 'yes' : 'no'}` })), _jsx(Text, { children: `Auto-update channel: ${updates.channel}` }), updates.registryStatus === 'available' ? (_jsxs(_Fragment, { children: [updates.stableVersion !== null && (_jsx(Text, { children: `Stable version: ${updates.stableVersion}` })), _jsx(Text, { children: `Latest version: ${updates.latestVersion ?? 'unknown'}` })] })) : (_jsx(Text, { children: "\u2514 Failed to fetch versions" })), _jsx(WarningGroups, { report: report, screenReader: screenReader }), _jsx(Text, { children: " " }), _jsx(Text, { children: doctorSummary(report) }), _jsx(Text, { children: `Summary: ${report.summary.passed} passed, ${report.summary.warnings} warnings, ${report.summary.failed} failed.` }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: footer })] }));
80
+ return (_jsxs(Box, { flexDirection: "column", width: Math.min(100, width), children: [_jsx(Text, { bold: true, children: " Doctor:" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Diagnostics" }), _jsx(Text, { children: `Currently running: Praxis ${diagnostic.version} (${diagnostic.installationType})` }), diagnostic.packageManager !== null && (_jsx(Text, { children: `Package manager: ${diagnostic.packageManager}` })), _jsx(Text, { children: `Path: ${diagnostic.installationPath}` }), _jsx(Text, { children: `Invoked: ${diagnostic.invokedBinary}` }), _jsx(Text, { children: `Config install method: ${diagnostic.configInstallMethod}` }), diagnostic.search.working ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: `Search: OK (${diagnostic.search.mode})` }), diagnostic.search.systemPath !== null && (_jsx(Text, { children: `└ ${diagnostic.search.systemPath}` }))] })) : (_jsx(Text, { children: "Search: unavailable" })), diagnostic.multipleInstallations.length > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Multiple installations found" }), diagnostic.multipleInstallations.map((path) => (_jsx(Text, { children: `- ${path}` }, path)))] })), diagnostic.recommendation !== null && (_jsx(Text, { children: `Recommendation: ${diagnostic.recommendation}` })), diagnostic.warnings.map((warning) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: warning.issue }), _jsx(Text, { children: `└ Fix: ${warning.fix}` })] }, warning.issue))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Updates" }), _jsx(Text, { children: `Auto-updates: ${updates.autoUpdates}` }), updates.hasUpdatePermissions !== null && (_jsx(Text, { children: `Update permissions: ${updates.hasUpdatePermissions ? 'yes' : 'no'}` })), _jsx(Text, { children: `Auto-update channel: ${updates.channel}` }), updates.registryStatus === 'available' ? (_jsxs(_Fragment, { children: [updates.stableVersion !== null && (_jsx(Text, { children: `Stable version: ${updates.stableVersion}` })), _jsx(Text, { children: `Latest version: ${updates.latestVersion ?? 'unknown'}` })] })) : updates.registryStatus === 'loading' ? (_jsx(Text, { children: "Checking for updates\u2026" })) : (_jsx(Text, { children: "\u2514 Failed to fetch versions" })), _jsx(WarningGroups, { report: report, screenReader: screenReader }), _jsx(Text, { children: " " }), _jsx(Text, { children: doctorSummary(report) }), _jsx(Text, { children: `Summary: ${report.summary.passed} passed, ${report.summary.warnings} warnings, ${report.summary.failed} failed.` }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: footer })] }));
81
81
  }
82
82
  //# sourceMappingURL=doctor-dashboard.js.map
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ import { ClaudeMcpToolRegistry, } from './mcp/claude-mcp-tools.js';
31
31
  import { ClaudeMcpManagement, filterDisabledMcpResources, mcpScope, } from './mcp/claude-mcp-management.js';
32
32
  import { authenticateMcpServer, ClaudeMcpOAuthStore, mcpOAuthServerIdentity, readMcpClientSecret, } from './mcp/claude-mcp-oauth.js';
33
33
  import { servePraxisMcpStdio } from './mcp/praxis-mcp-server.js';
34
+ import { VERIFIED_CLAUDE_SCHEMA_VERSION } from './compatibility/claude/schema.js';
34
35
  import { detectInstalledClaudeVersion } from './platform/claude-version.js';
35
36
  import { redactSensitiveText, sensitiveEnvironmentValues, } from './platform/sensitive-data.js';
36
37
  import { AnthropicCompatibleProvider } from './providers/anthropic-compatible.js';
@@ -697,7 +698,7 @@ const consoleIO = {
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, }) => {
698
699
  const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
699
700
  const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
700
- const claudeVersion = await detectInstalledClaudeVersion();
701
+ const claudeVersion = VERIFIED_CLAUDE_SCHEMA_VERSION;
701
702
  const cwd = requestedCwd ?? process.cwd();
702
703
  const workspace = new WorkspaceContext(cwd);
703
704
  const configuredRoot = runtimeEnvironment.CLAUDE_CONFIG_DIR || undefined;
@@ -4436,8 +4437,14 @@ function isDirectExecution(moduleUrl, argvPath) {
4436
4437
  if (isDirectExecution(import.meta.url, process.argv[1])) {
4437
4438
  const controller = new AbortController();
4438
4439
  const cancel = () => controller.abort();
4439
- process.once('SIGINT', cancel);
4440
- process.exitCode = await run(process.argv.slice(2), consoleIO, defaultDependencies, controller.signal);
4441
- process.removeListener('SIGINT', cancel);
4440
+ process.on('SIGINT', cancel);
4441
+ process.on('SIGTERM', cancel);
4442
+ try {
4443
+ process.exitCode = await run(process.argv.slice(2), consoleIO, defaultDependencies, controller.signal);
4444
+ }
4445
+ finally {
4446
+ process.removeListener('SIGINT', cancel);
4447
+ process.removeListener('SIGTERM', cancel);
4448
+ }
4442
4449
  }
4443
4450
  //# sourceMappingURL=cli.js.map
@@ -11,6 +11,7 @@ export interface ClaudeSchemaAdapter {
11
11
  serializeForSidechainAppend(entry: ClaudeTranscriptEntry): string;
12
12
  serializeForFork(entry: ClaudeTranscriptEntry): string;
13
13
  }
14
+ export declare const VERIFIED_CLAUDE_SCHEMA_VERSION = "2.1.208";
14
15
  export declare function isClaudeForkableEntryType(type: string): boolean;
15
16
  export declare function copyClaudeEntryWithSessionId(entry: ClaudeTranscriptEntry, sessionId: string): ClaudeTranscriptEntry;
16
17
  export declare function selectClaudeSchemaAdapter(version: string): ClaudeSchemaAdapter;
@@ -1,5 +1,5 @@
1
1
  import { isAgentColorValue } from './agent-color.js';
2
- const SUPPORTED_VERSION = '2.1.208';
2
+ export const VERIFIED_CLAUDE_SCHEMA_VERSION = '2.1.208';
3
3
  const APPENDABLE_ENTRY_TYPES = new Set([
4
4
  'agent-color',
5
5
  'agent-name',
@@ -775,8 +775,8 @@ function validateAppendableEntry(entry) {
775
775
  throw new Error(`Claude transcript entry is missing ${field}`);
776
776
  }
777
777
  }
778
- if (entry.version !== SUPPORTED_VERSION) {
779
- throw new Error(`Claude transcript append must target Claude Code ${SUPPORTED_VERSION}`);
778
+ if (entry.version !== VERIFIED_CLAUDE_SCHEMA_VERSION) {
779
+ throw new Error(`Claude transcript append must target Claude Code ${VERIFIED_CLAUDE_SCHEMA_VERSION}`);
780
780
  }
781
781
  if (!('parentUuid' in entry) ||
782
782
  (entry.parentUuid !== null && !isNonEmptyString(entry.parentUuid))) {
@@ -836,8 +836,8 @@ function validateSidechainEntry(entry) {
836
836
  throw new Error(`Claude sidechain entry is missing ${field}`);
837
837
  }
838
838
  }
839
- if (entry.version !== SUPPORTED_VERSION) {
840
- throw new Error(`Claude sidechain append must target Claude Code ${SUPPORTED_VERSION}`);
839
+ if (entry.version !== VERIFIED_CLAUDE_SCHEMA_VERSION) {
840
+ throw new Error(`Claude sidechain append must target Claude Code ${VERIFIED_CLAUDE_SCHEMA_VERSION}`);
841
841
  }
842
842
  if (!('parentUuid' in entry) ||
843
843
  (entry.parentUuid !== null && !isNonEmptyString(entry.parentUuid))) {
@@ -925,8 +925,8 @@ function validateForkableEntry(entry) {
925
925
  throw new Error(`Claude transcript entry is missing ${field}`);
926
926
  }
927
927
  }
928
- if (entry.version !== SUPPORTED_VERSION) {
929
- throw new Error(`Claude transcript fork must target Claude Code ${SUPPORTED_VERSION}`);
928
+ if (entry.version !== VERIFIED_CLAUDE_SCHEMA_VERSION) {
929
+ throw new Error(`Claude transcript fork must target Claude Code ${VERIFIED_CLAUDE_SCHEMA_VERSION}`);
930
930
  }
931
931
  if (!('parentUuid' in entry) ||
932
932
  (entry.parentUuid !== null && !isNonEmptyString(entry.parentUuid))) {
@@ -969,7 +969,7 @@ function validateForkableEntry(entry) {
969
969
  validateForkAssistantMessage(entry.message);
970
970
  }
971
971
  class ClaudeCode21208Adapter {
972
- version = SUPPORTED_VERSION;
972
+ version = VERIFIED_CLAUDE_SCHEMA_VERSION;
973
973
  writeMode = 'read-write';
974
974
  parse(line) {
975
975
  return parseEntry(line);
@@ -1019,7 +1019,7 @@ class ReadOnlyClaudeAdapter {
1019
1019
  }
1020
1020
  }
1021
1021
  export function selectClaudeSchemaAdapter(version) {
1022
- if (version === SUPPORTED_VERSION) {
1022
+ if (version === VERIFIED_CLAUDE_SCHEMA_VERSION) {
1023
1023
  return new ClaudeCode21208Adapter();
1024
1024
  }
1025
1025
  return new ReadOnlyClaudeAdapter(version);
@@ -20,15 +20,22 @@ export interface DoctorInstallationDiagnostic {
20
20
  readonly multipleInstallations: readonly string[];
21
21
  readonly warnings: readonly DoctorDiagnosticWarning[];
22
22
  }
23
- export interface DoctorUpdateDiagnostic {
23
+ export interface DoctorUpdateBaseDiagnostic {
24
24
  readonly autoUpdates: string;
25
25
  readonly hasUpdatePermissions: boolean | null;
26
26
  readonly channel: 'latest' | 'stable';
27
+ }
28
+ export interface DoctorUpdateDiagnostic extends DoctorUpdateBaseDiagnostic {
27
29
  readonly stableVersion: string | null;
28
30
  readonly latestVersion: string | null;
29
31
  readonly registryStatus: 'available' | 'unavailable';
30
32
  readonly error?: string;
31
33
  }
34
+ export interface DoctorPendingUpdateDiagnostic extends DoctorUpdateBaseDiagnostic {
35
+ readonly stableVersion: null;
36
+ readonly latestVersion: null;
37
+ readonly registryStatus: 'loading';
38
+ }
32
39
  export interface PraxisDistTags {
33
40
  stable?: string;
34
41
  latest?: string;
@@ -49,6 +56,12 @@ export interface DoctorDiagnosticsResult {
49
56
  diagnostic: DoctorInstallationDiagnostic;
50
57
  updates: DoctorUpdateDiagnostic;
51
58
  }
59
+ export interface DoctorLocalDiagnosticsResult {
60
+ diagnostic: DoctorInstallationDiagnostic;
61
+ updates: DoctorPendingUpdateDiagnostic;
62
+ }
52
63
  export declare function loadPraxisDistTags(): Promise<PraxisDistTags>;
64
+ export declare function resolveDoctorUpdates(options: DoctorDiagnosticOptions, pendingUpdates: DoctorPendingUpdateDiagnostic): Promise<DoctorUpdateDiagnostic>;
65
+ export declare function collectDoctorLocalDiagnostics(options: DoctorDiagnosticOptions): Promise<DoctorLocalDiagnosticsResult>;
53
66
  export declare function collectDoctorDiagnostics(options: DoctorDiagnosticOptions): Promise<DoctorDiagnosticsResult>;
54
67
  //# sourceMappingURL=doctor-diagnostic.d.ts.map
@@ -96,9 +96,21 @@ async function updatePermissions(directory) {
96
96
  return code === 'EACCES' || code === 'EPERM' ? false : null;
97
97
  }
98
98
  }
99
- async function buildUpdateDiagnostic(options, installation) {
99
+ async function buildPendingUpdateDiagnostic(options, installation) {
100
100
  const checkUpdatePermissions = options.checkUpdatePermissions ?? updatePermissions;
101
101
  const hasUpdatePermissions = await checkUpdatePermissions(dirname(installation.invokedBinary));
102
+ return {
103
+ autoUpdates: installation.installationType === 'npm'
104
+ ? 'Manual (praxis update)'
105
+ : 'Managed by source checkout',
106
+ hasUpdatePermissions,
107
+ channel: options.autoUpdateChannel,
108
+ stableVersion: null,
109
+ latestVersion: null,
110
+ registryStatus: 'loading',
111
+ };
112
+ }
113
+ export async function resolveDoctorUpdates(options, pendingUpdates) {
102
114
  let stableVersion = null;
103
115
  let latestVersion = null;
104
116
  let registryStatus = 'unavailable';
@@ -120,18 +132,16 @@ async function buildUpdateDiagnostic(options, installation) {
120
132
  error = `${REGISTRY_ERROR}: ${cause instanceof Error ? cause.message : String(cause)}`;
121
133
  }
122
134
  return {
123
- autoUpdates: installation.installationType === 'npm'
124
- ? 'Manual (praxis update)'
125
- : 'Managed by source checkout',
126
- hasUpdatePermissions,
127
- channel: options.autoUpdateChannel,
135
+ autoUpdates: pendingUpdates.autoUpdates,
136
+ hasUpdatePermissions: pendingUpdates.hasUpdatePermissions,
137
+ channel: pendingUpdates.channel,
128
138
  stableVersion,
129
139
  latestVersion,
130
140
  registryStatus,
131
141
  ...(error === undefined ? {} : { error }),
132
142
  };
133
143
  }
134
- export async function collectDoctorDiagnostics(options) {
144
+ export async function collectDoctorLocalDiagnostics(options) {
135
145
  const installationPath = await readableExecutable(options.executablePath);
136
146
  const invokedBinary = resolve(options.invokedBinaryPath ?? options.executablePath);
137
147
  const installationType = installationPath
@@ -171,10 +181,15 @@ export async function collectDoctorDiagnostics(options) {
171
181
  multipleInstallations,
172
182
  warnings,
173
183
  };
174
- const updates = await buildUpdateDiagnostic(options, {
184
+ const updates = await buildPendingUpdateDiagnostic(options, {
175
185
  installationType,
176
186
  invokedBinary,
177
187
  });
178
188
  return { diagnostic, updates };
179
189
  }
190
+ export async function collectDoctorDiagnostics(options) {
191
+ const local = await collectDoctorLocalDiagnostics(options);
192
+ const updates = await resolveDoctorUpdates(options, local.updates);
193
+ return { diagnostic: local.diagnostic, updates };
194
+ }
180
195
  //# sourceMappingURL=doctor-diagnostic.js.map
@@ -1,5 +1,5 @@
1
1
  import { type VersionCommand } from '../platform/claude-version.js';
2
- import { type DoctorInstallationDiagnostic, type DoctorUpdateDiagnostic, type PraxisDistTagLoader } from './doctor-diagnostic.js';
2
+ import { type DoctorInstallationDiagnostic, type DoctorPendingUpdateDiagnostic, type DoctorUpdateDiagnostic, type PraxisDistTagLoader } from './doctor-diagnostic.js';
3
3
  export interface DoctorCheck {
4
4
  id: 'installation' | 'node' | 'provider' | 'config-root' | 'settings' | 'plugins' | 'mcp' | 'permissions' | 'resources' | 'hooks' | 'claude-runtime';
5
5
  status: 'pass' | 'warn' | 'fail';
@@ -19,6 +19,10 @@ export interface DoctorReport {
19
19
  failed: number;
20
20
  };
21
21
  }
22
+ export type DoctorProgressReport = Omit<DoctorReport, 'updates'> & {
23
+ updates: DoctorPendingUpdateDiagnostic;
24
+ };
25
+ export type DoctorProgressListener = (report: DoctorProgressReport) => void;
22
26
  export interface DoctorOptions {
23
27
  version: string;
24
28
  executablePath: string;
@@ -33,6 +37,7 @@ export interface DoctorOptions {
33
37
  autoUpdateChannel: 'latest' | 'stable';
34
38
  invokedBinaryPath?: string;
35
39
  loadDistTags?: PraxisDistTagLoader;
40
+ onProgress?: DoctorProgressListener;
36
41
  }
37
42
  export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
38
43
  export declare function formatDoctorReport(report: DoctorReport): string;
@@ -15,7 +15,7 @@ import { ContextBudget } from '../core/context-budget.js';
15
15
  import { ModelPricingRegistry } from '../core/usage.js';
16
16
  import { detectInstalledClaudeVersion, } from '../platform/claude-version.js';
17
17
  import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
18
- import { collectDoctorDiagnostics, } from './doctor-diagnostic.js';
18
+ import { collectDoctorLocalDiagnostics, resolveDoctorUpdates, } from './doctor-diagnostic.js';
19
19
  function isRecord(value) {
20
20
  return typeof value === 'object' && value !== null && !Array.isArray(value);
21
21
  }
@@ -129,7 +129,7 @@ export async function runDoctor(options) {
129
129
  const configRoot = resolve(options.configRoot);
130
130
  const cwd = resolve(options.cwd);
131
131
  const sensitiveValues = sensitiveEnvironmentValues(options.environment);
132
- const { diagnostic, updates } = await collectDoctorDiagnostics({
132
+ const diagnosticOptions = {
133
133
  version: options.version,
134
134
  executablePath: options.executablePath,
135
135
  ...(options.invokedBinaryPath === undefined
@@ -141,7 +141,9 @@ export async function runDoctor(options) {
141
141
  ...(options.loadDistTags === undefined
142
142
  ? {}
143
143
  : { loadDistTags: options.loadDistTags }),
144
- });
144
+ };
145
+ const local = await collectDoctorLocalDiagnostics(diagnosticOptions);
146
+ const updatesPromise = resolveDoctorUpdates(diagnosticOptions, local.updates);
145
147
  let settings;
146
148
  const checks = [];
147
149
  checks.push(await capture('installation', async () => {
@@ -372,26 +374,28 @@ export async function runDoctor(options) {
372
374
  check.summary = redactSensitiveText(check.summary, sensitiveValues);
373
375
  }
374
376
  }
375
- const reportUpdates = updates.error === undefined
376
- ? updates
377
- : {
378
- ...updates,
379
- error: redactSensitiveText(updates.error, sensitiveValues),
380
- };
381
377
  const summary = {
382
378
  passed: checks.filter((check) => check.status === 'pass').length,
383
379
  warnings: checks.filter((check) => check.status === 'warn').length,
384
380
  failed: checks.filter((check) => check.status === 'fail').length,
385
381
  };
386
- return {
382
+ const base = {
387
383
  type: 'doctor',
388
384
  ok: checks.every((check) => check.status !== 'fail'),
389
385
  praxisVersion: options.version,
390
- diagnostic,
391
- updates: reportUpdates,
386
+ diagnostic: local.diagnostic,
392
387
  checks,
393
388
  summary,
394
389
  };
390
+ options.onProgress?.({ ...base, updates: local.updates });
391
+ const updates = await updatesPromise;
392
+ const reportUpdates = updates.error === undefined
393
+ ? updates
394
+ : {
395
+ ...updates,
396
+ error: redactSensitiveText(updates.error, sensitiveValues),
397
+ };
398
+ return { ...base, updates: reportUpdates };
395
399
  }
396
400
  export function formatDoctorReport(report) {
397
401
  const lines = ['Praxis doctor', ''];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",