@remnic/core 9.10.0 → 9.12.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.
@@ -452,6 +452,9 @@ import {
452
452
  import {
453
453
  assertPathInsideRoot
454
454
  } from "./chunk-GCASGYIO.js";
455
+ import {
456
+ openBetterSqlite3
457
+ } from "./chunk-RL6NEVXT.js";
455
458
  import {
456
459
  inferMemoryStatus,
457
460
  isActiveMemoryStatus
@@ -2075,7 +2078,7 @@ var RecallRerankCoordinator = class _RecallRerankCoordinator {
2075
2078
  namespaces,
2076
2079
  {
2077
2080
  readNamespaceMemories: async (ns) => (await this.getStorage(ns)).readAllMemories(),
2078
- readMemoryFrontmatter: async (path31, preferredNamespace) => {
2081
+ readMemoryFrontmatter: async (path33, preferredNamespace) => {
2079
2082
  if (!fallbackReader) {
2080
2083
  for (const ns of namespaces) {
2081
2084
  try {
@@ -2087,7 +2090,7 @@ var RecallRerankCoordinator = class _RecallRerankCoordinator {
2087
2090
  }
2088
2091
  if (!fallbackReader) return null;
2089
2092
  const memory = await this.readQmdResultMemory(
2090
- path31,
2093
+ path33,
2091
2094
  fallbackReader,
2092
2095
  namespaces,
2093
2096
  preferredNamespace
@@ -19165,9 +19168,9 @@ function readStructuralSymbol(entry) {
19165
19168
  const rec = entry;
19166
19169
  const symbol = typeof rec.symbol === "string" ? rec.symbol.trim() : "";
19167
19170
  if (!symbol) return null;
19168
- const path31 = typeof rec.path === "string" && rec.path.length > 0 ? rec.path : void 0;
19171
+ const path33 = typeof rec.path === "string" && rec.path.length > 0 ? rec.path : void 0;
19169
19172
  const kind = typeof rec.kind === "string" && rec.kind.length > 0 ? rec.kind : void 0;
19170
- const item = path31 !== void 0 && kind !== void 0 ? { symbol, path: path31, kind } : path31 !== void 0 ? { symbol, path: path31 } : kind !== void 0 ? { symbol, kind } : { symbol };
19173
+ const item = path33 !== void 0 && kind !== void 0 ? { symbol, path: path33, kind } : path33 !== void 0 ? { symbol, path: path33 } : kind !== void 0 ? { symbol, kind } : { symbol };
19171
19174
  return item;
19172
19175
  }
19173
19176
  function classifySpawnError(err) {
@@ -22912,10 +22915,580 @@ function publisherForConnector(connectorId) {
22912
22915
  return publisherFor(hostIdForConnector(connectorId));
22913
22916
  }
22914
22917
 
22915
- // src/session-summaries/index.ts
22916
- import { createHash as createHash9 } from "crypto";
22917
- import { lstat as lstat2, mkdir as mkdir7, readFile as readFile6, readdir as readdir4, rename, rm, stat as stat4, writeFile as writeFile7 } from "fs/promises";
22918
+ // src/activity/store.ts
22919
+ import { mkdirSync } from "fs";
22920
+ import { mkdir as mkdir7 } from "fs/promises";
22918
22921
  import path29 from "path";
22922
+ var ACTIVITY_SCHEMA_VERSION = 1;
22923
+ var MAX_SEARCH_RESULTS = 100;
22924
+ function activityDatabasePath(memoryDir) {
22925
+ return path29.join(memoryDir, "state", "activity.sqlite");
22926
+ }
22927
+ async function ensureActivityStateDir(memoryDir) {
22928
+ await mkdir7(path29.join(memoryDir, "state"), { recursive: true });
22929
+ }
22930
+ function openActivityDatabase(memoryDir) {
22931
+ mkdirSync(path29.join(memoryDir, "state"), { recursive: true });
22932
+ const db = openBetterSqlite3(activityDatabasePath(memoryDir));
22933
+ db.pragma("journal_mode = WAL");
22934
+ db.pragma("busy_timeout = 5000");
22935
+ db.pragma("synchronous = NORMAL");
22936
+ applySchema(db);
22937
+ return db;
22938
+ }
22939
+ function applyActivitySchema(db) {
22940
+ applySchema(db);
22941
+ }
22942
+ function applySchema(db) {
22943
+ db.exec(`
22944
+ CREATE TABLE IF NOT EXISTS activity_meta (
22945
+ key TEXT PRIMARY KEY,
22946
+ value TEXT NOT NULL
22947
+ );
22948
+
22949
+ CREATE TABLE IF NOT EXISTS activity_snapshots (
22950
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22951
+ machine TEXT NOT NULL,
22952
+ captured_at_utc TEXT NOT NULL,
22953
+ app_name TEXT NOT NULL,
22954
+ window_title TEXT NOT NULL,
22955
+ browser_url TEXT,
22956
+ text TEXT NOT NULL,
22957
+ text_source TEXT NOT NULL,
22958
+ content_hash TEXT NOT NULL,
22959
+ simhash TEXT
22960
+ );
22961
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_activity_snapshots_dedup
22962
+ ON activity_snapshots(machine, captured_at_utc, content_hash);
22963
+ CREATE INDEX IF NOT EXISTS idx_activity_snapshots_time
22964
+ ON activity_snapshots(captured_at_utc, id);
22965
+
22966
+ CREATE TABLE IF NOT EXISTS activity_sync_state (
22967
+ machine TEXT PRIMARY KEY,
22968
+ cursor TEXT,
22969
+ updated_at_utc TEXT NOT NULL
22970
+ );
22971
+ `);
22972
+ const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='activity_snapshots_fts'").get();
22973
+ if (!hasFts) {
22974
+ db.exec(`
22975
+ CREATE VIRTUAL TABLE activity_snapshots_fts USING fts5(
22976
+ text, app_name, window_title, browser_url
22977
+ );
22978
+ `);
22979
+ }
22980
+ db.prepare("INSERT OR REPLACE INTO activity_meta (key, value) VALUES ('schema_version', ?)").run(
22981
+ String(ACTIVITY_SCHEMA_VERSION)
22982
+ );
22983
+ }
22984
+ function canonicalizeUtc(iso) {
22985
+ const parsed = Date.parse(iso);
22986
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : iso;
22987
+ }
22988
+ function assertRequiredSnapshotFields(s) {
22989
+ const required = [
22990
+ ["machine", s.machine],
22991
+ ["app", s.app],
22992
+ ["windowTitle", s.windowTitle],
22993
+ ["text", s.text],
22994
+ ["textSource", s.textSource],
22995
+ ["contentHash", s.contentHash]
22996
+ ];
22997
+ for (const [field, value] of required) {
22998
+ if (typeof value !== "string") {
22999
+ throw new RangeError(`activity: snapshot field "${field}" is required (got ${value === null ? "null" : typeof value}).`);
23000
+ }
23001
+ }
23002
+ if (s.machine.length === 0 || s.contentHash.length === 0) {
23003
+ throw new RangeError('activity: snapshot "machine" and "contentHash" must be non-empty.');
23004
+ }
23005
+ if (s.textSource !== "ax" && s.textSource !== "ocr") {
23006
+ throw new RangeError(`activity: snapshot "textSource" must be "ax" or "ocr" (got "${s.textSource}").`);
23007
+ }
23008
+ }
23009
+ function assertValidUtcInstant(iso, what) {
23010
+ const m = iso.match(
23011
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](?:0\d:[0-5]\d|1[0-3]:[0-5]\d|14:00))$/
23012
+ );
23013
+ if (m === null || !Number.isFinite(Date.parse(iso))) {
23014
+ throw new RangeError(`activity: invalid ${what} "${iso}".`);
23015
+ }
23016
+ const [year, month, day, hour, minute, second] = m.slice(1).map(Number);
23017
+ const daysInMonth = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
23018
+ if (day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59) {
23019
+ throw new RangeError(`activity: ${what} "${iso}" is not a real calendar instant.`);
23020
+ }
23021
+ }
23022
+ function isRecord(value) {
23023
+ return typeof value === "object" && value !== null;
23024
+ }
23025
+ function str(value) {
23026
+ return typeof value === "string" ? value : "";
23027
+ }
23028
+ function optStr(value) {
23029
+ return typeof value === "string" && value.length > 0 ? value : void 0;
23030
+ }
23031
+ function ftsMatchFor(query) {
23032
+ if (typeof query !== "string") return null;
23033
+ const tokens = query.match(/[\p{L}\p{N}_]+/gu);
23034
+ if (!tokens || tokens.length === 0) return null;
23035
+ return tokens.map((token) => `"${token}"`).join(" ");
23036
+ }
23037
+ function rowToSnapshot(row) {
23038
+ if (!isRecord(row)) {
23039
+ throw new Error("activity store: unexpected non-object row");
23040
+ }
23041
+ const textSource = row.text_source === "ocr" ? "ocr" : "ax";
23042
+ return {
23043
+ id: typeof row.id === "number" ? row.id : void 0,
23044
+ machine: str(row.machine),
23045
+ capturedAtUtc: str(row.captured_at_utc),
23046
+ app: str(row.app_name),
23047
+ windowTitle: str(row.window_title),
23048
+ ...optStr(row.browser_url) !== void 0 ? { browserUrl: optStr(row.browser_url) } : {},
23049
+ text: str(row.text),
23050
+ textSource,
23051
+ contentHash: str(row.content_hash),
23052
+ ...optStr(row.simhash) !== void 0 ? { simhash: optStr(row.simhash) } : {}
23053
+ };
23054
+ }
23055
+ var ActivityStore = class _ActivityStore {
23056
+ db;
23057
+ constructor(db) {
23058
+ this.db = db;
23059
+ }
23060
+ static open(memoryDir) {
23061
+ return new _ActivityStore(openActivityDatabase(memoryDir));
23062
+ }
23063
+ /**
23064
+ * Insert a snapshot, idempotent on (machine, captured_at_utc, content_hash):
23065
+ * the same screen content recurring at a *different* time is kept; only an
23066
+ * exact re-ingestion of the same capture dedups. Returns `inserted: false`
23067
+ * for a duplicate. The FTS row is written only on a real insert (atomically),
23068
+ * so it never drifts from the base table.
23069
+ */
23070
+ insertSnapshot(snapshot) {
23071
+ const runInsert = this.db.transaction((s) => {
23072
+ assertValidUtcInstant(s.capturedAtUtc, "capture timestamp");
23073
+ assertRequiredSnapshotFields(s);
23074
+ const capturedAtUtc = canonicalizeUtc(s.capturedAtUtc);
23075
+ const info = this.db.prepare(
23076
+ `INSERT OR IGNORE INTO activity_snapshots
23077
+ (machine, captured_at_utc, app_name, window_title, browser_url, text, text_source, content_hash, simhash)
23078
+ VALUES (@machine, @captured_at_utc, @app_name, @window_title, @browser_url, @text, @text_source, @content_hash, @simhash)`
23079
+ ).run({
23080
+ machine: s.machine,
23081
+ captured_at_utc: capturedAtUtc,
23082
+ app_name: s.app,
23083
+ window_title: s.windowTitle,
23084
+ browser_url: s.browserUrl ?? null,
23085
+ text: s.text,
23086
+ text_source: s.textSource,
23087
+ content_hash: s.contentHash,
23088
+ simhash: s.simhash ?? null
23089
+ });
23090
+ if (info.changes === 0) {
23091
+ const existing = this.db.prepare("SELECT id FROM activity_snapshots WHERE machine = ? AND captured_at_utc = ? AND content_hash = ?").get(s.machine, capturedAtUtc, s.contentHash);
23092
+ const id2 = isRecord(existing) && typeof existing.id === "number" ? existing.id : -1;
23093
+ return { inserted: false, id: id2 };
23094
+ }
23095
+ const id = Number(info.lastInsertRowid);
23096
+ this.db.prepare(
23097
+ `INSERT INTO activity_snapshots_fts (rowid, text, app_name, window_title, browser_url)
23098
+ VALUES (?, ?, ?, ?, ?)`
23099
+ ).run(id, s.text, s.app, s.windowTitle, s.browserUrl ?? "");
23100
+ return { inserted: true, id };
23101
+ });
23102
+ return runInsert(snapshot);
23103
+ }
23104
+ /** Snapshots whose capture instant is in the half-open [start, end) window. */
23105
+ listSnapshotsForDay(machine, startUtcInclusive, endUtcExclusive) {
23106
+ assertValidUtcInstant(startUtcInclusive, "range start");
23107
+ assertValidUtcInstant(endUtcExclusive, "range end");
23108
+ const start = canonicalizeUtc(startUtcInclusive);
23109
+ const end = canonicalizeUtc(endUtcExclusive);
23110
+ const rows = machine === null ? this.db.prepare(
23111
+ `SELECT * FROM activity_snapshots
23112
+ WHERE captured_at_utc >= ? AND captured_at_utc < ?
23113
+ ORDER BY captured_at_utc ASC, id ASC`
23114
+ ).all(start, end) : this.db.prepare(
23115
+ `SELECT * FROM activity_snapshots
23116
+ WHERE machine = ? AND captured_at_utc >= ? AND captured_at_utc < ?
23117
+ ORDER BY captured_at_utc ASC, id ASC`
23118
+ ).all(machine, start, end);
23119
+ return rows.map(rowToSnapshot);
23120
+ }
23121
+ getCursor(machine) {
23122
+ const row = this.db.prepare("SELECT cursor FROM activity_sync_state WHERE machine = ?").get(machine);
23123
+ return isRecord(row) && typeof row.cursor === "string" ? row.cursor : null;
23124
+ }
23125
+ setCursor(machine, cursor, updatedAtUtc = (/* @__PURE__ */ new Date()).toISOString()) {
23126
+ this.db.prepare(
23127
+ `INSERT INTO activity_sync_state (machine, cursor, updated_at_utc)
23128
+ VALUES (?, ?, ?)
23129
+ ON CONFLICT(machine) DO UPDATE SET cursor = excluded.cursor, updated_at_utc = excluded.updated_at_utc`
23130
+ ).run(machine, cursor, updatedAtUtc);
23131
+ }
23132
+ /** Full-text search over snapshot text/app/window/url; newest first. */
23133
+ searchSnapshots(query, limit) {
23134
+ const capped = Number.isInteger(limit) && limit > 0 ? Math.min(limit, MAX_SEARCH_RESULTS) : 20;
23135
+ const match = ftsMatchFor(query);
23136
+ if (match === null) return [];
23137
+ const rows = this.db.prepare(
23138
+ `SELECT s.* FROM activity_snapshots_fts f
23139
+ JOIN activity_snapshots s ON s.id = f.rowid
23140
+ WHERE activity_snapshots_fts MATCH ?
23141
+ ORDER BY s.captured_at_utc DESC, s.id DESC
23142
+ LIMIT ?`
23143
+ ).all(match, capped);
23144
+ return rows.map(rowToSnapshot);
23145
+ }
23146
+ /** Retention: drop snapshots captured strictly before `cutoffUtc`. */
23147
+ pruneOlderThan(cutoffUtc) {
23148
+ assertValidUtcInstant(cutoffUtc, "prune cutoff");
23149
+ const ids = this.db.prepare("SELECT id FROM activity_snapshots WHERE captured_at_utc < ?").all(canonicalizeUtc(cutoffUtc)).map((row) => isRecord(row) && typeof row.id === "number" ? row.id : -1).filter((id) => id >= 0);
23150
+ const deleteFts = this.db.prepare("DELETE FROM activity_snapshots_fts WHERE rowid = ?");
23151
+ const deleteRow = this.db.prepare("DELETE FROM activity_snapshots WHERE id = ?");
23152
+ const tx = this.db.transaction((rowIds) => {
23153
+ for (const id of rowIds) {
23154
+ deleteFts.run(id);
23155
+ deleteRow.run(id);
23156
+ }
23157
+ });
23158
+ tx(ids);
23159
+ return ids.length;
23160
+ }
23161
+ close() {
23162
+ this.db.close();
23163
+ }
23164
+ };
23165
+
23166
+ // src/activity/digest.ts
23167
+ import { createHash as createHash9 } from "crypto";
23168
+ import path30 from "path";
23169
+ var ACTIVITY_DIGEST_FORMAT_VERSION = 1;
23170
+ var ACTIVITY_DIR_NAME = "activity";
23171
+ var MAX_DWELL_MS = 15 * 6e4;
23172
+ var NOTABLE_MAX_WINDOWS = 10;
23173
+ var NOTABLE_EXCERPT_CHARS = 280;
23174
+ var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
23175
+ function isValidActivityDate(date) {
23176
+ if (typeof date !== "string" || !DATE_PATTERN.test(date)) return false;
23177
+ const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
23178
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === date;
23179
+ }
23180
+ function activityDigestPath(memoryDir, date) {
23181
+ if (!isValidActivityDate(date)) {
23182
+ throw new RangeError(`Invalid activity date "${date}"; expected YYYY-MM-DD.`);
23183
+ }
23184
+ return path30.join(memoryDir, ACTIVITY_DIR_NAME, `${date}.md`);
23185
+ }
23186
+ function timezoneOffsetIso(instant, timezone) {
23187
+ const parts = new Intl.DateTimeFormat("en-US", {
23188
+ timeZone: timezone,
23189
+ timeZoneName: "longOffset"
23190
+ }).formatToParts(instant);
23191
+ const name = parts.find((part) => part.type === "timeZoneName")?.value ?? "GMT";
23192
+ const match = name.match(/GMT([+-]\d{2}:\d{2})?/);
23193
+ return match?.[1] ?? "+00:00";
23194
+ }
23195
+ function assertValidTimezone(timezone) {
23196
+ try {
23197
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
23198
+ } catch {
23199
+ throw new RangeError(`Invalid IANA timezone "${timezone}" for the activity digest.`);
23200
+ }
23201
+ }
23202
+ function zonedDayStartIso(date, timezone) {
23203
+ const prevDate = shiftIsoDate(date, -1);
23204
+ const probeOffsets = new Set(
23205
+ [
23206
+ `${prevDate}T12:00:00Z`,
23207
+ `${prevDate}T23:00:00Z`,
23208
+ `${date}T00:00:00Z`,
23209
+ `${date}T12:00:00Z`,
23210
+ `${date}T23:00:00Z`
23211
+ ].map((iso) => timezoneOffsetIso(new Date(iso), timezone))
23212
+ );
23213
+ let best = null;
23214
+ for (const offset of probeOffsets) {
23215
+ const candidate = Date.parse(`${date}T00:00:00${offset}`);
23216
+ if (!Number.isFinite(candidate)) continue;
23217
+ if (timezoneOffsetIso(new Date(candidate), timezone) !== offset) continue;
23218
+ if (best === null || candidate < best) best = candidate;
23219
+ }
23220
+ if (best === null) {
23221
+ for (let minute = 1; minute <= 180 && best === null; minute++) {
23222
+ const hh = String(Math.floor(minute / 60)).padStart(2, "0");
23223
+ const mm = String(minute % 60).padStart(2, "0");
23224
+ for (const offset of probeOffsets) {
23225
+ const candidate = Date.parse(`${date}T${hh}:${mm}:00${offset}`);
23226
+ if (!Number.isFinite(candidate)) continue;
23227
+ if (timezoneOffsetIso(new Date(candidate), timezone) !== offset) continue;
23228
+ if (best === null || candidate < best) best = candidate;
23229
+ }
23230
+ }
23231
+ }
23232
+ if (best === null) {
23233
+ const noon = timezoneOffsetIso(/* @__PURE__ */ new Date(`${date}T12:00:00Z`), timezone);
23234
+ best = Date.parse(`${date}T00:00:00${noon}`);
23235
+ }
23236
+ if (best === null || !Number.isFinite(best)) {
23237
+ throw new RangeError(`activity: could not resolve a local day start for "${date}" in "${timezone}".`);
23238
+ }
23239
+ return new Date(best).toISOString();
23240
+ }
23241
+ function shiftIsoDate(date, days) {
23242
+ const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
23243
+ parsed.setUTCDate(parsed.getUTCDate() + days);
23244
+ return parsed.toISOString().slice(0, 10);
23245
+ }
23246
+ function nextIsoDate(date) {
23247
+ return shiftIsoDate(date, 1);
23248
+ }
23249
+ function activityDayWindow(date, timezone) {
23250
+ if (!isValidActivityDate(date)) {
23251
+ throw new RangeError(`Invalid activity date "${date}"; expected a real YYYY-MM-DD day.`);
23252
+ }
23253
+ assertValidTimezone(timezone);
23254
+ return {
23255
+ startUtc: new Date(zonedDayStartIso(date, timezone)).toISOString(),
23256
+ endUtc: new Date(zonedDayStartIso(nextIsoDate(date), timezone)).toISOString()
23257
+ };
23258
+ }
23259
+ function sortedByTime(snapshots) {
23260
+ return [...snapshots].sort((a, b) => {
23261
+ const at2 = Date.parse(a.capturedAtUtc);
23262
+ const bt = Date.parse(b.capturedAtUtc);
23263
+ if (at2 !== bt && Number.isFinite(at2) && Number.isFinite(bt)) return at2 < bt ? -1 : 1;
23264
+ const aid = a.id ?? 0;
23265
+ const bid = b.id ?? 0;
23266
+ if (aid < bid) return -1;
23267
+ if (aid > bid) return 1;
23268
+ if (a.contentHash !== b.contentHash) return a.contentHash < b.contentHash ? -1 : 1;
23269
+ if (a.app !== b.app) return a.app < b.app ? -1 : 1;
23270
+ if (a.windowTitle !== b.windowTitle) return a.windowTitle < b.windowTitle ? -1 : 1;
23271
+ return 0;
23272
+ });
23273
+ }
23274
+ function computeDwell(snapshots) {
23275
+ const byMachine = /* @__PURE__ */ new Map();
23276
+ for (const snapshot of snapshots) {
23277
+ const list = byMachine.get(snapshot.machine);
23278
+ if (list === void 0) byMachine.set(snapshot.machine, [snapshot]);
23279
+ else list.push(snapshot);
23280
+ }
23281
+ const dwell = /* @__PURE__ */ new Map();
23282
+ for (const list of byMachine.values()) {
23283
+ const ordered = sortedByTime(list);
23284
+ for (let index = 0; index < ordered.length; index++) {
23285
+ const current = ordered[index];
23286
+ if (current === void 0) continue;
23287
+ const next = ordered[index + 1];
23288
+ let value = 0;
23289
+ if (next !== void 0) {
23290
+ const delta = Date.parse(next.capturedAtUtc) - Date.parse(current.capturedAtUtc);
23291
+ if (Number.isFinite(delta) && delta > 0) value = Math.min(delta, MAX_DWELL_MS);
23292
+ }
23293
+ dwell.set(current, value);
23294
+ }
23295
+ }
23296
+ return dwell;
23297
+ }
23298
+ function formatDurationMinutes(ms) {
23299
+ return `${Math.round(ms / 6e4)}m`;
23300
+ }
23301
+ function clockHhMm(iso, timezone) {
23302
+ const ms = Date.parse(iso);
23303
+ if (!Number.isFinite(ms)) return "??:??";
23304
+ const parts = new Intl.DateTimeFormat("en-GB", {
23305
+ timeZone: timezone,
23306
+ hour: "2-digit",
23307
+ minute: "2-digit",
23308
+ hour12: false
23309
+ }).formatToParts(new Date(ms));
23310
+ const hour = parts.find((p) => p.type === "hour")?.value ?? "00";
23311
+ const minute = parts.find((p) => p.type === "minute")?.value ?? "00";
23312
+ return `${hour}:${minute}`;
23313
+ }
23314
+ function collapseWhitespace(text) {
23315
+ return text.replace(/\s+/g, " ").trim();
23316
+ }
23317
+ function perAppSection(ordered, dwell) {
23318
+ const totals = /* @__PURE__ */ new Map();
23319
+ for (const snapshot of ordered) {
23320
+ totals.set(snapshot.app, (totals.get(snapshot.app) ?? 0) + (dwell.get(snapshot) ?? 0));
23321
+ }
23322
+ const rows = [...totals.entries()].sort((a, b) => {
23323
+ if (b[1] !== a[1]) return b[1] - a[1];
23324
+ if (a[0] < b[0]) return -1;
23325
+ if (a[0] > b[0]) return 1;
23326
+ return 0;
23327
+ });
23328
+ const lines = ["## Per-app time", ""];
23329
+ if (rows.length === 0) {
23330
+ lines.push("_No activity recorded._");
23331
+ } else {
23332
+ for (const [app, ms] of rows) {
23333
+ lines.push(`- ${app}: ${formatDurationMinutes(ms)}`);
23334
+ }
23335
+ }
23336
+ return lines.join("\n");
23337
+ }
23338
+ function timelineSpans(ordered) {
23339
+ const spans = [];
23340
+ for (const snapshot of ordered) {
23341
+ const last = spans[spans.length - 1];
23342
+ if (last !== void 0 && last.machine === snapshot.machine && last.app === snapshot.app && last.windowTitle === snapshot.windowTitle && last.browserUrl === snapshot.browserUrl) {
23343
+ continue;
23344
+ }
23345
+ spans.push({
23346
+ startIso: snapshot.capturedAtUtc,
23347
+ machine: snapshot.machine,
23348
+ app: snapshot.app,
23349
+ windowTitle: snapshot.windowTitle,
23350
+ ...snapshot.browserUrl !== void 0 ? { browserUrl: snapshot.browserUrl } : {}
23351
+ });
23352
+ }
23353
+ return spans;
23354
+ }
23355
+ function timelineSection(ordered, timezone) {
23356
+ const lines = ["## Timeline", ""];
23357
+ const spans = timelineSpans(ordered);
23358
+ if (spans.length === 0) {
23359
+ lines.push("_No activity recorded._");
23360
+ return lines.join("\n");
23361
+ }
23362
+ for (const span of spans) {
23363
+ const clock = clockHhMm(span.startIso, timezone);
23364
+ const window = collapseWhitespace(span.windowTitle);
23365
+ const url = span.browserUrl !== void 0 ? ` (${collapseWhitespace(span.browserUrl)})` : "";
23366
+ lines.push(`- [${clock}] ${span.app}${window.length > 0 ? ` \u2014 ${window}` : ""}${url}`);
23367
+ }
23368
+ return lines.join("\n");
23369
+ }
23370
+ function notableSection(ordered, dwell) {
23371
+ const withDwell = ordered.map((snapshot) => ({ snapshot, dwell: dwell.get(snapshot) ?? 0 }));
23372
+ const ranked = withDwell.filter((entry) => collapseWhitespace(entry.snapshot.text).length > 0).sort((a, b) => {
23373
+ if (b.dwell !== a.dwell) return b.dwell - a.dwell;
23374
+ const at2 = Date.parse(a.snapshot.capturedAtUtc);
23375
+ const bt = Date.parse(b.snapshot.capturedAtUtc);
23376
+ if (at2 !== bt && Number.isFinite(at2) && Number.isFinite(bt)) return at2 < bt ? -1 : 1;
23377
+ const idDelta = (a.snapshot.id ?? 0) - (b.snapshot.id ?? 0);
23378
+ if (idDelta !== 0) return idDelta;
23379
+ if (a.snapshot.contentHash !== b.snapshot.contentHash) {
23380
+ return a.snapshot.contentHash < b.snapshot.contentHash ? -1 : 1;
23381
+ }
23382
+ return 0;
23383
+ }).slice(0, NOTABLE_MAX_WINDOWS);
23384
+ const lines = ["## Notable", ""];
23385
+ if (ranked.length === 0) {
23386
+ lines.push("_No notable text captured._");
23387
+ return lines.join("\n");
23388
+ }
23389
+ for (const { snapshot } of ranked) {
23390
+ const excerpt = collapseWhitespace(snapshot.text).slice(0, NOTABLE_EXCERPT_CHARS);
23391
+ lines.push(`- **${snapshot.app}** \u2014 ${excerpt}`);
23392
+ }
23393
+ return lines.join("\n");
23394
+ }
23395
+ function composeActivityDigestBody(date, timezone, snapshots) {
23396
+ assertValidTimezone(timezone);
23397
+ const ordered = sortedByTime(snapshots);
23398
+ const dwell = computeDwell(ordered);
23399
+ return [
23400
+ `# Activity \u2014 ${date}`,
23401
+ "",
23402
+ perAppSection(ordered, dwell),
23403
+ "",
23404
+ timelineSection(ordered, timezone),
23405
+ "",
23406
+ notableSection(ordered, dwell),
23407
+ ""
23408
+ ].join("\n");
23409
+ }
23410
+ function hashActivityBody(body) {
23411
+ return createHash9("sha256").update(body, "utf8").digest("hex");
23412
+ }
23413
+ function composeActivityDigestMeta(date, machines, snapshots, body) {
23414
+ const uniqueMachines = [...new Set(machines)].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
23415
+ return {
23416
+ kind: "activity-digest",
23417
+ date,
23418
+ machines: uniqueMachines,
23419
+ snapshotCount: snapshots.length,
23420
+ contentHash: hashActivityBody(body),
23421
+ formatVersion: ACTIVITY_DIGEST_FORMAT_VERSION
23422
+ };
23423
+ }
23424
+ function serializeActivityDigest(meta, body) {
23425
+ const frontmatter = [
23426
+ "---",
23427
+ `kind: ${meta.kind}`,
23428
+ `date: ${meta.date}`,
23429
+ `machines: ${JSON.stringify(meta.machines)}`,
23430
+ `snapshotCount: ${meta.snapshotCount}`,
23431
+ `contentHash: ${meta.contentHash}`,
23432
+ `formatVersion: ${meta.formatVersion}`,
23433
+ "---",
23434
+ ""
23435
+ ].join("\n");
23436
+ return `${frontmatter}${body}`;
23437
+ }
23438
+ function parseActivityDigest(raw) {
23439
+ if (typeof raw !== "string" || !raw.startsWith("---\n")) return null;
23440
+ const end = raw.indexOf("\n---\n", 4);
23441
+ if (end === -1) return null;
23442
+ const frontmatter = raw.slice(4, end);
23443
+ const body = raw.slice(end + 5).replace(/^\n/, "");
23444
+ const fields = /* @__PURE__ */ new Map();
23445
+ for (const line of frontmatter.split("\n")) {
23446
+ const idx = line.indexOf(":");
23447
+ if (idx === -1) continue;
23448
+ fields.set(line.slice(0, idx).trim(), line.slice(idx + 1).trim());
23449
+ }
23450
+ const date = fields.get("date");
23451
+ const contentHash = fields.get("contentHash");
23452
+ if (date === void 0 || !isValidActivityDate(date) || contentHash === void 0) return null;
23453
+ if (fields.get("kind") !== "activity-digest") return null;
23454
+ const machines = parseMachinesList(fields.get("machines") ?? "[]");
23455
+ const snapshotCount = parseNonNegativeInt(fields.get("snapshotCount"));
23456
+ const formatVersion = parseNonNegativeInt(fields.get("formatVersion"));
23457
+ if (snapshotCount === null || formatVersion === null) return null;
23458
+ return {
23459
+ meta: {
23460
+ kind: "activity-digest",
23461
+ date,
23462
+ machines,
23463
+ snapshotCount,
23464
+ contentHash,
23465
+ formatVersion
23466
+ },
23467
+ body
23468
+ };
23469
+ }
23470
+ function parseNonNegativeInt(value) {
23471
+ if (value === void 0) return null;
23472
+ const trimmed = value.trim();
23473
+ if (!/^\d+$/.test(trimmed)) return null;
23474
+ const parsed = Number(trimmed);
23475
+ return Number.isSafeInteger(parsed) ? parsed : null;
23476
+ }
23477
+ function parseMachinesList(raw) {
23478
+ try {
23479
+ const parsed = JSON.parse(raw);
23480
+ if (Array.isArray(parsed)) {
23481
+ return parsed.filter((entry) => typeof entry === "string");
23482
+ }
23483
+ } catch {
23484
+ }
23485
+ return raw.replace(/^\[/, "").replace(/\]$/, "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
23486
+ }
23487
+
23488
+ // src/session-summaries/index.ts
23489
+ import { createHash as createHash10 } from "crypto";
23490
+ import { lstat as lstat2, mkdir as mkdir8, readFile as readFile6, readdir as readdir4, rename, rm, stat as stat4, writeFile as writeFile7 } from "fs/promises";
23491
+ import path31 from "path";
22919
23492
 
22920
23493
  // src/session-summaries/adapters.ts
22921
23494
  var VALID_ROLES = /* @__PURE__ */ new Set(["user", "assistant", "tool", "system", "other"]);
@@ -23415,12 +23988,12 @@ var DEFAULT_MAX_FILES = 5e3;
23415
23988
  var DEFAULT_MAX_SESSIONS = 500;
23416
23989
  var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".jsonl"]);
23417
23990
  function shortHash(value, length = 16) {
23418
- return createHash9("sha256").update(value).digest("hex").slice(0, length);
23991
+ return createHash10("sha256").update(value).digest("hex").slice(0, length);
23419
23992
  }
23420
23993
  async function listTranscriptFiles(root, maxFiles) {
23421
23994
  const out = [];
23422
23995
  let truncated = false;
23423
- const entrySortKey = (entryName, isDirectory) => isDirectory ? `${entryName}${path29.sep}` : entryName;
23996
+ const entrySortKey = (entryName, isDirectory) => isDirectory ? `${entryName}${path31.sep}` : entryName;
23424
23997
  async function visit(dir) {
23425
23998
  if (truncated) return;
23426
23999
  const entries = (await readdir4(dir, { withFileTypes: true })).sort(
@@ -23430,13 +24003,13 @@ async function listTranscriptFiles(root, maxFiles) {
23430
24003
  if (truncated) return;
23431
24004
  if (entry.name.startsWith(".")) continue;
23432
24005
  if (entry.isSymbolicLink()) continue;
23433
- const fullPath = path29.join(dir, entry.name);
24006
+ const fullPath = path31.join(dir, entry.name);
23434
24007
  if (entry.isDirectory()) {
23435
24008
  await visit(fullPath);
23436
24009
  continue;
23437
24010
  }
23438
24011
  if (!entry.isFile()) continue;
23439
- const ext = path29.extname(entry.name).toLowerCase();
24012
+ const ext = path31.extname(entry.name).toLowerCase();
23440
24013
  if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
23441
24014
  out.push(fullPath);
23442
24015
  if (out.length > maxFiles) {
@@ -23463,7 +24036,7 @@ function normalizeInputDir(inputDir) {
23463
24036
  if (typeof inputDir !== "string" || inputDir.trim().length === 0) {
23464
24037
  throw new Error("inputDir must be a non-empty string");
23465
24038
  }
23466
- return path29.resolve(expandTildePath(inputDir.trim()));
24039
+ return path31.resolve(expandTildePath(inputDir.trim()));
23467
24040
  }
23468
24041
  function initRoleCounts() {
23469
24042
  return {
@@ -23620,11 +24193,11 @@ async function collectLocalSessionSummaries(options) {
23620
24193
  continue;
23621
24194
  }
23622
24195
  seenFileHashes.add(fileHash);
23623
- const fileExtension = path29.extname(filePath).toLowerCase();
24196
+ const fileExtension = path31.extname(filePath).toLowerCase();
23624
24197
  const parsed = await adapter.parseFile(
23625
24198
  {
23626
24199
  content,
23627
- fileName: path29.basename(filePath),
24200
+ fileName: path31.basename(filePath),
23628
24201
  fileExtension,
23629
24202
  fileRef: fileHash
23630
24203
  },
@@ -23680,7 +24253,7 @@ async function collectLocalSessionSummaries(options) {
23680
24253
  }
23681
24254
  async function readRedactionConfig(pathLike) {
23682
24255
  if (!pathLike) return void 0;
23683
- const raw = await readFile6(path29.resolve(expandTildePath(pathLike)), "utf-8");
24256
+ const raw = await readFile6(path31.resolve(expandTildePath(pathLike)), "utf-8");
23684
24257
  const parsed = JSON.parse(raw);
23685
24258
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
23686
24259
  throw new Error("redaction config must be a JSON object");
@@ -23693,15 +24266,15 @@ function toJsonl(drafts) {
23693
24266
  }
23694
24267
  function defaultDraftOutputPath(memoryDir, generatedAt) {
23695
24268
  const stamp = generatedAt.replace(/[:.]/g, "-");
23696
- return path29.join(
23697
- path29.resolve(expandTildePath(memoryDir)),
24269
+ return path31.join(
24270
+ path31.resolve(expandTildePath(memoryDir)),
23698
24271
  "state",
23699
24272
  "session-summary-drafts",
23700
24273
  `session-summaries-${stamp}.jsonl`
23701
24274
  );
23702
24275
  }
23703
24276
  async function writeDrafts(filePath, drafts) {
23704
- await mkdir7(path29.dirname(filePath), { recursive: true });
24277
+ await mkdir8(path31.dirname(filePath), { recursive: true });
23705
24278
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
23706
24279
  try {
23707
24280
  await writeFile7(tempPath, toJsonl(drafts), "utf-8");
@@ -23719,7 +24292,7 @@ async function runLocalSessionSummaryCliCommand(options) {
23719
24292
  });
23720
24293
  const wroteFiles = [];
23721
24294
  if (options.output) {
23722
- const outputPath = path29.resolve(expandTildePath(options.output));
24295
+ const outputPath = path31.resolve(expandTildePath(options.output));
23723
24296
  await writeDrafts(outputPath, report.drafts);
23724
24297
  wroteFiles.push(outputPath);
23725
24298
  }
@@ -23753,20 +24326,20 @@ async function runLocalSessionSummaryCliCommand(options) {
23753
24326
  }
23754
24327
 
23755
24328
  // src/transfer/capsule-fork.ts
23756
- import { lstat as lstat3, mkdir as mkdir8, readFile as readFile7, realpath as realpath2, writeFile as writeFile8 } from "fs/promises";
23757
- import path30 from "path";
24329
+ import { lstat as lstat3, mkdir as mkdir9, readFile as readFile7, realpath as realpath2, writeFile as writeFile8 } from "fs/promises";
24330
+ import path32 from "path";
23758
24331
  async function forkCapsule(opts) {
23759
24332
  validateForkId(opts.forkId);
23760
- const rootAbs = path30.resolve(opts.targetRoot);
24333
+ const rootAbs = path32.resolve(opts.targetRoot);
23761
24334
  await assertIsDirectoryNotSymlink(rootAbs, "forkCapsule", "targetRoot");
23762
- const forkDirAbs = path30.join(rootAbs, "forks", opts.forkId);
24335
+ const forkDirAbs = path32.join(rootAbs, "forks", opts.forkId);
23763
24336
  const forkEntryExists = await pathEntryExists(forkDirAbs);
23764
24337
  if (forkEntryExists) {
23765
24338
  throw new Error(
23766
24339
  `forkCapsule: fork path already exists \u2014 forkId "${opts.forkId}" is already in use at: ${forkDirAbs}`
23767
24340
  );
23768
24341
  }
23769
- const archiveAbs = path30.resolve(opts.sourceArchive);
24342
+ const archiveAbs = path32.resolve(opts.sourceArchive);
23770
24343
  const importResult = await importCapsule({
23771
24344
  archivePath: archiveAbs,
23772
24345
  root: rootAbs,
@@ -23788,10 +24361,10 @@ async function forkCapsule(opts) {
23788
24361
  importedRecords: importResult.imported.length,
23789
24362
  skippedRecords: importResult.skipped.length
23790
24363
  };
23791
- const lineagePath = path30.join(forkDirAbs, "lineage.json");
24364
+ const lineagePath = path32.join(forkDirAbs, "lineage.json");
23792
24365
  const rootReal = await realpath2(rootAbs);
23793
24366
  await assertRealpathInsideRoot(rootReal, lineagePath, `forks/${opts.forkId}/lineage.json`, "forkCapsule");
23794
- await mkdir8(path30.dirname(lineagePath), { recursive: true });
24367
+ await mkdir9(path32.dirname(lineagePath), { recursive: true });
23795
24368
  await writeFile8(lineagePath, JSON.stringify(lineage, null, 2) + "\n", "utf-8");
23796
24369
  return {
23797
24370
  archivePath: archiveAbs,
@@ -23805,11 +24378,11 @@ async function readForkLineage(targetRoot, forkId) {
23805
24378
  if (typeof forkId !== "string" || forkId.length === 0 || forkId.length > 64 || !CAPSULE_ID_PATTERN.test(forkId)) {
23806
24379
  return null;
23807
24380
  }
23808
- const rootAbs = path30.resolve(targetRoot);
24381
+ const rootAbs = path32.resolve(targetRoot);
23809
24382
  const rootReal = await realpath2(rootAbs).catch(() => rootAbs);
23810
- const lineagePath = path30.join(rootReal, "forks", forkId, "lineage.json");
23811
- const rel = path30.relative(rootReal, lineagePath);
23812
- if (rel.startsWith("..") || path30.isAbsolute(rel)) {
24383
+ const lineagePath = path32.join(rootReal, "forks", forkId, "lineage.json");
24384
+ const rel = path32.relative(rootReal, lineagePath);
24385
+ if (rel.startsWith("..") || path32.isAbsolute(rel)) {
23813
24386
  return null;
23814
24387
  }
23815
24388
  if (!await isLineagePathContained(rootReal, lineagePath)) {
@@ -23850,19 +24423,19 @@ async function pathEntryExists(absPath) {
23850
24423
  async function isLineagePathContained(rootReal, lineagePath) {
23851
24424
  let existing = lineagePath;
23852
24425
  const suffix = [];
23853
- while (existing !== path30.dirname(existing)) {
24426
+ while (existing !== path32.dirname(existing)) {
23854
24427
  const st = await lstat3(existing).catch(() => null);
23855
24428
  if (st !== null) break;
23856
- suffix.unshift(path30.basename(existing));
23857
- existing = path30.dirname(existing);
24429
+ suffix.unshift(path32.basename(existing));
24430
+ existing = path32.dirname(existing);
23858
24431
  }
23859
24432
  const existingReal = await realpath2(existing).catch(() => existing);
23860
- const targetReal = suffix.length > 0 ? path30.join(existingReal, ...suffix) : existingReal;
23861
- const rel = path30.relative(rootReal, targetReal);
24433
+ const targetReal = suffix.length > 0 ? path32.join(existingReal, ...suffix) : existingReal;
24434
+ const rel = path32.relative(rootReal, targetReal);
23862
24435
  if (rel === "") return true;
23863
24436
  if (rel === "..") return false;
23864
- if (rel.startsWith(`..${path30.sep}`)) return false;
23865
- if (path30.isAbsolute(rel)) return false;
24437
+ if (rel.startsWith(`..${path32.sep}`)) return false;
24438
+ if (path32.isAbsolute(rel)) return false;
23866
24439
  return true;
23867
24440
  }
23868
24441
 
@@ -23989,6 +24562,21 @@ export {
23989
24562
  ensureBuiltInWearableConnectors,
23990
24563
  WearablesService,
23991
24564
  locateTranscriptPath,
24565
+ activityDatabasePath,
24566
+ ensureActivityStateDir,
24567
+ openActivityDatabase,
24568
+ applyActivitySchema,
24569
+ ActivityStore,
24570
+ ACTIVITY_DIGEST_FORMAT_VERSION,
24571
+ ACTIVITY_DIR_NAME,
24572
+ isValidActivityDate,
24573
+ activityDigestPath,
24574
+ activityDayWindow,
24575
+ composeActivityDigestBody,
24576
+ hashActivityBody,
24577
+ composeActivityDigestMeta,
24578
+ serializeActivityDigest,
24579
+ parseActivityDigest,
23992
24580
  registerLocalSessionSourceAdapter,
23993
24581
  getLocalSessionSourceAdapter,
23994
24582
  listLocalSessionSourceAdapters,
@@ -24004,4 +24592,4 @@ export {
24004
24592
  blendGraphExpandedRecallScore,
24005
24593
  Orchestrator
24006
24594
  };
24007
- //# sourceMappingURL=chunk-7PY55HMI.js.map
24595
+ //# sourceMappingURL=chunk-3VBXP5HS.js.map