@tekmidian/pai 0.6.5 → 0.7.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,50 @@
1
+ {
2
+ "name": "@tekmidian/pai",
3
+ "displayName": "PAI Knowledge OS",
4
+ "description": "Personal AI Infrastructure — persistent memory, session continuity, and knowledge graph for Claude Code",
5
+ "version": "0.7.0",
6
+ "author": "Matthias Nott",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/mnott/PAI",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/mnott/PAI"
12
+ },
13
+ "keywords": [
14
+ "memory",
15
+ "knowledge-os",
16
+ "session-continuity",
17
+ "mcp",
18
+ "personal-ai",
19
+ "hooks",
20
+ "skills",
21
+ "zettelkasten"
22
+ ],
23
+ "engines": {
24
+ "claude-code": ">=1.0.0",
25
+ "node": ">=20.0.0"
26
+ },
27
+ "setup": "pai setup",
28
+ "mcp": {
29
+ "server": "dist/daemon-mcp/index.mjs",
30
+ "daemon": "dist/daemon/index.mjs"
31
+ },
32
+ "hooks": "plugins/core/hooks/hooks.json",
33
+ "skills": [
34
+ "plugins/core/skills/",
35
+ "plugins/productivity/skills/",
36
+ "plugins/observability/skills/",
37
+ "plugins/zettelkasten/skills/",
38
+ "plugins/creative/skills/"
39
+ ],
40
+ "templates": [
41
+ "templates/claude-md.template.md",
42
+ "templates/pai-skill.template.md",
43
+ "templates/agent-prefs.example.md"
44
+ ],
45
+ "userExtensions": {
46
+ "hooks": "user-extensions/hooks/",
47
+ "skills": "user-extensions/skills/",
48
+ "prompts": "src/daemon-mcp/prompts/custom/"
49
+ }
50
+ }
@@ -0,0 +1,365 @@
1
+ # PAI Plugin Architecture
2
+
3
+ Technical reference for PAI's modular plugin system, cross-platform support, user extensions, and monetization tiers.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ PAI is structured as a modular plugin system with 8 named modules organized into 3 pricing tiers. The architecture supports Claude Code (full integration), Cursor (MCP only), and Gemini CLI (MCP only).
10
+
11
+ ```
12
+ PAI Knowledge OS
13
+ ├── Core (free, required)
14
+ │ ├── Memory engine (keyword search, SQLite)
15
+ │ ├── Session management
16
+ │ ├── Project registry
17
+ │ ├── 5 essential hooks
18
+ │ └── 3 essential skills
19
+ ├── Free Extensions
20
+ │ ├── Productivity (Plan, Review, Journal, Research, Share)
21
+ │ ├── UI Customization (tab titles, statusline, tab colors)
22
+ │ └── Context Preservation (compression, relay, checkpoint)
23
+ ├── Pro Extensions
24
+ │ ├── Semantic Search (pgvector, reranking, hybrid)
25
+ │ └── Observability (capture, classify, summarize)
26
+ └── Enterprise Extensions
27
+ ├── Zettelkasten Intelligence (6 graph operations)
28
+ └── Creative Studio (art, story, voice/prosody)
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Module System
34
+
35
+ ### Module Manifest
36
+
37
+ Each module has a `plugins/<module>/plugin.json` that declares:
38
+
39
+ ```json
40
+ {
41
+ "name": "pai-core",
42
+ "displayName": "PAI Core",
43
+ "description": "Core memory engine, session management, and project registry",
44
+ "version": "0.7.0",
45
+ "tier": "free",
46
+ "required": true,
47
+ "depends": [],
48
+ "hooks": "hooks/hooks.json",
49
+ "skills": ["Sessions", "Route", "Name"]
50
+ }
51
+ ```
52
+
53
+ ### Module Inventory
54
+
55
+ | Module | Tier | Hooks | Skills | Description |
56
+ |--------|------|-------|--------|-------------|
57
+ | `core` | free | 6 | 3 | Memory engine, sessions, projects, security |
58
+ | `productivity` | free | 2 | 6 | Plan, Review, Journal, Research, Share, Createskill |
59
+ | `ui` | free | 2 | 0 | Tab titles, statusline, tab coloring |
60
+ | `context-preservation` | free | 3 | 0 | Context compression and relay |
61
+ | `semantic-search` | pro | 0 | 0 | pgvector, reranking, hybrid search |
62
+ | `observability` | pro | 13 | 2 | Event capture, classification, summaries |
63
+ | `zettelkasten` | enterprise | 0 | 5 | Graph operations, vault intelligence |
64
+ | `creative` | enterprise | 0 | 2 | Art direction, story, voice/prosody |
65
+
66
+ ### Hook Distribution
67
+
68
+ Total: 26 hook registrations across 6 modules.
69
+
70
+ **Core (6):** load-core-context, load-project-context, initialize-session, security-validator, stop-hook, pai-session-stop.sh
71
+
72
+ **Productivity (2):** sync-todo-to-md, cleanup-session-files
73
+
74
+ **UI (2):** update-tab-titles, update-tab-on-action
75
+
76
+ **Context Preservation (3):** context-compression-hook, pai-pre-compact.sh, post-compact-inject
77
+
78
+ **Observability (13):** capture-all-events (7 events), observe, inject-observations, capture-tool-output, capture-session-summary, subagent-stop-hook
79
+
80
+ ### Skill Distribution
81
+
82
+ Total: 18 skills across 5 modules.
83
+
84
+ **Core (3):** Sessions, Route, Name
85
+
86
+ **Productivity (6):** Plan, Review, Journal, Research, Share, Createskill
87
+
88
+ **Observability (2):** Observability, SearchHistory
89
+
90
+ **Zettelkasten (5):** VaultConnect, VaultContext, VaultEmerge, VaultOrphans, VaultTrace
91
+
92
+ **Creative (2):** Art, StoryExplanation
93
+
94
+ ---
95
+
96
+ ## Directory Structure
97
+
98
+ ```
99
+ PAI/
100
+ ├── .claude-plugin/
101
+ │ └── plugin.json # Claude Code plugin manifest
102
+ ├── .cursor/
103
+ │ └── plugin.json # Cursor plugin manifest
104
+ ├── gemini-extension.json # Gemini CLI extension manifest
105
+ ├── pai-plugin.json # Canonical module manifest (build reads this)
106
+
107
+ ├── plugins/ # Module definitions
108
+ │ ├── core/
109
+ │ │ ├── plugin.json # Module metadata
110
+ │ │ ├── hooks/
111
+ │ │ │ └── hooks.json # Core hook definitions
112
+ │ │ └── skills/ # (populated by build symlinks)
113
+ │ ├── productivity/
114
+ │ │ ├── plugin.json
115
+ │ │ ├── hooks/
116
+ │ │ │ └── hooks.json
117
+ │ │ └── skills/
118
+ │ ├── ui/
119
+ │ │ ├── plugin.json
120
+ │ │ └── hooks/
121
+ │ │ └── hooks.json
122
+ │ ├── context-preservation/
123
+ │ │ ├── plugin.json
124
+ │ │ └── hooks/
125
+ │ │ └── hooks.json
126
+ │ ├── semantic-search/
127
+ │ │ └── plugin.json
128
+ │ ├── observability/
129
+ │ │ ├── plugin.json
130
+ │ │ ├── hooks/
131
+ │ │ │ └── hooks.json
132
+ │ │ └── skills/
133
+ │ ├── zettelkasten/
134
+ │ │ ├── plugin.json
135
+ │ │ └── skills/
136
+ │ └── creative/
137
+ │ ├── plugin.json
138
+ │ └── skills/
139
+
140
+ ├── user-extensions/ # User customization point (gitignored)
141
+ │ ├── README.md
142
+ │ ├── hooks/
143
+ │ │ └── .gitkeep
144
+ │ └── skills/
145
+ │ └── .gitkeep
146
+
147
+ ├── src/ # Source code (unchanged)
148
+ ├── dist/ # Build output (unchanged)
149
+ ├── templates/ # Setup templates (unchanged)
150
+ └── scripts/ # Build scripts
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Cross-Platform Support
156
+
157
+ ### Claude Code (Full Integration)
158
+
159
+ Claude Code gets the complete PAI experience:
160
+
161
+ | Capability | Support |
162
+ |------------|---------|
163
+ | MCP Tools (9) | Full |
164
+ | MCP Resources (11) | Full |
165
+ | MCP Prompts (18) | Full |
166
+ | Hooks (26 registrations) | Full |
167
+ | Skills (18 SKILL.md stubs) | Full |
168
+ | Statusline | Full |
169
+ | Tab management | Full |
170
+
171
+ Manifest: `.claude-plugin/plugin.json`
172
+
173
+ ### Cursor (MCP Only)
174
+
175
+ Cursor supports MCP servers but not Claude Code's hook or skill system:
176
+
177
+ | Capability | Support |
178
+ |------------|---------|
179
+ | MCP Tools (9) | Full |
180
+ | MCP Resources | Not supported |
181
+ | MCP Prompts | Not supported |
182
+ | Hooks | Not supported |
183
+ | Skills | Not supported (use Cursor Rules instead) |
184
+
185
+ Manifest: `.cursor/plugin.json`
186
+
187
+ To use PAI with Cursor, add to `.cursor/mcp.json`:
188
+ ```json
189
+ {
190
+ "mcpServers": {
191
+ "pai": {
192
+ "command": "node",
193
+ "args": ["/path/to/PAI/dist/daemon-mcp/index.mjs"]
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ ### Gemini CLI (MCP Only)
200
+
201
+ Gemini CLI supports MCP servers via extensions:
202
+
203
+ | Capability | Support |
204
+ |------------|---------|
205
+ | MCP Tools (9) | Full |
206
+ | Hooks | Not supported |
207
+ | Skills | Not supported |
208
+
209
+ Manifest: `gemini-extension.json`
210
+
211
+ ### Codex (Future)
212
+
213
+ OpenAI's Codex supports MCP. When available, a `codex-extension.json` can follow the same pattern.
214
+
215
+ ---
216
+
217
+ ## User Extensions
218
+
219
+ PAI provides three extension points that survive git pull and PAI updates.
220
+
221
+ ### Custom Skills
222
+
223
+ Create `user-extensions/skills/MySkill/SKILL.md`:
224
+
225
+ ```markdown
226
+ ---
227
+ name: MySkill
228
+ description: "What the skill does. USE WHEN user says 'trigger phrase'."
229
+ ---
230
+
231
+ ## My Skill Instructions
232
+
233
+ Your skill content here...
234
+ ```
235
+
236
+ Run `bun run build` to deploy. The build script discovers and symlinks custom skills into `~/.claude/skills/`.
237
+
238
+ ### Custom Hooks
239
+
240
+ Create `user-extensions/hooks/my-hook.ts` or `user-extensions/hooks/my-hook.sh`:
241
+
242
+ TypeScript hooks are compiled during build. Shell hooks are symlinked directly. Register in `~/.claude/settings.json` under the appropriate hook event.
243
+
244
+ ### Custom MCP Prompts
245
+
246
+ Create `src/daemon-mcp/prompts/custom/my-prompt.ts`:
247
+
248
+ ```typescript
249
+ export const myPrompt = {
250
+ description: "What the prompt does",
251
+ content: `## My Prompt
252
+ USE WHEN user says 'trigger phrase'...
253
+ Your prompt content here...`,
254
+ };
255
+ ```
256
+
257
+ Run `bun run build` to generate the skill stub.
258
+
259
+ ### Extension Safety
260
+
261
+ | Location | Gitignored | PAI Updates | Discovery |
262
+ |----------|------------|-------------|-----------|
263
+ | `user-extensions/skills/` | Yes | Never touched | Build sync |
264
+ | `user-extensions/hooks/` | Yes | Never touched | Build compile |
265
+ | `src/daemon-mcp/prompts/custom/` | Yes | Never touched | Build generate |
266
+ | `~/.claude/skills/user/` | N/A (outside repo) | Never touched | Claude Code scanner |
267
+
268
+ ---
269
+
270
+ ## Monetization Architecture
271
+
272
+ ### Tier Model
273
+
274
+ | Tier | Price | Modules |
275
+ |------|-------|---------|
276
+ | Free | $0 | core, productivity, ui, context-preservation |
277
+ | Pro | $9/mo or $79/yr | Free + semantic-search, observability |
278
+ | Enterprise | $29/mo or $249/yr | Pro + zettelkasten, creative |
279
+
280
+ ### Gating Strategy (Future)
281
+
282
+ The tier annotations in `pai-plugin.json` are structural markers for future license gating. The planned approach:
283
+
284
+ 1. License key stored in `~/.config/pai/license.json`
285
+ 2. Signed JWT for offline validation (no phone-home)
286
+ 3. Checked at daemon startup and premium MCP tool invocation
287
+ 4. Graceful degradation: premium features return "upgrade required" message
288
+ 5. `pai license activate <key>` CLI command
289
+
290
+ Currently (v0.7.0): all features ship as free. Tier annotations are informational only.
291
+
292
+ ### What Justifies Each Tier
293
+
294
+ **Pro** ($9/mo):
295
+ - Semantic search is a significant infrastructure requirement (PostgreSQL + pgvector)
296
+ - Cross-encoder reranking adds meaningful relevance improvement
297
+ - Observability provides professional-grade session tracking
298
+ - The value: "Your AI remembers better and you can see what it learned"
299
+
300
+ **Enterprise** ($29/mo):
301
+ - Zettelkasten requires Obsidian + significant graph computation
302
+ - 6 specialized operations (explore, surprise, converse, themes, health, suggest)
303
+ - Creative studio for specialized content creation workflows
304
+ - The value: "Your knowledge graph is actively maintained by AI"
305
+
306
+ ---
307
+
308
+ ## Build System Integration
309
+
310
+ The existing build system continues to work unchanged:
311
+
312
+ ```bash
313
+ bun run build
314
+ # = tsdown (compile TS)
315
+ # + node scripts/build-hooks.mjs --sync (compile hooks, symlink to ~/.claude/Hooks/)
316
+ # + node scripts/build-skill-stubs.mjs --sync (generate skills, symlink to ~/.claude/skills/)
317
+ ```
318
+
319
+ The plugin manifests (`pai-plugin.json`, `.claude-plugin/plugin.json`, etc.) are static JSON files maintained alongside the codebase. They declare the module structure but do not participate in the build process.
320
+
321
+ Future enhancement: a `scripts/build-plugin-manifest.mjs` that generates manifests from the module plugin.json files, ensuring version consistency.
322
+
323
+ ---
324
+
325
+ ## Migration Path
326
+
327
+ ### From Pre-Plugin PAI (v0.6.x)
328
+
329
+ No migration needed. The plugin architecture is purely additive:
330
+
331
+ 1. All existing symlinks continue to work
332
+ 2. `~/.claude/settings.json` hook registrations unchanged
333
+ 3. MCP server registration unchanged
334
+ 4. User skills in `~/.claude/skills/user/` unchanged
335
+ 5. Custom prompts in `src/daemon-mcp/prompts/custom/` unchanged
336
+
337
+ ### For New Users
338
+
339
+ `pai setup` handles everything. The setup wizard installs all modules by default. Users can selectively disable modules later.
340
+
341
+ ---
342
+
343
+ ## Future Roadmap
344
+
345
+ ### Phase 1 (v0.7.0 — Current)
346
+ - Module manifest system
347
+ - Cross-platform manifests
348
+ - User extension points
349
+ - Tier annotations (no enforcement)
350
+
351
+ ### Phase 2 (v0.8.0)
352
+ - `pai plugins list` — show installed modules and tiers
353
+ - `pai plugins enable/disable <module>` — selective module activation
354
+ - Build system reads `pai-plugin.json` to generate platform manifests
355
+
356
+ ### Phase 3 (v0.9.0)
357
+ - License validation system
358
+ - `pai license activate <key>` command
359
+ - Graceful tier gating with upgrade prompts
360
+
361
+ ### Phase 4 (v1.0.0)
362
+ - Plugin marketplace integration
363
+ - Third-party plugin support
364
+ - Plugin dependency resolution
365
+ - Community plugin repository
@@ -73,7 +73,17 @@ async function main() {
73
73
  if (!input || input.trim() === "") {
74
74
  process.exit(0);
75
75
  }
76
- const data = JSON.parse(input);
76
+ if (!process.env.__PAI_HOOK_BG) {
77
+ const { spawn } = await import("child_process");
78
+ const child = spawn(process.execPath, [process.argv[1], "--background"], {
79
+ detached: true,
80
+ stdio: "ignore",
81
+ env: { ...process.env, __PAI_HOOK_BG: "1", __PAI_HOOK_INPUT: input }
82
+ });
83
+ child.unref();
84
+ process.exit(0);
85
+ }
86
+ const data = JSON.parse(process.env.__PAI_HOOK_INPUT || input);
77
87
  const now = /* @__PURE__ */ new Date();
78
88
  const timestamp = now.toISOString().replace(/:/g, "").replace(/\..+/, "").replace("T", "-");
79
89
  const yearMonth = timestamp.substring(0, 7);
@@ -99,7 +109,10 @@ async function analyzeSession(conversationId, yearMonth) {
99
109
  let toolsUsed = /* @__PURE__ */ new Set();
100
110
  try {
101
111
  if (existsSync2(rawOutputsDir)) {
102
- const files = readdirSync(rawOutputsDir).filter((f) => f.endsWith(".jsonl"));
112
+ const todayPrefix = (/* @__PURE__ */ new Date()).toISOString().substring(0, 10);
113
+ const files = readdirSync(rawOutputsDir).filter(
114
+ (f) => f.endsWith(".jsonl") && f.startsWith(todayPrefix)
115
+ );
103
116
  for (const file of files) {
104
117
  const filePath = join2(rawOutputsDir, file);
105
118
  const content = readFileSync2(filePath, "utf-8");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/ts/session-end/capture-session-summary.ts", "../../src/hooks/ts/lib/pai-paths.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * SessionEnd Hook - Captures session summary for UOCS\n *\n * Generates a session summary document when a Claude Code session ends,\n * documenting what was accomplished during the session.\n */\n\nimport { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';\n\ninterface SessionData {\n conversation_id: string;\n timestamp: string;\n [key: string]: any;\n}\n\nasync function main() {\n try {\n // Read input from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n if (!input || input.trim() === '') {\n process.exit(0);\n }\n\n const data: SessionData = JSON.parse(input);\n\n // Generate timestamp for filename\n const now = new Date();\n const timestamp = now.toISOString()\n .replace(/:/g, '')\n .replace(/\\..+/, '')\n .replace('T', '-'); // YYYY-MM-DD-HHMMSS\n\n const yearMonth = timestamp.substring(0, 7); // YYYY-MM\n\n // Try to extract session info from raw outputs\n const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);\n\n // Generate filename\n const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;\n\n // Ensure directory exists\n const sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);\n if (!existsSync(sessionDir)) {\n mkdirSync(sessionDir, { recursive: true });\n }\n\n // Generate session document\n const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);\n\n // Write session file\n writeFileSync(join(sessionDir, filename), sessionDoc);\n\n // Also store structured summary via daemon IPC for the observations system\n await storeStructuredSummary(data.conversation_id, sessionInfo);\n\n // Exit successfully\n process.exit(0);\n } catch (error) {\n // Silent failure - don't disrupt workflow\n console.error(`[UOCS] SessionEnd hook error: ${error}`);\n process.exit(0);\n }\n}\n\nasync function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {\n // Try to read raw outputs for this session\n const rawOutputsDir = join(HISTORY_DIR, 'raw-outputs', yearMonth);\n\n let filesChanged: string[] = [];\n let commandsExecuted: string[] = [];\n let toolsUsed: Set<string> = new Set();\n\n try {\n if (existsSync(rawOutputsDir)) {\n const files = readdirSync(rawOutputsDir).filter(f => f.endsWith('.jsonl'));\n\n for (const file of files) {\n const filePath = join(rawOutputsDir, file);\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n').filter(l => l.trim());\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.session === conversationId) {\n toolsUsed.add(entry.tool);\n\n // Extract file changes\n if (entry.tool === 'Edit' || entry.tool === 'Write') {\n if (entry.input?.file_path) {\n filesChanged.push(entry.input.file_path);\n }\n }\n\n // Extract bash commands\n if (entry.tool === 'Bash' && entry.input?.command) {\n commandsExecuted.push(entry.input.command);\n }\n }\n } catch (e) {\n // Skip invalid JSON lines\n }\n }\n }\n }\n } catch (error) {\n // Silent failure\n }\n\n return {\n focus: 'general-work',\n filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10\n commandsExecuted: commandsExecuted.slice(0, 10), // Max 10\n toolsUsed: Array.from(toolsUsed),\n duration: 0 // Unknown\n };\n}\n\nfunction formatSessionDocument(timestamp: string, data: SessionData, info: any): string {\n const date = timestamp.substring(0, 10); // YYYY-MM-DD\n const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS\n const da = process.env.DA || 'PAI';\n\n return `---\ncapture_type: SESSION\ntimestamp: ${new Date().toISOString()}\nsession_id: ${data.conversation_id}\nduration_minutes: ${info.duration}\nexecutor: ${da}\n---\n\n# Session: ${info.focus}\n\n**Date:** ${date}\n**Time:** ${time}\n**Session ID:** ${data.conversation_id}\n\n---\n\n## Session Overview\n\n**Focus:** General development work\n**Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}\n\n---\n\n## Tools Used\n\n${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\\n') : '- None recorded'}\n\n---\n\n## Files Modified\n\n${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \\`${f}\\``).join('\\n') : '- None recorded'}\n\n**Total Files Changed:** ${info.filesChanged.length}\n\n---\n\n## Commands Executed\n\n${info.commandsExecuted.length > 0 ? '```bash\\n' + info.commandsExecuted.join('\\n') + '\\n```' : 'None recorded'}\n\n---\n\n## Notes\n\nThis session summary was automatically generated by the UOCS SessionEnd hook.\n\nFor detailed tool outputs, see: \\`\\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\\`\n\n---\n\n**Session Outcome:** Completed\n**Generated:** ${new Date().toISOString()}\n`;\n}\n\nasync function storeStructuredSummary(\n sessionId: string,\n info: { focus: string; filesChanged: string[]; commandsExecuted: string[]; toolsUsed: string[]; duration: number }\n): Promise<void> {\n try {\n const cwd = process.cwd();\n const net = await import('net');\n\n await new Promise<void>((resolve, _reject) => {\n const client = net.createConnection('/tmp/pai.sock', () => {\n const msg = JSON.stringify({\n id: 1,\n method: 'session_summary_store',\n params: {\n session_id: sessionId,\n cwd,\n request: null, // We don't have the original request\n investigated: null,\n learned: null,\n completed: info.filesChanged.length > 0\n ? `Modified ${info.filesChanged.length} file(s): ${info.filesChanged.slice(0, 5).join(', ')}`\n : null,\n next_steps: null,\n observation_count: 0, // Will be filled by daemon from actual count\n }\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', () => { client.end(); resolve(); });\n client.on('error', () => resolve()); // Silent failure\n setTimeout(() => { client.destroy(); resolve(); }, 3000);\n });\n } catch {\n // Silent failure \u2014 don't disrupt session end\n }\n}\n\nmain();\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * SessionEnd Hook - Captures session summary for UOCS\n *\n * Generates a session summary document when a Claude Code session ends,\n * documenting what was accomplished during the session.\n */\n\nimport { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';\n\ninterface SessionData {\n conversation_id: string;\n timestamp: string;\n [key: string]: any;\n}\n\nasync function main() {\n try {\n // Read input from stdin FIRST \u2014 this must complete before CC's abort signal fires.\n // Then fork the heavy work into a detached child so CC can't kill it.\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n if (!input || input.trim() === '') {\n process.exit(0);\n }\n\n // Fork: re-exec ourselves with --background flag and pipe the stdin data via env.\n // This detaches the heavy work (JSONL scan, IPC) from CC's abort signal.\n if (!process.env.__PAI_HOOK_BG) {\n const { spawn } = await import('child_process');\n const child = spawn(process.execPath, [process.argv[1], '--background'], {\n detached: true,\n stdio: 'ignore',\n env: { ...process.env, __PAI_HOOK_BG: '1', __PAI_HOOK_INPUT: input },\n });\n child.unref();\n process.exit(0); // Return immediately \u2014 CC sees success, abort signal is harmless\n }\n\n // Background mode: we're detached, safe from abort signals\n const data: SessionData = JSON.parse(process.env.__PAI_HOOK_INPUT || input);\n\n // Generate timestamp for filename\n const now = new Date();\n const timestamp = now.toISOString()\n .replace(/:/g, '')\n .replace(/\\..+/, '')\n .replace('T', '-'); // YYYY-MM-DD-HHMMSS\n\n const yearMonth = timestamp.substring(0, 7); // YYYY-MM\n\n // Try to extract session info from raw outputs\n const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);\n\n // Generate filename\n const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;\n\n // Ensure directory exists\n const sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);\n if (!existsSync(sessionDir)) {\n mkdirSync(sessionDir, { recursive: true });\n }\n\n // Generate session document\n const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);\n\n // Write session file\n writeFileSync(join(sessionDir, filename), sessionDoc);\n\n // Also store structured summary via daemon IPC for the observations system\n await storeStructuredSummary(data.conversation_id, sessionInfo);\n\n // Exit successfully\n process.exit(0);\n } catch (error) {\n // Silent failure - don't disrupt workflow\n console.error(`[UOCS] SessionEnd hook error: ${error}`);\n process.exit(0);\n }\n}\n\nasync function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {\n // Try to read raw outputs for this session\n const rawOutputsDir = join(HISTORY_DIR, 'raw-outputs', yearMonth);\n\n let filesChanged: string[] = [];\n let commandsExecuted: string[] = [];\n let toolsUsed: Set<string> = new Set();\n\n try {\n if (existsSync(rawOutputsDir)) {\n // Only scan today's file \u2014 not the entire month (which can be 400MB+).\n // JSONL filenames are prefixed with YYYY-MM-DD.\n const todayPrefix = new Date().toISOString().substring(0, 10);\n const files = readdirSync(rawOutputsDir).filter(\n f => f.endsWith('.jsonl') && f.startsWith(todayPrefix)\n );\n\n for (const file of files) {\n const filePath = join(rawOutputsDir, file);\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n').filter(l => l.trim());\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.session === conversationId) {\n toolsUsed.add(entry.tool);\n\n // Extract file changes\n if (entry.tool === 'Edit' || entry.tool === 'Write') {\n if (entry.input?.file_path) {\n filesChanged.push(entry.input.file_path);\n }\n }\n\n // Extract bash commands\n if (entry.tool === 'Bash' && entry.input?.command) {\n commandsExecuted.push(entry.input.command);\n }\n }\n } catch (e) {\n // Skip invalid JSON lines\n }\n }\n }\n }\n } catch (error) {\n // Silent failure\n }\n\n return {\n focus: 'general-work',\n filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10\n commandsExecuted: commandsExecuted.slice(0, 10), // Max 10\n toolsUsed: Array.from(toolsUsed),\n duration: 0 // Unknown\n };\n}\n\nfunction formatSessionDocument(timestamp: string, data: SessionData, info: any): string {\n const date = timestamp.substring(0, 10); // YYYY-MM-DD\n const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS\n const da = process.env.DA || 'PAI';\n\n return `---\ncapture_type: SESSION\ntimestamp: ${new Date().toISOString()}\nsession_id: ${data.conversation_id}\nduration_minutes: ${info.duration}\nexecutor: ${da}\n---\n\n# Session: ${info.focus}\n\n**Date:** ${date}\n**Time:** ${time}\n**Session ID:** ${data.conversation_id}\n\n---\n\n## Session Overview\n\n**Focus:** General development work\n**Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}\n\n---\n\n## Tools Used\n\n${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\\n') : '- None recorded'}\n\n---\n\n## Files Modified\n\n${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \\`${f}\\``).join('\\n') : '- None recorded'}\n\n**Total Files Changed:** ${info.filesChanged.length}\n\n---\n\n## Commands Executed\n\n${info.commandsExecuted.length > 0 ? '```bash\\n' + info.commandsExecuted.join('\\n') + '\\n```' : 'None recorded'}\n\n---\n\n## Notes\n\nThis session summary was automatically generated by the UOCS SessionEnd hook.\n\nFor detailed tool outputs, see: \\`\\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\\`\n\n---\n\n**Session Outcome:** Completed\n**Generated:** ${new Date().toISOString()}\n`;\n}\n\nasync function storeStructuredSummary(\n sessionId: string,\n info: { focus: string; filesChanged: string[]; commandsExecuted: string[]; toolsUsed: string[]; duration: number }\n): Promise<void> {\n try {\n const cwd = process.cwd();\n const net = await import('net');\n\n await new Promise<void>((resolve, _reject) => {\n const client = net.createConnection('/tmp/pai.sock', () => {\n const msg = JSON.stringify({\n id: 1,\n method: 'session_summary_store',\n params: {\n session_id: sessionId,\n cwd,\n request: null, // We don't have the original request\n investigated: null,\n learned: null,\n completed: info.filesChanged.length > 0\n ? `Modified ${info.filesChanged.length} file(s): ${info.filesChanged.slice(0, 5).join(', ')}`\n : null,\n next_steps: null,\n observation_count: 0, // Will be filled by daemon from actual count\n }\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', () => { client.end(); resolve(); });\n client.on('error', () => resolve()); // Silent failure\n setTimeout(() => { client.destroy(); resolve(); }, 3000);\n });\n } catch {\n // Silent failure \u2014 don't disrupt session end\n }\n}\n\nmain();\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["existsSync", "readFileSync", "join", "join", "existsSync", "readFileSync", "resolve"]
