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