free-pi-cli 0.6.19 → 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.19",
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
  }
@@ -7243,7 +7247,7 @@ function createAdsExtension(opts) {
7243
7247
  }
7244
7248
 
7245
7249
  // src/pi-launch.ts
7246
- 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";
7247
7251
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7248
7252
 
7249
7253
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
@@ -7573,6 +7577,28 @@ var integerKeyPattern = new RegExp("^(?:0|[1-9][0-9]*)$");
7573
7577
 
7574
7578
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/indexed/from_object.mjs
7575
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
+
7576
7602
  // src/usage-tool.ts
7577
7603
  var USAGE_TOOL_NAME = "free_pi_usage";
7578
7604
  var PARAMS = _Object_({});
@@ -7630,11 +7656,11 @@ async function fetchUsageText(opts) {
7630
7656
  signal: AbortSignal.timeout(5000)
7631
7657
  });
7632
7658
  if (!res.ok)
7633
- return { ok: false, text: `Could not fetch usage (HTTP ${res.status}).` };
7659
+ return { ok: false, text: friendlyStatus(res.status) };
7634
7660
  const stats = await res.json();
7635
7661
  return { ok: true, text: formatStats(stats), stats };
7636
7662
  } catch (err) {
7637
- return { ok: false, text: `Could not fetch usage: ${String(err)}` };
7663
+ return { ok: false, text: friendlyThrown(err) };
7638
7664
  }
7639
7665
  }
7640
7666
  function createUsageToolExtension(opts) {
@@ -7702,11 +7728,9 @@ async function post(opts, body, signal) {
7702
7728
  const mapped = ERROR_TEXT[code];
7703
7729
  if (mapped)
7704
7730
  return { ok: false, text: mapped };
7705
- 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) };
7706
7732
  } 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)}` };
7733
+ return { ok: false, text: friendlyThrown(e) };
7710
7734
  }
7711
7735
  }
7712
7736
  function slug(s) {
@@ -7783,7 +7807,8 @@ function createImageToolExtension(opts) {
7783
7807
  await fh.close();
7784
7808
  }
7785
7809
  } catch (e) {
7786
- 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}.`);
7787
7812
  }
7788
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 });
7789
7814
  }
