@linkedclaw/cli 0.1.3 → 0.1.6
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 +172 -10
- package/dist/bin.js +2037 -185
- package/dist/bin.js.map +1 -1
- package/package.json +4 -4
- package/src/arena/api.ts +154 -0
- package/src/arena/hash.ts +15 -0
- package/src/arena/types.ts +106 -0
- package/src/bin.ts +12 -2
- package/src/commands/agent.ts +264 -0
- package/src/commands/arena.ts +393 -0
- package/src/commands/converge.ts +969 -0
- package/src/commands/provider.ts +8 -8
- package/src/commands/requester.ts +64 -21
- package/src/config.ts +11 -2
- package/src/converge/api.ts +213 -0
- package/src/converge/hash.ts +35 -0
- package/src/converge/lock.ts +30 -0
- package/src/converge/staging.ts +83 -0
- package/src/converge/types.ts +91 -0
- package/src/converge/workspace.ts +92 -0
- package/src/handlers/subprocess.ts +8 -8
- package/src/types.ts +5 -5
- package/test/agent-help.test.ts +207 -0
- package/test/arena-api.test.ts +211 -0
- package/test/arena-commands.test.ts +559 -0
- package/test/arena-hash.test.ts +33 -0
- package/test/cli-help.test.ts +23 -3
- package/test/converge-accept.test.ts +206 -0
- package/test/converge-decision.test.ts +274 -0
- package/test/converge-hash.test.ts +58 -0
- package/test/converge-help.test.ts +58 -0
- package/test/converge-lock.test.ts +48 -0
- package/test/converge-review.test.ts +135 -0
- package/test/converge-run.test.ts +286 -0
- package/test/converge-staging.test.ts +161 -0
- package/test/converge-status.test.ts +141 -0
- package/test/converge-workspace.test.ts +92 -0
package/dist/bin.js
CHANGED
|
@@ -7,50 +7,218 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/bin.ts
|
|
9
9
|
import { Command } from "commander";
|
|
10
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
10
13
|
|
|
11
|
-
// src/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return process.env["LINKEDCLAW_CONFIG_DIR"] ?? join(homedir(), ".linkedclaw");
|
|
14
|
+
// src/commands/agent.ts
|
|
15
|
+
import { spawn } from "child_process";
|
|
16
|
+
import { accessSync, constants, statSync } from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
function registerAgentCommands(program2) {
|
|
19
|
+
const agent = program2.command("agent").description("Owner-agent runtime commands");
|
|
20
|
+
agent.command("run").description("Run a long-lived owner agent from a local config").option("--config <path>", "Owner-agent config path").option("--watch <debate_id:commons_log_id>", "Watch a debate Commons Log; repeatable", collect, []).option("--once", "Process pending local tasks once, then exit").option("--python-command <cmd>", "Python executable to use").action((opts) => runOwnerAgent(opts));
|
|
21
|
+
agent.command("rotate-mandate").description("Issue a replacement owner-agent mandate, update local config, then revoke the old one").option("--config <path>", "Owner-agent config path").option("--old-mandate-id <id>", "Mandate id to replace; defaults to the configured transport mandate").option("--expires-at <iso>", "Replacement mandate expiry timestamp").option("--python-command <cmd>", "Python executable to use").action((opts) => rotateOwnerAgentMandate(opts));
|
|
20
22
|
}
|
|
21
|
-
function
|
|
22
|
-
return
|
|
23
|
+
function collect(value, previous) {
|
|
24
|
+
return [...previous, value];
|
|
23
25
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
function runOwnerAgent(opts) {
|
|
27
|
+
let pythonCommand;
|
|
28
|
+
let watches;
|
|
29
|
+
let configPath2;
|
|
30
|
+
try {
|
|
31
|
+
pythonCommand = resolvePythonCommand(opts.pythonCommand ?? process.env.LINKEDCLAW_OWNER_AGENT_PYTHON ?? "python3");
|
|
32
|
+
watches = (opts.watch ?? []).map(validateWatch);
|
|
33
|
+
const rawConfigPath = opts.config ?? process.env.LINKEDCLAW_OWNER_AGENT_CONFIG;
|
|
34
|
+
configPath2 = rawConfigPath === void 0 ? void 0 : validateConfigPath(rawConfigPath);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
error: "invalid_agent_run_option",
|
|
39
|
+
message: err instanceof Error ? err.message : String(err)
|
|
40
|
+
}) + "\n"
|
|
41
|
+
);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
31
44
|
}
|
|
32
|
-
|
|
45
|
+
const args = ["-m", "linkedclaw.owner_agent.cli", "run"];
|
|
46
|
+
if (configPath2 !== void 0) {
|
|
47
|
+
args.push("--config", configPath2);
|
|
48
|
+
}
|
|
49
|
+
for (const watch of watches) {
|
|
50
|
+
args.push("--watch", watch);
|
|
51
|
+
}
|
|
52
|
+
if (opts.once) {
|
|
53
|
+
args.push("--once");
|
|
54
|
+
}
|
|
55
|
+
spawnOwnerAgentPython(args, pythonCommand);
|
|
33
56
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
function rotateOwnerAgentMandate(opts) {
|
|
58
|
+
let pythonCommand;
|
|
59
|
+
let configPath2;
|
|
60
|
+
try {
|
|
61
|
+
pythonCommand = resolvePythonCommand(opts.pythonCommand ?? process.env.LINKEDCLAW_OWNER_AGENT_PYTHON ?? "python3");
|
|
62
|
+
const rawConfigPath = opts.config ?? process.env.LINKEDCLAW_OWNER_AGENT_CONFIG;
|
|
63
|
+
if (rawConfigPath === void 0) {
|
|
64
|
+
throw new Error("Config path required; pass --config or set LINKEDCLAW_OWNER_AGENT_CONFIG");
|
|
65
|
+
}
|
|
66
|
+
configPath2 = validateConfigPath(rawConfigPath);
|
|
67
|
+
if (opts.oldMandateId !== void 0) {
|
|
68
|
+
validateNonEmptyNoNul(opts.oldMandateId, "--old-mandate-id");
|
|
69
|
+
}
|
|
70
|
+
if (opts.expiresAt !== void 0) {
|
|
71
|
+
validateNonEmptyNoNul(opts.expiresAt, "--expires-at");
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
error: "invalid_agent_rotate_mandate_option",
|
|
77
|
+
message: err instanceof Error ? err.message : String(err)
|
|
78
|
+
}) + "\n"
|
|
79
|
+
);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const args = [
|
|
84
|
+
"-m",
|
|
85
|
+
"linkedclaw.owner_agent.cli",
|
|
86
|
+
"rotate-mandate",
|
|
87
|
+
"--config",
|
|
88
|
+
configPath2
|
|
89
|
+
];
|
|
90
|
+
if (opts.oldMandateId !== void 0) {
|
|
91
|
+
args.push("--old-mandate-id", opts.oldMandateId);
|
|
92
|
+
}
|
|
93
|
+
if (opts.expiresAt !== void 0) {
|
|
94
|
+
args.push("--expires-at", opts.expiresAt);
|
|
95
|
+
}
|
|
96
|
+
spawnOwnerAgentPython(args, pythonCommand);
|
|
39
97
|
}
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
...file,
|
|
48
|
-
...overrides,
|
|
49
|
-
cloudUrl,
|
|
50
|
-
relayUrl,
|
|
51
|
-
...apiKey !== void 0 ? { apiKey } : {}
|
|
98
|
+
function spawnOwnerAgentPython(args, pythonCommand) {
|
|
99
|
+
const child = spawn(pythonCommand, args, { stdio: "inherit" });
|
|
100
|
+
const forwardSignal = (signal) => {
|
|
101
|
+
if (!child.killed) {
|
|
102
|
+
child.kill(signal);
|
|
103
|
+
}
|
|
52
104
|
};
|
|
105
|
+
const onSigterm = () => forwardSignal("SIGTERM");
|
|
106
|
+
const onSigint = () => forwardSignal("SIGINT");
|
|
107
|
+
const cleanupSignals = () => {
|
|
108
|
+
process.off("SIGTERM", onSigterm);
|
|
109
|
+
process.off("SIGINT", onSigint);
|
|
110
|
+
};
|
|
111
|
+
process.on("SIGTERM", onSigterm);
|
|
112
|
+
process.on("SIGINT", onSigint);
|
|
113
|
+
child.on("error", (err) => {
|
|
114
|
+
cleanupSignals();
|
|
115
|
+
process.stderr.write(
|
|
116
|
+
JSON.stringify({ error: "owner_agent_python_unavailable", message: err.message }) + "\n"
|
|
117
|
+
);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
});
|
|
120
|
+
child.on("exit", (code, signal) => {
|
|
121
|
+
cleanupSignals();
|
|
122
|
+
process.exitCode = signal ? 1 : code ?? 1;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function validateNonEmptyNoNul(value, label) {
|
|
126
|
+
if (value.length === 0 || value.trim().length === 0) {
|
|
127
|
+
throw new Error(`${label} must be non-empty`);
|
|
128
|
+
}
|
|
129
|
+
if (value.includes("\0")) {
|
|
130
|
+
throw new Error(`${label} must not contain NUL bytes`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function validateWatch(value) {
|
|
134
|
+
const separator = value.indexOf(":");
|
|
135
|
+
if (separator <= 0 || separator === value.length - 1 || value.indexOf(":", separator + 1) !== -1) {
|
|
136
|
+
throw new Error("--watch must use debate_id:commons_log_id with both ids non-empty");
|
|
137
|
+
}
|
|
138
|
+
const debateId = value.slice(0, separator).trim();
|
|
139
|
+
const commonsLogId = value.slice(separator + 1).trim();
|
|
140
|
+
if (!debateId || !commonsLogId) {
|
|
141
|
+
throw new Error("--watch must use debate_id:commons_log_id with both ids non-empty");
|
|
142
|
+
}
|
|
143
|
+
return `${debateId}:${commonsLogId}`;
|
|
144
|
+
}
|
|
145
|
+
function validateConfigPath(value) {
|
|
146
|
+
if (value.length === 0 || value.trim().length === 0) {
|
|
147
|
+
throw new Error("Config path must be a non-empty file path");
|
|
148
|
+
}
|
|
149
|
+
if (value.includes("\0")) {
|
|
150
|
+
throw new Error("Config path must not contain NUL bytes");
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
if (!statSync(value).isFile()) {
|
|
154
|
+
throw new Error(`Config path is not a regular file: ${value}`);
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err instanceof Error && err.message.startsWith("Config path is not a regular file:")) {
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`Config path does not exist or cannot be read: ${value}`);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function resolvePythonCommand(command) {
|
|
165
|
+
const trimmed = command.trim();
|
|
166
|
+
if (!trimmed) {
|
|
167
|
+
throw new Error("Python command must be a non-empty executable name or absolute path");
|
|
168
|
+
}
|
|
169
|
+
if (trimmed.includes("\0") || /[\s;&|<>`$\n\r]/.test(trimmed)) {
|
|
170
|
+
throw new Error("Python command must be an executable only; arguments and shell metacharacters are rejected");
|
|
171
|
+
}
|
|
172
|
+
const isAbsolute3 = path.isAbsolute(trimmed) || path.win32.isAbsolute(trimmed);
|
|
173
|
+
const hasPathSeparator = trimmed.includes("/") || trimmed.includes("\\");
|
|
174
|
+
if (hasPathSeparator && !isAbsolute3) {
|
|
175
|
+
throw new Error("Python command path must be absolute");
|
|
176
|
+
}
|
|
177
|
+
if (isAbsolute3) {
|
|
178
|
+
assertExecutable(trimmed);
|
|
179
|
+
return trimmed;
|
|
180
|
+
}
|
|
181
|
+
if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
182
|
+
throw new Error("Python command must be a bare executable name or absolute path");
|
|
183
|
+
}
|
|
184
|
+
const resolved = findExecutable(trimmed);
|
|
185
|
+
if (resolved === null) {
|
|
186
|
+
throw new Error(`Python command not found on PATH: ${trimmed}`);
|
|
187
|
+
}
|
|
188
|
+
return resolved;
|
|
189
|
+
}
|
|
190
|
+
function findExecutable(command) {
|
|
191
|
+
const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
192
|
+
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
|
|
193
|
+
for (const dir of pathEntries) {
|
|
194
|
+
for (const ext of extensions) {
|
|
195
|
+
const candidate = path.join(dir, command.toLowerCase().endsWith(ext.toLowerCase()) ? command : `${command}${ext}`);
|
|
196
|
+
if (isExecutable(candidate)) {
|
|
197
|
+
return candidate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
53
202
|
}
|
|
203
|
+
function assertExecutable(file) {
|
|
204
|
+
if (!isExecutable(file)) {
|
|
205
|
+
throw new Error(`Python command is not an executable file: ${file}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function isExecutable(file) {
|
|
209
|
+
try {
|
|
210
|
+
if (!statSync(file).isFile()) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
accessSync(file, constants.X_OK);
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/commands/arena.ts
|
|
221
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
54
222
|
|
|
55
223
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
56
224
|
var external_exports = {};
|
|
@@ -530,8 +698,8 @@ function getErrorMap() {
|
|
|
530
698
|
|
|
531
699
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
|
|
532
700
|
var makeIssue = (params) => {
|
|
533
|
-
const { data, path, errorMaps, issueData } = params;
|
|
534
|
-
const fullPath = [...
|
|
701
|
+
const { data, path: path2, errorMaps, issueData } = params;
|
|
702
|
+
const fullPath = [...path2, ...issueData.path || []];
|
|
535
703
|
const fullIssue = {
|
|
536
704
|
...issueData,
|
|
537
705
|
path: fullPath
|
|
@@ -647,11 +815,11 @@ var errorUtil;
|
|
|
647
815
|
|
|
648
816
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
|
|
649
817
|
var ParseInputLazyPath = class {
|
|
650
|
-
constructor(parent, value,
|
|
818
|
+
constructor(parent, value, path2, key) {
|
|
651
819
|
this._cachedPath = [];
|
|
652
820
|
this.parent = parent;
|
|
653
821
|
this.data = value;
|
|
654
|
-
this._path =
|
|
822
|
+
this._path = path2;
|
|
655
823
|
this._key = key;
|
|
656
824
|
}
|
|
657
825
|
get path() {
|
|
@@ -4119,7 +4287,8 @@ var SessionSchema = external_exports.object({
|
|
|
4119
4287
|
max_credits: external_exports.number().int(),
|
|
4120
4288
|
agreed_quote: external_exports.record(external_exports.unknown()).nullable().optional(),
|
|
4121
4289
|
manifest_id: external_exports.string().nullable().optional(),
|
|
4122
|
-
arbitrator: external_exports.string().nullable().optional()
|
|
4290
|
+
arbitrator: external_exports.string().nullable().optional(),
|
|
4291
|
+
on_behalf_of_id: external_exports.string().nullable().optional()
|
|
4123
4292
|
}).passthrough();
|
|
4124
4293
|
var TrustScoreSchema = external_exports.object({
|
|
4125
4294
|
score: external_exports.number().int(),
|
|
@@ -4256,7 +4425,8 @@ var ReceiptSchema = external_exports.object({
|
|
|
4256
4425
|
requester_rating: external_exports.number().int().nullable().optional(),
|
|
4257
4426
|
rating_comment: external_exports.string().nullable().optional(),
|
|
4258
4427
|
verified: external_exports.boolean().optional(),
|
|
4259
|
-
finalized: external_exports.boolean().optional()
|
|
4428
|
+
finalized: external_exports.boolean().optional(),
|
|
4429
|
+
on_behalf_of_id: external_exports.string().nullable().optional()
|
|
4260
4430
|
}).passthrough();
|
|
4261
4431
|
var RateReceiptResultSchema = external_exports.object({
|
|
4262
4432
|
receipt_id: external_exports.string().optional(),
|
|
@@ -4402,17 +4572,50 @@ var CommonsLogEventSchema = external_exports.object({
|
|
|
4402
4572
|
}).passthrough();
|
|
4403
4573
|
|
|
4404
4574
|
// ../../sdk/consumer-ts/dist/client.js
|
|
4575
|
+
var GigTaskCreationError = class extends Error {
|
|
4576
|
+
mandate;
|
|
4577
|
+
cause;
|
|
4578
|
+
constructor(mandate, cause) {
|
|
4579
|
+
super(`Gig task creation failed after mandate ${mandate.mandate_id} was issued: ${cause.message}`);
|
|
4580
|
+
this.mandate = mandate;
|
|
4581
|
+
this.cause = cause;
|
|
4582
|
+
this.name = "GigTaskCreationError";
|
|
4583
|
+
}
|
|
4584
|
+
};
|
|
4405
4585
|
var ConsumerClient = class {
|
|
4406
|
-
|
|
4586
|
+
// Substrate / network surface — identity, sessions, mandates, registration.
|
|
4587
|
+
networkUrl;
|
|
4588
|
+
// First-party service product surface — where Gig PA et al expose their
|
|
4589
|
+
// resource APIs. Distinct from networkUrl per LAYER_SYSTEM §8: services are
|
|
4590
|
+
// L5 service agents, not the network.
|
|
4591
|
+
serviceUrl;
|
|
4407
4592
|
apiKey;
|
|
4408
4593
|
fetchImpl;
|
|
4409
|
-
|
|
4410
|
-
|
|
4594
|
+
_gigPaAgentIdCache;
|
|
4595
|
+
constructor(networkUrl, apiKey, options) {
|
|
4596
|
+
this.networkUrl = networkUrl.replace(/\/+$/, "");
|
|
4597
|
+
this.serviceUrl = options?.serviceUrl !== void 0 ? options.serviceUrl.replace(/\/+$/, "") : void 0;
|
|
4411
4598
|
this.apiKey = apiKey;
|
|
4412
4599
|
this.fetchImpl = options?.fetch ?? fetch;
|
|
4413
4600
|
}
|
|
4414
|
-
async
|
|
4415
|
-
|
|
4601
|
+
async _resolveGigPaAgentId() {
|
|
4602
|
+
if (!this._gigPaAgentIdCache) {
|
|
4603
|
+
const data = await this.request("/api/v1/resolve/gig-pa-operator/gig-platform-agent", { method: "GET" }, AgentSchema);
|
|
4604
|
+
this._gigPaAgentIdCache = data.agent_id;
|
|
4605
|
+
}
|
|
4606
|
+
return this._gigPaAgentIdCache;
|
|
4607
|
+
}
|
|
4608
|
+
async request(path2, init, schema) {
|
|
4609
|
+
return this._call(this.networkUrl, path2, init, schema);
|
|
4610
|
+
}
|
|
4611
|
+
async serviceRequest(path2, init, schema) {
|
|
4612
|
+
if (this.serviceUrl === void 0) {
|
|
4613
|
+
throw new Error("ConsumerClient.serviceUrl is not set. Methods that talk to first-party service surfaces (e.g. Gig PA's /api/v1/gig-tasks/*) require a serviceUrl because services are L5 service agents, not part of the network. Pass { serviceUrl: <gig_pa_url> } in options.");
|
|
4614
|
+
}
|
|
4615
|
+
return this._call(this.serviceUrl, path2, init, schema);
|
|
4616
|
+
}
|
|
4617
|
+
async _call(baseUrl, path2, init, schema) {
|
|
4618
|
+
const res = await this.fetchImpl(`${baseUrl}${path2}`, {
|
|
4416
4619
|
...init,
|
|
4417
4620
|
headers: {
|
|
4418
4621
|
"Content-Type": "application/json",
|
|
@@ -4441,7 +4644,7 @@ var ConsumerClient = class {
|
|
|
4441
4644
|
}
|
|
4442
4645
|
// ───────── Identity / Auth ─────────
|
|
4443
4646
|
/** @implements POST /api/v1/register */
|
|
4444
|
-
static async register(
|
|
4647
|
+
static async register(networkUrl, params = {}, options) {
|
|
4445
4648
|
const fetchImpl = options?.fetch ?? fetch;
|
|
4446
4649
|
const body = { role: params.role ?? "requester" };
|
|
4447
4650
|
if (params.email !== void 0)
|
|
@@ -4450,7 +4653,7 @@ var ConsumerClient = class {
|
|
|
4450
4653
|
body.handle = params.handle;
|
|
4451
4654
|
if (params.displayName !== void 0)
|
|
4452
4655
|
body.display_name = params.displayName;
|
|
4453
|
-
const res = await fetchImpl(`${
|
|
4656
|
+
const res = await fetchImpl(`${networkUrl.replace(/\/+$/, "")}/api/v1/register`, {
|
|
4454
4657
|
method: "POST",
|
|
4455
4658
|
headers: {
|
|
4456
4659
|
"Content-Type": "application/json"
|
|
@@ -4514,6 +4717,10 @@ var ConsumerClient = class {
|
|
|
4514
4717
|
})
|
|
4515
4718
|
}, InvokeResultSchema);
|
|
4516
4719
|
}
|
|
4720
|
+
/** @implements POST /api/v1/agents/{agent_id}/messages */
|
|
4721
|
+
async deliverAgentMessage(agentId, payload) {
|
|
4722
|
+
return this.request(`/api/v1/agents/${encodeURIComponent(agentId)}/messages`, { method: "POST", body: JSON.stringify({ payload }) }, external_exports.record(external_exports.unknown()));
|
|
4723
|
+
}
|
|
4517
4724
|
async getTrustScore(agentId, options) {
|
|
4518
4725
|
return this.request(`/api/v1/agents/${agentId}/trust${this.queryString({
|
|
4519
4726
|
capability: options?.capability
|
|
@@ -4547,6 +4754,16 @@ var ConsumerClient = class {
|
|
|
4547
4754
|
async getSession(sessionId) {
|
|
4548
4755
|
return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "GET" }, SessionSchema);
|
|
4549
4756
|
}
|
|
4757
|
+
/** @implements POST /api/v1/sessions/{session_id}/activate
|
|
4758
|
+
*
|
|
4759
|
+
* Required by the requester after WS SESSION_ACCEPT to flip the DB-side
|
|
4760
|
+
* session record from `pending` to `active`. Without this call, subsequent
|
|
4761
|
+
* `POST /messages` will reject with 409 once cloud notices the WS dropped
|
|
4762
|
+
* (status flips `pending` → `interrupted` on heartbeat watchdog).
|
|
4763
|
+
*/
|
|
4764
|
+
async activateSession(sessionId) {
|
|
4765
|
+
return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/activate`, { method: "POST", body: JSON.stringify({}) }, external_exports.record(external_exports.unknown()));
|
|
4766
|
+
}
|
|
4550
4767
|
/** @implements POST /api/v1/sessions/{session_id}/end */
|
|
4551
4768
|
async endSession(sessionId, options) {
|
|
4552
4769
|
const body = {};
|
|
@@ -4568,40 +4785,88 @@ var ConsumerClient = class {
|
|
|
4568
4785
|
})}`, { method: "GET" }, SessionEventListSchema);
|
|
4569
4786
|
}
|
|
4570
4787
|
// ───────── Gig PA (requester) ─────────
|
|
4571
|
-
/** @implements GET /api/v1/
|
|
4788
|
+
/** @implements GET /api/v1/gig-tasks/ */
|
|
4572
4789
|
async listGigTasks(options) {
|
|
4573
|
-
return this.
|
|
4790
|
+
return this.serviceRequest(`/api/v1/gig-tasks/${this.queryString({
|
|
4574
4791
|
status: options?.status,
|
|
4575
4792
|
capability: options?.capability
|
|
4576
4793
|
})}`, { method: "GET" }, external_exports.array(GigTaskSchema));
|
|
4577
4794
|
}
|
|
4578
|
-
/** @implements POST /api/v1/
|
|
4795
|
+
/** @implements POST /api/v1/gig-tasks/ */
|
|
4579
4796
|
async createGigTask(body) {
|
|
4580
|
-
return this.
|
|
4797
|
+
return this.serviceRequest("/api/v1/gig-tasks/", { method: "POST", body: JSON.stringify(body) }, GigTaskSchema);
|
|
4798
|
+
}
|
|
4799
|
+
async createGigTaskWithMandate(options) {
|
|
4800
|
+
const { principal_agent_id, capability, instruction, target_providers, credits_per_provider, mandate_expires_at, gig_pa_agent_id, deadline, idempotency_key, mandate_id: preIssuedMandateId, ...taskExtra } = options;
|
|
4801
|
+
let issuedMandate;
|
|
4802
|
+
let resolvedMandateId = preIssuedMandateId;
|
|
4803
|
+
if (!resolvedMandateId) {
|
|
4804
|
+
const gigPaAgentId = gig_pa_agent_id ?? await this._resolveGigPaAgentId();
|
|
4805
|
+
const mandateHeaders = {};
|
|
4806
|
+
if (idempotency_key)
|
|
4807
|
+
mandateHeaders["Idempotency-Key"] = `${idempotency_key}:mandate`;
|
|
4808
|
+
issuedMandate = await this.request("/api/v1/mandates", {
|
|
4809
|
+
method: "POST",
|
|
4810
|
+
body: JSON.stringify({
|
|
4811
|
+
principal_agent_id,
|
|
4812
|
+
delegate_agent_id: gigPaAgentId,
|
|
4813
|
+
scope: ["session.open"],
|
|
4814
|
+
scope_params: { "session.open": { max_credits_per_op: credits_per_provider } },
|
|
4815
|
+
budget: target_providers * credits_per_provider,
|
|
4816
|
+
expires_at: mandate_expires_at
|
|
4817
|
+
}),
|
|
4818
|
+
headers: mandateHeaders
|
|
4819
|
+
}, MandateRecordSchema);
|
|
4820
|
+
resolvedMandateId = issuedMandate.mandate_id;
|
|
4821
|
+
}
|
|
4822
|
+
const taskHeaders = {};
|
|
4823
|
+
if (idempotency_key)
|
|
4824
|
+
taskHeaders["Idempotency-Key"] = idempotency_key;
|
|
4825
|
+
const taskBody = {
|
|
4826
|
+
capability,
|
|
4827
|
+
instruction,
|
|
4828
|
+
target_providers,
|
|
4829
|
+
credits_per_provider,
|
|
4830
|
+
mandate_id: resolvedMandateId,
|
|
4831
|
+
...taskExtra
|
|
4832
|
+
};
|
|
4833
|
+
if (deadline)
|
|
4834
|
+
taskBody.deadline = deadline;
|
|
4835
|
+
let task;
|
|
4836
|
+
try {
|
|
4837
|
+
task = await this.serviceRequest("/api/v1/gig-tasks/", { method: "POST", body: JSON.stringify(taskBody), headers: taskHeaders }, GigTaskSchema);
|
|
4838
|
+
} catch (err) {
|
|
4839
|
+
if (issuedMandate && err instanceof Error) {
|
|
4840
|
+
throw new GigTaskCreationError(issuedMandate, err);
|
|
4841
|
+
}
|
|
4842
|
+
throw err;
|
|
4843
|
+
}
|
|
4844
|
+
const mandate = issuedMandate ?? { mandate_id: resolvedMandateId };
|
|
4845
|
+
return { task, mandate };
|
|
4581
4846
|
}
|
|
4582
|
-
/** @implements GET /api/v1/
|
|
4847
|
+
/** @implements GET /api/v1/gig-tasks/policy */
|
|
4583
4848
|
async getGigTaskPolicy() {
|
|
4584
|
-
return this.
|
|
4849
|
+
return this.serviceRequest("/api/v1/gig-tasks/policy", { method: "GET" }, GigTaskPolicySchema);
|
|
4585
4850
|
}
|
|
4586
|
-
/** @implements GET /api/v1/
|
|
4851
|
+
/** @implements GET /api/v1/gig-tasks/{task_id} */
|
|
4587
4852
|
async getGigTask(taskId) {
|
|
4588
|
-
return this.
|
|
4853
|
+
return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}`, { method: "GET" }, GigTaskSchema);
|
|
4589
4854
|
}
|
|
4590
|
-
/** @implements POST /api/v1/
|
|
4855
|
+
/** @implements POST /api/v1/gig-tasks/{task_id}/results/{result_id}/verify */
|
|
4591
4856
|
async verifyGigTaskResult(taskId, resultId, body) {
|
|
4592
|
-
return this.
|
|
4857
|
+
return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/verify`, { method: "POST", body: JSON.stringify(body ?? {}) }, GigTaskVerifyResultSchema);
|
|
4593
4858
|
}
|
|
4594
|
-
/** @implements POST /api/v1/
|
|
4859
|
+
/** @implements POST /api/v1/gig-tasks/{task_id}/results/{result_id}/review */
|
|
4595
4860
|
async reviewGigTaskResult(taskId, resultId, body) {
|
|
4596
|
-
return this.
|
|
4861
|
+
return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/results/${encodeURIComponent(resultId)}/review`, { method: "POST", body: JSON.stringify(body) }, GigTaskVerifyResultSchema);
|
|
4597
4862
|
}
|
|
4598
|
-
/** @implements POST /api/v1/
|
|
4863
|
+
/** @implements POST /api/v1/gig-tasks/{task_id}/release */
|
|
4599
4864
|
async releaseGigTask(taskId) {
|
|
4600
|
-
return this.
|
|
4865
|
+
return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/release`, { method: "POST", body: JSON.stringify({}) }, GigTaskActionResultSchema);
|
|
4601
4866
|
}
|
|
4602
|
-
/** @implements POST /api/v1/
|
|
4867
|
+
/** @implements POST /api/v1/gig-tasks/{task_id}/cancel */
|
|
4603
4868
|
async cancelGigTask(taskId, reason) {
|
|
4604
|
-
return this.
|
|
4869
|
+
return this.serviceRequest(`/api/v1/gig-tasks/${encodeURIComponent(taskId)}/cancel`, { method: "POST", body: JSON.stringify(reason !== void 0 ? { reason } : {}) }, GigTaskActionResultSchema);
|
|
4605
4870
|
}
|
|
4606
4871
|
// ───────── Credits ─────────
|
|
4607
4872
|
/** @implements GET /api/v1/credits */
|
|
@@ -4715,10 +4980,6 @@ var ConsumerClient = class {
|
|
|
4715
4980
|
async revokeMandate(mandateId) {
|
|
4716
4981
|
await this.request(`/api/v1/mandates/${encodeURIComponent(mandateId)}`, { method: "DELETE" }, external_exports.unknown());
|
|
4717
4982
|
}
|
|
4718
|
-
/** @implements POST /api/v1/mandates/transport */
|
|
4719
|
-
async createTransportMandate(body) {
|
|
4720
|
-
return this.request("/api/v1/mandates/transport", { method: "POST", body: JSON.stringify(body) }, MandateRecordSchema);
|
|
4721
|
-
}
|
|
4722
4983
|
// ───────── Observability ─────────
|
|
4723
4984
|
/** @implements GET /api/v1/events/verify */
|
|
4724
4985
|
async verifyEvents(options) {
|
|
@@ -4737,29 +4998,71 @@ var ConsumerClient = class {
|
|
|
4737
4998
|
// ───────── Health (no auth required) ─────────
|
|
4738
4999
|
/** @implements GET /health */
|
|
4739
5000
|
async health() {
|
|
4740
|
-
const res = await this.fetchImpl(`${this.
|
|
5001
|
+
const res = await this.fetchImpl(`${this.networkUrl}/health`, { method: "GET" });
|
|
4741
5002
|
if (!res.ok)
|
|
4742
5003
|
throw new Error(`LinkedClaw /health ${res.status}`);
|
|
4743
5004
|
return HealthStatusSchema.parse(await res.json());
|
|
4744
5005
|
}
|
|
4745
5006
|
/** @implements GET /health/ready */
|
|
4746
5007
|
async healthReady() {
|
|
4747
|
-
const res = await this.fetchImpl(`${this.
|
|
5008
|
+
const res = await this.fetchImpl(`${this.networkUrl}/health/ready`, { method: "GET" });
|
|
4748
5009
|
if (!res.ok)
|
|
4749
5010
|
throw new Error(`LinkedClaw /health/ready ${res.status}`);
|
|
4750
5011
|
return HealthStatusSchema.parse(await res.json());
|
|
4751
5012
|
}
|
|
4752
5013
|
/** @implements GET /metrics */
|
|
4753
5014
|
async metrics() {
|
|
4754
|
-
const res = await this.fetchImpl(`${this.
|
|
5015
|
+
const res = await this.fetchImpl(`${this.networkUrl}/metrics`, { method: "GET" });
|
|
4755
5016
|
if (!res.ok)
|
|
4756
5017
|
throw new Error(`LinkedClaw /metrics ${res.status}`);
|
|
4757
5018
|
return res.text();
|
|
4758
5019
|
}
|
|
4759
5020
|
};
|
|
4760
5021
|
|
|
5022
|
+
// ../../sdk/consumer-ts/dist/capabilitySchema.js
|
|
5023
|
+
var CapabilitySchemaError = class extends Error {
|
|
5024
|
+
constructor(message) {
|
|
5025
|
+
super(message);
|
|
5026
|
+
this.name = "CapabilitySchemaError";
|
|
5027
|
+
}
|
|
5028
|
+
};
|
|
5029
|
+
async function fetchCapabilitySchema(listing, capability, options) {
|
|
5030
|
+
const meta = listing.capabilities_meta?.[capability];
|
|
5031
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
5032
|
+
throw new CapabilitySchemaError(`no capabilities_meta entry for ${JSON.stringify(capability)}`);
|
|
5033
|
+
}
|
|
5034
|
+
const entry = meta;
|
|
5035
|
+
const schemaUrl = entry["schema_url"];
|
|
5036
|
+
if (!schemaUrl || typeof schemaUrl !== "string") {
|
|
5037
|
+
throw new CapabilitySchemaError(`no schema_url for capability ${JSON.stringify(capability)}`);
|
|
5038
|
+
}
|
|
5039
|
+
const schemaDigest = entry["schema_digest"];
|
|
5040
|
+
const fetchFn = options?.fetch ?? globalThis.fetch;
|
|
5041
|
+
const response = await fetchFn(schemaUrl);
|
|
5042
|
+
if (!response.ok) {
|
|
5043
|
+
throw new CapabilitySchemaError(`failed to fetch schema for ${JSON.stringify(capability)}: HTTP ${response.status}`);
|
|
5044
|
+
}
|
|
5045
|
+
const buffer = await response.arrayBuffer();
|
|
5046
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
|
|
5047
|
+
const actual = "sha256:" + Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
5048
|
+
if (actual !== schemaDigest) {
|
|
5049
|
+
throw new CapabilitySchemaError(`schema digest mismatch for ${JSON.stringify(capability)}: expected ${String(schemaDigest)}, got ${actual}`);
|
|
5050
|
+
}
|
|
5051
|
+
const text = new TextDecoder().decode(buffer);
|
|
5052
|
+
let parsed;
|
|
5053
|
+
try {
|
|
5054
|
+
parsed = JSON.parse(text);
|
|
5055
|
+
} catch {
|
|
5056
|
+
throw new CapabilitySchemaError(`schema body is not valid JSON for ${JSON.stringify(capability)}`);
|
|
5057
|
+
}
|
|
5058
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5059
|
+
throw new CapabilitySchemaError(`schema body is not a JSON object for ${JSON.stringify(capability)}`);
|
|
5060
|
+
}
|
|
5061
|
+
return parsed;
|
|
5062
|
+
}
|
|
5063
|
+
|
|
4761
5064
|
// ../../sdk/consumer-ts/dist/index.js
|
|
4762
|
-
var
|
|
5065
|
+
var DEFAULT_RELAY_URL = "wss://api.linkedclaw.com/ws";
|
|
4763
5066
|
|
|
4764
5067
|
// ../../sdk/provider-ts/dist/models.js
|
|
4765
5068
|
var SessionSchema2 = external_exports.object({
|
|
@@ -5014,8 +5317,8 @@ var ProviderClient = class {
|
|
|
5014
5317
|
this.apiKey = apiKey;
|
|
5015
5318
|
this.fetchImpl = options?.fetch ?? fetch;
|
|
5016
5319
|
}
|
|
5017
|
-
async request(
|
|
5018
|
-
const res = await this.fetchImpl(`${this.baseUrl}${
|
|
5320
|
+
async request(path2, init, schema) {
|
|
5321
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path2}`, {
|
|
5019
5322
|
...init,
|
|
5020
5323
|
headers: {
|
|
5021
5324
|
"Content-Type": "application/json",
|
|
@@ -5417,12 +5720,18 @@ function strOrEmpty(frame, key) {
|
|
|
5417
5720
|
|
|
5418
5721
|
// ../../sdk/consumer-runtime-ts/dist/index.js
|
|
5419
5722
|
import WebSocket2 from "ws";
|
|
5420
|
-
var
|
|
5723
|
+
var ACP_CONNECT_TIMEOUT_MS = 2e3;
|
|
5724
|
+
var SESSION_ACCEPT_TIMEOUT_MS = 3e4;
|
|
5421
5725
|
var SessionRejectedError = class extends Error {
|
|
5422
5726
|
constructor(reason) {
|
|
5423
5727
|
super(`session rejected: ${reason}`);
|
|
5424
5728
|
}
|
|
5425
5729
|
};
|
|
5730
|
+
var TransportMissError = class extends Error {
|
|
5731
|
+
constructor(reason) {
|
|
5732
|
+
super(`transport miss: ${reason}`);
|
|
5733
|
+
}
|
|
5734
|
+
};
|
|
5426
5735
|
var RequesterFlows = class {
|
|
5427
5736
|
constructor(client) {
|
|
5428
5737
|
this.client = client;
|
|
@@ -5432,8 +5741,11 @@ var RequesterFlows = class {
|
|
|
5432
5741
|
return this.client.discover({ capability, ...extra });
|
|
5433
5742
|
}
|
|
5434
5743
|
/**
|
|
5435
|
-
* Open a session.
|
|
5436
|
-
*
|
|
5744
|
+
* Open a session. HTTP create → WS handshake (SESSION_CREATE/ACCEPT) → done.
|
|
5745
|
+
*
|
|
5746
|
+
* Default transport is /ws — native providers register there
|
|
5747
|
+
* (`SkillConfig.try_acp=False` upstream default). Set `tryAcp: true` to try
|
|
5748
|
+
* /acp first; on opt-in, falls back to /ws when the recipient isn't on /acp.
|
|
5437
5749
|
*/
|
|
5438
5750
|
async hire(params) {
|
|
5439
5751
|
const session = await this.client.createSession({
|
|
@@ -5443,43 +5755,81 @@ var RequesterFlows = class {
|
|
|
5443
5755
|
...params.referredBy !== void 0 ? { referred_by: params.referredBy } : {}
|
|
5444
5756
|
});
|
|
5445
5757
|
if (params.autoActivate === false) return { session, activated: false };
|
|
5446
|
-
const relayUrl = params.relayUrl ??
|
|
5447
|
-
|
|
5448
|
-
|
|
5758
|
+
const relayUrl = params.relayUrl ?? DEFAULT_RELAY_URL;
|
|
5759
|
+
try {
|
|
5760
|
+
if (params.tryAcp) {
|
|
5761
|
+
const acpUrl = relayUrl.replace(/\/ws$/, "/acp");
|
|
5762
|
+
try {
|
|
5763
|
+
await this.attemptHandshake(acpUrl, session.session_id, params, ACP_CONNECT_TIMEOUT_MS);
|
|
5764
|
+
} catch (err) {
|
|
5765
|
+
if (!(err instanceof TransportMissError)) throw err;
|
|
5766
|
+
await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
|
|
5767
|
+
}
|
|
5768
|
+
} else {
|
|
5769
|
+
await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
|
|
5770
|
+
}
|
|
5771
|
+
await this.client.activateSession(session.session_id);
|
|
5772
|
+
} catch (err) {
|
|
5773
|
+
await this.client.endSession(session.session_id, {}).catch(() => {
|
|
5774
|
+
});
|
|
5775
|
+
if (err instanceof TransportMissError) {
|
|
5776
|
+
throw new SessionRejectedError(`agent unreachable`);
|
|
5777
|
+
}
|
|
5778
|
+
throw err;
|
|
5779
|
+
}
|
|
5780
|
+
return { session, activated: true };
|
|
5781
|
+
}
|
|
5782
|
+
async attemptHandshake(url, sessionId, params, connectTimeoutMs) {
|
|
5783
|
+
const ws = new WebSocket2(url);
|
|
5449
5784
|
try {
|
|
5450
|
-
await new Promise((
|
|
5451
|
-
|
|
5452
|
-
|
|
5785
|
+
await new Promise((resolve3, reject) => {
|
|
5786
|
+
const timer = setTimeout(
|
|
5787
|
+
() => reject(new TransportMissError("connect timeout")),
|
|
5788
|
+
connectTimeoutMs
|
|
5789
|
+
);
|
|
5790
|
+
ws.once("open", () => {
|
|
5791
|
+
clearTimeout(timer);
|
|
5792
|
+
resolve3();
|
|
5793
|
+
});
|
|
5794
|
+
ws.once("error", (err) => {
|
|
5795
|
+
clearTimeout(timer);
|
|
5796
|
+
reject(new TransportMissError(`connect failed: ${err.message}`));
|
|
5797
|
+
});
|
|
5453
5798
|
});
|
|
5454
5799
|
ws.send(JSON.stringify({ type: MessageType.IDENTIFY, agent_id: params.agentId, token: params.apiKey }));
|
|
5455
5800
|
ws.send(JSON.stringify({
|
|
5456
5801
|
type: MessageType.SESSION_CREATE,
|
|
5457
|
-
session_id:
|
|
5802
|
+
session_id: sessionId,
|
|
5458
5803
|
recipient: params.providerAgentId,
|
|
5459
5804
|
capability: params.capability
|
|
5460
5805
|
}));
|
|
5461
|
-
const reply = await new Promise((
|
|
5462
|
-
const timer = setTimeout(
|
|
5806
|
+
const reply = await new Promise((resolve3, reject) => {
|
|
5807
|
+
const timer = setTimeout(
|
|
5808
|
+
() => reject(new Error("SESSION_ACCEPT timeout")),
|
|
5809
|
+
SESSION_ACCEPT_TIMEOUT_MS
|
|
5810
|
+
);
|
|
5463
5811
|
ws.once("message", (data) => {
|
|
5464
5812
|
clearTimeout(timer);
|
|
5465
|
-
|
|
5813
|
+
resolve3(JSON.parse(data.toString()));
|
|
5466
5814
|
});
|
|
5467
5815
|
ws.once("error", (err) => {
|
|
5468
5816
|
clearTimeout(timer);
|
|
5469
5817
|
reject(err);
|
|
5470
5818
|
});
|
|
5471
5819
|
});
|
|
5472
|
-
if (reply.type === MessageType.ERROR)
|
|
5820
|
+
if (reply.type === MessageType.ERROR) {
|
|
5821
|
+
const errMsg = reply.error ?? "relay error";
|
|
5822
|
+
if (errMsg.includes("not connected")) throw new TransportMissError(errMsg);
|
|
5823
|
+
throw new SessionRejectedError(errMsg);
|
|
5824
|
+
}
|
|
5473
5825
|
if (reply.type === MessageType.SESSION_REJECT) throw new SessionRejectedError(reply.reason ?? "rejected");
|
|
5474
|
-
if (reply.type !== MessageType.SESSION_ACCEPT) throw new Error(`unexpected
|
|
5475
|
-
}
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
}
|
|
5479
|
-
|
|
5826
|
+
if (reply.type !== MessageType.SESSION_ACCEPT) throw new Error(`unexpected reply: ${reply.type}`);
|
|
5827
|
+
} finally {
|
|
5828
|
+
try {
|
|
5829
|
+
ws.close();
|
|
5830
|
+
} catch {
|
|
5831
|
+
}
|
|
5480
5832
|
}
|
|
5481
|
-
ws.close();
|
|
5482
|
-
return { session, activated: true };
|
|
5483
5833
|
}
|
|
5484
5834
|
send(sessionId, payload, seq) {
|
|
5485
5835
|
const normalized = typeof payload === "string" ? { text: payload } : payload;
|
|
@@ -5490,6 +5840,52 @@ var RequesterFlows = class {
|
|
|
5490
5840
|
}
|
|
5491
5841
|
};
|
|
5492
5842
|
|
|
5843
|
+
// src/config.ts
|
|
5844
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
5845
|
+
import { homedir } from "os";
|
|
5846
|
+
import { join, dirname } from "path";
|
|
5847
|
+
import { load as yamlLoad, dump as yamlDump } from "js-yaml";
|
|
5848
|
+
var DEFAULT_CLOUD_URL = "https://api.linkedclaw.com";
|
|
5849
|
+
var DEFAULT_RELAY_URL2 = "wss://api.linkedclaw.com/ws";
|
|
5850
|
+
function configDir() {
|
|
5851
|
+
return process.env["LINKEDCLAW_CONFIG_DIR"] ?? join(homedir(), ".linkedclaw");
|
|
5852
|
+
}
|
|
5853
|
+
function configPath() {
|
|
5854
|
+
return join(configDir(), "config.yaml");
|
|
5855
|
+
}
|
|
5856
|
+
function readFileConfig(path2 = configPath()) {
|
|
5857
|
+
if (!existsSync(path2)) return {};
|
|
5858
|
+
const raw = readFileSync(path2, "utf8");
|
|
5859
|
+
const parsed = yamlLoad(raw);
|
|
5860
|
+
if (parsed === null || parsed === void 0) return {};
|
|
5861
|
+
if (typeof parsed !== "object") {
|
|
5862
|
+
throw new Error(`config file ${path2} is not a YAML object`);
|
|
5863
|
+
}
|
|
5864
|
+
return parsed;
|
|
5865
|
+
}
|
|
5866
|
+
function writeFileConfig(cfg, path2 = configPath()) {
|
|
5867
|
+
const dir = dirname(path2);
|
|
5868
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
|
|
5869
|
+
writeFileSync(path2, yamlDump(cfg), { mode: 384 });
|
|
5870
|
+
if (process.platform !== "win32") chmodSync(path2, 384);
|
|
5871
|
+
}
|
|
5872
|
+
function resolveConfig(overrides = {}) {
|
|
5873
|
+
const env = process.env;
|
|
5874
|
+
const file = readFileConfig();
|
|
5875
|
+
const cloudUrl = overrides.cloudUrl ?? env["LINKEDCLAW_CLOUD_URL"] ?? file.cloudUrl ?? DEFAULT_CLOUD_URL;
|
|
5876
|
+
const relayUrl = overrides.relayUrl ?? env["LINKEDCLAW_RELAY_URL"] ?? file.relayUrl ?? DEFAULT_RELAY_URL2;
|
|
5877
|
+
const servicesHostUrl = overrides.servicesHostUrl ?? env["LINKEDCLAW_SERVICES_HOST_URL"] ?? file.servicesHostUrl ?? cloudUrl;
|
|
5878
|
+
const apiKey = overrides.apiKey ?? env["LINKEDCLAW_API_KEY"] ?? file.apiKey;
|
|
5879
|
+
return {
|
|
5880
|
+
...file,
|
|
5881
|
+
...overrides,
|
|
5882
|
+
cloudUrl,
|
|
5883
|
+
relayUrl,
|
|
5884
|
+
servicesHostUrl,
|
|
5885
|
+
...apiKey !== void 0 ? { apiKey } : {}
|
|
5886
|
+
};
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5493
5889
|
// src/errors.ts
|
|
5494
5890
|
var LinkedClawError = class extends Error {
|
|
5495
5891
|
code;
|
|
@@ -5507,11 +5903,11 @@ var NetworkError = class extends LinkedClawError {
|
|
|
5507
5903
|
}
|
|
5508
5904
|
};
|
|
5509
5905
|
var ApiError = class extends LinkedClawError {
|
|
5510
|
-
constructor(status, detail,
|
|
5511
|
-
super(`api_error_${status}`, `[${status}] ${
|
|
5906
|
+
constructor(status, detail, path2) {
|
|
5907
|
+
super(`api_error_${status}`, `[${status}] ${path2}: ${detail}`);
|
|
5512
5908
|
this.status = status;
|
|
5513
5909
|
this.detail = detail;
|
|
5514
|
-
this.path =
|
|
5910
|
+
this.path = path2;
|
|
5515
5911
|
this.name = "ApiError";
|
|
5516
5912
|
}
|
|
5517
5913
|
status;
|
|
@@ -5581,48 +5977,419 @@ async function readStdin() {
|
|
|
5581
5977
|
return Buffer.concat(chunks).toString("utf8");
|
|
5582
5978
|
}
|
|
5583
5979
|
|
|
5584
|
-
// src/
|
|
5585
|
-
function
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5980
|
+
// src/arena/api.ts
|
|
5981
|
+
function errorDetail(body) {
|
|
5982
|
+
if (body && typeof body === "object" && "detail" in body) {
|
|
5983
|
+
const detail = body.detail;
|
|
5984
|
+
return typeof detail === "string" ? detail : JSON.stringify(detail) ?? String(detail);
|
|
5985
|
+
}
|
|
5986
|
+
if (body == null) return "";
|
|
5987
|
+
return typeof body === "string" ? body : JSON.stringify(body);
|
|
5988
|
+
}
|
|
5989
|
+
function makeArenaApi(targetUrl, apiKey) {
|
|
5990
|
+
async function apiFetch(path2, opts = {}) {
|
|
5991
|
+
const url = targetUrl.replace(/\/$/, "") + path2;
|
|
5992
|
+
const res = await fetch(url, {
|
|
5993
|
+
...opts,
|
|
5994
|
+
headers: {
|
|
5995
|
+
"Content-Type": "application/json",
|
|
5996
|
+
Authorization: `Bearer ${apiKey}`,
|
|
5997
|
+
"X-CSRF-Token": apiKey,
|
|
5998
|
+
...opts.headers ?? {}
|
|
5591
5999
|
}
|
|
5592
|
-
if (!apiKey) throw new Error("empty api key");
|
|
5593
|
-
const prev = readFileConfig();
|
|
5594
|
-
const next = { ...prev, apiKey, ...opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {} };
|
|
5595
|
-
writeFileConfig(next);
|
|
5596
|
-
return { ok: true, path: configPath() };
|
|
5597
6000
|
});
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
} catch {
|
|
5611
|
-
}
|
|
5612
|
-
}
|
|
5613
|
-
if (!opened) {
|
|
5614
|
-
process.stderr.write(`Open this URL in a browser to register:
|
|
5615
|
-
${portalUrl}
|
|
5616
|
-
|
|
5617
|
-
`);
|
|
6001
|
+
let body;
|
|
6002
|
+
try {
|
|
6003
|
+
body = await res.json();
|
|
6004
|
+
} catch {
|
|
6005
|
+
body = null;
|
|
6006
|
+
}
|
|
6007
|
+
if (!res.ok) {
|
|
6008
|
+
try {
|
|
6009
|
+
throw new ApiError(res.status, errorDetail(body), path2);
|
|
6010
|
+
} catch (err) {
|
|
6011
|
+
if (err instanceof ApiError) throw err;
|
|
6012
|
+
throw new LinkedClawError(`api_${res.status}`, `HTTP ${res.status}`);
|
|
5618
6013
|
}
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
6014
|
+
}
|
|
6015
|
+
return body;
|
|
6016
|
+
}
|
|
6017
|
+
return {
|
|
6018
|
+
async createTournamentArena(body, opts) {
|
|
6019
|
+
return apiFetch("/api/v1/arena/arenas", {
|
|
6020
|
+
method: "POST",
|
|
6021
|
+
headers: { "Idempotency-Key": opts.idempotencyKey },
|
|
6022
|
+
body: JSON.stringify(body)
|
|
6023
|
+
});
|
|
6024
|
+
},
|
|
6025
|
+
async register(body) {
|
|
6026
|
+
return apiFetch("/api/v1/arena/contestants/register", {
|
|
6027
|
+
method: "POST",
|
|
6028
|
+
body: JSON.stringify(body)
|
|
6029
|
+
});
|
|
6030
|
+
},
|
|
6031
|
+
async listOffers() {
|
|
6032
|
+
return apiFetch("/api/v1/arena/offers", { method: "GET" });
|
|
6033
|
+
},
|
|
6034
|
+
async acceptOffer(offerId) {
|
|
6035
|
+
return apiFetch(`/api/v1/arena/offers/${encodeURIComponent(offerId)}/accept`, {
|
|
6036
|
+
method: "POST",
|
|
6037
|
+
body: JSON.stringify({})
|
|
6038
|
+
});
|
|
6039
|
+
},
|
|
6040
|
+
async submit(arenaId, body) {
|
|
6041
|
+
const { submission_hash: _submissionHash, ...wireBody } = body;
|
|
6042
|
+
return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/submissions`, {
|
|
6043
|
+
method: "POST",
|
|
6044
|
+
body: JSON.stringify(wireBody)
|
|
6045
|
+
});
|
|
6046
|
+
},
|
|
6047
|
+
async commitJuror(arenaId, body) {
|
|
6048
|
+
return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/jurors/commit`, {
|
|
6049
|
+
method: "POST",
|
|
6050
|
+
body: JSON.stringify(body)
|
|
6051
|
+
});
|
|
6052
|
+
},
|
|
6053
|
+
async voteTask(arenaId, body) {
|
|
6054
|
+
return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/juror-votes`, {
|
|
6055
|
+
method: "POST",
|
|
6056
|
+
body: JSON.stringify(body)
|
|
6057
|
+
});
|
|
6058
|
+
},
|
|
6059
|
+
async voteMatch(arenaId, matchId, body) {
|
|
6060
|
+
return apiFetch(
|
|
6061
|
+
`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/matches/${encodeURIComponent(matchId)}/juror-votes`,
|
|
6062
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
6063
|
+
);
|
|
6064
|
+
},
|
|
6065
|
+
async listArenas(opts = {}) {
|
|
6066
|
+
const suffix = opts.registered ? "?registered=true" : "";
|
|
6067
|
+
return apiFetch(`/api/v1/arena/arenas${suffix}`, { method: "GET" });
|
|
6068
|
+
},
|
|
6069
|
+
async getLeaderboard(arenaId) {
|
|
6070
|
+
return apiFetch(`/api/v1/arena/arenas/${encodeURIComponent(arenaId)}/leaderboard`, {
|
|
6071
|
+
method: "GET"
|
|
6072
|
+
});
|
|
6073
|
+
},
|
|
6074
|
+
async getCategoryLeaderboard(category, mode = "match") {
|
|
6075
|
+
const q = new URLSearchParams({
|
|
6076
|
+
category_topic: category.topic,
|
|
6077
|
+
category_subtopic: category.subtopic,
|
|
6078
|
+
mode
|
|
6079
|
+
});
|
|
6080
|
+
return apiFetch(`/api/v1/arena/leaderboard?${q.toString()}`, {
|
|
6081
|
+
method: "GET"
|
|
6082
|
+
});
|
|
6083
|
+
}
|
|
6084
|
+
};
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
// src/arena/hash.ts
|
|
6088
|
+
import { createHash } from "crypto";
|
|
6089
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
6090
|
+
function sha256Hex(bytes) {
|
|
6091
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
6092
|
+
}
|
|
6093
|
+
function sha256Digest(bytes) {
|
|
6094
|
+
return `sha256:${sha256Hex(bytes)}`;
|
|
6095
|
+
}
|
|
6096
|
+
function hashFile(path2) {
|
|
6097
|
+
const bytes = readFileSync2(path2);
|
|
6098
|
+
return { bytes, digest: sha256Digest(bytes) };
|
|
6099
|
+
}
|
|
6100
|
+
|
|
6101
|
+
// src/commands/arena.ts
|
|
6102
|
+
var FIRST_PARTY_ARENA_HANDLE = "gig-pa-operator";
|
|
6103
|
+
var FIRST_PARTY_ARENA_SLUG = "arena-v1";
|
|
6104
|
+
var ARENA_CAPABILITY = "arena.v1";
|
|
6105
|
+
function servicesHostBaseUrl(ctx) {
|
|
6106
|
+
return ctx.cfg.servicesHostUrl ?? process.env.LINKEDCLAW_SERVICES_HOST_URL ?? ctx.cfg.cloudUrl;
|
|
6107
|
+
}
|
|
6108
|
+
function endpointForListing(listing, ctx) {
|
|
6109
|
+
const endpoint = listing.external_endpoint;
|
|
6110
|
+
return typeof endpoint === "string" && endpoint.length > 0 ? endpoint : servicesHostBaseUrl(ctx);
|
|
6111
|
+
}
|
|
6112
|
+
function assertArenaPa(listing, source) {
|
|
6113
|
+
const caps = Array.isArray(listing.capabilities) ? listing.capabilities : [];
|
|
6114
|
+
if (!caps.includes(ARENA_CAPABILITY)) {
|
|
6115
|
+
throw new LinkedClawError(
|
|
6116
|
+
"arena_target_not_arena_pa",
|
|
6117
|
+
`${source} does not advertise ${ARENA_CAPABILITY}.`
|
|
6118
|
+
);
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
async function resolveArenaTarget(ctx, opts) {
|
|
6122
|
+
if (opts.target && /^https?:\/\//i.test(opts.target)) {
|
|
6123
|
+
throw new LinkedClawError(
|
|
6124
|
+
"arena_target_must_be_agent_id",
|
|
6125
|
+
"--target now expects an arena.v1 agent id, not a URL."
|
|
6126
|
+
);
|
|
6127
|
+
}
|
|
6128
|
+
const listing = opts.target ? await ctx.consumer.getAgent(opts.target) : await ctx.consumer.resolveAgentHandle(FIRST_PARTY_ARENA_HANDLE, FIRST_PARTY_ARENA_SLUG);
|
|
6129
|
+
assertArenaPa(listing, opts.target ?? `${FIRST_PARTY_ARENA_HANDLE}/${FIRST_PARTY_ARENA_SLUG}`);
|
|
6130
|
+
return { agentId: listing.agent_id, baseUrl: endpointForListing(listing, ctx) };
|
|
6131
|
+
}
|
|
6132
|
+
async function buildArenaApi(opts) {
|
|
6133
|
+
const ctx = buildContext();
|
|
6134
|
+
if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
|
|
6135
|
+
const target = await resolveArenaTarget(ctx, opts);
|
|
6136
|
+
return makeArenaApi(target.baseUrl, ctx.cfg.apiKey);
|
|
6137
|
+
}
|
|
6138
|
+
function parseCategory(opts) {
|
|
6139
|
+
if (!opts.categoryTopic) {
|
|
6140
|
+
throw new LinkedClawError("missing_category_topic", "--category-topic is required.");
|
|
6141
|
+
}
|
|
6142
|
+
if (!opts.categorySubtopic) {
|
|
6143
|
+
throw new LinkedClawError("missing_category_subtopic", "--category-subtopic is required.");
|
|
6144
|
+
}
|
|
6145
|
+
return { topic: opts.categoryTopic, subtopic: opts.categorySubtopic };
|
|
6146
|
+
}
|
|
6147
|
+
function parseSeq(value) {
|
|
6148
|
+
const seq = Number(value);
|
|
6149
|
+
if (!Number.isInteger(seq) || seq < 1) {
|
|
6150
|
+
throw new LinkedClawError("invalid_seq", "--seq must be a positive integer.");
|
|
6151
|
+
}
|
|
6152
|
+
return seq;
|
|
6153
|
+
}
|
|
6154
|
+
function parseScore(value) {
|
|
6155
|
+
const score = Number(value);
|
|
6156
|
+
if (!Number.isFinite(score) || score < 0 || score > 1) {
|
|
6157
|
+
throw new LinkedClawError("invalid_juror_score", "score must be a number between 0 and 1.");
|
|
6158
|
+
}
|
|
6159
|
+
return score;
|
|
6160
|
+
}
|
|
6161
|
+
function isPlainObject(value) {
|
|
6162
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6163
|
+
}
|
|
6164
|
+
function readTournamentManifest(path2) {
|
|
6165
|
+
if (path2 === "-" && process.stdin.isTTY) {
|
|
6166
|
+
throw new LinkedClawError(
|
|
6167
|
+
"arena_tournament_manifest_stdin_tty",
|
|
6168
|
+
"stdin is a TTY; pass a file path or pipe JSON via stdin (e.g. cat tournament.json | linkedclaw arena tournament create -)."
|
|
6169
|
+
);
|
|
6170
|
+
}
|
|
6171
|
+
let raw;
|
|
6172
|
+
try {
|
|
6173
|
+
raw = readFileSync3(path2 === "-" ? 0 : path2, "utf8");
|
|
6174
|
+
} catch (err) {
|
|
6175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6176
|
+
throw new LinkedClawError(
|
|
6177
|
+
"arena_tournament_manifest_read_failed",
|
|
6178
|
+
path2 === "-" ? `could not read from stdin (use "-" to pipe JSON): ${message}` : message
|
|
6179
|
+
);
|
|
6180
|
+
}
|
|
6181
|
+
let parsed;
|
|
6182
|
+
try {
|
|
6183
|
+
parsed = JSON.parse(raw);
|
|
6184
|
+
} catch (err) {
|
|
6185
|
+
throw new LinkedClawError(
|
|
6186
|
+
"arena_tournament_manifest_json_invalid",
|
|
6187
|
+
err instanceof Error ? err.message : String(err)
|
|
6188
|
+
);
|
|
6189
|
+
}
|
|
6190
|
+
if (!isPlainObject(parsed)) {
|
|
6191
|
+
throw new LinkedClawError(
|
|
6192
|
+
"arena_tournament_manifest_shape_invalid",
|
|
6193
|
+
"manifest must be a JSON object."
|
|
6194
|
+
);
|
|
6195
|
+
}
|
|
6196
|
+
if (parsed.mode !== "tournament") {
|
|
6197
|
+
throw new LinkedClawError(
|
|
6198
|
+
"arena_tournament_manifest_mode_invalid",
|
|
6199
|
+
'manifest mode must be exactly "tournament".'
|
|
6200
|
+
);
|
|
6201
|
+
}
|
|
6202
|
+
if (!isPlainObject(parsed.category) || !isPlainObject(parsed.config)) {
|
|
6203
|
+
throw new LinkedClawError(
|
|
6204
|
+
"arena_tournament_manifest_shape_invalid",
|
|
6205
|
+
"manifest must include category and config object fields."
|
|
6206
|
+
);
|
|
6207
|
+
}
|
|
6208
|
+
return {
|
|
6209
|
+
mode: "tournament",
|
|
6210
|
+
category: parsed.category,
|
|
6211
|
+
config: parsed.config
|
|
6212
|
+
};
|
|
6213
|
+
}
|
|
6214
|
+
function parseIdempotencyKey(value) {
|
|
6215
|
+
const idempotencyKey = value?.trim();
|
|
6216
|
+
if (!idempotencyKey) {
|
|
6217
|
+
throw new LinkedClawError(
|
|
6218
|
+
"arena_idempotency_key_required",
|
|
6219
|
+
"--idempotency-key must be a non-empty string."
|
|
6220
|
+
);
|
|
6221
|
+
}
|
|
6222
|
+
if (/[\r\n]/.test(idempotencyKey)) {
|
|
6223
|
+
throw new LinkedClawError(
|
|
6224
|
+
"arena_idempotency_key_invalid",
|
|
6225
|
+
"--idempotency-key must not contain newlines."
|
|
6226
|
+
);
|
|
6227
|
+
}
|
|
6228
|
+
return idempotencyKey;
|
|
6229
|
+
}
|
|
6230
|
+
function mergeSubmissionHash(response, submissionHash) {
|
|
6231
|
+
if (response && typeof response === "object" && "submission" in response && response.submission && typeof response.submission === "object") {
|
|
6232
|
+
const submission = response.submission;
|
|
6233
|
+
if (typeof submission.submission_hash === "string") return response;
|
|
6234
|
+
return { ...response, submission: { ...submission, submission_hash: submissionHash } };
|
|
6235
|
+
}
|
|
6236
|
+
if (response && typeof response === "object" && "submission_hash" in response) return response;
|
|
6237
|
+
return { response, submission_hash: submissionHash };
|
|
6238
|
+
}
|
|
6239
|
+
function registerArenaCommands(program2) {
|
|
6240
|
+
const arena = program2.command("arena").description("Arena PA commands");
|
|
6241
|
+
const tournament = arena.command("tournament").description("Arena tournament commands");
|
|
6242
|
+
tournament.command("create <manifest.json>").description("Create a tournament Arena from an exact JSON manifest").option("--idempotency-key <key>", "Required replay key for tournament creation").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (manifestPath, opts) => {
|
|
6243
|
+
await runCommand(async () => {
|
|
6244
|
+
const idempotencyKey = parseIdempotencyKey(opts.idempotencyKey);
|
|
6245
|
+
return (await buildArenaApi(opts)).createTournamentArena(readTournamentManifest(manifestPath), {
|
|
6246
|
+
idempotencyKey
|
|
6247
|
+
});
|
|
6248
|
+
}, { human: opts.human });
|
|
6249
|
+
});
|
|
6250
|
+
arena.command("register").description("Register a contestant agent for Arena offers").requiredOption("--agent-id <agt_id>").requiredOption("--mandate-id <mandate_id>").requiredOption("--category-topic <topic>").requiredOption("--category-subtopic <subtopic>").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(
|
|
6251
|
+
async (opts) => {
|
|
6252
|
+
await runCommand(async () => {
|
|
6253
|
+
const api = await buildArenaApi(opts);
|
|
6254
|
+
return api.register({
|
|
6255
|
+
contestant_agent_id: opts.agentId,
|
|
6256
|
+
mandate_id: opts.mandateId,
|
|
6257
|
+
category: parseCategory(opts)
|
|
6258
|
+
});
|
|
6259
|
+
}, { human: opts.human });
|
|
6260
|
+
}
|
|
6261
|
+
);
|
|
6262
|
+
arena.command("offers").description("List durable Arena offers for this owner").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (opts) => {
|
|
6263
|
+
await runCommand(async () => (await buildArenaApi(opts)).listOffers(), { human: opts.human });
|
|
6264
|
+
});
|
|
6265
|
+
arena.command("accept <offer_id>").description("Accept an Arena offer").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (offerId, opts) => {
|
|
6266
|
+
await runCommand(async () => (await buildArenaApi(opts)).acceptOffer(offerId), { human: opts.human });
|
|
6267
|
+
});
|
|
6268
|
+
arena.command("submit <arena_id>").description("Submit a text or file answer to an Arena").requiredOption("--offer-id <offer_id>").option("--match-id <match_id>", "Pending match id for match-mode submissions").option("--file <path>").option("--body <text>").option("--content-ref <ref>").option("--seq <n>", "Submission sequence number", parseSeq, 1).option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(
|
|
6269
|
+
async (arenaId, opts) => {
|
|
6270
|
+
await runCommand(async () => {
|
|
6271
|
+
if (opts.file && opts.body !== void 0) {
|
|
6272
|
+
throw new LinkedClawError("submission_source_conflict", "Use exactly one of --file or --body.");
|
|
6273
|
+
}
|
|
6274
|
+
if (!opts.file && opts.body === void 0) {
|
|
6275
|
+
throw new LinkedClawError("submission_source_required", "Use exactly one of --file or --body.");
|
|
6276
|
+
}
|
|
6277
|
+
let request;
|
|
6278
|
+
if (opts.file) {
|
|
6279
|
+
const { bytes, digest } = hashFile(opts.file);
|
|
6280
|
+
request = {
|
|
6281
|
+
offer_id: opts.offerId,
|
|
6282
|
+
raw_content: bytes.toString("utf8"),
|
|
6283
|
+
content_ref: opts.contentRef ?? opts.file,
|
|
6284
|
+
...opts.matchId ? { match_id: opts.matchId } : {},
|
|
6285
|
+
seq: opts.seq,
|
|
6286
|
+
submission_hash: digest
|
|
6287
|
+
};
|
|
6288
|
+
} else {
|
|
6289
|
+
const body = opts.body ?? "";
|
|
6290
|
+
request = {
|
|
6291
|
+
offer_id: opts.offerId,
|
|
6292
|
+
raw_content: body,
|
|
6293
|
+
...opts.contentRef ? { content_ref: opts.contentRef } : {},
|
|
6294
|
+
...opts.matchId ? { match_id: opts.matchId } : {},
|
|
6295
|
+
seq: opts.seq,
|
|
6296
|
+
submission_hash: sha256Digest(Buffer.from(body, "utf8"))
|
|
6297
|
+
};
|
|
6298
|
+
}
|
|
6299
|
+
const response = await (await buildArenaApi(opts)).submit(arenaId, request);
|
|
6300
|
+
return mergeSubmissionHash(response, request.submission_hash);
|
|
6301
|
+
}, { human: opts.human });
|
|
6302
|
+
}
|
|
6303
|
+
);
|
|
6304
|
+
const vote = arena.command("vote").description("Arena juror voting commands");
|
|
6305
|
+
vote.command("task <arena_id> <submission_id> <score>").description("Submit a task-submission juror score").option("--rationale-ref <ref>").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (arenaId, submissionId, scoreValue, opts) => {
|
|
6306
|
+
await runCommand(async () => {
|
|
6307
|
+
const score = parseScore(scoreValue);
|
|
6308
|
+
return (await buildArenaApi(opts)).voteTask(arenaId, {
|
|
6309
|
+
submission_id: submissionId,
|
|
6310
|
+
score,
|
|
6311
|
+
...opts.rationaleRef ? { rationale_ref: opts.rationaleRef } : {}
|
|
6312
|
+
});
|
|
6313
|
+
}, { human: opts.human });
|
|
6314
|
+
});
|
|
6315
|
+
vote.command("match <arena_id> <match_id> <outcome>").description("Submit a match-mode juror outcome").option("--rationale-ref <ref>").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (arenaId, matchId, outcome, opts) => {
|
|
6316
|
+
await runCommand(async () => {
|
|
6317
|
+
if (!["a", "b", "tie", "both_bad"].includes(outcome)) {
|
|
6318
|
+
throw new LinkedClawError(
|
|
6319
|
+
"invalid_juror_outcome",
|
|
6320
|
+
"outcome must be one of: a, b, tie, both_bad."
|
|
6321
|
+
);
|
|
6322
|
+
}
|
|
6323
|
+
return (await buildArenaApi(opts)).voteMatch(arenaId, matchId, {
|
|
6324
|
+
outcome,
|
|
6325
|
+
...opts.rationaleRef ? { rationale_ref: opts.rationaleRef } : {}
|
|
6326
|
+
});
|
|
6327
|
+
}, { human: opts.human });
|
|
6328
|
+
});
|
|
6329
|
+
arena.command("list").description("List visible Arenas").option("--registered", "Only arenas where this owner is registered or submitted").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (opts) => {
|
|
6330
|
+
await runCommand(async () => (await buildArenaApi(opts)).listArenas({ registered: opts.registered }), {
|
|
6331
|
+
human: opts.human
|
|
6332
|
+
});
|
|
6333
|
+
});
|
|
6334
|
+
arena.command("leaderboard [arena_id]").description("Read an Arena leaderboard").option("--category-topic <topic>").option("--category-subtopic <subtopic>").option("--mode <mode>", "Leaderboard mode", "match").option("--target <agent_id>", "Arena PA agent id override").option("--human", "Human-readable output").action(async (arenaId, opts) => {
|
|
6335
|
+
await runCommand(async () => {
|
|
6336
|
+
const api = await buildArenaApi(opts);
|
|
6337
|
+
if (arenaId) {
|
|
6338
|
+
return api.getLeaderboard(arenaId);
|
|
6339
|
+
}
|
|
6340
|
+
if (opts.mode !== "match") {
|
|
6341
|
+
throw new LinkedClawError(
|
|
6342
|
+
"unsupported_arena_leaderboard_mode",
|
|
6343
|
+
"--mode must be match when no arena_id is provided."
|
|
6344
|
+
);
|
|
6345
|
+
}
|
|
6346
|
+
return api.getCategoryLeaderboard(parseCategory(opts), opts.mode);
|
|
6347
|
+
}, { human: opts.human });
|
|
6348
|
+
});
|
|
6349
|
+
}
|
|
6350
|
+
|
|
6351
|
+
// src/commands/auth.ts
|
|
6352
|
+
function registerAuthCommands(program2) {
|
|
6353
|
+
program2.command("login").description("Store API key in ~/.linkedclaw/config.yaml").option("--api-key <key>", "API key (otherwise read from stdin)").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
|
|
6354
|
+
await runCommand(async () => {
|
|
6355
|
+
let apiKey = opts.apiKey;
|
|
6356
|
+
if (!apiKey) {
|
|
6357
|
+
apiKey = await readLine("Paste API key: ");
|
|
6358
|
+
}
|
|
6359
|
+
if (!apiKey) throw new Error("empty api key");
|
|
6360
|
+
const prev = readFileConfig();
|
|
6361
|
+
const next = { ...prev, apiKey, ...opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {} };
|
|
6362
|
+
writeFileConfig(next);
|
|
6363
|
+
return { ok: true, path: configPath() };
|
|
6364
|
+
});
|
|
6365
|
+
});
|
|
6366
|
+
program2.command("register").description("Open browser to create a LinkedClaw account, then paste your API key").option("--no-browser", "Print URL instead of attempting to open the browser").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
|
|
6367
|
+
await runCommand(async () => {
|
|
6368
|
+
const prev = readFileConfig();
|
|
6369
|
+
const cloudUrl = opts.cloudUrl ?? prev.cloudUrl ?? process.env.LINKEDCLAW_CLOUD_URL ?? DEFAULT_CLOUD_URL;
|
|
6370
|
+
const portalUrl = cloudUrl.replace(/\/$/, "") + "/register";
|
|
6371
|
+
let opened = false;
|
|
6372
|
+
if (opts.browser !== false) {
|
|
6373
|
+
try {
|
|
6374
|
+
const open = (await import("open")).default;
|
|
6375
|
+
await open(portalUrl);
|
|
6376
|
+
opened = true;
|
|
6377
|
+
} catch {
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
if (!opened) {
|
|
6381
|
+
process.stderr.write(`Open this URL in a browser to register:
|
|
6382
|
+
${portalUrl}
|
|
6383
|
+
|
|
6384
|
+
`);
|
|
6385
|
+
}
|
|
6386
|
+
const apiKey = await readLine("Paste your API key (from portal Settings \u2192 API Keys): ");
|
|
6387
|
+
if (!apiKey) throw new Error("empty api key");
|
|
6388
|
+
const next = {
|
|
6389
|
+
...prev,
|
|
6390
|
+
apiKey,
|
|
6391
|
+
cloudUrl
|
|
6392
|
+
};
|
|
5626
6393
|
writeFileConfig(next);
|
|
5627
6394
|
return { ok: true, path: configPath(), opened };
|
|
5628
6395
|
});
|
|
@@ -5661,9 +6428,1060 @@ function tryParseJson(v) {
|
|
|
5661
6428
|
}
|
|
5662
6429
|
}
|
|
5663
6430
|
|
|
6431
|
+
// src/commands/converge.ts
|
|
6432
|
+
import { spawnSync } from "child_process";
|
|
6433
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
6434
|
+
import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
|
|
6435
|
+
|
|
6436
|
+
// src/converge/api.ts
|
|
6437
|
+
function makeFetchError(code, body) {
|
|
6438
|
+
const err = new LinkedClawError(`api_${code}`, `HTTP ${code}`);
|
|
6439
|
+
err.code = code;
|
|
6440
|
+
err.body = body;
|
|
6441
|
+
return err;
|
|
6442
|
+
}
|
|
6443
|
+
function makeConvergeApi(cloudUrl, apiKey) {
|
|
6444
|
+
async function apiFetch(path2, opts = {}) {
|
|
6445
|
+
const url = cloudUrl.replace(/\/$/, "") + path2;
|
|
6446
|
+
const res = await fetch(url, {
|
|
6447
|
+
...opts,
|
|
6448
|
+
headers: {
|
|
6449
|
+
"Content-Type": "application/json",
|
|
6450
|
+
Authorization: `Bearer ${apiKey}`,
|
|
6451
|
+
...opts.headers ?? {}
|
|
6452
|
+
}
|
|
6453
|
+
});
|
|
6454
|
+
let body;
|
|
6455
|
+
try {
|
|
6456
|
+
body = await res.json();
|
|
6457
|
+
} catch {
|
|
6458
|
+
body = null;
|
|
6459
|
+
}
|
|
6460
|
+
if (!res.ok) throw makeFetchError(res.status, body);
|
|
6461
|
+
return body;
|
|
6462
|
+
}
|
|
6463
|
+
return {
|
|
6464
|
+
async getDebate(debateId) {
|
|
6465
|
+
return apiFetch(`/api/v1/debates/${debateId}`);
|
|
6466
|
+
},
|
|
6467
|
+
async getCommonsLogEvents(cid, opts = {}) {
|
|
6468
|
+
const requested = opts.limit ?? 1e3;
|
|
6469
|
+
const PAGE = 1e3;
|
|
6470
|
+
const offsetStart = opts.offset ?? 0;
|
|
6471
|
+
let collected = [];
|
|
6472
|
+
let cursor = offsetStart;
|
|
6473
|
+
while (collected.length < requested) {
|
|
6474
|
+
const params = new URLSearchParams();
|
|
6475
|
+
params.set("offset", String(cursor));
|
|
6476
|
+
params.set("limit", String(Math.min(PAGE, requested - collected.length)));
|
|
6477
|
+
const page = await apiFetch(
|
|
6478
|
+
`/api/v1/commons-logs/${cid}/events?${params}`
|
|
6479
|
+
);
|
|
6480
|
+
const hoisted = page.events.map((e) => ({
|
|
6481
|
+
...e,
|
|
6482
|
+
event_type: e.event_type ?? e.payload?.event_type ?? ""
|
|
6483
|
+
}));
|
|
6484
|
+
collected = collected.concat(hoisted);
|
|
6485
|
+
if (page.events.length === 0 || page.next_offset === cursor) break;
|
|
6486
|
+
cursor = page.next_offset;
|
|
6487
|
+
}
|
|
6488
|
+
return { events: collected, next_offset: cursor };
|
|
6489
|
+
},
|
|
6490
|
+
async discoverPaAgentId() {
|
|
6491
|
+
const result = await apiFetch(
|
|
6492
|
+
"/api/v1/agents?capability=convergence_synthesizer.v1"
|
|
6493
|
+
);
|
|
6494
|
+
const listings = Array.isArray(result) ? result : result.agents ?? [];
|
|
6495
|
+
if (listings.length === 0) {
|
|
6496
|
+
throw new LinkedClawError("pa_not_found", "No agent found with capability convergence_synthesizer.v1");
|
|
6497
|
+
}
|
|
6498
|
+
return listings[0].agent_id;
|
|
6499
|
+
},
|
|
6500
|
+
async findExistingMandate(principalAgentId, delegateAgentId, requiredScopes) {
|
|
6501
|
+
const result = await apiFetch(`/api/v1/mandates?kind=generalized`);
|
|
6502
|
+
const list = Array.isArray(result) ? result : result.mandates ?? [];
|
|
6503
|
+
const required = new Set(requiredScopes);
|
|
6504
|
+
const now = Date.now();
|
|
6505
|
+
for (const m of list) {
|
|
6506
|
+
if (m.principal_agent_id !== principalAgentId) continue;
|
|
6507
|
+
if (m.delegate_agent_id !== delegateAgentId) continue;
|
|
6508
|
+
if (m.revoked_at) continue;
|
|
6509
|
+
if (m.expires_at && new Date(m.expires_at).getTime() <= now) continue;
|
|
6510
|
+
if (![...required].every((s) => m.scope.includes(s))) continue;
|
|
6511
|
+
return m;
|
|
6512
|
+
}
|
|
6513
|
+
return null;
|
|
6514
|
+
},
|
|
6515
|
+
async issueMandate(principalAgentId, delegateAgentId, scopes, expiresAt) {
|
|
6516
|
+
return apiFetch("/api/v1/mandates", {
|
|
6517
|
+
method: "POST",
|
|
6518
|
+
body: JSON.stringify({
|
|
6519
|
+
principal_agent_id: principalAgentId,
|
|
6520
|
+
delegate_agent_id: delegateAgentId,
|
|
6521
|
+
scope: scopes,
|
|
6522
|
+
...expiresAt ? { expires_at: expiresAt } : {}
|
|
6523
|
+
})
|
|
6524
|
+
});
|
|
6525
|
+
},
|
|
6526
|
+
async startRun(sourceDebateId) {
|
|
6527
|
+
return apiFetch("/api/v1/convergence/runs", {
|
|
6528
|
+
method: "POST",
|
|
6529
|
+
body: JSON.stringify({ source_debate_id: sourceDebateId })
|
|
6530
|
+
});
|
|
6531
|
+
},
|
|
6532
|
+
async getRun(runId) {
|
|
6533
|
+
return apiFetch(`/api/v1/convergence/runs/${runId}`);
|
|
6534
|
+
},
|
|
6535
|
+
async acceptOwnerB(runId) {
|
|
6536
|
+
return apiFetch(`/api/v1/convergence/runs/${runId}/owner_b_accept`, {
|
|
6537
|
+
method: "POST"
|
|
6538
|
+
});
|
|
6539
|
+
},
|
|
6540
|
+
async appendCommonsLog(cid, eventType, payload) {
|
|
6541
|
+
return apiFetch(`/api/v1/commons-logs/${cid}/append`, {
|
|
6542
|
+
method: "POST",
|
|
6543
|
+
body: JSON.stringify({ event_type: eventType, payload })
|
|
6544
|
+
});
|
|
6545
|
+
},
|
|
6546
|
+
async acceptCruxDecision(runId, cruxId, body) {
|
|
6547
|
+
return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/accept`, {
|
|
6548
|
+
method: "POST",
|
|
6549
|
+
body: JSON.stringify(body)
|
|
6550
|
+
});
|
|
6551
|
+
},
|
|
6552
|
+
async rejectCruxDecision(runId, cruxId, body) {
|
|
6553
|
+
return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/reject`, {
|
|
6554
|
+
method: "POST",
|
|
6555
|
+
body: JSON.stringify(body)
|
|
6556
|
+
});
|
|
6557
|
+
},
|
|
6558
|
+
async attestCruxDecision(runId, cruxId, body) {
|
|
6559
|
+
return apiFetch(`/api/v1/convergence/runs/${runId}/cruxes/${cruxId}/attest`, {
|
|
6560
|
+
method: "POST",
|
|
6561
|
+
body: JSON.stringify(body)
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
6564
|
+
};
|
|
6565
|
+
}
|
|
6566
|
+
|
|
6567
|
+
// src/converge/hash.ts
|
|
6568
|
+
import { createHash as createHash2 } from "crypto";
|
|
6569
|
+
function encodeString(s) {
|
|
6570
|
+
let out = '"';
|
|
6571
|
+
for (let i = 0; i < s.length; i++) {
|
|
6572
|
+
const cp = s.charCodeAt(i);
|
|
6573
|
+
if (cp === 34) out += '\\"';
|
|
6574
|
+
else if (cp === 92) out += "\\\\";
|
|
6575
|
+
else if (cp === 8) out += "\\b";
|
|
6576
|
+
else if (cp === 9) out += "\\t";
|
|
6577
|
+
else if (cp === 10) out += "\\n";
|
|
6578
|
+
else if (cp === 12) out += "\\f";
|
|
6579
|
+
else if (cp === 13) out += "\\r";
|
|
6580
|
+
else if (cp < 32 || cp > 126) out += `\\u${cp.toString(16).padStart(4, "0")}`;
|
|
6581
|
+
else out += s[i];
|
|
6582
|
+
}
|
|
6583
|
+
return out + '"';
|
|
6584
|
+
}
|
|
6585
|
+
function canonicalize(value) {
|
|
6586
|
+
if (value === null) return "null";
|
|
6587
|
+
if (typeof value === "string") return encodeString(value);
|
|
6588
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
6589
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
|
|
6590
|
+
const keys = Object.keys(value).sort();
|
|
6591
|
+
return "{" + keys.map((k) => encodeString(k) + ":" + canonicalize(value[k])).join(",") + "}";
|
|
6592
|
+
}
|
|
6593
|
+
function sha256OfCanonicalJson(value) {
|
|
6594
|
+
const h = createHash2("sha256");
|
|
6595
|
+
h.update(canonicalize(value));
|
|
6596
|
+
return "sha256:" + h.digest("hex");
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6599
|
+
// src/converge/lock.ts
|
|
6600
|
+
import { closeSync, openSync, unlinkSync, writeSync } from "fs";
|
|
6601
|
+
import { join as join2 } from "path";
|
|
6602
|
+
var LOCK_FILENAME = ".lock";
|
|
6603
|
+
function acquireLock(stagingDir) {
|
|
6604
|
+
const path2 = join2(stagingDir, LOCK_FILENAME);
|
|
6605
|
+
let fd;
|
|
6606
|
+
try {
|
|
6607
|
+
fd = openSync(path2, "wx");
|
|
6608
|
+
} catch (e) {
|
|
6609
|
+
if (e.code === "EEXIST") {
|
|
6610
|
+
throw new LinkedClawError(
|
|
6611
|
+
"lock_held",
|
|
6612
|
+
`Lock held at ${path2}. If no other run/accept is in progress, delete ${path2} to recover.`
|
|
6613
|
+
);
|
|
6614
|
+
}
|
|
6615
|
+
throw e;
|
|
6616
|
+
}
|
|
6617
|
+
writeSync(fd, JSON.stringify({ pid: process.pid }));
|
|
6618
|
+
closeSync(fd);
|
|
6619
|
+
return () => {
|
|
6620
|
+
try {
|
|
6621
|
+
unlinkSync(path2);
|
|
6622
|
+
} catch {
|
|
6623
|
+
}
|
|
6624
|
+
};
|
|
6625
|
+
}
|
|
6626
|
+
|
|
6627
|
+
// src/converge/staging.ts
|
|
6628
|
+
import { createHash as createHash3 } from "crypto";
|
|
6629
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
6630
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
6631
|
+
import { load as yamlLoad2, dump as yamlDump2 } from "js-yaml";
|
|
6632
|
+
function stagingPathFor(stagingDir, cruxId) {
|
|
6633
|
+
return join3(stagingDir, `${cruxId}.md`);
|
|
6634
|
+
}
|
|
6635
|
+
function listCruxFiles(stagingDir) {
|
|
6636
|
+
if (!existsSync2(stagingDir)) return [];
|
|
6637
|
+
return readdirSync(stagingDir).filter(
|
|
6638
|
+
(f) => f.endsWith(".md") && !f.startsWith(".")
|
|
6639
|
+
);
|
|
6640
|
+
}
|
|
6641
|
+
function parseStaging(text) {
|
|
6642
|
+
if (!text.startsWith("---\n")) {
|
|
6643
|
+
throw new Error("Missing YAML frontmatter: document must start with ---\\n");
|
|
6644
|
+
}
|
|
6645
|
+
const endIdx = text.indexOf("\n---\n", 4);
|
|
6646
|
+
if (endIdx === -1) {
|
|
6647
|
+
throw new Error("Malformed frontmatter: no closing ---");
|
|
6648
|
+
}
|
|
6649
|
+
const yamlText = text.slice(4, endIdx);
|
|
6650
|
+
const body = text.slice(endIdx + 5);
|
|
6651
|
+
const raw = yamlLoad2(yamlText);
|
|
6652
|
+
if (!raw || typeof raw !== "object") {
|
|
6653
|
+
throw new Error("Frontmatter parsed to non-object");
|
|
6654
|
+
}
|
|
6655
|
+
const userResponse = typeof raw._user_response === "string" ? raw._user_response : "";
|
|
6656
|
+
delete raw._user_response;
|
|
6657
|
+
return {
|
|
6658
|
+
frontmatter: raw,
|
|
6659
|
+
userResponse,
|
|
6660
|
+
body
|
|
6661
|
+
};
|
|
6662
|
+
}
|
|
6663
|
+
function dumpStaging(doc) {
|
|
6664
|
+
const fmRaw = { ...doc.frontmatter };
|
|
6665
|
+
fmRaw._user_response = doc.userResponse ?? "";
|
|
6666
|
+
const yamlText = yamlDump2(fmRaw, { lineWidth: -1, sortKeys: false });
|
|
6667
|
+
return `---
|
|
6668
|
+
${yamlText}---
|
|
6669
|
+
${doc.body}`;
|
|
6670
|
+
}
|
|
6671
|
+
function readStaging(path2) {
|
|
6672
|
+
return parseStaging(readFileSync4(path2, "utf8"));
|
|
6673
|
+
}
|
|
6674
|
+
function writeStaging(path2, doc) {
|
|
6675
|
+
mkdirSync2(dirname2(path2), { recursive: true });
|
|
6676
|
+
writeFileSync2(path2, dumpStaging(doc), "utf8");
|
|
6677
|
+
}
|
|
6678
|
+
function computePaBodyHash(body) {
|
|
6679
|
+
return "sha256:" + createHash3("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
6680
|
+
}
|
|
6681
|
+
|
|
6682
|
+
// src/converge/workspace.ts
|
|
6683
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
6684
|
+
import { dirname as dirname3, isAbsolute, join as join4, resolve } from "path";
|
|
6685
|
+
import { load as yamlLoad3, dump as yamlDump3 } from "js-yaml";
|
|
6686
|
+
var META_FILENAME = ".run-meta.yaml";
|
|
6687
|
+
function readRunMeta(stagingDir) {
|
|
6688
|
+
const metaPath = join4(stagingDir, META_FILENAME);
|
|
6689
|
+
if (!existsSync3(metaPath)) return null;
|
|
6690
|
+
return yamlLoad3(readFileSync5(metaPath, "utf8"));
|
|
6691
|
+
}
|
|
6692
|
+
function writeRunMeta(stagingDir, meta) {
|
|
6693
|
+
mkdirSync3(stagingDir, { recursive: true });
|
|
6694
|
+
writeFileSync3(join4(stagingDir, META_FILENAME), yamlDump3(meta), "utf8");
|
|
6695
|
+
}
|
|
6696
|
+
function searchUpward(startDir, maxLevels = 5) {
|
|
6697
|
+
let dir = startDir;
|
|
6698
|
+
for (let i = 0; i < maxLevels; i++) {
|
|
6699
|
+
if (existsSync3(join4(dir, META_FILENAME))) return dir;
|
|
6700
|
+
const parent = dirname3(dir);
|
|
6701
|
+
if (parent === dir) break;
|
|
6702
|
+
dir = parent;
|
|
6703
|
+
}
|
|
6704
|
+
return null;
|
|
6705
|
+
}
|
|
6706
|
+
async function resolveWorkspace(opts) {
|
|
6707
|
+
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
6708
|
+
let stagingDir;
|
|
6709
|
+
let meta = null;
|
|
6710
|
+
if (opts.stagingDir) {
|
|
6711
|
+
stagingDir = isAbsolute(opts.stagingDir) ? opts.stagingDir : resolve(cwd, opts.stagingDir);
|
|
6712
|
+
meta = readRunMeta(stagingDir);
|
|
6713
|
+
if (!meta) {
|
|
6714
|
+
throw new LinkedClawError(
|
|
6715
|
+
"meta_not_found",
|
|
6716
|
+
`No ${META_FILENAME} found in --staging-dir: ${stagingDir}`
|
|
6717
|
+
);
|
|
6718
|
+
}
|
|
6719
|
+
} else {
|
|
6720
|
+
const found = searchUpward(cwd);
|
|
6721
|
+
if (found) {
|
|
6722
|
+
stagingDir = found;
|
|
6723
|
+
meta = readRunMeta(found);
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
if (!meta) {
|
|
6727
|
+
if (opts.runId) {
|
|
6728
|
+
throw new LinkedClawError(
|
|
6729
|
+
"meta_not_found",
|
|
6730
|
+
`--run-id given but no ${META_FILENAME} found (searched upward from ${cwd}). Provide --staging-dir to locate the run workspace.`
|
|
6731
|
+
);
|
|
6732
|
+
}
|
|
6733
|
+
throw new LinkedClawError(
|
|
6734
|
+
"meta_not_found",
|
|
6735
|
+
`No ${META_FILENAME} found (searched upward from ${cwd}). Run 'lc converge run <debate_id>' first.`
|
|
6736
|
+
);
|
|
6737
|
+
}
|
|
6738
|
+
if (opts.runId && opts.runId !== meta.run_id) {
|
|
6739
|
+
throw new LinkedClawError(
|
|
6740
|
+
"run_id_mismatch",
|
|
6741
|
+
`--run-id ${opts.runId} does not match run_id ${meta.run_id} in ${META_FILENAME}`
|
|
6742
|
+
);
|
|
6743
|
+
}
|
|
6744
|
+
const targetCorpus = isAbsolute(meta.target_corpus) ? meta.target_corpus : resolve(stagingDir, meta.target_corpus);
|
|
6745
|
+
return {
|
|
6746
|
+
runId: meta.run_id,
|
|
6747
|
+
sourceDebateId: meta.source_debate_id,
|
|
6748
|
+
paAgentId: meta.pa_agent_id,
|
|
6749
|
+
targetCorpus,
|
|
6750
|
+
stagingDir
|
|
6751
|
+
};
|
|
6752
|
+
}
|
|
6753
|
+
|
|
6754
|
+
// src/commands/converge.ts
|
|
6755
|
+
function resolveAbs(p) {
|
|
6756
|
+
return isAbsolute2(p) ? p : resolve2(process.cwd(), p);
|
|
6757
|
+
}
|
|
6758
|
+
async function getMyUserId(ctx) {
|
|
6759
|
+
const me = await ctx.consumer.getMe();
|
|
6760
|
+
if (!me.user_id) throw new LinkedClawError("no_user_id", "Could not determine user_id from /api/v1/me");
|
|
6761
|
+
return me.user_id;
|
|
6762
|
+
}
|
|
6763
|
+
function recomputeSourceCruxMapHash(events) {
|
|
6764
|
+
const ev = [...events].reverse().find((e) => e.event_type === "crux_map");
|
|
6765
|
+
if (!ev) return null;
|
|
6766
|
+
return sha256OfCanonicalJson(ev.payload.crux_map_data);
|
|
6767
|
+
}
|
|
6768
|
+
function recordedSourceHash(events) {
|
|
6769
|
+
const ev = events.find((e) => e.event_type === "run_started");
|
|
6770
|
+
if (!ev) return null;
|
|
6771
|
+
return typeof ev.payload.source_crux_map_hash === "string" ? ev.payload.source_crux_map_hash : null;
|
|
6772
|
+
}
|
|
6773
|
+
function buildPaBody(op) {
|
|
6774
|
+
const synthesis = typeof op.synthesis_text === "string" ? op.synthesis_text : "";
|
|
6775
|
+
const questions = Array.isArray(op.open_questions) ? op.open_questions : [];
|
|
6776
|
+
const qText = questions.map((q) => `- ${String(q)}`).join("\n");
|
|
6777
|
+
return `# Synthesis
|
|
6778
|
+
|
|
6779
|
+
${synthesis}
|
|
6780
|
+
|
|
6781
|
+
# Open questions
|
|
6782
|
+
|
|
6783
|
+
${qText || "(none)"}
|
|
6784
|
+
`;
|
|
6785
|
+
}
|
|
6786
|
+
function countPreviouslyClarifiedSections(body) {
|
|
6787
|
+
const m = body.match(/^# Previously clarified \(round \d+\)/gm);
|
|
6788
|
+
return m ? m.length : 0;
|
|
6789
|
+
}
|
|
6790
|
+
function slugify(s, maxLen = 64) {
|
|
6791
|
+
const base = s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen).replace(/-+$/g, "");
|
|
6792
|
+
return base || "untitled";
|
|
6793
|
+
}
|
|
6794
|
+
function extractSynthesisSlug(body, maxLen = 32) {
|
|
6795
|
+
const synthIdx = body.indexOf("# Synthesis");
|
|
6796
|
+
const search = synthIdx >= 0 ? body.slice(synthIdx) : body;
|
|
6797
|
+
const lines = search.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
6798
|
+
const first = lines[0] ?? "";
|
|
6799
|
+
return slugify(first, maxLen);
|
|
6800
|
+
}
|
|
6801
|
+
function tryGitAdd(absPath) {
|
|
6802
|
+
try {
|
|
6803
|
+
const r = spawnSync("git", ["add", absPath], { encoding: "utf8" });
|
|
6804
|
+
if (r.error) return `git_add_failed: ${r.error.message}`;
|
|
6805
|
+
if (r.status !== 0) return `git_add_failed: ${(r.stderr || "").trim() || `exit ${r.status}`}`;
|
|
6806
|
+
return null;
|
|
6807
|
+
} catch (e) {
|
|
6808
|
+
return `git_add_failed: ${e.message}`;
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
function assertSafeCruxId(cruxId) {
|
|
6812
|
+
if (!cruxId || cruxId.includes("/") || cruxId.includes("\\") || cruxId.includes("..") || cruxId.includes("\0")) {
|
|
6813
|
+
throw new LinkedClawError("invalid_crux_id", `Invalid crux_id for local file operation: ${cruxId}`);
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
function assertInside(parentDir, childPath) {
|
|
6817
|
+
const parent = resolve2(parentDir);
|
|
6818
|
+
const child = resolve2(childPath);
|
|
6819
|
+
const rel = relative(parent, child);
|
|
6820
|
+
if (rel === "" || !rel.startsWith("..") && !isAbsolute2(rel)) return;
|
|
6821
|
+
throw new LinkedClawError("path_escape", `Refusing local path outside ${parentDir}: ${childPath}`);
|
|
6822
|
+
}
|
|
6823
|
+
function safeStagingPathFor(stagingDir, cruxId) {
|
|
6824
|
+
assertSafeCruxId(cruxId);
|
|
6825
|
+
const path2 = stagingPathFor(stagingDir, cruxId);
|
|
6826
|
+
assertInside(stagingDir, path2);
|
|
6827
|
+
return path2;
|
|
6828
|
+
}
|
|
6829
|
+
function safeAcceptedPath(finalDir, cruxId, synthSlug) {
|
|
6830
|
+
assertSafeCruxId(cruxId);
|
|
6831
|
+
const path2 = join5(finalDir, `${cruxId}__${synthSlug}.md`);
|
|
6832
|
+
assertInside(finalDir, path2);
|
|
6833
|
+
return path2;
|
|
6834
|
+
}
|
|
6835
|
+
function computeDecisionBodyHash(synthesisText, citationsA, citationsB) {
|
|
6836
|
+
return sha256OfCanonicalJson({
|
|
6837
|
+
citations_a: citationsA,
|
|
6838
|
+
citations_b: citationsB,
|
|
6839
|
+
synthesis_text: synthesisText
|
|
6840
|
+
});
|
|
6841
|
+
}
|
|
6842
|
+
function eventKind(ev) {
|
|
6843
|
+
return typeof ev.payload.event_type === "string" ? ev.payload.event_type : ev.event_type;
|
|
6844
|
+
}
|
|
6845
|
+
function decisionEventTypeForAction(action) {
|
|
6846
|
+
if (action === "accept") return "accept_attestation";
|
|
6847
|
+
if (action === "reject") return "reject_attestation";
|
|
6848
|
+
return "attest_only";
|
|
6849
|
+
}
|
|
6850
|
+
function latestConvergenceMapEvent(events) {
|
|
6851
|
+
const operatorUserId = process.env.LINKEDCLAW_OPERATOR_USER_ID ?? "usr_operator";
|
|
6852
|
+
const reversed = [...events].reverse();
|
|
6853
|
+
const ev = reversed.find((e) => eventKind(e) === "convergence_map" && e.signed_by === operatorUserId);
|
|
6854
|
+
if (!ev) {
|
|
6855
|
+
throw new LinkedClawError(
|
|
6856
|
+
"convergence_map_not_found",
|
|
6857
|
+
`No PA-signed convergence_map event found for this run (expected signed_by=${operatorUserId}).`
|
|
6858
|
+
);
|
|
6859
|
+
}
|
|
6860
|
+
return ev;
|
|
6861
|
+
}
|
|
6862
|
+
function latestConvergenceMap(events) {
|
|
6863
|
+
return latestConvergenceMapEvent(events).payload;
|
|
6864
|
+
}
|
|
6865
|
+
function getCruxFromMap(map, cruxId) {
|
|
6866
|
+
const cruxes = Array.isArray(map.cruxes) ? map.cruxes : [];
|
|
6867
|
+
const crux = cruxes.find(
|
|
6868
|
+
(c) => c && typeof c === "object" && c.crux_id === cruxId
|
|
6869
|
+
);
|
|
6870
|
+
if (!crux || typeof crux !== "object") {
|
|
6871
|
+
throw new LinkedClawError("crux_not_found", `crux ${cruxId} not found in latest convergence_map.`);
|
|
6872
|
+
}
|
|
6873
|
+
return crux;
|
|
6874
|
+
}
|
|
6875
|
+
function citationsFromCrux(value) {
|
|
6876
|
+
if (!Array.isArray(value)) return [];
|
|
6877
|
+
return value.filter((v) => v != null && typeof v === "object" && !Array.isArray(v));
|
|
6878
|
+
}
|
|
6879
|
+
function classifyDecisionAttestation(action, outcome, bilateralMandateIntact, synthesisEdited = false) {
|
|
6880
|
+
if (action === "attest") return "user_attested_no_dialog";
|
|
6881
|
+
if (action === "reject") return "user_attested_with_network_context";
|
|
6882
|
+
if (outcome === "already_aligned") {
|
|
6883
|
+
throw new LinkedClawError(
|
|
6884
|
+
"attest_required",
|
|
6885
|
+
"outcome=already_aligned must be decided with `lc converge attest`."
|
|
6886
|
+
);
|
|
6887
|
+
}
|
|
6888
|
+
if ((outcome === "converged" || outcome === "partial_overlap") && bilateralMandateIntact && !synthesisEdited) {
|
|
6889
|
+
return "bilateral_convergence";
|
|
6890
|
+
}
|
|
6891
|
+
return "user_attested_with_network_context";
|
|
6892
|
+
}
|
|
6893
|
+
function decisionPayloadCitations(value) {
|
|
6894
|
+
return citationsFromCrux(value);
|
|
6895
|
+
}
|
|
6896
|
+
function stringFromPayload(payload, key) {
|
|
6897
|
+
const value = payload[key];
|
|
6898
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
6899
|
+
throw new LinkedClawError("decision_payload_invalid", `terminal decision missing ${key}.`);
|
|
6900
|
+
}
|
|
6901
|
+
return value;
|
|
6902
|
+
}
|
|
6903
|
+
function booleanFromPayload(payload, key) {
|
|
6904
|
+
const value = payload[key];
|
|
6905
|
+
if (typeof value !== "boolean") {
|
|
6906
|
+
throw new LinkedClawError("decision_payload_invalid", `terminal decision missing ${key}.`);
|
|
6907
|
+
}
|
|
6908
|
+
return value;
|
|
6909
|
+
}
|
|
6910
|
+
function attestationFromPayload(payload) {
|
|
6911
|
+
const value = stringFromPayload(payload, "attestation");
|
|
6912
|
+
if (value !== "bilateral_convergence" && value !== "user_attested_with_network_context" && value !== "user_attested_no_dialog") {
|
|
6913
|
+
throw new LinkedClawError("decision_payload_invalid", `terminal decision has invalid attestation: ${value}`);
|
|
6914
|
+
}
|
|
6915
|
+
return value;
|
|
6916
|
+
}
|
|
6917
|
+
function terminalOutcomeFromPayload(payload) {
|
|
6918
|
+
const value = stringFromPayload(payload, "terminal_outcome");
|
|
6919
|
+
if (value !== "converged" && value !== "partial_overlap" && value !== "needs_input" && value !== "irreconcilable" && value !== "already_aligned") {
|
|
6920
|
+
throw new LinkedClawError("decision_payload_invalid", `terminal decision has invalid outcome: ${value}`);
|
|
6921
|
+
}
|
|
6922
|
+
return value;
|
|
6923
|
+
}
|
|
6924
|
+
async function buildCruxDecisionRequest(api, ws, cruxId, action, opts = {}) {
|
|
6925
|
+
assertSafeCruxId(cruxId);
|
|
6926
|
+
const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
|
|
6927
|
+
const map = latestConvergenceMap(events);
|
|
6928
|
+
const crux = getCruxFromMap(map, cruxId);
|
|
6929
|
+
const generationId = typeof crux.generation_id === "string" ? crux.generation_id : "";
|
|
6930
|
+
const sourceHash = typeof map.source_crux_map_hash === "string" ? map.source_crux_map_hash : "";
|
|
6931
|
+
const outcome = typeof crux.outcome === "string" ? crux.outcome : "";
|
|
6932
|
+
const latestSubDebateId = typeof crux.latest_sub_debate_id === "string" ? crux.latest_sub_debate_id : null;
|
|
6933
|
+
const bilateralMandateIntact = typeof crux.bilateral_mandate_intact_at_outcome === "boolean" ? crux.bilateral_mandate_intact_at_outcome : typeof crux.bilateral_mandate_intact === "boolean" ? crux.bilateral_mandate_intact : false;
|
|
6934
|
+
const synthesisText = typeof crux.synthesis_text === "string" ? crux.synthesis_text : "";
|
|
6935
|
+
const citationsA = citationsFromCrux(crux.citations_a);
|
|
6936
|
+
const citationsB = citationsFromCrux(crux.citations_b);
|
|
6937
|
+
if (!generationId) throw new LinkedClawError("missing_generation_id", `crux ${cruxId} has no generation_id.`);
|
|
6938
|
+
if (!sourceHash) throw new LinkedClawError("missing_source_hash", "latest convergence_map has no source_crux_map_hash.");
|
|
6939
|
+
if (!outcome) throw new LinkedClawError("missing_outcome", `crux ${cruxId} has no outcome.`);
|
|
6940
|
+
if (action === "accept" && outcome === "already_aligned") {
|
|
6941
|
+
throw new LinkedClawError(
|
|
6942
|
+
"attest_required",
|
|
6943
|
+
"outcome=already_aligned must be decided with `lc converge attest`."
|
|
6944
|
+
);
|
|
6945
|
+
}
|
|
6946
|
+
if (!synthesisText) throw new LinkedClawError("missing_synthesis_text", `crux ${cruxId} has no synthesis_text.`);
|
|
6947
|
+
const paBodyHash = computeDecisionBodyHash(synthesisText, citationsA, citationsB);
|
|
6948
|
+
let acceptedSynthesisText = synthesisText;
|
|
6949
|
+
let synthesisEdited = false;
|
|
6950
|
+
if (action === "accept") {
|
|
6951
|
+
const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
|
|
6952
|
+
if (existsSync4(stagingPath)) {
|
|
6953
|
+
const doc = readStaging(stagingPath);
|
|
6954
|
+
synthesisEdited = computePaBodyHash(doc.body) !== doc.frontmatter.pa_body_hash;
|
|
6955
|
+
if (synthesisEdited) acceptedSynthesisText = doc.body;
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
const acceptedBodyHash = computeDecisionBodyHash(acceptedSynthesisText, citationsA, citationsB);
|
|
6959
|
+
return {
|
|
6960
|
+
convergence_map_generation_id: generationId,
|
|
6961
|
+
source_crux_map_hash: sourceHash,
|
|
6962
|
+
latest_sub_debate_id: latestSubDebateId,
|
|
6963
|
+
terminal_outcome: outcome,
|
|
6964
|
+
bilateral_mandate_intact: bilateralMandateIntact,
|
|
6965
|
+
attestation: classifyDecisionAttestation(action, outcome, bilateralMandateIntact, synthesisEdited),
|
|
6966
|
+
synthesis_edited: synthesisEdited,
|
|
6967
|
+
pa_body_hash: paBodyHash,
|
|
6968
|
+
accepted_body_hash: acceptedBodyHash,
|
|
6969
|
+
synthesis_text: acceptedSynthesisText,
|
|
6970
|
+
citations_a: citationsA,
|
|
6971
|
+
citations_b: citationsB,
|
|
6972
|
+
...opts.message ? { user_message: opts.message } : {}
|
|
6973
|
+
};
|
|
6974
|
+
}
|
|
6975
|
+
async function postCruxDecision(api, ws, cruxId, action, opts = {}) {
|
|
6976
|
+
assertSafeCruxId(cruxId);
|
|
6977
|
+
const body = await buildCruxDecisionRequest(api, ws, cruxId, action, opts);
|
|
6978
|
+
const resp = action === "accept" ? await api.acceptCruxDecision(ws.runId, cruxId, body) : action === "reject" ? await api.rejectCruxDecision(ws.runId, cruxId, body) : await api.attestCruxDecision(ws.runId, cruxId, body);
|
|
6979
|
+
return { event_id: resp.event_id, body };
|
|
6980
|
+
}
|
|
6981
|
+
async function materializeAcceptedCrux(ctx, api, ws, cruxId, payload, opts = {}) {
|
|
6982
|
+
const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
|
|
6983
|
+
if (!existsSync4(stagingPath)) return { warning: `staging_not_found: ${stagingPath}` };
|
|
6984
|
+
const doc = readStaging(stagingPath);
|
|
6985
|
+
const fm = doc.frontmatter;
|
|
6986
|
+
const sourceDebate = await api.getDebate(ws.sourceDebateId);
|
|
6987
|
+
const body = stringFromPayload(payload, "synthesis_text");
|
|
6988
|
+
const citationsA = decisionPayloadCitations(payload.citations_a);
|
|
6989
|
+
const citationsB = decisionPayloadCitations(payload.citations_b);
|
|
6990
|
+
const acceptedBodyHash = stringFromPayload(payload, "accepted_body_hash");
|
|
6991
|
+
const computedAcceptedBodyHash = computeDecisionBodyHash(body, citationsA, citationsB);
|
|
6992
|
+
if (acceptedBodyHash !== computedAcceptedBodyHash) {
|
|
6993
|
+
return { warning: `decision_body_hash_mismatch: ${cruxId}` };
|
|
6994
|
+
}
|
|
6995
|
+
const paBodyHash = stringFromPayload(payload, "pa_body_hash");
|
|
6996
|
+
const synthesisEdited = booleanFromPayload(payload, "synthesis_edited");
|
|
6997
|
+
const attestation = attestationFromPayload(payload);
|
|
6998
|
+
const terminalOutcome = terminalOutcomeFromPayload(payload);
|
|
6999
|
+
const generationId = stringFromPayload(payload, "convergence_map_generation_id");
|
|
7000
|
+
const me = await getMyUserId(ctx);
|
|
7001
|
+
const acceptedDoc = {
|
|
7002
|
+
frontmatter: {
|
|
7003
|
+
...fm,
|
|
7004
|
+
generation_id: generationId,
|
|
7005
|
+
pa_body_hash: paBodyHash,
|
|
7006
|
+
outcome: terminalOutcome,
|
|
7007
|
+
bilateral_mandate_intact: booleanFromPayload(payload, "bilateral_mandate_intact"),
|
|
7008
|
+
citations_a: citationsA,
|
|
7009
|
+
citations_b: citationsB,
|
|
7010
|
+
provenance: {
|
|
7011
|
+
signed_off_by: me,
|
|
7012
|
+
signed_off_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7013
|
+
accepted_generation_id: generationId,
|
|
7014
|
+
attestation,
|
|
7015
|
+
synthesis_edited: synthesisEdited,
|
|
7016
|
+
...opts.message ? { user_message: opts.message } : {},
|
|
7017
|
+
...synthesisEdited ? { pa_body_hash: paBodyHash, accepted_body_hash: acceptedBodyHash } : {}
|
|
7018
|
+
}
|
|
7019
|
+
},
|
|
7020
|
+
userResponse: "",
|
|
7021
|
+
body
|
|
7022
|
+
};
|
|
7023
|
+
const topicSlug = slugify(sourceDebate.topic ?? ws.sourceDebateId);
|
|
7024
|
+
const synthSlug = extractSynthesisSlug(body);
|
|
7025
|
+
const finalDir = join5(ws.targetCorpus, "converged", topicSlug);
|
|
7026
|
+
const finalPath = safeAcceptedPath(finalDir, cruxId, synthSlug);
|
|
7027
|
+
if (existsSync4(finalPath) && readStaging(finalPath).body !== acceptedDoc.body) {
|
|
7028
|
+
return { warning: `sync_conflict: ${finalPath}` };
|
|
7029
|
+
}
|
|
7030
|
+
mkdirSync4(finalDir, { recursive: true });
|
|
7031
|
+
if (!existsSync4(finalPath) || dumpStaging(readStaging(finalPath)) !== dumpStaging(acceptedDoc)) {
|
|
7032
|
+
writeStaging(finalPath, acceptedDoc);
|
|
7033
|
+
}
|
|
7034
|
+
unlinkSync2(stagingPath);
|
|
7035
|
+
const gitWarning = tryGitAdd(finalPath);
|
|
7036
|
+
return {
|
|
7037
|
+
accepted_path: finalPath,
|
|
7038
|
+
attestation,
|
|
7039
|
+
synthesis_edited: synthesisEdited,
|
|
7040
|
+
...gitWarning ? { warning: gitWarning } : {}
|
|
7041
|
+
};
|
|
7042
|
+
}
|
|
7043
|
+
async function syncTerminalDecisions(ctx, api, ws, opts = {}) {
|
|
7044
|
+
if (opts.cruxId) assertSafeCruxId(opts.cruxId);
|
|
7045
|
+
const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
|
|
7046
|
+
const warnings = [];
|
|
7047
|
+
const mapEvent = latestConvergenceMapEvent(events);
|
|
7048
|
+
const map = mapEvent.payload;
|
|
7049
|
+
const cruxGeneration = /* @__PURE__ */ new Map();
|
|
7050
|
+
for (const crux of Array.isArray(map.cruxes) ? map.cruxes : []) {
|
|
7051
|
+
if (!crux || typeof crux !== "object" || Array.isArray(crux)) continue;
|
|
7052
|
+
const c = crux;
|
|
7053
|
+
if (typeof c.crux_id === "string" && typeof c.generation_id === "string") {
|
|
7054
|
+
cruxGeneration.set(c.crux_id, c.generation_id);
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7057
|
+
const terminal = /* @__PURE__ */ new Map();
|
|
7058
|
+
for (const ev of [...events].sort((a, b) => a.seq - b.seq)) {
|
|
7059
|
+
if (ev.seq <= mapEvent.seq) continue;
|
|
7060
|
+
if (ev.signed_by !== mapEvent.signed_by) continue;
|
|
7061
|
+
const eventType = eventKind(ev);
|
|
7062
|
+
if (!["accept_attestation", "reject_attestation", "attest_only"].includes(eventType)) continue;
|
|
7063
|
+
const cid = typeof ev.payload.crux_id === "string" ? ev.payload.crux_id : "";
|
|
7064
|
+
if (!cid || terminal.has(cid)) continue;
|
|
7065
|
+
try {
|
|
7066
|
+
assertSafeCruxId(cid);
|
|
7067
|
+
} catch (err) {
|
|
7068
|
+
warnings.push(`${cid}: ${err.message}`);
|
|
7069
|
+
continue;
|
|
7070
|
+
}
|
|
7071
|
+
if (ev.payload.convergence_map_generation_id !== cruxGeneration.get(cid)) continue;
|
|
7072
|
+
terminal.set(cid, { eventType, payload: ev.payload });
|
|
7073
|
+
}
|
|
7074
|
+
if (opts.injectedTerminal) {
|
|
7075
|
+
assertSafeCruxId(opts.injectedTerminal.cruxId);
|
|
7076
|
+
terminal.set(opts.injectedTerminal.cruxId, {
|
|
7077
|
+
eventType: opts.injectedTerminal.eventType,
|
|
7078
|
+
payload: opts.injectedTerminal.payload
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
const materialized = [];
|
|
7082
|
+
const cleaned = [];
|
|
7083
|
+
const release = acquireLock(ws.stagingDir);
|
|
7084
|
+
try {
|
|
7085
|
+
for (const [cid, terminalEvent] of terminal.entries()) {
|
|
7086
|
+
if (opts.cruxId && cid !== opts.cruxId) continue;
|
|
7087
|
+
const stagingPath = safeStagingPathFor(ws.stagingDir, cid);
|
|
7088
|
+
const eventType = terminalEvent.eventType;
|
|
7089
|
+
if (eventType === "accept_attestation") {
|
|
7090
|
+
const result = await materializeAcceptedCrux(ctx, api, ws, cid, terminalEvent.payload, opts);
|
|
7091
|
+
if (result.accepted_path) materialized.push(result.accepted_path);
|
|
7092
|
+
if (result.warning) warnings.push(`${cid}: ${result.warning}`);
|
|
7093
|
+
continue;
|
|
7094
|
+
}
|
|
7095
|
+
if (existsSync4(stagingPath)) {
|
|
7096
|
+
unlinkSync2(stagingPath);
|
|
7097
|
+
cleaned.push(cid);
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
} finally {
|
|
7101
|
+
release();
|
|
7102
|
+
}
|
|
7103
|
+
return { materialized, cleaned, warnings };
|
|
7104
|
+
}
|
|
7105
|
+
function registerConvergeCommands(program2) {
|
|
7106
|
+
const converge = program2.command("converge").description("Convergence: bilateral merger of two crux maps into shared corpus");
|
|
7107
|
+
converge.command("run [ref]").description("Start a convergence run (Owner A) or accept (Owner B with --accept), then sync staging").option("--target-corpus <path>", "Absolute path to the target corpus directory").option("--staging-dir <path>", "Override staging directory").option("--accept", "Owner B: accept an existing run by run_id").option("--source-debate-id <id>", "Owner B fallback when /convergence/runs/{run_id} is unavailable").option("--force-regenerate", "Bypass source-hash drift check and regenerate staging files").option("--wait <secs>", "Poll until terminal_emitted or timeout", parseInt).action(
|
|
7108
|
+
async (ref, opts) => {
|
|
7109
|
+
await runCommand(async () => {
|
|
7110
|
+
const ctx = buildContext();
|
|
7111
|
+
if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
|
|
7112
|
+
const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
|
|
7113
|
+
let ws;
|
|
7114
|
+
const resolvedStagingDir = opts.stagingDir ? resolveAbs(opts.stagingDir) : null;
|
|
7115
|
+
const metaExisting = resolvedStagingDir ? readRunMeta(resolvedStagingDir) : null;
|
|
7116
|
+
if (opts.accept) {
|
|
7117
|
+
if (!ref) {
|
|
7118
|
+
throw new LinkedClawError("missing_run_id", "--accept requires the run_id as a positional argument.");
|
|
7119
|
+
}
|
|
7120
|
+
const runId = ref;
|
|
7121
|
+
if (metaExisting && resolvedStagingDir) {
|
|
7122
|
+
const meta = metaExisting;
|
|
7123
|
+
ws = {
|
|
7124
|
+
runId: meta.run_id,
|
|
7125
|
+
sourceDebateId: meta.source_debate_id,
|
|
7126
|
+
paAgentId: meta.pa_agent_id,
|
|
7127
|
+
targetCorpus: meta.target_corpus,
|
|
7128
|
+
stagingDir: resolvedStagingDir
|
|
7129
|
+
};
|
|
7130
|
+
} else {
|
|
7131
|
+
if (!opts.targetCorpus) {
|
|
7132
|
+
throw new LinkedClawError(
|
|
7133
|
+
"missing_target_corpus",
|
|
7134
|
+
"Owner B --accept requires --target-corpus on first call."
|
|
7135
|
+
);
|
|
7136
|
+
}
|
|
7137
|
+
const targetCorpus = resolveAbs(opts.targetCorpus);
|
|
7138
|
+
let sourceDebateId;
|
|
7139
|
+
let paAgentId;
|
|
7140
|
+
let principalAgentId;
|
|
7141
|
+
if (opts.sourceDebateId) {
|
|
7142
|
+
sourceDebateId = opts.sourceDebateId;
|
|
7143
|
+
paAgentId = await api.discoverPaAgentId();
|
|
7144
|
+
const sourceDebate = await api.getDebate(sourceDebateId);
|
|
7145
|
+
principalAgentId = sourceDebate.agent_b_id;
|
|
7146
|
+
} else {
|
|
7147
|
+
const runMeta = await api.getRun(runId);
|
|
7148
|
+
sourceDebateId = runMeta.source_debate_id;
|
|
7149
|
+
paAgentId = runMeta.pa_agent_id;
|
|
7150
|
+
principalAgentId = runMeta.agent_b_id;
|
|
7151
|
+
}
|
|
7152
|
+
const existing = await api.findExistingMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
|
|
7153
|
+
if (!existing) await api.issueMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
|
|
7154
|
+
await api.acceptOwnerB(runId);
|
|
7155
|
+
const stagingDir = join5(targetCorpus, "converged", "staging", runId);
|
|
7156
|
+
mkdirSync4(stagingDir, { recursive: true });
|
|
7157
|
+
const meta = {
|
|
7158
|
+
run_id: runId,
|
|
7159
|
+
source_debate_id: sourceDebateId,
|
|
7160
|
+
pa_agent_id: paAgentId,
|
|
7161
|
+
target_corpus: targetCorpus,
|
|
7162
|
+
owner_role: "b"
|
|
7163
|
+
};
|
|
7164
|
+
writeRunMeta(stagingDir, meta);
|
|
7165
|
+
ws = { runId, sourceDebateId, paAgentId, targetCorpus, stagingDir };
|
|
7166
|
+
}
|
|
7167
|
+
} else if (!metaExisting) {
|
|
7168
|
+
if (!ref) {
|
|
7169
|
+
throw new LinkedClawError(
|
|
7170
|
+
"missing_source_debate_id",
|
|
7171
|
+
"First run requires a source_debate_id argument. Use --staging-dir to resume an existing run."
|
|
7172
|
+
);
|
|
7173
|
+
}
|
|
7174
|
+
if (!opts.targetCorpus) {
|
|
7175
|
+
throw new LinkedClawError("missing_target_corpus", "First call requires --target-corpus.");
|
|
7176
|
+
}
|
|
7177
|
+
const targetCorpus = resolveAbs(opts.targetCorpus);
|
|
7178
|
+
const paAgentId = await api.discoverPaAgentId();
|
|
7179
|
+
const sourceDebate = await api.getDebate(ref);
|
|
7180
|
+
const principalAgentId = sourceDebate.agent_a_id;
|
|
7181
|
+
const existing = await api.findExistingMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
|
|
7182
|
+
if (!existing) await api.issueMandate(principalAgentId, paAgentId, ["debate.create", "debate.accept"]);
|
|
7183
|
+
const { run_id } = await api.startRun(ref);
|
|
7184
|
+
const stagingDir = join5(targetCorpus, "converged", "staging", run_id);
|
|
7185
|
+
mkdirSync4(stagingDir, { recursive: true });
|
|
7186
|
+
const meta = {
|
|
7187
|
+
run_id,
|
|
7188
|
+
source_debate_id: ref,
|
|
7189
|
+
pa_agent_id: paAgentId,
|
|
7190
|
+
target_corpus: targetCorpus,
|
|
7191
|
+
owner_role: "a"
|
|
7192
|
+
};
|
|
7193
|
+
writeRunMeta(stagingDir, meta);
|
|
7194
|
+
ws = { runId: run_id, sourceDebateId: ref, paAgentId, targetCorpus, stagingDir };
|
|
7195
|
+
} else {
|
|
7196
|
+
ws = await resolveWorkspace({ stagingDir: opts.stagingDir });
|
|
7197
|
+
}
|
|
7198
|
+
const refreshStaging = async (cruxes, canonicalSourceHash2) => {
|
|
7199
|
+
for (const c of cruxes) {
|
|
7200
|
+
if (!c.latest_sub_debate_id) continue;
|
|
7201
|
+
const subD = await api.getDebate(c.latest_sub_debate_id);
|
|
7202
|
+
const subEvs = await api.getCommonsLogEvents(subD.commons_log_id, { limit: 2e3 });
|
|
7203
|
+
const outcomeEv = [...subEvs.events].reverse().find(
|
|
7204
|
+
(e) => e.payload.event_type === "convergence_outcome" || e.event_type === "convergence_outcome"
|
|
7205
|
+
);
|
|
7206
|
+
if (!outcomeEv) continue;
|
|
7207
|
+
const op = outcomeEv.payload;
|
|
7208
|
+
const body = buildPaBody(op);
|
|
7209
|
+
const newPaBodyHash = computePaBodyHash(body);
|
|
7210
|
+
const path2 = safeStagingPathFor(ws.stagingDir, c.crux_id);
|
|
7211
|
+
const existingDoc = existsSync4(path2) ? readStaging(path2) : null;
|
|
7212
|
+
if (existingDoc && existingDoc.frontmatter.pa_body_hash === newPaBodyHash) continue;
|
|
7213
|
+
const fm = {
|
|
7214
|
+
debate_id: ws.sourceDebateId,
|
|
7215
|
+
run_id: ws.runId,
|
|
7216
|
+
crux_id: c.crux_id,
|
|
7217
|
+
sub_debate_chain: c.sub_debate_chain,
|
|
7218
|
+
latest_sub_debate_id: c.latest_sub_debate_id,
|
|
7219
|
+
source_crux_map_hash: canonicalSourceHash2,
|
|
7220
|
+
generation_id: `gen_${ws.runId.slice(-8)}`,
|
|
7221
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7222
|
+
pa_body_hash: newPaBodyHash,
|
|
7223
|
+
outcome: op.outcome ?? c.outcome,
|
|
7224
|
+
bilateral_mandate_intact: typeof op.bilateral_mandate_intact === "boolean" ? op.bilateral_mandate_intact : c.bilateral_mandate_intact ?? false,
|
|
7225
|
+
citations_a: Array.isArray(op.citations_a) ? op.citations_a : [],
|
|
7226
|
+
citations_b: Array.isArray(op.citations_b) ? op.citations_b : [],
|
|
7227
|
+
mod_progress_summary: op.final_progress_signal != null && typeof op.final_progress_signal === "object" && !Array.isArray(op.final_progress_signal) ? op.final_progress_signal : {},
|
|
7228
|
+
attested_by_user: existingDoc?.frontmatter.attested_by_user ?? false
|
|
7229
|
+
};
|
|
7230
|
+
writeStaging(path2, { frontmatter: fm, userResponse: existingDoc?.userResponse ?? "", body });
|
|
7231
|
+
}
|
|
7232
|
+
};
|
|
7233
|
+
let summary;
|
|
7234
|
+
let canonicalSourceHash = "";
|
|
7235
|
+
{
|
|
7236
|
+
const release = acquireLock(ws.stagingDir);
|
|
7237
|
+
try {
|
|
7238
|
+
for (const fn of listCruxFiles(ws.stagingDir)) {
|
|
7239
|
+
const path2 = join5(ws.stagingDir, fn);
|
|
7240
|
+
const doc = readStaging(path2);
|
|
7241
|
+
const text = (doc.userResponse || "").trim();
|
|
7242
|
+
if (!text) continue;
|
|
7243
|
+
const subDebateId = doc.frontmatter.latest_sub_debate_id;
|
|
7244
|
+
if (!subDebateId) continue;
|
|
7245
|
+
const subDebate = await api.getDebate(subDebateId);
|
|
7246
|
+
await api.appendCommonsLog(subDebate.commons_log_id, "owner_clarification", {
|
|
7247
|
+
event_type: "owner_clarification",
|
|
7248
|
+
content: text,
|
|
7249
|
+
in_response_to_event_id: null
|
|
7250
|
+
});
|
|
7251
|
+
const round = countPreviouslyClarifiedSections(doc.body) + 1;
|
|
7252
|
+
doc.userResponse = "";
|
|
7253
|
+
doc.body = doc.body.trimEnd() + `
|
|
7254
|
+
|
|
7255
|
+
# Previously clarified (round ${round})
|
|
7256
|
+
|
|
7257
|
+
${text}
|
|
7258
|
+
`;
|
|
7259
|
+
writeStaging(path2, doc);
|
|
7260
|
+
}
|
|
7261
|
+
const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
|
|
7262
|
+
summary = reduceRunState(ws, events);
|
|
7263
|
+
const sourceDebate = await api.getDebate(ws.sourceDebateId);
|
|
7264
|
+
const sourceEvents = await api.getCommonsLogEvents(sourceDebate.commons_log_id, {
|
|
7265
|
+
limit: 2e3
|
|
7266
|
+
});
|
|
7267
|
+
const liveSourceHash = recomputeSourceCruxMapHash(sourceEvents.events);
|
|
7268
|
+
const recordedHash = recordedSourceHash(events);
|
|
7269
|
+
if (recordedHash && liveSourceHash && liveSourceHash !== recordedHash && !opts.forceRegenerate) {
|
|
7270
|
+
throw new LinkedClawError(
|
|
7271
|
+
"source_crux_map_drift",
|
|
7272
|
+
`Source crux-map changed since run started (recorded=${recordedHash} live=${liveSourceHash}). Re-run with --force-regenerate.`
|
|
7273
|
+
);
|
|
7274
|
+
}
|
|
7275
|
+
canonicalSourceHash = recordedHash ?? liveSourceHash ?? "";
|
|
7276
|
+
await refreshStaging(summary.cruxes, canonicalSourceHash);
|
|
7277
|
+
} finally {
|
|
7278
|
+
release();
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7281
|
+
if (opts.wait && !summary.terminal_emitted) {
|
|
7282
|
+
const deadline = Date.now() + opts.wait * 1e3;
|
|
7283
|
+
let polledEvents = null;
|
|
7284
|
+
while (Date.now() < deadline) {
|
|
7285
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
7286
|
+
const { events: polled } = await api.getCommonsLogEvents(ws.runId, { limit: 5e3 });
|
|
7287
|
+
if (reduceRunState(ws, polled).terminal_emitted) {
|
|
7288
|
+
polledEvents = polled;
|
|
7289
|
+
break;
|
|
7290
|
+
}
|
|
7291
|
+
}
|
|
7292
|
+
if (polledEvents) {
|
|
7293
|
+
summary = reduceRunState(ws, polledEvents);
|
|
7294
|
+
const release2 = acquireLock(ws.stagingDir);
|
|
7295
|
+
try {
|
|
7296
|
+
await refreshStaging(summary.cruxes, canonicalSourceHash);
|
|
7297
|
+
} finally {
|
|
7298
|
+
release2();
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
return { run_id: ws.runId, summary, terminal_emitted: summary.terminal_emitted };
|
|
7303
|
+
});
|
|
7304
|
+
}
|
|
7305
|
+
);
|
|
7306
|
+
converge.command("clarify <sub_debate_id> <text>").description("Post owner_clarification.v1 directly to a sub-debate's Commons Log").action(async (subDebateId, text) => {
|
|
7307
|
+
await runCommand(async () => {
|
|
7308
|
+
const ctx = buildContext();
|
|
7309
|
+
if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
|
|
7310
|
+
const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
|
|
7311
|
+
const subDebate = await api.getDebate(subDebateId);
|
|
7312
|
+
const { seq } = await api.appendCommonsLog(subDebate.commons_log_id, "owner_clarification", {
|
|
7313
|
+
event_type: "owner_clarification",
|
|
7314
|
+
content: text,
|
|
7315
|
+
in_response_to_event_id: null
|
|
7316
|
+
});
|
|
7317
|
+
return { sub_debate_id: subDebateId, commons_log_id: subDebate.commons_log_id, seq };
|
|
7318
|
+
});
|
|
7319
|
+
});
|
|
7320
|
+
converge.command("attest <crux_id>").description("POST an attest_only decision to the Convergence PA").option("--run-id <id>").option("--staging-dir <path>").action(async (cruxId, opts) => {
|
|
7321
|
+
await runCommand(async () => {
|
|
7322
|
+
const ctx = buildContext();
|
|
7323
|
+
if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
|
|
7324
|
+
const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
|
|
7325
|
+
const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
|
|
7326
|
+
const { event_id } = await postCruxDecision(api, ws, cruxId, "attest");
|
|
7327
|
+
return { run_id: ws.runId, crux_id: cruxId, action: "attest", event_id, synced: false };
|
|
7328
|
+
});
|
|
7329
|
+
});
|
|
7330
|
+
converge.command("accept <crux_id>").description("POST an accept decision to the Convergence PA").option("--run-id <id>").option("--staging-dir <path>").option("--message <text>", "Optional user_message recorded in provenance").option("--with-sync", "Compatibility: after the PA accepts, materialize local corpus files").action(async (cruxId, opts) => {
|
|
7331
|
+
await runCommand(async () => {
|
|
7332
|
+
const ctx = buildContext();
|
|
7333
|
+
if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
|
|
7334
|
+
const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
|
|
7335
|
+
const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
|
|
7336
|
+
const { event_id, body } = await postCruxDecision(api, ws, cruxId, "accept", { message: opts.message });
|
|
7337
|
+
if (opts.withSync) {
|
|
7338
|
+
const sync = await syncTerminalDecisions(ctx, api, ws, {
|
|
7339
|
+
cruxId,
|
|
7340
|
+
message: opts.message,
|
|
7341
|
+
injectedTerminal: {
|
|
7342
|
+
cruxId,
|
|
7343
|
+
eventType: decisionEventTypeForAction("accept"),
|
|
7344
|
+
payload: { event_type: decisionEventTypeForAction("accept"), crux_id: cruxId, ...body }
|
|
7345
|
+
}
|
|
7346
|
+
});
|
|
7347
|
+
return {
|
|
7348
|
+
run_id: ws.runId,
|
|
7349
|
+
crux_id: cruxId,
|
|
7350
|
+
action: "accept",
|
|
7351
|
+
event_id,
|
|
7352
|
+
synced: true,
|
|
7353
|
+
...sync
|
|
7354
|
+
};
|
|
7355
|
+
}
|
|
7356
|
+
return { run_id: ws.runId, crux_id: cruxId, action: "accept", event_id, synced: false };
|
|
7357
|
+
});
|
|
7358
|
+
});
|
|
7359
|
+
converge.command("reject <crux_id>").description("POST a reject decision to the Convergence PA").option("--run-id <id>").option("--staging-dir <path>").action(async (cruxId, opts) => {
|
|
7360
|
+
await runCommand(async () => {
|
|
7361
|
+
const ctx = buildContext();
|
|
7362
|
+
if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
|
|
7363
|
+
const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
|
|
7364
|
+
const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
|
|
7365
|
+
const { event_id } = await postCruxDecision(api, ws, cruxId, "reject");
|
|
7366
|
+
return { run_id: ws.runId, crux_id: cruxId, action: "reject", event_id, synced: false };
|
|
7367
|
+
});
|
|
7368
|
+
});
|
|
7369
|
+
converge.command("sync").description("Materialize PA decision events into local corpus/staging files").option("--run-id <id>").option("--staging-dir <path>").option("--crux-id <id>", "Limit sync to one crux").action(async (opts) => {
|
|
7370
|
+
await runCommand(async () => {
|
|
7371
|
+
const ctx = buildContext();
|
|
7372
|
+
if (!ctx.cfg.apiKey) throw new LinkedClawError("missing_api_key", "Run `linkedclaw login` first.");
|
|
7373
|
+
const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
|
|
7374
|
+
const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
|
|
7375
|
+
const result = await syncTerminalDecisions(ctx, api, ws, { cruxId: opts.cruxId });
|
|
7376
|
+
return { run_id: ws.runId, synced: true, ...result };
|
|
7377
|
+
});
|
|
7378
|
+
});
|
|
7379
|
+
converge.command("review").description("List staging cruxes; surface already_aligned cruxes prominently").option("--run-id <id>").option("--staging-dir <path>").action(async (opts) => {
|
|
7380
|
+
await runCommand(async () => {
|
|
7381
|
+
const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
|
|
7382
|
+
const files = listCruxFiles(ws.stagingDir);
|
|
7383
|
+
const cruxes = [];
|
|
7384
|
+
for (const fn of files) {
|
|
7385
|
+
const doc = readStaging(join5(ws.stagingDir, fn));
|
|
7386
|
+
const fm = doc.frontmatter;
|
|
7387
|
+
cruxes.push({
|
|
7388
|
+
crux_id: fm.crux_id,
|
|
7389
|
+
outcome: fm.outcome,
|
|
7390
|
+
bilateral_mandate_intact: fm.bilateral_mandate_intact,
|
|
7391
|
+
attested_by_user: fm.attested_by_user,
|
|
7392
|
+
latest_sub_debate_id: fm.latest_sub_debate_id,
|
|
7393
|
+
has_user_response: (doc.userResponse || "").trim().length > 0,
|
|
7394
|
+
next_action: fm.outcome === "already_aligned" && !fm.attested_by_user ? "attest" : fm.outcome === "needs_input" ? "clarify_or_accept" : "accept_or_reject"
|
|
7395
|
+
});
|
|
7396
|
+
}
|
|
7397
|
+
const alignedAwaitingAttest = cruxes.filter((c) => c.outcome === "already_aligned" && !c.attested_by_user);
|
|
7398
|
+
return {
|
|
7399
|
+
run_id: ws.runId,
|
|
7400
|
+
staging_dir: ws.stagingDir,
|
|
7401
|
+
cruxes,
|
|
7402
|
+
already_aligned_awaiting_attest: alignedAwaitingAttest.map((c) => c.crux_id)
|
|
7403
|
+
};
|
|
7404
|
+
});
|
|
7405
|
+
});
|
|
7406
|
+
converge.command("status").description("Show reduced run state from the convergence run-log").option("--run-id <id>").option("--staging-dir <path>").option("--all", "List every run-log event (no reduction)").action(async (opts) => {
|
|
7407
|
+
await runCommand(async () => {
|
|
7408
|
+
const ctx = buildContext();
|
|
7409
|
+
if (!ctx.cfg.apiKey) {
|
|
7410
|
+
throw new Error("missing apiKey \u2014 run `linkedclaw login` first");
|
|
7411
|
+
}
|
|
7412
|
+
const ws = await resolveWorkspace({ runId: opts.runId, stagingDir: opts.stagingDir });
|
|
7413
|
+
const api = makeConvergeApi(ctx.cfg.cloudUrl, ctx.cfg.apiKey);
|
|
7414
|
+
const { events } = await api.getCommonsLogEvents(ws.runId, { limit: 1e3 });
|
|
7415
|
+
if (opts.all) return { run_id: ws.runId, events };
|
|
7416
|
+
return reduceRunState(ws, events);
|
|
7417
|
+
});
|
|
7418
|
+
});
|
|
7419
|
+
}
|
|
7420
|
+
function reduceRunState(ws, events) {
|
|
7421
|
+
let started_at = null;
|
|
7422
|
+
let owner_b_accepted = false;
|
|
7423
|
+
let terminal_emitted = false;
|
|
7424
|
+
const cruxMap = /* @__PURE__ */ new Map();
|
|
7425
|
+
for (const ev of events) {
|
|
7426
|
+
const p = ev.payload;
|
|
7427
|
+
switch (ev.event_type) {
|
|
7428
|
+
case "run_started":
|
|
7429
|
+
started_at = ev.appended_at;
|
|
7430
|
+
break;
|
|
7431
|
+
case "owner_b_accepted":
|
|
7432
|
+
owner_b_accepted = true;
|
|
7433
|
+
break;
|
|
7434
|
+
case "sub_debate_dispatched": {
|
|
7435
|
+
const cruxId = p.crux_id;
|
|
7436
|
+
const subDebateId = p.sub_debate_id;
|
|
7437
|
+
if (!cruxMap.has(cruxId)) {
|
|
7438
|
+
cruxMap.set(cruxId, {
|
|
7439
|
+
crux_id: cruxId,
|
|
7440
|
+
latest_sub_debate_id: subDebateId,
|
|
7441
|
+
sub_debate_chain: [subDebateId],
|
|
7442
|
+
outcome: null,
|
|
7443
|
+
bilateral_mandate_intact: null
|
|
7444
|
+
});
|
|
7445
|
+
} else {
|
|
7446
|
+
const entry = cruxMap.get(cruxId);
|
|
7447
|
+
entry.latest_sub_debate_id = subDebateId;
|
|
7448
|
+
entry.sub_debate_chain.push(subDebateId);
|
|
7449
|
+
}
|
|
7450
|
+
break;
|
|
7451
|
+
}
|
|
7452
|
+
case "sub_debate_outcome_observed": {
|
|
7453
|
+
const cruxId = p.crux_id;
|
|
7454
|
+
const entry = cruxMap.get(cruxId);
|
|
7455
|
+
if (entry) {
|
|
7456
|
+
entry.outcome = p.outcome;
|
|
7457
|
+
if (typeof p.bilateral_mandate_intact === "boolean") {
|
|
7458
|
+
entry.bilateral_mandate_intact = p.bilateral_mandate_intact;
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7461
|
+
break;
|
|
7462
|
+
}
|
|
7463
|
+
case "convergence_map":
|
|
7464
|
+
terminal_emitted = true;
|
|
7465
|
+
break;
|
|
7466
|
+
default:
|
|
7467
|
+
if (ev.event_type.startsWith("terminal_")) {
|
|
7468
|
+
terminal_emitted = true;
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
}
|
|
7472
|
+
return {
|
|
7473
|
+
run_id: ws.runId,
|
|
7474
|
+
source_debate_id: ws.sourceDebateId,
|
|
7475
|
+
started_at,
|
|
7476
|
+
owner_b_accepted,
|
|
7477
|
+
cruxes: Array.from(cruxMap.values()),
|
|
7478
|
+
terminal_emitted
|
|
7479
|
+
};
|
|
7480
|
+
}
|
|
7481
|
+
|
|
5664
7482
|
// src/commands/provider.ts
|
|
5665
|
-
import { readFileSync as
|
|
5666
|
-
import { load as
|
|
7483
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
7484
|
+
import { load as yamlLoad4 } from "js-yaml";
|
|
5667
7485
|
|
|
5668
7486
|
// ../../sdk/provider-runtime-ts/dist/index.js
|
|
5669
7487
|
import { EventEmitter } from "events";
|
|
@@ -5855,8 +7673,8 @@ var nodeWsConnector = async (url) => {
|
|
|
5855
7673
|
const ws = new WebSocket3(url);
|
|
5856
7674
|
const transport = {
|
|
5857
7675
|
send: (data) => {
|
|
5858
|
-
return new Promise((
|
|
5859
|
-
ws.send(data, (err) => err ? reject(err) :
|
|
7676
|
+
return new Promise((resolve3, reject) => {
|
|
7677
|
+
ws.send(data, (err) => err ? reject(err) : resolve3());
|
|
5860
7678
|
});
|
|
5861
7679
|
},
|
|
5862
7680
|
close: (code, reason) => {
|
|
@@ -5872,8 +7690,8 @@ var nodeWsConnector = async (url) => {
|
|
|
5872
7690
|
ws.on("error", (err) => fn(err));
|
|
5873
7691
|
}
|
|
5874
7692
|
};
|
|
5875
|
-
const ready = new Promise((
|
|
5876
|
-
ws.once("open", () =>
|
|
7693
|
+
const ready = new Promise((resolve3, reject) => {
|
|
7694
|
+
ws.once("open", () => resolve3());
|
|
5877
7695
|
ws.once("error", (err) => reject(err));
|
|
5878
7696
|
});
|
|
5879
7697
|
return { transport, ready };
|
|
@@ -6308,12 +8126,12 @@ function normalizeReply(reply, nextSeq) {
|
|
|
6308
8126
|
return { payload: reply, seq: nextSeq };
|
|
6309
8127
|
}
|
|
6310
8128
|
function withTimeout(p, ms, code) {
|
|
6311
|
-
return new Promise((
|
|
8129
|
+
return new Promise((resolve3, reject) => {
|
|
6312
8130
|
const t = setTimeout(() => reject(new HandlerError(code, `timed out after ${ms}ms`)), ms);
|
|
6313
8131
|
p.then(
|
|
6314
8132
|
(v) => {
|
|
6315
8133
|
clearTimeout(t);
|
|
6316
|
-
|
|
8134
|
+
resolve3(v);
|
|
6317
8135
|
},
|
|
6318
8136
|
(err) => {
|
|
6319
8137
|
clearTimeout(t);
|
|
@@ -6327,7 +8145,7 @@ function escapeRegex(s) {
|
|
|
6327
8145
|
}
|
|
6328
8146
|
|
|
6329
8147
|
// src/handlers/subprocess.ts
|
|
6330
|
-
import { spawn } from "child_process";
|
|
8148
|
+
import { spawn as spawn2 } from "child_process";
|
|
6331
8149
|
import { randomUUID } from "crypto";
|
|
6332
8150
|
import { createInterface } from "readline";
|
|
6333
8151
|
var SubprocessHandler = class {
|
|
@@ -6339,7 +8157,7 @@ var SubprocessHandler = class {
|
|
|
6339
8157
|
this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e5;
|
|
6340
8158
|
const shell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
|
|
6341
8159
|
const shellArgs = process.platform === "win32" ? ["/c", opts.cmd] : ["-c", opts.cmd];
|
|
6342
|
-
this.child =
|
|
8160
|
+
this.child = spawn2(shell, shellArgs, {
|
|
6343
8161
|
stdio: ["pipe", "pipe", "inherit"],
|
|
6344
8162
|
cwd: opts.cwd,
|
|
6345
8163
|
env: { ...process.env, ...opts.env }
|
|
@@ -6376,10 +8194,10 @@ var SubprocessHandler = class {
|
|
|
6376
8194
|
async onInvoke(evt) {
|
|
6377
8195
|
return this.request(evt);
|
|
6378
8196
|
}
|
|
6379
|
-
async
|
|
8197
|
+
async onGigTaskOffer(evt) {
|
|
6380
8198
|
return this.request(evt);
|
|
6381
8199
|
}
|
|
6382
|
-
async
|
|
8200
|
+
async onGigTaskExecute(evt) {
|
|
6383
8201
|
return this.request(evt);
|
|
6384
8202
|
}
|
|
6385
8203
|
// ───── shutdown ─────
|
|
@@ -6400,7 +8218,7 @@ var SubprocessHandler = class {
|
|
|
6400
8218
|
request(frame) {
|
|
6401
8219
|
const id = randomUUID();
|
|
6402
8220
|
const line = JSON.stringify({ id, ...frame }) + "\n";
|
|
6403
|
-
return new Promise((
|
|
8221
|
+
return new Promise((resolve3, reject) => {
|
|
6404
8222
|
const timer = setTimeout(() => {
|
|
6405
8223
|
this.pending.delete(id);
|
|
6406
8224
|
reject(new Error(`handler_timeout: no response for ${frame.type} after ${this.requestTimeoutMs}ms`));
|
|
@@ -6408,7 +8226,7 @@ var SubprocessHandler = class {
|
|
|
6408
8226
|
if (typeof timer.unref === "function") {
|
|
6409
8227
|
timer.unref();
|
|
6410
8228
|
}
|
|
6411
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
8229
|
+
this.pending.set(id, { resolve: resolve3, reject, timer });
|
|
6412
8230
|
this.stdin.write(line, (err) => {
|
|
6413
8231
|
if (err) {
|
|
6414
8232
|
this.pending.delete(id);
|
|
@@ -6456,9 +8274,9 @@ var SubprocessHandler = class {
|
|
|
6456
8274
|
// src/commands/provider.ts
|
|
6457
8275
|
function registerProviderCommands(program2) {
|
|
6458
8276
|
const provider = program2.command("provider").description("Provider-side commands");
|
|
6459
|
-
provider.command("register <config>").description('Register (create/update) an agent listing from a provider YAML file. Use "-" for stdin.').option("--human", "Human-readable output").action(async (
|
|
8277
|
+
provider.command("register <config>").description('Register (create/update) an agent listing from a provider YAML file. Use "-" for stdin.').option("--human", "Human-readable output").action(async (path2, opts) => {
|
|
6460
8278
|
await runCommand(async () => {
|
|
6461
|
-
const cfg = await loadProviderYaml(
|
|
8279
|
+
const cfg = await loadProviderYaml(path2);
|
|
6462
8280
|
const { providerClient } = buildContext();
|
|
6463
8281
|
const body = buildCreateAgentRequest(cfg);
|
|
6464
8282
|
if (cfg.agentId) {
|
|
@@ -6474,7 +8292,7 @@ function registerProviderCommands(program2) {
|
|
|
6474
8292
|
});
|
|
6475
8293
|
provider.command("update <listing_id>").description("Patch an existing agent listing. Body = JSON file or stdin.").requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)').option("--human", "Human-readable output").action(async (listingId, opts) => {
|
|
6476
8294
|
await runCommand(async () => {
|
|
6477
|
-
const raw = opts.body === "-" ? await readStdin() :
|
|
8295
|
+
const raw = opts.body === "-" ? await readStdin() : readFileSync6(opts.body, "utf8");
|
|
6478
8296
|
const body = JSON.parse(raw);
|
|
6479
8297
|
const { providerClient } = buildContext();
|
|
6480
8298
|
return providerClient.updateAgent(listingId, body);
|
|
@@ -6486,9 +8304,9 @@ function registerProviderCommands(program2) {
|
|
|
6486
8304
|
return consumer.discover({ owner: "me" });
|
|
6487
8305
|
}, { human: opts.human });
|
|
6488
8306
|
});
|
|
6489
|
-
provider.command("run <config>").description("Run a provider daemon: connect WS, dispatch events to handler subprocess").option("--handler-cmd <cmd>", "Shell command to spawn as the handler child process").option("--handler-http <url>", "HTTP webhook URL to POST each event to (alternative to --handler-cmd)").option("--human", "Human-readable status output").action(async (
|
|
8307
|
+
provider.command("run <config>").description("Run a provider daemon: connect WS, dispatch events to handler subprocess").option("--handler-cmd <cmd>", "Shell command to spawn as the handler child process").option("--handler-http <url>", "HTTP webhook URL to POST each event to (alternative to --handler-cmd)").option("--human", "Human-readable status output").action(async (path2, opts) => {
|
|
6490
8308
|
try {
|
|
6491
|
-
const yamlCfg = await loadProviderYaml(
|
|
8309
|
+
const yamlCfg = await loadProviderYaml(path2);
|
|
6492
8310
|
if (!yamlCfg.agentId) {
|
|
6493
8311
|
process.stderr.write(
|
|
6494
8312
|
JSON.stringify({ error: "provider_unconfigured", message: "agentId missing \u2014 run `linkedclaw provider register` first or set it in YAML" }) + "\n"
|
|
@@ -6516,7 +8334,7 @@ function registerProviderCommands(program2) {
|
|
|
6516
8334
|
const handler = opts.handlerCmd ? new SubprocessHandler({ cmd: opts.handlerCmd }) : makeHttpHandler(opts.handlerHttp);
|
|
6517
8335
|
const runtime = new ProviderRuntime({
|
|
6518
8336
|
cloud: {
|
|
6519
|
-
|
|
8337
|
+
gigTasks: {
|
|
6520
8338
|
accept: (taskId, body) => providerClient.acceptGigTask(taskId, body),
|
|
6521
8339
|
submit: (taskId, body) => providerClient.submitGigTask(taskId, body)
|
|
6522
8340
|
}
|
|
@@ -6555,7 +8373,7 @@ function registerProviderCommands(program2) {
|
|
|
6555
8373
|
process.exit(1);
|
|
6556
8374
|
}
|
|
6557
8375
|
});
|
|
6558
|
-
provider.command("pick <bct_id>").description("Manually accept a
|
|
8376
|
+
provider.command("pick <bct_id>").description("Manually accept a gig task (provider side)").requiredOption("--agent-id <agt_id>", "Which of your agents is accepting").option("--slot-key <key>", "Slot key for sliced gig tasks").option("--human", "Human-readable output").action(async (taskId, opts) => {
|
|
6559
8377
|
await runCommand(async () => {
|
|
6560
8378
|
const { providerClient } = buildContext();
|
|
6561
8379
|
const body = { agent_id: opts.agentId };
|
|
@@ -6563,20 +8381,20 @@ function registerProviderCommands(program2) {
|
|
|
6563
8381
|
return providerClient.acceptGigTask(taskId, body);
|
|
6564
8382
|
}, { human: opts.human });
|
|
6565
8383
|
});
|
|
6566
|
-
provider.command("submit <bct_id> <result_file>").description('Submit a
|
|
8384
|
+
provider.command("submit <bct_id> <result_file>").description('Submit a gig task result. result_file = JSON path or "-" for stdin.').option("--human", "Human-readable output").action(async (taskId, resultFile, opts) => {
|
|
6567
8385
|
await runCommand(async () => {
|
|
6568
|
-
const raw = resultFile === "-" ? await readStdin() :
|
|
8386
|
+
const raw = resultFile === "-" ? await readStdin() : readFileSync6(resultFile, "utf8");
|
|
6569
8387
|
const body = JSON.parse(raw);
|
|
6570
8388
|
const { providerClient } = buildContext();
|
|
6571
8389
|
return providerClient.submitGigTask(taskId, body);
|
|
6572
8390
|
}, { human: opts.human });
|
|
6573
8391
|
});
|
|
6574
8392
|
}
|
|
6575
|
-
async function loadProviderYaml(
|
|
6576
|
-
const raw =
|
|
6577
|
-
const parsed =
|
|
8393
|
+
async function loadProviderYaml(path2) {
|
|
8394
|
+
const raw = path2 === "-" ? await readStdin() : readFileSync6(path2, "utf8");
|
|
8395
|
+
const parsed = yamlLoad4(raw);
|
|
6578
8396
|
if (parsed === null || typeof parsed !== "object") {
|
|
6579
|
-
throw new Error(`provider config ${
|
|
8397
|
+
throw new Error(`provider config ${path2} is not a YAML object`);
|
|
6580
8398
|
}
|
|
6581
8399
|
return parsed;
|
|
6582
8400
|
}
|
|
@@ -6617,10 +8435,10 @@ function makeHttpHandler(url) {
|
|
|
6617
8435
|
async onInvoke(evt) {
|
|
6618
8436
|
return await postEvent(url, evt);
|
|
6619
8437
|
},
|
|
6620
|
-
async
|
|
8438
|
+
async onGigTaskOffer(evt) {
|
|
6621
8439
|
return await postEvent(url, evt);
|
|
6622
8440
|
},
|
|
6623
|
-
async
|
|
8441
|
+
async onGigTaskExecute(evt) {
|
|
6624
8442
|
return await postEvent(url, evt);
|
|
6625
8443
|
}
|
|
6626
8444
|
};
|
|
@@ -6636,10 +8454,10 @@ async function postEvent(url, body) {
|
|
|
6636
8454
|
}
|
|
6637
8455
|
|
|
6638
8456
|
// src/commands/requester.ts
|
|
6639
|
-
import { readFileSync as
|
|
6640
|
-
import { load as
|
|
8457
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
8458
|
+
import { load as yamlLoad5 } from "js-yaml";
|
|
6641
8459
|
function registerRequesterCommands(program2) {
|
|
6642
|
-
program2.command("search <capability>").description("Search public agent listings by capability").option("--owner <owner>", '"me" or a "usr_..." id').option("--status <status>", "filter by status (online/offline/disabled)").option("--sort <sort>", "newest |
|
|
8460
|
+
program2.command("search <capability>").description("Search public agent listings by capability").option("--owner <owner>", '"me" or a "usr_..." id').option("--status <status>", "filter by status (online/offline/disabled)").option("--sort <sort>", "newest | trust (default)").option("--human", "Human-readable output").action(async (capability, opts) => {
|
|
6643
8461
|
await runCommand(async () => {
|
|
6644
8462
|
const { requesterFlows } = buildContext();
|
|
6645
8463
|
return requesterFlows.search(capability, {
|
|
@@ -6734,24 +8552,24 @@ function registerRequesterCommands(program2) {
|
|
|
6734
8552
|
return invokeAgent(consumer, agentId, body);
|
|
6735
8553
|
}, { human: opts.human });
|
|
6736
8554
|
});
|
|
6737
|
-
const
|
|
6738
|
-
|
|
6739
|
-
'Create a
|
|
8555
|
+
const gigTask = program2.command("gig-task").description("Gig Task commands");
|
|
8556
|
+
gigTask.command("create <manifest>").description(
|
|
8557
|
+
'Create a gig task from a YAML/JSON manifest file. Use "-" for stdin. Required fields: capability, instruction, target_providers, credits_per_provider.'
|
|
6740
8558
|
).option("--human", "Human-readable output").action(async (manifestPath, opts) => {
|
|
6741
8559
|
await runCommand(async () => {
|
|
6742
8560
|
const { consumer } = buildContext();
|
|
6743
|
-
const raw = manifestPath === "-" ? await readStdin() :
|
|
8561
|
+
const raw = manifestPath === "-" ? await readStdin() : readFileSync7(manifestPath, "utf8");
|
|
6744
8562
|
const body = parseYamlOrJson(raw);
|
|
6745
8563
|
return consumer.createGigTask(body);
|
|
6746
8564
|
}, { human: opts.human });
|
|
6747
8565
|
});
|
|
6748
|
-
|
|
8566
|
+
gigTask.command("get <bct_id>").description("Get a gig task by id").option("--human", "Human-readable output").action(async (taskId, opts) => {
|
|
6749
8567
|
await runCommand(async () => {
|
|
6750
8568
|
const { consumer } = buildContext();
|
|
6751
8569
|
return consumer.getGigTask(taskId);
|
|
6752
8570
|
}, { human: opts.human });
|
|
6753
8571
|
});
|
|
6754
|
-
|
|
8572
|
+
gigTask.command("list").description("List gig tasks I own").option("--status <s>", "Filter by status").option("--human", "Human-readable output").action(async (opts) => {
|
|
6755
8573
|
await runCommand(async () => {
|
|
6756
8574
|
const { consumer } = buildContext();
|
|
6757
8575
|
return consumer.listGigTasks({
|
|
@@ -6759,13 +8577,13 @@ function registerRequesterCommands(program2) {
|
|
|
6759
8577
|
});
|
|
6760
8578
|
}, { human: opts.human });
|
|
6761
8579
|
});
|
|
6762
|
-
|
|
8580
|
+
gigTask.command("available").description("List open gig tasks I could pick up (as provider)").option("--human", "Human-readable output").action(async (opts) => {
|
|
6763
8581
|
await runCommand(async () => {
|
|
6764
8582
|
const { providerClient } = buildContext();
|
|
6765
8583
|
return providerClient.listGigTasksAvailable();
|
|
6766
8584
|
}, { human: opts.human });
|
|
6767
8585
|
});
|
|
6768
|
-
|
|
8586
|
+
gigTask.command("accept <bct_id>").description("Accept a gig task (provider side) \u2014 returns a result_id").requiredOption("--agent-id <agt_id>", "Which of your agents is accepting").option("--slot-key <key>", "Slot key for sliced gig tasks").option("--human", "Human-readable output").action(async (taskId, opts) => {
|
|
6769
8587
|
await runCommand(async () => {
|
|
6770
8588
|
const { providerClient } = buildContext();
|
|
6771
8589
|
const body = { agent_id: opts.agentId };
|
|
@@ -6773,8 +8591,8 @@ function registerRequesterCommands(program2) {
|
|
|
6773
8591
|
return providerClient.acceptGigTask(taskId, body);
|
|
6774
8592
|
}, { human: opts.human });
|
|
6775
8593
|
});
|
|
6776
|
-
|
|
6777
|
-
"Submit
|
|
8594
|
+
gigTask.command("submit <bct_id>").description(
|
|
8595
|
+
"Submit gig task result (provider side). Body must include `result_data` (string) and may include `result_payload` (object) and `proof` (array)."
|
|
6778
8596
|
).requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)').option("--human", "Human-readable output").action(async (taskId, opts) => {
|
|
6779
8597
|
await runCommand(async () => {
|
|
6780
8598
|
const { providerClient } = buildContext();
|
|
@@ -6804,6 +8622,36 @@ function registerRequesterCommands(program2) {
|
|
|
6804
8622
|
return consumer.getBalance();
|
|
6805
8623
|
}, { human: opts.human });
|
|
6806
8624
|
});
|
|
8625
|
+
program2.command("show <agent_id>").description("Show full agent listing including capabilities_meta").option("--capability <name>", "Print only this capability's meta entry").option("--human", "Human-readable output").action(async (agentId, opts) => {
|
|
8626
|
+
await runCommand(async () => {
|
|
8627
|
+
const { consumer } = buildContext();
|
|
8628
|
+
const agent = await consumer.getAgent(agentId);
|
|
8629
|
+
if (opts.capability) {
|
|
8630
|
+
const meta = agent.capabilities_meta?.[opts.capability];
|
|
8631
|
+
if (!meta) {
|
|
8632
|
+
throw new Error(
|
|
8633
|
+
`agent ${agentId} has no capabilities_meta entry for ${JSON.stringify(opts.capability)}`
|
|
8634
|
+
);
|
|
8635
|
+
}
|
|
8636
|
+
return meta;
|
|
8637
|
+
}
|
|
8638
|
+
return agent;
|
|
8639
|
+
}, { human: opts.human });
|
|
8640
|
+
});
|
|
8641
|
+
program2.command("schema <agent_id>").description("Fetch + sha256-verify a capability's input JSON Schema").requiredOption("--capability <name>", "Capability name").option("--human", "Human-readable output").action(async (agentId, opts) => {
|
|
8642
|
+
await runCommand(async () => {
|
|
8643
|
+
const { consumer } = buildContext();
|
|
8644
|
+
const agent = await consumer.getAgent(agentId);
|
|
8645
|
+
try {
|
|
8646
|
+
return await fetchCapabilitySchema(agent, opts.capability);
|
|
8647
|
+
} catch (err) {
|
|
8648
|
+
if (err instanceof CapabilitySchemaError) {
|
|
8649
|
+
throw new Error(`schema fetch failed: ${err.message}`);
|
|
8650
|
+
}
|
|
8651
|
+
throw err;
|
|
8652
|
+
}
|
|
8653
|
+
}, { human: opts.human });
|
|
8654
|
+
});
|
|
6807
8655
|
}
|
|
6808
8656
|
async function runHireRepl(sessionId, ctx) {
|
|
6809
8657
|
const readline = await import("readline/promises");
|
|
@@ -6882,7 +8730,7 @@ function parseJsonOrFail(raw, label) {
|
|
|
6882
8730
|
function parseYamlOrJson(raw) {
|
|
6883
8731
|
const trimmed = raw.trimStart();
|
|
6884
8732
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return JSON.parse(raw);
|
|
6885
|
-
return
|
|
8733
|
+
return yamlLoad5(raw);
|
|
6886
8734
|
}
|
|
6887
8735
|
async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultIntention) {
|
|
6888
8736
|
if (manifestOpt !== void 0 && manifestId !== void 0) {
|
|
@@ -6896,7 +8744,7 @@ async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultInt
|
|
|
6896
8744
|
if (manifestOpt === "-") {
|
|
6897
8745
|
raw = await readStdin();
|
|
6898
8746
|
} else if (manifestOpt.startsWith("@")) {
|
|
6899
|
-
raw =
|
|
8747
|
+
raw = readFileSync7(manifestOpt.slice(1), "utf8");
|
|
6900
8748
|
} else {
|
|
6901
8749
|
raw = manifestOpt;
|
|
6902
8750
|
}
|
|
@@ -6910,12 +8758,16 @@ async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultInt
|
|
|
6910
8758
|
}
|
|
6911
8759
|
|
|
6912
8760
|
// src/bin.ts
|
|
6913
|
-
var
|
|
8761
|
+
var pkgPath = join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
8762
|
+
var CLI_VERSION = JSON.parse(readFileSync8(pkgPath, "utf8")).version;
|
|
6914
8763
|
var program = new Command();
|
|
6915
|
-
program.name("linkedclaw").description("Official LinkedClaw CLI \u2014 any agent can shell out to hire providers, invoke, or
|
|
8764
|
+
program.name("linkedclaw").description("Official LinkedClaw CLI \u2014 any agent can shell out to hire providers, invoke, or gig task").version(`cli ${CLI_VERSION}`);
|
|
6916
8765
|
registerAuthCommands(program);
|
|
6917
8766
|
registerRequesterCommands(program);
|
|
6918
8767
|
registerProviderCommands(program);
|
|
8768
|
+
registerConvergeCommands(program);
|
|
8769
|
+
registerArenaCommands(program);
|
|
8770
|
+
registerAgentCommands(program);
|
|
6919
8771
|
program.parseAsync(process.argv).catch((err) => {
|
|
6920
8772
|
process.stderr.write(
|
|
6921
8773
|
JSON.stringify({ error: "internal_error", message: err instanceof Error ? err.message : String(err) }) + "\n"
|