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,20 +1,34 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { copyFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { copyFile, cp, mkdir, readFile, readlink, readdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, relative, resolve } from "node:path";
|
|
4
4
|
import { ensureProfileSandbox } from "../lib/profile-sandbox.ts";
|
|
5
|
+
import { BUILTIN_TOOLS, activeBuiltinTools, updatedBuiltinTools } from "../lib/builtin-tools.ts";
|
|
6
|
+
import { packageExtensions } from "../lib/package-resources.ts";
|
|
5
7
|
|
|
6
|
-
export
|
|
8
|
+
export { BUILTIN_TOOLS } from "../lib/builtin-tools.ts";
|
|
7
9
|
export const RESOURCE_KINDS = ["tools", "skills", "extensions"] as const;
|
|
8
10
|
export type ResourceKind = typeof RESOURCE_KINDS[number];
|
|
9
11
|
type PolicyKey = "enabledTools" | "enabledSkills" | "enabledProfileSkills" | "enabledExtensions";
|
|
10
12
|
type SkillSources = { shared?: boolean; profile?: boolean };
|
|
11
13
|
type SkillResourceSource = "shared" | "profile" | "package";
|
|
12
|
-
type
|
|
14
|
+
export type ProfileMetadata = { description: string; tags: string[] };
|
|
15
|
+
type ProfilePolicy = Partial<Record<PolicyKey, string[]>> & Partial<ProfileMetadata> & { skillSources?: SkillSources; contextMemory?: { mode: "file"; target: "profile" | "identity" } | { mode: "handler"; handler: string } };
|
|
13
16
|
export type ProfileSettings = Record<string, unknown> & { profile?: ProfilePolicy; sandbox?: boolean };
|
|
14
17
|
export type Resource = { name: string; kind: ResourceKind; path?: string; source: "builtin" | "shared" | "profile" | "extension" | "package"; package?: string; enabled: boolean; protected?: boolean };
|
|
15
18
|
export type Package = { name: string; version?: string; description?: string; enabled: boolean; installed: boolean };
|
|
16
19
|
|
|
17
20
|
const validName = (name: string) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name);
|
|
21
|
+
const maxProfileDescriptionLength = 500;
|
|
22
|
+
const maxProfileTags = 20;
|
|
23
|
+
const maxProfileTagLength = 50;
|
|
24
|
+
const profileMetadata = (value: unknown): ProfileMetadata => {
|
|
25
|
+
const input = value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
26
|
+
if (input.description !== undefined && (typeof input.description !== "string" || input.description.length > maxProfileDescriptionLength)) throw Object.assign(new Error(`description must be a string of at most ${maxProfileDescriptionLength} characters.`), { status: 400 });
|
|
27
|
+
if (input.tags !== undefined && (!Array.isArray(input.tags) || input.tags.some((tag) => typeof tag !== "string"))) throw Object.assign(new Error("tags must be an array of strings."), { status: 400 });
|
|
28
|
+
const tags = [...new Map((input.tags as string[] | undefined ?? []).map((tag) => tag.trim()).filter(Boolean).map((tag) => [tag.toLocaleLowerCase(), tag])).values()];
|
|
29
|
+
if (tags.length > maxProfileTags || tags.some((tag) => tag.length > maxProfileTagLength)) throw Object.assign(new Error(`Use at most ${maxProfileTags} tags of at most ${maxProfileTagLength} characters.`), { status: 400 });
|
|
30
|
+
return { description: (input.description as string | undefined ?? "").trim(), tags };
|
|
31
|
+
};
|
|
18
32
|
const policyKey: Record<ResourceKind, PolicyKey> = {
|
|
19
33
|
tools: "enabledTools",
|
|
20
34
|
skills: "enabledSkills",
|
|
@@ -24,12 +38,13 @@ const protectedExtensions = new Set(["profiles", "api-server"]);
|
|
|
24
38
|
|
|
25
39
|
export class ProfileStore {
|
|
26
40
|
private readonly extensionTools = new Map<string, Set<string>>();
|
|
41
|
+
readonly agentDir: string;
|
|
27
42
|
|
|
28
|
-
constructor(
|
|
43
|
+
constructor(agentDir: string) { this.agentDir = agentDir; }
|
|
29
44
|
|
|
30
45
|
registerExtensionTools(profile: string, names: Iterable<string>): void {
|
|
31
46
|
const tools = this.extensionTools.get(profile) ?? new Set<string>();
|
|
32
|
-
for (const name of names) if (name && !BUILTIN_TOOLS.includes(name)) tools.add(name);
|
|
47
|
+
for (const name of names) if (name && !(BUILTIN_TOOLS as readonly string[]).includes(name)) tools.add(name);
|
|
33
48
|
this.extensionTools.set(profile, tools);
|
|
34
49
|
}
|
|
35
50
|
|
|
@@ -49,6 +64,13 @@ export class ProfileStore {
|
|
|
49
64
|
return join(this.profilesDir(), name);
|
|
50
65
|
}
|
|
51
66
|
private settingsPath(name: string): string { return join(this.directory(name), "settings.json"); }
|
|
67
|
+
private async linkSharedModels(name: string): Promise<void> {
|
|
68
|
+
if (name === "default") return;
|
|
69
|
+
const source = join(this.agentDir, "models.json"), target = join(this.directory(name), "models.json");
|
|
70
|
+
try { if (resolve(this.directory(name), await readlink(target)) === source) return; } catch { /* Missing or non-symlink target must be replaced. */ }
|
|
71
|
+
await rm(target, { force: true });
|
|
72
|
+
if (existsSync(source)) await symlink(source, target);
|
|
73
|
+
}
|
|
52
74
|
private skillSources(name: string, settings: ProfileSettings): Required<SkillSources> {
|
|
53
75
|
if (name === "default") return { shared: true, profile: false };
|
|
54
76
|
return { shared: settings.profile?.skillSources?.shared !== false, profile: settings.profile?.skillSources?.profile === true };
|
|
@@ -62,6 +84,7 @@ export class ProfileStore {
|
|
|
62
84
|
try {
|
|
63
85
|
const value = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
64
86
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("settings.json must contain an object");
|
|
87
|
+
await this.linkSharedModels(name);
|
|
65
88
|
return value as ProfileSettings;
|
|
66
89
|
} catch (error) {
|
|
67
90
|
if ((error as NodeJS.ErrnoException).code === "ENOENT" && name === "default") return {};
|
|
@@ -70,14 +93,13 @@ export class ProfileStore {
|
|
|
70
93
|
}
|
|
71
94
|
}
|
|
72
95
|
|
|
73
|
-
async list(): Promise<Array<{ name: string; path: string }>> {
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
96
|
+
async list(): Promise<Array<{ name: string; path: string } & ProfileMetadata>> {
|
|
97
|
+
const names = ["default"];
|
|
98
|
+
if (existsSync(this.profilesDir())) for (const entry of await readdir(this.profilesDir(), { withFileTypes: true })) if (entry.isDirectory() && existsSync(join(this.profilesDir(), entry.name, "settings.json"))) names.push(entry.name);
|
|
99
|
+
const profiles = await Promise.all(names.map(async (name) => {
|
|
100
|
+
const settings = await this.readSettings(name);
|
|
101
|
+
return { name, path: this.directory(name), ...profileMetadata(settings.profile) };
|
|
102
|
+
}));
|
|
81
103
|
return profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
82
104
|
}
|
|
83
105
|
|
|
@@ -106,6 +128,7 @@ export class ProfileStore {
|
|
|
106
128
|
private enabled(settings: ProfileSettings, kind: ResourceKind, name: string, path?: string): boolean {
|
|
107
129
|
const values = settings.profile?.[policyKey[kind]];
|
|
108
130
|
if (values) return values.includes("*") || values.includes(name);
|
|
131
|
+
if (kind === "tools" && (BUILTIN_TOOLS as readonly string[]).includes(name)) return activeBuiltinTools(settings).includes(name);
|
|
109
132
|
const configured = settings[kind];
|
|
110
133
|
const exclusions = Array.isArray(configured) ? configured.filter((value): value is string => typeof value === "string" && value.startsWith("!")) : [];
|
|
111
134
|
return !path || !exclusions.includes(`!${path}`);
|
|
@@ -172,19 +195,15 @@ export class ProfileStore {
|
|
|
172
195
|
}
|
|
173
196
|
|
|
174
197
|
private async packageExtensions(settings: ProfileSettings): Promise<Array<{ name: string; path: string; package: string }>> {
|
|
175
|
-
|
|
176
|
-
for (const pkg of await this.configuredPackages(settings)) {
|
|
177
|
-
for (const extension of Array.isArray(pkg.manifest?.pi?.extensions) ? pkg.manifest.pi.extensions : []) if (typeof extension === "string") entries.push({ name: extension.split("/").pop()?.replace(/\.(?:ts|js)$/, "") ?? pkg.name, path: join(pkg.base, extension), package: pkg.name });
|
|
178
|
-
}
|
|
179
|
-
return entries;
|
|
198
|
+
return packageExtensions(await this.configuredPackages(settings)).map(({ name, path, packageName }) => ({ name, path, package: packageName }));
|
|
180
199
|
}
|
|
181
200
|
|
|
182
201
|
private async packageToolSources(settings: ProfileSettings, names: Iterable<string>): Promise<Map<string, string>> {
|
|
183
202
|
const sources = new Map<string, string>(), tools = [...names];
|
|
184
203
|
for (const extension of await this.packageExtensions(settings)) try {
|
|
185
204
|
const source = await readFile(extension.path, "utf8");
|
|
186
|
-
for (const tool of tools) if (source.includes(`"${tool}"`) || source.includes(`'${tool}'`) || source.includes(`\`${tool}\``)) sources.set(tool, extension.package);
|
|
187
|
-
for (const block of source.matchAll(/TOOL_NAMES[^=]*=\s*\{([\s\S]*?)\}/g)) for (const name of block[1].matchAll(/:\s*["']([A-Za-z][A-Za-z0-9_-]{0,63})["']/g)) sources.set(name[1], extension.package);
|
|
205
|
+
for (const tool of tools) if (!(BUILTIN_TOOLS as readonly string[]).includes(tool) && (source.includes(`"${tool}"`) || source.includes(`'${tool}'`) || source.includes(`\`${tool}\``))) sources.set(tool, extension.package);
|
|
206
|
+
for (const block of source.matchAll(/TOOL_NAMES[^=]*=\s*\{([\s\S]*?)\}/g)) for (const name of block[1].matchAll(/:\s*["']([A-Za-z][A-Za-z0-9_-]{0,63})["']/g)) if (!(BUILTIN_TOOLS as readonly string[]).includes(name[1])) sources.set(name[1], extension.package);
|
|
188
207
|
} catch {}
|
|
189
208
|
return sources;
|
|
190
209
|
}
|
|
@@ -276,6 +295,19 @@ export class ProfileStore {
|
|
|
276
295
|
async setResource(profile: string, kind: ResourceKind, name: string, enabled: boolean, source?: SkillResourceSource): Promise<Resource> {
|
|
277
296
|
const current = await this.resource(profile, kind, name, source);
|
|
278
297
|
if (current.protected && !enabled) throw Object.assign(new Error("This extension is required and cannot be disabled."), { status: 422 });
|
|
298
|
+
// The default profile uses Pi's defaultTools setting for native tool state
|
|
299
|
+
// unless it already has a legacy explicit enabledTools policy. Named profiles
|
|
300
|
+
// retain their explicit enabledTools policy.
|
|
301
|
+
if (kind === "tools" && current.source === "builtin" && profile === "default") {
|
|
302
|
+
const settings = await this.readSettings(profile);
|
|
303
|
+
if (!settings.profile?.enabledTools) {
|
|
304
|
+
const tools = updatedBuiltinTools(settings, name, enabled);
|
|
305
|
+
if (tools) settings.defaultTools = tools;
|
|
306
|
+
else delete settings.defaultTools;
|
|
307
|
+
await this.writeSettings(profile, settings);
|
|
308
|
+
return this.resource(profile, kind, name, source);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
279
311
|
// Extensions are shared runtime resources, so toggling one always updates
|
|
280
312
|
// the default runtime rather than materializing it in a profile workspace.
|
|
281
313
|
const settingsProfile = kind === "extensions" ? "default" : profile;
|
|
@@ -299,29 +331,67 @@ export class ProfileStore {
|
|
|
299
331
|
await this.refreshSkills(profile);
|
|
300
332
|
}
|
|
301
333
|
|
|
302
|
-
async create(name: string): Promise<{ name: string; path: string }> {
|
|
334
|
+
async create(name: string, metadata: unknown = {}, cloneFrom?: string): Promise<{ name: string; path: string } & ProfileMetadata> {
|
|
303
335
|
if (!validName(name) || name === "default") throw Object.assign(new Error("Invalid profile name"), { status: 400 });
|
|
304
336
|
const destination = this.directory(name);
|
|
305
337
|
if (existsSync(destination)) throw Object.assign(new Error("Profile already exists"), { status: 409 });
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
profile: { enabledTools: ["*"], enabledSkills: ["*"], enabledProfileSkills: ["*"], skillSources: { shared: true, profile: false } },
|
|
315
|
-
};
|
|
316
|
-
await this.writeSettings(name, settings);
|
|
317
|
-
await ensureProfileSandbox(destination, resolve(process.argv[1]));
|
|
318
|
-
await writeFile(join(destination, "SOUL.md"), "", "utf8");
|
|
319
|
-
await writeFile(join(destination, "guardrails.json"), "{\n \"guardrails\": []\n}\n", "utf8");
|
|
320
|
-
for (const file of ["auth.json", "models.json"]) {
|
|
321
|
-
const source = join(this.agentDir, file);
|
|
322
|
-
if (existsSync(source)) await copyFile(source, join(destination, file));
|
|
338
|
+
const details = profileMetadata(metadata);
|
|
339
|
+
if (cloneFrom !== undefined && (typeof cloneFrom !== "string" || !validName(cloneFrom) || cloneFrom === "default")) throw Object.assign(new Error("Clone source must be an existing named profile."), { status: 400 });
|
|
340
|
+
|
|
341
|
+
let sourceSettings: ProfileSettings | undefined;
|
|
342
|
+
let sourceDirectory: string | undefined;
|
|
343
|
+
if (cloneFrom) {
|
|
344
|
+
sourceSettings = await this.readSettings(cloneFrom);
|
|
345
|
+
sourceDirectory = this.directory(cloneFrom);
|
|
323
346
|
}
|
|
324
|
-
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
await mkdir(join(destination, "sessions"), { recursive: true });
|
|
350
|
+
let settings: ProfileSettings;
|
|
351
|
+
if (sourceSettings && sourceDirectory) {
|
|
352
|
+
const sourceSkills = join(sourceDirectory, "skills");
|
|
353
|
+
if (existsSync(sourceSkills)) await cp(sourceSkills, join(destination, "skills"), { recursive: true, force: false });
|
|
354
|
+
for (const file of ["SOUL.md", "REFINE.md", "guardrails.json", ".env", "auth.json"]) {
|
|
355
|
+
const source = join(sourceDirectory, file);
|
|
356
|
+
if (existsSync(source)) await copyFile(source, join(destination, file));
|
|
357
|
+
}
|
|
358
|
+
settings = {
|
|
359
|
+
...sourceSettings,
|
|
360
|
+
sandbox: true,
|
|
361
|
+
profile: { ...(sourceSettings.profile ?? {}), ...details },
|
|
362
|
+
};
|
|
363
|
+
} else {
|
|
364
|
+
let base: ProfileSettings = {};
|
|
365
|
+
try { base = await this.readSettings("default"); } catch {}
|
|
366
|
+
const { packages: _packages, extensions: _extensions, ...profileBase } = base;
|
|
367
|
+
settings = {
|
|
368
|
+
...profileBase,
|
|
369
|
+
defaultTools: BUILTIN_TOOLS,
|
|
370
|
+
sandbox: true,
|
|
371
|
+
profile: { enabledTools: ["*"], enabledSkills: ["*"], enabledProfileSkills: ["*"], skillSources: { shared: true, profile: false }, ...details },
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
await this.writeSettings(name, settings);
|
|
375
|
+
await ensureProfileSandbox(destination, resolve(process.argv[1]));
|
|
376
|
+
if (!sourceSettings) {
|
|
377
|
+
await writeFile(join(destination, "SOUL.md"), "", "utf8");
|
|
378
|
+
await writeFile(join(destination, "guardrails.json"), "{\n \"guardrails\": []\n}\n", "utf8");
|
|
379
|
+
const auth = join(this.agentDir, "auth.json");
|
|
380
|
+
if (existsSync(auth)) await copyFile(auth, join(destination, "auth.json"));
|
|
381
|
+
}
|
|
382
|
+
await this.linkSharedModels(name);
|
|
383
|
+
return { name, path: destination, ...details };
|
|
384
|
+
} catch (error) {
|
|
385
|
+
await rm(destination, { recursive: true, force: true });
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async updateMetadata(name: string, metadata: unknown): Promise<{ name: string; path: string } & ProfileMetadata> {
|
|
391
|
+
const details = profileMetadata(metadata);
|
|
392
|
+
const settings = await this.readSettings(name);
|
|
393
|
+
await this.writeSettings(name, { ...settings, profile: { ...(settings.profile ?? {}), ...details } });
|
|
394
|
+
return { name, path: this.directory(name), ...details };
|
|
325
395
|
}
|
|
326
396
|
|
|
327
397
|
async delete(name: string): Promise<void> {
|