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 +1 -1
- package/dist/{main-zv5s2zn8.js → main-q0zat1nk.js} +361 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
} from "./index-n5qs9pq1.js";
|
|
6
6
|
|
|
7
7
|
// src/main.ts
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
24615
|
-
import { dirname as
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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((
|
|
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,
|
|
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()),
|