@rivus/agent 0.16.6 → 0.17.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.
Files changed (39) hide show
  1. package/LICENSE +674 -21
  2. package/README.md +23 -2
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/index.d.ts +3 -12
  5. package/dist/index.js +3 -5
  6. package/dist/testing/index.d.ts +21 -50
  7. package/dist/testing/index.js +58 -89
  8. package/package.json +14 -41
  9. package/dist/acp.d.ts +0 -2
  10. package/dist/acp.js +0 -2
  11. package/dist/bootstrap/pi-feishu.d.ts +0 -2
  12. package/dist/bootstrap/pi-feishu.js +0 -2
  13. package/dist/chunks/rivus-plugin-testkit.d.ts +0 -27
  14. package/dist/chunks/rivus-plugin-testkit.js +0 -63
  15. package/dist/mcp.d.ts +0 -2
  16. package/dist/mcp.js +0 -6
  17. package/dist/pi.d.ts +0 -2
  18. package/dist/pi.js +0 -2
  19. package/examples/a-share-briefing-analysis.mjs +0 -93
  20. package/examples/a-share-briefing-renderer.mjs +0 -257
  21. package/examples/a-share-index-evidence.mjs +0 -99
  22. package/examples/a-share-market-briefing.mjs +0 -83
  23. package/examples/a-share-market-date.mjs +0 -10
  24. package/examples/a-share-overseas-evidence.mjs +0 -86
  25. package/examples/a-share-policy-evidence.mjs +0 -145
  26. package/examples/a-share-provider-response.mjs +0 -21
  27. package/examples/a-share-sector-evidence.mjs +0 -70
  28. package/examples/acp-stdio-proxy.mjs +0 -58
  29. package/examples/current-weather.mjs +0 -117
  30. package/examples/html-drive-tools.mjs +0 -262
  31. package/examples/https-response-reader.mjs +0 -36
  32. package/examples/langfuse-drive-e2e.mjs +0 -175
  33. package/examples/pi-feishu-deployment.bootstrap.ts +0 -3
  34. package/examples/pi-feishu.bootstrap.ts +0 -242
  35. package/examples/rivus-agents.plugin.mjs +0 -290
  36. package/examples/rivus-langfuse-demo.config.json +0 -37
  37. package/examples/rivus-starter.plugin.mjs +0 -47
  38. package/examples/rivus.config.json +0 -114
  39. package/skills/runtime-management/SKILL.md +0 -65
