pi-feats 0.1.1

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +508 -0
  3. package/extensions/README.md +27 -0
  4. package/extensions/api-server/PLAN.md +70 -0
  5. package/extensions/api-server/README.md +103 -0
  6. package/extensions/api-server/application-log-store.ts +21 -0
  7. package/extensions/api-server/application-runtime.ts +212 -0
  8. package/extensions/api-server/application-store.ts +30 -0
  9. package/extensions/api-server/index.ts +52 -0
  10. package/extensions/api-server/profile-store.ts +367 -0
  11. package/extensions/api-server/server.ts +863 -0
  12. package/extensions/cli-resources.ts +564 -0
  13. package/extensions/guardrails/index.ts +178 -0
  14. package/extensions/lib/application-handler-templates.ts +63 -0
  15. package/extensions/lib/profile-env.ts +61 -0
  16. package/extensions/lib/profile-sandbox.ts +197 -0
  17. package/extensions/lib/remote-hosts.ts +392 -0
  18. package/extensions/pi-console-webui/app/[section]/page.tsx +4 -0
  19. package/extensions/pi-console-webui/app/api/admin/config/[target]/route.ts +5 -0
  20. package/extensions/pi-console-webui/app/api/admin/services/[service]/restart/route.ts +5 -0
  21. package/extensions/pi-console-webui/app/api/auth/login/route.ts +9 -0
  22. package/extensions/pi-console-webui/app/api/auth/logout/route.ts +3 -0
  23. package/extensions/pi-console-webui/app/api/message/app/[slug]/route.ts +11 -0
  24. package/extensions/pi-console-webui/app/api/pi/[...path]/route.ts +31 -0
  25. package/extensions/pi-console-webui/app/applications/[slug]/page.tsx +2 -0
  26. package/extensions/pi-console-webui/app/globals.css +41 -0
  27. package/extensions/pi-console-webui/app/icon.svg +1 -0
  28. package/extensions/pi-console-webui/app/layout.tsx +5 -0
  29. package/extensions/pi-console-webui/app/login/page.tsx +11 -0
  30. package/extensions/pi-console-webui/app/page.tsx +2 -0
  31. package/extensions/pi-console-webui/app/terminal/page.tsx +4 -0
  32. package/extensions/pi-console-webui/components/admin-config-form.tsx +16 -0
  33. package/extensions/pi-console-webui/components/application-handler-editor.tsx +39 -0
  34. package/extensions/pi-console-webui/components/application-logs.tsx +38 -0
  35. package/extensions/pi-console-webui/components/application-mappings.tsx +28 -0
  36. package/extensions/pi-console-webui/components/application-sessions.tsx +11 -0
  37. package/extensions/pi-console-webui/components/application-settings.tsx +60 -0
  38. package/extensions/pi-console-webui/components/application-workspace.tsx +14 -0
  39. package/extensions/pi-console-webui/components/applications.tsx +15 -0
  40. package/extensions/pi-console-webui/components/chat-workspace.tsx +42 -0
  41. package/extensions/pi-console-webui/components/console-page.tsx +23 -0
  42. package/extensions/pi-console-webui/components/console-state.tsx +30 -0
  43. package/extensions/pi-console-webui/components/console.tsx +115 -0
  44. package/extensions/pi-console-webui/components/guardrails-panel.tsx +78 -0
  45. package/extensions/pi-console-webui/components/package-resources.tsx +13 -0
  46. package/extensions/pi-console-webui/components/pulse-resources.tsx +41 -0
  47. package/extensions/pi-console-webui/components/skill-resources.tsx +35 -0
  48. package/extensions/pi-console-webui/components/skill-source-document-preview.tsx +7 -0
  49. package/extensions/pi-console-webui/components/skill-source-import.tsx +7 -0
  50. package/extensions/pi-console-webui/components/skill-sources.tsx +12 -0
  51. package/extensions/pi-console-webui/components/terminal-client.tsx +39 -0
  52. package/extensions/pi-console-webui/components/toast.tsx +18 -0
  53. package/extensions/pi-console-webui/components/ui/button.tsx +4 -0
  54. package/extensions/pi-console-webui/components/ui/card.tsx +4 -0
  55. package/extensions/pi-console-webui/components/ui/input.tsx +4 -0
  56. package/extensions/pi-console-webui/components/ui/switch.tsx +6 -0
  57. package/extensions/pi-console-webui/components/ui/tabs.tsx +11 -0
  58. package/extensions/pi-console-webui/components.json +8 -0
  59. package/extensions/pi-console-webui/index.ts +33 -0
  60. package/extensions/pi-console-webui/lib/admin-config.ts +22 -0
  61. package/extensions/pi-console-webui/lib/auth.ts +21 -0
  62. package/extensions/pi-console-webui/lib/config.ts +15 -0
  63. package/extensions/pi-console-webui/lib/pi-api.ts +9 -0
  64. package/extensions/pi-console-webui/lib/utils.ts +3 -0
  65. package/extensions/pi-console-webui/next-env.d.ts +6 -0
  66. package/extensions/pi-console-webui/next.config.js +5 -0
  67. package/extensions/pi-console-webui/postcss.config.js +1 -0
  68. package/extensions/pi-console-webui/tailwind.config.ts +2 -0
  69. package/extensions/pi-console-webui/tsconfig.json +41 -0
  70. package/extensions/profiles.ts +439 -0
  71. package/extensions/pulse/index.ts +62 -0
  72. package/extensions/pulse/store.ts +105 -0
  73. package/extensions/sequential-workflow.ts +270 -0
  74. package/extensions/skill-sources/index.ts +4 -0
  75. package/extensions/skill-sources/store.ts +118 -0
  76. package/package.json +89 -0
  77. package/scripts/install-nono.sh +34 -0
