@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 +743 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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": "
|
|
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":
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
493
|
-
|
|
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
|
|
580
|
-
if (
|
|
581
|
-
res.status(400).json({ error: "
|
|
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>♫</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…<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…" 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">↓ Input</span><span class="token-val input" id="tok-in">0</span></div> <div class="token-item"><span class="token-label">↑ 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(" · ");}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)+" · "+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>♫</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…<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…" 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">↓ Input</span><span class="token-val input" id="tok-in">0</span></div> <div class="token-item"><span class="token-label">↑ 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(" · ");}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)+" · "+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
|
|
1304
|
+
var logger9 = createLogger9("tutti-cli");
|
|
617
1305
|
process.on("unhandledRejection", (reason) => {
|
|
618
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
634
|
-
|
|
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
|