@archznn/crewloop-skills 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -16
- package/package.json +1 -2
- package/packages/cli/dist/agents.js +1 -1
- package/packages/cli/dist/agents.js.map +1 -1
- package/packages/cli/dist/cli.d.ts.map +1 -1
- package/packages/cli/dist/cli.js +2 -30
- package/packages/cli/dist/cli.js.map +1 -1
- package/packages/cli/dist/hooks.d.ts +6 -4
- package/packages/cli/dist/hooks.d.ts.map +1 -1
- package/packages/cli/dist/hooks.js +250 -98
- package/packages/cli/dist/hooks.js.map +1 -1
- package/packages/cli/dist/tests/hooks.test.js +245 -33
- package/packages/cli/dist/tests/hooks.test.js.map +1 -1
- package/references/conventions.md +1 -10
- package/references/workflow.md +1 -1
- package/servers/dashboard/README.md +55 -1
- package/servers/dashboard/dist/adapters/agy.d.ts +19 -0
- package/servers/dashboard/dist/adapters/agy.d.ts.map +1 -0
- package/servers/dashboard/dist/adapters/agy.js +108 -0
- package/servers/dashboard/dist/adapters/agy.js.map +1 -0
- package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/codex.js +2 -0
- package/servers/dashboard/dist/adapters/codex.js.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.d.ts +1 -1
- package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.js +9 -0
- package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.d.ts +1 -1
- package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/shim.js +32 -11
- package/servers/dashboard/dist/adapters/shim.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.test.js +46 -4
- package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
- package/servers/dashboard/dist/lib/constants.d.ts +5 -0
- package/servers/dashboard/dist/lib/constants.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/constants.js +46 -0
- package/servers/dashboard/dist/lib/constants.js.map +1 -0
- package/servers/dashboard/dist/lib/format.d.ts +6 -0
- package/servers/dashboard/dist/lib/format.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/format.js +52 -0
- package/servers/dashboard/dist/lib/format.js.map +1 -0
- package/servers/dashboard/dist/lib/graph.d.ts +22 -0
- package/servers/dashboard/dist/lib/graph.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/graph.js +45 -0
- package/servers/dashboard/dist/lib/graph.js.map +1 -0
- package/servers/dashboard/dist/lib/invocations.d.ts +32 -0
- package/servers/dashboard/dist/lib/invocations.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/invocations.js +135 -0
- package/servers/dashboard/dist/lib/invocations.js.map +1 -0
- package/servers/dashboard/dist/lib/invocations.test.d.ts +2 -0
- package/servers/dashboard/dist/lib/invocations.test.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/invocations.test.js +68 -0
- package/servers/dashboard/dist/lib/invocations.test.js.map +1 -0
- package/servers/dashboard/dist/lib/paths.d.ts +2 -0
- package/servers/dashboard/dist/lib/paths.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/paths.js +40 -0
- package/servers/dashboard/dist/lib/paths.js.map +1 -0
- package/servers/dashboard/dist/presenter.d.ts.map +1 -1
- package/servers/dashboard/dist/presenter.js +2 -0
- package/servers/dashboard/dist/presenter.js.map +1 -1
- package/servers/dashboard/dist/public/assets/index-DjmMKbPN.css +1 -0
- package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js +5323 -0
- package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js.map +1 -0
- package/servers/dashboard/dist/public/index.html +16 -0
- package/servers/dashboard/dist/server.d.ts.map +1 -1
- package/servers/dashboard/dist/server.js +5 -1
- package/servers/dashboard/dist/server.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/infer.js +0 -6
- package/servers/dashboard/dist/skills/infer.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.test.js +10 -3
- package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
- package/servers/dashboard/dist/skills/mapping.d.ts +0 -3
- package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/mapping.js +0 -18
- package/servers/dashboard/dist/skills/mapping.js.map +1 -1
- package/servers/dashboard/dist/skills/registry.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/registry.js +0 -1
- package/servers/dashboard/dist/skills/registry.js.map +1 -1
- package/servers/dashboard/dist/tests/adapters.test.d.ts +2 -0
- package/servers/dashboard/dist/tests/adapters.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/adapters.test.js +180 -0
- package/servers/dashboard/dist/tests/adapters.test.js.map +1 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.d.ts +2 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.js +123 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.js.map +1 -0
- package/servers/dashboard/dist/tests/shim.test.js +88 -2
- package/servers/dashboard/dist/tests/shim.test.js.map +1 -1
- package/servers/dashboard/dist/types.d.ts +5 -2
- package/servers/dashboard/dist/types.d.ts.map +1 -1
- package/servers/dashboard/package.json +22 -5
- package/servers/dashboard/src/adapters/agy.ts +136 -0
- package/servers/dashboard/src/adapters/codex.ts +2 -0
- package/servers/dashboard/src/adapters/kimi.ts +11 -1
- package/servers/dashboard/src/adapters/shim.test.ts +57 -4
- package/servers/dashboard/src/adapters/shim.ts +31 -11
- package/servers/dashboard/src/lib/constants.ts +44 -0
- package/servers/dashboard/src/lib/format.ts +44 -0
- package/servers/dashboard/src/lib/graph.ts +69 -0
- package/servers/dashboard/src/lib/invocations.test.ts +70 -0
- package/servers/dashboard/src/lib/invocations.ts +172 -0
- package/servers/dashboard/src/lib/paths.ts +35 -0
- package/servers/dashboard/src/presenter.ts +2 -0
- package/servers/dashboard/src/server.ts +5 -1
- package/servers/dashboard/src/skills/infer.test.ts +11 -3
- package/servers/dashboard/src/skills/infer.ts +1 -8
- package/servers/dashboard/src/skills/mapping.ts +0 -20
- package/servers/dashboard/src/skills/registry.ts +0 -1
- package/servers/dashboard/src/tests/adapters.test.ts +198 -0
- package/servers/dashboard/src/tests/lib-helpers.test.ts +133 -0
- package/servers/dashboard/src/tests/shim.test.ts +110 -2
- package/servers/dashboard/src/types.ts +5 -3
- package/servers/dashboard/ui/index.html +15 -0
- package/servers/dashboard/ui/postcss.config.js +6 -0
- package/servers/dashboard/ui/src/App.tsx +360 -0
- package/servers/dashboard/ui/src/components/ActiveSkillPanel.tsx +69 -0
- package/servers/dashboard/ui/src/components/ActivityGraph.tsx +74 -0
- package/servers/dashboard/ui/src/components/CommandPalette.tsx +200 -0
- package/servers/dashboard/ui/src/components/FileActivity.tsx +20 -0
- package/servers/dashboard/ui/src/components/FileDiff.tsx +68 -0
- package/servers/dashboard/ui/src/components/FileList.tsx +64 -0
- package/servers/dashboard/ui/src/components/FilterBar.tsx +208 -0
- package/servers/dashboard/ui/src/components/Network3D.tsx +178 -0
- package/servers/dashboard/ui/src/components/SessionSelector.tsx +95 -0
- package/servers/dashboard/ui/src/components/Sidebar.tsx +110 -0
- package/servers/dashboard/ui/src/components/TelemetryPanel.tsx +57 -0
- package/servers/dashboard/ui/src/components/Timeline.tsx +57 -0
- package/servers/dashboard/ui/src/components/TimelineRow.tsx +112 -0
- package/servers/dashboard/ui/src/components/TopBar.tsx +116 -0
- package/servers/dashboard/ui/src/components/ViewHeader.tsx +19 -0
- package/servers/dashboard/ui/src/components/ui/Icon.tsx +105 -0
- package/servers/dashboard/ui/src/components/ui/StatusBadge.tsx +19 -0
- package/servers/dashboard/ui/src/components/views/FilesView.tsx +23 -0
- package/servers/dashboard/ui/src/components/views/NetworkView.tsx +20 -0
- package/servers/dashboard/ui/src/components/views/Overview.tsx +135 -0
- package/servers/dashboard/ui/src/components/views/SessionsView.tsx +84 -0
- package/servers/dashboard/ui/src/components/views/SettingsView.tsx +138 -0
- package/servers/dashboard/ui/src/components/views/SkillsView.tsx +92 -0
- package/servers/dashboard/ui/src/components/views/TimelineView.tsx +46 -0
- package/servers/dashboard/ui/src/contexts/FilterContext.tsx +41 -0
- package/servers/dashboard/ui/src/contexts/PinnedSessionsContext.tsx +80 -0
- package/servers/dashboard/ui/src/contexts/SettingsContext.tsx +60 -0
- package/servers/dashboard/ui/src/hooks/useCommandPalette.ts +36 -0
- package/servers/dashboard/ui/src/hooks/useKeyboardShortcut.ts +38 -0
- package/servers/dashboard/ui/src/hooks/useNow.ts +12 -0
- package/servers/dashboard/ui/src/hooks/useReducedMotion.ts +15 -0
- package/servers/dashboard/ui/src/hooks/useSessions.ts +64 -0
- package/servers/dashboard/ui/src/hooks/useTheme.ts +30 -0
- package/servers/dashboard/ui/src/hooks/useViewport.ts +19 -0
- package/servers/dashboard/ui/src/hooks/useWebSocket.ts +118 -0
- package/servers/dashboard/ui/src/lib/export.test.ts +33 -0
- package/servers/dashboard/ui/src/lib/export.ts +39 -0
- package/servers/dashboard/ui/src/lib/filter.test.ts +95 -0
- package/servers/dashboard/ui/src/lib/filter.ts +178 -0
- package/servers/dashboard/ui/src/lib/format.test.ts +25 -0
- package/servers/dashboard/ui/src/lib/search.test.ts +52 -0
- package/servers/dashboard/ui/src/lib/search.ts +60 -0
- package/servers/dashboard/ui/src/lib/settings.test.ts +50 -0
- package/servers/dashboard/ui/src/lib/settings.ts +56 -0
- package/servers/dashboard/ui/src/lib/types.ts +124 -0
- package/servers/dashboard/ui/src/main.tsx +19 -0
- package/servers/dashboard/ui/src/styles/index.css +155 -0
- package/servers/dashboard/ui/tailwind.config.js +45 -0
- package/servers/dashboard/ui/tsconfig.json +33 -0
- package/servers/dashboard/ui/tsconfig.node.json +10 -0
- package/servers/dashboard/ui/vite.config.ts +37 -0
- package/servers/dashboard/ui/vitest.config.ts +8 -0
- package/skills/accessibility-auditor/SKILL.md +0 -20
- package/skills/architect/SKILL.md +0 -45
- package/skills/designer/SKILL.md +0 -30
- package/skills/docs-writer/SKILL.md +0 -13
- package/skills/engineer/SKILL.md +0 -30
- package/skills/maintainer/SKILL.md +0 -20
- package/skills/orchestrator/SKILL.md +0 -13
- package/skills/product-manager/SKILL.md +0 -20
- package/skills/researcher/SKILL.md +0 -20
- package/skills/reviewer/SKILL.md +0 -30
- package/skills/security-guard/SKILL.md +0 -20
- package/skills/shipper/SKILL.md +0 -33
- package/skills/tester/SKILL.md +0 -20
- package/packages/cli/dist/mcp.d.ts +0 -28
- package/packages/cli/dist/mcp.d.ts.map +0 -1
- package/packages/cli/dist/mcp.js +0 -148
- package/packages/cli/dist/mcp.js.map +0 -1
- package/packages/cli/dist/tests/mcp.test.d.ts +0 -2
- package/packages/cli/dist/tests/mcp.test.d.ts.map +0 -1
- package/packages/cli/dist/tests/mcp.test.js +0 -232
- package/packages/cli/dist/tests/mcp.test.js.map +0 -1
- package/references/obsidian-mcp-usage.md +0 -190
- package/servers/dashboard/public/app.js +0 -516
- package/servers/dashboard/public/index.html +0 -96
- package/servers/dashboard/public/styles.css +0 -819
- package/servers/obsidian-mcp/README.md +0 -82
- package/servers/obsidian-mcp/pyproject.toml +0 -32
- package/servers/obsidian-mcp/src/obsidian_mcp/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/config.py +0 -47
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/embeddings.py +0 -105
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/indexer.py +0 -79
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/store.py +0 -141
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/sync.py +0 -37
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/detector.py +0 -66
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/note_generator.py +0 -40
- package/servers/obsidian-mcp/src/obsidian_mcp/main.py +0 -4
- package/servers/obsidian-mcp/src/obsidian_mcp/models.py +0 -42
- package/servers/obsidian-mcp/src/obsidian_mcp/privacy/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/privacy/filter.py +0 -68
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/engine.py +0 -50
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/graph_search.py +0 -55
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/text_search.py +0 -37
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/vector_search.py +0 -118
- package/servers/obsidian-mcp/src/obsidian_mcp/server.py +0 -61
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/create.py +0 -43
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/delete.py +0 -16
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/learn.py +0 -42
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/list.py +0 -16
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/read.py +0 -15
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/registry.py +0 -130
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/related.py +0 -20
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/search.py +0 -26
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/sync.py +0 -22
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/update.py +0 -34
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/parser.py +0 -82
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/repository.py +0 -68
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/writer.py +0 -61
- package/servers/obsidian-mcp/tests/conftest.py +0 -39
- package/servers/obsidian-mcp/tests/test_async_tools.py +0 -87
- package/servers/obsidian-mcp/tests/test_edge_cases.py +0 -59
- package/servers/obsidian-mcp/tests/test_indexer.py +0 -27
- package/servers/obsidian-mcp/tests/test_integration.py +0 -90
- package/servers/obsidian-mcp/tests/test_learning.py +0 -34
- package/servers/obsidian-mcp/tests/test_privacy.py +0 -31
- package/servers/obsidian-mcp/tests/test_privacy_config.py +0 -44
- package/servers/obsidian-mcp/tests/test_rag.py +0 -64
- package/servers/obsidian-mcp/tests/test_read_raw.py +0 -37
- package/servers/obsidian-mcp/tests/test_tfidf_fallback.py +0 -54
- package/servers/obsidian-mcp/tests/test_tools.py +0 -108
- package/servers/obsidian-mcp/tests/test_vault.py +0 -103
- package/servers/obsidian-mcp/tests/test_writer.py +0 -139
- package/skills/obsidian-second-brain/SKILL.md +0 -298
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# Obsidian MCP Second Brain — Usage for Agents
|
|
2
|
-
|
|
3
|
-
This document explains how the Loop Engineering Agents team should use the local Obsidian MCP server (`servers/obsidian-mcp`).
|
|
4
|
-
|
|
5
|
-
## What It Is
|
|
6
|
-
|
|
7
|
-
A local MCP server exposes a vault at `~/.lea` as a second brain. The AI can:
|
|
8
|
-
|
|
9
|
-
- Search existing knowledge (bundle + vault notes).
|
|
10
|
-
- Read notes.
|
|
11
|
-
- Create and update notes.
|
|
12
|
-
- Learn from conversation text automatically via `learn_from_text`.
|
|
13
|
-
|
|
14
|
-
## Vault Architecture
|
|
15
|
-
|
|
16
|
-
The vault follows a **three-layer memory architecture**. Every skill in the bundle that reads from or writes to `~/.lea` must use these layers.
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
~/.lea/
|
|
20
|
-
├── AGENT.md # Entry point: read this first
|
|
21
|
-
├── MEMORY.md # Curated memory: read at session start
|
|
22
|
-
├── memory/ # Working memory: raw session logs
|
|
23
|
-
├── Memory/ # Durable user profile and preferences
|
|
24
|
-
├── Knowledge/ # Long-lived technical guides and decisions
|
|
25
|
-
├── Journal/ # Session logs and dashboards worth keeping
|
|
26
|
-
├── Notes/ # Temporary notes, drafts, and research
|
|
27
|
-
└── _Inbox/ # Agent proposals before promotion
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Layer Semantics
|
|
31
|
-
|
|
32
|
-
| Layer | Path | When to Use |
|
|
33
|
-
|-------|------|-------------|
|
|
34
|
-
| Agent entry | `AGENT.md` | Read once per session before using the vault. |
|
|
35
|
-
| Curated memory | `MEMORY.md` | Read at the start of every major task. Soft cap ~500 words. |
|
|
36
|
-
| Working memory | `memory/` | Append raw, timestamped session logs. Pattern: `YYYY-MM-DD-HHMM.md`. |
|
|
37
|
-
| User profile | `Memory/` | Durable facts about the user: role, preferences, goals. |
|
|
38
|
-
| Knowledge | `Knowledge/` | Durable technical guides, architectural decisions, reusable docs. |
|
|
39
|
-
| Journal | `Journal/` | Important session outcomes, project briefs, dashboards. |
|
|
40
|
-
| Notes | `Notes/` | Temporary scratchpads, drafts, research not yet canonical. |
|
|
41
|
-
| Inbox | `_Inbox/` | Proposed new canonical notes. Auto-promotion is allowed. |
|
|
42
|
-
|
|
43
|
-
### Session Start
|
|
44
|
-
|
|
45
|
-
1. Call `sync_from_bundle` once per session.
|
|
46
|
-
2. Read `AGENT.md`.
|
|
47
|
-
3. Read `MEMORY.md`.
|
|
48
|
-
4. Search targeted layers based on the user's question.
|
|
49
|
-
|
|
50
|
-
### Layer Selection Rules
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
User asks a question
|
|
54
|
-
↓
|
|
55
|
-
Read AGENT.md and MEMORY.md
|
|
56
|
-
↓
|
|
57
|
-
Durable concept or guide? → search Knowledge/
|
|
58
|
-
User preference or identity? → search Memory/
|
|
59
|
-
Recent session or decision? → search Journal/
|
|
60
|
-
Raw log of current conversation? → append to memory/
|
|
61
|
-
Proposing new canonical knowledge? → create in _Inbox/
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Heartbeat / Distillation
|
|
65
|
-
|
|
66
|
-
Every 2-4 sessions (or at the end of a significant task), the agent should:
|
|
67
|
-
|
|
68
|
-
1. Read recent files in `memory/`.
|
|
69
|
-
2. Update `MEMORY.md` with distilled, short-term-relevant facts.
|
|
70
|
-
3. Promote durable facts from `_Inbox/` to `Memory/`, `Knowledge/`, `Journal/`, or `Notes/`.
|
|
71
|
-
4. Archive or delete obsolete raw logs.
|
|
72
|
-
|
|
73
|
-
Keep `MEMORY.md` under ~500 words. If it grows, move long-lived facts to `Knowledge/` or `Memory/` and keep only active context in `MEMORY.md`.
|
|
74
|
-
|
|
75
|
-
## Frontmatter Rules
|
|
76
|
-
|
|
77
|
-
The MCP server manages note frontmatter automatically. Callers should **not** include YAML frontmatter delimiters (`---`) inside `content`.
|
|
78
|
-
|
|
79
|
-
### Server-managed fields
|
|
80
|
-
|
|
81
|
-
| Field | Source | Behavior |
|
|
82
|
-
|-------|--------|----------|
|
|
83
|
-
| `title` | `title` parameter or derived from path | Always present; server value wins over any embedded value. |
|
|
84
|
-
| `tags` | `tags` parameter plus any embedded tags | Union of caller tags and embedded tags, sorted, deduplicated. |
|
|
85
|
-
| `created` | First save timestamp | Preserved across updates. |
|
|
86
|
-
| `updated` | Current save timestamp | Refreshed on every save. |
|
|
87
|
-
|
|
88
|
-
### Caller responsibilities
|
|
89
|
-
|
|
90
|
-
- Pass `title` and `tags` as top-level parameters of `create_note` / `update_note`.
|
|
91
|
-
- Keep `content` as the Markdown body only.
|
|
92
|
-
- If `content` accidentally contains a frontmatter block, the server extracts it, merges the fields, and writes a single valid block.
|
|
93
|
-
|
|
94
|
-
## Available MCP Tools
|
|
95
|
-
|
|
96
|
-
| Tool | Purpose |
|
|
97
|
-
|------|---------|
|
|
98
|
-
| `search_notes` | Find relevant notes. Prefer `mode: "hybrid"`. |
|
|
99
|
-
| `read_note` | Read a specific note by path. |
|
|
100
|
-
| `create_note` | Create a new note in the correct layer. |
|
|
101
|
-
| `update_note` | Append or replace content. Use `append` to preserve history. |
|
|
102
|
-
| `delete_note` | Remove a note. Use sparingly. |
|
|
103
|
-
| `list_notes` | List notes, optionally filtered by folder. |
|
|
104
|
-
| `get_related_notes` | Explore graph relationships from a note. |
|
|
105
|
-
| `sync_from_bundle` | Re-index the skill bundle and local vault. |
|
|
106
|
-
| `learn_from_text` | Detect concepts/decisions in text and auto-create notes. |
|
|
107
|
-
|
|
108
|
-
## Note Paths
|
|
109
|
-
|
|
110
|
-
Use English folder names and English note content:
|
|
111
|
-
|
|
112
|
-
- `Memory/profile-user.md`
|
|
113
|
-
- `Knowledge/obsidian-mcp-setup.md`
|
|
114
|
-
- `Journal/2026-06-15-project-brief.md`
|
|
115
|
-
- `Notes/2026-06-15-ml-concepts.md`
|
|
116
|
-
- `_Inbox/proposed-knowledge-2026-06-15.md`
|
|
117
|
-
|
|
118
|
-
## Privacy Rules
|
|
119
|
-
|
|
120
|
-
Never persist in the vault:
|
|
121
|
-
|
|
122
|
-
- API keys, secrets, tokens, passwords
|
|
123
|
-
- Private keys or certificates
|
|
124
|
-
- `.env` contents
|
|
125
|
-
- Personal identifiable information (PII)
|
|
126
|
-
|
|
127
|
-
The server has a privacy filter, but avoid sensitive input regardless. Review working memory before promoting anything to curated or structured layers.
|
|
128
|
-
|
|
129
|
-
## Configuration Reminder
|
|
130
|
-
|
|
131
|
-
The server must be installed and added to the agent's MCP config:
|
|
132
|
-
|
|
133
|
-
```toml
|
|
134
|
-
[mcpServers.obsidian-mcp]
|
|
135
|
-
command = "/path/to/servers/obsidian-mcp/.venv/bin/python"
|
|
136
|
-
args = ["-m", "obsidian_mcp.main"]
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
See `servers/obsidian-mcp/README.md` for full setup instructions.
|
|
140
|
-
|
|
141
|
-
## Migration from Flat Folders
|
|
142
|
-
|
|
143
|
-
Old flat folders (`concepts/`, `decisions/`, `projects/`, `dashboards/`) should be migrated to the new layers:
|
|
144
|
-
|
|
145
|
-
- `concepts/` → `Knowledge/` (durable) or `Notes/` (transient)
|
|
146
|
-
- `decisions/` → `Knowledge/`
|
|
147
|
-
- `projects/` → `Journal/` (active) or `Knowledge/` (reference)
|
|
148
|
-
- `dashboards/` → `Journal/`
|
|
149
|
-
|
|
150
|
-
## Skill Memory & Context Pattern
|
|
151
|
-
|
|
152
|
-
Every skill in the bundle must access the local Obsidian vault at `~/.lea` **only** through the `obsidian-second-brain` skill. Never read or write vault files directly with `Read`, `Edit`, `Write`, or `Bash`.
|
|
153
|
-
|
|
154
|
-
### Before acting
|
|
155
|
-
|
|
156
|
-
Invoke the `obsidian-second-brain` skill via the `Skill` tool. It will:
|
|
157
|
-
|
|
158
|
-
1. Read `AGENT.md` once per session if not already loaded.
|
|
159
|
-
2. Read `MEMORY.md` at the start of the task.
|
|
160
|
-
3. Search the layers relevant to this skill's role (see table below).
|
|
161
|
-
|
|
162
|
-
If the vault or MCP server is unavailable, continue without memory.
|
|
163
|
-
|
|
164
|
-
### After acting
|
|
165
|
-
|
|
166
|
-
Invoke the `obsidian-second-brain` skill again to persist outcomes to the correct layer:
|
|
167
|
-
|
|
168
|
-
- Reusable guides, conventions, or architecture notes → `Knowledge/`
|
|
169
|
-
- Session outcomes, decisions, or findings → `Journal/`
|
|
170
|
-
- User profile facts or preferences → `Memory/`
|
|
171
|
-
- Proposed canonical notes awaiting review → `_Inbox/`
|
|
172
|
-
- Active context and priorities → update `MEMORY.md`
|
|
173
|
-
|
|
174
|
-
## Per-Skill Memory Targets
|
|
175
|
-
|
|
176
|
-
Each skill in the bundle should read from and write to the layers relevant to its role. See `skills/obsidian-second-brain/SKILL.md` for tool usage details.
|
|
177
|
-
|
|
178
|
-
| Skill | Read at start | Persist at end |
|
|
179
|
-
|-------|---------------|----------------|
|
|
180
|
-
| `orchestrator` | `MEMORY.md`, `Memory/preferences.md` | User priorities/context to `MEMORY.md`; unclear items to `_Inbox/` |
|
|
181
|
-
| `architect` | `Knowledge/`, `Memory/`, `Journal/decisions*` | Specs rationale to `Knowledge/`; ADRs to `Knowledge/` |
|
|
182
|
-
| `designer` | `Memory/preferences.md`, `Journal/design*`, `Knowledge/brand*` | Design direction to `Journal/`; reusable systems to `Knowledge/` |
|
|
183
|
-
| `engineer` | `Knowledge/`, `Journal/`, `Memory/` | Implementation notes to `Journal/`; reusable patterns to `Knowledge/` |
|
|
184
|
-
| `reviewer` | `Knowledge/conventions*`, `Journal/decisions*`, `Memory/` | Review findings to `Journal/`; process updates to `Knowledge/` |
|
|
185
|
-
| `shipper` | `Knowledge/conventions*`, `Memory/preferences.md` | Shipping log to `Journal/` |
|
|
186
|
-
| `docs-writer` | `Knowledge/`, `Memory/preferences.md`, `Journal/` | New/updated docs to `Knowledge/` |
|
|
187
|
-
| `researcher` | `Knowledge/`, `Journal/` | Research summaries to `Knowledge/` or `_Inbox/` |
|
|
188
|
-
| `maintainer` | `Knowledge/`, `Journal/incidents*` | Incident/debt notes to `Journal/`; runbooks to `Knowledge/` |
|
|
189
|
-
| `product-manager` | `Memory/preferences.md`, `Journal/`, `Knowledge/` | Decisions/metrics to `Knowledge/` or `Journal/` |
|
|
190
|
-
| `tester` | `Knowledge/`, `Journal/bugs*` | Test strategies/heuristics to `Knowledge/` |
|
|
@@ -1,516 +0,0 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// ---- Config ----
|
|
5
|
-
const WS_URL = `ws://${location.host}/ws`;
|
|
6
|
-
const MAX_EVENTS = 100;
|
|
7
|
-
const SKILL_ICONS = {
|
|
8
|
-
orchestrator: 'ph-target',
|
|
9
|
-
architect: 'ph-blueprint',
|
|
10
|
-
designer: 'ph-palette',
|
|
11
|
-
engineer: 'ph-wrench',
|
|
12
|
-
reviewer: 'ph-magnifying-glass',
|
|
13
|
-
shipper: 'ph-rocket-launch',
|
|
14
|
-
'docs-writer': 'ph-article',
|
|
15
|
-
tester: 'ph-flask',
|
|
16
|
-
'product-manager': 'ph-chart-bar',
|
|
17
|
-
maintainer: 'ph-toolbox',
|
|
18
|
-
researcher: 'ph-microscope',
|
|
19
|
-
'security-guard': 'ph-shield',
|
|
20
|
-
'accessibility-auditor': 'ph-person',
|
|
21
|
-
'obsidian-second-brain': 'ph-brain',
|
|
22
|
-
'project-mapper': 'ph-tree-structure',
|
|
23
|
-
default: 'ph-circle',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// ---- State ----
|
|
27
|
-
const state = {
|
|
28
|
-
sessions: new Map(),
|
|
29
|
-
activeSessionId: null,
|
|
30
|
-
selectedSessionId: null,
|
|
31
|
-
theme: localStorage.getItem('crewloop-theme') || 'system',
|
|
32
|
-
connection: 'connecting',
|
|
33
|
-
lastPong: 0,
|
|
34
|
-
eventCountWindow: [],
|
|
35
|
-
ws: null,
|
|
36
|
-
reconnectTimer: null,
|
|
37
|
-
pingTimer: null,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// ---- DOM refs ----
|
|
41
|
-
const $ = (id) => document.getElementById(id);
|
|
42
|
-
const themeToggle = $('theme-toggle');
|
|
43
|
-
const connectionDot = $('connection-dot');
|
|
44
|
-
const sessionTrigger = $('session-trigger');
|
|
45
|
-
const sessionLabel = $('session-label');
|
|
46
|
-
const sessionList = $('session-list');
|
|
47
|
-
const activeStrip = $('active-strip');
|
|
48
|
-
const activeSkillIcon = $('active-skill-icon');
|
|
49
|
-
const activeSkillName = $('active-skill-name');
|
|
50
|
-
const statusBadge = $('status-badge');
|
|
51
|
-
const statusDot = $('status-dot');
|
|
52
|
-
const statusText = $('status-text');
|
|
53
|
-
const confidenceBadge = $('confidence-badge');
|
|
54
|
-
const activeSkillSource = $('active-skill-source');
|
|
55
|
-
const toolCount = $('tool-count');
|
|
56
|
-
const durationEl = $('duration');
|
|
57
|
-
const eventRate = $('event-rate');
|
|
58
|
-
const timeline = $('timeline');
|
|
59
|
-
const activityGraph = $('activity-graph');
|
|
60
|
-
|
|
61
|
-
// ---- Utils ----
|
|
62
|
-
function formatDuration(ms) {
|
|
63
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
64
|
-
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
|
|
65
|
-
const seconds = (totalSeconds % 60).toString().padStart(2, '0');
|
|
66
|
-
if (totalSeconds < 3600) return `${minutes}:${seconds}`;
|
|
67
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
68
|
-
return `${hours}:${minutes}:${seconds}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function formatTime(ts) {
|
|
72
|
-
const d = new Date(ts);
|
|
73
|
-
return d.toLocaleTimeString(undefined, { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function updateEventRate() {
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
const windowStart = now - 60000;
|
|
79
|
-
state.eventCountWindow = state.eventCountWindow.filter((t) => t > windowStart);
|
|
80
|
-
eventRate.textContent = state.eventCountWindow.length.toString();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function skillIcon(skillName) {
|
|
84
|
-
const key = skillName?.toLowerCase().replace(/\s+/g, '-');
|
|
85
|
-
return SKILL_ICONS[key] || SKILL_ICONS.default;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function prefersReducedMotion() {
|
|
89
|
-
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---- Theme ----
|
|
93
|
-
function applyTheme(theme) {
|
|
94
|
-
const root = document.documentElement;
|
|
95
|
-
let resolved = theme;
|
|
96
|
-
if (theme === 'system') {
|
|
97
|
-
resolved = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
98
|
-
}
|
|
99
|
-
root.setAttribute('data-theme', resolved);
|
|
100
|
-
const icon = themeToggle.querySelector('i');
|
|
101
|
-
if (resolved === 'light') {
|
|
102
|
-
icon.className = 'ph ph-sun';
|
|
103
|
-
} else {
|
|
104
|
-
icon.className = 'ph ph-moon';
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function cycleTheme() {
|
|
109
|
-
const order = ['system', 'light', 'dark'];
|
|
110
|
-
const idx = order.indexOf(state.theme);
|
|
111
|
-
state.theme = order[(idx + 1) % order.length];
|
|
112
|
-
localStorage.setItem('crewloop-theme', state.theme);
|
|
113
|
-
applyTheme(state.theme);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ---- WebSocket ----
|
|
117
|
-
function setConnection(status) {
|
|
118
|
-
state.connection = status;
|
|
119
|
-
connectionDot.className = 'connection-dot ' + status;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function connect() {
|
|
123
|
-
setConnection('connecting');
|
|
124
|
-
const ws = new WebSocket(WS_URL);
|
|
125
|
-
state.ws = ws;
|
|
126
|
-
|
|
127
|
-
ws.addEventListener('open', () => {
|
|
128
|
-
setConnection('connected');
|
|
129
|
-
state.lastPong = Date.now();
|
|
130
|
-
startPing();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
ws.addEventListener('message', (event) => {
|
|
134
|
-
let msg;
|
|
135
|
-
try {
|
|
136
|
-
msg = JSON.parse(event.data);
|
|
137
|
-
} catch {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
handleMessage(msg);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
ws.addEventListener('close', () => {
|
|
144
|
-
setConnection('disconnected');
|
|
145
|
-
stopPing();
|
|
146
|
-
scheduleReconnect();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
ws.addEventListener('error', () => {
|
|
150
|
-
ws.close();
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function scheduleReconnect() {
|
|
155
|
-
if (state.reconnectTimer) return;
|
|
156
|
-
state.reconnectTimer = setTimeout(() => {
|
|
157
|
-
state.reconnectTimer = null;
|
|
158
|
-
connect();
|
|
159
|
-
}, 3000);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function startPing() {
|
|
163
|
-
stopPing();
|
|
164
|
-
state.pingTimer = setInterval(() => {
|
|
165
|
-
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
|
166
|
-
state.ws.send(JSON.stringify({ type: 'ping' }));
|
|
167
|
-
if (Date.now() - state.lastPong > 35000) {
|
|
168
|
-
state.ws.close();
|
|
169
|
-
}
|
|
170
|
-
}, 15000);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function stopPing() {
|
|
174
|
-
if (state.pingTimer) {
|
|
175
|
-
clearInterval(state.pingTimer);
|
|
176
|
-
state.pingTimer = null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function handleMessage(msg) {
|
|
181
|
-
if (msg.type === 'pong') {
|
|
182
|
-
state.lastPong = Date.now();
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (msg.type === 'snapshot') {
|
|
186
|
-
state.sessions.clear();
|
|
187
|
-
(msg.sessions || []).forEach((s) => state.sessions.set(s.id, s));
|
|
188
|
-
if (!state.selectedSessionId) {
|
|
189
|
-
state.selectedSessionId = state.activeSessionId || state.sessions.keys().next().value || null;
|
|
190
|
-
}
|
|
191
|
-
renderSessionSelector();
|
|
192
|
-
renderAll();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (msg.type === 'update') {
|
|
196
|
-
const session = msg.session;
|
|
197
|
-
if (!session) return;
|
|
198
|
-
state.sessions.set(session.id, session);
|
|
199
|
-
if (msg.isActive) {
|
|
200
|
-
state.activeSessionId = session.id;
|
|
201
|
-
if (!state.selectedSessionId) state.selectedSessionId = session.id;
|
|
202
|
-
}
|
|
203
|
-
state.eventCountWindow.push(Date.now());
|
|
204
|
-
renderSessionSelector();
|
|
205
|
-
renderAll();
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ---- Rendering ----
|
|
210
|
-
function getSession() {
|
|
211
|
-
return state.sessions.get(state.selectedSessionId) || null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function renderAll() {
|
|
215
|
-
const session = getSession();
|
|
216
|
-
renderActiveSkill(session);
|
|
217
|
-
renderTelemetry(session);
|
|
218
|
-
renderTimeline(session);
|
|
219
|
-
renderActivityGraph(session);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let lifecycleBadgeEl = null;
|
|
223
|
-
let emptyStateEl = null;
|
|
224
|
-
|
|
225
|
-
function getActiveSkillContent() {
|
|
226
|
-
return activeSkillName.parentElement;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function setChildrenVisibility(visible) {
|
|
230
|
-
const content = getActiveSkillContent();
|
|
231
|
-
if (!content) return;
|
|
232
|
-
Array.from(content.children).forEach((child) => {
|
|
233
|
-
if (child === emptyStateEl) return;
|
|
234
|
-
child.style.display = visible ? '' : 'none';
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function renderEmptyState() {
|
|
239
|
-
if (!emptyStateEl) {
|
|
240
|
-
emptyStateEl = document.createElement('div');
|
|
241
|
-
emptyStateEl.className = 'empty-state';
|
|
242
|
-
emptyStateEl.innerHTML = `
|
|
243
|
-
<div class="empty-state-icon"><i class="ph ph-monitor-play"></i></div>
|
|
244
|
-
<h2 class="empty-state-title">NO ACTIVE SESSION</h2>
|
|
245
|
-
<p class="empty-state-body">Start an agent session to see it here.</p>
|
|
246
|
-
`;
|
|
247
|
-
getActiveSkillContent()?.appendChild(emptyStateEl);
|
|
248
|
-
}
|
|
249
|
-
emptyStateEl.style.display = 'flex';
|
|
250
|
-
setChildrenVisibility(false);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function hideEmptyState() {
|
|
254
|
-
if (emptyStateEl) {
|
|
255
|
-
emptyStateEl.style.display = 'none';
|
|
256
|
-
}
|
|
257
|
-
setChildrenVisibility(true);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function renderLifecycleBadge(session) {
|
|
261
|
-
if (!lifecycleBadgeEl) {
|
|
262
|
-
lifecycleBadgeEl = document.createElement('span');
|
|
263
|
-
lifecycleBadgeEl.className = 'lifecycle-badge';
|
|
264
|
-
statusBadge.parentNode?.insertBefore(lifecycleBadgeEl, statusBadge.nextSibling);
|
|
265
|
-
}
|
|
266
|
-
const lifecycle = session.lifecycle || 'starting';
|
|
267
|
-
lifecycleBadgeEl.className = 'lifecycle-badge ' + lifecycle;
|
|
268
|
-
lifecycleBadgeEl.innerHTML = `<span class="lifecycle-dot"></span>${lifecycle.toUpperCase()}`;
|
|
269
|
-
lifecycleBadgeEl.style.display = 'inline-flex';
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function hideLifecycleBadge() {
|
|
273
|
-
if (lifecycleBadgeEl) {
|
|
274
|
-
lifecycleBadgeEl.style.display = 'none';
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function renderActiveSkill(session) {
|
|
279
|
-
if (!session) {
|
|
280
|
-
renderEmptyState();
|
|
281
|
-
activeStrip.className = 'panel-accent-strip';
|
|
282
|
-
hideLifecycleBadge();
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
hideEmptyState();
|
|
287
|
-
|
|
288
|
-
const skill = session.activeSkill || { name: 'UNKNOWN', confidence: 'low' };
|
|
289
|
-
const iconClass = skillIcon(skill.name);
|
|
290
|
-
activeSkillName.textContent = skill.name.toUpperCase();
|
|
291
|
-
activeSkillIcon.className = 'ph ' + iconClass;
|
|
292
|
-
|
|
293
|
-
const lifecycle = session.lifecycle || 'starting';
|
|
294
|
-
activeStrip.className = 'panel-accent-strip ' + lifecycle;
|
|
295
|
-
|
|
296
|
-
if (lifecycle === 'ended') {
|
|
297
|
-
statusDot.className = 'status-dot ' + (session.status || '');
|
|
298
|
-
statusText.textContent = session.status ? session.status.toUpperCase() : 'ENDED';
|
|
299
|
-
} else {
|
|
300
|
-
statusDot.className = 'status-dot ' + lifecycle;
|
|
301
|
-
statusText.textContent = lifecycle.toUpperCase();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
renderLifecycleBadge(session);
|
|
305
|
-
confidenceBadge.textContent = skill.confidence || 'low';
|
|
306
|
-
|
|
307
|
-
activeSkillSource.innerHTML = `<i class="ph ph-${sourceIcon(session.source)}"></i><span>${session.source || 'unknown'}</span>`;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function sourceIcon(source) {
|
|
311
|
-
switch (source) {
|
|
312
|
-
case 'kimi': return 'chat-teardrop-text';
|
|
313
|
-
case 'codex': return 'terminal';
|
|
314
|
-
case 'opencode': return 'code-block';
|
|
315
|
-
case 'log-watcher': return 'file-text';
|
|
316
|
-
default: return 'monitor';
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function renderTelemetry(session) {
|
|
321
|
-
if (!session) {
|
|
322
|
-
toolCount.textContent = '0';
|
|
323
|
-
durationEl.textContent = '00:00';
|
|
324
|
-
updateEventRate();
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const events = session.events || [];
|
|
328
|
-
const toolEvents = events.filter((e) => e.event_type === 'tool_start' || e.event_type === 'tool_end');
|
|
329
|
-
toolCount.textContent = String(Math.ceil(toolEvents.length / 2));
|
|
330
|
-
const endTime = session.endedAt || session.lastActivity;
|
|
331
|
-
const dur = endTime && session.startTime ? endTime - session.startTime : 0;
|
|
332
|
-
durationEl.textContent = formatDuration(dur);
|
|
333
|
-
updateEventRate();
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function renderTimeline(session) {
|
|
337
|
-
timeline.innerHTML = '';
|
|
338
|
-
if (!session) return;
|
|
339
|
-
const events = (session.events || []).slice(-MAX_EVENTS).reverse();
|
|
340
|
-
const fragment = document.createDocumentFragment();
|
|
341
|
-
events.forEach((ev) => {
|
|
342
|
-
const li = document.createElement('li');
|
|
343
|
-
li.className = 'timeline-item';
|
|
344
|
-
const outcome = ev.status || (ev.event_type.endsWith('_end') ? 'success' : '');
|
|
345
|
-
li.innerHTML = `
|
|
346
|
-
<span class="timeline-time">${formatTime(ev.timestamp)}</span>
|
|
347
|
-
<span class="timeline-dot ${outcome}"></span>
|
|
348
|
-
<div class="timeline-main">
|
|
349
|
-
<span class="timeline-tool">${ev.tool || ev.event_type}</span>
|
|
350
|
-
<span class="timeline-detail">${escapeHtml(ev.detail || ev.skill || '')}</span>
|
|
351
|
-
</div>
|
|
352
|
-
<span class="timeline-outcome">${outcome}</span>
|
|
353
|
-
`;
|
|
354
|
-
fragment.appendChild(li);
|
|
355
|
-
});
|
|
356
|
-
timeline.appendChild(fragment);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function escapeHtml(str) {
|
|
360
|
-
return String(str)
|
|
361
|
-
.replace(/&/g, '&')
|
|
362
|
-
.replace(/</g, '<')
|
|
363
|
-
.replace(/>/g, '>')
|
|
364
|
-
.replace(/"/g, '"')
|
|
365
|
-
.replace(/'/g, ''');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function renderActivityGraph(session) {
|
|
369
|
-
activityGraph.innerHTML = '';
|
|
370
|
-
if (!session || !session.events || session.events.length === 0) {
|
|
371
|
-
activityGraph.innerHTML = '<div class="activity-empty">Waiting for agent activity...</div>';
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const events = session.events;
|
|
376
|
-
const now = Date.now();
|
|
377
|
-
const span = Math.max(60000, now - Math.min(...events.map((e) => e.timestamp)));
|
|
378
|
-
const buckets = 40;
|
|
379
|
-
const bucketMs = span / buckets;
|
|
380
|
-
const counts = new Array(buckets).fill(0);
|
|
381
|
-
|
|
382
|
-
events.forEach((e) => {
|
|
383
|
-
const idx = Math.min(buckets - 1, Math.floor((now - e.timestamp) / bucketMs));
|
|
384
|
-
const safeIdx = buckets - 1 - idx;
|
|
385
|
-
counts[safeIdx]++;
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
const max = Math.max(1, ...counts);
|
|
389
|
-
const canvas = document.createElement('canvas');
|
|
390
|
-
canvas.className = 'activity-canvas';
|
|
391
|
-
const rect = activityGraph.getBoundingClientRect();
|
|
392
|
-
const dpr = window.devicePixelRatio || 1;
|
|
393
|
-
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
|
394
|
-
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
|
395
|
-
const ctx = canvas.getContext('2d');
|
|
396
|
-
ctx.scale(dpr, dpr);
|
|
397
|
-
const width = rect.width;
|
|
398
|
-
const height = rect.height;
|
|
399
|
-
const pad = 4;
|
|
400
|
-
const barW = (width - pad * 2) / buckets;
|
|
401
|
-
|
|
402
|
-
const style = getComputedStyle(document.documentElement);
|
|
403
|
-
const accent = style.getPropertyValue('--accent').trim() || '#22d3ee';
|
|
404
|
-
const muted = style.getPropertyValue('--bg-inset').trim() || '#1c1c1f';
|
|
405
|
-
|
|
406
|
-
// background grid
|
|
407
|
-
ctx.fillStyle = muted;
|
|
408
|
-
ctx.fillRect(0, 0, width, height);
|
|
409
|
-
|
|
410
|
-
counts.forEach((count, i) => {
|
|
411
|
-
const barH = (count / max) * (height - pad * 2);
|
|
412
|
-
const x = pad + i * barW;
|
|
413
|
-
const y = height - pad - barH;
|
|
414
|
-
ctx.fillStyle = accent;
|
|
415
|
-
ctx.fillRect(x + 1, y, Math.max(1, barW - 2), Math.max(1, barH));
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
activityGraph.appendChild(canvas);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function renderSessionSelector() {
|
|
422
|
-
sessionList.innerHTML = '';
|
|
423
|
-
if (state.sessions.size === 0) {
|
|
424
|
-
sessionLabel.textContent = 'No session';
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const sessions = Array.from(state.sessions.values()).sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0));
|
|
429
|
-
if (!state.selectedSessionId || !state.sessions.has(state.selectedSessionId)) {
|
|
430
|
-
state.selectedSessionId = sessions[0].id;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const activeSession = state.sessions.get(state.activeSessionId);
|
|
434
|
-
let label = 'No session';
|
|
435
|
-
const current = state.sessions.get(state.selectedSessionId);
|
|
436
|
-
if (current) {
|
|
437
|
-
label = current.skill ? current.skill.toUpperCase() : truncate(current.id, 12);
|
|
438
|
-
if (current.id === state.activeSessionId && activeSession) {
|
|
439
|
-
label = '● ' + label;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
sessionLabel.textContent = label;
|
|
443
|
-
|
|
444
|
-
sessions.forEach((s) => {
|
|
445
|
-
const li = document.createElement('li');
|
|
446
|
-
li.className = 'session-item' + (s.id === state.selectedSessionId ? ' active' : '');
|
|
447
|
-
li.setAttribute('role', 'option');
|
|
448
|
-
li.setAttribute('aria-selected', String(s.id === state.selectedSessionId));
|
|
449
|
-
const duration = s.endedAt
|
|
450
|
-
? `ended after ${formatDuration(s.endedAt - s.startTime)}`
|
|
451
|
-
: formatDuration(Date.now() - s.startTime);
|
|
452
|
-
li.innerHTML = `
|
|
453
|
-
<div class="session-item-main">
|
|
454
|
-
<span class="session-item-id">${truncate(s.id, 16)}</span>
|
|
455
|
-
<span class="session-item-meta">${formatTime(s.startTime)} · ${duration}</span>
|
|
456
|
-
</div>
|
|
457
|
-
<span class="session-item-source">${s.source || 'unknown'}</span>
|
|
458
|
-
`;
|
|
459
|
-
li.addEventListener('click', () => {
|
|
460
|
-
state.selectedSessionId = s.id;
|
|
461
|
-
closeSessionList();
|
|
462
|
-
renderAll();
|
|
463
|
-
});
|
|
464
|
-
sessionList.appendChild(li);
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function truncate(str, n) {
|
|
469
|
-
if (!str) return '';
|
|
470
|
-
return str.length > n ? str.slice(0, n - 1) + '…' : str;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function toggleSessionList() {
|
|
474
|
-
const open = sessionList.classList.toggle('open');
|
|
475
|
-
sessionTrigger.setAttribute('aria-expanded', String(open));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function closeSessionList() {
|
|
479
|
-
sessionList.classList.remove('open');
|
|
480
|
-
sessionTrigger.setAttribute('aria-expanded', 'false');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ---- Event listeners ----
|
|
484
|
-
themeToggle.addEventListener('click', cycleTheme);
|
|
485
|
-
|
|
486
|
-
sessionTrigger.addEventListener('click', (e) => {
|
|
487
|
-
e.stopPropagation();
|
|
488
|
-
toggleSessionList();
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
document.addEventListener('click', (e) => {
|
|
492
|
-
if (!sessionTrigger.contains(e.target) && !sessionList.contains(e.target)) {
|
|
493
|
-
closeSessionList();
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
window.addEventListener('resize', () => {
|
|
498
|
-
if (prefersReducedMotion()) {
|
|
499
|
-
renderActivityGraph(getSession());
|
|
500
|
-
} else {
|
|
501
|
-
window.requestAnimationFrame(() => renderActivityGraph(getSession()));
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// Keep duration ticking
|
|
506
|
-
setInterval(() => {
|
|
507
|
-
const session = getSession();
|
|
508
|
-
if (session && session.status === 'running') {
|
|
509
|
-
renderTelemetry(session);
|
|
510
|
-
}
|
|
511
|
-
}, 1000);
|
|
512
|
-
|
|
513
|
-
// Init
|
|
514
|
-
applyTheme(state.theme);
|
|
515
|
-
connect();
|
|
516
|
-
})();
|