free-pi-cli 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,4 +27,4 @@ if (err) {
27
27
  console.error(err);
28
28
  process.exit(1);
29
29
  }
30
- await import("./main-ep5298x9.js");
30
+ await import("./main-jaz36mav.js");
@@ -3,7 +3,7 @@
3
3
  // src/main.ts
4
4
  import { spawn as spawn2 } from "node:child_process";
5
5
  import { realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
6
- import { dirname as dirname5, join as join6 } from "node:path";
6
+ import { dirname as dirname5, join as join7 } from "node:path";
7
7
 
8
8
  // src/browser.ts
9
9
  import { spawn } from "node:child_process";
@@ -46,7 +46,7 @@ function openBrowser(url, platform = process.platform, spawnImpl = spawn) {
46
46
  // package.json
47
47
  var package_default = {
48
48
  name: "free-pi-cli",
49
- version: "0.4.7",
49
+ version: "0.4.9",
50
50
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
51
51
  license: "MIT",
52
52
  type: "module",
@@ -4906,7 +4906,8 @@ var ClientVersionResponseSchema = object({
4906
4906
  latest: string2(),
4907
4907
  model: string2().optional(),
4908
4908
  notice: string2().optional(),
4909
- models: array(object({ id: string2(), name: string2() })).optional()
4909
+ models: array(object({ id: string2(), name: string2() })).optional(),
4910
+ fusion: object({ planner: string2(), executor: string2() }).optional()
4910
4911
  });
4911
4912
  var SessionResetResponseSchema = object({
4912
4913
  released: boolean2()
@@ -4970,6 +4971,7 @@ var ConfigSchema = object({
4970
4971
  abuseRestrictedCapUsd: number2().default(0.5),
4971
4972
  abuseRestrictedHourlyCap: number2().int().default(5),
4972
4973
  abuseNormalHourlyCap: number2().int().default(30),
4974
+ abuseSignedInNormalHourlyCap: number2().int().default(0),
4973
4975
  abuseFlipAlertThreshold: number2().int().default(10),
4974
4976
  abuseAdBypassMinRequests: number2().int().default(20),
4975
4977
  abuseAdBypassRatio: number2().int().default(10),
@@ -5042,6 +5044,7 @@ function loadConfig(env = process.env) {
5042
5044
  abuseRestrictedCapUsd: envNumber(env.ABUSE_RESTRICTED_CAP_USD),
5043
5045
  abuseRestrictedHourlyCap: envNumber(env.ABUSE_RESTRICTED_HOURLY_CAP),
5044
5046
  abuseNormalHourlyCap: envNumber(env.ABUSE_NORMAL_HOURLY_CAP),
5047
+ abuseSignedInNormalHourlyCap: envNumber(env.ABUSE_SIGNED_IN_NORMAL_HOURLY_CAP),
5045
5048
  abuseFlipAlertThreshold: envNumber(env.ABUSE_FLIP_ALERT_THRESHOLD),
5046
5049
  abuseAdBypassMinRequests: envNumber(env.ABUSE_AD_BYPASS_MIN_REQUESTS),
5047
5050
  abuseAdBypassRatio: envNumber(env.ABUSE_AD_BYPASS_RATIO),
@@ -5086,6 +5089,42 @@ function formatCreditsLeftLine(remaining, allowance, refillAt) {
5086
5089
  return `${left} / ${total} credits left today · refills ${formatRefillEt(refillAt)}`;
5087
5090
  }
5088
5091
  var SLOW_LANE_METER_LINE = "Slow lane: cheapest model until refill";
5092
+ var LIMIT_NOTICE_LABELS = {
5093
+ hourly_ceiling: "hourly limit",
5094
+ daily_cap: "daily limit",
5095
+ rolling_cap: "rolling limit",
5096
+ global_cap: "service daily limit",
5097
+ queue_wait: "queue wait",
5098
+ concurrent: "one request at a time",
5099
+ concurrent_session: "another session is active"
5100
+ };
5101
+ var EXTRA_CREDITS_URL = "freepi.ai/extra-credits";
5102
+ var LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS = 90 * 60;
5103
+ function formatLimitNotice(fields, opts = {}) {
5104
+ const slow = fields.lane === "slow" && fields.limit === "hourly_ceiling" && fields.hourly_ceiling != null;
5105
+ const label = slow ? `You've used today's free allowance, so you're on the slow lane (${fields.hourly_ceiling.toLocaleString("en-US")} requests an hour).` : LIMIT_NOTICE_LABELS[fields.limit] ?? fields.limit.replace(/_/g, " ");
5106
+ const parts = [label];
5107
+ const retry = fields.retry_after_seconds;
5108
+ if (retry != null && retry > 0) {
5109
+ const useClock = Boolean(opts.localTime) && Boolean(fields.clears_at) && retry >= LIMIT_NOTICE_LOCAL_TIME_AFTER_SECONDS;
5110
+ if (useClock) {
5111
+ const formatted = new Intl.DateTimeFormat("en-US", {
5112
+ timeZone: opts.timeZone,
5113
+ hour: "numeric",
5114
+ minute: "2-digit"
5115
+ }).format(new Date(fields.clears_at));
5116
+ parts.push(`back at ${formatted}`);
5117
+ } else {
5118
+ parts.push(`back in ${Math.max(1, Math.ceil(retry / 60))} min`);
5119
+ }
5120
+ }
5121
+ if (slow) {
5122
+ const wait = parts.length > 1 ? ` ${parts[1][0].toUpperCase()}${parts[1].slice(1)} — or get` : " Get";
5123
+ return `${label}${wait} credits at ${EXTRA_CREDITS_URL} to keep going at full speed.`;
5124
+ }
5125
+ parts.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
5126
+ return parts.join(", ");
5127
+ }
5089
5128
  // src/gravity-pixel.ts
5090
5129
  var GRAVITY_PIXEL_ID = "6cd74804-0541-4d9b-899d-d7fceb465b6f";
5091
5130
  function userIdFromJwt(jwt) {
@@ -8129,7 +8168,7 @@ function createAdsExtension(opts) {
8129
8168
  }
8130
8169
 
8131
8170
  // src/pi-launch.ts
8132
- import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
8171
+ import { dirname as dirname3, join as join5, resolve as resolve2 } from "node:path";
8133
8172
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8134
8173
 
8135
8174
  // src/provider.ts
@@ -8545,6 +8584,104 @@ function createUsageToolExtension(opts) {
8545
8584
  };
8546
8585
  }
8547
8586
 
8587
+ // src/image-tool.ts
8588
+ import { randomBytes } from "node:crypto";
8589
+ import { mkdir as mkdir2, open } from "node:fs/promises";
8590
+ import { join as join3 } from "node:path";
8591
+ var IMAGE_TOOL_NAME = "generate_image";
8592
+ var MAX_BYTES = 8 * 1024 * 1024;
8593
+ var TIMEOUT_MS = 120000;
8594
+ var EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
8595
+ var IMAGE_GUIDELINES = [
8596
+ "Use generate_image only when the user asks for an image, mockup, icon, illustration or other visual asset. Do not call it for diagrams you can write as code, SVG or ASCII.",
8597
+ "Make one call per asset. Put the style, any exact text to render, and the composition in the prompt, and pick aspect_ratio to suit the use (16:9 for banners, 1:1 for icons).",
8598
+ "You cannot see the generated image. The tool saves it to a file and returns the relative path; refer to that path in code, docs or your answer, and tell the user you could not check how it looks.",
8599
+ "Each image uses part of the user's daily allowance, so do not regenerate unless the user asks."
8600
+ ];
8601
+ var ERROR_TEXT = {
8602
+ daily_cap: "The user's free allowance for today is used up. Do not retry. Tell the user they can get credits at freepi.ai/extra-credits, and continue without this tool.",
8603
+ rolling_cap: "The user's short-term usage limit is reached. Do not retry; continue without this tool.",
8604
+ rate_limited: "Too many image requests right now. Do not call generate_image again in this turn unless the user asks.",
8605
+ not_configured: "Image generation is not available on this server. Do not call generate_image again in this session.",
8606
+ upstream_error: "The image provider failed. You may retry once.",
8607
+ account_review: "This account is under review, so image generation is disabled. Do not retry; tell the user and continue without it.",
8608
+ content_rejected: "The image provider rejected this prompt. Do not retry the same prompt; tell the user and offer to reword it."
8609
+ };
8610
+ async function post(opts, body, signal) {
8611
+ try {
8612
+ const timeout = AbortSignal.timeout(TIMEOUT_MS);
8613
+ const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl}/tools/image`, {
8614
+ method: "POST",
8615
+ headers: { Authorization: `Bearer ${await opts.getToken()}`, "Content-Type": "application/json" },
8616
+ body: JSON.stringify(body),
8617
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout
8618
+ });
8619
+ if (res.ok)
8620
+ return { ok: true, data: await res.json() };
8621
+ const j = await res.json().catch(() => ({}));
8622
+ const code = j.error?.code ?? j.code ?? "";
8623
+ const mapped = ERROR_TEXT[code];
8624
+ if (mapped)
8625
+ return { ok: false, text: mapped };
8626
+ return { ok: false, text: `Request failed (HTTP ${res.status}): ${j.error?.message ?? j.message ?? "no message"}` };
8627
+ } catch (e) {
8628
+ if (e instanceof Error && e.name === "TimeoutError")
8629
+ return { ok: false, text: "Image generation timed out after 120 seconds." };
8630
+ return { ok: false, text: `Request failed: ${String(e)}` };
8631
+ }
8632
+ }
8633
+ function slug(s) {
8634
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").slice(0, 40).replace(/-+$/, "") || "image";
8635
+ }
8636
+ var done = (text, details) => ({ content: [{ type: "text", text }], details });
8637
+ var fail = (text) => done(text, { ok: false });
8638
+ function createImageToolExtension(opts) {
8639
+ return {
8640
+ name: "free-pi-image-tool",
8641
+ factory(pi) {
8642
+ pi.registerTool({
8643
+ name: IMAGE_TOOL_NAME,
8644
+ label: "Generate Image",
8645
+ description: "Generate one image from a text prompt and save it as a file under images/ in the project. Returns the file path; you cannot see the image. Use it only when the user asks for an image, mockup, icon or illustration. Each call uses part of the user's daily allowance.",
8646
+ promptSnippet: "Generate an image file from a prompt when the user asks for one (counts against the user's daily allowance)",
8647
+ promptGuidelines: IMAGE_GUIDELINES,
8648
+ parameters: _Object_({
8649
+ prompt: String2({ minLength: 1, maxLength: 1000, description: "What to draw: subject, style, any exact text to render, and composition" }),
8650
+ aspect_ratio: Optional(Union([Literal("1:1"), Literal("16:9"), Literal("9:16"), Literal("4:3"), Literal("3:2")], { description: "Default 1:1" })),
8651
+ name: Optional(String2({ maxLength: 60, description: "Short file name hint, e.g. hero-banner; used only for the saved file's name" }))
8652
+ }),
8653
+ async execute(_id, params, signal) {
8654
+ const r = await post(opts, { prompt: params.prompt, ...params.aspect_ratio ? { aspect_ratio: params.aspect_ratio } : {} }, signal);
8655
+ if (!r.ok)
8656
+ return fail(r.text);
8657
+ const d = r.data;
8658
+ const ext = EXT[d.media_type];
8659
+ if (!ext)
8660
+ return fail(`The server returned an unsupported image type (${d.media_type}). Nothing was saved.`);
8661
+ const buf = Buffer.from(d.image_b64 ?? "", "base64");
8662
+ if (buf.length === 0)
8663
+ return fail("The server returned an empty image. Nothing was saved.");
8664
+ if (buf.length > MAX_BYTES)
8665
+ return fail("The generated image was too large to save (over 8 MB). Nothing was saved.");
8666
+ const rel = `images/${slug(params.name ?? params.prompt)}-${randomBytes(3).toString("hex")}.${ext}`;
8667
+ try {
8668
+ await mkdir2(join3(opts.cwd ?? process.cwd(), "images"), { recursive: true });
8669
+ const fh = await open(join3(opts.cwd ?? process.cwd(), rel), "wx");
8670
+ try {
8671
+ await fh.writeFile(buf);
8672
+ } finally {
8673
+ await fh.close();
8674
+ }
8675
+ } catch (e) {
8676
+ return fail(`Could not save the image: ${String(e)}`);
8677
+ }
8678
+ 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 });
8679
+ }
8680
+ });
8681
+ }
8682
+ };
8683
+ }
8684
+
8548
8685
  // src/web-tools.ts
8549
8686
  var WEB_SEARCH_TOOL_NAME = "web_search";
8550
8687
  var DOCS_LOOKUP_TOOL_NAME = "docs_lookup";
@@ -8559,7 +8696,7 @@ var WEB_SEARCH_GUIDELINES = [
8559
8696
  "Write a specific query the way you would type it into a search engine (include the product name, the version and the exact error text). Read the returned page text before searching again, and do not repeat a query that already returned results. Cite the URLs you relied on in your answer.",
8560
8697
  COST_GUIDELINE
8561
8698
  ];
8562
- var ERROR_TEXT = {
8699
+ var ERROR_TEXT2 = {
8563
8700
  daily_cap: "The user's daily free usage is used up. Do not retry; tell the user and continue without this tool.",
8564
8701
  rolling_cap: "The user's short-term usage limit is reached. Do not retry; continue without this tool.",
8565
8702
  rate_limited: "Too many calls right now. Wait for the current call to finish, and do not call again in this turn unless needed.",
@@ -8567,7 +8704,7 @@ var ERROR_TEXT = {
8567
8704
  not_configured: "This tool is not available on this server. Do not call it again in this session.",
8568
8705
  upstream_error: "The search provider failed. You may retry once."
8569
8706
  };
8570
- async function post(opts, path, body, label, signal) {
8707
+ async function post2(opts, path, body, label, signal) {
8571
8708
  try {
8572
8709
  const timeout = AbortSignal.timeout(30000);
8573
8710
  const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl}${path}`, {
@@ -8579,7 +8716,7 @@ async function post(opts, path, body, label, signal) {
8579
8716
  if (res.ok)
8580
8717
  return { ok: true, data: await res.json() };
8581
8718
  const err = await res.json().catch(() => ({}));
8582
- const mapped = ERROR_TEXT[err.code ?? ""];
8719
+ const mapped = ERROR_TEXT2[err.code ?? ""];
8583
8720
  if (mapped)
8584
8721
  return { ok: false, text: typeof mapped === "function" ? mapped(label) : mapped };
8585
8722
  return { ok: false, text: `Request failed (HTTP ${res.status}): ${err.message ?? "no message"}` };
@@ -8589,7 +8726,7 @@ async function post(opts, path, body, label, signal) {
8589
8726
  return { ok: false, text: `Request failed: ${String(e)}` };
8590
8727
  }
8591
8728
  }
8592
- var done = (o) => ({ content: [{ type: "text", text: o.text }], details: o.details });
8729
+ var done2 = (o) => ({ content: [{ type: "text", text: o.text }], details: o.details });
8593
8730
  function createWebToolsExtension(opts) {
8594
8731
  return {
8595
8732
  name: "free-pi-web-tools",
@@ -8605,9 +8742,9 @@ function createWebToolsExtension(opts) {
8605
8742
  num_results: Optional(Integer({ minimum: 1, maximum: 10, description: "1-10, default 5; use 3 for a quick fact, 8-10 for a survey" }))
8606
8743
  }),
8607
8744
  async execute(_id, params, signal) {
8608
- const r = await post(opts, "/tools/web-search", params, "", signal);
8745
+ const r = await post2(opts, "/tools/web-search", params, "", signal);
8609
8746
  if (!r.ok)
8610
- return done({ text: r.text, details: { ok: false } });
8747
+ return done2({ text: r.text, details: { ok: false } });
8611
8748
  const results = r.data.results ?? [];
8612
8749
  const text = results.length ? `Web results for "${params.query}":
8613
8750
 
@@ -8617,7 +8754,7 @@ Published: ${x.published_date}` : ""}
8617
8754
  ${x.text}`).join(`
8618
8755
 
8619
8756
  `) : `No results for "${params.query}". Try a broader or differently worded query.`;
8620
- return done({ text, details: { ok: true, cost_usd: r.data.cost_usd } });
8757
+ return done2({ text, details: { ok: true, cost_usd: r.data.cost_usd } });
8621
8758
  }
8622
8759
  });
8623
8760
  pi.registerTool({
@@ -8632,11 +8769,11 @@ ${x.text}`).join(`
8632
8769
  library_id: Optional(String2({ description: "The library_id printed by an earlier docs_lookup, e.g. /vercel/next.js; skips the name search" }))
8633
8770
  }),
8634
8771
  async execute(_id, params, signal) {
8635
- const r = await post(opts, "/tools/docs", params, params.library, signal);
8772
+ const r = await post2(opts, "/tools/docs", params, params.library, signal);
8636
8773
  if (!r.ok)
8637
- return done({ text: r.text, details: { ok: false } });
8774
+ return done2({ text: r.text, details: { ok: false } });
8638
8775
  const d = r.data;
8639
- return done({ text: `${d.title} (library_id: ${d.library_id})
8776
+ return done2({ text: `${d.title} (library_id: ${d.library_id})
8640
8777
 
8641
8778
  ${d.docs}`, details: { ok: true, cost_usd: d.cost_usd } });
8642
8779
  }
@@ -9051,7 +9188,7 @@ function createTodoToolExtension(options = {}) {
9051
9188
 
9052
9189
  // src/tool-aliases.ts
9053
9190
  import { homedir as homedir2 } from "node:os";
9054
- import { join as join3 } from "node:path";
9191
+ import { join as join4 } from "node:path";
9055
9192
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9056
9193
  import {
9057
9194
  createBashToolDefinition,
@@ -9077,7 +9214,7 @@ function normalizeShellPath(input, platform = process.platform, home = homedir2(
9077
9214
  if (p === "~")
9078
9215
  return home;
9079
9216
  if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
9080
- return join3(home, p.slice(2));
9217
+ return join4(home, p.slice(2));
9081
9218
  if (/^file:\/\//.test(p))
9082
9219
  return fileURLToPath2(p);
9083
9220
  return p;
@@ -9109,6 +9246,12 @@ function createToolAliasesExtension() {
9109
9246
 
9110
9247
  // src/error-notice.ts
9111
9248
  var NOTICE_DEDUPE_MS = 15000;
9249
+ function optionalNumber(value) {
9250
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
9251
+ }
9252
+ function optionalString(value) {
9253
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
9254
+ }
9112
9255
  function parseErrorEnvelope(errorMessage) {
9113
9256
  if (typeof errorMessage !== "string")
9114
9257
  return;
@@ -9121,14 +9264,53 @@ function parseErrorEnvelope(errorMessage) {
9121
9264
  const code = body.code ?? body.error?.code;
9122
9265
  if (typeof message !== "string" || message.trim() === "")
9123
9266
  return;
9124
- return { status: Number(m[1]), code: typeof code === "string" ? code : "unknown", message };
9267
+ const parsed = {
9268
+ status: Number(m[1]),
9269
+ code: typeof code === "string" ? code : "unknown",
9270
+ message
9271
+ };
9272
+ const limit = optionalString(body.limit);
9273
+ const retryAfter = optionalNumber(body.retry_after_seconds);
9274
+ const clearsAt = optionalString(body.clears_at);
9275
+ const remaining = optionalNumber(body.remaining_credits_today);
9276
+ const allowance = optionalNumber(body.allowance_credits);
9277
+ if (limit)
9278
+ parsed.limit = limit;
9279
+ if (retryAfter !== undefined)
9280
+ parsed.retry_after_seconds = retryAfter;
9281
+ if (clearsAt)
9282
+ parsed.clears_at = clearsAt;
9283
+ if (remaining !== undefined)
9284
+ parsed.remaining_credits_today = remaining;
9285
+ if (allowance !== undefined)
9286
+ parsed.allowance_credits = allowance;
9287
+ const ceiling = optionalNumber(body.hourly_ceiling);
9288
+ if (body.lane === "slow")
9289
+ parsed.lane = "slow";
9290
+ if (ceiling !== undefined)
9291
+ parsed.hourly_ceiling = ceiling;
9292
+ return parsed;
9125
9293
  } catch {
9126
9294
  return;
9127
9295
  }
9128
9296
  }
9297
+ function formatLimitEnvelope(env, opts) {
9298
+ if (env.code === "concurrent_session" || typeof env.remaining_credits_today !== "number")
9299
+ return env.message;
9300
+ return formatLimitNotice({
9301
+ limit: env.limit ?? env.code,
9302
+ retry_after_seconds: env.retry_after_seconds,
9303
+ clears_at: env.clears_at,
9304
+ remaining_credits_today: env.remaining_credits_today,
9305
+ allowance_credits: env.allowance_credits,
9306
+ lane: env.lane,
9307
+ hourly_ceiling: env.hourly_ceiling
9308
+ }, { localTime: true, timeZone: opts?.timeZone });
9309
+ }
9129
9310
  function createErrorNoticeExtension(opts = {}) {
9130
9311
  const lastAt = new Map;
9131
9312
  const now = opts.now ?? Date.now;
9313
+ const timeZone = opts.timeZone;
9132
9314
  return {
9133
9315
  name: "free-pi-error-notice",
9134
9316
  factory(pi) {
@@ -9143,7 +9325,7 @@ function createErrorNoticeExtension(opts = {}) {
9143
9325
  if (t - (lastAt.get(env.code) ?? -Infinity) < NOTICE_DEDUPE_MS)
9144
9326
  return;
9145
9327
  lastAt.set(env.code, t);
9146
- ctx.ui.notify(env.message, env.status >= 500 ? "error" : "warning");
9328
+ ctx.ui.notify(formatLimitEnvelope(env, { timeZone }), env.status >= 500 ? "error" : "warning");
9147
9329
  });
9148
9330
  }
9149
9331
  };
@@ -9277,6 +9459,19 @@ function installFooterRightAlign(mode) {
9277
9459
 
9278
9460
  // src/changelog.ts
9279
9461
  var CHANGELOG_HIGHLIGHTS = [
9462
+ {
9463
+ version: "0.4.9",
9464
+ highlights: [
9465
+ "\uD83E\uDDE8 Fusion is now much cheaper: DeepSeek V4 Pro plans and DeepSeek V4 Flash builds, so each Fusion turn uses a small fraction of the allowance it used to. Fusion is also listed first in /model."
9466
+ ]
9467
+ },
9468
+ {
9469
+ version: "0.4.8",
9470
+ highlights: [
9471
+ "The agent can now make images: ask for a mockup, icon or illustration and generate_image saves it into your project's images/ folder. Each image counts toward your daily allowance.",
9472
+ "Clearer limit messages: you now see when a limit clears and how many credits you have left today."
9473
+ ]
9474
+ },
9280
9475
  {
9281
9476
  version: "0.4.7",
9282
9477
  highlights: [
@@ -9708,9 +9903,18 @@ function createHeaderExtension(opts) {
9708
9903
 
9709
9904
  // src/fusion.ts
9710
9905
  var FUSION_ID = "fusion";
9711
- var FUSION_NAME = "Fusion · Argon plans, DeepSeek builds";
9712
- var FUSION_PLANNER_ID = "gemini-4-argon-flex";
9906
+ var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
9907
+ var FUSION_NAME = "\uD83E\uDDE8 Fusion · DeepSeek Pro plans, Flash builds";
9908
+ var FUSION_FALLBACK_NAME = "\uD83E\uDDE8 Fusion · Argon plans, DeepSeek builds";
9909
+ var FUSION_PLANNER_ID = "deepseek-v4-pro";
9713
9910
  var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
9911
+ var FUSION_FALLBACK_PLANNER_ID = "gemini-4-argon-flex";
9912
+ function resolveFusionIds(catalogIds, server) {
9913
+ const has = (id) => catalogIds.includes(id);
9914
+ if (server && has(server.planner) && has(server.executor))
9915
+ return server;
9916
+ return { planner: has(FUSION_PLANNER_ID) ? FUSION_PLANNER_ID : FUSION_FALLBACK_PLANNER_ID, executor: FUSION_EXECUTOR_ID };
9917
+ }
9714
9918
  function editedThisTurn(messages) {
9715
9919
  for (let i = messages.length - 1;i >= 0; i--) {
9716
9920
  const message = messages[i];
@@ -9755,7 +9959,7 @@ function routeFusion(request, models) {
9755
9959
  if (restartPlan)
9756
9960
  return route(models.planner, "plan");
9757
9961
  const failed = request.failed;
9758
- if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === FUSION_PLANNER_ID) {
9962
+ if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === models.planner?.id) {
9759
9963
  return route(models.executor, "build");
9760
9964
  }
9761
9965
  return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
@@ -9764,21 +9968,24 @@ function routeFusion(request, models) {
9764
9968
  return phase === "build" || editedThisTurn(request.messages) ? route(models.executor, "build") : route(models.planner, "plan");
9765
9969
  }
9766
9970
  }
9767
- function createFusionExtension(defaultModelId) {
9971
+ function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
9972
+ const name = ids.planner === FUSION_PLANNER_ID && ids.executor === FUSION_EXECUTOR_ID ? FUSION_NAME : ids.planner === FUSION_FALLBACK_PLANNER_ID ? FUSION_FALLBACK_NAME : "\uD83E\uDDE8 Fusion";
9768
9973
  return {
9769
9974
  name: "free-pi-fusion",
9770
9975
  factory: (pi) => {
9771
- pi.registerVirtualModel({
9772
- provider: PROVIDER_NAME,
9773
- id: FUSION_ID,
9774
- name: FUSION_NAME,
9775
- input: ["text"],
9776
- route: (request, ctx) => routeFusion(request, {
9777
- planner: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_PLANNER_ID),
9778
- executor: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_EXECUTOR_ID),
9779
- defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
9780
- })
9781
- });
9976
+ for (const id of [FUSION_PICKER_ID, FUSION_ID]) {
9977
+ pi.registerVirtualModel({
9978
+ provider: PROVIDER_NAME,
9979
+ id,
9980
+ name,
9981
+ input: ["text"],
9982
+ route: (request, ctx) => routeFusion(request, {
9983
+ planner: ctx.modelRegistry.find(PROVIDER_NAME, ids.planner),
9984
+ executor: ctx.modelRegistry.find(PROVIDER_NAME, ids.executor),
9985
+ defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
9986
+ })
9987
+ });
9988
+ }
9782
9989
  }
9783
9990
  };
9784
9991
  }
@@ -9788,7 +9995,7 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
9788
9995
  if (env.FREEPI_KEEP_ENV_PROVIDERS)
9789
9996
  return [];
9790
9997
  const scoped = [];
9791
- for (const id of new Set([...modelIds, FUSION_ID])) {
9998
+ for (const id of new Set([FUSION_PICKER_ID, ...modelIds])) {
9792
9999
  const model = modelRuntime.getModel(providerName, id);
9793
10000
  if (model)
9794
10001
  scoped.push({ model });
@@ -9872,6 +10079,7 @@ var ALLOWED_TOOL_NAMES = [
9872
10079
  DOCS_TOOL_NAME,
9873
10080
  WEB_SEARCH_TOOL_NAME,
9874
10081
  DOCS_LOOKUP_TOOL_NAME,
10082
+ IMAGE_TOOL_NAME,
9875
10083
  TODO_TOOL_NAME,
9876
10084
  "codemode",
9877
10085
  "tool_search"
@@ -9934,6 +10142,10 @@ function buildRuntimeOptions(opts, sessionId) {
9934
10142
  baseUrl: opts.baseUrl,
9935
10143
  getToken: () => opts.jwt
9936
10144
  });
10145
+ const imageToolExtension = createImageToolExtension({
10146
+ baseUrl: opts.baseUrl,
10147
+ getToken: () => opts.jwt
10148
+ });
9937
10149
  const errorNoticeExtension = createErrorNoticeExtension();
9938
10150
  const toolGuardExtension = createToolGuardExtension({
9939
10151
  baseUrl: opts.baseUrl,
@@ -9956,12 +10168,13 @@ function buildRuntimeOptions(opts, sessionId) {
9956
10168
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
9957
10169
  const extensionFactories = [
9958
10170
  providerExtension,
9959
- createFusionExtension(models[0].id),
10171
+ createFusionExtension(models[0].id, resolveFusionIds(models.map((m) => m.id), opts.fusion)),
9960
10172
  adsExtension,
9961
10173
  usageToolExtension,
9962
10174
  buyToolExtension,
9963
10175
  docsToolExtension,
9964
10176
  webToolsExtension,
10177
+ imageToolExtension,
9965
10178
  errorNoticeExtension,
9966
10179
  toolGuardExtension,
9967
10180
  commandsExtension,
@@ -9987,7 +10200,7 @@ function buildRuntimeOptions(opts, sessionId) {
9987
10200
  function getSessionDir(cwd, agentDir) {
9988
10201
  const resolvedCwd = resolve2(cwd);
9989
10202
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
9990
- return join4(resolve2(agentDir), "sessions", safePath);
10203
+ return join5(resolve2(agentDir), "sessions", safePath);
9991
10204
  }
9992
10205
  async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager) {
9993
10206
  if (sessionId) {
@@ -10132,7 +10345,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
10132
10345
  // src/relaunch.ts
10133
10346
  import { mkdtempSync, existsSync as existsSync2, rmSync } from "node:fs";
10134
10347
  import { constants as osConstants, tmpdir } from "node:os";
10135
- import { join as join5 } from "node:path";
10348
+ import { join as join6 } from "node:path";
10136
10349
  var SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
10137
10350
  var WIN_SAFE_ARG = /^[A-Za-z0-9._:=\/@+,-]+$/;
10138
10351
  async function relaunchViaNpx(latest, deps) {
@@ -10145,8 +10358,8 @@ async function relaunchViaNpx(latest, deps) {
10145
10358
  if (win && !deps.argv.every((a) => WIN_SAFE_ARG.test(a)))
10146
10359
  return;
10147
10360
  const target = latest && SEMVER.test(latest) ? latest : "latest";
10148
- const readyDir = mkdtempSync(join5(tmpdir(), "free-pi-relaunch-"));
10149
- const readyFile = join5(readyDir, "started");
10361
+ const readyDir = mkdtempSync(join6(tmpdir(), "free-pi-relaunch-"));
10362
+ const readyFile = join6(readyDir, "started");
10150
10363
  try {
10151
10364
  deps.log(`Updating free-pi to ${target}…`);
10152
10365
  return await new Promise((resolve) => {
@@ -10444,6 +10657,7 @@ async function run(deps, argv = process.argv) {
10444
10657
  session: sessionArg.session,
10445
10658
  model: versionCheck.model,
10446
10659
  models: versionCheck.models,
10660
+ fusion: versionCheck.fusion,
10447
10661
  updateLatest,
10448
10662
  getGravityContext
10449
10663
  });
@@ -10500,7 +10714,7 @@ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch) {
10500
10714
  const parsed = ClientVersionResponseSchema.safeParse(await res.json());
10501
10715
  if (!parsed.success)
10502
10716
  return { action: "ok" };
10503
- const { min, latest, model, models, notice } = parsed.data;
10717
+ const { min, latest, model, models, notice, fusion } = parsed.data;
10504
10718
  const extra = {};
10505
10719
  if (model !== undefined)
10506
10720
  extra.model = model;
@@ -10508,6 +10722,8 @@ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch) {
10508
10722
  extra.models = models;
10509
10723
  if (notice !== undefined)
10510
10724
  extra.notice = notice;
10725
+ if (fusion !== undefined)
10726
+ extra.fusion = fusion;
10511
10727
  if (compareVersions(cliVersion, min) === -1)
10512
10728
  return { action: "block", latest, ...extra };
10513
10729
  if (compareVersions(cliVersion, latest) === -1)
@@ -10576,7 +10792,7 @@ ${HELP_TEXT}`);
10576
10792
  launchPi,
10577
10793
  log: (message) => console.log(message),
10578
10794
  checkClientVersion,
10579
- startGravityPixel: (jwt) => startGravityPixel(jwt, join6(dirname5(agentDir), "gravity")),
10795
+ startGravityPixel: (jwt) => startGravityPixel(jwt, join7(dirname5(agentDir), "gravity")),
10580
10796
  relaunchLatest: (latest) => relaunchViaNpx(latest, {
10581
10797
  env: process.env,
10582
10798
  argv: process.argv.slice(2),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
5
5
  "license": "MIT",
6
6
  "type": "module",