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.
- package/README.md +90 -9
- package/extensions/api-server/README.md +21 -1
- package/extensions/api-server/application-log-store.ts +4 -2
- package/extensions/api-server/application-runtime.ts +79 -8
- package/extensions/api-server/application-store.ts +23 -10
- package/extensions/api-server/index.ts +6 -2
- package/extensions/api-server/profile-store.ts +109 -39
- package/extensions/api-server/server.ts +220 -44
- package/extensions/cli-resources.ts +144 -113
- package/extensions/cli-tui-mode.ts +7 -0
- package/extensions/context-memory/index.ts +6 -1
- package/extensions/conversation-search/index.ts +42 -0
- package/extensions/guardrails/index.ts +2 -1
- package/extensions/lib/application-context.ts +2 -0
- package/extensions/lib/builtin-tools.ts +24 -0
- package/extensions/lib/cli-tui-mode.ts +72 -0
- package/extensions/lib/context-memory.ts +6 -1
- package/extensions/lib/conversation-search.ts +33 -0
- package/extensions/lib/package-resources.ts +58 -0
- package/extensions/lib/pi-command.ts +11 -0
- package/extensions/lib/profile-sandbox.ts +14 -3
- package/extensions/lib/remote-hosts.ts +7 -5
- package/extensions/lib/workflow-evaluator.ts +113 -0
- package/extensions/lib/workflow-harness.ts +409 -43
- package/extensions/pi-console-webui/app/[section]/page.tsx +1 -1
- package/extensions/pi-console-webui/app/api/admin/models/route.ts +22 -0
- package/extensions/pi-console-webui/app/globals.css +3 -0
- package/extensions/pi-console-webui/components/application-handler-editor.tsx +1 -1
- package/extensions/pi-console-webui/components/application-logs.tsx +4 -4
- package/extensions/pi-console-webui/components/application-mappings.tsx +10 -12
- package/extensions/pi-console-webui/components/application-sessions.tsx +2 -2
- package/extensions/pi-console-webui/components/application-settings.tsx +12 -6
- package/extensions/pi-console-webui/components/chat-workspace.tsx +17 -7
- package/extensions/pi-console-webui/components/console-page.tsx +4 -0
- package/extensions/pi-console-webui/components/console-state.tsx +1 -1
- package/extensions/pi-console-webui/components/console.tsx +32 -11
- package/extensions/pi-console-webui/components/guardrails-panel.tsx +2 -2
- package/extensions/pi-console-webui/components/models-config-form.tsx +31 -0
- package/extensions/pi-console-webui/components/profile-model-form.tsx +54 -0
- package/extensions/pi-console-webui/components/pulse-resources.tsx +7 -6
- package/extensions/pi-console-webui/components/sequential-workflow-config.tsx +32 -0
- package/extensions/pi-console-webui/components/skill-resources.tsx +4 -3
- package/extensions/pi-console-webui/components/ui/input.tsx +1 -1
- package/extensions/pi-console-webui/index.ts +2 -1
- package/extensions/pi-console-webui/lib/admin-models.ts +38 -0
- package/extensions/pi-console-webui/lib/page-metadata.ts +4 -1
- package/extensions/pi-console-webui/next.config.js +1 -0
- package/extensions/profile-ssh.ts +274 -0
- package/extensions/profiles.ts +69 -25
- package/extensions/pulse/index.ts +148 -16
- package/extensions/pulse/store.ts +132 -18
- package/extensions/sequential-workflow.ts +121 -79
- package/extensions/skill-sources/store.ts +29 -10
- package/package.json +21 -9
- package/scripts/build-console-webui.mjs +7 -0
- package/scripts/console-webui-cache.mjs +79 -0
- package/scripts/ensure-console-build.mjs +43 -0
- package/skills/profile-ssh/SKILL.md +65 -0
- package/skills/sequential-workflow/SKILL.md +4 -3
- 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
|
|
68
|
-
const
|
|
69
|
-
const
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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,
|
|
199
|
-
const
|
|
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[],
|
|
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,
|
|
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
|
-
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
|
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
|
|
319
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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 =
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
444
|
-
if (updatedTools
|
|
445
|
-
|
|
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
|
|
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
|
|
487
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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)}` :
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|