@kanadego/dsh-heartbeat 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/assets/frontwin.ps1 +44 -0
  4. package/assets/idle.ps1 +27 -0
  5. package/assets/notify.ps1 +69 -0
  6. package/assets/presets/heartbeat/agent.cordis.yml +66 -0
  7. package/assets/presets/heartbeat/preset.yml +2 -0
  8. package/assets/screenpulse.ps1 +168 -0
  9. package/assets/vault.ps1 +52 -0
  10. package/client.js +428 -0
  11. package/config/busy-rules.json +63 -0
  12. package/config/interests.json +30 -0
  13. package/config/policy.json +57 -0
  14. package/config/profile-schema.json +68 -0
  15. package/config/watchlist.json +9 -0
  16. package/cordis.patch.yml +14 -0
  17. package/dist/bindings-XPPSKILN.js +19 -0
  18. package/dist/bindings-XPPSKILN.js.map +1 -0
  19. package/dist/chunk-2M35HRL6.js +1207 -0
  20. package/dist/chunk-2M35HRL6.js.map +1 -0
  21. package/dist/chunk-4UE74TUB.js +98 -0
  22. package/dist/chunk-4UE74TUB.js.map +1 -0
  23. package/dist/chunk-AISZRA4C.js +235 -0
  24. package/dist/chunk-AISZRA4C.js.map +1 -0
  25. package/dist/chunk-J6ZTRFFW.js +64 -0
  26. package/dist/chunk-J6ZTRFFW.js.map +1 -0
  27. package/dist/chunk-LLD7LUNN.js +202 -0
  28. package/dist/chunk-LLD7LUNN.js.map +1 -0
  29. package/dist/chunk-S7PTR42P.js +19 -0
  30. package/dist/chunk-S7PTR42P.js.map +1 -0
  31. package/dist/cli/index.js +589 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/inbox-MMLHISQV.js +22 -0
  34. package/dist/inbox-MMLHISQV.js.map +1 -0
  35. package/dist/index.js +2745 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/lib-FJP7J4T6.js +2281 -0
  38. package/dist/lib-FJP7J4T6.js.map +1 -0
  39. package/dist/runtime-J5NOPRBA.js +11 -0
  40. package/dist/runtime-J5NOPRBA.js.map +1 -0
  41. package/package.json +61 -0