@@ -8392,11 +8417,9 @@ async function post2(opts, path, body, label, signal) {
8392
8417
  const mapped = ERROR_TEXT3[err.code ?? ""];
8393
8418
  if (mapped)
8394
8419
  return { ok: false, text: typeof mapped === "function" ? mapped(label) : mapped };
8395
- return { ok: false, text: `Request failed (HTTP ${res.status}): ${err.message ?? "no message"}` };
8420
+ return { ok: false, text: friendlyStatus(res.status, "model", err.message) };
8396
8421
  } 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)}` };
8422
+ return { ok: false, text: friendlyThrown(e) };
8400
8423
  }
8401
8424
  }
8402
8425
  var done3 = (o) => ({ content: [{ type: "text", text: o.text }], details: o.details });
@@ -8465,11 +8488,11 @@ async function resolveBuyPage(opts) {
8465
8488
  signal: AbortSignal.timeout(5000)
8466
8489
  });
8467
8490
  if (!res.ok)
8468
- return { kind: "error", text: `Could not fetch the buy page (HTTP ${res.status}).` };
8491
+ return { kind: "error", text: friendlyStatus(res.status) };
8469
8492
  const me = await res.json();
8470
8493
  return me.buy_url ? { kind: "url", url: me.buy_url } : { kind: "unavailable" };
8471
8494
  } catch (err) {
8472
- return { kind: "error", text: `Could not fetch the buy page: ${String(err)}` };
8495
+ return { kind: "error", text: friendlyThrown(err) };
8473
8496
  }
8474
8497
  }
8475
8498
  var BUY_NOT_AVAILABLE_TEXT = "Buying credits is not available on this server yet.";
@@ -8980,6 +9003,142 @@ function createToolAliasesExtension(opts = {}) {
8980
9003
  };
8981
9004
  }
8982
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
+
8983
9142
  // src/fusion.ts
8984
9143
  var FUSION_ID = "fusion";
8985
9144
  var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
@@ -9175,7 +9334,11 @@ function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
9175
9334
 
9176
9335
  // src/error-notice.ts
9177
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.";
9178
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.";
9179
9342
  var FREE_OFFER_TITLE = "Switch to the free model and resend this prompt?";
9180
9343
  var FREE_OFFER_ACCEPT = "Switch and resend";
9181
9344
  var FREE_OFFER_DECLINE = "Stay on this model";
@@ -9235,6 +9398,8 @@ function parseErrorEnvelope(errorMessage) {
9235
9398
  }
9236
9399
  }
9237
9400
  function formatLimitEnvelope(env, opts) {
9401
+ if (env.code === "model_requires_credits" && !/premium credit/i.test(env.message))
9402
+ return PREMIUM_MODEL_NOTICE;
9238
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") {
9239
9404
  return env.message;
9240
9405
  }
@@ -9280,6 +9445,36 @@ function lastUserEntryId(ctx) {
9280
9445
  }
9281
9446
  return "";
9282
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
+ }
9283
9478
  function createErrorNoticeExtension(opts = {}) {
9284
9479
  const lastAt = new Map;
9285
9480
  const now = opts.now ?? Date.now;
@@ -9312,21 +9507,44 @@ function createErrorNoticeExtension(opts = {}) {
9312
9507
  return {
9313
9508
  name: "free-pi-error-notice",
9314
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
+ });
9315
9531
  pi.on("turn_end", async (event, ctx) => {
9316
9532
  lastTurnEntryId = event.messageEntryId;
9317
9533
  const msg = event.message;
9318
9534
  if (!msg || msg.stopReason !== "error")
9319
9535
  return;
9320
- 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));
9321
9537
  if (!env)
9322
9538
  return;
9323
9539
  const t = now();
9324
9540
  if (t - (lastAt.get(env.code) ?? -Infinity) >= NOTICE_DEDUPE_MS) {
9325
9541
  lastAt.set(env.code, t);
9326
9542
  const extra = opts.extraNotice?.(env);
9327
- 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 ? `
9328
9545
  ${extra}` : "");
9329
- 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");
9330
9548
  }
9331
9549
  await offerFreeModel(pi, env, event.messageEntryId, ctx);
9332
9550
  });
@@ -9820,6 +10038,20 @@ function installBottomPin(mode) {
9820
10038
  return [...lines, ...Array(bottomPadding(lines.length, other, rows)).fill("")];
9821
10039
  };
9822
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
+ }
9823
10055
 
9824
10056
  // src/guest-handoff.ts
9825
10057
  var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
@@ -9909,6 +10141,10 @@ function mergeAttribution(a, b) {
9909
10141
 
9910
10142
  // src/changelog.ts
9911
10143
  var CHANGELOG_HIGHLIGHTS = [
10144
+ {
10145
+ version: "0.6.20",
10146
+ highlights: ["Error messages are now plain sentences instead of raw server text."]
10147
+ },
9912
10148
  {
9913
10149
  version: "0.6.19",
9914
10150
  highlights: ["The ad banner now refreshes when you send a message, not on a timer."]
@@ -10273,6 +10509,7 @@ function highlightsFor(version) {
10273
10509
 
10274
10510
  // src/commands.ts
10275
10511
  import { access } from "node:fs/promises";
10512
+ import { basename as basename3 } from "node:path";
10276
10513
 
10277
10514
  // src/auth-flow.ts
10278
10515
  class UnauthorizedError extends Error {
@@ -10287,13 +10524,23 @@ async function postJson(baseUrl, path, body, extraHeaders = {}) {
10287
10524
  body: JSON.stringify(body)
10288
10525
  });
10289
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
+ }
10290
10537
  async function postJsonRetry(baseUrl, path, body, attempts = 4, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), extraHeaders = {}) {
10291
10538
  for (let attempt = 1;; attempt++) {
10292
10539
  try {
10293
10540
  return await postJson(baseUrl, path, body, extraHeaders);
10294
10541
  } catch (err) {
10295
10542
  if (attempt >= attempts) {
10296
- throw new Error(`network unreachable after ${attempts} attempts (${err instanceof Error ? err.message : String(err)})`);
10543
+ throw new Error(NETWORK_TEXT, { cause: err });
10297
10544
  }
10298
10545
  await sleep(1500 * attempt);
10299
10546
  }
@@ -10310,6 +10557,16 @@ function refusalMessage(status, body) {
10310
10557
  }
10311
10558
  return;
10312
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
+ }
10313
10570
  async function readRefusal(res) {
10314
10571
  return await res.json().catch(() => {
10315
10572
  return;
@@ -10318,9 +10575,9 @@ async function readRefusal(res) {
10318
10575
  async function startDeviceFlow(baseUrl) {
10319
10576
  const res = await postJsonRetry(baseUrl, "/auth/github/device", {});
10320
10577
  if (!res.ok) {
10321
- 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)));
10322
10579
  }
10323
- return GithubDeviceResponseSchema.parse(await res.json());
10580
+ return parseReply(res, GithubDeviceResponseSchema);
10324
10581
  }
10325
10582
  async function startConnect(baseUrl, consentVersion, attribution = {}, client = {}) {
10326
10583
  const res = await postJsonRetry(baseUrl, "/auth/connect/start", {
@@ -10330,8 +10587,8 @@ async function startConnect(baseUrl, consentVersion, attribution = {}, client =
10330
10587
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
10331
10588
  }, undefined, undefined, { [SIGNIN_EXP_HEADER]: "1" });
10332
10589
  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());
10590
+ throw new Error(failureText(res.status, await readRefusal(res)));
10591
+ return parseReply(res, ConnectStartResponseSchema);
10335
10592
  }
10336
10593
  async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {}) {
10337
10594
  const res = await postJsonRetry(baseUrl, "/auth/consent", {
@@ -10341,7 +10598,7 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
10341
10598
  ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
10342
10599
  });
10343
10600
  if (!res.ok) {
10344
- 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)));
10345
10602
  }
10346
10603
  }
10347
10604
  function deviceReport() {
@@ -10374,8 +10631,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
10374
10631
  throw new GuestUnavailableError(body?.reason ?? "disabled");
10375
10632
  }
10376
10633
  if (!res.ok)
10377
- throw new Error(`guest start failed: HTTP ${res.status}`);
10378
- const parsed = GuestStartResponseSchema.parse(await res.json());
10634
+ throw new Error(failureText(res.status, await readRefusal(res)));
10635
+ const parsed = await parseReply(res, GuestStartResponseSchema);
10379
10636
  return {
10380
10637
  token: parsed.token,
10381
10638
  allowance_usd: parsed.allowance_usd,
@@ -10455,7 +10712,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
10455
10712
  throw new PollAbortedError;
10456
10713
  consecutiveNetworkFailures++;
10457
10714
  if (consecutiveNetworkFailures >= 6) {
10458
- 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 });
10459
10716
  }
10460
10717
  await sleep(Math.max(waitSeconds, 5) * 1000);
10461
10718
  continue;
@@ -10473,9 +10730,9 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
10473
10730
  throw new LoginTimeoutError;
10474
10731
  if (isExpiredTokenPoll(body))
10475
10732
  throw new DeviceCodeExpiredError;
10476
- throw new Error(refusalMessage(res.status, body) ?? `token poll failed: HTTP ${res.status}`);
10733
+ throw new Error(failureText(res.status, body));
10477
10734
  }
10478
- const parsed = AuthTokenResponseSchema.parse(await res.json());
10735
+ const parsed = await parseReply(res, AuthTokenResponseSchema);
10479
10736
  if (opts.signal?.aborted)
10480
10737
  throw new PollAbortedError;
10481
10738
  if ("token" in parsed)
@@ -11449,7 +11706,7 @@ async function runLoginUnguarded(opts, ctx, lead) {
11449
11706
  ctx.ui.notify(LOGIN_DENIED_LINE, "info");
11450
11707
  return false;
11451
11708
  }
11452
- const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
11709
+ const reason = err instanceof Error && err.message ? `: ${err.message.replace(/\.$/, "")}` : "";
11453
11710
  ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
11454
11711
  return false;
11455
11712
  }
@@ -11467,6 +11724,26 @@ function installLoginHandler(mode, opts) {
11467
11724
  return runLogin(opts, { ui: { notify } });
11468
11725
  };
11469
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
+ }
11470
11747
  function installImportHandler(mode, opts) {
11471
11748
  const m = mode;
11472
11749
  mode.handleImportCommand = async (text) => {
@@ -11489,11 +11766,11 @@ function installImportHandler(mode, opts) {
11489
11766
  for (let waited = 0;m.session.isCompacting && waited < 60000; waited += 100)
11490
11767
  await new Promise((r) => setTimeout(r, 100));
11491
11768
  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)));
11769
+ m.session.prompt(message, options).catch((err) => m.showError(friendlyImportError(err, "send the import")));
11493
11770
  }
11494
11771
  });
11495
11772
  } catch (err) {
11496
- m.showError(`Couldn't import: ${err instanceof Error ? err.message : String(err)}`);
11773
+ m.showError(friendlyImportError(err));
11497
11774
  }
