@tekmidian/pai 0.6.6 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.claude-plugin/plugin.json +50 -0
  2. package/PLUGIN-ARCHITECTURE.md +365 -0
  3. package/dist/cli/index.mjs +1 -1
  4. package/dist/daemon/index.mjs +1 -1
  5. package/dist/{daemon-D3hYb5_C.mjs → daemon-DJoesjez.mjs} +847 -4
  6. package/dist/daemon-DJoesjez.mjs.map +1 -0
  7. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  8. package/dist/hooks/load-project-context.mjs +4 -23
  9. package/dist/hooks/load-project-context.mjs.map +2 -2
  10. package/dist/hooks/stop-hook.mjs +206 -125
  11. package/dist/hooks/stop-hook.mjs.map +3 -3
  12. package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
  13. package/gemini-extension.json +26 -0
  14. package/package.json +8 -2
  15. package/pai-plugin.json +212 -0
  16. package/plugins/context-preservation/hooks/hooks.json +23 -0
  17. package/plugins/context-preservation/plugin.json +10 -0
  18. package/plugins/core/hooks/hooks.json +37 -0
  19. package/plugins/core/plugin.json +10 -0
  20. package/plugins/creative/plugin.json +10 -0
  21. package/plugins/observability/hooks/hooks.json +75 -0
  22. package/plugins/observability/plugin.json +11 -0
  23. package/plugins/productivity/hooks/hooks.json +17 -0
  24. package/plugins/productivity/plugin.json +11 -0
  25. package/plugins/semantic-search/plugin.json +21 -0
  26. package/plugins/ui/hooks/hooks.json +17 -0
  27. package/plugins/ui/plugin.json +11 -0
  28. package/plugins/zettelkasten/plugin.json +19 -0
  29. package/src/hooks/ts/lib/project-utils/session-notes.ts +24 -5
  30. package/src/hooks/ts/session-start/load-project-context.ts +9 -25
  31. package/src/hooks/ts/stop/stop-hook.ts +259 -199
  32. package/user-extensions/README.md +87 -0
  33. package/user-extensions/hooks/.gitkeep +0 -0
  34. package/user-extensions/skills/.gitkeep +0 -0
  35. package/dist/daemon-D3hYb5_C.mjs.map +0 -1
@@ -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
+ }
@@ -215,6 +215,21 @@ export function sanitizeForFilename(str: string): string {
215
215
  .substring(0, 50);
216
216
  }
217
217
 
218
+ /**
219
+ * Return true if the candidate string should be rejected as a meaningful name.
220
+ * Rejects file paths, shebangs, timestamps, and "[object Object]" artifacts.
221
+ */
222
+ function isMeaninglessCandidate(text: string): boolean {
223
+ const t = text.trim();
224
+ if (!t) return true;
225
+ if (t.startsWith('/')) return true; // file path
226
+ if (t.startsWith('#!')) return true; // shebang
227
+ if (t.includes('[object Object]')) return true; // serialization artifact
228
+ if (/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp
229
+ if (/^\d{1,2}:\d{2}(:\d{2})?(\s*(AM|PM))?$/i.test(t)) return true; // time-only
230
+ return false;
231
+ }
232
+
218
233
  /**
219
234
  * Extract a meaningful name from session note content and summary.
220
235
  * Looks at Work Done section headers, bold text, and summary.
@@ -228,7 +243,7 @@ export function extractMeaningfulName(noteContent: string, summary: string): str
228
243
  const subheadings = workDoneSection.match(/### ([^\n]+)/g);
229
244
  if (subheadings && subheadings.length > 0) {
230
245
  const firstHeading = subheadings[0].replace('### ', '').trim();
231
- if (firstHeading.length > 5 && firstHeading.length < 60) {
246
+ if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {
232
247
  return sanitizeForFilename(firstHeading);
233
248
  }
234
249
  }
@@ -236,23 +251,27 @@ export function extractMeaningfulName(noteContent: string, summary: string): str
236
251
  const boldMatches = workDoneSection.match(/\*\*([^*]+)\*\*/g);
237
252
  if (boldMatches && boldMatches.length > 0) {
238
253
  const firstBold = boldMatches[0].replace(/\*\*/g, '').trim();
239
- if (firstBold.length > 3 && firstBold.length < 50) {
254
+ if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {
240
255
  return sanitizeForFilename(firstBold);
241
256
  }
242
257
  }
243
258
 
244
259
  const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
245
- if (numberedItems) return sanitizeForFilename(numberedItems[1]);
260
+ if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {
261
+ return sanitizeForFilename(numberedItems[1]);
262
+ }
246
263
  }
247
264
 
248
- if (summary && summary.length > 5 && summary !== 'Session completed.') {
265
+ if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {
249
266
  const cleanSummary = summary
250
267
  .replace(/[^\w\s-]/g, ' ')
251
268
  .trim()
252
269
  .split(/\s+/)
253
270
  .slice(0, 5)
254
271
  .join(' ');
255
- if (cleanSummary.length > 3) return sanitizeForFilename(cleanSummary);
272
+ if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {
273
+ return sanitizeForFilename(cleanSummary);
274
+ }
256
275
  }
257
276
 
258
277
  return '';
@@ -239,36 +239,20 @@ async function main() {
239
239
  if (notesDir) { // notesDir is always set now (local or central)
240
240
  const currentNotePath = getCurrentNotePath(notesDir);
241
241
 
242
- // Determine if we need a new note
243
- let needsNewNote = false;
242
+ // Only create a new note if there is truly no note at all.
243
+ // A completed note is still used — it will be updated or continued.
244
+ // This prevents duplicate notes at month boundaries and on every compaction.
244
245
  if (!currentNotePath) {
245
- needsNewNote = true;
246
+ // Defensive: ensure projectName is a usable string
247
+ const safeProjectName = (typeof projectName === 'string' && projectName.trim().length > 0)
248
+ ? projectName.trim()
249
+ : 'Untitled Session';
246
250
  console.error('\nNo previous session notes found - creating new one');
247
- } else {
248
- // Check if the existing note is completed
249
- try {
250
- const content = readFileSync(currentNotePath, 'utf-8');
251
- if (content.includes('**Status:** Completed') || content.includes('**Completed:**')) {
252
- needsNewNote = true;
253
- console.error(`\nPrevious note completed - creating new one`);
254
- const summaryMatch = content.match(/## Next Steps\n\n([^\n]+)/);
255
- if (summaryMatch) {
256
- console.error(` Previous: ${summaryMatch[1].substring(0, 60)}...`);
257
- }
258
- } else {
259
- console.error(`\nContinuing session note: ${basename(currentNotePath)}`);
260
- }
261
- } catch {
262
- needsNewNote = true;
263
- }
264
- }
265
-
266
- // Create new note if needed
267
- if (needsNewNote) {
268
- activeNotePath = createSessionNote(notesDir, projectName);
251
+ activeNotePath = createSessionNote(notesDir, String(safeProjectName));
269
252
  console.error(`Created: ${basename(activeNotePath)}`);
270
253
  } else {
271
254
  activeNotePath = currentNotePath!;
255
+ console.error(`\nUsing existing session note: ${basename(activeNotePath)}`);
272
256
  // Show preview of current note
273
257
  try {
274
258
  const content = readFileSync(activeNotePath, 'utf-8');