free-pi-cli 0.6.1 → 0.6.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
@@ -30,4 +30,4 @@ if (err) {
30
30
  console.error(err);
31
31
  process.exit(1);
32
32
  }
33
- await import("./main-zv5s2zn8.js");
33
+ await import("./main-q0zat1nk.js");
@@ -5,8 +5,10 @@ import {
5
5
  } from "./index-n5qs9pq1.js";
6
6
 
7
7
  // src/main.ts
8
- import { realpathSync as realpathSync3, writeFileSync as writeFileSync3 } from "node:fs";
9
- import { dirname as dirname10, join as join11 } from "node:path";
8
+ import { spawn as spawn3 } from "node:child_process";
9
+ import { realpathSync as realpathSync5, writeFileSync as writeFileSync3 } from "node:fs";
10
+ import { homedir as homedir7 } from "node:os";
11
+ import { dirname as dirname11, join as join13 } from "node:path";
10
12
 
11
13
  // src/browser.ts
12
14
  import { spawn } from "node:child_process";
@@ -29,6 +31,8 @@ function buildOpenCommand(url, platform) {
29
31
  windowsVerbatimArguments: true
30
32
  };
31
33
  }
34
+ if (platform === "android")
35
+ return { command: "termux-open-url", args: [url] };
32
36
  return { command: platform === "darwin" ? "open" : "xdg-open", args: [url] };
33
37
  }
