chat-recall 0.5.21 → 0.5.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -13470,8 +13470,11 @@ function isInside(child, parent) {
13470
13470
  return c.startsWith(p);
13471
13471
  }
13472
13472
  function globMatch(pattern, path) {
13473
- const re = new RegExp("^" + globToRegex(pattern) + "$");
13474
- return re.test(path);
13473
+ const re = new RegExp("^" + globToRegex(toPosix(pattern)) + "$");
13474
+ return re.test(toPosix(path));
13475
+ }
13476
+ function toPosix(p) {
13477
+ return p.replace(/\\/g, "/");
13475
13478
  }
13476
13479
  function globToRegex(glob) {
13477
13480
  let out = "";
@@ -19727,7 +19730,7 @@ var init_http = __esm({
19727
19730
  CLI_VERSION = JSON.parse(readFileSync33(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
19728
19731
  } catch {
19729
19732
  try {
19730
- if (true) CLI_VERSION = "0.5.21";
19733
+ if (true) CLI_VERSION = "0.5.23";
19731
19734
  } catch {
19732
19735
  }
19733
19736
  }
@@ -20239,7 +20242,7 @@ var init_telemetry = __esm({
20239
20242
  init_sync_client();
20240
20243
  cliVersion2 = () => {
20241
20244
  try {
20242
- return true ? "0.5.21" : "dev";
20245
+ return true ? "0.5.23" : "dev";
20243
20246
  } catch {
20244
20247
  return "dev";
20245
20248
  }
@@ -20351,6 +20354,143 @@ var init_collector_health = __esm({
20351
20354
  }
20352
20355
  });
20353
20356
 
20357
+ // src/server-probe.ts
20358
+ var server_probe_exports = {};
20359
+ __export(server_probe_exports, {
20360
+ classifyUnreachable: () => classifyUnreachable,
20361
+ describeCause: () => describeCause,
20362
+ describeUnreachable: () => describeUnreachable,
20363
+ probeAdvice: () => probeAdvice,
20364
+ probeOk: () => probeOk,
20365
+ probeServer: () => probeServer
20366
+ });
20367
+ function probeOk(r) {
20368
+ return r.kind === "ok" && r.apiVersion >= 2;
20369
+ }
20370
+ async function probeServer(base2, opts = {}) {
20371
+ const f = opts.fetchImpl || fetch;
20372
+ let res;
20373
+ try {
20374
+ res = await f(`${base2.replace(/\/+$/, "")}/api/capabilities`, {
20375
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
20376
+ });
20377
+ } catch (err) {
20378
+ return {
20379
+ kind: "unreachable",
20380
+ error: describeCause(err),
20381
+ reason: classifyUnreachable(err)
20382
+ };
20383
+ }
20384
+ if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
20385
+ if (res.status === 404 || res.status === 410) return { kind: "moved", status: res.status };
20386
+ if (res.status >= 500) return { kind: "server_error", status: res.status };
20387
+ if (!res.ok) return { kind: "moved", status: res.status };
20388
+ let body;
20389
+ try {
20390
+ body = await res.text();
20391
+ } catch (err) {
20392
+ return { kind: "unreachable", error: describeCause(err), reason: classifyUnreachable(err) };
20393
+ }
20394
+ let parsed;
20395
+ try {
20396
+ parsed = JSON.parse(body);
20397
+ } catch {
20398
+ return { kind: "not_api", status: res.status, snippet: body.trim().slice(0, 60) };
20399
+ }
20400
+ if (typeof parsed?.apiVersion !== "number") {
20401
+ return { kind: "not_api", status: res.status, snippet: body.trim().slice(0, 60) };
20402
+ }
20403
+ return {
20404
+ kind: "ok",
20405
+ apiVersion: parsed.apiVersion,
20406
+ edition: parsed.edition,
20407
+ cliVersion: parsed.cli?.version,
20408
+ raw: parsed
20409
+ };
20410
+ }
20411
+ function probeAdvice(r, base2) {
20412
+ switch (r.kind) {
20413
+ case "ok":
20414
+ return r.apiVersion >= 2 ? `apiVersion ${r.apiVersion}${r.edition ? `, edition ${r.edition}` : ""}` : `apiVersion ${r.apiVersion} is too old for this CLI \u2014 upgrade the server`;
20415
+ case "moved":
20416
+ return `HTTP ${r.status} \u2014 nothing is served at ${base2}. If the host moved, re-point it: \`chat-recall login <new-server-url>\``;
20417
+ case "unauthorized":
20418
+ return `HTTP ${r.status} \u2014 ${base2} rejected the credential. Re-login: \`chat-recall login ` + base2 + "`";
20419
+ case "server_error":
20420
+ return `HTTP ${r.status} \u2014 ${base2} is up but failing. Nothing to fix locally; check the server.`;
20421
+ case "not_api":
20422
+ return `${base2} answered ${r.status} with something that is not the chat-recall API (${JSON.stringify(r.snippet)}) \u2014 check the URL, a proxy or a captive portal`;
20423
+ case "unreachable":
20424
+ switch (r.reason) {
20425
+ case "dns":
20426
+ return `cannot resolve the host in ${base2} (${r.error}) \u2014 check the URL for a typo, and your DNS or VPN`;
20427
+ case "refused":
20428
+ return `${base2} resolves but refused the connection (${r.error}) \u2014 the server is not running, or the port is wrong`;
20429
+ case "tls":
20430
+ return `the TLS certificate at ${base2} was rejected (${r.error}) \u2014 expired, self-signed, or issued for another host. A corporate proxy that re-signs traffic does this.`;
20431
+ case "timeout":
20432
+ return `${base2} did not answer in time (${r.error}) \u2014 a firewall or proxy that drops traffic silently looks exactly like this`;
20433
+ case "reset":
20434
+ return `the connection to ${base2} was cut (${r.error}) \u2014 often a proxy or a captive portal`;
20435
+ case "bad_port":
20436
+ return `${base2} names a port that Node and every browser refuse to open (the WHATWG bad-port list) \u2014 use the server's real port`;
20437
+ default:
20438
+ return `no HTTP response from ${base2} (${r.error}) \u2014 check the URL, DNS, or that the server is running`;
20439
+ }
20440
+ }
20441
+ }
20442
+ function classifyUnreachable(err) {
20443
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) return "timeout";
20444
+ for (const e of walkErrorChain(err)) {
20445
+ const code2 = String(e.code ?? "");
20446
+ const msg = String(e.message ?? "");
20447
+ if (code2 === "ENOTFOUND" || code2 === "EAI_AGAIN") return "dns";
20448
+ if (code2 === "ECONNREFUSED") return "refused";
20449
+ if (code2 === "ECONNRESET" || code2 === "EPIPE") return "reset";
20450
+ if (code2 === "ETIMEDOUT" || code2 === "UND_ERR_CONNECT_TIMEOUT" || code2 === "UND_ERR_HEADERS_TIMEOUT") return "timeout";
20451
+ if (code2.startsWith("CERT_") || code2.startsWith("ERR_TLS") || code2 === "DEPTH_ZERO_SELF_SIGNED_CERT" || code2 === "SELF_SIGNED_CERT_IN_CHAIN" || code2 === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" || /certificate/i.test(msg)) return "tls";
20452
+ if (/^bad port$/i.test(msg)) return "bad_port";
20453
+ }
20454
+ return "unknown";
20455
+ }
20456
+ function* walkErrorChain(err, depth = 0) {
20457
+ if (!err || typeof err !== "object" || depth > 5) return;
20458
+ yield err;
20459
+ const members = err.errors;
20460
+ if (Array.isArray(members)) {
20461
+ for (const m of members) yield* walkErrorChain(m, depth + 1);
20462
+ }
20463
+ yield* walkErrorChain(err.cause, depth + 1);
20464
+ }
20465
+ function describeCause(err) {
20466
+ const parts = [];
20467
+ for (const e of walkErrorChain(err)) {
20468
+ const code2 = e.code;
20469
+ if (typeof code2 === "string" && code2 && !parts.includes(code2)) parts.push(code2);
20470
+ }
20471
+ const base2 = err instanceof Error ? err.message : String(err);
20472
+ return parts.length ? `${base2}: ${parts.join(" \u2192 ")}` : base2;
20473
+ }
20474
+ function describeUnreachable(err) {
20475
+ const reason = classifyUnreachable(err);
20476
+ const detail = describeCause(err);
20477
+ const phrase = {
20478
+ dns: "the host name does not resolve",
20479
+ refused: "the connection was refused \u2014 nothing is listening there",
20480
+ tls: "the TLS certificate was rejected",
20481
+ timeout: "no answer before the timeout",
20482
+ reset: "the connection was cut mid-request",
20483
+ bad_port: "that port is one Node and every browser refuse to open",
20484
+ unknown: "no HTTP response"
20485
+ };
20486
+ return `${phrase[reason]} (${detail})`;
20487
+ }
20488
+ var init_server_probe = __esm({
20489
+ "src/server-probe.ts"() {
20490
+ "use strict";
20491
+ }
20492
+ });
20493
+
20354
20494
  // src/client-events.ts
20355
20495
  var client_events_exports = {};
20356
20496
  __export(client_events_exports, {
@@ -20758,6 +20898,7 @@ async function fetchTenantSyncConfig(cred) {
20758
20898
  persistSyncConfig(base2, config2);
20759
20899
  return config2;
20760
20900
  } catch (err) {
20901
+ if (isSyncRulesRefusal(err)) throw err;
20761
20902
  return lastKnownSyncConfig(base2, err instanceof Error ? err.message : "request failed");
20762
20903
  }
20763
20904
  }
@@ -20767,9 +20908,14 @@ function lastKnownSyncConfig(base2, reason) {
20767
20908
  console.error(`[sync] could not read server-side sync rules (${reason}) \u2014 applying the last known set from ${new Date(remembered.fetchedAt).toISOString()}`);
20768
20909
  return remembered.config;
20769
20910
  }
20770
- throw new Error(
20911
+ const refusal = new Error(
20771
20912
  `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.`
20772
20913
  );
20914
+ refusal[SYNC_RULES_REFUSAL] = true;
20915
+ throw refusal;
20916
+ }
20917
+ function isSyncRulesRefusal(err) {
20918
+ return !!err && typeof err === "object" && err[SYNC_RULES_REFUSAL] === true;
20773
20919
  }
20774
20920
  function deviceLabel() {
20775
20921
  try {
@@ -21030,7 +21176,12 @@ async function verifyServerApi(serverUrl, minApi) {
21030
21176
  const base2 = serverUrl.replace(/\/$/, "");
21031
21177
  const seen = capsOk.get(base2);
21032
21178
  if (seen !== void 0 && Date.now() - seen < CAPS_TTL_MS) return true;
21033
- const caps = await fetchWithTimeout(`${base2}/api/capabilities`).then((r) => r.json());
21179
+ const { probeServer: probeServer2, probeAdvice: probeAdvice2 } = await Promise.resolve().then(() => (init_server_probe(), server_probe_exports));
21180
+ const probe = await probeServer2(base2, {
21181
+ fetchImpl: ((url, init) => fetchWithTimeout(String(url), init ?? {}))
21182
+ });
21183
+ if (probe.kind !== "ok") throw new Error(probeAdvice2(probe, base2));
21184
+ const caps = probe.raw;
21034
21185
  if ((caps.apiVersion ?? 0) < minApi) {
21035
21186
  throw new Error(`server too old: apiVersion ${caps.apiVersion ?? "none"} < required ${minApi} \u2014 update the server image before syncing`);
21036
21187
  }
@@ -22020,7 +22171,7 @@ async function syncIncremental(opts = {}) {
22020
22171
  lock.release();
22021
22172
  }
22022
22173
  }
22023
- 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;
22174
+ var ownVersion, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, SYNC_RULES_REFUSAL, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
22024
22175
  var init_sync_client = __esm({
22025
22176
  "src/sync-client.ts"() {
22026
22177
  "use strict";
@@ -22066,7 +22217,7 @@ var init_sync_client = __esm({
22066
22217
  ownVersion = JSON.parse(readFileSync37(join44(dir, "../package.json"), "utf-8")).version || ownVersion;
22067
22218
  } catch {
22068
22219
  try {
22069
- if (true) ownVersion = "0.5.21";
22220
+ if (true) ownVersion = "0.5.23";
22070
22221
  } catch {
22071
22222
  }
22072
22223
  }
@@ -22078,6 +22229,7 @@ var init_sync_client = __esm({
22078
22229
  _syncConfigCache = /* @__PURE__ */ new Map();
22079
22230
  EMPTY_SYNC_CONFIG = { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
22080
22231
  _fetchTenantSyncConfigForTests = fetchTenantSyncConfig;
22232
+ SYNC_RULES_REFUSAL = /* @__PURE__ */ Symbol.for("chat-recall.syncRulesRefusal");
22081
22233
  hashPath = (p) => p ? "p_" + createHash13("sha256").update(p).digest("hex").slice(0, 12) : "";
22082
22234
  MAX_DERIVED_ROW_BYTES = 2 * 1024 * 1024;
22083
22235
  isLocalHost = isPrivateHost;
@@ -22237,75 +22389,6 @@ var init_which = __esm({
22237
22389
  }
22238
22390
  });
22239
22391
 
22240
- // ../engine/dist/core/sync-scope.js
22241
- var sync_scope_exports = {};
22242
- __export(sync_scope_exports, {
22243
- summariseSyncScope: () => summariseSyncScope
22244
- });
22245
- function summariseSyncScope(sinceMs = 0) {
22246
- const sync = loadSettings().sync;
22247
- const excludeProjects = sync?.excludeProjects ?? [];
22248
- const excludeTools = new Set(sync?.excludeTools ?? []);
22249
- const allowlistMode = (sync?.syncMode ?? "all") === "only";
22250
- const syncOnly2 = new Set(sync?.syncOnlyProjects ?? []);
22251
- const perProject = /* @__PURE__ */ new Map();
22252
- const perTool = /* @__PURE__ */ new Map();
22253
- for (const backend of listAvailableBackends()) {
22254
- let refs = [];
22255
- try {
22256
- refs = backend.listSessions({ sinceMs });
22257
- } catch {
22258
- continue;
22259
- }
22260
- for (const ref of refs) {
22261
- const projectPath = ref.projectPath || "";
22262
- const toolId = ref.toolId || backend.id;
22263
- let heldBackBy = null;
22264
- if (excludeTools.has(toolId))
22265
- heldBackBy = "excluded-tool";
22266
- else if (excludeProjects.some((x) => projectPathIncludes(projectPath, x)))
22267
- heldBackBy = "excluded-project";
22268
- else if (!isProjectSyncable(idOf(projectPath), projectPath, { syncMode: sync?.syncMode, syncOnly: syncOnly2 })) {
22269
- heldBackBy = "not-in-allowlist";
22270
- }
22271
- const id = idOf(projectPath);
22272
- const key2 = `${id}\0${projectPath}`;
22273
- const row = perProject.get(key2) ?? { id, projectPath, sessions: 0, heldBackBy };
22274
- row.sessions++;
22275
- row.heldBackBy = row.heldBackBy ?? heldBackBy;
22276
- perProject.set(key2, row);
22277
- if (!heldBackBy)
22278
- perTool.set(toolId, (perTool.get(toolId) ?? 0) + 1);
22279
- }
22280
- }
22281
- const projects = [...perProject.values()].sort((a, b) => b.sessions - a.sessions);
22282
- const noPathSessions = projects.filter((p) => !p.projectPath && !p.heldBackBy).reduce((n, p) => n + p.sessions, 0);
22283
- return {
22284
- included: projects.filter((p) => !p.heldBackBy).reduce((n, p) => n + p.sessions, 0),
22285
- heldBack: projects.filter((p) => p.heldBackBy).reduce((n, p) => n + p.sessions, 0),
22286
- byTool: [...perTool.entries()].map(([tool, sessions]) => ({ tool, sessions })).sort((a, b) => b.sessions - a.sessions),
22287
- projects,
22288
- allowlistMode,
22289
- noPathSessions
22290
- };
22291
- }
22292
- function idOf(projectPath) {
22293
- if (!projectPath)
22294
- return "";
22295
- const r = resolveProjectId(projectPath);
22296
- return r.source === "ignored" ? "" : r.id;
22297
- }
22298
- var init_sync_scope = __esm({
22299
- "../engine/dist/core/sync-scope.js"() {
22300
- "use strict";
22301
- init_tool_backend();
22302
- init_backends();
22303
- init_project_resolver();
22304
- init_settings();
22305
- init_project_path_match();
22306
- }
22307
- });
22308
-
22309
22392
  // ../engine/dist/core/mcp-clients.js
22310
22393
  var mcp_clients_exports = {};
22311
22394
  __export(mcp_clients_exports, {
@@ -23571,7 +23654,7 @@ __export(toolkit_pull_exports, {
23571
23654
  portableCommand: () => portableCommand
23572
23655
  });
23573
23656
  import { existsSync as existsSync48, mkdirSync as mkdirSync14, statSync as statSync42, writeFileSync as writeFileSync15 } from "node:fs";
23574
- import { delimiter as delimiter3, dirname as dirname22, join as join52, basename as basename25, isAbsolute as isAbsolute3 } from "node:path";
23657
+ import { dirname as dirname22, join as join52, basename as basename25, isAbsolute as isAbsolute3 } from "node:path";
23575
23658
  function isExecutableFile(p) {
23576
23659
  try {
23577
23660
  return statSync42(p).isFile();
@@ -23580,13 +23663,7 @@ function isExecutableFile(p) {
23580
23663
  }
23581
23664
  }
23582
23665
  function onPath(name) {
23583
- const dirs = (process.env.PATH || "").split(delimiter3).filter(Boolean);
23584
- for (const d of dirs) {
23585
- const candidate = join52(d, name);
23586
- if (isExecutableFile(candidate))
23587
- return candidate;
23588
- }
23589
- return null;
23666
+ return resolveOnPath(name);
23590
23667
  }
23591
23668
  function portableCommand(cmd) {
23592
23669
  if (!isAbsolute3(cmd)) {
@@ -23848,6 +23925,7 @@ var NOT_REBUILDABLE;
23848
23925
  var init_toolkit_pull = __esm({
23849
23926
  "../engine/dist/core/toolkit-pull.js"() {
23850
23927
  "use strict";
23928
+ init_which();
23851
23929
  init_toolkit_sync();
23852
23930
  init_artifact_codec();
23853
23931
  NOT_REBUILDABLE = {
@@ -24214,74 +24292,6 @@ var init_escalate = __esm({
24214
24292
  }
24215
24293
  });
24216
24294
 
24217
- // src/server-probe.ts
24218
- var server_probe_exports = {};
24219
- __export(server_probe_exports, {
24220
- probeAdvice: () => probeAdvice,
24221
- probeOk: () => probeOk,
24222
- probeServer: () => probeServer
24223
- });
24224
- function probeOk(r) {
24225
- return r.kind === "ok" && r.apiVersion >= 2;
24226
- }
24227
- async function probeServer(base2, opts = {}) {
24228
- const f = opts.fetchImpl || fetch;
24229
- let res;
24230
- try {
24231
- res = await f(`${base2.replace(/\/+$/, "")}/api/capabilities`, {
24232
- signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
24233
- });
24234
- } catch (err) {
24235
- return { kind: "unreachable", error: err instanceof Error ? err.message : String(err) };
24236
- }
24237
- if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
24238
- if (res.status === 404 || res.status === 410) return { kind: "moved", status: res.status };
24239
- if (res.status >= 500) return { kind: "server_error", status: res.status };
24240
- if (!res.ok) return { kind: "moved", status: res.status };
24241
- let body;
24242
- try {
24243
- body = await res.text();
24244
- } catch (err) {
24245
- return { kind: "unreachable", error: err instanceof Error ? err.message : String(err) };
24246
- }
24247
- let parsed;
24248
- try {
24249
- parsed = JSON.parse(body);
24250
- } catch {
24251
- return { kind: "not_api", status: res.status, snippet: body.trim().slice(0, 60) };
24252
- }
24253
- if (typeof parsed?.apiVersion !== "number") {
24254
- return { kind: "not_api", status: res.status, snippet: body.trim().slice(0, 60) };
24255
- }
24256
- return {
24257
- kind: "ok",
24258
- apiVersion: parsed.apiVersion,
24259
- edition: parsed.edition,
24260
- cliVersion: parsed.cli?.version
24261
- };
24262
- }
24263
- function probeAdvice(r, base2) {
24264
- switch (r.kind) {
24265
- case "ok":
24266
- return r.apiVersion >= 2 ? `apiVersion ${r.apiVersion}${r.edition ? `, edition ${r.edition}` : ""}` : `apiVersion ${r.apiVersion} is too old for this CLI \u2014 upgrade the server`;
24267
- case "moved":
24268
- return `HTTP ${r.status} \u2014 nothing is served at ${base2}. If the host moved, re-point it: \`chat-recall login <new-server-url>\``;
24269
- case "unauthorized":
24270
- return `HTTP ${r.status} \u2014 ${base2} rejected the credential. Re-login: \`chat-recall login ` + base2 + "`";
24271
- case "server_error":
24272
- return `HTTP ${r.status} \u2014 ${base2} is up but failing. Nothing to fix locally; check the server.`;
24273
- case "not_api":
24274
- return `${base2} answered ${r.status} with something that is not the chat-recall API (${JSON.stringify(r.snippet)}) \u2014 check the URL, a proxy or a captive portal`;
24275
- case "unreachable":
24276
- return `no HTTP response from ${base2} (${r.error}) \u2014 check the URL, DNS, or that the server is running`;
24277
- }
24278
- }
24279
- var init_server_probe = __esm({
24280
- "src/server-probe.ts"() {
24281
- "use strict";
24282
- }
24283
- });
24284
-
24285
24295
  // src/verify-deep.ts
24286
24296
  var verify_deep_exports = {};
24287
24297
  __export(verify_deep_exports, {
@@ -25862,7 +25872,7 @@ var init_intent_drain = __esm({
25862
25872
  ownVersion2 = JSON.parse(readFileSync46(join55(dir, "../package.json"), "utf-8")).version || ownVersion2;
25863
25873
  } catch {
25864
25874
  try {
25865
- if (true) ownVersion2 = "0.5.21";
25875
+ if (true) ownVersion2 = "0.5.23";
25866
25876
  } catch {
25867
25877
  }
25868
25878
  }
@@ -27137,6 +27147,24 @@ __export(device_auth_exports, {
27137
27147
  deviceLogin: () => deviceLogin
27138
27148
  });
27139
27149
  import { createHash as createHash19, randomBytes } from "node:crypto";
27150
+ async function readDeviceCode(res, where) {
27151
+ const text = await res.text().catch(() => "");
27152
+ let parsed;
27153
+ try {
27154
+ parsed = JSON.parse(text);
27155
+ } catch {
27156
+ throw new Error(
27157
+ `${where} answered ${res.status} with something that is not a device-code response (${JSON.stringify(text.trim().slice(0, 60))}) \u2014 this host is not serving the chat-recall API`
27158
+ );
27159
+ }
27160
+ const d = parsed;
27161
+ if (typeof d?.device_code !== "string" || typeof d?.user_code !== "string" || typeof d?.verification_uri !== "string" || typeof d?.expires_in !== "number") {
27162
+ throw new Error(
27163
+ `${where} answered ${res.status} with JSON that is not a device-code response (${JSON.stringify(text.trim().slice(0, 60))}) \u2014 this host is not serving the chat-recall API`
27164
+ );
27165
+ }
27166
+ return d;
27167
+ }
27140
27168
  async function deviceLogin(opts, onPrompt) {
27141
27169
  const issuer = (opts.issuer || DEFAULT_ISSUER).replace(/\/+$/, "");
27142
27170
  if (!issuer) {
@@ -27156,7 +27184,7 @@ async function deviceLogin(opts, onPrompt) {
27156
27184
  if (!startRes.ok) {
27157
27185
  throw new Error(`device-auth init failed: HTTP ${startRes.status} ${await startRes.text().catch(() => "")}`);
27158
27186
  }
27159
- const start = await startRes.json();
27187
+ const start = await readDeviceCode(startRes, issuer);
27160
27188
  onPrompt({
27161
27189
  url: start.verification_uri_complete || start.verification_uri,
27162
27190
  userCode: start.user_code,
@@ -27210,7 +27238,7 @@ async function betterAuthDeviceLogin(serverUrl, onPrompt) {
27210
27238
  if (!startRes.ok) {
27211
27239
  throw new Error(`device-auth init failed: HTTP ${startRes.status} ${await startRes.text().catch(() => "")}`);
27212
27240
  }
27213
- const start = await startRes.json();
27241
+ const start = await readDeviceCode(startRes, base2);
27214
27242
  onPrompt({
27215
27243
  url: start.verification_uri_complete || start.verification_uri,
27216
27244
  userCode: start.user_code,
@@ -27261,6 +27289,50 @@ var init_device_auth = __esm({
27261
27289
  }
27262
27290
  });
27263
27291
 
27292
+ // src/install-report.ts
27293
+ var install_report_exports = {};
27294
+ __export(install_report_exports, {
27295
+ reportInstallFailure: () => reportInstallFailure
27296
+ });
27297
+ async function reportInstallFailure(serverUrl, failure, opts = {}) {
27298
+ if (!serverUrl) return false;
27299
+ if (!userConsents() && !mayReport(serverUrl)) return false;
27300
+ const payload = {
27301
+ step: failure.step,
27302
+ reason: trim(failure.reason, 40),
27303
+ message: trim(failure.message, 200),
27304
+ cliVersion: trim(process.env.CHAT_RECALL_CLI_VERSION, 20),
27305
+ node: trim(process.versions.node, 20),
27306
+ os: `${process.platform}-${process.arch}`
27307
+ };
27308
+ try {
27309
+ assertNoSensitiveKeys(payload);
27310
+ } catch {
27311
+ return false;
27312
+ }
27313
+ const f = opts.fetchImpl || fetch;
27314
+ try {
27315
+ const res = await f(`${serverUrl.replace(/\/+$/, "")}/install/report`, {
27316
+ method: "POST",
27317
+ headers: { "content-type": "application/json" },
27318
+ body: JSON.stringify(payload),
27319
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 3e3)
27320
+ });
27321
+ return res.ok || res.status === 204;
27322
+ } catch {
27323
+ return false;
27324
+ }
27325
+ }
27326
+ function trim(v, n) {
27327
+ return typeof v === "string" && v.trim() ? v.trim().slice(0, n) : null;
27328
+ }
27329
+ var init_install_report = __esm({
27330
+ "src/install-report.ts"() {
27331
+ "use strict";
27332
+ init_telemetry_consent();
27333
+ }
27334
+ });
27335
+
27264
27336
  // ../engine/dist/core/resume-command.js
27265
27337
  function toolOfSessionId(sessionId) {
27266
27338
  if (sessionId.startsWith("codex_"))
@@ -27337,6 +27409,7 @@ function tierAll(results) {
27337
27409
  // src/cli.ts
27338
27410
  init_sync_client();
27339
27411
  init_update_notice();
27412
+ init_server_probe();
27340
27413
  init_which();
27341
27414
  init_collector_health();
27342
27415
  init_telemetry_consent();
@@ -27377,54 +27450,15 @@ function firstTarget() {
27377
27450
  if (!cred) return null;
27378
27451
  return { base: cred.serverUrl.replace(/\/+$/, ""), token: cred.token };
27379
27452
  }
27380
- async function confirmFirstSyncScope(skipPause) {
27381
- const { summariseSyncScope: summariseSyncScope2 } = await Promise.resolve().then(() => (init_sync_scope(), sync_scope_exports));
27382
- let scope;
27383
- try {
27384
- scope = summariseSyncScope2();
27385
- } catch {
27386
- console.log(chalk.dim(" (could not summarise local scope \u2014 proceeding)"));
27387
- return true;
27388
- }
27389
- console.log(chalk.bold("6. What would leave this machine"));
27390
- if (scope.included === 0 && scope.heldBack === 0) {
27391
- console.log(chalk.dim(" No local sessions found yet \u2014 nothing to ship."));
27392
- return true;
27393
- }
27394
- const tools = scope.byTool.map((t) => `${t.tool} ${t.sessions}`).join(", ");
27395
- console.log(` ${chalk.bold(String(scope.included))} session(s) would upload${tools ? chalk.dim(` (${tools})`) : ""}`);
27396
- if (scope.heldBack > 0) {
27397
- console.log(` ${chalk.green(String(scope.heldBack))} held back by your existing rules`);
27398
- }
27399
- if (scope.allowlistMode) {
27400
- console.log(chalk.yellow(" Allowlist mode is on \u2014 only the projects you listed would ship."));
27401
- }
27402
- if (scope.noPathSessions > 0) {
27403
- console.log(` ${chalk.yellow(String(scope.noPathSessions))} of those carry no project path \u2014 only \`exclude tool\` can hold those back`);
27404
- }
27405
- const shown = scope.projects.filter((p) => !p.heldBackBy).slice(0, 8);
27406
- for (const p of shown) {
27407
- console.log(` ${chalk.cyan(p.projectPath || p.id)} ${chalk.dim(`\xB7 ${p.sessions}`)}`);
27408
- }
27409
- const more = scope.projects.filter((p) => !p.heldBackBy).length - shown.length;
27410
- if (more > 0) console.log(chalk.dim(` \u2026and ${more} more project(s)`));
27411
- console.log(chalk.dim(" Secrets are masked on this machine before anything uploads."));
27412
- console.log(chalk.dim(" Hold one back: chat-recall exclude project <path>"));
27413
- console.log(chalk.dim(" Or invert it: chat-recall sync-only add <project>"));
27414
- if (skipPause || !process.stdin.isTTY) {
27415
- console.log();
27416
- return true;
27417
- }
27418
- const { createInterface: createInterface3 } = await import("node:readline/promises");
27419
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
27420
- const answer = (await rl.question(" Upload these now? [Y/n] ")).trim().toLowerCase();
27421
- rl.close();
27422
- if (answer === "n" || answer === "no") {
27423
- console.log(chalk.dim(" Skipped. Set your rules, then run `chat-recall sync`."));
27424
- return false;
27425
- }
27426
- console.log();
27427
- return true;
27453
+ function describeProbeError(err) {
27454
+ const msg = err instanceof Error ? err.message : String(err);
27455
+ if (!/fetch failed|ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|CERT_|aborted|timeout/i.test(msg)) return msg;
27456
+ return describeUnreachable(err);
27457
+ }
27458
+ function failLogin(opts, message) {
27459
+ if (opts.fromInit) throw new Error(message);
27460
+ console.error(chalk.red("login failed:"), message);
27461
+ process.exit(1);
27428
27462
  }
27429
27463
  function requireTarget() {
27430
27464
  const t = firstTarget();
@@ -27434,11 +27468,21 @@ function requireTarget() {
27434
27468
  }
27435
27469
  return t;
27436
27470
  }
27471
+ async function readJsonBody(res, base2, what) {
27472
+ const text = await res.text().catch(() => "");
27473
+ try {
27474
+ return JSON.parse(text);
27475
+ } catch {
27476
+ throw new Error(
27477
+ `${base2} answered ${res.status} with something that is not JSON (${JSON.stringify(text.trim().slice(0, 60))}) for ${what} \u2014 a proxy, a captive portal or a host that no longer serves the chat-recall API`
27478
+ );
27479
+ }
27480
+ }
27437
27481
  async function serverGet(path) {
27438
27482
  const t = requireTarget();
27439
27483
  const res = await fetch(t.base + path, { headers: t.token ? { authorization: `Bearer ${t.token}` } : {} });
27440
27484
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27441
- return res.json();
27485
+ return readJsonBody(res, t.base, `GET ${path}`);
27442
27486
  }
27443
27487
  async function serverGetSoft(path) {
27444
27488
  const t = requireTarget();
@@ -27448,7 +27492,7 @@ async function serverGetSoft(path) {
27448
27492
  return { status: res.status, data: null, message: body.message };
27449
27493
  }
27450
27494
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27451
- return { status: res.status, data: await res.json() };
27495
+ return { status: res.status, data: await readJsonBody(res, t.base, `GET ${path}`) };
27452
27496
  }
27453
27497
  async function serverPost(path, body) {
27454
27498
  const t = requireTarget();
@@ -27458,7 +27502,7 @@ async function serverPost(path, body) {
27458
27502
  body: JSON.stringify(body)
27459
27503
  });
27460
27504
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27461
- return res.json();
27505
+ return readJsonBody(res, t.base, `POST ${path}`);
27462
27506
  }
27463
27507
  async function serverPatch(path, body) {
27464
27508
  const t = requireTarget();
@@ -27468,7 +27512,7 @@ async function serverPatch(path, body) {
27468
27512
  body: JSON.stringify(body)
27469
27513
  });
27470
27514
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27471
- return res.json();
27515
+ return readJsonBody(res, t.base, `PATCH ${path}`);
27472
27516
  }
27473
27517
  async function serverDelete(path, body) {
27474
27518
  const t = requireTarget();
@@ -27478,12 +27522,19 @@ async function serverDelete(path, body) {
27478
27522
  body: JSON.stringify(body)
27479
27523
  });
27480
27524
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27481
- return res.json();
27525
+ return readJsonBody(res, t.base, `DELETE ${path}`);
27482
27526
  }
27527
+ var pkgEngines = (() => {
27528
+ try {
27529
+ return JSON.parse(readFileSync49(new URL("../package.json", import.meta.url), "utf-8")).engines?.node ?? "";
27530
+ } catch {
27531
+ return "";
27532
+ }
27533
+ })();
27483
27534
  var pkgVersion = JSON.parse(
27484
27535
  readFileSync49(new URL("../package.json", import.meta.url), "utf-8")
27485
27536
  ).version;
27486
- var DEFAULT_SERVER = "https://chatrecall.dev";
27537
+ var DEFAULT_SERVER = (process.env.CHAT_RECALL_DEFAULT_SERVER || "https://chatrecall.dev").replace(/\/+$/, "");
27487
27538
  var DEFAULT_ALLOW = [
27488
27539
  "recall_search",
27489
27540
  "recall_show",
@@ -27556,11 +27607,21 @@ var DEFAULT_ALLOW = [
27556
27607
  "recall_toolkit_sync"
27557
27608
  ];
27558
27609
  var SELF_HOST_DOCS = "https://github.com/munhq/chat-recall/blob/main/docs/SELF_HOSTING.md";
27610
+ function enforceNodeVersion() {
27611
+ const required = Number(/(\d+)/.exec(String(pkgEngines))?.[1] ?? 0);
27612
+ const found = Number(process.versions.node.split(".")[0]);
27613
+ if (!required || !found || found >= required) return;
27614
+ console.error(`chat-recall needs Node ${required} or newer. This is Node ${process.versions.node}.`);
27615
+ console.error(`Install it with your version manager (\`nvm install ${required}\`, \`fnm install ${required}\`, \`brew upgrade node\`), then run this again.`);
27616
+ process.exit(1);
27617
+ }
27618
+ process.env.CHAT_RECALL_CLI_VERSION = pkgVersion;
27619
+ enforceNodeVersion();
27559
27620
  var program = new Command();
27560
27621
  program.name("chat-recall").description("Semantic search for Claude Code sessions - recall and resume past conversations").version(pkgVersion);
27561
27622
  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) => {
27562
27623
  try {
27563
- console.log(chalk.bold("chat-recall init"));
27624
+ console.log(chalk.bold("chat-recall init") + chalk.dim(` v${pkgVersion}`));
27564
27625
  console.log();
27565
27626
  console.log(chalk.bold("1. Detecting AI tools..."));
27566
27627
  const clis = [
@@ -27585,9 +27646,15 @@ program.command("init").description("Set up chat-recall: connect to your server,
27585
27646
  console.log();
27586
27647
  console.log(chalk.bold("2. Connecting to your server..."));
27587
27648
  let target = firstTarget();
27649
+ let deadTarget = false;
27588
27650
  if (options.server) {
27589
- await runLogin(options.server, { token: options.token, fromInit: true, promptJson: options.promptJson });
27590
- target = firstTarget();
27651
+ try {
27652
+ await runLogin(options.server, { token: options.token, fromInit: true, promptJson: options.promptJson });
27653
+ target = firstTarget();
27654
+ } catch (e) {
27655
+ console.log(` ${chalk.yellow("Not connected.")} ${describeProbeError(e)}`);
27656
+ console.log(` ${chalk.dim(`Run ${chalk.bold(`chat-recall login ${options.server}`)} when ready, or self-host: ${SELF_HOST_DOCS}`)}`);
27657
+ }
27591
27658
  } else if (!target) {
27592
27659
  console.log(` Connecting to ${chalk.bold(DEFAULT_SERVER)} \u2014 sign in to approve this machine.`);
27593
27660
  console.log(` ${chalk.dim(`Prefer your own server? ${chalk.bold("--server <url>")}. Self-hosting is free: ${SELF_HOST_DOCS}`)}`);
@@ -27595,16 +27662,43 @@ program.command("init").description("Set up chat-recall: connect to your server,
27595
27662
  await runLogin(DEFAULT_SERVER, { token: options.token, fromInit: true, promptJson: options.promptJson });
27596
27663
  target = firstTarget();
27597
27664
  } catch (e) {
27598
- console.log(` ${chalk.yellow("Not connected.")} ${e instanceof Error ? e.message : e}`);
27665
+ console.log(` ${chalk.yellow("Not connected.")} ${describeProbeError(e)}`);
27599
27666
  console.log(` ${chalk.dim(`Run ${chalk.bold("chat-recall login <server-url>")} when ready, or self-host: ${SELF_HOST_DOCS}`)}`);
27600
27667
  }
27601
27668
  }
27602
- if (target) {
27669
+ if (target && !options.server) {
27670
+ const { probeServer: probeServer2, probeOk: probeOk2, probeAdvice: probeAdvice2 } = await Promise.resolve().then(() => (init_server_probe(), server_probe_exports));
27671
+ const probe = await probeServer2(target.base, { timeoutMs: 1e4 });
27672
+ if (probeOk2(probe)) {
27673
+ console.log(` ${chalk.green("Connected")} \u2192 ${target.base}`);
27674
+ } else if (probe.kind === "moved" || probe.kind === "not_api") {
27675
+ const retired = target.base;
27676
+ console.log(` ${chalk.yellow("The saved server no longer serves chat-recall")} \u2192 ${retired}`);
27677
+ console.log(` ${probeAdvice2(probe, retired)}`);
27678
+ console.log(` Switching to ${chalk.bold(DEFAULT_SERVER)} \u2014 sign in to approve this machine.`);
27679
+ try {
27680
+ await runLogin(DEFAULT_SERVER, { token: options.token, fromInit: true, promptJson: options.promptJson });
27681
+ const { removeCredentials: removeCredentials2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
27682
+ if (removeCredentials2(retired)) {
27683
+ console.log(` ${chalk.dim(`Removed the retired target ${retired} (log in there again any time).`)}`);
27684
+ }
27685
+ target = firstTarget();
27686
+ if (target) console.log(` ${chalk.green("Connected")} \u2192 ${target.base}`);
27687
+ } catch (e) {
27688
+ console.log(` ${chalk.yellow("Not connected.")} ${describeProbeError(e)}`);
27689
+ console.log(` ${chalk.dim(`Run ${chalk.bold(`chat-recall login ${DEFAULT_SERVER}`)} when ready, then ${chalk.bold(`chat-recall logout ${retired}`)}.`)}`);
27690
+ deadTarget = true;
27691
+ }
27692
+ } else {
27693
+ console.log(` ${chalk.yellow("Saved server did not answer")} \u2192 ${target.base}`);
27694
+ console.log(` ${probeAdvice2(probe, target.base)}`);
27695
+ }
27696
+ } else if (target) {
27603
27697
  console.log(` ${chalk.green("Connected")} \u2192 ${target.base}`);
27604
27698
  }
27605
27699
  console.log();
27606
27700
  if (!options.skipMcp) {
27607
- console.log(chalk.bold("4. Configuring MCP server..."));
27701
+ console.log(chalk.bold("3. Configuring MCP server..."));
27608
27702
  const projectRoot = join60(import.meta.dirname, "..");
27609
27703
  let mcpBinOnPath = false;
27610
27704
  mcpBinOnPath = isOnPath("chat-recall-mcp");
@@ -27629,13 +27723,13 @@ program.command("init").description("Set up chat-recall: connect to your server,
27629
27723
  }
27630
27724
  console.log(chalk.dim(` Launch: ${launch.command}${launch.args ? " " + launch.args.join(" ") : ""}`));
27631
27725
  } else {
27632
- console.log(chalk.bold("4. Skipping MCP configuration (--skip-mcp)"));
27726
+ console.log(chalk.bold("3. Skipping MCP configuration (--skip-mcp)"));
27633
27727
  }
27634
27728
  console.log();
27635
27729
  const skipCodeindex = options.skipCodeindex === true;
27636
27730
  const forceInstall = options.withCodeindex === true || process.env.CHAT_RECALL_WITH_CODEINDEX === "1";
27637
27731
  if (skipCodeindex) {
27638
- console.log(chalk.bold("5. Skipping codeindex companion (--skip-codeindex)"));
27732
+ console.log(chalk.bold("4. Skipping codeindex companion (--skip-codeindex)"));
27639
27733
  } else {
27640
27734
  const {
27641
27735
  checkCodeindexStatus: checkCodeindexStatus2,
@@ -27645,7 +27739,7 @@ program.command("init").description("Set up chat-recall: connect to your server,
27645
27739
  } = await Promise.resolve().then(() => (init_companions(), companions_exports));
27646
27740
  const detected = checkCodeindexStatus2();
27647
27741
  if (detected.installed && !forceInstall) {
27648
- console.log(chalk.bold("5. Detected codeindex companion"));
27742
+ console.log(chalk.bold("4. Detected codeindex companion"));
27649
27743
  console.log(` ${chalk.green("codeindex")}: ${detected.path}`);
27650
27744
  if (!options.skipMcp) {
27651
27745
  const mcpJsonPath = join60(homedir22(), ".mcp.json");
@@ -27654,7 +27748,7 @@ program.command("init").description("Set up chat-recall: connect to your server,
27654
27748
  else console.log(` codeindex MCP server: ${chalk.green("already registered")}`);
27655
27749
  }
27656
27750
  } else if (forceInstall) {
27657
- console.log(chalk.bold("5. Installing codeindex companion (--with-codeindex)..."));
27751
+ console.log(chalk.bold("4. Installing codeindex companion (--with-codeindex)..."));
27658
27752
  try {
27659
27753
  const result = await installCodeindex2({ force: true });
27660
27754
  if (result.installed) {
@@ -27677,7 +27771,7 @@ program.command("init").description("Set up chat-recall: connect to your server,
27677
27771
  }
27678
27772
  }
27679
27773
  } else {
27680
- console.log(chalk.bold("5. codeindex companion: not installed"));
27774
+ console.log(chalk.bold("4. codeindex companion: not installed"));
27681
27775
  console.log(chalk.dim(" codeindex is a separate MCP server for code-level lookup \u2014"));
27682
27776
  console.log(chalk.dim(" find_symbol, plan_change, get_change_impact, etc."));
27683
27777
  console.log(chalk.dim(" To install: `chat-recall init --with-codeindex`"));
@@ -27708,22 +27802,40 @@ program.command("init").description("Set up chat-recall: connect to your server,
27708
27802
  const { executePull: executePull2 } = await Promise.resolve().then(() => (init_toolkit_pull(), toolkit_pull_exports));
27709
27803
  const { hostname: hostname3 } = await import("node:os");
27710
27804
  const rows = [];
27805
+ let answered = 0;
27806
+ const askFailed = [];
27711
27807
  for (const t of loadAllCredentials2()) {
27712
27808
  try {
27713
27809
  const res = await fetch(`${t.serverUrl}/api/toolkit/browse/mcp?limit=1000`, {
27714
27810
  headers: t.token ? { authorization: `Bearer ${t.token}` } : {},
27715
27811
  signal: AbortSignal.timeout(2e4)
27716
27812
  });
27717
- if (!res.ok) continue;
27718
- const body = await res.json();
27813
+ if (!res.ok) {
27814
+ askFailed.push(`${t.serverUrl} (HTTP ${res.status})`);
27815
+ continue;
27816
+ }
27817
+ const text = await res.text();
27818
+ let body;
27819
+ try {
27820
+ body = JSON.parse(text);
27821
+ } catch {
27822
+ askFailed.push(`${t.serverUrl} (not JSON: ${JSON.stringify(text.trim().slice(0, 40))})`);
27823
+ continue;
27824
+ }
27825
+ answered++;
27719
27826
  for (const it of body.items || []) rows.push(it);
27720
- } catch {
27827
+ } catch (e) {
27828
+ askFailed.push(`${t.serverUrl} (${describeProbeError(e)})`);
27721
27829
  }
27722
27830
  }
27723
27831
  const report = executePull2(rows, { thisDeviceId: hostname3(), types: ["mcp"] });
27724
27832
  const written = report.outcomes.filter((o) => o.status === "written");
27725
27833
  const present = report.outcomes.filter((o) => o.status === "present").length;
27726
- if (written.length === 0 && present === 0) {
27834
+ if (answered === 0) {
27835
+ console.log(` ${chalk.yellow("Could not ask your server what MCP servers are on your account")}`);
27836
+ for (const f of askFailed) console.log(` ${chalk.dim(f)}`);
27837
+ console.log(` ${chalk.dim("Re-run `chat-recall toolkit pull` once the server answers.")}`);
27838
+ } else if (written.length === 0 && present === 0) {
27727
27839
  console.log(chalk.dim(" Nothing on your account to install yet."));
27728
27840
  } else {
27729
27841
  console.log(` ${chalk.green(`Installed ${written.length}`)} ${chalk.dim(`(${present} already present)`)}`);
@@ -27738,28 +27850,26 @@ program.command("init").description("Set up chat-recall: connect to your server,
27738
27850
  }
27739
27851
  console.log();
27740
27852
  }
27741
- if (!options.skipSync && firstTarget()) {
27742
- const shipped = await confirmFirstSyncScope(!!options.yes);
27743
- if (!shipped) {
27744
- console.log();
27745
- } else {
27746
- console.log(chalk.dim(" Shipping..."));
27747
- try {
27748
- const { syncSessions: syncSessions2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
27749
- const r = await syncSessions2();
27750
- 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)` : ""}`)}`);
27751
- } catch (err) {
27752
- console.log(` ${chalk.yellow("Sync failed")} \u2014 ${err instanceof Error ? err.message : err}`);
27753
- console.log(` ${chalk.dim("Re-run `chat-recall sync` once your server is reachable.")}`);
27754
- }
27853
+ if (deadTarget) {
27854
+ console.log(chalk.bold("5. Skipping first sync \u2014 the saved server is not serving chat-recall"));
27855
+ console.log(` ${chalk.dim(`Re-point with the two commands above, then run ${chalk.bold("chat-recall sync")}.`)}`);
27856
+ } else if (!options.skipSync && firstTarget()) {
27857
+ console.log(chalk.bold("5. Indexing and uploading your sessions"));
27858
+ try {
27859
+ const { syncSessions: syncSessions2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
27860
+ const r = await syncSessions2();
27861
+ 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)` : ""}`)}`);
27862
+ } catch (err) {
27863
+ console.log(` ${chalk.yellow("Sync failed")} \u2014 ${err instanceof Error ? err.message : err}`);
27864
+ console.log(` ${chalk.dim("Re-run `chat-recall sync` once your server is reachable.")}`);
27755
27865
  }
27756
27866
  } else if (!options.skipSync) {
27757
- console.log(chalk.bold("6. Skipping first sync (not logged in)"));
27867
+ console.log(chalk.bold("5. Skipping first sync (not logged in)"));
27758
27868
  } else {
27759
- console.log(chalk.bold("6. Skipping first sync (--skip-sync)"));
27869
+ console.log(chalk.bold("5. Skipping first sync (--skip-sync)"));
27760
27870
  }
27761
27871
  console.log();
27762
- console.log(chalk.bold("7. Background sync"));
27872
+ console.log(chalk.bold("6. Background sync"));
27763
27873
  console.log(chalk.dim(" Runs via the MCP server while Claude Code is open (no extra service to install)."));
27764
27874
  console.log(chalk.dim(" Always-on / headless? Opt in with `chat-recall watch --install-service`."));
27765
27875
  void options.skipService;
@@ -29601,25 +29711,31 @@ async function runLogin(serverUrl, opts) {
29601
29711
  });
29602
29712
  return { ok: res.ok, status: res.status };
29603
29713
  } catch (err) {
29604
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
29714
+ return { ok: false, error: describeUnreachable(err) };
29605
29715
  }
29606
29716
  };
29607
29717
  if (opts.token) {
29608
29718
  const token = opts.token.trim();
29609
29719
  if (/[^\x21-\x7e]/.test(token)) {
29610
- console.error(chalk.red('login failed: the token contains non-ASCII characters \u2014 this looks like a masked/truncated copy (e.g. "ct_12345678\u2026").'));
29611
29720
  console.error(chalk.dim("Use the Copy button next to the token in the dashboard and paste the full line."));
29612
- process.exit(1);
29721
+ return failLogin(opts, 'the token contains non-ASCII characters \u2014 this looks like a masked/truncated copy (e.g. "ct_12345678\u2026")');
29613
29722
  }
29723
+ const { probeServer: identifyServer, probeOk: identified, probeAdvice: identifyAdvice } = await Promise.resolve().then(() => (init_server_probe(), server_probe_exports));
29724
+ const who = await identifyServer(base2, { timeoutMs: 1e4 });
29725
+ if (!identified(who)) return failLogin(opts, identifyAdvice(who, base2));
29614
29726
  const check = await verifyToken(token);
29615
29727
  if (!check.ok) {
29616
29728
  if (check.status === 401 || check.status === 403) {
29617
- console.error(chalk.red(`login failed: ${base2} rejected the token (HTTP ${check.status}).`));
29618
29729
  console.error(chalk.dim("Tokens are shown once \u2014 mint a fresh one (Account \u2192 Connect your machine) and copy the whole line."));
29619
- } else {
29620
- console.error(chalk.red(`login failed: could not verify the token against ${base2}:`), check.error || `HTTP ${check.status}`);
29730
+ return failLogin(opts, `${base2} rejected the token (HTTP ${check.status})`);
29621
29731
  }
29622
- process.exit(1);
29732
+ if (check.status !== void 0 && check.status >= 500) {
29733
+ return failLogin(opts, `${base2} answered HTTP ${check.status} \u2014 the server is up but failing. The token was never checked. Nothing to fix locally.`);
29734
+ }
29735
+ if (check.status === 404 || check.status === 410) {
29736
+ return failLogin(opts, `${base2} does not serve the chat-recall API (HTTP ${check.status}). If the server moved, log in to its new URL.`);
29737
+ }
29738
+ return failLogin(opts, `could not verify the token against ${base2}: ${check.error || `HTTP ${check.status}`}`);
29623
29739
  }
29624
29740
  saveCredentials2({ serverUrl, token });
29625
29741
  console.log(chalk.green("\u2713 Logged in.") + chalk.dim(` server: ${serverUrl} (token verified)`));
@@ -29631,11 +29747,15 @@ async function runLogin(serverUrl, opts) {
29631
29747
  return;
29632
29748
  }
29633
29749
  try {
29634
- const probe = await fetch(`${base2}/api/status`, { signal: AbortSignal.timeout(8e3) });
29635
- if (probe.status === 200) {
29636
- saveCredentials2({ serverUrl, token: "" });
29637
- console.log(chalk.green("\u2713 Logged in.") + chalk.dim(` ${serverUrl} (local server \u2014 no auth, no token needed)`));
29638
- return;
29750
+ const { probeServer: probeServer2, probeOk: probeOk2 } = await Promise.resolve().then(() => (init_server_probe(), server_probe_exports));
29751
+ const identified = await probeServer2(base2, { timeoutMs: 8e3 });
29752
+ if (probeOk2(identified)) {
29753
+ const probe = await fetch(`${base2}/api/status`, { signal: AbortSignal.timeout(8e3) });
29754
+ if (probe.status === 200) {
29755
+ saveCredentials2({ serverUrl, token: "" });
29756
+ console.log(chalk.green("\u2713 Logged in.") + chalk.dim(` ${serverUrl} (local server \u2014 no auth, no token needed)`));
29757
+ return;
29758
+ }
29639
29759
  }
29640
29760
  } catch {
29641
29761
  }
@@ -29649,19 +29769,18 @@ async function runLogin(serverUrl, opts) {
29649
29769
  let authProvider;
29650
29770
  let capsError = null;
29651
29771
  if (!issuer) {
29772
+ const { probeServer: probeServer2, probeOk: probeOk2, probeAdvice: probeAdvice2 } = await Promise.resolve().then(() => (init_server_probe(), server_probe_exports));
29652
29773
  for (let attempt = 1; attempt <= 3 && !authProvider; attempt++) {
29653
- try {
29654
- const res = await fetch(`${base2}/api/capabilities`, { signal: AbortSignal.timeout(15e3) });
29655
- if (!res.ok) {
29656
- capsError = `HTTP ${res.status}`;
29657
- } else {
29658
- const caps = await res.json();
29659
- authProvider = caps?.authProvider;
29660
- if (caps?.oidcIssuer) issuer = caps.oidcIssuer;
29661
- capsError = null;
29662
- }
29663
- } catch (err) {
29664
- capsError = err instanceof Error ? err.message : String(err);
29774
+ const probe = await probeServer2(base2, { timeoutMs: 15e3 });
29775
+ if (probeOk2(probe)) {
29776
+ const caps = probe.raw;
29777
+ authProvider = caps?.authProvider;
29778
+ if (caps?.oidcIssuer) issuer = caps.oidcIssuer;
29779
+ capsError = null;
29780
+ } else if (probe.kind === "moved" || probe.kind === "not_api") {
29781
+ return failLogin(opts, probeAdvice2(probe, base2));
29782
+ } else {
29783
+ capsError = probeAdvice2(probe, base2);
29665
29784
  }
29666
29785
  if (!authProvider && attempt < 3) await new Promise((r) => setTimeout(r, attempt * 1e3));
29667
29786
  }
@@ -29670,6 +29789,12 @@ async function runLogin(serverUrl, opts) {
29670
29789
  authProvider = "better-auth";
29671
29790
  if (capsError) {
29672
29791
  console.log(` ${chalk.yellow("Could not read the server's capabilities")} (${capsError}) \u2014 assuming the standard sign-in flow.`);
29792
+ void Promise.resolve().then(() => (init_install_report(), install_report_exports)).then(({ reportInstallFailure: reportInstallFailure2 }) => reportInstallFailure2(base2, {
29793
+ step: "cli_login_probe",
29794
+ reason: capsError?.slice(0, 40) ?? null,
29795
+ message: capsError
29796
+ })).catch(() => {
29797
+ });
29673
29798
  }
29674
29799
  }
29675
29800
  const onPrompt = (p) => {
@@ -29686,7 +29811,9 @@ async function runLogin(serverUrl, opts) {
29686
29811
  };
29687
29812
  const tokens = authProvider === "better-auth" ? await betterAuthDeviceLogin2(base2, onPrompt) : await deviceLogin2({ issuer, clientId: opts.clientId }, onPrompt);
29688
29813
  const authHdr = { authorization: `Bearer ${tokens.accessToken}`, "content-type": "application/json" };
29689
- const me = await fetch(`${base2}/api/me`, { headers: authHdr }).then((r) => r.json());
29814
+ const meRes = await fetch(`${base2}/api/me`, { headers: authHdr });
29815
+ if (!meRes.ok) throw new Error(`${base2}/api/me: HTTP ${meRes.status} ${await meRes.text().catch(() => "")}`);
29816
+ const me = await readJsonBody(meRes, base2, "GET /api/me");
29690
29817
  const teams = me.teams || [];
29691
29818
  let slug = opts.team;
29692
29819
  if (!slug) {
@@ -29695,12 +29822,11 @@ async function runLogin(serverUrl, opts) {
29695
29822
  const name = me.user?.email?.split("@")[0]?.replace(/[^a-z0-9]/gi, "") || "workspace";
29696
29823
  const created = await fetch(`${base2}/api/teams`, { method: "POST", headers: authHdr, body: JSON.stringify({ name }) });
29697
29824
  if (!created.ok) throw new Error(`workspace create failed: HTTP ${created.status} ${await created.text().catch(() => "")}`);
29698
- slug = (await created.json()).slug;
29825
+ slug = (await readJsonBody(created, base2, "POST /api/teams")).slug;
29699
29826
  console.log(chalk.dim(`Created workspace "${name}" (${slug}) \u2014 you're its owner.`));
29700
29827
  } else {
29701
- console.error(chalk.red("You belong to multiple teams \u2014 pass --team <slug>:"));
29702
29828
  teams.forEach((t) => console.error(` ${t.team_slug} ${chalk.dim(t.name)}`));
29703
- process.exit(1);
29829
+ throw new Error("you belong to multiple teams \u2014 pass --team <slug> (listed above)");
29704
29830
  }
29705
29831
  }
29706
29832
  const deviceId = opts.deviceId || (await import("node:os")).hostname();
@@ -29710,13 +29836,14 @@ async function runLogin(serverUrl, opts) {
29710
29836
  body: JSON.stringify({ device_id: deviceId })
29711
29837
  });
29712
29838
  if (!mint.ok) throw new Error(`token mint failed: HTTP ${mint.status} ${await mint.text().catch(() => "")}`);
29713
- const { token } = await mint.json();
29839
+ const { token } = await readJsonBody(mint, base2, "POST /api/teams/:slug/tokens");
29714
29840
  saveCredentials2({ serverUrl, token });
29715
29841
  console.log(chalk.green(`\u2713 Logged in to ${chalk.bold(slug)}`) + chalk.dim(` (device: ${deviceId}) server: ${serverUrl}`));
29716
29842
  if (!opts.fromInit) {
29717
29843
  console.log(chalk.dim("Run `chat-recall sync` to push redacted conversations."));
29718
29844
  }
29719
29845
  } catch (err) {
29846
+ if (opts.fromInit) throw err instanceof Error ? err : new Error(String(err));
29720
29847
  console.error(chalk.red("login failed:"), err instanceof Error ? err.message : err);
29721
29848
  process.exit(1);
29722
29849
  }