@artale/pi-pai 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +32 -0
- package/INSTALL.md +224 -0
- package/README.md +192 -138
- package/SYSTEM.md +120 -0
- package/SYSTEM.md.pai +120 -204
- package/VERSION +1 -0
- package/dist/extension.d.ts +32 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +1118 -0
- package/dist/extension.js.map +1 -0
- package/dist/lifeos-pai-core.d.ts +15 -0
- package/dist/lifeos-pai-core.d.ts.map +1 -0
- package/dist/lifeos-pai-core.js +290 -0
- package/dist/lifeos-pai-core.js.map +1 -0
- package/memory/learning/.gitkeep +0 -0
- package/memory/state/.gitkeep +0 -0
- package/memory/work/.gitkeep +0 -0
- package/models.json +43 -0
- package/package.json +21 -7
- package/settings.json +14 -0
- package/src/lifeos-pai-core.ts +608 -0
- package/agents/context_bundles/MON_14_unknown.jsonl +0 -1
- package/src/pi-pai-core.ts +0 -215
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAI Core Extension for Pi
|
|
3
|
+
*
|
|
4
|
+
* Ports the key PAI capabilities to Pi's extension system:
|
|
5
|
+
* - Voice notifications (optional TTS integration)
|
|
6
|
+
* - Security validation (blocks dangerous commands)
|
|
7
|
+
* - Session lifecycle (startup greeting, shutdown logging)
|
|
8
|
+
* - PRD work tracking (Algorithm methodology support)
|
|
9
|
+
* - Learning signal capture (cross-session improvement)
|
|
10
|
+
* - Telos Life OS (goals, projects, wisdom tracking)
|
|
11
|
+
* - PAI Algorithm session management
|
|
12
|
+
* - Content analysis and research tools
|
|
13
|
+
*
|
|
14
|
+
* Based on PAI v4.0.3 — https://github.com/danielmiessler/PAI
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { Type } from "@sinclair/typebox";
|
|
19
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, readdirSync } from "fs";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
|
|
23
|
+
// ─── Configuration via environment variables ────────────────
|
|
24
|
+
const PAI_PI_DIR = process.env.PAI_PI_DIR || join(homedir(), ".config", "PAI-pi");
|
|
25
|
+
const MEMORY_DIR = join(PAI_PI_DIR, "memory");
|
|
26
|
+
const WORK_DIR = join(MEMORY_DIR, "work");
|
|
27
|
+
const LEARNING_DIR = join(MEMORY_DIR, "learning");
|
|
28
|
+
const STATE_DIR = join(MEMORY_DIR, "state");
|
|
29
|
+
|
|
30
|
+
// Voice configuration — set these env vars to enable TTS
|
|
31
|
+
const VOICE_ENDPOINT = process.env.PAI_VOICE_ENDPOINT || "http://localhost:8888/notify";
|
|
32
|
+
const VOICE_ID = process.env.PAI_VOICE_ID || "";
|
|
33
|
+
const VOICE_ENABLED = process.env.PAI_VOICE_ENABLED === "true";
|
|
34
|
+
|
|
35
|
+
const STATE_FILE = join(STATE_DIR, "last-session.json");
|
|
36
|
+
|
|
37
|
+
// Ensure directories exist
|
|
38
|
+
for (const dir of [MEMORY_DIR, WORK_DIR, LEARNING_DIR, STATE_DIR]) {
|
|
39
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DANGEROUS_PATTERNS = [
|
|
43
|
+
/rm\s+(-rf?|--recursive)\s+[\/~]/,
|
|
44
|
+
/rm\s+-rf?\s+\./,
|
|
45
|
+
/git\s+push\s+.*--force/,
|
|
46
|
+
/git\s+reset\s+--hard/,
|
|
47
|
+
/git\s+clean\s+-f/,
|
|
48
|
+
/git\s+checkout\s+\./,
|
|
49
|
+
/drop\s+table/i,
|
|
50
|
+
/truncate\s+table/i,
|
|
51
|
+
/:\(\)\{ :\|:& \};:/,
|
|
52
|
+
/mkfs\./,
|
|
53
|
+
/dd\s+if=/,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const sendVoiceNotification = async (message: string): Promise<boolean> => {
|
|
57
|
+
if (!VOICE_ENABLED || !VOICE_ID) return false;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(VOICE_ENDPOINT, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
message,
|
|
67
|
+
voice_id: VOICE_ID,
|
|
68
|
+
voice_enabled: true,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
return response.ok;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default function (pi: ExtensionAPI) {
|
|
78
|
+
// ─── Voice Notification Tool ─────────────────────────
|
|
79
|
+
pi.registerTool({
|
|
80
|
+
name: "voice_notify",
|
|
81
|
+
label: "Voice",
|
|
82
|
+
description: "Send a voice notification via TTS server (requires PAI_VOICE_ENDPOINT and PAI_VOICE_ID env vars)",
|
|
83
|
+
parameters: Type.Object({
|
|
84
|
+
message: Type.String({ description: "Text to speak" }),
|
|
85
|
+
}),
|
|
86
|
+
async execute(_toolCallId, params) {
|
|
87
|
+
if (!VOICE_ENABLED || !VOICE_ID) {
|
|
88
|
+
return { details: undefined, content: [{ type: "text", text: "Voice disabled (set PAI_VOICE_ENABLED=true and PAI_VOICE_ID)" }] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sent = await sendVoiceNotification(params.message);
|
|
92
|
+
return sent
|
|
93
|
+
? { details: undefined, content: [{ type: "text", text: `Voice: "${params.message}"` }] }
|
|
94
|
+
: { details: undefined, content: [{ type: "text", text: "Voice server unavailable" }] };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── Security: Block dangerous commands ──────────────
|
|
99
|
+
pi.on("tool_call", async (event) => {
|
|
100
|
+
const { isToolCallEventType } = await import("@mariozechner/pi-coding-agent");
|
|
101
|
+
if (!isToolCallEventType("bash", event)) return;
|
|
102
|
+
const cmd = (event as { input?: { command?: string } }).input?.command || "";
|
|
103
|
+
|
|
104
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
105
|
+
if (pattern.test(cmd)) {
|
|
106
|
+
return {
|
|
107
|
+
block: true,
|
|
108
|
+
reason: `BLOCKED: Dangerous command detected. Pattern: ${pattern.source}. Ask before proceeding.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ─── Session Start: Voice (non-blocking) ─────────────
|
|
115
|
+
pi.on("session_start", async () => {
|
|
116
|
+
void sendVoiceNotification("PAI online. Ready for work.");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── Session Shutdown: Capture learnings ─────────────
|
|
120
|
+
pi.on("session_shutdown", async () => {
|
|
121
|
+
const timestamp = new Date().toISOString();
|
|
122
|
+
const logPath = join(LEARNING_DIR, "session-log.jsonl");
|
|
123
|
+
const sessionName = pi.getSessionName() || "unnamed";
|
|
124
|
+
|
|
125
|
+
const entry = {
|
|
126
|
+
timestamp,
|
|
127
|
+
event: "session_end",
|
|
128
|
+
sessionName,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
133
|
+
writeFileSync(STATE_FILE, JSON.stringify({ ...entry, active: false }, null, 2));
|
|
134
|
+
} catch {
|
|
135
|
+
// Best effort
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─── PRD Management Tool ─────────────────────────────
|
|
140
|
+
pi.registerTool({
|
|
141
|
+
name: "prd_create",
|
|
142
|
+
label: "PRD",
|
|
143
|
+
description: "Create a PRD (Product Requirements Document) for Algorithm work tracking",
|
|
144
|
+
parameters: Type.Object({
|
|
145
|
+
task: Type.String({ description: "8 word task description" }),
|
|
146
|
+
slug: Type.String({ description: "kebab-case slug for the work directory" }),
|
|
147
|
+
effort: Type.String({ description: "standard|extended|advanced|deep|comprehensive" }),
|
|
148
|
+
phase: Type.String({ description: "observe|think|plan|build|execute|verify|learn|complete" }),
|
|
149
|
+
criteria: Type.Optional(Type.Array(Type.String(), { description: "ISC criteria list" })),
|
|
150
|
+
progress: Type.Optional(Type.String({ description: "e.g. 3/8" })),
|
|
151
|
+
}),
|
|
152
|
+
async execute(_toolCallId, params) {
|
|
153
|
+
const workDir = join(WORK_DIR, params.slug);
|
|
154
|
+
if (!existsSync(workDir)) mkdirSync(workDir, { recursive: true });
|
|
155
|
+
|
|
156
|
+
const timestamp = new Date().toISOString();
|
|
157
|
+
const criteriaBlock = params.criteria
|
|
158
|
+
? params.criteria.map((c, i) => `- [ ] ISC-${i + 1}: ${c}`).join("\n")
|
|
159
|
+
: "";
|
|
160
|
+
|
|
161
|
+
const content = `---
|
|
162
|
+
task: ${params.task}
|
|
163
|
+
slug: ${params.slug}
|
|
164
|
+
effort: ${params.effort}
|
|
165
|
+
phase: ${params.phase}
|
|
166
|
+
progress: ${params.progress || "0/0"}
|
|
167
|
+
started: ${timestamp}
|
|
168
|
+
updated: ${timestamp}
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Context
|
|
172
|
+
|
|
173
|
+
${params.task}
|
|
174
|
+
|
|
175
|
+
## Criteria
|
|
176
|
+
|
|
177
|
+
${criteriaBlock}
|
|
178
|
+
|
|
179
|
+
## Decisions
|
|
180
|
+
|
|
181
|
+
## Verification
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
const prdPath = join(workDir, "PRD.md");
|
|
185
|
+
writeFileSync(prdPath, content);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
details: { path: prdPath, slug: params.slug },
|
|
189
|
+
content: [{ type: "text", text: `PRD created at ${prdPath}` }],
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ─── PRD Update Tool ─────────────────────────────────
|
|
195
|
+
pi.registerTool({
|
|
196
|
+
name: "prd_update",
|
|
197
|
+
label: "PRD Update",
|
|
198
|
+
description: "Update PRD phase, progress, or mark criteria complete",
|
|
199
|
+
parameters: Type.Object({
|
|
200
|
+
slug: Type.String({ description: "PRD slug" }),
|
|
201
|
+
phase: Type.Optional(Type.String({ description: "New phase" })),
|
|
202
|
+
progress: Type.Optional(Type.String({ description: "e.g. 5/8" })),
|
|
203
|
+
complete_criteria: Type.Optional(
|
|
204
|
+
Type.Array(Type.Number(), { description: "ISC numbers to mark complete" })
|
|
205
|
+
),
|
|
206
|
+
}),
|
|
207
|
+
async execute(_toolCallId, params) {
|
|
208
|
+
const prdPath = join(WORK_DIR, params.slug, "PRD.md");
|
|
209
|
+
if (!existsSync(prdPath)) {
|
|
210
|
+
return { details: undefined, content: [{ type: "text", text: `PRD not found: ${params.slug}` }] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let content = readFileSync(prdPath, "utf-8");
|
|
214
|
+
const timestamp = new Date().toISOString();
|
|
215
|
+
|
|
216
|
+
if (params.phase) {
|
|
217
|
+
content = content.replace(/^phase: .+$/m, `phase: ${params.phase}`);
|
|
218
|
+
}
|
|
219
|
+
if (params.progress) {
|
|
220
|
+
content = content.replace(/^progress: .+$/m, `progress: ${params.progress}`);
|
|
221
|
+
}
|
|
222
|
+
content = content.replace(/^updated: .+$/m, `updated: ${timestamp}`);
|
|
223
|
+
|
|
224
|
+
if (params.complete_criteria) {
|
|
225
|
+
for (const num of params.complete_criteria) {
|
|
226
|
+
content = content.replace(
|
|
227
|
+
new RegExp(`^- \[ \] ISC-${num}:`, "m"),
|
|
228
|
+
`- [x] ISC-${num}:`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
writeFileSync(prdPath, content);
|
|
234
|
+
return { details: undefined, content: [{ type: "text", text: `PRD updated: ${params.slug}` }] };
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ─── Learning Signal Tool ────────────────────────────
|
|
239
|
+
pi.registerTool({
|
|
240
|
+
name: "lifeos_capture_learning",
|
|
241
|
+
label: "Learn",
|
|
242
|
+
description: "Capture a learning signal or reflection from this session",
|
|
243
|
+
parameters: Type.Object({
|
|
244
|
+
signal_type: Type.String({ description: "rating|reflection|failure|pattern" }),
|
|
245
|
+
content: Type.String({ description: "The learning content" }),
|
|
246
|
+
score: Type.Optional(Type.Number({ description: "Rating 1-10 if applicable" })),
|
|
247
|
+
}),
|
|
248
|
+
async execute(_toolCallId, params) {
|
|
249
|
+
const timestamp = new Date().toISOString();
|
|
250
|
+
const entry = {
|
|
251
|
+
timestamp,
|
|
252
|
+
type: params.signal_type,
|
|
253
|
+
content: params.content,
|
|
254
|
+
score: params.score,
|
|
255
|
+
session: pi.getSessionName() || "unnamed",
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const logPath = join(LEARNING_DIR, "signals.jsonl");
|
|
259
|
+
appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
details: entry,
|
|
263
|
+
content: [{ type: "text", text: `Learning captured: ${params.signal_type}` }],
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ─── Status Line ─────────────────────────────────────
|
|
269
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
270
|
+
ctx.ui.setStatus("pai", "PAI on Pi | LifeOS Core Scaffold");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ─── Slash Commands ──────────────────────────────────
|
|
274
|
+
pi.registerCommand("algorithm", {
|
|
275
|
+
description: "Start an Algorithm session for complex work",
|
|
276
|
+
handler: async (_args, ctx) => {
|
|
277
|
+
ctx.ui.notify(
|
|
278
|
+
"Algorithm mode activated. Use the 7-phase methodology for this task.",
|
|
279
|
+
"info"
|
|
280
|
+
);
|
|
281
|
+
await pi.sendMessage({
|
|
282
|
+
customType: "pai-algorithm",
|
|
283
|
+
content: "Use ALGORITHM mode for the next task. Follow all 7 phases.",
|
|
284
|
+
details: undefined,
|
|
285
|
+
} as any, { triggerTurn: false });
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
pi.registerCommand("status", {
|
|
290
|
+
description: "Show PAI system status",
|
|
291
|
+
handler: async (_args, ctx) => {
|
|
292
|
+
const sessionCount = (() => {
|
|
293
|
+
try {
|
|
294
|
+
const logPath = join(LEARNING_DIR, "session-log.jsonl");
|
|
295
|
+
if (!existsSync(logPath)) return 0;
|
|
296
|
+
return readFileSync(logPath, "utf-8").trim().split("\n").length;
|
|
297
|
+
} catch {
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
})();
|
|
301
|
+
|
|
302
|
+
const signalCount = (() => {
|
|
303
|
+
try {
|
|
304
|
+
const logPath = join(LEARNING_DIR, "signals.jsonl");
|
|
305
|
+
if (!existsSync(logPath)) return 0;
|
|
306
|
+
return readFileSync(logPath, "utf-8").trim().split("\n").length;
|
|
307
|
+
} catch {
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
})();
|
|
311
|
+
|
|
312
|
+
ctx.ui.notify(
|
|
313
|
+
`PAI on Pi | Sessions: ${sessionCount} | Signals: ${signalCount}`,
|
|
314
|
+
"info"
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pi.registerCommand("voice", {
|
|
320
|
+
description: "Send a voice notification",
|
|
321
|
+
handler: async (args, _ctx) => {
|
|
322
|
+
const message = args || "Hello from PAI";
|
|
323
|
+
if (!VOICE_ENABLED || !VOICE_ID) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
void sendVoiceNotification(String(message));
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ══════════════════════════════════════════════════════
|
|
331
|
+
// Telos — Life OS (goals, projects, wisdom)
|
|
332
|
+
// ══════════════════════════════════════════════════════
|
|
333
|
+
|
|
334
|
+
const TELOS_DIR = join(PAI_PI_DIR, "telos");
|
|
335
|
+
if (!existsSync(TELOS_DIR)) mkdirSync(TELOS_DIR, { recursive: true });
|
|
336
|
+
|
|
337
|
+
function workDir(slug: string): string {
|
|
338
|
+
const d = join(WORK_DIR, slug);
|
|
339
|
+
if (!existsSync(d)) mkdirSync(d, { recursive: true });
|
|
340
|
+
return d;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function loadDir(dir: string): Record<string, string> {
|
|
344
|
+
const result: Record<string, string> = {};
|
|
345
|
+
if (!existsSync(dir)) return result;
|
|
346
|
+
for (const f of readdirSync(dir)) {
|
|
347
|
+
if (f.endsWith(".md")) {
|
|
348
|
+
const slug = f.replace(/\.md$/, "");
|
|
349
|
+
result[slug] = readFileSync(join(dir, f), "utf-8");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
pi.registerTool({
|
|
356
|
+
name: "telos_goal",
|
|
357
|
+
label: "Goal",
|
|
358
|
+
description: "Create or update a Life OS goal with timeline, dependencies, and projects",
|
|
359
|
+
parameters: Type.Object({
|
|
360
|
+
title: Type.String({ description: "Goal title" }),
|
|
361
|
+
slug: Type.String({ description: "kebab-case slug" }),
|
|
362
|
+
deadline: Type.Optional(Type.String({ description: "Target date (ISO or relative)" })),
|
|
363
|
+
dependencies: Type.Optional(Type.Array(Type.String({ description: "Dependency slugs" }))),
|
|
364
|
+
projects: Type.Optional(Type.Array(Type.String({ description: "Project slugs serving this goal" }))),
|
|
365
|
+
status: Type.Optional(Type.Unsafe<"active" | "paused" | "completed" | "archived">({
|
|
366
|
+
type: "string",
|
|
367
|
+
enum: ["active", "paused", "completed", "archived"],
|
|
368
|
+
default: "active",
|
|
369
|
+
})),
|
|
370
|
+
}),
|
|
371
|
+
async execute(_callId, params) {
|
|
372
|
+
const dir = join(TELOS_DIR, params.slug);
|
|
373
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
374
|
+
const existing = existsSync(join(dir, "goal.md"))
|
|
375
|
+
? readFileSync(join(dir, "goal.md"), "utf-8") : "";
|
|
376
|
+
const now = new Date().toISOString();
|
|
377
|
+
const created = existing.match(/^created: .+$/m)?.[0]?.replace(/^created: /, "") || now;
|
|
378
|
+
const content = `---
|
|
379
|
+
slug: ${params.slug}
|
|
380
|
+
title: ${params.title}
|
|
381
|
+
status: ${params.status || "active"}
|
|
382
|
+
deadline: ${params.deadline || "none"}
|
|
383
|
+
created: ${created}
|
|
384
|
+
updated: ${now}
|
|
385
|
+
dependencies: ${JSON.stringify(params.dependencies || [])}
|
|
386
|
+
projects: ${JSON.stringify(params.projects || [])}
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
# Goal: ${params.title}
|
|
390
|
+
|
|
391
|
+
## Dependency Graph
|
|
392
|
+
${(params.dependencies || []).map((d) => `- ${d}`).join("\n") || "None"}
|
|
393
|
+
|
|
394
|
+
## Projects
|
|
395
|
+
${(params.projects || []).map((p) => `- ${p}`).join("\n") || "None"}
|
|
396
|
+
|
|
397
|
+
## Notes
|
|
398
|
+
`;
|
|
399
|
+
writeFileSync(join(dir, "goal.md"), content);
|
|
400
|
+
return { content: [{ type: "text", text: `Goal saved: ${params.title} (${params.slug})` }], details: {} };
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
pi.registerTool({
|
|
405
|
+
name: "telos_wisdom",
|
|
406
|
+
label: "Wisdom",
|
|
407
|
+
description: "Record a wisdom entry — insight from experience, books, or reflection",
|
|
408
|
+
parameters: Type.Object({
|
|
409
|
+
insight: Type.String({ description: "The wisdom or insight" }),
|
|
410
|
+
source: Type.Optional(Type.String({ description: "Source (book, experience, person, etc.)" })),
|
|
411
|
+
tags: Type.Optional(Type.Array(Type.String({ description: "Category tags" }))),
|
|
412
|
+
impact: Type.Optional(Type.Unsafe<"low" | "medium" | "high" | "life-changing">({
|
|
413
|
+
type: "string",
|
|
414
|
+
enum: ["low", "medium", "high", "life-changing"],
|
|
415
|
+
default: "medium",
|
|
416
|
+
})),
|
|
417
|
+
}),
|
|
418
|
+
async execute(_callId, params) {
|
|
419
|
+
const logPath = join(TELOS_DIR, "wisdom.jsonl");
|
|
420
|
+
const entry = { timestamp: new Date().toISOString(), insight: params.insight, source: params.source || "", tags: params.tags || [], impact: params.impact || "medium" };
|
|
421
|
+
appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
422
|
+
return { content: [{ type: "text", text: `Wisdom saved. Impact: ${params.impact}` }], details: {} };
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
pi.registerTool({
|
|
427
|
+
name: "telos_dashboard",
|
|
428
|
+
label: "Dashboard",
|
|
429
|
+
description: "Generate a Telos dashboard showing goals, projects, and recent wisdom",
|
|
430
|
+
parameters: Type.Object({
|
|
431
|
+
focus: Type.Optional(Type.Unsafe<"all" | "goals" | "wisdom" | "projects">({
|
|
432
|
+
type: "string",
|
|
433
|
+
enum: ["all", "goals", "wisdom", "projects"],
|
|
434
|
+
default: "all",
|
|
435
|
+
})),
|
|
436
|
+
}),
|
|
437
|
+
async execute(_callId, params) {
|
|
438
|
+
const goals = loadDir(TELOS_DIR);
|
|
439
|
+
const wisdomPath = join(TELOS_DIR, "wisdom.jsonl");
|
|
440
|
+
const wisdomEntries: Array<Record<string, string>> = [];
|
|
441
|
+
if (existsSync(wisdomPath)) {
|
|
442
|
+
const lines = readFileSync(wisdomPath, "utf-8").trim().split("\n");
|
|
443
|
+
for (const line of lines.slice(-10)) {
|
|
444
|
+
try { wisdomEntries.push(JSON.parse(line)); } catch { /* skip */ }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const sections: string[] = [];
|
|
448
|
+
if (params.focus === "all" || params.focus === "goals") {
|
|
449
|
+
sections.push("## Goals");
|
|
450
|
+
for (const [slug, content] of Object.entries(goals)) {
|
|
451
|
+
const title = content.match(/^title: (.+)$/m)?.[1] || slug;
|
|
452
|
+
const status = content.match(/^status: (.+)$/m)?.[1] || "?";
|
|
453
|
+
sections.push(`- **${title}** — ${status}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (params.focus === "all" || params.focus === "wisdom") {
|
|
457
|
+
sections.push("\n## Recent Wisdom");
|
|
458
|
+
for (const w of wisdomEntries.reverse()) {
|
|
459
|
+
sections.push(`- [${w.impact}] ${w.insight}${w.source ? ` — ${w.source}` : ""}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return { content: [{ type: "text", text: sections.join("\n") || "No data yet." }], details: {} };
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// ══════════════════════════════════════════════════════
|
|
467
|
+
// PAI Algorithm — session management
|
|
468
|
+
// ══════════════════════════════════════════════════════
|
|
469
|
+
|
|
470
|
+
pi.registerTool({
|
|
471
|
+
name: "algorithm_start",
|
|
472
|
+
label: "Algorithm",
|
|
473
|
+
description: "Start a PAI Algorithm session with structured PRD tracking",
|
|
474
|
+
parameters: Type.Object({
|
|
475
|
+
task: Type.String({ description: "Task description" }),
|
|
476
|
+
slug: Type.String({ description: "kebab-case slug for work directory" }),
|
|
477
|
+
effort: Type.Optional(Type.Unsafe<"standard" | "extended" | "advanced" | "deep" | "comprehensive">({
|
|
478
|
+
type: "string",
|
|
479
|
+
enum: ["standard", "extended", "advanced", "deep", "comprehensive"],
|
|
480
|
+
default: "standard",
|
|
481
|
+
})),
|
|
482
|
+
criteria: Type.Optional(Type.Array(Type.String({ description: "ISC criteria" }))),
|
|
483
|
+
}),
|
|
484
|
+
async execute(_callId, params) {
|
|
485
|
+
const dir = workDir(params.slug);
|
|
486
|
+
const prdPath = join(dir, "PRD.md");
|
|
487
|
+
if (existsSync(prdPath)) {
|
|
488
|
+
return { content: [{ type: "text", text: `PRD already exists at ${prdPath} — use algorithm_advance to update phase/progress instead.` }], details: {} };
|
|
489
|
+
}
|
|
490
|
+
const criteriaBlock = params.criteria
|
|
491
|
+
? params.criteria.map((c, i) => `- [ ] ISC-${i + 1}: ${c}`).join("\n")
|
|
492
|
+
: "- [ ] ISC-1: TBD";
|
|
493
|
+
const content = `---
|
|
494
|
+
task: ${params.task}
|
|
495
|
+
slug: ${params.slug}
|
|
496
|
+
effort: ${params.effort || "standard"}
|
|
497
|
+
phase: observe
|
|
498
|
+
progress: 0/${params.criteria?.length || 0}
|
|
499
|
+
started: ${new Date().toISOString()}
|
|
500
|
+
updated: ${new Date().toISOString()}
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Task
|
|
504
|
+
${params.task}
|
|
505
|
+
|
|
506
|
+
## Phases
|
|
507
|
+
1. OBSERVE — Define ISC
|
|
508
|
+
2. THINK — Risks, premortem
|
|
509
|
+
3. PLAN — Design approach
|
|
510
|
+
4. BUILD — Prepare artifacts
|
|
511
|
+
5. EXECUTE — Mark ISC as they pass
|
|
512
|
+
6. VERIFY — Test every ISC
|
|
513
|
+
7. LEARN — Reflect
|
|
514
|
+
|
|
515
|
+
## ISC
|
|
516
|
+
${criteriaBlock}
|
|
517
|
+
`;
|
|
518
|
+
writeFileSync(join(dir, "PRD.md"), content);
|
|
519
|
+
return { content: [{ type: "text", text: `Algorithm PRD: ${params.slug} — OBSERVE phase.` }], details: {} };
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
pi.registerTool({
|
|
524
|
+
name: "algorithm_advance",
|
|
525
|
+
label: "Advance",
|
|
526
|
+
description: "Advance the Algorithm phase or mark ISC complete",
|
|
527
|
+
parameters: Type.Object({
|
|
528
|
+
slug: Type.String({ description: "PRD slug" }),
|
|
529
|
+
phase: Type.Optional(Type.Unsafe<"observe" | "think" | "plan" | "build" | "execute" | "verify" | "learn" | "complete">({
|
|
530
|
+
type: "string",
|
|
531
|
+
enum: ["observe", "think", "plan", "build", "execute", "verify", "learn", "complete"],
|
|
532
|
+
})),
|
|
533
|
+
complete: Type.Optional(Type.Array(Type.Number({ description: "ISC numbers to mark done" }))),
|
|
534
|
+
}),
|
|
535
|
+
async execute(_callId, params) {
|
|
536
|
+
const prdPath = join(WORK_DIR, params.slug, "PRD.md");
|
|
537
|
+
if (!existsSync(prdPath)) {
|
|
538
|
+
return { content: [{ type: "text", text: `PRD not found: ${params.slug}` }], details: {} };
|
|
539
|
+
}
|
|
540
|
+
let content = readFileSync(prdPath, "utf-8");
|
|
541
|
+
const now = new Date().toISOString();
|
|
542
|
+
content = content.replace(/^updated: .+$/m, `updated: ${now}`);
|
|
543
|
+
if (params.phase) {
|
|
544
|
+
content = content.replace(/^phase: .+$/m, `phase: ${params.phase}`);
|
|
545
|
+
}
|
|
546
|
+
if (params.complete) {
|
|
547
|
+
for (const n of params.complete) {
|
|
548
|
+
content = content.replace(new RegExp(`^- \\[ \\] ISC-${n}:`, "m"), `- [x] ISC-${n}:`);
|
|
549
|
+
}
|
|
550
|
+
const done = (content.match(/- \[x\] ISC-/g) || []).length;
|
|
551
|
+
const total = (content.match(/- \[ \] ISC-/g) || []).length + done;
|
|
552
|
+
content = content.replace(/^progress: .+$/m, `progress: ${done}/${total}`);
|
|
553
|
+
}
|
|
554
|
+
writeFileSync(prdPath, content);
|
|
555
|
+
return { content: [{ type: "text", text: `PRD updated: ${params.slug} → ${params.phase || "progress"}` }], details: {} };
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// ══════════════════════════════════════════════════════
|
|
560
|
+
// Content analysis + research
|
|
561
|
+
// ══════════════════════════════════════════════════════
|
|
562
|
+
|
|
563
|
+
pi.registerTool({
|
|
564
|
+
name: "content_analyze",
|
|
565
|
+
label: "Analyze",
|
|
566
|
+
description: "Extract wisdom and key insights from any content (URL, text, file)",
|
|
567
|
+
parameters: Type.Object({
|
|
568
|
+
content: Type.String({ description: "Text content or URL to analyze" }),
|
|
569
|
+
focus: Type.Optional(Type.String({ description: "Specific angle to analyze for" })),
|
|
570
|
+
}),
|
|
571
|
+
async execute(_callId, params) {
|
|
572
|
+
workDir("analysis-" + Date.now().toString(36));
|
|
573
|
+
const truncated = params.content.length > 3000;
|
|
574
|
+
const body = truncated ? params.content.slice(0, 3000) : params.content;
|
|
575
|
+
const truncWarn = truncated ? ` (truncated from ${params.content.length} chars to 3000)` : "";
|
|
576
|
+
return { content: [{ type: "text", text: `Analyzing content${params.focus ? `, focusing on: ${params.focus}` : ""}${truncWarn}.\n\n## Content\n${body}\n\nExtract wisdom, key quotes, actionable insights, and cross-references.` }], details: {} };
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
pi.registerTool({
|
|
581
|
+
name: "research_start",
|
|
582
|
+
label: "Research",
|
|
583
|
+
description: "Start a structured research investigation",
|
|
584
|
+
parameters: Type.Object({
|
|
585
|
+
question: Type.String({ description: "Research question or topic" }),
|
|
586
|
+
sub_questions: Type.Optional(Type.Array(Type.String({ description: "Sub-questions to explore" }))),
|
|
587
|
+
}),
|
|
588
|
+
async execute(_callId, params) {
|
|
589
|
+
workDir("research-" + Date.now().toString(36));
|
|
590
|
+
return {
|
|
591
|
+
content: [{ type: "text", text: `Research on: ${params.question}\n\nDecompose into sub-questions, search across multiple angles, extract findings with sources, then synthesize.${params.sub_questions?.length ? `\n\nSub-questions:\n${params.sub_questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}` : ""}` }],
|
|
592
|
+
details: {},
|
|
593
|
+
};
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// ══════════════════════════════════════════════════════
|
|
598
|
+
// Commands
|
|
599
|
+
// ══════════════════════════════════════════════════════
|
|
600
|
+
|
|
601
|
+
pi.registerCommand("telos", {
|
|
602
|
+
description: "Life OS dashboard. Usage: /telos [goals|wisdom|all]",
|
|
603
|
+
handler: async (args, ctx) => {
|
|
604
|
+
const focus = (args || "all").toLowerCase();
|
|
605
|
+
ctx.ui.notify(`Telos dashboard — use telos_dashboard tool with focus=${focus}`, "info");
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"operation": "prompt", "prompt": "<user_query>\nhi\n</user_query>", "timestamp": "2026-05-18T14:12:26.348907"}
|