free-pi-cli 0.6.18 → 0.6.20

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.18",
518
+ version: "0.6.20",
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;
@@ -6583,14 +6584,15 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6583
6584
  function piMcpCliPath() {
6584
6585
  return join4(dirname5(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "extensions", "mcp", "cli.js");
6585
6586
  }
6587
+ var MCP_UNAVAILABLE = "free-pi-cli mcp isn't available in this install. Reinstall with npx free-pi-cli@latest.";
6586
6588
  async function loadPiRunner() {
6587
6589
  const path = piMcpCliPath();
6588
6590
  if (!existsSync2(path)) {
6589
- throw new Error(`free-pi-cli mcp is unavailable: the bundled pi package has no ${path}. Reinstall free-pi-cli.`);
6591
+ throw new Error(MCP_UNAVAILABLE);
6590
6592
  }
6591
6593
  const mod = await import(pathToFileURL(path).href);
6592
6594
  if (typeof mod.runMcpCommand !== "function") {
6593
- throw new Error(`free-pi-cli mcp is unavailable: ${path} does not export runMcpCommand. Reinstall free-pi-cli.`);
6595
+ throw new Error(MCP_UNAVAILABLE);
6594
6596
  }
6595
6597
  return mod.runMcpCommand;
6596
6598
  }
@@ -6608,7 +6610,9 @@ async function runFreePiMcp(args, deps) {
6608
6610
  error: (line) => deps.error(brandMcpLine(line))
6609
6611
  });
6610
6612
  } catch (err) {
6611
- deps.error(err instanceof Error ? err.message : String(err));
6613
+ const msg = (err instanceof Error ? err.message : String(err)).split(`
6614
+ at `)[0];
6615
+ deps.error(msg === MCP_UNAVAILABLE ? msg : `free-pi-cli mcp failed: ${msg}`);
6612
6616
  return 1;
6613
6617
  }
6614
6618
  }
@@ -6708,6 +6712,28 @@ function loadConfig(env = process.env) {
6708
6712
 
6709
6713
  // ../pi-ads/src/api.ts
6710
6714
  import { arch } from "node:os";
6715
+
6716
+ // ../pi-ads/src/turn.ts
6717
+ var TURN_HEADER = "x-turn-id";
6718
+ var current;
6719
+ function startTurn() {
6720
+ current = crypto.randomUUID();
6721
+ return current;
6722
+ }
6723
+ var currentTurnId = () => current;
6724
+ function createTurnHeaderExtension() {
6725
+ return {
6726
+ name: "free-pi-turn-header",
6727
+ factory(pi) {
6728
+ pi.on("before_provider_headers", (event) => {
6729
+ if (current)
6730
+ event.headers[TURN_HEADER] = current;
6731
+ });
6732
+ }
6733
+ };
6734
+ }
6735
+
6736
+ // ../pi-ads/src/api.ts
6711
6737
  var REQUEST_TIMEOUT_MS = 5000;
6712
6738
  async function authedFetch(deps, path, init) {
6713
6739
  const token = await deps.getToken();
@@ -6721,7 +6747,7 @@ async function authedFetch(deps, path, init) {
6721
6747
  Authorization: `Bearer ${token}`,
6722
6748
  "Content-Type": "application/json",
6723
6749
  ...deps.sessionId && (path.startsWith("/ads/next") || path === "/ads/impression") ? { "x-session-id": deps.sessionId } : {},
6724
- ...path.startsWith("/ads/next") ? { ...deps.clientVersion ? { "x-client-version": deps.clientVersion } : {}, "x-client-os": process.platform, "x-client-arch": arch(), ...localeHeaders(), ...gravityHeader(deps) } : {}
6750
+ ...path.startsWith("/ads/next") ? { ...currentTurnId() ? { "x-turn-id": currentTurnId() } : {}, ...deps.clientVersion ? { "x-client-version": deps.clientVersion } : {}, "x-client-os": process.platform, "x-client-arch": arch(), ...localeHeaders(), ...gravityHeader(deps) } : {}
6725
6751
  }
6726
6752
  });
6727
6753
  }
@@ -6760,9 +6786,9 @@ function fireGravityImpression(deps, ad) {
6760
6786
  } catch {}
6761
6787
  return true;
6762
6788
  }
6763
- async function fetchAdNext(deps, slot) {
6789
+ async function fetchAdNext(deps, slot, trigger) {
6764
6790
  try {
6765
- const res = await authedFetch(deps, `/ads/next?slot=${slot}`);
6791
+ const res = await authedFetch(deps, `/ads/next?slot=${slot}`, trigger ? { headers: { "x-ad-trigger": trigger } } : undefined);
6766
6792
  if (res.status === 204 || !res.ok)
6767
6793
  return;
6768
6794
  return await res.json();
@@ -6862,8 +6888,8 @@ function createBannerRenderer(opts = {}) {
6862
6888
  let lastPostedToken;
6863
6889
  const screen = opts.screen ?? {};
6864
6890
  return {
6865
- async render(deps, ctx, config) {
6866
- const ad = await fetchAdNext(deps, "banner");
6891
+ async render(deps, ctx, config, trigger) {
6892
+ const ad = await fetchAdNext(deps, "banner", trigger);
6867
6893
  if (opts.isActive && !opts.isActive())
6868
6894
  return;
6869
6895
  if (!ad) {
@@ -7129,7 +7155,7 @@ async function renderMeter(deps, ctx, opts = {}) {
7129
7155
  }
7130
7156
 
7131
7157
  // ../pi-ads/src/index.ts
7132
- var BANNER_REFRESH_MS = 10 * 60 * 1000;
7158
+ var REFRESH_ON_TURN_END = false;
7133
7159
  var BANNER_MIN_REFRESH_MS = 60 * 1000;
7134
7160
  function createAdsExtension(opts) {
7135
7161
  const deps = {
@@ -7149,51 +7175,79 @@ function createAdsExtension(opts) {
7149
7175
  const screen = {};
7150
7176
  const banner = createBannerRenderer({ onBannerAd: opts.onBannerAd, isActive: () => !stopped, screen });
7151
7177
  const inline = createInlineRenderer(screen);
7152
- let bannerInterval;
7178
+ let launchTurnUnused = false;
7179
+ let launchToken;
7180
+ let launchAt = 0;
7153
7181
  let lastBannerAt = 0;
7154
7182
  let bannerInFlight = false;
7155
- const renderBanner = async (ctx, force = false) => {
7183
+ const renderBanner = async (ctx, trigger, force = false) => {
7156
7184
  if (stopped)
7157
7185
  return;
7158
7186
  if (!force && (bannerInFlight || Date.now() - lastBannerAt < BANNER_MIN_REFRESH_MS))
7159
7187
  return;
7160
7188
  bannerInFlight = true;
7161
7189
  try {
7162
- await banner.render(deps, ctx, config);
7190
+ await banner.render(deps, ctx, config, trigger);
7163
7191
  } finally {
7164
7192
  bannerInFlight = false;
7165
7193
  lastBannerAt = Date.now();
7166
7194
  }
7167
7195
  };
7196
+ pi.on("before_provider_headers", (event) => {
7197
+ const id = currentTurnId();
7198
+ if (id)
7199
+ event.headers[TURN_HEADER] = id;
7200
+ });
7168
7201
  pi.on("session_start", async (_event, ctx) => {
7169
7202
  stopped = false;
7170
7203
  inline.reset(ctx);
7204
+ startTurn();
7205
+ launchTurnUnused = true;
7206
+ launchAt = Date.now();
7207
+ launchToken = await Promise.resolve(deps.getToken()).catch(() => {
7208
+ return;
7209
+ });
7171
7210
  opts.registerMeterRefresh?.(() => renderMeter(deps, ctx));
7172
- await Promise.all([renderBanner(ctx, true), renderMeter(deps, ctx)]);
7173
- if (bannerInterval)
7174
- clearInterval(bannerInterval);
7175
- bannerInterval = setInterval(() => {
7176
- renderBanner(ctx);
7177
- }, BANNER_REFRESH_MS);
7178
- bannerInterval.unref?.();
7211
+ await Promise.all([renderBanner(ctx, "launch", true), renderMeter(deps, ctx)]);
7212
+ });
7213
+ const LAUNCH_AD_MAX_AGE_MS = 60 * 60000;
7214
+ const newMessage = async (ctx) => {
7215
+ if (launchTurnUnused) {
7216
+ launchTurnUnused = false;
7217
+ const token = await Promise.resolve(deps.getToken()).catch(() => {
7218
+ return;
7219
+ });
7220
+ if (token === launchToken && Date.now() - launchAt < LAUNCH_AD_MAX_AGE_MS)
7221
+ return;
7222
+ }
7223
+ startTurn();
7224
+ renderBanner(ctx, "user_message", true);
7225
+ };
7226
+ pi.on("before_agent_start", async (event, ctx) => {
7227
+ if (event.messageType !== "user")
7228
+ return;
7229
+ await newMessage(ctx);
7230
+ });
7231
+ pi.on("input", (event, ctx) => {
7232
+ const e = event;
7233
+ if (e.streamingBehavior && e.source !== "extension")
7234
+ return newMessage(ctx).then(() => ({ action: "continue" }));
7235
+ return { action: "continue" };
7179
7236
  });
7180
7237
  pi.on("turn_end", async (_event, ctx) => {
7181
- renderBanner(ctx);
7182
- await Promise.all([inline.onTurnEnd(deps, ctx, config), renderMeter(deps, ctx)]);
7238
+ if (REFRESH_ON_TURN_END)
7239
+ renderBanner(ctx, "turn_end");
7240
+ await renderMeter(deps, ctx);
7183
7241
  });
7184
7242
  pi.on("session_shutdown", () => {
7185
7243
  stopped = true;
7186
- if (bannerInterval) {
7187
- clearInterval(bannerInterval);
7188
- bannerInterval = undefined;
7189
- }
7190
7244
  });
7191
7245
  }
7192
7246
  };
7193
7247
  }
7194
7248
 
7195
7249
  // src/pi-launch.ts
7196
- import { dirname as dirname11, join as join11, resolve as resolve4 } from "node:path";
7250
+ import { dirname as dirname12, join as join12, resolve as resolve4 } from "node:path";
7197
7251
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7198
7252
 
7199
7253
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
@@ -7523,6 +7577,28 @@ var integerKeyPattern = new RegExp("^(?:0|[1-9][0-9]*)$");
7523
7577
 
7524
7578
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/indexed/from_object.mjs
7525
7579
  var NumericKeyPattern = new RegExp(IntegerKey);
7580
+ // src/friendly-error.ts
7581
+ function friendlyThrown(e) {
7582
+ const name = e instanceof Error ? e.name : "";
7583
+ if (name === "TimeoutError")
7584
+ return "free-pi took too long to answer. Try again.";
7585
+ if (name === "AbortError")
7586
+ return "Cancelled.";
7587
+ if (name === "SyntaxError")
7588
+ return "free-pi sent an unexpected reply. Try again.";
7589
+ return "Couldn't reach free-pi — check your connection.";
7590
+ }
7591
+ function friendlyStatus(status, audience = "user", message) {
7592
+ if (status === 401) {
7593
+ 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.";
7594
+ }
7595
+ if (message?.trim())
7596
+ return message.trim();
7597
+ if (status >= 500)
7598
+ return `free-pi is having trouble right now (HTTP ${status}). Try again in a minute.`;
7599
+ return `free-pi could not complete the request (HTTP ${status}).`;
7600
+ }
7601
+
7526
7602
  // src/usage-tool.ts
7527
7603
  var USAGE_TOOL_NAME = "free_pi_usage";
7528
7604
  var PARAMS = _Object_({});
@@ -7580,11 +7656,11 @@ async function fetchUsageText(opts) {
7580
7656
  signal: AbortSignal.timeout(5000)
7581
7657
  });
7582
7658
  if (!res.ok)
7583
- return { ok: false, text: `Could not fetch usage (HTTP ${res.status}).` };
7659
+ return { ok: false, text: friendlyStatus(res.status) };
7584
7660
  const stats = await res.json();
7585
7661
  return { ok: true, text: formatStats(stats), stats };
7586
7662
  } catch (err) {
7587
- return { ok: false, text: `Could not fetch usage: ${String(err)}` };
7663
+ return { ok: false, text: friendlyThrown(err) };
7588
7664
  }
7589
7665
  }
7590
7666
  function createUsageToolExtension(opts) {
@@ -7652,11 +7728,9 @@ async function post(opts, body, signal) {
7652
7728
  const mapped = ERROR_TEXT[code];
7653
7729
  if (mapped)
7654
7730
  return { ok: false, text: mapped };
7655
- return { ok: false, text: `Request failed (HTTP ${res.status}): ${j.error?.message ?? j.message ?? "no message"}` };
7731
+ return { ok: false, text: friendlyStatus(res.status, "model", j.error?.message ?? j.message) };
7656
7732
  } catch (e) {
7657
- if (e instanceof Error && e.name === "TimeoutError")
7658
- return { ok: false, text: "Image generation timed out after 120 seconds." };
7659
- return { ok: false, text: `Request failed: ${String(e)}` };
7733
+ return { ok: false, text: friendlyThrown(e) };
7660
7734
  }
7661
7735
  }
7662
7736
  function slug(s) {
@@ -7733,7 +7807,8 @@ function createImageToolExtension(opts) {
7733
7807
  await fh.close();
7734
7808
  }
7735
7809
  } catch (e) {
7736
- return fail(`Could not save the image: ${String(e)}`);
7810
+ 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";
7811
+ return fail(`Couldn't save the image to images: ${why}.`);
7737
7812
  }
7738
7813
  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 });
7739
7814
  }
@@ -8342,11 +8417,9 @@ async function post2(opts, path, body, label, signal) {
8342
8417
  const mapped = ERROR_TEXT3[err.code ?? ""];
8343
8418
  if (mapped)
8344
8419
  return { ok: false, text: typeof mapped === "function" ? mapped(label) : mapped };
8345
- return { ok: false, text: `Request failed (HTTP ${res.status}): ${err.message ?? "no message"}` };
8420
+ return { ok: false, text: friendlyStatus(res.status, "model", err.message) };
8346
8421
  } catch (e) {
8347
- if (e instanceof Error && e.name === "TimeoutError")
8348
- return { ok: false, text: "Request timed out after 30 seconds." };
8349
- return { ok: false, text: `Request failed: ${String(e)}` };
8422
+ return { ok: false, text: friendlyThrown(e) };
8350
8423
  }
8351
8424
  }
8352
8425
  var done3 = (o) => ({ content: [{ type: "text", text: o.text }], details: o.details });
@@ -8415,11 +8488,11 @@ async function resolveBuyPage(opts) {
8415
8488
  signal: AbortSignal.timeout(5000)
8416
8489
  });
8417
8490
  if (!res.ok)
8418
- return { kind: "error", text: `Could not fetch the buy page (HTTP ${res.status}).` };
8491
+ return { kind: "error", text: friendlyStatus(res.status) };
8419
8492
  const me = await res.json();
8420
8493
  return me.buy_url ? { kind: "url", url: me.buy_url } : { kind: "unavailable" };
8421
8494
  } catch (err) {
8422
- return { kind: "error", text: `Could not fetch the buy page: ${String(err)}` };
8495
+ return { kind: "error", text: friendlyThrown(err) };
8423
8496
  }