11498
11775
  };
11499
11776
  }
@@ -11641,7 +11918,7 @@ function createGuestLoginExtension(opts) {
11641
11918
  if (!opts.isGuest?.())
11642
11919
  return;
11643
11920
  const msg = event.message;
11644
- const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(msg.errorMessage) : undefined;
11921
+ const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(rawErrorMessage(msg)) : undefined;
11645
11922
  if (envelope && (envelope.code === "guest_allowance" || envelope.code === "guest_unavailable" || envelope.prompt === "github_signin")) {
11646
11923
  if (!inFlight && !signedIn) {
11647
11924
  const busy = envelope.code === "guest_allowance" && (envelope.remaining_credits_today ?? 0) > 0;
@@ -11751,7 +12028,7 @@ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync a
11751
12028
  import { homedir as homedir4 } from "node:os";
11752
12029
  import { dirname as dirname8 } from "node:path";
11753
12030
  import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
11754
- 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";
11755
12032
  var TOOL_MODES = ["compact", "full", "hidden"];
11756
12033
  var TOOLS_USAGE = "Usage: /tools compact|full|hidden (compact: one line per tool call; full: every call in full; hidden: just a count)";
11757
12034
  function readToolMode(path) {
@@ -11851,12 +12128,30 @@ function toolLines(i) {
11851
12128
  }
11852
12129
  return done;
11853
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
+ }
11854
12149
  function component(rows, theme, pad) {
11855
12150
  return {
11856
12151
  render: (w) => rows.flatMap((r) => {
11857
12152
  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));
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), ""));
11860
12155
  }),
11861
12156
  invalidate() {}
11862
12157
  };
@@ -12128,16 +12423,63 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
12128
12423
  return scoped;
12129
12424
  }
12130
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
+
12131
12473
  // src/hive.ts
12132
12474
  import { createHash as createHash3 } from "node:crypto";
12133
12475
  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";
12476
+ import { lstatSync as lstatSync2, readFileSync as readFileSync6, readlinkSync as readlinkSync2 } from "node:fs";
12477
+ import { join as join10 } from "node:path";
12136
12478
 
12137
12479
  // src/hive-guard.ts
