lsd-pi 1.1.4 → 1.1.6

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.
Files changed (175) hide show
  1. package/README.md +2 -1
  2. package/dist/headless-ui.js +2 -0
  3. package/dist/onboarding.js +11 -8
  4. package/dist/resources/extensions/async-jobs/async-bash-tool.js +14 -0
  5. package/dist/resources/extensions/async-jobs/await-tool.js +14 -0
  6. package/dist/resources/extensions/async-jobs/cancel-job-tool.js +7 -0
  7. package/dist/resources/extensions/cache-timer/index.js +5 -0
  8. package/dist/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  9. package/dist/resources/extensions/codex-rotate/README.md +9 -3
  10. package/dist/resources/extensions/codex-rotate/commands.js +15 -8
  11. package/dist/resources/extensions/codex-rotate/index.js +17 -8
  12. package/dist/resources/extensions/memory/auto-extract.js +196 -80
  13. package/dist/resources/extensions/memory/dream.js +86 -19
  14. package/dist/resources/extensions/shared/rtk.js +89 -87
  15. package/dist/resources/extensions/subagent/index.js +33 -7
  16. package/dist/startup-model-validation.js +12 -2
  17. package/dist/update-check.js +2 -2
  18. package/dist/update-cmd.js +3 -3
  19. package/dist/welcome-screen.js +43 -14
  20. package/package.json +3 -2
  21. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts +2 -0
  22. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts.map +1 -0
  23. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js +46 -0
  24. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js.map +1 -0
  25. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +8 -0
  26. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/core/agent-session.js +43 -4
  28. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +3 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  33. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  35. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts +48 -0
  37. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts.map +1 -0
  38. package/packages/pi-coding-agent/dist/core/pty-executor.js +173 -0
  39. package/packages/pi-coding-agent/dist/core/pty-executor.js.map +1 -0
  40. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/sdk.js +16 -3
  42. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  44. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/settings-manager.js +18 -0
  46. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/tool-approval.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/tool-approval.js +2 -2
  49. package/packages/pi-coding-agent/dist/core/tool-approval.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +7 -0
  51. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/tools/index.js +23 -2
  53. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +50 -0
  55. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/core/tools/pty.js +289 -0
  57. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +36 -22
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +3 -5
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +23 -62
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -4
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -4
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts +39 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +182 -0
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +6 -0
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +36 -0
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -4
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -2
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +106 -77
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +2 -5
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +4 -13
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +11 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +49 -13
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +2 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +27 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +251 -39
  107. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +2 -2
  109. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts +10 -0
  111. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/utils/terminal-screen.js +67 -0
  113. package/packages/pi-coding-agent/dist/utils/terminal-screen.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts +7 -0
  115. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js +67 -0
  117. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js.map +1 -0
  118. package/packages/pi-coding-agent/package.json +9 -4
  119. package/packages/pi-coding-agent/src/core/agent-session.clear-queue.test.ts +50 -0
  120. package/packages/pi-coding-agent/src/core/agent-session.ts +50 -4
  121. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  122. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  123. package/packages/pi-coding-agent/src/core/pty-executor.ts +229 -0
  124. package/packages/pi-coding-agent/src/core/sdk.ts +16 -3
  125. package/packages/pi-coding-agent/src/core/settings-manager.ts +27 -0
  126. package/packages/pi-coding-agent/src/core/tool-approval.ts +2 -2
  127. package/packages/pi-coding-agent/src/core/tools/index.ts +35 -2
  128. package/packages/pi-coding-agent/src/core/tools/pty.ts +354 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +37 -24
  130. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +22 -70
  131. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -3
  132. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -3
  133. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +224 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +45 -0
  135. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -3
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +104 -81
  137. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +5 -19
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +55 -13
  139. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  140. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +2 -0
  141. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +296 -48
  143. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +2 -2
  144. package/packages/pi-coding-agent/src/utils/terminal-screen.ts +77 -0
  145. package/packages/pi-coding-agent/src/utils/terminal-serializer.ts +72 -0
  146. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts +2 -0
  147. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts.map +1 -0
  148. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js +105 -0
  149. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js.map +1 -0
  150. package/packages/pi-tui/dist/components/editor.d.ts +4 -0
  151. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  152. package/packages/pi-tui/dist/components/editor.js +57 -3
  153. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  154. package/packages/pi-tui/dist/components/loader.d.ts +26 -6
  155. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  156. package/packages/pi-tui/dist/components/loader.js +178 -18
  157. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  158. package/packages/pi-tui/src/components/editor.ts +65 -3
  159. package/packages/pi-tui/src/components/loader.ts +196 -19
  160. package/pkg/dist/modes/interactive/theme/themes.js +2 -2
  161. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/async-jobs/async-bash-tool.ts +13 -0
  164. package/src/resources/extensions/async-jobs/await-tool.ts +13 -0
  165. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +8 -0
  166. package/src/resources/extensions/cache-timer/index.ts +102 -96
  167. package/src/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  168. package/src/resources/extensions/codex-rotate/README.md +9 -3
  169. package/src/resources/extensions/codex-rotate/commands.ts +335 -329
  170. package/src/resources/extensions/codex-rotate/index.ts +85 -75
  171. package/src/resources/extensions/memory/auto-extract.ts +330 -204
  172. package/src/resources/extensions/memory/dream.ts +88 -21
  173. package/src/resources/extensions/memory/tests/auto-extract.test.ts +200 -144
  174. package/src/resources/extensions/shared/rtk.js +112 -0
  175. package/src/resources/extensions/subagent/index.ts +35 -6
