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/watch.js CHANGED
@@ -115,6 +115,37 @@ var init_tool_backend = __esm({
115
115
  }
116
116
  });
117
117
 
118
+ // ../engine/dist/core/project-path-match.js
119
+ function canonicalProjectPath(p) {
120
+ return (p || "").trim().replace(/\\/g, "/").replace(/[-_]/g, "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "").toLowerCase();
121
+ }
122
+ function projectPathIncludes(candidate, rule) {
123
+ if (!candidate || !rule)
124
+ return false;
125
+ if (candidate.includes(rule))
126
+ return true;
127
+ const c = canonicalProjectPath(candidate);
128
+ const r = canonicalProjectPath(rule);
129
+ return !!r && c.includes(r);
130
+ }
131
+ function projectPathAtOrUnder(candidate, rule) {
132
+ if (!candidate || !rule)
133
+ return false;
134
+ const strip = (s) => s.replace(/\/+$/, "");
135
+ const cRaw = strip(candidate);
136
+ const rRaw = strip(rule);
137
+ if (cRaw === rRaw || cRaw.startsWith(`${rRaw}/`))
138
+ return true;
139
+ const c = canonicalProjectPath(candidate);
140
+ const r = canonicalProjectPath(rule);
141
+ return !!r && (c === r || c.startsWith(`${r}/`));
142
+ }
143
+ var init_project_path_match = __esm({
144
+ "../engine/dist/core/project-path-match.js"() {
145
+ "use strict";
146
+ }
147
+ });
148
+
118
149
  // ../../node_modules/pino-std-serializers/lib/err-helpers.js
119
150
  var require_err_helpers = __commonJS({
120
151
  "../../node_modules/pino-std-serializers/lib/err-helpers.js"(exports, module) {
@@ -4649,7 +4680,7 @@ function isProjectSyncable(projectId, projectPath, cfg) {
4649
4680
  if (projectId && cfg.syncOnly.has(projectId))
4650
4681
  return true;
4651
4682
  for (const x of cfg.syncOnly)
4652
- if (x && projectPath.includes(x))
4683
+ if (projectPathIncludes(projectPath, x))
4653
4684
  return true;
4654
4685
  return false;
4655
4686
  }
@@ -4963,6 +4994,7 @@ var CLI_DETECTION_ORDER, SCHEMA_VERSION, PERSONAL_DIR_NAMES, PERSONAL_DIR_PATTER
4963
4994
  var init_settings = __esm({
4964
4995
  "../engine/dist/core/settings.js"() {
4965
4996
  "use strict";
4997
+ init_project_path_match();
4966
4998
  init_paths();
4967
4999
  CLI_DETECTION_ORDER = [
4968
5000
  { preset: "gemini", bin: "gemini", cmd: 'gemini -p " "' },
@@ -14828,7 +14860,7 @@ function matchesProjectList(projectPath, patterns) {
14828
14860
  const tail = projectPath.slice(parent.length + 1);
14829
14861
  if (projectPath.startsWith(parent + "/") && !tail.includes("/"))
14830
14862
  return true;
14831
- } else if (projectPath === p || projectPath.startsWith(p + "/")) {
14863
+ } else if (projectPathAtOrUnder(projectPath, p)) {
14832
14864
  return true;
14833
14865
  }
14834
14866
  }
@@ -14854,6 +14886,7 @@ var cache2, TOOLS;
14854
14886
  var init_source_policy = __esm({
14855
14887
  "../engine/dist/core/source-policy.js"() {
14856
14888
  "use strict";
14889
+ init_project_path_match();
14857
14890
  init_settings();
14858
14891
  cache2 = null;
14859
14892
  TOOLS = ["claude", "gemini", "opencode", "codex", "agy", "cursor"];
@@ -19177,7 +19210,7 @@ var init_http = __esm({
19177
19210
  CLI_VERSION = JSON.parse(readFileSync33(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
19178
19211
  } catch {
19179
19212
  try {
19180
- if (true) CLI_VERSION = "0.5.16";
19213
+ if (true) CLI_VERSION = "0.5.18";
19181
19214
  } catch {
19182
19215
  }
19183
19216
  }
@@ -19670,8 +19703,8 @@ async function flush() {
19670
19703
  }
19671
19704
  function hashHost() {
19672
19705
  try {
19673
- const { createHash: createHash16 } = __require("node:crypto");
19674
- return createHash16("sha256").update(hostname()).digest("hex").slice(0, 12);
19706
+ const { createHash: createHash17 } = __require("node:crypto");
19707
+ return createHash17("sha256").update(hostname()).digest("hex").slice(0, 12);
19675
19708
  } catch {
19676
19709
  return "";
19677
19710
  }
@@ -19685,7 +19718,7 @@ var init_telemetry = __esm({
19685
19718
  init_sync_client();
19686
19719
  cliVersion = () => {
19687
19720
  try {
19688
- return true ? "0.5.16" : "dev";
19721
+ return true ? "0.5.18" : "dev";
19689
19722
  } catch {
19690
19723
  return "dev";
19691
19724
  }
@@ -20008,6 +20041,25 @@ async function fetchTenantSecurityConfig(cred) {
20008
20041
  return { tenantRules: [], verifySecrets: true, rulePackVersion: null };
20009
20042
  }
20010
20043
  }
20044
+ function syncConfigCachePath() {
20045
+ return join44(getDataDir(), "sync-config-cache.json");
20046
+ }
20047
+ function readPersistedSyncConfigs() {
20048
+ try {
20049
+ return JSON.parse(readFileSync36(syncConfigCachePath(), "utf-8"));
20050
+ } catch {
20051
+ return {};
20052
+ }
20053
+ }
20054
+ function persistSyncConfig(base2, config) {
20055
+ try {
20056
+ const all = readPersistedSyncConfigs();
20057
+ all[base2] = { fetchedAt: Date.now(), config };
20058
+ mkdirSync9(dirname18(syncConfigCachePath()), { recursive: true });
20059
+ writeFileSync9(syncConfigCachePath(), JSON.stringify(all, null, 2));
20060
+ } catch {
20061
+ }
20062
+ }
20011
20063
  async function fetchTenantSyncConfig(cred) {
20012
20064
  const base2 = cred.serverUrl.replace(/\/+$/, "");
20013
20065
  const cached = _syncConfigCache.get(base2);
@@ -20027,7 +20079,11 @@ async function fetchTenantSyncConfig(cred) {
20027
20079
  } catch {
20028
20080
  }
20029
20081
  const res = await fetchWithTimeout(`${base2}/api/sync-config`, { headers });
20030
- if (!res.ok) return { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
20082
+ if (res.status === 404) {
20083
+ _syncConfigCache.set(base2, { fetchedAt: Date.now(), config: EMPTY_SYNC_CONFIG });
20084
+ return EMPTY_SYNC_CONFIG;
20085
+ }
20086
+ if (!res.ok) return lastKnownSyncConfig(base2, `HTTP ${res.status}`);
20031
20087
  const body = await res.json().catch(() => ({}));
20032
20088
  const config = {
20033
20089
  excludeTools: Array.isArray(body.excludeTools) ? body.excludeTools.filter((t) => typeof t === "string") : [],
@@ -20065,11 +20121,22 @@ async function fetchTenantSyncConfig(cred) {
20065
20121
  } catch {
20066
20122
  }
20067
20123
  _syncConfigCache.set(base2, { fetchedAt: Date.now(), config });
20124
+ persistSyncConfig(base2, config);
20068
20125
  return config;
20069
- } catch {
20070
- return { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
20126
+ } catch (err) {
20127
+ return lastKnownSyncConfig(base2, err instanceof Error ? err.message : "request failed");
20071
20128
  }
20072
20129
  }
20130
+ function lastKnownSyncConfig(base2, reason) {
20131
+ const remembered = _syncConfigCache.get(base2) ?? readPersistedSyncConfigs()[base2];
20132
+ if (remembered) {
20133
+ console.error(`[sync] could not read server-side sync rules (${reason}) \u2014 applying the last known set from ${new Date(remembered.fetchedAt).toISOString()}`);
20134
+ return remembered.config;
20135
+ }
20136
+ throw new Error(
20137
+ `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.`
20138
+ );
20139
+ }
20073
20140
  function deviceLabel() {
20074
20141
  try {
20075
20142
  return hostname2();
@@ -20322,7 +20389,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20322
20389
  };
20323
20390
  const cleartext = opts.cleartextPaths ?? sync?.pathsCleartext === true;
20324
20391
  const mapPath = (p) => cleartext ? p : hashPath(p);
20325
- const excluded = (projectPath) => excludeProjects.some((x) => x && projectPath.includes(x));
20392
+ const excluded = (projectPath) => excludeProjects.some((x) => projectPathIncludes(projectPath, x));
20326
20393
  const syncOnly = /* @__PURE__ */ new Set([...sync?.syncOnlyProjects ?? [], ...serverCfg.syncOnlyProjects ?? []]);
20327
20394
  const includedProject = (projectPath) => {
20328
20395
  if ((sync?.syncMode ?? "all") !== "only") return true;
@@ -21282,7 +21349,7 @@ async function syncIncremental(opts = {}) {
21282
21349
  lock.release();
21283
21350
  }
21284
21351
  }
21285
- 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;
21352
+ var ownVersion, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
21286
21353
  var init_sync_client = __esm({
21287
21354
  "src/sync-client.ts"() {
21288
21355
  "use strict";
@@ -21304,6 +21371,7 @@ var init_sync_client = __esm({
21304
21371
  init_tool_backend();
21305
21372
  init_session();
21306
21373
  init_project_resolver();
21374
+ init_project_path_match();
21307
21375
  init_entity_extractor();
21308
21376
  init_all_sources();
21309
21377
  init_sync_ledger();
@@ -21327,7 +21395,7 @@ var init_sync_client = __esm({
21327
21395
  ownVersion = JSON.parse(readFileSync36(join44(dir, "../package.json"), "utf-8")).version || ownVersion;
21328
21396
  } catch {
21329
21397
  try {
21330
- if (true) ownVersion = "0.5.16";
21398
+ if (true) ownVersion = "0.5.18";
21331
21399
  } catch {
21332
21400
  }
21333
21401
  }
@@ -21337,6 +21405,7 @@ var init_sync_client = __esm({
21337
21405
  _securityConfigCache = /* @__PURE__ */ new Map();
21338
21406
  SECURITY_CONFIG_TTL_MS = 5 * 60 * 1e3;
21339
21407
  _syncConfigCache = /* @__PURE__ */ new Map();
21408
+ EMPTY_SYNC_CONFIG = { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
21340
21409
  hashPath = (p) => p ? "p_" + createHash12("sha256").update(p).digest("hex").slice(0, 12) : "";
21341
21410
  MAX_DERIVED_ROW_BYTES = 2 * 1024 * 1024;
21342
21411
  isLocalHost = isPrivateHost;
@@ -22531,6 +22600,49 @@ var init_shadow_prune = __esm({
22531
22600
  }
22532
22601
  });
22533
22602
 
22603
+ // ../engine/dist/types/code-intel.js
22604
+ import { createHash as createHash15 } from "crypto";
22605
+ function shortHash(s) {
22606
+ return createHash15("sha256").update(s).digest("hex").slice(0, 16);
22607
+ }
22608
+ function snippetKey(snippet) {
22609
+ return (snippet ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
22610
+ }
22611
+ function codeFindingIds(projectId, findings) {
22612
+ const groups = /* @__PURE__ */ new Map();
22613
+ findings.forEach((f, i) => {
22614
+ const key2 = [f.category, f.file, f.rule, identityTitle(f.title ?? ""), snippetKey(f.snippet)].join("|");
22615
+ const g = groups.get(key2);
22616
+ if (g)
22617
+ g.push(i);
22618
+ else
22619
+ groups.set(key2, [i]);
22620
+ });
22621
+ const lineOf = (i) => findings[i].line ?? -1;
22622
+ const ids = new Array(findings.length);
22623
+ for (const [key2, idxs] of groups) {
22624
+ idxs.sort((a, b) => lineOf(a) - lineOf(b));
22625
+ let ordinal = -1;
22626
+ let prevLine = Number.NaN;
22627
+ for (const idx of idxs) {
22628
+ if (lineOf(idx) !== prevLine) {
22629
+ ordinal++;
22630
+ prevLine = lineOf(idx);
22631
+ }
22632
+ ids[idx] = "cf_" + shortHash([projectId, key2, ordinal].join("|"));
22633
+ }
22634
+ }
22635
+ return ids;
22636
+ }
22637
+ function identityTitle(title) {
22638
+ return title.replace(/\d+/g, "#");
22639
+ }
22640
+ var init_code_intel = __esm({
22641
+ "../engine/dist/types/code-intel.js"() {
22642
+ "use strict";
22643
+ }
22644
+ });
22645
+
22534
22646
  // ../engine/dist/core/which.js
22535
22647
  import { existsSync as existsSync48, statSync as statSync43, accessSync, constants } from "node:fs";
22536
22648
  import { join as join52, isAbsolute as isAbsolute3, delimiter as delimiter3, sep as sep2 } from "node:path";
@@ -22595,7 +22707,7 @@ import { existsSync as existsSync49, mkdirSync as mkdirSync15, statSync as statS
22595
22707
  import { homedir as homedir19 } from "node:os";
22596
22708
  import { join as join53 } from "node:path";
22597
22709
  import { execSync as execSync3 } from "node:child_process";
22598
- import { createHash as createHash15 } from "node:crypto";
22710
+ import { createHash as createHash16 } from "node:crypto";
22599
22711
  function expectedCodeindexSha256(artifact) {
22600
22712
  return CODEINDEX_SHA256[artifact.replace(/^codeindex-/, "").replace(/\.exe$/, "")] ?? null;
22601
22713
  }
@@ -22703,7 +22815,7 @@ async function installCodeindex(opts = {}) {
22703
22815
  if (!expected) {
22704
22816
  throw new Error(`No pinned checksum for ${status.artifactName} \u2014 refusing to install an unverified binary. Add its sha256 to CODEINDEX_SHA256 in companions.ts.`);
22705
22817
  }
22706
- const actual = createHash15("sha256").update(readFileSync43(tmpPath)).digest("hex");
22818
+ const actual = createHash16("sha256").update(readFileSync43(tmpPath)).digest("hex");
22707
22819
  if (actual !== expected) {
22708
22820
  throw new Error(`Checksum mismatch for ${status.artifactName}.
22709
22821
  expected ${expected}
@@ -23090,6 +23202,11 @@ async function collectCode(opts) {
23090
23202
  }
23091
23203
  }
23092
23204
  const findings = [];
23205
+ const coveredBySecurityRule = /* @__PURE__ */ new Map();
23206
+ const coveredByCloneGroup = /* @__PURE__ */ new Map();
23207
+ const coveredByDupCluster = /* @__PURE__ */ new Map();
23208
+ const coveredByGodModule = /* @__PURE__ */ new Map();
23209
+ const coveredByCycle = /* @__PURE__ */ new Map();
23093
23210
  const sevFromLiteral = (cat) => cat === "secret_suspect" ? "medium" : cat === "todo_marker" ? "info" : "low";
23094
23211
  const securityRows = [];
23095
23212
  const heuristicSec = [];
@@ -23139,7 +23256,7 @@ async function collectCode(opts) {
23139
23256
  log6(`security: repo secret scan unavailable (${e instanceof Error ? e.message : e}) \u2014 install gitleaks/trufflehog for secret coverage`);
23140
23257
  }
23141
23258
  for (const f of [...securityRows, ...heuristicSec]) {
23142
- findings.push({
23259
+ const finding = {
23143
23260
  category: "security",
23144
23261
  severity: f.severity,
23145
23262
  file: f.file,
@@ -23149,7 +23266,13 @@ async function collectCode(opts) {
23149
23266
  snippet: f.snippet,
23150
23267
  why: FINDING_WHY.security,
23151
23268
  agentPrompt: securityPrompt(f.rule, f.severity, f.file, f.line)
23152
- });
23269
+ };
23270
+ findings.push(finding);
23271
+ const byRuleList = coveredBySecurityRule.get(f.rule);
23272
+ if (byRuleList)
23273
+ byRuleList.push(finding);
23274
+ else
23275
+ coveredBySecurityRule.set(f.rule, [finding]);
23153
23276
  }
23154
23277
  for (const f of (lit?.findings ?? []).slice(0, 150)) {
23155
23278
  const file = rel(f.file);
@@ -23171,7 +23294,7 @@ async function collectCode(opts) {
23171
23294
  const names = [...new Set(fns.map((x) => x.name))];
23172
23295
  const fileList = fns.map((x) => rel(x.file)).slice(0, 6);
23173
23296
  const heavy = (g.lines ?? 0) * (g.count ?? 0) >= 150;
23174
- findings.push({
23297
+ const finding = {
23175
23298
  category: "clone",
23176
23299
  severity: heavy ? "high" : "medium",
23177
23300
  file: fileList[0] ?? "",
@@ -23181,10 +23304,12 @@ async function collectCode(opts) {
23181
23304
  snippet: `${g.lines} lines, ${g.count} copies`,
23182
23305
  why: FINDING_WHY.clone,
23183
23306
  agentPrompt: clonePrompt(names, g.lines ?? 0, g.count ?? 0, fileList)
23184
- });
23307
+ };
23308
+ findings.push(finding);
23309
+ coveredByCloneGroup.set(g, finding);
23185
23310
  }
23186
23311
  for (const c of (dup?.clusters ?? []).slice(0, 40)) {
23187
- findings.push({
23312
+ const finding = {
23188
23313
  category: "duplication",
23189
23314
  severity: "low",
23190
23315
  file: c.name ?? "",
@@ -23194,7 +23319,9 @@ async function collectCode(opts) {
23194
23319
  snippet: `${c.kind}, ${c.count} files`,
23195
23320
  why: FINDING_WHY.duplication,
23196
23321
  agentPrompt: duplicationPrompt(c.name, c.kind ?? "", c.count ?? 0)
23197
- });
23322
+ };
23323
+ findings.push(finding);
23324
+ coveredByDupCluster.set(c, finding);
23198
23325
  }
23199
23326
  for (const s of (dead?.symbols ?? []).slice(0, 120)) {
23200
23327
  const file = rel(s.file);
@@ -23212,7 +23339,7 @@ async function collectCode(opts) {
23212
23339
  }
23213
23340
  for (const m of (coup?.god_modules ?? []).slice(0, 15)) {
23214
23341
  const file = rel(m.file);
23215
- findings.push({
23342
+ const finding = {
23216
23343
  category: "coupling",
23217
23344
  severity: "medium",
23218
23345
  file,
@@ -23222,11 +23349,13 @@ async function collectCode(opts) {
23222
23349
  snippet: `fan-in ${m.fan_in}, fan-out ${m.fan_out}`,
23223
23350
  why: FINDING_WHY.coupling,
23224
23351
  agentPrompt: couplingPrompt(file, m.fan_in ?? 0, m.fan_out ?? 0, Math.round((m.instability ?? 0) * 100) / 100)
23225
- });
23352
+ };
23353
+ findings.push(finding);
23354
+ coveredByGodModule.set(file, finding);
23226
23355
  }
23227
23356
  for (const c of (cyc?.cycles ?? []).slice(0, 25)) {
23228
23357
  const chain = (c.files ?? []).map(rel);
23229
- findings.push({
23358
+ const finding = {
23230
23359
  category: "cycle",
23231
23360
  severity: "medium",
23232
23361
  file: chain[0] ?? "",
@@ -23236,7 +23365,9 @@ async function collectCode(opts) {
23236
23365
  snippet: chain.slice(0, 4).join(" \u2192 "),
23237
23366
  why: FINDING_WHY.cycle,
23238
23367
  agentPrompt: cyclePrompt(chain.slice(0, 6))
23239
- });
23368
+ };
23369
+ findings.push(finding);
23370
+ coveredByCycle.set(c, finding);
23240
23371
  }
23241
23372
  for (const f of (unwrap?.findings ?? []).slice(0, 150)) {
23242
23373
  const file = rel(f.file);
@@ -23465,7 +23596,13 @@ async function collectCode(opts) {
23465
23596
  }
23466
23597
  log6("synthesising action plan \u2026");
23467
23598
  const actions = [];
23468
- const pushAction = (pri, category, title, fix, loc, agentPrompt) => actions.push({ pri, category, title, fix, loc, agentPrompt });
23599
+ const findingIdOf = /* @__PURE__ */ new Map();
23600
+ {
23601
+ const ids = codeFindingIds(projectId, findings);
23602
+ findings.forEach((f, i) => findingIdOf.set(f, ids[i]));
23603
+ }
23604
+ const coversOf = (fs) => [...new Set(fs.map((f) => f ? findingIdOf.get(f) : void 0).filter((id) => !!id))];
23605
+ const pushAction = (pri, category, title, fix, loc, agentPrompt, covers = []) => actions.push({ pri, category, title, fix, loc, agentPrompt, covers });
23469
23606
  const byRule = /* @__PURE__ */ new Map();
23470
23607
  for (const f of securityRows) {
23471
23608
  if (isTestOrExampleFile(f.file))
@@ -23477,7 +23614,17 @@ async function collectCode(opts) {
23477
23614
  for (const [rule, hits] of byRule) {
23478
23615
  const sev = hits.some((h) => h.sev === "critical") ? "critical" : hits.some((h) => h.sev === "high") ? "high" : "medium";
23479
23616
  const pri = sev === "critical" ? 0 : sev === "high" ? 1 : 2;
23480
- 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));
23617
+ pushAction(
23618
+ pri,
23619
+ "security",
23620
+ `${rule} \u2014 ${hits.length} occurrence(s)`,
23621
+ secFix(rule),
23622
+ hits.slice(0, 4).map((h) => ({ file: h.file, line: h.line })),
23623
+ securityPrompt(rule, sev, hits[0].file, hits[0].line),
23624
+ // Every security finding on this rule. The card IS the roll-up, so the
23625
+ // members must not each file one of their own.
23626
+ coversOf(coveredBySecurityRule.get(rule) ?? [])
23627
+ );
23481
23628
  }
23482
23629
  for (const g of clones?.groups ?? []) {
23483
23630
  if ((g.count ?? 0) >= 3 || (g.lines ?? 0) >= 12) {
@@ -23486,7 +23633,7 @@ async function collectCode(opts) {
23486
23633
  continue;
23487
23634
  const names = [...new Set(fns.map((x) => x.name))];
23488
23635
  const locs = fns.map((x) => rel(x.file)).slice(0, 5);
23489
- 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));
23636
+ 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)]));
23490
23637
  }
23491
23638
  }
23492
23639
  for (const c of (dup?.clusters ?? []).slice(0, 12)) {
@@ -23498,19 +23645,19 @@ async function collectCode(opts) {
23498
23645
  continue;
23499
23646
  if (kind === "type_alias" || kind === "interface" || kind === "enum" || kind === "type")
23500
23647
  continue;
23501
- pushAction(2, "reuse", `${nm} (${c.kind}) reimplemented in ${c.count} files`, "Consolidate into a single shared utility/module.", [], duplicationPrompt(nm, c.kind ?? "", c.count));
23648
+ 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)]));
23502
23649
  }
23503
23650
  for (const m of (coup?.god_modules ?? []).slice(0, 5)) {
23504
23651
  const file = rel(m.file);
23505
23652
  if (isGenerated(file))
23506
23653
  continue;
23507
- 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));
23654
+ 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)]));
23508
23655
  }
23509
23656
  for (const c of (cyc?.cycles ?? []).slice(0, 5)) {
23510
23657
  const chain = (c.files ?? []).map(rel).filter((f) => !isGenerated(f));
23511
23658
  if (chain.length < 2)
23512
23659
  continue;
23513
- 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)));
23660
+ 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)]));
23514
23661
  }
23515
23662
  for (const h of hotspotsAll.slice(0, 5)) {
23516
23663
  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));
@@ -23623,11 +23770,12 @@ var init_collector = __esm({
23623
23770
  "../engine/dist/core/code/collector.js"() {
23624
23771
  "use strict";
23625
23772
  init_project_resolver();
23773
+ init_code_intel();
23626
23774
  init_companions();
23627
23775
  init_secret_redactor();
23628
23776
  init_secret_scanner();
23629
23777
  init_prompts();
23630
- COLLECTOR_VERSION = 2;
23778
+ COLLECTOR_VERSION = 3;
23631
23779
  ANALYSES = [
23632
23780
  "health",
23633
23781
  "security",
@@ -24040,7 +24188,7 @@ try {
24040
24188
  ownVersion2 = JSON.parse(readFileSync40(join49(dir, "../package.json"), "utf-8")).version || ownVersion2;
24041
24189
  } catch {
24042
24190
  try {
24043
- if (true) ownVersion2 = "0.5.16";
24191
+ if (true) ownVersion2 = "0.5.18";
24044
24192
  } catch {
24045
24193
  }
24046
24194
  }
@@ -24384,7 +24532,7 @@ async function runCollectorMigration(opts) {
24384
24532
  init_sync_ledger();
24385
24533
  init_collector_health();
24386
24534
  import { existsSync as existsSync50, readFileSync as readFileSync45 } from "fs";
24387
- var CLI_VERSION3 = true ? "0.5.16" : "0.0.0";
24535
+ var CLI_VERSION3 = true ? "0.5.18" : "0.0.0";
24388
24536
  var DEBOUNCE_MS = 5e3;
24389
24537
  var CLAUDE_DIR = claudeBackend.projectsDir();
24390
24538
  var PLANS_DIR = claudeBackend.plansDir();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat-recall",
3
- "version": "0.5.16",
3
+ "version": "0.5.18",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/munhq/chat-recall.git"
@@ -35,6 +35,16 @@ and apply the test in `chat-recall-memory` before calling one:
35
35
  `recall_security_dismiss`, `recall_rename_session`, `recall_regenerate_summary`,
36
36
  `recall_reclassify`, `recall_index`, `recall_code_index`.
37
37
 
38
+ Two of those are in a class of their own — they REMOVE what the user has stored,
39
+ and the host will ask before each call because they are deliberately not on the
40
+ auto-approve list. `recall_forget` deletes one conversation from the server
41
+ permanently (their own transcript file is untouched — say so). `recall_exclude_path`
42
+ stops a path syncing, here and on their other devices. Both take `confirm: true`,
43
+ and you pass it only when the user actually asked: "forget this conversation",
44
+ "stop syncing this repo". Never infer it from frustration about a conversation.
45
+ There is no tool that undoes either — an agent may narrow what is stored, never
46
+ widen it. Widening is `chat-recall exclude remove` or the dashboard.
47
+
38
48
  `recall_improvements` is a read UNLESS you pass `create_tasks: true`, which opens
39
49
  one task per returned item on the shared team board. Pass it only when the user
40
50
  asked for the work to be tracked — the board is visible to teammates and the API
@@ -81,6 +91,8 @@ CLAUDE.md rule is additive and reversible, so take it; ask first before setting
81
91
  **Security** — `recall_security_summary` (leaked secrets that still need action — start here for "did I paste a key somewhere"), `recall_security_session` (findings for one session), `recall_security_dismiss` (mark rotated / false positive — do this only when the user confirms which), `recall_security_rules` (tenant detection rules; also tests a regex).
82
92
  **Health / maintenance** — `recall_status` (is the index alive, what is synced), `recall_index` (sync now), `recall_help` (names the tools the lean profile leaves unlisted — they all still work by name), `recall_heal_audit` (sessions whose rendered text is thinner than the raw archive, i.e. truncated upstream), `recall_regenerate_summary` (stored summary is stale or wrong), `recall_reclassify` (re-run the classifier over old chunks), `recall_rename_session`.
83
93
 
94
+ **Removing things** — `recall_forget` (delete one conversation from the server, permanently, tombstoned so no later sync restores it), `recall_exclude_path` (stop syncing a path, on this machine and on the account). Both need `confirm: true` and an explicit request from the user.
95
+
84
96
  ## If you're unsure where to start
85
97
 
86
98
  - Free-text "is there past work relevant to this?" → `recall_search` with `include_outcome: true`.
@@ -65,6 +65,17 @@ diary conclusions, and cost. Accepts a path, a name substring, or a project id
65
65
  a teammate and attached to a project. Returns the new task id.
66
66
  - `mcp__chat-recall__recall_task_update` — change status, reassign, rename or
67
67
  comment. Takes the `t_…` id from `recall_tasks`.
68
+ - **`done` needs the session that did the work.** Pass your own session id;
69
+ without one the update is refused. A card asserts a problem in the code, so
70
+ closing it asserts the code changed, and the board shows the files and commits
71
+ behind the claim. If a card should not be worked at all, use `rejected` — that
72
+ is the verdict a machine may not give itself.
73
+ - Cards filed from findings also close THEMSELVES once a re-index stops
74
+ reporting the finding, so fixing the code and re-indexing is usually enough.
75
+ Attach your session anyway: the closure keeps its evidence.
76
+ - Filing is capped — at most 10 new cards per run and 50 open at once. A board
77
+ at the ceiling files nothing until cards close; the backlog stays visible in
78
+ the ranked findings view.
68
79
  - `mcp__chat-recall__recall_team_activity` — per-teammate × per-project rollup
69
80
  (session counts, last activity) for "what is the team working on".
70
81
  - `mcp__chat-recall__recall_shares` — which projects are shared into the team