free-pi-cli 0.6.0 → 0.6.2

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-98djq99p.js");
33
+ await import("./main-cwwgr8bh.js");
@@ -5,8 +5,8 @@ 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 { realpathSync as realpathSync4, writeFileSync as writeFileSync3 } from "node:fs";
9
+ import { dirname as dirname10, join as join12 } from "node:path";
10
10
 
11
11
  // src/browser.ts
12
12
  import { spawn } from "node:child_process";
@@ -29,6 +29,8 @@ function buildOpenCommand(url, platform) {
29
29
  windowsVerbatimArguments: true
30
30
  };
31
31
  }
32
+ if (platform === "android")
33
+ return { command: "termux-open-url", args: [url] };
32
34
  return { command: platform === "darwin" ? "open" : "xdg-open", args: [url] };
33
35
  }
34
36
  function openBrowser(url, platform = process.platform, spawnImpl = spawn, exitWaitMs = 2000) {
@@ -14410,7 +14412,8 @@ var GuestStartRequestSchema = exports_external.object({
14410
14412
  });
14411
14413
  var ExperimentVariantSchema = exports_external.enum(["control", "treatment"]);
14412
14414
  var ExperimentAssignmentSchema = exports_external.object({
14413
- guest_handoff: ExperimentVariantSchema.optional()
14415
+ guest_handoff: ExperimentVariantSchema.optional(),
14416
+ first_task: ExperimentVariantSchema.optional()
14414
14417
  });
14415
14418
  var GuestStartResponseSchema = exports_external.object({
14416
14419
  token: exports_external.string(),
@@ -14432,7 +14435,7 @@ var ReturnBonusTelemetrySchema = exports_external.object({
14432
14435
  type: exports_external.enum(["card_shown", "credits_back"])
14433
14436
  });
14434
14437
  var ExperimentExposureSchema = exports_external.object({
14435
- experiment: exports_external.enum(["resume_on_launch"]),
14438
+ experiment: exports_external.enum(["resume_on_launch", "retain.first_task"]),
14436
14439
  event: exports_external.enum(["offer_shown", "offer_accepted"])
14437
14440
  });
14438
14441
  var ReturnBonusSurfaceSchema = exports_external.object({
@@ -14626,6 +14629,18 @@ var ConfigSchema = exports_external.object({
14626
14629
  emailProviderUrl: exports_external.string().optional(),
14627
14630
  hiveEnabled: exports_external.boolean().default(false),
14628
14631
  providerOrder: exports_external.array(exports_external.string()).optional(),
14632
+ openrouterFlashProviderOrder: exports_external.array(exports_external.string()).default([
14633
+ "streamlake/fp8",
14634
+ "deepinfra/fp8",
14635
+ "gmicloud/fp8",
14636
+ "digitalocean"
14637
+ ]),
14638
+ openrouterFlashProviderIgnore: exports_external.array(exports_external.string()).default(["baidu", "relace"]),
14639
+ openrouterFlashMaxPricePrompt: exports_external.string().default("0.12"),
14640
+ openrouterFlashMaxPriceCompletion: exports_external.string().default("0.25"),
14641
+ openrouterSessionPinTtlSeconds: exports_external.number().int().min(1).default(1800),
14642
+ costAlertMultiplier: exports_external.number().int().min(1).default(2),
14643
+ costAlertMinHours: exports_external.number().int().min(1).default(7),
14629
14644
  tierNewMaxDays: exports_external.number().int().default(30),
14630
14645
  tierYoungMaxDays: exports_external.number().int().default(365),
14631
14646
  tierCapNewUsd: exports_external.number().default(0.45),
@@ -14710,6 +14725,12 @@ function envList(value) {
14710
14725
  const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
14711
14726
  return items.length > 0 ? items : undefined;
14712
14727
  }
14728
+ function envDecimalString(value) {
14729
+ if (value === undefined || value.trim() === "")
14730
+ return;
14731
+ const t = value.trim();
14732
+ return /^\d+(\.\d+)?$/.test(t) ? t : undefined;
14733
+ }
14713
14734
  function loadConfig(env = process.env) {
14714
14735
  return ConfigSchema.parse({
14715
14736
  perUserDailyCapUsd: envNumber(env.PER_USER_DAILY_CAP_USD),
@@ -14742,6 +14763,13 @@ function loadConfig(env = process.env) {
14742
14763
  emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
14743
14764
  hiveEnabled: envBoolean(env.HIVE_ENABLED),
14744
14765
  providerOrder: envList(env.PROVIDER_ORDER),
14766
+ openrouterFlashProviderOrder: envList(env.OPENROUTER_FLASH_PROVIDER_ORDER),
14767
+ openrouterFlashProviderIgnore: envList(env.OPENROUTER_FLASH_PROVIDER_IGNORE),
14768
+ openrouterFlashMaxPricePrompt: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_PROMPT),
14769
+ openrouterFlashMaxPriceCompletion: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_COMPLETION),
14770
+ openrouterSessionPinTtlSeconds: envNumber(env.OPENROUTER_SESSION_PIN_TTL_SECONDS),
14771
+ costAlertMultiplier: envNumber(env.COST_ALERT_MULTIPLIER),
14772
+ costAlertMinHours: envNumber(env.COST_ALERT_MIN_HOURS),
14745
14773
  tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
14746
14774
  tierYoungMaxDays: envNumber(env.TIER_YOUNG_MAX_DAYS),
14747
14775
  tierCapNewUsd: envNumber(env.TIER_CAP_NEW_USD),
@@ -15029,7 +15057,7 @@ var HiveClassifyResponseSchema = exports_external.object({
15029
15057
  // package.json
15030
15058
  var package_default = {
15031
15059
  name: "free-pi-cli",
15032
- version: "0.6.0",
15060
+ version: "0.6.2",
15033
15061
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
15034
15062
  license: "MIT",
15035
15063
  type: "module",
@@ -15768,6 +15796,34 @@ function createAdsExtension(opts) {
15768
15796
  import { dirname as dirname8, join as join10, resolve as resolve3 } from "node:path";
15769
15797
  import { fileURLToPath as fileURLToPath4 } from "node:url";
15770
15798
 
15799
+ // src/output-limit.ts
15800
+ var MAX_OUTPUT_TOKENS = 8192;
15801
+ 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).";
15802
+ function createOutputLimitExtension() {
15803
+ let truncated = new Set;
15804
+ return {
15805
+ name: "free-pi-output-limit",
15806
+ factory(pi) {
15807
+ pi.on("message_end", (event) => {
15808
+ const msg = event.message;
15809
+ if (msg.role === "assistant") {
15810
+ const calls = (msg.content ?? []).filter((c) => c.type === "toolCall" && c.id);
15811
+ const last = msg.content?.[msg.content.length - 1];
15812
+ const atCap = (msg.usage?.output ?? 0) >= MAX_OUTPUT_TOKENS && last?.type === "toolCall" && last.id;
15813
+ truncated = new Set(msg.stopReason === "length" ? calls.map((c) => c.id) : atCap ? [last.id] : []);
15814
+ return;
15815
+ }
15816
+ if (msg.role !== "toolResult" || !msg.toolCallId || !truncated.has(msg.toolCallId))
15817
+ return;
15818
+ truncated.delete(msg.toolCallId);
15819
+ return {
15820
+ message: { ...event.message, content: [{ type: "text", text: OUTPUT_LIMIT_MESSAGE }], isError: true }
15821
+ };
15822
+ });
15823
+ }
15824
+ };
15825
+ }
15826
+
15771
15827
  // src/provider.ts
15772
15828
  var PROVIDER_NAME = "free-pi";
15773
15829
  var DEFENSE_PROVIDER_NAME = "free-pi-defense";
@@ -15798,7 +15854,7 @@ function buildProviderConfig(baseUrl, jwt2, sessionId, models, name = PROVIDER_N
15798
15854
  input: ["text"],
15799
15855
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
15800
15856
  contextWindow: 1048576,
15801
- maxTokens: 8192
15857
+ maxTokens: MAX_OUTPUT_TOKENS
15802
15858
  }))
15803
15859
  };
15804
15860
  }
@@ -20433,13 +20489,19 @@ at first run, before the GitHub sign-in.
20433
20489
  Each user gets a free allowance every day. The allowance can change with
20434
20490
  supply and demand. \`/usage\` shows how much of today's allowance is spent, as
20435
20491
  a percentage, with token counts.
20436
- One session per account runs at a time. If a previous session is stuck,
20492
+ You can run free-pi in more than one terminal. If a previous session is stuck,
20437
20493
  \`/close-other-session\` frees it.
20438
20494
 
20439
20495
  ## Credits
20440
20496
  \`/buy-credits\` opens the buy page in your browser. The free allowance spends
20441
20497
  first. Purchased credits cover use beyond it and do not expire.
20442
20498
 
20499
+ ## Model and restarting
20500
+ \`/model\` shows the current model and lets you switch it; the model can change
20501
+ during a session. Credits are free-pi's usage unit, not model tokens. To
20502
+ restart, exit and run \`npx free-pi-cli\` again; \`--continue\` resumes the last
20503
+ session.
20504
+
20443
20505
  ## Commands
20444
20506
  - \`/usage\` — spend and remaining budget today
20445
20507
  - \`/support\` — visit today's advertiser
@@ -20771,8 +20833,40 @@ import { join as join5 } from "node:path";
20771
20833
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20772
20834
  import {
20773
20835
  createBashToolDefinition,
20836
+ createLocalPowerShellOperations,
20774
20837
  createReadToolDefinition
20775
20838
  } from "@earendil-works/pi-coding-agent";
20839
+
20840
+ // src/windows-shell.ts
20841
+ import { getPowerShellConfig, getShellConfig } from "@earendil-works/pi-coding-agent";
20842
+ var works = (f) => () => {
20843
+ try {
20844
+ f();
20845
+ return true;
20846
+ } catch {
20847
+ return false;
20848
+ }
20849
+ };
20850
+ var defaultHasBash = works(() => getShellConfig());
20851
+ var defaultHasPowerShell = works(() => getPowerShellConfig());
20852
+ function detectShell(p = {}) {
20853
+ if ((p.platform ?? process.platform) !== "win32")
20854
+ return "posix";
20855
+ if ((p.hasBash ?? defaultHasBash)())
20856
+ return "bash";
20857
+ return (p.hasPowerShell ?? defaultHasPowerShell)() ? "powershell" : "none";
20858
+ }
20859
+ function shellPromptLine(kind) {
20860
+ if (kind === "bash") {
20861
+ 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/...";
20862
+ }
20863
+ if (kind === "powershell") {
20864
+ 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.";
20865
+ }
20866
+ return;
20867
+ }
20868
+
20869
+ // src/tool-aliases.ts
20776
20870
  function aliasArgs(input, key, aliases) {
20777
20871
  if (!input || typeof input !== "object" || key in input)
20778
20872
  return input;
@@ -20800,6 +20894,8 @@ function normalizeShellPath(input, platform = process.platform, home = homedir2(
20800
20894
  }
20801
20895
  function createToolAliasesExtension(opts = {}) {
20802
20896
  const { sandbox } = opts;
20897
+ const powershell = (opts.shell ?? detectShell()) === "powershell";
20898
+ const psOps = powershell ? createLocalPowerShellOperations() : undefined;
20803
20899
  return {
20804
20900
  name: "free-pi-tool-aliases",
20805
20901
  factory: (pi) => {
@@ -20808,6 +20904,10 @@ function createToolAliasesExtension(opts = {}) {
20808
20904
  const read = createReadToolDefinition(cwd);
20809
20905
  pi.registerTool({
20810
20906
  ...bash,
20907
+ ...powershell ? {
20908
+ description: bash.description.replaceAll("bash", "PowerShell"),
20909
+ promptSnippet: bash.promptSnippet?.replaceAll("bash", "PowerShell")
20910
+ } : {},
20811
20911
  prepareArguments: (a) => aliasArgs(bash.prepareArguments ? bash.prepareArguments(a) : a, "command", ["cmd"]),
20812
20912
  execute: async (id, params, signal, onUpdate, ctx) => {
20813
20913
  const s = pi.getSettings();
@@ -20816,7 +20916,8 @@ function createToolAliasesExtension(opts = {}) {
20816
20916
  if (taken.mode === "none")
20817
20917
  throw new Error("Hive: no approval was recorded for this shell command, so it did not run.");
20818
20918
  if (taken.mode === "unsandboxed") {
20819
- return createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath }).execute(id, params, signal, onUpdate, ctx);
20919
+ const operations = powershell && !shellPath ? psOps : undefined;
20920
+ return createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath, operations }).execute(id, params, signal, onUpdate, ctx);
20820
20921
  }
20821
20922
  try {
20822
20923
  return await createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath: taken.shell, spawnHook: taken.hook }).execute(id, params, signal, onUpdate, ctx);
@@ -20938,7 +21039,18 @@ var AGENT_RULES = `## Working rules
20938
21039
  or full error output, or try a different approach. Do not re-read a file you already read
20939
21040
  unless it has changed.
20940
21041
  - 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.`;
21042
+ the output, or run the specific test. Say plainly what you checked and what you did not.
21043
+ - End every turn with a short visible status: done, blocked and why, or the exact step that
21044
+ failed. Never an empty reply.
21045
+ - For questions about free-pi itself (model, credits, limits, commands, restart), call the
21046
+ ${DOCS_TOOL_NAME} tool instead of searching files. Credits are free-pi's usage unit, not model tokens.
21047
+ To restart, run npx free-pi-cli.
21048
+ - Write files longer than about 300 lines in parts, not one huge write.
21049
+ - Never print whole env, config or credential files. Search for the specific name and never
21050
+ echo secret values.
21051
+ - Ask before irreversible actions outside the project: deleting other tools' config or
21052
+ credentials, system or boot settings, bulk permission changes, remote root sessions.
21053
+ - After finishing a task, suggest one concrete next step.`;
20942
21054
 
20943
21055
  // src/tool-guard.ts
20944
21056
  async function reportToolGuardTrigger(opts, toolName) {
@@ -21054,20 +21166,65 @@ function registerBugAutocomplete(pi) {
21054
21166
  }
21055
21167
 
21056
21168
  // src/footer-align.ts
21057
- var ANSI = /\x1b\[[0-9;]*m/g;
21058
- var SIMPLE = /^[\x20-\x7e\xa0-\xff]*$/;
21169
+ import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
21170
+ var MIN_GAP = 2;
21171
+ var MIN_CWD = 8;
21172
+ var LEADING_SGR = /^(?:\x1b\[[0-9;]*m)+/;
21173
+ function mergeStatusIntoCwdLine(lines, width) {
21174
+ if (lines.length < 3)
21175
+ return lines;
21176
+ const [cwd, stats] = lines;
21177
+ const status = lines[lines.length - 1];
21178
+ const statusW = visibleWidth2(status);
21179
+ let left = cwd;
21180
+ if (visibleWidth2(left) + MIN_GAP + statusW > width) {
21181
+ const room = width - MIN_GAP - statusW;
21182
+ if (room < MIN_CWD)
21183
+ return lines;
21184
+ const style = LEADING_SGR.exec(cwd)?.[0] ?? "";
21185
+ left = truncateToWidth4(cwd, room, style ? `${style}...\x1B[0m` : "...");
21186
+ }
21187
+ const gap = " ".repeat(Math.max(MIN_GAP, width - visibleWidth2(left) - statusW));
21188
+ return [left + gap + status, stats, ...lines.slice(2, -1)];
21189
+ }
21059
21190
  function installFooterRightAlign(mode) {
21060
21191
  const footer = mode.footer;
21061
21192
  const render = footer.render.bind(footer);
21062
- footer.render = (w) => {
21063
- const lines = render(w);
21064
- if (lines.length <= 2)
21065
- return lines;
21066
- const last = lines[lines.length - 1];
21067
- const text = last.replace(ANSI, "");
21068
- if (!SIMPLE.test(text))
21069
- return lines;
21070
- return [...lines.slice(0, -1), " ".repeat(Math.max(0, w - text.length)) + last];
21193
+ footer.render = (w) => mergeStatusIntoCwdLine(render(w), w);
21194
+ }
21195
+
21196
+ // src/guest-handoff.ts
21197
+ var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
21198
+ var GUEST_NEAR_WALL_LINE = `A few guest requests left. ${LOGIN_VALUE_LINE}`;
21199
+ var SIGNED_IN_CONTINUE_LINE = "Signed in. Your conversation continues — full daily allowance is unlocked.";
21200
+ var HEADLESS_FALLBACK_LINE = "No browser or clipboard here — open the URL and enter the code above.";
21201
+ function isHandoffTreatment(variant) {
21202
+ return variant === "treatment";
21203
+ }
21204
+ var GUEST_NOTES = [
21205
+ "Signed-in users get the Defense models in /model, for security work on code and systems you own. Sign in free with /login.",
21206
+ "With the Defense models you can audit your own repo for vulnerabilities and get fixes, without the usual refusals. /login to unlock.",
21207
+ "Write detection rules and review your server configs with the Defense models. Sign in free with /login.",
21208
+ "More on security work with free-pi: https://freepi.ai/cybersecurity. /login to unlock the Defense models."
21209
+ ];
21210
+ var GUEST_NOTE_EVERY_TURNS = 3;
21211
+ function createGuestNotesExtension(opts) {
21212
+ let turns = 0;
21213
+ let shown = 0;
21214
+ return {
21215
+ name: "free-pi-guest-notes",
21216
+ factory(pi) {
21217
+ pi.on("agent_end", (event, ctx) => {
21218
+ if (!opts.isGuest() || shown >= GUEST_NOTES.length)
21219
+ return;
21220
+ const last = [...event.messages].reverse().find((m) => m.role === "assistant");
21221
+ if (last?.stopReason !== "stop")
21222
+ return;
21223
+ if (++turns % GUEST_NOTE_EVERY_TURNS !== 0)
21224
+ return;
21225
+ ctx.ui.notify(GUEST_NOTES[shown++], "info");
21226
+ });
21227
+ }
21071
21228
  };
21072
21229
  }
21073
21230
 
@@ -21113,6 +21270,27 @@ function mergeAttribution(a, b) {
21113
21270
 
21114
21271
  // src/changelog.ts
21115
21272
  var CHANGELOG_HIGHLIGHTS = [
21273
+ {
21274
+ version: "0.6.2",
21275
+ highlights: [
21276
+ "Clearer status at the end of each turn, and a suggested next step.",
21277
+ "Questions about free-pi are answered from the built-in docs.",
21278
+ "Large files are written in parts, and credential files are never printed.",
21279
+ "The agent asks before risky actions.",
21280
+ "Tool calls cut off by the output limit are retried in parts.",
21281
+ "/login reports failures clearly, and opens the browser on Termux.",
21282
+ "Windows: the bash tool falls back to PowerShell when bash is missing."
21283
+ ]
21284
+ },
21285
+ {
21286
+ version: "0.6.1",
21287
+ highlights: [
21288
+ "/model shows the Defense models by their real names.",
21289
+ "Five more Defense models: Abliterated Large V2, Qwen 3.6 Plus Uncensored, Gemma 4 Uncensored, GLM 4.7 Flash Heretic and Uncensored 1.2.",
21290
+ "The footer is two lines: credits left now sit next to the folder name.",
21291
+ "The welcome text no longer says you can only run one session."
21292
+ ]
21293
+ },
21116
21294
  {
21117
21295
  version: "0.6.0",
21118
21296
  highlights: [
@@ -21414,7 +21592,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
21414
21592
  token: parsed.token,
21415
21593
  allowance_usd: parsed.allowance_usd,
21416
21594
  remaining_usd: parsed.remaining_usd,
21417
- guestHandoffVariant: parsed.experiments?.guest_handoff
21595
+ guestHandoffVariant: parsed.experiments?.guest_handoff,
21596
+ firstTaskVariant: parsed.experiments?.first_task
21418
21597
  };
21419
21598
  }
21420
21599
  async function reportFunnelEvent(baseUrl, event, opts = {}) {
@@ -21522,15 +21701,6 @@ async function submitUserConsent(baseUrl, jwt2, consentVersion) {
21522
21701
  throw new Error(`consent submission failed: HTTP ${res.status}`);
21523
21702
  }
21524
21703
 
21525
- // src/guest-handoff.ts
21526
- var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
21527
- var GUEST_NEAR_WALL_LINE = `A few guest requests left. ${LOGIN_VALUE_LINE}`;
21528
- var SIGNED_IN_CONTINUE_LINE = "Signed in. Your conversation continues — full daily allowance is unlocked.";
21529
- var HEADLESS_FALLBACK_LINE = "No browser or clipboard here — open the URL and enter the code above.";
21530
- function isHandoffTreatment(variant) {
21531
- return variant === "treatment";
21532
- }
21533
-
21534
21704
  // src/clipboard.ts
21535
21705
  import { spawn as spawn2 } from "node:child_process";
21536
21706
  import { existsSync as existsSync2 } from "node:fs";
@@ -21780,6 +21950,7 @@ async function clearPendingLogin(credentialsPath) {
21780
21950
  var LOGIN_REWARD_LINE = "Sign in with GitHub to get 15,000 free credits a day";
21781
21951
  var LOGIN_WAITING_LINE = "Waiting for GitHub…";
21782
21952
  var LOGIN_TIMEOUT_LINE = "GitHub sign-in timed out. Run /login to try again.";
21953
+ var BROWSER_FAILED_LINE = "Couldn't open a browser here. Open the URL above yourself and enter the code.";
21783
21954
  function formatDeviceCodePrompt(device, variant) {
21784
21955
  const value = isHandoffTreatment(variant) ? LOGIN_VALUE_LINE : LOGIN_REWARD_LINE;
21785
21956
  return [
@@ -21821,8 +21992,8 @@ async function runDeviceLogin(deps, attribution = {}) {
21821
21992
  jwt: deps.jwt,
21822
21993
  fetchImpl: deps.fetchImpl
21823
21994
  });
21824
- } else if (!copied) {
21825
- deps.log(HEADLESS_FALLBACK_LINE);
21995
+ } else {
21996
+ deps.log(copied ? BROWSER_FAILED_LINE : `${BROWSER_FAILED_LINE} ${HEADLESS_FALLBACK_LINE}`);
21826
21997
  }
21827
21998
  await submitConsent(deps.baseUrl, device.session_id, CONSENT_VERSION, attribution);
21828
21999
  const expiresInMs = (device.expires_in ?? 900) * 1000;
@@ -22005,7 +22176,8 @@ async function runLogin(opts, ctx) {
22005
22176
  ctx.ui.notify(LOGIN_TIMEOUT_LINE, "warning");
22006
22177
  return;
22007
22178
  }
22008
- ctx.ui.notify("Couldn't finish GitHub sign-in. Run /login to try again.", "error");
22179
+ const reason = err instanceof Error && err.message ? ` (${err.message})` : "";
22180
+ ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
22009
22181
  }
22010
22182
  }
22011
22183
  function installLoginHandler(mode, opts) {
@@ -24206,7 +24378,8 @@ function buildRuntimeOptions(opts, sessionId) {
24206
24378
  });
24207
24379
  const todoToolExtension = createTodoToolExtension();
24208
24380
  const shellSandbox = opts.shellSandbox ?? createShellSandbox();
24209
- const toolAliasesExtension = createToolAliasesExtension({ sandbox: shellSandbox });
24381
+ const shell = opts.shell ?? detectShell();
24382
+ const toolAliasesExtension = createToolAliasesExtension({ sandbox: shellSandbox, shell });
24210
24383
  const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
24211
24384
  const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
24212
24385
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
@@ -24226,6 +24399,7 @@ function buildRuntimeOptions(opts, sessionId) {
24226
24399
  webToolsExtension,
24227
24400
  imageToolExtension,
24228
24401
  errorNoticeExtension,
24402
+ createOutputLimitExtension(),
24229
24403
  toolGuardExtension,
24230
24404
  commandsExtension,
24231
24405
  headerExtension,
@@ -24235,7 +24409,8 @@ function buildRuntimeOptions(opts, sessionId) {
24235
24409
  codemodeExtension,
24236
24410
  toolSearchExtension,
24237
24411
  hiveExtension,
24238
- ...guestLoginExtension ? [guestLoginExtension] : []
24412
+ ...guestLoginExtension ? [guestLoginExtension] : [],
24413
+ ...opts.guest ? [createGuestNotesExtension({ isGuest: () => auth.isGuest() })] : []
24239
24414
  ];
24240
24415
  const settingsManager = buildSettingsManager(opts, models, opts.projectTrusted === true);
24241
24416
  return {
@@ -24243,7 +24418,7 @@ function buildRuntimeOptions(opts, sessionId) {
24243
24418
  resourceLoaderOptions: {
24244
24419
  extensionFactories,
24245
24420
  additionalSkillPaths: [SKILLS_DIR],
24246
- appendSystemPromptOverride: (base) => [...base, AGENT_RULES]
24421
+ appendSystemPromptOverride: (base) => [...base, AGENT_RULES, ...shellPromptLine(shell) ? [shellPromptLine(shell)] : []]
24247
24422
  },
24248
24423
  extensionNames: extensionFactories.map((e) => e.name),
24249
24424
  tools: SDK_TOOLS
@@ -24539,6 +24714,9 @@ async function promptResumeInteractive(session) {
24539
24714
  }
24540
24715
 
24541
24716
  // src/onboarding.ts
24717
+ import { existsSync as existsSync5, readdirSync as readdirSync3, realpathSync as realpathSync3 } from "node:fs";
24718
+ import { homedir as homedir6 } from "node:os";
24719
+ import { join as join11 } from "node:path";
24542
24720
  var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
24543
24721
  var INTRO_TEXT = `
24544
24722
  Welcome to free-pi — a free coding agent in your terminal.
@@ -24546,8 +24724,8 @@ Welcome to free-pi — a free coding agent in your terminal.
24546
24724
  • It's the pi coding agent, free to use. Ads in the terminal pay for the
24547
24725
  inference — ad content never reaches the model.
24548
24726
  • Tell it what to build, fix, or explain, like any coding assistant.
24549
- • One session per account at a time. If you close the terminal, relaunch
24550
- with "npx free-pi-cli" — or resume where you left off with
24727
+ • You can run free-pi in more than one terminal. If you close the terminal,
24728
+ relaunch with "npx free-pi-cli" — or resume where you left off with
24551
24729
  "npx free-pi-cli --session <id>" (the id prints when you exit).
24552
24730
 
24553
24731
  Set ${INTRO_ENV_SKIP}=1 to skip this next time.
@@ -24563,14 +24741,45 @@ function showIntro(log, env = process.env) {
24563
24741
  ${INTRO_TEXT}
24564
24742
  `);
24565
24743
  }
24744
+ var FIRST_TASK_EXPERIMENT = "retain.first_task";
24745
+ var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile", "composer.json"];
24746
+ function looksLikeProject(cwd, home = homedir6()) {
24747
+ try {
24748
+ if (realpathSync3(cwd) === realpathSync3(home))
24749
+ return false;
24750
+ if (PROJECT_MARKERS.some((m) => existsSync5(join11(cwd, m))))
24751
+ return true;
24752
+ return readdirSync3(cwd).some((f) => /\.(sln|csproj)$/.test(f));
24753
+ } catch {
24754
+ return false;
24755
+ }
24756
+ }
24757
+ var FIRST_TASK_PROJECT_TEXT = `
24758
+ Try one of these on this project — just type it:
24759
+ • Explain how this project is organized
24760
+ • Find and fix one bug
24761
+ • Add a test for the most important function
24762
+ `.trim();
24763
+ var FIRST_TASK_EMPTY_TEXT = `
24764
+ This folder doesn't look like a project. free-pi works best on code you
24765
+ already have: quit, "cd" into a project, and run "npx free-pi-cli" there.
24766
+ Or just type: Create a small runnable hello-world app here and run it
24767
+ `.trim();
24768
+ function showFirstTaskHint(log, cwd = process.cwd(), home = homedir6()) {
24769
+ const text = looksLikeProject(cwd, home) ? FIRST_TASK_PROJECT_TEXT : FIRST_TASK_EMPTY_TEXT;
24770
+ log(`
24771
+ ${text}
24772
+ `);
24773
+ return text;
24774
+ }
24566
24775
 
24567
24776
  // src/whats-new.ts
24568
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24777
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24569
24778
  import { dirname as dirname9 } from "node:path";
24570
24779
  function maybeWhatsNew(cliVersion, agentDir) {
24571
24780
  try {
24572
24781
  const path = getLastSeenVersionPath(agentDir);
24573
- const lastSeen = existsSync5(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24782
+ const lastSeen = existsSync6(path) ? readFileSync4(path, "utf-8").trim() : undefined;
24574
24783
  writeLastSeen(path, cliVersion);
24575
24784
  if (lastSeen === undefined)
24576
24785
  return;
@@ -24632,18 +24841,22 @@ function validateSessionId(value) {
24632
24841
  return `Invalid --session value: ${err instanceof Error ? err.message : String(err)}`;
24633
24842
  }
24634
24843
  }
24635
- function reportResumeExposure(deps, jwt2, event) {
24636
- if (deps.reportResumeExposure) {
24844
+ function reportResumeExposure(deps, jwt2, event, experiment = RESUME_ON_LAUNCH_EXPERIMENT) {
24845
+ if (experiment === RESUME_ON_LAUNCH_EXPERIMENT && deps.reportResumeExposure) {
24637
24846
  deps.reportResumeExposure(event);
24638
24847
  return;
24639
24848
  }
24849
+ if (experiment === FIRST_TASK_EXPERIMENT && deps.reportFirstTaskExposure) {
24850
+ deps.reportFirstTaskExposure();
24851
+ return;
24852
+ }
24640
24853
  fetch(new URL("/telemetry/experiment", deps.baseUrl), {
24641
24854
  method: "POST",
24642
24855
  headers: {
24643
24856
  authorization: `Bearer ${jwt2}`,
24644
24857
  "content-type": "application/json"
24645
24858
  },
24646
- body: JSON.stringify({ experiment: RESUME_ON_LAUNCH_EXPERIMENT, event }),
24859
+ body: JSON.stringify({ experiment, event }),
24647
24860
  signal: AbortSignal.timeout(3000)
24648
24861
  }).catch(() => {});
24649
24862
  }
@@ -24670,6 +24883,7 @@ ${FINISH_SIGNIN_LINE}
24670
24883
  `);
24671
24884
  let jwt2 = await loadJwt(deps.credentialsPath);
24672
24885
  let guestHandoffVariant;
24886
+ let showFirstTask = false;
24673
24887
  let consentCurrent = false;
24674
24888
  if (jwt2) {
24675
24889
  try {
@@ -24718,6 +24932,7 @@ You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
24718
24932
  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.
24719
24933
  `);
24720
24934
  guestHandoffVariant = guest.guestHandoffVariant;
24935
+ showFirstTask = guest.firstTaskVariant === "treatment" && !resumeFlags.hasPrompt;
24721
24936
  } catch (err) {
24722
24937
  if (!(err instanceof GuestUnavailableError))
24723
24938
  throw err;
@@ -24757,6 +24972,10 @@ Guest trial is not available right now.
24757
24972
  if (me)
24758
24973
  updateAllowanceCache(me);
24759
24974
  guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
24975
+ if (showFirstTask) {
24976
+ showFirstTaskHint(deps.log);
24977
+ reportResumeExposure(deps, jwt2, "offer_shown", FIRST_TASK_EXPERIMENT);
24978
+ }
24760
24979
  const startRefill = await fetchRefillStats(deps.baseUrl, jwt2).catch(() => {
24761
24980
  return;
24762
24981
  });
@@ -24899,7 +25118,7 @@ async function checkClientVersion(baseUrl, cliVersion, fetchImpl = fetch, jwt2)
24899
25118
  // src/main.ts
24900
25119
  function resolveCliRealpath() {
24901
25120
  try {
24902
- return realpathSync3(process.argv[1] ?? "");
25121
+ return realpathSync4(process.argv[1] ?? "");
24903
25122
  } catch {
24904
25123
  return "";
24905
25124
  }
@@ -24954,7 +25173,7 @@ ${HELP_TEXT}`);
24954
25173
  launchPi,
24955
25174
  log: (message) => console.log(message),
24956
25175
  checkClientVersion: (baseUrl, cliVersion, jwt2) => checkClientVersion(baseUrl, cliVersion, fetch, jwt2),
24957
- startGravityPixel: (jwt2) => startGravityPixel(jwt2, join11(dirname10(agentDir), "gravity")),
25176
+ startGravityPixel: (jwt2) => startGravityPixel(jwt2, join12(dirname10(agentDir), "gravity")),
24958
25177
  maybeSelfUpdate: (action2, latest) => maybeSelfUpdate(action2, latest, {
24959
25178
  env: process.env,
24960
25179
  autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",