praxis-agent 0.17.0 → 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.
@@ -21,7 +21,7 @@ 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
23
  import { runDoctor } from '../maintenance/doctor.js';
24
- import { canonicalClaudeCostModelName, } from './tui/cost-summary.js';
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';
27
27
  import { editTuiPrompt, openTuiEditorFile, } from './tui/external-editor.js';
@@ -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,
@@ -1718,6 +1723,23 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
1718
1723
  };
1719
1724
  });
1720
1725
  };
1726
+ const showCostSummary = (sessionId) => {
1727
+ const loading = (async () => {
1728
+ setBusy(true);
1729
+ try {
1730
+ const summary = await loadCostUsage(sessionId);
1731
+ append({ kind: 'local-result', text: formatCostSummary(summary) });
1732
+ }
1733
+ catch (error) {
1734
+ warn(error);
1735
+ }
1736
+ finally {
1737
+ setBusy(false);
1738
+ }
1739
+ })();
1740
+ onTurnChange?.(loading);
1741
+ void loading.finally(() => onTurnChange?.(null));
1742
+ };
1721
1743
  const loadConfigMenuUsage = (generation, sessionId) => {
1722
1744
  const requestId = ++configUsageRequestRef.current;
1723
1745
  const loading = (async () => {
@@ -5453,7 +5475,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
5453
5475
  void loading.finally(() => onTurnChange?.(null));
5454
5476
  }
5455
5477
  else if (prompt === '/cost') {
5456
- openSettings('usage');
5478
+ showCostSummary(sessionIdRef.current);
5457
5479
  }
5458
5480
  else if (prompt === '/doctor') {
5459
5481
  openDoctor();
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",