free-pi-cli 0.4.2 → 0.4.4

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,6 +2,7 @@
2
2
 
3
3
  // src/main.ts
4
4
  import { realpathSync } from "node:fs";
5
+ import { dirname as dirname5, join as join5 } from "node:path";
5
6
 
6
7
  // src/browser.ts
7
8
  import { spawn } from "node:child_process";
@@ -44,7 +45,7 @@ function openBrowser(url, platform = process.platform, spawnImpl = spawn) {
44
45
  // package.json
45
46
  var package_default = {
46
47
  name: "free-pi-cli",
47
- version: "0.4.2",
48
+ version: "0.4.4",
48
49
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
49
50
  license: "MIT",
50
51
  type: "module",
@@ -53,6 +54,7 @@ var package_default = {
53
54
  },
54
55
  files: [
55
56
  "dist",
57
+ "skills",
56
58
  "README.md"
57
59
  ],
58
60
  engines: {
@@ -63,7 +65,8 @@ var package_default = {
63
65
  prepublishOnly: "bun run build"
64
66
  },
65
67
  dependencies: {
66
- "@earendil-works/pi-coding-agent": "1.2.0"
68
+ "@earendil-works/pi-coding-agent": "1.2.0",
69
+ "@gravity-ai/api": "1.2.1"
67
70
  },
68
71
  devDependencies: {
69
72
  "@freepi/pi-ads": "workspace:*",
@@ -91,6 +94,7 @@ Usage:
91
94
  Environment:
92
95
  FREEPI_BASE_URL override the backend base URL (default https://api.freepi.ai)
93
96
  FREEPI_NO_AUTO_UPDATE disable in-place self-update for this run
97
+ FREEPI_NO_GRAVITY_PIXEL=1 do not run Gravity's ad-measurement pixel
94
98
  `.trim();
95
99
  function versionLine() {
96
100
  return `free-pi-cli ${CLI_VERSION}`;
@@ -221,18 +225,8 @@ function getConfigPath() {
221
225
  return join2(homedir(), FREE_PI_DIR_NAME, "config.json");
222
226
  }
223
227
 
224
- // src/pi-launch.ts
225
- import {
226
- createAgentSessionFromServices,
227
- createAgentSessionRuntime,
228
- createAgentSessionServices,
229
- createCodemodeExtension,
230
- createMcpExtension,
231
- createToolSearchExtension,
232
- InteractiveMode,
233
- SessionManager,
234
- SettingsManager
235
- } from "@earendil-works/pi-coding-agent";
228
+ // src/gravity-pixel.ts
229
+ import { createHash } from "node:crypto";
236
230
 
237
231
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/core/core.js
238
232
  var _a;
@@ -4828,6 +4822,7 @@ var MeResponseSchema = object({
4828
4822
  cap_usd_today: number2(),
4829
4823
  tier: AccountTierSchema,
4830
4824
  credit_usd: number2(),
4825
+ consent_version: string2().nullable().optional(),
4831
4826
  notice: string2().optional(),
4832
4827
  buy_url: string2().optional(),
4833
4828
  allowance_credits: number2().optional(),
@@ -4874,11 +4869,16 @@ var AdNextResponseSchema = object({
4874
4869
  ad_id: string2(),
4875
4870
  click_token: string2(),
4876
4871
  creative: AdCreativeSchema,
4877
- click_url: string2()
4872
+ click_url: string2(),
4873
+ source: _enum(["gravity", "own"]).optional(),
4874
+ imp_url: string2().optional()
4878
4875
  });
4876
+ var GRAVITY_CONTEXT_HEADER = "x-gravity-context";
4877
+ var GRAVITY_CONTEXT_MAX_BYTES = 8192;
4879
4878
  var AdImpressionRequestSchema = object({
4880
4879
  ad_id: string2(),
4881
- click_token: string2()
4880
+ click_token: string2(),
4881
+ imp_fired: boolean2().optional()
4882
4882
  });
4883
4883
  var AdminAdRequestSchema = object({
4884
4884
  slot: AdSlotSchema,
@@ -5085,8 +5085,85 @@ function formatCreditsLeftLine(remaining, allowance, refillAt) {
5085
5085
  return `${left} / ${total} credits left today · refills ${formatRefillEt(refillAt)}`;
5086
5086
  }
5087
5087
  var SLOW_LANE_METER_LINE = "Slow lane: cheapest model until refill";
5088
+ // src/gravity-pixel.ts
5089
+ var GRAVITY_PIXEL_ID = "6cd74804-0541-4d9b-899d-d7fceb465b6f";
5090
+ function userIdFromJwt(jwt) {
5091
+ try {
5092
+ const payload = JSON.parse(Buffer.from(jwt.split(".")[1] ?? "", "base64url").toString("utf8"));
5093
+ return typeof payload.sub === "string" ? payload.sub : undefined;
5094
+ } catch {
5095
+ return;
5096
+ }
5097
+ }
5098
+ function dropPixelSignalHandlers() {
5099
+ const onNew = (event, fn) => {
5100
+ if (event !== "SIGINT" && event !== "SIGTERM" || typeof fn !== "function")
5101
+ return;
5102
+ if (!/shutdownSync/.test(Function.prototype.toString.call(fn)))
5103
+ return;
5104
+ queueMicrotask(() => process.removeListener(event, fn));
5105
+ };
5106
+ process.on("newListener", onNew);
5107
+ return () => {
5108
+ process.removeListener("newListener", onNew);
5109
+ };
5110
+ }
5111
+ function startGravityPixel(jwt, storageDir, env = process.env, load = () => import("./index-xkn61g24.js")) {
5112
+ if (env.FREEPI_NO_GRAVITY_PIXEL === "1")
5113
+ return;
5114
+ const userId = userIdFromJwt(jwt);
5115
+ if (!userId)
5116
+ return;
5117
+ const userHash = createHash("sha256").update(userId).digest("hex");
5118
+ let sdk;
5119
+ let pixel;
5120
+ (async () => {
5121
+ try {
5122
+ sdk = await load();
5123
+ const p = sdk.gravityPixel({ pixelId: GRAVITY_PIXEL_ID, appName: "free-pi", storageDir });
5124
+ pixel = p;
5125
+ const stopWatching = dropPixelSignalHandlers();
5126
+ try {
5127
+ await p.start();
5128
+ } finally {
5129
+ stopWatching();
5130
+ }
5131
+ } catch {
5132
+ pixel = undefined;
5133
+ }
5134
+ })();
5135
+ return () => {
5136
+ try {
5137
+ const sessionId = pixel?.getSessionId();
5138
+ if (!sdk || !pixel || !sessionId)
5139
+ return;
5140
+ const ctx = sdk.gravityContext({
5141
+ sessionId,
5142
+ user: { userId: userHash, gruid: pixel.getVisitorId(), grclid: pixel.getClickId(), graid: pixel.getGraid() }
5143
+ });
5144
+ const encoded = Buffer.from(JSON.stringify(ctx)).toString("base64url");
5145
+ return encoded.length <= GRAVITY_CONTEXT_MAX_BYTES ? encoded : undefined;
5146
+ } catch {
5147
+ return;
5148
+ }
5149
+ };
5150
+ }
5151
+
5152
+ // src/pi-launch.ts
5153
+ import {
5154
+ createAgentSessionFromServices,
5155
+ createAgentSessionRuntime,
5156
+ createAgentSessionServices,
5157
+ createCodemodeExtension,
5158
+ createMcpExtension,
5159
+ createToolSearchExtension,
5160
+ InteractiveMode,
5161
+ SessionManager,
5162
+ SettingsManager
5163
+ } from "@earendil-works/pi-coding-agent";
5164
+
5088
5165
  // ../pi-ads/src/api.ts
5089
- var REQUEST_TIMEOUT_MS = 3000;
5166
+ var REQUEST_TIMEOUT_MS = 5000;
5090
5167
  async function authedFetch(deps, path, init) {
5091
5168
  const token = await deps.getToken();
5092
5169
  const doFetch = deps.fetchImpl ?? fetch;
@@ -5098,10 +5175,38 @@ async function authedFetch(deps, path, init) {
5098
5175
  ...init?.headers ?? {},
5099
5176
  Authorization: `Bearer ${token}`,
5100
5177
  "Content-Type": "application/json",
5101
- ...deps.sessionId && (path.startsWith("/ads/next") || path === "/ads/impression") ? { "x-session-id": deps.sessionId } : {}
5178
+ ...deps.sessionId && (path.startsWith("/ads/next") || path === "/ads/impression") ? { "x-session-id": deps.sessionId } : {},
5179
+ ...path.startsWith("/ads/next") ? gravityHeader(deps) : {}
5102
5180
  }
5103
5181
  });
5104
5182
  }
5183
+ function gravityHeader(deps) {
5184
+ try {
5185
+ const ctx = deps.getGravityContext?.();
5186
+ return ctx ? { [GRAVITY_CONTEXT_HEADER]: ctx } : {};
5187
+ } catch {
5188
+ return {};
5189
+ }
5190
+ }
5191
+ var IMP_TIMEOUT_MS = 3000;
5192
+ function isGravityImpUrl(raw) {
5193
+ if (!raw)
5194
+ return false;
5195
+ try {
5196
+ const u = new URL(raw);
5197
+ return u.protocol === "https:" && /(^|\.)trygravity\.(ai|com)$/.test(u.hostname);
5198
+ } catch {
5199
+ return false;
5200
+ }
5201
+ }
5202
+ function fireGravityImpression(deps, ad) {
5203
+ if (ad.source !== "gravity" || !isGravityImpUrl(ad.imp_url))
5204
+ return false;
5205
+ try {
5206
+ Promise.resolve((deps.fetchImpl ?? fetch)(ad.imp_url, { method: "GET", signal: AbortSignal.timeout(IMP_TIMEOUT_MS) })).catch(() => {});
5207
+ } catch {}
5208
+ return true;
5209
+ }
5105
5210
  async function fetchAdNext(deps, slot) {
5106
5211
  try {
5107
5212
  const res = await authedFetch(deps, `/ads/next?slot=${slot}`);
@@ -5112,11 +5217,11 @@ async function fetchAdNext(deps, slot) {
5112
5217
  return;
5113
5218
  }
5114
5219
  }
5115
- async function postImpression(deps, adId, clickToken) {
5220
+ async function postImpression(deps, adId, clickToken, impFired = false) {
5116
5221
  try {
5117
5222
  await authedFetch(deps, "/ads/impression", {
5118
5223
  method: "POST",
5119
- body: JSON.stringify({ ad_id: adId, click_token: clickToken })
5224
+ body: JSON.stringify({ ad_id: adId, click_token: clickToken, ...impFired ? { imp_fired: true } : {} })
5120
5225
  });
5121
5226
  } catch {}
5122
5227
  }
@@ -5132,7 +5237,7 @@ async function fetchMe(deps) {
5132
5237
  }
5133
5238
 
5134
5239
  // ../pi-ads/src/style.ts
5135
- import { createHash } from "node:crypto";
5240
+ import { createHash as createHash2 } from "node:crypto";
5136
5241
  var OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
5137
5242
  var CSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
5138
5243
  var C1_RE = /[\x80-\x9f]/g;
@@ -5165,7 +5270,7 @@ function renderAdCard(creative, clickUrl, theme, columns) {
5165
5270
  const label = " AD ░▒▓ ";
5166
5271
  const top = `┌${label}${"─".repeat(Math.max(0, innerWidth + 2 - label.length))}┐`;
5167
5272
  const bottom = `└${"─".repeat(innerWidth + 2)}┘`;
5168
- const linkId = createHash("sha256").update(url).digest("hex").slice(0, 12);
5273
+ const linkId = createHash2("sha256").update(url).digest("hex").slice(0, 12);
5169
5274
  const row = (text, opts) => {
5170
5275
  const plain = pad(truncate(text, innerWidth), innerWidth);
5171
5276
  let styled = theme.fg("accent", plain);
@@ -5199,14 +5304,33 @@ function renderWarningLine(message, theme) {
5199
5304
  var BANNER_WIDGET_ID = "freepi-ad";
5200
5305
  function createBannerRenderer(opts = {}) {
5201
5306
  let lastPostedToken;
5307
+ const screen = opts.screen ?? {};
5202
5308
  return {
5203
5309
  async render(deps, ctx, config, columnsOverride) {
5204
5310
  const ad = await fetchAdNext(deps, "banner");
5311
+ if (opts.isActive && !opts.isActive())
5312
+ return;
5205
5313
  if (!ad) {
5314
+ screen.banner = undefined;
5206
5315
  ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
5207
5316
  opts.onBannerAd?.(undefined);
5208
5317
  return;
5209
5318
  }
5319
+ const isGravity = ad.source === "gravity";
5320
+ if (screen.inline === "gravity") {
5321
+ screen.banner = undefined;
5322
+ ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
5323
+ opts.onBannerAd?.(undefined);
5324
+ return;
5325
+ }
5326
+ if (isGravity && screen.inline)
5327
+ screen.clearInline?.();
5328
+ screen.banner = isGravity ? "gravity" : "own";
5329
+ screen.clearBanner = () => {
5330
+ screen.banner = undefined;
5331
+ ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
5332
+ opts.onBannerAd?.(undefined);
5333
+ };
5210
5334
  const theme = ctx.ui.theme;
5211
5335
  const columns = columnsOverride ?? process.stdout.columns ?? 80;
5212
5336
  const lines = columns >= config.adMinColumns ? renderAdCard(ad.creative, ad.click_url, theme, columns) : [renderPlainAdLine(ad.creative, ad.click_url, theme, columns)];
@@ -5214,7 +5338,8 @@ function createBannerRenderer(opts = {}) {
5214
5338
  opts.onBannerAd?.(ad.click_url);
5215
5339
  if (lastPostedToken !== ad.click_token) {
5216
5340
  lastPostedToken = ad.click_token;
5217
- await postImpression(deps, ad.ad_id, ad.click_token);
5341
+ const impFired = fireGravityImpression(deps, ad);
5342
+ await postImpression(deps, ad.ad_id, ad.click_token, impFired);
5218
5343
  }
5219
5344
  }
5220
5345
  };
@@ -5222,37 +5347,48 @@ function createBannerRenderer(opts = {}) {
5222
5347
 
5223
5348
  // ../pi-ads/src/inline.ts
5224
5349
  var INLINE_WIDGET_ID = "freepi-ad-inline";
5225
- function createInlineRenderer() {
5350
+ function createInlineRenderer(screen = {}) {
5226
5351
  let turnCount = 0;
5227
5352
  let inlineVisible = false;
5228
5353
  let lastPostedToken;
5354
+ const clearInline = (ctx) => {
5355
+ if (inlineVisible)
5356
+ ctx.ui.setWidget(INLINE_WIDGET_ID, undefined);
5357
+ inlineVisible = false;
5358
+ screen.inline = undefined;
5359
+ };
5229
5360
  return {
5230
5361
  async onTurnEnd(deps, ctx, config, columnsOverride) {
5231
5362
  turnCount += 1;
5232
- if (inlineVisible) {
5233
- ctx.ui.setWidget(INLINE_WIDGET_ID, undefined);
5234
- inlineVisible = false;
5235
- }
5363
+ clearInline(ctx);
5236
5364
  if (turnCount % config.adInlineTurnFrequency !== 0)
5237
5365
  return;
5366
+ if (screen.banner === "gravity")
5367
+ return;
5238
5368
  const ad = await fetchAdNext(deps, "inline");
5239
5369
  if (!ad)
5240
5370
  return;
5371
+ const isGravity = ad.source === "gravity";
5372
+ if (screen.banner === "gravity")
5373
+ return;
5374
+ if (isGravity && screen.banner)
5375
+ screen.clearBanner?.();
5241
5376
  const theme = ctx.ui.theme;
5242
5377
  const columns = columnsOverride ?? process.stdout.columns ?? 80;
5243
5378
  const lines = columns >= config.adMinColumns ? renderAdCard(ad.creative, ad.click_url, theme, columns) : [renderPlainAdLine(ad.creative, ad.click_url, theme, columns)];
5244
5379
  ctx.ui.setWidget(INLINE_WIDGET_ID, lines, { placement: "belowEditor" });
5245
5380
  inlineVisible = true;
5381
+ screen.inline = isGravity ? "gravity" : "own";
5382
+ screen.clearInline = () => clearInline(ctx);
5246
5383
  if (lastPostedToken !== ad.click_token) {
5247
5384
  lastPostedToken = ad.click_token;
5248
- await postImpression(deps, ad.ad_id, ad.click_token);
5385
+ const impFired = fireGravityImpression(deps, ad);
5386
+ await postImpression(deps, ad.ad_id, ad.click_token, impFired);
5249
5387
  }
5250
5388
  },
5251
5389
  reset(ctx) {
5252
5390
  turnCount = 0;
5253
- if (inlineVisible)
5254
- ctx.ui.setWidget(INLINE_WIDGET_ID, undefined);
5255
- inlineVisible = false;
5391
+ clearInline(ctx);
5256
5392
  }
5257
5393
  };
5258
5394
  }
@@ -5361,34 +5497,56 @@ async function renderMeter(deps, ctx) {
5361
5497
 
5362
5498
  // ../pi-ads/src/index.ts
5363
5499
  var BANNER_REFRESH_MS = 10 * 60 * 1000;
5500
+ var BANNER_MIN_REFRESH_MS = 60 * 1000;
5364
5501
  function createAdsExtension(opts) {
5365
5502
  const deps = {
5366
5503
  baseUrl: opts.baseUrl,
5367
5504
  getToken: opts.getToken,
5368
5505
  sessionId: opts.sessionId,
5369
- fetchImpl: opts.fetchImpl
5506
+ fetchImpl: opts.fetchImpl,
5507
+ getGravityContext: opts.getGravityContext
5370
5508
  };
5371
5509
  const config = loadConfig();
5372
5510
  return {
5373
5511
  name: "free-pi-ads",
5374
5512
  factory(pi) {
5375
- const banner = createBannerRenderer({ onBannerAd: opts.onBannerAd });
5376
- const inline = createInlineRenderer();
5513
+ let stopped = false;
5514
+ const screen = {};
5515
+ const banner = createBannerRenderer({ onBannerAd: opts.onBannerAd, isActive: () => !stopped, screen });
5516
+ const inline = createInlineRenderer(screen);
5377
5517
  let bannerInterval;
5518
+ let lastBannerAt = 0;
5519
+ let bannerInFlight = false;
5520
+ const renderBanner = async (ctx, force = false) => {
5521
+ if (stopped)
5522
+ return;
5523
+ if (!force && (bannerInFlight || Date.now() - lastBannerAt < BANNER_MIN_REFRESH_MS))
5524
+ return;
5525
+ bannerInFlight = true;
5526
+ try {
5527
+ await banner.render(deps, ctx, config);
5528
+ } finally {
5529
+ bannerInFlight = false;
5530
+ lastBannerAt = Date.now();
5531
+ }
5532
+ };
5378
5533
  pi.on("session_start", async (_event, ctx) => {
5534
+ stopped = false;
5379
5535
  inline.reset(ctx);
5380
- await Promise.all([banner.render(deps, ctx, config), renderMeter(deps, ctx)]);
5536
+ await Promise.all([renderBanner(ctx, true), renderMeter(deps, ctx)]);
5381
5537
  if (bannerInterval)
5382
5538
  clearInterval(bannerInterval);
5383
5539
  bannerInterval = setInterval(() => {
5384
- banner.render(deps, ctx, config);
5540
+ renderBanner(ctx);
5385
5541
  }, BANNER_REFRESH_MS);
5386
5542
  bannerInterval.unref?.();
5387
5543
  });
5388
5544
  pi.on("turn_end", async (_event, ctx) => {
5545
+ renderBanner(ctx);
5389
5546
  await Promise.all([inline.onTurnEnd(deps, ctx, config), renderMeter(deps, ctx)]);
5390
5547
  });
5391
5548
  pi.on("session_shutdown", () => {
5549
+ stopped = true;
5392
5550
  if (bannerInterval) {
5393
5551
  clearInterval(bannerInterval);
5394
5552
  bannerInterval = undefined;
@@ -5399,7 +5557,8 @@ function createAdsExtension(opts) {
5399
5557
  }
5400
5558
 
5401
5559
  // src/pi-launch.ts
5402
- import { join as join3, resolve as resolve2 } from "node:path";
5560
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
5561
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
5403
5562
 
5404
5563
  // src/provider.ts
5405
5564
  var PROVIDER_NAME = "free-pi";
@@ -5428,7 +5587,7 @@ function buildProviderConfig(baseUrl, jwt, sessionId, models) {
5428
5587
  };
5429
5588
  }
5430
5589
 
5431
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/memory/metrics.mjs
5590
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
5432
5591
  var Metrics = {
5433
5592
  assign: 0,
5434
5593
  create: 0,
@@ -5436,7 +5595,8 @@ var Metrics = {
5436
5595
  discard: 0,
5437
5596
  update: 0
5438
5597
  };
5439
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/guard/guard.mjs
5598
+
5599
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/guard/guard.mjs
5440
5600
  function IsArray(value) {
5441
5601
  return Array.isArray(value);
5442
5602
  }
@@ -5481,7 +5641,7 @@ function Keys(value) {
5481
5641
  function Symbols(value) {
5482
5642
  return Object.getOwnPropertySymbols(value);
5483
5643
  }
5484
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/guard/globals.mjs
5644
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/guard/globals.mjs
5485
5645
  function IsTypeArray(value) {
5486
5646
  return globalThis.ArrayBuffer.isView(value);
5487
5647
  }
@@ -5494,7 +5654,25 @@ function IsSet(value) {
5494
5654
  function IsMap(value) {
5495
5655
  return value instanceof globalThis.Map;
5496
5656
  }
5497
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/memory/clone.mjs
5657
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/settings/settings.mjs
5658
+ var settings = {
5659
+ immutableTypes: false,
5660
+ maxErrors: 8,
5661
+ maxInstantiationCount: 128,
5662
+ useAcceleration: true,
5663
+ exactOptionalPropertyTypes: false,
5664
+ enumerableKind: false,
5665
+ correctiveParse: false,
5666
+ unionPrioritySort: true
5667
+ };
5668
+ function Get() {
5669
+ return settings;
5670
+ }
5671
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/freeze.mjs
5672
+ function Freeze(value) {
5673
+ return Get().immutableTypes ? Object.freeze(value) : value;
5674
+ }
5675
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/clone.mjs
5498
5676
  function FromClassInstance(value) {
5499
5677
  return value;
5500
5678
  }
@@ -5553,21 +5731,7 @@ function Clone(value) {
5553
5731
  Metrics.clone += 1;
5554
5732
  return FromValue(value);
5555
5733
  }
5556
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/settings/settings.mjs
5557
- var settings = {
5558
- immutableTypes: false,
5559
- maxErrors: 8,
5560
- maxInstantiationCount: 128,
5561
- useAcceleration: true,
5562
- exactOptionalPropertyTypes: false,
5563
- enumerableKind: false,
5564
- correctiveParse: false,
5565
- unionPrioritySort: true
5566
- };
5567
- function Get() {
5568
- return settings;
5569
- }
5570
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/memory/create.mjs
5734
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/create.mjs
5571
5735
  function MergeHidden(left, right) {
5572
5736
  for (const key of Object.keys(right)) {
5573
5737
  Object.defineProperty(left, key, {
@@ -5584,12 +5748,11 @@ function Merge(left, right) {
5584
5748
  }
5585
5749
  function Create(hidden, enumerable, options = {}) {
5586
5750
  Metrics.create += 1;
5587
- const settings = Get();
5588
5751
  const withOptions = Merge(enumerable, options);
5589
- const withHidden = settings.enumerableKind ? Merge(withOptions, hidden) : MergeHidden(withOptions, hidden);
5590
- return settings.immutableTypes ? Object.freeze(withHidden) : withHidden;
5752
+ const withHidden = Get().enumerableKind ? Merge(withOptions, hidden) : MergeHidden(withOptions, hidden);
5753
+ return Freeze(withHidden);
5591
5754
  }
5592
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/memory/update.mjs
5755
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/update.mjs
5593
5756
  function Update(current, hidden, enumerable) {
5594
5757
  Metrics.update += 1;
5595
5758
  const settings = Get();
@@ -5610,14 +5773,14 @@ function Update(current, hidden, enumerable) {
5610
5773
  value: enumerable[key]
5611
5774
  });
5612
5775
  }
5613
- return result;
5776
+ return Freeze(result);
5614
5777
  }
5615
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/schema.mjs
5778
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/schema.mjs
5616
5779
  function IsSchema(value) {
5617
5780
  return IsObject(value);
5618
5781
  }
5619
5782
 
5620
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/engine/optional/instantiate_add.mjs
5783
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/optional/instantiate_add.mjs
5621
5784
  function AddOptionalOperation(type) {
5622
5785
  return Update(type, { "~optional": true }, {});
5623
5786
  }
@@ -5626,12 +5789,12 @@ function AddOptionalAction(type, options) {
5626
5789
  return result;
5627
5790
  }
5628
5791
 
5629
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/action/_add_optional.mjs
5792
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/action/_add_optional.mjs
5630
5793
  function AddOptional(type, options = {}) {
5631
5794
  return AddOptionalAction(type, options);
5632
5795
  }
5633
5796
 
5634
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/_optional.mjs
5797
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/_optional.mjs
5635
5798
  function Optional(type) {
5636
5799
  return AddOptional(type);
5637
5800
  }
@@ -5639,18 +5802,18 @@ function IsOptional(value) {
5639
5802
  return IsSchema(value) && HasPropertyKey(value, "~optional");
5640
5803
  }
5641
5804
 
5642
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/properties.mjs
5805
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/properties.mjs
5643
5806
  function RequiredArray(properties) {
5644
5807
  return Keys(properties).filter((key) => !IsOptional(properties[key]));
5645
5808
  }
5646
5809
 
5647
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/object.mjs
5810
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/object.mjs
5648
5811
  function _Object_(properties, options = {}) {
5649
5812
  const requiredKeys = RequiredArray(properties);
5650
5813
  const required = requiredKeys.length > 0 ? { required: requiredKeys } : {};
5651
5814
  return Create({ "~kind": "Object" }, { type: "object", ...required, properties }, options);
5652
5815
  }
5653
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/hashing/hash.mjs
5816
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/hashing/hash.mjs
5654
5817
  var ByteMarker;
5655
5818
  (function(ByteMarker) {
5656
5819
  ByteMarker[ByteMarker["Array"] = 0] = "Array";
@@ -5675,9 +5838,9 @@ var F64 = new Float64Array(1);
5675
5838
  var F64In = new DataView(F64.buffer);
5676
5839
  var F64Out = new Uint8Array(F64.buffer);
5677
5840
  var encoder = new TextEncoder;
5678
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/integer.mjs
5841
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/integer.mjs
5679
5842
  var IntegerPattern = "-?(?:0|[1-9][0-9]*)";
5680
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/literal.mjs
5843
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/literal.mjs
5681
5844
  class InvalidLiteralValue extends Error {
5682
5845
  constructor(value) {
5683
5846
  super(`Invalid Literal value`);
@@ -5697,27 +5860,27 @@ function LiteralTypeName(value) {
5697
5860
  function Literal(value, options) {
5698
5861
  return Create({ "~kind": "Literal" }, { type: LiteralTypeName(value), const: value }, options);
5699
5862
  }
5700
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/number.mjs
5863
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/number.mjs
5701
5864
  var NumberPattern = "-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?";
5702
5865
  function Number2(options) {
5703
5866
  return Create({ "~kind": "Number" }, { type: "number" }, options);
5704
5867
  }
5705
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/string.mjs
5868
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/string.mjs
5706
5869
  var StringPattern = ".*";
5707
5870
  function String2(options) {
5708
5871
  return Create({ "~kind": "String" }, { type: "string" }, options);
5709
5872
  }
5710
5873
 
5711
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/union.mjs
5874
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/union.mjs
5712
5875
  function Union(anyOf, options = {}) {
5713
5876
  return Create({ "~kind": "Union" }, { anyOf }, options);
5714
5877
  }
5715
5878
 
5716
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/record.mjs
5879
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/record.mjs
5717
5880
  var IntegerKey = `^${IntegerPattern}$`;
5718
5881
  var NumberKey = `^${NumberPattern}$`;
5719
5882
  var StringKey = `^${StringPattern}$`;
5720
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/script/token/internal/char.mjs
5883
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/script/token/internal/char.mjs
5721
5884
  function Range(start, end) {
5722
5885
  return Array.from({ length: end - start + 1 }, (_, i) => String.fromCharCode(start + i));
5723
5886
  }
@@ -5731,17 +5894,17 @@ var Digit = [Zero, ...NonZero];
5731
5894
  var UnderScore = "_";
5732
5895
  var DollarSign = "$";
5733
5896
 
5734
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/script/token/unsigned_integer.mjs
5897
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/script/token/unsigned_integer.mjs
5735
5898
  var AllowedDigits = [...Digit, UnderScore];
5736
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/script/token/ident.mjs
5899
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/script/token/ident.mjs
5737
5900
  var Initial = [...Alpha, UnderScore, DollarSign];
5738
5901
  var Remaining = [...Initial, ...Digit];
5739
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/script/token/unsigned_number.mjs
5902
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/script/token/unsigned_number.mjs
5740
5903
  var AllowedDigits2 = [...Digit, UnderScore];
5741
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/engine/helpers/keys.mjs
5904
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/helpers/keys.mjs
5742
5905
  var integerKeyPattern = new RegExp("^(?:0|[1-9][0-9]*)$");
5743
5906
 
5744
- // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/engine/indexed/from_object.mjs
5907
+ // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/indexed/from_object.mjs
5745
5908
  var NumericKeyPattern = new RegExp(IntegerKey);
5746
5909
  // src/usage-tool.ts
5747
5910
  var USAGE_TOOL_NAME = "free_pi_usage";
@@ -6211,6 +6374,64 @@ function createTodoToolExtension(options = {}) {
6211
6374
  };
6212
6375
  }
6213
6376
 
6377
+ // src/tool-aliases.ts
6378
+ import { homedir as homedir2 } from "node:os";
6379
+ import { join as join3 } from "node:path";
6380
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6381
+ import {
6382
+ createBashToolDefinition,
6383
+ createReadToolDefinition
6384
+ } from "@earendil-works/pi-coding-agent";
6385
+ function aliasArgs(input, key, aliases) {
6386
+ if (!input || typeof input !== "object" || key in input)
6387
+ return input;
6388
+ const args = input;
6389
+ const alias = aliases.find((a) => (a in args));
6390
+ if (!alias)
6391
+ return input;
6392
+ const { [alias]: value, ...rest } = args;
6393
+ return { ...rest, [key]: value };
6394
+ }
6395
+ function normalizeShellPath(input, platform = process.platform, home = homedir2()) {
6396
+ let p = input;
6397
+ if (platform === "win32" && p.startsWith("/") && !p.startsWith("//") && !p.includes("\\")) {
6398
+ const m = p.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
6399
+ if (m)
6400
+ p = `${m[1].toUpperCase()}:\\${m[2]?.replaceAll("/", "\\") ?? ""}`;
6401
+ }
6402
+ if (p === "~")
6403
+ return home;
6404
+ if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
6405
+ return join3(home, p.slice(2));
6406
+ if (/^file:\/\//.test(p))
6407
+ return fileURLToPath2(p);
6408
+ return p;
6409
+ }
6410
+ function createToolAliasesExtension() {
6411
+ return {
6412
+ name: "free-pi-tool-aliases",
6413
+ factory: (pi) => {
6414
+ const cwd = process.cwd();
6415
+ const bash = createBashToolDefinition(cwd);
6416
+ const read = createReadToolDefinition(cwd);
6417
+ pi.registerTool({
6418
+ ...bash,
6419
+ prepareArguments: (a) => aliasArgs(bash.prepareArguments ? bash.prepareArguments(a) : a, "command", ["cmd"]),
6420
+ execute: (...args) => {
6421
+ const s = pi.getSettings();
6422
+ const shellPath = s.shellPath ? normalizeShellPath(s.shellPath) : s.shellPath;
6423
+ return createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath }).execute(...args);
6424
+ }
6425
+ });
6426
+ pi.registerTool({
6427
+ ...read,
6428
+ prepareArguments: (a) => aliasArgs(read.prepareArguments ? read.prepareArguments(a) : a, "path", ["file", "file_path"]),
6429
+ execute: (...args) => createReadToolDefinition(cwd, { autoResizeImages: pi.getSettings().images?.autoResize ?? true }).execute(...args)
6430
+ });
6431
+ }
6432
+ };
6433
+ }
6434
+
6214
6435
  // src/error-notice.ts
6215
6436
  var NOTICE_DEDUPE_MS = 15000;
6216
6437
  function parseErrorEnvelope(errorMessage) {
@@ -6253,6 +6474,16 @@ function createErrorNoticeExtension(opts = {}) {
6253
6474
  };
6254
6475
  }
6255
6476
 
6477
+ // src/agent-rules.ts
6478
+ var AGENT_RULES = `## Working rules
6479
+ - When you say you will run, check, or change something, make that tool call in the same
6480
+ turn. Never end a turn with only a plan or a script printed as text.
6481
+ - If the same error appears twice, stop retrying variants. Read the relevant source, docs
6482
+ or full error output, or try a different approach. Do not re-read a file you already read
6483
+ unless it has changed.
6484
+ - Before saying a task is done, check the result the user will see: run the program, open
6485
+ the output, or run the specific test. Say plainly what you checked and what you did not.`;
6486
+
6256
6487
  // src/tool-guard.ts
6257
6488
  async function reportToolGuardTrigger(opts, toolName) {
6258
6489
  try {
@@ -6371,6 +6602,20 @@ function installFooterRightAlign(mode) {
6371
6602
 
6372
6603
  // src/changelog.ts
6373
6604
  var CHANGELOG_HIGHLIGHTS = [
6605
+ {
6606
+ version: "0.4.4",
6607
+ highlights: [
6608
+ "Updated terms: you'll be asked to accept them once.",
6609
+ "Some ads now come from Gravity, measured with a small tracker. Turn it off with FREEPI_NO_GRAVITY_PIXEL=1."
6610
+ ]
6611
+ },
6612
+ {
6613
+ version: "0.4.3",
6614
+ highlights: [
6615
+ "The ad banner now refreshes while you work, at most once a minute.",
6616
+ "If our terms have changed since you signed up, you'll be asked to review and accept them."
6617
+ ]
6618
+ },
6374
6619
  {
6375
6620
  version: "0.4.2",
6376
6621
  highlights: ["Credits now sit on the right of the footer, under the model name."]
@@ -6736,12 +6981,89 @@ function createHeaderExtension(opts) {
6736
6981
  };
6737
6982
  }
6738
6983
 
6984
+ // src/fusion.ts
6985
+ var FUSION_ID = "fusion";
6986
+ var FUSION_NAME = "Fusion · Argon plans, DeepSeek builds";
6987
+ var FUSION_PLANNER_ID = "gemini-4-argon-flex";
6988
+ var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
6989
+ function editedThisTurn(messages) {
6990
+ for (let i = messages.length - 1;i >= 0; i--) {
6991
+ const message = messages[i];
6992
+ if (message.role === "user")
6993
+ break;
6994
+ if (message.role === "toolResult" && (message.toolName === "edit" || message.toolName === "write") && !message.isError) {
6995
+ return true;
6996
+ }
6997
+ }
6998
+ return false;
6999
+ }
7000
+ function hasNewUserInput(request) {
7001
+ const failedAt = request.failed?.message.timestamp;
7002
+ if (failedAt === undefined)
7003
+ return false;
7004
+ for (let i = request.messages.length - 1;i >= 0; i--) {
7005
+ const message = request.messages[i];
7006
+ if (message.role === "assistant")
7007
+ return false;
7008
+ if (message.role === "user")
7009
+ return message.timestamp > failedAt;
7010
+ }
7011
+ return false;
7012
+ }
7013
+ function routeFusion(request, models) {
7014
+ const phase = request.state?.phase ?? "plan";
7015
+ const restartPlan = request.reason === "user" || request.reason === "retry" && hasNewUserInput(request);
7016
+ const route = (model, nextPhase = phase) => ({
7017
+ model,
7018
+ thinkingLevel: request.thinkingLevel,
7019
+ state: request.reason === "direct" ? undefined : request.state?.phase === nextPhase ? request.state : { phase: nextPhase }
7020
+ });
7021
+ if (!models.planner || !models.executor) {
7022
+ return route(models.defaultModel, restartPlan ? "plan" : phase);
7023
+ }
7024
+ switch (request.reason) {
7025
+ case "user":
7026
+ return route(models.planner, "plan");
7027
+ case "direct":
7028
+ return route(models.executor);
7029
+ case "retry": {
7030
+ if (restartPlan)
7031
+ return route(models.planner, "plan");
7032
+ const failed = request.failed;
7033
+ if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === FUSION_PLANNER_ID) {
7034
+ return route(models.executor, "build");
7035
+ }
7036
+ return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
7037
+ }
7038
+ case "continuation":
7039
+ return phase === "build" || editedThisTurn(request.messages) ? route(models.executor, "build") : route(models.planner, "plan");
7040
+ }
7041
+ }
7042
+ function createFusionExtension(defaultModelId) {
7043
+ return {
7044
+ name: "free-pi-fusion",
7045
+ factory: (pi) => {
7046
+ pi.registerVirtualModel({
7047
+ provider: PROVIDER_NAME,
7048
+ id: FUSION_ID,
7049
+ name: FUSION_NAME,
7050
+ input: ["text"],
7051
+ route: (request, ctx) => routeFusion(request, {
7052
+ planner: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_PLANNER_ID),
7053
+ executor: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_EXECUTOR_ID),
7054
+ defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
7055
+ })
7056
+ });
7057
+ }
7058
+ };
7059
+ }
7060
+
6739
7061
  // src/provider-lock.ts
6740
7062
  function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.env) {
6741
7063
  if (env.FREEPI_KEEP_ENV_PROVIDERS)
6742
7064
  return [];
6743
7065
  const scoped = [];
6744
- for (const id of modelIds) {
7066
+ for (const id of new Set([...modelIds, FUSION_ID])) {
6745
7067
  const model = modelRuntime.getModel(providerName, id);
6746
7068
  if (model)
6747
7069
  scoped.push({ model });
@@ -6834,6 +7156,7 @@ var SDK_TOOLS = [
6834
7156
  "list_mcp_resource_templates",
6835
7157
  "read_mcp_resource"
6836
7158
  ];
7159
+ var SKILLS_DIR = resolve2(dirname3(fileURLToPath3(import.meta.url)), "..", "skills");
6837
7160
  function resolveModelName(opts) {
6838
7161
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
6839
7162
  }
@@ -6866,6 +7189,7 @@ function buildRuntimeOptions(opts, sessionId) {
6866
7189
  baseUrl: opts.baseUrl,
6867
7190
  getToken: () => opts.jwt,
6868
7191
  sessionId,
7192
+ getGravityContext: opts.getGravityContext,
6869
7193
  onBannerAd: (u) => {
6870
7194
  advertiserUrl = u;
6871
7195
  }
@@ -6895,11 +7219,13 @@ function buildRuntimeOptions(opts, sessionId) {
6895
7219
  updateLatest: opts.updateLatest
6896
7220
  });
6897
7221
  const todoToolExtension = createTodoToolExtension();
7222
+ const toolAliasesExtension = createToolAliasesExtension();
6898
7223
  const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
6899
7224
  const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
6900
7225
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
6901
7226
  const extensionFactories = [
6902
7227
  providerExtension,
7228
+ createFusionExtension(models[0].id),
6903
7229
  adsExtension,
6904
7230
  usageToolExtension,
6905
7231
  buyToolExtension,
@@ -6909,6 +7235,7 @@ function buildRuntimeOptions(opts, sessionId) {
6909
7235
  commandsExtension,
6910
7236
  headerExtension,
6911
7237
  todoToolExtension,
7238
+ toolAliasesExtension,
6912
7239
  mcpExtension,
6913
7240
  codemodeExtension,
6914
7241
  toolSearchExtension
@@ -6916,7 +7243,11 @@ function buildRuntimeOptions(opts, sessionId) {
6916
7243
  const settingsManager = buildSettingsManager(opts, models, opts.projectTrusted === true);
6917
7244
  return {
6918
7245
  settingsManager,
6919
- resourceLoaderOptions: { extensionFactories },
7246
+ resourceLoaderOptions: {
7247
+ extensionFactories,
7248
+ additionalSkillPaths: [SKILLS_DIR],
7249
+ appendSystemPromptOverride: (base) => [...base, AGENT_RULES]
7250
+ },
6920
7251
  extensionNames: extensionFactories.map((e) => e.name),
6921
7252
  tools: SDK_TOOLS
6922
7253
  };
@@ -6924,7 +7255,7 @@ function buildRuntimeOptions(opts, sessionId) {
6924
7255
  function getSessionDir(cwd, agentDir) {
6925
7256
  const resolvedCwd = resolve2(cwd);
6926
7257
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
6927
- return join3(resolve2(agentDir), "sessions", safePath);
7258
+ return join4(resolve2(agentDir), "sessions", safePath);
6928
7259
  }
6929
7260
  async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager) {
6930
7261
  if (sessionId) {
@@ -6999,6 +7330,9 @@ free-pi — before you start
6999
7330
  * Ad cards render in the terminal UI. Some ads come from a third-party ad
7000
7331
  provider (Gravity), which receives excerpts of your recent conversation
7001
7332
  to pick a relevant ad.
7333
+ * Gravity also keeps a session and visitor ID on your machine (in
7334
+ ~/.free-pi) and receives usage events from free-pi to measure and
7335
+ attribute ads.
7002
7336
  * Your coding sessions (prompts, code, files, and model responses) are saved
7003
7337
  as traces and used to train models. We may share or sell that data to
7004
7338
  third parties. Once you accept, there is no opt-out, except where the law
@@ -7017,24 +7351,25 @@ and the Privacy Policy (https://freepi.ai/privacy).
7017
7351
 
7018
7352
  Decline and nothing is sent to the free-pi server.
7019
7353
  `.trim();
7020
- var CONSENT_VERSION = "3";
7354
+ var CONSENT_VERSION = "4";
7021
7355
  var NON_INTERACTIVE_CONSENT_TEXT = "free-pi first-run consent needs an interactive terminal (stdin is not a TTY). " + "Run `npx free-pi-cli` in a terminal once to review and accept; " + "after that, non-interactive runs work.";
7022
7356
  var CONSENT_PROMPT = "Accept? [y/N] ";
7023
7357
  function parseConsentAnswer(answer) {
7024
7358
  return /^\s*y(es)?\s*$/i.test(answer);
7025
7359
  }
7360
+ var REPROMPT_CONSENT_TEXT = CONSENT_TEXT.replace("Decline and nothing is sent to the free-pi server.", "Decline and free-pi exits; you will be asked again next time.");
7026
7361
 
7027
7362
  // src/prompt.ts
7028
- async function promptConsent(deps) {
7363
+ async function promptConsent(deps, text = CONSENT_TEXT) {
7029
7364
  if (!deps.stdinIsTTY) {
7030
7365
  deps.log(NON_INTERACTIVE_CONSENT_TEXT);
7031
7366
  return false;
7032
7367
  }
7033
- deps.log(CONSENT_TEXT);
7368
+ deps.log(text);
7034
7369
  const answer = await deps.askLine(CONSENT_PROMPT);
7035
7370
  return parseConsentAnswer(answer);
7036
7371
  }
7037
- async function promptConsentInteractive() {
7372
+ async function promptConsentInteractive(text) {
7038
7373
  return promptConsent({
7039
7374
  stdinIsTTY: process.stdin.isTTY === true,
7040
7375
  log: (message) => console.log(message),
@@ -7046,7 +7381,7 @@ async function promptConsentInteractive() {
7046
7381
  rl.close();
7047
7382
  }
7048
7383
  }
7049
- });
7384
+ }, text);
7050
7385
  }
7051
7386
 
7052
7387
  // src/real-spawn.ts
@@ -7142,6 +7477,24 @@ async function checkToken(baseUrl, jwt) {
7142
7477
  });
7143
7478
  if (res.status === 401)
7144
7479
  throw new UnauthorizedError;
7480
+ if (!res.ok)
7481
+ return;
7482
+ const body = await res.json().catch(() => {
7483
+ return;
7484
+ });
7485
+ const v = body?.consent_version;
7486
+ return typeof v === "string" || v === null ? v : undefined;
7487
+ }
7488
+ async function submitUserConsent(baseUrl, jwt, consentVersion) {
7489
+ const res = await fetch(new URL("/me/consent", baseUrl), {
7490
+ method: "POST",
7491
+ headers: { authorization: `Bearer ${jwt}`, "content-type": "application/json" },
7492
+ body: JSON.stringify({ consent_version: consentVersion })
7493
+ });
7494
+ if (res.status === 401)
7495
+ throw new UnauthorizedError;
7496
+ if (!res.ok)
7497
+ throw new Error(`consent submission failed: HTTP ${res.status}`);
7145
7498
  }
7146
7499
 
7147
7500
  // src/run.ts
@@ -7175,7 +7528,7 @@ ${INTRO_TEXT}
7175
7528
 
7176
7529
  // src/whats-new.ts
7177
7530
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
7178
- import { dirname as dirname3 } from "node:path";
7531
+ import { dirname as dirname4 } from "node:path";
7179
7532
  function maybeWhatsNew(cliVersion, agentDir) {
7180
7533
  try {
7181
7534
  const path = getLastSeenVersionPath(agentDir);
@@ -7195,7 +7548,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
7195
7548
  }
7196
7549
  }
7197
7550
  function writeLastSeen(path, version) {
7198
- mkdirSync(dirname3(path), { recursive: true });
7551
+ mkdirSync(dirname4(path), { recursive: true });
7199
7552
  writeFileSync(path, version, "utf-8");
7200
7553
  }
7201
7554
 
@@ -7233,9 +7586,24 @@ async function run(deps, argv = process.argv) {
7233
7586
  return 1;
7234
7587
  }
7235
7588
  let jwt = await loadJwt(deps.credentialsPath);
7589
+ let consentCurrent = false;
7236
7590
  if (jwt) {
7237
7591
  try {
7238
- await checkToken(deps.baseUrl, jwt);
7592
+ const serverConsent = await checkToken(deps.baseUrl, jwt);
7593
+ consentCurrent = serverConsent === CONSENT_VERSION;
7594
+ if (serverConsent !== undefined && serverConsent !== CONSENT_VERSION) {
7595
+ if (!await deps.promptConsent(REPROMPT_CONSENT_TEXT)) {
7596
+ deps.log("Declined. free-pi needs you to accept the updated terms to continue; you will be asked again next time.");
7597
+ return 1;
7598
+ }
7599
+ consentCurrent = true;
7600
+ try {
7601
+ await submitUserConsent(deps.baseUrl, jwt, CONSENT_VERSION);
7602
+ } catch (err) {
7603
+ if (err instanceof UnauthorizedError)
7604
+ throw err;
7605
+ }
7606
+ }
7239
7607
  } catch (err) {
7240
7608
  if (err instanceof UnauthorizedError) {
7241
7609
  await clearJwt(deps.credentialsPath);
@@ -7251,6 +7619,7 @@ async function run(deps, argv = process.argv) {
7251
7619
  }
7252
7620
  showIntro(deps.log);
7253
7621
  jwt = await loginFlow(deps);
7622
+ consentCurrent = true;
7254
7623
  }
7255
7624
  const versionCheck = await deps.checkClientVersion(deps.baseUrl, CLI_VERSION);
7256
7625
  let updateLatest;
@@ -7271,6 +7640,12 @@ async function run(deps, argv = process.argv) {
7271
7640
  deps.log(whatsNew);
7272
7641
  if (versionCheck.notice)
7273
7642
  deps.log(versionCheck.notice);
7643
+ let getGravityContext;
7644
+ if (consentCurrent) {
7645
+ try {
7646
+ getGravityContext = deps.startGravityPixel?.(jwt);
7647
+ } catch {}
7648
+ }
7274
7649
  const finalSessionId = await deps.launchPi({
7275
7650
  baseUrl: deps.baseUrl,
7276
7651
  jwt,
@@ -7278,7 +7653,8 @@ async function run(deps, argv = process.argv) {
7278
7653
  session: sessionArg.session,
7279
7654
  model: versionCheck.model,
7280
7655
  models: versionCheck.models,
7281
- updateLatest
7656
+ updateLatest,
7657
+ getGravityContext
7282
7658
  });
7283
7659
  deps.log(finalSessionId ? `
7284
7660
  free-pi session ended. Start it again anytime: npx free-pi-cli --session ${finalSessionId}` : `
@@ -7404,6 +7780,7 @@ ${HELP_TEXT}`);
7404
7780
  launchPi,
7405
7781
  log: (message) => console.log(message),
7406
7782
  checkClientVersion,
7783
+ startGravityPixel: (jwt) => startGravityPixel(jwt, join5(dirname5(agentDir), "gravity")),
7407
7784
  maybeSelfUpdate: (action, latest) => maybeSelfUpdate(action, latest, {
7408
7785
  env: process.env,
7409
7786
  autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),