free-pi-cli 0.2.19 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,4 +27,4 @@ if (err) {
27
27
  console.error(err);
28
28
  process.exit(1);
29
29
  }
30
- await import("./main-tc7xq892.js");
30
+ await import("./main-nyekh9dy.js");
@@ -44,7 +44,7 @@ function openBrowser(url, platform = process.platform, spawnImpl = spawn) {
44
44
  // package.json
45
45
  var package_default = {
46
46
  name: "free-pi-cli",
47
- version: "0.2.19",
47
+ version: "0.4.0",
48
48
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
49
49
  license: "MIT",
50
50
  type: "module",
@@ -63,7 +63,7 @@ var package_default = {
63
63
  prepublishOnly: "bun run build"
64
64
  },
65
65
  dependencies: {
66
- "@earendil-works/pi-coding-agent": "0.84.4"
66
+ "@earendil-works/pi-coding-agent": "1.2.0"
67
67
  },
68
68
  devDependencies: {
69
69
  "@freepi/pi-ads": "workspace:*",
@@ -80,10 +80,13 @@ var HELP_TEXT = `
80
80
  free-pi — free AI coding in your terminal; ads pay for the inference.
81
81
 
82
82
  Usage:
83
- free-pi-cli start the coding agent (first run: consent + GitHub sign-in)
84
- free-pi-cli logout remove the stored sign-in token; the next run signs in again
85
- free-pi-cli --version print the CLI version
86
- free-pi-cli --help show this help
83
+ free-pi-cli start the coding agent (first run: consent + GitHub sign-in)
84
+ free-pi-cli --session <id> resume a previous session
85
+ free-pi-cli logout remove the stored sign-in token; the next run signs in again
86
+ free-pi-cli mcp <command> add, remove, list, or sign in to your own MCP servers
87
+ (free-pi-cli mcp --help for details)
88
+ free-pi-cli --version print the CLI version
89
+ free-pi-cli --help show this help
87
90
 
88
91
  Environment:
89
92
  FREEPI_BASE_URL override the backend base URL (default https://api.freepi.ai)
@@ -96,6 +99,11 @@ function parseCliArgs(argv) {
96
99
  const [first, ...rest] = argv;
97
100
  if (first === undefined)
98
101
  return { kind: "run" };
102
+ if (first === "mcp")
103
+ return { kind: "mcp", args: rest };
104
+ if (first === "--session") {
105
+ return rest.length > 1 ? { kind: "unknown", arg: rest[1] } : { kind: "run" };
106
+ }
99
107
  let kind;
100
108
  if (first === "--help" || first === "-h" || first === "help")
101
109
  kind = "help";
@@ -159,21 +167,58 @@ async function runLogout(deps) {
159
167
  return 0;
160
168
  }
161
169
 
170
+ // src/mcp-command.ts
171
+ import { existsSync } from "node:fs";
172
+ import { dirname as dirname2, join } from "node:path";
173
+ import { fileURLToPath, pathToFileURL } from "node:url";
174
+ function piMcpCliPath() {
175
+ return join(dirname2(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "extensions", "mcp", "cli.js");
176
+ }
177
+ async function loadPiRunner() {
178
+ const path = piMcpCliPath();
179
+ if (!existsSync(path)) {
180
+ throw new Error(`free-pi-cli mcp is unavailable: the bundled pi package has no ${path}. Reinstall free-pi-cli.`);
181
+ }
182
+ const mod = await import(pathToFileURL(path).href);
183
+ if (typeof mod.runMcpCommand !== "function") {
184
+ throw new Error(`free-pi-cli mcp is unavailable: ${path} does not export runMcpCommand. Reinstall free-pi-cli.`);
185
+ }
186
+ return mod.runMcpCommand;
187
+ }
188
+ function brandMcpLine(line) {
189
+ return line.replace(/\bpi (?=mcp\b)/g, "free-pi-cli ").replace(/~\/\.pi\/agent\b/g, "~/.free-pi/agent").replace(/\b([Ss]tart) pi\b/g, "$1 free-pi-cli");
190
+ }
191
+ async function runFreePiMcp(args, deps) {
192
+ try {
193
+ const run = await (deps.loadRunner ?? loadPiRunner)();
194
+ process.env.PI_CODING_AGENT_DIR = deps.agentDir;
195
+ return await run(args, {
196
+ cwd: deps.cwd,
197
+ agentDir: deps.agentDir,
198
+ log: (line) => deps.log(brandMcpLine(line)),
199
+ error: (line) => deps.error(brandMcpLine(line))
200
+ });
201
+ } catch (err) {
202
+ deps.error(err instanceof Error ? err.message : String(err));
203
+ return 1;
204
+ }
205
+ }
206
+
162
207
  // src/paths.ts
163
208
  import { homedir } from "node:os";
164
- import { join } from "node:path";
209
+ import { join as join2 } from "node:path";
165
210
  var FREE_PI_DIR_NAME = ".free-pi";
166
211
  function getFreePiAgentDir() {
167
- return join(homedir(), FREE_PI_DIR_NAME, "agent");
212
+ return join2(homedir(), FREE_PI_DIR_NAME, "agent");
168
213
  }
169
214
  function getCredentialsPath(agentDir = getFreePiAgentDir()) {
170
- return join(agentDir, "credentials.json");
215
+ return join2(agentDir, "credentials.json");
171
216
  }
172
217
  function getLastSeenVersionPath(agentDir = getFreePiAgentDir()) {
173
- return join(agentDir, "last-seen-version");
218
+ return join2(agentDir, "last-seen-version");
174
219
  }
175
220
  function getConfigPath() {
176
- return join(homedir(), FREE_PI_DIR_NAME, "config.json");
221
+ return join2(homedir(), FREE_PI_DIR_NAME, "config.json");
177
222
  }
178
223
 
179
224
  // src/pi-launch.ts
@@ -181,6 +226,9 @@ import {
181
226
  createAgentSessionFromServices,
182
227
  createAgentSessionRuntime,
183
228
  createAgentSessionServices,
229
+ createCodemodeExtension,
230
+ createMcpExtension,
231
+ createToolSearchExtension,
184
232
  InteractiveMode,
185
233
  SessionManager,
186
234
  SettingsManager
@@ -4851,6 +4899,13 @@ var ClientVersionResponseSchema = object({
4851
4899
  var SessionResetResponseSchema = object({
4852
4900
  released: boolean2()
4853
4901
  });
4902
+ var BugReportRequestSchema = object({
4903
+ description: string2().trim().min(1).max(4000),
4904
+ session_id: string2().max(128).optional(),
4905
+ cli_version: string2().max(32).optional(),
4906
+ model: string2().max(128).optional()
4907
+ });
4908
+ var BugReportResponseSchema = object({ id: string2() });
4854
4909
  // ../shared/src/errors.ts
4855
4910
  function assertExhaustiveErrorCode(code) {
4856
4911
  throw new Error(`unhandled error code: ${String(code)}`);
@@ -4865,6 +4920,8 @@ var ConfigSchema = object({
4865
4920
  accountsPerDayCeiling: number2().int().default(500),
4866
4921
  authRatePerIpPerHour: number2().int().default(30),
4867
4922
  webBuyUrlRatePerProviderPerHour: number2().int().default(20),
4923
+ handoffTokenRatePerProviderPerHour: number2().int().default(60),
4924
+ handoffHourlyCompletionCap: number2().int().default(120),
4868
4925
  tokenRatePerIpPerHour: number2().int().default(3600),
4869
4926
  consentRatePerIpPerHour: number2().int().default(60),
4870
4927
  adInlineTurnFrequency: number2().int().default(5),
@@ -4931,6 +4988,8 @@ function loadConfig(env = process.env) {
4931
4988
  accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
4932
4989
  authRatePerIpPerHour: envNumber(env.AUTH_RATE_PER_IP_PER_HOUR),
4933
4990
  webBuyUrlRatePerProviderPerHour: envNumber(env.WEB_BUY_URL_RATE_PER_PROVIDER_PER_HOUR),
4991
+ handoffTokenRatePerProviderPerHour: envNumber(env.HANDOFF_TOKEN_RATE_PER_PROVIDER_PER_HOUR),
4992
+ handoffHourlyCompletionCap: envNumber(env.HANDOFF_HOURLY_COMPLETION_CAP),
4934
4993
  tokenRatePerIpPerHour: envNumber(env.TOKEN_RATE_PER_IP_PER_HOUR),
4935
4994
  consentRatePerIpPerHour: envNumber(env.CONSENT_RATE_PER_IP_PER_HOUR),
4936
4995
  adInlineTurnFrequency: envNumber(env.AD_INLINE_TURN_FREQUENCY),
@@ -5186,6 +5245,8 @@ function mapErrorCode(code) {
5186
5245
  return "too many requests this hour on the free tier — wait for the Retry-After window";
5187
5246
  case "account_review":
5188
5247
  return "this account is under review — contact support";
5248
+ case "ads_required":
5249
+ return "ads aren't displaying for this account, so free completions are paused — run free-pi through `npx free-pi-cli`, then retry";
5189
5250
  default:
5190
5251
  return assertExhaustiveErrorCode(code);
5191
5252
  }
@@ -5273,7 +5334,7 @@ function createAdsExtension(opts) {
5273
5334
  }
5274
5335
 
5275
5336
  // src/pi-launch.ts
5276
- import { join as join2, resolve } from "node:path";
5337
+ import { join as join3, resolve as resolve2 } from "node:path";
5277
5338
 
5278
5339
  // src/provider.ts
5279
5340
  var PROVIDER_NAME = "free-pi";
@@ -5311,15 +5372,38 @@ var Metrics = {
5311
5372
  update: 0
5312
5373
  };
5313
5374
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/guard/guard.mjs
5375
+ function IsArray(value) {
5376
+ return Array.isArray(value);
5377
+ }
5378
+ function IsBigInt(value) {
5379
+ return IsEqual(typeof value, "bigint");
5380
+ }
5381
+ function IsBoolean(value) {
5382
+ return IsEqual(typeof value, "boolean");
5383
+ }
5314
5384
  function IsNull(value) {
5315
5385
  return IsEqual(value, null);
5316
5386
  }
5387
+ function IsNumber(value) {
5388
+ return Number.isFinite(value);
5389
+ }
5317
5390
  function IsObject(value) {
5318
5391
  return IsEqual(typeof value, "object") && !IsNull(value);
5319
5392
  }
5393
+ function IsString(value) {
5394
+ return IsEqual(typeof value, "string");
5395
+ }
5320
5396
  function IsEqual(left, right) {
5321
5397
  return left === right;
5322
5398
  }
5399
+ function IsClassInstance(value) {
5400
+ if (!IsObject(value))
5401
+ return false;
5402
+ const proto = globalThis.Object.getPrototypeOf(value);
5403
+ if (IsNull(proto))
5404
+ return false;
5405
+ return IsEqual(typeof proto.constructor, "function") && !(IsEqual(proto.constructor, globalThis.Object) || IsEqual(proto.constructor.name, "Object"));
5406
+ }
5323
5407
  function IsUnsafePropertyKey(key) {
5324
5408
  return IsEqual(key, "__proto__") || IsEqual(key, "constructor") || IsEqual(key, "prototype");
5325
5409
  }
@@ -5329,6 +5413,81 @@ function HasPropertyKey(value, key) {
5329
5413
  function Keys(value) {
5330
5414
  return Object.getOwnPropertyNames(value);
5331
5415
  }
5416
+ function Symbols(value) {
5417
+ return Object.getOwnPropertySymbols(value);
5418
+ }
5419
+ // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/guard/globals.mjs
5420
+ function IsTypeArray(value) {
5421
+ return globalThis.ArrayBuffer.isView(value);
5422
+ }
5423
+ function IsRegExp(value) {
5424
+ return value instanceof globalThis.RegExp;
5425
+ }
5426
+ function IsSet(value) {
5427
+ return value instanceof globalThis.Set;
5428
+ }
5429
+ function IsMap(value) {
5430
+ return value instanceof globalThis.Map;
5431
+ }
5432
+ // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/memory/clone.mjs
5433
+ function FromClassInstance(value) {
5434
+ return value;
5435
+ }
5436
+ function IsSchemaObject(value) {
5437
+ return HasPropertyKey(value, "~kind") || HasPropertyKey(value, "~unsafe");
5438
+ }
5439
+ function FromSchemaObject(value) {
5440
+ const result = {};
5441
+ for (const key of Keys(value)) {
5442
+ if (IsUnsafePropertyKey(key))
5443
+ continue;
5444
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
5445
+ descriptor.value = FromValue(descriptor.value);
5446
+ if (IsEqual(descriptor.enumerable, true)) {
5447
+ result[key] = descriptor.value;
5448
+ } else {
5449
+ Object.defineProperty(result, key, descriptor);
5450
+ }
5451
+ }
5452
+ return result;
5453
+ }
5454
+ function FromPlainObject(value) {
5455
+ const result = {};
5456
+ for (const key of Keys(value)) {
5457
+ if (IsUnsafePropertyKey(key))
5458
+ continue;
5459
+ result[key] = FromValue(value[key]);
5460
+ }
5461
+ for (const key of Symbols(value)) {
5462
+ result[key] = FromValue(value[key]);
5463
+ }
5464
+ return result;
5465
+ }
5466
+ function FromObject(value) {
5467
+ return IsClassInstance(value) ? FromClassInstance(value) : IsSchemaObject(value) ? FromSchemaObject(value) : FromPlainObject(value);
5468
+ }
5469
+ function FromArray(value) {
5470
+ return value.map((element) => FromValue(element));
5471
+ }
5472
+ function FromTypedArray(value) {
5473
+ return value.slice();
5474
+ }
5475
+ function FromRegExp(value) {
5476
+ return new RegExp(value.source, value.flags);
5477
+ }
5478
+ function FromMap(value) {
5479
+ return new Map(FromValue([...value.entries()]));
5480
+ }
5481
+ function FromSet(value) {
5482
+ return new Set(FromValue([...value.values()]));
5483
+ }
5484
+ function FromValue(value) {
5485
+ return IsTypeArray(value) ? FromTypedArray(value) : IsRegExp(value) ? FromRegExp(value) : IsMap(value) ? FromMap(value) : IsSet(value) ? FromSet(value) : IsArray(value) ? FromArray(value) : IsObject(value) ? FromObject(value) : value;
5486
+ }
5487
+ function Clone(value) {
5488
+ Metrics.clone += 1;
5489
+ return FromValue(value);
5490
+ }
5332
5491
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/settings/settings.mjs
5333
5492
  var settings = {
5334
5493
  immutableTypes: false,
@@ -5365,12 +5524,52 @@ function Create(hidden, enumerable, options = {}) {
5365
5524
  const withHidden = settings.enumerableKind ? Merge(withOptions, hidden) : MergeHidden(withOptions, hidden);
5366
5525
  return settings.immutableTypes ? Object.freeze(withHidden) : withHidden;
5367
5526
  }
5527
+ // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/memory/update.mjs
5528
+ function Update(current, hidden, enumerable) {
5529
+ Metrics.update += 1;
5530
+ const settings = Get();
5531
+ const result = Clone(current);
5532
+ for (const key of Object.keys(hidden)) {
5533
+ Object.defineProperty(result, key, {
5534
+ configurable: true,
5535
+ writable: true,
5536
+ enumerable: settings.enumerableKind,
5537
+ value: hidden[key]
5538
+ });
5539
+ }
5540
+ for (const key of Object.keys(enumerable)) {
5541
+ Object.defineProperty(result, key, {
5542
+ configurable: true,
5543
+ enumerable: true,
5544
+ writable: true,
5545
+ value: enumerable[key]
5546
+ });
5547
+ }
5548
+ return result;
5549
+ }
5368
5550
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/schema.mjs
5369
5551
  function IsSchema(value) {
5370
5552
  return IsObject(value);
5371
5553
  }
5372
5554
 
5555
+ // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/engine/optional/instantiate_add.mjs
5556
+ function AddOptionalOperation(type) {
5557
+ return Update(type, { "~optional": true }, {});
5558
+ }
5559
+ function AddOptionalAction(type, options) {
5560
+ const result = Update(AddOptionalOperation(type), {}, options);
5561
+ return result;
5562
+ }
5563
+
5564
+ // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/action/_add_optional.mjs
5565
+ function AddOptional(type, options = {}) {
5566
+ return AddOptionalAction(type, options);
5567
+ }
5568
+
5373
5569
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/_optional.mjs
5570
+ function Optional(type) {
5571
+ return AddOptional(type);
5572
+ }
5374
5573
  function IsOptional(value) {
5375
5574
  return IsSchema(value) && HasPropertyKey(value, "~optional");
5376
5575
  }
@@ -5413,10 +5612,41 @@ var F64Out = new Uint8Array(F64.buffer);
5413
5612
  var encoder = new TextEncoder;
5414
5613
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/integer.mjs
5415
5614
  var IntegerPattern = "-?(?:0|[1-9][0-9]*)";
5615
+ // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/literal.mjs
5616
+ class InvalidLiteralValue extends Error {
5617
+ constructor(value) {
5618
+ super(`Invalid Literal value`);
5619
+ Object.defineProperty(this, "cause", {
5620
+ value: { value },
5621
+ writable: false,
5622
+ configurable: false,
5623
+ enumerable: false
5624
+ });
5625
+ }
5626
+ }
5627
+ function LiteralTypeName(value) {
5628
+ return IsBigInt(value) ? "bigint" : IsBoolean(value) ? "boolean" : IsNumber(value) ? "number" : IsString(value) ? "string" : (() => {
5629
+ throw new InvalidLiteralValue(value);
5630
+ })();
5631
+ }
5632
+ function Literal(value, options) {
5633
+ return Create({ "~kind": "Literal" }, { type: LiteralTypeName(value), const: value }, options);
5634
+ }
5416
5635
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/number.mjs
5417
5636
  var NumberPattern = "-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?";
5637
+ function Number2(options) {
5638
+ return Create({ "~kind": "Number" }, { type: "number" }, options);
5639
+ }
5418
5640
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/string.mjs
5419
5641
  var StringPattern = ".*";
5642
+ function String2(options) {
5643
+ return Create({ "~kind": "String" }, { type: "string" }, options);
5644
+ }
5645
+
5646
+ // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/union.mjs
5647
+ function Union(anyOf, options = {}) {
5648
+ return Create({ "~kind": "Union" }, { anyOf }, options);
5649
+ }
5420
5650
 
5421
5651
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/record.mjs
5422
5652
  var IntegerKey = `^${IntegerPattern}$`;
@@ -5573,9 +5803,10 @@ exciting models. Ads shown in the terminal pay for it. You never handle a
5573
5803
  model API key. Config and the sign-in token live under \`~/.free-pi/agent\`,
5574
5804
  separate from any \`~/.pi/agent\` install.
5575
5805
 
5576
- ## The promise
5577
- Ad text never enters the model's context. Ads render only in the terminal,
5578
- in a framed card, and the model never sees them.
5806
+ ## Ads and your data
5807
+ Ads render in the terminal, in a framed card. Some ads come from Gravity, a
5808
+ third-party ad provider that receives excerpts of your recent conversation
5809
+ to pick a relevant ad.
5579
5810
  Coding sessions may be used to train models. This is on the consent screen
5580
5811
  at first run, before the GitHub sign-in.
5581
5812
 
@@ -5599,6 +5830,33 @@ first. Purchased credits cover use beyond it and do not expire.
5599
5830
  - \`/close-other-session\` — free a stuck session on another machine
5600
5831
  - \`/update\` — get the latest free-pi
5601
5832
  - \`/whats-new\` — what changed in recent releases
5833
+ - \`/todos\` — show the agent's todo list
5834
+ - \`/mcp\` — manage your MCP servers (connect, sign in, turn on or off)
5835
+
5836
+ ## Todo list
5837
+ The agent keeps its own todo list with the \`todo\` tool, to track multi-step
5838
+ work. While any item is open, the list shows live above the input box, and
5839
+ each todo change prints a progress line in the conversation. \`/todos\` shows
5840
+ the full list. The list belongs to the session and comes back when you resume
5841
+ it.
5842
+
5843
+ ## MCP servers
5844
+ MCP (Model Context Protocol) servers give the agent extra tools, such as a
5845
+ database, a browser, or an internal API. free-pi includes none; you add your
5846
+ own. The agent reaches their tools by writing a short script for its
5847
+ \`codemode\` tool.
5848
+ Servers live in \`~/.free-pi/agent/mcp.json\`. A project can have its own
5849
+ \`.pi/mcp.json\`; it is read only after you trust the project when asked.
5850
+ Add a server from a shell, or ask the agent to run the command for you with
5851
+ its bash tool:
5852
+ \`free-pi-cli mcp add files -- npx -y @modelcontextprotocol/server-filesystem .\`
5853
+ \`free-pi-cli mcp add docs --url https://example.com/mcp\`
5854
+ Other commands: \`free-pi-cli mcp list\` (add \`--json\` for machine output),
5855
+ \`free-pi-cli mcp remove <server>\`, \`free-pi-cli mcp login <server>\` to sign
5856
+ in to a server that uses OAuth, and \`free-pi-cli mcp logout <server>\`. Add
5857
+ \`-l\` to add or remove in the current project instead of globally.
5858
+ Inside a session, \`/mcp\` shows each server and its tools, and lets you sign
5859
+ in, reconnect, or turn a server on or off.
5602
5860
 
5603
5861
  ## Support
5604
5862
  Today's sponsor keeps free-pi free. When the user asks how to help the
@@ -5630,6 +5888,257 @@ function createDocsToolExtension() {
5630
5888
  };
5631
5889
  }
5632
5890
 
5891
+ // src/todo-view.ts
5892
+ var PLAIN_STYLE = { bold: (s) => s, dim: (s) => s, accent: (s) => s, error: (s) => s };
5893
+ var GAP = " ";
5894
+ function truncate2(text, max) {
5895
+ if (max <= 0)
5896
+ return "";
5897
+ if ([...text].length <= max)
5898
+ return text;
5899
+ return [...text].slice(0, Math.max(0, max - 1)).join("") + "…";
5900
+ }
5901
+ function counts(todos) {
5902
+ return { done: todos.filter((t) => t.done).length, total: todos.length };
5903
+ }
5904
+ function widgetLines(todos, width, st = PLAIN_STYLE) {
5905
+ if (todos.length === 0)
5906
+ return [];
5907
+ const { done, total } = counts(todos);
5908
+ const header = `Todos ${done}/${total}`;
5909
+ const firstOpen = todos.findIndex((t) => !t.done);
5910
+ const marks = todos.map((t, i) => t.done ? `✓ ${t.text}` : i === firstOpen ? `▸ ${t.text}` : `○ ${t.text}`);
5911
+ const compactLength = [...[header, ...marks].join(GAP)].length;
5912
+ if (compactLength <= width) {
5913
+ return [
5914
+ [
5915
+ st.bold(header),
5916
+ ...todos.map((t, i) => t.done ? st.dim(marks[i]) : i === firstOpen ? st.accent(st.bold(marks[i])) : st.dim(marks[i]))
5917
+ ].join(GAP)
5918
+ ];
5919
+ }
5920
+ const lines = [st.bold(truncate2(header, width))];
5921
+ const open = todos.filter((t) => !t.done);
5922
+ if (open.length === 0) {
5923
+ lines.push(st.dim(truncate2("✓ All done", width)));
5924
+ return lines;
5925
+ }
5926
+ open.slice(0, 3).forEach((t, i) => {
5927
+ const line = truncate2(`${i === 0 ? "▸" : "○"} ${t.text}`, width);
5928
+ lines.push(i === 0 ? st.accent(st.bold(line)) : st.dim(line));
5929
+ });
5930
+ const more = open.length - 3;
5931
+ if (more > 0)
5932
+ lines.push(st.dim(truncate2(`+${more} more · /todos for all`, width)));
5933
+ return lines;
5934
+ }
5935
+ function resultLines(d, st = PLAIN_STYLE) {
5936
+ if (d.error)
5937
+ return [st.error(`Error: ${d.error}`)];
5938
+ if (d.action === "clear")
5939
+ return [`${st.accent("●")} ${st.bold("todo")} cleared`];
5940
+ const { done, total } = counts(d.todos);
5941
+ const head = `${st.accent("●")} ${st.bold("todo")} ${done}/${total}`;
5942
+ let first = head;
5943
+ if (d.item && d.action === "add")
5944
+ first = `${head}${GAP}+ ${d.item.text}`;
5945
+ if (d.item && d.action === "toggle")
5946
+ first = `${head}${GAP}${d.item.done ? "✓" : "○"} ${d.item.text}`;
5947
+ const lines = [first];
5948
+ const next = d.todos.find((t) => !t.done);
5949
+ if ((d.action === "add" || d.action === "toggle") && next && next.id !== d.item?.id)
5950
+ lines.push(st.dim(`${GAP}next: ${next.text}`));
5951
+ return lines;
5952
+ }
5953
+
5954
+ // src/todo-tool.ts
5955
+ var TODO_TOOL_NAME = "todo";
5956
+ var TODO_WIDGET_ID = "freepi-todo";
5957
+ var TODO_ENTRY_TYPE = "free-pi-todo";
5958
+ var ALL_DONE_LINGER_MS = 3000;
5959
+ var REASSERT_ORDER_MS = 15000;
5960
+ var realTimers = {
5961
+ setTimeout: (fn, ms) => {
5962
+ const h = setTimeout(fn, ms);
5963
+ h.unref?.();
5964
+ return h;
5965
+ },
5966
+ clearTimeout: (h) => clearTimeout(h),
5967
+ setInterval: (fn, ms) => {
5968
+ const h = setInterval(fn, ms);
5969
+ h.unref?.();
5970
+ return h;
5971
+ },
5972
+ clearInterval: (h) => clearInterval(h)
5973
+ };
5974
+ var PARAMS4 = _Object_({
5975
+ action: Union([Literal("list"), Literal("add"), Literal("toggle"), Literal("clear")]),
5976
+ text: Optional(String2({ description: "Todo text (for add)" })),
5977
+ id: Optional(Number2({ description: "Todo ID (for toggle)" }))
5978
+ });
5979
+ function asSnapshot(value) {
5980
+ const v = value;
5981
+ if (!v || !Array.isArray(v.todos) || typeof v.nextId !== "number")
5982
+ return;
5983
+ return { todos: v.todos, nextId: v.nextId };
5984
+ }
5985
+ function formatTodos(todos) {
5986
+ return todos.length ? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join(`
5987
+ `) : "No todos";
5988
+ }
5989
+ function styleFrom(theme) {
5990
+ return {
5991
+ bold: (s) => theme.bold(s),
5992
+ dim: (s) => theme.fg("dim", s),
5993
+ accent: (s) => theme.fg("accent", s),
5994
+ error: (s) => theme.fg("error", s)
5995
+ };
5996
+ }
5997
+ function createTodoToolExtension(options = {}) {
5998
+ const timers = options.timers ?? realTimers;
5999
+ return {
6000
+ name: "free-pi-todo",
6001
+ factory(pi) {
6002
+ let todos = [];
6003
+ let nextId = 1;
6004
+ let widgetCtx;
6005
+ let lingerTimer;
6006
+ let orderTimer;
6007
+ const stopTimers = () => {
6008
+ if (lingerTimer !== undefined)
6009
+ timers.clearTimeout(lingerTimer);
6010
+ if (orderTimer !== undefined)
6011
+ timers.clearInterval(orderTimer);
6012
+ lingerTimer = orderTimer = undefined;
6013
+ };
6014
+ const hideWidget = () => {
6015
+ stopTimers();
6016
+ try {
6017
+ widgetCtx?.ui.setWidget(TODO_WIDGET_ID, undefined);
6018
+ } catch {}
6019
+ };
6020
+ const showWidget = () => {
6021
+ try {
6022
+ widgetCtx?.ui.setWidget(TODO_WIDGET_ID, (_tui, theme) => {
6023
+ const style = styleFrom(theme);
6024
+ return {
6025
+ render: (width) => widgetLines(todos, Math.max(1, width - 2), style).map((l) => ` ${l}`),
6026
+ invalidate() {}
6027
+ };
6028
+ });
6029
+ } catch {}
6030
+ };
6031
+ const refreshWidget = (ctx, replay = false) => {
6032
+ if (ctx?.mode !== "tui")
6033
+ return;
6034
+ widgetCtx = ctx;
6035
+ const hasOpen = todos.some((t) => !t.done);
6036
+ if (todos.length === 0 || !hasOpen && replay) {
6037
+ hideWidget();
6038
+ return;
6039
+ }
6040
+ if (lingerTimer !== undefined) {
6041
+ timers.clearTimeout(lingerTimer);
6042
+ lingerTimer = undefined;
6043
+ }
6044
+ showWidget();
6045
+ if (orderTimer === undefined)
6046
+ orderTimer = timers.setInterval(showWidget, REASSERT_ORDER_MS);
6047
+ if (!hasOpen) {
6048
+ lingerTimer = timers.setTimeout(() => {
6049
+ lingerTimer = undefined;
6050
+ hideWidget();
6051
+ }, ALL_DONE_LINGER_MS);
6052
+ }
6053
+ };
6054
+ const reconstructState = (ctx) => {
6055
+ todos = [];
6056
+ nextId = 1;
6057
+ for (const entry of ctx.sessionManager.getBranch()) {
6058
+ let snap;
6059
+ if (entry.type === "custom" && entry.customType === TODO_ENTRY_TYPE)
6060
+ snap = asSnapshot(entry.data);
6061
+ else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === TODO_TOOL_NAME)
6062
+ snap = asSnapshot(entry.message.details);
6063
+ if (snap) {
6064
+ todos = snap.todos.map((t) => ({ ...t }));
6065
+ nextId = snap.nextId;
6066
+ }
6067
+ }
6068
+ refreshWidget(ctx, true);
6069
+ };
6070
+ pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
6071
+ pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
6072
+ pi.on("session_shutdown", async () => {
6073
+ stopTimers();
6074
+ widgetCtx = undefined;
6075
+ });
6076
+ const result = (ctx, text, action, error, item) => {
6077
+ const snapshot = { todos: todos.map((t) => ({ ...t })), nextId };
6078
+ if (action !== "list" && !error)
6079
+ pi.appendEntry(TODO_ENTRY_TYPE, snapshot);
6080
+ if (action !== "list" && !error)
6081
+ refreshWidget(ctx);
6082
+ return {
6083
+ content: [{ type: "text", text }],
6084
+ details: { action, ...snapshot, ...item ? { item: { ...item } } : {}, ...error ? { error } : {} }
6085
+ };
6086
+ };
6087
+ pi.registerTool({
6088
+ name: TODO_TOOL_NAME,
6089
+ label: "Todo",
6090
+ description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
6091
+ promptSnippet: "Track your own multi-step work in a todo list: add items, toggle them done, list them",
6092
+ parameters: PARAMS4,
6093
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
6094
+ switch (params.action) {
6095
+ case "list":
6096
+ return result(ctx, formatTodos(todos), "list");
6097
+ case "add": {
6098
+ if (!params.text)
6099
+ return result(ctx, "Error: text required for add", "add", "text required");
6100
+ const todo = { id: nextId++, text: params.text, done: false };
6101
+ todos.push(todo);
6102
+ return result(ctx, `Added todo #${todo.id}: ${todo.text}`, "add", undefined, todo);
6103
+ }
6104
+ case "toggle": {
6105
+ if (params.id === undefined)
6106
+ return result(ctx, "Error: id required for toggle", "toggle", "id required");
6107
+ const todo = todos.find((t) => t.id === params.id);
6108
+ if (!todo)
6109
+ return result(ctx, `Todo #${params.id} not found`, "toggle", `#${params.id} not found`);
6110
+ todo.done = !todo.done;
6111
+ return result(ctx, `Todo #${todo.id} ${todo.done ? "completed" : "uncompleted"}`, "toggle", undefined, todo);
6112
+ }
6113
+ case "clear": {
6114
+ const count = todos.length;
6115
+ todos = [];
6116
+ nextId = 1;
6117
+ return result(ctx, `Cleared ${count} todos`, "clear");
6118
+ }
6119
+ default:
6120
+ return result(ctx, `Unknown action: ${String(params.action)}`, "list", "unknown action");
6121
+ }
6122
+ },
6123
+ renderCall() {
6124
+ return { render: () => [], invalidate() {} };
6125
+ },
6126
+ renderResult(res, _opts, theme) {
6127
+ const details = res.details;
6128
+ const lines = details?.action ? resultLines(details, styleFrom(theme)) : [res.content[0]?.type === "text" ? res.content[0].text : ""];
6129
+ return { render: () => lines, invalidate() {} };
6130
+ }
6131
+ });
6132
+ pi.registerCommand("todos", {
6133
+ description: "Show all todos on the current branch",
6134
+ handler: async (_args, ctx) => {
6135
+ ctx.ui.notify(formatTodos(todos), "info");
6136
+ }
6137
+ });
6138
+ }
6139
+ };
6140
+ }
6141
+
5633
6142
  // src/error-notice.ts
