@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.
- package/.cursorrules +32 -0
- package/.github/workflows/ci.yml +13 -0
- package/.github/workflows/deploy.yml +28 -0
- package/.github/workflows/docker-build.yml +25 -0
- package/.github/workflows/drift-check.yml +10 -0
- package/.github/workflows/profiles-validate.yml +16 -0
- package/.github/workflows/release.yml +8 -0
- package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
- package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
- package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
- package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
- package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
- package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
- package/.kb/devkit/agents/release-manager/context.globs +7 -0
- package/.kb/devkit/agents/release-manager/prompt.md +27 -0
- package/.kb/devkit/agents/release-manager/runbook.md +17 -0
- package/.kb/devkit/agents/test-generator/context.globs +7 -0
- package/.kb/devkit/agents/test-generator/prompt.md +27 -0
- package/.kb/devkit/agents/test-generator/runbook.md +18 -0
- package/.vscode/settings.json +23 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +117 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/docs/DECLARATIVE-FLAGS-AND-ENV.md +622 -0
- package/docs/DOCUMENTATION.md +70 -0
- package/docs/adr/0000-template.md +52 -0
- package/docs/adr/0001-architecture-and-repository-layout.md +31 -0
- package/docs/adr/0002-plugins-and-extensibility.md +44 -0
- package/docs/adr/0003-package-and-module-boundaries.md +35 -0
- package/docs/adr/0004-versioning-and-release-policy.md +36 -0
- package/docs/adr/0005-reactive-loader-pattern.md +179 -0
- package/docs/adr/0006-declarative-flags-and-env-systems.md +376 -0
- package/eslint.config.js +27 -0
- package/kb-labs.config.json +5 -0
- package/package.json +88 -0
- package/package.json.bin +25 -0
- package/package.json.lib +30 -0
- package/packages/shared-cli-ui/CHANGELOG.md +20 -0
- package/packages/shared-cli-ui/README.md +342 -0
- package/packages/shared-cli-ui/docs/ARCHITECTURE.md +105 -0
- package/packages/shared-cli-ui/eslint.config.js +27 -0
- package/packages/shared-cli-ui/package.json +72 -0
- package/packages/shared-cli-ui/src/__tests__/artifacts-display.spec.ts +89 -0
- package/packages/shared-cli-ui/src/__tests__/format.spec.ts +44 -0
- package/packages/shared-cli-ui/src/__tests__/loader-json-mode.test.ts +119 -0
- package/packages/shared-cli-ui/src/artifacts-display.ts +266 -0
- package/packages/shared-cli-ui/src/cli-auto-discovery.ts +120 -0
- package/packages/shared-cli-ui/src/colors.ts +142 -0
- package/packages/shared-cli-ui/src/command-discovery.ts +72 -0
- package/packages/shared-cli-ui/src/command-output.ts +153 -0
- package/packages/shared-cli-ui/src/command-result.ts +267 -0
- package/packages/shared-cli-ui/src/command-runner.ts +310 -0
- package/packages/shared-cli-ui/src/command-suggestions.ts +204 -0
- package/packages/shared-cli-ui/src/debug/components/output.ts +141 -0
- package/packages/shared-cli-ui/src/debug/components/trace.ts +101 -0
- package/packages/shared-cli-ui/src/debug/components/tree.ts +88 -0
- package/packages/shared-cli-ui/src/debug/formatters/ai.ts +17 -0
- package/packages/shared-cli-ui/src/debug/formatters/human.ts +98 -0
- package/packages/shared-cli-ui/src/debug/formatters/timeline.ts +94 -0
- package/packages/shared-cli-ui/src/debug/index.ts +56 -0
- package/packages/shared-cli-ui/src/debug/types.ts +57 -0
- package/packages/shared-cli-ui/src/debug/utilities.ts +203 -0
- package/packages/shared-cli-ui/src/dynamic-command-discovery.ts +131 -0
- package/packages/shared-cli-ui/src/format.ts +412 -0
- package/packages/shared-cli-ui/src/index.ts +34 -0
- package/packages/shared-cli-ui/src/loader.ts +196 -0
- package/packages/shared-cli-ui/src/manifest-parser.ts +151 -0
- package/packages/shared-cli-ui/src/modern-format.ts +271 -0
- package/packages/shared-cli-ui/src/multi-cli-suggestions.ts +159 -0
- package/packages/shared-cli-ui/src/table.ts +134 -0
- package/packages/shared-cli-ui/src/timing-tracker.ts +68 -0
- package/packages/shared-cli-ui/src/utils/context.ts +12 -0
- package/packages/shared-cli-ui/src/utils/env.ts +164 -0
- package/packages/shared-cli-ui/src/utils/flags.ts +269 -0
- package/packages/shared-cli-ui/src/utils/path.ts +8 -0
- package/packages/shared-cli-ui/tsconfig.build.json +15 -0
- package/packages/shared-cli-ui/tsconfig.json +9 -0
- package/packages/shared-cli-ui/tsup.config.ts +11 -0
- package/packages/shared-cli-ui/vitest.config.ts +15 -0
- package/packages/shared-command-kit/CHANGELOG.md +20 -0
- package/packages/shared-command-kit/LICENSE +22 -0
- package/packages/shared-command-kit/README.md +1030 -0
- package/packages/shared-command-kit/docs/HIGH-LEVEL-API.md +89 -0
- package/packages/shared-command-kit/docs/LOW-LEVEL-API.md +105 -0
- package/packages/shared-command-kit/docs/MIGRATION-GUIDE.md +135 -0
- package/packages/shared-command-kit/eslint.config.js +27 -0
- package/packages/shared-command-kit/eslint.config.ts +14 -0
- package/packages/shared-command-kit/package.json +76 -0
- package/packages/shared-command-kit/prettierrc.json +5 -0
- package/packages/shared-command-kit/src/__tests__/define-command.spec.ts +294 -0
- package/packages/shared-command-kit/src/__tests__/define-route.test.ts +285 -0
- package/packages/shared-command-kit/src/__tests__/define-system-command.spec.ts +508 -0
- package/packages/shared-command-kit/src/__tests__/define-webhook.test.ts +156 -0
- package/packages/shared-command-kit/src/__tests__/define-websocket.test.ts +316 -0
- package/packages/shared-command-kit/src/__tests__/errors.spec.ts +45 -0
- package/packages/shared-command-kit/src/__tests__/flags.spec.ts +353 -0
- package/packages/shared-command-kit/src/__tests__/platform-api.test.ts +135 -0
- package/packages/shared-command-kit/src/__tests__/plugin-context-v3.snapshot.spec.ts +240 -0
- package/packages/shared-command-kit/src/__tests__/ws-types.test.ts +359 -0
- package/packages/shared-command-kit/src/analytics/index.ts +6 -0
- package/packages/shared-command-kit/src/analytics/with-analytics.ts +195 -0
- package/packages/shared-command-kit/src/define-action.ts +100 -0
- package/packages/shared-command-kit/src/define-command.ts +113 -0
- package/packages/shared-command-kit/src/define-route.ts +113 -0
- package/packages/shared-command-kit/src/define-system-command.ts +362 -0
- package/packages/shared-command-kit/src/define-webhook.ts +115 -0
- package/packages/shared-command-kit/src/define-websocket.ts +308 -0
- package/packages/shared-command-kit/src/errors/factory.ts +282 -0
- package/packages/shared-command-kit/src/errors/format-validation.ts +144 -0
- package/packages/shared-command-kit/src/errors/format.ts +92 -0
- package/packages/shared-command-kit/src/errors/index.ts +9 -0
- package/packages/shared-command-kit/src/errors/types.ts +32 -0
- package/packages/shared-command-kit/src/flags/define.ts +92 -0
- package/packages/shared-command-kit/src/flags/index.ts +9 -0
- package/packages/shared-command-kit/src/flags/types.ts +153 -0
- package/packages/shared-command-kit/src/flags/validate.ts +358 -0
- package/packages/shared-command-kit/src/helpers/context.ts +8 -0
- package/packages/shared-command-kit/src/helpers/flags.ts +84 -0
- package/packages/shared-command-kit/src/helpers/index.ts +42 -0
- package/packages/shared-command-kit/src/helpers/patterns.ts +464 -0
- package/packages/shared-command-kit/src/helpers/platform.ts +335 -0
- package/packages/shared-command-kit/src/helpers/use-analytics.ts +95 -0
- package/packages/shared-command-kit/src/helpers/use-cache.ts +97 -0
- package/packages/shared-command-kit/src/helpers/use-config.ts +99 -0
- package/packages/shared-command-kit/src/helpers/use-embeddings.ts +49 -0
- package/packages/shared-command-kit/src/helpers/use-llm.ts +316 -0
- package/packages/shared-command-kit/src/helpers/use-logger.ts +77 -0
- package/packages/shared-command-kit/src/helpers/use-platform.ts +111 -0
- package/packages/shared-command-kit/src/helpers/use-resource-broker.ts +106 -0
- package/packages/shared-command-kit/src/helpers/use-storage.ts +71 -0
- package/packages/shared-command-kit/src/helpers/use-vector-store.ts +49 -0
- package/packages/shared-command-kit/src/helpers/validation.ts +398 -0
- package/packages/shared-command-kit/src/index.ts +410 -0
- package/packages/shared-command-kit/src/jobs.ts +132 -0
- package/packages/shared-command-kit/src/lifecycle/define-handlers.ts +366 -0
- package/packages/shared-command-kit/src/lifecycle/index.ts +6 -0
- package/packages/shared-command-kit/src/manifest.ts +127 -0
- package/packages/shared-command-kit/src/rest/define-handler.ts +187 -0
- package/packages/shared-command-kit/src/rest/index.ts +11 -0
- package/packages/shared-command-kit/src/studio/index.ts +12 -0
- package/packages/shared-command-kit/src/validation/index.ts +6 -0
- package/packages/shared-command-kit/src/validation/schema-builders.ts +409 -0
- package/packages/shared-command-kit/src/ws-types.ts +106 -0
- package/packages/shared-command-kit/tsconfig.build.json +15 -0
- package/packages/shared-command-kit/tsconfig.json +9 -0
- package/packages/shared-command-kit/tsup.config.ts +30 -0
- package/packages/shared-command-kit/vitest.config.ts +4 -0
- package/packages/shared-http/package.json +67 -0
- package/packages/shared-http/src/__tests__/log-correlation.test.ts +81 -0
- package/packages/shared-http/src/__tests__/operation-metrics-tracker.test.ts +55 -0
- package/packages/shared-http/src/http-observability-collector.ts +363 -0
- package/packages/shared-http/src/index.ts +36 -0
- package/packages/shared-http/src/log-correlation.ts +89 -0
- package/packages/shared-http/src/operation-metrics-tracker.ts +107 -0
- package/packages/shared-http/src/register-openapi.ts +108 -0
- package/packages/shared-http/src/resolve-schema-ref.ts +75 -0
- package/packages/shared-http/src/schemas.ts +29 -0
- package/packages/shared-http/src/service-observability.ts +63 -0
- package/packages/shared-http/tsconfig.build.json +15 -0
- package/packages/shared-http/tsconfig.json +9 -0
- package/packages/shared-http/tsup.config.ts +23 -0
- package/packages/shared-http/vitest.config.ts +13 -0
- package/packages/shared-perm-presets/CHANGELOG.md +20 -0
- package/packages/shared-perm-presets/README.md +78 -0
- package/packages/shared-perm-presets/eslint.config.js +27 -0
- package/packages/shared-perm-presets/package.json +45 -0
- package/packages/shared-perm-presets/src/__tests__/combine.test.ts +403 -0
- package/packages/shared-perm-presets/src/__tests__/presets.test.ts +205 -0
- package/packages/shared-perm-presets/src/combine.ts +278 -0
- package/packages/shared-perm-presets/src/index.ts +18 -0
- package/packages/shared-perm-presets/src/presets/ci-environment.ts +34 -0
- package/packages/shared-perm-presets/src/presets/full-env.ts +16 -0
- package/packages/shared-perm-presets/src/presets/git-workflow.ts +40 -0
- package/packages/shared-perm-presets/src/presets/index.ts +8 -0
- package/packages/shared-perm-presets/src/presets/kb-platform.ts +30 -0
- package/packages/shared-perm-presets/src/presets/llm-access.ts +29 -0
- package/packages/shared-perm-presets/src/presets/minimal.ts +21 -0
- package/packages/shared-perm-presets/src/presets/npm-publish.ts +48 -0
- package/packages/shared-perm-presets/src/presets/vector-store.ts +40 -0
- package/packages/shared-perm-presets/src/types.ts +192 -0
- package/packages/shared-perm-presets/tsconfig.build.json +15 -0
- package/packages/shared-perm-presets/tsconfig.json +9 -0
- package/packages/shared-perm-presets/tsup.config.ts +8 -0
- package/packages/shared-perm-presets/vitest.config.ts +9 -0
- package/packages/shared-testing/CHANGELOG.md +20 -0
- package/packages/shared-testing/README.md +430 -0
- package/packages/shared-testing/package.json +51 -0
- package/packages/shared-testing/src/__tests__/create-test-context.test.ts +199 -0
- package/packages/shared-testing/src/__tests__/mock-cache.test.ts +174 -0
- package/packages/shared-testing/src/__tests__/mock-llm.test.ts +212 -0
- package/packages/shared-testing/src/__tests__/setup-platform.test.ts +90 -0
- package/packages/shared-testing/src/__tests__/test-command.test.ts +557 -0
- package/packages/shared-testing/src/create-test-context.ts +550 -0
- package/packages/shared-testing/src/index.ts +77 -0
- package/packages/shared-testing/src/mock-cache.ts +179 -0
- package/packages/shared-testing/src/mock-llm.ts +319 -0
- package/packages/shared-testing/src/mock-logger.ts +97 -0
- package/packages/shared-testing/src/mock-storage.ts +108 -0
- package/packages/shared-testing/src/setup-platform.ts +101 -0
- package/packages/shared-testing/src/test-command.ts +288 -0
- package/packages/shared-testing/tsconfig.build.json +15 -0
- package/packages/shared-testing/tsconfig.json +9 -0
- package/packages/shared-testing/tsup.config.ts +20 -0
- package/packages/shared-testing/vitest.config.ts +3 -0
- package/packages/shared-tool-kit/CHANGELOG.md +20 -0
- package/packages/shared-tool-kit/package.json +47 -0
- package/packages/shared-tool-kit/src/__tests__/factory.test.ts +103 -0
- package/packages/shared-tool-kit/src/__tests__/mock-tool.test.ts +95 -0
- package/packages/shared-tool-kit/src/factory.ts +126 -0
- package/packages/shared-tool-kit/src/index.ts +32 -0
- package/packages/shared-tool-kit/src/testing/index.ts +84 -0
- package/packages/shared-tool-kit/tsconfig.build.json +15 -0
- package/packages/shared-tool-kit/tsconfig.json +9 -0
- package/packages/shared-tool-kit/tsup.config.ts +21 -0
- package/pnpm-workspace.yaml +11070 -0
- package/prettierrc.json +1 -0
- package/scripts/devkit-sync.mjs +37 -0
- package/scripts/hooks/post-push +9 -0
- package/scripts/hooks/pre-commit +9 -0
- package/scripts/hooks/pre-push +9 -0
- package/tsconfig.base.json +9 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +9 -0
- package/tsconfig.paths.json +50 -0
- package/tsconfig.tools.json +18 -0
- package/tsup.config.bin.ts +34 -0
- package/tsup.config.cli.ts +41 -0
- package/tsup.config.dual.ts +46 -0
- package/tsup.config.ts +36 -0
- package/tsup.external.json +104 -0
- 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;
|