hammoc 1.6.0 → 1.7.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.
Files changed (226) hide show
  1. package/README.md +6 -4
  2. package/package.json +2 -2
  3. package/packages/client/dist/assets/{agentExampleHighlight-ltj9ce0U.js → agentExampleHighlight-7Olvqdpf.js} +1 -1
  4. package/packages/client/dist/assets/{commandTokenHighlight-ji_ViMb4.js → commandTokenHighlight-DtKwh4lx.js} +1 -1
  5. package/packages/client/dist/assets/{index-BT4RIi0U.js → index-Bee52CKG.js} +556 -510
  6. package/packages/client/dist/assets/index-CVy582lU.css +32 -0
  7. package/packages/client/dist/assets/{index-B-DiRGuz.js → index-DLHBUw8c.js} +1 -1
  8. package/packages/client/dist/assets/{snippetTokenHighlight-CP3v4o2g.js → snippetTokenHighlight-C8O688f4.js} +1 -1
  9. package/packages/client/dist/index.html +2 -2
  10. package/packages/client/dist/sw.js +2 -2
  11. package/packages/server/dist/_dbg_agent_rows.d.ts +2 -0
  12. package/packages/server/dist/_dbg_agent_rows.d.ts.map +1 -0
  13. package/packages/server/dist/_dbg_agent_rows.js +60 -0
  14. package/packages/server/dist/_dbg_agent_rows.js.map +1 -0
  15. package/packages/server/dist/_dbg_flicker.d.ts +2 -0
  16. package/packages/server/dist/_dbg_flicker.d.ts.map +1 -0
  17. package/packages/server/dist/_dbg_flicker.js +38 -0
  18. package/packages/server/dist/_dbg_flicker.js.map +1 -0
  19. package/packages/server/dist/_harness_cli.d.ts +2 -0
  20. package/packages/server/dist/_harness_cli.d.ts.map +1 -0
  21. package/packages/server/dist/_harness_cli.js +81 -0
  22. package/packages/server/dist/_harness_cli.js.map +1 -0
  23. package/packages/server/dist/_harness_provcard.d.ts +2 -0
  24. package/packages/server/dist/_harness_provcard.d.ts.map +1 -0
  25. package/packages/server/dist/_harness_provcard.js +67 -0
  26. package/packages/server/dist/_harness_provcard.js.map +1 -0
  27. package/packages/server/dist/_replay_dump.d.ts +2 -0
  28. package/packages/server/dist/_replay_dump.d.ts.map +1 -0
  29. package/packages/server/dist/_replay_dump.js +62 -0
  30. package/packages/server/dist/_replay_dump.js.map +1 -0
  31. package/packages/server/dist/app.d.ts.map +1 -1
  32. package/packages/server/dist/app.js +6 -0
  33. package/packages/server/dist/app.js.map +1 -1
  34. package/packages/server/dist/config/index.d.ts.map +1 -1
  35. package/packages/server/dist/config/index.js.map +1 -1
  36. package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
  37. package/packages/server/dist/controllers/projectController.js +7 -1
  38. package/packages/server/dist/controllers/projectController.js.map +1 -1
  39. package/packages/server/dist/controllers/sessionController.d.ts.map +1 -1
  40. package/packages/server/dist/controllers/sessionController.js +16 -4
  41. package/packages/server/dist/controllers/sessionController.js.map +1 -1
  42. package/packages/server/dist/controllers/systemBrowseController.d.ts +33 -0
  43. package/packages/server/dist/controllers/systemBrowseController.d.ts.map +1 -0
  44. package/packages/server/dist/controllers/systemBrowseController.js +105 -0
  45. package/packages/server/dist/controllers/systemBrowseController.js.map +1 -0
  46. package/packages/server/dist/handlers/streamCallbacks.d.ts +2 -0
  47. package/packages/server/dist/handlers/streamCallbacks.d.ts.map +1 -1
  48. package/packages/server/dist/handlers/streamCallbacks.js +17 -4
  49. package/packages/server/dist/handlers/streamCallbacks.js.map +1 -1
  50. package/packages/server/dist/handlers/websocket.d.ts +43 -3
  51. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  52. package/packages/server/dist/handlers/websocket.js +362 -92
  53. package/packages/server/dist/handlers/websocket.js.map +1 -1
  54. package/packages/server/dist/index.js +2 -0
  55. package/packages/server/dist/index.js.map +1 -1
  56. package/packages/server/dist/locales/en/server.json +17 -0
  57. package/packages/server/dist/locales/es/server.json +17 -0
  58. package/packages/server/dist/locales/ja/server.json +17 -0
  59. package/packages/server/dist/locales/ko/server.json +17 -0
  60. package/packages/server/dist/locales/pt/server.json +17 -0
  61. package/packages/server/dist/locales/zh-CN/server.json +17 -0
  62. package/packages/server/dist/routes/preferences.d.ts.map +1 -1
  63. package/packages/server/dist/routes/preferences.js +12 -2
  64. package/packages/server/dist/routes/preferences.js.map +1 -1
  65. package/packages/server/dist/routes/systemBrowse.d.ts +17 -0
  66. package/packages/server/dist/routes/systemBrowse.d.ts.map +1 -0
  67. package/packages/server/dist/routes/systemBrowse.js +22 -0
  68. package/packages/server/dist/routes/systemBrowse.js.map +1 -0
  69. package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
  70. package/packages/server/dist/services/bmadStatusService.js +18 -7
  71. package/packages/server/dist/services/bmadStatusService.js.map +1 -1
  72. package/packages/server/dist/services/chatEngine.d.ts +81 -0
  73. package/packages/server/dist/services/chatEngine.d.ts.map +1 -0
  74. package/packages/server/dist/services/chatEngine.js +2 -0
  75. package/packages/server/dist/services/chatEngine.js.map +1 -0
  76. package/packages/server/dist/services/chatEngineFactory.d.ts +15 -0
  77. package/packages/server/dist/services/chatEngineFactory.d.ts.map +1 -0
  78. package/packages/server/dist/services/chatEngineFactory.js +28 -0
  79. package/packages/server/dist/services/chatEngineFactory.js.map +1 -0
  80. package/packages/server/dist/services/chatService.d.ts +34 -26
  81. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  82. package/packages/server/dist/services/chatService.js +43 -103
  83. package/packages/server/dist/services/chatService.js.map +1 -1
  84. package/packages/server/dist/services/cliChatEngine.d.ts +198 -0
  85. package/packages/server/dist/services/cliChatEngine.d.ts.map +1 -0
  86. package/packages/server/dist/services/cliChatEngine.js +2977 -0
  87. package/packages/server/dist/services/cliChatEngine.js.map +1 -0
  88. package/packages/server/dist/services/cliGridCards.d.ts +87 -0
  89. package/packages/server/dist/services/cliGridCards.d.ts.map +1 -0
  90. package/packages/server/dist/services/cliGridCards.js +298 -0
  91. package/packages/server/dist/services/cliGridCards.js.map +1 -0
  92. package/packages/server/dist/services/cliGridRegion.d.ts +44 -0
  93. package/packages/server/dist/services/cliGridRegion.d.ts.map +1 -0
  94. package/packages/server/dist/services/cliGridRegion.js +94 -0
  95. package/packages/server/dist/services/cliGridRegion.js.map +1 -0
  96. package/packages/server/dist/services/cliModalDetect.d.ts +299 -0
  97. package/packages/server/dist/services/cliModalDetect.d.ts.map +1 -0
  98. package/packages/server/dist/services/cliModalDetect.js +583 -0
  99. package/packages/server/dist/services/cliModalDetect.js.map +1 -0
  100. package/packages/server/dist/services/cliScreenCache.d.ts +33 -0
  101. package/packages/server/dist/services/cliScreenCache.d.ts.map +1 -0
  102. package/packages/server/dist/services/cliScreenCache.js +48 -0
  103. package/packages/server/dist/services/cliScreenCache.js.map +1 -0
  104. package/packages/server/dist/services/cliScreenModel.d.ts +141 -0
  105. package/packages/server/dist/services/cliScreenModel.d.ts.map +1 -0
  106. package/packages/server/dist/services/cliScreenModel.js +196 -0
  107. package/packages/server/dist/services/cliScreenModel.js.map +1 -0
  108. package/packages/server/dist/services/cliScreenStallWatchdog.d.ts +41 -0
  109. package/packages/server/dist/services/cliScreenStallWatchdog.d.ts.map +1 -0
  110. package/packages/server/dist/services/cliScreenStallWatchdog.js +45 -0
  111. package/packages/server/dist/services/cliScreenStallWatchdog.js.map +1 -0
  112. package/packages/server/dist/services/cliSessionPool.d.ts +97 -0
  113. package/packages/server/dist/services/cliSessionPool.d.ts.map +1 -0
  114. package/packages/server/dist/services/cliSessionPool.js +278 -0
  115. package/packages/server/dist/services/cliSessionPool.js.map +1 -0
  116. package/packages/server/dist/services/cliSpinnerProgress.d.ts +41 -0
  117. package/packages/server/dist/services/cliSpinnerProgress.d.ts.map +1 -0
  118. package/packages/server/dist/services/cliSpinnerProgress.js +131 -0
  119. package/packages/server/dist/services/cliSpinnerProgress.js.map +1 -0
  120. package/packages/server/dist/services/fileRewind.d.ts +46 -0
  121. package/packages/server/dist/services/fileRewind.d.ts.map +1 -0
  122. package/packages/server/dist/services/fileRewind.js +77 -0
  123. package/packages/server/dist/services/fileRewind.js.map +1 -0
  124. package/packages/server/dist/services/historyParser.d.ts +11 -0
  125. package/packages/server/dist/services/historyParser.d.ts.map +1 -1
  126. package/packages/server/dist/services/historyParser.js +10 -8
  127. package/packages/server/dist/services/historyParser.js.map +1 -1
  128. package/packages/server/dist/services/imageStorageService.d.ts +12 -0
  129. package/packages/server/dist/services/imageStorageService.d.ts.map +1 -1
  130. package/packages/server/dist/services/imageStorageService.js +22 -0
  131. package/packages/server/dist/services/imageStorageService.js.map +1 -1
  132. package/packages/server/dist/services/projectService.d.ts +13 -1
  133. package/packages/server/dist/services/projectService.d.ts.map +1 -1
  134. package/packages/server/dist/services/projectService.js +32 -0
  135. package/packages/server/dist/services/projectService.js.map +1 -1
  136. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  137. package/packages/server/dist/services/queueService.js +9 -3
  138. package/packages/server/dist/services/queueService.js.map +1 -1
  139. package/packages/server/dist/services/rateLimitProbeService.d.ts +15 -0
  140. package/packages/server/dist/services/rateLimitProbeService.d.ts.map +1 -1
  141. package/packages/server/dist/services/rateLimitProbeService.js +48 -19
  142. package/packages/server/dist/services/rateLimitProbeService.js.map +1 -1
  143. package/packages/server/dist/services/streamHandler.d.ts +8 -1
  144. package/packages/server/dist/services/streamHandler.d.ts.map +1 -1
  145. package/packages/server/dist/services/streamHandler.js +12 -5
  146. package/packages/server/dist/services/streamHandler.js.map +1 -1
  147. package/packages/server/dist/services/systemBrowseService.d.ts +91 -0
  148. package/packages/server/dist/services/systemBrowseService.d.ts.map +1 -0
  149. package/packages/server/dist/services/systemBrowseService.js +372 -0
  150. package/packages/server/dist/services/systemBrowseService.js.map +1 -0
  151. package/packages/server/dist/services/workspaceContext.d.ts +24 -0
  152. package/packages/server/dist/services/workspaceContext.d.ts.map +1 -0
  153. package/packages/server/dist/services/workspaceContext.js +109 -0
  154. package/packages/server/dist/services/workspaceContext.js.map +1 -0
  155. package/packages/server/dist/snippets/qa-review +2 -0
  156. package/packages/server/dist/utils/backgroundBlock.d.ts +29 -0
  157. package/packages/server/dist/utils/backgroundBlock.d.ts.map +1 -0
  158. package/packages/server/dist/utils/backgroundBlock.js +33 -0
  159. package/packages/server/dist/utils/backgroundBlock.js.map +1 -0
  160. package/packages/server/dist/utils/backgroundTaskTracker.d.ts +20 -0
  161. package/packages/server/dist/utils/backgroundTaskTracker.d.ts.map +1 -0
  162. package/packages/server/dist/utils/backgroundTaskTracker.js +40 -0
  163. package/packages/server/dist/utils/backgroundTaskTracker.js.map +1 -0
  164. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts +7 -0
  165. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts.map +1 -1
  166. package/packages/server/dist/utils/bundledBinaryModelSupport.js +9 -4
  167. package/packages/server/dist/utils/bundledBinaryModelSupport.js.map +1 -1
  168. package/packages/server/dist/utils/cliDebugLog.d.ts +38 -0
  169. package/packages/server/dist/utils/cliDebugLog.d.ts.map +1 -0
  170. package/packages/server/dist/utils/cliDebugLog.js +65 -0
  171. package/packages/server/dist/utils/cliDebugLog.js.map +1 -0
  172. package/packages/server/dist/utils/cliEngineUtils.d.ts +35 -0
  173. package/packages/server/dist/utils/cliEngineUtils.d.ts.map +1 -0
  174. package/packages/server/dist/utils/cliEngineUtils.js +38 -0
  175. package/packages/server/dist/utils/cliEngineUtils.js.map +1 -0
  176. package/packages/server/dist/utils/effortUtils.d.ts.map +1 -1
  177. package/packages/server/dist/utils/effortUtils.js +5 -3
  178. package/packages/server/dist/utils/effortUtils.js.map +1 -1
  179. package/packages/server/dist/utils/trailingThrottle.d.ts +21 -0
  180. package/packages/server/dist/utils/trailingThrottle.d.ts.map +1 -0
  181. package/packages/server/dist/utils/trailingThrottle.js +36 -0
  182. package/packages/server/dist/utils/trailingThrottle.js.map +1 -0
  183. package/packages/server/package.json +61 -59
  184. package/packages/server/resources/internals/image-storage.md +9 -0
  185. package/packages/server/resources/manual/02-chat.md +22 -3
  186. package/packages/server/resources/manual/03-sessions.md +1 -0
  187. package/packages/server/resources/manual/05-projects.md +12 -1
  188. package/packages/server/resources/manual/11-bmad-method-integration.md +2 -0
  189. package/packages/server/resources/manual/13-settings.md +57 -5
  190. package/packages/shared/dist/index.d.ts +3 -0
  191. package/packages/shared/dist/index.d.ts.map +1 -1
  192. package/packages/shared/dist/index.js +6 -0
  193. package/packages/shared/dist/index.js.map +1 -1
  194. package/packages/shared/dist/types/bmadStatus.d.ts +16 -0
  195. package/packages/shared/dist/types/bmadStatus.d.ts.map +1 -1
  196. package/packages/shared/dist/types/bmadStatus.js.map +1 -1
  197. package/packages/shared/dist/types/preferences.d.ts +14 -2
  198. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  199. package/packages/shared/dist/types/preferences.js +1 -0
  200. package/packages/shared/dist/types/preferences.js.map +1 -1
  201. package/packages/shared/dist/types/project.d.ts +7 -1
  202. package/packages/shared/dist/types/project.d.ts.map +1 -1
  203. package/packages/shared/dist/types/project.js.map +1 -1
  204. package/packages/shared/dist/types/sdk.d.ts +123 -11
  205. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  206. package/packages/shared/dist/types/sdk.js +64 -19
  207. package/packages/shared/dist/types/sdk.js.map +1 -1
  208. package/packages/shared/dist/types/streaming.d.ts +2 -2
  209. package/packages/shared/dist/types/streaming.d.ts.map +1 -1
  210. package/packages/shared/dist/types/streaming.js.map +1 -1
  211. package/packages/shared/dist/types/systemBrowse.d.ts +120 -0
  212. package/packages/shared/dist/types/systemBrowse.d.ts.map +1 -0
  213. package/packages/shared/dist/types/systemBrowse.js +57 -0
  214. package/packages/shared/dist/types/systemBrowse.js.map +1 -0
  215. package/packages/shared/dist/types/websocket.d.ts +38 -0
  216. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  217. package/packages/shared/dist/utils/interruptFiller.d.ts +30 -0
  218. package/packages/shared/dist/utils/interruptFiller.d.ts.map +1 -0
  219. package/packages/shared/dist/utils/interruptFiller.js +38 -0
  220. package/packages/shared/dist/utils/interruptFiller.js.map +1 -0
  221. package/packages/shared/dist/utils/toolResultContent.d.ts +22 -0
  222. package/packages/shared/dist/utils/toolResultContent.d.ts.map +1 -0
  223. package/packages/shared/dist/utils/toolResultContent.js +29 -0
  224. package/packages/shared/dist/utils/toolResultContent.js.map +1 -0
  225. package/scripts/run-integration-test.mjs +362 -362
  226. package/packages/client/dist/assets/index-DyNJ5jEW.css +0 -32