@@ -1,262 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { constants } from "node:fs";
4
- import { lstat, mkdir, mkdtemp, open, readFile, realpath, rm, writeFile } from "node:fs/promises";
5
- import { join, relative, resolve, sep } from "node:path";
6
- import { promisify } from "node:util";
7
-
8
- const execFileAsync = promisify(execFile);
9
- const DEFAULT_ARTIFACT_ROOT = ".rivus/deployment/html-artifacts";
10
-
11
- export function createHtmlArtifactWriter(options = {}) {
12
- const cwd = options.cwd ?? process.cwd();
13
- const artifactRoot = resolveArtifactRoot(cwd, options.artifactRoot);
14
- return {
15
- async execute(input, context) {
16
- const html = readCompleteHtml(input);
17
- const operationId = readOperationId(context);
18
- const agentDirectory = resolveAgentDirectory(artifactRoot, context?.agentId);
19
- const artifactId = createHash("sha256")
20
- .update(JSON.stringify({ agentId: context.agentId, operationId }))
21
- .digest("hex")
22
- .slice(0, 32);
23
- const trustedDirectory = await createTrustedDirectory(cwd, agentDirectory);
24
- const filePath = resolve(trustedDirectory, `${artifactId}.html`);
25
- try {
26
- await writeFile(filePath, html, { encoding: "utf8", flag: "wx" });
27
- } catch (error) {
28
- if (!isFileAlreadyPresent(error) || (await readFile(filePath, "utf8")) !== html) throw error;
29
- }
30
- return Object.freeze({
31
- artifactId,
32
- path: relative(cwd, resolve(agentDirectory, `${artifactId}.html`)),
33
- sha256: createHash("sha256").update(html).digest("hex"),
34
- size: Buffer.byteLength(html)
35
- });
36
- }
37
- };
38
- }
39
-
40
- export function createLarkDriveHtmlUploader(options = {}) {
41
- const cwd = options.cwd ?? process.cwd();
42
- const artifactRoot = resolveArtifactRoot(cwd, options.artifactRoot);
43
- const onCleanupError = options.onCleanupError ?? reportCleanupError;
44
- const removeDirectory = options.removeDirectory ?? rm;
45
- const runCommand = options.runCommand ?? runLarkCli;
46
- return {
47
- async execute(input, context) {
48
- readOperationId(context);
49
- const { artifactId, name } = readUploadInput(input);
50
- const agentDirectory = resolveAgentDirectory(artifactRoot, context?.agentId);
51
- const trustedDirectory = await resolveExistingTrustedDirectory(cwd, agentDirectory);
52
- const filePath = resolve(trustedDirectory, `${artifactId}.html`);
53
- const bytes = await readRegularFileNoFollow(filePath, artifactId);
54
- const stagingRoot = resolve(cwd, ".rivus/deployment/upload-staging");
55
- const trustedStagingRoot = await createTrustedDirectory(cwd, stagingRoot);
56
- const trustedWorkspace = await realpath(cwd);
57
- const stagingDirectory = await mkdtemp(join(trustedStagingRoot, "upload-"));
58
- const stagedPath = resolve(stagingDirectory, name);
59
- try {
60
- await writeFile(stagedPath, bytes, { flag: "wx", mode: 0o600 });
61
- const command = process.env.RIVUS_LARK_CLI_PATH?.trim() || "lark-cli";
62
- const { stdout } = await runCommand(
63
- command,
64
- [
65
- "drive",
66
- "+upload",
67
- "--as",
68
- "user",
69
- "--file",
70
- relative(trustedWorkspace, stagedPath),
71
- "--name",
72
- name,
73
- "--format",
74
- "json"
75
- ],
76
- { cwd, timeout: 120_000 }
77
- );
78
- return readUploadResult(stdout, name);
79
- } finally {
80
- await cleanupStaging(removeDirectory, onCleanupError, stagingDirectory);
81
- }
82
- }
83
- };
84
- }
85
-
86
- async function readRegularFileNoFollow(filePath, artifactId) {
87
- const linkMetadata = await lstat(filePath);
88
- if (linkMetadata.isSymbolicLink()) throw new Error(`HTML artifact cannot be a symbolic link: ${artifactId}`);
89
- if (typeof constants.O_NOFOLLOW !== "number") {
90
- throw new Error("HTML artifact upload requires filesystem no-follow support");
91
- }
92
- const handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
93
- try {
94
- const metadata = await handle.stat();
95
- if (!metadata.isFile()) throw new Error(`HTML artifact is not a regular file: ${artifactId}`);
96
- if (metadata.size > 256 * 1024) throw new Error(`HTML artifact exceeds the 256 KiB limit: ${artifactId}`);
97
- return await handle.readFile();
98
- } finally {
99
- await handle.close();
100
- }
101
- }
102
-
103
- async function cleanupStaging(removeDirectory, onCleanupError, stagingDirectory) {
104
- try {
105
- await removeDirectory(stagingDirectory, { force: true, recursive: true });
106
- } catch (error) {
107
- try {
108
- onCleanupError(error);
109
- } catch {
110
- // Cleanup diagnostics must not change the known remote upload outcome.
111
- }
112
- }
113
- }
114
-
115
- function reportCleanupError(error) {
116
- process.emitWarning(
117
- `Could not remove the private HTML upload staging directory: ${error instanceof Error ? error.message : String(error)}`
118
- );
119
- }
120
-
121
- async function runLarkCli(command, args, options) {
122
- return execFileAsync(command, args, {
123
- ...options,
124
- env: {
125
- ...process.env,
126
- LARKSUITE_CLI_NO_SKILLS_NOTIFIER: "1",
127
- LARKSUITE_CLI_NO_UPDATE_NOTIFIER: "1"
128
- },
129
- maxBuffer: 1024 * 1024
130
- });
131
- }
132
-
133
- function readCompleteHtml(input) {
134
- if (!isRecord(input) || typeof input.html !== "string") {
135
- throw new Error("write-html-artifact requires an html string");
136
- }
137
- const html = input.html.trim();
138
- if (!/^<!doctype html>/i.test(html) || !/<html(?:\s|>)/i.test(html) || !/<\/html>\s*$/i.test(html)) {
139
- throw new Error("write-html-artifact requires a complete HTML document");
140
- }
141
- if (Buffer.byteLength(html) > 256 * 1024) throw new Error("HTML artifact exceeds the 256 KiB limit");
142
- return html;
143
- }
144
-
145
- function readUploadInput(input) {
146
- if (!isRecord(input) || typeof input.artifactId !== "string" || !/^[a-f0-9]{32}$/.test(input.artifactId)) {
147
- throw new Error("lark-drive-upload-html requires a valid artifactId");
148
- }
149
- if (
150
- typeof input.name !== "string" ||
151
- !/^[^/\\]{1,120}\.html$/i.test(input.name) ||
152
- input.name === ".html" ||
153
- input.name.includes("..")
154
- ) {
155
- throw new Error("lark-drive-upload-html requires a safe .html file name");
156
- }
157
- return { artifactId: input.artifactId, name: input.name };
158
- }
159
-
160
- function readUploadResult(stdout, requestedName) {
161
- let parsed;
162
- try {
163
- parsed = JSON.parse(stdout);
164
- } catch {
165
- throw new Error("lark-cli upload returned invalid JSON");
166
- }
167
- const result = isRecord(parsed?.data) ? parsed.data : parsed;
168
- if (!isRecord(result)) throw new Error("lark-cli upload returned an invalid result");
169
- const fileToken = readFirstString(result, ["file_token", "fileToken", "token"]);
170
- const url = readFirstString(result, ["url", "web_url", "file_url"]);
171
- const name = readFirstString(result, ["name", "file_name"]) ?? requestedName;
172
- if (!fileToken || !url) throw new Error("lark-cli upload result is missing file token or URL");
173
- return Object.freeze({ fileToken, name, url });
174
- }
175
-
176
- function readFirstString(value, keys) {
177
- for (const key of keys) {
178
- const candidate = value[key];
179
- if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
180
- }
181
- return undefined;
182
- }
183
-
184
- function resolveArtifactRoot(cwd, artifactRoot = DEFAULT_ARTIFACT_ROOT) {
185
- const root = resolve(cwd, artifactRoot);
186
- const relativeRoot = relative(cwd, root);
187
- if (relativeRoot === ".." || relativeRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
188
- throw new Error("HTML artifact root must stay inside the workspace");
189
- }
190
- return root;
191
- }
192
-
193
- function resolveAgentDirectory(artifactRoot, agentId) {
194
- if (typeof agentId !== "string" || !agentId.trim()) throw new Error("HTML artifact tool requires an agent id");
195
- return resolve(artifactRoot, createHash("sha256").update(agentId).digest("hex").slice(0, 16));
196
- }
197
-
198
- async function createTrustedDirectory(cwd, directory) {
199
- return resolveTrustedDirectory(cwd, directory, true);
200
- }
201
-
202
- async function resolveExistingTrustedDirectory(cwd, directory) {
203
- return resolveTrustedDirectory(cwd, directory, false);
204
- }
205
-
206
- async function resolveTrustedDirectory(cwd, directory, create) {
207
- const lexicalWorkspace = resolve(cwd);
208
- const lexicalDirectory = resolve(directory);
209
- const lexicalRelativePath = relative(lexicalWorkspace, lexicalDirectory);
210
- if (lexicalRelativePath === ".." || lexicalRelativePath.startsWith(`..${sep}`)) {
211
- throw new Error("HTML artifact directory must stay inside the workspace");
212
- }
213
- const segments = lexicalRelativePath.split(sep).filter(Boolean);
214
- let current = lexicalWorkspace;
215
- for (const segment of segments) {
216
- current = resolve(current, segment);
217
- const metadata = await readOrCreateDirectory(current, create);
218
- if (metadata.isSymbolicLink()) {
219
- throw new Error("HTML artifact directory cannot contain symbolic links");
220
- }
221
- if (!metadata.isDirectory()) throw new Error("HTML artifact path component is not a directory");
222
- }
223
- const [workspacePath, directoryPath] = await Promise.all([realpath(lexicalWorkspace), realpath(lexicalDirectory)]);
224
- const resolvedRelativePath = relative(workspacePath, directoryPath);
225
- if (resolvedRelativePath === ".." || resolvedRelativePath.startsWith(`..${sep}`)) {
226
- throw new Error("HTML artifact directory resolved outside the workspace");
227
- }
228
- return directoryPath;
229
- }
230
-
231
- async function readOrCreateDirectory(directory, create) {
232
- try {
233
- return await lstat(directory);
234
- } catch (error) {
235
- if (!isMissingFile(error) || !create) throw error;
236
- }
237
- try {
238
- await mkdir(directory);
239
- } catch (error) {
240
- if (!isFileAlreadyPresent(error)) throw error;
241
- }
242
- return lstat(directory);
243
- }
244
-
245
- function readOperationId(context) {
246
- if (typeof context?.operationId !== "string" || !context.operationId.trim()) {
247
- throw new Error("HTML artifact tool requires a stable operation id");
248
- }
249
- return context.operationId;
250
- }
251
-
252
- function isRecord(value) {
253
- return value !== null && typeof value === "object" && !Array.isArray(value);
254
- }
255
-
256
- function isFileAlreadyPresent(error) {
257
- return error instanceof Error && "code" in error && error.code === "EEXIST";
258
- }
259
-
260
- function isMissingFile(error) {
261
- return error instanceof Error && "code" in error && error.code === "ENOENT";
262
- }
@@ -1,36 +0,0 @@
1
- import { get } from "node:https";
2
-
3
- export function readBoundedHttpsResponse(url, options) {
4
- return new Promise((resolve, reject) => {
5
- const request = get(
6
- url,
7
- {
8
- family: 4,
9
- headers: options.headers,
10
- signal: AbortSignal.timeout(options.timeoutMs ?? 10_000)
11
- },
12
- (response) => {
13
- const status = response.statusCode ?? 0;
14
- response.once("error", reject);
15
- if (status < 200 || status >= 300) {
16
- response.resume();
17
- resolve({ body: Buffer.alloc(0), status });
18
- return;
19
- }
20
- const chunks = [];
21
- let length = 0;
22
- response.on("data", (chunk) => {
23
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
24
- length += buffer.length;
25
- if (length > options.maxBytes) {
26
- request.destroy(new Error(`HTTPS response exceeds ${options.maxBytes} bytes`));
27
- return;
28
- }
29
- chunks.push(buffer);
30
- });
31
- response.once("end", () => resolve({ body: Buffer.concat(chunks), status }));
32
- }
33
- );
34
- request.once("error", reject);
35
- });
36
- }
@@ -1,175 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import { mkdtemp, readFile, rm } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import { promisify } from "node:util";
7
-
8
- import { runLangfuseDriveE2E } from "../dist/testing/index.js";
9
-
10
- const execFileAsync = promisify(execFile);
11
- const baseUrl = requiredEnv("LANGFUSE_BASE_URL");
12
- const publicKey = requiredEnv("LANGFUSE_PUBLIC_KEY");
13
- const secretKey = requiredEnv("LANGFUSE_SECRET_KEY");
14
- const marker = `${new Date()
15
- .toISOString()
16
- .replace(/[^0-9]/g, "")
17
- .slice(0, 14)}-${randomUUID().slice(0, 8)}`;
18
- const sessionKey = `e2e:langfuse-drive:${marker}`;
19
- const expectedDriveTitle = `langfuse-e2e-${marker}.html`;
20
- const discoveredTraceIds = new Set();
21
- const deploymentStateDirectory = await mkdtemp(join(tmpdir(), "rivus-langfuse-e2e-state-"));
22
- const prompt = [
23
- "Read the granted Langfuse HTML publishing Skill.",
24
- "Create a polished, accessible, self-contained Chinese Langfuse introduction page.",
25
- `Include the visible E2E marker ${marker}, then upload it as ${expectedDriveTitle}.`,
26
- "Return the verified Feishu Drive URL."
27
- ].join(" ");
28
-
29
- const result = await runLangfuseDriveE2E({
30
- dependencies: {
31
- inspectDrive: async (url) => {
32
- const output = await command(process.env.RIVUS_LARK_CLI_PATH?.trim() || "lark-cli", [
33
- "drive",
34
- "+inspect",
35
- "--as",
36
- "user",
37
- "--url",
38
- url,
39
- "--format",
40
- "json"
41
- ]);
42
- const parsed = JSON.parse(output);
43
- return isRecord(parsed.data) ? parsed.data : parsed;
44
- },
45
- listObservations: async ({ deadlineAt, fromStartTime, sessionKey: expectedSessionKey, toStartTime }) => {
46
- if (discoveredTraceIds.size === 0) {
47
- let cursor;
48
- do {
49
- const endpoint = new URL("/api/public/v2/observations", baseUrl);
50
- endpoint.searchParams.set("fields", "core,basic");
51
- endpoint.searchParams.set("fromStartTime", fromStartTime);
52
- endpoint.searchParams.set("limit", "1000");
53
- endpoint.searchParams.set("toStartTime", toStartTime);
54
- if (cursor) endpoint.searchParams.set("cursor", cursor);
55
- const body = await fetchLangfuseJson(endpoint, deadlineAt);
56
- if (!isRecord(body) || !Array.isArray(body.data))
57
- throw new Error("Langfuse returned an invalid observation page");
58
- for (const observation of body.data.filter(isObservation)) {
59
- if (observation.sessionId === expectedSessionKey) discoveredTraceIds.add(observation.traceId);
60
- }
61
- cursor = isRecord(body.meta) && typeof body.meta.cursor === "string" ? body.meta.cursor : undefined;
62
- } while (cursor && discoveredTraceIds.size === 0);
63
- }
64
- const traces = [];
65
- for (const traceId of [...discoveredTraceIds].slice(0, 4)) {
66
- traces.push(await fetchLangfuseJson(new URL(`/api/public/traces/${traceId}`, baseUrl), deadlineAt));
67
- }
68
- return traces.flatMap((trace) => {
69
- if (
70
- !isRecord(trace) ||
71
- typeof trace.id !== "string" ||
72
- typeof trace.sessionId !== "string" ||
73
- !Array.isArray(trace.observations)
74
- )
75
- return [];
76
- return trace.observations.filter(isTraceObservation).map((observation) => ({
77
- ...observation,
78
- sessionId: trace.sessionId,
79
- traceId: trace.id
80
- }));
81
- });
82
- },
83
- now: () => new Date(),
84
- readDriveFile: async (fileToken) => {
85
- const directory = await mkdtemp(join(tmpdir(), "rivus-langfuse-drive-e2e-"));
86
- const outputName = `./${expectedDriveTitle}`;
87
- try {
88
- await command(
89
- process.env.RIVUS_LARK_CLI_PATH?.trim() || "lark-cli",
90
- ["drive", "+download", "--as", "user", "--file-token", fileToken, "--output", outputName],
91
- directory
92
- );
93
- return await readFile(join(directory, expectedDriveTitle), "utf8");
94
- } finally {
95
- await rm(directory, { force: true, recursive: true });
96
- }
97
- },
98
- runAgent: ({ prompt: agentPrompt, sessionKey: agentSessionKey }) =>
99
- command(
100
- process.execPath,
101
- [
102
- "dist/cli.js",
103
- "--bootstrap",
104
- "./examples/pi-feishu-deployment.bootstrap.ts",
105
- "--manifest",
106
- "./examples/rivus-langfuse-demo.config.json",
107
- "--prompt",
108
- agentPrompt,
109
- "--session-key",
110
- agentSessionKey
111
- ],
112
- process.cwd(),
113
- { RIVUS_DEPLOYMENT_STATE_DIR: deploymentStateDirectory }
114
- ),
115
- sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
116
- },
117
- expectedDriveContentMarker: marker,
118
- expectedDriveTitle,
119
- prompt,
120
- sessionKey
121
- }).finally(() => rm(deploymentStateDirectory, { force: true, recursive: true }));
122
-
123
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
124
-
125
- async function fetchLangfuseJson(endpoint, deadlineAt) {
126
- const remainingMs = new Date(deadlineAt).getTime() - Date.now();
127
- if (!Number.isFinite(remainingMs) || remainingMs <= 0) throw new Error("Langfuse request deadline expired");
128
- const response = await fetch(endpoint, {
129
- headers: {
130
- authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`
131
- },
132
- signal: AbortSignal.timeout(Math.min(remainingMs, 10_000))
133
- });
134
- if (!response.ok) throw new Error(`Langfuse request failed with HTTP ${response.status}`);
135
- return response.json();
136
- }
137
-
138
- async function command(executable, args, cwd = process.cwd(), extraEnv = {}) {
139
- const { stdout } = await execFileAsync(executable, args, {
140
- cwd,
141
- env: {
142
- ...process.env,
143
- ...extraEnv,
144
- LARKSUITE_CLI_NO_SKILLS_NOTIFIER: "1",
145
- LARKSUITE_CLI_NO_UPDATE_NOTIFIER: "1"
146
- },
147
- maxBuffer: 2 * 1024 * 1024,
148
- timeout: 300_000
149
- });
150
- return stdout;
151
- }
152
-
153
- function requiredEnv(name) {
154
- const value = process.env[name]?.trim();
155
- if (!value) throw new Error(`${name} is required`);
156
- return value;
157
- }
158
-
159
- function isObservation(value) {
160
- return isTraceObservation(value) && typeof value.traceId === "string" && typeof value.sessionId === "string";
161
- }
162
-
163
- function isTraceObservation(value) {
164
- return (
165
- isRecord(value) &&
166
- typeof value.id === "string" &&
167
- typeof value.name === "string" &&
168
- typeof value.startTime === "string" &&
169
- typeof value.type === "string"
170
- );
171
- }
172
-
173
- function isRecord(value) {
174
- return value !== null && typeof value === "object" && !Array.isArray(value);
175
- }
@@ -1,3 +0,0 @@
1
- // Projects initialized by Gateway keep the historical bootstrap specifier while
2
- // the implementation remains in the Gateway package canonical owner.
3
- export { createRivusDeploymentAdapters } from "@rivus/agent/bootstrap/pi-feishu";