@tuttiai/cli 0.8.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 +434 -155
- 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
|
|
@@ -514,52 +514,312 @@ async function runCommand(scorePath) {
|
|
|
514
514
|
process.exit(0);
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
// src/commands/
|
|
518
|
-
import { existsSync as existsSync3
|
|
517
|
+
// src/commands/resume.ts
|
|
518
|
+
import { existsSync as existsSync3 } from "fs";
|
|
519
519
|
import { resolve as resolve2 } from "path";
|
|
520
|
-
import {
|
|
520
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
521
521
|
import chalk3 from "chalk";
|
|
522
522
|
import ora2 from "ora";
|
|
523
|
-
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";
|
|
524
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");
|
|
525
785
|
var OFFICIAL_VOICES = {
|
|
526
786
|
filesystem: {
|
|
527
787
|
package: "@tuttiai/filesystem",
|
|
528
788
|
setup: ` Add to your score:
|
|
529
|
-
${
|
|
530
|
-
${
|
|
789
|
+
${chalk4.cyan('import { FilesystemVoice } from "@tuttiai/filesystem"')}
|
|
790
|
+
${chalk4.cyan("voices: [new FilesystemVoice()]")}`
|
|
531
791
|
},
|
|
532
792
|
github: {
|
|
533
793
|
package: "@tuttiai/github",
|
|
534
|
-
setup: ` Add ${
|
|
535
|
-
${
|
|
794
|
+
setup: ` Add ${chalk4.bold("GITHUB_TOKEN")} to your .env file:
|
|
795
|
+
${chalk4.cyan("GITHUB_TOKEN=ghp_your_token_here")}
|
|
536
796
|
|
|
537
797
|
Add to your score:
|
|
538
|
-
${
|
|
539
|
-
${
|
|
798
|
+
${chalk4.cyan('import { GitHubVoice } from "@tuttiai/github"')}
|
|
799
|
+
${chalk4.cyan("voices: [new GitHubVoice()]")}`
|
|
540
800
|
},
|
|
541
801
|
playwright: {
|
|
542
802
|
package: "@tuttiai/playwright",
|
|
543
803
|
setup: ` Install the browser:
|
|
544
|
-
${
|
|
804
|
+
${chalk4.cyan("npx playwright install chromium")}
|
|
545
805
|
|
|
546
806
|
Add to your score:
|
|
547
|
-
${
|
|
548
|
-
${
|
|
807
|
+
${chalk4.cyan('import { PlaywrightVoice } from "@tuttiai/playwright"')}
|
|
808
|
+
${chalk4.cyan("voices: [new PlaywrightVoice()]")}`
|
|
549
809
|
},
|
|
550
810
|
postgres: {
|
|
551
811
|
package: "pg",
|
|
552
|
-
setup: ` Add ${
|
|
553
|
-
${
|
|
812
|
+
setup: ` Add ${chalk4.bold("DATABASE_URL")} to your .env file:
|
|
813
|
+
${chalk4.cyan("DATABASE_URL=postgres://user:pass@localhost:5432/tutti")}
|
|
554
814
|
|
|
555
815
|
Add to your score:
|
|
556
|
-
${
|
|
816
|
+
${chalk4.cyan("memory: { provider: 'postgres' }")}
|
|
557
817
|
|
|
558
818
|
Or with an explicit URL:
|
|
559
|
-
${
|
|
819
|
+
${chalk4.cyan("memory: { provider: 'postgres', url: process.env.DATABASE_URL }")}
|
|
560
820
|
|
|
561
821
|
Use the async factory for initialization:
|
|
562
|
-
${
|
|
822
|
+
${chalk4.cyan("const tutti = await TuttiRuntime.create(score)")}`
|
|
563
823
|
}
|
|
564
824
|
};
|
|
565
825
|
function resolvePackageName(input) {
|
|
@@ -572,8 +832,8 @@ function resolvePackageName(input) {
|
|
|
572
832
|
return `@tuttiai/${input}`;
|
|
573
833
|
}
|
|
574
834
|
function isAlreadyInstalled(packageName) {
|
|
575
|
-
const pkgPath =
|
|
576
|
-
if (!
|
|
835
|
+
const pkgPath = resolve3(process.cwd(), "package.json");
|
|
836
|
+
if (!existsSync4(pkgPath)) return false;
|
|
577
837
|
try {
|
|
578
838
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
579
839
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -584,17 +844,17 @@ function isAlreadyInstalled(packageName) {
|
|
|
584
844
|
}
|
|
585
845
|
function addCommand(voiceName) {
|
|
586
846
|
const packageName = resolvePackageName(voiceName);
|
|
587
|
-
const pkgPath =
|
|
588
|
-
if (!
|
|
589
|
-
|
|
590
|
-
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.'));
|
|
591
851
|
process.exit(1);
|
|
592
852
|
}
|
|
593
853
|
if (isAlreadyInstalled(packageName)) {
|
|
594
|
-
console.log(
|
|
854
|
+
console.log(chalk4.green(` \u2714 ${packageName} is already installed`));
|
|
595
855
|
return;
|
|
596
856
|
}
|
|
597
|
-
const spinner =
|
|
857
|
+
const spinner = ora3(`Installing ${packageName}...`).start();
|
|
598
858
|
try {
|
|
599
859
|
execSync(`npm install ${packageName}`, {
|
|
600
860
|
cwd: process.cwd(),
|
|
@@ -604,7 +864,7 @@ function addCommand(voiceName) {
|
|
|
604
864
|
} catch (error) {
|
|
605
865
|
spinner.fail(`Failed to install ${packageName}`);
|
|
606
866
|
const message = error instanceof Error ? error.message : String(error);
|
|
607
|
-
|
|
867
|
+
logger4.error({ error: message, package: packageName }, "Installation failed");
|
|
608
868
|
process.exit(1);
|
|
609
869
|
}
|
|
610
870
|
const official = OFFICIAL_VOICES[voiceName];
|
|
@@ -616,43 +876,43 @@ function addCommand(voiceName) {
|
|
|
616
876
|
} else {
|
|
617
877
|
console.log();
|
|
618
878
|
console.log(
|
|
619
|
-
|
|
879
|
+
chalk4.dim(" Check the package README for setup instructions.")
|
|
620
880
|
);
|
|
621
881
|
console.log();
|
|
622
882
|
}
|
|
623
883
|
}
|
|
624
884
|
|
|
625
885
|
// src/commands/check.ts
|
|
626
|
-
import { existsSync as
|
|
627
|
-
import { resolve as
|
|
628
|
-
import
|
|
886
|
+
import { existsSync as existsSync5 } from "fs";
|
|
887
|
+
import { resolve as resolve4 } from "path";
|
|
888
|
+
import chalk5 from "chalk";
|
|
629
889
|
import {
|
|
630
|
-
ScoreLoader as
|
|
631
|
-
AnthropicProvider as
|
|
632
|
-
OpenAIProvider as
|
|
633
|
-
GeminiProvider as
|
|
634
|
-
SecretsManager as
|
|
635
|
-
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
|
|
636
896
|
} from "@tuttiai/core";
|
|
637
|
-
var
|
|
638
|
-
var ok = (msg) => console.log(
|
|
639
|
-
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));
|
|
640
900
|
async function checkCommand(scorePath) {
|
|
641
|
-
const file =
|
|
642
|
-
console.log(
|
|
901
|
+
const file = resolve4(scorePath ?? "./tutti.score.ts");
|
|
902
|
+
console.log(chalk5.cyan(`
|
|
643
903
|
Checking ${file}...
|
|
644
904
|
`));
|
|
645
|
-
if (!
|
|
905
|
+
if (!existsSync5(file)) {
|
|
646
906
|
fail("Score file not found: " + file);
|
|
647
907
|
process.exit(1);
|
|
648
908
|
}
|
|
649
909
|
let score;
|
|
650
910
|
try {
|
|
651
|
-
score = await
|
|
911
|
+
score = await ScoreLoader3.load(file);
|
|
652
912
|
ok("Score file is valid");
|
|
653
913
|
} catch (err) {
|
|
654
914
|
fail("Score validation failed");
|
|
655
|
-
|
|
915
|
+
logger5.error(
|
|
656
916
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
657
917
|
"Score validation failed"
|
|
658
918
|
);
|
|
@@ -660,15 +920,15 @@ Checking ${file}...
|
|
|
660
920
|
}
|
|
661
921
|
let hasErrors = false;
|
|
662
922
|
const providerChecks = [
|
|
663
|
-
[
|
|
664
|
-
[
|
|
665
|
-
[
|
|
923
|
+
[AnthropicProvider3, "AnthropicProvider", "ANTHROPIC_API_KEY"],
|
|
924
|
+
[OpenAIProvider3, "OpenAIProvider", "OPENAI_API_KEY"],
|
|
925
|
+
[GeminiProvider3, "GeminiProvider", "GEMINI_API_KEY"]
|
|
666
926
|
];
|
|
667
927
|
let providerDetected = false;
|
|
668
928
|
for (const [ProviderClass, name, envVar] of providerChecks) {
|
|
669
929
|
if (score.provider instanceof ProviderClass) {
|
|
670
930
|
providerDetected = true;
|
|
671
|
-
const key =
|
|
931
|
+
const key = SecretsManager3.optional(envVar);
|
|
672
932
|
if (key) {
|
|
673
933
|
ok("Provider: " + name + " (" + envVar + " is set)");
|
|
674
934
|
} else {
|
|
@@ -690,7 +950,7 @@ Checking ${file}...
|
|
|
690
950
|
};
|
|
691
951
|
const envVar = voiceEnvMap[voiceName];
|
|
692
952
|
if (envVar) {
|
|
693
|
-
const key =
|
|
953
|
+
const key = SecretsManager3.optional(envVar);
|
|
694
954
|
if (key) {
|
|
695
955
|
ok(
|
|
696
956
|
"Voice: " + voiceName + " on " + agentKey + " (" + envVar + " is set)"
|
|
@@ -709,28 +969,28 @@ Checking ${file}...
|
|
|
709
969
|
console.log("");
|
|
710
970
|
if (hasErrors) {
|
|
711
971
|
console.log(
|
|
712
|
-
|
|
972
|
+
chalk5.yellow("Some checks failed. Fix the issues above and re-run.")
|
|
713
973
|
);
|
|
714
974
|
process.exit(1);
|
|
715
975
|
} else {
|
|
716
976
|
console.log(
|
|
717
|
-
|
|
977
|
+
chalk5.green("All checks passed.") + chalk5.dim(" Run tutti-ai run to start.")
|
|
718
978
|
);
|
|
719
979
|
}
|
|
720
980
|
}
|
|
721
981
|
|
|
722
982
|
// src/commands/studio.ts
|
|
723
|
-
import { existsSync as
|
|
724
|
-
import { resolve as
|
|
983
|
+
import { existsSync as existsSync6 } from "fs";
|
|
984
|
+
import { resolve as resolve5 } from "path";
|
|
725
985
|
import { execFile } from "child_process";
|
|
726
986
|
import express from "express";
|
|
727
|
-
import
|
|
987
|
+
import chalk6 from "chalk";
|
|
728
988
|
import {
|
|
729
|
-
TuttiRuntime as
|
|
730
|
-
ScoreLoader as
|
|
731
|
-
createLogger as
|
|
989
|
+
TuttiRuntime as TuttiRuntime3,
|
|
990
|
+
ScoreLoader as ScoreLoader4,
|
|
991
|
+
createLogger as createLogger6
|
|
732
992
|
} from "@tuttiai/core";
|
|
733
|
-
var
|
|
993
|
+
var logger6 = createLogger6("tutti-studio");
|
|
734
994
|
var envPort = Number.parseInt(process.env.PORT ?? "", 10);
|
|
735
995
|
var PORT = Number.isInteger(envPort) && envPort > 0 && envPort <= 65535 ? envPort : 4747;
|
|
736
996
|
function safeStringify(obj) {
|
|
@@ -749,20 +1009,20 @@ function openBrowser(url) {
|
|
|
749
1009
|
execFile(cmd, [url]);
|
|
750
1010
|
}
|
|
751
1011
|
async function studioCommand(scorePath) {
|
|
752
|
-
const file =
|
|
753
|
-
if (!
|
|
754
|
-
|
|
755
|
-
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.'));
|
|
756
1016
|
process.exit(1);
|
|
757
1017
|
}
|
|
758
1018
|
let score;
|
|
759
1019
|
try {
|
|
760
|
-
score = await
|
|
1020
|
+
score = await ScoreLoader4.load(file);
|
|
761
1021
|
} catch (err) {
|
|
762
|
-
|
|
1022
|
+
logger6.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to load score");
|
|
763
1023
|
process.exit(1);
|
|
764
1024
|
}
|
|
765
|
-
const runtime = new
|
|
1025
|
+
const runtime = new TuttiRuntime3(score);
|
|
766
1026
|
const sessionRegistry = /* @__PURE__ */ new Map();
|
|
767
1027
|
runtime.events.on("agent:start", (e) => {
|
|
768
1028
|
if (!sessionRegistry.has(e.session_id)) {
|
|
@@ -865,16 +1125,16 @@ async function studioCommand(scorePath) {
|
|
|
865
1125
|
app.listen(PORT, () => {
|
|
866
1126
|
const url = "http://localhost:" + PORT;
|
|
867
1127
|
console.log();
|
|
868
|
-
console.log(
|
|
869
|
-
console.log(
|
|
1128
|
+
console.log(chalk6.bold(" Tutti Studio"));
|
|
1129
|
+
console.log(chalk6.dim(" " + url));
|
|
870
1130
|
console.log();
|
|
871
|
-
console.log(
|
|
872
|
-
console.log(
|
|
1131
|
+
console.log(chalk6.dim(" Score: ") + (runtime.score.name ?? file));
|
|
1132
|
+
console.log(chalk6.dim(" Agents: ") + Object.keys(runtime.score.agents).join(", "));
|
|
873
1133
|
console.log();
|
|
874
1134
|
openBrowser(url);
|
|
875
1135
|
});
|
|
876
1136
|
process.on("SIGINT", () => {
|
|
877
|
-
console.log(
|
|
1137
|
+
console.log(chalk6.dim("\nShutting down Tutti Studio..."));
|
|
878
1138
|
process.exit(0);
|
|
879
1139
|
});
|
|
880
1140
|
}
|
|
@@ -883,12 +1143,12 @@ function getStudioHtml() {
|
|
|
883
1143
|
}
|
|
884
1144
|
|
|
885
1145
|
// src/commands/search.ts
|
|
886
|
-
import { existsSync as
|
|
887
|
-
import { resolve as
|
|
888
|
-
import
|
|
889
|
-
import
|
|
890
|
-
import { createLogger as
|
|
891
|
-
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");
|
|
892
1152
|
var REGISTRY_URL = "https://raw.githubusercontent.com/tuttiai/voices/main/voices.json";
|
|
893
1153
|
var BUILTIN_VOICES = [
|
|
894
1154
|
{
|
|
@@ -939,7 +1199,7 @@ async function fetchRegistry() {
|
|
|
939
1199
|
if (voices.length === 0) throw new Error("Empty registry");
|
|
940
1200
|
return voices;
|
|
941
1201
|
} catch {
|
|
942
|
-
|
|
1202
|
+
logger7.debug("Registry unreachable, using built-in voice list");
|
|
943
1203
|
return BUILTIN_VOICES;
|
|
944
1204
|
}
|
|
945
1205
|
}
|
|
@@ -955,8 +1215,8 @@ function matchesQuery(voice, query) {
|
|
|
955
1215
|
return false;
|
|
956
1216
|
}
|
|
957
1217
|
function isInstalled(packageName) {
|
|
958
|
-
const pkgPath =
|
|
959
|
-
if (!
|
|
1218
|
+
const pkgPath = resolve6(process.cwd(), "package.json");
|
|
1219
|
+
if (!existsSync7(pkgPath)) return false;
|
|
960
1220
|
try {
|
|
961
1221
|
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
962
1222
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -966,35 +1226,35 @@ function isInstalled(packageName) {
|
|
|
966
1226
|
}
|
|
967
1227
|
}
|
|
968
1228
|
function printVoice(voice, showInstallStatus) {
|
|
969
|
-
const badge = voice.official ?
|
|
1229
|
+
const badge = voice.official ? chalk7.green(" [official]") : chalk7.blue(" [community]");
|
|
970
1230
|
const installed = showInstallStatus && isInstalled(voice.package);
|
|
971
|
-
const status = showInstallStatus ? installed ?
|
|
1231
|
+
const status = showInstallStatus ? installed ? chalk7.green(" \u2714 installed") : chalk7.dim(" not installed") : "";
|
|
972
1232
|
console.log();
|
|
973
|
-
console.log(" " +
|
|
1233
|
+
console.log(" " + chalk7.bold(voice.package) + badge + status);
|
|
974
1234
|
console.log(" " + voice.description);
|
|
975
1235
|
const installCmd = voice.official && voice.name !== "postgres" ? "tutti-ai add " + voice.name : "npm install " + voice.package;
|
|
976
|
-
console.log(" " +
|
|
1236
|
+
console.log(" " + chalk7.dim("Install: ") + chalk7.cyan(installCmd));
|
|
977
1237
|
if (voice.tags.length > 0) {
|
|
978
|
-
console.log(" " +
|
|
1238
|
+
console.log(" " + chalk7.dim("Tags: ") + voice.tags.join(", "));
|
|
979
1239
|
}
|
|
980
1240
|
}
|
|
981
1241
|
async function searchCommand(query) {
|
|
982
|
-
const spinner =
|
|
1242
|
+
const spinner = ora4("Searching the Repertoire...").start();
|
|
983
1243
|
const voices = await fetchRegistry();
|
|
984
1244
|
const results = voices.filter((v) => matchesQuery(v, query));
|
|
985
1245
|
spinner.stop();
|
|
986
1246
|
if (results.length === 0) {
|
|
987
1247
|
console.log();
|
|
988
|
-
console.log(
|
|
1248
|
+
console.log(chalk7.yellow(' No voices found for "' + query + '"'));
|
|
989
1249
|
console.log();
|
|
990
|
-
console.log(
|
|
991
|
-
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>"));
|
|
992
1252
|
console.log();
|
|
993
1253
|
return;
|
|
994
1254
|
}
|
|
995
1255
|
console.log();
|
|
996
1256
|
console.log(
|
|
997
|
-
" Found " +
|
|
1257
|
+
" Found " + chalk7.bold(String(results.length)) + " voice" + (results.length !== 1 ? "s" : "") + " matching " + chalk7.cyan("'" + query + "'") + ":"
|
|
998
1258
|
);
|
|
999
1259
|
for (const voice of results) {
|
|
1000
1260
|
printVoice(voice, false);
|
|
@@ -1002,12 +1262,12 @@ async function searchCommand(query) {
|
|
|
1002
1262
|
console.log();
|
|
1003
1263
|
}
|
|
1004
1264
|
async function voicesCommand() {
|
|
1005
|
-
const spinner =
|
|
1265
|
+
const spinner = ora4("Loading voices...").start();
|
|
1006
1266
|
const voices = await fetchRegistry();
|
|
1007
1267
|
const official = voices.filter((v) => v.official);
|
|
1008
1268
|
spinner.stop();
|
|
1009
1269
|
console.log();
|
|
1010
|
-
console.log(" " +
|
|
1270
|
+
console.log(" " + chalk7.bold("Official Tutti Voices"));
|
|
1011
1271
|
console.log();
|
|
1012
1272
|
for (const voice of official) {
|
|
1013
1273
|
printVoice(voice, true);
|
|
@@ -1015,49 +1275,49 @@ async function voicesCommand() {
|
|
|
1015
1275
|
const community = voices.filter((v) => !v.official);
|
|
1016
1276
|
if (community.length > 0) {
|
|
1017
1277
|
console.log();
|
|
1018
|
-
console.log(" " +
|
|
1278
|
+
console.log(" " + chalk7.bold("Community Voices"));
|
|
1019
1279
|
for (const voice of community) {
|
|
1020
1280
|
printVoice(voice, true);
|
|
1021
1281
|
}
|
|
1022
1282
|
}
|
|
1023
1283
|
console.log();
|
|
1024
|
-
console.log(
|
|
1025
|
-
console.log(
|
|
1284
|
+
console.log(chalk7.dim(" Search: tutti-ai search <query>"));
|
|
1285
|
+
console.log(chalk7.dim(" Browse: https://tutti-ai.com/voices"));
|
|
1026
1286
|
console.log();
|
|
1027
1287
|
}
|
|
1028
1288
|
|
|
1029
1289
|
// src/commands/publish.ts
|
|
1030
|
-
import { existsSync as
|
|
1031
|
-
import { resolve as
|
|
1290
|
+
import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
|
|
1291
|
+
import { resolve as resolve7 } from "path";
|
|
1032
1292
|
import { execSync as execSync2 } from "child_process";
|
|
1033
|
-
import
|
|
1034
|
-
import
|
|
1293
|
+
import chalk8 from "chalk";
|
|
1294
|
+
import ora5 from "ora";
|
|
1035
1295
|
import Enquirer2 from "enquirer";
|
|
1036
|
-
import { createLogger as
|
|
1296
|
+
import { createLogger as createLogger8, SecretsManager as SecretsManager4 } from "@tuttiai/core";
|
|
1037
1297
|
var { prompt: prompt2 } = Enquirer2;
|
|
1038
|
-
var
|
|
1298
|
+
var logger8 = createLogger8("tutti-cli");
|
|
1039
1299
|
function readPkg(dir) {
|
|
1040
|
-
const p =
|
|
1041
|
-
if (!
|
|
1300
|
+
const p = resolve7(dir, "package.json");
|
|
1301
|
+
if (!existsSync8(p)) return void 0;
|
|
1042
1302
|
return JSON.parse(readFileSync3(p, "utf-8"));
|
|
1043
1303
|
}
|
|
1044
1304
|
function run(cmd, cwd) {
|
|
1045
1305
|
return execSync2(cmd, { cwd, stdio: "pipe", encoding: "utf-8" });
|
|
1046
1306
|
}
|
|
1047
1307
|
function fail2(msg) {
|
|
1048
|
-
console.error(
|
|
1308
|
+
console.error(chalk8.red(" " + msg));
|
|
1049
1309
|
process.exit(1);
|
|
1050
1310
|
}
|
|
1051
|
-
var ok2 = (msg) => console.log(
|
|
1311
|
+
var ok2 = (msg) => console.log(chalk8.green(" \u2714 " + msg));
|
|
1052
1312
|
async function publishCommand(opts) {
|
|
1053
1313
|
const cwd = process.cwd();
|
|
1054
1314
|
const pkg = readPkg(cwd);
|
|
1055
1315
|
console.log();
|
|
1056
|
-
console.log(
|
|
1316
|
+
console.log(chalk8.bold(" Tutti Voice Publisher"));
|
|
1057
1317
|
console.log();
|
|
1058
|
-
const spinner =
|
|
1318
|
+
const spinner = ora5("Running pre-flight checks...").start();
|
|
1059
1319
|
if (!pkg) fail2("No package.json found in the current directory.");
|
|
1060
|
-
if (!
|
|
1320
|
+
if (!existsSync8(resolve7(cwd, "src/index.ts"))) fail2("No src/index.ts found \u2014 are you inside a voice directory?");
|
|
1061
1321
|
const missing = [];
|
|
1062
1322
|
if (!pkg.name) missing.push("name");
|
|
1063
1323
|
if (!pkg.version) missing.push("version");
|
|
@@ -1069,22 +1329,22 @@ async function publishCommand(opts) {
|
|
|
1069
1329
|
const version = pkg.version;
|
|
1070
1330
|
const validName = name.startsWith("@tuttiai/") || name.startsWith("tutti");
|
|
1071
1331
|
if (!validName) fail2("Package name must start with @tuttiai/ or tutti \u2014 got: " + name);
|
|
1072
|
-
const src = readFileSync3(
|
|
1332
|
+
const src = readFileSync3(resolve7(cwd, "src/index.ts"), "utf-8");
|
|
1073
1333
|
if (!src.includes("required_permissions")) {
|
|
1074
1334
|
fail2("Voice class must declare required_permissions in src/index.ts");
|
|
1075
1335
|
}
|
|
1076
1336
|
spinner.succeed("Pre-flight checks passed");
|
|
1077
|
-
const buildSpinner =
|
|
1337
|
+
const buildSpinner = ora5("Building...").start();
|
|
1078
1338
|
try {
|
|
1079
1339
|
run("npm run build", cwd);
|
|
1080
1340
|
buildSpinner.succeed("Build succeeded");
|
|
1081
1341
|
} catch (err) {
|
|
1082
1342
|
buildSpinner.fail("Build failed");
|
|
1083
1343
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1084
|
-
console.error(
|
|
1344
|
+
console.error(chalk8.dim(" " + msg.split("\n").slice(0, 5).join("\n ")));
|
|
1085
1345
|
process.exit(1);
|
|
1086
1346
|
}
|
|
1087
|
-
const testSpinner =
|
|
1347
|
+
const testSpinner = ora5("Running tests...").start();
|
|
1088
1348
|
try {
|
|
1089
1349
|
run("npx vitest run", cwd);
|
|
1090
1350
|
testSpinner.succeed("Tests passed");
|
|
@@ -1092,15 +1352,15 @@ async function publishCommand(opts) {
|
|
|
1092
1352
|
testSpinner.fail("Tests failed");
|
|
1093
1353
|
process.exit(1);
|
|
1094
1354
|
}
|
|
1095
|
-
const auditSpinner =
|
|
1355
|
+
const auditSpinner = ora5("Checking vulnerabilities...").start();
|
|
1096
1356
|
try {
|
|
1097
1357
|
run("npm audit --audit-level=high", cwd);
|
|
1098
1358
|
auditSpinner.succeed("No high/critical vulnerabilities");
|
|
1099
1359
|
} catch {
|
|
1100
|
-
auditSpinner.stopAndPersist({ symbol:
|
|
1360
|
+
auditSpinner.stopAndPersist({ symbol: chalk8.yellow("\u26A0"), text: "Vulnerabilities found (npm audit)" });
|
|
1101
1361
|
}
|
|
1102
1362
|
console.log();
|
|
1103
|
-
const drySpinner =
|
|
1363
|
+
const drySpinner = ora5("Packing (dry run)...").start();
|
|
1104
1364
|
let packOutput;
|
|
1105
1365
|
try {
|
|
1106
1366
|
packOutput = run("npm pack --dry-run 2>&1", cwd);
|
|
@@ -1108,24 +1368,24 @@ async function publishCommand(opts) {
|
|
|
1108
1368
|
} catch (err) {
|
|
1109
1369
|
drySpinner.fail("Pack dry-run failed");
|
|
1110
1370
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1111
|
-
console.error(
|
|
1371
|
+
console.error(chalk8.dim(" " + msg));
|
|
1112
1372
|
process.exit(1);
|
|
1113
1373
|
}
|
|
1114
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*/, ""));
|
|
1115
1375
|
if (fileLines.length > 0) {
|
|
1116
|
-
console.log(
|
|
1376
|
+
console.log(chalk8.dim(" Files:"));
|
|
1117
1377
|
for (const line of fileLines) {
|
|
1118
|
-
console.log(
|
|
1378
|
+
console.log(chalk8.dim(" " + line.trim()));
|
|
1119
1379
|
}
|
|
1120
1380
|
}
|
|
1121
1381
|
const sizeLine = packOutput.split("\n").find((l) => l.includes("package size"));
|
|
1122
1382
|
const totalLine = packOutput.split("\n").find((l) => l.includes("total files"));
|
|
1123
|
-
if (sizeLine) console.log(
|
|
1124
|
-
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()));
|
|
1125
1385
|
if (opts.dryRun) {
|
|
1126
1386
|
console.log();
|
|
1127
1387
|
ok2("Dry run complete \u2014 no packages were published");
|
|
1128
|
-
console.log(
|
|
1388
|
+
console.log(chalk8.dim(" Run without --dry-run to publish for real."));
|
|
1129
1389
|
console.log();
|
|
1130
1390
|
return;
|
|
1131
1391
|
}
|
|
@@ -1133,38 +1393,38 @@ async function publishCommand(opts) {
|
|
|
1133
1393
|
const { confirm } = await prompt2({
|
|
1134
1394
|
type: "confirm",
|
|
1135
1395
|
name: "confirm",
|
|
1136
|
-
message: "Publish " +
|
|
1396
|
+
message: "Publish " + chalk8.cyan(name + "@" + version) + "?"
|
|
1137
1397
|
});
|
|
1138
1398
|
if (!confirm) {
|
|
1139
|
-
console.log(
|
|
1399
|
+
console.log(chalk8.dim(" Cancelled."));
|
|
1140
1400
|
return;
|
|
1141
1401
|
}
|
|
1142
|
-
const pubSpinner =
|
|
1402
|
+
const pubSpinner = ora5("Publishing to npm...").start();
|
|
1143
1403
|
try {
|
|
1144
1404
|
run("npm publish --access public", cwd);
|
|
1145
|
-
pubSpinner.succeed("Published " +
|
|
1405
|
+
pubSpinner.succeed("Published " + chalk8.cyan(name + "@" + version));
|
|
1146
1406
|
} catch (err) {
|
|
1147
1407
|
pubSpinner.fail("Publish failed");
|
|
1148
1408
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1149
|
-
|
|
1409
|
+
logger8.error({ error: msg }, "npm publish failed");
|
|
1150
1410
|
process.exit(1);
|
|
1151
1411
|
}
|
|
1152
|
-
const ghToken =
|
|
1412
|
+
const ghToken = SecretsManager4.optional("GITHUB_TOKEN");
|
|
1153
1413
|
let prUrl;
|
|
1154
1414
|
if (ghToken) {
|
|
1155
|
-
const prSpinner =
|
|
1415
|
+
const prSpinner = ora5("Opening PR to voice registry...").start();
|
|
1156
1416
|
try {
|
|
1157
1417
|
prUrl = await openRegistryPR(name, version, pkg.description ?? "", ghToken);
|
|
1158
1418
|
prSpinner.succeed("PR opened: " + prUrl);
|
|
1159
1419
|
} catch (err) {
|
|
1160
1420
|
prSpinner.fail("Failed to open PR");
|
|
1161
1421
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1162
|
-
|
|
1422
|
+
logger8.error({ error: msg }, "Registry PR failed");
|
|
1163
1423
|
}
|
|
1164
1424
|
} else {
|
|
1165
1425
|
console.log();
|
|
1166
|
-
console.log(
|
|
1167
|
-
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"));
|
|
1168
1428
|
}
|
|
1169
1429
|
console.log();
|
|
1170
1430
|
ok2(name + "@" + version + " published to npm");
|
|
@@ -1249,69 +1509,69 @@ async function openRegistryPR(packageName, version, description, token) {
|
|
|
1249
1509
|
}
|
|
1250
1510
|
|
|
1251
1511
|
// src/commands/eval.ts
|
|
1252
|
-
import { existsSync as
|
|
1253
|
-
import { resolve as
|
|
1254
|
-
import
|
|
1255
|
-
import
|
|
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";
|
|
1256
1516
|
import {
|
|
1257
|
-
ScoreLoader as
|
|
1517
|
+
ScoreLoader as ScoreLoader5,
|
|
1258
1518
|
EvalRunner,
|
|
1259
1519
|
printEvalTable,
|
|
1260
|
-
createLogger as
|
|
1520
|
+
createLogger as createLogger9
|
|
1261
1521
|
} from "@tuttiai/core";
|
|
1262
|
-
var
|
|
1522
|
+
var logger9 = createLogger9("tutti-cli");
|
|
1263
1523
|
async function evalCommand(suitePath, opts) {
|
|
1264
|
-
const suiteFile =
|
|
1265
|
-
if (!
|
|
1266
|
-
|
|
1524
|
+
const suiteFile = resolve8(suitePath);
|
|
1525
|
+
if (!existsSync9(suiteFile)) {
|
|
1526
|
+
logger9.error({ file: suiteFile }, "Suite file not found");
|
|
1267
1527
|
process.exit(1);
|
|
1268
1528
|
}
|
|
1269
1529
|
let suite;
|
|
1270
1530
|
try {
|
|
1271
1531
|
suite = JSON.parse(readFileSync4(suiteFile, "utf-8"));
|
|
1272
1532
|
} catch (err) {
|
|
1273
|
-
|
|
1533
|
+
logger9.error({ error: err instanceof Error ? err.message : String(err) }, "Failed to parse suite file");
|
|
1274
1534
|
process.exit(1);
|
|
1275
1535
|
}
|
|
1276
|
-
const scoreFile =
|
|
1277
|
-
if (!
|
|
1278
|
-
|
|
1536
|
+
const scoreFile = resolve8(opts.score ?? "./tutti.score.ts");
|
|
1537
|
+
if (!existsSync9(scoreFile)) {
|
|
1538
|
+
logger9.error({ file: scoreFile }, "Score file not found");
|
|
1279
1539
|
process.exit(1);
|
|
1280
1540
|
}
|
|
1281
|
-
const spinner =
|
|
1541
|
+
const spinner = ora6("Loading score...").start();
|
|
1282
1542
|
let score;
|
|
1283
1543
|
try {
|
|
1284
|
-
score = await
|
|
1544
|
+
score = await ScoreLoader5.load(scoreFile);
|
|
1285
1545
|
} catch (err) {
|
|
1286
1546
|
spinner.fail("Failed to load score");
|
|
1287
|
-
|
|
1547
|
+
logger9.error({ error: err instanceof Error ? err.message : String(err) }, "Score load failed");
|
|
1288
1548
|
process.exit(1);
|
|
1289
1549
|
}
|
|
1290
1550
|
spinner.succeed("Score loaded");
|
|
1291
|
-
const evalSpinner =
|
|
1551
|
+
const evalSpinner = ora6("Running " + suite.cases.length + " eval cases...").start();
|
|
1292
1552
|
const runner = new EvalRunner(score);
|
|
1293
1553
|
const report = await runner.run(suite);
|
|
1294
1554
|
evalSpinner.stop();
|
|
1295
1555
|
printEvalTable(report);
|
|
1296
1556
|
if (opts.ci && report.summary.failed > 0) {
|
|
1297
|
-
console.error(
|
|
1557
|
+
console.error(chalk9.red(" CI mode: " + report.summary.failed + " case(s) failed"));
|
|
1298
1558
|
process.exit(1);
|
|
1299
1559
|
}
|
|
1300
1560
|
}
|
|
1301
1561
|
|
|
1302
1562
|
// src/index.ts
|
|
1303
1563
|
config();
|
|
1304
|
-
var
|
|
1564
|
+
var logger10 = createLogger10("tutti-cli");
|
|
1305
1565
|
process.on("unhandledRejection", (reason) => {
|
|
1306
|
-
|
|
1566
|
+
logger10.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
|
|
1307
1567
|
process.exit(1);
|
|
1308
1568
|
});
|
|
1309
1569
|
process.on("uncaughtException", (err) => {
|
|
1310
|
-
|
|
1570
|
+
logger10.error({ error: err.message }, "Fatal error");
|
|
1311
1571
|
process.exit(1);
|
|
1312
1572
|
});
|
|
1313
1573
|
var program = new Command();
|
|
1314
|
-
program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.
|
|
1574
|
+
program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.9.0");
|
|
1315
1575
|
program.command("init [project-name]").description("Create a new Tutti project").option("-t, --template <id>", "Project template to use").action(async (projectName, opts) => {
|
|
1316
1576
|
await initCommand(projectName, opts.template);
|
|
1317
1577
|
});
|
|
@@ -1321,6 +1581,25 @@ program.command("templates").description("List all available project templates")
|
|
|
1321
1581
|
program.command("run [score]").description("Run a Tutti score interactively").action(async (score) => {
|
|
1322
1582
|
await runCommand(score);
|
|
1323
1583
|
});
|
|
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
|
+
);
|
|
1324
1603
|
program.command("add <voice>").description("Add a voice to your project").action((voice) => {
|
|
1325
1604
|
addCommand(voice);
|
|
1326
1605
|
});
|