12138
12480
  import { existsSync as existsSync4, lstatSync, readdirSync, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
12139
12481
  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";
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";
12141
12483
  import { fileURLToPath as fileURLToPath3 } from "node:url";
12142
12484
 
12143
12485
  // src/subagents.ts
@@ -12254,8 +12596,10 @@ function lastAssistant(session) {
12254
12596
  continue;
12255
12597
  const text = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
12256
12598
  `).trim() : "";
12257
- if (m.stopReason === "error")
12258
- 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
+ }
12259
12603
  if (m.stopReason === "aborted")
12260
12604
  return { text, error: "aborted" };
12261
12605
  return { text };
@@ -12646,7 +12990,7 @@ var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
12646
12990
  var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
12647
12991
  var CLASSIFIED = ["bash", "write", "edit"];
12648
12992
  var SECRET_BASENAME = [/^\.env(\..*)?$/, /\.pem$/, /^id_(rsa|dsa|ecdsa|ed25519)/];
12649
- var SECRET_HOME_DIRS = [".ssh", ".aws", join8(".config", "freepi"), ".free-pi"];
12993
+ var SECRET_HOME_DIRS = [".ssh", ".aws", join9(".config", "freepi"), ".free-pi"];
12650
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/;
12651
12995
  var BASH_DENY = [
12652
12996
  [/\brm\s+(?:-\S+\s+)*(?:\/|~|\$HOME)\*?(?:\s|$)/, "destructive command"],
@@ -12669,7 +13013,7 @@ function protectedLinkTargets(root, limit) {
12669
13013
  const note = (link) => {
12670
13014
  let target;
12671
13015
  try {
12672
- target = resolve2(dirname9(link), readlinkSync(link));
13016
+ target = resolve2(dirname10(link), readlinkSync(link));
12673
13017
  } catch {
12674
13018
  return;
12675
13019
  }
@@ -12679,7 +13023,7 @@ function protectedLinkTargets(root, limit) {
12679
13023
  out.push(real);
12680
13024
  };
12681
13025
  for (const entry of [".git/hooks", ".git/config"]) {
12682
- const link = join8(root, entry);
13026
+ const link = join9(root, entry);
12683
13027
  try {
12684
13028
  if (lstatSync(link).isSymbolicLink())
12685
13029
  note(link);
@@ -12698,7 +13042,7 @@ function protectedLinkTargets(root, limit) {
12698
13042
  for (const entry of entries) {
12699
13043
  if (++seen > limit)
12700
13044
  return null;
12701
- const path = join8(dir, entry.name);
13045
+ const path = join9(dir, entry.name);
12702
13046
  if (entry.isSymbolicLink()) {
12703
13047
  if (protectedByName(relative2(root, path)))
12704
13048
  note(path);
@@ -12748,7 +13092,7 @@ function resolveLikePi(raw, cwd, home, platform) {
12748
13092
  if (p === "~")
12749
13093
  p = home;
12750
13094
  else if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
12751
- p = join8(home, p.slice(2));
13095
+ p = join9(home, p.slice(2));
12752
13096
  if (/^file:\/\//.test(p))
12753
13097
  p = fileURLToPath3(p);
12754
13098
  return resolve2(cwd, p);
@@ -12758,17 +13102,17 @@ function realpathDeepest(p) {
12758
13102
  const rest = [];
12759
13103
  for (;; ) {
12760
13104
  try {
12761
- return join8(realpathSync3(current), ...rest.reverse());
13105
+ return join9(realpathSync3(current), ...rest.reverse());
12762
13106
  } catch {
12763
13107
  try {
12764
13108
  lstatSync(current);
12765
13109
  return null;
12766
13110
  } catch {}
12767
13111
  }
12768
- const parent = dirname9(current);
13112
+ const parent = dirname10(current);
12769
13113
  if (parent === current)
12770
13114
  return null;
12771
- rest.push(basename3(current));
13115
+ rest.push(basename4(current));
12772
13116
  current = parent;
12773
13117
  }
12774
13118
  }
@@ -12777,10 +13121,10 @@ function isInside(root, target) {
12777
13121
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
12778
13122
  }
12779
13123
  function isSecret(target, home) {
12780
- if (SECRET_BASENAME.some((re) => re.test(basename3(target))))
13124
+ if (SECRET_BASENAME.some((re) => re.test(basename4(target))))
12781
13125
  return true;
12782
13126
  return SECRET_HOME_DIRS.some((dir) => {
12783
- const lexical = join8(home, dir);
13127
+ const lexical = join9(home, dir);
12784
13128
  return isInside(lexical, target) || isInside(realpathDeepest(lexical) ?? lexical, target);
12785
13129
  });
12786
13130
  }
@@ -12801,7 +13145,7 @@ function scanForSecret(dir) {
12801
13145
  if (SECRET_BASENAME.some((re) => re.test(entry.name)))
12802
13146
  return "yes";
12803
13147
  if (entry.isDirectory())
12804
- queue.push(join8(current, entry.name));
13148
+ queue.push(join9(current, entry.name));
12805
13149
  }
12806
13150
  }
12807
13151
  return "no";
@@ -12925,6 +13269,33 @@ async function decideToolCall(event, opts) {
12925
13269
  }
12926
13270
 
12927
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}).`;
12928
13299
  var RUN_IN_SANDBOX = "Run in sandbox";
12929
13300
  var RUN_OUTSIDE = "Run outside the sandbox";
12930
13301
  var DENY = "Deny";
@@ -12948,7 +13319,7 @@ function fingerprint(path) {
12948
13319
  return "";
12949
13320
  if (st.size > HASH_MAX_BYTES)
12950
13321
  return `${st.size}:${st.mtimeMs}`;
12951
- return createHash3("sha256").update(readFileSync5(path)).digest("hex");
13322
+ return createHash3("sha256").update(readFileSync6(path)).digest("hex");
12952
13323
  } catch {
12953
13324
  return "";
12954
13325
  }
@@ -12978,7 +13349,7 @@ function worktreeSnapshot(g) {
12978
13349
  const status = entry.slice(0, 2);
12979
13350
  if (status[0] === "R" || status[0] === "C")
12980
13351
  i++;
12981
- 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)))}`);
12982
13353
  }
12983
13354
  return { files, head: runGit(g, "rev-parse", "HEAD")?.trim() };
12984
13355
  } catch (e) {
@@ -13140,10 +13511,13 @@ function createHiveExtension(opts) {
13140
13511
  };
13141
13512
  socket.onopen = () => socket.send(JSON.stringify({ type: "auth", token }));
13142
13513
  socket.onerror = failed;
13143
- socket.onclose = () => {
13514
+ socket.onclose = (event) => {
13144
13515
  if (ws !== socket)
13145
13516
  return;
13146
- if (!roomUrl)
13517
+ const known = event?.reason ? HIVE_CLOSED.get(event.reason) : undefined;
13518
+ if (known)
13519
+ endRoom(known);
13520
+ else if (!roomUrl)
13147
13521
  failed();
13148
13522
  else
13149
13523
  endRoom("Hive room closed.");
@@ -13217,10 +13591,10 @@ Guests sign in with GitHub to watch and chat. They can ask to drive; you approve
13217
13591
  break;
13218
13592
  case "error":
13219
13593
  if (frame.code !== "stale")
13220
- notify(`Hive: ${frame.code}`, "warning");
13594
+ notify(hiveErrorText(frame.code), "warning");
13221
13595
  break;
13222
13596
  case "closed":
13223
- endRoom(`Hive room closed (${frame.reason}).`);
13597
+ endRoom(hiveClosedText(frame.reason));
13224
13598
  break;
13225
13599
  }
13226
13600
  }
@@ -13437,16 +13811,26 @@ import {
13437
13811
  chmodSync,
13438
13812
  existsSync as existsSync5,
13439
13813
  lstatSync as lstatSync3,
13440
- mkdirSync as mkdirSync3,
13814
+ mkdirSync as mkdirSync4,
13441
13815
  mkdtempSync,
13442
13816
  readdirSync as readdirSync2,
13443
13817
  realpathSync as realpathSync4,
13444
13818
  rmSync,
13445
13819
  statSync as statSync3,
13446
- writeFileSync as writeFileSync3
13820
+ writeFileSync as writeFileSync4
13447
13821
  } from "node:fs";
13448
13822
  import { homedir as homedir6, tmpdir } from "node:os";
13449
- 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
+ }
13450
13834
  var SECRET_NAME_PATTERNS = [
13451
13835
  "\\.env(\\..*)?",
13452
13836
  "[^/]*\\.pem",
@@ -13482,7 +13866,7 @@ var TOOLCHAIN_ROOTS = [
13482
13866
  "Library/pnpm"
13483
13867
  ];
13484
13868
  function toolchainRoots(home, exists = existsSync5) {
13485
- 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));
13486
13870
  }
13487
13871
  var ENV_ALLOW = ["PATH", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "TZ", "USER", "LOGNAME", "SHELL", "PI_PROVIDER", "PI_MODEL"];
13488
13872
  function under(path, dir) {
@@ -13500,10 +13884,10 @@ function sandboxEnv(base, o) {
13500
13884
  for (const k of ENV_ALLOW)
13501
13885
  if (base[k] !== undefined)
13502
13886
  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(":");
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(":");
13504
13888
  if (o.developerDir)
13505
13889
  env.DEVELOPER_DIR = o.developerDir;
13506
- env.HOME = join10(o.scratch, "home");
13890
+ env.HOME = join11(o.scratch, "home");
13507
13891
  env.TMP = env.TEMP = o.scratch;
13508
13892
  env.TMPDIR = `${o.scratch}/`;
13509
13893
  env.BUN_OPTIONS = "--no-env-file";
@@ -13513,7 +13897,7 @@ function sandboxEnv(base, o) {
13513
13897
  env.GIT_CONFIG_COUNT = "1";
13514
13898
  env.GIT_CONFIG_KEY_0 = "credential.helper";
13515
13899
  env.GIT_CONFIG_VALUE_0 = "";
13516
- env.npm_config_cache = join10(o.scratch, "npm-cache");
13900
+ env.npm_config_cache = join11(o.scratch, "npm-cache");
13517
13901
  env.FREEPI_HIVE_CALL = o.callId;
13518
13902
  return env;
13519
13903
  }
@@ -13559,7 +13943,7 @@ function scanProject(project, limit = SCAN_LIMIT2) {
13559
13943
  scan.overflow = true;
13560
13944
  break;
13561
13945
  }
13562
- const path = join10(dir, e.name);
13946
+ const path = join11(dir, e.name);
13563
13947
  if (e.isSymbolicLink())
13564
13948
  continue;
13565
13949
  if (e.isDirectory()) {
@@ -13659,7 +14043,7 @@ function developerDir(o = {}) {
13659
14043
  continue;
13660
14044
  try {
13661
14045
  const real = realpath(c);
13662
- if (exists(join10(real, "usr/bin/git")))
14046
+ if (exists(join11(real, "usr/bin/git")))
13663
14047
  return real;
13664
14048
  } catch {}
13665
14049
  }
@@ -13677,18 +14061,18 @@ function opensslFiles(o = {}) {
13677
14061
  const files = [];
13678
14062
  const dirs = [];
13679
14063
  for (const prefix of o.prefixes ?? ["/opt/homebrew", "/usr/local"]) {
13680
- const etc = join10(prefix, "etc");
14064
+ const etc = join11(prefix, "etc");
13681
14065
  for (const name of list(etc)) {
13682
14066
  if (!/^openssl(@[0-9.]+)?$/.test(name))
13683
14067
  continue;
13684
14068
  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"));
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"));
13689
14073
  }
13690
- if (exists(join10(etc, "ca-certificates/cert.pem")))
13691
- 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"));
13692
14076
  }
13693
14077
  return { files, dirs };
13694
14078
  }
@@ -13696,7 +14080,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
13696
14080
  var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
13697
14081
  function ancestors(p) {
13698
14082
  const out = [];
13699
- for (let d = dirname10(p);; d = dirname10(d)) {
14083
+ for (let d = dirname11(p);; d = dirname11(d)) {
13700
14084
  out.push(d);
13701
14085
  if (d === "/")
13702
14086
  break;
@@ -13732,9 +14116,9 @@ function buildMacProfile(i) {
13732
14116
  L.push(`(deny file-read* file-write* (literal ${sbString(s)}))`);
13733
14117
  for (const f of i.links)
13734
14118
  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))
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))
13738
14122
  guarded.add(d);
13739
14123
  }
13740
14124
  for (const d of [...guarded].sort())
@@ -13803,10 +14187,10 @@ function buildBwrapArgs(i) {
13803
14187
  a.push("--bind", i.scratch, i.scratch);
13804
14188
  for (const d of i.nodeModules)
13805
14189
  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"));
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"));
13810
14194
  for (const f of i.links)
13811
14195
  ro(f);
13812
14196
  for (const s of i.sockets)
@@ -13836,7 +14220,7 @@ function removeTree(path) {
13836
14220
  return;
13837
14221
  chmodSync(p, 448);
13838
14222
  for (const e of readdirSync2(p))
13839
- walk(join10(p, e));
14223
+ walk(join11(p, e));
13840
14224
  } catch {}
13841
14225
  };
13842
14226
  walk(path);
@@ -13874,7 +14258,7 @@ function createShellSandbox(deps = {}) {
13874
14258
  if (deps.python !== undefined)
13875
14259
  return deps.python;
13876
14260
  const dev = platform === "darwin" ? macHost().developerDir : null;
13877
- 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;
13878
14262
  };
13879
14263
  let cached;
13880
14264
  function probe() {
@@ -13992,13 +14376,13 @@ function createShellSandbox(deps = {}) {
13992
14376
  return { ready: false, reason: "a folder in the project cannot be read, so it cannot be checked for sockets and hard links" };
13993
14377
  let scratch;
13994
14378
  try {
13995
- scratch = mkdtempSync(join10(realpathSync4(tmpdir()), "freepi-hive-"));
14379
+ scratch = mkdtempSync(join11(realpathSync4(tmpdir()), "freepi-hive-"));
13996
14380
  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 });
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 });
14002
14386
  const roots = toolchainRoots(home);
14003
14387
  const mac = platform === "darwin";
14004
14388
  const host = mac ? macHost() : undefined;
@@ -14050,7 +14434,7 @@ function createShellSandbox(deps = {}) {
14050
14434
  } catch (err) {
14051
14435
  if (scratch)
14052
14436
  removeTree(scratch);
14053
- 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)}.` };
14054
14438
  }
