@openvcs/sdk 0.2.2 → 0.2.4

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 (51) hide show
  1. package/README.md +76 -7
  2. package/lib/build.d.ts +28 -0
  3. package/lib/build.js +188 -0
  4. package/lib/cli.js +21 -2
  5. package/lib/dist.d.ts +4 -7
  6. package/lib/dist.js +67 -113
  7. package/lib/init.d.ts +2 -0
  8. package/lib/init.js +13 -8
  9. package/lib/runtime/contracts.d.ts +45 -0
  10. package/lib/runtime/contracts.js +4 -0
  11. package/lib/runtime/dispatcher.d.ts +16 -0
  12. package/lib/runtime/dispatcher.js +133 -0
  13. package/lib/runtime/errors.d.ts +5 -0
  14. package/lib/runtime/errors.js +26 -0
  15. package/lib/runtime/host.d.ts +10 -0
  16. package/lib/runtime/host.js +48 -0
  17. package/lib/runtime/index.d.ts +9 -0
  18. package/lib/runtime/index.js +166 -0
  19. package/lib/runtime/transport.d.ts +14 -0
  20. package/lib/runtime/transport.js +72 -0
  21. package/lib/types/host.d.ts +57 -0
  22. package/lib/types/host.js +4 -0
  23. package/lib/types/index.d.ts +4 -0
  24. package/lib/types/index.js +22 -0
  25. package/lib/types/plugin.d.ts +56 -0
  26. package/lib/types/plugin.js +4 -0
  27. package/lib/types/protocol.d.ts +77 -0
  28. package/lib/types/protocol.js +13 -0
  29. package/lib/types/vcs.d.ts +459 -0
  30. package/lib/types/vcs.js +4 -0
  31. package/package.json +16 -3
  32. package/src/lib/build.ts +229 -0
  33. package/src/lib/cli.ts +21 -2
  34. package/src/lib/dist.ts +76 -128
  35. package/src/lib/init.ts +13 -8
  36. package/src/lib/runtime/contracts.ts +52 -0
  37. package/src/lib/runtime/dispatcher.ts +185 -0
  38. package/src/lib/runtime/errors.ts +27 -0
  39. package/src/lib/runtime/host.ts +72 -0
  40. package/src/lib/runtime/index.ts +201 -0
  41. package/src/lib/runtime/transport.ts +93 -0
  42. package/src/lib/types/host.ts +71 -0
  43. package/src/lib/types/index.ts +7 -0
  44. package/src/lib/types/plugin.ts +110 -0
  45. package/src/lib/types/protocol.ts +97 -0
  46. package/src/lib/types/vcs.ts +579 -0
  47. package/test/build.test.js +95 -0
  48. package/test/cli.test.js +37 -0
  49. package/test/dist.test.js +239 -15
  50. package/test/init.test.js +25 -0
  51. package/test/runtime.test.js +118 -0
