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/README.md +29 -4
- package/dist/cli.js +387 -48
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +761 -136
- package/dist/mcp.js.map +1 -1
- package/dist/open-browser.d.ts +7 -0
- package/dist/open-browser.d.ts.map +1 -0
- package/dist/open-browser.js +54 -0
- package/dist/open-browser.js.map +1 -0
- package/dist/scan-worker.js +8 -0
- package/dist/sync-client.d.ts +12 -0
- package/dist/sync-client.d.ts.map +1 -1
- package/dist/sync-client.js +66 -4
- package/dist/sync-client.js.map +1 -1
- package/dist/watch.js +181 -33
- package/package.json +1 -1
- package/skills/chat-recall/SKILL.md +12 -0
- package/skills/chat-recall-project/SKILL.md +11 -0
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 (
|
|
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
|
|
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.
|
|
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:
|
|
19674
|
-
return
|
|
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.
|
|
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 (
|
|
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
|
|
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) =>
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
@@ -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
|