14055
14439
  },
14056
14440
  mark(toolCallId, mode) {
@@ -14147,6 +14531,44 @@ function installSafeUrlOpen(mode, open = openLink) {
14147
14531
  });
14148
14532
  }
14149
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
+
14150
14572
  // src/project-trust.ts
14151
14573
  import { createInterface } from "node:readline/promises";
14152
14574
  import { resolve as resolve3 } from "node:path";
@@ -14293,14 +14715,20 @@ var SDK_TOOLS = [
14293
14715
  "list_mcp_resource_templates",
14294
14716
  "read_mcp_resource"
14295
14717
  ];
14296
- var SKILLS_DIR = resolve4(dirname11(fileURLToPath4(import.meta.url)), "..", "skills");
14718
+ var SKILLS_DIR = resolve4(dirname12(fileURLToPath4(import.meta.url)), "..", "skills");
14297
14719
  function resolveModelName(opts) {
14298
14720
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
14299
14721
  }
14300
14722
  function buildSettingsManager(opts, models, projectTrusted) {
14301
- return SettingsManager2.inMemory({
14302
- defaultProvider: PROVIDER_NAME,
14303
- 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,
14304
14732
  defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
14305
14733
  packages: [],
14306
14734
  quietStartup: true,
@@ -14310,7 +14738,10 @@ function buildSettingsManager(opts, models, projectTrusted) {
14310
14738
  theme: "light/dark",
14311
14739
  enableInstallTelemetry: false,
14312
14740
  ...compactionSettingsFor(opts.costArm) ? { compaction: compactionSettingsFor(opts.costArm) } : {}
14313
- }, { projectTrusted });
14741
+ };
14742
+ const storage = new DefaultModelStorage(opts.agentDir);
14743
+ storage.withLock("global", () => JSON.stringify(settings, null, 2));
14744
+ return SettingsManager2.fromStorage(storage, { projectTrusted });
14314
14745
  }
