kimi-agent-swarm-cli 0.7.0

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/src/cli.ts ADDED
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env bun
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ import { runBenchmark } from "./benchmark";
6
+ import { loadConfig } from "./config";
7
+ import { exportRun, supportedExportFormats } from "./export";
8
+ import { getInitInstructions, runInit } from "./init";
9
+ import { MemoryQueueAdapter } from "./distributed/memory-adapter";
10
+ import { RedisQueueAdapter } from "./distributed/redis-adapter";
11
+ import { workerLoop } from "./distributed/runner";
12
+ import {
13
+ clearLeaderboard,
14
+ compareRuns,
15
+ generateHtmlReport,
16
+ getLeaderboard,
17
+ } from "./leaderboard";
18
+ import { runWideSearch } from "./runtime";
19
+ import { verifyRun } from "./verifier";
20
+ import type {
21
+ BudgetOptions,
22
+ DistributedRunOptions,
23
+ ExecutionProfile,
24
+ ExportFormat,
25
+ RunWideSearchResult,
26
+ UsageMetrics,
27
+ } from "./types";
28
+
29
+ function readFlag(args: string[], name: string, fallback: string | undefined = undefined): string | undefined {
30
+ const index = args.indexOf(name);
31
+ if (index === -1) return fallback;
32
+ return args[index + 1] ?? fallback;
33
+ }
34
+
35
+ function readNumberFlag(args: string[], name: string): number | undefined {
36
+ const raw = readFlag(args, name);
37
+ if (raw === undefined) return undefined;
38
+ const value = Number(raw);
39
+ if (Number.isNaN(value)) {
40
+ throw new Error(`Flag ${name} requires a numeric value`);
41
+ }
42
+ return value;
43
+ }
44
+
45
+ function readBooleanFlag(args: string[], name: string): boolean {
46
+ return args.includes(name);
47
+ }
48
+
49
+ async function inspectRun(runDir: string): Promise<{
50
+ runId: string;
51
+ objective: string;
52
+ executionProfile: ExecutionProfile;
53
+ status: string;
54
+ verificationStatus: string;
55
+ acceptedSources: number;
56
+ rejectedSources: number;
57
+ }> {
58
+ const run = JSON.parse(await readFile(join(runDir, "run.json"), "utf8")) as {
59
+ runId: string;
60
+ objective: string;
61
+ executionProfile: ExecutionProfile;
62
+ status: string;
63
+ };
64
+ const verification = JSON.parse(await readFile(join(runDir, "verification-report.json"), "utf8")) as {
65
+ status: string;
66
+ acceptedSources: number;
67
+ rejectedSources: number;
68
+ };
69
+ return {
70
+ runId: run.runId,
71
+ objective: run.objective,
72
+ executionProfile: run.executionProfile,
73
+ status: run.status,
74
+ verificationStatus: verification.status,
75
+ acceptedSources: verification.acceptedSources,
76
+ rejectedSources: verification.rejectedSources,
77
+ };
78
+ }
79
+
80
+ async function handleRun(args: string[]): Promise<void> {
81
+ const workDir = readFlag(args, "--work-dir", process.cwd());
82
+ const config = await loadConfig(workDir);
83
+
84
+ const objective = readFlag(args, "--objective") ?? args.join(" ");
85
+ const profile = (readFlag(args, "--profile", config.defaults.profile) ?? "fixture") as ExecutionProfile;
86
+ const providerCommand = readFlag(args, "--provider-command");
87
+ const providerArgsRaw = readFlag(args, "--provider-args", "");
88
+ const providerArgs = providerArgsRaw ? providerArgsRaw.split(" ").filter(Boolean) : [];
89
+ const providerName = readFlag(args, "--provider") ?? readFlag(args, "--provider-name", config.defaults.provider);
90
+ const searchDepth = (readFlag(args, "--depth", config.defaults.depth) ?? "standard") as
91
+ | "light"
92
+ | "standard"
93
+ | "deep"
94
+ | "maximum";
95
+
96
+ const replayRunId = readFlag(args, "--replay");
97
+ const useCache = readBooleanFlag(args, "--use-cache");
98
+ const distributedEnabled = readBooleanFlag(args, "--distributed");
99
+ const workers = readNumberFlag(args, "--workers");
100
+ const maxRetries = readNumberFlag(args, "--max-retries");
101
+ const queueType = readFlag(args, "--queue-type") as "memory" | "redis" | undefined;
102
+ const resumeJobId = readFlag(args, "--resume-job-id");
103
+
104
+ if (!objective && !replayRunId && !resumeJobId) {
105
+ throw new Error("run command requires --objective, a positional objective, --replay, or --resume-job-id");
106
+ }
107
+
108
+ const budget: BudgetOptions = {
109
+ maxCostUsd: readNumberFlag(args, "--max-cost-usd"),
110
+ maxProviderCalls: readNumberFlag(args, "--max-provider-calls"),
111
+ maxApiCalls: readNumberFlag(args, "--max-api-calls"),
112
+ dryRun: readBooleanFlag(args, "--dry-run"),
113
+ };
114
+
115
+ const distributed: DistributedRunOptions | undefined = distributedEnabled
116
+ ? {
117
+ enabled: true,
118
+ workers,
119
+ maxRetries,
120
+ queueType,
121
+ resumeJobId,
122
+ }
123
+ : undefined;
124
+
125
+ const result: RunWideSearchResult = await runWideSearch({
126
+ objective,
127
+ workDir,
128
+ profile,
129
+ providerCommand,
130
+ providerArgs,
131
+ providerName,
132
+ searchDepth,
133
+ budget,
134
+ useCache,
135
+ replayRunId,
136
+ distributed,
137
+ });
138
+ console.log(JSON.stringify(result, null, 2));
139
+ }
140
+
141
+ async function handleVerify(args: string[]): Promise<void> {
142
+ const runDir = readFlag(args, "--run-dir");
143
+ const result = await verifyRun({ runDir });
144
+ console.log(JSON.stringify(result, null, 2));
145
+ }
146
+
147
+ async function handleInspect(args: string[]): Promise<void> {
148
+ const runDir = readFlag(args, "--run-dir");
149
+ if (!runDir) {
150
+ throw new Error("inspect command requires --run-dir");
151
+ }
152
+ const result = await inspectRun(runDir);
153
+ console.log(JSON.stringify(result, null, 2));
154
+ }
155
+
156
+ async function handleExport(args: string[]): Promise<void> {
157
+ const runDir = readFlag(args, "--run-dir");
158
+ const format = readFlag(args, "--format") as ExportFormat | undefined;
159
+ const outPath = readFlag(args, "--out");
160
+
161
+ if (!runDir) {
162
+ throw new Error("export command requires --run-dir");
163
+ }
164
+ if (!format || !supportedExportFormats().includes(format)) {
165
+ throw new Error(`export command requires --format json|csv`);
166
+ }
167
+
168
+ const destination = await exportRun({ runDir, format, outPath });
169
+ console.log(JSON.stringify({ exportedTo: destination }, null, 2));
170
+ }
171
+
172
+ async function handleBenchmark(args: string[]): Promise<void> {
173
+ const profile = readFlag(args, "--profile");
174
+ const workDir = readFlag(args, "--work-dir", process.cwd());
175
+
176
+ if (!profile) {
177
+ throw new Error("benchmark command requires --profile");
178
+ }
179
+
180
+ // Golden answers are bundled per fixture for repeatable CI scoring.
181
+ const { goldenAnswers } = await import("../fixtures/golden-answers");
182
+ const golden = goldenAnswers[profile];
183
+ if (!golden) {
184
+ throw new Error(`No golden answer defined for profile: ${profile}`);
185
+ }
186
+
187
+ const result = await runBenchmark(profile, golden, workDir);
188
+ console.log(JSON.stringify(result, null, 2));
189
+ }
190
+
191
+ async function handleLeaderboard(args: string[]): Promise<void> {
192
+ const profile = readFlag(args, "--profile");
193
+ const compareRaw = readFlag(args, "--compare");
194
+ const html = readBooleanFlag(args, "--html");
195
+ const outPath = readFlag(args, "--out");
196
+ const shouldClear = readBooleanFlag(args, "--clear");
197
+
198
+ if (shouldClear) {
199
+ await clearLeaderboard();
200
+ console.log(JSON.stringify({ cleared: true }, null, 2));
201
+ return;
202
+ }
203
+
204
+ if (compareRaw) {
205
+ const runIds = compareRaw.split(",").map((id) => id.trim());
206
+ const comparison = await compareRuns(runIds);
207
+ console.log(JSON.stringify(comparison, null, 2));
208
+ return;
209
+ }
210
+
211
+ const entries = await getLeaderboard(profile);
212
+
213
+ if (html) {
214
+ const destination = outPath ?? "leaderboard-report.html";
215
+ await generateHtmlReport(entries, destination);
216
+ console.log(JSON.stringify({ report: destination }, null, 2));
217
+ return;
218
+ }
219
+
220
+ console.log(JSON.stringify(entries, null, 2));
221
+ }
222
+
223
+ async function handleInit(args: string[]): Promise<void> {
224
+ const nonInteractive = readBooleanFlag(args, "--non-interactive");
225
+ const local = readBooleanFlag(args, "--local");
226
+ const workDir = readFlag(args, "--work-dir", process.cwd());
227
+
228
+ const result = await runInit({
229
+ nonInteractive,
230
+ global: !local,
231
+ workDir,
232
+ });
233
+
234
+ console.log(JSON.stringify({ configPath: result.configPath, configured: result.wrote }, null, 2));
235
+ console.log(getInitInstructions());
236
+ }
237
+
238
+ async function handleWorker(args: string[]): Promise<void> {
239
+ const jobId = readFlag(args, "--job-id");
240
+ const workerId = readFlag(args, "--worker-id") ?? "cli-worker";
241
+ const workDir = readFlag(args, "--work-dir", process.cwd());
242
+
243
+ if (!jobId) {
244
+ throw new Error("worker command requires --job-id");
245
+ }
246
+
247
+ const memoryAdapter = new MemoryQueueAdapter({ workDir });
248
+ const job = await memoryAdapter.getJob(jobId);
249
+ if (!job) {
250
+ throw new Error(`Job not found: ${jobId}`);
251
+ }
252
+
253
+ const adapter = job.queueType === "redis" ? new RedisQueueAdapter() : memoryAdapter;
254
+ const metrics: UsageMetrics = { providerCalls: 0, apiCalls: 0 };
255
+
256
+ await workerLoop(
257
+ adapter,
258
+ jobId,
259
+ workerId,
260
+ job.executionProfile,
261
+ job.providerName,
262
+ job.searchDepth,
263
+ metrics,
264
+ );
265
+ console.log(JSON.stringify({ workerId, done: true, metrics }, null, 2));
266
+ }
267
+
268
+ function handleProviders(): void {
269
+ const providers = [
270
+ { name: "mock", env: "none", note: "deterministic demo/CI" },
271
+ { name: "serper", env: "SERPER_API_KEY", note: "Google Search via Serper.dev" },
272
+ { name: "tavily", env: "TAVILY_API_KEY", note: "AI-native search" },
273
+ { name: "brave", env: "BRAVE_API_KEY", note: "Brave Search API" },
274
+ { name: "github", env: "GITHUB_TOKEN", note: "GitHub repository search" },
275
+ ];
276
+ console.log(JSON.stringify(providers, null, 2));
277
+ }
278
+
279
+ function printUsage(): void {
280
+ console.error("Usage: kasw <research|run|verify|inspect|export|benchmark|leaderboard|providers|init|worker>");
281
+ console.error("");
282
+ console.error(" research|run <objective> [options]");
283
+ console.error(" --profile <profile> fixture | fixture-asset-mgmt | fixture-sellside-research |");
284
+ console.error(" fixture-youtube-niche | fixture-paul-graham-corpus |");
285
+ console.error(" fixture-github-repo-landscape | fixture-market-scan |");
286
+ console.error(" local-command | web-search");
287
+ console.error(" --provider|--provider-name mock (default) | serper | tavily | brave | github");
288
+ console.error(" --depth <depth> light | standard (default) | deep | maximum");
289
+ console.error(" --work-dir <dir> working directory (default: cwd)");
290
+ console.error(" --max-cost-usd <n> abort if estimated/actual cost exceeds budget");
291
+ console.error(" --max-provider-calls <n> abort if provider calls exceed budget");
292
+ console.error(" --max-api-calls <n> abort if API calls exceed budget");
293
+ console.error(" --dry-run print cost estimate without executing");
294
+ console.error(" --use-cache reuse cached provider responses when available");
295
+ console.error(" --replay <run-id> rerun a previous run with the same inputs");
296
+ console.error(" --distributed execute using distributed worker tasks");
297
+ console.error(" --workers <n> number of in-process workers (default: 4)");
298
+ console.error(" --max-retries <n> max retries per task (default: 3)");
299
+ console.error(" --queue-type <memory|redis> distributed queue backend (default: memory)");
300
+ console.error(" --resume-job-id <id> resume a previous distributed job");
301
+ console.error("");
302
+ console.error(" worker --job-id <id> [--worker-id <id>] [--work-dir <dir>]");
303
+ console.error(" init [--non-interactive] [--local] [--work-dir <dir>]");
304
+ console.error(" verify --run-dir <dir>");
305
+ console.error(" inspect --run-dir <dir>");
306
+ console.error(" export --run-dir <dir> --format json|csv [--out <path>]");
307
+ console.error(" benchmark --profile <fixture> [--work-dir <dir>]");
308
+ console.error(" leaderboard [options]");
309
+ console.error(" --profile <fixture> filter by profile");
310
+ console.error(" --compare <run-id-1>,<run-id-2> compare specific runs");
311
+ console.error(" --html [--out <path>] generate HTML report");
312
+ console.error(" --clear clear all leaderboard entries");
313
+ console.error(" providers list available providers and required env vars");
314
+ process.exitCode = 1;
315
+ }
316
+
317
+ async function main(): Promise<void> {
318
+ const [command, ...args] = process.argv.slice(2);
319
+
320
+ if (command === "run" || command === "research") {
321
+ await handleRun(args);
322
+ return;
323
+ }
324
+
325
+ if (command === "verify") {
326
+ await handleVerify(args);
327
+ return;
328
+ }
329
+
330
+ if (command === "inspect") {
331
+ await handleInspect(args);
332
+ return;
333
+ }
334
+
335
+ if (command === "export") {
336
+ await handleExport(args);
337
+ return;
338
+ }
339
+
340
+ if (command === "benchmark") {
341
+ await handleBenchmark(args);
342
+ return;
343
+ }
344
+
345
+ if (command === "leaderboard") {
346
+ await handleLeaderboard(args);
347
+ return;
348
+ }
349
+
350
+ if (command === "init") {
351
+ await handleInit(args);
352
+ return;
353
+ }
354
+
355
+ if (command === "worker") {
356
+ await handleWorker(args);
357
+ return;
358
+ }
359
+
360
+ if (command === "providers") {
361
+ handleProviders();
362
+ return;
363
+ }
364
+
365
+ if (command === "--help" || command === "-h" || command === undefined) {
366
+ printUsage();
367
+ return;
368
+ }
369
+
370
+ console.error(`Unknown command: ${command}`);
371
+ printUsage();
372
+ }
373
+
374
+ main().catch((error: Error) => {
375
+ console.error(error.message);
376
+ process.exitCode = 1;
377
+ });
@@ -0,0 +1,99 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { Source } from "./types";
3
+
4
+ export interface LoadCommandSourcesOptions {
5
+ providerCommand?: string;
6
+ providerArgs?: string[];
7
+ objective?: string;
8
+ }
9
+
10
+ interface ProviderEvent {
11
+ type?: string;
12
+ source?: Source;
13
+ message?: string;
14
+ }
15
+
16
+ export async function loadCommandSources({
17
+ providerCommand,
18
+ providerArgs = [],
19
+ objective,
20
+ }: LoadCommandSourcesOptions = {}): Promise<Source[]> {
21
+ if (!providerCommand) {
22
+ throw new Error("local-command profile requires providerCommand");
23
+ }
24
+
25
+ const output = await runProviderCommand({ providerCommand, providerArgs, objective });
26
+ return parseProviderJsonl(output);
27
+ }
28
+
29
+ function runProviderCommand({
30
+ providerCommand,
31
+ providerArgs,
32
+ objective,
33
+ }: Required<Pick<LoadCommandSourcesOptions, "providerCommand" | "providerArgs">> &
34
+ Pick<LoadCommandSourcesOptions, "objective">): Promise<string> {
35
+ return new Promise((resolve, reject) => {
36
+ const child = spawn(providerCommand, providerArgs, {
37
+ env: {
38
+ ...process.env,
39
+ WIDE_SEARCH_OBJECTIVE: objective,
40
+ },
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ });
43
+
44
+ let stdout = "";
45
+ let stderr = "";
46
+
47
+ child.stdout.setEncoding("utf8");
48
+ child.stderr.setEncoding("utf8");
49
+ child.stdout.on("data", (chunk: string) => {
50
+ stdout += chunk;
51
+ });
52
+ child.stderr.on("data", (chunk: string) => {
53
+ stderr += chunk;
54
+ });
55
+ child.on("error", reject);
56
+ child.on("close", (code) => {
57
+ if (code !== 0) {
58
+ reject(new Error(`provider command exited ${code}: ${stderr.trim()}`));
59
+ return;
60
+ }
61
+ resolve(stdout);
62
+ });
63
+ });
64
+ }
65
+
66
+ function parseProviderJsonl(output: string): Source[] {
67
+ const sources: Source[] = [];
68
+ const errors: string[] = [];
69
+
70
+ for (const [index, line] of output.split(/\r?\n/).entries()) {
71
+ if (!line.trim()) continue;
72
+ let event: ProviderEvent;
73
+ try {
74
+ event = JSON.parse(line) as ProviderEvent;
75
+ } catch {
76
+ errors.push(`line ${index + 1} is not valid JSON`);
77
+ continue;
78
+ }
79
+
80
+ if (event.type === "source_candidate" && event.source) {
81
+ sources.push(event.source);
82
+ continue;
83
+ }
84
+
85
+ if (event.type === "error") {
86
+ errors.push(event.message ?? `provider error on line ${index + 1}`);
87
+ }
88
+ }
89
+
90
+ if (errors.length > 0) {
91
+ throw new Error(`provider output errors: ${errors.join("; ")}`);
92
+ }
93
+
94
+ if (sources.length === 0) {
95
+ throw new Error("provider emitted no source_candidate events");
96
+ }
97
+
98
+ return sources;
99
+ }
package/src/config.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import type { ExecutionProfile, SearchDepth } from "./types";
6
+
7
+ export interface ProviderConfig {
8
+ apiKey?: string;
9
+ token?: string;
10
+ }
11
+
12
+ export interface KaswConfig {
13
+ providers: Record<string, ProviderConfig>;
14
+ defaults: {
15
+ provider?: string;
16
+ depth?: SearchDepth;
17
+ profile?: ExecutionProfile;
18
+ };
19
+ }
20
+
21
+ export const DEFAULT_CONFIG: KaswConfig = {
22
+ providers: {},
23
+ defaults: {
24
+ provider: "mock",
25
+ depth: "standard",
26
+ profile: "fixture",
27
+ },
28
+ };
29
+
30
+ export function getGlobalConfigDir(): string {
31
+ return join(homedir(), ".kasw");
32
+ }
33
+
34
+ export function getGlobalConfigPath(): string {
35
+ return join(getGlobalConfigDir(), "config.json");
36
+ }
37
+
38
+ export function getLocalConfigPath(workDir: string = process.cwd()): string {
39
+ return join(workDir, ".kasw.json");
40
+ }
41
+
42
+ export async function loadConfig(workDir: string = process.cwd()): Promise<KaswConfig> {
43
+ const globalConfig = await loadConfigFile(getGlobalConfigPath());
44
+ const localConfig = await loadConfigFile(getLocalConfigPath(workDir));
45
+
46
+ return mergeConfigs(DEFAULT_CONFIG, globalConfig, localConfig);
47
+ }
48
+
49
+ async function loadConfigFile(path: string): Promise<Partial<KaswConfig>> {
50
+ try {
51
+ const text = await readFile(path, "utf8");
52
+ const parsed = JSON.parse(text) as Partial<KaswConfig>;
53
+ return normalizeConfig(parsed);
54
+ } catch (error) {
55
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
56
+ return {};
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ function normalizeConfig(config: Partial<KaswConfig>): Partial<KaswConfig> {
63
+ return {
64
+ providers: config.providers ?? {},
65
+ defaults: config.defaults ?? {},
66
+ };
67
+ }
68
+
69
+ function mergeConfigs(
70
+ base: KaswConfig,
71
+ global: Partial<KaswConfig>,
72
+ local: Partial<KaswConfig>,
73
+ ): KaswConfig {
74
+ return {
75
+ providers: {
76
+ ...base.providers,
77
+ ...global.providers,
78
+ ...local.providers,
79
+ },
80
+ defaults: {
81
+ ...base.defaults,
82
+ ...global.defaults,
83
+ ...local.defaults,
84
+ },
85
+ };
86
+ }
87
+
88
+ export async function writeConfig(
89
+ config: KaswConfig,
90
+ options: { global?: boolean; workDir?: string } = {},
91
+ ): Promise<string> {
92
+ const path = options.global
93
+ ? getGlobalConfigPath()
94
+ : getLocalConfigPath(options.workDir ?? process.cwd());
95
+
96
+ await mkdir(getGlobalConfigDir(), { recursive: true });
97
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
98
+ return path;
99
+ }
100
+
101
+ const PROVIDER_ENV_VARS: Record<string, string> = {
102
+ serper: "SERPER_API_KEY",
103
+ tavily: "TAVILY_API_KEY",
104
+ brave: "BRAVE_API_KEY",
105
+ github: "GITHUB_TOKEN",
106
+ };
107
+
108
+ export function resolveProviderCredential(
109
+ config: KaswConfig,
110
+ providerName: string,
111
+ ): string | undefined {
112
+ const envVar = PROVIDER_ENV_VARS[providerName];
113
+ const envValue = envVar ? process.env[envVar] : undefined;
114
+ if (envValue) {
115
+ return envValue;
116
+ }
117
+
118
+ const providerConfig = config.providers[providerName];
119
+ if (providerConfig) {
120
+ return providerConfig.apiKey ?? providerConfig.token;
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+
126
+ export function listConfiguredProviders(config: KaswConfig): string[] {
127
+ const fromConfig = Object.keys(config.providers).filter(
128
+ (name) => config.providers[name]?.apiKey || config.providers[name]?.token,
129
+ );
130
+ const fromEnv = Object.keys(PROVIDER_ENV_VARS).filter(
131
+ (name) => process.env[PROVIDER_ENV_VARS[name]],
132
+ );
133
+ return [...new Set([...fromConfig, ...fromEnv])];
134
+ }