@chainlesschain/personal-data-hub 0.2.3 → 0.3.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/__tests__/adapters/browser-history-chrome.test.js +377 -0
- package/__tests__/adapters/browser-history-edge.test.js +159 -0
- package/__tests__/adapters/git-activity.test.js +216 -0
- package/__tests__/adapters/local-files.test.js +264 -0
- package/__tests__/adapters/shell-history.test.js +180 -0
- package/__tests__/adapters/system-data-android.test.js +104 -3
- package/__tests__/adapters/vscode.test.js +299 -0
- package/__tests__/adapters/win-recent.test.js +192 -0
- package/__tests__/analysis.test.js +841 -2
- package/__tests__/categories.test.js +92 -0
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +146 -0
- package/__tests__/entity-resolver-vault.test.js +5 -2
- package/__tests__/integration/local-data-adapters-pipeline.test.js +373 -0
- package/__tests__/longtail-adapters.test.js +7 -2
- package/__tests__/query-parser.test.js +66 -0
- package/__tests__/registry.test.js +114 -0
- package/__tests__/sidecar-contacts-cross-validate.test.js +24 -1
- package/__tests__/sidecar-supervisor.test.js +9 -1
- package/__tests__/social-kuaishou-snapshot.test.js +55 -2
- package/__tests__/social-toutiao-snapshot.test.js +54 -2
- package/__tests__/vault-search-helpers.test.js +104 -0
- package/__tests__/vault-search.test.js +423 -0
- package/__tests__/vault.test.js +77 -3
- package/lib/adapters/browser-history-chrome/adapter.js +247 -0
- package/lib/adapters/browser-history-chrome/bookmarks-reader.js +79 -0
- package/lib/adapters/browser-history-chrome/chrome-db-reader.js +223 -0
- package/lib/adapters/browser-history-chrome/index.js +23 -0
- package/lib/adapters/browser-history-edge/adapter.js +34 -0
- package/lib/adapters/browser-history-edge/index.js +13 -0
- package/lib/adapters/git-activity/adapter.js +155 -0
- package/lib/adapters/git-activity/git-reader.js +125 -0
- package/lib/adapters/git-activity/index.js +17 -0
- package/lib/adapters/local-files/adapter.js +149 -0
- package/lib/adapters/local-files/file-walker.js +125 -0
- package/lib/adapters/local-files/index.js +18 -0
- package/lib/adapters/shell-history/adapter.js +137 -0
- package/lib/adapters/shell-history/index.js +17 -0
- package/lib/adapters/shell-history/shell-reader.js +100 -0
- package/lib/adapters/social-kuaishou/index.js +57 -1
- package/lib/adapters/social-toutiao/index.js +59 -1
- package/lib/adapters/system-data-android/adapter.js +220 -3
- package/lib/adapters/vscode/adapter.js +285 -0
- package/lib/adapters/vscode/index.js +18 -0
- package/lib/adapters/vscode/vscode-reader.js +191 -0
- package/lib/adapters/win-recent/adapter.js +150 -0
- package/lib/adapters/win-recent/index.js +16 -0
- package/lib/adapters/win-recent/win-recent-reader.js +72 -0
- package/lib/analysis.js +227 -9
- package/lib/categories.js +101 -0
- package/lib/index.js +61 -0
- package/lib/migrations.js +146 -0
- package/lib/query-parser.js +74 -0
- package/lib/registry.js +162 -0
- package/lib/vault.js +363 -2
- package/package.json +2 -1
- package/scripts/run-native-tests-sandbox.sh +53 -0
|
@@ -23,11 +23,18 @@ const {
|
|
|
23
23
|
ENTITY_TYPES,
|
|
24
24
|
PERSON_SUBTYPES,
|
|
25
25
|
ITEM_SUBTYPES,
|
|
26
|
+
EVENT_SUBTYPES,
|
|
26
27
|
CAPTURED_BY,
|
|
27
28
|
} = require("../../constants");
|
|
28
29
|
|
|
29
30
|
const NAME = "system-data-android";
|
|
30
|
-
|
|
31
|
+
// v0.3.0 (2026-05-24): added kind="media-file" via bridge mode
|
|
32
|
+
// (host-adb-bridge media.list across 5 /sdcard categories). Metadata
|
|
33
|
+
// only — path/size/mtime/ext, no file content.
|
|
34
|
+
// v0.2.0 (2026-05-24): added kind="sms" + kind="call" via bridge mode.
|
|
35
|
+
// Snapshot mode still v1 schema — sms/calls/media only land via
|
|
36
|
+
// bridge path until Android snapshot writer is updated to include them.
|
|
37
|
+
const VERSION = "0.3.0";
|
|
31
38
|
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
32
39
|
|
|
33
40
|
// Stable per-source originalId — registry.putRawEvent rejects null originalId
|
|
@@ -48,6 +55,25 @@ function appOriginalId(a) {
|
|
|
48
55
|
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
49
56
|
return `android-app:${k}`;
|
|
50
57
|
}
|
|
58
|
+
function smsOriginalId(s) {
|
|
59
|
+
// Stable across re-syncs: use SMS _id from the system content provider.
|
|
60
|
+
const k = (s && s.id != null && String(s.id)) ||
|
|
61
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
62
|
+
return `android-sms:${k}`;
|
|
63
|
+
}
|
|
64
|
+
function callOriginalId(c) {
|
|
65
|
+
// Stable across re-syncs: use call_log _id from the system content provider.
|
|
66
|
+
const k = (c && c.id != null && String(c.id)) ||
|
|
67
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
68
|
+
return `android-call:${k}`;
|
|
69
|
+
}
|
|
70
|
+
function mediaOriginalId(m) {
|
|
71
|
+
// Full filesystem path is stable as long as the file isn't moved/renamed.
|
|
72
|
+
// Path is unique within the device.
|
|
73
|
+
const k = (m && typeof m.path === "string" && m.path) ||
|
|
74
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
75
|
+
return `android-media:${k}`;
|
|
76
|
+
}
|
|
51
77
|
|
|
52
78
|
class SystemDataAndroidAdapter {
|
|
53
79
|
constructor(opts = {}) {
|
|
@@ -56,6 +82,9 @@ class SystemDataAndroidAdapter {
|
|
|
56
82
|
this.capabilities = [
|
|
57
83
|
"sync:android-content-provider",
|
|
58
84
|
"sync:android-package-manager",
|
|
85
|
+
"sync:android-sms",
|
|
86
|
+
"sync:android-call-log",
|
|
87
|
+
"sync:android-media-files",
|
|
59
88
|
];
|
|
60
89
|
this.extractMode = "device-pull";
|
|
61
90
|
this.rateLimits = { perDay: 24 };
|
|
@@ -63,10 +92,20 @@ class SystemDataAndroidAdapter {
|
|
|
63
92
|
fields: [
|
|
64
93
|
"contacts:displayName,phones,emails,starred,organization,photoUri",
|
|
65
94
|
"installed_apps:packageName,label,versionName,versionCode,firstInstallTime,lastUpdateTime,isSystem",
|
|
95
|
+
"sms:id,address,body,date,dateSent,type,threadId,read,subject",
|
|
96
|
+
"callLog:id,number,name,duration,date,type,geocoded",
|
|
97
|
+
// Media is metadata-only — file content never leaves the device.
|
|
98
|
+
"media:path,size,mtimeMs,ext,category(photos|pictures|videos|downloads|documents)",
|
|
66
99
|
],
|
|
67
|
-
sensitivity: "
|
|
100
|
+
sensitivity: "high",
|
|
68
101
|
legalGate: false,
|
|
69
|
-
defaultInclude: {
|
|
102
|
+
defaultInclude: {
|
|
103
|
+
contacts: true,
|
|
104
|
+
apps: true,
|
|
105
|
+
sms: true,
|
|
106
|
+
calls: true,
|
|
107
|
+
media: { photos: true, pictures: true, videos: true, downloads: true, documents: true },
|
|
108
|
+
},
|
|
70
109
|
};
|
|
71
110
|
|
|
72
111
|
// _deps for test injection — mirrors the pattern in cli-dev.md so test
|
|
@@ -191,6 +230,68 @@ class SystemDataAndroidAdapter {
|
|
|
191
230
|
emitted += 1;
|
|
192
231
|
}
|
|
193
232
|
}
|
|
233
|
+
|
|
234
|
+
const includeSms = opts.include?.sms !== false;
|
|
235
|
+
if (includeSms) {
|
|
236
|
+
const res = await bridge.invoke("sms.query", {
|
|
237
|
+
since: Number.isInteger(opts.since) ? opts.since : undefined,
|
|
238
|
+
});
|
|
239
|
+
const arr = Array.isArray(res) ? res : Array.isArray(res?.sms) ? res.sms : [];
|
|
240
|
+
for (const s of arr) {
|
|
241
|
+
if (emitted >= limit) return;
|
|
242
|
+
yield {
|
|
243
|
+
kind: "sms",
|
|
244
|
+
originalId: smsOriginalId(s),
|
|
245
|
+
capturedAt,
|
|
246
|
+
payload: s,
|
|
247
|
+
};
|
|
248
|
+
emitted += 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const includeCalls = opts.include?.calls !== false;
|
|
253
|
+
if (includeCalls) {
|
|
254
|
+
const res = await bridge.invoke("call.query", {
|
|
255
|
+
since: Number.isInteger(opts.since) ? opts.since : undefined,
|
|
256
|
+
});
|
|
257
|
+
const arr = Array.isArray(res) ? res : Array.isArray(res?.calls) ? res.calls : [];
|
|
258
|
+
for (const c of arr) {
|
|
259
|
+
if (emitted >= limit) return;
|
|
260
|
+
yield {
|
|
261
|
+
kind: "call",
|
|
262
|
+
originalId: callOriginalId(c),
|
|
263
|
+
capturedAt,
|
|
264
|
+
payload: c,
|
|
265
|
+
};
|
|
266
|
+
emitted += 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Media files — metadata only (path, size, mtime, ext). Never reads
|
|
271
|
+
// file content off the device. UI uses per-category include keys so
|
|
272
|
+
// a privacy-conscious user can keep photos but skip downloads, etc.
|
|
273
|
+
const MEDIA_CATEGORIES = ["photos", "pictures", "videos", "downloads", "documents"];
|
|
274
|
+
for (const cat of MEDIA_CATEGORIES) {
|
|
275
|
+
// Per-category include key: include.media.photos, include.media.videos, ...
|
|
276
|
+
// Top-level `include.media === false` disables ALL media in one switch.
|
|
277
|
+
if (opts.include?.media === false) break;
|
|
278
|
+
if (opts.include?.media?.[cat] === false) continue;
|
|
279
|
+
const res = await bridge.invoke("media.list", {
|
|
280
|
+
category: cat,
|
|
281
|
+
since: Number.isInteger(opts.since) ? opts.since : undefined,
|
|
282
|
+
});
|
|
283
|
+
const arr = Array.isArray(res) ? res : Array.isArray(res?.files) ? res.files : [];
|
|
284
|
+
for (const f of arr) {
|
|
285
|
+
if (emitted >= limit) return;
|
|
286
|
+
yield {
|
|
287
|
+
kind: "media-file",
|
|
288
|
+
originalId: mediaOriginalId(f),
|
|
289
|
+
capturedAt,
|
|
290
|
+
payload: f,
|
|
291
|
+
};
|
|
292
|
+
emitted += 1;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
194
295
|
}
|
|
195
296
|
|
|
196
297
|
async *_syncViaSnapshot(opts) {
|
|
@@ -336,6 +437,122 @@ class SystemDataAndroidAdapter {
|
|
|
336
437
|
};
|
|
337
438
|
}
|
|
338
439
|
|
|
440
|
+
if (raw.kind === "sms") {
|
|
441
|
+
const p = raw.payload || {};
|
|
442
|
+
// SMS type from Android SDK Telephony.Sms (Inbox.MESSAGE_TYPE_*):
|
|
443
|
+
// 1 INBOX, 2 SENT, 3 DRAFT, 4 OUTBOX, 5 FAILED, 6 QUEUED
|
|
444
|
+
const direction = p.type === 2 || p.type === 4 ? "out" : "in";
|
|
445
|
+
const eventId = `event-android-sms-${p.id || raw.capturedAt}`;
|
|
446
|
+
const occurredAt = Number.isInteger(p.date) ? p.date : raw.capturedAt;
|
|
447
|
+
const bodyText = typeof p.body === "string" ? p.body : "";
|
|
448
|
+
const event = {
|
|
449
|
+
id: eventId,
|
|
450
|
+
type: ENTITY_TYPES.EVENT,
|
|
451
|
+
subtype: EVENT_SUBTYPES.MESSAGE,
|
|
452
|
+
occurredAt,
|
|
453
|
+
ingestedAt,
|
|
454
|
+
source: source(`android-sms:${p.id || raw.capturedAt}`),
|
|
455
|
+
actor: direction === "in" ? p.address : "self",
|
|
456
|
+
// Participants on the OTHER side of the message.
|
|
457
|
+
participants: p.address ? [p.address] : [],
|
|
458
|
+
// Validator (lib/schemas.js validateEvent) requires `content` to be
|
|
459
|
+
// a plain object — title/text/etc go INSIDE this object, not on the
|
|
460
|
+
// event root.
|
|
461
|
+
content: {
|
|
462
|
+
title: bodyText.length > 0
|
|
463
|
+
? (bodyText.length > 80 ? bodyText.substring(0, 80) + "…" : bodyText)
|
|
464
|
+
: "(空短信)",
|
|
465
|
+
text: bodyText,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
const extra = { direction, threadId: p.threadId };
|
|
469
|
+
if (typeof p.dateSent === "number") extra.dateSent = p.dateSent;
|
|
470
|
+
if (typeof p.read === "boolean") extra.read = p.read;
|
|
471
|
+
if (typeof p.subject === "string" && p.subject.length > 0) extra.subject = p.subject;
|
|
472
|
+
if (Number.isInteger(p.type)) extra.smsType = p.type;
|
|
473
|
+
event.extra = extra;
|
|
474
|
+
|
|
475
|
+
return { events: [event], persons: [], places: [], items: [], topics: [] };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (raw.kind === "call") {
|
|
479
|
+
const p = raw.payload || {};
|
|
480
|
+
// Call type from Android SDK CallLog.Calls.TYPE:
|
|
481
|
+
// 1 INCOMING, 2 OUTGOING, 3 MISSED, 4 VOICEMAIL, 5 REJECTED, 6 BLOCKED
|
|
482
|
+
const direction = p.type === 2 ? "out" : "in";
|
|
483
|
+
const eventId = `event-android-call-${p.id || raw.capturedAt}`;
|
|
484
|
+
const occurredAt = Number.isInteger(p.date) ? p.date : raw.capturedAt;
|
|
485
|
+
const callTypeName =
|
|
486
|
+
p.type === 1 ? "incoming" :
|
|
487
|
+
p.type === 2 ? "outgoing" :
|
|
488
|
+
p.type === 3 ? "missed" :
|
|
489
|
+
p.type === 4 ? "voicemail" :
|
|
490
|
+
p.type === 5 ? "rejected" :
|
|
491
|
+
p.type === 6 ? "blocked" : "unknown";
|
|
492
|
+
const titleName =
|
|
493
|
+
(typeof p.name === "string" && p.name.trim().length > 0) ? p.name.trim() : (p.number || "未知号码");
|
|
494
|
+
const title =
|
|
495
|
+
`${callTypeName === "missed" ? "未接 " : ""}${callTypeName === "outgoing" ? "拨打 " : ""}${callTypeName === "incoming" ? "来电 " : ""}${titleName}`;
|
|
496
|
+
const event = {
|
|
497
|
+
id: eventId,
|
|
498
|
+
type: ENTITY_TYPES.EVENT,
|
|
499
|
+
subtype: EVENT_SUBTYPES.CALL,
|
|
500
|
+
occurredAt,
|
|
501
|
+
ingestedAt,
|
|
502
|
+
source: source(`android-call:${p.id || raw.capturedAt}`),
|
|
503
|
+
actor: direction === "in" ? p.number : "self",
|
|
504
|
+
participants: p.number ? [p.number] : [],
|
|
505
|
+
// Schema-required `content` object — title goes here, not on root.
|
|
506
|
+
content: { title },
|
|
507
|
+
};
|
|
508
|
+
if (Number.isInteger(p.duration) && p.duration > 0) {
|
|
509
|
+
event.durationMs = p.duration * 1000;
|
|
510
|
+
}
|
|
511
|
+
const extra = { direction, callType: callTypeName };
|
|
512
|
+
if (Number.isInteger(p.type)) extra.androidCallType = p.type;
|
|
513
|
+
if (typeof p.geocoded === "string" && p.geocoded.length > 0) extra.geocoded = p.geocoded;
|
|
514
|
+
if (typeof p.name === "string" && p.name.length > 0) extra.name = p.name;
|
|
515
|
+
event.extra = extra;
|
|
516
|
+
|
|
517
|
+
return { events: [event], persons: [], places: [], items: [], topics: [] };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (raw.kind === "media-file") {
|
|
521
|
+
const p = raw.payload || {};
|
|
522
|
+
const path = typeof p.path === "string" ? p.path : "";
|
|
523
|
+
const fileName = path.includes("/") ? path.substring(path.lastIndexOf("/") + 1) : path;
|
|
524
|
+
// Category → item subtype + category string
|
|
525
|
+
let subtype = ITEM_SUBTYPES.OTHER;
|
|
526
|
+
let category = "media";
|
|
527
|
+
if (p.category === "photos" || p.category === "pictures" || p.category === "videos") {
|
|
528
|
+
subtype = ITEM_SUBTYPES.MEDIA;
|
|
529
|
+
category = p.category;
|
|
530
|
+
} else if (p.category === "documents") {
|
|
531
|
+
subtype = ITEM_SUBTYPES.DOCUMENT;
|
|
532
|
+
category = "documents";
|
|
533
|
+
} else if (p.category === "downloads") {
|
|
534
|
+
subtype = ITEM_SUBTYPES.OTHER;
|
|
535
|
+
category = "downloads";
|
|
536
|
+
}
|
|
537
|
+
const item = {
|
|
538
|
+
id: `item-android-media-${path}`,
|
|
539
|
+
type: ENTITY_TYPES.ITEM,
|
|
540
|
+
subtype,
|
|
541
|
+
name: fileName || "(无名)",
|
|
542
|
+
category,
|
|
543
|
+
ingestedAt,
|
|
544
|
+
source: source(`android-media:${path}`),
|
|
545
|
+
extra: {
|
|
546
|
+
path,
|
|
547
|
+
size: Number.isInteger(p.size) ? p.size : null,
|
|
548
|
+
mtimeMs: Number.isInteger(p.mtimeMs) ? p.mtimeMs : null,
|
|
549
|
+
ext: typeof p.ext === "string" ? p.ext : null,
|
|
550
|
+
androidCategory: p.category,
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
return { events: [], persons: [], places: [], items: [item], topics: [] };
|
|
554
|
+
}
|
|
555
|
+
|
|
339
556
|
throw new Error(`system-data-android.normalize: unknown raw.kind=${raw.kind}`);
|
|
340
557
|
}
|
|
341
558
|
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// VSCodeAdapter — pulls VSCode workspace history + global terminal history
|
|
4
|
+
// from on-disk state. Desktop-local, zero network, no extension required.
|
|
5
|
+
//
|
|
6
|
+
// Sources (all under `%APPDATA%\Code\` on Win, equivalent on macOS/Linux):
|
|
7
|
+
// - User/workspaceStorage/<hash>/workspace.json → each opened project
|
|
8
|
+
// - User/globalStorage/state.vscdb → terminal command history
|
|
9
|
+
//
|
|
10
|
+
// Yields:
|
|
11
|
+
// kind="workspace" → Item(LINK, category="code-project")
|
|
12
|
+
// kind="terminal-command" → Event(OTHER, content.title=cmd[0..80])
|
|
13
|
+
// kind="terminal-dir" → Event(OTHER, content.title=cd <dir>)
|
|
14
|
+
//
|
|
15
|
+
// Caveat: terminal history has NO per-entry timestamp in VSCode — only a
|
|
16
|
+
// single "snapshot updated" ts. We anchor every command/dir to that ts and
|
|
17
|
+
// add `sourceIndex` to extra so callers can reconstruct order. Re-syncing
|
|
18
|
+
// after a new command lands gives that command (and only that command) a
|
|
19
|
+
// fresher snapshot ts.
|
|
20
|
+
|
|
21
|
+
const path = require("node:path");
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
ENTITY_TYPES,
|
|
25
|
+
EVENT_SUBTYPES,
|
|
26
|
+
ITEM_SUBTYPES,
|
|
27
|
+
CAPTURED_BY,
|
|
28
|
+
} = require("../../constants");
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
defaultVscodeRoot,
|
|
32
|
+
decodeFileUri,
|
|
33
|
+
readWorkspaces,
|
|
34
|
+
readTerminalHistory,
|
|
35
|
+
} = require("./vscode-reader");
|
|
36
|
+
|
|
37
|
+
const NAME = "vscode";
|
|
38
|
+
const VERSION = "0.1.0";
|
|
39
|
+
|
|
40
|
+
class VSCodeAdapter {
|
|
41
|
+
constructor(opts = {}) {
|
|
42
|
+
this.name = NAME;
|
|
43
|
+
this.version = VERSION;
|
|
44
|
+
this.capabilities = [
|
|
45
|
+
"sync:vscode-workspace-storage",
|
|
46
|
+
"sync:vscode-globalstorage-sqlite",
|
|
47
|
+
];
|
|
48
|
+
this.extractMode = "file-import";
|
|
49
|
+
this.rateLimits = { perDay: 96 };
|
|
50
|
+
this.dataDisclosure = {
|
|
51
|
+
fields: [
|
|
52
|
+
"workspaces:hash,folderUri,folderPath,lastOpenedMs",
|
|
53
|
+
"terminal-commands:command,shellType,sourceIndex,snapshotTs",
|
|
54
|
+
"terminal-dirs:dir,shellType,sourceIndex,snapshotTs",
|
|
55
|
+
],
|
|
56
|
+
sensitivity: "high",
|
|
57
|
+
legalGate: false,
|
|
58
|
+
defaultInclude: { workspaces: true, terminal: true },
|
|
59
|
+
};
|
|
60
|
+
this._deps = {
|
|
61
|
+
fs: require("node:fs"),
|
|
62
|
+
defaultRoot: defaultVscodeRoot,
|
|
63
|
+
};
|
|
64
|
+
this._rootOverride = typeof opts.vscodeRoot === "string" ? opts.vscodeRoot : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_resolveRoot(opts) {
|
|
68
|
+
if (typeof opts?.vscodeRoot === "string" && opts.vscodeRoot.length > 0) {
|
|
69
|
+
return opts.vscodeRoot;
|
|
70
|
+
}
|
|
71
|
+
if (this._rootOverride) return this._rootOverride;
|
|
72
|
+
return this._deps.defaultRoot();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async authenticate(ctx = {}) {
|
|
76
|
+
const root = this._resolveRoot(ctx);
|
|
77
|
+
if (!root) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: "VSCODE_ROOT_UNRESOLVED",
|
|
81
|
+
message: "no default VSCode root on this platform; pass opts.vscodeRoot",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const wsRoot = path.join(root, "User", "workspaceStorage");
|
|
85
|
+
const stateDb = path.join(root, "User", "globalStorage", "state.vscdb");
|
|
86
|
+
const wsExists = this._deps.fs.existsSync(wsRoot);
|
|
87
|
+
const stateExists = this._deps.fs.existsSync(stateDb);
|
|
88
|
+
if (!wsExists && !stateExists) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
reason: "VSCODE_NOT_FOUND",
|
|
92
|
+
message: `no VSCode state at ${root} — install VS Code / open it at least once, or pass opts.vscodeRoot`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
mode: "file-import",
|
|
98
|
+
vscodeRoot: root,
|
|
99
|
+
hasWorkspaces: wsExists,
|
|
100
|
+
hasTerminalHistory: stateExists,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async healthCheck() {
|
|
105
|
+
const root = this._resolveRoot({});
|
|
106
|
+
const ok =
|
|
107
|
+
!!root &&
|
|
108
|
+
(this._deps.fs.existsSync(path.join(root, "User", "workspaceStorage")) ||
|
|
109
|
+
this._deps.fs.existsSync(path.join(root, "User", "globalStorage", "state.vscdb")));
|
|
110
|
+
return { ok, lastChecked: Date.now() };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async *sync(opts = {}) {
|
|
114
|
+
const root = this._resolveRoot(opts);
|
|
115
|
+
if (!root) {
|
|
116
|
+
throw new Error("vscode.sync: no VSCode root resolved — pass opts.vscodeRoot");
|
|
117
|
+
}
|
|
118
|
+
const includeWorkspaces = opts.include?.workspaces !== false;
|
|
119
|
+
const includeTerminal = opts.include?.terminal !== false;
|
|
120
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
121
|
+
const capturedAt = Date.now();
|
|
122
|
+
let emitted = 0;
|
|
123
|
+
|
|
124
|
+
if (includeWorkspaces) {
|
|
125
|
+
for (const w of readWorkspaces(root, { fs: this._deps.fs, since: opts.since })) {
|
|
126
|
+
if (emitted >= limit) return;
|
|
127
|
+
yield {
|
|
128
|
+
kind: "workspace",
|
|
129
|
+
originalId: `vscode-workspace:${w.hash}`,
|
|
130
|
+
capturedAt,
|
|
131
|
+
payload: w,
|
|
132
|
+
};
|
|
133
|
+
emitted += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (includeTerminal) {
|
|
138
|
+
const hist = readTerminalHistory(root, { fs: this._deps.fs });
|
|
139
|
+
const cmdTs = Number.isInteger(hist.commandsTimestampMs)
|
|
140
|
+
? hist.commandsTimestampMs
|
|
141
|
+
: capturedAt;
|
|
142
|
+
const dirTs = Number.isInteger(hist.dirsTimestampMs)
|
|
143
|
+
? hist.dirsTimestampMs
|
|
144
|
+
: capturedAt;
|
|
145
|
+
if (opts.include?.terminalCommands !== false) {
|
|
146
|
+
for (const c of hist.commands) {
|
|
147
|
+
if (emitted >= limit) return;
|
|
148
|
+
if (Number.isInteger(opts.since) && cmdTs < opts.since) break;
|
|
149
|
+
yield {
|
|
150
|
+
kind: "terminal-command",
|
|
151
|
+
// Index disambiguates entries that re-occur with identical command
|
|
152
|
+
// text — keeps registry.putRawEvent's UNIQUE(source.originalId) happy.
|
|
153
|
+
originalId: `vscode-terminal-cmd:${c.sourceIndex}:${hashCommand(c.value)}`,
|
|
154
|
+
capturedAt,
|
|
155
|
+
payload: { ...c, snapshotTs: cmdTs },
|
|
156
|
+
};
|
|
157
|
+
emitted += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (opts.include?.terminalDirs !== false) {
|
|
161
|
+
for (const d of hist.dirs) {
|
|
162
|
+
if (emitted >= limit) return;
|
|
163
|
+
if (Number.isInteger(opts.since) && dirTs < opts.since) break;
|
|
164
|
+
yield {
|
|
165
|
+
kind: "terminal-dir",
|
|
166
|
+
originalId: `vscode-terminal-dir:${d.sourceIndex}:${hashCommand(d.value)}`,
|
|
167
|
+
capturedAt,
|
|
168
|
+
payload: { ...d, snapshotTs: dirTs },
|
|
169
|
+
};
|
|
170
|
+
emitted += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
normalize(raw) {
|
|
177
|
+
const ingestedAt = Date.now();
|
|
178
|
+
const source = (originalId) => ({
|
|
179
|
+
adapter: NAME,
|
|
180
|
+
adapterVersion: VERSION,
|
|
181
|
+
capturedAt: raw.capturedAt,
|
|
182
|
+
capturedBy: CAPTURED_BY.SQLITE,
|
|
183
|
+
originalId,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (raw.kind === "workspace") {
|
|
187
|
+
const p = raw.payload || {};
|
|
188
|
+
const uri = p.folderUri || p.workspaceUri || `vscode-hash:${p.hash}`;
|
|
189
|
+
const name =
|
|
190
|
+
(p.folderPath && p.folderPath.split(/[\\/]/).filter(Boolean).pop()) ||
|
|
191
|
+
decodeFileUri(uri) ||
|
|
192
|
+
uri;
|
|
193
|
+
const item = {
|
|
194
|
+
id: `item-vscode-workspace-${p.hash}`,
|
|
195
|
+
type: ENTITY_TYPES.ITEM,
|
|
196
|
+
subtype: ITEM_SUBTYPES.LINK,
|
|
197
|
+
name: name || "(无名工程)",
|
|
198
|
+
category: "code-project",
|
|
199
|
+
ingestedAt,
|
|
200
|
+
source: source(`vscode-workspace:${p.hash}`),
|
|
201
|
+
extra: {
|
|
202
|
+
folderUri: p.folderUri || null,
|
|
203
|
+
workspaceUri: p.workspaceUri || null,
|
|
204
|
+
folderPath: p.folderPath || null,
|
|
205
|
+
lastOpenedMs: Number.isInteger(p.lastOpenedMs) ? p.lastOpenedMs : null,
|
|
206
|
+
editor: "vscode",
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
return { events: [], persons: [], places: [], items: [item], topics: [] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (raw.kind === "terminal-command") {
|
|
213
|
+
const p = raw.payload || {};
|
|
214
|
+
const cmd = typeof p.value === "string" ? p.value : "";
|
|
215
|
+
const event = {
|
|
216
|
+
id: `event-vscode-terminal-cmd-${p.sourceIndex}-${shortHash(cmd)}`,
|
|
217
|
+
type: ENTITY_TYPES.EVENT,
|
|
218
|
+
subtype: EVENT_SUBTYPES.OTHER,
|
|
219
|
+
occurredAt: Number.isInteger(p.snapshotTs) ? p.snapshotTs : raw.capturedAt,
|
|
220
|
+
ingestedAt,
|
|
221
|
+
source: source(raw.originalId),
|
|
222
|
+
actor: "self",
|
|
223
|
+
content: {
|
|
224
|
+
title: cmd.length > 80 ? cmd.substring(0, 80) + "…" : cmd,
|
|
225
|
+
text: cmd,
|
|
226
|
+
},
|
|
227
|
+
extra: {
|
|
228
|
+
kind: "terminal-command",
|
|
229
|
+
shellType: p.shellType || null,
|
|
230
|
+
sourceIndex: p.sourceIndex,
|
|
231
|
+
editor: "vscode",
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
return { events: [event], persons: [], places: [], items: [], topics: [] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (raw.kind === "terminal-dir") {
|
|
238
|
+
const p = raw.payload || {};
|
|
239
|
+
const dir = typeof p.value === "string" ? p.value : "";
|
|
240
|
+
const event = {
|
|
241
|
+
id: `event-vscode-terminal-dir-${p.sourceIndex}-${shortHash(dir)}`,
|
|
242
|
+
type: ENTITY_TYPES.EVENT,
|
|
243
|
+
subtype: EVENT_SUBTYPES.OTHER,
|
|
244
|
+
occurredAt: Number.isInteger(p.snapshotTs) ? p.snapshotTs : raw.capturedAt,
|
|
245
|
+
ingestedAt,
|
|
246
|
+
source: source(raw.originalId),
|
|
247
|
+
actor: "self",
|
|
248
|
+
content: {
|
|
249
|
+
title: `cd ${dir.length > 76 ? dir.substring(0, 76) + "…" : dir}`,
|
|
250
|
+
text: dir,
|
|
251
|
+
},
|
|
252
|
+
extra: {
|
|
253
|
+
kind: "terminal-dir",
|
|
254
|
+
shellType: p.shellType || null,
|
|
255
|
+
sourceIndex: p.sourceIndex,
|
|
256
|
+
editor: "vscode",
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
return { events: [event], persons: [], places: [], items: [], topics: [] };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
throw new Error(`vscode.normalize: unknown raw.kind=${raw.kind}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Cheap non-crypto hash for de-duping originalId + event id collisions.
|
|
267
|
+
// Don't need cryptographic strength here — the registry UNIQUE constraint
|
|
268
|
+
// gives the real guarantee.
|
|
269
|
+
function hashCommand(s) {
|
|
270
|
+
let h = 5381;
|
|
271
|
+
for (let i = 0; i < s.length; i++) {
|
|
272
|
+
h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
|
273
|
+
}
|
|
274
|
+
return h.toString(36);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function shortHash(s) {
|
|
278
|
+
return hashCommand(s).substring(0, 8);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
VSCodeAdapter,
|
|
283
|
+
VSCODE_NAME: NAME,
|
|
284
|
+
VSCODE_VERSION: VERSION,
|
|
285
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
VSCodeAdapter,
|
|
5
|
+
VSCODE_NAME,
|
|
6
|
+
VSCODE_VERSION,
|
|
7
|
+
} = require("./adapter");
|
|
8
|
+
const reader = require("./vscode-reader");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
VSCodeAdapter,
|
|
12
|
+
VSCODE_NAME,
|
|
13
|
+
VSCODE_VERSION,
|
|
14
|
+
defaultVscodeRoot: reader.defaultVscodeRoot,
|
|
15
|
+
decodeFileUri: reader.decodeFileUri,
|
|
16
|
+
readWorkspaces: reader.readWorkspaces,
|
|
17
|
+
readTerminalHistory: reader.readTerminalHistory,
|
|
18
|
+
};
|