free-pi-cli 0.3.0 → 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-376chsck.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.3.0",
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",
@@ -83,6 +83,8 @@ Usage:
83
83
  free-pi-cli start the coding agent (first run: consent + GitHub sign-in)
84
84
  free-pi-cli --session <id> resume a previous session
85
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)
86
88
  free-pi-cli --version print the CLI version
87
89
  free-pi-cli --help show this help
88
90
 
@@ -97,6 +99,8 @@ function parseCliArgs(argv) {
97
99
  const [first, ...rest] = argv;
98
100
  if (first === undefined)
99
101
  return { kind: "run" };
102
+ if (first === "mcp")
103
+ return { kind: "mcp", args: rest };
100
104
  if (first === "--session") {
101
105
  return rest.length > 1 ? { kind: "unknown", arg: rest[1] } : { kind: "run" };
102
106
  }
@@ -163,21 +167,58 @@ async function runLogout(deps) {
163
167
  return 0;
164
168
  }
165
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
+
166
207
  // src/paths.ts
167
208
  import { homedir } from "node:os";
168
- import { join } from "node:path";
209
+ import { join as join2 } from "node:path";
169
210
  var FREE_PI_DIR_NAME = ".free-pi";
170
211
  function getFreePiAgentDir() {
171
- return join(homedir(), FREE_PI_DIR_NAME, "agent");
212
+ return join2(homedir(), FREE_PI_DIR_NAME, "agent");
172
213
  }
173
214
  function getCredentialsPath(agentDir = getFreePiAgentDir()) {
174
- return join(agentDir, "credentials.json");
215
+ return join2(agentDir, "credentials.json");
175
216
  }
176
217
  function getLastSeenVersionPath(agentDir = getFreePiAgentDir()) {
177
- return join(agentDir, "last-seen-version");
218
+ return join2(agentDir, "last-seen-version");
178
219
  }
179
220
  function getConfigPath() {
180
- return join(homedir(), FREE_PI_DIR_NAME, "config.json");
221
+ return join2(homedir(), FREE_PI_DIR_NAME, "config.json");
181
222
  }
182
223
 
183
224
  // src/pi-launch.ts
@@ -185,6 +226,9 @@ import {
185
226
  createAgentSessionFromServices,
186
227
  createAgentSessionRuntime,
187
228
  createAgentSessionServices,
229
+ createCodemodeExtension,
230
+ createMcpExtension,
231
+ createToolSearchExtension,
188
232
  InteractiveMode,
189
233
  SessionManager,
190
234
  SettingsManager
@@ -5290,7 +5334,7 @@ function createAdsExtension(opts) {
5290
5334
  }
5291
5335
 
5292
5336
  // src/pi-launch.ts
5293
- import { join as join2, resolve } from "node:path";
5337
+ import { join as join3, resolve as resolve2 } from "node:path";
5294
5338
 
5295
5339
  // src/provider.ts
5296
5340
  var PROVIDER_NAME = "free-pi";
@@ -5328,15 +5372,38 @@ var Metrics = {
5328
5372
  update: 0
5329
5373
  };
5330
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
+ }
5331
5384
  function IsNull(value) {
5332
5385
  return IsEqual(value, null);
5333
5386
  }
5387
+ function IsNumber(value) {
5388
+ return Number.isFinite(value);
5389
+ }
5334
5390
  function IsObject(value) {
5335
5391
  return IsEqual(typeof value, "object") && !IsNull(value);
5336
5392
  }
5393
+ function IsString(value) {
5394
+ return IsEqual(typeof value, "string");
5395
+ }
5337
5396
  function IsEqual(left, right) {
5338
5397
  return left === right;
5339
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
+ }
5340
5407
  function IsUnsafePropertyKey(key) {
5341
5408
  return IsEqual(key, "__proto__") || IsEqual(key, "constructor") || IsEqual(key, "prototype");
5342
5409
  }
@@ -5346,6 +5413,81 @@ function HasPropertyKey(value, key) {
5346
5413
  function Keys(value) {
5347
5414
  return Object.getOwnPropertyNames(value);
5348
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
+ }
5349
5491
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/system/settings/settings.mjs
5350
5492
  var settings = {
5351
5493
  immutableTypes: false,
@@ -5382,12 +5524,52 @@ function Create(hidden, enumerable, options = {}) {
5382
5524
  const withHidden = settings.enumerableKind ? Merge(withOptions, hidden) : MergeHidden(withOptions, hidden);
5383
5525
  return settings.immutableTypes ? Object.freeze(withHidden) : withHidden;
5384
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
+ }
5385
5550
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/schema.mjs
5386
5551
  function IsSchema(value) {
5387
5552
  return IsObject(value);
5388
5553
  }
5389
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
+
5390
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
+ }
5391
5573
  function IsOptional(value) {
5392
5574
  return IsSchema(value) && HasPropertyKey(value, "~optional");
5393
5575
  }
@@ -5430,10 +5612,41 @@ var F64Out = new Uint8Array(F64.buffer);
5430
5612
  var encoder = new TextEncoder;
5431
5613
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/integer.mjs
5432
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
+ }
5433
5635
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/number.mjs
5434
5636
  var NumberPattern = "-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?";
5637
+ function Number2(options) {
5638
+ return Create({ "~kind": "Number" }, { type: "number" }, options);
5639
+ }
5435
5640
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/string.mjs
5436
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
+ }
5437
5650
 
