free-pi-cli 0.6.19 → 0.6.21

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.
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/main.ts
4
4
  import { spawn as spawn4 } from "node:child_process";
5
- import { realpathSync as realpathSync8, writeFileSync as writeFileSync6 } from "node:fs";
5
+ import { realpathSync as realpathSync8, writeFileSync as writeFileSync7 } from "node:fs";
6
6
  import { homedir as homedir10 } from "node:os";
7
- import { dirname as dirname14, join as join15 } from "node:path";
7
+ import { dirname as dirname15, join as join16 } from "node:path";
8
8
 
9
9
  // src/browser.ts
10
10
  import { spawn } from "node:child_process";
@@ -515,7 +515,7 @@ function folderMatches(cwd, sessionCwd) {
515
515
  // package.json
516
516
  var package_default = {
517
517
  name: "free-pi-cli",
518
- version: "0.6.19",
518
+ version: "0.6.21",
519
519
  description: "Free AI coding agent for your terminal. Ads and a daily free allowance pay for the models; sign in with GitHub and run npx free-pi-cli.",
520
520
  keywords: [
521
521
  "coding agent",
@@ -554,6 +554,7 @@ var package_default = {
554
554
  prepublishOnly: "bun run build"
555
555
  },
556
556
  dependencies: {
557
+ "@earendil-works/pi-ai": "1.2.0",
557
558
  "@earendil-works/pi-coding-agent": "1.2.0",
558
559
  "@earendil-works/pi-tui": "1.2.0",
559
560
  "@gravity-ai/api": "1.2.1"
@@ -569,7 +570,7 @@ var package_default = {
569
570
  var CLI_VERSION = package_default.version;
570
571
 
571
572
  // src/output-limit.ts
572
- var MAX_OUTPUT_TOKENS = 8192;
573
+ var MAX_OUTPUT_TOKENS = 32768;
573
574
  var OUTPUT_LIMIT_MESSAGE = "This tool call was cut off by the output limit before it finished. It either did not run or ran with incomplete content, " + "so a file may be partly written. Check what was written, then redo the work in smaller parts " + "(for example write the first part, then add the rest with edit).";
574
575
  function createOutputLimitExtension() {
575
576
  let truncated = new Set;
@@ -6020,6 +6021,8 @@ var AdNextResponseSchema = object({
6020
6021
  });
6021
6022
  var GRAVITY_CONTEXT_HEADER = "x-gravity-context";
6022
6023
  var GRAVITY_CONTEXT_MAX_BYTES = 8192;
6024
+ var AD_CONTEXT_HEADER = "x-ad-context";
6025
+ var AD_CONTEXT_MAX_CHARS = 1000;
6023
6026
  var AdImpressionRequestSchema = object({
6024
6027
  ad_id: string2(),
6025
6028
  click_token: string2(),
@@ -6583,14 +6586,15 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6583
6586
  function piMcpCliPath() {
6584
6587
  return join4(dirname5(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "extensions", "mcp", "cli.js");
6585
6588
  }
6589
+ var MCP_UNAVAILABLE = "free-pi-cli mcp isn't available in this install. Reinstall with npx free-pi-cli@latest.";
6586
6590
  async function loadPiRunner() {
6587
6591
  const path = piMcpCliPath();
6588
6592
  if (!existsSync2(path)) {
6589
- throw new Error(`free-pi-cli mcp is unavailable: the bundled pi package has no ${path}. Reinstall free-pi-cli.`);
6593
+ throw new Error(MCP_UNAVAILABLE);
6590
6594
  }
6591
6595
  const mod = await import(pathToFileURL(path).href);
6592
6596
  if (typeof mod.runMcpCommand !== "function") {
6593
- throw new Error(`free-pi-cli mcp is unavailable: ${path} does not export runMcpCommand. Reinstall free-pi-cli.`);
6597
+ throw new Error(MCP_UNAVAILABLE);
6594
6598
  }
6595
6599
  return mod.runMcpCommand;
6596
6600
  }
@@ -6608,7 +6612,9 @@ async function runFreePiMcp(args, deps) {
6608
6612
  error: (line) => deps.error(brandMcpLine(line))
6609
6613
  });
6610
6614
  } catch (err) {
6611
- deps.error(err instanceof Error ? err.message : String(err));
6615
+ const msg = (err instanceof Error ? err.message : String(err)).split(`
6616
+ at `)[0];
6617
+ deps.error(msg === MCP_UNAVAILABLE ? msg : `free-pi-cli mcp failed: ${msg}`);
6612
6618
  return 1;
6613
6619
  }
6614
6620
  }
@@ -6763,6 +6769,52 @@ function gravityHeader(deps) {
6763
6769
  return {};
6764
6770
  }
6765
6771
  }
6772
+ function textParts(content) {
6773
+ if (typeof content === "string")
6774
+ return content;
6775
+ if (!Array.isArray(content))
6776
+ return "";
6777
+ return content.map((p) => p && p.type === "text" && typeof p.text === "string" ? p.text : "").filter(Boolean).join(`
6778
+ `);
6779
+ }
6780
+ function cut(redact, text) {
6781
+ const full = redact(text.slice(0, 20000));
6782
+ const s = full.slice(0, AD_CONTEXT_MAX_CHARS);
6783
+ return full.length > s.length && /\S/.test(full[s.length]) ? s.replace(/\S+$/, "").trimEnd() : s;
6784
+ }
6785
+ function adContextHeader(deps, ctx, prompt) {
6786
+ if (!deps.adContext || !ctx)
6787
+ return;
6788
+ try {
6789
+ const out = {};
6790
+ const entries = ctx.sessionManager.getBranch();
6791
+ if (prompt?.trim())
6792
+ out.user = cut(deps.adContext.redact, prompt.trim());
6793
+ for (let i = entries.length - 1;i >= 0 && !(out.user && out.assistant); i--) {
6794
+ const m = entries[i].type === "message" ? entries[i].message : undefined;
6795
+ if (m?.role !== "user" && m?.role !== "assistant" || out[m.role])
6796
+ continue;
6797
+ const text = textParts(m.content).trim();
6798
+ if (text)
6799
+ out[m.role] = cut(deps.adContext.redact, text);
6800
+ }
6801
+ if (!out.user && !out.assistant)
6802
+ return;
6803
+ const limit = Math.min(4096, 16 * 1024 - 2500 - (deps.getGravityContext?.()?.length ?? 0));
6804
+ if (limit < 600)
6805
+ return;
6806
+ let encoded = Buffer.from(JSON.stringify(out)).toString("base64url");
6807
+ while (encoded.length > limit && (out.user || out.assistant)) {
6808
+ for (const k of ["user", "assistant"])
6809
+ if (out[k])
6810
+ out[k] = out[k].slice(0, Math.floor(out[k].length / 2));
6811
+ encoded = Buffer.from(JSON.stringify(out)).toString("base64url");
6812
+ }
6813
+ return encoded;
6814
+ } catch {
6815
+ return;
6816
+ }
6817
+ }
6766
6818
  var IMP_TIMEOUT_MS = 3000;
6767
6819
  function isGravityImpUrl(raw) {
6768
6820
  if (!raw)
@@ -6782,9 +6834,11 @@ function fireGravityImpression(deps, ad) {
6782
6834
  } catch {}
6783
6835
  return true;
6784
6836
  }
6785
- async function fetchAdNext(deps, slot, trigger) {
6837
+ async function fetchAdNext(deps, slot, trigger, ctx, prompt) {
6786
6838
  try {
6787
- const res = await authedFetch(deps, `/ads/next?slot=${slot}`, trigger ? { headers: { "x-ad-trigger": trigger } } : undefined);
6839
+ const pair = adContextHeader(deps, ctx, prompt);
6840
+ const headers = { ...trigger ? { "x-ad-trigger": trigger } : {}, ...pair ? { [AD_CONTEXT_HEADER]: pair } : {} };
6841
+ const res = await authedFetch(deps, `/ads/next?slot=${slot}`, { headers });
6788
6842
  if (res.status === 204 || !res.ok)
6789
6843
  return;
6790
6844
  return await res.json();
@@ -6884,8 +6938,8 @@ function createBannerRenderer(opts = {}) {
6884
6938
  let lastPostedToken;
6885
6939
  const screen = opts.screen ?? {};
6886
6940
  return {
6887
- async render(deps, ctx, config, trigger) {
6888
- const ad = await fetchAdNext(deps, "banner", trigger);
6941
+ async render(deps, ctx, config, trigger, prompt) {
6942
+ const ad = await fetchAdNext(deps, "banner", trigger, ctx, prompt);
6889
6943
  if (opts.isActive && !opts.isActive())
6890
6944
  return;
6891
6945
  if (!ad) {
@@ -6943,7 +6997,7 @@ function createInlineRenderer(screen = {}) {
6943
6997
  return;
6944
6998
  if (screen.banner === "gravity")
6945
6999
  return;
6946
- const ad = await fetchAdNext(deps, "inline");
7000
+ const ad = await fetchAdNext(deps, "inline", undefined, ctx);
6947
7001
  if (!ad)
6948
7002
  return;
6949
7003
  const isGravity = ad.source === "gravity";
@@ -7161,6 +7215,7 @@ function createAdsExtension(opts) {
7161
7215
  clientVersion: opts.clientVersion,
7162
7216
  fetchImpl: opts.fetchImpl,
7163
7217
  getGravityContext: opts.getGravityContext,
7218
+ adContext: opts.adContext,
7164
7219
  onMe: opts.onMe
7165
7220
  };
7166
7221
  const config = loadConfig();
@@ -7176,14 +7231,14 @@ function createAdsExtension(opts) {
7176
7231
  let launchAt = 0;
7177
7232
  let lastBannerAt = 0;
7178
7233
  let bannerInFlight = false;
7179
- const renderBanner = async (ctx, trigger, force = false) => {
7234
+ const renderBanner = async (ctx, trigger, force = false, prompt) => {
7180
7235
  if (stopped)
7181
7236
  return;
7182
7237
  if (!force && (bannerInFlight || Date.now() - lastBannerAt < BANNER_MIN_REFRESH_MS))
7183
7238
  return;
7184
7239
  bannerInFlight = true;
7185
7240
  try {
7186
- await banner.render(deps, ctx, config, trigger);
7241
+ await banner.render(deps, ctx, config, trigger, prompt);
7187
7242
  } finally {
7188
7243
  bannerInFlight = false;
7189
7244
  lastBannerAt = Date.now();
@@ -7207,7 +7262,7 @@ function createAdsExtension(opts) {
7207
7262
  await Promise.all([renderBanner(ctx, "launch", true), renderMeter(deps, ctx)]);
7208
7263
  });
7209
7264
  const LAUNCH_AD_MAX_AGE_MS = 60 * 60000;
7210
- const newMessage = async (ctx) => {
7265
+ const newMessage = async (ctx, prompt) => {
7211
7266
  if (launchTurnUnused) {
7212
7267
  launchTurnUnused = false;
7213
7268
  const token = await Promise.resolve(deps.getToken()).catch(() => {
@@ -7217,17 +7272,17 @@ function createAdsExtension(opts) {
7217
7272
  return;
7218
7273
  }
7219
7274
  startTurn();
7220
- renderBanner(ctx, "user_message", true);
7275
+ renderBanner(ctx, "user_message", true, prompt);
7221
7276
  };
7222
7277
  pi.on("before_agent_start", async (event, ctx) => {
7223
7278
  if (event.messageType !== "user")
7224
7279
  return;
7225
- await newMessage(ctx);
7280
+ await newMessage(ctx, event.prompt);
7226
7281
  });
7227
7282
  pi.on("input", (event, ctx) => {
7228
7283
  const e = event;
7229
7284
  if (e.streamingBehavior && e.source !== "extension")
7230
- return newMessage(ctx).then(() => ({ action: "continue" }));
7285
+ return newMessage(ctx, e.text).then(() => ({ action: "continue" }));
7231
7286
  return { action: "continue" };
7232
7287
  });
7233
7288
  pi.on("turn_end", async (_event, ctx) => {
@@ -7243,7 +7298,7 @@ function createAdsExtension(opts) {
7243
7298
  }
7244
7299
 
7245
7300
  // src/pi-launch.ts
7246
- import { dirname as dirname11, join as join11, resolve as resolve4 } from "node:path";
7301
+ import { dirname as dirname12, join as join12, resolve as resolve4 } from "node:path";
7247
7302
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7248
7303
 
7249
7304
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
@@ -7573,6 +7628,28 @@ var integerKeyPattern = new RegExp("^(?:0|[1-9][0-9]*)$");
7573
7628
 
7574
7629
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/indexed/from_object.mjs
7575
7630
  var NumericKeyPattern = new RegExp(IntegerKey);
7631
+ // src/friendly-error.ts
7632
+ function friendlyThrown(e) {
7633
+ const name = e instanceof Error ? e.name : "";
7634
+ if (name === "TimeoutError")
7635
+ return "free-pi took too long to answer. Try again.";
7636
+ if (name === "AbortError")
7637
+ return "Cancelled.";
7638
+ if (name === "SyntaxError")
7639
+ return "free-pi sent an unexpected reply. Try again.";
7640
+ return "Couldn't reach free-pi — check your connection.";
7641
+ }
7642
+ function friendlyStatus(status, audience = "user", message) {
7643
+ if (status === 401) {
7644
+ return audience === "model" ? "The user's free-pi sign-in has expired; tell them to run /login." : "Your free-pi sign-in has expired. Run /login.";
7645
+ }
7646
+ if (message?.trim())
7647
+ return message.trim();
7648
+ if (status >= 500)
7649
+ return `free-pi is having trouble right now (HTTP ${status}). Try again in a minute.`;
7650
+ return `free-pi could not complete the request (HTTP ${status}).`;
7651
+ }
7652
+
7576
7653
  // src/usage-tool.ts
7577
7654
  var USAGE_TOOL_NAME = "free_pi_usage";
7578
7655
  var PARAMS = _Object_({});
@@ -7630,11 +7707,11 @@ async function fetchUsageText(opts) {
7630
7707
  signal: AbortSignal.timeout(5000)
7631
7708
  });
7632
7709
  if (!res.ok)
7633
- return { ok: false, text: `Could not fetch usage (HTTP ${res.status}).` };
7710
+ return { ok: false, text: friendlyStatus(res.status) };
7634
7711
  const stats = await res.json();
7635
7712
  return { ok: true, text: formatStats(stats), stats };
7636
7713
  } catch (err) {
7637
- return { ok: false, text: `Could not fetch usage: ${String(err)}` };
7714
+ return { ok: false, text: friendlyThrown(err) };
7638
7715
  }
7639
7716
  }
7640
7717
  function createUsageToolExtension(opts) {
@@ -7702,11 +7779,9 @@ async function post(opts, body, signal) {
7702
7779
  const mapped = ERROR_TEXT[code];
7703
7780
  if (mapped)
7704
7781
  return { ok: false, text: mapped };
7705
- return { ok: false, text: `Request failed (HTTP ${res.status}): ${j.error?.message ?? j.message ?? "no message"}` };
7782
+ return { ok: false, text: friendlyStatus(res.status, "model", j.error?.message ?? j.message) };
7706
7783
  } catch (e) {
7707
- if (e instanceof Error && e.name === "TimeoutError")
7708
- return { ok: false, text: "Image generation timed out after 120 seconds." };
7709
- return { ok: false, text: `Request failed: ${String(e)}` };
7784
+ return { ok: false, text: friendlyThrown(e) };
7710
7785
  }
7711
7786
  }
7712
7787
  function slug(s) {
@@ -7783,7 +7858,8 @@ function createImageToolExtension(opts) {
7783
7858
  await fh.close();
7784
7859
  }
7785
7860
  } catch (e) {
7786
- return fail(`Could not save the image: ${String(e)}`);
7861
+ const why = { EACCES: "permission denied", EPERM: "permission denied", ENOSPC: "the disk is full", ENOENT: "the folder does not exist" }[e.code ?? ""] ?? "it could not be written";
7862
+ return fail(`Couldn't save the image to images: ${why}.`);
7787
7863
  }
7788
7864
  return done(`Saved image to ${rel} (${ext}, ${buf.length} bytes). You cannot see the image; reference this path in your answer or code, and tell the user you could not check how it looks.`, { ok: true, path: rel, cost_usd: d.cost_usd, model: d.model });
7789
7865
  }
@@ -8392,11 +8468,9 @@ async function post2(opts, path, body, label, signal) {
8392
8468
  const mapped = ERROR_TEXT3[err.code ?? ""];
8393
8469
  if (mapped)
8394
8470
  return { ok: false, text: typeof mapped === "function" ? mapped(label) : mapped };
8395
- return { ok: false, text: `Request failed (HTTP ${res.status}): ${err.message ?? "no message"}` };
8471
+ return { ok: false, text: friendlyStatus(res.status, "model", err.message) };
8396
8472
  } catch (e) {
8397
- if (e instanceof Error && e.name === "TimeoutError")
8398
- return { ok: false, text: "Request timed out after 30 seconds." };
8399
- return { ok: false, text: `Request failed: ${String(e)}` };
8473
+ return { ok: false, text: friendlyThrown(e) };
8400
8474
  }
8401
8475
  }
8402
8476
  var done3 = (o) => ({ content: [{ type: "text", text: o.text }], details: o.details });
@@ -8465,11 +8539,11 @@ async function resolveBuyPage(opts) {
8465
8539
  signal: AbortSignal.timeout(5000)
8466
8540
  });
8467
8541
  if (!res.ok)
8468
- return { kind: "error", text: `Could not fetch the buy page (HTTP ${res.status}).` };
8542
+ return { kind: "error", text: friendlyStatus(res.status) };
8469
8543
  const me = await res.json();
8470
8544
  return me.buy_url ? { kind: "url", url: me.buy_url } : { kind: "unavailable" };
8471
8545
  } catch (err) {
8472
- return { kind: "error", text: `Could not fetch the buy page: ${String(err)}` };
8546
+ return { kind: "error", text: friendlyThrown(err) };
8473
8547
  }
8474
8548
  }
8475
8549
  var BUY_NOT_AVAILABLE_TEXT = "Buying credits is not available on this server yet.";
@@ -8549,7 +8623,6 @@ session.
8549
8623
 
8550
8624
  ## Commands
8551
8625
  - \`/usage\` — spend and remaining budget today
8552
- - \`/support\` — visit today's advertiser
8553
8626
  - \`/feedback <text>\` — send feedback to the free-pi team
8554
8627
  - \`/tos\` — open the Terms of Service
8555
8628
  - \`/privacy-policy\` — open the Privacy Policy
@@ -8585,9 +8658,6 @@ in to a server that uses OAuth, and \`free-pi-cli mcp logout <server>\`. Add
8585
8658
  Inside a session, \`/mcp\` shows each server and its tools, and lets you sign
8586
8659
  in, reconnect, or turn a server on or off.
8587
8660
 
8588
- ## Support
8589
- Today's sponsor keeps free-pi free. When the user asks how to help the
8590
- project, or seems to like free-pi, suggest \`/support\` to visit the sponsor.
8591
8661
  More at https://freepi.ai
8592
8662
  `;
8593
8663
 
@@ -8980,6 +9050,142 @@ function createToolAliasesExtension(opts = {}) {
8980
9050
  };
8981
9051
  }
8982
9052
 
9053
+ // ../../node_modules/.bun/@earendil-works+pi-ai@1.2.0+a410056ef8f430f0/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js
9054
+ var OVERFLOW_PATTERNS = [
9055
+ /prompt (?:is )?too long/i,
9056
+ /prompt exceeds max length/i,
9057
+ /request_too_large/i,
9058
+ /input is too long for requested model/i,
9059
+ /exceeds the context window/i,
9060
+ /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
9061
+ /input token count.*exceeds the maximum/i,
9062
+ /maximum prompt length is \d+/i,
9063
+ /reduce the length of the messages/i,
9064
+ /maximum context length is \d+ tokens/i,
9065
+ /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
9066
+ /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i,
9067
+ /exceeds the limit of \d+/i,
9068
+ /exceeds the available context size/i,
9069
+ /greater than the context length/i,
9070
+ /context window exceeds limit/i,
9071
+ /exceeded model token limit/i,
9072
+ /too large for model with \d+ maximum context length/i,
9073
+ /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
9074
+ /model_context_window_exceeded/i,
9075
+ /prompt too long; exceeded (?:max )?context length/i,
9076
+ /range of input length should be/i,
9077
+ /context[_ ]length[_ ]exceeded/i,
9078
+ /too many tokens/i,
9079
+ /token limit exceeded/i
9080
+ ];
9081
+ var CEREBRAS_BODYLESS_OVERFLOW_PATTERN = /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i;
9082
+ var NON_OVERFLOW_PATTERNS = [
9083
+ /^(Throttling error|Service unavailable):/i,
9084
+ /rate limit/i,
9085
+ /too many requests/i
9086
+ ];
9087
+ function isContextOverflow(message, contextWindow) {
9088
+ if (message.stopReason === "error" && message.errorMessage) {
9089
+ const isNonOverflow = NON_OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage));
9090
+ if (!isNonOverflow) {
9091
+ if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage))) {
9092
+ return true;
9093
+ }
9094
+ if (message.provider === "cerebras" && CEREBRAS_BODYLESS_OVERFLOW_PATTERN.test(message.errorMessage)) {
9095
+ return true;
9096
+ }
9097
+ }
9098
+ }
9099
+ if (contextWindow && message.stopReason === "stop") {
9100
+ const inputTokens = message.usage.input + message.usage.cacheRead;
9101
+ if (inputTokens > contextWindow) {
9102
+ return true;
9103
+ }
9104
+ }
9105
+ if (contextWindow && message.stopReason === "length" && message.usage.output === 0) {
9106
+ const inputTokens = message.usage.input + message.usage.cacheRead;
9107
+ if (inputTokens >= contextWindow * 0.99) {
9108
+ return true;
9109
+ }
9110
+ }
9111
+ return false;
9112
+ }
9113
+
9114
+ // ../../node_modules/.bun/@earendil-works+pi-ai@1.2.0+a410056ef8f430f0/node_modules/@earendil-works/pi-ai/dist/utils/retry.js
9115
+ function buildProviderErrorPattern(patterns) {
9116
+ return new RegExp(patterns.join("|"), "i");
9117
+ }
9118
+ var NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([
9119
+ "GoUsageLimitError",
9120
+ "FreeUsageLimitError",
9121
+ "Monthly usage limit reached",
9122
+ "available balance",
9123
+ "insufficient_quota",
9124
+ "out of budget",
9125
+ "quota exceeded",
9126
+ "billing",
9127
+ "subscription_sharing_usage_limit_exceeded"
9128
+ ]);
9129
+ var RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
9130
+ "overloaded",
9131
+ "server_busy",
9132
+ "servers are currently busy",
9133
+ "currently experiencing high demand",
9134
+ "model is at capacity",
9135
+ "rate.?limit",
9136
+ "too many requests",
9137
+ "429",
9138
+ "500",
9139
+ "502",
9140
+ "503",
9141
+ "504",
9142
+ "520",
9143
+ "524",
9144
+ "service.?unavailable",
9145
+ "server.?error",
9146
+ "internal.?error",
9147
+ "provider.?returned.?(?:an.?)?error",
9148
+ "exceeded request buffer limit while retrying upstream",
9149
+ "network.?error",
9150
+ "connection.?error",
9151
+ "connection.?refused",
9152
+ "connection.?lost",
9153
+ "other side closed",
9154
+ "fetch failed",
9155
+ "getaddrinfo",
9156
+ "ENOTFOUND",
9157
+ "EAI_AGAIN",
9158
+ "upstream.?connect",
9159
+ "reset before headers",
9160
+ "socket hang up",
9161
+ "socket connection was closed",
9162
+ "timed? out",
9163
+ "timeout",
9164
+ "terminated",
9165
+ "websocket.?closed",
9166
+ "websocket.?error",
9167
+ "ended without",
9168
+ "stream ended before message_stop",
9169
+ "stream ended before a terminal response event",
9170
+ "http2 request did not get a response",
9171
+ "pending stream has been canceled",
9172
+ "retry delay",
9173
+ "you can retry your request",
9174
+ "try your request again",
9175
+ "please retry your request",
9176
+ "ResourceExhausted",
9177
+ "subscription_sharing_usage_unavailable",
9178
+ "subscription_sharing_user_unavailable"
9179
+ ]);
9180
+ function isRetryableAssistantError(message) {
9181
+ if (message.stopReason !== "error" || !message.errorMessage)
9182
+ return false;
9183
+ const errorMessage = message.errorMessage;
9184
+ if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage))
9185
+ return false;
9186
+ return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);
9187
+ }
9188
+
8983
9189
  // src/fusion.ts
8984
9190
  var FUSION_ID = "fusion";
8985
9191
  var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
@@ -9175,7 +9381,11 @@ function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
9175
9381
 
9176
9382
  // src/error-notice.ts
9177
9383
  var UPSTREAM_ERROR_FRAME_MESSAGE = "upstream returned an error frame";
9384
+ var PREMIUM_MODEL_NOTICE = "This model needs premium credits. Get some with /buy-credits, or switch to another model with /model.";
9178
9385
  var PROVIDER_FAILING_NOTICE = "This model's provider is failing right now. Try again in a moment, or switch models with /model.";
9386
+ var SIGNIN_EXPIRED_NOTICE = "Your free-pi sign-in has expired. Run /login, or restart free-pi-cli.";
9387
+ var CONNECTION_NOTICE = "Can't reach free-pi right now (connection error). Check your internet connection.";
9388
+ var CONTENT_FILTER_NOTICE = "The model's reply was blocked by a content filter. Try rephrasing your request.";
9179
9389
  var FREE_OFFER_TITLE = "Switch to the free model and resend this prompt?";
9180
9390
  var FREE_OFFER_ACCEPT = "Switch and resend";
9181
9391
  var FREE_OFFER_DECLINE = "Stay on this model";
@@ -9235,6 +9445,8 @@ function parseErrorEnvelope(errorMessage) {
9235
9445
  }
9236
9446
  }
9237
9447
  function formatLimitEnvelope(env, opts) {
9448
+ if (env.code === "model_requires_credits" && !/premium credit/i.test(env.message))
9449
+ return PREMIUM_MODEL_NOTICE;
9238
9450
  if (env.code === "concurrent_session" || SERVER_MESSAGE_CODES.has(env.code) || env.prompt === "github_signin" || env.copy_variant === "treatment" || typeof env.remaining_credits_today !== "number") {
9239
9451
  return env.message;
9240
9452
  }
@@ -9280,6 +9492,36 @@ function lastUserEntryId(ctx) {
9280
9492
  }
9281
9493
  return "";
9282
9494
  }
9495
+ var RAW_ERROR_FIELD = "freePiRawError";
9496
+ function rawErrorMessage(msg) {
9497
+ if (!msg)
9498
+ return;
9499
+ const m = msg;
9500
+ return typeof m[RAW_ERROR_FIELD] === "string" ? m[RAW_ERROR_FIELD] : m.errorMessage;
9501
+ }
9502
+ function envelopeOf(raw) {
9503
+ return raw === UPSTREAM_ERROR_FRAME_MESSAGE ? { status: 502, code: "upstream_error", message: UPSTREAM_ERROR_FRAME_MESSAGE } : parseErrorEnvelope(raw);
9504
+ }
9505
+ var friendlyText = (env, timeZone) => env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : env.code === "unauthorized" ? SIGNIN_EXPIRED_NOTICE : formatLimitEnvelope(env, { timeZone });
9506
+ var isFreePiProvider = (provider) => provider === PROVIDER_NAME || provider === DEFENSE_PROVIDER_NAME;
9507
+ function friendlyRawText(raw) {
9508
+ const text = raw.trim();
9509
+ if (/^(?:connection error\.?|request timed out\.?|terminated|fetch failed)$/i.test(text))
9510
+ return CONNECTION_NOTICE;
9511
+ const http = /^(5\d\d)(?: status code \(no body\)$|\s*<(?:!doctype html|html)\b)/i.exec(text);
9512
+ if (http)
9513
+ return `${PROVIDER_FAILING_NOTICE} (HTTP ${http[1]})`;
9514
+ const finish = /^Provider finish_reason: (\w+)$/.exec(text);
9515
+ if (finish)
9516
+ return finish[1] === "content_filter" ? CONTENT_FILTER_NOTICE : `${PROVIDER_FAILING_NOTICE} (${finish[1].replace(/_/g, " ")})`;
9517
+ return;
9518
+ }
9519
+ function friendlyError(raw, timeZone) {
9520
+ if (typeof raw !== "string")
9521
+ return;
9522
+ const env = envelopeOf(raw);
9523
+ return env ? friendlyText(env, timeZone) : friendlyRawText(raw);
9524
+ }
9283
9525
  function createErrorNoticeExtension(opts = {}) {
9284
9526
  const lastAt = new Map;
9285
9527
  const now = opts.now ?? Date.now;
@@ -9312,21 +9554,44 @@ function createErrorNoticeExtension(opts = {}) {
9312
9554
  return {
9313
9555
  name: "free-pi-error-notice",
9314
9556
  factory(pi) {
9557
+ pi.on("message_end", (event) => {
9558
+ const msg = event.message;
9559
+ if (msg.role !== "assistant" || !isFreePiProvider(msg.provider) || msg.stopReason !== "error" || typeof msg.errorMessage !== "string")
9560
+ return;
9561
+ const env = envelopeOf(msg.errorMessage);
9562
+ const friendly = env ? friendlyText(env, timeZone) : friendlyRawText(msg.errorMessage);
9563
+ if (friendly === undefined)
9564
+ return;
9565
+ const same = (text) => {
9566
+ const next = { ...msg, errorMessage: text };
9567
+ return isRetryableAssistantError(next) === retryable && isContextOverflow(next, 0) === overflow;
9568
+ };
9569
+ const retryable = isRetryableAssistantError(msg);
9570
+ const overflow = isContextOverflow(msg, 0);
9571
+ let text = friendly;
9572
+ if (env && retryable && !isRetryableAssistantError({ ...msg, errorMessage: text }))
9573
+ text += ` (HTTP ${env.status})`;
9574
+ if (!same(text))
9575
+ return;
9576
+ return { message: { ...msg, errorMessage: text, [RAW_ERROR_FIELD]: msg.errorMessage } };
9577
+ });
9315
9578
  pi.on("turn_end", async (event, ctx) => {
9316
9579
  lastTurnEntryId = event.messageEntryId;
9317
9580
  const msg = event.message;
9318
9581
  if (!msg || msg.stopReason !== "error")
9319
9582
  return;
9320
- const env = msg.errorMessage === UPSTREAM_ERROR_FRAME_MESSAGE ? { status: 502, code: "upstream_error", message: UPSTREAM_ERROR_FRAME_MESSAGE } : parseErrorEnvelope(msg.errorMessage);
9583
+ const env = envelopeOf(rawErrorMessage(msg));
9321
9584
  if (!env)
9322
9585
  return;
9323
9586
  const t = now();
9324
9587
  if (t - (lastAt.get(env.code) ?? -Infinity) >= NOTICE_DEDUPE_MS) {
9325
9588
  lastAt.set(env.code, t);
9326
9589
  const extra = opts.extraNotice?.(env);
9327
- const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
9590
+ const shown = msg[RAW_ERROR_FIELD] !== undefined;
9591
+ const text = shown ? extra : friendlyText(env, timeZone) + (extra ? `
9328
9592
  ${extra}` : "");
9329
- ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
9593
+ if (text)
9594
+ ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
9330
9595
  }
9331
9596
  await offerFreeModel(pi, env, event.messageEntryId, ctx);
9332
9597
  });
@@ -9464,9 +9729,16 @@ function createUpgradeNoticeExtension(opts) {
9464
9729
 
9465
9730
  // src/tool-outcomes.ts
9466
9731
  var MAX_IDS = 50;
9732
+ var TOOL_META_HEADER = "x-freepi-tool-meta";
9733
+ var COMPACTED_HEADER = "x-freepi-compacted";
9734
+ var META_BUDGET = 1500;
9467
9735
  function createToolOutcomesExtension() {
9468
9736
  const failed = [];
9469
9737
  let evicted = false;
9738
+ const meta = [];
9739
+ let metaBytes = 3;
9740
+ let droppedMessages = 0;
9741
+ let pendingDropped = 0;
9470
9742
  return {
9471
9743
  name: "free-pi-tool-outcomes",
9472
9744
  factory(pi) {
@@ -9480,7 +9752,37 @@ function createToolOutcomesExtension() {
9480
9752
  evicted = true;
9481
9753
  }
9482
9754
  });
9755
+ pi.on("tool_execution_end", (event) => {
9756
+ const e = event;
9757
+ if (!e.toolCallId || !/^[\w.:-]{1,100}$/.test(e.toolCallId) || typeof e.durationMs !== "number")
9758
+ return;
9759
+ let exit = "-";
9760
+ if (e.toolName === "bash" || e.toolName === "powershell") {
9761
+ const m = e.isError ? /Command exited with code (\d{1,3})\s*$/.exec(e.result?.content?.map((b) => b.text ?? "").join("") ?? "") : null;
9762
+ exit = m ? m[1] : e.isError ? "-" : "0";
9763
+ }
9764
+ const entry = `${e.toolCallId}~${Math.max(0, Math.round(e.durationMs))}~${exit}~${e.result?.details?.truncation?.truncated ? 1 : 0}`;
9765
+ if (entry.length > 150)
9766
+ return;
9767
+ meta.push(entry);
9768
+ metaBytes += entry.length + 1;
9769
+ while (metaBytes > META_BUDGET && meta.length > 1)
9770
+ metaBytes -= meta.shift().length + 1;
9771
+ });
9772
+ pi.on("session_before_compact", (event) => {
9773
+ const p = event.preparation;
9774
+ pendingDropped = (p?.messagesToSummarize?.length ?? 0) + (p?.turnPrefixMessages?.length ?? 0);
9775
+ });
9776
+ pi.on("session_compact", () => {
9777
+ droppedMessages = pendingDropped;
9778
+ });
9483
9779
  pi.on("before_provider_headers", (event) => {
9780
+ if (meta.length)
9781
+ event.headers[TOOL_META_HEADER] = `v1:${meta.join(",")}`;
9782
+ if (droppedMessages) {
9783
+ event.headers[COMPACTED_HEADER] = `v1:${droppedMessages}`;
9784
+ droppedMessages = 0;
9785
+ }
9484
9786
  event.headers["x-freepi-tool-errors"] = `${evicted ? "v1x" : "v1"}:${failed.join(",")}`;
9485
9787
  });
9486
9788
  }
@@ -9820,6 +10122,20 @@ function installBottomPin(mode) {
9820
10122
  return [...lines, ...Array(bottomPadding(lines.length, other, rows)).fill("")];
9821
10123
  };
9822
10124
  }
10125
+ var IMAGE_MARKS = ["\x1B_G", "\x1B]1337;File="];
10126
+ function clampLines(lines, width) {
10127
+ return lines.map((l) => IMAGE_MARKS.some((m) => l.includes(m)) || visibleWidth3(l) <= width ? l : truncateToWidth6(l, Math.max(0, width), ""));
10128
+ }
10129
+ function installWidthGuard(mode) {
10130
+ const m = mode;
10131
+ for (const k of ["headerContainer", "loadedResourcesContainer", "chatContainer", "pendingMessagesContainer", "statusContainer", "widgetContainerAbove", "editorContainer", "widgetContainerBelow", "footerContainer"]) {
10132
+ const c = m[k];
10133
+ if (!c)
10134
+ continue;
10135
+ const render = c.render.bind(c);
10136
+ c.render = (w) => clampLines(render(w), w);
10137
+ }
10138
+ }
9823
10139
 
9824
10140
  // src/guest-handoff.ts
9825
10141
  var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
@@ -9909,6 +10225,14 @@ function mergeAttribution(a, b) {
9909
10225
 
9910
10226
  // src/changelog.ts
9911
10227
  var CHANGELOG_HIGHLIGHTS = [
10228
+ {
10229
+ version: "0.6.21",
10230
+ highlights: ["The /support command is removed."]
10231
+ },
10232
+ {
10233
+ version: "0.6.20",
10234
+ highlights: ["Error messages are now plain sentences instead of raw server text."]
10235
+ },
9912
10236
  {
9913
10237
  version: "0.6.19",
9914
10238
  highlights: ["The ad banner now refreshes when you send a message, not on a timer."]
@@ -10273,6 +10597,7 @@ function highlightsFor(version) {
10273
10597
 
10274
10598
  // src/commands.ts
10275
10599
  import { access } from "node:fs/promises";
10600
+ import { basename as basename3 } from "node:path";
10276
10601
 
10277
10602
  // src/auth-flow.ts
10278
10603
  class UnauthorizedError extends Error {
@@ -10287,13 +10612,23 @@ async function postJson(baseUrl, path, body, extraHeaders = {}) {
10287
10612
  body: JSON.stringify(body)
10288
10613
  });
10289
10614
  }
10615
+ var NETWORK_TEXT = "Can't reach free-pi. Check your internet connection and try again.";
10616
+ var RATE_LIMIT_TEXT = "Too many sign-in attempts from your network. Wait a minute and try again.";
10617
+ var UNEXPECTED_REPLY_TEXT = "free-pi sent an unexpected reply. If you're on public wifi, sign in to the network first, then try again.";
10618
+ async function parseReply(res, schema) {
10619
+ try {
10620
+ return schema.parse(await res.json());
10621
+ } catch (err) {
10622
+ throw new Error(UNEXPECTED_REPLY_TEXT, { cause: err });
10623
+ }
10624
+ }
10290
10625
  async function postJsonRetry(baseUrl, path, body, attempts = 4, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), extraHeaders = {}) {
10291
10626
  for (let attempt = 1;; attempt++) {
10292
10627
  try {
10293
10628
  return await postJson(baseUrl, path, body, extraHeaders);
10294
10629
  } catch (err) {
10295
10630
  if (attempt >= attempts) {
10296
- throw new Error(`network unreachable after ${attempts} attempts (${err instanceof Error ? err.message : String(err)})`);
10631
+ throw new Error(NETWORK_TEXT, { cause: err });
10297
10632
  }
10298
10633
  await sleep(1500 * attempt);
10299
10634
  }
@@ -10310,6 +10645,16 @@ function refusalMessage(status, body) {
10310
10645
  }
10311
10646
  return;
10312
10647
  }
10648
+ function failureText(status, body) {
10649
+ if (status === 429 || body?.code === "rate_limited" && status < 500)
10650
+ return RATE_LIMIT_TEXT;
10651
+ const refusal = refusalMessage(status, body);
10652
+ if (refusal)
10653
+ return refusal;
10654
+ if (status >= 500)
10655
+ return `free-pi is having trouble right now (HTTP ${status}). Try again in a minute.`;
10656
+ return `free-pi could not complete sign-in (HTTP ${status}). Try again in a minute.`;
10657
+ }
10313
10658
  async function readRefusal(res) {
10314
10659
  return await res.json().catch(() => {
10315
10660
  return;
@@ -10318,9 +10663,9 @@ async function readRefusal(res) {
10318
10663
  async function startDeviceFlow(baseUrl) {
10319
10664
  const res = await postJsonRetry(baseUrl, "/auth/github/device", {});
10320
10665
  if (!res.ok) {
10321
- throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `device flow start failed: HTTP ${res.status}`);
10666
+ throw new Error(failureText(res.status, await readRefusal(res)));
10322
10667
  }
10323
- return GithubDeviceResponseSchema.parse(await res.json());
10668
+ return parseReply(res, GithubDeviceResponseSchema);
10324
10669
  }
10325
10670
  async function startConnect(baseUrl, consentVersion, attribution = {}, client = {}) {
10326
10671
  const res = await postJsonRetry(baseUrl, "/auth/connect/start", {
@@ -10330,8 +10675,8 @@ async function startConnect(baseUrl, consentVersion, attribution = {}, client =
10330
10675
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
10331
10676
  }, undefined, undefined, { [SIGNIN_EXP_HEADER]: "1" });
10332
10677
  if (!res.ok)
10333
- throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `connect start failed: HTTP ${res.status}`);
10334
- return ConnectStartResponseSchema.parse(await res.json());
10678
+ throw new Error(failureText(res.status, await readRefusal(res)));
10679
+ return parseReply(res, ConnectStartResponseSchema);
10335
10680
  }
10336
10681
  async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {}) {
10337
10682
  const res = await postJsonRetry(baseUrl, "/auth/consent", {
@@ -10341,7 +10686,7 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
10341
10686
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
10342
10687
  });
10343
10688
  if (!res.ok) {
10344
- throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `consent submission failed: HTTP ${res.status}`);
10689
+ throw new Error(failureText(res.status, await readRefusal(res)));
10345
10690
  }
10346
10691
  }
10347
10692
  function deviceReport() {
@@ -10374,8 +10719,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
10374
10719
  throw new GuestUnavailableError(body?.reason ?? "disabled");
10375
10720
  }
10376
10721
  if (!res.ok)
10377
- throw new Error(`guest start failed: HTTP ${res.status}`);
10378
- const parsed = GuestStartResponseSchema.parse(await res.json());
10722
+ throw new Error(failureText(res.status, await readRefusal(res)));
10723
+ const parsed = await parseReply(res, GuestStartResponseSchema);
10379
10724
  return {
10380
10725
  token: parsed.token,
10381
10726
  allowance_usd: parsed.allowance_usd,
@@ -10455,7 +10800,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
10455
10800
  throw new PollAbortedError;
10456
10801
  consecutiveNetworkFailures++;
10457
10802
  if (consecutiveNetworkFailures >= 6) {
10458
- throw new Error(`token poll failed: network unreachable after ${consecutiveNetworkFailures} attempts (${err instanceof Error ? err.message : String(err)})`);
10803
+ throw new Error(NETWORK_TEXT, { cause: err });
10459
10804
  }
10460
10805
  await sleep(Math.max(waitSeconds, 5) * 1000);
10461
10806
  continue;
@@ -10473,9 +10818,9 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
10473
10818
  throw new LoginTimeoutError;
10474
10819
  if (isExpiredTokenPoll(body))
10475
10820
  throw new DeviceCodeExpiredError;
10476
- throw new Error(refusalMessage(res.status, body) ?? `token poll failed: HTTP ${res.status}`);
10821
+ throw new Error(failureText(res.status, body));
10477
10822
  }
10478
- const parsed = AuthTokenResponseSchema.parse(await res.json());
10823
+ const parsed = await parseReply(res, AuthTokenResponseSchema);
10479
10824
  if (opts.signal?.aborted)
10480
10825
  throw new PollAbortedError;
10481
10826
  if ("token" in parsed)
@@ -11377,15 +11722,6 @@ async function buyCredits(opts, ctx) {
11377
11722
  (opts.openBrowserImpl ?? openBrowser)(page.url);
11378
11723
  ctx.ui.notify(buyPageText(page.url), "info");
11379
11724
  }
11380
- async function openAdvertiser(opts, ctx) {
11381
- const url = opts.getAdvertiserUrl?.();
11382
- if (!url) {
11383
- ctx.ui.notify("No advertiser loaded yet.", "info");
11384
- return;
11385
- }
11386
- (opts.openBrowserImpl ?? openBrowser)(url);
11387
- ctx.ui.notify("Opening today's advertiser in your browser. Thank you for supporting free-pi.", "info");
11388
- }
11389
11725
  var TERMS_URL = "https://freepi.ai/terms";
11390
11726
  var PRIVACY_URL = "https://freepi.ai/privacy";
11391
11727
  async function openTos(opts, ctx) {
@@ -11449,7 +11785,7 @@ async function runLoginUnguarded(opts, ctx, lead) {
11449
11785
  ctx.ui.notify(LOGIN_DENIED_LINE, "info");
11450
11786
  return false;
11451
11787
  }
11452
- const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
11788
+ const reason = err instanceof Error && err.message ? `: ${err.message.replace(/\.$/, "")}` : "";
11453
11789
  ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
11454
11790
  return false;
11455
11791
  }
@@ -11467,6 +11803,26 @@ function installLoginHandler(mode, opts) {
11467
11803
  return runLogin(opts, { ui: { notify } });
11468
11804
  };
11469
11805
  }
11806
+ var IMPORT_NEXT_STEP = "Run /import again, or start a fresh session.";
11807
+ var FS_REASONS = {
11808
+ EACCES: "permission denied",
11809
+ EPERM: "permission denied",
11810
+ ENOENT: "not found",
11811
+ EISDIR: "that is a folder, not a file",
11812
+ EMFILE: "too many files are open",
11813
+ ENOSPC: "the disk is full"
11814
+ };
11815
+ function friendlyImportError(err, action = "import") {
11816
+ const e = err;
11817
+ const code = typeof e?.code === "string" && /^E[A-Z0-9]+$/.test(e.code) ? e.code : undefined;
11818
+ if (code) {
11819
+ const file = typeof e.path === "string" && e.path ? basename3(e.path) : undefined;
11820
+ const reason = FS_REASONS[code] ?? `error ${code}`;
11821
+ return `Couldn't ${action}${file ? ` ${file}` : ""}: ${reason}. ${IMPORT_NEXT_STEP}`;
11822
+ }
11823
+ const msg = (err instanceof Error ? err.message : String(err)).trim().replace(/[.\s]+$/, "");
11824
+ return `Couldn't ${action}: ${msg || "unknown error"}. ${IMPORT_NEXT_STEP}`;
11825
+ }
11470
11826
  function installImportHandler(mode, opts) {
11471
11827
  const m = mode;
11472
11828
  mode.handleImportCommand = async (text) => {
@@ -11489,11 +11845,11 @@ function installImportHandler(mode, opts) {
11489
11845
  for (let waited = 0;m.session.isCompacting && waited < 60000; waited += 100)
11490
11846
  await new Promise((r) => setTimeout(r, 100));
11491
11847
  const options = m.session.isStreaming ? { streamingBehavior: "followUp" } : undefined;
11492
- m.session.prompt(message, options).catch((err) => m.showError(err instanceof Error ? err.message : String(err)));
11848
+ m.session.prompt(message, options).catch((err) => m.showError(friendlyImportError(err, "send the import")));
11493
11849
  }
11494
11850
  });
11495
11851
  } catch (err) {
11496
- m.showError(`Couldn't import: ${err instanceof Error ? err.message : String(err)}`);
11852
+ m.showError(friendlyImportError(err));
11497
11853
  }
11498
11854
  };
11499
11855
  }
@@ -11641,7 +11997,7 @@ function createGuestLoginExtension(opts) {
11641
11997
  if (!opts.isGuest?.())
11642
11998
  return;
11643
11999
  const msg = event.message;
11644
- const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(msg.errorMessage) : undefined;
12000
+ const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(rawErrorMessage(msg)) : undefined;
11645
12001
  if (envelope && (envelope.code === "guest_allowance" || envelope.code === "guest_unavailable" || envelope.prompt === "github_signin")) {
11646
12002
  if (!inFlight && !signedIn) {
11647
12003
  const busy = envelope.code === "guest_allowance" && (envelope.remaining_credits_today ?? 0) > 0;
@@ -11711,10 +12067,6 @@ function createFreePiCommandsExtension(opts) {
11711
12067
  description: FEEDBACK_DESCRIPTION,
11712
12068
  handler: (args, ctx) => sendFeedback(opts, args, ctx)
11713
12069
  });
11714
- pi.registerCommand("support", {
11715
- description: "Open today's advertiser in your browser to support free-pi.",
11716
- handler: (_args, ctx) => openAdvertiser(opts, ctx)
11717
- });
11718
12070
  pi.registerCommand("invite", {
11719
12071
  description: "Get your invite code and link: you and a friend both get +5k credits/day.",
11720
12072
  handler: (_args, ctx) => showInvite(opts, ctx)
@@ -11751,7 +12103,7 @@ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync a
11751
12103
  import { homedir as homedir4 } from "node:os";
11752
12104
  import { dirname as dirname8 } from "node:path";
11753
12105
  import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
11754
- import { Box, Text as Text2, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
12106
+ import { Box, Text as Text2, truncateToWidth as truncateToWidth7, visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
11755
12107
  var TOOL_MODES = ["compact", "full", "hidden"];
11756
12108
  var TOOLS_USAGE = "Usage: /tools compact|full|hidden (compact: one line per tool call; full: every call in full; hidden: just a count)";
11757
12109
  function readToolMode(path) {
@@ -11851,12 +12203,30 @@ function toolLines(i) {
11851
12203
  }
11852
12204
  return done;
11853
12205
  }
12206
+ var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
12207
+ function wrapByWidth(text, room) {
12208
+ const clean = text.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[ -\/]*[@-~]/g, "").replace(/\t/g, " ").replace(/[\x00-\x08\x0a-\x1f\x7f-\x9f]/g, "");
12209
+ const pieces = [];
12210
+ let cur = "";
12211
+ let curW = 0;
12212
+ for (const { segment } of graphemes.segment(clean)) {
12213
+ const w = visibleWidth4(segment);
12214
+ if (curW + w > room && cur) {
12215
+ pieces.push(cur);
12216
+ cur = "";
12217
+ curW = 0;
12218
+ }
12219
+ cur += segment;
12220
+ curW += w;
12221
+ }
12222
+ return [...pieces, cur];
12223
+ }
11854
12224
  function component(rows, theme, pad) {
11855
12225
  return {
11856
12226
  render: (w) => rows.flatMap((r) => {
11857
12227
  const room = Math.max(1, w - pad);
11858
- const pieces = r.raw ? Array.from(r.text).join("").match(new RegExp(`.{1,${room}}`, "gu")) ?? [""] : [truncateToWidth7(r.text, room, "…")];
11859
- return pieces.map((t) => " ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t));
12228
+ const pieces = r.raw ? wrapByWidth(r.text, room) : [truncateToWidth7(r.text, room, "…")];
12229
+ return pieces.map((t) => truncateToWidth7(" ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t), Math.max(0, w), ""));
11860
12230
  }),
11861
12231
  invalidate() {}
11862
12232
  };
@@ -11959,7 +12329,6 @@ var ALL_COMMANDS = [
11959
12329
  { name: "/usage", description: "credits left today and refill time" },
11960
12330
  { name: "/import", description: "continue a Claude Code or Codex session" },
11961
12331
  { name: "/hive", description: "share this session with friends" },
11962
- { name: "/support", description: "visit today's advertiser" },
11963
12332
  { name: "/tos", description: "terms of service" },
11964
12333
  { name: "/privacy-policy", description: "privacy policy" },
11965
12334
  { name: "/buy-credits", description: "get more usage" },
@@ -12128,16 +12497,63 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
12128
12497
  return scoped;
12129
12498
  }
12130
12499
 
12500
+ // src/default-model.ts
12501
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
12502
+ import { dirname as dirname9, join as join8 } from "node:path";
12503
+ var getDefaultModelPath = (agentDir) => join8(agentDir, "default-model.json");
12504
+ function readDefaultModel(agentDir) {
12505
+ if (!agentDir)
12506
+ return;
12507
+ try {
12508
+ const v = JSON.parse(readFileSync5(getDefaultModelPath(agentDir), "utf8"));
12509
+ return typeof v.provider === "string" && typeof v.model === "string" && v.provider && v.model ? { provider: v.provider, model: v.model } : undefined;
12510
+ } catch {
12511
+ return;
12512
+ }
12513
+ }
12514
+
12515
+ class DefaultModelStorage {
12516
+ agentDir;
12517
+ global;
12518
+ project;
12519
+ constructor(agentDir) {
12520
+ this.agentDir = agentDir;
12521
+ }
12522
+ withLock(scope, fn) {
12523
+ const next = fn(scope === "global" ? this.global : this.project);
12524
+ if (next === undefined)
12525
+ return;
12526
+ if (scope === "project") {
12527
+ this.project = next;
12528
+ return;
12529
+ }
12530
+ const before = this.global;
12531
+ this.global = next;
12532
+ if (before === undefined || !this.agentDir)
12533
+ return;
12534
+ try {
12535
+ const a = JSON.parse(before);
12536
+ const b = JSON.parse(next);
12537
+ if (a.defaultProvider === b.defaultProvider && a.defaultModel === b.defaultModel)
12538
+ return;
12539
+ if (!b.defaultProvider || !b.defaultModel)
12540
+ return;
12541
+ mkdirSync3(dirname9(getDefaultModelPath(this.agentDir)), { recursive: true });
12542
+ writeFileSync3(getDefaultModelPath(this.agentDir), JSON.stringify({ provider: b.defaultProvider, model: b.defaultModel }));
12543
+ } catch {}
12544
+ }
12545
+ }
12546
+
12131
12547
  // src/hive.ts
12132
12548
  import { createHash as createHash3 } from "node:crypto";
12133
12549
  import { execFileSync } from "node:child_process";
12134
- import { lstatSync as lstatSync2, readFileSync as readFileSync5, readlinkSync as readlinkSync2 } from "node:fs";
12135
- import { join as join9 } from "node:path";
12550
+ import { lstatSync as lstatSync2, readFileSync as readFileSync6, readlinkSync as readlinkSync2 } from "node:fs";
12551
+ import { join as join10 } from "node:path";
12136
12552
 
12137
12553
  // src/hive-guard.ts
12138
12554
  import { existsSync as existsSync4, lstatSync, readdirSync, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
12139
12555
  import { homedir as homedir5 } from "node:os";
12140
- import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
12556
+ import { basename as basename4, dirname as dirname10, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
12141
12557
  import { fileURLToPath as fileURLToPath3 } from "node:url";
12142
12558
 
12143
12559
  // src/subagents.ts
@@ -12254,8 +12670,10 @@ function lastAssistant(session) {
12254
12670
  continue;
12255
12671
  const text = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
12256
12672
  `).trim() : "";
12257
- if (m.stopReason === "error")
12258
- return { text, error: m.errorMessage || "the model request failed" };
12673
+ if (m.stopReason === "error") {
12674
+ const raw = rawErrorMessage(m);
12675
+ return { text, error: (raw && isFreePiProvider(m.provider) && !raw.includes("subagents_unavailable") ? friendlyError(raw) : undefined) ?? (raw || "the model request failed") };
12676
+ }
12259
12677
  if (m.stopReason === "aborted")
12260
12678
  return { text, error: "aborted" };
12261
12679
  return { text };
@@ -12646,7 +13064,7 @@ var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
12646
13064
  var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
12647
13065
  var CLASSIFIED = ["bash", "write", "edit"];
12648
13066
  var SECRET_BASENAME = [/^\.env(\..*)?$/, /\.pem$/, /^id_(rsa|dsa|ecdsa|ed25519)/];
12649
- var SECRET_HOME_DIRS = [".ssh", ".aws", join8(".config", "freepi"), ".free-pi"];
13067
+ var SECRET_HOME_DIRS = [".ssh", ".aws", join9(".config", "freepi"), ".free-pi"];
12650
13068
  var SECRET_IN_TEXT = /(?:^|[\s"'=:/])\.env(?:\.[\w.-]+)?(?=$|[\s"';|&)])|\.pem\b|\bid_(?:rsa|dsa|ecdsa|ed25519)|\.ssh\b|\.aws\b|\.config\/freepi|\.free-pi\b/;
12651
13069
  var BASH_DENY = [
12652
13070
  [/\brm\s+(?:-\S+\s+)*(?:\/|~|\$HOME)\*?(?:\s|$)/, "destructive command"],
@@ -12669,7 +13087,7 @@ function protectedLinkTargets(root, limit) {
12669
13087
  const note = (link) => {
12670
13088
  let target;
12671
13089
  try {
12672
- target = resolve2(dirname9(link), readlinkSync(link));
13090
+ target = resolve2(dirname10(link), readlinkSync(link));
12673
13091
  } catch {
12674
13092
  return;
12675
13093
  }
@@ -12679,7 +13097,7 @@ function protectedLinkTargets(root, limit) {
12679
13097
  out.push(real);
12680
13098
  };
12681
13099
  for (const entry of [".git/hooks", ".git/config"]) {
12682
- const link = join8(root, entry);
13100
+ const link = join9(root, entry);
12683
13101
  try {
12684
13102
  if (lstatSync(link).isSymbolicLink())
12685
13103
  note(link);
@@ -12698,7 +13116,7 @@ function protectedLinkTargets(root, limit) {
12698
13116
  for (const entry of entries) {
12699
13117
  if (++seen > limit)
12700
13118
  return null;
12701
- const path = join8(dir, entry.name);
13119
+ const path = join9(dir, entry.name);
12702
13120
  if (entry.isSymbolicLink()) {
12703
13121
  if (protectedByName(relative2(root, path)))
12704
13122
  note(path);
@@ -12748,7 +13166,7 @@ function resolveLikePi(raw, cwd, home, platform) {
12748
13166
  if (p === "~")
12749
13167
  p = home;
12750
13168
  else if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
12751
- p = join8(home, p.slice(2));
13169
+ p = join9(home, p.slice(2));
12752
13170
  if (/^file:\/\//.test(p))
12753
13171
  p = fileURLToPath3(p);
12754
13172
  return resolve2(cwd, p);
@@ -12758,17 +13176,17 @@ function realpathDeepest(p) {
12758
13176
  const rest = [];
12759
13177
  for (;; ) {
12760
13178
  try {
12761
- return join8(realpathSync3(current), ...rest.reverse());
13179
+ return join9(realpathSync3(current), ...rest.reverse());
12762
13180
  } catch {
12763
13181
  try {
12764
13182
  lstatSync(current);
12765
13183
  return null;
12766
13184
  } catch {}
12767
13185
  }
12768
- const parent = dirname9(current);
13186
+ const parent = dirname10(current);
12769
13187
  if (parent === current)
12770
13188
  return null;
12771
- rest.push(basename3(current));
13189
+ rest.push(basename4(current));
12772
13190
  current = parent;
12773
13191
  }
12774
13192
  }
@@ -12777,10 +13195,10 @@ function isInside(root, target) {
12777
13195
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
12778
13196
  }
12779
13197
  function isSecret(target, home) {
12780
- if (SECRET_BASENAME.some((re) => re.test(basename3(target))))
13198
+ if (SECRET_BASENAME.some((re) => re.test(basename4(target))))
12781
13199
  return true;
12782
13200
  return SECRET_HOME_DIRS.some((dir) => {
12783
- const lexical = join8(home, dir);
13201
+ const lexical = join9(home, dir);
12784
13202
  return isInside(lexical, target) || isInside(realpathDeepest(lexical) ?? lexical, target);
12785
13203
  });
12786
13204
  }
@@ -12801,7 +13219,7 @@ function scanForSecret(dir) {
12801
13219
  if (SECRET_BASENAME.some((re) => re.test(entry.name)))
12802
13220
  return "yes";
12803
13221
  if (entry.isDirectory())
12804
- queue.push(join8(current, entry.name));
13222
+ queue.push(join9(current, entry.name));
12805
13223
  }
12806
13224
  }
12807
13225
  return "no";
@@ -12925,6 +13343,33 @@ async function decideToolCall(event, opts) {
12925
13343
  }
12926
13344
 
12927
13345
  // src/hive.ts
13346
+ var HIVE_ERRORS = new Map(Object.entries({
13347
+ rate_limited: "Hive: slow down, too many messages in a short time.",
13348
+ pending: "Hive: another request is still waiting for an answer.",
13349
+ not_driver: "Hive: only the person driving the room can do that.",
13350
+ already_driver: "Hive: you are already driving the room.",
13351
+ unknown_conn: "Hive: that person has left the room.",
13352
+ unknown_prompt: "Hive: that prompt is no longer waiting.",
13353
+ unknown_request: "Hive: that request is no longer waiting.",
13354
+ bad_frame: "Hive: the room got a message it could not read. Try again.",
13355
+ internal: "Hive: something went wrong on the server. Try again."
13356
+ }));
13357
+ var HIVE_CLOSED = new Map(Object.entries({
13358
+ replaced: "Hive room closed: you opened a newer room.",
13359
+ host_left: "Hive room closed: the host left.",
13360
+ ended: "Hive room closed: the host ended it.",
13361
+ expired: "Hive room closed: it expired.",
13362
+ kicked: "Hive room closed: you were removed from it.",
13363
+ shutdown: "Hive room closed: the server is restarting.",
13364
+ no_room: "Hive room closed: the room no longer exists.",
13365
+ unauthorized: "Hive room closed: you are not signed in.",
13366
+ auth_timeout: "Hive room closed: sign-in took too long.",
13367
+ busy: "Hive room closed: the server is busy. Try again soon.",
13368
+ overloaded: "Hive room closed: the server is busy. Try again soon.",
13369
+ slow: "Hive room closed: the connection was too slow."
13370
+ }));
13371
+ var hiveErrorText = (code) => HIVE_ERRORS.get(code) ?? `Hive: ${code}`;
13372
+ var hiveClosedText = (reason) => HIVE_CLOSED.get(reason) ?? `Hive room closed (${reason}).`;
12928
13373
  var RUN_IN_SANDBOX = "Run in sandbox";
12929
13374
  var RUN_OUTSIDE = "Run outside the sandbox";
12930
13375
  var DENY = "Deny";
@@ -12948,7 +13393,7 @@ function fingerprint(path) {
12948
13393
  return "";
12949
13394
  if (st.size > HASH_MAX_BYTES)
12950
13395
  return `${st.size}:${st.mtimeMs}`;
12951
- return createHash3("sha256").update(readFileSync5(path)).digest("hex");
13396
+ return createHash3("sha256").update(readFileSync6(path)).digest("hex");
12952
13397
  } catch {
12953
13398
  return "";
12954
13399
  }
@@ -12978,7 +13423,7 @@ function worktreeSnapshot(g) {
12978
13423
  const status = entry.slice(0, 2);
12979
13424
  if (status[0] === "R" || status[0] === "C")
12980
13425
  i++;
12981
- files.set(entry.slice(3), `${status}:${fingerprint(join9(root, entry.slice(3)))}`);
13426
+ files.set(entry.slice(3), `${status}:${fingerprint(join10(root, entry.slice(3)))}`);
12982
13427
  }
12983
13428
  return { files, head: runGit(g, "rev-parse", "HEAD")?.trim() };
12984
13429
  } catch (e) {
@@ -13106,6 +13551,10 @@ function createHiveExtension(opts) {
13106
13551
  roomWaiter = undefined;
13107
13552
  }
13108
13553
  async function start(ctx) {
13554
+ if (opts.isRemoteAttached?.()) {
13555
+ notify("Hive: turn the Telegram remote off first (/remote off).");
13556
+ return;
13557
+ }
13109
13558
  if (ws || starting) {
13110
13559
  notify(roomUrl ? `Hive room: ${roomUrl}` : "Hive: still connecting...");
13111
13560
  return;
@@ -13140,10 +13589,13 @@ function createHiveExtension(opts) {
13140
13589
  };
13141
13590
  socket.onopen = () => socket.send(JSON.stringify({ type: "auth", token }));
13142
13591
  socket.onerror = failed;
13143
- socket.onclose = () => {
13592
+ socket.onclose = (event) => {
13144
13593
  if (ws !== socket)
13145
13594
  return;
13146
- if (!roomUrl)
13595
+ const known = event?.reason ? HIVE_CLOSED.get(event.reason) : undefined;
13596
+ if (known)
13597
+ endRoom(known);
13598
+ else if (!roomUrl)
13147
13599
  failed();
13148
13600
  else
13149
13601
  endRoom("Hive room closed.");
@@ -13217,10 +13669,10 @@ Guests sign in with GitHub to watch and chat. They can ask to drive; you approve
13217
13669
  break;
13218
13670
  case "error":
13219
13671
  if (frame.code !== "stale")
13220
- notify(`Hive: ${frame.code}`, "warning");
13672
+ notify(hiveErrorText(frame.code), "warning");
13221
13673
  break;
13222
13674
  case "closed":
13223
- endRoom(`Hive room closed (${frame.reason}).`);
13675
+ endRoom(hiveClosedText(frame.reason));
13224
13676
  break;
13225
13677
  }
13226
13678
  }
@@ -13431,22 +13883,440 @@ ${decision.reason}`, ACTION_DIALOG_MS);
13431
13883
  };
13432
13884
  }
13433
13885
 
13886
+ // src/remote.ts
13887
+ import { randomUUID as randomUUID2 } from "node:crypto";
13888
+ import { basename as basename5 } from "node:path";
13889
+
13890
+ // ../shared/src/remote.ts
13891
+ var REMOTE_MAX_TITLE_CHARS = 80;
13892
+ var REMOTE_MAX_COMMAND_CHARS = 2000;
13893
+ var IdSchema = string2().min(1).max(64);
13894
+ var RemoteChoiceSchema = _enum(["once", "turn", "deny"]);
13895
+ var RemoteSessionFrameSchema = discriminatedUnion("type", [
13896
+ HiveAuthFrameSchema,
13897
+ object({ type: literal("hello"), session_id: IdSchema, title: string2().max(REMOTE_MAX_TITLE_CHARS) }),
13898
+ object({ type: literal("entry"), entry: HiveEntrySchema }),
13899
+ object({ type: literal("busy"), settled: boolean2() }),
13900
+ object({ type: literal("prompt_result"), prompt_id: IdSchema, accepted: boolean2(), reason: string2().max(100).optional() }),
13901
+ object({ type: literal("question"), id: IdSchema, text: string2().max(500), command: string2().max(REMOTE_MAX_COMMAND_CHARS) }),
13902
+ object({ type: literal("question_closed"), id: IdSchema })
13903
+ ]);
13904
+ var RemoteServerFrameSchema = discriminatedUnion("type", [
13905
+ object({ type: literal("ready"), linked: boolean2(), link_url: string2().optional() }),
13906
+ object({ type: literal("prompt"), prompt_id: IdSchema, text: string2().min(1).max(HIVE_MAX_PROMPT_CHARS) }),
13907
+ object({ type: literal("answer"), question_id: IdSchema, choice: RemoteChoiceSchema }),
13908
+ object({ type: literal("stop") }),
13909
+ object({ type: literal("error"), code: string2() }),
13910
+ object({ type: literal("closed"), reason: string2() })
13911
+ ]);
13912
+ var RemoteLinkCodeResponseSchema = object({ url: string2(), expires_at: string2() });
13913
+ var RemoteLinkStatusSchema = object({ linked: boolean2() });
13914
+
13915
+ // src/remote.ts
13916
+ var OPEN2 = 1;
13917
+ var MCP_RESOURCE_TOOLS3 = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
13918
+ var HARD_BLOCKED = new Set([SUBAGENT_TOOL, SEND_MESSAGE_TOOL, BUY_TOOL_NAME]);
13919
+ function wsUrl2(baseUrl) {
13920
+ const url = new URL("/remote/ws", baseUrl);
13921
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
13922
+ return url.toString();
13923
+ }
13924
+ function createRemoteExtension(opts) {
13925
+ return {
13926
+ name: "free-pi-remote",
13927
+ factory(pi) {
13928
+ let ws;
13929
+ let ctxRef;
13930
+ let attachedCwd = "";
13931
+ let title = "";
13932
+ let settled = true;
13933
+ let compacting = false;
13934
+ let allowTurn = false;
13935
+ let localDetach = false;
13936
+ let reconnectAttempt = 0;
13937
+ let reconnectTimer;
13938
+ let injectTimer;
13939
+ let questionChain = Promise.resolve();
13940
+ let openQuestion;
13941
+ const notify = (message, type = "info") => ctxRef?.ui.notify(message, type);
13942
+ const send = (frame) => {
13943
+ if (ws?.readyState === OPEN2)
13944
+ ws.send(JSON.stringify(frame));
13945
+ };
13946
+ const sendEntry = (entry) => send({ type: "entry", entry });
13947
+ function setSettled(next) {
13948
+ if (settled === next)
13949
+ return;
13950
+ settled = next;
13951
+ send({ type: "busy", settled });
13952
+ }
13953
+ function finishQuestion(choice, reason) {
13954
+ const question = openQuestion;
13955
+ if (!question)
13956
+ return;
13957
+ openQuestion = undefined;
13958
+ question.finish(choice, reason);
13959
+ if (choice === undefined)
13960
+ send({ type: "question_closed", id: question.id });
13961
+ }
13962
+ function cancelQuestion(reason) {
13963
+ finishQuestion(undefined, reason);
13964
+ }
13965
+ function clearRemoteState(reason = "turn ended") {
13966
+ opts.state.remoteTurn = false;
13967
+ allowTurn = false;
13968
+ cancelQuestion(reason);
13969
+ }
13970
+ function endApprovals(reason) {
13971
+ allowTurn = false;
13972
+ cancelQuestion(reason);
13973
+ }
13974
+ function recoverRejectedPrompt() {
13975
+ setSettled(true);
13976
+ clearRemoteState();
13977
+ }
13978
+ function clearSocket() {
13979
+ const old = ws;
13980
+ ws = undefined;
13981
+ clearTimeout(reconnectTimer);
13982
+ reconnectTimer = undefined;
13983
+ ctxRef?.ui.setStatus("remote", undefined);
13984
+ cancelQuestion("disconnected");
13985
+ try {
13986
+ old?.close();
13987
+ } catch {}
13988
+ }
13989
+ function detach(notice) {
13990
+ localDetach = true;
13991
+ opts.state.attached = false;
13992
+ endApprovals("detached");
13993
+ clearSocket();
13994
+ attachedCwd = "";
13995
+ reconnectAttempt = 0;
13996
+ if (notice)
13997
+ notify(notice);
13998
+ }
13999
+ function questionFor(event, ctx) {
14000
+ const toolName = String(event.toolName);
14001
+ const input = event.input ?? {};
14002
+ const summary = toolName === "bash" ? String(input.command ?? "") : toolName + " " + JSON.stringify(input);
14003
+ const command = summary.slice(0, 2000);
14004
+ const run = async () => {
14005
+ if (!opts.state.remoteTurn)
14006
+ return { block: true, reason: "Remote: turn ended before approval" };
14007
+ if (allowTurn)
14008
+ return;
14009
+ if (ws?.readyState !== OPEN2)
14010
+ return { block: true, reason: "Remote: disconnected before approval" };
14011
+ const id = randomUUID2();
14012
+ let timer;
14013
+ let abortListener;
14014
+ const answer = await new Promise((resolve) => {
14015
+ openQuestion = {
14016
+ id,
14017
+ finish: (choice, reason) => {
14018
+ clearTimeout(timer);
14019
+ if (abortListener && ctx.signal)
14020
+ ctx.signal.removeEventListener("abort", abortListener);
14021
+ resolve({ choice, reason });
14022
+ }
14023
+ };
14024
+ const timeoutMs = opts.questionTimeoutMs ?? 5 * 60000;
14025
+ timer = setTimeout(() => finishQuestion(undefined, "timed out"), timeoutMs);
14026
+ timer.unref?.();
14027
+ abortListener = () => finishQuestion(undefined, "turn aborted");
14028
+ if (ctx.signal?.aborted)
14029
+ abortListener();
14030
+ else
14031
+ ctx.signal?.addEventListener("abort", abortListener, { once: true });
14032
+ send({ type: "question", id, text: "The agent wants to run this:", command });
14033
+ });
14034
+ if (answer.choice === "deny")
14035
+ return { block: true, reason: "Remote: denied from Telegram" };
14036
+ if (answer.choice === "turn")
14037
+ allowTurn = true;
14038
+ if (answer.choice === "once" || answer.choice === "turn")
14039
+ return;
14040
+ const why = answer.reason ?? "approval ended";
14041
+ return { block: true, reason: "Remote: " + why };
14042
+ };
14043
+ const current = questionChain.then(run, run);
14044
+ questionChain = current.catch(() => {
14045
+ return;
14046
+ });
14047
+ return current;
14048
+ }
14049
+ function handle(frame) {
14050
+ switch (frame.type) {
14051
+ case "ready":
14052
+ if (!frame.linked && frame.link_url) {
14053
+ notify("Remote: open this link in Telegram to connect your account: " + frame.link_url);
14054
+ } else if (frame.linked) {
14055
+ notify("Remote is on. Messages you send to the free-pi bot in Telegram go to this session.");
14056
+ }
14057
+ ctxRef?.ui.setStatus("remote", "remote: on");
14058
+ reconnectAttempt = 0;
14059
+ if (!settled)
14060
+ send({ type: "busy", settled: false });
14061
+ break;
14062
+ case "prompt": {
14063
+ if (!settled || compacting) {
14064
+ send({ type: "prompt_result", prompt_id: frame.prompt_id, accepted: false, reason: "busy" });
14065
+ break;
14066
+ }
14067
+ opts.state.remoteTurn = true;
14068
+ allowTurn = false;
14069
+ setSettled(false);
14070
+ clearTimeout(injectTimer);
14071
+ injectTimer = setTimeout(() => {
14072
+ injectTimer = undefined;
14073
+ if (ctxRef?.isIdle() !== false)
14074
+ recoverRejectedPrompt();
14075
+ }, opts.injectGraceMs ?? 2000);
14076
+ injectTimer.unref?.();
14077
+ try {
14078
+ Promise.resolve(pi.sendUserMessage(frame.text, { expandPromptTemplates: false })).catch(recoverRejectedPrompt);
14079
+ send({ type: "prompt_result", prompt_id: frame.prompt_id, accepted: true });
14080
+ } catch {
14081
+ recoverRejectedPrompt();
14082
+ send({ type: "prompt_result", prompt_id: frame.prompt_id, accepted: false, reason: "error" });
14083
+ }
14084
+ break;
14085
+ }
14086
+ case "answer":
14087
+ if (openQuestion?.id === frame.question_id)
14088
+ finishQuestion(frame.choice);
14089
+ break;
14090
+ case "stop":
14091
+ cancelQuestion("stopped");
14092
+ if (!settled)
14093
+ ctxRef?.abort();
14094
+ break;
14095
+ case "closed":
14096
+ if (frame.reason === "shutdown")
14097
+ break;
14098
+ detach(frame.reason === "replaced" ? "Remote moved to another session." : "Remote closed.");
14099
+ break;
14100
+ case "error":
14101
+ notify("Remote: the server could not process the connection.", "warning");
14102
+ break;
14103
+ }
14104
+ }
14105
+ function connect() {
14106
+ let socket;
14107
+ try {
14108
+ const Impl = opts.WebSocketImpl ?? WebSocket;
14109
+ socket = new Impl(wsUrl2(opts.baseUrl));
14110
+ } catch {
14111
+ retryConnect();
14112
+ return;
14113
+ }
14114
+ ws = socket;
14115
+ localDetach = false;
14116
+ socket.onopen = async () => {
14117
+ if (ws !== socket)
14118
+ return;
14119
+ try {
14120
+ const token = await opts.getToken();
14121
+ if (ws !== socket)
14122
+ return;
14123
+ socket.send(JSON.stringify({ type: "auth", token }));
14124
+ socket.send(JSON.stringify({ type: "hello", session_id: opts.sessionId, title }));
14125
+ } catch {
14126
+ socket.close();
14127
+ }
14128
+ };
14129
+ socket.onerror = () => {
14130
+ if (ws === socket)
14131
+ socket.close();
14132
+ };
14133
+ socket.onclose = () => {
14134
+ if (ws !== socket)
14135
+ return;
14136
+ ws = undefined;
14137
+ ctxRef?.ui.setStatus("remote", undefined);
14138
+ cancelQuestion("disconnected");
14139
+ if (!localDetach)
14140
+ retryConnect();
14141
+ };
14142
+ socket.onmessage = (event) => {
14143
+ if (ws !== socket || typeof event.data !== "string")
14144
+ return;
14145
+ let json;
14146
+ try {
14147
+ json = JSON.parse(event.data);
14148
+ } catch {
14149
+ return;
14150
+ }
14151
+ const parsed = RemoteServerFrameSchema.safeParse(json);
14152
+ if (parsed.success)
14153
+ handle(parsed.data);
14154
+ };
14155
+ }
14156
+ function retryConnect() {
14157
+ if (localDetach)
14158
+ return;
14159
+ const delays = opts.reconnectDelaysMs ?? [1000, 2000, 5000, 15000, 30000];
14160
+ if (reconnectAttempt >= delays.length) {
14161
+ localDetach = true;
14162
+ opts.state.attached = false;
14163
+ endApprovals("disconnected");
14164
+ notify("Remote: lost the connection. Run /remote to attach again.", "warning");
14165
+ return;
14166
+ }
14167
+ const delay = delays[reconnectAttempt++];
14168
+ reconnectTimer = setTimeout(() => {
14169
+ reconnectTimer = undefined;
14170
+ connect();
14171
+ }, delay);
14172
+ reconnectTimer.unref?.();
14173
+ }
14174
+ function attach(ctx) {
14175
+ if (ws || reconnectTimer) {
14176
+ notify("Remote is already attached.");
14177
+ return;
14178
+ }
14179
+ ctxRef = ctx;
14180
+ if (opts.isHiveOpen?.()) {
14181
+ notify("Remote: end the Hive room first (/hive end).");
14182
+ return;
14183
+ }
14184
+ opts.state.attached = true;
14185
+ attachedCwd = ctx.cwd;
14186
+ title = basename5(ctx.cwd).slice(0, 80);
14187
+ settled = ctx.isIdle();
14188
+ compacting = false;
14189
+ localDetach = false;
14190
+ reconnectAttempt = 0;
14191
+ connect();
14192
+ }
14193
+ pi.on("tool_call", (event, ctx) => {
14194
+ if (!opts.state.remoteTurn)
14195
+ return;
14196
+ ctxRef = ctx;
14197
+ if (HARD_BLOCKED.has(event.toolName)) {
14198
+ return { block: true, reason: "Remote: this tool is not run for a prompt sent from Telegram" };
14199
+ }
14200
+ const needsApproval = event.toolName === "bash" || event.toolName.startsWith("mcp__") || MCP_RESOURCE_TOOLS3.includes(event.toolName) || Boolean(event.parentToolCallId);
14201
+ return needsApproval ? questionFor(event, ctx) : undefined;
14202
+ });
14203
+ pi.on("agent_start", () => {
14204
+ clearTimeout(injectTimer);
14205
+ injectTimer = undefined;
14206
+ setSettled(false);
14207
+ });
14208
+ pi.on("agent_settled", () => {
14209
+ setSettled(true);
14210
+ clearRemoteState();
14211
+ sendEntry({ kind: "settled" });
14212
+ });
14213
+ pi.on("session_before_compact", (_event, ctx) => {
14214
+ ctxRef = ctx;
14215
+ compacting = true;
14216
+ setSettled(false);
14217
+ });
14218
+ const compactionEnded = (_event, ctx) => {
14219
+ ctxRef = ctx;
14220
+ compacting = false;
14221
+ if (ctx.isIdle())
14222
+ setSettled(true);
14223
+ };
14224
+ pi.on("session_compact", compactionEnded);
14225
+ pi.on("session_compact_failed", compactionEnded);
14226
+ pi.on("session_start", (_event, ctx) => {
14227
+ ctxRef = ctx;
14228
+ if (attachedCwd && ctx.cwd !== attachedCwd)
14229
+ detach();
14230
+ });
14231
+ pi.on("session_before_switch", () => detach());
14232
+ pi.on("session_shutdown", () => detach());
14233
+ pi.on("input", (event) => {
14234
+ if (event.source !== "extension")
14235
+ sendEntry({ kind: "prompt", text: event.text.slice(0, HIVE_MAX_PROMPT_CHARS) });
14236
+ return { action: "continue" };
14237
+ });
14238
+ pi.on("message_end", (event) => {
14239
+ const message = event.message;
14240
+ if (message.role !== "assistant" || !Array.isArray(message.content))
14241
+ return;
14242
+ const text = message.content.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("");
14243
+ for (let i = 0;i < text.length; i += HIVE_MAX_ASSISTANT_CHARS) {
14244
+ sendEntry({ kind: "assistant", text: text.slice(i, i + HIVE_MAX_ASSISTANT_CHARS) });
14245
+ }
14246
+ });
14247
+ pi.on("tool_execution_end", (event) => {
14248
+ sendEntry({ kind: "tool", name: event.toolName, is_error: event.isError });
14249
+ });
14250
+ if (process.env.FREEPI_REMOTE === "1") {
14251
+ pi.registerCommand("remote", {
14252
+ description: "Attach this session to Telegram: /remote, /remote off, /remote unlink",
14253
+ handler: async (args, ctx) => {
14254
+ ctxRef = ctx;
14255
+ const sub = args.trim().toLowerCase();
14256
+ if (sub === "")
14257
+ return attach(ctx);
14258
+ if (sub === "off") {
14259
+ detach();
14260
+ notify("Remote is off.");
14261
+ return;
14262
+ }
14263
+ if (sub === "unlink") {
14264
+ allowTurn = false;
14265
+ cancelQuestion("unlinked");
14266
+ const token = await opts.getToken().catch(() => {
14267
+ return;
14268
+ });
14269
+ if (!token) {
14270
+ notify("Remote: couldn't read your free-pi login.", "error");
14271
+ return;
14272
+ }
14273
+ try {
14274
+ const response = await (opts.fetchImpl ?? fetch)(opts.baseUrl + "/remote/link", {
14275
+ method: "DELETE",
14276
+ headers: { authorization: "Bearer " + token }
14277
+ });
14278
+ if (response.ok)
14279
+ detach();
14280
+ notify(response.ok ? "Remote account unlinked." : "Remote: couldn't unlink the account.", response.ok ? "info" : "warning");
14281
+ } catch {
14282
+ notify("Remote: couldn't unlink the account.", "warning");
14283
+ }
14284
+ return;
14285
+ }
14286
+ notify("Usage: /remote, /remote off, or /remote unlink.");
14287
+ }
14288
+ });
14289
+ }
14290
+ }
14291
+ };
14292
+ }
14293
+
13434
14294
  // src/shell-sandbox.ts
13435
14295
  import { spawnSync } from "node:child_process";
13436
14296
  import {
13437
14297
  chmodSync,
13438
14298
  existsSync as existsSync5,
13439
14299
  lstatSync as lstatSync3,
13440
- mkdirSync as mkdirSync3,
14300
+ mkdirSync as mkdirSync4,
13441
14301
  mkdtempSync,
13442
14302
  readdirSync as readdirSync2,
13443
14303
  realpathSync as realpathSync4,
13444
14304
  rmSync,
13445
14305
  statSync as statSync3,
13446
- writeFileSync as writeFileSync3
14306
+ writeFileSync as writeFileSync4
13447
14307
  } from "node:fs";
13448
14308
  import { homedir as homedir6, tmpdir } from "node:os";
13449
- import { dirname as dirname10, join as join10 } from "node:path";
14309
+ import { dirname as dirname11, join as join11 } from "node:path";
14310
+ var ERRNO_WORDS = { EACCES: "permission denied", EPERM: "operation not permitted", ENOENT: "not found", ENOSPC: "disk full", EROFS: "read-only file system" };
14311
+ function sandboxCause(err) {
14312
+ const e = err;
14313
+ if (typeof e?.code === "string") {
14314
+ const where = [e.syscall, e.path].filter((x) => typeof x === "string").join(" ");
14315
+ return `${Object.hasOwn(ERRNO_WORDS, e.code) ? ERRNO_WORDS[e.code] : e.code}${where ? ` (${where})` : ""}`;
14316
+ }
14317
+ return (err instanceof Error ? err.message : String(err)).split(`
14318
+ `)[0].replace(/\.$/, "");
14319
+ }
13450
14320
  var SECRET_NAME_PATTERNS = [
13451
14321
  "\\.env(\\..*)?",
13452
14322
  "[^/]*\\.pem",
@@ -13482,7 +14352,7 @@ var TOOLCHAIN_ROOTS = [
13482
14352
  "Library/pnpm"
13483
14353
  ];
13484
14354
  function toolchainRoots(home, exists = existsSync5) {
13485
- return TOOLCHAIN_ROOTS.map((r) => join10(home, r)).filter((p) => exists(p));
14355
+ return TOOLCHAIN_ROOTS.map((r) => join11(home, r)).filter((p) => exists(p));
13486
14356
  }
13487
14357
  var ENV_ALLOW = ["PATH", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "TZ", "USER", "LOGNAME", "SHELL", "PI_PROVIDER", "PI_MODEL"];
13488
14358
  function under(path, dir) {
@@ -13500,10 +14370,10 @@ function sandboxEnv(base, o) {
13500
14370
  for (const k of ENV_ALLOW)
13501
14371
  if (base[k] !== undefined)
13502
14372
  env[k] = base[k];
13503
- env.PATH = [...o.developerDir ? [join10(o.developerDir, "usr/bin")] : [], ...o.pathPrefix ?? [], ...(base.PATH ?? "/usr/bin:/bin").split(":")].filter((p) => p.startsWith("/") && !under(p, o.project) && !under(realOrSelf(p), o.project)).join(":");
14373
+ env.PATH = [...o.developerDir ? [join11(o.developerDir, "usr/bin")] : [], ...o.pathPrefix ?? [], ...(base.PATH ?? "/usr/bin:/bin").split(":")].filter((p) => p.startsWith("/") && !under(p, o.project) && !under(realOrSelf(p), o.project)).join(":");
13504
14374
  if (o.developerDir)
13505
14375
  env.DEVELOPER_DIR = o.developerDir;
13506
- env.HOME = join10(o.scratch, "home");
14376
+ env.HOME = join11(o.scratch, "home");
13507
14377
  env.TMP = env.TEMP = o.scratch;
13508
14378
  env.TMPDIR = `${o.scratch}/`;
13509
14379
  env.BUN_OPTIONS = "--no-env-file";
@@ -13513,7 +14383,7 @@ function sandboxEnv(base, o) {
13513
14383
  env.GIT_CONFIG_COUNT = "1";
13514
14384
  env.GIT_CONFIG_KEY_0 = "credential.helper";
13515
14385
  env.GIT_CONFIG_VALUE_0 = "";
13516
- env.npm_config_cache = join10(o.scratch, "npm-cache");
14386
+ env.npm_config_cache = join11(o.scratch, "npm-cache");
13517
14387
  env.FREEPI_HIVE_CALL = o.callId;
13518
14388
  return env;
13519
14389
  }
@@ -13559,7 +14429,7 @@ function scanProject(project, limit = SCAN_LIMIT2) {
13559
14429
  scan.overflow = true;
13560
14430
  break;
13561
14431
  }
13562
- const path = join10(dir, e.name);
14432
+ const path = join11(dir, e.name);
13563
14433
  if (e.isSymbolicLink())
13564
14434
  continue;
13565
14435
  if (e.isDirectory()) {
@@ -13659,7 +14529,7 @@ function developerDir(o = {}) {
13659
14529
  continue;
13660
14530
  try {
13661
14531
  const real = realpath(c);
13662
- if (exists(join10(real, "usr/bin/git")))
14532
+ if (exists(join11(real, "usr/bin/git")))
13663
14533
  return real;
13664
14534
  } catch {}
13665
14535
  }
@@ -13677,18 +14547,18 @@ function opensslFiles(o = {}) {
13677
14547
  const files = [];
13678
14548
  const dirs = [];
13679
14549
  for (const prefix of o.prefixes ?? ["/opt/homebrew", "/usr/local"]) {
13680
- const etc = join10(prefix, "etc");
14550
+ const etc = join11(prefix, "etc");
13681
14551
  for (const name of list(etc)) {
13682
14552
  if (!/^openssl(@[0-9.]+)?$/.test(name))
13683
14553
  continue;
13684
14554
  for (const f of ["openssl.cnf", "ct_log_list.cnf", "cert.pem"])
13685
- if (exists(join10(etc, name, f)))
13686
- files.push(join10(etc, name, f));
13687
- if (exists(join10(etc, name, "certs")))
13688
- dirs.push(join10(etc, name, "certs"));
14555
+ if (exists(join11(etc, name, f)))
14556
+ files.push(join11(etc, name, f));
14557
+ if (exists(join11(etc, name, "certs")))
14558
+ dirs.push(join11(etc, name, "certs"));
13689
14559
  }
13690
- if (exists(join10(etc, "ca-certificates/cert.pem")))
13691
- files.push(join10(etc, "ca-certificates/cert.pem"));
14560
+ if (exists(join11(etc, "ca-certificates/cert.pem")))
14561
+ files.push(join11(etc, "ca-certificates/cert.pem"));
13692
14562
  }
13693
14563
  return { files, dirs };
13694
14564
  }
@@ -13696,7 +14566,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
13696
14566
  var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
13697
14567
  function ancestors(p) {
13698
14568
  const out = [];
13699
- for (let d = dirname10(p);; d = dirname10(d)) {
14569
+ for (let d = dirname11(p);; d = dirname11(d)) {
13700
14570
  out.push(d);
13701
14571
  if (d === "/")
13702
14572
  break;
@@ -13732,9 +14602,9 @@ function buildMacProfile(i) {
13732
14602
  L.push(`(deny file-read* file-write* (literal ${sbString(s)}))`);
13733
14603
  for (const f of i.links)
13734
14604
  L.push(`(deny file-write* (literal ${sbString(f)}))`);
13735
- const guarded = new Set([i.project, join10(i.project, ".git"), ...i.nodeModules ?? []]);
13736
- for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join10(i.project, ".git")]) {
13737
- for (let d = dirname10(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname10(d))
14605
+ const guarded = new Set([i.project, join11(i.project, ".git"), ...i.nodeModules ?? []]);
14606
+ for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join11(i.project, ".git")]) {
14607
+ for (let d = dirname11(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname11(d))
13738
14608
  guarded.add(d);
13739
14609
  }
13740
14610
  for (const d of [...guarded].sort())
@@ -13803,10 +14673,10 @@ function buildBwrapArgs(i) {
13803
14673
  a.push("--bind", i.scratch, i.scratch);
13804
14674
  for (const d of i.nodeModules)
13805
14675
  ro(d);
13806
- if (exists(join10(i.project, ".git/hooks")))
13807
- ro(join10(i.project, ".git/hooks"));
13808
- if (exists(join10(i.project, ".git/config")))
13809
- ro(join10(i.project, ".git/config"));
14676
+ if (exists(join11(i.project, ".git/hooks")))
14677
+ ro(join11(i.project, ".git/hooks"));
14678
+ if (exists(join11(i.project, ".git/config")))
14679
+ ro(join11(i.project, ".git/config"));
13810
14680
  for (const f of i.links)
13811
14681
  ro(f);
13812
14682
  for (const s of i.sockets)
@@ -13836,7 +14706,7 @@ function removeTree(path) {
13836
14706
  return;
13837
14707
  chmodSync(p, 448);
13838
14708
  for (const e of readdirSync2(p))
13839
- walk(join10(p, e));
14709
+ walk(join11(p, e));
13840
14710
  } catch {}
13841
14711
  };
13842
14712
  walk(path);
@@ -13874,7 +14744,7 @@ function createShellSandbox(deps = {}) {
13874
14744
  if (deps.python !== undefined)
13875
14745
  return deps.python;
13876
14746
  const dev = platform === "darwin" ? macHost().developerDir : null;
13877
- return dev && existsSync5(join10(dev, "usr/bin/python3")) ? join10(dev, "usr/bin/python3") : null;
14747
+ return dev && existsSync5(join11(dev, "usr/bin/python3")) ? join11(dev, "usr/bin/python3") : null;
13878
14748
  };
13879
14749
  let cached;
13880
14750
  function probe() {
@@ -13992,13 +14862,13 @@ function createShellSandbox(deps = {}) {
13992
14862
  return { ready: false, reason: "a folder in the project cannot be read, so it cannot be checked for sockets and hard links" };
13993
14863
  let scratch;
13994
14864
  try {
13995
- scratch = mkdtempSync(join10(realpathSync4(tmpdir()), "freepi-hive-"));
14865
+ scratch = mkdtempSync(join11(realpathSync4(tmpdir()), "freepi-hive-"));
13996
14866
  const dir = scratch;
13997
- mkdirSync3(join10(dir, "home"));
13998
- mkdirSync3(join10(dir, "npm-cache"));
13999
- const emptyFile = join10(dir, "empty");
14000
- const marker = join10(dir, "call.fd");
14001
- writeFileSync3(emptyFile, "", { mode: 292 });
14867
+ mkdirSync4(join11(dir, "home"));
14868
+ mkdirSync4(join11(dir, "npm-cache"));
14869
+ const emptyFile = join11(dir, "empty");
14870
+ const marker = join11(dir, "call.fd");
14871
+ writeFileSync4(emptyFile, "", { mode: 292 });
14002
14872
  const roots = toolchainRoots(home);
14003
14873
  const mac = platform === "darwin";
14004
14874
  const host = mac ? macHost() : undefined;
@@ -14050,7 +14920,7 @@ function createShellSandbox(deps = {}) {
14050
14920
  } catch (err) {
14051
14921
  if (scratch)
14052
14922
  removeTree(scratch);
14053
- return { ready: false, reason: `could not prepare the sandbox: ${err instanceof Error ? err.message : String(err)}` };
14923
+ return { ready: false, reason: `Couldn't set up the command sandbox: ${sandboxCause(err)}.` };
14054
14924
  }
14055
14925
  },
14056
14926
  mark(toolCallId, mode) {
@@ -14147,6 +15017,44 @@ function installSafeUrlOpen(mode, open = openLink) {
14147
15017
  });
14148
15018
  }
14149
15019
 
15020
+ // src/retry-failed-line.ts
15021
+ var RETRY_FAILED = /^Retry failed after (\d+) attempts?: ([\s\S]*)$/;
15022
+ var CRASH_WARNING = /^\S+ crashed on /;
15023
+ var PRODUCT = "free-pi-cli";
15024
+ function installRetryFailedLine(mode) {
15025
+ const m = mode;
15026
+ const original = m.showError.bind(m);
15027
+ m.showError = (msg) => {
15028
+ const hit = typeof msg === "string" ? RETRY_FAILED.exec(msg) : null;
15029
+ if (!hit)
15030
+ return original(msg);
15031
+ const [, n, cause = ""] = hit;
15032
+ if (cause === "Retry cancelled")
15033
+ return m.showWarning("Retry cancelled.");
15034
+ const tries = `${n} ${n === "1" ? "try" : "tries"}`;
15035
+ m.showWarning(`Gave up after ${tries}: ${cause}`);
15036
+ };
15037
+ const warn = m.showWarning.bind(m);
15038
+ m.showWarning = (msg) => warn(typeof msg === "string" ? msg.replace(CRASH_WARNING, `${PRODUCT} crashed on `) : msg);
15039
+ for (const name of ["renderProjectTrustWarningIfNeeded", "suggestBugReport"]) {
15040
+ const orig = m[name]?.bind(m);
15041
+ if (!orig)
15042
+ continue;
15043
+ m[name] = () => {
15044
+ const before = m.chatContainer?.children?.length ?? 0;
15045
+ orig();
15046
+ for (const child of m.chatContainer?.children?.slice(before) ?? []) {
15047
+ const build = child.build;
15048
+ if (typeof build === "function")
15049
+ child.build = () => reword(build.call(child));
15050
+ }
15051
+ };
15052
+ }
15053
+ }
15054
+ function reword(text) {
15055
+ return text.replace(/restart pi\./, `restart ${PRODUCT}.`).replace(/looks like a pi bug/, "looks like a free-pi bug");
15056
+ }
15057
+
14150
15058
  // src/project-trust.ts
14151
15059
  import { createInterface } from "node:readline/promises";
14152
15060
  import { resolve as resolve3 } from "node:path";
@@ -14293,14 +15201,20 @@ var SDK_TOOLS = [
14293
15201
  "list_mcp_resource_templates",
14294
15202
  "read_mcp_resource"
14295
15203
  ];
14296
- var SKILLS_DIR = resolve4(dirname11(fileURLToPath4(import.meta.url)), "..", "skills");
15204
+ var SKILLS_DIR = resolve4(dirname12(fileURLToPath4(import.meta.url)), "..", "skills");
14297
15205
  function resolveModelName(opts) {
14298
15206
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
14299
15207
  }
14300
15208
  function buildSettingsManager(opts, models, projectTrusted) {
14301
- return SettingsManager2.inMemory({
14302
- defaultProvider: PROVIDER_NAME,
14303
- defaultModel: models[0].id,
15209
+ const saved = readDefaultModel(opts.agentDir);
15210
+ const known = new Map([
15211
+ [PROVIDER_NAME, new Set([...models.map((m) => m.id), FUSION_PICKER_ID, ...models.some((m) => m.id === ULTRAFAST_CATALOG_ID) ? [ULTRAFAST_PICKER_ID] : []])],
15212
+ [DEFENSE_PROVIDER_NAME, new Set(defenseModelsFor(opts).map((m) => m.id))]
15213
+ ]);
15214
+ const start = saved && known.get(saved.provider)?.has(saved.model) ? saved : undefined;
15215
+ const settings = {
15216
+ defaultProvider: start?.provider ?? PROVIDER_NAME,
15217
+ defaultModel: start?.model ?? models[0].id,
14304
15218
  defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
14305
15219
  packages: [],
14306
15220
  quietStartup: true,
@@ -14310,7 +15224,10 @@ function buildSettingsManager(opts, models, projectTrusted) {
14310
15224
  theme: "light/dark",
14311
15225
  enableInstallTelemetry: false,
14312
15226
  ...compactionSettingsFor(opts.costArm) ? { compaction: compactionSettingsFor(opts.costArm) } : {}
14313
- }, { projectTrusted });
15227
+ };
15228
+ const storage = new DefaultModelStorage(opts.agentDir);
15229
+ storage.withLock("global", () => JSON.stringify(settings, null, 2));
15230
+ return SettingsManager2.fromStorage(storage, { projectTrusted });
14314
15231
  }
14315
15232
  function settingsForRuntime(opts, rtCwd, launch) {
14316
15233
  return buildSettingsManager(opts, catalogModelsFor(opts), projectTrustForCwd(rtCwd, launch, opts.agentDir));
@@ -14334,7 +15251,6 @@ function buildRuntimeOptions(opts, sessionId) {
14334
15251
  name: "free-pi-provider",
14335
15252
  factory: (pi) => registerProviders(pi, sessionId)
14336
15253
  };
14337
- let advertiserUrl;
14338
15254
  const adsExtension = createAdsExtension({
14339
15255
  registerMeterRefresh: (refresh) => auth.setOnSignedIn(() => void refresh().catch(() => {})),
14340
15256
  baseUrl: opts.baseUrl,
@@ -14342,9 +15258,7 @@ function buildRuntimeOptions(opts, sessionId) {
14342
15258
  sessionId,
14343
15259
  clientVersion: CLI_VERSION,
14344
15260
  getGravityContext: opts.getGravityContext,
14345
- onBannerAd: (u) => {
14346
- advertiserUrl = u;
14347
- },
15261
+ adContext: opts.adContext,
14348
15262
  onMe: (me) => {
14349
15263
  updateAllowanceCache(me);
14350
15264
  }
@@ -14398,7 +15312,6 @@ function buildRuntimeOptions(opts, sessionId) {
14398
15312
  const commandsExtension = createFreePiCommandsExtension({
14399
15313
  baseUrl: opts.baseUrl,
14400
15314
  getToken: () => auth.getJwt(),
14401
- getAdvertiserUrl: () => advertiserUrl,
14402
15315
  credentialsPath: opts.credentialsPath,
14403
15316
  agentDir: opts.agentDir,
14404
15317
  isGuest: () => auth.isGuest(),
@@ -14429,12 +15342,21 @@ function buildRuntimeOptions(opts, sessionId) {
14429
15342
  const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
14430
15343
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
14431
15344
  const hiveRoom = { open: false };
15345
+ const remoteState = { remoteTurn: false, attached: false };
14432
15346
  const hiveExtension = createHiveExtension({
14433
15347
  baseUrl: opts.baseUrl,
14434
15348
  getToken: () => auth.getJwt(),
14435
15349
  knownTools: allowedTools,
14436
15350
  sandbox: shellSandbox,
14437
- roomState: hiveRoom
15351
+ roomState: hiveRoom,
15352
+ isRemoteAttached: () => remoteState.attached
15353
+ });
15354
+ const remoteExtension = createRemoteExtension({
15355
+ baseUrl: opts.baseUrl,
15356
+ getToken: async () => auth.getJwt(),
15357
+ sessionId,
15358
+ state: remoteState,
15359
+ isHiveOpen: () => hiveRoom.open
14438
15360
  });
14439
15361
  const choiceExtension = choiceOn ? createChoiceToolExtension({
14440
15362
  baseUrl: opts.baseUrl,
@@ -14442,7 +15364,7 @@ function buildRuntimeOptions(opts, sessionId) {
14442
15364
  sessionId,
14443
15365
  arm: () => opts.choiceArm ?? null,
14444
15366
  headless: !(tty.stdin && tty.stdout),
14445
- isRoomOpen: () => hiveRoom.open
15367
+ isRoomOpen: () => hiveRoom.open || remoteState.remoteTurn
14446
15368
  }) : undefined;
14447
15369
  const subagentsExtension = createSubagentsExtension({
14448
15370
  sessionId,
@@ -14487,6 +15409,7 @@ function buildRuntimeOptions(opts, sessionId) {
14487
15409
  toolSearchExtension,
14488
15410
  subagentsExtension,
14489
15411
  hiveExtension,
15412
+ remoteExtension,
14490
15413
  ...guestLoginExtension ? [guestLoginExtension] : [],
14491
15414
  ...opts.guest ? [createGuestNotesExtension({ isGuest: () => auth.isGuest() })] : []
14492
15415
  ];
@@ -14506,7 +15429,7 @@ ${CHOICE_RULES}` : agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [she
14506
15429
  function getSessionDir(cwd, agentDir) {
14507
15430
  const resolvedCwd = resolve4(cwd);
14508
15431
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
14509
- return join11(resolve4(agentDir), "sessions", safePath);
15432
+ return join12(resolve4(agentDir), "sessions", safePath);
14510
15433
  }
14511
15434
  async function findLatestSession(cwd, sessionDir, api = SessionManager2) {
14512
15435
  const infos = await api.list(cwd, sessionDir);
@@ -14608,11 +15531,13 @@ async function launchPi(opts) {
14608
15531
  installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
14609
15532
  installModelAllowanceHandler(mode);
14610
15533
  installSafeUrlOpen(mode);
15534
+ installRetryFailedLine(mode);
14611
15535
  installFooterRightAlign(mode, () => {
14612
15536
  const id = runtime.session?.model?.id;
14613
15537
  return freeModelBase(pickerModelName(opts, id));
14614
15538
  });
14615
15539
  installEditorPlaceholder(mode);
15540
+ installWidthGuard(mode);
14616
15541
  if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
14617
15542
  process.stdout.write("\x1B[2J\x1B[H");
14618
15543
  installBottomPin(mode);
@@ -14665,7 +15590,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
14665
15590
  // src/relaunch.ts
14666
15591
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
14667
15592
  import { constants as osConstants, tmpdir as tmpdir2 } from "node:os";
14668
- import { delimiter, dirname as dirname12, isAbsolute as isAbsolute3, join as join12, resolve as resolve5, sep as sep4, win32 } from "node:path";
15593
+ import { delimiter, dirname as dirname13, isAbsolute as isAbsolute3, join as join13, resolve as resolve5, sep as sep4, win32 } from "node:path";
14669
15594
  function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
14670
15595
  try {
14671
15596
  return statSync4(p).isDirectory();
@@ -14706,7 +15631,7 @@ function hasNodeModulesBin(entry) {
14706
15631
  }
14707
15632
  function sanitizedEnv(env, nodeDir, project, home, win) {
14708
15633
  const proj = canon(project, win);
14709
- const broad = proj === canon(home, win) || dirname12(proj) === proj;
15634
+ const broad = proj === canon(home, win) || dirname13(proj) === proj;
14710
15635
  const out = {};
14711
15636
  let pathKey = "PATH";
14712
15637
  let oldPath = "";
@@ -14743,13 +15668,13 @@ async function relaunchViaNpx(latest, deps) {
14743
15668
  if (!latest || !SEMVER.test(latest))
14744
15669
  return;
14745
15670
  const target = latest;
14746
- const nodeDir = dirname12(deps.execPath);
14747
- const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(dirname12(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
15671
+ const nodeDir = dirname13(deps.execPath);
15672
+ const npxCli = win ? join13(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join13(dirname13(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
14748
15673
  const exists = deps.exists ?? existsSync6;
14749
15674
  const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
14750
15675
  const healthy = (cli) => {
14751
- const root = join12(dirname12(cli), "..");
14752
- return exists(join12(root, "package.json")) && exists(join12(root, "lib", "cli.js"));
15676
+ const root = join13(dirname13(cli), "..");
15677
+ return exists(join13(root, "package.json")) && exists(join13(root, "lib", "cli.js"));
14753
15678
  };
14754
15679
  let npxCliPath = npxCli;
14755
15680
  if (!exists(npxCli)) {
@@ -14761,8 +15686,8 @@ async function relaunchViaNpx(latest, deps) {
14761
15686
  if (!healthy(npxCliPath))
14762
15687
  return;
14763
15688
  const childEnv = sanitizedEnv(env, nodeDir, deps.cwd, deps.homedir, win);
14764
- const readyDir = mkdtempSync2(join12(tmpdir2(), "free-pi-relaunch-"));
14765
- const readyFile = join12(readyDir, "started");
15689
+ const readyDir = mkdtempSync2(join13(tmpdir2(), "free-pi-relaunch-"));
15690
+ const readyFile = join13(readyDir, "started");
14766
15691
  try {
14767
15692
  deps.log(`Updating free-pi to ${target}…`);
14768
15693
  return await new Promise((resolve) => {
@@ -14804,13 +15729,13 @@ async function relaunchViaNpx(latest, deps) {
14804
15729
 
14805
15730
  // src/run.ts
14806
15731
  import { createInterface as createInterface3 } from "node:readline/promises";
14807
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
15732
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
14808
15733
  import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
14809
15734
 
14810
15735
  // src/onboarding.ts
14811
15736
  import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync as realpathSync6 } from "node:fs";
14812
15737
  import { homedir as homedir8 } from "node:os";
14813
- import { join as join13 } from "node:path";
15738
+ import { join as join14 } from "node:path";
14814
15739
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
14815
15740
  var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
14816
15741
  function introSkipped(env = process.env) {
@@ -14830,7 +15755,7 @@ function looksLikeProject(cwd, home = homedir8()) {
14830
15755
  try {
14831
15756
  if (realpathSync6(cwd) === realpathSync6(home))
14832
15757
  return false;
14833
- if (PROJECT_MARKERS.some((m) => existsSync7(join13(cwd, m))))
15758
+ if (PROJECT_MARKERS.some((m) => existsSync7(join14(cwd, m))))
14834
15759
  return true;
14835
15760
  return readdirSync3(cwd).some((f) => /\.(sln|csproj)$/.test(f));
14836
15761
  } catch {
@@ -14859,7 +15784,7 @@ ${text}
14859
15784
  // src/starter.ts
14860
15785
  import { existsSync as existsSync8, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
14861
15786
  import { homedir as homedir9 } from "node:os";
14862
- import { join as join14 } from "node:path";
15787
+ import { join as join15 } from "node:path";
14863
15788
  var STARTER_EXPERIMENT = "first_session_starter";
14864
15789
  var STARTER_PROJECT = [
14865
15790
  {
@@ -14892,7 +15817,7 @@ var STARTER_EMPTY = [
14892
15817
  var STARTER_HEADER_PROJECT = "Not sure where to start? Press 1, 2 or 3 to send one of these, or any other key to skip:";
14893
15818
  var STARTER_HEADER_EMPTY = "This folder is empty. Press 1, 2 or 3 to start with one of these, or any other key to skip:";
14894
15819
  function getStarterMarkerPath(agentDir = getFreePiAgentDir()) {
14895
- return join14(agentDir, "starter-shown");
15820
+ return join15(agentDir, "starter-shown");
14896
15821
  }
14897
15822
  function starterShownBefore(agentDir) {
14898
15823
  return existsSync8(getStarterMarkerPath(agentDir));
@@ -14924,12 +15849,12 @@ function parseStarterKey(raw, count) {
14924
15849
  }
14925
15850
 
14926
15851
  // src/whats-new.ts
14927
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
14928
- import { dirname as dirname13 } from "node:path";
15852
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
15853
+ import { dirname as dirname14 } from "node:path";
14929
15854
  function maybeWhatsNew(cliVersion, agentDir) {
14930
15855
  try {
14931
15856
  const path = getLastSeenVersionPath(agentDir);
14932
- const lastSeen = existsSync9(path) ? readFileSync6(path, "utf-8").trim() : undefined;
15857
+ const lastSeen = existsSync9(path) ? readFileSync7(path, "utf-8").trim() : undefined;
14933
15858
  writeLastSeen(path, cliVersion);
14934
15859
  if (lastSeen === undefined)
14935
15860
  return;
@@ -14945,8 +15870,8 @@ function maybeWhatsNew(cliVersion, agentDir) {
14945
15870
  }
14946
15871
  }
14947
15872
  function writeLastSeen(path, version) {
14948
- mkdirSync4(dirname13(path), { recursive: true });
14949
- writeFileSync4(path, version, "utf-8");
15873
+ mkdirSync5(dirname14(path), { recursive: true });
15874
+ writeFileSync5(path, version, "utf-8");
14950
15875
  }
14951
15876
 
14952
15877
  // src/run.ts
@@ -15197,7 +16122,7 @@ Your free-pi sign-in has expired. Signing in with GitHub again.
15197
16122
  deps.log(SIGNIN_UNFINISHED_LINE);
15198
16123
  return 0;
15199
16124
  }
15200
- const reason = err instanceof Error && err.message ? err.message : "unknown error";
16125
+ const reason = (err instanceof Error && err.message ? err.message : "unknown error").replace(/\.$/, "");
15201
16126
  deps.log(`Couldn't finish GitHub sign-in: ${reason}. Run npx free-pi-cli again to retry.`);
15202
16127
  return 1;
15203
16128
  }
@@ -15310,6 +16235,7 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
15310
16235
  getGravityContext = deps.startGravityPixel?.(jwt);
15311
16236
  } catch {}
15312
16237
  }
16238
+ const adContext = consentCurrent && process.env.FREEPI_NO_GRAVITY_PIXEL !== "1" ? { redact: redactSecrets } : undefined;
15313
16239
  if (me)
15314
16240
  updateAllowanceCache(me);
15315
16241
  guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
@@ -15393,8 +16319,8 @@ ${r.text}
15393
16319
  const choices = starterChoices();
15394
16320
  if (choices) {
15395
16321
  try {
15396
- mkdirSync5(deps.agentDir, { recursive: true });
15397
- writeFileSync5(getStarterMarkerPath(deps.agentDir), "1");
16322
+ mkdirSync6(deps.agentDir, { recursive: true });
16323
+ writeFileSync6(getStarterMarkerPath(deps.agentDir), "1");
15398
16324
  } catch {}
15399
16325
  const report = (event) => deps.reportStarterEvent ? deps.reportStarterEvent(event) : reportStarterEvent(deps, jwt, event);
15400
16326
  deps.log(formatStarterMenu(choices));
@@ -15435,6 +16361,7 @@ ${r.text}
15435
16361
  costArm: versionCheck.costArm,
15436
16362
  updateLatest,
15437
16363
  getGravityContext,
16364
+ adContext,
15438
16365
  guest: Boolean(me?.guest),
15439
16366
  refillCard: refillSession,
15440
16367
  allowanceLine: allowanceLine(),
@@ -15547,7 +16474,7 @@ async function main() {
15547
16474
  }
15548
16475
  if (process.env.FREEPI_RELAUNCH_READY) {
15549
16476
  try {
15550
- writeFileSync6(process.env.FREEPI_RELAUNCH_READY, "");
16477
+ writeFileSync7(process.env.FREEPI_RELAUNCH_READY, "");
15551
16478
  } catch {}
15552
16479
  }
15553
16480
  const command = parseCliArgs(process.argv.slice(2));
@@ -15608,7 +16535,7 @@ ${HELP_TEXT}`);
15608
16535
  launchPi,
15609
16536
  log: (message) => console.log(message),
15610
16537
  checkClientVersion: (baseUrl, cliVersion, jwt) => checkClientVersion(baseUrl, cliVersion, fetch, jwt),
15611
- startGravityPixel: (jwt) => startGravityPixel(jwt, join15(dirname14(agentDir), "gravity")),
16538
+ startGravityPixel: (jwt) => startGravityPixel(jwt, join16(dirname15(agentDir), "gravity")),
15612
16539
  relaunchLatest: (latest) => relaunchViaNpx(latest, {
15613
16540
  env: process.env,
15614
16541
  argv: process.argv.slice(2),