@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.
@@ -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
+ }