free-pi-cli 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,4 +27,4 @@ if (err) {
27
27
  console.error(err);
28
28
  process.exit(1);
29
29
  }
30
- await import("./main-37kszx0y.js");
30
+ await import("./main-xn8n5cb2.js");
@@ -44,7 +44,7 @@ function openBrowser(url, platform = process.platform, spawnImpl = spawn) {
44
44
  // package.json
45
45
  var package_default = {
46
46
  name: "free-pi-cli",
47
- version: "0.4.2",
47
+ version: "0.4.3",
48
48
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
49
49
  license: "MIT",
50
50
  type: "module",
@@ -53,6 +53,7 @@ var package_default = {
53
53
  },
54
54
  files: [
55
55
  "dist",
56
+ "skills",
56
57
  "README.md"
57
58
  ],
58
59
  engines: {
@@ -4828,6 +4829,7 @@ var MeResponseSchema = object({
4828
4829
  cap_usd_today: number2(),
4829
4830
  tier: AccountTierSchema,
4830
4831
  credit_usd: number2(),
4832
+ consent_version: string2().nullable().optional(),
4831
4833
  notice: string2().optional(),
4832
4834
  buy_url: string2().optional(),
4833
4835
  allowance_credits: number2().optional(),
@@ -5086,7 +5088,7 @@ function formatCreditsLeftLine(remaining, allowance, refillAt) {
5086
5088
  }
5087
5089
  var SLOW_LANE_METER_LINE = "Slow lane: cheapest model until refill";
5088
5090
  // ../pi-ads/src/api.ts
5089
- var REQUEST_TIMEOUT_MS = 3000;
5091
+ var REQUEST_TIMEOUT_MS = 5000;
5090
5092
  async function authedFetch(deps, path, init) {
5091
5093
  const token = await deps.getToken();
5092
5094
  const doFetch = deps.fetchImpl ?? fetch;
@@ -5202,6 +5204,8 @@ function createBannerRenderer(opts = {}) {
5202
5204
  return {
5203
5205
  async render(deps, ctx, config, columnsOverride) {
5204
5206
  const ad = await fetchAdNext(deps, "banner");
5207
+ if (opts.isActive && !opts.isActive())
5208
+ return;
5205
5209
  if (!ad) {
5206
5210
  ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
5207
5211
  opts.onBannerAd?.(undefined);
@@ -5361,6 +5365,7 @@ async function renderMeter(deps, ctx) {
5361
5365
 
5362
5366
  // ../pi-ads/src/index.ts
5363
5367
  var BANNER_REFRESH_MS = 10 * 60 * 1000;
5368
+ var BANNER_MIN_REFRESH_MS = 60 * 1000;
5364
5369
  function createAdsExtension(opts) {
5365
5370
  const deps = {
5366
5371
  baseUrl: opts.baseUrl,
@@ -5372,23 +5377,42 @@ function createAdsExtension(opts) {
5372
5377
  return {
5373
5378
  name: "free-pi-ads",
5374
5379
  factory(pi) {
5375
- const banner = createBannerRenderer({ onBannerAd: opts.onBannerAd });
5380
+ let stopped = false;
5381
+ const banner = createBannerRenderer({ onBannerAd: opts.onBannerAd, isActive: () => !stopped });
5376
5382
  const inline = createInlineRenderer();
5377
5383
  let bannerInterval;
5384
+ let lastBannerAt = 0;
5385
+ let bannerInFlight = false;
5386
+ const renderBanner = async (ctx, force = false) => {
5387
+ if (stopped)
5388
+ return;
5389
+ if (!force && (bannerInFlight || Date.now() - lastBannerAt < BANNER_MIN_REFRESH_MS))
5390
+ return;
5391
+ bannerInFlight = true;
5392
+ try {
5393
+ await banner.render(deps, ctx, config);
5394
+ } finally {
5395
+ bannerInFlight = false;
5396
+ lastBannerAt = Date.now();
5397
+ }
5398
+ };
5378
5399
  pi.on("session_start", async (_event, ctx) => {
5400
+ stopped = false;
5379
5401
  inline.reset(ctx);
5380
- await Promise.all([banner.render(deps, ctx, config), renderMeter(deps, ctx)]);
5402
+ await Promise.all([renderBanner(ctx, true), renderMeter(deps, ctx)]);
5381
5403
  if (bannerInterval)
5382
5404
  clearInterval(bannerInterval);
5383
5405
  bannerInterval = setInterval(() => {
5384
- banner.render(deps, ctx, config);
5406
+ renderBanner(ctx);
5385
5407
  }, BANNER_REFRESH_MS);
5386
5408
  bannerInterval.unref?.();
5387
5409
  });
5388
5410
  pi.on("turn_end", async (_event, ctx) => {
5411
+ renderBanner(ctx);
5389
5412
  await Promise.all([inline.onTurnEnd(deps, ctx, config), renderMeter(deps, ctx)]);
5390
5413
  });
5391
5414
  pi.on("session_shutdown", () => {
5415
+ stopped = true;
5392
5416
  if (bannerInterval) {
5393
5417
  clearInterval(bannerInterval);
5394
5418
  bannerInterval = undefined;
@@ -5399,7 +5423,8 @@ function createAdsExtension(opts) {
5399
5423
  }
5400
5424
 
5401
5425
  // src/pi-launch.ts
5402
- import { join as join3, resolve as resolve2 } from "node:path";
5426
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
5427
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
5403
5428
 
5404
5429
  // src/provider.ts
5405
5430
  var PROVIDER_NAME = "free-pi";
@@ -6211,6 +6236,64 @@ function createTodoToolExtension(options = {}) {
6211
6236
  };
6212
6237
  }
6213
6238
 
6239
+ // src/tool-aliases.ts
6240
+ import { homedir as homedir2 } from "node:os";
6241
+ import { join as join3 } from "node:path";
6242
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6243
+ import {
6244
+ createBashToolDefinition,
6245
+ createReadToolDefinition
6246
+ } from "@earendil-works/pi-coding-agent";
6247
+ function aliasArgs(input, key, aliases) {
6248
+ if (!input || typeof input !== "object" || key in input)
6249
+ return input;
6250
+ const args = input;
6251
+ const alias = aliases.find((a) => (a in args));
6252
+ if (!alias)
6253
+ return input;
6254
+ const { [alias]: value, ...rest } = args;
6255
+ return { ...rest, [key]: value };
6256
+ }
6257
+ function normalizeShellPath(input, platform = process.platform, home = homedir2()) {
6258
+ let p = input;
6259
+ if (platform === "win32" && p.startsWith("/") && !p.startsWith("//") && !p.includes("\\")) {
6260
+ const m = p.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
6261
+ if (m)
6262
+ p = `${m[1].toUpperCase()}:\\${m[2]?.replaceAll("/", "\\") ?? ""}`;
6263
+ }
6264
+ if (p === "~")
6265
+ return home;
6266
+ if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
6267
+ return join3(home, p.slice(2));
6268
+ if (/^file:\/\//.test(p))
6269
+ return fileURLToPath2(p);
6270
+ return p;
6271
+ }
6272
+ function createToolAliasesExtension() {
6273
+ return {
6274
+ name: "free-pi-tool-aliases",
6275
+ factory: (pi) => {
6276
+ const cwd = process.cwd();
6277
+ const bash = createBashToolDefinition(cwd);
6278
+ const read = createReadToolDefinition(cwd);
6279
+ pi.registerTool({
6280
+ ...bash,
6281
+ prepareArguments: (a) => aliasArgs(bash.prepareArguments ? bash.prepareArguments(a) : a, "command", ["cmd"]),
6282
+ execute: (...args) => {
6283
+ const s = pi.getSettings();
6284
+ const shellPath = s.shellPath ? normalizeShellPath(s.shellPath) : s.shellPath;
6285
+ return createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath }).execute(...args);
6286
+ }
6287
+ });
6288
+ pi.registerTool({
6289
+ ...read,
6290
+ prepareArguments: (a) => aliasArgs(read.prepareArguments ? read.prepareArguments(a) : a, "path", ["file", "file_path"]),
6291
+ execute: (...args) => createReadToolDefinition(cwd, { autoResizeImages: pi.getSettings().images?.autoResize ?? true }).execute(...args)
6292
+ });
6293
+ }
6294
+ };
6295
+ }
6296
+
6214
6297
  // src/error-notice.ts
6215
6298
  var NOTICE_DEDUPE_MS = 15000;
6216
6299
  function parseErrorEnvelope(errorMessage) {
@@ -6253,6 +6336,16 @@ function createErrorNoticeExtension(opts = {}) {
6253
6336
  };
6254
6337
  }
6255
6338
 
6339
+ // src/agent-rules.ts
6340
+ var AGENT_RULES = `## Working rules
6341
+ - When you say you will run, check, or change something, make that tool call in the same
6342
+ turn. Never end a turn with only a plan or a script printed as text.
6343
+ - If the same error appears twice, stop retrying variants. Read the relevant source, docs
6344
+ or full error output, or try a different approach. Do not re-read a file you already read
6345
+ unless it has changed.
6346
+ - Before saying a task is done, check the result the user will see: run the program, open
6347
+ the output, or run the specific test. Say plainly what you checked and what you did not.`;
6348
+
6256
6349
  // src/tool-guard.ts
6257
6350
  async function reportToolGuardTrigger(opts, toolName) {
6258
6351
  try {
@@ -6371,6 +6464,13 @@ function installFooterRightAlign(mode) {
6371
6464
 
6372
6465
  // src/changelog.ts
6373
6466
  var CHANGELOG_HIGHLIGHTS = [
6467
+ {
6468
+ version: "0.4.3",
6469
+ highlights: [
6470
+ "The ad banner now refreshes while you work, at most once a minute.",
6471
+ "If our terms have changed since you signed up, you'll be asked to review and accept them."
6472
+ ]
6473
+ },
6374
6474
  {
6375
6475
  version: "0.4.2",
6376
6476
  highlights: ["Credits now sit on the right of the footer, under the model name."]
@@ -6736,12 +6836,89 @@ function createHeaderExtension(opts) {
6736
6836
  };
6737
6837
  }
6738
6838
 
6839
+ // src/fusion.ts
6840
+ var FUSION_ID = "fusion";
6841
+ var FUSION_NAME = "Fusion · Argon plans, DeepSeek builds";
6842
+ var FUSION_PLANNER_ID = "gemini-4-argon-flex";
6843
+ var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
6844
+ function editedThisTurn(messages) {
6845
+ for (let i = messages.length - 1;i >= 0; i--) {
6846
+ const message = messages[i];
6847
+ if (message.role === "user")
6848
+ break;
6849
+ if (message.role === "toolResult" && (message.toolName === "edit" || message.toolName === "write") && !message.isError) {
6850
+ return true;
6851
+ }
6852
+ }
6853
+ return false;
6854
+ }
6855
+ function hasNewUserInput(request) {
6856
+ const failedAt = request.failed?.message.timestamp;
6857
+ if (failedAt === undefined)
6858
+ return false;
6859
+ for (let i = request.messages.length - 1;i >= 0; i--) {
6860
+ const message = request.messages[i];
6861
+ if (message.role === "assistant")
6862
+ return false;
6863
+ if (message.role === "user")
6864
+ return message.timestamp > failedAt;
6865
+ }
6866
+ return false;
6867
+ }
6868
+ function routeFusion(request, models) {
6869
+ const phase = request.state?.phase ?? "plan";
6870
+ const restartPlan = request.reason === "user" || request.reason === "retry" && hasNewUserInput(request);
6871
+ const route = (model, nextPhase = phase) => ({
6872
+ model,
6873
+ thinkingLevel: request.thinkingLevel,
6874
+ state: request.reason === "direct" ? undefined : request.state?.phase === nextPhase ? request.state : { phase: nextPhase }
6875
+ });
6876
+ if (!models.planner || !models.executor) {
6877
+ return route(models.defaultModel, restartPlan ? "plan" : phase);
6878
+ }
6879
+ switch (request.reason) {
6880
+ case "user":
6881
+ return route(models.planner, "plan");
6882
+ case "direct":
6883
+ return route(models.executor);
6884
+ case "retry": {
6885
+ if (restartPlan)
6886
+ return route(models.planner, "plan");
6887
+ const failed = request.failed;
6888
+ if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === FUSION_PLANNER_ID) {
6889
+ return route(models.executor, "build");
6890
+ }
6891
+ return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
6892
+ }
6893
+ case "continuation":
6894
+ return phase === "build" || editedThisTurn(request.messages) ? route(models.executor, "build") : route(models.planner, "plan");
6895
+ }
6896
+ }
6897
+ function createFusionExtension(defaultModelId) {
6898
+ return {
6899
+ name: "free-pi-fusion",
6900
+ factory: (pi) => {
6901
+ pi.registerVirtualModel({
6902
+ provider: PROVIDER_NAME,
6903
+ id: FUSION_ID,
6904
+ name: FUSION_NAME,
6905
+ input: ["text"],
6906
+ route: (request, ctx) => routeFusion(request, {
6907
+ planner: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_PLANNER_ID),
6908
+ executor: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_EXECUTOR_ID),
6909
+ defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
6910
+ })
6911
+ });
6912
+ }
6913
+ };
6914
+ }
6915
+
6739
6916
  // src/provider-lock.ts
6740
6917
  function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.env) {
6741
6918
  if (env.FREEPI_KEEP_ENV_PROVIDERS)
6742
6919
  return [];
6743
6920
  const scoped = [];
6744
- for (const id of modelIds) {
6921
+ for (const id of new Set([...modelIds, FUSION_ID])) {
6745
6922
  const model = modelRuntime.getModel(providerName, id);
6746
6923
  if (model)
6747
6924
  scoped.push({ model });
@@ -6834,6 +7011,7 @@ var SDK_TOOLS = [
6834
7011
  "list_mcp_resource_templates",
6835
7012
  "read_mcp_resource"
6836
7013
  ];
7014
+ var SKILLS_DIR = resolve2(dirname3(fileURLToPath3(import.meta.url)), "..", "skills");
6837
7015
  function resolveModelName(opts) {
6838
7016
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
6839
7017
  }
@@ -6895,11 +7073,13 @@ function buildRuntimeOptions(opts, sessionId) {
6895
7073
  updateLatest: opts.updateLatest
6896
7074
  });
6897
7075
  const todoToolExtension = createTodoToolExtension();
7076
+ const toolAliasesExtension = createToolAliasesExtension();
6898
7077
  const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
6899
7078
  const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
6900
7079
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
6901
7080
  const extensionFactories = [
6902
7081
  providerExtension,
7082
+ createFusionExtension(models[0].id),
6903
7083
  adsExtension,
6904
7084
  usageToolExtension,
6905
7085
  buyToolExtension,
@@ -6909,6 +7089,7 @@ function buildRuntimeOptions(opts, sessionId) {
6909
7089
  commandsExtension,
6910
7090
  headerExtension,
6911
7091
  todoToolExtension,
7092
+ toolAliasesExtension,
6912
7093
  mcpExtension,
6913
7094
  codemodeExtension,
6914
7095
  toolSearchExtension
@@ -6916,7 +7097,11 @@ function buildRuntimeOptions(opts, sessionId) {
6916
7097
  const settingsManager = buildSettingsManager(opts, models, opts.projectTrusted === true);
6917
7098
  return {
6918
7099
  settingsManager,
6919
- resourceLoaderOptions: { extensionFactories },
7100
+ resourceLoaderOptions: {
7101
+ extensionFactories,
7102
+ additionalSkillPaths: [SKILLS_DIR],
7103
+ appendSystemPromptOverride: (base) => [...base, AGENT_RULES]
7104
+ },
6920
7105
  extensionNames: extensionFactories.map((e) => e.name),
6921
7106
  tools: SDK_TOOLS
6922
7107
  };
@@ -6924,7 +7109,7 @@ function buildRuntimeOptions(opts, sessionId) {
6924
7109
  function getSessionDir(cwd, agentDir) {
6925
7110
  const resolvedCwd = resolve2(cwd);
6926
7111
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
6927
- return join3(resolve2(agentDir), "sessions", safePath);
7112
+ return join4(resolve2(agentDir), "sessions", safePath);
6928
7113
  }
6929
7114
  async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager) {
6930
7115
  if (sessionId) {
@@ -7023,18 +7208,19 @@ var CONSENT_PROMPT = "Accept? [y/N] ";
7023
7208
  function parseConsentAnswer(answer) {
7024
7209
  return /^\s*y(es)?\s*$/i.test(answer);
7025
7210
  }
7211
+ 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
7212
 
7027
7213
  // src/prompt.ts
7028
- async function promptConsent(deps) {
7214
+ async function promptConsent(deps, text = CONSENT_TEXT) {
7029
7215
  if (!deps.stdinIsTTY) {
7030
7216
  deps.log(NON_INTERACTIVE_CONSENT_TEXT);
7031
7217
  return false;
7032
7218
  }
7033
- deps.log(CONSENT_TEXT);
7219
+ deps.log(text);
7034
7220
  const answer = await deps.askLine(CONSENT_PROMPT);
7035
7221
  return parseConsentAnswer(answer);
7036
7222
  }
7037
- async function promptConsentInteractive() {
7223
+ async function promptConsentInteractive(text) {
7038
7224
  return promptConsent({
7039
7225
  stdinIsTTY: process.stdin.isTTY === true,
7040
7226
  log: (message) => console.log(message),
@@ -7046,7 +7232,7 @@ async function promptConsentInteractive() {
7046
7232
  rl.close();
7047
7233
  }
7048
7234
  }
7049
- });
7235
+ }, text);
7050
7236
  }
7051
7237
 
7052
7238
  // src/real-spawn.ts
@@ -7142,6 +7328,24 @@ async function checkToken(baseUrl, jwt) {
7142
7328
  });
7143
7329
  if (res.status === 401)
7144
7330
  throw new UnauthorizedError;
7331
+ if (!res.ok)
7332
+ return;
7333
+ const body = await res.json().catch(() => {
7334
+ return;
7335
+ });
7336
+ const v = body?.consent_version;
7337
+ return typeof v === "string" || v === null ? v : undefined;
7338
+ }
7339
+ async function submitUserConsent(baseUrl, jwt, consentVersion) {
7340
+ const res = await fetch(new URL("/me/consent", baseUrl), {
7341
+ method: "POST",
7342
+ headers: { authorization: `Bearer ${jwt}`, "content-type": "application/json" },
7343
+ body: JSON.stringify({ consent_version: consentVersion })
7344
+ });
7345
+ if (res.status === 401)
7346
+ throw new UnauthorizedError;
7347
+ if (!res.ok)
7348
+ throw new Error(`consent submission failed: HTTP ${res.status}`);
7145
7349
  }
7146
7350
 
7147
7351
  // src/run.ts
@@ -7175,7 +7379,7 @@ ${INTRO_TEXT}
7175
7379
 
7176
7380
  // src/whats-new.ts
7177
7381
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
7178
- import { dirname as dirname3 } from "node:path";
7382
+ import { dirname as dirname4 } from "node:path";
7179
7383
  function maybeWhatsNew(cliVersion, agentDir) {
7180
7384
  try {
7181
7385
  const path = getLastSeenVersionPath(agentDir);
@@ -7195,7 +7399,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
7195
7399
  }
7196
7400
  }
7197
7401
  function writeLastSeen(path, version) {
7198
- mkdirSync(dirname3(path), { recursive: true });
7402
+ mkdirSync(dirname4(path), { recursive: true });
7199
7403
  writeFileSync(path, version, "utf-8");
7200
7404
  }
7201
7405
 
@@ -7235,7 +7439,19 @@ async function run(deps, argv = process.argv) {
7235
7439
  let jwt = await loadJwt(deps.credentialsPath);
7236
7440
  if (jwt) {
7237
7441
  try {
7238
- await checkToken(deps.baseUrl, jwt);
7442
+ const serverConsent = await checkToken(deps.baseUrl, jwt);
7443
+ if (serverConsent !== undefined && serverConsent !== CONSENT_VERSION) {
7444
+ if (!await deps.promptConsent(REPROMPT_CONSENT_TEXT)) {
7445
+ deps.log("Declined. free-pi needs you to accept the updated terms to continue; you will be asked again next time.");
7446
+ return 1;
7447
+ }
7448
+ try {
7449
+ await submitUserConsent(deps.baseUrl, jwt, CONSENT_VERSION);
7450
+ } catch (err) {
7451
+ if (err instanceof UnauthorizedError)
7452
+ throw err;
7453
+ }
7454
+ }
7239
7455
  } catch (err) {
7240
7456
  if (err instanceof UnauthorizedError) {
7241
7457
  await clearJwt(deps.credentialsPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "skills",
12
13
  "README.md"
13
14
  ],
14
15
  "engines": {
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: long-jobs
3
+ description: Use when running builds, test suites, dev servers, downloads, decompilers, or any command expected to take longer than about a minute. Covers checking tools first, running jobs in the background with a log, waiting once, and not hiding errors.
4
+ ---
5
+
6
+ # Long-running jobs
7
+
8
+ - Before relying on a tool, check it exists once (`command -v gh cmake timeout`) and note what is missing. Do not assume `sudo`.
9
+ - Run the specific test or target first. Run a full suite once, at the end.
10
+ - Before rerunning a long job, check whether its earlier output or log already exists.
11
+ - Start long processes in the background with output to a log file and the PID saved: `nohup cmd > job.log 2>&1 & echo $! > job.pid`. Stop them with `kill $(cat job.pid)`. Never use `pkill -f` with a pattern that also matches your own command line.
12
+ - Wait once, with a time limit, for a marker in the log: `timeout 600 sh -c 'until grep -q "done" job.log; do sleep 5; done'`. To wait for the process instead: `timeout 600 sh -c 'while kill -0 $(cat job.pid) 2>/dev/null; do sleep 5; done'`. Do not poll with repeated short sleep-and-tail calls, and do not use `tail -f | grep` (it keeps waiting after the match).
13
+ - Check a port is free before starting a server, and stop the servers you started.
14
+ - Do not hide errors: no `2>/dev/null` and no `| tail` on a command whose failure you need to diagnose. Test a new parser or script on a small sample first.