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