@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,308 @@
1
+ /**
2
+ * Define a WebSocket channel handler with enhanced DX
3
+ *
4
+ * Provides type-safe WebSocket handlers following the same pattern as defineCommand, defineRoute, etc.
5
+ */
6
+
7
+ import type { PluginContextV3, CommandResult, HostContext } from '@kb-labs/plugin-contracts';
8
+ import type { WSSender, WSInput, WSMessage } from '@kb-labs/plugin-contracts';
9
+
10
+ /**
11
+ * Validate that a message conforms to WSMessage structure
12
+ * @throws {Error} if message is invalid
13
+ */
14
+ function validateWSMessage(message: unknown): asserts message is WSMessage {
15
+ if (!message || typeof message !== 'object') {
16
+ throw new Error('Invalid WebSocket message: must be an object');
17
+ }
18
+
19
+ const msg = message as Record<string, unknown>;
20
+
21
+ if (typeof msg.type !== 'string' || msg.type.length === 0) {
22
+ throw new Error('Invalid WebSocket message: type must be a non-empty string');
23
+ }
24
+
25
+ if (msg.timestamp !== undefined && typeof msg.timestamp !== 'number') {
26
+ throw new Error('Invalid WebSocket message: timestamp must be a number');
27
+ }
28
+
29
+ if (msg.messageId !== undefined && typeof msg.messageId !== 'string') {
30
+ throw new Error('Invalid WebSocket message: messageId must be a string');
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Typed sender with message builders
36
+ *
37
+ * Wraps WSSender to provide generic type support for outgoing messages.
38
+ */
39
+ export interface TypedSender<TOutgoing = WSMessage> extends Omit<WSSender, 'send' | 'broadcast' | 'sendTo'> {
40
+ /** Send typed message to this client */
41
+ send(message: TOutgoing): Promise<void>;
42
+ /** Broadcast typed message to all clients in channel */
43
+ broadcast(message: TOutgoing, excludeSelf?: boolean): Promise<void>;
44
+ /** Send typed message to specific connections */
45
+ sendTo(connectionIds: string[], message: TOutgoing): Promise<void>;
46
+ /** Original WSSender methods for raw messages */
47
+ raw: WSSender;
48
+ }
49
+
50
+ /**
51
+ * Create typed sender wrapper
52
+ */
53
+ function createTypedSender<TOutgoing = WSMessage>(raw: WSSender): TypedSender<TOutgoing> {
54
+ return {
55
+ send: (msg) => raw.send(msg as WSMessage),
56
+ broadcast: (msg, exclude) => raw.broadcast(msg as WSMessage, exclude),
57
+ sendTo: (ids, msg) => raw.sendTo(ids, msg as WSMessage),
58
+ close: raw.close.bind(raw),
59
+ getConnectionId: raw.getConnectionId.bind(raw),
60
+ raw,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * WebSocket handler with typed messages
66
+ */
67
+ export interface WebSocketHandler<TConfig = unknown, TIncoming = unknown, TOutgoing = WSMessage> {
68
+ /**
69
+ * Called when client connects
70
+ */
71
+ onConnect?(
72
+ context: PluginContextV3<TConfig>,
73
+ sender: TypedSender<TOutgoing>
74
+ ): Promise<void> | void;
75
+
76
+ /**
77
+ * Called when message received from client
78
+ */
79
+ onMessage?(
80
+ context: PluginContextV3<TConfig>,
81
+ message: TIncoming,
82
+ sender: TypedSender<TOutgoing>
83
+ ): Promise<void> | void;
84
+
85
+ /**
86
+ * Called when client disconnects
87
+ */
88
+ onDisconnect?(
89
+ context: PluginContextV3<TConfig>,
90
+ code: number,
91
+ reason: string
92
+ ): Promise<void> | void;
93
+
94
+ /**
95
+ * Called on error
96
+ */
97
+ onError?(
98
+ context: PluginContextV3<TConfig>,
99
+ error: Error,
100
+ sender: TypedSender<TOutgoing>
101
+ ): Promise<void> | void;
102
+
103
+ /**
104
+ * Optional cleanup - called after any lifecycle event
105
+ */
106
+ cleanup?(): Promise<void> | void;
107
+ }
108
+
109
+ export interface WebSocketDefinition<TConfig = unknown, TIncoming = unknown, TOutgoing = WSMessage> {
110
+ /**
111
+ * Channel path (e.g., "/live", "/chat")
112
+ */
113
+ path: string;
114
+
115
+ /**
116
+ * Channel description
117
+ */
118
+ description?: string;
119
+
120
+ /**
121
+ * Handler implementation
122
+ */
123
+ handler: WebSocketHandler<TConfig, TIncoming, TOutgoing>;
124
+
125
+ /**
126
+ * Optional message schema validation (future: use Zod/JSON Schema)
127
+ */
128
+ schema?: unknown;
129
+ }
130
+
131
+ /**
132
+ * Define a WebSocket channel handler
133
+ *
134
+ * @example Basic usage
135
+ * ```typescript
136
+ * export default defineWebSocket({
137
+ * path: '/chat',
138
+ * handler: {
139
+ * async onConnect(ctx, sender) {
140
+ * await sender.send({ type: 'ready', payload: {}, timestamp: Date.now() });
141
+ * },
142
+ * async onMessage(ctx, message, sender) {
143
+ * console.log('Received:', message);
144
+ * },
145
+ * }
146
+ * });
147
+ * ```
148
+ *
149
+ * @example With typed messages
150
+ * ```typescript
151
+ * interface IncomingMsg {
152
+ * type: 'start' | 'stop';
153
+ * payload: { scope?: string };
154
+ * }
155
+ *
156
+ * interface OutgoingMsg {
157
+ * type: 'progress' | 'complete';
158
+ * payload: { progress: number; message: string };
159
+ * }
160
+ *
161
+ * export default defineWebSocket<unknown, IncomingMsg, OutgoingMsg>({
162
+ * path: '/live',
163
+ * handler: {
164
+ * async onMessage(ctx, message, sender) {
165
+ * if (message.type === 'start') {
166
+ * await sender.send({
167
+ * type: 'progress',
168
+ * payload: { progress: 50, message: 'Processing...' },
169
+ * });
170
+ * }
171
+ * }
172
+ * }
173
+ * });
174
+ * ```
175
+ *
176
+ * @example With message router (advanced pattern matching)
177
+ * ```typescript
178
+ * import { defineMessage, MessageRouter } from '@kb-labs/sdk';
179
+ *
180
+ * // Define message types
181
+ * const StartMsg = defineMessage<{ scope?: string }>('start');
182
+ * const StopMsg = defineMessage<object>('stop');
183
+ *
184
+ * // Create outgoing message builders
185
+ * const ProgressMsg = defineMessage<{ phase: string; progress: number }>('progress');
186
+ * const CompleteMsg = defineMessage<{ commits: any[] }>('complete');
187
+ *
188
+ * export default defineWebSocket({
189
+ * path: '/live',
190
+ * handler: {
191
+ * async onMessage(ctx, message, sender) {
192
+ * const router = new MessageRouter()
193
+ * .on(StartMsg, async (ctx, payload, sender) => {
194
+ * await sender.send(ProgressMsg.create({
195
+ * phase: 'analyzing',
196
+ * progress: 0,
197
+ * }));
198
+ * })
199
+ * .on(StopMsg, async (ctx, payload, sender) => {
200
+ * sender.close(1000, 'Stopped by user');
201
+ * });
202
+ *
203
+ * await router.handle(ctx, message, sender.raw);
204
+ * }
205
+ * }
206
+ * });
207
+ * ```
208
+ */
209
+ export function defineWebSocket<TConfig = unknown, TIncoming = unknown, TOutgoing = WSMessage>(
210
+ definition: WebSocketDefinition<TConfig, TIncoming, TOutgoing>
211
+ ) {
212
+ // Return a unified handler that router can call with WSInput
213
+ return {
214
+ async execute(context: PluginContextV3<TConfig>, input: WSInput): Promise<CommandResult | void> {
215
+ // Validate host type at runtime
216
+ if (context.host !== 'ws') {
217
+ throw new Error(
218
+ `WebSocket channel ${definition.path} can only run in ws host (current: ${context.host})`
219
+ );
220
+ }
221
+
222
+ // Validate channel path if provided in host context
223
+ if (isWSHost(context.hostContext)) {
224
+ const expectedPath = definition.path;
225
+ const actualPath = context.hostContext.channelPath;
226
+ if (actualPath !== expectedPath) {
227
+ throw new Error(
228
+ `WebSocket expects channel ${expectedPath} but got ${actualPath}`
229
+ );
230
+ }
231
+ }
232
+
233
+ // Get sender from input (passed from channel-mounter)
234
+ const sender = input.sender;
235
+ if (!sender && input.event !== 'disconnect') {
236
+ throw new Error('WebSocket sender not provided in input');
237
+ }
238
+
239
+ // Create typed sender wrapper (only if sender is available)
240
+ const typedSender = sender ? createTypedSender<TOutgoing>(sender) : undefined;
241
+
242
+ // Route to appropriate lifecycle handler
243
+ try {
244
+ switch (input.event) {
245
+ case 'connect':
246
+ if (typedSender) {
247
+ await definition.handler.onConnect?.(context, typedSender);
248
+ }
249
+ break;
250
+
251
+ case 'message':
252
+ if (input.message && typedSender && sender) {
253
+ // Validate message structure before processing
254
+ try {
255
+ validateWSMessage(input.message);
256
+ } catch (error) {
257
+ // Log validation error and send error message to client
258
+ context.platform.logger.error('Invalid WebSocket message received', error as Error, {
259
+ connectionId: sender.getConnectionId(),
260
+ message: input.message,
261
+ });
262
+ // Send error response to client
263
+ await sender.send({
264
+ type: 'error',
265
+ payload: { error: 'Invalid message format' },
266
+ timestamp: Date.now(),
267
+ });
268
+ break;
269
+ }
270
+
271
+ // Parse incoming message as TIncoming (now validated)
272
+ const incomingMessage = input.message as unknown as TIncoming;
273
+ await definition.handler.onMessage?.(context, incomingMessage, typedSender);
274
+ }
275
+ break;
276
+
277
+ case 'disconnect':
278
+ await definition.handler.onDisconnect?.(
279
+ context,
280
+ input.disconnectCode ?? 1000,
281
+ input.disconnectReason ?? ''
282
+ );
283
+ break;
284
+
285
+ case 'error':
286
+ if (input.error && typedSender) {
287
+ await definition.handler.onError?.(context, input.error, typedSender);
288
+ }
289
+ break;
290
+ }
291
+ } finally {
292
+ // Always call cleanup after lifecycle event
293
+ await definition.handler.cleanup?.();
294
+ }
295
+
296
+ return { exitCode: 0 };
297
+ },
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Type guard to check if host context is WebSocket
303
+ */
304
+ export function isWSHost(
305
+ hostContext: HostContext
306
+ ): hostContext is Extract<HostContext, { host: 'ws' }> {
307
+ return hostContext.host === 'ws';
308
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Error Factory for KB Labs Plugins
3
+ *
4
+ * Optional helper for defining plugin errors without boilerplate.
5
+ * You can always use standard Error classes - this is just convenience.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { defineError } from '@kb-labs/shared-command-kit';
10
+ *
11
+ * export const MindError = defineError('MIND', {
12
+ * ValidationFailed: { code: 400, message: 'Validation failed' },
13
+ * IndexNotFound: { code: 404, message: (scope: string) => `Index '${scope}' not found` },
14
+ * QueryFailed: { code: 500, message: 'Query execution failed' },
15
+ * });
16
+ *
17
+ * // Usage:
18
+ * throw new MindError.IndexNotFound('default');
19
+ * throw new MindError.ValidationFailed({ details: { field: 'cwd' } });
20
+ * ```
21
+ */
22
+
23
+ /**
24
+ * Error definition with HTTP code and message
25
+ */
26
+ export interface ErrorDefinition {
27
+ /** HTTP status code (400, 404, 500, etc.) */
28
+ code: number;
29
+ /** Error message - can be string or function for parameterized messages */
30
+ message: string | ((...args: any[]) => string);
31
+ /** Optional additional details */
32
+ details?: Record<string, unknown>;
33
+ }
34
+
35
+ /**
36
+ * Error definitions map
37
+ */
38
+ export type ErrorDefinitions = Record<string, ErrorDefinition>;
39
+
40
+ /**
41
+ * Base error class with HTTP status code support
42
+ */
43
+ export class PluginError extends Error {
44
+ public readonly statusCode: number;
45
+ public readonly errorCode: string;
46
+ public readonly details?: Record<string, unknown>;
47
+
48
+ constructor(
49
+ errorCode: string,
50
+ message: string,
51
+ statusCode: number,
52
+ details?: Record<string, unknown>
53
+ ) {
54
+ super(message);
55
+ this.name = 'PluginError';
56
+ this.errorCode = errorCode;
57
+ this.statusCode = statusCode;
58
+ this.details = details;
59
+
60
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
61
+ if (Error.captureStackTrace) {
62
+ Error.captureStackTrace(this, PluginError);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Convert error to JSON for logging/serialization
68
+ */
69
+ toJSON() {
70
+ return {
71
+ name: this.name,
72
+ errorCode: this.errorCode,
73
+ message: this.message,
74
+ statusCode: this.statusCode,
75
+ details: this.details,
76
+ stack: this.stack,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Check if error is a PluginError
82
+ */
83
+ static isPluginError(error: unknown): error is PluginError {
84
+ return error instanceof PluginError;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Type for error constructor created by defineError
90
+ */
91
+ type ErrorConstructor<TArgs extends any[] = any[]> = {
92
+ new (details?: Record<string, unknown>): PluginError;
93
+ new (...args: TArgs): PluginError;
94
+ };
95
+
96
+ /**
97
+ * Type for error namespace created by defineError
98
+ */
99
+ type ErrorNamespace<TDefs extends ErrorDefinitions> = {
100
+ [K in keyof TDefs]: ErrorConstructor;
101
+ } & {
102
+ /** Check if error is from this namespace */
103
+ is(error: unknown): error is PluginError;
104
+ /** Check if error has specific error code */
105
+ hasCode(error: unknown, code: keyof TDefs): boolean;
106
+ };
107
+
108
+ /**
109
+ * Define a namespace of plugin errors
110
+ *
111
+ * @param prefix - Error code prefix (e.g., 'MIND', 'WORKFLOW')
112
+ * @param definitions - Error definitions map
113
+ * @returns Error namespace with error constructors
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * export const MindError = defineError('MIND', {
118
+ * IndexNotFound: {
119
+ * code: 404,
120
+ * message: (scope: string) => `Index '${scope}' not found`
121
+ * },
122
+ * QueryFailed: {
123
+ * code: 500,
124
+ * message: 'Query execution failed'
125
+ * },
126
+ * });
127
+ *
128
+ * // Throw with template params
129
+ * throw new MindError.IndexNotFound('default');
130
+ * // Error message: "Index 'default' not found"
131
+ *
132
+ * // Throw with details
133
+ * throw new MindError.QueryFailed({
134
+ * details: { query: 'test', reason: 'timeout' }
135
+ * });
136
+ * ```
137
+ */
138
+ export function defineError<TDefs extends ErrorDefinitions>(
139
+ prefix: string,
140
+ definitions: TDefs
141
+ ): ErrorNamespace<TDefs> {
142
+ const errorNamespace: any = {};
143
+
144
+ // Create error constructor for each definition
145
+ for (const [key, def] of Object.entries(definitions)) {
146
+ const errorCode = `${prefix}_${key.toUpperCase()}`;
147
+
148
+ // Create error class
149
+ class DefinedError extends PluginError {
150
+ constructor(...args: any[]) {
151
+ const { message, details } = buildMessageAndDetails(def, args);
152
+ super(errorCode, message, def.code, details);
153
+ this.name = `${prefix}Error`;
154
+ }
155
+ }
156
+
157
+ errorNamespace[key] = DefinedError;
158
+ }
159
+
160
+ // Add helper methods
161
+ errorNamespace.is = (error: unknown): error is PluginError => {
162
+ return PluginError.isPluginError(error) && error.errorCode.startsWith(prefix + '_');
163
+ };
164
+
165
+ errorNamespace.hasCode = (error: unknown, code: keyof TDefs): boolean => {
166
+ const errorCode = `${prefix}_${String(code).toUpperCase()}`;
167
+ return PluginError.isPluginError(error) && error.errorCode === errorCode;
168
+ };
169
+
170
+ return errorNamespace as ErrorNamespace<TDefs>;
171
+ }
172
+
173
+ /**
174
+ * Build error message and details from definition and constructor args
175
+ */
176
+ function buildMessageAndDetails(
177
+ def: ErrorDefinition,
178
+ args: any[]
179
+ ): { message: string; details?: Record<string, unknown> } {
180
+ let message: string;
181
+ let details: Record<string, unknown> | undefined;
182
+
183
+ if (typeof def.message === 'function') {
184
+ // Template message - args are template params
185
+ const lastArg = args[args.length - 1];
186
+
187
+ // Check if last arg is details object (has 'details' key)
188
+ const hasDetails = lastArg && typeof lastArg === 'object' && 'details' in lastArg;
189
+
190
+ if (hasDetails) {
191
+ // Last arg is details, rest are template params
192
+ const templateArgs = args.slice(0, -1);
193
+ message = def.message(...templateArgs);
194
+ details = { ...def.details, ...lastArg.details };
195
+ } else {
196
+ // All args are template params
197
+ message = def.message(...args);
198
+ details = def.details;
199
+ }
200
+ } else {
201
+ // Static message
202
+ message = def.message;
203
+
204
+ // First arg can be details object
205
+ if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
206
+ details = { ...def.details, ...args[0].details };
207
+ } else {
208
+ details = def.details;
209
+ }
210
+ }
211
+
212
+ return { message, details };
213
+ }
214
+
215
+ /**
216
+ * Common error definitions that can be reused across plugins
217
+ */
218
+ export const commonErrors = {
219
+ /**
220
+ * Validation error (400)
221
+ */
222
+ ValidationFailed: {
223
+ code: 400,
224
+ message: 'Validation failed',
225
+ },
226
+
227
+ /**
228
+ * Resource not found (404)
229
+ */
230
+ NotFound: {
231
+ code: 404,
232
+ message: (resource: string) => `${resource} not found`,
233
+ },
234
+
235
+ /**
236
+ * Unauthorized access (401)
237
+ */
238
+ Unauthorized: {
239
+ code: 401,
240
+ message: 'Unauthorized',
241
+ },
242
+
243
+ /**
244
+ * Forbidden access (403)
245
+ */
246
+ Forbidden: {
247
+ code: 403,
248
+ message: 'Forbidden',
249
+ },
250
+
251
+ /**
252
+ * Internal server error (500)
253
+ */
254
+ InternalError: {
255
+ code: 500,
256
+ message: 'Internal server error',
257
+ },
258
+
259
+ /**
260
+ * Service unavailable (503)
261
+ */
262
+ ServiceUnavailable: {
263
+ code: 503,
264
+ message: (service: string) => `Service '${service}' is unavailable`,
265
+ },
266
+
267
+ /**
268
+ * Timeout error (504)
269
+ */
270
+ Timeout: {
271
+ code: 504,
272
+ message: (operation: string) => `Operation '${operation}' timed out`,
273
+ },
274
+
275
+ /**
276
+ * Conflict error (409)
277
+ */
278
+ Conflict: {
279
+ code: 409,
280
+ message: (resource: string) => `${resource} already exists`,
281
+ },
282
+ } satisfies ErrorDefinitions;