@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.
- package/dist/adapters/claude.d.ts +2 -0
- package/dist/adapters/claude.js +43 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex-cloud.d.ts +4 -0
- package/dist/adapters/codex-cloud.js +80 -0
- package/dist/adapters/codex-cloud.js.map +1 -0
- package/dist/adapters/codex-local.d.ts +17 -0
- package/dist/adapters/codex-local.js +180 -0
- package/dist/adapters/codex-local.js.map +1 -0
- package/dist/adapters/cursor-cloud.d.ts +2 -0
- package/dist/adapters/cursor-cloud.js +54 -0
- package/dist/adapters/cursor-cloud.js.map +1 -0
- package/dist/adapters/cursor-local.d.ts +2 -0
- package/dist/adapters/cursor-local.js +37 -0
- package/dist/adapters/cursor-local.js.map +1 -0
- package/dist/adapters/helpers.d.ts +15 -0
- package/dist/adapters/helpers.js +33 -0
- package/dist/adapters/helpers.js.map +1 -0
- package/dist/adapters/hermes.d.ts +2 -0
- package/dist/adapters/hermes.js +41 -0
- package/dist/adapters/hermes.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +37 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openclaw.d.ts +2 -0
- package/dist/adapters/openclaw.js +41 -0
- package/dist/adapters/openclaw.js.map +1 -0
- package/dist/adapters/presence.d.ts +2 -0
- package/dist/adapters/presence.js +36 -0
- package/dist/adapters/presence.js.map +1 -0
- package/dist/adapters/static-source.d.ts +15 -0
- package/dist/adapters/static-source.js +41 -0
- package/dist/adapters/static-source.js.map +1 -0
- package/dist/adapters/types.d.ts +44 -0
- package/dist/adapters/types.js +3 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/app-server.d.ts +94 -0
- package/dist/app-server.js +382 -0
- package/dist/app-server.js.map +1 -0
- package/dist/app-settings.d.ts +52 -0
- package/dist/app-settings.js +268 -0
- package/dist/app-settings.js.map +1 -0
- package/dist/appearance.d.ts +6 -0
- package/dist/appearance.js +84 -0
- package/dist/appearance.js.map +1 -0
- package/dist/aseprite.d.ts +4 -0
- package/dist/aseprite.js +42 -0
- package/dist/aseprite.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +44 -0
- package/dist/claude-agent-sdk.js +347 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/claude-home-cache.d.ts +61 -0
- package/dist/claude-home-cache.js +438 -0
- package/dist/claude-home-cache.js.map +1 -0
- package/dist/claude.d.ts +154 -0
- package/dist/claude.js +3495 -0
- package/dist/claude.js.map +1 -0
- package/dist/cloud.d.ts +2 -0
- package/dist/cloud.js +25 -0
- package/dist/cloud.js.map +1 -0
- package/dist/codex-command.d.ts +25 -0
- package/dist/codex-command.js +235 -0
- package/dist/codex-command.js.map +1 -0
- package/dist/codex-session-files.d.ts +8 -0
- package/dist/codex-session-files.js +509 -0
- package/dist/codex-session-files.js.map +1 -0
- package/dist/codex-thread-query.d.ts +14 -0
- package/dist/codex-thread-query.js +82 -0
- package/dist/codex-thread-query.js.map +1 -0
- package/dist/cursor-cloud-data.d.ts +34 -0
- package/dist/cursor-cloud-data.js +344 -0
- package/dist/cursor-cloud-data.js.map +1 -0
- package/dist/cursor-lib/local-discovery.d.ts +17 -0
- package/dist/cursor-lib/local-discovery.js +251 -0
- package/dist/cursor-lib/local-discovery.js.map +1 -0
- package/dist/cursor-lib/shared.d.ts +5 -0
- package/dist/cursor-lib/shared.js +88 -0
- package/dist/cursor-lib/shared.js.map +1 -0
- package/dist/cursor.d.ts +8 -0
- package/dist/cursor.js +1584 -0
- package/dist/cursor.js.map +1 -0
- package/dist/domain/codex-turn-semantics.d.ts +4 -0
- package/dist/domain/codex-turn-semantics.js +49 -0
- package/dist/domain/codex-turn-semantics.js.map +1 -0
- package/dist/domain/workload-policy.d.ts +8 -0
- package/dist/domain/workload-policy.js +268 -0
- package/dist/domain/workload-policy.js.map +1 -0
- package/dist/domain/workspace-activity.d.ts +10 -0
- package/dist/domain/workspace-activity.js +372 -0
- package/dist/domain/workspace-activity.js.map +1 -0
- package/dist/goal.d.ts +11 -0
- package/dist/goal.js +76 -0
- package/dist/goal.js.map +1 -0
- package/dist/hermes-hook-install.d.ts +12 -0
- package/dist/hermes-hook-install.js +335 -0
- package/dist/hermes-hook-install.js.map +1 -0
- package/dist/hermes.d.ts +97 -0
- package/dist/hermes.js +2446 -0
- package/dist/hermes.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/live-monitor-lib/events.d.ts +43 -0
- package/dist/live-monitor-lib/events.js +1463 -0
- package/dist/live-monitor-lib/events.js.map +1 -0
- package/dist/live-monitor-lib/rollout-hooks.d.ts +13 -0
- package/dist/live-monitor-lib/rollout-hooks.js +253 -0
- package/dist/live-monitor-lib/rollout-hooks.js.map +1 -0
- package/dist/live-monitor.d.ts +102 -0
- package/dist/live-monitor.js +1461 -0
- package/dist/live-monitor.js.map +1 -0
- package/dist/local-thread-selection.d.ts +2 -0
- package/dist/local-thread-selection.js +58 -0
- package/dist/local-thread-selection.js.map +1 -0
- package/dist/openclaw.d.ts +41 -0
- package/dist/openclaw.js +715 -0
- package/dist/openclaw.js.map +1 -0
- package/dist/presence.d.ts +9 -0
- package/dist/presence.js +108 -0
- package/dist/presence.js.map +1 -0
- package/dist/project-identity.d.ts +7 -0
- package/dist/project-identity.js +162 -0
- package/dist/project-identity.js.map +1 -0
- package/dist/project-paths.d.ts +23 -0
- package/dist/project-paths.js +394 -0
- package/dist/project-paths.js.map +1 -0
- package/dist/project-storage.d.ts +7 -0
- package/dist/project-storage.js +79 -0
- package/dist/project-storage.js.map +1 -0
- package/dist/room-config.d.ts +21 -0
- package/dist/room-config.js +230 -0
- package/dist/room-config.js.map +1 -0
- package/dist/services/project-discovery.d.ts +1 -0
- package/dist/services/project-discovery.js +16 -0
- package/dist/services/project-discovery.js.map +1 -0
- package/dist/services/project-live-monitor.d.ts +1 -0
- package/dist/services/project-live-monitor.js +6 -0
- package/dist/services/project-live-monitor.js.map +1 -0
- package/dist/services/project-snapshot-coordinator.d.ts +49 -0
- package/dist/services/project-snapshot-coordinator.js +152 -0
- package/dist/services/project-snapshot-coordinator.js.map +1 -0
- package/dist/services/refresh-scheduler.d.ts +8 -0
- package/dist/services/refresh-scheduler.js +44 -0
- package/dist/services/refresh-scheduler.js.map +1 -0
- package/dist/services/snapshot-assembler.d.ts +8 -0
- package/dist/services/snapshot-assembler.js +167 -0
- package/dist/services/snapshot-assembler.js.map +1 -0
- package/dist/services/thread-store.d.ts +4 -0
- package/dist/services/thread-store.js +8 -0
- package/dist/services/thread-store.js.map +1 -0
- package/dist/snapshot-lib/activity-summary.d.ts +3 -0
- package/dist/snapshot-lib/activity-summary.js +220 -0
- package/dist/snapshot-lib/activity-summary.js.map +1 -0
- package/dist/snapshot-lib/dashboard-builder.d.ts +21 -0
- package/dist/snapshot-lib/dashboard-builder.js +146 -0
- package/dist/snapshot-lib/dashboard-builder.js.map +1 -0
- package/dist/snapshot-lib/thread-summary.d.ts +27 -0
- package/dist/snapshot-lib/thread-summary.js +953 -0
- package/dist/snapshot-lib/thread-summary.js.map +1 -0
- package/dist/snapshot.d.ts +2 -0
- package/dist/snapshot.js +21 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/types.d.ts +368 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/json.d.ts +4 -0
- package/dist/utils/json.js +29 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/text.d.ts +3 -0
- package/dist/utils/text.js +26 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/web-search.d.ts +1 -0
- package/dist/web-search.js +52 -0
- package/dist/web-search.js.map +1 -0
- package/dist/workload.d.ts +1 -0
- package/dist/workload.js +12 -0
- package/dist/workload.js.map +1 -0
- 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
|