@rivus/agent 0.8.6 → 0.10.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/dist/acp.d.ts +31 -1
- package/dist/acp.js +188 -11
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +43 -33
- package/dist/pi.js +1 -1
- package/dist/rivus-daemon-cli.js +2 -2
- package/examples/acp-stdio-proxy.mjs +3 -0
- package/package.json +1 -1
package/dist/acp.d.ts
CHANGED
|
@@ -77,6 +77,23 @@ interface AcpAgentServerOptions {
|
|
|
77
77
|
declare function createAcpAgentServer(options: AcpAgentServerOptions): acp.AgentApp;
|
|
78
78
|
declare function serveAcpAgentOnStdio(app: acp.AgentApp): Promise<void>;
|
|
79
79
|
//#endregion
|
|
80
|
+
//#region src/infrastructure/acp/acp-session-store.d.ts
|
|
81
|
+
interface AcpSessionRecord {
|
|
82
|
+
readonly sessionId: string;
|
|
83
|
+
}
|
|
84
|
+
interface AcpSessionStore {
|
|
85
|
+
load(sessionKey: string): Promise<AcpSessionRecord | undefined>;
|
|
86
|
+
save(sessionKey: string, record: AcpSessionRecord): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
interface JsonAcpSessionStoreOptions {
|
|
89
|
+
readonly filePath: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A small deployment-owned store for ACP provider session identities.
|
|
93
|
+
* The file contains no prompts or credentials, only session-key bindings.
|
|
94
|
+
*/
|
|
95
|
+
declare function createJsonAcpSessionStore(options: JsonAcpSessionStoreOptions): AcpSessionStore;
|
|
96
|
+
//#endregion
|
|
80
97
|
//#region src/infrastructure/acp/acp-stdio-agent-loop.d.ts
|
|
81
98
|
interface AcpMcpServerEnvironmentVariable {
|
|
82
99
|
readonly name: string;
|
|
@@ -97,6 +114,8 @@ interface AcpStdioAgentLoopOptions {
|
|
|
97
114
|
readonly mcpServers?: (input: AgentLoopInput) => ReadonlyArray<AcpMcpServer>;
|
|
98
115
|
readonly onStderr?: (text: string) => void;
|
|
99
116
|
readonly permissionPolicy?: AcpSessionPermissionPolicy;
|
|
117
|
+
/** Persist provider session IDs so a restarted ACP child can load/resume them. */
|
|
118
|
+
readonly sessionStore?: AcpSessionStore;
|
|
100
119
|
readonly terminationTimeoutMs?: number;
|
|
101
120
|
readonly workingDirectory: string;
|
|
102
121
|
}
|
|
@@ -105,5 +124,16 @@ interface AcpStdioAgentLoopHandle {
|
|
|
105
124
|
dispose(): Promise<void>;
|
|
106
125
|
}
|
|
107
126
|
declare function createAcpStdioAgentLoop(options: AcpStdioAgentLoopOptions): AcpStdioAgentLoopHandle;
|
|
127
|
+
declare class AcpSessionResumeUnavailable extends Error {
|
|
128
|
+
readonly sessionId: string;
|
|
129
|
+
readonly name = "AcpSessionResumeUnavailable";
|
|
130
|
+
constructor(sessionId: string);
|
|
131
|
+
}
|
|
132
|
+
declare class AcpSessionResumeFailed extends Error {
|
|
133
|
+
readonly sessionId: string;
|
|
134
|
+
readonly cause: unknown;
|
|
135
|
+
readonly name = "AcpSessionResumeFailed";
|
|
136
|
+
constructor(sessionId: string, cause: unknown);
|
|
137
|
+
}
|
|
108
138
|
//#endregion
|
|
109
|
-
export { type AcpAgentLoopOptions, type AcpAgentServerOptions, type AcpAgentSession, type AcpPermissionBridge, type AcpPermissionDecision, type AcpPermissionOption, type AcpPermissionPolicy, type AcpPermissionRequest, type AcpPermissionSelection, type AcpPermissionToolCall, type AcpPromptResult, type AcpSessionPermissionContext, type AcpSessionPermissionPolicy, type AcpSessionUpdate, type AcpStdioAgentLoopHandle, type AcpStdioAgentLoopOptions, type AcpToolCallStatus, createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, decideAcpPermission, serveAcpAgentOnStdio };
|
|
139
|
+
export { type AcpAgentLoopOptions, type AcpAgentServerOptions, type AcpAgentSession, type AcpPermissionBridge, type AcpPermissionDecision, type AcpPermissionOption, type AcpPermissionPolicy, type AcpPermissionRequest, type AcpPermissionSelection, type AcpPermissionToolCall, type AcpPromptResult, type AcpSessionPermissionContext, type AcpSessionPermissionPolicy, type AcpSessionRecord, AcpSessionResumeFailed, AcpSessionResumeUnavailable, type AcpSessionStore, type AcpSessionUpdate, type AcpStdioAgentLoopHandle, type AcpStdioAgentLoopOptions, type AcpToolCallStatus, type JsonAcpSessionStoreOptions, createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, createJsonAcpSessionStore, decideAcpPermission, serveAcpAgentOnStdio };
|
package/dist/acp.js
CHANGED
|
@@ -4,6 +4,8 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { Readable, Writable } from "node:stream";
|
|
5
5
|
import * as acp from "@agentclientprotocol/sdk";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname } from "node:path";
|
|
7
9
|
//#region src/infrastructure/acp/acp-agent-loop.ts
|
|
8
10
|
function createAcpAgentLoop(options) {
|
|
9
11
|
return createAsyncIterableAgentLoop({ run: (input) => runAcpSession(input, options) });
|
|
@@ -109,10 +111,10 @@ function mapAcpSessionUpdate(update, tools) {
|
|
|
109
111
|
})];
|
|
110
112
|
}
|
|
111
113
|
function readTextContent(content) {
|
|
112
|
-
if (!isRecord(content) || content.type !== "text" || typeof content.text !== "string") return void 0;
|
|
114
|
+
if (!isRecord$1(content) || content.type !== "text" || typeof content.text !== "string") return void 0;
|
|
113
115
|
return content.text;
|
|
114
116
|
}
|
|
115
|
-
function isRecord(value) {
|
|
117
|
+
function isRecord$1(value) {
|
|
116
118
|
return typeof value === "object" && value !== null;
|
|
117
119
|
}
|
|
118
120
|
//#endregion
|
|
@@ -252,17 +254,48 @@ async function decideAcpPermission(request, policy) {
|
|
|
252
254
|
//#endregion
|
|
253
255
|
//#region src/infrastructure/acp/acp-stdio-agent-loop.ts
|
|
254
256
|
async function buildAcpSession(processConnection, options, input) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
+
const mcpServers = createMcpServers(options, input);
|
|
258
|
+
const persisted = await options.sessionStore?.load(input.sessionKey);
|
|
259
|
+
if (persisted) return restoreAcpSession(processConnection, options, persisted.sessionId, mcpServers);
|
|
260
|
+
const session = await processConnection.connection.agent.buildSession({
|
|
261
|
+
cwd: options.workingDirectory,
|
|
262
|
+
mcpServers
|
|
263
|
+
}).start();
|
|
264
|
+
await options.sessionStore?.save(input.sessionKey, { sessionId: session.sessionId });
|
|
265
|
+
return session;
|
|
266
|
+
}
|
|
267
|
+
function createMcpServers(options, input) {
|
|
268
|
+
return (options.mcpServers?.(input) ?? []).map((server) => ({
|
|
257
269
|
args: [...server.args],
|
|
258
270
|
command: server.command,
|
|
259
271
|
env: server.env.map(({ name, value }) => ({
|
|
260
272
|
name,
|
|
261
273
|
value
|
|
262
274
|
})),
|
|
263
|
-
name: server.name
|
|
264
|
-
|
|
265
|
-
|
|
275
|
+
name: server.name,
|
|
276
|
+
type: "stdio"
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
async function restoreAcpSession(processConnection, options, sessionId, mcpServers) {
|
|
280
|
+
const session = new ResumedAcpSession(processConnection, sessionId);
|
|
281
|
+
try {
|
|
282
|
+
if (processConnection.agentCapabilities?.loadSession) await processConnection.connection.agent.request(acp.methods.agent.session.load, {
|
|
283
|
+
cwd: options.workingDirectory,
|
|
284
|
+
mcpServers,
|
|
285
|
+
sessionId
|
|
286
|
+
});
|
|
287
|
+
else if (processConnection.agentCapabilities?.sessionCapabilities?.resume) await processConnection.connection.agent.request(acp.methods.agent.session.resume, {
|
|
288
|
+
cwd: options.workingDirectory,
|
|
289
|
+
mcpServers,
|
|
290
|
+
sessionId
|
|
291
|
+
});
|
|
292
|
+
else throw new AcpSessionResumeUnavailable(sessionId);
|
|
293
|
+
session.clearReplay();
|
|
294
|
+
return session;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
session.dispose();
|
|
297
|
+
throw new AcpSessionResumeFailed(sessionId, error);
|
|
298
|
+
}
|
|
266
299
|
}
|
|
267
300
|
function createAcpStdioAgentLoop(options) {
|
|
268
301
|
let pendingConnection;
|
|
@@ -291,6 +324,7 @@ function createAcpStdioAgentLoop(options) {
|
|
|
291
324
|
sessionKeys.clear();
|
|
292
325
|
for (const session of processConnection.sessions.values()) session.dispose();
|
|
293
326
|
processConnection.sessions.clear();
|
|
327
|
+
processConnection.sessionUpdateHandlers.clear();
|
|
294
328
|
terminateChild(processConnection.child, options.terminationTimeoutMs);
|
|
295
329
|
});
|
|
296
330
|
}, () => {
|
|
@@ -312,6 +346,7 @@ function createAcpStdioAgentLoop(options) {
|
|
|
312
346
|
if (processConnection) {
|
|
313
347
|
for (const session of processConnection.sessions.values()) session.dispose();
|
|
314
348
|
processConnection.sessions.clear();
|
|
349
|
+
processConnection.sessionUpdateHandlers.clear();
|
|
315
350
|
processConnection.connection.close();
|
|
316
351
|
}
|
|
317
352
|
sessionKeys.clear();
|
|
@@ -330,7 +365,7 @@ function createAcpStdioAgentLoop(options) {
|
|
|
330
365
|
current.dispose();
|
|
331
366
|
processConnection.sessions.delete(input.sessionKey);
|
|
332
367
|
}
|
|
333
|
-
const session = new SdkAcpAgentSession(await buildAcpSession(processConnection, options, input), processConnection.connection.agent);
|
|
368
|
+
const session = new SdkAcpAgentSession(await buildAcpSession(processConnection, options, input), processConnection.connection.agent, options.sessionStore !== void 0);
|
|
334
369
|
sessionKeys.set(session.sessionId, input.sessionKey);
|
|
335
370
|
processConnection.sessions.set(input.sessionKey, session);
|
|
336
371
|
return session;
|
|
@@ -341,10 +376,12 @@ function createAcpStdioAgentLoop(options) {
|
|
|
341
376
|
var SdkAcpAgentSession = class {
|
|
342
377
|
session;
|
|
343
378
|
agent;
|
|
379
|
+
preserveOnCancel;
|
|
344
380
|
reusable = true;
|
|
345
|
-
constructor(session, agent) {
|
|
381
|
+
constructor(session, agent, preserveOnCancel) {
|
|
346
382
|
this.session = session;
|
|
347
383
|
this.agent = agent;
|
|
384
|
+
this.preserveOnCancel = preserveOnCancel;
|
|
348
385
|
}
|
|
349
386
|
get sessionId() {
|
|
350
387
|
return this.session.sessionId;
|
|
@@ -353,7 +390,7 @@ var SdkAcpAgentSession = class {
|
|
|
353
390
|
return this.reusable;
|
|
354
391
|
}
|
|
355
392
|
cancel() {
|
|
356
|
-
this.reusable = false;
|
|
393
|
+
if (!this.preserveOnCancel) this.reusable = false;
|
|
357
394
|
return this.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.session.sessionId });
|
|
358
395
|
}
|
|
359
396
|
dispose() {
|
|
@@ -369,6 +406,88 @@ var SdkAcpAgentSession = class {
|
|
|
369
406
|
}
|
|
370
407
|
}
|
|
371
408
|
};
|
|
409
|
+
var ResumedAcpSession = class {
|
|
410
|
+
processConnection;
|
|
411
|
+
sessionId;
|
|
412
|
+
updates = [];
|
|
413
|
+
waiters = [];
|
|
414
|
+
disposed = false;
|
|
415
|
+
failure;
|
|
416
|
+
constructor(processConnection, sessionId) {
|
|
417
|
+
this.processConnection = processConnection;
|
|
418
|
+
this.sessionId = sessionId;
|
|
419
|
+
processConnection.sessionUpdateHandlers.set(sessionId, (notification) => {
|
|
420
|
+
this.enqueue({
|
|
421
|
+
kind: "session_update",
|
|
422
|
+
update: notification.update
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
clearReplay() {
|
|
427
|
+
this.updates.splice(0);
|
|
428
|
+
}
|
|
429
|
+
dispose() {
|
|
430
|
+
if (this.disposed) return;
|
|
431
|
+
this.disposed = true;
|
|
432
|
+
this.processConnection.sessionUpdateHandlers.delete(this.sessionId);
|
|
433
|
+
const error = /* @__PURE__ */ new Error(`ACP session ${this.sessionId} observer disposed`);
|
|
434
|
+
for (const waiter of this.waiters.splice(0)) waiter.reject(error);
|
|
435
|
+
this.updates.splice(0);
|
|
436
|
+
}
|
|
437
|
+
nextUpdate() {
|
|
438
|
+
if (this.updates.length > 0) return Promise.resolve(this.updates.shift());
|
|
439
|
+
if (this.failure !== void 0) return Promise.reject(this.failure);
|
|
440
|
+
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error(`ACP session ${this.sessionId} observer disposed`));
|
|
441
|
+
return new Promise((resolve, reject) => this.waiters.push({
|
|
442
|
+
reject,
|
|
443
|
+
resolve
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
prompt(text) {
|
|
447
|
+
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error(`ACP session ${this.sessionId} observer disposed`));
|
|
448
|
+
const response = this.processConnection.connection.agent.request(acp.methods.agent.session.prompt, {
|
|
449
|
+
prompt: [{
|
|
450
|
+
text,
|
|
451
|
+
type: "text"
|
|
452
|
+
}],
|
|
453
|
+
sessionId: this.sessionId
|
|
454
|
+
});
|
|
455
|
+
response.then((result) => this.enqueue({
|
|
456
|
+
kind: "stop",
|
|
457
|
+
stopReason: result.stopReason
|
|
458
|
+
}), (error) => this.fail(error));
|
|
459
|
+
return response;
|
|
460
|
+
}
|
|
461
|
+
enqueue(message) {
|
|
462
|
+
if (this.disposed) return;
|
|
463
|
+
const waiter = this.waiters.shift();
|
|
464
|
+
if (waiter) waiter.resolve(message);
|
|
465
|
+
else this.updates.push(message);
|
|
466
|
+
}
|
|
467
|
+
fail(error) {
|
|
468
|
+
if (this.failure !== void 0 || this.disposed) return;
|
|
469
|
+
this.failure = error;
|
|
470
|
+
for (const waiter of this.waiters.splice(0)) waiter.reject(error);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
var AcpSessionResumeUnavailable = class extends Error {
|
|
474
|
+
sessionId;
|
|
475
|
+
name = "AcpSessionResumeUnavailable";
|
|
476
|
+
constructor(sessionId) {
|
|
477
|
+
super(`ACP Agent cannot load or resume persisted session ${sessionId}`);
|
|
478
|
+
this.sessionId = sessionId;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var AcpSessionResumeFailed = class extends Error {
|
|
482
|
+
sessionId;
|
|
483
|
+
cause;
|
|
484
|
+
name = "AcpSessionResumeFailed";
|
|
485
|
+
constructor(sessionId, cause) {
|
|
486
|
+
super(`ACP Agent failed to restore persisted session ${sessionId}`);
|
|
487
|
+
this.sessionId = sessionId;
|
|
488
|
+
this.cause = cause;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
372
491
|
async function openConnection(options, sessionKeys, onSpawn) {
|
|
373
492
|
const child = spawn(options.command, [...options.arguments ?? []], {
|
|
374
493
|
cwd: options.workingDirectory,
|
|
@@ -383,9 +502,12 @@ async function openConnection(options, sessionKeys, onSpawn) {
|
|
|
383
502
|
child.stderr.setEncoding("utf8");
|
|
384
503
|
if (options.onStderr) child.stderr.on("data", options.onStderr);
|
|
385
504
|
else child.stderr.resume();
|
|
505
|
+
const sessionUpdateHandlers = /* @__PURE__ */ new Map();
|
|
386
506
|
const app = acp.client({ name: options.clientName ?? "rivus" }).onRequest(acp.methods.client.session.requestPermission, async ({ params }) => {
|
|
387
507
|
const sessionKey = sessionKeys.get(params.sessionId);
|
|
388
508
|
return { outcome: await decideAcpPermission(toPermissionRequest(params), sessionKey && options.permissionPolicy ? (request) => options.permissionPolicy?.(request, { sessionKey }) : void 0) };
|
|
509
|
+
}).onNotification(acp.methods.client.session.update, ({ params }) => {
|
|
510
|
+
sessionUpdateHandlers.get(params.sessionId)?.(params);
|
|
389
511
|
});
|
|
390
512
|
const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
391
513
|
const connection = app.connect(stream);
|
|
@@ -400,8 +522,10 @@ async function openConnection(options, sessionKeys, onSpawn) {
|
|
|
400
522
|
}), options.initializationTimeoutMs ?? 1e4, "ACP initialization timed out");
|
|
401
523
|
if (initialized.protocolVersion !== acp.PROTOCOL_VERSION) throw new Error(`ACP protocol version ${initialized.protocolVersion} is not supported`);
|
|
402
524
|
return {
|
|
525
|
+
...initialized.agentCapabilities ? { agentCapabilities: initialized.agentCapabilities } : {},
|
|
403
526
|
child,
|
|
404
527
|
connection,
|
|
528
|
+
sessionUpdateHandlers,
|
|
405
529
|
sessions: /* @__PURE__ */ new Map()
|
|
406
530
|
};
|
|
407
531
|
} catch (error) {
|
|
@@ -456,4 +580,57 @@ function withTimeout(promise, timeoutMs, message) {
|
|
|
456
580
|
});
|
|
457
581
|
}
|
|
458
582
|
//#endregion
|
|
459
|
-
|
|
583
|
+
//#region src/infrastructure/acp/acp-session-store.ts
|
|
584
|
+
/**
|
|
585
|
+
* A small deployment-owned store for ACP provider session identities.
|
|
586
|
+
* The file contains no prompts or credentials, only session-key bindings.
|
|
587
|
+
*/
|
|
588
|
+
function createJsonAcpSessionStore(options) {
|
|
589
|
+
let recordsPromise;
|
|
590
|
+
let writeChain = Promise.resolve();
|
|
591
|
+
const readRecords = async () => {
|
|
592
|
+
try {
|
|
593
|
+
const raw = await readFile(options.filePath, "utf8");
|
|
594
|
+
const parsed = JSON.parse(raw);
|
|
595
|
+
if (!isRecord(parsed)) throw new Error("ACP session store must contain an object");
|
|
596
|
+
const records = /* @__PURE__ */ new Map();
|
|
597
|
+
for (const [sessionKey, value] of Object.entries(parsed)) {
|
|
598
|
+
if (!isRecord(value) || typeof value.sessionId !== "string" || value.sessionId.trim() === "") throw new Error(`invalid ACP session record for ${sessionKey}`);
|
|
599
|
+
records.set(sessionKey, { sessionId: value.sessionId });
|
|
600
|
+
}
|
|
601
|
+
return records;
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (isMissingFile(error)) return /* @__PURE__ */ new Map();
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
const records = async () => {
|
|
608
|
+
recordsPromise ??= readRecords();
|
|
609
|
+
return recordsPromise;
|
|
610
|
+
};
|
|
611
|
+
const persist = async (value) => {
|
|
612
|
+
const temporaryPath = `${options.filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
613
|
+
await mkdir(dirname(options.filePath), { recursive: true });
|
|
614
|
+
await writeFile(temporaryPath, `${JSON.stringify(Object.fromEntries(value), null, 2)}\n`, "utf8");
|
|
615
|
+
await rename(temporaryPath, options.filePath);
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
load: async (sessionKey) => (await records()).get(sessionKey),
|
|
619
|
+
save: async (sessionKey, record) => {
|
|
620
|
+
writeChain = writeChain.then(async () => {
|
|
621
|
+
const current = await records();
|
|
622
|
+
current.set(sessionKey, { sessionId: record.sessionId });
|
|
623
|
+
await persist(current);
|
|
624
|
+
});
|
|
625
|
+
await writeChain;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function isMissingFile(error) {
|
|
630
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
631
|
+
}
|
|
632
|
+
function isRecord(value) {
|
|
633
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
export { AcpSessionResumeFailed, AcpSessionResumeUnavailable, createAcpAgentLoop, createAcpAgentServer, createAcpPermissionBridge, createAcpStdioAgentLoop, createJsonAcpSessionStore, decideAcpPermission, serveAcpAgentOnStdio };
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { D as loadRivusDeploymentManifest, T as resolveNodeRivusPluginModulePath, X as resolveFeishuEndpointCredentials, Z as loadMergedLocalEnvFile, et as validateRivusDeploymentManifest, t as runRivusDaemonCli } from "./rivus-daemon-cli.js";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
4
|
import { lstat, mkdir, readFile, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
//#region src/infrastructure/project/rivus-project-doctor.ts
|
|
8
8
|
const REQUIRED_FILES = Object.freeze([
|
|
9
9
|
"package.json",
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import { a as readBackgroundSessionWaitInput, i as readBackgroundSessionString,
|
|
|
8
8
|
import { n as assertRivusPluginConforms, r as createFakeRivusPlugin, t as RivusPluginConformanceError } from "./rivus-plugin-testkit.js";
|
|
9
9
|
import { Cause, Deferred, Effect, Either, Exit, Fiber, Option, Stream } from "effect";
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
12
11
|
import { appendFile, lstat, mkdir, readFile, readdir, realpath, rename, stat, truncate, unlink, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
13
|
import { isDeepStrictEqual } from "node:util";
|
|
14
14
|
import { createServer } from "node:http";
|
|
15
15
|
import { Buffer as Buffer$1 } from "node:buffer";
|
package/dist/mcp.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ declare function toControlContext(input: {
|
|
|
40
40
|
declare const BACKGROUND_SESSION_MCP_SERVER_NAME = "rivus-background-sessions";
|
|
41
41
|
declare const BACKGROUND_SESSION_MCP_SERVER_VERSION = "1.0.0";
|
|
42
42
|
interface BackgroundSessionMcpServerOptions {
|
|
43
|
-
readonly
|
|
43
|
+
readonly capability: string;
|
|
44
44
|
readonly controlUrl: string;
|
|
45
45
|
readonly controlToken: string;
|
|
46
46
|
readonly enabledTools?: ReadonlyArray<BackgroundSessionControlCommand>;
|
|
@@ -74,6 +74,7 @@ interface BackgroundSessionControlHttpServerOptions {
|
|
|
74
74
|
}
|
|
75
75
|
interface BackgroundSessionControlHttpServer {
|
|
76
76
|
port(): number;
|
|
77
|
+
issueCapability(context: BackgroundSessionControlContext): string;
|
|
77
78
|
close(): Promise<void>;
|
|
78
79
|
start(): Promise<void>;
|
|
79
80
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -24,15 +24,14 @@ function createBackgroundSessionMcpServer(options) {
|
|
|
24
24
|
const tools = createBackgroundSessionToolContracts().filter(({ id }) => enabledTools.has(id.slice(11))).map((contract) => ({
|
|
25
25
|
description: contract.description,
|
|
26
26
|
inputSchema: contract.inputSchema,
|
|
27
|
-
name: contract.id
|
|
28
|
-
outputSchema: { type: "object" }
|
|
27
|
+
name: contract.id
|
|
29
28
|
}));
|
|
30
29
|
const serverInfo = {
|
|
31
30
|
name: options.serverName ?? "rivus-background-sessions",
|
|
32
31
|
version: options.serverVersion ?? "1.0.0"
|
|
33
32
|
};
|
|
34
33
|
return { run: () => runMcpServer({
|
|
35
|
-
|
|
34
|
+
capability: options.capability,
|
|
36
35
|
controlToken: options.controlToken,
|
|
37
36
|
controlUrl: options.controlUrl,
|
|
38
37
|
fetchImplementation,
|
|
@@ -45,10 +44,10 @@ function createBackgroundSessionMcpServer(options) {
|
|
|
45
44
|
async function runBackgroundSessionMcpServer(options = {}) {
|
|
46
45
|
const controlUrl = requireEnv("RIVUS_MCP_CONTROL_URL");
|
|
47
46
|
const controlToken = requireEnv("RIVUS_MCP_CONTROL_TOKEN");
|
|
48
|
-
const
|
|
47
|
+
const capability = requireEnv("RIVUS_MCP_CAPABILITY");
|
|
49
48
|
const enabledTools = optionalEnv("RIVUS_MCP_TOOLS")?.split(",").map((tool) => tool.trim()).filter(Boolean);
|
|
50
49
|
await createBackgroundSessionMcpServer({
|
|
51
|
-
|
|
50
|
+
capability,
|
|
52
51
|
controlToken,
|
|
53
52
|
controlUrl,
|
|
54
53
|
...enabledTools ? { enabledTools } : {},
|
|
@@ -60,16 +59,17 @@ async function runBackgroundSessionMcpServer(options = {}) {
|
|
|
60
59
|
async function runMcpServer(options) {
|
|
61
60
|
const send = (message) => {
|
|
62
61
|
const body = JSON.stringify(message);
|
|
63
|
-
options.output.write(
|
|
62
|
+
options.output.write(`${body}\n`);
|
|
64
63
|
};
|
|
65
64
|
for await (const message of readMcpMessages(options.input)) {
|
|
66
|
-
if (!isRecord(message) ||
|
|
65
|
+
if (!isRecord(message) || !isJsonRpcRequestId(message.id)) continue;
|
|
67
66
|
const method = typeof message.method === "string" ? message.method : void 0;
|
|
68
67
|
if (!method) continue;
|
|
69
68
|
try {
|
|
70
69
|
switch (method) {
|
|
71
70
|
case "initialize":
|
|
72
71
|
send({
|
|
72
|
+
jsonrpc: "2.0",
|
|
73
73
|
id: message.id,
|
|
74
74
|
result: {
|
|
75
75
|
capabilities: { tools: { listChanged: false } },
|
|
@@ -80,12 +80,14 @@ async function runMcpServer(options) {
|
|
|
80
80
|
break;
|
|
81
81
|
case "ping":
|
|
82
82
|
send({
|
|
83
|
+
jsonrpc: "2.0",
|
|
83
84
|
id: message.id,
|
|
84
85
|
result: {}
|
|
85
86
|
});
|
|
86
87
|
break;
|
|
87
88
|
case "tools/list":
|
|
88
89
|
send({
|
|
90
|
+
jsonrpc: "2.0",
|
|
89
91
|
id: message.id,
|
|
90
92
|
result: { tools: options.tools }
|
|
91
93
|
});
|
|
@@ -96,6 +98,7 @@ async function runMcpServer(options) {
|
|
|
96
98
|
const command = name.startsWith("background.") ? name.slice(11) : void 0;
|
|
97
99
|
if (!command || !options.tools.some((tool) => tool.name === name)) {
|
|
98
100
|
send({
|
|
101
|
+
jsonrpc: "2.0",
|
|
99
102
|
id: message.id,
|
|
100
103
|
error: {
|
|
101
104
|
code: -32602,
|
|
@@ -106,8 +109,8 @@ async function runMcpServer(options) {
|
|
|
106
109
|
}
|
|
107
110
|
const response = await options.fetchImplementation(options.controlUrl, {
|
|
108
111
|
body: JSON.stringify({
|
|
112
|
+
capability: options.capability,
|
|
109
113
|
command,
|
|
110
|
-
context: options.controlContext,
|
|
111
114
|
input: params.arguments ?? {}
|
|
112
115
|
}),
|
|
113
116
|
headers: {
|
|
@@ -120,6 +123,7 @@ async function runMcpServer(options) {
|
|
|
120
123
|
if (!response.ok || envelope.error) {
|
|
121
124
|
const messageText = envelope.error?.message ?? `background session control failed with ${response.status}`;
|
|
122
125
|
send({
|
|
126
|
+
jsonrpc: "2.0",
|
|
123
127
|
id: message.id,
|
|
124
128
|
error: {
|
|
125
129
|
code: -32e3,
|
|
@@ -129,6 +133,7 @@ async function runMcpServer(options) {
|
|
|
129
133
|
break;
|
|
130
134
|
}
|
|
131
135
|
send({
|
|
136
|
+
jsonrpc: "2.0",
|
|
132
137
|
id: message.id,
|
|
133
138
|
result: {
|
|
134
139
|
content: [{
|
|
@@ -141,6 +146,7 @@ async function runMcpServer(options) {
|
|
|
141
146
|
break;
|
|
142
147
|
}
|
|
143
148
|
default: send({
|
|
149
|
+
jsonrpc: "2.0",
|
|
144
150
|
id: message.id,
|
|
145
151
|
error: {
|
|
146
152
|
code: -32601,
|
|
@@ -150,6 +156,7 @@ async function runMcpServer(options) {
|
|
|
150
156
|
}
|
|
151
157
|
} catch (error) {
|
|
152
158
|
send({
|
|
159
|
+
jsonrpc: "2.0",
|
|
153
160
|
id: message.id,
|
|
154
161
|
error: {
|
|
155
162
|
code: -32e3,
|
|
@@ -163,36 +170,25 @@ async function* readMcpMessages(input) {
|
|
|
163
170
|
let buffer = "";
|
|
164
171
|
for await (const chunk of input) {
|
|
165
172
|
buffer += chunk.toString("utf8");
|
|
166
|
-
let
|
|
167
|
-
while (
|
|
168
|
-
|
|
169
|
-
buffer =
|
|
170
|
-
|
|
173
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
174
|
+
while (newlineIndex >= 0) {
|
|
175
|
+
const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
176
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
177
|
+
if (line.trim() !== "") yield parseMcpMessage(line);
|
|
178
|
+
newlineIndex = buffer.indexOf("\n");
|
|
171
179
|
}
|
|
172
180
|
}
|
|
181
|
+
if (buffer.trim() !== "") yield parseMcpMessage(buffer.trim());
|
|
173
182
|
}
|
|
174
|
-
function
|
|
175
|
-
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
176
|
-
if (headerEnd < 0) return void 0;
|
|
177
|
-
const contentLength = buffer.slice(0, headerEnd).split("\r\n").find((header) => /^Content-Length:\s*\d+$/i.test(header));
|
|
178
|
-
if (!contentLength) throw new BackgroundSessionMcpError("MCP message is missing a Content-Length header");
|
|
179
|
-
const byteLength = Number(contentLength.slice(15).trim());
|
|
180
|
-
const headerBytes = Buffer.byteLength(buffer.slice(0, headerEnd + 4));
|
|
181
|
-
if (Buffer.byteLength(buffer) < headerBytes + byteLength) return void 0;
|
|
182
|
-
const payload = Buffer.from(buffer).subarray(headerBytes, headerBytes + byteLength).toString("utf8");
|
|
183
|
+
function parseMcpMessage(payload) {
|
|
183
184
|
try {
|
|
184
|
-
return
|
|
185
|
-
byteLength: headerBytes + byteLength,
|
|
186
|
-
message: JSON.parse(payload)
|
|
187
|
-
};
|
|
185
|
+
return JSON.parse(payload);
|
|
188
186
|
} catch {
|
|
189
187
|
throw new BackgroundSessionMcpError("invalid MCP JSON payload");
|
|
190
188
|
}
|
|
191
189
|
}
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
if (!isRecord(parsed)) throw new BackgroundSessionMcpError("RIVUS_MCP_CONTEXT must be a JSON object");
|
|
195
|
-
return parsed;
|
|
190
|
+
function isJsonRpcRequestId(value) {
|
|
191
|
+
return value === null || typeof value === "string" || typeof value === "number";
|
|
196
192
|
}
|
|
197
193
|
function requireEnv(name) {
|
|
198
194
|
const value = process.env[name]?.trim();
|
|
@@ -335,6 +331,8 @@ const COMMANDS = [
|
|
|
335
331
|
function createBackgroundSessionControlHttpServer(options) {
|
|
336
332
|
let server;
|
|
337
333
|
let boundPort = options.port;
|
|
334
|
+
const capabilities = /* @__PURE__ */ new Map();
|
|
335
|
+
const capabilitiesBySessionKey = /* @__PURE__ */ new Map();
|
|
338
336
|
const serverHandle = createServer((request, response) => {
|
|
339
337
|
(async () => {
|
|
340
338
|
try {
|
|
@@ -349,11 +347,13 @@ function createBackgroundSessionControlHttpServer(options) {
|
|
|
349
347
|
if (request.method !== "POST" || request.url !== "/background-sessions") throw new BackgroundSessionControlHttpError(404, "not found");
|
|
350
348
|
if (request.headers.authorization !== `Bearer ${options.token}`) throw new BackgroundSessionControlHttpError(401, "unauthorized");
|
|
351
349
|
const envelope = parseEnvelope(await readBody(request, 64 * 1024));
|
|
350
|
+
const context = capabilities.get(envelope.capability);
|
|
351
|
+
if (!context) throw new BackgroundSessionControlHttpError(401, "unknown or expired capability");
|
|
352
352
|
const command = envelope.command;
|
|
353
353
|
if (!COMMANDS.includes(command)) throw new BackgroundSessionControlHttpError(400, `unsupported background session command: ${String(command)}`);
|
|
354
354
|
respond(response, 200, {
|
|
355
355
|
ok: true,
|
|
356
|
-
result: await options.control.handle(command,
|
|
356
|
+
result: await options.control.handle(command, context, envelope.input)
|
|
357
357
|
});
|
|
358
358
|
} catch (error) {
|
|
359
359
|
respond(response, error instanceof BackgroundSessionControlHttpError ? error.statusCode : 500, {
|
|
@@ -372,8 +372,18 @@ function createBackgroundSessionControlHttpServer(options) {
|
|
|
372
372
|
close: async () => {
|
|
373
373
|
const active = server;
|
|
374
374
|
server = void 0;
|
|
375
|
+
capabilities.clear();
|
|
376
|
+
capabilitiesBySessionKey.clear();
|
|
375
377
|
if (active) await new Promise((resolve) => active.close(() => resolve()));
|
|
376
378
|
},
|
|
379
|
+
issueCapability: (context) => {
|
|
380
|
+
const existing = capabilitiesBySessionKey.get(context.sessionKey);
|
|
381
|
+
if (existing) return existing;
|
|
382
|
+
const capability = randomUUID();
|
|
383
|
+
capabilities.set(capability, context);
|
|
384
|
+
capabilitiesBySessionKey.set(context.sessionKey, capability);
|
|
385
|
+
return capability;
|
|
386
|
+
},
|
|
377
387
|
start: async () => {
|
|
378
388
|
if (server) return;
|
|
379
389
|
await new Promise((resolve, reject) => {
|
|
@@ -399,10 +409,10 @@ function parseEnvelope(body) {
|
|
|
399
409
|
const parsed = JSON.parse(body);
|
|
400
410
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new BackgroundSessionControlHttpError(400, "invalid background session control envelope");
|
|
401
411
|
const record = parsed;
|
|
402
|
-
if (typeof record.command !== "string" || record.
|
|
412
|
+
if (typeof record.command !== "string" || typeof record.capability !== "string") throw new BackgroundSessionControlHttpError(400, "background session control envelope requires command and capability");
|
|
403
413
|
return {
|
|
414
|
+
capability: record.capability,
|
|
404
415
|
command: record.command,
|
|
405
|
-
context: record.context,
|
|
406
416
|
input: record.input
|
|
407
417
|
};
|
|
408
418
|
}
|
package/dist/pi.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { d as requiresToolApproval } from "./agent-memory.js";
|
|
2
2
|
import { l as createPiSkillRuntime, o as createInvocationAuthority, r as createToolInputDigest } from "./tool-input-digest.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { isAbsolute, relative } from "node:path";
|
|
5
4
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, relative } from "node:path";
|
|
6
6
|
import { Unsafe } from "typebox";
|
|
7
7
|
import { createReadToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
//#region src/infrastructure/pi/pi-project-skill-read-tool.ts
|
package/dist/rivus-daemon-cli.js
CHANGED
|
@@ -4,9 +4,9 @@ import { n as resolveRivusAgentDefinition, r as deepFreeze, t as createRivusPlug
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
8
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
7
|
import { lstat, open, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
8
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { constants } from "node:fs";
|
|
11
11
|
//#region src/application/plugin/rivus-automation-runtime-definition.ts
|
|
12
12
|
function resolveRivusAutomationRuntimeDefinition(definition, requestedToolIds, requestedSkillIds) {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createAcpAgentServer,
|
|
5
5
|
createAcpPermissionBridge,
|
|
6
6
|
createAcpStdioAgentLoop,
|
|
7
|
+
createJsonAcpSessionStore,
|
|
7
8
|
serveAcpAgentOnStdio
|
|
8
9
|
} from "@rivus/agent/acp";
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ const command = process.env.RIVUS_ACP_SERVER_COMMAND?.trim();
|
|
|
11
12
|
if (!command) throw new Error("RIVUS_ACP_SERVER_COMMAND is required and must resolve to an explicit executable");
|
|
12
13
|
|
|
13
14
|
const workingDirectory = process.env.RIVUS_ACP_WORKING_DIRECTORY?.trim() || process.cwd();
|
|
15
|
+
const sessionStorePath = process.env.RIVUS_ACP_SESSION_STORE?.trim();
|
|
14
16
|
const permissionBridge = createAcpPermissionBridge();
|
|
15
17
|
const downstream = createAcpStdioAgentLoop({
|
|
16
18
|
arguments: parseArguments(process.env.RIVUS_ACP_SERVER_ARGUMENTS),
|
|
@@ -18,6 +20,7 @@ const downstream = createAcpStdioAgentLoop({
|
|
|
18
20
|
environment: selectEnvironment(process.env.RIVUS_ACP_SERVER_ENV_KEYS),
|
|
19
21
|
onStderr: (text) => process.stderr.write(text),
|
|
20
22
|
permissionPolicy: permissionBridge.policy,
|
|
23
|
+
...(sessionStorePath ? { sessionStore: createJsonAcpSessionStore({ filePath: sessionStorePath }) } : {}),
|
|
21
24
|
workingDirectory
|
|
22
25
|
});
|
|
23
26
|
const server = createAcpAgentServer({
|