@agents-tower/core 0.1.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 (178) hide show
  1. package/dist/adapters/claude.d.ts +2 -0
  2. package/dist/adapters/claude.js +43 -0
  3. package/dist/adapters/claude.js.map +1 -0
  4. package/dist/adapters/codex-cloud.d.ts +4 -0
  5. package/dist/adapters/codex-cloud.js +80 -0
  6. package/dist/adapters/codex-cloud.js.map +1 -0
  7. package/dist/adapters/codex-local.d.ts +17 -0
  8. package/dist/adapters/codex-local.js +180 -0
  9. package/dist/adapters/codex-local.js.map +1 -0
  10. package/dist/adapters/cursor-cloud.d.ts +2 -0
  11. package/dist/adapters/cursor-cloud.js +54 -0
  12. package/dist/adapters/cursor-cloud.js.map +1 -0
  13. package/dist/adapters/cursor-local.d.ts +2 -0
  14. package/dist/adapters/cursor-local.js +37 -0
  15. package/dist/adapters/cursor-local.js.map +1 -0
  16. package/dist/adapters/helpers.d.ts +15 -0
  17. package/dist/adapters/helpers.js +33 -0
  18. package/dist/adapters/helpers.js.map +1 -0
  19. package/dist/adapters/hermes.d.ts +2 -0
  20. package/dist/adapters/hermes.js +41 -0
  21. package/dist/adapters/hermes.js.map +1 -0
  22. package/dist/adapters/index.d.ts +2 -0
  23. package/dist/adapters/index.js +37 -0
  24. package/dist/adapters/index.js.map +1 -0
  25. package/dist/adapters/openclaw.d.ts +2 -0
  26. package/dist/adapters/openclaw.js +41 -0
  27. package/dist/adapters/openclaw.js.map +1 -0
  28. package/dist/adapters/presence.d.ts +2 -0
  29. package/dist/adapters/presence.js +36 -0
  30. package/dist/adapters/presence.js.map +1 -0
  31. package/dist/adapters/static-source.d.ts +15 -0
  32. package/dist/adapters/static-source.js +41 -0
  33. package/dist/adapters/static-source.js.map +1 -0
  34. package/dist/adapters/types.d.ts +44 -0
  35. package/dist/adapters/types.js +3 -0
  36. package/dist/adapters/types.js.map +1 -0
  37. package/dist/app-server.d.ts +94 -0
  38. package/dist/app-server.js +382 -0
  39. package/dist/app-server.js.map +1 -0
  40. package/dist/app-settings.d.ts +52 -0
  41. package/dist/app-settings.js +268 -0
  42. package/dist/app-settings.js.map +1 -0
  43. package/dist/appearance.d.ts +6 -0
  44. package/dist/appearance.js +84 -0
  45. package/dist/appearance.js.map +1 -0
  46. package/dist/aseprite.d.ts +4 -0
  47. package/dist/aseprite.js +42 -0
  48. package/dist/aseprite.js.map +1 -0
  49. package/dist/claude-agent-sdk.d.ts +44 -0
  50. package/dist/claude-agent-sdk.js +347 -0
  51. package/dist/claude-agent-sdk.js.map +1 -0
  52. package/dist/claude-home-cache.d.ts +61 -0
  53. package/dist/claude-home-cache.js +438 -0
  54. package/dist/claude-home-cache.js.map +1 -0
  55. package/dist/claude.d.ts +154 -0
  56. package/dist/claude.js +3495 -0
  57. package/dist/claude.js.map +1 -0
  58. package/dist/cloud.d.ts +2 -0
  59. package/dist/cloud.js +25 -0
  60. package/dist/cloud.js.map +1 -0
  61. package/dist/codex-command.d.ts +25 -0
  62. package/dist/codex-command.js +235 -0
  63. package/dist/codex-command.js.map +1 -0
  64. package/dist/codex-session-files.d.ts +8 -0
  65. package/dist/codex-session-files.js +509 -0
  66. package/dist/codex-session-files.js.map +1 -0
  67. package/dist/codex-thread-query.d.ts +14 -0
  68. package/dist/codex-thread-query.js +82 -0
  69. package/dist/codex-thread-query.js.map +1 -0
  70. package/dist/cursor-cloud-data.d.ts +34 -0
  71. package/dist/cursor-cloud-data.js +344 -0
  72. package/dist/cursor-cloud-data.js.map +1 -0
  73. package/dist/cursor-lib/local-discovery.d.ts +17 -0
  74. package/dist/cursor-lib/local-discovery.js +251 -0
  75. package/dist/cursor-lib/local-discovery.js.map +1 -0
  76. package/dist/cursor-lib/shared.d.ts +5 -0
  77. package/dist/cursor-lib/shared.js +88 -0
  78. package/dist/cursor-lib/shared.js.map +1 -0
  79. package/dist/cursor.d.ts +8 -0
  80. package/dist/cursor.js +1584 -0
  81. package/dist/cursor.js.map +1 -0
  82. package/dist/domain/codex-turn-semantics.d.ts +4 -0
  83. package/dist/domain/codex-turn-semantics.js +49 -0
  84. package/dist/domain/codex-turn-semantics.js.map +1 -0
  85. package/dist/domain/workload-policy.d.ts +8 -0
  86. package/dist/domain/workload-policy.js +268 -0
  87. package/dist/domain/workload-policy.js.map +1 -0
  88. package/dist/domain/workspace-activity.d.ts +10 -0
  89. package/dist/domain/workspace-activity.js +372 -0
  90. package/dist/domain/workspace-activity.js.map +1 -0
  91. package/dist/goal.d.ts +11 -0
  92. package/dist/goal.js +76 -0
  93. package/dist/goal.js.map +1 -0
  94. package/dist/hermes-hook-install.d.ts +12 -0
  95. package/dist/hermes-hook-install.js +335 -0
  96. package/dist/hermes-hook-install.js.map +1 -0
  97. package/dist/hermes.d.ts +97 -0
  98. package/dist/hermes.js +2446 -0
  99. package/dist/hermes.js.map +1 -0
  100. package/dist/index.d.ts +28 -0
  101. package/dist/index.js +47 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/live-monitor-lib/events.d.ts +43 -0
  104. package/dist/live-monitor-lib/events.js +1463 -0
  105. package/dist/live-monitor-lib/events.js.map +1 -0
  106. package/dist/live-monitor-lib/rollout-hooks.d.ts +13 -0
  107. package/dist/live-monitor-lib/rollout-hooks.js +253 -0
  108. package/dist/live-monitor-lib/rollout-hooks.js.map +1 -0
  109. package/dist/live-monitor.d.ts +102 -0
  110. package/dist/live-monitor.js +1461 -0
  111. package/dist/live-monitor.js.map +1 -0
  112. package/dist/local-thread-selection.d.ts +2 -0
  113. package/dist/local-thread-selection.js +58 -0
  114. package/dist/local-thread-selection.js.map +1 -0
  115. package/dist/openclaw.d.ts +41 -0
  116. package/dist/openclaw.js +715 -0
  117. package/dist/openclaw.js.map +1 -0
  118. package/dist/presence.d.ts +9 -0
  119. package/dist/presence.js +108 -0
  120. package/dist/presence.js.map +1 -0
  121. package/dist/project-identity.d.ts +7 -0
  122. package/dist/project-identity.js +162 -0
  123. package/dist/project-identity.js.map +1 -0
  124. package/dist/project-paths.d.ts +23 -0
  125. package/dist/project-paths.js +394 -0
  126. package/dist/project-paths.js.map +1 -0
  127. package/dist/project-storage.d.ts +7 -0
  128. package/dist/project-storage.js +79 -0
  129. package/dist/project-storage.js.map +1 -0
  130. package/dist/room-config.d.ts +21 -0
  131. package/dist/room-config.js +230 -0
  132. package/dist/room-config.js.map +1 -0
  133. package/dist/services/project-discovery.d.ts +1 -0
  134. package/dist/services/project-discovery.js +16 -0
  135. package/dist/services/project-discovery.js.map +1 -0
  136. package/dist/services/project-live-monitor.d.ts +1 -0
  137. package/dist/services/project-live-monitor.js +6 -0
  138. package/dist/services/project-live-monitor.js.map +1 -0
  139. package/dist/services/project-snapshot-coordinator.d.ts +49 -0
  140. package/dist/services/project-snapshot-coordinator.js +152 -0
  141. package/dist/services/project-snapshot-coordinator.js.map +1 -0
  142. package/dist/services/refresh-scheduler.d.ts +8 -0
  143. package/dist/services/refresh-scheduler.js +44 -0
  144. package/dist/services/refresh-scheduler.js.map +1 -0
  145. package/dist/services/snapshot-assembler.d.ts +8 -0
  146. package/dist/services/snapshot-assembler.js +167 -0
  147. package/dist/services/snapshot-assembler.js.map +1 -0
  148. package/dist/services/thread-store.d.ts +4 -0
  149. package/dist/services/thread-store.js +8 -0
  150. package/dist/services/thread-store.js.map +1 -0
  151. package/dist/snapshot-lib/activity-summary.d.ts +3 -0
  152. package/dist/snapshot-lib/activity-summary.js +220 -0
  153. package/dist/snapshot-lib/activity-summary.js.map +1 -0
  154. package/dist/snapshot-lib/dashboard-builder.d.ts +21 -0
  155. package/dist/snapshot-lib/dashboard-builder.js +146 -0
  156. package/dist/snapshot-lib/dashboard-builder.js.map +1 -0
  157. package/dist/snapshot-lib/thread-summary.d.ts +27 -0
  158. package/dist/snapshot-lib/thread-summary.js +953 -0
  159. package/dist/snapshot-lib/thread-summary.js.map +1 -0
  160. package/dist/snapshot.d.ts +2 -0
  161. package/dist/snapshot.js +21 -0
  162. package/dist/snapshot.js.map +1 -0
  163. package/dist/types.d.ts +368 -0
  164. package/dist/types.js +3 -0
  165. package/dist/types.js.map +1 -0
  166. package/dist/utils/json.d.ts +4 -0
  167. package/dist/utils/json.js +29 -0
  168. package/dist/utils/json.js.map +1 -0
  169. package/dist/utils/text.d.ts +3 -0
  170. package/dist/utils/text.js +26 -0
  171. package/dist/utils/text.js.map +1 -0
  172. package/dist/web-search.d.ts +1 -0
  173. package/dist/web-search.js +52 -0
  174. package/dist/web-search.js.map +1 -0
  175. package/dist/workload.d.ts +1 -0
  176. package/dist/workload.js +12 -0
  177. package/dist/workload.js.map +1 -0
  178. package/package.json +19 -0