@@ -0,0 +1,367 @@
1
+ import { existsSync } from "node:fs";
2
+ import { copyFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
3
+ import { join, relative, resolve } from "node:path";
4
+ import { ensureProfileSandbox } from "../lib/profile-sandbox.ts";
5
+
6
+ export const BUILTIN_TOOLS = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"];
7
+ export const RESOURCE_KINDS = ["tools", "skills", "extensions"] as const;
8
+ export type ResourceKind = typeof RESOURCE_KINDS[number];
9
+ type PolicyKey = "enabledTools" | "enabledSkills" | "enabledProfileSkills" | "enabledExtensions";
10
+ type SkillSources = { shared?: boolean; profile?: boolean };
11
+ type ProfilePolicy = Partial<Record<PolicyKey, string[]>> & { skillSources?: SkillSources };
12
+ export type ProfileSettings = Record<string, unknown> & { profile?: ProfilePolicy; sandbox?: boolean };
13
+ export type Resource = { name: string; kind: ResourceKind; path?: string; source: "builtin" | "shared" | "profile" | "extension" | "package"; package?: string; enabled: boolean; protected?: boolean };
14
+ export type Package = { name: string; version?: string; description?: string; enabled: boolean; installed: boolean };
15
+
16
+ const validName = (name: string) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name);
17
+ const policyKey: Record<ResourceKind, PolicyKey> = {
18
+ tools: "enabledTools",
19
+ skills: "enabledSkills",
20
+ extensions: "enabledExtensions",
21
+ };
22
+ const protectedExtensions = new Set(["profiles", "api-server"]);
23
+
24
+ export class ProfileStore {
25
+ private readonly extensionTools = new Set<string>();
26
+
27
+ constructor(readonly agentDir: string) {}
28
+
29
+ registerExtensionTools(names: Iterable<string>): void {
30
+ for (const name of names) if (name && !BUILTIN_TOOLS.includes(name)) this.extensionTools.add(name);
31
+ }
32
+
33
+ private profilesDir(): string { return join(this.agentDir, "profiles"); }
34
+ directory(name: string): string {
35
+ if (name === "default") return this.agentDir;
36
+ if (!validName(name)) throw Object.assign(new Error("Invalid profile"), { status: 404 });
37
+ return join(this.profilesDir(), name);
38
+ }
39
+ private settingsPath(name: string): string { return join(this.directory(name), "settings.json"); }
40
+ private skillSources(name: string, settings: ProfileSettings): Required<SkillSources> {
41
+ if (name === "default") return { shared: true, profile: false };
42
+ return { shared: settings.profile?.skillSources?.shared !== false, profile: settings.profile?.skillSources?.profile === true };
43
+ }
44
+ private skillBase(name: string, source: "shared" | "profile"): string {
45
+ return source === "shared" ? join(this.agentDir, "skills") : join(this.directory(name), "skills");
46
+ }
47
+
48
+ async readSettings(name: string): Promise<ProfileSettings> {
49
+ const path = this.settingsPath(name);
50
+ try {
51
+ const value = JSON.parse(await readFile(path, "utf8")) as unknown;
52
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("settings.json must contain an object");
53
+ return value as ProfileSettings;
54
+ } catch (error) {
55
+ if ((error as NodeJS.ErrnoException).code === "ENOENT" && name === "default") return {};
56
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw Object.assign(new Error("Profile not found"), { status: 404 });
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ async list(): Promise<Array<{ name: string; path: string }>> {
62
+ const profiles = [{ name: "default", path: this.agentDir }];
63
+ if (!existsSync(this.profilesDir())) return profiles;
64
+ for (const entry of await readdir(this.profilesDir(), { withFileTypes: true })) {
65
+ if (entry.isDirectory() && existsSync(join(this.profilesDir(), entry.name, "settings.json"))) {
66
+ profiles.push({ name: entry.name, path: this.directory(entry.name) });
67
+ }
68
+ }
69
+ return profiles.sort((a, b) => a.name.localeCompare(b.name));
70
+ }
71
+
72
+ private async skillNames(base: string): Promise<Array<{ name: string; path: string }>> {
73
+ if (!existsSync(base)) return [];
74
+ const skills: Array<{ name: string; path: string }> = [];
75
+ const visit = async (directory: string): Promise<void> => {
76
+ if (existsSync(join(directory, "SKILL.md"))) { skills.push({ name: relative(base, directory).replaceAll("\\", "/"), path: directory }); return; }
77
+ for (const entry of await readdir(directory, { withFileTypes: true })) if (entry.isDirectory()) await visit(join(directory, entry.name));
78
+ };
79
+ await visit(base);
80
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
81
+ }
82
+
83
+ private async names(kind: Exclude<ResourceKind, "tools" | "skills">): Promise<Array<{ name: string; path: string }>> {
84
+ const base = join(this.agentDir, kind);
85
+ if (!existsSync(base)) return [];
86
+ const entries = await readdir(base, { withFileTypes: true });
87
+ return entries.flatMap((entry) => {
88
+ if (entry.isFile() && /\.(?:ts|js)$/.test(entry.name)) return [{ name: entry.name.replace(/\.(?:ts|js)$/, ""), path: join(base, entry.name) }];
89
+ if (entry.isDirectory() && (existsSync(join(base, entry.name, "index.ts")) || existsSync(join(base, entry.name, "index.js")))) return [{ name: entry.name, path: join(base, entry.name) }];
90
+ return [];
91
+ });
92
+ }
93
+
94
+ private enabled(settings: ProfileSettings, kind: ResourceKind, name: string, path?: string): boolean {
95
+ const values = settings.profile?.[policyKey[kind]];
96
+ if (values) return values.includes("*") || values.includes(name);
97
+ const configured = settings[kind];
98
+ const exclusions = Array.isArray(configured) ? configured.filter((value): value is string => typeof value === "string" && value.startsWith("!")) : [];
99
+ return !path || !exclusions.includes(`!${path}`);
100
+ }
101
+
102
+ private async skillResources(profile: string, settings: ProfileSettings): Promise<Resource[]> {
103
+ const sources = this.skillSources(profile, settings);
104
+ const entries = await Promise.all((["profile", "shared"] as const).filter((source) => sources[source]).map(async (source) => ({ source, entries: await this.skillNames(this.skillBase(profile, source)) })));
105
+ return entries.flatMap(({ source, entries }) => entries.map(({ name, path }) => ({ name, kind: "skills" as const, path, source, enabled: this.skillEnabled(settings, source, name) })));
106
+ }
107
+
108
+ private skillEnabled(settings: ProfileSettings, source: "shared" | "profile", name: string): boolean {
109
+ const values = settings.profile?.[source === "shared" ? "enabledSkills" : "enabledProfileSkills"];
110
+ return !values || values.includes("*") || values.includes(name);
111
+ }
112
+
113
+ private packageBase(source: string): string | undefined {
114
+ if (source.startsWith("npm:")) return join(this.agentDir, "npm", "node_modules", source.slice(4));
115
+ if (source.startsWith(".") || source.startsWith("/")) return resolve(this.agentDir, source);
116
+ if (!source.startsWith("git:") && !/^(?:https?|ssh):\/\//.test(source)) return undefined;
117
+ let remote = source.replace(/^git:/, ""), ref = remote.lastIndexOf("@");
118
+ if (ref > remote.lastIndexOf("/")) remote = remote.slice(0, ref);
119
+ remote = remote.replace(/^https?:\/\//, "").replace(/^ssh:\/\/git@/, "").replace(/^git@/, "").replace(/^([^/:]+):/, "$1/").replace(/\.git$/, "");
120
+ return join(this.agentDir, "git", remote);
121
+ }
122
+
123
+ private async configuredPackages(settings: ProfileSettings): Promise<Array<{ source: string; base: string; name: string; manifest?: { version?: string; description?: string; pi?: { extensions?: unknown } } }>> {
124
+ const sources = Array.isArray(settings.packages) ? settings.packages.filter((value): value is string => typeof value === "string") : [];
125
+ const packages: Array<{ source: string; base: string; name: string; manifest?: { version?: string; description?: string; pi?: { extensions?: unknown } } }> = [];
126
+ for (const source of sources) {
127
+ const base = this.packageBase(source); if (!base) continue;
128
+ try { const manifest = JSON.parse(await readFile(join(base, "package.json"), "utf8")) as { name?: string; version?: string; description?: string; pi?: { extensions?: unknown } }; packages.push({ source, base, name: manifest.name ?? source, manifest }); }
129
+ catch { packages.push({ source, base, name: source.replace(/^npm:/, "") }); }
130
+ }
131
+ return packages;
132
+ }
133
+
134
+ async packages(_profile: string): Promise<Package[]> {
135
+ const settings = await this.readSettings("default");
136
+ return (await this.configuredPackages(settings)).map((pkg) => ({ name: pkg.name, version: pkg.manifest?.version, description: pkg.manifest?.description, enabled: existsSync(join(pkg.base, "package.json")), installed: existsSync(join(pkg.base, "package.json")) })).sort((left, right) => left.name.localeCompare(right.name));
137
+ }
138
+
139
+ async setPackage(_profile: string, name: string, enabled: boolean): Promise<Package> {
140
+ if (!/^(?:@[-a-zA-Z0-9_.]+\/)?[-a-zA-Z0-9_.]+$/.test(name)) throw Object.assign(new Error("Invalid package name"), { status: 400 });
141
+ if (!existsSync(join(this.agentDir, "npm", "node_modules", name, "package.json"))) throw Object.assign(new Error("Package is not installed globally"), { status: 404 });
142
+ const settings = await this.readSettings("default"), entry = `npm:${name}`;
143
+ const packages = Array.isArray(settings.packages) ? settings.packages.filter((value): value is string => typeof value === "string" && value !== name && value !== entry) : [];
144
+ if (enabled) packages.push(entry);
145
+ if (packages.length) settings.packages = packages; else delete settings.packages;
146
+ await this.writeSettings("default", settings);
147
+ return (await this.packages("default")).find((item) => item.name === name)!;
148
+ }
149
+
150
+ private async packageExtensions(settings: ProfileSettings): Promise<Array<{ name: string; path: string; package: string }>> {
151
+ const entries: Array<{ name: string; path: string; package: string }> = [];
152
+ for (const pkg of await this.configuredPackages(settings)) {
153
+ 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 });
154
+ }
155
+ return entries;
156
+ }
157
+
158
+ private async packageToolSources(settings: ProfileSettings, names: Iterable<string>): Promise<Map<string, string>> {
159
+ const sources = new Map<string, string>(), tools = [...names];
160
+ for (const extension of await this.packageExtensions(settings)) try {
161
+ const source = await readFile(extension.path, "utf8");
162
+ for (const tool of tools) if (source.includes(`"${tool}"`) || source.includes(`'${tool}'`) || source.includes(`\`${tool}\``)) sources.set(tool, extension.package);
163
+ 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);
164
+ } catch {}
165
+ return sources;
166
+ }
167
+
168
+ async resources(profile: string, kind: ResourceKind): Promise<Resource[]> {
169
+ const settings = await this.readSettings(profile);
170
+ const runtimeSettings = profile === "default" ? settings : await this.readSettings("default");
171
+ if (kind === "skills") return this.skillResources(profile, settings);
172
+ if (kind === "tools") {
173
+ const sources = await this.packageToolSources(runtimeSettings, this.extensionTools);
174
+ const names = new Set([...this.extensionTools, ...sources.keys()]);
175
+ return [...BUILTIN_TOOLS.map((name) => ({ name, kind, source: "builtin" as const, enabled: this.enabled(settings, kind, name) })), ...[...names].sort().map((name) => sources.has(name) ? ({ name, kind, source: "package" as const, package: sources.get(name), enabled: this.enabled(settings, kind, name) }) : ({ name, kind, source: "extension" as const, enabled: this.enabled(settings, kind, name) }))];
176
+ }
177
+ return [...(await this.names("extensions")).map(({ name, path }) => ({ name, kind, path, source: "shared" as const, enabled: protectedExtensions.has(name) ? true : this.enabled(runtimeSettings, kind, name, path), protected: protectedExtensions.has(name) || undefined })), ...(await this.packageExtensions(runtimeSettings)).map(({ name, path, package: packageName }) => ({ name, kind, path, source: "package" as const, package: packageName, enabled: true }))];
178
+ }
179
+
180
+ async resource(profile: string, kind: ResourceKind, name: string, source?: "shared" | "profile"): Promise<Resource> {
181
+ const resource = (await this.resources(profile, kind)).find((item) => item.name === name && (!source || item.source === source));
182
+ if (!resource) throw Object.assign(new Error("Resource not found"), { status: 404 });
183
+ return resource;
184
+ }
185
+
186
+ private async skillPaths(name: string, settings: ProfileSettings): Promise<string[]> {
187
+ return (await this.skillResources(name, settings)).filter((skill) => skill.enabled).map((skill) => skill.path!).filter(Boolean);
188
+ }
189
+
190
+ private async materializeResources(name: string, settings: ProfileSettings): Promise<ProfileSettings> {
191
+ const profile = { ...(settings.profile ?? {}) };
192
+ if (name !== "default") delete profile.enabledExtensions;
193
+ const normalized: ProfileSettings = {
194
+ ...settings,
195
+ skills: await this.skillPaths(name, settings),
196
+ prompts: [join(this.agentDir, "prompts")],
197
+ themes: [join(this.agentDir, "themes")],
198
+ profile,
199
+ };
200
+ if (name !== "default") {
201
+ delete normalized.extensions;
202
+ delete normalized.packages;
203
+ }
204
+ return normalized;
205
+ }
206
+
207
+ async writeSettings(name: string, settings: ProfileSettings): Promise<ProfileSettings> {
208
+ const normalized = await this.materializeResources(name, settings);
209
+ await mkdir(this.directory(name), { recursive: true });
210
+ await writeFile(this.settingsPath(name), `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
211
+ return normalized;
212
+ }
213
+
214
+ skillImportDirectory(profile: string): string { return profile === "default" ? join(this.agentDir, "skills") : join(this.directory(profile), "skills"); }
215
+
216
+ async refreshSkills(profile: string): Promise<void> {
217
+ const settings = await this.readSettings(profile);
218
+ if (profile !== "default") settings.profile = { ...(settings.profile ?? {}), skillSources: { ...this.skillSources(profile, settings), profile: true } };
219
+ await this.writeSettings(profile, settings);
220
+ }
221
+
222
+ async readSkillSources(profile: string): Promise<Required<SkillSources>> { return this.skillSources(profile, await this.readSettings(profile)); }
223
+
224
+ async writeSkillSources(profile: string, sources: SkillSources): Promise<Required<SkillSources>> {
225
+ if (profile === "default") throw Object.assign(new Error("The default profile always uses Shared Skills."), { status: 422 });
226
+ if (typeof sources.shared !== "boolean" || typeof sources.profile !== "boolean" || (!sources.shared && !sources.profile)) throw Object.assign(new Error("Enable at least one skill source."), { status: 400 });
227
+ const settings = await this.readSettings(profile);
228
+ settings.profile = { ...(settings.profile ?? {}), skillSources: sources };
229
+ await this.writeSettings(profile, settings);
230
+ await mkdir(this.skillBase(profile, "profile"), { recursive: true });
231
+ await ensureProfileSandbox(this.directory(profile), resolve(process.argv[1]));
232
+ return this.skillSources(profile, settings);
233
+ }
234
+
235
+ async readSkillDocument(profile: string, name: string, source?: "shared" | "profile"): Promise<{ name: string; frontmatter: Record<string, string>; content: string }> {
236
+ const skill = await this.resource(profile, "skills", name, source);
237
+ if (!skill.path) throw Object.assign(new Error("Skill document not found"), { status: 404 });
238
+ const document = await readFile(join(skill.path, "SKILL.md"), "utf8");
239
+ const match = document.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
240
+ const frontmatter: Record<string, string> = {};
241
+ if (match) {
242
+ for (const line of match[1].split(/\r?\n/)) {
243
+ const separator = line.indexOf(":");
244
+ if (separator > 0) frontmatter[line.slice(0, separator).trim()] = line.slice(separator + 1).trim().replace(/^['\"]|['\"]$/g, "");
245
+ }
246
+ }
247
+ return { name, frontmatter, content: match ? document.slice(match[0].length) : document };
248
+ }
249
+
250
+ async setResource(profile: string, kind: ResourceKind, name: string, enabled: boolean, source?: "shared" | "profile"): Promise<Resource> {
251
+ const current = await this.resource(profile, kind, name, source);
252
+ if (current.protected && !enabled) throw Object.assign(new Error("This extension is required and cannot be disabled."), { status: 422 });
253
+ // Extensions are shared runtime resources, so toggling one always updates
254
+ // the default runtime rather than materializing it in a profile workspace.
255
+ const settingsProfile = kind === "extensions" ? "default" : profile;
256
+ const settings = await this.readSettings(settingsProfile);
257
+ const key: PolicyKey = kind === "skills" ? (current.source === "profile" ? "enabledProfileSkills" : "enabledSkills") : policyKey[kind];
258
+ const allNames = (await this.resources(profile, kind)).filter((item) => kind !== "skills" || item.source === current.source).map((item) => item.name);
259
+ const selected = new Set(settings.profile?.[key]?.includes("*") || !settings.profile?.[key] ? allNames : settings.profile![key]);
260
+ if (enabled) selected.add(name);
261
+ else selected.delete(name);
262
+ settings.profile = { ...(settings.profile ?? {}), [key]: [...selected].sort() };
263
+ await this.writeSettings(settingsProfile, settings);
264
+ return this.resource(profile, kind, name, source);
265
+ }
266
+
267
+ async deleteSkill(profile: string, name: string, source?: "shared" | "profile"): Promise<void> {
268
+ const current = await this.resource(profile, "skills", name, source);
269
+ if (current.source === "shared" && profile !== "default") { await this.setResource(profile, "skills", name, false, "shared"); return; }
270
+ if (!current.path) throw Object.assign(new Error("Skill path not found."), { status: 404 });
271
+ await rm(current.path, { recursive: true, force: false });
272
+ await this.refreshSkills(profile);
273
+ }
274
+
275
+ async create(name: string): Promise<{ name: string; path: string }> {
276
+ if (!validName(name) || name === "default") throw Object.assign(new Error("Invalid profile name"), { status: 400 });
277
+ const destination = this.directory(name);
278
+ if (existsSync(destination)) throw Object.assign(new Error("Profile already exists"), { status: 409 });
279
+ await mkdir(join(destination, "sessions"), { recursive: true });
280
+ let base: ProfileSettings = {};
281
+ try { base = await this.readSettings("default"); } catch {}
282
+ const { packages: _packages, extensions: _extensions, ...profileBase } = base;
283
+ const settings: ProfileSettings = {
284
+ ...profileBase,
285
+ defaultTools: BUILTIN_TOOLS,
286
+ sandbox: true,
287
+ profile: { enabledTools: ["*"], enabledSkills: ["*"], enabledProfileSkills: ["*"], skillSources: { shared: true, profile: false } },
288
+ };
289
+ await this.writeSettings(name, settings);
290
+ await ensureProfileSandbox(destination, resolve(process.argv[1]));
291
+ await writeFile(join(destination, "SOUL.md"), "", "utf8");
292
+ await writeFile(join(destination, "guardrails.json"), "{\n \"guardrails\": []\n}\n", "utf8");
293
+ for (const file of ["auth.json", "models.json"]) {
294
+ const source = join(this.agentDir, file);
295
+ if (existsSync(source)) await copyFile(source, join(destination, file));
296
+ }
297
+ return { name, path: destination };
298
+ }
299
+
300
+ async delete(name: string): Promise<void> {
301
+ if (name === "default") throw Object.assign(new Error("The default profile cannot be deleted"), { status: 400 });
302
+ await this.readSettings(name);
303
+ await rm(this.directory(name), { recursive: true, force: false });
304
+ }
305
+
306
+ async readApplicationWorkingSessions(profile: string, application: string): Promise<Record<string, { sessionId: string; updatedAt: string; reason: "message" | "rollover" }>> {
307
+ await this.readSettings(profile);
308
+ try {
309
+ const value = JSON.parse(await readFile(join(this.directory(profile), "application-sessions.json"), "utf8")) as { applications?: Record<string, unknown> };
310
+ const sessions = value.applications?.[application];
311
+ return sessions && typeof sessions === "object" && !Array.isArray(sessions) ? sessions as Record<string, { sessionId: string; updatedAt: string; reason: "message" | "rollover" }> : {};
312
+ } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return {}; throw error; }
313
+ }
314
+
315
+ async setApplicationWorkingSession(profile: string, application: string, key: string, session: { sessionId: string; updatedAt: string; reason: "message" | "rollover" }): Promise<void> {
316
+ await this.readSettings(profile);
317
+ const path = join(this.directory(profile), "application-sessions.json");
318
+ let value: { applications: Record<string, Record<string, unknown>> } = { applications: {} };
319
+ try { value = JSON.parse(await readFile(path, "utf8")) as typeof value; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
320
+ value.applications ??= {}; value.applications[application] ??= {}; value.applications[application][key] = session;
321
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
322
+ }
323
+
324
+ async readGuardrailsConfig(profile: string): Promise<string> {
325
+ await this.readSettings(profile);
326
+ try { return await readFile(join(this.directory(profile), "guardrails.json"), "utf8"); }
327
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return "{\n \"guardrails\": []\n}\n"; throw error; }
328
+ }
329
+
330
+ async writeGuardrailsConfig(profile: string, content: string): Promise<void> {
331
+ await this.readSettings(profile);
332
+ if (typeof content !== "string") throw Object.assign(new Error("The 'content' field must be a string."), { status: 400 });
333
+ JSON.parse(content);
334
+ await writeFile(join(this.directory(profile), "guardrails.json"), content, "utf8");
335
+ }
336
+
337
+ private guardrailPath(file: string): string {
338
+ if (typeof file !== "string" || file !== file.split("/").pop() || !file.endsWith(".md")) throw Object.assign(new Error("Invalid guardrail file"), { status: 400 });
339
+ return join(this.agentDir, "guardrails", file);
340
+ }
341
+
342
+ async readGuardrailDocument(profile: string, file: string): Promise<string> {
343
+ await this.readSettings(profile);
344
+ try { return await readFile(this.guardrailPath(file), "utf8"); }
345
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return ""; throw error; }
346
+ }
347
+
348
+ async writeGuardrailDocument(profile: string, file: string, content: string): Promise<void> {
349
+ await this.readSettings(profile);
350
+ if (typeof content !== "string") throw Object.assign(new Error("The 'content' field must be a string."), { status: 400 });
351
+ const path = this.guardrailPath(file);
352
+ await mkdir(join(this.agentDir, "guardrails"), { recursive: true });
353
+ await writeFile(path, content, "utf8");
354
+ }
355
+
356
+ async readDocument(profile: string, document: "SOUL.md" | "REFINE.md"): Promise<string> {
357
+ await this.readSettings(profile);
358
+ try { return await readFile(join(this.directory(profile), document), "utf8"); }
359
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return ""; throw error; }
360
+ }
361
+
362
+ async writeDocument(profile: string, document: "SOUL.md" | "REFINE.md", content: string): Promise<void> {
363
+ await this.readSettings(profile);
364
+ if (typeof content !== "string") throw Object.assign(new Error("The 'content' field must be a string."), { status: 400 });
365
+ await writeFile(join(this.directory(profile), document), content, "utf8");
366
+ }
367
+ }