8424
8497
  }
8425
8498
  var BUY_NOT_AVAILABLE_TEXT = "Buying credits is not available on this server yet.";
@@ -8930,6 +9003,142 @@ function createToolAliasesExtension(opts = {}) {
8930
9003
  };
8931
9004
  }
8932
9005
 
9006
+ // ../../node_modules/.bun/@earendil-works+pi-ai@1.2.0+a410056ef8f430f0/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js
9007
+ var OVERFLOW_PATTERNS = [
9008
+ /prompt (?:is )?too long/i,
9009
+ /prompt exceeds max length/i,
9010
+ /request_too_large/i,
9011
+ /input is too long for requested model/i,
9012
+ /exceeds the context window/i,
9013
+ /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
9014
+ /input token count.*exceeds the maximum/i,
9015
+ /maximum prompt length is \d+/i,
9016
+ /reduce the length of the messages/i,
9017
+ /maximum context length is \d+ tokens/i,
9018
+ /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
9019
+ /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i,
9020
+ /exceeds the limit of \d+/i,
9021
+ /exceeds the available context size/i,
9022
+ /greater than the context length/i,
9023
+ /context window exceeds limit/i,
9024
+ /exceeded model token limit/i,
9025
+ /too large for model with \d+ maximum context length/i,
9026
+ /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
9027
+ /model_context_window_exceeded/i,
9028
+ /prompt too long; exceeded (?:max )?context length/i,
9029
+ /range of input length should be/i,
9030
+ /context[_ ]length[_ ]exceeded/i,
9031
+ /too many tokens/i,
9032
+ /token limit exceeded/i
9033
+ ];
9034
+ var CEREBRAS_BODYLESS_OVERFLOW_PATTERN = /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i;
9035
+ var NON_OVERFLOW_PATTERNS = [
9036
+ /^(Throttling error|Service unavailable):/i,
9037
+ /rate limit/i,
9038
+ /too many requests/i
9039
+ ];
9040
+ function isContextOverflow(message, contextWindow) {
9041
+ if (message.stopReason === "error" && message.errorMessage) {
9042
+ const isNonOverflow = NON_OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage));
9043
+ if (!isNonOverflow) {
9044
+ if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage))) {
9045
+ return true;
9046
+ }
9047
+ if (message.provider === "cerebras" && CEREBRAS_BODYLESS_OVERFLOW_PATTERN.test(message.errorMessage)) {
9048
+ return true;
9049
+ }
9050
+ }
9051
+ }
9052
+ if (contextWindow && message.stopReason === "stop") {
9053
+ const inputTokens = message.usage.input + message.usage.cacheRead;
9054
+ if (inputTokens > contextWindow) {
9055
+ return true;
9056
+ }
9057
+ }
9058
+ if (contextWindow && message.stopReason === "length" && message.usage.output === 0) {
9059
+ const inputTokens = message.usage.input + message.usage.cacheRead;
9060
+ if (inputTokens >= contextWindow * 0.99) {
9061
+ return true;
9062
+ }
9063
+ }
9064
+ return false;
9065
+ }
9066
+
9067
+ // ../../node_modules/.bun/@earendil-works+pi-ai@1.2.0+a410056ef8f430f0/node_modules/@earendil-works/pi-ai/dist/utils/retry.js
9068
+ function buildProviderErrorPattern(patterns) {
9069
+ return new RegExp(patterns.join("|"), "i");
9070
+ }
9071
+ var NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([
9072
+ "GoUsageLimitError",
9073
+ "FreeUsageLimitError",
9074
+ "Monthly usage limit reached",
9075
+ "available balance",
9076
+ "insufficient_quota",
9077
+ "out of budget",
9078
+ "quota exceeded",
9079
+ "billing",
9080
+ "subscription_sharing_usage_limit_exceeded"
9081
+ ]);
9082
+ var RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
9083
+ "overloaded",
9084
+ "server_busy",
9085
+ "servers are currently busy",
9086
+ "currently experiencing high demand",
9087
+ "model is at capacity",
9088
+ "rate.?limit",
9089
+ "too many requests",
9090
+ "429",
9091
+ "500",
9092
+ "502",
9093
+ "503",
9094
+ "504",
9095
+ "520",
9096
+ "524",
9097
+ "service.?unavailable",
9098
+ "server.?error",
9099
+ "internal.?error",
9100
+ "provider.?returned.?(?:an.?)?error",
9101
+ "exceeded request buffer limit while retrying upstream",
9102
+ "network.?error",
9103
+ "connection.?error",
9104
+ "connection.?refused",
9105
+ "connection.?lost",
9106
+ "other side closed",
9107
+ "fetch failed",
9108
+ "getaddrinfo",
9109
+ "ENOTFOUND",
9110
+ "EAI_AGAIN",
9111
+ "upstream.?connect",
9112
+ "reset before headers",
9113
+ "socket hang up",
9114
+ "socket connection was closed",
9115
+ "timed? out",
9116
+ "timeout",
9117
+ "terminated",
9118
+ "websocket.?closed",
9119
+ "websocket.?error",
9120
+ "ended without",
9121
+ "stream ended before message_stop",
9122
+ "stream ended before a terminal response event",
9123
+ "http2 request did not get a response",
9124
+ "pending stream has been canceled",
9125
+ "retry delay",
9126
+ "you can retry your request",
9127
+ "try your request again",
9128
+ "please retry your request",
9129
+ "ResourceExhausted",
9130
+ "subscription_sharing_usage_unavailable",
9131
+ "subscription_sharing_user_unavailable"
9132
+ ]);
9133
+ function isRetryableAssistantError(message) {
9134
+ if (message.stopReason !== "error" || !message.errorMessage)
9135
+ return false;
9136
+ const errorMessage = message.errorMessage;
9137
+ if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage))
9138
+ return false;
9139
+ return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);
9140
+ }
9141
+
8933
9142
  // src/fusion.ts