7
7
  }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@tekmidian/pai",
3
+ "displayName": "PAI Knowledge OS",
4
+ "description": "Personal AI Infrastructure — persistent memory and session continuity for AI coding assistants",
5
+ "version": "0.7.0",
6
+ "author": "Matthias Nott",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/mnott/PAI",
9
+ "mcpServers": {
10
+ "pai": {
11
+ "command": "node",
12
+ "args": ["dist/daemon-mcp/index.mjs"],
13
+ "description": "PAI Knowledge OS — federated memory search, project registry, session management. Provides 9 tools: memory_search, memory_get, project_info, project_list, session_list, registry_search, project_detect, project_health, project_todo."
14
+ }
15
+ },
16
+ "capabilities": {
17
+ "mcp": true,
18
+ "hooks": false,
19
+ "skills": false
20
+ },
21
+ "setup": {
22
+ "prerequisite": "Run 'pai setup' to configure storage backend, indexing, and daemon.",
23
+ "daemon": "Run 'pai daemon install' to start the background indexing service."
24
+ },
25
+ "notes": "Gemini CLI supports MCP servers. PAI's 9 MCP tools work fully. Hooks and skills are Claude Code-specific features that require Claude Code."
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -14,6 +14,11 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "templates",
17
+ "plugins",
18
+ "user-extensions",
19
+ ".claude-plugin",
20
+ "pai-plugin.json",
21
+ "gemini-extension.json",
17
22
  "statusline-command.sh",
