@eidentic/cli 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1127 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-QGM4M3NI.js";
3
+
4
+ // src/index.ts
5
+ import { defineCommand, runMain } from "citty";
6
+ import { consola } from "consola";
7
+ import pc from "picocolors";
8
+ import { createRequire } from "node:module";
9
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
10
+ import { dirname as dirname2, join as join2 } from "node:path";
11
+ import { existsSync as existsSync2 } from "node:fs";
12
+ import { spawn } from "node:child_process";
13
+
14
+ // src/commands.ts
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync, copyFileSync, statSync } from "node:fs";
16
+ import { join, resolve, isAbsolute, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { createJiti } from "jiti";
19
+ import { createServer } from "@eidentic/server";
20
+ var INIT_PROVIDERS = {
21
+ anthropic: {
22
+ package: "@ai-sdk/anthropic",
23
+ envVar: "ANTHROPIC_API_KEY",
24
+ importLine: 'import { anthropic } from "@ai-sdk/anthropic";',
25
+ modelId: "claude-sonnet-4-5",
26
+ providerFn: "anthropic",
27
+ models: ["claude-sonnet-4-5", "claude-opus-4-1", "claude-haiku-4-5"]
28
+ },
29
+ openai: {
30
+ package: "@ai-sdk/openai",
31
+ envVar: "OPENAI_API_KEY",
32
+ importLine: 'import { openai } from "@ai-sdk/openai";',
33
+ modelId: "gpt-4o",
34
+ providerFn: "openai",
35
+ models: ["gpt-4o", "gpt-4o-mini", "o4-mini"]
36
+ },
37
+ google: {
38
+ package: "@ai-sdk/google",
39
+ envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
40
+ importLine: 'import { google } from "@ai-sdk/google";',
41
+ modelId: "gemini-2.5-pro",
42
+ providerFn: "google",
43
+ models: ["gemini-2.5-pro", "gemini-2.5-flash"]
44
+ },
45
+ deepseek: {
46
+ package: "@ai-sdk/deepseek",
47
+ envVar: "DEEPSEEK_API_KEY",
48
+ importLine: 'import { deepseek } from "@ai-sdk/deepseek";',
49
+ modelId: "deepseek-chat",
50
+ providerFn: "deepseek",
51
+ models: ["deepseek-chat", "deepseek-reasoner"]
52
+ },
53
+ mistral: {
54
+ package: "@ai-sdk/mistral",
55
+ envVar: "MISTRAL_API_KEY",
56
+ importLine: 'import { mistral } from "@ai-sdk/mistral";',
57
+ modelId: "mistral-large-latest",
58
+ providerFn: "mistral",
59
+ models: ["mistral-large-latest", "mistral-small-latest"]
60
+ }
61
+ };
62
+ function eidenticConfigTs(p, modelId) {
63
+ return `import { Agent, AIModel, SqliteStore, createTool, defaultPrices } from "eidentic";
64
+ ${p.importLine}
65
+ import { z } from "zod";
66
+
67
+ const store = new SqliteStore("./eidentic.sqlite");
68
+ await store.migrate();
69
+
70
+ // A tiny example tool \u2014 replace or extend with your own.
71
+ const getTime = createTool({
72
+ id: "get_time",
73
+ description: "Get the current server time as an ISO string.",
74
+ inputSchema: z.object({}),
75
+ execute: async () => ({ now: new Date().toISOString() }),
76
+ });
77
+
78
+ export const agents = {
79
+ assistant: new Agent({
80
+ id: "assistant",
81
+ instructions: "You are a helpful assistant.",
82
+ model: new AIModel(${p.providerFn}("${modelId}")), // reads ${p.envVar} from env/.env
83
+ tools: [getTime],
84
+ store,
85
+ prices: defaultPrices, // bundled price table \u2014 cost.usd populated in every run result
86
+ }),
87
+ };
88
+ `;
89
+ }
90
+ function srcAgentTs(p, modelId) {
91
+ return `try { process.loadEnvFile(); } catch {}
92
+ import { Agent, AIModel, SqliteStore, createTool, defaultPrices } from "eidentic";
93
+ ${p.importLine}
94
+ import { z } from "zod";
95
+
96
+ // Persistent, event-sourced session store.
97
+ const store = new SqliteStore("./eidentic.sqlite");
98
+ await store.migrate();
99
+
100
+ // A tiny example tool \u2014 replace with your own.
101
+ const getTime = createTool({
102
+ id: "get_time",
103
+ description: "Get the current server time as an ISO string.",
104
+ inputSchema: z.object({}),
105
+ execute: async () => ({ now: new Date().toISOString() }),
106
+ });
107
+
108
+ const agent = new Agent({
109
+ id: "assistant",
110
+ instructions: "You are a helpful assistant. Use tools when relevant, then answer concisely.",
111
+ model: new AIModel(${p.providerFn}("${modelId}")), // reads ${p.envVar} from env
112
+ tools: [getTime],
113
+ store,
114
+ prices: defaultPrices, // bundled price table \u2014 cost.usd populated in every run result
115
+ });
116
+
117
+ for await (const ev of agent.query("What time is it right now?", { sessionId: "session-1" })) {
118
+ if (ev.type === "result") console.log("\\n" + String(ev.output));
119
+ }
120
+
121
+ await store.close();
122
+ `;
123
+ }
124
+ function initProject(cwd, opts = {}) {
125
+ const provider = opts.provider ?? "anthropic";
126
+ const p = INIT_PROVIDERS[provider];
127
+ const modelId = opts.model ?? p.modelId;
128
+ const created = [];
129
+ const skipped = [];
130
+ function writeIfAbsent(rel, content) {
131
+ const abs = join(cwd, rel);
132
+ if (existsSync(abs)) {
133
+ skipped.push(rel);
134
+ } else {
135
+ const dir = join(cwd, rel.includes("/") ? rel.split("/").slice(0, -1).join("/") : ".");
136
+ mkdirSync(dir, { recursive: true });
137
+ writeFileSync(abs, content, "utf8");
138
+ created.push(rel);
139
+ }
140
+ }
141
+ writeIfAbsent("eidentic.config.ts", eidenticConfigTs(p, modelId));
142
+ writeIfAbsent("src/agent.ts", srcAgentTs(p, modelId));
143
+ writeIfAbsent(".env.example", `# Get a key at https://console.anthropic.com (or your provider's dashboard)
144
+ ${p.envVar}=
145
+ `);
146
+ const giAbs = join(cwd, ".gitignore");
147
+ if (existsSync(giAbs)) {
148
+ const existing = readFileSync(giAbs, "utf8");
149
+ if (!existing.split("\n").map((l) => l.trim()).includes(".env")) {
150
+ appendFileSync(giAbs, "\n.env\n");
151
+ created.push(".gitignore (appended .env)");
152
+ } else {
153
+ skipped.push(".gitignore");
154
+ }
155
+ } else {
156
+ writeFileSync(giAbs, "node_modules\ndist\n*.sqlite\n.env\n", "utf8");
157
+ created.push(".gitignore");
158
+ }
159
+ const envAbs = join(cwd, ".env");
160
+ if (existsSync(envAbs)) {
161
+ skipped.push(".env");
162
+ } else {
163
+ const envValue = opts.apiKey ? opts.apiKey : "";
164
+ writeFileSync(envAbs, `${p.envVar}=${envValue}
165
+ `, "utf8");
166
+ created.push(".env");
167
+ }
168
+ return { created, skipped };
169
+ }
170
+ var PROVIDER_KEYS = [
171
+ "ANTHROPIC_API_KEY",
172
+ "OPENAI_API_KEY",
173
+ "GOOGLE_GENERATIVE_AI_API_KEY",
174
+ "DEEPSEEK_API_KEY",
175
+ "MISTRAL_API_KEY"
176
+ ];
177
+ function doctor(env = process.env, cwd = process.cwd()) {
178
+ const checks = [];
179
+ const rawVersion = process.version;
180
+ const major = parseInt(rawVersion.replace(/^v/, "").split(".")[0] ?? "0", 10);
181
+ const nodeOk = major >= 22;
182
+ checks.push({
183
+ name: "Node.js >= 22",
184
+ ok: nodeOk,
185
+ detail: nodeOk ? `Node ${rawVersion} (OK)` : `Node ${rawVersion} is too old \u2014 upgrade to >= 22`
186
+ });
187
+ const foundKey = PROVIDER_KEYS.find((k) => Boolean(env[k]));
188
+ const providerOk = foundKey !== void 0;
189
+ checks.push({
190
+ name: "Model provider key",
191
+ ok: providerOk,
192
+ detail: providerOk ? `${foundKey} is set` : `None of ${PROVIDER_KEYS.join(", ")} found in environment`
193
+ });
194
+ const configPath = resolveConfigPath(cwd);
195
+ const configOk = configPath !== null;
196
+ checks.push({
197
+ name: "eidentic.config file",
198
+ ok: configOk,
199
+ detail: configOk ? `Found ${configPath}` : `No eidentic.config.{ts,js,mjs} in ${cwd}`
200
+ });
201
+ const dotEnvExists = existsSync(join(cwd, ".env"));
202
+ checks.push({
203
+ name: ".env file",
204
+ ok: true,
205
+ detail: dotEnvExists ? `Found ${join(cwd, ".env")}` : `No .env in ${cwd} (optional \u2014 set env vars another way)`
206
+ });
207
+ const ok = checks.every((c) => c.ok);
208
+ return { checks, ok };
209
+ }
210
+ var CONFIG_NAMES = ["eidentic.config.ts", "eidentic.config.js", "eidentic.config.mjs"];
211
+ function resolveConfigPath(cwd, explicit) {
212
+ if (explicit) {
213
+ return existsSync(explicit) ? explicit : null;
214
+ }
215
+ for (const name of CONFIG_NAMES) {
216
+ const full = join(cwd, name);
217
+ if (existsSync(full)) return full;
218
+ }
219
+ return null;
220
+ }
221
+ async function loadConfig(configPath) {
222
+ const jiti = createJiti(import.meta.url);
223
+ const raw = await jiti.import(
224
+ configPath,
225
+ { default: true }
226
+ );
227
+ const config = raw;
228
+ if (!config || typeof config !== "object" || !config.agents || typeof config.agents !== "object") {
229
+ throw new Error(
230
+ `Config at ${configPath} must export an object with an "agents" field (Record<string, Agent>).`
231
+ );
232
+ }
233
+ const agentIds = Object.keys(config.agents);
234
+ if (agentIds.length === 0) {
235
+ throw new Error(
236
+ `Config at ${configPath} has an empty "agents" object \u2014 add at least one agent.`
237
+ );
238
+ }
239
+ return config;
240
+ }
241
+ function buildServer(config) {
242
+ return createServer({
243
+ agents: config.agents,
244
+ auth: config.auth,
245
+ basePath: config.basePath,
246
+ exposeEvents: config.exposeEvents
247
+ });
248
+ }
249
+ function computePassRate(aggregate) {
250
+ const entries = Object.values(aggregate);
251
+ return entries.length === 0 ? 0 : entries.reduce((sum, e) => sum + e.pass, 0) / entries.length;
252
+ }
253
+ async function runEval(configPath, opts = {}) {
254
+ const jiti = createJiti(import.meta.url);
255
+ const raw = await jiti.import(configPath, { default: true });
256
+ const evalConfig = raw;
257
+ if (!evalConfig || typeof evalConfig !== "object" || !evalConfig.runner || !evalConfig.dataset || !evalConfig.scorers) {
258
+ throw new Error(
259
+ `Eval config at ${configPath} must export { runner, dataset, scorers } (and optionally samples).`
260
+ );
261
+ }
262
+ const evalMod = await import("@eidentic/eval");
263
+ const report = await evalMod.evaluate(
264
+ evalConfig.runner,
265
+ evalConfig.dataset,
266
+ {
267
+ scorers: evalConfig.scorers,
268
+ samples: evalConfig.samples
269
+ }
270
+ );
271
+ const summary = evalMod.summarize(report);
272
+ const passRate = computePassRate(report.aggregate);
273
+ let compareResult;
274
+ if (opts.baselinePath) {
275
+ const baselineRaw = readFileSync(opts.baselinePath, "utf8");
276
+ const baseline = JSON.parse(baselineRaw);
277
+ compareResult = evalMod.compareReports(baseline, report);
278
+ }
279
+ const markdownReport = evalMod.renderReportMarkdown(
280
+ report,
281
+ compareResult ? { compare: compareResult } : {}
282
+ );
283
+ if (opts.reportPath) {
284
+ const reportDir = dirname(opts.reportPath);
285
+ if (reportDir && reportDir !== ".") {
286
+ mkdirSync(reportDir, { recursive: true });
287
+ }
288
+ writeFileSync(opts.reportPath, markdownReport, "utf8");
289
+ }
290
+ if (opts.saveBaselinePath) {
291
+ const baselineDir = dirname(opts.saveBaselinePath);
292
+ if (baselineDir && baselineDir !== ".") {
293
+ mkdirSync(baselineDir, { recursive: true });
294
+ }
295
+ writeFileSync(opts.saveBaselinePath, JSON.stringify(report, null, 2), "utf8");
296
+ }
297
+ if (opts.ci) {
298
+ if (compareResult && compareResult.verdict === "regressed") {
299
+ const regressionMsg = compareResult.regressions.map((r) => ` - ${r.caseName}/${r.scorer}: ${(r.baseline * 100).toFixed(1)}% \u2192 ${(r.current * 100).toFixed(1)}% (${(r.delta * 100).toFixed(1)}pp)`).join("\n");
300
+ const err = new Error(
301
+ `Eval regressions detected (${compareResult.regressions.length}):
302
+ ${regressionMsg}`
303
+ );
304
+ err.name = "EvalRegressionError";
305
+ err.actualPassRate = passRate;
306
+ err.requiredPassRate = opts.threshold ?? 1;
307
+ throw err;
308
+ }
309
+ evalMod.assertPassRate(report, opts.threshold ?? 1);
310
+ }
311
+ return { summary, report, passRate, compareResult, markdownReport };
312
+ }
313
+ var SKILLS_DIR_NAME = "skills";
314
+ function makeDirResolver(sourceDir) {
315
+ return (name) => {
316
+ const candidate = join(resolve(sourceDir), name);
317
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
318
+ return candidate;
319
+ }
320
+ return null;
321
+ };
322
+ }
323
+ async function addSkill(source, opts = {}) {
324
+ const projectRoot = opts.projectRoot ?? process.cwd();
325
+ const resolver = opts.resolver ?? (() => null);
326
+ const force = opts.force ?? false;
327
+ let skillSrcDir;
328
+ const isPath = isAbsolute(source) || source.startsWith(".") || source.includes("/");
329
+ if (isPath) {
330
+ skillSrcDir = isAbsolute(source) ? source : resolve(projectRoot, source);
331
+ } else {
332
+ const resolved = resolver(source);
333
+ if (resolved === null) {
334
+ throw new Error(
335
+ `addSkill: cannot resolve skill "${source}". Pass a path (starting with . / .. or absolute) or inject a resolver that knows where to find "${source}".`
336
+ );
337
+ }
338
+ skillSrcDir = resolved;
339
+ }
340
+ if (!existsSync(skillSrcDir)) {
341
+ throw new Error(`addSkill: source not found: "${skillSrcDir}"`);
342
+ }
343
+ const srcStat = statSync(skillSrcDir);
344
+ if (!srcStat.isDirectory()) {
345
+ throw new Error(
346
+ `addSkill: source must be a directory containing SKILL.md, got a file: "${skillSrcDir}"`
347
+ );
348
+ }
349
+ const skillMdPath = join(skillSrcDir, "SKILL.md");
350
+ if (!existsSync(skillMdPath)) {
351
+ throw new Error(
352
+ `addSkill: no SKILL.md found in "${skillSrcDir}". A skill directory must contain a SKILL.md file.`
353
+ );
354
+ }
355
+ const skillMdContent = readFileSync(skillMdPath, "utf8");
356
+ let parseSkillMd;
357
+ try {
358
+ const mod = await import("./dist-DDLWLV6O.js");
359
+ parseSkillMd = mod.parseSkillMd;
360
+ } catch {
361
+ throw new Error(
362
+ "addSkill: @eidentic/skills is not installed. Add it to your project to use `eidentic add skill`."
363
+ );
364
+ }
365
+ let manifest;
366
+ try {
367
+ ({ manifest } = parseSkillMd(skillMdContent));
368
+ } catch (err) {
369
+ throw new Error(
370
+ `addSkill: invalid SKILL.md in "${skillSrcDir}": ${err.message}`
371
+ );
372
+ }
373
+ const skillsDir = join(projectRoot, SKILLS_DIR_NAME);
374
+ const destDir = join(skillsDir, manifest.name);
375
+ const alreadyExists = existsSync(destDir);
376
+ if (alreadyExists && !force) {
377
+ throw new Error(
378
+ `addSkill: skill "${manifest.name}" already exists at "${destDir}". Pass --force to overwrite.`
379
+ );
380
+ }
381
+ const executableFiles = collectExecutableFiles(skillSrcDir);
382
+ if (executableFiles.length > 0) {
383
+ const warn = opts.warnImpl ?? console.warn;
384
+ warn(
385
+ `[eidentic add skill] The skill bundle contains executable file(s) \u2014 review before trusting:
386
+ ` + executableFiles.map((f) => ` \u2022 ${f}`).join("\n")
387
+ );
388
+ }
389
+ mkdirSync(destDir, { recursive: true });
390
+ copySkillDir(skillSrcDir, destDir);
391
+ return {
392
+ name: manifest.name,
393
+ installedAt: destDir,
394
+ replaced: alreadyExists
395
+ };
396
+ }
397
+ var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".mjs", ".cjs", ".tsx", ".jsx", ".sh", ".py", ".rb", ".pl"]);
398
+ function collectExecutableFiles(dir, base = "") {
399
+ const results = [];
400
+ const entries = readdirSync(dir, { withFileTypes: true });
401
+ for (const entry of entries) {
402
+ if (entry.isSymbolicLink()) continue;
403
+ if (entry.name === ".memory.md") continue;
404
+ const rel = base ? `${base}/${entry.name}` : entry.name;
405
+ if (entry.isDirectory()) {
406
+ results.push(...collectExecutableFiles(join(dir, entry.name), rel));
407
+ } else if (entry.isFile()) {
408
+ const ext = entry.name.includes(".") ? `.${entry.name.split(".").pop().toLowerCase()}` : "";
409
+ if (EXECUTABLE_EXTENSIONS.has(ext)) {
410
+ results.push(rel);
411
+ }
412
+ }
413
+ }
414
+ return results;
415
+ }
416
+ function copySkillDir(src, dest) {
417
+ const entries = readdirSync(src, { withFileTypes: true });
418
+ for (const entry of entries) {
419
+ if (entry.isSymbolicLink()) continue;
420
+ if (entry.name === ".memory.md") continue;
421
+ const srcPath = join(src, entry.name);
422
+ const destPath = join(dest, entry.name);
423
+ if (entry.isDirectory()) {
424
+ mkdirSync(destPath, { recursive: true });
425
+ copySkillDir(srcPath, destPath);
426
+ } else if (entry.isFile()) {
427
+ copyFileSync(srcPath, destPath);
428
+ }
429
+ }
430
+ }
431
+ var COMPONENT_NAMES = ["chat", "workflow-trace", "run-status"];
432
+ var COMPONENT_DESCRIPTIONS = {
433
+ chat: "Full-featured streaming chat UI using useAgent (messages, tool-call display, stop).",
434
+ "workflow-trace": "Workflow run trace viewer using useWorkflowRun (step timeline, status badges).",
435
+ "run-status": "Fire-and-poll async run status badge using useAsyncRun and useRunStatus."
436
+ };
437
+ var COMPONENTS_DIR_NAME = "components/eidentic";
438
+ function resolveBuiltinTemplatesDir() {
439
+ let pkgRoot;
440
+ try {
441
+ pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
442
+ } catch {
443
+ pkgRoot = dirname(dirname(globalThis.__filename ?? __filename));
444
+ }
445
+ return join(pkgRoot, "templates", "components");
446
+ }
447
+ function addComponent(name, opts = {}) {
448
+ if (!COMPONENT_NAMES.includes(name)) {
449
+ throw new Error(
450
+ `addComponent: unknown component "${name}". Available components: ${COMPONENT_NAMES.join(", ")}.`
451
+ );
452
+ }
453
+ const componentName = name;
454
+ const projectRoot = opts.projectRoot ?? process.cwd();
455
+ const force = opts.force ?? opts.overwrite ?? false;
456
+ const relTargetDir = opts.targetDir ?? COMPONENTS_DIR_NAME;
457
+ const templatesDir = opts.templatesDir ?? resolveBuiltinTemplatesDir();
458
+ const srcFile = join(templatesDir, `${componentName}.tsx`);
459
+ if (!existsSync(srcFile)) {
460
+ throw new Error(
461
+ `addComponent: template file not found: "${srcFile}". This is a bug in @eidentic/cli \u2014 please file an issue.`
462
+ );
463
+ }
464
+ const destDir = join(projectRoot, relTargetDir);
465
+ const destFile = join(destDir, `${componentName}.tsx`);
466
+ const alreadyExists = existsSync(destFile);
467
+ if (alreadyExists && !force) {
468
+ throw new Error(
469
+ `addComponent: component "${componentName}" already exists at "${destFile}". Pass --force to overwrite.`
470
+ );
471
+ }
472
+ mkdirSync(destDir, { recursive: true });
473
+ copyFileSync(srcFile, destFile);
474
+ return {
475
+ name: componentName,
476
+ installedAt: destFile,
477
+ replaced: alreadyExists
478
+ };
479
+ }
480
+
481
+ // src/index.ts
482
+ import { serveNode, NoAuth } from "@eidentic/server";
483
+ try {
484
+ process.loadEnvFile();
485
+ } catch {
486
+ }
487
+ var __dirname = dirname2(fileURLToPath2(import.meta.url));
488
+ var _require = createRequire(import.meta.url);
489
+ var PKG_VERSION = "0.0.0";
490
+ try {
491
+ const pkg = _require(join2(__dirname, "../package.json"));
492
+ PKG_VERSION = pkg.version;
493
+ } catch {
494
+ }
495
+ var doctorCmd = defineCommand({
496
+ meta: {
497
+ name: "doctor",
498
+ description: "Check environment readiness: Node version, model-provider key, config file."
499
+ },
500
+ async run() {
501
+ const report = doctor();
502
+ for (const check of report.checks) {
503
+ if (check.ok) {
504
+ consola.log(` ${pc.green("\u2713")} ${check.name}: ${check.detail}`);
505
+ } else {
506
+ consola.log(` ${pc.red("\u2717")} ${check.name}: ${check.detail}`);
507
+ }
508
+ }
509
+ consola.log("");
510
+ if (report.ok) {
511
+ consola.success("All checks passed.");
512
+ } else {
513
+ consola.error("Some checks failed. Fix the issues above before running eidentic dev.");
514
+ process.exit(1);
515
+ }
516
+ }
517
+ });
518
+ var devCmd = defineCommand({
519
+ meta: {
520
+ name: "dev",
521
+ description: "Start a local dev server. Loads eidentic.config.{ts,js,mjs} from the current directory (or an explicit path)."
522
+ },
523
+ args: {
524
+ config: {
525
+ type: "positional",
526
+ description: "Path to eidentic config file (optional)",
527
+ required: false
528
+ },
529
+ port: {
530
+ type: "string",
531
+ description: "Port to listen on (default: 3000)",
532
+ alias: ["p"]
533
+ }
534
+ },
535
+ async run({ args }) {
536
+ const explicitPath = args.config;
537
+ const port = args.port ? parseInt(String(args.port), 10) : void 0;
538
+ const configPath = resolveConfigPath(process.cwd(), explicitPath);
539
+ if (!configPath) {
540
+ consola.error("No eidentic.config.{ts,js,mjs} found.");
541
+ consola.info(
542
+ "Create a eidentic.config.ts in the current directory, or pass the path explicitly."
543
+ );
544
+ consola.info("Run `eidentic doctor` for a full environment check.");
545
+ process.exit(1);
546
+ }
547
+ let config;
548
+ try {
549
+ config = await loadConfig(configPath);
550
+ } catch (err) {
551
+ consola.error(`Error loading config: ${err.message}`);
552
+ process.exit(1);
553
+ }
554
+ const app = buildServer(config);
555
+ const listenPort = port ?? config.port ?? 3e3;
556
+ let handle;
557
+ try {
558
+ handle = await serveNode(app, { port: listenPort });
559
+ } catch (err) {
560
+ consola.error(`Failed to start server: ${err.message}`);
561
+ process.exit(1);
562
+ }
563
+ const agentIds = Object.keys(config.agents);
564
+ consola.success(
565
+ `eidentic dev server running at ${pc.cyan(`http://localhost:${listenPort}`)}`
566
+ );
567
+ consola.info(`Agents: ${pc.bold(agentIds.join(", "))}`);
568
+ consola.info("Press Ctrl+C to stop.");
569
+ process.on("SIGINT", () => {
570
+ handle.close();
571
+ process.exit(0);
572
+ });
573
+ }
574
+ });
575
+ function openBrowser(url) {
576
+ try {
577
+ const platform = process.platform;
578
+ let cmd;
579
+ let args;
580
+ if (platform === "darwin") {
581
+ cmd = "open";
582
+ args = [url];
583
+ } else if (platform === "win32") {
584
+ cmd = "cmd";
585
+ args = ["/c", "start", url];
586
+ } else {
587
+ cmd = "xdg-open";
588
+ args = [url];
589
+ }
590
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
591
+ } catch {
592
+ }
593
+ }
594
+ var studioCmd = defineCommand({
595
+ meta: {
596
+ name: "studio",
597
+ description: "Start Eidentic Studio (dev tool) on port 3535. Loads eidentic.config.{ts,js,mjs} from the current directory."
598
+ },
599
+ args: {
600
+ config: {
601
+ type: "positional",
602
+ description: "Path to eidentic config file (optional)",
603
+ required: false
604
+ },
605
+ port: {
606
+ type: "string",
607
+ description: "Port to listen on (default: 3535)",
608
+ alias: ["p"]
609
+ }
610
+ },
611
+ async run({ args }) {
612
+ const explicitPath = args.config;
613
+ const port = args.port ? parseInt(String(args.port), 10) : 3535;
614
+ const configPath = resolveConfigPath(process.cwd(), explicitPath);
615
+ if (!configPath) {
616
+ consola.error("No eidentic.config.{ts,js,mjs} found.");
617
+ consola.info(
618
+ "Create a eidentic.config.ts in the current directory, or pass the path explicitly."
619
+ );
620
+ process.exit(1);
621
+ }
622
+ let config;
623
+ try {
624
+ config = await loadConfig(configPath);
625
+ } catch (err) {
626
+ consola.error(`Error loading config: ${err.message}`);
627
+ process.exit(1);
628
+ }
629
+ if (config.auth && config.auth !== NoAuth) {
630
+ consola.warn(`Studio auth is active. Open the studio URL with ?key=<your-token> appended so the UI can authenticate (e.g. http://localhost:${port}/?key=<your-token>).`);
631
+ }
632
+ let serveStudio;
633
+ try {
634
+ const studioMod = await import("@eidentic/studio");
635
+ serveStudio = studioMod.serveStudio;
636
+ } catch (err) {
637
+ consola.error(`Failed to load @eidentic/studio: ${err.message}`);
638
+ consola.info("Make sure @eidentic/studio is installed.");
639
+ process.exit(1);
640
+ }
641
+ let handle;
642
+ try {
643
+ handle = await serveStudio(
644
+ {
645
+ agents: config.agents,
646
+ auth: config.auth,
647
+ skillBanks: config.skillBanks
648
+ },
649
+ { port }
650
+ );
651
+ } catch (err) {
652
+ consola.error(`Failed to start studio: ${err.message}`);
653
+ process.exit(1);
654
+ }
655
+ const url = `http://localhost:${port}`;
656
+ consola.success(`Eidentic Studio \u2192 ${pc.cyan(url)}`);
657
+ consola.info("Press Ctrl+C to stop.");
658
+ openBrowser(url);
659
+ process.on("SIGINT", () => {
660
+ handle.close();
661
+ process.exit(0);
662
+ });
663
+ }
664
+ });
665
+ var PROVIDER_CHOICES = ["anthropic", "openai", "google", "deepseek", "mistral"];
666
+ var OTHER_MODEL_SENTINEL = "__other__";
667
+ function detectPackageManager(cwd) {
668
+ if (existsSync2(join2(cwd, "pnpm-lock.yaml"))) return "pnpm";
669
+ if (existsSync2(join2(cwd, "yarn.lock"))) return "yarn";
670
+ if (existsSync2(join2(cwd, "bun.lockb"))) return "bun";
671
+ return "npm";
672
+ }
673
+ function spawnInstall(pm, pkgs, devPkgs) {
674
+ return new Promise((resolve2, reject) => {
675
+ const installArgs = pm === "npm" ? ["install"] : ["add"];
676
+ const devFlag = pm === "npm" ? "--save-dev" : "-D";
677
+ const child = spawn(pm, [...installArgs, ...pkgs], { stdio: "inherit" });
678
+ child.on("close", (code) => {
679
+ if (code !== 0) {
680
+ reject(new Error(`${pm} install exited with code ${code}`));
681
+ return;
682
+ }
683
+ const devChild = spawn(pm, [...installArgs, devFlag, ...devPkgs], { stdio: "inherit" });
684
+ devChild.on("close", (devCode) => {
685
+ if (devCode !== 0) {
686
+ reject(new Error(`${pm} install -D exited with code ${devCode}`));
687
+ return;
688
+ }
689
+ resolve2();
690
+ });
691
+ devChild.on("error", reject);
692
+ });
693
+ child.on("error", reject);
694
+ });
695
+ }
696
+ function printInitResult(created, skipped) {
697
+ for (const f of created) {
698
+ consola.log(` ${pc.green("+")} ${f}`);
699
+ }
700
+ for (const f of skipped) {
701
+ consola.log(` ${pc.yellow("~")} ${f} ${pc.dim("(already exists, skipped)")}`);
702
+ }
703
+ }
704
+ function printNextSteps(provider, apiKeyProvided) {
705
+ consola.log("");
706
+ consola.success("Eidentic init complete.");
707
+ consola.log("");
708
+ consola.log(pc.bold("Next steps:"));
709
+ let step = 1;
710
+ if (!apiKeyProvided) {
711
+ const envVar = INIT_PROVIDERS[provider].envVar;
712
+ consola.log(` ${pc.cyan(`${step})`)} Add your API key to ${pc.bold(".env")}: ${pc.dim(`${envVar}=<your-key>`)}`);
713
+ step++;
714
+ }
715
+ consola.log(` ${pc.cyan(`${step})`)} ${pc.cyan("npx eidentic dev")} ${pc.dim("(or npx eidentic studio)")}`);
716
+ consola.log(` ${pc.cyan(`${step + 1})`)} ${pc.cyan("npx tsx src/agent.ts")} ${pc.dim("(run the agent script directly)")}`);
717
+ }
718
+ var initCmd = defineCommand({
719
+ meta: {
720
+ name: "init",
721
+ description: "Scaffold Eidentic into the current directory. Creates eidentic.config.ts, src/agent.ts, .env, .env.example, .gitignore. Idempotent \u2014 never overwrites existing files."
722
+ },
723
+ args: {
724
+ provider: {
725
+ type: "string",
726
+ description: `Model provider (${PROVIDER_CHOICES.join(" | ")}). Default: anthropic`,
727
+ alias: ["p"]
728
+ },
729
+ model: {
730
+ type: "string",
731
+ description: "Model ID to use (e.g. claude-sonnet-4-5). Defaults to the provider default.",
732
+ alias: ["m"]
733
+ },
734
+ "api-key": {
735
+ type: "string",
736
+ description: "API key to write into .env (optional \u2014 can be added later)."
737
+ },
738
+ yes: {
739
+ type: "boolean",
740
+ description: "Skip interactive prompts; use flags/defaults. Does not install deps unless --install is also given.",
741
+ alias: ["y"]
742
+ },
743
+ install: {
744
+ type: "boolean",
745
+ description: "Install dependencies after scaffolding (default: prompted interactively)."
746
+ }
747
+ },
748
+ async run({ args }) {
749
+ const isInteractive = process.stdin.isTTY && !args.yes;
750
+ let provider;
751
+ {
752
+ const rawProvider = args.provider;
753
+ if (rawProvider) {
754
+ if (!PROVIDER_CHOICES.includes(rawProvider)) {
755
+ consola.error(`Unknown provider "${rawProvider}". Choose one of: ${PROVIDER_CHOICES.join(", ")}`);
756
+ process.exit(1);
757
+ }
758
+ provider = rawProvider;
759
+ } else if (isInteractive) {
760
+ const { intro, select, isCancel, cancel } = await import("@clack/prompts");
761
+ intro(pc.bold("eidentic init"));
762
+ const providerAnswer = await select({
763
+ message: "Model provider",
764
+ options: [
765
+ { value: "anthropic", label: "Anthropic", hint: "Claude \u2014 needs ANTHROPIC_API_KEY" },
766
+ { value: "openai", label: "OpenAI", hint: "GPT \u2014 needs OPENAI_API_KEY" },
767
+ { value: "google", label: "Google", hint: "Gemini \u2014 needs GOOGLE_GENERATIVE_AI_API_KEY" },
768
+ { value: "deepseek", label: "DeepSeek", hint: "needs DEEPSEEK_API_KEY" },
769
+ { value: "mistral", label: "Mistral", hint: "needs MISTRAL_API_KEY" }
770
+ ]
771
+ });
772
+ if (isCancel(providerAnswer)) {
773
+ cancel("Cancelled.");
774
+ process.exit(0);
775
+ }
776
+ provider = providerAnswer;
777
+ } else {
778
+ provider = "anthropic";
779
+ }
780
+ }
781
+ const providerMeta = INIT_PROVIDERS[provider];
782
+ let model;
783
+ {
784
+ const rawModel = args.model;
785
+ if (rawModel) {
786
+ model = rawModel;
787
+ } else if (isInteractive) {
788
+ const { select, text, isCancel, cancel } = await import("@clack/prompts");
789
+ const modelOptions = [
790
+ ...providerMeta.models.map((m) => ({ value: m, label: m })),
791
+ { value: OTHER_MODEL_SENTINEL, label: "Other (type manually)" }
792
+ ];
793
+ const modelAnswer = await select({ message: "Model", options: modelOptions });
794
+ if (isCancel(modelAnswer)) {
795
+ cancel("Cancelled.");
796
+ process.exit(0);
797
+ }
798
+ if (modelAnswer === OTHER_MODEL_SENTINEL) {
799
+ const customModel = await text({
800
+ message: "Enter model ID",
801
+ placeholder: providerMeta.modelId
802
+ });
803
+ if (isCancel(customModel)) {
804
+ cancel("Cancelled.");
805
+ process.exit(0);
806
+ }
807
+ model = customModel || providerMeta.modelId;
808
+ } else {
809
+ model = modelAnswer;
810
+ }
811
+ } else {
812
+ model = providerMeta.modelId;
813
+ }
814
+ }
815
+ let apiKey;
816
+ {
817
+ const rawKey = args["api-key"];
818
+ if (rawKey !== void 0) {
819
+ apiKey = rawKey || void 0;
820
+ } else if (isInteractive) {
821
+ const { password, isCancel, cancel } = await import("@clack/prompts");
822
+ const keyAnswer = await password({
823
+ message: `API key for ${providerMeta.envVar} (leave blank to add later)`
824
+ });
825
+ if (isCancel(keyAnswer)) {
826
+ cancel("Cancelled.");
827
+ process.exit(0);
828
+ }
829
+ apiKey = keyAnswer || void 0;
830
+ }
831
+ }
832
+ let shouldInstall;
833
+ {
834
+ if (args.install !== void 0) {
835
+ shouldInstall = Boolean(args.install);
836
+ } else if (isInteractive) {
837
+ const { confirm, isCancel, cancel } = await import("@clack/prompts");
838
+ const installAnswer = await confirm({
839
+ message: `Install dependencies now? (eidentic, ai, @ai-sdk/${provider}, zod)`,
840
+ initialValue: true
841
+ });
842
+ if (isCancel(installAnswer)) {
843
+ cancel("Cancelled.");
844
+ process.exit(0);
845
+ }
846
+ shouldInstall = Boolean(installAnswer);
847
+ } else {
848
+ shouldInstall = false;
849
+ }
850
+ }
851
+ const cwd = process.cwd();
852
+ if (!isInteractive) {
853
+ consola.info(`Scaffolding Eidentic into ${pc.cyan(cwd)} (provider: ${pc.bold(provider)}, model: ${pc.bold(model)})`);
854
+ }
855
+ const { created, skipped } = initProject(cwd, { provider, model, apiKey });
856
+ printInitResult(created, skipped);
857
+ if (shouldInstall) {
858
+ const pm = detectPackageManager(cwd);
859
+ const prodPkgs = ["eidentic", "ai", `@ai-sdk/${provider}`, "zod"];
860
+ const devPkgs = ["tsx", "typescript"];
861
+ consola.info(`Running ${pm} install...`);
862
+ try {
863
+ await spawnInstall(pm, prodPkgs, devPkgs);
864
+ consola.success("Dependencies installed.");
865
+ } catch (err) {
866
+ consola.warn(`Install failed: ${err.message}`);
867
+ consola.info(`Run manually: ${pm} ${pm === "npm" ? "install" : "add"} ${prodPkgs.join(" ")}`);
868
+ }
869
+ }
870
+ if (isInteractive) {
871
+ const { outro } = await import("@clack/prompts");
872
+ const steps = [];
873
+ if (!apiKey) {
874
+ steps.push(`Set your API key in .env: ${providerMeta.envVar}=<your-key>`);
875
+ }
876
+ if (!shouldInstall) {
877
+ steps.push(`Install deps: ${detectPackageManager(cwd)} ${detectPackageManager(cwd) === "npm" ? "install" : "add"} eidentic ai @ai-sdk/${provider} zod`);
878
+ }
879
+ steps.push("Start dev server: npx eidentic dev");
880
+ steps.push("Or run the agent: npx tsx src/agent.ts");
881
+ outro(
882
+ [pc.bold("Done!"), "", ...steps.map((s, i) => ` ${pc.cyan(`${i + 1})`)} ${s}`)].join("\n")
883
+ );
884
+ } else {
885
+ printNextSteps(provider, Boolean(apiKey));
886
+ }
887
+ }
888
+ });
889
+ var evalCmd = defineCommand({
890
+ meta: {
891
+ name: "eval",
892
+ description: "Run an evaluation against a config/dataset file. Prints a summary and, with --ci, exits non-zero when the pass rate is below --threshold or when regressions are detected against --baseline."
893
+ },
894
+ args: {
895
+ config: {
896
+ type: "positional",
897
+ description: "Path to the eval config file (.ts/.js/.mjs) that exports { runner, dataset, scorers }.",
898
+ required: true
899
+ },
900
+ ci: {
901
+ type: "boolean",
902
+ description: "Exit non-zero when the pass rate is below --threshold or regressions are detected.",
903
+ default: false
904
+ },
905
+ threshold: {
906
+ type: "string",
907
+ description: "Pass-rate threshold in [0, 1] (default: 1). Only used with --ci.",
908
+ alias: ["t"]
909
+ },
910
+ baseline: {
911
+ type: "string",
912
+ description: "Path to a baseline EvalReport JSON file. When provided, compares current run against the baseline and fails CI on any regression."
913
+ },
914
+ "save-baseline": {
915
+ type: "string",
916
+ description: "Path where the current EvalReport will be saved as a JSON baseline snapshot."
917
+ },
918
+ report: {
919
+ type: "string",
920
+ description: "Path where the Markdown eval report will be written (GitHub-comment-friendly)."
921
+ }
922
+ },
923
+ async run({ args }) {
924
+ const configPath = args.config;
925
+ const ci = Boolean(args.ci);
926
+ const threshold = args.threshold ? parseFloat(String(args.threshold)) : 1;
927
+ const baselinePath = args.baseline;
928
+ const saveBaselinePath = args["save-baseline"];
929
+ const reportPath = args.report;
930
+ if (ci && (isNaN(threshold) || threshold < 0 || threshold > 1)) {
931
+ consola.error(`--threshold must be a number in [0, 1], got: ${args.threshold}`);
932
+ process.exit(1);
933
+ }
934
+ let result;
935
+ try {
936
+ result = await runEval(configPath, { ci, threshold, baselinePath, saveBaselinePath, reportPath });
937
+ } catch (err) {
938
+ const e = err;
939
+ if (e.name === "EvalThresholdError") {
940
+ consola.log(e.message);
941
+ consola.error(
942
+ `CI gate FAILED \u2014 pass rate ${(e.actualPassRate * 100).toFixed(1)}% < required ${(e.requiredPassRate * 100).toFixed(1)}%`
943
+ );
944
+ process.exit(1);
945
+ }
946
+ if (e.name === "EvalRegressionError") {
947
+ consola.log(e.message);
948
+ consola.error("CI gate FAILED \u2014 regressions detected against baseline.");
949
+ process.exit(1);
950
+ }
951
+ consola.error(`Eval failed: ${e.message}`);
952
+ process.exit(1);
953
+ }
954
+ consola.log(result.summary);
955
+ consola.log("");
956
+ consola.success(`Pass rate: ${pc.bold((result.passRate * 100).toFixed(1) + "%")}`);
957
+ if (result.compareResult) {
958
+ const { regressions, improvements, unchanged, verdict } = result.compareResult;
959
+ consola.log(`Baseline comparison: ${verdict === "pass" ? pc.green("no regressions") : pc.red(`${regressions.length} regression(s)`)}, ${improvements.length} improvement(s), ${unchanged} unchanged.`);
960
+ }
961
+ if (reportPath) {
962
+ consola.success(`Markdown report written to ${pc.cyan(reportPath)}`);
963
+ }
964
+ if (saveBaselinePath) {
965
+ consola.success(`Baseline snapshot saved to ${pc.cyan(saveBaselinePath)}`);
966
+ }
967
+ if (ci) {
968
+ consola.success("CI gate passed.");
969
+ }
970
+ }
971
+ });
972
+ var addSkillCmd = defineCommand({
973
+ meta: {
974
+ name: "skill",
975
+ description: "Install a skill into the project's local skills directory (skills/<name>/)."
976
+ },
977
+ args: {
978
+ source: {
979
+ type: "positional",
980
+ description: "Path to a skill directory (must contain SKILL.md), or a skill name resolvable via --from.",
981
+ required: true
982
+ },
983
+ force: {
984
+ type: "boolean",
985
+ description: "Overwrite an existing skill with the same name.",
986
+ alias: ["f"],
987
+ default: false
988
+ },
989
+ from: {
990
+ type: "string",
991
+ description: "Path to a local skills source directory (a directory of skill subdirs). Used when <source> is a name rather than a path."
992
+ },
993
+ cwd: {
994
+ type: "string",
995
+ description: "Project root to install into (default: current directory)."
996
+ }
997
+ },
998
+ async run({ args }) {
999
+ const source = args.source;
1000
+ const force = Boolean(args.force);
1001
+ const fromDir = args.from;
1002
+ const projectRoot = args.cwd ?? process.cwd();
1003
+ const resolver = fromDir ? makeDirResolver(fromDir) : void 0;
1004
+ let result;
1005
+ try {
1006
+ result = await addSkill(source, { force, resolver, projectRoot });
1007
+ } catch (err) {
1008
+ consola.error(err.message);
1009
+ process.exit(1);
1010
+ }
1011
+ if (result.replaced) {
1012
+ consola.success(
1013
+ `Skill ${pc.bold(result.name)} replaced at ${pc.cyan(result.installedAt)}`
1014
+ );
1015
+ } else {
1016
+ consola.success(
1017
+ `Skill ${pc.bold(result.name)} installed at ${pc.cyan(result.installedAt)}`
1018
+ );
1019
+ }
1020
+ }
1021
+ });
1022
+ var addComponentCmd = defineCommand({
1023
+ meta: {
1024
+ name: "component",
1025
+ description: `Copy a pre-built UI component into the project (components/eidentic/<name>.tsx). Use --list to see all available components.`
1026
+ },
1027
+ args: {
1028
+ name: {
1029
+ type: "positional",
1030
+ description: `Component name: ${COMPONENT_NAMES.join(" | ")}. Omit when using --list.`,
1031
+ required: false
1032
+ },
1033
+ list: {
1034
+ type: "boolean",
1035
+ description: "List available components with descriptions and exit.",
1036
+ alias: ["l"],
1037
+ default: false
1038
+ },
1039
+ overwrite: {
1040
+ type: "boolean",
1041
+ description: "Overwrite an existing component file (required if the file already exists).",
1042
+ alias: ["f"],
1043
+ default: false
1044
+ },
1045
+ dir: {
1046
+ type: "string",
1047
+ description: "Target directory relative to --cwd (default: components/eidentic)."
1048
+ },
1049
+ cwd: {
1050
+ type: "string",
1051
+ description: "Project root to install into (default: current directory)."
1052
+ }
1053
+ },
1054
+ run({ args }) {
1055
+ if (args.list) {
1056
+ consola.log(pc.bold("Available components:"));
1057
+ consola.log("");
1058
+ for (const name2 of COMPONENT_NAMES) {
1059
+ consola.log(` ${pc.cyan(pc.bold(name2))}`);
1060
+ consola.log(` ${pc.dim(COMPONENT_DESCRIPTIONS[name2])}`);
1061
+ }
1062
+ consola.log("");
1063
+ consola.info(`Install with: ${pc.cyan("eidentic add component <name>")}`);
1064
+ return;
1065
+ }
1066
+ const name = args.name;
1067
+ if (!name) {
1068
+ consola.error("Component name is required. Use --list to see available components.");
1069
+ consola.info(`Available components: ${pc.bold(COMPONENT_NAMES.join(", "))}`);
1070
+ process.exit(1);
1071
+ }
1072
+ const overwrite = Boolean(args.overwrite);
1073
+ const projectRoot = args.cwd ?? process.cwd();
1074
+ const targetDir = args.dir;
1075
+ let result;
1076
+ try {
1077
+ result = addComponent(name, { overwrite, projectRoot, targetDir });
1078
+ } catch (err) {
1079
+ const msg = err.message;
1080
+ if (msg.includes("unknown component")) {
1081
+ consola.error(msg);
1082
+ consola.info(`Available components: ${pc.bold(COMPONENT_NAMES.join(", "))}`);
1083
+ } else if (msg.includes("already exists")) {
1084
+ consola.error(msg);
1085
+ consola.info(`Re-run with ${pc.bold("--overwrite")} to replace the existing file.`);
1086
+ } else {
1087
+ consola.error(msg);
1088
+ }
1089
+ process.exit(1);
1090
+ }
1091
+ if (result.replaced) {
1092
+ consola.success(
1093
+ `Component ${pc.bold(result.name)} replaced at ${pc.cyan(result.installedAt)}`
1094
+ );
1095
+ } else {
1096
+ consola.success(
1097
+ `Component ${pc.bold(result.name)} installed at ${pc.cyan(result.installedAt)}`
1098
+ );
1099
+ }
1100
+ }
1101
+ });
1102
+ var addCmd = defineCommand({
1103
+ meta: {
1104
+ name: "add",
1105
+ description: "Add resources to the current Eidentic project."
1106
+ },
1107
+ subCommands: {
1108
+ skill: addSkillCmd,
1109
+ component: addComponentCmd
1110
+ }
1111
+ });
1112
+ var main = defineCommand({
1113
+ meta: {
1114
+ name: "eidentic",
1115
+ version: PKG_VERSION,
1116
+ description: "Eidentic agent toolkit CLI"
1117
+ },
1118
+ subCommands: {
1119
+ doctor: doctorCmd,
1120
+ dev: devCmd,
1121
+ eval: evalCmd,
1122
+ studio: studioCmd,
1123
+ init: initCmd,
1124
+ add: addCmd
1125
+ }
1126
+ });
1127
+ runMain(main);