@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.
Files changed (47) hide show
  1. package/README.md +248 -48
  2. package/dist/bin.js +8099 -4778
  3. package/dist/bin.js.map +1 -1
  4. package/package.json +17 -32
  5. package/src/arena/api.ts +154 -0
  6. package/src/arena/hash.ts +15 -0
  7. package/src/arena/types.ts +106 -0
  8. package/src/bin.ts +33 -0
  9. package/src/commands/agent.ts +264 -0
  10. package/src/commands/arena.ts +393 -0
  11. package/src/commands/auth.ts +116 -0
  12. package/src/commands/converge.ts +969 -0
  13. package/src/commands/provider.ts +245 -0
  14. package/src/commands/requester.ts +479 -0
  15. package/src/config.ts +85 -0
  16. package/src/context.ts +27 -0
  17. package/src/converge/api.ts +213 -0
  18. package/src/converge/hash.ts +35 -0
  19. package/src/converge/lock.ts +30 -0
  20. package/src/converge/staging.ts +83 -0
  21. package/src/converge/types.ts +91 -0
  22. package/src/converge/workspace.ts +92 -0
  23. package/src/errors.ts +41 -0
  24. package/src/handlers/subprocess.ts +185 -0
  25. package/src/output.ts +57 -0
  26. package/src/types.ts +90 -0
  27. package/test/agent-help.test.ts +207 -0
  28. package/test/arena-api.test.ts +211 -0
  29. package/test/arena-commands.test.ts +559 -0
  30. package/test/arena-hash.test.ts +33 -0
  31. package/test/cli-help.test.ts +82 -0
  32. package/test/converge-accept.test.ts +206 -0
  33. package/test/converge-decision.test.ts +274 -0
  34. package/test/converge-hash.test.ts +58 -0
  35. package/test/converge-help.test.ts +58 -0
  36. package/test/converge-lock.test.ts +48 -0
  37. package/test/converge-review.test.ts +135 -0
  38. package/test/converge-run.test.ts +286 -0
  39. package/test/converge-staging.test.ts +161 -0
  40. package/test/converge-status.test.ts +141 -0
  41. package/test/converge-workspace.test.ts +92 -0
  42. package/test/hire-flags.test.ts +55 -0
  43. package/test/recv-flags.test.ts +83 -0
  44. package/test/register-browser.test.ts +55 -0
  45. package/tsconfig.json +14 -0
  46. package/tsup.config.ts +25 -0
  47. 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
+ }