@tuttiai/cli 0.7.0 → 0.9.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { config } from "dotenv";
5
- import { createLogger as createLogger8 } from "@tuttiai/core";
5
+ import { createLogger as createLogger10 } from "@tuttiai/core";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/init.ts
@@ -11,9 +11,209 @@ import { join } from "path";
11
11
  import chalk from "chalk";
12
12
  import Enquirer from "enquirer";
13
13
  import { createLogger } from "@tuttiai/core";
14
+
15
+ // src/templates/index.ts
16
+ var minimal = {
17
+ id: "minimal",
18
+ name: "Minimal",
19
+ description: "One agent, no voices \u2014 the simplest starting point",
20
+ deps: {},
21
+ envVars: [],
22
+ score: `import { defineScore, AnthropicProvider } from "@tuttiai/core"
23
+
24
+ export default defineScore({
25
+ provider: new AnthropicProvider(),
26
+ default_model: "claude-sonnet-4-20250514",
27
+ agents: {
28
+ assistant: {
29
+ name: "Assistant",
30
+ system_prompt: "You are a helpful assistant.",
31
+ voices: [],
32
+ }
33
+ }
34
+ })
35
+ `
36
+ };
37
+ var codingAgent = {
38
+ id: "coding-agent",
39
+ name: "Coding Agent",
40
+ description: "TypeScript developer with filesystem + GitHub access",
41
+ deps: { "@tuttiai/filesystem": "*", "@tuttiai/github": "*" },
42
+ envVars: ["GITHUB_TOKEN=ghp_your_token_here"],
43
+ score: `import { defineScore, AnthropicProvider } from "@tuttiai/core"
44
+ import { FilesystemVoice } from "@tuttiai/filesystem"
45
+ import { GitHubVoice } from "@tuttiai/github"
46
+
47
+ export default defineScore({
48
+ provider: new AnthropicProvider(),
49
+ default_model: "claude-sonnet-4-20250514",
50
+ agents: {
51
+ coder: {
52
+ name: "Coder",
53
+ system_prompt:
54
+ "You are an expert TypeScript developer. " +
55
+ "You read and write code using the filesystem voice, " +
56
+ "and manage issues and PRs via the GitHub voice. " +
57
+ "Write clean, tested, well-documented code.",
58
+ voices: [new FilesystemVoice(), new GitHubVoice()],
59
+ permissions: ["filesystem", "network"],
60
+ streaming: true,
61
+ }
62
+ }
63
+ })
64
+ `
65
+ };
66
+ var researchAgent = {
67
+ id: "research-agent",
68
+ name: "Research Agent",
69
+ description: "Researcher that saves structured notes to files",
70
+ deps: { "@tuttiai/filesystem": "*" },
71
+ envVars: [],
72
+ score: `import { defineScore, AnthropicProvider } from "@tuttiai/core"
73
+ import { FilesystemVoice } from "@tuttiai/filesystem"
74
+
75
+ export default defineScore({
76
+ provider: new AnthropicProvider(),
77
+ default_model: "claude-sonnet-4-20250514",
78
+ agents: {
79
+ researcher: {
80
+ name: "Researcher",
81
+ system_prompt:
82
+ "You are an expert researcher. " +
83
+ "Analyze topics thoroughly, cite sources, and save " +
84
+ "structured notes as markdown files using the filesystem voice. " +
85
+ "Organize findings with clear headings and bullet points.",
86
+ voices: [new FilesystemVoice()],
87
+ permissions: ["filesystem"],
88
+ streaming: true,
89
+ }
90
+ }
91
+ })
92
+ `
93
+ };
94
+ var qaPipeline = {
95
+ id: "qa-pipeline",
96
+ name: "QA Pipeline",
97
+ description: "Orchestrator + QA specialist with browser testing and HITL",
98
+ deps: { "@tuttiai/playwright": "*" },
99
+ envVars: [],
100
+ score: `import { defineScore, AnthropicProvider } from "@tuttiai/core"
101
+ import { PlaywrightVoice } from "@tuttiai/playwright"
102
+
103
+ export default defineScore({
104
+ provider: new AnthropicProvider(),
105
+ default_model: "claude-sonnet-4-20250514",
106
+ entry: "orchestrator",
107
+ agents: {
108
+ orchestrator: {
109
+ name: "QA Lead",
110
+ system_prompt:
111
+ "You are a QA lead. Triage incoming bugs by delegating " +
112
+ "browser testing to the QA specialist. Use human-in-the-loop " +
113
+ "to ask for approval before marking bugs as verified.",
114
+ voices: [],
115
+ role: "orchestrator",
116
+ delegates: ["qa"],
117
+ allow_human_input: true,
118
+ streaming: true,
119
+ },
120
+ qa: {
121
+ name: "QA Specialist",
122
+ system_prompt:
123
+ "You are a QA engineer. Navigate to URLs, check elements, " +
124
+ "take screenshots, and verify bug reports using the browser.",
125
+ voices: [new PlaywrightVoice()],
126
+ permissions: ["network", "browser"],
127
+ role: "specialist",
128
+ budget: { max_cost_usd: 0.50, warn_at_percent: 80 },
129
+ streaming: true,
130
+ }
131
+ }
132
+ })
133
+ `
134
+ };
135
+ var devTeam = {
136
+ id: "dev-team",
137
+ name: "Dev Team",
138
+ description: "Full team: orchestrator + coder + PM + QA with all voices",
139
+ deps: {
140
+ "@tuttiai/filesystem": "*",
141
+ "@tuttiai/github": "*",
142
+ "@tuttiai/playwright": "*"
143
+ },
144
+ envVars: ["GITHUB_TOKEN=ghp_your_token_here"],
145
+ score: `import { defineScore, AnthropicProvider, createLoggingHook, createBlocklistHook, createLogger } from "@tuttiai/core"
146
+ import { FilesystemVoice } from "@tuttiai/filesystem"
147
+ import { GitHubVoice } from "@tuttiai/github"
148
+ import { PlaywrightVoice } from "@tuttiai/playwright"
149
+
150
+ const logger = createLogger("dev-team")
151
+
152
+ export default defineScore({
153
+ provider: new AnthropicProvider(),
154
+ default_model: "claude-sonnet-4-20250514",
155
+ entry: "orchestrator",
156
+ hooks: {
157
+ ...createLoggingHook(logger),
158
+ ...createBlocklistHook(["delete_file"]),
159
+ },
160
+ agents: {
161
+ orchestrator: {
162
+ name: "Tech Lead",
163
+ system_prompt:
164
+ "You are the tech lead. Break tasks into subtasks and delegate: " +
165
+ "code tasks to Coder, documentation to PM, testing to QA. " +
166
+ "Review outputs before presenting to the user.",
167
+ voices: [],
168
+ role: "orchestrator",
169
+ delegates: ["coder", "pm", "qa"],
170
+ allow_human_input: true,
171
+ streaming: true,
172
+ },
173
+ coder: {
174
+ name: "Coder",
175
+ system_prompt:
176
+ "You are a senior TypeScript developer. Write clean, tested code. " +
177
+ "Use the filesystem voice to read/write files and GitHub to manage PRs.",
178
+ voices: [new FilesystemVoice(), new GitHubVoice()],
179
+ permissions: ["filesystem", "network"],
180
+ role: "specialist",
181
+ streaming: true,
182
+ },
183
+ pm: {
184
+ name: "PM",
185
+ system_prompt:
186
+ "You are a product manager. Write specs, update documentation, " +
187
+ "and create GitHub issues for tracking. Focus on clarity and completeness.",
188
+ voices: [new FilesystemVoice(), new GitHubVoice()],
189
+ permissions: ["filesystem", "network"],
190
+ role: "specialist",
191
+ streaming: true,
192
+ },
193
+ qa: {
194
+ name: "QA",
195
+ system_prompt:
196
+ "You are a QA engineer. Test features in the browser, verify bugs, " +
197
+ "and write test reports. Screenshot evidence for every finding.",
198
+ voices: [new PlaywrightVoice()],
199
+ permissions: ["network", "browser"],
200
+ role: "specialist",
201
+ budget: { max_cost_usd: 1.00 },
202
+ streaming: true,
203
+ }
204
+ }
205
+ })
206
+ `
207
+ };
208
+ var TEMPLATES = [minimal, codingAgent, researchAgent, qaPipeline, devTeam];
209
+ function getTemplate(id) {
210
+ return TEMPLATES.find((t) => t.id === id);
211
+ }
212
+
213
+ // src/commands/init.ts
14
214
  var logger = createLogger("tutti-cli");
15
215
  var { prompt } = Enquirer;
