free-pi-cli 0.6.14 → 0.6.15

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 spawn3 } from "node:child_process";
5
- import { realpathSync as realpathSync8, writeFileSync as writeFileSync5 } from "node:fs";
6
- import { homedir as homedir9 } from "node:os";
7
- import { dirname as dirname13, join as join15 } from "node:path";
5
+ import { realpathSync as realpathSync8, writeFileSync as writeFileSync6 } from "node:fs";
6
+ import { homedir as homedir10 } from "node:os";
7
+ import { dirname as dirname14, join as join15 } 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.14",
518
+ version: "0.6.15",
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",
@@ -530,6 +530,13 @@ var package_default = {
530
530
  ],
531
531
  homepage: "https://freepi.ai",
532
532
  license: "MIT",
533
+ repository: {
534
+ type: "git",
535
+ url: "git+https://github.com/dennisonbertram/free-pi-cli.git"
536
+ },
537
+ bugs: {
538
+ url: "https://github.com/dennisonbertram/free-pi-cli/issues"
539
+ },
533
540
  type: "module",
534
541
  bin: {
535
542
  "free-pi-cli": "dist/index.js"
@@ -543,7 +550,7 @@ var package_default = {
543
550
  node: ">=22.15.0"
544
551
  },
545
552
  scripts: {
546
- build: "rm -rf dist && bun build src/index.ts --target=node --outdir dist --splitting --external @earendil-works/pi-coding-agent --external @earendil-works/pi-tui --banner '#!/usr/bin/env node' && chmod +x dist/index.js",
553
+ build: "bun scripts/build.ts",
547
554
  prepublishOnly: "bun run build"
548
555
  },
549
556
  dependencies: {
@@ -605,13 +612,13 @@ function splitCatalogModels(models) {
605
612
  defense: models.filter((m) => m.section === "defense")
606
613
  };
607
614
  }
608
- function buildProviderConfig(baseUrl, jwt, sessionId, models, name = PROVIDER_NAME) {
615
+ function buildProviderConfig(baseUrl, jwt, sessionId, models, name = PROVIDER_NAME, extraHeaders = {}) {
609
616
  return {
610
617
  name,
611
618
  baseUrl: completionsBaseUrl(baseUrl),
612
619
  apiKey: jwt,
613
620
  api: "openai-completions",
614
- headers: { "x-session-id": sessionId, "x-client-version": CLI_VERSION },
621
+ headers: { "x-session-id": sessionId, "x-client-version": CLI_VERSION, ...extraHeaders },
615
622
  models: models.map((m) => ({
616
623
  id: m.id,
617
624
  name: m.name,
@@ -1057,6 +1064,9 @@ function getLastTipPath(agentDir = getFreePiAgentDir()) {
1057
1064
  function getConfigPath() {
1058
1065
  return join2(homedir(), FREE_PI_DIR_NAME, "config.json");
1059
1066
  }
1067
+ function getToolModePath(agentDir = getFreePiAgentDir()) {
1068
+ return join2(agentDir, "tools-mode");
1069
+ }
1060
1070
 
1061
1071
  // src/tips.ts
1062
1072
  var TIPS = [
@@ -1787,7 +1797,6 @@ var string = (params) => {
1787
1797
  const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
1788
1798
  return new RegExp(`^${regex}$`);
1789
1799
  };
1790
- var bigint = /^-?\d+n?$/;
1791
1800
  var integer = /^-?\d+$/;
1792
1801
  var number = /^-?\d+(?:\.\d+)?$/;
1793
1802
  var boolean = /^(?:true|false)$/i;
@@ -2692,25 +2701,6 @@ var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
2692
2701
  return payload;
2693
2702
  };
2694
2703
  });
2695
- var $ZodBigInt = /* @__PURE__ */ $constructor("$ZodBigInt", (inst, def) => {
2696
- $ZodType.init(inst, def);
2697
- inst._zod.pattern = bigint;
2698
- inst._zod.parse = (payload, _ctx) => {
2699
- if (def.coerce)
2700
- try {
2701
- payload.value = BigInt(payload.value);
2702
- } catch (_) {}
2703
- if (typeof payload.value === "bigint")
2704
- return payload;
2705
- payload.issues.push({
2706
- expected: "bigint",
2707
- code: "invalid_type",
2708
- input: payload.value,
2709
- inst
2710
- });
2711
- return payload;
2712
- };
2713
- });
2714
2704
  var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
2715
2705
  $ZodType.init(inst, def);
2716
2706
  inst._zod.parse = (payload) => payload;
@@ -2727,29 +2717,6 @@ var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => {
2727
2717
  return payload;
2728
2718
  };
2729
2719
  });
2730
- var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
2731
- $ZodType.init(inst, def);
2732
- inst._zod.parse = (payload, _ctx) => {
2733
- if (def.coerce) {
2734
- try {
2735
- payload.value = new Date(payload.value);
2736
- } catch (_err) {}
2737
- }
2738
- const input = payload.value;
2739
- const isDate = input instanceof Date;
2740
- const isValidDate = isDate && !Number.isNaN(input.getTime());
2741
- if (isValidDate)
2742
- return payload;
2743
- payload.issues.push({
2744
- expected: "date",
2745
- code: "invalid_type",
2746
- input,
2747
- ...isDate ? { received: "Invalid Date" } : {},
2748
- inst
2749
- });
2750
- return payload;
2751
- };
2752
- });
2753
2720
  function handleArrayResult(result, final, index) {
2754
2721
  if (result.issues.length) {
2755
2722
  final.issues.push(...prefixIssues(index, result.issues));
@@ -4039,12 +4006,6 @@ function _boolean(Class, params) {
4039
4006
  ...normalizeParams(params)
4040
4007
  });
4041
4008
  }
4042
- function _bigint(Class, params) {
4043
- return new Class({
4044
- type: "bigint",
4045
- ...normalizeParams(params)
4046
- });
4047
- }
4048
4009
  function _unknown(Class) {
4049
4010
  return new Class({
4050
4011
  type: "unknown"
@@ -4056,12 +4017,6 @@ function _never(Class, params) {
4056
4017
  ...normalizeParams(params)
4057
4018
  });
4058
4019
  }
4059
- function _date(Class, params) {
4060
- return new Class({
4061
- type: "date",
4062
- ...normalizeParams(params)
4063
- });
4064
- }
4065
4020
  function _lt(value, params) {
4066
4021
  return new $ZodCheckLessThan({
4067
4022
  check: "less_than",
@@ -4662,20 +4617,10 @@ var numberProcessor = (schema, ctx, _json, _params) => {
4662
4617
  var booleanProcessor = (_schema, _ctx, json, _params) => {
4663
4618
  json.type = "boolean";
4664
4619
  };
4665
- var bigintProcessor = (_schema, ctx, _json, _params) => {
4666
- if (ctx.unrepresentable === "throw") {
4667
- throw new Error("BigInt cannot be represented in JSON Schema");
4668
- }
4669
- };
4670
4620
  var neverProcessor = (_schema, _ctx, json, _params) => {
4671
4621
  json.not = {};
4672
4622
  };
4673
4623
  var unknownProcessor = (_schema, _ctx, _json, _params) => {};
4674
- var dateProcessor = (_schema, ctx, _json, _params) => {
4675
- if (ctx.unrepresentable === "throw") {
4676
- throw new Error("Date cannot be represented in JSON Schema");
4677
- }
4678
- };
4679
4624
  var enumProcessor = (schema, _ctx, json, _params) => {
4680
4625
  const def = schema._zod.def;
4681
4626
  const values = getEnumValues(def.entries);
@@ -5406,31 +5351,6 @@ var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
5406
5351
  function boolean2(params) {
5407
5352
  return _boolean(ZodBoolean, params);
5408
5353
  }
5409
- var ZodBigInt = /* @__PURE__ */ $constructor("ZodBigInt", (inst, def) => {
5410
- $ZodBigInt.init(inst, def);
5411
- ZodType.init(inst, def);
5412
- inst._zod.processJSONSchema = (ctx, json, params) => bigintProcessor(inst, ctx, json, params);
5413
- inst.gte = (value, params) => inst.check(_gte(value, params));
5414
- inst.min = (value, params) => inst.check(_gte(value, params));
5415
- inst.gt = (value, params) => inst.check(_gt(value, params));
5416
- inst.gte = (value, params) => inst.check(_gte(value, params));
5417
- inst.min = (value, params) => inst.check(_gte(value, params));
5418
- inst.lt = (value, params) => inst.check(_lt(value, params));
5419
- inst.lte = (value, params) => inst.check(_lte(value, params));
5420
- inst.max = (value, params) => inst.check(_lte(value, params));
5421
- inst.positive = (params) => inst.check(_gt(BigInt(0), params));
5422
- inst.negative = (params) => inst.check(_lt(BigInt(0), params));
5423
- inst.nonpositive = (params) => inst.check(_lte(BigInt(0), params));
5424
- inst.nonnegative = (params) => inst.check(_gte(BigInt(0), params));
5425
- inst.multipleOf = (value, params) => inst.check(_multipleOf(value, params));
5426
- const bag = inst._zod.bag;
5427
- inst.minValue = bag.minimum ?? null;
5428
- inst.maxValue = bag.maximum ?? null;
5429
- inst.format = bag.format ?? null;
5430
- });
5431
- function bigint2(params) {
5432
- return _bigint(ZodBigInt, params);
5433
- }
5434
5354
  var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
5435
5355
  $ZodUnknown.init(inst, def);
5436
5356
  ZodType.init(inst, def);
@@ -5447,19 +5367,6 @@ var ZodNever = /* @__PURE__ */ $constructor("ZodNever", (inst, def) => {
5447
5367
  function never(params) {
5448
5368
  return _never(ZodNever, params);
5449
5369
  }
5450
- var ZodDate = /* @__PURE__ */ $constructor("ZodDate", (inst, def) => {
5451
- $ZodDate.init(inst, def);
5452
- ZodType.init(inst, def);
5453
- inst._zod.processJSONSchema = (ctx, json, params) => dateProcessor(inst, ctx, json, params);
5454
- inst.min = (value, params) => inst.check(_gte(value, params));
5455
- inst.max = (value, params) => inst.check(_lte(value, params));
5456
- const c = inst._zod.bag;
5457
- inst.minDate = c.minimum ? new Date(c.minimum) : null;
5458
- inst.maxDate = c.maximum ? new Date(c.maximum) : null;
5459
- });
5460
- function date3(params) {
5461
- return _date(ZodDate, params);
5462
- }
5463
5370
  var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
5464
5371
  $ZodArray.init(inst, def);
5465
5372
  ZodType.init(inst, def);
@@ -5885,8 +5792,10 @@ var GithubDeviceResponseSchema = object({
5885
5792
  expires_in: number2().optional(),
5886
5793
  verification_uri_complete: string2().optional()
5887
5794
  });
5795
+ var DeviceReportShape = { timezone: unknown().optional(), locale: unknown().optional() };
5888
5796
  var AuthTokenRequestSchema = object({
5889
- session_id: string2().uuid()
5797
+ session_id: string2().uuid(),
5798
+ ...DeviceReportShape
5890
5799
  });
5891
5800
  var AuthTokenPendingSchema = object({
5892
5801
  status: literal("pending"),
@@ -5936,7 +5845,8 @@ var GuestStartRequestSchema = object({
5936
5845
  consent_version: string2().min(1).max(32),
5937
5846
  signup_src: string2().optional(),
5938
5847
  referral_code: string2().optional(),
5939
- client: object({ os: string2().max(64).optional(), cli_version: string2().max(64).optional() }).optional()
5848
+ client: object({ os: string2().max(64).optional(), cli_version: string2().max(64).optional() }).optional(),
5849
+ ...DeviceReportShape
5940
5850
  });
5941
5851
  var ExperimentVariantSchema = _enum(["control", "treatment"]);
5942
5852
  var ExperimentAssignmentSchema = object({
@@ -6003,6 +5913,7 @@ var EarnRewardSurfaceSchema = object({
6003
5913
  remaining_micros: string2(),
6004
5914
  tokens_about: number2().optional()
6005
5915
  });
5916
+ var SlowTierSurfaceSchema = object({ until: string2(), line: string2() });
6006
5917
  var MeResponseSchema = object({
6007
5918
  user_id: string2(),
6008
5919
  handle: string2(),
@@ -6036,8 +5947,10 @@ var MeResponseSchema = object({
6036
5947
  return_bonus: ReturnBonusSurfaceSchema.optional(),
6037
5948
  streak: StreakSurfaceSchema.optional(),
6038
5949
  visible_allowance: boolean2().optional(),
5950
+ choice_prompts_arm: _enum(["control", "treatment"]).optional(),
6039
5951
  earn_reward: EarnRewardSurfaceSchema.nullable().optional(),
6040
- active_perks: array(object({ kind: string2(), models: array(string2()), ends_at: string2() })).optional()
5952
+ active_perks: array(object({ kind: string2(), models: array(string2()), ends_at: string2() })).optional(),
5953
+ slow_tier: SlowTierSurfaceSchema.optional()
6041
5954
  });
6042
5955
  var MeStatsResponseSchema = object({
6043
5956
  user_id: string2(),
@@ -6063,6 +5976,7 @@ var MeStatsResponseSchema = object({
6063
5976
  credit_tiers: boolean2().optional(),
6064
5977
  premium_get_line: string2().optional(),
6065
5978
  use_bonus: string2().optional(),
5979
+ slow_tier: SlowTierSurfaceSchema.optional(),
6066
5980
  request_count_today: number2(),
6067
5981
  prompt_tokens_today: number2(),
6068
5982
  completion_tokens_today: number2(),
@@ -6164,501 +6078,12 @@ var ReferralStatusResponseSchema = object({
6164
6078
  var ReferralLeaderboardResponseSchema = object({
6165
6079
  entries: array(object({ rank: number2(), qualified_count: number2(), handle: string2().nullable() }))
6166
6080
  });
6081
+
6167
6082
  // ../shared/src/errors.ts
6168
6083
  function assertExhaustiveErrorCode(code) {
6169
6084
  throw new Error(`unhandled error code: ${String(code)}`);
6170
6085
  }
6171
- // ../shared/src/lane.ts
6172
- function usdToMicros(value) {
6173
- const s = typeof value === "number" ? value.toFixed(6) : String(value).trim();
6174
- const negative = s.startsWith("-");
6175
- const [whole = "0", frac = ""] = (negative ? s.slice(1) : s).split(".");
6176
- const micros = BigInt(whole || "0") * 1000000n + BigInt(`${frac}000000`.slice(0, 6));
6177
- return negative ? -micros : micros;
6178
- }
6179
-
6180
- // ../shared/src/config.ts
6181
- var ConfigSchema = object({
6182
- perUserDailyCapUsd: number2().default(1.125),
6183
- perUserRollingCapUsd: number2().default(0),
6184
- rollingCapWindowHours: number2().int().default(5),
6185
- globalDailyCapUsd: number2().default(50),
6186
- contextCharBudget: number2().int().default(1200000),
6187
- maxOutputTokens: number2().int().min(1).default(32768),
6188
- accountsPerDayCeiling: number2().int().default(500),
6189
- authRatePerIpPerHour: number2().int().default(30),
6190
- siteLoginStartRateLimitEnabled: boolean2().default(false),
6191
- siteLoginStartPerIp: number2().int().min(0).default(5),
6192
- siteLoginStartPerSubject: number2().int().min(0).default(5),
6193
- siteLoginStartWindowSeconds: number2().int().min(1).default(600),
6194
- siteLoginStartCooldownSeconds: number2().int().min(0).default(60),
6195
- webBuyUrlRatePerProviderPerHour: number2().int().default(20),
6196
- handoffTokenRatePerProviderPerHour: number2().int().default(60),
6197
- handoffHourlyCompletionCap: number2().int().default(120),
6198
- tokenRatePerIpPerHour: number2().int().default(3600),
6199
- consentRatePerIpPerHour: number2().int().default(60),
6200
- adInlineTurnFrequency: number2().int().default(5),
6201
- adMinColumns: number2().int().default(60),
6202
- proxyBaseUrl: string2().default("https://api.freepi.ai"),
6203
- modelId: string2().default("deepseek/deepseek-v4-flash"),
6204
- modelDisplay: string2().optional(),
6205
- modelNotice: string2().optional(),
6206
- providerDataCollection: _enum(["allow", "deny"]).optional(),
6207
- promptCacheEnabled: boolean2().default(false),
6208
- emailCaptureEnabled: boolean2().default(false),
6209
- lifecycleEmailExperimentEnabled: boolean2().default(false),
6210
- lifecycleEmailEnabled: boolean2().default(false),
6211
- lifecycleEmailSend: boolean2().default(false),
6212
- emailProviderConfigured: boolean2().default(false),
6213
- emailFrom: string2().optional(),
6214
- emailReplyTo: string2().optional(),
6215
- emailPostalAddress: string2().optional(),
6216
- emailProviderUrl: string2().optional(),
6217
- hiveEnabled: boolean2().default(false),
6218
- earnEnabled: boolean2().default(false),
6219
- earnShare: number2().min(0).max(1).default(0.1),
6220
- earnBudgetMicros: bigint2().default(300000000n),
6221
- earnTestGithubIds: array(string2().regex(/^\d{1,20}$/)).default([]),
6222
- providerOrder: array(string2()).optional(),
6223
- openrouterFlashProviderOrder: array(string2()).default([
6224
- "streamlake/fp8",
6225
- "deepinfra/fp8",
6226
- "gmicloud/fp8",
6227
- "digitalocean"
6228
- ]),
6229
- openrouterFlashProviderIgnore: array(string2()).default(["baidu", "relace"]),
6230
- openrouterFlashMaxPricePrompt: string2().default("0.12"),
6231
- openrouterFlashMaxPriceCompletion: string2().default("0.25"),
6232
- openrouterSessionPinTtlSeconds: number2().int().min(1).default(1800),
6233
- costAlertMultiplier: number2().int().min(1).default(2),
6234
- costAlertMinHours: number2().int().min(1).default(7),
6235
- tierNewMaxDays: number2().int().default(30),
6236
- tierYoungMaxDays: number2().int().default(365),
6237
- tierCapNewUsd: number2().default(0.45),
6238
- tierCapYoungUsd: number2().default(1.125),
6239
- fallbackLaneUsd: number2().default(0.225),
6240
- fallbackHourlyCap: number2().int().default(10),
6241
- fallbackLaneMinHeadroomBps: number2().int().default(1000),
6242
- queueWaitRetryAfterSeconds: number2().int().default(0),
6243
- sessionLeaseTtlMinutes: number2().int().default(30),
6244
- signedInMaxCliSessions: number2().int().min(1).default(4),
6245
- shutdownDrainMs: number2().int().min(0).max(25000).default(20000),
6246
- premiumRatioThreshold: number2().int().min(1).default(5),
6247
- fallbackExpensiveRatio: number2().int().min(1).default(5),
6248
- larprouterTrialModels: array(string2()).default([]),
6249
- larprouterTrialSharePct: number2().int().min(0).max(100).default(0),
6250
- larprouterBaseUrl: string2().default("https://larprouter.com/v1"),
6251
- larprouterFallbackModels: array(string2()).default([]),
6252
- larprouterFallbackTerminal: boolean2().default(false),
6253
- larprouterTtftMs: number2().int().min(1).default(8000),
6254
- larprouterPrices: record(string2(), object({ input: string2(), cacheRead: string2(), output: string2() })).default({ "deepseek-v4.1-flash-1017-non-abliterated": { input: "80000", cacheRead: "10000", output: "350000" } }),
6255
- larprouterModelMap: record(string2(), string2()).default({
6256
- "deepseek-v4-flash": "deepseek-v4.1-flash-1017-non-abliterated",
6257
- "deepseek-v4.1-flash": "deepseek-v4.1-flash-1017-non-abliterated"
6258
- }),
6259
- experientialBaseUrl: string2().default("https://api.experientiallabs.ai/v1"),
6260
- experientialModelMap: record(string2(), string2()).default({
6261
- "deepseek-v4.1-flash": "deepseek-v4.1-flash-1017",
6262
- "deepseek-v4-flash": "deepseek-v4.1-flash-1017",
6263
- "gpt-6-luna": "gpt-6-luna"
6264
- }),
6265
- experientialPrimaryModels: array(string2()).default([]),
6266
- experientialFallbackModels: array(string2()).default([]),
6267
- experientialTtftMs: number2().int().min(1).default(1e4),
6268
- experientialMaxCostUsdPerRequest: string2().default("0.0005"),
6269
- experientialPriceCooldownMs: number2().int().min(1).default(3600000),
6270
- particleEnabled: boolean2().default(false),
6271
- particleBaseUrl: string2().default("https://api.particle.ai/v1"),
6272
- particleModelMap: record(string2(), string2()).default({
6273
- particle: "particle-1",
6274
- "deepseek-v4.1-flash": "deepseek-ai/deepseek-v41-flash-1017",
6275
- "glm-5.3-flash": "z-ai/glm-5.3-flash"
6276
- }),
6277
- particleTtftMs: number2().int().min(1).default(1e4),
6278
- minCliVersion: string2().default("0.0.0"),
6279
- latestCliVersion: string2().default("0.2.0"),
6280
- minDesktopVersion: string2().default("0.0.0"),
6281
- latestDesktopVersion: string2().default("0.0.0"),
6282
- desktopDownloadUrl: string2().default(""),
6283
- minWebVersion: string2().default("0.0.0"),
6284
- latestWebVersion: string2().default("0.0.0"),
6285
- workspaceMaxStreams: number2().int().min(1).default(1),
6286
- gravityDesktopEnabled: boolean2().default(false),
6287
- abuseWindowMinutes: number2().int().default(30),
6288
- abuseSettleGraceSeconds: number2().int().default(120),
6289
- abuseGraceSettledStarts: number2().int().default(2),
6290
- abuseUnverifiedMinStarts: number2().int().default(3),
6291
- abuseUnverifiedMinFinishes: number2().int().default(2),
6292
- abuseUnverifiedRestrictHours: number2().int().default(6),
6293
- abuseLikelyBypassMinStarts: number2().int().default(5),
6294
- abuseLikelyBypassRestrictHours: number2().int().default(24),
6295
- abuseRepeatWindowDays: number2().int().default(7),
6296
- abuseRepeatMinOccurrences: number2().int().default(3),
6297
- abuseRestrictedCapUsd: number2().default(0.5),
6298
- abuseRestrictedHourlyCap: number2().int().default(5),
6299
- abuseNormalHourlyCap: number2().int().default(30),
6300
- abuseSignedInNormalHourlyCap: number2().int().default(0),
6301
- abuseDay0SkipHourly: boolean2().default(true),
6302
- abuseFlipAlertThreshold: number2().int().default(10),
6303
- abuseAdBypassMinRequests: number2().int().default(20),
6304
- abuseAdBypassRatio: number2().int().default(10),
6305
- traceMaxInflightWrites: number2().int().default(32),
6306
- catalogCacheTtlSeconds: number2().int().default(60),
6307
- eventMaxInflightWrites: number2().int().default(128),
6308
- guestEnabled: boolean2().default(false),
6309
- guestAllowanceMicros: bigint2().default(30000n),
6310
- guestGlobalDailyCapMicros: bigint2().default(3000000n),
6311
- guestHoldMicros: bigint2().default(5000n),
6312
- guestIpSessionsPerDay: number2().int().default(8),
6313
- guestIpAllowanceMicrosPerDay: bigint2().default(240000n),
6314
- guestMaxConcurrentStreams: number2().int().default(10),
6315
- guestMinRequestHeadroomMicros: bigint2().default(5000n),
6316
- guestIpHashRetentionDays: number2().int().default(7),
6317
- returnBonusDay2Credits: number2().int().default(2000),
6318
- returnBonusDay7Credits: number2().int().default(1000),
6319
- guestGithubBonusCredits: number2().int().default(1e4),
6320
- returnNudgeEmailEnabled: boolean2().default(false),
6321
- referralBonusCreditsPerDay: number2().int().default(5000),
6322
- referralBonusDays: number2().int().default(30),
6323
- referralMaxActivePerReferrer: number2().int().default(10),
6324
- referralGithubMinAgeDays: number2().int().default(30),
6325
- referralCreditGrantCredits: number2().int().default(0),
6326
- webClaimCredits: number2().int().default(0),
6327
- bountyCredits: number2().int().positive().default(5000),
6328
- bountyMonthlyCapUsd: string2().regex(/^\d+(\.\d{1,6})?$/).default("50"),
6329
- geoipDbPath: string2().optional(),
6330
- sanctionedCountries: array(string2()).default(["IR", "CU", "KP", "SY"]),
6331
- sanctionedTimezones: array(string2()).default(["Asia/Tehran", "Asia/Pyongyang", "America/Havana", "Asia/Damascus", "Europe/Simferopol"]),
6332
- regionLimitsStart: date3().optional(),
6333
- fullAllowanceCountries: array(string2()).default(["US", "CA", "GB", "AU", "NZ", "IE"]),
6334
- priorityCountries: array(string2()).default(["US", "CA", "AU", "NZ"]),
6335
- freeSpeed: _enum(["off", "log", "on"]).default("log"),
6336
- freeSpeedDelaysS: array(number2().int().min(0).max(120)).length(4).default([0, 10, 5, 20]),
6337
- winbackEmail: _enum(["off", "dry_run", "live"]).default("off"),
6338
- winbackMinDays: number2().int().default(3),
6339
- winbackMaxDays: number2().int().default(14),
6340
- winbackMinRequests: number2().int().default(5),
6341
- winbackDailyMax: number2().int().default(100),
6342
- winbackClaimHours: number2().int().default(72),
6343
- winbackContentImportBullet: boolean2().default(false),
6344
- winbackOfferPriority: string2().default("credits:10000"),
6345
- winbackOfferOther: string2().default("credits:3000"),
6346
- winbackArmsPriority: array(string2()).default([]),
6347
- winbackArmsOther: array(string2()).default([]),
6348
- regionLowCapMicros: bigint2().default(100000n),
6349
- streaksEnabled: _enum(["off", "log", "on"]).default("log"),
6350
- streakMinRequests: number2().int().min(1).default(10),
6351
- streakRewards: array(object({ day: number2().int().min(1), credits: number2().int().min(1) })).default([
6352
- { day: 3, credits: 2000 },
6353
- { day: 7, credits: 5000 },
6354
- { day: 14, credits: 1e4 },
6355
- { day: 30, credits: 20000 }
6356
- ])
6357
- });
6358
- function envNumber(value) {
6359
- if (value === undefined || value === "")
6360
- return;
6361
- const n = Number(value);
6362
- return Number.isNaN(n) ? undefined : n;
6363
- }
6364
- function envEmailAddress(name, value) {
6365
- const v = value?.trim();
6366
- if (!v)
6367
- return;
6368
- if (!/^([^<>"\\,;\r\n]+ <[^\s<>@,;]+@[^\s<>@,;]+\.[^\s<>@,;]+>|[^\s<>@,;]+@[^\s<>@,;]+\.[^\s<>@,;]+)$/.test(v)) {
6369
- console.warn(`${name} ignored: must be an address like "help@freepi.ai" or "Name <help@freepi.ai>"`);
6370
- return;
6371
- }
6372
- return v;
6373
- }
6374
- function envMicros(name, value) {
6375
- if (value === undefined || value === "")
6376
- return;
6377
- const v = value.trim();
6378
- if (!/^\d+(\.\d{1,6})?$/.test(v)) {
6379
- throw new Error(`${name} must be a non-negative decimal USD amount with at most 6 decimals, got "${value}"`);
6380
- }
6381
- return usdToMicros(v);
6382
- }
6383
- function envStrictInt(name, value) {
6384
- if (value === undefined || value === "")
6385
- return;
6386
- const v = value.trim();
6387
- if (!/^\d+$/.test(v))
6388
- throw new Error(`${name} must be a non-negative integer, got "${value}"`);
6389
- return Number(v);
6390
- }
6391
- function envPairs(value) {
6392
- const out = {};
6393
- for (const part of (value ?? "").split(",")) {
6394
- const bits = part.split("=").map((s) => s.trim());
6395
- if (bits.length === 2 && bits[0] && bits[1])
6396
- out[bits[0]] = bits[1];
6397
- }
6398
- return Object.keys(out).length > 0 ? out : undefined;
6399
- }
6400
- function usdToMicrosString(v) {
6401
- const m = /^(\d{1,6})(?:\.(\d{1,6}))?$/.exec(v.trim());
6402
- return m ? (BigInt(m[1]) * 1000000n + BigInt((m[2] ?? "").padEnd(6, "0"))).toString() : null;
6403
- }
6404
- function envPrices(value) {
6405
- const out = {};
6406
- for (const part of (value ?? "").split(",")) {
6407
- const bits = part.split("=").map((s) => s.trim());
6408
- const nums = bits.length === 2 ? bits[1].split("/").map(usdToMicrosString) : [];
6409
- if (bits[0] && nums.length === 3 && nums.every((n) => n !== null))
6410
- out[bits[0]] = { input: nums[0], cacheRead: nums[1], output: nums[2] };
6411
- }
6412
- return Object.keys(out).length > 0 ? out : undefined;
6413
- }
6414
- function envPercent(value) {
6415
- const n = envNumber(value);
6416
- return n === undefined ? undefined : Math.min(100, Math.max(0, Math.trunc(n)));
6417
- }
6418
- function envBoolean(value) {
6419
- if (value === undefined || value === "")
6420
- return;
6421
- return value === "true" || value === "1";
6422
- }
6423
- function envFlagOn(value) {
6424
- return ["true", "1", "on", "yes"].includes((value ?? "").trim().toLowerCase());
6425
- }
6426
- function envList(value) {
6427
- if (!value)
6428
- return;
6429
- const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6430
- return items.length > 0 ? items : undefined;
6431
- }
6432
- function envCountries(value) {
6433
- return envList(value)?.map((s) => s.toUpperCase());
6434
- }
6435
- function envTimezones(value) {
6436
- if (value === undefined)
6437
- return;
6438
- return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6439
- }
6440
- function envStreakRewards(value) {
6441
- if (!value?.trim())
6442
- return;
6443
- const seen = new Set;
6444
- return value.split(",").map((part) => {
6445
- const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(part);
6446
- const day = m ? Number(m[1]) : 0;
6447
- const credits = m ? Number(m[2]) : 0;
6448
- if (!m || day < 1 || credits < 1 || seen.has(day)) {
6449
- throw new Error(`STREAK_REWARDS must be "day:credits" pairs with distinct days, got "${value}"`);
6450
- }
6451
- seen.add(day);
6452
- return { day, credits };
6453
- });
6454
- }
6455
- function envStreaksMode(value) {
6456
- if (value === undefined || value.trim() === "")
6457
- return;
6458
- const v = value.trim().toLowerCase();
6459
- if (v !== "off" && v !== "log" && v !== "on")
6460
- throw new Error(`STREAKS_ENABLED must be off, log or on, got "${value}"`);
6461
- return v;
6462
- }
6463
- function envFreeSpeedMode(value) {
6464
- if (value === undefined || value.trim() === "")
6465
- return;
6466
- const v = value.trim().toLowerCase();
6467
- if (v !== "off" && v !== "log" && v !== "on")
6468
- throw new Error(`FREE_SPEED must be off, log or on, got "${value}"`);
6469
- return v;
6470
- }
6471
- function envFreeSpeedDelays(value) {
6472
- if (!value?.trim())
6473
- return;
6474
- const parts = value.split(",").map((s) => s.trim());
6475
- const nums = parts.map((s) => /^\d+$/.test(s) ? Number(s) : NaN);
6476
- if (nums.length !== 4 || nums.some((n) => !Number.isInteger(n) || n > 120)) {
6477
- throw new Error(`FREE_SPEED_DELAYS must be four whole seconds 0-120 (priority+clicking, priority+not_clicking, other+clicking, other+not_clicking), got "${value}"`);
6478
- }
6479
- return nums;
6480
- }
6481
- function envDate(value) {
6482
- if (!value?.trim())
6483
- return;
6484
- const d = new Date(value.trim());
6485
- return Number.isNaN(d.getTime()) ? undefined : d;
6486
- }
6487
- function envDecimalString(value) {
6488
- if (value === undefined || value.trim() === "")
6489
- return;
6490
- const t = value.trim();
6491
- return /^\d+(\.\d+)?$/.test(t) ? t : undefined;
6492
- }
6493
- function envBountyCap(value) {
6494
- const t = value?.trim();
6495
- if (!t)
6496
- return;
6497
- if (!/^\d+(\.\d{1,6})?$/.test(t))
6498
- throw new Error(`BOUNTY_MONTHLY_CAP_USD must be a decimal like "50" or "0.50", got "${value}"`);
6499
- return t;
6500
- }
6501
- function loadConfig(env = process.env) {
6502
- return ConfigSchema.parse({
6503
- perUserDailyCapUsd: envNumber(env.PER_USER_DAILY_CAP_USD),
6504
- perUserRollingCapUsd: envNumber(env.PER_USER_ROLLING_CAP_USD),
6505
- rollingCapWindowHours: envNumber(env.ROLLING_CAP_WINDOW_HOURS),
6506
- globalDailyCapUsd: envNumber(env.GLOBAL_DAILY_CAP_USD),
6507
- contextCharBudget: envNumber(env.CONTEXT_CHAR_BUDGET),
6508
- maxOutputTokens: envNumber(env.MAX_OUTPUT_TOKENS),
6509
- accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
6510
- authRatePerIpPerHour: envNumber(env.AUTH_RATE_PER_IP_PER_HOUR),
6511
- siteLoginStartRateLimitEnabled: envFlagOn(env.SITE_LOGIN_START_RATE_LIMIT),
6512
- siteLoginStartPerIp: envNumber(env.SITE_LOGIN_START_PER_IP),
6513
- siteLoginStartPerSubject: envNumber(env.SITE_LOGIN_START_PER_SUBJECT),
6514
- siteLoginStartWindowSeconds: envNumber(env.SITE_LOGIN_START_WINDOW_SECONDS),
6515
- siteLoginStartCooldownSeconds: envNumber(env.SITE_LOGIN_START_COOLDOWN_SECONDS),
6516
- webBuyUrlRatePerProviderPerHour: envNumber(env.WEB_BUY_URL_RATE_PER_PROVIDER_PER_HOUR),
6517
- handoffTokenRatePerProviderPerHour: envNumber(env.HANDOFF_TOKEN_RATE_PER_PROVIDER_PER_HOUR),
6518
- handoffHourlyCompletionCap: envNumber(env.HANDOFF_HOURLY_COMPLETION_CAP),
6519
- tokenRatePerIpPerHour: envNumber(env.TOKEN_RATE_PER_IP_PER_HOUR),
6520
- consentRatePerIpPerHour: envNumber(env.CONSENT_RATE_PER_IP_PER_HOUR),
6521
- adInlineTurnFrequency: envNumber(env.AD_INLINE_TURN_FREQUENCY),
6522
- adMinColumns: envNumber(env.AD_MIN_COLUMNS),
6523
- proxyBaseUrl: env.PROXY_BASE_URL || undefined,
6524
- modelId: env.MODEL_ID || undefined,
6525
- modelDisplay: env.MODEL_DISPLAY || undefined,
6526
- modelNotice: env.MODEL_NOTICE || undefined,
6527
- providerDataCollection: env.PROVIDER_DATA_COLLECTION || undefined,
6528
- promptCacheEnabled: envBoolean(env.PROMPT_CACHE_ENABLED),
6529
- emailCaptureEnabled: envBoolean(env.EMAIL_CAPTURE_ENABLED),
6530
- lifecycleEmailExperimentEnabled: envBoolean(env.LIFECYCLE_EMAIL_EXPERIMENT),
6531
- lifecycleEmailEnabled: envBoolean(env.LIFECYCLE_EMAIL_ENABLED),
6532
- lifecycleEmailSend: envBoolean(env.LIFECYCLE_EMAIL_SEND),
6533
- emailProviderConfigured: env.EMAIL_PROVIDER_KEY ? env.EMAIL_PROVIDER_KEY.trim().length > 0 : undefined,
6534
- emailFrom: env.EMAIL_FROM?.trim() || undefined,
6535
- emailReplyTo: envEmailAddress("EMAIL_REPLY_TO", env.EMAIL_REPLY_TO),
6536
- emailPostalAddress: env.EMAIL_POSTAL_ADDRESS?.trim() || undefined,
6537
- emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
6538
- hiveEnabled: envBoolean(env.HIVE_ENABLED),
6539
- earnEnabled: envBoolean(env.EARN_ENABLED),
6540
- earnShare: (() => {
6541
- const n = envNumber(env.EARN_SHARE);
6542
- return n === undefined ? undefined : Math.min(1, Math.max(0, n));
6543
- })(),
6544
- earnBudgetMicros: envStrictInt("EARN_BUDGET_MICROS", env.EARN_BUDGET_MICROS) === undefined ? undefined : BigInt(env.EARN_BUDGET_MICROS.trim()),
6545
- earnTestGithubIds: envList(env.EARN_TEST_GITHUB_IDS)?.filter((id) => /^\d{1,20}$/.test(id)),
6546
- providerOrder: envList(env.PROVIDER_ORDER),
6547
- openrouterFlashProviderOrder: envList(env.OPENROUTER_FLASH_PROVIDER_ORDER),
6548
- openrouterFlashProviderIgnore: envList(env.OPENROUTER_FLASH_PROVIDER_IGNORE),
6549
- openrouterFlashMaxPricePrompt: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_PROMPT),
6550
- openrouterFlashMaxPriceCompletion: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_COMPLETION),
6551
- openrouterSessionPinTtlSeconds: envNumber(env.OPENROUTER_SESSION_PIN_TTL_SECONDS),
6552
- costAlertMultiplier: envNumber(env.COST_ALERT_MULTIPLIER),
6553
- costAlertMinHours: envNumber(env.COST_ALERT_MIN_HOURS),
6554
- tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
6555
- tierYoungMaxDays: envNumber(env.TIER_YOUNG_MAX_DAYS),
6556
- tierCapNewUsd: envNumber(env.TIER_CAP_NEW_USD),
6557
- tierCapYoungUsd: envNumber(env.TIER_CAP_YOUNG_USD),
6558
- fallbackLaneUsd: envNumber(env.FALLBACK_LANE_USD),
6559
- fallbackHourlyCap: envNumber(env.FALLBACK_HOURLY_CAP),
6560
- fallbackLaneMinHeadroomBps: envNumber(env.FALLBACK_LANE_MIN_HEADROOM_BPS),
6561
- queueWaitRetryAfterSeconds: envNumber(env.QUEUE_WAIT_RETRY_AFTER_SECONDS),
6562
- sessionLeaseTtlMinutes: envNumber(env.SESSION_LEASE_TTL_MINUTES),
6563
- signedInMaxCliSessions: envNumber(env.SIGNED_IN_MAX_CLI_SESSIONS),
6564
- shutdownDrainMs: envNumber(env.SHUTDOWN_DRAIN_MS),
6565
- premiumRatioThreshold: envNumber(env.PREMIUM_RATIO_THRESHOLD),
6566
- fallbackExpensiveRatio: envNumber(env.FALLBACK_EXPENSIVE_RATIO),
6567
- larprouterTrialModels: envList(env.LARPROUTER_TRIAL_MODELS),
6568
- larprouterTrialSharePct: envPercent(env.LARPROUTER_TRIAL_SHARE_PCT),
6569
- larprouterFallbackModels: envList(env.LARPROUTER_FALLBACK_MODELS),
6570
- larprouterTtftMs: envNumber(env.LARPROUTER_TTFT_MS),
6571
- larprouterFallbackTerminal: envBoolean(env.LARPROUTER_FALLBACK_TERMINAL),
6572
- larprouterModelMap: envPairs(env.LARPROUTER_MODEL_MAP),
6573
- larprouterPrices: envPrices(env.LARPROUTER_PRICES),
6574
- larprouterBaseUrl: env.LARPROUTER_BASE_URL?.trim().replace(/\/$/, "") || undefined,
6575
- experientialBaseUrl: env.EXPERIENTIAL_BASE_URL?.trim().replace(/\/$/, "") || undefined,
6576
- experientialModelMap: envPairs(env.EXPERIENTIAL_MODEL_MAP),
6577
- experientialPrimaryModels: envList(env.EXPERIENTIAL_PRIMARY_MODELS),
6578
- experientialFallbackModels: envList(env.EXPERIENTIAL_FALLBACK_MODELS),
6579
- experientialTtftMs: envNumber(env.EXPERIENTIAL_TTFT_MS),
6580
- experientialMaxCostUsdPerRequest: /^\d{1,6}(\.\d{1,6})?$/.test(env.EXPERIENTIAL_MAX_COST_USD_PER_REQUEST?.trim() ?? "") ? env.EXPERIENTIAL_MAX_COST_USD_PER_REQUEST.trim() : undefined,
6581
- experientialPriceCooldownMs: envNumber(env.EXPERIENTIAL_PRICE_COOLDOWN_MS),
6582
- particleEnabled: envBoolean(env.PARTICLE_ENABLED),
6583
- particleBaseUrl: env.PARTICLE_BASE_URL?.trim().replace(/\/$/, "") || undefined,
6584
- particleModelMap: envPairs(env.PARTICLE_MODEL_MAP),
6585
- particleTtftMs: envNumber(env.PARTICLE_TTFT_MS),
6586
- minCliVersion: env.MIN_CLI_VERSION || undefined,
6587
- latestCliVersion: env.LATEST_CLI_VERSION || undefined,
6588
- minDesktopVersion: env.MIN_DESKTOP_VERSION || undefined,
6589
- latestDesktopVersion: env.LATEST_DESKTOP_VERSION || undefined,
6590
- desktopDownloadUrl: env.DESKTOP_DOWNLOAD_URL || undefined,
6591
- minWebVersion: env.MIN_WEB_VERSION || undefined,
6592
- latestWebVersion: env.LATEST_WEB_VERSION || undefined,
6593
- workspaceMaxStreams: envNumber(env.WORKSPACE_MAX_STREAMS),
6594
- gravityDesktopEnabled: env.GRAVITY_DESKTOP_ENABLED === "true" ? true : undefined,
6595
- abuseWindowMinutes: envNumber(env.ABUSE_WINDOW_MINUTES),
6596
- abuseSettleGraceSeconds: envNumber(env.ABUSE_SETTLE_GRACE_SECONDS),
6597
- abuseGraceSettledStarts: envNumber(env.ABUSE_GRACE_SETTLED_STARTS),
6598
- abuseUnverifiedMinStarts: envNumber(env.ABUSE_UNVERIFIED_MIN_STARTS),
6599
- abuseUnverifiedMinFinishes: envNumber(env.ABUSE_UNVERIFIED_MIN_FINISHES),
6600
- abuseUnverifiedRestrictHours: envNumber(env.ABUSE_UNVERIFIED_RESTRICT_HOURS),
6601
- abuseLikelyBypassMinStarts: envNumber(env.ABUSE_LIKELY_BYPASS_MIN_STARTS),
6602
- abuseLikelyBypassRestrictHours: envNumber(env.ABUSE_LIKELY_BYPASS_RESTRICT_HOURS),
6603
- abuseRepeatWindowDays: envNumber(env.ABUSE_REPEAT_WINDOW_DAYS),
6604
- abuseRepeatMinOccurrences: envNumber(env.ABUSE_REPEAT_MIN_OCCURRENCES),
6605
- abuseRestrictedCapUsd: envNumber(env.ABUSE_RESTRICTED_CAP_USD),
6606
- abuseRestrictedHourlyCap: envNumber(env.ABUSE_RESTRICTED_HOURLY_CAP),
6607
- abuseNormalHourlyCap: envNumber(env.ABUSE_NORMAL_HOURLY_CAP),
6608
- abuseSignedInNormalHourlyCap: envNumber(env.ABUSE_SIGNED_IN_NORMAL_HOURLY_CAP),
6609
- abuseDay0SkipHourly: envBoolean(env.ABUSE_DAY0_SKIP_HOURLY),
6610
- abuseFlipAlertThreshold: envNumber(env.ABUSE_FLIP_ALERT_THRESHOLD),
6611
- abuseAdBypassMinRequests: envNumber(env.ABUSE_AD_BYPASS_MIN_REQUESTS),
6612
- abuseAdBypassRatio: envNumber(env.ABUSE_AD_BYPASS_RATIO),
6613
- traceMaxInflightWrites: envNumber(env.TRACE_MAX_INFLIGHT_WRITES),
6614
- catalogCacheTtlSeconds: envNumber(env.CATALOG_CACHE_TTL_SECONDS),
6615
- eventMaxInflightWrites: envNumber(env.EVENT_MAX_INFLIGHT_WRITES),
6616
- guestEnabled: envBoolean(env.GUEST_ENABLED),
6617
- guestAllowanceMicros: envMicros("GUEST_ALLOWANCE_USD", env.GUEST_ALLOWANCE_USD),
6618
- guestGlobalDailyCapMicros: envMicros("GUEST_GLOBAL_DAILY_CAP_USD", env.GUEST_GLOBAL_DAILY_CAP_USD),
6619
- guestHoldMicros: envMicros("GUEST_HOLD_USD", env.GUEST_HOLD_USD),
6620
- guestIpSessionsPerDay: envStrictInt("GUEST_IP_SESSIONS_PER_DAY", env.GUEST_IP_SESSIONS_PER_DAY),
6621
- guestIpAllowanceMicrosPerDay: envMicros("GUEST_IP_ALLOWANCE_USD_PER_DAY", env.GUEST_IP_ALLOWANCE_USD_PER_DAY),
6622
- guestMaxConcurrentStreams: envStrictInt("GUEST_MAX_CONCURRENT_STREAMS", env.GUEST_MAX_CONCURRENT_STREAMS),
6623
- guestMinRequestHeadroomMicros: envMicros("GUEST_MIN_REQUEST_HEADROOM_USD", env.GUEST_MIN_REQUEST_HEADROOM_USD),
6624
- guestIpHashRetentionDays: envStrictInt("GUEST_IP_HASH_RETENTION_DAYS", env.GUEST_IP_HASH_RETENTION_DAYS),
6625
- returnBonusDay2Credits: envStrictInt("RETURN_BONUS_DAY2_CREDITS", env.RETURN_BONUS_DAY2_CREDITS),
6626
- returnBonusDay7Credits: envStrictInt("RETURN_BONUS_DAY7_CREDITS", env.RETURN_BONUS_DAY7_CREDITS),
6627
- guestGithubBonusCredits: envStrictInt("GUEST_GITHUB_BONUS_CREDITS", env.GUEST_GITHUB_BONUS_CREDITS),
6628
- returnNudgeEmailEnabled: envBoolean(env.RETURN_NUDGE_EMAIL_ENABLED),
6629
- referralBonusCreditsPerDay: envStrictInt("REFERRAL_BONUS_CREDITS_PER_DAY", env.REFERRAL_BONUS_CREDITS_PER_DAY),
6630
- referralBonusDays: envStrictInt("REFERRAL_BONUS_DAYS", env.REFERRAL_BONUS_DAYS),
6631
- referralMaxActivePerReferrer: envStrictInt("REFERRAL_MAX_ACTIVE_PER_REFERRER", env.REFERRAL_MAX_ACTIVE_PER_REFERRER),
6632
- referralGithubMinAgeDays: envStrictInt("REFERRAL_GITHUB_MIN_AGE_DAYS", env.REFERRAL_GITHUB_MIN_AGE_DAYS),
6633
- referralCreditGrantCredits: envStrictInt("REFERRAL_CREDIT_GRANT_CREDITS", env.REFERRAL_CREDIT_GRANT_CREDITS),
6634
- webClaimCredits: envStrictInt("WEB_CLAIM_CREDITS", env.WEB_CLAIM_CREDITS),
6635
- bountyCredits: envStrictInt("BOUNTY_CREDITS", env.BOUNTY_CREDITS),
6636
- bountyMonthlyCapUsd: envBountyCap(env.BOUNTY_MONTHLY_CAP_USD),
6637
- geoipDbPath: env.GEOIP_DB_PATH?.trim() || undefined,
6638
- sanctionedCountries: envCountries(env.SANCTIONED_COUNTRIES),
6639
- sanctionedTimezones: envTimezones(env.SANCTIONED_TIMEZONES),
6640
- regionLimitsStart: envDate(env.REGION_LIMITS_START),
6641
- fullAllowanceCountries: envCountries(env.FULL_ALLOWANCE_COUNTRIES),
6642
- regionLowCapMicros: envMicros("REGION_LOW_CAP_USD", env.REGION_LOW_CAP_USD),
6643
- priorityCountries: envCountries(env.PRIORITY_COUNTRIES),
6644
- freeSpeed: envFreeSpeedMode(env.FREE_SPEED),
6645
- freeSpeedDelaysS: envFreeSpeedDelays(env.FREE_SPEED_DELAYS),
6646
- streaksEnabled: envStreaksMode(env.STREAKS_ENABLED),
6647
- streakMinRequests: envStrictInt("STREAK_MIN_REQUESTS", env.STREAK_MIN_REQUESTS),
6648
- streakRewards: envStreakRewards(env.STREAK_REWARDS),
6649
- winbackEmail: env.WINBACK_EMAIL?.trim().toLowerCase() === "live" ? "live" : env.WINBACK_EMAIL?.trim().toLowerCase() === "dry_run" ? "dry_run" : undefined,
6650
- winbackMinDays: envStrictInt("WINBACK_MIN_DAYS", env.WINBACK_MIN_DAYS),
6651
- winbackMaxDays: envStrictInt("WINBACK_MAX_DAYS", env.WINBACK_MAX_DAYS),
6652
- winbackMinRequests: envStrictInt("WINBACK_MIN_REQUESTS", env.WINBACK_MIN_REQUESTS),
6653
- winbackDailyMax: envStrictInt("WINBACK_DAILY_MAX", env.WINBACK_DAILY_MAX),
6654
- winbackClaimHours: envStrictInt("WINBACK_CLAIM_HOURS", env.WINBACK_CLAIM_HOURS),
6655
- winbackContentImportBullet: envFlagOn(env.WINBACK_CONTENT_IMPORT_BULLET),
6656
- winbackOfferPriority: env.WINBACK_OFFER_PRIORITY?.trim() || undefined,
6657
- winbackOfferOther: env.WINBACK_OFFER_OTHER?.trim() || undefined,
6658
- winbackArmsPriority: envList(env.WINBACK_ARMS_PRIORITY),
6659
- winbackArmsOther: envList(env.WINBACK_ARMS_OTHER)
6660
- });
6661
- }
6086
+
6662
6087
  // ../shared/src/credits.ts
6663
6088
  var CREDITS_PER_USD = 40000 / 3;
6664
6089
  function usdToCredits(usd) {
@@ -6769,6 +6194,7 @@ function formatLimitNotice(fields, opts = {}) {
6769
6194
  bits.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
6770
6195
  return bits.join(", ");
6771
6196
  }
6197
+
6772
6198
  // ../shared/src/version.ts
6773
6199
  function compareVersions(a, b) {
6774
6200
  const pa = a.split(".").map(Number);
@@ -6785,6 +6211,7 @@ function compareVersions(a, b) {
6785
6211
  }
6786
6212
  return 0;
6787
6213
  }
6214
+
6788
6215
  // ../shared/src/hive.ts
6789
6216
  var HIVE_MAX_CHAT_CHARS = 2048;
6790
6217
  var HIVE_MAX_PROMPT_CHARS = 8192;
@@ -6895,6 +6322,7 @@ var HiveClassifyResponseSchema = object({
6895
6322
  decision: _enum(["allow", "ask"]),
6896
6323
  reason: string2()
6897
6324
  });
6325
+
6898
6326
  // ../shared/src/credit-tiers.ts
6899
6327
  var n = (v) => v.toLocaleString("en-US");
6900
6328
  var day = (iso, timeZone) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", ...timeZone ? { timeZone } : {} }).format(new Date(iso));
@@ -6934,6 +6362,7 @@ function premiumExpiryWarning(i) {
6934
6362
  return;
6935
6363
  return `${n(i.expiring)} premium credits expire today at ${formatRefillEt(at, tz)}`;
6936
6364
  }
6365
+
6937
6366
  // src/cli-args.ts
6938
6367
  var HELP_TEXT = `
6939
6368
  free-pi — free AI coding in your terminal; ads pay for the inference.
@@ -7093,6 +6522,14 @@ function readAutoUpdate(configPath) {
7093
6522
  return true;
7094
6523
  }
7095
6524
  }
6525
+ function readChoicePrompts(configPath) {
6526
+ try {
6527
+ const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
6528
+ return parsed?.choicePrompts !== false;
6529
+ } catch {
6530
+ return true;
6531
+ }
6532
+ }
7096
6533
 
7097
6534
  // src/credentials.ts
7098
6535
  import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
@@ -7238,17 +6675,31 @@ function startGravityPixel(jwt, storageDir, env = process.env, load = () => impo
7238
6675
 
7239
6676
  // src/pi-launch.ts
7240
6677
  import {
7241
- createAgentSessionFromServices,
6678
+ createAgentSessionFromServices as createAgentSessionFromServices2,
7242
6679
  createAgentSessionRuntime,
7243
- createAgentSessionServices,
6680
+ createAgentSessionServices as createAgentSessionServices2,
7244
6681
  createCodemodeExtension,
7245
6682
  createMcpExtension,
7246
6683
  createToolSearchExtension,
7247
6684
  InteractiveMode,
7248
- SessionManager,
7249
- SettingsManager
6685
+ SessionManager as SessionManager2,
6686
+ SettingsManager as SettingsManager2
7250
6687
  } from "@earendil-works/pi-coding-agent";
7251
6688
 
6689
+ // ../pi-ads/src/config.ts
6690
+ function envInt(value, fallback) {
6691
+ if (value === undefined || value === "")
6692
+ return fallback;
6693
+ const n = Number(value);
6694
+ return Number.isInteger(n) ? n : fallback;
6695
+ }
6696
+ function loadConfig(env = process.env) {
6697
+ return {
6698
+ adInlineTurnFrequency: envInt(env.AD_INLINE_TURN_FREQUENCY, 5),
6699
+ adMinColumns: envInt(env.AD_MIN_COLUMNS, 60)
6700
+ };
6701
+ }
6702
+
7252
6703
  // ../pi-ads/src/api.ts
7253
6704
  var REQUEST_TIMEOUT_MS = 5000;
7254
6705
  async function authedFetch(deps, path, init) {
@@ -7402,6 +6853,7 @@ function createBannerRenderer(opts = {}) {
7402
6853
  return;
7403
6854
  if (!ad) {
7404
6855
  screen.banner = undefined;
6856
+ screen.bannerAdId = undefined;
7405
6857
  ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
7406
6858
  opts.onBannerAd?.(undefined);
7407
6859
  return;
@@ -7416,8 +6868,10 @@ function createBannerRenderer(opts = {}) {
7416
6868
  if (isGravity && screen.inline)
7417
6869
  screen.clearInline?.();
7418
6870
  screen.banner = isGravity ? "gravity" : "own";
6871
+ screen.bannerAdId = ad.ad_id;
7419
6872
  screen.clearBanner = () => {
7420
6873
  screen.banner = undefined;
6874
+ screen.bannerAdId = undefined;
7421
6875
  ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
7422
6876
  opts.onBannerAd?.(undefined);
7423
6877
  };
@@ -7458,6 +6912,8 @@ function createInlineRenderer(screen = {}) {
7458
6912
  const isGravity = ad.source === "gravity";
7459
6913
  if (screen.banner === "gravity")
7460
6914
  return;
6915
+ if (!isGravity && screen.banner && screen.bannerAdId === ad.ad_id)
6916
+ return;
7461
6917
  if (isGravity && screen.banner)
7462
6918
  screen.clearBanner?.();
7463
6919
  ctx.ui.setWidget(INLINE_WIDGET_ID, adWidget(ad.creative, ad.click_url, config.adMinColumns), { placement: "belowEditor" });
@@ -7525,6 +6981,8 @@ function mapErrorCode(code) {
7525
6981
  return "this conversation is too long — run /compact or start a new session with /new";
7526
6982
  case "reward_required":
7527
6983
  return "This model is only for reward holders. Finish an EARN task from your terminal to unlock it, or switch models with /model.";
6984
+ case "subagents_unavailable":
6985
+ return "Subagents run on the free Particle model, or with credits. Switch with /model or run /buy-credits.";
7528
6986
  default:
7529
6987
  return assertExhaustiveErrorCode(code);
7530
6988
  }
@@ -7716,7 +7174,7 @@ function createAdsExtension(opts) {
7716
7174
  }
7717
7175
 
7718
7176
  // src/pi-launch.ts
7719
- import { dirname as dirname10, join as join11, resolve as resolve4 } from "node:path";
7177
+ import { dirname as dirname11, join as join11, resolve as resolve4 } from "node:path";
7720
7178
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7721
7179
 
7722
7180
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
@@ -7921,6 +7379,11 @@ function AddOptionalAction(type, options) {
7921
7379
  return result;
7922
7380
  }
7923
7381
 
7382
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/array.mjs
7383
+ function _Array_(items, options) {
7384
+ return Create({ "~kind": "Array" }, { type: "array", items }, options);
7385
+ }
7386
+
7924
7387
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/action/_add_optional.mjs
7925
7388
  function AddOptional(type, options = {}) {
7926
7389
  return AddOptionalAction(type, options);
@@ -8260,6 +7723,439 @@ function createImageToolExtension(opts) {
8260
7723
  };
8261
7724
  }
8262
7725
 
7726
+ // src/choice-columns.ts
7727
+ import { matchesKey, truncateToWidth as truncateToWidth3, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
7728
+ var STACK_BELOW = 72;
7729
+ var COLUMN_FOOTER = "left/right compare enter select o something else d let the agent decide esc cancel";
7730
+ var pad2 = (s, w) => s + " ".repeat(Math.max(0, w - visibleWidth(s)));
7731
+ function cell(s, w, st) {
7732
+ const out = [];
7733
+ const add = (mark, t) => wrapTextWithAnsi(t, Math.max(1, w - 2)).forEach((l, i) => out.push((i ? " " : mark + " ") + l));
7734
+ for (const p of s.pros ?? [])
7735
+ add(st.success("+"), p);
7736
+ for (const c of s.cons ?? [])
7737
+ add(st.error("-"), c);
7738
+ if (s.best_for) {
7739
+ if (out.length)
7740
+ out.push("");
7741
+ out.push(st.dim("Best for"));
7742
+ wrapTextWithAnsi(s.best_for, w).forEach((l) => out.push(l));
7743
+ }
7744
+ return out;
7745
+ }
7746
+ function build(o, width, st, rows, scroll) {
7747
+ const budget = Math.max(8, rows - 3);
7748
+ const hw = Math.max(1, width - 2);
7749
+ const headAll = [...wrapTextWithAnsi(st.bold(o.question), hw), ...o.context ? wrapTextWithAnsi(st.dim(o.context), hw) : []];
7750
+ const headMax = Math.max(2, Math.floor(budget / 3));
7751
+ const head = headAll.slice(0, headMax).map((l, i, a) => " " + (i === a.length - 1 && headAll.length > headMax ? truncateToWidth3(l, hw - 1) + "…" : l));
7752
+ head.push("");
7753
+ const foot = wrapTextWithAnsi(st.dim(COLUMN_FOOTER), hw).map((l) => " " + l);
7754
+ const title = (i, w) => {
7755
+ const t = truncateToWidth3(o.sides[i].label, Math.max(1, w - 2));
7756
+ return o.selected === i ? st.accent(st.bold("> " + t)) : " " + t;
7757
+ };
7758
+ const hint = (from, n, total) => " " + st.dim(`up/down scroll (${from + 1}-${from + n} of ${total})`);
7759
+ const clamp = (lines) => lines.map((l) => visibleWidth(l) > width ? truncateToWidth3(l, width) : l);
7760
+ const windowed = (detail, avail) => {
7761
+ if (detail.length <= avail)
7762
+ return { rows: detail, hint: [], max: 0 };
7763
+ const n = Math.max(1, avail - 1);
7764
+ const max = detail.length - n;
7765
+ const sc = Math.min(Math.max(0, scroll), max);
7766
+ return { rows: detail.slice(sc, sc + n), hint: [hint(sc, n, detail.length)], max };
7767
+ };
7768
+ if (width < STACK_BELOW) {
7769
+ const w = Math.max(1, width - 4);
7770
+ const cells = o.sides.map((s) => cell(s, w, st));
7771
+ const full = o.sides.flatMap((s, i) => [" " + title(i, w + 2), ...cells[i].map((l) => " " + l), ""]);
7772
+ if (head.length + full.length + foot.length <= budget)
7773
+ return { lines: clamp([...head, ...full, ...foot]), maxScroll: 0 };
7774
+ const titles = o.sides.map((_, i) => " " + title(i, w + 2));
7775
+ const avail = Math.max(1, budget - head.length - titles.length - foot.length);
7776
+ const detail = cells[o.selected].map((l) => " " + l);
7777
+ const win = windowed(detail, avail);
7778
+ return { lines: clamp([...head, ...titles, ...win.rows, ...win.hint, ...foot]), maxScroll: win.max };
7779
+ }
7780
+ const w = Math.floor((width - 8) / 2);
7781
+ const rule = (l, m, r) => " " + l + "─".repeat(w + 2) + m + "─".repeat(w + 2) + r;
7782
+ const rowOf = (x, y) => " │ " + pad2(x, w) + " │ " + pad2(y, w) + " │";
7783
+ const ca = cell(o.sides[0], w, st), cb = cell(o.sides[1], w, st);
7784
+ const total = Math.max(ca.length, cb.length);
7785
+ const fixed = head.length + 4 + foot.length;
7786
+ const avail = Math.max(1, budget - fixed);
7787
+ const n = total <= avail ? total : Math.max(1, avail - 1);
7788
+ const max = Math.max(0, total - n);
7789
+ const sc = Math.min(Math.max(0, scroll), max);
7790
+ const body = [];
7791
+ for (let i = sc;i < sc + n; i++)
7792
+ body.push(rowOf(ca[i] ?? "", cb[i] ?? ""));
7793
+ const more = max > 0 ? [hint(sc, n, total)] : [];
7794
+ return { lines: clamp([...head, rule("┌", "┬", "┐"), rowOf(title(0, w), title(1, w)), rule("├", "┼", "┤"), ...body, rule("└", "┴", "┘"), ...more, ...foot]), maxScroll: max };
7795
+ }
7796
+ function renderColumns(o, width, st, rows = 1000, scroll = 0) {
7797
+ return build(o, width, st, rows, scroll).lines;
7798
+ }
7799
+ function columnsMaxScroll(o, width, st, rows) {
7800
+ return build(o, width, st, rows, 0).maxScroll;
7801
+ }
7802
+ function columnsComponent(o, st, tui, done) {
7803
+ let selected = 0;
7804
+ let scroll = 0;
7805
+ let lastWidth = 100;
7806
+ const rows = () => tui.terminal?.rows ?? 1000;
7807
+ let finished = false;
7808
+ const finish = (r) => {
7809
+ if (!finished) {
7810
+ finished = true;
7811
+ o.signal?.removeEventListener("abort", onAbort);
7812
+ done(r);
7813
+ }
7814
+ };
7815
+ const onAbort = () => finish(undefined);
7816
+ if (o.signal?.aborted)
7817
+ queueMicrotask(onAbort);
7818
+ else
7819
+ o.signal?.addEventListener("abort", onAbort, { once: true });
7820
+ return {
7821
+ invalidate() {},
7822
+ dispose() {
7823
+ o.signal?.removeEventListener("abort", onAbort);
7824
+ },
7825
+ render: (width) => {
7826
+ lastWidth = width;
7827
+ return renderColumns({ question: o.question, context: o.context, sides: o.sides, selected }, width, st, rows(), scroll);
7828
+ },
7829
+ handleInput(d) {
7830
+ if (matchesKey(d, "left") || matchesKey(d, "right") || matchesKey(d, "tab")) {
7831
+ selected = selected === 0 ? 1 : 0;
7832
+ scroll = 0;
7833
+ tui.requestRender();
7834
+ } else if (matchesKey(d, "up") || matchesKey(d, "down")) {
7835
+ const max = columnsMaxScroll({ question: o.question, context: o.context, sides: o.sides, selected }, lastWidth, st, rows());
7836
+ scroll = Math.min(max, Math.max(0, scroll + (matchesKey(d, "down") ? 1 : -1)));
7837
+ tui.requestRender();
7838
+ } else if (matchesKey(d, "enter"))
7839
+ finish(o.labels[selected]);
7840
+ else if (d === "o")
7841
+ finish(o.else_);
7842
+ else if (d === "d")
7843
+ finish(o.decide);
7844
+ else if (matchesKey(d, "escape") || matchesKey(d, "ctrl+c"))
7845
+ finish(undefined);
7846
+ }
7847
+ };
7848
+ }
7849
+
7850
+ // src/choice-tool.ts
7851
+ var ASK_USER_TOOL_NAME = "ask_user";
7852
+ var CHOICE_NO_ANSWER = "No answer; decide yourself and state your pick in one line.";
7853
+ var CHOICE_LIMIT_TEXT = "Ask limit reached; decide yourself.";
7854
+ var AGENT_DECIDES = "Let the agent decide";
7855
+ var SOMETHING_ELSE = "Something else (type it)";
7856
+ var COMPARE = "Compare pros and cons";
7857
+ var CHOICE_COMPARE_TEXT = "The user asked to compare the options. Explain the pros and cons of each option briefly as visible text, then call ask_user again with the same options.";
7858
+ var OTHER_MAX_CHARS = 500;
7859
+ var CAPS_ENTRY = "free-pi-choice-caps";
7860
+ var MAX_PER_SESSION = 10;
7861
+ var MAX_QUESTIONS_PER_SESSION = 5;
7862
+ var ANSWER_MAX_CHARS = 2000;
7863
+ var INT_MAX = 2147483647;
7864
+ var POST_TIMEOUT_MS = 3000;
7865
+ var COUNTER_MAX = 50;
7866
+ var CHOICE_RULES = `- Before choosing a library, service, language or other tool that the request and
7867
+ existing code do not already settle, call ask_user. Narrow to the best two options
7868
+ where you can and give pros, cons and best_for for each; use 3 or 4 options only for
7869
+ a genuinely wider choice. Pass your recommendation.
7870
+ - When an answer would change what you do and you cannot find it in the request or the
7871
+ code, call ask_user with just a question. Do not ask for permission to continue or to
7872
+ confirm what you already know.
7873
+ - Wait for the answer before continuing. Do not ask about trivia such as names,
7874
+ formatting or file layout, or twice on the same topic. If there is no answer, pick
7875
+ yourself and say what you picked.`;
7876
+ function choiceEnabled(o) {
7877
+ if (o.env.FREEPI_NO_CHOICE === "1")
7878
+ return false;
7879
+ if (!readChoicePrompts(o.configPath))
7880
+ return false;
7881
+ return o.env.FREEPI_CHOICE === "1" || o.arm === "treatment";
7882
+ }
7883
+ var capsBySession = new Map;
7884
+ var emptyCounters = () => ({ attempted: 0, shown: 0, suppressed: {}, delivery_failures: 0 });
7885
+ var norm = (s) => s.trim().replace(/\s+/g, " ").toLowerCase();
7886
+ function createChoiceToolExtension(opts) {
7887
+ const doFetch = opts.fetchImpl ?? fetch;
7888
+ const now = opts.now ?? Date.now;
7889
+ const random = opts.random ?? Math.random;
7890
+ const shutdownBoundMs = opts.shutdownBoundMs ?? POST_TIMEOUT_MS;
7891
+ const flushMs = opts.counterFlushMs ?? 60000;
7892
+ return {
7893
+ name: "free-pi-choice-tool",
7894
+ factory(pi) {
7895
+ function capsFor(sm) {
7896
+ const id = sm.getSessionId();
7897
+ let caps = capsBySession.get(id);
7898
+ if (!caps) {
7899
+ caps = { shownCount: 0, sets: new Set, asked: 0, questions: new Set };
7900
+ const saved = [...sm.getEntries()].reverse().find((e) => e.type === "custom" && e.customType === CAPS_ENTRY);
7901
+ if (saved?.data) {
7902
+ caps.shownCount = saved.data.shownCount ?? 0;
7903
+ caps.sets = new Set(saved.data.sets ?? []);
7904
+ caps.asked = saved.data.asked ?? 0;
7905
+ caps.questions = new Set(saved.data.questions ?? []);
7906
+ }
7907
+ capsBySession.set(id, caps);
7908
+ }
7909
+ return caps;
7910
+ }
7911
+ let counters = emptyCounters();
7912
+ let flushTimer;
7913
+ const dirty = (c) => c.attempted + c.shown + c.delivery_failures + Object.values(c.suppressed).reduce((a, b) => a + (b ?? 0), 0) > 0;
7914
+ function takeCounters() {
7915
+ if (!dirty(counters))
7916
+ return;
7917
+ const out = emptyCounters();
7918
+ const left = emptyCounters();
7919
+ const cut = (v) => [Math.min(v, COUNTER_MAX), v - Math.min(v, COUNTER_MAX)];
7920
+ [out.attempted, left.attempted] = cut(counters.attempted);
7921
+ [out.shown, left.shown] = cut(counters.shown);
7922
+ [out.delivery_failures, left.delivery_failures] = cut(counters.delivery_failures);
7923
+ for (const [k, v] of Object.entries(counters.suppressed)) {
7924
+ const [a, b] = cut(v ?? 0);
7925
+ if (a)
7926
+ out.suppressed[k] = a;
7927
+ if (b)
7928
+ left.suppressed[k] = b;
7929
+ }
7930
+ counters = left;
7931
+ return out;
7932
+ }
7933
+ function giveBack(c) {
7934
+ counters.attempted += c.attempted;
7935
+ counters.shown += c.shown;
7936
+ counters.delivery_failures += c.delivery_failures + 1;
7937
+ for (const [k, v] of Object.entries(c.suppressed))
7938
+ counters.suppressed[k] = (counters.suppressed[k] ?? 0) + (v ?? 0);
7939
+ }
7940
+ async function post(path, body, attempts = 2) {
7941
+ for (let attempt = 0;attempt < attempts; attempt++) {
7942
+ try {
7943
+ const token = await opts.getToken();
7944
+ const res = await doFetch(`${opts.baseUrl}${path}`, {
7945
+ method: "POST",
7946
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
7947
+ body: JSON.stringify(body),
7948
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS)
7949
+ });
7950
+ if (res.ok)
7951
+ return true;
7952
+ if (res.status >= 400 && res.status < 500)
7953
+ return false;
7954
+ } catch {}
7955
+ }
7956
+ return false;
7957
+ }
7958
+ function scheduleFlush() {
7959
+ if (flushTimer)
7960
+ return;
7961
+ flushTimer = setTimeout(() => {
7962
+ flushTimer = undefined;
7963
+ const c = takeCounters();
7964
+ if (!c)
7965
+ return;
7966
+ post("/telemetry/choice/counters", c).then((ok) => {
7967
+ if (!ok)
7968
+ giveBack(c);
7969
+ });
7970
+ if (dirty(counters))
7971
+ scheduleFlush();
7972
+ }, flushMs);
7973
+ flushTimer.unref?.();
7974
+ }
7975
+ function suppress(reason) {
7976
+ counters.suppressed[reason] = (counters.suppressed[reason] ?? 0) + 1;
7977
+ scheduleFlush();
7978
+ }
7979
+ function deliver(event) {
7980
+ setTimeout(() => {
7981
+ const c = takeCounters();
7982
+ post("/telemetry/choice", c ? { ...event, counters: c } : event).then((ok) => {
7983
+ if (ok)
7984
+ return;
7985
+ if (c)
7986
+ giveBack(c);
7987
+ else
7988
+ counters.delivery_failures += 1;
7989
+ scheduleFlush();
7990
+ });
7991
+ }, 0);
7992
+ }
7993
+ pi.on("session_shutdown", async () => {
7994
+ clearTimeout(flushTimer);
7995
+ flushTimer = undefined;
7996
+ const c = takeCounters();
7997
+ if (!c)
7998
+ return;
7999
+ const bound = new Promise((r) => {
8000
+ const t = setTimeout(() => r(false), shutdownBoundMs);
8001
+ t.unref?.();
8002
+ });
8003
+ await Promise.race([post("/telemetry/choice/counters", c, 1), bound]);
8004
+ });
8005
+ pi.registerTool({
8006
+ name: ASK_USER_TOOL_NAME,
8007
+ executionMode: "sequential",
8008
+ label: "Ask User",
8009
+ description: "Ask the user something. With options: pick between them when the request and existing code do not already settle a choice of library, service, language or tool; prefer the best two options with pros, cons and best_for for each (shown side by side), and 3 or 4 only for a wider choice. With no options: ask an open question when the answer would change what you do and you cannot find it in the request or code. Returns the user's answer or choice, or says to decide yourself.",
8010
+ parameters: _Object_({
8011
+ question: String2({ minLength: 1, maxLength: 200 }),
8012
+ options: Optional(_Array_(_Object_({
8013
+ id: Optional(String2({ minLength: 1, maxLength: 40 })),
8014
+ label: String2({ minLength: 1, maxLength: 40 }),
8015
+ note: Optional(String2({ maxLength: 120 })),
8016
+ pros: Optional(_Array_(String2({ minLength: 1, maxLength: 100 }), { maxItems: 4 })),
8017
+ cons: Optional(_Array_(String2({ minLength: 1, maxLength: 100 }), { maxItems: 4 })),
8018
+ best_for: Optional(String2({ maxLength: 120 }))
8019
+ }), { maxItems: 4 })),
8020
+ context: Optional(String2({ maxLength: 300 })),
8021
+ recommended: Optional(Integer({ minimum: 0, maximum: 3 }))
8022
+ }),
8023
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
8024
+ counters.attempted += 1;
8025
+ const text = (t, details = {}) => ({ content: [{ type: "text", text: t }], details });
8026
+ if (params.options?.length === 1)
8027
+ throw new Error("ask_user takes either no options (an open question) or 2 to 4 options. Add a second option, or omit options to ask a free-text question.");
8028
+ const open = !params.options?.length;
8029
+ const options = (params.options ?? []).map((o, i) => ({ id: o.id ?? `opt${i + 1}`, label: o.label.trim(), ...o.note ? { note: o.note } : {}, ...o.pros?.length ? { pros: o.pros } : {}, ...o.cons?.length ? { cons: o.cons } : {}, ...o.best_for ? { best_for: o.best_for } : {} }));
8030
+ const labels = options.map((o) => norm(o.label));
8031
+ if (new Set(labels).size !== labels.length || [AGENT_DECIDES, SOMETHING_ELSE, COMPARE].some((r) => labels.includes(norm(r)))) {
8032
+ suppress("duplicate_labels");
8033
+ throw new Error(`ask_user options must have distinct labels (case-insensitive) and none may be "${AGENT_DECIDES}", "${SOMETHING_ELSE}" or "${COMPARE}". Fix the labels and call again.`);
8034
+ }
8035
+ if (new Set(options.map((o) => o.id)).size !== options.length)
8036
+ throw new Error("ask_user option ids must be distinct. Fix them or omit the ids.");
8037
+ if (opts.headless || !ctx.hasUI || opts.isRoomOpen?.()) {
8038
+ suppress("headless");
8039
+ return text(CHOICE_NO_ANSWER, { suppressed: "headless" });
8040
+ }
8041
+ const state = capsFor(ctx.sessionManager);
8042
+ const setKey = open ? norm(params.question) : [...labels].sort().join("\x00");
8043
+ if ((open ? state.questions : state.sets).has(setKey)) {
8044
+ suppress("repeat");
8045
+ return text(CHOICE_LIMIT_TEXT, { suppressed: "repeat" });
8046
+ }
8047
+ if (open ? state.asked >= MAX_QUESTIONS_PER_SESSION : state.shownCount >= MAX_PER_SESSION) {
8048
+ suppress("capped_session");
8049
+ return text(CHOICE_LIMIT_TEXT, { suppressed: "capped_session" });
8050
+ }
8051
+ const saveCaps = () => pi.appendEntry(CAPS_ENTRY, { shownCount: state.shownCount, sets: [...state.sets], asked: state.asked, questions: [...state.questions] });
8052
+ if (open) {
8053
+ state.asked += 1;
8054
+ state.questions.add(setKey);
8055
+ } else {
8056
+ state.shownCount += 1;
8057
+ state.sets.add(setKey);
8058
+ }
8059
+ saveCaps();
8060
+ counters.shown += 1;
8061
+ const displayOrder = options.map((_, i) => i);
8062
+ for (let i = displayOrder.length - 1;i > 0; i--) {
8063
+ const j = Math.floor(random() * (i + 1));
8064
+ [displayOrder[i], displayOrder[j]] = [displayOrder[j], displayOrder[i]];
8065
+ }
8066
+ const byLabel = new Map(options.map((o, i) => [o.label, i]));
8067
+ const notes = options.filter((o) => o.note).map((o) => ` ${o.label}: ${o.note}`);
8068
+ const title = [params.question, params.context, ...notes.length ? ["", ...notes] : []].filter((l) => l !== undefined).join(`
8069
+ `);
8070
+ const startedAt = now();
8071
+ let picked;
8072
+ let typed;
8073
+ let threw = false;
8074
+ try {
8075
+ const ui = ctx.ui;
8076
+ if (open) {
8077
+ typed = (await ctx.ui.input([params.question, params.context].filter(Boolean).join(`
8078
+ `), "Type your answer", { signal }))?.trim().slice(0, ANSWER_MAX_CHARS);
8079
+ } else if (options.length === 2 && options.some((o) => o.pros || o.cons) && typeof ui.custom === "function") {
8080
+ const sides = displayOrder.map((i) => options[i]);
8081
+ picked = await ctx.ui.custom((tui, theme, _kb, done) => columnsComponent({
8082
+ question: params.question,
8083
+ context: params.context,
8084
+ sides,
8085
+ labels: [sides[0].label, sides[1].label],
8086
+ else_: SOMETHING_ELSE,
8087
+ decide: AGENT_DECIDES,
8088
+ signal
8089
+ }, {
8090
+ accent: (x) => theme.fg("accent", x),
8091
+ success: (x) => theme.fg("success", x),
8092
+ error: (x) => theme.fg("error", x),
8093
+ dim: (x) => theme.fg("dim", x),
8094
+ bold: (x) => theme.bold(x)
8095
+ }, tui, done));
8096
+ } else
8097
+ picked = await ctx.ui.select(title, [...displayOrder.map((i) => options[i].label), SOMETHING_ELSE, COMPARE, AGENT_DECIDES], { signal });
8098
+ if (!open && picked === SOMETHING_ELSE) {
8099
+ typed = (await ctx.ui.input(params.question, "Type your answer", { signal }))?.trim().slice(0, OTHER_MAX_CHARS);
8100
+ }
8101
+ } catch {
8102
+ threw = true;
8103
+ }
8104
+ const latency = Math.min(INT_MAX, Math.max(0, Math.round(now() - startedAt)));
8105
+ let outcome;
8106
+ let chosenIndex = null;
8107
+ if (threw)
8108
+ outcome = "cancel_ambiguous";
8109
+ else if (open)
8110
+ outcome = typed ? "answered" : "aborted";
8111
+ else if (picked === AGENT_DECIDES)
8112
+ outcome = "delegated";
8113
+ else if (picked === COMPARE)
8114
+ outcome = "compare";
8115
+ else if (picked === SOMETHING_ELSE && typed)
8116
+ outcome = "other";
8117
+ else if (picked === SOMETHING_ELSE)
8118
+ outcome = "aborted";
8119
+ else if (picked !== undefined && byLabel.has(picked)) {
8120
+ outcome = "chosen";
8121
+ chosenIndex = byLabel.get(picked);
8122
+ } else if (picked === undefined)
8123
+ outcome = "aborted";
8124
+ else
8125
+ outcome = "cancel_ambiguous";
8126
+ if (outcome === "compare") {
8127
+ state.sets.delete(setKey);
8128
+ state.shownCount -= 1;
8129
+ saveCaps();
8130
+ }
8131
+ const rec = typeof params.recommended === "number" && params.recommended < options.length ? params.recommended : null;
8132
+ const model = ctx.model?.id ?? "unknown";
8133
+ const event = {
8134
+ session_id: opts.sessionId,
8135
+ tool_call_id: toolCallId,
8136
+ question: params.question,
8137
+ context: params.context ?? "",
8138
+ options,
8139
+ display_order: displayOrder,
8140
+ outcome,
8141
+ chosen_index: chosenIndex,
8142
+ ...outcome === "other" || outcome === "answered" ? { other_text: typed } : {},
8143
+ agent_pick_index: rec,
8144
+ model,
8145
+ latency_ms: latency,
8146
+ cli_version: CLI_VERSION,
8147
+ arm: opts.arm()
8148
+ };
8149
+ deliver(event);
8150
+ if (outcome === "compare")
8151
+ return text(CHOICE_COMPARE_TEXT, event);
8152
+ return text(outcome === "other" || outcome === "answered" ? `User replied: ${typed}` : chosenIndex !== null ? `User chose: ${options[chosenIndex].label}` : CHOICE_NO_ANSWER, event);
8153
+ }
8154
+ });
8155
+ }
8156
+ };
8157
+ }
8158
+
8263
8159
  // src/vision-tool.ts
8264
8160
  import { closeSync, fstatSync, openSync, readSync, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
8265
8161
  import { isAbsolute, relative, resolve, sep as sep2 } from "node:path";
@@ -8651,14 +8547,14 @@ function createDocsToolExtension() {
8651
8547
  }
8652
8548
 
8653
8549
  // src/todo-tool.ts
8654
- import { truncateToWidth as truncateToWidth4 } from "@earendil-works/pi-tui";
8550
+ import { truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
8655
8551
 
8656
8552
  // src/todo-view.ts
8657
- import { truncateToWidth as truncateToWidth3, visibleWidth } from "@earendil-works/pi-tui";
8553
+ import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
8658
8554
  var PLAIN_STYLE = { bold: (s) => s, dim: (s) => s, accent: (s) => s, error: (s) => s };
8659
8555
  var GAP = " ";
8660
8556
  function truncate(text, max) {
8661
- return max <= 0 ? "" : truncateToWidth3(text, max, "…");
8557
+ return max <= 0 ? "" : truncateToWidth4(text, max, "…");
8662
8558
  }
8663
8559
  function counts(todos) {
8664
8560
  return { done: todos.filter((t) => t.done).length, total: todos.length };
@@ -8670,7 +8566,7 @@ function widgetLines(todos, width, st = PLAIN_STYLE) {
8670
8566
  const header = `Todos ${done}/${total}`;
8671
8567
  const firstOpen = todos.findIndex((t) => !t.done);
8672
8568
  const marks = todos.map((t, i) => t.done ? `✓ ${t.text}` : i === firstOpen ? `▸ ${t.text}` : `○ ${t.text}`);
8673
- const compactLength = visibleWidth([header, ...marks].join(GAP));
8569
+ const compactLength = visibleWidth2([header, ...marks].join(GAP));
8674
8570
  if (compactLength <= width) {
8675
8571
  return [
8676
8572
  [
@@ -8695,7 +8591,7 @@ function widgetLines(todos, width, st = PLAIN_STYLE) {
8695
8591
  return lines;
8696
8592
  }
8697
8593
  function resultLines(d, st = PLAIN_STYLE, width = Infinity) {
8698
- return rawResultLines(d, st).map((line) => Number.isFinite(width) ? truncateToWidth3(line, Math.max(0, width), "…") : line);
8594
+ return rawResultLines(d, st).map((line) => Number.isFinite(width) ? truncateToWidth4(line, Math.max(0, width), "…") : line);
8699
8595
  }
8700
8596
  function rawResultLines(d, st) {
8701
8597
  if (d.error)
@@ -8892,7 +8788,7 @@ function createTodoToolExtension(options = {}) {
8892
8788
  const details = res.details;
8893
8789
  const text = res.content[0]?.type === "text" ? res.content[0].text : "";
8894
8790
  return {
8895
- render: (width) => details?.action ? resultLines(details, styleFrom(theme), width) : [truncateToWidth4(text, width, "…")],
8791
+ render: (width) => details?.action ? resultLines(details, styleFrom(theme), width) : [truncateToWidth5(text, width, "…")],
8896
8792
  invalidate() {}
8897
8793
  };
8898
8794
  }
@@ -9015,42 +8911,239 @@ function createToolAliasesExtension(opts = {}) {
9015
8911
  };
9016
8912
  }
9017
8913
 
9018
- // src/error-notice.ts
9019
- var UPSTREAM_ERROR_FRAME_MESSAGE = "upstream returned an error frame";
9020
- var PROVIDER_FAILING_NOTICE = "This model's provider is failing right now. Try again in a moment, or switch models with /model.";
9021
- var NOTICE_DEDUPE_MS = 15000;
9022
- function optionalNumber(value) {
9023
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
8914
+ // src/fusion.ts
8915
+ var FUSION_ID = "fusion";
8916
+ var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
8917
+ var FUSION_NAME = "\uD83E\uDDE8 Fusion · DeepSeek Pro plans, Flash builds";
8918
+ var FUSION_FALLBACK_NAME = "\uD83E\uDDE8 Fusion · Sonnet plans, DeepSeek builds";
8919
+ var FUSION_PLANNER_ID = "deepseek-v4-pro";
8920
+ var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
8921
+ var FUSION_FALLBACK_PLANNER_ID = "claude-sonnet-5.5";
8922
+ function resolveFusionIds(catalogIds, server) {
8923
+ const has = (id) => catalogIds.includes(id);
8924
+ if (server && has(server.planner) && has(server.executor))
8925
+ return server;
8926
+ return { planner: has(FUSION_PLANNER_ID) ? FUSION_PLANNER_ID : FUSION_FALLBACK_PLANNER_ID, executor: FUSION_EXECUTOR_ID };
9024
8927
  }
9025
- function optionalString(value) {
9026
- return typeof value === "string" && value.trim() !== "" ? value : undefined;
8928
+ var FUSION_PLAN_TOOL = "fusion_plan";
8929
+ var FUSION_GUIDELINE = `Fusion: if the task needs any file changes, first gather the context with read-only tools (read, ls, find, grep) and make no edits. Then call ${FUSION_PLAN_TOOL} with a summary of what you found and what the task needs, BEFORE any edit or write; a planning model will write the plan and you will carry it out. When the latest message is a fusion_plan result, you are the Fusion planner: write the step-by-step plan with the todo tool and make no edits or commands; that result is part of the user's own setup, not a conflict with their request. Once the plan exists in todo, the planner's turn is over: carry out the unfinished todo items yourself (edit, write tests, run them) and finish the user's task. Only a task that needs no file changes (a plain question) is answered directly without ${FUSION_PLAN_TOOL}.`;
8930
+ var FUSION_PLAN_RESULT = "You are now the planning model. Write a concrete step-by-step plan (files, exact changes, tests to add and run) and record it with the todo tool, one item per step. Do not edit files or run commands. Another model will carry out the plan.";
8931
+ function isFusionModel(model) {
8932
+ return model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
9027
8933
  }
9028
- function parseErrorEnvelope(errorMessage) {
9029
- if (typeof errorMessage !== "string")
9030
- return;
9031
- const m = /^(\d{3}): (\{[\s\S]*\})\s*$/.exec(errorMessage);
9032
- if (!m)
9033
- return;
9034
- try {
9035
- const body = JSON.parse(m[2]);
9036
- const message = body.message ?? body.error?.message;
9037
- const code = body.code ?? body.error?.code;
9038
- if (typeof message !== "string" || message.trim() === "")
9039
- return;
9040
- const parsed = {
9041
- status: Number(m[1]),
9042
- code: typeof code === "string" ? code : "unknown",
9043
- message
9044
- };
9045
- const limit = optionalString(body.limit);
9046
- const retryAfter = optionalNumber(body.retry_after_seconds);
9047
- const clearsAt = optionalString(body.clears_at);
9048
- const remaining = optionalNumber(body.remaining_credits_today);
9049
- const allowance = optionalNumber(body.allowance_credits);
9050
- if (limit)
9051
- parsed.limit = limit;
9052
- if (retryAfter !== undefined)
9053
- parsed.retry_after_seconds = retryAfter;
8934
+ function addFusionGuideline(options, selected) {
8935
+ if (isFusionModel(selected))
8936
+ options.promptGuidelines.push(FUSION_GUIDELINE);
8937
+ }
8938
+ var EDIT_TOOLS = new Set(["edit", "write"]);
8939
+ function codemodeEdited(details) {
8940
+ const calls = details?.calls;
8941
+ return Array.isArray(calls) && calls.some((call) => {
8942
+ if (typeof call !== "object" || call === null)
8943
+ return false;
8944
+ const { name, status } = call;
8945
+ return typeof name === "string" && EDIT_TOOLS.has(name) && status === "ok";
8946
+ });
8947
+ }
8948
+ function handedOffThisTurn(messages) {
8949
+ for (let i = messages.length - 1;i >= 0; i--) {
8950
+ const message = messages[i];
8951
+ if (message.role === "user")
8952
+ break;
8953
+ if (message.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL && !message.isError)
8954
+ return true;
8955
+ }
8956
+ return false;
8957
+ }
8958
+ var STALL_CONTINUATION = {
8959
+ continue: true,
8960
+ entries: [{ type: "custom_message", customType: "fusion-continue", display: false, content: "The plan is recorded. Carry out the unfinished todo items now and finish the user's task." }]
8961
+ };
8962
+ function plannerMadeTheCall(messages, plannerId) {
8963
+ for (let i = messages.length - 1;i >= 0; i--) {
8964
+ const message = messages[i];
8965
+ if (message.role === "assistant")
8966
+ return message.provider === PROVIDER_NAME && message.model === plannerId;
8967
+ }
8968
+ return false;
8969
+ }
8970
+ function branchMessages(ctx) {
8971
+ return ctx.sessionManager.getBranch().flatMap((entry) => entry.type === "message" ? [entry.message] : []);
8972
+ }
8973
+ function alreadyContinued(ctx) {
8974
+ const branch = ctx.sessionManager.getBranch();
8975
+ for (let i = branch.length - 1;i >= 0; i--) {
8976
+ const entry = branch[i];
8977
+ if (entry.type === "custom_message" && entry.customType === STALL_CONTINUATION.entries[0].customType)
8978
+ return true;
8979
+ const message = entry.type === "message" ? entry.message : undefined;
8980
+ if (message?.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL)
8981
+ return false;
8982
+ }
8983
+ return false;
8984
+ }
8985
+ function plannerStalled(messages, plannerId) {
8986
+ const last = messages[messages.length - 1];
8987
+ if (!last || last.role !== "assistant" || last.provider !== PROVIDER_NAME || last.model !== plannerId)
8988
+ return false;
8989
+ if (last.stopReason !== "stop")
8990
+ return false;
8991
+ return handedOffThisTurn(messages) && !last.content.some((part) => part.type === "toolCall");
8992
+ }
8993
+ function editedThisTurn(messages) {
8994
+ for (let i = messages.length - 1;i >= 0; i--) {
8995
+ const message = messages[i];
8996
+ if (message.role === "user")
8997
+ break;
8998
+ if (message.role !== "toolResult")
8999
+ continue;
9000
+ if (message.toolName === "codemode" ? codemodeEdited(message.details) : EDIT_TOOLS.has(message.toolName) && !message.isError) {
9001
+ return true;
9002
+ }
9003
+ }
9004
+ return false;
9005
+ }
9006
+ function hasNewUserInput(request) {
9007
+ const failedAt = request.failed?.message.timestamp;
9008
+ if (failedAt === undefined)
9009
+ return false;
9010
+ for (let i = request.messages.length - 1;i >= 0; i--) {
9011
+ const message = request.messages[i];
9012
+ if (message.role === "assistant")
9013
+ return false;
9014
+ if (message.role === "user")
9015
+ return message.timestamp > failedAt;
9016
+ }
9017
+ return false;
9018
+ }
9019
+ function routeFusion(request, models) {
9020
+ const phase = request.state?.phase ?? "explore";
9021
+ const restart = request.reason === "user" || request.reason === "retry" && hasNewUserInput(request);
9022
+ const route = (model, nextPhase = phase) => ({
9023
+ model,
9024
+ thinkingLevel: request.thinkingLevel,
9025
+ state: request.reason === "direct" ? undefined : request.state?.phase === nextPhase ? request.state : { phase: nextPhase }
9026
+ });
9027
+ if (!models.planner || !models.executor) {
9028
+ return route(models.defaultModel, restart ? "explore" : phase);
9029
+ }
9030
+ switch (request.reason) {
9031
+ case "user":
9032
+ return route(models.executor, "explore");
9033
+ case "direct":
9034
+ return route(models.executor);
9035
+ case "retry": {
9036
+ if (restart)
9037
+ return route(models.executor, "explore");
9038
+ const failed = request.failed;
9039
+ if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === models.planner?.id) {
9040
+ return route(models.executor, "build");
9041
+ }
9042
+ return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
9043
+ }
9044
+ case "continuation":
9045
+ if (phase !== "explore")
9046
+ return route(models.executor, "build");
9047
+ if (editedThisTurn(request.messages))
9048
+ return route(models.executor, "build");
9049
+ return handedOffThisTurn(request.messages) ? route(models.planner, "plan") : route(models.executor, "explore");
9050
+ }
9051
+ }
9052
+ function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
9053
+ 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";
9054
+ return {
9055
+ name: "free-pi-fusion",
9056
+ factory: (pi) => {
9057
+ const syncPlanTool = (selected) => {
9058
+ const on = isFusionModel(selected);
9059
+ const active = pi.getActiveTools();
9060
+ if (on === active.includes(FUSION_PLAN_TOOL))
9061
+ return;
9062
+ pi.setActiveTools(on ? [...active, FUSION_PLAN_TOOL] : active.filter((name) => name !== FUSION_PLAN_TOOL));
9063
+ };
9064
+ pi.on("session_start", (_event, ctx) => syncPlanTool(ctx.model));
9065
+ pi.on("model_select", (event) => syncPlanTool(event.model));
9066
+ pi.registerTool({
9067
+ name: FUSION_PLAN_TOOL,
9068
+ label: "Fusion plan",
9069
+ description: "Hand off to the planning model once you have gathered enough context. Takes a summary of what you found and what the task needs.",
9070
+ promptSnippet: "Fusion only: hand off to the planning model after read-only exploration",
9071
+ exposure: "model-only",
9072
+ parameters: _Object_({ summary: String2({ description: "What you found and what the task needs" }) }),
9073
+ async execute() {
9074
+ return { content: [{ type: "text", text: FUSION_PLAN_RESULT }], details: {} };
9075
+ }
9076
+ });
9077
+ const isFusion = isFusionModel;
9078
+ pi.on("before_agent_start", (event, ctx) => addFusionGuideline(event.systemPromptOptions, ctx.model));
9079
+ pi.on("tool_call", (event, ctx) => {
9080
+ if (!isFusion(ctx.model) || !EDIT_TOOLS.has(event.toolName))
9081
+ return;
9082
+ const messages = branchMessages(ctx);
9083
+ if (plannerMadeTheCall(messages, ids.planner))
9084
+ return { block: true, reason: "Planner: record the plan with todo only; the executor makes the changes." };
9085
+ if (!handedOffThisTurn(messages)) {
9086
+ return { block: true, reason: `Blocked: call ${FUSION_PLAN_TOOL} first (summarize what you found); a planning model writes the plan before any edit.` };
9087
+ }
9088
+ });
9089
+ pi.on("agent_before_settle", (event, ctx) => isFusion(ctx.model) && event.outcome === "completed" && !alreadyContinued(ctx) && plannerStalled(branchMessages(ctx), ids.planner) ? STALL_CONTINUATION : undefined);
9090
+ for (const id of [FUSION_PICKER_ID, FUSION_ID]) {
9091
+ pi.registerVirtualModel({
9092
+ provider: PROVIDER_NAME,
9093
+ id,
9094
+ name,
9095
+ input: ["text"],
9096
+ route: (request, ctx) => routeFusion(request, {
9097
+ planner: ctx.modelRegistry.find(PROVIDER_NAME, ids.planner),
9098
+ executor: ctx.modelRegistry.find(PROVIDER_NAME, ids.executor),
9099
+ defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
9100
+ })
9101
+ });
9102
+ }
9103
+ }
9104
+ };
9105
+ }
9106
+
9107
+ // src/error-notice.ts
9108
+ var UPSTREAM_ERROR_FRAME_MESSAGE = "upstream returned an error frame";
9109
+ var PROVIDER_FAILING_NOTICE = "This model's provider is failing right now. Try again in a moment, or switch models with /model.";
9110
+ var FREE_OFFER_TITLE = "Switch to the free model and resend this prompt?";
9111
+ var FREE_OFFER_ACCEPT = "Switch and resend";
9112
+ var FREE_OFFER_DECLINE = "Stay on this model";
9113
+ var FREE_OFFER_SWITCHED = "Switched to the free model. Sending your prompt again.";
9114
+ var NOTICE_DEDUPE_MS = 15000;
9115
+ function optionalNumber(value) {
9116
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
9117
+ }
9118
+ function optionalString(value) {
9119
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
9120
+ }
9121
+ function parseErrorEnvelope(errorMessage) {
9122
+ if (typeof errorMessage !== "string")
9123
+ return;
9124
+ const m = /^(\d{3}): (\{[\s\S]*\})\s*$/.exec(errorMessage);
9125
+ if (!m)
9126
+ return;
9127
+ try {
9128
+ const body = JSON.parse(m[2]);
9129
+ const message = body.message ?? body.error?.message;
9130
+ const code = body.code ?? body.error?.code;
9131
+ if (typeof message !== "string" || message.trim() === "")
9132
+ return;
9133
+ const parsed = {
9134
+ status: Number(m[1]),
9135
+ code: typeof code === "string" ? code : "unknown",
9136
+ message
9137
+ };
9138
+ const limit = optionalString(body.limit);
9139
+ const retryAfter = optionalNumber(body.retry_after_seconds);
9140
+ const clearsAt = optionalString(body.clears_at);
9141
+ const remaining = optionalNumber(body.remaining_credits_today);
9142
+ const allowance = optionalNumber(body.allowance_credits);
9143
+ if (limit)
9144
+ parsed.limit = limit;
9145
+ if (retryAfter !== undefined)
9146
+ parsed.retry_after_seconds = retryAfter;
9054
9147
  if (clearsAt)
9055
9148
  parsed.clears_at = clearsAt;
9056
9149
  if (remaining !== undefined)
@@ -9108,14 +9201,50 @@ var LIMIT_CODES = new Set([
9108
9201
  "reward_required"
9109
9202
  ]);
9110
9203
  var SERVER_MESSAGE_CODES = new Set(["reward_required", "premium_allowance", "model_at_capacity", "context_too_long_for_fallback", "context_too_long"]);
9204
+ var FREE_OFFER_CODES = new Set(["daily_cap", "queue_wait", "rolling_cap", "premium_allowance", "model_at_capacity", "model_requires_credits"]);
9205
+ var isFusion = (model) => model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
9206
+ function lastUserEntryId(ctx) {
9207
+ const branch = ctx.sessionManager.getBranch();
9208
+ for (let i = branch.length - 1;i >= 0; i--) {
9209
+ if (branch[i].type === "message" && branch[i].message?.role === "user")
9210
+ return branch[i].id;
9211
+ }
9212
+ return "";
9213
+ }
9111
9214
  function createErrorNoticeExtension(opts = {}) {
9112
9215
  const lastAt = new Map;
9113
9216
  const now = opts.now ?? Date.now;
9114
9217
  const timeZone = opts.timeZone;
9218
+ const offered = new Set;
9219
+ let lastTurnEntryId;
9220
+ let acceptedEntryId;
9221
+ async function offerFreeModel(pi, env, entryId, ctx) {
9222
+ if (!opts.freeModels || opts.terminal !== true || !FREE_OFFER_CODES.has(env.code) || ctx.mode !== "tui" || !ctx.hasUI || isFusion(ctx.model))
9223
+ return;
9224
+ const free = opts.freeModels();
9225
+ const target = free[0];
9226
+ if (!target || free.some((m) => m.provider === ctx.model?.provider && m.id === ctx.model?.id))
9227
+ return;
9228
+ const prompt = lastUserEntryId(ctx);
9229
+ if (offered.has(prompt))
9230
+ return;
9231
+ offered.add(prompt);
9232
+ const choice = await ctx.ui.select(FREE_OFFER_TITLE, [FREE_OFFER_ACCEPT, FREE_OFFER_DECLINE]).catch(() => {
9233
+ return;
9234
+ });
9235
+ if (choice !== FREE_OFFER_ACCEPT)
9236
+ return;
9237
+ const model = ctx.modelRegistry.find(target.provider, target.id);
9238
+ if (!model || !await pi.setModel(model))
9239
+ return;
9240
+ ctx.ui.notify(FREE_OFFER_SWITCHED, "info");
9241
+ acceptedEntryId = entryId;
9242
+ }
9115
9243
  return {
9116
9244
  name: "free-pi-error-notice",
9117
9245
  factory(pi) {
9118
- pi.on("turn_end", (event, ctx) => {
9246
+ pi.on("turn_end", async (event, ctx) => {
9247
+ lastTurnEntryId = event.messageEntryId;
9119
9248
  const msg = event.message;
9120
9249
  if (!msg || msg.stopReason !== "error")
9121
9250
  return;
@@ -9123,13 +9252,23 @@ function createErrorNoticeExtension(opts = {}) {
9123
9252
  if (!env)
9124
9253
  return;
9125
9254
  const t = now();
9126
- if (t - (lastAt.get(env.code) ?? -Infinity) < NOTICE_DEDUPE_MS)
9127
- return;
9128
- lastAt.set(env.code, t);
9129
- const extra = opts.extraNotice?.(env);
9130
- const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
9255
+ if (t - (lastAt.get(env.code) ?? -Infinity) >= NOTICE_DEDUPE_MS) {
9256
+ lastAt.set(env.code, t);
9257
+ const extra = opts.extraNotice?.(env);
9258
+ const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
9131
9259
  ${extra}` : "");
9132
- ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
9260
+ ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
9261
+ }
9262
+ await offerFreeModel(pi, env, event.messageEntryId, ctx);
9263
+ });
9264
+ if (!opts.freeModels)
9265
+ return;
9266
+ pi.on("agent_before_settle", (event) => {
9267
+ const entryId = acceptedEntryId;
9268
+ acceptedEntryId = undefined;
9269
+ if (!entryId || event.outcome !== "error" || entryId !== lastTurnEntryId)
9270
+ return;
9271
+ return { entries: [{ type: "context_edit", targetId: entryId, replacement: null }], continue: true };
9133
9272
  });
9134
9273
  }
9135
9274
  };
@@ -9533,31 +9672,84 @@ function registerBugAutocomplete(pi) {
9533
9672
  }
9534
9673
 
9535
9674
  // src/footer-align.ts
9536
- import { truncateToWidth as truncateToWidth5, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
9675
+ import { truncateToWidth as truncateToWidth6, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
9537
9676
  var MIN_GAP = 2;
9538
9677
  var MIN_CWD = 8;
9539
9678
  var LEADING_SGR = /^(?:\x1b\[[0-9;]*m)+/;
9540
- function mergeStatusIntoCwdLine(lines, width) {
9541
- if (lines.length < 3)
9542
- return lines;
9543
- const [cwd, stats] = lines;
9544
- const status = lines[lines.length - 1];
9545
- const statusW = visibleWidth2(status);
9679
+ var dim = (s) => `\x1B[2m${s}\x1B[22m`;
9680
+ function footerLines(lines, width, freeName) {
9681
+ return footerLinesRaw(lines, width, freeName).map((l) => visibleWidth3(l) > width ? truncateToWidth6(l, Math.max(0, width), "…") : l);
9682
+ }
9683
+ function footerLinesRaw(lines, width, freeName) {
9684
+ const cwd = lines[0] ?? "";
9685
+ const status = lines.length >= 3 ? lines[lines.length - 1].replace("credits left today", "credits left").replace(/([AP]M) [A-Z]{2,5}\b/, "$1") : "";
9686
+ const tag = freeName ? dim(`\uD83D\uDD25 ${freeName.toLowerCase()} · free`) : "";
9687
+ const right = status && tag ? `${status}${dim(" · ")}${tag}` : status || tag;
9688
+ if (!right)
9689
+ return [cwd];
9690
+ const rightW = visibleWidth3(right);
9546
9691
  let left = cwd;
9547
- if (visibleWidth2(left) + MIN_GAP + statusW > width) {
9548
- const room = width - MIN_GAP - statusW;
9692
+ if (visibleWidth3(left) + MIN_GAP + rightW > width) {
9693
+ const room = width - MIN_GAP - rightW;
9549
9694
  if (room < MIN_CWD)
9550
- return lines;
9695
+ return [cwd, right];
9551
9696
  const style = LEADING_SGR.exec(cwd)?.[0] ?? "";
9552
- left = truncateToWidth5(cwd, room, style ? `${style}...\x1B[0m` : "...");
9697
+ left = truncateToWidth6(cwd, room, style ? `${style}...\x1B[0m` : "...");
9553
9698
  }
9554
- const gap = " ".repeat(Math.max(MIN_GAP, width - visibleWidth2(left) - statusW));
9555
- return [left + gap + status, stats, ...lines.slice(2, -1)];
9699
+ return [left + " ".repeat(Math.max(MIN_GAP, width - visibleWidth3(left) - rightW)) + right];
9556
9700
  }
9557
- function installFooterRightAlign(mode) {
9701
+ function installFooterRightAlign(mode, freeName = () => {
9702
+ return;
9703
+ }) {
9558
9704
  const footer = mode.footer;
9559
9705
  const render = footer.render.bind(footer);
9560
- footer.render = (w) => mergeStatusIntoCwdLine(render(w), w);
9706
+ footer.render = (w) => footerLines(render(w), w, freeName());
9707
+ }
9708
+ var INPUT_PLACEHOLDER = 'Try "explain this repo" · /help for commands';
9709
+ function withPlaceholder(lines, width, empty) {
9710
+ if (!empty || lines.length < 3)
9711
+ return lines;
9712
+ const base = lines[1].replace(/ +$/, "");
9713
+ const room = Math.max(0, width - visibleWidth3(base) - 1);
9714
+ return [lines[0], `${base} ${dim(truncateToWidth6(INPUT_PLACEHOLDER, room, "…"))}`, ...lines.slice(2)];
9715
+ }
9716
+ function installEditorPlaceholder(mode) {
9717
+ const ed = mode.defaultEditor;
9718
+ const render = ed.render.bind(ed);
9719
+ ed.render = (w) => withPlaceholder(render(w), w, ed.getText() === "");
9720
+ }
9721
+ function bottomPadding(headerHeight, otherHeight, rows) {
9722
+ return Math.max(0, rows - headerHeight - otherHeight);
9723
+ }
9724
+ function installBottomPin(mode) {
9725
+ const m = mode;
9726
+ const render = m.headerContainer.render.bind(m.headerContainer);
9727
+ let measuring = false;
9728
+ m.headerContainer.render = (w) => {
9729
+ const lines = render(w);
9730
+ const rows = process.stdout.rows;
9731
+ if (!rows || measuring)
9732
+ return lines;
9733
+ measuring = true;
9734
+ try {
9735
+ return pad(lines, w, rows);
9736
+ } finally {
9737
+ measuring = false;
9738
+ }
9739
+ };
9740
+ const pad = (lines, w, rows) => {
9741
+ const below = [
9742
+ ...m.documentContainer.children.filter((c) => c !== m.headerContainer),
9743
+ ...["pendingMessagesContainer", "statusContainer", "widgetContainerAbove", "editorContainer", "widgetContainerBelow", "footerContainer"].map((k) => m[k])
9744
+ ];
9745
+ let other = 0;
9746
+ for (const c of below) {
9747
+ other += c.render(w).length;
9748
+ if (lines.length + other >= rows)
9749
+ return lines;
9750
+ }
9751
+ return [...lines, ...Array(bottomPadding(lines.length, other, rows)).fill("")];
9752
+ };
9561
9753
  }
9562
9754
 
9563
9755
  // src/guest-handoff.ts
@@ -9648,6 +9840,12 @@ function mergeAttribution(a, b) {
9648
9840
 
9649
9841
  // src/changelog.ts
9650
9842
  var CHANGELOG_HIGHLIGHTS = [
9843
+ {
9844
+ version: "0.6.15",
9845
+ highlights: [
9846
+ "A cleaner start: the screen clears and a one-line header shows the model. Commands are behind /help, and the status line shows your folder and credits."
9847
+ ]
9848
+ },
9651
9849
  {
9652
9850
  version: "0.6.14",
9653
9851
  highlights: [
@@ -10054,6 +10252,14 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
10054
10252
  throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `consent submission failed: HTTP ${res.status}`);
10055
10253
  }
10056
10254
  }
10255
+ function deviceReport() {
10256
+ try {
10257
+ const { timeZone, locale } = Intl.DateTimeFormat().resolvedOptions();
10258
+ return { ...timeZone ? { timezone: timeZone } : {}, ...locale ? { locale } : {} };
10259
+ } catch {
10260
+ return {};
10261
+ }
10262
+ }
10057
10263
 
10058
10264
  class GuestUnavailableError extends Error {
10059
10265
  reason;
@@ -10066,7 +10272,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
10066
10272
  const res = await postJsonRetry(baseUrl, "/auth/guest", {
10067
10273
  consent_version: consentVersion,
10068
10274
  ...attribution.signup_src ? { signup_src: attribution.signup_src } : {},
10069
- ...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
10275
+ ...attribution.referral_code ? { referral_code: attribution.referral_code } : {},
10276
+ ...deviceReport()
10070
10277
  });
10071
10278
  if (res.status === 403) {
10072
10279
  const body = await readRefusal(res);
@@ -10147,7 +10354,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
10147
10354
  res = await doFetch(new URL(opts.endpoint ?? "/auth/token", baseUrl), {
10148
10355
  method: "POST",
10149
10356
  headers,
10150
- body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {} }),
10357
+ body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {}, ...deviceReport() }),
10151
10358
  signal: opts.signal
10152
10359
  });
10153
10360
  consecutiveNetworkFailures = 0;
@@ -10212,9 +10419,13 @@ async function checkToken(baseUrl, jwt) {
10212
10419
  const v = body?.consent_version;
10213
10420
  return typeof v === "string" || v === null ? v : undefined;
10214
10421
  }
10215
- async function fetchMe2(baseUrl, jwt) {
10422
+ async function fetchMe2(baseUrl, jwt, o = { env: process.env, configPath: getConfigPath() }) {
10423
+ const optedOut = o.env.FREEPI_NO_CHOICE === "1" || !readChoicePrompts(o.configPath);
10216
10424
  const res = await fetch(new URL("/me", baseUrl), {
10217
- headers: { authorization: `Bearer ${jwt}` }
10425
+ headers: {
10426
+ authorization: `Bearer ${jwt}`,
10427
+ ...optedOut ? {} : { "x-freepi-features": "choice-prompts" }
10428
+ }
10218
10429
  });
10219
10430
  if (res.status === 401)
10220
10431
  throw new UnauthorizedError;
@@ -10427,6 +10638,12 @@ function formatExitRefillCard(session, end, timeZone) {
10427
10638
  timeZone
10428
10639
  });
10429
10640
  }
10641
+ function suppressCardOnAllowanceMismatch(session, refusalAllowance) {
10642
+ if (!session.show || refusalAllowance === undefined || refusalAllowance === session.allowance)
10643
+ return false;
10644
+ session.show = false;
10645
+ return true;
10646
+ }
10430
10647
 
10431
10648
  // src/pending-login.ts
10432
10649
  import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
@@ -10949,12 +11166,16 @@ function updateAllowanceCache(input) {
10949
11166
  allowance: input.allowance_credits,
10950
11167
  refillAt: refill,
10951
11168
  bonusCredits: typeof input.bonus_credits === "number" ? input.bonus_credits : undefined,
11169
+ boughtCredits: typeof input.premium_bought_credits === "number" ? input.premium_bought_credits : undefined,
10952
11170
  guest: input.guest === true,
10953
11171
  visible: input.visible_allowance === true,
10954
11172
  ...input.credit_tiers === true && typeof input.premium_credits === "number" ? { premium: input.premium_credits, ...typeof input.regular_wallet_credits === "number" ? { regularWallet: input.regular_wallet_credits } : {}, ...typeof input.premium_get_line === "string" ? { getLine: input.premium_get_line } : {} } : {}
10955
11173
  };
10956
11174
  return cached2;
10957
11175
  }
11176
+ function getAllowanceCache() {
11177
+ return cached2;
11178
+ }
10958
11179
  function allowanceLine(snapshot = cached2, timeZone) {
10959
11180
  if (snapshot && !snapshot.guest && typeof snapshot.premium === "number") {
10960
11181
  return formatTieredModelStatus({ remaining: snapshot.remaining, regularWallet: snapshot.regularWallet, premium: snapshot.premium, getLine: snapshot.getLine });
@@ -11431,302 +11652,345 @@ function createFreePiCommandsExtension(opts) {
11431
11652
  }
11432
11653
 
11433
11654
  // src/header.ts
11434
- var ALL_COMMANDS = [
11435
- { name: "/login", description: "sign in with GitHub for 15,000 credits/day" },
11436
- { name: "/usage", description: "credits left today and refill time" },
11437
- { name: "/import", description: "continue a Claude Code or Codex session" },
11438
- { name: "/hive", description: "share this session with friends" },
11439
- { name: "/support", description: "visit today's advertiser" },
11440
- { name: "/tos", description: "terms of service" },
11441
- { name: "/privacy-policy", description: "privacy policy" },
11442
- { name: "/buy-credits", description: "get more usage" },
11443
- { name: "/close-other-session", description: "free a stuck session on another machine" },
11444
- { name: "/invite", description: "invite a friend, you both get +5k credits/day" },
11445
- { name: "/referrals", description: "your referral status" },
11446
- { name: "/redeem", description: "redeem a credit code" },
11447
- { name: "/update", description: "get the latest free-pi" }
11448
- ];
11449
- var commands = () => ALL_COMMANDS.filter((c) => c.name !== "/hive" || process.env.FREEPI_HIVE === "1");
11450
- var NAME_COLUMN = 24;
11451
- var WELCOME_LINE = "Welcome to Free Pi, ad-supported inference. Please visit our advertisers to support us.";
11452
- var FREE_LABEL = "Free Unlimited: limited time";
11453
- var PARTICLE_LINE = `${FREE_LABEL} — doesn't use any credits.`;
11454
- var GUEST_LINE = "Guest trial — sign in with GitHub (/login) to unlock daily credits and a comeback bonus.";
11455
- var CONSENT_LINE_1 = "Usage is funded by ads and training. By using free-pi you consent.";
11456
- var CONSENT_LINE_2 = "See /tos and /privacy-policy.";
11457
- var EXPANDED_HINT_LINE = "esc interrupt · ctrl+c clear / exit · / commands · ! bash";
11458
- var UPDATE_BORDER_CHAR = "─";
11459
- var UPDATE_MAX_WIDTH = 100;
11460
- function updateBannerLines(theme, latest, width) {
11461
- const headline = "Update Available";
11462
- const detail = `New version ${latest} is available. Run /update`;
11463
- const inner = Math.max(0, Math.min(width - 1, UPDATE_MAX_WIDTH));
11464
- if (inner <= 0)
11465
- return [];
11466
- const border = theme.fg("warning", UPDATE_BORDER_CHAR.repeat(inner));
11467
- return [
11468
- "",
11469
- border,
11470
- theme.bold(theme.fg("warning", truncate2(headline, inner))),
11471
- theme.fg("dim", truncate2(detail, inner)),
11472
- border
11473
- ];
11474
- }
11475
- function truncate2(text, maxWidth) {
11476
- if (maxWidth <= 0)
11477
- return "";
11478
- if (text.length <= maxWidth)
11479
- return text;
11480
- if (maxWidth <= 1)
11481
- return text.slice(0, maxWidth);
11482
- return `${text.slice(0, maxWidth - 1)}…`;
11483
- }
11484
- function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest, guest, allowanceLine, particleFree) {
11485
- const inner = width - 1;
11486
- const fit = (plain, styled) => plain.length <= inner ? styled() : theme.fg("dim", truncate2(plain, inner));
11487
- const titleRest = ` · ${modelName} · ctrl+o for help`;
11488
- const title = fit(`free-pi${titleRest}`, () => `${theme.bold(theme.fg("accent", "free-pi"))}${theme.fg("dim", titleRest)}`);
11489
- const commandLines = commands().map(({ name, description }) => {
11490
- const padded = ` ${name}`.padEnd(NAME_COLUMN, " ");
11491
- return fit(`${padded}${description}`, () => `${padded}${theme.fg("dim", description)}`);
11492
- });
11493
- const dim = (plain) => fit(plain, () => theme.fg("dim", plain));
11494
- const lines = [
11495
- "",
11496
- title,
11497
- "",
11498
- dim(WELCOME_LINE),
11499
- ...particleFree ? [dim(PARTICLE_LINE)] : [],
11500
- ...guest ? [dim(GUEST_LINE)] : [],
11501
- ...allowanceLine ? [dim(allowanceLine)] : [],
11502
- "",
11503
- dim(CONSENT_LINE_1),
11504
- dim(CONSENT_LINE_2),
11505
- "",
11506
- ...commandLines,
11507
- ""
11508
- ];
11509
- if (expanded)
11510
- lines.push(dim(EXPANDED_HINT_LINE));
11511
- if (updateLatest)
11512
- lines.push(...updateBannerLines(theme, updateLatest, width));
11513
- return lines.map((line) => line === "" ? line : ` ${line}`);
11514
- }
11515
- function createHeaderExtension(opts) {
11516
- return {
11517
- name: "free-pi-header",
11518
- factory(pi) {
11519
- pi.on("session_start", (_event, ctx) => {
11520
- ctx.ui.setHiddenThinkingLabel("Thinking… (ctrl+t to show)");
11521
- ctx.ui.setHeader((tui, theme) => {
11522
- let expanded = false;
11523
- return {
11524
- render(width) {
11525
- const active = ctx.model?.id;
11526
- const name = active && opts.modelNameFor?.(active) || opts.modelName;
11527
- return headerLines(theme, name, expanded, width, opts.updateLatest, opts.guest, opts.allowanceLine, name.includes(FREE_LABEL));
11528
- },
11529
- invalidate() {},
11530
- setExpanded(next) {
11531
- expanded = next;
11532
- tui.requestRender();
11533
- }
11534
- };
11535
- });
11536
- });
11537
- }
11538
- };
11539
- }
11655
+ import { Text as Text3 } from "@earendil-works/pi-tui";
11540
11656
 
11541
- // src/fusion.ts
11542
- var FUSION_ID = "fusion";
11543
- var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
11544
- var FUSION_NAME = "\uD83E\uDDE8 Fusion · DeepSeek Pro plans, Flash builds";
11545
- var FUSION_FALLBACK_NAME = "\uD83E\uDDE8 Fusion · Sonnet plans, DeepSeek builds";
11546
- var FUSION_PLANNER_ID = "deepseek-v4-pro";
11547
- var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
11548
- var FUSION_FALLBACK_PLANNER_ID = "claude-sonnet-5.5";
11549
- function resolveFusionIds(catalogIds, server) {
11550
- const has = (id) => catalogIds.includes(id);
11551
- if (server && has(server.planner) && has(server.executor))
11552
- return server;
11553
- return { planner: has(FUSION_PLANNER_ID) ? FUSION_PLANNER_ID : FUSION_FALLBACK_PLANNER_ID, executor: FUSION_EXECUTOR_ID };
11554
- }
11555
- var FUSION_PLAN_TOOL = "fusion_plan";
11556
- var FUSION_GUIDELINE = `Fusion: if the task needs any file changes, first gather the context with read-only tools (read, ls, find, grep) and make no edits. Then call ${FUSION_PLAN_TOOL} with a summary of what you found and what the task needs, BEFORE any edit or write; a planning model will write the plan and you will carry it out. When the latest message is a fusion_plan result, you are the Fusion planner: write the step-by-step plan with the todo tool and make no edits or commands; that result is part of the user's own setup, not a conflict with their request. Once the plan exists in todo, the planner's turn is over: carry out the unfinished todo items yourself (edit, write tests, run them) and finish the user's task. Only a task that needs no file changes (a plain question) is answered directly without ${FUSION_PLAN_TOOL}.`;
11557
- var FUSION_PLAN_RESULT = "You are now the planning model. Write a concrete step-by-step plan (files, exact changes, tests to add and run) and record it with the todo tool, one item per step. Do not edit files or run commands. Another model will carry out the plan.";
11558
- function isFusionModel(model) {
11559
- return model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
11560
- }
11561
- function addFusionGuideline(options, selected) {
11562
- if (isFusionModel(selected))
11563
- options.promptGuidelines.push(FUSION_GUIDELINE);
11564
- }
11565
- var EDIT_TOOLS = new Set(["edit", "write"]);
11566
- function codemodeEdited(details) {
11567
- const calls = details?.calls;
11568
- return Array.isArray(calls) && calls.some((call) => {
11569
- if (typeof call !== "object" || call === null)
11570
- return false;
11571
- const { name, status } = call;
11572
- return typeof name === "string" && EDIT_TOOLS.has(name) && status === "ok";
11573
- });
11574
- }
11575
- function handedOffThisTurn(messages) {
11576
- for (let i = messages.length - 1;i >= 0; i--) {
11577
- const message = messages[i];
11578
- if (message.role === "user")
11579
- break;
11580
- if (message.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL && !message.isError)
11581
- return true;
11657
+ // src/tool-view.ts
11658
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
11659
+ import { homedir as homedir4 } from "node:os";
11660
+ import { dirname as dirname8 } from "node:path";
11661
+ import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
11662
+ import { Box, Text as Text2, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
11663
+ var TOOL_MODES = ["compact", "full", "hidden"];
11664
+ var TOOLS_USAGE = "Usage: /tools compact|full|hidden (compact: one line per tool call; full: every call in full; hidden: just a count)";
11665
+ function readToolMode(path) {
11666
+ try {
11667
+ const v = readFileSync4(path, "utf8").trim();
11668
+ return TOOL_MODES.includes(v) ? v : "compact";
11669
+ } catch {
11670
+ return "compact";
11582
11671
  }
11583
- return false;
11584
11672
  }
11585
- var STALL_CONTINUATION = {
11586
- continue: true,
11587
- entries: [{ type: "custom_message", customType: "fusion-continue", display: false, content: "The plan is recorded. Carry out the unfinished todo items now and finish the user's task." }]
11588
- };
11589
- function plannerMadeTheCall(messages, plannerId) {
11590
- for (let i = messages.length - 1;i >= 0; i--) {
11591
- const message = messages[i];
11592
- if (message.role === "assistant")
11593
- return message.provider === PROVIDER_NAME && message.model === plannerId;
11594
- }
11595
- return false;
11673
+ function writeToolMode(path, mode) {
11674
+ try {
11675
+ mkdirSync2(dirname8(path), { recursive: true });
11676
+ writeFileSync2(path, mode);
11677
+ } catch {}
11596
11678
  }
11597
- function branchMessages(ctx) {
11598
- return ctx.sessionManager.getBranch().flatMap((entry) => entry.type === "message" ? [entry.message] : []);
11679
+ var MAX_ARG = 70;
11680
+ function outputText(r) {
11681
+ return (r?.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
11682
+ `);
11599
11683
  }
11600
- function alreadyContinued(ctx) {
11601
- const branch = ctx.sessionManager.getBranch();
11602
- for (let i = branch.length - 1;i >= 0; i--) {
11603
- const entry = branch[i];
11604
- if (entry.type === "custom_message" && entry.customType === STALL_CONTINUATION.entries[0].customType)
11605
- return true;
11606
- const message = entry.type === "message" ? entry.message : undefined;
11607
- if (message?.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL)
11608
- return false;
11684
+ function oneLine(text, max = MAX_ARG) {
11685
+ const flat = text.replace(/\s+/g, " ").trim();
11686
+ const multi = text.trim().includes(`
11687
+ `);
11688
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : multi ? `${flat} …` : flat;
11689
+ }
11690
+ function shortPath(p) {
11691
+ const home = homedir4();
11692
+ return p === home ? "~" : p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
11693
+ }
11694
+ var str = (v) => typeof v === "string" ? v : "";
11695
+ function callSummary(name, args) {
11696
+ const a = args ?? {};
11697
+ switch (name) {
11698
+ case "bash":
11699
+ return oneLine(str(a.command));
11700
+ case "read":
11701
+ case "edit":
11702
+ case "write":
11703
+ case "ls":
11704
+ return shortPath(str(a.path) || str(a.file_path));
11705
+ case "grep":
11706
+ case "find":
11707
+ return oneLine([str(a.pattern), a.path ? `in ${shortPath(str(a.path))}` : ""].filter(Boolean).join(" "));
11708
+ default: {
11709
+ const first = Object.values(a).find((v) => typeof v === "string" && v !== "");
11710
+ return first ? oneLine(first) : "";
11711
+ }
11712
+ }
11713
+ }
11714
+ var lineCount = (text) => text.trim() === "" ? 0 : text.replace(/\n+$/, "").split(`
11715
+ `).length;
11716
+ var plural2 = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
11717
+ function doneNote(name, args, result, ms) {
11718
+ const parts = [];
11719
+ if (name === "bash" || name === "grep" || name === "find" || name === "ls") {
11720
+ const n = lineCount(outputText(result));
11721
+ parts.push(n === 0 ? "no output" : plural2(n, "line"));
11722
+ } else if (name === "edit") {
11723
+ const diff = String(result?.details?.diff ?? "");
11724
+ const lines = diff.split(`
11725
+ `);
11726
+ const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
11727
+ const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
11728
+ if (diff)
11729
+ parts.push(`+${added} −${removed}`);
11730
+ } else if (name === "write") {
11731
+ parts.push(plural2(lineCount(str(args?.content)), "line"));
11732
+ }
11733
+ if (ms !== undefined && name === "bash")
11734
+ parts.push(`${(ms / 1000).toFixed(1)}s`);
11735
+ return parts.length ? ` (${parts.join(" · ")})` : "";
11736
+ }
11737
+ function toolLines(i) {
11738
+ const label = i.name === "bash" ? "Bash" : i.name.charAt(0).toUpperCase() + i.name.slice(1);
11739
+ const what = callSummary(i.name, i.args);
11740
+ const head = `${label}${what ? ` ${what}` : ""}`;
11741
+ if (!i.result)
11742
+ return [{ text: `● ${head} …`, error: false }];
11743
+ if (i.isError) {
11744
+ const out = outputText(i.result);
11745
+ const code = /exited with code (\d+)/.exec(out)?.[1];
11746
+ const first = out.split(`
11747
+ `).map((l) => l.trim()).find((l) => l !== "" && !/^Command exited/.test(l));
11748
+ return [
11749
+ { text: `✗ ${head}${code ? ` exit ${code}` : " failed"}`, error: true },
11750
+ ...first ? [{ text: ` ${oneLine(first, 100)}`, error: true }] : []
11751
+ ];
11609
11752
  }
11610
- return false;
11611
- }
11612
- function plannerStalled(messages, plannerId) {
11613
- const last = messages[messages.length - 1];
11614
- if (!last || last.role !== "assistant" || last.provider !== PROVIDER_NAME || last.model !== plannerId)
11615
- return false;
11616
- if (last.stopReason !== "stop")
11617
- return false;
11618
- return handedOffThisTurn(messages) && !last.content.some((part) => part.type === "toolCall");
11619
- }
11620
- function editedThisTurn(messages) {
11621
- for (let i = messages.length - 1;i >= 0; i--) {
11622
- const message = messages[i];
11623
- if (message.role === "user")
11624
- break;
11625
- if (message.role !== "toolResult")
11626
- continue;
11627
- if (message.toolName === "codemode" ? codemodeEdited(message.details) : EDIT_TOOLS.has(message.toolName) && !message.isError) {
11628
- return true;
11629
- }
11753
+ const done = [{ text: `● ${head}${doneNote(i.name, i.args, i.result, i.durationMs)}`, error: false, raw: false }];
11754
+ const out = outputText(i.result);
11755
+ if (/https?:\/\//.test(out)) {
11756
+ const urlRows = out.split(`
11757
+ `).filter((l) => /https?:\/\//.test(l)).slice(0, 3);
11758
+ return [...done, ...urlRows.map((l) => ({ text: ` ${l.trim()}`, error: false, raw: true }))];
11630
11759
  }
11631
- return false;
11760
+ return done;
11632
11761
  }
11633
- function hasNewUserInput(request) {
11634
- const failedAt = request.failed?.message.timestamp;
11635
- if (failedAt === undefined)
11636
- return false;
11637
- for (let i = request.messages.length - 1;i >= 0; i--) {
11638
- const message = request.messages[i];
11639
- if (message.role === "assistant")
11640
- return false;
11641
- if (message.role === "user")
11642
- return message.timestamp > failedAt;
11643
- }
11644
- return false;
11762
+ function component(rows, theme, pad) {
11763
+ return {
11764
+ render: (w) => rows.flatMap((r) => {
11765
+ const room = Math.max(1, w - pad);
11766
+ const pieces = r.raw ? Array.from(r.text).join("").match(new RegExp(`.{1,${room}}`, "gu")) ?? [""] : [truncateToWidth7(r.text, room, "…")];
11767
+ return pieces.map((t) => " ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t));
11768
+ }),
11769
+ invalidate() {}
11770
+ };
11645
11771
  }
11646
- function routeFusion(request, models) {
11647
- const phase = request.state?.phase ?? "explore";
11648
- const restart = request.reason === "user" || request.reason === "retry" && hasNewUserInput(request);
11649
- const route = (model, nextPhase = phase) => ({
11650
- model,
11651
- thinkingLevel: request.thinkingLevel,
11652
- state: request.reason === "direct" ? undefined : request.state?.phase === nextPhase ? request.state : { phase: nextPhase }
11772
+ var empty = { render: () => [], invalidate() {} };
11773
+ var toolCountText = (n) => `· ${plural2(n, "tool call")}`;
11774
+ function registerToolView(pi, modePath) {
11775
+ let mode = readToolMode(modePath);
11776
+ let count = 0;
11777
+ const redraws = new Map;
11778
+ pi.registerToolRenderer((toolName, next) => {
11779
+ const base = next();
11780
+ const ownShell = base?.renderShell === "self";
11781
+ const frame = (c, theme, ctx) => {
11782
+ if (ownShell)
11783
+ return c;
11784
+ const bg = ctx.isPartial ? "toolPendingBg" : ctx.isError ? "toolErrorBg" : "toolSuccessBg";
11785
+ const box = new Box(ctx.outputPad, 0, (t) => theme.bg(bg, t));
11786
+ box.addChild(c);
11787
+ return box;
11788
+ };
11789
+ return {
11790
+ renderShell: "self",
11791
+ renderCall: (args, theme, ctx) => {
11792
+ redraws.set(ctx.toolCallId, ctx.invalidate);
11793
+ const a = args;
11794
+ if (mode === "full" || ctx.expanded) {
11795
+ return frame(base?.renderCall ? base.renderCall(args, theme, ctx) : new Text2(theme.fg("accent", `${toolName} ${callSummary(toolName, a)}`), 0, 0), theme, ctx);
11796
+ }
11797
+ if (!ctx.isPartial)
11798
+ return empty;
11799
+ return mode === "hidden" ? empty : component(toolLines({ name: toolName, args: a }), theme, ctx.outputPad);
11800
+ },
11801
+ renderResult: (result, options, theme, ctx) => {
11802
+ redraws.set(ctx.toolCallId, ctx.invalidate);
11803
+ if (mode === "full" || options.expanded) {
11804
+ if (base?.renderResult)
11805
+ return frame(base.renderResult(result, options, theme, ctx), theme, ctx);
11806
+ const all = outputText(result).split(`
11807
+ `);
11808
+ const cap = options.expanded ? all.length : 12;
11809
+ const more = all.length - cap;
11810
+ const text = [...all.slice(0, cap).map((l) => theme.fg("toolOutput", l)), ...more > 0 ? [theme.fg("muted", `... (${more} more lines, ctrl+o to expand)`)] : []].join(`
11811
+ `);
11812
+ return frame(new Text2(text, 0, 0), theme, ctx);
11813
+ }
11814
+ if (options.isPartial)
11815
+ return empty;
11816
+ if (mode === "hidden" && !ctx.isError && !/https?:\/\//.test(outputText(result)))
11817
+ return empty;
11818
+ const rows = toolLines({ name: toolName, args: ctx.args, result, isError: ctx.isError, durationMs: ctx.durationMs });
11819
+ return component(rows, theme, ctx.outputPad);
11820
+ }
11821
+ };
11653
11822
  });
11654
- if (!models.planner || !models.executor) {
11655
- return route(models.defaultModel, restart ? "explore" : phase);
11656
- }
11657
- switch (request.reason) {
11658
- case "user":
11659
- return route(models.executor, "explore");
11660
- case "direct":
11661
- return route(models.executor);
11662
- case "retry": {
11663
- if (restart)
11664
- return route(models.executor, "explore");
11665
- const failed = request.failed;
11666
- if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === models.planner?.id) {
11667
- return route(models.executor, "build");
11823
+ const proto = ToolExecutionComponent.prototype;
11824
+ const render = proto.render;
11825
+ if (!render.__freepiCompact) {
11826
+ const wrapped = function(w) {
11827
+ const lines = render.call(this, w);
11828
+ return mode !== "full" && !this.expanded && lines[0] === "" ? lines.slice(1) : lines;
11829
+ };
11830
+ wrapped.__freepiCompact = true;
11831
+ proto.render = wrapped;
11832
+ }
11833
+ const show = (ctx) => ctx.ui.setWidget("freepi-toolcount", mode === "hidden" && count > 0 ? [toolCountText(count)] : undefined);
11834
+ pi.on("agent_start", (_e, ctx) => {
11835
+ count = 0;
11836
+ show(ctx);
11837
+ });
11838
+ pi.on("tool_result", (_e, ctx) => {
11839
+ count++;
11840
+ show(ctx);
11841
+ });
11842
+ pi.registerCommand("tools", {
11843
+ description: "How tool calls are shown: compact (one line each), full, or hidden (a count).",
11844
+ handler: async (args, ctx) => {
11845
+ const want = args.trim().toLowerCase();
11846
+ if (!want)
11847
+ return ctx.ui.notify(`Tool calls are shown ${mode}. ${TOOLS_USAGE}`, "info");
11848
+ if (!TOOL_MODES.includes(want))
11849
+ return ctx.ui.notify(TOOLS_USAGE, "warning");
11850
+ mode = want;
11851
+ writeToolMode(modePath, mode);
11852
+ for (const redraw of redraws.values()) {
11853
+ try {
11854
+ redraw();
11855
+ } catch {}
11668
11856
  }
11669
- return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
11857
+ show(ctx);
11858
+ ctx.ui.notify(`Tool calls are now ${mode}${mode === "compact" ? " (ctrl+o expands one)" : ""}.`, "info");
11859
+ }
11860
+ });
11861
+ return { getMode: () => mode, setMode: (m) => mode = m, redraws };
11862
+ }
11863
+
11864
+ // src/header.ts
11865
+ var ALL_COMMANDS = [
11866
+ { name: "/login", description: "sign in with GitHub for 15,000 credits/day" },
11867
+ { name: "/usage", description: "credits left today and refill time" },
11868
+ { name: "/import", description: "continue a Claude Code or Codex session" },
11869
+ { name: "/hive", description: "share this session with friends" },
11870
+ { name: "/support", description: "visit today's advertiser" },
11871
+ { name: "/tos", description: "terms of service" },
11872
+ { name: "/privacy-policy", description: "privacy policy" },
11873
+ { name: "/buy-credits", description: "get more usage" },
11874
+ { name: "/close-other-session", description: "free a stuck session on another machine" },
11875
+ { name: "/invite", description: "invite a friend, you both get +5k credits/day" },
11876
+ { name: "/referrals", description: "your referral status" },
11877
+ { name: "/redeem", description: "redeem a credit code" },
11878
+ { name: "/update", description: "get the latest free-pi" },
11879
+ { name: "/tools", description: "tool calls: compact (default), full, or hidden" }
11880
+ ];
11881
+ var commands = () => ALL_COMMANDS.filter((c) => c.name !== "/hive" || process.env.FREEPI_HIVE === "1");
11882
+ var NAME_COLUMN = 22;
11883
+ var FREE_LABEL = "Free Unlimited: limited time";
11884
+ var STARTUP_NOTES_WIDGET = "freepi-startup-notes";
11885
+ var COMPACTION_NOTE = "◇ Long conversation: earlier steps were summarized to keep replies fast. Some models summarize sooner than others.";
11886
+ var PI_BUILTINS = [
11887
+ { name: "/new", description: "start a new session" },
11888
+ { name: "/resume", description: "resume a different session" },
11889
+ { name: "/compact", description: "summarize the conversation to free up room" },
11890
+ { name: "/logout", description: "sign out" },
11891
+ { name: "/quit", description: "quit free-pi" }
11892
+ ];
11893
+ function helpLines(registered = []) {
11894
+ const rows = [
11895
+ { name: "/model", description: "switch models (\uD83D\uDD25 = free, no credits)" },
11896
+ ...commands(),
11897
+ ...PI_BUILTINS,
11898
+ { name: "/help", description: "show this list" }
11899
+ ];
11900
+ const seen = new Set(rows.map((r) => r.name));
11901
+ for (const c of registered) {
11902
+ const name = c.name.startsWith("/") ? c.name : `/${c.name}`;
11903
+ if (name === "/hive" && process.env.FREEPI_HIVE !== "1")
11904
+ continue;
11905
+ if (!seen.has(name)) {
11906
+ seen.add(name);
11907
+ rows.push({ name, description: c.description ?? "" });
11670
11908
  }
11671
- case "continuation":
11672
- if (phase !== "explore")
11673
- return route(models.executor, "build");
11674
- if (editedThisTurn(request.messages))
11675
- return route(models.executor, "build");
11676
- return handedOffThisTurn(request.messages) ? route(models.planner, "plan") : route(models.executor, "explore");
11677
11909
  }
11910
+ return rows.map(({ name, description }) => (name.length >= NAME_COLUMN ? `${name} ` : name.padEnd(NAME_COLUMN, " ")) + description);
11678
11911
  }
11679
- function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
11680
- 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";
11912
+ function freeModelBase(pickerName) {
11913
+ if (!pickerName?.includes(FREE_LABEL))
11914
+ return;
11915
+ return pickerName.split(" · ")[0].trim() || undefined;
11916
+ }
11917
+ var UPDATE_BORDER_CHAR = "─";
11918
+ var UPDATE_MAX_WIDTH = 100;
11919
+ function updateBannerLines(theme, latest, width) {
11920
+ const headline = "Update Available";
11921
+ const detail = `New version ${latest} is available. Run /update`;
11922
+ const inner = Math.max(0, Math.min(width - 1, UPDATE_MAX_WIDTH));
11923
+ if (inner <= 0)
11924
+ return [];
11925
+ const border = theme.fg("warning", UPDATE_BORDER_CHAR.repeat(inner));
11926
+ return [
11927
+ "",
11928
+ border,
11929
+ theme.bold(theme.fg("warning", truncate2(headline, inner))),
11930
+ theme.fg("dim", truncate2(detail, inner)),
11931
+ border
11932
+ ];
11933
+ }
11934
+ function truncate2(text, maxWidth) {
11935
+ if (maxWidth <= 0)
11936
+ return "";
11937
+ if (text.length <= maxWidth)
11938
+ return text;
11939
+ if (maxWidth <= 1)
11940
+ return text.slice(0, maxWidth);
11941
+ return `${text.slice(0, maxWidth - 1)}…`;
11942
+ }
11943
+ function headerLines(theme, modelName, width = Number.POSITIVE_INFINITY, version = CLI_VERSION, updateLatest) {
11944
+ const inner = width - 1;
11945
+ const free = freeModelBase(modelName);
11946
+ const mark = "›π";
11947
+ const head = `${mark} free-pi v${version} · `;
11948
+ const plain = free ? `${head}\uD83D\uDD25 ${free} · ${FREE_LABEL}` : `${head}${modelName}`;
11949
+ const line = plain.length > inner ? theme.fg("dim", truncate2(plain, inner)) : `${theme.fg("accent", mark)}${plain.slice(mark.length)}`.replace(FREE_LABEL, free ? theme.fg("warning", FREE_LABEL) : FREE_LABEL);
11950
+ const lines = [line];
11951
+ if (updateLatest)
11952
+ lines.push(...updateBannerLines(theme, updateLatest, width));
11953
+ return lines.map((l) => l === "" ? l : ` ${l}`);
11954
+ }
11955
+ function createHeaderExtension(opts) {
11681
11956
  return {
11682
- name: "free-pi-fusion",
11683
- factory: (pi) => {
11684
- const syncPlanTool = (selected) => {
11685
- const on = isFusionModel(selected);
11686
- const active = pi.getActiveTools();
11687
- if (on === active.includes(FUSION_PLAN_TOOL))
11688
- return;
11689
- pi.setActiveTools(on ? [...active, FUSION_PLAN_TOOL] : active.filter((name) => name !== FUSION_PLAN_TOOL));
11690
- };
11691
- pi.on("session_start", (_event, ctx) => syncPlanTool(ctx.model));
11692
- pi.on("model_select", (event) => syncPlanTool(event.model));
11693
- pi.registerTool({
11694
- name: FUSION_PLAN_TOOL,
11695
- label: "Fusion plan",
11696
- description: "Hand off to the planning model once you have gathered enough context. Takes a summary of what you found and what the task needs.",
11697
- promptSnippet: "Fusion only: hand off to the planning model after read-only exploration",
11698
- exposure: "model-only",
11699
- parameters: _Object_({ summary: String2({ description: "What you found and what the task needs" }) }),
11700
- async execute() {
11701
- return { content: [{ type: "text", text: FUSION_PLAN_RESULT }], details: {} };
11702
- }
11957
+ name: "free-pi-header",
11958
+ factory(pi) {
11959
+ if (opts.toolModePath)
11960
+ registerToolView(pi, opts.toolModePath);
11961
+ pi.registerCommand("help", {
11962
+ description: "List every free-pi command.",
11963
+ handler: async (_args, ctx) => ctx.ui.notify(helpLines(pi.getCommands().filter((c) => c.source === "extension")).join(`
11964
+ `), "info")
11703
11965
  });
11704
- const isFusion = isFusionModel;
11705
- pi.on("before_agent_start", (event, ctx) => addFusionGuideline(event.systemPromptOptions, ctx.model));
11706
- pi.on("tool_call", (event, ctx) => {
11707
- if (!isFusion(ctx.model) || !EDIT_TOOLS.has(event.toolName))
11966
+ let noteTimer;
11967
+ pi.on("session_compact", (event, ctx) => {
11968
+ if (event.reason === "manual")
11708
11969
  return;
11709
- const messages = branchMessages(ctx);
11710
- if (plannerMadeTheCall(messages, ids.planner))
11711
- return { block: true, reason: "Planner: record the plan with todo only; the executor makes the changes." };
11712
- if (!handedOffThisTurn(messages)) {
11713
- return { block: true, reason: `Blocked: call ${FUSION_PLAN_TOOL} first (summarize what you found); a planning model writes the plan before any edit.` };
11970
+ clearTimeout(noteTimer);
11971
+ noteTimer = setTimeout(() => ctx.ui.notify(COMPACTION_NOTE, "info"), 250);
11972
+ });
11973
+ pi.on("session_shutdown", () => clearTimeout(noteTimer));
11974
+ pi.on("agent_start", (_e, ctx) => ctx.ui.setWidget(STARTUP_NOTES_WIDGET, undefined));
11975
+ let notes = opts.startupNotes;
11976
+ pi.on("session_start", (_event, ctx) => {
11977
+ clearTimeout(noteTimer);
11978
+ if (notes?.length) {
11979
+ const text = notes.map((n) => n.replace(/^\n+|\n+$/g, "")).join(`
11980
+ `);
11981
+ notes = undefined;
11982
+ ctx.ui.setWidget(STARTUP_NOTES_WIDGET, (_tui, theme) => new Text3(theme.fg("dim", text), 1, 0));
11714
11983
  }
11984
+ ctx.ui.setHiddenThinkingLabel("Thinking… (ctrl+t to show)");
11985
+ ctx.ui.setHeader((_tui, theme) => ({
11986
+ render(width) {
11987
+ const active = ctx.model?.id;
11988
+ const name = active && opts.modelNameFor?.(active) || opts.modelName;
11989
+ return headerLines(theme, name, width, CLI_VERSION, opts.updateLatest);
11990
+ },
11991
+ invalidate() {}
11992
+ }));
11715
11993
  });
11716
- pi.on("agent_before_settle", (event, ctx) => isFusion(ctx.model) && event.outcome === "completed" && !alreadyContinued(ctx) && plannerStalled(branchMessages(ctx), ids.planner) ? STALL_CONTINUATION : undefined);
11717
- for (const id of [FUSION_PICKER_ID, FUSION_ID]) {
11718
- pi.registerVirtualModel({
11719
- provider: PROVIDER_NAME,
11720
- id,
11721
- name,
11722
- input: ["text"],
11723
- route: (request, ctx) => routeFusion(request, {
11724
- planner: ctx.modelRegistry.find(PROVIDER_NAME, ids.planner),
11725
- executor: ctx.modelRegistry.find(PROVIDER_NAME, ids.executor),
11726
- defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
11727
- })
11728
- });
11729
- }
11730
11994
  }
11731
11995
  };
11732
11996
  }
@@ -11747,20 +12011,514 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
11747
12011
  // src/hive.ts
11748
12012
  import { createHash as createHash3 } from "node:crypto";
11749
12013
  import { execFileSync } from "node:child_process";
11750
- import { lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync as readlinkSync2 } from "node:fs";
12014
+ import { lstatSync as lstatSync2, readFileSync as readFileSync5, readlinkSync as readlinkSync2 } from "node:fs";
11751
12015
  import { join as join9 } from "node:path";
11752
12016
 
11753
12017
  // src/hive-guard.ts
11754
12018
  import { existsSync as existsSync4, lstatSync, readdirSync, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
11755
- import { homedir as homedir4 } from "node:os";
11756
- import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
12019
+ import { homedir as homedir5 } from "node:os";
12020
+ 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";
11757
12021
  import { fileURLToPath as fileURLToPath3 } from "node:url";
12022
+
12023
+ // src/subagents.ts
12024
+ import {
12025
+ createAgentSessionFromServices,
12026
+ createAgentSessionServices,
12027
+ SessionManager,
12028
+ SettingsManager
12029
+ } from "@earendil-works/pi-coding-agent";
12030
+ var SUBAGENT_TOOL = "subagent";
12031
+ var SEND_MESSAGE_TOOL = "send_message";
12032
+ var SUBMIT_PLAN_TOOL = "submit_plan";
12033
+ var MAX_PARALLEL = 8;
12034
+ var PARTICLE_ID = "particle";
12035
+ var REFUSAL_FALLBACK = "Subagents run on the free Particle model, or with credits. Switch with /model or run /buy-credits.";
12036
+ function subagentModelId(agent, parentModelId, ids) {
12037
+ if (parentModelId === PARTICLE_ID)
12038
+ return PARTICLE_ID;
12039
+ if (agent === "planner")
12040
+ return ids.planner;
12041
+ return parentModelId === FUSION_ID || parentModelId === FUSION_PICKER_ID ? ids.executor : parentModelId;
12042
+ }
12043
+ function subagentsAvailable(modelId, hasCredit) {
12044
+ return modelId === PARTICLE_ID || hasCredit;
12045
+ }
12046
+ function subagentConcurrency(modelId) {
12047
+ return modelId === PARTICLE_ID ? 4 : 1;
12048
+ }
12049
+ function substitutePrevious(task, previous) {
12050
+ return task.split("{previous}").join(previous);
12051
+ }
12052
+ function parsePlan(input) {
12053
+ const tasks = input?.tasks;
12054
+ if (!Array.isArray(tasks) || tasks.length === 0)
12055
+ return { ok: false, error: "submit_plan needs a non-empty tasks array" };
12056
+ if (tasks.length > MAX_PARALLEL)
12057
+ return { ok: false, error: `A plan has at most ${MAX_PARALLEL} tasks; merge some (got ${tasks.length})` };
12058
+ const out = [];
12059
+ for (const [i, t] of tasks.entries()) {
12060
+ const { title, task } = t ?? {};
12061
+ if (typeof title !== "string" || !title.trim() || typeof task !== "string" || !task.trim()) {
12062
+ return { ok: false, error: `Task ${i + 1} needs a non-empty title and task` };
12063
+ }
12064
+ out.push({ title: title.trim(), task: task.trim() });
12065
+ }
12066
+ return { ok: true, tasks: out };
12067
+ }
12068
+ var READ_ONLY = ["read", "grep", "find", "ls"];
12069
+ var AGENTS = {
12070
+ scout: {
12071
+ tools: [...READ_ONLY, "bash"],
12072
+ prompt: `You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything. Your output goes to an agent who has NOT seen the files you explored.
12073
+ Bash is for read-only commands only. Do NOT modify files.
12074
+ Strategy: grep/find to locate relevant code; read key sections (not whole files); note types, key functions, and how pieces connect.
12075
+ Output format:
12076
+ ## Files Retrieved
12077
+ 1. \`path/to/file.ts\` (lines 10-50) - what is here
12078
+ ## Key Code
12079
+ The critical types, interfaces, or functions, quoted from the files.
12080
+ ## Architecture
12081
+ How the pieces connect, briefly.
12082
+ ## Start Here
12083
+ Which file to look at first and why.`
12084
+ },
12085
+ planner: {
12086
+ tools: [...READ_ONLY],
12087
+ prompt: `You are a planning specialist. You receive context and requirements, then produce a clear implementation plan. You must NOT make any changes. Only read, analyze, and plan.
12088
+ Output format:
12089
+ ## Goal
12090
+ One sentence.
12091
+ ## Plan
12092
+ Numbered steps, each small and actionable, naming the file or function to change.
12093
+ ## Files to Modify
12094
+ ## New Files (if any)
12095
+ ## Risks
12096
+ Keep the plan concrete: a worker agent will execute it verbatim.`
12097
+ },
12098
+ worker: {
12099
+ tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
12100
+ prompt: `You are a worker agent with full capabilities, working in an isolated context window on a task delegated by the lead. Work autonomously to complete it.
12101
+ Output format when finished:
12102
+ ## Completed
12103
+ What was done.
12104
+ ## Files Changed
12105
+ - \`path/to/file.ts\` - what changed
12106
+ ## Notes (if any)
12107
+ Anything the lead should know.`
12108
+ },
12109
+ reviewer: {
12110
+ tools: [...READ_ONLY, "bash"],
12111
+ prompt: `You are a senior code reviewer. Analyze code for quality, security, and maintainability.
12112
+ Bash is for read-only commands only: \`git diff\`, \`git log\`, \`git show\`. Do NOT modify files or run builds. Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
12113
+ Strategy: run \`git diff\` to see recent changes (if applicable), read the modified files, check for bugs, security issues, and code smells.
12114
+ Output format:
12115
+ ## Files Reviewed
12116
+ ## Critical (must fix)
12117
+ - \`file.ts:42\` - issue
12118
+ ## Warnings (should fix)
12119
+ ## Suggestions (consider)
12120
+ ## Summary
12121
+ Overall assessment in 2-3 sentences. Be specific with file paths and line numbers.`
12122
+ }
12123
+ };
12124
+ var isAgent = (name) => typeof name === "string" && Object.hasOwn(AGENTS, name);
12125
+ var PLANNER_WORKFLOW = `Your plan will be carried out by parallel worker agents. When your exploration is done, call ${SUBMIT_PLAN_TOOL} ONCE with 1 to ${MAX_PARALLEL} independent tasks (title, task). Each task must be self-contained (name files and exact changes) and must not depend on another task's result. Calling ${SUBMIT_PLAN_TOOL} is how you finish; do not write the plan as text instead.`;
12126
+ var textResult = (text) => ({ content: [{ type: "text", text }], details: {} });
12127
+ function lastAssistant(session) {
12128
+ for (let i = session.messages.length - 1;i >= 0; i--) {
12129
+ const m = session.messages[i];
12130
+ if (m.role !== "assistant")
12131
+ continue;
12132
+ const text = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
12133
+ `).trim() : "";
12134
+ if (m.stopReason === "error")
12135
+ return { text, error: m.errorMessage || "the model request failed" };
12136
+ if (m.stopReason === "aborted")
12137
+ return { text, error: "aborted" };
12138
+ return { text };
12139
+ }
12140
+ return { text: "" };
12141
+ }
12142
+ function refusalMessage2(error) {
12143
+ if (!error.includes("subagents_unavailable"))
12144
+ return;
12145
+ const m = error.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"/);
12146
+ try {
12147
+ return m ? JSON.parse(`"${m[1]}"`) : REFUSAL_FALLBACK;
12148
+ } catch {
12149
+ return REFUSAL_FALLBACK;
12150
+ }
12151
+ }
12152
+ function formatRun(r) {
12153
+ return `## ${r.name} (${r.agent})${r.error ? " - failed" : ""}
12154
+ ${r.error ? `${r.error}${r.text ? `
12155
+ ${r.text}` : ""}` : r.text || "(no output)"}`;
12156
+ }
12157
+ function createSubagentsExtension(opts) {
12158
+ return {
12159
+ name: "free-pi-subagents",
12160
+ factory: (pi) => {
12161
+ const subs = new Map;
12162
+ const leadInbox = [];
12163
+ let counter = 0;
12164
+ let parentModelId;
12165
+ let active = 0;
12166
+ let waiters = [];
12167
+ const acquire = async () => {
12168
+ while (active >= subagentConcurrency(parentModelId))
12169
+ await new Promise((r) => waiters.push(r));
12170
+ active++;
12171
+ };
12172
+ const release = () => {
12173
+ active--;
12174
+ const w = waiters;
12175
+ waiters = [];
12176
+ for (const r of w)
12177
+ r();
12178
+ };
12179
+ const alloc = (agent) => {
12180
+ const n = ++counter;
12181
+ return { n, name: `${agent}-${n}` };
12182
+ };
12183
+ const syncTools = (model) => {
12184
+ parentModelId = model?.id;
12185
+ const on = subagentsAvailable(model?.id, opts.hasCredit());
12186
+ const active = pi.getActiveTools();
12187
+ if (on === active.includes(SUBAGENT_TOOL))
12188
+ return;
12189
+ const names = [SUBAGENT_TOOL, SEND_MESSAGE_TOOL];
12190
+ pi.setActiveTools(on ? [...active, ...names.filter((n) => !active.includes(n))] : active.filter((n) => !names.includes(n)));
12191
+ };
12192
+ pi.on("session_start", (_e, ctx) => syncTools(ctx.model));
12193
+ pi.on("model_select", (e) => syncTools(e.model));
12194
+ pi.on("before_agent_start", (_e, ctx) => syncTools(ctx.model));
12195
+ pi.on("session_shutdown", () => {
12196
+ for (const s of subs.values())
12197
+ s.session.dispose();
12198
+ subs.clear();
12199
+ });
12200
+ const sendParams = _Object_({
12201
+ to: String2({ description: 'A subagent name such as "worker-2", or "lead"' }),
12202
+ message: String2({ description: "The message" })
12203
+ });
12204
+ const sendMessageTool = (from) => ({
12205
+ name: SEND_MESSAGE_TOOL,
12206
+ label: "Send message",
12207
+ description: 'Send a short message to another subagent by name, or to "lead". A running agent receives it after its current turn. A finished agent cannot; the lead can resume it by messaging it.',
12208
+ promptSnippet: "Send a message to a subagent or the lead",
12209
+ exposure: "model-only",
12210
+ parameters: sendParams,
12211
+ async execute(_id, p, signal) {
12212
+ return textResult(await deliver(from, p.to, p.message, { signal }));
12213
+ }
12214
+ });
12215
+ const createSub = async (agent, alloced, siblings, env, workflowPlan) => {
12216
+ const def = AGENTS[agent];
12217
+ const subSessionId = `${opts.sessionId}~sub-${alloced.n}`;
12218
+ const modelId = subagentModelId(agent, env.model.id, opts.fusionIds);
12219
+ const provider = modelId === env.model.id ? env.model.provider : PROVIDER_NAME;
12220
+ const settingsManager = SettingsManager.inMemory({
12221
+ defaultProvider: provider,
12222
+ defaultModel: modelId,
12223
+ defaultTools: [...def.tools],
12224
+ packages: [],
12225
+ quietStartup: true,
12226
+ defaultThinkingLevel: "off",
12227
+ enableInstallTelemetry: false,
12228
+ cacheWarming: "off"
12229
+ }, { projectTrusted: env.trusted });
12230
+ const others = siblings.filter((s) => s !== alloced.name);
12231
+ const prompt = [
12232
+ `You are ${alloced.name}, a ${agent} subagent working for the lead agent.`,
12233
+ def.prompt,
12234
+ workflowPlan ? PLANNER_WORKFLOW : "",
12235
+ others.length > 0 ? `Other subagents running alongside you: ${others.join(", ")}.` : "",
12236
+ `Use ${SEND_MESSAGE_TOOL} ({to, message}) to send a short note to a sibling by name or to "lead", only when it helps (a finding another agent needs, a conflict). A sibling gets it after its current turn. Your final reply goes to the lead automatically.`
12237
+ ].filter(Boolean).join(`
12238
+
12239
+ `);
12240
+ const services = await createAgentSessionServices({
12241
+ cwd: env.cwd,
12242
+ agentDir: opts.agentDir,
12243
+ settingsManager,
12244
+ resourceLoaderOptions: {
12245
+ extensionFactories: opts.extensionsFor(subSessionId),
12246
+ noExtensions: true,
12247
+ noSkills: true,
12248
+ noPromptTemplates: true,
12249
+ noThemes: true,
12250
+ appendSystemPrompt: [prompt]
12251
+ }
12252
+ });
12253
+ const customTools = [sendMessageTool(alloced.name)];
12254
+ if (workflowPlan) {
12255
+ customTools.push({
12256
+ name: SUBMIT_PLAN_TOOL,
12257
+ label: "Submit plan",
12258
+ description: `Submit the plan as 1 to ${MAX_PARALLEL} independent tasks for parallel workers. Call once, when done.`,
12259
+ parameters: _Object_({
12260
+ tasks: _Array_(_Object_({ title: String2(), task: String2() }), { minItems: 1, maxItems: MAX_PARALLEL })
12261
+ }),
12262
+ async execute(_id, p) {
12263
+ const r = parsePlan(p);
12264
+ if (!r.ok)
12265
+ throw new Error(r.error);
12266
+ workflowPlan(r.tasks);
12267
+ return textResult(`Plan recorded (${r.tasks.length} tasks). Finish with a one-line summary.`);
12268
+ }
12269
+ });
12270
+ }
12271
+ const { session } = await createAgentSessionFromServices({
12272
+ services,
12273
+ sessionManager: SessionManager.inMemory(env.cwd),
12274
+ tools: [...def.tools, SEND_MESSAGE_TOOL, ...workflowPlan ? [SUBMIT_PLAN_TOOL] : []],
12275
+ customTools
12276
+ });
12277
+ await session.bindExtensions({});
12278
+ const sub = { name: alloced.name, agent, session, modelId, running: false, started: false, pending: [], last: "starting" };
12279
+ session.subscribe((e) => {
12280
+ if (e.type !== "tool_execution_start")
12281
+ return;
12282
+ const arg = e.args?.command ?? e.args?.path ?? e.args?.file_path ?? e.args?.pattern ?? "";
12283
+ sub.last = `${e.toolName}${arg ? ` ${String(arg).slice(0, 60)}` : ""}`;
12284
+ sub.progress?.();
12285
+ });
12286
+ subs.set(sub.name, sub);
12287
+ return sub;
12288
+ };
12289
+ const runPrompt = async (sub, text, ctx) => {
12290
+ const base = { name: sub.name, agent: sub.agent };
12291
+ if (ctx.signal?.aborted)
12292
+ return { ...base, text: "", error: "aborted" };
12293
+ await acquire();
12294
+ try {
12295
+ return await runPromptHeld(sub, text, ctx);
12296
+ } finally {
12297
+ release();
12298
+ }
12299
+ };
12300
+ const runPromptHeld = async (sub, text, ctx) => {
12301
+ const base = { name: sub.name, agent: sub.agent };
12302
+ if (ctx.signal?.aborted)
12303
+ return { ...base, text: "", error: "aborted" };
12304
+ const onAbort = () => void sub.session.abort();
12305
+ ctx.signal?.addEventListener("abort", onAbort, { once: true });
12306
+ sub.progress = ctx.progress;
12307
+ sub.started = true;
12308
+ sub.running = true;
12309
+ sub.last = "thinking";
12310
+ try {
12311
+ const queued = sub.pending.splice(0);
12312
+ await sub.session.prompt(queued.length > 0 ? `${queued.join(`
12313
+ `)}
12314
+
12315
+ ${text}` : text);
12316
+ } catch (e) {
12317
+ return { ...base, text: "", error: e instanceof Error ? e.message : String(e) };
12318
+ } finally {
12319
+ sub.running = false;
12320
+ sub.last = "done";
12321
+ sub.progress = undefined;
12322
+ ctx.signal?.removeEventListener("abort", onAbort);
12323
+ }
12324
+ return { ...base, ...lastAssistant(sub.session) };
12325
+ };
12326
+ const prepare = async (agent, alloced, siblings, env) => {
12327
+ try {
12328
+ return await createSub(agent, alloced, siblings, env);
12329
+ } catch (e) {
12330
+ return { name: alloced.name, agent, text: "", error: e instanceof Error ? e.message : String(e) };
12331
+ }
12332
+ };
12333
+ const runPrepared = (p, task, env) => ("session" in p) ? runPrompt(p, task, env) : Promise.resolve(p);
12334
+ const runNew = async (agent, alloced, task, siblings, env) => runPrepared(await prepare(agent, alloced, siblings, env), task, env);
12335
+ async function deliver(from, to, message, ctx) {
12336
+ if (to === from)
12337
+ return "You cannot message yourself.";
12338
+ if (to === "lead") {
12339
+ if (from === "lead")
12340
+ return "You are the lead.";
12341
+ leadInbox.push({ from, message });
12342
+ return "Delivered to lead. It appears in the lead's subagent result.";
12343
+ }
12344
+ const target = subs.get(to);
12345
+ if (!target)
12346
+ return `No subagent named ${to}. Known: ${[...subs.keys()].join(", ") || "none"}.`;
12347
+ const note = `Message from ${from}: ${message}`;
12348
+ if (target.running) {
12349
+ await target.session.steer(note);
12350
+ return `Delivered to ${to}. It will see it after its current turn.`;
12351
+ }
12352
+ if (!target.started) {
12353
+ target.pending.push(note);
12354
+ return `${to} has not started yet. Your message is queued and arrives with its task.`;
12355
+ }
12356
+ if (from !== "lead") {
12357
+ target.pending.push(note);
12358
+ return `${to} has finished, so it cannot read messages now. Your message is queued; the lead can resume ${to}.`;
12359
+ }
12360
+ const current = ctx.modelId ?? parentModelId;
12361
+ if (current && subagentModelId(target.agent, current, opts.fusionIds) !== target.modelId) {
12362
+ return `${to} ran on ${target.modelId}; start a new subagent on the current model.`;
12363
+ }
12364
+ return formatRun(await runPrompt(target, note, ctx));
12365
+ }
12366
+ const withInbox = (text) => {
12367
+ if (leadInbox.length === 0)
12368
+ return text;
12369
+ const lines = leadInbox.splice(0).map((m) => `- from ${m.from}: ${m.message}`);
12370
+ return `${text}
12371
+
12372
+ ## Messages to lead
12373
+ ${lines.join(`
12374
+ `)}`;
12375
+ };
12376
+ pi.registerTool({
12377
+ ...sendMessageTool("lead"),
12378
+ async execute(_id, p, signal, _onUpdate, ctx) {
12379
+ if (ctx.model)
12380
+ parentModelId = ctx.model.id;
12381
+ return textResult(withInbox(await deliver("lead", p.to, p.message, { signal, modelId: ctx.model?.id })));
12382
+ }
12383
+ });
12384
+ async function runWorkflow(goal, env) {
12385
+ let plan;
12386
+ const plannerAlloc = alloc("planner");
12387
+ let plannerRun;
12388
+ try {
12389
+ const planner = await createSub("planner", plannerAlloc, [], env, (t) => plan = t);
12390
+ plannerRun = await runPrompt(planner, `Goal: ${goal}
12391
+
12392
+ Explore the project as needed, then call ${SUBMIT_PLAN_TOOL}.`, env);
12393
+ } catch (e) {
12394
+ plannerRun = { name: plannerAlloc.name, agent: "planner", text: "", error: e instanceof Error ? e.message : String(e) };
12395
+ }
12396
+ if (plannerRun.error)
12397
+ return { runs: [plannerRun], preface: "" };
12398
+ const tasks = plan;
12399
+ if (!tasks)
12400
+ return { runs: [{ ...plannerRun, error: `The planner finished without calling ${SUBMIT_PLAN_TOOL}.` }], preface: "" };
12401
+ const planText = tasks.map((t, i) => `${i + 1}. ${t.title}: ${t.task}`).join(`
12402
+ `);
12403
+ const allocs = tasks.map(() => alloc("worker"));
12404
+ const names = allocs.map((a) => a.name);
12405
+ const prepared = await Promise.all(allocs.map((a) => prepare("worker", a, names, env)));
12406
+ const workers = await Promise.all(tasks.map((t, i) => runPrepared(prepared[i], `Overall goal: ${goal}
12407
+
12408
+ Your part (${t.title}):
12409
+ ${t.task}`, env)));
12410
+ const results = workers.map((w, i) => `### ${tasks[i].title} (${w.name})
12411
+ ${w.error ?? w.text}`).join(`
12412
+
12413
+ `);
12414
+ const reviewer = await runNew("reviewer", alloc("reviewer"), `Review the work done toward this goal.
12415
+
12416
+ Goal: ${goal}
12417
+
12418
+ Plan:
12419
+ ${planText}
12420
+
12421
+ Worker results:
12422
+ ${results}
12423
+
12424
+ Check the changes against the goal and report problems.`, [], env);
12425
+ return { runs: [...workers, reviewer], preface: `## Plan (${plannerRun.name})
12426
+ ${planText}
12427
+
12428
+ ` };
12429
+ }
12430
+ const TaskItem = _Object_({
12431
+ agent: String2({ description: "scout | planner | worker | reviewer" }),
12432
+ task: String2({ description: "The task for that agent" })
12433
+ });
12434
+ pi.registerTool({
12435
+ name: SUBAGENT_TOOL,
12436
+ label: "Subagent",
12437
+ description: [
12438
+ "Delegate work to subagents, each with its own context window. Agents: scout (read-only recon), planner (read-only plan), worker (full tools), reviewer (read-only review). Use exactly one mode:",
12439
+ "- single: {agent, task}",
12440
+ `- parallel: {tasks: [{agent, task}]} (max ${MAX_PARALLEL}); independent tasks run concurrently`,
12441
+ "- chain: {chain: [{agent, task}]}; {previous} in a task is replaced by the prior step's final text",
12442
+ "- workflow: {workflow: {goal}}; a planner splits the goal into independent tasks, workers run them in parallel, a reviewer checks the result",
12443
+ "Subagents can message each other and the lead with send_message. You (the lead) can message a finished subagent by name to resume it."
12444
+ ].join(`
12445
+ `),
12446
+ promptSnippet: "Delegate to scout/planner/worker/reviewer subagents (single, parallel, chain, workflow)",
12447
+ promptGuidelines: ["Use subagent to fan out independent research or edits, or to keep a large exploration out of your own context. Do small tasks yourself."],
12448
+ exposure: "model-only",
12449
+ parameters: _Object_({
12450
+ agent: Optional(String2({ description: "Single mode: agent name" })),
12451
+ task: Optional(String2({ description: "Single mode: the task" })),
12452
+ tasks: Optional(_Array_(TaskItem, { maxItems: MAX_PARALLEL, description: "Parallel mode" })),
12453
+ chain: Optional(_Array_(TaskItem, { maxItems: MAX_PARALLEL, description: "Chain mode" })),
12454
+ workflow: Optional(_Object_({ goal: String2() }, { description: "Workflow mode" }))
12455
+ }),
12456
+ async execute(_id, params, signal, onUpdate, ctx) {
12457
+ if (!ctx.model)
12458
+ throw new Error("No model selected.");
12459
+ const env = {
12460
+ cwd: ctx.cwd,
12461
+ trusted: ctx.isProjectTrusted(),
12462
+ model: { provider: ctx.model.provider, id: ctx.model.id },
12463
+ signal,
12464
+ progress: () => {
12465
+ const lines = [...subs.values()].filter((s) => s.running).map((s) => `${s.name}: ${s.last}`);
12466
+ onUpdate?.(textResult(lines.join(`
12467
+ `) || "subagents working"));
12468
+ }
12469
+ };
12470
+ parentModelId = ctx.model.id;
12471
+ const single = params.agent !== undefined || params.task !== undefined;
12472
+ if ([single, params.tasks, params.chain, params.workflow].filter(Boolean).length !== 1) {
12473
+ throw new Error("Use exactly one mode: {agent, task} | {tasks} | {chain} | {workflow}.");
12474
+ }
12475
+ const check = (items) => {
12476
+ for (const i of items)
12477
+ if (!isAgent(i.agent))
12478
+ throw new Error(`Unknown agent "${i.agent}". Use scout, planner, worker or reviewer.`);
12479
+ };
12480
+ let runs = [];
12481
+ let preface = "";
12482
+ if (single) {
12483
+ if (!isAgent(params.agent) || !params.task)
12484
+ throw new Error("Single mode needs a valid agent and a task.");
12485
+ runs = [await runNew(params.agent, alloc(params.agent), params.task, [], env)];
12486
+ } else if (params.tasks) {
12487
+ check(params.tasks);
12488
+ const allocs = params.tasks.map((t) => alloc(t.agent));
12489
+ const names = allocs.map((a) => a.name);
12490
+ const prepared = await Promise.all(params.tasks.map((t, i) => prepare(t.agent, allocs[i], names, env)));
12491
+ runs = await Promise.all(params.tasks.map((t, i) => runPrepared(prepared[i], t.task, env)));
12492
+ } else if (params.chain) {
12493
+ check(params.chain);
12494
+ let previous = "";
12495
+ for (const step of params.chain) {
12496
+ const r = await runNew(step.agent, alloc(step.agent), substitutePrevious(step.task, previous), [], env);
12497
+ runs.push(r);
12498
+ if (r.error)
12499
+ break;
12500
+ previous = r.text;
12501
+ }
12502
+ } else if (params.workflow) {
12503
+ ({ runs, preface } = await runWorkflow(params.workflow.goal, env));
12504
+ }
12505
+ const refused = runs.map((r) => r.error ? refusalMessage2(r.error) : undefined).find(Boolean);
12506
+ return textResult(withInbox(refused ?? `${preface}${runs.map(formatRun).join(`
12507
+
12508
+ `)}`));
12509
+ }
12510
+ });
12511
+ }
12512
+ };
12513
+ }
12514
+
12515
+ // src/hive-guard.ts
11758
12516
  var GUARD_INPUT_LIMIT = 1500;
11759
12517
  var CLASSIFY_TIMEOUT_MS = 2500;
11760
12518
  var SCAN_LIMIT = 20000;
11761
12519
  var MCP_RESOURCE_TOOLS2 = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
11762
12520
  var PATH_TOOLS = ["read", "write", "edit", "grep", "find", "ls"];
11763
- var SAFE_TOOLS2 = [TODO_TOOL_NAME, USAGE_TOOL_NAME, DOCS_TOOL_NAME];
12521
+ var SAFE_TOOLS2 = [TODO_TOOL_NAME, USAGE_TOOL_NAME, DOCS_TOOL_NAME, ASK_USER_TOOL_NAME];
11764
12522
  var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
11765
12523
  var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
11766
12524
  var CLASSIFIED = ["bash", "write", "edit"];
@@ -11788,7 +12546,7 @@ function protectedLinkTargets(root, limit) {
11788
12546
  const note = (link) => {
11789
12547
  let target;
11790
12548
  try {
11791
- target = resolve2(dirname8(link), readlinkSync(link));
12549
+ target = resolve2(dirname9(link), readlinkSync(link));
11792
12550
  } catch {
11793
12551
  return;
11794
12552
  }
@@ -11884,7 +12642,7 @@ function realpathDeepest(p) {
11884
12642
  return null;
11885
12643
  } catch {}
11886
12644
  }
11887
- const parent = dirname8(current);
12645
+ const parent = dirname9(current);
11888
12646
  if (parent === current)
11889
12647
  return null;
11890
12648
  rest.push(basename3(current));
@@ -11998,7 +12756,7 @@ function createClassifier(opts) {
11998
12756
  }
11999
12757
  async function decideToolCall(event, opts) {
12000
12758
  const { toolName: tool, input } = event;
12001
- const home = opts.home ?? homedir4();
12759
+ const home = opts.home ?? homedir5();
12002
12760
  const ask = (reason) => ({ action: "ask", reason, summary: summarize2(tool, input) });
12003
12761
  if (event.parentToolCallId)
12004
12762
  return ask("nested call made by another tool");
@@ -12007,6 +12765,8 @@ async function decideToolCall(event, opts) {
12007
12765
  return deny("tool not permitted");
12008
12766
  if (tool === BUY_TOOL_NAME)
12009
12767
  return deny("purchases are never run for a guest");
12768
+ if (tool === SUBAGENT_TOOL || tool === SEND_MESSAGE_TOOL)
12769
+ return deny("subagents are never started for a guest");
12010
12770
  const path = pathVerdict(tool, input, opts, home);
12011
12771
  if (path && "deny" in path)
12012
12772
  return deny(path.deny);
@@ -12065,7 +12825,7 @@ function fingerprint(path) {
12065
12825
  return "";
12066
12826
  if (st.size > HASH_MAX_BYTES)
12067
12827
  return `${st.size}:${st.mtimeMs}`;
12068
- return createHash3("sha256").update(readFileSync4(path)).digest("hex");
12828
+ return createHash3("sha256").update(readFileSync5(path)).digest("hex");
12069
12829
  } catch {
12070
12830
  return "";
12071
12831
  }
@@ -12203,6 +12963,8 @@ function createHiveExtension(opts) {
12203
12963
  const changes = startSnapshot && changedSummary(startSnapshot, { cwd: roomCwd, exec: opts.execGit ?? execFileSync });
12204
12964
  startSnapshot = undefined;
12205
12965
  ws = undefined;
12966
+ if (opts.roomState)
12967
+ opts.roomState.open = false;
12206
12968
  roomUrl = undefined;
12207
12969
  members = [];
12208
12970
  driver = "host";
@@ -12238,6 +13000,8 @@ function createHiveExtension(opts) {
12238
13000
  const Impl = opts.WebSocketImpl ?? WebSocket;
12239
13001
  const socket = new Impl(wsUrl(opts.baseUrl));
12240
13002
  ws = socket;
13003
+ if (opts.roomState)
13004
+ opts.roomState.open = true;
12241
13005
  roomCwd = ctx.cwd;
12242
13006
  revision = 0;
12243
13007
  driver = "host";
@@ -12550,16 +13314,16 @@ import {
12550
13314
  chmodSync,
12551
13315
  existsSync as existsSync5,
12552
13316
  lstatSync as lstatSync3,
12553
- mkdirSync as mkdirSync2,
13317
+ mkdirSync as mkdirSync3,
12554
13318
  mkdtempSync,
12555
13319
  readdirSync as readdirSync2,
12556
13320
  realpathSync as realpathSync4,
12557
13321
  rmSync,
12558
13322
  statSync as statSync3,
12559
- writeFileSync as writeFileSync2
13323
+ writeFileSync as writeFileSync3
12560
13324
  } from "node:fs";
12561
- import { homedir as homedir5, tmpdir } from "node:os";
12562
- import { dirname as dirname9, join as join10 } from "node:path";
13325
+ import { homedir as homedir6, tmpdir } from "node:os";
13326
+ import { dirname as dirname10, join as join10 } from "node:path";
12563
13327
  var SECRET_NAME_PATTERNS = [
12564
13328
  "\\.env(\\..*)?",
12565
13329
  "[^/]*\\.pem",
@@ -12809,7 +13573,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
12809
13573
  var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
12810
13574
  function ancestors(p) {
12811
13575
  const out = [];
12812
- for (let d = dirname9(p);; d = dirname9(d)) {
13576
+ for (let d = dirname10(p);; d = dirname10(d)) {
12813
13577
  out.push(d);
12814
13578
  if (d === "/")
12815
13579
  break;
@@ -12847,7 +13611,7 @@ function buildMacProfile(i) {
12847
13611
  L.push(`(deny file-write* (literal ${sbString(f)}))`);
12848
13612
  const guarded = new Set([i.project, join10(i.project, ".git"), ...i.nodeModules ?? []]);
12849
13613
  for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join10(i.project, ".git")]) {
12850
- for (let d = dirname9(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname9(d))
13614
+ for (let d = dirname10(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname10(d))
12851
13615
  guarded.add(d);
12852
13616
  }
12853
13617
  for (const d of [...guarded].sort())
@@ -12976,7 +13740,7 @@ for arg in sys.argv[3:]:
12976
13740
  `;
12977
13741
  function createShellSandbox(deps = {}) {
12978
13742
  const platform = deps.platform ?? process.platform;
12979
- const home = deps.home ?? homedir5();
13743
+ const home = deps.home ?? homedir6();
12980
13744
  const exec = deps.exec ?? defaultExec;
12981
13745
  const launcher = deps.launcher ?? { sandbox: platform === "darwin" ? "/usr/bin/sandbox-exec" : "/usr/bin/bwrap", shell: "/bin/bash" };
12982
13746
  const stat = deps.launcherStat ?? statSync3;
@@ -13107,11 +13871,11 @@ function createShellSandbox(deps = {}) {
13107
13871
  try {
13108
13872
  scratch = mkdtempSync(join10(realpathSync4(tmpdir()), "freepi-hive-"));
13109
13873
  const dir = scratch;
13110
- mkdirSync2(join10(dir, "home"));
13111
- mkdirSync2(join10(dir, "npm-cache"));
13874
+ mkdirSync3(join10(dir, "home"));
13875
+ mkdirSync3(join10(dir, "npm-cache"));
13112
13876
  const emptyFile = join10(dir, "empty");
13113
13877
  const marker = join10(dir, "call.fd");
13114
- writeFileSync2(emptyFile, "", { mode: 292 });
13878
+ writeFileSync3(emptyFile, "", { mode: 292 });
13115
13879
  const roots = toolchainRoots(home);
13116
13880
  const mac = platform === "darwin";
13117
13881
  const host = mac ? macHost() : undefined;
@@ -13298,9 +14062,15 @@ function catalogModelsFor(opts) {
13298
14062
  const id = opts.model || MODEL_ID;
13299
14063
  return [{ id, name: id }];
13300
14064
  }
14065
+ function pickerModelName(opts, id) {
14066
+ return [...catalogModelsFor(opts), ...defenseModelsFor(opts)].find((m) => m.id === id)?.name;
14067
+ }
13301
14068
  function defenseModelsFor(opts) {
13302
14069
  return splitCatalogModels(opts.models ?? []).defense;
13303
14070
  }
14071
+ function freeCatalogModels(opts) {
14072
+ return (opts.models ?? []).filter((m) => m.name.includes(FREE_LABEL)).map((m) => ({ provider: m.section === "defense" ? DEFENSE_PROVIDER_NAME : PROVIDER_NAME, id: m.id }));
14073
+ }
13304
14074
  var ALLOWED_TOOL_NAMES = [
13305
14075
  ...SAFE_TOOLS,
13306
14076
  USAGE_TOOL_NAME,
@@ -13312,6 +14082,8 @@ var ALLOWED_TOOL_NAMES = [
13312
14082
  VISION_TOOL_NAME,
13313
14083
  TODO_TOOL_NAME,
13314
14084
  FUSION_PLAN_TOOL,
14085
+ SUBAGENT_TOOL,
14086
+ SEND_MESSAGE_TOOL,
13315
14087
  "codemode",
13316
14088
  "tool_search"
13317
14089
  ];
@@ -13322,12 +14094,12 @@ var SDK_TOOLS = [
13322
14094
  "list_mcp_resource_templates",
13323
14095
  "read_mcp_resource"
13324
14096
  ];
13325
- var SKILLS_DIR = resolve4(dirname10(fileURLToPath4(import.meta.url)), "..", "skills");
14097
+ var SKILLS_DIR = resolve4(dirname11(fileURLToPath4(import.meta.url)), "..", "skills");
13326
14098
  function resolveModelName(opts) {
13327
14099
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
13328
14100
  }
13329
14101
  function buildSettingsManager(opts, models, projectTrusted) {
13330
- return SettingsManager.inMemory({
14102
+ return SettingsManager2.inMemory({
13331
14103
  defaultProvider: PROVIDER_NAME,
13332
14104
  defaultModel: models[0].id,
13333
14105
  defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
@@ -13349,18 +14121,19 @@ function buildRuntimeOptions(opts, sessionId) {
13349
14121
  const models = catalogModelsFor(opts);
13350
14122
  const auth = opts.sessionAuth ?? createSessionAuth({ jwt: opts.jwt, guest: Boolean(opts.guest) });
13351
14123
  opts.sessionAuth = auth;
14124
+ const registerProviders = (pi, sid, extraHeaders) => {
14125
+ pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sid, models, PROVIDER_NAME, extraHeaders));
14126
+ const defense = defenseModelsFor(opts);
14127
+ if (defense.length > 0) {
14128
+ pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sid, defense, DEFENSE_PROVIDER_NAME, extraHeaders));
14129
+ }
14130
+ pi.on("before_provider_headers", (event) => {
14131
+ event.headers.Authorization = `Bearer ${auth.getJwt()}`;
14132
+ });
14133
+ };
13352
14134
  const providerExtension = {
13353
14135
  name: "free-pi-provider",
13354
- factory: (pi) => {
13355
- pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, models));
13356
- const defense = defenseModelsFor(opts);
13357
- if (defense.length > 0) {
13358
- pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, defense, DEFENSE_PROVIDER_NAME));
13359
- }
13360
- pi.on("before_provider_headers", (event) => {
13361
- event.headers.Authorization = `Bearer ${auth.getJwt()}`;
13362
- });
13363
- }
14136
+ factory: (pi) => registerProviders(pi, sessionId)
13364
14137
  };
13365
14138
  let advertiserUrl;
13366
14139
  const adsExtension = createAdsExtension({
@@ -13397,11 +14170,16 @@ function buildRuntimeOptions(opts, sessionId) {
13397
14170
  baseUrl: opts.baseUrl,
13398
14171
  getToken: () => auth.getJwt()
13399
14172
  });
14173
+ const tty = opts.tty ?? { stdin: process.stdin.isTTY === true, stdout: process.stdout.isTTY === true };
13400
14174
  const errorNoticeExtension = createErrorNoticeExtension({
14175
+ freeModels: () => freeCatalogModels(opts),
14176
+ terminal: tty.stdin && tty.stdout,
13401
14177
  extraNotice: (env) => {
13402
14178
  const session = opts.refillCard;
13403
14179
  if (!session || typeof env.remaining_credits_today !== "number")
13404
14180
  return;
14181
+ if (suppressCardOnAllowanceMismatch(session, env.allowance_credits))
14182
+ return;
13405
14183
  const card = formatLowAllowanceCard(session, env.remaining_credits_today);
13406
14184
  if (!card)
13407
14185
  return;
@@ -13410,10 +14188,12 @@ function buildRuntimeOptions(opts, sessionId) {
13410
14188
  return card;
13411
14189
  }
13412
14190
  });
14191
+ const choiceOn = choiceEnabled({ env: process.env, arm: opts.choiceArm, configPath: opts.choiceConfigPath ?? getConfigPath() });
14192
+ const allowedTools = choiceOn ? [...ALLOWED_TOOL_NAMES, ASK_USER_TOOL_NAME] : ALLOWED_TOOL_NAMES;
13413
14193
  const toolGuardExtension = createToolGuardExtension({
13414
14194
  baseUrl: opts.baseUrl,
13415
14195
  getToken: () => auth.getJwt(),
13416
- allowedTools: ALLOWED_TOOL_NAMES
14196
+ allowedTools
13417
14197
  });
13418
14198
  const commandsExtension = createFreePiCommandsExtension({
13419
14199
  baseUrl: opts.baseUrl,
@@ -13437,9 +14217,9 @@ function buildRuntimeOptions(opts, sessionId) {
13437
14217
  const headerExtension = createHeaderExtension({
13438
14218
  modelName: resolveModelName(opts),
13439
14219
  updateLatest: opts.updateLatest,
13440
- guest: opts.guest,
13441
- allowanceLine: opts.allowanceLine,
13442
- modelNameFor: (id) => catalogModelsFor(opts).find((m) => m.id === id)?.name
14220
+ startupNotes: opts.startupNotes,
14221
+ toolModePath: getToolModePath(opts.agentDir),
14222
+ modelNameFor: (id) => pickerModelName(opts, id)
13443
14223
  });
13444
14224
  const todoToolExtension = createTodoToolExtension();
13445
14225
  const shellSandbox = opts.shellSandbox ?? createShellSandbox();
@@ -13448,11 +14228,36 @@ function buildRuntimeOptions(opts, sessionId) {
13448
14228
  const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
13449
14229
  const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
13450
14230
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
14231
+ const hiveRoom = { open: false };
13451
14232
  const hiveExtension = createHiveExtension({
13452
14233
  baseUrl: opts.baseUrl,
13453
14234
  getToken: () => auth.getJwt(),
13454
- knownTools: ALLOWED_TOOL_NAMES,
13455
- sandbox: shellSandbox
14235
+ knownTools: allowedTools,
14236
+ sandbox: shellSandbox,
14237
+ roomState: hiveRoom
14238
+ });
14239
+ const choiceExtension = choiceOn ? createChoiceToolExtension({
14240
+ baseUrl: opts.baseUrl,
14241
+ getToken: () => auth.getJwt(),
14242
+ sessionId,
14243
+ arm: () => opts.choiceArm ?? null,
14244
+ headless: !(tty.stdin && tty.stdout),
14245
+ isRoomOpen: () => hiveRoom.open
14246
+ }) : undefined;
14247
+ const subagentsExtension = createSubagentsExtension({
14248
+ sessionId,
14249
+ agentDir: opts.agentDir,
14250
+ fusionIds: resolveFusionIds(models.map((m) => m.id), opts.fusion),
14251
+ hasCredit: () => (getAllowanceCache()?.boughtCredits ?? 0) > 0,
14252
+ extensionsFor: (subSessionId) => [
14253
+ { name: "free-pi-subagent-provider", factory: (pi) => registerProviders(pi, subSessionId, { "x-freepi-subagent": "1" }) },
14254
+ createToolGuardExtension({
14255
+ baseUrl: opts.baseUrl,
14256
+ getToken: () => auth.getJwt(),
14257
+ allowedTools: [...SAFE_TOOLS, SEND_MESSAGE_TOOL, SUBMIT_PLAN_TOOL]
14258
+ }),
14259
+ createToolAliasesExtension({ sandbox: shellSandbox, shell })
14260
+ ]
13456
14261
  });
13457
14262
  const extensionFactories = [
13458
14263
  providerExtension,
@@ -13464,6 +14269,7 @@ function buildRuntimeOptions(opts, sessionId) {
13464
14269
  webToolsExtension,
13465
14270
  imageToolExtension,
13466
14271
  visionToolExtension,
14272
+ ...choiceExtension ? [choiceExtension] : [],
13467
14273
  errorNoticeExtension,
13468
14274
  createUpgradeNoticeExtension({ baseUrl: opts.baseUrl }),
13469
14275
  createOutputLimitExtension(),
@@ -13477,6 +14283,7 @@ function buildRuntimeOptions(opts, sessionId) {
13477
14283
  mcpExtension,
13478
14284
  codemodeExtension,
13479
14285
  toolSearchExtension,
14286
+ subagentsExtension,
13480
14287
  hiveExtension,
13481
14288
  ...guestLoginExtension ? [guestLoginExtension] : [],
13482
14289
  ...opts.guest ? [createGuestNotesExtension({ isGuest: () => auth.isGuest() })] : []
@@ -13487,10 +14294,11 @@ function buildRuntimeOptions(opts, sessionId) {
13487
14294
  resourceLoaderOptions: {
13488
14295
  extensionFactories,
13489
14296
  additionalSkillPaths: [SKILLS_DIR],
13490
- appendSystemPromptOverride: (base) => [...base, agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [shellPromptLine(shell)] : []]
14297
+ appendSystemPromptOverride: (base) => [...base, choiceOn ? `${agentRulesFor(opts.costArm)}
14298
+ ${CHOICE_RULES}` : agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [shellPromptLine(shell)] : []]
13491
14299
  },
13492
14300
  extensionNames: extensionFactories.map((e) => e.name),
13493
- tools: SDK_TOOLS
14301
+ tools: choiceOn ? [...SDK_TOOLS, ASK_USER_TOOL_NAME] : SDK_TOOLS
13494
14302
  };
13495
14303
  }
13496
14304
  function getSessionDir(cwd, agentDir) {
@@ -13498,7 +14306,7 @@ function getSessionDir(cwd, agentDir) {
13498
14306
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
13499
14307
  return join11(resolve4(agentDir), "sessions", safePath);
13500
14308
  }
13501
- async function findLatestSession(cwd, sessionDir, api = SessionManager) {
14309
+ async function findLatestSession(cwd, sessionDir, api = SessionManager2) {
13502
14310
  const infos = await api.list(cwd, sessionDir);
13503
14311
  if (infos.length === 0)
13504
14312
  return;
@@ -13511,7 +14319,7 @@ async function findLatestSession(cwd, sessionDir, api = SessionManager) {
13511
14319
  firstMessage: latest.firstMessage ?? ""
13512
14320
  };
13513
14321
  }
13514
- async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager) {
14322
+ async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager2) {
13515
14323
  if (sessionId) {
13516
14324
  const infos = await api.list(cwd, sessionDir);
13517
14325
  const info = infos.find((session) => session.id === sessionId);
@@ -13554,7 +14362,7 @@ async function launchPi(opts) {
13554
14362
  sessionManager,
13555
14363
  sessionStartEvent
13556
14364
  }) => {
13557
- const services = await createAgentSessionServices({
14365
+ const services = await createAgentSessionServices2({
13558
14366
  cwd: rtCwd,
13559
14367
  agentDir: opts.agentDir,
13560
14368
  settingsManager: settingsForRuntime(opts, rtCwd, launchTrust),
@@ -13562,7 +14370,7 @@ async function launchPi(opts) {
13562
14370
  });
13563
14371
  const scopedModels = pickerScopeFor(services.modelRuntime, opts);
13564
14372
  return {
13565
- ...await createAgentSessionFromServices({
14373
+ ...await createAgentSessionFromServices2({
13566
14374
  services,
13567
14375
  sessionManager,
13568
14376
  sessionStartEvent,
@@ -13597,7 +14405,15 @@ async function launchPi(opts) {
13597
14405
  });
13598
14406
  installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
13599
14407
  installModelAllowanceHandler(mode);
13600
- installFooterRightAlign(mode);
14408
+ installFooterRightAlign(mode, () => {
14409
+ const id = runtime.session?.model?.id;
14410
+ return freeModelBase(pickerModelName(opts, id));
14411
+ });
14412
+ installEditorPlaceholder(mode);
14413
+ if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
14414
+ process.stdout.write("\x1B[2J\x1B[H");
14415
+ installBottomPin(mode);
14416
+ }
13601
14417
  await mode.run();
13602
14418
  return sm.getSessionId();
13603
14419
  }
@@ -13630,14 +14446,14 @@ async function promptConsentInteractive(text) {
13630
14446
 
13631
14447
  // src/real-spawn.ts
13632
14448
  import { spawnSync as spawnSync2 } from "node:child_process";
13633
- import { homedir as homedir6 } from "node:os";
14449
+ import { homedir as homedir7 } from "node:os";
13634
14450
  function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawnSync2 }) {
13635
14451
  return (cmd, args, opts) => {
13636
14452
  const result = io.spawnSyncImpl(cmd, args, {
13637
14453
  timeout: opts.timeout,
13638
14454
  encoding: "utf8",
13639
14455
  shell: io.platform === "win32",
13640
- cwd: homedir6()
14456
+ cwd: homedir7()
13641
14457
  });
13642
14458
  return { status: result.status, stdout: result.stdout ?? "", error: result.error };
13643
14459
  };
@@ -13646,7 +14462,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
13646
14462
  // src/relaunch.ts
13647
14463
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
13648
14464
  import { constants as osConstants, tmpdir as tmpdir2 } from "node:os";
13649
- import { delimiter, dirname as dirname11, isAbsolute as isAbsolute3, join as join12, resolve as resolve5, sep as sep4, win32 } from "node:path";
14465
+ import { delimiter, dirname as dirname12, isAbsolute as isAbsolute3, join as join12, resolve as resolve5, sep as sep4, win32 } from "node:path";
13650
14466
  function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
13651
14467
  try {
13652
14468
  return statSync4(p).isDirectory();
@@ -13687,7 +14503,7 @@ function hasNodeModulesBin(entry) {
13687
14503
  }
13688
14504
  function sanitizedEnv(env, nodeDir, project, home, win) {
13689
14505
  const proj = canon(project, win);
13690
- const broad = proj === canon(home, win) || dirname11(proj) === proj;
14506
+ const broad = proj === canon(home, win) || dirname12(proj) === proj;
13691
14507
  const out = {};
13692
14508
  let pathKey = "PATH";
13693
14509
  let oldPath = "";
@@ -13724,12 +14540,12 @@ async function relaunchViaNpx(latest, deps) {
13724
14540
  if (!latest || !SEMVER.test(latest))
13725
14541
  return;
13726
14542
  const target = latest;
13727
- const nodeDir = dirname11(deps.execPath);
13728
- const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(dirname11(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
14543
+ const nodeDir = dirname12(deps.execPath);
14544
+ const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(dirname12(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
13729
14545
  const exists = deps.exists ?? existsSync6;
13730
14546
  const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
13731
14547
  const healthy = (cli) => {
13732
- const root = join12(dirname11(cli), "..");
14548
+ const root = join12(dirname12(cli), "..");
13733
14549
  return exists(join12(root, "package.json")) && exists(join12(root, "lib", "cli.js"));
13734
14550
  };
13735
14551
  let npxCliPath = npxCli;
@@ -13785,12 +14601,12 @@ async function relaunchViaNpx(latest, deps) {
13785
14601
 
13786
14602
  // src/run.ts
13787
14603
  import { createInterface as createInterface3 } from "node:readline/promises";
13788
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
13789
- import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
14604
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
14605
+ import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
13790
14606
 
13791
14607
  // src/onboarding.ts
13792
14608
  import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync as realpathSync6 } from "node:fs";
13793
- import { homedir as homedir7 } from "node:os";
14609
+ import { homedir as homedir8 } from "node:os";
13794
14610
  import { join as join13 } from "node:path";
13795
14611
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
13796
14612
  var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
@@ -13807,7 +14623,7 @@ ${INTRO_TEXT}
13807
14623
  }
13808
14624
  var FIRST_TASK_EXPERIMENT = "retain.first_task";
13809
14625
  var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile", "composer.json"];
13810
- function looksLikeProject(cwd, home = homedir7()) {
14626
+ function looksLikeProject(cwd, home = homedir8()) {
13811
14627
  try {
13812
14628
  if (realpathSync6(cwd) === realpathSync6(home))
13813
14629
  return false;
@@ -13829,7 +14645,7 @@ This folder doesn't look like a project. free-pi works best on code you
13829
14645
  already have: quit, "cd" into a project, and run "npx free-pi-cli@latest" there.
13830
14646
  Or just type: Create a small runnable hello-world app here and run it
13831
14647
  `.trim();
13832
- function showFirstTaskHint(log, cwd = process.cwd(), home = homedir7()) {
14648
+ function showFirstTaskHint(log, cwd = process.cwd(), home = homedir8()) {
13833
14649
  const text = looksLikeProject(cwd, home) ? FIRST_TASK_PROJECT_TEXT : FIRST_TASK_EMPTY_TEXT;
13834
14650
  log(`
13835
14651
  ${text}
@@ -13839,7 +14655,7 @@ ${text}
13839
14655
 
13840
14656
  // src/starter.ts
13841
14657
  import { existsSync as existsSync8, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
13842
- import { homedir as homedir8 } from "node:os";
14658
+ import { homedir as homedir9 } from "node:os";
13843
14659
  import { join as join14 } from "node:path";
13844
14660
  var STARTER_EXPERIMENT = "first_session_starter";
13845
14661
  var STARTER_PROJECT = [
@@ -13878,7 +14694,7 @@ function getStarterMarkerPath(agentDir = getFreePiAgentDir()) {
13878
14694
  function starterShownBefore(agentDir) {
13879
14695
  return existsSync8(getStarterMarkerPath(agentDir));
13880
14696
  }
13881
- function starterChoices(cwd = process.cwd(), home = homedir8()) {
14697
+ function starterChoices(cwd = process.cwd(), home = homedir9()) {
13882
14698
  if (looksLikeProject(cwd, home))
13883
14699
  return STARTER_PROJECT;
13884
14700
  try {
@@ -13905,12 +14721,12 @@ function parseStarterKey(raw, count) {
13905
14721
  }
13906
14722
 
13907
14723
  // src/whats-new.ts
13908
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
13909
- import { dirname as dirname12 } from "node:path";
14724
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
14725
+ import { dirname as dirname13 } from "node:path";
13910
14726
  function maybeWhatsNew(cliVersion, agentDir) {
13911
14727
  try {
13912
14728
  const path = getLastSeenVersionPath(agentDir);
13913
- const lastSeen = existsSync9(path) ? readFileSync5(path, "utf-8").trim() : undefined;
14729
+ const lastSeen = existsSync9(path) ? readFileSync6(path, "utf-8").trim() : undefined;
13914
14730
  writeLastSeen(path, cliVersion);
13915
14731
  if (lastSeen === undefined)
13916
14732
  return;
@@ -13926,8 +14742,8 @@ function maybeWhatsNew(cliVersion, agentDir) {
13926
14742
  }
13927
14743
  }
13928
14744
  function writeLastSeen(path, version) {
13929
- mkdirSync3(dirname12(path), { recursive: true });
13930
- writeFileSync3(path, version, "utf-8");
14745
+ mkdirSync4(dirname13(path), { recursive: true });
14746
+ writeFileSync4(path, version, "utf-8");
13931
14747
  }
13932
14748
 
13933
14749
  // src/run.ts
@@ -14016,7 +14832,7 @@ var SIGNIN_UNFINISHED_LINE = "Sign-in didn't finish. Run npx free-pi-cli again w
14016
14832
  var DECLINED_CALM_LINE = "OK, free-pi won't start. Run it again if you change your mind.";
14017
14833
  function validateSessionId(value) {
14018
14834
  try {
14019
- SessionManager2.inMemory(process.cwd(), { id: value });
14835
+ SessionManager3.inMemory(process.cwd(), { id: value });
14020
14836
  return;
14021
14837
  } catch (err) {
14022
14838
  return `Invalid --session value: ${err instanceof Error ? err.message : String(err)}`;
@@ -14067,6 +14883,13 @@ or: npm i -g free-pi-cli@latest`);
14067
14883
  }
14068
14884
  return outcome === "notice" ? { updateLatest: versionCheck.latest } : {};
14069
14885
  }
14886
+ function guestTrialNote(variant) {
14887
+ return isHandoffTreatment(variant) ? `
14888
+ You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
14889
+ ` : `
14890
+ You're on a short guest trial — ads pay for it. Sign in with GitHub anytime (/login) to get 15,000 free credits a day and unlock a one-time comeback bonus.
14891
+ `;
14892
+ }
14070
14893
  async function run(deps, argv = process.argv) {
14071
14894
  const resumeFlags = parseResumeFlags(argv);
14072
14895
  if (resumeFlags.sessionError) {
@@ -14104,6 +14927,12 @@ ${FINISH_SIGNIN_LINE}
14104
14927
  let startedGuestThisRun = false;
14105
14928
  let signedInAfterExpiry = false;
14106
14929
  let guestHandoffVariant;
14930
+ const startupNotes = [];
14931
+ const note = (text) => void startupNotes.push(text);
14932
+ const flushNotes = () => {
14933
+ for (const n of startupNotes.splice(0))
14934
+ deps.log(n);
14935
+ };
14107
14936
  let showFirstTask = false;
14108
14937
  let consentCurrent = false;
14109
14938
  if (jwt) {
@@ -14207,13 +15036,9 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
14207
15036
  jwt = guest.token;
14208
15037
  startedGuestThisRun = true;
14209
15038
  showIntro(deps.log);
14210
- deps.log(isHandoffTreatment(guest.guestHandoffVariant) ? `
14211
- You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
14212
- ` : `
14213
- You're on a short guest trial — ads pay for it. Sign in with GitHub anytime (/login) to get 15,000 free credits a day and unlock a one-time comeback bonus.
14214
- `);
15039
+ note(guestTrialNote(guest.guestHandoffVariant));
14215
15040
  if (guest.claimCode && guest.githubBonusCredits) {
14216
- deps.log(emphasize(formatGuestBonusBox(guest.claimCode, guest.githubBonusCredits).join(`
15041
+ note(emphasize(formatGuestBonusBox(guest.claimCode, guest.githubBonusCredits).join(`
14217
15042
  `)) + `
14218
15043
  `);
14219
15044
  }
@@ -14247,14 +15072,15 @@ ${GUEST_REFUSAL_LINE}`);
14247
15072
  }
14248
15073
  const whatsNew = maybeWhatsNew(CLI_VERSION, deps.agentDir);
14249
15074
  if (whatsNew)
14250
- deps.log(whatsNew);
15075
+ note(whatsNew);
14251
15076
  if (versionCheck.notice)
14252
- deps.log(versionCheck.notice);
15077
+ note(versionCheck.notice);
14253
15078
  let me = await fetchMe2(deps.baseUrl, jwt).catch(() => null);
14254
15079
  if (me?.guest && !startedGuestThisRun && canPrompt && consentCurrent) {
14255
15080
  try {
14256
15081
  const choice = await (deps.promptGuestSignin ?? promptGuestSigninInteractive)();
14257
15082
  if (choice === "aborted") {
15083
+ flushNotes();
14258
15084
  deps.log("Aborted.");
14259
15085
  return 1;
14260
15086
  }
@@ -14284,8 +15110,10 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
14284
15110
  if (me)
14285
15111
  updateAllowanceCache(me);
14286
15112
  guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
15113
+ if (me?.guest && !startedGuestThisRun)
15114
+ note(guestTrialNote(guestHandoffVariant));
14287
15115
  if (showFirstTask) {
14288
- showFirstTaskHint(deps.log);
15116
+ showFirstTaskHint(note);
14289
15117
  reportResumeExposure(deps, jwt, "offer_shown", FIRST_TASK_EXPERIMENT);
14290
15118
  }
14291
15119
  const startRefill = await fetchRefillStats(deps.baseUrl, jwt).catch(() => {
@@ -14294,14 +15122,14 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
14294
15122
  const refillSession = sessionFromStats(startRefill);
14295
15123
  const creditsBack = startRefill ? formatCreditsBackNudge(startRefill) : undefined;
14296
15124
  if (creditsBack) {
14297
- deps.log(`
15125
+ note(`
14298
15126
  ${creditsBack}`);
14299
15127
  reportRefillCardShown({ baseUrl: deps.baseUrl, jwt });
14300
15128
  }
14301
15129
  const redeemFlag = parseRedeemFlag(argv);
14302
15130
  if (redeemFlag.requested) {
14303
15131
  const r = await redeemCode({ baseUrl: deps.baseUrl, getToken: () => jwt, isGuest: () => Boolean(me?.guest) }, redeemFlag.code ?? "");
14304
- deps.log(`
15132
+ note(`
14305
15133
  ${r.text}
14306
15134
  `);
14307
15135
  }
@@ -14311,7 +15139,7 @@ ${r.text}
14311
15139
  if (!session && resumeFlags.continueLast && !resumeFlags.startNew) {
14312
15140
  session = (await lookupLatest(process.cwd(), deps.agentDir))?.id;
14313
15141
  if (!session)
14314
- deps.log("No earlier session in this folder; starting a new one.");
15142
+ note("No earlier session in this folder; starting a new one.");
14315
15143
  }
14316
15144
  const experiment = versionCheck.experiments?.[RESUME_ON_LAUNCH_EXPERIMENT];
14317
15145
  const variant = experiment?.variant === "treatment" ? "treatment" : "control";
@@ -14323,6 +15151,7 @@ ${r.text}
14323
15151
  reportResumeExposure(deps, jwt, "offer_shown");
14324
15152
  const answer = await (deps.promptResume ?? promptResumeInteractive)(latest);
14325
15153
  if (answer === "aborted") {
15154
+ flushNotes();
14326
15155
  deps.log("Aborted.");
14327
15156
  return 1;
14328
15157
  }
@@ -14334,11 +15163,12 @@ ${r.text}
14334
15163
  }
14335
15164
  let initialMessage;
14336
15165
  let hintShown = false;
15166
+ let importHintNote;
14337
15167
  if (importFlag.requested && deps.prepareImport) {
14338
15168
  if (resumeFlags.session || resumeFlags.continueLast) {
14339
- deps.log("--import starts a new session, so it is ignored together with --session or --continue.");
15169
+ note("--import starts a new session, so it is ignored together with --session or --continue.");
14340
15170
  } else if (!interactive) {
14341
- deps.log("--import needs an interactive terminal. Starting normally.");
15171
+ note("--import needs an interactive terminal. Starting normally.");
14342
15172
  } else {
14343
15173
  initialMessage = await deps.prepareImport({ source: importFlag.source, jwt, model: versionCheck.model }).catch(() => {
14344
15174
  return;
@@ -14352,7 +15182,7 @@ ${r.text}
14352
15182
  return;
14353
15183
  });
14354
15184
  if (hint) {
14355
- deps.log(hint);
15185
+ importHintNote = hint;
14356
15186
  hintShown = true;
14357
15187
  }
14358
15188
  }
@@ -14360,33 +15190,36 @@ ${r.text}
14360
15190
  const choices = starterChoices();
14361
15191
  if (choices) {
14362
15192
  try {
14363
- mkdirSync4(deps.agentDir, { recursive: true });
14364
- writeFileSync4(getStarterMarkerPath(deps.agentDir), "1");
15193
+ mkdirSync5(deps.agentDir, { recursive: true });
15194
+ writeFileSync5(getStarterMarkerPath(deps.agentDir), "1");
14365
15195
  } catch {}
14366
15196
  const report = (event) => deps.reportStarterEvent ? deps.reportStarterEvent(event) : reportStarterEvent(deps, jwt, event);
14367
15197
  deps.log(formatStarterMenu(choices));
14368
15198
  report("starter_shown");
14369
15199
  const picked = parseStarterKey(await (deps.askStarterKey ?? (() => readOneKeypress("")))(), choices.length);
14370
15200
  if (picked === "abort") {
15201
+ flushNotes();
14371
15202
  deps.log("Aborted.");
14372
15203
  return 1;
14373
15204
  }
14374
15205
  if (picked !== "dismiss") {
14375
15206
  report("starter_picked");
14376
15207
  initialMessage = choices[picked].prompt;
14377
- deps.log(`Sending: ${choices[picked].label}
15208
+ note(`Sending: ${choices[picked].label}
14378
15209
  `);
14379
15210
  }
14380
15211
  }
14381
15212
  }
14382
- if (deps.startupTip && !hintShown && !importFlag.requested && canPrompt && hadJwt) {
15213
+ if (deps.startupTip && !importFlag.requested && canPrompt && hadJwt) {
14383
15214
  try {
14384
- const importAvailable = await deps.hasImportableSession?.().catch(() => false) ?? false;
15215
+ const importAvailable = !hintShown && (await deps.hasImportableSession?.().catch(() => false) ?? false);
14385
15216
  const tip = deps.startupTip({ importAvailable });
14386
15217
  if (tip)
14387
- deps.log(tip);
15218
+ note(tip);
14388
15219
  } catch {}
14389
15220
  }
15221
+ if (importHintNote)
15222
+ note(importHintNote);
14390
15223
  const finalSessionId = await deps.launchPi({
14391
15224
  baseUrl: deps.baseUrl,
14392
15225
  jwt,
@@ -14403,7 +15236,9 @@ ${r.text}
14403
15236
  refillCard: refillSession,
14404
15237
  allowanceLine: allowanceLine(),
14405
15238
  guestHandoffVariant,
14406
- initialMessage
15239
+ choiceArm: me?.choice_prompts_arm,
15240
+ initialMessage,
15241
+ startupNotes: startupNotes.splice(0)
14407
15242
  });
14408
15243
  const endRefill = await fetchRefillStats(deps.baseUrl, jwt).catch(() => {
14409
15244
  return;
@@ -14509,7 +15344,7 @@ async function main() {
14509
15344
  }
14510
15345
  if (process.env.FREEPI_RELAUNCH_READY) {
14511
15346
  try {
14512
- writeFileSync5(process.env.FREEPI_RELAUNCH_READY, "");
15347
+ writeFileSync6(process.env.FREEPI_RELAUNCH_READY, "");
14513
15348
  } catch {}
14514
15349
  }
14515
15350
  const command = parseCliArgs(process.argv.slice(2));
@@ -14551,7 +15386,7 @@ ${HELP_TEXT}`);
14551
15386
  baseUrl,
14552
15387
  prepareImport: ({ source, jwt, model }) => prepareStartupImport({
14553
15388
  cwd: process.cwd(),
14554
- home: homedir9(),
15389
+ home: homedir10(),
14555
15390
  baseUrl,
14556
15391
  jwt,
14557
15392
  model,
@@ -14560,8 +15395,8 @@ ${HELP_TEXT}`);
14560
15395
  askKey: (prompt) => readOneKeypress(prompt)
14561
15396
  }),
14562
15397
  startupTip: ({ importAvailable }) => maybeTip({ agentDir, importAvailable }),
14563
- hasImportableSession: async () => (await listFolderSessions({ cwd: process.cwd(), home: homedir9(), limit: 1 })).length > 0,
14564
- importHint: (latestFreePi) => startupImportHint({ cwd: process.cwd(), home: homedir9(), now: new Date, latestFreePi }),
15398
+ hasImportableSession: async () => (await listFolderSessions({ cwd: process.cwd(), home: homedir10(), limit: 1 })).length > 0,
15399
+ importHint: (latestFreePi) => startupImportHint({ cwd: process.cwd(), home: homedir10(), now: new Date, latestFreePi }),
14565
15400
  agentDir,
14566
15401
  credentialsPath: getCredentialsPath(agentDir),
14567
15402
  promptConsent: promptConsentInteractive,
@@ -14570,13 +15405,13 @@ ${HELP_TEXT}`);
14570
15405
  launchPi,
14571
15406
  log: (message) => console.log(message),
14572
15407
  checkClientVersion: (baseUrl, cliVersion, jwt) => checkClientVersion(baseUrl, cliVersion, fetch, jwt),
14573
- startGravityPixel: (jwt) => startGravityPixel(jwt, join15(dirname13(agentDir), "gravity")),
15408
+ startGravityPixel: (jwt) => startGravityPixel(jwt, join15(dirname14(agentDir), "gravity")),
14574
15409
  relaunchLatest: (latest) => relaunchViaNpx(latest, {
14575
15410
  env: process.env,
14576
15411
  argv: process.argv.slice(2),
14577
15412
  platform: process.platform,
14578
15413
  cwd: process.cwd(),
14579
- homedir: homedir9(),
15414
+ homedir: homedir10(),
14580
15415
  execPath: process.execPath,
14581
15416
  autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),
14582
15417
  spawn: (cmd, args, opts) => spawn3(cmd, args, opts),