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