@rivus/agent 0.1.1 → 0.3.1
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/README.md +18 -3
- package/dist/cli.js +504 -5
- package/dist/index.d.ts +11 -9
- package/dist/index.js +70 -33
- package/dist/rivus-daemon-cli.js +547 -520
- package/dist/rivus-plugin-testkit.js +2 -2
- package/examples/pi-feishu-deployment.bootstrap.ts +2 -2
- package/examples/rivus-starter.plugin.mjs +45 -0
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The first milestone is intentionally narrow: make the agent loop and agent harne
|
|
|
14
14
|
- Host-issued invocation authority and a call-time Tool Broker that checks exact grants, pinned versions, online revocation, stable operation IDs, durable side-effect fencing, and one-time approval receipts bound to canonical input digests. The Pi Tool Proxy and Feishu Human Interaction gateway are wired in the deployment example.
|
|
15
15
|
- Multi-agent Host primitives with trusted Endpoint bindings, stable Runtime Generation/Instance identities, lazy per-instance Runtime pooling, isolated active-run gates, and restart restoration from persisted registry records.
|
|
16
16
|
- Scoped Agent Memory with a single-writer append-only JSONL adapter, explicit Profile/Deployment grants, Host-bound tenant/subject/conversation/audience identity, and a provider-neutral `rivus_memory` Tool wired into the Pi manifest Runtime. Agent-private proposals survive process replacement; group invocations fail closed to their current Conversation scope, and Shared Profile reads expose only confirmed records. Automatic Recall, durable confirmation UX, typed Shared Profile fields, full provenance/retention metadata, and durable Compaction are still separate work.
|
|
17
|
-
- Manifest-driven daily Agent Automations with stable Mandate/Tick identity, per-template Tool grant narrowing, isolated Runtime and invocation authority, a durable single-writer JSON Tick adapter that records the actual Agent Run, generated-before-delivery recovery, and idempotent proactive Feishu
|
|
17
|
+
- Manifest-driven daily Agent Automations with stable Mandate/Tick identity, per-template Tool grant narrowing, isolated Runtime and invocation authority, a durable single-writer JSON Tick adapter that records the actual Agent Run, generated-before-delivery recovery, and idempotent proactive Feishu interactive-card delivery. The first Markdown heading becomes the card title while the remaining Markdown stays scannable in the card body. Revisioned Plugin State and the general Delivery Outbox remain composable domain primitives.
|
|
18
18
|
- Peer Delegation and Subagent coordination primitives with explicit edges, hop/budget limits, monotonic ToolGrantSet intersection, isolated child authority, denied ambient sender/private-memory/Shell capabilities, and parent cancellation cascading.
|
|
19
19
|
- `@rivus/agent/testing` Conformance TestKit with a reusable Fake Plugin and checks for registration/reference failures, ToolGrantSet expansion, and lifecycle disposal leaks.
|
|
20
20
|
- Effect-based `AgentHarness` use case with result-oriented `prompt()`, turn-oriented `promptTurn()`, event-oriented `stream()`, text-oriented `streamText()`, event-handling `promptWithEvents()`, active-run/run/session state and transcript lookup APIs, and a default runtime factory.
|
|
@@ -34,7 +34,7 @@ The first milestone is intentionally narrow: make the agent loop and agent harne
|
|
|
34
34
|
- A small Feishu agent runtime factory that assembles the harness, queue, worker, and event handlers.
|
|
35
35
|
- A Feishu WebSocket daemon adapter that starts an official-SDK-shaped `WSClient` with the runtime handlers.
|
|
36
36
|
- A background Feishu worker loop that drains accepted messages outside the WebSocket acknowledgement callback.
|
|
37
|
-
- A small `rivus` CLI
|
|
37
|
+
- A small `rivus` CLI with conflict-safe project initialization, side-effect-free local readiness diagnostics, bootstrap loading, daemon startup, and `SIGINT`/`SIGTERM` shutdown.
|
|
38
38
|
- A configured daemon bootstrap helper that wires Feishu WebSocket, CardKit target preparation, CardKit publishing, event logging, history replay, worker draining, and managed process lifecycle.
|
|
39
39
|
- A daemon status reporter that combines active harness run details, process/transport/worker/queue state, and restored run/session history.
|
|
40
40
|
- A small CardKit OpenAPI client for streaming text updates and replacing the card with completed, failed, or cancelled terminal projections.
|
|
@@ -53,6 +53,21 @@ npm install @rivus/agent
|
|
|
53
53
|
npx rivus --help
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
Create a standalone one-Agent project without cloning this repository:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install --global @rivus/agent
|
|
60
|
+
rivus init my-rivus-agent
|
|
61
|
+
cd my-rivus-agent
|
|
62
|
+
npm install
|
|
63
|
+
cp .env.example .env.local
|
|
64
|
+
# Fill the Feishu and model-provider values in .env.local.
|
|
65
|
+
npm run doctor
|
|
66
|
+
npm start
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`init` refuses to overwrite any generated path. `doctor` checks Node, project files, local dependencies, the deployment manifest, and enabled Endpoint credentials without importing Plugin code or contacting external services. See the [local deployment runbook](docs/en/operations/local-deployment.md) for foreground verification and the generated systemd/launchd templates.
|
|
70
|
+
|
|
56
71
|
The Pi plus Feishu example is application-edge code: install `@earendil-works/pi-coding-agent` and `@larksuiteoapi/node-sdk` in the daemon app that uses it. The `@rivus/agent` core package keeps those SDKs out of its runtime dependency graph.
|
|
57
72
|
|
|
58
73
|
```bash
|
|
@@ -884,7 +899,7 @@ const daemon = createFeishuAgentDaemon({
|
|
|
884
899
|
});
|
|
885
900
|
```
|
|
886
901
|
|
|
887
|
-
The daemon converts Feishu text messages into prompt commands, consumes harness `AgentRunUpdate` values
|
|
902
|
+
The daemon converts Feishu text and rich-text Post messages into prompt commands, preferring Post `content_v2` and flattening its textual paragraphs. It consumes harness `AgentRunUpdate` values and emits `update_text`, `finish`, `fail`, or `cancel` actions from projected run state. It also accepts `/cancel <runId>` as a daemon command; the intake attaches the message's stable `sessionKey`, and the daemon calls `harness.forSession(sessionKey).cancelRun(runId, "Feishu cancel command")` so a cancel message from another chat cannot cancel the active run. `/cancel` without an exact run id is rejected at intake instead of being sent to the model. `card.action.trigger` can cancel through the same exact path or resolve a persisted Human Interaction using only the trusted Feishu operator identity. Card-action tokens are process-locally deduplicated. Interaction decisions are persisted before the callback returns a toast and updated raw card; resolved cards contain no stale buttons. `prepareRun` is called on `agent_run_accepted`, before any stream action is published, so adapters can create a card and bind `runId` to `{ cardId, elementId }`. Publish effects are awaited in update order, and terminal publication replaces the entire card with a completed, failed, or cancelled projection. When daemon-level `dedupe` is enabled, a failed run releases its message-id marker so a retried delivery can run again; runtime-level queues treat `AgentRunCancelled` as terminal and do not retry cancelled messages.
|
|
888
903
|
|
|
889
904
|
For Feishu's 3-second event handling requirement, put a queue in front of the daemon:
|
|
890
905
|
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,516 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as runRivusDaemonCli } from "./rivus-daemon-cli.js";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { C as resolveNodeRivusPluginModulePath, D as resolveFeishuEndpointCredentials, O as loadMergedLocalEnvFile, T as loadRivusDeploymentManifest, j as validateRivusDeploymentManifest, t as runRivusDaemonCli } from "./rivus-daemon-cli.js";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
|
+
import { lstat, mkdir, readFile, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
//#region src/infrastructure/project/rivus-project-doctor.ts
|
|
8
|
+
const REQUIRED_FILES = Object.freeze([
|
|
9
|
+
"package.json",
|
|
10
|
+
"rivus.bootstrap.ts",
|
|
11
|
+
"rivus.config.json"
|
|
12
|
+
]);
|
|
13
|
+
const REQUIRED_DEPENDENCIES = Object.freeze([
|
|
14
|
+
"@rivus/agent",
|
|
15
|
+
"@earendil-works/pi-coding-agent",
|
|
16
|
+
"@larksuiteoapi/node-sdk"
|
|
17
|
+
]);
|
|
18
|
+
async function diagnoseRivusProject(options) {
|
|
19
|
+
const directory = resolve(options.directory);
|
|
20
|
+
const checks = [];
|
|
21
|
+
checks.push(checkNode(options.nodeVersion));
|
|
22
|
+
checks.push(await checkFiles(directory));
|
|
23
|
+
checks.push(await checkDependencies(directory));
|
|
24
|
+
const manifestResult = await checkManifest(directory);
|
|
25
|
+
checks.push(manifestResult.check);
|
|
26
|
+
const envFilePath = resolve(directory, options.envFilePath ?? ".env.local");
|
|
27
|
+
checks.push(await checkCredentials(manifestResult.manifest, envFilePath, options.env));
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
checks: Object.freeze(checks),
|
|
30
|
+
directory,
|
|
31
|
+
ready: checks.every(({ status }) => status === "pass")
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function checkNode(version) {
|
|
35
|
+
const [major, minor] = version.split(".").map(Number);
|
|
36
|
+
if (major === 24 && Number.isInteger(minor) && minor >= 11) return {
|
|
37
|
+
message: `Node.js ${version} satisfies the supported ^24.11 runtime`,
|
|
38
|
+
name: "node",
|
|
39
|
+
status: "pass"
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
message: `Node.js 24.11 or newer on major 24 is required; current version is ${version}`,
|
|
43
|
+
name: "node",
|
|
44
|
+
status: "fail"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function checkFiles(directory) {
|
|
48
|
+
const missing = await missingRegularFiles(directory, REQUIRED_FILES);
|
|
49
|
+
return missing.length === 0 ? {
|
|
50
|
+
message: "required project files are present",
|
|
51
|
+
name: "files",
|
|
52
|
+
status: "pass"
|
|
53
|
+
} : {
|
|
54
|
+
message: `required project files are missing: ${missing.join(", ")}`,
|
|
55
|
+
name: "files",
|
|
56
|
+
status: "fail"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function checkDependencies(directory) {
|
|
60
|
+
const packageFiles = REQUIRED_DEPENDENCIES.map((name) => join("node_modules", ...name.split("/"), "package.json"));
|
|
61
|
+
const missingIndexes = new Set((await missingRegularFiles(directory, packageFiles)).map((path) => packageFiles.indexOf(path)));
|
|
62
|
+
const missing = REQUIRED_DEPENDENCIES.filter((_, index) => missingIndexes.has(index));
|
|
63
|
+
return missing.length === 0 ? {
|
|
64
|
+
message: "Rivus, Pi, and Feishu dependencies are installed",
|
|
65
|
+
name: "dependencies",
|
|
66
|
+
status: "pass"
|
|
67
|
+
} : {
|
|
68
|
+
message: `run npm install; missing local dependencies: ${missing.join(", ")}`,
|
|
69
|
+
name: "dependencies",
|
|
70
|
+
status: "fail"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function checkManifest(directory) {
|
|
74
|
+
try {
|
|
75
|
+
const manifest = await loadRivusDeploymentManifest(join(directory, "rivus.config.json"));
|
|
76
|
+
validateRivusDeploymentManifest(manifest);
|
|
77
|
+
const missingModules = [];
|
|
78
|
+
for (const plugin of manifest.plugins) try {
|
|
79
|
+
await resolveNodeRivusPluginModulePath({
|
|
80
|
+
deploymentRoot: directory,
|
|
81
|
+
module: plugin.module,
|
|
82
|
+
pluginId: plugin.id
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
missingModules.push(plugin.module);
|
|
86
|
+
}
|
|
87
|
+
if (missingModules.length > 0) return { check: {
|
|
88
|
+
message: `manifest Plugin modules are missing: ${missingModules.join(", ")}`,
|
|
89
|
+
name: "manifest",
|
|
90
|
+
status: "fail"
|
|
91
|
+
} };
|
|
92
|
+
return {
|
|
93
|
+
check: {
|
|
94
|
+
message: "deployment manifest is valid",
|
|
95
|
+
name: "manifest",
|
|
96
|
+
status: "pass"
|
|
97
|
+
},
|
|
98
|
+
manifest
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { check: {
|
|
102
|
+
message: `deployment manifest is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
103
|
+
name: "manifest",
|
|
104
|
+
status: "fail"
|
|
105
|
+
} };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function checkCredentials(manifest, envFilePath, env) {
|
|
109
|
+
let mergedEnv;
|
|
110
|
+
try {
|
|
111
|
+
mergedEnv = await loadMergedLocalEnvFile(envFilePath, env);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
message: `required env file is unavailable at ${envFilePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
|
+
name: "credentials",
|
|
116
|
+
status: "fail"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!manifest) return {
|
|
120
|
+
message: "enabled Endpoint credentials cannot be checked until the deployment manifest is valid",
|
|
121
|
+
name: "credentials",
|
|
122
|
+
status: "fail"
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
for (const endpoint of manifest.endpoints.filter(({ enabled }) => enabled)) resolveFeishuEndpointCredentials(endpoint.credentialRef, mergedEnv);
|
|
126
|
+
return {
|
|
127
|
+
message: "enabled Endpoint credential references resolve",
|
|
128
|
+
name: "credentials",
|
|
129
|
+
status: "pass"
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
message: `enabled Endpoint credentials are incomplete: ${error instanceof Error ? error.message : String(error)}`,
|
|
134
|
+
name: "credentials",
|
|
135
|
+
status: "fail"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function missingRegularFiles(directory, paths) {
|
|
140
|
+
const missing = [];
|
|
141
|
+
for (const path of paths) try {
|
|
142
|
+
if (!(await stat(resolve(directory, path))).isFile()) missing.push(path);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (!isMissingPath$1(error)) throw error;
|
|
145
|
+
missing.push(path);
|
|
146
|
+
}
|
|
147
|
+
return missing;
|
|
148
|
+
}
|
|
149
|
+
function isMissingPath$1(error) {
|
|
150
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/infrastructure/project/rivus-project-initializer.ts
|
|
154
|
+
const TEMPLATE_FILES = Object.freeze({
|
|
155
|
+
"current-weather.mjs": "current-weather.mjs",
|
|
156
|
+
"rivus-agents.plugin.mjs": "rivus-starter.plugin.mjs",
|
|
157
|
+
"rivus.bootstrap.ts": "pi-feishu-deployment.bootstrap.ts"
|
|
158
|
+
});
|
|
159
|
+
async function initializeRivusProject(options) {
|
|
160
|
+
const directory = resolve(options.directory);
|
|
161
|
+
const manifest = await readPackageManifest(options.packageManifestPath);
|
|
162
|
+
const files = /* @__PURE__ */ new Map([
|
|
163
|
+
[".env.example", environmentTemplate()],
|
|
164
|
+
[".gitignore", ".env.local\n.rivus/\nnode_modules/\n"],
|
|
165
|
+
["deploy/launchd/com.rivus.agent.plist", launchdService(directory, options.nodeExecutable)],
|
|
166
|
+
["deploy/systemd/rivus.service", systemdService(directory, options.nodeExecutable)],
|
|
167
|
+
["package.json", projectPackageJson(directory, manifest)],
|
|
168
|
+
["rivus.config.json", deploymentManifest()]
|
|
169
|
+
]);
|
|
170
|
+
for (const [target, source] of Object.entries(TEMPLATE_FILES)) files.set(target, await readFile(join(options.templateDirectory, source), "utf8"));
|
|
171
|
+
const paths = [...files.keys()].sort();
|
|
172
|
+
await assertSafeProjectAncestors(directory, paths);
|
|
173
|
+
const conflicts = await findConflicts(directory, paths);
|
|
174
|
+
if (conflicts.length > 0) throw new Error(`Rivus project initialization refused; refusing to overwrite: ${conflicts.join(", ")}`);
|
|
175
|
+
const createdDirectories = [];
|
|
176
|
+
const createdFiles = [];
|
|
177
|
+
const writeProjectFile = options.writeProjectFile ?? writeExclusiveProjectFile;
|
|
178
|
+
try {
|
|
179
|
+
for (const path of paths) {
|
|
180
|
+
const destination = join(directory, path);
|
|
181
|
+
await ensureDirectory(dirname(destination), directory, createdDirectories);
|
|
182
|
+
await assertSafeProjectAncestors(directory, [path]);
|
|
183
|
+
await writeProjectFile(destination, files.get(path));
|
|
184
|
+
createdFiles.push(destination);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const rollbackErrors = await rollbackCreatedPaths(createdFiles, createdDirectories);
|
|
188
|
+
if (rollbackErrors.length > 0) throw new AggregateError([error, ...rollbackErrors], "Rivus project initialization failed and rollback was incomplete");
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
directory,
|
|
193
|
+
files: Object.freeze(paths)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function writeExclusiveProjectFile(path, contents) {
|
|
197
|
+
await writeFile(path, contents, {
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
flag: "wx"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async function assertSafeProjectAncestors(directory, paths) {
|
|
203
|
+
const rootState = await lstatOrUndefined(directory);
|
|
204
|
+
if (rootState?.isSymbolicLink()) throw new Error("Rivus project initialization refused; symbolic link ancestor: .");
|
|
205
|
+
if (rootState && !rootState.isDirectory()) throw new Error("Rivus project initialization refused; project path is not a directory");
|
|
206
|
+
if (!rootState) return;
|
|
207
|
+
for (const path of paths) {
|
|
208
|
+
let current = directory;
|
|
209
|
+
for (const segment of path.split("/").slice(0, -1)) {
|
|
210
|
+
current = join(current, segment);
|
|
211
|
+
const state = await lstatOrUndefined(current);
|
|
212
|
+
if (!state) break;
|
|
213
|
+
if (state.isSymbolicLink()) throw new Error(`Rivus project initialization refused; symbolic link ancestor: ${relative(directory, current)}`);
|
|
214
|
+
if (!state.isDirectory()) throw new Error(`Rivus project initialization refused; non-directory ancestor: ${relative(directory, current)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function ensureDirectory(path, projectRoot, createdDirectories) {
|
|
219
|
+
try {
|
|
220
|
+
await mkdir(path);
|
|
221
|
+
createdDirectories.push(path);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (isMissingPath(error)) {
|
|
224
|
+
const parent = dirname(path);
|
|
225
|
+
if (parent === path) throw error;
|
|
226
|
+
await ensureDirectory(parent, projectRoot, createdDirectories);
|
|
227
|
+
await ensureDirectory(path, projectRoot, createdDirectories);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!isAlreadyExists(error)) throw error;
|
|
231
|
+
const state = await lstat(path);
|
|
232
|
+
if (state.isSymbolicLink()) {
|
|
233
|
+
if (isWithin(projectRoot, path)) throw new Error(`Rivus project initialization refused; symbolic link ancestor: ${relative(projectRoot, path) || "."}`);
|
|
234
|
+
if ((await stat(path)).isDirectory()) return;
|
|
235
|
+
}
|
|
236
|
+
if (!state.isDirectory()) throw new Error(`Rivus project initialization refused; non-directory ancestor: ${path}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function rollbackCreatedPaths(files, directories) {
|
|
240
|
+
const errors = [];
|
|
241
|
+
for (const path of files.reverse()) try {
|
|
242
|
+
await unlink(path);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (!isMissingPath(error)) errors.push(asError(error));
|
|
245
|
+
}
|
|
246
|
+
for (const path of directories.reverse()) try {
|
|
247
|
+
await rmdir(path);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (!isMissingPath(error) && !isDirectoryNotEmpty(error)) errors.push(asError(error));
|
|
250
|
+
}
|
|
251
|
+
return errors;
|
|
252
|
+
}
|
|
253
|
+
async function lstatOrUndefined(path) {
|
|
254
|
+
try {
|
|
255
|
+
return await lstat(path);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (isMissingPath(error)) return void 0;
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function isWithin(root, candidate) {
|
|
262
|
+
const child = relative(root, candidate);
|
|
263
|
+
return child === "" || !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child);
|
|
264
|
+
}
|
|
265
|
+
function isAlreadyExists(error) {
|
|
266
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
267
|
+
}
|
|
268
|
+
function isDirectoryNotEmpty(error) {
|
|
269
|
+
return error instanceof Error && "code" in error && error.code === "ENOTEMPTY";
|
|
270
|
+
}
|
|
271
|
+
function asError(error) {
|
|
272
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
273
|
+
}
|
|
274
|
+
async function readPackageManifest(path) {
|
|
275
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
276
|
+
if (manifest.name !== "@rivus/agent" || typeof manifest.version !== "string") throw new Error("Rivus package manifest is missing its release identity");
|
|
277
|
+
return manifest;
|
|
278
|
+
}
|
|
279
|
+
async function findConflicts(directory, paths) {
|
|
280
|
+
const conflicts = [];
|
|
281
|
+
for (const path of paths) try {
|
|
282
|
+
await lstat(join(directory, path));
|
|
283
|
+
conflicts.push(path);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (!isMissingPath(error)) throw error;
|
|
286
|
+
}
|
|
287
|
+
return conflicts;
|
|
288
|
+
}
|
|
289
|
+
function isMissingPath(error) {
|
|
290
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
291
|
+
}
|
|
292
|
+
function projectPackageJson(directory, manifest) {
|
|
293
|
+
const pi = manifest.peerDependencies?.["@earendil-works/pi-coding-agent"];
|
|
294
|
+
const lark = manifest.peerDependencies?.["@larksuiteoapi/node-sdk"];
|
|
295
|
+
const effect = manifest.dependencies?.effect;
|
|
296
|
+
if (!pi || !lark || !effect) throw new Error("Rivus package manifest is missing its Effect, Pi, or Feishu dependency range");
|
|
297
|
+
const projectName = sanitizePackageName(basename(directory));
|
|
298
|
+
return `${JSON.stringify({
|
|
299
|
+
name: projectName,
|
|
300
|
+
private: true,
|
|
301
|
+
type: "module",
|
|
302
|
+
scripts: {
|
|
303
|
+
"check-config": "rivus --manifest ./rivus.config.json --check-config",
|
|
304
|
+
doctor: "rivus doctor .",
|
|
305
|
+
start: "rivus --env-file .env.local --bootstrap ./rivus.bootstrap.ts --manifest ./rivus.config.json"
|
|
306
|
+
},
|
|
307
|
+
dependencies: {
|
|
308
|
+
"@earendil-works/pi-coding-agent": pi,
|
|
309
|
+
"@larksuiteoapi/node-sdk": lark,
|
|
310
|
+
"@rivus/agent": `^${manifest.version}`,
|
|
311
|
+
effect
|
|
312
|
+
}
|
|
313
|
+
}, null, 2)}\n`;
|
|
314
|
+
}
|
|
315
|
+
function sanitizePackageName(value) {
|
|
316
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "") || "rivus-app";
|
|
317
|
+
}
|
|
318
|
+
function deploymentManifest() {
|
|
319
|
+
return `${JSON.stringify({
|
|
320
|
+
plugins: [{
|
|
321
|
+
id: "rivus-starter",
|
|
322
|
+
module: "./rivus-agents.plugin.mjs",
|
|
323
|
+
required: true
|
|
324
|
+
}],
|
|
325
|
+
agents: [{
|
|
326
|
+
agentId: "agent-a",
|
|
327
|
+
endpointIds: ["feishu-agent-a"],
|
|
328
|
+
pluginId: "rivus-starter",
|
|
329
|
+
profileId: "agent-a",
|
|
330
|
+
skills: { allow: [] },
|
|
331
|
+
tools: { allow: ["rivus-starter/current-weather"] }
|
|
332
|
+
}],
|
|
333
|
+
defaultAgentId: "agent-a",
|
|
334
|
+
defaultEndpointId: "feishu-agent-a",
|
|
335
|
+
endpoints: [{
|
|
336
|
+
agentId: "agent-a",
|
|
337
|
+
baseUrl: "https://open.feishu.cn",
|
|
338
|
+
credentialRef: "env:RIVUS_FEISHU",
|
|
339
|
+
enabled: true,
|
|
340
|
+
experimental: { cotMessages: false },
|
|
341
|
+
groupPolicy: "mention-only",
|
|
342
|
+
id: "feishu-agent-a",
|
|
343
|
+
required: true,
|
|
344
|
+
sessionNamespace: "rivus-starter",
|
|
345
|
+
streamMinIntervalMs: 200
|
|
346
|
+
}]
|
|
347
|
+
}, null, 2)}\n`;
|
|
348
|
+
}
|
|
349
|
+
function environmentTemplate() {
|
|
350
|
+
return [
|
|
351
|
+
"# Copy this file to .env.local and keep the real values untracked.",
|
|
352
|
+
"RIVUS_FEISHU_APP_ID=",
|
|
353
|
+
"RIVUS_FEISHU_APP_SECRET=",
|
|
354
|
+
"PI_MODEL=",
|
|
355
|
+
"PI_API_KEY=",
|
|
356
|
+
"# PI_BASE_URL=",
|
|
357
|
+
"RIVUS_WEATHER_DEFAULT_LOCATION=上海",
|
|
358
|
+
"# LANGFUSE_BASE_URL=https://jp.cloud.langfuse.com",
|
|
359
|
+
"# LANGFUSE_PUBLIC_KEY=",
|
|
360
|
+
"# LANGFUSE_SECRET_KEY=",
|
|
361
|
+
"# RIVUS_TELEMETRY_CONTENT=redacted",
|
|
362
|
+
""
|
|
363
|
+
].join("\n");
|
|
364
|
+
}
|
|
365
|
+
function systemdService(directory, nodeExecutable) {
|
|
366
|
+
const cli = join(directory, "node_modules", "@rivus", "agent", "dist", "cli.js");
|
|
367
|
+
return `[Unit]\nDescription=Rivus Agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nWorkingDirectory=${systemdQuote(directory)}\nExecStart=${systemdQuote(nodeExecutable)} ${systemdQuote(cli)} --env-file .env.local --bootstrap ./rivus.bootstrap.ts --manifest ./rivus.config.json\nRestart=on-failure\nRestartSec=5\nEnvironment=NODE_ENV=production\n\n[Install]\nWantedBy=default.target\n`;
|
|
368
|
+
}
|
|
369
|
+
function systemdQuote(value) {
|
|
370
|
+
return `"${value.replace(/%/g, "%%").replace(/\$/g, () => "$$").replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
371
|
+
}
|
|
372
|
+
function launchdService(directory, nodeExecutable) {
|
|
373
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key>\n <string>com.rivus.agent</string>\n <key>ProgramArguments</key>\n <array>\n${[
|
|
374
|
+
nodeExecutable,
|
|
375
|
+
join(directory, "node_modules", "@rivus", "agent", "dist", "cli.js"),
|
|
376
|
+
"--env-file",
|
|
377
|
+
".env.local",
|
|
378
|
+
"--bootstrap",
|
|
379
|
+
"./rivus.bootstrap.ts",
|
|
380
|
+
"--manifest",
|
|
381
|
+
"./rivus.config.json"
|
|
382
|
+
].map((value) => ` <string>${xmlEscape(value)}</string>`).join("\n")}\n </array>\n <key>WorkingDirectory</key>\n <string>${xmlEscape(directory)}</string>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n <key>ThrottleInterval</key>\n <integer>5</integer>\n</dict>\n</plist>\n`;
|
|
383
|
+
}
|
|
384
|
+
function xmlEscape(value) {
|
|
385
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/composition/rivus-cli.ts
|
|
389
|
+
const USAGE = `Usage:
|
|
390
|
+
rivus init [directory]
|
|
391
|
+
rivus doctor [directory] [--env-file <path>]
|
|
392
|
+
rivus --bootstrap <module> [--manifest <rivus.config.json>] [options]
|
|
393
|
+
|
|
394
|
+
Commands:
|
|
395
|
+
init Create a standalone local Rivus project without overwriting files
|
|
396
|
+
doctor Check Node, files, dependencies, manifest, and enabled Endpoint credentials
|
|
397
|
+
|
|
398
|
+
Run rivus --help for the complete daemon option list.
|
|
399
|
+
`;
|
|
400
|
+
function runRivusCli(options) {
|
|
401
|
+
const command = options.argv[0];
|
|
402
|
+
if (command === "init") return runInitCommand(options);
|
|
403
|
+
if (command === "doctor") return runDoctorCommand(options);
|
|
404
|
+
if (command && !command.startsWith("-")) return Effect.sync(() => {
|
|
405
|
+
options.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
|
|
406
|
+
return 1;
|
|
407
|
+
});
|
|
408
|
+
return runRivusDaemonCli(options);
|
|
409
|
+
}
|
|
410
|
+
function runInitCommand(options) {
|
|
411
|
+
return Effect.promise(async () => {
|
|
412
|
+
const args = options.argv.slice(1);
|
|
413
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
414
|
+
options.stdout.write("Usage: rivus init [directory]\n");
|
|
415
|
+
return 0;
|
|
416
|
+
}
|
|
417
|
+
if (args.length > 1 || args[0]?.startsWith("-")) {
|
|
418
|
+
options.stderr.write("Usage: rivus init [directory]\n");
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
const directory = resolve(options.cwd, args[0] ?? ".");
|
|
422
|
+
try {
|
|
423
|
+
const result = await initializeRivusProject({
|
|
424
|
+
directory,
|
|
425
|
+
nodeExecutable: options.nodeExecutable,
|
|
426
|
+
packageManifestPath: options.packageManifestPath,
|
|
427
|
+
templateDirectory: options.templateDirectory
|
|
428
|
+
});
|
|
429
|
+
options.stdout.write(`Initialized Rivus project in ${result.directory}\n\nNext:\n cd ${shellQuote(result.directory)}\n npm install\n cp .env.example .env.local\n npm run doctor\n npm start\n`);
|
|
430
|
+
return 0;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
options.stderr.write(`${formatError(error)}\n`);
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function runDoctorCommand(options) {
|
|
438
|
+
return Effect.promise(async () => {
|
|
439
|
+
const parsed = parseDoctorArguments(options.argv.slice(1));
|
|
440
|
+
if (parsed.help) {
|
|
441
|
+
options.stdout.write("Usage: rivus doctor [directory] [--env-file <path>]\n");
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
if (parsed.error) {
|
|
445
|
+
options.stderr.write(`${parsed.error}\nUsage: rivus doctor [directory] [--env-file <path>]\n`);
|
|
446
|
+
return 1;
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
const report = await diagnoseRivusProject({
|
|
450
|
+
directory: resolve(options.cwd, parsed.directory ?? "."),
|
|
451
|
+
env: options.env,
|
|
452
|
+
...parsed.envFilePath ? { envFilePath: parsed.envFilePath } : {},
|
|
453
|
+
nodeVersion: options.nodeVersion
|
|
454
|
+
});
|
|
455
|
+
options.stdout.write(`Rivus doctor: ${report.directory}\n`);
|
|
456
|
+
for (const check of report.checks) options.stdout.write(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.name}: ${check.message}\n`);
|
|
457
|
+
options.stdout.write(report.ready ? "Rivus project is ready\n" : "Rivus project is not ready\n");
|
|
458
|
+
return report.ready ? 0 : 1;
|
|
459
|
+
} catch (error) {
|
|
460
|
+
options.stderr.write(`${formatError(error)}\n`);
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function parseDoctorArguments(argv) {
|
|
466
|
+
let directory;
|
|
467
|
+
let envFilePath;
|
|
468
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
469
|
+
const argument = argv[index];
|
|
470
|
+
if (argument === "--help" || argument === "-h") return { help: true };
|
|
471
|
+
if (argument === "--env-file") {
|
|
472
|
+
const value = argv[index + 1];
|
|
473
|
+
if (!value) return { error: "--env-file requires a path" };
|
|
474
|
+
envFilePath = value;
|
|
475
|
+
index += 1;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (argument.startsWith("--env-file=")) {
|
|
479
|
+
envFilePath = argument.slice(11);
|
|
480
|
+
if (!envFilePath) return { error: "--env-file requires a path" };
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (argument.startsWith("-")) return { error: `Unknown doctor option: ${argument}` };
|
|
484
|
+
if (directory !== void 0) return { error: "doctor accepts at most one directory" };
|
|
485
|
+
directory = argument;
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
...directory !== void 0 ? { directory } : {},
|
|
489
|
+
...envFilePath !== void 0 ? { envFilePath } : {}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function formatError(error) {
|
|
493
|
+
return error instanceof Error ? error.message : String(error);
|
|
494
|
+
}
|
|
495
|
+
function shellQuote(value) {
|
|
496
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
6
499
|
//#region src/cli.ts
|
|
7
|
-
const
|
|
500
|
+
const packageDirectory = fileURLToPath(new URL("..", import.meta.url));
|
|
501
|
+
const exitCode = await Effect.runPromise(runRivusCli({
|
|
8
502
|
argv: process.argv.slice(2),
|
|
503
|
+
cwd: process.cwd(),
|
|
9
504
|
env: process.env,
|
|
10
505
|
exitAfterSignal: (code) => process.exit(code),
|
|
11
506
|
loadBootstrap: (specifier) => import(toImportSpecifier(specifier)),
|
|
507
|
+
nodeExecutable: process.execPath,
|
|
508
|
+
nodeVersion: process.versions.node,
|
|
509
|
+
packageManifestPath: join(packageDirectory, "package.json"),
|
|
12
510
|
signalSource: process,
|
|
13
511
|
stderr: process.stderr,
|
|
14
|
-
stdout: process.stdout
|
|
512
|
+
stdout: process.stdout,
|
|
513
|
+
templateDirectory: join(packageDirectory, "examples")
|
|
15
514
|
}));
|
|
16
515
|
process.exitCode = exitCode;
|
|
17
516
|
function toImportSpecifier(specifier) {
|