@ascenda-one/history-import 0.1.12 → 0.1.13

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/dist/cli.js CHANGED
@@ -242,10 +242,29 @@ var require_afterHours = __commonJS({
242
242
  "../packages/tool-kit/out/afterHours.js"(exports) {
243
243
  "use strict";
244
244
  Object.defineProperty(exports, "__esModule", { value: true });
245
- exports.isAfterHours = isAfterHours4;
245
+ exports.BUSINESS_DAY = void 0;
246
+ exports.isOutsideBusinessHours = isOutsideBusinessHours2;
247
+ exports.isAfterHours = isAfterHours3;
246
248
  exports.utcOffsetMinutesAt = utcOffsetMinutesAt2;
247
249
  exports.localHourAt = localHourAt;
248
- function isAfterHours4(now = /* @__PURE__ */ new Date(), start = "19:00", end = "07:00") {
250
+ exports.BUSINESS_DAY = {
251
+ /** 0 = Sunday … 6 = Saturday. */
252
+ days: [1, 2, 3, 4, 5],
253
+ start: "09:00",
254
+ end: "17:00"
255
+ };
256
+ function isOutsideBusinessHours2(at = /* @__PURE__ */ new Date(), opts = {}) {
257
+ const days = opts.days ?? exports.BUSINESS_DAY.days;
258
+ if (!days.includes(at.getDay()))
259
+ return true;
260
+ const minutes = at.getHours() * 60 + at.getMinutes();
261
+ const start = parseTimeToMinutes(opts.start ?? exports.BUSINESS_DAY.start, 9 * 60);
262
+ const end = parseTimeToMinutes(opts.end ?? exports.BUSINESS_DAY.end, 17 * 60);
263
+ if (start >= end)
264
+ return false;
265
+ return minutes < start || minutes >= end;
266
+ }
267
+ function isAfterHours3(now = /* @__PURE__ */ new Date(), start = "19:00", end = "07:00") {
249
268
  const currentMinutes = now.getHours() * 60 + now.getMinutes();
250
269
  const startMinutes = parseTimeToMinutes(start, 19 * 60);
251
270
  const endMinutes = parseTimeToMinutes(end, 7 * 60);
@@ -304,9 +323,9 @@ var require_payload = __commonJS({
304
323
  }
305
324
  return void 0;
306
325
  }
307
- function getNested(input, path9) {
326
+ function getNested(input, path10) {
308
327
  let current = input;
309
- for (const segment of path9) {
328
+ for (const segment of path10) {
310
329
  if (!current || typeof current !== "object")
311
330
  return void 0;
312
331
  current = current[segment];
@@ -314,16 +333,16 @@ var require_payload = __commonJS({
314
333
  return current;
315
334
  }
316
335
  function getNestedString(input, paths) {
317
- for (const path9 of paths) {
318
- const value = getNested(input, path9);
336
+ for (const path10 of paths) {
337
+ const value = getNested(input, path10);
319
338
  if (typeof value === "string" && value.trim())
320
339
  return value;
321
340
  }
322
341
  return void 0;
323
342
  }
324
343
  function getNestedNumber(input, paths) {
325
- for (const path9 of paths) {
326
- const value = getNested(input, path9);
344
+ for (const path10 of paths) {
345
+ const value = getNested(input, path10);
327
346
  if (typeof value === "number" && Number.isFinite(value))
328
347
  return value;
329
348
  }
@@ -471,9 +490,9 @@ var require_eventLog = __commonJS({
471
490
  exports.expandUserPath = expandUserPath;
472
491
  exports.resolveEventLogPath = resolveEventLogPath;
473
492
  exports.appendEventLog = appendEventLog;
474
- var fs9 = __importStar(__require("fs"));
493
+ var fs10 = __importStar(__require("fs"));
475
494
  var os3 = __importStar(__require("os"));
476
- var path9 = __importStar(__require("path"));
495
+ var path10 = __importStar(__require("path"));
477
496
  exports.EVENT_LOG_ENV_VAR = "ASCENDA_EVENT_LOG_FILE";
478
497
  var MAX_BYTES = 5 * 1024 * 1024;
479
498
  function expandUserPath(configured) {
@@ -483,8 +502,8 @@ var require_eventLog = __commonJS({
483
502
  if (value === "~")
484
503
  return os3.homedir();
485
504
  if (value.startsWith("~/"))
486
- return path9.join(os3.homedir(), value.slice(2));
487
- return path9.resolve(value);
505
+ return path10.join(os3.homedir(), value.slice(2));
506
+ return path10.resolve(value);
488
507
  }
489
508
  function resolveEventLogPath() {
490
509
  return expandUserPath(process.env[exports.EVENT_LOG_ENV_VAR]);
@@ -492,20 +511,20 @@ var require_eventLog = __commonJS({
492
511
  function appendEventLog(logFilePath, entry) {
493
512
  try {
494
513
  rotateIfLarge(logFilePath);
495
- const dir = path9.dirname(logFilePath);
496
- fs9.mkdirSync(dir, { recursive: true, mode: 448 });
497
- fs9.appendFileSync(logFilePath, `${JSON.stringify(entry)}
514
+ const dir = path10.dirname(logFilePath);
515
+ fs10.mkdirSync(dir, { recursive: true, mode: 448 });
516
+ fs10.appendFileSync(logFilePath, `${JSON.stringify(entry)}
498
517
  `, { encoding: "utf8", mode: 384 });
499
518
  if (process.platform !== "win32")
500
- fs9.chmodSync(logFilePath, 384);
519
+ fs10.chmodSync(logFilePath, 384);
501
520
  } catch {
502
521
  }
503
522
  }
504
523
  function rotateIfLarge(logFilePath) {
505
524
  try {
506
- if (fs9.statSync(logFilePath).size < MAX_BYTES)
525
+ if (fs10.statSync(logFilePath).size < MAX_BYTES)
507
526
  return;
508
- fs9.renameSync(logFilePath, `${logFilePath}.1`);
527
+ fs10.renameSync(logFilePath, `${logFilePath}.1`);
509
528
  } catch {
510
529
  }
511
530
  }
@@ -660,26 +679,26 @@ var require_tokenStore = __commonJS({
660
679
  exports.persistEventWriteToken = persistEventWriteToken;
661
680
  exports.readTokenFile = readTokenFile2;
662
681
  exports.sanitizeFilePart = sanitizeFilePart;
663
- var fs9 = __importStar(__require("fs"));
682
+ var fs10 = __importStar(__require("fs"));
664
683
  var os3 = __importStar(__require("os"));
665
- var path9 = __importStar(__require("path"));
684
+ var path10 = __importStar(__require("path"));
666
685
  function defaultTokenFilePath2(toolInstallationId) {
667
- return path9.join(os3.homedir(), ".ascenda", "tokens", sanitizeFilePart(toolInstallationId));
686
+ return path10.join(os3.homedir(), ".ascenda", "tokens", sanitizeFilePart(toolInstallationId));
668
687
  }
669
688
  function persistEventWriteToken(tokenFilePath, token) {
670
- const dir = path9.dirname(tokenFilePath);
671
- fs9.mkdirSync(dir, { recursive: true, mode: 448 });
672
- fs9.writeFileSync(tokenFilePath, token, { encoding: "utf8", mode: 384 });
689
+ const dir = path10.dirname(tokenFilePath);
690
+ fs10.mkdirSync(dir, { recursive: true, mode: 448 });
691
+ fs10.writeFileSync(tokenFilePath, token, { encoding: "utf8", mode: 384 });
673
692
  if (process.platform !== "win32") {
674
- fs9.chmodSync(dir, 448);
675
- fs9.chmodSync(tokenFilePath, 384);
693
+ fs10.chmodSync(dir, 448);
694
+ fs10.chmodSync(tokenFilePath, 384);
676
695
  }
677
696
  }
678
697
  function readTokenFile2(tokenFilePath) {
679
698
  try {
680
- if (!fs9.existsSync(tokenFilePath))
699
+ if (!fs10.existsSync(tokenFilePath))
681
700
  return void 0;
682
- const value = fs9.readFileSync(tokenFilePath, "utf8").trim();
701
+ const value = fs10.readFileSync(tokenFilePath, "utf8").trim();
683
702
  return value || void 0;
684
703
  } catch {
685
704
  return void 0;
@@ -738,20 +757,20 @@ var require_stateStore = __commonJS({
738
757
  exports.recordSendOutcome = recordSendOutcome;
739
758
  exports.shouldAnnounceFailure = shouldAnnounceFailure;
740
759
  exports.markFailureNotified = markFailureNotified;
741
- var fs9 = __importStar(__require("fs"));
760
+ var fs10 = __importStar(__require("fs"));
742
761
  var os3 = __importStar(__require("os"));
743
- var path9 = __importStar(__require("path"));
762
+ var path10 = __importStar(__require("path"));
744
763
  var tokenStore_1 = require_tokenStore();
745
764
  function defaultStateFilePath(toolInstallationId) {
746
765
  const dir = process.env.ASCENDA_STATE_DIR?.trim();
747
- const base = dir ? dir : path9.join(os3.homedir(), ".ascenda", "state");
748
- return path9.join(base, `${(0, tokenStore_1.sanitizeFilePart)(toolInstallationId)}.json`);
766
+ const base = dir ? dir : path10.join(os3.homedir(), ".ascenda", "state");
767
+ return path10.join(base, `${(0, tokenStore_1.sanitizeFilePart)(toolInstallationId)}.json`);
749
768
  }
750
769
  function readCollectorState(stateFilePath) {
751
770
  try {
752
- if (!fs9.existsSync(stateFilePath))
771
+ if (!fs10.existsSync(stateFilePath))
753
772
  return void 0;
754
- const raw = fs9.readFileSync(stateFilePath, "utf8").trim();
773
+ const raw = fs10.readFileSync(stateFilePath, "utf8").trim();
755
774
  if (!raw)
756
775
  return void 0;
757
776
  const parsed = JSON.parse(raw);
@@ -797,17 +816,17 @@ var require_stateStore = __commonJS({
797
816
  function writeStateFile(stateFilePath, state) {
798
817
  const temporaryPath = `${stateFilePath}.${process.pid}.tmp`;
799
818
  try {
800
- const dir = path9.dirname(stateFilePath);
801
- fs9.mkdirSync(dir, { recursive: true, mode: 448 });
802
- fs9.writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}
819
+ const dir = path10.dirname(stateFilePath);
820
+ fs10.mkdirSync(dir, { recursive: true, mode: 448 });
821
+ fs10.writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}
803
822
  `, { encoding: "utf8", mode: 384 });
804
- fs9.renameSync(temporaryPath, stateFilePath);
823
+ fs10.renameSync(temporaryPath, stateFilePath);
805
824
  if (process.platform !== "win32")
806
- fs9.chmodSync(stateFilePath, 384);
825
+ fs10.chmodSync(stateFilePath, 384);
807
826
  } catch {
808
827
  try {
809
- if (fs9.existsSync(temporaryPath))
810
- fs9.unlinkSync(temporaryPath);
828
+ if (fs10.existsSync(temporaryPath))
829
+ fs10.unlinkSync(temporaryPath);
811
830
  } catch {
812
831
  }
813
832
  }
@@ -847,6 +866,7 @@ var require_eventSender = __commonJS({
847
866
  utcOffsetMinutes: (0, afterHours_1.utcOffsetMinutesAt)(/* @__PURE__ */ new Date()),
848
867
  sessionId: identity.sessionId ?? void 0,
849
868
  workspaceHash: identity.workspaceHash ?? void 0,
869
+ projectHash: identity.projectHash ?? void 0,
850
870
  consentScope: tool_contract_1.ASCENDA_CONSENT_SCOPE,
851
871
  provenance: tool_contract_1.ASCENDA_PROVENANCE,
852
872
  privacyMode: "metadata_only",
@@ -902,6 +922,7 @@ var require_eventSender = __commonJS({
902
922
  severity: "low",
903
923
  sessionId: this.config.sessionId ?? void 0,
904
924
  workspaceHash: this.config.workspaceHash ?? void 0,
925
+ projectHash: this.config.projectHash ?? void 0,
905
926
  consentScope: tool_contract_1.ASCENDA_SEMANTIC_CONSENT_SCOPE,
906
927
  provenance: tool_contract_1.ASCENDA_SEMANTIC_PROVENANCE,
907
928
  privacyMode: "metadata_only",
@@ -931,6 +952,7 @@ var require_eventSender = __commonJS({
931
952
  severity: "low",
932
953
  sessionId: this.config.sessionId ?? void 0,
933
954
  workspaceHash: this.config.workspaceHash ?? void 0,
955
+ projectHash: this.config.projectHash ?? void 0,
934
956
  consentScope: tool_contract_1.ASCENDA_COLLABORATION_CONSENT_SCOPE,
935
957
  provenance: tool_contract_1.ASCENDA_PROVENANCE,
936
958
  privacyMode: "metadata_only",
@@ -970,7 +992,7 @@ var require_eventSender = __commonJS({
970
992
  return (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
971
993
  }
972
994
  if (outcome.result === "transport_error" && (outcome.httpStatus === void 0 || (0, http_1.isRetryableStatus)(outcome.httpStatus))) {
973
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
995
+ await new Promise((resolve2) => setTimeout(resolve2, RETRY_DELAY_MS));
974
996
  return (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
975
997
  }
976
998
  return outcome;
@@ -1021,19 +1043,387 @@ var require_eventSender = __commonJS({
1021
1043
  }
1022
1044
  });
1023
1045
 
1046
+ // ../packages/tool-kit/out/contextRegistry.js
1047
+ var require_contextRegistry = __commonJS({
1048
+ "../packages/tool-kit/out/contextRegistry.js"(exports) {
1049
+ "use strict";
1050
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
1051
+ if (k2 === void 0) k2 = k;
1052
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1053
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1054
+ desc = { enumerable: true, get: function() {
1055
+ return m[k];
1056
+ } };
1057
+ }
1058
+ Object.defineProperty(o, k2, desc);
1059
+ } : function(o, m, k, k2) {
1060
+ if (k2 === void 0) k2 = k;
1061
+ o[k2] = m[k];
1062
+ });
1063
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
1064
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
1065
+ } : function(o, v) {
1066
+ o["default"] = v;
1067
+ });
1068
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
1069
+ var ownKeys = function(o) {
1070
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
1071
+ var ar = [];
1072
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1073
+ return ar;
1074
+ };
1075
+ return ownKeys(o);
1076
+ };
1077
+ return function(mod) {
1078
+ if (mod && mod.__esModule) return mod;
1079
+ var result = {};
1080
+ if (mod != null) {
1081
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1082
+ }
1083
+ __setModuleDefault(result, mod);
1084
+ return result;
1085
+ };
1086
+ }();
1087
+ Object.defineProperty(exports, "__esModule", { value: true });
1088
+ exports.workContextRegistryFilePath = workContextRegistryFilePath;
1089
+ exports.readWorkContextRegistry = readWorkContextRegistry;
1090
+ exports.recordWorkContext = recordWorkContext2;
1091
+ exports.recordWorkContextAlias = recordWorkContextAlias2;
1092
+ var fs10 = __importStar(__require("fs"));
1093
+ var os3 = __importStar(__require("os"));
1094
+ var path10 = __importStar(__require("path"));
1095
+ var MAX_PATHS_PER_ENTRY = 8;
1096
+ function workContextRegistryFilePath() {
1097
+ return path10.join(os3.homedir(), ".ascenda", "work-contexts.json");
1098
+ }
1099
+ function readWorkContextRegistry(registryFilePath = workContextRegistryFilePath()) {
1100
+ try {
1101
+ const parsed = JSON.parse(fs10.readFileSync(registryFilePath, "utf8"));
1102
+ if (parsed && parsed.version === 1 && parsed.contexts && typeof parsed.contexts === "object") {
1103
+ return parsed;
1104
+ }
1105
+ } catch {
1106
+ }
1107
+ return { version: 1, contexts: {} };
1108
+ }
1109
+ function recordWorkContext2(context, options) {
1110
+ if (!context)
1111
+ return false;
1112
+ const updates = [];
1113
+ if (context.projectHash && context.projectLabel) {
1114
+ updates.push({ hash: context.projectHash, kind: "project", label: context.projectLabel, observedPath: context.projectPath ?? context.workspacePath });
1115
+ }
1116
+ if (context.workspaceHash && context.workspaceLabel && context.workspaceHash !== context.projectHash) {
1117
+ updates.push({ hash: context.workspaceHash, kind: "workspace", label: context.workspaceLabel, observedPath: context.workspacePath });
1118
+ }
1119
+ return upsert(updates, options);
1120
+ }
1121
+ function recordWorkContextAlias2(hash, label, observedPath, options) {
1122
+ if (!hash || !label)
1123
+ return false;
1124
+ return upsert([{ hash, kind: "alias", label, observedPath: observedPath ?? null }], options);
1125
+ }
1126
+ function upsert(updates, options) {
1127
+ if (updates.length === 0)
1128
+ return false;
1129
+ try {
1130
+ const registryFilePath = options?.registryFilePath ?? workContextRegistryFilePath();
1131
+ const nowIso = (options?.now ?? /* @__PURE__ */ new Date()).toISOString();
1132
+ const registry = readWorkContextRegistry(registryFilePath);
1133
+ let dirty = false;
1134
+ for (const update of updates) {
1135
+ const existing = registry.contexts[update.hash];
1136
+ if (!existing) {
1137
+ registry.contexts[update.hash] = {
1138
+ kind: update.kind,
1139
+ label: update.label,
1140
+ paths: update.observedPath ? [update.observedPath] : [],
1141
+ firstSeenAt: nowIso,
1142
+ lastSeenAt: nowIso
1143
+ };
1144
+ dirty = true;
1145
+ continue;
1146
+ }
1147
+ if (existing.kind === "alias" && update.kind !== "alias") {
1148
+ existing.kind = update.kind;
1149
+ dirty = true;
1150
+ }
1151
+ if (existing.label !== update.label && update.kind !== "alias") {
1152
+ existing.label = update.label;
1153
+ dirty = true;
1154
+ }
1155
+ if (update.observedPath && !existing.paths.includes(update.observedPath)) {
1156
+ if (existing.paths.length < MAX_PATHS_PER_ENTRY)
1157
+ existing.paths.push(update.observedPath);
1158
+ dirty = true;
1159
+ }
1160
+ if (dayOf(existing.lastSeenAt) !== dayOf(nowIso)) {
1161
+ existing.lastSeenAt = nowIso;
1162
+ dirty = true;
1163
+ }
1164
+ }
1165
+ if (!dirty)
1166
+ return false;
1167
+ writeRegistry(registryFilePath, registry);
1168
+ return true;
1169
+ } catch {
1170
+ return false;
1171
+ }
1172
+ }
1173
+ function dayOf(iso) {
1174
+ return iso.slice(0, 10);
1175
+ }
1176
+ function writeRegistry(registryFilePath, registry) {
1177
+ const dir = path10.dirname(registryFilePath);
1178
+ fs10.mkdirSync(dir, { recursive: true, mode: 448 });
1179
+ const tmp = `${registryFilePath}.${process.pid}.tmp`;
1180
+ fs10.writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}
1181
+ `, { encoding: "utf8", mode: 384 });
1182
+ fs10.renameSync(tmp, registryFilePath);
1183
+ if (process.platform !== "win32") {
1184
+ fs10.chmodSync(registryFilePath, 384);
1185
+ }
1186
+ }
1187
+ }
1188
+ });
1189
+
1190
+ // ../packages/tool-kit/out/salt.js
1191
+ var require_salt = __commonJS({
1192
+ "../packages/tool-kit/out/salt.js"(exports) {
1193
+ "use strict";
1194
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
1195
+ if (k2 === void 0) k2 = k;
1196
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1197
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1198
+ desc = { enumerable: true, get: function() {
1199
+ return m[k];
1200
+ } };
1201
+ }
1202
+ Object.defineProperty(o, k2, desc);
1203
+ } : function(o, m, k, k2) {
1204
+ if (k2 === void 0) k2 = k;
1205
+ o[k2] = m[k];
1206
+ });
1207
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
1208
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
1209
+ } : function(o, v) {
1210
+ o["default"] = v;
1211
+ });
1212
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
1213
+ var ownKeys = function(o) {
1214
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
1215
+ var ar = [];
1216
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1217
+ return ar;
1218
+ };
1219
+ return ownKeys(o);
1220
+ };
1221
+ return function(mod) {
1222
+ if (mod && mod.__esModule) return mod;
1223
+ var result = {};
1224
+ if (mod != null) {
1225
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1226
+ }
1227
+ __setModuleDefault(result, mod);
1228
+ return result;
1229
+ };
1230
+ }();
1231
+ Object.defineProperty(exports, "__esModule", { value: true });
1232
+ exports.machineSaltFilePath = machineSaltFilePath;
1233
+ exports.readOrCreateMachineSalt = readOrCreateMachineSalt;
1234
+ exports.hashWithMachineSalt = hashWithMachineSalt2;
1235
+ var crypto = __importStar(__require("crypto"));
1236
+ var fs10 = __importStar(__require("fs"));
1237
+ var os3 = __importStar(__require("os"));
1238
+ var path10 = __importStar(__require("path"));
1239
+ function machineSaltFilePath() {
1240
+ return path10.join(os3.homedir(), ".ascenda", "salt");
1241
+ }
1242
+ var cache = /* @__PURE__ */ new Map();
1243
+ function readOrCreateMachineSalt(saltFilePath = machineSaltFilePath()) {
1244
+ const hit = cache.get(saltFilePath);
1245
+ if (hit)
1246
+ return hit;
1247
+ const dir = path10.dirname(saltFilePath);
1248
+ fs10.mkdirSync(dir, { recursive: true, mode: 448 });
1249
+ let salt;
1250
+ try {
1251
+ salt = crypto.randomBytes(32).toString("hex");
1252
+ fs10.writeFileSync(saltFilePath, salt, { encoding: "utf8", mode: 384, flag: "wx" });
1253
+ } catch (error) {
1254
+ if (error.code !== "EEXIST")
1255
+ throw error;
1256
+ salt = fs10.readFileSync(saltFilePath, "utf8").trim();
1257
+ }
1258
+ if (process.platform !== "win32") {
1259
+ fs10.chmodSync(dir, 448);
1260
+ fs10.chmodSync(saltFilePath, 384);
1261
+ }
1262
+ cache.set(saltFilePath, salt);
1263
+ return salt;
1264
+ }
1265
+ function hashWithMachineSalt2(value, saltFilePath) {
1266
+ if (!value)
1267
+ return null;
1268
+ return crypto.createHash("sha256").update(readOrCreateMachineSalt(saltFilePath)).update("\0").update(value).digest("hex").slice(0, 16);
1269
+ }
1270
+ }
1271
+ });
1272
+
1273
+ // ../packages/tool-kit/out/workContext.js
1274
+ var require_workContext = __commonJS({
1275
+ "../packages/tool-kit/out/workContext.js"(exports) {
1276
+ "use strict";
1277
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
1278
+ if (k2 === void 0) k2 = k;
1279
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1280
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1281
+ desc = { enumerable: true, get: function() {
1282
+ return m[k];
1283
+ } };
1284
+ }
1285
+ Object.defineProperty(o, k2, desc);
1286
+ } : function(o, m, k, k2) {
1287
+ if (k2 === void 0) k2 = k;
1288
+ o[k2] = m[k];
1289
+ });
1290
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
1291
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
1292
+ } : function(o, v) {
1293
+ o["default"] = v;
1294
+ });
1295
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
1296
+ var ownKeys = function(o) {
1297
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
1298
+ var ar = [];
1299
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1300
+ return ar;
1301
+ };
1302
+ return ownKeys(o);
1303
+ };
1304
+ return function(mod) {
1305
+ if (mod && mod.__esModule) return mod;
1306
+ var result = {};
1307
+ if (mod != null) {
1308
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1309
+ }
1310
+ __setModuleDefault(result, mod);
1311
+ return result;
1312
+ };
1313
+ }();
1314
+ Object.defineProperty(exports, "__esModule", { value: true });
1315
+ exports.deriveWorkContext = deriveWorkContext2;
1316
+ var fs10 = __importStar(__require("fs"));
1317
+ var path10 = __importStar(__require("path"));
1318
+ var salt_1 = require_salt();
1319
+ var MAX_WALK_DEPTH = 64;
1320
+ function deriveWorkContext2(cwd, saltFilePath) {
1321
+ if (!cwd || !cwd.trim())
1322
+ return null;
1323
+ const startPath = stripTrailingSeparators(cwd.trim());
1324
+ let roots = null;
1325
+ try {
1326
+ roots = resolveRepositoryRoots(startPath);
1327
+ } catch {
1328
+ roots = null;
1329
+ }
1330
+ const workspacePath = roots?.checkoutRoot ?? startPath;
1331
+ const workspaceLabel = basenameOf(workspacePath);
1332
+ if (!workspaceLabel)
1333
+ return null;
1334
+ const projectPath = roots?.canonicalRoot ?? null;
1335
+ const projectLabel = (projectPath ? basenameOf(projectPath) : null) ?? workspaceLabel;
1336
+ return {
1337
+ workspaceLabel,
1338
+ projectLabel,
1339
+ workspaceHash: (0, salt_1.hashWithMachineSalt)(workspaceLabel, saltFilePath),
1340
+ projectHash: (0, salt_1.hashWithMachineSalt)(projectLabel, saltFilePath),
1341
+ workspacePath,
1342
+ projectPath
1343
+ };
1344
+ }
1345
+ function resolveRepositoryRoots(startDir) {
1346
+ if (!fs10.existsSync(startDir))
1347
+ return null;
1348
+ let dir = startDir;
1349
+ for (let depth = 0; depth < MAX_WALK_DEPTH; depth++) {
1350
+ const dotGit = path10.join(dir, ".git");
1351
+ let stat6 = null;
1352
+ try {
1353
+ stat6 = fs10.statSync(dotGit);
1354
+ } catch {
1355
+ stat6 = null;
1356
+ }
1357
+ if (stat6?.isDirectory())
1358
+ return { checkoutRoot: dir, canonicalRoot: dir };
1359
+ if (stat6?.isFile())
1360
+ return { checkoutRoot: dir, canonicalRoot: worktreeParentRoot(dotGit, dir) ?? dir };
1361
+ const parent = path10.dirname(dir);
1362
+ if (parent === dir)
1363
+ return null;
1364
+ dir = parent;
1365
+ }
1366
+ return null;
1367
+ }
1368
+ function worktreeParentRoot(dotGitFile, containingDir) {
1369
+ let gitdir;
1370
+ try {
1371
+ const match = /^gitdir:\s*(.+)\s*$/m.exec(fs10.readFileSync(dotGitFile, "utf8"));
1372
+ if (!match)
1373
+ return null;
1374
+ gitdir = match[1].trim();
1375
+ } catch {
1376
+ return null;
1377
+ }
1378
+ const resolved = path10.resolve(containingDir, gitdir);
1379
+ const marker = `${path10.sep}.git${path10.sep}worktrees${path10.sep}`;
1380
+ const idx = resolved.indexOf(marker);
1381
+ if (idx === -1)
1382
+ return null;
1383
+ return resolved.slice(0, idx);
1384
+ }
1385
+ function stripTrailingSeparators(value) {
1386
+ let end = value.length;
1387
+ while (end > 1 && (value[end - 1] === "/" || value[end - 1] === "\\"))
1388
+ end--;
1389
+ return value.slice(0, end);
1390
+ }
1391
+ function basenameOf(value) {
1392
+ const segment = value.split(/[\\/]/).filter(Boolean).pop() ?? null;
1393
+ return segment && segment.length > 0 ? segment : null;
1394
+ }
1395
+ }
1396
+ });
1397
+
1024
1398
  // ../packages/tool-kit/out/hookAdapter.js
1025
1399
  var require_hookAdapter = __commonJS({
1026
1400
  "../packages/tool-kit/out/hookAdapter.js"(exports) {
1027
1401
  "use strict";
1028
1402
  Object.defineProperty(exports, "__esModule", { value: true });
1029
1403
  exports.DEFAULT_API_BASE_URL = void 0;
1404
+ exports.resolveContextHashes = resolveContextHashes;
1030
1405
  exports.loadCliAgentConfig = loadCliAgentConfig;
1031
1406
  exports.deliverHookEvents = deliverHookEvents;
1407
+ var contextRegistry_1 = require_contextRegistry();
1032
1408
  var eventLog_1 = require_eventLog();
1033
1409
  var eventSender_1 = require_eventSender();
1034
1410
  var tokenStore_1 = require_tokenStore();
1411
+ var workContext_1 = require_workContext();
1035
1412
  exports.DEFAULT_API_BASE_URL = "https://api.ascenda.one";
1036
- function loadCliAgentConfig(toolType, sessionIdFromHook) {
1413
+ function resolveContextHashes(cwd) {
1414
+ const workspaceOverride = process.env.ASCENDA_WORKSPACE_HASH?.trim() || null;
1415
+ const projectOverride = process.env.ASCENDA_PROJECT_HASH?.trim() || null;
1416
+ if (workspaceOverride && projectOverride)
1417
+ return { workspaceHash: workspaceOverride, projectHash: projectOverride };
1418
+ const context = (0, workContext_1.deriveWorkContext)(cwd ?? process.cwd());
1419
+ if (context)
1420
+ (0, contextRegistry_1.recordWorkContext)(context);
1421
+ return {
1422
+ workspaceHash: workspaceOverride ?? context?.workspaceHash ?? null,
1423
+ projectHash: projectOverride ?? context?.projectHash ?? null
1424
+ };
1425
+ }
1426
+ function loadCliAgentConfig(toolType, sessionIdFromHook, cwd) {
1037
1427
  const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? exports.DEFAULT_API_BASE_URL).replace(/\/$/, "");
1038
1428
  const toolInstallationIdRaw = process.env.ASCENDA_TOOL_INSTALLATION_ID;
1039
1429
  if (!toolInstallationIdRaw)
@@ -1046,13 +1436,15 @@ var require_hookAdapter = __commonJS({
1046
1436
  throw new Error("Missing ASCENDA_EVENT_WRITE_TOKEN (or token file)");
1047
1437
  if (!fileToken)
1048
1438
  (0, tokenStore_1.persistEventWriteToken)(tokenFilePath, eventWriteToken);
1439
+ const contextHashes = resolveContextHashes(cwd);
1049
1440
  return {
1050
1441
  apiBaseUrl,
1051
1442
  toolInstallationId,
1052
1443
  eventWriteToken,
1053
1444
  tokenFilePath,
1054
1445
  sessionId: process.env.ASCENDA_SESSION_ID ?? sessionIdFromHook ?? null,
1055
- workspaceHash: process.env.ASCENDA_WORKSPACE_HASH ?? null,
1446
+ workspaceHash: contextHashes.workspaceHash,
1447
+ projectHash: contextHashes.projectHash,
1056
1448
  // Agents await command hooks; fail fast rather than stall the user's turn.
1057
1449
  timeoutMs: parsePositiveInt(process.env.ASCENDA_HTTP_TIMEOUT_MS) ?? 3e3
1058
1450
  };
@@ -1063,11 +1455,12 @@ var require_hookAdapter = __commonJS({
1063
1455
  const notice = options.onNotice ?? ((message) => console.error(message));
1064
1456
  let config;
1065
1457
  try {
1066
- config = loadCliAgentConfig(options.toolType, options.sessionId);
1458
+ config = loadCliAgentConfig(options.toolType, options.sessionId, options.cwd);
1067
1459
  } catch (error) {
1068
1460
  const logFile = (0, eventLog_1.resolveEventLogPath)();
1069
1461
  if (!logFile)
1070
1462
  throw error;
1463
+ const contextHashes = resolveContextHashes(options.cwd);
1071
1464
  for (const event of events) {
1072
1465
  (0, eventLog_1.appendEventLog)(logFile, {
1073
1466
  loggedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1076,7 +1469,8 @@ var require_hookAdapter = __commonJS({
1076
1469
  toolInstallationId: `${options.toolType}:unpaired`,
1077
1470
  source: options.source,
1078
1471
  sessionId: options.sessionId ?? null,
1079
- workspaceHash: process.env.ASCENDA_WORKSPACE_HASH ?? null
1472
+ workspaceHash: contextHashes.workspaceHash,
1473
+ projectHash: contextHashes.projectHash
1080
1474
  }, event)
1081
1475
  });
1082
1476
  }
@@ -1090,6 +1484,7 @@ var require_hookAdapter = __commonJS({
1090
1484
  tokenFilePath: config.tokenFilePath,
1091
1485
  sessionId: config.sessionId,
1092
1486
  workspaceHash: config.workspaceHash,
1487
+ projectHash: config.projectHash,
1093
1488
  timeoutMs: config.timeoutMs
1094
1489
  });
1095
1490
  for (const event of events) {
@@ -1159,12 +1554,12 @@ var require_turnState = __commonJS({
1159
1554
  Object.defineProperty(exports, "__esModule", { value: true });
1160
1555
  exports.recordTurnStart = recordTurnStart;
1161
1556
  exports.consumeTurnDurationMs = consumeTurnDurationMs;
1162
- var fs9 = __importStar(__require("fs"));
1557
+ var fs10 = __importStar(__require("fs"));
1163
1558
  var os3 = __importStar(__require("os"));
1164
- var path9 = __importStar(__require("path"));
1165
- var stateDir = () => process.env.ASCENDA_STATE_DIR ?? path9.join(os3.homedir(), ".ascenda", "state");
1559
+ var path10 = __importStar(__require("path"));
1560
+ var stateDir = () => process.env.ASCENDA_STATE_DIR ?? path10.join(os3.homedir(), ".ascenda", "state");
1166
1561
  function turnFile(agent, sessionId) {
1167
- return path9.join(stateDir(), `${sanitize(agent)}-turn-${sanitize(sessionId)}`);
1562
+ return path10.join(stateDir(), `${sanitize(agent)}-turn-${sanitize(sessionId)}`);
1168
1563
  }
1169
1564
  function sanitize(value) {
1170
1565
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
@@ -1173,8 +1568,8 @@ var require_turnState = __commonJS({
1173
1568
  if (!sessionId)
1174
1569
  return;
1175
1570
  try {
1176
- fs9.mkdirSync(stateDir(), { recursive: true });
1177
- fs9.writeFileSync(turnFile(agent, sessionId), String(now), { encoding: "utf8", mode: 384 });
1571
+ fs10.mkdirSync(stateDir(), { recursive: true });
1572
+ fs10.writeFileSync(turnFile(agent, sessionId), String(now), { encoding: "utf8", mode: 384 });
1178
1573
  } catch {
1179
1574
  }
1180
1575
  }
@@ -1183,8 +1578,8 @@ var require_turnState = __commonJS({
1183
1578
  return void 0;
1184
1579
  const file = turnFile(agent, sessionId);
1185
1580
  try {
1186
- const started = Number(fs9.readFileSync(file, "utf8").trim());
1187
- fs9.rmSync(file, { force: true });
1581
+ const started = Number(fs10.readFileSync(file, "utf8").trim());
1582
+ fs10.rmSync(file, { force: true });
1188
1583
  if (!Number.isFinite(started) || started <= 0 || started > now)
1189
1584
  return void 0;
1190
1585
  return now - started;
@@ -1195,9 +1590,9 @@ var require_turnState = __commonJS({
1195
1590
  }
1196
1591
  });
1197
1592
 
1198
- // ../packages/tool-kit/out/salt.js
1199
- var require_salt = __commonJS({
1200
- "../packages/tool-kit/out/salt.js"(exports) {
1593
+ // ../packages/tool-kit/out/liveBus.js
1594
+ var require_liveBus = __commonJS({
1595
+ "../packages/tool-kit/out/liveBus.js"(exports) {
1201
1596
  "use strict";
1202
1597
  var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
1203
1598
  if (k2 === void 0) k2 = k;
@@ -1237,114 +1632,31 @@ var require_salt = __commonJS({
1237
1632
  };
1238
1633
  }();
1239
1634
  Object.defineProperty(exports, "__esModule", { value: true });
1240
- exports.machineSaltFilePath = machineSaltFilePath;
1241
- exports.readOrCreateMachineSalt = readOrCreateMachineSalt;
1242
- exports.hashWithMachineSalt = hashWithMachineSalt2;
1243
- var crypto = __importStar(__require("crypto"));
1244
- var fs9 = __importStar(__require("fs"));
1635
+ exports.liveBusSocketCandidates = liveBusSocketCandidates;
1636
+ exports.liveBusSocketPath = liveBusSocketPath;
1637
+ exports.bucketPromptSize = bucketPromptSize;
1638
+ exports.emitLiveSignal = emitLiveSignal;
1639
+ var fs10 = __importStar(__require("fs"));
1640
+ var net = __importStar(__require("net"));
1245
1641
  var os3 = __importStar(__require("os"));
1246
- var path9 = __importStar(__require("path"));
1247
- function machineSaltFilePath() {
1248
- return path9.join(os3.homedir(), ".ascenda", "salt");
1249
- }
1250
- var cache = /* @__PURE__ */ new Map();
1251
- function readOrCreateMachineSalt(saltFilePath = machineSaltFilePath()) {
1252
- const hit = cache.get(saltFilePath);
1253
- if (hit)
1254
- return hit;
1255
- const dir = path9.dirname(saltFilePath);
1256
- fs9.mkdirSync(dir, { recursive: true, mode: 448 });
1257
- let salt;
1258
- try {
1259
- salt = crypto.randomBytes(32).toString("hex");
1260
- fs9.writeFileSync(saltFilePath, salt, { encoding: "utf8", mode: 384, flag: "wx" });
1261
- } catch (error) {
1262
- if (error.code !== "EEXIST")
1263
- throw error;
1264
- salt = fs9.readFileSync(saltFilePath, "utf8").trim();
1265
- }
1266
- if (process.platform !== "win32") {
1267
- fs9.chmodSync(dir, 448);
1268
- fs9.chmodSync(saltFilePath, 384);
1269
- }
1270
- cache.set(saltFilePath, salt);
1271
- return salt;
1272
- }
1273
- function hashWithMachineSalt2(value, saltFilePath) {
1274
- if (!value)
1275
- return null;
1276
- return crypto.createHash("sha256").update(readOrCreateMachineSalt(saltFilePath)).update("\0").update(value).digest("hex").slice(0, 16);
1277
- }
1278
- }
1279
- });
1280
-
1281
- // ../packages/tool-kit/out/liveBus.js
1282
- var require_liveBus = __commonJS({
1283
- "../packages/tool-kit/out/liveBus.js"(exports) {
1284
- "use strict";
1285
- var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
1286
- if (k2 === void 0) k2 = k;
1287
- var desc = Object.getOwnPropertyDescriptor(m, k);
1288
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1289
- desc = { enumerable: true, get: function() {
1290
- return m[k];
1291
- } };
1292
- }
1293
- Object.defineProperty(o, k2, desc);
1294
- } : function(o, m, k, k2) {
1295
- if (k2 === void 0) k2 = k;
1296
- o[k2] = m[k];
1297
- });
1298
- var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
1299
- Object.defineProperty(o, "default", { enumerable: true, value: v });
1300
- } : function(o, v) {
1301
- o["default"] = v;
1302
- });
1303
- var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
1304
- var ownKeys = function(o) {
1305
- ownKeys = Object.getOwnPropertyNames || function(o2) {
1306
- var ar = [];
1307
- for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1308
- return ar;
1309
- };
1310
- return ownKeys(o);
1311
- };
1312
- return function(mod) {
1313
- if (mod && mod.__esModule) return mod;
1314
- var result = {};
1315
- if (mod != null) {
1316
- for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1317
- }
1318
- __setModuleDefault(result, mod);
1319
- return result;
1320
- };
1321
- }();
1322
- Object.defineProperty(exports, "__esModule", { value: true });
1323
- exports.liveBusSocketCandidates = liveBusSocketCandidates;
1324
- exports.liveBusSocketPath = liveBusSocketPath;
1325
- exports.bucketPromptSize = bucketPromptSize;
1326
- exports.emitLiveSignal = emitLiveSignal;
1327
- var fs9 = __importStar(__require("fs"));
1328
- var net = __importStar(__require("net"));
1329
- var os3 = __importStar(__require("os"));
1330
- var path9 = __importStar(__require("path"));
1331
- var WRITE_TIMEOUT_MS = 50;
1332
- var APP_BUNDLE_ID2 = "one.ascenda.ascendaMissionControl";
1333
- function liveBusSocketCandidates() {
1334
- const override = process.env.ASCENDA_LIVE_BUS_SOCKET;
1335
- if (override)
1336
- return [override];
1337
- const home = os3.homedir();
1338
- return [
1339
- path9.join(home, ".ascenda", "live.sock"),
1340
- path9.join(home, "Library", "Containers", APP_BUNDLE_ID2, "Data", ".ascenda", "live.sock")
1341
- ];
1642
+ var path10 = __importStar(__require("path"));
1643
+ var WRITE_TIMEOUT_MS = 50;
1644
+ var APP_BUNDLE_ID2 = "one.ascenda.ascendaMissionControl";
1645
+ function liveBusSocketCandidates() {
1646
+ const override = process.env.ASCENDA_LIVE_BUS_SOCKET;
1647
+ if (override)
1648
+ return [override];
1649
+ const home = os3.homedir();
1650
+ return [
1651
+ path10.join(home, ".ascenda", "live.sock"),
1652
+ path10.join(home, "Library", "Containers", APP_BUNDLE_ID2, "Data", ".ascenda", "live.sock")
1653
+ ];
1342
1654
  }
1343
1655
  function liveBusSocketPath() {
1344
1656
  const candidates = liveBusSocketCandidates();
1345
1657
  for (const candidate of candidates) {
1346
1658
  try {
1347
- if (fs9.statSync(candidate).isSocket())
1659
+ if (fs10.statSync(candidate).isSocket())
1348
1660
  return candidate;
1349
1661
  } catch {
1350
1662
  }
@@ -1362,7 +1674,7 @@ var require_liveBus = __commonJS({
1362
1674
  return "xl";
1363
1675
  }
1364
1676
  function emitLiveSignal(signal) {
1365
- return new Promise((resolve) => {
1677
+ return new Promise((resolve2) => {
1366
1678
  let settled = false;
1367
1679
  const done = () => {
1368
1680
  if (settled)
@@ -1372,13 +1684,13 @@ var require_liveBus = __commonJS({
1372
1684
  socket.destroy();
1373
1685
  } catch {
1374
1686
  }
1375
- resolve();
1687
+ resolve2();
1376
1688
  };
1377
1689
  let socket;
1378
1690
  try {
1379
1691
  socket = net.createConnection(liveBusSocketPath());
1380
1692
  } catch {
1381
- resolve();
1693
+ resolve2();
1382
1694
  return;
1383
1695
  }
1384
1696
  const timer = setTimeout(done, WRITE_TIMEOUT_MS);
@@ -1409,8 +1721,8 @@ var require_out2 = __commonJS({
1409
1721
  "../packages/tool-kit/out/index.js"(exports) {
1410
1722
  "use strict";
1411
1723
  Object.defineProperty(exports, "__esModule", { value: true });
1412
- exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.markFailureNotified = exports.shouldAnnounceFailure = exports.recordSendOutcome = exports.readCollectorState = exports.defaultStateFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.recordTurnStart = exports.consumeTurnDurationMs = exports.loadCliAgentConfig = exports.deliverHookEvents = exports.DEFAULT_API_BASE_URL = exports.resolveEventLogPath = exports.expandUserPath = exports.appendEventLog = exports.EVENT_LOG_ENV_VAR = exports.buildEventPayload = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.localHourAt = exports.utcOffsetMinutesAt = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
1413
- exports.isRetryableStatus = exports.parseIngestResponse = exports.postToolEventsBatch = exports.postToolEvent = void 0;
1724
+ exports.workContextRegistryFilePath = exports.readWorkContextRegistry = exports.recordWorkContextAlias = exports.recordWorkContext = exports.deriveWorkContext = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.markFailureNotified = exports.shouldAnnounceFailure = exports.recordSendOutcome = exports.readCollectorState = exports.defaultStateFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.recordTurnStart = exports.consumeTurnDurationMs = exports.resolveContextHashes = exports.loadCliAgentConfig = exports.deliverHookEvents = exports.DEFAULT_API_BASE_URL = exports.resolveEventLogPath = exports.expandUserPath = exports.appendEventLog = exports.EVENT_LOG_ENV_VAR = exports.buildEventPayload = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.localHourAt = exports.utcOffsetMinutesAt = exports.BUSINESS_DAY = exports.isOutsideBusinessHours = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
1725
+ exports.isRetryableStatus = exports.parseIngestResponse = exports.postToolEventsBatch = exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = void 0;
1414
1726
  var commandClassifier_1 = require_commandClassifier();
1415
1727
  Object.defineProperty(exports, "classifyCommand", { enumerable: true, get: function() {
1416
1728
  return commandClassifier_1.classifyCommand;
@@ -1443,6 +1755,12 @@ var require_out2 = __commonJS({
1443
1755
  Object.defineProperty(exports, "isAfterHours", { enumerable: true, get: function() {
1444
1756
  return afterHours_1.isAfterHours;
1445
1757
  } });
1758
+ Object.defineProperty(exports, "isOutsideBusinessHours", { enumerable: true, get: function() {
1759
+ return afterHours_1.isOutsideBusinessHours;
1760
+ } });
1761
+ Object.defineProperty(exports, "BUSINESS_DAY", { enumerable: true, get: function() {
1762
+ return afterHours_1.BUSINESS_DAY;
1763
+ } });
1446
1764
  Object.defineProperty(exports, "utcOffsetMinutesAt", { enumerable: true, get: function() {
1447
1765
  return afterHours_1.utcOffsetMinutesAt;
1448
1766
  } });
@@ -1507,6 +1825,9 @@ var require_out2 = __commonJS({
1507
1825
  Object.defineProperty(exports, "loadCliAgentConfig", { enumerable: true, get: function() {
1508
1826
  return hookAdapter_1.loadCliAgentConfig;
1509
1827
  } });
1828
+ Object.defineProperty(exports, "resolveContextHashes", { enumerable: true, get: function() {
1829
+ return hookAdapter_1.resolveContextHashes;
1830
+ } });
1510
1831
  var turnState_1 = require_turnState();
1511
1832
  Object.defineProperty(exports, "consumeTurnDurationMs", { enumerable: true, get: function() {
1512
1833
  return turnState_1.consumeTurnDurationMs;
@@ -1550,6 +1871,23 @@ var require_out2 = __commonJS({
1550
1871
  Object.defineProperty(exports, "hashWithMachineSalt", { enumerable: true, get: function() {
1551
1872
  return salt_1.hashWithMachineSalt;
1552
1873
  } });
1874
+ var workContext_1 = require_workContext();
1875
+ Object.defineProperty(exports, "deriveWorkContext", { enumerable: true, get: function() {
1876
+ return workContext_1.deriveWorkContext;
1877
+ } });
1878
+ var contextRegistry_1 = require_contextRegistry();
1879
+ Object.defineProperty(exports, "recordWorkContext", { enumerable: true, get: function() {
1880
+ return contextRegistry_1.recordWorkContext;
1881
+ } });
1882
+ Object.defineProperty(exports, "recordWorkContextAlias", { enumerable: true, get: function() {
1883
+ return contextRegistry_1.recordWorkContextAlias;
1884
+ } });
1885
+ Object.defineProperty(exports, "readWorkContextRegistry", { enumerable: true, get: function() {
1886
+ return contextRegistry_1.readWorkContextRegistry;
1887
+ } });
1888
+ Object.defineProperty(exports, "workContextRegistryFilePath", { enumerable: true, get: function() {
1889
+ return contextRegistry_1.workContextRegistryFilePath;
1890
+ } });
1553
1891
  var liveBus_1 = require_liveBus();
1554
1892
  Object.defineProperty(exports, "emitLiveSignal", { enumerable: true, get: function() {
1555
1893
  return liveBus_1.emitLiveSignal;
@@ -1592,8 +1930,8 @@ var require_out2 = __commonJS({
1592
1930
  });
1593
1931
 
1594
1932
  // src/cli.ts
1595
- import * as fs8 from "node:fs/promises";
1596
- import * as path8 from "node:path";
1933
+ import * as fs9 from "node:fs/promises";
1934
+ import * as path9 from "node:path";
1597
1935
 
1598
1936
  // src/stores.ts
1599
1937
  import * as os from "node:os";
@@ -1905,6 +2243,107 @@ async function snapshotVsCodeWorkspaceStorage(area, sourceRoot, label) {
1905
2243
  }
1906
2244
  return dest;
1907
2245
  }
2246
+ var STAGING_KEEP = ["events.jsonl"];
2247
+ async function entrySizeBytes(target) {
2248
+ let st;
2249
+ try {
2250
+ st = await fs3.lstat(target);
2251
+ } catch {
2252
+ return 0;
2253
+ }
2254
+ if (st.isSymbolicLink()) return 0;
2255
+ if (!st.isDirectory()) return st.size;
2256
+ let total = 0;
2257
+ let entries = [];
2258
+ try {
2259
+ entries = await fs3.readdir(target);
2260
+ } catch {
2261
+ return total;
2262
+ }
2263
+ for (const entry of entries) {
2264
+ total += await entrySizeBytes(path3.join(target, entry));
2265
+ }
2266
+ return total;
2267
+ }
2268
+ async function disposeStagingArea(area, keep = STAGING_KEEP) {
2269
+ const result = { freedBytes: 0, kept: [] };
2270
+ let entries = [];
2271
+ try {
2272
+ entries = await fs3.readdir(area.root);
2273
+ } catch {
2274
+ return result;
2275
+ }
2276
+ for (const entry of entries) {
2277
+ if (keep.includes(entry)) {
2278
+ result.kept.push(entry);
2279
+ continue;
2280
+ }
2281
+ const target = path3.join(area.root, entry);
2282
+ const size = await entrySizeBytes(target);
2283
+ try {
2284
+ await fs3.rm(target, { recursive: true, force: true });
2285
+ result.freedBytes += size;
2286
+ } catch {
2287
+ }
2288
+ }
2289
+ return result;
2290
+ }
2291
+ async function sweepStagingRoot(stagingRoot, exceptRun, keep = STAGING_KEEP) {
2292
+ const result = { runsSwept: 0, freedBytes: 0 };
2293
+ let runs = [];
2294
+ try {
2295
+ runs = (await fs3.readdir(stagingRoot, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
2296
+ } catch {
2297
+ return result;
2298
+ }
2299
+ for (const run of runs) {
2300
+ if (run === exceptRun) continue;
2301
+ const disposed = await disposeStagingArea({ extractionId: run, root: path3.join(stagingRoot, run) }, keep);
2302
+ if (disposed.freedBytes > 0) {
2303
+ result.runsSwept += 1;
2304
+ result.freedBytes += disposed.freedBytes;
2305
+ }
2306
+ }
2307
+ return result;
2308
+ }
2309
+ async function freeSpaceBytes(dir) {
2310
+ let probe = path3.resolve(dir);
2311
+ for (; ; ) {
2312
+ try {
2313
+ const st = await fs3.statfs(probe);
2314
+ return Number(st.bsize) * Number(st.bavail);
2315
+ } catch {
2316
+ const parent = path3.dirname(probe);
2317
+ if (parent === probe) return null;
2318
+ probe = parent;
2319
+ }
2320
+ }
2321
+ }
2322
+ async function estimateSnapshotBytes(sources) {
2323
+ let total = 0;
2324
+ for (const source of sources) total += await entrySizeBytes(source);
2325
+ return total;
2326
+ }
2327
+ var SPACE_HEADROOM_BYTES = 2 * 1024 * 1024 * 1024;
2328
+ async function checkSpaceForSnapshot(stagingRoot, sources) {
2329
+ const requiredBytes = await estimateSnapshotBytes(sources) + SPACE_HEADROOM_BYTES;
2330
+ const freeBytes = await freeSpaceBytes(stagingRoot);
2331
+ return {
2332
+ requiredBytes,
2333
+ freeBytes,
2334
+ sufficient: freeBytes === null || freeBytes >= requiredBytes
2335
+ };
2336
+ }
2337
+ function formatBytes(bytes) {
2338
+ const units = ["B", "KB", "MB", "GB", "TB"];
2339
+ let value = bytes;
2340
+ let unit = 0;
2341
+ while (value >= 1024 && unit < units.length - 1) {
2342
+ value /= 1024;
2343
+ unit += 1;
2344
+ }
2345
+ return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
2346
+ }
1908
2347
 
1909
2348
  // src/extractors/claudeCode.ts
1910
2349
  var import_tool_kit = __toESM(require_out2(), 1);
@@ -2122,7 +2561,7 @@ var ASSUMED_CONTEXT_WINDOW_TOKENS = 2e5;
2122
2561
  function contextWindowPeakPctOf(fold) {
2123
2562
  if (fold.contextWindowPeakTokens <= 0) return 0;
2124
2563
  const pct = fold.contextWindowPeakTokens / ASSUMED_CONTEXT_WINDOW_TOKENS;
2125
- return Math.round(Math.min(1, pct) * 1e3) / 1e3;
2564
+ return Math.round(pct * 1e3) / 1e3;
2126
2565
  }
2127
2566
  async function foldLinesInto(fold, filePath, opts) {
2128
2567
  const handle = await fs4.open(filePath);
@@ -2169,7 +2608,7 @@ async function foldLinesInto(fold, filePath, opts) {
2169
2608
  fold.subagentPrompts += 1;
2170
2609
  } else {
2171
2610
  fold.humanPrompts += 1;
2172
- if (sniffed.occurredAt && (0, import_tool_kit.isAfterHours)(new Date(sniffed.occurredAt))) {
2611
+ if (sniffed.occurredAt && (0, import_tool_kit.isOutsideBusinessHours)(new Date(sniffed.occurredAt))) {
2173
2612
  fold.afterHoursPrompts += 1;
2174
2613
  }
2175
2614
  fold.humanPromptTimestamps.push(sniffed.occurredAt);
@@ -2915,6 +3354,10 @@ import { fileURLToPath } from "node:url";
2915
3354
  var KNOWN_HISTORY_VERSION = 1;
2916
3355
  var KNOWN_CHAT_SESSION_VERSION = 3;
2917
3356
  var CHAT_EDIT_PREFIX = "Chat Edit:";
3357
+ function isAbsence(error) {
3358
+ const code = error?.code;
3359
+ return code === "ENOENT" || code === "ENOTDIR";
3360
+ }
2918
3361
  function uriToPath(uri) {
2919
3362
  if (typeof uri !== "string" || !uri.startsWith("file://")) return null;
2920
3363
  try {
@@ -2976,6 +3419,7 @@ async function foldEditDays(historyDir, index) {
2976
3419
  const result = {
2977
3420
  days: /* @__PURE__ */ new Map(),
2978
3421
  unparsedFiles: 0,
3422
+ unreadableFiles: 0,
2979
3423
  malformedEntries: 0,
2980
3424
  oldest: null,
2981
3425
  newest: null
@@ -2990,7 +3434,8 @@ async function foldEditDays(historyDir, index) {
2990
3434
  let raw;
2991
3435
  try {
2992
3436
  raw = await fs6.readFile(path6.join(historyDir, hash, "entries.json"), "utf8");
2993
- } catch {
3437
+ } catch (error) {
3438
+ if (!isAbsence(error)) result.unreadableFiles += 1;
2994
3439
  continue;
2995
3440
  }
2996
3441
  const sniffed = sniffHistoryFile(raw);
@@ -3032,9 +3477,9 @@ async function foldEditDays(historyDir, index) {
3032
3477
  var JSONL_KIND_HEADER = 0;
3033
3478
  var JSONL_KIND_SET = 1;
3034
3479
  var JSONL_KIND_SPLICE = 2;
3035
- function resolvePath(root, path9) {
3480
+ function resolvePath(root, path10) {
3036
3481
  let cur = root;
3037
- for (const seg of path9) {
3482
+ for (const seg of path10) {
3038
3483
  if (typeof seg === "number") {
3039
3484
  if (!Array.isArray(cur) || seg >= cur.length) return null;
3040
3485
  cur = cur[seg];
@@ -3045,10 +3490,10 @@ function resolvePath(root, path9) {
3045
3490
  }
3046
3491
  return cur ?? null;
3047
3492
  }
3048
- function applySet(root, path9, value) {
3049
- if (path9.length === 0) return;
3050
- const parent = resolvePath(root, path9.slice(0, -1));
3051
- const last = path9[path9.length - 1];
3493
+ function applySet(root, path10, value) {
3494
+ if (path10.length === 0) return;
3495
+ const parent = resolvePath(root, path10.slice(0, -1));
3496
+ const last = path10[path10.length - 1];
3052
3497
  if (typeof last === "number") {
3053
3498
  if (!Array.isArray(parent)) return;
3054
3499
  while (parent.length <= last) parent.push(null);
@@ -3058,8 +3503,8 @@ function applySet(root, path9, value) {
3058
3503
  parent[last] = value;
3059
3504
  }
3060
3505
  }
3061
- function applySplice(root, path9, values, index) {
3062
- const target = resolvePath(root, path9);
3506
+ function applySplice(root, path10, values, index) {
3507
+ const target = resolvePath(root, path10);
3063
3508
  if (!Array.isArray(target)) return;
3064
3509
  if (index !== null && index >= 0 && index <= target.length) target.splice(index, 0, ...values);
3065
3510
  else target.push(...values);
@@ -3116,6 +3561,7 @@ async function foldChatSessions(workspaceStorageDir) {
3116
3561
  const result = {
3117
3562
  sessions: [],
3118
3563
  unparsedFiles: 0,
3564
+ unreadableFiles: 0,
3119
3565
  unrecognisedFiles: 0,
3120
3566
  emptyDrafts: 0,
3121
3567
  malformedLines: 0
@@ -3148,6 +3594,7 @@ async function foldChatSessions(workspaceStorageDir) {
3148
3594
  try {
3149
3595
  raw = await fs6.readFile(path6.join(wsDir, "chatSessions", file), "utf8");
3150
3596
  } catch {
3597
+ result.unreadableFiles += 1;
3151
3598
  continue;
3152
3599
  }
3153
3600
  let record = null;
@@ -3213,9 +3660,9 @@ async function foldChatSessions(workspaceStorageDir) {
3213
3660
  }
3214
3661
  return result;
3215
3662
  }
3216
- async function* extractVsCode(snapshotDir, extractionId) {
3217
- const historyDir = path6.join(snapshotDir, "history");
3218
- const workspaceStorageDir = path6.join(snapshotDir, "workspaceStorage");
3663
+ async function* extractVsCode(source, extractionId) {
3664
+ const historyDir = typeof source === "string" ? path6.join(source, "history") : source.historyDir;
3665
+ const workspaceStorageDir = typeof source === "string" ? path6.join(source, "workspaceStorage") : source.workspaceStorageDir;
3219
3666
  const index = await buildWorkspaceFolderIndex(workspaceStorageDir);
3220
3667
  const editDays = await foldEditDays(historyDir, index);
3221
3668
  const chatSessions = await foldChatSessions(workspaceStorageDir);
@@ -3315,7 +3762,7 @@ async function* extractVsCode(snapshotDir, extractionId) {
3315
3762
  };
3316
3763
  }
3317
3764
  }
3318
- const readFailures = editDays.unparsedFiles + editDays.malformedEntries + chatSessions.unparsedFiles + chatSessions.unrecognisedFiles + chatSessions.malformedLines;
3765
+ const readFailures = editDays.unparsedFiles + editDays.unreadableFiles + editDays.malformedEntries + chatSessions.unparsedFiles + chatSessions.unreadableFiles + chatSessions.unrecognisedFiles + chatSessions.malformedLines;
3319
3766
  if (windowOldest && windowNewest || readFailures > 0) {
3320
3767
  const window = windowOldest && windowNewest ? { windowOldest, windowNewest } : {};
3321
3768
  yield {
@@ -3332,8 +3779,10 @@ async function* extractVsCode(snapshotDir, extractionId) {
3332
3779
  editDayCount: editDays.days.size,
3333
3780
  sessionCount,
3334
3781
  unparsedHistoryFiles: editDays.unparsedFiles,
3782
+ unreadableHistoryFiles: editDays.unreadableFiles,
3335
3783
  malformedHistoryEntries: editDays.malformedEntries,
3336
3784
  unparsedChatSessionFiles: chatSessions.unparsedFiles,
3785
+ unreadableChatSessionFiles: chatSessions.unreadableFiles,
3337
3786
  unrecognisedChatSessionFiles: chatSessions.unrecognisedFiles,
3338
3787
  emptyChatSessions: chatSessions.emptyDrafts,
3339
3788
  malformedChatSessionLines: chatSessions.malformedLines
@@ -3344,9 +3793,314 @@ async function* extractVsCode(snapshotDir, extractionId) {
3344
3793
  }
3345
3794
  }
3346
3795
 
3796
+ // src/archive.ts
3797
+ import { createHash } from "node:crypto";
3798
+ import { createReadStream } from "node:fs";
3799
+ import * as fs7 from "node:fs/promises";
3800
+ import * as path7 from "node:path";
3801
+ import { pipeline } from "node:stream/promises";
3802
+ var ARCHIVE_SCHEMA = 1;
3803
+ function defaultArchiveRoot(home) {
3804
+ return path7.join(home, ".ascenda", "history-import", "archive");
3805
+ }
3806
+ function defaultArchiveSources(paths, includeVsCodeSessions = false) {
3807
+ const sources = [
3808
+ { store: "claude_code", root: paths.claudeProjects, label: "projects" },
3809
+ { store: "cursor", root: paths.cursorStateDb, siblings: ["-wal", "-shm"], label: "state" },
3810
+ { store: "vscode", root: paths.vscodeHistory, label: "history" }
3811
+ ];
3812
+ if (includeVsCodeSessions) {
3813
+ sources.push({ store: "vscode", root: paths.vscodeWorkspaceStorage, label: "workspaceStorage" });
3814
+ }
3815
+ return sources;
3816
+ }
3817
+ function blobPath(archiveRoot, sha256) {
3818
+ return path7.join(archiveRoot, "objects", sha256.slice(0, 2), sha256);
3819
+ }
3820
+ function manifestDir(archiveRoot) {
3821
+ return path7.join(archiveRoot, "manifests");
3822
+ }
3823
+ async function hashFile(file) {
3824
+ const hash = createHash("sha256");
3825
+ await pipeline(createReadStream(file), hash);
3826
+ return hash.digest("hex");
3827
+ }
3828
+ async function walkFiles(root) {
3829
+ let st;
3830
+ try {
3831
+ st = await fs7.stat(root);
3832
+ } catch {
3833
+ return [];
3834
+ }
3835
+ if (!st.isDirectory()) return [{ absolute: root, relative: path7.basename(root) }];
3836
+ const found = [];
3837
+ async function walk(dir, prefix) {
3838
+ let entries;
3839
+ try {
3840
+ entries = await fs7.readdir(dir, { withFileTypes: true });
3841
+ } catch {
3842
+ return;
3843
+ }
3844
+ for (const entry of entries) {
3845
+ const absolute = path7.join(dir, entry.name);
3846
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
3847
+ if (entry.isDirectory()) await walk(absolute, relative);
3848
+ else if (entry.isFile()) found.push({ absolute, relative });
3849
+ }
3850
+ }
3851
+ await walk(root, "");
3852
+ return found;
3853
+ }
3854
+ async function listManifests(archiveRoot) {
3855
+ try {
3856
+ return (await fs7.readdir(manifestDir(archiveRoot))).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length)).sort();
3857
+ } catch {
3858
+ return [];
3859
+ }
3860
+ }
3861
+ async function readManifest(archiveRoot, generation) {
3862
+ try {
3863
+ const raw = await fs7.readFile(path7.join(manifestDir(archiveRoot), `${generation}.json`), "utf8");
3864
+ const parsed = JSON.parse(raw);
3865
+ if (parsed.schema !== ARCHIVE_SCHEMA) return null;
3866
+ return parsed;
3867
+ } catch {
3868
+ return null;
3869
+ }
3870
+ }
3871
+ async function readLatestManifest(archiveRoot) {
3872
+ const generations = await listManifests(archiveRoot);
3873
+ const latest = generations[generations.length - 1];
3874
+ return latest ? readManifest(archiveRoot, latest) : null;
3875
+ }
3876
+ async function archiveStores(options) {
3877
+ const { archiveRoot, sources, generation, now } = options;
3878
+ const result = {
3879
+ generation,
3880
+ manifestPath: path7.join(manifestDir(archiveRoot), `${generation}.json`),
3881
+ filesArchived: 0,
3882
+ filesDeduplicated: 0,
3883
+ newBytes: 0,
3884
+ totalBytes: 0,
3885
+ perStore: {},
3886
+ unreadable: 0
3887
+ };
3888
+ await fs7.mkdir(path7.join(archiveRoot, "objects"), { recursive: true });
3889
+ await fs7.mkdir(manifestDir(archiveRoot), { recursive: true });
3890
+ const previous = await readLatestManifest(archiveRoot);
3891
+ const previousByKey = /* @__PURE__ */ new Map();
3892
+ for (const file of previous?.files ?? []) {
3893
+ previousByKey.set(`${file.store}/${file.relativePath}`, file);
3894
+ }
3895
+ const files = [];
3896
+ const roots = {};
3897
+ for (const source of sources) {
3898
+ roots[`${source.store}/${source.label}`] = source.root;
3899
+ const targets = await walkFiles(source.root);
3900
+ for (const suffix of source.siblings ?? []) {
3901
+ const absolute = source.root + suffix;
3902
+ try {
3903
+ await fs7.stat(absolute);
3904
+ targets.push({ absolute, relative: path7.basename(absolute) });
3905
+ } catch {
3906
+ }
3907
+ }
3908
+ const storeStats = result.perStore[source.store] ??= {
3909
+ files: 0,
3910
+ newBytes: 0
3911
+ };
3912
+ for (const target of targets) {
3913
+ const relativePath = `${source.label}/${target.relative}`;
3914
+ let st;
3915
+ try {
3916
+ st = await fs7.stat(target.absolute);
3917
+ } catch {
3918
+ result.unreadable += 1;
3919
+ continue;
3920
+ }
3921
+ const prior = previousByKey.get(`${source.store}/${relativePath}`);
3922
+ let sha256 = null;
3923
+ if (prior && prior.size === st.size && prior.mtimeMs === st.mtimeMs) {
3924
+ try {
3925
+ await fs7.stat(blobPath(archiveRoot, prior.sha256));
3926
+ sha256 = prior.sha256;
3927
+ } catch {
3928
+ sha256 = null;
3929
+ }
3930
+ }
3931
+ if (sha256 === null) {
3932
+ try {
3933
+ sha256 = await hashFile(target.absolute);
3934
+ } catch {
3935
+ result.unreadable += 1;
3936
+ continue;
3937
+ }
3938
+ }
3939
+ const destination = blobPath(archiveRoot, sha256);
3940
+ let alreadyHeld = true;
3941
+ try {
3942
+ await fs7.stat(destination);
3943
+ } catch {
3944
+ alreadyHeld = false;
3945
+ }
3946
+ if (alreadyHeld) {
3947
+ result.filesDeduplicated += 1;
3948
+ } else {
3949
+ await fs7.mkdir(path7.dirname(destination), { recursive: true });
3950
+ const temporary2 = `${destination}.tmp-${generation}`;
3951
+ try {
3952
+ await fs7.copyFile(target.absolute, temporary2);
3953
+ await fs7.rename(temporary2, destination);
3954
+ result.newBytes += st.size;
3955
+ storeStats.newBytes += st.size;
3956
+ } catch {
3957
+ await fs7.rm(temporary2, { force: true });
3958
+ result.unreadable += 1;
3959
+ continue;
3960
+ }
3961
+ }
3962
+ files.push({
3963
+ store: source.store,
3964
+ relativePath,
3965
+ sha256,
3966
+ size: st.size,
3967
+ mtimeMs: st.mtimeMs
3968
+ });
3969
+ result.filesArchived += 1;
3970
+ result.totalBytes += st.size;
3971
+ storeStats.files += 1;
3972
+ }
3973
+ }
3974
+ const manifest = {
3975
+ schema: ARCHIVE_SCHEMA,
3976
+ generation,
3977
+ createdAt: now,
3978
+ roots,
3979
+ files
3980
+ };
3981
+ const temporary = `${result.manifestPath}.tmp`;
3982
+ await fs7.writeFile(temporary, JSON.stringify(manifest, null, 2) + "\n", "utf8");
3983
+ await fs7.rename(temporary, result.manifestPath);
3984
+ return result;
3985
+ }
3986
+ async function verifyArchive(archiveRoot, manifest, deep = true) {
3987
+ const result = { generation: manifest.generation, checked: 0, missing: [], corrupted: [] };
3988
+ const hashed = /* @__PURE__ */ new Set();
3989
+ for (const file of manifest.files) {
3990
+ const blob = blobPath(archiveRoot, file.sha256);
3991
+ result.checked += 1;
3992
+ try {
3993
+ await fs7.stat(blob);
3994
+ } catch {
3995
+ result.missing.push(file.relativePath);
3996
+ continue;
3997
+ }
3998
+ if (!deep || hashed.has(file.sha256)) continue;
3999
+ hashed.add(file.sha256);
4000
+ let actual;
4001
+ try {
4002
+ actual = await hashFile(blob);
4003
+ } catch {
4004
+ result.corrupted.push(file.relativePath);
4005
+ continue;
4006
+ }
4007
+ if (actual !== file.sha256) result.corrupted.push(file.relativePath);
4008
+ }
4009
+ return result;
4010
+ }
4011
+ async function restoreArchive(archiveRoot, manifest, destination) {
4012
+ const result = { restored: 0, skipped: 0, destination };
4013
+ for (const file of manifest.files) {
4014
+ const blob = blobPath(archiveRoot, file.sha256);
4015
+ const target = path7.join(destination, file.store, file.relativePath);
4016
+ try {
4017
+ await fs7.mkdir(path7.dirname(target), { recursive: true });
4018
+ await fs7.copyFile(blob, target);
4019
+ result.restored += 1;
4020
+ } catch {
4021
+ result.skipped += 1;
4022
+ }
4023
+ }
4024
+ return result;
4025
+ }
4026
+ async function pruneArchive(archiveRoot, keep) {
4027
+ const result = { generationsRemoved: [], blobsRemoved: 0, freedBytes: 0 };
4028
+ const generations = await listManifests(archiveRoot);
4029
+ const doomed = keep >= generations.length ? [] : generations.slice(0, generations.length - keep);
4030
+ for (const generation of doomed) {
4031
+ try {
4032
+ await fs7.rm(path7.join(manifestDir(archiveRoot), `${generation}.json`), { force: true });
4033
+ result.generationsRemoved.push(generation);
4034
+ } catch {
4035
+ }
4036
+ }
4037
+ const live = /* @__PURE__ */ new Set();
4038
+ for (const generation of await listManifests(archiveRoot)) {
4039
+ const manifest = await readManifest(archiveRoot, generation);
4040
+ for (const file of manifest?.files ?? []) live.add(file.sha256);
4041
+ }
4042
+ const objectsRoot = path7.join(archiveRoot, "objects");
4043
+ let shards = [];
4044
+ try {
4045
+ shards = (await fs7.readdir(objectsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
4046
+ } catch {
4047
+ return result;
4048
+ }
4049
+ for (const shard of shards) {
4050
+ let blobs = [];
4051
+ try {
4052
+ blobs = await fs7.readdir(path7.join(objectsRoot, shard));
4053
+ } catch {
4054
+ continue;
4055
+ }
4056
+ for (const blob of blobs) {
4057
+ if (live.has(blob)) continue;
4058
+ const target = path7.join(objectsRoot, shard, blob);
4059
+ let size = 0;
4060
+ try {
4061
+ size = (await fs7.stat(target)).size;
4062
+ } catch {
4063
+ continue;
4064
+ }
4065
+ try {
4066
+ await fs7.rm(target, { force: true });
4067
+ result.blobsRemoved += 1;
4068
+ result.freedBytes += size;
4069
+ } catch {
4070
+ }
4071
+ }
4072
+ }
4073
+ return result;
4074
+ }
4075
+ async function archiveSizeBytes(archiveRoot) {
4076
+ const objectsRoot = path7.join(archiveRoot, "objects");
4077
+ let total = 0;
4078
+ let shards = [];
4079
+ try {
4080
+ shards = (await fs7.readdir(objectsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
4081
+ } catch {
4082
+ return 0;
4083
+ }
4084
+ for (const shard of shards) {
4085
+ let blobs = [];
4086
+ try {
4087
+ blobs = await fs7.readdir(path7.join(objectsRoot, shard));
4088
+ } catch {
4089
+ continue;
4090
+ }
4091
+ for (const blob of blobs) {
4092
+ try {
4093
+ total += (await fs7.stat(path7.join(objectsRoot, shard, blob))).size;
4094
+ } catch {
4095
+ }
4096
+ }
4097
+ }
4098
+ return total;
4099
+ }
4100
+
3347
4101
  // src/ship.ts
3348
4102
  var import_tool_kit4 = __toESM(require_out2(), 1);
3349
- import { createHash } from "node:crypto";
4103
+ import { createHash as createHash2 } from "node:crypto";
3350
4104
  var DEFAULT_API_BASE_URL = "https://api.ascenda.one";
3351
4105
  function loadShipConfig() {
3352
4106
  const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
@@ -3376,12 +4130,28 @@ function importOrdinals(events) {
3376
4130
  });
3377
4131
  }
3378
4132
  function importKeyOf(event, ordinal) {
3379
- return createHash("sha256").update(
4133
+ return createHash2("sha256").update(
3380
4134
  [event.store, event.sessionRef ?? "", event.eventKind, event.occurredAt, String(ordinal)].join(
3381
4135
  "|"
3382
4136
  )
3383
4137
  ).digest("hex").slice(0, 16);
3384
4138
  }
4139
+ var workContextMemo = /* @__PURE__ */ new Map();
4140
+ function workContextOf(repoRef) {
4141
+ if (!repoRef) return null;
4142
+ const hit = workContextMemo.get(repoRef);
4143
+ if (hit !== void 0) return hit;
4144
+ const context = (0, import_tool_kit4.deriveWorkContext)(repoRef);
4145
+ if (context) {
4146
+ (0, import_tool_kit4.recordWorkContext)(context);
4147
+ const legacyHash = (0, import_tool_kit4.hashWithMachineSalt)(repoRef);
4148
+ if (legacyHash && legacyHash !== context.workspaceHash && legacyHash !== context.projectHash) {
4149
+ (0, import_tool_kit4.recordWorkContextAlias)(legacyHash, context.workspaceLabel ?? repoRef, context.workspacePath);
4150
+ }
4151
+ }
4152
+ workContextMemo.set(repoRef, context);
4153
+ return context;
4154
+ }
3385
4155
  function toWirePayload(event, ordinal, toolInstallationId) {
3386
4156
  const metadata = {
3387
4157
  importKey: importKeyOf(event, ordinal),
@@ -3396,7 +4166,7 @@ function toWirePayload(event, ordinal, toolInstallationId) {
3396
4166
  metadata.gitBranchHash = (0, import_tool_kit4.hashWithMachineSalt)(metadata.gitBranch) ?? "";
3397
4167
  delete metadata.gitBranch;
3398
4168
  }
3399
- const workspaceHash = event.repoRef ? (0, import_tool_kit4.hashWithMachineSalt)(event.repoRef) : null;
4169
+ const context = workContextOf(event.repoRef);
3400
4170
  return {
3401
4171
  toolInstallationId,
3402
4172
  source: STORE_SOURCE[event.store],
@@ -3412,8 +4182,8 @@ function toWirePayload(event, ordinal, toolInstallationId) {
3412
4182
  utcOffsetMinutes: (0, import_tool_kit4.utcOffsetMinutesAt)(new Date(event.occurredAt)),
3413
4183
  severity: "low",
3414
4184
  sessionId: event.sessionRef,
3415
- workspaceHash,
3416
- projectHash: workspaceHash,
4185
+ workspaceHash: context?.workspaceHash ?? null,
4186
+ projectHash: context?.projectHash ?? null,
3417
4187
  // A real contract ToolConsentScope now, and a real gate: the backend
3418
4188
  // requires an active historical-import consent lease for anything carrying
3419
4189
  // one of the provenance classes below, and decides that on the provenance
@@ -3425,6 +4195,9 @@ function toWirePayload(event, ordinal, toolInstallationId) {
3425
4195
  metadata
3426
4196
  };
3427
4197
  }
4198
+ function storeCounts(result, store) {
4199
+ return result.perStore[store] ??= { sent: 0, accepted: 0, duplicate: 0, rejected: 0 };
4200
+ }
3428
4201
  var BATCH_SIZE = 200;
3429
4202
  function shippableEvents(events) {
3430
4203
  return events.filter(
@@ -3438,7 +4211,9 @@ async function shipEvents(events, config, onProgress) {
3438
4211
  duplicate: 0,
3439
4212
  rejected: 0,
3440
4213
  rejectionReasons: {},
3441
- httpFailures: 0
4214
+ httpFailures: 0,
4215
+ perStore: {},
4216
+ attributionComplete: true
3442
4217
  };
3443
4218
  const wireEvents = shippableEvents(events);
3444
4219
  const ordinals = importOrdinals(wireEvents);
@@ -3459,11 +4234,14 @@ async function shipEvents(events, config, onProgress) {
3459
4234
  });
3460
4235
  } catch {
3461
4236
  result.httpFailures += 1;
4237
+ result.attributionComplete = false;
3462
4238
  continue;
3463
4239
  }
3464
4240
  result.sent += chunk.length;
4241
+ for (const event of chunk) storeCounts(result, event.store).sent += 1;
3465
4242
  if (!response.ok) {
3466
4243
  result.httpFailures += 1;
4244
+ result.attributionComplete = false;
3467
4245
  if (response.status === 401 || response.status === 403) {
3468
4246
  throw new Error(
3469
4247
  `Ingest refused (${response.status}) \u2014 token invalid/revoked or consent missing; aborting rather than burning ${wireEvents.length - offset} more events`
@@ -3476,14 +4254,38 @@ async function shipEvents(events, config, onProgress) {
3476
4254
  result.accepted += body.accepted ?? 0;
3477
4255
  result.duplicate += body.duplicate ?? 0;
3478
4256
  result.rejected += body.rejected ?? 0;
3479
- for (const item of body.results ?? []) {
4257
+ const items = body.results ?? [];
4258
+ const attributable = items.length === chunk.length;
4259
+ if (!attributable && (body.accepted ?? body.duplicate ?? body.rejected) !== void 0) {
4260
+ result.attributionComplete = false;
4261
+ }
4262
+ for (let i = 0; i < items.length; i += 1) {
4263
+ const item = items[i];
4264
+ if (attributable) {
4265
+ const counts = storeCounts(result, chunk[i].store);
4266
+ if (item.status === "accepted") counts.accepted += 1;
4267
+ else if (item.status === "duplicate") counts.duplicate += 1;
4268
+ else counts.rejected += 1;
4269
+ }
3480
4270
  if (item.status !== "accepted" && item.status !== "duplicate") {
3481
4271
  const reason = item.reason ?? "unknown";
3482
4272
  result.rejectionReasons[reason] = (result.rejectionReasons[reason] ?? 0) + 1;
3483
4273
  }
3484
4274
  }
4275
+ if (items.length === chunk.length && items.length > 0) {
4276
+ const blocked = items.every(
4277
+ (item) => item.status !== "accepted" && item.status !== "duplicate" && (item.reason === "consent_missing_or_expired" || item.reason === "unknown_consent_scope")
4278
+ );
4279
+ if (blocked) {
4280
+ result.consentBlocked = true;
4281
+ result.attributionComplete = false;
4282
+ onProgress?.(Math.min(offset + BATCH_SIZE, wireEvents.length), wireEvents.length);
4283
+ return result;
4284
+ }
4285
+ }
3485
4286
  } catch {
3486
4287
  result.httpFailures += 1;
4288
+ result.attributionComplete = false;
3487
4289
  }
3488
4290
  onProgress?.(Math.min(offset + BATCH_SIZE, wireEvents.length), wireEvents.length);
3489
4291
  }
@@ -3491,16 +4293,16 @@ async function shipEvents(events, config, onProgress) {
3491
4293
  }
3492
4294
 
3493
4295
  // src/localHandoff.ts
3494
- import * as fs7 from "node:fs/promises";
4296
+ import * as fs8 from "node:fs/promises";
3495
4297
  import * as os2 from "node:os";
3496
- import * as path7 from "node:path";
4298
+ import * as path8 from "node:path";
3497
4299
  var APP_BUNDLE_ID = "one.ascenda.ascendaMissionControl";
3498
4300
  var HANDOFF_SCHEMA = 2;
3499
4301
  function handoffDir(home = os2.homedir()) {
3500
- return path7.join(home, "Library", "Containers", APP_BUNDLE_ID, "Data", ".ascenda", "history-import");
4302
+ return path8.join(home, "Library", "Containers", APP_BUNDLE_ID, "Data", ".ascenda", "history-import");
3501
4303
  }
3502
4304
  function handoffFilePath(home = os2.homedir(), store = "claude_code") {
3503
- return path7.join(handoffDir(home), `${store}.json`);
4305
+ return path8.join(handoffDir(home), `${store}.json`);
3504
4306
  }
3505
4307
  function projectLabelOf(repoRef) {
3506
4308
  if (!repoRef) return null;
@@ -3534,7 +4336,15 @@ function buildHandoff(events, extractionId, generatedAt) {
3534
4336
  compactionCount: Number(event.metrics.compactionCount ?? 0),
3535
4337
  toolFailureCount: Number(event.metrics.toolFailureCount ?? 0),
3536
4338
  contextWindowPeakPct: Number(event.metrics.contextWindowPeakPct ?? 0),
3537
- userModifiedEditCount: Number(event.metrics.userModifiedEditCount ?? 0),
4339
+ contextWindowPeakTokens: Number(event.metrics.contextWindowPeakTokens ?? 0),
4340
+ // Null, never 0. `toolUseResult.userModified` occurs 20,133 times
4341
+ // across a real 363-session store and is `false` on every single one
4342
+ // — Claude Code does not appear to set it true. Shipping 0 says "no
4343
+ // AI edit was ever corrected by hand", which is a finding; the truth
4344
+ // is that this store cannot answer the question. The extractor keeps
4345
+ // counting it in case upstream starts populating it, and the handoff
4346
+ // reports the count only when it is non-zero.
4347
+ userModifiedEditCount: Number(event.metrics.userModifiedEditCount ?? 0) > 0 ? Number(event.metrics.userModifiedEditCount) : null,
3538
4348
  subagentTranscripts: Number(event.metrics.subagentTranscripts ?? 0),
3539
4349
  days: event.dayBreakdown ?? [],
3540
4350
  provenance: event.provenance
@@ -3651,18 +4461,18 @@ function buildVsCodeHandoff(events, extractionId, generatedAt) {
3651
4461
  };
3652
4462
  }
3653
4463
  async function writeHandoff(handoff, home = os2.homedir()) {
3654
- const containerRoot = path7.join(home, "Library", "Containers", APP_BUNDLE_ID, "Data");
4464
+ const containerRoot = path8.join(home, "Library", "Containers", APP_BUNDLE_ID, "Data");
3655
4465
  try {
3656
- await fs7.stat(containerRoot);
4466
+ await fs8.stat(containerRoot);
3657
4467
  } catch {
3658
4468
  return null;
3659
4469
  }
3660
4470
  const dir = handoffDir(home);
3661
- await fs7.mkdir(dir, { recursive: true });
4471
+ await fs8.mkdir(dir, { recursive: true });
3662
4472
  const target = handoffFilePath(home, handoff.store);
3663
4473
  const temp = `${target}.tmp`;
3664
- await fs7.writeFile(temp, JSON.stringify(handoff, null, 2) + "\n", "utf8");
3665
- await fs7.rename(temp, target);
4474
+ await fs8.writeFile(temp, JSON.stringify(handoff, null, 2) + "\n", "utf8");
4475
+ await fs8.rename(temp, target);
3666
4476
  return target;
3667
4477
  }
3668
4478
 
@@ -3682,6 +4492,185 @@ function formatInventory(inv) {
3682
4492
  }
3683
4493
  return lines.join("\n");
3684
4494
  }
4495
+ async function safeScan(store, rootPath, scan) {
4496
+ try {
4497
+ return { inventory: await scan(), scanError: null };
4498
+ } catch (error) {
4499
+ return {
4500
+ inventory: { store, rootPath, present: false, counts: {}, notes: [] },
4501
+ scanError: error instanceof Error ? error.message : String(error)
4502
+ };
4503
+ }
4504
+ }
4505
+ var READ_FAILURE_METRICS = [
4506
+ "unparsedFiles",
4507
+ "unreadableFiles",
4508
+ "unparsedHistoryFiles",
4509
+ "unreadableHistoryFiles",
4510
+ "malformedHistoryEntries",
4511
+ "unparsedChatSessionFiles",
4512
+ "unreadableChatSessionFiles",
4513
+ "unrecognisedChatSessionFiles",
4514
+ "malformedChatSessionLines",
4515
+ "unparsedLines",
4516
+ "unparsedTranscripts"
4517
+ ];
4518
+ function readFailuresOf(events) {
4519
+ let total = 0;
4520
+ for (const event of events) {
4521
+ if (event.eventKind !== "extraction_epoch") continue;
4522
+ for (const metric of READ_FAILURE_METRICS) {
4523
+ const value = event.metrics[metric];
4524
+ if (typeof value === "number") total += value;
4525
+ }
4526
+ }
4527
+ return total;
4528
+ }
4529
+ async function runSource(outcomes, store, inventory, allEvents, extract, buildStoreHandoff, scanError = null) {
4530
+ if (scanError !== null) {
4531
+ process.stdout.write(`${inventory.store} FAILED to scan: ${scanError}
4532
+
4533
+ `);
4534
+ outcomes.push({ store, status: "failed", events: 0, error: scanError, readFailures: 0 });
4535
+ return;
4536
+ }
4537
+ process.stdout.write(formatInventory(inventory) + "\n\n");
4538
+ if (!inventory.present) {
4539
+ outcomes.push({ store, status: "absent", events: 0, readFailures: 0 });
4540
+ return;
4541
+ }
4542
+ const events = [];
4543
+ const kindCounts = {};
4544
+ try {
4545
+ await extract((event) => {
4546
+ events.push(event);
4547
+ kindCounts[event.eventKind] = (kindCounts[event.eventKind] ?? 0) + 1;
4548
+ });
4549
+ } catch (error) {
4550
+ allEvents.push(...events);
4551
+ outcomes.push({
4552
+ store,
4553
+ status: "failed",
4554
+ events: events.length,
4555
+ error: error instanceof Error ? error.message : String(error),
4556
+ readFailures: readFailuresOf(events)
4557
+ });
4558
+ process.stdout.write(`${store} FAILED after ${events.length.toLocaleString("en-US")} events
4559
+
4560
+ `);
4561
+ return;
4562
+ }
4563
+ allEvents.push(...events);
4564
+ process.stdout.write(`${store} extracted: ${events.length.toLocaleString("en-US")} events
4565
+ `);
4566
+ for (const [kind, n] of Object.entries(kindCounts)) {
4567
+ process.stdout.write(` ${kind}: ${n.toLocaleString("en-US")}
4568
+ `);
4569
+ }
4570
+ const epoch = events.find((e) => e.eventKind === "extraction_epoch");
4571
+ if (epoch) {
4572
+ process.stdout.write(` window: ${epoch.metrics.windowOldest} \u2192 ${epoch.metrics.windowNewest}
4573
+ `);
4574
+ }
4575
+ const handoffPath = await writeHandoff(buildStoreHandoff(events));
4576
+ process.stdout.write(
4577
+ handoffPath ? ` handoff: \u2192 ${handoffPath}
4578
+
4579
+ ` : " handoff: desktop app container not found \u2014 skipped\n\n"
4580
+ );
4581
+ outcomes.push({
4582
+ store,
4583
+ status: "extracted",
4584
+ events: events.length,
4585
+ readFailures: readFailuresOf(events)
4586
+ });
4587
+ }
4588
+ function writeSummary(input) {
4589
+ const { outcomes, shipResult, ship } = input;
4590
+ const out = process.stdout;
4591
+ out.write("\n" + "\u2500".repeat(60) + "\nsummary\n");
4592
+ for (const outcome of outcomes) {
4593
+ const name = outcome.store.padEnd(12);
4594
+ if (outcome.status === "absent") {
4595
+ out.write(` ${name} not present on this machine
4596
+ `);
4597
+ continue;
4598
+ }
4599
+ if (outcome.status === "failed") {
4600
+ out.write(` ${name} FAILED \u2014 ${outcome.error}
4601
+ `);
4602
+ out.write(` ${" ".repeat(12)} ${outcome.events.toLocaleString("en-US")} events salvaged before the failure
4603
+ `);
4604
+ continue;
4605
+ }
4606
+ const counts = shipResult?.perStore[outcome.store];
4607
+ let line = ` ${name} ${outcome.events.toLocaleString("en-US")} extracted`;
4608
+ if (counts) {
4609
+ line += `, ${counts.sent.toLocaleString("en-US")} sent`;
4610
+ line += ` (accepted ${counts.accepted.toLocaleString("en-US")}`;
4611
+ line += `, duplicate ${counts.duplicate.toLocaleString("en-US")}`;
4612
+ line += `, rejected ${counts.rejected.toLocaleString("en-US")})`;
4613
+ }
4614
+ out.write(line + "\n");
4615
+ if (outcome.readFailures > 0) {
4616
+ out.write(
4617
+ ` ${" ".repeat(12)} \u26A0 ${outcome.readFailures.toLocaleString("en-US")} file(s)/record(s) this store could not read \u2014 the window is short by an unknown amount
4618
+ `
4619
+ );
4620
+ }
4621
+ }
4622
+ out.write(` ${"total".padEnd(12)} ${input.totalExtracted.toLocaleString("en-US")} extracted`);
4623
+ out.write(ship ? `, ${input.shippable.toLocaleString("en-US")} eligible for the wire
4624
+ ` : "\n");
4625
+ out.write(` events file: ${input.eventsFile}
4626
+ `);
4627
+ if (!ship) {
4628
+ out.write("\ndry run \u2014 pass --ship to send these over the batch wire.\n");
4629
+ return;
4630
+ }
4631
+ if (input.shipError) {
4632
+ out.write(`
4633
+ ship FAILED: ${input.shipError}
4634
+ `);
4635
+ return;
4636
+ }
4637
+ if (!shipResult) {
4638
+ out.write("\nship did not run.\n");
4639
+ return;
4640
+ }
4641
+ out.write(
4642
+ `
4643
+ shipped: sent=${shipResult.sent} accepted=${shipResult.accepted} duplicate=${shipResult.duplicate} rejected=${shipResult.rejected} httpFailures=${shipResult.httpFailures}
4644
+ `
4645
+ );
4646
+ if (shipResult.duplicate > 0) {
4647
+ out.write(
4648
+ ` ${shipResult.duplicate.toLocaleString("en-US")} already imported \u2014 the backend kept its existing copy and stored nothing new.
4649
+ `
4650
+ );
4651
+ }
4652
+ if (shipResult.consentBlocked) {
4653
+ out.write(
4654
+ "\n stopped early: this account has no consent for a retrospective import, so the\n backend refused the whole batch and every remaining event would be refused too.\n Nothing was stored. Grant it in Flow \u2014 Connections > Bring in daily tools \u2014 and\n run this again; the import is idempotent, so nothing here is wasted.\n"
4655
+ );
4656
+ }
4657
+ for (const [reason, n] of Object.entries(shipResult.rejectionReasons)) {
4658
+ out.write(` rejected ${reason}: ${n}
4659
+ `);
4660
+ }
4661
+ if (!shipResult.attributionComplete) {
4662
+ out.write(
4663
+ " note: at least one batch could not be attributed per source \u2014 the per-source accept/reject splits above are incomplete.\n"
4664
+ );
4665
+ }
4666
+ }
4667
+ function exitCodeFor(outcomes, ship, shipResult, shipError) {
4668
+ if (outcomes.some((o) => o.status === "failed")) return 1;
4669
+ if (!ship) return 0;
4670
+ if (shipError || !shipResult) return 1;
4671
+ if (shipResult.rejected > 0 || shipResult.httpFailures > 0) return 1;
4672
+ return shipResult.accepted + shipResult.duplicate > 0 ? 0 : 1;
4673
+ }
3685
4674
  async function main() {
3686
4675
  const [command, ...rest] = process.argv.slice(2);
3687
4676
  const paths = resolveStorePaths();
@@ -3697,158 +4686,258 @@ async function main() {
3697
4686
  }
3698
4687
  case "import": {
3699
4688
  const ship = rest.includes("--ship");
3700
- const claudeInventory = await scanClaudeCode(paths);
3701
- const cursorInventory = await scanCursor(paths);
3702
- const vsCodeInventory = await scanVsCode(paths);
3703
- if (!claudeInventory.present && !cursorInventory.present && !vsCodeInventory.present) {
4689
+ const keepStaging = rest.includes("--keep-staging");
4690
+ const snapshotSessions = rest.includes("--snapshot-sessions");
4691
+ const claudeScan = await safeScan("claude_code", paths.claudeProjects, () => scanClaudeCode(paths));
4692
+ const cursorScan = await safeScan("cursor", paths.cursorStateDb, () => scanCursor(paths));
4693
+ const vsCodeScan = await safeScan("vscode", paths.vscodeHistory, () => scanVsCode(paths));
4694
+ const claudeInventory = claudeScan.inventory;
4695
+ const cursorInventory = cursorScan.inventory;
4696
+ const vsCodeInventory = vsCodeScan.inventory;
4697
+ const anyScanFailed = [claudeScan, cursorScan, vsCodeScan].some((r) => r.scanError !== null);
4698
+ if (!claudeInventory.present && !cursorInventory.present && !vsCodeInventory.present && !anyScanFailed) {
3704
4699
  process.stderr.write(
3705
4700
  `no Claude Code store at ${claudeInventory.rootPath}, no Cursor store at ${cursorInventory.rootPath}, and no VS Code store at ${vsCodeInventory.rootPath} \u2014 nothing to import
3706
4701
  `
3707
4702
  );
3708
4703
  return 2;
3709
4704
  }
3710
- const area = await createStagingArea(defaultStagingRoot(paths.home));
4705
+ const stagingRoot = defaultStagingRoot(paths.home);
4706
+ const swept = await sweepStagingRoot(stagingRoot);
4707
+ if (swept.runsSwept > 0) {
4708
+ process.stdout.write(
4709
+ `swept ${swept.runsSwept} stale staging run(s), freed ${formatBytes(swept.freedBytes)}
4710
+ `
4711
+ );
4712
+ }
4713
+ const willCopy = [paths.claudeProjects, paths.cursorStateDb, paths.vscodeHistory].filter(
4714
+ (_, i) => [claudeInventory.present, cursorInventory.present, vsCodeInventory.present][i]
4715
+ );
4716
+ if (snapshotSessions && vsCodeInventory.present) willCopy.push(paths.vscodeWorkspaceStorage);
4717
+ const space = await checkSpaceForSnapshot(stagingRoot, willCopy);
4718
+ if (!space.sufficient) {
4719
+ process.stderr.write(
4720
+ `not enough space to stage this import: need ~${formatBytes(space.requiredBytes)}, ${formatBytes(space.freeBytes ?? 0)} free on the volume holding ${stagingRoot}
4721
+ refusing to start rather than failing halfway through the copy
4722
+ `
4723
+ );
4724
+ return 2;
4725
+ }
4726
+ const area = await createStagingArea(stagingRoot);
3711
4727
  process.stdout.write(`staging: ${area.root}
3712
4728
 
3713
4729
  `);
3714
4730
  const allEvents = [];
3715
- if (claudeInventory.present) {
3716
- process.stdout.write(formatInventory(claudeInventory) + "\n\n");
3717
- const snapshotRoot = path8.join(area.root, "claude_code");
3718
- await snapshotPath(area, paths.claudeProjects, path8.join("claude_code", "projects"));
3719
- const claudeEvents = [];
3720
- const kindCounts = {};
3721
- for await (const event of extractClaudeCode(snapshotRoot, area.extractionId)) {
3722
- claudeEvents.push(event);
3723
- kindCounts[event.eventKind] = (kindCounts[event.eventKind] ?? 0) + 1;
3724
- }
3725
- allEvents.push(...claudeEvents);
3726
- const epoch = claudeEvents.find((e) => e.eventKind === "extraction_epoch");
3727
- process.stdout.write(`claude_code extracted: ${claudeEvents.length.toLocaleString("en-US")} events
3728
- `);
3729
- for (const [kind, n] of Object.entries(kindCounts)) {
3730
- process.stdout.write(` ${kind}: ${n.toLocaleString("en-US")}
3731
- `);
3732
- }
3733
- if (epoch) {
3734
- process.stdout.write(` window: ${epoch.metrics.windowOldest} \u2192 ${epoch.metrics.windowNewest}
4731
+ const outcomes = [];
4732
+ try {
4733
+ await runSource(outcomes, "claude_code", claudeInventory, allEvents, async (collect) => {
4734
+ const snapshotRoot = path9.join(area.root, "claude_code");
4735
+ await snapshotPath(area, paths.claudeProjects, path9.join("claude_code", "projects"));
4736
+ for await (const event of extractClaudeCode(snapshotRoot, area.extractionId)) collect(event);
4737
+ }, (events) => buildHandoff(events, area.extractionId, (/* @__PURE__ */ new Date()).toISOString()), claudeScan.scanError);
4738
+ await runSource(outcomes, "cursor", cursorInventory, allEvents, async (collect) => {
4739
+ const cursorSnapshotRoot = path9.join(area.root, "cursor");
4740
+ await snapshotPath(area, paths.cursorStateDb, path9.join("cursor", "state.vscdb"));
4741
+ for await (const event of extractCursor(cursorSnapshotRoot, area.extractionId)) collect(event);
4742
+ }, (events) => buildCursorHandoff(events, area.extractionId, (/* @__PURE__ */ new Date()).toISOString()), cursorScan.scanError);
4743
+ await runSource(outcomes, "vscode", vsCodeInventory, allEvents, async (collect) => {
4744
+ await snapshotPath(area, paths.vscodeHistory, path9.join("vscode", "history"));
4745
+ let workspaceStorageDir = paths.vscodeWorkspaceStorage;
4746
+ if (snapshotSessions) {
4747
+ workspaceStorageDir = await snapshotVsCodeWorkspaceStorage(
4748
+ area,
4749
+ paths.vscodeWorkspaceStorage,
4750
+ path9.join("vscode", "workspaceStorage")
4751
+ );
4752
+ }
4753
+ const source = {
4754
+ historyDir: path9.join(area.root, "vscode", "history"),
4755
+ workspaceStorageDir
4756
+ };
4757
+ for await (const event of extractVsCode(source, area.extractionId)) collect(event);
4758
+ }, (events) => buildVsCodeHandoff(events, area.extractionId, (/* @__PURE__ */ new Date()).toISOString()), vsCodeScan.scanError);
4759
+ const eventsFile = path9.join(area.root, "events.jsonl");
4760
+ await fs9.writeFile(eventsFile, allEvents.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
4761
+ let shipResult = null;
4762
+ let shipError = null;
4763
+ if (ship) {
4764
+ const config = loadShipConfig();
4765
+ process.stdout.write(`
4766
+ shipping to ${config.apiBaseUrl} as ${config.toolInstallationId}\u2026
3735
4767
  `);
4768
+ try {
4769
+ shipResult = await shipEvents(allEvents, config, (done, total) => {
4770
+ process.stdout.write(` ${done.toLocaleString("en-US")}/${total.toLocaleString("en-US")}\r`);
4771
+ });
4772
+ process.stdout.write("\n");
4773
+ } catch (error) {
4774
+ process.stdout.write("\n");
4775
+ shipError = error instanceof Error ? error.message : String(error);
4776
+ }
3736
4777
  }
3737
- const handoff = buildHandoff(claudeEvents, area.extractionId, (/* @__PURE__ */ new Date()).toISOString());
3738
- const handoffPath = await writeHandoff(handoff);
3739
- if (handoffPath) {
3740
- process.stdout.write(` handoff: ${handoff.sessions.length} sessions \u2192 ${handoffPath}
3741
-
4778
+ writeSummary({
4779
+ outcomes,
4780
+ eventsFile,
4781
+ totalExtracted: allEvents.length,
4782
+ shippable: shippableEvents(allEvents).length,
4783
+ ship,
4784
+ shipResult,
4785
+ shipError
4786
+ });
4787
+ return exitCodeFor(outcomes, ship, shipResult, shipError);
4788
+ } finally {
4789
+ if (keepStaging) {
4790
+ process.stdout.write(`
4791
+ staging kept at ${area.root} (--keep-staging)
3742
4792
  `);
3743
4793
  } else {
3744
- process.stdout.write(" handoff: desktop app container not found \u2014 skipped\n\n");
3745
- }
3746
- } else {
3747
- process.stdout.write(formatInventory(claudeInventory) + "\n\n");
3748
- }
3749
- if (cursorInventory.present) {
3750
- process.stdout.write(formatInventory(cursorInventory) + "\n\n");
3751
- const cursorSnapshotRoot = path8.join(area.root, "cursor");
3752
- await snapshotPath(area, paths.cursorStateDb, path8.join("cursor", "state.vscdb"));
3753
- const cursorEvents = [];
3754
- const kindCounts = {};
3755
- for await (const event of extractCursor(cursorSnapshotRoot, area.extractionId)) {
3756
- cursorEvents.push(event);
3757
- kindCounts[event.eventKind] = (kindCounts[event.eventKind] ?? 0) + 1;
4794
+ const disposed = await disposeStagingArea(area);
4795
+ if (disposed.freedBytes > 0) {
4796
+ process.stdout.write(
4797
+ `
4798
+ staging cleaned: freed ${formatBytes(disposed.freedBytes)}, kept ${disposed.kept.join(", ") || "nothing"}
4799
+ `
4800
+ );
4801
+ }
3758
4802
  }
3759
- allEvents.push(...cursorEvents);
3760
- const epoch = cursorEvents.find((e) => e.eventKind === "extraction_epoch");
3761
- process.stdout.write(`cursor extracted: ${cursorEvents.length.toLocaleString("en-US")} events
3762
- `);
3763
- for (const [kind, n] of Object.entries(kindCounts)) {
3764
- process.stdout.write(` ${kind}: ${n.toLocaleString("en-US")}
4803
+ }
4804
+ }
4805
+ case "archive": {
4806
+ const archiveRoot = defaultArchiveRoot(paths.home);
4807
+ const flagValue = (name) => {
4808
+ const at = rest.indexOf(name);
4809
+ return at >= 0 && rest[at + 1] ? rest[at + 1] : null;
4810
+ };
4811
+ const generationFlag = flagValue("--generation");
4812
+ if (rest.includes("--list")) {
4813
+ const generations = await listManifests(archiveRoot);
4814
+ if (generations.length === 0) {
4815
+ process.stdout.write(`no archive at ${archiveRoot} \u2014 run \`archive\` to create one
3765
4816
  `);
4817
+ return 0;
3766
4818
  }
3767
- if (epoch) {
3768
- process.stdout.write(` window: ${epoch.metrics.windowOldest} \u2192 ${epoch.metrics.windowNewest}
4819
+ for (const generation2 of generations) {
4820
+ const manifest = await readManifest(archiveRoot, generation2);
4821
+ if (!manifest) {
4822
+ process.stdout.write(` ${generation2} UNREADABLE
3769
4823
  `);
4824
+ continue;
4825
+ }
4826
+ const bytes = manifest.files.reduce((sum, f) => sum + f.size, 0);
4827
+ process.stdout.write(
4828
+ ` ${generation2} ${manifest.files.length.toLocaleString("en-US")} files ${formatBytes(bytes)} logical
4829
+ `
4830
+ );
3770
4831
  }
3771
- const handoff = buildCursorHandoff(cursorEvents, area.extractionId, (/* @__PURE__ */ new Date()).toISOString());
3772
- const handoffPath = await writeHandoff(handoff);
3773
- if (handoffPath) {
3774
- process.stdout.write(` handoff: ${handoff.sessions.length} sessions \u2192 ${handoffPath}
3775
-
4832
+ process.stdout.write(`
4833
+ archive on disk: ${formatBytes(await archiveSizeBytes(archiveRoot))}
3776
4834
  `);
3777
- } else {
3778
- process.stdout.write(" handoff: desktop app container not found \u2014 skipped\n\n");
3779
- }
3780
- } else {
3781
- process.stdout.write(formatInventory(cursorInventory) + "\n\n");
3782
- }
3783
- if (vsCodeInventory.present) {
3784
- process.stdout.write(formatInventory(vsCodeInventory) + "\n\n");
3785
- const vsCodeSnapshotRoot = path8.join(area.root, "vscode");
3786
- await snapshotPath(area, paths.vscodeHistory, path8.join("vscode", "history"));
3787
- await snapshotVsCodeWorkspaceStorage(area, paths.vscodeWorkspaceStorage, path8.join("vscode", "workspaceStorage"));
3788
- const vsCodeEvents = [];
3789
- const kindCounts = {};
3790
- for await (const event of extractVsCode(vsCodeSnapshotRoot, area.extractionId)) {
3791
- vsCodeEvents.push(event);
3792
- kindCounts[event.eventKind] = (kindCounts[event.eventKind] ?? 0) + 1;
4835
+ return 0;
4836
+ }
4837
+ if (rest.includes("--verify")) {
4838
+ const manifest = generationFlag ? await readManifest(archiveRoot, generationFlag) : await readLatestManifest(archiveRoot);
4839
+ if (!manifest) {
4840
+ process.stderr.write(`no readable generation to verify in ${archiveRoot}
4841
+ `);
4842
+ return 2;
3793
4843
  }
3794
- allEvents.push(...vsCodeEvents);
3795
- const epoch = vsCodeEvents.find((e) => e.eventKind === "extraction_epoch");
3796
- process.stdout.write(`vscode extracted: ${vsCodeEvents.length.toLocaleString("en-US")} events
4844
+ const verified = await verifyArchive(archiveRoot, manifest);
4845
+ process.stdout.write(
4846
+ `generation ${verified.generation}: ${verified.checked.toLocaleString("en-US")} files checked, ${verified.missing.length} missing, ${verified.corrupted.length} corrupted
4847
+ `
4848
+ );
4849
+ for (const file of verified.missing.slice(0, 10)) process.stdout.write(` missing: ${file}
3797
4850
  `);
3798
- for (const [kind, n] of Object.entries(kindCounts)) {
3799
- process.stdout.write(` ${kind}: ${n.toLocaleString("en-US")}
4851
+ for (const file of verified.corrupted.slice(0, 10)) process.stdout.write(` corrupted: ${file}
3800
4852
  `);
4853
+ return verified.missing.length + verified.corrupted.length > 0 ? 1 : 0;
4854
+ }
4855
+ const restoreTo = flagValue("--restore");
4856
+ if (rest.includes("--restore")) {
4857
+ if (!restoreTo) {
4858
+ process.stderr.write("--restore needs a destination directory\n");
4859
+ return 2;
3801
4860
  }
3802
- if (epoch) {
3803
- process.stdout.write(` window: ${epoch.metrics.windowOldest} \u2192 ${epoch.metrics.windowNewest}
4861
+ const manifest = generationFlag ? await readManifest(archiveRoot, generationFlag) : await readLatestManifest(archiveRoot);
4862
+ if (!manifest) {
4863
+ process.stderr.write(`no readable generation to restore in ${archiveRoot}
3804
4864
  `);
4865
+ return 2;
3805
4866
  }
3806
- const handoff = buildVsCodeHandoff(vsCodeEvents, area.extractionId, (/* @__PURE__ */ new Date()).toISOString());
3807
- const handoffPath = await writeHandoff(handoff);
3808
- if (handoffPath) {
3809
- process.stdout.write(
3810
- ` handoff: ${handoff.editDays.length} edit-days, ${handoff.chatSessions.length} chat sessions \u2192 ${handoffPath}
3811
-
4867
+ const restored = await restoreArchive(archiveRoot, manifest, restoreTo);
4868
+ process.stdout.write(
4869
+ `restored ${restored.restored.toLocaleString("en-US")} files from ${manifest.generation} \u2192 ${restored.destination}
3812
4870
  `
3813
- );
3814
- } else {
3815
- process.stdout.write(" handoff: desktop app container not found \u2014 skipped\n\n");
4871
+ );
4872
+ if (restored.skipped > 0) {
4873
+ process.stdout.write(` ${restored.skipped} could not be written
4874
+ `);
3816
4875
  }
3817
- } else {
3818
- process.stdout.write(formatInventory(vsCodeInventory) + "\n\n");
4876
+ process.stdout.write(
4877
+ "\nnothing was written to the live stores \u2014 copy back by hand once you have looked at this.\n"
4878
+ );
4879
+ return restored.skipped > 0 ? 1 : 0;
3819
4880
  }
3820
- const eventsFile = path8.join(area.root, "events.jsonl");
3821
- await fs8.writeFile(eventsFile, allEvents.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
3822
- process.stdout.write(`total extracted: ${allEvents.length.toLocaleString("en-US")} events
3823
- `);
3824
- process.stdout.write(`events file: ${eventsFile}
4881
+ if (rest.includes("--prune")) {
4882
+ const keep = Number(flagValue("--keep") ?? 10);
4883
+ if (!Number.isInteger(keep) || keep < 1) {
4884
+ process.stderr.write("--keep needs a positive integer\n");
4885
+ return 2;
4886
+ }
4887
+ const pruned = await pruneArchive(archiveRoot, keep);
4888
+ process.stdout.write(
4889
+ `pruned ${pruned.generationsRemoved.length} generation(s) and ${pruned.blobsRemoved} unreferenced blob(s), freed ${formatBytes(pruned.freedBytes)}
4890
+ `
4891
+ );
4892
+ process.stdout.write(`archive on disk: ${formatBytes(await archiveSizeBytes(archiveRoot))}
3825
4893
  `);
3826
- if (!ship) {
3827
- process.stdout.write("dry run \u2014 pass --ship to send these over the batch wire.\n");
3828
4894
  return 0;
3829
4895
  }
3830
- const config = loadShipConfig();
3831
- process.stdout.write(`shipping to ${config.apiBaseUrl} as ${config.toolInstallationId}\u2026
4896
+ const includeSessions = rest.includes("--include-vscode-sessions");
4897
+ const sources = defaultArchiveSources(paths, includeSessions);
4898
+ const generation = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4899
+ process.stdout.write(`archiving to ${archiveRoot}
3832
4900
  `);
3833
- const result = await shipEvents(allEvents, config, (done, total) => {
3834
- process.stdout.write(` ${done.toLocaleString("en-US")}/${total.toLocaleString("en-US")}\r`);
3835
- });
4901
+ for (const source of sources) process.stdout.write(` ${source.store}/${source.label}: ${source.root}
4902
+ `);
4903
+ if (!includeSessions) {
4904
+ process.stdout.write(
4905
+ " vscode/workspaceStorage: skipped \u2014 15 GB that VS Code is not deleting (--include-vscode-sessions to add)\n"
4906
+ );
4907
+ }
3836
4908
  process.stdout.write("\n");
4909
+ const result = await archiveStores({
4910
+ archiveRoot,
4911
+ sources,
4912
+ generation,
4913
+ now: (/* @__PURE__ */ new Date()).toISOString()
4914
+ });
4915
+ process.stdout.write("\u2500".repeat(60) + `
4916
+ generation ${result.generation}
4917
+ `);
4918
+ for (const [store, counts] of Object.entries(result.perStore)) {
4919
+ process.stdout.write(
4920
+ ` ${store.padEnd(12)} ${counts.files.toLocaleString("en-US")} files, ${formatBytes(counts.newBytes)} new
4921
+ `
4922
+ );
4923
+ }
3837
4924
  process.stdout.write(
3838
- `shipped: sent=${result.sent} accepted=${result.accepted} duplicate=${result.duplicate} rejected=${result.rejected} httpFailures=${result.httpFailures}
4925
+ ` ${"total".padEnd(12)} ${result.filesArchived.toLocaleString("en-US")} files (${result.filesDeduplicated.toLocaleString("en-US")} already held), ${formatBytes(result.newBytes)} new
3839
4926
  `
3840
4927
  );
3841
- if (result.duplicate > 0) {
4928
+ if (result.unreadable > 0) {
3842
4929
  process.stdout.write(
3843
- ` ${result.duplicate.toLocaleString("en-US")} already imported \u2014 the backend kept its existing copy and stored nothing new.
4930
+ ` \u26A0 ${result.unreadable} file(s) could not be read \u2014 this generation is short by an unknown amount
3844
4931
  `
3845
4932
  );
3846
4933
  }
3847
- for (const [reason, n] of Object.entries(result.rejectionReasons)) {
3848
- process.stdout.write(` rejected ${reason}: ${n}
4934
+ const onDisk = await archiveSizeBytes(archiveRoot);
4935
+ process.stdout.write(` archive on disk: ${formatBytes(onDisk)} across ${(await listManifests(archiveRoot)).length} generation(s)
3849
4936
  `);
3850
- }
3851
- return result.accepted + result.duplicate > 0 && result.rejected === 0 && result.httpFailures === 0 ? 0 : 1;
4937
+ process.stdout.write(` manifest: ${result.manifestPath}
4938
+ `);
4939
+ process.stdout.write("\nverify it: ascenda-history-import archive --verify\n");
4940
+ return result.unreadable > 0 ? 1 : 0;
3852
4941
  }
3853
4942
  case "fix-retention": {
3854
4943
  const plan = await planClaudeRetentionFix(paths);
@@ -3873,7 +4962,7 @@ async function main() {
3873
4962
  }
3874
4963
  default: {
3875
4964
  process.stderr.write(
3876
- "usage: ascenda-history-import <scan [--json] | import [--ship] | fix-retention [--apply]>\n"
4965
+ "usage: ascenda-history-import <scan [--json] | import [--ship] | fix-retention [--apply] | archive [--verify|--list|--restore <dir>|--prune]>\n"
3877
4966
  );
3878
4967
  return command === void 0 || command === "--help" ? 0 : 2;
3879
4968
  }