@kb-labs/shared 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/workflows/ci.yml +13 -0
  3. package/.github/workflows/deploy.yml +28 -0
  4. package/.github/workflows/docker-build.yml +25 -0
  5. package/.github/workflows/drift-check.yml +10 -0
  6. package/.github/workflows/profiles-validate.yml +16 -0
  7. package/.github/workflows/release.yml +8 -0
  8. package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
  9. package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
  10. package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
  11. package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
  12. package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
  13. package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
  14. package/.kb/devkit/agents/release-manager/context.globs +7 -0
  15. package/.kb/devkit/agents/release-manager/prompt.md +27 -0
  16. package/.kb/devkit/agents/release-manager/runbook.md +17 -0
  17. package/.kb/devkit/agents/test-generator/context.globs +7 -0
  18. package/.kb/devkit/agents/test-generator/prompt.md +27 -0
  19. package/.kb/devkit/agents/test-generator/runbook.md +18 -0
  20. package/.vscode/settings.json +23 -0
  21. package/CHANGELOG.md +33 -0
  22. package/CONTRIBUTING.md +117 -0
  23. package/LICENSE +21 -0
  24. package/README.md +306 -0
  25. package/docs/DECLARATIVE-FLAGS-AND-ENV.md +622 -0
  26. package/docs/DOCUMENTATION.md +70 -0
  27. package/docs/adr/0000-template.md +52 -0
  28. package/docs/adr/0001-architecture-and-repository-layout.md +31 -0
  29. package/docs/adr/0002-plugins-and-extensibility.md +44 -0
  30. package/docs/adr/0003-package-and-module-boundaries.md +35 -0
  31. package/docs/adr/0004-versioning-and-release-policy.md +36 -0
  32. package/docs/adr/0005-reactive-loader-pattern.md +179 -0
  33. package/docs/adr/0006-declarative-flags-and-env-systems.md +376 -0
  34. package/eslint.config.js +27 -0
  35. package/kb-labs.config.json +5 -0
  36. package/package.json +88 -0
  37. package/package.json.bin +25 -0
  38. package/package.json.lib +30 -0
  39. package/packages/shared-cli-ui/CHANGELOG.md +20 -0
  40. package/packages/shared-cli-ui/README.md +342 -0
  41. package/packages/shared-cli-ui/docs/ARCHITECTURE.md +105 -0
  42. package/packages/shared-cli-ui/eslint.config.js +27 -0
  43. package/packages/shared-cli-ui/package.json +72 -0
  44. package/packages/shared-cli-ui/src/__tests__/artifacts-display.spec.ts +89 -0
  45. package/packages/shared-cli-ui/src/__tests__/format.spec.ts +44 -0
  46. package/packages/shared-cli-ui/src/__tests__/loader-json-mode.test.ts +119 -0
  47. package/packages/shared-cli-ui/src/artifacts-display.ts +266 -0
  48. package/packages/shared-cli-ui/src/cli-auto-discovery.ts +120 -0
  49. package/packages/shared-cli-ui/src/colors.ts +142 -0
  50. package/packages/shared-cli-ui/src/command-discovery.ts +72 -0
  51. package/packages/shared-cli-ui/src/command-output.ts +153 -0
  52. package/packages/shared-cli-ui/src/command-result.ts +267 -0
  53. package/packages/shared-cli-ui/src/command-runner.ts +310 -0
  54. package/packages/shared-cli-ui/src/command-suggestions.ts +204 -0
  55. package/packages/shared-cli-ui/src/debug/components/output.ts +141 -0
  56. package/packages/shared-cli-ui/src/debug/components/trace.ts +101 -0
  57. package/packages/shared-cli-ui/src/debug/components/tree.ts +88 -0
  58. package/packages/shared-cli-ui/src/debug/formatters/ai.ts +17 -0
  59. package/packages/shared-cli-ui/src/debug/formatters/human.ts +98 -0
  60. package/packages/shared-cli-ui/src/debug/formatters/timeline.ts +94 -0
  61. package/packages/shared-cli-ui/src/debug/index.ts +56 -0
  62. package/packages/shared-cli-ui/src/debug/types.ts +57 -0
  63. package/packages/shared-cli-ui/src/debug/utilities.ts +203 -0
  64. package/packages/shared-cli-ui/src/dynamic-command-discovery.ts +131 -0
  65. package/packages/shared-cli-ui/src/format.ts +412 -0
  66. package/packages/shared-cli-ui/src/index.ts +34 -0
  67. package/packages/shared-cli-ui/src/loader.ts +196 -0
  68. package/packages/shared-cli-ui/src/manifest-parser.ts +151 -0
  69. package/packages/shared-cli-ui/src/modern-format.ts +271 -0
  70. package/packages/shared-cli-ui/src/multi-cli-suggestions.ts +159 -0
  71. package/packages/shared-cli-ui/src/table.ts +134 -0
  72. package/packages/shared-cli-ui/src/timing-tracker.ts +68 -0
  73. package/packages/shared-cli-ui/src/utils/context.ts +12 -0
  74. package/packages/shared-cli-ui/src/utils/env.ts +164 -0
  75. package/packages/shared-cli-ui/src/utils/flags.ts +269 -0
  76. package/packages/shared-cli-ui/src/utils/path.ts +8 -0
  77. package/packages/shared-cli-ui/tsconfig.build.json +15 -0
  78. package/packages/shared-cli-ui/tsconfig.json +9 -0
  79. package/packages/shared-cli-ui/tsup.config.ts +11 -0
  80. package/packages/shared-cli-ui/vitest.config.ts +15 -0
  81. package/packages/shared-command-kit/CHANGELOG.md +20 -0
  82. package/packages/shared-command-kit/LICENSE +22 -0
  83. package/packages/shared-command-kit/README.md +1030 -0
  84. package/packages/shared-command-kit/docs/HIGH-LEVEL-API.md +89 -0
  85. package/packages/shared-command-kit/docs/LOW-LEVEL-API.md +105 -0
  86. package/packages/shared-command-kit/docs/MIGRATION-GUIDE.md +135 -0
  87. package/packages/shared-command-kit/eslint.config.js +27 -0
  88. package/packages/shared-command-kit/eslint.config.ts +14 -0
  89. package/packages/shared-command-kit/package.json +76 -0
  90. package/packages/shared-command-kit/prettierrc.json +5 -0
  91. package/packages/shared-command-kit/src/__tests__/define-command.spec.ts +294 -0
  92. package/packages/shared-command-kit/src/__tests__/define-route.test.ts +285 -0
  93. package/packages/shared-command-kit/src/__tests__/define-system-command.spec.ts +508 -0
  94. package/packages/shared-command-kit/src/__tests__/define-webhook.test.ts +156 -0
  95. package/packages/shared-command-kit/src/__tests__/define-websocket.test.ts +316 -0
  96. package/packages/shared-command-kit/src/__tests__/errors.spec.ts +45 -0
  97. package/packages/shared-command-kit/src/__tests__/flags.spec.ts +353 -0
  98. package/packages/shared-command-kit/src/__tests__/platform-api.test.ts +135 -0
  99. package/packages/shared-command-kit/src/__tests__/plugin-context-v3.snapshot.spec.ts +240 -0
  100. package/packages/shared-command-kit/src/__tests__/ws-types.test.ts +359 -0
  101. package/packages/shared-command-kit/src/analytics/index.ts +6 -0
  102. package/packages/shared-command-kit/src/analytics/with-analytics.ts +195 -0
  103. package/packages/shared-command-kit/src/define-action.ts +100 -0
  104. package/packages/shared-command-kit/src/define-command.ts +113 -0
  105. package/packages/shared-command-kit/src/define-route.ts +113 -0
  106. package/packages/shared-command-kit/src/define-system-command.ts +362 -0
  107. package/packages/shared-command-kit/src/define-webhook.ts +115 -0
  108. package/packages/shared-command-kit/src/define-websocket.ts +308 -0
  109. package/packages/shared-command-kit/src/errors/factory.ts +282 -0
  110. package/packages/shared-command-kit/src/errors/format-validation.ts +144 -0
  111. package/packages/shared-command-kit/src/errors/format.ts +92 -0
  112. package/packages/shared-command-kit/src/errors/index.ts +9 -0
  113. package/packages/shared-command-kit/src/errors/types.ts +32 -0
  114. package/packages/shared-command-kit/src/flags/define.ts +92 -0
  115. package/packages/shared-command-kit/src/flags/index.ts +9 -0
  116. package/packages/shared-command-kit/src/flags/types.ts +153 -0
  117. package/packages/shared-command-kit/src/flags/validate.ts +358 -0
  118. package/packages/shared-command-kit/src/helpers/context.ts +8 -0
  119. package/packages/shared-command-kit/src/helpers/flags.ts +84 -0
  120. package/packages/shared-command-kit/src/helpers/index.ts +42 -0
  121. package/packages/shared-command-kit/src/helpers/patterns.ts +464 -0
  122. package/packages/shared-command-kit/src/helpers/platform.ts +335 -0
  123. package/packages/shared-command-kit/src/helpers/use-analytics.ts +95 -0
  124. package/packages/shared-command-kit/src/helpers/use-cache.ts +97 -0
  125. package/packages/shared-command-kit/src/helpers/use-config.ts +99 -0
  126. package/packages/shared-command-kit/src/helpers/use-embeddings.ts +49 -0
  127. package/packages/shared-command-kit/src/helpers/use-llm.ts +316 -0
  128. package/packages/shared-command-kit/src/helpers/use-logger.ts +77 -0
  129. package/packages/shared-command-kit/src/helpers/use-platform.ts +111 -0
  130. package/packages/shared-command-kit/src/helpers/use-resource-broker.ts +106 -0
  131. package/packages/shared-command-kit/src/helpers/use-storage.ts +71 -0
  132. package/packages/shared-command-kit/src/helpers/use-vector-store.ts +49 -0
  133. package/packages/shared-command-kit/src/helpers/validation.ts +398 -0
  134. package/packages/shared-command-kit/src/index.ts +410 -0
  135. package/packages/shared-command-kit/src/jobs.ts +132 -0
  136. package/packages/shared-command-kit/src/lifecycle/define-handlers.ts +366 -0
  137. package/packages/shared-command-kit/src/lifecycle/index.ts +6 -0
  138. package/packages/shared-command-kit/src/manifest.ts +127 -0
  139. package/packages/shared-command-kit/src/rest/define-handler.ts +187 -0
  140. package/packages/shared-command-kit/src/rest/index.ts +11 -0
  141. package/packages/shared-command-kit/src/studio/index.ts +12 -0
  142. package/packages/shared-command-kit/src/validation/index.ts +6 -0
  143. package/packages/shared-command-kit/src/validation/schema-builders.ts +409 -0
  144. package/packages/shared-command-kit/src/ws-types.ts +106 -0
  145. package/packages/shared-command-kit/tsconfig.build.json +15 -0
  146. package/packages/shared-command-kit/tsconfig.json +9 -0
  147. package/packages/shared-command-kit/tsup.config.ts +30 -0
  148. package/packages/shared-command-kit/vitest.config.ts +4 -0
  149. package/packages/shared-http/package.json +67 -0
  150. package/packages/shared-http/src/__tests__/log-correlation.test.ts +81 -0
  151. package/packages/shared-http/src/__tests__/operation-metrics-tracker.test.ts +55 -0
  152. package/packages/shared-http/src/http-observability-collector.ts +363 -0
  153. package/packages/shared-http/src/index.ts +36 -0
  154. package/packages/shared-http/src/log-correlation.ts +89 -0
  155. package/packages/shared-http/src/operation-metrics-tracker.ts +107 -0
  156. package/packages/shared-http/src/register-openapi.ts +108 -0
  157. package/packages/shared-http/src/resolve-schema-ref.ts +75 -0
  158. package/packages/shared-http/src/schemas.ts +29 -0
  159. package/packages/shared-http/src/service-observability.ts +63 -0
  160. package/packages/shared-http/tsconfig.build.json +15 -0
  161. package/packages/shared-http/tsconfig.json +9 -0
  162. package/packages/shared-http/tsup.config.ts +23 -0
  163. package/packages/shared-http/vitest.config.ts +13 -0
  164. package/packages/shared-perm-presets/CHANGELOG.md +20 -0
  165. package/packages/shared-perm-presets/README.md +78 -0
  166. package/packages/shared-perm-presets/eslint.config.js +27 -0
  167. package/packages/shared-perm-presets/package.json +45 -0
  168. package/packages/shared-perm-presets/src/__tests__/combine.test.ts +403 -0
  169. package/packages/shared-perm-presets/src/__tests__/presets.test.ts +205 -0
  170. package/packages/shared-perm-presets/src/combine.ts +278 -0
  171. package/packages/shared-perm-presets/src/index.ts +18 -0
  172. package/packages/shared-perm-presets/src/presets/ci-environment.ts +34 -0
  173. package/packages/shared-perm-presets/src/presets/full-env.ts +16 -0
  174. package/packages/shared-perm-presets/src/presets/git-workflow.ts +40 -0
  175. package/packages/shared-perm-presets/src/presets/index.ts +8 -0
  176. package/packages/shared-perm-presets/src/presets/kb-platform.ts +30 -0
  177. package/packages/shared-perm-presets/src/presets/llm-access.ts +29 -0
  178. package/packages/shared-perm-presets/src/presets/minimal.ts +21 -0
  179. package/packages/shared-perm-presets/src/presets/npm-publish.ts +48 -0
  180. package/packages/shared-perm-presets/src/presets/vector-store.ts +40 -0
  181. package/packages/shared-perm-presets/src/types.ts +192 -0
  182. package/packages/shared-perm-presets/tsconfig.build.json +15 -0
  183. package/packages/shared-perm-presets/tsconfig.json +9 -0
  184. package/packages/shared-perm-presets/tsup.config.ts +8 -0
  185. package/packages/shared-perm-presets/vitest.config.ts +9 -0
  186. package/packages/shared-testing/CHANGELOG.md +20 -0
  187. package/packages/shared-testing/README.md +430 -0
  188. package/packages/shared-testing/package.json +51 -0
  189. package/packages/shared-testing/src/__tests__/create-test-context.test.ts +199 -0
  190. package/packages/shared-testing/src/__tests__/mock-cache.test.ts +174 -0
  191. package/packages/shared-testing/src/__tests__/mock-llm.test.ts +212 -0
  192. package/packages/shared-testing/src/__tests__/setup-platform.test.ts +90 -0
  193. package/packages/shared-testing/src/__tests__/test-command.test.ts +557 -0
  194. package/packages/shared-testing/src/create-test-context.ts +550 -0
  195. package/packages/shared-testing/src/index.ts +77 -0
  196. package/packages/shared-testing/src/mock-cache.ts +179 -0
  197. package/packages/shared-testing/src/mock-llm.ts +319 -0
  198. package/packages/shared-testing/src/mock-logger.ts +97 -0
  199. package/packages/shared-testing/src/mock-storage.ts +108 -0
  200. package/packages/shared-testing/src/setup-platform.ts +101 -0
  201. package/packages/shared-testing/src/test-command.ts +288 -0
  202. package/packages/shared-testing/tsconfig.build.json +15 -0
  203. package/packages/shared-testing/tsconfig.json +9 -0
  204. package/packages/shared-testing/tsup.config.ts +20 -0
  205. package/packages/shared-testing/vitest.config.ts +3 -0
  206. package/packages/shared-tool-kit/CHANGELOG.md +20 -0
  207. package/packages/shared-tool-kit/package.json +47 -0
  208. package/packages/shared-tool-kit/src/__tests__/factory.test.ts +103 -0
  209. package/packages/shared-tool-kit/src/__tests__/mock-tool.test.ts +95 -0
  210. package/packages/shared-tool-kit/src/factory.ts +126 -0
  211. package/packages/shared-tool-kit/src/index.ts +32 -0
  212. package/packages/shared-tool-kit/src/testing/index.ts +84 -0
  213. package/packages/shared-tool-kit/tsconfig.build.json +15 -0
  214. package/packages/shared-tool-kit/tsconfig.json +9 -0
  215. package/packages/shared-tool-kit/tsup.config.ts +21 -0
  216. package/pnpm-workspace.yaml +11070 -0
  217. package/prettierrc.json +1 -0
  218. package/scripts/devkit-sync.mjs +37 -0
  219. package/scripts/hooks/post-push +9 -0
  220. package/scripts/hooks/pre-commit +9 -0
  221. package/scripts/hooks/pre-push +9 -0
  222. package/tsconfig.base.json +9 -0
  223. package/tsconfig.build.json +15 -0
  224. package/tsconfig.json +9 -0
  225. package/tsconfig.paths.json +50 -0
  226. package/tsconfig.tools.json +18 -0
  227. package/tsup.config.bin.ts +34 -0
  228. package/tsup.config.cli.ts +41 -0
  229. package/tsup.config.dual.ts +46 -0
  230. package/tsup.config.ts +36 -0
  231. package/tsup.external.json +104 -0
  232. package/vitest.config.ts +48 -0
