@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.
Files changed (245) hide show
  1. package/README.md +4 -16
  2. package/package.json +1 -2
  3. package/packages/cli/dist/agents.js +1 -1
  4. package/packages/cli/dist/agents.js.map +1 -1
  5. package/packages/cli/dist/cli.d.ts.map +1 -1
  6. package/packages/cli/dist/cli.js +2 -30
  7. package/packages/cli/dist/cli.js.map +1 -1
  8. package/packages/cli/dist/hooks.d.ts +6 -4
  9. package/packages/cli/dist/hooks.d.ts.map +1 -1
  10. package/packages/cli/dist/hooks.js +250 -98
  11. package/packages/cli/dist/hooks.js.map +1 -1
  12. package/packages/cli/dist/tests/hooks.test.js +245 -33
  13. package/packages/cli/dist/tests/hooks.test.js.map +1 -1
  14. package/references/conventions.md +1 -10
  15. package/references/workflow.md +1 -1
  16. package/servers/dashboard/README.md +55 -1
  17. package/servers/dashboard/dist/adapters/agy.d.ts +19 -0
  18. package/servers/dashboard/dist/adapters/agy.d.ts.map +1 -0
  19. package/servers/dashboard/dist/adapters/agy.js +108 -0
  20. package/servers/dashboard/dist/adapters/agy.js.map +1 -0
  21. package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
  22. package/servers/dashboard/dist/adapters/codex.js +2 -0
  23. package/servers/dashboard/dist/adapters/codex.js.map +1 -1
  24. package/servers/dashboard/dist/adapters/kimi.d.ts +1 -1
  25. package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
  26. package/servers/dashboard/dist/adapters/kimi.js +9 -0
  27. package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
  28. package/servers/dashboard/dist/adapters/shim.d.ts +1 -1
  29. package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
  30. package/servers/dashboard/dist/adapters/shim.js +32 -11
  31. package/servers/dashboard/dist/adapters/shim.js.map +1 -1
  32. package/servers/dashboard/dist/adapters/shim.test.js +46 -4
  33. package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
  34. package/servers/dashboard/dist/lib/constants.d.ts +5 -0
  35. package/servers/dashboard/dist/lib/constants.d.ts.map +1 -0
  36. package/servers/dashboard/dist/lib/constants.js +46 -0
  37. package/servers/dashboard/dist/lib/constants.js.map +1 -0
  38. package/servers/dashboard/dist/lib/format.d.ts +6 -0
  39. package/servers/dashboard/dist/lib/format.d.ts.map +1 -0
  40. package/servers/dashboard/dist/lib/format.js +52 -0
  41. package/servers/dashboard/dist/lib/format.js.map +1 -0
  42. package/servers/dashboard/dist/lib/graph.d.ts +22 -0
  43. package/servers/dashboard/dist/lib/graph.d.ts.map +1 -0
  44. package/servers/dashboard/dist/lib/graph.js +45 -0
  45. package/servers/dashboard/dist/lib/graph.js.map +1 -0
  46. package/servers/dashboard/dist/lib/invocations.d.ts +32 -0
  47. package/servers/dashboard/dist/lib/invocations.d.ts.map +1 -0
  48. package/servers/dashboard/dist/lib/invocations.js +135 -0
  49. package/servers/dashboard/dist/lib/invocations.js.map +1 -0
  50. package/servers/dashboard/dist/lib/invocations.test.d.ts +2 -0
  51. package/servers/dashboard/dist/lib/invocations.test.d.ts.map +1 -0
  52. package/servers/dashboard/dist/lib/invocations.test.js +68 -0
  53. package/servers/dashboard/dist/lib/invocations.test.js.map +1 -0
  54. package/servers/dashboard/dist/lib/paths.d.ts +2 -0
  55. package/servers/dashboard/dist/lib/paths.d.ts.map +1 -0
  56. package/servers/dashboard/dist/lib/paths.js +40 -0
  57. package/servers/dashboard/dist/lib/paths.js.map +1 -0
  58. package/servers/dashboard/dist/presenter.d.ts.map +1 -1
  59. package/servers/dashboard/dist/presenter.js +2 -0
  60. package/servers/dashboard/dist/presenter.js.map +1 -1
  61. package/servers/dashboard/dist/public/assets/index-DjmMKbPN.css +1 -0
  62. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js +5323 -0
  63. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js.map +1 -0
  64. package/servers/dashboard/dist/public/index.html +16 -0
  65. package/servers/dashboard/dist/server.d.ts.map +1 -1
  66. package/servers/dashboard/dist/server.js +5 -1
  67. package/servers/dashboard/dist/server.js.map +1 -1
  68. package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
  69. package/servers/dashboard/dist/skills/infer.js +0 -6
  70. package/servers/dashboard/dist/skills/infer.js.map +1 -1
  71. package/servers/dashboard/dist/skills/infer.test.js +10 -3
  72. package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
  73. package/servers/dashboard/dist/skills/mapping.d.ts +0 -3
  74. package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -1
  75. package/servers/dashboard/dist/skills/mapping.js +0 -18
  76. package/servers/dashboard/dist/skills/mapping.js.map +1 -1
  77. package/servers/dashboard/dist/skills/registry.d.ts.map +1 -1
  78. package/servers/dashboard/dist/skills/registry.js +0 -1
  79. package/servers/dashboard/dist/skills/registry.js.map +1 -1
  80. package/servers/dashboard/dist/tests/adapters.test.d.ts +2 -0
  81. package/servers/dashboard/dist/tests/adapters.test.d.ts.map +1 -0
  82. package/servers/dashboard/dist/tests/adapters.test.js +180 -0
  83. package/servers/dashboard/dist/tests/adapters.test.js.map +1 -0
  84. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts +2 -0
  85. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts.map +1 -0
  86. package/servers/dashboard/dist/tests/lib-helpers.test.js +123 -0
  87. package/servers/dashboard/dist/tests/lib-helpers.test.js.map +1 -0
  88. package/servers/dashboard/dist/tests/shim.test.js +88 -2
  89. package/servers/dashboard/dist/tests/shim.test.js.map +1 -1
  90. package/servers/dashboard/dist/types.d.ts +5 -2
  91. package/servers/dashboard/dist/types.d.ts.map +1 -1
  92. package/servers/dashboard/package.json +22 -5
  93. package/servers/dashboard/src/adapters/agy.ts +136 -0
  94. package/servers/dashboard/src/adapters/codex.ts +2 -0
  95. package/servers/dashboard/src/adapters/kimi.ts +11 -1
  96. package/servers/dashboard/src/adapters/shim.test.ts +57 -4
  97. package/servers/dashboard/src/adapters/shim.ts +31 -11
  98. package/servers/dashboard/src/lib/constants.ts +44 -0
  99. package/servers/dashboard/src/lib/format.ts +44 -0
  100. package/servers/dashboard/src/lib/graph.ts +69 -0
  101. package/servers/dashboard/src/lib/invocations.test.ts +70 -0
  102. package/servers/dashboard/src/lib/invocations.ts +172 -0
  103. package/servers/dashboard/src/lib/paths.ts +35 -0
  104. package/servers/dashboard/src/presenter.ts +2 -0
  105. package/servers/dashboard/src/server.ts +5 -1
  106. package/servers/dashboard/src/skills/infer.test.ts +11 -3
  107. package/servers/dashboard/src/skills/infer.ts +1 -8
  108. package/servers/dashboard/src/skills/mapping.ts +0 -20
  109. package/servers/dashboard/src/skills/registry.ts +0 -1
  110. package/servers/dashboard/src/tests/adapters.test.ts +198 -0
  111. package/servers/dashboard/src/tests/lib-helpers.test.ts +133 -0
  112. package/servers/dashboard/src/tests/shim.test.ts +110 -2
  113. package/servers/dashboard/src/types.ts +5 -3
  114. package/servers/dashboard/ui/index.html +15 -0
  115. package/servers/dashboard/ui/postcss.config.js +6 -0
  116. package/servers/dashboard/ui/src/App.tsx +360 -0
  117. package/servers/dashboard/ui/src/components/ActiveSkillPanel.tsx +69 -0
  118. package/servers/dashboard/ui/src/components/ActivityGraph.tsx +74 -0
  119. package/servers/dashboard/ui/src/components/CommandPalette.tsx +200 -0
  120. package/servers/dashboard/ui/src/components/FileActivity.tsx +20 -0
  121. package/servers/dashboard/ui/src/components/FileDiff.tsx +68 -0
  122. package/servers/dashboard/ui/src/components/FileList.tsx +64 -0
  123. package/servers/dashboard/ui/src/components/FilterBar.tsx +208 -0
  124. package/servers/dashboard/ui/src/components/Network3D.tsx +178 -0
  125. package/servers/dashboard/ui/src/components/SessionSelector.tsx +95 -0
  126. package/servers/dashboard/ui/src/components/Sidebar.tsx +110 -0
  127. package/servers/dashboard/ui/src/components/TelemetryPanel.tsx +57 -0
  128. package/servers/dashboard/ui/src/components/Timeline.tsx +57 -0
  129. package/servers/dashboard/ui/src/components/TimelineRow.tsx +112 -0
  130. package/servers/dashboard/ui/src/components/TopBar.tsx +116 -0
  131. package/servers/dashboard/ui/src/components/ViewHeader.tsx +19 -0
  132. package/servers/dashboard/ui/src/components/ui/Icon.tsx +105 -0
  133. package/servers/dashboard/ui/src/components/ui/StatusBadge.tsx +19 -0
  134. package/servers/dashboard/ui/src/components/views/FilesView.tsx +23 -0
  135. package/servers/dashboard/ui/src/components/views/NetworkView.tsx +20 -0
  136. package/servers/dashboard/ui/src/components/views/Overview.tsx +135 -0
  137. package/servers/dashboard/ui/src/components/views/SessionsView.tsx +84 -0
  138. package/servers/dashboard/ui/src/components/views/SettingsView.tsx +138 -0
  139. package/servers/dashboard/ui/src/components/views/SkillsView.tsx +92 -0
  140. package/servers/dashboard/ui/src/components/views/TimelineView.tsx +46 -0
  141. package/servers/dashboard/ui/src/contexts/FilterContext.tsx +41 -0
  142. package/servers/dashboard/ui/src/contexts/PinnedSessionsContext.tsx +80 -0
  143. package/servers/dashboard/ui/src/contexts/SettingsContext.tsx +60 -0
  144. package/servers/dashboard/ui/src/hooks/useCommandPalette.ts +36 -0
  145. package/servers/dashboard/ui/src/hooks/useKeyboardShortcut.ts +38 -0
  146. package/servers/dashboard/ui/src/hooks/useNow.ts +12 -0
  147. package/servers/dashboard/ui/src/hooks/useReducedMotion.ts +15 -0
  148. package/servers/dashboard/ui/src/hooks/useSessions.ts +64 -0
  149. package/servers/dashboard/ui/src/hooks/useTheme.ts +30 -0
  150. package/servers/dashboard/ui/src/hooks/useViewport.ts +19 -0
  151. package/servers/dashboard/ui/src/hooks/useWebSocket.ts +118 -0
  152. package/servers/dashboard/ui/src/lib/export.test.ts +33 -0
  153. package/servers/dashboard/ui/src/lib/export.ts +39 -0
  154. package/servers/dashboard/ui/src/lib/filter.test.ts +95 -0
  155. package/servers/dashboard/ui/src/lib/filter.ts +178 -0
  156. package/servers/dashboard/ui/src/lib/format.test.ts +25 -0
  157. package/servers/dashboard/ui/src/lib/search.test.ts +52 -0
  158. package/servers/dashboard/ui/src/lib/search.ts +60 -0
  159. package/servers/dashboard/ui/src/lib/settings.test.ts +50 -0
  160. package/servers/dashboard/ui/src/lib/settings.ts +56 -0
  161. package/servers/dashboard/ui/src/lib/types.ts +124 -0
  162. package/servers/dashboard/ui/src/main.tsx +19 -0
  163. package/servers/dashboard/ui/src/styles/index.css +155 -0
  164. package/servers/dashboard/ui/tailwind.config.js +45 -0
  165. package/servers/dashboard/ui/tsconfig.json +33 -0
  166. package/servers/dashboard/ui/tsconfig.node.json +10 -0
  167. package/servers/dashboard/ui/vite.config.ts +37 -0
  168. package/servers/dashboard/ui/vitest.config.ts +8 -0
  169. package/skills/accessibility-auditor/SKILL.md +0 -20
  170. package/skills/architect/SKILL.md +0 -45
  171. package/skills/designer/SKILL.md +0 -30
  172. package/skills/docs-writer/SKILL.md +0 -13
  173. package/skills/engineer/SKILL.md +0 -30
  174. package/skills/maintainer/SKILL.md +0 -20
  175. package/skills/orchestrator/SKILL.md +0 -13
  176. package/skills/product-manager/SKILL.md +0 -20
  177. package/skills/researcher/SKILL.md +0 -20
  178. package/skills/reviewer/SKILL.md +0 -30
  179. package/skills/security-guard/SKILL.md +0 -20
  180. package/skills/shipper/SKILL.md +0 -33
  181. package/skills/tester/SKILL.md +0 -20
  182. package/packages/cli/dist/mcp.d.ts +0 -28
  183. package/packages/cli/dist/mcp.d.ts.map +0 -1
  184. package/packages/cli/dist/mcp.js +0 -148
  185. package/packages/cli/dist/mcp.js.map +0 -1
  186. package/packages/cli/dist/tests/mcp.test.d.ts +0 -2
  187. package/packages/cli/dist/tests/mcp.test.d.ts.map +0 -1
  188. package/packages/cli/dist/tests/mcp.test.js +0 -232
  189. package/packages/cli/dist/tests/mcp.test.js.map +0 -1
  190. package/references/obsidian-mcp-usage.md +0 -190
  191. package/servers/dashboard/public/app.js +0 -516
  192. package/servers/dashboard/public/index.html +0 -96
  193. package/servers/dashboard/public/styles.css +0 -819
  194. package/servers/obsidian-mcp/README.md +0 -82
  195. package/servers/obsidian-mcp/pyproject.toml +0 -32
  196. package/servers/obsidian-mcp/src/obsidian_mcp/__init__.py +0 -0
  197. package/servers/obsidian-mcp/src/obsidian_mcp/config.py +0 -47
  198. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/__init__.py +0 -0
  199. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/embeddings.py +0 -105
  200. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/indexer.py +0 -79
  201. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/store.py +0 -141
  202. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/sync.py +0 -37
  203. package/servers/obsidian-mcp/src/obsidian_mcp/learning/__init__.py +0 -0
  204. package/servers/obsidian-mcp/src/obsidian_mcp/learning/detector.py +0 -66
  205. package/servers/obsidian-mcp/src/obsidian_mcp/learning/note_generator.py +0 -40
  206. package/servers/obsidian-mcp/src/obsidian_mcp/main.py +0 -4
  207. package/servers/obsidian-mcp/src/obsidian_mcp/models.py +0 -42
  208. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/__init__.py +0 -0
  209. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/filter.py +0 -68
  210. package/servers/obsidian-mcp/src/obsidian_mcp/rag/__init__.py +0 -0
  211. package/servers/obsidian-mcp/src/obsidian_mcp/rag/engine.py +0 -50
  212. package/servers/obsidian-mcp/src/obsidian_mcp/rag/graph_search.py +0 -55
  213. package/servers/obsidian-mcp/src/obsidian_mcp/rag/text_search.py +0 -37
  214. package/servers/obsidian-mcp/src/obsidian_mcp/rag/vector_search.py +0 -118
  215. package/servers/obsidian-mcp/src/obsidian_mcp/server.py +0 -61
  216. package/servers/obsidian-mcp/src/obsidian_mcp/tools/__init__.py +0 -0
  217. package/servers/obsidian-mcp/src/obsidian_mcp/tools/create.py +0 -43
  218. package/servers/obsidian-mcp/src/obsidian_mcp/tools/delete.py +0 -16
  219. package/servers/obsidian-mcp/src/obsidian_mcp/tools/learn.py +0 -42
  220. package/servers/obsidian-mcp/src/obsidian_mcp/tools/list.py +0 -16
  221. package/servers/obsidian-mcp/src/obsidian_mcp/tools/read.py +0 -15
  222. package/servers/obsidian-mcp/src/obsidian_mcp/tools/registry.py +0 -130
  223. package/servers/obsidian-mcp/src/obsidian_mcp/tools/related.py +0 -20
  224. package/servers/obsidian-mcp/src/obsidian_mcp/tools/search.py +0 -26
  225. package/servers/obsidian-mcp/src/obsidian_mcp/tools/sync.py +0 -22
  226. package/servers/obsidian-mcp/src/obsidian_mcp/tools/update.py +0 -34
  227. package/servers/obsidian-mcp/src/obsidian_mcp/vault/__init__.py +0 -0
  228. package/servers/obsidian-mcp/src/obsidian_mcp/vault/parser.py +0 -82
  229. package/servers/obsidian-mcp/src/obsidian_mcp/vault/repository.py +0 -68
  230. package/servers/obsidian-mcp/src/obsidian_mcp/vault/writer.py +0 -61
  231. package/servers/obsidian-mcp/tests/conftest.py +0 -39
  232. package/servers/obsidian-mcp/tests/test_async_tools.py +0 -87
  233. package/servers/obsidian-mcp/tests/test_edge_cases.py +0 -59
  234. package/servers/obsidian-mcp/tests/test_indexer.py +0 -27
  235. package/servers/obsidian-mcp/tests/test_integration.py +0 -90
  236. package/servers/obsidian-mcp/tests/test_learning.py +0 -34
  237. package/servers/obsidian-mcp/tests/test_privacy.py +0 -31
  238. package/servers/obsidian-mcp/tests/test_privacy_config.py +0 -44
  239. package/servers/obsidian-mcp/tests/test_rag.py +0 -64
  240. package/servers/obsidian-mcp/tests/test_read_raw.py +0 -37
  241. package/servers/obsidian-mcp/tests/test_tfidf_fallback.py +0 -54
  242. package/servers/obsidian-mcp/tests/test_tools.py +0 -108
  243. package/servers/obsidian-mcp/tests/test_vault.py +0 -103
  244. package/servers/obsidian-mcp/tests/test_writer.py +0 -139
  245. package/skills/obsidian-second-brain/SKILL.md +0 -298