@@ -0,0 +1,185 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import {
5
+ PLUGIN_INTERNAL_ERROR_CODE,
6
+ PROTOCOL_VERSION,
7
+ PROTOCOL_VERSION_MISMATCH_CODE,
8
+ } from '../types';
9
+ import type {
10
+ JsonRpcId,
11
+ PluginDelegates,
12
+ PluginImplements,
13
+ RequestParams,
14
+ RpcMethodHandler,
15
+ VcsDelegates,
16
+ } from '../types';
17
+ import type { PluginHost } from '../types';
18
+
19
+ import type { CreatePluginRuntimeOptions, PluginRuntimeContext } from './contracts';
20
+ import { isPluginFailure, pluginError } from './errors';
21
+
22
+ /** Describes the response emitter used by the dispatcher. */
23
+ export interface DispatcherResponseWriter {
24
+ /** Emits a JSON-RPC success response. */
25
+ sendResult<TResult>(id: JsonRpcId, result: TResult): void;
26
+ /** Emits a JSON-RPC error response. */
27
+ sendError(id: JsonRpcId, code: number, message: string, data?: unknown): void;
28
+ }
29
+
30
+ /** Describes the request handler built by `createRuntimeDispatcher`. */
31
+ export type RuntimeRequestDispatcher = (
32
+ id: JsonRpcId,
33
+ method: string,
34
+ params: RequestParams,
35
+ ) => Promise<void>;
36
+
37
+ /** Creates the plugin default handlers used when a delegate is omitted. */
38
+ export function createDefaultPluginDelegates<
39
+ TContext,
40
+ >(): Required<Omit<PluginDelegates<TContext>, 'plugin.initialize'>> {
41
+ return {
42
+ async 'plugin.init'(): Promise<null> {
43
+ return null;
44
+ },
45
+ async 'plugin.deinit'(): Promise<null> {
46
+ return null;
47
+ },
48
+ async 'plugin.get_menus'(): Promise<[]> {
49
+ return [];
50
+ },
51
+ async 'plugin.handle_action'(): Promise<null> {
52
+ return null;
53
+ },
54
+ async 'plugin.settings.defaults'(): Promise<[]> {
55
+ return [];
56
+ },
57
+ async 'plugin.settings.on_load'(params: { values?: unknown[] }): Promise<unknown[]> {
58
+ return Array.isArray(params.values) ? params.values : [];
59
+ },
60
+ async 'plugin.settings.on_apply'(): Promise<null> {
61
+ return null;
62
+ },
63
+ async 'plugin.settings.on_save'(params: { values?: unknown[] }): Promise<unknown[]> {
64
+ return Array.isArray(params.values) ? params.values : [];
65
+ },
66
+ async 'plugin.settings.on_reset'(): Promise<null> {
67
+ return null;
68
+ },
69
+ };
70
+ }
71
+
72
+ /** Creates the JSON-RPC dispatcher used by the SDK plugin runtime. */
73
+ export function createRuntimeDispatcher(
74
+ options: CreatePluginRuntimeOptions,
75
+ host: PluginHost,
76
+ writer: DispatcherResponseWriter,
77
+ ): RuntimeRequestDispatcher {
78
+ const defaultPluginDelegates = createDefaultPluginDelegates<PluginRuntimeContext>();
79
+ const pluginDelegates = options.plugin ?? {};
80
+ const vcsDelegates = options.vcs ?? {};
81
+ const runtimeImplements = buildRuntimeImplements(options.implements, options.vcs);
82
+ const timeout = options.timeout;
83
+ const typedPluginDelegates = pluginDelegates as Record<
84
+ string,
85
+ RpcMethodHandler<RequestParams, unknown, PluginRuntimeContext> | undefined
86
+ >;
87
+ const typedDefaultPluginDelegates = defaultPluginDelegates as Record<
88
+ string,
89
+ RpcMethodHandler<RequestParams, unknown, PluginRuntimeContext> | undefined
90
+ >;
91
+ const typedVcsDelegates = vcsDelegates as Record<
92
+ string,
93
+ RpcMethodHandler<RequestParams, unknown, PluginRuntimeContext> | undefined
94
+ >;
95
+
96
+ return async (id: JsonRpcId, method: string, params: RequestParams): Promise<void> => {
97
+ try {
98
+ if (method === 'plugin.initialize') {
99
+ const expectedVersion = params.expected_protocol_version;
100
+ if (typeof expectedVersion === 'number' && expectedVersion !== PROTOCOL_VERSION) {
101
+ writer.sendError(id, PROTOCOL_VERSION_MISMATCH_CODE, 'protocol version mismatch', {
102
+ code: 'protocol-version-mismatch',
103
+ message: `host expects protocol ${expectedVersion}, plugin supports ${PROTOCOL_VERSION}`,
104
+ });
105
+ return;
106
+ }
107
+
108
+ const override = pluginDelegates['plugin.initialize']
109
+ ? await pluginDelegates['plugin.initialize'](params, {
110
+ host,
111
+ requestId: id,
112
+ method,
113
+ })
114
+ : {};
115
+ writer.sendResult(id, {
116
+ protocol_version: override.protocol_version ?? PROTOCOL_VERSION,
117
+ implements: {
118
+ ...runtimeImplements,
119
+ ...override.implements,
120
+ },
121
+ });
122
+ return;
123
+ }
124
+
125
+ const handler =
126
+ typedPluginDelegates[method] ??
127
+ typedDefaultPluginDelegates[method] ??
128
+ typedVcsDelegates[method];
129
+
130
+ if (!handler) {
131
+ throw pluginError('rpc-method-not-found', `method '${method}' is not implemented`);
132
+ }
133
+
134
+ let result: unknown;
135
+ if (timeout && timeout > 0) {
136
+ const controller = new AbortController();
137
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
138
+ try {
139
+ result = await handler(params, {
140
+ host,
141
+ requestId: id,
142
+ method,
143
+ });
144
+ } catch (error) {
145
+ clearTimeout(timeoutId);
146
+ if ((error as Error).name === 'AbortError') {
147
+ throw pluginError('request-timeout', `method '${method}' timed out after ${timeout}ms`);
148
+ }
149
+ throw error;
150
+ }
151
+ clearTimeout(timeoutId);
152
+ } else {
153
+ result = await handler(params, {
154
+ host,
155
+ requestId: id,
156
+ method,
157
+ });
158
+ }
159
+ writer.sendResult(id, result == null ? null : result);
160
+ } catch (error) {
161
+ if (isPluginFailure(error)) {
162
+ writer.sendError(id, error.code, error.message, error.data);
163
+ return;
164
+ }
165
+
166
+ const messageText = error instanceof Error ? error.message : String(error || 'unknown error');
167
+ host.error(messageText);
168
+ writer.sendError(id, PLUGIN_INTERNAL_ERROR_CODE, messageText, {
169
+ code: 'plugin-internal-error',
170
+ message: messageText,
171
+ });
172
+ }
173
+ };
174
+ }
175
+
176
+ /** Builds the handshake capability flags returned from `plugin.initialize`. */
177
+ function buildRuntimeImplements(
178
+ overrides: Partial<PluginImplements> | undefined,
179
+ vcsDelegates: VcsDelegates<PluginRuntimeContext> | undefined,
180
+ ): PluginImplements {
181
+ return {
182
+ plugin: overrides?.plugin ?? true,
183
+ vcs: overrides?.vcs ?? Boolean(vcsDelegates && Object.keys(vcsDelegates).length > 0),
184
+ };
185
+ }
@@ -0,0 +1,27 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import { PLUGIN_FAILURE_CODE } from '../types';
5
+ import type { PluginFailure } from '../types';
6
+
7
+ /** Builds the host-facing plugin failure payload used for operational errors. */
8
+ export function pluginError(code: string, message: string): PluginFailure {
9
+ return {
10
+ code: PLUGIN_FAILURE_CODE,
11
+ message,
12
+ data: {
13
+ code,
14
+ message,
15
+ },
16
+ };
17
+ }
18
+
19
+ /** Returns whether the supplied value is a structured plugin failure. */
20
+ export function isPluginFailure(value: unknown): value is PluginFailure {
21
+ if (value == null || typeof value !== 'object') {
22
+ return false;
23
+ }
24
+
25
+ const candidate = value as Partial<PluginFailure>;
26
+ return candidate.code === PLUGIN_FAILURE_CODE && typeof candidate.message === 'string';
27
+ }
@@ -0,0 +1,72 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type {
5
+ HostEventEmitParams,
6
+ HostLogLevel,
7
+ HostStatusSetParams,
8
+ HostUiNotifyParams,
9
+ JsonRpcId,
10
+ PluginHost,
11
+ } from '../types';
12
+
13
+ /** Describes the low-level notification sender used by `createHost`. */
14
+ export type HostNotificationSender = (method: string, params: unknown) => void;
15
+
16
+ /** Describes the options accepted by `createHost`. */
17
+ export interface CreateHostOptions {
18
+ /** Stores the `host.log` target name to emit for diagnostic messages. */
19
+ logTarget?: string;
20
+ }
21
+
22
+ /** Creates the host notification helper used inside runtime delegates. */
23
+ export function createHost(
24
+ sendNotification: HostNotificationSender,
25
+ options: CreateHostOptions = {},
26
+ ): PluginHost {
27
+ const logTarget = options.logTarget ?? 'openvcs.plugin';
28
+
29
+ return {
30
+ log(level: HostLogLevel, message: string): void {
31
+ sendNotification('host.log', {
32
+ level,
33
+ target: logTarget,
34
+ message,
35
+ });
36
+ },
37
+ info(message: string): void {
38
+ sendNotification('host.log', {
39
+ level: 'info',
40
+ target: logTarget,
41
+ message,
42
+ });
43
+ },
44
+ error(message: string): void {
45
+ sendNotification('host.log', {
46
+ level: 'error',
47
+ target: logTarget,
48
+ message,
49
+ });
50
+ },
51
+ uiNotify(params: HostUiNotifyParams): void {
52
+ sendNotification('host.ui_notify', params);
53
+ },
54
+ statusSet(params: HostStatusSetParams): void {
55
+ sendNotification('host.status_set', params);
56
+ },
57
+ emitEvent(params: HostEventEmitParams): void {
58
+ sendNotification('host.event_emit', params);
59
+ },
60
+ emitVcsEvent(
61
+ sessionId: string,
62
+ requestId: JsonRpcId | null,
63
+ event: Record<string, unknown>,
64
+ ): void {
65
+ sendNotification('vcs.event', {
66
+ session_id: sessionId,
67
+ request_id: requestId,
68
+ event,
69
+ });
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,201 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type { JsonRpcId, JsonRpcRequest, RequestParams } from '../types';
5
+
6
+ import type {
7
+ CreatePluginRuntimeOptions,
8
+ PluginRuntime,
9
+ PluginRuntimeContext,
10
+ PluginRuntimeTransport,
11
+ } from './contracts';
12
+ import { createRuntimeDispatcher } from './dispatcher';
13
+ import { createHost } from './host';
14
+ import { parseFramedMessages, writeFramedMessage } from './transport';
15
+
16
+ export type {
17
+ CreatePluginRuntimeOptions,
18
+ PluginRuntime,
19
+ PluginRuntimeContext,
20
+ PluginRuntimeTransport,
21
+ } from './contracts';
22
+ export { createDefaultPluginDelegates } from './dispatcher';
23
+ export { isPluginFailure, pluginError } from './errors';
24
+ export { createHost } from './host';
25
+
26
+ /** Creates a reusable stdio JSON-RPC runtime for OpenVCS Node plugins. */
27
+ export function createPluginRuntime(
28
+ options: CreatePluginRuntimeOptions = {},
29
+ ): PluginRuntime {
30
+ let buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
31
+ let processing: Promise<void> = Promise.resolve();
32
+ let started = false;
33
+ let currentTransport: PluginRuntimeTransport | null = null;
34
+ let chunkLock: Promise<void> = Promise.resolve();
35
+
36
+ const runtime: PluginRuntime = {
37
+ start(transport: PluginRuntimeTransport = defaultTransport()): void {
38
+ if (started) {
39
+ return;
40
+ }
41
+
42
+ started = true;
43
+ currentTransport = transport;
44
+ transport.stdin.on('data', (chunk: Buffer | string) => {
45
+ runtime.consumeChunk(chunk);
46
+ });
47
+ transport.stdin.on('error', () => {
48
+ process.exit(1);
49
+ });
50
+ options.onStart?.();
51
+ },
52
+ stop(): void {
53
+ if (!started) {
54
+ return;
55
+ }
56
+ started = false;
57
+ processing = processing
58
+ .catch(async (error: unknown) => {
59
+ const errorMessage = error instanceof Error ? error.message : String(error);
60
+ console.error(`[runtime] shutdown error: ${errorMessage}`);
61
+ const err = error instanceof Error ? error : new Error(errorMessage);
62
+ await options.onShutdown?.(err);
63
+ })
64
+ .then(async () => {
65
+ await options.onShutdown?.();
66
+ });
67
+ currentTransport = null;
68
+ },
69
+ consumeChunk(chunk: Buffer | string): void {
70
+ if (!started) {
71
+ return;
72
+ }
73
+
74
+ const lock = chunkLock.then(() => {
75
+ buffer = Buffer.concat([buffer, normalizeChunk(chunk)]);
76
+ const parsed = parseFramedMessages(buffer);
77
+ buffer = parsed.remainder;
78
+
79
+ for (const request of parsed.messages) {
80
+ processing = processing
81
+ .then(async () => {
82
+ await runtime.dispatchRequest(request);
83
+ })
84
+ .catch((error: unknown) => {
85
+ const message = error instanceof Error ? error.message : String(error || 'unknown plugin processing error');
86
+ const host = createRuntimeHost(currentTransport, options.logTarget);
87
+ host.error(message);
88
+ });
89
+ }
90
+ });
91
+
92
+ chunkLock = lock;
93
+ },
94
+ async dispatchRequest(request: JsonRpcRequest): Promise<void> {
95
+ const id = request.id;
96
+ const host = createRuntimeHost(currentTransport, options.logTarget);
97
+ const method = asTrimmedString(request.method);
98
+ const validationErrors: string[] = [];
99
+ if (!method) validationErrors.push('missing method');
100
+ if (typeof id !== 'number' && typeof id !== 'string') validationErrors.push(`invalid id type: ${typeof id}`);
101
+ if (validationErrors.length > 0) {
102
+ host.error(`invalid request: ${validationErrors.join(', ')}`);
103
+ return;
104
+ }
105
+ const dispatcher = createRuntimeDispatcher(options, host, {
106
+ async sendResult<TResult>(requestId: JsonRpcId, result: TResult): Promise<void> {
107
+ await sendMessage(currentTransport, {
108
+ jsonrpc: '2.0',
109
+ id: requestId,
110
+ result,
111
+ });
112
+ },
113
+ async sendError(requestId: JsonRpcId, code: number, message: string, data?: unknown): Promise<void> {
114
+ await sendMessage(currentTransport, {
115
+ jsonrpc: '2.0',
116
+ id: requestId,
117
+ error: {
118
+ code,
119
+ message,
120
+ ...(data == null ? {} : { data }),
121
+ },
122
+ });
123
+ },
124
+ });
125
+
126
+ const methodName = method as string;
127
+ const params = asRecord(request.params) ?? {};
128
+ const requestId = id as JsonRpcId;
129
+ await dispatcher(requestId, methodName, params);
130
+ },
131
+ };
132
+
133
+ return runtime;
134
+ }
135
+
136
+ /** Starts a previously created plugin runtime on process stdio. */
137
+ export function startPluginRuntime(
138
+ runtime: PluginRuntime,
139
+ transport?: PluginRuntimeTransport,
140
+ ): void {
141
+ runtime.start(transport);
142
+ }
143
+
144
+ /** Returns the default stdio transport used by the runtime. */
145
+ function defaultTransport(): PluginRuntimeTransport {
146
+ return {
147
+ stdin: process.stdin,
148
+ stdout: process.stdout,
149
+ };
150
+ }
151
+
152
+ /** Sends one JSON-RPC response or notification through the active transport. */
153
+ async function sendMessage(
154
+ transport: PluginRuntimeTransport | null,
155
+ value: unknown,
156
+ ): Promise<void> {
157
+ await writeFramedMessage((transport ?? defaultTransport()).stdout, value);
158
+ }
159
+
160
+ /** Builds the host helper for the currently active transport. */
161
+ function createRuntimeHost(
162
+ transport: PluginRuntimeTransport | null,
163
+ logTarget: string | undefined,
164
+ ) {
165
+ return createHost(
166
+ async (method: string, params: unknown) => {
167
+ await sendMessage(transport, {
168
+ jsonrpc: '2.0',
169
+ method,
170
+ params,
171
+ });
172
+ },
173
+ { logTarget },
174
+ );
175
+ }
176
+
177
+ /** Type guard that returns true if the value is a valid RequestParams object. */
178
+ function isRequestParams(value: unknown): value is RequestParams {
179
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
180
+ return false;
181
+ }
182
+ return true;
183
+ }
184
+
185
+ /** Coerces unknown request method values into trimmed strings. Returns null for non-strings. */
186
+ function asTrimmedString(value: unknown): string | null {
187
+ return typeof value === 'string' ? value.trim() : null;
188
+ }
189
+
190
+ /** Coerces unknown params to a Record, returning null for invalid input. */
191
+ function asRecord(value: unknown): Record<string, unknown> | null {
192
+ if (isRequestParams(value)) {
193
+ return value as Record<string, unknown>;
194
+ }
195
+ return null;
196
+ }
197
+
198
+ /** Normalizes incoming data chunks to UTF-8 buffers. */
199
+ function normalizeChunk(chunk: Buffer | string): Buffer {
200
+ return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
201
+ }
@@ -0,0 +1,93 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type { JsonRpcRequest } from '../types';
5
+
6
+ /** Describes the parsed result of draining one buffered transport chunk. */
7
+ export interface FramedMessageParseResult {
8
+ /** Stores the decoded requests found in the current buffer. */
9
+ messages: JsonRpcRequest[];
10
+ /** Stores any remaining incomplete bytes. */
11
+ remainder: Buffer;
12
+ }
13
+
14
+ /** Serializes one JSON-RPC payload into an LSP-style framed stdio message. */
15
+ export function serializeFramedMessage(value: unknown): Buffer {
16
+ const payload = Buffer.from(JSON.stringify(value), 'utf8');
17
+ const header = Buffer.from(`Content-Length: ${payload.length}\r\n\r\n`, 'utf8');
18
+ return Buffer.concat([header, payload]);
19
+ }
20
+
21
+ /** Writes one JSON-RPC payload to the supplied stdout-like stream. */
22
+ export async function writeFramedMessage(
23
+ writer: NodeJS.WritableStream,
24
+ value: unknown,
25
+ ): Promise<void> {
26
+ try {
27
+ const serialized = serializeFramedMessage(value);
28
+ const canContinue = writer.write(serialized);
29
+ if (!canContinue) {
30
+ await new Promise<void>((resolve) => writer.once('drain', resolve));
31
+ }
32
+ } catch (error) {
33
+ console.error(
34
+ `[transport] failed to write framed message: ${error instanceof Error ? error.message : String(error)}`,
35
+ );
36
+ }
37
+ }
38
+
39
+ /** Parses all complete framed JSON-RPC requests currently present in a buffer. */
40
+ export function parseFramedMessages(buffer: Buffer): FramedMessageParseResult {
41
+ const messages: JsonRpcRequest[] = [];
42
+ let remainder = buffer;
43
+
44
+ while (true) {
45
+ const marker = remainder.indexOf('\r\n\r\n');
46
+ if (marker < 0) {
47
+ return { messages, remainder };
48
+ }
49
+
50
+ const header = remainder.subarray(0, marker).toString('utf8');
51
+ const contentLength = readContentLength(header);
52
+ if (contentLength == null) {
53
+ remainder = remainder.subarray(marker + 4);
54
+ continue;
55
+ }
56
+
57
+ const totalLength = marker + 4 + contentLength;
58
+ if (remainder.length < totalLength) {
59
+ return { messages, remainder };
60
+ }
61
+
62
+ const payload = remainder.subarray(marker + 4, totalLength).toString('utf8');
63
+ remainder = remainder.subarray(totalLength);
64
+
65
+ try {
66
+ messages.push(JSON.parse(payload) as JsonRpcRequest);
67
+ } catch {
68
+ continue;
69
+ }
70
+ }
71
+ }
72
+
73
+ /** Reads one `Content-Length` header value from a framed message header block. */
74
+ function readContentLength(header: string): number | null {
75
+ const headerLines = header.split(/\r?\n/g);
76
+
77
+ for (const line of headerLines) {
78
+ const separatorIndex = line.indexOf(':');
79
+ if (separatorIndex < 0) {
80
+ continue;
81
+ }
82
+
83
+ const name = line.slice(0, separatorIndex).trim().toLowerCase();
84
+ if (name !== 'content-length') {
85
+ continue;
86
+ }
87
+
88
+ const rawValue = Number(line.slice(separatorIndex + 1).trim());
89
+ return Number.isFinite(rawValue) && rawValue >= 0 ? rawValue : null;
90
+ }
91
+
92
+ return null;
93
+ }
@@ -0,0 +1,71 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type { JsonRpcId } from './protocol';
5
+
6
+ /** Enumerates the log levels accepted by the host log notification. */
7
+ export type HostLogLevel = 'error' | 'info';
8
+
9
+ /** Describes one `host.log` notification payload. */
10
+ export interface HostLogParams {
11
+ /** Stores the emitted log severity. */
12
+ level: HostLogLevel;
13
+ /** Stores the plugin log target name. */
14
+ target: string;
15
+ /** Stores the log message text. */
16
+ message: string;
17
+ }
18
+
19
+ /** Describes one `host.ui_notify` notification payload. */
20
+ export interface HostUiNotifyParams {
21
+ /** Stores the message shown by the host UI. */
22
+ message: string;
23
+ /** Stores an optional severity or category understood by the host. */
24
+ level?: string;
25
+ }
26
+
27
+ /** Describes one `host.status_set` notification payload. */
28
+ export interface HostStatusSetParams {
29
+ /** Stores the status text shown by the host. */
30
+ text: string;
31
+ }
32
+
33
+ /** Describes one `host.event_emit` notification payload. */
34
+ export interface HostEventEmitParams {
35
+ /** Stores the plugin-defined event name. */
36
+ name: string;
37
+ /** Stores the event payload forwarded to the host. */
38
+ payload?: Record<string, unknown>;
39
+ }
40
+
41
+ /** Describes one `vcs.event` notification payload. */
42
+ export interface VcsEventParams {
43
+ /** Stores the repository session id associated with the event. */
44
+ session_id: string;
45
+ /** Stores the originating request id when one exists. */
46
+ request_id: JsonRpcId | null;
47
+ /** Stores the event payload. */
48
+ event: Record<string, unknown>;
49
+ }
50
+
51
+ /** Describes the host helper API available inside delegate handlers. */
52
+ export interface PluginHost {
53
+ /** Emits a `host.log` notification. */
54
+ log(level: HostLogLevel, message: string): void;
55
+ /** Emits an informational `host.log` notification. */
56
+ info(message: string): void;
57
+ /** Emits an error `host.log` notification. */
58
+ error(message: string): void;
59
+ /** Emits a `host.ui_notify` notification. */
60
+ uiNotify(params: HostUiNotifyParams): void;
61
+ /** Emits a `host.status_set` notification. */
62
+ statusSet(params: HostStatusSetParams): void;
63
+ /** Emits a `host.event_emit` notification. */
64
+ emitEvent(params: HostEventEmitParams): void;
65
+ /** Emits a `vcs.event` notification. */
66
+ emitVcsEvent(
67
+ sessionId: string,
68
+ requestId: JsonRpcId | null,
69
+ event: Record<string, unknown>,
70
+ ): void;
71
+ }
@@ -0,0 +1,7 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ export * from './host';
5
+ export * from './plugin';
6
+ export * from './protocol';
7
+ export * from './vcs';