agentlytics 0.1.18 → 0.1.20

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/cache.js CHANGED
@@ -771,8 +771,8 @@ function safeParseJson(s) {
771
771
  * Async version of scanAll that yields the event loop between iterations.
772
772
  * Required for SSE streaming so progress events actually flush to the client.
773
773
  */
774
- async function scanAllAsync(onProgress) {
775
- const chats = getAllChats();
774
+ async function scanAllAsync(onProgress, opts = {}) {
775
+ const chats = opts.chats || getAllChats();
776
776
  const total = chats.length;
777
777
  let scanned = 0;
778
778
  let analyzed = 0;
@@ -1375,6 +1375,7 @@ function getDb() { return db; }
1375
1375
  module.exports = {
1376
1376
  initDb,
1377
1377
  scanAll,
1378
+ scanAllAsync,
1378
1379
  getCachedChats,
1379
1380
  countCachedChats,
1380
1381
  getCachedOverview,
@@ -1,5 +1,18 @@
1
- const { execSync } = require('child_process');
1
+ const { execSync, execFileSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
2
4
  const os = require('os');
5
+ const Database = require('better-sqlite3');
6
+ const { getAppDataPath } = require('./base');
7
+
8
+ const HOME = os.homedir();
9
+ const ANTIGRAVITY_USER_DIR = path.join(getAppDataPath('Antigravity'), 'User');
10
+ const ANTIGRAVITY_GLOBAL_STORAGE_DB = path.join(ANTIGRAVITY_USER_DIR, 'globalStorage', 'state.vscdb');
11
+ const ANTIGRAVITY_BRAIN_DIR = path.join(HOME, '.gemini', 'antigravity', 'brain');
12
+ const OFFLINE_TRAJECTORY_SUMMARIES_KEYS = [
13
+ 'antigravityUnifiedStateSync.trajectorySummaries',
14
+ 'unifiedStateSync.trajectorySummaries',
15
+ ];
3
16
 
4
17
  // Static fallback for legacy placeholders no longer returned by the LS
5
18
  const LEGACY_MODEL_MAP = {
@@ -53,6 +66,412 @@ function normalizeModel(modelId) {
53
66
  return modelId;
54
67
  }
55
68
 
69
+ function fileUriToPath(uri) {
70
+ try {
71
+ const parsed = new URL(uri);
72
+ if (parsed.protocol !== 'file:') return null;
73
+ let filePath = decodeURIComponent(parsed.pathname);
74
+ if (IS_WINDOWS && /^\/[A-Za-z]:/.test(filePath)) filePath = filePath.slice(1);
75
+ return filePath || null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ function base64ToBytes(b64) {
82
+ try {
83
+ return Uint8Array.from(Buffer.from(String(b64 || '').trim(), 'base64'));
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function bytesToUtf8(bytes) {
90
+ try {
91
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function readVarint(buf, offset) {
98
+ let value = 0;
99
+ let shift = 0;
100
+ let i = offset;
101
+ while (i < buf.length) {
102
+ const b = buf[i];
103
+ i += 1;
104
+ value += (b & 0x7f) * (2 ** shift);
105
+ if ((b & 0x80) === 0) return { value, offset: i };
106
+ shift += 7;
107
+ if (shift > 53) return null;
108
+ }
109
+ return null;
110
+ }
111
+
112
+ function readLengthDelimited(buf, offset) {
113
+ const lenRes = readVarint(buf, offset);
114
+ if (!lenRes) return null;
115
+ const len = lenRes.value;
116
+ const start = lenRes.offset;
117
+ const end = start + len;
118
+ if (end > buf.length) return null;
119
+ return { bytes: buf.subarray(start, end), offset: end };
120
+ }
121
+
122
+ function skipField(buf, offset, wireType) {
123
+ if (wireType === 0) {
124
+ const v = readVarint(buf, offset);
125
+ return v ? { offset: v.offset } : null;
126
+ }
127
+ if (wireType === 1) {
128
+ const end = offset + 8;
129
+ return end <= buf.length ? { offset: end } : null;
130
+ }
131
+ if (wireType === 2) {
132
+ const ld = readLengthDelimited(buf, offset);
133
+ return ld ? { offset: ld.offset } : null;
134
+ }
135
+ if (wireType === 5) {
136
+ const end = offset + 4;
137
+ return end <= buf.length ? { offset: end } : null;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ function* iterAllUtf8StringsInProto(buf, maxDepth, depth = 0) {
143
+ if (depth > maxDepth) return;
144
+ let offset = 0;
145
+ while (offset < buf.length) {
146
+ const tagRes = readVarint(buf, offset);
147
+ if (!tagRes) return;
148
+ offset = tagRes.offset;
149
+
150
+ const wireType = tagRes.value & 0x7;
151
+ if (wireType !== 2) {
152
+ const skipped = skipField(buf, offset, wireType);
153
+ if (!skipped) return;
154
+ offset = skipped.offset;
155
+ continue;
156
+ }
157
+
158
+ const ld = readLengthDelimited(buf, offset);
159
+ if (!ld) return;
160
+ offset = ld.offset;
161
+
162
+ const asString = bytesToUtf8(ld.bytes);
163
+ if (asString !== null) yield asString;
164
+ yield* iterAllUtf8StringsInProto(ld.bytes, maxDepth, depth + 1);
165
+ }
166
+ }
167
+
168
+ function parseTimestampMessage(bytes) {
169
+ let seconds = null;
170
+ let nanos = 0;
171
+ let offset = 0;
172
+
173
+ while (offset < bytes.length) {
174
+ const tagRes = readVarint(bytes, offset);
175
+ if (!tagRes) return null;
176
+ offset = tagRes.offset;
177
+
178
+ const fieldNumber = tagRes.value >>> 3;
179
+ const wireType = tagRes.value & 0x7;
180
+
181
+ if (wireType === 0) {
182
+ const valueRes = readVarint(bytes, offset);
183
+ if (!valueRes) return null;
184
+ offset = valueRes.offset;
185
+ if (fieldNumber === 1) seconds = valueRes.value;
186
+ if (fieldNumber === 2) nanos = valueRes.value;
187
+ continue;
188
+ }
189
+
190
+ const skipped = skipField(bytes, offset, wireType);
191
+ if (!skipped) return null;
192
+ offset = skipped.offset;
193
+ }
194
+
195
+ if (seconds == null) return null;
196
+ if (seconds < 946684800 || seconds > 4102444800) return null;
197
+ if (nanos >= 1e9) nanos = 0;
198
+
199
+ return Math.round((seconds * 1000) + (nanos / 1e6));
200
+ }
201
+
202
+ function findTimestampInProto(bytes, maxDepth = 2, depth = 0) {
203
+ const direct = parseTimestampMessage(bytes);
204
+ if (direct) return direct;
205
+ if (depth >= maxDepth) return null;
206
+
207
+ let offset = 0;
208
+ while (offset < bytes.length) {
209
+ const tagRes = readVarint(bytes, offset);
210
+ if (!tagRes) return null;
211
+ offset = tagRes.offset;
212
+
213
+ const wireType = tagRes.value & 0x7;
214
+ if (wireType !== 2) {
215
+ const skipped = skipField(bytes, offset, wireType);
216
+ if (!skipped) return null;
217
+ offset = skipped.offset;
218
+ continue;
219
+ }
220
+
221
+ const ld = readLengthDelimited(bytes, offset);
222
+ if (!ld) return null;
223
+ offset = ld.offset;
224
+
225
+ const nested = findTimestampInProto(ld.bytes, maxDepth, depth + 1);
226
+ if (nested) return nested;
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ function readGlobalStateValue(key) {
233
+ if (!fs.existsSync(ANTIGRAVITY_GLOBAL_STORAGE_DB)) return null;
234
+
235
+ let db = null;
236
+ try {
237
+ db = new Database(ANTIGRAVITY_GLOBAL_STORAGE_DB, { readonly: true, fileMustExist: true });
238
+ const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
239
+ if (!row) return null;
240
+ const v = row.value;
241
+ if (typeof v === 'string') return v;
242
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v).toString('utf-8');
243
+ return v == null ? null : String(v);
244
+ } catch {
245
+ return null;
246
+ } finally {
247
+ if (db) db.close();
248
+ }
249
+ }
250
+
251
+ function extractFolderFromSummaryProtoBytes(summaryProtoBytes) {
252
+ for (const s of iterAllUtf8StringsInProto(summaryProtoBytes, 6)) {
253
+ const match = s.match(/#?file:\/\/[^\s\x00-\x1f"]+/);
254
+ if (!match) continue;
255
+ let uri = match[0];
256
+ if (uri.startsWith('#')) uri = uri.slice(1);
257
+ const folder = fileUriToPath(uri);
258
+ if (folder) return folder;
259
+ }
260
+ return null;
261
+ }
262
+
263
+ function extractOfflineMetaFromSummaryProtoBytes(summaryProtoBytes) {
264
+ let title = null;
265
+ let primaryCount = 0;
266
+ let secondaryCount = 0;
267
+ const timestamps = [];
268
+
269
+ let offset = 0;
270
+ while (offset < summaryProtoBytes.length) {
271
+ const tagRes = readVarint(summaryProtoBytes, offset);
272
+ if (!tagRes) break;
273
+ offset = tagRes.offset;
274
+
275
+ const fieldNumber = tagRes.value >>> 3;
276
+ const wireType = tagRes.value & 0x7;
277
+
278
+ if (wireType === 0) {
279
+ const valueRes = readVarint(summaryProtoBytes, offset);
280
+ if (!valueRes) break;
281
+ offset = valueRes.offset;
282
+ if (fieldNumber === 2) primaryCount = valueRes.value;
283
+ if (fieldNumber === 16) secondaryCount = valueRes.value;
284
+ continue;
285
+ }
286
+
287
+ if (wireType === 2) {
288
+ const ld = readLengthDelimited(summaryProtoBytes, offset);
289
+ if (!ld) break;
290
+ offset = ld.offset;
291
+
292
+ if (fieldNumber === 1 && !title) {
293
+ const text = bytesToUtf8(ld.bytes);
294
+ if (text && text.trim()) title = text.trim();
295
+ continue;
296
+ }
297
+
298
+ if (fieldNumber === 3 || fieldNumber === 7 || fieldNumber === 10 || fieldNumber === 15) {
299
+ const ts = fieldNumber === 15 ? findTimestampInProto(ld.bytes, 2) : (parseTimestampMessage(ld.bytes) || findTimestampInProto(ld.bytes, 1));
300
+ if (ts) timestamps.push(ts);
301
+ continue;
302
+ }
303
+
304
+ continue;
305
+ }
306
+
307
+ const skipped = skipField(summaryProtoBytes, offset, wireType);
308
+ if (!skipped) break;
309
+ offset = skipped.offset;
310
+ }
311
+
312
+ const uniqueTimestamps = [...new Set(timestamps)].sort((a, b) => a - b);
313
+
314
+ return {
315
+ title,
316
+ folder: extractFolderFromSummaryProtoBytes(summaryProtoBytes),
317
+ bubbleCount: Math.max(primaryCount || 0, secondaryCount || 0),
318
+ createdAt: uniqueTimestamps[0] || null,
319
+ lastUpdatedAt: uniqueTimestamps[uniqueTimestamps.length - 1] || null,
320
+ };
321
+ }
322
+
323
+ function buildOfflineMetaMapFromGlobalStateTrajectorySummariesValue(outerValueBase64) {
324
+ const outerBytes = base64ToBytes(outerValueBase64);
325
+ if (!outerBytes) return {};
326
+
327
+ const chats = {};
328
+ let offset = 0;
329
+
330
+ while (offset < outerBytes.length) {
331
+ const tagRes = readVarint(outerBytes, offset);
332
+ if (!tagRes) break;
333
+ offset = tagRes.offset;
334
+
335
+ const fieldNumber = tagRes.value >>> 3;
336
+ const wireType = tagRes.value & 0x7;
337
+ if (fieldNumber !== 1 || wireType !== 2) {
338
+ const skipped = skipField(outerBytes, offset, wireType);
339
+ if (!skipped) break;
340
+ offset = skipped.offset;
341
+ continue;
342
+ }
343
+
344
+ const entryLd = readLengthDelimited(outerBytes, offset);
345
+ if (!entryLd) break;
346
+ offset = entryLd.offset;
347
+
348
+ let composerId = null;
349
+ let summaryBase64 = null;
350
+ let entryOffset = 0;
351
+
352
+ while (entryOffset < entryLd.bytes.length) {
353
+ const entryTag = readVarint(entryLd.bytes, entryOffset);
354
+ if (!entryTag) break;
355
+ entryOffset = entryTag.offset;
356
+
357
+ const entryField = entryTag.value >>> 3;
358
+ const entryWire = entryTag.value & 0x7;
359
+
360
+ if (entryField === 1 && entryWire === 2) {
361
+ const keyLd = readLengthDelimited(entryLd.bytes, entryOffset);
362
+ if (!keyLd) break;
363
+ entryOffset = keyLd.offset;
364
+ composerId = bytesToUtf8(keyLd.bytes);
365
+ continue;
366
+ }
367
+
368
+ if (entryField === 2 && entryWire === 2) {
369
+ const valueLd = readLengthDelimited(entryLd.bytes, entryOffset);
370
+ if (!valueLd) break;
371
+ entryOffset = valueLd.offset;
372
+
373
+ let valueOffset = 0;
374
+ while (valueOffset < valueLd.bytes.length) {
375
+ const valueTag = readVarint(valueLd.bytes, valueOffset);
376
+ if (!valueTag) break;
377
+ valueOffset = valueTag.offset;
378
+
379
+ const valueField = valueTag.value >>> 3;
380
+ const valueWire = valueTag.value & 0x7;
381
+ if (valueField === 1 && valueWire === 2) {
382
+ const summaryLd = readLengthDelimited(valueLd.bytes, valueOffset);
383
+ if (!summaryLd) break;
384
+ valueOffset = summaryLd.offset;
385
+ summaryBase64 = bytesToUtf8(summaryLd.bytes);
386
+ break;
387
+ }
388
+
389
+ const skipped = skipField(valueLd.bytes, valueOffset, valueWire);
390
+ if (!skipped) break;
391
+ valueOffset = skipped.offset;
392
+ }
393
+
394
+ continue;
395
+ }
396
+
397
+ const skipped = skipField(entryLd.bytes, entryOffset, entryWire);
398
+ if (!skipped) break;
399
+ entryOffset = skipped.offset;
400
+ }
401
+
402
+ if (!composerId || !summaryBase64) continue;
403
+ const summaryProtoBytes = base64ToBytes(summaryBase64);
404
+ if (!summaryProtoBytes) continue;
405
+
406
+ chats[composerId] = extractOfflineMetaFromSummaryProtoBytes(summaryProtoBytes);
407
+ }
408
+
409
+ return chats;
410
+ }
411
+
412
+ function getOfflineChats() {
413
+ for (const key of OFFLINE_TRAJECTORY_SUMMARIES_KEYS) {
414
+ const value = readGlobalStateValue(key);
415
+ if (!value) continue;
416
+
417
+ const map = buildOfflineMetaMapFromGlobalStateTrajectorySummariesValue(value);
418
+ const chats = Object.entries(map).map(([composerId, meta]) => ({
419
+ source: 'antigravity',
420
+ composerId,
421
+ name: meta.title || null,
422
+ createdAt: meta.createdAt || null,
423
+ lastUpdatedAt: meta.lastUpdatedAt || null,
424
+ mode: 'cascade',
425
+ folder: meta.folder || null,
426
+ encrypted: false,
427
+ bubbleCount: meta.bubbleCount || 0,
428
+ _stepCount: meta.bubbleCount || 0,
429
+ _type: 'antigravity-offline',
430
+ _dbPath: ANTIGRAVITY_GLOBAL_STORAGE_DB,
431
+ _rawSource: 'offline-global-state',
432
+ }));
433
+
434
+ if (chats.length > 0) {
435
+ return chats.sort((a, b) => (b.lastUpdatedAt || b.createdAt || 0) - (a.lastUpdatedAt || a.createdAt || 0));
436
+ }
437
+ }
438
+
439
+ return [];
440
+ }
441
+
442
+ function mergeChats(liveChats, offlineChats) {
443
+ if (liveChats.length === 0) return offlineChats;
444
+ if (offlineChats.length === 0) return liveChats;
445
+
446
+ const map = new Map();
447
+
448
+ for (const chat of offlineChats) {
449
+ map.set(chat.composerId, { ...chat });
450
+ }
451
+
452
+ for (const chat of liveChats) {
453
+ const existing = map.get(chat.composerId);
454
+ if (!existing) {
455
+ map.set(chat.composerId, chat);
456
+ continue;
457
+ }
458
+
459
+ map.set(chat.composerId, {
460
+ ...existing,
461
+ ...chat,
462
+ name: chat.name || existing.name,
463
+ createdAt: chat.createdAt || existing.createdAt,
464
+ lastUpdatedAt: chat.lastUpdatedAt || existing.lastUpdatedAt,
465
+ folder: chat.folder || existing.folder,
466
+ bubbleCount: chat.bubbleCount || existing.bubbleCount,
467
+ _stepCount: chat._stepCount || existing._stepCount,
468
+ });
469
+ }
470
+
471
+ return Array.from(map.values()).sort((a, b) => (b.lastUpdatedAt || b.createdAt || 0) - (a.lastUpdatedAt || a.createdAt || 0));
472
+ }
473
+
474
+
56
475
  // ============================================================
57
476
  // Cross-platform process utilities
58
477
  // ============================================================
@@ -62,16 +481,19 @@ const IS_WINDOWS = process.platform === 'win32';
62
481
  function getProcessList() {
63
482
  try {
64
483
  if (IS_WINDOWS) {
65
- const output = execSync('wmic process get CommandLine,ProcessId /format:csv', {
484
+ // Use PowerShell Get-Process (WMIC is deprecated in Windows 10/11)
485
+ const output = execFileSync('powershell', ['-Command', 'Get-Process | Select-Object Id, Path, CommandLine | ConvertTo-Csv -NoTypeInformation'], {
66
486
  encoding: 'utf-8',
67
487
  maxBuffer: 10 * 1024 * 1024,
68
488
  });
489
+ // Parse CSV: skip header
69
490
  const lines = output.split('\n').slice(1);
70
491
  return lines.map(line => {
71
492
  const parts = line.split(',');
72
- if (parts.length < 2) return null;
73
- const commandLine = parts.slice(0, -1).join(',').trim().replace(/^"|"$/g, '');
74
- const pid = parts[parts.length - 1].trim();
493
+ if (parts.length < 3) return null;
494
+ const pid = parts[0].trim().replace(/^"|"$/g, '');
495
+ const commandLine = parts[2].trim().replace(/^"|"$/g, '');
496
+ if (!pid || !commandLine) return null;
75
497
  return { commandLine, pid };
76
498
  }).filter(Boolean);
77
499
  } else {
@@ -90,7 +512,8 @@ function getProcessList() {
90
512
  function getListeningPorts(pid) {
91
513
  try {
92
514
  if (IS_WINDOWS) {
93
- const output = execSync(`netstat -ano | findstr ${pid}`, {
515
+ // Use PowerShell to get netstat output and filter by PID
516
+ const output = execFileSync('powershell', ['-Command', `netstat -ano | Select-String "${pid}$"`], {
94
517
  encoding: 'utf-8',
95
518
  maxBuffer: 10 * 1024 * 1024,
96
519
  });
@@ -125,9 +548,10 @@ function getListeningPorts(pid) {
125
548
  // ============================================================
126
549
 
127
550
  let _lsCache = null;
551
+ let _lsCacheCheckedAt = 0;
128
552
 
129
553
  function findLanguageServer() {
130
- if (_lsCache !== null) return _lsCache;
554
+ if (_lsCache !== null && (Date.now() - _lsCacheCheckedAt) < 10000) return _lsCache || null;
131
555
 
132
556
  const serverProcessName = IS_WINDOWS
133
557
  ? 'language_server_windows'
@@ -158,10 +582,12 @@ function findLanguageServer() {
158
582
  }
159
583
 
160
584
  _lsCache = { port, csrf: csrfMatch[1], pid };
585
+ _lsCacheCheckedAt = Date.now();
161
586
  return _lsCache;
162
587
  }
163
588
 
164
589
  _lsCache = false;
590
+ _lsCacheCheckedAt = Date.now();
165
591
  return null;
166
592
  }
167
593
 
@@ -197,30 +623,31 @@ const name = 'antigravity';
197
623
 
198
624
  function getChats() {
199
625
  const resp = callRpc('GetAllCascadeTrajectories', {});
200
- if (!resp || !resp.trajectorySummaries) return [];
201
-
202
- const chats = [];
203
- for (const [cascadeId, summary] of Object.entries(resp.trajectorySummaries)) {
204
- const ws = (summary.workspaces || [])[0];
205
- const folder = ws?.workspaceFolderAbsoluteUri?.replace('file://', '') || null;
206
- const rawModel = summary.lastGeneratorModelUid;
207
- chats.push({
208
- source: 'antigravity',
209
- composerId: cascadeId,
210
- name: summary.summary || null,
211
- createdAt: summary.createdTime ? new Date(summary.createdTime).getTime() : null,
212
- lastUpdatedAt: summary.lastModifiedTime ? new Date(summary.lastModifiedTime).getTime() : null,
213
- mode: 'cascade',
214
- folder,
215
- encrypted: false,
216
- bubbleCount: summary.stepCount || 0,
217
- _stepCount: summary.stepCount,
218
- _model: rawModel ? normalizeModel(rawModel) : rawModel,
219
- _rawModel: rawModel,
220
- });
626
+ const liveChats = [];
627
+
628
+ if (resp && resp.trajectorySummaries) {
629
+ for (const [cascadeId, summary] of Object.entries(resp.trajectorySummaries)) {
630
+ const ws = (summary.workspaces || [])[0];
631
+ const folder = ws?.workspaceFolderAbsoluteUri?.replace('file://', '') || null;
632
+ const rawModel = summary.lastGeneratorModelUid;
633
+ liveChats.push({
634
+ source: 'antigravity',
635
+ composerId: cascadeId,
636
+ name: summary.summary || null,
637
+ createdAt: summary.createdTime ? new Date(summary.createdTime).getTime() : null,
638
+ lastUpdatedAt: summary.lastModifiedTime ? new Date(summary.lastModifiedTime).getTime() : null,
639
+ mode: 'cascade',
640
+ folder,
641
+ encrypted: false,
642
+ bubbleCount: summary.stepCount || 0,
643
+ _stepCount: summary.stepCount,
644
+ _model: rawModel ? normalizeModel(rawModel) : rawModel,
645
+ _rawModel: rawModel,
646
+ });
647
+ }
221
648
  }
222
649
 
223
- return chats;
650
+ return mergeChats(liveChats, getOfflineChats());
224
651
  }
225
652
 
226
653
  function getSteps(chat) {
@@ -500,8 +927,54 @@ function getUsage() {
500
927
  };
501
928
  }
502
929
 
503
- function resetCache() { _lsCache = null; _modelMap = null; }
930
+ function resetCache() { _lsCache = null; _lsCacheCheckedAt = 0; _modelMap = null; }
504
931
 
505
932
  const labels = { 'antigravity': 'Antigravity' };
506
933
 
507
- module.exports = { name, labels, getChats, getMessages, resetCache, getUsage };
934
+ function getArtifacts(folder) {
935
+ const { scanArtifacts } = require('./base');
936
+ const artifacts = folder ? scanArtifacts(folder, {
937
+ editor: 'antigravity',
938
+ label: 'Antigravity',
939
+ files: [],
940
+ dirs: ['.gemini/skills', '.gemini/rules', '.gemini/plans', '.gemini/workflows'],
941
+ }) : [];
942
+
943
+ // Add brain artifacts (task.md, implementation_plan.md, walkthrough.md) per session
944
+ if (fs.existsSync(ANTIGRAVITY_BRAIN_DIR)) {
945
+ try {
946
+ const sessions = fs.readdirSync(ANTIGRAVITY_BRAIN_DIR);
947
+ const brainFileNames = ['task.md', 'implementation_plan.md', 'walkthrough.md'];
948
+ for (const sessionId of sessions) {
949
+ const sessionDir = path.join(ANTIGRAVITY_BRAIN_DIR, sessionId);
950
+ try {
951
+ if (!fs.statSync(sessionDir).isDirectory()) continue;
952
+ } catch { continue; }
953
+ for (const fileName of brainFileNames) {
954
+ const filePath = path.join(sessionDir, fileName);
955
+ if (!fs.existsSync(filePath)) continue;
956
+ try {
957
+ const stat = fs.statSync(filePath);
958
+ const content = fs.readFileSync(filePath, 'utf-8');
959
+ if (!content.trim()) continue;
960
+ const lines = content.split('\n').length;
961
+ artifacts.push({
962
+ name: fileName,
963
+ path: filePath,
964
+ relativePath: `brain/${sessionId.slice(0, 8)}/${fileName}`,
965
+ size: stat.size,
966
+ lines,
967
+ modifiedAt: stat.mtimeMs,
968
+ editor: 'antigravity',
969
+ editorLabel: 'Antigravity',
970
+ });
971
+ } catch { /* skip unreadable */ }
972
+ }
973
+ }
974
+ } catch { /* skip if brain dir unreadable */ }
975
+ }
976
+
977
+ return artifacts;
978
+ }
979
+
980
+ module.exports = { name, labels, getChats, getMessages, resetCache, getUsage, getArtifacts };