@@ -0,0 +1,1207 @@
1
+ import {
2
+ loadEncryptedText,
3
+ loadJson,
4
+ readText,
5
+ saveEncryptedText,
6
+ saveJson,
7
+ writeText
8
+ } from "./chunk-LLD7LUNN.js";
9
+
10
+ // src/core/path-guard.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ var PathOutsideWorkspaceError = class extends Error {
14
+ constructor(target, workspace) {
15
+ super(`path outside workspace: "${target}" (workspace: "${workspace}")`);
16
+ this.name = "PathOutsideWorkspaceError";
17
+ }
18
+ };
19
+ function canonicalize(target) {
20
+ const abs = path.resolve(target);
21
+ try {
22
+ return fs.realpathSync(abs);
23
+ } catch {
24
+ const tail = [];
25
+ let dir = abs;
26
+ for (; ; ) {
27
+ const base = path.basename(dir);
28
+ const parent = path.dirname(dir);
29
+ if (parent === dir) {
30
+ throw new Error(`cannot canonicalize "${target}": no existing ancestor`);
31
+ }
32
+ tail.push(base);
33
+ dir = parent;
34
+ try {
35
+ const realDir = fs.realpathSync(dir);
36
+ return path.join(realDir, ...tail.reverse());
37
+ } catch {
38
+ continue;
39
+ }
40
+ }
41
+ }
42
+ }
43
+ function isInsideWorkspace(workspaceCanon, targetCanon) {
44
+ const norm = (p) => {
45
+ let n = path.normalize(p).toLowerCase();
46
+ if (!n.endsWith(path.sep)) n += path.sep;
47
+ return n;
48
+ };
49
+ const w = norm(workspaceCanon);
50
+ const t = norm(targetCanon);
51
+ return t === w || t.startsWith(w);
52
+ }
53
+ function createPathGuard(workspaceDir) {
54
+ const workspace = canonicalize(workspaceDir);
55
+ const guard = {
56
+ workspace,
57
+ check(target) {
58
+ const canon = canonicalize(target);
59
+ return isInsideWorkspace(workspace, canon) ? canon : null;
60
+ },
61
+ assert(target) {
62
+ const canon = guard.check(target);
63
+ if (canon === null) throw new PathOutsideWorkspaceError(target, workspace);
64
+ return canon;
65
+ }
66
+ };
67
+ return guard;
68
+ }
69
+
70
+ // src/core/audit-log.ts
71
+ import fs3 from "fs";
72
+ import path3 from "path";
73
+
74
+ // src/core/atomic-fs.ts
75
+ import fs2 from "fs";
76
+ import path2 from "path";
77
+ import { randomUUID, randomFillSync } from "crypto";
78
+ function tmpSibling(target, tag = "w") {
79
+ return path2.join(
80
+ path2.dirname(target),
81
+ `.${path2.basename(target)}.${tag}-${randomUUID().slice(0, 8)}.tmp`
82
+ );
83
+ }
84
+ function atomicWriteFileSync(target, data) {
85
+ const tmp = tmpSibling(target);
86
+ try {
87
+ fs2.writeFileSync(tmp, data);
88
+ fs2.renameSync(tmp, target);
89
+ } finally {
90
+ fs2.rmSync(tmp, { force: true });
91
+ }
92
+ }
93
+ function atomicWriteJsonSync(target, value) {
94
+ atomicWriteFileSync(target, JSON.stringify(value, null, 2));
95
+ }
96
+ function shredFileSync(target, passes = 3) {
97
+ const stat = fs2.statSync(target);
98
+ if (!stat.isFile()) throw new Error(`shred: not a file: ${target}`);
99
+ const buf = Buffer.alloc(Math.max(stat.size, 1));
100
+ for (let i = 0; i < passes; i++) {
101
+ randomFillSync(buf);
102
+ fs2.writeFileSync(target, buf);
103
+ }
104
+ fs2.rmSync(target, { force: true });
105
+ }
106
+
107
+ // src/core/audit-log.ts
108
+ function appendAuditLine(file, event) {
109
+ fs3.mkdirSync(path3.dirname(file), { recursive: true });
110
+ const line = JSON.stringify({ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(), ...event });
111
+ fs3.appendFileSync(file, line + "\n", "utf8");
112
+ }
113
+ function readAuditLines(file) {
114
+ if (!fs3.existsSync(file)) return [];
115
+ const out = [];
116
+ const raw = fs3.readFileSync(file, "utf8");
117
+ for (const line of raw.split("\n")) {
118
+ const trimmed = line.trim();
119
+ if (!trimmed) continue;
120
+ try {
121
+ out.push(JSON.parse(trimmed));
122
+ } catch {
123
+ out.push({ ts: "", corrupt: true, raw: trimmed.slice(0, 200) });
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ function pruneAuditFile(file, maxAgeMs, now = Date.now()) {
129
+ if (!fs3.existsSync(file)) return 0;
130
+ const lines = readAuditLines(file);
131
+ const kept = lines.filter((e) => {
132
+ const ev = e;
133
+ const ts = Date.parse(ev.ts ?? "");
134
+ if (!Number.isFinite(ts)) return true;
135
+ return now - ts <= maxAgeMs;
136
+ });
137
+ const removed = lines.length - kept.length;
138
+ if (removed === 0) return 0;
139
+ const body = kept.map((e) => JSON.stringify(e)).join("\n");
140
+ atomicWriteFileSync(file, body ? body + "\n" : "");
141
+ return removed;
142
+ }
143
+
144
+ // src/config/schema.ts
145
+ var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
146
+ function isPlainObject(v) {
147
+ return typeof v === "object" && v !== null && !Array.isArray(v);
148
+ }
149
+ function fail(msg) {
150
+ throw new Error(`policy: ${msg}`);
151
+ }
152
+ function assertPolicy(input) {
153
+ if (!isPlainObject(input)) fail("root must be an object");
154
+ const p = input;
155
+ const hb = p.heartbeat;
156
+ if (!isPlainObject(hb)) fail("heartbeat missing");
157
+ if (typeof hb.intervalMin !== "number" || hb.intervalMin < 1 || hb.intervalMin > 1440) {
158
+ fail("heartbeat.intervalMin must be a number in [1, 1440]");
159
+ }
160
+ const g = p.gate;
161
+ if (!isPlainObject(g)) fail("gate missing");
162
+ if (typeof g.maxDailySend !== "number" || g.maxDailySend < 0) fail("gate.maxDailySend must be >= 0");
163
+ if (typeof g.cooldownMinutes !== "number" || g.cooldownMinutes < 0) fail("gate.cooldownMinutes must be >= 0");
164
+ const qh = g.quietHours;
165
+ if (!isPlainObject(qh)) fail("gate.quietHours missing");
166
+ if (typeof qh.start !== "string" || !HHMM.test(qh.start) || typeof qh.end !== "string" || !HHMM.test(qh.end)) {
167
+ fail('gate.quietHours must be {start:"HH:MM", end:"HH:MM"}');
168
+ }
169
+ const b = p.browse;
170
+ if (!isPlainObject(b)) fail("browse missing");
171
+ if (!Array.isArray(b.windows) || b.windows.length === 0) fail("browse.windows must be a non-empty array");
172
+ for (const w of b.windows) {
173
+ if (!isPlainObject(w)) fail("browse.windows entries must be objects");
174
+ if (typeof w.start !== "string" || !HHMM.test(w.start) || typeof w.end !== "string" || !HHMM.test(w.end)) {
175
+ fail('browse.windows entries must be {start:"HH:MM", end:"HH:MM"}');
176
+ }
177
+ }
178
+ if (typeof b.minIntervalHours !== "number" || b.minIntervalHours <= 0) fail("browse.minIntervalHours must be > 0");
179
+ if (typeof b.maxSeedsPerVisit !== "number" || b.maxSeedsPerVisit < 1) fail("browse.maxSeedsPerVisit must be >= 1");
180
+ const s = p.seeds;
181
+ if (!isPlainObject(s)) fail("seeds missing");
182
+ if (typeof s.maxActive !== "number" || s.maxActive < 1) fail("seeds.maxActive must be >= 1");
183
+ if (!isPlainObject(s.ttlDays)) fail("seeds.ttlDays missing");
184
+ for (const k of ["news", "fandom", "scene", "promise"]) {
185
+ if (typeof s.ttlDays[k] !== "number") fail(`seeds.ttlDays.${k} missing`);
186
+ }
187
+ if (typeof s.coldBenchDays !== "number") fail("seeds.coldBenchDays missing");
188
+ if (typeof s.retireAfterUsed !== "number" || s.retireAfterUsed < 1) fail("seeds.retireAfterUsed must be >= 1");
189
+ if (!isPlainObject(s.scoreWeights)) fail("seeds.scoreWeights missing");
190
+ const pr = p.profile;
191
+ if (!isPlainObject(pr)) fail("profile missing");
192
+ const c = pr.consolidation;
193
+ if (!isPlainObject(c)) fail("profile.consolidation missing");
194
+ if (typeof c.minIntervalHours !== "number" || typeof c.inboxBacklog !== "number") fail("profile.consolidation fields missing");
195
+ if (typeof pr.partitionCap !== "number" || pr.partitionCap < 1) fail("profile.partitionCap must be >= 1");
196
+ if (typeof pr.maxOpsPerRun !== "number" || pr.maxOpsPerRun < 1) fail("profile.maxOpsPerRun must be >= 1");
197
+ const cc = pr.confidenceCap;
198
+ if (!isPlainObject(cc)) fail("profile.confidenceCap missing");
199
+ if (typeof cc.chat !== "number" || typeof cc.screen !== "number" || typeof cc.browse !== "number") {
200
+ fail("profile.confidenceCap fields missing");
201
+ }
202
+ if (typeof pr.volatileDays !== "number" || pr.volatileDays < 1) fail("profile.volatileDays must be >= 1");
203
+ if (typeof pr.stableLowActivityDays !== "number" || pr.stableLowActivityDays < 1) fail("profile.stableLowActivityDays must be >= 1");
204
+ if (typeof pr.psyEnabled !== "boolean") fail("profile.psyEnabled must be boolean");
205
+ const r = p.retention;
206
+ if (!isPlainObject(r)) fail("retention missing");
207
+ if (typeof r.envPulseHours !== "number" || typeof r.decisionLogDays !== "number") fail("retention fields missing");
208
+ }
209
+ function deepMerge(base, override) {
210
+ if (!isPlainObject(base) || !isPlainObject(override)) {
211
+ return override === void 0 ? base : override;
212
+ }
213
+ const out = { ...base };
214
+ for (const [k, v] of Object.entries(override)) {
215
+ out[k] = v === void 0 ? base[k] : deepMerge(base[k], v);
216
+ }
217
+ return out;
218
+ }
219
+
220
+ // src/config/load.ts
221
+ import fs4 from "fs";
222
+ import path4 from "path";
223
+ var USER_POLICY_FILE = "policy.json";
224
+ function loadPolicy(guard, configDir, settingsDir) {
225
+ const factoryPath = path4.join(configDir, "policy.json");
226
+ let factoryRaw;
227
+ try {
228
+ factoryRaw = JSON.parse(fs4.readFileSync(factoryPath, "utf8"));
229
+ } catch (e) {
230
+ throw new Error(`factory policy unreadable at ${factoryPath}: ${String(e)}`);
231
+ }
232
+ assertPolicy(factoryRaw);
233
+ const userPath = guard.assert(path4.join(settingsDir, USER_POLICY_FILE));
234
+ let merged = factoryRaw;
235
+ if (fs4.existsSync(userPath)) {
236
+ try {
237
+ const userRaw = JSON.parse(fs4.readFileSync(userPath, "utf8"));
238
+ merged = deepMerge(factoryRaw, userRaw);
239
+ } catch (e) {
240
+ throw new Error(`user policy layer unparseable at ${userPath}: ${String(e)}`);
241
+ }
242
+ }
243
+ assertPolicy(merged);
244
+ return merged;
245
+ }
246
+
247
+ // src/seeds/pool.ts
248
+ import path5 from "path";
249
+
250
+ // src/seeds/types.ts
251
+ function emptySeedDb() {
252
+ return { seq: 0, seeds: [] };
253
+ }
254
+ var SEED_SOURCE_DEFAULT_CONFIDENCE = {
255
+ hand: 1,
256
+ profile: 0.7,
257
+ chat: 0.6,
258
+ screen: 0.4,
259
+ browse: 0.4
260
+ };
261
+
262
+ // src/seeds/pool.ts
263
+ var DAY_MS = 864e5;
264
+ var TTL_KEYS = ["news", "fandom", "scene", "promise"];
265
+ function normalizeTag(tag) {
266
+ if (tag && TTL_KEYS.includes(tag)) return tag;
267
+ return "scene";
268
+ }
269
+ function parseIso(v) {
270
+ const t = Date.parse(v);
271
+ return Number.isFinite(t) ? t : 0;
272
+ }
273
+ function seedsFilePath(dataDir) {
274
+ return path5.join(dataDir, "seeds.jsonl");
275
+ }
276
+ function loadPool(guard, file) {
277
+ const raw = loadEncryptedText(guard, file);
278
+ if (raw === null) return emptySeedDb();
279
+ const db = emptySeedDb();
280
+ for (const line of raw.split("\n")) {
281
+ const trimmed = line.trim();
282
+ if (!trimmed) continue;
283
+ try {
284
+ const obj = JSON.parse(trimmed);
285
+ if (typeof obj.id === "string" && obj.id.startsWith("s")) {
286
+ db.seeds.push(obj);
287
+ const n = Number(obj.id.slice(1));
288
+ if (Number.isFinite(n) && n > db.seq) db.seq = n;
289
+ }
290
+ } catch {
291
+ }
292
+ }
293
+ return db;
294
+ }
295
+ function savePool(guard, file, db) {
296
+ const body = db.seeds.map((s) => JSON.stringify(s)).join("\n");
297
+ saveEncryptedText(guard, file, body ? body + "\n" : "");
298
+ }
299
+ var activeSeeds = (db) => db.seeds.filter((s) => s.status === "active");
300
+ var archivedSeeds = (db) => db.seeds.filter((s) => s.status === "archived");
301
+ function evictionScore(seed, policy, now) {
302
+ const ttlDays = policy.seeds.ttlDays[seed.tag] || 14;
303
+ const daysStale = Math.max(0, (now - parseIso(seed.lastEvidenceAt)) / DAY_MS);
304
+ const freshness = Math.max(0, 1 - daysStale / ttlDays);
305
+ const unused = seed.used === 0 ? 1 : 1 / seed.used;
306
+ const w = policy.seeds.scoreWeights;
307
+ return freshness * w.freshness + unused * w.unused + seed.confidence * w.confidence;
308
+ }
309
+ function pickEvictionVictim(db, policy, now) {
310
+ const actives = activeSeeds(db);
311
+ if (actives.length === 0) return null;
312
+ const unprotected = actives.filter((s) => !s.protected);
313
+ const candidates = unprotected.length > 0 ? unprotected : actives;
314
+ let worst = null;
315
+ let worstScore = Number.POSITIVE_INFINITY;
316
+ for (const s of candidates) {
317
+ const score = evictionScore(s, policy, now);
318
+ if (score < worstScore) {
319
+ worstScore = score;
320
+ worst = s;
321
+ }
322
+ }
323
+ return worst;
324
+ }
325
+ function archiveSeed(seed, reason, now) {
326
+ seed.status = "archived";
327
+ seed.retireReason = reason;
328
+ seed.retiredAt = new Date(now).toISOString();
329
+ }
330
+ function addSeed(guard, file, policy, input, now = Date.now()) {
331
+ const db = loadPool(guard, file);
332
+ const text = input.text.trim();
333
+ if (!text) throw new Error("seed text must not be empty");
334
+ const tag = normalizeTag(input.tag);
335
+ const source = input.source ?? "chat";
336
+ const confidence = input.confidence ?? SEED_SOURCE_DEFAULT_CONFIDENCE[source];
337
+ const dup = activeSeeds(db).find((s) => s.text === text);
338
+ if (dup) return { kind: "duplicate", seed: dup };
339
+ const nowIso = new Date(now).toISOString();
340
+ const ttlMs = (policy.seeds.ttlDays[tag] || 14) * DAY_MS;
341
+ const sameTopic = activeSeeds(db).filter((s) => s.topic === (input.topic ?? text.slice(0, 24)));
342
+ let seed;
343
+ if (sameTopic.length > 0) {
344
+ const kept = sameTopic[0];
345
+ kept.text = text;
346
+ kept.tag = tag;
347
+ kept.source = source;
348
+ kept.confidence = Math.max(kept.confidence, confidence);
349
+ kept.used = sameTopic.reduce((acc, s) => acc + s.used, 0);
350
+ kept.lastEvidenceAt = [kept.lastEvidenceAt, nowIso, ...sameTopic.map((s) => s.lastEvidenceAt)].reduce((a, b) => parseIso(b) > parseIso(a) ? b : a);
351
+ kept.expiresAt = new Date(Math.max(parseIso(kept.expiresAt), now + ttlMs)).toISOString();
352
+ kept.protected = kept.protected || source === "hand" || source === "profile" && confidence >= 0.7;
353
+ for (const extra of sameTopic.slice(1)) {
354
+ extra.status = "archived";
355
+ extra.retireReason = "completed";
356
+ extra.retiredAt = nowIso;
357
+ }
358
+ seed = kept;
359
+ if (activeSeeds(db).length > policy.seeds.maxActive) {
360
+ const victim = pickEvictionVictim(db, policy, now);
361
+ if (victim && victim.id !== kept.id) {
362
+ archiveSeed(victim, "pool_cap", now);
363
+ savePool(guard, file, db);
364
+ return { kind: "merged", seed: kept, evicted: victim };
365
+ }
366
+ }
367
+ savePool(guard, file, db);
368
+ return { kind: "merged", seed: kept };
369
+ }
370
+ let evicted;
371
+ if (activeSeeds(db).length >= policy.seeds.maxActive) {
372
+ const victim = pickEvictionVictim(db, policy, now);
373
+ if (victim) {
374
+ archiveSeed(victim, "pool_cap", now);
375
+ evicted = victim;
376
+ }
377
+ }
378
+ db.seq += 1;
379
+ seed = {
380
+ id: `s${db.seq}`,
381
+ text,
382
+ topic: input.topic ?? text.slice(0, 24),
383
+ tag,
384
+ source,
385
+ confidence,
386
+ protected: source === "hand" || source === "profile" && confidence >= 0.7,
387
+ used: 0,
388
+ bornAt: nowIso,
389
+ expiresAt: new Date(now + ttlMs).toISOString(),
390
+ lastUsedAt: null,
391
+ lastEvidenceAt: nowIso,
392
+ status: "active"
393
+ };
394
+ db.seeds.push(seed);
395
+ savePool(guard, file, db);
396
+ return { kind: "added", seed, evicted };
397
+ }
398
+ function gcPool(guard, file, policy, now = Date.now()) {
399
+ const db = loadPool(guard, file);
400
+ const report = { consumed: 0, expired: 0, coldBench: 0, activeAfter: 0 };
401
+ for (const s of activeSeeds(db)) {
402
+ const ageMs = now - parseIso(s.bornAt);
403
+ const sinceEvidence = parseIso(s.lastEvidenceAt);
404
+ const sinceUsed = s.lastUsedAt ? parseIso(s.lastUsedAt) : 0;
405
+ if (s.used >= policy.seeds.retireAfterUsed && sinceEvidence <= sinceUsed) {
406
+ archiveSeed(s, "consumed", now);
407
+ report.consumed += 1;
408
+ } else if (now > parseIso(s.expiresAt)) {
409
+ archiveSeed(s, "expired", now);
410
+ report.expired += 1;
411
+ } else if (s.used === 0 && ageMs >= policy.seeds.coldBenchDays * DAY_MS) {
412
+ archiveSeed(s, "cold_bench", now);
413
+ report.coldBench += 1;
414
+ }
415
+ }
416
+ report.activeAfter = activeSeeds(db).length;
417
+ savePool(guard, file, db);
418
+ return report;
419
+ }
420
+ function surfaceSeed(guard, file, policy, id, now = Date.now()) {
421
+ const db = loadPool(guard, file);
422
+ const s = db.seeds.find((x) => x.id === id && x.status === "active");
423
+ if (!s) return null;
424
+ s.used += 1;
425
+ s.lastUsedAt = new Date(now).toISOString();
426
+ if (s.used >= policy.seeds.retireAfterUsed && parseIso(s.lastEvidenceAt) <= parseIso(s.lastUsedAt)) {
427
+ archiveSeed(s, "consumed", now);
428
+ }
429
+ savePool(guard, file, db);
430
+ return s;
431
+ }
432
+ function archiveSeedById(guard, file, id, reason = "completed", now = Date.now()) {
433
+ const db = loadPool(guard, file);
434
+ const s = db.seeds.find((x) => x.id === id && x.status === "active");
435
+ if (!s) return null;
436
+ archiveSeed(s, reason, now);
437
+ savePool(guard, file, db);
438
+ return s;
439
+ }
440
+ function restoreSeed(guard, file, policy, id, now = Date.now()) {
441
+ const db = loadPool(guard, file);
442
+ const s = db.seeds.find((x) => x.id === id && x.status === "archived");
443
+ if (!s) return { ok: false, reason: "archived seed not found" };
444
+ if (activeSeeds(db).length >= policy.seeds.maxActive) {
445
+ return { ok: false, reason: `pool full (${policy.seeds.maxActive}); archive something first` };
446
+ }
447
+ s.status = "active";
448
+ s.retireReason = void 0;
449
+ s.retiredAt = void 0;
450
+ s.expiresAt = new Date(now + (policy.seeds.ttlDays[s.tag] || 14) * DAY_MS).toISOString();
451
+ s.lastEvidenceAt = new Date(now).toISOString();
452
+ savePool(guard, file, db);
453
+ return { ok: true, seed: s };
454
+ }
455
+ function deleteSeed(guard, file, id) {
456
+ const db = loadPool(guard, file);
457
+ const before = db.seeds.length;
458
+ db.seeds = db.seeds.filter((x) => x.id !== id);
459
+ if (db.seeds.length === before) return false;
460
+ savePool(guard, file, db);
461
+ return true;
462
+ }
463
+
464
+ // src/ledger/ledger.ts
465
+ import path6 from "path";
466
+ import { randomUUID as randomUUID2 } from "crypto";
467
+ var DAY_MS2 = 864e5;
468
+ function ledgerFilePath(dataDir) {
469
+ return path6.join(dataDir, "ledger.md");
470
+ }
471
+ var LINE_RE = /^- \[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]\[(open|done)\]\[#([0-9a-f]{6})\] (.*)$/;
472
+ function renderEntry(e) {
473
+ return `- [${e.date} ${e.time}][${e.status}][#${e.id}] ${e.text}`;
474
+ }
475
+ function readLedger(guard, file) {
476
+ const raw = readText(guard, file, "# \u8D26\u672C\n");
477
+ const lines = raw.split("\n");
478
+ const entries = [];
479
+ const rawLines = [];
480
+ for (const line of lines) {
481
+ const m = LINE_RE.exec(line);
482
+ if (m) {
483
+ entries.push({ date: m[1], time: m[2], status: m[3], id: m[4], text: m[5] });
484
+ }
485
+ rawLines.push(line);
486
+ }
487
+ return { header: lines[0] ?? "# \u8D26\u672C", entries, rawLines };
488
+ }
489
+ function appendEntry(guard, file, text, now = Date.now()) {
490
+ const textTrimmed = text.trim();
491
+ if (!textTrimmed) throw new Error("ledger entry must not be empty");
492
+ const d = new Date(now);
493
+ const pad = (n) => String(n).padStart(2, "0");
494
+ const entry = {
495
+ id: randomUUID2().slice(0, 6),
496
+ date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
497
+ time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
498
+ status: "open",
499
+ text: textTrimmed.replace(/\r?\n/g, " ")
500
+ };
501
+ const { rawLines } = readLedger(guard, file);
502
+ rawLines.push(renderEntry(entry));
503
+ writeText(guard, file, rawLines.join("\n").replace(/\n*$/, "\n"));
504
+ return entry;
505
+ }
506
+ function markDone(guard, file, key, now = Date.now()) {
507
+ const { rawLines, entries } = readLedger(guard, file);
508
+ const target = entries.find((e) => e.status === "open" && (e.id === key || e.text.includes(key)));
509
+ if (!target) return null;
510
+ const d = new Date(now);
511
+ const pad = (n) => String(n).padStart(2, "0");
512
+ const out = rawLines.map((line) => {
513
+ if (line.includes(`#${target.id}] `)) {
514
+ return `- [${target.date} ${pad(d.getHours())}:${pad(d.getMinutes())}][done][#${target.id}] ${target.text}`;
515
+ }
516
+ return line;
517
+ });
518
+ writeText(guard, file, out.join("\n").replace(/\n*$/, "\n"));
519
+ return target;
520
+ }
521
+ function scanPending(guard, file, now = Date.now()) {
522
+ const { entries } = readLedger(guard, file);
523
+ return entries.filter((e) => e.status === "open").sort((a, b) => a.date < b.date ? -1 : 1);
524
+ }
525
+ function pendingOlderThan(guard, file, days, now = Date.now()) {
526
+ const cutoff = new Date(now - days * DAY_MS2).toISOString().slice(0, 10);
527
+ return scanPending(guard, file, now).filter((e) => e.date <= cutoff);
528
+ }
529
+
530
+ // src/browse/browse.ts
531
+ import fs5 from "fs";
532
+ import path7 from "path";
533
+ var WATCH_THROTTLE_MS = 6 * 36e5;
534
+ var UA = { "User-Agent": "dsh-heartbeat/2.0 (+local; personal companion)" };
535
+ function emptyBrowseState() {
536
+ return { targets: {}, last_check_at: 0, wander: { focusHistory: {}, focusCount: {}, last_wander_at: 0 } };
537
+ }
538
+ function browseStatePath(paths) {
539
+ return path7.join(paths.dataDir, "browse.json");
540
+ }
541
+ function readJsonFile(file, fallback) {
542
+ try {
543
+ return JSON.parse(fs5.readFileSync(file, "utf8"));
544
+ } catch {
545
+ return fallback;
546
+ }
547
+ }
548
+ function loadInterests(paths) {
549
+ const userPath = path7.join(paths.settingsDir, "interests.json");
550
+ if (fs5.existsSync(userPath)) return readJsonFile(userPath, { interests: [], _schedule: {} });
551
+ return readJsonFile(path7.join(paths.configDir, "interests.json"), { interests: [], _schedule: {} });
552
+ }
553
+ function loadWatchlist(paths) {
554
+ const userPath = path7.join(paths.settingsDir, "watchlist.json");
555
+ if (fs5.existsSync(userPath)) return readJsonFile(userPath, { targets: [] });
556
+ return readJsonFile(path7.join(paths.configDir, "watchlist.json"), { targets: [] });
557
+ }
558
+ function loadState(guard, paths) {
559
+ return loadJson(guard, browseStatePath(paths)) ?? emptyBrowseState();
560
+ }
561
+ async function checkNpm(fetcher, name) {
562
+ const r = await fetcher(`https://registry.npmjs.org/${name}/latest`, { headers: UA });
563
+ if (!r.ok) throw new Error(`npm ${r.status}`);
564
+ const j = await r.json();
565
+ if (!j.version) throw new Error("npm: no version");
566
+ return { version: j.version, seen: `npm:${j.version}` };
567
+ }
568
+ async function checkGithub(fetcher, repo) {
569
+ const r = await fetcher(`https://api.github.com/repos/${repo}/releases/latest`, {
570
+ headers: { ...UA, Accept: "application/vnd.github+json" }
571
+ });
572
+ if (r.status === 404) return null;
573
+ if (!r.ok) throw new Error(`gh ${r.status}`);
574
+ const j = await r.json();
575
+ if (!j.tag_name) throw new Error("gh: no tag");
576
+ return { version: j.tag_name, seen: `gh:${j.tag_name}`, title: j.name || "" };
577
+ }
578
+ async function checkWatchlist(guard, paths, opts = {}, now = Date.now()) {
579
+ const fetcher = opts.fetcher ?? globalThis.fetch;
580
+ const state = loadState(guard, paths);
581
+ if (opts.throttleOk !== true && now - state.last_check_at < WATCH_THROTTLE_MS) {
582
+ return { items: [], errors: [], checked: 0 };
583
+ }
584
+ const watchlist = loadWatchlist(paths);
585
+ const report = { items: [], errors: [], checked: 0 };
586
+ for (const t of watchlist.targets ?? []) {
587
+ report.checked += 1;
588
+ try {
589
+ const info = t.type === "npm" && t.name ? await checkNpm(fetcher, t.name) : t.type === "github" && t.repo ? await checkGithub(fetcher, t.repo) : null;
590
+ if (!info) continue;
591
+ const prev = state.targets[t.id];
592
+ if (prev && prev.seen !== info.seen) {
593
+ const title = info.title ? `\uFF08${info.title.slice(0, 60)}\uFF09` : "";
594
+ report.items.push({
595
+ text: `${t.note || t.id} \u6709\u66F4\u65B0\uFF1A${prev.version} -> ${info.version}${title}`,
596
+ topic: `watch:${t.id}`,
597
+ tag: "news",
598
+ source: "browse",
599
+ confidence: 0.4
600
+ });
601
+ }
602
+ state.targets[t.id] = info;
603
+ } catch (e) {
604
+ report.errors.push(`${t.id}: ${String(e)}`);
605
+ }
606
+ }
607
+ state.last_check_at = now;
608
+ saveJson(guard, browseStatePath(paths), state);
609
+ return report;
610
+ }
611
+ function inWanderWindow(now, windows) {
612
+ const hm = now.getHours() * 60 + now.getMinutes();
613
+ for (const w of windows) {
614
+ const [sh, sm] = w.start.split(":").map(Number);
615
+ const [eh, em] = w.end.split(":").map(Number);
616
+ if (hm >= sh * 60 + sm && hm <= eh * 60 + em) return `${w.start}-${w.end}`;
617
+ }
618
+ return null;
619
+ }
620
+ function onCooldown(state, focus, cooldownDays, now) {
621
+ const last = state.wander.focusHistory[focus] ?? 0;
622
+ return last > now - cooldownDays * 864e5;
623
+ }
624
+ function pickFocus(state, interests, now) {
625
+ const sc = interests._schedule ?? {};
626
+ const cooldown = sc.focus_cooldown_days ?? 3;
627
+ const pool = (interests.interests ?? []).filter((t) => !onCooldown(state, t, cooldown, now));
628
+ if (pool.length === 0) return null;
629
+ pool.sort((a, b) => (state.wander.focusHistory[a] ?? 0) - (state.wander.focusHistory[b] ?? 0));
630
+ return pool[0];
631
+ }
632
+ function adviseWander(guard, paths, policy, now = /* @__PURE__ */ new Date()) {
633
+ const state = loadState(guard, paths);
634
+ const interests = loadInterests(paths);
635
+ const windows = interests._schedule?.windows?.length ? interests._schedule.windows : policy.browse.windows;
636
+ const win = inWanderWindow(now, windows);
637
+ if (!win) {
638
+ const hh = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
639
+ return { focus: null, query: null, skipped: `window(now=${hh})` };
640
+ }
641
+ const minGap = policy.browse.minIntervalHours * 36e5;
642
+ if (now.getTime() - state.wander.last_wander_at < minGap) {
643
+ return { focus: null, query: null, skipped: "min-interval" };
644
+ }
645
+ const focus = pickFocus(state, interests, now.getTime());
646
+ if (!focus) return { focus: null, query: null, skipped: "no-focus" };
647
+ return { focus, query: `${focus} 2026 \u6700\u65B0`, skipped: null };
648
+ }
649
+ function completeWander(guard, paths, focus, now = Date.now()) {
650
+ const state = loadState(guard, paths);
651
+ state.wander.focusHistory[focus] = now;
652
+ state.wander.focusCount[focus] = (state.wander.focusCount[focus] ?? 0) + 1;
653
+ state.wander.last_wander_at = now;
654
+ saveJson(guard, browseStatePath(paths), state);
655
+ return { focus, count: state.wander.focusCount[focus] };
656
+ }
657
+ function browseStatus(guard, paths) {
658
+ return loadState(guard, paths);
659
+ }
660
+
661
+ // src/profile/store.ts
662
+ import path9 from "path";
663
+ import { randomUUID as randomUUID3 } from "crypto";
664
+ import fs7 from "fs";
665
+
666
+ // src/profile/types.ts
667
+ var PARTITIONS = ["interest", "projects", "comm", "psy"];
668
+ var CONFIDENCE_CAP = {
669
+ chat: 0.6,
670
+ screen: 0.4,
671
+ browse: 0.4,
672
+ hand: 1,
673
+ ledger: 0.6
674
+ };
675
+ function emptyProfile() {
676
+ return {
677
+ version: 1,
678
+ partitions: { interest: { entries: [] }, projects: { entries: [] }, comm: { entries: [] }, psy: { entries: [] } }
679
+ };
680
+ }
681
+
682
+ // src/profile/schema.ts
683
+ import fs6 from "fs";
684
+ import path8 from "path";
685
+ function loadProfileSchema(paths) {
686
+ const userPath = path8.join(paths.settingsDir, "profile-schema.json");
687
+ const file = fs6.existsSync(userPath) ? userPath : path8.join(paths.configDir, "profile-schema.json");
688
+ try {
689
+ const raw = JSON.parse(fs6.readFileSync(file, "utf8"));
690
+ if (!raw.partitions) throw new Error("partitions missing");
691
+ return raw;
692
+ } catch (e) {
693
+ throw new Error(`profile-schema unreadable at ${file}: ${String(e)}`);
694
+ }
695
+ }
696
+ function checkAddAgainstSchema(schema, partition, topic, subTopic, nominated) {
697
+ const p = schema.partitions[partition];
698
+ if (!p) return { ok: false, reason: `partition not in schema: ${partition}`, temporal: "stable" };
699
+ const t = p.topics[topic];
700
+ if (!t) return { ok: false, reason: `topic not in schema: ${partition}/${topic}`, temporal: "stable" };
701
+ const st = t.subtopics[subTopic];
702
+ if (!st) return { ok: false, reason: `sub_topic not in schema: ${partition}/${topic}/${subTopic}`, temporal: "stable" };
703
+ const allowed = st.allowed && st.allowed.length > 0 ? st.allowed : ["stable"];
704
+ const def = st.default && allowed.includes(st.default) ? st.default : allowed[0];
705
+ if (!nominated) return { ok: true, temporal: def };
706
+ if (!allowed.includes(nominated)) {
707
+ return {
708
+ ok: false,
709
+ reason: `temporal "${nominated}" not allowed for ${partition}/${topic}/${subTopic} (allowed: ${allowed.join("|")})`,
710
+ temporal: def
711
+ };
712
+ }
713
+ return { ok: true, temporal: nominated };
714
+ }
715
+
716
+ // src/profile/store.ts
717
+ var DAY_MS3 = 864e5;
718
+ function profileFilePath(dataDir) {
719
+ return path9.join(dataDir, "profile.json");
720
+ }
721
+ function journalFilePath(dataDir) {
722
+ return path9.join(dataDir, "profile_journal.jsonl");
723
+ }
724
+ function loadProfile(guard, file) {
725
+ const doc = loadJson(guard, file);
726
+ if (!doc || !doc.partitions) return emptyProfile();
727
+ for (const p of PARTITIONS) {
728
+ if (!doc.partitions[p]) doc.partitions[p] = { entries: [] };
729
+ }
730
+ return doc;
731
+ }
732
+ function parseIso2(v) {
733
+ const t = Date.parse(v);
734
+ return Number.isFinite(t) ? t : 0;
735
+ }
736
+ function refExists(guard, dataDir, ref) {
737
+ const base = ref.split("#")[0] ?? "";
738
+ if (!base) return false;
739
+ const target = path9.join(dataDir, base);
740
+ try {
741
+ return fs7.existsSync(guard.assert(target));
742
+ } catch {
743
+ return false;
744
+ }
745
+ }
746
+ function capForKinds(kinds) {
747
+ if (kinds.length === 0) return 0.4;
748
+ return Math.min(...kinds.map((k) => CONFIDENCE_CAP[k] ?? 0.4));
749
+ }
750
+ function findActive(doc, id) {
751
+ for (const p of PARTITIONS) {
752
+ const hit = doc.partitions[p].entries.find((e) => e.id === id && e.validTo === null);
753
+ if (hit) return hit;
754
+ }
755
+ return void 0;
756
+ }
757
+ function applyOpsToDoc(guard, dataDir, doc, ops, schema, policy, now) {
758
+ const applied = [];
759
+ const rejected = [];
760
+ const nowIso = new Date(now).toISOString();
761
+ for (const op of ops) {
762
+ if (op.op === "NOOP") {
763
+ applied.push(op);
764
+ continue;
765
+ }
766
+ if (op.op === "ADD") {
767
+ if (op.partition === "psy" && !policy.profile.psyEnabled) {
768
+ rejected.push({ op, reason: "psy partition is disabled" });
769
+ continue;
770
+ }
771
+ const check = checkAddAgainstSchema(schema, op.partition, op.topic, op.subTopic, op.temporal);
772
+ if (!check.ok) {
773
+ rejected.push({ op, reason: check.reason });
774
+ continue;
775
+ }
776
+ if (!op.evidence || op.evidence.length === 0) {
777
+ rejected.push({ op, reason: "ADD without evidence (no provenance, axiom 1)" });
778
+ continue;
779
+ }
780
+ const badRef = op.evidence.find((e) => !refExists(guard, dataDir, e.ref));
781
+ if (badRef) {
782
+ rejected.push({ op, reason: `evidence ref does not resolve: ${badRef.ref}` });
783
+ continue;
784
+ }
785
+ const cap = capForKinds(op.evidence.map((e) => e.kind));
786
+ const active = doc.partitions[op.partition].entries.filter((e) => e.validTo === null);
787
+ if (active.length >= policy.profile.partitionCap) {
788
+ rejected.push({ op, reason: `partition ${op.partition} at cap (${policy.profile.partitionCap}); converge first` });
789
+ continue;
790
+ }
791
+ dbSeq += 1;
792
+ const entry = {
793
+ id: `p${dbSeq.toString(36)}${randomUUID3().slice(0, 4)}`,
794
+ partition: op.partition,
795
+ topic: op.topic,
796
+ subTopic: op.subTopic,
797
+ content: op.content.trim(),
798
+ confidence: Math.min(op.confidence ?? cap, cap),
799
+ temporal: check.temporal,
800
+ validFrom: nowIso,
801
+ validTo: null,
802
+ supersededBy: null,
803
+ evidence: op.evidence,
804
+ createdAt: nowIso,
805
+ updatedAt: nowIso,
806
+ updateCount: 0
807
+ };
808
+ op.assignedId = entry.id;
809
+ doc.partitions[op.partition].entries.push(entry);
810
+ applied.push(op);
811
+ continue;
812
+ }
813
+ if (op.op === "UPDATE") {
814
+ const entry = findActive(doc, op.id);
815
+ if (!entry) {
816
+ rejected.push({ op, reason: `unknown or inactive entry: ${op.id}` });
817
+ continue;
818
+ }
819
+ if (op.changes.content !== void 0) entry.content = op.changes.content.trim();
820
+ if (op.changes.confidence !== void 0) {
821
+ const cap = capForKinds(entry.evidence.map((e) => e.kind));
822
+ if (op.changes.confidence > entry.confidence && entry.evidence.length < 2) {
823
+ rejected.push({ op, reason: "confidence upgrade requires a second confirming observation" });
824
+ continue;
825
+ }
826
+ entry.confidence = Math.min(op.changes.confidence, cap);
827
+ }
828
+ entry.updatedAt = nowIso;
829
+ entry.updateCount += 1;
830
+ applied.push(op);
831
+ continue;
832
+ }
833
+ if (op.op === "INVALIDATE") {
834
+ const entry = findActive(doc, op.id);
835
+ if (!entry) {
836
+ rejected.push({ op, reason: `unknown or inactive entry: ${op.id}` });
837
+ continue;
838
+ }
839
+ if (entry.temporal === "volatile") {
840
+ rejected.push({ op, reason: "volatile expiry is code-owned (time-driven), not LLM-nominated" });
841
+ continue;
842
+ }
843
+ const hasNewObservation = (op.evidence ?? []).length > 0 && (op.evidence ?? []).some((e) => parseIso2(e.at) > parseIso2(entry.evidence[entry.evidence.length - 1]?.at ?? ""));
844
+ if (!hasNewObservation) {
845
+ rejected.push({ op, reason: "stable INVALIDATE requires a newer contradicting observation" });
846
+ continue;
847
+ }
848
+ entry.validTo = nowIso;
849
+ entry.supersededBy = null;
850
+ entry.updatedAt = nowIso;
851
+ entry.updateCount += 1;
852
+ if (op.evidence) entry.evidence.push(...op.evidence);
853
+ applied.push(op);
854
+ continue;
855
+ }
856
+ }
857
+ return { applied, rejected };
858
+ }
859
+ var dbSeq = 0;
860
+ function runDeterministicAging(doc, policy, now) {
861
+ const nowIso = new Date(now).toISOString();
862
+ let volatileExpired = 0;
863
+ let lowActivityMarked = 0;
864
+ for (const p of PARTITIONS) {
865
+ for (const e of doc.partitions[p].entries) {
866
+ if (e.validTo !== null) continue;
867
+ const lastEvidence = Math.max(...e.evidence.map((x) => parseIso2(x.at)), parseIso2(e.updatedAt));
868
+ if (e.temporal === "volatile") {
869
+ if (now - lastEvidence > policy.profile.volatileDays * DAY_MS3) {
870
+ e.validTo = nowIso;
871
+ e.updatedAt = nowIso;
872
+ e.updateCount += 1;
873
+ volatileExpired += 1;
874
+ }
875
+ } else if (!e.lowActivity && now - lastEvidence > policy.profile.stableLowActivityDays * DAY_MS3) {
876
+ e.lowActivity = true;
877
+ lowActivityMarked += 1;
878
+ }
879
+ }
880
+ }
881
+ return { volatileExpired, lowActivityMarked };
882
+ }
883
+ function persistWithJournal(guard, dataDir, doc, record) {
884
+ saveJson(guard, profileFilePath(dataDir), doc);
885
+ const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...record });
886
+ const journal = journalFilePath(dataDir);
887
+ try {
888
+ fs7.appendFileSync(guard.assert(journal), line + "\n", "utf8");
889
+ } catch {
890
+ fs7.mkdirSync(dataDir, { recursive: true });
891
+ fs7.appendFileSync(guard.assert(journal), line + "\n", "utf8");
892
+ }
893
+ }
894
+ function applyOpPermissive(doc, op, ts) {
895
+ if (op.op === "ADD") {
896
+ dbSeq += 1;
897
+ doc.partitions[op.partition].entries.push({
898
+ id: op.assignedId ?? `r${dbSeq.toString(36)}${randomUUID3().slice(0, 4)}`,
899
+ partition: op.partition,
900
+ topic: op.topic,
901
+ subTopic: op.subTopic,
902
+ content: op.content,
903
+ confidence: op.confidence ?? 0.5,
904
+ temporal: op.temporal ?? "stable",
905
+ validFrom: ts,
906
+ validTo: null,
907
+ supersededBy: null,
908
+ evidence: op.evidence,
909
+ createdAt: ts,
910
+ updatedAt: ts,
911
+ updateCount: 0
912
+ });
913
+ return;
914
+ }
915
+ if (op.op === "UPDATE") {
916
+ const e = [...PARTITIONS].flatMap((p) => doc.partitions[p].entries).find((x) => x.id === op.id);
917
+ if (e) {
918
+ if (op.changes.content !== void 0) e.content = op.changes.content;
919
+ if (op.changes.confidence !== void 0) e.confidence = op.changes.confidence;
920
+ e.updatedAt = ts;
921
+ e.updateCount += 1;
922
+ }
923
+ return;
924
+ }
925
+ if (op.op === "INVALIDATE") {
926
+ const e = [...PARTITIONS].flatMap((p) => doc.partitions[p].entries).find((x) => x.id === op.id);
927
+ if (e) {
928
+ e.validTo = ts;
929
+ e.updatedAt = ts;
930
+ e.updateCount += 1;
931
+ }
932
+ }
933
+ }
934
+ function replayJournal(guard, dataDir) {
935
+ const journal = journalFilePath(dataDir);
936
+ const raw = readText(guard, journal, "");
937
+ const doc = emptyProfile();
938
+ let records = 0;
939
+ let truncatedTail = 0;
940
+ const lines = raw.split("\n");
941
+ for (let i = 0; i < lines.length; i++) {
942
+ const trimmed = lines[i].trim();
943
+ if (!trimmed) continue;
944
+ try {
945
+ const rec = JSON.parse(trimmed);
946
+ for (const op of rec.applied ?? []) applyOpPermissive(doc, op, rec.ts);
947
+ records += 1;
948
+ } catch {
949
+ const isLast = lines.slice(i + 1).every((l) => !l.trim());
950
+ if (isLast) {
951
+ truncatedTail = lines.length - i;
952
+ break;
953
+ }
954
+ }
955
+ }
956
+ return { doc, truncatedTail, records };
957
+ }
958
+ function verifyProfile(guard, dataDir) {
959
+ const replayed = replayJournal(guard, dataDir);
960
+ const onDisk = loadProfile(guard, profileFilePath(dataDir));
961
+ const strip = (doc) => JSON.stringify(doc.partitions, (k, v) => ["id", "supersededBy", "validFrom", "createdAt", "updatedAt", "retiredAt"].includes(k) ? "<norm>" : v);
962
+ const ok = strip(replayed.doc) === strip(onDisk);
963
+ if (ok) return { ok: true, truncatedTail: replayed.truncatedTail, records: replayed.records };
964
+ const diskIds = new Set([...PARTITIONS].flatMap((p) => onDisk.partitions[p].entries.map((e) => e.content)));
965
+ const replayIds = new Set([...PARTITIONS].flatMap((p) => replayed.doc.partitions[p].entries.map((e) => e.content)));
966
+ const onlyDisk = [...diskIds].find((c) => !replayIds.has(c));
967
+ const onlyReplay = [...replayIds].find((c) => !diskIds.has(c));
968
+ return {
969
+ ok: false,
970
+ firstDivergence: {
971
+ id: onlyDisk ?? onlyReplay ?? "(content)",
972
+ expected: onlyReplay ? "absent in journal replay" : "present in journal replay",
973
+ actual: onlyDisk ? "present on disk" : "absent on disk"
974
+ },
975
+ truncatedTail: replayed.truncatedTail,
976
+ records: replayed.records
977
+ };
978
+ }
979
+ function rebuildProfile(guard, dataDir, opts = {}) {
980
+ const replayed = replayJournal(guard, dataDir);
981
+ const target = profileFilePath(dataDir);
982
+ const tmp = path9.join(dataDir, `.profile.rebuild.${Date.now()}.tmp`);
983
+ atomicWriteFileSync(tmp, JSON.stringify(replayed.doc, null, 2));
984
+ if (opts.check) {
985
+ const onDisk = loadProfile(guard, target);
986
+ const same = JSON.stringify(onDisk) === JSON.stringify(replayed.doc);
987
+ fs7.rmSync(tmp, { force: true });
988
+ return {
989
+ ok: same,
990
+ truncatedTail: replayed.truncatedTail,
991
+ records: replayed.records,
992
+ wrote: false,
993
+ diffSummary: same ? "no diff" : "materialized view differs from journal replay"
994
+ };
995
+ }
996
+ fs7.renameSync(tmp, target);
997
+ if (replayed.truncatedTail > 0) {
998
+ writeText(
999
+ guard,
1000
+ path9.join(dataDir, "logs", "rebuild-report.txt"),
1001
+ `rebuild truncated ${replayed.truncatedTail} torn line(s) at journal tail; ${replayed.records} records applied
1002
+ `
1003
+ );
1004
+ }
1005
+ return { ok: true, truncatedTail: replayed.truncatedTail, records: replayed.records, wrote: true };
1006
+ }
1007
+
1008
+ // src/notify/notify.ts
1009
+ import { spawnSync } from "child_process";
1010
+ import path10 from "path";
1011
+ function runNotify(paths, args) {
1012
+ const r = spawnSync(
1013
+ "powershell.exe",
1014
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path10.join(paths.assetsDir, "notify.ps1"), ...args],
1015
+ { timeout: 2e4, encoding: "utf8" }
1016
+ );
1017
+ return { status: r.status ?? -1, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
1018
+ }
1019
+ function ensureRegistered(paths) {
1020
+ const check = runNotify(paths, ["-Check"]);
1021
+ if (/REGISTERED: yes/.test(check.out)) return true;
1022
+ const reg = runNotify(paths, ["-RegisterOnly"]);
1023
+ return reg.status === 0;
1024
+ }
1025
+ function sendNewMessageHint(paths) {
1026
+ const r = runNotify(paths, ["-Title", "Heartbeat", "-Message", "\u6709\u65B0\u6D88\u606F"]);
1027
+ return r.status === 0 && /TOAST_SENT/.test(r.out);
1028
+ }
1029
+
1030
+ // src/core/preset-install.ts
1031
+ import fs8 from "fs";
1032
+ import os from "os";
1033
+ import path11 from "path";
1034
+ import { fileURLToPath } from "url";
1035
+ var COMPOSITION_FILE = "agent.cordis.yml";
1036
+ var METADATA_FILE = "preset.yml";
1037
+ var BUNDLED_PRESET_ID = "heartbeat";
1038
+ function bundledPresetDir(moduleUrl, id = BUNDLED_PRESET_ID) {
1039
+ let dir;
1040
+ try {
1041
+ dir = path11.dirname(fileURLToPath(moduleUrl));
1042
+ } catch {
1043
+ return void 0;
1044
+ }
1045
+ for (let depth = 0; depth < 5; depth += 1) {
1046
+ const candidate = path11.join(dir, "assets", "presets", id);
1047
+ if (fs8.existsSync(path11.join(candidate, COMPOSITION_FILE))) return candidate;
1048
+ const parent = path11.dirname(dir);
1049
+ if (parent === dir) break;
1050
+ dir = parent;
1051
+ }
1052
+ return void 0;
1053
+ }
1054
+ function userPresetRoot(roots) {
1055
+ const found = roots?.find(
1056
+ (root) => root?.trust === "user" && typeof root.path === "string" && root.path.length > 0
1057
+ );
1058
+ return found?.path === void 0 ? void 0 : path11.resolve(found.path);
1059
+ }
1060
+ function conventionalUserPresetRoot(env = process.env, home = os.homedir()) {
1061
+ const override = env.DSH_HOME?.trim();
1062
+ const root = override && override.length > 0 ? override : path11.join(home, ".dsh");
1063
+ return path11.join(root, ".agent-presets");
1064
+ }
1065
+ function installBundledPreset(options) {
1066
+ const id = options.id && options.id.length > 0 ? options.id : BUNDLED_PRESET_ID;
1067
+ if (options.enabled === false) {
1068
+ return { action: "skipped-disabled", id, detail: "installPreset=false" };
1069
+ }
1070
+ const bundledDir = bundledPresetDir(options.moduleUrl, BUNDLED_PRESET_ID);
1071
+ if (id !== BUNDLED_PRESET_ID) {
1072
+ return {
1073
+ action: "skipped-custom-id",
1074
+ id,
1075
+ ...bundledDir === void 0 ? {} : { bundledDir },
1076
+ detail: `only "${BUNDLED_PRESET_ID}" ships with the plugin; "${id}" is yours to provide`
1077
+ };
1078
+ }
1079
+ if (bundledDir === void 0) {
1080
+ return {
1081
+ action: "error",
1082
+ id,
1083
+ detail: "bundled template not found next to the plugin (assets/presets/heartbeat)"
1084
+ };
1085
+ }
1086
+ const root = options.root ?? (options.rosterKnown ? void 0 : conventionalUserPresetRoot());
1087
+ if (root === void 0) {
1088
+ return {
1089
+ action: "skipped-no-root",
1090
+ id,
1091
+ bundledDir,
1092
+ detail: "the roster mounts no user preset root (includeUserRoot=false)"
1093
+ };
1094
+ }
1095
+ const dir = path11.join(root, id);
1096
+ const composition = path11.join(dir, COMPOSITION_FILE);
1097
+ try {
1098
+ if (fs8.existsSync(composition)) {
1099
+ if (options.force !== true) {
1100
+ const drifted = !sameBytes(composition, path11.join(bundledDir, COMPOSITION_FILE));
1101
+ return {
1102
+ action: "exists",
1103
+ id,
1104
+ dir,
1105
+ bundledDir,
1106
+ detail: drifted ? "kept as-is (differs from the bundled template)" : "kept as-is"
1107
+ };
1108
+ }
1109
+ fs8.copyFileSync(path11.join(bundledDir, COMPOSITION_FILE), composition);
1110
+ return {
1111
+ action: "restored",
1112
+ id,
1113
+ dir,
1114
+ bundledDir,
1115
+ detail: "composition replaced from the bundled template"
1116
+ };
1117
+ }
1118
+ const existed = fs8.existsSync(dir);
1119
+ fs8.mkdirSync(dir, { recursive: true });
1120
+ fs8.copyFileSync(path11.join(bundledDir, COMPOSITION_FILE), composition);
1121
+ const metadata = path11.join(dir, METADATA_FILE);
1122
+ if (!fs8.existsSync(metadata)) fs8.copyFileSync(path11.join(bundledDir, METADATA_FILE), metadata);
1123
+ return {
1124
+ action: existed ? "repaired" : "created",
1125
+ id,
1126
+ dir,
1127
+ bundledDir,
1128
+ detail: existed ? "directory existed without a composition file (it occupied the id as a broken row)" : void 0
1129
+ };
1130
+ } catch (error) {
1131
+ return { action: "error", id, dir, bundledDir, detail: String(error).slice(0, 200) };
1132
+ }
1133
+ }
1134
+ function describeInstall(result) {
1135
+ const where = result.dir === void 0 ? "" : ` (${result.dir})`;
1136
+ const why = result.detail === void 0 ? "" : ` \u2014 ${result.detail}`;
1137
+ return `preset ${result.id} ${result.action}${where}${why}`;
1138
+ }
1139
+ function presetStatus(moduleUrl, id = BUNDLED_PRESET_ID, root = conventionalUserPresetRoot()) {
1140
+ const dir = path11.join(root, id);
1141
+ const bundledDir = bundledPresetDir(moduleUrl, id);
1142
+ const installed = fs8.existsSync(path11.join(dir, COMPOSITION_FILE));
1143
+ return {
1144
+ id,
1145
+ dir,
1146
+ ...bundledDir === void 0 ? {} : { bundledDir },
1147
+ installed,
1148
+ compositionMatches: installed && bundledDir !== void 0 && sameBytes(path11.join(dir, COMPOSITION_FILE), path11.join(bundledDir, COMPOSITION_FILE)),
1149
+ metadataMatches: bundledDir !== void 0 && fs8.existsSync(path11.join(dir, METADATA_FILE)) && sameBytes(path11.join(dir, METADATA_FILE), path11.join(bundledDir, METADATA_FILE))
1150
+ };
1151
+ }
1152
+ function sameBytes(left, right) {
1153
+ try {
1154
+ return fs8.readFileSync(left).equals(fs8.readFileSync(right));
1155
+ } catch {
1156
+ return false;
1157
+ }
1158
+ }
1159
+
1160
+ export {
1161
+ createPathGuard,
1162
+ atomicWriteJsonSync,
1163
+ shredFileSync,
1164
+ appendAuditLine,
1165
+ pruneAuditFile,
1166
+ deepMerge,
1167
+ loadPolicy,
1168
+ seedsFilePath,
1169
+ loadPool,
1170
+ activeSeeds,
1171
+ archivedSeeds,
1172
+ addSeed,
1173
+ gcPool,
1174
+ surfaceSeed,
1175
+ archiveSeedById,
1176
+ restoreSeed,
1177
+ deleteSeed,
1178
+ ledgerFilePath,
1179
+ readLedger,
1180
+ appendEntry,
1181
+ markDone,
1182
+ scanPending,
1183
+ pendingOlderThan,
1184
+ loadInterests,
1185
+ loadWatchlist,
1186
+ checkWatchlist,
1187
+ adviseWander,
1188
+ completeWander,
1189
+ browseStatus,
1190
+ loadProfileSchema,
1191
+ profileFilePath,
1192
+ loadProfile,
1193
+ applyOpsToDoc,
1194
+ runDeterministicAging,
1195
+ persistWithJournal,
1196
+ verifyProfile,
1197
+ rebuildProfile,
1198
+ ensureRegistered,
1199
+ sendNewMessageHint,
1200
+ BUNDLED_PRESET_ID,
1201
+ userPresetRoot,
1202
+ conventionalUserPresetRoot,
1203
+ installBundledPreset,
1204
+ describeInstall,
1205
+ presetStatus
1206
+ };
1207
+ //# sourceMappingURL=chunk-2M35HRL6.js.map