pi-feats 0.1.4 → 0.1.7

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.
Files changed (60) hide show
  1. package/README.md +90 -9
  2. package/extensions/api-server/README.md +21 -1
  3. package/extensions/api-server/application-log-store.ts +4 -2
  4. package/extensions/api-server/application-runtime.ts +79 -8
  5. package/extensions/api-server/application-store.ts +23 -10
  6. package/extensions/api-server/index.ts +6 -2
  7. package/extensions/api-server/profile-store.ts +109 -39
  8. package/extensions/api-server/server.ts +220 -44
  9. package/extensions/cli-resources.ts +144 -113
  10. package/extensions/cli-tui-mode.ts +7 -0
  11. package/extensions/context-memory/index.ts +6 -1
  12. package/extensions/conversation-search/index.ts +42 -0
  13. package/extensions/guardrails/index.ts +2 -1
  14. package/extensions/lib/application-context.ts +2 -0
  15. package/extensions/lib/builtin-tools.ts +24 -0
  16. package/extensions/lib/cli-tui-mode.ts +72 -0
  17. package/extensions/lib/context-memory.ts +6 -1
  18. package/extensions/lib/conversation-search.ts +33 -0
  19. package/extensions/lib/package-resources.ts +58 -0
  20. package/extensions/lib/pi-command.ts +11 -0
  21. package/extensions/lib/profile-sandbox.ts +14 -3
  22. package/extensions/lib/remote-hosts.ts +7 -5
  23. package/extensions/lib/workflow-evaluator.ts +113 -0
  24. package/extensions/lib/workflow-harness.ts +409 -43
  25. package/extensions/pi-console-webui/app/[section]/page.tsx +1 -1
  26. package/extensions/pi-console-webui/app/api/admin/models/route.ts +22 -0
  27. package/extensions/pi-console-webui/app/globals.css +3 -0
  28. package/extensions/pi-console-webui/components/application-handler-editor.tsx +1 -1
  29. package/extensions/pi-console-webui/components/application-logs.tsx +4 -4
  30. package/extensions/pi-console-webui/components/application-mappings.tsx +10 -12
  31. package/extensions/pi-console-webui/components/application-sessions.tsx +2 -2
  32. package/extensions/pi-console-webui/components/application-settings.tsx +12 -6
  33. package/extensions/pi-console-webui/components/chat-workspace.tsx +17 -7
  34. package/extensions/pi-console-webui/components/console-page.tsx +4 -0
  35. package/extensions/pi-console-webui/components/console-state.tsx +1 -1
  36. package/extensions/pi-console-webui/components/console.tsx +32 -11
  37. package/extensions/pi-console-webui/components/guardrails-panel.tsx +2 -2
  38. package/extensions/pi-console-webui/components/models-config-form.tsx +31 -0
  39. package/extensions/pi-console-webui/components/profile-model-form.tsx +54 -0
  40. package/extensions/pi-console-webui/components/pulse-resources.tsx +7 -6
  41. package/extensions/pi-console-webui/components/sequential-workflow-config.tsx +32 -0
  42. package/extensions/pi-console-webui/components/skill-resources.tsx +4 -3
  43. package/extensions/pi-console-webui/components/ui/input.tsx +1 -1
  44. package/extensions/pi-console-webui/index.ts +2 -1
  45. package/extensions/pi-console-webui/lib/admin-models.ts +38 -0
  46. package/extensions/pi-console-webui/lib/page-metadata.ts +4 -1
  47. package/extensions/pi-console-webui/next.config.js +1 -0
  48. package/extensions/profile-ssh.ts +274 -0
  49. package/extensions/profiles.ts +69 -25
  50. package/extensions/pulse/index.ts +148 -16
  51. package/extensions/pulse/store.ts +132 -18
  52. package/extensions/sequential-workflow.ts +121 -79
  53. package/extensions/skill-sources/store.ts +29 -10
  54. package/package.json +21 -9
  55. package/scripts/build-console-webui.mjs +7 -0
  56. package/scripts/console-webui-cache.mjs +79 -0
  57. package/scripts/ensure-console-build.mjs +43 -0
  58. package/skills/profile-ssh/SKILL.md +65 -0
  59. package/skills/sequential-workflow/SKILL.md +4 -3
  60. package/themes/legacy-dark.json +85 -0
@@ -1,10 +1,14 @@
1
1
  import { SessionManager, type ExtensionAPI, type SessionInfo } from "@earendil-works/pi-coding-agent";
2
- import { Box, render, Text } from "ink";
3
- import React from "react";
4
2
  import { existsSync } from "node:fs";
5
3
  import { readFile, readdir, writeFile } from "node:fs/promises";
6
4
  import { basename, dirname, extname, join, relative, resolve } from "node:path";
7
5
  import { spawn } from "node:child_process";
6
+ import { DatabaseSync } from "node:sqlite";
7
+ import { BUILTIN_TOOLS, activeBuiltinTools, updatedBuiltinTools } from "./lib/builtin-tools.ts";
8
+ import { packageExtensions, type InstalledPackage } from "./lib/package-resources.ts";
9
+ import { withoutTuiModeArgs } from "./lib/cli-tui-mode.ts";
10
+ import { piCommand } from "./lib/pi-command.ts";
11
+ import { PulseStore, type PulseExecutionSession } from "./pulse/store.ts";
8
12
 
9
13
  type ListKind = "tools" | "skills" | "extensions";
10
14
  type Row = [string, string, string];
@@ -19,8 +23,6 @@ const kindFromArgs = (args: string[]): ListKind | undefined => {
19
23
  return undefined;
20
24
  };
21
25
 
22
- const BUILTIN_TOOLS = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"] as const;
23
-
24
26
  type Action = "disable" | "enable";
25
27
 
