@tuttiai/cli 0.8.0 → 0.10.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 createLogger9 } from "@tuttiai/core";
5
+ import { createLogger as createLogger10 } from "@tuttiai/core";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/init.ts
@@ -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,55 +660,322 @@ 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
- // src/commands/add.ts
518
- import { existsSync as existsSync3, readFileSync } from "fs";
519
- import { resolve as resolve2 } from "path";
520
- import { execSync } from "child_process";
673
+ // src/commands/resume.ts
674
+ import { existsSync as existsSync3 } from "fs";
675
+ import { resolve as resolve3 } from "path";
676
+ import { createInterface as createInterface2 } from "readline/promises";
521
677
  import chalk3 from "chalk";
522
678
  import ora2 from "ora";
523
- import { createLogger as createLogger3 } from "@tuttiai/core";
679
+ import {
680
+ AnthropicProvider as AnthropicProvider2,
681
+ GeminiProvider as GeminiProvider2,
682
+ OpenAIProvider as OpenAIProvider2,
683
+ ScoreLoader as ScoreLoader2,
684
+ SecretsManager as SecretsManager2,
685
+ TuttiRuntime as TuttiRuntime2,
686
+ createCheckpointStore,
687
+ createLogger as createLogger3
688
+ } from "@tuttiai/core";
524
689
  var logger3 = createLogger3("tutti-cli");
