@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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @agentoryhq/node
2
+
3
+ CLI `agentory-node` for Agentory: register with a control plane, discover local Agents, apply confirmed align patches, run the daemon.
4
+
5
+ Open-source Host lives in [agentory-node](https://github.com/tunsuy/agentory-node). Control plane is separate/product.
@@ -0,0 +1,28 @@
1
+ import type { AlignPatch } from "@agentoryhq/shared";
2
+ export type ApplyAlignOpts = {
3
+ /** Project root for the runtime (Cursor: `.cursor/`; Claude: `.mcp.json` root). */
4
+ cursorRoot: string;
5
+ patch: AlignPatch;
6
+ confirmed: boolean;
7
+ runtime?: string;
8
+ };
9
+ export type ApplyAlignResult = {
10
+ ok: boolean;
11
+ error?: string;
12
+ mcpPath: string;
13
+ skillsPath: string;
14
+ snapshotPath?: string;
15
+ };
16
+ /** Host facade — routes to the matching RuntimeAdapter.applyAlign. */
17
+ export declare function applyAlignPatch(opts: ApplyAlignOpts): ApplyAlignResult;
18
+ /** @deprecated Prefer applyAlignPatch({ runtime }) — kept for explicit cursor tests. */
19
+ export declare function applyCursorAlignPatch(opts: {
20
+ cursorRoot: string;
21
+ patch: AlignPatch;
22
+ confirmed: boolean;
23
+ }): ApplyAlignResult;
24
+ export declare function applyClaudeAlignPatch(opts: {
25
+ projectRoot: string;
26
+ patch: AlignPatch;
27
+ confirmed: boolean;
28
+ }): ApplyAlignResult;
package/dist/align.js ADDED
@@ -0,0 +1,32 @@
1
+ import { applyAlignPatch as cursorApplyAlign } from "@agentoryhq/adapter-cursor";
2
+ import { applyAlignPatch as claudeApplyAlign } from "@agentoryhq/adapter-claude-code";
3
+ import { adapterForRuntime } from "./registry.js";
4
+ /** Host facade — routes to the matching RuntimeAdapter.applyAlign. */
5
+ export function applyAlignPatch(opts) {
6
+ const runtime = opts.runtime ?? "cursor";
7
+ const adapter = adapterForRuntime(runtime);
8
+ if (!adapter?.applyAlign) {
9
+ return {
10
+ ok: false,
11
+ error: `no applyAlign adapter for runtime ${runtime}`,
12
+ mcpPath: "",
13
+ skillsPath: "",
14
+ };
15
+ }
16
+ return adapter.applyAlign({
17
+ projectRoot: opts.cursorRoot,
18
+ patch: opts.patch,
19
+ confirmed: opts.confirmed,
20
+ });
21
+ }
22
+ /** @deprecated Prefer applyAlignPatch({ runtime }) — kept for explicit cursor tests. */
23
+ export function applyCursorAlignPatch(opts) {
24
+ return cursorApplyAlign({
25
+ projectRoot: opts.cursorRoot,
26
+ patch: opts.patch,
27
+ confirmed: opts.confirmed,
28
+ });
29
+ }
30
+ export function applyClaudeAlignPatch(opts) {
31
+ return claudeApplyAlign(opts);
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
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 { applyAlignPatch } from "./align.js";
7
+ test("applyAlignPatch refuses unconfirmed write", () => {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "agy-aln-"));
9
+ fs.mkdirSync(path.join(root, ".cursor"), { recursive: true });
10
+ const mcpPath = path.join(root, ".cursor", "mcp.json");
11
+ fs.writeFileSync(mcpPath, JSON.stringify({ mcpServers: { a: { command: "old" } } }));
12
+ const r = applyAlignPatch({
13
+ cursorRoot: root,
14
+ patch: { mcp: [{ name: "a", command: "npx new" }], skills: [] },
15
+ confirmed: false,
16
+ });
17
+ assert.equal(r.ok, false);
18
+ assert.match(fs.readFileSync(mcpPath, "utf8"), /old/);
19
+ });
20
+ test("applyAlignPatch writes mcp+skills with snapshot", () => {
21
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "agy-aln-"));
22
+ fs.mkdirSync(path.join(root, ".cursor"), { recursive: true });
23
+ const mcpPath = path.join(root, ".cursor", "mcp.json");
24
+ fs.writeFileSync(mcpPath, JSON.stringify({ mcpServers: { github: { command: "npx", args: ["live"] } } }));
25
+ const r = applyAlignPatch({
26
+ cursorRoot: root,
27
+ patch: {
28
+ mcp: [
29
+ { name: "github", command: "npx desired" },
30
+ { name: "extra", command: "npx extra" },
31
+ ],
32
+ skills: ["brief"],
33
+ },
34
+ confirmed: true,
35
+ });
36
+ assert.equal(r.ok, true);
37
+ assert.ok(r.snapshotPath && fs.existsSync(r.snapshotPath));
38
+ const after = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
39
+ assert.equal(after.mcpServers.github.command, "npx");
40
+ assert.deepEqual(after.mcpServers.github.args, ["desired"]);
41
+ assert.ok(after.mcpServers.extra);
42
+ const skills = JSON.parse(fs.readFileSync(path.join(root, ".cursor", "agentory-skills.json"), "utf8"));
43
+ assert.deepEqual(skills.skills, ["brief"]);
44
+ });
45
+ test("applyAlignPatch routes claude-code to .mcp.json not .cursor/", () => {
46
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "agy-aln-claude-"));
47
+ fs.mkdirSync(path.join(root, ".cursor"), { recursive: true });
48
+ fs.writeFileSync(path.join(root, ".cursor", "mcp.json"), JSON.stringify({ mcpServers: { keep: { command: "cursor-only" } } }));
49
+ fs.writeFileSync(path.join(root, ".mcp.json"), JSON.stringify({ mcpServers: { a: { command: "old" } } }));
50
+ const r = applyAlignPatch({
51
+ cursorRoot: root,
52
+ runtime: "claude-code",
53
+ patch: { mcp: [{ name: "a", command: "npx claude" }], skills: ["s"] },
54
+ confirmed: true,
55
+ });
56
+ assert.equal(r.ok, true);
57
+ const claudeMcp = JSON.parse(fs.readFileSync(path.join(root, ".mcp.json"), "utf8"));
58
+ assert.equal(claudeMcp.mcpServers.a.command, "npx");
59
+ assert.deepEqual(claudeMcp.mcpServers.a.args, ["claude"]);
60
+ const cursorMcp = JSON.parse(fs.readFileSync(path.join(root, ".cursor", "mcp.json"), "utf8"));
61
+ assert.equal(cursorMcp.mcpServers.keep?.command, "cursor-only");
62
+ assert.ok(fs.existsSync(path.join(root, ".claude", "agentory-skills.json")));
63
+ });
@@ -0,0 +1,15 @@
1
+ import type { MemoryIndexReportItem, SessionIndexReportItem } from "@agentoryhq/shared";
2
+ export type AssetIndexDiscoverOpts = {
3
+ homeDir?: string;
4
+ cwd?: string;
5
+ /** JSON fixture: { sessions?, memories? } or AGENTORY_ASSET_INDEX_FIXTURE */
6
+ fixturePath?: string;
7
+ };
8
+ /**
9
+ * Aggregate asset index: Host fixtures/drop-in + each adapter with `indexSessions`.
10
+ */
11
+ export declare function discoverAssetIndex(opts?: AssetIndexDiscoverOpts): {
12
+ sessions: SessionIndexReportItem[];
13
+ memories: MemoryIndexReportItem[];
14
+ };
15
+ export { buildCursorProjectPathMap, titleFromTranscriptJsonl, } from "@agentoryhq/adapter-cursor";
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { builtinAdapters } from "./registry.js";
5
+ function readJsonFile(file) {
6
+ try {
7
+ return JSON.parse(fs.readFileSync(file, "utf8"));
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
13
+ /**
14
+ * Aggregate asset index: Host fixtures/drop-in + each adapter with `indexSessions`.
15
+ */
16
+ export function discoverAssetIndex(opts = {}) {
17
+ const fixture = opts.fixturePath ?? process.env.AGENTORY_ASSET_INDEX_FIXTURE;
18
+ if (fixture) {
19
+ const raw = readJsonFile(fixture);
20
+ if (raw && typeof raw === "object") {
21
+ const o = raw;
22
+ return {
23
+ sessions: Array.isArray(o.sessions) ? o.sessions : [],
24
+ memories: Array.isArray(o.memories) ? o.memories : [],
25
+ };
26
+ }
27
+ }
28
+ const cwd = opts.cwd ?? process.cwd();
29
+ const homeDir = opts.homeDir ?? os.homedir();
30
+ const sessions = [];
31
+ const memories = [];
32
+ const seen = new Set();
33
+ const pushSession = (s) => {
34
+ if (seen.has(s.sourceKey))
35
+ return;
36
+ seen.add(s.sourceKey);
37
+ sessions.push(s);
38
+ };
39
+ const dropIn = path.join(cwd, ".agentory", "session-index.json");
40
+ const drop = readJsonFile(dropIn);
41
+ if (drop && typeof drop === "object") {
42
+ const o = drop;
43
+ if (Array.isArray(o.sessions)) {
44
+ for (const s of o.sessions)
45
+ pushSession(s);
46
+ }
47
+ }
48
+ const ctx = { homeDir, cwd };
49
+ for (const adapter of builtinAdapters) {
50
+ if (!adapter.indexSessions)
51
+ continue;
52
+ const part = adapter.indexSessions(ctx);
53
+ for (const s of part.sessions)
54
+ pushSession(s);
55
+ memories.push(...part.memories);
56
+ }
57
+ sessions.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0));
58
+ return { sessions, memories };
59
+ }
60
+ export { buildCursorProjectPathMap, titleFromTranscriptJsonl, } from "@agentoryhq/adapter-cursor";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
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 { discoverAssetIndex } from "./asset-index.js";
7
+ test("discoverAssetIndex reads rules and session meta without bodies", () => {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "agentory-idx-"));
9
+ try {
10
+ const rules = path.join(root, ".cursor", "rules");
11
+ fs.mkdirSync(rules, { recursive: true });
12
+ fs.writeFileSync(path.join(rules, "harness.mdc"), "SECRET=should-not-appear-in-index\n");
13
+ const sessions = path.join(root, ".cursor", "sessions");
14
+ fs.mkdirSync(sessions, { recursive: true });
15
+ fs.writeFileSync(path.join(sessions, "a.meta.json"), JSON.stringify({
16
+ displayName: "Demo session",
17
+ projectPath: root,
18
+ updatedAt: "2026-09-16T10:00:00.000Z",
19
+ }));
20
+ const out = discoverAssetIndex({ cwd: root, homeDir: root });
21
+ assert.equal(out.sessions.length, 1);
22
+ assert.equal(out.sessions[0].displayName, "Demo session");
23
+ assert.ok(!JSON.stringify(out).includes("SECRET"));
24
+ assert.equal(out.memories.length, 1);
25
+ assert.equal(out.memories[0].kind, "rules");
26
+ assert.match(out.memories[0].pathLabel, /harness\.mdc/);
27
+ }
28
+ finally {
29
+ fs.rmSync(root, { recursive: true, force: true });
30
+ }
31
+ });
32
+ test("discoverAssetIndex fixture path", () => {
33
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "agentory-fix-"));
34
+ try {
35
+ const file = path.join(root, "fixture.json");
36
+ fs.writeFileSync(file, JSON.stringify({
37
+ sessions: [
38
+ {
39
+ sourceKey: "fx1",
40
+ displayName: "From fixture",
41
+ updatedAt: "2026-09-01T00:00:00.000Z",
42
+ },
43
+ ],
44
+ memories: [],
45
+ }));
46
+ const out = discoverAssetIndex({ fixturePath: file, cwd: root });
47
+ assert.equal(out.sessions.length, 1);
48
+ assert.equal(out.sessions[0].displayName, "From fixture");
49
+ }
50
+ finally {
51
+ fs.rmSync(root, { recursive: true, force: true });
52
+ }
53
+ });
54
+ test("discoverAssetIndex reads native agent-transcripts metadata only", () => {
55
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "agentory-home-"));
56
+ try {
57
+ const id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
58
+ const projSlug = "Users-demo-work-app";
59
+ const projectPath = "/Users/demo/work/app";
60
+ const jsonl = path.join(home, ".cursor", "projects", projSlug, "agent-transcripts", id, `${id}.jsonl`);
61
+ fs.mkdirSync(path.dirname(jsonl), { recursive: true });
62
+ const secret = "SUPER_SECRET_BODY_SHOULD_NOT_LEAK";
63
+ fs.writeFileSync(jsonl, [
64
+ JSON.stringify({
65
+ role: "user",
66
+ message: { content: [{ type: "text", text: "Fix the login bug please" }] },
67
+ }),
68
+ JSON.stringify({
69
+ role: "assistant",
70
+ message: { content: [{ type: "text", text: `I will help ${secret}` }] },
71
+ }),
72
+ ].join("\n"));
73
+ const ws = path.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage", "ws1");
74
+ fs.mkdirSync(ws, { recursive: true });
75
+ fs.writeFileSync(path.join(ws, "workspace.json"), JSON.stringify({ folder: `file://${projectPath}` }));
76
+ const out = discoverAssetIndex({ cwd: home, homeDir: home });
77
+ assert.equal(out.sessions.length, 1);
78
+ assert.equal(out.sessions[0].projectPath, projectPath);
79
+ assert.match(out.sessions[0].displayName, /Fix the login bug/);
80
+ assert.ok(!out.sessions[0].displayName.includes(secret));
81
+ assert.ok(!JSON.stringify(out).includes(secret));
82
+ assert.match(out.sessions[0].sourceKey, /cursor-transcript:/);
83
+ }
84
+ finally {
85
+ fs.rmSync(home, { recursive: true, force: true });
86
+ }
87
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function runCli(argv: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { PKG_VERSION, controlPlaneBaseUrl, jsonHealth } from "@agentoryhq/shared";
5
+ import { applyPendingAligns, fulfillPendingTranscripts, heartbeat, registerNode, reportAssetIndex, reportDiscovery } from "./client.js";
6
+ import { discoverAssetIndex } from "./asset-index.js";
7
+ import { discoverAgents } from "./discover.js";
8
+ import { runDaemon } from "./daemon.js";
9
+ function flag(argv, name) {
10
+ const i = argv.indexOf(name);
11
+ if (i < 0)
12
+ return undefined;
13
+ return argv[i + 1];
14
+ }
15
+ export async function runCli(argv) {
16
+ const cmd = argv[2] ?? "version";
17
+ if (cmd === "version" || cmd === "-v" || cmd === "--version") {
18
+ const body = jsonHealth("node");
19
+ console.log(`${body.service} ${PKG_VERSION}`);
20
+ return 0;
21
+ }
22
+ if (cmd === "ping") {
23
+ const base = controlPlaneBaseUrl();
24
+ try {
25
+ const res = await fetch(`${base}/health`);
26
+ const text = await res.text();
27
+ console.log(res.status, text);
28
+ return res.ok ? 0 : 1;
29
+ }
30
+ catch (err) {
31
+ console.error(`unreachable ${base}:`, err instanceof Error ? err.message : err);
32
+ return 2;
33
+ }
34
+ }
35
+ if (cmd === "register") {
36
+ const token = flag(argv, "--token") ?? process.env.AGENTORY_NODE_ROOT_TOKEN;
37
+ if (!token) {
38
+ console.error("usage: agentory-node register --token <node_root_token>");
39
+ return 2;
40
+ }
41
+ const name = flag(argv, "--name") ?? "local";
42
+ return registerNode(token, name);
43
+ }
44
+ if (cmd === "heartbeat")
45
+ return heartbeat();
46
+ if (cmd === "discover") {
47
+ if (argv.includes("--dry-run")) {
48
+ console.log(JSON.stringify({ agents: discoverAgents() }, null, 2));
49
+ return 0;
50
+ }
51
+ return reportDiscovery();
52
+ }
53
+ if (cmd === "asset-index") {
54
+ if (argv.includes("--dry-run")) {
55
+ console.log(JSON.stringify(discoverAssetIndex(), null, 2));
56
+ return 0;
57
+ }
58
+ return reportAssetIndex();
59
+ }
60
+ if (cmd === "align") {
61
+ return applyPendingAligns();
62
+ }
63
+ if (cmd === "transcript") {
64
+ return fulfillPendingTranscripts();
65
+ }
66
+ if (cmd === "daemon") {
67
+ return runDaemon({ once: argv.includes("--once") });
68
+ }
69
+ console.error("usage: agentory-node <version|ping|register|heartbeat|discover|asset-index|align|transcript|daemon [--once]>");
70
+ return 2;
71
+ }
72
+ const thisFile = fileURLToPath(import.meta.url);
73
+ function isCliEntry(argv1) {
74
+ if (!argv1)
75
+ return false;
76
+ try {
77
+ const resolved = fileURLToPath(pathToFileURL(path.resolve(argv1)));
78
+ if (resolved === thisFile)
79
+ return true;
80
+ }
81
+ catch {
82
+ /* ignore */
83
+ }
84
+ const base = path.basename(argv1);
85
+ return base === "agentory-node" || base === "agentory-node.js";
86
+ }
87
+ if (isCliEntry(process.argv[1])) {
88
+ runCli(process.argv).then((code) => process.exit(code));
89
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { runCli } from "./cli.js";
4
+ test("version exits 0", async () => {
5
+ const code = await runCli(["node", "cli.js", "version"]);
6
+ assert.equal(code, 0);
7
+ });
@@ -0,0 +1,20 @@
1
+ export type NodeState = {
2
+ workspaceId?: string;
3
+ nodeId: string;
4
+ nodeToken: string;
5
+ controlPlaneBaseUrl: string;
6
+ };
7
+ export declare function loadState(): NodeState | undefined;
8
+ export declare function saveState(state: NodeState): void;
9
+ export declare function registerNode(rootToken: string, name: string): Promise<number>;
10
+ export declare function heartbeat(): Promise<number>;
11
+ export declare function reportDiscovery(): Promise<number>;
12
+ export declare function reportAssetIndex(): Promise<number>;
13
+ /**
14
+ * Model B pull: fetch pending transcript requests, capture each session
15
+ * locally (mask + window, never logs the body — AC-9), POST snapshots back.
16
+ * Called from the daemon index channel and `agentory-node transcript` CLI.
17
+ */
18
+ export declare function fulfillPendingTranscripts(): Promise<number>;
19
+ /** Pull confirmed plans and write Cursor files (AC-3). */
20
+ export declare function applyPendingAligns(): Promise<number>;
package/dist/client.js ADDED
@@ -0,0 +1,198 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { controlPlaneBaseUrl } from "@agentoryhq/shared";
5
+ import { applyAlignPatch } from "./align.js";
6
+ import { discoverAssetIndex } from "./asset-index.js";
7
+ import { discoverAgentsWithHealth } from "./discover.js";
8
+ import { captureTranscriptSnapshot, transcriptFileForSourceKey } from "./transcript.js";
9
+ function statePath() {
10
+ return process.env.AGENTORY_NODE_STATE ?? path.join(os.homedir(), ".agentory", "node.json");
11
+ }
12
+ export function loadState() {
13
+ try {
14
+ return JSON.parse(fs.readFileSync(statePath(), "utf8"));
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ export function saveState(state) {
21
+ const file = statePath();
22
+ fs.mkdirSync(path.dirname(file), { recursive: true });
23
+ fs.writeFileSync(file, JSON.stringify(state, null, 2), { mode: 0o600 });
24
+ }
25
+ function cursorRoot() {
26
+ return process.env.AGENTORY_CURSOR_ROOT?.trim() || process.cwd();
27
+ }
28
+ async function api(method, urlPath, opts) {
29
+ const res = await fetch(`${controlPlaneBaseUrl()}${urlPath}`, {
30
+ method,
31
+ headers: {
32
+ authorization: `Bearer ${opts.token}`,
33
+ "content-type": "application/json",
34
+ },
35
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
36
+ });
37
+ const body = (await res.json());
38
+ return { status: res.status, body };
39
+ }
40
+ export async function registerNode(rootToken, name) {
41
+ const hostname = os.hostname();
42
+ const { status, body } = await api("POST", "/v1/nodes/register", {
43
+ token: rootToken,
44
+ body: { name, hostname },
45
+ });
46
+ if (status !== 200) {
47
+ console.error(status, body);
48
+ return 1;
49
+ }
50
+ const node = body.node;
51
+ const nodeToken = body.nodeToken;
52
+ saveState({
53
+ workspaceId: node.workspaceId,
54
+ nodeId: node.id,
55
+ nodeToken,
56
+ controlPlaneBaseUrl: controlPlaneBaseUrl(),
57
+ });
58
+ console.log(JSON.stringify({ nodeId: node.id, workspaceId: node.workspaceId }, null, 2));
59
+ return 0;
60
+ }
61
+ export async function heartbeat() {
62
+ const st = loadState();
63
+ if (!st) {
64
+ console.error("not registered; run: agentory-node register --token <root>");
65
+ return 2;
66
+ }
67
+ const { status, body } = await api("POST", `/v1/nodes/${st.nodeId}/heartbeat`, {
68
+ token: st.nodeToken,
69
+ });
70
+ console.log(status, JSON.stringify(body));
71
+ return status === 200 ? 0 : 1;
72
+ }
73
+ export async function reportDiscovery() {
74
+ const st = loadState();
75
+ if (!st) {
76
+ console.error("not registered; run: agentory-node register --token <root>");
77
+ return 2;
78
+ }
79
+ const agents = await discoverAgentsWithHealth();
80
+ const { status, body } = await api("POST", `/v1/nodes/${st.nodeId}/discovery`, {
81
+ token: st.nodeToken,
82
+ body: { agents },
83
+ });
84
+ console.log(status, JSON.stringify(body, null, 2));
85
+ return status === 200 ? 0 : 1;
86
+ }
87
+ export async function reportAssetIndex() {
88
+ const st = loadState();
89
+ if (!st) {
90
+ console.error("not registered; run: agentory-node register --token <root>");
91
+ return 2;
92
+ }
93
+ const index = discoverAssetIndex();
94
+ const { status, body } = await api("POST", `/v1/nodes/${st.nodeId}/asset-index`, {
95
+ token: st.nodeToken,
96
+ body: index,
97
+ });
98
+ console.log(status, JSON.stringify(body, null, 2));
99
+ return status === 200 ? 0 : 1;
100
+ }
101
+ /**
102
+ * Model B pull: fetch pending transcript requests, capture each session
103
+ * locally (mask + window, never logs the body — AC-9), POST snapshots back.
104
+ * Called from the daemon index channel and `agentory-node transcript` CLI.
105
+ */
106
+ export async function fulfillPendingTranscripts() {
107
+ const st = loadState();
108
+ if (!st) {
109
+ console.error("not registered; run: agentory-node register --token <root>");
110
+ return 2;
111
+ }
112
+ const { status, body } = await api("GET", `/v1/nodes/${st.nodeId}/transcript/pending`, {
113
+ token: st.nodeToken,
114
+ });
115
+ if (status !== 200) {
116
+ console.error(status, JSON.stringify(body));
117
+ return 1;
118
+ }
119
+ const pending = body.requests ?? [];
120
+ if (!pending.length) {
121
+ console.log(JSON.stringify({ fulfilled: 0, requests: [] }, null, 2));
122
+ return 0;
123
+ }
124
+ const snapshots = [];
125
+ const misses = [];
126
+ for (const req of pending) {
127
+ const file = transcriptFileForSourceKey(req.sourceKey);
128
+ const snap = file ? captureTranscriptSnapshot(file) : undefined;
129
+ if (snap)
130
+ snapshots.push(snap);
131
+ else
132
+ misses.push(req.sourceKey);
133
+ }
134
+ const { status: reportStatus, body: reportBody } = await api("POST", `/v1/nodes/${st.nodeId}/transcript/report`, {
135
+ token: st.nodeToken,
136
+ body: { snapshots, missing: misses },
137
+ });
138
+ if (reportStatus !== 200) {
139
+ console.error(reportStatus, JSON.stringify(reportBody));
140
+ return 1;
141
+ }
142
+ // 审计侧只回 count——快照正文不落节点日志(AC-9)。
143
+ console.log(JSON.stringify({
144
+ fulfilled: snapshots.length,
145
+ missing: misses.length,
146
+ reported: reportBody.snapshots,
147
+ }, null, 2));
148
+ return 0;
149
+ }
150
+ /** Pull confirmed plans and write Cursor files (AC-3). */
151
+ export async function applyPendingAligns() {
152
+ const st = loadState();
153
+ if (!st) {
154
+ console.error("not registered; run: agentory-node register --token <root>");
155
+ return 2;
156
+ }
157
+ const { status, body } = await api("GET", `/v1/nodes/${st.nodeId}/align/pending`, {
158
+ token: st.nodeToken,
159
+ });
160
+ if (status !== 200) {
161
+ console.error(status, body);
162
+ return 1;
163
+ }
164
+ const plans = body.plans ?? [];
165
+ if (!plans.length) {
166
+ console.log(JSON.stringify({ applied: 0, plans: [] }, null, 2));
167
+ return 0;
168
+ }
169
+ const root = cursorRoot();
170
+ const results = [];
171
+ let failed = 0;
172
+ for (const plan of plans) {
173
+ const local = applyAlignPatch({
174
+ cursorRoot: root,
175
+ patch: plan.patch,
176
+ confirmed: plan.status === "confirmed",
177
+ runtime: plan.runtime,
178
+ });
179
+ const report = await api("POST", `/v1/nodes/${st.nodeId}/align/${plan.id}/result`, {
180
+ token: st.nodeToken,
181
+ body: {
182
+ ok: local.ok,
183
+ error: local.error,
184
+ snapshotPath: local.snapshotPath,
185
+ },
186
+ });
187
+ if (!local.ok || report.status !== 200)
188
+ failed += 1;
189
+ results.push({
190
+ planId: plan.id,
191
+ local,
192
+ reportStatus: report.status,
193
+ plan: report.body.plan,
194
+ });
195
+ }
196
+ console.log(JSON.stringify({ applied: plans.length - failed, failed, results }, null, 2));
197
+ return failed ? 1 : 0;
198
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resident daemon: periodic heartbeat (+ align pull), asset-index report
3
+ * (sha-deduped), discovery report. Pure outbound — the control plane never
4
+ * reaches back into the node (invariants §4/§5).
5
+ */
6
+ export type DaemonIntervals = {
7
+ heartbeatSec: number;
8
+ indexSec: number;
9
+ discoverSec: number;
10
+ };
11
+ export type DaemonOpts = {
12
+ intervals?: Partial<DaemonIntervals>;
13
+ /** Run every channel once, then exit 0 (cron / testing entry). */
14
+ once?: boolean;
15
+ /** 测试注入:可控时钟与睡眠。 */
16
+ clock?: () => number;
17
+ sleep?: (ms: number) => Promise<void>;
18
+ signalHandlers?: NodeJS.Signals[];
19
+ /** 提早注入信号(测试)。 */
20
+ registerSignal?: (onSignal: (sig: NodeJS.Signals) => void) => void;
21
+ };
22
+ /** Env override with hard clamp to MIN_INTERVAL_SEC (AC-6). */
23
+ export declare function parseIntervalSec(envName: string, fallback: number): number;
24
+ export declare function resolveIntervals(): DaemonIntervals;
25
+ /** 0–10% of the period of jitter, so a fleet of nodes doesn't fire in unison. */
26
+ export declare function jitterMs(periodSec: number, rand?: () => number): number;
27
+ export declare function assetIndexSha(index: unknown): string;
28
+ /** Report the index only when its content hash changed since last report (AC-3). */
29
+ export declare function reportAssetIndexIfChanged(): Promise<number>;
30
+ /** One full round: all channels (--once 与首轮同语义,AC-1)。失败通道 → 非 0。 */
31
+ export declare function runFullRound(): Promise<number>;
32
+ export declare function runDaemon(opts?: DaemonOpts): Promise<number>;
@@ -0,0 +1 @@
1
+ export {};