26
28
  interface ParsedAction {
@@ -64,61 +66,29 @@ const actionFromArgs = (args: string[]): ParsedAction | undefined => {
64
66
  const clean = (value: string) => value.replaceAll("\n", " ").replaceAll("\t", " ");
65
67
  const clip = (value: string, width: number) => value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}…`;
66
68
 
67
- function ResourceTable({ title, headers, rows, highlightStatus = false }: { title: string; headers: Row; rows: Row[]; highlightStatus?: boolean }) {
68
- const terminalWidth = Math.max(80, process.stdout.columns ?? 80);
69
- const available = terminalWidth - 10;
70
- const widths: [number, number, number] = [
71
- Math.max(12, Math.floor(available * 0.23)),
72
- Math.max(12, Math.floor(available * 0.18)),
73
- 0,
74
- ];
69
+ function ResourceTable({ title, headers, rows }: { title: string; headers: Row; rows: Row[]; highlightStatus?: boolean }): string {
70
+ const available = Math.max(80, process.stdout.columns ?? 80) - 10;
71
+ const widths: [number, number, number] = [Math.max(12, Math.floor(available * 0.23)), Math.max(12, Math.floor(available * 0.18)), 0];
75
72
  widths[2] = available - widths[0] - widths[1];
76
73
  const cell = (value: string, width: number) => clip(clean(value), width).padEnd(width);
77
74
  const line = `┼${"─".repeat(widths[0] + 2)}┼${"─".repeat(widths[1] + 2)}┼${"─".repeat(widths[2] + 2)}┼`;
78
- const top = line.replaceAll("┼", "┬").replace(/^┬/, "┌").replace(/┬$/, "┐");
79
- const bottom = line.replaceAll("┼", "┴").replace(/^┴/, "└").replace(/┴$/, "┘");
75
+ const top = line.replaceAll("┼", "┬").replace(/^┬/, "┌").replace(/┬$/, "┐"), bottom = line.replaceAll("┼", "┴").replace(/^┴/, "└").replace(/┴$/, "┘");
80
76
  const row = (values: Row) => `│ ${cell(values[0], widths[0])} │ ${cell(values[1], widths[1])} │ ${cell(values[2], widths[2])} │`;
81
-
82
- const renderedRow = (values: Row, index: number) => {
83
- const statusColor = values[1] === "enabled" ? "green" : "#f5c2d7";
84
- return React.createElement(
85
- Box,
86
- { flexDirection: "row", key: `${values.join("\0")}-${index}` },
87
- React.createElement(Text, { color: "gray" }, "│ "),
88
- React.createElement(Text, { color: "white" }, cell(values[0], widths[0])),
89
- React.createElement(Text, { color: "gray" }, " │ "),
90
- React.createElement(Text, { color: highlightStatus ? statusColor : "white" }, cell(values[1], widths[1])),
91
- React.createElement(Text, { color: "gray" }, " │ "),
92
- React.createElement(Text, { color: "white" }, cell(values[2], widths[2])),
93
- React.createElement(Text, { color: "gray" }, " │"),
94
- );
95
- };
96
-
97
- return React.createElement(
98
- Box,
99
- { flexDirection: "column" },
100
- React.createElement(Text, { color: "cyan", bold: true }, title),
101
- React.createElement(Text, { color: "gray" }, top),
102
- React.createElement(Text, { color: "cyan", bold: true }, row(headers)),
103
- React.createElement(Text, { color: "gray" }, line),
104
- ...rows.map(renderedRow),
105
- React.createElement(Text, { color: "gray" }, bottom),
106
- );
77
+ return [title, top, row(headers), line, ...rows.map(row), bottom].join("\n");
107
78
  }
108
79
 
109
80
  type SourceRow = [string, string, string, string];
110
- function SourceTable({ title, detailHeader, rows }: { title: string; detailHeader: string; rows: SourceRow[] }) {
81
+ function SourceTable({ title, detailHeader, rows }: { title: string; detailHeader: string; rows: SourceRow[] }): string {
111
82
  const available = Math.max(80, process.stdout.columns ?? 80) - 13, widths: [number, number, number, number] = [Math.max(14, Math.floor(available * .20)), 10, Math.max(18, Math.floor(available * .27)), 0];
112
83
  widths[3] = available - widths[0] - widths[1] - widths[2];
113
84
  const cell = (value: string, width: number) => clip(clean(value), width).padEnd(width);
114
85
  const line = `┼${"─".repeat(widths[0] + 2)}┼${"─".repeat(widths[1] + 2)}┼${"─".repeat(widths[2] + 2)}┼${"─".repeat(widths[3] + 2)}┼`;
115
86
  const top = line.replaceAll("┼", "┬").replace(/^┬/, "┌").replace(/┬$/, "┐"), bottom = line.replaceAll("┼", "┴").replace(/^┴/, "└").replace(/┴$/, "┘");
116
87
  const row = (values: SourceRow) => `│ ${cell(values[0], widths[0])} │ ${cell(values[1], widths[1])} │ ${cell(values[2], widths[2])} │ ${cell(values[3], widths[3])} │`;
117
- const renderedRow = (values: SourceRow, index: number) => React.createElement(Box, { flexDirection: "row", key: `${values.join("\0")}-${index}` }, React.createElement(Text, { color: "gray" }, "│ "), React.createElement(Text, { color: "white" }, cell(values[0], widths[0])), React.createElement(Text, { color: "gray" }, " │ "), React.createElement(Text, { color: values[1] === "enabled" ? "green" : "#f5c2d7" }, cell(values[1], widths[1])), React.createElement(Text, { color: "gray" }, " │ "), React.createElement(Text, { color: "white" }, cell(values[2], widths[2])), React.createElement(Text, { color: "gray" }, " │ "), React.createElement(Text, { color: "white" }, cell(values[3], widths[3])), React.createElement(Text, { color: "gray" }, " │"));
118
- return React.createElement(Box, { flexDirection: "column" }, React.createElement(Text, { color: "cyan", bold: true }, title), React.createElement(Text, { color: "gray" }, top), React.createElement(Text, { color: "cyan", bold: true }, row(["NAME", "STATUS", "SOURCE", detailHeader])), React.createElement(Text, { color: "gray" }, line), ...rows.map(renderedRow), React.createElement(Text, { color: "gray" }, bottom));
88
+ return [title, top, row(["NAME", "STATUS", "SOURCE", detailHeader]), line, ...rows.map(row), bottom].join("\n");
119
89
  }
120
90
 
121
- function SessionTable({ sessions }: { sessions: SessionInfo[] }) {
91
+ function SessionTable({ sessions }: { sessions: SessionInfo[] }): string {
122
92
  const terminalWidth = Math.max(80, process.stdout.columns ?? 80);
123
93
  const available = terminalWidth - 13;
124
94
  const widths = {
@@ -139,20 +109,17 @@ function SessionTable({ sessions }: { sessions: SessionInfo[] }) {
139
109
  hour12: false,
140
110
  }).format(date);
141
111
 
142
- return React.createElement(
143
- Box,
144
- { flexDirection: "column" },
145
- React.createElement(Text, { color: "cyan", bold: true }, "SESSIONS"),
146
- React.createElement(Text, { color: "gray" }, top),
147
- React.createElement(Text, { color: "cyan", bold: true }, row(["ID", "SESSION", "LAST ACTIVITY", "PATH"])),
148
- React.createElement(Text, { color: "gray" }, line),
149
- ...sessions.map((session) => React.createElement(
150
- Text,
151
- { color: "white", key: session.path },
152
- row([session.id, session.name ?? session.firstMessage, formatActivity(session.modified), session.path]),
153
- )),
154
- React.createElement(Text, { color: "gray" }, bottom),
155
- );
112
+ return ["SESSIONS", top, row(["ID", "SESSION", "LAST ACTIVITY", "PATH"]), line, ...sessions.map((session) => row([session.id, session.name ?? session.firstMessage, formatActivity(session.modified), session.path])), bottom].join("\n");
113
+ }
114
+
115
+ type PulseListedSession = SessionInfo & { source: "execution" | "injected-result"; pulse?: PulseExecutionSession };
116
+ function isPulseOnlySession(session: SessionInfo, sessionDir: string | undefined): boolean { const entries = SessionManager.open(session.path, sessionDir, session.cwd).getBranch() as Array<{ type?: string }>; const relevant = entries.filter((entry) => entry.type !== "session_info"); return relevant.length > 0 && relevant.some((entry) => entry.type === "pulse") && relevant.every((entry) => entry.type === "pulse"); }
117
+ function PulseSessionTable(sessions: PulseListedSession[]): string { if (!sessions.length) return "PULSE SESSIONS\nNo Pulse sessions found."; return `PULSE SESSIONS\nID SOURCE TYPE PULSE STATUS LAST ACTIVITY\n${sessions.map((session) => `${session.id} ${session.source} ${session.pulse?.pulseType ?? "—"} ${session.pulse?.pulseName ?? "Pulse result"} ${session.pulse?.status ?? "—"} ${session.modified.toISOString()}`).join("\n")}`; }
118
+
119
+ function ApplicationSessionTable(root: string, profile: string): string {
120
+ const path = join(root, "applications.db"); if (!existsSync(path)) return "";
121
+ const db = new DatabaseSync(path, { readOnly: true });
122
+ try { const rows = db.prepare("SELECT s.application_slug,s.session_prefix,s.session_id,s.session_type,s.message_count,s.updated_at FROM application_session_state st JOIN application_sessions s ON s.session_id=st.current_session_id WHERE st.profile=? ORDER BY s.updated_at DESC").all(profile) as Array<Record<string, unknown>>; if (!rows.length) return ""; const lines = rows.map((row) => `${String(row.session_id)} ${String(row.session_type)} ${Number(row.message_count)} ${String(row.session_type) === "stateless" ? "requests" : "messages"} ${String(row.application_slug)}/${String(row.session_prefix)}`); return `APPLICATION SESSIONS\nID TYPE COUNT ROUTE\n${lines.join("\n")}`; } finally { db.close(); }
156
123
  }
157
124
 
158
125
  function requestedProfile(): string | undefined {
@@ -178,7 +145,7 @@ async function readSettings() {
178
145
  }
179
146
 
180
147
  const isExcluded = (resourcePath: string, exclusions: string[]): boolean =>
181
- exclusions.includes(`!${resourcePath}`);
148
+ exclusions.includes(`-${resourcePath}`) || exclusions.includes(`!${resourcePath}`);
182
149
 
183
150
  async function findSkillFiles(path: string): Promise<string[]> {
184
151
  if (!existsSync(path)) return [];
@@ -195,15 +162,18 @@ async function findSkillFiles(path: string): Promise<string[]> {
195
162
  return files;
196
163
  }
197
164
 
198
- function skillEnabled(resourcePath: string, skillRoot: string, settings: Record<string, unknown>, exclusions: string[]): boolean {
199
- const policy = (settings.profile as { enabledSkills?: unknown } | undefined)?.enabledSkills;
165
+ export function skillEnabled(resourcePath: string, sharedSkillRoot: string, profileSkillRoot: string | undefined, settings: Record<string, unknown>, exclusions: string[]): boolean {
166
+ const isProfileSkill = profileSkillRoot !== undefined && isWithin(resourcePath, profileSkillRoot);
167
+ const skillRoot = isProfileSkill ? profileSkillRoot! : sharedSkillRoot;
168
+ const policyKey = isProfileSkill ? "enabledProfileSkills" : "enabledSkills";
169
+ const policy = (settings.profile as Record<string, unknown> | undefined)?.[policyKey];
200
170
  const relativeName = relative(skillRoot, resourcePath).replaceAll("\\", "/");
201
171
  const name = relativeName.startsWith("../") ? basename(resourcePath) : relativeName;
202
172
  if (Array.isArray(policy)) return policy.includes("*") || policy.includes(name);
203
173
  return !isExcluded(resourcePath, exclusions);
204
174
  }
205
175
 
206
- async function skillRows(paths: string[], skillRoot: string, settings: Record<string, unknown>, exclusions: string[]): Promise<Row[]> {
176
+ export async function skillRows(paths: string[], sharedSkillRoot: string, profileSkillRoot: string | undefined, settings: Record<string, unknown>, exclusions: string[]): Promise<Row[]> {
207
177
  const rows: Row[] = [];
208
178
  const seen = new Set<string>();
209
179
  for (const path of paths) {
@@ -211,35 +181,42 @@ async function skillRows(paths: string[], skillRoot: string, settings: Record<st
211
181
  if (seen.has(skillFile)) continue;
212
182
  seen.add(skillFile);
213
183
  const resourcePath = dirname(skillFile);
184
+ const skillRoot = profileSkillRoot !== undefined && isWithin(resourcePath, profileSkillRoot) ? profileSkillRoot : sharedSkillRoot;
214
185
  const name = relative(skillRoot, resourcePath).replaceAll("\\", "/");
215
- rows.push([name && !name.startsWith("..") ? name : basename(resourcePath), skillEnabled(resourcePath, skillRoot, settings, exclusions) ? "enabled" : "disabled", skillFile]);
186
+ rows.push([name && !name.startsWith("..") ? name : basename(resourcePath), skillEnabled(resourcePath, sharedSkillRoot, profileSkillRoot, settings, exclusions) ? "enabled" : "disabled", skillFile]);
216
187
  }
217
188
  }
218
189
  return rows.sort((a, b) => a[0].localeCompare(b[0]) || a[2].localeCompare(b[2]));
219
190
  }
220
191
 
221
- async function extensionRows(paths: string[], exclusions: string[], enabledExtensions?: string[]): Promise<Row[]> {
192
+ type ExtensionIdentity = { name: string; aliases: string[] };
193
+
194
+ export async function extensionRows(paths: string[], exclusions: string[], enabledExtensions?: string[], identitiesByPath = new Map<string, ExtensionIdentity>()): Promise<Row[]> {
222
195
  const rows: Row[] = [];
223
- const enabled = (name: string, path: string) => !enabledExtensions || enabledExtensions.includes("*") || enabledExtensions.includes(name) ? !isExcluded(path, exclusions) : false;
196
+ const enabled = (identity: ExtensionIdentity, path: string) => !enabledExtensions || enabledExtensions.includes("*") || identity.aliases.some((name) => enabledExtensions.includes(name)) ? !isExcluded(path, exclusions) : false;
224
197
  for (const path of paths) {
225
198
  if (!existsSync(path)) continue;
226
199
  if ([".ts", ".js"].includes(extname(path))) {
227
- rows.push([basename(path, extname(path)), enabled(basename(path, extname(path)), path) ? "enabled" : "disabled", path]);
200
+ const identity = identitiesByPath.get(path) ?? { name: basename(path, extname(path)), aliases: [basename(path, extname(path))] };
201
+ rows.push([identity.name, enabled(identity, path) ? "enabled" : "disabled", path]);
228
202
  continue;
229
203
  }
230
204
  // A configured directory with index.ts/index.js is one extension. Its
231
205
  // sibling source/config files are implementation details, not extensions.
232
206
  if (existsSync(join(path, "index.ts")) || existsSync(join(path, "index.js"))) {
233
- rows.push([basename(path), enabled(basename(path), path) ? "enabled" : "disabled", path]);
207
+ const identity = identitiesByPath.get(path) ?? { name: basename(path), aliases: [basename(path)] };
208
+ rows.push([identity.name, enabled(identity, path) ? "enabled" : "disabled", path]);
234
209
  continue;
235
210
  }
236
211
  for (const entry of await readdir(path, { withFileTypes: true })) {
237
212
  if (entry.isFile() && [".ts", ".js"].includes(extname(entry.name))) {
238
213
  const resourcePath = join(path, entry.name);
239
- rows.push([basename(entry.name, extname(entry.name)), enabled(basename(entry.name, extname(entry.name)), resourcePath) ? "enabled" : "disabled", resourcePath]);
214
+ const identity = identitiesByPath.get(resourcePath) ?? { name: basename(entry.name, extname(entry.name)), aliases: [basename(entry.name, extname(entry.name))] };
215
+ rows.push([identity.name, enabled(identity, resourcePath) ? "enabled" : "disabled", resourcePath]);
240
216
  } else if (entry.isDirectory() && (existsSync(join(path, entry.name, "index.ts")) || existsSync(join(path, entry.name, "index.js")))) {
241
217
  const resourcePath = join(path, entry.name);
242
- rows.push([entry.name, enabled(entry.name, resourcePath) ? "enabled" : "disabled", resourcePath]);
218
+ const identity = identitiesByPath.get(resourcePath) ?? { name: entry.name, aliases: [entry.name] };
219
+ rows.push([identity.name, enabled(identity, resourcePath) ? "enabled" : "disabled", resourcePath]);
243
220
  }
244
221
  }
245
222
  }
@@ -247,7 +224,7 @@ async function extensionRows(paths: string[], exclusions: string[], enabledExten
247
224
  }
248
225
 
249
226
  type PackageResourceKind = "extensions" | "skills";
250
- type ConfiguredPackage = { source: string; base: string; name: string; manifest?: { version?: string; description?: string; pi?: { extensions?: unknown; skills?: unknown } } };
227
+ type ConfiguredPackage = InstalledPackage & { source: string; manifest?: InstalledPackage["manifest"] & { version?: string; description?: string } };
251
228
 
252
229
  function packageBase(source: string, resourceRoot: string): string | undefined {
253
230
  if (source.startsWith("npm:")) return join(resourceRoot, "npm", "node_modules", source.slice(4));
@@ -276,8 +253,8 @@ async function configuredPackages(resourceRoot: string, settings: Record<string,
276
253
  return packages;
277
254
  }
278
255
 
279
- async function configuredPackageResources(resourceRoot: string, settings: Record<string, unknown>, kind: PackageResourceKind): Promise<Array<{ path: string; packageName: string }>> {
280
- const paths: Array<{ path: string; packageName: string }> = [];
256
+ async function configuredPackageResources(resourceRoot: string, settings: Record<string, unknown>, kind: PackageResourceKind): Promise<Array<{ path: string; packageName: string; name?: string }>> {
257
+ const paths: Array<{ path: string; packageName: string; name?: string }> = [];
281
258
  for (const pkg of await configuredPackages(resourceRoot, settings)) {
282
259
  const entries = pkg.manifest?.pi?.[kind];
283
260
  for (const entry of Array.isArray(entries) ? entries : []) {
@@ -288,7 +265,14 @@ async function configuredPackageResources(resourceRoot: string, settings: Record
288
265
  }
289
266
 
290
267
  async function configuredPackageExtensions(resourceRoot: string, settings: Record<string, unknown>) {
291
- return configuredPackageResources(resourceRoot, settings, "extensions");
268
+ return packageExtensions(await configuredPackages(resourceRoot, settings));
269
+ }
270
+
271
+ async function extensionIdentity(resourcePath: string, resourceRoot: string, settings: Record<string, unknown>): Promise<ExtensionIdentity> {
272
+ const packageExtension = (await configuredPackageExtensions(resourceRoot, settings)).find((entry) => entry.path === resourcePath);
273
+ if (packageExtension) return { name: packageExtension.name, aliases: packageExtension.aliases };
274
+ const name = basename(resourcePath, extname(resourcePath));
275
+ return { name, aliases: [name] };
292
276
  }
293
277
 
294
278
  async function packageToolSources(resourceRoot: string, settings: Record<string, unknown>, names: string[]): Promise<Map<string, string>> {
@@ -315,19 +299,41 @@ async function applyPackageAction(action: PackageAction) {
315
299
  console.log(`Package "${name}" ${action.action === "enable" ? "enabled" : "disabled"} in ${join(agentDir, "settings.json")}. Use /reload to apply.`);
316
300
  }
317
301
 
318
- async function renderAndClose(element: React.ReactElement) {
319
- const app = render(element, { stdout: process.stdout, stdin: process.stdin, exitOnCtrlC: false, patchConsole: false });
320
- await new Promise((resolveRender) => setTimeout(resolveRender, 25));
321
- app.unmount();
302
+ async function writeTable(table: string) {
303
+ await new Promise<void>((resolveWrite, reject) => process.stdout.write(`${table}\n`, (error) => error ? reject(error) : resolveWrite()));
322
304
  }
323
305
 
324
306
  function setExclusion(entries: string[], resourcePath: string, disable: boolean) {
325
- const exclusion = `!${resourcePath}`;
326
- if (disable) return entries.includes(exclusion) ? entries : [...entries, exclusion];
327
- return entries.filter((entry) => entry !== exclusion);
307
+ // Pi applies exact exclusions (`-path`) to resources discovered through a
308
+ // package manifest. `!path` is a glob exclusion and is retained only as a
309
+ // legacy value when reading existing settings.
310
+ const exactExclusion = `-${resourcePath}`;
311
+ const legacyGlobExclusion = `!${resourcePath}`;
312
+ if (disable) return [...entries.filter((entry) => entry !== legacyGlobExclusion && entry !== exactExclusion), exactExclusion];
313
+ return entries.filter((entry) => entry !== exactExclusion && entry !== legacyGlobExclusion);
314
+ }
315
+
316
+ export function updatedExtensionSettings(settings: Record<string, unknown>, resourcePath: string, identity: ExtensionIdentity, enable: boolean): Record<string, unknown> {
317
+ const entries = Array.isArray(settings.extensions) ? settings.extensions.filter((entry): entry is string => typeof entry === "string") : [];
318
+ const extensions = setExclusion(entries, resourcePath, !enable);
319
+ const updated: Record<string, unknown> = { ...settings };
320
+ if (extensions.length) updated.extensions = extensions;
321
+ else delete updated.extensions;
322
+
323
+ const profile = settings.profile;
324
+ const enabledExtensions = profile && typeof profile === "object" && Array.isArray((profile as Record<string, unknown>).enabledExtensions)
325
+ ? (profile as Record<string, unknown>).enabledExtensions.filter((entry): entry is string => typeof entry === "string")
326
+ : undefined;
327
+ if (enabledExtensions && !enabledExtensions.includes("*")) {
328
+ const selected = new Set(enabledExtensions);
329
+ for (const alias of identity.aliases) selected.delete(alias);
330
+ if (enable) selected.add(identity.name);
331
+ updated.profile = { ...(profile as Record<string, unknown>), enabledExtensions: [...selected].sort() };
332
+ }
333
+ return updated;
328
334
  }
329
335
 
330
- async function findResource(kind: "extensions" | "skills", target: string, settings: Record<string, unknown>, agentDir: string, resourceRoot: string) {
336
+ export async function findResource(kind: "extensions" | "skills", target: string, settings: Record<string, unknown>, agentDir: string, resourceRoot: string) {
331
337
  if (existsSync(target)) {
332
338
  if (kind === "skills" && (basename(target) === "SKILL.md" || existsSync(join(target, "SKILL.md")))) {
333
339
  return basename(target) === "SKILL.md" ? dirname(target) : target;
@@ -341,10 +347,9 @@ async function findResource(kind: "extensions" | "skills", target: string, setti
341
347
  const roots = [join(agentDir, kind), ...(kind === "skills" ? [join(resourceRoot, "skills")] : []), ...rawEntries.filter((entry) => !/^[!+\-]/.test(entry)), join(process.cwd(), ".pi", kind)];
342
348
  const name = target.replace(/^[!+\-]+/, "");
343
349
  if (kind === "extensions") {
344
- for (const entry of await configuredPackageExtensions(resourceRoot, settings)) {
345
- const extensionName = basename(entry.path, extname(entry.path));
346
- if (extensionName === name) return entry.path;
347
- }
350
+ const matches = (await configuredPackageExtensions(resourceRoot, settings)).filter((entry) => entry.aliases.includes(name));
351
+ if (matches.length === 1) return matches[0].path;
352
+ if (matches.length > 1) throw new Error(`Extension name \"${target}\" is ambiguous; use its path.`);
348
353
  } else {
349
354
  for (const entry of await configuredPackageResources(resourceRoot, settings, "skills")) {
350
355
  for (const skillFile of await findSkillFiles(entry.path)) {
@@ -430,20 +435,16 @@ async function applyAction(action: ParsedAction) {
430
435
  if (!(BUILTIN_TOOLS as readonly string[]).includes(action.target)) {
431
436
  throw new Error(`\"${action.target}\" não é uma tool nativa. Desabilite a extension que fornece essa tool.`);
432
437
  }
433
- const configured = Array.isArray(settings.defaultTools)
434
- ? settings.defaultTools.filter((entry): entry is string => typeof entry === "string")
435
- : [...BUILTIN_TOOLS];
436
- const updatedTools = enable
437
- ? [...new Set([...configured, action.target])]
438
- : configured.filter((tool) => tool !== action.target);
439
- if (configured.length === updatedTools.length && configured.every((tool, index) => tool === updatedTools[index])) {
438
+ const configured = activeBuiltinTools(settings);
439
+ const updatedTools = updatedBuiltinTools(settings, action.target, enable);
440
+ const effectiveUpdatedTools = updatedTools ?? activeBuiltinTools({});
441
+ if (configured.length === effectiveUpdatedTools.length && configured.every((tool, index) => tool === effectiveUpdatedTools[index])) {
440
442
  console.log(`Tool \"${action.target}\" já está ${enable ? "habilitada" : "desabilitada"}.`);
441
443
  return;
442
444
  }
443
- const updatedSettings: Record<string, unknown> = { ...settings, defaultTools: updatedTools };
444
- if (updatedTools.length === BUILTIN_TOOLS.length && BUILTIN_TOOLS.every((tool) => updatedTools.includes(tool))) {
445
- delete updatedSettings.defaultTools;
446
- }
445
+ const updatedSettings: Record<string, unknown> = { ...settings };
446
+ if (updatedTools) updatedSettings.defaultTools = updatedTools;
447
+ else delete updatedSettings.defaultTools;
447
448
  await writeFile(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
448
449
  console.log(`Tool \"${action.target}\" ${enable ? "habilitada" : "desabilitada"} em ${settingsPath}. Use /reload para aplicar.`);
449
450
  return;
@@ -452,6 +453,17 @@ async function applyAction(action: ParsedAction) {
452
453
  const resourcePath = await findResource(action.kind, action.target, settings, agentDir, resourceRoot);
453
454
  if (!resourcePath) throw new Error(`${action.kind.slice(0, -1)} \"${action.target}\" não encontrada.`);
454
455
  if (action.kind === "skills" && await applyProfileSkillAction(action, resourcePath, settings, agentDir, resourceRoot, settingsPath)) return;
456
+ if (action.kind === "extensions") {
457
+ const identity = await extensionIdentity(resourcePath, resourceRoot, settings);
458
+ const updatedSettings = updatedExtensionSettings(settings, resourcePath, identity, enable);
459
+ if (JSON.stringify(settings) === JSON.stringify(updatedSettings)) {
460
+ console.log(`Extension \"${action.target}\" is already ${enable ? "enabled" : "disabled"}.`);
461
+ return;
462
+ }
463
+ await writeFile(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
464
+ console.log(`Extension \"${identity.name}\" ${enable ? "enabled" : "disabled"} in ${settingsPath}. Use /reload to apply.`);
465
+ return;
466
+ }
455
467
  const entries = Array.isArray(settings[action.kind])
456
468
  ? settings[action.kind].filter((entry): entry is string => typeof entry === "string")
457
469
  : [];
@@ -473,7 +485,8 @@ export default async function (pi: ExtensionAPI) {
473
485
  default: false,
474
486
  });
475
487
 
476
- const args = process.argv.slice(2);
488
+ const rawArgs = process.argv.slice(2);
489
+ const args = withoutTuiModeArgs(rawArgs);
477
490
  // Remote commands must reach the profiles extension before this extension
478
491
  // creates its local --no-session helper process. The remote Pi will process
479
492
  // resource commands on its own host/runtime.
@@ -483,8 +496,11 @@ export default async function (pi: ExtensionAPI) {
483
496
  const packageRequested = packageRequestedFromArgs(args);
484
497
  const packageAction = packageActionFromArgs(args);
485
498
  const sessionRename = sessionRenameFromArgs(args);
486
- const sessionRequested = args.some((arg) => arg === "--list-sessions" || arg === "--list-sessions=true")
487
- || args.some((arg, index) => arg === "sessions" && args[index + 1] === "list");
499
+ const sessionListIndex = args.findIndex((arg, index) => arg === "sessions" && args[index + 1] === "list");
500
+ const sessionFilterCandidate = sessionListIndex >= 0 ? args[sessionListIndex + 2] : undefined;
501
+ const sessionFilter = sessionFilterCandidate?.startsWith("--") ? undefined : sessionFilterCandidate;
502
+ if (sessionFilter !== undefined && sessionFilter !== "pulses") throw new Error(`Unknown sessions list filter: ${sessionFilter}`);
503
+ const sessionRequested = args.some((arg) => arg === "--list-sessions" || arg === "--list-sessions=true") || sessionListIndex >= 0;
488
504
  if (!kind && !action && !packageRequested && !packageAction && !sessionRequested && !sessionRename) return;
489
505
 
490
506
  // Run CLI operations in an ephemeral child so they never create an empty session.
@@ -492,7 +508,8 @@ export default async function (pi: ExtensionAPI) {
492
508
  const profile = requestedProfile();
493
509
  const root = process.env.PI_PROFILE_ROOT ?? process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? "", ".pi", "agent");
494
510
  const target = profile ? join(root, "profiles", profile) : process.env.PI_CODING_AGENT_DIR;
495
- const child = spawn(process.execPath, [process.argv[1], "--no-session", ...args], {
511
+ const pi = piCommand();
512
+ const child = spawn(pi.command, [...pi.args, "--no-session", ...rawArgs], {
496
513
  stdio: "inherit",
497
514
  env: { ...process.env, PI_CLI_RESOURCES: "1", PI_CODING_AGENT_DIR: target, PI_PROFILE_ROOT: root, ...(profile ? { PI_ACTIVE_PROFILE: profile } : {}) },
498
515
  });
@@ -503,6 +520,18 @@ export default async function (pi: ExtensionAPI) {
503
520
  process.exit(code);
504
521
  }
505
522
 
523
+ if (sessionRequested) {
524
+ const sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR;
525
+ const sessions = sessionDir ? await SessionManager.listAll(sessionDir) : await SessionManager.listAll();
526
+ const profile = process.env.PI_ACTIVE_PROFILE ?? requestedProfile() ?? "default", root = process.env.PI_PROFILE_ROOT ?? process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? "", ".pi", "agent");
527
+ const pulseExecutions = new PulseStore(join(root, "pulse.db")).executionSessions(profile);
528
+ const pulseSessions: PulseListedSession[] = sessions.flatMap((session) => { const pulse = pulseExecutions.get(session.id); if (pulse) return [{ ...session, source: "execution", pulse }]; return isPulseOnlySession(session, sessionDir) ? [{ ...session, source: "injected-result" }] : []; });
529
+ const pulseSessionIds = new Set(pulseSessions.map((session) => session.id));
530
+ if (sessionFilter === "pulses") await writeTable(PulseSessionTable(pulseSessions));
531
+ else await writeTable([SessionTable({ sessions: sessions.filter((session) => !pulseSessionIds.has(session.id)) }), ApplicationSessionTable(root, profile)].filter(Boolean).join("\n\n"));
532
+ process.exit(0);
533
+ }
534
+
506
535
  if (sessionRename) {
507
536
  try {
508
537
  if (!sessionRename.name) throw new Error("Session name cannot be empty.");
@@ -542,21 +571,17 @@ export default async function (pi: ExtensionAPI) {
542
571
  }
543
572
 
544
573
  pi.on("session_start", async () => {
545
- if (sessionRequested) {
546
- const sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR;
547
- const sessions = sessionDir ? await SessionManager.listAll(sessionDir) : await SessionManager.listAll();
548
- await renderAndClose(React.createElement(SessionTable, { sessions }));
549
- } else if (packageRequested) {
574
+ if (packageRequested) {
550
575
  const { resourceRoot, runtimeSettings } = await readSettings();
551
- await renderAndClose(React.createElement(ResourceTable, { title: "PACKAGES", headers: ["NAME", "STATUS", "VERSION / DESCRIPTION"], rows: await packageRows(resourceRoot, runtimeSettings), highlightStatus: true }));
576
+ await writeTable(ResourceTable({ title: "PACKAGES", headers: ["NAME", "STATUS", "VERSION / DESCRIPTION"], rows: await packageRows(resourceRoot, runtimeSettings), highlightStatus: true }));
552
577
  } else if (kind === "tools") {
553
578
  const active = new Set(pi.getActiveTools());
554
579
  const { resourceRoot, runtimeSettings } = await readSettings();
555
- const sources = await packageToolSources(resourceRoot, runtimeSettings, pi.getAllTools().map((tool) => tool.name));
580
+ const sources = await packageToolSources(resourceRoot, runtimeSettings, pi.getAllTools().map((tool) => tool.name).filter((name) => !(BUILTIN_TOOLS as readonly string[]).includes(name)));
556
581
  const rows: SourceRow[] = pi.getAllTools()
557
- .map((tool) => [tool.name, active.has(tool.name) ? "enabled" : "disabled", sources.has(tool.name) ? `npm: ${sources.get(tool.name)}` : (BUILTIN_TOOLS as readonly string[]).includes(tool.name) ? "Built-in" : "Extension", tool.description] as SourceRow)
582
+ .map((tool) => [tool.name, active.has(tool.name) ? "enabled" : "disabled", (BUILTIN_TOOLS as readonly string[]).includes(tool.name) ? "Built-in" : sources.has(tool.name) ? `npm: ${sources.get(tool.name)}` : "Extension", tool.description] as SourceRow)
558
583
  .sort((a, b) => a[0].localeCompare(b[0]));
559
- await renderAndClose(React.createElement(SourceTable, { title: "TOOLS", detailHeader: "DESCRIPTION", rows }));
584
+ await writeTable(SourceTable({ title: "TOOLS", detailHeader: "DESCRIPTION", rows }));
560
585
  } else {
561
586
  const loaded = await readSettings();
562
587
  const resourceRoot = loaded.resourceRoot;
@@ -564,18 +589,24 @@ export default async function (pi: ExtensionAPI) {
564
589
  const settings = kind === "extensions" ? loaded.runtimeSettings : loaded.settings;
565
590
  const rawEntries = Array.isArray(settings[kind]) ? settings[kind].filter((value): value is string => typeof value === "string") : [];
566
591
  const configuredPaths = rawEntries.filter((value) => !/^[!+\-]/.test(value));
567
- const exclusions = rawEntries.filter((value) => value.startsWith("!"));
592
+ const exclusions = rawEntries.filter((value) => value.startsWith("!") || value.startsWith("-"));
568
593
  const catalogRoot = join(resourceRoot, kind);
569
- const packageResources = await configuredPackageResources(resourceRoot, loaded.runtimeSettings, kind === "extensions" ? "extensions" : "skills");
594
+ const packageResources = kind === "extensions"
595
+ ? await configuredPackageExtensions(resourceRoot, loaded.runtimeSettings)
596
+ : await configuredPackageResources(resourceRoot, loaded.runtimeSettings, "skills");
570
597
  const paths = kind === "skills"
571
598
  ? [...new Set([catalogRoot, ...configuredPaths, ...packageResources.map(({ path }) => path)])]
572
599
  : [...new Set([...(configuredPaths.length > 0 ? configuredPaths : [catalogRoot]), ...packageResources.map(({ path }) => path)])];
573
- const rows: SourceRow[] = (kind === "skills" ? await skillRows(paths, catalogRoot, settings, exclusions) : await extensionRows(paths, exclusions, (loaded.runtimeSettings.profile as { enabledExtensions?: string[] } | undefined)?.enabledExtensions)).map((row) => {
600
+ const profileSkillRoot = kind === "skills" && loaded.agentDir !== resourceRoot ? join(loaded.agentDir, "skills") : undefined;
601
+ const extensionIdentities = new Map(packageResources.flatMap((resource) => "aliases" in resource && Array.isArray(resource.aliases)
602
+ ? [[resource.path, { name: resource.name, aliases: resource.aliases }] as const]
603
+ : []));
604
+ const rows: SourceRow[] = (kind === "skills" ? await skillRows(paths, catalogRoot, profileSkillRoot, settings, exclusions) : await extensionRows(paths, exclusions, (loaded.runtimeSettings.profile as { enabledExtensions?: string[] } | undefined)?.enabledExtensions, extensionIdentities)).map((row) => {
574
605
  const packageName = packageResources.find(({ path }) => row[2] === path || isWithin(row[2], path))?.packageName;
575
606
  const source = packageName ? `Package: ${packageName}` : (row[2].startsWith(join(resourceRoot, "skills")) ? "Shared" : row[2].includes("/profiles/") ? "Profile" : "Local");
576
607
  return [row[0], row[1], source, row[2]];
577
608
  });
578
- await renderAndClose(React.createElement(SourceTable, { title: kind.toUpperCase(), detailHeader: "PATH", rows }));
609
+ await writeTable(SourceTable({ title: kind.toUpperCase(), detailHeader: "PATH", rows }));
579
610
  }
580
611
  // This command always runs in an ephemeral child process.
581
612
  process.exit(0);
@@ -0,0 +1,7 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { reexecCliWithRegularTui } from "./lib/cli-tui-mode.ts";
3
+
4
+ /** Keep finite pi-feats CLI commands out of Pi's alternate-screen TUI. */
5
+ export default async function (_pi: ExtensionAPI) {
6
+ await reexecCliWithRegularTui();
7
+ }
@@ -10,7 +10,12 @@ const agentRoot = () => process.env.PI_PROFILE_ROOT ?? process.env.PI_CODING_AGE
10
10
  const profileDirectory = () => process.env.PI_CODING_AGENT_DIR ?? agentRoot();
11
11
  function executionContext() {
12
12
  const application = applicationExecutionContext.getStore();
13
- return { application: application?.application ?? process.env.PI_APPLICATION_SLUG, identityKey: application?.identityKey ?? process.env.PI_APPLICATION_IDENTITY_KEY, profile: application?.profile ?? process.env.PI_ACTIVE_PROFILE ?? "default", sessionId: application?.sessionId ?? process.env.PI_APPLICATION_SESSION_ID };
13
+ const encodedDirectMemory = process.env.PI_APPLICATION_DIRECT_CONTEXT_MEMORY;
14
+ let directContextMemory = application?.directContextMemory;
15
+ if (directContextMemory === undefined && encodedDirectMemory) {
16
+ try { directContextMemory = Buffer.from(encodedDirectMemory, "base64url").toString("utf8"); } catch { /* validation happens during context resolution */ }
17
+ }
18
+ return { application: application?.application ?? process.env.PI_APPLICATION_SLUG, identityKey: application?.identityKey ?? process.env.PI_APPLICATION_IDENTITY_KEY, profile: application?.profile ?? process.env.PI_ACTIVE_PROFILE ?? "default", sessionId: application?.sessionId ?? process.env.PI_APPLICATION_SESSION_ID, directContextMemory };
14
19
  }
15
20
 
16
21
  export default function registerContextMemory(pi: ExtensionAPI): void {
@@ -0,0 +1,42 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { join } from "node:path";
5
+ import { searchConversationEntries, textBlocks, type ConversationCandidate } from "../lib/conversation-search.ts";
6
+
7
+ const agentDirectory = () => process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? "", ".pi", "agent");
8
+ const sessionDirectory = () => process.env.PI_ACTIVE_PROFILE && process.env.PI_ACTIVE_PROFILE !== "default" ? join(agentDirectory(), "sessions") : undefined;
9
+
10
+ export default function registerConversationSearch(pi: ExtensionAPI): void {
11
+ let currentSessionId: string | undefined;
12
+ pi.on("session_start", async (_event, ctx) => { currentSessionId = ctx.sessionManager.getSessionId(); });
13
+ pi.registerTool({
14
+ name: "conversation_search",
15
+ label: "Search Previous Conversations",
16
+ description: "Search previous conversations in the active profile by topic, wording, session name, or session ID. Use this when the user asks what was discussed in another conversation or a previous session; the user does not need to know a session ID. Searches only this profile and never other profiles.",
17
+ promptSnippet: "Search previous conversations in the active profile by topic, name, or session ID.",
18
+ promptGuidelines: ["Use conversation_search when the user asks about information discussed in another conversation, a prior session, or asks you to search previous chats. Do not ask for a session ID before searching by topic. When the user supplies a session ID, pass it as sessionId so the exact session is prioritized."],
19
+ parameters: Type.Object({ query: Type.String({ minLength: 2, description: "Topic, phrase, session name, or session ID to search for in prior conversations." }), sessionId: Type.Optional(Type.String({ minLength: 2, description: "Exact or partial session ID, when the user supplied one." })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10, description: "Maximum number of matching conversations to return." })) }),
20
+ async execute(_id, params) {
21
+ const directory = sessionDirectory();
22
+ const sessions = (await SessionManager.listAll(directory)).filter((session) => session.id !== currentSessionId).sort((left, right) => right.modified.getTime() - left.modified.getTime()).slice(0, 500);
23
+ const candidates: ConversationCandidate[] = [];
24
+ for (const session of sessions) {
25
+ try {
26
+ const entries = SessionManager.open(session.path, directory, process.cwd()).getBranch().flatMap((entry: any) => {
27
+ if (entry.type !== "message") return [];
28
+ const role = entry.message?.role;
29
+ if (role !== "user" && role !== "assistant") return [];
30
+ const text = textBlocks(entry.message?.content);
31
+ return text.trim() ? [{ role, text, timestamp: entry.timestamp }] : [];
32
+ });
33
+ if (entries.length) candidates.push({ id: session.id, name: session.name, updatedAt: session.modified, entries });
34
+ } catch { /* A malformed or concurrently removed session must not block search. */ }
35
+ }
36
+ const search = params.sessionId ?? params.query;
37
+ const results = searchConversationEntries(search, candidates, params.limit ?? 5);
38
+ const text = results.length ? results.map((result, index) => [`${index + 1}. ${result.name?.trim() || "Untitled conversation"} (${result.id}) — updated ${result.updatedAt.toISOString()}`, ...result.excerpts.map((excerpt) => ` [${excerpt.role}] ${excerpt.text}`)].join("\n")).join("\n\n") : "No previous conversation in this profile matched that topic.";
39
+ return { content: [{ type: "text" as const, text }], details: { query: search, searchedSessions: sessions.length, matches: results.map((result) => ({ id: result.id, name: result.name ?? null, updatedAt: result.updatedAt.toISOString(), score: result.score })) } };
40
+ },
41
+ });
42
+ }
@@ -5,6 +5,7 @@ import { existsSync } from "node:fs";
5
5
  import { readFile } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
7
  import { basename, join } from "node:path";
8
+ import { withoutTuiModeArgs } from "../lib/cli-tui-mode.ts";
8
9
 
9
10
  type Stage = "input" | "pre_tool" | "post_tool" | "output";
10
11
  type Mode = "transform" | "evaluate" | "reflect";
@@ -97,7 +98,7 @@ export default function (pi: ExtensionAPI) {
97
98
  // `pi guardrails …` is implemented by profiles.ts before Pi opens a model
98
99
  // session. Do not register a slash command for that positional CLI form:
99
100
  // Pi would dispatch it as an interactive command and require credentials.
100
- if (process.argv.slice(2)[0] === "guardrails") return;
101
+ if (withoutTuiModeArgs(process.argv.slice(2))[0] === "guardrails") return;
101
102
 
102
103
  let reflectionCount = 0;
103
104
 
@@ -16,6 +16,8 @@ export type ApplicationExecutionContext = {
16
16
  identityKey: string;
17
17
  profile: string;
18
18
  sessionId: string;
19
+ /** Non-persistent personal context supplied by an Application Direct Mode request. */
20
+ directContextMemory?: string;
19
21
  onContextMemoryExecution?: (event: ContextMemoryExecutionLog) => void;
20
22
  };
21
23
 
@@ -0,0 +1,24 @@
1
+ export const BUILTIN_TOOLS = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"] as const;
2
+
3
+ // Pi 0.87 enables these four built-ins when settings.defaultTools is absent.
4
+ export const DEFAULT_ACTIVE_BUILTIN_TOOLS = ["read", "bash", "edit", "write"] as const;
5
+
6
+ type ToolSettings = Record<string, unknown>;
7
+
8
+ export function activeBuiltinTools(settings: ToolSettings): string[] {
9
+ const configured = settings.defaultTools;
10
+ return Array.isArray(configured)
11
+ ? configured.filter((tool): tool is string => typeof tool === "string")
12
+ : [...DEFAULT_ACTIVE_BUILTIN_TOOLS];
13
+ }
14
+
15
+ export function updatedBuiltinTools(settings: ToolSettings, name: string, enabled: boolean): string[] | undefined {
16
+ const current = activeBuiltinTools(settings);
17
+ const updated = enabled
18
+ ? [...new Set([...current, name])]
19
+ : current.filter((tool) => tool !== name);
20
+ return updated.length === DEFAULT_ACTIVE_BUILTIN_TOOLS.length
21
+ && DEFAULT_ACTIVE_BUILTIN_TOOLS.every((tool) => updated.includes(tool))
22
+ ? undefined
23
+ : updated;
24
+ }