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.
- package/.env.example +58 -0
- package/Claude/CLAUDE.md +92 -0
- package/Claude/hooks/lib/__init__.py +1 -0
- package/Claude/hooks/lib/hook_client.py +207 -0
- package/Claude/hooks/log-event.py +97 -0
- package/Claude/hooks/pre-compact.py +46 -0
- package/Claude/hooks/session-end.py +26 -0
- package/Claude/hooks/session-start.py +35 -0
- package/Claude/hooks/stop-extract.py +40 -0
- package/Claude/rules/frontmatter.md +54 -0
- package/Claude/rules/markdown.md +43 -0
- package/Claude/rules/note-organization.md +33 -0
- package/Claude/settings.json +54 -0
- package/Claude/skills/README.md +136 -0
- package/Claude/skills/_lib/__init__.py +1 -0
- package/Claude/skills/_lib/api.py +164 -0
- package/Claude/skills/_lib/output.py +95 -0
- package/Claude/skills/environment/SKILL.md +73 -0
- package/Claude/skills/environment/environment.py +239 -0
- package/Claude/skills/memory/SKILL.md +153 -0
- package/Claude/skills/memory/memory.py +270 -0
- package/Claude/skills/project/SKILL.md +105 -0
- package/Claude/skills/project/project.py +203 -0
- package/Claude/skills/skill-creator/SKILL.md +261 -0
- package/Claude/skills/task/SKILL.md +135 -0
- package/Claude/skills/task/task.py +310 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +39 -0
- package/app/assets/css/main.css +10 -0
- package/app/components/AppLogo.vue +40 -0
- package/app/components/AssistantPanel.client.vue +518 -0
- package/app/components/ConfirmModal.vue +84 -0
- package/app/components/TemplateMenu.vue +49 -0
- package/app/components/agents/AgentActivityChart.client.vue +105 -0
- package/app/components/agents/AgentActivityChart.server.vue +25 -0
- package/app/components/agents/AgentForm.vue +304 -0
- package/app/components/agents/AgentRunModal.vue +154 -0
- package/app/components/agents/AgentStatsCards.vue +98 -0
- package/app/components/chat/ChatInput.vue +85 -0
- package/app/components/chat/ConversationList.vue +78 -0
- package/app/components/chat/MessageBubble.vue +81 -0
- package/app/components/chat/StreamingMessage.vue +36 -0
- package/app/components/chat/ToolCallBlock.vue +77 -0
- package/app/components/editor/CodeEditor.client.vue +212 -0
- package/app/components/editor/CodeEditorFallback.vue +12 -0
- package/app/components/editor/DocumentEditor.vue +326 -0
- package/app/components/editor/DocumentMetadata.vue +140 -0
- package/app/components/editor/MarkdownEditor.vue +146 -0
- package/app/components/files/FileTree.vue +436 -0
- package/app/components/hooks/HookActivityChart.client.vue +117 -0
- package/app/components/hooks/HookActivityChart.server.vue +25 -0
- package/app/components/hooks/HookStatsCards.vue +63 -0
- package/app/components/hooks/RecentEventsTable.vue +123 -0
- package/app/components/hooks/ToolBreakdownTable.vue +72 -0
- package/app/components/search/DashboardSearch.vue +122 -0
- package/app/components/tasks/ProjectSelect.vue +35 -0
- package/app/components/tasks/TaskCard.vue +182 -0
- package/app/components/tasks/TaskDetail.vue +160 -0
- package/app/components/tasks/TaskForm.vue +280 -0
- package/app/components/tasks/TaskList.vue +69 -0
- package/app/components/view/ViewToc.vue +85 -0
- package/app/composables/useAgents.ts +153 -0
- package/app/composables/useAuth.ts +73 -0
- package/app/composables/useChat.ts +298 -0
- package/app/composables/useDocument.ts +141 -0
- package/app/composables/useEditor.ts +100 -0
- package/app/composables/useFileTree.ts +220 -0
- package/app/composables/useHookEvents.ts +68 -0
- package/app/composables/useMemories.ts +83 -0
- package/app/composables/useNotificationBus.ts +154 -0
- package/app/composables/usePreferences.ts +131 -0
- package/app/composables/useProjects.ts +97 -0
- package/app/composables/useSearch.ts +52 -0
- package/app/composables/useTasks.ts +201 -0
- package/app/composables/useTerminal.ts +135 -0
- package/app/layouts/auth.vue +20 -0
- package/app/layouts/dashboard.vue +186 -0
- package/app/layouts/view.vue +60 -0
- package/app/middleware/auth.ts +9 -0
- package/app/pages/agents/[id].vue +602 -0
- package/app/pages/agents/index.vue +412 -0
- package/app/pages/chat.vue +146 -0
- package/app/pages/dashboard.vue +80 -0
- package/app/pages/docs.vue +131 -0
- package/app/pages/hooks.vue +163 -0
- package/app/pages/index.vue +249 -0
- package/app/pages/login.vue +60 -0
- package/app/pages/memories.vue +282 -0
- package/app/pages/settings.vue +625 -0
- package/app/pages/tasks.vue +312 -0
- package/app/pages/view/[uuid].vue +376 -0
- package/dist/cli/index.js +2711 -0
- package/drizzle.config.ts +10 -0
- package/nuxt.config.ts +98 -0
- package/package.json +107 -0
- package/server/api/agents/[id]/cancel.post.ts +27 -0
- package/server/api/agents/[id]/run.post.ts +34 -0
- package/server/api/agents/[id]/runs.get.ts +45 -0
- package/server/api/agents/[id]/stats.get.ts +94 -0
- package/server/api/agents/[id].delete.ts +29 -0
- package/server/api/agents/[id].get.ts +25 -0
- package/server/api/agents/[id].patch.ts +55 -0
- package/server/api/agents/index.get.ts +15 -0
- package/server/api/agents/index.post.ts +48 -0
- package/server/api/agents/stats.get.ts +86 -0
- package/server/api/auth/[...all].ts +5 -0
- package/server/api/conversations/[id].delete.ts +16 -0
- package/server/api/conversations/[id].get.ts +34 -0
- package/server/api/conversations/index.get.ts +17 -0
- package/server/api/documents/[id]/index.delete.ts +47 -0
- package/server/api/documents/[id]/index.put.ts +102 -0
- package/server/api/documents/[id]/public.get.ts +60 -0
- package/server/api/documents/[id]/restore.post.ts +65 -0
- package/server/api/documents/by-path.post.ts +168 -0
- package/server/api/documents/index.get.ts +48 -0
- package/server/api/fs/delete.post.ts +41 -0
- package/server/api/fs/list.get.ts +99 -0
- package/server/api/fs/mkdir.post.ts +44 -0
- package/server/api/fs/move.post.ts +68 -0
- package/server/api/fs/read.post.ts +48 -0
- package/server/api/fs/rename.post.ts +55 -0
- package/server/api/fs/write.post.ts +51 -0
- package/server/api/health.get.ts +40 -0
- package/server/api/home.get.ts +26 -0
- package/server/api/hooks/events/index.get.ts +56 -0
- package/server/api/hooks/events/index.post.ts +36 -0
- package/server/api/hooks/stats.get.ts +99 -0
- package/server/api/memory/[id].delete.ts +26 -0
- package/server/api/memory/context.get.ts +83 -0
- package/server/api/memory/extract.post.ts +42 -0
- package/server/api/memory/search.get.ts +70 -0
- package/server/api/memory/store.post.ts +31 -0
- package/server/api/projects/[id]/index.delete.ts +40 -0
- package/server/api/projects/[id]/index.get.ts +25 -0
- package/server/api/projects/[id]/index.put.ts +50 -0
- package/server/api/projects/index.get.ts +20 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/secrets/[key].delete.ts +31 -0
- package/server/api/secrets/[key].get.ts +30 -0
- package/server/api/secrets/[key].put.ts +52 -0
- package/server/api/secrets/index.get.ts +20 -0
- package/server/api/secrets/index.post.ts +58 -0
- package/server/api/tasks/[id]/index.delete.ts +46 -0
- package/server/api/tasks/[id]/index.get.ts +24 -0
- package/server/api/tasks/[id]/index.put.ts +70 -0
- package/server/api/tasks/[id]/restore.post.ts +49 -0
- package/server/api/tasks/index.get.ts +53 -0
- package/server/api/tasks/index.post.ts +47 -0
- package/server/api/tasks/tags.get.ts +21 -0
- package/server/api/user/email.patch.ts +56 -0
- package/server/db/index.ts +76 -0
- package/server/db/migrate.ts +41 -0
- package/server/db/schema.ts +345 -0
- package/server/db/seed.ts +46 -0
- package/server/db/types.ts +28 -0
- package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
- package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
- package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
- package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
- package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
- package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
- package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
- package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
- package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
- package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
- package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
- package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
- package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
- package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
- package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
- package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
- package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
- package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
- package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
- package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
- package/server/drizzle/migrations/meta/_journal.json +76 -0
- package/server/middleware/auth.ts +79 -0
- package/server/plugins/00.env-validate.ts +38 -0
- package/server/plugins/01.api-token.ts +31 -0
- package/server/plugins/02.database.ts +54 -0
- package/server/plugins/03.file-watcher.ts +65 -0
- package/server/plugins/04.cron-agents.ts +26 -0
- package/server/routes/_ws/chat.ts +252 -0
- package/server/routes/notifications.ts +47 -0
- package/server/routes/terminal.ts +98 -0
- package/server/services/agent-executor.ts +218 -0
- package/server/services/cron-scheduler.ts +78 -0
- package/server/services/memory-extractor.ts +120 -0
- package/server/utils/agent-cleanup.ts +91 -0
- package/server/utils/agent-registry.ts +95 -0
- package/server/utils/auth.ts +33 -0
- package/server/utils/chat-session-manager.ts +59 -0
- package/server/utils/crypto.ts +40 -0
- package/server/utils/db-guard.ts +12 -0
- package/server/utils/db-state.ts +63 -0
- package/server/utils/document-sync.ts +207 -0
- package/server/utils/frontmatter.ts +84 -0
- package/server/utils/notification-bus.ts +60 -0
- package/server/utils/path-validator.ts +55 -0
- package/server/utils/pty-manager.ts +130 -0
- package/shared/types/index.ts +604 -0
- package/shared/utils/language-detection.ts +87 -0
- 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)
|
package/Claude/CLAUDE.md
ADDED
|
@@ -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.
|