@@ -17,9 +17,74 @@ import { scanMemoryFiles, formatMemoryManifest } from './memory-scan.js';
17
17
  import { normalizeSubagentModel } from '../subagent/model-resolution.js';
18
18
 
19
19
  const AUTO_EXTRACT_ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
20
+ const AUTO_EXTRACT_CACHE_TIMER_RE = /^\[phase\]\s+cache-timer(?:\s*:\s*.*)?\s*$/i;
21
+ const AUTO_EXTRACT_SESSION_ENDED_RE = /^\[agent\]\s+Session ended/;
22
+ const AUTO_EXTRACT_HEADLESS_STATUS_RE = /^\[headless\]\s+Status:\s+(\w+)\s*$/i;
20
23
 
21
24
  export function stripAnsiForAutoExtractLog(text: string): string {
22
- return text.replace(AUTO_EXTRACT_ANSI_PATTERN, '');
25
+ return text.replace(AUTO_EXTRACT_ANSI_PATTERN, '');
26
+ }
27
+
28
+ export function classifyAutoExtractLogLine(rawLine: string): {
29
+ stripped: string;
30
+ keep: boolean;
31
+ completion: 'none' | 'success' | 'failure';
32
+ completionReason: string | null;
33
+ } {
34
+ const stripped = stripAnsiForAutoExtractLog(rawLine).trim();
35
+ if (!stripped) {
36
+ return {
37
+ stripped,
38
+ keep: false,
39
+ completion: 'none',
40
+ completionReason: null,
41
+ };
42
+ }
43
+
44
+ if (AUTO_EXTRACT_CACHE_TIMER_RE.test(stripped)) {
45
+ return {
46
+ stripped,
47
+ keep: false,
48
+ completion: 'none',
49
+ completionReason: null,
50
+ };
51
+ }
52
+
53
+ if (AUTO_EXTRACT_SESSION_ENDED_RE.test(stripped)) {
54
+ return {
55
+ stripped,
56
+ keep: true,
57
+ completion: 'success',
58
+ completionReason: 'session_end_detected',
59
+ };
60
+ }
61
+
62
+ const headlessStatusMatch = stripped.match(AUTO_EXTRACT_HEADLESS_STATUS_RE);
63
+ if (headlessStatusMatch) {
64
+ const status = headlessStatusMatch[1].toLowerCase();
65
+ if (status === 'complete') {
66
+ return {
67
+ stripped,
68
+ keep: true,
69
+ completion: 'success',
70
+ completionReason: 'headless_status_complete',
71
+ };
72
+ }
73
+
74
+ return {
75
+ stripped,
76
+ keep: true,
77
+ completion: 'failure',
78
+ completionReason: `headless_status_${status}`,
79
+ };
80
+ }
81
+
82
+ return {
83
+ stripped,
84
+ keep: true,
85
+ completion: 'none',
86
+ completionReason: null,
87
+ };
23
88
  }
24
89
 
