@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,245 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { load as yamlLoad } from "js-yaml";
|
|
4
|
+
import {
|
|
5
|
+
ProviderRuntime,
|
|
6
|
+
RelayClient,
|
|
7
|
+
type ProviderHandler,
|
|
8
|
+
} from "@linkedclaw/provider-runtime";
|
|
9
|
+
import type { CreateAgentRequest, UpdateAgentRequest, GigTaskSubmitRequest } from "../types.js";
|
|
10
|
+
import type { ProviderConfig } from "../config.js";
|
|
11
|
+
import { buildContext } from "../context.js";
|
|
12
|
+
import { SubprocessHandler } from "../handlers/subprocess.js";
|
|
13
|
+
import { readStdin, runCommand, printError } from "../output.js";
|
|
14
|
+
|
|
15
|
+
export function registerProviderCommands(program: Command): void {
|
|
16
|
+
const provider = program.command("provider").description("Provider-side commands");
|
|
17
|
+
|
|
18
|
+
provider
|
|
19
|
+
.command("register <config>")
|
|
20
|
+
.description("Register (create/update) an agent listing from a provider YAML file. Use \"-\" for stdin.")
|
|
21
|
+
.option("--human", "Human-readable output")
|
|
22
|
+
.action(async (path: string, opts) => {
|
|
23
|
+
await runCommand(async () => {
|
|
24
|
+
const cfg = await loadProviderYaml(path);
|
|
25
|
+
const { providerClient } = buildContext();
|
|
26
|
+
const body = buildCreateAgentRequest(cfg);
|
|
27
|
+
if (cfg.agentId) {
|
|
28
|
+
return providerClient.updateAgent(cfg.agentId, body as unknown as Record<string, unknown>);
|
|
29
|
+
}
|
|
30
|
+
return providerClient.createAgent({
|
|
31
|
+
slug: body.slug,
|
|
32
|
+
name: body.name,
|
|
33
|
+
description: body.description ?? "",
|
|
34
|
+
capabilities: body.capabilities,
|
|
35
|
+
});
|
|
36
|
+
}, { human: opts.human });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
provider
|
|
40
|
+
.command("update <listing_id>")
|
|
41
|
+
.description("Patch an existing agent listing. Body = JSON file or stdin.")
|
|
42
|
+
.requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)')
|
|
43
|
+
.option("--human", "Human-readable output")
|
|
44
|
+
.action(async (listingId: string, opts) => {
|
|
45
|
+
await runCommand(async () => {
|
|
46
|
+
const raw = opts.body === "-" ? await readStdin() : readFileSync(opts.body, "utf8");
|
|
47
|
+
const body = JSON.parse(raw) as UpdateAgentRequest;
|
|
48
|
+
const { providerClient } = buildContext();
|
|
49
|
+
return providerClient.updateAgent(listingId, body as unknown as Record<string, unknown>);
|
|
50
|
+
}, { human: opts.human });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
provider
|
|
54
|
+
.command("listings")
|
|
55
|
+
.description("Show my own agent listings (as provider)")
|
|
56
|
+
.option("--human", "Human-readable output")
|
|
57
|
+
.action(async (opts) => {
|
|
58
|
+
await runCommand(async () => {
|
|
59
|
+
const { consumer } = buildContext();
|
|
60
|
+
return consumer.discover({ owner: "me" });
|
|
61
|
+
}, { human: opts.human });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
provider
|
|
65
|
+
.command("run <config>")
|
|
66
|
+
.description("Run a provider daemon: connect WS, dispatch events to handler subprocess")
|
|
67
|
+
.option("--handler-cmd <cmd>", "Shell command to spawn as the handler child process")
|
|
68
|
+
.option("--handler-http <url>", "HTTP webhook URL to POST each event to (alternative to --handler-cmd)")
|
|
69
|
+
.option("--human", "Human-readable status output")
|
|
70
|
+
.action(async (path: string, opts) => {
|
|
71
|
+
try {
|
|
72
|
+
const yamlCfg = await loadProviderYaml(path);
|
|
73
|
+
if (!yamlCfg.agentId) {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
JSON.stringify({ error: "provider_unconfigured", message: "agentId missing — run `linkedclaw provider register` first or set it in YAML" }) + "\n",
|
|
76
|
+
);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
if (!opts.handlerCmd && !opts.handlerHttp) {
|
|
80
|
+
process.stderr.write(
|
|
81
|
+
JSON.stringify({ error: "config_error", message: "either --handler-cmd or --handler-http is required" }) + "\n",
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (opts.handlerCmd && opts.handlerHttp) {
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
JSON.stringify({ error: "config_error", message: "--handler-cmd and --handler-http are mutually exclusive" }) + "\n",
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { providerClient, cfg: merged } = buildContext({ ...yamlCfg });
|
|
93
|
+
const relay = new RelayClient({
|
|
94
|
+
url: merged.relayUrl,
|
|
95
|
+
apiKey: merged.apiKey!,
|
|
96
|
+
agentId: merged.agentId!,
|
|
97
|
+
});
|
|
98
|
+
const handler = opts.handlerCmd
|
|
99
|
+
? new SubprocessHandler({ cmd: opts.handlerCmd as string })
|
|
100
|
+
: makeHttpHandler(opts.handlerHttp as string);
|
|
101
|
+
|
|
102
|
+
const runtime = new ProviderRuntime({
|
|
103
|
+
cloud: {
|
|
104
|
+
gigTasks: {
|
|
105
|
+
accept: (taskId, body) => providerClient.acceptGigTask(taskId, body as unknown as Record<string, unknown>),
|
|
106
|
+
submit: (taskId, body) => providerClient.submitGigTask(taskId, body as unknown as Record<string, unknown>),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
relay,
|
|
110
|
+
handler,
|
|
111
|
+
config: merged,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let stopping = false;
|
|
115
|
+
const shutdown = async (sig: string) => {
|
|
116
|
+
if (stopping) return;
|
|
117
|
+
stopping = true;
|
|
118
|
+
process.stderr.write(`\n[provider] received ${sig}, stopping...\n`);
|
|
119
|
+
await runtime.stop();
|
|
120
|
+
if (handler instanceof SubprocessHandler) await handler.close();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
};
|
|
123
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
124
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
125
|
+
|
|
126
|
+
relay.on("connected", () => {
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
JSON.stringify({ event: "connected", agent_id: merged.agentId }) + "\n",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
relay.on("disconnected", (reason) => {
|
|
132
|
+
process.stderr.write(JSON.stringify({ event: "disconnected", reason }) + "\n");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await runtime.run();
|
|
136
|
+
process.stderr.write(JSON.stringify({ event: "provider_running", agent_id: merged.agentId }) + "\n");
|
|
137
|
+
// Keep the process alive; runtime.run() returns after initial connect.
|
|
138
|
+
await new Promise<void>(() => {});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
printError(err);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
provider
|
|
146
|
+
.command("pick <bct_id>")
|
|
147
|
+
.description("Manually accept a gig task (provider side)")
|
|
148
|
+
.requiredOption("--agent-id <agt_id>", "Which of your agents is accepting")
|
|
149
|
+
.option("--slot-key <key>", "Slot key for sliced gig tasks")
|
|
150
|
+
.option("--human", "Human-readable output")
|
|
151
|
+
.action(async (taskId: string, opts) => {
|
|
152
|
+
await runCommand(async () => {
|
|
153
|
+
const { providerClient } = buildContext();
|
|
154
|
+
const body: { agent_id: string; slot_key?: string } = { agent_id: opts.agentId };
|
|
155
|
+
if (opts.slotKey !== undefined) body.slot_key = opts.slotKey;
|
|
156
|
+
return providerClient.acceptGigTask(taskId, body as unknown as Record<string, unknown>);
|
|
157
|
+
}, { human: opts.human });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
provider
|
|
161
|
+
.command("submit <bct_id> <result_file>")
|
|
162
|
+
.description('Submit a gig task result. result_file = JSON path or "-" for stdin.')
|
|
163
|
+
.option("--human", "Human-readable output")
|
|
164
|
+
.action(async (taskId: string, resultFile: string, opts) => {
|
|
165
|
+
await runCommand(async () => {
|
|
166
|
+
const raw = resultFile === "-" ? await readStdin() : readFileSync(resultFile, "utf8");
|
|
167
|
+
const body = JSON.parse(raw) as GigTaskSubmitRequest;
|
|
168
|
+
const { providerClient } = buildContext();
|
|
169
|
+
return providerClient.submitGigTask(taskId, body as unknown as Record<string, unknown>);
|
|
170
|
+
}, { human: opts.human });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ───── helpers ─────
|
|
175
|
+
|
|
176
|
+
async function loadProviderYaml(path: string): Promise<Partial<ProviderConfig>> {
|
|
177
|
+
const raw = path === "-" ? await readStdin() : readFileSync(path, "utf8");
|
|
178
|
+
const parsed = yamlLoad(raw);
|
|
179
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
180
|
+
throw new Error(`provider config ${path} is not a YAML object`);
|
|
181
|
+
}
|
|
182
|
+
return parsed as Partial<ProviderConfig>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildCreateAgentRequest(
|
|
186
|
+
cfg: Partial<ProviderConfig>,
|
|
187
|
+
): CreateAgentRequest {
|
|
188
|
+
if (!cfg.slug) throw new Error("provider config missing `slug`");
|
|
189
|
+
if (!cfg.agentName) throw new Error("provider config missing `agentName`");
|
|
190
|
+
if (!cfg.capabilities || cfg.capabilities.length === 0) {
|
|
191
|
+
throw new Error("provider config missing `capabilities`");
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
slug: cfg.slug,
|
|
195
|
+
name: cfg.agentName,
|
|
196
|
+
description: cfg.description ?? "",
|
|
197
|
+
capabilities: cfg.capabilities,
|
|
198
|
+
...(cfg.pricingModel !== undefined ? { pricing_model: cfg.pricingModel as CreateAgentRequest["pricing_model"] } : {}),
|
|
199
|
+
...(cfg.priceCredits !== undefined ? { price_credits: cfg.priceCredits } : {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function makeHttpHandler(url: string): ProviderHandler {
|
|
204
|
+
return {
|
|
205
|
+
async onSessionCreate(evt) {
|
|
206
|
+
return (await postEvent(url, evt)) as { accept: boolean; reason?: string };
|
|
207
|
+
},
|
|
208
|
+
async onSessionMessage(evt) {
|
|
209
|
+
const r = await postEvent(url, evt);
|
|
210
|
+
if (r === null || r === undefined) return null;
|
|
211
|
+
if (typeof r === "string") return r;
|
|
212
|
+
if (typeof r === "object") {
|
|
213
|
+
const obj = r as Record<string, unknown>;
|
|
214
|
+
if (typeof obj["content"] === "string") return obj["content"] as string;
|
|
215
|
+
if ("payload" in obj) return obj as { payload: unknown };
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
},
|
|
219
|
+
async onSessionEnd(evt) {
|
|
220
|
+
await postEvent(url, evt);
|
|
221
|
+
},
|
|
222
|
+
async onInvoke(evt) {
|
|
223
|
+
return (await postEvent(url, evt)) as {
|
|
224
|
+
output?: Record<string, unknown>;
|
|
225
|
+
error?: { code: string; message: string };
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
async onGigTaskOffer(evt) {
|
|
229
|
+
return (await postEvent(url, evt)) as { accept: boolean; slot_key?: string };
|
|
230
|
+
},
|
|
231
|
+
async onGigTaskExecute(evt) {
|
|
232
|
+
return (await postEvent(url, evt)) as { result_payload?: Record<string, unknown> };
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function postEvent(url: string, body: object): Promise<unknown> {
|
|
238
|
+
const res = await fetch(url, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { "Content-Type": "application/json" },
|
|
241
|
+
body: JSON.stringify(body),
|
|
242
|
+
});
|
|
243
|
+
if (!res.ok) throw new Error(`handler HTTP ${res.status}`);
|
|
244
|
+
return res.json();
|
|
245
|
+
}
|