cognova 0.1.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.
Files changed (205) hide show
  1. package/.env.example +58 -0
  2. package/Claude/CLAUDE.md +92 -0
  3. package/Claude/hooks/lib/__init__.py +1 -0
  4. package/Claude/hooks/lib/hook_client.py +207 -0
  5. package/Claude/hooks/log-event.py +97 -0
  6. package/Claude/hooks/pre-compact.py +46 -0
  7. package/Claude/hooks/session-end.py +26 -0
  8. package/Claude/hooks/session-start.py +35 -0
  9. package/Claude/hooks/stop-extract.py +40 -0
  10. package/Claude/rules/frontmatter.md +54 -0
  11. package/Claude/rules/markdown.md +43 -0
  12. package/Claude/rules/note-organization.md +33 -0
  13. package/Claude/settings.json +54 -0
  14. package/Claude/skills/README.md +136 -0
  15. package/Claude/skills/_lib/__init__.py +1 -0
  16. package/Claude/skills/_lib/api.py +164 -0
  17. package/Claude/skills/_lib/output.py +95 -0
  18. package/Claude/skills/environment/SKILL.md +73 -0
  19. package/Claude/skills/environment/environment.py +239 -0
  20. package/Claude/skills/memory/SKILL.md +153 -0
  21. package/Claude/skills/memory/memory.py +270 -0
  22. package/Claude/skills/project/SKILL.md +105 -0
  23. package/Claude/skills/project/project.py +203 -0
  24. package/Claude/skills/skill-creator/SKILL.md +261 -0
  25. package/Claude/skills/task/SKILL.md +135 -0
  26. package/Claude/skills/task/task.py +310 -0
  27. package/LICENSE +21 -0
  28. package/README.md +176 -0
  29. package/app/app.config.ts +8 -0
  30. package/app/app.vue +39 -0
  31. package/app/assets/css/main.css +10 -0
  32. package/app/components/AppLogo.vue +40 -0
  33. package/app/components/AssistantPanel.client.vue +518 -0
  34. package/app/components/ConfirmModal.vue +84 -0
  35. package/app/components/TemplateMenu.vue +49 -0
  36. package/app/components/agents/AgentActivityChart.client.vue +105 -0
  37. package/app/components/agents/AgentActivityChart.server.vue +25 -0
  38. package/app/components/agents/AgentForm.vue +304 -0
  39. package/app/components/agents/AgentRunModal.vue +154 -0
  40. package/app/components/agents/AgentStatsCards.vue +98 -0
  41. package/app/components/chat/ChatInput.vue +85 -0
  42. package/app/components/chat/ConversationList.vue +78 -0
  43. package/app/components/chat/MessageBubble.vue +81 -0
  44. package/app/components/chat/StreamingMessage.vue +36 -0
  45. package/app/components/chat/ToolCallBlock.vue +77 -0
  46. package/app/components/editor/CodeEditor.client.vue +212 -0
  47. package/app/components/editor/CodeEditorFallback.vue +12 -0
  48. package/app/components/editor/DocumentEditor.vue +326 -0
  49. package/app/components/editor/DocumentMetadata.vue +140 -0
  50. package/app/components/editor/MarkdownEditor.vue +146 -0
  51. package/app/components/files/FileTree.vue +436 -0
  52. package/app/components/hooks/HookActivityChart.client.vue +117 -0
  53. package/app/components/hooks/HookActivityChart.server.vue +25 -0
  54. package/app/components/hooks/HookStatsCards.vue +63 -0
  55. package/app/components/hooks/RecentEventsTable.vue +123 -0
  56. package/app/components/hooks/ToolBreakdownTable.vue +72 -0
  57. package/app/components/search/DashboardSearch.vue +122 -0
  58. package/app/components/tasks/ProjectSelect.vue +35 -0
  59. package/app/components/tasks/TaskCard.vue +182 -0
  60. package/app/components/tasks/TaskDetail.vue +160 -0
  61. package/app/components/tasks/TaskForm.vue +280 -0
  62. package/app/components/tasks/TaskList.vue +69 -0
  63. package/app/components/view/ViewToc.vue +85 -0
  64. package/app/composables/useAgents.ts +153 -0
  65. package/app/composables/useAuth.ts +73 -0
  66. package/app/composables/useChat.ts +298 -0
  67. package/app/composables/useDocument.ts +141 -0
  68. package/app/composables/useEditor.ts +100 -0
  69. package/app/composables/useFileTree.ts +220 -0
  70. package/app/composables/useHookEvents.ts +68 -0
  71. package/app/composables/useMemories.ts +83 -0
  72. package/app/composables/useNotificationBus.ts +154 -0
  73. package/app/composables/usePreferences.ts +131 -0
  74. package/app/composables/useProjects.ts +97 -0
  75. package/app/composables/useSearch.ts +52 -0
  76. package/app/composables/useTasks.ts +201 -0
  77. package/app/composables/useTerminal.ts +135 -0
  78. package/app/layouts/auth.vue +20 -0
  79. package/app/layouts/dashboard.vue +186 -0
  80. package/app/layouts/view.vue +60 -0
  81. package/app/middleware/auth.ts +9 -0
  82. package/app/pages/agents/[id].vue +602 -0
  83. package/app/pages/agents/index.vue +412 -0
  84. package/app/pages/chat.vue +146 -0
  85. package/app/pages/dashboard.vue +80 -0
  86. package/app/pages/docs.vue +131 -0
  87. package/app/pages/hooks.vue +163 -0
  88. package/app/pages/index.vue +249 -0
  89. package/app/pages/login.vue +60 -0
  90. package/app/pages/memories.vue +282 -0
  91. package/app/pages/settings.vue +625 -0
  92. package/app/pages/tasks.vue +312 -0
  93. package/app/pages/view/[uuid].vue +376 -0
  94. package/dist/cli/index.js +2711 -0
  95. package/drizzle.config.ts +10 -0
  96. package/nuxt.config.ts +98 -0
  97. package/package.json +107 -0
  98. package/server/api/agents/[id]/cancel.post.ts +27 -0
  99. package/server/api/agents/[id]/run.post.ts +34 -0
  100. package/server/api/agents/[id]/runs.get.ts +45 -0
  101. package/server/api/agents/[id]/stats.get.ts +94 -0
  102. package/server/api/agents/[id].delete.ts +29 -0
  103. package/server/api/agents/[id].get.ts +25 -0
  104. package/server/api/agents/[id].patch.ts +55 -0
  105. package/server/api/agents/index.get.ts +15 -0
  106. package/server/api/agents/index.post.ts +48 -0
  107. package/server/api/agents/stats.get.ts +86 -0
  108. package/server/api/auth/[...all].ts +5 -0
  109. package/server/api/conversations/[id].delete.ts +16 -0
  110. package/server/api/conversations/[id].get.ts +34 -0
  111. package/server/api/conversations/index.get.ts +17 -0
  112. package/server/api/documents/[id]/index.delete.ts +47 -0
  113. package/server/api/documents/[id]/index.put.ts +102 -0
  114. package/server/api/documents/[id]/public.get.ts +60 -0
  115. package/server/api/documents/[id]/restore.post.ts +65 -0
  116. package/server/api/documents/by-path.post.ts +168 -0
  117. package/server/api/documents/index.get.ts +48 -0
  118. package/server/api/fs/delete.post.ts +41 -0
  119. package/server/api/fs/list.get.ts +99 -0
  120. package/server/api/fs/mkdir.post.ts +44 -0
  121. package/server/api/fs/move.post.ts +68 -0
  122. package/server/api/fs/read.post.ts +48 -0
  123. package/server/api/fs/rename.post.ts +55 -0
  124. package/server/api/fs/write.post.ts +51 -0
  125. package/server/api/health.get.ts +40 -0
  126. package/server/api/home.get.ts +26 -0
  127. package/server/api/hooks/events/index.get.ts +56 -0
  128. package/server/api/hooks/events/index.post.ts +36 -0
  129. package/server/api/hooks/stats.get.ts +99 -0
  130. package/server/api/memory/[id].delete.ts +26 -0
  131. package/server/api/memory/context.get.ts +83 -0
  132. package/server/api/memory/extract.post.ts +42 -0
  133. package/server/api/memory/search.get.ts +70 -0
  134. package/server/api/memory/store.post.ts +31 -0
  135. package/server/api/projects/[id]/index.delete.ts +40 -0
  136. package/server/api/projects/[id]/index.get.ts +25 -0
  137. package/server/api/projects/[id]/index.put.ts +50 -0
  138. package/server/api/projects/index.get.ts +20 -0
  139. package/server/api/projects/index.post.ts +34 -0
  140. package/server/api/secrets/[key].delete.ts +31 -0
  141. package/server/api/secrets/[key].get.ts +30 -0
  142. package/server/api/secrets/[key].put.ts +52 -0
  143. package/server/api/secrets/index.get.ts +20 -0
  144. package/server/api/secrets/index.post.ts +58 -0
  145. package/server/api/tasks/[id]/index.delete.ts +46 -0
  146. package/server/api/tasks/[id]/index.get.ts +24 -0
  147. package/server/api/tasks/[id]/index.put.ts +70 -0
  148. package/server/api/tasks/[id]/restore.post.ts +49 -0
  149. package/server/api/tasks/index.get.ts +53 -0
  150. package/server/api/tasks/index.post.ts +47 -0
  151. package/server/api/tasks/tags.get.ts +21 -0
  152. package/server/api/user/email.patch.ts +56 -0
  153. package/server/db/index.ts +76 -0
  154. package/server/db/migrate.ts +41 -0
  155. package/server/db/schema.ts +345 -0
  156. package/server/db/seed.ts +46 -0
  157. package/server/db/types.ts +28 -0
  158. package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
  159. package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
  160. package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
  161. package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
  162. package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
  163. package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
  164. package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
  165. package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
  166. package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
  167. package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
  168. package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
  169. package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
  170. package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
  171. package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
  172. package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
  173. package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
  174. package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
  175. package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
  176. package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
  177. package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
  178. package/server/drizzle/migrations/meta/_journal.json +76 -0
  179. package/server/middleware/auth.ts +79 -0
  180. package/server/plugins/00.env-validate.ts +38 -0
  181. package/server/plugins/01.api-token.ts +31 -0
  182. package/server/plugins/02.database.ts +54 -0
  183. package/server/plugins/03.file-watcher.ts +65 -0
  184. package/server/plugins/04.cron-agents.ts +26 -0
  185. package/server/routes/_ws/chat.ts +252 -0
  186. package/server/routes/notifications.ts +47 -0
  187. package/server/routes/terminal.ts +98 -0
  188. package/server/services/agent-executor.ts +218 -0
  189. package/server/services/cron-scheduler.ts +78 -0
  190. package/server/services/memory-extractor.ts +120 -0
  191. package/server/utils/agent-cleanup.ts +91 -0
  192. package/server/utils/agent-registry.ts +95 -0
  193. package/server/utils/auth.ts +33 -0
  194. package/server/utils/chat-session-manager.ts +59 -0
  195. package/server/utils/crypto.ts +40 -0
  196. package/server/utils/db-guard.ts +12 -0
  197. package/server/utils/db-state.ts +63 -0
  198. package/server/utils/document-sync.ts +207 -0
  199. package/server/utils/frontmatter.ts +84 -0
  200. package/server/utils/notification-bus.ts +60 -0
  201. package/server/utils/path-validator.ts +55 -0
  202. package/server/utils/pty-manager.ts +130 -0
  203. package/shared/types/index.ts +604 -0
  204. package/shared/utils/language-detection.ts +87 -0
  205. package/tsconfig.json +10 -0