25
90
  /**
@@ -29,40 +94,40 @@ export function stripAnsiForAutoExtractLog(text: string): string {
29
94
  * Returns an empty string only when there is no user-authored content.
30
95
  */
31
96
  export function buildTranscriptSummary(entries: any[]): string {
32
- const lines: string[] = [];
33
- let sawUserMessage = false;
34
-
35
- for (const entry of entries) {
36
- if (entry.type !== 'message') continue;
37
-
38
- const role = entry.message?.role;
39
- if (role !== 'user' && role !== 'assistant') continue;
40
-
41
- const raw = entry.message.content;
42
- let text = '';
43
-
44
- if (typeof raw === 'string') {
45
- text = raw;
46
- } else if (Array.isArray(raw)) {
47
- // Multi-part messages — extract text blocks only, skip tool_use / tool_result
48
- text = raw
49
- .filter((part: any) => part.type === 'text' && typeof part.text === 'string')
50
- .map((part: any) => part.text)
51
- .join('\n');
52
- }
97
+ const lines: string[] = [];
98
+ let sawUserMessage = false;
53
99
 
54
- if (!text.trim()) continue;
100
+ for (const entry of entries) {
101
+ if (entry.type !== 'message') continue;
55
102
 
56
- if (role === 'user') sawUserMessage = true;
103
+ const role = entry.message?.role;
104
+ if (role !== 'user' && role !== 'assistant') continue;
57
105
 
58
- // Truncate individual messages to keep the transcript manageable
59
- const truncated = text.length > 2000 ? text.slice(0, 2000) + '' : text;
60
- const label = role === 'user' ? 'User' : 'Assistant';
61
- lines.push(`${label}: ${truncated}`);
62
- }
106
+ const raw = entry.message.content;
107
+ let text = '';
108
+
109
+ if (typeof raw === 'string') {
110
+ text = raw;
111
+ } else if (Array.isArray(raw)) {
112
+ // Multi-part messages — extract text blocks only, skip tool_use / tool_result
113
+ text = raw
114
+ .filter((part: any) => part.type === 'text' && typeof part.text === 'string')
115
+ .map((part: any) => part.text)
116
+ .join('\n');
117
+ }
118
+
119
+ if (!text.trim()) continue;
120
+
121
+ if (role === 'user') sawUserMessage = true;
63
122
 
64
- if (!sawUserMessage || lines.length === 0) return '';
65
- return lines.join('\n\n');
123
+ // Truncate individual messages to keep the transcript manageable
124
+ const truncated = text.length > 2000 ? text.slice(0, 2000) + '' : text;
125
+ const label = role === 'user' ? 'User' : 'Assistant';
126
+ lines.push(`${label}: ${truncated}`);
127
+ }
128
+
129
+ if (!sawUserMessage || lines.length === 0) return '';
130
+ return lines.join('\n\n');
66
131
  }
67
132
 
68
133
  /**
@@ -70,17 +135,17 @@ export function buildTranscriptSummary(entries: any[]): string {
70
135
  * on what to save (and what to skip).
71
136
  */