8934
9143
  var FUSION_ID = "fusion";
8935
9144
  var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
@@ -9125,7 +9334,11 @@ function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
9125
9334
 
9126
9335
  // src/error-notice.ts
9127
9336
  var UPSTREAM_ERROR_FRAME_MESSAGE = "upstream returned an error frame";
9337
+ var PREMIUM_MODEL_NOTICE = "This model needs premium credits. Get some with /buy-credits, or switch to another model with /model.";
9128
9338
  var PROVIDER_FAILING_NOTICE = "This model's provider is failing right now. Try again in a moment, or switch models with /model.";
9339
+ var SIGNIN_EXPIRED_NOTICE = "Your free-pi sign-in has expired. Run /login, or restart free-pi-cli.";
9340
+ var CONNECTION_NOTICE = "Can't reach free-pi right now (connection error). Check your internet connection.";
9341
+ var CONTENT_FILTER_NOTICE = "The model's reply was blocked by a content filter. Try rephrasing your request.";
9129
9342
  var FREE_OFFER_TITLE = "Switch to the free model and resend this prompt?";
9130
9343
  var FREE_OFFER_ACCEPT = "Switch and resend";
9131
9344
  var FREE_OFFER_DECLINE = "Stay on this model";
@@ -9185,6 +9398,8 @@ function parseErrorEnvelope(errorMessage) {
9185
9398
  }
9186
9399
  }
9187
9400
  function formatLimitEnvelope(env, opts) {
9401
+ if (env.code === "model_requires_credits" && !/premium credit/i.test(env.message))
9402
+ return PREMIUM_MODEL_NOTICE;
9188
9403
  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") {
9189
9404
  return env.message;
9190
9405
  }
@@ -9230,6 +9445,36 @@ function lastUserEntryId(ctx) {
9230
9445
  }
9231
9446
  return "";
9232
9447
  }
