chat-recall 0.5.22 → 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.22";
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.22" : "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.22";
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;
@@ -23502,7 +23654,7 @@ __export(toolkit_pull_exports, {
23502
23654
  portableCommand: () => portableCommand
23503
23655
  });
23504
23656
  import { existsSync as existsSync48, mkdirSync as mkdirSync14, statSync as statSync42, writeFileSync as writeFileSync15 } from "node:fs";
23505
- 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";
23506
23658
  function isExecutableFile(p) {
23507
23659
  try {
23508
23660
  return statSync42(p).isFile();
@@ -23511,13 +23663,7 @@ function isExecutableFile(p) {
23511
23663
  }
23512
23664
  }
23513
23665
  function onPath(name) {
23514
- const dirs = (process.env.PATH || "").split(delimiter3).filter(Boolean);
23515
- for (const d of dirs) {
23516
- const candidate = join52(d, name);
23517
- if (isExecutableFile(candidate))
23518
- return candidate;
23519
- }
23520
- return null;
23666
+ return resolveOnPath(name);
23521
23667
  }
23522
23668
  function portableCommand(cmd) {
23523
23669
  if (!isAbsolute3(cmd)) {
@@ -23779,6 +23925,7 @@ var NOT_REBUILDABLE;
23779
23925
  var init_toolkit_pull = __esm({
23780
23926
  "../engine/dist/core/toolkit-pull.js"() {
23781
23927
  "use strict";
23928
+ init_which();
23782
23929
  init_toolkit_sync();
23783
23930
  init_artifact_codec();
23784
23931
  NOT_REBUILDABLE = {
@@ -24145,117 +24292,6 @@ var init_escalate = __esm({
24145
24292
  }
24146
24293
  });
24147
24294
 
24148
- // src/server-probe.ts
24149
- var server_probe_exports = {};
24150
- __export(server_probe_exports, {
24151
- classifyUnreachable: () => classifyUnreachable,
24152
- describeCause: () => describeCause,
24153
- probeAdvice: () => probeAdvice,
24154
- probeOk: () => probeOk,
24155
- probeServer: () => probeServer
24156
- });
24157
- function probeOk(r) {
24158
- return r.kind === "ok" && r.apiVersion >= 2;
24159
- }
24160
- async function probeServer(base2, opts = {}) {
24161
- const f = opts.fetchImpl || fetch;
24162
- let res;
24163
- try {
24164
- res = await f(`${base2.replace(/\/+$/, "")}/api/capabilities`, {
24165
- signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
24166
- });
24167
- } catch (err) {
24168
- return {
24169
- kind: "unreachable",
24170
- error: describeCause(err),
24171
- reason: classifyUnreachable(err)
24172
- };
24173
- }
24174
- if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
24175
- if (res.status === 404 || res.status === 410) return { kind: "moved", status: res.status };
24176
- if (res.status >= 500) return { kind: "server_error", status: res.status };
24177
- if (!res.ok) return { kind: "moved", status: res.status };
24178
- let body;
24179
- try {
24180
- body = await res.text();
24181
- } catch (err) {
24182
- return { kind: "unreachable", error: describeCause(err), reason: classifyUnreachable(err) };
24183
- }
24184
- let parsed;
24185
- try {
24186
- parsed = JSON.parse(body);
24187
- } catch {
24188
- return { kind: "not_api", status: res.status, snippet: body.trim().slice(0, 60) };
24189
- }
24190
- if (typeof parsed?.apiVersion !== "number") {
24191
- return { kind: "not_api", status: res.status, snippet: body.trim().slice(0, 60) };
24192
- }
24193
- return {
24194
- kind: "ok",
24195
- apiVersion: parsed.apiVersion,
24196
- edition: parsed.edition,
24197
- cliVersion: parsed.cli?.version
24198
- };
24199
- }
24200
- function probeAdvice(r, base2) {
24201
- switch (r.kind) {
24202
- case "ok":
24203
- 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`;
24204
- case "moved":
24205
- return `HTTP ${r.status} \u2014 nothing is served at ${base2}. If the host moved, re-point it: \`chat-recall login <new-server-url>\``;
24206
- case "unauthorized":
24207
- return `HTTP ${r.status} \u2014 ${base2} rejected the credential. Re-login: \`chat-recall login ` + base2 + "`";
24208
- case "server_error":
24209
- return `HTTP ${r.status} \u2014 ${base2} is up but failing. Nothing to fix locally; check the server.`;
24210
- case "not_api":
24211
- 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`;
24212
- case "unreachable":
24213
- switch (r.reason) {
24214
- case "dns":
24215
- return `cannot resolve the host in ${base2} (${r.error}) \u2014 check the URL for a typo, and your DNS or VPN`;
24216
- case "refused":
24217
- return `${base2} resolves but refused the connection (${r.error}) \u2014 the server is not running, or the port is wrong`;
24218
- case "tls":
24219
- 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.`;
24220
- case "timeout":
24221
- return `${base2} did not answer in time (${r.error}) \u2014 a firewall or proxy that drops traffic silently looks exactly like this`;
24222
- case "reset":
24223
- return `the connection to ${base2} was cut (${r.error}) \u2014 often a proxy or a captive portal`;
24224
- default:
24225
- return `no HTTP response from ${base2} (${r.error}) \u2014 check the URL, DNS, or that the server is running`;
24226
- }
24227
- }
24228
- }
24229
- function classifyUnreachable(err) {
24230
- if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) return "timeout";
24231
- for (let e = err, depth = 0; e && depth < 5; depth++) {
24232
- const code2 = String(e.code ?? "");
24233
- const msg = String(e.message ?? "");
24234
- if (code2 === "ENOTFOUND" || code2 === "EAI_AGAIN") return "dns";
24235
- if (code2 === "ECONNREFUSED") return "refused";
24236
- if (code2 === "ECONNRESET" || code2 === "EPIPE") return "reset";
24237
- if (code2 === "ETIMEDOUT" || code2 === "UND_ERR_CONNECT_TIMEOUT" || code2 === "UND_ERR_HEADERS_TIMEOUT") return "timeout";
24238
- 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";
24239
- e = e.cause;
24240
- }
24241
- return "unknown";
24242
- }
24243
- function describeCause(err) {
24244
- const parts = [];
24245
- for (let e = err, depth = 0; e && depth < 5; depth++) {
24246
- const code2 = e.code;
24247
- if (typeof code2 === "string" && code2 && !parts.includes(code2)) parts.push(code2);
24248
- e = e.cause;
24249
- }
24250
- const base2 = err instanceof Error ? err.message : String(err);
24251
- return parts.length ? `${base2}: ${parts.join(" \u2192 ")}` : base2;
24252
- }
24253
- var init_server_probe = __esm({
24254
- "src/server-probe.ts"() {
24255
- "use strict";
24256
- }
24257
- });
24258
-
24259
24295
  // src/verify-deep.ts
24260
24296
  var verify_deep_exports = {};
24261
24297
  __export(verify_deep_exports, {
@@ -25836,7 +25872,7 @@ var init_intent_drain = __esm({
25836
25872
  ownVersion2 = JSON.parse(readFileSync46(join55(dir, "../package.json"), "utf-8")).version || ownVersion2;
25837
25873
  } catch {
25838
25874
  try {
25839
- if (true) ownVersion2 = "0.5.22";
25875
+ if (true) ownVersion2 = "0.5.23";
25840
25876
  } catch {
25841
25877
  }
25842
25878
  }
@@ -27111,6 +27147,24 @@ __export(device_auth_exports, {
27111
27147
  deviceLogin: () => deviceLogin
27112
27148
  });
27113
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
+ }
27114
27168
  async function deviceLogin(opts, onPrompt) {
27115
27169
  const issuer = (opts.issuer || DEFAULT_ISSUER).replace(/\/+$/, "");
27116
27170
  if (!issuer) {
@@ -27130,7 +27184,7 @@ async function deviceLogin(opts, onPrompt) {
27130
27184
  if (!startRes.ok) {
27131
27185
  throw new Error(`device-auth init failed: HTTP ${startRes.status} ${await startRes.text().catch(() => "")}`);
27132
27186
  }
27133
- const start = await startRes.json();
27187
+ const start = await readDeviceCode(startRes, issuer);
27134
27188
  onPrompt({
27135
27189
  url: start.verification_uri_complete || start.verification_uri,
27136
27190
  userCode: start.user_code,
@@ -27184,7 +27238,7 @@ async function betterAuthDeviceLogin(serverUrl, onPrompt) {
27184
27238
  if (!startRes.ok) {
27185
27239
  throw new Error(`device-auth init failed: HTTP ${startRes.status} ${await startRes.text().catch(() => "")}`);
27186
27240
  }
27187
- const start = await startRes.json();
27241
+ const start = await readDeviceCode(startRes, base2);
27188
27242
  onPrompt({
27189
27243
  url: start.verification_uri_complete || start.verification_uri,
27190
27244
  userCode: start.user_code,
@@ -27355,6 +27409,7 @@ function tierAll(results) {
27355
27409
  // src/cli.ts
27356
27410
  init_sync_client();
27357
27411
  init_update_notice();
27412
+ init_server_probe();
27358
27413
  init_which();
27359
27414
  init_collector_health();
27360
27415
  init_telemetry_consent();
@@ -27396,14 +27451,9 @@ function firstTarget() {
27396
27451
  return { base: cred.serverUrl.replace(/\/+$/, ""), token: cred.token };
27397
27452
  }
27398
27453
  function describeProbeError(err) {
27399
- const codes = [];
27400
- for (let e = err, depth = 0; e && depth < 5; depth++) {
27401
- const code2 = e.code;
27402
- if (typeof code2 === "string" && code2 && !codes.includes(code2)) codes.push(code2);
27403
- e = e.cause;
27404
- }
27405
- const base2 = err instanceof Error ? err.message : String(err);
27406
- return codes.length ? `${base2}: ${codes.join(" \u2192 ")}` : base2;
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);
27407
27457
  }
27408
27458
  function failLogin(opts, message) {
27409
27459
  if (opts.fromInit) throw new Error(message);
@@ -27418,11 +27468,21 @@ function requireTarget() {
27418
27468
  }
27419
27469
  return t;
27420
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
+ }
27421
27481
  async function serverGet(path) {
27422
27482
  const t = requireTarget();
27423
27483
  const res = await fetch(t.base + path, { headers: t.token ? { authorization: `Bearer ${t.token}` } : {} });
27424
27484
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27425
- return res.json();
27485
+ return readJsonBody(res, t.base, `GET ${path}`);
27426
27486
  }
27427
27487
  async function serverGetSoft(path) {
27428
27488
  const t = requireTarget();
@@ -27432,7 +27492,7 @@ async function serverGetSoft(path) {
27432
27492
  return { status: res.status, data: null, message: body.message };
27433
27493
  }
27434
27494
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27435
- return { status: res.status, data: await res.json() };
27495
+ return { status: res.status, data: await readJsonBody(res, t.base, `GET ${path}`) };
27436
27496
  }
27437
27497
  async function serverPost(path, body) {
27438
27498
  const t = requireTarget();
@@ -27442,7 +27502,7 @@ async function serverPost(path, body) {
27442
27502
  body: JSON.stringify(body)
27443
27503
  });
27444
27504
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27445
- return res.json();
27505
+ return readJsonBody(res, t.base, `POST ${path}`);
27446
27506
  }
27447
27507
  async function serverPatch(path, body) {
27448
27508
  const t = requireTarget();
@@ -27452,7 +27512,7 @@ async function serverPatch(path, body) {
27452
27512
  body: JSON.stringify(body)
27453
27513
  });
27454
27514
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27455
- return res.json();
27515
+ return readJsonBody(res, t.base, `PATCH ${path}`);
27456
27516
  }
27457
27517
  async function serverDelete(path, body) {
27458
27518
  const t = requireTarget();
@@ -27462,7 +27522,7 @@ async function serverDelete(path, body) {
27462
27522
  body: JSON.stringify(body)
27463
27523
  });
27464
27524
  if (!res.ok) throw new Error(`server ${path}: HTTP ${res.status} ${await res.text().catch(() => "")}`);
27465
- return res.json();
27525
+ return readJsonBody(res, t.base, `DELETE ${path}`);
27466
27526
  }
27467
27527
  var pkgEngines = (() => {
27468
27528
  try {
@@ -27474,7 +27534,7 @@ var pkgEngines = (() => {
27474
27534
  var pkgVersion = JSON.parse(
27475
27535
  readFileSync49(new URL("../package.json", import.meta.url), "utf-8")
27476
27536
  ).version;
27477
- var DEFAULT_SERVER = "https://chatrecall.dev";
27537
+ var DEFAULT_SERVER = (process.env.CHAT_RECALL_DEFAULT_SERVER || "https://chatrecall.dev").replace(/\/+$/, "");
27478
27538
  var DEFAULT_ALLOW = [
27479
27539
  "recall_search",
27480
27540
  "recall_show",
@@ -27586,6 +27646,7 @@ program.command("init").description("Set up chat-recall: connect to your server,
27586
27646
  console.log();
27587
27647
  console.log(chalk.bold("2. Connecting to your server..."));
27588
27648
  let target = firstTarget();
27649
+ let deadTarget = false;
27589
27650
  if (options.server) {
27590
27651
  try {
27591
27652
  await runLogin(options.server, { token: options.token, fromInit: true, promptJson: options.promptJson });
@@ -27605,7 +27666,34 @@ program.command("init").description("Set up chat-recall: connect to your server,
27605
27666
  console.log(` ${chalk.dim(`Run ${chalk.bold("chat-recall login <server-url>")} when ready, or self-host: ${SELF_HOST_DOCS}`)}`);
27606
27667
  }
27607
27668
  }
27608
- 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) {
27609
27697
  console.log(` ${chalk.green("Connected")} \u2192 ${target.base}`);
27610
27698
  }
27611
27699
  console.log();
@@ -27714,22 +27802,40 @@ program.command("init").description("Set up chat-recall: connect to your server,
27714
27802
  const { executePull: executePull2 } = await Promise.resolve().then(() => (init_toolkit_pull(), toolkit_pull_exports));
27715
27803
  const { hostname: hostname3 } = await import("node:os");
27716
27804
  const rows = [];
27805
+ let answered = 0;
27806
+ const askFailed = [];
27717
27807
  for (const t of loadAllCredentials2()) {
27718
27808
  try {
27719
27809
  const res = await fetch(`${t.serverUrl}/api/toolkit/browse/mcp?limit=1000`, {
27720
27810
  headers: t.token ? { authorization: `Bearer ${t.token}` } : {},
27721
27811
  signal: AbortSignal.timeout(2e4)
27722
27812
  });
27723
- if (!res.ok) continue;
27724
- 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++;
27725
27826
  for (const it of body.items || []) rows.push(it);
27726
- } catch {
27827
+ } catch (e) {
27828
+ askFailed.push(`${t.serverUrl} (${describeProbeError(e)})`);
27727
27829
  }
27728
27830
  }
27729
27831
  const report = executePull2(rows, { thisDeviceId: hostname3(), types: ["mcp"] });
27730
27832
  const written = report.outcomes.filter((o) => o.status === "written");
27731
27833
  const present = report.outcomes.filter((o) => o.status === "present").length;
27732
- 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) {
27733
27839
  console.log(chalk.dim(" Nothing on your account to install yet."));
27734
27840
  } else {
27735
27841
  console.log(` ${chalk.green(`Installed ${written.length}`)} ${chalk.dim(`(${present} already present)`)}`);
@@ -27744,7 +27850,10 @@ program.command("init").description("Set up chat-recall: connect to your server,
27744
27850
  }
27745
27851
  console.log();
27746
27852
  }
27747
- if (!options.skipSync && firstTarget()) {
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()) {
27748
27857
  console.log(chalk.bold("5. Indexing and uploading your sessions"));
27749
27858
  try {
27750
27859
  const { syncSessions: syncSessions2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
@@ -29602,7 +29711,7 @@ async function runLogin(serverUrl, opts) {
29602
29711
  });
29603
29712
  return { ok: res.ok, status: res.status };
29604
29713
  } catch (err) {
29605
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
29714
+ return { ok: false, error: describeUnreachable(err) };
29606
29715
  }
29607
29716
  };
29608
29717
  if (opts.token) {
@@ -29611,12 +29720,21 @@ async function runLogin(serverUrl, opts) {
29611
29720
  console.error(chalk.dim("Use the Copy button next to the token in the dashboard and paste the full line."));
29612
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
29729
  console.error(chalk.dim("Tokens are shown once \u2014 mint a fresh one (Account \u2192 Connect your machine) and copy the whole line."));
29618
29730
  return failLogin(opts, `${base2} rejected the token (HTTP ${check.status})`);
29619
29731
  }
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
+ }
29620
29738
  return failLogin(opts, `could not verify the token against ${base2}: ${check.error || `HTTP ${check.status}`}`);
29621
29739
  }
29622
29740
  saveCredentials2({ serverUrl, token });
@@ -29629,11 +29747,15 @@ async function runLogin(serverUrl, opts) {
29629
29747
  return;
29630
29748
  }
29631
29749
  try {
29632
- const probe = await fetch(`${base2}/api/status`, { signal: AbortSignal.timeout(8e3) });
29633
- if (probe.status === 200) {
29634
- saveCredentials2({ serverUrl, token: "" });
29635
- console.log(chalk.green("\u2713 Logged in.") + chalk.dim(` ${serverUrl} (local server \u2014 no auth, no token needed)`));
29636
- 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
+ }
29637
29759
  }
29638
29760
  } catch {
29639
29761
  }
@@ -29651,14 +29773,12 @@ async function runLogin(serverUrl, opts) {
29651
29773
  for (let attempt = 1; attempt <= 3 && !authProvider; attempt++) {
29652
29774
  const probe = await probeServer2(base2, { timeoutMs: 15e3 });
29653
29775
  if (probeOk2(probe)) {
29654
- try {
29655
- const caps = await fetch(`${base2}/api/capabilities`, { signal: AbortSignal.timeout(15e3) }).then((r) => r.json());
29656
- authProvider = caps?.authProvider;
29657
- if (caps?.oidcIssuer) issuer = caps.oidcIssuer;
29658
- capsError = null;
29659
- } catch (err) {
29660
- capsError = describeProbeError(err);
29661
- }
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));
29662
29782
  } else {
29663
29783
  capsError = probeAdvice2(probe, base2);
29664
29784
  }
@@ -29691,7 +29811,9 @@ async function runLogin(serverUrl, opts) {
29691
29811
  };
29692
29812
  const tokens = authProvider === "better-auth" ? await betterAuthDeviceLogin2(base2, onPrompt) : await deviceLogin2({ issuer, clientId: opts.clientId }, onPrompt);
29693
29813
  const authHdr = { authorization: `Bearer ${tokens.accessToken}`, "content-type": "application/json" };
29694
- 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");
29695
29817
  const teams = me.teams || [];
29696
29818
  let slug = opts.team;
29697
29819
  if (!slug) {
@@ -29700,7 +29822,7 @@ async function runLogin(serverUrl, opts) {
29700
29822
  const name = me.user?.email?.split("@")[0]?.replace(/[^a-z0-9]/gi, "") || "workspace";
29701
29823
  const created = await fetch(`${base2}/api/teams`, { method: "POST", headers: authHdr, body: JSON.stringify({ name }) });
29702
29824
  if (!created.ok) throw new Error(`workspace create failed: HTTP ${created.status} ${await created.text().catch(() => "")}`);
29703
- slug = (await created.json()).slug;
29825
+ slug = (await readJsonBody(created, base2, "POST /api/teams")).slug;
29704
29826
  console.log(chalk.dim(`Created workspace "${name}" (${slug}) \u2014 you're its owner.`));
29705
29827
  } else {
29706
29828
  teams.forEach((t) => console.error(` ${t.team_slug} ${chalk.dim(t.name)}`));
@@ -29714,7 +29836,7 @@ async function runLogin(serverUrl, opts) {
29714
29836
  body: JSON.stringify({ device_id: deviceId })
29715
29837
  });
29716
29838
  if (!mint.ok) throw new Error(`token mint failed: HTTP ${mint.status} ${await mint.text().catch(() => "")}`);
29717
- const { token } = await mint.json();
29839
+ const { token } = await readJsonBody(mint, base2, "POST /api/teams/:slug/tokens");
29718
29840
  saveCredentials2({ serverUrl, token });
29719
29841
  console.log(chalk.green(`\u2713 Logged in to ${chalk.bold(slug)}`) + chalk.dim(` (device: ${deviceId}) server: ${serverUrl}`));
29720
29842
  if (!opts.fromInit) {