34
38
  function openBrowser(url, platform = process.platform, spawnImpl = spawn, exitWaitMs = 2000) {
@@ -14410,7 +14414,8 @@ var GuestStartRequestSchema = exports_external.object({
14410
14414
  });
14411
14415
  var ExperimentVariantSchema = exports_external.enum(["control", "treatment"]);
14412
14416
  var ExperimentAssignmentSchema = exports_external.object({
14413
- guest_handoff: ExperimentVariantSchema.optional()
14417
+ guest_handoff: ExperimentVariantSchema.optional(),
14418
+ first_task: ExperimentVariantSchema.optional()
14414
14419
  });
14415
14420
  var GuestStartResponseSchema = exports_external.object({
14416
14421
  token: exports_external.string(),
@@ -14432,7 +14437,7 @@ var ReturnBonusTelemetrySchema = exports_external.object({
14432
14437
  type: exports_external.enum(["card_shown", "credits_back"])
14433
14438
  });
14434
14439
  var ExperimentExposureSchema = exports_external.object({
14435
- experiment: exports_external.enum(["resume_on_launch"]),
14440
+ experiment: exports_external.enum(["resume_on_launch", "retain.first_task"]),
14436
14441
  event: exports_external.enum(["offer_shown", "offer_accepted"])
14437
14442
  });
14438
14443
  var ReturnBonusSurfaceSchema = exports_external.object({
@@ -14626,6 +14631,18 @@ var ConfigSchema = exports_external.object({
14626
14631
  emailProviderUrl: exports_external.string().optional(),
14627
14632
  hiveEnabled: exports_external.boolean().default(false),
14628
14633
  providerOrder: exports_external.array(exports_external.string()).optional(),
14634
+ openrouterFlashProviderOrder: exports_external.array(exports_external.string()).default([
14635
+ "streamlake/fp8",
14636
+ "deepinfra/fp8",
14637
+ "gmicloud/fp8",
14638
+ "digitalocean"
14639
+ ]),
14640
+ openrouterFlashProviderIgnore: exports_external.array(exports_external.string()).default(["baidu", "relace"]),
14641
+ openrouterFlashMaxPricePrompt: exports_external.string().default("0.12"),
14642
+ openrouterFlashMaxPriceCompletion: exports_external.string().default("0.25"),
14643
+ openrouterSessionPinTtlSeconds: exports_external.number().int().min(1).default(1800),
14644
+ costAlertMultiplier: exports_external.number().int().min(1).default(2),
14645
+ costAlertMinHours: exports_external.number().int().min(1).default(7),
14629
14646
  tierNewMaxDays: exports_external.number().int().default(30),
14630
14647
  tierYoungMaxDays: exports_external.number().int().default(365),
14631
14648
  tierCapNewUsd: exports_external.number().default(0.45),
@@ -14710,6 +14727,12 @@ function envList(value) {
14710
14727
  const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
14711
14728
  return items.length > 0 ? items : undefined;
14712
14729
  }
14730
+ function envDecimalString(value) {
14731
+ if (value === undefined || value.trim() === "")
14732
+ return;
14733
+ const t = value.trim();
14734
+ return /^\d+(\.\d+)?$/.test(t) ? t : undefined;
14735
+ }
14713
14736
  function loadConfig(env = process.env) {
14714
14737
  return ConfigSchema.parse({
14715
14738
  perUserDailyCapUsd: envNumber(env.PER_USER_DAILY_CAP_USD),
@@ -14742,6 +14765,13 @@ function loadConfig(env = process.env) {
14742
14765
  emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
14743
14766
  hiveEnabled: envBoolean(env.HIVE_ENABLED),
14744
14767
  providerOrder: envList(env.PROVIDER_ORDER),
14768
+ openrouterFlashProviderOrder: envList(env.OPENROUTER_FLASH_PROVIDER_ORDER),
14769
+ openrouterFlashProviderIgnore: envList(env.OPENROUTER_FLASH_PROVIDER_IGNORE),
14770
+ openrouterFlashMaxPricePrompt: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_PROMPT),
14771
+ openrouterFlashMaxPriceCompletion: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_COMPLETION),
14772
+ openrouterSessionPinTtlSeconds: envNumber(env.OPENROUTER_SESSION_PIN_TTL_SECONDS),
14773
+ costAlertMultiplier: envNumber(env.COST_ALERT_MULTIPLIER),
14774
+ costAlertMinHours: envNumber(env.COST_ALERT_MIN_HOURS),
14745
14775
  tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
14746
14776
  tierYoungMaxDays: envNumber(env.TIER_YOUNG_MAX_DAYS),
14747
14777
  tierCapNewUsd: envNumber(env.TIER_CAP_NEW_USD),
@@ -15029,7 +15059,7 @@ var HiveClassifyResponseSchema = exports_external.object({
15029
15059
  // package.json
15030
15060
  var package_default = {
15031
15061
  name: "free-pi-cli",
15032
- version: "0.6.1",
15062
+ version: "0.6.3",
15033
15063
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
15034
15064
  license: "MIT",
15035
15065
  type: "module",
@@ -15768,6 +15798,34 @@ function createAdsExtension(opts) {
15768
15798
  import { dirname as dirname8, join as join10, resolve as resolve3 } from "node:path";
15769
15799
  import { fileURLToPath as fileURLToPath4 } from "node:url";
15770
15800
 
15801
+ // src/output-limit.ts
15802
+ var MAX_OUTPUT_TOKENS = 8192;
15803
+ var OUTPUT_LIMIT_MESSAGE = "This tool call was cut off by the output limit before it finished. It either did not run or ran with incomplete content, " + "so a file may be partly written. Check what was written, then redo the work in smaller parts " + "(for example write the first part, then add the rest with edit).";
15804
+ function createOutputLimitExtension() {
15805
+ let truncated = new Set;
15806
+ return {
15807
+ name: "free-pi-output-limit",
15808
+ factory(pi) {
15809
+ pi.on("message_end", (event) => {
15810
+ const msg = event.message;
15811
+ if (msg.role === "assistant") {
15812
+ const calls = (msg.content ?? []).filter((c) => c.type === "toolCall" && c.id);
15813
+ const last = msg.content?.[msg.content.length - 1];
15814
+ const atCap = (msg.usage?.output ?? 0) >= MAX_OUTPUT_TOKENS && last?.type === "toolCall" && last.id;
15815
+ truncated = new Set(msg.stopReason === "length" ? calls.map((c) => c.id) : atCap ? [last.id] : []);
15816
+ return;
15817
+ }
15818
+ if (msg.role !== "toolResult" || !msg.toolCallId || !truncated.has(msg.toolCallId))
15819
+ return;
15820
+ truncated.delete(msg.toolCallId);
15821
+ return {
15822
+ message: { ...event.message, content: [{ type: "text", text: OUTPUT_LIMIT_MESSAGE }], isError: true }
15823
+ };
15824
+ });
15825
+ }
15826
+ };
15827
+ }
15828
+
15771
15829
  // src/provider.ts
15772
15830
  var PROVIDER_NAME = "free-pi";
15773
15831
  var DEFENSE_PROVIDER_NAME = "free-pi-defense";
@@ -15798,7 +15856,7 @@ function buildProviderConfig(baseUrl, jwt2, sessionId, models, name = PROVIDER_N
15798
15856
  input: ["text"],
15799
15857
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
15800
15858
  contextWindow: 1048576,
15801
- maxTokens: 8192
15859
+ maxTokens: MAX_OUTPUT_TOKENS
15802
15860
  }))
15803
15861
  };
15804
15862
  }
@@ -20440,6 +20498,12 @@ You can run free-pi in more than one terminal. If a previous session is stuck,
20440
20498
  \`/buy-credits\` opens the buy page in your browser. The free allowance spends
20441
20499
  first. Purchased credits cover use beyond it and do not expire.
20442
20500
 
20501
+ ## Model and restarting
20502
+ \`/model\` shows the current model and lets you switch it; the model can change
20503
+ during a session. Credits are free-pi's usage unit, not model tokens. To
20504
+ restart, exit and run \`npx free-pi-cli\` again; \`--continue\` resumes the last
20505
+ session.
20506
+
20443
20507
  ## Commands
20444
20508
  - \`/usage\` — spend and remaining budget today
20445
20509
  - \`/support\` — visit today's advertiser
@@ -20771,8 +20835,40 @@ import { join as join5 } from "node:path";
20771
20835
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20772
20836
  import {
20773
20837
  createBashToolDefinition,
20838
+ createLocalPowerShellOperations,
20774
20839
  createReadToolDefinition
20775
20840
  } from "@earendil-works/pi-coding-agent";
20841
+
20842
+ // src/windows-shell.ts
20843
+ import { getPowerShellConfig, getShellConfig } from "@earendil-works/pi-coding-agent";
20844
+ var works = (f) => () => {
20845
+ try {
20846
+ f();
20847
+ return true;
20848
+ } catch {
20849
+ return false;
20850
+ }
20851
+ };
20852
+ var defaultHasBash = works(() => getShellConfig());
20853
+ var defaultHasPowerShell = works(() => getPowerShellConfig());
20854
+ function detectShell(p = {}) {
20855
+ if ((p.platform ?? process.platform) !== "win32")
20856
+ return "posix";
20857
+ if ((p.hasBash ?? defaultHasBash)())
20858
+ return "bash";
20859
+ return (p.hasPowerShell ?? defaultHasPowerShell)() ? "powershell" : "none";
20860
+ }
20861
+ function shellPromptLine(kind) {
20862
+ if (kind === "bash") {
20863
+ return "Environment: Windows with a bash shell (usually Git Bash). The bash tool runs POSIX shell syntax with forward slashes; Git Bash writes drive paths as /c/Users/...";
20864
+ }
20865
+ if (kind === "powershell") {
20866
+ return "Environment: Windows without bash. The bash tool runs its command in PowerShell, so write PowerShell syntax (Get-ChildItem, $env:VAR, ;) and Windows paths like C:\\Users\\..., never POSIX shell syntax.";
20867
+ }
20868
+ return;
20869
+ }
20870
+
20871
+ // src/tool-aliases.ts
20776
20872
  function aliasArgs(input, key, aliases) {
20777
20873
  if (!input || typeof input !== "object" || key in input)
20778
20874
  return input;
@@ -20800,6 +20896,8 @@ function normalizeShellPath(input, platform = process.platform, home = homedir2(
20800
20896
  }
20801
20897
  function createToolAliasesExtension(opts = {}) {
20802
20898
  const { sandbox } = opts;
20899
+ const powershell = (opts.shell ?? detectShell()) === "powershell";
20900
+ const psOps = powershell ? createLocalPowerShellOperations() : undefined;
20803
20901
  return {
20804
20902
  name: "free-pi-tool-aliases",
20805
20903
  factory: (pi) => {
@@ -20808,6 +20906,10 @@ function createToolAliasesExtension(opts = {}) {
20808
20906
  const read = createReadToolDefinition(cwd);
20809
20907
  pi.registerTool({
20810
20908
  ...bash,
20909
+ ...powershell ? {
20910
+ description: bash.description.replaceAll("bash", "PowerShell"),
20911
+ promptSnippet: bash.promptSnippet?.replaceAll("bash", "PowerShell")
20912
+ } : {},
20811
20913
  prepareArguments: (a) => aliasArgs(bash.prepareArguments ? bash.prepareArguments(a) : a, "command", ["cmd"]),
20812
20914
  execute: async (id, params, signal, onUpdate, ctx) => {
20813
20915
  const s = pi.getSettings();
@@ -20816,7 +20918,8 @@ function createToolAliasesExtension(opts = {}) {
20816
20918
  if (taken.mode === "none")
20817
20919
  throw new Error("Hive: no approval was recorded for this shell command, so it did not run.");
20818
20920
  if (taken.mode === "unsandboxed") {
20819
- return createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath }).execute(id, params, signal, onUpdate, ctx);
20921
+ const operations = powershell && !shellPath ? psOps : undefined;
20922
+ return createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath, operations }).execute(id, params, signal, onUpdate, ctx);
20820
20923
  }
20821
20924
  try {
20822
20925
  return await createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath: taken.shell, spawnHook: taken.hook }).execute(id, params, signal, onUpdate, ctx);
@@ -20938,7 +21041,18 @@ var AGENT_RULES = `## Working rules
20938
21041
  or full error output, or try a different approach. Do not re-read a file you already read
20939
21042
  unless it has changed.
20940
21043
  - Before saying a task is done, check the result the user will see: run the program, open
20941
- the output, or run the specific test. Say plainly what you checked and what you did not.`;
21044
+ the output, or run the specific test. Say plainly what you checked and what you did not.
21045
+ - End every turn with a short visible status: done, blocked and why, or the exact step that
21046
+ failed. Never an empty reply.
21047
+ - For questions about free-pi itself (model, credits, limits, commands, restart), call the
21048
+ ${DOCS_TOOL_NAME} tool instead of searching files. Credits are free-pi's usage unit, not model tokens.
21049
+ To restart, run npx free-pi-cli.
21050
+ - Write files longer than about 300 lines in parts, not one huge write.
21051
+ - Never print whole env, config or credential files. Search for the specific name and never
21052
+ echo secret values.
21053
+ - Ask before irreversible actions outside the project: deleting other tools' config or
21054
+ credentials, system or boot settings, bulk permission changes, remote root sessions.
21055
+ - After finishing a task, suggest one concrete next step.`;
20942
21056
 
20943
21057
  // src/tool-guard.ts
20944
21058
  async function reportToolGuardTrigger(opts, toolName) {
@@ -21158,6 +21272,24 @@ function mergeAttribution(a, b) {
21158
21272
 
21159
21273
  // src/changelog.ts
21160
21274
  var CHANGELOG_HIGHLIGHTS = [
21275
+ {
21276
+ version: "0.6.3",
21277
+ highlights: [
21278
+ "When your version is older than the minimum, free-pi now updates itself and restarts in the same folder, instead of asking you to run a command."
21279
+ ]
21280
+ },
21281
+ {
21282
+ version: "0.6.2",
21283
+ highlights: [
21284
+ "Clearer status at the end of each turn, and a suggested next step.",
21285
+ "Questions about free-pi are answered from the built-in docs.",
21286
+ "Large files are written in parts, and credential files are never printed.",
21287
+ "The agent asks before risky actions.",
21288
+ "Tool calls cut off by the output limit are retried in parts.",
21289
+ "/login reports failures clearly, and opens the browser on Termux.",
21290
+ "Windows: the bash tool falls back to PowerShell when bash is missing."
21291
+ ]
21292
+ },
21161
21293
  {
21162
21294
  version: "0.6.1",
21163
21295
  highlights: [
@@ -21468,7 +21600,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
21468
21600
  token: parsed.token,
21469
21601
  allowance_usd: parsed.allowance_usd,
21470
21602
  remaining_usd: parsed.remaining_usd,
21471
- guestHandoffVariant: parsed.experiments?.guest_handoff
21603
+ guestHandoffVariant: parsed.experiments?.guest_handoff,
21604
+ firstTaskVariant: parsed.experiments?.first_task
21472
21605
  };
21473
21606
  }
21474
21607
  async function reportFunnelEvent(baseUrl, event, opts = {}) {
@@ -21825,6 +21958,7 @@ async function clearPendingLogin(credentialsPath) {
21825
21958
  var LOGIN_REWARD_LINE = "Sign in with GitHub to get 15,000 free credits a day";
21826
21959
  var LOGIN_WAITING_LINE = "Waiting for GitHub…";
21827
21960
  var LOGIN_TIMEOUT_LINE = "GitHub sign-in timed out. Run /login to try again.";
21961
+ var BROWSER_FAILED_LINE = "Couldn't open a browser here. Open the URL above yourself and enter the code.";
21828
21962
  function formatDeviceCodePrompt(device, variant) {
21829
21963
  const value = isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE;
21830
21964
  return [
@@ -21866,8 +22000,8 @@ async function runDeviceLogin(deps, attribution = {}) {
21866
22000
  jwt: deps.jwt,
21867
22001
  fetchImpl: deps.fetchImpl
21868
22002
  });
21869
- } else if (!copied) {
21870
- deps.log(HEADLESS_FALLBACK_LINE);
22003
+ } else {
22004
+ deps.log(copied ? BROWSER_FAILED_LINE : `${BROWSER_FAILED_LINE} ${HEADLESS_FALLBACK_LINE}`);
21871
22005
  }
21872
22006
  await submitConsent(deps.baseUrl, device.session_id, CONSENT_VERSION, attribution);
21873
22007
  const expiresInMs = (device.expires_in ?? 900) * 1000;
@@ -22050,7 +22184,8 @@ async function runLogin(opts, ctx) {
22050
22184
  ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
22051
22185
  return;
22052
22186
  }
22053
- ctx.ui.notify("Couldn't finish GitHub sign-in. Run /login to try again.", "error");
22187
+ const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
22188
+ ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
22054
22189
  }
22055
22190
  }
22056
22191
  function installLoginHandler(mode, opts) {
@@ -24251,7 +24386,8 @@ function buildRuntimeOptions(opts, sessionId) {
24251
24386
  });
24252
24387
  const todoToolExtension = createTodoToolExtension();
24253
24388
  const shellSandbox = opts.shellSandbox ?? createShellSandbox();
24254
- const toolAliasesExtension = createToolAliasesExtension({ sandbox: shellSandbox });
24389
+ const shell = opts.shell ?? detectShell();
24390
+ const toolAliasesExtension = createToolAliasesExtension({ sandbox: shellSandbox, shell });
24255
24391
  const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
24256
24392
  const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
24257
24393
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
@@ -24271,6 +24407,7 @@ function buildRuntimeOptions(opts, sessionId) {
24271
24407
  webToolsExtension,
24272
24408
  imageToolExtension,
24273
24409
  errorNoticeExtension,
24410
+ createOutputLimitExtension(),
24274
24411
  toolGuardExtension,
24275
24412
  commandsExtension,
24276
24413
  headerExtension,
@@ -24289,7 +24426,7 @@ function buildRuntimeOptions(opts, sessionId) {
24289
24426
  resourceLoaderOptions: {
24290
24427
  extensionFactories,
24291
24428
  additionalSkillPaths: [SKILLS_DIR],
24292
- appendSystemPromptOverride: (base) => [...base, AGENT_RULES]
24429
+ appendSystemPromptOverride: (base) => [...base, AGENT_RULES, ...shellPromptLine(shell) ? [shellPromptLine(shell)] : []]
24293
24430
  },
24294
24431
  extensionNames: extensionFactories.map((e) => e.name),
24295
24432
  tools: SDK_TOOLS
@@ -24433,6 +24570,140 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
24433
24570
  };
24434
24571
  }
24435
24572
 
24573
+ // src/relaunch.ts
24574
+ import { existsSync as existsSync5, mkdtempSync as mkdtempSync2, realpathSync as realpathSync3, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
24575
+ import { constants as osConstants, tmpdir as tmpdir2 } from "node:os";
24576
+ import { delimiter, dirname as dirname9, isAbsolute as isAbsolute2, join as join11, resolve as resolve4, sep as sep2, win32 } from "node:path";
24577
+ function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
24578
+ try {
24579
+ return statSync3(p).isDirectory();
24580
+ } catch {
24581
+ return false;
24582
+ }
24583
+ }) {
24584
+ const dir = env.FREEPI_RELAUNCH_CWD;
24585
+ delete env.FREEPI_RELAUNCH_CWD;
24586
+ if (!dir)
24587
+ return;
24588
+ const fail2 = `free-pi: could not return to your project folder (${dir}) after updating. Run free-pi again from that folder.`;
24589
+ if (!isDir(dir))
24590
+ return fail2;
24591
+ try {
24592
+ chdir(dir);
24593
+ } catch {
24594
+ return fail2;
24595
+ }
24596
+ return;
24597
+ }
24598
+ var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
24599
+ function canon(p, fold) {
24600
+ let r;
24601
+ try {
24602
+ r = realpathSync3.native(p);
24603
+ } catch {
24604
+ r = resolve4(p);
24605
+ }
24606
+ return fold ? r.toLowerCase() : r;
24607
+ }
24608
+ function isInside2(e, d) {
24609
+ return e === d || e.startsWith(d.endsWith(sep2) ? d : d + sep2);
24610
+ }
24611
+ function hasNodeModulesBin(entry) {
24612
+ const parts = entry.split(/[\\/]+/).map((x) => x.toLowerCase());
24613
+ return parts.some((x, i) => x === "node_modules" && parts[i + 1] === ".bin");
24614
+ }
24615
+ function sanitizedEnv(env, nodeDir, project, home, win) {
24616
+ const proj = canon(project, win);
24617
+ const broad = proj === canon(home, win) || dirname9(proj) === proj;
24618
+ const out = {};
24619
+ let pathKey = "PATH";
24620
+ let oldPath = "";
24621
+ for (const [k, v] of Object.entries(env)) {
24622
+ const lk = k.toLowerCase();
24623
+ if (lk.startsWith("npm_") || lk === "node_options" || lk === "node_path")
24624
+ continue;
24625
+ if (lk === "path") {
24626
+ pathKey = k;
24627
+ oldPath = v ?? "";
24628
+ continue;
24629
+ }
24630
+ if (win && lk === "comspec")
24631
+ continue;
24632
+ out[k] = v;
24633
+ }
24634
+ if (win) {
24635
+ const sysRoot = Object.entries(env).find(([k]) => k.toLowerCase() === "systemroot")?.[1];
24636
+ const root = sysRoot && win32.isAbsolute(sysRoot) ? sysRoot : "C:\\Windows";
24637
+ const comKey = Object.keys(env).find((k) => k.toLowerCase() === "comspec") ?? "ComSpec";
24638
+ out[comKey] = win32.join(root, "System32", "cmd.exe");
24639
+ }
24640
+ const kept = oldPath.split(delimiter).filter((e) => e && isAbsolute2(e) && (broad || !isInside2(canon(e, win), proj)) && !hasNodeModulesBin(e));
24641
+ out[pathKey] = [nodeDir, ...kept].join(delimiter);
24642
+ return out;
24643
+ }
24644
+ async function relaunchViaNpx(latest, deps) {
24645
+ const { env } = deps;
24646
+ if ("FREEPI_NO_AUTO_UPDATE" in env || !deps.autoUpdateEnabled())
24647
+ return;
24648
+ if (env.FREEPI_AUTO_UPDATED)
24649
+ return;
24650
+ const win = deps.platform === "win32";
24651
+ if (!latest || !SEMVER.test(latest))
24652
+ return;
24653
+ const target2 = latest;
24654
+ const nodeDir = dirname9(deps.execPath);
24655
+ const npxCli = win ? join11(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join11(dirname9(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
24656
+ const exists = deps.exists ?? existsSync5;
24657
+ const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
24658
+ let npxCliPath = npxCli;
24659
+ if (!exists(npxCli)) {
24660
+ if (!win && deps.execPath === "/usr/bin/node" && exists(distroNpxCli))
24661
+ npxCliPath = distroNpxCli;
24662
+ else
24663
+ return;
24664
+ }
24665
+ const childEnv = sanitizedEnv(env, nodeDir, deps.cwd, deps.homedir, win);
24666
+ const readyDir = mkdtempSync2(join11(tmpdir2(), "free-pi-relaunch-"));
24667
+ const readyFile = join11(readyDir, "started");
24668
+ try {
24669
+ deps.log(`Updating free-pi to ${target2}…`);
24670
+ return await new Promise((resolve5) => {
24671
+ let child;
24672
+ try {
24673
+ child = deps.spawn(deps.execPath, [npxCliPath, "--yes", `free-pi-cli@${target2}`, ...deps.argv], {
24674
+ stdio: "inherit",
24675
+ cwd: deps.homedir,
24676
+ env: { ...childEnv, FREEPI_AUTO_UPDATED: target2, FREEPI_RELAUNCH_READY: readyFile, FREEPI_RELAUNCH_CWD: deps.cwd },
24677
+ shell: false
24678
+ });
24679
+ } catch {
24680
+ resolve5(undefined);
24681
+ return;
24682
+ }
24683
+ const handlers = win ? [] : [
24684
+ ["SIGINT", () => {}],
24685
+ ["SIGTERM", () => void child.kill("SIGTERM")],
24686
+ ["SIGHUP", () => void child.kill("SIGTERM")]
24687
+ ];
24688
+ for (const [sig, cb] of handlers)
24689
+ deps.proc.on(sig, cb);
24690
+ const done3 = (code) => {
24691
+ for (const [sig, cb] of handlers)
24692
+ deps.proc.off(sig, cb);
24693
+ resolve5(code);
24694
+ };
24695
+ child.on("error", () => done3(undefined));
24696
+ child.on("exit", (code, signal) => {
24697
+ const exitCode = code ?? 128 + (signal ? osConstants.signals[signal] ?? 0 : 0);
24698
+ const cancelled = signal === "SIGINT" || signal === "SIGTERM" || signal === "SIGHUP" || exitCode === 130 || exitCode === 143 || exitCode === 129;
24699
+ done3(exitCode !== 0 && !cancelled && !existsSync5(readyFile) ? undefined : exitCode);
24700
+ });
24701
+ });
24702
+ } finally {
24703
+ rmSync2(readyDir, { recursive: true, force: true });
24704
+ }
24705
+ }
24706
+
24436
24707
  // src/run.ts
24437
24708
  import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
24438
24709
 
@@ -24563,13 +24834,13 @@ async function readOneKeypress(prompt, stdin = process.stdin, stdout = process.s
24563
24834
  if (typeof stdin.setRawMode === "function")
24564
24835
  stdin.setRawMode(true);
24565
24836
  stdin.resume();
24566
- return await new Promise((resolve4) => {
24837
+ return await new Promise((resolve5) => {
24567
24838
  const onData = (chunk) => {
24568
24839
  stdin.off("data", onData);
24569
24840
  if (typeof stdin.setRawMode === "function")
24570
24841
  stdin.setRawMode(wasRaw ?? false);
24571
24842
  stdin.pause();
24572
- resolve4(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
24843
+ resolve5(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
24573
24844
  };
24574
24845
  stdin.once("data", onData);
24575
24846
  });
@@ -24585,6 +24856,9 @@ async function promptResumeInteractive(session) {
24585
24856
  }
24586
24857
 
24587
24858
  // src/onboarding.ts
24859
+ import { existsSync as existsSync6, readdirSync as readdirSync3, realpathSync as realpathSync4 } from "node:fs";
24860
+ import { homedir as homedir6 } from "node:os";
24861
+ import { join as join12 } from "node:path";
24588
24862
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
24589
24863
  var INTRO_TEXT = `
24590
24864
  Welcome to free-pi — a free coding agent in your terminal.
@@ -24609,14 +24883,45 @@ function showIntro(log, env = process.env) {
24609
24883
  ${INTRO_TEXT}
24610
24884
  `);
24611
24885
  }
24886
+ var FIRST_TASK_EXPERIMENT = "retain.first_task";
24887
+ var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile", "composer.json"];
24888
+ function looksLikeProject(cwd, home = homedir6()) {
24889
+ try {
24890
+ if (realpathSync4(cwd) === realpathSync4(home))
24891
+ return false;
24892
+ if (PROJECT_MARKERS.some((m) => existsSync6(join12(cwd, m))))
24893
+ return true;
24894
+ return readdirSync3(cwd).some((f) => /\.(sln|csproj)$/.test(f));
24895
+ } catch {
24896
+ return false;
24897
+ }
24898
+ }
24899
+ var FIRST_TASK_PROJECT_TEXT = `
24900
+ Try one of these on this project — just type it:
24901
+ • Explain how this project is organized
24902
+ • Find and fix one bug
24903
+ • Add a test for the most important function
24904
+ `.trim();
24905
+ var FIRST_TASK_EMPTY_TEXT = `
24906
+ This folder doesn't look like a project. free-pi works best on code you
24907
+ already have: quit, "cd" into a project, and run "npx free-pi-cli" there.
24908
+ Or just type: Create a small runnable hello-world app here and run it
24909
+ `.trim();
24910
+ function showFirstTaskHint(log, cwd = process.cwd(), home = homedir6()) {
24911
+ const text = looksLikeProject(cwd, home) ? FIRST_TASK_PROJECT_TEXT : FIRST_TASK_EMPTY_TEXT;
24912
+ log(`
24913
+ ${text}
24914
+ `);
24915
+ return text;
24916
+ }
24612
24917
 
24613
24918
  // src/whats-new.ts
24614
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24615
- import { dirname as dirname9 } from "node:path";
24919
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24920
+ import { dirname as dirname10 } from "node:path";
24616
24921
  function maybeWhatsNew(cliVersion, agentDir) {
24617
24922
  try {
24618
24923
  const path = getLastSeenVersionPath(agentDir);
24619
- const lastSeen = existsSync5(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24924
+ const lastSeen = existsSync7(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24620
24925
  writeLastSeen(path, cliVersion);
24621
24926
  if (lastSeen === undefined)
24622
24927
  return;
@@ -24632,7 +24937,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
24632
24937
  }
24633
24938
  }
24634
24939
  function writeLastSeen(path, version3) {
24635
- mkdirSync2(dirname9(path), { recursive: true });
24940
+ mkdirSync2(dirname10(path), { recursive: true });
24636
24941
  writeFileSync2(path, version3, "utf-8");
24637
24942
  }
24638
24943
 
@@ -24678,18 +24983,22 @@ function validateSessionId(value) {
24678
24983
  return `Invalid --session value: ${err instanceof Error ? err.message : String(err)}`;
24679
24984
  }
24680
24985
  }
24681
- function reportResumeExposure(deps, jwt2, event) {
24682
- if (deps.reportResumeExposure) {
24986
+ function reportResumeExposure(deps, jwt2, event, experiment = RESUME_ON_LAUNCH_EXPERIMENT) {
24987
+ if (experiment === RESUME_ON_LAUNCH_EXPERIMENT && deps.reportResumeExposure) {
24683
24988
  deps.reportResumeExposure(event);
24684
24989
  return;
24685
24990
  }
24991
+ if (experiment === FIRST_TASK_EXPERIMENT && deps.reportFirstTaskExposure) {
24992
+ deps.reportFirstTaskExposure();
24993
+ return;
24994
+ }
24686
24995
  fetch(new URL("/telemetry/experiment", deps.baseUrl), {
24687
24996
  method: "POST",
24688
24997
  headers: {
24689
24998
  authorization: `Bearer ${jwt2}`,
24690
24999
  "content-type": "application/json"
24691
25000
  },
24692
- body: JSON.stringify({ experiment: RESUME_ON_LAUNCH_EXPERIMENT, event }),
25001
+ body: JSON.stringify({ experiment, event }),
24693
25002
  signal: AbortSignal.timeout(3000)
24694
25003
  }).catch(() => {});
24695
25004
  }
@@ -24716,6 +25025,7 @@ ${FINISH_SIGNIN_LINE}
24716
25025
  `);
24717
25026
  let jwt2 = await loadJwt(deps.credentialsPath);
24718
25027
  let guestHandoffVariant;
25028
+ let showFirstTask = false;
24719
25029
  let consentCurrent = false;
24720
25030
  if (jwt2) {
24721
25031
  try {
@@ -24764,6 +25074,7 @@ You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
24764
25074
  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.
24765
25075
  `);
24766
25076
  guestHandoffVariant = guest.guestHandoffVariant;
25077
+ showFirstTask = guest.firstTaskVariant === "treatment" && !resumeFlags.hasPrompt;
24767
25078
  } catch (err) {
24768
25079
  if (!(err instanceof GuestUnavailableError))
24769
25080
  throw err;
@@ -24781,6 +25092,9 @@ Guest trial is not available right now.
24781
25092
  if (outcome === "updated-exit")
24782
25093
  return 0;
24783
25094
  if (outcome === "blocked") {
25095
+ const relaunched = await deps.relaunchLatest?.(versionCheck.latest);
25096
+ if (relaunched !== undefined)
25097
+ return relaunched;
24784
25098
  deps.log("free-pi-cli needs an update. From a trusted folder (for example: cd ~), run: npx --yes free-pi-cli@latest (or: npm i -g free-pi-cli@latest)");
24785
25099
  return 1;
24786
25100
  }
@@ -24803,6 +25117,10 @@ Guest trial is not available right now.
24803
25117
  if (me)
24804
25118
  updateAllowanceCache(me);
24805
25119
  guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
25120
+ if (showFirstTask) {
25121
+ showFirstTaskHint(deps.log);
25122
+ reportResumeExposure(deps, jwt2, "offer_shown", FIRST_TASK_EXPERIMENT);
25123
+ }
24806
25124
  const startRefill = await fetchRefillStats(deps.baseUrl, jwt2).catch(() => {
24807
25125
  return;
24808
25126
  });
@@ -24945,12 +25263,17 @@ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch, jwt2)
24945
25263
  // src/main.ts
24946
25264
  function resolveCliRealpath() {
24947
25265
  try {
24948
- return realpathSync3(process.argv[1] ?? "");
25266
+ return realpathSync5(process.argv[1] ?? "");
24949
25267
  } catch {
24950
25268
  return "";
24951
25269
  }
24952
25270
  }
24953
25271
  async function main() {
25272
+ const cwdError = applyRelaunchCwd(process.env);
25273
+ if (cwdError) {
25274
+ console.error(cwdError);
25275
+ process.exit(1);
25276
+ }
24954
25277
  if (process.env.FREEPI_RELAUNCH_READY) {
24955
25278
  try {
24956
25279
  writeFileSync3(process.env.FREEPI_RELAUNCH_READY, "");
@@ -24996,11 +25319,23 @@ ${HELP_TEXT}`);
24996
25319
  credentialsPath: getCredentialsPath(agentDir),
24997
25320
  promptConsent: promptConsentInteractive,
24998
25321
  openBrowser,
24999
- sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms)),
25322
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
25000
25323
  launchPi,
25001
25324
  log: (message) => console.log(message),
25002
25325
  checkClientVersion: (baseUrl, cliVersion, jwt2) => checkClientVersion(baseUrl, cliVersion, fetch, jwt2),
25003
- startGravityPixel: (jwt2) => startGravityPixel(jwt2, join11(dirname10(agentDir), "gravity")),
25326
+ startGravityPixel: (jwt2) => startGravityPixel(jwt2, join13(dirname11(agentDir), "gravity")),
25327
+ relaunchLatest: (latest) => relaunchViaNpx(latest, {
25328
+ env: process.env,
25329
+ argv: process.argv.slice(2),
25330
+ platform: process.platform,
25331
+ cwd: process.cwd(),
25332
+ homedir: homedir7(),
25333
+ execPath: process.execPath,
25334
+ autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),
25335
+ spawn: (cmd, args, opts) => spawn3(cmd, args, opts),
25336
+ proc: process,
25337
+ log: (message) => console.log(message)
25338
+ }),
25004
25339
  maybeSelfUpdate: (action2, latest) => maybeSelfUpdate(action2, latest, {
25005
25340
  env: process.env,
25006
25341
  autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.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",