690
+ async function resumeCommand(sessionId, opts) {
691
+ const scoreFile = resolve3(opts.score ?? "./tutti.score.ts");
692
+ if (!existsSync3(scoreFile)) {
693
+ logger3.error({ file: scoreFile }, "Score file not found");
694
+ console.error(chalk3.dim('Run "tutti-ai init" to create a new project.'));
695
+ process.exit(1);
696
+ }
697
+ let score;
698
+ try {
699
+ score = await ScoreLoader2.load(scoreFile);
700
+ } catch (err) {
701
+ logger3.error(
702
+ { error: err instanceof Error ? err.message : String(err) },
703
+ "Failed to load score"
704
+ );
705
+ process.exit(1);
706
+ }
707
+ const providerKeyMap = [
708
+ [AnthropicProvider2, "ANTHROPIC_API_KEY"],
709
+ [OpenAIProvider2, "OPENAI_API_KEY"],
710
+ [GeminiProvider2, "GEMINI_API_KEY"]
711
+ ];
712
+ for (const [ProviderClass, envVar] of providerKeyMap) {
713
+ if (score.provider instanceof ProviderClass) {
714
+ if (!SecretsManager2.optional(envVar)) {
715
+ logger3.error({ envVar }, "Missing API key");
716
+ process.exit(1);
717
+ }
718
+ }
719
+ }
720
+ const agentName = resolveAgentName(score, opts.agent);
721
+ const agent = score.agents[agentName];
722
+ if (!agent) {
723
+ logger3.error(
724
+ { agent: agentName, available: Object.keys(score.agents) },
725
+ "Agent not found in score"
726
+ );
727
+ process.exit(1);
728
+ }
729
+ if (!agent.durable) {
730
+ console.error(
731
+ chalk3.yellow(
732
+ "Agent '" + agentName + "' does not have `durable: true` set \u2014 resume has nothing to restore."
733
+ )
734
+ );
735
+ console.error(
736
+ chalk3.dim(
737
+ "Enable durable checkpointing on the agent before the run that created this session."
738
+ )
739
+ );
740
+ process.exit(1);
741
+ }
742
+ const spinner = ora2({ color: "cyan" }).start("Loading checkpoint...");
743
+ let checkpointStore;
744
+ let checkpoint;
745
+ try {
746
+ checkpointStore = createCheckpointStore({ store: opts.store });
747
+ checkpoint = await checkpointStore.loadLatest(sessionId);
748
+ } catch (err) {
749
+ spinner.fail("Failed to load checkpoint");
750
+ logger3.error(
751
+ { error: err instanceof Error ? err.message : String(err), store: opts.store },
752
+ "Checkpoint store error"
753
+ );
754
+ process.exit(1);
755
+ }
756
+ spinner.stop();
757
+ if (!checkpoint) {
758
+ console.error(
759
+ chalk3.red("No checkpoint found for session " + sessionId + ".")
760
+ );
761
+ console.error(
762
+ chalk3.dim(
763
+ "Verify TUTTI_" + (opts.store === "redis" ? "REDIS" : "PG") + "_URL points to the same " + opts.store + " the original run used."
764
+ )
765
+ );
766
+ process.exit(1);
767
+ }
768
+ printSummary(checkpoint);
769
+ if (!opts.yes && !await confirmResume(checkpoint.turn)) {
770
+ console.log(chalk3.dim("Cancelled."));
771
+ process.exit(0);
772
+ }
773
+ const runtime = new TuttiRuntime2(score, { checkpointStore });
774
+ const sessions = runtime.sessions;
775
+ if ("save" in sessions && typeof sessions.save === "function") {
776
+ sessions.save({
777
+ id: sessionId,
778
+ agent_name: agentName,
779
+ messages: [...checkpoint.messages],
780
+ created_at: checkpoint.saved_at,
781
+ updated_at: /* @__PURE__ */ new Date()
782
+ });
783
+ } else {
784
+ console.error(
785
+ chalk3.red(
786
+ "Session store does not support resume seeding. Use the default InMemorySessionStore or PostgresSessionStore."
787
+ )
788
+ );
789
+ process.exit(1);
790
+ }
791
+ wireProgress(runtime);
792
+ try {
793
+ const result = await runtime.run(agentName, "[resume]", sessionId);
794
+ console.log();
795
+ console.log(chalk3.green("\u2713 Resumed run complete."));
796
+ console.log(chalk3.dim(" Final turn: " + result.turns));
797
+ console.log(chalk3.dim(" Session ID: " + result.session_id));
798
+ console.log(
799
+ chalk3.dim(
800
+ " Token usage: " + result.usage.input_tokens + " in / " + result.usage.output_tokens + " out"
801
+ )
802
+ );
803
+ console.log();
804
+ console.log(result.output);
805
+ } catch (err) {
806
+ logger3.error(
807
+ { error: err instanceof Error ? err.message : String(err) },
808
+ "Resume failed"
809
+ );
810
+ process.exit(1);
811
+ }
812
+ }
813
+ function resolveAgentName(score, override) {
814
+ if (override) return override;
815
+ if (typeof score.entry === "string") return score.entry;
816
+ const first = Object.keys(score.agents)[0];
817
+ if (!first) {
818
+ console.error(chalk3.red("Score has no agents defined."));
819
+ process.exit(1);
820
+ }
821
+ return first;
822
+ }
823
+ function printSummary(checkpoint) {
824
+ console.log();
825
+ console.log(chalk3.cyan.bold("Checkpoint summary"));
826
+ console.log(
827
+ chalk3.dim(" Session ID: ") + checkpoint.session_id
828
+ );
829
+ console.log(
830
+ chalk3.dim(" Last turn: ") + String(checkpoint.turn)
831
+ );
832
+ console.log(
833
+ chalk3.dim(" Saved at: ") + checkpoint.saved_at.toISOString()
834
+ );
835
+ console.log(
836
+ chalk3.dim(" Messages: ") + String(checkpoint.messages.length) + " total"
837
+ );
838
+ console.log();
839
+ console.log(chalk3.cyan("First messages"));
840
+ const preview = checkpoint.messages.slice(0, 3);
841
+ for (const msg of preview) {
842
+ const text = excerpt(messageToText(msg), 200);
843
+ console.log(chalk3.dim(" [" + msg.role + "] ") + text);
844
+ }
845
+ if (checkpoint.messages.length > preview.length) {
846
+ console.log(
847
+ chalk3.dim(
848
+ " \u2026 " + String(checkpoint.messages.length - preview.length) + " more"
849
+ )
850
+ );
851
+ }
852
+ console.log();
853
+ }
854
+ function messageToText(msg) {
855
+ if (typeof msg.content === "string") return msg.content;
856
+ const parts = [];
857
+ for (const block of msg.content) {
858
+ if (block.type === "text") {
859
+ parts.push(block.text);
860
+ } else if (block.type === "tool_use") {
861
+ parts.push("[tool_use " + block.name + "]");
862
+ } else if (block.type === "tool_result") {
863
+ parts.push("[tool_result " + excerpt(block.content, 80) + "]");
864
+ }
865
+ }
866
+ return parts.join(" ");
867
+ }
868
+ function excerpt(text, max) {
869
+ const oneLine = text.replace(/\s+/g, " ").trim();
870
+ return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
871
+ }
872
+ async function confirmResume(turn) {
873
+ const rl = createInterface2({
874
+ input: process.stdin,
875
+ output: process.stdout
876
+ });
877
+ try {
878
+ const answer = (await rl.question(
879
+ chalk3.cyan("Resume from turn " + turn + "? ") + chalk3.dim("(y/n) ")
880
+ )).trim().toLowerCase();
881
+ return answer === "y" || answer === "yes";
882
+ } finally {
883
+ rl.close();
884
+ }
885
+ }
886
+ function wireProgress(runtime) {
887
+ const spinner = ora2({ color: "cyan" });
888
+ let streaming = false;
889
+ runtime.events.on("checkpoint:restored", (e) => {
890
+ console.log(
891
+ chalk3.dim("\u21BB Restored from turn " + e.turn) + chalk3.dim(" (session " + e.session_id.slice(0, 8) + "\u2026)")
892
+ );
893
+ });
894
+ runtime.events.on("checkpoint:saved", (e) => {
895
+ console.log(chalk3.dim("\xB7 Checkpoint saved at turn " + e.turn));
896
+ });
897
+ runtime.events.on("llm:request", () => {
898
+ spinner.start("Thinking...");
899
+ });
900
+ runtime.events.on("token:stream", (e) => {
901
+ if (!streaming) {
902
+ spinner.stop();
903
+ streaming = true;
904
+ }
905
+ process.stdout.write(e.text);
906
+ });
907
+ runtime.events.on("llm:response", () => {
908
+ if (streaming) {
909
+ process.stdout.write("\n");
910
+ } else {
911
+ spinner.stop();
912
+ }
913
+ });
914
+ runtime.events.on("tool:start", (e) => {
915
+ if (streaming) {
916
+ process.stdout.write(chalk3.dim("\n [using: " + e.tool_name + "]"));
917
+ } else {
918
+ spinner.stop();
919
+ console.log(chalk3.dim(" [using: " + e.tool_name + "]"));
920
+ }
921
+ });
922
+ runtime.events.on("tool:end", (e) => {
923
+ if (streaming) {
924
+ process.stdout.write(chalk3.dim(" [done: " + e.tool_name + "]\n"));
925
+ }
926
+ });
927
+ runtime.events.on("tool:error", (e) => {
928
+ spinner.stop();
929
+ logger3.error({ tool: e.tool_name }, "Tool error");
930
+ });
931
+ }
932
+
933
+ // src/commands/add.ts
934
+ import { existsSync as existsSync4, readFileSync } from "fs";
935
+ import { resolve as resolve4 } from "path";
936
+ import { execSync } from "child_process";
937
+ import chalk4 from "chalk";
938
+ import ora3 from "ora";
939
+ import { createLogger as createLogger4 } from "@tuttiai/core";
940
+ var logger4 = createLogger4("tutti-cli");
525
941
  var OFFICIAL_VOICES = {
526
942
  filesystem: {
527
943
  package: "@tuttiai/filesystem",
528
944
  setup: ` Add to your score:
529
- ${chalk3.cyan('import { FilesystemVoice } from "@tuttiai/filesystem"')}
530
- ${chalk3.cyan("voices: [new FilesystemVoice()]")}`
945
+ ${chalk4.cyan('import { FilesystemVoice } from "@tuttiai/filesystem"')}
946
+ ${chalk4.cyan("voices: [new FilesystemVoice()]")}`
531
947
  },
532
948
  github: {
533
949
  package: "@tuttiai/github",
534
- setup: ` Add ${chalk3.bold("GITHUB_TOKEN")} to your .env file:
535
- ${chalk3.cyan("GITHUB_TOKEN=ghp_your_token_here")}
950
+ setup: ` Add ${chalk4.bold("GITHUB_TOKEN")} to your .env file:
951
+ ${chalk4.cyan("GITHUB_TOKEN=ghp_your_token_here")}
536
952
 
537
953
  Add to your score:
538
- ${chalk3.cyan('import { GitHubVoice } from "@tuttiai/github"')}
539
- ${chalk3.cyan("voices: [new GitHubVoice()]")}`
954
+ ${chalk4.cyan('import { GitHubVoice } from "@tuttiai/github"')}
955
+ ${chalk4.cyan("voices: [new GitHubVoice()]")}`
540
956
  },
541
957
  playwright: {
542
958
  package: "@tuttiai/playwright",
543
959
  setup: ` Install the browser:
544
- ${chalk3.cyan("npx playwright install chromium")}
960
+ ${chalk4.cyan("npx playwright install chromium")}
545
961
 
546
962
  Add to your score:
547
- ${chalk3.cyan('import { PlaywrightVoice } from "@tuttiai/playwright"')}
548
- ${chalk3.cyan("voices: [new PlaywrightVoice()]")}`
963
+ ${chalk4.cyan('import { PlaywrightVoice } from "@tuttiai/playwright"')}
964
+ ${chalk4.cyan("voices: [new PlaywrightVoice()]")}`
549
965
  },
550
966
  postgres: {
551
967
  package: "pg",
552
- setup: ` Add ${chalk3.bold("DATABASE_URL")} to your .env file:
553
- ${chalk3.cyan("DATABASE_URL=postgres://user:pass@localhost:5432/tutti")}
968
+ setup: ` Add ${chalk4.bold("DATABASE_URL")} to your .env file:
969
+ ${chalk4.cyan("DATABASE_URL=postgres://user:pass@localhost:5432/tutti")}
554
970
 
555
971
  Add to your score:
556
- ${chalk3.cyan("memory: { provider: 'postgres' }")}
972
+ ${chalk4.cyan("memory: { provider: 'postgres' }")}
557
973
 
558
974
  Or with an explicit URL:
559
- ${chalk3.cyan("memory: { provider: 'postgres', url: process.env.DATABASE_URL }")}
975
+ ${chalk4.cyan("memory: { provider: 'postgres', url: process.env.DATABASE_URL }")}
560
976
 
561
977
  Use the async factory for initialization:
562
- ${chalk3.cyan("const tutti = await TuttiRuntime.create(score)")}`
978
+ ${chalk4.cyan("const tutti = await TuttiRuntime.create(score)")}`
563
979
  }
564
980
  };
