@tuttiai/cli 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { config } from "dotenv";
5
- import { createLogger as createLogger10 } from "@tuttiai/core";
5
+ import { createLogger as createLogger11 } from "@tuttiai/core";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/init.ts
@@ -356,7 +356,7 @@ function templatesCommand() {
356
356
 
357
357
  // src/commands/run.ts
358
358
  import { existsSync as existsSync2 } from "fs";
359
- import { resolve } from "path";
359
+ import { resolve as resolve2 } from "path";
360
360
  import { createInterface } from "readline/promises";
361
361
  import chalk2 from "chalk";
362
362
  import ora from "ora";
@@ -367,11 +367,122 @@ import {
367
367
  OpenAIProvider,
368
368
  GeminiProvider,
369
369
  SecretsManager,
370
+ InMemorySessionStore,
370
371
  createLogger as createLogger2
371
372
  } from "@tuttiai/core";
373
+
374
+ // src/watch/score-watcher.ts
375
+ import { dirname, resolve } from "path";
376
+ import { EventEmitter } from "events";
377
+ import chokidar from "chokidar";
378
+ import { validateScore } from "@tuttiai/core";
379
+ var DEFAULT_DEBOUNCE_MS = 200;
380
+ var DEFAULT_IGNORED = [
381
+ /(^|[/\\])\../,
382
+ // dotfiles (.git, .env, etc.)
383
+ /node_modules/,
384
+ /[/\\]dist[/\\]/,
385
+ /[/\\]coverage[/\\]/
386
+ ];
387
+ async function defaultLoadScore(path) {
388
+ const absolute = resolve(path);
389
+ const { pathToFileURL } = await import("url");
390
+ const url = pathToFileURL(absolute).href + "?t=" + Date.now().toString(36);
391
+ const mod = await import(url);
392
+ if (!mod.default) {
393
+ throw new Error(
394
+ "Score file has no default export: " + path + " \u2014 your score must export `defineScore({ ... })` as its default."
395
+ );
396
+ }
397
+ validateScore(mod.default);
398
+ return mod.default;
399
+ }
400
+ var ReactiveScore = class extends EventEmitter {
401
+ _current;
402
+ scorePath;
403
+ load;
404
+ debounceMs;
405
+ watcher;
406
+ debounceTimer;
407
+ closed = false;
408
+ _pendingReload = false;
409
+ constructor(initialScore, scorePath, options = {}) {
410
+ super();
411
+ this._current = initialScore;
412
+ this.scorePath = resolve(scorePath);
413
+ this.load = options.load ?? defaultLoadScore;
414
+ this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
415
+ const watchTargets = [
416
+ this.scorePath,
417
+ dirname(this.scorePath),
418
+ ...options.extraPaths ?? []
419
+ ];
420
+ this.watcher = chokidar.watch(watchTargets, {
421
+ ignored: DEFAULT_IGNORED,
422
+ ignoreInitial: true,
423
+ // awaitWriteFinish: guard against partial writes from editors that
424
+ // save atomically via rename/move.
425
+ awaitWriteFinish: {
426
+ stabilityThreshold: 50,
427
+ pollInterval: 20
428
+ }
429
+ });
430
+ this.watcher.on("change", (path) => this.handleChange(path));
431
+ this.watcher.on("add", (path) => this.handleChange(path));
432
+ }
433
+ /** The most recent successfully-loaded score. Never stale. */
434
+ get current() {
435
+ return this._current;
436
+ }
437
+ /**
438
+ * True when a file change has been observed and a reload is pending
439
+ * (or just completed and not yet consumed). Readers call
440
+ * {@link consumePendingReload} to clear the flag when they've taken
441
+ * action on the new config.
442
+ */
443
+ get pendingReload() {
444
+ return this._pendingReload;
445
+ }
446
+ consumePendingReload() {
447
+ this._pendingReload = false;
448
+ }
449
+ /** Release the underlying filesystem watchers. */
450
+ async close() {
451
+ this.closed = true;
452
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
453
+ await this.watcher.close();
454
+ }
455
+ /**
456
+ * Force an immediate reload without waiting for a filesystem event.
457
+ * Exposed for tests and for the `reload` REPL command.
458
+ */
459
+ async reloadNow() {
460
+ if (this.closed) return;
461
+ this.emit("reloading");
462
+ try {
463
+ const next = await this.load(this.scorePath);
464
+ this._current = next;
465
+ this._pendingReload = true;
466
+ this.emit("reloaded", next);
467
+ } catch (err) {
468
+ const error = err instanceof Error ? err : new Error(String(err));
469
+ this.emit("reload-failed", error);
470
+ }
471
+ }
472
+ handleChange(path) {
473
+ if (this.closed) return;
474
+ this.emit("file-change", path);
475
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
476
+ this.debounceTimer = setTimeout(() => {
477
+ void this.reloadNow();
478
+ }, this.debounceMs);
479
+ }
480
+ };
481
+
482
+ // src/commands/run.ts
372
483
  var logger2 = createLogger2("tutti-cli");
373
- async function runCommand(scorePath) {
374
- const file = resolve(scorePath ?? "./tutti.score.ts");
484
+ async function runCommand(scorePath, options = {}) {
485
+ const file = resolve2(scorePath ?? "./tutti.score.ts");
375
486
  if (!existsSync2(file)) {
376
487
  logger2.error({ file }, "Score file not found");
377
488
  console.error(chalk2.dim('Run "tutti-ai init" to create a new project.'));
@@ -401,90 +512,128 @@ async function runCommand(scorePath) {
401
512
  }
402
513
  }
403
514
  }
404
- for (const agent of Object.values(score.agents)) {
405
- agent.streaming = true;
406
- }
407
- const runtime = new TuttiRuntime(score);
515
+ const applyRunDefaults = (cfg) => {
516
+ for (const agent of Object.values(cfg.agents)) {
517
+ agent.streaming = true;
518
+ }
519
+ };
520
+ applyRunDefaults(score);
521
+ const sharedSessions = options.watch ? new InMemorySessionStore() : void 0;
408
522
  const spinner = ora({ color: "cyan" });
409
523
  let streaming = false;
410
- runtime.events.on("agent:start", (e) => {
411
- logger2.info({ agent: e.agent_name }, "Running agent");
412
- });
413
- runtime.events.on("llm:request", () => {
414
- spinner.start("Thinking...");
415
- });
416
- runtime.events.on("token:stream", (e) => {
417
- if (!streaming) {
418
- spinner.stop();
419
- streaming = true;
420
- }
421
- process.stdout.write(e.text);
422
- });
423
- runtime.events.on("llm:response", () => {
424
- if (streaming) {
425
- process.stdout.write("\n");
426
- } else {
427
- spinner.stop();
428
- }
429
- });
430
- runtime.events.on("tool:start", (e) => {
431
- if (streaming) {
432
- process.stdout.write(chalk2.dim("\n [using: " + e.tool_name + "]"));
433
- } else {
434
- spinner.stop();
435
- console.log(chalk2.dim(" [using: " + e.tool_name + "]"));
436
- }
437
- });
438
- runtime.events.on("tool:end", (e) => {
439
- if (streaming) {
440
- process.stdout.write(chalk2.dim(" [done: " + e.tool_name + "]\n"));
441
- }
442
- });
443
- runtime.events.on("tool:error", (e) => {
444
- spinner.stop();
445
- logger2.error({ tool: e.tool_name }, "Tool error");
446
- });
447
- runtime.events.on("security:injection_detected", (e) => {
448
- logger2.warn({ tool: e.tool_name }, "Potential prompt injection detected");
449
- });
450
- runtime.events.on("budget:warning", () => {
451
- logger2.warn("Approaching token budget (80%)");
452
- });
453
- runtime.events.on("budget:exceeded", () => {
454
- logger2.error("Token budget exceeded \u2014 stopping");
455
- });
524
+ let runtime = buildRuntime(score, sharedSessions);
525
+ attachListeners(runtime);
456
526
  const rl = createInterface({
457
527
  input: process.stdin,
458
528
  output: process.stdout
459
529
  });
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));
530
+ function attachListeners(r) {
531
+ r.events.on("agent:start", (e) => {
532
+ logger2.info({ agent: e.agent_name }, "Running agent");
533
+ });
534
+ r.events.on("llm:request", () => {
535
+ spinner.start("Thinking...");
536
+ });
537
+ r.events.on("token:stream", (e) => {
538
+ if (!streaming) {
539
+ spinner.stop();
540
+ streaming = true;
541
+ }
542
+ process.stdout.write(e.text);
543
+ });
544
+ r.events.on("llm:response", () => {
545
+ if (streaming) {
546
+ process.stdout.write("\n");
547
+ } else {
548
+ spinner.stop();
549
+ }
550
+ });
551
+ r.events.on("tool:start", (e) => {
552
+ if (streaming) {
553
+ process.stdout.write(chalk2.dim("\n [using: " + e.tool_name + "]"));
554
+ } else {
555
+ spinner.stop();
556
+ console.log(chalk2.dim(" [using: " + e.tool_name + "]"));
557
+ }
558
+ });
559
+ r.events.on("tool:end", (e) => {
560
+ if (streaming) {
561
+ process.stdout.write(chalk2.dim(" [done: " + e.tool_name + "]\n"));
562
+ }
563
+ });
564
+ r.events.on("tool:error", (e) => {
565
+ spinner.stop();
566
+ logger2.error({ tool: e.tool_name }, "Tool error");
567
+ });
568
+ r.events.on("security:injection_detected", (e) => {
569
+ logger2.warn({ tool: e.tool_name }, "Potential prompt injection detected");
570
+ });
571
+ r.events.on("budget:warning", () => {
572
+ logger2.warn("Approaching token budget (80%)");
573
+ });
574
+ r.events.on("budget:exceeded", () => {
575
+ logger2.error("Token budget exceeded \u2014 stopping");
576
+ });
577
+ r.events.on("hitl:requested", (e) => {
578
+ spinner.stop();
579
+ if (streaming) {
580
+ process.stdout.write("\n");
581
+ streaming = false;
582
+ }
583
+ console.log();
584
+ console.log(
585
+ chalk2.yellow(
586
+ " " + chalk2.bold("[Agent needs input]") + " " + e.question
587
+ )
588
+ );
589
+ if (e.options) {
590
+ e.options.forEach((opt, i) => {
591
+ console.log(chalk2.yellow(" " + (i + 1) + ". " + opt));
592
+ });
593
+ }
594
+ void rl.question(chalk2.yellow(" > ")).then((answer) => {
595
+ runtime.answer(e.session_id, answer.trim());
471
596
  });
472
- }
473
- void rl.question(chalk2.yellow(" > ")).then((answer) => {
474
- runtime.answer(e.session_id, answer.trim());
475
597
  });
476
- });
598
+ }
599
+ let reactive;
600
+ if (options.watch) {
601
+ reactive = new ReactiveScore(score, file);
602
+ reactive.on("file-change", () => {
603
+ console.log(chalk2.cyan("\n[tutti] Score changed, reloading..."));
604
+ });
605
+ reactive.on("reloaded", () => {
606
+ console.log(chalk2.green("[tutti] Score reloaded. Changes applied."));
607
+ });
608
+ reactive.on("reload-failed", (err) => {
609
+ logger2.error(
610
+ { error: err instanceof Error ? err.message : String(err) },
611
+ "[tutti] Reload failed \u2014 using previous config"
612
+ );
613
+ });
614
+ }
477
615
  console.log(chalk2.dim('Tutti REPL \u2014 type "exit" to quit\n'));