package/.env.example ADDED
@@ -0,0 +1,58 @@
1
+ # Cognova Environment Variables
2
+ # Copy this file to .env and fill in your values
3
+
4
+ # =============================================================================
5
+ # REQUIRED
6
+ # =============================================================================
7
+
8
+ # Path to your vault directory (where markdown files are stored)
9
+ VAULT_PATH=~/vault
10
+
11
+ # Auth secret - generate with: openssl rand -base64 32
12
+ BETTER_AUTH_SECRET=your-secret-here-generate-with-openssl
13
+
14
+ # Base URL of your application (used for callbacks)
15
+ BETTER_AUTH_URL=http://localhost:3000
16
+
17
+ # =============================================================================
18
+ # OPTIONAL - Database
19
+ # =============================================================================
20
+
21
+ # Local dev (pnpm dev + pnpm db:up) - uses localhost
22
+ DATABASE_URL=postgres://postgres:postgres@localhost:5432/cognova
23
+
24
+ # For Neon PostgreSQL (production), replace with your connection string:
25
+ # DATABASE_URL=postgres://user:password@ep-xxx.us-east-2.aws.neon.tech/cognova?sslmode=require
26
+
27
+ # =============================================================================
28
+ # OPTIONAL - Initial Admin User
29
+ # =============================================================================
30
+
31
+ # On first startup with an empty database, a default user is created automatically.
32
+ # Customize the default user with these variables:
33
+ # ADMIN_EMAIL=admin@example.com
34
+ # ADMIN_PASSWORD=changeme
35
+ # ADMIN_NAME=Admin
36
+ #
37
+ # Default credentials if not set: admin@example.com / changeme123
38
+
39
+ # =============================================================================
40
+ # OPTIONAL - CLI/Skill API Access
41
+ # =============================================================================
42
+
43
+ # API token for CLI tools and skills to access the API without browser auth.
44
+ # A token is AUTO-GENERATED on server startup and written to .api-token file.
45
+ # Only set this if you want to use a fixed token instead:
46
+ # COGNOVA_API_TOKEN=your-token-here
47
+
48
+ # =============================================================================
49
+ # DOCKER ONLY - Claude Code Integration
50
+ # =============================================================================
51
+
52
+ # Claude Code CLI is pre-installed in the container.
53
+ # To authenticate, open the terminal in the app and run: claude auth
54
+ #
55
+ # Optional: If you already have Claude Code configured on your host,
56
+ # docker-compose.yml will mount your existing config:
57
+ # - ~/.claude:/home/node/.claude (Claude Code config)
58
+ # - ~/.anthropic:/home/node/.anthropic (API key)
@@ -0,0 +1,92 @@
1
+ # Cognova
2
+
3
+ You are an AI assistant running through **Cognova**, a self-hosted personal knowledge management and productivity system. You run directly on the user's machine via the Claude Agent SDK — you are not sandboxed.
4
+
5
+ ## What You Are
6
+
7
+ You are a Claude-powered agent embedded in a Cognova installation. The user has granted you full system access: file system, shell, local services, and the Cognova API. You can read and write files, execute commands, manage processes, and interact with all Cognova features.
8
+
9
+ You run as a persistent service managed by PM2. Your conversations are streamed to the user through the Cognova web dashboard.
10
+
11
+ ## What Cognova Is
12
+
13
+ Cognova is a self-hosted Nuxt 4 web application for personal knowledge management:
14
+
15
+ - **Vault** — A folder of markdown documents organized using the PARA method (Projects, Areas, Resources, Archive, Inbox)
16
+ - **Tasks & Projects** — Structured task tracking with project association, priorities, due dates, and tags
17
+ - **Memory** — Persistent memory extracted from conversations that survives across sessions
18
+ - **Dashboard** — Web UI for browsing documents, managing tasks, viewing memory, and chatting with you
19
+ - **Cron Agents** — Background agents that run on schedules for maintenance and analysis
20
+ - **API** — RESTful API powering all data operations
21
+
22
+ ## Skills
23
+
24
+ | Skill | Command | Purpose |
25
+ |-------|---------|---------|
26
+ | Task Management | `/task` | Create, list, update, complete tasks |
27
+ | Project Management | `/project` | Organize tasks into projects |
28
+ | Memory | `/memory` | Search past decisions, store insights, recall context |
29
+ | Environment | `/environment` | Check system status, troubleshoot issues |
30
+ | Skill Creator | `/skill-creator` | Create new Claude Code skills |
31
+
32
+ ## Environment
33
+
34
+ Key paths and services (actual values come from environment variables):
35
+
36
+ | Resource | Variable / Location |
37
+ |----------|---------------------|
38
+ | Install Directory | `$COGNOVA_PROJECT_DIR` |
39
+ | Vault | `$VAULT_PATH` |
40
+ | API | `$COGNOVA_API_URL` (default: `http://localhost:3000`) |
41
+ | Skills | `~/.claude/skills/` |
42
+ | Process Manager | PM2 — `pm2 status`, `pm2 logs cognova` |
43
+ | Database | PostgreSQL via Drizzle ORM (`$DATABASE_URL`) |
44
+
45
+ ## API Access
46
+
47
+ All Python skills use the shared client at `~/.claude/skills/_lib/api.py`. Authentication is automatic via `.api-token` file or `COGNOVA_API_TOKEN` env var.
48
+
49
+ ```bash
50
+ # Quick health check
51
+ curl -s $COGNOVA_API_URL/api/health
52
+
53
+ # Or use the environment skill
54
+ python3 ~/.claude/skills/environment/environment.py health
55
+ ```
56
+
57
+ ## Vault Documents
58
+
59
+ Documents are markdown files with YAML frontmatter:
60
+
61
+ ```yaml
62
+ ---
63
+ tags: []
64
+ shared: false
65
+ ---
66
+ ```
67
+
68
+ Organized in PARA folders: `inbox/`, `projects/`, `areas/`, `resources/`, `archive/`. Use lowercase-hyphenated filenames (`project-ideas.md`). Documentation standards are in `~/.claude/rules/`.
69
+
70
+ ## Behaviors
71
+
72
+ ### Task Management
73
+ - Offer to create tasks for action items mentioned in conversation
74
+ - Use `/task create` with appropriate priority and project association
75
+ - Always search for existing projects before creating new ones
76
+
77
+ ### Memory
78
+ - Store key decisions: `/memory store --type decision "chose X because Y"`
79
+ - Check history before major changes: `/memory about "topic"`
80
+ - Memory types: decision, fact, solution, pattern, preference, summary
81
+ - Memories are also auto-extracted from conversations via hooks
82
+
83
+ ### Troubleshooting
84
+ - Use `/environment status` or `/environment health` to diagnose issues
85
+ - Check logs: `pm2 logs cognova --lines 50`
86
+ - Restart: `pm2 restart cognova`
87
+
88
+ ### Self-Modification
89
+ - You MAY update `~/.claude/CLAUDE.md` to refine your own behavior
90
+ - You MAY create new skills in `~/.claude/skills/`
91
+ - You MAY update existing skills when you find improvements
92
+ - Always inform the user when modifying your own configuration
@@ -0,0 +1 @@
1
+ # Hook client library
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook client library for logging events to Cognova API.
4
+ Uses curl subprocess to avoid pip dependencies (same as skills/_lib/api.py).
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Optional, Dict, Any
13
+
14
+
15
+ def _get_api_base() -> str:
16
+ """Get the Cognova API URL from environment."""
17
+ return os.environ.get('COGNOVA_API_URL', 'http://localhost:3000')
18
+
19
+
20
+ def _get_api_token() -> str:
21
+ """Get API token from environment or .api-token file."""
22
+ token = os.environ.get('COGNOVA_API_TOKEN', '')
23
+ if token:
24
+ return token
25
+
26
+ possible_paths = [
27
+ # Bare-metal: project dir from environment (set by PM2/settings.json)
28
+ Path(os.environ.get('COGNOVA_PROJECT_DIR', '')) / '.api-token',
29
+ # Docker: app is at /home/node/app
30
+ Path('/home/node/app/.api-token'),
31
+ # Local dev: navigate from lib -> hooks -> Claude -> project root
32
+ Path(__file__).parent.parent.parent.parent / '.api-token',
33
+ # Current working directory
34
+ Path.cwd() / '.api-token',
35
+ ]
36
+
37
+ for token_file in possible_paths:
38
+ if token_file.exists():
39
+ try:
40
+ return token_file.read_text().strip()
41
+ except Exception:
42
+ pass
43
+
44
+ return ''
45
+
46
+
47
+ def log_event(
48
+ event_type: str,
49
+ tool_name: Optional[str] = None,
50
+ tool_matcher: Optional[str] = None,
51
+ event_data: Optional[Dict[str, Any]] = None,
52
+ exit_code: Optional[int] = None,
53
+ blocked: bool = False,
54
+ block_reason: Optional[str] = None,
55
+ duration_ms: Optional[int] = None,
56
+ hook_script: Optional[str] = None,
57
+ session_id: Optional[str] = None
58
+ ) -> bool:
59
+ """
60
+ Log a hook event to the Cognova API.
61
+
62
+ Returns True if successful, False otherwise.
63
+ Fails silently to not block Claude operations.
64
+ """
65
+ api_base = _get_api_base()
66
+ api_token = _get_api_token()
67
+
68
+ if not api_token:
69
+ print(f"[hook_client] No API token found. Checked: {[str(p) for p in [Path('/home/node/app/.api-token'), Path(__file__).parent.parent.parent.parent / '.api-token', Path.cwd() / '.api-token']]}", file=sys.stderr)
70
+ return False
71
+
72
+ payload = {
73
+ 'eventType': event_type,
74
+ 'projectDir': os.environ.get('CLAUDE_PROJECT_DIR'),
75
+ 'sessionId': session_id or os.environ.get('CLAUDE_SESSION_ID'),
76
+ 'toolName': tool_name,
77
+ 'toolMatcher': tool_matcher,
78
+ 'eventData': event_data,
79
+ 'exitCode': exit_code,
80
+ 'blocked': blocked,
81
+ 'blockReason': block_reason,
82
+ 'durationMs': duration_ms,
83
+ 'hookScript': hook_script
84
+ }
85
+
86
+ # Remove None values
87
+ payload = {k: v for k, v in payload.items() if v is not None}
88
+
89
+ cmd = [
90
+ "curl", "-sL", "-X", "POST",
91
+ "-H", "Content-Type: application/json",
92
+ "-H", f"X-API-Token: {api_token}",
93
+ "-d", json.dumps(payload),
94
+ "--connect-timeout", "2",
95
+ "--max-time", "5",
96
+ f"{api_base}/api/hooks/events"
97
+ ]
98
+
99
+ try:
100
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
101
+ # Debug output to stderr (visible in claude --debug)
102
+ if os.environ.get('DEBUG') or result.returncode != 0:
103
+ print(f"[hook_client] API: {api_base}, Token: {'set' if api_token else 'NOT SET'}", file=sys.stderr)
104
+ print(f"[hook_client] Response: {result.returncode} - {result.stdout[:200] if result.stdout else 'no output'}", file=sys.stderr)
105
+ if result.stderr:
106
+ print(f"[hook_client] Error: {result.stderr[:200]}", file=sys.stderr)
107
+ return result.returncode == 0
108
+ except Exception as e:
109
+ print(f"[hook_client] Exception: {e}", file=sys.stderr)
110
+ return False
111
+
112
+
113
+ def read_stdin_json() -> Optional[Dict[str, Any]]:
114
+ """Read and parse JSON from stdin (hook input)."""
115
+ try:
116
+ return json.load(sys.stdin)
117
+ except Exception:
118
+ return None
119
+
120
+
121
+ def extract_memories(
122
+ transcript_path: Optional[str] = None,
123
+ transcript: Optional[str] = None,
124
+ session_id: Optional[str] = None,
125
+ project_path: Optional[str] = None
126
+ ) -> bool:
127
+ """
128
+ Trigger memory extraction from a transcript.
129
+
130
+ Returns True if successful, False otherwise.
131
+ """
132
+ api_base = _get_api_base()
133
+ api_token = _get_api_token()
134
+
135
+ if not api_token:
136
+ print("[hook_client] No API token for memory extraction", file=sys.stderr)
137
+ return False
138
+
139
+ payload = {
140
+ 'sessionId': session_id or os.environ.get('CLAUDE_SESSION_ID'),
141
+ 'projectPath': project_path or os.environ.get('CLAUDE_PROJECT_DIR')
142
+ }
143
+
144
+ if transcript_path:
145
+ payload['transcriptPath'] = transcript_path
146
+ elif transcript:
147
+ payload['transcript'] = transcript
148
+ else:
149
+ print("[hook_client] No transcript provided for memory extraction", file=sys.stderr)
150
+ return False
151
+
152
+ cmd = [
153
+ "curl", "-sL", "-X", "POST",
154
+ "-H", "Content-Type: application/json",
155
+ "-H", f"X-API-Token: {api_token}",
156
+ "-d", json.dumps(payload),
157
+ "--connect-timeout", "5",
158
+ "--max-time", "30",
159
+ f"{api_base}/api/memory/extract"
160
+ ]
161
+
162
+ try:
163
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
164
+ if os.environ.get('DEBUG') or result.returncode != 0:
165
+ print(f"[hook_client] Memory extraction: {result.returncode}", file=sys.stderr)
166
+ if result.stdout:
167
+ print(f"[hook_client] Response: {result.stdout[:200]}", file=sys.stderr)
168
+ return result.returncode == 0
169
+ except Exception as e:
170
+ print(f"[hook_client] Memory extraction failed: {e}", file=sys.stderr)
171
+ return False
172
+
173
+
174
+ def get_memory_context(project_path: Optional[str] = None, limit: int = 5) -> Optional[str]:
175
+ """
176
+ Get formatted memory context for session start.
177
+
178
+ Returns formatted context string or None if failed.
179
+ """
180
+ api_base = _get_api_base()
181
+ api_token = _get_api_token()
182
+
183
+ if not api_token:
184
+ return None
185
+
186
+ project = project_path or os.environ.get('CLAUDE_PROJECT_DIR', '')
187
+ url = f"{api_base}/api/memory/context?project={project}&limit={limit}"
188
+
189
+ cmd = [
190
+ "curl", "-sL",
191
+ "-H", f"X-API-Token: {api_token}",
192
+ "--connect-timeout", "2",
193
+ "--max-time", "5",
194
+ url
195
+ ]
196
+
197
+ try:
198
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
199
+ if result.returncode == 0 and result.stdout:
200
+ data = json.loads(result.stdout)
201
+ if 'data' in data and 'formatted' in data['data']:
202
+ return data['data']['formatted']
203
+ except Exception as e:
204
+ if os.environ.get('DEBUG'):
205
+ print(f"[hook_client] Memory context failed: {e}", file=sys.stderr)
206
+
207
+ return None
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generic hook event logger.
4
+
5
+ This script wraps other hooks, logging their execution while passing through
6
+ to the original hook behavior. It can also be used standalone to log events.
7
+
8
+ Usage:
9
+ As wrapper: python3 log-event.py <event_type> <original_hook_command>
10
+ Standalone: python3 log-event.py <event_type>
11
+
12
+ The hook input JSON is passed via stdin (as per Claude Code hooks spec).
13
+ """
14
+
15
+ import json
16
+ import subprocess
17
+ import sys
18
+ import time
19
+ from pathlib import Path
20
+
21
+ # Add lib to path
22
+ sys.path.insert(0, str(Path(__file__).parent / 'lib'))
23
+
24
+ from hook_client import log_event
25
+
26
+
27
+ def main():
28
+ if len(sys.argv) < 2:
29
+ print("Usage: log-event.py <event_type> [original_hook_command...]", file=sys.stderr)
30
+ sys.exit(1)
31
+
32
+ event_type = sys.argv[1]
33
+ wrapped_command = sys.argv[2:] if len(sys.argv) > 2 else None
34
+
35
+ # Read hook input from stdin
36
+ stdin_data = sys.stdin.read()
37
+ try:
38
+ hook_input = json.loads(stdin_data) if stdin_data else {}
39
+ except json.JSONDecodeError:
40
+ hook_input = {}
41
+
42
+ # Extract tool info if present
43
+ tool_name = hook_input.get('tool_name')
44
+ tool_input = hook_input.get('tool_input', {})
45
+
46
+ start_time = time.time()
47
+ exit_code = 0
48
+ blocked = False
49
+ block_reason = None
50
+
51
+ if wrapped_command:
52
+ # Run the wrapped command, passing hook input via stdin
53
+ try:
54
+ result = subprocess.run(
55
+ wrapped_command,
56
+ input=stdin_data,
57
+ capture_output=True,
58
+ text=True
59
+ )
60
+ exit_code = result.returncode
61
+
62
+ # Check if the wrapped hook blocked the action
63
+ if exit_code == 2:
64
+ blocked = True
65
+ # Try to parse block reason from stdout
66
+ try:
67
+ output = json.loads(result.stdout)
68
+ block_reason = output.get('reason', result.stdout.strip())
69
+ except json.JSONDecodeError:
70
+ block_reason = result.stdout.strip() or result.stderr.strip()
71
+ except Exception as e:
72
+ exit_code = 1
73
+ block_reason = str(e)
74
+
75
+ duration_ms = int((time.time() - start_time) * 1000)
76
+
77
+ # Log the event
78
+ log_event(
79
+ event_type=event_type,
80
+ tool_name=tool_name,
81
+ event_data={
82
+ 'tool_input': tool_input,
83
+ 'wrapped_command': wrapped_command
84
+ } if tool_input or wrapped_command else None,
85
+ exit_code=exit_code,
86
+ blocked=blocked,
87
+ block_reason=block_reason,
88
+ duration_ms=duration_ms,
89
+ hook_script='log-event.py'
90
+ )
91
+
92
+ # Exit with the wrapped command's exit code to preserve behavior
93
+ sys.exit(exit_code)
94
+
95
+
96
+ if __name__ == '__main__':
97
+ main()
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreCompact
4
+ Extracts memories from the conversation before context compaction.
5
+
6
+ This is critical for preserving key decisions and facts before
7
+ the conversation is summarized.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ # Add lib to path
15
+ sys.path.insert(0, str(Path(__file__).parent / 'lib'))
16
+
17
+ from hook_client import log_event, read_stdin_json, extract_memories
18
+
19
+
20
+ def main():
21
+ hook_input = read_stdin_json() or {}
22
+
23
+ transcript_path = hook_input.get('transcript_path')
24
+ trigger = hook_input.get('trigger', 'unknown') # 'manual' or 'auto'
25
+
26
+ # Log the event
27
+ log_event(
28
+ event_type='PreCompact',
29
+ event_data={'trigger': trigger},
30
+ hook_script='pre-compact.py'
31
+ )
32
+
33
+ # Extract memories before compaction
34
+ if transcript_path and os.path.exists(transcript_path):
35
+ success = extract_memories(transcript_path=transcript_path)
36
+ if success:
37
+ print(f"[pre-compact] Extracted memories before {trigger} compaction", file=sys.stderr)
38
+ else:
39
+ print(f"[pre-compact] No transcript path available: {transcript_path}", file=sys.stderr)
40
+
41
+ # Always exit 0 to not block compaction
42
+ sys.exit(0)
43
+
44
+
45
+ if __name__ == '__main__':
46
+ main()
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: SessionEnd
4
+ Logs when a Claude session ends.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ # Add lib to path
11
+ sys.path.insert(0, str(Path(__file__).parent / 'lib'))
12
+
13
+ from hook_client import log_event, read_stdin_json
14
+
15
+ def main():
16
+ hook_input = read_stdin_json()
17
+
18
+ log_event(
19
+ event_type='SessionEnd',
20
+ event_data=hook_input,
21
+ hook_script='session-end.py'
22
+ )
23
+
24
+
25
+ if __name__ == '__main__':
26
+ main()
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: SessionStart
4
+ Logs when a Claude session begins and injects memory context.
5
+
6
+ Memory context from previous conversations is printed to stdout,
7
+ which Claude receives as additional context.
8
+ """
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ # Add lib to path
14
+ sys.path.insert(0, str(Path(__file__).parent / 'lib'))
15
+
16
+ from hook_client import log_event, read_stdin_json, get_memory_context
17
+
18
+
19
+ def main():
20
+ hook_input = read_stdin_json()
21
+
22
+ log_event(
23
+ event_type='SessionStart',
24
+ event_data=hook_input,
25
+ hook_script='session-start.py'
26
+ )
27
+
28
+ # Inject memory context (printed to stdout goes to Claude)
29
+ context = get_memory_context()
30
+ if context:
31
+ print(context)
32
+
33
+
34
+ if __name__ == '__main__':
35
+ main()
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: Stop (async)
4
+ Extracts memories from Claude's final response when it finishes.
5
+
6
+ Runs asynchronously so it doesn't block Claude.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ # Add lib to path
14
+ sys.path.insert(0, str(Path(__file__).parent / 'lib'))
15
+
16
+ from hook_client import extract_memories, read_stdin_json
17
+
18
+
19
+ def main():
20
+ hook_input = read_stdin_json() or {}
21
+
22
+ transcript_path = hook_input.get('transcript_path')
23
+ stop_hook_active = hook_input.get('stop_hook_active', False)
24
+
25
+ # Skip if we're already continuing from a stop hook
26
+ # to avoid infinite loops
27
+ if stop_hook_active:
28
+ sys.exit(0)
29
+
30
+ # Extract memories from the transcript
31
+ if transcript_path and os.path.exists(transcript_path):
32
+ success = extract_memories(transcript_path=transcript_path)
33
+ if success and os.environ.get('DEBUG'):
34
+ print(f"[stop-extract] Async memory extraction completed", file=sys.stderr)
35
+
36
+ sys.exit(0)
37
+
38
+
39
+ if __name__ == '__main__':
40
+ main()
@@ -0,0 +1,54 @@
1
+ # Frontmatter Standards
2
+
3
+ All markdown documents in the vault use YAML frontmatter for metadata. When documents are created or discovered without frontmatter, defaults are automatically added by the document sync system.
4
+
5
+ ## Default Frontmatter
6
+
7
+ ```yaml
8
+ ---
9
+ tags: []
10
+ shared: false
11
+ ---
12
+ ```
13
+
14
+ ## All Fields
15
+
16
+ | Field | Type | Default | Description |
17
+ |-------|------|---------|-------------|
18
+ | `tags` | string[] | `[]` | Array of tag strings for categorization |
19
+ | `shared` | boolean | `false` | Whether document is publicly accessible |
20
+ | `shareType` | string | - | When `shared: true`: `'private'` or `'public'` |
21
+ | `title` | string | - | Optional; auto-extracted from first H1 or filename |
22
+ | `project` | string | - | Project ID for document association |
23
+
24
+ ## Visibility Modes
25
+
26
+ Documents support three visibility levels controlled by `shared` and `shareType`:
27
+
28
+ 1. **Hidden** (default) - `shared: false`
29
+ - Requires authentication to view
30
+ - Never publicly accessible
31
+
32
+ 2. **Private** - `shared: true, shareType: 'private'`
33
+ - Accessible via direct link only
34
+ - Not indexed by search engines
35
+
36
+ 3. **Public** - `shared: true, shareType: 'public'`
37
+ - Fully publicly accessible
38
+ - Indexed by search engines
39
+
40
+ ## Tag Conventions
41
+
42
+ - Use lowercase: `productivity` not `Productivity`
43
+ - Use hyphens for multi-word: `project-management`
44
+ - Limit to 3-5 tags per document
45
+ - Prefer specific over generic: `nuxt` over `programming`
46
+
47
+ ## Title Extraction
48
+
49
+ The document title is determined in this order:
50
+ 1. `title` field in frontmatter (if present)
51
+ 2. First H1 heading in document body
52
+ 3. Filename without extension
53
+
54
+ The `title` field in frontmatter is optional and only needed to override automatic extraction.