free-pi-cli 0.6.13 → 0.6.15
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/LICENSE +21 -0
- package/dist/index.js +1 -1
- package/dist/{main-p8g9x0pd.js → main-jmyqcr54.js} +1892 -1048
- package/package.json +9 -2
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { spawn as spawn3 } from "node:child_process";
|
|
5
|
-
import { realpathSync as realpathSync8, writeFileSync as
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { dirname as
|
|
5
|
+
import { realpathSync as realpathSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6
|
+
import { homedir as homedir10 } from "node:os";
|
|
7
|
+
import { dirname as dirname14, join as join15 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// src/browser.ts
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
@@ -515,7 +515,7 @@ function folderMatches(cwd, sessionCwd) {
|
|
|
515
515
|
// package.json
|
|
516
516
|
var package_default = {
|
|
517
517
|
name: "free-pi-cli",
|
|
518
|
-
version: "0.6.
|
|
518
|
+
version: "0.6.15",
|
|
519
519
|
description: "Free AI coding agent for your terminal. Ads and a daily free allowance pay for the models; sign in with GitHub and run npx free-pi-cli.",
|
|
520
520
|
keywords: [
|
|
521
521
|
"coding agent",
|
|
@@ -530,6 +530,13 @@ var package_default = {
|
|
|
530
530
|
],
|
|
531
531
|
homepage: "https://freepi.ai",
|
|
532
532
|
license: "MIT",
|
|
533
|
+
repository: {
|
|
534
|
+
type: "git",
|
|
535
|
+
url: "git+https://github.com/dennisonbertram/free-pi-cli.git"
|
|
536
|
+
},
|
|
537
|
+
bugs: {
|
|
538
|
+
url: "https://github.com/dennisonbertram/free-pi-cli/issues"
|
|
539
|
+
},
|
|
533
540
|
type: "module",
|
|
534
541
|
bin: {
|
|
535
542
|
"free-pi-cli": "dist/index.js"
|
|
@@ -543,7 +550,7 @@ var package_default = {
|
|
|
543
550
|
node: ">=22.15.0"
|
|
544
551
|
},
|
|
545
552
|
scripts: {
|
|
546
|
-
build: "
|
|
553
|
+
build: "bun scripts/build.ts",
|
|
547
554
|
prepublishOnly: "bun run build"
|
|
548
555
|
},
|
|
549
556
|
dependencies: {
|
|
@@ -605,13 +612,13 @@ function splitCatalogModels(models) {
|
|
|
605
612
|
defense: models.filter((m) => m.section === "defense")
|
|
606
613
|
};
|
|
607
614
|
}
|
|
608
|
-
function buildProviderConfig(baseUrl, jwt, sessionId, models, name = PROVIDER_NAME) {
|
|
615
|
+
function buildProviderConfig(baseUrl, jwt, sessionId, models, name = PROVIDER_NAME, extraHeaders = {}) {
|
|
609
616
|
return {
|
|
610
617
|
name,
|
|
611
618
|
baseUrl: completionsBaseUrl(baseUrl),
|
|
612
619
|
apiKey: jwt,
|
|
613
620
|
api: "openai-completions",
|
|
614
|
-
headers: { "x-session-id": sessionId, "x-client-version": CLI_VERSION },
|
|
621
|
+
headers: { "x-session-id": sessionId, "x-client-version": CLI_VERSION, ...extraHeaders },
|
|
615
622
|
models: models.map((m) => ({
|
|
616
623
|
id: m.id,
|
|
617
624
|
name: m.name,
|
|
@@ -1057,6 +1064,9 @@ function getLastTipPath(agentDir = getFreePiAgentDir()) {
|
|
|
1057
1064
|
function getConfigPath() {
|
|
1058
1065
|
return join2(homedir(), FREE_PI_DIR_NAME, "config.json");
|
|
1059
1066
|
}
|
|
1067
|
+
function getToolModePath(agentDir = getFreePiAgentDir()) {
|
|
1068
|
+
return join2(agentDir, "tools-mode");
|
|
1069
|
+
}
|
|
1060
1070
|
|
|
1061
1071
|
// src/tips.ts
|
|
1062
1072
|
var TIPS = [
|
|
@@ -1787,7 +1797,6 @@ var string = (params) => {
|
|
|
1787
1797
|
const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
|
|
1788
1798
|
return new RegExp(`^${regex}$`);
|
|
1789
1799
|
};
|
|
1790
|
-
var bigint = /^-?\d+n?$/;
|
|
1791
1800
|
var integer = /^-?\d+$/;
|
|
1792
1801
|
var number = /^-?\d+(?:\.\d+)?$/;
|
|
1793
1802
|
var boolean = /^(?:true|false)$/i;
|
|
@@ -2692,25 +2701,6 @@ var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
|
|
|
2692
2701
|
return payload;
|
|
2693
2702
|
};
|
|
2694
2703
|
});
|
|
2695
|
-
var $ZodBigInt = /* @__PURE__ */ $constructor("$ZodBigInt", (inst, def) => {
|
|
2696
|
-
$ZodType.init(inst, def);
|
|
2697
|
-
inst._zod.pattern = bigint;
|
|
2698
|
-
inst._zod.parse = (payload, _ctx) => {
|
|
2699
|
-
if (def.coerce)
|
|
2700
|
-
try {
|
|
2701
|
-
payload.value = BigInt(payload.value);
|
|
2702
|
-
} catch (_) {}
|
|
2703
|
-
if (typeof payload.value === "bigint")
|
|
2704
|
-
return payload;
|
|
2705
|
-
payload.issues.push({
|
|
2706
|
-
expected: "bigint",
|
|
2707
|
-
code: "invalid_type",
|
|
2708
|
-
input: payload.value,
|
|
2709
|
-
inst
|
|
2710
|
-
});
|
|
2711
|
-
return payload;
|
|
2712
|
-
};
|
|
2713
|
-
});
|
|
2714
2704
|
var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
|
|
2715
2705
|
$ZodType.init(inst, def);
|
|
2716
2706
|
inst._zod.parse = (payload) => payload;
|
|
@@ -2727,29 +2717,6 @@ var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => {
|
|
|
2727
2717
|
return payload;
|
|
2728
2718
|
};
|
|
2729
2719
|
});
|
|
2730
|
-
var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
|
|
2731
|
-
$ZodType.init(inst, def);
|
|
2732
|
-
inst._zod.parse = (payload, _ctx) => {
|
|
2733
|
-
if (def.coerce) {
|
|
2734
|
-
try {
|
|
2735
|
-
payload.value = new Date(payload.value);
|
|
2736
|
-
} catch (_err) {}
|
|
2737
|
-
}
|
|
2738
|
-
const input = payload.value;
|
|
2739
|
-
const isDate = input instanceof Date;
|
|
2740
|
-
const isValidDate = isDate && !Number.isNaN(input.getTime());
|
|
2741
|
-
if (isValidDate)
|
|
2742
|
-
return payload;
|
|
2743
|
-
payload.issues.push({
|
|
2744
|
-
expected: "date",
|
|
2745
|
-
code: "invalid_type",
|
|
2746
|
-
input,
|
|
2747
|
-
...isDate ? { received: "Invalid Date" } : {},
|
|
2748
|
-
inst
|
|
2749
|
-
});
|
|
2750
|
-
return payload;
|
|
2751
|
-
};
|
|
2752
|
-
});
|
|
2753
2720
|
function handleArrayResult(result, final, index) {
|
|
2754
2721
|
if (result.issues.length) {
|
|
2755
2722
|
final.issues.push(...prefixIssues(index, result.issues));
|
|
@@ -4039,12 +4006,6 @@ function _boolean(Class, params) {
|
|
|
4039
4006
|
...normalizeParams(params)
|
|
4040
4007
|
});
|
|
4041
4008
|
}
|
|
4042
|
-
function _bigint(Class, params) {
|
|
4043
|
-
return new Class({
|
|
4044
|
-
type: "bigint",
|
|
4045
|
-
...normalizeParams(params)
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
4009
|
function _unknown(Class) {
|
|
4049
4010
|
return new Class({
|
|
4050
4011
|
type: "unknown"
|
|
@@ -4056,12 +4017,6 @@ function _never(Class, params) {
|
|
|
4056
4017
|
...normalizeParams(params)
|
|
4057
4018
|
});
|
|
4058
4019
|
}
|
|
4059
|
-
function _date(Class, params) {
|
|
4060
|
-
return new Class({
|
|
4061
|
-
type: "date",
|
|
4062
|
-
...normalizeParams(params)
|
|
4063
|
-
});
|
|
4064
|
-
}
|
|
4065
4020
|
function _lt(value, params) {
|
|
4066
4021
|
return new $ZodCheckLessThan({
|
|
4067
4022
|
check: "less_than",
|
|
@@ -4662,20 +4617,10 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
4662
4617
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4663
4618
|
json.type = "boolean";
|
|
4664
4619
|
};
|
|
4665
|
-
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
4666
|
-
if (ctx.unrepresentable === "throw") {
|
|
4667
|
-
throw new Error("BigInt cannot be represented in JSON Schema");
|
|
4668
|
-
}
|
|
4669
|
-
};
|
|
4670
4620
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4671
4621
|
json.not = {};
|
|
4672
4622
|
};
|
|
4673
4623
|
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4674
|
-
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
4675
|
-
if (ctx.unrepresentable === "throw") {
|
|
4676
|
-
throw new Error("Date cannot be represented in JSON Schema");
|
|
4677
|
-
}
|
|
4678
|
-
};
|
|
4679
4624
|
var enumProcessor = (schema, _ctx, json, _params) => {
|
|
4680
4625
|
const def = schema._zod.def;
|
|
4681
4626
|
const values = getEnumValues(def.entries);
|
|
@@ -5406,31 +5351,6 @@ var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
|
|
|
5406
5351
|
function boolean2(params) {
|
|
5407
5352
|
return _boolean(ZodBoolean, params);
|
|
5408
5353
|
}
|
|
5409
|
-
var ZodBigInt = /* @__PURE__ */ $constructor("ZodBigInt", (inst, def) => {
|
|
5410
|
-
$ZodBigInt.init(inst, def);
|
|
5411
|
-
ZodType.init(inst, def);
|
|
5412
|
-
inst._zod.processJSONSchema = (ctx, json, params) => bigintProcessor(inst, ctx, json, params);
|
|
5413
|
-
inst.gte = (value, params) => inst.check(_gte(value, params));
|
|
5414
|
-
inst.min = (value, params) => inst.check(_gte(value, params));
|
|
5415
|
-
inst.gt = (value, params) => inst.check(_gt(value, params));
|
|
5416
|
-
inst.gte = (value, params) => inst.check(_gte(value, params));
|
|
5417
|
-
inst.min = (value, params) => inst.check(_gte(value, params));
|
|
5418
|
-
inst.lt = (value, params) => inst.check(_lt(value, params));
|
|
5419
|
-
inst.lte = (value, params) => inst.check(_lte(value, params));
|
|
5420
|
-
inst.max = (value, params) => inst.check(_lte(value, params));
|
|
5421
|
-
inst.positive = (params) => inst.check(_gt(BigInt(0), params));
|
|
5422
|
-
inst.negative = (params) => inst.check(_lt(BigInt(0), params));
|
|
5423
|
-
inst.nonpositive = (params) => inst.check(_lte(BigInt(0), params));
|
|
5424
|
-
inst.nonnegative = (params) => inst.check(_gte(BigInt(0), params));
|
|
5425
|
-
inst.multipleOf = (value, params) => inst.check(_multipleOf(value, params));
|
|
5426
|
-
const bag = inst._zod.bag;
|
|
5427
|
-
inst.minValue = bag.minimum ?? null;
|
|
5428
|
-
inst.maxValue = bag.maximum ?? null;
|
|
5429
|
-
inst.format = bag.format ?? null;
|
|
5430
|
-
});
|
|
5431
|
-
function bigint2(params) {
|
|
5432
|
-
return _bigint(ZodBigInt, params);
|
|
5433
|
-
}
|
|
5434
5354
|
var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
|
|
5435
5355
|
$ZodUnknown.init(inst, def);
|
|
5436
5356
|
ZodType.init(inst, def);
|
|
@@ -5447,19 +5367,6 @@ var ZodNever = /* @__PURE__ */ $constructor("ZodNever", (inst, def) => {
|
|
|
5447
5367
|
function never(params) {
|
|
5448
5368
|
return _never(ZodNever, params);
|
|
5449
5369
|
}
|
|
5450
|
-
var ZodDate = /* @__PURE__ */ $constructor("ZodDate", (inst, def) => {
|
|
5451
|
-
$ZodDate.init(inst, def);
|
|
5452
|
-
ZodType.init(inst, def);
|
|
5453
|
-
inst._zod.processJSONSchema = (ctx, json, params) => dateProcessor(inst, ctx, json, params);
|
|
5454
|
-
inst.min = (value, params) => inst.check(_gte(value, params));
|
|
5455
|
-
inst.max = (value, params) => inst.check(_lte(value, params));
|
|
5456
|
-
const c = inst._zod.bag;
|
|
5457
|
-
inst.minDate = c.minimum ? new Date(c.minimum) : null;
|
|
5458
|
-
inst.maxDate = c.maximum ? new Date(c.maximum) : null;
|
|
5459
|
-
});
|
|
5460
|
-
function date3(params) {
|
|
5461
|
-
return _date(ZodDate, params);
|
|
5462
|
-
}
|
|
5463
5370
|
var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
|
|
5464
5371
|
$ZodArray.init(inst, def);
|
|
5465
5372
|
ZodType.init(inst, def);
|
|
@@ -5885,8 +5792,10 @@ var GithubDeviceResponseSchema = object({
|
|
|
5885
5792
|
expires_in: number2().optional(),
|
|
5886
5793
|
verification_uri_complete: string2().optional()
|
|
5887
5794
|
});
|
|
5795
|
+
var DeviceReportShape = { timezone: unknown().optional(), locale: unknown().optional() };
|
|
5888
5796
|
var AuthTokenRequestSchema = object({
|
|
5889
|
-
session_id: string2().uuid()
|
|
5797
|
+
session_id: string2().uuid(),
|
|
5798
|
+
...DeviceReportShape
|
|
5890
5799
|
});
|
|
5891
5800
|
var AuthTokenPendingSchema = object({
|
|
5892
5801
|
status: literal("pending"),
|
|
@@ -5936,7 +5845,8 @@ var GuestStartRequestSchema = object({
|
|
|
5936
5845
|
consent_version: string2().min(1).max(32),
|
|
5937
5846
|
signup_src: string2().optional(),
|
|
5938
5847
|
referral_code: string2().optional(),
|
|
5939
|
-
client: object({ os: string2().max(64).optional(), cli_version: string2().max(64).optional() }).optional()
|
|
5848
|
+
client: object({ os: string2().max(64).optional(), cli_version: string2().max(64).optional() }).optional(),
|
|
5849
|
+
...DeviceReportShape
|
|
5940
5850
|
});
|
|
5941
5851
|
var ExperimentVariantSchema = _enum(["control", "treatment"]);
|
|
5942
5852
|
var ExperimentAssignmentSchema = object({
|
|
@@ -6003,6 +5913,7 @@ var EarnRewardSurfaceSchema = object({
|
|
|
6003
5913
|
remaining_micros: string2(),
|
|
6004
5914
|
tokens_about: number2().optional()
|
|
6005
5915
|
});
|
|
5916
|
+
var SlowTierSurfaceSchema = object({ until: string2(), line: string2() });
|
|
6006
5917
|
var MeResponseSchema = object({
|
|
6007
5918
|
user_id: string2(),
|
|
6008
5919
|
handle: string2(),
|
|
@@ -6036,8 +5947,10 @@ var MeResponseSchema = object({
|
|
|
6036
5947
|
return_bonus: ReturnBonusSurfaceSchema.optional(),
|
|
6037
5948
|
streak: StreakSurfaceSchema.optional(),
|
|
6038
5949
|
visible_allowance: boolean2().optional(),
|
|
5950
|
+
choice_prompts_arm: _enum(["control", "treatment"]).optional(),
|
|
6039
5951
|
earn_reward: EarnRewardSurfaceSchema.nullable().optional(),
|
|
6040
|
-
active_perks: array(object({ kind: string2(), models: array(string2()), ends_at: string2() })).optional()
|
|
5952
|
+
active_perks: array(object({ kind: string2(), models: array(string2()), ends_at: string2() })).optional(),
|
|
5953
|
+
slow_tier: SlowTierSurfaceSchema.optional()
|
|
6041
5954
|
});
|
|
6042
5955
|
var MeStatsResponseSchema = object({
|
|
6043
5956
|
user_id: string2(),
|
|
@@ -6063,6 +5976,7 @@ var MeStatsResponseSchema = object({
|
|
|
6063
5976
|
credit_tiers: boolean2().optional(),
|
|
6064
5977
|
premium_get_line: string2().optional(),
|
|
6065
5978
|
use_bonus: string2().optional(),
|
|
5979
|
+
slow_tier: SlowTierSurfaceSchema.optional(),
|
|
6066
5980
|
request_count_today: number2(),
|
|
6067
5981
|
prompt_tokens_today: number2(),
|
|
6068
5982
|
completion_tokens_today: number2(),
|
|
@@ -6164,497 +6078,12 @@ var ReferralStatusResponseSchema = object({
|
|
|
6164
6078
|
var ReferralLeaderboardResponseSchema = object({
|
|
6165
6079
|
entries: array(object({ rank: number2(), qualified_count: number2(), handle: string2().nullable() }))
|
|
6166
6080
|
});
|
|
6081
|
+
|
|
6167
6082
|
// ../shared/src/errors.ts
|
|
6168
6083
|
function assertExhaustiveErrorCode(code) {
|
|
6169
6084
|
throw new Error(`unhandled error code: ${String(code)}`);
|
|
6170
6085
|
}
|
|
6171
|
-
|
|
6172
|
-
function usdToMicros(value) {
|
|
6173
|
-
const s = typeof value === "number" ? value.toFixed(6) : String(value).trim();
|
|
6174
|
-
const negative = s.startsWith("-");
|
|
6175
|
-
const [whole = "0", frac = ""] = (negative ? s.slice(1) : s).split(".");
|
|
6176
|
-
const micros = BigInt(whole || "0") * 1000000n + BigInt(`${frac}000000`.slice(0, 6));
|
|
6177
|
-
return negative ? -micros : micros;
|
|
6178
|
-
}
|
|
6179
|
-
|
|
6180
|
-
// ../shared/src/config.ts
|
|
6181
|
-
var ConfigSchema = object({
|
|
6182
|
-
perUserDailyCapUsd: number2().default(1.125),
|
|
6183
|
-
perUserRollingCapUsd: number2().default(0),
|
|
6184
|
-
rollingCapWindowHours: number2().int().default(5),
|
|
6185
|
-
globalDailyCapUsd: number2().default(50),
|
|
6186
|
-
contextCharBudget: number2().int().default(1200000),
|
|
6187
|
-
maxOutputTokens: number2().int().min(1).default(32768),
|
|
6188
|
-
accountsPerDayCeiling: number2().int().default(500),
|
|
6189
|
-
authRatePerIpPerHour: number2().int().default(30),
|
|
6190
|
-
siteLoginStartRateLimitEnabled: boolean2().default(false),
|
|
6191
|
-
siteLoginStartPerIp: number2().int().min(0).default(5),
|
|
6192
|
-
siteLoginStartPerSubject: number2().int().min(0).default(5),
|
|
6193
|
-
siteLoginStartWindowSeconds: number2().int().min(1).default(600),
|
|
6194
|
-
siteLoginStartCooldownSeconds: number2().int().min(0).default(60),
|
|
6195
|
-
webBuyUrlRatePerProviderPerHour: number2().int().default(20),
|
|
6196
|
-
handoffTokenRatePerProviderPerHour: number2().int().default(60),
|
|
6197
|
-
handoffHourlyCompletionCap: number2().int().default(120),
|
|
6198
|
-
tokenRatePerIpPerHour: number2().int().default(3600),
|
|
6199
|
-
consentRatePerIpPerHour: number2().int().default(60),
|
|
6200
|
-
adInlineTurnFrequency: number2().int().default(5),
|
|
6201
|
-
adMinColumns: number2().int().default(60),
|
|
6202
|
-
proxyBaseUrl: string2().default("https://api.freepi.ai"),
|
|
6203
|
-
modelId: string2().default("deepseek/deepseek-v4-flash"),
|
|
6204
|
-
modelDisplay: string2().optional(),
|
|
6205
|
-
modelNotice: string2().optional(),
|
|
6206
|
-
providerDataCollection: _enum(["allow", "deny"]).optional(),
|
|
6207
|
-
promptCacheEnabled: boolean2().default(false),
|
|
6208
|
-
emailCaptureEnabled: boolean2().default(false),
|
|
6209
|
-
lifecycleEmailExperimentEnabled: boolean2().default(false),
|
|
6210
|
-
lifecycleEmailEnabled: boolean2().default(false),
|
|
6211
|
-
lifecycleEmailSend: boolean2().default(false),
|
|
6212
|
-
emailProviderConfigured: boolean2().default(false),
|
|
6213
|
-
emailFrom: string2().optional(),
|
|
6214
|
-
emailReplyTo: string2().optional(),
|
|
6215
|
-
emailPostalAddress: string2().optional(),
|
|
6216
|
-
emailProviderUrl: string2().optional(),
|
|
6217
|
-
hiveEnabled: boolean2().default(false),
|
|
6218
|
-
earnEnabled: boolean2().default(false),
|
|
6219
|
-
earnShare: number2().min(0).max(1).default(0.1),
|
|
6220
|
-
earnBudgetMicros: bigint2().default(300000000n),
|
|
6221
|
-
earnTestGithubIds: array(string2().regex(/^\d{1,20}$/)).default([]),
|
|
6222
|
-
providerOrder: array(string2()).optional(),
|
|
6223
|
-
openrouterFlashProviderOrder: array(string2()).default([
|
|
6224
|
-
"streamlake/fp8",
|
|
6225
|
-
"deepinfra/fp8",
|
|
6226
|
-
"gmicloud/fp8",
|
|
6227
|
-
"digitalocean"
|
|
6228
|
-
]),
|
|
6229
|
-
openrouterFlashProviderIgnore: array(string2()).default(["baidu", "relace"]),
|
|
6230
|
-
openrouterFlashMaxPricePrompt: string2().default("0.12"),
|
|
6231
|
-
openrouterFlashMaxPriceCompletion: string2().default("0.25"),
|
|
6232
|
-
openrouterSessionPinTtlSeconds: number2().int().min(1).default(1800),
|
|
6233
|
-
costAlertMultiplier: number2().int().min(1).default(2),
|
|
6234
|
-
costAlertMinHours: number2().int().min(1).default(7),
|
|
6235
|
-
tierNewMaxDays: number2().int().default(30),
|
|
6236
|
-
tierYoungMaxDays: number2().int().default(365),
|
|
6237
|
-
tierCapNewUsd: number2().default(0.45),
|
|
6238
|
-
tierCapYoungUsd: number2().default(1.125),
|
|
6239
|
-
fallbackLaneUsd: number2().default(0.225),
|
|
6240
|
-
fallbackHourlyCap: number2().int().default(10),
|
|
6241
|
-
fallbackLaneMinHeadroomBps: number2().int().default(1000),
|
|
6242
|
-
queueWaitRetryAfterSeconds: number2().int().default(0),
|
|
6243
|
-
sessionLeaseTtlMinutes: number2().int().default(30),
|
|
6244
|
-
signedInMaxCliSessions: number2().int().min(1).default(4),
|
|
6245
|
-
shutdownDrainMs: number2().int().min(0).max(25000).default(20000),
|
|
6246
|
-
premiumRatioThreshold: number2().int().min(1).default(5),
|
|
6247
|
-
fallbackExpensiveRatio: number2().int().min(1).default(5),
|
|
6248
|
-
larprouterTrialModels: array(string2()).default([]),
|
|
6249
|
-
larprouterTrialSharePct: number2().int().min(0).max(100).default(0),
|
|
6250
|
-
larprouterBaseUrl: string2().default("https://larprouter.com/v1"),
|
|
6251
|
-
larprouterFallbackModels: array(string2()).default([]),
|
|
6252
|
-
larprouterFallbackTerminal: boolean2().default(false),
|
|
6253
|
-
larprouterTtftMs: number2().int().min(1).default(8000),
|
|
6254
|
-
larprouterPrices: record(string2(), object({ input: string2(), cacheRead: string2(), output: string2() })).default({ "deepseek-v4.1-flash-1017-non-abliterated": { input: "80000", cacheRead: "10000", output: "350000" } }),
|
|
6255
|
-
larprouterModelMap: record(string2(), string2()).default({
|
|
6256
|
-
"deepseek-v4-flash": "deepseek-v4.1-flash-1017-non-abliterated",
|
|
6257
|
-
"deepseek-v4.1-flash": "deepseek-v4.1-flash-1017-non-abliterated"
|
|
6258
|
-
}),
|
|
6259
|
-
experientialBaseUrl: string2().default("https://api.experientiallabs.ai/v1"),
|
|
6260
|
-
experientialModelMap: record(string2(), string2()).default({
|
|
6261
|
-
"deepseek-v4.1-flash": "deepseek-v4.1-flash-1017",
|
|
6262
|
-
"deepseek-v4-flash": "deepseek-v4.1-flash-1017",
|
|
6263
|
-
"gpt-6-luna": "gpt-6-luna"
|
|
6264
|
-
}),
|
|
6265
|
-
experientialPrimaryModels: array(string2()).default([]),
|
|
6266
|
-
experientialFallbackModels: array(string2()).default([]),
|
|
6267
|
-
experientialTtftMs: number2().int().min(1).default(1e4),
|
|
6268
|
-
experientialMaxCostUsdPerRequest: string2().default("0.0005"),
|
|
6269
|
-
experientialPriceCooldownMs: number2().int().min(1).default(3600000),
|
|
6270
|
-
particleEnabled: boolean2().default(false),
|
|
6271
|
-
particleBaseUrl: string2().default("https://api.particle.ai/v1"),
|
|
6272
|
-
particleModel: string2().default("particle-1"),
|
|
6273
|
-
particleTtftMs: number2().int().min(1).default(1e4),
|
|
6274
|
-
minCliVersion: string2().default("0.0.0"),
|
|
6275
|
-
latestCliVersion: string2().default("0.2.0"),
|
|
6276
|
-
minDesktopVersion: string2().default("0.0.0"),
|
|
6277
|
-
latestDesktopVersion: string2().default("0.0.0"),
|
|
6278
|
-
desktopDownloadUrl: string2().default(""),
|
|
6279
|
-
minWebVersion: string2().default("0.0.0"),
|
|
6280
|
-
latestWebVersion: string2().default("0.0.0"),
|
|
6281
|
-
workspaceMaxStreams: number2().int().min(1).default(1),
|
|
6282
|
-
gravityDesktopEnabled: boolean2().default(false),
|
|
6283
|
-
abuseWindowMinutes: number2().int().default(30),
|
|
6284
|
-
abuseSettleGraceSeconds: number2().int().default(120),
|
|
6285
|
-
abuseGraceSettledStarts: number2().int().default(2),
|
|
6286
|
-
abuseUnverifiedMinStarts: number2().int().default(3),
|
|
6287
|
-
abuseUnverifiedMinFinishes: number2().int().default(2),
|
|
6288
|
-
abuseUnverifiedRestrictHours: number2().int().default(6),
|
|
6289
|
-
abuseLikelyBypassMinStarts: number2().int().default(5),
|
|
6290
|
-
abuseLikelyBypassRestrictHours: number2().int().default(24),
|
|
6291
|
-
abuseRepeatWindowDays: number2().int().default(7),
|
|
6292
|
-
abuseRepeatMinOccurrences: number2().int().default(3),
|
|
6293
|
-
abuseRestrictedCapUsd: number2().default(0.5),
|
|
6294
|
-
abuseRestrictedHourlyCap: number2().int().default(5),
|
|
6295
|
-
abuseNormalHourlyCap: number2().int().default(30),
|
|
6296
|
-
abuseSignedInNormalHourlyCap: number2().int().default(0),
|
|
6297
|
-
abuseDay0SkipHourly: boolean2().default(true),
|
|
6298
|
-
abuseFlipAlertThreshold: number2().int().default(10),
|
|
6299
|
-
abuseAdBypassMinRequests: number2().int().default(20),
|
|
6300
|
-
abuseAdBypassRatio: number2().int().default(10),
|
|
6301
|
-
traceMaxInflightWrites: number2().int().default(32),
|
|
6302
|
-
catalogCacheTtlSeconds: number2().int().default(60),
|
|
6303
|
-
eventMaxInflightWrites: number2().int().default(128),
|
|
6304
|
-
guestEnabled: boolean2().default(false),
|
|
6305
|
-
guestAllowanceMicros: bigint2().default(30000n),
|
|
6306
|
-
guestGlobalDailyCapMicros: bigint2().default(3000000n),
|
|
6307
|
-
guestHoldMicros: bigint2().default(5000n),
|
|
6308
|
-
guestIpSessionsPerDay: number2().int().default(8),
|
|
6309
|
-
guestIpAllowanceMicrosPerDay: bigint2().default(240000n),
|
|
6310
|
-
guestMaxConcurrentStreams: number2().int().default(10),
|
|
6311
|
-
guestMinRequestHeadroomMicros: bigint2().default(5000n),
|
|
6312
|
-
guestIpHashRetentionDays: number2().int().default(7),
|
|
6313
|
-
returnBonusDay2Credits: number2().int().default(2000),
|
|
6314
|
-
returnBonusDay7Credits: number2().int().default(1000),
|
|
6315
|
-
guestGithubBonusCredits: number2().int().default(1e4),
|
|
6316
|
-
returnNudgeEmailEnabled: boolean2().default(false),
|
|
6317
|
-
referralBonusCreditsPerDay: number2().int().default(5000),
|
|
6318
|
-
referralBonusDays: number2().int().default(30),
|
|
6319
|
-
referralMaxActivePerReferrer: number2().int().default(10),
|
|
6320
|
-
referralGithubMinAgeDays: number2().int().default(30),
|
|
6321
|
-
referralCreditGrantCredits: number2().int().default(0),
|
|
6322
|
-
webClaimCredits: number2().int().default(0),
|
|
6323
|
-
bountyCredits: number2().int().positive().default(5000),
|
|
6324
|
-
bountyMonthlyCapUsd: string2().regex(/^\d+(\.\d{1,6})?$/).default("50"),
|
|
6325
|
-
geoipDbPath: string2().optional(),
|
|
6326
|
-
sanctionedCountries: array(string2()).default(["IR", "CU", "KP", "SY"]),
|
|
6327
|
-
sanctionedTimezones: array(string2()).default(["Asia/Tehran", "Asia/Pyongyang", "America/Havana", "Asia/Damascus", "Europe/Simferopol"]),
|
|
6328
|
-
regionLimitsStart: date3().optional(),
|
|
6329
|
-
fullAllowanceCountries: array(string2()).default(["US", "CA", "GB", "AU", "NZ", "IE"]),
|
|
6330
|
-
priorityCountries: array(string2()).default(["US", "CA", "AU", "NZ"]),
|
|
6331
|
-
freeSpeed: _enum(["off", "log", "on"]).default("log"),
|
|
6332
|
-
freeSpeedDelaysS: array(number2().int().min(0).max(120)).length(4).default([0, 10, 5, 20]),
|
|
6333
|
-
winbackEmail: _enum(["off", "dry_run", "live"]).default("off"),
|
|
6334
|
-
winbackMinDays: number2().int().default(3),
|
|
6335
|
-
winbackMaxDays: number2().int().default(14),
|
|
6336
|
-
winbackMinRequests: number2().int().default(5),
|
|
6337
|
-
winbackDailyMax: number2().int().default(100),
|
|
6338
|
-
winbackClaimHours: number2().int().default(72),
|
|
6339
|
-
winbackContentImportBullet: boolean2().default(false),
|
|
6340
|
-
winbackOfferPriority: string2().default("credits:10000"),
|
|
6341
|
-
winbackOfferOther: string2().default("credits:3000"),
|
|
6342
|
-
winbackArmsPriority: array(string2()).default([]),
|
|
6343
|
-
winbackArmsOther: array(string2()).default([]),
|
|
6344
|
-
regionLowCapMicros: bigint2().default(100000n),
|
|
6345
|
-
streaksEnabled: _enum(["off", "log", "on"]).default("log"),
|
|
6346
|
-
streakMinRequests: number2().int().min(1).default(10),
|
|
6347
|
-
streakRewards: array(object({ day: number2().int().min(1), credits: number2().int().min(1) })).default([
|
|
6348
|
-
{ day: 3, credits: 2000 },
|
|
6349
|
-
{ day: 7, credits: 5000 },
|
|
6350
|
-
{ day: 14, credits: 1e4 },
|
|
6351
|
-
{ day: 30, credits: 20000 }
|
|
6352
|
-
])
|
|
6353
|
-
});
|
|
6354
|
-
function envNumber(value) {
|
|
6355
|
-
if (value === undefined || value === "")
|
|
6356
|
-
return;
|
|
6357
|
-
const n = Number(value);
|
|
6358
|
-
return Number.isNaN(n) ? undefined : n;
|
|
6359
|
-
}
|
|
6360
|
-
function envEmailAddress(name, value) {
|
|
6361
|
-
const v = value?.trim();
|
|
6362
|
-
if (!v)
|
|
6363
|
-
return;
|
|
6364
|
-
if (!/^([^<>"\\,;\r\n]+ <[^\s<>@,;]+@[^\s<>@,;]+\.[^\s<>@,;]+>|[^\s<>@,;]+@[^\s<>@,;]+\.[^\s<>@,;]+)$/.test(v)) {
|
|
6365
|
-
console.warn(`${name} ignored: must be an address like "help@freepi.ai" or "Name <help@freepi.ai>"`);
|
|
6366
|
-
return;
|
|
6367
|
-
}
|
|
6368
|
-
return v;
|
|
6369
|
-
}
|
|
6370
|
-
function envMicros(name, value) {
|
|
6371
|
-
if (value === undefined || value === "")
|
|
6372
|
-
return;
|
|
6373
|
-
const v = value.trim();
|
|
6374
|
-
if (!/^\d+(\.\d{1,6})?$/.test(v)) {
|
|
6375
|
-
throw new Error(`${name} must be a non-negative decimal USD amount with at most 6 decimals, got "${value}"`);
|
|
6376
|
-
}
|
|
6377
|
-
return usdToMicros(v);
|
|
6378
|
-
}
|
|
6379
|
-
function envStrictInt(name, value) {
|
|
6380
|
-
if (value === undefined || value === "")
|
|
6381
|
-
return;
|
|
6382
|
-
const v = value.trim();
|
|
6383
|
-
if (!/^\d+$/.test(v))
|
|
6384
|
-
throw new Error(`${name} must be a non-negative integer, got "${value}"`);
|
|
6385
|
-
return Number(v);
|
|
6386
|
-
}
|
|
6387
|
-
function envPairs(value) {
|
|
6388
|
-
const out = {};
|
|
6389
|
-
for (const part of (value ?? "").split(",")) {
|
|
6390
|
-
const bits = part.split("=").map((s) => s.trim());
|
|
6391
|
-
if (bits.length === 2 && bits[0] && bits[1])
|
|
6392
|
-
out[bits[0]] = bits[1];
|
|
6393
|
-
}
|
|
6394
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
6395
|
-
}
|
|
6396
|
-
function usdToMicrosString(v) {
|
|
6397
|
-
const m = /^(\d{1,6})(?:\.(\d{1,6}))?$/.exec(v.trim());
|
|
6398
|
-
return m ? (BigInt(m[1]) * 1000000n + BigInt((m[2] ?? "").padEnd(6, "0"))).toString() : null;
|
|
6399
|
-
}
|
|
6400
|
-
function envPrices(value) {
|
|
6401
|
-
const out = {};
|
|
6402
|
-
for (const part of (value ?? "").split(",")) {
|
|
6403
|
-
const bits = part.split("=").map((s) => s.trim());
|
|
6404
|
-
const nums = bits.length === 2 ? bits[1].split("/").map(usdToMicrosString) : [];
|
|
6405
|
-
if (bits[0] && nums.length === 3 && nums.every((n) => n !== null))
|
|
6406
|
-
out[bits[0]] = { input: nums[0], cacheRead: nums[1], output: nums[2] };
|
|
6407
|
-
}
|
|
6408
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
6409
|
-
}
|
|
6410
|
-
function envPercent(value) {
|
|
6411
|
-
const n = envNumber(value);
|
|
6412
|
-
return n === undefined ? undefined : Math.min(100, Math.max(0, Math.trunc(n)));
|
|
6413
|
-
}
|
|
6414
|
-
function envBoolean(value) {
|
|
6415
|
-
if (value === undefined || value === "")
|
|
6416
|
-
return;
|
|
6417
|
-
return value === "true" || value === "1";
|
|
6418
|
-
}
|
|
6419
|
-
function envFlagOn(value) {
|
|
6420
|
-
return ["true", "1", "on", "yes"].includes((value ?? "").trim().toLowerCase());
|
|
6421
|
-
}
|
|
6422
|
-
function envList(value) {
|
|
6423
|
-
if (!value)
|
|
6424
|
-
return;
|
|
6425
|
-
const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6426
|
-
return items.length > 0 ? items : undefined;
|
|
6427
|
-
}
|
|
6428
|
-
function envCountries(value) {
|
|
6429
|
-
return envList(value)?.map((s) => s.toUpperCase());
|
|
6430
|
-
}
|
|
6431
|
-
function envTimezones(value) {
|
|
6432
|
-
if (value === undefined)
|
|
6433
|
-
return;
|
|
6434
|
-
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6435
|
-
}
|
|
6436
|
-
function envStreakRewards(value) {
|
|
6437
|
-
if (!value?.trim())
|
|
6438
|
-
return;
|
|
6439
|
-
const seen = new Set;
|
|
6440
|
-
return value.split(",").map((part) => {
|
|
6441
|
-
const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(part);
|
|
6442
|
-
const day = m ? Number(m[1]) : 0;
|
|
6443
|
-
const credits = m ? Number(m[2]) : 0;
|
|
6444
|
-
if (!m || day < 1 || credits < 1 || seen.has(day)) {
|
|
6445
|
-
throw new Error(`STREAK_REWARDS must be "day:credits" pairs with distinct days, got "${value}"`);
|
|
6446
|
-
}
|
|
6447
|
-
seen.add(day);
|
|
6448
|
-
return { day, credits };
|
|
6449
|
-
});
|
|
6450
|
-
}
|
|
6451
|
-
function envStreaksMode(value) {
|
|
6452
|
-
if (value === undefined || value.trim() === "")
|
|
6453
|
-
return;
|
|
6454
|
-
const v = value.trim().toLowerCase();
|
|
6455
|
-
if (v !== "off" && v !== "log" && v !== "on")
|
|
6456
|
-
throw new Error(`STREAKS_ENABLED must be off, log or on, got "${value}"`);
|
|
6457
|
-
return v;
|
|
6458
|
-
}
|
|
6459
|
-
function envFreeSpeedMode(value) {
|
|
6460
|
-
if (value === undefined || value.trim() === "")
|
|
6461
|
-
return;
|
|
6462
|
-
const v = value.trim().toLowerCase();
|
|
6463
|
-
if (v !== "off" && v !== "log" && v !== "on")
|
|
6464
|
-
throw new Error(`FREE_SPEED must be off, log or on, got "${value}"`);
|
|
6465
|
-
return v;
|
|
6466
|
-
}
|
|
6467
|
-
function envFreeSpeedDelays(value) {
|
|
6468
|
-
if (!value?.trim())
|
|
6469
|
-
return;
|
|
6470
|
-
const parts = value.split(",").map((s) => s.trim());
|
|
6471
|
-
const nums = parts.map((s) => /^\d+$/.test(s) ? Number(s) : NaN);
|
|
6472
|
-
if (nums.length !== 4 || nums.some((n) => !Number.isInteger(n) || n > 120)) {
|
|
6473
|
-
throw new Error(`FREE_SPEED_DELAYS must be four whole seconds 0-120 (priority+clicking, priority+not_clicking, other+clicking, other+not_clicking), got "${value}"`);
|
|
6474
|
-
}
|
|
6475
|
-
return nums;
|
|
6476
|
-
}
|
|
6477
|
-
function envDate(value) {
|
|
6478
|
-
if (!value?.trim())
|
|
6479
|
-
return;
|
|
6480
|
-
const d = new Date(value.trim());
|
|
6481
|
-
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
6482
|
-
}
|
|
6483
|
-
function envDecimalString(value) {
|
|
6484
|
-
if (value === undefined || value.trim() === "")
|
|
6485
|
-
return;
|
|
6486
|
-
const t = value.trim();
|
|
6487
|
-
return /^\d+(\.\d+)?$/.test(t) ? t : undefined;
|
|
6488
|
-
}
|
|
6489
|
-
function envBountyCap(value) {
|
|
6490
|
-
const t = value?.trim();
|
|
6491
|
-
if (!t)
|
|
6492
|
-
return;
|
|
6493
|
-
if (!/^\d+(\.\d{1,6})?$/.test(t))
|
|
6494
|
-
throw new Error(`BOUNTY_MONTHLY_CAP_USD must be a decimal like "50" or "0.50", got "${value}"`);
|
|
6495
|
-
return t;
|
|
6496
|
-
}
|
|
6497
|
-
function loadConfig(env = process.env) {
|
|
6498
|
-
return ConfigSchema.parse({
|
|
6499
|
-
perUserDailyCapUsd: envNumber(env.PER_USER_DAILY_CAP_USD),
|
|
6500
|
-
perUserRollingCapUsd: envNumber(env.PER_USER_ROLLING_CAP_USD),
|
|
6501
|
-
rollingCapWindowHours: envNumber(env.ROLLING_CAP_WINDOW_HOURS),
|
|
6502
|
-
globalDailyCapUsd: envNumber(env.GLOBAL_DAILY_CAP_USD),
|
|
6503
|
-
contextCharBudget: envNumber(env.CONTEXT_CHAR_BUDGET),
|
|
6504
|
-
maxOutputTokens: envNumber(env.MAX_OUTPUT_TOKENS),
|
|
6505
|
-
accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
|
|
6506
|
-
authRatePerIpPerHour: envNumber(env.AUTH_RATE_PER_IP_PER_HOUR),
|
|
6507
|
-
siteLoginStartRateLimitEnabled: envFlagOn(env.SITE_LOGIN_START_RATE_LIMIT),
|
|
6508
|
-
siteLoginStartPerIp: envNumber(env.SITE_LOGIN_START_PER_IP),
|
|
6509
|
-
siteLoginStartPerSubject: envNumber(env.SITE_LOGIN_START_PER_SUBJECT),
|
|
6510
|
-
siteLoginStartWindowSeconds: envNumber(env.SITE_LOGIN_START_WINDOW_SECONDS),
|
|
6511
|
-
siteLoginStartCooldownSeconds: envNumber(env.SITE_LOGIN_START_COOLDOWN_SECONDS),
|
|
6512
|
-
webBuyUrlRatePerProviderPerHour: envNumber(env.WEB_BUY_URL_RATE_PER_PROVIDER_PER_HOUR),
|
|
6513
|
-
handoffTokenRatePerProviderPerHour: envNumber(env.HANDOFF_TOKEN_RATE_PER_PROVIDER_PER_HOUR),
|
|
6514
|
-
handoffHourlyCompletionCap: envNumber(env.HANDOFF_HOURLY_COMPLETION_CAP),
|
|
6515
|
-
tokenRatePerIpPerHour: envNumber(env.TOKEN_RATE_PER_IP_PER_HOUR),
|
|
6516
|
-
consentRatePerIpPerHour: envNumber(env.CONSENT_RATE_PER_IP_PER_HOUR),
|
|
6517
|
-
adInlineTurnFrequency: envNumber(env.AD_INLINE_TURN_FREQUENCY),
|
|
6518
|
-
adMinColumns: envNumber(env.AD_MIN_COLUMNS),
|
|
6519
|
-
proxyBaseUrl: env.PROXY_BASE_URL || undefined,
|
|
6520
|
-
modelId: env.MODEL_ID || undefined,
|
|
6521
|
-
modelDisplay: env.MODEL_DISPLAY || undefined,
|
|
6522
|
-
modelNotice: env.MODEL_NOTICE || undefined,
|
|
6523
|
-
providerDataCollection: env.PROVIDER_DATA_COLLECTION || undefined,
|
|
6524
|
-
promptCacheEnabled: envBoolean(env.PROMPT_CACHE_ENABLED),
|
|
6525
|
-
emailCaptureEnabled: envBoolean(env.EMAIL_CAPTURE_ENABLED),
|
|
6526
|
-
lifecycleEmailExperimentEnabled: envBoolean(env.LIFECYCLE_EMAIL_EXPERIMENT),
|
|
6527
|
-
lifecycleEmailEnabled: envBoolean(env.LIFECYCLE_EMAIL_ENABLED),
|
|
6528
|
-
lifecycleEmailSend: envBoolean(env.LIFECYCLE_EMAIL_SEND),
|
|
6529
|
-
emailProviderConfigured: env.EMAIL_PROVIDER_KEY ? env.EMAIL_PROVIDER_KEY.trim().length > 0 : undefined,
|
|
6530
|
-
emailFrom: env.EMAIL_FROM?.trim() || undefined,
|
|
6531
|
-
emailReplyTo: envEmailAddress("EMAIL_REPLY_TO", env.EMAIL_REPLY_TO),
|
|
6532
|
-
emailPostalAddress: env.EMAIL_POSTAL_ADDRESS?.trim() || undefined,
|
|
6533
|
-
emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
|
|
6534
|
-
hiveEnabled: envBoolean(env.HIVE_ENABLED),
|
|
6535
|
-
earnEnabled: envBoolean(env.EARN_ENABLED),
|
|
6536
|
-
earnShare: (() => {
|
|
6537
|
-
const n = envNumber(env.EARN_SHARE);
|
|
6538
|
-
return n === undefined ? undefined : Math.min(1, Math.max(0, n));
|
|
6539
|
-
})(),
|
|
6540
|
-
earnBudgetMicros: envStrictInt("EARN_BUDGET_MICROS", env.EARN_BUDGET_MICROS) === undefined ? undefined : BigInt(env.EARN_BUDGET_MICROS.trim()),
|
|
6541
|
-
earnTestGithubIds: envList(env.EARN_TEST_GITHUB_IDS)?.filter((id) => /^\d{1,20}$/.test(id)),
|
|
6542
|
-
providerOrder: envList(env.PROVIDER_ORDER),
|
|
6543
|
-
openrouterFlashProviderOrder: envList(env.OPENROUTER_FLASH_PROVIDER_ORDER),
|
|
6544
|
-
openrouterFlashProviderIgnore: envList(env.OPENROUTER_FLASH_PROVIDER_IGNORE),
|
|
6545
|
-
openrouterFlashMaxPricePrompt: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_PROMPT),
|
|
6546
|
-
openrouterFlashMaxPriceCompletion: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_COMPLETION),
|
|
6547
|
-
openrouterSessionPinTtlSeconds: envNumber(env.OPENROUTER_SESSION_PIN_TTL_SECONDS),
|
|
6548
|
-
costAlertMultiplier: envNumber(env.COST_ALERT_MULTIPLIER),
|
|
6549
|
-
costAlertMinHours: envNumber(env.COST_ALERT_MIN_HOURS),
|
|
6550
|
-
tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
|
|
6551
|
-
tierYoungMaxDays: envNumber(env.TIER_YOUNG_MAX_DAYS),
|
|
6552
|
-
tierCapNewUsd: envNumber(env.TIER_CAP_NEW_USD),
|
|
6553
|
-
tierCapYoungUsd: envNumber(env.TIER_CAP_YOUNG_USD),
|
|
6554
|
-
fallbackLaneUsd: envNumber(env.FALLBACK_LANE_USD),
|
|
6555
|
-
fallbackHourlyCap: envNumber(env.FALLBACK_HOURLY_CAP),
|
|
6556
|
-
fallbackLaneMinHeadroomBps: envNumber(env.FALLBACK_LANE_MIN_HEADROOM_BPS),
|
|
6557
|
-
queueWaitRetryAfterSeconds: envNumber(env.QUEUE_WAIT_RETRY_AFTER_SECONDS),
|
|
6558
|
-
sessionLeaseTtlMinutes: envNumber(env.SESSION_LEASE_TTL_MINUTES),
|
|
6559
|
-
signedInMaxCliSessions: envNumber(env.SIGNED_IN_MAX_CLI_SESSIONS),
|
|
6560
|
-
shutdownDrainMs: envNumber(env.SHUTDOWN_DRAIN_MS),
|
|
6561
|
-
premiumRatioThreshold: envNumber(env.PREMIUM_RATIO_THRESHOLD),
|
|
6562
|
-
fallbackExpensiveRatio: envNumber(env.FALLBACK_EXPENSIVE_RATIO),
|
|
6563
|
-
larprouterTrialModels: envList(env.LARPROUTER_TRIAL_MODELS),
|
|
6564
|
-
larprouterTrialSharePct: envPercent(env.LARPROUTER_TRIAL_SHARE_PCT),
|
|
6565
|
-
larprouterFallbackModels: envList(env.LARPROUTER_FALLBACK_MODELS),
|
|
6566
|
-
larprouterTtftMs: envNumber(env.LARPROUTER_TTFT_MS),
|
|
6567
|
-
larprouterFallbackTerminal: envBoolean(env.LARPROUTER_FALLBACK_TERMINAL),
|
|
6568
|
-
larprouterModelMap: envPairs(env.LARPROUTER_MODEL_MAP),
|
|
6569
|
-
larprouterPrices: envPrices(env.LARPROUTER_PRICES),
|
|
6570
|
-
larprouterBaseUrl: env.LARPROUTER_BASE_URL?.trim().replace(/\/$/, "") || undefined,
|
|
6571
|
-
experientialBaseUrl: env.EXPERIENTIAL_BASE_URL?.trim().replace(/\/$/, "") || undefined,
|
|
6572
|
-
experientialModelMap: envPairs(env.EXPERIENTIAL_MODEL_MAP),
|
|
6573
|
-
experientialPrimaryModels: envList(env.EXPERIENTIAL_PRIMARY_MODELS),
|
|
6574
|
-
experientialFallbackModels: envList(env.EXPERIENTIAL_FALLBACK_MODELS),
|
|
6575
|
-
experientialTtftMs: envNumber(env.EXPERIENTIAL_TTFT_MS),
|
|
6576
|
-
experientialMaxCostUsdPerRequest: /^\d{1,6}(\.\d{1,6})?$/.test(env.EXPERIENTIAL_MAX_COST_USD_PER_REQUEST?.trim() ?? "") ? env.EXPERIENTIAL_MAX_COST_USD_PER_REQUEST.trim() : undefined,
|
|
6577
|
-
experientialPriceCooldownMs: envNumber(env.EXPERIENTIAL_PRICE_COOLDOWN_MS),
|
|
6578
|
-
particleEnabled: envBoolean(env.PARTICLE_ENABLED),
|
|
6579
|
-
particleBaseUrl: env.PARTICLE_BASE_URL?.trim().replace(/\/$/, "") || undefined,
|
|
6580
|
-
particleModel: env.PARTICLE_MODEL?.trim() || undefined,
|
|
6581
|
-
particleTtftMs: envNumber(env.PARTICLE_TTFT_MS),
|
|
6582
|
-
minCliVersion: env.MIN_CLI_VERSION || undefined,
|
|
6583
|
-
latestCliVersion: env.LATEST_CLI_VERSION || undefined,
|
|
6584
|
-
minDesktopVersion: env.MIN_DESKTOP_VERSION || undefined,
|
|
6585
|
-
latestDesktopVersion: env.LATEST_DESKTOP_VERSION || undefined,
|
|
6586
|
-
desktopDownloadUrl: env.DESKTOP_DOWNLOAD_URL || undefined,
|
|
6587
|
-
minWebVersion: env.MIN_WEB_VERSION || undefined,
|
|
6588
|
-
latestWebVersion: env.LATEST_WEB_VERSION || undefined,
|
|
6589
|
-
workspaceMaxStreams: envNumber(env.WORKSPACE_MAX_STREAMS),
|
|
6590
|
-
gravityDesktopEnabled: env.GRAVITY_DESKTOP_ENABLED === "true" ? true : undefined,
|
|
6591
|
-
abuseWindowMinutes: envNumber(env.ABUSE_WINDOW_MINUTES),
|
|
6592
|
-
abuseSettleGraceSeconds: envNumber(env.ABUSE_SETTLE_GRACE_SECONDS),
|
|
6593
|
-
abuseGraceSettledStarts: envNumber(env.ABUSE_GRACE_SETTLED_STARTS),
|
|
6594
|
-
abuseUnverifiedMinStarts: envNumber(env.ABUSE_UNVERIFIED_MIN_STARTS),
|
|
6595
|
-
abuseUnverifiedMinFinishes: envNumber(env.ABUSE_UNVERIFIED_MIN_FINISHES),
|
|
6596
|
-
abuseUnverifiedRestrictHours: envNumber(env.ABUSE_UNVERIFIED_RESTRICT_HOURS),
|
|
6597
|
-
abuseLikelyBypassMinStarts: envNumber(env.ABUSE_LIKELY_BYPASS_MIN_STARTS),
|
|
6598
|
-
abuseLikelyBypassRestrictHours: envNumber(env.ABUSE_LIKELY_BYPASS_RESTRICT_HOURS),
|
|
6599
|
-
abuseRepeatWindowDays: envNumber(env.ABUSE_REPEAT_WINDOW_DAYS),
|
|
6600
|
-
abuseRepeatMinOccurrences: envNumber(env.ABUSE_REPEAT_MIN_OCCURRENCES),
|
|
6601
|
-
abuseRestrictedCapUsd: envNumber(env.ABUSE_RESTRICTED_CAP_USD),
|
|
6602
|
-
abuseRestrictedHourlyCap: envNumber(env.ABUSE_RESTRICTED_HOURLY_CAP),
|
|
6603
|
-
abuseNormalHourlyCap: envNumber(env.ABUSE_NORMAL_HOURLY_CAP),
|
|
6604
|
-
abuseSignedInNormalHourlyCap: envNumber(env.ABUSE_SIGNED_IN_NORMAL_HOURLY_CAP),
|
|
6605
|
-
abuseDay0SkipHourly: envBoolean(env.ABUSE_DAY0_SKIP_HOURLY),
|
|
6606
|
-
abuseFlipAlertThreshold: envNumber(env.ABUSE_FLIP_ALERT_THRESHOLD),
|
|
6607
|
-
abuseAdBypassMinRequests: envNumber(env.ABUSE_AD_BYPASS_MIN_REQUESTS),
|
|
6608
|
-
abuseAdBypassRatio: envNumber(env.ABUSE_AD_BYPASS_RATIO),
|
|
6609
|
-
traceMaxInflightWrites: envNumber(env.TRACE_MAX_INFLIGHT_WRITES),
|
|
6610
|
-
catalogCacheTtlSeconds: envNumber(env.CATALOG_CACHE_TTL_SECONDS),
|
|
6611
|
-
eventMaxInflightWrites: envNumber(env.EVENT_MAX_INFLIGHT_WRITES),
|
|
6612
|
-
guestEnabled: envBoolean(env.GUEST_ENABLED),
|
|
6613
|
-
guestAllowanceMicros: envMicros("GUEST_ALLOWANCE_USD", env.GUEST_ALLOWANCE_USD),
|
|
6614
|
-
guestGlobalDailyCapMicros: envMicros("GUEST_GLOBAL_DAILY_CAP_USD", env.GUEST_GLOBAL_DAILY_CAP_USD),
|
|
6615
|
-
guestHoldMicros: envMicros("GUEST_HOLD_USD", env.GUEST_HOLD_USD),
|
|
6616
|
-
guestIpSessionsPerDay: envStrictInt("GUEST_IP_SESSIONS_PER_DAY", env.GUEST_IP_SESSIONS_PER_DAY),
|
|
6617
|
-
guestIpAllowanceMicrosPerDay: envMicros("GUEST_IP_ALLOWANCE_USD_PER_DAY", env.GUEST_IP_ALLOWANCE_USD_PER_DAY),
|
|
6618
|
-
guestMaxConcurrentStreams: envStrictInt("GUEST_MAX_CONCURRENT_STREAMS", env.GUEST_MAX_CONCURRENT_STREAMS),
|
|
6619
|
-
guestMinRequestHeadroomMicros: envMicros("GUEST_MIN_REQUEST_HEADROOM_USD", env.GUEST_MIN_REQUEST_HEADROOM_USD),
|
|
6620
|
-
guestIpHashRetentionDays: envStrictInt("GUEST_IP_HASH_RETENTION_DAYS", env.GUEST_IP_HASH_RETENTION_DAYS),
|
|
6621
|
-
returnBonusDay2Credits: envStrictInt("RETURN_BONUS_DAY2_CREDITS", env.RETURN_BONUS_DAY2_CREDITS),
|
|
6622
|
-
returnBonusDay7Credits: envStrictInt("RETURN_BONUS_DAY7_CREDITS", env.RETURN_BONUS_DAY7_CREDITS),
|
|
6623
|
-
guestGithubBonusCredits: envStrictInt("GUEST_GITHUB_BONUS_CREDITS", env.GUEST_GITHUB_BONUS_CREDITS),
|
|
6624
|
-
returnNudgeEmailEnabled: envBoolean(env.RETURN_NUDGE_EMAIL_ENABLED),
|
|
6625
|
-
referralBonusCreditsPerDay: envStrictInt("REFERRAL_BONUS_CREDITS_PER_DAY", env.REFERRAL_BONUS_CREDITS_PER_DAY),
|
|
6626
|
-
referralBonusDays: envStrictInt("REFERRAL_BONUS_DAYS", env.REFERRAL_BONUS_DAYS),
|
|
6627
|
-
referralMaxActivePerReferrer: envStrictInt("REFERRAL_MAX_ACTIVE_PER_REFERRER", env.REFERRAL_MAX_ACTIVE_PER_REFERRER),
|
|
6628
|
-
referralGithubMinAgeDays: envStrictInt("REFERRAL_GITHUB_MIN_AGE_DAYS", env.REFERRAL_GITHUB_MIN_AGE_DAYS),
|
|
6629
|
-
referralCreditGrantCredits: envStrictInt("REFERRAL_CREDIT_GRANT_CREDITS", env.REFERRAL_CREDIT_GRANT_CREDITS),
|
|
6630
|
-
webClaimCredits: envStrictInt("WEB_CLAIM_CREDITS", env.WEB_CLAIM_CREDITS),
|
|
6631
|
-
bountyCredits: envStrictInt("BOUNTY_CREDITS", env.BOUNTY_CREDITS),
|
|
6632
|
-
bountyMonthlyCapUsd: envBountyCap(env.BOUNTY_MONTHLY_CAP_USD),
|
|
6633
|
-
geoipDbPath: env.GEOIP_DB_PATH?.trim() || undefined,
|
|
6634
|
-
sanctionedCountries: envCountries(env.SANCTIONED_COUNTRIES),
|
|
6635
|
-
sanctionedTimezones: envTimezones(env.SANCTIONED_TIMEZONES),
|
|
6636
|
-
regionLimitsStart: envDate(env.REGION_LIMITS_START),
|
|
6637
|
-
fullAllowanceCountries: envCountries(env.FULL_ALLOWANCE_COUNTRIES),
|
|
6638
|
-
regionLowCapMicros: envMicros("REGION_LOW_CAP_USD", env.REGION_LOW_CAP_USD),
|
|
6639
|
-
priorityCountries: envCountries(env.PRIORITY_COUNTRIES),
|
|
6640
|
-
freeSpeed: envFreeSpeedMode(env.FREE_SPEED),
|
|
6641
|
-
freeSpeedDelaysS: envFreeSpeedDelays(env.FREE_SPEED_DELAYS),
|
|
6642
|
-
streaksEnabled: envStreaksMode(env.STREAKS_ENABLED),
|
|
6643
|
-
streakMinRequests: envStrictInt("STREAK_MIN_REQUESTS", env.STREAK_MIN_REQUESTS),
|
|
6644
|
-
streakRewards: envStreakRewards(env.STREAK_REWARDS),
|
|
6645
|
-
winbackEmail: env.WINBACK_EMAIL?.trim().toLowerCase() === "live" ? "live" : env.WINBACK_EMAIL?.trim().toLowerCase() === "dry_run" ? "dry_run" : undefined,
|
|
6646
|
-
winbackMinDays: envStrictInt("WINBACK_MIN_DAYS", env.WINBACK_MIN_DAYS),
|
|
6647
|
-
winbackMaxDays: envStrictInt("WINBACK_MAX_DAYS", env.WINBACK_MAX_DAYS),
|
|
6648
|
-
winbackMinRequests: envStrictInt("WINBACK_MIN_REQUESTS", env.WINBACK_MIN_REQUESTS),
|
|
6649
|
-
winbackDailyMax: envStrictInt("WINBACK_DAILY_MAX", env.WINBACK_DAILY_MAX),
|
|
6650
|
-
winbackClaimHours: envStrictInt("WINBACK_CLAIM_HOURS", env.WINBACK_CLAIM_HOURS),
|
|
6651
|
-
winbackContentImportBullet: envFlagOn(env.WINBACK_CONTENT_IMPORT_BULLET),
|
|
6652
|
-
winbackOfferPriority: env.WINBACK_OFFER_PRIORITY?.trim() || undefined,
|
|
6653
|
-
winbackOfferOther: env.WINBACK_OFFER_OTHER?.trim() || undefined,
|
|
6654
|
-
winbackArmsPriority: envList(env.WINBACK_ARMS_PRIORITY),
|
|
6655
|
-
winbackArmsOther: envList(env.WINBACK_ARMS_OTHER)
|
|
6656
|
-
});
|
|
6657
|
-
}
|
|
6086
|
+
|
|
6658
6087
|
// ../shared/src/credits.ts
|
|
6659
6088
|
var CREDITS_PER_USD = 40000 / 3;
|
|
6660
6089
|
function usdToCredits(usd) {
|
|
@@ -6765,6 +6194,7 @@ function formatLimitNotice(fields, opts = {}) {
|
|
|
6765
6194
|
bits.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
|
|
6766
6195
|
return bits.join(", ");
|
|
6767
6196
|
}
|
|
6197
|
+
|
|
6768
6198
|
// ../shared/src/version.ts
|
|
6769
6199
|
function compareVersions(a, b) {
|
|
6770
6200
|
const pa = a.split(".").map(Number);
|
|
@@ -6781,6 +6211,7 @@ function compareVersions(a, b) {
|
|
|
6781
6211
|
}
|
|
6782
6212
|
return 0;
|
|
6783
6213
|
}
|
|
6214
|
+
|
|
6784
6215
|
// ../shared/src/hive.ts
|
|
6785
6216
|
var HIVE_MAX_CHAT_CHARS = 2048;
|
|
6786
6217
|
var HIVE_MAX_PROMPT_CHARS = 8192;
|
|
@@ -6891,6 +6322,7 @@ var HiveClassifyResponseSchema = object({
|
|
|
6891
6322
|
decision: _enum(["allow", "ask"]),
|
|
6892
6323
|
reason: string2()
|
|
6893
6324
|
});
|
|
6325
|
+
|
|
6894
6326
|
// ../shared/src/credit-tiers.ts
|
|
6895
6327
|
var n = (v) => v.toLocaleString("en-US");
|
|
6896
6328
|
var day = (iso, timeZone) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", ...timeZone ? { timeZone } : {} }).format(new Date(iso));
|
|
@@ -6930,6 +6362,7 @@ function premiumExpiryWarning(i) {
|
|
|
6930
6362
|
return;
|
|
6931
6363
|
return `${n(i.expiring)} premium credits expire today at ${formatRefillEt(at, tz)}`;
|
|
6932
6364
|
}
|
|
6365
|
+
|
|
6933
6366
|
// src/cli-args.ts
|
|
6934
6367
|
var HELP_TEXT = `
|
|
6935
6368
|
free-pi — free AI coding in your terminal; ads pay for the inference.
|
|
@@ -7089,6 +6522,14 @@ function readAutoUpdate(configPath) {
|
|
|
7089
6522
|
return true;
|
|
7090
6523
|
}
|
|
7091
6524
|
}
|
|
6525
|
+
function readChoicePrompts(configPath) {
|
|
6526
|
+
try {
|
|
6527
|
+
const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
|
|
6528
|
+
return parsed?.choicePrompts !== false;
|
|
6529
|
+
} catch {
|
|
6530
|
+
return true;
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
7092
6533
|
|
|
7093
6534
|
// src/credentials.ts
|
|
7094
6535
|
import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
|
|
@@ -7234,17 +6675,31 @@ function startGravityPixel(jwt, storageDir, env = process.env, load = () => impo
|
|
|
7234
6675
|
|
|
7235
6676
|
// src/pi-launch.ts
|
|
7236
6677
|
import {
|
|
7237
|
-
createAgentSessionFromServices,
|
|
6678
|
+
createAgentSessionFromServices as createAgentSessionFromServices2,
|
|
7238
6679
|
createAgentSessionRuntime,
|
|
7239
|
-
createAgentSessionServices,
|
|
6680
|
+
createAgentSessionServices as createAgentSessionServices2,
|
|
7240
6681
|
createCodemodeExtension,
|
|
7241
6682
|
createMcpExtension,
|
|
7242
6683
|
createToolSearchExtension,
|
|
7243
6684
|
InteractiveMode,
|
|
7244
|
-
SessionManager,
|
|
7245
|
-
SettingsManager
|
|
6685
|
+
SessionManager as SessionManager2,
|
|
6686
|
+
SettingsManager as SettingsManager2
|
|
7246
6687
|
} from "@earendil-works/pi-coding-agent";
|
|
7247
6688
|
|
|
6689
|
+
// ../pi-ads/src/config.ts
|
|
6690
|
+
function envInt(value, fallback) {
|
|
6691
|
+
if (value === undefined || value === "")
|
|
6692
|
+
return fallback;
|
|
6693
|
+
const n = Number(value);
|
|
6694
|
+
return Number.isInteger(n) ? n : fallback;
|
|
6695
|
+
}
|
|
6696
|
+
function loadConfig(env = process.env) {
|
|
6697
|
+
return {
|
|
6698
|
+
adInlineTurnFrequency: envInt(env.AD_INLINE_TURN_FREQUENCY, 5),
|
|
6699
|
+
adMinColumns: envInt(env.AD_MIN_COLUMNS, 60)
|
|
6700
|
+
};
|
|
6701
|
+
}
|
|
6702
|
+
|
|
7248
6703
|
// ../pi-ads/src/api.ts
|
|
7249
6704
|
var REQUEST_TIMEOUT_MS = 5000;
|
|
7250
6705
|
async function authedFetch(deps, path, init) {
|
|
@@ -7398,6 +6853,7 @@ function createBannerRenderer(opts = {}) {
|
|
|
7398
6853
|
return;
|
|
7399
6854
|
if (!ad) {
|
|
7400
6855
|
screen.banner = undefined;
|
|
6856
|
+
screen.bannerAdId = undefined;
|
|
7401
6857
|
ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
|
|
7402
6858
|
opts.onBannerAd?.(undefined);
|
|
7403
6859
|
return;
|
|
@@ -7412,8 +6868,10 @@ function createBannerRenderer(opts = {}) {
|
|
|
7412
6868
|
if (isGravity && screen.inline)
|
|
7413
6869
|
screen.clearInline?.();
|
|
7414
6870
|
screen.banner = isGravity ? "gravity" : "own";
|
|
6871
|
+
screen.bannerAdId = ad.ad_id;
|
|
7415
6872
|
screen.clearBanner = () => {
|
|
7416
6873
|
screen.banner = undefined;
|
|
6874
|
+
screen.bannerAdId = undefined;
|
|
7417
6875
|
ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
|
|
7418
6876
|
opts.onBannerAd?.(undefined);
|
|
7419
6877
|
};
|
|
@@ -7454,6 +6912,8 @@ function createInlineRenderer(screen = {}) {
|
|
|
7454
6912
|
const isGravity = ad.source === "gravity";
|
|
7455
6913
|
if (screen.banner === "gravity")
|
|
7456
6914
|
return;
|
|
6915
|
+
if (!isGravity && screen.banner && screen.bannerAdId === ad.ad_id)
|
|
6916
|
+
return;
|
|
7457
6917
|
if (isGravity && screen.banner)
|
|
7458
6918
|
screen.clearBanner?.();
|
|
7459
6919
|
ctx.ui.setWidget(INLINE_WIDGET_ID, adWidget(ad.creative, ad.click_url, config.adMinColumns), { placement: "belowEditor" });
|
|
@@ -7521,6 +6981,8 @@ function mapErrorCode(code) {
|
|
|
7521
6981
|
return "this conversation is too long — run /compact or start a new session with /new";
|
|
7522
6982
|
case "reward_required":
|
|
7523
6983
|
return "This model is only for reward holders. Finish an EARN task from your terminal to unlock it, or switch models with /model.";
|
|
6984
|
+
case "subagents_unavailable":
|
|
6985
|
+
return "Subagents run on the free Particle model, or with credits. Switch with /model or run /buy-credits.";
|
|
7524
6986
|
default:
|
|
7525
6987
|
return assertExhaustiveErrorCode(code);
|
|
7526
6988
|
}
|
|
@@ -7712,7 +7174,7 @@ function createAdsExtension(opts) {
|
|
|
7712
7174
|
}
|
|
7713
7175
|
|
|
7714
7176
|
// src/pi-launch.ts
|
|
7715
|
-
import { dirname as
|
|
7177
|
+
import { dirname as dirname11, join as join11, resolve as resolve4 } from "node:path";
|
|
7716
7178
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7717
7179
|
|
|
7718
7180
|
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
|
|
@@ -7917,6 +7379,11 @@ function AddOptionalAction(type, options) {
|
|
|
7917
7379
|
return result;
|
|
7918
7380
|
}
|
|
7919
7381
|
|
|
7382
|
+
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/array.mjs
|
|
7383
|
+
function _Array_(items, options) {
|
|
7384
|
+
return Create({ "~kind": "Array" }, { type: "array", items }, options);
|
|
7385
|
+
}
|
|
7386
|
+
|
|
7920
7387
|
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/action/_add_optional.mjs
|
|
7921
7388
|
function AddOptional(type, options = {}) {
|
|
7922
7389
|
return AddOptionalAction(type, options);
|
|
@@ -8256,6 +7723,439 @@ function createImageToolExtension(opts) {
|
|
|
8256
7723
|
};
|
|
8257
7724
|
}
|
|
8258
7725
|
|
|
7726
|
+
// src/choice-columns.ts
|
|
7727
|
+
import { matchesKey, truncateToWidth as truncateToWidth3, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7728
|
+
var STACK_BELOW = 72;
|
|
7729
|
+
var COLUMN_FOOTER = "left/right compare enter select o something else d let the agent decide esc cancel";
|
|
7730
|
+
var pad2 = (s, w) => s + " ".repeat(Math.max(0, w - visibleWidth(s)));
|
|
7731
|
+
function cell(s, w, st) {
|
|
7732
|
+
const out = [];
|
|
7733
|
+
const add = (mark, t) => wrapTextWithAnsi(t, Math.max(1, w - 2)).forEach((l, i) => out.push((i ? " " : mark + " ") + l));
|
|
7734
|
+
for (const p of s.pros ?? [])
|
|
7735
|
+
add(st.success("+"), p);
|
|
7736
|
+
for (const c of s.cons ?? [])
|
|
7737
|
+
add(st.error("-"), c);
|
|
7738
|
+
if (s.best_for) {
|
|
7739
|
+
if (out.length)
|
|
7740
|
+
out.push("");
|
|
7741
|
+
out.push(st.dim("Best for"));
|
|
7742
|
+
wrapTextWithAnsi(s.best_for, w).forEach((l) => out.push(l));
|
|
7743
|
+
}
|
|
7744
|
+
return out;
|
|
7745
|
+
}
|
|
7746
|
+
function build(o, width, st, rows, scroll) {
|
|
7747
|
+
const budget = Math.max(8, rows - 3);
|
|
7748
|
+
const hw = Math.max(1, width - 2);
|
|
7749
|
+
const headAll = [...wrapTextWithAnsi(st.bold(o.question), hw), ...o.context ? wrapTextWithAnsi(st.dim(o.context), hw) : []];
|
|
7750
|
+
const headMax = Math.max(2, Math.floor(budget / 3));
|
|
7751
|
+
const head = headAll.slice(0, headMax).map((l, i, a) => " " + (i === a.length - 1 && headAll.length > headMax ? truncateToWidth3(l, hw - 1) + "…" : l));
|
|
7752
|
+
head.push("");
|
|
7753
|
+
const foot = wrapTextWithAnsi(st.dim(COLUMN_FOOTER), hw).map((l) => " " + l);
|
|
7754
|
+
const title = (i, w) => {
|
|
7755
|
+
const t = truncateToWidth3(o.sides[i].label, Math.max(1, w - 2));
|
|
7756
|
+
return o.selected === i ? st.accent(st.bold("> " + t)) : " " + t;
|
|
7757
|
+
};
|
|
7758
|
+
const hint = (from, n, total) => " " + st.dim(`up/down scroll (${from + 1}-${from + n} of ${total})`);
|
|
7759
|
+
const clamp = (lines) => lines.map((l) => visibleWidth(l) > width ? truncateToWidth3(l, width) : l);
|
|
7760
|
+
const windowed = (detail, avail) => {
|
|
7761
|
+
if (detail.length <= avail)
|
|
7762
|
+
return { rows: detail, hint: [], max: 0 };
|
|
7763
|
+
const n = Math.max(1, avail - 1);
|
|
7764
|
+
const max = detail.length - n;
|
|
7765
|
+
const sc = Math.min(Math.max(0, scroll), max);
|
|
7766
|
+
return { rows: detail.slice(sc, sc + n), hint: [hint(sc, n, detail.length)], max };
|
|
7767
|
+
};
|
|
7768
|
+
if (width < STACK_BELOW) {
|
|
7769
|
+
const w = Math.max(1, width - 4);
|
|
7770
|
+
const cells = o.sides.map((s) => cell(s, w, st));
|
|
7771
|
+
const full = o.sides.flatMap((s, i) => [" " + title(i, w + 2), ...cells[i].map((l) => " " + l), ""]);
|
|
7772
|
+
if (head.length + full.length + foot.length <= budget)
|
|
7773
|
+
return { lines: clamp([...head, ...full, ...foot]), maxScroll: 0 };
|
|
7774
|
+
const titles = o.sides.map((_, i) => " " + title(i, w + 2));
|
|
7775
|
+
const avail = Math.max(1, budget - head.length - titles.length - foot.length);
|
|
7776
|
+
const detail = cells[o.selected].map((l) => " " + l);
|
|
7777
|
+
const win = windowed(detail, avail);
|
|
7778
|
+
return { lines: clamp([...head, ...titles, ...win.rows, ...win.hint, ...foot]), maxScroll: win.max };
|
|
7779
|
+
}
|
|
7780
|
+
const w = Math.floor((width - 8) / 2);
|
|
7781
|
+
const rule = (l, m, r) => " " + l + "─".repeat(w + 2) + m + "─".repeat(w + 2) + r;
|
|
7782
|
+
const rowOf = (x, y) => " │ " + pad2(x, w) + " │ " + pad2(y, w) + " │";
|
|
7783
|
+
const ca = cell(o.sides[0], w, st), cb = cell(o.sides[1], w, st);
|
|
7784
|
+
const total = Math.max(ca.length, cb.length);
|
|
7785
|
+
const fixed = head.length + 4 + foot.length;
|
|
7786
|
+
const avail = Math.max(1, budget - fixed);
|
|
7787
|
+
const n = total <= avail ? total : Math.max(1, avail - 1);
|
|
7788
|
+
const max = Math.max(0, total - n);
|
|
7789
|
+
const sc = Math.min(Math.max(0, scroll), max);
|
|
7790
|
+
const body = [];
|
|
7791
|
+
for (let i = sc;i < sc + n; i++)
|
|
7792
|
+
body.push(rowOf(ca[i] ?? "", cb[i] ?? ""));
|
|
7793
|
+
const more = max > 0 ? [hint(sc, n, total)] : [];
|
|
7794
|
+
return { lines: clamp([...head, rule("┌", "┬", "┐"), rowOf(title(0, w), title(1, w)), rule("├", "┼", "┤"), ...body, rule("└", "┴", "┘"), ...more, ...foot]), maxScroll: max };
|
|
7795
|
+
}
|
|
7796
|
+
function renderColumns(o, width, st, rows = 1000, scroll = 0) {
|
|
7797
|
+
return build(o, width, st, rows, scroll).lines;
|
|
7798
|
+
}
|
|
7799
|
+
function columnsMaxScroll(o, width, st, rows) {
|
|
7800
|
+
return build(o, width, st, rows, 0).maxScroll;
|
|
7801
|
+
}
|
|
7802
|
+
function columnsComponent(o, st, tui, done) {
|
|
7803
|
+
let selected = 0;
|
|
7804
|
+
let scroll = 0;
|
|
7805
|
+
let lastWidth = 100;
|
|
7806
|
+
const rows = () => tui.terminal?.rows ?? 1000;
|
|
7807
|
+
let finished = false;
|
|
7808
|
+
const finish = (r) => {
|
|
7809
|
+
if (!finished) {
|
|
7810
|
+
finished = true;
|
|
7811
|
+
o.signal?.removeEventListener("abort", onAbort);
|
|
7812
|
+
done(r);
|
|
7813
|
+
}
|
|
7814
|
+
};
|
|
7815
|
+
const onAbort = () => finish(undefined);
|
|
7816
|
+
if (o.signal?.aborted)
|
|
7817
|
+
queueMicrotask(onAbort);
|
|
7818
|
+
else
|
|
7819
|
+
o.signal?.addEventListener("abort", onAbort, { once: true });
|
|
7820
|
+
return {
|
|
7821
|
+
invalidate() {},
|
|
7822
|
+
dispose() {
|
|
7823
|
+
o.signal?.removeEventListener("abort", onAbort);
|
|
7824
|
+
},
|
|
7825
|
+
render: (width) => {
|
|
7826
|
+
lastWidth = width;
|
|
7827
|
+
return renderColumns({ question: o.question, context: o.context, sides: o.sides, selected }, width, st, rows(), scroll);
|
|
7828
|
+
},
|
|
7829
|
+
handleInput(d) {
|
|
7830
|
+
if (matchesKey(d, "left") || matchesKey(d, "right") || matchesKey(d, "tab")) {
|
|
7831
|
+
selected = selected === 0 ? 1 : 0;
|
|
7832
|
+
scroll = 0;
|
|
7833
|
+
tui.requestRender();
|
|
7834
|
+
} else if (matchesKey(d, "up") || matchesKey(d, "down")) {
|
|
7835
|
+
const max = columnsMaxScroll({ question: o.question, context: o.context, sides: o.sides, selected }, lastWidth, st, rows());
|
|
7836
|
+
scroll = Math.min(max, Math.max(0, scroll + (matchesKey(d, "down") ? 1 : -1)));
|
|
7837
|
+
tui.requestRender();
|
|
7838
|
+
} else if (matchesKey(d, "enter"))
|
|
7839
|
+
finish(o.labels[selected]);
|
|
7840
|
+
else if (d === "o")
|
|
7841
|
+
finish(o.else_);
|
|
7842
|
+
else if (d === "d")
|
|
7843
|
+
finish(o.decide);
|
|
7844
|
+
else if (matchesKey(d, "escape") || matchesKey(d, "ctrl+c"))
|
|
7845
|
+
finish(undefined);
|
|
7846
|
+
}
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
|
|
7850
|
+
// src/choice-tool.ts
|
|
7851
|
+
var ASK_USER_TOOL_NAME = "ask_user";
|
|
7852
|
+
var CHOICE_NO_ANSWER = "No answer; decide yourself and state your pick in one line.";
|
|
7853
|
+
var CHOICE_LIMIT_TEXT = "Ask limit reached; decide yourself.";
|
|
7854
|
+
var AGENT_DECIDES = "Let the agent decide";
|
|
7855
|
+
var SOMETHING_ELSE = "Something else (type it)";
|
|
7856
|
+
var COMPARE = "Compare pros and cons";
|
|
7857
|
+
var CHOICE_COMPARE_TEXT = "The user asked to compare the options. Explain the pros and cons of each option briefly as visible text, then call ask_user again with the same options.";
|
|
7858
|
+
var OTHER_MAX_CHARS = 500;
|
|
7859
|
+
var CAPS_ENTRY = "free-pi-choice-caps";
|
|
7860
|
+
var MAX_PER_SESSION = 10;
|
|
7861
|
+
var MAX_QUESTIONS_PER_SESSION = 5;
|
|
7862
|
+
var ANSWER_MAX_CHARS = 2000;
|
|
7863
|
+
var INT_MAX = 2147483647;
|
|
7864
|
+
var POST_TIMEOUT_MS = 3000;
|
|
7865
|
+
var COUNTER_MAX = 50;
|
|
7866
|
+
var CHOICE_RULES = `- Before choosing a library, service, language or other tool that the request and
|
|
7867
|
+
existing code do not already settle, call ask_user. Narrow to the best two options
|
|
7868
|
+
where you can and give pros, cons and best_for for each; use 3 or 4 options only for
|
|
7869
|
+
a genuinely wider choice. Pass your recommendation.
|
|
7870
|
+
- When an answer would change what you do and you cannot find it in the request or the
|
|
7871
|
+
code, call ask_user with just a question. Do not ask for permission to continue or to
|
|
7872
|
+
confirm what you already know.
|
|
7873
|
+
- Wait for the answer before continuing. Do not ask about trivia such as names,
|
|
7874
|
+
formatting or file layout, or twice on the same topic. If there is no answer, pick
|
|
7875
|
+
yourself and say what you picked.`;
|
|
7876
|
+
function choiceEnabled(o) {
|
|
7877
|
+
if (o.env.FREEPI_NO_CHOICE === "1")
|
|
7878
|
+
return false;
|
|
7879
|
+
if (!readChoicePrompts(o.configPath))
|
|
7880
|
+
return false;
|
|
7881
|
+
return o.env.FREEPI_CHOICE === "1" || o.arm === "treatment";
|
|
7882
|
+
}
|
|
7883
|
+
var capsBySession = new Map;
|
|
7884
|
+
var emptyCounters = () => ({ attempted: 0, shown: 0, suppressed: {}, delivery_failures: 0 });
|
|
7885
|
+
var norm = (s) => s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
7886
|
+
function createChoiceToolExtension(opts) {
|
|
7887
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
7888
|
+
const now = opts.now ?? Date.now;
|
|
7889
|
+
const random = opts.random ?? Math.random;
|
|
7890
|
+
const shutdownBoundMs = opts.shutdownBoundMs ?? POST_TIMEOUT_MS;
|
|
7891
|
+
const flushMs = opts.counterFlushMs ?? 60000;
|
|
7892
|
+
return {
|
|
7893
|
+
name: "free-pi-choice-tool",
|
|
7894
|
+
factory(pi) {
|
|
7895
|
+
function capsFor(sm) {
|
|
7896
|
+
const id = sm.getSessionId();
|
|
7897
|
+
let caps = capsBySession.get(id);
|
|
7898
|
+
if (!caps) {
|
|
7899
|
+
caps = { shownCount: 0, sets: new Set, asked: 0, questions: new Set };
|
|
7900
|
+
const saved = [...sm.getEntries()].reverse().find((e) => e.type === "custom" && e.customType === CAPS_ENTRY);
|
|
7901
|
+
if (saved?.data) {
|
|
7902
|
+
caps.shownCount = saved.data.shownCount ?? 0;
|
|
7903
|
+
caps.sets = new Set(saved.data.sets ?? []);
|
|
7904
|
+
caps.asked = saved.data.asked ?? 0;
|
|
7905
|
+
caps.questions = new Set(saved.data.questions ?? []);
|
|
7906
|
+
}
|
|
7907
|
+
capsBySession.set(id, caps);
|
|
7908
|
+
}
|
|
7909
|
+
return caps;
|
|
7910
|
+
}
|
|
7911
|
+
let counters = emptyCounters();
|
|
7912
|
+
let flushTimer;
|
|
7913
|
+
const dirty = (c) => c.attempted + c.shown + c.delivery_failures + Object.values(c.suppressed).reduce((a, b) => a + (b ?? 0), 0) > 0;
|
|
7914
|
+
function takeCounters() {
|
|
7915
|
+
if (!dirty(counters))
|
|
7916
|
+
return;
|
|
7917
|
+
const out = emptyCounters();
|
|
7918
|
+
const left = emptyCounters();
|
|
7919
|
+
const cut = (v) => [Math.min(v, COUNTER_MAX), v - Math.min(v, COUNTER_MAX)];
|
|
7920
|
+
[out.attempted, left.attempted] = cut(counters.attempted);
|
|
7921
|
+
[out.shown, left.shown] = cut(counters.shown);
|
|
7922
|
+
[out.delivery_failures, left.delivery_failures] = cut(counters.delivery_failures);
|
|
7923
|
+
for (const [k, v] of Object.entries(counters.suppressed)) {
|
|
7924
|
+
const [a, b] = cut(v ?? 0);
|
|
7925
|
+
if (a)
|
|
7926
|
+
out.suppressed[k] = a;
|
|
7927
|
+
if (b)
|
|
7928
|
+
left.suppressed[k] = b;
|
|
7929
|
+
}
|
|
7930
|
+
counters = left;
|
|
7931
|
+
return out;
|
|
7932
|
+
}
|
|
7933
|
+
function giveBack(c) {
|
|
7934
|
+
counters.attempted += c.attempted;
|
|
7935
|
+
counters.shown += c.shown;
|
|
7936
|
+
counters.delivery_failures += c.delivery_failures + 1;
|
|
7937
|
+
for (const [k, v] of Object.entries(c.suppressed))
|
|
7938
|
+
counters.suppressed[k] = (counters.suppressed[k] ?? 0) + (v ?? 0);
|
|
7939
|
+
}
|
|
7940
|
+
async function post(path, body, attempts = 2) {
|
|
7941
|
+
for (let attempt = 0;attempt < attempts; attempt++) {
|
|
7942
|
+
try {
|
|
7943
|
+
const token = await opts.getToken();
|
|
7944
|
+
const res = await doFetch(`${opts.baseUrl}${path}`, {
|
|
7945
|
+
method: "POST",
|
|
7946
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
7947
|
+
body: JSON.stringify(body),
|
|
7948
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS)
|
|
7949
|
+
});
|
|
7950
|
+
if (res.ok)
|
|
7951
|
+
return true;
|
|
7952
|
+
if (res.status >= 400 && res.status < 500)
|
|
7953
|
+
return false;
|
|
7954
|
+
} catch {}
|
|
7955
|
+
}
|
|
7956
|
+
return false;
|
|
7957
|
+
}
|
|
7958
|
+
function scheduleFlush() {
|
|
7959
|
+
if (flushTimer)
|
|
7960
|
+
return;
|
|
7961
|
+
flushTimer = setTimeout(() => {
|
|
7962
|
+
flushTimer = undefined;
|
|
7963
|
+
const c = takeCounters();
|
|
7964
|
+
if (!c)
|
|
7965
|
+
return;
|
|
7966
|
+
post("/telemetry/choice/counters", c).then((ok) => {
|
|
7967
|
+
if (!ok)
|
|
7968
|
+
giveBack(c);
|
|
7969
|
+
});
|
|
7970
|
+
if (dirty(counters))
|
|
7971
|
+
scheduleFlush();
|
|
7972
|
+
}, flushMs);
|
|
7973
|
+
flushTimer.unref?.();
|
|
7974
|
+
}
|
|
7975
|
+
function suppress(reason) {
|
|
7976
|
+
counters.suppressed[reason] = (counters.suppressed[reason] ?? 0) + 1;
|
|
7977
|
+
scheduleFlush();
|
|
7978
|
+
}
|
|
7979
|
+
function deliver(event) {
|
|
7980
|
+
setTimeout(() => {
|
|
7981
|
+
const c = takeCounters();
|
|
7982
|
+
post("/telemetry/choice", c ? { ...event, counters: c } : event).then((ok) => {
|
|
7983
|
+
if (ok)
|
|
7984
|
+
return;
|
|
7985
|
+
if (c)
|
|
7986
|
+
giveBack(c);
|
|
7987
|
+
else
|
|
7988
|
+
counters.delivery_failures += 1;
|
|
7989
|
+
scheduleFlush();
|
|
7990
|
+
});
|
|
7991
|
+
}, 0);
|
|
7992
|
+
}
|
|
7993
|
+
pi.on("session_shutdown", async () => {
|
|
7994
|
+
clearTimeout(flushTimer);
|
|
7995
|
+
flushTimer = undefined;
|
|
7996
|
+
const c = takeCounters();
|
|
7997
|
+
if (!c)
|
|
7998
|
+
return;
|
|
7999
|
+
const bound = new Promise((r) => {
|
|
8000
|
+
const t = setTimeout(() => r(false), shutdownBoundMs);
|
|
8001
|
+
t.unref?.();
|
|
8002
|
+
});
|
|
8003
|
+
await Promise.race([post("/telemetry/choice/counters", c, 1), bound]);
|
|
8004
|
+
});
|
|
8005
|
+
pi.registerTool({
|
|
8006
|
+
name: ASK_USER_TOOL_NAME,
|
|
8007
|
+
executionMode: "sequential",
|
|
8008
|
+
label: "Ask User",
|
|
8009
|
+
description: "Ask the user something. With options: pick between them when the request and existing code do not already settle a choice of library, service, language or tool; prefer the best two options with pros, cons and best_for for each (shown side by side), and 3 or 4 only for a wider choice. With no options: ask an open question when the answer would change what you do and you cannot find it in the request or code. Returns the user's answer or choice, or says to decide yourself.",
|
|
8010
|
+
parameters: _Object_({
|
|
8011
|
+
question: String2({ minLength: 1, maxLength: 200 }),
|
|
8012
|
+
options: Optional(_Array_(_Object_({
|
|
8013
|
+
id: Optional(String2({ minLength: 1, maxLength: 40 })),
|
|
8014
|
+
label: String2({ minLength: 1, maxLength: 40 }),
|
|
8015
|
+
note: Optional(String2({ maxLength: 120 })),
|
|
8016
|
+
pros: Optional(_Array_(String2({ minLength: 1, maxLength: 100 }), { maxItems: 4 })),
|
|
8017
|
+
cons: Optional(_Array_(String2({ minLength: 1, maxLength: 100 }), { maxItems: 4 })),
|
|
8018
|
+
best_for: Optional(String2({ maxLength: 120 }))
|
|
8019
|
+
}), { maxItems: 4 })),
|
|
8020
|
+
context: Optional(String2({ maxLength: 300 })),
|
|
8021
|
+
recommended: Optional(Integer({ minimum: 0, maximum: 3 }))
|
|
8022
|
+
}),
|
|
8023
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
8024
|
+
counters.attempted += 1;
|
|
8025
|
+
const text = (t, details = {}) => ({ content: [{ type: "text", text: t }], details });
|
|
8026
|
+
if (params.options?.length === 1)
|
|
8027
|
+
throw new Error("ask_user takes either no options (an open question) or 2 to 4 options. Add a second option, or omit options to ask a free-text question.");
|
|
8028
|
+
const open = !params.options?.length;
|
|
8029
|
+
const options = (params.options ?? []).map((o, i) => ({ id: o.id ?? `opt${i + 1}`, label: o.label.trim(), ...o.note ? { note: o.note } : {}, ...o.pros?.length ? { pros: o.pros } : {}, ...o.cons?.length ? { cons: o.cons } : {}, ...o.best_for ? { best_for: o.best_for } : {} }));
|
|
8030
|
+
const labels = options.map((o) => norm(o.label));
|
|
8031
|
+
if (new Set(labels).size !== labels.length || [AGENT_DECIDES, SOMETHING_ELSE, COMPARE].some((r) => labels.includes(norm(r)))) {
|
|
8032
|
+
suppress("duplicate_labels");
|
|
8033
|
+
throw new Error(`ask_user options must have distinct labels (case-insensitive) and none may be "${AGENT_DECIDES}", "${SOMETHING_ELSE}" or "${COMPARE}". Fix the labels and call again.`);
|
|
8034
|
+
}
|
|
8035
|
+
if (new Set(options.map((o) => o.id)).size !== options.length)
|
|
8036
|
+
throw new Error("ask_user option ids must be distinct. Fix them or omit the ids.");
|
|
8037
|
+
if (opts.headless || !ctx.hasUI || opts.isRoomOpen?.()) {
|
|
8038
|
+
suppress("headless");
|
|
8039
|
+
return text(CHOICE_NO_ANSWER, { suppressed: "headless" });
|
|
8040
|
+
}
|
|
8041
|
+
const state = capsFor(ctx.sessionManager);
|
|
8042
|
+
const setKey = open ? norm(params.question) : [...labels].sort().join("\x00");
|
|
8043
|
+
if ((open ? state.questions : state.sets).has(setKey)) {
|
|
8044
|
+
suppress("repeat");
|
|
8045
|
+
return text(CHOICE_LIMIT_TEXT, { suppressed: "repeat" });
|
|
8046
|
+
}
|
|
8047
|
+
if (open ? state.asked >= MAX_QUESTIONS_PER_SESSION : state.shownCount >= MAX_PER_SESSION) {
|
|
8048
|
+
suppress("capped_session");
|
|
8049
|
+
return text(CHOICE_LIMIT_TEXT, { suppressed: "capped_session" });
|
|
8050
|
+
}
|
|
8051
|
+
const saveCaps = () => pi.appendEntry(CAPS_ENTRY, { shownCount: state.shownCount, sets: [...state.sets], asked: state.asked, questions: [...state.questions] });
|
|
8052
|
+
if (open) {
|
|
8053
|
+
state.asked += 1;
|
|
8054
|
+
state.questions.add(setKey);
|
|
8055
|
+
} else {
|
|
8056
|
+
state.shownCount += 1;
|
|
8057
|
+
state.sets.add(setKey);
|
|
8058
|
+
}
|
|
8059
|
+
saveCaps();
|
|
8060
|
+
counters.shown += 1;
|
|
8061
|
+
const displayOrder = options.map((_, i) => i);
|
|
8062
|
+
for (let i = displayOrder.length - 1;i > 0; i--) {
|
|
8063
|
+
const j = Math.floor(random() * (i + 1));
|
|
8064
|
+
[displayOrder[i], displayOrder[j]] = [displayOrder[j], displayOrder[i]];
|
|
8065
|
+
}
|
|
8066
|
+
const byLabel = new Map(options.map((o, i) => [o.label, i]));
|
|
8067
|
+
const notes = options.filter((o) => o.note).map((o) => ` ${o.label}: ${o.note}`);
|
|
8068
|
+
const title = [params.question, params.context, ...notes.length ? ["", ...notes] : []].filter((l) => l !== undefined).join(`
|
|
8069
|
+
`);
|
|
8070
|
+
const startedAt = now();
|
|
8071
|
+
let picked;
|
|
8072
|
+
let typed;
|
|
8073
|
+
let threw = false;
|
|
8074
|
+
try {
|
|
8075
|
+
const ui = ctx.ui;
|
|
8076
|
+
if (open) {
|
|
8077
|
+
typed = (await ctx.ui.input([params.question, params.context].filter(Boolean).join(`
|
|
8078
|
+
`), "Type your answer", { signal }))?.trim().slice(0, ANSWER_MAX_CHARS);
|
|
8079
|
+
} else if (options.length === 2 && options.some((o) => o.pros || o.cons) && typeof ui.custom === "function") {
|
|
8080
|
+
const sides = displayOrder.map((i) => options[i]);
|
|
8081
|
+
picked = await ctx.ui.custom((tui, theme, _kb, done) => columnsComponent({
|
|
8082
|
+
question: params.question,
|
|
8083
|
+
context: params.context,
|
|
8084
|
+
sides,
|
|
8085
|
+
labels: [sides[0].label, sides[1].label],
|
|
8086
|
+
else_: SOMETHING_ELSE,
|
|
8087
|
+
decide: AGENT_DECIDES,
|
|
8088
|
+
signal
|
|
8089
|
+
}, {
|
|
8090
|
+
accent: (x) => theme.fg("accent", x),
|
|
8091
|
+
success: (x) => theme.fg("success", x),
|
|
8092
|
+
error: (x) => theme.fg("error", x),
|
|
8093
|
+
dim: (x) => theme.fg("dim", x),
|
|
8094
|
+
bold: (x) => theme.bold(x)
|
|
8095
|
+
}, tui, done));
|
|
8096
|
+
} else
|
|
8097
|
+
picked = await ctx.ui.select(title, [...displayOrder.map((i) => options[i].label), SOMETHING_ELSE, COMPARE, AGENT_DECIDES], { signal });
|
|
8098
|
+
if (!open && picked === SOMETHING_ELSE) {
|
|
8099
|
+
typed = (await ctx.ui.input(params.question, "Type your answer", { signal }))?.trim().slice(0, OTHER_MAX_CHARS);
|
|
8100
|
+
}
|
|
8101
|
+
} catch {
|
|
8102
|
+
threw = true;
|
|
8103
|
+
}
|
|
8104
|
+
const latency = Math.min(INT_MAX, Math.max(0, Math.round(now() - startedAt)));
|
|
8105
|
+
let outcome;
|
|
8106
|
+
let chosenIndex = null;
|
|
8107
|
+
if (threw)
|
|
8108
|
+
outcome = "cancel_ambiguous";
|
|
8109
|
+
else if (open)
|
|
8110
|
+
outcome = typed ? "answered" : "aborted";
|
|
8111
|
+
else if (picked === AGENT_DECIDES)
|
|
8112
|
+
outcome = "delegated";
|
|
8113
|
+
else if (picked === COMPARE)
|
|
8114
|
+
outcome = "compare";
|
|
8115
|
+
else if (picked === SOMETHING_ELSE && typed)
|
|
8116
|
+
outcome = "other";
|
|
8117
|
+
else if (picked === SOMETHING_ELSE)
|
|
8118
|
+
outcome = "aborted";
|
|
8119
|
+
else if (picked !== undefined && byLabel.has(picked)) {
|
|
8120
|
+
outcome = "chosen";
|
|
8121
|
+
chosenIndex = byLabel.get(picked);
|
|
8122
|
+
} else if (picked === undefined)
|
|
8123
|
+
outcome = "aborted";
|
|
8124
|
+
else
|
|
8125
|
+
outcome = "cancel_ambiguous";
|
|
8126
|
+
if (outcome === "compare") {
|
|
8127
|
+
state.sets.delete(setKey);
|
|
8128
|
+
state.shownCount -= 1;
|
|
8129
|
+
saveCaps();
|
|
8130
|
+
}
|
|
8131
|
+
const rec = typeof params.recommended === "number" && params.recommended < options.length ? params.recommended : null;
|
|
8132
|
+
const model = ctx.model?.id ?? "unknown";
|
|
8133
|
+
const event = {
|
|
8134
|
+
session_id: opts.sessionId,
|
|
8135
|
+
tool_call_id: toolCallId,
|
|
8136
|
+
question: params.question,
|
|
8137
|
+
context: params.context ?? "",
|
|
8138
|
+
options,
|
|
8139
|
+
display_order: displayOrder,
|
|
8140
|
+
outcome,
|
|
8141
|
+
chosen_index: chosenIndex,
|
|
8142
|
+
...outcome === "other" || outcome === "answered" ? { other_text: typed } : {},
|
|
8143
|
+
agent_pick_index: rec,
|
|
8144
|
+
model,
|
|
8145
|
+
latency_ms: latency,
|
|
8146
|
+
cli_version: CLI_VERSION,
|
|
8147
|
+
arm: opts.arm()
|
|
8148
|
+
};
|
|
8149
|
+
deliver(event);
|
|
8150
|
+
if (outcome === "compare")
|
|
8151
|
+
return text(CHOICE_COMPARE_TEXT, event);
|
|
8152
|
+
return text(outcome === "other" || outcome === "answered" ? `User replied: ${typed}` : chosenIndex !== null ? `User chose: ${options[chosenIndex].label}` : CHOICE_NO_ANSWER, event);
|
|
8153
|
+
}
|
|
8154
|
+
});
|
|
8155
|
+
}
|
|
8156
|
+
};
|
|
8157
|
+
}
|
|
8158
|
+
|
|
8259
8159
|
// src/vision-tool.ts
|
|
8260
8160
|
import { closeSync, fstatSync, openSync, readSync, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
|
|
8261
8161
|
import { isAbsolute, relative, resolve, sep as sep2 } from "node:path";
|
|
@@ -8647,14 +8547,14 @@ function createDocsToolExtension() {
|
|
|
8647
8547
|
}
|
|
8648
8548
|
|
|
8649
8549
|
// src/todo-tool.ts
|
|
8650
|
-
import { truncateToWidth as
|
|
8550
|
+
import { truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
|
|
8651
8551
|
|
|
8652
8552
|
// src/todo-view.ts
|
|
8653
|
-
import { truncateToWidth as
|
|
8553
|
+
import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
8654
8554
|
var PLAIN_STYLE = { bold: (s) => s, dim: (s) => s, accent: (s) => s, error: (s) => s };
|
|
8655
8555
|
var GAP = " ";
|
|
8656
8556
|
function truncate(text, max) {
|
|
8657
|
-
return max <= 0 ? "" :
|
|
8557
|
+
return max <= 0 ? "" : truncateToWidth4(text, max, "…");
|
|
8658
8558
|
}
|
|
8659
8559
|
function counts(todos) {
|
|
8660
8560
|
return { done: todos.filter((t) => t.done).length, total: todos.length };
|
|
@@ -8666,7 +8566,7 @@ function widgetLines(todos, width, st = PLAIN_STYLE) {
|
|
|
8666
8566
|
const header = `Todos ${done}/${total}`;
|
|
8667
8567
|
const firstOpen = todos.findIndex((t) => !t.done);
|
|
8668
8568
|
const marks = todos.map((t, i) => t.done ? `✓ ${t.text}` : i === firstOpen ? `▸ ${t.text}` : `○ ${t.text}`);
|
|
8669
|
-
const compactLength =
|
|
8569
|
+
const compactLength = visibleWidth2([header, ...marks].join(GAP));
|
|
8670
8570
|
if (compactLength <= width) {
|
|
8671
8571
|
return [
|
|
8672
8572
|
[
|
|
@@ -8691,7 +8591,7 @@ function widgetLines(todos, width, st = PLAIN_STYLE) {
|
|
|
8691
8591
|
return lines;
|
|
8692
8592
|
}
|
|
8693
8593
|
function resultLines(d, st = PLAIN_STYLE, width = Infinity) {
|
|
8694
|
-
return rawResultLines(d, st).map((line) => Number.isFinite(width) ?
|
|
8594
|
+
return rawResultLines(d, st).map((line) => Number.isFinite(width) ? truncateToWidth4(line, Math.max(0, width), "…") : line);
|
|
8695
8595
|
}
|
|
8696
8596
|
function rawResultLines(d, st) {
|
|
8697
8597
|
if (d.error)
|
|
@@ -8888,7 +8788,7 @@ function createTodoToolExtension(options = {}) {
|
|
|
8888
8788
|
const details = res.details;
|
|
8889
8789
|
const text = res.content[0]?.type === "text" ? res.content[0].text : "";
|
|
8890
8790
|
return {
|
|
8891
|
-
render: (width) => details?.action ? resultLines(details, styleFrom(theme), width) : [
|
|
8791
|
+
render: (width) => details?.action ? resultLines(details, styleFrom(theme), width) : [truncateToWidth5(text, width, "…")],
|
|
8892
8792
|
invalidate() {}
|
|
8893
8793
|
};
|
|
8894
8794
|
}
|
|
@@ -9011,43 +8911,240 @@ function createToolAliasesExtension(opts = {}) {
|
|
|
9011
8911
|
};
|
|
9012
8912
|
}
|
|
9013
8913
|
|
|
9014
|
-
// src/
|
|
9015
|
-
var
|
|
9016
|
-
var
|
|
9017
|
-
var
|
|
9018
|
-
|
|
9019
|
-
|
|
8914
|
+
// src/fusion.ts
|
|
8915
|
+
var FUSION_ID = "fusion";
|
|
8916
|
+
var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
|
|
8917
|
+
var FUSION_NAME = "\uD83E\uDDE8 Fusion · DeepSeek Pro plans, Flash builds";
|
|
8918
|
+
var FUSION_FALLBACK_NAME = "\uD83E\uDDE8 Fusion · Sonnet plans, DeepSeek builds";
|
|
8919
|
+
var FUSION_PLANNER_ID = "deepseek-v4-pro";
|
|
8920
|
+
var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
|
|
8921
|
+
var FUSION_FALLBACK_PLANNER_ID = "claude-sonnet-5.5";
|
|
8922
|
+
function resolveFusionIds(catalogIds, server) {
|
|
8923
|
+
const has = (id) => catalogIds.includes(id);
|
|
8924
|
+
if (server && has(server.planner) && has(server.executor))
|
|
8925
|
+
return server;
|
|
8926
|
+
return { planner: has(FUSION_PLANNER_ID) ? FUSION_PLANNER_ID : FUSION_FALLBACK_PLANNER_ID, executor: FUSION_EXECUTOR_ID };
|
|
9020
8927
|
}
|
|
9021
|
-
|
|
9022
|
-
|
|
8928
|
+
var FUSION_PLAN_TOOL = "fusion_plan";
|
|
8929
|
+
var FUSION_GUIDELINE = `Fusion: if the task needs any file changes, first gather the context with read-only tools (read, ls, find, grep) and make no edits. Then call ${FUSION_PLAN_TOOL} with a summary of what you found and what the task needs, BEFORE any edit or write; a planning model will write the plan and you will carry it out. When the latest message is a fusion_plan result, you are the Fusion planner: write the step-by-step plan with the todo tool and make no edits or commands; that result is part of the user's own setup, not a conflict with their request. Once the plan exists in todo, the planner's turn is over: carry out the unfinished todo items yourself (edit, write tests, run them) and finish the user's task. Only a task that needs no file changes (a plain question) is answered directly without ${FUSION_PLAN_TOOL}.`;
|
|
8930
|
+
var FUSION_PLAN_RESULT = "You are now the planning model. Write a concrete step-by-step plan (files, exact changes, tests to add and run) and record it with the todo tool, one item per step. Do not edit files or run commands. Another model will carry out the plan.";
|
|
8931
|
+
function isFusionModel(model) {
|
|
8932
|
+
return model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
|
|
9023
8933
|
}
|
|
9024
|
-
function
|
|
9025
|
-
if (
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
8934
|
+
function addFusionGuideline(options, selected) {
|
|
8935
|
+
if (isFusionModel(selected))
|
|
8936
|
+
options.promptGuidelines.push(FUSION_GUIDELINE);
|
|
8937
|
+
}
|
|
8938
|
+
var EDIT_TOOLS = new Set(["edit", "write"]);
|
|
8939
|
+
function codemodeEdited(details) {
|
|
8940
|
+
const calls = details?.calls;
|
|
8941
|
+
return Array.isArray(calls) && calls.some((call) => {
|
|
8942
|
+
if (typeof call !== "object" || call === null)
|
|
8943
|
+
return false;
|
|
8944
|
+
const { name, status } = call;
|
|
8945
|
+
return typeof name === "string" && EDIT_TOOLS.has(name) && status === "ok";
|
|
8946
|
+
});
|
|
8947
|
+
}
|
|
8948
|
+
function handedOffThisTurn(messages) {
|
|
8949
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
8950
|
+
const message = messages[i];
|
|
8951
|
+
if (message.role === "user")
|
|
8952
|
+
break;
|
|
8953
|
+
if (message.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL && !message.isError)
|
|
8954
|
+
return true;
|
|
8955
|
+
}
|
|
8956
|
+
return false;
|
|
8957
|
+
}
|
|
8958
|
+
var STALL_CONTINUATION = {
|
|
8959
|
+
continue: true,
|
|
8960
|
+
entries: [{ type: "custom_message", customType: "fusion-continue", display: false, content: "The plan is recorded. Carry out the unfinished todo items now and finish the user's task." }]
|
|
8961
|
+
};
|
|
8962
|
+
function plannerMadeTheCall(messages, plannerId) {
|
|
8963
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
8964
|
+
const message = messages[i];
|
|
8965
|
+
if (message.role === "assistant")
|
|
8966
|
+
return message.provider === PROVIDER_NAME && message.model === plannerId;
|
|
8967
|
+
}
|
|
8968
|
+
return false;
|
|
8969
|
+
}
|
|
8970
|
+
function branchMessages(ctx) {
|
|
8971
|
+
return ctx.sessionManager.getBranch().flatMap((entry) => entry.type === "message" ? [entry.message] : []);
|
|
8972
|
+
}
|
|
8973
|
+
function alreadyContinued(ctx) {
|
|
8974
|
+
const branch = ctx.sessionManager.getBranch();
|
|
8975
|
+
for (let i = branch.length - 1;i >= 0; i--) {
|
|
8976
|
+
const entry = branch[i];
|
|
8977
|
+
if (entry.type === "custom_message" && entry.customType === STALL_CONTINUATION.entries[0].customType)
|
|
8978
|
+
return true;
|
|
8979
|
+
const message = entry.type === "message" ? entry.message : undefined;
|
|
8980
|
+
if (message?.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL)
|
|
8981
|
+
return false;
|
|
8982
|
+
}
|
|
8983
|
+
return false;
|
|
8984
|
+
}
|
|
8985
|
+
function plannerStalled(messages, plannerId) {
|
|
8986
|
+
const last = messages[messages.length - 1];
|
|
8987
|
+
if (!last || last.role !== "assistant" || last.provider !== PROVIDER_NAME || last.model !== plannerId)
|
|
8988
|
+
return false;
|
|
8989
|
+
if (last.stopReason !== "stop")
|
|
8990
|
+
return false;
|
|
8991
|
+
return handedOffThisTurn(messages) && !last.content.some((part) => part.type === "toolCall");
|
|
8992
|
+
}
|
|
8993
|
+
function editedThisTurn(messages) {
|
|
8994
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
8995
|
+
const message = messages[i];
|
|
8996
|
+
if (message.role === "user")
|
|
8997
|
+
break;
|
|
8998
|
+
if (message.role !== "toolResult")
|
|
8999
|
+
continue;
|
|
9000
|
+
if (message.toolName === "codemode" ? codemodeEdited(message.details) : EDIT_TOOLS.has(message.toolName) && !message.isError) {
|
|
9001
|
+
return true;
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
return false;
|
|
9005
|
+
}
|
|
9006
|
+
function hasNewUserInput(request) {
|
|
9007
|
+
const failedAt = request.failed?.message.timestamp;
|
|
9008
|
+
if (failedAt === undefined)
|
|
9009
|
+
return false;
|
|
9010
|
+
for (let i = request.messages.length - 1;i >= 0; i--) {
|
|
9011
|
+
const message = request.messages[i];
|
|
9012
|
+
if (message.role === "assistant")
|
|
9013
|
+
return false;
|
|
9014
|
+
if (message.role === "user")
|
|
9015
|
+
return message.timestamp > failedAt;
|
|
9016
|
+
}
|
|
9017
|
+
return false;
|
|
9018
|
+
}
|
|
9019
|
+
function routeFusion(request, models) {
|
|
9020
|
+
const phase = request.state?.phase ?? "explore";
|
|
9021
|
+
const restart = request.reason === "user" || request.reason === "retry" && hasNewUserInput(request);
|
|
9022
|
+
const route = (model, nextPhase = phase) => ({
|
|
9023
|
+
model,
|
|
9024
|
+
thinkingLevel: request.thinkingLevel,
|
|
9025
|
+
state: request.reason === "direct" ? undefined : request.state?.phase === nextPhase ? request.state : { phase: nextPhase }
|
|
9026
|
+
});
|
|
9027
|
+
if (!models.planner || !models.executor) {
|
|
9028
|
+
return route(models.defaultModel, restart ? "explore" : phase);
|
|
9029
|
+
}
|
|
9030
|
+
switch (request.reason) {
|
|
9031
|
+
case "user":
|
|
9032
|
+
return route(models.executor, "explore");
|
|
9033
|
+
case "direct":
|
|
9034
|
+
return route(models.executor);
|
|
9035
|
+
case "retry": {
|
|
9036
|
+
if (restart)
|
|
9037
|
+
return route(models.executor, "explore");
|
|
9038
|
+
const failed = request.failed;
|
|
9039
|
+
if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === models.planner?.id) {
|
|
9040
|
+
return route(models.executor, "build");
|
|
9041
|
+
}
|
|
9042
|
+
return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
|
|
9043
|
+
}
|
|
9044
|
+
case "continuation":
|
|
9045
|
+
if (phase !== "explore")
|
|
9046
|
+
return route(models.executor, "build");
|
|
9047
|
+
if (editedThisTurn(request.messages))
|
|
9048
|
+
return route(models.executor, "build");
|
|
9049
|
+
return handedOffThisTurn(request.messages) ? route(models.planner, "plan") : route(models.executor, "explore");
|
|
9050
|
+
}
|
|
9051
|
+
}
|
|
9052
|
+
function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
|
|
9053
|
+
const name = ids.planner === FUSION_PLANNER_ID && ids.executor === FUSION_EXECUTOR_ID ? FUSION_NAME : ids.planner === FUSION_FALLBACK_PLANNER_ID ? FUSION_FALLBACK_NAME : "\uD83E\uDDE8 Fusion";
|
|
9054
|
+
return {
|
|
9055
|
+
name: "free-pi-fusion",
|
|
9056
|
+
factory: (pi) => {
|
|
9057
|
+
const syncPlanTool = (selected) => {
|
|
9058
|
+
const on = isFusionModel(selected);
|
|
9059
|
+
const active = pi.getActiveTools();
|
|
9060
|
+
if (on === active.includes(FUSION_PLAN_TOOL))
|
|
9061
|
+
return;
|
|
9062
|
+
pi.setActiveTools(on ? [...active, FUSION_PLAN_TOOL] : active.filter((name) => name !== FUSION_PLAN_TOOL));
|
|
9063
|
+
};
|
|
9064
|
+
pi.on("session_start", (_event, ctx) => syncPlanTool(ctx.model));
|
|
9065
|
+
pi.on("model_select", (event) => syncPlanTool(event.model));
|
|
9066
|
+
pi.registerTool({
|
|
9067
|
+
name: FUSION_PLAN_TOOL,
|
|
9068
|
+
label: "Fusion plan",
|
|
9069
|
+
description: "Hand off to the planning model once you have gathered enough context. Takes a summary of what you found and what the task needs.",
|
|
9070
|
+
promptSnippet: "Fusion only: hand off to the planning model after read-only exploration",
|
|
9071
|
+
exposure: "model-only",
|
|
9072
|
+
parameters: _Object_({ summary: String2({ description: "What you found and what the task needs" }) }),
|
|
9073
|
+
async execute() {
|
|
9074
|
+
return { content: [{ type: "text", text: FUSION_PLAN_RESULT }], details: {} };
|
|
9075
|
+
}
|
|
9076
|
+
});
|
|
9077
|
+
const isFusion = isFusionModel;
|
|
9078
|
+
pi.on("before_agent_start", (event, ctx) => addFusionGuideline(event.systemPromptOptions, ctx.model));
|
|
9079
|
+
pi.on("tool_call", (event, ctx) => {
|
|
9080
|
+
if (!isFusion(ctx.model) || !EDIT_TOOLS.has(event.toolName))
|
|
9081
|
+
return;
|
|
9082
|
+
const messages = branchMessages(ctx);
|
|
9083
|
+
if (plannerMadeTheCall(messages, ids.planner))
|
|
9084
|
+
return { block: true, reason: "Planner: record the plan with todo only; the executor makes the changes." };
|
|
9085
|
+
if (!handedOffThisTurn(messages)) {
|
|
9086
|
+
return { block: true, reason: `Blocked: call ${FUSION_PLAN_TOOL} first (summarize what you found); a planning model writes the plan before any edit.` };
|
|
9087
|
+
}
|
|
9088
|
+
});
|
|
9089
|
+
pi.on("agent_before_settle", (event, ctx) => isFusion(ctx.model) && event.outcome === "completed" && !alreadyContinued(ctx) && plannerStalled(branchMessages(ctx), ids.planner) ? STALL_CONTINUATION : undefined);
|
|
9090
|
+
for (const id of [FUSION_PICKER_ID, FUSION_ID]) {
|
|
9091
|
+
pi.registerVirtualModel({
|
|
9092
|
+
provider: PROVIDER_NAME,
|
|
9093
|
+
id,
|
|
9094
|
+
name,
|
|
9095
|
+
input: ["text"],
|
|
9096
|
+
route: (request, ctx) => routeFusion(request, {
|
|
9097
|
+
planner: ctx.modelRegistry.find(PROVIDER_NAME, ids.planner),
|
|
9098
|
+
executor: ctx.modelRegistry.find(PROVIDER_NAME, ids.executor),
|
|
9099
|
+
defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
|
|
9100
|
+
})
|
|
9101
|
+
});
|
|
9102
|
+
}
|
|
9103
|
+
}
|
|
9104
|
+
};
|
|
9105
|
+
}
|
|
9106
|
+
|
|
9107
|
+
// src/error-notice.ts
|
|
9108
|
+
var UPSTREAM_ERROR_FRAME_MESSAGE = "upstream returned an error frame";
|
|
9109
|
+
var PROVIDER_FAILING_NOTICE = "This model's provider is failing right now. Try again in a moment, or switch models with /model.";
|
|
9110
|
+
var FREE_OFFER_TITLE = "Switch to the free model and resend this prompt?";
|
|
9111
|
+
var FREE_OFFER_ACCEPT = "Switch and resend";
|
|
9112
|
+
var FREE_OFFER_DECLINE = "Stay on this model";
|
|
9113
|
+
var FREE_OFFER_SWITCHED = "Switched to the free model. Sending your prompt again.";
|
|
9114
|
+
var NOTICE_DEDUPE_MS = 15000;
|
|
9115
|
+
function optionalNumber(value) {
|
|
9116
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9117
|
+
}
|
|
9118
|
+
function optionalString(value) {
|
|
9119
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
9120
|
+
}
|
|
9121
|
+
function parseErrorEnvelope(errorMessage) {
|
|
9122
|
+
if (typeof errorMessage !== "string")
|
|
9123
|
+
return;
|
|
9124
|
+
const m = /^(\d{3}): (\{[\s\S]*\})\s*$/.exec(errorMessage);
|
|
9125
|
+
if (!m)
|
|
9126
|
+
return;
|
|
9127
|
+
try {
|
|
9128
|
+
const body = JSON.parse(m[2]);
|
|
9129
|
+
const message = body.message ?? body.error?.message;
|
|
9130
|
+
const code = body.code ?? body.error?.code;
|
|
9131
|
+
if (typeof message !== "string" || message.trim() === "")
|
|
9132
|
+
return;
|
|
9133
|
+
const parsed = {
|
|
9134
|
+
status: Number(m[1]),
|
|
9135
|
+
code: typeof code === "string" ? code : "unknown",
|
|
9136
|
+
message
|
|
9137
|
+
};
|
|
9138
|
+
const limit = optionalString(body.limit);
|
|
9139
|
+
const retryAfter = optionalNumber(body.retry_after_seconds);
|
|
9140
|
+
const clearsAt = optionalString(body.clears_at);
|
|
9141
|
+
const remaining = optionalNumber(body.remaining_credits_today);
|
|
9142
|
+
const allowance = optionalNumber(body.allowance_credits);
|
|
9143
|
+
if (limit)
|
|
9144
|
+
parsed.limit = limit;
|
|
9145
|
+
if (retryAfter !== undefined)
|
|
9146
|
+
parsed.retry_after_seconds = retryAfter;
|
|
9147
|
+
if (clearsAt)
|
|
9051
9148
|
parsed.clears_at = clearsAt;
|
|
9052
9149
|
if (remaining !== undefined)
|
|
9053
9150
|
parsed.remaining_credits_today = remaining;
|
|
@@ -9104,14 +9201,50 @@ var LIMIT_CODES = new Set([
|
|
|
9104
9201
|
"reward_required"
|
|
9105
9202
|
]);
|
|
9106
9203
|
var SERVER_MESSAGE_CODES = new Set(["reward_required", "premium_allowance", "model_at_capacity", "context_too_long_for_fallback", "context_too_long"]);
|
|
9204
|
+
var FREE_OFFER_CODES = new Set(["daily_cap", "queue_wait", "rolling_cap", "premium_allowance", "model_at_capacity", "model_requires_credits"]);
|
|
9205
|
+
var isFusion = (model) => model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
|
|
9206
|
+
function lastUserEntryId(ctx) {
|
|
9207
|
+
const branch = ctx.sessionManager.getBranch();
|
|
9208
|
+
for (let i = branch.length - 1;i >= 0; i--) {
|
|
9209
|
+
if (branch[i].type === "message" && branch[i].message?.role === "user")
|
|
9210
|
+
return branch[i].id;
|
|
9211
|
+
}
|
|
9212
|
+
return "";
|
|
9213
|
+
}
|
|
9107
9214
|
function createErrorNoticeExtension(opts = {}) {
|
|
9108
9215
|
const lastAt = new Map;
|
|
9109
9216
|
const now = opts.now ?? Date.now;
|
|
9110
9217
|
const timeZone = opts.timeZone;
|
|
9218
|
+
const offered = new Set;
|
|
9219
|
+
let lastTurnEntryId;
|
|
9220
|
+
let acceptedEntryId;
|
|
9221
|
+
async function offerFreeModel(pi, env, entryId, ctx) {
|
|
9222
|
+
if (!opts.freeModels || opts.terminal !== true || !FREE_OFFER_CODES.has(env.code) || ctx.mode !== "tui" || !ctx.hasUI || isFusion(ctx.model))
|
|
9223
|
+
return;
|
|
9224
|
+
const free = opts.freeModels();
|
|
9225
|
+
const target = free[0];
|
|
9226
|
+
if (!target || free.some((m) => m.provider === ctx.model?.provider && m.id === ctx.model?.id))
|
|
9227
|
+
return;
|
|
9228
|
+
const prompt = lastUserEntryId(ctx);
|
|
9229
|
+
if (offered.has(prompt))
|
|
9230
|
+
return;
|
|
9231
|
+
offered.add(prompt);
|
|
9232
|
+
const choice = await ctx.ui.select(FREE_OFFER_TITLE, [FREE_OFFER_ACCEPT, FREE_OFFER_DECLINE]).catch(() => {
|
|
9233
|
+
return;
|
|
9234
|
+
});
|
|
9235
|
+
if (choice !== FREE_OFFER_ACCEPT)
|
|
9236
|
+
return;
|
|
9237
|
+
const model = ctx.modelRegistry.find(target.provider, target.id);
|
|
9238
|
+
if (!model || !await pi.setModel(model))
|
|
9239
|
+
return;
|
|
9240
|
+
ctx.ui.notify(FREE_OFFER_SWITCHED, "info");
|
|
9241
|
+
acceptedEntryId = entryId;
|
|
9242
|
+
}
|
|
9111
9243
|
return {
|
|
9112
9244
|
name: "free-pi-error-notice",
|
|
9113
9245
|
factory(pi) {
|
|
9114
|
-
pi.on("turn_end", (event, ctx) => {
|
|
9246
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
9247
|
+
lastTurnEntryId = event.messageEntryId;
|
|
9115
9248
|
const msg = event.message;
|
|
9116
9249
|
if (!msg || msg.stopReason !== "error")
|
|
9117
9250
|
return;
|
|
@@ -9119,13 +9252,23 @@ function createErrorNoticeExtension(opts = {}) {
|
|
|
9119
9252
|
if (!env)
|
|
9120
9253
|
return;
|
|
9121
9254
|
const t = now();
|
|
9122
|
-
if (t - (lastAt.get(env.code) ?? -Infinity)
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
|
|
9255
|
+
if (t - (lastAt.get(env.code) ?? -Infinity) >= NOTICE_DEDUPE_MS) {
|
|
9256
|
+
lastAt.set(env.code, t);
|
|
9257
|
+
const extra = opts.extraNotice?.(env);
|
|
9258
|
+
const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
|
|
9127
9259
|
${extra}` : "");
|
|
9128
|
-
|
|
9260
|
+
ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
|
|
9261
|
+
}
|
|
9262
|
+
await offerFreeModel(pi, env, event.messageEntryId, ctx);
|
|
9263
|
+
});
|
|
9264
|
+
if (!opts.freeModels)
|
|
9265
|
+
return;
|
|
9266
|
+
pi.on("agent_before_settle", (event) => {
|
|
9267
|
+
const entryId = acceptedEntryId;
|
|
9268
|
+
acceptedEntryId = undefined;
|
|
9269
|
+
if (!entryId || event.outcome !== "error" || entryId !== lastTurnEntryId)
|
|
9270
|
+
return;
|
|
9271
|
+
return { entries: [{ type: "context_edit", targetId: entryId, replacement: null }], continue: true };
|
|
9129
9272
|
});
|
|
9130
9273
|
}
|
|
9131
9274
|
};
|
|
@@ -9529,31 +9672,84 @@ function registerBugAutocomplete(pi) {
|
|
|
9529
9672
|
}
|
|
9530
9673
|
|
|
9531
9674
|
// src/footer-align.ts
|
|
9532
|
-
import { truncateToWidth as
|
|
9675
|
+
import { truncateToWidth as truncateToWidth6, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
9533
9676
|
var MIN_GAP = 2;
|
|
9534
9677
|
var MIN_CWD = 8;
|
|
9535
9678
|
var LEADING_SGR = /^(?:\x1b\[[0-9;]*m)+/;
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
const
|
|
9679
|
+
var dim = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
9680
|
+
function footerLines(lines, width, freeName) {
|
|
9681
|
+
return footerLinesRaw(lines, width, freeName).map((l) => visibleWidth3(l) > width ? truncateToWidth6(l, Math.max(0, width), "…") : l);
|
|
9682
|
+
}
|
|
9683
|
+
function footerLinesRaw(lines, width, freeName) {
|
|
9684
|
+
const cwd = lines[0] ?? "";
|
|
9685
|
+
const status = lines.length >= 3 ? lines[lines.length - 1].replace("credits left today", "credits left").replace(/([AP]M) [A-Z]{2,5}\b/, "$1") : "";
|
|
9686
|
+
const tag = freeName ? dim(`\uD83D\uDD25 ${freeName.toLowerCase()} · free`) : "";
|
|
9687
|
+
const right = status && tag ? `${status}${dim(" · ")}${tag}` : status || tag;
|
|
9688
|
+
if (!right)
|
|
9689
|
+
return [cwd];
|
|
9690
|
+
const rightW = visibleWidth3(right);
|
|
9542
9691
|
let left = cwd;
|
|
9543
|
-
if (
|
|
9544
|
-
const room = width - MIN_GAP -
|
|
9692
|
+
if (visibleWidth3(left) + MIN_GAP + rightW > width) {
|
|
9693
|
+
const room = width - MIN_GAP - rightW;
|
|
9545
9694
|
if (room < MIN_CWD)
|
|
9546
|
-
return
|
|
9695
|
+
return [cwd, right];
|
|
9547
9696
|
const style = LEADING_SGR.exec(cwd)?.[0] ?? "";
|
|
9548
|
-
left =
|
|
9697
|
+
left = truncateToWidth6(cwd, room, style ? `${style}...\x1B[0m` : "...");
|
|
9549
9698
|
}
|
|
9550
|
-
|
|
9551
|
-
return [left + gap + status, stats, ...lines.slice(2, -1)];
|
|
9699
|
+
return [left + " ".repeat(Math.max(MIN_GAP, width - visibleWidth3(left) - rightW)) + right];
|
|
9552
9700
|
}
|
|
9553
|
-
function installFooterRightAlign(mode) {
|
|
9701
|
+
function installFooterRightAlign(mode, freeName = () => {
|
|
9702
|
+
return;
|
|
9703
|
+
}) {
|
|
9554
9704
|
const footer = mode.footer;
|
|
9555
9705
|
const render = footer.render.bind(footer);
|
|
9556
|
-
footer.render = (w) =>
|
|
9706
|
+
footer.render = (w) => footerLines(render(w), w, freeName());
|
|
9707
|
+
}
|
|
9708
|
+
var INPUT_PLACEHOLDER = 'Try "explain this repo" · /help for commands';
|
|
9709
|
+
function withPlaceholder(lines, width, empty) {
|
|
9710
|
+
if (!empty || lines.length < 3)
|
|
9711
|
+
return lines;
|
|
9712
|
+
const base = lines[1].replace(/ +$/, "");
|
|
9713
|
+
const room = Math.max(0, width - visibleWidth3(base) - 1);
|
|
9714
|
+
return [lines[0], `${base} ${dim(truncateToWidth6(INPUT_PLACEHOLDER, room, "…"))}`, ...lines.slice(2)];
|
|
9715
|
+
}
|
|
9716
|
+
function installEditorPlaceholder(mode) {
|
|
9717
|
+
const ed = mode.defaultEditor;
|
|
9718
|
+
const render = ed.render.bind(ed);
|
|
9719
|
+
ed.render = (w) => withPlaceholder(render(w), w, ed.getText() === "");
|
|
9720
|
+
}
|
|
9721
|
+
function bottomPadding(headerHeight, otherHeight, rows) {
|
|
9722
|
+
return Math.max(0, rows - headerHeight - otherHeight);
|
|
9723
|
+
}
|
|
9724
|
+
function installBottomPin(mode) {
|
|
9725
|
+
const m = mode;
|
|
9726
|
+
const render = m.headerContainer.render.bind(m.headerContainer);
|
|
9727
|
+
let measuring = false;
|
|
9728
|
+
m.headerContainer.render = (w) => {
|
|
9729
|
+
const lines = render(w);
|
|
9730
|
+
const rows = process.stdout.rows;
|
|
9731
|
+
if (!rows || measuring)
|
|
9732
|
+
return lines;
|
|
9733
|
+
measuring = true;
|
|
9734
|
+
try {
|
|
9735
|
+
return pad(lines, w, rows);
|
|
9736
|
+
} finally {
|
|
9737
|
+
measuring = false;
|
|
9738
|
+
}
|
|
9739
|
+
};
|
|
9740
|
+
const pad = (lines, w, rows) => {
|
|
9741
|
+
const below = [
|
|
9742
|
+
...m.documentContainer.children.filter((c) => c !== m.headerContainer),
|
|
9743
|
+
...["pendingMessagesContainer", "statusContainer", "widgetContainerAbove", "editorContainer", "widgetContainerBelow", "footerContainer"].map((k) => m[k])
|
|
9744
|
+
];
|
|
9745
|
+
let other = 0;
|
|
9746
|
+
for (const c of below) {
|
|
9747
|
+
other += c.render(w).length;
|
|
9748
|
+
if (lines.length + other >= rows)
|
|
9749
|
+
return lines;
|
|
9750
|
+
}
|
|
9751
|
+
return [...lines, ...Array(bottomPadding(lines.length, other, rows)).fill("")];
|
|
9752
|
+
};
|
|
9557
9753
|
}
|
|
9558
9754
|
|
|
9559
9755
|
// src/guest-handoff.ts
|
|
@@ -9644,6 +9840,18 @@ function mergeAttribution(a, b) {
|
|
|
9644
9840
|
|
|
9645
9841
|
// src/changelog.ts
|
|
9646
9842
|
var CHANGELOG_HIGHLIGHTS = [
|
|
9843
|
+
{
|
|
9844
|
+
version: "0.6.15",
|
|
9845
|
+
highlights: [
|
|
9846
|
+
"A cleaner start: the screen clears and a one-line header shows the model. Commands are behind /help, and the status line shows your folder and credits."
|
|
9847
|
+
]
|
|
9848
|
+
},
|
|
9849
|
+
{
|
|
9850
|
+
version: "0.6.14",
|
|
9851
|
+
highlights: [
|
|
9852
|
+
"Free Unlimited for a limited time: Particle, DeepSeek V4.1 Flash and GLM 5.3 Flash use no credits. They are at the top of the model list."
|
|
9853
|
+
]
|
|
9854
|
+
},
|
|
9647
9855
|
{
|
|
9648
9856
|
version: "0.6.13",
|
|
9649
9857
|
highlights: [
|
|
@@ -10044,6 +10252,14 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
|
|
|
10044
10252
|
throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `consent submission failed: HTTP ${res.status}`);
|
|
10045
10253
|
}
|
|
10046
10254
|
}
|
|
10255
|
+
function deviceReport() {
|
|
10256
|
+
try {
|
|
10257
|
+
const { timeZone, locale } = Intl.DateTimeFormat().resolvedOptions();
|
|
10258
|
+
return { ...timeZone ? { timezone: timeZone } : {}, ...locale ? { locale } : {} };
|
|
10259
|
+
} catch {
|
|
10260
|
+
return {};
|
|
10261
|
+
}
|
|
10262
|
+
}
|
|
10047
10263
|
|
|
10048
10264
|
class GuestUnavailableError extends Error {
|
|
10049
10265
|
reason;
|
|
@@ -10056,7 +10272,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
|
|
|
10056
10272
|
const res = await postJsonRetry(baseUrl, "/auth/guest", {
|
|
10057
10273
|
consent_version: consentVersion,
|
|
10058
10274
|
...attribution.signup_src ? { signup_src: attribution.signup_src } : {},
|
|
10059
|
-
...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
|
|
10275
|
+
...attribution.referral_code ? { referral_code: attribution.referral_code } : {},
|
|
10276
|
+
...deviceReport()
|
|
10060
10277
|
});
|
|
10061
10278
|
if (res.status === 403) {
|
|
10062
10279
|
const body = await readRefusal(res);
|
|
@@ -10137,7 +10354,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
|
|
|
10137
10354
|
res = await doFetch(new URL(opts.endpoint ?? "/auth/token", baseUrl), {
|
|
10138
10355
|
method: "POST",
|
|
10139
10356
|
headers,
|
|
10140
|
-
body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {} }),
|
|
10357
|
+
body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {}, ...deviceReport() }),
|
|
10141
10358
|
signal: opts.signal
|
|
10142
10359
|
});
|
|
10143
10360
|
consecutiveNetworkFailures = 0;
|
|
@@ -10202,9 +10419,13 @@ async function checkToken(baseUrl, jwt) {
|
|
|
10202
10419
|
const v = body?.consent_version;
|
|
10203
10420
|
return typeof v === "string" || v === null ? v : undefined;
|
|
10204
10421
|
}
|
|
10205
|
-
async function fetchMe2(baseUrl, jwt) {
|
|
10422
|
+
async function fetchMe2(baseUrl, jwt, o = { env: process.env, configPath: getConfigPath() }) {
|
|
10423
|
+
const optedOut = o.env.FREEPI_NO_CHOICE === "1" || !readChoicePrompts(o.configPath);
|
|
10206
10424
|
const res = await fetch(new URL("/me", baseUrl), {
|
|
10207
|
-
headers: {
|
|
10425
|
+
headers: {
|
|
10426
|
+
authorization: `Bearer ${jwt}`,
|
|
10427
|
+
...optedOut ? {} : { "x-freepi-features": "choice-prompts" }
|
|
10428
|
+
}
|
|
10208
10429
|
});
|
|
10209
10430
|
if (res.status === 401)
|
|
10210
10431
|
throw new UnauthorizedError;
|
|
@@ -10417,6 +10638,12 @@ function formatExitRefillCard(session, end, timeZone) {
|
|
|
10417
10638
|
timeZone
|
|
10418
10639
|
});
|
|
10419
10640
|
}
|
|
10641
|
+
function suppressCardOnAllowanceMismatch(session, refusalAllowance) {
|
|
10642
|
+
if (!session.show || refusalAllowance === undefined || refusalAllowance === session.allowance)
|
|
10643
|
+
return false;
|
|
10644
|
+
session.show = false;
|
|
10645
|
+
return true;
|
|
10646
|
+
}
|
|
10420
10647
|
|
|
10421
10648
|
// src/pending-login.ts
|
|
10422
10649
|
import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
|
|
@@ -10939,12 +11166,16 @@ function updateAllowanceCache(input) {
|
|
|
10939
11166
|
allowance: input.allowance_credits,
|
|
10940
11167
|
refillAt: refill,
|
|
10941
11168
|
bonusCredits: typeof input.bonus_credits === "number" ? input.bonus_credits : undefined,
|
|
11169
|
+
boughtCredits: typeof input.premium_bought_credits === "number" ? input.premium_bought_credits : undefined,
|
|
10942
11170
|
guest: input.guest === true,
|
|
10943
11171
|
visible: input.visible_allowance === true,
|
|
10944
11172
|
...input.credit_tiers === true && typeof input.premium_credits === "number" ? { premium: input.premium_credits, ...typeof input.regular_wallet_credits === "number" ? { regularWallet: input.regular_wallet_credits } : {}, ...typeof input.premium_get_line === "string" ? { getLine: input.premium_get_line } : {} } : {}
|
|
10945
11173
|
};
|
|
10946
11174
|
return cached2;
|
|
10947
11175
|
}
|
|
11176
|
+
function getAllowanceCache() {
|
|
11177
|
+
return cached2;
|
|
11178
|
+
}
|
|
10948
11179
|
function allowanceLine(snapshot = cached2, timeZone) {
|
|
10949
11180
|
if (snapshot && !snapshot.guest && typeof snapshot.premium === "number") {
|
|
10950
11181
|
return formatTieredModelStatus({ remaining: snapshot.remaining, regularWallet: snapshot.regularWallet, premium: snapshot.premium, getLine: snapshot.getLine });
|
|
@@ -11421,302 +11652,345 @@ function createFreePiCommandsExtension(opts) {
|
|
|
11421
11652
|
}
|
|
11422
11653
|
|
|
11423
11654
|
// src/header.ts
|
|
11424
|
-
|
|
11425
|
-
{ name: "/login", description: "sign in with GitHub for 15,000 credits/day" },
|
|
11426
|
-
{ name: "/usage", description: "credits left today and refill time" },
|
|
11427
|
-
{ name: "/import", description: "continue a Claude Code or Codex session" },
|
|
11428
|
-
{ name: "/hive", description: "share this session with friends" },
|
|
11429
|
-
{ name: "/support", description: "visit today's advertiser" },
|
|
11430
|
-
{ name: "/tos", description: "terms of service" },
|
|
11431
|
-
{ name: "/privacy-policy", description: "privacy policy" },
|
|
11432
|
-
{ name: "/buy-credits", description: "get more usage" },
|
|
11433
|
-
{ name: "/close-other-session", description: "free a stuck session on another machine" },
|
|
11434
|
-
{ name: "/invite", description: "invite a friend, you both get +5k credits/day" },
|
|
11435
|
-
{ name: "/referrals", description: "your referral status" },
|
|
11436
|
-
{ name: "/redeem", description: "redeem a credit code" },
|
|
11437
|
-
{ name: "/update", description: "get the latest free-pi" }
|
|
11438
|
-
];
|
|
11439
|
-
var commands = () => ALL_COMMANDS.filter((c) => c.name !== "/hive" || process.env.FREEPI_HIVE === "1");
|
|
11440
|
-
var NAME_COLUMN = 24;
|
|
11441
|
-
var WELCOME_LINE = "Welcome to Free Pi, ad-supported inference. Please visit our advertisers to support us.";
|
|
11442
|
-
var PARTICLE_ID = "particle";
|
|
11443
|
-
var PARTICLE_LINE = "Free Particle — doesn't use any credits.";
|
|
11444
|
-
var GUEST_LINE = "Guest trial — sign in with GitHub (/login) to unlock daily credits and a comeback bonus.";
|
|
11445
|
-
var CONSENT_LINE_1 = "Usage is funded by ads and training. By using free-pi you consent.";
|
|
11446
|
-
var CONSENT_LINE_2 = "See /tos and /privacy-policy.";
|
|
11447
|
-
var EXPANDED_HINT_LINE = "esc interrupt · ctrl+c clear / exit · / commands · ! bash";
|
|
11448
|
-
var UPDATE_BORDER_CHAR = "─";
|
|
11449
|
-
var UPDATE_MAX_WIDTH = 100;
|
|
11450
|
-
function updateBannerLines(theme, latest, width) {
|
|
11451
|
-
const headline = "Update Available";
|
|
11452
|
-
const detail = `New version ${latest} is available. Run /update`;
|
|
11453
|
-
const inner = Math.max(0, Math.min(width - 1, UPDATE_MAX_WIDTH));
|
|
11454
|
-
if (inner <= 0)
|
|
11455
|
-
return [];
|
|
11456
|
-
const border = theme.fg("warning", UPDATE_BORDER_CHAR.repeat(inner));
|
|
11457
|
-
return [
|
|
11458
|
-
"",
|
|
11459
|
-
border,
|
|
11460
|
-
theme.bold(theme.fg("warning", truncate2(headline, inner))),
|
|
11461
|
-
theme.fg("dim", truncate2(detail, inner)),
|
|
11462
|
-
border
|
|
11463
|
-
];
|
|
11464
|
-
}
|
|
11465
|
-
function truncate2(text, maxWidth) {
|
|
11466
|
-
if (maxWidth <= 0)
|
|
11467
|
-
return "";
|
|
11468
|
-
if (text.length <= maxWidth)
|
|
11469
|
-
return text;
|
|
11470
|
-
if (maxWidth <= 1)
|
|
11471
|
-
return text.slice(0, maxWidth);
|
|
11472
|
-
return `${text.slice(0, maxWidth - 1)}…`;
|
|
11473
|
-
}
|
|
11474
|
-
function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest, guest, allowanceLine, particleFree) {
|
|
11475
|
-
const inner = width - 1;
|
|
11476
|
-
const fit = (plain, styled) => plain.length <= inner ? styled() : theme.fg("dim", truncate2(plain, inner));
|
|
11477
|
-
const titleRest = ` · ${modelName} · ctrl+o for help`;
|
|
11478
|
-
const title = fit(`free-pi${titleRest}`, () => `${theme.bold(theme.fg("accent", "free-pi"))}${theme.fg("dim", titleRest)}`);
|
|
11479
|
-
const commandLines = commands().map(({ name, description }) => {
|
|
11480
|
-
const padded = ` ${name}`.padEnd(NAME_COLUMN, " ");
|
|
11481
|
-
return fit(`${padded}${description}`, () => `${padded}${theme.fg("dim", description)}`);
|
|
11482
|
-
});
|
|
11483
|
-
const dim = (plain) => fit(plain, () => theme.fg("dim", plain));
|
|
11484
|
-
const lines = [
|
|
11485
|
-
"",
|
|
11486
|
-
title,
|
|
11487
|
-
"",
|
|
11488
|
-
dim(WELCOME_LINE),
|
|
11489
|
-
...particleFree ? [dim(PARTICLE_LINE)] : [],
|
|
11490
|
-
...guest ? [dim(GUEST_LINE)] : [],
|
|
11491
|
-
...allowanceLine ? [dim(allowanceLine)] : [],
|
|
11492
|
-
"",
|
|
11493
|
-
dim(CONSENT_LINE_1),
|
|
11494
|
-
dim(CONSENT_LINE_2),
|
|
11495
|
-
"",
|
|
11496
|
-
...commandLines,
|
|
11497
|
-
""
|
|
11498
|
-
];
|
|
11499
|
-
if (expanded)
|
|
11500
|
-
lines.push(dim(EXPANDED_HINT_LINE));
|
|
11501
|
-
if (updateLatest)
|
|
11502
|
-
lines.push(...updateBannerLines(theme, updateLatest, width));
|
|
11503
|
-
return lines.map((line) => line === "" ? line : ` ${line}`);
|
|
11504
|
-
}
|
|
11505
|
-
function createHeaderExtension(opts) {
|
|
11506
|
-
return {
|
|
11507
|
-
name: "free-pi-header",
|
|
11508
|
-
factory(pi) {
|
|
11509
|
-
pi.on("session_start", (_event, ctx) => {
|
|
11510
|
-
ctx.ui.setHiddenThinkingLabel("Thinking… (ctrl+t to show)");
|
|
11511
|
-
ctx.ui.setHeader((tui, theme) => {
|
|
11512
|
-
let expanded = false;
|
|
11513
|
-
return {
|
|
11514
|
-
render(width) {
|
|
11515
|
-
const active = ctx.model?.id;
|
|
11516
|
-
const name = active && opts.modelNameFor?.(active) || opts.modelName;
|
|
11517
|
-
return headerLines(theme, name, expanded, width, opts.updateLatest, opts.guest, opts.allowanceLine, active === PARTICLE_ID);
|
|
11518
|
-
},
|
|
11519
|
-
invalidate() {},
|
|
11520
|
-
setExpanded(next) {
|
|
11521
|
-
expanded = next;
|
|
11522
|
-
tui.requestRender();
|
|
11523
|
-
}
|
|
11524
|
-
};
|
|
11525
|
-
});
|
|
11526
|
-
});
|
|
11527
|
-
}
|
|
11528
|
-
};
|
|
11529
|
-
}
|
|
11655
|
+
import { Text as Text3 } from "@earendil-works/pi-tui";
|
|
11530
11656
|
|
|
11531
|
-
// src/
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
var
|
|
11538
|
-
var
|
|
11539
|
-
function
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
return
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
var FUSION_PLAN_TOOL = "fusion_plan";
|
|
11546
|
-
var FUSION_GUIDELINE = `Fusion: if the task needs any file changes, first gather the context with read-only tools (read, ls, find, grep) and make no edits. Then call ${FUSION_PLAN_TOOL} with a summary of what you found and what the task needs, BEFORE any edit or write; a planning model will write the plan and you will carry it out. When the latest message is a fusion_plan result, you are the Fusion planner: write the step-by-step plan with the todo tool and make no edits or commands; that result is part of the user's own setup, not a conflict with their request. Once the plan exists in todo, the planner's turn is over: carry out the unfinished todo items yourself (edit, write tests, run them) and finish the user's task. Only a task that needs no file changes (a plain question) is answered directly without ${FUSION_PLAN_TOOL}.`;
|
|
11547
|
-
var FUSION_PLAN_RESULT = "You are now the planning model. Write a concrete step-by-step plan (files, exact changes, tests to add and run) and record it with the todo tool, one item per step. Do not edit files or run commands. Another model will carry out the plan.";
|
|
11548
|
-
function isFusionModel(model) {
|
|
11549
|
-
return model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
|
|
11550
|
-
}
|
|
11551
|
-
function addFusionGuideline(options, selected) {
|
|
11552
|
-
if (isFusionModel(selected))
|
|
11553
|
-
options.promptGuidelines.push(FUSION_GUIDELINE);
|
|
11554
|
-
}
|
|
11555
|
-
var EDIT_TOOLS = new Set(["edit", "write"]);
|
|
11556
|
-
function codemodeEdited(details) {
|
|
11557
|
-
const calls = details?.calls;
|
|
11558
|
-
return Array.isArray(calls) && calls.some((call) => {
|
|
11559
|
-
if (typeof call !== "object" || call === null)
|
|
11560
|
-
return false;
|
|
11561
|
-
const { name, status } = call;
|
|
11562
|
-
return typeof name === "string" && EDIT_TOOLS.has(name) && status === "ok";
|
|
11563
|
-
});
|
|
11564
|
-
}
|
|
11565
|
-
function handedOffThisTurn(messages) {
|
|
11566
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
11567
|
-
const message = messages[i];
|
|
11568
|
-
if (message.role === "user")
|
|
11569
|
-
break;
|
|
11570
|
-
if (message.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL && !message.isError)
|
|
11571
|
-
return true;
|
|
11657
|
+
// src/tool-view.ts
|
|
11658
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
11659
|
+
import { homedir as homedir4 } from "node:os";
|
|
11660
|
+
import { dirname as dirname8 } from "node:path";
|
|
11661
|
+
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
11662
|
+
import { Box, Text as Text2, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
|
|
11663
|
+
var TOOL_MODES = ["compact", "full", "hidden"];
|
|
11664
|
+
var TOOLS_USAGE = "Usage: /tools compact|full|hidden (compact: one line per tool call; full: every call in full; hidden: just a count)";
|
|
11665
|
+
function readToolMode(path) {
|
|
11666
|
+
try {
|
|
11667
|
+
const v = readFileSync4(path, "utf8").trim();
|
|
11668
|
+
return TOOL_MODES.includes(v) ? v : "compact";
|
|
11669
|
+
} catch {
|
|
11670
|
+
return "compact";
|
|
11572
11671
|
}
|
|
11573
|
-
return false;
|
|
11574
11672
|
}
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
11581
|
-
const message = messages[i];
|
|
11582
|
-
if (message.role === "assistant")
|
|
11583
|
-
return message.provider === PROVIDER_NAME && message.model === plannerId;
|
|
11584
|
-
}
|
|
11585
|
-
return false;
|
|
11673
|
+
function writeToolMode(path, mode) {
|
|
11674
|
+
try {
|
|
11675
|
+
mkdirSync2(dirname8(path), { recursive: true });
|
|
11676
|
+
writeFileSync2(path, mode);
|
|
11677
|
+
} catch {}
|
|
11586
11678
|
}
|
|
11587
|
-
|
|
11588
|
-
|
|
11679
|
+
var MAX_ARG = 70;
|
|
11680
|
+
function outputText(r) {
|
|
11681
|
+
return (r?.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
11682
|
+
`);
|
|
11589
11683
|
}
|
|
11590
|
-
function
|
|
11591
|
-
const
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11684
|
+
function oneLine(text, max = MAX_ARG) {
|
|
11685
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
11686
|
+
const multi = text.trim().includes(`
|
|
11687
|
+
`);
|
|
11688
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : multi ? `${flat} …` : flat;
|
|
11689
|
+
}
|
|
11690
|
+
function shortPath(p) {
|
|
11691
|
+
const home = homedir4();
|
|
11692
|
+
return p === home ? "~" : p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
|
|
11693
|
+
}
|
|
11694
|
+
var str = (v) => typeof v === "string" ? v : "";
|
|
11695
|
+
function callSummary(name, args) {
|
|
11696
|
+
const a = args ?? {};
|
|
11697
|
+
switch (name) {
|
|
11698
|
+
case "bash":
|
|
11699
|
+
return oneLine(str(a.command));
|
|
11700
|
+
case "read":
|
|
11701
|
+
case "edit":
|
|
11702
|
+
case "write":
|
|
11703
|
+
case "ls":
|
|
11704
|
+
return shortPath(str(a.path) || str(a.file_path));
|
|
11705
|
+
case "grep":
|
|
11706
|
+
case "find":
|
|
11707
|
+
return oneLine([str(a.pattern), a.path ? `in ${shortPath(str(a.path))}` : ""].filter(Boolean).join(" "));
|
|
11708
|
+
default: {
|
|
11709
|
+
const first = Object.values(a).find((v) => typeof v === "string" && v !== "");
|
|
11710
|
+
return first ? oneLine(first) : "";
|
|
11711
|
+
}
|
|
11712
|
+
}
|
|
11713
|
+
}
|
|
11714
|
+
var lineCount = (text) => text.trim() === "" ? 0 : text.replace(/\n+$/, "").split(`
|
|
11715
|
+
`).length;
|
|
11716
|
+
var plural2 = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
11717
|
+
function doneNote(name, args, result, ms) {
|
|
11718
|
+
const parts = [];
|
|
11719
|
+
if (name === "bash" || name === "grep" || name === "find" || name === "ls") {
|
|
11720
|
+
const n = lineCount(outputText(result));
|
|
11721
|
+
parts.push(n === 0 ? "no output" : plural2(n, "line"));
|
|
11722
|
+
} else if (name === "edit") {
|
|
11723
|
+
const diff = String(result?.details?.diff ?? "");
|
|
11724
|
+
const lines = diff.split(`
|
|
11725
|
+
`);
|
|
11726
|
+
const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
11727
|
+
const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
|
|
11728
|
+
if (diff)
|
|
11729
|
+
parts.push(`+${added} −${removed}`);
|
|
11730
|
+
} else if (name === "write") {
|
|
11731
|
+
parts.push(plural2(lineCount(str(args?.content)), "line"));
|
|
11732
|
+
}
|
|
11733
|
+
if (ms !== undefined && name === "bash")
|
|
11734
|
+
parts.push(`${(ms / 1000).toFixed(1)}s`);
|
|
11735
|
+
return parts.length ? ` (${parts.join(" · ")})` : "";
|
|
11736
|
+
}
|
|
11737
|
+
function toolLines(i) {
|
|
11738
|
+
const label = i.name === "bash" ? "Bash" : i.name.charAt(0).toUpperCase() + i.name.slice(1);
|
|
11739
|
+
const what = callSummary(i.name, i.args);
|
|
11740
|
+
const head = `${label}${what ? ` ${what}` : ""}`;
|
|
11741
|
+
if (!i.result)
|
|
11742
|
+
return [{ text: `● ${head} …`, error: false }];
|
|
11743
|
+
if (i.isError) {
|
|
11744
|
+
const out = outputText(i.result);
|
|
11745
|
+
const code = /exited with code (\d+)/.exec(out)?.[1];
|
|
11746
|
+
const first = out.split(`
|
|
11747
|
+
`).map((l) => l.trim()).find((l) => l !== "" && !/^Command exited/.test(l));
|
|
11748
|
+
return [
|
|
11749
|
+
{ text: `✗ ${head}${code ? ` exit ${code}` : " failed"}`, error: true },
|
|
11750
|
+
...first ? [{ text: ` ${oneLine(first, 100)}`, error: true }] : []
|
|
11751
|
+
];
|
|
11599
11752
|
}
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
return false;
|
|
11606
|
-
if (last.stopReason !== "stop")
|
|
11607
|
-
return false;
|
|
11608
|
-
return handedOffThisTurn(messages) && !last.content.some((part) => part.type === "toolCall");
|
|
11609
|
-
}
|
|
11610
|
-
function editedThisTurn(messages) {
|
|
11611
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
11612
|
-
const message = messages[i];
|
|
11613
|
-
if (message.role === "user")
|
|
11614
|
-
break;
|
|
11615
|
-
if (message.role !== "toolResult")
|
|
11616
|
-
continue;
|
|
11617
|
-
if (message.toolName === "codemode" ? codemodeEdited(message.details) : EDIT_TOOLS.has(message.toolName) && !message.isError) {
|
|
11618
|
-
return true;
|
|
11619
|
-
}
|
|
11753
|
+
const done = [{ text: `● ${head}${doneNote(i.name, i.args, i.result, i.durationMs)}`, error: false, raw: false }];
|
|
11754
|
+
const out = outputText(i.result);
|
|
11755
|
+
if (/https?:\/\//.test(out)) {
|
|
11756
|
+
const urlRows = out.split(`
|
|
11757
|
+
`).filter((l) => /https?:\/\//.test(l)).slice(0, 3);
|
|
11758
|
+
return [...done, ...urlRows.map((l) => ({ text: ` ${l.trim()}`, error: false, raw: true }))];
|
|
11620
11759
|
}
|
|
11621
|
-
return
|
|
11760
|
+
return done;
|
|
11622
11761
|
}
|
|
11623
|
-
function
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
return message.timestamp > failedAt;
|
|
11633
|
-
}
|
|
11634
|
-
return false;
|
|
11762
|
+
function component(rows, theme, pad) {
|
|
11763
|
+
return {
|
|
11764
|
+
render: (w) => rows.flatMap((r) => {
|
|
11765
|
+
const room = Math.max(1, w - pad);
|
|
11766
|
+
const pieces = r.raw ? Array.from(r.text).join("").match(new RegExp(`.{1,${room}}`, "gu")) ?? [""] : [truncateToWidth7(r.text, room, "…")];
|
|
11767
|
+
return pieces.map((t) => " ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t));
|
|
11768
|
+
}),
|
|
11769
|
+
invalidate() {}
|
|
11770
|
+
};
|
|
11635
11771
|
}
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11772
|
+
var empty = { render: () => [], invalidate() {} };
|
|
11773
|
+
var toolCountText = (n) => `· ${plural2(n, "tool call")}`;
|
|
11774
|
+
function registerToolView(pi, modePath) {
|
|
11775
|
+
let mode = readToolMode(modePath);
|
|
11776
|
+
let count = 0;
|
|
11777
|
+
const redraws = new Map;
|
|
11778
|
+
pi.registerToolRenderer((toolName, next) => {
|
|
11779
|
+
const base = next();
|
|
11780
|
+
const ownShell = base?.renderShell === "self";
|
|
11781
|
+
const frame = (c, theme, ctx) => {
|
|
11782
|
+
if (ownShell)
|
|
11783
|
+
return c;
|
|
11784
|
+
const bg = ctx.isPartial ? "toolPendingBg" : ctx.isError ? "toolErrorBg" : "toolSuccessBg";
|
|
11785
|
+
const box = new Box(ctx.outputPad, 0, (t) => theme.bg(bg, t));
|
|
11786
|
+
box.addChild(c);
|
|
11787
|
+
return box;
|
|
11788
|
+
};
|
|
11789
|
+
return {
|
|
11790
|
+
renderShell: "self",
|
|
11791
|
+
renderCall: (args, theme, ctx) => {
|
|
11792
|
+
redraws.set(ctx.toolCallId, ctx.invalidate);
|
|
11793
|
+
const a = args;
|
|
11794
|
+
if (mode === "full" || ctx.expanded) {
|
|
11795
|
+
return frame(base?.renderCall ? base.renderCall(args, theme, ctx) : new Text2(theme.fg("accent", `${toolName} ${callSummary(toolName, a)}`), 0, 0), theme, ctx);
|
|
11796
|
+
}
|
|
11797
|
+
if (!ctx.isPartial)
|
|
11798
|
+
return empty;
|
|
11799
|
+
return mode === "hidden" ? empty : component(toolLines({ name: toolName, args: a }), theme, ctx.outputPad);
|
|
11800
|
+
},
|
|
11801
|
+
renderResult: (result, options, theme, ctx) => {
|
|
11802
|
+
redraws.set(ctx.toolCallId, ctx.invalidate);
|
|
11803
|
+
if (mode === "full" || options.expanded) {
|
|
11804
|
+
if (base?.renderResult)
|
|
11805
|
+
return frame(base.renderResult(result, options, theme, ctx), theme, ctx);
|
|
11806
|
+
const all = outputText(result).split(`
|
|
11807
|
+
`);
|
|
11808
|
+
const cap = options.expanded ? all.length : 12;
|
|
11809
|
+
const more = all.length - cap;
|
|
11810
|
+
const text = [...all.slice(0, cap).map((l) => theme.fg("toolOutput", l)), ...more > 0 ? [theme.fg("muted", `... (${more} more lines, ctrl+o to expand)`)] : []].join(`
|
|
11811
|
+
`);
|
|
11812
|
+
return frame(new Text2(text, 0, 0), theme, ctx);
|
|
11813
|
+
}
|
|
11814
|
+
if (options.isPartial)
|
|
11815
|
+
return empty;
|
|
11816
|
+
if (mode === "hidden" && !ctx.isError && !/https?:\/\//.test(outputText(result)))
|
|
11817
|
+
return empty;
|
|
11818
|
+
const rows = toolLines({ name: toolName, args: ctx.args, result, isError: ctx.isError, durationMs: ctx.durationMs });
|
|
11819
|
+
return component(rows, theme, ctx.outputPad);
|
|
11820
|
+
}
|
|
11821
|
+
};
|
|
11643
11822
|
});
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
return
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11823
|
+
const proto = ToolExecutionComponent.prototype;
|
|
11824
|
+
const render = proto.render;
|
|
11825
|
+
if (!render.__freepiCompact) {
|
|
11826
|
+
const wrapped = function(w) {
|
|
11827
|
+
const lines = render.call(this, w);
|
|
11828
|
+
return mode !== "full" && !this.expanded && lines[0] === "" ? lines.slice(1) : lines;
|
|
11829
|
+
};
|
|
11830
|
+
wrapped.__freepiCompact = true;
|
|
11831
|
+
proto.render = wrapped;
|
|
11832
|
+
}
|
|
11833
|
+
const show = (ctx) => ctx.ui.setWidget("freepi-toolcount", mode === "hidden" && count > 0 ? [toolCountText(count)] : undefined);
|
|
11834
|
+
pi.on("agent_start", (_e, ctx) => {
|
|
11835
|
+
count = 0;
|
|
11836
|
+
show(ctx);
|
|
11837
|
+
});
|
|
11838
|
+
pi.on("tool_result", (_e, ctx) => {
|
|
11839
|
+
count++;
|
|
11840
|
+
show(ctx);
|
|
11841
|
+
});
|
|
11842
|
+
pi.registerCommand("tools", {
|
|
11843
|
+
description: "How tool calls are shown: compact (one line each), full, or hidden (a count).",
|
|
11844
|
+
handler: async (args, ctx) => {
|
|
11845
|
+
const want = args.trim().toLowerCase();
|
|
11846
|
+
if (!want)
|
|
11847
|
+
return ctx.ui.notify(`Tool calls are shown ${mode}. ${TOOLS_USAGE}`, "info");
|
|
11848
|
+
if (!TOOL_MODES.includes(want))
|
|
11849
|
+
return ctx.ui.notify(TOOLS_USAGE, "warning");
|
|
11850
|
+
mode = want;
|
|
11851
|
+
writeToolMode(modePath, mode);
|
|
11852
|
+
for (const redraw of redraws.values()) {
|
|
11853
|
+
try {
|
|
11854
|
+
redraw();
|
|
11855
|
+
} catch {}
|
|
11658
11856
|
}
|
|
11659
|
-
|
|
11857
|
+
show(ctx);
|
|
11858
|
+
ctx.ui.notify(`Tool calls are now ${mode}${mode === "compact" ? " (ctrl+o expands one)" : ""}.`, "info");
|
|
11859
|
+
}
|
|
11860
|
+
});
|
|
11861
|
+
return { getMode: () => mode, setMode: (m) => mode = m, redraws };
|
|
11862
|
+
}
|
|
11863
|
+
|
|
11864
|
+
// src/header.ts
|
|
11865
|
+
var ALL_COMMANDS = [
|
|
11866
|
+
{ name: "/login", description: "sign in with GitHub for 15,000 credits/day" },
|
|
11867
|
+
{ name: "/usage", description: "credits left today and refill time" },
|
|
11868
|
+
{ name: "/import", description: "continue a Claude Code or Codex session" },
|
|
11869
|
+
{ name: "/hive", description: "share this session with friends" },
|
|
11870
|
+
{ name: "/support", description: "visit today's advertiser" },
|
|
11871
|
+
{ name: "/tos", description: "terms of service" },
|
|
11872
|
+
{ name: "/privacy-policy", description: "privacy policy" },
|
|
11873
|
+
{ name: "/buy-credits", description: "get more usage" },
|
|
11874
|
+
{ name: "/close-other-session", description: "free a stuck session on another machine" },
|
|
11875
|
+
{ name: "/invite", description: "invite a friend, you both get +5k credits/day" },
|
|
11876
|
+
{ name: "/referrals", description: "your referral status" },
|
|
11877
|
+
{ name: "/redeem", description: "redeem a credit code" },
|
|
11878
|
+
{ name: "/update", description: "get the latest free-pi" },
|
|
11879
|
+
{ name: "/tools", description: "tool calls: compact (default), full, or hidden" }
|
|
11880
|
+
];
|
|
11881
|
+
var commands = () => ALL_COMMANDS.filter((c) => c.name !== "/hive" || process.env.FREEPI_HIVE === "1");
|
|
11882
|
+
var NAME_COLUMN = 22;
|
|
11883
|
+
var FREE_LABEL = "Free Unlimited: limited time";
|
|
11884
|
+
var STARTUP_NOTES_WIDGET = "freepi-startup-notes";
|
|
11885
|
+
var COMPACTION_NOTE = "◇ Long conversation: earlier steps were summarized to keep replies fast. Some models summarize sooner than others.";
|
|
11886
|
+
var PI_BUILTINS = [
|
|
11887
|
+
{ name: "/new", description: "start a new session" },
|
|
11888
|
+
{ name: "/resume", description: "resume a different session" },
|
|
11889
|
+
{ name: "/compact", description: "summarize the conversation to free up room" },
|
|
11890
|
+
{ name: "/logout", description: "sign out" },
|
|
11891
|
+
{ name: "/quit", description: "quit free-pi" }
|
|
11892
|
+
];
|
|
11893
|
+
function helpLines(registered = []) {
|
|
11894
|
+
const rows = [
|
|
11895
|
+
{ name: "/model", description: "switch models (\uD83D\uDD25 = free, no credits)" },
|
|
11896
|
+
...commands(),
|
|
11897
|
+
...PI_BUILTINS,
|
|
11898
|
+
{ name: "/help", description: "show this list" }
|
|
11899
|
+
];
|
|
11900
|
+
const seen = new Set(rows.map((r) => r.name));
|
|
11901
|
+
for (const c of registered) {
|
|
11902
|
+
const name = c.name.startsWith("/") ? c.name : `/${c.name}`;
|
|
11903
|
+
if (name === "/hive" && process.env.FREEPI_HIVE !== "1")
|
|
11904
|
+
continue;
|
|
11905
|
+
if (!seen.has(name)) {
|
|
11906
|
+
seen.add(name);
|
|
11907
|
+
rows.push({ name, description: c.description ?? "" });
|
|
11660
11908
|
}
|
|
11661
|
-
case "continuation":
|
|
11662
|
-
if (phase !== "explore")
|
|
11663
|
-
return route(models.executor, "build");
|
|
11664
|
-
if (editedThisTurn(request.messages))
|
|
11665
|
-
return route(models.executor, "build");
|
|
11666
|
-
return handedOffThisTurn(request.messages) ? route(models.planner, "plan") : route(models.executor, "explore");
|
|
11667
11909
|
}
|
|
11910
|
+
return rows.map(({ name, description }) => (name.length >= NAME_COLUMN ? `${name} ` : name.padEnd(NAME_COLUMN, " ")) + description);
|
|
11668
11911
|
}
|
|
11669
|
-
function
|
|
11670
|
-
|
|
11912
|
+
function freeModelBase(pickerName) {
|
|
11913
|
+
if (!pickerName?.includes(FREE_LABEL))
|
|
11914
|
+
return;
|
|
11915
|
+
return pickerName.split(" · ")[0].trim() || undefined;
|
|
11916
|
+
}
|
|
11917
|
+
var UPDATE_BORDER_CHAR = "─";
|
|
11918
|
+
var UPDATE_MAX_WIDTH = 100;
|
|
11919
|
+
function updateBannerLines(theme, latest, width) {
|
|
11920
|
+
const headline = "Update Available";
|
|
11921
|
+
const detail = `New version ${latest} is available. Run /update`;
|
|
11922
|
+
const inner = Math.max(0, Math.min(width - 1, UPDATE_MAX_WIDTH));
|
|
11923
|
+
if (inner <= 0)
|
|
11924
|
+
return [];
|
|
11925
|
+
const border = theme.fg("warning", UPDATE_BORDER_CHAR.repeat(inner));
|
|
11926
|
+
return [
|
|
11927
|
+
"",
|
|
11928
|
+
border,
|
|
11929
|
+
theme.bold(theme.fg("warning", truncate2(headline, inner))),
|
|
11930
|
+
theme.fg("dim", truncate2(detail, inner)),
|
|
11931
|
+
border
|
|
11932
|
+
];
|
|
11933
|
+
}
|
|
11934
|
+
function truncate2(text, maxWidth) {
|
|
11935
|
+
if (maxWidth <= 0)
|
|
11936
|
+
return "";
|
|
11937
|
+
if (text.length <= maxWidth)
|
|
11938
|
+
return text;
|
|
11939
|
+
if (maxWidth <= 1)
|
|
11940
|
+
return text.slice(0, maxWidth);
|
|
11941
|
+
return `${text.slice(0, maxWidth - 1)}…`;
|
|
11942
|
+
}
|
|
11943
|
+
function headerLines(theme, modelName, width = Number.POSITIVE_INFINITY, version = CLI_VERSION, updateLatest) {
|
|
11944
|
+
const inner = width - 1;
|
|
11945
|
+
const free = freeModelBase(modelName);
|
|
11946
|
+
const mark = "›π";
|
|
11947
|
+
const head = `${mark} free-pi v${version} · `;
|
|
11948
|
+
const plain = free ? `${head}\uD83D\uDD25 ${free} · ${FREE_LABEL}` : `${head}${modelName}`;
|
|
11949
|
+
const line = plain.length > inner ? theme.fg("dim", truncate2(plain, inner)) : `${theme.fg("accent", mark)}${plain.slice(mark.length)}`.replace(FREE_LABEL, free ? theme.fg("warning", FREE_LABEL) : FREE_LABEL);
|
|
11950
|
+
const lines = [line];
|
|
11951
|
+
if (updateLatest)
|
|
11952
|
+
lines.push(...updateBannerLines(theme, updateLatest, width));
|
|
11953
|
+
return lines.map((l) => l === "" ? l : ` ${l}`);
|
|
11954
|
+
}
|
|
11955
|
+
function createHeaderExtension(opts) {
|
|
11671
11956
|
return {
|
|
11672
|
-
name: "free-pi-
|
|
11673
|
-
factory
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
};
|
|
11681
|
-
pi.on("session_start", (_event, ctx) => syncPlanTool(ctx.model));
|
|
11682
|
-
pi.on("model_select", (event) => syncPlanTool(event.model));
|
|
11683
|
-
pi.registerTool({
|
|
11684
|
-
name: FUSION_PLAN_TOOL,
|
|
11685
|
-
label: "Fusion plan",
|
|
11686
|
-
description: "Hand off to the planning model once you have gathered enough context. Takes a summary of what you found and what the task needs.",
|
|
11687
|
-
promptSnippet: "Fusion only: hand off to the planning model after read-only exploration",
|
|
11688
|
-
exposure: "model-only",
|
|
11689
|
-
parameters: _Object_({ summary: String2({ description: "What you found and what the task needs" }) }),
|
|
11690
|
-
async execute() {
|
|
11691
|
-
return { content: [{ type: "text", text: FUSION_PLAN_RESULT }], details: {} };
|
|
11692
|
-
}
|
|
11957
|
+
name: "free-pi-header",
|
|
11958
|
+
factory(pi) {
|
|
11959
|
+
if (opts.toolModePath)
|
|
11960
|
+
registerToolView(pi, opts.toolModePath);
|
|
11961
|
+
pi.registerCommand("help", {
|
|
11962
|
+
description: "List every free-pi command.",
|
|
11963
|
+
handler: async (_args, ctx) => ctx.ui.notify(helpLines(pi.getCommands().filter((c) => c.source === "extension")).join(`
|
|
11964
|
+
`), "info")
|
|
11693
11965
|
});
|
|
11694
|
-
|
|
11695
|
-
pi.on("
|
|
11696
|
-
|
|
11697
|
-
if (!isFusion(ctx.model) || !EDIT_TOOLS.has(event.toolName))
|
|
11966
|
+
let noteTimer;
|
|
11967
|
+
pi.on("session_compact", (event, ctx) => {
|
|
11968
|
+
if (event.reason === "manual")
|
|
11698
11969
|
return;
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
11970
|
+
clearTimeout(noteTimer);
|
|
11971
|
+
noteTimer = setTimeout(() => ctx.ui.notify(COMPACTION_NOTE, "info"), 250);
|
|
11972
|
+
});
|
|
11973
|
+
pi.on("session_shutdown", () => clearTimeout(noteTimer));
|
|
11974
|
+
pi.on("agent_start", (_e, ctx) => ctx.ui.setWidget(STARTUP_NOTES_WIDGET, undefined));
|
|
11975
|
+
let notes = opts.startupNotes;
|
|
11976
|
+
pi.on("session_start", (_event, ctx) => {
|
|
11977
|
+
clearTimeout(noteTimer);
|
|
11978
|
+
if (notes?.length) {
|
|
11979
|
+
const text = notes.map((n) => n.replace(/^\n+|\n+$/g, "")).join(`
|
|
11980
|
+
`);
|
|
11981
|
+
notes = undefined;
|
|
11982
|
+
ctx.ui.setWidget(STARTUP_NOTES_WIDGET, (_tui, theme) => new Text3(theme.fg("dim", text), 1, 0));
|
|
11704
11983
|
}
|
|
11984
|
+
ctx.ui.setHiddenThinkingLabel("Thinking… (ctrl+t to show)");
|
|
11985
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
11986
|
+
render(width) {
|
|
11987
|
+
const active = ctx.model?.id;
|
|
11988
|
+
const name = active && opts.modelNameFor?.(active) || opts.modelName;
|
|
11989
|
+
return headerLines(theme, name, width, CLI_VERSION, opts.updateLatest);
|
|
11990
|
+
},
|
|
11991
|
+
invalidate() {}
|
|
11992
|
+
}));
|
|
11705
11993
|
});
|
|
11706
|
-
pi.on("agent_before_settle", (event, ctx) => isFusion(ctx.model) && event.outcome === "completed" && !alreadyContinued(ctx) && plannerStalled(branchMessages(ctx), ids.planner) ? STALL_CONTINUATION : undefined);
|
|
11707
|
-
for (const id of [FUSION_PICKER_ID, FUSION_ID]) {
|
|
11708
|
-
pi.registerVirtualModel({
|
|
11709
|
-
provider: PROVIDER_NAME,
|
|
11710
|
-
id,
|
|
11711
|
-
name,
|
|
11712
|
-
input: ["text"],
|
|
11713
|
-
route: (request, ctx) => routeFusion(request, {
|
|
11714
|
-
planner: ctx.modelRegistry.find(PROVIDER_NAME, ids.planner),
|
|
11715
|
-
executor: ctx.modelRegistry.find(PROVIDER_NAME, ids.executor),
|
|
11716
|
-
defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
|
|
11717
|
-
})
|
|
11718
|
-
});
|
|
11719
|
-
}
|
|
11720
11994
|
}
|
|
11721
11995
|
};
|
|
11722
11996
|
}
|
|
@@ -11737,20 +12011,514 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
|
|
|
11737
12011
|
// src/hive.ts
|
|
11738
12012
|
import { createHash as createHash3 } from "node:crypto";
|
|
11739
12013
|
import { execFileSync } from "node:child_process";
|
|
11740
|
-
import { lstatSync as lstatSync2, readFileSync as
|
|
12014
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync5, readlinkSync as readlinkSync2 } from "node:fs";
|
|
11741
12015
|
import { join as join9 } from "node:path";
|
|
11742
12016
|
|
|
11743
12017
|
// src/hive-guard.ts
|
|
11744
12018
|
import { existsSync as existsSync4, lstatSync, readdirSync, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
|
|
11745
|
-
import { homedir as
|
|
11746
|
-
import { basename as basename3, dirname as
|
|
12019
|
+
import { homedir as homedir5 } from "node:os";
|
|
12020
|
+
import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
|
|
11747
12021
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
12022
|
+
|
|
12023
|
+
// src/subagents.ts
|
|
12024
|
+
import {
|
|
12025
|
+
createAgentSessionFromServices,
|
|
12026
|
+
createAgentSessionServices,
|
|
12027
|
+
SessionManager,
|
|
12028
|
+
SettingsManager
|
|
12029
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12030
|
+
var SUBAGENT_TOOL = "subagent";
|
|
12031
|
+
var SEND_MESSAGE_TOOL = "send_message";
|
|
12032
|
+
var SUBMIT_PLAN_TOOL = "submit_plan";
|
|
12033
|
+
var MAX_PARALLEL = 8;
|
|
12034
|
+
var PARTICLE_ID = "particle";
|
|
12035
|
+
var REFUSAL_FALLBACK = "Subagents run on the free Particle model, or with credits. Switch with /model or run /buy-credits.";
|
|
12036
|
+
function subagentModelId(agent, parentModelId, ids) {
|
|
12037
|
+
if (parentModelId === PARTICLE_ID)
|
|
12038
|
+
return PARTICLE_ID;
|
|
12039
|
+
if (agent === "planner")
|
|
12040
|
+
return ids.planner;
|
|
12041
|
+
return parentModelId === FUSION_ID || parentModelId === FUSION_PICKER_ID ? ids.executor : parentModelId;
|
|
12042
|
+
}
|
|
12043
|
+
function subagentsAvailable(modelId, hasCredit) {
|
|
12044
|
+
return modelId === PARTICLE_ID || hasCredit;
|
|
12045
|
+
}
|
|
12046
|
+
function subagentConcurrency(modelId) {
|
|
12047
|
+
return modelId === PARTICLE_ID ? 4 : 1;
|
|
12048
|
+
}
|
|
12049
|
+
function substitutePrevious(task, previous) {
|
|
12050
|
+
return task.split("{previous}").join(previous);
|
|
12051
|
+
}
|
|
12052
|
+
function parsePlan(input) {
|
|
12053
|
+
const tasks = input?.tasks;
|
|
12054
|
+
if (!Array.isArray(tasks) || tasks.length === 0)
|
|
12055
|
+
return { ok: false, error: "submit_plan needs a non-empty tasks array" };
|
|
12056
|
+
if (tasks.length > MAX_PARALLEL)
|
|
12057
|
+
return { ok: false, error: `A plan has at most ${MAX_PARALLEL} tasks; merge some (got ${tasks.length})` };
|
|
12058
|
+
const out = [];
|
|
12059
|
+
for (const [i, t] of tasks.entries()) {
|
|
12060
|
+
const { title, task } = t ?? {};
|
|
12061
|
+
if (typeof title !== "string" || !title.trim() || typeof task !== "string" || !task.trim()) {
|
|
12062
|
+
return { ok: false, error: `Task ${i + 1} needs a non-empty title and task` };
|
|
12063
|
+
}
|
|
12064
|
+
out.push({ title: title.trim(), task: task.trim() });
|
|
12065
|
+
}
|
|
12066
|
+
return { ok: true, tasks: out };
|
|
12067
|
+
}
|
|
12068
|
+
var READ_ONLY = ["read", "grep", "find", "ls"];
|
|
12069
|
+
var AGENTS = {
|
|
12070
|
+
scout: {
|
|
12071
|
+
tools: [...READ_ONLY, "bash"],
|
|
12072
|
+
prompt: `You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything. Your output goes to an agent who has NOT seen the files you explored.
|
|
12073
|
+
Bash is for read-only commands only. Do NOT modify files.
|
|
12074
|
+
Strategy: grep/find to locate relevant code; read key sections (not whole files); note types, key functions, and how pieces connect.
|
|
12075
|
+
Output format:
|
|
12076
|
+
## Files Retrieved
|
|
12077
|
+
1. \`path/to/file.ts\` (lines 10-50) - what is here
|
|
12078
|
+
## Key Code
|
|
12079
|
+
The critical types, interfaces, or functions, quoted from the files.
|
|
12080
|
+
## Architecture
|
|
12081
|
+
How the pieces connect, briefly.
|
|
12082
|
+
## Start Here
|
|
12083
|
+
Which file to look at first and why.`
|
|
12084
|
+
},
|
|
12085
|
+
planner: {
|
|
12086
|
+
tools: [...READ_ONLY],
|
|
12087
|
+
prompt: `You are a planning specialist. You receive context and requirements, then produce a clear implementation plan. You must NOT make any changes. Only read, analyze, and plan.
|
|
12088
|
+
Output format:
|
|
12089
|
+
## Goal
|
|
12090
|
+
One sentence.
|
|
12091
|
+
## Plan
|
|
12092
|
+
Numbered steps, each small and actionable, naming the file or function to change.
|
|
12093
|
+
## Files to Modify
|
|
12094
|
+
## New Files (if any)
|
|
12095
|
+
## Risks
|
|
12096
|
+
Keep the plan concrete: a worker agent will execute it verbatim.`
|
|
12097
|
+
},
|
|
12098
|
+
worker: {
|
|
12099
|
+
tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
12100
|
+
prompt: `You are a worker agent with full capabilities, working in an isolated context window on a task delegated by the lead. Work autonomously to complete it.
|
|
12101
|
+
Output format when finished:
|
|
12102
|
+
## Completed
|
|
12103
|
+
What was done.
|
|
12104
|
+
## Files Changed
|
|
12105
|
+
- \`path/to/file.ts\` - what changed
|
|
12106
|
+
## Notes (if any)
|
|
12107
|
+
Anything the lead should know.`
|
|
12108
|
+
},
|
|
12109
|
+
reviewer: {
|
|
12110
|
+
tools: [...READ_ONLY, "bash"],
|
|
12111
|
+
prompt: `You are a senior code reviewer. Analyze code for quality, security, and maintainability.
|
|
12112
|
+
Bash is for read-only commands only: \`git diff\`, \`git log\`, \`git show\`. Do NOT modify files or run builds. Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
|
|
12113
|
+
Strategy: run \`git diff\` to see recent changes (if applicable), read the modified files, check for bugs, security issues, and code smells.
|
|
12114
|
+
Output format:
|
|
12115
|
+
## Files Reviewed
|
|
12116
|
+
## Critical (must fix)
|
|
12117
|
+
- \`file.ts:42\` - issue
|
|
12118
|
+
## Warnings (should fix)
|
|
12119
|
+
## Suggestions (consider)
|
|
12120
|
+
## Summary
|
|
12121
|
+
Overall assessment in 2-3 sentences. Be specific with file paths and line numbers.`
|
|
12122
|
+
}
|
|
12123
|
+
};
|
|
12124
|
+
var isAgent = (name) => typeof name === "string" && Object.hasOwn(AGENTS, name);
|
|
12125
|
+
var PLANNER_WORKFLOW = `Your plan will be carried out by parallel worker agents. When your exploration is done, call ${SUBMIT_PLAN_TOOL} ONCE with 1 to ${MAX_PARALLEL} independent tasks (title, task). Each task must be self-contained (name files and exact changes) and must not depend on another task's result. Calling ${SUBMIT_PLAN_TOOL} is how you finish; do not write the plan as text instead.`;
|
|
12126
|
+
var textResult = (text) => ({ content: [{ type: "text", text }], details: {} });
|
|
12127
|
+
function lastAssistant(session) {
|
|
12128
|
+
for (let i = session.messages.length - 1;i >= 0; i--) {
|
|
12129
|
+
const m = session.messages[i];
|
|
12130
|
+
if (m.role !== "assistant")
|
|
12131
|
+
continue;
|
|
12132
|
+
const text = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
|
|
12133
|
+
`).trim() : "";
|
|
12134
|
+
if (m.stopReason === "error")
|
|
12135
|
+
return { text, error: m.errorMessage || "the model request failed" };
|
|
12136
|
+
if (m.stopReason === "aborted")
|
|
12137
|
+
return { text, error: "aborted" };
|
|
12138
|
+
return { text };
|
|
12139
|
+
}
|
|
12140
|
+
return { text: "" };
|
|
12141
|
+
}
|
|
12142
|
+
function refusalMessage2(error) {
|
|
12143
|
+
if (!error.includes("subagents_unavailable"))
|
|
12144
|
+
return;
|
|
12145
|
+
const m = error.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
12146
|
+
try {
|
|
12147
|
+
return m ? JSON.parse(`"${m[1]}"`) : REFUSAL_FALLBACK;
|
|
12148
|
+
} catch {
|
|
12149
|
+
return REFUSAL_FALLBACK;
|
|
12150
|
+
}
|
|
12151
|
+
}
|
|
12152
|
+
function formatRun(r) {
|
|
12153
|
+
return `## ${r.name} (${r.agent})${r.error ? " - failed" : ""}
|
|
12154
|
+
${r.error ? `${r.error}${r.text ? `
|
|
12155
|
+
${r.text}` : ""}` : r.text || "(no output)"}`;
|
|
12156
|
+
}
|
|
12157
|
+
function createSubagentsExtension(opts) {
|
|
12158
|
+
return {
|
|
12159
|
+
name: "free-pi-subagents",
|
|
12160
|
+
factory: (pi) => {
|
|
12161
|
+
const subs = new Map;
|
|
12162
|
+
const leadInbox = [];
|
|
12163
|
+
let counter = 0;
|
|
12164
|
+
let parentModelId;
|
|
12165
|
+
let active = 0;
|
|
12166
|
+
let waiters = [];
|
|
12167
|
+
const acquire = async () => {
|
|
12168
|
+
while (active >= subagentConcurrency(parentModelId))
|
|
12169
|
+
await new Promise((r) => waiters.push(r));
|
|
12170
|
+
active++;
|
|
12171
|
+
};
|
|
12172
|
+
const release = () => {
|
|
12173
|
+
active--;
|
|
12174
|
+
const w = waiters;
|
|
12175
|
+
waiters = [];
|
|
12176
|
+
for (const r of w)
|
|
12177
|
+
r();
|
|
12178
|
+
};
|
|
12179
|
+
const alloc = (agent) => {
|
|
12180
|
+
const n = ++counter;
|
|
12181
|
+
return { n, name: `${agent}-${n}` };
|
|
12182
|
+
};
|
|
12183
|
+
const syncTools = (model) => {
|
|
12184
|
+
parentModelId = model?.id;
|
|
12185
|
+
const on = subagentsAvailable(model?.id, opts.hasCredit());
|
|
12186
|
+
const active = pi.getActiveTools();
|
|
12187
|
+
if (on === active.includes(SUBAGENT_TOOL))
|
|
12188
|
+
return;
|
|
12189
|
+
const names = [SUBAGENT_TOOL, SEND_MESSAGE_TOOL];
|
|
12190
|
+
pi.setActiveTools(on ? [...active, ...names.filter((n) => !active.includes(n))] : active.filter((n) => !names.includes(n)));
|
|
12191
|
+
};
|
|
12192
|
+
pi.on("session_start", (_e, ctx) => syncTools(ctx.model));
|
|
12193
|
+
pi.on("model_select", (e) => syncTools(e.model));
|
|
12194
|
+
pi.on("before_agent_start", (_e, ctx) => syncTools(ctx.model));
|
|
12195
|
+
pi.on("session_shutdown", () => {
|
|
12196
|
+
for (const s of subs.values())
|
|
12197
|
+
s.session.dispose();
|
|
12198
|
+
subs.clear();
|
|
12199
|
+
});
|
|
12200
|
+
const sendParams = _Object_({
|
|
12201
|
+
to: String2({ description: 'A subagent name such as "worker-2", or "lead"' }),
|
|
12202
|
+
message: String2({ description: "The message" })
|
|
12203
|
+
});
|
|
12204
|
+
const sendMessageTool = (from) => ({
|
|
12205
|
+
name: SEND_MESSAGE_TOOL,
|
|
12206
|
+
label: "Send message",
|
|
12207
|
+
description: 'Send a short message to another subagent by name, or to "lead". A running agent receives it after its current turn. A finished agent cannot; the lead can resume it by messaging it.',
|
|
12208
|
+
promptSnippet: "Send a message to a subagent or the lead",
|
|
12209
|
+
exposure: "model-only",
|
|
12210
|
+
parameters: sendParams,
|
|
12211
|
+
async execute(_id, p, signal) {
|
|
12212
|
+
return textResult(await deliver(from, p.to, p.message, { signal }));
|
|
12213
|
+
}
|
|
12214
|
+
});
|
|
12215
|
+
const createSub = async (agent, alloced, siblings, env, workflowPlan) => {
|
|
12216
|
+
const def = AGENTS[agent];
|
|
12217
|
+
const subSessionId = `${opts.sessionId}~sub-${alloced.n}`;
|
|
12218
|
+
const modelId = subagentModelId(agent, env.model.id, opts.fusionIds);
|
|
12219
|
+
const provider = modelId === env.model.id ? env.model.provider : PROVIDER_NAME;
|
|
12220
|
+
const settingsManager = SettingsManager.inMemory({
|
|
12221
|
+
defaultProvider: provider,
|
|
12222
|
+
defaultModel: modelId,
|
|
12223
|
+
defaultTools: [...def.tools],
|
|
12224
|
+
packages: [],
|
|
12225
|
+
quietStartup: true,
|
|
12226
|
+
defaultThinkingLevel: "off",
|
|
12227
|
+
enableInstallTelemetry: false,
|
|
12228
|
+
cacheWarming: "off"
|
|
12229
|
+
}, { projectTrusted: env.trusted });
|
|
12230
|
+
const others = siblings.filter((s) => s !== alloced.name);
|
|
12231
|
+
const prompt = [
|
|
12232
|
+
`You are ${alloced.name}, a ${agent} subagent working for the lead agent.`,
|
|
12233
|
+
def.prompt,
|
|
12234
|
+
workflowPlan ? PLANNER_WORKFLOW : "",
|
|
12235
|
+
others.length > 0 ? `Other subagents running alongside you: ${others.join(", ")}.` : "",
|
|
12236
|
+
`Use ${SEND_MESSAGE_TOOL} ({to, message}) to send a short note to a sibling by name or to "lead", only when it helps (a finding another agent needs, a conflict). A sibling gets it after its current turn. Your final reply goes to the lead automatically.`
|
|
12237
|
+
].filter(Boolean).join(`
|
|
12238
|
+
|
|
12239
|
+
`);
|
|
12240
|
+
const services = await createAgentSessionServices({
|
|
12241
|
+
cwd: env.cwd,
|
|
12242
|
+
agentDir: opts.agentDir,
|
|
12243
|
+
settingsManager,
|
|
12244
|
+
resourceLoaderOptions: {
|
|
12245
|
+
extensionFactories: opts.extensionsFor(subSessionId),
|
|
12246
|
+
noExtensions: true,
|
|
12247
|
+
noSkills: true,
|
|
12248
|
+
noPromptTemplates: true,
|
|
12249
|
+
noThemes: true,
|
|
12250
|
+
appendSystemPrompt: [prompt]
|
|
12251
|
+
}
|
|
12252
|
+
});
|
|
12253
|
+
const customTools = [sendMessageTool(alloced.name)];
|
|
12254
|
+
if (workflowPlan) {
|
|
12255
|
+
customTools.push({
|
|
12256
|
+
name: SUBMIT_PLAN_TOOL,
|
|
12257
|
+
label: "Submit plan",
|
|
12258
|
+
description: `Submit the plan as 1 to ${MAX_PARALLEL} independent tasks for parallel workers. Call once, when done.`,
|
|
12259
|
+
parameters: _Object_({
|
|
12260
|
+
tasks: _Array_(_Object_({ title: String2(), task: String2() }), { minItems: 1, maxItems: MAX_PARALLEL })
|
|
12261
|
+
}),
|
|
12262
|
+
async execute(_id, p) {
|
|
12263
|
+
const r = parsePlan(p);
|
|
12264
|
+
if (!r.ok)
|
|
12265
|
+
throw new Error(r.error);
|
|
12266
|
+
workflowPlan(r.tasks);
|
|
12267
|
+
return textResult(`Plan recorded (${r.tasks.length} tasks). Finish with a one-line summary.`);
|
|
12268
|
+
}
|
|
12269
|
+
});
|
|
12270
|
+
}
|
|
12271
|
+
const { session } = await createAgentSessionFromServices({
|
|
12272
|
+
services,
|
|
12273
|
+
sessionManager: SessionManager.inMemory(env.cwd),
|
|
12274
|
+
tools: [...def.tools, SEND_MESSAGE_TOOL, ...workflowPlan ? [SUBMIT_PLAN_TOOL] : []],
|
|
12275
|
+
customTools
|
|
12276
|
+
});
|
|
12277
|
+
await session.bindExtensions({});
|
|
12278
|
+
const sub = { name: alloced.name, agent, session, modelId, running: false, started: false, pending: [], last: "starting" };
|
|
12279
|
+
session.subscribe((e) => {
|
|
12280
|
+
if (e.type !== "tool_execution_start")
|
|
12281
|
+
return;
|
|
12282
|
+
const arg = e.args?.command ?? e.args?.path ?? e.args?.file_path ?? e.args?.pattern ?? "";
|
|
12283
|
+
sub.last = `${e.toolName}${arg ? ` ${String(arg).slice(0, 60)}` : ""}`;
|
|
12284
|
+
sub.progress?.();
|
|
12285
|
+
});
|
|
12286
|
+
subs.set(sub.name, sub);
|
|
12287
|
+
return sub;
|
|
12288
|
+
};
|
|
12289
|
+
const runPrompt = async (sub, text, ctx) => {
|
|
12290
|
+
const base = { name: sub.name, agent: sub.agent };
|
|
12291
|
+
if (ctx.signal?.aborted)
|
|
12292
|
+
return { ...base, text: "", error: "aborted" };
|
|
12293
|
+
await acquire();
|
|
12294
|
+
try {
|
|
12295
|
+
return await runPromptHeld(sub, text, ctx);
|
|
12296
|
+
} finally {
|
|
12297
|
+
release();
|
|
12298
|
+
}
|
|
12299
|
+
};
|
|
12300
|
+
const runPromptHeld = async (sub, text, ctx) => {
|
|
12301
|
+
const base = { name: sub.name, agent: sub.agent };
|
|
12302
|
+
if (ctx.signal?.aborted)
|
|
12303
|
+
return { ...base, text: "", error: "aborted" };
|
|
12304
|
+
const onAbort = () => void sub.session.abort();
|
|
12305
|
+
ctx.signal?.addEventListener("abort", onAbort, { once: true });
|
|
12306
|
+
sub.progress = ctx.progress;
|
|
12307
|
+
sub.started = true;
|
|
12308
|
+
sub.running = true;
|
|
12309
|
+
sub.last = "thinking";
|
|
12310
|
+
try {
|
|
12311
|
+
const queued = sub.pending.splice(0);
|
|
12312
|
+
await sub.session.prompt(queued.length > 0 ? `${queued.join(`
|
|
12313
|
+
`)}
|
|
12314
|
+
|
|
12315
|
+
${text}` : text);
|
|
12316
|
+
} catch (e) {
|
|
12317
|
+
return { ...base, text: "", error: e instanceof Error ? e.message : String(e) };
|
|
12318
|
+
} finally {
|
|
12319
|
+
sub.running = false;
|
|
12320
|
+
sub.last = "done";
|
|
12321
|
+
sub.progress = undefined;
|
|
12322
|
+
ctx.signal?.removeEventListener("abort", onAbort);
|
|
12323
|
+
}
|
|
12324
|
+
return { ...base, ...lastAssistant(sub.session) };
|
|
12325
|
+
};
|
|
12326
|
+
const prepare = async (agent, alloced, siblings, env) => {
|
|
12327
|
+
try {
|
|
12328
|
+
return await createSub(agent, alloced, siblings, env);
|
|
12329
|
+
} catch (e) {
|
|
12330
|
+
return { name: alloced.name, agent, text: "", error: e instanceof Error ? e.message : String(e) };
|
|
12331
|
+
}
|
|
12332
|
+
};
|
|
12333
|
+
const runPrepared = (p, task, env) => ("session" in p) ? runPrompt(p, task, env) : Promise.resolve(p);
|
|
12334
|
+
const runNew = async (agent, alloced, task, siblings, env) => runPrepared(await prepare(agent, alloced, siblings, env), task, env);
|
|
12335
|
+
async function deliver(from, to, message, ctx) {
|
|
12336
|
+
if (to === from)
|
|
12337
|
+
return "You cannot message yourself.";
|
|
12338
|
+
if (to === "lead") {
|
|
12339
|
+
if (from === "lead")
|
|
12340
|
+
return "You are the lead.";
|
|
12341
|
+
leadInbox.push({ from, message });
|
|
12342
|
+
return "Delivered to lead. It appears in the lead's subagent result.";
|
|
12343
|
+
}
|
|
12344
|
+
const target = subs.get(to);
|
|
12345
|
+
if (!target)
|
|
12346
|
+
return `No subagent named ${to}. Known: ${[...subs.keys()].join(", ") || "none"}.`;
|
|
12347
|
+
const note = `Message from ${from}: ${message}`;
|
|
12348
|
+
if (target.running) {
|
|
12349
|
+
await target.session.steer(note);
|
|
12350
|
+
return `Delivered to ${to}. It will see it after its current turn.`;
|
|
12351
|
+
}
|
|
12352
|
+
if (!target.started) {
|
|
12353
|
+
target.pending.push(note);
|
|
12354
|
+
return `${to} has not started yet. Your message is queued and arrives with its task.`;
|
|
12355
|
+
}
|
|
12356
|
+
if (from !== "lead") {
|
|
12357
|
+
target.pending.push(note);
|
|
12358
|
+
return `${to} has finished, so it cannot read messages now. Your message is queued; the lead can resume ${to}.`;
|
|
12359
|
+
}
|
|
12360
|
+
const current = ctx.modelId ?? parentModelId;
|
|
12361
|
+
if (current && subagentModelId(target.agent, current, opts.fusionIds) !== target.modelId) {
|
|
12362
|
+
return `${to} ran on ${target.modelId}; start a new subagent on the current model.`;
|
|
12363
|
+
}
|
|
12364
|
+
return formatRun(await runPrompt(target, note, ctx));
|
|
12365
|
+
}
|
|
12366
|
+
const withInbox = (text) => {
|
|
12367
|
+
if (leadInbox.length === 0)
|
|
12368
|
+
return text;
|
|
12369
|
+
const lines = leadInbox.splice(0).map((m) => `- from ${m.from}: ${m.message}`);
|
|
12370
|
+
return `${text}
|
|
12371
|
+
|
|
12372
|
+
## Messages to lead
|
|
12373
|
+
${lines.join(`
|
|
12374
|
+
`)}`;
|
|
12375
|
+
};
|
|
12376
|
+
pi.registerTool({
|
|
12377
|
+
...sendMessageTool("lead"),
|
|
12378
|
+
async execute(_id, p, signal, _onUpdate, ctx) {
|
|
12379
|
+
if (ctx.model)
|
|
12380
|
+
parentModelId = ctx.model.id;
|
|
12381
|
+
return textResult(withInbox(await deliver("lead", p.to, p.message, { signal, modelId: ctx.model?.id })));
|
|
12382
|
+
}
|
|
12383
|
+
});
|
|
12384
|
+
async function runWorkflow(goal, env) {
|
|
12385
|
+
let plan;
|
|
12386
|
+
const plannerAlloc = alloc("planner");
|
|
12387
|
+
let plannerRun;
|
|
12388
|
+
try {
|
|
12389
|
+
const planner = await createSub("planner", plannerAlloc, [], env, (t) => plan = t);
|
|
12390
|
+
plannerRun = await runPrompt(planner, `Goal: ${goal}
|
|
12391
|
+
|
|
12392
|
+
Explore the project as needed, then call ${SUBMIT_PLAN_TOOL}.`, env);
|
|
12393
|
+
} catch (e) {
|
|
12394
|
+
plannerRun = { name: plannerAlloc.name, agent: "planner", text: "", error: e instanceof Error ? e.message : String(e) };
|
|
12395
|
+
}
|
|
12396
|
+
if (plannerRun.error)
|
|
12397
|
+
return { runs: [plannerRun], preface: "" };
|
|
12398
|
+
const tasks = plan;
|
|
12399
|
+
if (!tasks)
|
|
12400
|
+
return { runs: [{ ...plannerRun, error: `The planner finished without calling ${SUBMIT_PLAN_TOOL}.` }], preface: "" };
|
|
12401
|
+
const planText = tasks.map((t, i) => `${i + 1}. ${t.title}: ${t.task}`).join(`
|
|
12402
|
+
`);
|
|
12403
|
+
const allocs = tasks.map(() => alloc("worker"));
|
|
12404
|
+
const names = allocs.map((a) => a.name);
|
|
12405
|
+
const prepared = await Promise.all(allocs.map((a) => prepare("worker", a, names, env)));
|
|
12406
|
+
const workers = await Promise.all(tasks.map((t, i) => runPrepared(prepared[i], `Overall goal: ${goal}
|
|
12407
|
+
|
|
12408
|
+
Your part (${t.title}):
|
|
12409
|
+
${t.task}`, env)));
|
|
12410
|
+
const results = workers.map((w, i) => `### ${tasks[i].title} (${w.name})
|
|
12411
|
+
${w.error ?? w.text}`).join(`
|
|
12412
|
+
|
|
12413
|
+
`);
|
|
12414
|
+
const reviewer = await runNew("reviewer", alloc("reviewer"), `Review the work done toward this goal.
|
|
12415
|
+
|
|
12416
|
+
Goal: ${goal}
|
|
12417
|
+
|
|
12418
|
+
Plan:
|
|
12419
|
+
${planText}
|
|
12420
|
+
|
|
12421
|
+
Worker results:
|
|
12422
|
+
${results}
|
|
12423
|
+
|
|
12424
|
+
Check the changes against the goal and report problems.`, [], env);
|
|
12425
|
+
return { runs: [...workers, reviewer], preface: `## Plan (${plannerRun.name})
|
|
12426
|
+
${planText}
|
|
12427
|
+
|
|
12428
|
+
` };
|
|
12429
|
+
}
|
|
12430
|
+
const TaskItem = _Object_({
|
|
12431
|
+
agent: String2({ description: "scout | planner | worker | reviewer" }),
|
|
12432
|
+
task: String2({ description: "The task for that agent" })
|
|
12433
|
+
});
|
|
12434
|
+
pi.registerTool({
|
|
12435
|
+
name: SUBAGENT_TOOL,
|
|
12436
|
+
label: "Subagent",
|
|
12437
|
+
description: [
|
|
12438
|
+
"Delegate work to subagents, each with its own context window. Agents: scout (read-only recon), planner (read-only plan), worker (full tools), reviewer (read-only review). Use exactly one mode:",
|
|
12439
|
+
"- single: {agent, task}",
|
|
12440
|
+
`- parallel: {tasks: [{agent, task}]} (max ${MAX_PARALLEL}); independent tasks run concurrently`,
|
|
12441
|
+
"- chain: {chain: [{agent, task}]}; {previous} in a task is replaced by the prior step's final text",
|
|
12442
|
+
"- workflow: {workflow: {goal}}; a planner splits the goal into independent tasks, workers run them in parallel, a reviewer checks the result",
|
|
12443
|
+
"Subagents can message each other and the lead with send_message. You (the lead) can message a finished subagent by name to resume it."
|
|
12444
|
+
].join(`
|
|
12445
|
+
`),
|
|
12446
|
+
promptSnippet: "Delegate to scout/planner/worker/reviewer subagents (single, parallel, chain, workflow)",
|
|
12447
|
+
promptGuidelines: ["Use subagent to fan out independent research or edits, or to keep a large exploration out of your own context. Do small tasks yourself."],
|
|
12448
|
+
exposure: "model-only",
|
|
12449
|
+
parameters: _Object_({
|
|
12450
|
+
agent: Optional(String2({ description: "Single mode: agent name" })),
|
|
12451
|
+
task: Optional(String2({ description: "Single mode: the task" })),
|
|
12452
|
+
tasks: Optional(_Array_(TaskItem, { maxItems: MAX_PARALLEL, description: "Parallel mode" })),
|
|
12453
|
+
chain: Optional(_Array_(TaskItem, { maxItems: MAX_PARALLEL, description: "Chain mode" })),
|
|
12454
|
+
workflow: Optional(_Object_({ goal: String2() }, { description: "Workflow mode" }))
|
|
12455
|
+
}),
|
|
12456
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
12457
|
+
if (!ctx.model)
|
|
12458
|
+
throw new Error("No model selected.");
|
|
12459
|
+
const env = {
|
|
12460
|
+
cwd: ctx.cwd,
|
|
12461
|
+
trusted: ctx.isProjectTrusted(),
|
|
12462
|
+
model: { provider: ctx.model.provider, id: ctx.model.id },
|
|
12463
|
+
signal,
|
|
12464
|
+
progress: () => {
|
|
12465
|
+
const lines = [...subs.values()].filter((s) => s.running).map((s) => `${s.name}: ${s.last}`);
|
|
12466
|
+
onUpdate?.(textResult(lines.join(`
|
|
12467
|
+
`) || "subagents working"));
|
|
12468
|
+
}
|
|
12469
|
+
};
|
|
12470
|
+
parentModelId = ctx.model.id;
|
|
12471
|
+
const single = params.agent !== undefined || params.task !== undefined;
|
|
12472
|
+
if ([single, params.tasks, params.chain, params.workflow].filter(Boolean).length !== 1) {
|
|
12473
|
+
throw new Error("Use exactly one mode: {agent, task} | {tasks} | {chain} | {workflow}.");
|
|
12474
|
+
}
|
|
12475
|
+
const check = (items) => {
|
|
12476
|
+
for (const i of items)
|
|
12477
|
+
if (!isAgent(i.agent))
|
|
12478
|
+
throw new Error(`Unknown agent "${i.agent}". Use scout, planner, worker or reviewer.`);
|
|
12479
|
+
};
|
|
12480
|
+
let runs = [];
|
|
12481
|
+
let preface = "";
|
|
12482
|
+
if (single) {
|
|
12483
|
+
if (!isAgent(params.agent) || !params.task)
|
|
12484
|
+
throw new Error("Single mode needs a valid agent and a task.");
|
|
12485
|
+
runs = [await runNew(params.agent, alloc(params.agent), params.task, [], env)];
|
|
12486
|
+
} else if (params.tasks) {
|
|
12487
|
+
check(params.tasks);
|
|
12488
|
+
const allocs = params.tasks.map((t) => alloc(t.agent));
|
|
12489
|
+
const names = allocs.map((a) => a.name);
|
|
12490
|
+
const prepared = await Promise.all(params.tasks.map((t, i) => prepare(t.agent, allocs[i], names, env)));
|
|
12491
|
+
runs = await Promise.all(params.tasks.map((t, i) => runPrepared(prepared[i], t.task, env)));
|
|
12492
|
+
} else if (params.chain) {
|
|
12493
|
+
check(params.chain);
|
|
12494
|
+
let previous = "";
|
|
12495
|
+
for (const step of params.chain) {
|
|
12496
|
+
const r = await runNew(step.agent, alloc(step.agent), substitutePrevious(step.task, previous), [], env);
|
|
12497
|
+
runs.push(r);
|
|
12498
|
+
if (r.error)
|
|
12499
|
+
break;
|
|
12500
|
+
previous = r.text;
|
|
12501
|
+
}
|
|
12502
|
+
} else if (params.workflow) {
|
|
12503
|
+
({ runs, preface } = await runWorkflow(params.workflow.goal, env));
|
|
12504
|
+
}
|
|
12505
|
+
const refused = runs.map((r) => r.error ? refusalMessage2(r.error) : undefined).find(Boolean);
|
|
12506
|
+
return textResult(withInbox(refused ?? `${preface}${runs.map(formatRun).join(`
|
|
12507
|
+
|
|
12508
|
+
`)}`));
|
|
12509
|
+
}
|
|
12510
|
+
});
|
|
12511
|
+
}
|
|
12512
|
+
};
|
|
12513
|
+
}
|
|
12514
|
+
|
|
12515
|
+
// src/hive-guard.ts
|
|
11748
12516
|
var GUARD_INPUT_LIMIT = 1500;
|
|
11749
12517
|
var CLASSIFY_TIMEOUT_MS = 2500;
|
|
11750
12518
|
var SCAN_LIMIT = 20000;
|
|
11751
12519
|
var MCP_RESOURCE_TOOLS2 = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
|
|
11752
12520
|
var PATH_TOOLS = ["read", "write", "edit", "grep", "find", "ls"];
|
|
11753
|
-
var SAFE_TOOLS2 = [TODO_TOOL_NAME, USAGE_TOOL_NAME, DOCS_TOOL_NAME];
|
|
12521
|
+
var SAFE_TOOLS2 = [TODO_TOOL_NAME, USAGE_TOOL_NAME, DOCS_TOOL_NAME, ASK_USER_TOOL_NAME];
|
|
11754
12522
|
var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
|
|
11755
12523
|
var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
|
|
11756
12524
|
var CLASSIFIED = ["bash", "write", "edit"];
|
|
@@ -11778,7 +12546,7 @@ function protectedLinkTargets(root, limit) {
|
|
|
11778
12546
|
const note = (link) => {
|
|
11779
12547
|
let target;
|
|
11780
12548
|
try {
|
|
11781
|
-
target = resolve2(
|
|
12549
|
+
target = resolve2(dirname9(link), readlinkSync(link));
|
|
11782
12550
|
} catch {
|
|
11783
12551
|
return;
|
|
11784
12552
|
}
|
|
@@ -11874,7 +12642,7 @@ function realpathDeepest(p) {
|
|
|
11874
12642
|
return null;
|
|
11875
12643
|
} catch {}
|
|
11876
12644
|
}
|
|
11877
|
-
const parent =
|
|
12645
|
+
const parent = dirname9(current);
|
|
11878
12646
|
if (parent === current)
|
|
11879
12647
|
return null;
|
|
11880
12648
|
rest.push(basename3(current));
|
|
@@ -11988,7 +12756,7 @@ function createClassifier(opts) {
|
|
|
11988
12756
|
}
|
|
11989
12757
|
async function decideToolCall(event, opts) {
|
|
11990
12758
|
const { toolName: tool, input } = event;
|
|
11991
|
-
const home = opts.home ??
|
|
12759
|
+
const home = opts.home ?? homedir5();
|
|
11992
12760
|
const ask = (reason) => ({ action: "ask", reason, summary: summarize2(tool, input) });
|
|
11993
12761
|
if (event.parentToolCallId)
|
|
11994
12762
|
return ask("nested call made by another tool");
|
|
@@ -11997,6 +12765,8 @@ async function decideToolCall(event, opts) {
|
|
|
11997
12765
|
return deny("tool not permitted");
|
|
11998
12766
|
if (tool === BUY_TOOL_NAME)
|
|
11999
12767
|
return deny("purchases are never run for a guest");
|
|
12768
|
+
if (tool === SUBAGENT_TOOL || tool === SEND_MESSAGE_TOOL)
|
|
12769
|
+
return deny("subagents are never started for a guest");
|
|
12000
12770
|
const path = pathVerdict(tool, input, opts, home);
|
|
12001
12771
|
if (path && "deny" in path)
|
|
12002
12772
|
return deny(path.deny);
|
|
@@ -12055,7 +12825,7 @@ function fingerprint(path) {
|
|
|
12055
12825
|
return "";
|
|
12056
12826
|
if (st.size > HASH_MAX_BYTES)
|
|
12057
12827
|
return `${st.size}:${st.mtimeMs}`;
|
|
12058
|
-
return createHash3("sha256").update(
|
|
12828
|
+
return createHash3("sha256").update(readFileSync5(path)).digest("hex");
|
|
12059
12829
|
} catch {
|
|
12060
12830
|
return "";
|
|
12061
12831
|
}
|
|
@@ -12193,6 +12963,8 @@ function createHiveExtension(opts) {
|
|
|
12193
12963
|
const changes = startSnapshot && changedSummary(startSnapshot, { cwd: roomCwd, exec: opts.execGit ?? execFileSync });
|
|
12194
12964
|
startSnapshot = undefined;
|
|
12195
12965
|
ws = undefined;
|
|
12966
|
+
if (opts.roomState)
|
|
12967
|
+
opts.roomState.open = false;
|
|
12196
12968
|
roomUrl = undefined;
|
|
12197
12969
|
members = [];
|
|
12198
12970
|
driver = "host";
|
|
@@ -12228,6 +13000,8 @@ function createHiveExtension(opts) {
|
|
|
12228
13000
|
const Impl = opts.WebSocketImpl ?? WebSocket;
|
|
12229
13001
|
const socket = new Impl(wsUrl(opts.baseUrl));
|
|
12230
13002
|
ws = socket;
|
|
13003
|
+
if (opts.roomState)
|
|
13004
|
+
opts.roomState.open = true;
|
|
12231
13005
|
roomCwd = ctx.cwd;
|
|
12232
13006
|
revision = 0;
|
|
12233
13007
|
driver = "host";
|
|
@@ -12540,16 +13314,16 @@ import {
|
|
|
12540
13314
|
chmodSync,
|
|
12541
13315
|
existsSync as existsSync5,
|
|
12542
13316
|
lstatSync as lstatSync3,
|
|
12543
|
-
mkdirSync as
|
|
13317
|
+
mkdirSync as mkdirSync3,
|
|
12544
13318
|
mkdtempSync,
|
|
12545
13319
|
readdirSync as readdirSync2,
|
|
12546
13320
|
realpathSync as realpathSync4,
|
|
12547
13321
|
rmSync,
|
|
12548
13322
|
statSync as statSync3,
|
|
12549
|
-
writeFileSync as
|
|
13323
|
+
writeFileSync as writeFileSync3
|
|
12550
13324
|
} from "node:fs";
|
|
12551
|
-
import { homedir as
|
|
12552
|
-
import { dirname as
|
|
13325
|
+
import { homedir as homedir6, tmpdir } from "node:os";
|
|
13326
|
+
import { dirname as dirname10, join as join10 } from "node:path";
|
|
12553
13327
|
var SECRET_NAME_PATTERNS = [
|
|
12554
13328
|
"\\.env(\\..*)?",
|
|
12555
13329
|
"[^/]*\\.pem",
|
|
@@ -12799,7 +13573,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
|
|
|
12799
13573
|
var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
|
|
12800
13574
|
function ancestors(p) {
|
|
12801
13575
|
const out = [];
|
|
12802
|
-
for (let d =
|
|
13576
|
+
for (let d = dirname10(p);; d = dirname10(d)) {
|
|
12803
13577
|
out.push(d);
|
|
12804
13578
|
if (d === "/")
|
|
12805
13579
|
break;
|
|
@@ -12837,7 +13611,7 @@ function buildMacProfile(i) {
|
|
|
12837
13611
|
L.push(`(deny file-write* (literal ${sbString(f)}))`);
|
|
12838
13612
|
const guarded = new Set([i.project, join10(i.project, ".git"), ...i.nodeModules ?? []]);
|
|
12839
13613
|
for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join10(i.project, ".git")]) {
|
|
12840
|
-
for (let d =
|
|
13614
|
+
for (let d = dirname10(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname10(d))
|
|
12841
13615
|
guarded.add(d);
|
|
12842
13616
|
}
|
|
12843
13617
|
for (const d of [...guarded].sort())
|
|
@@ -12966,7 +13740,7 @@ for arg in sys.argv[3:]:
|
|
|
12966
13740
|
`;
|
|
12967
13741
|
function createShellSandbox(deps = {}) {
|
|
12968
13742
|
const platform = deps.platform ?? process.platform;
|
|
12969
|
-
const home = deps.home ??
|
|
13743
|
+
const home = deps.home ?? homedir6();
|
|
12970
13744
|
const exec = deps.exec ?? defaultExec;
|
|
12971
13745
|
const launcher = deps.launcher ?? { sandbox: platform === "darwin" ? "/usr/bin/sandbox-exec" : "/usr/bin/bwrap", shell: "/bin/bash" };
|
|
12972
13746
|
const stat = deps.launcherStat ?? statSync3;
|
|
@@ -13097,11 +13871,11 @@ function createShellSandbox(deps = {}) {
|
|
|
13097
13871
|
try {
|
|
13098
13872
|
scratch = mkdtempSync(join10(realpathSync4(tmpdir()), "freepi-hive-"));
|
|
13099
13873
|
const dir = scratch;
|
|
13100
|
-
|
|
13101
|
-
|
|
13874
|
+
mkdirSync3(join10(dir, "home"));
|
|
13875
|
+
mkdirSync3(join10(dir, "npm-cache"));
|
|
13102
13876
|
const emptyFile = join10(dir, "empty");
|
|
13103
13877
|
const marker = join10(dir, "call.fd");
|
|
13104
|
-
|
|
13878
|
+
writeFileSync3(emptyFile, "", { mode: 292 });
|
|
13105
13879
|
const roots = toolchainRoots(home);
|
|
13106
13880
|
const mac = platform === "darwin";
|
|
13107
13881
|
const host = mac ? macHost() : undefined;
|
|
@@ -13288,9 +14062,15 @@ function catalogModelsFor(opts) {
|
|
|
13288
14062
|
const id = opts.model || MODEL_ID;
|
|
13289
14063
|
return [{ id, name: id }];
|
|
13290
14064
|
}
|
|
14065
|
+
function pickerModelName(opts, id) {
|
|
14066
|
+
return [...catalogModelsFor(opts), ...defenseModelsFor(opts)].find((m) => m.id === id)?.name;
|
|
14067
|
+
}
|
|
13291
14068
|
function defenseModelsFor(opts) {
|
|
13292
14069
|
return splitCatalogModels(opts.models ?? []).defense;
|
|
13293
14070
|
}
|
|
14071
|
+
function freeCatalogModels(opts) {
|
|
14072
|
+
return (opts.models ?? []).filter((m) => m.name.includes(FREE_LABEL)).map((m) => ({ provider: m.section === "defense" ? DEFENSE_PROVIDER_NAME : PROVIDER_NAME, id: m.id }));
|
|
14073
|
+
}
|
|
13294
14074
|
var ALLOWED_TOOL_NAMES = [
|
|
13295
14075
|
...SAFE_TOOLS,
|
|
13296
14076
|
USAGE_TOOL_NAME,
|
|
@@ -13302,6 +14082,8 @@ var ALLOWED_TOOL_NAMES = [
|
|
|
13302
14082
|
VISION_TOOL_NAME,
|
|
13303
14083
|
TODO_TOOL_NAME,
|
|
13304
14084
|
FUSION_PLAN_TOOL,
|
|
14085
|
+
SUBAGENT_TOOL,
|
|
14086
|
+
SEND_MESSAGE_TOOL,
|
|
13305
14087
|
"codemode",
|
|
13306
14088
|
"tool_search"
|
|
13307
14089
|
];
|
|
@@ -13312,12 +14094,12 @@ var SDK_TOOLS = [
|
|
|
13312
14094
|
"list_mcp_resource_templates",
|
|
13313
14095
|
"read_mcp_resource"
|
|
13314
14096
|
];
|
|
13315
|
-
var SKILLS_DIR = resolve4(
|
|
14097
|
+
var SKILLS_DIR = resolve4(dirname11(fileURLToPath4(import.meta.url)), "..", "skills");
|
|
13316
14098
|
function resolveModelName(opts) {
|
|
13317
14099
|
return opts.models?.[0]?.name || opts.model || MODEL_ID;
|
|
13318
14100
|
}
|
|
13319
14101
|
function buildSettingsManager(opts, models, projectTrusted) {
|
|
13320
|
-
return
|
|
14102
|
+
return SettingsManager2.inMemory({
|
|
13321
14103
|
defaultProvider: PROVIDER_NAME,
|
|
13322
14104
|
defaultModel: models[0].id,
|
|
13323
14105
|
defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
|
|
@@ -13339,18 +14121,19 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13339
14121
|
const models = catalogModelsFor(opts);
|
|
13340
14122
|
const auth = opts.sessionAuth ?? createSessionAuth({ jwt: opts.jwt, guest: Boolean(opts.guest) });
|
|
13341
14123
|
opts.sessionAuth = auth;
|
|
14124
|
+
const registerProviders = (pi, sid, extraHeaders) => {
|
|
14125
|
+
pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sid, models, PROVIDER_NAME, extraHeaders));
|
|
14126
|
+
const defense = defenseModelsFor(opts);
|
|
14127
|
+
if (defense.length > 0) {
|
|
14128
|
+
pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sid, defense, DEFENSE_PROVIDER_NAME, extraHeaders));
|
|
14129
|
+
}
|
|
14130
|
+
pi.on("before_provider_headers", (event) => {
|
|
14131
|
+
event.headers.Authorization = `Bearer ${auth.getJwt()}`;
|
|
14132
|
+
});
|
|
14133
|
+
};
|
|
13342
14134
|
const providerExtension = {
|
|
13343
14135
|
name: "free-pi-provider",
|
|
13344
|
-
factory: (pi) =>
|
|
13345
|
-
pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, models));
|
|
13346
|
-
const defense = defenseModelsFor(opts);
|
|
13347
|
-
if (defense.length > 0) {
|
|
13348
|
-
pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, defense, DEFENSE_PROVIDER_NAME));
|
|
13349
|
-
}
|
|
13350
|
-
pi.on("before_provider_headers", (event) => {
|
|
13351
|
-
event.headers.Authorization = `Bearer ${auth.getJwt()}`;
|
|
13352
|
-
});
|
|
13353
|
-
}
|
|
14136
|
+
factory: (pi) => registerProviders(pi, sessionId)
|
|
13354
14137
|
};
|
|
13355
14138
|
let advertiserUrl;
|
|
13356
14139
|
const adsExtension = createAdsExtension({
|
|
@@ -13387,11 +14170,16 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13387
14170
|
baseUrl: opts.baseUrl,
|
|
13388
14171
|
getToken: () => auth.getJwt()
|
|
13389
14172
|
});
|
|
14173
|
+
const tty = opts.tty ?? { stdin: process.stdin.isTTY === true, stdout: process.stdout.isTTY === true };
|
|
13390
14174
|
const errorNoticeExtension = createErrorNoticeExtension({
|
|
14175
|
+
freeModels: () => freeCatalogModels(opts),
|
|
14176
|
+
terminal: tty.stdin && tty.stdout,
|
|
13391
14177
|
extraNotice: (env) => {
|
|
13392
14178
|
const session = opts.refillCard;
|
|
13393
14179
|
if (!session || typeof env.remaining_credits_today !== "number")
|
|
13394
14180
|
return;
|
|
14181
|
+
if (suppressCardOnAllowanceMismatch(session, env.allowance_credits))
|
|
14182
|
+
return;
|
|
13395
14183
|
const card = formatLowAllowanceCard(session, env.remaining_credits_today);
|
|
13396
14184
|
if (!card)
|
|
13397
14185
|
return;
|
|
@@ -13400,10 +14188,12 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13400
14188
|
return card;
|
|
13401
14189
|
}
|
|
13402
14190
|
});
|
|
14191
|
+
const choiceOn = choiceEnabled({ env: process.env, arm: opts.choiceArm, configPath: opts.choiceConfigPath ?? getConfigPath() });
|
|
14192
|
+
const allowedTools = choiceOn ? [...ALLOWED_TOOL_NAMES, ASK_USER_TOOL_NAME] : ALLOWED_TOOL_NAMES;
|
|
13403
14193
|
const toolGuardExtension = createToolGuardExtension({
|
|
13404
14194
|
baseUrl: opts.baseUrl,
|
|
13405
14195
|
getToken: () => auth.getJwt(),
|
|
13406
|
-
allowedTools
|
|
14196
|
+
allowedTools
|
|
13407
14197
|
});
|
|
13408
14198
|
const commandsExtension = createFreePiCommandsExtension({
|
|
13409
14199
|
baseUrl: opts.baseUrl,
|
|
@@ -13427,9 +14217,9 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13427
14217
|
const headerExtension = createHeaderExtension({
|
|
13428
14218
|
modelName: resolveModelName(opts),
|
|
13429
14219
|
updateLatest: opts.updateLatest,
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
modelNameFor: (id) =>
|
|
14220
|
+
startupNotes: opts.startupNotes,
|
|
14221
|
+
toolModePath: getToolModePath(opts.agentDir),
|
|
14222
|
+
modelNameFor: (id) => pickerModelName(opts, id)
|
|
13433
14223
|
});
|
|
13434
14224
|
const todoToolExtension = createTodoToolExtension();
|
|
13435
14225
|
const shellSandbox = opts.shellSandbox ?? createShellSandbox();
|
|
@@ -13438,11 +14228,36 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13438
14228
|
const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
|
|
13439
14229
|
const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
|
|
13440
14230
|
const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
|
|
14231
|
+
const hiveRoom = { open: false };
|
|
13441
14232
|
const hiveExtension = createHiveExtension({
|
|
13442
14233
|
baseUrl: opts.baseUrl,
|
|
13443
14234
|
getToken: () => auth.getJwt(),
|
|
13444
|
-
knownTools:
|
|
13445
|
-
sandbox: shellSandbox
|
|
14235
|
+
knownTools: allowedTools,
|
|
14236
|
+
sandbox: shellSandbox,
|
|
14237
|
+
roomState: hiveRoom
|
|
14238
|
+
});
|
|
14239
|
+
const choiceExtension = choiceOn ? createChoiceToolExtension({
|
|
14240
|
+
baseUrl: opts.baseUrl,
|
|
14241
|
+
getToken: () => auth.getJwt(),
|
|
14242
|
+
sessionId,
|
|
14243
|
+
arm: () => opts.choiceArm ?? null,
|
|
14244
|
+
headless: !(tty.stdin && tty.stdout),
|
|
14245
|
+
isRoomOpen: () => hiveRoom.open
|
|
14246
|
+
}) : undefined;
|
|
14247
|
+
const subagentsExtension = createSubagentsExtension({
|
|
14248
|
+
sessionId,
|
|
14249
|
+
agentDir: opts.agentDir,
|
|
14250
|
+
fusionIds: resolveFusionIds(models.map((m) => m.id), opts.fusion),
|
|
14251
|
+
hasCredit: () => (getAllowanceCache()?.boughtCredits ?? 0) > 0,
|
|
14252
|
+
extensionsFor: (subSessionId) => [
|
|
14253
|
+
{ name: "free-pi-subagent-provider", factory: (pi) => registerProviders(pi, subSessionId, { "x-freepi-subagent": "1" }) },
|
|
14254
|
+
createToolGuardExtension({
|
|
14255
|
+
baseUrl: opts.baseUrl,
|
|
14256
|
+
getToken: () => auth.getJwt(),
|
|
14257
|
+
allowedTools: [...SAFE_TOOLS, SEND_MESSAGE_TOOL, SUBMIT_PLAN_TOOL]
|
|
14258
|
+
}),
|
|
14259
|
+
createToolAliasesExtension({ sandbox: shellSandbox, shell })
|
|
14260
|
+
]
|
|
13446
14261
|
});
|
|
13447
14262
|
const extensionFactories = [
|
|
13448
14263
|
providerExtension,
|
|
@@ -13454,6 +14269,7 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13454
14269
|
webToolsExtension,
|
|
13455
14270
|
imageToolExtension,
|
|
13456
14271
|
visionToolExtension,
|
|
14272
|
+
...choiceExtension ? [choiceExtension] : [],
|
|
13457
14273
|
errorNoticeExtension,
|
|
13458
14274
|
createUpgradeNoticeExtension({ baseUrl: opts.baseUrl }),
|
|
13459
14275
|
createOutputLimitExtension(),
|
|
@@ -13467,6 +14283,7 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13467
14283
|
mcpExtension,
|
|
13468
14284
|
codemodeExtension,
|
|
13469
14285
|
toolSearchExtension,
|
|
14286
|
+
subagentsExtension,
|
|
13470
14287
|
hiveExtension,
|
|
13471
14288
|
...guestLoginExtension ? [guestLoginExtension] : [],
|
|
13472
14289
|
...opts.guest ? [createGuestNotesExtension({ isGuest: () => auth.isGuest() })] : []
|
|
@@ -13477,10 +14294,11 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13477
14294
|
resourceLoaderOptions: {
|
|
13478
14295
|
extensionFactories,
|
|
13479
14296
|
additionalSkillPaths: [SKILLS_DIR],
|
|
13480
|
-
appendSystemPromptOverride: (base) => [...base, agentRulesFor(opts.costArm)
|
|
14297
|
+
appendSystemPromptOverride: (base) => [...base, choiceOn ? `${agentRulesFor(opts.costArm)}
|
|
14298
|
+
${CHOICE_RULES}` : agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [shellPromptLine(shell)] : []]
|
|
13481
14299
|
},
|
|
13482
14300
|
extensionNames: extensionFactories.map((e) => e.name),
|
|
13483
|
-
tools: SDK_TOOLS
|
|
14301
|
+
tools: choiceOn ? [...SDK_TOOLS, ASK_USER_TOOL_NAME] : SDK_TOOLS
|
|
13484
14302
|
};
|
|
13485
14303
|
}
|
|
13486
14304
|
function getSessionDir(cwd, agentDir) {
|
|
@@ -13488,7 +14306,7 @@ function getSessionDir(cwd, agentDir) {
|
|
|
13488
14306
|
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
13489
14307
|
return join11(resolve4(agentDir), "sessions", safePath);
|
|
13490
14308
|
}
|
|
13491
|
-
async function findLatestSession(cwd, sessionDir, api =
|
|
14309
|
+
async function findLatestSession(cwd, sessionDir, api = SessionManager2) {
|
|
13492
14310
|
const infos = await api.list(cwd, sessionDir);
|
|
13493
14311
|
if (infos.length === 0)
|
|
13494
14312
|
return;
|
|
@@ -13501,7 +14319,7 @@ async function findLatestSession(cwd, sessionDir, api = SessionManager) {
|
|
|
13501
14319
|
firstMessage: latest.firstMessage ?? ""
|
|
13502
14320
|
};
|
|
13503
14321
|
}
|
|
13504
|
-
async function createSessionManager(cwd, sessionDir, sessionId, api =
|
|
14322
|
+
async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager2) {
|
|
13505
14323
|
if (sessionId) {
|
|
13506
14324
|
const infos = await api.list(cwd, sessionDir);
|
|
13507
14325
|
const info = infos.find((session) => session.id === sessionId);
|
|
@@ -13518,14 +14336,13 @@ patchResumeLine(process.stdout);
|
|
|
13518
14336
|
function pickerScopeFor(modelRuntime, opts) {
|
|
13519
14337
|
const main = resolveFreePiScope(modelRuntime, PROVIDER_NAME, catalogModelsFor(opts).map((m) => m.id));
|
|
13520
14338
|
const defense = resolveFreePiScope(modelRuntime, DEFENSE_PROVIDER_NAME, defenseModelsFor(opts).map((m) => m.id));
|
|
13521
|
-
const
|
|
13522
|
-
const
|
|
13523
|
-
const
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
return [...fusion, ...defense, ...rest];
|
|
14339
|
+
const all = [...main, ...defense];
|
|
14340
|
+
const nameOf = (s) => s.model.name ?? "";
|
|
14341
|
+
const idOf = (s) => s.model.id;
|
|
14342
|
+
const serverOrder = new Map((opts.models ?? []).map((m, i) => [m.id, i]));
|
|
14343
|
+
const free = all.filter((s) => nameOf(s).includes(FREE_LABEL)).sort((a, b) => (serverOrder.get(idOf(a)) ?? 0) - (serverOrder.get(idOf(b)) ?? 0));
|
|
14344
|
+
const others = all.filter((s) => !free.includes(s)).sort((a, b) => idOf(a).localeCompare(idOf(b)));
|
|
14345
|
+
return [...free, ...others];
|
|
13529
14346
|
}
|
|
13530
14347
|
function interactiveModeOptions(opts) {
|
|
13531
14348
|
return { initialImages: [], initialMessages: [], ...opts.initialMessage ? { initialMessage: opts.initialMessage } : {} };
|
|
@@ -13545,7 +14362,7 @@ async function launchPi(opts) {
|
|
|
13545
14362
|
sessionManager,
|
|
13546
14363
|
sessionStartEvent
|
|
13547
14364
|
}) => {
|
|
13548
|
-
const services = await
|
|
14365
|
+
const services = await createAgentSessionServices2({
|
|
13549
14366
|
cwd: rtCwd,
|
|
13550
14367
|
agentDir: opts.agentDir,
|
|
13551
14368
|
settingsManager: settingsForRuntime(opts, rtCwd, launchTrust),
|
|
@@ -13553,7 +14370,7 @@ async function launchPi(opts) {
|
|
|
13553
14370
|
});
|
|
13554
14371
|
const scopedModels = pickerScopeFor(services.modelRuntime, opts);
|
|
13555
14372
|
return {
|
|
13556
|
-
...await
|
|
14373
|
+
...await createAgentSessionFromServices2({
|
|
13557
14374
|
services,
|
|
13558
14375
|
sessionManager,
|
|
13559
14376
|
sessionStartEvent,
|
|
@@ -13588,7 +14405,15 @@ async function launchPi(opts) {
|
|
|
13588
14405
|
});
|
|
13589
14406
|
installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
|
|
13590
14407
|
installModelAllowanceHandler(mode);
|
|
13591
|
-
installFooterRightAlign(mode)
|
|
14408
|
+
installFooterRightAlign(mode, () => {
|
|
14409
|
+
const id = runtime.session?.model?.id;
|
|
14410
|
+
return freeModelBase(pickerModelName(opts, id));
|
|
14411
|
+
});
|
|
14412
|
+
installEditorPlaceholder(mode);
|
|
14413
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
14414
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
14415
|
+
installBottomPin(mode);
|
|
14416
|
+
}
|
|
13592
14417
|
await mode.run();
|
|
13593
14418
|
return sm.getSessionId();
|
|
13594
14419
|
}
|
|
@@ -13621,14 +14446,14 @@ async function promptConsentInteractive(text) {
|
|
|
13621
14446
|
|
|
13622
14447
|
// src/real-spawn.ts
|
|
13623
14448
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
13624
|
-
import { homedir as
|
|
14449
|
+
import { homedir as homedir7 } from "node:os";
|
|
13625
14450
|
function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawnSync2 }) {
|
|
13626
14451
|
return (cmd, args, opts) => {
|
|
13627
14452
|
const result = io.spawnSyncImpl(cmd, args, {
|
|
13628
14453
|
timeout: opts.timeout,
|
|
13629
14454
|
encoding: "utf8",
|
|
13630
14455
|
shell: io.platform === "win32",
|
|
13631
|
-
cwd:
|
|
14456
|
+
cwd: homedir7()
|
|
13632
14457
|
});
|
|
13633
14458
|
return { status: result.status, stdout: result.stdout ?? "", error: result.error };
|
|
13634
14459
|
};
|
|
@@ -13637,7 +14462,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
|
|
|
13637
14462
|
// src/relaunch.ts
|
|
13638
14463
|
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
|
|
13639
14464
|
import { constants as osConstants, tmpdir as tmpdir2 } from "node:os";
|
|
13640
|
-
import { delimiter, dirname as
|
|
14465
|
+
import { delimiter, dirname as dirname12, isAbsolute as isAbsolute3, join as join12, resolve as resolve5, sep as sep4, win32 } from "node:path";
|
|
13641
14466
|
function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
|
|
13642
14467
|
try {
|
|
13643
14468
|
return statSync4(p).isDirectory();
|
|
@@ -13678,7 +14503,7 @@ function hasNodeModulesBin(entry) {
|
|
|
13678
14503
|
}
|
|
13679
14504
|
function sanitizedEnv(env, nodeDir, project, home, win) {
|
|
13680
14505
|
const proj = canon(project, win);
|
|
13681
|
-
const broad = proj === canon(home, win) ||
|
|
14506
|
+
const broad = proj === canon(home, win) || dirname12(proj) === proj;
|
|
13682
14507
|
const out = {};
|
|
13683
14508
|
let pathKey = "PATH";
|
|
13684
14509
|
let oldPath = "";
|
|
@@ -13715,12 +14540,12 @@ async function relaunchViaNpx(latest, deps) {
|
|
|
13715
14540
|
if (!latest || !SEMVER.test(latest))
|
|
13716
14541
|
return;
|
|
13717
14542
|
const target = latest;
|
|
13718
|
-
const nodeDir =
|
|
13719
|
-
const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(
|
|
14543
|
+
const nodeDir = dirname12(deps.execPath);
|
|
14544
|
+
const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(dirname12(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
|
|
13720
14545
|
const exists = deps.exists ?? existsSync6;
|
|
13721
14546
|
const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
|
|
13722
14547
|
const healthy = (cli) => {
|
|
13723
|
-
const root = join12(
|
|
14548
|
+
const root = join12(dirname12(cli), "..");
|
|
13724
14549
|
return exists(join12(root, "package.json")) && exists(join12(root, "lib", "cli.js"));
|
|
13725
14550
|
};
|
|
13726
14551
|
let npxCliPath = npxCli;
|
|
@@ -13776,12 +14601,12 @@ async function relaunchViaNpx(latest, deps) {
|
|
|
13776
14601
|
|
|
13777
14602
|
// src/run.ts
|
|
13778
14603
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
13779
|
-
import { mkdirSync as
|
|
13780
|
-
import { SessionManager as
|
|
14604
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
14605
|
+
import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
|
|
13781
14606
|
|
|
13782
14607
|
// src/onboarding.ts
|
|
13783
14608
|
import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync as realpathSync6 } from "node:fs";
|
|
13784
|
-
import { homedir as
|
|
14609
|
+
import { homedir as homedir8 } from "node:os";
|
|
13785
14610
|
import { join as join13 } from "node:path";
|
|
13786
14611
|
var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
|
|
13787
14612
|
var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
|
|
@@ -13798,7 +14623,7 @@ ${INTRO_TEXT}
|
|
|
13798
14623
|
}
|
|
13799
14624
|
var FIRST_TASK_EXPERIMENT = "retain.first_task";
|
|
13800
14625
|
var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile", "composer.json"];
|
|
13801
|
-
function looksLikeProject(cwd, home =
|
|
14626
|
+
function looksLikeProject(cwd, home = homedir8()) {
|
|
13802
14627
|
try {
|
|
13803
14628
|
if (realpathSync6(cwd) === realpathSync6(home))
|
|
13804
14629
|
return false;
|
|
@@ -13820,7 +14645,7 @@ This folder doesn't look like a project. free-pi works best on code you
|
|
|
13820
14645
|
already have: quit, "cd" into a project, and run "npx free-pi-cli@latest" there.
|
|
13821
14646
|
Or just type: Create a small runnable hello-world app here and run it
|
|
13822
14647
|
`.trim();
|
|
13823
|
-
function showFirstTaskHint(log, cwd = process.cwd(), home =
|
|
14648
|
+
function showFirstTaskHint(log, cwd = process.cwd(), home = homedir8()) {
|
|
13824
14649
|
const text = looksLikeProject(cwd, home) ? FIRST_TASK_PROJECT_TEXT : FIRST_TASK_EMPTY_TEXT;
|
|
13825
14650
|
log(`
|
|
13826
14651
|
${text}
|
|
@@ -13830,7 +14655,7 @@ ${text}
|
|
|
13830
14655
|
|
|
13831
14656
|
// src/starter.ts
|
|
13832
14657
|
import { existsSync as existsSync8, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
|
|
13833
|
-
import { homedir as
|
|
14658
|
+
import { homedir as homedir9 } from "node:os";
|
|
13834
14659
|
import { join as join14 } from "node:path";
|
|
13835
14660
|
var STARTER_EXPERIMENT = "first_session_starter";
|
|
13836
14661
|
var STARTER_PROJECT = [
|
|
@@ -13869,7 +14694,7 @@ function getStarterMarkerPath(agentDir = getFreePiAgentDir()) {
|
|
|
13869
14694
|
function starterShownBefore(agentDir) {
|
|
13870
14695
|
return existsSync8(getStarterMarkerPath(agentDir));
|
|
13871
14696
|
}
|
|
13872
|
-
function starterChoices(cwd = process.cwd(), home =
|
|
14697
|
+
function starterChoices(cwd = process.cwd(), home = homedir9()) {
|
|
13873
14698
|
if (looksLikeProject(cwd, home))
|
|
13874
14699
|
return STARTER_PROJECT;
|
|
13875
14700
|
try {
|
|
@@ -13896,12 +14721,12 @@ function parseStarterKey(raw, count) {
|
|
|
13896
14721
|
}
|
|
13897
14722
|
|
|
13898
14723
|
// src/whats-new.ts
|
|
13899
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
13900
|
-
import { dirname as
|
|
14724
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
14725
|
+
import { dirname as dirname13 } from "node:path";
|
|
13901
14726
|
function maybeWhatsNew(cliVersion, agentDir) {
|
|
13902
14727
|
try {
|
|
13903
14728
|
const path = getLastSeenVersionPath(agentDir);
|
|
13904
|
-
const lastSeen = existsSync9(path) ?
|
|
14729
|
+
const lastSeen = existsSync9(path) ? readFileSync6(path, "utf-8").trim() : undefined;
|
|
13905
14730
|
writeLastSeen(path, cliVersion);
|
|
13906
14731
|
if (lastSeen === undefined)
|
|
13907
14732
|
return;
|
|
@@ -13917,8 +14742,8 @@ function maybeWhatsNew(cliVersion, agentDir) {
|
|
|
13917
14742
|
}
|
|
13918
14743
|
}
|
|
13919
14744
|
function writeLastSeen(path, version) {
|
|
13920
|
-
|
|
13921
|
-
|
|
14745
|
+
mkdirSync4(dirname13(path), { recursive: true });
|
|
14746
|
+
writeFileSync4(path, version, "utf-8");
|
|
13922
14747
|
}
|
|
13923
14748
|
|
|
13924
14749
|
// src/run.ts
|
|
@@ -14007,7 +14832,7 @@ var SIGNIN_UNFINISHED_LINE = "Sign-in didn't finish. Run npx free-pi-cli again w
|
|
|
14007
14832
|
var DECLINED_CALM_LINE = "OK, free-pi won't start. Run it again if you change your mind.";
|
|
14008
14833
|
function validateSessionId(value) {
|
|
14009
14834
|
try {
|
|
14010
|
-
|
|
14835
|
+
SessionManager3.inMemory(process.cwd(), { id: value });
|
|
14011
14836
|
return;
|
|
14012
14837
|
} catch (err) {
|
|
14013
14838
|
return `Invalid --session value: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -14058,6 +14883,13 @@ or: npm i -g free-pi-cli@latest`);
|
|
|
14058
14883
|
}
|
|
14059
14884
|
return outcome === "notice" ? { updateLatest: versionCheck.latest } : {};
|
|
14060
14885
|
}
|
|
14886
|
+
function guestTrialNote(variant) {
|
|
14887
|
+
return isHandoffTreatment(variant) ? `
|
|
14888
|
+
You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
|
|
14889
|
+
` : `
|
|
14890
|
+
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.
|
|
14891
|
+
`;
|
|
14892
|
+
}
|
|
14061
14893
|
async function run(deps, argv = process.argv) {
|
|
14062
14894
|
const resumeFlags = parseResumeFlags(argv);
|
|
14063
14895
|
if (resumeFlags.sessionError) {
|
|
@@ -14095,6 +14927,12 @@ ${FINISH_SIGNIN_LINE}
|
|
|
14095
14927
|
let startedGuestThisRun = false;
|
|
14096
14928
|
let signedInAfterExpiry = false;
|
|
14097
14929
|
let guestHandoffVariant;
|
|
14930
|
+
const startupNotes = [];
|
|
14931
|
+
const note = (text) => void startupNotes.push(text);
|
|
14932
|
+
const flushNotes = () => {
|
|
14933
|
+
for (const n of startupNotes.splice(0))
|
|
14934
|
+
deps.log(n);
|
|
14935
|
+
};
|
|
14098
14936
|
let showFirstTask = false;
|
|
14099
14937
|
let consentCurrent = false;
|
|
14100
14938
|
if (jwt) {
|
|
@@ -14198,13 +15036,9 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
|
|
|
14198
15036
|
jwt = guest.token;
|
|
14199
15037
|
startedGuestThisRun = true;
|
|
14200
15038
|
showIntro(deps.log);
|
|
14201
|
-
|
|
14202
|
-
You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
|
|
14203
|
-
` : `
|
|
14204
|
-
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.
|
|
14205
|
-
`);
|
|
15039
|
+
note(guestTrialNote(guest.guestHandoffVariant));
|
|
14206
15040
|
if (guest.claimCode && guest.githubBonusCredits) {
|
|
14207
|
-
|
|
15041
|
+
note(emphasize(formatGuestBonusBox(guest.claimCode, guest.githubBonusCredits).join(`
|
|
14208
15042
|
`)) + `
|
|
14209
15043
|
`);
|
|
14210
15044
|
}
|
|
@@ -14238,14 +15072,15 @@ ${GUEST_REFUSAL_LINE}`);
|
|
|
14238
15072
|
}
|
|
14239
15073
|
const whatsNew = maybeWhatsNew(CLI_VERSION, deps.agentDir);
|
|
14240
15074
|
if (whatsNew)
|
|
14241
|
-
|
|
15075
|
+
note(whatsNew);
|
|
14242
15076
|
if (versionCheck.notice)
|
|
14243
|
-
|
|
15077
|
+
note(versionCheck.notice);
|
|
14244
15078
|
let me = await fetchMe2(deps.baseUrl, jwt).catch(() => null);
|
|
14245
15079
|
if (me?.guest && !startedGuestThisRun && canPrompt && consentCurrent) {
|
|
14246
15080
|
try {
|
|
14247
15081
|
const choice = await (deps.promptGuestSignin ?? promptGuestSigninInteractive)();
|
|
14248
15082
|
if (choice === "aborted") {
|
|
15083
|
+
flushNotes();
|
|
14249
15084
|
deps.log("Aborted.");
|
|
14250
15085
|
return 1;
|
|
14251
15086
|
}
|
|
@@ -14275,8 +15110,10 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
|
|
|
14275
15110
|
if (me)
|
|
14276
15111
|
updateAllowanceCache(me);
|
|
14277
15112
|
guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
|
|
15113
|
+
if (me?.guest && !startedGuestThisRun)
|
|
15114
|
+
note(guestTrialNote(guestHandoffVariant));
|
|
14278
15115
|
if (showFirstTask) {
|
|
14279
|
-
showFirstTaskHint(
|
|
15116
|
+
showFirstTaskHint(note);
|
|
14280
15117
|
reportResumeExposure(deps, jwt, "offer_shown", FIRST_TASK_EXPERIMENT);
|
|
14281
15118
|
}
|
|
14282
15119
|
const startRefill = await fetchRefillStats(deps.baseUrl, jwt).catch(() => {
|
|
@@ -14285,14 +15122,14 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
|
|
|
14285
15122
|
const refillSession = sessionFromStats(startRefill);
|
|
14286
15123
|
const creditsBack = startRefill ? formatCreditsBackNudge(startRefill) : undefined;
|
|
14287
15124
|
if (creditsBack) {
|
|
14288
|
-
|
|
15125
|
+
note(`
|
|
14289
15126
|
${creditsBack}`);
|
|
14290
15127
|
reportRefillCardShown({ baseUrl: deps.baseUrl, jwt });
|
|
14291
15128
|
}
|
|
14292
15129
|
const redeemFlag = parseRedeemFlag(argv);
|
|
14293
15130
|
if (redeemFlag.requested) {
|
|
14294
15131
|
const r = await redeemCode({ baseUrl: deps.baseUrl, getToken: () => jwt, isGuest: () => Boolean(me?.guest) }, redeemFlag.code ?? "");
|
|
14295
|
-
|
|
15132
|
+
note(`
|
|
14296
15133
|
${r.text}
|
|
14297
15134
|
`);
|
|
14298
15135
|
}
|
|
@@ -14302,7 +15139,7 @@ ${r.text}
|
|
|
14302
15139
|
if (!session && resumeFlags.continueLast && !resumeFlags.startNew) {
|
|
14303
15140
|
session = (await lookupLatest(process.cwd(), deps.agentDir))?.id;
|
|
14304
15141
|
if (!session)
|
|
14305
|
-
|
|
15142
|
+
note("No earlier session in this folder; starting a new one.");
|
|
14306
15143
|
}
|
|
14307
15144
|
const experiment = versionCheck.experiments?.[RESUME_ON_LAUNCH_EXPERIMENT];
|
|
14308
15145
|
const variant = experiment?.variant === "treatment" ? "treatment" : "control";
|
|
@@ -14314,6 +15151,7 @@ ${r.text}
|
|
|
14314
15151
|
reportResumeExposure(deps, jwt, "offer_shown");
|
|
14315
15152
|
const answer = await (deps.promptResume ?? promptResumeInteractive)(latest);
|
|
14316
15153
|
if (answer === "aborted") {
|
|
15154
|
+
flushNotes();
|
|
14317
15155
|
deps.log("Aborted.");
|
|
14318
15156
|
return 1;
|
|
14319
15157
|
}
|
|
@@ -14325,11 +15163,12 @@ ${r.text}
|
|
|
14325
15163
|
}
|
|
14326
15164
|
let initialMessage;
|
|
14327
15165
|
let hintShown = false;
|
|
15166
|
+
let importHintNote;
|
|
14328
15167
|
if (importFlag.requested && deps.prepareImport) {
|
|
14329
15168
|
if (resumeFlags.session || resumeFlags.continueLast) {
|
|
14330
|
-
|
|
15169
|
+
note("--import starts a new session, so it is ignored together with --session or --continue.");
|
|
14331
15170
|
} else if (!interactive) {
|
|
14332
|
-
|
|
15171
|
+
note("--import needs an interactive terminal. Starting normally.");
|
|
14333
15172
|
} else {
|
|
14334
15173
|
initialMessage = await deps.prepareImport({ source: importFlag.source, jwt, model: versionCheck.model }).catch(() => {
|
|
14335
15174
|
return;
|
|
@@ -14343,7 +15182,7 @@ ${r.text}
|
|
|
14343
15182
|
return;
|
|
14344
15183
|
});
|
|
14345
15184
|
if (hint) {
|
|
14346
|
-
|
|
15185
|
+
importHintNote = hint;
|
|
14347
15186
|
hintShown = true;
|
|
14348
15187
|
}
|
|
14349
15188
|
}
|
|
@@ -14351,33 +15190,36 @@ ${r.text}
|
|
|
14351
15190
|
const choices = starterChoices();
|
|
14352
15191
|
if (choices) {
|
|
14353
15192
|
try {
|
|
14354
|
-
|
|
14355
|
-
|
|
15193
|
+
mkdirSync5(deps.agentDir, { recursive: true });
|
|
15194
|
+
writeFileSync5(getStarterMarkerPath(deps.agentDir), "1");
|
|
14356
15195
|
} catch {}
|
|
14357
15196
|
const report = (event) => deps.reportStarterEvent ? deps.reportStarterEvent(event) : reportStarterEvent(deps, jwt, event);
|
|
14358
15197
|
deps.log(formatStarterMenu(choices));
|
|
14359
15198
|
report("starter_shown");
|
|
14360
15199
|
const picked = parseStarterKey(await (deps.askStarterKey ?? (() => readOneKeypress("")))(), choices.length);
|
|
14361
15200
|
if (picked === "abort") {
|
|
15201
|
+
flushNotes();
|
|
14362
15202
|
deps.log("Aborted.");
|
|
14363
15203
|
return 1;
|
|
14364
15204
|
}
|
|
14365
15205
|
if (picked !== "dismiss") {
|
|
14366
15206
|
report("starter_picked");
|
|
14367
15207
|
initialMessage = choices[picked].prompt;
|
|
14368
|
-
|
|
15208
|
+
note(`Sending: ${choices[picked].label}
|
|
14369
15209
|
`);
|
|
14370
15210
|
}
|
|
14371
15211
|
}
|
|
14372
15212
|
}
|
|
14373
|
-
if (deps.startupTip && !
|
|
15213
|
+
if (deps.startupTip && !importFlag.requested && canPrompt && hadJwt) {
|
|
14374
15214
|
try {
|
|
14375
|
-
const importAvailable = await deps.hasImportableSession?.().catch(() => false) ?? false;
|
|
15215
|
+
const importAvailable = !hintShown && (await deps.hasImportableSession?.().catch(() => false) ?? false);
|
|
14376
15216
|
const tip = deps.startupTip({ importAvailable });
|
|
14377
15217
|
if (tip)
|
|
14378
|
-
|
|
15218
|
+
note(tip);
|
|
14379
15219
|
} catch {}
|
|
14380
15220
|
}
|
|
15221
|
+
if (importHintNote)
|
|
15222
|
+
note(importHintNote);
|
|
14381
15223
|
const finalSessionId = await deps.launchPi({
|
|
14382
15224
|
baseUrl: deps.baseUrl,
|
|
14383
15225
|
jwt,
|
|
@@ -14394,7 +15236,9 @@ ${r.text}
|
|
|
14394
15236
|
refillCard: refillSession,
|
|
14395
15237
|
allowanceLine: allowanceLine(),
|
|
14396
15238
|
guestHandoffVariant,
|
|
14397
|
-
|
|
15239
|
+
choiceArm: me?.choice_prompts_arm,
|
|
15240
|
+
initialMessage,
|
|
15241
|
+
startupNotes: startupNotes.splice(0)
|
|
14398
15242
|
});
|
|
14399
15243
|
const endRefill = await fetchRefillStats(deps.baseUrl, jwt).catch(() => {
|
|
14400
15244
|
return;
|
|
@@ -14500,7 +15344,7 @@ async function main() {
|
|
|
14500
15344
|
}
|
|
14501
15345
|
if (process.env.FREEPI_RELAUNCH_READY) {
|
|
14502
15346
|
try {
|
|
14503
|
-
|
|
15347
|
+
writeFileSync6(process.env.FREEPI_RELAUNCH_READY, "");
|
|
14504
15348
|
} catch {}
|
|
14505
15349
|
}
|
|
14506
15350
|
const command = parseCliArgs(process.argv.slice(2));
|
|
@@ -14542,7 +15386,7 @@ ${HELP_TEXT}`);
|
|
|
14542
15386
|
baseUrl,
|
|
14543
15387
|
prepareImport: ({ source, jwt, model }) => prepareStartupImport({
|
|
14544
15388
|
cwd: process.cwd(),
|
|
14545
|
-
home:
|
|
15389
|
+
home: homedir10(),
|
|
14546
15390
|
baseUrl,
|
|
14547
15391
|
jwt,
|
|
14548
15392
|
model,
|
|
@@ -14551,8 +15395,8 @@ ${HELP_TEXT}`);
|
|
|
14551
15395
|
askKey: (prompt) => readOneKeypress(prompt)
|
|
14552
15396
|
}),
|
|
14553
15397
|
startupTip: ({ importAvailable }) => maybeTip({ agentDir, importAvailable }),
|
|
14554
|
-
hasImportableSession: async () => (await listFolderSessions({ cwd: process.cwd(), home:
|
|
14555
|
-
importHint: (latestFreePi) => startupImportHint({ cwd: process.cwd(), home:
|
|
15398
|
+
hasImportableSession: async () => (await listFolderSessions({ cwd: process.cwd(), home: homedir10(), limit: 1 })).length > 0,
|
|
15399
|
+
importHint: (latestFreePi) => startupImportHint({ cwd: process.cwd(), home: homedir10(), now: new Date, latestFreePi }),
|
|
14556
15400
|
agentDir,
|
|
14557
15401
|
credentialsPath: getCredentialsPath(agentDir),
|
|
14558
15402
|
promptConsent: promptConsentInteractive,
|
|
@@ -14561,13 +15405,13 @@ ${HELP_TEXT}`);
|
|
|
14561
15405
|
launchPi,
|
|
14562
15406
|
log: (message) => console.log(message),
|
|
14563
15407
|
checkClientVersion: (baseUrl, cliVersion, jwt) => checkClientVersion(baseUrl, cliVersion, fetch, jwt),
|
|
14564
|
-
startGravityPixel: (jwt) => startGravityPixel(jwt, join15(
|
|
15408
|
+
startGravityPixel: (jwt) => startGravityPixel(jwt, join15(dirname14(agentDir), "gravity")),
|
|
14565
15409
|
relaunchLatest: (latest) => relaunchViaNpx(latest, {
|
|
14566
15410
|
env: process.env,
|
|
14567
15411
|
argv: process.argv.slice(2),
|
|
14568
15412
|
platform: process.platform,
|
|
14569
15413
|
cwd: process.cwd(),
|
|
14570
|
-
homedir:
|
|
15414
|
+
homedir: homedir10(),
|
|
14571
15415
|
execPath: process.execPath,
|
|
14572
15416
|
autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),
|
|
14573
15417
|
spawn: (cmd, args, opts) => spawn3(cmd, args, opts),
|