5438
5651
  // ../../node_modules/.bun/typebox@1.3.16/node_modules/typebox/build/type/types/record.mjs
5439
5652
  var IntegerKey = `^${IntegerPattern}$`;
@@ -5617,6 +5830,33 @@ first. Purchased credits cover use beyond it and do not expire.
5617
5830
  - \`/close-other-session\` — free a stuck session on another machine
5618
5831
  - \`/update\` — get the latest free-pi
5619
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.
5620
5860
 
5621
5861
  ## Support
5622
5862
  Today's sponsor keeps free-pi free. When the user asks how to help the
@@ -5648,6 +5888,257 @@ function createDocsToolExtension() {
5648
5888
  };
5649
5889
  }
5650
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
+
5651
6142
  // src/error-notice.ts
5652
6143
  var NOTICE_DEDUPE_MS = 15000;
5653
6144
  function parseErrorEnvelope(errorMessage) {
@@ -5703,12 +6194,16 @@ async function reportToolGuardTrigger(opts, toolName) {
5703
6194
  });
5704
6195
  } catch {}
5705
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
+ }
5706
6201
  function createToolGuardExtension(opts) {
5707
6202
  return {
5708
6203
  name: "free-pi-tool-guard",
5709
6204
  factory(pi) {
5710
6205
  pi.on("tool_call", (event) => {
5711
- if (opts.allowedTools.includes(event.toolName))
6206
+ if (isAllowed(opts, event.toolName))
5712
6207
  return;
5713
6208
  reportToolGuardTrigger(opts, event.toolName);
5714
6209
  return { block: true, reason: "tool not permitted in the free-pi distro" };
@@ -5786,6 +6281,13 @@ function registerBugAutocomplete(pi) {
5786
6281
 
5787
6282
  // src/changelog.ts
5788
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
+ },
5789
6291
  {
5790
6292
  version: "0.3.0",
5791
6293
  highlights: [
@@ -6068,12 +6570,12 @@ function updateBannerLines(theme, latest, width) {
6068
6570
  return [
6069
6571
  "",
6070
6572
  border,
6071
- theme.bold(theme.fg("warning", truncate2(headline, inner))),
6072
- theme.fg("dim", truncate2(detail, inner)),
6573
+ theme.bold(theme.fg("warning", truncate3(headline, inner))),
6574
+ theme.fg("dim", truncate3(detail, inner)),
6073
6575
  border
6074
6576
  ];
6075
6577
  }
6076
- function truncate2(text, maxWidth) {
6578
+ function truncate3(text, maxWidth) {
6077
6579
  if (maxWidth <= 0)
6078
6580
  return "";
6079
6581
  if (text.length <= maxWidth)
@@ -6084,7 +6586,7 @@ function truncate2(text, maxWidth) {
6084
6586
  }
6085
6587
  function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest) {
6086
6588
  const inner = width - 1;
6087
- 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));
6088
6590
  const titleRest = ` · ${modelName} · ctrl+o for help`;
6089
6591
  const title = fit(`free-pi${titleRest}`, () => `${theme.bold(theme.fg("accent", "free-pi"))}${theme.fg("dim", titleRest)}`);
6090
6592
  const commandLines = COMMANDS.map(({ name, description }) => {
@@ -6166,6 +6668,48 @@ function patchResumeLine(stream) {
6166
6668
  };
6167
6669
  }
6168
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
+
6169
6713
  // src/pi-launch.ts
6170
6714
  function catalogModelsFor(opts) {
6171
6715
  if (opts.models && opts.models.length > 0)
@@ -6173,11 +6717,42 @@ function catalogModelsFor(opts) {
6173
6717
  const id = opts.model || MODEL_ID;
6174
6718
  return [{ id, name: id }];
6175
6719
  }
6176
- 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
+ ];
6177
6736
  function resolveModelName(opts) {
6178
6737
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
6179
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
+ }
6180
6754
  function buildRuntimeOptions(opts, sessionId) {
6755
+ process.env.PI_CODING_AGENT_DIR = opts.agentDir;
6181
6756
  const models = catalogModelsFor(opts);
6182
6757
  const providerExtension = {
6183
6758
  name: "free-pi-provider",
@@ -6218,6 +6793,10 @@ function buildRuntimeOptions(opts, sessionId) {
6218
6793
  modelName: resolveModelName(opts),
6219
6794
  updateLatest: opts.updateLatest
6220
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() };
6221
6800
  const extensionFactories = [
6222
6801
  providerExtension,
6223
6802
  adsExtension,
@@ -6227,29 +6806,24 @@ function buildRuntimeOptions(opts, sessionId) {
6227
6806
  errorNoticeExtension,
6228
6807
  toolGuardExtension,
6229
6808
  commandsExtension,
6230
- headerExtension
6809
+ headerExtension,
6810
+ todoToolExtension,
6811
+ mcpExtension,
6812
+ codemodeExtension,
6813
+ toolSearchExtension
6231
6814
  ];
6232
- const settingsManager = SettingsManager.inMemory({
6233
- defaultProvider: PROVIDER_NAME,
6234
- defaultModel: models[0].id,
6235
- defaultTools: [...SAFE_TOOLS],
6236
- packages: [],
6237
- quietStartup: true,
6238
- tuiMode: "regular",
6239
- theme: "light/dark",
6240
- enableInstallTelemetry: false
6241
- });
6815
+ const settingsManager = buildSettingsManager(opts, models, opts.projectTrusted === true);
6242
6816
  return {
6243
6817
  settingsManager,
6244
6818
  resourceLoaderOptions: { extensionFactories },
6245
6819
  extensionNames: extensionFactories.map((e) => e.name),
6246
- tools: ALLOWED_TOOL_NAMES
6820
+ tools: SDK_TOOLS
6247
6821
  };
6248
6822
  }
6249
6823
  function getSessionDir(cwd, agentDir) {
6250
- const resolvedCwd = resolve(cwd);
6824
+ const resolvedCwd = resolve2(cwd);
6251
6825
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
6252
- return join2(resolve(agentDir), "sessions", safePath);
6826
+ return join3(resolve2(agentDir), "sessions", safePath);
6253
6827
  }
6254
6828
  async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager) {
6255
6829
  if (sessionId) {
@@ -6271,7 +6845,9 @@ async function launchPi(opts) {
6271
6845
  const sm = await createSessionManager(cwd, sessionDir, opts.session);
6272
6846
  const sessionId = sm.getSessionId();
6273
6847
  const scopeIds = catalogModelsFor(opts).map((m) => m.id);
6274
- 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 };
6275
6851
  const createRuntime = async ({
6276
6852
  cwd: rtCwd,
6277
6853
  sessionManager,
@@ -6280,7 +6856,7 @@ async function launchPi(opts) {
6280
6856
  const services = await createAgentSessionServices({
6281
6857
  cwd: rtCwd,
6282
6858
  agentDir: opts.agentDir,
6283
- settingsManager,
6859
+ settingsManager: settingsForRuntime(opts, rtCwd, launchTrust),
6284
6860
  resourceLoaderOptions
6285
6861
  });
6286
6862
  const scopedModels = resolveFreePiScope(services.modelRuntime, PROVIDER_NAME, scopeIds);
@@ -6311,7 +6887,7 @@ async function launchPi(opts) {
6311
6887
  }
6312
6888
 
6313
6889
  // src/prompt.ts
6314
- import { createInterface } from "node:readline/promises";
6890
+ import { createInterface as createInterface2 } from "node:readline/promises";
6315
6891
 
6316
6892
  // src/consent.ts
6317
6893
  var CONSENT_TEXT = `