616
+ if (options.watch) {
617
+ console.log(chalk2.dim("Watching " + file + " for changes\u2026\n"));
618
+ }
478
619
  let sessionId;
479
620
  process.on("SIGINT", () => {
480
621
  if (streaming) process.stdout.write("\n");
481
622
  spinner.stop();
482
623
  console.log(chalk2.dim("Goodbye!"));
483
624
  rl.close();
625
+ if (reactive) void reactive.close();
484
626
  process.exit(0);
485
627
  });
486
628
  try {
487
629
  while (true) {
630
+ if (reactive?.pendingReload) {
631
+ const nextScore = reactive.current;
632
+ applyRunDefaults(nextScore);
633
+ runtime = buildRuntime(nextScore, sharedSessions);
634
+ attachListeners(runtime);
635
+ reactive.consumePendingReload();
636
+ }
488
637
  const input = await rl.question(chalk2.cyan("> "));
489
638
  const trimmed = input.trim();
490
639
  if (!trimmed) continue;
@@ -511,12 +660,19 @@ async function runCommand(scorePath) {
511
660
  }
512
661
  console.log(chalk2.dim("Goodbye!"));
513
662
  rl.close();
663
+ if (reactive) await reactive.close();
514
664
  process.exit(0);
515
665
  }
666
+ function buildRuntime(score, sessionStore) {
667
+ return new TuttiRuntime(
668
+ score,
669
+ sessionStore ? { sessionStore } : {}
670
+ );
671
+ }
516
672
 
517
673
  // src/commands/resume.ts
518
674
  import { existsSync as existsSync3 } from "fs";
519
- import { resolve as resolve2 } from "path";
675
+ import { resolve as resolve3 } from "path";
520
676
  import { createInterface as createInterface2 } from "readline/promises";
521
677
  import chalk3 from "chalk";
522
678
  import ora2 from "ora";
@@ -532,7 +688,7 @@ import {
532
688
  } from "@tuttiai/core";
533
689
  var logger3 = createLogger3("tutti-cli");
534
690
  async function resumeCommand(sessionId, opts) {
535
- const scoreFile = resolve2(opts.score ?? "./tutti.score.ts");
691
+ const scoreFile = resolve3(opts.score ?? "./tutti.score.ts");
536
692
  if (!existsSync3(scoreFile)) {
537
693
  logger3.error({ file: scoreFile }, "Score file not found");
538
694
  console.error(chalk3.dim('Run "tutti-ai init" to create a new project.'));
@@ -776,7 +932,7 @@ function wireProgress(runtime) {
776
932
 
777
933
  // src/commands/add.ts
778
934
  import { existsSync as existsSync4, readFileSync } from "fs";
779
- import { resolve as resolve3 } from "path";
935
+ import { resolve as resolve4 } from "path";
780
936
  import { execSync } from "child_process";
781
937
  import chalk4 from "chalk";
782
938
  import ora3 from "ora";
@@ -832,7 +988,7 @@ function resolvePackageName(input) {
832
988
  return `@tuttiai/${input}`;
833
989
  }
834
990
  function isAlreadyInstalled(packageName) {
835
- const pkgPath = resolve3(process.cwd(), "package.json");
991
+ const pkgPath = resolve4(process.cwd(), "package.json");
836
992
  if (!existsSync4(pkgPath)) return false;
837
993
  try {
838
994
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
@@ -844,7 +1000,7 @@ function isAlreadyInstalled(packageName) {
844
1000
  }
845
1001
  function addCommand(voiceName) {
846
1002
  const packageName = resolvePackageName(voiceName);
847
- const pkgPath = resolve3(process.cwd(), "package.json");
1003
+ const pkgPath = resolve4(process.cwd(), "package.json");
848
1004
  if (!existsSync4(pkgPath)) {
849
1005
  logger4.error("No package.json found in the current directory");
850
1006
  console.error(chalk4.dim('Run "tutti-ai init" to create a new project first.'));
@@ -884,7 +1040,7 @@ function addCommand(voiceName) {
884
1040
 
885
1041
  // src/commands/check.ts
886
1042
  import { existsSync as existsSync5 } from "fs";
887
- import { resolve as resolve4 } from "path";
1043
+ import { resolve as resolve5 } from "path";
888
1044
  import chalk5 from "chalk";
889
1045
  import {
890
1046
  ScoreLoader as ScoreLoader3,
@@ -898,7 +1054,7 @@ var logger5 = createLogger5("tutti-cli");
898
1054
  var ok = (msg) => console.log(chalk5.green(" \u2714 " + msg));
899
1055
  var fail = (msg) => console.log(chalk5.red(" \u2718 " + msg));
900
1056
  async function checkCommand(scorePath) {
901
- const file = resolve4(scorePath ?? "./tutti.score.ts");
1057
+ const file = resolve5(scorePath ?? "./tutti.score.ts");
902
1058
  console.log(chalk5.cyan(`
903
1059
  Checking ${file}...
904
1060
  `));
@@ -981,7 +1137,7 @@ Checking ${file}...
981
1137
 
982
1138
  // src/commands/studio.ts
983
1139
  import { existsSync as existsSync6 } from "fs";
984
- import { resolve as resolve5 } from "path";
1140
+ import { resolve as resolve6 } from "path";
985
1141
  import { execFile } from "child_process";
986
1142
  import express from "express";
987
1143
  import chalk6 from "chalk";
@@ -1009,7 +1165,7 @@ function openBrowser(url) {
1009
1165
  execFile(cmd, [url]);
1010
1166
  }
1011
1167
  async function studioCommand(scorePath) {
1012
- const file = resolve5(scorePath ?? "./tutti.score.ts");
1168
+ const file = resolve6(scorePath ?? "./tutti.score.ts");
1013
1169
  if (!existsSync6(file)) {
1014
1170
  logger6.error({ file }, "Score file not found");
1015
1171
  console.error(chalk6.dim('Run "tutti-ai init" to create a new project.'));
@@ -1144,7 +1300,7 @@ function getStudioHtml() {
1144
1300
 
1145
1301
  // src/commands/search.ts
1146
1302
  import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
1147
- import { resolve as resolve6 } from "path";
1303
+ import { resolve as resolve7 } from "path";
1148
1304
  import chalk7 from "chalk";
1149
1305
  import ora4 from "ora";
1150
1306
  import { createLogger as createLogger7 } from "@tuttiai/core";
@@ -1215,7 +1371,7 @@ function matchesQuery(voice, query) {
1215
1371
  return false;
1216
1372
  }
1217
1373
  function isInstalled(packageName) {
1218
- const pkgPath = resolve6(process.cwd(), "package.json");
1374
+ const pkgPath = resolve7(process.cwd(), "package.json");
1219
1375
  if (!existsSync7(pkgPath)) return false;
1220
1376
  try {
1221
1377
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
@@ -1288,7 +1444,7 @@ async function voicesCommand() {
1288
1444
 
1289
1445
  // src/commands/publish.ts
1290
1446
  import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
1291
- import { resolve as resolve7 } from "path";
1447
+ import { resolve as resolve8 } from "path";
1292
1448
  import { execSync as execSync2 } from "child_process";
1293
1449
  import chalk8 from "chalk";
1294
1450
  import ora5 from "ora";
@@ -1297,7 +1453,7 @@ import { createLogger as createLogger8, SecretsManager as SecretsManager4 } from
1297
1453
  var { prompt: prompt2 } = Enquirer2;
1298
1454
  var logger8 = createLogger8("tutti-cli");
1299
1455
  function readPkg(dir) {
1300
- const p = resolve7(dir, "package.json");
1456
+ const p = resolve8(dir, "package.json");
1301
1457
  if (!existsSync8(p)) return void 0;
1302
1458
  return JSON.parse(readFileSync3(p, "utf-8"));
1303
1459
  }
@@ -1317,7 +1473,7 @@ async function publishCommand(opts) {
1317
1473
  console.log();
1318
1474
  const spinner = ora5("Running pre-flight checks...").start();
1319
1475
  if (!pkg) fail2("No package.json found in the current directory.");
1320
- if (!existsSync8(resolve7(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
1476
+ if (!existsSync8(resolve8(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
1321
1477
  const missing = [];
1322
1478
  if (!pkg.name) missing.push("name");
1323
1479
  if (!pkg.version) missing.push("version");
@@ -1329,7 +1485,7 @@ async function publishCommand(opts) {
1329
1485
  const version = pkg.version;
1330
1486
  const validName = name.startsWith("@tuttiai/") || name.startsWith("tutti");
1331
1487
  if (!validName) fail2("Package name must start with @tuttiai/ or tutti \u2014 got: " + name);
1332
- const src = readFileSync3(resolve7(cwd, "src/index.ts"), "utf-8");
1488
+ const src = readFileSync3(resolve8(cwd, "src/index.ts"), "utf-8");
1333
1489
  if (!src.includes("required_permissions")) {
1334
1490
  fail2("Voice class must declare required_permissions in src/index.ts");
1335
1491
  }
@@ -1510,7 +1666,7 @@ async function openRegistryPR(packageName, version, description, token) {
1510
1666
 
1511
1667
  // src/commands/eval.ts
1512
1668
  import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
1513
- import { resolve as resolve8 } from "path";
1669
+ import { resolve as resolve9 } from "path";
1514
1670
  import chalk9 from "chalk";
1515
1671
  import ora6 from "ora";
1516
1672
  import {
@@ -1521,7 +1677,7 @@ import {
1521
1677
  } from "@tuttiai/core";
1522
1678
  var logger9 = createLogger9("tutti-cli");
1523
1679
  async function evalCommand(suitePath, opts) {
1524
- const suiteFile = resolve8(suitePath);
1680
+ const suiteFile = resolve9(suitePath);
1525
1681
  if (!existsSync9(suiteFile)) {
1526
1682
  logger9.error({ file: suiteFile }, "Suite file not found");
1527
1683
  process.exit(1);
@@ -1533,7 +1689,7 @@ async function evalCommand(suitePath, opts) {
1533
1689
  logger9.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to parse suite file");
1534
1690
  process.exit(1);
1535
1691
  }
1536
- const scoreFile = resolve8(opts.score ?? "./tutti.score.ts");
1692
+ const scoreFile = resolve9(opts.score ?? "./tutti.score.ts");
1537
1693
  if (!existsSync9(scoreFile)) {
1538
1694
  logger9.error({ file: scoreFile }, "Score file not found");
1539
1695
  process.exit(1);
@@ -1559,27 +1715,178 @@ async function evalCommand(suitePath, opts) {
1559
1715
  }
1560
1716
  }
1561
1717
 
1718
+ // src/commands/serve.ts
1719
+ import { existsSync as existsSync10 } from "fs";
1720
+ import { resolve as resolve10 } from "path";
1721
+ import chalk10 from "chalk";
1722
+ import {
1723
+ TuttiRuntime as TuttiRuntime4,
1724
+ ScoreLoader as ScoreLoader6,
1725
+ AnthropicProvider as AnthropicProvider4,
1726
+ OpenAIProvider as OpenAIProvider4,
1727
+ GeminiProvider as GeminiProvider4,
1728
+ SecretsManager as SecretsManager5,
1729
+ InMemorySessionStore as InMemorySessionStore2,
1730
+ createLogger as createLogger10
1731
+ } from "@tuttiai/core";
1732
+ import { createServer, DEFAULT_PORT, SERVER_VERSION } from "@tuttiai/server";
1733
+ var logger10 = createLogger10("tutti-serve");
1734
+ async function serveCommand(scorePath, options = {}) {
1735
+ const file = resolve10(scorePath ?? "./tutti.score.ts");
1736
+ if (!existsSync10(file)) {
1737
+ logger10.error({ file }, "Score file not found");
1738
+ console.error(chalk10.dim('Run "tutti-ai init" to create a new project.'));
1739
+ process.exit(1);
1740
+ }
1741
+ let score;
1742
+ try {
1743
+ score = await ScoreLoader6.load(file);
1744
+ } catch (err) {
1745
+ logger10.error(
1746
+ { error: err instanceof Error ? err.message : String(err) },
1747
+ "Failed to load score"
1748
+ );
1749
+ process.exit(1);
1750
+ }
1751
+ const providerKeyMap = [
1752
+ [AnthropicProvider4, "ANTHROPIC_API_KEY"],
1753
+ [OpenAIProvider4, "OPENAI_API_KEY"],
1754
+ [GeminiProvider4, "GEMINI_API_KEY"]
1755
+ ];
1756
+ for (const [ProviderClass, envVar] of providerKeyMap) {
1757
+ if (score.provider instanceof ProviderClass) {
1758
+ const key = SecretsManager5.optional(envVar);
1759
+ if (!key) {
1760
+ logger10.error({ envVar }, "Missing API key");
1761
+ process.exit(1);
1762
+ }
1763
+ }
1764
+ }
1765
+ const agentNames = Object.keys(score.agents);
1766
+ const agentName = options.agent ?? (typeof score.entry === "string" ? score.entry : void 0) ?? agentNames[0];
1767
+ if (!agentName || !score.agents[agentName]) {
1768
+ logger10.error(
1769
+ { requested: agentName, available: agentNames },
1770
+ "Agent not found in score"
1771
+ );
1772
+ process.exit(1);
1773
+ }
1774
+ const port = parsePort(options.port);
1775
+ const host = options.host ?? "0.0.0.0";
1776
+ const sharedSessions = options.watch ? new InMemorySessionStore2() : void 0;
1777
+ let runtime = buildRuntime2(score, sharedSessions);
1778
+ let app = await buildApp(runtime, agentName, port, host, options.apiKey);
1779
+ let reactive;
1780
+ if (options.watch) {
1781
+ reactive = new ReactiveScore(score, file);
1782
+ reactive.on("file-change", () => {
1783
+ console.log(chalk10.cyan("\n[tutti] Score changed, reloading..."));
1784
+ });
1785
+ reactive.on("reloaded", async () => {
1786
+ try {
1787
+ const nextScore = reactive.current;
1788
+ const nextRuntime = buildRuntime2(nextScore, sharedSessions);
1789
+ const nextApp = await buildApp(nextRuntime, agentName, port, host, options.apiKey);
1790
+ await app.close();
1791
+ runtime = nextRuntime;
1792
+ app = nextApp;
1793
+ await app.listen({ port, host });
1794
+ console.log(chalk10.green("[tutti] Score reloaded. Server restarted."));
1795
+ } catch (err) {
1796
+ logger10.error(
1797
+ { error: err instanceof Error ? err.message : String(err) },
1798
+ "[tutti] Reload failed \u2014 server continues with previous config"
1799
+ );
1800
+ }
1801
+ });
1802
+ reactive.on("reload-failed", (err) => {
1803
+ logger10.error(
1804
+ { error: err instanceof Error ? err.message : String(err) },
1805
+ "[tutti] Reload failed \u2014 server continues with previous config"
1806
+ );
1807
+ });
1808
+ }
1809
+ await app.listen({ port, host });
1810
+ printBanner(port, host, agentName, score, file, options.watch);
1811
+ const shutdown = async (signal) => {
1812
+ console.log(chalk10.dim("\n" + signal + " received \u2014 shutting down..."));
1813
+ if (reactive) await reactive.close();
1814
+ await app.close();
1815
+ process.exit(0);
1816
+ };
1817
+ process.on("SIGINT", () => void shutdown("SIGINT"));
1818
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
1819
+ }
1820
+ function parsePort(raw) {
1821
+ if (raw === void 0) return DEFAULT_PORT;
1822
+ const n = Number.parseInt(raw, 10);
1823
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
1824
+ logger10.error({ port: raw }, "Invalid port number");
1825
+ process.exit(1);
1826
+ }
1827
+ return n;
1828
+ }
1829
+ function buildRuntime2(score, sessionStore) {
1830
+ return new TuttiRuntime4(
1831
+ score,
1832
+ sessionStore ? { sessionStore } : {}
1833
+ );
1834
+ }
1835
+ async function buildApp(runtime, agentName, port, host, apiKey) {
1836
+ return createServer({
1837
+ port,
1838
+ host,
1839
+ runtime,
1840
+ agent_name: agentName,
1841
+ api_key: apiKey
1842
+ });
1843
+ }
1844
+ function printBanner(port, host, agentName, score, file, watch) {
1845
+ const display = host === "0.0.0.0" || host === "::" ? "localhost" : host;
1846
+ const url = "http://" + display + ":" + port;
1847
+ console.log();
1848
+ console.log(chalk10.bold(" Tutti Server v" + SERVER_VERSION));
1849
+ console.log(chalk10.dim(" " + url));
1850
+ console.log();
1851
+ console.log(chalk10.dim(" Score: ") + (score.name ?? file));
1852
+ console.log(chalk10.dim(" Agent: ") + agentName);
1853
+ console.log(chalk10.dim(" Agents: ") + Object.keys(score.agents).join(", "));
1854
+ if (watch) {
1855
+ console.log(chalk10.dim(" Watch: ") + chalk10.cyan("enabled"));
1856
+ }
1857
+ console.log();
1858
+ console.log(chalk10.dim(" Endpoints:"));
1859
+ console.log(chalk10.dim(" POST ") + url + "/run");
1860
+ console.log(chalk10.dim(" POST ") + url + "/run/stream");
1861
+ console.log(chalk10.dim(" GET ") + url + "/sessions/:id");
1862
+ console.log(chalk10.dim(" GET ") + url + "/health");
1863
+ console.log();
1864
+ }
1865
+
1562
1866
  // src/index.ts
1563
1867
  config();
1564
- var logger10 = createLogger10("tutti-cli");
1868
+ var logger11 = createLogger11("tutti-cli");
1565
1869
  process.on("unhandledRejection", (reason) => {
1566
- logger10.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
1870
+ logger11.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
1567
1871
  process.exit(1);
1568
1872
  });
1569
1873
  process.on("uncaughtException", (err) => {
1570
- logger10.error({ error: err.message }, "Fatal error");
1874
+ logger11.error({ error: err.message }, "Fatal error");
1571
1875
  process.exit(1);
1572
1876
  });
1573
1877
  var program = new Command();
1574
- program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.9.0");
1878
+ program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.11.0");
1575
1879
  program.command("init [project-name]").description("Create a new Tutti project").option("-t, --template <id>", "Project template to use").action(async (projectName, opts) => {
1576
1880
  await initCommand(projectName, opts.template);
1577
1881
  });
1578
1882
  program.command("templates").description("List all available project templates").action(() => {
1579
1883
  templatesCommand();
1580
1884
  });
1581
- program.command("run [score]").description("Run a Tutti score interactively").action(async (score) => {
1582
- await runCommand(score);
1885
+ program.command("run [score]").description("Run a Tutti score interactively").option("-w, --watch", "Reload the score on file changes").action(async (score, opts) => {
1886
+ await runCommand(score, { watch: opts.watch });
1887
+ });
1888
+ program.command("serve [score]").description("Start the Tutti HTTP server").option("-p, --port <number>", "Port to listen on (default: 3847)").option("-H, --host <address>", "Host to bind to (default: 0.0.0.0)").option("-k, --api-key <key>", "API key for bearer auth (or set TUTTI_API_KEY)").option("-a, --agent <name>", "Agent to expose (default: score entry or first agent)").option("-w, --watch", "Reload the score on file changes").action(async (score, opts) => {
1889
+ await serveCommand(score, opts);
1583
1890
  });
1584
1891
  program.command("resume <session-id>").description("Resume a crashed or interrupted run from its last checkpoint").option(
1585
1892
  "--store <backend>",