@tuttiai/cli 0.6.0 → 0.8.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 createLogger6 } from "@tuttiai/core";
5
+ import { createLogger as createLogger9 } 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", () => {
@@ -331,7 +582,7 @@ function isAlreadyInstalled(packageName) {
331
582
  return false;
332
583
  }
333
584
  }
334
- async function addCommand(voiceName) {
585
+ function addCommand(voiceName) {
335
586
  const packageName = resolvePackageName(voiceName);
336
587
  const pkgPath = resolve2(process.cwd(), "package.json");
337
588
  if (!existsSync3(pkgPath)) {
@@ -471,7 +722,7 @@ Checking ${file}...
471
722
  // src/commands/studio.ts
472
723
  import { existsSync as existsSync5 } from "fs";
473
724
  import { resolve as resolve4 } from "path";
474
- import { exec } from "child_process";
725
+ import { execFile } from "child_process";
475
726
  import express from "express";
476
727
  import chalk5 from "chalk";
477
728
  import {
@@ -480,7 +731,8 @@ import {
480
731
  createLogger as createLogger5
481
732
  } from "@tuttiai/core";
482
733
  var logger5 = createLogger5("tutti-studio");
483
- var PORT = 4747;
734
+ var envPort = Number.parseInt(process.env.PORT ?? "", 10);
735
+ var PORT = Number.isInteger(envPort) && envPort > 0 && envPort <= 65535 ? envPort : 4747;
484
736
  function safeStringify(obj) {
485
737
  return JSON.stringify(obj, (_key, value) => {
486
738
  if (value instanceof Error) return { message: value.message, name: value.name };
@@ -489,8 +741,12 @@ function safeStringify(obj) {
489
741
  });
490
742
  }
491
743
  function openBrowser(url) {
492
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
493
- exec(cmd + " " + url);
744
+ if (process.platform === "win32") {
745
+ execFile("cmd.exe", ["/c", "start", "", url]);
746
+ return;
747
+ }
748
+ const cmd = process.platform === "darwin" ? "open" : "xdg-open";
749
+ execFile(cmd, [url]);
494
750
  }
495
751
  async function studioCommand(scorePath) {
496
752
  const file = resolve4(scorePath ?? "./tutti.score.ts");
@@ -576,9 +832,24 @@ async function studioCommand(scorePath) {
576
832
  res.json(session);
577
833
  });
578
834
  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" });
835
+ const body = req.body;
836
+ if (typeof body !== "object" || body === null) {
837
+ res.status(400).json({ error: "Invalid request body" });
838
+ return;
839
+ }
840
+ const agent = body.agent;
841
+ const input = body.input;
842
+ const session_id = body.session_id;
843
+ if (typeof agent !== "string" || agent.trim().length === 0) {
844
+ res.status(400).json({ error: "agent must be a non-empty string" });
845
+ return;
846
+ }
847
+ if (typeof input !== "string" || input.trim().length === 0) {
848
+ res.status(400).json({ error: "input must be a non-empty string" });
849
+ return;
850
+ }
851
+ if (session_id !== void 0 && (typeof session_id !== "string" || session_id.trim().length === 0)) {
852
+ res.status(400).json({ error: "session_id must be a non-empty string when provided" });
582
853
  return;
583
854
  }
584
855
  try {
@@ -608,30 +879,450 @@ async function studioCommand(scorePath) {
608
879
  });
609
880
  }
610
881
  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>`;
882
+ 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>`;
883
+ }
884
+
885
+ // src/commands/search.ts
886
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
887
+ import { resolve as resolve5 } from "path";
888
+ import chalk6 from "chalk";
889
+ import ora3 from "ora";
890
+ import { createLogger as createLogger6 } from "@tuttiai/core";
891
+ var logger6 = createLogger6("tutti-cli");
892
+ var REGISTRY_URL = "https://raw.githubusercontent.com/tuttiai/voices/main/voices.json";
893
+ var BUILTIN_VOICES = [
894
+ {
895
+ name: "filesystem",
896
+ package: "@tuttiai/filesystem",
897
+ description: "Read, write, search, and manage files and directories",
898
+ tags: ["filesystem", "files", "io", "read", "write"],
899
+ official: true,
900
+ tools: 7
901
+ },
902
+ {
903
+ name: "github",
904
+ package: "@tuttiai/github",
905
+ description: "Interact with GitHub repos, issues, PRs, and code search",
906
+ tags: ["github", "git", "code", "issues", "pull-requests", "api"],
907
+ official: true,
908
+ tools: 10
909
+ },
910
+ {
911
+ name: "playwright",
912
+ package: "@tuttiai/playwright",
913
+ description: "Control a browser like a human \u2014 navigate, click, type, screenshot",
914
+ tags: ["browser", "playwright", "web", "qa", "testing", "automation", "scraping"],
915
+ official: true,
916
+ tools: 12
917
+ },
918
+ {
919
+ name: "postgres",
920
+ package: "pg",
921
+ description: "PostgreSQL session persistence and database access",
922
+ tags: ["database", "postgres", "sql", "persistence", "sessions"],
923
+ official: true,
924
+ tools: 0
925
+ }
926
+ ];
927
+ async function fetchRegistry() {
928
+ try {
929
+ const res = await fetch(REGISTRY_URL);
930
+ if (!res.ok) throw new Error("HTTP " + res.status);
931
+ const data = await res.json();
932
+ const voices = [];
933
+ for (const entry of data.official ?? []) {
934
+ voices.push({ ...entry, official: true, tools: toolCount(entry.name) });
935
+ }
936
+ for (const entry of data.community ?? []) {
937
+ voices.push({ ...entry, official: false, tools: 0 });
938
+ }
939
+ if (voices.length === 0) throw new Error("Empty registry");
940
+ return voices;
941
+ } catch {
942
+ logger6.debug("Registry unreachable, using built-in voice list");
943
+ return BUILTIN_VOICES;
944
+ }
945
+ }
946
+ function toolCount(name) {
947
+ const counts = { filesystem: 7, github: 10, playwright: 12 };
948
+ return counts[name] ?? 0;
949
+ }
950
+ function matchesQuery(voice, query) {
951
+ const q = query.toLowerCase();
952
+ if (voice.name.toLowerCase().includes(q)) return true;
953
+ if (voice.description.toLowerCase().includes(q)) return true;
954
+ if (voice.tags.some((t) => t.toLowerCase().includes(q))) return true;
955
+ return false;
956
+ }
957
+ function isInstalled(packageName) {
958
+ const pkgPath = resolve5(process.cwd(), "package.json");
959
+ if (!existsSync6(pkgPath)) return false;
960
+ try {
961
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
962
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
963
+ return packageName in deps;
964
+ } catch {
965
+ return false;
966
+ }
967
+ }
968
+ function printVoice(voice, showInstallStatus) {
969
+ const badge = voice.official ? chalk6.green(" [official]") : chalk6.blue(" [community]");
970
+ const installed = showInstallStatus && isInstalled(voice.package);
971
+ const status = showInstallStatus ? installed ? chalk6.green(" \u2714 installed") : chalk6.dim(" not installed") : "";
972
+ console.log();
973
+ console.log(" " + chalk6.bold(voice.package) + badge + status);
974
+ console.log(" " + voice.description);
975
+ const installCmd = voice.official && voice.name !== "postgres" ? "tutti-ai add " + voice.name : "npm install " + voice.package;
976
+ console.log(" " + chalk6.dim("Install: ") + chalk6.cyan(installCmd));
977
+ if (voice.tags.length > 0) {
978
+ console.log(" " + chalk6.dim("Tags: ") + voice.tags.join(", "));
979
+ }
980
+ }
981
+ async function searchCommand(query) {
982
+ const spinner = ora3("Searching the Repertoire...").start();
983
+ const voices = await fetchRegistry();
984
+ const results = voices.filter((v) => matchesQuery(v, query));
985
+ spinner.stop();
986
+ if (results.length === 0) {
987
+ console.log();
988
+ console.log(chalk6.yellow(' No voices found for "' + query + '"'));
989
+ console.log();
990
+ console.log(chalk6.dim(" Browse all: https://tutti-ai.com/voices"));
991
+ console.log(chalk6.dim(" Build your own: tutti-ai create voice <name>"));
992
+ console.log();
993
+ return;
994
+ }
995
+ console.log();
996
+ console.log(
997
+ " Found " + chalk6.bold(String(results.length)) + " voice" + (results.length !== 1 ? "s" : "") + " matching " + chalk6.cyan("'" + query + "'") + ":"
998
+ );
999
+ for (const voice of results) {
1000
+ printVoice(voice, false);
1001
+ }
1002
+ console.log();
1003
+ }
1004
+ async function voicesCommand() {
1005
+ const spinner = ora3("Loading voices...").start();
1006
+ const voices = await fetchRegistry();
1007
+ const official = voices.filter((v) => v.official);
1008
+ spinner.stop();
1009
+ console.log();
1010
+ console.log(" " + chalk6.bold("Official Tutti Voices"));
1011
+ console.log();
1012
+ for (const voice of official) {
1013
+ printVoice(voice, true);
1014
+ }
1015
+ const community = voices.filter((v) => !v.official);
1016
+ if (community.length > 0) {
1017
+ console.log();
1018
+ console.log(" " + chalk6.bold("Community Voices"));
1019
+ for (const voice of community) {
1020
+ printVoice(voice, true);
1021
+ }
1022
+ }
1023
+ console.log();
1024
+ console.log(chalk6.dim(" Search: tutti-ai search <query>"));
1025
+ console.log(chalk6.dim(" Browse: https://tutti-ai.com/voices"));
1026
+ console.log();
1027
+ }
1028
+
1029
+ // src/commands/publish.ts
1030
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1031
+ import { resolve as resolve6 } from "path";
1032
+ import { execSync as execSync2 } from "child_process";
1033
+ import chalk7 from "chalk";
1034
+ import ora4 from "ora";
1035
+ import Enquirer2 from "enquirer";
1036
+ import { createLogger as createLogger7, SecretsManager as SecretsManager3 } from "@tuttiai/core";
1037
+ var { prompt: prompt2 } = Enquirer2;
1038
+ var logger7 = createLogger7("tutti-cli");
1039
+ function readPkg(dir) {
1040
+ const p = resolve6(dir, "package.json");
1041
+ if (!existsSync7(p)) return void 0;
1042
+ return JSON.parse(readFileSync3(p, "utf-8"));
1043
+ }
1044
+ function run(cmd, cwd) {
1045
+ return execSync2(cmd, { cwd, stdio: "pipe", encoding: "utf-8" });
1046
+ }
1047
+ function fail2(msg) {
1048
+ console.error(chalk7.red(" " + msg));
1049
+ process.exit(1);
1050
+ }
1051
+ var ok2 = (msg) => console.log(chalk7.green(" \u2714 " + msg));
1052
+ async function publishCommand(opts) {
1053
+ const cwd = process.cwd();
1054
+ const pkg = readPkg(cwd);
1055
+ console.log();
1056
+ console.log(chalk7.bold(" Tutti Voice Publisher"));
1057
+ console.log();
1058
+ const spinner = ora4("Running pre-flight checks...").start();
1059
+ if (!pkg) fail2("No package.json found in the current directory.");
1060
+ if (!existsSync7(resolve6(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
1061
+ const missing = [];
1062
+ if (!pkg.name) missing.push("name");
1063
+ if (!pkg.version) missing.push("version");
1064
+ if (!pkg.description) missing.push("description");
1065
+ if (!pkg.license) missing.push("license");
1066
+ if (!pkg.exports) missing.push("exports");
1067
+ if (missing.length > 0) fail2("package.json is missing: " + missing.join(", "));
1068
+ const name = pkg.name;
1069
+ const version = pkg.version;
1070
+ const validName = name.startsWith("@tuttiai/") || name.startsWith("tutti");
1071
+ if (!validName) fail2("Package name must start with @tuttiai/ or tutti \u2014 got: " + name);
1072
+ const src = readFileSync3(resolve6(cwd, "src/index.ts"), "utf-8");
1073
+ if (!src.includes("required_permissions")) {
1074
+ fail2("Voice class must declare required_permissions in src/index.ts");
1075
+ }
1076
+ spinner.succeed("Pre-flight checks passed");
1077
+ const buildSpinner = ora4("Building...").start();
1078
+ try {
1079
+ run("npm run build", cwd);
1080
+ buildSpinner.succeed("Build succeeded");
1081
+ } catch (err) {
1082
+ buildSpinner.fail("Build failed");
1083
+ const msg = err instanceof Error ? err.message : String(err);
1084
+ console.error(chalk7.dim(" " + msg.split("\n").slice(0, 5).join("\n ")));
1085
+ process.exit(1);
1086
+ }
1087
+ const testSpinner = ora4("Running tests...").start();
1088
+ try {
1089
+ run("npx vitest run", cwd);
1090
+ testSpinner.succeed("Tests passed");
1091
+ } catch {
1092
+ testSpinner.fail("Tests failed");
1093
+ process.exit(1);
1094
+ }
1095
+ const auditSpinner = ora4("Checking vulnerabilities...").start();
1096
+ try {
1097
+ run("npm audit --audit-level=high", cwd);
1098
+ auditSpinner.succeed("No high/critical vulnerabilities");
1099
+ } catch {
1100
+ auditSpinner.stopAndPersist({ symbol: chalk7.yellow("\u26A0"), text: "Vulnerabilities found (npm audit)" });
1101
+ }
1102
+ console.log();
1103
+ const drySpinner = ora4("Packing (dry run)...").start();
1104
+ let packOutput;
1105
+ try {
1106
+ packOutput = run("npm pack --dry-run 2>&1", cwd);
1107
+ drySpinner.succeed("Pack dry-run complete");
1108
+ } catch (err) {
1109
+ drySpinner.fail("Pack dry-run failed");
1110
+ const msg = err instanceof Error ? err.message : String(err);
1111
+ console.error(chalk7.dim(" " + msg));
1112
+ process.exit(1);
1113
+ }
1114
+ 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*/, ""));
1115
+ if (fileLines.length > 0) {
1116
+ console.log(chalk7.dim(" Files:"));
1117
+ for (const line of fileLines) {
1118
+ console.log(chalk7.dim(" " + line.trim()));
1119
+ }
1120
+ }
1121
+ const sizeLine = packOutput.split("\n").find((l) => l.includes("package size"));
1122
+ const totalLine = packOutput.split("\n").find((l) => l.includes("total files"));
1123
+ if (sizeLine) console.log(chalk7.dim(" " + sizeLine.replace(/npm notice\s*/, "").trim()));
1124
+ if (totalLine) console.log(chalk7.dim(" " + totalLine.replace(/npm notice\s*/, "").trim()));
1125
+ if (opts.dryRun) {
1126
+ console.log();
1127
+ ok2("Dry run complete \u2014 no packages were published");
1128
+ console.log(chalk7.dim(" Run without --dry-run to publish for real."));
1129
+ console.log();
1130
+ return;
1131
+ }
1132
+ console.log();
1133
+ const { confirm } = await prompt2({
1134
+ type: "confirm",
1135
+ name: "confirm",
1136
+ message: "Publish " + chalk7.cyan(name + "@" + version) + "?"
1137
+ });
1138
+ if (!confirm) {
1139
+ console.log(chalk7.dim(" Cancelled."));
1140
+ return;
1141
+ }
1142
+ const pubSpinner = ora4("Publishing to npm...").start();
1143
+ try {
1144
+ run("npm publish --access public", cwd);
1145
+ pubSpinner.succeed("Published " + chalk7.cyan(name + "@" + version));
1146
+ } catch (err) {
1147
+ pubSpinner.fail("Publish failed");
1148
+ const msg = err instanceof Error ? err.message : String(err);
1149
+ logger7.error({ error: msg }, "npm publish failed");
1150
+ process.exit(1);
1151
+ }
1152
+ const ghToken = SecretsManager3.optional("GITHUB_TOKEN");
1153
+ let prUrl;
1154
+ if (ghToken) {
1155
+ const prSpinner = ora4("Opening PR to voice registry...").start();
1156
+ try {
1157
+ prUrl = await openRegistryPR(name, version, pkg.description ?? "", ghToken);
1158
+ prSpinner.succeed("PR opened: " + prUrl);
1159
+ } catch (err) {
1160
+ prSpinner.fail("Failed to open PR");
1161
+ const msg = err instanceof Error ? err.message : String(err);
1162
+ logger7.error({ error: msg }, "Registry PR failed");
1163
+ }
1164
+ } else {
1165
+ console.log();
1166
+ console.log(chalk7.dim(" To list in the Repertoire, set GITHUB_TOKEN and re-run"));
1167
+ console.log(chalk7.dim(" Or open a PR manually: github.com/tuttiai/voices"));
1168
+ }
1169
+ console.log();
1170
+ ok2(name + "@" + version + " published to npm");
1171
+ if (prUrl) ok2("PR opened to tuttiai/voices");
1172
+ const shortName = name.replace("@tuttiai/", "").replace(/^tutti-?/, "");
1173
+ ok2("Install: tutti-ai add " + shortName);
1174
+ ok2("View: https://www.npmjs.com/package/" + name);
1175
+ console.log();
1176
+ }
1177
+ async function openRegistryPR(packageName, version, description, token) {
1178
+ const owner = "tuttiai";
1179
+ const repo = "voices";
1180
+ const branch = "add-" + packageName.replace(/[@/]/g, "-").replace(/^-/, "");
1181
+ const shortName = packageName.replace("@tuttiai/", "").replace(/^tutti-?/, "");
1182
+ const isOfficial = packageName.startsWith("@tuttiai/");
1183
+ const fileRes = await fetch(
1184
+ "https://api.github.com/repos/" + owner + "/" + repo + "/contents/voices.json",
1185
+ { headers: { Authorization: "Bearer " + token, Accept: "application/vnd.github.v3+json" } }
1186
+ );
1187
+ if (!fileRes.ok) throw new Error("Failed to fetch voices.json: " + fileRes.status);
1188
+ const fileData = await fileRes.json();
1189
+ const registry = JSON.parse(Buffer.from(fileData.content, "base64").toString("utf-8"));
1190
+ const section = isOfficial ? "official" : "community";
1191
+ const entry = {
1192
+ name: shortName,
1193
+ package: packageName,
1194
+ description,
1195
+ repo: "https://github.com/tuttiai/tutti/tree/main/voices/" + shortName,
1196
+ version,
1197
+ author: isOfficial ? "tuttiai" : packageName.split("/")[0]?.replace("@", "") ?? "community",
1198
+ tags: [shortName]
1199
+ };
1200
+ if (!registry[section]) registry[section] = [];
1201
+ const exists = registry[section].some((v) => v.package === packageName);
1202
+ if (exists) {
1203
+ const idx = registry[section].findIndex((v) => v.package === packageName);
1204
+ registry[section][idx] = { ...registry[section][idx], ...entry };
1205
+ } else {
1206
+ registry[section].push(entry);
1207
+ }
1208
+ const updatedContent = Buffer.from(JSON.stringify(registry, null, 2) + "\n").toString("base64");
1209
+ const mainRes = await fetch(
1210
+ "https://api.github.com/repos/" + owner + "/" + repo + "/git/ref/heads/main",
1211
+ { headers: { Authorization: "Bearer " + token, Accept: "application/vnd.github.v3+json" } }
1212
+ );
1213
+ if (!mainRes.ok) throw new Error("Failed to get main ref: " + mainRes.status);
1214
+ const mainData = await mainRes.json();
1215
+ await fetch("https://api.github.com/repos/" + owner + "/" + repo + "/git/refs", {
1216
+ method: "POST",
1217
+ headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
1218
+ body: JSON.stringify({ ref: "refs/heads/" + branch, sha: mainData.object.sha })
1219
+ });
1220
+ await fetch(
1221
+ "https://api.github.com/repos/" + owner + "/" + repo + "/contents/voices.json",
1222
+ {
1223
+ method: "PUT",
1224
+ headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
1225
+ body: JSON.stringify({
1226
+ message: "feat: add " + packageName + " to the Repertoire",
1227
+ content: updatedContent,
1228
+ sha: fileData.sha,
1229
+ branch
1230
+ })
1231
+ }
1232
+ );
1233
+ const prRes = await fetch("https://api.github.com/repos/" + owner + "/" + repo + "/pulls", {
1234
+ method: "POST",
1235
+ headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
1236
+ body: JSON.stringify({
1237
+ title: "feat: add " + packageName + " to the Repertoire",
1238
+ head: branch,
1239
+ base: "main",
1240
+ body: "## New voice: " + packageName + "@" + version + "\n\n" + description + "\n\nPublished via `tutti-ai publish`."
1241
+ })
1242
+ });
1243
+ if (!prRes.ok) {
1244
+ const err = await prRes.text();
1245
+ throw new Error("Failed to create PR: " + prRes.status + " " + err);
1246
+ }
1247
+ const prData = await prRes.json();
1248
+ return prData.html_url;
1249
+ }
1250
+
1251
+ // src/commands/eval.ts
1252
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
1253
+ import { resolve as resolve7 } from "path";
1254
+ import chalk8 from "chalk";
1255
+ import ora5 from "ora";
1256
+ import {
1257
+ ScoreLoader as ScoreLoader4,
1258
+ EvalRunner,
1259
+ printEvalTable,
1260
+ createLogger as createLogger8
1261
+ } from "@tuttiai/core";
1262
+ var logger8 = createLogger8("tutti-cli");
1263
+ async function evalCommand(suitePath, opts) {
1264
+ const suiteFile = resolve7(suitePath);
1265
+ if (!existsSync8(suiteFile)) {
1266
+ logger8.error({ file: suiteFile }, "Suite file not found");
1267
+ process.exit(1);
1268
+ }
1269
+ let suite;
1270
+ try {
1271
+ suite = JSON.parse(readFileSync4(suiteFile, "utf-8"));
1272
+ } catch (err) {
1273
+ logger8.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to parse suite file");
1274
+ process.exit(1);
1275
+ }
1276
+ const scoreFile = resolve7(opts.score ?? "./tutti.score.ts");
1277
+ if (!existsSync8(scoreFile)) {
1278
+ logger8.error({ file: scoreFile }, "Score file not found");
1279
+ process.exit(1);
1280
+ }
1281
+ const spinner = ora5("Loading score...").start();
1282
+ let score;
1283
+ try {
1284
+ score = await ScoreLoader4.load(scoreFile);
1285
+ } catch (err) {
1286
+ spinner.fail("Failed to load score");
1287
+ logger8.error({ error: err instanceof Error ? err.message : String(err) }, "Score load failed");
1288
+ process.exit(1);
1289
+ }
1290
+ spinner.succeed("Score loaded");
1291
+ const evalSpinner = ora5("Running " + suite.cases.length + " eval cases...").start();
1292
+ const runner = new EvalRunner(score);
1293
+ const report = await runner.run(suite);
1294
+ evalSpinner.stop();
1295
+ printEvalTable(report);
1296
+ if (opts.ci && report.summary.failed > 0) {
1297
+ console.error(chalk8.red(" CI mode: " + report.summary.failed + " case(s) failed"));
1298
+ process.exit(1);
1299
+ }
612
1300
  }
613
1301
 
614
1302
  // src/index.ts
615
1303
  config();
616
- var logger6 = createLogger6("tutti-cli");
1304
+ var logger9 = createLogger9("tutti-cli");
617
1305
  process.on("unhandledRejection", (reason) => {
618
- logger6.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
1306
+ logger9.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
619
1307
  process.exit(1);
620
1308
  });
621
1309
  process.on("uncaughtException", (err) => {
622
- logger6.error({ error: err.message }, "Fatal error");
1310
+ logger9.error({ error: err.message }, "Fatal error");
623
1311
  process.exit(1);
624
1312
  });
625
1313
  var program = new Command();
626
- program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.7.0");
627
- program.command("init [project-name]").description("Create a new Tutti project").action(async (projectName) => {
628
- await initCommand(projectName);
1314
+ program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.8.0");
1315
+ program.command("init [project-name]").description("Create a new Tutti project").option("-t, --template <id>", "Project template to use").action(async (projectName, opts) => {
1316
+ await initCommand(projectName, opts.template);
1317
+ });
1318
+ program.command("templates").description("List all available project templates").action(() => {
1319
+ templatesCommand();
629
1320
  });
630
1321
  program.command("run [score]").description("Run a Tutti score interactively").action(async (score) => {
631
1322
  await runCommand(score);
632
1323
  });
633
- program.command("add <voice>").description("Add a voice to your project").action(async (voice) => {
634
- await addCommand(voice);
1324
+ program.command("add <voice>").description("Add a voice to your project").action((voice) => {
1325
+ addCommand(voice);
635
1326
  });
636
1327
  program.command("check [score]").description("Validate a score file without running it").action(async (score) => {
637
1328
  await checkCommand(score);
@@ -642,5 +1333,17 @@ program.command("doctor [score]").description("Alias for check \u2014 validate a
642
1333
  program.command("studio [score]").description("Launch Tutti Studio \u2014 local web UI for inspecting agent runs").action(async (score) => {
643
1334
  await studioCommand(score);
644
1335
  });
1336
+ program.command("search <query>").description("Search the voice registry for voices matching a query").action(async (query) => {
1337
+ await searchCommand(query);
1338
+ });
1339
+ program.command("voices").description("List all available official voices and install status").action(async () => {
1340
+ await voicesCommand();
1341
+ });
1342
+ 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) => {
1343
+ await publishCommand(opts);
1344
+ });
1345
+ 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) => {
1346
+ await evalCommand(suitePath, opts);
1347
+ });
645
1348
  program.parse();
646
1349
  //# sourceMappingURL=index.js.map