@@ -6361,7 +6937,7 @@ async function promptConsentInteractive() {
6361
6937
  stdinIsTTY: process.stdin.isTTY === true,
6362
6938
  log: (message) => console.log(message),
6363
6939
  askLine: async (prompt) => {
6364
- const rl = createInterface({ input: process.stdin, output: process.stdout });
6940
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
6365
6941
  try {
6366
6942
  return await rl.question(prompt);
6367
6943
  } finally {
@@ -6496,12 +7072,12 @@ ${INTRO_TEXT}
6496
7072
  }
6497
7073
 
6498
7074
  // src/whats-new.ts
6499
- import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
6500
- 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";
6501
7077
  function maybeWhatsNew(cliVersion, agentDir) {
6502
7078
  try {
6503
7079
  const path = getLastSeenVersionPath(agentDir);
6504
- const lastSeen = existsSync(path) ? readFileSync2(path, "utf-8").trim() : undefined;
7080
+ const lastSeen = existsSync2(path) ? readFileSync2(path, "utf-8").trim() : undefined;
6505
7081
  writeLastSeen(path, cliVersion);
6506
7082
  if (lastSeen === undefined)
6507
7083
  return;
@@ -6517,7 +7093,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
6517
7093
  }
6518
7094
  }
6519
7095
  function writeLastSeen(path, version) {
6520
- mkdirSync(dirname2(path), { recursive: true });
7096
+ mkdirSync(dirname3(path), { recursive: true });
6521
7097
  writeFileSync(path, version, "utf-8");
6522
7098
  }
6523
7099
 
@@ -6706,6 +7282,15 @@ ${HELP_TEXT}`);
6706
7282
  });
6707
7283
  process.exit(logoutCode);
6708
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
+ }
6709
7294
  const agentDir = getFreePiAgentDir();
6710
7295
  const code = await run({
6711
7296
  baseUrl: resolveBaseUrl(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.3.0",
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",