5634
6143
  var NOTICE_DEDUPE_MS = 15000;
5635
6144
  function parseErrorEnvelope(errorMessage) {
@@ -5685,12 +6194,16 @@ async function reportToolGuardTrigger(opts, toolName) {
5685
6194
  });
5686
6195
  } catch {}
5687
6196
  }
6197
+ var MCP_RESOURCE_TOOLS = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
6198
+ function isAllowed(opts, name) {
6199
+ return opts.allowedTools.includes(name) || name.startsWith("mcp__") || MCP_RESOURCE_TOOLS.includes(name);
6200
+ }
5688
6201
  function createToolGuardExtension(opts) {
5689
6202
  return {
5690
6203
  name: "free-pi-tool-guard",
5691
6204
  factory(pi) {
5692
6205
  pi.on("tool_call", (event) => {
5693
- if (opts.allowedTools.includes(event.toolName))
6206
+ if (isAllowed(opts, event.toolName))
5694
6207
  return;
5695
6208
  reportToolGuardTrigger(opts, event.toolName);
5696
6209
  return { block: true, reason: "tool not permitted in the free-pi distro" };
@@ -5699,8 +6212,90 @@ function createToolGuardExtension(opts) {
5699
6212
  };
5700
6213
  }
5701
6214
 
6215
+ // src/bug-report.ts
6216
+ var BUG_USAGE = "Usage: /bug <what went wrong>";
6217
+ var BUG_DESCRIPTION = "Report a bug to free-pi";
6218
+ function buildBugRequest(description, ctx) {
6219
+ return { description, session_id: ctx.sessionId, cli_version: CLI_VERSION, model: ctx.model };
6220
+ }
6221
+ function mapBugFailure(status) {
6222
+ if (status === 401)
6223
+ return "Couldn't send the bug report: you're signed out. Restart free-pi-cli to sign in again.";
6224
+ if (status === 429)
6225
+ return "Couldn't send the bug report: you've reached today's limit of 20 reports. Try again tomorrow.";
6226
+ if (status === 413)
6227
+ return "Couldn't send the bug report: it is too large.";
6228
+ if (status === 400)
6229
+ return "Couldn't send the bug report: the description is too long (4000 characters max).";
6230
+ return `Couldn't send the bug report (the server returned ${status}). Try again in a moment.`;
6231
+ }
6232
+ async function sendBugReport(opts, body) {
6233
+ try {
6234
+ const res = await (opts.fetchImpl ?? fetch)(`${opts.baseUrl}/bug-reports`, {
6235
+ method: "POST",
6236
+ headers: { Authorization: `Bearer ${await opts.getToken()}`, "Content-Type": "application/json" },
6237
+ body: JSON.stringify(body),
6238
+ signal: AbortSignal.timeout(1e4)
6239
+ });
6240
+ if (!res.ok)
6241
+ return { ok: false, message: mapBugFailure(res.status) };
6242
+ const parsed = BugReportResponseSchema.safeParse(await res.json());
6243
+ if (!parsed.success)
6244
+ return { ok: false, message: mapBugFailure(res.status) };
6245
+ return { ok: true, id: parsed.data.id };
6246
+ } catch {
6247
+ return { ok: false, message: "Couldn't reach free-pi to send the bug report — check your connection." };
6248
+ }
6249
+ }
6250
+ function makeBugHandler(mode, opts) {
6251
+ return async (hint) => {
6252
+ if (!hint)
6253
+ return mode.showStatus(BUG_USAGE);
6254
+ const r = await sendBugReport(opts, buildBugRequest(hint, { sessionId: mode.session.sessionId, model: mode.session.model?.id }));
6255
+ if (r.ok)
6256
+ return mode.showStatus(`Thanks — bug report received (#${r.id}).`);
6257
+ if (!mode.editor.getText())
6258
+ mode.editor.setText(`/bug ${hint}`);
6259
+ mode.showError(r.message);
6260
+ };
6261
+ }
6262
+ function installBugHandler(mode, opts) {
6263
+ mode.handleBugCommand = makeBugHandler(mode, opts);
6264
+ }
6265
+ function registerBugAutocomplete(pi) {
6266
+ pi.on("session_start", (_event, ctx) => {
6267
+ ctx.ui.addAutocompleteProvider((cur) => ({
6268
+ triggerCharacters: cur.triggerCharacters,
6269
+ applyCompletion: (...a) => cur.applyCompletion(...a),
6270
+ shouldTriggerFileCompletion: cur.shouldTriggerFileCompletion?.bind(cur),
6271
+ getSuggestions: async (...a) => {
6272
+ const r = await cur.getSuggestions(...a);
6273
+ return r && {
6274
+ ...r,
6275
+ items: r.items.map((i) => i.value === "bug" && i.label === "bug" ? { ...i, description: BUG_DESCRIPTION } : i)
6276
+ };
6277
+ }
6278
+ }));
6279
+ });
6280
+ }
6281
+
5702
6282
  // src/changelog.ts
5703
6283
  var CHANGELOG_HIGHLIGHTS = [
6284
+ {
6285
+ version: "0.4.0",
6286
+ highlights: [
6287
+ "The agent now keeps a todo list for multi-step work: it shows live above the input box, and /todos shows the full list.",
6288
+ "Add your own MCP servers: `npx free-pi-cli mcp add`, then /mcp in a session to manage them."
6289
+ ]
6290
+ },
6291
+ {
6292
+ version: "0.3.0",
6293
+ highlights: [
6294
+ "Upgraded the underlying pi agent engine to 1.2.0.",
6295
+ "/bug now sends reports to the free-pi team.",
6296
+ "`npx free-pi-cli --session <id>` resumes a session again."
6297
+ ]
6298
+ },
5704
6299
  {
5705
6300
  version: "0.2.19",
5706
6301
  highlights: [
@@ -5911,6 +6506,7 @@ function createFreePiCommandsExtension(opts) {
5911
6506
  return {
5912
6507
  name: "free-pi-commands",
5913
6508
  factory(pi) {
6509
+ registerBugAutocomplete(pi);
5914
6510
  pi.registerCommand("close-other-session", {
5915
6511
  description: "Clear your other running free-pi session (use if a previous one is stuck).",
5916
6512
  handler: (_args, ctx) => closeOtherSession(opts, ctx)
@@ -5974,12 +6570,12 @@ function updateBannerLines(theme, latest, width) {
5974
6570
  return [
5975
6571
  "",
5976
6572
  border,
5977
- theme.bold(theme.fg("warning", truncate2(headline, inner))),
5978
- theme.fg("dim", truncate2(detail, inner)),
6573
+ theme.bold(theme.fg("warning", truncate3(headline, inner))),
6574
+ theme.fg("dim", truncate3(detail, inner)),
5979
6575
  border
5980
6576
  ];
5981
6577
  }
5982
- function truncate2(text, maxWidth) {
6578
+ function truncate3(text, maxWidth) {
5983
6579
  if (maxWidth <= 0)
5984
6580
  return "";
5985
6581
  if (text.length <= maxWidth)
@@ -5990,7 +6586,7 @@ function truncate2(text, maxWidth) {
5990
6586
  }
5991
6587
  function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest) {
5992
6588
  const inner = width - 1;
5993
- const fit = (plain, styled) => plain.length <= inner ? styled() : theme.fg("dim", truncate2(plain, inner));
6589
+ const fit = (plain, styled) => plain.length <= inner ? styled() : theme.fg("dim", truncate3(plain, inner));
5994
6590
  const titleRest = ` · ${modelName} · ctrl+o for help`;
5995
6591
  const title = fit(`free-pi${titleRest}`, () => `${theme.bold(theme.fg("accent", "free-pi"))}${theme.fg("dim", titleRest)}`);
5996
6592
  const commandLines = COMMANDS.map(({ name, description }) => {
@@ -6054,7 +6650,7 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
6054
6650
 
6055
6651
  // src/resume-line.ts
6056
6652
  var RESUME_LABEL = "To resume this session:";
6057
- var RESUME_COMMAND_RE = /(To resume this session:(?:\u001b\[[0-9;]*m|\s)*)pi(?=\s)/g;
6653
+ var RESUME_COMMAND_RE = /(To resume this session:(?:\u001b\[[0-9;]*m|\s)*)pi(?=\s)( --session-dir (?:'(?:[^']|'\\'')*'|\S+))?/g;
6058
6654
  function rewriteResumeLine(chunk) {
6059
6655
  if (!chunk.includes(RESUME_LABEL))
6060
6656
  return chunk;
@@ -6072,6 +6668,48 @@ function patchResumeLine(stream) {
6072
6668
  };
6073
6669
  }
6074
6670
 
6671
+ // src/project-trust.ts
6672
+ import { createInterface } from "node:readline/promises";
6673
+ import { resolve } from "node:path";
6674
+ import { hasTrustRequiringProjectResources, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
6675
+ var PROJECT_TRUST_PROMPT = "This folder has project settings for free-pi (.pi/ or .agents/skills), which can run code such as MCP servers and extensions. Trust this folder? [y/N] ";
6676
+ async function decideProjectTrust(cwd, agentDir, deps) {
6677
+ if (!(deps.hasTrustRequiringResources ?? hasTrustRequiringProjectResources)(cwd))
6678
+ return true;
6679
+ const store = new ProjectTrustStore(agentDir);
6680
+ const saved = store.get(cwd);
6681
+ if (typeof saved === "boolean")
6682
+ return saved;
6683
+ if (!deps.stdinIsTTY) {
6684
+ deps.log("This folder has project settings (.pi/ or .agents/skills); they are ignored because it is not trusted.");
6685
+ return false;
6686
+ }
6687
+ const decision = /^y(es)?$/i.test((await deps.askLine(PROJECT_TRUST_PROMPT)).trim());
6688
+ store.set(cwd, decision);
6689
+ return decision;
6690
+ }
6691
+ function decideProjectTrustInteractive(cwd, agentDir) {
6692
+ return decideProjectTrust(cwd, agentDir, {
6693
+ stdinIsTTY: process.stdin.isTTY === true,
6694
+ log: (message) => console.log(message),
6695
+ askLine: async (prompt) => {
6696
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
6697
+ try {
6698
+ return await rl.question(prompt);
6699
+ } finally {
6700
+ rl.close();
6701
+ }
6702
+ }
6703
+ });
6704
+ }
6705
+ function projectTrustForCwd(cwd, launch, agentDir, hasTrustRequiringResources = hasTrustRequiringProjectResources) {
6706
+ if (resolve(cwd) === resolve(launch.cwd))
6707
+ return launch.trusted;
6708
+ if (!hasTrustRequiringResources(cwd))
6709
+ return true;
6710
+ return new ProjectTrustStore(agentDir).get(cwd) === true;
6711
+ }
6712
+
6075
6713
  // src/pi-launch.ts
6076
6714
  function catalogModelsFor(opts) {
6077
6715
  if (opts.models && opts.models.length > 0)
@@ -6079,11 +6717,42 @@ function catalogModelsFor(opts) {
6079
6717
  const id = opts.model || MODEL_ID;
6080
6718
  return [{ id, name: id }];
6081
6719
  }
6082
- var ALLOWED_TOOL_NAMES = [...SAFE_TOOLS, USAGE_TOOL_NAME, BUY_TOOL_NAME, DOCS_TOOL_NAME];
6720
+ var ALLOWED_TOOL_NAMES = [
6721
+ ...SAFE_TOOLS,
6722
+ USAGE_TOOL_NAME,
6723
+ BUY_TOOL_NAME,
6724
+ DOCS_TOOL_NAME,
6725
+ TODO_TOOL_NAME,
6726
+ "codemode",
6727
+ "tool_search"
6728
+ ];
6729
+ var SDK_TOOLS = [
6730
+ ...ALLOWED_TOOL_NAMES,
6731
+ "mcp__*",
6732
+ "list_mcp_resources",
6733
+ "list_mcp_resource_templates",
6734
+ "read_mcp_resource"
6735
+ ];
6083
6736
  function resolveModelName(opts) {
6084
6737
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
6085
6738
  }
6739
+ function buildSettingsManager(opts, models, projectTrusted) {
6740
+ return SettingsManager.inMemory({
6741
+ defaultProvider: PROVIDER_NAME,
6742
+ defaultModel: models[0].id,
6743
+ defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
6744
+ packages: [],
6745
+ quietStartup: true,
6746
+ tuiMode: "regular",
6747
+ theme: "light/dark",
6748
+ enableInstallTelemetry: false
6749
+ }, { projectTrusted });
6750
+ }
6751
+ function settingsForRuntime(opts, rtCwd, launch) {
6752
+ return buildSettingsManager(opts, catalogModelsFor(opts), projectTrustForCwd(rtCwd, launch, opts.agentDir));
6753
+ }
6086
6754
  function buildRuntimeOptions(opts, sessionId) {
6755
+ process.env.PI_CODING_AGENT_DIR = opts.agentDir;
6087
6756
  const models = catalogModelsFor(opts);
6088
6757
  const providerExtension = {
6089
6758
  name: "free-pi-provider",
@@ -6124,6 +6793,10 @@ function buildRuntimeOptions(opts, sessionId) {
6124
6793
  modelName: resolveModelName(opts),
6125
6794
  updateLatest: opts.updateLatest
6126
6795
  });
6796
+ const todoToolExtension = createTodoToolExtension();
6797
+ const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
6798
+ const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
6799
+ const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
6127
6800
  const extensionFactories = [
6128
6801
  providerExtension,
6129
6802
  adsExtension,
@@ -6133,26 +6806,24 @@ function buildRuntimeOptions(opts, sessionId) {
6133
6806
  errorNoticeExtension,
6134
6807
  toolGuardExtension,
6135
6808
  commandsExtension,
6136
- headerExtension
6809
+ headerExtension,
6810
+ todoToolExtension,
6811
+ mcpExtension,
6812
+ codemodeExtension,
6813
+ toolSearchExtension
6137
6814
  ];
6138
- const settingsManager = SettingsManager.inMemory({
6139
- defaultProvider: PROVIDER_NAME,
6140
- defaultModel: models[0].id,
6141
- defaultTools: [...SAFE_TOOLS],
6142
- packages: [],
6143
- quietStartup: true
6144
- });
6815
+ const settingsManager = buildSettingsManager(opts, models, opts.projectTrusted === true);
6145
6816
  return {
6146
6817
  settingsManager,
6147
6818
  resourceLoaderOptions: { extensionFactories },
6148
6819
  extensionNames: extensionFactories.map((e) => e.name),
6149
- tools: ALLOWED_TOOL_NAMES
6820
+ tools: SDK_TOOLS
6150
6821
  };
6151
6822
  }
6152
6823
  function getSessionDir(cwd, agentDir) {
6153
- const resolvedCwd = resolve(cwd);
6824
+ const resolvedCwd = resolve2(cwd);
6154
6825
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
6155
- return join2(resolve(agentDir), "sessions", safePath);
6826
+ return join3(resolve2(agentDir), "sessions", safePath);
6156
6827
  }
6157
6828
  async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager) {
6158
6829
  if (sessionId) {
@@ -6166,6 +6837,7 @@ async function createSessionManager(cwd, sessionDir, sessionId, api = SessionMan
6166
6837
  return api.create(cwd, sessionDir);
6167
6838
  }
6168
6839
  process.env.PI_SKIP_VERSION_CHECK = "1";
6840
+ process.env.PI_TELEMETRY = "0";
6169
6841
  patchResumeLine(process.stdout);
6170
6842
  async function launchPi(opts) {
6171
6843
  const cwd = process.cwd();
@@ -6173,7 +6845,9 @@ async function launchPi(opts) {
6173
6845
  const sm = await createSessionManager(cwd, sessionDir, opts.session);
6174
6846
  const sessionId = sm.getSessionId();
6175
6847
  const scopeIds = catalogModelsFor(opts).map((m) => m.id);
6176
- const { settingsManager, resourceLoaderOptions, tools } = buildRuntimeOptions(opts, sessionId);
6848
+ const projectTrusted = await decideProjectTrustInteractive(cwd, opts.agentDir);
6849
+ const { resourceLoaderOptions, tools } = buildRuntimeOptions({ ...opts, projectTrusted }, sessionId);
6850
+ const launchTrust = { cwd, trusted: projectTrusted };
6177
6851
  const createRuntime = async ({
6178
6852
  cwd: rtCwd,
6179
6853
  sessionManager,
@@ -6182,7 +6856,7 @@ async function launchPi(opts) {
6182
6856
  const services = await createAgentSessionServices({
6183
6857
  cwd: rtCwd,
6184
6858
  agentDir: opts.agentDir,
6185
- settingsManager,
6859
+ settingsManager: settingsForRuntime(opts, rtCwd, launchTrust),
6186
6860
  resourceLoaderOptions
6187
6861
  });
6188
6862
  const scopedModels = resolveFreePiScope(services.modelRuntime, PROVIDER_NAME, scopeIds);
@@ -6207,20 +6881,22 @@ async function launchPi(opts) {
6207
6881
  initialImages: [],
6208
6882
  initialMessages: []
6209
6883
  });
6884
+ installBugHandler(mode, { baseUrl: opts.baseUrl, getToken: () => opts.jwt });
6210
6885
  await mode.run();
6211
6886
  return sm.getSessionId();
6212
6887
  }
6213
6888
 
6214
6889
  // src/prompt.ts
6215
- import { createInterface } from "node:readline/promises";
6890
+ import { createInterface as createInterface2 } from "node:readline/promises";
6216
6891
 
6217
6892
  // src/consent.ts
6218
6893
  var CONSENT_TEXT = `
6219
6894
  free-pi — before you start
6220
6895
 
6221
6896
  * Inference is free. Ads pay for it, and so does training.
6222
- * Ad cards render in the terminal UI. Ad content never enters the model's
6223
- context.
6897
+ * Ad cards render in the terminal UI. Some ads come from a third-party ad
6898
+ provider (Gravity), which receives excerpts of your recent conversation
6899
+ to pick a relevant ad.
6224
6900
  * Your coding sessions (prompts, code, files, and model responses) are saved
6225
6901
  as traces and used to train models. We may share or sell that data to
6226
6902
  third parties. Once you accept, there is no opt-out, except where the law
@@ -6239,7 +6915,7 @@ and the Privacy Policy (https://freepi.ai/privacy).
6239
6915
 
6240
6916
  Decline and nothing is sent to the free-pi server.
6241
6917
  `.trim();
6242
- var CONSENT_VERSION = "2";
6918
+ var CONSENT_VERSION = "3";
6243
6919
  var NON_INTERACTIVE_CONSENT_TEXT = "free-pi first-run consent needs an interactive terminal (stdin is not a TTY). " + "Run `npx free-pi-cli` in a terminal once to review and accept; " + "after that, non-interactive runs work.";
6244
6920
  var CONSENT_PROMPT = "Accept? [y/N] ";
6245
6921
  function parseConsentAnswer(answer) {
@@ -6261,7 +6937,7 @@ async function promptConsentInteractive() {
6261
6937
  stdinIsTTY: process.stdin.isTTY === true,
6262
6938
  log: (message) => console.log(message),
6263
6939
  askLine: async (prompt) => {
6264
- const rl = createInterface({ input: process.stdin, output: process.stdout });
6940
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
6265
6941
  try {
6266
6942
  return await rl.question(prompt);
6267
6943
  } finally {
@@ -6396,12 +7072,12 @@ ${INTRO_TEXT}
6396
7072
  }
6397
7073
 
6398
7074
  // src/whats-new.ts
6399
- import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
6400
- import { dirname as dirname2 } from "node:path";
7075
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
7076
+ import { dirname as dirname3 } from "node:path";
6401
7077
  function maybeWhatsNew(cliVersion, agentDir) {
6402
7078
  try {
6403
7079
  const path = getLastSeenVersionPath(agentDir);
6404
- const lastSeen = existsSync(path) ? readFileSync2(path, "utf-8").trim() : undefined;
7080
+ const lastSeen = existsSync2(path) ? readFileSync2(path, "utf-8").trim() : undefined;
6405
7081
  writeLastSeen(path, cliVersion);
6406
7082
  if (lastSeen === undefined)
6407
7083
  return;
@@ -6417,7 +7093,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
6417
7093
  }
6418
7094
  }
6419
7095
  function writeLastSeen(path, version) {
6420
- mkdirSync(dirname2(path), { recursive: true });
7096
+ mkdirSync(dirname3(path), { recursive: true });
6421
7097
  writeFileSync(path, version, "utf-8");
6422
7098
  }
6423
7099
 
@@ -6606,6 +7282,15 @@ ${HELP_TEXT}`);
6606
7282
  });
6607
7283
  process.exit(logoutCode);
6608
7284
  }
7285
+ if (command.kind === "mcp") {
7286
+ const mcpCode = await runFreePiMcp(command.args, {
7287
+ cwd: process.cwd(),
7288
+ agentDir: getFreePiAgentDir(),
7289
+ log: (line) => console.log(line),
7290
+ error: (line) => console.error(line)
7291
+ });
7292
+ process.exit(mcpCode);
7293
+ }
6609
7294
  const agentDir = getFreePiAgentDir();
6610
7295
  const code = await run({
6611
7296
  baseUrl: resolveBaseUrl(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.2.19",
3
+ "version": "0.4.0",
4
4
  "description": "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "prepublishOnly": "bun run build"
20
20
  },
21
21
  "dependencies": {
22
- "@earendil-works/pi-coding-agent": "0.84.4"
22
+ "@earendil-works/pi-coding-agent": "1.2.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@freepi/pi-ads": "workspace:*",