16
- async function initCommand(projectName) {
216
+ async function initCommand(projectName, templateId) {
17
217
  if (!projectName) {
18
218
  const response = await prompt({
19
219
  type: "input",
@@ -31,7 +231,44 @@ async function initCommand(projectName) {
31
231
  logger.error({ dir: `${projectName}/` }, "Directory already exists");
32
232
  process.exit(1);
33
233
  }
234
+ let template;
235
+ if (templateId) {
236
+ template = getTemplate(templateId);
237
+ if (!template) {
238
+ logger.error({ template: templateId }, "Unknown template");
239
+ console.error(chalk.dim(" Available: " + TEMPLATES.map((t) => t.id).join(", ")));
240
+ process.exit(1);
241
+ }
242
+ } else {
243
+ const response = await prompt({
244
+ type: "select",
245
+ name: "templateId",
246
+ message: "Which template?",
247
+ choices: TEMPLATES.map((t) => ({
248
+ name: t.id,
249
+ message: t.name + chalk.dim(" \u2014 " + t.description)
250
+ }))
251
+ });
252
+ template = getTemplate(response.templateId);
253
+ if (!template) template = TEMPLATES[0];
254
+ }
34
255
  mkdirSync(dir, { recursive: true });
256
+ const deps = {
257
+ "@tuttiai/core": "*",
258
+ "@tuttiai/types": "*",
259
+ ...template.deps
260
+ };
261
+ const envLines = [
262
+ "ANTHROPIC_API_KEY=your_key_here",
263
+ ...template.envVars,
264
+ "",
265
+ "# Log level: debug | info | warn | error (default: info)",
266
+ "TUTTI_LOG_LEVEL=info",
267
+ "",
268
+ "# OpenTelemetry (optional)",
269
+ "# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
270
+ "# OTEL_SERVICE_NAME=tutti"
271
+ ];
35
272
  const files = {
36
273
  "package.json": JSON.stringify(
37
274
  {
@@ -42,10 +279,7 @@ async function initCommand(projectName) {
42
279
  dev: "tsx watch tutti.score.ts",
43
280
  start: "tsx tutti.score.ts"
44
281
  },
45
- dependencies: {
46
- "@tuttiai/core": "*",
47
- "@tuttiai/types": "*"
48
- },
282
+ dependencies: deps,
49
283
  devDependencies: {
50
284
  tsx: "^4.0.0",
51
285
  typescript: "^5.7.0"
@@ -54,7 +288,7 @@ async function initCommand(projectName) {
54
288
  null,
55
289
  2
56
290
  ),
57
- ".env.example": "ANTHROPIC_API_KEY=your_key_here\n\n# Log level: debug | info | warn | error (default: info)\nTUTTI_LOG_LEVEL=info\n\n# OpenTelemetry (optional)\n# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\n# OTEL_SERVICE_NAME=tutti\n",
291
+ ".env.example": envLines.join("\n") + "\n",
58
292
  ".gitignore": "node_modules\ndist\n.env\n",
59
293
  "tsconfig.json": JSON.stringify(
60
294
  {
@@ -73,29 +307,18 @@ async function initCommand(projectName) {
73
307
  null,
74
308
  2
75
309
  ),
76
- "tutti.score.ts": `import { defineScore, AnthropicProvider } from "@tuttiai/core"
77
-
78
- export default defineScore({
79
- provider: new AnthropicProvider(),
80
- default_model: "claude-sonnet-4-20250514",
81
- agents: {
82
- assistant: {
83
- name: "Assistant",
84
- system_prompt: "You are a helpful assistant.",
85
- voices: [],
86
- }
87
- }
88
- })
89
- `,
310
+ "tutti.score.ts": template.score,
90
311
  "README.md": `# ${projectName}
91
312
 
92
313
  A Tutti agent project. All agents. All together.
93
314
 
315
+ **Template:** ${template.name} \u2014 ${template.description}
316
+
94
317
  ## Setup
95
318
 
96
319
  \`\`\`bash
97
320
  cp .env.example .env
98
- # Add your ANTHROPIC_API_KEY to .env
321
+ # Add your API keys to .env
99
322
  npm install
100
323
  \`\`\`
101
324
 
@@ -110,7 +333,7 @@ npm run dev
110
333
  writeFileSync(join(dir, filename), content);
111
334
  }
112
335
  console.log();
113
- console.log(chalk.green(` \u2714 Created ${projectName}/`));
336
+ console.log(chalk.green(` \u2714 Created ${projectName}/`) + chalk.dim(` (${template.name})`));
114
337
  console.log();
115
338
  console.log(" Next steps:");
116
339
  console.log(chalk.cyan(` cd ${projectName}`));
@@ -119,6 +342,17 @@ npm run dev
119
342
  console.log(chalk.cyan(" npm run dev"));
120
343
  console.log();
121
344
  }
345
+ function templatesCommand() {
346
+ console.log();
347
+ console.log(chalk.bold(" Available Templates"));
348
+ console.log();
349
+ for (const t of TEMPLATES) {
350
+ console.log(" " + chalk.cyan(t.id.padEnd(18)) + t.description);
351
+ }
352
+ console.log();
353
+ console.log(chalk.dim(" Use: tutti-ai init my-project --template " + TEMPLATES[0].id));
354
+ console.log();
355
+ }
122
356
 
123
357
  // src/commands/run.ts
124
358
  import { existsSync as existsSync2 } from "fs";
@@ -223,6 +457,23 @@ async function runCommand(scorePath) {
223
457
  input: process.stdin,
224
458
  output: process.stdout
225
459
  });
460
+ runtime.events.on("hitl:requested", (e) => {
461
+ spinner.stop();
462
+ if (streaming) {
463
+ process.stdout.write("\n");
464
+ streaming = false;
465
+ }
466
+ console.log();
467
+ console.log(chalk2.yellow(" " + chalk2.bold("[Agent needs input]") + " " + e.question));
468
+ if (e.options) {
469
+ e.options.forEach((opt, i) => {
470
+ console.log(chalk2.yellow(" " + (i + 1) + ". " + opt));
471
+ });
472
+ }
473
+ void rl.question(chalk2.yellow(" > ")).then((answer) => {
474
+ runtime.answer(e.session_id, answer.trim());
475
+ });
476
+ });
226
477
  console.log(chalk2.dim('Tutti REPL \u2014 type "exit" to quit\n'));
227
478
  let sessionId;
228
479
  process.on("SIGINT", () => {
@@ -263,52 +514,312 @@ async function runCommand(scorePath) {
263
514
  process.exit(0);
264
515
  }
265
516
 
266
- // src/commands/add.ts
267
- import { existsSync as existsSync3, readFileSync } from "fs";
517
+ // src/commands/resume.ts
518
+ import { existsSync as existsSync3 } from "fs";
268
519
  import { resolve as resolve2 } from "path";
269
- import { execSync } from "child_process";
520
+ import { createInterface as createInterface2 } from "readline/promises";
270
521
  import chalk3 from "chalk";
271
522
  import ora2 from "ora";
272
- import { createLogger as createLogger3 } from "@tuttiai/core";
523
+ import {
524
+ AnthropicProvider as AnthropicProvider2,
525
+ GeminiProvider as GeminiProvider2,
526
+ OpenAIProvider as OpenAIProvider2,
527
+ ScoreLoader as ScoreLoader2,
528
+ SecretsManager as SecretsManager2,
529
+ TuttiRuntime as TuttiRuntime2,
530
+ createCheckpointStore,
531
+ createLogger as createLogger3
532
+ } from "@tuttiai/core";
273
533
  var logger3 = createLogger3("tutti-cli");
534
+ async function resumeCommand(sessionId, opts) {
535
+ const scoreFile = resolve2(opts.score ?? "./tutti.score.ts");
536
+ if (!existsSync3(scoreFile)) {
537
+ logger3.error({ file: scoreFile }, "Score file not found");
538
+ console.error(chalk3.dim('Run "tutti-ai init" to create a new project.'));
539
+ process.exit(1);
540
+ }
541
+ let score;
542
+ try {
543
+ score = await ScoreLoader2.load(scoreFile);
544
+ } catch (err) {
545
+ logger3.error(
546
+ { error: err instanceof Error ? err.message : String(err) },
547
+ "Failed to load score"
548
+ );
549
+ process.exit(1);
550
+ }
551
+ const providerKeyMap = [
552
+ [AnthropicProvider2, "ANTHROPIC_API_KEY"],
553
+ [OpenAIProvider2, "OPENAI_API_KEY"],
554
+ [GeminiProvider2, "GEMINI_API_KEY"]
555
+ ];
556
+ for (const [ProviderClass, envVar] of providerKeyMap) {
557
+ if (score.provider instanceof ProviderClass) {
558
+ if (!SecretsManager2.optional(envVar)) {
559
+ logger3.error({ envVar }, "Missing API key");
560
+ process.exit(1);
561
+ }
562
+ }
563
+ }
564
+ const agentName = resolveAgentName(score, opts.agent);
565
+ const agent = score.agents[agentName];
566
+ if (!agent) {
567
+ logger3.error(
568
+ { agent: agentName, available: Object.keys(score.agents) },
569
+ "Agent not found in score"
570
+ );
571
+ process.exit(1);
572
+ }
573
+ if (!agent.durable) {
574
+ console.error(
575
+ chalk3.yellow(
576
+ "Agent '" + agentName + "' does not have `durable: true` set \u2014 resume has nothing to restore."
577
+ )
578
+ );
579
+ console.error(
580
+ chalk3.dim(
581
+ "Enable durable checkpointing on the agent before the run that created this session."
582
+ )
583
+ );
584
+ process.exit(1);
585
+ }
586
+ const spinner = ora2({ color: "cyan" }).start("Loading checkpoint...");
587
+ let checkpointStore;
588
+ let checkpoint;
589
+ try {
590
+ checkpointStore = createCheckpointStore({ store: opts.store });
591
+ checkpoint = await checkpointStore.loadLatest(sessionId);
592
+ } catch (err) {
593
+ spinner.fail("Failed to load checkpoint");
594
+ logger3.error(
595
+ { error: err instanceof Error ? err.message : String(err), store: opts.store },
596
+ "Checkpoint store error"
597
+ );
598
+ process.exit(1);
599
+ }
600
+ spinner.stop();
601
+ if (!checkpoint) {
602
+ console.error(
603
+ chalk3.red("No checkpoint found for session " + sessionId + ".")
604
+ );
605
+ console.error(
606
+ chalk3.dim(
607
+ "Verify TUTTI_" + (opts.store === "redis" ? "REDIS" : "PG") + "_URL points to the same " + opts.store + " the original run used."
608
+ )
609
+ );
610
+ process.exit(1);
611
+ }
612
+ printSummary(checkpoint);
613
+ if (!opts.yes && !await confirmResume(checkpoint.turn)) {
614
+ console.log(chalk3.dim("Cancelled."));
615
+ process.exit(0);
616
+ }
617
+ const runtime = new TuttiRuntime2(score, { checkpointStore });
618
+ const sessions = runtime.sessions;
619
+ if ("save" in sessions && typeof sessions.save === "function") {
620
+ sessions.save({
621
+ id: sessionId,
622
+ agent_name: agentName,
623
+ messages: [...checkpoint.messages],
624
+ created_at: checkpoint.saved_at,
625
+ updated_at: /* @__PURE__ */ new Date()
626
+ });
627
+ } else {
628
+ console.error(
629
+ chalk3.red(
630
+ "Session store does not support resume seeding. Use the default InMemorySessionStore or PostgresSessionStore."
631
+ )
632
+ );
633
+ process.exit(1);
634
+ }
635
+ wireProgress(runtime);
636
+ try {
637
+ const result = await runtime.run(agentName, "[resume]", sessionId);
638
+ console.log();
639
+ console.log(chalk3.green("\u2713 Resumed run complete."));
640
+ console.log(chalk3.dim(" Final turn: " + result.turns));
641
+ console.log(chalk3.dim(" Session ID: " + result.session_id));
642
+ console.log(
643
+ chalk3.dim(
644
+ " Token usage: " + result.usage.input_tokens + " in / " + result.usage.output_tokens + " out"
645
+ )
646
+ );
647
+ console.log();
648
+ console.log(result.output);
649
+ } catch (err) {
650
+ logger3.error(
651
+ { error: err instanceof Error ? err.message : String(err) },
652
+ "Resume failed"
653
+ );
654
+ process.exit(1);
655
+ }
656
+ }
657
+ function resolveAgentName(score, override) {
658
+ if (override) return override;
659
+ if (typeof score.entry === "string") return score.entry;
660
+ const first = Object.keys(score.agents)[0];
661
+ if (!first) {
662
+ console.error(chalk3.red("Score has no agents defined."));
663
+ process.exit(1);
664
+ }
665
+ return first;
666
+ }
667
+ function printSummary(checkpoint) {
668
+ console.log();
669
+ console.log(chalk3.cyan.bold("Checkpoint summary"));
670
+ console.log(
671
+ chalk3.dim(" Session ID: ") + checkpoint.session_id
672
+ );
673
+ console.log(
674
+ chalk3.dim(" Last turn: ") + String(checkpoint.turn)
675
+ );
676
+ console.log(
677
+ chalk3.dim(" Saved at: ") + checkpoint.saved_at.toISOString()
678
+ );
679
+ console.log(
680
+ chalk3.dim(" Messages: ") + String(checkpoint.messages.length) + " total"
681
+ );
682
+ console.log();
683
+ console.log(chalk3.cyan("First messages"));
684
+ const preview = checkpoint.messages.slice(0, 3);
685
+ for (const msg of preview) {
686
+ const text = excerpt(messageToText(msg), 200);
687
+ console.log(chalk3.dim(" [" + msg.role + "] ") + text);
688
+ }
689
+ if (checkpoint.messages.length > preview.length) {
690
+ console.log(
691
+ chalk3.dim(
692
+ " \u2026 " + String(checkpoint.messages.length - preview.length) + " more"
693
+ )
694
+ );
695
+ }
696
+ console.log();
697
+ }
698
+ function messageToText(msg) {
699
+ if (typeof msg.content === "string") return msg.content;
700
+ const parts = [];
701
+ for (const block of msg.content) {
702
+ if (block.type === "text") {
703
+ parts.push(block.text);
704
+ } else if (block.type === "tool_use") {
705
+ parts.push("[tool_use " + block.name + "]");
706
+ } else if (block.type === "tool_result") {
707
+ parts.push("[tool_result " + excerpt(block.content, 80) + "]");
708
+ }
709
+ }
710
+ return parts.join(" ");
711
+ }
712
+ function excerpt(text, max) {
713
+ const oneLine = text.replace(/\s+/g, " ").trim();
714
+ return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
715
+ }
716
+ async function confirmResume(turn) {
717
+ const rl = createInterface2({
718
+ input: process.stdin,
719
+ output: process.stdout
720
+ });
721
+ try {
722
+ const answer = (await rl.question(
723
+ chalk3.cyan("Resume from turn " + turn + "? ") + chalk3.dim("(y/n) ")
724
+ )).trim().toLowerCase();
725
+ return answer === "y" || answer === "yes";
726
+ } finally {
727
+ rl.close();
728
+ }
729
+ }
730
+ function wireProgress(runtime) {
731
+ const spinner = ora2({ color: "cyan" });
732
+ let streaming = false;
733
+ runtime.events.on("checkpoint:restored", (e) => {
734
+ console.log(
735
+ chalk3.dim("\u21BB Restored from turn " + e.turn) + chalk3.dim(" (session " + e.session_id.slice(0, 8) + "\u2026)")
736
+ );
737
+ });
738
+ runtime.events.on("checkpoint:saved", (e) => {
739
+ console.log(chalk3.dim("\xB7 Checkpoint saved at turn " + e.turn));
740
+ });
741
+ runtime.events.on("llm:request", () => {
742
+ spinner.start("Thinking...");
743
+ });
744
+ runtime.events.on("token:stream", (e) => {
745
+ if (!streaming) {
746
+ spinner.stop();
747
+ streaming = true;
748
+ }
749
+ process.stdout.write(e.text);
750
+ });
751
+ runtime.events.on("llm:response", () => {
752
+ if (streaming) {
753
+ process.stdout.write("\n");
754
+ } else {
755
+ spinner.stop();
756
+ }
757
+ });
758
+ runtime.events.on("tool:start", (e) => {
759
+ if (streaming) {
760
+ process.stdout.write(chalk3.dim("\n [using: " + e.tool_name + "]"));
761
+ } else {
762
+ spinner.stop();
763
+ console.log(chalk3.dim(" [using: " + e.tool_name + "]"));
764
+ }
765
+ });
766
+ runtime.events.on("tool:end", (e) => {
767
+ if (streaming) {
768
+ process.stdout.write(chalk3.dim(" [done: " + e.tool_name + "]\n"));
769
+ }
770
+ });
771
+ runtime.events.on("tool:error", (e) => {
772
+ spinner.stop();
773
+ logger3.error({ tool: e.tool_name }, "Tool error");
774
+ });
775
+ }
776
+
777
+ // src/commands/add.ts
778
+ import { existsSync as existsSync4, readFileSync } from "fs";
779
+ import { resolve as resolve3 } from "path";
780
+ import { execSync } from "child_process";
781
+ import chalk4 from "chalk";
782
+ import ora3 from "ora";
783
+ import { createLogger as createLogger4 } from "@tuttiai/core";
784
+ var logger4 = createLogger4("tutti-cli");
274
785
  var OFFICIAL_VOICES = {
275
786
  filesystem: {
276
787
  package: "@tuttiai/filesystem",
277
788
  setup: ` Add to your score:
278
- ${chalk3.cyan('import { FilesystemVoice } from "@tuttiai/filesystem"')}
279
- ${chalk3.cyan("voices: [new FilesystemVoice()]")}`
789
+ ${chalk4.cyan('import { FilesystemVoice } from "@tuttiai/filesystem"')}
790
+ ${chalk4.cyan("voices: [new FilesystemVoice()]")}`
280
791
  },
281
792
  github: {
282
793
  package: "@tuttiai/github",
283
- setup: ` Add ${chalk3.bold("GITHUB_TOKEN")} to your .env file:
284
- ${chalk3.cyan("GITHUB_TOKEN=ghp_your_token_here")}
794
+ setup: ` Add ${chalk4.bold("GITHUB_TOKEN")} to your .env file:
795
+ ${chalk4.cyan("GITHUB_TOKEN=ghp_your_token_here")}
285
796
 
286
797
  Add to your score:
287
- ${chalk3.cyan('import { GitHubVoice } from "@tuttiai/github"')}
288
- ${chalk3.cyan("voices: [new GitHubVoice()]")}`
798
+ ${chalk4.cyan('import { GitHubVoice } from "@tuttiai/github"')}
799
+ ${chalk4.cyan("voices: [new GitHubVoice()]")}`
289
800
  },
290
801
  playwright: {
291
802
  package: "@tuttiai/playwright",
292
803
  setup: ` Install the browser:
293
- ${chalk3.cyan("npx playwright install chromium")}
804
+ ${chalk4.cyan("npx playwright install chromium")}
294
805
 
295
806
  Add to your score:
296
- ${chalk3.cyan('import { PlaywrightVoice } from "@tuttiai/playwright"')}
297
- ${chalk3.cyan("voices: [new PlaywrightVoice()]")}`
807
+ ${chalk4.cyan('import { PlaywrightVoice } from "@tuttiai/playwright"')}
808
+ ${chalk4.cyan("voices: [new PlaywrightVoice()]")}`
298
809
  },
299
810
  postgres: {
300
811
  package: "pg",
301
- setup: ` Add ${chalk3.bold("DATABASE_URL")} to your .env file:
302
- ${chalk3.cyan("DATABASE_URL=postgres://user:pass@localhost:5432/tutti")}
812
+ setup: ` Add ${chalk4.bold("DATABASE_URL")} to your .env file:
813
+ ${chalk4.cyan("DATABASE_URL=postgres://user:pass@localhost:5432/tutti")}
303
814
 
304
815
  Add to your score:
305
- ${chalk3.cyan("memory: { provider: 'postgres' }")}
816
+ ${chalk4.cyan("memory: { provider: 'postgres' }")}
306
817
 
307
818
  Or with an explicit URL:
308
- ${chalk3.cyan("memory: { provider: 'postgres', url: process.env.DATABASE_URL }")}
819
+ ${chalk4.cyan("memory: { provider: 'postgres', url: process.env.DATABASE_URL }")}
309
820
 
310
821
  Use the async factory for initialization:
311
- ${chalk3.cyan("const tutti = await TuttiRuntime.create(score)")}`
822
+ ${chalk4.cyan("const tutti = await TuttiRuntime.create(score)")}`
312
823
  }
313
824
  };
314
825
  function resolvePackageName(input) {
@@ -321,8 +832,8 @@ function resolvePackageName(input) {
321
832
  return `@tuttiai/${input}`;
322
833
  }
323
834
  function isAlreadyInstalled(packageName) {
324
- const pkgPath = resolve2(process.cwd(), "package.json");
325
- if (!existsSync3(pkgPath)) return false;
835
+ const pkgPath = resolve3(process.cwd(), "package.json");
836
+ if (!existsSync4(pkgPath)) return false;
326
837
  try {
327
838
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
328
839
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -331,19 +842,19 @@ function isAlreadyInstalled(packageName) {
331
842
  return false;
332
843
  }
333
844
  }
334
- async function addCommand(voiceName) {
845
+ function addCommand(voiceName) {
335
846
  const packageName = resolvePackageName(voiceName);
336
- const pkgPath = resolve2(process.cwd(), "package.json");
337
- if (!existsSync3(pkgPath)) {
338
- logger3.error("No package.json found in the current directory");
339
- console.error(chalk3.dim('Run "tutti-ai init" to create a new project first.'));
847
+ const pkgPath = resolve3(process.cwd(), "package.json");
848
+ if (!existsSync4(pkgPath)) {
849
+ logger4.error("No package.json found in the current directory");
850
+ console.error(chalk4.dim('Run "tutti-ai init" to create a new project first.'));
340
851
  process.exit(1);
341
852
  }
342
853
  if (isAlreadyInstalled(packageName)) {
343
- console.log(chalk3.green(` \u2714 ${packageName} is already installed`));
854
+ console.log(chalk4.green(` \u2714 ${packageName} is already installed`));
344
855
  return;
345
856
  }
346
- const spinner = ora2(`Installing ${packageName}...`).start();
857
+ const spinner = ora3(`Installing ${packageName}...`).start();
347
858
  try {
348
859
  execSync(`npm install ${packageName}`, {
349
860
  cwd: process.cwd(),
@@ -353,7 +864,7 @@ async function addCommand(voiceName) {
353
864
  } catch (error) {
354
865
  spinner.fail(`Failed to install ${packageName}`);
355
866
  const message = error instanceof Error ? error.message : String(error);
356
- logger3.error({ error: message, package: packageName }, "Installation failed");
867
+ logger4.error({ error: message, package: packageName }, "Installation failed");
357
868
  process.exit(1);
358
869
  }
359
870
  const official = OFFICIAL_VOICES[voiceName];
@@ -365,43 +876,43 @@ async function addCommand(voiceName) {
365
876
  } else {
366
877
  console.log();
367
878
  console.log(
368
- chalk3.dim(" Check the package README for setup instructions.")
879
+ chalk4.dim(" Check the package README for setup instructions.")
369
880
  );
370
881
  console.log();
371
882
  }
372
883
  }
373
884
 
374
885
  // src/commands/check.ts
375
- import { existsSync as existsSync4 } from "fs";
376
- import { resolve as resolve3 } from "path";
377
- import chalk4 from "chalk";
886
+ import { existsSync as existsSync5 } from "fs";
887
+ import { resolve as resolve4 } from "path";
888
+ import chalk5 from "chalk";
378
889
  import {
379
- ScoreLoader as ScoreLoader2,
380
- AnthropicProvider as AnthropicProvider2,
381
- OpenAIProvider as OpenAIProvider2,
382
- GeminiProvider as GeminiProvider2,
383
- SecretsManager as SecretsManager2,
384
- createLogger as createLogger4
890
+ ScoreLoader as ScoreLoader3,
891
+ AnthropicProvider as AnthropicProvider3,
892
+ OpenAIProvider as OpenAIProvider3,
893
+ GeminiProvider as GeminiProvider3,
894
+ SecretsManager as SecretsManager3,
895
+ createLogger as createLogger5
385
896
  } from "@tuttiai/core";
386
- var logger4 = createLogger4("tutti-cli");
387
- var ok = (msg) => console.log(chalk4.green(" \u2714 " + msg));
388
- var fail = (msg) => console.log(chalk4.red(" \u2718 " + msg));
897
+ var logger5 = createLogger5("tutti-cli");
898
+ var ok = (msg) => console.log(chalk5.green(" \u2714 " + msg));
899
+ var fail = (msg) => console.log(chalk5.red(" \u2718 " + msg));
389
900
  async function checkCommand(scorePath) {
390
- const file = resolve3(scorePath ?? "./tutti.score.ts");
391
- console.log(chalk4.cyan(`
901
+ const file = resolve4(scorePath ?? "./tutti.score.ts");
902
+ console.log(chalk5.cyan(`
392
903
  Checking ${file}...
393
904
  `));
394
- if (!existsSync4(file)) {
905
+ if (!existsSync5(file)) {
395
906
  fail("Score file not found: " + file);
396
907
  process.exit(1);
397
908
  }
398
909
  let score;
399
910
  try {
400
- score = await ScoreLoader2.load(file);
911
+ score = await ScoreLoader3.load(file);
401
912
  ok("Score file is valid");
402
913
  } catch (err) {
403
914
  fail("Score validation failed");
404
- logger4.error(
915
+ logger5.error(
405
916
  { error: err instanceof Error ? err.message : String(err) },
406
917
  "Score validation failed"
407
918
  );
@@ -409,15 +920,15 @@ Checking ${file}...
409
920
  }
410
921
  let hasErrors = false;
411
922
  const providerChecks = [
412
- [AnthropicProvider2, "AnthropicProvider", "ANTHROPIC_API_KEY"],
413
- [OpenAIProvider2, "OpenAIProvider", "OPENAI_API_KEY"],
414
- [GeminiProvider2, "GeminiProvider", "GEMINI_API_KEY"]
923
+ [AnthropicProvider3, "AnthropicProvider", "ANTHROPIC_API_KEY"],
924
+ [OpenAIProvider3, "OpenAIProvider", "OPENAI_API_KEY"],
925
+ [GeminiProvider3, "GeminiProvider", "GEMINI_API_KEY"]
415
926
  ];
416
927
  let providerDetected = false;
417
928
  for (const [ProviderClass, name, envVar] of providerChecks) {
418
929
  if (score.provider instanceof ProviderClass) {
419
930
  providerDetected = true;
420
- const key = SecretsManager2.optional(envVar);
931
+ const key = SecretsManager3.optional(envVar);
421
932
  if (key) {
422
933
  ok("Provider: " + name + " (" + envVar + " is set)");
423
934
  } else {
@@ -439,7 +950,7 @@ Checking ${file}...
439
950
  };
440
951
  const envVar = voiceEnvMap[voiceName];
441
952
  if (envVar) {
442
- const key = SecretsManager2.optional(envVar);
953
+ const key = SecretsManager3.optional(envVar);
443
954
  if (key) {
444
955
  ok(
445
956
  "Voice: " + voiceName + " on " + agentKey + " (" + envVar + " is set)"
@@ -458,29 +969,30 @@ Checking ${file}...
458
969
  console.log("");
459
970
  if (hasErrors) {
460
971
  console.log(
461
- chalk4.yellow("Some checks failed. Fix the issues above and re-run.")
972
+ chalk5.yellow("Some checks failed. Fix the issues above and re-run.")
462
973
  );
463
974
  process.exit(1);
464
975
  } else {
465
976
  console.log(
466
- chalk4.green("All checks passed.") + chalk4.dim(" Run tutti-ai run to start.")
977
+ chalk5.green("All checks passed.") + chalk5.dim(" Run tutti-ai run to start.")
467
978
  );
468
979
  }
469
980
  }
470
981
 
471
982
  // src/commands/studio.ts
472
- import { existsSync as existsSync5 } from "fs";
473
- import { resolve as resolve4 } from "path";
474
- import { exec } from "child_process";
983
+ import { existsSync as existsSync6 } from "fs";
984
+ import { resolve as resolve5 } from "path";
985
+ import { execFile } from "child_process";
475
986
  import express from "express";
476
- import chalk5 from "chalk";
987
+ import chalk6 from "chalk";
477
988
  import {
478
- TuttiRuntime as TuttiRuntime2,
479
- ScoreLoader as ScoreLoader3,
480
- createLogger as createLogger5
989
+ TuttiRuntime as TuttiRuntime3,
990
+ ScoreLoader as ScoreLoader4,
991
+ createLogger as createLogger6
481
992
  } from "@tuttiai/core";
482
- var logger5 = createLogger5("tutti-studio");
483
- var PORT = 4747;
993
+ var logger6 = createLogger6("tutti-studio");
994
+ var envPort = Number.parseInt(process.env.PORT ?? "", 10);
995
+ var PORT = Number.isInteger(envPort) && envPort > 0 && envPort <= 65535 ? envPort : 4747;
484
996
  function safeStringify(obj) {
485
997
  return JSON.stringify(obj, (_key, value) => {
486
998
  if (value instanceof Error) return { message: value.message, name: value.name };
@@ -489,24 +1001,28 @@ function safeStringify(obj) {
489
1001
  });
490
1002
  }
491
1003
  function openBrowser(url) {
492
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
493
- exec(cmd + " " + url);
1004
+ if (process.platform === "win32") {
1005
+ execFile("cmd.exe", ["/c", "start", "", url]);
1006
+ return;
1007
+ }
1008
+ const cmd = process.platform === "darwin" ? "open" : "xdg-open";
1009
+ execFile(cmd, [url]);
494
1010
  }
495
1011
  async function studioCommand(scorePath) {
496
- const file = resolve4(scorePath ?? "./tutti.score.ts");
497
- if (!existsSync5(file)) {
498
- logger5.error({ file }, "Score file not found");
499
- console.error(chalk5.dim('Run "tutti-ai init" to create a new project.'));
1012
+ const file = resolve5(scorePath ?? "./tutti.score.ts");
1013
+ if (!existsSync6(file)) {
1014
+ logger6.error({ file }, "Score file not found");
1015
+ console.error(chalk6.dim('Run "tutti-ai init" to create a new project.'));
500
1016
  process.exit(1);
501
1017
  }
502
1018
  let score;
503
1019
  try {
504
- score = await ScoreLoader3.load(file);
1020
+ score = await ScoreLoader4.load(file);
505
1021
  } catch (err) {
506
- logger5.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to load score");
1022
+ logger6.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to load score");
507
1023
  process.exit(1);
508
1024
  }
509
- const runtime = new TuttiRuntime2(score);
1025
+ const runtime = new TuttiRuntime3(score);
510
1026
  const sessionRegistry = /* @__PURE__ */ new Map();
511
1027
  runtime.events.on("agent:start", (e) => {
512
1028
  if (!sessionRegistry.has(e.session_id)) {
@@ -576,9 +1092,24 @@ async function studioCommand(scorePath) {
576
1092
  res.json(session);
577
1093
  });
578
1094
  app.post("/api/run", async (req, res) => {
579
- const { agent, input, session_id } = req.body;
580
- if (!agent || !input) {
581
- res.status(400).json({ error: "agent and input are required" });
1095
+ const body = req.body;
1096
+ if (typeof body !== "object" || body === null) {
1097
+ res.status(400).json({ error: "Invalid request body" });
1098
+ return;
1099
+ }
1100
+ const agent = body.agent;
1101
+ const input = body.input;
1102
+ const session_id = body.session_id;
1103
+ if (typeof agent !== "string" || agent.trim().length === 0) {
1104
+ res.status(400).json({ error: "agent must be a non-empty string" });
1105
+ return;
1106
+ }
1107
+ if (typeof input !== "string" || input.trim().length === 0) {
1108
+ res.status(400).json({ error: "input must be a non-empty string" });
1109
+ return;
1110
+ }
1111
+ if (session_id !== void 0 && (typeof session_id !== "string" || session_id.trim().length === 0)) {
1112
+ res.status(400).json({ error: "session_id must be a non-empty string when provided" });
582
1113
  return;
583
1114
  }
584
1115
  try {
@@ -594,30 +1125,30 @@ async function studioCommand(scorePath) {
594
1125
  app.listen(PORT, () => {
595
1126
  const url = "http://localhost:" + PORT;
596
1127
  console.log();
597
- console.log(chalk5.bold(" Tutti Studio"));
598
- console.log(chalk5.dim(" " + url));
1128
+ console.log(chalk6.bold(" Tutti Studio"));
1129
+ console.log(chalk6.dim(" " + url));
599
1130
  console.log();
600
- console.log(chalk5.dim(" Score: ") + (runtime.score.name ?? file));
601
- console.log(chalk5.dim(" Agents: ") + Object.keys(runtime.score.agents).join(", "));
1131
+ console.log(chalk6.dim(" Score: ") + (runtime.score.name ?? file));
1132
+ console.log(chalk6.dim(" Agents: ") + Object.keys(runtime.score.agents).join(", "));
602
1133
  console.log();
603
1134
  openBrowser(url);
604
1135
  });
605
1136
  process.on("SIGINT", () => {
606
- console.log(chalk5.dim("\nShutting down Tutti Studio..."));
1137
+ console.log(chalk6.dim("\nShutting down Tutti Studio..."));
607
1138
  process.exit(0);
608
1139
  });
609
1140
  }
610
1141
  function getStudioHtml() {
611
- return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tutti Studio</title><style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#0a0a0f;--panel:#12121a;--card:#1a1a26;--input:#0f0f17;--border:#2a2a3a;--text:#e2e8f0;--muted:#64748b;--purple:#8b5cf6;--teal:#14b8a6;--blue:#3b82f6;--green:#10b981;--red:#ef4444;--orange:#f97316;--amber:#f59e0b;--indigo:#6366f1;}html,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);font-size:13px}#app{display:flex;flex-direction:column;height:100vh}header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;border-bottom:1px solid var(--border);background:var(--panel)}header .logo{font-weight:700;font-size:15px;letter-spacing:.5px}header .logo span{color:var(--purple)}header .meta{color:var(--muted);font-size:12px}header .status{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}header .dot{width:7px;height:7px;border-radius:50%;background:var(--green)}header .dot.off{background:var(--red)}main{display:grid;grid-template-columns:260px 1fr 280px;flex:1;overflow:hidden;border-bottom:1px solid var(--border)}.panel{display:flex;flex-direction:column;border-right:1px solid var(--border);overflow:hidden}.panel:last-child{border-right:none}.panel-title{padding:10px 14px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);border-bottom:1px solid var(--border);background:var(--panel);flex-shrink:0}.panel-body{flex:1;overflow-y:auto;padding:10px}.panel-body::-webkit-scrollbar{width:5px}.panel-body::-webkit-scrollbar-track{background:transparent}.panel-body::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}#graph-panel .panel-body{padding:0;display:flex;align-items:center;justify-content:center}#graph-panel svg text{font-family:system-ui,-apple-system,sans-serif}#events-panel{display:flex;flex-direction:column}#event-stream{flex:1;overflow-y:auto;padding:10px}#event-stream::-webkit-scrollbar{width:5px}#event-stream::-webkit-scrollbar-track{background:transparent}#event-stream::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.ev{padding:7px 10px;margin-bottom:6px;border-radius:6px;background:var(--card);border-left:3px solid var(--muted);font-size:12px;line-height:1.5}.ev .ev-head{display:flex;justify-content:space-between;align-items:center}.ev .ev-type{font-weight:600;font-family:"SF Mono",Menlo,monospace;font-size:11px}.ev .ev-time{color:var(--muted);font-size:10px;font-family:"SF Mono",Menlo,monospace}.ev .ev-detail{color:var(--muted);margin-top:3px;font-size:11px;word-break:break-all}.ev.agent{border-left-color:var(--purple)}.ev.agent .ev-type{color:var(--purple)}.ev.turn{border-left-color:var(--blue)}.ev.turn .ev-type{color:var(--blue)}.ev.llm{border-left-color:var(--green)}.ev.llm .ev-type{color:var(--green)}.ev.tool{border-left-color:var(--teal)}.ev.tool .ev-type{color:var(--teal)}.ev.tool-error{border-left-color:var(--red)}.ev.tool-error .ev-type{color:var(--red)}.ev.security{border-left-color:var(--orange)}.ev.security .ev-type{color:var(--orange)}.ev.budget-warn{border-left-color:var(--amber)}.ev.budget-warn .ev-type{color:var(--amber)}.ev.budget-exceed{border-left-color:var(--red)}.ev.budget-exceed .ev-type{color:var(--red)}.ev.delegate{border-left-color:var(--indigo)}.ev.delegate .ev-type{color:var(--indigo)}#input-bar{display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--border);background:var(--panel);flex-shrink:0}#agent-select{background:var(--input);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:12px;outline:none;cursor:pointer;min-width:110px}#user-input{flex:1;background:var(--input);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:13px;outline:none}#user-input:focus{border-color:var(--purple)}#send-btn{background:var(--purple);color:#fff;border:none;border-radius:6px;padding:6px 16px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}#send-btn:hover{opacity:.9}#send-btn:disabled{opacity:.4;cursor:default}.session-item{padding:8px 10px;margin-bottom:4px;border-radius:6px;background:var(--card);cursor:pointer;transition:background .15s}.session-item:hover{background:#22223a}.session-item.active{background:#22223a;border:1px solid var(--purple)}.session-id{font-family:"SF Mono",Menlo,monospace;font-size:11px;color:var(--purple)}.session-meta{font-size:11px;color:var(--muted);margin-top:2px}#session-detail{margin-top:10px;border-top:1px solid var(--border);padding-top:10px}.msg{padding:6px 8px;margin-bottom:4px;border-radius:5px;font-size:12px;line-height:1.5;word-break:break-word}.msg.user{background:#1c1c3a;border-left:2px solid var(--blue)}.msg.assistant{background:#1a2a1a;border-left:2px solid var(--green)}.msg .msg-role{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px}.msg.user .msg-role{color:var(--blue)}.msg.assistant .msg-role{color:var(--green)}footer{display:flex;align-items:center;gap:32px;padding:8px 20px;background:var(--panel);font-size:12px}.token-item{display:flex;align-items:center;gap:6px}.token-label{color:var(--muted)}.token-val{font-family:"SF Mono",Menlo,monospace;font-weight:600}.token-val.input{color:var(--blue)}.token-val.output{color:var(--green)}.token-val.cost{color:var(--amber)}.empty{color:var(--muted);text-align:center;padding:30px 10px;font-size:12px}</style></head><body><div id="app"><header> <div class="logo"><span>&#9835;</span> Tutti Studio</div> <div class="meta" id="score-name"></div> <div class="status"><div class="dot" id="sse-dot"></div><span id="sse-label">connecting</span></div></header><main> <div class="panel" id="graph-panel"> <div class="panel-title">Agent Graph</div> <div class="panel-body" id="graph-body"></div> </div> <div class="panel" id="events-panel"> <div class="panel-title">Live Event Stream</div> <div id="event-stream"><div class="empty">Waiting for events&hellip;<br>Send a message below to start an agent run.</div></div> <div id="input-bar"> <select id="agent-select"></select> <input id="user-input" placeholder="Type a message&hellip;" autocomplete="off"> <button id="send-btn">Send</button> </div> </div> <div class="panel" id="sessions-panel"> <div class="panel-title">Sessions</div> <div class="panel-body" id="sessions-body"><div class="empty">No sessions yet</div></div> </div></main><footer> <div class="token-item"><span class="token-label">&#x2193; Input</span><span class="token-val input" id="tok-in">0</span></div> <div class="token-item"><span class="token-label">&#x2191; Output</span><span class="token-val output" id="tok-out">0</span></div> <div class="token-item"><span class="token-label">$ Est. cost</span><span class="token-val cost" id="tok-cost">0.0000</span></div></footer></div><script>(function(){var tokIn=0,tokOut=0;var sessionMap={};var activeSession=null;/* ---- helpers ---- */function esc(s){var d=document.createElement("div");d.textContent=s;return d.innerHTML}function fmt(n){return n.toLocaleString()}function timeStr(){var d=new Date();return ("0"+d.getHours()).slice(-2)+":"+("0"+d.getMinutes()).slice(-2)+":"+("0"+d.getSeconds()).slice(-2)}function truncId(id){return id.slice(0,8)}/* ---- score + graph ---- */function loadScore(){ fetch("/api/score").then(function(r){return r.json()}).then(function(s){ document.getElementById("score-name").textContent=s.name||"tutti.score.ts"; var sel=document.getElementById("agent-select"); sel.innerHTML=""; Object.keys(s.agents).forEach(function(id){ var o=document.createElement("option");o.value=id;o.textContent=s.agents[id].name;sel.appendChild(o); }); renderGraph(s); });}function renderGraph(score){ var body=document.getElementById("graph-body"); var W=260,ids=Object.keys(score.agents),N=ids.length; if(N===0){body.innerHTML="<div class=\\"empty\\">No agents</div>";return} var hasDelegate=false; ids.forEach(function(id){if(score.agents[id].delegates&&score.agents[id].delegates.length)hasDelegate=true}); var nodeR=26,padY=90,padTop=50; var leftIds=[],rightIds=[]; if(hasDelegate){ ids.forEach(function(id){var a=score.agents[id];if(a.delegates&&a.delegates.length)leftIds.push(id);else rightIds.push(id)}); }else{leftIds=ids} var cols=hasDelegate?2:1; var cx1=cols===1?W/2:72,cx2=W-72; var H=Math.max(leftIds.length,rightIds.length)*padY+padTop*2; if(H<200)H=200; var pos={}; var svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+W+'" height="'+H+'" viewBox="0 0 '+W+" "+H+'">'; svg+='<defs><marker id="ah" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0 0L10 5L0 10z" fill="#64748b"/></marker></defs>'; function drawNode(id,cx,cy){ var a=score.agents[id]; var col=a.role==="orchestrator"?"#8b5cf6":"#14b8a6"; pos[id]={x:cx,y:cy}; svg+='<circle cx="'+cx+'" cy="'+cy+'" r="'+nodeR+'" fill="'+col+'" fill-opacity="0.15" stroke="'+col+'" stroke-width="2"/>'; svg+='<text x="'+cx+'" y="'+(cy+4)+'" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">'+esc(a.name)+'</text>'; var model=a.model||score.default_model||""; if(model){var sh=model.replace(/-\\d{8}$/,"");if(sh.length>18)sh=sh.slice(0,18)+"\\u2026";svg+='<text x="'+cx+'" y="'+(cy+nodeR+14)+'" text-anchor="middle" fill="#64748b" font-size="9">'+esc(sh)+'</text>'} svg+='<text x="'+cx+'" y="'+(cy+nodeR+26)+'" text-anchor="middle" fill="#64748b" font-size="9">'+a.voice_count+" voice"+(a.voice_count!==1?"s":"")+'</text>'; } leftIds.forEach(function(id,i){drawNode(id,cx1,padTop+i*padY)}); rightIds.forEach(function(id,i){drawNode(id,cx2,padTop+i*padY)}); ids.forEach(function(id){ var a=score.agents[id]; if(a.delegates)a.delegates.forEach(function(did){ if(pos[id]&&pos[did]){ var x1=pos[id].x+nodeR,y1=pos[id].y,x2=pos[did].x-nodeR,y2=pos[did].y; var mx=(x1+x2)/2; svg+='<path d="M'+x1+" "+y1+" C"+mx+" "+y1+" "+mx+" "+y2+" "+x2+" "+y2+'" fill="none" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4 3" marker-end="url(#ah)"/>'; } }); }); svg+="</svg>"; body.innerHTML=svg;}/* ---- SSE ---- */function connectSSE(){ var es=new EventSource("/events"); es.addEventListener("tutti",function(e){ var ev=JSON.parse(e.data); addEvent(ev); if(ev.type==="llm:response"&&ev.response&&ev.response.usage){ tokIn+=ev.response.usage.input_tokens||0; tokOut+=ev.response.usage.output_tokens||0; document.getElementById("tok-in").textContent=fmt(tokIn); document.getElementById("tok-out").textContent=fmt(tokOut); document.getElementById("tok-cost").textContent=estimateCost(tokIn,tokOut); } if(ev.type==="agent:start"||ev.type==="agent:end")refreshSessions(); }); es.onopen=function(){document.getElementById("sse-dot").className="dot";document.getElementById("sse-label").textContent="connected"}; es.onerror=function(){document.getElementById("sse-dot").className="dot off";document.getElementById("sse-label").textContent="disconnected"};}function estimateCost(inp,out){ var c=(inp/1e6)*3+(out/1e6)*15; return c.toFixed(4);}function evClass(t){ if(t.indexOf("agent")===0)return "agent"; if(t.indexOf("turn")===0)return "turn"; if(t==="llm:request"||t==="llm:response")return "llm"; if(t==="tool:error")return "tool-error"; if(t.indexOf("tool")===0)return "tool"; if(t.indexOf("security")===0)return "security"; if(t==="budget:warning")return "budget-warn"; if(t==="budget:exceeded")return "budget-exceed"; if(t.indexOf("delegate")===0)return "delegate"; return "";}function evDetail(ev){ var parts=[]; if(ev.agent_name)parts.push("agent: "+ev.agent_name); if(ev.session_id)parts.push("session: "+truncId(ev.session_id)); if(ev.turn!==undefined)parts.push("turn: "+ev.turn); if(ev.tool_name)parts.push("tool: "+ev.tool_name); if(ev.from)parts.push("from: "+ev.from); if(ev.to)parts.push("to: "+ev.to); if(ev.tokens!==undefined)parts.push("tokens: "+fmt(ev.tokens)); if(ev.cost_usd!==undefined)parts.push("cost: $"+ev.cost_usd.toFixed(4)); if(ev.response&&ev.response.usage)parts.push("tokens: "+fmt(ev.response.usage.input_tokens)+" in / "+fmt(ev.response.usage.output_tokens)+" out"); if(ev.error){var em=typeof ev.error==="object"?ev.error.message||"":ev.error;if(em)parts.push("error: "+em)} if(ev.patterns)parts.push("patterns: "+ev.patterns.join(", ")); return parts.join(" &middot; ");}var firstEvent=true;function addEvent(ev){ var stream=document.getElementById("event-stream"); if(firstEvent){stream.innerHTML="";firstEvent=false} var div=document.createElement("div"); div.className="ev "+evClass(ev.type); div.innerHTML='<div class="ev-head"><span class="ev-type">'+esc(ev.type)+'</span><span class="ev-time">'+timeStr()+'</span></div>'; var det=evDetail(ev); if(det)div.innerHTML+='<div class="ev-detail">'+det+"</div>"; stream.appendChild(div); stream.scrollTop=stream.scrollHeight;}/* ---- sessions ---- */function refreshSessions(){ fetch("/api/sessions").then(function(r){return r.json()}).then(function(list){ var body=document.getElementById("sessions-body"); if(!list.length){body.innerHTML='<div class="empty">No sessions yet</div>';return} var html=""; list.forEach(function(s){ var cls="session-item"+(activeSession===s.id?" active":""); html+='<div class="'+cls+'" data-id="'+s.id+'">'; html+='<div class="session-id">'+truncId(s.id)+"</div>"; html+='<div class="session-meta">'+esc(s.agent_name)+" &middot; "+s.message_count+" msgs</div>"; html+="</div>"; }); if(activeSession)html+='<div id="session-detail"></div>'; body.innerHTML=html; body.querySelectorAll(".session-item").forEach(function(el){ el.addEventListener("click",function(){selectSession(el.getAttribute("data-id"))}); }); if(activeSession)loadSessionDetail(activeSession); });}function selectSession(id){ activeSession=activeSession===id?null:id; refreshSessions();}function loadSessionDetail(id){ var det=document.getElementById("session-detail"); if(!det)return; fetch("/api/sessions/"+id).then(function(r){return r.json()}).then(function(session){ if(!session||session.error){det.innerHTML='<div class="empty">Session not found</div>';return} var html=""; (session.messages||[]).forEach(function(m){ var role=m.role; var text=""; if(typeof m.content==="string")text=m.content; else if(Array.isArray(m.content)){ m.content.forEach(function(b){ if(b.type==="text")text+=b.text+"\\n"; else if(b.type==="tool_use")text+="[tool_use: "+b.name+"]\\n"; else if(b.type==="tool_result")text+="[tool_result]\\n"; }); } html+='<div class="msg '+role+'"><div class="msg-role">'+role+"</div>"+esc(text.trim())+"</div>"; }); det.innerHTML=html; });}/* ---- send ---- */function sendMessage(){ var agentSel=document.getElementById("agent-select"); var inputEl=document.getElementById("user-input"); var btn=document.getElementById("send-btn"); var agent=agentSel.value; var input=inputEl.value.trim(); if(!input)return; btn.disabled=true;btn.textContent="Running\\u2026"; inputEl.value=""; var sid=sessionMap[agent]||undefined; fetch("/api/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agent:agent,input:input,session_id:sid})}) .then(function(r){return r.json()}) .then(function(result){ if(result.session_id)sessionMap[agent]=result.session_id; if(result.output){ addEvent({type:"__output",agent_name:agent,output:result.output}); } refreshSessions(); }) .catch(function(err){addEvent({type:"__error",error:err.message||String(err)})}) .finally(function(){btn.disabled=false;btn.textContent="Send"});}document.getElementById("send-btn").addEventListener("click",sendMessage);document.getElementById("user-input").addEventListener("keydown",function(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();sendMessage()}});/* ---- init ---- */loadScore();connectSSE();})();</script></body></html>`;
1142
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tutti Studio</title><style>*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#0a0a0f;--panel:#12121a;--card:#1a1a26;--input:#0f0f17;--border:#2a2a3a;--text:#e2e8f0;--muted:#64748b;--purple:#8b5cf6;--teal:#14b8a6;--blue:#3b82f6;--green:#10b981;--red:#ef4444;--orange:#f97316;--amber:#f59e0b;--indigo:#6366f1;}html,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);font-size:13px}#app{display:flex;flex-direction:column;height:100vh}header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;border-bottom:1px solid var(--border);background:var(--panel)}header .logo{font-weight:700;font-size:15px;letter-spacing:.5px}header .logo span{color:var(--purple)}header .meta{color:var(--muted);font-size:12px}header .status{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}header .dot{width:7px;height:7px;border-radius:50%;background:var(--green)}header .dot.off{background:var(--red)}main{display:grid;grid-template-columns:260px 1fr 280px;flex:1;overflow:hidden;border-bottom:1px solid var(--border)}.panel{display:flex;flex-direction:column;border-right:1px solid var(--border);overflow:hidden}.panel:last-child{border-right:none}.panel-title{padding:10px 14px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);border-bottom:1px solid var(--border);background:var(--panel);flex-shrink:0}.panel-body{flex:1;overflow-y:auto;padding:10px}.panel-body::-webkit-scrollbar{width:5px}.panel-body::-webkit-scrollbar-track{background:transparent}.panel-body::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}#graph-panel .panel-body{padding:0;display:flex;align-items:center;justify-content:center}#graph-panel svg text{font-family:system-ui,-apple-system,sans-serif}#events-panel{display:flex;flex-direction:column}#event-stream{flex:1;overflow-y:auto;padding:10px}#event-stream::-webkit-scrollbar{width:5px}#event-stream::-webkit-scrollbar-track{background:transparent}#event-stream::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.ev{padding:7px 10px;margin-bottom:6px;border-radius:6px;background:var(--card);border-left:3px solid var(--muted);font-size:12px;line-height:1.5}.ev .ev-head{display:flex;justify-content:space-between;align-items:center}.ev .ev-type{font-weight:600;font-family:"SF Mono",Menlo,monospace;font-size:11px}.ev .ev-time{color:var(--muted);font-size:10px;font-family:"SF Mono",Menlo,monospace}.ev .ev-detail{color:var(--muted);margin-top:3px;font-size:11px;word-break:break-all}.ev.agent{border-left-color:var(--purple)}.ev.agent .ev-type{color:var(--purple)}.ev.turn{border-left-color:var(--blue)}.ev.turn .ev-type{color:var(--blue)}.ev.llm{border-left-color:var(--green)}.ev.llm .ev-type{color:var(--green)}.ev.tool{border-left-color:var(--teal)}.ev.tool .ev-type{color:var(--teal)}.ev.tool-error{border-left-color:var(--red)}.ev.tool-error .ev-type{color:var(--red)}.ev.security{border-left-color:var(--orange)}.ev.security .ev-type{color:var(--orange)}.ev.budget-warn{border-left-color:var(--amber)}.ev.budget-warn .ev-type{color:var(--amber)}.ev.budget-exceed{border-left-color:var(--red)}.ev.budget-exceed .ev-type{color:var(--red)}.ev.delegate{border-left-color:var(--indigo)}.ev.delegate .ev-type{color:var(--indigo)}#input-bar{display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--border);background:var(--panel);flex-shrink:0}#agent-select{background:var(--input);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:12px;outline:none;cursor:pointer;min-width:110px}#user-input{flex:1;background:var(--input);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:13px;outline:none}#user-input:focus{border-color:var(--purple)}#send-btn{background:var(--purple);color:#fff;border:none;border-radius:6px;padding:6px 16px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}#send-btn:hover{opacity:.9}#send-btn:disabled{opacity:.4;cursor:default}.session-item{padding:8px 10px;margin-bottom:4px;border-radius:6px;background:var(--card);cursor:pointer;transition:background .15s}.session-item:hover{background:#22223a}.session-item.active{background:#22223a;border:1px solid var(--purple)}.session-id{font-family:"SF Mono",Menlo,monospace;font-size:11px;color:var(--purple)}.session-meta{font-size:11px;color:var(--muted);margin-top:2px}#session-detail{margin-top:10px;border-top:1px solid var(--border);padding-top:10px}.msg{padding:6px 8px;margin-bottom:4px;border-radius:5px;font-size:12px;line-height:1.5;word-break:break-word}.msg.user{background:#1c1c3a;border-left:2px solid var(--blue)}.msg.assistant{background:#1a2a1a;border-left:2px solid var(--green)}.msg .msg-role{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px}.msg.user .msg-role{color:var(--blue)}.msg.assistant .msg-role{color:var(--green)}footer{display:flex;align-items:center;gap:32px;padding:8px 20px;background:var(--panel);font-size:12px}.token-item{display:flex;align-items:center;gap:6px}.token-label{color:var(--muted)}.token-val{font-family:"SF Mono",Menlo,monospace;font-weight:600}.token-val.input{color:var(--blue)}.token-val.output{color:var(--green)}.token-val.cost{color:var(--amber)}.empty{color:var(--muted);text-align:center;padding:30px 10px;font-size:12px}</style></head><body><div id="app"><header> <div class="logo"><span>&#9835;</span> Tutti Studio</div> <div class="meta" id="score-name"></div> <div class="status"><div class="dot" id="sse-dot"></div><span id="sse-label">connecting</span></div></header><main> <div class="panel" id="graph-panel"> <div class="panel-title">Agent Graph</div> <div class="panel-body" id="graph-body"></div> </div> <div class="panel" id="events-panel"> <div class="panel-title">Live Event Stream</div> <div id="event-stream"><div class="empty">Waiting for events&hellip;<br>Send a message below to start an agent run.</div></div> <div id="input-bar"> <select id="agent-select"></select> <input id="user-input" placeholder="Type a message&hellip;" autocomplete="off"> <button id="send-btn">Send</button> </div> </div> <div class="panel" id="sessions-panel"> <div class="panel-title">Sessions</div> <div class="panel-body" id="sessions-body"><div class="empty">No sessions yet</div></div> </div></main><footer> <div class="token-item"><span class="token-label">&#x2193; Input</span><span class="token-val input" id="tok-in">0</span></div> <div class="token-item"><span class="token-label">&#x2191; Output</span><span class="token-val output" id="tok-out">0</span></div> <div class="token-item"><span class="token-label">$ Est. cost</span><span class="token-val cost" id="tok-cost">0.0000</span></div></footer></div><script>(function(){var tokIn=0,tokOut=0;var sessionMap={};var activeSession=null;/* ---- helpers ---- */function esc(s){var d=document.createElement("div");d.textContent=s;return d.innerHTML}function fmt(n){return n.toLocaleString()}function timeStr(){var d=new Date();return ("0"+d.getHours()).slice(-2)+":"+("0"+d.getMinutes()).slice(-2)+":"+("0"+d.getSeconds()).slice(-2)}function truncId(id){return id.slice(0,8)}/* ---- score + graph ---- */function loadScore(){ fetch("/api/score").then(function(r){return r.json()}).then(function(s){ document.getElementById("score-name").textContent=s.name||"tutti.score.ts"; var sel=document.getElementById("agent-select"); sel.innerHTML=""; Object.keys(s.agents).forEach(function(id){ var o=document.createElement("option");o.value=id;o.textContent=s.agents[id].name;sel.appendChild(o); }); renderGraph(s); });}function renderGraph(score){ var body=document.getElementById("graph-body"); var W=260,ids=Object.keys(score.agents),N=ids.length; if(N===0){body.innerHTML="<div class=\\"empty\\">No agents</div>";return} var hasDelegate=false; ids.forEach(function(id){if(score.agents[id].delegates&&score.agents[id].delegates.length)hasDelegate=true}); var nodeR=26,padY=90,padTop=50; var leftIds=[],rightIds=[]; if(hasDelegate){ ids.forEach(function(id){var a=score.agents[id];if(a.delegates&&a.delegates.length)leftIds.push(id);else rightIds.push(id)}); }else{leftIds=ids} var cols=hasDelegate?2:1; var cx1=cols===1?W/2:72,cx2=W-72; var H=Math.max(leftIds.length,rightIds.length)*padY+padTop*2; if(H<200)H=200; var pos={}; var svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+W+'" height="'+H+'" viewBox="0 0 '+W+" "+H+'">'; svg+='<defs><marker id="ah" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0 0L10 5L0 10z" fill="#64748b"/></marker></defs>'; function drawNode(id,cx,cy){ var a=score.agents[id]; var col=a.role==="orchestrator"?"#8b5cf6":"#14b8a6"; pos[id]={x:cx,y:cy}; svg+='<circle cx="'+cx+'" cy="'+cy+'" r="'+nodeR+'" fill="'+col+'" fill-opacity="0.15" stroke="'+col+'" stroke-width="2"/>'; svg+='<text x="'+cx+'" y="'+(cy+4)+'" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">'+esc(a.name)+'</text>'; var model=a.model||score.default_model||""; if(model){var sh=model.replace(/-\\d{8}$/,"");if(sh.length>18)sh=sh.slice(0,18)+"\\u2026";svg+='<text x="'+cx+'" y="'+(cy+nodeR+14)+'" text-anchor="middle" fill="#64748b" font-size="9">'+esc(sh)+'</text>'} svg+='<text x="'+cx+'" y="'+(cy+nodeR+26)+'" text-anchor="middle" fill="#64748b" font-size="9">'+a.voice_count+" voice"+(a.voice_count!==1?"s":"")+'</text>'; } leftIds.forEach(function(id,i){drawNode(id,cx1,padTop+i*padY)}); rightIds.forEach(function(id,i){drawNode(id,cx2,padTop+i*padY)}); ids.forEach(function(id){ var a=score.agents[id]; if(a.delegates)a.delegates.forEach(function(did){ if(pos[id]&&pos[did]){ var x1=pos[id].x+nodeR,y1=pos[id].y,x2=pos[did].x-nodeR,y2=pos[did].y; var mx=(x1+x2)/2; svg+='<path d="M'+x1+" "+y1+" C"+mx+" "+y1+" "+mx+" "+y2+" "+x2+" "+y2+'" fill="none" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4 3" marker-end="url(#ah)"/>'; } }); }); svg+="</svg>"; body.innerHTML=svg;}/* ---- SSE ---- */function connectSSE(){ var es=new EventSource("/events"); es.addEventListener("tutti",function(e){ var ev=JSON.parse(e.data); addEvent(ev); if(ev.type==="llm:response"&&ev.response&&ev.response.usage){ tokIn+=ev.response.usage.input_tokens||0; tokOut+=ev.response.usage.output_tokens||0; document.getElementById("tok-in").textContent=fmt(tokIn); document.getElementById("tok-out").textContent=fmt(tokOut); document.getElementById("tok-cost").textContent=estimateCost(tokIn,tokOut); } if(ev.type==="agent:start"||ev.type==="agent:end")refreshSessions(); }); es.onopen=function(){document.getElementById("sse-dot").className="dot";document.getElementById("sse-label").textContent="connected"}; es.onerror=function(){document.getElementById("sse-dot").className="dot off";document.getElementById("sse-label").textContent="disconnected"};}/* Pricing: USD per 1M tokens (Sonnet-class default) */var INPUT_PRICE_PER_MILLION=3;var OUTPUT_PRICE_PER_MILLION=15;function estimateCost(inp,out){ var c=(inp/1e6)*INPUT_PRICE_PER_MILLION+(out/1e6)*OUTPUT_PRICE_PER_MILLION; return c.toFixed(4);}function evClass(t){ if(t.indexOf("agent")===0)return "agent"; if(t.indexOf("turn")===0)return "turn"; if(t==="llm:request"||t==="llm:response")return "llm"; if(t==="tool:error")return "tool-error"; if(t.indexOf("tool")===0)return "tool"; if(t.indexOf("security")===0)return "security"; if(t==="budget:warning")return "budget-warn"; if(t==="budget:exceeded")return "budget-exceed"; if(t.indexOf("delegate")===0)return "delegate"; return "";}function evDetail(ev){ var parts=[]; if(ev.agent_name)parts.push("agent: "+ev.agent_name); if(ev.session_id)parts.push("session: "+truncId(ev.session_id)); if(ev.turn!==undefined)parts.push("turn: "+ev.turn); if(ev.tool_name)parts.push("tool: "+ev.tool_name); if(ev.from)parts.push("from: "+ev.from); if(ev.to)parts.push("to: "+ev.to); if(ev.tokens!==undefined)parts.push("tokens: "+fmt(ev.tokens)); if(ev.cost_usd!==undefined)parts.push("cost: $"+ev.cost_usd.toFixed(4)); if(ev.response&&ev.response.usage)parts.push("tokens: "+fmt(ev.response.usage.input_tokens)+" in / "+fmt(ev.response.usage.output_tokens)+" out"); if(ev.error){var em=typeof ev.error==="object"?ev.error.message||"":ev.error;if(em)parts.push("error: "+em)} if(ev.patterns)parts.push("patterns: "+ev.patterns.join(", ")); return parts.join(" &middot; ");}var firstEvent=true;function addEvent(ev){ var stream=document.getElementById("event-stream"); if(firstEvent){stream.innerHTML="";firstEvent=false} var div=document.createElement("div"); div.className="ev "+evClass(ev.type); div.innerHTML='<div class="ev-head"><span class="ev-type">'+esc(ev.type)+'</span><span class="ev-time">'+timeStr()+'</span></div>'; var det=evDetail(ev); if(det)div.innerHTML+='<div class="ev-detail">'+det+"</div>"; stream.appendChild(div); stream.scrollTop=stream.scrollHeight;}/* ---- sessions ---- */function refreshSessions(){ fetch("/api/sessions").then(function(r){return r.json()}).then(function(list){ var body=document.getElementById("sessions-body"); if(!list.length){body.innerHTML='<div class="empty">No sessions yet</div>';return} var html=""; list.forEach(function(s){ var cls="session-item"+(activeSession===s.id?" active":""); html+='<div class="'+cls+'" data-id="'+s.id+'">'; html+='<div class="session-id">'+truncId(s.id)+"</div>"; html+='<div class="session-meta">'+esc(s.agent_name)+" &middot; "+s.message_count+" msgs</div>"; html+="</div>"; }); if(activeSession)html+='<div id="session-detail"></div>'; body.innerHTML=html; body.querySelectorAll(".session-item").forEach(function(el){ el.addEventListener("click",function(){selectSession(el.getAttribute("data-id"))}); }); if(activeSession)loadSessionDetail(activeSession); });}function selectSession(id){ activeSession=activeSession===id?null:id; refreshSessions();}function loadSessionDetail(id){ var det=document.getElementById("session-detail"); if(!det)return; fetch("/api/sessions/"+id).then(function(r){return r.json()}).then(function(session){ if(!session||session.error){det.innerHTML='<div class="empty">Session not found</div>';return} var html=""; (session.messages||[]).forEach(function(m){ var role=m.role; var text=""; if(typeof m.content==="string")text=m.content; else if(Array.isArray(m.content)){ m.content.forEach(function(b){ if(b.type==="text")text+=b.text+"\\n"; else if(b.type==="tool_use")text+="[tool_use: "+b.name+"]\\n"; else if(b.type==="tool_result")text+="[tool_result]\\n"; }); } html+='<div class="msg '+role+'"><div class="msg-role">'+role+"</div>"+esc(text.trim())+"</div>"; }); det.innerHTML=html; });}/* ---- send ---- */function sendMessage(){ var agentSel=document.getElementById("agent-select"); var inputEl=document.getElementById("user-input"); var btn=document.getElementById("send-btn"); var agent=agentSel.value; var input=inputEl.value.trim(); if(!input)return; btn.disabled=true;btn.textContent="Running\\u2026"; inputEl.value=""; var sid=sessionMap[agent]||undefined; fetch("/api/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agent:agent,input:input,session_id:sid})}) .then(function(r){return r.json()}) .then(function(result){ if(result.session_id)sessionMap[agent]=result.session_id; if(result.output){ addEvent({type:"__output",agent_name:agent,output:result.output}); } refreshSessions(); }) .catch(function(err){addEvent({type:"__error",error:err.message||String(err)})}) .finally(function(){btn.disabled=false;btn.textContent="Send"});}document.getElementById("send-btn").addEventListener("click",sendMessage);document.getElementById("user-input").addEventListener("keydown",function(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();sendMessage()}});/* ---- init ---- */loadScore();connectSSE();})();</script></body></html>`;
612
1143
  }
613
1144
 
614
1145
  // src/commands/search.ts
615
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
616
- import { resolve as resolve5 } from "path";
617
- import chalk6 from "chalk";
618
- import ora3 from "ora";
619
- import { createLogger as createLogger6 } from "@tuttiai/core";
620
- var logger6 = createLogger6("tutti-cli");
1146
+ import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
1147
+ import { resolve as resolve6 } from "path";
1148
+ import chalk7 from "chalk";
1149
+ import ora4 from "ora";
1150
+ import { createLogger as createLogger7 } from "@tuttiai/core";
1151
+ var logger7 = createLogger7("tutti-cli");
621
1152
  var REGISTRY_URL = "https://raw.githubusercontent.com/tuttiai/voices/main/voices.json";
622
1153
  var BUILTIN_VOICES = [
623
1154
  {
@@ -668,7 +1199,7 @@ async function fetchRegistry() {
668
1199
  if (voices.length === 0) throw new Error("Empty registry");
669
1200
  return voices;
670
1201
  } catch {
671
- logger6.debug("Registry unreachable, using built-in voice list");
1202
+ logger7.debug("Registry unreachable, using built-in voice list");
672
1203
  return BUILTIN_VOICES;
673
1204
  }
674
1205
  }
@@ -684,8 +1215,8 @@ function matchesQuery(voice, query) {
684
1215
  return false;
685
1216
  }
686
1217
  function isInstalled(packageName) {
687
- const pkgPath = resolve5(process.cwd(), "package.json");
688
- if (!existsSync6(pkgPath)) return false;
1218
+ const pkgPath = resolve6(process.cwd(), "package.json");
1219
+ if (!existsSync7(pkgPath)) return false;
689
1220
  try {
690
1221
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
691
1222
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -695,35 +1226,35 @@ function isInstalled(packageName) {
695
1226
  }
696
1227
  }
697
1228
  function printVoice(voice, showInstallStatus) {
698
- const badge = voice.official ? chalk6.green(" [official]") : chalk6.blue(" [community]");
1229
+ const badge = voice.official ? chalk7.green(" [official]") : chalk7.blue(" [community]");
699
1230
  const installed = showInstallStatus && isInstalled(voice.package);
700
- const status = showInstallStatus ? installed ? chalk6.green(" \u2714 installed") : chalk6.dim(" not installed") : "";
1231
+ const status = showInstallStatus ? installed ? chalk7.green(" \u2714 installed") : chalk7.dim(" not installed") : "";
701
1232
  console.log();
702
- console.log(" " + chalk6.bold(voice.package) + badge + status);
1233
+ console.log(" " + chalk7.bold(voice.package) + badge + status);
703
1234
  console.log(" " + voice.description);
704
1235
  const installCmd = voice.official && voice.name !== "postgres" ? "tutti-ai add " + voice.name : "npm install " + voice.package;
705
- console.log(" " + chalk6.dim("Install: ") + chalk6.cyan(installCmd));
1236
+ console.log(" " + chalk7.dim("Install: ") + chalk7.cyan(installCmd));
706
1237
  if (voice.tags.length > 0) {
707
- console.log(" " + chalk6.dim("Tags: ") + voice.tags.join(", "));
1238
+ console.log(" " + chalk7.dim("Tags: ") + voice.tags.join(", "));
708
1239
  }
709
1240
  }
710
1241
  async function searchCommand(query) {
711
- const spinner = ora3("Searching the Repertoire...").start();
1242
+ const spinner = ora4("Searching the Repertoire...").start();
712
1243
  const voices = await fetchRegistry();
713
1244
  const results = voices.filter((v) => matchesQuery(v, query));
714
1245
  spinner.stop();
715
1246
  if (results.length === 0) {
716
1247
  console.log();
717
- console.log(chalk6.yellow(' No voices found for "' + query + '"'));
1248
+ console.log(chalk7.yellow(' No voices found for "' + query + '"'));
718
1249
  console.log();
719
- console.log(chalk6.dim(" Browse all: https://tutti-ai.com/voices"));
720
- console.log(chalk6.dim(" Build your own: tutti-ai create voice <name>"));
1250
+ console.log(chalk7.dim(" Browse all: https://tutti-ai.com/voices"));
1251
+ console.log(chalk7.dim(" Build your own: tutti-ai create voice <name>"));
721
1252
  console.log();
722
1253
  return;
723
1254
  }
724
1255
  console.log();
725
1256
  console.log(
726
- " Found " + chalk6.bold(String(results.length)) + " voice" + (results.length !== 1 ? "s" : "") + " matching " + chalk6.cyan("'" + query + "'") + ":"
1257
+ " Found " + chalk7.bold(String(results.length)) + " voice" + (results.length !== 1 ? "s" : "") + " matching " + chalk7.cyan("'" + query + "'") + ":"
727
1258
  );
728
1259
  for (const voice of results) {
729
1260
  printVoice(voice, false);
@@ -731,12 +1262,12 @@ async function searchCommand(query) {
731
1262
  console.log();
732
1263
  }
733
1264
  async function voicesCommand() {
734
- const spinner = ora3("Loading voices...").start();
1265
+ const spinner = ora4("Loading voices...").start();
735
1266
  const voices = await fetchRegistry();
736
1267
  const official = voices.filter((v) => v.official);
737
1268
  spinner.stop();
738
1269
  console.log();
739
- console.log(" " + chalk6.bold("Official Tutti Voices"));
1270
+ console.log(" " + chalk7.bold("Official Tutti Voices"));
740
1271
  console.log();
741
1272
  for (const voice of official) {
742
1273
  printVoice(voice, true);
@@ -744,49 +1275,49 @@ async function voicesCommand() {
744
1275
  const community = voices.filter((v) => !v.official);
745
1276
  if (community.length > 0) {
746
1277
  console.log();
747
- console.log(" " + chalk6.bold("Community Voices"));
1278
+ console.log(" " + chalk7.bold("Community Voices"));
748
1279
  for (const voice of community) {
749
1280
  printVoice(voice, true);
750
1281
  }
751
1282
  }
752
1283
  console.log();
753
- console.log(chalk6.dim(" Search: tutti-ai search <query>"));
754
- console.log(chalk6.dim(" Browse: https://tutti-ai.com/voices"));
1284
+ console.log(chalk7.dim(" Search: tutti-ai search <query>"));
1285
+ console.log(chalk7.dim(" Browse: https://tutti-ai.com/voices"));
755
1286
  console.log();
756
1287
  }
757
1288
 
758
1289
  // src/commands/publish.ts
759
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
760
- import { resolve as resolve6 } from "path";
1290
+ import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
1291
+ import { resolve as resolve7 } from "path";
761
1292
  import { execSync as execSync2 } from "child_process";
762
- import chalk7 from "chalk";
763
- import ora4 from "ora";
1293
+ import chalk8 from "chalk";
1294
+ import ora5 from "ora";
764
1295
  import Enquirer2 from "enquirer";
765
- import { createLogger as createLogger7, SecretsManager as SecretsManager3 } from "@tuttiai/core";
1296
+ import { createLogger as createLogger8, SecretsManager as SecretsManager4 } from "@tuttiai/core";
766
1297
  var { prompt: prompt2 } = Enquirer2;
767
- var logger7 = createLogger7("tutti-cli");
1298
+ var logger8 = createLogger8("tutti-cli");
768
1299
  function readPkg(dir) {
769
- const p = resolve6(dir, "package.json");
770
- if (!existsSync7(p)) return void 0;
1300
+ const p = resolve7(dir, "package.json");
1301
+ if (!existsSync8(p)) return void 0;
771
1302
  return JSON.parse(readFileSync3(p, "utf-8"));
772
1303
  }
773
1304
  function run(cmd, cwd) {
774
1305
  return execSync2(cmd, { cwd, stdio: "pipe", encoding: "utf-8" });
775
1306
  }
776
1307
  function fail2(msg) {
777
- console.error(chalk7.red(" " + msg));
1308
+ console.error(chalk8.red(" " + msg));
778
1309
  process.exit(1);
779
1310
  }
780
- var ok2 = (msg) => console.log(chalk7.green(" \u2714 " + msg));
1311
+ var ok2 = (msg) => console.log(chalk8.green(" \u2714 " + msg));
781
1312
  async function publishCommand(opts) {
782
1313
  const cwd = process.cwd();
783
1314
  const pkg = readPkg(cwd);
784
1315
  console.log();
785
- console.log(chalk7.bold(" Tutti Voice Publisher"));
1316
+ console.log(chalk8.bold(" Tutti Voice Publisher"));
786
1317
  console.log();
787
- const spinner = ora4("Running pre-flight checks...").start();
1318
+ const spinner = ora5("Running pre-flight checks...").start();
788
1319
  if (!pkg) fail2("No package.json found in the current directory.");
789
- if (!existsSync7(resolve6(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
1320
+ if (!existsSync8(resolve7(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
790
1321
  const missing = [];
791
1322
  if (!pkg.name) missing.push("name");
792
1323
  if (!pkg.version) missing.push("version");
@@ -798,22 +1329,22 @@ async function publishCommand(opts) {
798
1329
  const version = pkg.version;
799
1330
  const validName = name.startsWith("@tuttiai/") || name.startsWith("tutti");
800
1331
  if (!validName) fail2("Package name must start with @tuttiai/ or tutti \u2014 got: " + name);
801
- const src = readFileSync3(resolve6(cwd, "src/index.ts"), "utf-8");
1332
+ const src = readFileSync3(resolve7(cwd, "src/index.ts"), "utf-8");
802
1333
  if (!src.includes("required_permissions")) {
803
1334
  fail2("Voice class must declare required_permissions in src/index.ts");
804
1335
  }
805
1336
  spinner.succeed("Pre-flight checks passed");
806
- const buildSpinner = ora4("Building...").start();
1337
+ const buildSpinner = ora5("Building...").start();
807
1338
  try {
808
1339
  run("npm run build", cwd);
809
1340
  buildSpinner.succeed("Build succeeded");
810
1341
  } catch (err) {
811
1342
  buildSpinner.fail("Build failed");
812
1343
  const msg = err instanceof Error ? err.message : String(err);
813
- console.error(chalk7.dim(" " + msg.split("\n").slice(0, 5).join("\n ")));
1344
+ console.error(chalk8.dim(" " + msg.split("\n").slice(0, 5).join("\n ")));
814
1345
  process.exit(1);
815
1346
  }
816
- const testSpinner = ora4("Running tests...").start();
1347
+ const testSpinner = ora5("Running tests...").start();
817
1348
  try {
818
1349
  run("npx vitest run", cwd);
819
1350
  testSpinner.succeed("Tests passed");
@@ -821,15 +1352,15 @@ async function publishCommand(opts) {
821
1352
  testSpinner.fail("Tests failed");
822
1353
  process.exit(1);
823
1354
  }
824
- const auditSpinner = ora4("Checking vulnerabilities...").start();
1355
+ const auditSpinner = ora5("Checking vulnerabilities...").start();
825
1356
  try {
826
1357
  run("npm audit --audit-level=high", cwd);
827
1358
  auditSpinner.succeed("No high/critical vulnerabilities");
828
1359
  } catch {
829
- auditSpinner.stopAndPersist({ symbol: chalk7.yellow("\u26A0"), text: "Vulnerabilities found (npm audit)" });
1360
+ auditSpinner.stopAndPersist({ symbol: chalk8.yellow("\u26A0"), text: "Vulnerabilities found (npm audit)" });
830
1361
  }
831
1362
  console.log();
832
- const drySpinner = ora4("Packing (dry run)...").start();
1363
+ const drySpinner = ora5("Packing (dry run)...").start();
833
1364
  let packOutput;
834
1365
  try {
835
1366
  packOutput = run("npm pack --dry-run 2>&1", cwd);
@@ -837,24 +1368,24 @@ async function publishCommand(opts) {
837
1368
  } catch (err) {
838
1369
  drySpinner.fail("Pack dry-run failed");
839
1370
  const msg = err instanceof Error ? err.message : String(err);
840
- console.error(chalk7.dim(" " + msg));
1371
+ console.error(chalk8.dim(" " + msg));
841
1372
  process.exit(1);
842
1373
  }
843
1374
  const fileLines = packOutput.split("\n").filter((l) => l.includes("npm notice") && /\d+(\.\d+)?\s*[kM]?B\s/.test(l)).map((l) => l.replace(/npm notice\s*/, ""));
844
1375
  if (fileLines.length > 0) {
845
- console.log(chalk7.dim(" Files:"));
1376
+ console.log(chalk8.dim(" Files:"));
846
1377
  for (const line of fileLines) {
847
- console.log(chalk7.dim(" " + line.trim()));
1378
+ console.log(chalk8.dim(" " + line.trim()));
848
1379
  }
849
1380
  }
850
1381
  const sizeLine = packOutput.split("\n").find((l) => l.includes("package size"));
851
1382
  const totalLine = packOutput.split("\n").find((l) => l.includes("total files"));
852
- if (sizeLine) console.log(chalk7.dim(" " + sizeLine.replace(/npm notice\s*/, "").trim()));
853
- if (totalLine) console.log(chalk7.dim(" " + totalLine.replace(/npm notice\s*/, "").trim()));
1383
+ if (sizeLine) console.log(chalk8.dim(" " + sizeLine.replace(/npm notice\s*/, "").trim()));
1384
+ if (totalLine) console.log(chalk8.dim(" " + totalLine.replace(/npm notice\s*/, "").trim()));
854
1385
  if (opts.dryRun) {
855
1386
  console.log();
856
1387
  ok2("Dry run complete \u2014 no packages were published");
857
- console.log(chalk7.dim(" Run without --dry-run to publish for real."));
1388
+ console.log(chalk8.dim(" Run without --dry-run to publish for real."));
858
1389
  console.log();
859
1390
  return;
860
1391
  }
@@ -862,38 +1393,38 @@ async function publishCommand(opts) {
862
1393
  const { confirm } = await prompt2({
863
1394
  type: "confirm",
864
1395
  name: "confirm",
865
- message: "Publish " + chalk7.cyan(name + "@" + version) + "?"
1396
+ message: "Publish " + chalk8.cyan(name + "@" + version) + "?"
866
1397
  });
867
1398
  if (!confirm) {
868
- console.log(chalk7.dim(" Cancelled."));
1399
+ console.log(chalk8.dim(" Cancelled."));
869
1400
  return;
870
1401
  }
871
- const pubSpinner = ora4("Publishing to npm...").start();
1402
+ const pubSpinner = ora5("Publishing to npm...").start();
872
1403
  try {
873
1404
  run("npm publish --access public", cwd);
874
- pubSpinner.succeed("Published " + chalk7.cyan(name + "@" + version));
1405
+ pubSpinner.succeed("Published " + chalk8.cyan(name + "@" + version));
875
1406
  } catch (err) {
876
1407
  pubSpinner.fail("Publish failed");
877
1408
  const msg = err instanceof Error ? err.message : String(err);
878
- logger7.error({ error: msg }, "npm publish failed");
1409
+ logger8.error({ error: msg }, "npm publish failed");
879
1410
  process.exit(1);
880
1411
  }
881
- const ghToken = SecretsManager3.optional("GITHUB_TOKEN");
1412
+ const ghToken = SecretsManager4.optional("GITHUB_TOKEN");
882
1413
  let prUrl;
883
1414
  if (ghToken) {
884
- const prSpinner = ora4("Opening PR to voice registry...").start();
1415
+ const prSpinner = ora5("Opening PR to voice registry...").start();
885
1416
  try {
886
1417
  prUrl = await openRegistryPR(name, version, pkg.description ?? "", ghToken);
887
1418
  prSpinner.succeed("PR opened: " + prUrl);
888
1419
  } catch (err) {
889
1420
  prSpinner.fail("Failed to open PR");
890
1421
  const msg = err instanceof Error ? err.message : String(err);
891
- logger7.error({ error: msg }, "Registry PR failed");
1422
+ logger8.error({ error: msg }, "Registry PR failed");
892
1423
  }
893
1424
  } else {
894
1425
  console.log();
895
- console.log(chalk7.dim(" To list in the Repertoire, set GITHUB_TOKEN and re-run"));
896
- console.log(chalk7.dim(" Or open a PR manually: github.com/tuttiai/voices"));
1426
+ console.log(chalk8.dim(" To list in the Repertoire, set GITHUB_TOKEN and re-run"));
1427
+ console.log(chalk8.dim(" Or open a PR manually: github.com/tuttiai/voices"));
897
1428
  }
898
1429
  console.log();
899
1430
  ok2(name + "@" + version + " published to npm");
@@ -977,27 +1508,100 @@ async function openRegistryPR(packageName, version, description, token) {
977
1508
  return prData.html_url;
978
1509
  }
979
1510
 
1511
+ // src/commands/eval.ts
1512
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
1513
+ import { resolve as resolve8 } from "path";
1514
+ import chalk9 from "chalk";
1515
+ import ora6 from "ora";
1516
+ import {
1517
+ ScoreLoader as ScoreLoader5,
1518
+ EvalRunner,
1519
+ printEvalTable,
1520
+ createLogger as createLogger9
1521
+ } from "@tuttiai/core";
1522
+ var logger9 = createLogger9("tutti-cli");
1523
+ async function evalCommand(suitePath, opts) {
1524
+ const suiteFile = resolve8(suitePath);
1525
+ if (!existsSync9(suiteFile)) {
1526
+ logger9.error({ file: suiteFile }, "Suite file not found");
1527
+ process.exit(1);
1528
+ }
1529
+ let suite;
1530
+ try {
1531
+ suite = JSON.parse(readFileSync4(suiteFile, "utf-8"));
1532
+ } catch (err) {
1533
+ logger9.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to parse suite file");
1534
+ process.exit(1);
1535
+ }
1536
+ const scoreFile = resolve8(opts.score ?? "./tutti.score.ts");
1537
+ if (!existsSync9(scoreFile)) {
1538
+ logger9.error({ file: scoreFile }, "Score file not found");
1539
+ process.exit(1);
1540
+ }
1541
+ const spinner = ora6("Loading score...").start();
1542
+ let score;
1543
+ try {
1544
+ score = await ScoreLoader5.load(scoreFile);
1545
+ } catch (err) {
1546
+ spinner.fail("Failed to load score");
1547
+ logger9.error({ error: err instanceof Error ? err.message : String(err) }, "Score load failed");
1548
+ process.exit(1);
1549
+ }
1550
+ spinner.succeed("Score loaded");
1551
+ const evalSpinner = ora6("Running " + suite.cases.length + " eval cases...").start();
1552
+ const runner = new EvalRunner(score);
1553
+ const report = await runner.run(suite);
1554
+ evalSpinner.stop();
1555
+ printEvalTable(report);
1556
+ if (opts.ci && report.summary.failed > 0) {
1557
+ console.error(chalk9.red(" CI mode: " + report.summary.failed + " case(s) failed"));
1558
+ process.exit(1);
1559
+ }
1560
+ }
1561
+
980
1562
  // src/index.ts
981
1563
  config();
982
- var logger8 = createLogger8("tutti-cli");
1564
+ var logger10 = createLogger10("tutti-cli");
983
1565
  process.on("unhandledRejection", (reason) => {
984
- logger8.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
1566
+ logger10.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
985
1567
  process.exit(1);
986
1568
  });
987
1569
  process.on("uncaughtException", (err) => {
988
- logger8.error({ error: err.message }, "Fatal error");
1570
+ logger10.error({ error: err.message }, "Fatal error");
989
1571
  process.exit(1);
990
1572
  });
991
1573
  var program = new Command();
992
- program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.8.0");
993
- program.command("init [project-name]").description("Create a new Tutti project").action(async (projectName) => {
994
- await initCommand(projectName);
1574
+ program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.9.0");
1575
+ program.command("init [project-name]").description("Create a new Tutti project").option("-t, --template <id>", "Project template to use").action(async (projectName, opts) => {
1576
+ await initCommand(projectName, opts.template);
1577
+ });
1578
+ program.command("templates").description("List all available project templates").action(() => {
1579
+ templatesCommand();
995
1580
  });
996
1581
  program.command("run [score]").description("Run a Tutti score interactively").action(async (score) => {
997
1582
  await runCommand(score);
998
1583
  });
999
- program.command("add <voice>").description("Add a voice to your project").action(async (voice) => {
1000
- await addCommand(voice);
1584
+ program.command("resume <session-id>").description("Resume a crashed or interrupted run from its last checkpoint").option(
1585
+ "--store <backend>",
1586
+ "Durable store the checkpoint was written to (redis | postgres)",
1587
+ "redis"
1588
+ ).option("-s, --score <path>", "Path to score file (default: ./tutti.score.ts)").option("-a, --agent <name>", "Agent key to resume (default: score.entry or the first agent)").option("-y, --yes", "Skip the confirmation prompt").action(
1589
+ async (sessionId, opts) => {
1590
+ if (opts.store !== "redis" && opts.store !== "postgres") {
1591
+ console.error("--store must be 'redis' or 'postgres'");
1592
+ process.exit(1);
1593
+ }
1594
+ const resolved = {
1595
+ store: opts.store,
1596
+ ...opts.score !== void 0 ? { score: opts.score } : {},
1597
+ ...opts.agent !== void 0 ? { agent: opts.agent } : {},
1598
+ ...opts.yes !== void 0 ? { yes: opts.yes } : {}
1599
+ };
1600
+ await resumeCommand(sessionId, resolved);
1601
+ }
1602
+ );
1603
+ program.command("add <voice>").description("Add a voice to your project").action((voice) => {
1604
+ addCommand(voice);
1001
1605
  });
1002
1606
  program.command("check [score]").description("Validate a score file without running it").action(async (score) => {
1003
1607
  await checkCommand(score);
@@ -1017,5 +1621,8 @@ program.command("voices").description("List all available official voices and in
1017
1621
  program.command("publish").description("Publish the current voice to npm and the voice registry").option("--dry-run", "Run all checks without publishing").action(async (opts) => {
1018
1622
  await publishCommand(opts);
1019
1623
  });
1624
+ program.command("eval <suite-file>").description("Run an evaluation suite against a score").option("--ci", "Exit with code 1 if any case fails").option("-s, --score <path>", "Path to score file (default: ./tutti.score.ts)").action(async (suitePath, opts) => {
1625
+ await evalCommand(suitePath, opts);
1626
+ });
1020
1627
  program.parse();
1021
1628
  //# sourceMappingURL=index.js.map