@linkedclaw/cli 0.1.2 → 0.1.5
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 +248 -48
- package/dist/bin.js +8099 -4778
- package/dist/bin.js.map +1 -1
- package/package.json +17 -32
- 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 +33 -0
- package/src/commands/agent.ts +264 -0
- package/src/commands/arena.ts +393 -0
- package/src/commands/auth.ts +116 -0
- package/src/commands/converge.ts +969 -0
- package/src/commands/provider.ts +245 -0
- package/src/commands/requester.ts +479 -0
- package/src/config.ts +85 -0
- package/src/context.ts +27 -0
- 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/errors.ts +41 -0
- package/src/handlers/subprocess.ts +185 -0
- package/src/output.ts +57 -0
- package/src/types.ts +90 -0
- 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 +82 -0
- 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/test/hire-flags.test.ts +55 -0
- package/test/recv-flags.test.ts +83 -0
- package/test/register-browser.test.ts +55 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +25 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { load as yamlLoad } from "js-yaml";
|
|
4
|
+
import type { ConsumerClient } from "@linkedclaw/consumer";
|
|
5
|
+
import { fetchCapabilitySchema, CapabilitySchemaError } from "@linkedclaw/consumer";
|
|
6
|
+
import type {
|
|
7
|
+
AcceptGigTaskRequest,
|
|
8
|
+
CreateGigTaskRequest,
|
|
9
|
+
GigTaskSubmitRequest,
|
|
10
|
+
EndSessionRequest,
|
|
11
|
+
InvokeRequest,
|
|
12
|
+
TaskManifest,
|
|
13
|
+
} from "../types.js";
|
|
14
|
+
import { buildContext, type CliContext } from "../context.js";
|
|
15
|
+
import { runCommand, readStdin } from "../output.js";
|
|
16
|
+
|
|
17
|
+
export function registerRequesterCommands(program: Command): void {
|
|
18
|
+
program
|
|
19
|
+
.command("search <capability>")
|
|
20
|
+
.description("Search public agent listings by capability")
|
|
21
|
+
.option("--owner <owner>", '"me" or a "usr_..." id')
|
|
22
|
+
.option("--status <status>", "filter by status (online/offline/disabled)")
|
|
23
|
+
.option("--sort <sort>", "newest | trust (default)")
|
|
24
|
+
.option("--human", "Human-readable output")
|
|
25
|
+
.action(async (capability: string, opts) => {
|
|
26
|
+
await runCommand(async () => {
|
|
27
|
+
const { requesterFlows } = buildContext();
|
|
28
|
+
return requesterFlows.search(capability, {
|
|
29
|
+
...(opts.owner ? { owner: opts.owner } : {}),
|
|
30
|
+
...(opts.status ? { status: opts.status } : {}),
|
|
31
|
+
...(opts.sort ? { sort: opts.sort } : {}),
|
|
32
|
+
});
|
|
33
|
+
}, { human: opts.human });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command("hire <agent_id>")
|
|
38
|
+
.description("Create and activate a 1:1 session with an agent")
|
|
39
|
+
.requiredOption("--capability <cap>", "Capability being hired")
|
|
40
|
+
.option("--max-messages <n>", "Message cap (server default 1)", (v) => parseInt(v, 10))
|
|
41
|
+
.option("--referred-by <ref_id>", "Attribution: referrer agent id")
|
|
42
|
+
.option("--no-activate", "Create session but do not activate (returns session only)")
|
|
43
|
+
.option("--message <msg>", "Send a single message after activation, then return")
|
|
44
|
+
.option("--interactive", "Open a REPL after activation (> message / .end / .status / .quit)")
|
|
45
|
+
.option("--human", "Human-readable output")
|
|
46
|
+
.action(async (agentId: string, opts) => {
|
|
47
|
+
await runCommand(async () => {
|
|
48
|
+
const ctx = buildContext();
|
|
49
|
+
const { requesterFlows, cfg } = ctx;
|
|
50
|
+
const result = await requesterFlows.hire({
|
|
51
|
+
providerAgentId: agentId,
|
|
52
|
+
capability: opts.capability,
|
|
53
|
+
...(opts.maxMessages !== undefined ? { maxMessages: opts.maxMessages } : {}),
|
|
54
|
+
...(opts.referredBy !== undefined ? { referredBy: opts.referredBy } : {}),
|
|
55
|
+
autoActivate: opts.activate !== false,
|
|
56
|
+
apiKey: cfg.apiKey!,
|
|
57
|
+
agentId: cfg.agentId ?? "",
|
|
58
|
+
relayUrl: cfg.relayUrl,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const sessionId =
|
|
62
|
+
(result as { session?: { session_id?: string } }).session?.session_id;
|
|
63
|
+
|
|
64
|
+
if (opts.message && sessionId) {
|
|
65
|
+
await requesterFlows.send(sessionId, opts.message, 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (opts.interactive && sessionId) {
|
|
69
|
+
await runHireRepl(sessionId, ctx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}, { human: opts.human });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.command("send <session_id> <message>")
|
|
78
|
+
.description(
|
|
79
|
+
'Send a message in an active session. Message is wrapped as {"text": "..."} ' +
|
|
80
|
+
'if it\'s a plain string, or passed through if it\'s a JSON object. Use "-" to read from stdin.',
|
|
81
|
+
)
|
|
82
|
+
.requiredOption("--seq <n>", "Sequence number (server requires ≥ 1)", (v) => parseInt(v, 10))
|
|
83
|
+
.option("--json", "Interpret message as a JSON object literal (default: wrap as {text})")
|
|
84
|
+
.option("--human", "Human-readable output")
|
|
85
|
+
.action(async (sessionId: string, message: string, opts) => {
|
|
86
|
+
await runCommand(async () => {
|
|
87
|
+
const { requesterFlows } = buildContext();
|
|
88
|
+
const raw = message === "-" ? await readStdin() : message;
|
|
89
|
+
const payload: Record<string, unknown> | string = opts.json
|
|
90
|
+
? (parseJsonOrFail(raw, "<message>") as Record<string, unknown>)
|
|
91
|
+
: raw;
|
|
92
|
+
return requesterFlows.send(sessionId, payload, opts.seq);
|
|
93
|
+
}, { human: opts.human });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
program
|
|
97
|
+
.command("recv <session_id>")
|
|
98
|
+
.description(
|
|
99
|
+
"Fetch session events since --since (default 0). With --wait, polls " +
|
|
100
|
+
"every 1.5s until new events arrive or the deadline elapses.",
|
|
101
|
+
)
|
|
102
|
+
.option("--since <seq>", "Offset to fetch events from (default 0)", (v) => parseInt(v, 10))
|
|
103
|
+
.option("--wait <s>", "Long-poll up to N seconds for new events", (v) => parseInt(v, 10))
|
|
104
|
+
.option("--human", "Human-readable output")
|
|
105
|
+
.action(async (sessionId: string, opts) => {
|
|
106
|
+
await runCommand(async () => {
|
|
107
|
+
const { consumer } = buildContext();
|
|
108
|
+
const offset = opts.since ?? 0;
|
|
109
|
+
const deadline = opts.wait !== undefined ? Date.now() + opts.wait * 1000 : 0;
|
|
110
|
+
let resp = await consumer.getSessionEvents(sessionId, { offset });
|
|
111
|
+
while (
|
|
112
|
+
opts.wait !== undefined &&
|
|
113
|
+
(resp.events?.length ?? 0) === 0 &&
|
|
114
|
+
Date.now() < deadline
|
|
115
|
+
) {
|
|
116
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
117
|
+
resp = await consumer.getSessionEvents(sessionId, { offset });
|
|
118
|
+
}
|
|
119
|
+
if (opts.human) return formatEventsHuman(resp.events ?? []);
|
|
120
|
+
return resp;
|
|
121
|
+
}, { human: opts.human });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
program
|
|
125
|
+
.command("end <session_id>")
|
|
126
|
+
.description("Mark a session ended")
|
|
127
|
+
.option("--message-count <n>", "Declare final message count", (v) => parseInt(v, 10))
|
|
128
|
+
.option("--final-output <text>", "Final output blurb (optional)")
|
|
129
|
+
.option("--human", "Human-readable output")
|
|
130
|
+
.action(async (sessionId: string, opts) => {
|
|
131
|
+
await runCommand(async () => {
|
|
132
|
+
const { consumer } = buildContext();
|
|
133
|
+
const body: EndSessionRequest = {};
|
|
134
|
+
if (opts.messageCount !== undefined) body.message_count = opts.messageCount;
|
|
135
|
+
if (opts.finalOutput !== undefined) body.final_output = opts.finalOutput;
|
|
136
|
+
return endSession(consumer, sessionId, body);
|
|
137
|
+
}, { human: opts.human });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
program
|
|
141
|
+
.command("invoke <agent_id>")
|
|
142
|
+
.description("One-shot stateless invoke")
|
|
143
|
+
.requiredOption("--capability <cap>", "Capability")
|
|
144
|
+
.requiredOption("--input <json>", 'JSON input (or "-" to read from stdin)')
|
|
145
|
+
.option("--max-credits <n>", "Max credits", (v) => parseInt(v, 10))
|
|
146
|
+
.option("--timeout <s>", "Server-side timeout seconds (5..300, default 60)", (v) => parseInt(v, 10))
|
|
147
|
+
.option("--referred-by <ref_id>", "Attribution: referrer agent id")
|
|
148
|
+
.option("--manifest-id <mft_id>", "Pre-registered task manifest id")
|
|
149
|
+
.option(
|
|
150
|
+
"--manifest <json>",
|
|
151
|
+
'Inline task manifest JSON (literal, @file, or "-" for stdin). Minimum: {"intention":"..."}',
|
|
152
|
+
)
|
|
153
|
+
.option(
|
|
154
|
+
"--intention <text>",
|
|
155
|
+
"Shortcut: builds a minimal manifest {intention: <text>} if --manifest/--manifest-id are not set.",
|
|
156
|
+
)
|
|
157
|
+
.option("--human", "Human-readable output")
|
|
158
|
+
.action(async (agentId: string, opts) => {
|
|
159
|
+
await runCommand(async () => {
|
|
160
|
+
const { consumer } = buildContext();
|
|
161
|
+
const inputJson = opts.input === "-" ? await readStdin() : opts.input;
|
|
162
|
+
const input = parseJsonOrFail(inputJson, "--input");
|
|
163
|
+
const manifest = await resolveManifestOpt(
|
|
164
|
+
opts.manifest,
|
|
165
|
+
opts.manifestId,
|
|
166
|
+
opts.intention,
|
|
167
|
+
`invoke: ${opts.capability}`,
|
|
168
|
+
);
|
|
169
|
+
const body: InvokeRequest = { capability: opts.capability, input };
|
|
170
|
+
if (opts.maxCredits !== undefined) body.max_credits = opts.maxCredits;
|
|
171
|
+
if (opts.timeout !== undefined) body.timeout_seconds = opts.timeout;
|
|
172
|
+
if (opts.referredBy !== undefined) body.referred_by = opts.referredBy;
|
|
173
|
+
if (opts.manifestId !== undefined) body.manifest_id = opts.manifestId;
|
|
174
|
+
if (manifest !== undefined) body.manifest = manifest;
|
|
175
|
+
return invokeAgent(consumer, agentId, body);
|
|
176
|
+
}, { human: opts.human });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const gigTask = program.command("gig-task").description("Gig Task commands");
|
|
180
|
+
|
|
181
|
+
gigTask
|
|
182
|
+
.command("create <manifest>")
|
|
183
|
+
.description(
|
|
184
|
+
'Create a gig task from a YAML/JSON manifest file. Use "-" for stdin. ' +
|
|
185
|
+
'Required fields: capability, instruction, target_providers, credits_per_provider.',
|
|
186
|
+
)
|
|
187
|
+
.option("--human", "Human-readable output")
|
|
188
|
+
.action(async (manifestPath: string, opts) => {
|
|
189
|
+
await runCommand(async () => {
|
|
190
|
+
const { consumer } = buildContext();
|
|
191
|
+
const raw = manifestPath === "-" ? await readStdin() : readFileSync(manifestPath, "utf8");
|
|
192
|
+
const body = parseYamlOrJson(raw) as CreateGigTaskRequest;
|
|
193
|
+
return consumer.createGigTask(body as unknown as Record<string, unknown>);
|
|
194
|
+
}, { human: opts.human });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
gigTask
|
|
198
|
+
.command("get <bct_id>")
|
|
199
|
+
.description("Get a gig task by id")
|
|
200
|
+
.option("--human", "Human-readable output")
|
|
201
|
+
.action(async (taskId: string, opts) => {
|
|
202
|
+
await runCommand(async () => {
|
|
203
|
+
const { consumer } = buildContext();
|
|
204
|
+
return consumer.getGigTask(taskId);
|
|
205
|
+
}, { human: opts.human });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
gigTask
|
|
209
|
+
.command("list")
|
|
210
|
+
.description("List gig tasks I own")
|
|
211
|
+
.option("--status <s>", "Filter by status")
|
|
212
|
+
.option("--human", "Human-readable output")
|
|
213
|
+
.action(async (opts) => {
|
|
214
|
+
await runCommand(async () => {
|
|
215
|
+
const { consumer } = buildContext();
|
|
216
|
+
return consumer.listGigTasks({
|
|
217
|
+
...(opts.status ? { status: opts.status } : {}),
|
|
218
|
+
});
|
|
219
|
+
}, { human: opts.human });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
gigTask
|
|
223
|
+
.command("available")
|
|
224
|
+
.description("List open gig tasks I could pick up (as provider)")
|
|
225
|
+
.option("--human", "Human-readable output")
|
|
226
|
+
.action(async (opts) => {
|
|
227
|
+
await runCommand(async () => {
|
|
228
|
+
const { providerClient } = buildContext();
|
|
229
|
+
return providerClient.listGigTasksAvailable();
|
|
230
|
+
}, { human: opts.human });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
gigTask
|
|
234
|
+
.command("accept <bct_id>")
|
|
235
|
+
.description("Accept a gig task (provider side) — returns a result_id")
|
|
236
|
+
.requiredOption("--agent-id <agt_id>", "Which of your agents is accepting")
|
|
237
|
+
.option("--slot-key <key>", "Slot key for sliced gig tasks")
|
|
238
|
+
.option("--human", "Human-readable output")
|
|
239
|
+
.action(async (taskId: string, opts) => {
|
|
240
|
+
await runCommand(async () => {
|
|
241
|
+
const { providerClient } = buildContext();
|
|
242
|
+
const body: AcceptGigTaskRequest = { agent_id: opts.agentId };
|
|
243
|
+
if (opts.slotKey !== undefined) body.slot_key = opts.slotKey;
|
|
244
|
+
return providerClient.acceptGigTask(taskId, body as unknown as Record<string, unknown>);
|
|
245
|
+
}, { human: opts.human });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
gigTask
|
|
249
|
+
.command("submit <bct_id>")
|
|
250
|
+
.description(
|
|
251
|
+
"Submit gig task result (provider side). Body must include `result_data` (string) " +
|
|
252
|
+
"and may include `result_payload` (object) and `proof` (array).",
|
|
253
|
+
)
|
|
254
|
+
.requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)')
|
|
255
|
+
.option("--human", "Human-readable output")
|
|
256
|
+
.action(async (taskId: string, opts) => {
|
|
257
|
+
await runCommand(async () => {
|
|
258
|
+
const { providerClient } = buildContext();
|
|
259
|
+
const raw = opts.body === "-" ? await readStdin() : opts.body;
|
|
260
|
+
const body = parseJsonOrFail(raw, "--body") as unknown as GigTaskSubmitRequest;
|
|
261
|
+
if (typeof body !== "object" || body === null || typeof (body as { result_data?: unknown }).result_data !== "string") {
|
|
262
|
+
throw new Error("--body must include a `result_data` string field");
|
|
263
|
+
}
|
|
264
|
+
return providerClient.submitGigTask(taskId, body as unknown as Record<string, unknown>);
|
|
265
|
+
}, { human: opts.human });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
program
|
|
269
|
+
.command("receipt <rct_id>")
|
|
270
|
+
.description("Fetch a single receipt")
|
|
271
|
+
.option("--human", "Human-readable output")
|
|
272
|
+
.action(async (receiptId: string, opts) => {
|
|
273
|
+
await runCommand(async () => {
|
|
274
|
+
const { consumer } = buildContext();
|
|
275
|
+
return consumer.getReceipt(receiptId);
|
|
276
|
+
}, { human: opts.human });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
program
|
|
280
|
+
.command("trust <agent_id>")
|
|
281
|
+
.description("Get trust summary for an agent (per-capability)")
|
|
282
|
+
.option("--human", "Human-readable output")
|
|
283
|
+
.action(async (agentId: string, opts) => {
|
|
284
|
+
await runCommand(async () => {
|
|
285
|
+
const { consumer } = buildContext();
|
|
286
|
+
return consumer.getTrustScore(agentId);
|
|
287
|
+
}, { human: opts.human });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
program
|
|
291
|
+
.command("credits")
|
|
292
|
+
.description("Current credit balance")
|
|
293
|
+
.option("--human", "Human-readable output")
|
|
294
|
+
.action(async (opts) => {
|
|
295
|
+
await runCommand(async () => {
|
|
296
|
+
const { consumer } = buildContext();
|
|
297
|
+
return consumer.getBalance();
|
|
298
|
+
}, { human: opts.human });
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
program
|
|
302
|
+
.command("show <agent_id>")
|
|
303
|
+
.description("Show full agent listing including capabilities_meta")
|
|
304
|
+
.option("--capability <name>", "Print only this capability's meta entry")
|
|
305
|
+
.option("--human", "Human-readable output")
|
|
306
|
+
.action(async (agentId: string, opts) => {
|
|
307
|
+
await runCommand(async () => {
|
|
308
|
+
const { consumer } = buildContext();
|
|
309
|
+
const agent = await consumer.getAgent(agentId);
|
|
310
|
+
if (opts.capability) {
|
|
311
|
+
const meta = agent.capabilities_meta?.[opts.capability];
|
|
312
|
+
if (!meta) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`agent ${agentId} has no capabilities_meta entry for ${JSON.stringify(opts.capability)}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return meta;
|
|
318
|
+
}
|
|
319
|
+
return agent;
|
|
320
|
+
}, { human: opts.human });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
program
|
|
324
|
+
.command("schema <agent_id>")
|
|
325
|
+
.description("Fetch + sha256-verify a capability's input JSON Schema")
|
|
326
|
+
.requiredOption("--capability <name>", "Capability name")
|
|
327
|
+
.option("--human", "Human-readable output")
|
|
328
|
+
.action(async (agentId: string, opts) => {
|
|
329
|
+
await runCommand(async () => {
|
|
330
|
+
const { consumer } = buildContext();
|
|
331
|
+
const agent = await consumer.getAgent(agentId);
|
|
332
|
+
try {
|
|
333
|
+
return await fetchCapabilitySchema(agent, opts.capability);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
if (err instanceof CapabilitySchemaError) {
|
|
336
|
+
throw new Error(`schema fetch failed: ${err.message}`);
|
|
337
|
+
}
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
}, { human: opts.human });
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function runHireRepl(
|
|
345
|
+
sessionId: string,
|
|
346
|
+
ctx: CliContext,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
const readline = await import("node:readline/promises");
|
|
349
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
350
|
+
process.stderr.write(
|
|
351
|
+
`interactive: session ${sessionId}\n` +
|
|
352
|
+
` > <text> send message\n` +
|
|
353
|
+
` .status poll session events count\n` +
|
|
354
|
+
` .end end the session and exit\n` +
|
|
355
|
+
` .quit exit without ending\n`,
|
|
356
|
+
);
|
|
357
|
+
// seq=1 is reserved for --message; REPL starts at 2. If --message wasn't
|
|
358
|
+
// used, seq=2 is still legal (monotonic ≥1, server accepts any start value).
|
|
359
|
+
let seq = 2;
|
|
360
|
+
try {
|
|
361
|
+
while (true) {
|
|
362
|
+
const line = (await rl.question("> ")).trim();
|
|
363
|
+
if (!line) continue;
|
|
364
|
+
if (line === ".quit") return;
|
|
365
|
+
if (line === ".end") {
|
|
366
|
+
const out = await endSession(ctx.consumer, sessionId, {});
|
|
367
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (line === ".status") {
|
|
371
|
+
const events = await ctx.consumer.getSessionEvents(sessionId, { offset: 0 });
|
|
372
|
+
const count = Array.isArray((events as { events?: unknown[] }).events)
|
|
373
|
+
? (events as { events: unknown[] }).events.length
|
|
374
|
+
: 0;
|
|
375
|
+
process.stderr.write(`events: ${count}\n`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const out = await ctx.requesterFlows.send(sessionId, line, seq++);
|
|
379
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
380
|
+
}
|
|
381
|
+
} finally {
|
|
382
|
+
rl.close();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function endSession(
|
|
387
|
+
consumer: ConsumerClient,
|
|
388
|
+
sessionId: string,
|
|
389
|
+
body: EndSessionRequest,
|
|
390
|
+
): Promise<Record<string, unknown>> {
|
|
391
|
+
return consumer.endSession(sessionId, {
|
|
392
|
+
finalOutput: body.final_output ?? undefined,
|
|
393
|
+
messageCount: body.message_count ?? undefined,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function invokeAgent(
|
|
398
|
+
consumer: ConsumerClient,
|
|
399
|
+
agentId: string,
|
|
400
|
+
opts: InvokeRequest,
|
|
401
|
+
): Promise<unknown> {
|
|
402
|
+
return consumer.invoke(agentId, {
|
|
403
|
+
capability: opts.capability,
|
|
404
|
+
input: opts.input,
|
|
405
|
+
maxCredits: opts.max_credits ?? undefined,
|
|
406
|
+
manifestId: opts.manifest_id ?? undefined,
|
|
407
|
+
manifest: opts.manifest as Record<string, unknown> | undefined,
|
|
408
|
+
timeoutSeconds: opts.timeout_seconds ?? undefined,
|
|
409
|
+
referredBy: opts.referred_by ?? undefined,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function formatEventsHuman(events: Record<string, unknown>[]): string {
|
|
414
|
+
if (events.length === 0) return "(no events)";
|
|
415
|
+
return events
|
|
416
|
+
.map((ev) => {
|
|
417
|
+
const seq = ev.seq ?? ev.offset ?? "?";
|
|
418
|
+
const type = ev.event_type ?? ev.type ?? "event";
|
|
419
|
+
const from = ev.sender_agent_id ?? ev.from ?? "-";
|
|
420
|
+
const payload = ev.payload ?? ev.data ?? {};
|
|
421
|
+
return `[${seq}] ${type} from=${from} ${JSON.stringify(payload)}`;
|
|
422
|
+
})
|
|
423
|
+
.join("\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function parseJsonOrFail(raw: string, label: string): Record<string, unknown> {
|
|
427
|
+
try {
|
|
428
|
+
const parsed = JSON.parse(raw);
|
|
429
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
430
|
+
throw new Error(`${label} must be a JSON object`);
|
|
431
|
+
}
|
|
432
|
+
return parsed as Record<string, unknown>;
|
|
433
|
+
} catch (err) {
|
|
434
|
+
throw new Error(`invalid JSON in ${label}: ${(err as Error).message}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function parseYamlOrJson(raw: string): unknown {
|
|
439
|
+
const trimmed = raw.trimStart();
|
|
440
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return JSON.parse(raw);
|
|
441
|
+
return yamlLoad(raw);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Resolve the manifest for invoke. Server requires exactly one of manifest_id
|
|
446
|
+
* or manifest; the CLI auto-constructs a minimal `{intention}` manifest when
|
|
447
|
+
* neither is provided so "just invoke" works out of the box.
|
|
448
|
+
*/
|
|
449
|
+
async function resolveManifestOpt(
|
|
450
|
+
manifestOpt: string | undefined,
|
|
451
|
+
manifestId: string | undefined,
|
|
452
|
+
intention: string | undefined,
|
|
453
|
+
defaultIntention: string,
|
|
454
|
+
): Promise<TaskManifest | undefined> {
|
|
455
|
+
if (manifestOpt !== undefined && manifestId !== undefined) {
|
|
456
|
+
throw new Error("--manifest and --manifest-id are mutually exclusive");
|
|
457
|
+
}
|
|
458
|
+
if (manifestId !== undefined) {
|
|
459
|
+
return undefined; // body.manifest_id set separately by caller
|
|
460
|
+
}
|
|
461
|
+
if (manifestOpt !== undefined) {
|
|
462
|
+
let raw: string;
|
|
463
|
+
if (manifestOpt === "-") {
|
|
464
|
+
raw = await readStdin();
|
|
465
|
+
} else if (manifestOpt.startsWith("@")) {
|
|
466
|
+
raw = readFileSync(manifestOpt.slice(1), "utf8");
|
|
467
|
+
} else {
|
|
468
|
+
raw = manifestOpt;
|
|
469
|
+
}
|
|
470
|
+
const parsed = parseJsonOrFail(raw, "--manifest");
|
|
471
|
+
if (typeof parsed.intention !== "string" || parsed.intention.length === 0) {
|
|
472
|
+
throw new Error('--manifest must include a non-empty "intention" field');
|
|
473
|
+
}
|
|
474
|
+
return parsed as unknown as TaskManifest;
|
|
475
|
+
}
|
|
476
|
+
// Neither --manifest nor --manifest-id supplied — auto-construct the
|
|
477
|
+
// minimum manifest the server requires.
|
|
478
|
+
return { intention: intention ?? defaultIntention };
|
|
479
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { load as yamlLoad, dump as yamlDump } from "js-yaml";
|
|
5
|
+
import type { PricingModel } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CLOUD_URL = "https://api.linkedclaw.com";
|
|
8
|
+
export const DEFAULT_RELAY_URL = "wss://api.linkedclaw.com/ws";
|
|
9
|
+
export const DEFAULT_SERVICES_HOST_URL = "https://api.linkedclaw.com";
|
|
10
|
+
|
|
11
|
+
export interface SanitizeConfig {
|
|
12
|
+
scope?: { stripPatterns?: string[] };
|
|
13
|
+
output?: { stripPatterns?: string[] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderConfig {
|
|
17
|
+
cloudUrl: string;
|
|
18
|
+
relayUrl: string;
|
|
19
|
+
servicesHostUrl?: string;
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
agentId?: string;
|
|
22
|
+
agentName?: string;
|
|
23
|
+
slug?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
capabilities?: string[];
|
|
26
|
+
pricingModel?: PricingModel;
|
|
27
|
+
priceCredits?: number;
|
|
28
|
+
invokeTimeoutMs?: number;
|
|
29
|
+
sessionTurnTimeoutMs?: number;
|
|
30
|
+
gigTaskTimeoutMs?: number;
|
|
31
|
+
maxConcurrentRuns?: number;
|
|
32
|
+
perRequesterLimit?: number;
|
|
33
|
+
sanitize?: SanitizeConfig;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CliConfig extends ProviderConfig {}
|
|
37
|
+
|
|
38
|
+
export function configDir(): string {
|
|
39
|
+
return process.env["LINKEDCLAW_CONFIG_DIR"] ?? join(homedir(), ".linkedclaw");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function configPath(): string {
|
|
43
|
+
return join(configDir(), "config.yaml");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readFileConfig(path: string = configPath()): Partial<CliConfig> {
|
|
47
|
+
if (!existsSync(path)) return {};
|
|
48
|
+
const raw = readFileSync(path, "utf8");
|
|
49
|
+
const parsed = yamlLoad(raw);
|
|
50
|
+
if (parsed === null || parsed === undefined) return {};
|
|
51
|
+
if (typeof parsed !== "object") {
|
|
52
|
+
throw new Error(`config file ${path} is not a YAML object`);
|
|
53
|
+
}
|
|
54
|
+
return parsed as Partial<CliConfig>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function writeFileConfig(cfg: Partial<CliConfig>, path: string = configPath()): void {
|
|
58
|
+
const dir = dirname(path);
|
|
59
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
60
|
+
writeFileSync(path, yamlDump(cfg), { mode: 0o600 });
|
|
61
|
+
if (process.platform !== "win32") chmodSync(path, 0o600);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveConfig(overrides: Partial<ProviderConfig> = {}): ProviderConfig {
|
|
65
|
+
const env = process.env;
|
|
66
|
+
const file = readFileConfig();
|
|
67
|
+
const cloudUrl =
|
|
68
|
+
overrides.cloudUrl ?? env["LINKEDCLAW_CLOUD_URL"] ?? file.cloudUrl ?? DEFAULT_CLOUD_URL;
|
|
69
|
+
const relayUrl =
|
|
70
|
+
overrides.relayUrl ?? env["LINKEDCLAW_RELAY_URL"] ?? file.relayUrl ?? DEFAULT_RELAY_URL;
|
|
71
|
+
const servicesHostUrl =
|
|
72
|
+
overrides.servicesHostUrl ??
|
|
73
|
+
env["LINKEDCLAW_SERVICES_HOST_URL"] ??
|
|
74
|
+
file.servicesHostUrl ??
|
|
75
|
+
cloudUrl;
|
|
76
|
+
const apiKey = overrides.apiKey ?? env["LINKEDCLAW_API_KEY"] ?? file.apiKey;
|
|
77
|
+
return {
|
|
78
|
+
...file,
|
|
79
|
+
...overrides,
|
|
80
|
+
cloudUrl,
|
|
81
|
+
relayUrl,
|
|
82
|
+
servicesHostUrl,
|
|
83
|
+
...(apiKey !== undefined ? { apiKey } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ConsumerClient } from "@linkedclaw/consumer";
|
|
2
|
+
import { ProviderClient } from "@linkedclaw/provider";
|
|
3
|
+
import { RequesterFlows } from "@linkedclaw/consumer-runtime";
|
|
4
|
+
import { resolveConfig, type ProviderConfig } from "./config.js";
|
|
5
|
+
import { ConfigError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
export interface CliContext {
|
|
8
|
+
cfg: ProviderConfig;
|
|
9
|
+
consumer: ConsumerClient;
|
|
10
|
+
providerClient: ProviderClient;
|
|
11
|
+
requesterFlows: RequesterFlows;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildContext(overrides: Partial<ProviderConfig> = {}): CliContext {
|
|
15
|
+
const cfg = resolveConfig(overrides);
|
|
16
|
+
if (!cfg.apiKey) {
|
|
17
|
+
throw new ConfigError(
|
|
18
|
+
"missing apiKey — run `linkedclaw login`, set LINKEDCLAW_API_KEY, or edit ~/.linkedclaw/config.yaml",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const consumer = new ConsumerClient(cfg.cloudUrl, cfg.apiKey);
|
|
23
|
+
const providerClient = new ProviderClient(cfg.cloudUrl, cfg.apiKey);
|
|
24
|
+
const requesterFlows = new RequesterFlows(consumer);
|
|
25
|
+
|
|
26
|
+
return { cfg, consumer, providerClient, requesterFlows };
|
|
27
|
+
}
|