18
23
  "tab-color-command.sh",
19
24
  "scripts/build-hooks.mjs",
@@ -22,7 +27,8 @@
22
27
  "README.md",
23
28
  "LICENSE",
24
29
  "ARCHITECTURE.md",
25
- "FEATURE.md"
30
+ "FEATURE.md",
31
+ "PLUGIN-ARCHITECTURE.md"
26
32
  ],
27
33
  "keywords": [
28
34
  "ai",
@@ -0,0 +1,212 @@
1
+ {
2
+ "$schema": "https://pai.dev/schemas/plugin-manifest-v1.json",
3
+ "name": "@tekmidian/pai",
4
+ "displayName": "PAI Knowledge OS",
5
+ "description": "Personal AI Infrastructure — persistent memory, session continuity, and knowledge graph",
6
+ "version": "0.7.0",
7
+ "author": "Matthias Nott",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/mnott/PAI",
10
+ "repository": "https://github.com/mnott/PAI",
11
+
12
+ "modules": {
13
+ "core": {
14
+ "description": "Core memory engine, session management, project registry",
15
+ "tier": "free",
16
+ "required": true,
17
+ "hooks": "plugins/core/hooks/hooks.json",
18
+ "skills": ["Sessions", "Route", "Name"],
19
+ "mcp": {
20
+ "server": "dist/daemon-mcp/index.mjs",
21
+ "daemon": "dist/daemon/index.mjs",
22
+ "tools": [
23
+ "memory_search",
24
+ "memory_get",
25
+ "project_info",
26
+ "project_list",
27
+ "session_list",
28
+ "registry_search",
29
+ "project_detect",
30
+ "project_health",
31
+ "project_todo"
32
+ ],
33
+ "resources": [
34
+ "pai://constitution",
35
+ "pai://skill-system",
36
+ "pai://hook-system",
37
+ "pai://mcp-dev-guide",
38
+ "pai://terminal-tabs"
39
+ ]
40
+ },
41
+ "templates": [
42
+ "templates/claude-md.template.md",
43
+ "templates/pai-skill.template.md",
44
+ "templates/pai-project.template.md"
45
+ ]
46
+ },
47
+
48
+ "productivity": {
49
+ "description": "Planning, journaling, reviewing, research, and sharing workflows",
50
+ "tier": "free",
51
+ "required": false,
52
+ "depends": ["core"],
53
+ "hooks": "plugins/productivity/hooks/hooks.json",
54
+ "skills": ["Plan", "Review", "Journal", "Research", "Share", "Createskill"],
55
+ "templates": [
56
+ "templates/agent-prefs.example.md"
57
+ ]
58
+ },
59
+
60
+ "ui": {
61
+ "description": "Terminal tab titles, statusline, tab coloring, and visual customization",
62
+ "tier": "free",
63
+ "required": false,
64
+ "depends": ["core"],
65
+ "hooks": "plugins/ui/hooks/hooks.json",
66
+ "scripts": [
67
+ "statusline-command.sh",
68
+ "tab-color-command.sh"
69
+ ],
70
+ "templates": [
71
+ "templates/ai-steering-rules.template.md"
72
+ ]
73
+ },
74
+
75
+ "context-preservation": {
76
+ "description": "Context compression and relay across compaction cycles",
77
+ "tier": "free",
78
+ "required": false,
79
+ "depends": ["core"],
80
+ "hooks": "plugins/context-preservation/hooks/hooks.json"
81
+ },
82
+
83
+ "semantic-search": {
84
+ "description": "Vector search with pgvector, cross-encoder reranking, hybrid mode, recency boost",
85
+ "tier": "pro",
86
+ "required": false,
87
+ "depends": ["core"],
88
+ "features": [
89
+ "semantic_search_mode",
90
+ "hybrid_search_mode",
91
+ "cross_encoder_reranking",
92
+ "recency_boost",
93
+ "pgvector_storage"
94
+ ],
95
+ "requirements": {
96
+ "docker": true,
97
+ "postgresql": ">=15",
98
+ "pgvector": ">=0.5.0"
99
+ }
100
+ },
101
+
102
+ "observability": {
103
+ "description": "Automatic observation capture, classification, session summaries, and search history",
104
+ "tier": "pro",
105
+ "required": false,
106
+ "depends": ["core"],
107
+ "hooks": "plugins/observability/hooks/hooks.json",
108
+ "skills": ["Observability", "SearchHistory"],
109
+ "mcp": {
110
+ "resources": [
111
+ "pai://history-system"
112
+ ]
113
+ }
114
+ },
115
+
116
+ "zettelkasten": {
117
+ "description": "Luhmann-inspired vault intelligence — explore, surprise, converse, themes, health, suggest",
118
+ "tier": "enterprise",
119
+ "required": false,
120
+ "depends": ["core", "semantic-search"],
121
+ "skills": ["VaultConnect", "VaultContext", "VaultEmerge", "VaultOrphans", "VaultTrace"],
122
+ "features": [
123
+ "vault_indexer",
124
+ "zettel_explore",
125
+ "zettel_surprise",
126
+ "zettel_converse",
127
+ "zettel_themes",
128
+ "zettel_health",
129
+ "zettel_suggest"
130
+ ],
131
+ "requirements": {
132
+ "obsidian": true
133
+ }
134
+ },
135
+
136
+ "creative": {
137
+ "description": "Art direction, story explanation, voice/prosody configuration",
138
+ "tier": "enterprise",
139
+ "required": false,
140
+ "depends": ["core"],
141
+ "skills": ["Art", "StoryExplanation"],
142
+ "mcp": {
143
+ "resources": [
144
+ "pai://aesthetic",
145
+ "pai://prosody-guide",
146
+ "pai://prosody-agent-template",
147
+ "pai://voice",
148
+ "pai://prompting"
149
+ ]
150
+ },
151
+ "templates": [
152
+ "templates/voices.example.json"
153
+ ]
154
+ }
155
+ },
156
+
157
+ "tiers": {
158
+ "free": {
159
+ "description": "Core memory engine with keyword search, essential hooks, and productivity skills",
160
+ "price": 0,
161
+ "modules": ["core", "productivity", "ui", "context-preservation"]
162
+ },
163
+ "pro": {
164
+ "description": "Semantic search, observability suite, and advanced memory features",
165
+ "price": {
166
+ "monthly": 9,
167
+ "yearly": 79
168
+ },
169
+ "modules": ["core", "productivity", "ui", "context-preservation", "semantic-search", "observability"]
170
+ },
171
+ "enterprise": {
172
+ "description": "Full PAI — zettelkasten intelligence, creative studio, and all features",
173
+ "price": {
174
+ "monthly": 29,
175
+ "yearly": 249
176
+ },
177
+ "modules": ["core", "productivity", "ui", "context-preservation", "semantic-search", "observability", "zettelkasten", "creative"]
178
+ }
179
+ },
180
+
181
+ "userExtensions": {
182
+ "hooks": "user-extensions/hooks/",
183
+ "skills": "user-extensions/skills/",
184
+ "prompts": "src/daemon-mcp/prompts/custom/"
185
+ },
186
+
187
+ "platforms": {
188
+ "claude-code": {
189
+ "manifest": ".claude-plugin/plugin.json",
190
+ "hooks": true,
191
+ "skills": true,
192
+ "mcp": true,
193
+ "resources": true
194
+ },
195
+ "cursor": {
196
+ "manifest": ".cursor/plugin.json",
197
+ "hooks": false,
198
+ "skills": false,
199
+ "mcp": true,
200
+ "resources": false,
201
+ "notes": "Cursor supports MCP but not Claude Code hooks/skills"
202
+ },
203
+ "gemini-cli": {
204
+ "manifest": "gemini-extension.json",
205
+ "hooks": false,
206
+ "skills": false,
207
+ "mcp": true,
208
+ "resources": false,
209
+ "notes": "Gemini CLI supports MCP servers via extensions"
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "module": "context-preservation",
3
+ "description": "Context compression and relay across compaction cycles",
4
+ "hooks": [
5
+ {
6
+ "event": "PreCompact",
7
+ "command": "${PAI_DIR}/Hooks/context-compression-hook.mjs",
8
+ "description": "Extract session state, save checkpoint, write temp file for relay to post-compact"
9
+ },
10
+ {
11
+ "event": "PreCompact",
12
+ "matcher": "",
13
+ "command": "${PAI_DIR}/Hooks/pai-pre-compact.sh",
14
+ "description": "Shell-level pre-compact processing"
15
+ },
16
+ {
17
+ "event": "SessionStart",
18
+ "matcher": "compact",
19
+ "command": "${PAI_DIR}/Hooks/post-compact-inject.mjs",
20
+ "description": "Read saved state from temp file and inject into post-compaction context"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "pai-context-preservation",
3
+ "displayName": "PAI Context Preservation",
4
+ "description": "Context compression and relay across compaction cycles",
5
+ "version": "0.7.0",
6
+ "tier": "free",
7
+ "required": false,
8
+ "depends": ["core"],
9
+ "hooks": "hooks/hooks.json"
10
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "module": "core",
3
+ "description": "Essential lifecycle hooks for session management, context loading, and security",
4
+ "hooks": [
5
+ {
6
+ "event": "SessionStart",
7
+ "command": "${PAI_DIR}/Hooks/load-core-context.mjs",
8
+ "description": "Load PAI skill system and core configuration into session context"
9
+ },
10
+ {
11
+ "event": "SessionStart",
12
+ "command": "${PAI_DIR}/Hooks/load-project-context.mjs",
13
+ "description": "Detect project from CWD, load notes directory, TODO.md, and session note"
14
+ },
15
+ {
16
+ "event": "SessionStart",
17
+ "command": "${PAI_DIR}/Hooks/initialize-session.mjs",
18
+ "description": "Create numbered session note and register in PAI registry"
19
+ },
20
+ {
21
+ "event": "PreToolUse",
22
+ "matcher": "Bash",
23
+ "command": "${PAI_DIR}/Hooks/security-validator.mjs",
24
+ "description": "Validate shell commands against security rules before execution"
25
+ },
26
+ {
27
+ "event": "Stop",
28
+ "command": "${PAI_DIR}/Hooks/stop-hook.mjs",
29
+ "description": "Write work items and summary to session note, send notification"
30
+ },
31
+ {
32
+ "event": "Stop",
33
+ "command": "${PAI_DIR}/Hooks/pai-session-stop.sh",
34
+ "description": "Shell-level session cleanup on stop"
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "pai-core",
3
+ "displayName": "PAI Core",
4
+ "description": "Core memory engine, session management, and project registry",
5
+ "version": "0.7.0",
6
+ "tier": "free",
7
+ "required": true,
8
+ "hooks": "hooks/hooks.json",
9
+ "skills": ["Sessions", "Route", "Name"]
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "pai-creative",
3
+ "displayName": "PAI Creative Studio",
4
+ "description": "Art direction, story explanation, voice/prosody configuration",
5
+ "version": "0.7.0",
6
+ "tier": "enterprise",
7
+ "required": false,
8
+ "depends": ["core"],
9
+ "skills": ["Art", "StoryExplanation"]
10
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "module": "observability",
3
+ "description": "Automatic observation capture, event logging, tool output recording, and session summaries",
4
+ "hooks": [
5
+ {
6
+ "event": "SessionStart",
7
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type SessionStart",
8
+ "description": "Log SessionStart event to session timeline"
9
+ },
10
+ {
11
+ "event": "SessionStart",
12
+ "command": "${PAI_DIR}/Hooks/inject-observations.mjs",
13
+ "description": "Inject recent observation context (compact index + timeline)"
14
+ },
15
+ {
16
+ "event": "UserPromptSubmit",
17
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type UserPromptSubmit",
18
+ "description": "Log UserPromptSubmit event to session timeline"
19
+ },
20
+ {
21
+ "event": "PreToolUse",
22
+ "matcher": "*",
23
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type PreToolUse",
24
+ "description": "Log PreToolUse event to session timeline"
25
+ },
26
+ {
27
+ "event": "PostToolUse",
28
+ "matcher": "*",
29
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type PostToolUse",
30
+ "description": "Log PostToolUse event to session timeline"
31
+ },
32
+ {
33
+ "event": "PostToolUse",
34
+ "matcher": "*",
35
+ "command": "${PAI_DIR}/Hooks/observe.mjs",
36
+ "description": "Classify tool calls into typed observations (decision/bugfix/feature/refactor/discovery/change)"
37
+ },
38
+ {
39
+ "event": "PostToolUse",
40
+ "matcher": "*",
41
+ "command": "${PAI_DIR}/Hooks/capture-tool-output.mjs",
42
+ "description": "Record tool inputs/outputs for observability dashboard"
43
+ },
44
+ {
45
+ "event": "Stop",
46
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type Stop",
47
+ "description": "Log Stop event to session timeline"
48
+ },
49
+ {
50
+ "event": "SubagentStop",
51
+ "command": "${PAI_DIR}/Hooks/subagent-stop-hook.mjs",
52
+ "description": "Capture sub-agent completion for observability"
53
+ },
54
+ {
55
+ "event": "SubagentStop",
56
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type SubagentStop",
57
+ "description": "Log SubagentStop event to session timeline"
58
+ },
59
+ {
60
+ "event": "SessionEnd",
61
+ "command": "${PAI_DIR}/Hooks/capture-session-summary.mjs",
62
+ "description": "Generate final session summary and write to session note"
63
+ },
64
+ {
65
+ "event": "SessionEnd",
66
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type SessionEnd",
67
+ "description": "Log SessionEnd event to session timeline"
68
+ },
69
+ {
70
+ "event": "PreCompact",
71
+ "command": "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type PreCompact",
72
+ "description": "Log PreCompact event to session timeline"
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "pai-observability",
3
+ "displayName": "PAI Observability Suite",
4
+ "description": "Automatic observation capture, classification, session summaries, and search history",
5
+ "version": "0.7.0",
6
+ "tier": "pro",
7
+ "required": false,
8
+ "depends": ["core"],
9
+ "hooks": "hooks/hooks.json",
10
+ "skills": ["Observability", "SearchHistory"]
11
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "module": "productivity",
3
+ "description": "Hooks for TODO sync, session cleanup, and productivity workflows",
4
+ "hooks": [
5
+ {
6
+ "event": "PostToolUse",
7
+ "matcher": "TodoWrite",
8
+ "command": "${PAI_DIR}/Hooks/sync-todo-to-md.mjs",
9
+ "description": "Sync Claude's internal TODO list to Notes/TODO.md on every TodoWrite"
10
+ },
11
+ {
12
+ "event": "UserPromptSubmit",
13
+ "command": "${PAI_DIR}/Hooks/cleanup-session-files.mjs",
14
+ "description": "Clean up stale temp files between user prompts"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "pai-productivity",
3
+ "displayName": "PAI Productivity",
4
+ "description": "Planning, journaling, reviewing, research, and sharing workflows",
5
+ "version": "0.7.0",
6
+ "tier": "free",
7
+ "required": false,
8
+ "depends": ["core"],
9
+ "hooks": "hooks/hooks.json",
10
+ "skills": ["Plan", "Review", "Journal", "Research", "Share", "Createskill"]
11
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "pai-semantic-search",
3
+ "displayName": "PAI Semantic Search",
4
+ "description": "Vector search with pgvector, cross-encoder reranking, hybrid mode, recency boost",
5
+ "version": "0.7.0",
6
+ "tier": "pro",
7
+ "required": false,
8
+ "depends": ["core"],
9
+ "features": [
10
+ "semantic_search_mode",
11
+ "hybrid_search_mode",
12
+ "cross_encoder_reranking",
13
+ "recency_boost",
14
+ "pgvector_storage"
15
+ ],
16
+ "requirements": {
17
+ "docker": true,
18
+ "postgresql": ">=15",
19
+ "pgvector": ">=0.5.0"
20
+ }
21
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "module": "ui",
3
+ "description": "Terminal tab titles, statusline updates, and visual feedback hooks",
4
+ "hooks": [
5
+ {
6
+ "event": "UserPromptSubmit",
7
+ "command": "${PAI_DIR}/Hooks/update-tab-titles.mjs",
8
+ "description": "Set terminal tab title from session context"
9
+ },
10
+ {
11
+ "event": "PostToolUse",
12
+ "matcher": "*",
13
+ "command": "${PAI_DIR}/Hooks/update-tab-on-action.mjs",
14
+ "description": "Update terminal tab title based on current tool activity"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "pai-ui",
3
+ "displayName": "PAI UI Customization",
4
+ "description": "Terminal tab titles, statusline, tab coloring, and visual customization",
5
+ "version": "0.7.0",
6
+ "tier": "free",
7
+ "required": false,
8
+ "depends": ["core"],
9
+ "hooks": "hooks/hooks.json",
10
+ "scripts": ["statusline-command.sh", "tab-color-command.sh"]
11
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "pai-zettelkasten",
3
+ "displayName": "PAI Zettelkasten Intelligence",
4
+ "description": "Luhmann-inspired vault intelligence with graph operations and Obsidian integration",
5
+ "version": "0.7.0",
6
+ "tier": "enterprise",
7
+ "required": false,
8
+ "depends": ["core", "semantic-search"],
9
+ "skills": ["VaultConnect", "VaultContext", "VaultEmerge", "VaultOrphans", "VaultTrace"],
10
+ "features": [
11
+ "vault_indexer",
12
+ "zettel_explore",
13
+ "zettel_surprise",
14
+ "zettel_converse",
15
+ "zettel_themes",
16
+ "zettel_health",
17
+ "zettel_suggest"
18
+ ]
19
+ }
@@ -19,7 +19,8 @@ interface SessionData {
19
19
 
20
20
  async function main() {
21
21
  try {
22
- // Read input from stdin
22
+ // Read input from stdin FIRST — this must complete before CC's abort signal fires.
23
+ // Then fork the heavy work into a detached child so CC can't kill it.
23
24
  const chunks: Buffer[] = [];
24
25
  for await (const chunk of process.stdin) {
25
26
  chunks.push(chunk);
@@ -29,7 +30,21 @@ async function main() {
29
30
  process.exit(0);
30
31
  }
31
32
 
32
- const data: SessionData = JSON.parse(input);
33
+ // Fork: re-exec ourselves with --background flag and pipe the stdin data via env.
34
+ // This detaches the heavy work (JSONL scan, IPC) from CC's abort signal.
35
+ if (!process.env.__PAI_HOOK_BG) {
36
+ const { spawn } = await import('child_process');
37
+ const child = spawn(process.execPath, [process.argv[1], '--background'], {
38
+ detached: true,
39
+ stdio: 'ignore',
40
+ env: { ...process.env, __PAI_HOOK_BG: '1', __PAI_HOOK_INPUT: input },
41
+ });
42
+ child.unref();
43
+ process.exit(0); // Return immediately — CC sees success, abort signal is harmless
44
+ }
45
+
46
+ // Background mode: we're detached, safe from abort signals
47
+ const data: SessionData = JSON.parse(process.env.__PAI_HOOK_INPUT || input);
33
48
 
34
49
  // Generate timestamp for filename
35
50
  const now = new Date();
@@ -80,7 +95,12 @@ async function analyzeSession(conversationId: string, yearMonth: string): Promis
80
95
 
81
96
  try {
82
97
  if (existsSync(rawOutputsDir)) {
83
- const files = readdirSync(rawOutputsDir).filter(f => f.endsWith('.jsonl'));
98
+ // Only scan today's file — not the entire month (which can be 400MB+).
99
+ // JSONL filenames are prefixed with YYYY-MM-DD.
100
+ const todayPrefix = new Date().toISOString().substring(0, 10);
101
+ const files = readdirSync(rawOutputsDir).filter(
102
+ f => f.endsWith('.jsonl') && f.startsWith(todayPrefix)
103
+ );
84
104
 
85
105
  for (const file of files) {
86
106
  const filePath = join(rawOutputsDir, file);
@@ -0,0 +1,87 @@
1
+ # PAI User Extensions
2
+
3
+ This directory is your personal extension point for PAI. Files here are gitignored
4
+ and will never be overwritten by PAI updates.
5
+
6
+ ## Adding Custom Skills
7
+
8
+ Create a skill directory with a SKILL.md file:
9
+
10
+ ```
11
+ user-extensions/skills/MySkill/SKILL.md
12
+ ```
13
+
14
+ SKILL.md format:
15
+
16
+ ```markdown
17
+ ---
18
+ name: MySkill
19
+ description: "What the skill does. USE WHEN user says 'trigger phrase'."
20
+ ---
21
+
22
+ ## My Skill Instructions
23
+
24
+ Your skill content here...
25
+ ```
26
+
27
+ After creating, run `bun run build` or `pai setup` to symlink it into
28
+ `~/.claude/skills/` where Claude Code discovers it.
29
+
30
+ ## Adding Custom Hooks
31
+
32
+ Create a TypeScript or shell hook:
33
+
34
+ ```
35
+ user-extensions/hooks/my-hook.ts
36
+ user-extensions/hooks/my-hook.sh
37
+ ```
38
+
39
+ TypeScript hooks are compiled during `bun run build` and deployed to
40
+ `~/.claude/Hooks/`. Shell hooks are symlinked directly.
41
+
42
+ Hook input format (stdin JSON):
43
+ ```json
44
+ {
45
+ "session_id": "...",
46
+ "transcript_path": "...",
47
+ "cwd": "...",
48
+ "hook_event_name": "SessionStart|PreToolUse|PostToolUse|Stop|..."
49
+ }
50
+ ```
51
+
52
+ After creating, register in `~/.claude/settings.json` under the appropriate
53
+ hook event, or run `pai setup` to auto-register.
54
+
55
+ ## Adding Custom MCP Prompts
56
+
57
+ Create a prompt file in the custom prompts directory:
58
+
59
+ ```
60
+ src/daemon-mcp/prompts/custom/my-prompt.ts
61
+ ```
62
+
63
+ Format:
64
+ ```typescript
65
+ export const myPrompt = {
66
+ description: "What the prompt does",
67
+ content: `## My Prompt
68
+
69
+ USE WHEN user says 'trigger phrase'...
70
+
71
+ ### Instructions
72
+ Your prompt content here...`,
73
+ };
74
+ ```
75
+
76
+ Run `bun run build` to generate the skill stub and symlink.
77
+
78
+ ## Directory Structure
79
+
80
+ ```
81
+ user-extensions/
82
+ ├── hooks/ # Custom hook scripts (.ts or .sh)
83
+ │ └── .gitkeep
84
+ ├── skills/ # Custom skill directories (each with SKILL.md)
85
+ │ └── .gitkeep
86
+ └── README.md # This file
87
+ ```
File without changes
File without changes