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/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 (
|
|
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
|
|
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.
|
|
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:
|
|
20195
|
-
return
|
|
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.
|
|
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 (
|
|
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
|
|
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) =>
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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.
|
|
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
|
|
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 =
|
|
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 &&
|
|
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
|
|
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(
|
|
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
|
-
|
|
27456
|
-
|
|
27457
|
-
|
|
27458
|
-
|
|
27459
|
-
console.log(
|
|
27460
|
-
|
|
27461
|
-
|
|
27462
|
-
|
|
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
|
-
|
|
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
|
|
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)) {
|