chat-recall 0.5.16 → 0.5.18

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
@@ -4549,6 +4549,37 @@ var init_paths = __esm({
4549
4549
  }
4550
4550
  });
4551
4551
 
4552
+ // ../engine/dist/core/project-path-match.js
4553
+ function canonicalProjectPath(p) {
4554
+ return (p || "").trim().replace(/\\/g, "/").replace(/[-_]/g, "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "").toLowerCase();
4555
+ }
4556
+ function projectPathIncludes(candidate, rule) {
4557
+ if (!candidate || !rule)
4558
+ return false;
4559
+ if (candidate.includes(rule))
4560
+ return true;
4561
+ const c = canonicalProjectPath(candidate);
4562
+ const r = canonicalProjectPath(rule);
4563
+ return !!r && c.includes(r);
4564
+ }
4565
+ function projectPathAtOrUnder(candidate, rule) {
4566
+ if (!candidate || !rule)
4567
+ return false;
4568
+ const strip = (s) => s.replace(/\/+$/, "");
4569
+ const cRaw = strip(candidate);
4570
+ const rRaw = strip(rule);
4571
+ if (cRaw === rRaw || cRaw.startsWith(`${rRaw}/`))
4572
+ return true;
4573
+ const c = canonicalProjectPath(candidate);
4574
+ const r = canonicalProjectPath(rule);
4575
+ return !!r && (c === r || c.startsWith(`${r}/`));
4576
+ }
4577
+ var init_project_path_match = __esm({
4578
+ "../engine/dist/core/project-path-match.js"() {
4579
+ "use strict";
4580
+ }
4581
+ });
4582
+
4552
4583
  // ../engine/dist/core/settings.js
4553
4584
  var settings_exports = {};
4554
4585
  __export(settings_exports, {
@@ -4602,7 +4633,7 @@ function isProjectSyncable(projectId, projectPath, cfg) {
4602
4633
  if (projectId && cfg.syncOnly.has(projectId))
4603
4634
  return true;
4604
4635
  for (const x of cfg.syncOnly)
4605
- if (x && projectPath.includes(x))
4636
+ if (projectPathIncludes(projectPath, x))
4606
4637
  return true;
4607
4638
  return false;
4608
4639
  }
@@ -5015,6 +5046,7 @@ var CLI_DETECTION_ORDER, SCHEMA_VERSION, PERSONAL_DIR_NAMES, PERSONAL_DIR_PATTER
5015
5046
  var init_settings = __esm({
5016
5047
  "../engine/dist/core/settings.js"() {
5017
5048
  "use strict";
5049
+ init_project_path_match();
5018
5050
  init_paths();
5019
5051
  CLI_DETECTION_ORDER = [
5020
5052
  { preset: "gemini", bin: "gemini", cmd: 'gemini -p " "' },
@@ -15386,7 +15418,7 @@ function matchesProjectList(projectPath, patterns) {
15386
15418
  const tail = projectPath.slice(parent.length + 1);
15387
15419
  if (projectPath.startsWith(parent + "/") && !tail.includes("/"))
15388
15420
  return true;
15389
- } else if (projectPath === p || projectPath.startsWith(p + "/")) {
15421
+ } else if (projectPathAtOrUnder(projectPath, p)) {
15390
15422
  return true;
15391
15423
  }
15392
15424
  }
@@ -15412,6 +15444,7 @@ var cache2, TOOLS;
15412
15444
  var init_source_policy = __esm({
15413
15445
  "../engine/dist/core/source-policy.js"() {
15414
15446
  "use strict";
15447
+ init_project_path_match();
15415
15448
  init_settings();
15416
15449
  cache2 = null;
15417
15450
  TOOLS = ["claude", "gemini", "opencode", "codex", "agy", "cursor"];
@@ -19694,7 +19727,7 @@ var init_http = __esm({
19694
19727
  CLI_VERSION = JSON.parse(readFileSync33(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
19695
19728
  } catch {
19696
19729
  try {
19697
- if (true) CLI_VERSION = "0.5.16";
19730
+ if (true) CLI_VERSION = "0.5.18";
19698
19731
  } catch {
19699
19732
  }
19700
19733
  }
@@ -20191,8 +20224,8 @@ async function flush() {
20191
20224
  }
20192
20225
  function hashHost() {
20193
20226
  try {
20194
- const { createHash: createHash19 } = __require("node:crypto");
20195
- return createHash19("sha256").update(hostname()).digest("hex").slice(0, 12);
20227
+ const { createHash: createHash20 } = __require("node:crypto");
20228
+ return createHash20("sha256").update(hostname()).digest("hex").slice(0, 12);
20196
20229
  } catch {
20197
20230
  return "";
20198
20231
  }
@@ -20206,7 +20239,7 @@ var init_telemetry = __esm({
20206
20239
  init_sync_client();
20207
20240
  cliVersion2 = () => {
20208
20241
  try {
20209
- return true ? "0.5.16" : "dev";
20242
+ return true ? "0.5.18" : "dev";
20210
20243
  } catch {
20211
20244
  return "dev";
20212
20245
  }
@@ -20500,6 +20533,7 @@ var sync_client_exports = {};
20500
20533
  __export(sync_client_exports, {
20501
20534
  BATCH_SCAN_MAX_BYTES: () => BATCH_SCAN_MAX_BYTES,
20502
20535
  FULL_BUILD_MAX_BYTES: () => FULL_BUILD_MAX_BYTES,
20536
+ _fetchTenantSyncConfigForTests: () => _fetchTenantSyncConfigForTests,
20503
20537
  _resetCapsCacheForTests: () => _resetCapsCacheForTests,
20504
20538
  _resetTenantSecurityConfigCache: () => _resetTenantSecurityConfigCache,
20505
20539
  _resetTenantSyncConfigCache: () => _resetTenantSyncConfigCache,
@@ -20622,6 +20656,25 @@ async function fetchTenantSecurityConfig(cred) {
20622
20656
  function _resetTenantSecurityConfigCache() {
20623
20657
  _securityConfigCache.clear();
20624
20658
  }
20659
+ function syncConfigCachePath() {
20660
+ return join44(getDataDir(), "sync-config-cache.json");
20661
+ }
20662
+ function readPersistedSyncConfigs() {
20663
+ try {
20664
+ return JSON.parse(readFileSync36(syncConfigCachePath(), "utf-8"));
20665
+ } catch {
20666
+ return {};
20667
+ }
20668
+ }
20669
+ function persistSyncConfig(base2, config2) {
20670
+ try {
20671
+ const all = readPersistedSyncConfigs();
20672
+ all[base2] = { fetchedAt: Date.now(), config: config2 };
20673
+ mkdirSync9(dirname18(syncConfigCachePath()), { recursive: true });
20674
+ writeFileSync9(syncConfigCachePath(), JSON.stringify(all, null, 2));
20675
+ } catch {
20676
+ }
20677
+ }
20625
20678
  async function fetchTenantSyncConfig(cred) {
20626
20679
  const base2 = cred.serverUrl.replace(/\/+$/, "");
20627
20680
  const cached = _syncConfigCache.get(base2);
@@ -20641,7 +20694,11 @@ async function fetchTenantSyncConfig(cred) {
20641
20694
  } catch {
20642
20695
  }
20643
20696
  const res = await fetchWithTimeout(`${base2}/api/sync-config`, { headers });
20644
- if (!res.ok) return { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
20697
+ if (res.status === 404) {
20698
+ _syncConfigCache.set(base2, { fetchedAt: Date.now(), config: EMPTY_SYNC_CONFIG });
20699
+ return EMPTY_SYNC_CONFIG;
20700
+ }
20701
+ if (!res.ok) return lastKnownSyncConfig(base2, `HTTP ${res.status}`);
20645
20702
  const body = await res.json().catch(() => ({}));
20646
20703
  const config2 = {
20647
20704
  excludeTools: Array.isArray(body.excludeTools) ? body.excludeTools.filter((t) => typeof t === "string") : [],
@@ -20679,10 +20736,21 @@ async function fetchTenantSyncConfig(cred) {
20679
20736
  } catch {
20680
20737
  }
20681
20738
  _syncConfigCache.set(base2, { fetchedAt: Date.now(), config: config2 });
20739
+ persistSyncConfig(base2, config2);
20682
20740
  return config2;
20683
- } catch {
20684
- return { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
20741
+ } catch (err) {
20742
+ return lastKnownSyncConfig(base2, err instanceof Error ? err.message : "request failed");
20743
+ }
20744
+ }
20745
+ function lastKnownSyncConfig(base2, reason) {
20746
+ const remembered = _syncConfigCache.get(base2) ?? readPersistedSyncConfigs()[base2];
20747
+ if (remembered) {
20748
+ console.error(`[sync] could not read server-side sync rules (${reason}) \u2014 applying the last known set from ${new Date(remembered.fetchedAt).toISOString()}`);
20749
+ return remembered.config;
20685
20750
  }
20751
+ throw new Error(
20752
+ `Cannot read the server-side sync rules from ${base2} (${reason}), and this machine has never successfully read them. Refusing to sync: uploading without knowing your exclusions could ship a project you told the dashboard to hold back. Fix the server or retry.`
20753
+ );
20686
20754
  }
20687
20755
  function deviceLabel() {
20688
20756
  try {
@@ -20973,7 +21041,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20973
21041
  };
20974
21042
  const cleartext = opts.cleartextPaths ?? sync?.pathsCleartext === true;
20975
21043
  const mapPath = (p) => cleartext ? p : hashPath(p);
20976
- const excluded = (projectPath) => excludeProjects.some((x) => x && projectPath.includes(x));
21044
+ const excluded = (projectPath) => excludeProjects.some((x) => projectPathIncludes(projectPath, x));
20977
21045
  const syncOnly2 = /* @__PURE__ */ new Set([...sync?.syncOnlyProjects ?? [], ...serverCfg.syncOnlyProjects ?? []]);
20978
21046
  const includedProject = (projectPath) => {
20979
21047
  if ((sync?.syncMode ?? "all") !== "only") return true;
@@ -21933,7 +22001,7 @@ async function syncIncremental(opts = {}) {
21933
22001
  lock.release();
21934
22002
  }
21935
22003
  }
21936
- var ownVersion, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
22004
+ var ownVersion, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
21937
22005
  var init_sync_client = __esm({
21938
22006
  "src/sync-client.ts"() {
21939
22007
  "use strict";
@@ -21955,6 +22023,7 @@ var init_sync_client = __esm({
21955
22023
  init_tool_backend();
21956
22024
  init_session();
21957
22025
  init_project_resolver();
22026
+ init_project_path_match();
21958
22027
  init_entity_extractor();
21959
22028
  init_all_sources();
21960
22029
  init_sync_ledger();
@@ -21978,7 +22047,7 @@ var init_sync_client = __esm({
21978
22047
  ownVersion = JSON.parse(readFileSync36(join44(dir, "../package.json"), "utf-8")).version || ownVersion;
21979
22048
  } catch {
21980
22049
  try {
21981
- if (true) ownVersion = "0.5.16";
22050
+ if (true) ownVersion = "0.5.18";
21982
22051
  } catch {
21983
22052
  }
21984
22053
  }
@@ -21988,6 +22057,8 @@ var init_sync_client = __esm({
21988
22057
  _securityConfigCache = /* @__PURE__ */ new Map();
21989
22058
  SECURITY_CONFIG_TTL_MS = 5 * 60 * 1e3;
21990
22059
  _syncConfigCache = /* @__PURE__ */ new Map();
22060
+ EMPTY_SYNC_CONFIG = { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
22061
+ _fetchTenantSyncConfigForTests = fetchTenantSyncConfig;
21991
22062
  hashPath = (p) => p ? "p_" + createHash13("sha256").update(p).digest("hex").slice(0, 12) : "";
21992
22063
  MAX_DERIVED_ROW_BYTES = 2 * 1024 * 1024;
21993
22064
  isLocalHost = isPrivateHost;
@@ -22147,6 +22218,75 @@ var init_which = __esm({
22147
22218
  }
22148
22219
  });
22149
22220
 
22221
+ // ../engine/dist/core/sync-scope.js
22222
+ var sync_scope_exports = {};
22223
+ __export(sync_scope_exports, {
22224
+ summariseSyncScope: () => summariseSyncScope
22225
+ });
22226
+ function summariseSyncScope(sinceMs = 0) {
22227
+ const sync = loadSettings().sync;
22228
+ const excludeProjects = sync?.excludeProjects ?? [];
22229
+ const excludeTools = new Set(sync?.excludeTools ?? []);
22230
+ const allowlistMode = (sync?.syncMode ?? "all") === "only";
22231
+ const syncOnly2 = new Set(sync?.syncOnlyProjects ?? []);
22232
+ const perProject = /* @__PURE__ */ new Map();
22233
+ const perTool = /* @__PURE__ */ new Map();
22234
+ for (const backend of listAvailableBackends()) {
22235
+ let refs = [];
22236
+ try {
22237
+ refs = backend.listSessions({ sinceMs });
22238
+ } catch {
22239
+ continue;
22240
+ }
22241
+ for (const ref of refs) {
22242
+ const projectPath = ref.projectPath || "";
22243
+ const toolId = ref.toolId || backend.id;
22244
+ let heldBackBy = null;
22245
+ if (excludeTools.has(toolId))
22246
+ heldBackBy = "excluded-tool";
22247
+ else if (excludeProjects.some((x) => projectPathIncludes(projectPath, x)))
22248
+ heldBackBy = "excluded-project";
22249
+ else if (!isProjectSyncable(idOf(projectPath), projectPath, { syncMode: sync?.syncMode, syncOnly: syncOnly2 })) {
22250
+ heldBackBy = "not-in-allowlist";
22251
+ }
22252
+ const id = idOf(projectPath);
22253
+ const key2 = `${id}\0${projectPath}`;
22254
+ const row = perProject.get(key2) ?? { id, projectPath, sessions: 0, heldBackBy };
22255
+ row.sessions++;
22256
+ row.heldBackBy = row.heldBackBy ?? heldBackBy;
22257
+ perProject.set(key2, row);
22258
+ if (!heldBackBy)
22259
+ perTool.set(toolId, (perTool.get(toolId) ?? 0) + 1);
22260
+ }
22261
+ }
22262
+ const projects = [...perProject.values()].sort((a, b) => b.sessions - a.sessions);
22263
+ const noPathSessions = projects.filter((p) => !p.projectPath && !p.heldBackBy).reduce((n, p) => n + p.sessions, 0);
22264
+ return {
22265
+ included: projects.filter((p) => !p.heldBackBy).reduce((n, p) => n + p.sessions, 0),
22266
+ heldBack: projects.filter((p) => p.heldBackBy).reduce((n, p) => n + p.sessions, 0),
22267
+ byTool: [...perTool.entries()].map(([tool, sessions]) => ({ tool, sessions })).sort((a, b) => b.sessions - a.sessions),
22268
+ projects,
22269
+ allowlistMode,
22270
+ noPathSessions
22271
+ };
22272
+ }
22273
+ function idOf(projectPath) {
22274
+ if (!projectPath)
22275
+ return "";
22276
+ const r = resolveProjectId(projectPath);
22277
+ return r.source === "ignored" ? "" : r.id;
22278
+ }
22279
+ var init_sync_scope = __esm({
22280
+ "../engine/dist/core/sync-scope.js"() {
22281
+ "use strict";
22282
+ init_tool_backend();
22283
+ init_backends();
22284
+ init_project_resolver();
22285
+ init_settings();
22286
+ init_project_path_match();
22287
+ }
22288
+ });
22289
+
22150
22290
  // ../engine/dist/core/mcp-clients.js
22151
22291
  var mcp_clients_exports = {};
22152
22292
  __export(mcp_clients_exports, {
@@ -24231,6 +24371,49 @@ var init_verify_repair = __esm({
24231
24371
  }
24232
24372
  });
24233
24373
 
24374
+ // ../engine/dist/types/code-intel.js
24375
+ import { createHash as createHash17 } from "crypto";
24376
+ function shortHash(s) {
24377
+ return createHash17("sha256").update(s).digest("hex").slice(0, 16);
24378
+ }
24379
+ function snippetKey(snippet) {
24380
+ return (snippet ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
24381
+ }
24382
+ function codeFindingIds(projectId, findings) {
24383
+ const groups = /* @__PURE__ */ new Map();
24384
+ findings.forEach((f, i) => {
24385
+ const key2 = [f.category, f.file, f.rule, identityTitle(f.title ?? ""), snippetKey(f.snippet)].join("|");
24386
+ const g = groups.get(key2);
24387
+ if (g)
24388
+ g.push(i);
24389
+ else
24390
+ groups.set(key2, [i]);
24391
+ });
24392
+ const lineOf = (i) => findings[i].line ?? -1;
24393
+ const ids = new Array(findings.length);
24394
+ for (const [key2, idxs] of groups) {
24395
+ idxs.sort((a, b) => lineOf(a) - lineOf(b));
24396
+ let ordinal = -1;
24397
+ let prevLine = Number.NaN;
24398
+ for (const idx of idxs) {
24399
+ if (lineOf(idx) !== prevLine) {
24400
+ ordinal++;
24401
+ prevLine = lineOf(idx);
24402
+ }
24403
+ ids[idx] = "cf_" + shortHash([projectId, key2, ordinal].join("|"));
24404
+ }
24405
+ }
24406
+ return ids;
24407
+ }
24408
+ function identityTitle(title) {
24409
+ return title.replace(/\d+/g, "#");
24410
+ }
24411
+ var init_code_intel = __esm({
24412
+ "../engine/dist/types/code-intel.js"() {
24413
+ "use strict";
24414
+ }
24415
+ });
24416
+
24234
24417
  // ../engine/dist/core/code/prompts.js
24235
24418
  function secFix(rule) {
24236
24419
  return SEC_FIX[rule] ?? "Review and remediate.";
@@ -24578,6 +24761,11 @@ async function collectCode(opts) {
24578
24761
  }
24579
24762
  }
24580
24763
  const findings = [];
24764
+ const coveredBySecurityRule = /* @__PURE__ */ new Map();
24765
+ const coveredByCloneGroup = /* @__PURE__ */ new Map();
24766
+ const coveredByDupCluster = /* @__PURE__ */ new Map();
24767
+ const coveredByGodModule = /* @__PURE__ */ new Map();
24768
+ const coveredByCycle = /* @__PURE__ */ new Map();
24581
24769
  const sevFromLiteral = (cat) => cat === "secret_suspect" ? "medium" : cat === "todo_marker" ? "info" : "low";
24582
24770
  const securityRows = [];
24583
24771
  const heuristicSec = [];
@@ -24627,7 +24815,7 @@ async function collectCode(opts) {
24627
24815
  log6(`security: repo secret scan unavailable (${e instanceof Error ? e.message : e}) \u2014 install gitleaks/trufflehog for secret coverage`);
24628
24816
  }
24629
24817
  for (const f of [...securityRows, ...heuristicSec]) {
24630
- findings.push({
24818
+ const finding = {
24631
24819
  category: "security",
24632
24820
  severity: f.severity,
24633
24821
  file: f.file,
@@ -24637,7 +24825,13 @@ async function collectCode(opts) {
24637
24825
  snippet: f.snippet,
24638
24826
  why: FINDING_WHY.security,
24639
24827
  agentPrompt: securityPrompt(f.rule, f.severity, f.file, f.line)
24640
- });
24828
+ };
24829
+ findings.push(finding);
24830
+ const byRuleList = coveredBySecurityRule.get(f.rule);
24831
+ if (byRuleList)
24832
+ byRuleList.push(finding);
24833
+ else
24834
+ coveredBySecurityRule.set(f.rule, [finding]);
24641
24835
  }
24642
24836
  for (const f of (lit?.findings ?? []).slice(0, 150)) {
24643
24837
  const file = rel(f.file);
@@ -24659,7 +24853,7 @@ async function collectCode(opts) {
24659
24853
  const names = [...new Set(fns.map((x) => x.name))];
24660
24854
  const fileList = fns.map((x) => rel(x.file)).slice(0, 6);
24661
24855
  const heavy = (g.lines ?? 0) * (g.count ?? 0) >= 150;
24662
- findings.push({
24856
+ const finding = {
24663
24857
  category: "clone",
24664
24858
  severity: heavy ? "high" : "medium",
24665
24859
  file: fileList[0] ?? "",
@@ -24669,10 +24863,12 @@ async function collectCode(opts) {
24669
24863
  snippet: `${g.lines} lines, ${g.count} copies`,
24670
24864
  why: FINDING_WHY.clone,
24671
24865
  agentPrompt: clonePrompt(names, g.lines ?? 0, g.count ?? 0, fileList)
24672
- });
24866
+ };
24867
+ findings.push(finding);
24868
+ coveredByCloneGroup.set(g, finding);
24673
24869
  }
24674
24870
  for (const c of (dup?.clusters ?? []).slice(0, 40)) {
24675
- findings.push({
24871
+ const finding = {
24676
24872
  category: "duplication",
24677
24873
  severity: "low",
24678
24874
  file: c.name ?? "",
@@ -24682,7 +24878,9 @@ async function collectCode(opts) {
24682
24878
  snippet: `${c.kind}, ${c.count} files`,
24683
24879
  why: FINDING_WHY.duplication,
24684
24880
  agentPrompt: duplicationPrompt(c.name, c.kind ?? "", c.count ?? 0)
24685
- });
24881
+ };
24882
+ findings.push(finding);
24883
+ coveredByDupCluster.set(c, finding);
24686
24884
  }
24687
24885
  for (const s of (dead?.symbols ?? []).slice(0, 120)) {
24688
24886
  const file = rel(s.file);
@@ -24700,7 +24898,7 @@ async function collectCode(opts) {
24700
24898
  }
24701
24899
  for (const m of (coup?.god_modules ?? []).slice(0, 15)) {
24702
24900
  const file = rel(m.file);
24703
- findings.push({
24901
+ const finding = {
24704
24902
  category: "coupling",
24705
24903
  severity: "medium",
24706
24904
  file,
@@ -24710,11 +24908,13 @@ async function collectCode(opts) {
24710
24908
  snippet: `fan-in ${m.fan_in}, fan-out ${m.fan_out}`,
24711
24909
  why: FINDING_WHY.coupling,
24712
24910
  agentPrompt: couplingPrompt(file, m.fan_in ?? 0, m.fan_out ?? 0, Math.round((m.instability ?? 0) * 100) / 100)
24713
- });
24911
+ };
24912
+ findings.push(finding);
24913
+ coveredByGodModule.set(file, finding);
24714
24914
  }
24715
24915
  for (const c of (cyc?.cycles ?? []).slice(0, 25)) {
24716
24916
  const chain = (c.files ?? []).map(rel);
24717
- findings.push({
24917
+ const finding = {
24718
24918
  category: "cycle",
24719
24919
  severity: "medium",
24720
24920
  file: chain[0] ?? "",
@@ -24724,7 +24924,9 @@ async function collectCode(opts) {
24724
24924
  snippet: chain.slice(0, 4).join(" \u2192 "),
24725
24925
  why: FINDING_WHY.cycle,
24726
24926
  agentPrompt: cyclePrompt(chain.slice(0, 6))
24727
- });
24927
+ };
24928
+ findings.push(finding);
24929
+ coveredByCycle.set(c, finding);
24728
24930
  }
24729
24931
  for (const f of (unwrap?.findings ?? []).slice(0, 150)) {
24730
24932
  const file = rel(f.file);
@@ -24953,7 +25155,13 @@ async function collectCode(opts) {
24953
25155
  }
24954
25156
  log6("synthesising action plan \u2026");
24955
25157
  const actions = [];
24956
- const pushAction = (pri, category, title, fix, loc, agentPrompt) => actions.push({ pri, category, title, fix, loc, agentPrompt });
25158
+ const findingIdOf = /* @__PURE__ */ new Map();
25159
+ {
25160
+ const ids = codeFindingIds(projectId, findings);
25161
+ findings.forEach((f, i) => findingIdOf.set(f, ids[i]));
25162
+ }
25163
+ const coversOf = (fs) => [...new Set(fs.map((f) => f ? findingIdOf.get(f) : void 0).filter((id) => !!id))];
25164
+ const pushAction = (pri, category, title, fix, loc, agentPrompt, covers = []) => actions.push({ pri, category, title, fix, loc, agentPrompt, covers });
24957
25165
  const byRule = /* @__PURE__ */ new Map();
24958
25166
  for (const f of securityRows) {
24959
25167
  if (isTestOrExampleFile(f.file))
@@ -24965,7 +25173,17 @@ async function collectCode(opts) {
24965
25173
  for (const [rule, hits] of byRule) {
24966
25174
  const sev = hits.some((h) => h.sev === "critical") ? "critical" : hits.some((h) => h.sev === "high") ? "high" : "medium";
24967
25175
  const pri = sev === "critical" ? 0 : sev === "high" ? 1 : 2;
24968
- pushAction(pri, "security", `${rule} \u2014 ${hits.length} occurrence(s)`, secFix(rule), hits.slice(0, 4).map((h) => ({ file: h.file, line: h.line })), securityPrompt(rule, sev, hits[0].file, hits[0].line));
25176
+ pushAction(
25177
+ pri,
25178
+ "security",
25179
+ `${rule} \u2014 ${hits.length} occurrence(s)`,
25180
+ secFix(rule),
25181
+ hits.slice(0, 4).map((h) => ({ file: h.file, line: h.line })),
25182
+ securityPrompt(rule, sev, hits[0].file, hits[0].line),
25183
+ // Every security finding on this rule. The card IS the roll-up, so the
25184
+ // members must not each file one of their own.
25185
+ coversOf(coveredBySecurityRule.get(rule) ?? [])
25186
+ );
24969
25187
  }
24970
25188
  for (const g of clones?.groups ?? []) {
24971
25189
  if ((g.count ?? 0) >= 3 || (g.lines ?? 0) >= 12) {
@@ -24974,7 +25192,7 @@ async function collectCode(opts) {
24974
25192
  continue;
24975
25193
  const names = [...new Set(fns.map((x) => x.name))];
24976
25194
  const locs = fns.map((x) => rel(x.file)).slice(0, 5);
24977
- pushAction((g.lines ?? 0) * (g.count ?? 0) >= 150 ? 1 : 2, "duplication", `${names.join(", ")} copy-pasted ${g.count}\xD7 (${g.lines} lines each)`, "Extract one shared implementation and replace the copies.", locs.map((f) => ({ file: f })), clonePrompt(names, g.lines ?? 0, g.count ?? 0, locs));
25195
+ pushAction((g.lines ?? 0) * (g.count ?? 0) >= 150 ? 1 : 2, "duplication", `${names.join(", ")} copy-pasted ${g.count}\xD7 (${g.lines} lines each)`, "Extract one shared implementation and replace the copies.", locs.map((f) => ({ file: f })), clonePrompt(names, g.lines ?? 0, g.count ?? 0, locs), coversOf([coveredByCloneGroup.get(g)]));
24978
25196
  }
24979
25197
  }
24980
25198
  for (const c of (dup?.clusters ?? []).slice(0, 12)) {
@@ -24986,19 +25204,19 @@ async function collectCode(opts) {
24986
25204
  continue;
24987
25205
  if (kind === "type_alias" || kind === "interface" || kind === "enum" || kind === "type")
24988
25206
  continue;
24989
- pushAction(2, "reuse", `${nm} (${c.kind}) reimplemented in ${c.count} files`, "Consolidate into a single shared utility/module.", [], duplicationPrompt(nm, c.kind ?? "", c.count));
25207
+ pushAction(2, "reuse", `${nm} (${c.kind}) reimplemented in ${c.count} files`, "Consolidate into a single shared utility/module.", [], duplicationPrompt(nm, c.kind ?? "", c.count), coversOf([coveredByDupCluster.get(c)]));
24990
25208
  }
24991
25209
  for (const m of (coup?.god_modules ?? []).slice(0, 5)) {
24992
25210
  const file = rel(m.file);
24993
25211
  if (isGenerated(file))
24994
25212
  continue;
24995
- pushAction(2, "modularity", `God module ${file} (fan-in ${m.fan_in}, fan-out ${m.fan_out})`, "Split into cohesive sub-modules; introduce interfaces to cut fan-out.", [{ file }], couplingPrompt(file, m.fan_in ?? 0, m.fan_out ?? 0, Math.round((m.instability ?? 0) * 100) / 100));
25213
+ pushAction(2, "modularity", `God module ${file} (fan-in ${m.fan_in}, fan-out ${m.fan_out})`, "Split into cohesive sub-modules; introduce interfaces to cut fan-out.", [{ file }], couplingPrompt(file, m.fan_in ?? 0, m.fan_out ?? 0, Math.round((m.instability ?? 0) * 100) / 100), coversOf([coveredByGodModule.get(file)]));
24996
25214
  }
24997
25215
  for (const c of (cyc?.cycles ?? []).slice(0, 5)) {
24998
25216
  const chain = (c.files ?? []).map(rel).filter((f) => !isGenerated(f));
24999
25217
  if (chain.length < 2)
25000
25218
  continue;
25001
- pushAction(2, "architecture", `Circular dependency (${chain.length} files)`, "Break the cycle: extract the shared type/interface into a separate module.", chain.slice(0, 4).map((f) => ({ file: f })), cyclePrompt(chain.slice(0, 6)));
25219
+ pushAction(2, "architecture", `Circular dependency (${chain.length} files)`, "Break the cycle: extract the shared type/interface into a separate module.", chain.slice(0, 4).map((f) => ({ file: f })), cyclePrompt(chain.slice(0, 6)), coversOf([coveredByCycle.get(c)]));
25002
25220
  }
25003
25221
  for (const h of hotspotsAll.slice(0, 5)) {
25004
25222
  pushAction(2, "stability", `Hotspot ${h.file} (${h.churn}\xD7 changes, complexity ${h.complexity})`, "Add regression tests and refactor into smaller functions before the next change.", [{ file: h.file }], hotspotPrompt(h.file, h.churn, h.complexity, h.suggestion));
@@ -25111,11 +25329,12 @@ var init_collector = __esm({
25111
25329
  "../engine/dist/core/code/collector.js"() {
25112
25330
  "use strict";
25113
25331
  init_project_resolver();
25332
+ init_code_intel();
25114
25333
  init_companions();
25115
25334
  init_secret_redactor();
25116
25335
  init_secret_scanner();
25117
25336
  init_prompts();
25118
- COLLECTOR_VERSION = 2;
25337
+ COLLECTOR_VERSION = 3;
25119
25338
  ANALYSES = [
25120
25339
  "health",
25121
25340
  "security",
@@ -25624,7 +25843,7 @@ var init_intent_drain = __esm({
25624
25843
  ownVersion2 = JSON.parse(readFileSync45(join55(dir, "../package.json"), "utf-8")).version || ownVersion2;
25625
25844
  } catch {
25626
25845
  try {
25627
- if (true) ownVersion2 = "0.5.16";
25846
+ if (true) ownVersion2 = "0.5.18";
25628
25847
  } catch {
25629
25848
  }
25630
25849
  }
@@ -25827,7 +26046,7 @@ __export(team_merge_exports, {
25827
26046
  });
25828
26047
  import { existsSync as existsSync52, mkdirSync as mkdirSync17, readFileSync as readFileSync46, rmSync as rmSync7, writeFileSync as writeFileSync18, statSync as statSync44 } from "fs";
25829
26048
  import { dirname as dirname24, join as join56 } from "path";
25830
- import { createHash as createHash17 } from "crypto";
26049
+ import { createHash as createHash18 } from "crypto";
25831
26050
  function installPathFor(art, tool) {
25832
26051
  switch (art.type) {
25833
26052
  case "skill":
@@ -25907,14 +26126,14 @@ async function mergePullResult(opts) {
25907
26126
  continue;
25908
26127
  }
25909
26128
  const body = Buffer.from(a.bodyB64, "base64");
25910
- const sha = createHash17("sha256").update(body).digest("hex");
26129
+ const sha = createHash18("sha256").update(body).digest("hex");
25911
26130
  for (const tool of targets) {
25912
26131
  const path = installPathFor(a, tool);
25913
26132
  if (!path)
25914
26133
  continue;
25915
26134
  try {
25916
26135
  const existing = readFileIfExists(path);
25917
- if (existing && createHash17("sha256").update(existing).digest("hex") === sha) {
26136
+ if (existing && createHash18("sha256").update(existing).digest("hex") === sha) {
25918
26137
  report.skipped.push({ artifactId: a.id, path, reason: "unchanged" });
25919
26138
  pending.push({ artifactId: a.id, tool, artifactType: a.type, artifactName: a.name, sha256: sha, installedAt: Date.now() });
25920
26139
  continue;
@@ -26898,7 +27117,7 @@ __export(device_auth_exports, {
26898
27117
  betterAuthDeviceLogin: () => betterAuthDeviceLogin,
26899
27118
  deviceLogin: () => deviceLogin
26900
27119
  });
26901
- import { createHash as createHash18, randomBytes } from "node:crypto";
27120
+ import { createHash as createHash19, randomBytes } from "node:crypto";
26902
27121
  async function deviceLogin(opts, onPrompt) {
26903
27122
  const issuer = (opts.issuer || DEFAULT_ISSUER).replace(/\/+$/, "");
26904
27123
  if (!issuer) {
@@ -26909,7 +27128,7 @@ async function deviceLogin(opts, onPrompt) {
26909
27128
  const clientId = opts.clientId || DEFAULT_CLIENT_ID;
26910
27129
  const scope = opts.scope || "openid email profile";
26911
27130
  const codeVerifier = b64url(randomBytes(32));
26912
- const codeChallenge = b64url(createHash18("sha256").update(codeVerifier).digest());
27131
+ const codeChallenge = b64url(createHash19("sha256").update(codeVerifier).digest());
26913
27132
  const startRes = await fetch(`${issuer}/protocol/openid-connect/auth/device`, {
26914
27133
  method: "POST",
26915
27134
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -27139,6 +27358,55 @@ function firstTarget() {
27139
27358
  if (!cred) return null;
27140
27359
  return { base: cred.serverUrl.replace(/\/+$/, ""), token: cred.token };
27141
27360
  }
27361
+ async function confirmFirstSyncScope(skipPause) {
27362
+ const { summariseSyncScope: summariseSyncScope2 } = await Promise.resolve().then(() => (init_sync_scope(), sync_scope_exports));
27363
+ let scope;
27364
+ try {
27365
+ scope = summariseSyncScope2();
27366
+ } catch {
27367
+ console.log(chalk.dim(" (could not summarise local scope \u2014 proceeding)"));
27368
+ return true;
27369
+ }
27370
+ console.log(chalk.bold("6. What would leave this machine"));
27371
+ if (scope.included === 0 && scope.heldBack === 0) {
27372
+ console.log(chalk.dim(" No local sessions found yet \u2014 nothing to ship."));
27373
+ return true;
27374
+ }
27375
+ const tools = scope.byTool.map((t) => `${t.tool} ${t.sessions}`).join(", ");
27376
+ console.log(` ${chalk.bold(String(scope.included))} session(s) would upload${tools ? chalk.dim(` (${tools})`) : ""}`);
27377
+ if (scope.heldBack > 0) {
27378
+ console.log(` ${chalk.green(String(scope.heldBack))} held back by your existing rules`);
27379
+ }
27380
+ if (scope.allowlistMode) {
27381
+ console.log(chalk.yellow(" Allowlist mode is on \u2014 only the projects you listed would ship."));
27382
+ }
27383
+ if (scope.noPathSessions > 0) {
27384
+ console.log(` ${chalk.yellow(String(scope.noPathSessions))} of those carry no project path \u2014 only \`exclude tool\` can hold those back`);
27385
+ }
27386
+ const shown = scope.projects.filter((p) => !p.heldBackBy).slice(0, 8);
27387
+ for (const p of shown) {
27388
+ console.log(` ${chalk.cyan(p.projectPath || p.id)} ${chalk.dim(`\xB7 ${p.sessions}`)}`);
27389
+ }
27390
+ const more = scope.projects.filter((p) => !p.heldBackBy).length - shown.length;
27391
+ if (more > 0) console.log(chalk.dim(` \u2026and ${more} more project(s)`));
27392
+ console.log(chalk.dim(" Secrets are masked on this machine before anything uploads."));
27393
+ console.log(chalk.dim(" Hold one back: chat-recall exclude project <path>"));
27394
+ console.log(chalk.dim(" Or invert it: chat-recall sync-only add <project>"));
27395
+ if (skipPause || !process.stdin.isTTY) {
27396
+ console.log();
27397
+ return true;
27398
+ }
27399
+ const { createInterface: createInterface3 } = await import("node:readline/promises");
27400
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
27401
+ const answer = (await rl.question(" Upload these now? [Y/n] ")).trim().toLowerCase();
27402
+ rl.close();
27403
+ if (answer === "n" || answer === "no") {
27404
+ console.log(chalk.dim(" Skipped. Set your rules, then run `chat-recall sync`."));
27405
+ return false;
27406
+ }
27407
+ console.log();
27408
+ return true;
27409
+ }
27142
27410
  function requireTarget() {
27143
27411
  const t = firstTarget();
27144
27412
  if (!t) {
@@ -27271,7 +27539,7 @@ var DEFAULT_ALLOW = [
27271
27539
  var SELF_HOST_DOCS = "https://github.com/munhq/chat-recall/blob/main/docs/SELF_HOSTING.md";
27272
27540
  var program = new Command();
27273
27541
  program.name("chat-recall").description("Semantic search for Claude Code sessions - recall and resume past conversations").version(pkgVersion);
27274
- program.command("init").description("Set up chat-recall: connect to your server, configure MCP, ship your local sessions").option("--server <url>", "chat-recall server URL to connect this machine to").option("--token <token>", "Self-host device token (skips the interactive OIDC login)").option("--skip-mcp", "Skip MCP server configuration").option("--skip-sync", "Skip the first session sync").option("--with-codeindex", "Force-download the codeindex binary during init. Default behavior is to detect an already-installed codeindex on PATH and register it as an MCP server.").option("--skip-codeindex", "Skip the codeindex companion entirely (no detection, no registration).").option("--skip-service", "Skip installing the per-user background sync service (Linux/macOS/Windows). By default init installs it so new conversations ship automatically.").action(async (options) => {
27542
+ program.command("init").description("Set up chat-recall: connect to your server, configure MCP, ship your local sessions").option("--server <url>", "chat-recall server URL to connect this machine to").option("--token <token>", "Self-host device token (skips the interactive OIDC login)").option("--skip-mcp", "Skip MCP server configuration").option("--skip-sync", "Skip the first session sync").option("--with-codeindex", "Force-download the codeindex binary during init. Default behavior is to detect an already-installed codeindex on PATH and register it as an MCP server.").option("--skip-codeindex", "Skip the codeindex companion entirely (no detection, no registration).").option("--skip-service", "Skip installing the per-user background sync service (Linux/macOS/Windows). By default init installs it so new conversations ship automatically.").option("--yes", "Do not pause before the first upload. The scope summary still prints.", false).option("--prompt-json", "Print the device prompt as one JSON line on stdout, then continue", false).action(async (options) => {
27275
27543
  try {
27276
27544
  console.log(chalk.bold("chat-recall init"));
27277
27545
  console.log();
@@ -27299,13 +27567,13 @@ program.command("init").description("Set up chat-recall: connect to your server,
27299
27567
  console.log(chalk.bold("2. Connecting to your server..."));
27300
27568
  let target = firstTarget();
27301
27569
  if (options.server) {
27302
- await runLogin(options.server, { token: options.token });
27570
+ await runLogin(options.server, { token: options.token, fromInit: true, promptJson: options.promptJson });
27303
27571
  target = firstTarget();
27304
27572
  } else if (!target) {
27305
27573
  console.log(` Connecting to ${chalk.bold(DEFAULT_SERVER)} \u2014 sign in to approve this machine.`);
27306
27574
  console.log(` ${chalk.dim(`Prefer your own server? ${chalk.bold("--server <url>")}. Self-hosting is free: ${SELF_HOST_DOCS}`)}`);
27307
27575
  try {
27308
- await runLogin(DEFAULT_SERVER, { token: options.token });
27576
+ await runLogin(DEFAULT_SERVER, { token: options.token, fromInit: true, promptJson: options.promptJson });
27309
27577
  target = firstTarget();
27310
27578
  } catch (e) {
27311
27579
  console.log(` ${chalk.yellow("Not connected.")} ${e instanceof Error ? e.message : e}`);
@@ -27452,14 +27720,19 @@ program.command("init").description("Set up chat-recall: connect to your server,
27452
27720
  console.log();
27453
27721
  }
27454
27722
  if (!options.skipSync && firstTarget()) {
27455
- console.log(chalk.bold("6. Shipping local sessions to your server..."));
27456
- try {
27457
- const { syncSessions: syncSessions2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
27458
- const r = await syncSessions2();
27459
- console.log(` ${chalk.green(`Synced ${r.uploaded} session(s), ${r.items} item(s)`)} ${chalk.dim(`\u2014 ${r.links} links, ${r.derived} derived rows, ${r.kgTriples} KG triples, ${r.skipped} skipped, ${r.redactions} secrets redacted, ${r.findings} secret findings${r.scanned ? ` (scanned ${r.scanned} in ${r.scanMs}ms)` : ""}`)}`);
27460
- } catch (err) {
27461
- console.log(` ${chalk.yellow("Sync failed")} \u2014 ${err instanceof Error ? err.message : err}`);
27462
- console.log(` ${chalk.dim("Re-run `chat-recall sync` once your server is reachable.")}`);
27723
+ const shipped = await confirmFirstSyncScope(!!options.yes);
27724
+ if (!shipped) {
27725
+ console.log();
27726
+ } else {
27727
+ console.log(chalk.dim(" Shipping..."));
27728
+ try {
27729
+ const { syncSessions: syncSessions2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
27730
+ const r = await syncSessions2();
27731
+ console.log(` ${chalk.green(`Synced ${r.uploaded} session(s), ${r.items} item(s)`)} ${chalk.dim(`\u2014 ${r.links} links, ${r.derived} derived rows, ${r.kgTriples} KG triples, ${r.skipped} skipped, ${r.redactions} secrets redacted, ${r.findings} secret findings${r.scanned ? ` (scanned ${r.scanned} in ${r.scanMs}ms)` : ""}`)}`);
27732
+ } catch (err) {
27733
+ console.log(` ${chalk.yellow("Sync failed")} \u2014 ${err instanceof Error ? err.message : err}`);
27734
+ console.log(` ${chalk.dim("Re-run `chat-recall sync` once your server is reachable.")}`);
27735
+ }
27463
27736
  }
27464
27737
  } else if (!options.skipSync) {
27465
27738
  console.log(chalk.bold("6. Skipping first sync (not logged in)"));
@@ -29364,6 +29637,10 @@ async function runLogin(serverUrl, opts) {
29364
29637
  }
29365
29638
  }
29366
29639
  const onPrompt = (p) => {
29640
+ if (opts.promptJson) {
29641
+ console.log(JSON.stringify({ prompt: p }));
29642
+ return;
29643
+ }
29367
29644
  console.log();
29368
29645
  console.log(chalk.bold("To log in, open:"));
29369
29646
  console.log(" " + chalk.cyan(p.url));
@@ -29400,7 +29677,9 @@ async function runLogin(serverUrl, opts) {
29400
29677
  const { token } = await mint.json();
29401
29678
  saveCredentials2({ serverUrl, token });
29402
29679
  console.log(chalk.green(`\u2713 Logged in to ${chalk.bold(slug)}`) + chalk.dim(` (device: ${deviceId}) server: ${serverUrl}`));
29403
- console.log(chalk.dim("Run `chat-recall sync` to push redacted conversations."));
29680
+ if (!opts.fromInit) {
29681
+ console.log(chalk.dim("Run `chat-recall sync` to push redacted conversations."));
29682
+ }
29404
29683
  } catch (err) {
29405
29684
  console.error(chalk.red("login failed:"), err instanceof Error ? err.message : err);
29406
29685
  process.exit(1);
@@ -29463,6 +29742,66 @@ include.command("remove <path>").description("Undo an opt-in (personal folder go
29463
29742
  saveSettings2(s);
29464
29743
  console.log(chalk.green(`\u2713 Removed ${path}`) + chalk.dim(" \u2014 back to skipped."));
29465
29744
  });
29745
+ var retention = program.command("retention").description("How long the server keeps your synced sessions. Bare `retention` shows the current window.");
29746
+ retention.command("show", { isDefault: true }).description("Show the window and how many sessions it would delete").action(async () => {
29747
+ const t = requireTarget();
29748
+ const r = await serverGet("/api/data/retention");
29749
+ console.log(chalk.bold("Retention") + chalk.dim(` (server: ${t.base})`));
29750
+ if (!r.days) {
29751
+ console.log(` window: ${chalk.green("none")} \u2014 the server keeps everything you sync`);
29752
+ } else {
29753
+ console.log(` window: ${chalk.yellow(`${r.days} days`)}`);
29754
+ console.log(` sessions currently outside it: ${r.wouldDelete}`);
29755
+ }
29756
+ console.log(chalk.dim(` Set one: chat-recall retention set <${r.min}-${r.max}> \xB7 clear it: chat-recall retention set 0`));
29757
+ });
29758
+ retention.command("set <days>").description("Set the window in days, or 0 to keep everything. Prints what it would delete and asks first.").option("--yes", "Skip the confirmation (for scripts). The warning still prints.", false).action(async (daysArg, opts) => {
29759
+ requireTarget();
29760
+ const days = Number(daysArg);
29761
+ if (!Number.isInteger(days) || days < 0) {
29762
+ console.error(chalk.red(`'${daysArg}' is not a whole number of days.`));
29763
+ process.exit(1);
29764
+ }
29765
+ const preview = await serverGet(
29766
+ `/api/data/retention?days=${days}`
29767
+ );
29768
+ if (days === 0) {
29769
+ await serverPost("/api/data/retention", { days: 0 });
29770
+ console.log(chalk.green("\u2713 Window cleared") + chalk.dim(" \u2014 the server keeps everything you sync."));
29771
+ return;
29772
+ }
29773
+ if (preview.wouldDelete > 0) {
29774
+ console.log();
29775
+ console.log(chalk.yellow(`This deletes ${preview.wouldDelete} session(s) from the server, and keeps deleting`));
29776
+ console.log(chalk.yellow(`anything older than ${days} days from now on.`));
29777
+ console.log();
29778
+ console.log(chalk.dim(" Re-syncing can restore a session ONLY if its transcript is still on a"));
29779
+ console.log(chalk.dim(" machine you have. For a laptop you no longer own, history your AI tool"));
29780
+ console.log(chalk.dim(" has rotated, or files you deleted, our copy is the only copy."));
29781
+ console.log(chalk.dim(" Take one first if you want it: chat-recall export"));
29782
+ console.log();
29783
+ if (!opts.yes) {
29784
+ if (!process.stdin.isTTY) {
29785
+ console.error(chalk.red("Refusing: this would delete data and there is no terminal to confirm at."));
29786
+ console.error(chalk.dim("Pass --yes if you mean it."));
29787
+ process.exit(1);
29788
+ }
29789
+ const { createInterface: createInterface3 } = await import("node:readline/promises");
29790
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
29791
+ const answer = (await rl.question(`Type ${preview.wouldDelete} to confirm: `)).trim();
29792
+ rl.close();
29793
+ if (answer !== String(preview.wouldDelete)) {
29794
+ console.log(chalk.dim("Cancelled \u2014 nothing changed."));
29795
+ return;
29796
+ }
29797
+ }
29798
+ }
29799
+ const res = await serverPost(
29800
+ "/api/data/retention",
29801
+ { days, acknowledge: true }
29802
+ );
29803
+ console.log(chalk.green(`\u2713 Window set to ${res.days} days`) + chalk.dim(` \u2014 ${res.wouldDelete} session(s) will be removed on the next sweep.`));
29804
+ });
29466
29805
  var syncOnly = program.command("sync-only").description("Opt-in sync: ship ONLY chosen projects. Bare `sync-only` shows the mode + allowlist.");
29467
29806
  syncOnly.command("list", { isDefault: true }).description("Show the sync mode and allowlisted projects").action(async () => {
29468
29807
  const { loadSettings: loadSettings2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
@@ -29575,7 +29914,7 @@ exclude.command("remove <value>").description("Remove an exclusion (tool name or
29575
29914
  saveSettings2(s);
29576
29915
  console.log(chalk.green(`\u2713 Removed ${value}`));
29577
29916
  });
29578
- program.command("login <server-url>").description("Log in via Keycloak (device flow) and mint a sync device token \u2192 ~/.chat-recall/credentials.json (0600)").option("--token <token>", "Self-host: skip OIDC and save this device token directly").option("--check", "Report-only: exit 0 if a working credential for this server exists, 1 otherwise (never interactive)").option("--issuer <url>", "OIDC issuer URL (default: learned from the server via /api/capabilities)").option("--client-id <id>", "OIDC client id (default: chat-recall-web)").option("--team <slug>", "Team to mint the device token for (default: your only team)").option("--device-id <id>", "Device id for this machine (default: hostname)").action((serverUrl, opts) => runLogin(serverUrl, opts));
29917
+ program.command("login [server-url]").description(`Log in (device flow) and mint a sync device token \u2192 ~/.chat-recall/credentials.json (0600). Defaults to ${DEFAULT_SERVER}.`).option("--token <token>", "Self-host: skip OIDC and save this device token directly").option("--check", "Report-only: exit 0 if a working credential for this server exists, 1 otherwise (never interactive)").option("--issuer <url>", "OIDC issuer URL (default: learned from the server via /api/capabilities)").option("--client-id <id>", "OIDC client id (default: chat-recall-web)").option("--team <slug>", "Team to mint the device token for (default: your only team)").option("--device-id <id>", "Device id for this machine (default: hostname)").option("--prompt-json", "Print the device prompt as one JSON line on stdout, then wait for approval", false).action((serverUrl, opts) => runLogin(serverUrl || DEFAULT_SERVER, opts));
29579
29918
  program.command("logout <server-url>").description("Remove one sync target (other targets keep syncing)").action(async (serverUrl) => {
29580
29919
  const { removeCredentials: removeCredentials2, loadAllCredentials: loadAllCredentials2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
29581
29920
  if (removeCredentials2(serverUrl)) {