agentacta 2026.3.12 → 2026.3.27

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.
@@ -0,0 +1,443 @@
1
+ 'use strict';
2
+
3
+ const PATH_KEYS = new Set([
4
+ 'path', 'file', 'filename', 'file_path', 'filepath',
5
+ 'cwd', 'workdir', 'directory', 'dir', 'root',
6
+ 'repository_path', 'repositorypath', 'repo_path', 'repopath'
7
+ ]);
8
+
9
+ const PROJECT_KEYS = new Set([
10
+ 'project', 'project_name', 'projectname',
11
+ 'repo', 'repository', 'repo_name', 'reponame', 'repository_name', 'repositoryname',
12
+ 'workspace'
13
+ ]);
14
+
15
+ const BRANCH_KEYS = new Set([
16
+ 'branch', 'branch_name', 'branchname', 'ref', 'git_ref', 'gitref'
17
+ ]);
18
+
19
+ const LOOKAROUND_WINDOW = 6;
20
+ const MIN_CONFIDENCE = 2;
21
+
22
+ function safeParseJson(value) {
23
+ if (typeof value !== 'string') return null;
24
+ try {
25
+ return JSON.parse(value);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function normalizeKey(value) {
32
+ return String(value || '').trim().toLowerCase();
33
+ }
34
+
35
+ function normalizeProjectKey(value) {
36
+ return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]/g, '');
37
+ }
38
+
39
+ function looksLikeFilesystemPath(value, options = {}) {
40
+ const { allowRelative = false } = options;
41
+ if (typeof value !== 'string') return false;
42
+
43
+ const raw = value.trim();
44
+ if (!raw) return false;
45
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return false;
46
+ if (/^[\w.-]+@[\w.-]+:.+/.test(raw)) return false;
47
+ if (/^refs\/(heads|tags|remotes)\//i.test(raw)) return false;
48
+
49
+ const normalized = raw.replace(/\\/g, '/');
50
+ const isWindowsDriveAbs = /^[a-zA-Z]:\//.test(normalized);
51
+ const isUncAbs = normalized.startsWith('//');
52
+ if (
53
+ normalized.startsWith('/')
54
+ || normalized.startsWith('~/')
55
+ || normalized.startsWith('./')
56
+ || normalized.startsWith('../')
57
+ || isWindowsDriveAbs
58
+ || isUncAbs
59
+ ) {
60
+ return true;
61
+ }
62
+
63
+ if (!allowRelative || !normalized.includes('/')) return false;
64
+ if (/^(origin|remotes)\//i.test(normalized)) return false;
65
+
66
+ const parts = normalized.split('/').filter(Boolean);
67
+ if (!parts.length) return false;
68
+ if (parts.length === 2 && !parts[1].includes('.')) return false;
69
+
70
+ return parts.length >= 2;
71
+ }
72
+
73
+ function isInternalProjectTag(tag) {
74
+ if (!tag) return true;
75
+ return tag.startsWith('agent:') || tag.startsWith('claude:');
76
+ }
77
+
78
+ function toDisplayProject(tag) {
79
+ if (!tag || typeof tag !== 'string') return null;
80
+ const value = tag.trim();
81
+ if (!value || isInternalProjectTag(value)) return null;
82
+ return value;
83
+ }
84
+
85
+ function extractProjectFromPath(filePath) {
86
+ if (!filePath || typeof filePath !== 'string') return null;
87
+ const normalized = filePath.trim().replace(/\\/g, '/');
88
+ if (!looksLikeFilesystemPath(normalized, { allowRelative: true })) return null;
89
+ const isWindowsDriveAbs = /^[a-zA-Z]:\//.test(normalized);
90
+ const isUncAbs = normalized.startsWith('//');
91
+ if (!normalized.startsWith('/') && !normalized.startsWith('~') && !isWindowsDriveAbs && !isUncAbs) return null;
92
+
93
+ const rel = normalized
94
+ .replace(/^[a-zA-Z]:\//, '')
95
+ .replace(/^\/\/[^/]+\/[^/]+\//, '')
96
+ .replace(/^\/home\/[^/]+\//, '')
97
+ .replace(/^\/Users\/[^/]+\//, '')
98
+ .replace(/^Users\/[^/]+\//, '')
99
+ .replace(/^~\//, '');
100
+
101
+ const parts = rel.split('/').filter(Boolean);
102
+ if (!parts.length) return null;
103
+
104
+ if (parts[0] === 'Developer' && parts[1]) return parts[1];
105
+ if (parts[0] === 'dev' && parts[1]) return parts[1];
106
+ if (parts[0] === 'code' && parts[1]) return parts[1];
107
+ if (parts[0] === '.openclaw' && parts[1] === 'workspace') return 'workspace';
108
+ if (parts[0] === '.openclaw' && parts[1] === 'agents' && parts[2]) return `agent:${parts[2]}`;
109
+ if (parts[0] === '.claude' && parts[1] === 'projects' && parts[2]) return `claude:${parts[2]}`;
110
+ if (parts[0] === 'Shared') return 'shared';
111
+ return null;
112
+ }
113
+
114
+ function extractSessionProjects(session) {
115
+ const raw = session && session.projects;
116
+ if (!raw) return [];
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(raw);
120
+ } catch {
121
+ return [];
122
+ }
123
+ if (!Array.isArray(parsed)) return [];
124
+ return [...new Set(parsed.map(toDisplayProject).filter(Boolean))];
125
+ }
126
+
127
+ function addCandidate(candidateSet, value) {
128
+ const p = toDisplayProject(value);
129
+ if (p) candidateSet.add(p);
130
+ }
131
+
132
+ function visitObject(value, visitor, key = '') {
133
+ if (!value || typeof value !== 'object') return;
134
+ if (Array.isArray(value)) {
135
+ for (const item of value) visitObject(item, visitor, key);
136
+ return;
137
+ }
138
+ for (const [k, v] of Object.entries(value)) {
139
+ visitor(k, v);
140
+ if (v && typeof v === 'object') visitObject(v, visitor, k);
141
+ }
142
+ }
143
+
144
+ function buildCandidateProjects(session, events) {
145
+ const candidateSet = new Set(extractSessionProjects(session));
146
+
147
+ for (const event of events || []) {
148
+ const args = safeParseJson(event.tool_args);
149
+ if (!args) continue;
150
+
151
+ visitObject(args, (key, value) => {
152
+ if (typeof value !== 'string') return;
153
+
154
+ const keyNorm = normalizeKey(key);
155
+ if (PATH_KEYS.has(keyNorm)) {
156
+ if (!looksLikeFilesystemPath(value, { allowRelative: true })) return;
157
+ addCandidate(candidateSet, extractProjectFromPath(value));
158
+ return;
159
+ }
160
+
161
+ if (looksLikeFilesystemPath(value)) {
162
+ addCandidate(candidateSet, extractProjectFromPath(value));
163
+ }
164
+
165
+ if (PROJECT_KEYS.has(keyNorm)) {
166
+ addCandidate(candidateSet, value);
167
+ }
168
+ });
169
+ }
170
+
171
+ return [...candidateSet];
172
+ }
173
+
174
+ function escapeRegExp(value) {
175
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
176
+ }
177
+
178
+ function countCandidateMentions(text, candidate) {
179
+ if (!text || !candidate) return 0;
180
+ const rx = new RegExp(`(^|[^a-z0-9])${escapeRegExp(candidate.toLowerCase())}([^a-z0-9]|$)`, 'gi');
181
+ let matches = 0;
182
+ let m;
183
+ const haystack = text.toLowerCase();
184
+ while ((m = rx.exec(haystack)) !== null) {
185
+ matches += 1;
186
+ }
187
+ return matches;
188
+ }
189
+
190
+ function buildCandidateLookup(candidates) {
191
+ const byNorm = new Map();
192
+ for (const candidate of candidates) {
193
+ byNorm.set(normalizeProjectKey(candidate), candidate);
194
+ }
195
+ return byNorm;
196
+ }
197
+
198
+ function resolveCandidate(value, candidates, byNorm, options = {}) {
199
+ const { allowPath = true } = options;
200
+ if (!value || typeof value !== 'string') return null;
201
+ const raw = value.trim();
202
+ if (!raw) return null;
203
+
204
+ if (allowPath) {
205
+ const fromPath = toDisplayProject(extractProjectFromPath(raw));
206
+ if (fromPath) {
207
+ const resolved = byNorm.get(normalizeProjectKey(fromPath));
208
+ return resolved || fromPath;
209
+ }
210
+ }
211
+
212
+ const direct = byNorm.get(normalizeProjectKey(raw));
213
+ if (direct) return direct;
214
+
215
+ const lower = raw.toLowerCase();
216
+ for (const candidate of candidates) {
217
+ const rx = new RegExp(`(^|[^a-z0-9])${escapeRegExp(candidate.toLowerCase())}([^a-z0-9]|$)`, 'i');
218
+ if (rx.test(lower)) return candidate;
219
+ }
220
+
221
+ return null;
222
+ }
223
+
224
+ function chooseBestProject(scores) {
225
+ let bestProject = null;
226
+ let bestScore = 0;
227
+ let secondBest = 0;
228
+
229
+ for (const [project, score] of scores.entries()) {
230
+ if (score > bestScore) {
231
+ secondBest = bestScore;
232
+ bestScore = score;
233
+ bestProject = project;
234
+ continue;
235
+ }
236
+ if (score > secondBest) secondBest = score;
237
+ }
238
+
239
+ if (!bestProject || bestScore < MIN_CONFIDENCE) {
240
+ return { project: null, score: 0 };
241
+ }
242
+ if (bestScore === secondBest) {
243
+ return { project: null, score: 0 };
244
+ }
245
+ return { project: bestProject, score: bestScore };
246
+ }
247
+
248
+ function addScore(scores, project, value) {
249
+ if (!project || value <= 0) return;
250
+ scores.set(project, (scores.get(project) || 0) + value);
251
+ }
252
+
253
+ function extractCallBaseId(id) {
254
+ if (!id) return '';
255
+ return String(id).replace(/:(call|result)$/, '');
256
+ }
257
+
258
+ function scoreEvent(event, candidates, byNorm) {
259
+ const scores = new Map();
260
+ const args = safeParseJson(event.tool_args);
261
+
262
+ if (args) {
263
+ visitObject(args, (key, value) => {
264
+ if (typeof value !== 'string') return;
265
+ const keyNorm = normalizeKey(key);
266
+ if (PATH_KEYS.has(keyNorm)) {
267
+ const candidate = resolveCandidate(value, candidates, byNorm, { allowPath: true });
268
+ if (!candidate) return;
269
+ addScore(scores, candidate, 4);
270
+ return;
271
+ }
272
+
273
+ if (PROJECT_KEYS.has(keyNorm)) {
274
+ const candidate = resolveCandidate(value, candidates, byNorm, { allowPath: true });
275
+ if (!candidate) return;
276
+ addScore(scores, candidate, 3);
277
+ return;
278
+ }
279
+
280
+ if (BRANCH_KEYS.has(keyNorm)) {
281
+ const candidate = resolveCandidate(value, candidates, byNorm, { allowPath: false });
282
+ if (!candidate) return;
283
+ addScore(scores, candidate, 2);
284
+ return;
285
+ }
286
+
287
+ if (looksLikeFilesystemPath(value)) {
288
+ const candidate = resolveCandidate(value, candidates, byNorm, { allowPath: true });
289
+ if (!candidate) return;
290
+ addScore(scores, candidate, 3);
291
+ return;
292
+ }
293
+
294
+ const candidate = resolveCandidate(value, candidates, byNorm, { allowPath: false });
295
+ if (!candidate) return;
296
+ addScore(scores, candidate, 1);
297
+ });
298
+ }
299
+
300
+ if (typeof event.content === 'string' && event.content) {
301
+ for (const candidate of candidates) {
302
+ const count = countCandidateMentions(event.content, candidate);
303
+ if (count > 0) addScore(scores, candidate, Math.min(count, 2));
304
+ }
305
+ }
306
+
307
+ if (typeof event.tool_name === 'string' && event.tool_name) {
308
+ const candidate = resolveCandidate(event.tool_name, candidates, byNorm);
309
+ if (candidate) addScore(scores, candidate, 1);
310
+ }
311
+
312
+ return chooseBestProject(scores);
313
+ }
314
+
315
+ function findPrevAttributed(events, idx) {
316
+ for (let i = idx - 1; i >= 0 && idx - i <= LOOKAROUND_WINDOW; i--) {
317
+ if (events[i].project) return events[i].project;
318
+ }
319
+ return null;
320
+ }
321
+
322
+ function findNextAttributed(events, idx) {
323
+ for (let i = idx + 1; i < events.length && i - idx <= LOOKAROUND_WINDOW; i++) {
324
+ if (events[i].project) return events[i].project;
325
+ }
326
+ return null;
327
+ }
328
+
329
+ function attributeSessionEvents(session, events) {
330
+ const list = Array.isArray(events) ? events : [];
331
+ if (!list.length) return { events: [], projectFilters: [] };
332
+
333
+ const candidates = buildCandidateProjects(session, list);
334
+ const byNorm = buildCandidateLookup(candidates);
335
+ const withOrder = list.map((event, idx) => ({ idx, event }));
336
+
337
+ withOrder.sort((a, b) => {
338
+ const ta = Date.parse(a.event.timestamp || 0) || 0;
339
+ const tb = Date.parse(b.event.timestamp || 0) || 0;
340
+ if (ta !== tb) return ta - tb;
341
+ return String(a.event.id || '').localeCompare(String(b.event.id || ''));
342
+ });
343
+
344
+ const callProjectByBase = new Map();
345
+ const attributedOrdered = withOrder.map(({ idx, event }) => {
346
+ const base = {
347
+ ...event,
348
+ project: null,
349
+ project_confidence: 0
350
+ };
351
+
352
+ const scored = scoreEvent(base, candidates, byNorm);
353
+ if (scored.project) {
354
+ base.project = scored.project;
355
+ base.project_confidence = scored.score;
356
+ }
357
+
358
+ if (base.type === 'tool_call' && base.project) {
359
+ const callBaseId = extractCallBaseId(base.id);
360
+ if (callBaseId) callProjectByBase.set(callBaseId, base.project);
361
+ }
362
+
363
+ return { idx, event: base };
364
+ });
365
+
366
+ const orderedEvents = attributedOrdered.map(entry => entry.event);
367
+
368
+ for (let i = 0; i < orderedEvents.length; i++) {
369
+ const current = orderedEvents[i];
370
+ if (current.project) continue;
371
+
372
+ if (current.type === 'tool_result') {
373
+ const callBaseId = extractCallBaseId(current.id);
374
+ const linkedProject = callProjectByBase.get(callBaseId);
375
+ if (linkedProject) {
376
+ current.project = linkedProject;
377
+ current.project_confidence = 3;
378
+ continue;
379
+ }
380
+ }
381
+
382
+ if (current.type !== 'message') continue;
383
+
384
+ const prevProject = findPrevAttributed(orderedEvents, i);
385
+ const nextProject = findNextAttributed(orderedEvents, i);
386
+
387
+ if (prevProject && nextProject && prevProject === nextProject) {
388
+ current.project = prevProject;
389
+ current.project_confidence = 2;
390
+ continue;
391
+ }
392
+
393
+ if (prevProject && !nextProject) {
394
+ current.project = prevProject;
395
+ current.project_confidence = 2;
396
+ }
397
+ }
398
+
399
+ const eventsOut = new Array(list.length);
400
+ for (const entry of attributedOrdered) {
401
+ eventsOut[entry.idx] = entry.event;
402
+ }
403
+
404
+ const counts = new Map();
405
+ for (const event of eventsOut) {
406
+ if (!event.project || event.project_confidence < MIN_CONFIDENCE) {
407
+ event.project = null;
408
+ event.project_confidence = 0;
409
+ continue;
410
+ }
411
+ counts.set(event.project, (counts.get(event.project) || 0) + 1);
412
+ }
413
+
414
+ const projectFilters = [...counts.entries()]
415
+ .sort((a, b) => (b[1] - a[1]) || a[0].localeCompare(b[0]))
416
+ .map(([project, eventCount]) => ({ project, eventCount }));
417
+
418
+ return { events: eventsOut, projectFilters };
419
+ }
420
+
421
+ function attributeEventDelta(session, deltaEvents, contextEvents = []) {
422
+ const delta = Array.isArray(deltaEvents) ? deltaEvents : [];
423
+ if (!delta.length) return [];
424
+
425
+ const context = Array.isArray(contextEvents) ? contextEvents : [];
426
+ const merged = [...context, ...delta];
427
+ const attributed = attributeSessionEvents(session, merged).events;
428
+
429
+ const byId = new Map();
430
+ for (const event of attributed) {
431
+ if (!event || !event.id) continue;
432
+ byId.set(event.id, event);
433
+ }
434
+
435
+ return delta.map(event => byId.get(event.id) || { ...event, project: null, project_confidence: 0 });
436
+ }
437
+
438
+ module.exports = {
439
+ attributeSessionEvents,
440
+ attributeEventDelta,
441
+ extractProjectFromPath,
442
+ isInternalProjectTag
443
+ };