@holon-run/uxc-daemon-client 0.12.3

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/dist/index.cjs ADDED
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DaemonRpcError: () => DaemonRpcError,
34
+ UxcDaemonClient: () => UxcDaemonClient
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var import_node_child_process = require("child_process");
38
+ var import_node_crypto = require("crypto");
39
+ var import_node_os = require("os");
40
+ var import_node_path = require("path");
41
+ var import_node_net = __toESM(require("net"), 1);
42
+ var import_node_util = require("util");
43
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
44
+ var JSONRPC_VERSION = "2.0";
45
+ var DaemonRpcError = class extends Error {
46
+ constructor(message, code, method) {
47
+ super(message);
48
+ this.code = code;
49
+ this.method = method;
50
+ this.name = "DaemonRpcError";
51
+ }
52
+ };
53
+ var UxcDaemonClient = class {
54
+ socketPath;
55
+ binaryPath;
56
+ autoStart;
57
+ connectTimeoutMs;
58
+ requestTimeoutMs;
59
+ env;
60
+ ensureDaemonPromise;
61
+ constructor(options = {}) {
62
+ this.socketPath = options.socketPath ?? defaultSocketPath(options.env);
63
+ this.binaryPath = options.binaryPath ?? "uxc";
64
+ this.autoStart = options.autoStart ?? true;
65
+ this.connectTimeoutMs = options.connectTimeoutMs ?? 2e3;
66
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 2e4;
67
+ this.env = { ...process.env, ...options.env };
68
+ }
69
+ async daemonStatus() {
70
+ return this.request("daemon.status");
71
+ }
72
+ async daemonSessions() {
73
+ return this.request("daemon.sessions");
74
+ }
75
+ async call(args) {
76
+ return this.request("runtime.invoke", {
77
+ request_id: requestId("call"),
78
+ endpoint: args.endpoint,
79
+ action: "execute",
80
+ operation_id: args.operation,
81
+ args: args.payload ?? null,
82
+ options: normalizeOptions(args.options)
83
+ });
84
+ }
85
+ async subscribeStart(args) {
86
+ return this.request("subscription.start", {
87
+ request_id: requestId("subscribe"),
88
+ endpoint: args.endpoint,
89
+ sink: args.sink ?? "memory:",
90
+ operation_id: args.operationId ?? null,
91
+ args: args.args ?? null,
92
+ resource_uri: args.resourceUri ?? null,
93
+ read_resource: args.readResource ?? false,
94
+ transport_hint: args.transportHint ?? null,
95
+ subprotocols: [],
96
+ initial_text_frames: [],
97
+ mode: args.mode ?? "stream",
98
+ poll_config: null,
99
+ ephemeral: args.ephemeral ?? (args.sink ?? "memory:") === "memory:",
100
+ options: normalizeOptions(args.options)
101
+ });
102
+ }
103
+ async subscribeList() {
104
+ return this.request("subscription.list");
105
+ }
106
+ async subscribeStatus(jobId) {
107
+ return this.request("subscription.status", { job_id: jobId });
108
+ }
109
+ async subscribeStop(jobId) {
110
+ return this.request("subscription.stop", { job_id: jobId });
111
+ }
112
+ async subscriptionEvents(args) {
113
+ return this.request("subscription.events", {
114
+ job_id: args.jobId,
115
+ after_seq: args.afterSeq ?? 0,
116
+ limit: args.limit ?? 100,
117
+ wait_ms: args.waitMs ?? 15e3
118
+ });
119
+ }
120
+ async *subscribeEvents(jobId, options = {}) {
121
+ let afterSeq = options.afterSeq ?? 0;
122
+ while (true) {
123
+ if (options.signal?.aborted) {
124
+ return;
125
+ }
126
+ const batch = await this.subscriptionEvents({
127
+ jobId,
128
+ afterSeq,
129
+ limit: options.limit,
130
+ waitMs: options.waitMs
131
+ });
132
+ for (const event of batch.events) {
133
+ afterSeq = event.seq;
134
+ yield event;
135
+ }
136
+ if (batch.status !== "running" && batch.events.length === 0) {
137
+ return;
138
+ }
139
+ if (batch.events.length === 0 && batch.status === "running") {
140
+ continue;
141
+ }
142
+ afterSeq = batch.next_after_seq;
143
+ }
144
+ }
145
+ async request(method, params) {
146
+ await this.ensureDaemon();
147
+ try {
148
+ return await this.requestOnce(method, params);
149
+ } catch (error) {
150
+ if (!this.autoStart || !isSocketError(error)) {
151
+ throw error;
152
+ }
153
+ await this.ensureDaemon(true);
154
+ return this.requestOnce(method, params);
155
+ }
156
+ }
157
+ async ensureDaemon(force = false) {
158
+ if (!this.autoStart) {
159
+ return;
160
+ }
161
+ if (!this.ensureDaemonPromise || force) {
162
+ this.ensureDaemonPromise = execFileAsync(this.binaryPath, ["daemon", "start"], {
163
+ env: this.env,
164
+ timeout: this.requestTimeoutMs
165
+ }).then(() => void 0);
166
+ }
167
+ await this.ensureDaemonPromise;
168
+ }
169
+ async requestOnce(method, params) {
170
+ const socket = import_node_net.default.createConnection(this.socketPath);
171
+ const response = await new Promise((resolve, reject) => {
172
+ let timer;
173
+ let buffer = Buffer.alloc(0);
174
+ let resolved = false;
175
+ const cleanup = () => {
176
+ if (timer) {
177
+ clearTimeout(timer);
178
+ }
179
+ socket.removeAllListeners();
180
+ socket.end();
181
+ socket.destroy();
182
+ };
183
+ const fail = (error) => {
184
+ if (resolved) {
185
+ return;
186
+ }
187
+ resolved = true;
188
+ cleanup();
189
+ reject(error);
190
+ };
191
+ timer = setTimeout(() => fail(new Error(`Timed out waiting for ${method}`)), this.requestTimeoutMs);
192
+ socket.once("error", fail);
193
+ socket.once("connect", () => {
194
+ const body = Buffer.from(
195
+ JSON.stringify({
196
+ jsonrpc: JSONRPC_VERSION,
197
+ id: 1,
198
+ method,
199
+ params: params ?? null
200
+ })
201
+ );
202
+ const header = Buffer.from(`Content-Length: ${body.length}\r
203
+ \r
204
+ `);
205
+ socket.write(Buffer.concat([header, body]));
206
+ });
207
+ socket.on("data", (chunk) => {
208
+ buffer = Buffer.concat([buffer, chunk]);
209
+ const parsed = tryParseFrame(buffer);
210
+ if (!parsed) {
211
+ return;
212
+ }
213
+ buffer = parsed.remaining;
214
+ resolved = true;
215
+ cleanup();
216
+ if (parsed.message.error) {
217
+ reject(
218
+ new DaemonRpcError(
219
+ parsed.message.error.message,
220
+ parsed.message.error.code,
221
+ method
222
+ )
223
+ );
224
+ return;
225
+ }
226
+ resolve(parsed.message.result);
227
+ });
228
+ });
229
+ return response;
230
+ }
231
+ };
232
+ function normalizeOptions(options) {
233
+ return {
234
+ inject_env: [],
235
+ no_cache: false,
236
+ refresh_schema: false,
237
+ daemon_exclusive: [],
238
+ ...options
239
+ };
240
+ }
241
+ function defaultSocketPath(env) {
242
+ if (env?.XDG_RUNTIME_DIR) {
243
+ return (0, import_node_path.join)(env.XDG_RUNTIME_DIR, "uxc", "uxc.sock");
244
+ }
245
+ if (env?.HOME) {
246
+ return (0, import_node_path.join)(env.HOME, ".uxc", "daemon", "uxc.sock");
247
+ }
248
+ const label = (0, import_node_crypto.createHash)("sha1").update(env?.USER ?? env?.USERNAME ?? "user").digest("hex").slice(0, 8);
249
+ return (0, import_node_path.join)((0, import_node_os.tmpdir)(), `uxc-${label}`, "daemon", "uxc.sock");
250
+ }
251
+ function requestId(prefix) {
252
+ return `${prefix}-${process.pid}-${Date.now()}`;
253
+ }
254
+ function isSocketError(error) {
255
+ return error instanceof Error && /connect|socket|ENOENT|ECONNREFUSED/.test(error.message);
256
+ }
257
+ function tryParseFrame(buffer) {
258
+ const marker = buffer.indexOf("\r\n\r\n");
259
+ if (marker === -1) {
260
+ return null;
261
+ }
262
+ const header = buffer.subarray(0, marker).toString("utf8");
263
+ const match = header.match(/Content-Length:\s*(\d+)/i);
264
+ if (!match) {
265
+ throw new Error("Missing Content-Length header");
266
+ }
267
+ const bodyLength = Number(match[1]);
268
+ const bodyStart = marker + 4;
269
+ if (buffer.length < bodyStart + bodyLength) {
270
+ return null;
271
+ }
272
+ const body = buffer.subarray(bodyStart, bodyStart + bodyLength).toString("utf8");
273
+ const message = JSON.parse(body);
274
+ return {
275
+ message,
276
+ remaining: Buffer.from(buffer.subarray(bodyStart + bodyLength))
277
+ };
278
+ }
279
+ // Annotate the CommonJS export names for ESM import in node:
280
+ 0 && (module.exports = {
281
+ DaemonRpcError,
282
+ UxcDaemonClient
283
+ });
@@ -0,0 +1,150 @@
1
+ interface RuntimeInvokeOptions {
2
+ auth?: string;
3
+ inject_env?: unknown[];
4
+ no_cache?: boolean;
5
+ cache_ttl?: number;
6
+ refresh_schema?: boolean;
7
+ schema_url?: string;
8
+ link_name?: string;
9
+ schema_mapping_file?: string;
10
+ daemon_exclusive?: string[];
11
+ daemon_idle_ttl?: number;
12
+ }
13
+ interface RuntimeInvokeResponse {
14
+ protocol: string;
15
+ endpoint: string;
16
+ kind: string;
17
+ operation?: string | null;
18
+ data: unknown;
19
+ duration_ms?: number | null;
20
+ meta: Record<string, unknown>;
21
+ }
22
+ interface SubscriptionEventEnvelope {
23
+ version: string;
24
+ job_id: string;
25
+ seq: number;
26
+ timestamp_unix: number;
27
+ protocol: string;
28
+ source_kind: string;
29
+ event_kind: string;
30
+ data?: unknown;
31
+ meta?: unknown;
32
+ }
33
+ interface SubscribeStartResponse {
34
+ job_id: string;
35
+ mode: "stream" | "poll";
36
+ protocol: string;
37
+ endpoint: string;
38
+ sink: string;
39
+ resource_uri?: string | null;
40
+ status: string;
41
+ }
42
+ interface SubscribeStopResponse {
43
+ job_id: string;
44
+ stopped: boolean;
45
+ }
46
+ interface SubscriptionJobView {
47
+ job_id: string;
48
+ mode: "stream" | "poll";
49
+ endpoint: string;
50
+ protocol: string;
51
+ sink: string;
52
+ resource_uri?: string | null;
53
+ status: string;
54
+ durable: boolean;
55
+ auto_resume: boolean;
56
+ resume_strategy: string;
57
+ created_at_unix: number;
58
+ started_at_unix?: number | null;
59
+ stopped_at_unix?: number | null;
60
+ last_event_at_unix?: number | null;
61
+ last_error?: string | null;
62
+ restart_count: number;
63
+ last_resume_at_unix?: number | null;
64
+ last_resume_error?: string | null;
65
+ reconnect_count: number;
66
+ written_events: number;
67
+ }
68
+ interface DaemonStatus {
69
+ running: boolean;
70
+ pid?: number | null;
71
+ socket: string;
72
+ version?: string | null;
73
+ started_at_unix?: number | null;
74
+ request_count: number;
75
+ mcp_stdio_sessions: number;
76
+ mcp_http_sessions: number;
77
+ mcp_reuse_hits: number;
78
+ log_file?: string | null;
79
+ }
80
+ interface SubscriptionEventsResponse {
81
+ job_id: string;
82
+ status: string;
83
+ events: SubscriptionEventEnvelope[];
84
+ next_after_seq: number;
85
+ has_more: boolean;
86
+ }
87
+ interface UxcDaemonClientOptions {
88
+ socketPath?: string;
89
+ binaryPath?: string;
90
+ autoStart?: boolean;
91
+ connectTimeoutMs?: number;
92
+ requestTimeoutMs?: number;
93
+ env?: NodeJS.ProcessEnv;
94
+ }
95
+ interface SubscribeStartArgs {
96
+ endpoint: string;
97
+ resourceUri?: string;
98
+ operationId?: string;
99
+ args?: Record<string, unknown>;
100
+ mode?: "stream" | "poll";
101
+ options?: RuntimeInvokeOptions;
102
+ sink?: `file:${string}` | "memory:";
103
+ ephemeral?: boolean;
104
+ readResource?: boolean;
105
+ transportHint?: "websocket" | "discord_gateway" | "slack_socket_mode" | "feishu_long_connection";
106
+ }
107
+ declare class DaemonRpcError extends Error {
108
+ readonly code: number;
109
+ readonly method: string;
110
+ constructor(message: string, code: number, method: string);
111
+ }
112
+ declare class UxcDaemonClient {
113
+ private readonly socketPath;
114
+ private readonly binaryPath;
115
+ private readonly autoStart;
116
+ private readonly connectTimeoutMs;
117
+ private readonly requestTimeoutMs;
118
+ private readonly env;
119
+ private ensureDaemonPromise?;
120
+ constructor(options?: UxcDaemonClientOptions);
121
+ daemonStatus(): Promise<DaemonStatus>;
122
+ daemonSessions(): Promise<unknown[]>;
123
+ call(args: {
124
+ endpoint: string;
125
+ operation: string;
126
+ payload?: Record<string, unknown>;
127
+ options?: RuntimeInvokeOptions;
128
+ }): Promise<RuntimeInvokeResponse>;
129
+ subscribeStart(args: SubscribeStartArgs): Promise<SubscribeStartResponse>;
130
+ subscribeList(): Promise<SubscriptionJobView[]>;
131
+ subscribeStatus(jobId: string): Promise<SubscriptionJobView>;
132
+ subscribeStop(jobId: string): Promise<SubscribeStopResponse>;
133
+ subscriptionEvents(args: {
134
+ jobId: string;
135
+ afterSeq?: number;
136
+ limit?: number;
137
+ waitMs?: number;
138
+ }): Promise<SubscriptionEventsResponse>;
139
+ subscribeEvents(jobId: string, options?: {
140
+ afterSeq?: number;
141
+ limit?: number;
142
+ waitMs?: number;
143
+ signal?: AbortSignal;
144
+ }): AsyncIterable<SubscriptionEventEnvelope>;
145
+ request<T>(method: string, params?: unknown): Promise<T>;
146
+ private ensureDaemon;
147
+ private requestOnce;
148
+ }
149
+
150
+ export { DaemonRpcError, type DaemonStatus, type RuntimeInvokeOptions, type RuntimeInvokeResponse, type SubscribeStartArgs, type SubscribeStartResponse, type SubscribeStopResponse, type SubscriptionEventEnvelope, type SubscriptionEventsResponse, type SubscriptionJobView, UxcDaemonClient, type UxcDaemonClientOptions };
@@ -0,0 +1,150 @@
1
+ interface RuntimeInvokeOptions {
2
+ auth?: string;
3
+ inject_env?: unknown[];
4
+ no_cache?: boolean;
5
+ cache_ttl?: number;
6
+ refresh_schema?: boolean;
7
+ schema_url?: string;
8
+ link_name?: string;
9
+ schema_mapping_file?: string;
10
+ daemon_exclusive?: string[];
11
+ daemon_idle_ttl?: number;
12
+ }
13
+ interface RuntimeInvokeResponse {
14
+ protocol: string;
15
+ endpoint: string;
16
+ kind: string;
17
+ operation?: string | null;
18
+ data: unknown;
19
+ duration_ms?: number | null;
20
+ meta: Record<string, unknown>;
21
+ }
22
+ interface SubscriptionEventEnvelope {
23
+ version: string;
24
+ job_id: string;
25
+ seq: number;
26
+ timestamp_unix: number;
27
+ protocol: string;
28
+ source_kind: string;
29
+ event_kind: string;
30
+ data?: unknown;
31
+ meta?: unknown;
32
+ }
33
+ interface SubscribeStartResponse {
34
+ job_id: string;
35
+ mode: "stream" | "poll";
36
+ protocol: string;
37
+ endpoint: string;
38
+ sink: string;
39
+ resource_uri?: string | null;
40
+ status: string;
41
+ }
42
+ interface SubscribeStopResponse {
43
+ job_id: string;
44
+ stopped: boolean;
45
+ }
46
+ interface SubscriptionJobView {
47
+ job_id: string;
48
+ mode: "stream" | "poll";
49
+ endpoint: string;
50
+ protocol: string;
51
+ sink: string;
52
+ resource_uri?: string | null;
53
+ status: string;
54
+ durable: boolean;
55
+ auto_resume: boolean;
56
+ resume_strategy: string;
57
+ created_at_unix: number;
58
+ started_at_unix?: number | null;
59
+ stopped_at_unix?: number | null;
60
+ last_event_at_unix?: number | null;
61
+ last_error?: string | null;
62
+ restart_count: number;
63
+ last_resume_at_unix?: number | null;
64
+ last_resume_error?: string | null;
65
+ reconnect_count: number;
66
+ written_events: number;
67
+ }
68
+ interface DaemonStatus {
69
+ running: boolean;
70
+ pid?: number | null;
71
+ socket: string;
72
+ version?: string | null;
73
+ started_at_unix?: number | null;
74
+ request_count: number;
75
+ mcp_stdio_sessions: number;
76
+ mcp_http_sessions: number;
77
+ mcp_reuse_hits: number;
78
+ log_file?: string | null;
79
+ }
80
+ interface SubscriptionEventsResponse {
81
+ job_id: string;
82
+ status: string;
83
+ events: SubscriptionEventEnvelope[];
84
+ next_after_seq: number;
85
+ has_more: boolean;
86
+ }
87
+ interface UxcDaemonClientOptions {
88
+ socketPath?: string;
89
+ binaryPath?: string;
90
+ autoStart?: boolean;
91
+ connectTimeoutMs?: number;
92
+ requestTimeoutMs?: number;
93
+ env?: NodeJS.ProcessEnv;
94
+ }
95
+ interface SubscribeStartArgs {
96
+ endpoint: string;
97
+ resourceUri?: string;
98
+ operationId?: string;
99
+ args?: Record<string, unknown>;
100
+ mode?: "stream" | "poll";
101
+ options?: RuntimeInvokeOptions;
102
+ sink?: `file:${string}` | "memory:";
103
+ ephemeral?: boolean;
104
+ readResource?: boolean;
105
+ transportHint?: "websocket" | "discord_gateway" | "slack_socket_mode" | "feishu_long_connection";
106
+ }
107
+ declare class DaemonRpcError extends Error {
108
+ readonly code: number;
109
+ readonly method: string;
110
+ constructor(message: string, code: number, method: string);
111
+ }
112
+ declare class UxcDaemonClient {
113
+ private readonly socketPath;
114
+ private readonly binaryPath;
115
+ private readonly autoStart;
116
+ private readonly connectTimeoutMs;
117
+ private readonly requestTimeoutMs;
118
+ private readonly env;
119
+ private ensureDaemonPromise?;
120
+ constructor(options?: UxcDaemonClientOptions);
121
+ daemonStatus(): Promise<DaemonStatus>;
122
+ daemonSessions(): Promise<unknown[]>;
123
+ call(args: {
124
+ endpoint: string;
125
+ operation: string;
126
+ payload?: Record<string, unknown>;
127
+ options?: RuntimeInvokeOptions;
128
+ }): Promise<RuntimeInvokeResponse>;
129
+ subscribeStart(args: SubscribeStartArgs): Promise<SubscribeStartResponse>;
130
+ subscribeList(): Promise<SubscriptionJobView[]>;
131
+ subscribeStatus(jobId: string): Promise<SubscriptionJobView>;
132
+ subscribeStop(jobId: string): Promise<SubscribeStopResponse>;
133
+ subscriptionEvents(args: {
134
+ jobId: string;
135
+ afterSeq?: number;
136
+ limit?: number;
137
+ waitMs?: number;
138
+ }): Promise<SubscriptionEventsResponse>;
139
+ subscribeEvents(jobId: string, options?: {
140
+ afterSeq?: number;
141
+ limit?: number;
142
+ waitMs?: number;
143
+ signal?: AbortSignal;
144
+ }): AsyncIterable<SubscriptionEventEnvelope>;
145
+ request<T>(method: string, params?: unknown): Promise<T>;
146
+ private ensureDaemon;
147
+ private requestOnce;
148
+ }
149
+
150
+ export { DaemonRpcError, type DaemonStatus, type RuntimeInvokeOptions, type RuntimeInvokeResponse, type SubscribeStartArgs, type SubscribeStartResponse, type SubscribeStopResponse, type SubscriptionEventEnvelope, type SubscriptionEventsResponse, type SubscriptionJobView, UxcDaemonClient, type UxcDaemonClientOptions };
package/dist/index.js ADDED
@@ -0,0 +1,247 @@
1
+ // src/index.ts
2
+ import { execFile } from "child_process";
3
+ import { createHash } from "crypto";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
6
+ import net from "net";
7
+ import { promisify } from "util";
8
+ var execFileAsync = promisify(execFile);
9
+ var JSONRPC_VERSION = "2.0";
10
+ var DaemonRpcError = class extends Error {
11
+ constructor(message, code, method) {
12
+ super(message);
13
+ this.code = code;
14
+ this.method = method;
15
+ this.name = "DaemonRpcError";
16
+ }
17
+ };
18
+ var UxcDaemonClient = class {
19
+ socketPath;
20
+ binaryPath;
21
+ autoStart;
22
+ connectTimeoutMs;
23
+ requestTimeoutMs;
24
+ env;
25
+ ensureDaemonPromise;
26
+ constructor(options = {}) {
27
+ this.socketPath = options.socketPath ?? defaultSocketPath(options.env);
28
+ this.binaryPath = options.binaryPath ?? "uxc";
29
+ this.autoStart = options.autoStart ?? true;
30
+ this.connectTimeoutMs = options.connectTimeoutMs ?? 2e3;
31
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 2e4;
32
+ this.env = { ...process.env, ...options.env };
33
+ }
34
+ async daemonStatus() {
35
+ return this.request("daemon.status");
36
+ }
37
+ async daemonSessions() {
38
+ return this.request("daemon.sessions");
39
+ }
40
+ async call(args) {
41
+ return this.request("runtime.invoke", {
42
+ request_id: requestId("call"),
43
+ endpoint: args.endpoint,
44
+ action: "execute",
45
+ operation_id: args.operation,
46
+ args: args.payload ?? null,
47
+ options: normalizeOptions(args.options)
48
+ });
49
+ }
50
+ async subscribeStart(args) {
51
+ return this.request("subscription.start", {
52
+ request_id: requestId("subscribe"),
53
+ endpoint: args.endpoint,
54
+ sink: args.sink ?? "memory:",
55
+ operation_id: args.operationId ?? null,
56
+ args: args.args ?? null,
57
+ resource_uri: args.resourceUri ?? null,
58
+ read_resource: args.readResource ?? false,
59
+ transport_hint: args.transportHint ?? null,
60
+ subprotocols: [],
61
+ initial_text_frames: [],
62
+ mode: args.mode ?? "stream",
63
+ poll_config: null,
64
+ ephemeral: args.ephemeral ?? (args.sink ?? "memory:") === "memory:",
65
+ options: normalizeOptions(args.options)
66
+ });
67
+ }
68
+ async subscribeList() {
69
+ return this.request("subscription.list");
70
+ }
71
+ async subscribeStatus(jobId) {
72
+ return this.request("subscription.status", { job_id: jobId });
73
+ }
74
+ async subscribeStop(jobId) {
75
+ return this.request("subscription.stop", { job_id: jobId });
76
+ }
77
+ async subscriptionEvents(args) {
78
+ return this.request("subscription.events", {
79
+ job_id: args.jobId,
80
+ after_seq: args.afterSeq ?? 0,
81
+ limit: args.limit ?? 100,
82
+ wait_ms: args.waitMs ?? 15e3
83
+ });
84
+ }
85
+ async *subscribeEvents(jobId, options = {}) {
86
+ let afterSeq = options.afterSeq ?? 0;
87
+ while (true) {
88
+ if (options.signal?.aborted) {
89
+ return;
90
+ }
91
+ const batch = await this.subscriptionEvents({
92
+ jobId,
93
+ afterSeq,
94
+ limit: options.limit,
95
+ waitMs: options.waitMs
96
+ });
97
+ for (const event of batch.events) {
98
+ afterSeq = event.seq;
99
+ yield event;
100
+ }
101
+ if (batch.status !== "running" && batch.events.length === 0) {
102
+ return;
103
+ }
104
+ if (batch.events.length === 0 && batch.status === "running") {
105
+ continue;
106
+ }
107
+ afterSeq = batch.next_after_seq;
108
+ }
109
+ }
110
+ async request(method, params) {
111
+ await this.ensureDaemon();
112
+ try {
113
+ return await this.requestOnce(method, params);
114
+ } catch (error) {
115
+ if (!this.autoStart || !isSocketError(error)) {
116
+ throw error;
117
+ }
118
+ await this.ensureDaemon(true);
119
+ return this.requestOnce(method, params);
120
+ }
121
+ }
122
+ async ensureDaemon(force = false) {
123
+ if (!this.autoStart) {
124
+ return;
125
+ }
126
+ if (!this.ensureDaemonPromise || force) {
127
+ this.ensureDaemonPromise = execFileAsync(this.binaryPath, ["daemon", "start"], {
128
+ env: this.env,
129
+ timeout: this.requestTimeoutMs
130
+ }).then(() => void 0);
131
+ }
132
+ await this.ensureDaemonPromise;
133
+ }
134
+ async requestOnce(method, params) {
135
+ const socket = net.createConnection(this.socketPath);
136
+ const response = await new Promise((resolve, reject) => {
137
+ let timer;
138
+ let buffer = Buffer.alloc(0);
139
+ let resolved = false;
140
+ const cleanup = () => {
141
+ if (timer) {
142
+ clearTimeout(timer);
143
+ }
144
+ socket.removeAllListeners();
145
+ socket.end();
146
+ socket.destroy();
147
+ };
148
+ const fail = (error) => {
149
+ if (resolved) {
150
+ return;
151
+ }
152
+ resolved = true;
153
+ cleanup();
154
+ reject(error);
155
+ };
156
+ timer = setTimeout(() => fail(new Error(`Timed out waiting for ${method}`)), this.requestTimeoutMs);
157
+ socket.once("error", fail);
158
+ socket.once("connect", () => {
159
+ const body = Buffer.from(
160
+ JSON.stringify({
161
+ jsonrpc: JSONRPC_VERSION,
162
+ id: 1,
163
+ method,
164
+ params: params ?? null
165
+ })
166
+ );
167
+ const header = Buffer.from(`Content-Length: ${body.length}\r
168
+ \r
169
+ `);
170
+ socket.write(Buffer.concat([header, body]));
171
+ });
172
+ socket.on("data", (chunk) => {
173
+ buffer = Buffer.concat([buffer, chunk]);
174
+ const parsed = tryParseFrame(buffer);
175
+ if (!parsed) {
176
+ return;
177
+ }
178
+ buffer = parsed.remaining;
179
+ resolved = true;
180
+ cleanup();
181
+ if (parsed.message.error) {
182
+ reject(
183
+ new DaemonRpcError(
184
+ parsed.message.error.message,
185
+ parsed.message.error.code,
186
+ method
187
+ )
188
+ );
189
+ return;
190
+ }
191
+ resolve(parsed.message.result);
192
+ });
193
+ });
194
+ return response;
195
+ }
196
+ };
197
+ function normalizeOptions(options) {
198
+ return {
199
+ inject_env: [],
200
+ no_cache: false,
201
+ refresh_schema: false,
202
+ daemon_exclusive: [],
203
+ ...options
204
+ };
205
+ }
206
+ function defaultSocketPath(env) {
207
+ if (env?.XDG_RUNTIME_DIR) {
208
+ return join(env.XDG_RUNTIME_DIR, "uxc", "uxc.sock");
209
+ }
210
+ if (env?.HOME) {
211
+ return join(env.HOME, ".uxc", "daemon", "uxc.sock");
212
+ }
213
+ const label = createHash("sha1").update(env?.USER ?? env?.USERNAME ?? "user").digest("hex").slice(0, 8);
214
+ return join(tmpdir(), `uxc-${label}`, "daemon", "uxc.sock");
215
+ }
216
+ function requestId(prefix) {
217
+ return `${prefix}-${process.pid}-${Date.now()}`;
218
+ }
219
+ function isSocketError(error) {
220
+ return error instanceof Error && /connect|socket|ENOENT|ECONNREFUSED/.test(error.message);
221
+ }
222
+ function tryParseFrame(buffer) {
223
+ const marker = buffer.indexOf("\r\n\r\n");
224
+ if (marker === -1) {
225
+ return null;
226
+ }
227
+ const header = buffer.subarray(0, marker).toString("utf8");
228
+ const match = header.match(/Content-Length:\s*(\d+)/i);
229
+ if (!match) {
230
+ throw new Error("Missing Content-Length header");
231
+ }
232
+ const bodyLength = Number(match[1]);
233
+ const bodyStart = marker + 4;
234
+ if (buffer.length < bodyStart + bodyLength) {
235
+ return null;
236
+ }
237
+ const body = buffer.subarray(bodyStart, bodyStart + bodyLength).toString("utf8");
238
+ const message = JSON.parse(body);
239
+ return {
240
+ message,
241
+ remaining: Buffer.from(buffer.subarray(bodyStart + bodyLength))
242
+ };
243
+ }
244
+ export {
245
+ DaemonRpcError,
246
+ UxcDaemonClient
247
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@holon-run/uxc-daemon-client",
3
+ "version": "0.12.3",
4
+ "description": "Thin Node.js client for UXC daemon-backed operations",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/holon-run/uxc.git",
9
+ "directory": "packages/uxc-daemon-client"
10
+ },
11
+ "homepage": "https://github.com/holon-run/uxc",
12
+ "type": "module",
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup src/index.ts --format esm,cjs --dts",
31
+ "test": "vitest run"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^24.5.0",
35
+ "tsup": "^8.3.5",
36
+ "typescript": "^5.9.2",
37
+ "vitest": "^3.2.4",
38
+ "ws": "^8.18.3"
39
+ }
40
+ }