72
137
  export function buildExtractionPrompt(memoryDir: string, transcript: string): string {
73
- const existing = scanMemoryFiles(memoryDir);
74
- const manifest = existing.length > 0 ? formatMemoryManifest(existing) : 'None yet';
138
+ const existing = scanMemoryFiles(memoryDir);
139
+ const manifest = existing.length > 0 ? formatMemoryManifest(existing) : 'None yet';
75
140
 
76
- return `You are a memory extraction agent for a coding assistant. Read the conversation transcript and save any durable facts worth remembering.
141
+ return `You are a memory extraction agent for a coding assistant. Read the conversation transcript and save any durable facts worth remembering.
77
142
 
78
143
  Memory directory: ${memoryDir}
79
144
  This directory already exists — write files directly.
80
145
 
81
146
  Rules:
82
147
  - Save ONLY: user preferences/role, feedback/corrections, project context (deadlines, decisions), external references
83
- - Do NOT save: code patterns, architecture, file paths, git history, debugging steps, ephemeral task details
148
+ - Do NOT save: raw code snippets, low-level implementation details, file paths, git history, one-off debugging steps, ephemeral task details
84
149
  - Check existing memories below — update existing files rather than creating duplicates
85
150
  - Use frontmatter: ---\\nname: ...\\ndescription: ...\\ntype: user|feedback|project|reference\\n---
86
151
  - After writing topic files, update MEMORY.md with one-line index entries
@@ -99,113 +164,42 @@ ${transcript}`;
99
164
  * Returns null if no valid CLI binary can be found.
100
165
  */
101
166
  export function resolveCliPath(): string | null {
102
- // Prefer env vars set by loader.ts — reliable across all invocation styles
103
- const envPath = process.env.LSD_BIN_PATH || process.env.GSD_BIN_PATH;
104
- if (envPath && existsSync(envPath)) return envPath;
105
-
106
- // Fallback: the entry point used to launch the current process
107
- const argv1 = process.argv[1];
108
- if (argv1 && existsSync(argv1)) return argv1;
109
-
110
- // Last resort: walk up from argv1 to find a bin/ sibling
111
- if (argv1) {
112
- const binDir = join(dirname(argv1), '..', 'bin');
113
- for (const name of ['lsd', 'gsd']) {
114
- const candidate = join(binDir, name);
115
- if (existsSync(candidate)) return candidate;
167
+ // Prefer env vars set by loader.ts — reliable across all invocation styles
168
+ const envPath = process.env.LSD_BIN_PATH || process.env.GSD_BIN_PATH;
169
+ if (envPath && existsSync(envPath)) return envPath;
170
+
171
+ // Fallback: the entry point used to launch the current process
172
+ const argv1 = process.argv[1];
173
+ if (argv1 && existsSync(argv1)) return argv1;
174
+
175
+ // Last resort: walk up from argv1 to find a bin/ sibling
176
+ if (argv1) {
177
+ const binDir = join(dirname(argv1), '..', 'bin');
178
+ for (const name of ['lsd', 'gsd']) {
179
+ const candidate = join(binDir, name);
180
+ if (existsSync(candidate)) return candidate;
181
+ }
116
182
  }
117
- }
118
183
 
119
- return null;
184
+ return null;
120
185
  }
121
186
 
122
187
  export function readBudgetMemoryModel(): string | undefined {
123
- try {
124
- const settingsPath = join(getAgentDir(), 'settings.json');
125
- if (!existsSync(settingsPath)) return undefined;
126
- const raw = readFileSync(settingsPath, 'utf-8');
127
- const parsed = JSON.parse(raw) as { budgetSubagentModel?: unknown };
128
- return typeof parsed.budgetSubagentModel === 'string'
129
- ? normalizeSubagentModel(parsed.budgetSubagentModel)
130
- : undefined;
131
- } catch {
132
- return undefined;
133
- }
188
+ try {
189
+ const settingsPath = join(getAgentDir(), 'settings.json');
190
+ if (!existsSync(settingsPath)) return undefined;
191
+ const raw = readFileSync(settingsPath, 'utf-8');
192
+ const parsed = JSON.parse(raw) as { budgetSubagentModel?: unknown };
193
+ return typeof parsed.budgetSubagentModel === 'string'
194
+ ? normalizeSubagentModel(parsed.budgetSubagentModel)
195
+ : undefined;
196
+ } catch {
197
+ return undefined;
198
+ }
134
199
  }
135
200
 
136
- /**
137
- * Main entry point — called from the session_shutdown hook.
138
- *
139
- * Reads the conversation transcript, builds an extraction prompt,
140
- * and spawns a detached headless agent to process it.
141
- * Fire-and-forget: the parent can exit without killing the child.
142
- */
143
- export function extractMemories(ctx: any, cwd: string): void {
144
- // Guard: prevent recursive extraction
145
- if (process.env.LSD_MEMORY_EXTRACT === '1') return;
146
-
147
- // Guard: user opt-out
148
- if (process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY) return;
149
-
150
- const entries = ctx.sessionManager.getEntries();
151
-
152
- // Guard: need at least one user message to extract from
153
- const userMessageCount = entries.filter(
154
- (e: any) => e.type === 'message' && e.message?.role === 'user',
155
- ).length;
156
- if (userMessageCount < 1) return;
157
-
158
- const transcript = buildTranscriptSummary(entries);
159
- if (!transcript) return;
160
-
161
- const memoryDir = getMemoryDir(cwd);
162
- mkdirSync(memoryDir, { recursive: true });
163
-
164
- const prompt = buildExtractionPrompt(memoryDir, transcript);
165
- const auditPath = join(memoryDir, '.last-auto-extract.txt');
166
- const logPath = join(memoryDir, '.last-auto-extract.log');
167
-
168
- // Write prompt to a temp file so the spawned agent can read it
169
- const tmpPromptPath = join(tmpdir(), `lsd-memory-extract-${randomUUID()}.md`);
170
- writeFileSync(tmpPromptPath, prompt, 'utf-8');
171
-
172
- const cliPath = resolveCliPath();
173
- const budgetModel = readBudgetMemoryModel();
174
- if (!cliPath) {
175
- writeFileSync(
176
- auditPath,
177
- [
178
- `timestamp: ${new Date().toISOString()}`,
179
- 'status: skipped',
180
- 'reason: cli_path_not_found',
181
- `cwd: ${cwd}`,
182
- `userMessages: ${userMessageCount}`,
183
- `transcriptLength: ${transcript.length}`,
184
- `budgetModel: ${budgetModel ?? 'default'}`,
185
- ].join('\n') + '\n',
186
- 'utf-8',
187
- );
188
- return;
189
- }
190
-
191
- writeFileSync(
192
- auditPath,
193
- [
194
- `timestamp: ${new Date().toISOString()}`,
195
- 'status: spawning',
196
- `cwd: ${cwd}`,
197
- `userMessages: ${userMessageCount}`,
198
- `transcriptLength: ${transcript.length}`,
199
- `cliPath: ${cliPath}`,
200
- `budgetModel: ${budgetModel ?? 'default'}`,
201
- `logPath: ${logPath}`,
202
- ].join('\n') + '\n',
203
- 'utf-8',
204
- );
205
-
206
- const instruction = 'Extract memories from the transcript above. Write any worth-saving memories to the memory directory, then update MEMORY.md.';
207
-
208
- const helperScript = `
201
+ export function buildAutoExtractHelperScript(): string {
202
+ return String.raw`
209
203
  const { spawn } = require('node:child_process');
210
204
  const { appendFileSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync } = require('node:fs');
211
205
  const { join, delimiter } = require('node:path');
@@ -213,41 +207,67 @@ const { join, delimiter } = require('node:path');
213
207
  const [cliPath, cwd, tmpPromptPath, auditPath, logPath, memoryDir, instruction, model, userMessageCount, transcriptLength] = process.argv.slice(1);
214
208
  const startedAt = new Date().toISOString();
215
209
  let finalized = false;
216
- let sawSessionEnded = false;
217
- let sessionEndTimer = null;
210
+ let completionState = null;
211
+ let completionTimer = null;
218
212
  let hardTimeout = null;
219
213
  let pendingLogText = '';
220
214
  const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
215
+ const CACHE_TIMER_RE = /^\[phase\]\s+cache-timer(?:\s*:\s*.*)?\s*$/i;
216
+ const SESSION_ENDED_RE = /^\[agent\]\s+Session ended/;
217
+ const HEADLESS_STATUS_RE = /^\[headless\]\s+Status:\s+(\w+)\s*$/i;
221
218
 
222
219
  function stripAnsi(text) {
223
220
  return String(text).replace(ANSI_PATTERN, '');
224
221
  }
225
222
 
223
+ function classifyLogLine(rawLine) {
224
+ const stripped = stripAnsi(rawLine).trim();
225
+ if (!stripped) {
226
+ return { stripped, keep: false, completion: null, completionReason: null };
227
+ }
228
+ if (CACHE_TIMER_RE.test(stripped)) {
229
+ return { stripped, keep: false, completion: null, completionReason: null };
230
+ }
231
+ if (SESSION_ENDED_RE.test(stripped)) {
232
+ return { stripped, keep: true, completion: 'finished', completionReason: 'session_end_detected' };
233
+ }
234
+ const headlessStatusMatch = stripped.match(HEADLESS_STATUS_RE);
235
+ if (headlessStatusMatch) {
236
+ const status = String(headlessStatusMatch[1] || '').toLowerCase();
237
+ if (status === 'complete') {
238
+ return { stripped, keep: true, completion: 'finished', completionReason: 'headless_status_complete' };
239
+ }
240
+ return { stripped, keep: true, completion: 'failed', completionReason: 'headless_status_' + status };
241
+ }
242
+ return { stripped, keep: true, completion: null, completionReason: null };
243
+ }
244
+
245
+ function scheduleCompletion(completion, completionReason) {
246
+ if (!completion || completionState || completionTimer) return;
247
+ completionState = { completion, completionReason };
248
+ completionTimer = setTimeout(() => {
249
+ finalize(completion, completion === 'finished' ? 0 : 1, null, completionReason);
250
+ try { child.kill('SIGTERM'); } catch {}
251
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
252
+ }, 1500);
253
+ completionTimer.unref();
254
+ }
255
+
226
256
  function flushLogText(text, force = false) {
227
257
  pendingLogText += text;
228
- const parts = pendingLogText.split(/\r?\n/);
258
+ const parts = pendingLogText.split(/(?:\r?\n|\r)/);
229
259
  pendingLogText = force ? '' : (parts.pop() ?? '');
230
260
 
231
261
  const kept = [];
232
262
  for (const rawLine of parts) {
233
- const line = stripAnsi(rawLine).trim();
234
- if (!line) continue;
235
- if (/^\[phase\]\s+cache-timer\s*$/.test(line)) continue;
236
- kept.push(rawLine);
237
- if (/^\[agent\]\s+Session ended/.test(line)) {
238
- sawSessionEnded = true;
263
+ const classified = classifyLogLine(rawLine);
264
+ if (classified.keep) kept.push(rawLine);
265
+ if (classified.completion) {
266
+ scheduleCompletion(classified.completion, classified.completionReason);
239
267
  }
240
268
  }
241
269
 
242
270
  if (kept.length > 0) appendFileSync(logPath, kept.join('\n') + '\n');
243
- if (sawSessionEnded && !sessionEndTimer) {
244
- sessionEndTimer = setTimeout(() => {
245
- finalize('finished', 0, null, 'session_end_detected');
246
- try { child.kill('SIGTERM'); } catch {}
247
- setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
248
- }, 1500);
249
- sessionEndTimer.unref();
250
- }
251
271
  }
252
272
 
253
273
  function appendLog(chunk) {
@@ -269,7 +289,7 @@ function writeAudit(status, extra = []) {
269
289
  'model: ' + (model || 'default'),
270
290
  'logPath: ' + logPath,
271
291
  ...extra,
272
- ].join('\\n') + '\\n', 'utf-8');
292
+ ].join('\n') + '\n', 'utf-8');
273
293
  } catch {}
274
294
  }
275
295
 
@@ -291,7 +311,7 @@ function newestMemoryMtime(dir) {
291
311
  function finalize(status, code, signal, completionReason) {
292
312
  if (finalized) return;
293
313
  finalized = true;
294
- if (sessionEndTimer) clearTimeout(sessionEndTimer);
314
+ if (completionTimer) clearTimeout(completionTimer);
295
315
  if (hardTimeout) clearTimeout(hardTimeout);
296
316
  const afterMtime = newestMemoryMtime(memoryDir);
297
317
  const logText = existsSync(logPath) ? readFileSync(logPath, 'utf-8') : '';
@@ -318,7 +338,10 @@ const bundledPaths = Array.from(
318
338
  new Set(
319
339
  [process.env.GSD_BUNDLED_EXTENSION_PATHS, process.env.LSD_BUNDLED_EXTENSION_PATHS]
320
340
  .filter(Boolean)
321
- .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean)),
341
+ .flatMap((value) => String(value).split(delimiter).map((entry) => entry.trim()).filter(Boolean))
342
+ // Explicitly disable cache-timer extension for auto-memory workers.
343
+ // It is noisy in headless logs and provides no value for maintenance runs.
344
+ .filter((entry) => !/[\\/]cache-timer[\\/]/i.test(entry)),
322
345
  ),
323
346
  );
324
347
  for (const extensionPath of bundledPaths) childArgs.push('--extension', extensionPath);
@@ -328,11 +351,25 @@ childArgs.push('--bare', '--context', tmpPromptPath, '--context-text', instructi
328
351
  const child = spawn(
329
352
  process.execPath,
330
353
  childArgs,
331
- { cwd, env: { ...process.env, LSD_MEMORY_EXTRACT: '1' }, stdio: ['ignore', 'pipe', 'pipe'] },
354
+ {
355
+ cwd,
356
+ env: {
357
+ ...process.env,
358
+ LSD_MEMORY_EXTRACT: '1',
359
+ // Hard-disable cache timer in maintenance workers.
360
+ LSD_DISABLE_CACHE_TIMER: '1',
361
+ // Memory maintenance workers must not run in auto permission mode.
362
+ // In auto mode, write/edit calls require classifier approval, but headless
363
+ // maintenance workers have no interactive classifier responder.
364
+ // Keep writes safe via extension-level path restrictions instead.
365
+ LUCENT_CODE_PERMISSION_MODE: 'danger-full-access',
366
+ },
367
+ stdio: ['ignore', 'pipe', 'pipe'],
368
+ },
332
369
  );
333
370
 
334
371
  hardTimeout = setTimeout(() => {
335
- finalize('failed', null, 'timeout', sawSessionEnded ? 'hung_after_session_end' : 'timeout');
372
+ finalize('failed', null, 'timeout', completionState?.completionReason ?? 'timeout');
336
373
  try { child.kill('SIGTERM'); } catch {}
337
374
  setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
338
375
  }, 120000);
@@ -341,7 +378,7 @@ hardTimeout.unref();
341
378
  child.stdout.on('data', appendLog);
342
379
  child.stderr.on('data', appendLog);
343
380
  child.on('error', (err) => {
344
- appendLog(String(err && err.stack ? err.stack : err) + '\\n');
381
+ appendLog(String(err && err.stack ? err.stack : err) + '\n');
345
382
  flushLogText('', true);
346
383
  finalize('failed', null, 'spawn_error', String(err && err.message ? err.message : err));
347
384
  });
@@ -350,54 +387,143 @@ child.on('exit', (code, signal) => {
350
387
  finalize(code === 0 ? 'finished' : 'failed', code, signal, 'child_exit');
351
388
  });
352
389
  `;
390
+ }
353
391
 
354
- const proc = spawn(
355
- process.execPath,
356
- [
357
- '-e',
358
- helperScript,
359
- cliPath,
360
- cwd,
361
- tmpPromptPath,
362
- auditPath,
363
- logPath,
364
- memoryDir,
365
- instruction,
366
- budgetModel ?? '',
367
- String(userMessageCount),
368
- String(transcript.length),
369
- ],
370
- {
371
- cwd,
372
- detached: true,
373
- stdio: 'ignore',
374
- env: process.env,
375
- },
376
- );
377
- proc.unref();
378
-
379
- writeFileSync(
380
- auditPath,
381
- [
382
- `timestamp: ${new Date().toISOString()}`,
383
- 'status: spawned',
384
- `pid: ${proc.pid ?? 'unknown'}`,
385
- `cwd: ${cwd}`,
386
- `userMessages: ${userMessageCount}`,
387
- `transcriptLength: ${transcript.length}`,
388
- `cliPath: ${cliPath}`,
389
- `model: ${budgetModel ?? 'default'}`,
390
- `logPath: ${logPath}`,
391
- ].join('\n') + '\n',
392
- 'utf-8',
393
- );
394
-
395
- // Clean up the temp file after the child has had time to read it
396
- setTimeout(() => {
392
+ /**
393
+ * Main entry point — called from the session_shutdown hook.
394
+ *
395
+ * Reads the conversation transcript, builds an extraction prompt,
396
+ * and spawns a detached headless agent to process it.
397
+ * Fire-and-forget: the parent can exit without killing the child.
398
+ */
399
+ function readAutoMemoryEnabled(): boolean {
397
400
  try {
398
- unlinkSync(tmpPromptPath);
401
+ const settingsPath = join(getAgentDir(), 'settings.json');
402
+ if (!existsSync(settingsPath)) return false;
403
+ const raw = readFileSync(settingsPath, 'utf-8');
404
+ const parsed = JSON.parse(raw) as { autoMemory?: unknown };
405
+ return parsed.autoMemory === true;
399
406
  } catch {
400
- // Already cleaned up or inaccessible — safe to ignore
407
+ return false;
408
+ }
409
+ }
410
+
411
+ export function extractMemories(ctx: any, cwd: string): void {
412
+ // Guard: prevent recursive extraction
413
+ if (process.env.LSD_MEMORY_EXTRACT === '1') return;
414
+
415
+ // Guard: user opt-out via env var
416
+ if (process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY) return;
417
+
418
+ // Guard: auto memory must be enabled in settings (default: disabled)
419
+ if (!readAutoMemoryEnabled()) return;
420
+
421
+ const entries = ctx.sessionManager.getEntries();
422
+
423
+ // Guard: need at least one user message to extract from
424
+ const userMessageCount = entries.filter(
425
+ (e: any) => e.type === 'message' && e.message?.role === 'user',
426
+ ).length;
427
+ if (userMessageCount < 1) return;
428
+
429
+ const transcript = buildTranscriptSummary(entries);
430
+ if (!transcript) return;
431
+
432
+ const memoryDir = getMemoryDir(cwd);
433
+ mkdirSync(memoryDir, { recursive: true });
434
+
435
+ const prompt = buildExtractionPrompt(memoryDir, transcript);
436
+ const auditPath = join(memoryDir, '.last-auto-extract.txt');
437
+ const logPath = join(memoryDir, '.last-auto-extract.log');
438
+
439
+ // Write prompt to a temp file so the spawned agent can read it
440
+ const tmpPromptPath = join(tmpdir(), `lsd-memory-extract-${randomUUID()}.md`);
441
+ writeFileSync(tmpPromptPath, prompt, 'utf-8');
442
+
443
+ const cliPath = resolveCliPath();
444
+ const budgetModel = readBudgetMemoryModel();
445
+ if (!cliPath) {
446
+ writeFileSync(
447
+ auditPath,
448
+ [
449
+ `timestamp: ${new Date().toISOString()}`,
450
+ 'status: skipped',
451
+ 'reason: cli_path_not_found',
452
+ `cwd: ${cwd}`,
453
+ `userMessages: ${userMessageCount}`,
454
+ `transcriptLength: ${transcript.length}`,
455
+ `budgetModel: ${budgetModel ?? 'default'}`,
456
+ ].join('\n') + '\n',
457
+ 'utf-8',
458
+ );
459
+ return;
401
460
  }
402
- }, 120_000).unref();
461
+
462
+ writeFileSync(
463
+ auditPath,
464
+ [
465
+ `timestamp: ${new Date().toISOString()}`,
466
+ 'status: spawning',
467
+ `cwd: ${cwd}`,
468
+ `userMessages: ${userMessageCount}`,
469
+ `transcriptLength: ${transcript.length}`,
470
+ `cliPath: ${cliPath}`,
471
+ `budgetModel: ${budgetModel ?? 'default'}`,
472
+ `logPath: ${logPath}`,
473
+ ].join('\n') + '\n',
474
+ 'utf-8',
475
+ );
476
+
477
+ const instruction = 'Extract memories from the transcript above. Write any worth-saving memories to the memory directory, then update MEMORY.md.';
478
+ const helperScript = buildAutoExtractHelperScript();
479
+
480
+ const proc = spawn(
481
+ process.execPath,
482
+ [
483
+ '-e',
484
+ helperScript,
485
+ cliPath,
486
+ cwd,
487
+ tmpPromptPath,
488
+ auditPath,
489
+ logPath,
490
+ memoryDir,
491
+ instruction,
492
+ budgetModel ?? '',
493
+ String(userMessageCount),
494
+ String(transcript.length),
495
+ ],
496
+ {
497
+ cwd,
498
+ detached: true,
499
+ stdio: 'ignore',
500
+ env: process.env,
501
+ },
502
+ );
503
+ proc.unref();
504
+
505
+ writeFileSync(
506
+ auditPath,
507
+ [
508
+ `timestamp: ${new Date().toISOString()}`,
509
+ 'status: spawned',
510
+ `pid: ${proc.pid ?? 'unknown'}`,
511
+ `cwd: ${cwd}`,
512
+ `userMessages: ${userMessageCount}`,
513
+ `transcriptLength: ${transcript.length}`,
514
+ `cliPath: ${cliPath}`,
515
+ `model: ${budgetModel ?? 'default'}`,
516
+ `logPath: ${logPath}`,
517
+ ].join('\n') + '\n',
518
+ 'utf-8',
519
+ );
520
+
521
+ // Clean up the temp file after the child has had time to read it
522
+ setTimeout(() => {
523
+ try {
524
+ unlinkSync(tmpPromptPath);
525
+ } catch {
526
+ // Already cleaned up or inaccessible — safe to ignore
527
+ }
528
+ }, 120_000).unref();
403
529
  }