praxis-agent 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/interactive.js
CHANGED
|
@@ -274,12 +274,13 @@ 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
|
-
(() => {
|
|
277
|
+
(async () => {
|
|
278
278
|
const configuredRoot = process.env.CLAUDE_CONFIG_DIR || undefined;
|
|
279
279
|
const configRoot = resolve(configuredRoot ?? resolve(homedir(), '.claude'));
|
|
280
280
|
const claudeStatePath = configuredRoot
|
|
281
281
|
? join(configRoot, '.claude.json')
|
|
282
282
|
: resolve(homedir(), '.claude.json');
|
|
283
|
+
const runtimeSettings = await loadRuntimeSettings(configTarget);
|
|
283
284
|
return runDoctor({
|
|
284
285
|
version: display.version,
|
|
285
286
|
executablePath: resolve(process.argv[1] ??
|
|
@@ -290,8 +291,12 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
290
291
|
claudeStatePath,
|
|
291
292
|
cwd: runtimeCwd,
|
|
292
293
|
environment: process.env,
|
|
294
|
+
autoUpdateChannel: runtimeSettings.autoUpdatesChannel,
|
|
295
|
+
...(process.argv[1] === undefined
|
|
296
|
+
? {}
|
|
297
|
+
: { invokedBinaryPath: process.argv[1] }),
|
|
293
298
|
});
|
|
294
|
-
}), [doctorLoader, runtimeCwd, display.version]);
|
|
299
|
+
}), [doctorLoader, runtimeCwd, display.version, configTarget]);
|
|
295
300
|
const loadFiles = useMemo(() => fileLoader ??
|
|
296
301
|
(() => loadTuiFileEntries(runtimeCwd, {
|
|
297
302
|
respectGitignore: runtimeGitignoreRef.current,
|
package/dist/cli.js
CHANGED
|
@@ -2078,6 +2078,10 @@ async function executeDoctorCommand(args, invocation, io) {
|
|
|
2078
2078
|
const claudeStatePath = configuredRoot
|
|
2079
2079
|
? join(configRoot, '.claude.json')
|
|
2080
2080
|
: resolve(homedir(), '.claude.json');
|
|
2081
|
+
const runtimeSettings = await loadRuntimeSettings({
|
|
2082
|
+
configRoot,
|
|
2083
|
+
statePath: claudeStatePath,
|
|
2084
|
+
});
|
|
2081
2085
|
const report = await runDoctor({
|
|
2082
2086
|
version: VERSION,
|
|
2083
2087
|
executablePath: fileURLToPath(import.meta.url),
|
|
@@ -2087,6 +2091,10 @@ async function executeDoctorCommand(args, invocation, io) {
|
|
|
2087
2091
|
claudeStatePath,
|
|
2088
2092
|
cwd: process.cwd(),
|
|
2089
2093
|
environment: process.env,
|
|
2094
|
+
autoUpdateChannel: runtimeSettings.autoUpdatesChannel,
|
|
2095
|
+
...(process.argv[1] === undefined
|
|
2096
|
+
? {}
|
|
2097
|
+
: { invokedBinaryPath: process.argv[1] }),
|
|
2090
2098
|
});
|
|
2091
2099
|
if (invocation.legacyJson || invocation.outputFormat === 'json') {
|
|
2092
2100
|
writeJson(io, report);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type DoctorInstallationType = 'npm' | 'source';
|
|
2
|
+
export interface DoctorSearchStatus {
|
|
3
|
+
readonly working: boolean;
|
|
4
|
+
readonly mode: 'system';
|
|
5
|
+
readonly systemPath: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface DoctorDiagnosticWarning {
|
|
8
|
+
readonly issue: string;
|
|
9
|
+
readonly fix: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DoctorInstallationDiagnostic {
|
|
12
|
+
readonly installationType: DoctorInstallationType;
|
|
13
|
+
readonly version: string;
|
|
14
|
+
readonly packageManager: string | null;
|
|
15
|
+
readonly installationPath: string;
|
|
16
|
+
readonly invokedBinary: string;
|
|
17
|
+
readonly configInstallMethod: string;
|
|
18
|
+
readonly search: DoctorSearchStatus;
|
|
19
|
+
readonly recommendation: string | null;
|
|
20
|
+
readonly multipleInstallations: readonly string[];
|
|
21
|
+
readonly warnings: readonly DoctorDiagnosticWarning[];
|
|
22
|
+
}
|
|
23
|
+
export interface DoctorUpdateDiagnostic {
|
|
24
|
+
readonly autoUpdates: string;
|
|
25
|
+
readonly hasUpdatePermissions: boolean | null;
|
|
26
|
+
readonly channel: 'latest' | 'stable';
|
|
27
|
+
readonly stableVersion: string | null;
|
|
28
|
+
readonly latestVersion: string | null;
|
|
29
|
+
readonly registryStatus: 'available' | 'unavailable';
|
|
30
|
+
readonly error?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PraxisDistTags {
|
|
33
|
+
stable?: string;
|
|
34
|
+
latest?: string;
|
|
35
|
+
}
|
|
36
|
+
export type PraxisDistTagLoader = () => Promise<PraxisDistTags>;
|
|
37
|
+
export type DoctorUpdatePermissionChecker = (directory: string) => Promise<boolean | null>;
|
|
38
|
+
export interface DoctorDiagnosticOptions {
|
|
39
|
+
version: string;
|
|
40
|
+
executablePath: string;
|
|
41
|
+
invokedBinaryPath?: string;
|
|
42
|
+
configRoot: string;
|
|
43
|
+
environment: NodeJS.ProcessEnv;
|
|
44
|
+
autoUpdateChannel: 'latest' | 'stable';
|
|
45
|
+
loadDistTags?: PraxisDistTagLoader;
|
|
46
|
+
checkUpdatePermissions?: DoctorUpdatePermissionChecker;
|
|
47
|
+
}
|
|
48
|
+
export interface DoctorDiagnosticsResult {
|
|
49
|
+
diagnostic: DoctorInstallationDiagnostic;
|
|
50
|
+
updates: DoctorUpdateDiagnostic;
|
|
51
|
+
}
|
|
52
|
+
export declare function loadPraxisDistTags(): Promise<PraxisDistTags>;
|
|
53
|
+
export declare function collectDoctorDiagnostics(options: DoctorDiagnosticOptions): Promise<DoctorDiagnosticsResult>;
|
|
54
|
+
//# sourceMappingURL=doctor-diagnostic.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, delimiter, dirname, resolve } from 'node:path';
|
|
4
|
+
const PRAXIS_NPM_PACKAGE = 'praxis-agent';
|
|
5
|
+
const DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PRAXIS_NPM_PACKAGE}/dist-tags`;
|
|
6
|
+
const DIST_TAGS_TIMEOUT_MS = 5_000;
|
|
7
|
+
const REGISTRY_ERROR = 'Failed to fetch version information from the npm registry';
|
|
8
|
+
function parseDistTags(parsed) {
|
|
9
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
10
|
+
return {};
|
|
11
|
+
const record = parsed;
|
|
12
|
+
const tags = {};
|
|
13
|
+
for (const name of ['stable', 'latest']) {
|
|
14
|
+
const value = record[name];
|
|
15
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
16
|
+
tags[name] = value.trim();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return tags;
|
|
20
|
+
}
|
|
21
|
+
export async function loadPraxisDistTags() {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeout = setTimeout(() => controller.abort(), DIST_TAGS_TIMEOUT_MS);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(DIST_TAGS_URL, {
|
|
26
|
+
headers: { accept: 'application/json' },
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
return {};
|
|
31
|
+
return parseDistTags(await response.json());
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function readableExecutable(path) {
|
|
41
|
+
const metadata = await stat(path);
|
|
42
|
+
if (!metadata.isFile())
|
|
43
|
+
throw new Error(`Executable is not a file: ${path}`);
|
|
44
|
+
await access(path, constants.R_OK);
|
|
45
|
+
return realpath(path);
|
|
46
|
+
}
|
|
47
|
+
async function executableRealpath(path) {
|
|
48
|
+
try {
|
|
49
|
+
const metadata = await stat(path);
|
|
50
|
+
if (!metadata.isFile())
|
|
51
|
+
return null;
|
|
52
|
+
await access(path, constants.X_OK);
|
|
53
|
+
return await realpath(path);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function findOnPath(names, environment) {
|
|
60
|
+
for (const root of (environment.PATH ?? '')
|
|
61
|
+
.split(delimiter)
|
|
62
|
+
.filter(Boolean)) {
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
const candidate = resolve(root, name);
|
|
65
|
+
const canonical = await executableRealpath(candidate);
|
|
66
|
+
if (canonical !== null)
|
|
67
|
+
return canonical;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
async function praxisInstallations(installationPath, environment) {
|
|
73
|
+
const names = process.platform === 'win32' ? ['praxis.exe', 'praxis.cmd'] : ['praxis'];
|
|
74
|
+
const found = new Set();
|
|
75
|
+
for (const root of (environment.PATH ?? '')
|
|
76
|
+
.split(delimiter)
|
|
77
|
+
.filter(Boolean)) {
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
const candidate = resolve(root, name);
|
|
80
|
+
const canonical = await executableRealpath(candidate);
|
|
81
|
+
if (canonical !== null)
|
|
82
|
+
found.add(canonical);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (names.includes(basename(installationPath)))
|
|
86
|
+
found.add(installationPath);
|
|
87
|
+
return [...found].sort();
|
|
88
|
+
}
|
|
89
|
+
async function updatePermissions(directory) {
|
|
90
|
+
try {
|
|
91
|
+
await access(directory, constants.W_OK);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const code = error.code;
|
|
96
|
+
return code === 'EACCES' || code === 'EPERM' ? false : null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function buildUpdateDiagnostic(options, installation) {
|
|
100
|
+
const checkUpdatePermissions = options.checkUpdatePermissions ?? updatePermissions;
|
|
101
|
+
const hasUpdatePermissions = await checkUpdatePermissions(dirname(installation.invokedBinary));
|
|
102
|
+
let stableVersion = null;
|
|
103
|
+
let latestVersion = null;
|
|
104
|
+
let registryStatus = 'unavailable';
|
|
105
|
+
let error;
|
|
106
|
+
try {
|
|
107
|
+
const loader = options.loadDistTags ?? loadPraxisDistTags;
|
|
108
|
+
const tags = await loader();
|
|
109
|
+
const latest = tags.latest?.trim();
|
|
110
|
+
if (latest) {
|
|
111
|
+
registryStatus = 'available';
|
|
112
|
+
latestVersion = latest;
|
|
113
|
+
stableVersion = tags.stable?.trim() || null;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
error = REGISTRY_ERROR;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (cause) {
|
|
120
|
+
error = `${REGISTRY_ERROR}: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
autoUpdates: installation.installationType === 'npm'
|
|
124
|
+
? 'Manual (praxis update)'
|
|
125
|
+
: 'Managed by source checkout',
|
|
126
|
+
hasUpdatePermissions,
|
|
127
|
+
channel: options.autoUpdateChannel,
|
|
128
|
+
stableVersion,
|
|
129
|
+
latestVersion,
|
|
130
|
+
registryStatus,
|
|
131
|
+
...(error === undefined ? {} : { error }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export async function collectDoctorDiagnostics(options) {
|
|
135
|
+
const installationPath = await readableExecutable(options.executablePath);
|
|
136
|
+
const invokedBinary = resolve(options.invokedBinaryPath ?? options.executablePath);
|
|
137
|
+
const installationType = installationPath
|
|
138
|
+
.split(/[\\/]+/u)
|
|
139
|
+
.includes('node_modules')
|
|
140
|
+
? 'npm'
|
|
141
|
+
: 'source';
|
|
142
|
+
const warnings = [];
|
|
143
|
+
const rgPath = await findOnPath(process.platform === 'win32' ? ['rg.exe'] : ['rg'], options.environment);
|
|
144
|
+
if (rgPath === null) {
|
|
145
|
+
warnings.push({
|
|
146
|
+
issue: 'The ripgrep (rg) command is not installed on PATH',
|
|
147
|
+
fix: "Install ripgrep and ensure it is on PATH (for example: 'brew install ripgrep', 'apt install ripgrep', or 'npm install -g @vscode/ripgrep')",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const search = {
|
|
151
|
+
working: rgPath !== null,
|
|
152
|
+
mode: 'system',
|
|
153
|
+
systemPath: rgPath,
|
|
154
|
+
};
|
|
155
|
+
const multipleInstallations = await praxisInstallations(installationPath, options.environment);
|
|
156
|
+
const recommendation = multipleInstallations.length > 1
|
|
157
|
+
? 'Remove stale duplicate Praxis installations and keep only the executable reported by this doctor report'
|
|
158
|
+
: null;
|
|
159
|
+
const claudeConfigDir = options.environment.CLAUDE_CONFIG_DIR;
|
|
160
|
+
const diagnostic = {
|
|
161
|
+
installationType,
|
|
162
|
+
version: options.version,
|
|
163
|
+
packageManager: installationType === 'npm' ? 'npm' : null,
|
|
164
|
+
installationPath,
|
|
165
|
+
invokedBinary,
|
|
166
|
+
configInstallMethod: typeof claudeConfigDir === 'string' && claudeConfigDir.trim().length > 0
|
|
167
|
+
? 'CLAUDE_CONFIG_DIR'
|
|
168
|
+
: 'default (~/.claude)',
|
|
169
|
+
search,
|
|
170
|
+
recommendation,
|
|
171
|
+
multipleInstallations,
|
|
172
|
+
warnings,
|
|
173
|
+
};
|
|
174
|
+
const updates = await buildUpdateDiagnostic(options, {
|
|
175
|
+
installationType,
|
|
176
|
+
invokedBinary,
|
|
177
|
+
});
|
|
178
|
+
return { diagnostic, updates };
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=doctor-diagnostic.js.map
|
|
@@ -1,4 +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
3
|
export interface DoctorCheck {
|
|
3
4
|
id: 'installation' | 'node' | 'provider' | 'config-root' | 'settings' | 'plugins' | 'mcp' | 'permissions' | 'resources' | 'hooks' | 'claude-runtime';
|
|
4
5
|
status: 'pass' | 'warn' | 'fail';
|
|
@@ -9,6 +10,8 @@ export interface DoctorReport {
|
|
|
9
10
|
type: 'doctor';
|
|
10
11
|
ok: boolean;
|
|
11
12
|
praxisVersion: string;
|
|
13
|
+
diagnostic: DoctorInstallationDiagnostic;
|
|
14
|
+
updates: DoctorUpdateDiagnostic;
|
|
12
15
|
checks: readonly DoctorCheck[];
|
|
13
16
|
summary: {
|
|
14
17
|
passed: number;
|
|
@@ -27,6 +30,9 @@ export interface DoctorOptions {
|
|
|
27
30
|
environment: NodeJS.ProcessEnv;
|
|
28
31
|
detectClaudeVersion?: (execute?: VersionCommand) => Promise<string>;
|
|
29
32
|
executeVersion?: VersionCommand;
|
|
33
|
+
autoUpdateChannel: 'latest' | 'stable';
|
|
34
|
+
invokedBinaryPath?: string;
|
|
35
|
+
loadDistTags?: PraxisDistTagLoader;
|
|
30
36
|
}
|
|
31
37
|
export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
|
|
32
38
|
export declare function formatDoctorReport(report: DoctorReport): string;
|
|
@@ -15,6 +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
19
|
function isRecord(value) {
|
|
19
20
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
21
|
}
|
|
@@ -127,6 +128,20 @@ async function capture(id, operation) {
|
|
|
127
128
|
export async function runDoctor(options) {
|
|
128
129
|
const configRoot = resolve(options.configRoot);
|
|
129
130
|
const cwd = resolve(options.cwd);
|
|
131
|
+
const sensitiveValues = sensitiveEnvironmentValues(options.environment);
|
|
132
|
+
const { diagnostic, updates } = await collectDoctorDiagnostics({
|
|
133
|
+
version: options.version,
|
|
134
|
+
executablePath: options.executablePath,
|
|
135
|
+
...(options.invokedBinaryPath === undefined
|
|
136
|
+
? {}
|
|
137
|
+
: { invokedBinaryPath: options.invokedBinaryPath }),
|
|
138
|
+
configRoot,
|
|
139
|
+
environment: options.environment,
|
|
140
|
+
autoUpdateChannel: options.autoUpdateChannel,
|
|
141
|
+
...(options.loadDistTags === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: { loadDistTags: options.loadDistTags }),
|
|
144
|
+
});
|
|
130
145
|
let settings;
|
|
131
146
|
const checks = [];
|
|
132
147
|
checks.push(await capture('installation', async () => {
|
|
@@ -352,12 +367,17 @@ export async function runDoctor(options) {
|
|
|
352
367
|
validateClaudeHooks(settings);
|
|
353
368
|
return { summary: 'Hook configuration is valid' };
|
|
354
369
|
}));
|
|
355
|
-
const sensitiveValues = sensitiveEnvironmentValues(options.environment);
|
|
356
370
|
for (const check of checks) {
|
|
357
371
|
if (check.status === 'fail') {
|
|
358
372
|
check.summary = redactSensitiveText(check.summary, sensitiveValues);
|
|
359
373
|
}
|
|
360
374
|
}
|
|
375
|
+
const reportUpdates = updates.error === undefined
|
|
376
|
+
? updates
|
|
377
|
+
: {
|
|
378
|
+
...updates,
|
|
379
|
+
error: redactSensitiveText(updates.error, sensitiveValues),
|
|
380
|
+
};
|
|
361
381
|
const summary = {
|
|
362
382
|
passed: checks.filter((check) => check.status === 'pass').length,
|
|
363
383
|
warnings: checks.filter((check) => check.status === 'warn').length,
|
|
@@ -367,12 +387,42 @@ export async function runDoctor(options) {
|
|
|
367
387
|
type: 'doctor',
|
|
368
388
|
ok: checks.every((check) => check.status !== 'fail'),
|
|
369
389
|
praxisVersion: options.version,
|
|
390
|
+
diagnostic,
|
|
391
|
+
updates: reportUpdates,
|
|
370
392
|
checks,
|
|
371
393
|
summary,
|
|
372
394
|
};
|
|
373
395
|
}
|
|
374
396
|
export function formatDoctorReport(report) {
|
|
375
397
|
const lines = ['Praxis doctor', ''];
|
|
398
|
+
const { diagnostic, updates } = report;
|
|
399
|
+
lines.push('Diagnostics');
|
|
400
|
+
lines.push(` Currently running: Praxis ${diagnostic.version} (${diagnostic.installationType})`);
|
|
401
|
+
if (diagnostic.packageManager !== null) {
|
|
402
|
+
lines.push(` Package manager: ${diagnostic.packageManager}`);
|
|
403
|
+
}
|
|
404
|
+
lines.push(` Path: ${diagnostic.installationPath}`);
|
|
405
|
+
lines.push(` Invoked: ${diagnostic.invokedBinary}`);
|
|
406
|
+
lines.push(` Config install method: ${diagnostic.configInstallMethod}`);
|
|
407
|
+
lines.push(` Search: ${diagnostic.search.working
|
|
408
|
+
? `${diagnostic.search.mode} (${diagnostic.search.systemPath})`
|
|
409
|
+
: 'unavailable'}`);
|
|
410
|
+
lines.push('', 'Updates');
|
|
411
|
+
lines.push(` Auto-updates: ${updates.autoUpdates}`);
|
|
412
|
+
if (updates.hasUpdatePermissions !== null) {
|
|
413
|
+
lines.push(` Update permissions: ${updates.hasUpdatePermissions ? 'yes' : 'no'}`);
|
|
414
|
+
}
|
|
415
|
+
lines.push(` Update channel: ${updates.channel}`);
|
|
416
|
+
if (updates.registryStatus === 'available') {
|
|
417
|
+
if (updates.stableVersion !== null) {
|
|
418
|
+
lines.push(` Stable version: ${updates.stableVersion}`);
|
|
419
|
+
}
|
|
420
|
+
lines.push(` Latest version: ${updates.latestVersion ?? 'unknown'}`);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
lines.push(' └ Failed to fetch versions');
|
|
424
|
+
}
|
|
425
|
+
lines.push('');
|
|
376
426
|
for (const check of report.checks) {
|
|
377
427
|
lines.push(`[${check.status.toUpperCase()}] ${check.id}: ${check.summary}`);
|
|
378
428
|
if (check.details) {
|