14315
14746
  function settingsForRuntime(opts, rtCwd, launch) {
14316
14747
  return buildSettingsManager(opts, catalogModelsFor(opts), projectTrustForCwd(rtCwd, launch, opts.agentDir));
@@ -14506,7 +14937,7 @@ ${CHOICE_RULES}` : agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [she
14506
14937
  function getSessionDir(cwd, agentDir) {
14507
14938
  const resolvedCwd = resolve4(cwd);
14508
14939
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
14509
- return join11(resolve4(agentDir), "sessions", safePath);
14940
+ return join12(resolve4(agentDir), "sessions", safePath);
14510
14941
  }
14511
14942
  async function findLatestSession(cwd, sessionDir, api = SessionManager2) {
14512
14943
  const infos = await api.list(cwd, sessionDir);
@@ -14608,11 +15039,13 @@ async function launchPi(opts) {
14608
15039
  installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
14609
15040
  installModelAllowanceHandler(mode);
14610
15041
  installSafeUrlOpen(mode);
15042
+ installRetryFailedLine(mode);
14611
15043
  installFooterRightAlign(mode, () => {
14612
15044
  const id = runtime.session?.model?.id;
14613
15045
  return freeModelBase(pickerModelName(opts, id));
14614
15046
  });
14615
15047
  installEditorPlaceholder(mode);
15048
+ installWidthGuard(mode);
14616
15049
  if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
14617
15050
  process.stdout.write("\x1B[2J\x1B[H");
14618
15051
  installBottomPin(mode);
@@ -14665,7 +15098,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
14665
15098
  // src/relaunch.ts
14666
15099
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
14667
15100
  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";
15101
+ import { delimiter, dirname as dirname13, isAbsolute as isAbsolute3, join as join13, resolve as resolve5, sep as sep4, win32 } from "node:path";
14669
15102
  function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
14670
15103
  try {
14671
15104
  return statSync4(p).isDirectory();
@@ -14706,7 +15139,7 @@ function hasNodeModulesBin(entry) {
14706
15139
  }
14707
15140
  function sanitizedEnv(env, nodeDir, project, home, win) {
14708
15141
  const proj = canon(project, win);
14709
- const broad = proj === canon(home, win) || dirname12(proj) === proj;
15142
+ const broad = proj === canon(home, win) || dirname13(proj) === proj;
14710
15143
  const out = {};
14711
15144
  let pathKey = "PATH";
14712
15145
  let oldPath = "";
@@ -14743,13 +15176,13 @@ async function relaunchViaNpx(latest, deps) {
14743
15176
  if (!latest || !SEMVER.test(latest))
14744
15177
  return;
14745
15178
  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");
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");
14748
15181
  const exists = deps.exists ?? existsSync6;
14749
15182
  const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
14750
15183
  const healthy = (cli) => {
14751
- const root = join12(dirname12(cli), "..");
14752
- 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"));
14753
15186
  };
14754
15187
  let npxCliPath = npxCli;
14755
15188
  if (!exists(npxCli)) {
@@ -14761,8 +15194,8 @@ async function relaunchViaNpx(latest, deps) {
14761
15194
  if (!healthy(npxCliPath))
14762
15195
  return;
14763
15196
  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");
15197
+ const readyDir = mkdtempSync2(join13(tmpdir2(), "free-pi-relaunch-"));
15198
+ const readyFile = join13(readyDir, "started");
14766
15199
  try {
14767
15200
  deps.log(`Updating free-pi to ${target}…`);
14768
15201
  return await new Promise((resolve) => {
@@ -14804,13 +15237,13 @@ async function relaunchViaNpx(latest, deps) {
14804
15237
 
14805
15238
  // src/run.ts
14806
15239
  import { createInterface as createInterface3 } from "node:readline/promises";
14807
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
15240
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
14808
15241
  import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
14809
15242
 
14810
15243
  // src/onboarding.ts
14811
15244
  import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync as realpathSync6 } from "node:fs";
14812
15245
  import { homedir as homedir8 } from "node:os";
14813
- import { join as join13 } from "node:path";
15246
+ import { join as join14 } from "node:path";
14814
15247
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
14815
15248
  var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
14816
15249
  function introSkipped(env = process.env) {
@@ -14830,7 +15263,7 @@ function looksLikeProject(cwd, home = homedir8()) {
14830
15263
  try {
14831
15264
  if (realpathSync6(cwd) === realpathSync6(home))
14832
15265
  return false;
14833
- if (PROJECT_MARKERS.some((m) => existsSync7(join13(cwd, m))))
15266
+ if (PROJECT_MARKERS.some((m) => existsSync7(join14(cwd, m))))
14834
15267
  return true;
14835
15268
  return readdirSync3(cwd).some((f) => /\.(sln|csproj)$/.test(f));
14836
15269
  } catch {
@@ -14859,7 +15292,7 @@ ${text}
14859
15292
  // src/starter.ts
14860
15293
  import { existsSync as existsSync8, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
14861
15294
  import { homedir as homedir9 } from "node:os";
14862
- import { join as join14 } from "node:path";
15295
+ import { join as join15 } from "node:path";
14863
15296
  var STARTER_EXPERIMENT = "first_session_starter";
14864
15297
  var STARTER_PROJECT = [
14865
15298
  {
@@ -14892,7 +15325,7 @@ var STARTER_EMPTY = [
14892
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:";
14893
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:";
14894
15327
  function getStarterMarkerPath(agentDir = getFreePiAgentDir()) {
14895
- return join14(agentDir, "starter-shown");
15328
+ return join15(agentDir, "starter-shown");
14896
15329
  }
14897
15330
  function starterShownBefore(agentDir) {
14898
15331
  return existsSync8(getStarterMarkerPath(agentDir));
@@ -14924,12 +15357,12 @@ function parseStarterKey(raw, count) {
14924
15357
  }
14925
15358
 
14926
15359
  // 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";
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";
14929
15362
  function maybeWhatsNew(cliVersion, agentDir) {
14930
15363
  try {
14931
15364
  const path = getLastSeenVersionPath(agentDir);
14932
- const lastSeen = existsSync9(path) ? readFileSync6(path, "utf-8").trim() : undefined;
15365
+ const lastSeen = existsSync9(path) ? readFileSync7(path, "utf-8").trim() : undefined;
14933
15366
  writeLastSeen(path, cliVersion);
14934
15367
  if (lastSeen === undefined)
14935
15368
  return;
@@ -14945,8 +15378,8 @@ function maybeWhatsNew(cliVersion, agentDir) {
14945
15378
  }
14946
15379
  }
14947
15380
  function writeLastSeen(path, version) {
14948
- mkdirSync4(dirname13(path), { recursive: true });
14949
- writeFileSync4(path, version, "utf-8");
15381
+ mkdirSync5(dirname14(path), { recursive: true });
15382
+ writeFileSync5(path, version, "utf-8");
14950
15383
  }
14951
15384
 
14952
15385
  // src/run.ts
@@ -15197,7 +15630,7 @@ Your free-pi sign-in has expired. Signing in with GitHub again.
15197
15630
  deps.log(SIGNIN_UNFINISHED_LINE);
15198
15631
  return 0;
15199
15632
  }
15200
- const reason = err instanceof Error && err.message ? err.message : "unknown error";
15633
+ const reason = (err instanceof Error && err.message ? err.message : "unknown error").replace(/\.$/, "");
15201
15634
  deps.log(`Couldn't finish GitHub sign-in: ${reason}. Run npx free-pi-cli again to retry.`);
15202
15635
  return 1;
15203
15636
  }