@@ -0,0 +1,278 @@
1
+ /**
2
+ * CLI Session Pool (Epic 32 — Story 32.4)
3
+ *
4
+ * Owns the PTY processes for the CLI conversation engine (`CliChatEngine`), one
5
+ * per in-flight turn. This pool is **deliberately a separate module singleton
6
+ * from `ptyService`** (the web terminal pool): the dashboard's "active terminals"
7
+ * count is derived from `ptyService`'s session map, so routing CLI-engine PTYs
8
+ * through `ptyService` would pollute that count (Sprint Change Proposal §9.2 #3,
9
+ * spike §9-3). Keeping a distinct instance guarantees the CLI engine's PTYs never
10
+ * leak into the terminal count — isolation by construction.
11
+ *
12
+ * The Windows PATH normalization and binary-discovery patterns are *borrowed*
13
+ * from `ptyService` (which stays unmodified). They are duplicated here on purpose
14
+ * for file independence and isolation — the same convention `chatService.ts` and
15
+ * `streamHandler.ts` already use for `extractContextWindow`.
16
+ */
17
+ import * as pty from 'node-pty';
18
+ import { randomUUID } from 'crypto';
19
+ import { execSync } from 'child_process';
20
+ import os from 'os';
21
+ import path from 'path';
22
+ import { existsSync, statSync, accessSync, constants as fsConstants } from 'fs';
23
+ import { createLogger } from '../utils/logger.js';
24
+ import { resolveBundledBinaryPath } from '../utils/bundledBinaryModelSupport.js';
25
+ const log = createLogger('cliSessionPool');
26
+ /**
27
+ * "I am running inside claude" environment signals. Stripped before spawning a
28
+ * fresh interactive claude so it starts a clean session rather than trying to
29
+ * attach to the host Hammoc process (spike §2).
30
+ *
31
+ * CHILD_SESSION / SESSION_ID matter when Hammoc itself is launched FROM INSIDE a claude session:
32
+ * the spawned CLI claude would inherit "I am a child session" and then NOT write its own session
33
+ * JSONL transcript — which the engine drains for chat content, so every turn would look empty
34
+ * (verified 2026-06-15: a probe that spawned claude under these inherited vars produced no
35
+ * transcript until they were stripped). Production Hammoc is not nested so it never hits this, but
36
+ * stripping them is the correct defensive posture for in-session launches.
37
+ */
38
+ const CLAUDE_ENV_SIGNALS = [
39
+ 'CLAUDECODE',
40
+ 'CLAUDE_CODE_ENTRYPOINT',
41
+ 'CLAUDE_CODE_SSE_PORT',
42
+ 'CLAUDE_CODE_CHILD_SESSION',
43
+ 'CLAUDE_CODE_SESSION_ID',
44
+ ];
45
+ class CliSessionPool {
46
+ sessions = new Map();
47
+ // On Windows the server may be started from bash/MINGW with an incomplete PATH.
48
+ // Read the real Windows system PATH once via PowerShell+registry so spawned
49
+ // claude sessions inherit a complete environment regardless of launch shell.
50
+ // (Pattern borrowed from ptyService — kept private here for isolation.)
51
+ windowsSystemPath = (() => {
52
+ if (process.platform !== 'win32')
53
+ return null;
54
+ try {
55
+ const sysRoot = process.env.SystemRoot || 'C:\\Windows';
56
+ const ps = `${sysRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
57
+ const cmd = `[System.Environment]::GetEnvironmentVariable('Path','Machine')+';'+[System.Environment]::GetEnvironmentVariable('Path','User')`;
58
+ return execSync(`"${ps}" -NoProfile -Command "${cmd}"`, { encoding: 'utf8' }).trim();
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ })();
64
+ /**
65
+ * Convert MINGW64 Unix-style PATH entries to Windows format and merge in the
66
+ * real Windows system PATH. (Borrowed verbatim in spirit from ptyService.)
67
+ */
68
+ normalizePathForWindows(env) {
69
+ if (process.platform !== 'win32')
70
+ return env;
71
+ const pathKeys = Object.keys(env).filter((k) => k.toUpperCase() === 'PATH');
72
+ const parts = pathKeys.map((k) => env[k]).filter(Boolean);
73
+ if (this.windowsSystemPath)
74
+ parts.push(this.windowsSystemPath);
75
+ if (parts.length === 0)
76
+ return env;
77
+ const merged = parts.join(';');
78
+ if (!merged)
79
+ return env;
80
+ const normalized = { ...env };
81
+ for (const k of pathKeys) {
82
+ delete normalized[k];
83
+ }
84
+ const entries = [];
85
+ for (const rawSegment of merged.split(';')) {
86
+ const segment = rawSegment.trim().replace(/^"|"$/g, '');
87
+ if (!segment)
88
+ continue;
89
+ const segParts = segment.split(':');
90
+ let i = 0;
91
+ while (i < segParts.length) {
92
+ const part = segParts[i];
93
+ if (part.length === 1 && /^[a-zA-Z]$/.test(part) && i + 1 < segParts.length && /^[/\\]/.test(segParts[i + 1])) {
94
+ entries.push(`${part}:${segParts[i + 1]}`);
95
+ i += 2;
96
+ }
97
+ else if (part) {
98
+ entries.push(part);
99
+ i++;
100
+ }
101
+ else {
102
+ i++;
103
+ }
104
+ }
105
+ }
106
+ const windowsEntries = entries.map((p) => {
107
+ const match = p.match(/^\/([a-zA-Z])\/(.*)/);
108
+ if (match) {
109
+ return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, '\\')}`;
110
+ }
111
+ return p;
112
+ });
113
+ normalized['Path'] = windowsEntries.join(';');
114
+ return normalized;
115
+ }
116
+ /**
117
+ * Build the spawn environment: process.env minus undefined values and minus
118
+ * the "inside claude" signal vars, with Windows PATH normalized.
119
+ */
120
+ buildEnv() {
121
+ const base = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
122
+ for (const signal of CLAUDE_ENV_SIGNALS) {
123
+ delete base[signal];
124
+ }
125
+ return this.normalizePathForWindows(base);
126
+ }
127
+ /**
128
+ * Best-effort sanity check for a user-supplied binary override (Story 33.3): exists,
129
+ * is a regular file, and (POSIX only) is executable by this process. Windows has no
130
+ * comparable execute bit, so `.exe`/file existence is sufficient there. Intentionally
131
+ * shallow — no signature / "is this really claude" inspection (out of scope).
132
+ */
133
+ isValidBinaryOverride(candidate) {
134
+ try {
135
+ if (!statSync(candidate).isFile())
136
+ return false;
137
+ if (process.platform !== 'win32') {
138
+ accessSync(candidate, fsConstants.X_OK);
139
+ }
140
+ return true;
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ /**
147
+ * Resolve an executable path for `claude`. PATH lookup can fail in the server's
148
+ * child process (spike §9-5: `where claude` failed; the fallback resolved it),
149
+ * so known install locations are tried first, then a PATH probe with the
150
+ * corrected env, then a bare name as a last resort (pty resolves via env PATH).
151
+ *
152
+ * Story 33.3: when a non-empty `override` is supplied it is checked first and used
153
+ * if valid. An *invalid* override never hard-fails the turn — it logs a warning and
154
+ * falls through to the normal chain (auto-detect is the better UX than blocking CLI
155
+ * mode over a bad path). Empty / undefined skips the override entirely.
156
+ */
157
+ resolveClaudeBinary(env, override) {
158
+ const trimmedOverride = override?.trim();
159
+ if (trimmedOverride) {
160
+ if (this.isValidBinaryOverride(trimmedOverride))
161
+ return trimmedOverride;
162
+ log.warn(`Configured claude binary path is invalid (missing / not a file / not executable): "${trimmedOverride}" — falling back to auto-detect.`);
163
+ }
164
+ // Prefer the bundled engine binary shipped inside @anthropic-ai/claude-agent-sdk: it is the
165
+ // SAME Claude Code CLI as a system install (interactive by default) but version-pinned by
166
+ // Hammoc's package.json, so CLI mode and SDK mode run the identical engine — both recognize
167
+ // newly-released models (e.g. Fable 5). Auth/session are unaffected: the spawn inherits the
168
+ // same env, hence the same ~/.claude. A valid user override above still takes precedence.
169
+ const bundled = resolveBundledBinaryPath();
170
+ if (bundled && existsSync(bundled))
171
+ return bundled;
172
+ const home = os.homedir();
173
+ const isWin = process.platform === 'win32';
174
+ const candidates = isWin
175
+ ? [path.join(home, '.local', 'bin', 'claude.exe'), path.join(home, '.local', 'bin', 'claude')]
176
+ : [path.join(home, '.local', 'bin', 'claude')];
177
+ for (const candidate of candidates) {
178
+ if (existsSync(candidate))
179
+ return candidate;
180
+ }
181
+ try {
182
+ const probe = isWin ? 'where claude' : 'command -v claude';
183
+ const resolved = execSync(probe, { env, encoding: 'utf8' })
184
+ .split(/\r?\n/)
185
+ .map((s) => s.trim())
186
+ .find((s) => s && existsSync(s));
187
+ if (resolved)
188
+ return resolved;
189
+ }
190
+ catch {
191
+ // Not resolvable via PATH — fall through to bare name.
192
+ }
193
+ return isWin ? 'claude.exe' : 'claude';
194
+ }
195
+ /**
196
+ * Spawn an interactive claude TUI in a PTY and register it in the pool.
197
+ * The caller drives stdin (prompt injection), wires `pty.onData`, and attaches
198
+ * a watcher disposer via `registerDisposer`.
199
+ */
200
+ spawnClaude(opts) {
201
+ const env = this.buildEnv();
202
+ const bin = this.resolveClaudeBinary(env, opts.binaryPathOverride);
203
+ const handle = randomUUID();
204
+ const ptyProcess = pty.spawn(bin, opts.args, {
205
+ name: 'xterm-256color',
206
+ cols: opts.cols ?? 120,
207
+ rows: opts.rows ?? 40,
208
+ cwd: opts.cwd,
209
+ env,
210
+ });
211
+ this.sessions.set(handle, { pty: ptyProcess, handle, createdAt: Date.now() });
212
+ log.info(`CLI PTY spawned: handle=${handle} bin=${bin} args=[${opts.args.join(' ')}] cwd=${opts.cwd ?? '(inherit)'}`);
213
+ return { handle, pty: ptyProcess };
214
+ }
215
+ /** Attach the watcher teardown so dispose() tears down PTY + watcher together. */
216
+ registerDisposer(handle, dispose) {
217
+ const entry = this.sessions.get(handle);
218
+ if (entry)
219
+ entry.dispose = dispose;
220
+ }
221
+ /** Send Ctrl+C (interrupt) to the PTY without killing it. */
222
+ interrupt(handle) {
223
+ const entry = this.sessions.get(handle);
224
+ if (!entry)
225
+ return;
226
+ try {
227
+ entry.pty.write('\x03');
228
+ }
229
+ catch {
230
+ // PTY may already be gone.
231
+ }
232
+ }
233
+ /**
234
+ * Tear down a CLI PTY: run the watcher disposer, kill the process, remove from
235
+ * the pool. Removal happens first so any onExit listener's dispose is a no-op.
236
+ * The kill is wrapped in try/catch to absorb Windows ConPTY shutdown noise
237
+ * (`AttachConsole failed`, spike §9-6 — `ptyService.killSession` pattern).
238
+ */
239
+ dispose(handle) {
240
+ const entry = this.sessions.get(handle);
241
+ if (!entry)
242
+ return;
243
+ this.sessions.delete(handle);
244
+ try {
245
+ entry.dispose?.();
246
+ }
247
+ catch {
248
+ // Watcher teardown best-effort.
249
+ }
250
+ try {
251
+ entry.pty.kill();
252
+ }
253
+ catch {
254
+ // PTY may already be dead / ConPTY shutdown noise — ignore.
255
+ }
256
+ log.debug(`CLI PTY disposed: handle=${handle}`);
257
+ }
258
+ /** Destroy every CLI PTY (server shutdown). Symmetric with ptyService.destroyAll. */
259
+ destroyAll() {
260
+ if (this.sessions.size > 0) {
261
+ log.info(`Destroying all CLI PTYs (${this.sessions.size} active)`);
262
+ }
263
+ for (const handle of [...this.sessions.keys()]) {
264
+ this.dispose(handle);
265
+ }
266
+ }
267
+ /** Number of live CLI PTYs (for tests / introspection). */
268
+ get size() {
269
+ return this.sessions.size;
270
+ }
271
+ /** Whether a handle is still live in the pool. */
272
+ has(handle) {
273
+ return this.sessions.has(handle);
274
+ }
275
+ }
276
+ /** Module singleton — isolated from the web terminal pool (`ptyService`). */
277
+ export const cliSessionPool = new CliSessionPool();
278
+ //# sourceMappingURL=cliSessionPool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliSessionPool.js","sourceRoot":"","sources":["../../src/services/cliSessionPool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * CLI spinner-progress grid reader (Epic 37 — Story 37.2)
3
+ *
4
+ * Reads the generation "↓ N tokens" counter (and the optional leading "(Ns ·" elapsed
5
+ * clock) from a *settled screen grid* produced by the headless screen model (Story
6
+ * 37.1). This is the token data source that replaces the linear ANSI-stripped buffer
7
+ * scan: claude overwrites the spinner counter **in place** (the same screen cell), so a
8
+ * settled grid carries one final value per row — the "365" + "366" → "365366" fusion
9
+ * the linear path suffered is **structurally impossible**, and the old fusion-defense
10
+ * guards (digit cap, malformed-grouping, implausible-jump) are no longer needed.
11
+ *
12
+ * Pure — input is the grid rows, output is the parsed progress or null. The CLI engine
13
+ * schedules a `flush()` and then calls this on the settled grid; the callback contract
14
+ * (`{ tokens, elapsedSeconds }`, emit-on-change) is unchanged.
15
+ *
16
+ * @see docs/stories/37.2.story.md
17
+ * [Source: docs/prd/epic-37-cli-terminal-emulation.md#story-372]
18
+ */
19
+ export interface SpinnerProgress {
20
+ tokens: number;
21
+ elapsedSeconds: number;
22
+ /**
23
+ * True only when this spinner frame is in the THINKING phase (status segment reads
24
+ * "… · [still ]thinking …" after the token counter). Additive/optional — ABSENT means
25
+ * "not a recognized thinking phase" (generic generation), so existing callers/tests that
26
+ * read just `{ tokens, elapsedSeconds }` are unaffected. The client uses it to label the
27
+ * live progress indicator "Thinking…" (Story 37.11 AC1, achievable tier). See THINKING_PHASE_RE.
28
+ */
29
+ thinking?: boolean;
30
+ }
31
+ /**
32
+ * Read the freshest spinner counter from a settled screen grid. Returns null when no
33
+ * counter row is present (e.g. a thinking-phase spinner with no "↓ N tokens") — the
34
+ * caller treats null as "no emit", preserving the false-0 guard.
35
+ *
36
+ * Since claude overwrites the same cell, a settled grid has at most one counter row; if
37
+ * more than one matches, the BOTTOM-most (freshest rendered) row wins. Rows are never
38
+ * merged or concatenated — a single row already holds the whole current value.
39
+ */
40
+ export declare function readSpinnerProgress(grid: string[]): SpinnerProgress | null;
41
+ //# sourceMappingURL=cliSpinnerProgress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliSpinnerProgress.d.ts","sourceRoot":"","sources":["../../src/services/cliSpinnerProgress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAqEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,GAAG,IAAI,CAoC1E"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * CLI spinner-progress grid reader (Epic 37 — Story 37.2)
3
+ *
4
+ * Reads the generation "↓ N tokens" counter (and the optional leading "(Ns ·" elapsed
5
+ * clock) from a *settled screen grid* produced by the headless screen model (Story
6
+ * 37.1). This is the token data source that replaces the linear ANSI-stripped buffer
7
+ * scan: claude overwrites the spinner counter **in place** (the same screen cell), so a
8
+ * settled grid carries one final value per row — the "365" + "366" → "365366" fusion
9
+ * the linear path suffered is **structurally impossible**, and the old fusion-defense
10
+ * guards (digit cap, malformed-grouping, implausible-jump) are no longer needed.
11
+ *
12
+ * Pure — input is the grid rows, output is the parsed progress or null. The CLI engine
13
+ * schedules a `flush()` and then calls this on the settled grid; the callback contract
14
+ * (`{ tokens, elapsedSeconds }`, emit-on-change) is unchanged.
15
+ *
16
+ * @see docs/stories/37.2.story.md
17
+ * [Source: docs/prd/epic-37-cli-terminal-emulation.md#story-372]
18
+ */
19
+ import { liveFooterRows } from './cliGridRegion.js';
20
+ /**
21
+ * The spinner counter as RENDERED in the grid. Captures both the raw ("↓ 108 tokens")
22
+ * and abbreviated ("↓ 1.4k tokens") shapes claude paints — matching the Story 37.1
23
+ * realframes counter regex `↓ ?[\d.,]+k? tokens`. The old linear CLI_PROGRESS_RE used
24
+ * `[\d,]+`, which breaks at `.`/`k` and so MISSED the abbreviated form entirely; reading
25
+ * the grid lets us capture it. The rotating glyph word (Flowing…/Crunched/Brewed/
26
+ * Moseying…) is deliberately NOT part of the match — it is version-fragile, so the row
27
+ * is identified by the counter alone.
28
+ *
29
+ * Both arrow directions are accepted (`[↑↓]`): claude renders the counter as "↓ N tokens"
30
+ * for some phases and "↑ N tokens" for others (실측 2026-06-14 — a long step showed
31
+ * "↑ 95.6k tokens"). The ↓-only regex MISSED the ↑ form, so a long ↑-phase emitted no
32
+ * progress at all and the UI looked frozen ("멈춤 vs 느림" 구분 불가). The arrow is required
33
+ * (not optional) so a bare "N tokens" in prose can't flash a phantom counter.
34
+ */
35
+ const GRID_COUNTER_RE = /[↑↓]\s*([\d.,]+k?)\s*tokens/i;
36
+ /**
37
+ * The leading elapsed clock, paren-anchored (Story 37.3 — "Xm Ys" summation). claude
38
+ * renders the clock as the FIRST thing after the spinner's "(" — either bare seconds
39
+ * "(9s ·" or a minute form "(1m 36s ·". Both the minute and second segments are optional
40
+ * and captured separately; an absent segment counts as 0, so bare seconds preserve their
41
+ * old value ("9s" → 9) and a missing clock yields 0. The "(" anchor is load-bearing: the
42
+ * time is always rendered right after the opening paren, so anchoring keeps a counter or
43
+ * prose number ("↓ 365 tokens", "thinking") from being mistaken for the clock.
44
+ */
45
+ const GRID_ELAPSED_RE = /\((?:(\d+)m\s*)?(?:(\d+)s\b)?/;
46
+ /**
47
+ * The THINKING-phase status segment claude renders INSIDE the spinner's paren group, AFTER the
48
+ * token counter: "↓ 143 tokens · thinking with high effort)" / "· still thinking with high effort)"
49
+ * (실측 2026-06-16, verbose:on AND verbose:off real PTY — `cli-verbose-on/off-long-thinking` fixtures).
50
+ *
51
+ * Why a *phase* flag and not a body thinking card: in verbose:true mode (Hammoc's spawn) claude paints
52
+ * NO live thinking *content* into the screen body — only this bottom spinner advances (elapsed clock +
53
+ * token counter + this phrase); the reasoning block lands WHOLE at completion. So the only live
54
+ * "Claude is thinking" signal is this spinner segment. Surfacing it lets the client LABEL the
55
+ * already-live generation-progress indicator as "Thinking…" (vs generic generation) WITHOUT inventing
56
+ * a card from the version-fragile, region-EXCLUDED footer spinner (which `scrollbackBodyRows` drops to
57
+ * avoid resume-repaint poisoning).
58
+ *
59
+ * Anchored `tokens … thinking` WITHIN the paren (`[^)]*`, no `)` crossing) so a rotating gerund word
60
+ * BEFORE the counter (a hypothetical "Thinking…" spinner glyph) can't false-positive — only the
61
+ * post-counter phase label counts. Effort-suffix agnostic ("with high effort" optional). A
62
+ * response-phase row ("↓ 1.3k tokens)") carries no such segment → not thinking.
63
+ */
64
+ const THINKING_PHASE_RE = /\btokens\b[^)]*\bthinking\b/i;
65
+ /**
66
+ * Sum the paren-anchored elapsed clock on a spinner row to integer seconds. Pure — input
67
+ * is the rendered row, output is `minutes * 60 + seconds`. A minute form "(1m 36s ·" → 96,
68
+ * bare seconds "(9s ·" → 9 (minutes default to 0), and a row with no clock segment → 0
69
+ * (Story 37.3 AC3). Hour ("Xh") forms are unobserved and out of scope; if claude ever
70
+ * renders them, the same anchored-sum pattern extends. The grid overwrites the spinner
71
+ * cell in place, so an in-place "(1m 36s" → "(1m 37s" redraw reads as the latest value
72
+ * (97), never a fused "136137" — fusion is structurally impossible here too.
73
+ */
74
+ function sumElapsedSeconds(row) {
75
+ const m = GRID_ELAPSED_RE.exec(row);
76
+ if (!m)
77
+ return 0;
78
+ const minutes = m[1] ? parseInt(m[1], 10) : 0;
79
+ const seconds = m[2] ? parseInt(m[2], 10) : 0;
80
+ return minutes * 60 + seconds;
81
+ }
82
+ /**
83
+ * Read the freshest spinner counter from a settled screen grid. Returns null when no
84
+ * counter row is present (e.g. a thinking-phase spinner with no "↓ N tokens") — the
85
+ * caller treats null as "no emit", preserving the false-0 guard.
86
+ *
87
+ * Since claude overwrites the same cell, a settled grid has at most one counter row; if
88
+ * more than one matches, the BOTTOM-most (freshest rendered) row wins. Rows are never
89
+ * merged or concatenated — a single row already holds the whole current value.
90
+ */
91
+ export function readSpinnerProgress(grid) {
92
+ // Live region only: the spinner counter renders at the bottom of the screen, so a "↓ N tokens"
93
+ // value QUOTED in resume-repaint scrollback can't flash a phantom counter during the pre-generation
94
+ // thinking phase (no live counter row yet). Bottom-most (freshest) within the region wins.
95
+ const region = liveFooterRows(grid);
96
+ for (let y = region.length - 1; y >= 0; y--) {
97
+ const counter = GRID_COUNTER_RE.exec(region[y]);
98
+ if (!counter)
99
+ continue;
100
+ const raw = counter[1];
101
+ // Normalize to an integer token count: strip thousands separators, then expand a
102
+ // "k" suffix ("1.4k" → 1400). The grid never fuses values, so this is plain parsing.
103
+ const tokens = /k$/i.test(raw)
104
+ ? Math.round(parseFloat(raw.slice(0, -1).replace(/,/g, '')) * 1000)
105
+ : parseInt(raw.replace(/,/g, ''), 10);
106
+ if (!Number.isFinite(tokens))
107
+ return null;
108
+ // Phase label rides the SAME counter row we already matched (no extra scan, same region —
109
+ // so no new resume-poisoning surface). Set only when true to keep the shape additive.
110
+ const thinking = THINKING_PHASE_RE.test(region[y]);
111
+ return { tokens, elapsedSeconds: sumElapsedSeconds(region[y]), ...(thinking ? { thinking: true } : {}) };
112
+ }
113
+ // Fallback: no token counter yet, but an elapsed clock is on screen → emit time-only (tokens 0). claude
114
+ // paints "(Ns ·" the moment generation starts, BEFORE the first "↓ N tokens" counter appears — without
115
+ // this the live timer stays frozen until tokens show up (the token-less "(12s" the user saw not parsed).
116
+ // Require a real minute/second digit (GRID_ELAPSED_RE leaves both groups undefined for a bare "("), so a
117
+ // stray paren in prose can't flash a phantom. Bottom-most (freshest) within the live region wins.
118
+ for (let y = region.length - 1; y >= 0; y--) {
119
+ // A counter ARROW on the row means a counter is mid-write ("↓ 36" before "tokens" settles, split across
120
+ // PTY chunks) — wait for it to settle rather than flashing a time-only reading. Fall back ONLY when the
121
+ // row carries no counter glyph at all (the genuine pre-counter "(12s" before any counter appears).
122
+ if (/[↑↓]/.test(region[y]))
123
+ continue;
124
+ const m = GRID_ELAPSED_RE.exec(region[y]);
125
+ if (m && (m[1] !== undefined || m[2] !== undefined)) {
126
+ return { tokens: 0, elapsedSeconds: sumElapsedSeconds(region[y]) };
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ //# sourceMappingURL=cliSpinnerProgress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliSpinnerProgress.js","sourceRoot":"","sources":["../../src/services/cliSpinnerProgress.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared standalone file-rewind mechanism (Epic 32 — Story 32.5).
3
+ *
4
+ * `rewindSessionFiles` is the SINGLE billing-neutral rewind primitive that BOTH
5
+ * conversation engines delegate to — `ChatService` (SDK mode) and `CliChatEngine`
6
+ * (CLI / subscription-pool mode). It was extracted verbatim from the mechanism
7
+ * Story 32.3 placed in `ChatService.rewindFiles` so the `--session-id`+`--resume`
8
+ * footgun guard lives in exactly one place (no comment drift, no chance a later
9
+ * edit re-adds `sessionId` to one engine but not the other).
10
+ *
11
+ * Why CLI mode may reuse an SDK `query()` here even though Epic 32 exists to keep
12
+ * conversation streaming OFF the SDK billing path: **rewind is a file-checkpoint
13
+ * operation with no model call — 0 tokens.** A throwaway `query({ prompt: '' })`
14
+ * is resumed only to drive its file-rewind control request; its async iterator is
15
+ * never consumed, so no turn is ever sent to the model. Verified against real
16
+ * claude (Story 32.5 AC4): rewinding a CLI-created session moved session/weekly
17
+ * utilization by 0 and returned in ~2s (file op, not a model round-trip). Billing
18
+ * is by *token usage*, so a 0-token rewind is neutral regardless of engine.
19
+ *
20
+ * This is therefore the documented exception to the 32.4 rule "the CLI engine does
21
+ * not spawn the SDK directly": it is allowed precisely because it costs nothing.
22
+ * It is also the ONLY programmatic rewind path — interactive claude exposes no
23
+ * non-interactive rewind flag (`claude --help`: only `--resume`/`--continue`/
24
+ * `--fork-session`/`--session-id`), its `/rewind` command is a full-screen ANSI
25
+ * picker, and the SDK control-protocol rewind is headless-stream-json only.
26
+ *
27
+ * [Source: packages/server/src/services/chatService.ts (32.3 mechanism);
28
+ * docs/stories/32.5.story.md#AC1-AC3; node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts:2290-2300]
29
+ */
30
+ import { type RewindFilesResult } from '@anthropic-ai/claude-agent-sdk';
31
+ /**
32
+ * Rewind tracked files for `sessionId` to their state at `messageUuid`. `dryRun`
33
+ * previews the change (returns file stats) without writing. Returns the SDK's
34
+ * `RewindFilesResult` verbatim (`canRewind` + optional `error`/file stats) so the
35
+ * `session:rewind-files` handler maps it to the wire identically for either engine.
36
+ *
37
+ * @param cwd Project working directory — locates the session JSONL + tracked files.
38
+ * Pass `undefined` only when unknown; the dirty-marker side effect is then
39
+ * skipped (the SDK still resolves a cwd, but cannot be flagged for cleanup).
40
+ */
41
+ export declare function rewindSessionFiles(params: {
42
+ sessionId: string;
43
+ messageUuid: string;
44
+ dryRun?: boolean;
45
+ }, cwd: string | undefined): Promise<RewindFilesResult>;
46
+ //# sourceMappingURL=fileRewind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileRewind.d.ts","sourceRoot":"","sources":["../../src/services/fileRewind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAS,KAAK,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAM/E;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EACpE,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,OAAO,CAAC,iBAAiB,CAAC,CA4B5B"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shared standalone file-rewind mechanism (Epic 32 — Story 32.5).
3
+ *
4
+ * `rewindSessionFiles` is the SINGLE billing-neutral rewind primitive that BOTH
5
+ * conversation engines delegate to — `ChatService` (SDK mode) and `CliChatEngine`
6
+ * (CLI / subscription-pool mode). It was extracted verbatim from the mechanism
7
+ * Story 32.3 placed in `ChatService.rewindFiles` so the `--session-id`+`--resume`
8
+ * footgun guard lives in exactly one place (no comment drift, no chance a later
9
+ * edit re-adds `sessionId` to one engine but not the other).
10
+ *
11
+ * Why CLI mode may reuse an SDK `query()` here even though Epic 32 exists to keep
12
+ * conversation streaming OFF the SDK billing path: **rewind is a file-checkpoint
13
+ * operation with no model call — 0 tokens.** A throwaway `query({ prompt: '' })`
14
+ * is resumed only to drive its file-rewind control request; its async iterator is
15
+ * never consumed, so no turn is ever sent to the model. Verified against real
16
+ * claude (Story 32.5 AC4): rewinding a CLI-created session moved session/weekly
17
+ * utilization by 0 and returned in ~2s (file op, not a model round-trip). Billing
18
+ * is by *token usage*, so a 0-token rewind is neutral regardless of engine.
19
+ *
20
+ * This is therefore the documented exception to the 32.4 rule "the CLI engine does
21
+ * not spawn the SDK directly": it is allowed precisely because it costs nothing.
22
+ * It is also the ONLY programmatic rewind path — interactive claude exposes no
23
+ * non-interactive rewind flag (`claude --help`: only `--resume`/`--continue`/
24
+ * `--fork-session`/`--session-id`), its `/rewind` command is a full-screen ANSI
25
+ * picker, and the SDK control-protocol rewind is headless-stream-json only.
26
+ *
27
+ * [Source: packages/server/src/services/chatService.ts (32.3 mechanism);
28
+ * docs/stories/32.5.story.md#AC1-AC3; node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts:2290-2300]
29
+ */
30
+ import { query } from '@anthropic-ai/claude-agent-sdk';
31
+ import { SessionService } from './sessionService.js';
32
+ import { createLogger } from '../utils/logger.js';
33
+ const log = createLogger('fileRewind');
34
+ /**
35
+ * Rewind tracked files for `sessionId` to their state at `messageUuid`. `dryRun`
36
+ * previews the change (returns file stats) without writing. Returns the SDK's
37
+ * `RewindFilesResult` verbatim (`canRewind` + optional `error`/file stats) so the
38
+ * `session:rewind-files` handler maps it to the wire identically for either engine.
39
+ *
40
+ * @param cwd Project working directory — locates the session JSONL + tracked files.
41
+ * Pass `undefined` only when unknown; the dirty-marker side effect is then
42
+ * skipped (the SDK still resolves a cwd, but cannot be flagged for cleanup).
43
+ */
44
+ export async function rewindSessionFiles(params, cwd) {
45
+ const { sessionId, messageUuid, dryRun } = params;
46
+ const rewindQuery = query({
47
+ prompt: '',
48
+ options: {
49
+ resume: sessionId,
50
+ cwd,
51
+ enableFileCheckpointing: true,
52
+ // Do NOT pass sessionId here — CLI rejects --session-id combined with
53
+ // --resume unless --fork-session is also set. resume: sessionId already
54
+ // identifies the session.
55
+ },
56
+ });
57
+ try {
58
+ const result = await rewindQuery.rewindFiles(messageUuid, { dryRun: !!dryRun });
59
+ log.info(`rewindFiles result: canRewind=${result.canRewind}, filesChanged=${result.filesChanged?.length ?? 0}, insertions=${result.insertions ?? 0}, deletions=${result.deletions ?? 0}`);
60
+ return result;
61
+ }
62
+ finally {
63
+ // Mark session as dirty BEFORE close — the throwaway query({ prompt: '' })
64
+ // leaves an empty user message in the JSONL. On the SDK send path that empty
65
+ // message triggers cache_control 400, so cleanRewindDirty strips it before the
66
+ // next SDK resume. (Interactive claude --resume tolerates it — verified Story
67
+ // 32.5 AC4 — so the CLI engine does not clean; any later SDK resume self-heals.)
68
+ if (cwd)
69
+ new SessionService().markRewindDirty(cwd, sessionId);
70
+ // Clean up the throwaway query object.
71
+ try {
72
+ rewindQuery.close();
73
+ }
74
+ catch { /* best-effort */ }
75
+ }
76
+ }
77
+ //# sourceMappingURL=fileRewind.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileRewind.js","sourceRoot":"","sources":["../../src/services/fileRewind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,KAAK,EAA0B,MAAM,gCAAgC,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;AAEvC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAoE,EACpE,GAAuB;IAEvB,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAClD,MAAM,WAAW,GAAG,KAAK,CAAC;QACxB,MAAM,EAAE,EAAE;QACV,OAAO,EAAE;YACP,MAAM,EAAE,SAAS;YACjB,GAAG;YACH,uBAAuB,EAAE,IAAI;YAC7B,sEAAsE;YACtE,wEAAwE;YACxE,0BAA0B;SAC3B;KACF,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAChF,GAAG,CAAC,IAAI,CAAC,iCAAiC,MAAM,CAAC,SAAS,kBAAkB,MAAM,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,gBAAgB,MAAM,CAAC,UAAU,IAAI,CAAC,eAAe,MAAM,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,CAAC;QAC1L,OAAO,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,2EAA2E;QAC3E,6EAA6E;QAC7E,+EAA+E;QAC/E,8EAA8E;QAC9E,iFAAiF;QACjF,IAAI,GAAG;YAAE,IAAI,cAAc,EAAE,CAAC,eAAe,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC9D,uCAAuC;QACvC,IAAI,CAAC;YAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC"}
@@ -37,6 +37,17 @@ export declare function parseJSONLSessionMeta(filePath: string): Promise<{
37
37
  * @returns Sorted array of messages in conversation order
38
38
  */
39
39
  export declare function sortMessagesByParentUuid(messages: RawJSONLMessage[]): RawJSONLMessage[];
40
+ /**
41
+ * Check if content is a task-notification message inserted by the SDK
42
+ * @param content The raw message content
43
+ * @returns Parsed task notification data, or null if not a task notification
44
+ */
45
+ export declare function parseTaskNotification(content: string): {
46
+ taskId: string;
47
+ status: 'completed' | 'failed' | 'stopped';
48
+ summary: string;
49
+ toolUseId?: string;
50
+ } | null;
40
51
  /**
41
52
  * Clean up command tags from user messages.
42
53
  * Converts "<command-message>X</command-message>\n<command-name>/Y</command-name>\n<command-args>Z</command-args>"
@@ -1 +1 @@
1
- {"version":3,"file":"historyParser.d.ts","sourceRoot":"","sources":["../../src/services/historyParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EAQf,MAAM,gBAAgB,CAAC;AASxB;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAwBjF;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAuE7E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAqCvF;AA+HD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAuBxD;AAkDD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,eAAe,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CA2T7H"}
1
+ {"version":3,"file":"historyParser.d.ts","sourceRoot":"","sources":["../../src/services/historyParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EAQf,MAAM,gBAAgB,CAAC;AAUxB;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAwBjF;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAuE7E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAqCvF;AAqGD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAqBjK;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAuBxD;AAkDD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,eAAe,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CA0T7H"}