9448
+ var RAW_ERROR_FIELD = "freePiRawError";
9449
+ function rawErrorMessage(msg) {
9450
+ if (!msg)
9451
+ return;
9452
+ const m = msg;
9453
+ return typeof m[RAW_ERROR_FIELD] === "string" ? m[RAW_ERROR_FIELD] : m.errorMessage;
9454
+ }
9455
+ function envelopeOf(raw) {
9456
+ return raw === UPSTREAM_ERROR_FRAME_MESSAGE ? { status: 502, code: "upstream_error", message: UPSTREAM_ERROR_FRAME_MESSAGE } : parseErrorEnvelope(raw);
9457
+ }
9458
+ var friendlyText = (env, timeZone) => env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : env.code === "unauthorized" ? SIGNIN_EXPIRED_NOTICE : formatLimitEnvelope(env, { timeZone });
9459
+ var isFreePiProvider = (provider) => provider === PROVIDER_NAME || provider === DEFENSE_PROVIDER_NAME;
9460
+ function friendlyRawText(raw) {
9461
+ const text = raw.trim();
9462
+ if (/^(?:connection error\.?|request timed out\.?|terminated|fetch failed)$/i.test(text))
9463
+ return CONNECTION_NOTICE;
9464
+ const http = /^(5\d\d)(?: status code \(no body\)$|\s*<(?:!doctype html|html)\b)/i.exec(text);
9465
+ if (http)
9466
+ return `${PROVIDER_FAILING_NOTICE} (HTTP ${http[1]})`;
9467
+ const finish = /^Provider finish_reason: (\w+)$/.exec(text);
9468
+ if (finish)
9469
+ return finish[1] === "content_filter" ? CONTENT_FILTER_NOTICE : `${PROVIDER_FAILING_NOTICE} (${finish[1].replace(/_/g, " ")})`;
9470
+ return;
9471
+ }
9472
+ function friendlyError(raw, timeZone) {
9473
+ if (typeof raw !== "string")
9474
+ return;
9475
+ const env = envelopeOf(raw);
9476
+ return env ? friendlyText(env, timeZone) : friendlyRawText(raw);
9477
+ }
9233
9478
  function createErrorNoticeExtension(opts = {}) {
9234
9479
  const lastAt = new Map;
9235
9480
  const now = opts.now ?? Date.now;
@@ -9262,21 +9507,44 @@ function createErrorNoticeExtension(opts = {}) {
9262
9507
  return {
9263
9508
  name: "free-pi-error-notice",
9264
9509
  factory(pi) {
9510
+ pi.on("message_end", (event) => {
9511
+ const msg = event.message;
9512
+ if (msg.role !== "assistant" || !isFreePiProvider(msg.provider) || msg.stopReason !== "error" || typeof msg.errorMessage !== "string")
9513
+ return;
9514
+ const env = envelopeOf(msg.errorMessage);
9515
+ const friendly = env ? friendlyText(env, timeZone) : friendlyRawText(msg.errorMessage);
9516
+ if (friendly === undefined)
9517
+ return;
9518
+ const same = (text) => {
9519
+ const next = { ...msg, errorMessage: text };
9520
+ return isRetryableAssistantError(next) === retryable && isContextOverflow(next, 0) === overflow;
9521
+ };
9522
+ const retryable = isRetryableAssistantError(msg);
9523
+ const overflow = isContextOverflow(msg, 0);
9524
+ let text = friendly;
9525
+ if (env && retryable && !isRetryableAssistantError({ ...msg, errorMessage: text }))
9526
+ text += ` (HTTP ${env.status})`;
9527
+ if (!same(text))
9528
+ return;
9529
+ return { message: { ...msg, errorMessage: text, [RAW_ERROR_FIELD]: msg.errorMessage } };
9530
+ });
9265
9531
  pi.on("turn_end", async (event, ctx) => {
9266
9532
  lastTurnEntryId = event.messageEntryId;
9267
9533
  const msg = event.message;
9268
9534
  if (!msg || msg.stopReason !== "error")
9269
9535
  return;
9270
- const env = msg.errorMessage === UPSTREAM_ERROR_FRAME_MESSAGE ? { status: 502, code: "upstream_error", message: UPSTREAM_ERROR_FRAME_MESSAGE } : parseErrorEnvelope(msg.errorMessage);
9536
+ const env = envelopeOf(rawErrorMessage(msg));
9271
9537
  if (!env)
9272
9538
  return;
9273
9539
  const t = now();
9274
9540
  if (t - (lastAt.get(env.code) ?? -Infinity) >= NOTICE_DEDUPE_MS) {
9275
9541
  lastAt.set(env.code, t);
9276
9542
  const extra = opts.extraNotice?.(env);
9277
- const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
9543
+ const shown = msg[RAW_ERROR_FIELD] !== undefined;
9544
+ const text = shown ? extra : friendlyText(env, timeZone) + (extra ? `
9278
9545
  ${extra}` : "");
9279
- ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
9546
+ if (text)
9547
+ ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
9280
9548
  }
9281
9549
  await offerFreeModel(pi, env, event.messageEntryId, ctx);
9282
9550
  });
@@ -9770,6 +10038,20 @@ function installBottomPin(mode) {
9770
10038
  return [...lines, ...Array(bottomPadding(lines.length, other, rows)).fill("")];
9771
10039
  };
9772
10040
  }
10041
+ var IMAGE_MARKS = ["\x1B_G", "\x1B]1337;File="];
10042
+ function clampLines(lines, width) {
10043
+ return lines.map((l) => IMAGE_MARKS.some((m) => l.includes(m)) || visibleWidth3(l) <= width ? l : truncateToWidth6(l, Math.max(0, width), ""));
10044
+ }
10045
+ function installWidthGuard(mode) {
10046
+ const m = mode;
10047
+ for (const k of ["headerContainer", "loadedResourcesContainer", "chatContainer", "pendingMessagesContainer", "statusContainer", "widgetContainerAbove", "editorContainer", "widgetContainerBelow", "footerContainer"]) {
10048
+ const c = m[k];
10049
+ if (!c)
10050
+ continue;
10051
+ const render = c.render.bind(c);
10052
+ c.render = (w) => clampLines(render(w), w);
10053
+ }
10054
+ }
9773
10055
 
9774
10056
  // src/guest-handoff.ts
9775
10057
  var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
@@ -9859,6 +10141,14 @@ function mergeAttribution(a, b) {
9859
10141
 
9860
10142
  // src/changelog.ts
9861
10143
  var CHANGELOG_HIGHLIGHTS = [
10144
+ {
10145
+ version: "0.6.20",
10146
+ highlights: ["Error messages are now plain sentences instead of raw server text."]
10147
+ },
10148
+ {
10149
+ version: "0.6.19",
10150
+ highlights: ["The ad banner now refreshes when you send a message, not on a timer."]
10151
+ },
9862
10152
  {
9863
10153
  version: "0.6.18",
9864
10154
  highlights: ["Ads load correctly from your first prompt."]
@@ -10219,6 +10509,7 @@ function highlightsFor(version) {
10219
10509
 
10220
10510
  // src/commands.ts
10221
10511
  import { access } from "node:fs/promises";
10512
+ import { basename as basename3 } from "node:path";
10222
10513
 
10223
10514
  // src/auth-flow.ts
10224
10515
  class UnauthorizedError extends Error {
@@ -10233,13 +10524,23 @@ async function postJson(baseUrl, path, body, extraHeaders = {}) {
10233
10524
  body: JSON.stringify(body)
10234
10525
  });
10235
10526
  }
10527
+ var NETWORK_TEXT = "Can't reach free-pi. Check your internet connection and try again.";
10528
+ var RATE_LIMIT_TEXT = "Too many sign-in attempts from your network. Wait a minute and try again.";
10529
+ 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.";
10530
+ async function parseReply(res, schema) {
10531
+ try {
10532
+ return schema.parse(await res.json());
10533
+ } catch (err) {
10534
+ throw new Error(UNEXPECTED_REPLY_TEXT, { cause: err });
10535
+ }
10536
+ }
10236
10537
  async function postJsonRetry(baseUrl, path, body, attempts = 4, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), extraHeaders = {}) {
10237
10538
  for (let attempt = 1;; attempt++) {
10238
10539
  try {
10239
10540
  return await postJson(baseUrl, path, body, extraHeaders);
10240
10541
  } catch (err) {
10241
10542
  if (attempt >= attempts) {
10242
- throw new Error(`network unreachable after ${attempts} attempts (${err instanceof Error ? err.message : String(err)})`);
10543
+ throw new Error(NETWORK_TEXT, { cause: err });
10243
10544
  }
10244
10545
  await sleep(1500 * attempt);
10245
10546
  }
@@ -10256,6 +10557,16 @@ function refusalMessage(status, body) {
10256
10557
  }
10257
10558
  return;
10258
10559
  }
10560
+ function failureText(status, body) {
10561
+ if (status === 429 || body?.code === "rate_limited" && status < 500)
10562
+ return RATE_LIMIT_TEXT;
10563
+ const refusal = refusalMessage(status, body);
10564
+ if (refusal)
10565
+ return refusal;
10566
+ if (status >= 500)
10567
+ return `free-pi is having trouble right now (HTTP ${status}). Try again in a minute.`;
10568
+ return `free-pi could not complete sign-in (HTTP ${status}). Try again in a minute.`;
10569
+ }
10259
10570
  async function readRefusal(res) {
10260
10571
  return await res.json().catch(() => {
10261
10572
  return;
@@ -10264,9 +10575,9 @@ async function readRefusal(res) {
10264
10575
  async function startDeviceFlow(baseUrl) {
10265
10576
  const res = await postJsonRetry(baseUrl, "/auth/github/device", {});
10266
10577
  if (!res.ok) {
10267
- throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `device flow start failed: HTTP ${res.status}`);
10578
+ throw new Error(failureText(res.status, await readRefusal(res)));
10268
10579
  }
10269
- return GithubDeviceResponseSchema.parse(await res.json());
10580
+ return parseReply(res, GithubDeviceResponseSchema);
10270
10581
  }
10271
10582
  async function startConnect(baseUrl, consentVersion, attribution = {}, client = {}) {
10272
10583
  const res = await postJsonRetry(baseUrl, "/auth/connect/start", {
@@ -10276,8 +10587,8 @@ async function startConnect(baseUrl, consentVersion, attribution = {}, client =
10276
10587
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
10277
10588
  }, undefined, undefined, { [SIGNIN_EXP_HEADER]: "1" });
10278
10589
  if (!res.ok)
10279
- throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `connect start failed: HTTP ${res.status}`);
10280
- return ConnectStartResponseSchema.parse(await res.json());
10590
+ throw new Error(failureText(res.status, await readRefusal(res)));
10591
+ return parseReply(res, ConnectStartResponseSchema);
10281
10592
  }
10282
10593
  async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {}) {
10283
10594
  const res = await postJsonRetry(baseUrl, "/auth/consent", {
@@ -10287,7 +10598,7 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
10287
10598
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
10288
10599
  });
10289
10600
  if (!res.ok) {
10290
- throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `consent submission failed: HTTP ${res.status}`);
10601
+ throw new Error(failureText(res.status, await readRefusal(res)));
10291
10602
  }
10292
10603
  }
10293
10604
  function deviceReport() {
@@ -10320,8 +10631,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
10320
10631
  throw new GuestUnavailableError(body?.reason ?? "disabled");
10321
10632
  }
10322
10633
  if (!res.ok)
10323
- throw new Error(`guest start failed: HTTP ${res.status}`);
10324
- const parsed = GuestStartResponseSchema.parse(await res.json());
10634
+ throw new Error(failureText(res.status, await readRefusal(res)));
10635
+ const parsed = await parseReply(res, GuestStartResponseSchema);
10325
10636
  return {
10326
10637
  token: parsed.token,
10327
10638
  allowance_usd: parsed.allowance_usd,
@@ -10401,7 +10712,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
10401
10712
  throw new PollAbortedError;
10402
10713
  consecutiveNetworkFailures++;
10403
10714
  if (consecutiveNetworkFailures >= 6) {
10404
- throw new Error(`token poll failed: network unreachable after ${consecutiveNetworkFailures} attempts (${err instanceof Error ? err.message : String(err)})`);
10715
+ throw new Error(NETWORK_TEXT, { cause: err });
10405
10716
  }
10406
10717
  await sleep(Math.max(waitSeconds, 5) * 1000);
10407
10718
  continue;
@@ -10419,9 +10730,9 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
10419
10730
  throw new LoginTimeoutError;
10420
10731
  if (isExpiredTokenPoll(body))
10421
10732
  throw new DeviceCodeExpiredError;
10422
- throw new Error(refusalMessage(res.status, body) ?? `token poll failed: HTTP ${res.status}`);
10733
+ throw new Error(failureText(res.status, body));
10423
10734
  }
10424
- const parsed = AuthTokenResponseSchema.parse(await res.json());
10735
+ const parsed = await parseReply(res, AuthTokenResponseSchema);
10425
10736
  if (opts.signal?.aborted)
10426
10737
  throw new PollAbortedError;
10427
10738
  if ("token" in parsed)
@@ -11395,7 +11706,7 @@ async function runLoginUnguarded(opts, ctx, lead) {
11395
11706
  ctx.ui.notify(LOGIN_DENIED_LINE, "info");
11396
11707
  return false;
11397
11708
  }
11398
- const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
11709
+ const reason = err instanceof Error && err.message ? `: ${err.message.replace(/\.$/, "")}` : "";
11399
11710
  ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
11400
11711
  return false;
11401
11712
  }
@@ -11413,6 +11724,26 @@ function installLoginHandler(mode, opts) {
11413
11724
  return runLogin(opts, { ui: { notify } });
11414
11725
  };
11415
11726
  }
11727
+ var IMPORT_NEXT_STEP = "Run /import again, or start a fresh session.";
11728
+ var FS_REASONS = {
11729
+ EACCES: "permission denied",
11730
+ EPERM: "permission denied",
11731
+ ENOENT: "not found",
11732
+ EISDIR: "that is a folder, not a file",
11733
+ EMFILE: "too many files are open",
11734
+ ENOSPC: "the disk is full"
11735
+ };
11736
+ function friendlyImportError(err, action = "import") {
11737
+ const e = err;
11738
+ const code = typeof e?.code === "string" && /^E[A-Z0-9]+$/.test(e.code) ? e.code : undefined;
11739
+ if (code) {
11740
+ const file = typeof e.path === "string" && e.path ? basename3(e.path) : undefined;
11741
+ const reason = FS_REASONS[code] ?? `error ${code}`;
11742
+ return `Couldn't ${action}${file ? ` ${file}` : ""}: ${reason}. ${IMPORT_NEXT_STEP}`;
11743
+ }
11744
+ const msg = (err instanceof Error ? err.message : String(err)).trim().replace(/[.\s]+$/, "");
11745
+ return `Couldn't ${action}: ${msg || "unknown error"}. ${IMPORT_NEXT_STEP}`;
11746
+ }
11416
11747
  function installImportHandler(mode, opts) {
11417
11748
  const m = mode;
11418
11749
  mode.handleImportCommand = async (text) => {
@@ -11435,11 +11766,11 @@ function installImportHandler(mode, opts) {
11435
11766
  for (let waited = 0;m.session.isCompacting && waited < 60000; waited += 100)
11436
11767
  await new Promise((r) => setTimeout(r, 100));
11437
11768
  const options = m.session.isStreaming ? { streamingBehavior: "followUp" } : undefined;
11438
- m.session.prompt(message, options).catch((err) => m.showError(err instanceof Error ? err.message : String(err)));
11769
+ m.session.prompt(message, options).catch((err) => m.showError(friendlyImportError(err, "send the import")));
11439
11770
  }
11440
11771
  });
11441
11772
  } catch (err) {
11442
- m.showError(`Couldn't import: ${err instanceof Error ? err.message : String(err)}`);
11773
+ m.showError(friendlyImportError(err));
11443
11774
  }
11444
11775
  };
11445
11776
  }
@@ -11587,7 +11918,7 @@ function createGuestLoginExtension(opts) {
11587
11918
  if (!opts.isGuest?.())
11588
11919
  return;
11589
11920
  const msg = event.message;
11590
- const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(msg.errorMessage) : undefined;
11921
+ const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(rawErrorMessage(msg)) : undefined;
11591
11922
  if (envelope && (envelope.code === "guest_allowance" || envelope.code === "guest_unavailable" || envelope.prompt === "github_signin")) {
11592
11923
  if (!inFlight && !signedIn) {
11593
11924
  const busy = envelope.code === "guest_allowance" && (envelope.remaining_credits_today ?? 0) > 0;
@@ -11697,7 +12028,7 @@ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync a
11697
12028
  import { homedir as homedir4 } from "node:os";
11698
12029
  import { dirname as dirname8 } from "node:path";
11699
12030
  import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
11700
- import { Box, Text as Text2, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
12031
+ import { Box, Text as Text2, truncateToWidth as truncateToWidth7, visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
11701
12032
  var TOOL_MODES = ["compact", "full", "hidden"];
11702
12033
  var TOOLS_USAGE = "Usage: /tools compact|full|hidden (compact: one line per tool call; full: every call in full; hidden: just a count)";
11703
12034
  function readToolMode(path) {
@@ -11797,12 +12128,30 @@ function toolLines(i) {
11797
12128
  }
11798
12129
  return done;
11799
12130
  }
12131
+ var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
12132
+ function wrapByWidth(text, room) {
12133
+ const clean = text.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[ -\/]*[@-~]/g, "").replace(/\t/g, " ").replace(/[\x00-\x08\x0a-\x1f\x7f-\x9f]/g, "");
12134
+ const pieces = [];
12135
+ let cur = "";
12136
+ let curW = 0;
12137
+ for (const { segment } of graphemes.segment(clean)) {
12138
+ const w = visibleWidth4(segment);
12139
+ if (curW + w > room && cur) {
12140
+ pieces.push(cur);
12141
+ cur = "";
12142
+ curW = 0;
12143
+ }
12144
+ cur += segment;
12145
+ curW += w;
12146
+ }
12147
+ return [...pieces, cur];
12148
+ }
11800
12149
  function component(rows, theme, pad) {
11801
12150
  return {
11802
12151
  render: (w) => rows.flatMap((r) => {
11803
12152
  const room = Math.max(1, w - pad);
11804
- const pieces = r.raw ? Array.from(r.text).join("").match(new RegExp(`.{1,${room}}`, "gu")) ?? [""] : [truncateToWidth7(r.text, room, "…")];
11805
- return pieces.map((t) => " ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t));
12153
+ const pieces = r.raw ? wrapByWidth(r.text, room) : [truncateToWidth7(r.text, room, "…")];
12154
+ return pieces.map((t) => truncateToWidth7(" ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t), Math.max(0, w), ""));
11806
12155
  }),
11807
12156
  invalidate() {}
11808
12157
  };
@@ -12074,16 +12423,63 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
12074
12423
  return scoped;
12075
12424
  }
12076
12425
 
12426
+ // src/default-model.ts
12427
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
12428
+ import { dirname as dirname9, join as join8 } from "node:path";
12429
+ var getDefaultModelPath = (agentDir) => join8(agentDir, "default-model.json");
12430
+ function readDefaultModel(agentDir) {
12431
+ if (!agentDir)
12432
+ return;
12433
+ try {
12434
+ const v = JSON.parse(readFileSync5(getDefaultModelPath(agentDir), "utf8"));
12435
+ return typeof v.provider === "string" && typeof v.model === "string" && v.provider && v.model ? { provider: v.provider, model: v.model } : undefined;
12436
+ } catch {
12437
+ return;
12438
+ }
12439
+ }
12440
+
12441
+ class DefaultModelStorage {
12442
+ agentDir;
12443
+ global;
12444
+ project;
12445
+ constructor(agentDir) {
12446
+ this.agentDir = agentDir;
12447
+ }
12448
+ withLock(scope, fn) {
12449
+ const next = fn(scope === "global" ? this.global : this.project);
12450
+ if (next === undefined)
12451
+ return;
12452
+ if (scope === "project") {
12453
+ this.project = next;
12454
+ return;
12455
+ }
12456
+ const before = this.global;
12457
+ this.global = next;
12458
+ if (before === undefined || !this.agentDir)
12459
+ return;
12460
+ try {
12461
+ const a = JSON.parse(before);
12462
+ const b = JSON.parse(next);
12463
+ if (a.defaultProvider === b.defaultProvider && a.defaultModel === b.defaultModel)
12464
+ return;
12465
+ if (!b.defaultProvider || !b.defaultModel)
12466
+ return;
12467
+ mkdirSync3(dirname9(getDefaultModelPath(this.agentDir)), { recursive: true });
12468
+ writeFileSync3(getDefaultModelPath(this.agentDir), JSON.stringify({ provider: b.defaultProvider, model: b.defaultModel }));
12469
+ } catch {}
12470
+ }
12471
+ }
12472
+
12077
12473
  // src/hive.ts
12078
12474
  import { createHash as createHash3 } from "node:crypto";
12079
12475
  import { execFileSync } from "node:child_process";
12080
- import { lstatSync as lstatSync2, readFileSync as readFileSync5, readlinkSync as readlinkSync2 } from "node:fs";
12081
- import { join as join9 } from "node:path";
12476
+ import { lstatSync as lstatSync2, readFileSync as readFileSync6, readlinkSync as readlinkSync2 } from "node:fs";
12477
+ import { join as join10 } from "node:path";
12082
12478
 
12083
12479
  // src/hive-guard.ts
12084
12480
  import { existsSync as existsSync4, lstatSync, readdirSync, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
12085
12481
  import { homedir as homedir5 } from "node:os";
12086
- 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";
12482
+ 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";
12087
12483
  import { fileURLToPath as fileURLToPath3 } from "node:url";
12088
12484
 
12089
12485
  // src/subagents.ts
@@ -12200,8 +12596,10 @@ function lastAssistant(session) {
12200
12596
  continue;
12201
12597
  const text = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
12202
12598
  `).trim() : "";
12203
- if (m.stopReason === "error")
12204
- return { text, error: m.errorMessage || "the model request failed" };
12599
+ if (m.stopReason === "error") {
12600
+ const raw = rawErrorMessage(m);
12601
+ return { text, error: (raw && isFreePiProvider(m.provider) && !raw.includes("subagents_unavailable") ? friendlyError(raw) : undefined) ?? (raw || "the model request failed") };
12602
+ }
12205
12603
  if (m.stopReason === "aborted")
12206
12604
  return { text, error: "aborted" };
12207
12605
  return { text };
@@ -12592,7 +12990,7 @@ var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
12592
12990
  var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
12593
12991
  var CLASSIFIED = ["bash", "write", "edit"];
12594
12992
  var SECRET_BASENAME = [/^\.env(\..*)?$/, /\.pem$/, /^id_(rsa|dsa|ecdsa|ed25519)/];
12595
- var SECRET_HOME_DIRS = [".ssh", ".aws", join8(".config", "freepi"), ".free-pi"];
12993
+ var SECRET_HOME_DIRS = [".ssh", ".aws", join9(".config", "freepi"), ".free-pi"];
12596
12994
  var SECRET_IN_TEXT = /(?:^|[\s"'=:/])\.env(?:\.[\w.-]+)?(?=$|[\s"';|&)])|\.pem\b|\bid_(?:rsa|dsa|ecdsa|ed25519)|\.ssh\b|\.aws\b|\.config\/freepi|\.free-pi\b/;
12597
12995
  var BASH_DENY = [
12598
12996
  [/\brm\s+(?:-\S+\s+)*(?:\/|~|\$HOME)\*?(?:\s|$)/, "destructive command"],
@@ -12615,7 +13013,7 @@ function protectedLinkTargets(root, limit) {
12615
13013
  const note = (link) => {
12616
13014
  let target;
12617
13015
  try {
12618
- target = resolve2(dirname9(link), readlinkSync(link));
13016
+ target = resolve2(dirname10(link), readlinkSync(link));
12619
13017
  } catch {
12620
13018
  return;
12621
13019
  }
@@ -12625,7 +13023,7 @@ function protectedLinkTargets(root, limit) {
12625
13023
  out.push(real);
12626
13024
  };
12627
13025
  for (const entry of [".git/hooks", ".git/config"]) {
12628
- const link = join8(root, entry);
13026
+ const link = join9(root, entry);
12629
13027
  try {
12630
13028
  if (lstatSync(link).isSymbolicLink())
12631
13029
  note(link);
@@ -12644,7 +13042,7 @@ function protectedLinkTargets(root, limit) {
12644
13042
  for (const entry of entries) {
12645
13043
  if (++seen > limit)
12646
13044
  return null;
12647
- const path = join8(dir, entry.name);
13045
+ const path = join9(dir, entry.name);
12648
13046
  if (entry.isSymbolicLink()) {
12649
13047
  if (protectedByName(relative2(root, path)))
12650
13048
  note(path);
@@ -12694,7 +13092,7 @@ function resolveLikePi(raw, cwd, home, platform) {
12694
13092
  if (p === "~")
12695
13093
  p = home;
12696
13094
  else if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
12697
- p = join8(home, p.slice(2));
13095
+ p = join9(home, p.slice(2));
12698
13096
  if (/^file:\/\//.test(p))
12699
13097
  p = fileURLToPath3(p);
12700
13098
  return resolve2(cwd, p);
@@ -12704,17 +13102,17 @@ function realpathDeepest(p) {
12704
13102
  const rest = [];
12705
13103
  for (;; ) {
12706
13104
  try {
12707
- return join8(realpathSync3(current), ...rest.reverse());
13105
+ return join9(realpathSync3(current), ...rest.reverse());
12708
13106
  } catch {
12709
13107
  try {
12710
13108
  lstatSync(current);
12711
13109
  return null;
12712
13110
  } catch {}
12713
13111
  }
12714
- const parent = dirname9(current);
13112
+ const parent = dirname10(current);
12715
13113
  if (parent === current)
12716
13114
  return null;
12717
- rest.push(basename3(current));
13115
+ rest.push(basename4(current));
12718
13116
  current = parent;
12719
13117
  }
12720
13118
  }
@@ -12723,10 +13121,10 @@ function isInside(root, target) {
12723
13121
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
12724
13122
  }
12725
13123
  function isSecret(target, home) {
12726
- if (SECRET_BASENAME.some((re) => re.test(basename3(target))))
13124
+ if (SECRET_BASENAME.some((re) => re.test(basename4(target))))
12727
13125
  return true;
12728
13126
  return SECRET_HOME_DIRS.some((dir) => {
12729
- const lexical = join8(home, dir);
13127
+ const lexical = join9(home, dir);
12730
13128
  return isInside(lexical, target) || isInside(realpathDeepest(lexical) ?? lexical, target);
12731
13129
  });
12732
13130
  }
@@ -12747,7 +13145,7 @@ function scanForSecret(dir) {
12747
13145
  if (SECRET_BASENAME.some((re) => re.test(entry.name)))
12748
13146
  return "yes";
12749
13147
  if (entry.isDirectory())
12750
- queue.push(join8(current, entry.name));
13148
+ queue.push(join9(current, entry.name));
12751
13149
  }
12752
13150
  }
12753
13151
  return "no";
@@ -12871,6 +13269,33 @@ async function decideToolCall(event, opts) {
12871
13269
  }
12872
13270
 
12873
13271
  // src/hive.ts
13272
+ var HIVE_ERRORS = new Map(Object.entries({
13273
+ rate_limited: "Hive: slow down, too many messages in a short time.",
13274
+ pending: "Hive: another request is still waiting for an answer.",
13275
+ not_driver: "Hive: only the person driving the room can do that.",
13276
+ already_driver: "Hive: you are already driving the room.",
13277
+ unknown_conn: "Hive: that person has left the room.",
13278
+ unknown_prompt: "Hive: that prompt is no longer waiting.",
13279
+ unknown_request: "Hive: that request is no longer waiting.",
13280
+ bad_frame: "Hive: the room got a message it could not read. Try again.",
13281
+ internal: "Hive: something went wrong on the server. Try again."
13282
+ }));
13283
+ var HIVE_CLOSED = new Map(Object.entries({
13284
+ replaced: "Hive room closed: you opened a newer room.",
13285
+ host_left: "Hive room closed: the host left.",
13286
+ ended: "Hive room closed: the host ended it.",
13287
+ expired: "Hive room closed: it expired.",
13288
+ kicked: "Hive room closed: you were removed from it.",
13289
+ shutdown: "Hive room closed: the server is restarting.",
13290
+ no_room: "Hive room closed: the room no longer exists.",
13291
+ unauthorized: "Hive room closed: you are not signed in.",
13292
+ auth_timeout: "Hive room closed: sign-in took too long.",
13293
+ busy: "Hive room closed: the server is busy. Try again soon.",
13294
+ overloaded: "Hive room closed: the server is busy. Try again soon.",
13295
+ slow: "Hive room closed: the connection was too slow."
13296
+ }));
13297
+ var hiveErrorText = (code) => HIVE_ERRORS.get(code) ?? `Hive: ${code}`;
13298
+ var hiveClosedText = (reason) => HIVE_CLOSED.get(reason) ?? `Hive room closed (${reason}).`;
12874
13299
  var RUN_IN_SANDBOX = "Run in sandbox";
12875
13300
  var RUN_OUTSIDE = "Run outside the sandbox";
12876
13301
  var DENY = "Deny";
@@ -12894,7 +13319,7 @@ function fingerprint(path) {
12894
13319
  return "";
12895
13320
  if (st.size > HASH_MAX_BYTES)
12896
13321
  return `${st.size}:${st.mtimeMs}`;
12897
- return createHash3("sha256").update(readFileSync5(path)).digest("hex");
13322
+ return createHash3("sha256").update(readFileSync6(path)).digest("hex");
12898
13323
  } catch {
12899
13324
  return "";
12900
13325
  }
@@ -12924,7 +13349,7 @@ function worktreeSnapshot(g) {
12924
13349
  const status = entry.slice(0, 2);
12925
13350
  if (status[0] === "R" || status[0] === "C")
12926
13351
  i++;
12927
- files.set(entry.slice(3), `${status}:${fingerprint(join9(root, entry.slice(3)))}`);
13352
+ files.set(entry.slice(3), `${status}:${fingerprint(join10(root, entry.slice(3)))}`);
12928
13353
  }
12929
13354
  return { files, head: runGit(g, "rev-parse", "HEAD")?.trim() };
12930
13355
  } catch (e) {
@@ -13086,10 +13511,13 @@ function createHiveExtension(opts) {
13086
13511
  };
13087
13512
  socket.onopen = () => socket.send(JSON.stringify({ type: "auth", token }));
13088
13513
  socket.onerror = failed;
13089
- socket.onclose = () => {
13514
+ socket.onclose = (event) => {
13090
13515
  if (ws !== socket)
13091
13516
  return;
13092
- if (!roomUrl)
13517
+ const known = event?.reason ? HIVE_CLOSED.get(event.reason) : undefined;
13518
+ if (known)
13519
+ endRoom(known);
13520
+ else if (!roomUrl)
13093
13521
  failed();
13094
13522
  else
13095
13523
  endRoom("Hive room closed.");
@@ -13163,10 +13591,10 @@ Guests sign in with GitHub to watch and chat. They can ask to drive; you approve
13163
13591
  break;
13164
13592
  case "error":
13165
13593
  if (frame.code !== "stale")
13166
- notify(`Hive: ${frame.code}`, "warning");
13594
+ notify(hiveErrorText(frame.code), "warning");
13167
13595
  break;
13168
13596
  case "closed":
13169
- endRoom(`Hive room closed (${frame.reason}).`);
13597
+ endRoom(hiveClosedText(frame.reason));
13170
13598
  break;
13171
13599
  }
13172
13600
  }
@@ -13383,16 +13811,26 @@ import {
13383
13811
  chmodSync,
13384
13812
  existsSync as existsSync5,
13385
13813
  lstatSync as lstatSync3,
13386
- mkdirSync as mkdirSync3,
13814
+ mkdirSync as mkdirSync4,
13387
13815
  mkdtempSync,
13388
13816
  readdirSync as readdirSync2,
13389
13817
  realpathSync as realpathSync4,
13390
13818
  rmSync,
13391
13819
  statSync as statSync3,
13392
- writeFileSync as writeFileSync3
13820
+ writeFileSync as writeFileSync4
13393
13821
  } from "node:fs";
13394
13822
  import { homedir as homedir6, tmpdir } from "node:os";
13395
- import { dirname as dirname10, join as join10 } from "node:path";
13823
+ import { dirname as dirname11, join as join11 } from "node:path";
13824
+ var ERRNO_WORDS = { EACCES: "permission denied", EPERM: "operation not permitted", ENOENT: "not found", ENOSPC: "disk full", EROFS: "read-only file system" };
13825
+ function sandboxCause(err) {
13826
+ const e = err;
13827
+ if (typeof e?.code === "string") {
13828
+ const where = [e.syscall, e.path].filter((x) => typeof x === "string").join(" ");
13829
+ return `${Object.hasOwn(ERRNO_WORDS, e.code) ? ERRNO_WORDS[e.code] : e.code}${where ? ` (${where})` : ""}`;
13830
+ }
13831
+ return (err instanceof Error ? err.message : String(err)).split(`
13832
+ `)[0].replace(/\.$/, "");
13833
+ }
13396
13834
  var SECRET_NAME_PATTERNS = [
13397
13835
  "\\.env(\\..*)?",
13398
13836
  "[^/]*\\.pem",
@@ -13428,7 +13866,7 @@ var TOOLCHAIN_ROOTS = [
13428
13866
  "Library/pnpm"
13429
13867
  ];
13430
13868
  function toolchainRoots(home, exists = existsSync5) {
13431
- return TOOLCHAIN_ROOTS.map((r) => join10(home, r)).filter((p) => exists(p));
13869
+ return TOOLCHAIN_ROOTS.map((r) => join11(home, r)).filter((p) => exists(p));
13432
13870
  }
13433
13871
  var ENV_ALLOW = ["PATH", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "TZ", "USER", "LOGNAME", "SHELL", "PI_PROVIDER", "PI_MODEL"];
13434
13872
  function under(path, dir) {
@@ -13446,10 +13884,10 @@ function sandboxEnv(base, o) {
13446
13884
  for (const k of ENV_ALLOW)
13447
13885
  if (base[k] !== undefined)
13448
13886
  env[k] = base[k];
13449
- 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(":");
13887
+ 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(":");
13450
13888
  if (o.developerDir)
13451
13889
  env.DEVELOPER_DIR = o.developerDir;
13452
- env.HOME = join10(o.scratch, "home");
13890
+ env.HOME = join11(o.scratch, "home");
13453
13891
  env.TMP = env.TEMP = o.scratch;
13454
13892
  env.TMPDIR = `${o.scratch}/`;
13455
13893
  env.BUN_OPTIONS = "--no-env-file";
@@ -13459,7 +13897,7 @@ function sandboxEnv(base, o) {
13459
13897
  env.GIT_CONFIG_COUNT = "1";
13460
13898
  env.GIT_CONFIG_KEY_0 = "credential.helper";
13461
13899
  env.GIT_CONFIG_VALUE_0 = "";
13462
- env.npm_config_cache = join10(o.scratch, "npm-cache");
13900
+ env.npm_config_cache = join11(o.scratch, "npm-cache");
13463
13901
  env.FREEPI_HIVE_CALL = o.callId;
13464
13902
  return env;
13465
13903
  }
@@ -13505,7 +13943,7 @@ function scanProject(project, limit = SCAN_LIMIT2) {
13505
13943
  scan.overflow = true;
13506
13944
  break;
13507
13945
  }
13508
- const path = join10(dir, e.name);
13946
+ const path = join11(dir, e.name);
13509
13947
  if (e.isSymbolicLink())
13510
13948
  continue;
13511
13949
  if (e.isDirectory()) {
@@ -13605,7 +14043,7 @@ function developerDir(o = {}) {
13605
14043
  continue;
13606
14044
  try {
13607
14045
  const real = realpath(c);
13608
- if (exists(join10(real, "usr/bin/git")))
14046
+ if (exists(join11(real, "usr/bin/git")))
13609
14047
  return real;
13610
14048
  } catch {}
13611
14049
  }
@@ -13623,18 +14061,18 @@ function opensslFiles(o = {}) {
13623
14061
  const files = [];
13624
14062
  const dirs = [];
13625
14063
  for (const prefix of o.prefixes ?? ["/opt/homebrew", "/usr/local"]) {
13626
- const etc = join10(prefix, "etc");
14064
+ const etc = join11(prefix, "etc");
13627
14065
  for (const name of list(etc)) {
13628
14066
  if (!/^openssl(@[0-9.]+)?$/.test(name))
13629
14067
  continue;
13630
14068
  for (const f of ["openssl.cnf", "ct_log_list.cnf", "cert.pem"])
13631
- if (exists(join10(etc, name, f)))
13632
- files.push(join10(etc, name, f));
13633
- if (exists(join10(etc, name, "certs")))
13634
- dirs.push(join10(etc, name, "certs"));
14069
+ if (exists(join11(etc, name, f)))
14070
+ files.push(join11(etc, name, f));
14071
+ if (exists(join11(etc, name, "certs")))
14072
+ dirs.push(join11(etc, name, "certs"));
13635
14073
  }
13636
- if (exists(join10(etc, "ca-certificates/cert.pem")))
13637
- files.push(join10(etc, "ca-certificates/cert.pem"));
14074
+ if (exists(join11(etc, "ca-certificates/cert.pem")))
14075
+ files.push(join11(etc, "ca-certificates/cert.pem"));
13638
14076
  }
13639
14077
  return { files, dirs };
13640
14078
  }
@@ -13642,7 +14080,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
13642
14080
  var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
13643
14081
  function ancestors(p) {
13644
14082
  const out = [];
13645
- for (let d = dirname10(p);; d = dirname10(d)) {
14083
+ for (let d = dirname11(p);; d = dirname11(d)) {
13646
14084
  out.push(d);
13647
14085
  if (d === "/")
13648
14086
  break;
@@ -13678,9 +14116,9 @@ function buildMacProfile(i) {
13678
14116
  L.push(`(deny file-read* file-write* (literal ${sbString(s)}))`);
13679
14117
  for (const f of i.links)
13680
14118
  L.push(`(deny file-write* (literal ${sbString(f)}))`);
13681
- const guarded = new Set([i.project, join10(i.project, ".git"), ...i.nodeModules ?? []]);
13682
- for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join10(i.project, ".git")]) {
13683
- for (let d = dirname10(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname10(d))
14119
+ const guarded = new Set([i.project, join11(i.project, ".git"), ...i.nodeModules ?? []]);
14120
+ for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join11(i.project, ".git")]) {
14121
+ for (let d = dirname11(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname11(d))
13684
14122
  guarded.add(d);
13685
14123
  }
13686
14124
  for (const d of [...guarded].sort())
@@ -13749,10 +14187,10 @@ function buildBwrapArgs(i) {
13749
14187
  a.push("--bind", i.scratch, i.scratch);
13750
14188
  for (const d of i.nodeModules)
13751
14189
  ro(d);
13752
- if (exists(join10(i.project, ".git/hooks")))
13753
- ro(join10(i.project, ".git/hooks"));
13754
- if (exists(join10(i.project, ".git/config")))
13755
- ro(join10(i.project, ".git/config"));
14190
+ if (exists(join11(i.project, ".git/hooks")))
14191
+ ro(join11(i.project, ".git/hooks"));
14192
+ if (exists(join11(i.project, ".git/config")))
14193
+ ro(join11(i.project, ".git/config"));
13756
14194
  for (const f of i.links)
13757
14195
  ro(f);
13758
14196
  for (const s of i.sockets)
@@ -13782,7 +14220,7 @@ function removeTree(path) {
13782
14220
  return;
13783
14221
  chmodSync(p, 448);
13784
14222
  for (const e of readdirSync2(p))
13785
- walk(join10(p, e));
14223
+ walk(join11(p, e));
13786
14224
  } catch {}
13787
14225
  };
13788
14226
  walk(path);
@@ -13820,7 +14258,7 @@ function createShellSandbox(deps = {}) {
13820
14258
  if (deps.python !== undefined)
13821
14259
  return deps.python;
13822
14260
  const dev = platform === "darwin" ? macHost().developerDir : null;
13823
- return dev && existsSync5(join10(dev, "usr/bin/python3")) ? join10(dev, "usr/bin/python3") : null;
14261
+ return dev && existsSync5(join11(dev, "usr/bin/python3")) ? join11(dev, "usr/bin/python3") : null;
13824
14262
  };
13825
14263
  let cached;
13826
14264
  function probe() {
@@ -13938,13 +14376,13 @@ function createShellSandbox(deps = {}) {
13938
14376
  return { ready: false, reason: "a folder in the project cannot be read, so it cannot be checked for sockets and hard links" };
13939
14377
  let scratch;
13940
14378
  try {
13941
- scratch = mkdtempSync(join10(realpathSync4(tmpdir()), "freepi-hive-"));
14379
+ scratch = mkdtempSync(join11(realpathSync4(tmpdir()), "freepi-hive-"));
13942
14380
  const dir = scratch;
13943
- mkdirSync3(join10(dir, "home"));
13944
- mkdirSync3(join10(dir, "npm-cache"));
13945
- const emptyFile = join10(dir, "empty");
13946
- const marker = join10(dir, "call.fd");
13947
- writeFileSync3(emptyFile, "", { mode: 292 });
14381
+ mkdirSync4(join11(dir, "home"));
14382
+ mkdirSync4(join11(dir, "npm-cache"));
14383
+ const emptyFile = join11(dir, "empty");
14384
+ const marker = join11(dir, "call.fd");
14385
+ writeFileSync4(emptyFile, "", { mode: 292 });
13948
14386
  const roots = toolchainRoots(home);
13949
14387
  const mac = platform === "darwin";
13950
14388
  const host = mac ? macHost() : undefined;
@@ -13996,7 +14434,7 @@ function createShellSandbox(deps = {}) {
13996
14434
  } catch (err) {
13997
14435
  if (scratch)
13998
14436
  removeTree(scratch);
13999
- return { ready: false, reason: `could not prepare the sandbox: ${err instanceof Error ? err.message : String(err)}` };
14437
+ return { ready: false, reason: `Couldn't set up the command sandbox: ${sandboxCause(err)}.` };
14000
14438
  }
14001
14439
  },
14002
14440
  mark(toolCallId, mode) {
@@ -14093,6 +14531,44 @@ function installSafeUrlOpen(mode, open = openLink) {
14093
14531
  });
14094
14532
  }
14095
14533
 
14534
+ // src/retry-failed-line.ts
14535
+ var RETRY_FAILED = /^Retry failed after (\d+) attempts?: ([\s\S]*)$/;
14536
+ var CRASH_WARNING = /^\S+ crashed on /;
14537
+ var PRODUCT = "free-pi-cli";
14538
+ function installRetryFailedLine(mode) {
14539
+ const m = mode;
14540
+ const original = m.showError.bind(m);
14541
+ m.showError = (msg) => {
14542
+ const hit = typeof msg === "string" ? RETRY_FAILED.exec(msg) : null;
14543
+ if (!hit)
14544
+ return original(msg);
14545
+ const [, n, cause = ""] = hit;
14546
+ if (cause === "Retry cancelled")
14547
+ return m.showWarning("Retry cancelled.");
14548
+ const tries = `${n} ${n === "1" ? "try" : "tries"}`;
14549
+ m.showWarning(`Gave up after ${tries}: ${cause}`);
14550
+ };
14551
+ const warn = m.showWarning.bind(m);
14552
+ m.showWarning = (msg) => warn(typeof msg === "string" ? msg.replace(CRASH_WARNING, `${PRODUCT} crashed on `) : msg);
14553
+ for (const name of ["renderProjectTrustWarningIfNeeded", "suggestBugReport"]) {
14554
+ const orig = m[name]?.bind(m);
14555
+ if (!orig)
14556
+ continue;
14557
+ m[name] = () => {
14558
+ const before = m.chatContainer?.children?.length ?? 0;
14559
+ orig();
14560
+ for (const child of m.chatContainer?.children?.slice(before) ?? []) {
14561
+ const build = child.build;
14562
+ if (typeof build === "function")
14563
+ child.build = () => reword(build.call(child));
14564
+ }
14565
+ };
14566
+ }
14567
+ }
14568
+ function reword(text) {
14569
+ return text.replace(/restart pi\./, `restart ${PRODUCT}.`).replace(/looks like a pi bug/, "looks like a free-pi bug");
14570
+ }
14571
+
14096
14572
  // src/project-trust.ts
14097
14573
  import { createInterface } from "node:readline/promises";
14098
14574
  import { resolve as resolve3 } from "node:path";
@@ -14239,14 +14715,20 @@ var SDK_TOOLS = [
14239
14715
  "list_mcp_resource_templates",
14240
14716
  "read_mcp_resource"
14241
14717
  ];
14242
- var SKILLS_DIR = resolve4(dirname11(fileURLToPath4(import.meta.url)), "..", "skills");
14718
+ var SKILLS_DIR = resolve4(dirname12(fileURLToPath4(import.meta.url)), "..", "skills");
14243
14719
  function resolveModelName(opts) {
14244
14720
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
14245
14721
  }
14246
14722
  function buildSettingsManager(opts, models, projectTrusted) {
14247
- return SettingsManager2.inMemory({
14248
- defaultProvider: PROVIDER_NAME,
14249
- defaultModel: models[0].id,
14723
+ const saved = readDefaultModel(opts.agentDir);
14724
+ const known = new Map([
14725
+ [PROVIDER_NAME, new Set([...models.map((m) => m.id), FUSION_PICKER_ID, ...models.some((m) => m.id === ULTRAFAST_CATALOG_ID) ? [ULTRAFAST_PICKER_ID] : []])],
14726
+ [DEFENSE_PROVIDER_NAME, new Set(defenseModelsFor(opts).map((m) => m.id))]
14727
+ ]);
14728
+ const start = saved && known.get(saved.provider)?.has(saved.model) ? saved : undefined;
14729
+ const settings = {
14730
+ defaultProvider: start?.provider ?? PROVIDER_NAME,
14731
+ defaultModel: start?.model ?? models[0].id,
14250
14732
  defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
14251
14733
  packages: [],
14252
14734
  quietStartup: true,
@@ -14256,7 +14738,10 @@ function buildSettingsManager(opts, models, projectTrusted) {
14256
14738
  theme: "light/dark",
14257
14739
  enableInstallTelemetry: false,
14258
14740
  ...compactionSettingsFor(opts.costArm) ? { compaction: compactionSettingsFor(opts.costArm) } : {}
14259
- }, { projectTrusted });
14741
+ };
14742
+ const storage = new DefaultModelStorage(opts.agentDir);
14743
+ storage.withLock("global", () => JSON.stringify(settings, null, 2));
14744
+ return SettingsManager2.fromStorage(storage, { projectTrusted });
14260
14745
  }
14261
14746
  function settingsForRuntime(opts, rtCwd, launch) {
14262
14747
  return buildSettingsManager(opts, catalogModelsFor(opts), projectTrustForCwd(rtCwd, launch, opts.agentDir));
@@ -14397,6 +14882,7 @@ function buildRuntimeOptions(opts, sessionId) {
14397
14882
  hasCredit: () => (getAllowanceCache()?.boughtCredits ?? 0) > 0,
14398
14883
  extensionsFor: (subSessionId) => [
14399
14884
  { name: "free-pi-subagent-provider", factory: (pi) => registerProviders(pi, subSessionId, { "x-freepi-subagent": "1" }) },
14885
+ createTurnHeaderExtension(),
14400
14886
  createToolGuardExtension({
14401
14887
  baseUrl: opts.baseUrl,
14402
14888
  getToken: () => auth.getJwt(),
@@ -14451,7 +14937,7 @@ ${CHOICE_RULES}` : agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [she
14451
14937
  function getSessionDir(cwd, agentDir) {
14452
14938
  const resolvedCwd = resolve4(cwd);
14453
14939
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
14454
- return join11(resolve4(agentDir), "sessions", safePath);
14940
+ return join12(resolve4(agentDir), "sessions", safePath);
14455
14941
  }
14456
14942
  async function findLatestSession(cwd, sessionDir, api = SessionManager2) {
14457
14943
  const infos = await api.list(cwd, sessionDir);
@@ -14553,11 +15039,13 @@ async function launchPi(opts) {
14553
15039
  installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
14554
15040
  installModelAllowanceHandler(mode);
14555
15041
  installSafeUrlOpen(mode);
15042
+ installRetryFailedLine(mode);
14556
15043
  installFooterRightAlign(mode, () => {
14557
15044
  const id = runtime.session?.model?.id;
14558
15045
  return freeModelBase(pickerModelName(opts, id));
14559
15046
  });
14560
15047
  installEditorPlaceholder(mode);
15048
+ installWidthGuard(mode);
14561
15049
  if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
14562
15050
  process.stdout.write("\x1B[2J\x1B[H");
14563
15051
  installBottomPin(mode);
@@ -14610,7 +15098,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
14610
15098
  // src/relaunch.ts
14611
15099
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
14612
15100
  import { constants as osConstants, tmpdir as tmpdir2 } from "node:os";
14613
- import { delimiter, dirname as dirname12, isAbsolute as isAbsolute3, join as join12, resolve as resolve5, sep as sep4, win32 } from "node:path";
15101
+ import { delimiter, dirname as dirname13, isAbsolute as isAbsolute3, join as join13, resolve as resolve5, sep as sep4, win32 } from "node:path";
14614
15102
  function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
14615
15103
  try {
14616
15104
  return statSync4(p).isDirectory();
@@ -14651,7 +15139,7 @@ function hasNodeModulesBin(entry) {
14651
15139
  }
14652
15140
  function sanitizedEnv(env, nodeDir, project, home, win) {
14653
15141
  const proj = canon(project, win);
14654
- const broad = proj === canon(home, win) || dirname12(proj) === proj;
15142
+ const broad = proj === canon(home, win) || dirname13(proj) === proj;
14655
15143
  const out = {};
14656
15144
  let pathKey = "PATH";
14657
15145
  let oldPath = "";
@@ -14688,13 +15176,13 @@ async function relaunchViaNpx(latest, deps) {
14688
15176
  if (!latest || !SEMVER.test(latest))
14689
15177
  return;
14690
15178
  const target = latest;
14691
- const nodeDir = dirname12(deps.execPath);
14692
- const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(dirname12(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
15179
+ const nodeDir = dirname13(deps.execPath);
15180
+ const npxCli = win ? join13(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join13(dirname13(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
14693
15181
  const exists = deps.exists ?? existsSync6;
14694
15182
  const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
14695
15183
  const healthy = (cli) => {
14696
- const root = join12(dirname12(cli), "..");
14697
- return exists(join12(root, "package.json")) && exists(join12(root, "lib", "cli.js"));
15184
+ const root = join13(dirname13(cli), "..");
15185
+ return exists(join13(root, "package.json")) && exists(join13(root, "lib", "cli.js"));
14698
15186
  };
14699
15187
  let npxCliPath = npxCli;
14700
15188
  if (!exists(npxCli)) {
@@ -14706,8 +15194,8 @@ async function relaunchViaNpx(latest, deps) {
14706
15194
  if (!healthy(npxCliPath))
14707
15195
  return;
14708
15196
  const childEnv = sanitizedEnv(env, nodeDir, deps.cwd, deps.homedir, win);
14709
- const readyDir = mkdtempSync2(join12(tmpdir2(), "free-pi-relaunch-"));
14710
- const readyFile = join12(readyDir, "started");
15197
+ const readyDir = mkdtempSync2(join13(tmpdir2(), "free-pi-relaunch-"));
15198
+ const readyFile = join13(readyDir, "started");
14711
15199
  try {
14712
15200
  deps.log(`Updating free-pi to ${target}…`);
14713
15201
  return await new Promise((resolve) => {
@@ -14749,13 +15237,13 @@ async function relaunchViaNpx(latest, deps) {
14749
15237
 
14750
15238
  // src/run.ts
14751
15239
  import { createInterface as createInterface3 } from "node:readline/promises";
14752
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
15240
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
14753
15241
  import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
14754
15242
 
14755
15243
  // src/onboarding.ts
14756
15244
  import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync as realpathSync6 } from "node:fs";
14757
15245
  import { homedir as homedir8 } from "node:os";
14758
- import { join as join13 } from "node:path";
15246
+ import { join as join14 } from "node:path";
14759
15247
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
14760
15248
  var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
14761
15249
  function introSkipped(env = process.env) {
@@ -14775,7 +15263,7 @@ function looksLikeProject(cwd, home = homedir8()) {
14775
15263
  try {
14776
15264
  if (realpathSync6(cwd) === realpathSync6(home))
14777
15265
  return false;
14778
- if (PROJECT_MARKERS.some((m) => existsSync7(join13(cwd, m))))
15266
+ if (PROJECT_MARKERS.some((m) => existsSync7(join14(cwd, m))))
14779
15267
  return true;
14780
15268
  return readdirSync3(cwd).some((f) => /\.(sln|csproj)$/.test(f));
14781
15269
  } catch {
@@ -14804,7 +15292,7 @@ ${text}
14804
15292
  // src/starter.ts
14805
15293
  import { existsSync as existsSync8, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
14806
15294
  import { homedir as homedir9 } from "node:os";
14807
- import { join as join14 } from "node:path";
15295
+ import { join as join15 } from "node:path";
14808
15296
  var STARTER_EXPERIMENT = "first_session_starter";
14809
15297
  var STARTER_PROJECT = [
14810
15298
  {
@@ -14837,7 +15325,7 @@ var STARTER_EMPTY = [
14837
15325
  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:";
14838
15326
  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:";
14839
15327
  function getStarterMarkerPath(agentDir = getFreePiAgentDir()) {
14840
- return join14(agentDir, "starter-shown");
15328
+ return join15(agentDir, "starter-shown");
14841
15329
  }
14842
15330
  function starterShownBefore(agentDir) {
14843
15331
  return existsSync8(getStarterMarkerPath(agentDir));
@@ -14869,12 +15357,12 @@ function parseStarterKey(raw, count) {
14869
15357
  }
14870
15358
 
14871
15359
  // src/whats-new.ts
14872
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
14873
- import { dirname as dirname13 } from "node:path";
15360
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
15361
+ import { dirname as dirname14 } from "node:path";
14874
15362
  function maybeWhatsNew(cliVersion, agentDir) {
14875
15363
  try {
14876
15364
  const path = getLastSeenVersionPath(agentDir);
14877
- const lastSeen = existsSync9(path) ? readFileSync6(path, "utf-8").trim() : undefined;
15365
+ const lastSeen = existsSync9(path) ? readFileSync7(path, "utf-8").trim() : undefined;
14878
15366
  writeLastSeen(path, cliVersion);
14879
15367
  if (lastSeen === undefined)
14880
15368
  return;
@@ -14890,8 +15378,8 @@ function maybeWhatsNew(cliVersion, agentDir) {
14890
15378
  }
14891
15379
  }
14892
15380
  function writeLastSeen(path, version) {
14893
- mkdirSync4(dirname13(path), { recursive: true });
14894
- writeFileSync4(path, version, "utf-8");
15381
+ mkdirSync5(dirname14(path), { recursive: true });
15382
+ writeFileSync5(path, version, "utf-8");
14895
15383
  }
14896
15384
 
14897
15385
  // src/run.ts
@@ -15142,7 +15630,7 @@ Your free-pi sign-in has expired. Signing in with GitHub again.
15142
15630
  deps.log(SIGNIN_UNFINISHED_LINE);
15143
15631
  return 0;
15144
15632
  }
15145
- const reason = err instanceof Error && err.message ? err.message : "unknown error";
15633
+ const reason = (err instanceof Error && err.message ? err.message : "unknown error").replace(/\.$/, "");
15146
15634
  deps.log(`Couldn't finish GitHub sign-in: ${reason}. Run npx free-pi-cli again to retry.`);
15147
15635
  return 1;
15148
15636
  }
@@ -15338,8 +15826,8 @@ ${r.text}
15338
15826
  const choices = starterChoices();
15339
15827
  if (choices) {
15340
15828
  try {
15341
- mkdirSync5(deps.agentDir, { recursive: true });
15342
- writeFileSync5(getStarterMarkerPath(deps.agentDir), "1");
15829
+ mkdirSync6(deps.agentDir, { recursive: true });
15830
+ writeFileSync6(getStarterMarkerPath(deps.agentDir), "1");
15343
15831
  } catch {}
15344
15832
  const report = (event) => deps.reportStarterEvent ? deps.reportStarterEvent(event) : reportStarterEvent(deps, jwt, event);
15345
15833
  deps.log(formatStarterMenu(choices));
@@ -15492,7 +15980,7 @@ async function main() {
15492
15980
  }
15493
15981
  if (process.env.FREEPI_RELAUNCH_READY) {
15494
15982
  try {
15495
- writeFileSync6(process.env.FREEPI_RELAUNCH_READY, "");
15983
+ writeFileSync7(process.env.FREEPI_RELAUNCH_READY, "");
15496
15984
  } catch {}
15497
15985
  }
15498
15986
  const command = parseCliArgs(process.argv.slice(2));
@@ -15553,7 +16041,7 @@ ${HELP_TEXT}`);
15553
16041
  launchPi,
15554
16042
  log: (message) => console.log(message),
15555
16043
  checkClientVersion: (baseUrl, cliVersion, jwt) => checkClientVersion(baseUrl, cliVersion, fetch, jwt),
15556
- startGravityPixel: (jwt) => startGravityPixel(jwt, join15(dirname14(agentDir), "gravity")),
16044
+ startGravityPixel: (jwt) => startGravityPixel(jwt, join16(dirname15(agentDir), "gravity")),
15557
16045
  relaunchLatest: (latest) => relaunchViaNpx(latest, {
15558
16046
  env: process.env,
15559
16047
  argv: process.argv.slice(2),