@@ -15393,8 +15826,8 @@ ${r.text}
15393
15826
  const choices = starterChoices();
15394
15827
  if (choices) {
15395
15828
  try {
15396
- mkdirSync5(deps.agentDir, { recursive: true });
15397
- writeFileSync5(getStarterMarkerPath(deps.agentDir), "1");
15829
+ mkdirSync6(deps.agentDir, { recursive: true });
15830
+ writeFileSync6(getStarterMarkerPath(deps.agentDir), "1");
15398
15831
  } catch {}
15399
15832
  const report = (event) => deps.reportStarterEvent ? deps.reportStarterEvent(event) : reportStarterEvent(deps, jwt, event);
15400
15833
  deps.log(formatStarterMenu(choices));
@@ -15547,7 +15980,7 @@ async function main() {
15547
15980
  }
15548
15981
  if (process.env.FREEPI_RELAUNCH_READY) {
15549
15982
  try {
15550
- writeFileSync6(process.env.FREEPI_RELAUNCH_READY, "");
15983
+ writeFileSync7(process.env.FREEPI_RELAUNCH_READY, "");
15551
15984
  } catch {}
15552
15985
  }
15553
15986
  const command = parseCliArgs(process.argv.slice(2));
@@ -15608,7 +16041,7 @@ ${HELP_TEXT}`);
15608
16041
  launchPi,
15609
16042
  log: (message) => console.log(message),
15610
16043
  checkClientVersion: (baseUrl, cliVersion, jwt) => checkClientVersion(baseUrl, cliVersion, fetch, jwt),
15611
- startGravityPixel: (jwt) => startGravityPixel(jwt, join15(dirname14(agentDir), "gravity")),
16044
+ startGravityPixel: (jwt) => startGravityPixel(jwt, join16(dirname15(agentDir), "gravity")),
15612
16045
  relaunchLatest: (latest) => relaunchViaNpx(latest, {
15613
16046
  env: process.env,
15614
16047
  argv: process.argv.slice(2),