@@ -0,0 +1,178 @@
1
+ import type { AgentSource, ClientSession, EventStatus } from '../../../src/types';
2
+ import type { ToolInvocation } from '../../../src/lib/invocations';
3
+ import type { Graph3D, GraphNode, GraphLink } from '../../../src/lib/graph';
4
+ import { operationType } from '../../../src/lib/invocations';
5
+ import type { FilterOptions, FilterState, PinnedSession, TimeRange } from './types';
6
+ import { matchesInvocation } from './search';
7
+
8
+ function linkEndpointId(endpoint: string | { id?: unknown } | number): string {
9
+ if (typeof endpoint === 'string') return endpoint;
10
+ if (endpoint && typeof endpoint === 'object' && 'id' in endpoint) return String(endpoint.id);
11
+ return String(endpoint);
12
+ }
13
+
14
+ function timeRangeToMs(range: TimeRange): number | null {
15
+ switch (range) {
16
+ case '1m':
17
+ return 60_000;
18
+ case '5m':
19
+ return 5 * 60_000;
20
+ case '15m':
21
+ return 15 * 60_000;
22
+ case '1h':
23
+ return 60 * 60_000;
24
+ case '24h':
25
+ return 24 * 60 * 60_000;
26
+ default:
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function isWithinTimeRange(timestamp: number, range: TimeRange, now: number, session?: ClientSession): boolean {
32
+ if (range === 'all') return true;
33
+ if (range === 'session') {
34
+ if (!session) return true;
35
+ const start = session.startTime;
36
+ const end = session.endedAt || now;
37
+ return timestamp >= start && timestamp <= end;
38
+ }
39
+ const ms = timeRangeToMs(range);
40
+ if (ms == null) return true;
41
+ return timestamp >= now - ms;
42
+ }
43
+
44
+ function uniqueSorted<T extends string>(values: T[]): T[] {
45
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
46
+ }
47
+
48
+ export function buildOptions(
49
+ sessions: Map<string, ClientSession>,
50
+ selectedSessionId: string | null
51
+ ): FilterOptions {
52
+ const selected = selectedSessionId ? sessions.get(selectedSessionId) : undefined;
53
+ const relevant = selected ? [selected] : Array.from(sessions.values());
54
+
55
+ const sources = uniqueSorted<AgentSource>(relevant.map((s) => s.source));
56
+ const skills = uniqueSorted<string>(
57
+ relevant
58
+ .flatMap((s) => [s.activeSkill?.name, s.skill].filter(Boolean) as string[])
59
+ );
60
+ const statuses = uniqueSorted<EventStatus>(
61
+ relevant.map((s) => s.status).filter(Boolean) as EventStatus[]
62
+ );
63
+ const tools = uniqueSorted<string>(
64
+ relevant
65
+ .flatMap((s) => s.events)
66
+ .filter((e) => e.tool)
67
+ .map((e) => e.tool as string)
68
+ );
69
+ const opTypes = uniqueSorted<('read' | 'edit' | 'other')>(
70
+ tools.map((t) => operationType(t))
71
+ );
72
+
73
+ return { sources, skills, statuses, tools, opTypes };
74
+ }
75
+
76
+ export function filterInvocations(
77
+ invocations: ToolInvocation[],
78
+ session: ClientSession | undefined,
79
+ filters: FilterState,
80
+ now: number
81
+ ): ToolInvocation[] {
82
+ return invocations.filter((inv) => {
83
+ if (filters.query && !matchesInvocation(inv, filters.query)) return false;
84
+ if (filters.statuses.length > 0 && !filters.statuses.includes(inv.status as EventStatus)) return false;
85
+ if (filters.skills.length > 0 && !filters.skills.includes(inv.skill || '')) return false;
86
+ if (filters.tools.length > 0 && !filters.tools.includes(inv.tool)) return false;
87
+ if (filters.opTypes.length > 0 && !filters.opTypes.includes(operationType(inv.tool))) return false;
88
+ if (!isWithinTimeRange(inv.startTime, filters.timeRange, now, session)) return false;
89
+ return true;
90
+ });
91
+ }
92
+
93
+ export function filterSessions(
94
+ sessions: ClientSession[],
95
+ filters: FilterState,
96
+ pins: PinnedSession[],
97
+ now: number
98
+ ): ClientSession[] {
99
+ const pinOrder = new Map(pins.map((p, i) => [p.id, i]));
100
+
101
+ const filtered = sessions.filter((s) => {
102
+ if (filters.sources.length > 0 && !filters.sources.includes(s.source)) return false;
103
+ if (filters.skills.length > 0) {
104
+ const skill = s.activeSkill?.name || s.skill;
105
+ if (!skill || !filters.skills.includes(skill)) return false;
106
+ }
107
+ if (filters.statuses.length > 0 && !filters.statuses.includes(s.status || 'running')) return false;
108
+ if (!isWithinTimeRange(s.lastActivity, filters.timeRange, now, s)) return false;
109
+ return true;
110
+ });
111
+
112
+ return filtered.sort((a, b) => {
113
+ const aPin = pinOrder.has(a.id);
114
+ const bPin = pinOrder.has(b.id);
115
+ if (aPin && !bPin) return -1;
116
+ if (!aPin && bPin) return 1;
117
+ if (aPin && bPin) {
118
+ return (pinOrder.get(a.id) ?? 0) - (pinOrder.get(b.id) ?? 0);
119
+ }
120
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
121
+ });
122
+ }
123
+
124
+ export function filterGraph(
125
+ graph: Graph3D,
126
+ invocations: ToolInvocation[],
127
+ filters: FilterState
128
+ ): Graph3D {
129
+ const query = filters.query.trim().toLowerCase();
130
+ const toolSet = new Set(filters.tools);
131
+ const opSet = new Set(filters.opTypes);
132
+
133
+ const matchedTools = new Set<string>();
134
+ for (const inv of invocations) {
135
+ const keepTool =
136
+ (toolSet.size === 0 || toolSet.has(inv.tool)) &&
137
+ (opSet.size === 0 || opSet.has(operationType(inv.tool)));
138
+ if (keepTool) matchedTools.add(`tool:${inv.tool}`);
139
+ }
140
+
141
+ const skillNodes = graph.nodes.filter((n) => n.type === 'skill');
142
+
143
+ const keptNodes = new Map<string, GraphNode>();
144
+ for (const n of skillNodes) keptNodes.set(n.id, n);
145
+
146
+ const keptLinks: GraphLink[] = [];
147
+
148
+ for (const n of graph.nodes) {
149
+ if (n.type === 'tool') {
150
+ const matchesTool = matchedTools.has(n.id);
151
+ const matchesQuery = query ? n.label.toLowerCase().includes(query) : true;
152
+ if (matchesTool && matchesQuery) keptNodes.set(n.id, n);
153
+ }
154
+ }
155
+
156
+ for (const n of graph.nodes) {
157
+ if (n.type === 'file') {
158
+ const matchesQuery = query ? n.label.toLowerCase().includes(query) : true;
159
+ if (!matchesQuery) continue;
160
+ const hasToolLink = graph.links.some((l) => {
161
+ const sourceId = linkEndpointId(l.source);
162
+ const targetId = linkEndpointId(l.target);
163
+ return keptNodes.has(sourceId) && targetId === n.id;
164
+ });
165
+ if (hasToolLink) keptNodes.set(n.id, n);
166
+ }
167
+ }
168
+
169
+ for (const l of graph.links) {
170
+ const sourceId = linkEndpointId(l.source);
171
+ const targetId = linkEndpointId(l.target);
172
+ if (keptNodes.has(sourceId) && keptNodes.has(targetId)) {
173
+ keptLinks.push(l);
174
+ }
175
+ }
176
+
177
+ return { nodes: Array.from(keptNodes.values()), links: keptLinks };
178
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatDuration, formatTime, truncate, escapeHtml } from '../../../src/lib/format';
3
+
4
+ describe('format', () => {
5
+ it('formats duration', () => {
6
+ expect(formatDuration(0)).toBe('00:00');
7
+ expect(formatDuration(61000)).toBe('01:01');
8
+ expect(formatDuration(3661000)).toBe('1:01:01');
9
+ expect(formatDuration(undefined)).toBe('00:00');
10
+ });
11
+
12
+ it('formats time', () => {
13
+ const ts = new Date('2026-06-26T14:30:45').getTime();
14
+ expect(formatTime(ts)).toBe('14:30:45');
15
+ });
16
+
17
+ it('truncates strings', () => {
18
+ expect(truncate('hello', 10)).toBe('hello');
19
+ expect(truncate('hello world', 6)).toBe('hello…');
20
+ });
21
+
22
+ it('escapes html', () => {
23
+ expect(escapeHtml('<div>"x"</div>')).toBe('&lt;div&gt;&quot;x&quot;&lt;/div&gt;');
24
+ });
25
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { score, search, matchesInvocation } from './search';
3
+ import type { CommandPaletteItem, ToolInvocation } from './types';
4
+
5
+ function item(title: string, subtitle?: string, keywords?: string[]): CommandPaletteItem {
6
+ return { id: title, type: 'view', title, subtitle, keywords, action: () => {} };
7
+ }
8
+
9
+ describe('search', () => {
10
+ it('scores zero when no tokens match', () => {
11
+ expect(score(item('Timeline'), 'files')).toBe(0);
12
+ });
13
+
14
+ it('scores positive when all tokens match', () => {
15
+ expect(score(item('Session Overview', 'list all sessions'), 'session list')).toBe(2);
16
+ });
17
+
18
+ it('returns ranked results', () => {
19
+ const items = [item('Files'), item('Sessions'), item('Settings')];
20
+ expect(search(items, 'ses').map((i) => i.title)).toEqual(['Sessions']);
21
+ });
22
+
23
+ it('matches invocations by tool, detail, skill, and output', () => {
24
+ const inv: ToolInvocation = {
25
+ id: '1',
26
+ tool: 'Read',
27
+ eventType: 'tool_end',
28
+ status: 'success',
29
+ startTime: 0,
30
+ skill: 'engineer',
31
+ detail: 'opened readme',
32
+ output: { contentSnippet: 'hello world' },
33
+ };
34
+ expect(matchesInvocation(inv, 'Read')).toBe(true);
35
+ expect(matchesInvocation(inv, 'engineer')).toBe(true);
36
+ expect(matchesInvocation(inv, 'hello')).toBe(true);
37
+ expect(matchesInvocation(inv, 'missing')).toBe(false);
38
+ });
39
+
40
+ it('requires every token to match', () => {
41
+ const inv: ToolInvocation = {
42
+ id: '1',
43
+ tool: 'Edit',
44
+ eventType: 'tool_end',
45
+ status: 'success',
46
+ startTime: 0,
47
+ detail: 'updated config',
48
+ };
49
+ expect(matchesInvocation(inv, 'edit config')).toBe(true);
50
+ expect(matchesInvocation(inv, 'edit missing')).toBe(false);
51
+ });
52
+ });
@@ -0,0 +1,60 @@
1
+ import type { CommandPaletteItem, ToolInvocation } from './types';
2
+
3
+ function normalize(str: string): string {
4
+ return str
5
+ .toLowerCase()
6
+ .normalize('NFD')
7
+ .replace(/[\u0300-\u036f]/g, '');
8
+ }
9
+
10
+ function tokens(query: string): string[] {
11
+ return normalize(query)
12
+ .split(/\s+/)
13
+ .filter(Boolean);
14
+ }
15
+
16
+ function haystack(item: CommandPaletteItem): string {
17
+ const parts = [item.title, item.subtitle, ...(item.keywords || [])];
18
+ return normalize(parts.join(' '));
19
+ }
20
+
21
+ export function score(item: CommandPaletteItem, query: string): number {
22
+ const tks = tokens(query);
23
+ if (tks.length === 0) return 1;
24
+ const hay = haystack(item);
25
+ let total = 0;
26
+ for (const t of tks) {
27
+ if (hay.includes(t)) total += 1;
28
+ }
29
+ return total === tks.length ? total : 0;
30
+ }
31
+
32
+ export function search(items: CommandPaletteItem[], query: string): CommandPaletteItem[] {
33
+ const q = query.trim();
34
+ if (!q) return items.slice(0, 50);
35
+ const scored = items
36
+ .map((item) => ({ item, value: score(item, q) }))
37
+ .filter((s) => s.value > 0)
38
+ .sort((a, b) => b.value - a.value);
39
+ return scored.slice(0, 50).map((s) => s.item);
40
+ }
41
+
42
+ function invocationText(inv: ToolInvocation): string {
43
+ const parts = [
44
+ inv.tool,
45
+ inv.detail,
46
+ inv.skill,
47
+ inv.eventType,
48
+ inv.status,
49
+ inv.input ? JSON.stringify(inv.input) : '',
50
+ inv.output ? JSON.stringify(inv.output) : '',
51
+ ];
52
+ return normalize(parts.join(' '));
53
+ }
54
+
55
+ export function matchesInvocation(inv: ToolInvocation, query: string): boolean {
56
+ const tks = tokens(query);
57
+ if (tks.length === 0) return true;
58
+ const hay = invocationText(inv);
59
+ return tks.every((t) => hay.includes(t));
60
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { loadSettings, saveSettings, migrate } from './settings';
3
+ import { DEFAULT_SETTINGS } from './types';
4
+
5
+ const store: Record<string, string> = {};
6
+
7
+ beforeEach(() => {
8
+ Object.keys(store).forEach((k) => delete store[k]);
9
+ globalThis.localStorage = {
10
+ getItem: (key: string) => store[key] ?? null,
11
+ setItem: (key: string, value: string) => {
12
+ store[key] = value;
13
+ },
14
+ removeItem: (key: string) => {
15
+ delete store[key];
16
+ },
17
+ clear: () => {
18
+ Object.keys(store).forEach((k) => delete store[k]);
19
+ },
20
+ key: (index: number) => Object.keys(store)[index] ?? null,
21
+ length: 0,
22
+ } as Storage;
23
+ });
24
+
25
+ describe('settings', () => {
26
+ it('loads defaults when storage is empty', () => {
27
+ expect(loadSettings()).toEqual(DEFAULT_SETTINGS);
28
+ });
29
+
30
+ it('migrates partial settings with defaults', () => {
31
+ expect(migrate({ theme: 'dark' })).toEqual({ ...DEFAULT_SETTINGS, theme: 'dark' });
32
+ });
33
+
34
+ it('ignores invalid values during migration', () => {
35
+ expect(
36
+ migrate({ theme: 'purple', density: 'compact', reducedMotion: 'yes', maxEvents: -5 })
37
+ ).toEqual({ ...DEFAULT_SETTINGS, density: 'compact' });
38
+ });
39
+
40
+ it('saves and reloads settings', () => {
41
+ const next = { ...DEFAULT_SETTINGS, theme: 'light' as const, density: 'compact' as const };
42
+ saveSettings(next);
43
+ expect(loadSettings()).toEqual(next);
44
+ });
45
+
46
+ it('falls back to legacy crewloop-theme key', () => {
47
+ globalThis.localStorage.setItem('crewloop-theme', 'dark');
48
+ expect(loadSettings()).toEqual({ ...DEFAULT_SETTINGS, theme: 'dark' });
49
+ });
50
+ });
@@ -0,0 +1,56 @@
1
+ import type { DashboardSettings, Theme, Density } from './types';
2
+ import { DEFAULT_SETTINGS } from './types';
3
+
4
+ const STORAGE_KEY = 'crewloop-dashboard-settings';
5
+ const LEGACY_THEME_KEY = 'crewloop-theme';
6
+
7
+ function isTheme(value: unknown): value is Theme {
8
+ return value === 'dark' || value === 'light' || value === 'system';
9
+ }
10
+
11
+ function isDensity(value: unknown): value is Density {
12
+ return value === 'compact' || value === 'comfortable';
13
+ }
14
+
15
+ function isBoolean(value: unknown): value is boolean {
16
+ return typeof value === 'boolean';
17
+ }
18
+
19
+ function isPositiveInteger(value: unknown): value is number {
20
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 && Number.isInteger(value);
21
+ }
22
+
23
+ export function migrate(partial: unknown): DashboardSettings {
24
+ const next = { ...DEFAULT_SETTINGS };
25
+ if (partial && typeof partial === 'object') {
26
+ const p = partial as Record<string, unknown>;
27
+ if (isTheme(p.theme)) next.theme = p.theme;
28
+ if (isDensity(p.density)) next.density = p.density;
29
+ if (isBoolean(p.reducedMotion)) next.reducedMotion = p.reducedMotion;
30
+ if (isBoolean(p.autoFollowActive)) next.autoFollowActive = p.autoFollowActive;
31
+ if (isPositiveInteger(p.maxEvents)) next.maxEvents = p.maxEvents;
32
+ }
33
+ return next;
34
+ }
35
+
36
+ export function loadSettings(): DashboardSettings {
37
+ try {
38
+ const raw = localStorage.getItem(STORAGE_KEY);
39
+ if (raw) return migrate(JSON.parse(raw));
40
+ const legacy = localStorage.getItem(LEGACY_THEME_KEY) as Theme | null;
41
+ if (legacy && isTheme(legacy)) {
42
+ return { ...DEFAULT_SETTINGS, theme: legacy };
43
+ }
44
+ } catch {
45
+ // Ignore corrupted storage.
46
+ }
47
+ return { ...DEFAULT_SETTINGS };
48
+ }
49
+
50
+ export function saveSettings(settings: DashboardSettings): void {
51
+ try {
52
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
53
+ } catch {
54
+ // Ignore private-mode or quota errors.
55
+ }
56
+ }
@@ -0,0 +1,124 @@
1
+ import type { AgentSource, ClientSession, EventStatus } from '../../../src/types';
2
+ import type { Graph3D } from '../../../src/lib/graph';
3
+ import type { ToolInvocation } from '../../../src/lib/invocations';
4
+
5
+ export type { ToolInvocation };
6
+
7
+ export type View =
8
+ | 'overview'
9
+ | 'sessions'
10
+ | 'timeline'
11
+ | 'network'
12
+ | 'files'
13
+ | 'skills'
14
+ | 'settings';
15
+
16
+ export type Theme = 'dark' | 'light' | 'system';
17
+ export type Density = 'compact' | 'comfortable';
18
+
19
+ export type TimeRange =
20
+ | 'all'
21
+ | '1m'
22
+ | '5m'
23
+ | '15m'
24
+ | '1h'
25
+ | '24h'
26
+ | 'session';
27
+
28
+ export interface FilterState {
29
+ query: string;
30
+ sources: AgentSource[];
31
+ skills: string[];
32
+ statuses: EventStatus[];
33
+ tools: string[];
34
+ opTypes: ('read' | 'edit' | 'other')[];
35
+ timeRange: TimeRange;
36
+ }
37
+
38
+ export const DEFAULT_FILTER_STATE: FilterState = {
39
+ query: '',
40
+ sources: [],
41
+ skills: [],
42
+ statuses: [],
43
+ tools: [],
44
+ opTypes: [],
45
+ timeRange: 'all',
46
+ };
47
+
48
+ export interface DashboardSettings {
49
+ theme: Theme;
50
+ density: Density;
51
+ reducedMotion: boolean;
52
+ autoFollowActive: boolean;
53
+ maxEvents: number;
54
+ }
55
+
56
+ export const DEFAULT_SETTINGS: DashboardSettings = {
57
+ theme: 'system',
58
+ density: 'comfortable',
59
+ reducedMotion: false,
60
+ autoFollowActive: true,
61
+ maxEvents: 100,
62
+ };
63
+
64
+ export interface PinnedSession {
65
+ id: string;
66
+ pinnedAt: number;
67
+ }
68
+
69
+ export interface CommandPaletteItem {
70
+ id: string;
71
+ type: 'view' | 'session' | 'skill' | 'tool' | 'file' | 'event' | 'action';
72
+ title: string;
73
+ subtitle?: string;
74
+ icon?: string;
75
+ keywords?: string[];
76
+ action(): void;
77
+ }
78
+
79
+ export interface ExportableEvent {
80
+ id: string;
81
+ timestamp: number;
82
+ tool?: string;
83
+ eventType: string;
84
+ status: string;
85
+ skill?: string;
86
+ detail?: string;
87
+ path?: string;
88
+ durationMs?: number;
89
+ }
90
+
91
+ export interface FilterOptions {
92
+ sources: AgentSource[];
93
+ skills: string[];
94
+ statuses: EventStatus[];
95
+ tools: string[];
96
+ opTypes: ('read' | 'edit' | 'other')[];
97
+ }
98
+
99
+ export interface FilterEngine {
100
+ buildOptions(
101
+ sessions: Map<string, ClientSession>,
102
+ selectedSessionId: string | null
103
+ ): FilterOptions;
104
+
105
+ filterInvocations(
106
+ invocations: ToolInvocation[],
107
+ session: ClientSession | undefined,
108
+ filters: FilterState,
109
+ now: number
110
+ ): ToolInvocation[];
111
+
112
+ filterSessions(
113
+ sessions: ClientSession[],
114
+ filters: FilterState,
115
+ pins: PinnedSession[],
116
+ now: number
117
+ ): ClientSession[];
118
+
119
+ filterGraph(
120
+ graph: Graph3D,
121
+ invocations: ToolInvocation[],
122
+ filters: FilterState
123
+ ): Graph3D;
124
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import { SettingsProvider } from './contexts/SettingsContext';
5
+ import { PinnedSessionsProvider } from './contexts/PinnedSessionsContext';
6
+ import { FilterProvider } from './contexts/FilterContext';
7
+ import './styles/index.css';
8
+
9
+ ReactDOM.createRoot(document.getElementById('root')!).render(
10
+ <React.StrictMode>
11
+ <SettingsProvider>
12
+ <PinnedSessionsProvider>
13
+ <FilterProvider>
14
+ <App />
15
+ </FilterProvider>
16
+ </PinnedSessionsProvider>
17
+ </SettingsProvider>
18
+ </React.StrictMode>
19
+ );