praxis-agent 0.20.8 → 0.20.10

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.
@@ -1,9 +1,10 @@
1
+ import { HOOK_EVENTS, } from '../../hooks/claude-hooks.js';
1
2
  export const TUI_HOOK_MENU = {
2
3
  title: 'Hooks',
3
4
  readOnlyNotice: 'This menu is read-only. To add or modify hooks, edit settings.json directly or ask Claude.',
4
5
  visibleRows: 5,
5
6
  };
6
- export const TUI_HOOK_EVENTS = [
7
+ const TUI_HOOK_EVENT_DEFINITIONS = [
7
8
  {
8
9
  name: 'PreToolUse',
9
10
  description: 'Before tool execution',
@@ -109,6 +110,7 @@ export const TUI_HOOK_EVENTS = [
109
110
  detail: [],
110
111
  },
111
112
  ];
113
+ export const TUI_HOOK_EVENTS = TUI_HOOK_EVENT_DEFINITIONS.filter((definition) => HOOK_EVENTS.includes(definition.name));
112
114
  function isRecord(value) {
113
115
  return typeof value === 'object' && value !== null && !Array.isArray(value);
114
116
  }
@@ -5,6 +5,7 @@ export declare function indexClaudeToolLinks(entries: readonly ClaudeTranscriptE
5
5
  toolNames: Map<string, string>;
6
6
  completedToolCalls: Set<string>;
7
7
  };
8
+ export declare function recoverClaudeToolResultLinks(entries: readonly ClaudeTranscriptEntry[]): ClaudeTranscriptEntry[];
8
9
  export declare function findUnresolvedClaudeToolCalls(entries: readonly ClaudeTranscriptEntry[]): {
9
10
  id: string;
10
11
  name: string;
@@ -33,6 +33,82 @@ export function indexClaudeToolLinks(entries) {
33
33
  }
34
34
  return { toolCalls, toolNames, completedToolCalls };
35
35
  }
36
+ export function recoverClaudeToolResultLinks(entries) {
37
+ // Index each tool_use id to the uuid of its unique assistant entry. An id
38
+ // declared by more than one assistant tool_use block, or by an assistant
39
+ // entry without a string uuid, is ambiguous and is never recoverable.
40
+ const assistantUuidById = new Map();
41
+ const assistantIdCounts = new Map();
42
+ for (const entry of entries) {
43
+ if (entry.type !== 'assistant')
44
+ continue;
45
+ for (const block of getClaudeContentBlocks(entry)) {
46
+ if (block.type !== 'tool_use' || typeof block.id !== 'string')
47
+ continue;
48
+ const count = (assistantIdCounts.get(block.id) ?? 0) + 1;
49
+ assistantIdCounts.set(block.id, count);
50
+ if (count === 1 && typeof entry.uuid === 'string') {
51
+ assistantUuidById.set(block.id, entry.uuid);
52
+ }
53
+ else {
54
+ assistantUuidById.delete(block.id);
55
+ }
56
+ }
57
+ }
58
+ // Count every tool_result reference so a recovered link never duplicates a
59
+ // result that was already completed elsewhere in the transcript.
60
+ const resultIdCounts = new Map();
61
+ for (const entry of entries) {
62
+ if (entry.type !== 'user')
63
+ continue;
64
+ for (const block of getClaudeContentBlocks(entry)) {
65
+ if (block.type !== 'tool_result' ||
66
+ typeof block.tool_use_id !== 'string') {
67
+ continue;
68
+ }
69
+ resultIdCounts.set(block.tool_use_id, (resultIdCounts.get(block.tool_use_id) ?? 0) + 1);
70
+ }
71
+ }
72
+ const recovered = [];
73
+ for (const entry of entries) {
74
+ if (entry.type === 'user' &&
75
+ typeof entry.sourceToolAssistantUUID !== 'string') {
76
+ const blocks = getClaudeContentBlocks(entry).filter((block) => block.type === 'tool_result');
77
+ const sourceUuids = new Set();
78
+ let linkable = true;
79
+ for (const block of blocks) {
80
+ // Malformed tool_result blocks (non-string tool_use_id) are never
81
+ // recovered and prevent linking the whole entry.
82
+ if (typeof block.tool_use_id !== 'string') {
83
+ linkable = false;
84
+ break;
85
+ }
86
+ const sourceUuid = assistantUuidById.get(block.tool_use_id);
87
+ if (sourceUuid === undefined ||
88
+ resultIdCounts.get(block.tool_use_id) !== 1) {
89
+ linkable = false;
90
+ break;
91
+ }
92
+ sourceUuids.add(sourceUuid);
93
+ // A single entry has one sourceToolAssistantUUID, so every recoverable
94
+ // block must resolve to the same assistant entry.
95
+ if (sourceUuids.size > 1) {
96
+ linkable = false;
97
+ break;
98
+ }
99
+ }
100
+ if (linkable && sourceUuids.size === 1) {
101
+ recovered.push({
102
+ ...entry,
103
+ sourceToolAssistantUUID: [...sourceUuids][0],
104
+ });
105
+ continue;
106
+ }
107
+ }
108
+ recovered.push(entry);
109
+ }
110
+ return recovered;
111
+ }
36
112
  export function findUnresolvedClaudeToolCalls(entries) {
37
113
  const active = selectClaudeActiveTranscript(entries);
38
114
  const { completedToolCalls } = indexClaudeToolLinks(active);
@@ -72,6 +72,7 @@ export interface ClaudeHookRunnerOptions {
72
72
  executeCommand?: ClaudeHookCommandExecutor;
73
73
  onEvent?: (event: ClaudeHookStreamEvent) => void;
74
74
  }
75
+ export declare const HOOK_EVENTS: readonly ClaudeHookEventName[];
75
76
  export declare function validateClaudeHooks(settings: readonly ClaudeJsonResource[], maxTimeoutMs?: number): void;
76
77
  export declare class ClaudeHookRunner {
77
78
  private readonly settings;
@@ -4,7 +4,7 @@ import { redactSensitiveText, sanitizeChildEnvironment, sensitiveEnvironmentValu
4
4
  const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
5
5
  const DEFAULT_MAX_OUTPUT_BYTES = 128 * 1024;
6
6
  const KILL_GRACE_MS = 250;
7
- const HOOK_EVENTS = [
7
+ export const HOOK_EVENTS = [
8
8
  'Setup',
9
9
  'SessionStart',
10
10
  'SubagentStart',
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdir, open, readFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
4
  import { TextDecoder } from 'node:util';
5
- import { getClaudeContentBlocks, indexClaudeToolLinks, } from '../compatibility/claude/tool-links.js';
5
+ import { getClaudeContentBlocks, indexClaudeToolLinks, recoverClaudeToolResultLinks, } from '../compatibility/claude/tool-links.js';
6
6
  import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
7
7
  export class ClaudeTranscriptParseError extends Error {
8
8
  lineNumber;
@@ -200,10 +200,14 @@ export class ClaudeTranscriptStore {
200
200
  }
201
201
  async load() {
202
202
  const { entries, tail } = this.parseSource(await this.readSource(), false);
203
- return { entries, tail };
203
+ return { entries: recoverClaudeToolResultLinks(entries), tail };
204
204
  }
205
205
  async loadReadOnly() {
206
- return this.parseSource(await this.readSource(), true);
206
+ const recovery = this.parseSource(await this.readSource(), true);
207
+ return {
208
+ ...recovery,
209
+ entries: recoverClaudeToolResultLinks(recovery.entries),
210
+ };
207
211
  }
208
212
  async exportReadOnly() {
209
213
  return readFile(this.sessionFile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.20.8",
3
+ "version": "0.20.10",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",