565
981
  function resolvePackageName(input) {
@@ -572,8 +988,8 @@ function resolvePackageName(input) {
572
988
  return `@tuttiai/${input}`;
573
989
  }
574
990
  function isAlreadyInstalled(packageName) {
575
- const pkgPath = resolve2(process.cwd(), "package.json");
576
- if (!existsSync3(pkgPath)) return false;
991
+ const pkgPath = resolve4(process.cwd(), "package.json");
992
+ if (!existsSync4(pkgPath)) return false;
577
993
  try {
578
994
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
579
995
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -584,17 +1000,17 @@ function isAlreadyInstalled(packageName) {
584
1000
  }
585
1001
  function addCommand(voiceName) {
586
1002
  const packageName = resolvePackageName(voiceName);
587
- const pkgPath = resolve2(process.cwd(), "package.json");
588
- if (!existsSync3(pkgPath)) {
589
- logger3.error("No package.json found in the current directory");
590
- console.error(chalk3.dim('Run "tutti-ai init" to create a new project first.'));
1003
+ const pkgPath = resolve4(process.cwd(), "package.json");
1004
+ if (!existsSync4(pkgPath)) {
1005
+ logger4.error("No package.json found in the current directory");
1006
+ console.error(chalk4.dim('Run "tutti-ai init" to create a new project first.'));
591
1007
  process.exit(1);
592
1008
  }
593
1009
  if (isAlreadyInstalled(packageName)) {
594
- console.log(chalk3.green(` \u2714 ${packageName} is already installed`));
1010
+ console.log(chalk4.green(` \u2714 ${packageName} is already installed`));
595
1011
  return;
596
1012
  }
597
- const spinner = ora2(`Installing ${packageName}...`).start();
1013
+ const spinner = ora3(`Installing ${packageName}...`).start();
598
1014
  try {
599
1015
  execSync(`npm install ${packageName}`, {
600
1016
  cwd: process.cwd(),
@@ -604,7 +1020,7 @@ function addCommand(voiceName) {
604
1020
  } catch (error) {
605
1021
  spinner.fail(`Failed to install ${packageName}`);
606
1022
  const message = error instanceof Error ? error.message : String(error);
607
- logger3.error({ error: message, package: packageName }, "Installation failed");
1023
+ logger4.error({ error: message, package: packageName }, "Installation failed");
608
1024
  process.exit(1);
609
1025
  }
610
1026
  const official = OFFICIAL_VOICES[voiceName];
@@ -616,43 +1032,43 @@ function addCommand(voiceName) {
616
1032
  } else {
617
1033
  console.log();
618
1034
  console.log(
619
- chalk3.dim(" Check the package README for setup instructions.")
1035
+ chalk4.dim(" Check the package README for setup instructions.")
620
1036
  );
621
1037
  console.log();
622
1038
  }
623
1039
  }
624
1040
 
625
1041
  // src/commands/check.ts
626
- import { existsSync as existsSync4 } from "fs";
627
- import { resolve as resolve3 } from "path";
628
- import chalk4 from "chalk";
1042
+ import { existsSync as existsSync5 } from "fs";
1043
+ import { resolve as resolve5 } from "path";
1044
+ import chalk5 from "chalk";
629
1045
  import {
630
- ScoreLoader as ScoreLoader2,
631
- AnthropicProvider as AnthropicProvider2,
632
- OpenAIProvider as OpenAIProvider2,
633
- GeminiProvider as GeminiProvider2,
634
- SecretsManager as SecretsManager2,
635
- createLogger as createLogger4
1046
+ ScoreLoader as ScoreLoader3,
1047
+ AnthropicProvider as AnthropicProvider3,
1048
+ OpenAIProvider as OpenAIProvider3,
1049
+ GeminiProvider as GeminiProvider3,
1050
+ SecretsManager as SecretsManager3,
1051
+ createLogger as createLogger5
636
1052
  } from "@tuttiai/core";
637
- var logger4 = createLogger4("tutti-cli");
638
- var ok = (msg) => console.log(chalk4.green(" \u2714 " + msg));
639
- var fail = (msg) => console.log(chalk4.red(" \u2718 " + msg));
1053
+ var logger5 = createLogger5("tutti-cli");
1054
+ var ok = (msg) => console.log(chalk5.green(" \u2714 " + msg));
1055
+ var fail = (msg) => console.log(chalk5.red(" \u2718 " + msg));
640
1056
  async function checkCommand(scorePath) {
641
- const file = resolve3(scorePath ?? "./tutti.score.ts");
642
- console.log(chalk4.cyan(`
1057
+ const file = resolve5(scorePath ?? "./tutti.score.ts");
1058
+ console.log(chalk5.cyan(`
643
1059
  Checking ${file}...
644
1060
  `));
645
- if (!existsSync4(file)) {
1061
+ if (!existsSync5(file)) {
646
1062
  fail("Score file not found: " + file);
647
1063
  process.exit(1);
648
1064
  }
649
1065
  let score;
650
1066
  try {
651
- score = await ScoreLoader2.load(file);
1067
+ score = await ScoreLoader3.load(file);
652
1068
  ok("Score file is valid");
653
1069
  } catch (err) {
654
1070
  fail("Score validation failed");
655
- logger4.error(
1071
+ logger5.error(
656
1072
  { error: err instanceof Error ? err.message : String(err) },
657
1073
  "Score validation failed"
658
1074
  );
@@ -660,15 +1076,15 @@ Checking ${file}...
660
1076
  }
661
1077
  let hasErrors = false;
662
1078
  const providerChecks = [
663
- [AnthropicProvider2, "AnthropicProvider", "ANTHROPIC_API_KEY"],
664
- [OpenAIProvider2, "OpenAIProvider", "OPENAI_API_KEY"],
665
- [GeminiProvider2, "GeminiProvider", "GEMINI_API_KEY"]
1079
+ [AnthropicProvider3, "AnthropicProvider", "ANTHROPIC_API_KEY"],
1080
+ [OpenAIProvider3, "OpenAIProvider", "OPENAI_API_KEY"],
1081
+ [GeminiProvider3, "GeminiProvider", "GEMINI_API_KEY"]
666
1082
  ];
667
1083
  let providerDetected = false;
668
1084
  for (const [ProviderClass, name, envVar] of providerChecks) {
669
1085
  if (score.provider instanceof ProviderClass) {
670
1086
  providerDetected = true;
671
- const key = SecretsManager2.optional(envVar);
1087
+ const key = SecretsManager3.optional(envVar);
672
1088
  if (key) {
673
1089
  ok("Provider: " + name + " (" + envVar + " is set)");
674
1090
  } else {
@@ -690,7 +1106,7 @@ Checking ${file}...
690
1106
  };
691
1107
  const envVar = voiceEnvMap[voiceName];
692
1108
  if (envVar) {
693
- const key = SecretsManager2.optional(envVar);
1109
+ const key = SecretsManager3.optional(envVar);
694
1110
  if (key) {
695
1111
  ok(
696
1112
  "Voice: " + voiceName + " on " + agentKey + " (" + envVar + " is set)"
@@ -709,28 +1125,28 @@ Checking ${file}...
709
1125
  console.log("");
710
1126
  if (hasErrors) {
711
1127
  console.log(
712
- chalk4.yellow("Some checks failed. Fix the issues above and re-run.")
1128
+ chalk5.yellow("Some checks failed. Fix the issues above and re-run.")
713
1129
  );
714
1130
  process.exit(1);
715
1131
  } else {
716
1132
  console.log(
717
- chalk4.green("All checks passed.") + chalk4.dim(" Run tutti-ai run to start.")
1133
+ chalk5.green("All checks passed.") + chalk5.dim(" Run tutti-ai run to start.")
718
1134
  );
719
1135
  }
720
1136
  }
721
1137
 
722
1138
  // src/commands/studio.ts
723
- import { existsSync as existsSync5 } from "fs";
724
- import { resolve as resolve4 } from "path";
1139
+ import { existsSync as existsSync6 } from "fs";
1140
+ import { resolve as resolve6 } from "path";
725
1141
  import { execFile } from "child_process";
726
1142
  import express from "express";
727
- import chalk5 from "chalk";
1143
+ import chalk6 from "chalk";
728
1144
  import {
729
- TuttiRuntime as TuttiRuntime2,
730
- ScoreLoader as ScoreLoader3,
731
- createLogger as createLogger5
1145
+ TuttiRuntime as TuttiRuntime3,
1146
+ ScoreLoader as ScoreLoader4,
1147
+ createLogger as createLogger6
732
1148
  } from "@tuttiai/core";
733
- var logger5 = createLogger5("tutti-studio");
1149
+ var logger6 = createLogger6("tutti-studio");
734
1150
  var envPort = Number.parseInt(process.env.PORT ?? "", 10);
735
1151
  var PORT = Number.isInteger(envPort) && envPort > 0 && envPort <= 65535 ? envPort : 4747;
736
1152
  function safeStringify(obj) {
@@ -749,20 +1165,20 @@ function openBrowser(url) {
749
1165
  execFile(cmd, [url]);
750
1166
  }
751
1167
  async function studioCommand(scorePath) {
752
- const file = resolve4(scorePath ?? "./tutti.score.ts");
753
- if (!existsSync5(file)) {
754
- logger5.error({ file }, "Score file not found");
755
- console.error(chalk5.dim('Run "tutti-ai init" to create a new project.'));
1168
+ const file = resolve6(scorePath ?? "./tutti.score.ts");
1169
+ if (!existsSync6(file)) {
1170
+ logger6.error({ file }, "Score file not found");
1171
+ console.error(chalk6.dim('Run "tutti-ai init" to create a new project.'));
756
1172
  process.exit(1);
757
1173
  }
758
1174
  let score;
759
1175
  try {
760
- score = await ScoreLoader3.load(file);
1176
+ score = await ScoreLoader4.load(file);
761
1177
  } catch (err) {
762
- logger5.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to load score");
1178
+ logger6.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to load score");
763
1179
  process.exit(1);
764
1180
  }
765
- const runtime = new TuttiRuntime2(score);
1181
+ const runtime = new TuttiRuntime3(score);
766
1182
  const sessionRegistry = /* @__PURE__ */ new Map();
767
1183
  runtime.events.on("agent:start", (e) => {
768
1184
  if (!sessionRegistry.has(e.session_id)) {
@@ -865,16 +1281,16 @@ async function studioCommand(scorePath) {
865
1281
  app.listen(PORT, () => {
866
1282
  const url = "http://localhost:" + PORT;
867
1283
  console.log();
868
- console.log(chalk5.bold(" Tutti Studio"));
869
- console.log(chalk5.dim(" " + url));
1284
+ console.log(chalk6.bold(" Tutti Studio"));
1285
+ console.log(chalk6.dim(" " + url));
870
1286
  console.log();
871
- console.log(chalk5.dim(" Score: ") + (runtime.score.name ?? file));
872
- console.log(chalk5.dim(" Agents: ") + Object.keys(runtime.score.agents).join(", "));
1287
+ console.log(chalk6.dim(" Score: ") + (runtime.score.name ?? file));
1288
+ console.log(chalk6.dim(" Agents: ") + Object.keys(runtime.score.agents).join(", "));
873
1289
  console.log();
874
1290
  openBrowser(url);
875
1291
  });
876
1292
  process.on("SIGINT", () => {
877
- console.log(chalk5.dim("\nShutting down Tutti Studio..."));
1293
+ console.log(chalk6.dim("\nShutting down Tutti Studio..."));
878
1294
  process.exit(0);
879
1295
  });
880
1296
  }
@@ -883,12 +1299,12 @@ function getStudioHtml() {
883
1299
  }
884
1300
 
885
1301
  // src/commands/search.ts
886
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
887
- import { resolve as resolve5 } from "path";
888
- import chalk6 from "chalk";
889
- import ora3 from "ora";
890
- import { createLogger as createLogger6 } from "@tuttiai/core";
891
- var logger6 = createLogger6("tutti-cli");
1302
+ import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
1303
+ import { resolve as resolve7 } from "path";
1304
+ import chalk7 from "chalk";
1305
+ import ora4 from "ora";
1306
+ import { createLogger as createLogger7 } from "@tuttiai/core";
1307
+ var logger7 = createLogger7("tutti-cli");
892
1308
  var REGISTRY_URL = "https://raw.githubusercontent.com/tuttiai/voices/main/voices.json";
893
1309
  var BUILTIN_VOICES = [
894
1310
  {
@@ -939,7 +1355,7 @@ async function fetchRegistry() {
939
1355
  if (voices.length === 0) throw new Error("Empty registry");
940
1356
  return voices;
941
1357
  } catch {
942
- logger6.debug("Registry unreachable, using built-in voice list");
1358
+ logger7.debug("Registry unreachable, using built-in voice list");
943
1359
  return BUILTIN_VOICES;
944
1360
  }
945
1361
  }
@@ -955,8 +1371,8 @@ function matchesQuery(voice, query) {
955
1371
  return false;
956
1372
  }
957
1373
  function isInstalled(packageName) {
958
- const pkgPath = resolve5(process.cwd(), "package.json");
959
- if (!existsSync6(pkgPath)) return false;
1374
+ const pkgPath = resolve7(process.cwd(), "package.json");
1375
+ if (!existsSync7(pkgPath)) return false;
960
1376
  try {
961
1377
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
962
1378
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -966,35 +1382,35 @@ function isInstalled(packageName) {
966
1382
  }
967
1383
  }
968
1384
  function printVoice(voice, showInstallStatus) {
969
- const badge = voice.official ? chalk6.green(" [official]") : chalk6.blue(" [community]");
1385
+ const badge = voice.official ? chalk7.green(" [official]") : chalk7.blue(" [community]");
970
1386
  const installed = showInstallStatus && isInstalled(voice.package);
971
- const status = showInstallStatus ? installed ? chalk6.green(" \u2714 installed") : chalk6.dim(" not installed") : "";
1387
+ const status = showInstallStatus ? installed ? chalk7.green(" \u2714 installed") : chalk7.dim(" not installed") : "";
972
1388
  console.log();
973
- console.log(" " + chalk6.bold(voice.package) + badge + status);
1389
+ console.log(" " + chalk7.bold(voice.package) + badge + status);
974
1390
  console.log(" " + voice.description);
975
1391
  const installCmd = voice.official && voice.name !== "postgres" ? "tutti-ai add " + voice.name : "npm install " + voice.package;
976
- console.log(" " + chalk6.dim("Install: ") + chalk6.cyan(installCmd));
1392
+ console.log(" " + chalk7.dim("Install: ") + chalk7.cyan(installCmd));
977
1393
  if (voice.tags.length > 0) {
978
- console.log(" " + chalk6.dim("Tags: ") + voice.tags.join(", "));
1394
+ console.log(" " + chalk7.dim("Tags: ") + voice.tags.join(", "));
979
1395
  }
980
1396
  }
981
1397
  async function searchCommand(query) {
982
- const spinner = ora3("Searching the Repertoire...").start();
1398
+ const spinner = ora4("Searching the Repertoire...").start();
983
1399
  const voices = await fetchRegistry();
984
1400
  const results = voices.filter((v) => matchesQuery(v, query));
985
1401
  spinner.stop();
986
1402
  if (results.length === 0) {
987
1403
  console.log();
988
- console.log(chalk6.yellow(' No voices found for "' + query + '"'));
1404
+ console.log(chalk7.yellow(' No voices found for "' + query + '"'));
989
1405
  console.log();
990
- console.log(chalk6.dim(" Browse all: https://tutti-ai.com/voices"));
991
- console.log(chalk6.dim(" Build your own: tutti-ai create voice <name>"));
1406
+ console.log(chalk7.dim(" Browse all: https://tutti-ai.com/voices"));
1407
+ console.log(chalk7.dim(" Build your own: tutti-ai create voice <name>"));
992
1408
  console.log();
993
1409
  return;
994
1410
  }
995
1411
  console.log();
996
1412
  console.log(
997
- " Found " + chalk6.bold(String(results.length)) + " voice" + (results.length !== 1 ? "s" : "") + " matching " + chalk6.cyan("'" + query + "'") + ":"
1413
+ " Found " + chalk7.bold(String(results.length)) + " voice" + (results.length !== 1 ? "s" : "") + " matching " + chalk7.cyan("'" + query + "'") + ":"
998
1414
  );
999
1415
  for (const voice of results) {
1000
1416
  printVoice(voice, false);
@@ -1002,12 +1418,12 @@ async function searchCommand(query) {
1002
1418
  console.log();
1003
1419
  }
1004
1420
  async function voicesCommand() {
1005
- const spinner = ora3("Loading voices...").start();
1421
+ const spinner = ora4("Loading voices...").start();
1006
1422
  const voices = await fetchRegistry();
1007
1423
  const official = voices.filter((v) => v.official);
1008
1424
  spinner.stop();
1009
1425
  console.log();
1010
- console.log(" " + chalk6.bold("Official Tutti Voices"));
1426
+ console.log(" " + chalk7.bold("Official Tutti Voices"));
1011
1427
  console.log();
1012
1428
  for (const voice of official) {
1013
1429
  printVoice(voice, true);
@@ -1015,49 +1431,49 @@ async function voicesCommand() {
1015
1431
  const community = voices.filter((v) => !v.official);
1016
1432
  if (community.length > 0) {
1017
1433
  console.log();
1018
- console.log(" " + chalk6.bold("Community Voices"));
1434
+ console.log(" " + chalk7.bold("Community Voices"));
1019
1435
  for (const voice of community) {
1020
1436
  printVoice(voice, true);
1021
1437
  }
1022
1438
  }
1023
1439
  console.log();
1024
- console.log(chalk6.dim(" Search: tutti-ai search <query>"));
1025
- console.log(chalk6.dim(" Browse: https://tutti-ai.com/voices"));
1440
+ console.log(chalk7.dim(" Search: tutti-ai search <query>"));
1441
+ console.log(chalk7.dim(" Browse: https://tutti-ai.com/voices"));
1026
1442
  console.log();
1027
1443
  }
1028
1444
 
1029
1445
  // src/commands/publish.ts
1030
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1031
- import { resolve as resolve6 } from "path";
1446
+ import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
1447
+ import { resolve as resolve8 } from "path";
1032
1448
  import { execSync as execSync2 } from "child_process";
1033
- import chalk7 from "chalk";
1034
- import ora4 from "ora";
1449
+ import chalk8 from "chalk";
1450
+ import ora5 from "ora";
1035
1451
  import Enquirer2 from "enquirer";
1036
- import { createLogger as createLogger7, SecretsManager as SecretsManager3 } from "@tuttiai/core";
1452
+ import { createLogger as createLogger8, SecretsManager as SecretsManager4 } from "@tuttiai/core";
1037
1453
  var { prompt: prompt2 } = Enquirer2;
1038
- var logger7 = createLogger7("tutti-cli");
1454
+ var logger8 = createLogger8("tutti-cli");
1039
1455
  function readPkg(dir) {
1040
- const p = resolve6(dir, "package.json");
1041
- if (!existsSync7(p)) return void 0;
1456
+ const p = resolve8(dir, "package.json");
1457
+ if (!existsSync8(p)) return void 0;
1042
1458
  return JSON.parse(readFileSync3(p, "utf-8"));
1043
1459
  }
1044
1460
  function run(cmd, cwd) {
1045
1461
  return execSync2(cmd, { cwd, stdio: "pipe", encoding: "utf-8" });
1046
1462
  }
1047
1463
  function fail2(msg) {
1048
- console.error(chalk7.red(" " + msg));
1464
+ console.error(chalk8.red(" " + msg));
1049
1465
  process.exit(1);
1050
1466
  }
1051
- var ok2 = (msg) => console.log(chalk7.green(" \u2714 " + msg));
1467
+ var ok2 = (msg) => console.log(chalk8.green(" \u2714 " + msg));
1052
1468
  async function publishCommand(opts) {
1053
1469
  const cwd = process.cwd();
1054
1470
  const pkg = readPkg(cwd);
1055
1471
  console.log();
1056
- console.log(chalk7.bold(" Tutti Voice Publisher"));
1472
+ console.log(chalk8.bold(" Tutti Voice Publisher"));
1057
1473
  console.log();
1058
- const spinner = ora4("Running pre-flight checks...").start();
1474
+ const spinner = ora5("Running pre-flight checks...").start();
1059
1475
  if (!pkg) fail2("No package.json found in the current directory.");
1060
- if (!existsSync7(resolve6(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
1476
+ if (!existsSync8(resolve8(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
1061
1477
  const missing = [];
1062
1478
  if (!pkg.name) missing.push("name");
1063
1479
  if (!pkg.version) missing.push("version");
@@ -1069,22 +1485,22 @@ async function publishCommand(opts) {
1069
1485
  const version = pkg.version;
1070
1486
  const validName = name.startsWith("@tuttiai/") || name.startsWith("tutti");
1071
1487
  if (!validName) fail2("Package name must start with @tuttiai/ or tutti \u2014 got: " + name);
1072
- const src = readFileSync3(resolve6(cwd, "src/index.ts"), "utf-8");
1488
+ const src = readFileSync3(resolve8(cwd, "src/index.ts"), "utf-8");
1073
1489
  if (!src.includes("required_permissions")) {
1074
1490
  fail2("Voice class must declare required_permissions in src/index.ts");
1075
1491
  }
1076
1492
  spinner.succeed("Pre-flight checks passed");
1077
- const buildSpinner = ora4("Building...").start();
1493
+ const buildSpinner = ora5("Building...").start();
1078
1494
  try {
1079
1495
  run("npm run build", cwd);
1080
1496
  buildSpinner.succeed("Build succeeded");
1081
1497
  } catch (err) {
1082
1498
  buildSpinner.fail("Build failed");
1083
1499
  const msg = err instanceof Error ? err.message : String(err);
1084
- console.error(chalk7.dim(" " + msg.split("\n").slice(0, 5).join("\n ")));
1500
+ console.error(chalk8.dim(" " + msg.split("\n").slice(0, 5).join("\n ")));
1085
1501
  process.exit(1);
1086
1502
  }
1087
- const testSpinner = ora4("Running tests...").start();
1503
+ const testSpinner = ora5("Running tests...").start();
1088
1504
  try {
1089
1505
  run("npx vitest run", cwd);
1090
1506
  testSpinner.succeed("Tests passed");
@@ -1092,15 +1508,15 @@ async function publishCommand(opts) {
1092
1508
  testSpinner.fail("Tests failed");
1093
1509
  process.exit(1);
1094
1510
  }
1095
- const auditSpinner = ora4("Checking vulnerabilities...").start();
1511
+ const auditSpinner = ora5("Checking vulnerabilities...").start();
1096
1512
  try {
1097
1513
  run("npm audit --audit-level=high", cwd);
1098
1514
  auditSpinner.succeed("No high/critical vulnerabilities");
1099
1515
  } catch {
1100
- auditSpinner.stopAndPersist({ symbol: chalk7.yellow("\u26A0"), text: "Vulnerabilities found (npm audit)" });
1516
+ auditSpinner.stopAndPersist({ symbol: chalk8.yellow("\u26A0"), text: "Vulnerabilities found (npm audit)" });
1101
1517
  }
1102
1518
  console.log();
1103
- const drySpinner = ora4("Packing (dry run)...").start();
1519
+ const drySpinner = ora5("Packing (dry run)...").start();
1104
1520
  let packOutput;
1105
1521
  try {
1106
1522
  packOutput = run("npm pack --dry-run 2>&1", cwd);
@@ -1108,24 +1524,24 @@ async function publishCommand(opts) {
1108
1524
  } catch (err) {
1109
1525
  drySpinner.fail("Pack dry-run failed");
1110
1526
  const msg = err instanceof Error ? err.message : String(err);
1111
- console.error(chalk7.dim(" " + msg));
1527
+ console.error(chalk8.dim(" " + msg));
1112
1528
  process.exit(1);
1113
1529
  }
1114
1530
  const fileLines = packOutput.split("\n").filter((l) => l.includes("npm notice") && /\d+(\.\d+)?\s*[kM]?B\s/.test(l)).map((l) => l.replace(/npm notice\s*/, ""));
1115
1531
  if (fileLines.length > 0) {
1116
- console.log(chalk7.dim(" Files:"));
1532
+ console.log(chalk8.dim(" Files:"));
1117
1533
  for (const line of fileLines) {
1118
- console.log(chalk7.dim(" " + line.trim()));
1534
+ console.log(chalk8.dim(" " + line.trim()));
1119
1535
  }
1120
1536
  }
1121
1537
  const sizeLine = packOutput.split("\n").find((l) => l.includes("package size"));
1122
1538
  const totalLine = packOutput.split("\n").find((l) => l.includes("total files"));
1123
- if (sizeLine) console.log(chalk7.dim(" " + sizeLine.replace(/npm notice\s*/, "").trim()));
1124
- if (totalLine) console.log(chalk7.dim(" " + totalLine.replace(/npm notice\s*/, "").trim()));
1539
+ if (sizeLine) console.log(chalk8.dim(" " + sizeLine.replace(/npm notice\s*/, "").trim()));
1540
+ if (totalLine) console.log(chalk8.dim(" " + totalLine.replace(/npm notice\s*/, "").trim()));
1125
1541
  if (opts.dryRun) {
1126
1542
  console.log();
1127
1543
  ok2("Dry run complete \u2014 no packages were published");
1128
- console.log(chalk7.dim(" Run without --dry-run to publish for real."));
1544
+ console.log(chalk8.dim(" Run without --dry-run to publish for real."));
1129
1545
  console.log();
1130
1546
  return;
1131
1547
  }
@@ -1133,38 +1549,38 @@ async function publishCommand(opts) {
1133
1549
  const { confirm } = await prompt2({
1134
1550
  type: "confirm",
1135
1551
  name: "confirm",
1136
- message: "Publish " + chalk7.cyan(name + "@" + version) + "?"
1552
+ message: "Publish " + chalk8.cyan(name + "@" + version) + "?"
1137
1553
  });
1138
1554
  if (!confirm) {
1139
- console.log(chalk7.dim(" Cancelled."));
1555
+ console.log(chalk8.dim(" Cancelled."));
1140
1556
  return;
1141
1557
  }
1142
- const pubSpinner = ora4("Publishing to npm...").start();
1558
+ const pubSpinner = ora5("Publishing to npm...").start();
1143
1559
  try {
1144
1560
  run("npm publish --access public", cwd);
1145
- pubSpinner.succeed("Published " + chalk7.cyan(name + "@" + version));
1561
+ pubSpinner.succeed("Published " + chalk8.cyan(name + "@" + version));
1146
1562
  } catch (err) {
1147
1563
  pubSpinner.fail("Publish failed");
1148
1564
  const msg = err instanceof Error ? err.message : String(err);
1149
- logger7.error({ error: msg }, "npm publish failed");
1565
+ logger8.error({ error: msg }, "npm publish failed");
1150
1566
  process.exit(1);
1151
1567
  }
1152
- const ghToken = SecretsManager3.optional("GITHUB_TOKEN");
1568
+ const ghToken = SecretsManager4.optional("GITHUB_TOKEN");
1153
1569
  let prUrl;
1154
1570
  if (ghToken) {
1155
- const prSpinner = ora4("Opening PR to voice registry...").start();
1571
+ const prSpinner = ora5("Opening PR to voice registry...").start();
1156
1572
  try {
1157
1573
  prUrl = await openRegistryPR(name, version, pkg.description ?? "", ghToken);
1158
1574
  prSpinner.succeed("PR opened: " + prUrl);
1159
1575
  } catch (err) {
1160
1576
  prSpinner.fail("Failed to open PR");
1161
1577
  const msg = err instanceof Error ? err.message : String(err);
1162
- logger7.error({ error: msg }, "Registry PR failed");
1578
+ logger8.error({ error: msg }, "Registry PR failed");
1163
1579
  }
1164
1580
  } else {
1165
1581
  console.log();
1166
- console.log(chalk7.dim(" To list in the Repertoire, set GITHUB_TOKEN and re-run"));
1167
- console.log(chalk7.dim(" Or open a PR manually: github.com/tuttiai/voices"));
1582
+ console.log(chalk8.dim(" To list in the Repertoire, set GITHUB_TOKEN and re-run"));
1583
+ console.log(chalk8.dim(" Or open a PR manually: github.com/tuttiai/voices"));
1168
1584
  }
1169
1585
  console.log();
1170
1586
  ok2(name + "@" + version + " published to npm");
@@ -1249,78 +1665,97 @@ async function openRegistryPR(packageName, version, description, token) {
1249
1665
  }
1250
1666
 
1251
1667
  // src/commands/eval.ts
1252
- import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
1253
- import { resolve as resolve7 } from "path";
1254
- import chalk8 from "chalk";
1255
- import ora5 from "ora";
1668
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
1669
+ import { resolve as resolve9 } from "path";
1670
+ import chalk9 from "chalk";
1671
+ import ora6 from "ora";
1256
1672
  import {
1257
- ScoreLoader as ScoreLoader4,
1673
+ ScoreLoader as ScoreLoader5,
1258
1674
  EvalRunner,
1259
1675
  printEvalTable,
1260
- createLogger as createLogger8
1676
+ createLogger as createLogger9
1261
1677
  } from "@tuttiai/core";
1262
- var logger8 = createLogger8("tutti-cli");
1678
+ var logger9 = createLogger9("tutti-cli");
1263
1679
  async function evalCommand(suitePath, opts) {
1264
- const suiteFile = resolve7(suitePath);
1265
- if (!existsSync8(suiteFile)) {
1266
- logger8.error({ file: suiteFile }, "Suite file not found");
1680
+ const suiteFile = resolve9(suitePath);
1681
+ if (!existsSync9(suiteFile)) {
1682
+ logger9.error({ file: suiteFile }, "Suite file not found");
1267
1683
  process.exit(1);
1268
1684
  }
1269
1685
  let suite;
1270
1686
  try {
1271
1687
  suite = JSON.parse(readFileSync4(suiteFile, "utf-8"));
1272
1688
  } catch (err) {
1273
- logger8.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to parse suite file");
1689
+ logger9.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to parse suite file");
1274
1690
  process.exit(1);
1275
1691
  }
1276
- const scoreFile = resolve7(opts.score ?? "./tutti.score.ts");
1277
- if (!existsSync8(scoreFile)) {
1278
- logger8.error({ file: scoreFile }, "Score file not found");
1692
+ const scoreFile = resolve9(opts.score ?? "./tutti.score.ts");
1693
+ if (!existsSync9(scoreFile)) {
1694
+ logger9.error({ file: scoreFile }, "Score file not found");
1279
1695
  process.exit(1);
1280
1696
  }
1281
- const spinner = ora5("Loading score...").start();
1697
+ const spinner = ora6("Loading score...").start();
1282
1698
  let score;
1283
1699
  try {
1284
- score = await ScoreLoader4.load(scoreFile);
1700
+ score = await ScoreLoader5.load(scoreFile);
1285
1701
  } catch (err) {
1286
1702
  spinner.fail("Failed to load score");
1287
- logger8.error({ error: err instanceof Error ? err.message : String(err) }, "Score load failed");
1703
+ logger9.error({ error: err instanceof Error ? err.message : String(err) }, "Score load failed");
1288
1704
  process.exit(1);
1289
1705
  }
1290
1706
  spinner.succeed("Score loaded");
1291
- const evalSpinner = ora5("Running " + suite.cases.length + " eval cases...").start();
1707
+ const evalSpinner = ora6("Running " + suite.cases.length + " eval cases...").start();
1292
1708
  const runner = new EvalRunner(score);
1293
1709
  const report = await runner.run(suite);
1294
1710
  evalSpinner.stop();
1295
1711
  printEvalTable(report);
1296
1712
  if (opts.ci && report.summary.failed > 0) {
1297
- console.error(chalk8.red(" CI mode: " + report.summary.failed + " case(s) failed"));
1713
+ console.error(chalk9.red(" CI mode: " + report.summary.failed + " case(s) failed"));
1298
1714
  process.exit(1);
1299
1715
  }
1300
1716
  }
1301
1717
 
1302
1718
  // src/index.ts
1303
1719
  config();
1304
- var logger9 = createLogger9("tutti-cli");
1720
+ var logger10 = createLogger10("tutti-cli");
1305
1721
  process.on("unhandledRejection", (reason) => {
1306
- logger9.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
1722
+ logger10.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
1307
1723
  process.exit(1);
1308
1724
  });
1309
1725
  process.on("uncaughtException", (err) => {
1310
- logger9.error({ error: err.message }, "Fatal error");
1726
+ logger10.error({ error: err.message }, "Fatal error");
1311
1727
  process.exit(1);
1312
1728
  });
1313
1729
  var program = new Command();
1314
- program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.8.0");
1730
+ program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.10.0");
1315
1731
  program.command("init [project-name]").description("Create a new Tutti project").option("-t, --template <id>", "Project template to use").action(async (projectName, opts) => {
1316
1732
  await initCommand(projectName, opts.template);
1317
1733
  });
1318
1734
  program.command("templates").description("List all available project templates").action(() => {
1319
1735
  templatesCommand();
1320
1736
  });
1321
- program.command("run [score]").description("Run a Tutti score interactively").action(async (score) => {
1322
- await runCommand(score);
1737
+ program.command("run [score]").description("Run a Tutti score interactively").option("-w, --watch", "Reload the score on file changes").action(async (score, opts) => {
1738
+ await runCommand(score, { watch: opts.watch });
1323
1739
  });
1740
+ program.command("resume <session-id>").description("Resume a crashed or interrupted run from its last checkpoint").option(
1741
+ "--store <backend>",
1742
+ "Durable store the checkpoint was written to (redis | postgres)",
1743
+ "redis"
1744
+ ).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(
1745
+ async (sessionId, opts) => {
1746
+ if (opts.store !== "redis" && opts.store !== "postgres") {
1747
+ console.error("--store must be 'redis' or 'postgres'");
1748
+ process.exit(1);
1749
+ }
1750
+ const resolved = {
1751
+ store: opts.store,
1752
+ ...opts.score !== void 0 ? { score: opts.score } : {},
1753
+ ...opts.agent !== void 0 ? { agent: opts.agent } : {},
1754
+ ...opts.yes !== void 0 ? { yes: opts.yes } : {}
1755
+ };
1756
+ await resumeCommand(sessionId, resolved);
1757
+ }
1758
+ );
1324
1759
  program.command("add <voice>").description("Add a voice to your project").action((voice) => {
1325
1760
  addCommand(voice);
1326
1761
  });