@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 +283 -0
- package/dist/index.d.cts +150 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +247 -0
- package/package.json +40 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|