@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,44 @@
1
+ export function formatDuration(ms: number | undefined | null): string {
2
+ if (!ms || ms < 0) return '00:00';
3
+ const totalSeconds = Math.floor(ms / 1000);
4
+ const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
5
+ const seconds = (totalSeconds % 60).toString().padStart(2, '0');
6
+ if (totalSeconds < 3600) return `${minutes}:${seconds}`;
7
+ const hours = Math.floor(totalSeconds / 3600);
8
+ const mins = Math.floor((totalSeconds % 3600) / 60)
9
+ .toString()
10
+ .padStart(2, '0');
11
+ return `${hours}:${mins}:${seconds}`;
12
+ }
13
+
14
+ export function formatTime(ts: number): string {
15
+ const d = new Date(ts);
16
+ return d.toLocaleTimeString(undefined, {
17
+ hour12: false,
18
+ hour: '2-digit',
19
+ minute: '2-digit',
20
+ second: '2-digit',
21
+ });
22
+ }
23
+
24
+ export function truncate(str: string | undefined | null, n: number): string {
25
+ if (!str) return '';
26
+ return str.length > n ? str.slice(0, n - 1) + '…' : str;
27
+ }
28
+
29
+ export function escapeHtml(str: unknown): string {
30
+ return String(str)
31
+ .replace(/&/g, '&amp;')
32
+ .replace(/</g, '&lt;')
33
+ .replace(/>/g, '&gt;')
34
+ .replace(/"/g, '&quot;')
35
+ .replace(/'/g, '&#039;');
36
+ }
37
+
38
+ export function prettyJson(obj: unknown): string {
39
+ try {
40
+ return JSON.stringify(obj, null, 2);
41
+ } catch {
42
+ return String(obj);
43
+ }
44
+ }
@@ -0,0 +1,69 @@
1
+ import type { ClientSession } from '../types';
2
+ import type { ToolInvocation } from './invocations';
3
+ import { resolvePath } from './paths';
4
+
5
+ export interface GraphNode {
6
+ id: string;
7
+ type: 'skill' | 'tool' | 'file';
8
+ label: string;
9
+ weight: number;
10
+ x?: number;
11
+ y?: number;
12
+ z?: number;
13
+ }
14
+
15
+ export interface GraphLink {
16
+ source: string;
17
+ target: string;
18
+ weight: number;
19
+ }
20
+
21
+ export interface Graph3D {
22
+ nodes: GraphNode[];
23
+ links: GraphLink[];
24
+ }
25
+
26
+ export function buildGraph3D(session: ClientSession | undefined, invocations: ToolInvocation[]): Graph3D {
27
+ const nodes = new Map<string, GraphNode>();
28
+ const links: GraphLink[] = [];
29
+
30
+ const skillName = session?.activeSkill?.name || session?.skill || 'unknown';
31
+ const skillId = `skill:${skillName}`;
32
+ nodes.set(skillId, { id: skillId, type: 'skill', label: skillName, weight: 1 });
33
+
34
+ function ensureNode(id: string, type: GraphNode['type'], label: string, weight = 0): GraphNode {
35
+ const existing = nodes.get(id);
36
+ if (existing) {
37
+ existing.weight += weight;
38
+ return existing;
39
+ }
40
+ const node: GraphNode = { id, type, label, weight };
41
+ nodes.set(id, node);
42
+ return node;
43
+ }
44
+
45
+ function addLink(sourceId: string, targetId: string, weight = 1) {
46
+ const existing = links.find((l) => l.source === sourceId && l.target === targetId);
47
+ if (existing) {
48
+ existing.weight += weight;
49
+ } else {
50
+ links.push({ source: sourceId, target: targetId, weight });
51
+ }
52
+ }
53
+
54
+ for (const inv of invocations) {
55
+ if (inv.meta) continue;
56
+ const toolId = `tool:${inv.tool}`;
57
+ ensureNode(toolId, 'tool', inv.tool, 1);
58
+ addLink(skillId, toolId, 1);
59
+
60
+ const path = resolvePath(inv.input, inv.output);
61
+ if (path) {
62
+ const fileId = `file:${path}`;
63
+ ensureNode(fileId, 'file', path, 1);
64
+ addLink(toolId, fileId, 1);
65
+ }
66
+ }
67
+
68
+ return { nodes: Array.from(nodes.values()), links };
69
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { projectInvocations } from './invocations';
4
+ import type { ClientEvent } from '../types';
5
+
6
+ function makeEvent(overrides: Partial<ClientEvent> & { id: string }): ClientEvent {
7
+ return {
8
+ timestamp: Date.now(),
9
+ event_type: 'tool_start',
10
+ ...overrides,
11
+ } as ClientEvent;
12
+ }
13
+
14
+ describe('projectInvocations', () => {
15
+ it('pairs tool_start and tool_end by id when tool names differ or are missing', () => {
16
+ const start = makeEvent({
17
+ id: 'agy:conv-1:0',
18
+ event_type: 'tool_start',
19
+ tool: 'Bash',
20
+ detail: 'git status',
21
+ });
22
+ const end = makeEvent({
23
+ id: 'agy:conv-1:0',
24
+ event_type: 'tool_end',
25
+ status: 'success',
26
+ output: { result: 'ok' },
27
+ });
28
+
29
+ const invocations = projectInvocations([end, start]);
30
+ assert.strictEqual(invocations.length, 1);
31
+ assert.strictEqual(invocations[0].tool, 'Bash');
32
+ assert.strictEqual(invocations[0].status, 'success');
33
+ assert.strictEqual(invocations[0].detail, 'git status');
34
+ assert.deepStrictEqual(invocations[0].output, { result: 'ok' });
35
+ });
36
+
37
+ it('falls back to tool-name pairing when ids do not match', () => {
38
+ const start = makeEvent({
39
+ id: 'kimi-1',
40
+ event_type: 'tool_start',
41
+ tool: 'Read',
42
+ detail: '/tmp/foo.txt',
43
+ });
44
+ const end = makeEvent({
45
+ id: 'kimi-2',
46
+ event_type: 'tool_end',
47
+ tool: 'Read',
48
+ status: 'success',
49
+ });
50
+
51
+ const invocations = projectInvocations([end, start]);
52
+ assert.strictEqual(invocations.length, 1);
53
+ assert.strictEqual(invocations[0].tool, 'Read');
54
+ assert.strictEqual(invocations[0].status, 'success');
55
+ });
56
+
57
+ it('renders orphaned tool_end when no matching start exists', () => {
58
+ const end = makeEvent({
59
+ id: 'agy:conv-1:5',
60
+ event_type: 'tool_end',
61
+ status: 'error',
62
+ output: { error: 'failed' },
63
+ });
64
+
65
+ const invocations = projectInvocations([end]);
66
+ assert.strictEqual(invocations.length, 1);
67
+ assert.strictEqual(invocations[0].eventType, 'tool_end');
68
+ assert.strictEqual(invocations[0].status, 'error');
69
+ });
70
+ });
@@ -0,0 +1,172 @@
1
+ import type { ClientEvent } from '../types';
2
+
3
+ const MAX_EVENTS = 100;
4
+
5
+ export interface ToolInvocation {
6
+ id: string;
7
+ tool: string;
8
+ eventType: string;
9
+ status: 'running' | 'success' | 'error' | string;
10
+ startTime: number;
11
+ endTime?: number;
12
+ durationMs?: number;
13
+ detail?: string;
14
+ skill?: string;
15
+ input?: Record<string, unknown>;
16
+ output?: Record<string, unknown>;
17
+ meta?: boolean;
18
+ }
19
+
20
+ export interface FileOp {
21
+ id: string;
22
+ type: 'read' | 'edit' | 'other';
23
+ status: string;
24
+ timestamp: number;
25
+ tool: string;
26
+ snippet?: string;
27
+ }
28
+
29
+ export interface FileEntry {
30
+ path: string;
31
+ ops: FileOp[];
32
+ snippet?: string;
33
+ }
34
+
35
+ function statusFromEvent(ev: ClientEvent): string {
36
+ return ev.status || (ev.event_type && ev.event_type.endsWith('_end') ? 'success' : 'meta');
37
+ }
38
+
39
+ export function operationType(tool: string): 'read' | 'edit' | 'other' {
40
+ const t = tool.toLowerCase();
41
+ if (t === 'read') return 'read';
42
+ if (['write', 'edit', 'editfile'].includes(t)) return 'edit';
43
+ return 'other';
44
+ }
45
+
46
+ export function projectInvocations(events: ClientEvent[]): ToolInvocation[] {
47
+ const chronological = events.slice().reverse();
48
+ const invocations: ToolInvocation[] = [];
49
+ const runningById = new Map<string, ToolInvocation>();
50
+ const runningByTool = new Map<string, ToolInvocation[]>();
51
+
52
+ for (const ev of chronological) {
53
+ const tool = ev.tool || ev.event_type;
54
+ const status = statusFromEvent(ev);
55
+
56
+ if (ev.event_type === 'tool_start' && ev.tool) {
57
+ const inv: ToolInvocation = {
58
+ id: ev.id,
59
+ tool: ev.tool,
60
+ eventType: ev.event_type,
61
+ status: 'running',
62
+ startTime: ev.timestamp,
63
+ detail: ev.detail,
64
+ skill: ev.skill,
65
+ input: ev.input,
66
+ output: undefined,
67
+ };
68
+ invocations.push(inv);
69
+ runningById.set(ev.id, inv);
70
+ if (!runningByTool.has(ev.tool)) runningByTool.set(ev.tool, []);
71
+ runningByTool.get(ev.tool)!.push(inv);
72
+ continue;
73
+ }
74
+
75
+ if (ev.event_type === 'tool_end') {
76
+ let inv: ToolInvocation | undefined;
77
+
78
+ if (runningById.has(ev.id)) {
79
+ inv = runningById.get(ev.id);
80
+ runningById.delete(ev.id);
81
+ const stack = runningByTool.get(inv!.tool);
82
+ if (stack) {
83
+ const idx = stack.lastIndexOf(inv!);
84
+ if (idx !== -1) stack.splice(idx, 1);
85
+ }
86
+ } else if (ev.tool) {
87
+ const stack = runningByTool.get(ev.tool);
88
+ if (stack && stack.length) {
89
+ inv = stack.pop();
90
+ }
91
+ }
92
+
93
+ if (inv) {
94
+ inv.status = status;
95
+ inv.endTime = ev.timestamp;
96
+ inv.durationMs = ev.duration_ms;
97
+ inv.output = ev.output;
98
+ inv.detail = ev.detail || inv.detail;
99
+ continue;
100
+ }
101
+
102
+ invocations.push({
103
+ id: ev.id,
104
+ tool: ev.tool || ev.event_type,
105
+ eventType: ev.event_type,
106
+ status,
107
+ startTime: ev.timestamp,
108
+ endTime: ev.timestamp,
109
+ durationMs: ev.duration_ms,
110
+ detail: ev.detail,
111
+ skill: ev.skill,
112
+ input: ev.input,
113
+ output: ev.output,
114
+ });
115
+ continue;
116
+ }
117
+
118
+ invocations.push({
119
+ id: ev.id,
120
+ tool,
121
+ eventType: ev.event_type,
122
+ status,
123
+ startTime: ev.timestamp,
124
+ endTime: ev.timestamp,
125
+ durationMs: ev.duration_ms,
126
+ detail: ev.detail,
127
+ skill: ev.skill,
128
+ input: ev.input,
129
+ output: ev.output,
130
+ meta: true,
131
+ });
132
+ }
133
+
134
+ const recent = invocations.slice(-MAX_EVENTS);
135
+ recent.reverse();
136
+ return recent;
137
+ }
138
+
139
+ export function buildFileActivity(
140
+ invocations: ToolInvocation[],
141
+ resolvePathFn: (input?: unknown, output?: unknown) => string | undefined
142
+ ): FileEntry[] {
143
+ const files = new Map<string, FileEntry>();
144
+
145
+ for (const inv of invocations) {
146
+ const path = resolvePathFn(inv.input, inv.output);
147
+ if (!path) continue;
148
+
149
+ if (!files.has(path)) {
150
+ files.set(path, { path, ops: [] });
151
+ }
152
+ const entry = files.get(path)!;
153
+ entry.ops.push({
154
+ id: inv.id,
155
+ type: operationType(inv.tool),
156
+ status: inv.status,
157
+ timestamp: inv.startTime,
158
+ tool: inv.tool,
159
+ snippet: (inv.output?.diff as string | undefined) || (inv.output?.contentSnippet as string | undefined),
160
+ });
161
+ }
162
+
163
+ return Array.from(files.values()).map((entry) => {
164
+ entry.ops.sort((a, b) => a.timestamp - b.timestamp);
165
+ let snippet: string | undefined;
166
+ for (const op of entry.ops) {
167
+ if (op.snippet) snippet = op.snippet;
168
+ }
169
+ entry.snippet = snippet;
170
+ return entry;
171
+ });
172
+ }
@@ -0,0 +1,35 @@
1
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+
5
+ function firstOperationPath(operations: unknown): string | undefined {
6
+ if (!Array.isArray(operations)) return undefined;
7
+ for (const op of operations) {
8
+ if (isPlainObject(op)) {
9
+ const p = op.path ?? op.file_path;
10
+ if (typeof p === 'string') return p;
11
+ }
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ export function resolvePath(input?: unknown, output?: unknown): string | undefined {
17
+ const candidates = [
18
+ isPlainObject(input) ? input.path : undefined,
19
+ isPlainObject(input) ? input.file_path : undefined,
20
+ isPlainObject(input) ? input.filePath : undefined,
21
+ isPlainObject(input) && isPlainObject(input.args) ? input.args.path : undefined,
22
+ isPlainObject(input) && isPlainObject(input.args) ? input.args.file_path : undefined,
23
+ isPlainObject(input) && isPlainObject(input.args) ? input.args.filePath : undefined,
24
+ isPlainObject(input) ? firstOperationPath(input.operations) : undefined,
25
+ isPlainObject(output) ? output.path : undefined,
26
+ isPlainObject(output) ? output.file_path : undefined,
27
+ isPlainObject(output) && isPlainObject(output.args) ? output.args.path : undefined,
28
+ isPlainObject(output) && isPlainObject(output.args) ? output.args.file_path : undefined,
29
+ isPlainObject(output) ? firstOperationPath(output.operations) : undefined,
30
+ ];
31
+ for (const p of candidates) {
32
+ if (typeof p === 'string') return p;
33
+ }
34
+ return undefined;
35
+ }
@@ -10,6 +10,8 @@ export function presentEvent(event: Session['events'][number]): ClientEvent {
10
10
  status: event.status,
11
11
  duration_ms: event.duration_ms,
12
12
  skill: event.skill,
13
+ input: event.input,
14
+ output: event.output,
13
15
  };
14
16
  }
15
17
 
@@ -127,7 +127,7 @@ export function createDashboardServer(config: ServerConfig): DashboardServer {
127
127
  filePath: string,
128
128
  contentType: string
129
129
  ): void {
130
- const publicDir = path.resolve(__dirname, '..', 'public');
130
+ const publicDir = path.resolve(__dirname, '..', 'dist', 'public');
131
131
  const fullPath = path.resolve(publicDir, filePath);
132
132
  if (!fullPath.startsWith(publicDir + path.sep)) {
133
133
  res.statusCode = 403;
@@ -181,6 +181,10 @@ export function createDashboardServer(config: ServerConfig): DashboardServer {
181
181
  httpServer.listen(config.port, config.host, () => {
182
182
  httpServer.off('error', onError);
183
183
  wss.off('error', onError);
184
+ const uiIndex = path.resolve(__dirname, '..', 'dist', 'public', 'index.html');
185
+ if (!fs.existsSync(uiIndex)) {
186
+ console.warn('Warning: built UI not found at dist/public/index.html. Run `npm run build` first.');
187
+ }
184
188
  console.log(`CrewLoop dashboard running at http://${config.host}:${config.port}`);
185
189
  resolve();
186
190
  });
@@ -52,12 +52,20 @@ describe('SkillInferenceEngine', () => {
52
52
  assert.equal(result.confidence, 'explicit');
53
53
  });
54
54
 
55
- it('infers researcher from Read tool', () => {
55
+ it('returns unknown for generic Read tool', () => {
56
56
  const engine = new SkillInferenceEngine(skills);
57
57
  const event = makeEvent({ tool: 'Read', detail: 'README.md' });
58
58
  const result = engine.infer(event, makeSession());
59
- assert.equal(result.skill, 'researcher');
60
- assert.equal(result.confidence, 'heuristic');
59
+ assert.equal(result.skill, undefined);
60
+ assert.equal(result.confidence, 'unknown');
61
+ });
62
+
63
+ it('returns unknown for generic Bash tool', () => {
64
+ const engine = new SkillInferenceEngine(skills);
65
+ const event = makeEvent({ tool: 'Bash', detail: 'ls -la' });
66
+ const result = engine.infer(event, makeSession());
67
+ assert.equal(result.skill, undefined);
68
+ assert.equal(result.confidence, 'unknown');
61
69
  });
62
70
 
63
71
  it('infers shipper from git commit command', () => {
@@ -1,5 +1,5 @@
1
1
  import type { DashboardEvent, Session, SkillInferenceResult, SkillMeta } from '../types';
2
- import { inferFromTool, inferFromGitCommand } from './mapping';
2
+ import { inferFromGitCommand } from './mapping';
3
3
 
4
4
  export class SkillInferenceEngine {
5
5
  private skillNames: Set<string>;
@@ -35,13 +35,6 @@ export class SkillInferenceEngine {
35
35
  }
36
36
  }
37
37
 
38
- if (event.tool) {
39
- const mapped = inferFromTool(event.tool);
40
- if (mapped && this.skillNames.has(mapped)) {
41
- return { skill: mapped, confidence: 'heuristic' };
42
- }
43
- }
44
-
45
38
  if (session.active_skill && this.skillNames.has(session.active_skill)) {
46
39
  return { skill: session.active_skill, confidence: 'heuristic' };
47
40
  }
@@ -1,23 +1,3 @@
1
- import type { ToolToSkillMap } from '../types';
2
-
3
- export const DEFAULT_TOOL_TO_SKILL_MAP: ToolToSkillMap = {
4
- Task: 'orchestrator',
5
- Agent: 'orchestrator',
6
- Read: 'researcher',
7
- Grep: 'researcher',
8
- Glob: 'researcher',
9
- WebSearch: 'researcher',
10
- FetchURL: 'researcher',
11
- Edit: 'engineer',
12
- Write: 'engineer',
13
- Bash: 'engineer',
14
- Skill: undefined,
15
- };
16
-
17
- export function inferFromTool(toolName: string): string | undefined {
18
- return DEFAULT_TOOL_TO_SKILL_MAP[toolName];
19
- }
20
-
21
1
  export function inferFromGitCommand(command: string): string | undefined {
22
2
  if (/\b(git\s+(commit|push|branch|merge|tag|checkout))\b/.test(command)) {
23
3
  return 'shipper';
@@ -16,7 +16,6 @@ const SKILL_ICONS: Record<string, string> = {
16
16
  researcher: 'microscope',
17
17
  'security-guard': 'shield',
18
18
  'accessibility-auditor': 'person',
19
- 'obsidian-second-brain': 'brain',
20
19
  };
21
20
 
22
21
  export class SkillRegistry {