@@ -0,0 +1,359 @@
1
+ /**
2
+ * @file Unit tests for WebSocket message builders and routers
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { defineMessage, MessageBuilder, MessageRouter } from '../ws-types.js';
7
+ import type { WSMessage, WSSender } from '@kb-labs/plugin-contracts';
8
+
9
+ describe('MessageBuilder', () => {
10
+ describe('create', () => {
11
+ it('should create message with type and payload', () => {
12
+ const builder = new MessageBuilder<{ name: string }>('test');
13
+
14
+ const message = builder.create({ name: 'Alice' });
15
+
16
+ expect(message.type).toBe('test');
17
+ expect(message.payload).toEqual({ name: 'Alice' });
18
+ expect(message.timestamp).toBeGreaterThan(0);
19
+ });
20
+
21
+ it('should create message with messageId', () => {
22
+ const builder = new MessageBuilder<{ data: string }>('test');
23
+
24
+ const message = builder.create({ data: 'hello' }, 'msg-123');
25
+
26
+ expect(message.type).toBe('test');
27
+ expect(message.payload).toEqual({ data: 'hello' });
28
+ expect(message.messageId).toBe('msg-123');
29
+ expect(message.timestamp).toBeGreaterThan(0);
30
+ });
31
+
32
+ it('should create message without messageId', () => {
33
+ const builder = new MessageBuilder<object>('ping');
34
+
35
+ const message = builder.create({});
36
+
37
+ expect(message.type).toBe('ping');
38
+ expect(message.payload).toEqual({});
39
+ expect(message.messageId).toBeUndefined();
40
+ expect(message.timestamp).toBeGreaterThan(0);
41
+ });
42
+
43
+ it('should generate unique timestamps for sequential calls', () => {
44
+ const builder = new MessageBuilder<object>('test');
45
+
46
+ const msg1 = builder.create({});
47
+ const msg2 = builder.create({});
48
+
49
+ // Timestamps should be close but not necessarily different
50
+ // (depends on system clock resolution)
51
+ expect(msg1.timestamp).toBeLessThanOrEqual(msg2.timestamp);
52
+ });
53
+ });
54
+
55
+ describe('is', () => {
56
+ it('should return true for matching message type', () => {
57
+ const builder = new MessageBuilder<{ count: number }>('update');
58
+
59
+ const message: WSMessage = {
60
+ type: 'update',
61
+ payload: { count: 42 },
62
+ timestamp: Date.now(),
63
+ };
64
+
65
+ expect(builder.is(message)).toBe(true);
66
+ });
67
+
68
+ it('should return false for non-matching message type', () => {
69
+ const builder = new MessageBuilder<{ count: number }>('update');
70
+
71
+ const message: WSMessage = {
72
+ type: 'ping',
73
+ payload: {},
74
+ timestamp: Date.now(),
75
+ };
76
+
77
+ expect(builder.is(message)).toBe(false);
78
+ });
79
+
80
+ it('should narrow type when used in if statement', () => {
81
+ const UpdateMsg = new MessageBuilder<{ progress: number }>('update');
82
+
83
+ const message: WSMessage = {
84
+ type: 'update',
85
+ payload: { progress: 50 },
86
+ timestamp: Date.now(),
87
+ };
88
+
89
+ if (UpdateMsg.is(message)) {
90
+ // TypeScript should infer message.payload as { progress: number }
91
+ const progress: number = message.payload.progress;
92
+ expect(progress).toBe(50);
93
+ } else {
94
+ throw new Error('Should match');
95
+ }
96
+ });
97
+ });
98
+ });
99
+
100
+ describe('defineMessage', () => {
101
+ it('should create MessageBuilder instance', () => {
102
+ const PingMsg = defineMessage<object>('ping');
103
+
104
+ expect(PingMsg).toBeInstanceOf(MessageBuilder);
105
+ });
106
+
107
+ it('should create type-safe message builder', () => {
108
+ interface ProgressPayload {
109
+ phase: string;
110
+ progress: number;
111
+ }
112
+
113
+ const ProgressMsg = defineMessage<ProgressPayload>('progress');
114
+
115
+ const message = ProgressMsg.create({
116
+ phase: 'analyzing',
117
+ progress: 50,
118
+ });
119
+
120
+ expect(message.type).toBe('progress');
121
+ expect((message.payload as any).phase).toBe('analyzing');
122
+ expect((message.payload as any).progress).toBe(50);
123
+ });
124
+
125
+ it('should support different payload types', () => {
126
+ const StringMsg = defineMessage<string>('text');
127
+ const NumberMsg = defineMessage<number>('count');
128
+ const ObjectMsg = defineMessage<{ items: string[] }>('list');
129
+
130
+ const textMsg = StringMsg.create('hello');
131
+ const countMsg = NumberMsg.create(42);
132
+ const listMsg = ObjectMsg.create({ items: ['a', 'b'] });
133
+
134
+ expect(textMsg.payload).toBe('hello');
135
+ expect(countMsg.payload).toBe(42);
136
+ expect(listMsg.payload).toEqual({ items: ['a', 'b'] });
137
+ });
138
+ });
139
+
140
+ describe('MessageRouter', () => {
141
+ let mockContext: any;
142
+ let mockSender: WSSender;
143
+
144
+ beforeEach(() => {
145
+ mockContext = {
146
+ logger: {
147
+ info: vi.fn(),
148
+ error: vi.fn(),
149
+ },
150
+ };
151
+
152
+ mockSender = {
153
+ send: vi.fn(),
154
+ broadcast: vi.fn(),
155
+ sendTo: vi.fn(),
156
+ close: vi.fn(),
157
+ getConnectionId: vi.fn(() => 'conn-123'),
158
+ };
159
+ });
160
+
161
+ describe('on', () => {
162
+ it('should register message handler', () => {
163
+ const router = new MessageRouter();
164
+ const PingMsg = defineMessage<object>('ping');
165
+ const handler = vi.fn();
166
+
167
+ router.on(PingMsg, handler);
168
+
169
+ // Handler should be registered (tested via handle())
170
+ expect(router).toBeDefined();
171
+ });
172
+
173
+ it('should support chaining', () => {
174
+ const router = new MessageRouter();
175
+ const PingMsg = defineMessage<object>('ping');
176
+ const PongMsg = defineMessage<object>('pong');
177
+
178
+ const result = router.on(PingMsg, vi.fn()).on(PongMsg, vi.fn());
179
+
180
+ expect(result).toBe(router);
181
+ });
182
+
183
+ it('should register multiple handlers for different message types', () => {
184
+ const router = new MessageRouter();
185
+ const StartMsg = defineMessage<object>('start');
186
+ const StopMsg = defineMessage<object>('stop');
187
+
188
+ const startHandler = vi.fn();
189
+ const stopHandler = vi.fn();
190
+
191
+ router.on(StartMsg, startHandler).on(StopMsg, stopHandler);
192
+
193
+ // Handlers registered (tested via handle())
194
+ expect(router).toBeDefined();
195
+ });
196
+ });
197
+
198
+ describe('handle', () => {
199
+ it('should call handler for matching message type', async () => {
200
+ const router = new MessageRouter();
201
+ const PingMsg = defineMessage<object>('ping');
202
+ const handler = vi.fn();
203
+
204
+ router.on(PingMsg, handler);
205
+
206
+ const message: WSMessage = {
207
+ type: 'ping',
208
+ payload: {},
209
+ timestamp: Date.now(),
210
+ };
211
+
212
+ const handled = await router.handle(mockContext, message, mockSender);
213
+
214
+ expect(handled).toBe(true);
215
+ expect(handler).toHaveBeenCalledWith(mockContext, {}, mockSender);
216
+ });
217
+
218
+ it('should return false for unhandled message type', async () => {
219
+ const router = new MessageRouter();
220
+ const PingMsg = defineMessage<object>('ping');
221
+
222
+ router.on(PingMsg, vi.fn());
223
+
224
+ const message: WSMessage = {
225
+ type: 'unknown',
226
+ payload: {},
227
+ timestamp: Date.now(),
228
+ };
229
+
230
+ const handled = await router.handle(mockContext, message, mockSender);
231
+
232
+ expect(handled).toBe(false);
233
+ });
234
+
235
+ it('should pass payload to handler', async () => {
236
+ const router = new MessageRouter();
237
+ const UpdateMsg = defineMessage<{ progress: number }>('update');
238
+ const handler = vi.fn();
239
+
240
+ router.on(UpdateMsg, handler);
241
+
242
+ const message: WSMessage = {
243
+ type: 'update',
244
+ payload: { progress: 75 },
245
+ timestamp: Date.now(),
246
+ };
247
+
248
+ await router.handle(mockContext, message, mockSender);
249
+
250
+ expect(handler).toHaveBeenCalledWith(mockContext, { progress: 75 }, mockSender);
251
+ });
252
+
253
+ it('should handle async handlers', async () => {
254
+ const router = new MessageRouter();
255
+ const StartMsg = defineMessage<object>('start');
256
+
257
+ let resolved = false;
258
+ const asyncHandler = vi.fn(async () => {
259
+ await new Promise<void>((resolve) => { setTimeout(resolve, 10); });
260
+ resolved = true;
261
+ });
262
+
263
+ router.on(StartMsg, asyncHandler);
264
+
265
+ const message: WSMessage = {
266
+ type: 'start',
267
+ payload: {},
268
+ timestamp: Date.now(),
269
+ };
270
+
271
+ await router.handle(mockContext, message, mockSender);
272
+
273
+ expect(resolved).toBe(true);
274
+ expect(asyncHandler).toHaveBeenCalled();
275
+ });
276
+
277
+ it('should route to correct handler among multiple', async () => {
278
+ const router = new MessageRouter();
279
+ const PingMsg = defineMessage<object>('ping');
280
+ const PongMsg = defineMessage<object>('pong');
281
+ const StartMsg = defineMessage<object>('start');
282
+
283
+ const pingHandler = vi.fn();
284
+ const pongHandler = vi.fn();
285
+ const startHandler = vi.fn();
286
+
287
+ router.on(PingMsg, pingHandler).on(PongMsg, pongHandler).on(StartMsg, startHandler);
288
+
289
+ const pongMessage: WSMessage = {
290
+ type: 'pong',
291
+ payload: {},
292
+ timestamp: Date.now(),
293
+ };
294
+
295
+ await router.handle(mockContext, pongMessage, mockSender);
296
+
297
+ expect(pingHandler).not.toHaveBeenCalled();
298
+ expect(pongHandler).toHaveBeenCalled();
299
+ expect(startHandler).not.toHaveBeenCalled();
300
+ });
301
+
302
+ it('should propagate handler errors', async () => {
303
+ const router = new MessageRouter();
304
+ const ErrorMsg = defineMessage<object>('error');
305
+ const error = new Error('Handler failed');
306
+
307
+ router.on(ErrorMsg, async () => {
308
+ throw error;
309
+ });
310
+
311
+ const message: WSMessage = {
312
+ type: 'error',
313
+ payload: {},
314
+ timestamp: Date.now(),
315
+ };
316
+
317
+ await expect(router.handle(mockContext, message, mockSender)).rejects.toThrow(error);
318
+ });
319
+ });
320
+
321
+ describe('integration', () => {
322
+ it('should handle complete message flow', async () => {
323
+ const router = new MessageRouter();
324
+
325
+ // Define message types
326
+ const StartMsg = defineMessage<{ scope?: string }>('start');
327
+ const ProgressMsg = defineMessage<{ progress: number }>('progress');
328
+ const CompleteMsg = defineMessage<{ result: string }>('complete');
329
+
330
+ // Handlers
331
+ const startHandler = vi.fn(async (ctx, payload, sender) => {
332
+ await sender.send(ProgressMsg.create({ progress: 0 }));
333
+ });
334
+
335
+ const progressHandler = vi.fn();
336
+ const completeHandler = vi.fn();
337
+
338
+ router.on(StartMsg, startHandler).on(ProgressMsg, progressHandler).on(CompleteMsg, completeHandler);
339
+
340
+ // Handle start message
341
+ const startMessage: WSMessage = {
342
+ type: 'start',
343
+ payload: { scope: 'packages' },
344
+ timestamp: Date.now(),
345
+ };
346
+
347
+ const handled = await router.handle(mockContext, startMessage, mockSender);
348
+
349
+ expect(handled).toBe(true);
350
+ expect(startHandler).toHaveBeenCalledWith(mockContext, { scope: 'packages' }, mockSender);
351
+ expect(mockSender.send).toHaveBeenCalledWith(
352
+ expect.objectContaining({
353
+ type: 'progress',
354
+ payload: { progress: 0 },
355
+ })
356
+ );
357
+ });
358
+ });
359
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @module @kb-labs/shared-command-kit/analytics
3
+ * Analytics tracking helpers
4
+ */
5
+
6
+ export * from './with-analytics';
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Analytics Wrapper
3
+ *
4
+ * Optional helper for wrapping operations with analytics events.
5
+ * You can always use ctx.platform.analytics.track() directly - this is just convenience.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { withAnalytics } from '@kb-labs/shared-command-kit';
10
+ *
11
+ * const result = await withAnalytics(ctx, 'mind.query', {
12
+ * started: { queryId, text, mode },
13
+ * completed: (result) => ({ tokensIn: result.tokensIn, tokensOut: result.tokensOut }),
14
+ * failed: (error) => ({ errorMessage: error.message }),
15
+ * }, async () => {
16
+ * return await executeQuery(...);
17
+ * });
18
+ * ```
19
+ */
20
+
21
+ import type { IAnalytics } from '@kb-labs/core-platform';
22
+
23
+ /**
24
+ * Context with optional analytics
25
+ */
26
+ export interface AnalyticsContext {
27
+ platform?: {
28
+ analytics?: IAnalytics;
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Analytics event configuration
34
+ */
35
+ export interface AnalyticsEvents<TResult> {
36
+ /** Event properties when operation starts */
37
+ started?: Record<string, unknown>;
38
+ /** Event properties when operation completes (can be function of result) */
39
+ completed?: Record<string, unknown> | ((result: TResult) => Record<string, unknown>);
40
+ /** Event properties when operation fails (can be function of error) */
41
+ failed?: Record<string, unknown> | ((error: Error) => Record<string, unknown>);
42
+ }
43
+
44
+ /**
45
+ * Wrap an async operation with analytics tracking
46
+ *
47
+ * Automatically tracks:
48
+ * - `{eventName}.started` when operation begins
49
+ * - `{eventName}.completed` when operation succeeds
50
+ * - `{eventName}.failed` when operation fails
51
+ *
52
+ * @param ctx - Context with platform.analytics
53
+ * @param eventName - Base event name (e.g., 'mind.query', 'workflow.run')
54
+ * @param events - Event properties for started/completed/failed events
55
+ * @param operation - Async operation to execute
56
+ * @returns Result of the operation
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const result = await withAnalytics(ctx, 'mind.query', {
61
+ * started: { queryId: '123', text: 'search query' },
62
+ * completed: (result) => ({
63
+ * tokensIn: result.tokensIn,
64
+ * tokensOut: result.tokensOut,
65
+ * resultsCount: result.results.length,
66
+ * }),
67
+ * failed: (error) => ({
68
+ * errorCode: error.code,
69
+ * errorMessage: error.message,
70
+ * }),
71
+ * }, async () => {
72
+ * return await executeQuery('search query');
73
+ * });
74
+ * ```
75
+ */
76
+ export async function withAnalytics<TResult>(
77
+ ctx: AnalyticsContext,
78
+ eventName: string,
79
+ events: AnalyticsEvents<TResult>,
80
+ operation: () => Promise<TResult>
81
+ ): Promise<TResult> {
82
+ const analytics = ctx.platform?.analytics;
83
+ const startTime = Date.now();
84
+
85
+ // Track started event
86
+ if (analytics && events.started) {
87
+ await analytics.track(`${eventName}.started`, {
88
+ ...events.started,
89
+ timestamp: new Date().toISOString(),
90
+ });
91
+ }
92
+
93
+ try {
94
+ // Execute operation
95
+ const result = await operation();
96
+
97
+ // Track completed event
98
+ if (analytics && events.completed) {
99
+ const completedProps = typeof events.completed === 'function'
100
+ ? events.completed(result)
101
+ : events.completed;
102
+
103
+ await analytics.track(`${eventName}.completed`, {
104
+ ...completedProps,
105
+ durationMs: Date.now() - startTime,
106
+ timestamp: new Date().toISOString(),
107
+ });
108
+ }
109
+
110
+ return result;
111
+ } catch (error: any) {
112
+ // Track failed event
113
+ if (analytics && events.failed) {
114
+ const failedProps = typeof events.failed === 'function'
115
+ ? events.failed(error)
116
+ : events.failed;
117
+
118
+ await analytics.track(`${eventName}.failed`, {
119
+ ...failedProps,
120
+ durationMs: Date.now() - startTime,
121
+ timestamp: new Date().toISOString(),
122
+ });
123
+ }
124
+
125
+ // Re-throw error
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Create a reusable analytics wrapper for a specific event
132
+ *
133
+ * @param eventName - Base event name
134
+ * @param defaultEvents - Default event properties
135
+ * @returns Function that wraps operations with analytics
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * const trackQuery = createAnalyticsWrapper('mind.query', {
140
+ * started: { source: 'cli' },
141
+ * completed: (result) => ({ resultsCount: result.results.length }),
142
+ * });
143
+ *
144
+ * // Use multiple times
145
+ * const result1 = await trackQuery(ctx, { text: 'query 1' }, async () => {...});
146
+ * const result2 = await trackQuery(ctx, { text: 'query 2' }, async () => {...});
147
+ * ```
148
+ */
149
+ export function createAnalyticsWrapper<TResult>(
150
+ eventName: string,
151
+ defaultEvents: AnalyticsEvents<TResult>
152
+ ) {
153
+ return async (
154
+ ctx: AnalyticsContext,
155
+ additionalEvents: Partial<AnalyticsEvents<TResult>>,
156
+ operation: () => Promise<TResult>
157
+ ): Promise<TResult> => {
158
+ const mergedEvents: AnalyticsEvents<TResult> = {
159
+ started: { ...defaultEvents.started, ...additionalEvents.started },
160
+ completed: additionalEvents.completed || defaultEvents.completed,
161
+ failed: additionalEvents.failed || defaultEvents.failed,
162
+ };
163
+
164
+ return withAnalytics(ctx, eventName, mergedEvents, operation);
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Track a simple event (no wrapping)
170
+ *
171
+ * Convenience helper for tracking single events without wrapping an operation.
172
+ *
173
+ * @param ctx - Context with platform.analytics
174
+ * @param eventName - Event name
175
+ * @param properties - Event properties
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * await trackEvent(ctx, 'button.clicked', { buttonId: 'submit', page: 'settings' });
180
+ * ```
181
+ */
182
+ export async function trackEvent(
183
+ ctx: AnalyticsContext,
184
+ eventName: string,
185
+ properties?: Record<string, unknown>
186
+ ): Promise<void> {
187
+ const analytics = ctx.platform?.analytics;
188
+
189
+ if (analytics) {
190
+ await analytics.track(eventName, {
191
+ ...properties,
192
+ timestamp: new Date().toISOString(),
193
+ });
194
+ }
195
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Define a workflow action handler
3
+ */
4
+
5
+ import type { PluginContextV3, CommandResult, HostContext } from '@kb-labs/plugin-contracts';
6
+
7
+ export interface ActionHandler<TConfig = unknown, TInput = unknown> {
8
+ /**
9
+ * Execute the workflow action
10
+ */
11
+ execute(
12
+ context: PluginContextV3<TConfig>,
13
+ input: TInput
14
+ ): Promise<CommandResult | void> | CommandResult | void;
15
+
16
+ /**
17
+ * Optional cleanup - called after execute completes
18
+ */
19
+ cleanup?(): Promise<void> | void;
20
+ }
21
+
22
+ export interface ActionDefinition<TConfig = unknown, TInput = unknown> {
23
+ /**
24
+ * Action ID (e.g., "send-email")
25
+ */
26
+ id: string;
27
+
28
+ /**
29
+ * Action description
30
+ */
31
+ description?: string;
32
+
33
+ /**
34
+ * Handler implementation
35
+ */
36
+ handler: ActionHandler<TConfig, TInput>;
37
+
38
+ /**
39
+ * Optional input schema validation (future: use Zod/JSON Schema)
40
+ */
41
+ schema?: unknown;
42
+ }
43
+
44
+ /**
45
+ * Define a workflow action
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * export default defineAction({
50
+ * id: 'send-email',
51
+ * description: 'Send an email notification',
52
+ * handler: {
53
+ * async execute(context, input: { to: string; subject: string; body: string }) {
54
+ * // Access workflow context
55
+ * const { workflowId, runId, stepId } = context.hostContext as WorkflowHost;
56
+ *
57
+ * context.ui.info(`Sending email in workflow ${workflowId}`);
58
+ *
59
+ * // Send email...
60
+ *
61
+ * return {
62
+ * data: { sent: true, messageId: '123' },
63
+ * exitCode: 0,
64
+ * };
65
+ * }
66
+ * }
67
+ * });
68
+ * ```
69
+ */
70
+ export function defineAction<TConfig = unknown, TInput = unknown>(
71
+ definition: ActionDefinition<TConfig, TInput>
72
+ ): ActionHandler<TConfig, TInput> {
73
+ // Validate host type at runtime
74
+ const wrappedHandler: ActionHandler<TConfig, TInput> = {
75
+ execute: (context, input) => {
76
+ // Ensure we're running in workflow host
77
+ if (context.host !== 'workflow') {
78
+ throw new Error(
79
+ `Action ${definition.id} can only run in workflow host (current: ${context.host})`
80
+ );
81
+ }
82
+
83
+ // Call the actual handler
84
+ return definition.handler.execute(context, input);
85
+ },
86
+
87
+ cleanup: definition.handler.cleanup,
88
+ };
89
+
90
+ return wrappedHandler;
91
+ }
92
+
93
+ /**
94
+ * Type guard to check if host context is workflow
95
+ */
96
+ export function isWorkflowHost(
97
+ hostContext: HostContext
98
+ ): hostContext is Extract<HostContext, { host: 'workflow' }> {
99
+ return hostContext.host === 'workflow';
100
+ }