@agentoryhq/node 0.1.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/dist/probe.js ADDED
@@ -0,0 +1,86 @@
1
+ import { worstMcpStatus } from "@agentoryhq/shared";
2
+ const DEFAULT_TIMEOUT_MS = 50;
3
+ /**
4
+ * Binding-level health probe (slice 3 / AC-2).
5
+ * Simulation hooks in command string for tests: invalid / ENOENT / timeout-sim / degraded.
6
+ * Optional real HTTP(S) fetch when command looks like a URL and AGENTORY_MCP_HTTP_PROBE=1.
7
+ */
8
+ export async function probeMcpBinding(input, opts) {
9
+ const { name, command } = input;
10
+ if (!command || !command.trim()) {
11
+ return { name, status: "unknown", reason: "no command configured" };
12
+ }
13
+ const cmd = command.trim();
14
+ if (/ENOENT|invalid|bad-url|does-not-exist/i.test(cmd)) {
15
+ return {
16
+ name,
17
+ command: cmd,
18
+ status: "failed",
19
+ reason: readableFail(cmd),
20
+ };
21
+ }
22
+ if (/timeout-sim/i.test(cmd)) {
23
+ return {
24
+ name,
25
+ command: cmd,
26
+ status: "failed",
27
+ reason: `probe timeout after ${opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`,
28
+ };
29
+ }
30
+ if (/degraded/i.test(cmd)) {
31
+ return {
32
+ name,
33
+ command: cmd,
34
+ status: "degraded",
35
+ reason: "probe succeeded with warnings (simulated)",
36
+ };
37
+ }
38
+ const urlMatch = cmd.match(/https?:\/\/[^\s]+/i);
39
+ if (urlMatch && process.env.AGENTORY_MCP_HTTP_PROBE === "1") {
40
+ const fetchFn = opts?.fetchImpl ?? fetch;
41
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
42
+ try {
43
+ const ctrl = new AbortController();
44
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
45
+ const res = await fetchFn(urlMatch[0], { method: "GET", signal: ctrl.signal });
46
+ clearTimeout(t);
47
+ if (!res.ok) {
48
+ return {
49
+ name,
50
+ command: cmd,
51
+ status: "failed",
52
+ reason: `HTTP ${res.status} from ${urlMatch[0]}`,
53
+ };
54
+ }
55
+ return { name, command: cmd, status: "healthy" };
56
+ }
57
+ catch (err) {
58
+ const msg = err instanceof Error ? err.message : String(err);
59
+ return {
60
+ name,
61
+ command: cmd,
62
+ status: "failed",
63
+ reason: msg.includes("abort")
64
+ ? `probe timeout after ${timeoutMs}ms`
65
+ : `probe error: ${msg}`,
66
+ };
67
+ }
68
+ }
69
+ // Config present and no failure signal → healthy for MVP.
70
+ return { name, command: cmd, status: "healthy" };
71
+ }
72
+ function readableFail(cmd) {
73
+ if (/ENOENT/i.test(cmd)) {
74
+ const pathMatch = cmd.match(/\/[^\s]+/);
75
+ return pathMatch ? `ENOENT ${pathMatch[0]}` : "ENOENT path missing";
76
+ }
77
+ if (/bad-url|invalid/i.test(cmd))
78
+ return `invalid endpoint in command: ${cmd.slice(0, 80)}`;
79
+ return `probe failed for ${cmd.slice(0, 80)}`;
80
+ }
81
+ export async function probeAll(bindings, opts) {
82
+ return Promise.all(bindings.map((b) => probeMcpBinding(b, opts)));
83
+ }
84
+ export function worstStatus(statuses) {
85
+ return worstMcpStatus(statuses);
86
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { probeAll, probeMcpBinding, worstStatus } from "./probe.js";
4
+ test("probe marks missing command unknown", async () => {
5
+ const r = await probeMcpBinding({ name: "empty" });
6
+ assert.equal(r.status, "unknown");
7
+ assert.match(r.reason ?? "", /no command/i);
8
+ });
9
+ test("probe simulates failed invalid / ENOENT / timeout", async () => {
10
+ const bad = await probeMcpBinding({ name: "bad", command: "npx invalid bad-url" });
11
+ assert.equal(bad.status, "failed");
12
+ assert.ok(bad.reason && bad.reason.length > 0);
13
+ const enoent = await probeMcpBinding({
14
+ name: "gone",
15
+ command: "ENOENT /usr/bin/missing-mcp",
16
+ });
17
+ assert.equal(enoent.status, "failed");
18
+ assert.match(enoent.reason ?? "", /ENOENT/);
19
+ const timed = await probeMcpBinding({ name: "slow", command: "timeout-sim http://127.0.0.1:9" }, { timeoutMs: 25 });
20
+ assert.equal(timed.status, "failed");
21
+ assert.match(timed.reason ?? "", /timeout/i);
22
+ });
23
+ test("probe healthy and degraded", async () => {
24
+ const ok = await probeMcpBinding({ name: "gh", command: "npx -y @modelcontextprotocol/server" });
25
+ assert.equal(ok.status, "healthy");
26
+ const deg = await probeMcpBinding({ name: "warn", command: "degraded npx server" });
27
+ assert.equal(deg.status, "degraded");
28
+ assert.ok(deg.reason);
29
+ });
30
+ test("probeAll + worstStatus", async () => {
31
+ const all = await probeAll([
32
+ { name: "a", command: "npx ok" },
33
+ { name: "b", command: "invalid endpoint" },
34
+ ]);
35
+ assert.equal(all.length, 2);
36
+ assert.equal(worstStatus(all.map((b) => b.status)), "failed");
37
+ });
@@ -0,0 +1,4 @@
1
+ import type { RuntimeAdapter } from "@agentoryhq/runtime-contract";
2
+ /** Built-in adapters shipped with agentory-node (仓内 adapters/*). */
3
+ export declare const builtinAdapters: readonly RuntimeAdapter[];
4
+ export declare function adapterForRuntime(runtime: string): RuntimeAdapter | undefined;
@@ -0,0 +1,7 @@
1
+ import { claudeCodeAdapter } from "@agentoryhq/adapter-claude-code";
2
+ import { cursorAdapter } from "@agentoryhq/adapter-cursor";
3
+ /** Built-in adapters shipped with agentory-node (仓内 adapters/*). */
4
+ export const builtinAdapters = [cursorAdapter, claudeCodeAdapter];
5
+ export function adapterForRuntime(runtime) {
6
+ return builtinAdapters.find((a) => a.manifest.runtime === runtime);
7
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Host re-exports Cursor transcript capture (adapter owns paths / masking).
3
+ */
4
+ export { TRANSCRIPT_MESSAGE_MAX_CHARS, TRANSCRIPT_WINDOW_MESSAGES, captureTranscriptSnapshot, maskSecrets, transcriptFileForSourceKey, } from "@agentoryhq/adapter-cursor";
5
+ export type TranscriptFetchOpts = {
6
+ homeDir?: string;
7
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Host re-exports Cursor transcript capture (adapter owns paths / masking).
3
+ */
4
+ export { TRANSCRIPT_MESSAGE_MAX_CHARS, TRANSCRIPT_WINDOW_MESSAGES, captureTranscriptSnapshot, maskSecrets, transcriptFileForSourceKey, } from "@agentoryhq/adapter-cursor";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { test } from "node:test";
6
+ import { TRANSCRIPT_MESSAGE_MAX_CHARS, TRANSCRIPT_WINDOW_MESSAGES, captureTranscriptSnapshot, maskSecrets, transcriptFileForSourceKey, } from "./transcript.js";
7
+ function makeHome() {
8
+ return fs.mkdtempSync(path.join(os.tmpdir(), "agentory-tr-"));
9
+ }
10
+ function writeJsonl(home, rel, rows) {
11
+ const file = path.join(home, ".cursor", "projects", rel);
12
+ fs.mkdirSync(path.dirname(file), { recursive: true });
13
+ fs.writeFileSync(file, rows.map((r) => JSON.stringify(r)).join("\n"));
14
+ return file;
15
+ }
16
+ test("maskSecrets masks common token shapes (AC-3)", () => {
17
+ const cases = [
18
+ ["sk-abcdef0123456789abcdef", "sk-••••••••"],
19
+ ["ghp_0123456789abcdefghijklmnopqrstuvwxyz", "ghp_••••••••"],
20
+ ["xoxb-0123456789abcdefGHI", "xox•-••••••••"],
21
+ ];
22
+ for (const [raw, masked] of cases) {
23
+ assert.equal(maskSecrets(raw).includes(masked), true);
24
+ assert.equal(maskSecrets(raw).includes(raw), false);
25
+ }
26
+ const kv = maskSecrets("deploy with password=hunter2-secret now");
27
+ assert.equal(kv.includes("hunter2"), false);
28
+ // 普通文本不受影响
29
+ assert.equal(maskSecrets("fix the login bug please"), "fix the login bug please");
30
+ });
31
+ test("maskSecrets masks long hex / base64 blobs", () => {
32
+ const hex = "a".repeat(48);
33
+ const b64 = "AbcD" + "e6Fg7H" + "a9b8c7d6e5f4g3h2i1j0k9l8m7n6o5p4q3r2s1"; // ≥40 随机形态
34
+ for (const raw of [hex, b64]) {
35
+ const out = maskSecrets(raw);
36
+ assert.equal(out.includes(raw), false, `leaked: ${raw}`);
37
+ }
38
+ });
39
+ test("captureTranscriptSnapshot parses jsonl, caps long messages, masks secrets", () => {
40
+ const home = makeHome();
41
+ try {
42
+ const long = "x".repeat(TRANSCRIPT_MESSAGE_MAX_CHARS + 500);
43
+ const file = writeJsonl(home, "Users-dev-work-app/agent-transcripts/id1/id1.jsonl", [
44
+ { role: "user", message: { content: [{ type: "text", text: "Fix login with sk-abcdef0123456789abcdef" }] }, ts: "2026-09-24T10:00:00.000Z" },
45
+ { role: "assistant", message: { content: [{ type: "text", text: long }] }, ts: "2026-09-24T10:00:05.000Z" },
46
+ { role: "tool", message: { content: [{ type: "text", text: "ignored role" }] } },
47
+ "not-json",
48
+ { role: "user", message: { content: [{ type: "text", text: "<timestamp>x</timestamp>hi <user_query>there" }] }, ts: "2026-09-24T10:01:00.000Z" },
49
+ ]);
50
+ const snap = captureTranscriptSnapshot(file, new Date("2026-09-24T12:00:00.000Z"));
51
+ assert.ok(snap);
52
+ assert.equal(snap.sourceKey, `cursor-transcript:${file}`);
53
+ assert.equal(snap.messages.length, 3);
54
+ assert.equal(snap.messages[0].role, "user");
55
+ assert.equal(snap.messages[0].text.includes("sk-abcdef"), false);
56
+ assert.ok(snap.messages[0].text.includes("sk-••••••••"));
57
+ assert.ok(snap.messages[1].text.length <= TRANSCRIPT_MESSAGE_MAX_CHARS + 1);
58
+ assert.ok(snap.messages[1].text.endsWith("…"));
59
+ // timestamp 标签被清理
60
+ assert.ok(!snap.messages[2].text.includes("timestamp"));
61
+ assert.equal(snap.messages[2].text, "hi there");
62
+ assert.equal(snap.truncated, false);
63
+ assert.equal(snap.capturedAt, "2026-09-24T12:00:00.000Z");
64
+ }
65
+ finally {
66
+ fs.rmSync(home, { recursive: true, force: true });
67
+ }
68
+ });
69
+ test("captureTranscriptSnapshot keeps near-end window and sets truncated (AC-4)", () => {
70
+ const home = makeHome();
71
+ try {
72
+ const rows = Array.from({ length: TRANSCRIPT_WINDOW_MESSAGES + 27 }, (_, i) => ({
73
+ role: "user",
74
+ message: `msg-${i}`,
75
+ }));
76
+ const file = writeJsonl(home, "p/agent-transcripts/id2/id2.jsonl", rows);
77
+ const snap = captureTranscriptSnapshot(file);
78
+ assert.ok(snap);
79
+ assert.equal(snap.messages.length, TRANSCRIPT_WINDOW_MESSAGES);
80
+ assert.equal(snap.truncated, true);
81
+ assert.ok(snap.windowTotal);
82
+ assert.match(snap.windowTotal, new RegExp(`${TRANSCRIPT_WINDOW_MESSAGES} / ${rows.length}`));
83
+ // 近端:窗口第一条是倒数第 40 条
84
+ assert.equal(snap.messages[0].text, `msg-${rows.length - TRANSCRIPT_WINDOW_MESSAGES}`);
85
+ }
86
+ finally {
87
+ fs.rmSync(home, { recursive: true, force: true });
88
+ }
89
+ });
90
+ test("captureTranscriptSnapshot empty / missing files (AC-1 空态)", () => {
91
+ const home = makeHome();
92
+ try {
93
+ const file = writeJsonl(home, "p/agent-transcripts/id3/id3.jsonl", [
94
+ { role: "tool", message: "no user/assistant rows" },
95
+ ]);
96
+ const empty = captureTranscriptSnapshot(file);
97
+ assert.ok(empty);
98
+ assert.deepEqual(empty.messages, []);
99
+ const missing = captureTranscriptSnapshot(path.join(home, "nope.jsonl"));
100
+ assert.equal(missing, undefined);
101
+ }
102
+ finally {
103
+ fs.rmSync(home, { recursive: true, force: true });
104
+ }
105
+ });
106
+ test("transcriptFileForSourceKey enforces ~/.cursor/projects containment (invariants §4)", () => {
107
+ const home = makeHome();
108
+ try {
109
+ const inside = path.join(home, ".cursor", "projects", "slug", "agent-transcripts", "id", "id.jsonl");
110
+ assert.equal(transcriptFileForSourceKey(`cursor-transcript:${inside}`, home), inside);
111
+ // 逃逸出 projects 根 → 拒绝
112
+ const outside = path.join(home, ".ssh", "id_ed25519");
113
+ assert.equal(transcriptFileForSourceKey(`cursor-transcript:${outside}`, home), undefined);
114
+ // 其他前缀 / 非路径
115
+ assert.equal(transcriptFileForSourceKey(`cursor-session:${inside}`, home), undefined);
116
+ assert.equal(transcriptFileForSourceKey("cursor-transcript:", home), undefined);
117
+ }
118
+ finally {
119
+ fs.rmSync(home, { recursive: true, force: true });
120
+ }
121
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@agentoryhq/node",
3
+ "version": "0.1.1",
4
+ "description": "Agentory Node CLI — register, discover, align, daemon",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "agentory-node": "./dist/cli.js"
9
+ },
10
+ "files": ["dist", "README.md"],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "start": "node dist/cli.js"
14
+ },
15
+ "dependencies": {
16
+ "@agentoryhq/adapter-claude-code": "*",
17
+ "@agentoryhq/adapter-cursor": "*",
18
+ "@agentoryhq/runtime-contract": "*",
19
+ "@agentoryhq/shared": "*"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/tunsuy/agentory-node.git",
24
+ "directory": "apps/node"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ }
32
+ }