package/dist/claude.js ADDED
@@ -0,0 +1,3495 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildClaudeSessionEventsForTest = buildClaudeSessionEventsForTest;
4
+ exports.normalizeClaudeBackgroundJobForTest = normalizeClaudeBackgroundJobForTest;
5
+ exports.summariseClaudeHookRecord = summariseClaudeHookRecord;
6
+ exports.loadClaudeProjectSnapshotData = loadClaudeProjectSnapshotData;
7
+ exports.summariseClaudeSession = summariseClaudeSession;
8
+ exports.discoverClaudeProjects = discoverClaudeProjects;
9
+ exports.discoverClaudeProjectsFromTeamsForTest = discoverClaudeProjectsFromTeamsForTest;
10
+ exports.discoverClaudeProjectsFromCoworkForTest = discoverClaudeProjectsFromCoworkForTest;
11
+ exports.discoverClaudeProjectsFromBackgroundJobsForTest = discoverClaudeProjectsFromBackgroundJobsForTest;
12
+ exports.buildClaudeLeadAgentsForTest = buildClaudeLeadAgentsForTest;
13
+ exports.buildClaudeSubagentAgentsForTest = buildClaudeSubagentAgentsForTest;
14
+ exports.buildClaudeTeamAgentsForTest = buildClaudeTeamAgentsForTest;
15
+ exports.buildClaudeCoworkAgentsForTest = buildClaudeCoworkAgentsForTest;
16
+ exports.buildClaudeBackgroundAgentsForTest = buildClaudeBackgroundAgentsForTest;
17
+ exports.loadClaudeAgents = loadClaudeAgents;
18
+ const promises_1 = require("node:fs/promises");
19
+ const node_path_1 = require("node:path");
20
+ const node_os_1 = require("node:os");
21
+ const appearance_1 = require("./appearance");
22
+ const claude_agent_sdk_1 = require("./claude-agent-sdk");
23
+ const goal_1 = require("./goal");
24
+ const project_paths_1 = require("./project-paths");
25
+ const DEFAULT_CLAUDE_CONFIG_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude");
26
+ const CLAUDE_TEAMS_DIR = (0, node_path_1.join)(DEFAULT_CLAUDE_CONFIG_DIR, "teams");
27
+ const CLAUDE_COWORK_LOCAL_AGENT_DIR_NAME = "local-agent-mode-sessions";
28
+ const LOG_HEAD_BYTES = 4096;
29
+ const LOG_TAIL_BYTES = 65536;
30
+ const RECENT_CLAUDE_HOOK_ACTIVE_WINDOW_MS = 2 * 60 * 1000;
31
+ const RECENT_MESSAGE_WINDOW_MS = 5 * 60 * 1000;
32
+ const RECENT_DONE_WINDOW_MS = 15 * 60 * 1000;
33
+ const RECENT_CLAUDE_TEAM_DISCOVERY_WINDOW_MS = 24 * 60 * 60 * 1000;
34
+ const ACTIVE_CLAUDE_TEAM_DISCOVERY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
35
+ const RECENT_CLAUDE_COWORK_DISCOVERY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
36
+ const RECENT_CLAUDE_BACKGROUND_DISCOVERY_WINDOW_MS = 14 * 24 * 60 * 60 * 1000;
37
+ const CLAUDE_COWORK_SCAN_FILE_LIMIT = 200;
38
+ const CLAUDE_EVENT_WINDOW_MS = 2 * 60 * 1000;
39
+ function withInferredClaudeGoal(summary, kind, objective) {
40
+ return {
41
+ ...summary,
42
+ goal: (0, goal_1.inferredGoalFromText)({
43
+ kind,
44
+ objective,
45
+ state: summary.state,
46
+ updatedAt: summary.updatedAt
47
+ })
48
+ };
49
+ }
50
+ function isTransientClaudeState(state) {
51
+ return [
52
+ "planning",
53
+ "scanning",
54
+ "thinking",
55
+ "editing",
56
+ "running",
57
+ "validating",
58
+ "delegating"
59
+ ].includes(state);
60
+ }
61
+ function ageClaudeSummary(summary, now = Date.now()) {
62
+ if (summary.needsUser !== null || summary.state === "waiting" || summary.state === "blocked") {
63
+ return summary;
64
+ }
65
+ if (!isTransientClaudeState(summary.state)) {
66
+ return summary;
67
+ }
68
+ const updatedAtMs = Date.parse(summary.updatedAt);
69
+ if (!Number.isFinite(updatedAtMs)) {
70
+ return summary;
71
+ }
72
+ const ageMs = now - updatedAtMs;
73
+ if (ageMs <= RECENT_CLAUDE_HOOK_ACTIVE_WINDOW_MS) {
74
+ return summary;
75
+ }
76
+ if (ageMs <= RECENT_DONE_WINDOW_MS) {
77
+ return {
78
+ ...summary,
79
+ state: "done",
80
+ isOngoing: false,
81
+ activityEvent: null
82
+ };
83
+ }
84
+ return {
85
+ ...summary,
86
+ state: "idle",
87
+ detail: "Idle",
88
+ isOngoing: false,
89
+ activityEvent: null,
90
+ latestMessage: null
91
+ };
92
+ }
93
+ function claudeEventKindFromActivityEvent(event) {
94
+ if (!event) {
95
+ return "other";
96
+ }
97
+ if (event.type === "collabAgentToolCall" || event.type === "collabToolCall") {
98
+ return "subagent";
99
+ }
100
+ if (event.type === "mcpToolCall" || event.type === "dynamicToolCall") {
101
+ return "tool";
102
+ }
103
+ if (event.type === "fileChange") {
104
+ return "fileChange";
105
+ }
106
+ if (event.type === "commandExecution") {
107
+ return "command";
108
+ }
109
+ if (event.type === "userMessage" || event.type === "agentMessage") {
110
+ return "message";
111
+ }
112
+ return "other";
113
+ }
114
+ function claudeCollabActivityEvent(input) {
115
+ return {
116
+ type: "collabAgentToolCall",
117
+ action: "updated",
118
+ path: input.path,
119
+ title: shorten(input.detail, 88),
120
+ isImage: false
121
+ };
122
+ }
123
+ function claudeLeadAgentId(sessionId) {
124
+ return `claude:${sessionId}`;
125
+ }
126
+ function claudeChildAgentId(sessionId, agentId) {
127
+ return `${claudeLeadAgentId(sessionId)}:agent:${agentId}`;
128
+ }
129
+ function claudeTeamFallbackAgentId(teamName, agentId) {
130
+ return `claude:team:${teamName}:agent:${agentId}`;
131
+ }
132
+ function claudeTeamMemberContextKey(leadSessionId, agentId) {
133
+ return `${leadSessionId}\u0000${agentId}`;
134
+ }
135
+ function claudeTeamAgentId(context) {
136
+ if (context.member.sessionId) {
137
+ return claudeLeadAgentId(context.member.sessionId);
138
+ }
139
+ if (context.leadSessionId) {
140
+ return claudeChildAgentId(context.leadSessionId, context.member.agentId);
141
+ }
142
+ return claudeTeamFallbackAgentId(context.team.name, context.member.agentId);
143
+ }
144
+ function claudeTeamParentAgentId(context) {
145
+ return context.leadSessionId ? claudeLeadAgentId(context.leadSessionId) : null;
146
+ }
147
+ function claudeTeamMemberPrimaryCwd(member) {
148
+ return member.worktreePath ?? member.cwd;
149
+ }
150
+ function claudeHookAgentId(record) {
151
+ return stringValue(record, "agent_id", "agentId");
152
+ }
153
+ function isClaudeChildHookRecord(record) {
154
+ return Boolean(claudeHookAgentId(record));
155
+ }
156
+ function claudeHookAgentType(record) {
157
+ return stringValue(record, "agent_type", "agentType");
158
+ }
159
+ function claudeHookChildThreadId(input) {
160
+ const agentId = claudeHookAgentId(input.record);
161
+ if (!agentId) {
162
+ return input.sessionId;
163
+ }
164
+ const context = input.teamIndex?.byLeadAndAgentId.get(claudeTeamMemberContextKey(input.sessionId, agentId));
165
+ return context?.member.sessionId ?? claudeChildAgentId(input.sessionId, agentId);
166
+ }
167
+ function claudeCoworkAgentId(sessionId) {
168
+ return `claude:cowork:${sessionId}`;
169
+ }
170
+ function uniqueCanonicalPaths(values) {
171
+ return Array.from(new Set(values
172
+ .map((value) => canonicalizeProjectPath(value))
173
+ .filter((value) => Boolean(value))));
174
+ }
175
+ function appDataClaudeDirs() {
176
+ const candidates = [
177
+ process.env.APPDATA ? (0, node_path_1.join)(process.env.APPDATA, "Claude") : null,
178
+ (0, node_path_1.join)((0, node_os_1.homedir)(), "AppData", "Roaming", "Claude"),
179
+ (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "Application Support", "Claude"),
180
+ (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "Claude")
181
+ ];
182
+ return Array.from(new Set(candidates.filter((candidate) => Boolean(candidate))));
183
+ }
184
+ function claudeCoworkLocalAgentDirs() {
185
+ return appDataClaudeDirs().map((dir) => (0, node_path_1.join)(dir, CLAUDE_COWORK_LOCAL_AGENT_DIR_NAME));
186
+ }
187
+ function claudeConfigDirs() {
188
+ return Array.from(new Set([
189
+ process.env.CLAUDE_CONFIG_DIR,
190
+ DEFAULT_CLAUDE_CONFIG_DIR
191
+ ].filter((dir) => typeof dir === "string" && dir.trim().length > 0)));
192
+ }
193
+ function claudeJobsDirs() {
194
+ return claudeConfigDirs().map((dir) => (0, node_path_1.join)(dir, "jobs"));
195
+ }
196
+ function claudeProjectsDirs() {
197
+ return claudeConfigDirs().map((dir) => (0, node_path_1.join)(dir, "projects"));
198
+ }
199
+ function mergeClaudeProjectSourceKinds(project, sourceKind) {
200
+ project.sourceKinds = Array.from(new Set([
201
+ ...(Array.isArray(project.sourceKinds) ? project.sourceKinds : []),
202
+ project.sourceKind,
203
+ sourceKind
204
+ ].filter((kind) => typeof kind === "string" && kind.trim().length > 0)));
205
+ project.sourceKind = project.sourceKinds[0];
206
+ }
207
+ function titleCaseIdentifier(value) {
208
+ return value
209
+ .replace(/[_-]+/g, " ")
210
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
211
+ .split(/\s+/)
212
+ .filter(Boolean)
213
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
214
+ .join(" ");
215
+ }
216
+ function stringArray(value) {
217
+ return Array.isArray(value)
218
+ ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
219
+ : [];
220
+ }
221
+ function parseClaudeSchemaOptions(schema) {
222
+ const enumValues = stringArray(schema.enum);
223
+ if (enumValues.length > 0) {
224
+ return enumValues.map((value) => ({
225
+ label: value,
226
+ description: value
227
+ }));
228
+ }
229
+ const oneOf = Array.isArray(schema.oneOf) ? schema.oneOf : [];
230
+ const options = oneOf
231
+ .map((entry) => {
232
+ const option = asRecord(entry);
233
+ if (!option) {
234
+ return null;
235
+ }
236
+ const constValue = typeof option.const === "string" ? option.const : null;
237
+ if (!constValue) {
238
+ return null;
239
+ }
240
+ return {
241
+ label: typeof option.title === "string" && option.title.trim().length > 0 ? option.title : constValue,
242
+ description: typeof option.description === "string" && option.description.trim().length > 0 ? option.description : constValue
243
+ };
244
+ })
245
+ .filter((option) => Boolean(option));
246
+ return options.length > 0 ? options : null;
247
+ }
248
+ function parseClaudeElicitationQuestions(record) {
249
+ const requestedSchema = asRecord(record.requested_schema);
250
+ if (!requestedSchema) {
251
+ return null;
252
+ }
253
+ const properties = asRecord(requestedSchema.properties);
254
+ if (!properties) {
255
+ return null;
256
+ }
257
+ const required = new Set(stringArray(requestedSchema.required));
258
+ const questions = Object.entries(properties)
259
+ .map(([id, rawSchema]) => {
260
+ const schema = asRecord(rawSchema);
261
+ if (!schema) {
262
+ return null;
263
+ }
264
+ const header = typeof schema.title === "string" && schema.title.trim().length > 0
265
+ ? schema.title.trim()
266
+ : titleCaseIdentifier(id);
267
+ const question = typeof schema.description === "string" && schema.description.trim().length > 0
268
+ ? schema.description.trim()
269
+ : header;
270
+ return {
271
+ header,
272
+ id,
273
+ question,
274
+ required: required.has(id),
275
+ isSecret: schema.writeOnly === true || schema.format === "password",
276
+ options: parseClaudeSchemaOptions(schema)
277
+ };
278
+ })
279
+ .filter((question) => question !== null);
280
+ return questions.length > 0 ? questions : null;
281
+ }
282
+ function claudeEventFromSummary(input) {
283
+ const createdAtMs = Date.parse(input.createdAt);
284
+ if (!Number.isFinite(createdAtMs) || Date.now() - createdAtMs > CLAUDE_EVENT_WINDOW_MS) {
285
+ return null;
286
+ }
287
+ const event = input.summary.activityEvent;
288
+ if (!event && input.summary.needsUser === null && !input.summary.latestMessage) {
289
+ return null;
290
+ }
291
+ return {
292
+ id: input.id,
293
+ source: "claude",
294
+ confidence: input.confidence,
295
+ threadId: input.threadId,
296
+ createdAt: input.createdAt,
297
+ method: event?.type === "fileChange" ? "claude/fileChange"
298
+ : event?.type === "commandExecution" ? "claude/commandExecution"
299
+ : event?.type === "collabAgentToolCall" || event?.type === "collabToolCall" ? "claude/collabAgentToolCall"
300
+ : event?.type === "mcpToolCall" || event?.type === "dynamicToolCall" ? "claude/toolCall"
301
+ : event?.type === "userMessage" ? "claude/userMessage"
302
+ : event?.type === "agentMessage" ? "claude/agentMessage"
303
+ : input.summary.needsUser?.kind === "approval" ? "claude/permissionRequest"
304
+ : input.summary.needsUser?.kind === "input" ? "claude/inputRequest"
305
+ : "claude/activity",
306
+ kind: input.summary.needsUser?.kind === "approval" ? "approval"
307
+ : input.summary.needsUser?.kind === "input" ? "input"
308
+ : claudeEventKindFromActivityEvent(event),
309
+ phase: input.summary.state === "blocked" ? "failed"
310
+ : input.summary.state === "waiting" ? "waiting"
311
+ : input.summary.state === "done" ? "completed"
312
+ : "updated",
313
+ title: input.summary.detail,
314
+ detail: input.summary.latestMessage ?? input.summary.detail,
315
+ path: event?.path ?? input.summary.paths[0] ?? null,
316
+ action: event?.action,
317
+ isImage: event?.isImage,
318
+ command: input.summary.needsUser?.command,
319
+ cwd: input.summary.paths[0] ?? null,
320
+ grantRoot: input.summary.needsUser?.grantRoot
321
+ };
322
+ }
323
+ function buildClaudeSessionEvents(input) {
324
+ const events = new Map();
325
+ for (const [index, record] of input.hookRecords.entries()) {
326
+ const summary = summariseClaudeHookRecord({
327
+ sessionId: input.sessionId,
328
+ model: null,
329
+ fallbackCwd: input.fallbackCwd,
330
+ gitBranch: null,
331
+ record,
332
+ fallbackUpdatedAt: input.fallbackUpdatedAt
333
+ });
334
+ if (!summary) {
335
+ continue;
336
+ }
337
+ const createdAt = new Date(recordTimestampMs(record, input.fallbackUpdatedAt)).toISOString();
338
+ const threadId = claudeHookChildThreadId({
339
+ sessionId: input.sessionId,
340
+ record,
341
+ teamIndex: input.teamIndex
342
+ });
343
+ const event = claudeEventFromSummary({
344
+ threadId,
345
+ createdAt,
346
+ summary,
347
+ confidence: "typed",
348
+ id: `${input.sessionId}:hook:${index}:${createdAt}:${record.hook_event_name ?? "activity"}`
349
+ });
350
+ if (event) {
351
+ events.set(event.id, event);
352
+ }
353
+ }
354
+ for (const [index, record] of input.records.entries()) {
355
+ const createdAtMs = recordTimestampMs(record, input.fallbackUpdatedAt);
356
+ if (!Number.isFinite(createdAtMs) || Date.now() - createdAtMs > CLAUDE_EVENT_WINDOW_MS) {
357
+ continue;
358
+ }
359
+ const assistantText = extractAssistantText(record);
360
+ if (assistantText) {
361
+ const createdAt = new Date(createdAtMs).toISOString();
362
+ events.set(`${input.sessionId}:assistant:${index}:${createdAt}`, {
363
+ id: `${input.sessionId}:assistant:${index}:${createdAt}`,
364
+ source: "claude",
365
+ confidence: "inferred",
366
+ threadId: input.sessionId,
367
+ createdAt,
368
+ method: "claude/agentMessage",
369
+ kind: "message",
370
+ phase: "updated",
371
+ title: shorten(assistantText, 88),
372
+ detail: shorten(assistantText, 240),
373
+ path: extractPathsFromText(assistantText)[0] ?? input.fallbackCwd,
374
+ action: "said",
375
+ isImage: false
376
+ });
377
+ }
378
+ const tool = extractAssistantTool(record);
379
+ if (!tool) {
380
+ continue;
381
+ }
382
+ const summary = claudeToolSummary({
383
+ sessionId: input.sessionId,
384
+ model: null,
385
+ fallbackCwd: input.fallbackCwd,
386
+ gitBranch: null,
387
+ updatedAt: new Date(createdAtMs).toISOString(),
388
+ toolName: tool.name,
389
+ toolInput: tool.input,
390
+ failed: false
391
+ });
392
+ if (!summary) {
393
+ continue;
394
+ }
395
+ const createdAt = new Date(createdAtMs).toISOString();
396
+ const event = claudeEventFromSummary({
397
+ threadId: input.sessionId,
398
+ createdAt,
399
+ summary,
400
+ confidence: "inferred",
401
+ id: `${input.sessionId}:tool:${index}:${createdAt}:${tool.name}`
402
+ });
403
+ if (event) {
404
+ events.set(event.id, event);
405
+ }
406
+ }
407
+ return [...events.values()].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
408
+ }
409
+ function buildClaudeSessionEventsForTest(input) {
410
+ return buildClaudeSessionEvents(input);
411
+ }
412
+ function mergeClaudeAssistantTextSummary(input) {
413
+ if (!input.latestAssistantTextRecord) {
414
+ return input.base;
415
+ }
416
+ const baseUpdatedAtMs = Date.parse(input.base.updatedAt);
417
+ const assistantUpdatedAtMs = recordTimestampMs(input.latestAssistantTextRecord, input.fallbackUpdatedAt);
418
+ if (!Number.isFinite(assistantUpdatedAtMs)) {
419
+ return input.base;
420
+ }
421
+ const assistantText = extractAssistantText(input.latestAssistantTextRecord);
422
+ if (!assistantText) {
423
+ return input.base;
424
+ }
425
+ const textPaths = extractPathsFromText(assistantText);
426
+ const ageMs = Date.now() - assistantUpdatedAtMs;
427
+ const assistantState = ageMs <= 2 * 60 * 1000 ? "thinking"
428
+ : ageMs <= RECENT_DONE_WINDOW_MS ? "done"
429
+ : "idle";
430
+ const mergedUpdatedAtMs = Number.isFinite(baseUpdatedAtMs) ? Math.max(baseUpdatedAtMs, assistantUpdatedAtMs)
431
+ : assistantUpdatedAtMs;
432
+ if (assistantUpdatedAtMs >= baseUpdatedAtMs && input.base.needsUser === null && input.base.state !== "waiting" && input.base.state !== "blocked") {
433
+ return {
434
+ ...input.base,
435
+ state: assistantState,
436
+ detail: shorten(assistantText, 88),
437
+ updatedAt: new Date(mergedUpdatedAtMs).toISOString(),
438
+ paths: textPaths.length > 0 ? textPaths : input.base.paths,
439
+ activityEvent: ageMs <= RECENT_MESSAGE_WINDOW_MS
440
+ ? {
441
+ type: "agentMessage",
442
+ action: "said",
443
+ path: textPaths[0] ?? input.fallbackCwd,
444
+ title: shorten(assistantText, 88),
445
+ isImage: false
446
+ }
447
+ : null,
448
+ latestMessage: assistantText,
449
+ isOngoing: assistantState !== "done" && assistantState !== "idle" ? input.base.isOngoing : false
450
+ };
451
+ }
452
+ if (input.base.latestMessage || input.base.activityEvent) {
453
+ return input.base;
454
+ }
455
+ return {
456
+ ...input.base,
457
+ latestMessage: assistantText,
458
+ activityEvent: ageMs <= RECENT_MESSAGE_WINDOW_MS
459
+ ? {
460
+ type: "agentMessage",
461
+ action: "said",
462
+ path: textPaths[0] ?? input.fallbackCwd,
463
+ title: shorten(assistantText, 88),
464
+ isImage: false
465
+ }
466
+ : input.base.activityEvent
467
+ };
468
+ }
469
+ function trimTrailingSlash(value) {
470
+ return value.replace(/[\\/]+$/, "");
471
+ }
472
+ function canonicalizeProjectPath(input) {
473
+ if (typeof input !== "string") {
474
+ return null;
475
+ }
476
+ const raw = input.trim();
477
+ if (!raw) {
478
+ return null;
479
+ }
480
+ const windowsDriveMatch = raw.match(/^([a-zA-Z]):[\\/](.*)$/);
481
+ if (windowsDriveMatch) {
482
+ const drive = windowsDriveMatch[1].toLowerCase();
483
+ const rest = windowsDriveMatch[2].replace(/\\/g, "/");
484
+ return trimTrailingSlash(`/mnt/${drive}/${rest}`);
485
+ }
486
+ if (raw.startsWith("/")) {
487
+ return trimTrailingSlash(raw.replace(/\\/g, "/"));
488
+ }
489
+ return trimTrailingSlash(raw.replace(/\\/g, "/"));
490
+ }
491
+ function shorten(text, maxLength) {
492
+ const normalized = text.replace(/\s+/g, " ").trim();
493
+ if (normalized.length <= maxLength) {
494
+ return normalized;
495
+ }
496
+ return `${normalized.slice(0, maxLength - 1)}…`;
497
+ }
498
+ function isMeaningfulTranscriptText(text) {
499
+ if (typeof text !== "string") {
500
+ return false;
501
+ }
502
+ const normalized = text.replace(/\s+/g, " ").trim();
503
+ if (!normalized) {
504
+ return false;
505
+ }
506
+ return !/^[.\-_~`"'!,;:|/\\()[\]{}]+$/.test(normalized);
507
+ }
508
+ function isSyntheticClaudeUserText(text) {
509
+ const normalized = text.replace(/\s+/g, " ").trim();
510
+ return (/^<local-command-stdout>[\s\S]*<\/local-command-stdout>$/i.test(normalized)
511
+ || /^<command-name>[\s\S]*<\/command-name>/i.test(normalized)
512
+ || /^<command-message>[\s\S]*<\/command-message>/i.test(normalized)
513
+ || /^<command-args>[\s\S]*<\/command-args>/i.test(normalized));
514
+ }
515
+ function parseJsonLines(text, dropFirstPartial = false) {
516
+ const lines = text.split(/\r?\n/);
517
+ const usable = dropFirstPartial ? lines.slice(1) : lines;
518
+ const records = [];
519
+ for (const line of usable) {
520
+ if (!line.trim()) {
521
+ continue;
522
+ }
523
+ try {
524
+ const parsed = JSON.parse(line);
525
+ if (typeof parsed === "object" && parsed) {
526
+ records.push(parsed);
527
+ }
528
+ }
529
+ catch {
530
+ continue;
531
+ }
532
+ }
533
+ return records;
534
+ }
535
+ async function readSegment(path, start, length) {
536
+ const handle = await (0, promises_1.open)(path, "r");
537
+ try {
538
+ const buffer = Buffer.alloc(length);
539
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
540
+ return buffer.toString("utf8", 0, bytesRead);
541
+ }
542
+ finally {
543
+ await handle.close();
544
+ }
545
+ }
546
+ async function readLogSample(path) {
547
+ const handle = await (0, promises_1.open)(path, "r");
548
+ try {
549
+ const stats = await handle.stat();
550
+ const headLength = Math.min(Number(stats.size), LOG_HEAD_BYTES);
551
+ const tailLength = Math.min(Number(stats.size), LOG_TAIL_BYTES);
552
+ const tailStart = Math.max(0, Number(stats.size) - tailLength);
553
+ const [head, tail] = await Promise.all([
554
+ readSegment(path, 0, headLength),
555
+ readSegment(path, tailStart, tailLength)
556
+ ]);
557
+ return {
558
+ mtimeMs: stats.mtimeMs,
559
+ headRecords: parseJsonLines(head),
560
+ tailRecords: parseJsonLines(tail, tailStart > 0)
561
+ };
562
+ }
563
+ finally {
564
+ await handle.close();
565
+ }
566
+ }
567
+ function asRecord(value) {
568
+ return typeof value === "object" && value ? value : null;
569
+ }
570
+ function parseTimestampMs(value) {
571
+ if (typeof value !== "string") {
572
+ return Number.NaN;
573
+ }
574
+ const parsed = Date.parse(value);
575
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
576
+ }
577
+ function messageObject(record) {
578
+ return asRecord(record.message);
579
+ }
580
+ function recordTimestampMs(record, fallback) {
581
+ const parsed = parseTimestampMs(record.timestamp);
582
+ return Number.isFinite(parsed) ? parsed : fallback;
583
+ }
584
+ function explicitRecordTimestampMs(record) {
585
+ return parseTimestampMs(record.timestamp);
586
+ }
587
+ function latestTimestampedRecord(records, predicate) {
588
+ let latest = null;
589
+ for (const [index, record] of records.entries()) {
590
+ if (!predicate(record)) {
591
+ continue;
592
+ }
593
+ const timestamp = explicitRecordTimestampMs(record);
594
+ if (!Number.isFinite(timestamp)) {
595
+ continue;
596
+ }
597
+ if (!latest || timestamp > latest.timestamp || (timestamp === latest.timestamp && index > latest.index)) {
598
+ latest = { record, timestamp, index };
599
+ }
600
+ }
601
+ return latest?.record ?? null;
602
+ }
603
+ function extractTextEntries(content) {
604
+ if (typeof content === "string") {
605
+ return [content];
606
+ }
607
+ if (!Array.isArray(content)) {
608
+ return [];
609
+ }
610
+ return content.flatMap((entry) => {
611
+ const record = asRecord(entry);
612
+ if (!record) {
613
+ return [];
614
+ }
615
+ if (record.type === "text" && typeof record.text === "string") {
616
+ return [record.text];
617
+ }
618
+ if (record.type === "tool_result" && typeof record.content === "string") {
619
+ return [record.content];
620
+ }
621
+ return [];
622
+ });
623
+ }
624
+ function extractUserText(record) {
625
+ if (record.type !== "user") {
626
+ return null;
627
+ }
628
+ const message = messageObject(record);
629
+ if (!message) {
630
+ return null;
631
+ }
632
+ if (typeof message.content === "string") {
633
+ return isMeaningfulTranscriptText(message.content) && !isSyntheticClaudeUserText(message.content) ? message.content : null;
634
+ }
635
+ const text = Array.isArray(message.content)
636
+ ? message.content
637
+ .flatMap((entry) => {
638
+ const content = asRecord(entry);
639
+ return content?.type === "text" && typeof content.text === "string" ? [content.text] : [];
640
+ })
641
+ .find((entry) => isMeaningfulTranscriptText(entry) && !isSyntheticClaudeUserText(entry))
642
+ : null;
643
+ return text ?? null;
644
+ }
645
+ function extractAssistantText(record) {
646
+ if (record.type !== "assistant") {
647
+ return null;
648
+ }
649
+ const message = messageObject(record);
650
+ if (!message) {
651
+ return null;
652
+ }
653
+ const text = extractTextEntries(message.content).find((entry) => isMeaningfulTranscriptText(entry));
654
+ return text ?? null;
655
+ }
656
+ function extractAssistantTool(record) {
657
+ if (record.type !== "assistant") {
658
+ return null;
659
+ }
660
+ const message = messageObject(record);
661
+ if (!message || !Array.isArray(message.content)) {
662
+ return null;
663
+ }
664
+ for (const entry of [...message.content].reverse()) {
665
+ const item = asRecord(entry);
666
+ if (!item || item.type !== "tool_use" || typeof item.name !== "string") {
667
+ continue;
668
+ }
669
+ return {
670
+ name: item.name,
671
+ input: asRecord(item.input) ?? {}
672
+ };
673
+ }
674
+ return null;
675
+ }
676
+ function extractPathsFromText(text) {
677
+ const matches = text.match(/(?:[A-Za-z]:[\\/][^\s`'"]+|\/[^\s`'"]+)/g) ?? [];
678
+ return Array.from(new Set(matches.map((entry) => canonicalizeProjectPath(entry) ?? entry)));
679
+ }
680
+ function extractToolPaths(input) {
681
+ const values = [];
682
+ const directKeys = ["file_path", "path", "cwd"];
683
+ for (const key of directKeys) {
684
+ const value = input[key];
685
+ if (typeof value === "string") {
686
+ values.push(value);
687
+ }
688
+ }
689
+ for (const key of ["paths", "files"]) {
690
+ const value = input[key];
691
+ if (!Array.isArray(value)) {
692
+ continue;
693
+ }
694
+ for (const entry of value) {
695
+ if (typeof entry === "string") {
696
+ values.push(entry);
697
+ continue;
698
+ }
699
+ const record = asRecord(entry);
700
+ if (!record) {
701
+ continue;
702
+ }
703
+ for (const nestedKey of ["file_path", "path"]) {
704
+ if (typeof record[nestedKey] === "string") {
705
+ values.push(record[nestedKey]);
706
+ }
707
+ }
708
+ }
709
+ }
710
+ return Array.from(new Set(values
711
+ .map((value) => canonicalizeProjectPath(value) ?? value)
712
+ .filter((value) => typeof value === "string" && value.length > 0)));
713
+ }
714
+ function looksLikeValidationCommand(command) {
715
+ return /\b(test|tests|lint|build|check|verify|pytest|cargo test|go test|npm test|pnpm test|vitest|jest)\b/i.test(command);
716
+ }
717
+ function isImagePath(path) {
718
+ return Boolean(path && /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(path));
719
+ }
720
+ function stringValue(record, ...keys) {
721
+ for (const key of keys) {
722
+ const candidate = record[key];
723
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
724
+ return candidate;
725
+ }
726
+ }
727
+ return null;
728
+ }
729
+ function firstStringValue(records, ...keys) {
730
+ for (const record of records) {
731
+ if (!record) {
732
+ continue;
733
+ }
734
+ const value = stringValue(record, ...keys);
735
+ if (value) {
736
+ return value;
737
+ }
738
+ }
739
+ return null;
740
+ }
741
+ function numberValue(record, ...keys) {
742
+ for (const key of keys) {
743
+ const candidate = record[key];
744
+ if (typeof candidate !== "number" || !Number.isFinite(candidate)) {
745
+ continue;
746
+ }
747
+ return candidate < 10_000_000_000 ? candidate * 1000 : candidate;
748
+ }
749
+ return null;
750
+ }
751
+ function timestampValueMs(record, ...keys) {
752
+ for (const key of keys) {
753
+ const candidate = record[key];
754
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
755
+ return candidate < 10_000_000_000 ? candidate * 1000 : candidate;
756
+ }
757
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
758
+ const numeric = Number(candidate);
759
+ if (Number.isFinite(numeric)) {
760
+ return numeric < 10_000_000_000 ? numeric * 1000 : numeric;
761
+ }
762
+ const parsed = Date.parse(candidate);
763
+ if (Number.isFinite(parsed)) {
764
+ return parsed;
765
+ }
766
+ }
767
+ }
768
+ return null;
769
+ }
770
+ function firstTimestampValueMs(records, ...keys) {
771
+ for (const record of records) {
772
+ if (!record) {
773
+ continue;
774
+ }
775
+ const value = timestampValueMs(record, ...keys);
776
+ if (value !== null) {
777
+ return value;
778
+ }
779
+ }
780
+ return null;
781
+ }
782
+ function booleanValue(record, key, fallback) {
783
+ return typeof record[key] === "boolean" ? record[key] : fallback;
784
+ }
785
+ function firstBooleanValue(records, ...keys) {
786
+ for (const record of records) {
787
+ if (!record) {
788
+ continue;
789
+ }
790
+ for (const key of keys) {
791
+ if (typeof record[key] === "boolean") {
792
+ return record[key];
793
+ }
794
+ }
795
+ }
796
+ return null;
797
+ }
798
+ function normalizeClaudeTeamMember(value) {
799
+ const record = asRecord(value);
800
+ if (!record) {
801
+ return null;
802
+ }
803
+ const agentId = stringValue(record, "agentId", "agent_id");
804
+ const name = stringValue(record, "name");
805
+ const cwd = canonicalizeProjectPath(stringValue(record, "cwd"));
806
+ if (!agentId || !name || !cwd) {
807
+ return null;
808
+ }
809
+ return {
810
+ agentId,
811
+ name,
812
+ agentType: stringValue(record, "agentType", "agent_type"),
813
+ model: stringValue(record, "model"),
814
+ prompt: stringValue(record, "prompt"),
815
+ color: stringValue(record, "color"),
816
+ joinedAt: numberValue(record, "joinedAt", "joined_at"),
817
+ tmuxPaneId: stringValue(record, "tmuxPaneId", "tmux_pane_id"),
818
+ cwd,
819
+ worktreePath: canonicalizeProjectPath(stringValue(record, "worktreePath", "worktree_path")),
820
+ sessionId: stringValue(record, "sessionId", "session_id"),
821
+ subscriptions: stringArray(record.subscriptions),
822
+ backendType: stringValue(record, "backendType", "backend_type"),
823
+ isActive: booleanValue(record, "isActive", true),
824
+ mode: stringValue(record, "mode")
825
+ };
826
+ }
827
+ async function findClaudeCoworkFiles(input) {
828
+ const limit = input.limit ?? CLAUDE_COWORK_SCAN_FILE_LIMIT;
829
+ const results = [];
830
+ const roots = claudeCoworkLocalAgentDirs();
831
+ for (const root of roots) {
832
+ const queue = [{ dir: root, depth: 0 }];
833
+ while (queue.length > 0 && results.length < limit) {
834
+ const current = queue.shift();
835
+ if (!current) {
836
+ break;
837
+ }
838
+ const entries = await (0, promises_1.readdir)(current.dir, { withFileTypes: true }).catch(() => []);
839
+ for (const entry of entries) {
840
+ const path = (0, node_path_1.join)(current.dir, entry.name);
841
+ if (entry.isDirectory()) {
842
+ if (entry.name === "skills-plugin") {
843
+ continue;
844
+ }
845
+ if (current.depth < 4) {
846
+ queue.push({ dir: path, depth: current.depth + 1 });
847
+ }
848
+ continue;
849
+ }
850
+ if (!entry.isFile() || !input.fileNamePattern.test(entry.name)) {
851
+ continue;
852
+ }
853
+ results.push(path);
854
+ if (results.length >= limit) {
855
+ break;
856
+ }
857
+ }
858
+ }
859
+ }
860
+ return Array.from(new Set(results));
861
+ }
862
+ function normalizeClaudeCoworkFolderPaths(values) {
863
+ if (!Array.isArray(values)) {
864
+ return [];
865
+ }
866
+ const paths = values.flatMap((entry) => {
867
+ if (typeof entry === "string") {
868
+ return [entry];
869
+ }
870
+ const record = asRecord(entry);
871
+ if (!record) {
872
+ return [];
873
+ }
874
+ return [
875
+ stringValue(record, "path", "root", "rootPath", "hostPath"),
876
+ stringValue(record, "folder", "folderPath")
877
+ ];
878
+ });
879
+ return uniqueCanonicalPaths(paths);
880
+ }
881
+ function normalizeClaudeCoworkSpaces(value, fallbackUpdatedAt) {
882
+ const record = asRecord(value);
883
+ const rawSpaces = Array.isArray(record?.spaces) ? record.spaces : [];
884
+ const spaces = [];
885
+ for (const rawSpace of rawSpaces) {
886
+ const space = asRecord(rawSpace);
887
+ if (!space) {
888
+ continue;
889
+ }
890
+ const roots = uniqueCanonicalPaths([
891
+ ...normalizeClaudeCoworkFolderPaths(space.folders),
892
+ ...normalizeClaudeCoworkFolderPaths(space.projects)
893
+ ]);
894
+ if (roots.length === 0) {
895
+ continue;
896
+ }
897
+ const name = stringValue(space, "name") ?? "Claude Home";
898
+ const updatedAt = Math.max(fallbackUpdatedAt, numberValue(space, "updatedAt", "updated_at") ?? 0, numberValue(space, "createdAt", "created_at") ?? 0);
899
+ for (const root of roots) {
900
+ spaces.push({
901
+ id: stringValue(space, "id"),
902
+ name,
903
+ root,
904
+ instructions: stringValue(space, "instructions"),
905
+ updatedAt
906
+ });
907
+ }
908
+ }
909
+ return spaces;
910
+ }
911
+ async function readClaudeCoworkSpaces(limit = 50) {
912
+ const files = await findClaudeCoworkFiles({
913
+ fileNamePattern: /^spaces\.json$/i,
914
+ limit: Math.max(limit * 4, 20)
915
+ });
916
+ const spaces = await Promise.all(files.map(async (file) => {
917
+ try {
918
+ const [content, fileStats] = await Promise.all([
919
+ (0, promises_1.readFile)(file, "utf8"),
920
+ (0, promises_1.stat)(file)
921
+ ]);
922
+ return normalizeClaudeCoworkSpaces(JSON.parse(content), fileStats.mtimeMs);
923
+ }
924
+ catch {
925
+ return [];
926
+ }
927
+ }));
928
+ const now = Date.now();
929
+ return spaces
930
+ .flat()
931
+ .filter((space) => now - space.updatedAt <= RECENT_CLAUDE_COWORK_DISCOVERY_WINDOW_MS)
932
+ .sort((left, right) => right.updatedAt - left.updatedAt)
933
+ .slice(0, limit);
934
+ }
935
+ function normalizeClaudeCoworkSession(value, fallbackUpdatedAt) {
936
+ const record = asRecord(value);
937
+ if (!record) {
938
+ return null;
939
+ }
940
+ const sessionId = stringValue(record, "sessionId", "session_id");
941
+ if (!sessionId) {
942
+ return null;
943
+ }
944
+ const selectedRoots = normalizeClaudeCoworkFolderPaths(record.userSelectedFolders);
945
+ const detectedFiles = Array.isArray(record.fsDetectedFiles)
946
+ ? record.fsDetectedFiles
947
+ : record.fsDetectedFiles ? [record.fsDetectedFiles] : [];
948
+ const filePaths = uniqueCanonicalPaths(detectedFiles.flatMap((entry) => {
949
+ const file = asRecord(entry);
950
+ return file ? [stringValue(file, "hostPath", "path", "filePath")] : [];
951
+ }));
952
+ const fileRoots = uniqueCanonicalPaths(filePaths.map((path) => path ? (0, node_path_1.dirname)(path) : null));
953
+ const roots = uniqueCanonicalPaths([
954
+ ...selectedRoots,
955
+ ...fileRoots
956
+ ]);
957
+ if (roots.length === 0) {
958
+ return null;
959
+ }
960
+ const createdAt = numberValue(record, "createdAt", "created_at") ?? fallbackUpdatedAt;
961
+ const updatedAt = Math.max(fallbackUpdatedAt, numberValue(record, "lastActivityAt", "last_activity_at") ?? 0, numberValue(record, "updatedAt", "updated_at") ?? 0, createdAt);
962
+ return {
963
+ sessionId,
964
+ cliSessionId: stringValue(record, "cliSessionId", "cli_session_id"),
965
+ processName: stringValue(record, "processName", "process_name"),
966
+ vmProcessName: stringValue(record, "vmProcessName", "vm_process_name"),
967
+ title: stringValue(record, "title"),
968
+ initialMessage: stringValue(record, "initialMessage", "initial_message"),
969
+ model: stringValue(record, "model"),
970
+ spaceId: stringValue(record, "spaceId", "space_id"),
971
+ roots,
972
+ filePaths,
973
+ createdAt,
974
+ updatedAt,
975
+ isArchived: booleanValue(record, "isArchived", false)
976
+ };
977
+ }
978
+ async function readClaudeCoworkSessions(limit = 50) {
979
+ const files = await findClaudeCoworkFiles({
980
+ fileNamePattern: /^local_[^.]+\.json$/i,
981
+ limit: Math.max(limit * 4, 20)
982
+ });
983
+ const sessions = await Promise.all(files.map(async (file) => {
984
+ try {
985
+ const [content, fileStats] = await Promise.all([
986
+ (0, promises_1.readFile)(file, "utf8"),
987
+ (0, promises_1.stat)(file)
988
+ ]);
989
+ return normalizeClaudeCoworkSession(JSON.parse(content), fileStats.mtimeMs);
990
+ }
991
+ catch {
992
+ return null;
993
+ }
994
+ }));
995
+ const now = Date.now();
996
+ return sessions
997
+ .filter((session) => Boolean(session))
998
+ .filter((session) => now - session.updatedAt <= RECENT_CLAUDE_COWORK_DISCOVERY_WINDOW_MS)
999
+ .sort((left, right) => right.updatedAt - left.updatedAt)
1000
+ .slice(0, limit);
1001
+ }
1002
+ function normalizeClaudeTeamSnapshot(value, fallbackName, updatedAt) {
1003
+ const record = asRecord(value);
1004
+ if (!record) {
1005
+ return null;
1006
+ }
1007
+ const name = stringValue(record, "name") ?? fallbackName;
1008
+ const members = (Array.isArray(record.members) ? record.members : [])
1009
+ .map(normalizeClaudeTeamMember)
1010
+ .filter((member) => Boolean(member));
1011
+ if (members.length === 0) {
1012
+ return null;
1013
+ }
1014
+ const memberUpdatedAt = Math.max(0, ...members.map((member) => member.joinedAt ?? 0));
1015
+ return {
1016
+ name,
1017
+ description: stringValue(record, "description"),
1018
+ leadAgentId: stringValue(record, "leadAgentId", "lead_agent_id"),
1019
+ leadSessionId: stringValue(record, "leadSessionId", "lead_session_id"),
1020
+ updatedAt: Math.max(updatedAt, numberValue(record, "createdAt", "created_at") ?? 0, memberUpdatedAt),
1021
+ members
1022
+ };
1023
+ }
1024
+ function isVisibleClaudeTeam(team, now = Date.now()) {
1025
+ const ageMs = now - team.updatedAt;
1026
+ if (ageMs <= RECENT_CLAUDE_TEAM_DISCOVERY_WINDOW_MS) {
1027
+ return true;
1028
+ }
1029
+ return ageMs <= ACTIVE_CLAUDE_TEAM_DISCOVERY_WINDOW_MS
1030
+ && team.members.some((member) => member.name !== "team-lead" && member.isActive);
1031
+ }
1032
+ async function readClaudeTeamSnapshotsFromDir(teamsDir, limit = 50) {
1033
+ const entries = await (0, promises_1.readdir)(teamsDir, { withFileTypes: true }).catch(() => []);
1034
+ const teams = await Promise.all(entries
1035
+ .filter((entry) => entry.isDirectory())
1036
+ .map(async (entry) => {
1037
+ const configPath = (0, node_path_1.join)(teamsDir, entry.name, "config.json");
1038
+ try {
1039
+ const [content, fileStats] = await Promise.all([
1040
+ (0, promises_1.readFile)(configPath, "utf8"),
1041
+ (0, promises_1.stat)(configPath)
1042
+ ]);
1043
+ return normalizeClaudeTeamSnapshot(JSON.parse(content), entry.name, fileStats.mtimeMs);
1044
+ }
1045
+ catch {
1046
+ return null;
1047
+ }
1048
+ }));
1049
+ return teams
1050
+ .filter((team) => Boolean(team))
1051
+ .filter((team) => isVisibleClaudeTeam(team))
1052
+ .sort((left, right) => right.updatedAt - left.updatedAt)
1053
+ .slice(0, limit);
1054
+ }
1055
+ async function readClaudeTeamSnapshots(limit = 50) {
1056
+ return readClaudeTeamSnapshotsFromDir(CLAUDE_TEAMS_DIR, limit);
1057
+ }
1058
+ function inferClaudeTeamLeadSessionIds(teams, sessions) {
1059
+ const inferred = new Map();
1060
+ for (const team of teams) {
1061
+ if (team.leadSessionId) {
1062
+ inferred.set(team.name, { sessionId: team.leadSessionId, updatedAt: team.updatedAt });
1063
+ continue;
1064
+ }
1065
+ for (const session of sessions) {
1066
+ for (const record of session.hookRecords) {
1067
+ if (stringValue(record, "team_name", "teamName") !== team.name) {
1068
+ continue;
1069
+ }
1070
+ const updatedAt = recordTimestampMs(record, session.updatedAt);
1071
+ const existing = inferred.get(team.name);
1072
+ if (!existing || updatedAt > existing.updatedAt) {
1073
+ inferred.set(team.name, { sessionId: session.sessionId, updatedAt });
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+ return new Map([...inferred.entries()].map(([teamName, value]) => [teamName, value.sessionId]));
1079
+ }
1080
+ function buildClaudeTeamIndex(teams, inferredLeadSessionIds = new Map()) {
1081
+ const bySessionId = new Map();
1082
+ const byLeadAndAgentId = new Map();
1083
+ const contexts = [];
1084
+ for (const team of teams) {
1085
+ const leadSessionId = team.leadSessionId ?? inferredLeadSessionIds.get(team.name) ?? null;
1086
+ for (const member of team.members) {
1087
+ if (member.name === "team-lead") {
1088
+ continue;
1089
+ }
1090
+ const context = { team, member, leadSessionId };
1091
+ contexts.push(context);
1092
+ if (member.sessionId) {
1093
+ bySessionId.set(member.sessionId, context);
1094
+ }
1095
+ if (leadSessionId) {
1096
+ byLeadAndAgentId.set(claudeTeamMemberContextKey(leadSessionId, member.agentId), context);
1097
+ }
1098
+ }
1099
+ }
1100
+ return { bySessionId, byLeadAndAgentId, contexts };
1101
+ }
1102
+ function claudeProjectsFromTeams(teams, limit = 50) {
1103
+ const grouped = new Map();
1104
+ for (const team of teams) {
1105
+ for (const member of team.members) {
1106
+ if (member.name === "team-lead") {
1107
+ continue;
1108
+ }
1109
+ const root = claudeTeamMemberPrimaryCwd(member);
1110
+ if (!root) {
1111
+ continue;
1112
+ }
1113
+ const updatedAt = Math.max(team.updatedAt, member.joinedAt ?? 0);
1114
+ const existing = grouped.get(root);
1115
+ if (existing) {
1116
+ existing.count += 1;
1117
+ existing.updatedAt = Math.max(existing.updatedAt, updatedAt);
1118
+ mergeClaudeProjectSourceKinds(existing, "claude:teams");
1119
+ }
1120
+ else {
1121
+ grouped.set(root, { root, updatedAt, count: 1, sourceKind: "claude:teams", sourceKinds: ["claude:teams"] });
1122
+ }
1123
+ }
1124
+ }
1125
+ return [...grouped.values()]
1126
+ .sort((left, right) => right.updatedAt - left.updatedAt)
1127
+ .slice(0, limit);
1128
+ }
1129
+ function claudeProjectsFromCowork(input) {
1130
+ const grouped = new Map();
1131
+ const addRoot = (root, updatedAt) => {
1132
+ const existing = [...grouped.values()].find((candidate) => (0, project_paths_1.sameProjectPath)(candidate.root, root));
1133
+ if (existing) {
1134
+ existing.count += 1;
1135
+ existing.updatedAt = Math.max(existing.updatedAt, updatedAt);
1136
+ mergeClaudeProjectSourceKinds(existing, "claude:cowork");
1137
+ return;
1138
+ }
1139
+ grouped.set(root, { root, updatedAt, count: 1, sourceKind: "claude:cowork", sourceKinds: ["claude:cowork"] });
1140
+ };
1141
+ for (const space of input.spaces) {
1142
+ addRoot(space.root, space.updatedAt);
1143
+ }
1144
+ for (const session of input.sessions) {
1145
+ for (const root of session.roots) {
1146
+ addRoot(root, session.updatedAt);
1147
+ }
1148
+ }
1149
+ return [...grouped.values()]
1150
+ .sort((left, right) => right.updatedAt - left.updatedAt)
1151
+ .slice(0, input.limit ?? 50);
1152
+ }
1153
+ function claudeBackgroundJobAgentId(job) {
1154
+ return job.sessionId ? claudeLeadAgentId(job.sessionId) : `claude:bg:${job.jobId}`;
1155
+ }
1156
+ function claudeBackgroundJobRecords(record) {
1157
+ return [
1158
+ record,
1159
+ asRecord(record.job),
1160
+ asRecord(record.session),
1161
+ asRecord(record.sessionInfo),
1162
+ asRecord(record.metadata),
1163
+ asRecord(record.current),
1164
+ asRecord(record.task),
1165
+ asRecord(record.process)
1166
+ ].filter((entry) => Boolean(entry));
1167
+ }
1168
+ function stringArrayValue(record, ...keys) {
1169
+ for (const key of keys) {
1170
+ const value = record[key];
1171
+ if (Array.isArray(value)) {
1172
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
1173
+ }
1174
+ }
1175
+ return [];
1176
+ }
1177
+ function firstStringArrayValue(records, ...keys) {
1178
+ for (const record of records) {
1179
+ const value = stringArrayValue(record, ...keys);
1180
+ if (value.length > 0) {
1181
+ return value;
1182
+ }
1183
+ }
1184
+ return [];
1185
+ }
1186
+ function claudeProjectRootFromWorktreePath(path) {
1187
+ if (!path) {
1188
+ return null;
1189
+ }
1190
+ const marker = "/.claude/worktrees/";
1191
+ const normalized = path.replace(/\\/g, "/");
1192
+ const markerIndex = normalized.indexOf(marker);
1193
+ return markerIndex > 0 ? normalized.slice(0, markerIndex) : null;
1194
+ }
1195
+ function normalizeClaudeBackgroundJobState(input) {
1196
+ const text = `${input.stateText ?? ""} ${input.detail}`.toLowerCase();
1197
+ if (input.needsInput || /\b(needs?\s+(input|approval|permission|you)|waiting|blocked on user|prompt)\b/.test(text)) {
1198
+ return "waiting";
1199
+ }
1200
+ if (/\b(error|failed|failure|crash|killed|denied|blocked)\b/.test(text)) {
1201
+ return "blocked";
1202
+ }
1203
+ if (/\b(review|paused|sleep|suspended)\b/.test(text)) {
1204
+ return "waiting";
1205
+ }
1206
+ if (/\b(done|complete|completed|success|exited|stopped|finished)\b/.test(text)) {
1207
+ return "done";
1208
+ }
1209
+ if (input.isRunning === true || /\b(working|running|active|executing|processing|busy)\b/.test(text)) {
1210
+ return "running";
1211
+ }
1212
+ if (/\b(queued|pending|created|dispatch|starting)\b/.test(text)) {
1213
+ return "planning";
1214
+ }
1215
+ const ageMs = Date.now() - input.updatedAt;
1216
+ if (ageMs <= RECENT_CLAUDE_HOOK_ACTIVE_WINDOW_MS) {
1217
+ return "thinking";
1218
+ }
1219
+ return ageMs <= RECENT_DONE_WINDOW_MS ? "done" : "idle";
1220
+ }
1221
+ function isClaudeBackgroundJobOngoing(state) {
1222
+ return state !== "done" && state !== "idle" && state !== "blocked";
1223
+ }
1224
+ function normalizeClaudeBackgroundJobForTest(jobId, value, fallbackUpdatedAt) {
1225
+ return normalizeClaudeBackgroundJob(jobId, value, fallbackUpdatedAt);
1226
+ }
1227
+ function normalizeClaudeBackgroundJob(jobId, value, fallbackUpdatedAt) {
1228
+ const record = asRecord(value);
1229
+ if (!record) {
1230
+ return null;
1231
+ }
1232
+ const records = claudeBackgroundJobRecords(record);
1233
+ const cwd = canonicalizeProjectPath(firstStringValue(records, "cwd", "dir", "directory", "workingDirectory", "working_directory"))
1234
+ ?? null;
1235
+ const explicitWorktreePath = canonicalizeProjectPath(firstStringValue(records, "worktreePath", "worktree_path"));
1236
+ const worktreePath = explicitWorktreePath ?? (claudeProjectRootFromWorktreePath(cwd) ? cwd : null);
1237
+ const explicitProjectRoot = canonicalizeProjectPath(firstStringValue(records, "projectRoot", "project_root", "workspacePath", "workspace_path", "repoPath", "repo_path", "root"))
1238
+ ?? firstStringArrayValue(records, "workspaceRoots", "workspace_roots", "hostPaths", "host_paths", "roots")
1239
+ .map((root) => canonicalizeProjectPath(root))
1240
+ .find((root) => Boolean(root))
1241
+ ?? null;
1242
+ const resolvedProjectRoot = explicitProjectRoot ?? claudeProjectRootFromWorktreePath(worktreePath ?? cwd) ?? cwd;
1243
+ if (!resolvedProjectRoot) {
1244
+ return null;
1245
+ }
1246
+ const resolvedCwd = cwd ?? worktreePath ?? resolvedProjectRoot;
1247
+ const sessionId = firstStringValue(records, "sessionId", "session_id", "cliSessionId", "cli_session_id", "conversationId", "conversation_id");
1248
+ const name = firstStringValue(records, "name", "title", "label");
1249
+ const prompt = firstStringValue(records, "prompt", "initialPrompt", "initial_prompt", "input", "userPrompt", "user_prompt");
1250
+ const detail = firstStringValue(records, "currentActivity", "current_activity", "activity", "summary", "statusMessage", "status_message", "message", "need", "lastActivity", "last_activity")
1251
+ ?? prompt
1252
+ ?? name
1253
+ ?? "Claude background session";
1254
+ const stateText = firstStringValue(records, "state", "status", "phase", "lifecycle", "runState", "run_state");
1255
+ const updatedAt = firstTimestampValueMs(records, "updatedAt", "updated_at", "lastUpdatedAt", "last_updated_at", "lastModified", "last_modified", "lastActivityAt", "last_activity_at", "mtimeMs")
1256
+ ?? fallbackUpdatedAt;
1257
+ const createdAt = firstTimestampValueMs(records, "createdAt", "created_at", "startedAt", "started_at")
1258
+ ?? null;
1259
+ const needsInput = firstBooleanValue(records, "needsInput", "needs_input", "needsUser", "needs_user", "waitingForInput", "waiting_for_input") ?? false;
1260
+ const isRunning = firstBooleanValue(records, "isRunning", "is_running", "running", "active");
1261
+ const state = normalizeClaudeBackgroundJobState({
1262
+ stateText,
1263
+ detail,
1264
+ needsInput,
1265
+ isRunning,
1266
+ updatedAt
1267
+ });
1268
+ return {
1269
+ jobId,
1270
+ sessionId,
1271
+ name,
1272
+ prompt,
1273
+ cwd: resolvedCwd,
1274
+ projectRoot: resolvedProjectRoot,
1275
+ worktreePath,
1276
+ state,
1277
+ stateText,
1278
+ detail: shorten(detail, 88),
1279
+ updatedAt,
1280
+ createdAt,
1281
+ isOngoing: isClaudeBackgroundJobOngoing(state)
1282
+ };
1283
+ }
1284
+ async function readClaudeBackgroundJobs(limit = 50) {
1285
+ const jobsById = new Map();
1286
+ for (const jobsDir of claudeJobsDirs()) {
1287
+ const entries = await (0, promises_1.readdir)(jobsDir, { withFileTypes: true }).catch(() => []);
1288
+ const jobs = await Promise.all(entries
1289
+ .filter((entry) => entry.isDirectory())
1290
+ .map(async (entry) => {
1291
+ const statePath = (0, node_path_1.join)(jobsDir, entry.name, "state.json");
1292
+ try {
1293
+ const [content, fileStats] = await Promise.all([
1294
+ (0, promises_1.readFile)(statePath, "utf8"),
1295
+ (0, promises_1.stat)(statePath)
1296
+ ]);
1297
+ return normalizeClaudeBackgroundJob(entry.name, JSON.parse(content), fileStats.mtimeMs);
1298
+ }
1299
+ catch {
1300
+ return null;
1301
+ }
1302
+ }));
1303
+ for (const job of jobs) {
1304
+ if (!job) {
1305
+ continue;
1306
+ }
1307
+ const key = job.sessionId ?? job.jobId;
1308
+ const existing = jobsById.get(key);
1309
+ if (!existing || job.updatedAt >= existing.updatedAt) {
1310
+ jobsById.set(key, job);
1311
+ }
1312
+ }
1313
+ }
1314
+ const now = Date.now();
1315
+ return [...jobsById.values()]
1316
+ .filter((job) => job.isOngoing || now - job.updatedAt <= RECENT_CLAUDE_BACKGROUND_DISCOVERY_WINDOW_MS)
1317
+ .sort((left, right) => right.updatedAt - left.updatedAt)
1318
+ .slice(0, limit);
1319
+ }
1320
+ function claudeProjectsFromBackgroundJobs(jobs, limit = 50) {
1321
+ const grouped = new Map();
1322
+ for (const job of jobs) {
1323
+ const existing = [...grouped.values()].find((candidate) => (0, project_paths_1.sameProjectPath)(candidate.root, job.projectRoot));
1324
+ if (existing) {
1325
+ existing.count += 1;
1326
+ existing.updatedAt = Math.max(existing.updatedAt, job.updatedAt);
1327
+ mergeClaudeProjectSourceKinds(existing, "claude:background");
1328
+ continue;
1329
+ }
1330
+ grouped.set(job.projectRoot, {
1331
+ root: job.projectRoot,
1332
+ updatedAt: job.updatedAt,
1333
+ count: 1,
1334
+ sourceKind: "claude:background",
1335
+ sourceKinds: ["claude:background"]
1336
+ });
1337
+ }
1338
+ return [...grouped.values()]
1339
+ .sort((left, right) => right.updatedAt - left.updatedAt)
1340
+ .slice(0, limit);
1341
+ }
1342
+ function claudeToolSummary(input) {
1343
+ const toolName = input.toolName.trim();
1344
+ const toolPaths = extractToolPaths(input.toolInput);
1345
+ const primaryPath = toolPaths[0] ?? input.fallbackCwd;
1346
+ if (/(edit|write|multiedit)/i.test(toolName)) {
1347
+ return {
1348
+ label: labelFromModel(input.model, input.sessionId),
1349
+ sourceKind: sourceKindFromModel(input.model),
1350
+ state: input.failed ? "blocked" : "editing",
1351
+ detail: primaryPath ? `Editing ${primaryPath}` : "Editing files",
1352
+ updatedAt: input.updatedAt,
1353
+ paths: toolPaths.length > 0 ? toolPaths : [input.fallbackCwd],
1354
+ activityEvent: {
1355
+ type: "fileChange",
1356
+ action: "edited",
1357
+ path: primaryPath ?? null,
1358
+ title: primaryPath ? `edit ${primaryPath}` : "Editing files",
1359
+ isImage: isImagePath(primaryPath ?? null)
1360
+ },
1361
+ gitBranch: input.gitBranch,
1362
+ confidence: "typed",
1363
+ needsUser: null,
1364
+ latestMessage: null,
1365
+ isOngoing: !input.failed
1366
+ };
1367
+ }
1368
+ if (/(bash|shell)/i.test(toolName)) {
1369
+ const command = typeof input.toolInput.command === "string" ? input.toolInput.command
1370
+ : typeof input.toolInput.cmd === "string" ? input.toolInput.cmd
1371
+ : toolName;
1372
+ const cwd = canonicalizeProjectPath(typeof input.toolInput.cwd === "string" ? input.toolInput.cwd : null)
1373
+ ?? input.fallbackCwd;
1374
+ return {
1375
+ label: labelFromModel(input.model, input.sessionId),
1376
+ sourceKind: sourceKindFromModel(input.model),
1377
+ state: input.failed ? "blocked" : looksLikeValidationCommand(command) ? "validating" : "running",
1378
+ detail: command,
1379
+ updatedAt: input.updatedAt,
1380
+ paths: cwd ? [cwd] : [],
1381
+ activityEvent: {
1382
+ type: "commandExecution",
1383
+ action: "ran",
1384
+ path: cwd ?? null,
1385
+ title: command,
1386
+ isImage: false
1387
+ },
1388
+ gitBranch: input.gitBranch,
1389
+ confidence: "typed",
1390
+ needsUser: null,
1391
+ latestMessage: null,
1392
+ isOngoing: !input.failed
1393
+ };
1394
+ }
1395
+ if (/(read|grep|glob|search|ls|list)/i.test(toolName)) {
1396
+ return {
1397
+ label: labelFromModel(input.model, input.sessionId),
1398
+ sourceKind: sourceKindFromModel(input.model),
1399
+ state: input.failed ? "blocked" : "scanning",
1400
+ detail: primaryPath ? `Reading ${primaryPath}` : `Using ${toolName}`,
1401
+ updatedAt: input.updatedAt,
1402
+ paths: toolPaths.length > 0 ? toolPaths : [input.fallbackCwd],
1403
+ activityEvent: null,
1404
+ gitBranch: input.gitBranch,
1405
+ confidence: "typed",
1406
+ needsUser: null,
1407
+ latestMessage: null,
1408
+ isOngoing: !input.failed
1409
+ };
1410
+ }
1411
+ if (/(task|delegate|agent)/i.test(toolName)) {
1412
+ const description = stringValue(input.toolInput, "description", "name", "team_name");
1413
+ const subagentType = stringValue(input.toolInput, "subagent_type");
1414
+ const detail = input.failed ? "Delegation failed"
1415
+ : description ? `Delegating ${description}`
1416
+ : subagentType ? `Delegating to ${subagentType}`
1417
+ : "Delegating work";
1418
+ return {
1419
+ label: labelFromModel(input.model, input.sessionId),
1420
+ sourceKind: sourceKindFromModel(input.model),
1421
+ state: input.failed ? "blocked" : "delegating",
1422
+ detail: shorten(detail, 88),
1423
+ updatedAt: input.updatedAt,
1424
+ paths: [input.fallbackCwd],
1425
+ activityEvent: claudeCollabActivityEvent({
1426
+ detail,
1427
+ path: input.fallbackCwd
1428
+ }),
1429
+ gitBranch: input.gitBranch,
1430
+ confidence: "typed",
1431
+ needsUser: null,
1432
+ latestMessage: null,
1433
+ isOngoing: !input.failed
1434
+ };
1435
+ }
1436
+ return null;
1437
+ }
1438
+ function summariseClaudeHookRecord(input) {
1439
+ const hookEventName = typeof input.record.hook_event_name === "string" ? input.record.hook_event_name : null;
1440
+ if (!hookEventName) {
1441
+ return null;
1442
+ }
1443
+ const cwd = canonicalizeProjectPath(typeof input.record.cwd === "string" ? input.record.cwd : null)
1444
+ ?? input.fallbackCwd;
1445
+ const updatedAt = new Date(recordTimestampMs(input.record, input.fallbackUpdatedAt)).toISOString();
1446
+ const toolName = typeof input.record.tool_name === "string" ? input.record.tool_name : "";
1447
+ const toolInput = asRecord(input.record.tool_input) ?? {};
1448
+ const requestId = stringValue(input.record, "request_id", "requestId")
1449
+ ?? `${input.sessionId}:${hookEventName}:${updatedAt}`;
1450
+ const browserActionable = input.record.hook_source === "claude-agent-sdk";
1451
+ if (hookEventName === "PermissionRequest") {
1452
+ const detail = typeof toolInput.command === "string" ? toolInput.command
1453
+ : extractToolPaths(toolInput)[0]
1454
+ ?? toolName
1455
+ ?? "Permission requested";
1456
+ return {
1457
+ label: labelFromModel(input.model, input.sessionId),
1458
+ sourceKind: sourceKindFromModel(input.model),
1459
+ state: "blocked",
1460
+ detail: shorten(detail, 88),
1461
+ updatedAt,
1462
+ paths: extractToolPaths(toolInput).length > 0 ? extractToolPaths(toolInput) : [cwd],
1463
+ activityEvent: null,
1464
+ gitBranch: input.gitBranch,
1465
+ confidence: "typed",
1466
+ needsUser: {
1467
+ kind: "approval",
1468
+ requestId,
1469
+ reason: stringValue(input.record, "reason", "message") ?? shorten(detail, 88),
1470
+ command: stringValue(toolInput, "command", "cmd") ?? undefined,
1471
+ cwd,
1472
+ grantRoot: extractToolPaths(toolInput)[0] ?? undefined,
1473
+ ...(browserActionable ? { availableDecisions: ["accept", "decline"] } : {})
1474
+ },
1475
+ latestMessage: null,
1476
+ isOngoing: true
1477
+ };
1478
+ }
1479
+ if (hookEventName === "Elicitation") {
1480
+ const detail = typeof input.record.prompt === "string" ? input.record.prompt
1481
+ : typeof input.record.message === "string" ? input.record.message
1482
+ : "Waiting on input";
1483
+ return {
1484
+ label: labelFromModel(input.model, input.sessionId),
1485
+ sourceKind: sourceKindFromModel(input.model),
1486
+ state: "waiting",
1487
+ detail: shorten(detail, 88),
1488
+ updatedAt,
1489
+ paths: [cwd],
1490
+ activityEvent: null,
1491
+ gitBranch: input.gitBranch,
1492
+ confidence: "typed",
1493
+ needsUser: {
1494
+ kind: "input",
1495
+ requestId,
1496
+ reason: shorten(detail, 88),
1497
+ cwd,
1498
+ ...(browserActionable
1499
+ ? (() => {
1500
+ const questions = parseClaudeElicitationQuestions(input.record);
1501
+ return questions ? { questions } : {};
1502
+ })()
1503
+ : {})
1504
+ },
1505
+ latestMessage: null,
1506
+ isOngoing: true
1507
+ };
1508
+ }
1509
+ if (hookEventName === "AgentsOfficePermissionDecision") {
1510
+ const action = stringValue(input.record, "action") ?? "accept";
1511
+ return {
1512
+ label: labelFromModel(input.model, input.sessionId),
1513
+ sourceKind: sourceKindFromModel(input.model),
1514
+ state: "planning",
1515
+ detail: action === "decline" ? "Permission denied" : "Permission approved",
1516
+ updatedAt,
1517
+ paths: [cwd],
1518
+ activityEvent: null,
1519
+ gitBranch: input.gitBranch,
1520
+ confidence: "typed",
1521
+ needsUser: null,
1522
+ latestMessage: null,
1523
+ isOngoing: true
1524
+ };
1525
+ }
1526
+ if (hookEventName === "AgentsOfficeElicitationResponse") {
1527
+ const action = stringValue(input.record, "action") ?? "accept";
1528
+ return {
1529
+ label: labelFromModel(input.model, input.sessionId),
1530
+ sourceKind: sourceKindFromModel(input.model),
1531
+ state: "planning",
1532
+ detail: action === "decline" ? "Input request declined"
1533
+ : action === "cancel" ? "Input request cancelled"
1534
+ : "Input submitted",
1535
+ updatedAt,
1536
+ paths: [cwd],
1537
+ activityEvent: null,
1538
+ gitBranch: input.gitBranch,
1539
+ confidence: "typed",
1540
+ needsUser: null,
1541
+ latestMessage: null,
1542
+ isOngoing: true
1543
+ };
1544
+ }
1545
+ if (hookEventName === "UserPromptSubmit" || hookEventName === "UserPromptExpansion") {
1546
+ const detail = stringValue(input.record, "prompt", "message", "text") ?? "Updated plan";
1547
+ const paths = extractPathsFromText(detail);
1548
+ return {
1549
+ label: labelFromModel(input.model, input.sessionId),
1550
+ sourceKind: sourceKindFromModel(input.model),
1551
+ state: "planning",
1552
+ detail: shorten(detail, 88),
1553
+ updatedAt,
1554
+ paths: paths.length > 0 ? paths : [cwd],
1555
+ activityEvent: {
1556
+ type: "userMessage",
1557
+ action: "said",
1558
+ path: paths[0] ?? cwd,
1559
+ title: shorten(detail, 88),
1560
+ isImage: false
1561
+ },
1562
+ gitBranch: input.gitBranch,
1563
+ confidence: "typed",
1564
+ needsUser: null,
1565
+ latestMessage: null,
1566
+ isOngoing: true
1567
+ };
1568
+ }
1569
+ if (hookEventName === "SessionStart") {
1570
+ return {
1571
+ label: labelFromModel(input.model, input.sessionId),
1572
+ sourceKind: sourceKindFromModel(input.model),
1573
+ state: "planning",
1574
+ detail: "Session started",
1575
+ updatedAt,
1576
+ paths: [cwd],
1577
+ activityEvent: null,
1578
+ gitBranch: input.gitBranch,
1579
+ confidence: "typed",
1580
+ needsUser: null,
1581
+ latestMessage: null,
1582
+ isOngoing: true
1583
+ };
1584
+ }
1585
+ if (hookEventName === "SessionEnd") {
1586
+ return {
1587
+ label: labelFromModel(input.model, input.sessionId),
1588
+ sourceKind: sourceKindFromModel(input.model),
1589
+ state: "done",
1590
+ detail: "Session ended",
1591
+ updatedAt,
1592
+ paths: [cwd],
1593
+ activityEvent: null,
1594
+ gitBranch: input.gitBranch,
1595
+ confidence: "typed",
1596
+ needsUser: null,
1597
+ latestMessage: null,
1598
+ isOngoing: false
1599
+ };
1600
+ }
1601
+ if (hookEventName === "PreCompact") {
1602
+ return {
1603
+ label: labelFromModel(input.model, input.sessionId),
1604
+ sourceKind: sourceKindFromModel(input.model),
1605
+ state: "thinking",
1606
+ detail: "Compacting context",
1607
+ updatedAt,
1608
+ paths: [cwd],
1609
+ activityEvent: null,
1610
+ gitBranch: input.gitBranch,
1611
+ confidence: "typed",
1612
+ needsUser: null,
1613
+ latestMessage: null,
1614
+ isOngoing: true
1615
+ };
1616
+ }
1617
+ if (hookEventName === "PostCompact") {
1618
+ return {
1619
+ label: labelFromModel(input.model, input.sessionId),
1620
+ sourceKind: sourceKindFromModel(input.model),
1621
+ state: "thinking",
1622
+ detail: "Context compacted",
1623
+ updatedAt,
1624
+ paths: [cwd],
1625
+ activityEvent: null,
1626
+ gitBranch: input.gitBranch,
1627
+ confidence: "typed",
1628
+ needsUser: null,
1629
+ latestMessage: null,
1630
+ isOngoing: true
1631
+ };
1632
+ }
1633
+ if (hookEventName === "Setup") {
1634
+ const detail = input.record.trigger === "maintenance" ? "Running Claude maintenance"
1635
+ : "Initializing Claude session";
1636
+ return {
1637
+ label: labelFromModel(input.model, input.sessionId),
1638
+ sourceKind: sourceKindFromModel(input.model),
1639
+ state: "planning",
1640
+ detail,
1641
+ updatedAt,
1642
+ paths: [cwd],
1643
+ activityEvent: null,
1644
+ gitBranch: input.gitBranch,
1645
+ confidence: "typed",
1646
+ needsUser: null,
1647
+ latestMessage: null,
1648
+ isOngoing: true
1649
+ };
1650
+ }
1651
+ if (hookEventName === "SubagentStart") {
1652
+ const detail = typeof input.record.agent_type === "string" ? `Spawning ${input.record.agent_type} subagent`
1653
+ : "Spawning subagent";
1654
+ return {
1655
+ label: labelFromModel(input.model, input.sessionId),
1656
+ sourceKind: sourceKindFromModel(input.model),
1657
+ state: "delegating",
1658
+ detail,
1659
+ updatedAt,
1660
+ paths: [cwd],
1661
+ activityEvent: claudeCollabActivityEvent({
1662
+ detail,
1663
+ path: cwd
1664
+ }),
1665
+ gitBranch: input.gitBranch,
1666
+ confidence: "typed",
1667
+ needsUser: null,
1668
+ latestMessage: null,
1669
+ isOngoing: true
1670
+ };
1671
+ }
1672
+ if (hookEventName === "SubagentStop") {
1673
+ const detail = typeof input.record.agent_type === "string" ? `${input.record.agent_type} subagent finished`
1674
+ : "Subagent finished";
1675
+ const lastAssistantMessage = stringValue(input.record, "last_assistant_message", "message");
1676
+ return {
1677
+ label: labelFromModel(input.model, input.sessionId),
1678
+ sourceKind: sourceKindFromModel(input.model),
1679
+ state: "done",
1680
+ detail,
1681
+ updatedAt,
1682
+ paths: [cwd],
1683
+ activityEvent: claudeCollabActivityEvent({
1684
+ detail,
1685
+ path: cwd
1686
+ }),
1687
+ gitBranch: input.gitBranch,
1688
+ confidence: "typed",
1689
+ needsUser: null,
1690
+ latestMessage: lastAssistantMessage,
1691
+ isOngoing: false
1692
+ };
1693
+ }
1694
+ if (hookEventName === "StopFailure") {
1695
+ const detail = typeof input.record.error === "string" ? input.record.error
1696
+ : typeof input.record.reason === "string" ? input.record.reason
1697
+ : "Claude turn failed";
1698
+ return {
1699
+ label: labelFromModel(input.model, input.sessionId),
1700
+ sourceKind: sourceKindFromModel(input.model),
1701
+ state: "blocked",
1702
+ detail: shorten(detail, 88),
1703
+ updatedAt,
1704
+ paths: [cwd],
1705
+ activityEvent: null,
1706
+ gitBranch: input.gitBranch,
1707
+ confidence: "typed",
1708
+ needsUser: null,
1709
+ latestMessage: null,
1710
+ isOngoing: false
1711
+ };
1712
+ }
1713
+ if (hookEventName === "Stop" || hookEventName === "TaskCompleted") {
1714
+ const detail = hookEventName === "TaskCompleted"
1715
+ ? stringValue(input.record, "task_subject", "task_description", "message") ?? "Task completed"
1716
+ : stringValue(input.record, "last_assistant_message") ?? "Finished recently";
1717
+ return {
1718
+ label: labelFromModel(input.model, input.sessionId),
1719
+ sourceKind: sourceKindFromModel(input.model),
1720
+ state: "done",
1721
+ detail: shorten(detail, 88),
1722
+ updatedAt,
1723
+ paths: [cwd],
1724
+ activityEvent: null,
1725
+ gitBranch: input.gitBranch,
1726
+ confidence: "typed",
1727
+ needsUser: null,
1728
+ latestMessage: hookEventName === "Stop" ? stringValue(input.record, "last_assistant_message") : null,
1729
+ isOngoing: false
1730
+ };
1731
+ }
1732
+ if (hookEventName === "Notification") {
1733
+ const title = stringValue(input.record, "title");
1734
+ const message = stringValue(input.record, "message") ?? title ?? "Claude notification";
1735
+ const notificationType = stringValue(input.record, "notification_type");
1736
+ const state = notificationType && /(error|fail|denied|blocked)/i.test(notificationType) ? "blocked"
1737
+ : notificationType && /(wait|input|approval)/i.test(notificationType) ? "waiting"
1738
+ : "thinking";
1739
+ return {
1740
+ label: labelFromModel(input.model, input.sessionId),
1741
+ sourceKind: sourceKindFromModel(input.model),
1742
+ state,
1743
+ detail: shorten(message, 88),
1744
+ updatedAt,
1745
+ paths: [cwd],
1746
+ activityEvent: {
1747
+ type: "agentMessage",
1748
+ action: "said",
1749
+ path: cwd,
1750
+ title: shorten(title ? `${title}: ${message}` : message, 88),
1751
+ isImage: false
1752
+ },
1753
+ gitBranch: input.gitBranch,
1754
+ confidence: "typed",
1755
+ needsUser: null,
1756
+ latestMessage: message,
1757
+ isOngoing: state !== "blocked"
1758
+ };
1759
+ }
1760
+ if (hookEventName === "MessageDisplay") {
1761
+ const delta = stringValue(input.record, "delta") ?? "";
1762
+ const final = booleanValue(input.record, "final", false);
1763
+ const detail = delta.trim() || (final ? "Assistant message displayed" : "Claude is responding");
1764
+ return {
1765
+ label: labelFromModel(input.model, input.sessionId),
1766
+ sourceKind: sourceKindFromModel(input.model),
1767
+ state: "thinking",
1768
+ detail: shorten(detail, 88),
1769
+ updatedAt,
1770
+ paths: [cwd],
1771
+ activityEvent: delta.trim()
1772
+ ? {
1773
+ type: "agentMessage",
1774
+ action: "said",
1775
+ path: cwd,
1776
+ title: shorten(delta, 88),
1777
+ isImage: false
1778
+ }
1779
+ : null,
1780
+ gitBranch: input.gitBranch,
1781
+ confidence: "typed",
1782
+ needsUser: null,
1783
+ latestMessage: delta.trim() || null,
1784
+ // `final` ends this displayed assistant message, not the Claude turn.
1785
+ // Stop/StopFailure/SessionEnd remain the lifecycle signals that settle work.
1786
+ isOngoing: true
1787
+ };
1788
+ }
1789
+ if (hookEventName === "TeammateIdle") {
1790
+ const teammate = stringValue(input.record, "teammate_name") ?? "Teammate";
1791
+ const teamName = stringValue(input.record, "team_name");
1792
+ const detail = teamName ? `${teammate} is idle in ${teamName}` : `${teammate} is idle`;
1793
+ return {
1794
+ label: labelFromModel(input.model, input.sessionId),
1795
+ sourceKind: sourceKindFromModel(input.model),
1796
+ state: "waiting",
1797
+ detail,
1798
+ updatedAt,
1799
+ paths: [cwd],
1800
+ activityEvent: null,
1801
+ gitBranch: input.gitBranch,
1802
+ confidence: "typed",
1803
+ needsUser: null,
1804
+ latestMessage: null,
1805
+ isOngoing: true
1806
+ };
1807
+ }
1808
+ if (hookEventName === "TaskCreated") {
1809
+ const detail = stringValue(input.record, "task_subject", "task_description", "message") ?? "Task created";
1810
+ return {
1811
+ label: labelFromModel(input.model, input.sessionId),
1812
+ sourceKind: sourceKindFromModel(input.model),
1813
+ state: "delegating",
1814
+ detail: shorten(detail, 88),
1815
+ updatedAt,
1816
+ paths: [cwd],
1817
+ activityEvent: claudeCollabActivityEvent({
1818
+ detail,
1819
+ path: cwd
1820
+ }),
1821
+ gitBranch: input.gitBranch,
1822
+ confidence: "typed",
1823
+ needsUser: null,
1824
+ latestMessage: null,
1825
+ isOngoing: true
1826
+ };
1827
+ }
1828
+ if (hookEventName === "PermissionDenied") {
1829
+ const detail = stringValue(input.record, "reason", "message") ?? "Permission denied";
1830
+ return {
1831
+ label: labelFromModel(input.model, input.sessionId),
1832
+ sourceKind: sourceKindFromModel(input.model),
1833
+ state: "blocked",
1834
+ detail: shorten(detail, 88),
1835
+ updatedAt,
1836
+ paths: [cwd],
1837
+ activityEvent: null,
1838
+ gitBranch: input.gitBranch,
1839
+ confidence: "typed",
1840
+ needsUser: null,
1841
+ latestMessage: null,
1842
+ isOngoing: false
1843
+ };
1844
+ }
1845
+ if (hookEventName === "ElicitationResult") {
1846
+ const action = stringValue(input.record, "action") ?? "accept";
1847
+ return {
1848
+ label: labelFromModel(input.model, input.sessionId),
1849
+ sourceKind: sourceKindFromModel(input.model),
1850
+ state: "planning",
1851
+ detail: action === "decline" ? "Input request declined"
1852
+ : action === "cancel" ? "Input request cancelled"
1853
+ : "Input submitted",
1854
+ updatedAt,
1855
+ paths: [cwd],
1856
+ activityEvent: null,
1857
+ gitBranch: input.gitBranch,
1858
+ confidence: "typed",
1859
+ needsUser: null,
1860
+ latestMessage: null,
1861
+ isOngoing: true
1862
+ };
1863
+ }
1864
+ if (hookEventName === "ConfigChange") {
1865
+ const source = stringValue(input.record, "source") ?? "settings";
1866
+ const changedPath = stringValue(input.record, "file_path");
1867
+ return {
1868
+ label: labelFromModel(input.model, input.sessionId),
1869
+ sourceKind: sourceKindFromModel(input.model),
1870
+ state: "planning",
1871
+ detail: changedPath ? `Updated ${source} via ${changedPath}` : `Updated ${source}`,
1872
+ updatedAt,
1873
+ paths: changedPath ? [canonicalizeProjectPath(changedPath) ?? changedPath, cwd] : [cwd],
1874
+ activityEvent: null,
1875
+ gitBranch: input.gitBranch,
1876
+ confidence: "typed",
1877
+ needsUser: null,
1878
+ latestMessage: null,
1879
+ isOngoing: true
1880
+ };
1881
+ }
1882
+ if (hookEventName === "InstructionsLoaded") {
1883
+ const memoryType = stringValue(input.record, "memory_type") ?? "Project";
1884
+ const filePath = stringValue(input.record, "file_path");
1885
+ return {
1886
+ label: labelFromModel(input.model, input.sessionId),
1887
+ sourceKind: sourceKindFromModel(input.model),
1888
+ state: "planning",
1889
+ detail: filePath ? `Loaded ${memoryType} instructions from ${filePath}` : `Loaded ${memoryType} instructions`,
1890
+ updatedAt,
1891
+ paths: filePath ? [canonicalizeProjectPath(filePath) ?? filePath, cwd] : [cwd],
1892
+ activityEvent: null,
1893
+ gitBranch: input.gitBranch,
1894
+ confidence: "typed",
1895
+ needsUser: null,
1896
+ latestMessage: null,
1897
+ isOngoing: true
1898
+ };
1899
+ }
1900
+ if (hookEventName === "CwdChanged") {
1901
+ const nextCwd = canonicalizeProjectPath(stringValue(input.record, "new_cwd"))
1902
+ ?? cwd;
1903
+ return {
1904
+ label: labelFromModel(input.model, input.sessionId),
1905
+ sourceKind: sourceKindFromModel(input.model),
1906
+ state: "planning",
1907
+ detail: `Moved to ${nextCwd}`,
1908
+ updatedAt,
1909
+ paths: [nextCwd],
1910
+ activityEvent: null,
1911
+ gitBranch: input.gitBranch,
1912
+ confidence: "typed",
1913
+ needsUser: null,
1914
+ latestMessage: null,
1915
+ isOngoing: true
1916
+ };
1917
+ }
1918
+ if (hookEventName === "FileChanged") {
1919
+ const filePath = stringValue(input.record, "file_path");
1920
+ const normalizedPath = canonicalizeProjectPath(filePath) ?? filePath ?? cwd;
1921
+ const fileEvent = stringValue(input.record, "event") ?? "change";
1922
+ const action = fileEvent === "add" ? "created"
1923
+ : fileEvent === "unlink" ? "deleted"
1924
+ : "edited";
1925
+ return {
1926
+ label: labelFromModel(input.model, input.sessionId),
1927
+ sourceKind: sourceKindFromModel(input.model),
1928
+ state: "editing",
1929
+ detail: normalizedPath ? `${action[0].toUpperCase()}${action.slice(1)} ${normalizedPath}` : "Changed files",
1930
+ updatedAt,
1931
+ paths: normalizedPath ? [normalizedPath, cwd] : [cwd],
1932
+ activityEvent: {
1933
+ type: "fileChange",
1934
+ action,
1935
+ path: normalizedPath ?? null,
1936
+ title: normalizedPath ? `${action} ${normalizedPath}` : "Changed files",
1937
+ isImage: isImagePath(normalizedPath ?? null)
1938
+ },
1939
+ gitBranch: input.gitBranch,
1940
+ confidence: "typed",
1941
+ needsUser: null,
1942
+ latestMessage: null,
1943
+ isOngoing: true
1944
+ };
1945
+ }
1946
+ if (hookEventName === "WorktreeCreate" || hookEventName === "WorktreeRemove") {
1947
+ const worktreePath = canonicalizeProjectPath(stringValue(input.record, "worktree_path"))
1948
+ ?? canonicalizeProjectPath(stringValue(input.record, "name"))
1949
+ ?? cwd;
1950
+ return {
1951
+ label: labelFromModel(input.model, input.sessionId),
1952
+ sourceKind: sourceKindFromModel(input.model),
1953
+ state: "planning",
1954
+ detail: hookEventName === "WorktreeCreate" ? `Created worktree ${worktreePath}`
1955
+ : `Removed worktree ${worktreePath}`,
1956
+ updatedAt,
1957
+ paths: [worktreePath],
1958
+ activityEvent: null,
1959
+ gitBranch: input.gitBranch,
1960
+ confidence: "typed",
1961
+ needsUser: null,
1962
+ latestMessage: null,
1963
+ isOngoing: hookEventName === "WorktreeCreate"
1964
+ };
1965
+ }
1966
+ if (hookEventName === "PostToolBatch") {
1967
+ return {
1968
+ label: labelFromModel(input.model, input.sessionId),
1969
+ sourceKind: sourceKindFromModel(input.model),
1970
+ state: "thinking",
1971
+ detail: "Tool batch completed",
1972
+ updatedAt,
1973
+ paths: [cwd],
1974
+ activityEvent: null,
1975
+ gitBranch: input.gitBranch,
1976
+ confidence: "typed",
1977
+ needsUser: null,
1978
+ latestMessage: null,
1979
+ isOngoing: true
1980
+ };
1981
+ }
1982
+ if (hookEventName === "PreToolUse" || hookEventName === "PostToolUse" || hookEventName === "PostToolUseFailure") {
1983
+ return claudeToolSummary({
1984
+ sessionId: input.sessionId,
1985
+ model: input.model,
1986
+ fallbackCwd: cwd,
1987
+ gitBranch: input.gitBranch,
1988
+ updatedAt,
1989
+ toolName,
1990
+ toolInput,
1991
+ failed: hookEventName === "PostToolUseFailure"
1992
+ });
1993
+ }
1994
+ return null;
1995
+ }
1996
+ function sourceKindFromModel(model) {
1997
+ return model ? `claude:${model}` : "claude";
1998
+ }
1999
+ function normalizeClaudeDisplayModel(model) {
2000
+ if (!model) {
2001
+ return null;
2002
+ }
2003
+ const raw = model.trim();
2004
+ if (!raw || /^<[^>]+>$/.test(raw)) {
2005
+ return null;
2006
+ }
2007
+ const normalized = raw
2008
+ .replace(/^claude-/i, "")
2009
+ .replace(/-\d{8}$/i, "")
2010
+ .replace(/-/g, " ")
2011
+ .trim();
2012
+ return normalized || null;
2013
+ }
2014
+ function normalizeClaudeSessionTitle(title) {
2015
+ if (!title) {
2016
+ return null;
2017
+ }
2018
+ const normalized = title.replace(/\s+/g, " ").trim();
2019
+ if (!normalized || !isMeaningfulTranscriptText(normalized) || isSyntheticClaudeUserText(normalized)) {
2020
+ return null;
2021
+ }
2022
+ return normalized;
2023
+ }
2024
+ function extractClaudeSessionTitle(records) {
2025
+ for (const record of [...records].reverse()) {
2026
+ const type = typeof record.type === "string" ? record.type : "";
2027
+ if (type === "ai-title") {
2028
+ const title = normalizeClaudeSessionTitle(stringValue(record, "aiTitle", "ai_title", "title", "name"));
2029
+ if (title) {
2030
+ return title;
2031
+ }
2032
+ }
2033
+ if (type === "session-title" || type === "session_title") {
2034
+ const title = normalizeClaudeSessionTitle(stringValue(record, "title", "name", "summary"));
2035
+ if (title) {
2036
+ return title;
2037
+ }
2038
+ }
2039
+ }
2040
+ return null;
2041
+ }
2042
+ function labelFromSessionTitle(title, model, sessionId) {
2043
+ const normalizedTitle = normalizeClaudeSessionTitle(title);
2044
+ return normalizedTitle ? shorten(normalizedTitle, 42) : labelFromModel(model, sessionId);
2045
+ }
2046
+ function labelFromModel(model, sessionId) {
2047
+ const normalized = normalizeClaudeDisplayModel(model);
2048
+ if (!normalized) {
2049
+ return `Claude ${sessionId.slice(0, 4)}`;
2050
+ }
2051
+ return `Claude ${shorten(normalized, 18)}`;
2052
+ }
2053
+ function extractProjectRoot(records) {
2054
+ for (const record of records) {
2055
+ const root = canonicalizeProjectPath(typeof record.cwd === "string" ? record.cwd : null);
2056
+ if (root) {
2057
+ return root;
2058
+ }
2059
+ }
2060
+ return null;
2061
+ }
2062
+ async function scanClaudeProjectDirs() {
2063
+ const projects = [];
2064
+ for (const projectsDir of claudeProjectsDirs()) {
2065
+ const projectEntries = await (0, promises_1.readdir)(projectsDir, { withFileTypes: true }).catch(() => []);
2066
+ for (const entry of projectEntries) {
2067
+ if (!entry.isDirectory()) {
2068
+ continue;
2069
+ }
2070
+ const dirPath = (0, node_path_1.join)(projectsDir, entry.name);
2071
+ const fileEntries = await (0, promises_1.readdir)(dirPath, { withFileTypes: true }).catch(() => []);
2072
+ const files = await Promise.all(fileEntries
2073
+ .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
2074
+ .map(async (file) => {
2075
+ const path = (0, node_path_1.join)(dirPath, file.name);
2076
+ const sample = await readLogSample(path).catch(() => null);
2077
+ if (!sample) {
2078
+ return null;
2079
+ }
2080
+ return {
2081
+ path,
2082
+ updatedAt: sample.mtimeMs,
2083
+ root: extractProjectRoot([...sample.headRecords, ...sample.tailRecords])
2084
+ };
2085
+ }));
2086
+ const validFiles = files.filter((file) => Boolean(file));
2087
+ if (validFiles.length === 0) {
2088
+ continue;
2089
+ }
2090
+ const newestFile = [...validFiles].sort((left, right) => right.updatedAt - left.updatedAt)[0];
2091
+ if (!newestFile.root) {
2092
+ continue;
2093
+ }
2094
+ projects.push({
2095
+ root: newestFile.root,
2096
+ dirPath,
2097
+ updatedAt: newestFile.updatedAt,
2098
+ count: validFiles.length,
2099
+ files: validFiles
2100
+ .map((file) => ({ path: file.path, updatedAt: file.updatedAt }))
2101
+ .sort((left, right) => right.updatedAt - left.updatedAt)
2102
+ });
2103
+ }
2104
+ }
2105
+ return projects.sort((left, right) => right.updatedAt - left.updatedAt);
2106
+ }
2107
+ async function discoverClaudeProjectsViaSdk(limit = 50) {
2108
+ const sessions = await (0, claude_agent_sdk_1.listClaudeSdkSessions)({ limit });
2109
+ if (!sessions || sessions.length === 0) {
2110
+ return [];
2111
+ }
2112
+ const grouped = new Map();
2113
+ for (const session of sessions) {
2114
+ const root = canonicalizeProjectPath(session.cwd);
2115
+ if (!root) {
2116
+ continue;
2117
+ }
2118
+ const existing = grouped.get(root);
2119
+ if (existing) {
2120
+ existing.count += 1;
2121
+ existing.updatedAt = Math.max(existing.updatedAt, session.lastModified);
2122
+ continue;
2123
+ }
2124
+ grouped.set(root, {
2125
+ root,
2126
+ updatedAt: session.lastModified,
2127
+ count: 1,
2128
+ sourceKind: "claude:sdk",
2129
+ sourceKinds: ["claude:sdk"]
2130
+ });
2131
+ }
2132
+ return [...grouped.values()]
2133
+ .sort((left, right) => right.updatedAt - left.updatedAt)
2134
+ .slice(0, limit);
2135
+ }
2136
+ async function loadClaudeSessionsViaSdk(projectRoot, limit = 12) {
2137
+ const sessions = await (0, claude_agent_sdk_1.listClaudeSdkSessions)({
2138
+ dir: projectRoot,
2139
+ limit,
2140
+ includeWorktrees: true
2141
+ });
2142
+ if (!sessions || sessions.length === 0) {
2143
+ return null;
2144
+ }
2145
+ const projectDirPath = await scanClaudeProjectDirs()
2146
+ .then((projects) => projects.find((project) => (0, project_paths_1.sameProjectPath)(project.root, projectRoot))?.dirPath ?? null)
2147
+ .catch(() => null);
2148
+ const entries = await Promise.all(sessions.map(async (session) => {
2149
+ const cwd = canonicalizeProjectPath(session.cwd) ?? projectRoot;
2150
+ const records = await (0, claude_agent_sdk_1.getClaudeSdkSessionRecords)({
2151
+ sessionId: session.sessionId,
2152
+ dir: projectRoot,
2153
+ cwd,
2154
+ gitBranch: session.gitBranch,
2155
+ limit: 200
2156
+ });
2157
+ if (!records || records.length === 0) {
2158
+ return null;
2159
+ }
2160
+ return {
2161
+ sessionId: session.sessionId,
2162
+ title: normalizeClaudeSessionTitle(session.customTitle ?? session.summary ?? session.firstPrompt ?? null),
2163
+ projectDirPath,
2164
+ updatedAt: session.lastModified,
2165
+ cwd,
2166
+ gitBranch: session.gitBranch ?? null,
2167
+ records
2168
+ };
2169
+ }));
2170
+ return entries.filter((entry) => Boolean(entry));
2171
+ }
2172
+ async function collectClaudeLoadedSessions(projectRoot, limit = 12) {
2173
+ const sdkSessions = await loadClaudeSessionsViaSdk(projectRoot, limit);
2174
+ if (sdkSessions && sdkSessions.length > 0) {
2175
+ return Promise.all(sdkSessions.map(async (session) => {
2176
+ const hookSample = await (0, claude_agent_sdk_1.resolveReadableClaudeHooksFilePath)(projectRoot, session.sessionId)
2177
+ .then((filePath) => readLogSample(filePath))
2178
+ .catch(() => null);
2179
+ const hookRecords = hookSample ? [...hookSample.headRecords, ...hookSample.tailRecords] : [];
2180
+ const updatedAt = Math.max(session.updatedAt, hookSample?.mtimeMs ?? 0);
2181
+ const summary = summariseClaudeSession(session.sessionId, session.cwd, session.records, updatedAt, hookRecords, session.title);
2182
+ return {
2183
+ sessionId: session.sessionId,
2184
+ title: session.title,
2185
+ projectDirPath: session.projectDirPath,
2186
+ cwd: session.cwd,
2187
+ gitBranch: session.gitBranch,
2188
+ updatedAt,
2189
+ records: session.records,
2190
+ hookRecords,
2191
+ summary
2192
+ };
2193
+ }));
2194
+ }
2195
+ const projects = await scanClaudeProjectDirs();
2196
+ const project = projects.find((entry) => entry.root === projectRoot);
2197
+ if (!project) {
2198
+ return [];
2199
+ }
2200
+ const sessions = await Promise.all(project.files.slice(0, limit).map(async (file) => {
2201
+ const sample = await readLogSample(file.path).catch(() => null);
2202
+ if (!sample) {
2203
+ return null;
2204
+ }
2205
+ const records = [...sample.headRecords, ...sample.tailRecords];
2206
+ const cwd = extractProjectRoot(records) ?? projectRoot;
2207
+ const sessionId = file.path.match(/([0-9a-f-]{36})\.jsonl$/i)?.[1] ?? file.path;
2208
+ const hookSample = await (0, claude_agent_sdk_1.resolveReadableClaudeHooksFilePath)(projectRoot, sessionId)
2209
+ .then((filePath) => readLogSample(filePath))
2210
+ .catch(() => null);
2211
+ const hookRecords = hookSample ? [...hookSample.headRecords, ...hookSample.tailRecords] : [];
2212
+ const updatedAt = Math.max(file.updatedAt, hookSample?.mtimeMs ?? 0);
2213
+ const summary = summariseClaudeSession(sessionId, cwd, records, updatedAt, hookRecords);
2214
+ return {
2215
+ sessionId,
2216
+ title: extractClaudeSessionTitle(records),
2217
+ projectDirPath: project.dirPath,
2218
+ cwd,
2219
+ gitBranch: summary.gitBranch,
2220
+ updatedAt,
2221
+ records,
2222
+ hookRecords,
2223
+ summary
2224
+ };
2225
+ }));
2226
+ return sessions.filter((entry) => Boolean(entry));
2227
+ }
2228
+ function mergePathLists(...pathLists) {
2229
+ return Array.from(new Set(pathLists.flat().filter(Boolean)));
2230
+ }
2231
+ function maxTimestampMs(...timestamps) {
2232
+ return Math.max(0, ...timestamps
2233
+ .map((timestamp) => {
2234
+ if (typeof timestamp === "number") {
2235
+ return timestamp;
2236
+ }
2237
+ if (typeof timestamp === "string") {
2238
+ const parsed = Date.parse(timestamp);
2239
+ return Number.isFinite(parsed) ? parsed : 0;
2240
+ }
2241
+ return 0;
2242
+ }));
2243
+ }
2244
+ function maxIsoTimestamp(...timestamps) {
2245
+ const ms = maxTimestampMs(...timestamps);
2246
+ return new Date(ms || Date.now()).toISOString();
2247
+ }
2248
+ function isRecentClaudeEvidence(timestampMs, windowMs = RECENT_DONE_WINDOW_MS, now = Date.now()) {
2249
+ return Number.isFinite(timestampMs) && timestampMs > 0 && timestampMs >= now - windowMs;
2250
+ }
2251
+ function latestFreshClaudeHookEvidence(input) {
2252
+ return input.records.some((record) => isRecentClaudeEvidence(recordTimestampMs(record, input.fallbackUpdatedAt)));
2253
+ }
2254
+ function claudeTeamMemberBaseUpdatedAt(context) {
2255
+ return maxTimestampMs(context.team.updatedAt, context.member.joinedAt);
2256
+ }
2257
+ function hasFreshClaudeTeamActivity(input) {
2258
+ if (!input.context.member.isActive) {
2259
+ return false;
2260
+ }
2261
+ if (isRecentClaudeEvidence(claudeTeamMemberBaseUpdatedAt(input.context))) {
2262
+ return true;
2263
+ }
2264
+ if (input.latestHook && isRecentClaudeEvidence(input.latestHook.updatedAtMs)) {
2265
+ return true;
2266
+ }
2267
+ if (input.fallbackHookRecords && typeof input.fallbackUpdatedAt === "number") {
2268
+ return latestFreshClaudeHookEvidence({
2269
+ records: input.fallbackHookRecords,
2270
+ fallbackUpdatedAt: input.fallbackUpdatedAt
2271
+ });
2272
+ }
2273
+ return false;
2274
+ }
2275
+ function claudeAgentFromLoadedSession(session, appearance, teamContext = null) {
2276
+ const parentThreadId = teamContext ? claudeTeamParentAgentId(teamContext) : null;
2277
+ const teamCwd = teamContext ? claudeTeamMemberPrimaryCwd(teamContext.member) : null;
2278
+ const updatedAt = teamContext
2279
+ ? maxIsoTimestamp(session.summary.updatedAt, teamContext.team.updatedAt, teamContext.member.joinedAt)
2280
+ : session.summary.updatedAt;
2281
+ const memberIsFreshActive = teamContext
2282
+ ? hasFreshClaudeTeamActivity({
2283
+ context: teamContext,
2284
+ fallbackHookRecords: session.hookRecords.filter((record) => !isClaudeChildHookRecord(record)),
2285
+ fallbackUpdatedAt: session.updatedAt
2286
+ })
2287
+ : false;
2288
+ const state = memberIsFreshActive && (session.summary.state === "idle" || session.summary.state === "done")
2289
+ ? "running"
2290
+ : session.summary.state;
2291
+ const isOngoing = session.summary.isOngoing || memberIsFreshActive;
2292
+ const detail = memberIsFreshActive && (session.summary.state === "idle" || session.summary.state === "done")
2293
+ ? `${teamContext?.member.name ?? "Teammate"} active in ${teamContext?.team.name ?? "Claude team"}`
2294
+ : session.summary.detail;
2295
+ const paths = teamContext
2296
+ ? mergePathLists([teamCwd ?? session.cwd, teamContext.member.cwd], session.summary.paths)
2297
+ : session.summary.paths;
2298
+ return {
2299
+ id: claudeLeadAgentId(session.sessionId),
2300
+ label: teamContext?.member.name ?? session.summary.label,
2301
+ source: "claude",
2302
+ sourceKind: teamContext ? `claude:team:${teamContext.team.name}` : session.summary.sourceKind,
2303
+ parentThreadId,
2304
+ depth: parentThreadId ? 1 : 0,
2305
+ isCurrent: false,
2306
+ isOngoing,
2307
+ statusText: teamContext ? (memberIsFreshActive ? "running" : "idle") : "claude",
2308
+ role: teamContext?.member.agentType ?? "claude",
2309
+ nickname: teamContext?.member.name ?? null,
2310
+ isSubagent: Boolean(teamContext),
2311
+ state,
2312
+ detail,
2313
+ cwd: teamCwd ?? session.cwd,
2314
+ roomId: null,
2315
+ appearance,
2316
+ updatedAt,
2317
+ stoppedAt: !isOngoing && parentThreadId ? updatedAt : null,
2318
+ paths,
2319
+ activityEvent: session.summary.activityEvent,
2320
+ goal: session.summary.goal ?? null,
2321
+ latestMessage: session.summary.latestMessage,
2322
+ threadId: session.sessionId,
2323
+ taskId: null,
2324
+ resumeCommand: null,
2325
+ url: null,
2326
+ git: {
2327
+ sha: null,
2328
+ branch: session.summary.gitBranch ?? session.gitBranch,
2329
+ originUrl: null
2330
+ },
2331
+ provenance: "claude",
2332
+ confidence: session.summary.confidence,
2333
+ needsUser: session.summary.needsUser,
2334
+ liveSubscription: "readOnly",
2335
+ network: null
2336
+ };
2337
+ }
2338
+ function mergeClaudeDashboardAgents(existing, incoming) {
2339
+ const existingUpdatedAt = Date.parse(existing.updatedAt);
2340
+ const incomingUpdatedAt = Date.parse(incoming.updatedAt);
2341
+ const incomingIsNewer = Number.isFinite(incomingUpdatedAt)
2342
+ && (!Number.isFinite(existingUpdatedAt) || incomingUpdatedAt >= existingUpdatedAt);
2343
+ const primary = incomingIsNewer ? incoming : existing;
2344
+ const secondary = incomingIsNewer ? existing : incoming;
2345
+ const parentThreadId = primary.parentThreadId ?? secondary.parentThreadId;
2346
+ const teamStyled = incoming.sourceKind.startsWith("claude:team") ? incoming : existing.sourceKind.startsWith("claude:team") ? existing : null;
2347
+ return {
2348
+ ...primary,
2349
+ label: teamStyled?.nickname ?? teamStyled?.label ?? primary.label,
2350
+ sourceKind: teamStyled?.sourceKind ?? primary.sourceKind,
2351
+ parentThreadId,
2352
+ depth: parentThreadId ? Math.max(1, primary.depth, secondary.depth) : primary.depth,
2353
+ isOngoing: primary.isOngoing || (secondary.isOngoing && !primary.stoppedAt),
2354
+ role: teamStyled?.role ?? primary.role ?? secondary.role,
2355
+ nickname: teamStyled?.nickname ?? primary.nickname ?? secondary.nickname,
2356
+ isSubagent: primary.isSubagent || secondary.isSubagent || Boolean(parentThreadId),
2357
+ cwd: primary.cwd ?? secondary.cwd,
2358
+ paths: mergePathLists(primary.paths, secondary.paths),
2359
+ stoppedAt: primary.isOngoing || secondary.isOngoing ? null : primary.stoppedAt ?? secondary.stoppedAt,
2360
+ threadId: primary.threadId ?? secondary.threadId,
2361
+ goal: primary.goal ?? secondary.goal ?? null,
2362
+ confidence: primary.confidence === "typed" || secondary.confidence === "typed" ? "typed" : primary.confidence
2363
+ };
2364
+ }
2365
+ function latestClaudeTeamHookSummary(input) {
2366
+ let latest = null;
2367
+ for (const session of input.sessions) {
2368
+ if (input.context.leadSessionId
2369
+ && session.sessionId !== input.context.leadSessionId
2370
+ && session.sessionId !== input.context.member.sessionId) {
2371
+ continue;
2372
+ }
2373
+ for (const record of session.hookRecords) {
2374
+ const agentId = claudeHookAgentId(record);
2375
+ const teamName = stringValue(record, "team_name", "teamName");
2376
+ const teammateName = stringValue(record, "teammate_name", "teammateName");
2377
+ const matchesAgent = agentId === input.context.member.agentId;
2378
+ const matchesTeammate = teamName === input.context.team.name && teammateName === input.context.member.name;
2379
+ if (!matchesAgent && !matchesTeammate) {
2380
+ continue;
2381
+ }
2382
+ const summary = summariseClaudeHookRecord({
2383
+ sessionId: input.context.member.sessionId ?? session.sessionId,
2384
+ model: input.context.member.model,
2385
+ fallbackCwd: claudeTeamMemberPrimaryCwd(input.context.member),
2386
+ gitBranch: session.gitBranch,
2387
+ record,
2388
+ fallbackUpdatedAt: session.updatedAt
2389
+ });
2390
+ if (!summary) {
2391
+ continue;
2392
+ }
2393
+ const updatedAtMs = recordTimestampMs(record, session.updatedAt);
2394
+ if (!latest || updatedAtMs >= latest.updatedAtMs) {
2395
+ latest = { summary: ageClaudeSummary(summary), updatedAtMs };
2396
+ }
2397
+ }
2398
+ }
2399
+ return latest;
2400
+ }
2401
+ function claudeTeamMemberSummary(input) {
2402
+ const primaryCwd = claudeTeamMemberPrimaryCwd(input.context.member);
2403
+ const baseUpdatedAt = Math.max(input.context.team.updatedAt, input.context.member.joinedAt ?? 0);
2404
+ const latestHook = latestClaudeTeamHookSummary(input);
2405
+ const memberIsFreshActive = hasFreshClaudeTeamActivity({
2406
+ context: input.context,
2407
+ latestHook
2408
+ });
2409
+ const baseState = memberIsFreshActive ? "running" : "idle";
2410
+ const baseUpdatedAtIso = new Date(baseUpdatedAt || Date.now()).toISOString();
2411
+ const base = {
2412
+ label: input.context.member.name,
2413
+ sourceKind: `claude:team:${input.context.team.name}`,
2414
+ state: baseState,
2415
+ detail: memberIsFreshActive
2416
+ ? `${input.context.member.name} active in ${input.context.team.name}`
2417
+ : `${input.context.member.name} idle in ${input.context.team.name}`,
2418
+ updatedAt: baseUpdatedAtIso,
2419
+ paths: mergePathLists([primaryCwd], [input.context.member.cwd]),
2420
+ activityEvent: null,
2421
+ gitBranch: null,
2422
+ confidence: "typed",
2423
+ needsUser: null,
2424
+ latestMessage: null,
2425
+ isOngoing: memberIsFreshActive,
2426
+ goal: (0, goal_1.inferredGoalFromText)({
2427
+ kind: "claudeSubagent",
2428
+ objective: input.context.member.prompt ?? input.context.member.name,
2429
+ state: baseState,
2430
+ updatedAt: baseUpdatedAtIso,
2431
+ createdAt: input.context.member.joinedAt ? new Date(input.context.member.joinedAt).toISOString() : null
2432
+ })
2433
+ };
2434
+ if (!latestHook) {
2435
+ return base;
2436
+ }
2437
+ const hookState = memberIsFreshActive && (latestHook.summary.state === "done" || latestHook.summary.state === "idle")
2438
+ ? "running"
2439
+ : latestHook.summary.state;
2440
+ return {
2441
+ ...base,
2442
+ state: hookState,
2443
+ detail: latestHook.summary.detail,
2444
+ updatedAt: maxIsoTimestamp(base.updatedAt, latestHook.updatedAtMs),
2445
+ paths: mergePathLists(latestHook.summary.paths, base.paths),
2446
+ activityEvent: latestHook.summary.activityEvent,
2447
+ latestMessage: latestHook.summary.latestMessage,
2448
+ needsUser: latestHook.summary.needsUser,
2449
+ isOngoing: memberIsFreshActive || latestHook.summary.isOngoing
2450
+ };
2451
+ }
2452
+ async function claudeAgentFromTeamMemberContext(input) {
2453
+ const summary = claudeTeamMemberSummary({
2454
+ context: input.context,
2455
+ sessions: input.sessions
2456
+ });
2457
+ const id = claudeTeamAgentId(input.context);
2458
+ const parentThreadId = claudeTeamParentAgentId(input.context);
2459
+ const threadId = input.context.member.sessionId ?? id;
2460
+ const appearance = await (0, appearance_1.ensureAgentAppearance)(input.projectRoot, id);
2461
+ return {
2462
+ id,
2463
+ label: input.context.member.name,
2464
+ source: "claude",
2465
+ sourceKind: `claude:team:${input.context.team.name}`,
2466
+ parentThreadId,
2467
+ depth: parentThreadId ? 1 : 0,
2468
+ isCurrent: false,
2469
+ isOngoing: summary.isOngoing,
2470
+ statusText: summary.isOngoing ? "running" : "idle",
2471
+ role: input.context.member.agentType ?? "teammate",
2472
+ nickname: input.context.member.name,
2473
+ isSubagent: true,
2474
+ state: summary.state,
2475
+ detail: summary.detail,
2476
+ cwd: claudeTeamMemberPrimaryCwd(input.context.member),
2477
+ roomId: null,
2478
+ appearance,
2479
+ updatedAt: summary.updatedAt,
2480
+ stoppedAt: summary.isOngoing ? null : summary.updatedAt,
2481
+ paths: summary.paths,
2482
+ activityEvent: summary.activityEvent,
2483
+ goal: summary.goal ?? null,
2484
+ latestMessage: summary.latestMessage,
2485
+ threadId,
2486
+ taskId: null,
2487
+ resumeCommand: null,
2488
+ url: null,
2489
+ git: null,
2490
+ provenance: "claude",
2491
+ confidence: "typed",
2492
+ needsUser: summary.needsUser,
2493
+ liveSubscription: "readOnly",
2494
+ network: null
2495
+ };
2496
+ }
2497
+ function claudeSubagentIdFromTranscriptPath(filePath) {
2498
+ const fileName = (0, node_path_1.basename)(filePath);
2499
+ const match = fileName.match(/^agent-(.+)\.jsonl$/i);
2500
+ if (match) {
2501
+ return match[1].trim() || null;
2502
+ }
2503
+ return fileName.endsWith(".jsonl") ? fileName.slice(0, -".jsonl".length).trim() || null : null;
2504
+ }
2505
+ function claudeWorkflowSubagentRecordSources(record) {
2506
+ return [
2507
+ record,
2508
+ asRecord(record.agent),
2509
+ asRecord(record.subagent),
2510
+ asRecord(record.sub_agent),
2511
+ asRecord(record.metadata),
2512
+ asRecord(record.meta)
2513
+ ].filter((entry) => Boolean(entry));
2514
+ }
2515
+ function claudeWorkflowSubagentAgentIdFromRecord(record) {
2516
+ const sources = claudeWorkflowSubagentRecordSources(record);
2517
+ const explicit = firstStringValue(sources, "agentId", "agent_id", "subagentId", "subagent_id", "id");
2518
+ if (explicit) {
2519
+ return explicit;
2520
+ }
2521
+ const transcriptPath = firstStringValue(sources, "agentTranscriptPath", "agent_transcript_path", "transcriptPath", "transcript_path");
2522
+ return transcriptPath ? claudeSubagentIdFromTranscriptPath(transcriptPath) : null;
2523
+ }
2524
+ function claudeWorkflowIdFromPath(subagentsDir, filePath) {
2525
+ const parts = (0, node_path_1.relative)(subagentsDir, filePath).replace(/\\/g, "/").split("/");
2526
+ const workflowIndex = parts.findIndex((part) => part === "workflows");
2527
+ return workflowIndex >= 0 && parts[workflowIndex + 1] ? parts[workflowIndex + 1] : null;
2528
+ }
2529
+ function claudeWorkflowSubagentSeedKey(agentId, workflowId) {
2530
+ return `${workflowId ?? ""}\u0000${agentId}`;
2531
+ }
2532
+ function claudeWorkflowSubagentChildAgentId(seed, duplicatedAgentIds) {
2533
+ return seed.workflowId && duplicatedAgentIds.has(seed.agentId)
2534
+ ? `workflow:${seed.workflowId}:agent:${seed.agentId}`
2535
+ : seed.agentId;
2536
+ }
2537
+ async function listClaudeSubagentJsonlFiles(rootDir, limit = 200) {
2538
+ const files = [];
2539
+ async function walk(dir) {
2540
+ if (files.length >= limit) {
2541
+ return;
2542
+ }
2543
+ const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true }).catch(() => []);
2544
+ for (const entry of entries) {
2545
+ if (files.length >= limit) {
2546
+ break;
2547
+ }
2548
+ const path = (0, node_path_1.join)(dir, entry.name);
2549
+ if (entry.isDirectory()) {
2550
+ await walk(path);
2551
+ }
2552
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
2553
+ files.push(path);
2554
+ }
2555
+ }
2556
+ }
2557
+ await walk(rootDir);
2558
+ return files;
2559
+ }
2560
+ async function readClaudeWorkflowSubagentMeta(transcriptPath) {
2561
+ const agentIdFromPath = claudeSubagentIdFromTranscriptPath(transcriptPath);
2562
+ const baseName = (0, node_path_1.basename)(transcriptPath, ".jsonl");
2563
+ const candidates = Array.from(new Set([
2564
+ (0, node_path_1.join)((0, node_path_1.dirname)(transcriptPath), `${baseName}.meta.json`),
2565
+ agentIdFromPath ? (0, node_path_1.join)((0, node_path_1.dirname)(transcriptPath), `agent-${agentIdFromPath}.meta.json`) : null,
2566
+ agentIdFromPath ? (0, node_path_1.join)((0, node_path_1.dirname)(transcriptPath), `${agentIdFromPath}.meta.json`) : null
2567
+ ].filter((entry) => Boolean(entry))));
2568
+ for (const candidate of candidates) {
2569
+ const content = await (0, promises_1.readFile)(candidate, "utf8").catch(() => null);
2570
+ if (!content) {
2571
+ continue;
2572
+ }
2573
+ const record = (() => {
2574
+ try {
2575
+ return asRecord(JSON.parse(content));
2576
+ }
2577
+ catch {
2578
+ return null;
2579
+ }
2580
+ })();
2581
+ if (!record) {
2582
+ continue;
2583
+ }
2584
+ const sources = claudeWorkflowSubagentRecordSources(record);
2585
+ const agentId = firstStringValue(sources, "agentId", "agent_id", "subagentId", "subagent_id", "id") ?? agentIdFromPath;
2586
+ if (!agentId) {
2587
+ continue;
2588
+ }
2589
+ return {
2590
+ agentId,
2591
+ agentType: firstStringValue(sources, "agentType", "agent_type", "type", "role"),
2592
+ name: firstStringValue(sources, "name", "label", "title"),
2593
+ description: firstStringValue(sources, "description", "summary", "prompt"),
2594
+ cwd: canonicalizeProjectPath(firstStringValue(sources, "cwd", "workingDirectory", "working_directory"))
2595
+ };
2596
+ }
2597
+ return agentIdFromPath
2598
+ ? { agentId: agentIdFromPath, agentType: null, name: null, description: null, cwd: null }
2599
+ : null;
2600
+ }
2601
+ async function directoryExists(dirPath) {
2602
+ return (0, promises_1.stat)(dirPath)
2603
+ .then((stats) => stats.isDirectory())
2604
+ .catch(() => false);
2605
+ }
2606
+ function latestClaudeTimestamp(records, fallbackUpdatedAt) {
2607
+ return records.reduce((latest, record) => {
2608
+ const timestamp = explicitRecordTimestampMs(record);
2609
+ return Number.isFinite(timestamp) ? Math.max(latest, timestamp) : latest;
2610
+ }, fallbackUpdatedAt);
2611
+ }
2612
+ function latestClaudeAssistantText(records) {
2613
+ for (const record of [...records].reverse()) {
2614
+ const text = extractAssistantText(record);
2615
+ if (text) {
2616
+ return text;
2617
+ }
2618
+ }
2619
+ return null;
2620
+ }
2621
+ function claudeWorkflowResultText(record) {
2622
+ return firstStringValue(claudeWorkflowSubagentRecordSources(record), "last_assistant_message", "result", "summary", "message", "output", "text", "content");
2623
+ }
2624
+ function claudeWorkflowRecordKind(record) {
2625
+ return firstStringValue(claudeWorkflowSubagentRecordSources(record), "event", "type", "status", "phase", "kind");
2626
+ }
2627
+ function isClaudeWorkflowResultRecord(record) {
2628
+ const kind = claudeWorkflowRecordKind(record)?.replace(/[_-]+/g, " ");
2629
+ return Boolean(kind && /\b(result|done|complete|completed|finished|stop|stopped|success|failed|error)\b/i.test(kind));
2630
+ }
2631
+ function isClaudeWorkflowStartRecord(record) {
2632
+ const kind = claudeWorkflowRecordKind(record)?.replace(/[_-]+/g, " ");
2633
+ return Boolean(kind && /\b(start|started|spawn|spawned|created|queued|running|active)\b/i.test(kind));
2634
+ }
2635
+ function claudeWorkflowStateFromRecord(record, fallback) {
2636
+ const text = [claudeWorkflowRecordKind(record), claudeWorkflowResultText(record)]
2637
+ .filter(Boolean)
2638
+ .join(" ")
2639
+ .replace(/[_-]+/g, " ")
2640
+ .toLowerCase();
2641
+ if (/\b(error|failed|failure|blocked|denied)\b/.test(text) || record.is_error === true) {
2642
+ return "blocked";
2643
+ }
2644
+ if (/\b(result|done|complete|completed|finished|stop|stopped|success)\b/.test(text)) {
2645
+ return "done";
2646
+ }
2647
+ if (/\b(start|started|spawn|spawned|created|queued|running|active)\b/.test(text)) {
2648
+ return "running";
2649
+ }
2650
+ return fallback;
2651
+ }
2652
+ function claudeWorkflowSubagentSummary(input) {
2653
+ const latestRecord = input.records.at(-1) ?? null;
2654
+ const latestAssistantText = latestClaudeAssistantText(input.records);
2655
+ const resultRecord = [...input.records].reverse().find(isClaudeWorkflowResultRecord) ?? null;
2656
+ const resultText = resultRecord ? claudeWorkflowResultText(resultRecord) : null;
2657
+ const ageMs = Date.now() - input.updatedAtMs;
2658
+ const state = resultRecord ? claudeWorkflowStateFromRecord(resultRecord, "done")
2659
+ : latestAssistantText ? "done"
2660
+ : ageMs <= RECENT_DONE_WINDOW_MS ? "running"
2661
+ : "idle";
2662
+ const detail = resultText ?? latestAssistantText ?? (latestRecord ? claudeWorkflowResultText(latestRecord) : null) ?? input.fallbackDetail;
2663
+ const paths = extractPathsFromText(detail);
2664
+ const sourceKind = input.workflowId
2665
+ ? "claude:workflow-subagent"
2666
+ : input.agentType ? `claude:subagent:${input.agentType}` : "claude:subagent";
2667
+ return {
2668
+ label: labelFromModel(null, input.agentId),
2669
+ sourceKind,
2670
+ state,
2671
+ detail: state === "idle" ? "Idle" : shorten(detail, 88),
2672
+ updatedAt: new Date(input.updatedAtMs).toISOString(),
2673
+ paths: paths.length > 0 ? paths : [input.cwd],
2674
+ activityEvent: state === "running"
2675
+ ? claudeCollabActivityEvent({ detail: input.workflowId ? "Workflow subagent running" : "Subagent running", path: input.cwd })
2676
+ : null,
2677
+ gitBranch: null,
2678
+ confidence: "inferred",
2679
+ needsUser: null,
2680
+ latestMessage: state === "idle" ? null : latestAssistantText ?? resultText,
2681
+ isOngoing: state !== "done" && state !== "idle" && state !== "blocked"
2682
+ };
2683
+ }
2684
+ function claudeWorkflowJournalEntryFromRecord(record, fallbackUpdatedAt, workflowId) {
2685
+ const agentId = claudeWorkflowSubagentAgentIdFromRecord(record);
2686
+ if (!agentId || (!isClaudeWorkflowStartRecord(record) && !isClaudeWorkflowResultRecord(record))) {
2687
+ return null;
2688
+ }
2689
+ const sources = claudeWorkflowSubagentRecordSources(record);
2690
+ const updatedAtMs = recordTimestampMs(record, fallbackUpdatedAt);
2691
+ const detail = claudeWorkflowResultText(record)
2692
+ ?? firstStringValue(sources, "prompt", "description", "task", "name", "agentType", "agent_type")
2693
+ ?? "Workflow subagent updated";
2694
+ const state = claudeWorkflowStateFromRecord(record, isClaudeWorkflowResultRecord(record) ? "done" : "running");
2695
+ return {
2696
+ agentId,
2697
+ agentType: firstStringValue(sources, "agentType", "agent_type", "type", "role"),
2698
+ name: firstStringValue(sources, "name", "label", "title"),
2699
+ description: firstStringValue(sources, "description", "summary", "prompt", "task"),
2700
+ workflowId: firstStringValue(sources, "workflowId", "workflow_id") ?? workflowId,
2701
+ state,
2702
+ detail: shorten(detail, 88),
2703
+ updatedAtMs,
2704
+ latestMessage: state === "done" || state === "blocked" ? detail : null,
2705
+ cwd: canonicalizeProjectPath(firstStringValue(sources, "cwd", "workingDirectory", "working_directory"))
2706
+ };
2707
+ }
2708
+ async function readClaudeWorkflowJournalEntries(subagentsDir) {
2709
+ const files = await listClaudeSubagentJsonlFiles((0, node_path_1.join)(subagentsDir, "workflows"), 80).catch(() => []);
2710
+ const entries = [];
2711
+ for (const journalPath of files.filter((file) => (0, node_path_1.basename)(file) === "journal.jsonl")) {
2712
+ const sample = await readLogSample(journalPath).catch(() => null);
2713
+ if (!sample) {
2714
+ continue;
2715
+ }
2716
+ const workflowId = claudeWorkflowIdFromPath(subagentsDir, journalPath);
2717
+ for (const record of [...sample.headRecords, ...sample.tailRecords]) {
2718
+ const entry = claudeWorkflowJournalEntryFromRecord(record, sample.mtimeMs, workflowId);
2719
+ if (entry) {
2720
+ entries.push(entry);
2721
+ }
2722
+ }
2723
+ }
2724
+ return entries;
2725
+ }
2726
+ async function claudeSessionSubagentsDirs(session) {
2727
+ const candidates = new Set();
2728
+ if (session.projectDirPath) {
2729
+ candidates.add((0, node_path_1.join)(session.projectDirPath, session.sessionId, "subagents"));
2730
+ }
2731
+ const projectDirs = await scanClaudeProjectDirs().catch(() => []);
2732
+ for (const project of projectDirs) {
2733
+ candidates.add((0, node_path_1.join)(project.dirPath, session.sessionId, "subagents"));
2734
+ }
2735
+ const existing = await Promise.all([...candidates].map(async (candidate) => ((await directoryExists(candidate)) ? candidate : null)));
2736
+ return existing.filter((candidate) => Boolean(candidate));
2737
+ }
2738
+ async function readClaudeWorkflowSubagentSeeds(session) {
2739
+ const subagentsDirs = await claudeSessionSubagentsDirs(session);
2740
+ if (subagentsDirs.length === 0) {
2741
+ return [];
2742
+ }
2743
+ const scanResults = await Promise.all(subagentsDirs.map(async (subagentsDir) => ({
2744
+ subagentsDir,
2745
+ jsonlFiles: await listClaudeSubagentJsonlFiles(subagentsDir).catch(() => []),
2746
+ journalEntries: await readClaudeWorkflowJournalEntries(subagentsDir)
2747
+ })));
2748
+ const journalEntries = scanResults.flatMap((result) => result.journalEntries);
2749
+ const latestJournalBySeedKey = new Map();
2750
+ for (const entry of journalEntries) {
2751
+ const seedKey = claudeWorkflowSubagentSeedKey(entry.agentId, entry.workflowId);
2752
+ const existing = latestJournalBySeedKey.get(seedKey);
2753
+ if (!existing || entry.updatedAtMs >= existing.updatedAtMs) {
2754
+ latestJournalBySeedKey.set(seedKey, entry);
2755
+ }
2756
+ }
2757
+ const seedsByKey = new Map();
2758
+ for (const result of scanResults) {
2759
+ for (const transcriptPath of result.jsonlFiles.filter((file) => (0, node_path_1.basename)(file) !== "journal.jsonl")) {
2760
+ const [sample, fileStats, meta] = await Promise.all([
2761
+ readLogSample(transcriptPath).catch(() => null),
2762
+ (0, promises_1.stat)(transcriptPath).catch(() => null),
2763
+ readClaudeWorkflowSubagentMeta(transcriptPath)
2764
+ ]);
2765
+ const agentId = meta?.agentId ?? claudeSubagentIdFromTranscriptPath(transcriptPath);
2766
+ if (!agentId || !sample) {
2767
+ continue;
2768
+ }
2769
+ const records = [...sample.headRecords, ...sample.tailRecords];
2770
+ const workflowId = claudeWorkflowIdFromPath(result.subagentsDir, transcriptPath);
2771
+ const seedKey = claudeWorkflowSubagentSeedKey(agentId, workflowId);
2772
+ const journal = latestJournalBySeedKey.get(seedKey) ?? null;
2773
+ const cwd = journal?.cwd ?? meta?.cwd ?? session.cwd;
2774
+ const agentType = journal?.agentType ?? meta?.agentType ?? null;
2775
+ const name = journal?.name ?? meta?.name ?? null;
2776
+ const description = journal?.description ?? meta?.description ?? null;
2777
+ const updatedAtMs = Math.max(latestClaudeTimestamp(records, sample.mtimeMs), fileStats?.mtimeMs ?? 0, journal?.updatedAtMs ?? 0);
2778
+ const existing = seedsByKey.get(seedKey);
2779
+ if (existing && existing.updatedAtMs > updatedAtMs) {
2780
+ continue;
2781
+ }
2782
+ const summary = claudeWorkflowSubagentSummary({
2783
+ agentId,
2784
+ agentType,
2785
+ workflowId: journal?.workflowId ?? workflowId,
2786
+ records,
2787
+ cwd,
2788
+ updatedAtMs,
2789
+ fallbackDetail: journal?.detail ?? description ?? name ?? agentType ?? "Claude subagent"
2790
+ });
2791
+ const journalIsNewer = journal && journal.updatedAtMs >= Date.parse(summary.updatedAt);
2792
+ seedsByKey.set(seedKey, {
2793
+ agentId,
2794
+ agentType,
2795
+ name,
2796
+ description,
2797
+ workflowId: journal?.workflowId ?? workflowId,
2798
+ cwd,
2799
+ summary: journalIsNewer
2800
+ ? {
2801
+ ...summary,
2802
+ state: journal.state,
2803
+ detail: journal.state === "idle" ? "Idle" : journal.detail,
2804
+ updatedAt: new Date(journal.updatedAtMs).toISOString(),
2805
+ activityEvent: journal.state === "running" ? claudeCollabActivityEvent({ detail: journal.detail, path: cwd }) : null,
2806
+ latestMessage: journal.latestMessage ?? summary.latestMessage,
2807
+ isOngoing: journal.state !== "done" && journal.state !== "idle" && journal.state !== "blocked"
2808
+ }
2809
+ : summary,
2810
+ updatedAtMs
2811
+ });
2812
+ }
2813
+ }
2814
+ for (const journal of latestJournalBySeedKey.values()) {
2815
+ const seedKey = claudeWorkflowSubagentSeedKey(journal.agentId, journal.workflowId);
2816
+ if (seedsByKey.has(seedKey)) {
2817
+ continue;
2818
+ }
2819
+ const cwd = journal.cwd ?? session.cwd;
2820
+ const summary = {
2821
+ label: labelFromModel(null, journal.agentId),
2822
+ sourceKind: "claude:workflow-subagent",
2823
+ state: journal.state,
2824
+ detail: journal.state === "idle" ? "Idle" : journal.detail,
2825
+ updatedAt: new Date(journal.updatedAtMs).toISOString(),
2826
+ paths: [cwd],
2827
+ activityEvent: journal.state === "running" ? claudeCollabActivityEvent({ detail: journal.detail, path: cwd }) : null,
2828
+ gitBranch: null,
2829
+ confidence: "inferred",
2830
+ needsUser: null,
2831
+ latestMessage: journal.latestMessage,
2832
+ isOngoing: journal.state !== "done" && journal.state !== "idle" && journal.state !== "blocked"
2833
+ };
2834
+ seedsByKey.set(seedKey, {
2835
+ agentId: journal.agentId,
2836
+ agentType: journal.agentType,
2837
+ name: journal.name,
2838
+ description: journal.description,
2839
+ workflowId: journal.workflowId,
2840
+ cwd,
2841
+ summary,
2842
+ updatedAtMs: journal.updatedAtMs
2843
+ });
2844
+ }
2845
+ const workflowKeysByAgentId = new Map();
2846
+ for (const [seedKey, seed] of seedsByKey.entries()) {
2847
+ const keys = workflowKeysByAgentId.get(seed.agentId) ?? new Set();
2848
+ keys.add(seedKey);
2849
+ workflowKeysByAgentId.set(seed.agentId, keys);
2850
+ }
2851
+ const duplicatedAgentIds = new Set([...workflowKeysByAgentId.entries()]
2852
+ .filter(([, keys]) => keys.size > 1)
2853
+ .map(([agentId]) => agentId));
2854
+ return [...seedsByKey.values()]
2855
+ .map((seed) => ({
2856
+ ...seed,
2857
+ childAgentId: claudeWorkflowSubagentChildAgentId(seed, duplicatedAgentIds)
2858
+ }))
2859
+ .sort((left, right) => right.updatedAtMs - left.updatedAtMs);
2860
+ }
2861
+ async function buildClaudeSubagentAgents(input) {
2862
+ const latestById = new Map();
2863
+ for (const seed of await readClaudeWorkflowSubagentSeeds(input.session)) {
2864
+ const context = input.teamIndex.byLeadAndAgentId.get(claudeTeamMemberContextKey(input.session.sessionId, seed.agentId)) ?? null;
2865
+ const id = context ? claudeTeamAgentId(context) : claudeChildAgentId(input.session.sessionId, seed.childAgentId);
2866
+ latestById.set(id, {
2867
+ agentId: seed.agentId,
2868
+ agentType: seed.agentType ?? context?.member.agentType ?? null,
2869
+ name: seed.name,
2870
+ description: seed.description,
2871
+ workflowId: seed.workflowId,
2872
+ context,
2873
+ cwd: context ? claudeTeamMemberPrimaryCwd(context.member) : seed.cwd,
2874
+ summary: ageClaudeSummary(seed.summary),
2875
+ updatedAtMs: seed.updatedAtMs,
2876
+ confidence: "inferred"
2877
+ });
2878
+ }
2879
+ for (const record of input.session.hookRecords) {
2880
+ const agentId = claudeHookAgentId(record);
2881
+ if (!agentId) {
2882
+ continue;
2883
+ }
2884
+ const context = input.teamIndex.byLeadAndAgentId.get(claudeTeamMemberContextKey(input.session.sessionId, agentId)) ?? null;
2885
+ const summary = summariseClaudeHookRecord({
2886
+ sessionId: context?.member.sessionId ?? input.session.sessionId,
2887
+ model: context?.member.model ?? null,
2888
+ fallbackCwd: context ? claudeTeamMemberPrimaryCwd(context.member) : input.session.cwd,
2889
+ gitBranch: input.session.gitBranch,
2890
+ record,
2891
+ fallbackUpdatedAt: input.session.updatedAt
2892
+ });
2893
+ if (!summary) {
2894
+ continue;
2895
+ }
2896
+ const id = context ? claudeTeamAgentId(context) : claudeChildAgentId(input.session.sessionId, agentId);
2897
+ const updatedAtMs = recordTimestampMs(record, input.session.updatedAt);
2898
+ const existing = latestById.get(id);
2899
+ if (existing?.confidence === "typed" && existing.updatedAtMs > updatedAtMs) {
2900
+ continue;
2901
+ }
2902
+ latestById.set(id, {
2903
+ agentId,
2904
+ agentType: claudeHookAgentType(record) ?? existing?.agentType ?? context?.member.agentType ?? null,
2905
+ name: existing?.name ?? null,
2906
+ description: existing?.description ?? null,
2907
+ workflowId: existing?.workflowId ?? null,
2908
+ context,
2909
+ cwd: canonicalizeProjectPath(stringValue(record, "cwd"))
2910
+ ?? (context ? claudeTeamMemberPrimaryCwd(context.member) : input.session.cwd),
2911
+ summary: ageClaudeSummary(summary),
2912
+ updatedAtMs,
2913
+ confidence: "typed"
2914
+ });
2915
+ }
2916
+ return Promise.all([...latestById.entries()].map(async ([id, seed]) => {
2917
+ const parentThreadId = seed.context ? claudeTeamParentAgentId(seed.context) : claudeLeadAgentId(input.session.sessionId);
2918
+ const threadId = seed.context?.member.sessionId ?? id;
2919
+ const appearance = await (0, appearance_1.ensureAgentAppearance)(input.projectRoot, id);
2920
+ const label = seed.context?.member.name
2921
+ ?? seed.name
2922
+ ?? seed.description
2923
+ ?? (seed.agentType ? titleCaseIdentifier(seed.agentType) : `Claude ${seed.agentId.slice(0, 4)}`);
2924
+ const role = seed.context?.member.agentType ?? seed.agentType ?? "subagent";
2925
+ const sourceKind = seed.context ? `claude:team:${seed.context.team.name}`
2926
+ : seed.confidence === "typed" ? (seed.agentType ? `claude:subagent:${seed.agentType}` : "claude:subagent")
2927
+ : seed.workflowId ? "claude:workflow-subagent"
2928
+ : seed.agentType ? `claude:subagent:${seed.agentType}`
2929
+ : "claude:subagent";
2930
+ return {
2931
+ id,
2932
+ label,
2933
+ source: "claude",
2934
+ sourceKind,
2935
+ parentThreadId,
2936
+ depth: parentThreadId ? 1 : 0,
2937
+ isCurrent: false,
2938
+ isOngoing: seed.summary.isOngoing,
2939
+ statusText: seed.summary.isOngoing ? "running" : seed.summary.state,
2940
+ role,
2941
+ nickname: seed.context?.member.name ?? seed.agentId,
2942
+ isSubagent: true,
2943
+ state: seed.summary.state,
2944
+ detail: seed.summary.detail,
2945
+ cwd: seed.cwd,
2946
+ roomId: null,
2947
+ appearance,
2948
+ updatedAt: seed.summary.updatedAt,
2949
+ stoppedAt: seed.summary.isOngoing ? null : seed.summary.updatedAt,
2950
+ paths: seed.summary.paths.length > 0 ? seed.summary.paths : [seed.cwd],
2951
+ activityEvent: seed.summary.activityEvent,
2952
+ goal: seed.summary.goal ?? (0, goal_1.inferredGoalFromText)({
2953
+ kind: "claudeSubagent",
2954
+ objective: seed.description ?? seed.name,
2955
+ state: seed.summary.state,
2956
+ updatedAt: seed.summary.updatedAt
2957
+ }),
2958
+ latestMessage: seed.summary.latestMessage,
2959
+ threadId,
2960
+ taskId: null,
2961
+ resumeCommand: null,
2962
+ url: null,
2963
+ git: {
2964
+ sha: null,
2965
+ branch: seed.summary.gitBranch ?? input.session.gitBranch,
2966
+ originUrl: null
2967
+ },
2968
+ provenance: "claude",
2969
+ confidence: seed.confidence,
2970
+ needsUser: seed.summary.needsUser,
2971
+ liveSubscription: "readOnly",
2972
+ network: null
2973
+ };
2974
+ }));
2975
+ }
2976
+ async function buildClaudeTeamAgentsForProject(input) {
2977
+ const contexts = new Map();
2978
+ for (const context of input.teamIndex.contexts) {
2979
+ const primaryCwd = claudeTeamMemberPrimaryCwd(context.member);
2980
+ if (!(0, project_paths_1.sameProjectPath)(primaryCwd, input.projectRoot)) {
2981
+ continue;
2982
+ }
2983
+ contexts.set(claudeTeamAgentId(context), context);
2984
+ }
2985
+ return Promise.all([...contexts.values()].map((context) => claudeAgentFromTeamMemberContext({
2986
+ projectRoot: input.projectRoot,
2987
+ context,
2988
+ sessions: input.sessions
2989
+ })));
2990
+ }
2991
+ function claudeCoworkState(session, now = Date.now()) {
2992
+ if (session.isArchived) {
2993
+ return "idle";
2994
+ }
2995
+ const ageMs = now - session.updatedAt;
2996
+ if (ageMs <= RECENT_CLAUDE_HOOK_ACTIVE_WINDOW_MS) {
2997
+ return "thinking";
2998
+ }
2999
+ if (ageMs <= RECENT_DONE_WINDOW_MS) {
3000
+ return "done";
3001
+ }
3002
+ return "idle";
3003
+ }
3004
+ function claudeCoworkDetail(session) {
3005
+ return shorten(session.title ?? session.initialMessage ?? "Claude Home work session", 88);
3006
+ }
3007
+ function claudeCoworkActivityEvent(session, projectRoot) {
3008
+ const latestPath = session.filePaths.find((path) => (0, project_paths_1.sameProjectPath)((0, node_path_1.dirname)(path), projectRoot)) ?? session.filePaths[0] ?? null;
3009
+ if (!latestPath) {
3010
+ return null;
3011
+ }
3012
+ return {
3013
+ type: "fileChange",
3014
+ action: "updated",
3015
+ path: latestPath,
3016
+ title: `updated ${latestPath}`,
3017
+ isImage: isImagePath(latestPath)
3018
+ };
3019
+ }
3020
+ async function claudeAgentFromCoworkSession(input) {
3021
+ const id = claudeCoworkAgentId(input.session.sessionId);
3022
+ const appearance = await (0, appearance_1.ensureAgentAppearance)(input.projectRoot, id);
3023
+ const state = claudeCoworkState(input.session);
3024
+ const isOngoing = state === "thinking";
3025
+ const paths = mergePathLists(input.session.roots, input.session.filePaths);
3026
+ const updatedAt = new Date(input.session.updatedAt).toISOString();
3027
+ return {
3028
+ id,
3029
+ label: input.session.title ?? "Claude Home",
3030
+ source: "claude",
3031
+ sourceKind: input.session.model ? `claude:cowork:${input.session.model}` : "claude:cowork",
3032
+ parentThreadId: null,
3033
+ depth: 0,
3034
+ isCurrent: false,
3035
+ isOngoing,
3036
+ statusText: "home",
3037
+ role: "home work",
3038
+ nickname: null,
3039
+ isSubagent: false,
3040
+ state,
3041
+ detail: claudeCoworkDetail(input.session),
3042
+ cwd: input.projectRoot,
3043
+ roomId: null,
3044
+ appearance,
3045
+ updatedAt,
3046
+ stoppedAt: isOngoing ? null : updatedAt,
3047
+ paths: paths.length > 0 ? paths : [input.projectRoot],
3048
+ activityEvent: claudeCoworkActivityEvent(input.session, input.projectRoot),
3049
+ goal: (0, goal_1.inferredGoalFromText)({
3050
+ kind: "claudeCowork",
3051
+ objective: input.session.title ?? input.session.initialMessage,
3052
+ state,
3053
+ updatedAt,
3054
+ createdAt: new Date(input.session.createdAt).toISOString()
3055
+ }),
3056
+ latestMessage: null,
3057
+ threadId: input.session.sessionId,
3058
+ taskId: null,
3059
+ resumeCommand: null,
3060
+ url: null,
3061
+ git: null,
3062
+ provenance: "claude",
3063
+ confidence: "typed",
3064
+ needsUser: null,
3065
+ liveSubscription: "readOnly",
3066
+ network: null
3067
+ };
3068
+ }
3069
+ async function buildClaudeCoworkAgentsForProject(input) {
3070
+ const matching = input.sessions
3071
+ .filter((session) => session.roots.some((root) => (0, project_paths_1.sameProjectPath)(root, input.projectRoot)))
3072
+ .sort((left, right) => right.updatedAt - left.updatedAt)
3073
+ .slice(0, input.limit ?? 8);
3074
+ return Promise.all(matching.map((session) => claudeAgentFromCoworkSession({
3075
+ projectRoot: input.projectRoot,
3076
+ session
3077
+ })));
3078
+ }
3079
+ async function claudeAgentFromBackgroundJob(input) {
3080
+ const id = claudeBackgroundJobAgentId(input.job);
3081
+ const appearance = await (0, appearance_1.ensureAgentAppearance)(input.projectRoot, id);
3082
+ const updatedAt = new Date(input.job.updatedAt).toISOString();
3083
+ const label = input.job.name ?? (input.job.sessionId ? labelFromModel(null, input.job.sessionId) : `Claude ${input.job.jobId.slice(0, 4)}`);
3084
+ const paths = mergePathLists([input.job.projectRoot, input.job.cwd], input.job.worktreePath ? [input.job.worktreePath] : []);
3085
+ return {
3086
+ id,
3087
+ label,
3088
+ source: "claude",
3089
+ sourceKind: "claude:background",
3090
+ parentThreadId: null,
3091
+ depth: 0,
3092
+ isCurrent: false,
3093
+ isOngoing: input.job.isOngoing,
3094
+ statusText: input.job.stateText ?? "background",
3095
+ role: "background",
3096
+ nickname: input.job.name,
3097
+ isSubagent: false,
3098
+ state: input.job.state,
3099
+ detail: input.job.detail,
3100
+ cwd: input.job.cwd,
3101
+ roomId: null,
3102
+ appearance,
3103
+ updatedAt,
3104
+ stoppedAt: input.job.isOngoing ? null : updatedAt,
3105
+ paths,
3106
+ activityEvent: {
3107
+ type: input.job.state === "waiting" || input.job.state === "blocked" ? "other" : "agentMessage",
3108
+ action: "updated",
3109
+ path: input.job.cwd,
3110
+ title: input.job.detail,
3111
+ isImage: false
3112
+ },
3113
+ goal: (0, goal_1.inferredGoalFromText)({
3114
+ kind: "claudeBackground",
3115
+ objective: input.job.prompt ?? input.job.name,
3116
+ state: input.job.state,
3117
+ updatedAt,
3118
+ createdAt: input.job.createdAt ? new Date(input.job.createdAt).toISOString() : null
3119
+ }),
3120
+ latestMessage: null,
3121
+ threadId: input.job.sessionId ?? input.job.jobId,
3122
+ taskId: input.job.jobId,
3123
+ resumeCommand: `claude attach ${input.job.jobId}`,
3124
+ url: null,
3125
+ git: null,
3126
+ provenance: "claude",
3127
+ confidence: "typed",
3128
+ needsUser: null,
3129
+ liveSubscription: "readOnly",
3130
+ network: null
3131
+ };
3132
+ }
3133
+ async function buildClaudeBackgroundAgentsForProject(input) {
3134
+ const matching = input.jobs
3135
+ .filter((job) => (0, project_paths_1.sameProjectPath)(job.projectRoot, input.projectRoot))
3136
+ .sort((left, right) => right.updatedAt - left.updatedAt)
3137
+ .slice(0, input.limit ?? 8);
3138
+ return Promise.all(matching.map((job) => claudeAgentFromBackgroundJob({
3139
+ projectRoot: input.projectRoot,
3140
+ job
3141
+ })));
3142
+ }
3143
+ async function loadClaudeProjectSnapshotData(projectRoot, limit = 12) {
3144
+ const canonicalRoot = canonicalizeProjectPath(projectRoot);
3145
+ if (!canonicalRoot) {
3146
+ return { agents: [], events: [] };
3147
+ }
3148
+ const [sessions, teams, backgroundJobs] = await Promise.all([
3149
+ collectClaudeLoadedSessions(canonicalRoot, limit),
3150
+ readClaudeTeamSnapshots(limit * 4),
3151
+ readClaudeBackgroundJobs(limit * 4)
3152
+ ]);
3153
+ const coworkSessions = await readClaudeCoworkSessions(limit * 4);
3154
+ const inferredLeadSessionIds = inferClaudeTeamLeadSessionIds(teams, sessions);
3155
+ const teamIndex = buildClaudeTeamIndex(teams, inferredLeadSessionIds);
3156
+ const [teamAgents, coworkAgents, backgroundAgents] = await Promise.all([
3157
+ buildClaudeTeamAgentsForProject({
3158
+ projectRoot: canonicalRoot,
3159
+ sessions,
3160
+ teamIndex
3161
+ }),
3162
+ buildClaudeCoworkAgentsForProject({
3163
+ projectRoot: canonicalRoot,
3164
+ sessions: coworkSessions,
3165
+ limit
3166
+ }),
3167
+ buildClaudeBackgroundAgentsForProject({
3168
+ projectRoot: canonicalRoot,
3169
+ jobs: backgroundJobs,
3170
+ limit
3171
+ })
3172
+ ]);
3173
+ if (sessions.length === 0 && teamAgents.length === 0 && coworkAgents.length === 0 && backgroundAgents.length === 0) {
3174
+ return { agents: [], events: [] };
3175
+ }
3176
+ const agentsById = new Map();
3177
+ const events = new Map();
3178
+ const upsertAgent = (agent) => {
3179
+ const existing = agentsById.get(agent.id);
3180
+ agentsById.set(agent.id, existing ? mergeClaudeDashboardAgents(existing, agent) : agent);
3181
+ };
3182
+ for (const session of sessions) {
3183
+ const teamContext = teamIndex.bySessionId.get(session.sessionId) ?? null;
3184
+ const appearance = await (0, appearance_1.ensureAgentAppearance)(canonicalRoot, claudeLeadAgentId(session.sessionId));
3185
+ upsertAgent(claudeAgentFromLoadedSession(session, appearance, teamContext));
3186
+ for (const childAgent of await buildClaudeSubagentAgents({
3187
+ projectRoot: canonicalRoot,
3188
+ session,
3189
+ teamIndex
3190
+ })) {
3191
+ upsertAgent(childAgent);
3192
+ }
3193
+ for (const event of buildClaudeSessionEvents({
3194
+ sessionId: session.sessionId,
3195
+ fallbackCwd: session.cwd,
3196
+ records: session.records,
3197
+ fallbackUpdatedAt: session.updatedAt,
3198
+ hookRecords: session.hookRecords,
3199
+ teamIndex
3200
+ })) {
3201
+ events.set(event.id, event);
3202
+ }
3203
+ }
3204
+ for (const teamAgent of teamAgents) {
3205
+ upsertAgent(teamAgent);
3206
+ }
3207
+ for (const coworkAgent of coworkAgents) {
3208
+ upsertAgent(coworkAgent);
3209
+ }
3210
+ for (const backgroundAgent of backgroundAgents) {
3211
+ upsertAgent(backgroundAgent);
3212
+ }
3213
+ return {
3214
+ agents: [...agentsById.values()].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
3215
+ events: [...events.values()].sort((left, right) => right.createdAt.localeCompare(left.createdAt))
3216
+ };
3217
+ }
3218
+ function summariseClaudeSession(sessionId, fallbackCwd, records, fallbackUpdatedAt, hookRecords = [], sessionTitle = null) {
3219
+ const timestampedRecords = records
3220
+ .map((record, index) => ({ record, index, timestamp: explicitRecordTimestampMs(record) }))
3221
+ .filter((entry) => Number.isFinite(entry.timestamp))
3222
+ .sort((left, right) => left.timestamp - right.timestamp || left.index - right.index)
3223
+ .map((entry) => entry.record);
3224
+ const ordered = timestampedRecords.length > 0 ? timestampedRecords : [...records];
3225
+ const latestRecord = ordered.at(-1) ?? null;
3226
+ const latestAssistant = latestTimestampedRecord(records, (record) => record.type === "assistant")
3227
+ ?? [...records].reverse().find((record) => record.type === "assistant")
3228
+ ?? null;
3229
+ const latestToolRecord = latestTimestampedRecord(records, (record) => Boolean(extractAssistantTool(record)));
3230
+ const latestUserTextRecord = latestTimestampedRecord(records, (record) => Boolean(extractUserText(record)));
3231
+ const latestAssistantTextRecord = latestTimestampedRecord(records, (record) => Boolean(extractAssistantText(record)));
3232
+ const latestMessage = latestAssistant ? messageObject(latestAssistant) : null;
3233
+ const model = latestMessage && typeof latestMessage.model === "string" ? latestMessage.model : null;
3234
+ const sessionGoalObjective = normalizeClaudeSessionTitle(sessionTitle ?? extractClaudeSessionTitle(records));
3235
+ const displayLabel = labelFromSessionTitle(sessionGoalObjective, model, sessionId);
3236
+ const updatedAtMs = latestRecord ? recordTimestampMs(latestRecord, fallbackUpdatedAt) : fallbackUpdatedAt;
3237
+ const updatedAt = new Date(updatedAtMs).toISOString();
3238
+ const gitBranch = latestRecord && typeof latestRecord.gitBranch === "string" ? latestRecord.gitBranch : null;
3239
+ const latestToolUpdatedAt = latestToolRecord ? recordTimestampMs(latestToolRecord, fallbackUpdatedAt) : Number.NEGATIVE_INFINITY;
3240
+ const latestUserTextUpdatedAt = latestUserTextRecord ? recordTimestampMs(latestUserTextRecord, fallbackUpdatedAt) : Number.NEGATIVE_INFINITY;
3241
+ const latestAssistantTextUpdatedAt = latestAssistantTextRecord ? recordTimestampMs(latestAssistantTextRecord, fallbackUpdatedAt) : Number.NEGATIVE_INFINITY;
3242
+ const latestHookSummary = hookRecords
3243
+ .filter((record) => !isClaudeChildHookRecord(record))
3244
+ .reverse()
3245
+ .map((record) => summariseClaudeHookRecord({
3246
+ sessionId,
3247
+ model,
3248
+ fallbackCwd,
3249
+ gitBranch,
3250
+ record,
3251
+ fallbackUpdatedAt
3252
+ }))
3253
+ .find((summary) => Boolean(summary));
3254
+ if (latestHookSummary) {
3255
+ return withInferredClaudeGoal(mergeClaudeAssistantTextSummary({
3256
+ base: {
3257
+ ...ageClaudeSummary(latestHookSummary),
3258
+ label: displayLabel
3259
+ },
3260
+ latestAssistantTextRecord,
3261
+ fallbackUpdatedAt,
3262
+ fallbackCwd
3263
+ }), "claudeSession", sessionGoalObjective);
3264
+ }
3265
+ if (latestAssistantTextRecord && latestAssistantTextUpdatedAt >= Math.max(latestToolUpdatedAt, latestUserTextUpdatedAt)) {
3266
+ const text = extractAssistantText(latestAssistantTextRecord) ?? "Responded";
3267
+ const textPaths = extractPathsFromText(text);
3268
+ const ageMs = Date.now() - latestAssistantTextUpdatedAt;
3269
+ const state = ageMs <= 2 * 60 * 1000 ? "thinking"
3270
+ : ageMs <= RECENT_DONE_WINDOW_MS ? "done"
3271
+ : "idle";
3272
+ return withInferredClaudeGoal({
3273
+ label: displayLabel,
3274
+ sourceKind: sourceKindFromModel(model),
3275
+ state,
3276
+ detail: shorten(text, 88),
3277
+ updatedAt,
3278
+ paths: textPaths.length > 0 ? textPaths : [fallbackCwd],
3279
+ activityEvent: ageMs <= RECENT_MESSAGE_WINDOW_MS
3280
+ ? {
3281
+ type: "agentMessage",
3282
+ action: "said",
3283
+ path: textPaths[0] ?? fallbackCwd,
3284
+ title: shorten(text, 88),
3285
+ isImage: false
3286
+ }
3287
+ : null,
3288
+ gitBranch,
3289
+ confidence: "inferred",
3290
+ needsUser: null,
3291
+ latestMessage: text,
3292
+ isOngoing: state === "thinking"
3293
+ }, "claudeSession", sessionGoalObjective);
3294
+ }
3295
+ if (latestToolRecord && latestToolUpdatedAt >= latestUserTextUpdatedAt) {
3296
+ const tool = extractAssistantTool(latestToolRecord);
3297
+ if (tool) {
3298
+ const toolSummary = claudeToolSummary({
3299
+ sessionId,
3300
+ model,
3301
+ fallbackCwd,
3302
+ gitBranch,
3303
+ updatedAt: new Date(latestToolUpdatedAt).toISOString(),
3304
+ toolName: tool.name,
3305
+ toolInput: tool.input,
3306
+ failed: false
3307
+ });
3308
+ if (toolSummary) {
3309
+ return withInferredClaudeGoal({
3310
+ ...ageClaudeSummary(toolSummary),
3311
+ label: displayLabel,
3312
+ confidence: "inferred"
3313
+ }, "claudeSession", sessionGoalObjective);
3314
+ }
3315
+ }
3316
+ }
3317
+ if (latestUserTextRecord) {
3318
+ const text = extractUserText(latestUserTextRecord) ?? "Assigned work";
3319
+ const paths = extractPathsFromText(text);
3320
+ return withInferredClaudeGoal(ageClaudeSummary({
3321
+ label: displayLabel,
3322
+ sourceKind: sourceKindFromModel(model),
3323
+ state: "planning",
3324
+ detail: shorten(text, 88),
3325
+ updatedAt: new Date(latestUserTextUpdatedAt).toISOString(),
3326
+ paths: paths.length > 0 ? paths : [fallbackCwd],
3327
+ activityEvent: null,
3328
+ gitBranch,
3329
+ confidence: "inferred",
3330
+ needsUser: null,
3331
+ latestMessage: null,
3332
+ isOngoing: true
3333
+ }), "claudeSession", sessionGoalObjective);
3334
+ }
3335
+ return withInferredClaudeGoal({
3336
+ label: displayLabel,
3337
+ sourceKind: sourceKindFromModel(model),
3338
+ state: "idle",
3339
+ detail: "Idle",
3340
+ updatedAt,
3341
+ paths: [fallbackCwd],
3342
+ activityEvent: null,
3343
+ gitBranch,
3344
+ confidence: "inferred",
3345
+ needsUser: null,
3346
+ latestMessage: null,
3347
+ isOngoing: false
3348
+ }, "claudeSession", sessionGoalObjective);
3349
+ }
3350
+ async function discoverClaudeProjects(limit = 50) {
3351
+ const [sdkProjects, teams, coworkSpaces, coworkSessions, backgroundJobs] = await Promise.all([
3352
+ discoverClaudeProjectsViaSdk(limit),
3353
+ readClaudeTeamSnapshots(limit * 4),
3354
+ readClaudeCoworkSpaces(limit * 4),
3355
+ readClaudeCoworkSessions(limit * 4),
3356
+ readClaudeBackgroundJobs(limit * 4)
3357
+ ]);
3358
+ const fallbackProjects = sdkProjects.length > 0 ? [] : await scanClaudeProjectDirs();
3359
+ const grouped = new Map();
3360
+ for (const project of [
3361
+ ...sdkProjects,
3362
+ ...fallbackProjects.map((project) => ({
3363
+ root: project.root,
3364
+ updatedAt: project.updatedAt,
3365
+ count: project.count,
3366
+ sourceKind: "claude:transcript",
3367
+ sourceKinds: ["claude:transcript"]
3368
+ })),
3369
+ ...claudeProjectsFromTeams(teams, limit),
3370
+ ...claudeProjectsFromCowork({
3371
+ spaces: coworkSpaces,
3372
+ sessions: coworkSessions,
3373
+ limit
3374
+ }),
3375
+ ...claudeProjectsFromBackgroundJobs(backgroundJobs, limit)
3376
+ ]) {
3377
+ const existing = [...grouped.values()].find((candidate) => (0, project_paths_1.sameProjectPath)(candidate.root, project.root));
3378
+ if (existing) {
3379
+ existing.count += project.count;
3380
+ existing.updatedAt = Math.max(existing.updatedAt, project.updatedAt);
3381
+ for (const sourceKind of project.sourceKinds ?? (project.sourceKind ? [project.sourceKind] : [])) {
3382
+ mergeClaudeProjectSourceKinds(existing, sourceKind);
3383
+ }
3384
+ continue;
3385
+ }
3386
+ grouped.set(project.root, { ...project });
3387
+ }
3388
+ return [...grouped.values()]
3389
+ .sort((left, right) => right.updatedAt - left.updatedAt)
3390
+ .slice(0, limit)
3391
+ .map((project) => ({
3392
+ root: project.root,
3393
+ label: (0, node_path_1.basename)(project.root) || project.root,
3394
+ updatedAt: project.updatedAt,
3395
+ count: project.count,
3396
+ sourceKind: project.sourceKind,
3397
+ sourceKinds: project.sourceKinds
3398
+ }));
3399
+ }
3400
+ function discoverClaudeProjectsFromTeamsForTest(teams, limit = 50) {
3401
+ return claudeProjectsFromTeams(teams, limit).map((project) => ({
3402
+ root: project.root,
3403
+ label: (0, node_path_1.basename)(project.root) || project.root,
3404
+ updatedAt: project.updatedAt,
3405
+ count: project.count,
3406
+ sourceKind: project.sourceKind,
3407
+ sourceKinds: project.sourceKinds
3408
+ }));
3409
+ }
3410
+ function discoverClaudeProjectsFromCoworkForTest(input) {
3411
+ return claudeProjectsFromCowork({
3412
+ spaces: input.spaces ?? [],
3413
+ sessions: input.sessions ?? [],
3414
+ limit: input.limit ?? 50
3415
+ }).map((project) => ({
3416
+ root: project.root,
3417
+ label: (0, node_path_1.basename)(project.root) || project.root,
3418
+ updatedAt: project.updatedAt,
3419
+ count: project.count,
3420
+ sourceKind: project.sourceKind,
3421
+ sourceKinds: project.sourceKinds
3422
+ }));
3423
+ }
3424
+ function discoverClaudeProjectsFromBackgroundJobsForTest(jobs, limit = 50) {
3425
+ return claudeProjectsFromBackgroundJobs(jobs, limit).map((project) => ({
3426
+ root: project.root,
3427
+ label: (0, node_path_1.basename)(project.root) || project.root,
3428
+ updatedAt: project.updatedAt,
3429
+ count: project.count,
3430
+ sourceKind: project.sourceKind,
3431
+ sourceKinds: project.sourceKinds
3432
+ }));
3433
+ }
3434
+ async function buildClaudeLeadAgentsForTest(input) {
3435
+ const session = {
3436
+ sessionId: input.sessionId,
3437
+ title: extractClaudeSessionTitle(input.records ?? []),
3438
+ projectDirPath: null,
3439
+ cwd: canonicalizeProjectPath(input.cwd) ?? input.cwd,
3440
+ gitBranch: input.gitBranch ?? null,
3441
+ updatedAt: input.updatedAt,
3442
+ records: input.records ?? [],
3443
+ hookRecords: input.hookRecords ?? [],
3444
+ summary: summariseClaudeSession(input.sessionId, canonicalizeProjectPath(input.cwd) ?? input.cwd, input.records ?? [], input.updatedAt, input.hookRecords ?? [])
3445
+ };
3446
+ const teamIndex = buildClaudeTeamIndex(input.teams ?? [], inferClaudeTeamLeadSessionIds(input.teams ?? [], [session]));
3447
+ const teamContext = teamIndex.bySessionId.get(session.sessionId) ?? null;
3448
+ const appearance = await (0, appearance_1.ensureAgentAppearance)(canonicalizeProjectPath(input.projectRoot) ?? input.projectRoot, claudeLeadAgentId(session.sessionId));
3449
+ return [claudeAgentFromLoadedSession(session, appearance, teamContext)];
3450
+ }
3451
+ async function buildClaudeSubagentAgentsForTest(input) {
3452
+ const session = {
3453
+ sessionId: input.sessionId,
3454
+ title: extractClaudeSessionTitle(input.records ?? []),
3455
+ projectDirPath: input.projectDirPath ?? null,
3456
+ cwd: canonicalizeProjectPath(input.cwd) ?? input.cwd,
3457
+ gitBranch: input.gitBranch ?? null,
3458
+ updatedAt: input.updatedAt,
3459
+ records: input.records ?? [],
3460
+ hookRecords: input.hookRecords,
3461
+ summary: summariseClaudeSession(input.sessionId, canonicalizeProjectPath(input.cwd) ?? input.cwd, input.records ?? [], input.updatedAt, input.hookRecords)
3462
+ };
3463
+ const teamIndex = buildClaudeTeamIndex(input.teams ?? [], inferClaudeTeamLeadSessionIds(input.teams ?? [], [session]));
3464
+ return buildClaudeSubagentAgents({
3465
+ projectRoot: canonicalizeProjectPath(input.projectRoot) ?? input.projectRoot,
3466
+ session,
3467
+ teamIndex
3468
+ });
3469
+ }
3470
+ async function buildClaudeTeamAgentsForTest(input) {
3471
+ const sessions = [];
3472
+ const inferredLeadSessionIds = inferClaudeTeamLeadSessionIds(input.teams, sessions);
3473
+ const teamIndex = buildClaudeTeamIndex(input.teams, inferredLeadSessionIds);
3474
+ return buildClaudeTeamAgentsForProject({
3475
+ projectRoot: canonicalizeProjectPath(input.projectRoot) ?? input.projectRoot,
3476
+ sessions,
3477
+ teamIndex
3478
+ });
3479
+ }
3480
+ async function buildClaudeCoworkAgentsForTest(input) {
3481
+ return buildClaudeCoworkAgentsForProject({
3482
+ projectRoot: canonicalizeProjectPath(input.projectRoot) ?? input.projectRoot,
3483
+ sessions: input.sessions
3484
+ });
3485
+ }
3486
+ async function buildClaudeBackgroundAgentsForTest(input) {
3487
+ return buildClaudeBackgroundAgentsForProject({
3488
+ projectRoot: canonicalizeProjectPath(input.projectRoot) ?? input.projectRoot,
3489
+ jobs: input.jobs
3490
+ });
3491
+ }
3492
+ async function loadClaudeAgents(projectRoot, limit = 12) {
3493
+ return (await loadClaudeProjectSnapshotData(projectRoot, limit)).agents;
3494
+ }
3495
+ //# sourceMappingURL=claude.js.map