@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.
@@ -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"}