@tuttiai/cli 0.13.0 → 0.14.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/LICENSE +191 -21
- package/README.md +50 -1
- package/dist/index.js +906 -133
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 createLogger15 } from "@tuttiai/core";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -718,7 +718,8 @@ async function resumeCommand(sessionId, opts) {
|
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
720
|
const agentName = resolveAgentName(score, opts.agent);
|
|
721
|
-
const
|
|
721
|
+
const agentMap = new Map(Object.entries(score.agents));
|
|
722
|
+
const agent = agentMap.get(agentName);
|
|
722
723
|
if (!agent) {
|
|
723
724
|
logger3.error(
|
|
724
725
|
{ agent: agentName, available: Object.keys(score.agents) },
|
|
@@ -938,14 +939,14 @@ import chalk4 from "chalk";
|
|
|
938
939
|
import ora3 from "ora";
|
|
939
940
|
import { createLogger as createLogger4 } from "@tuttiai/core";
|
|
940
941
|
var logger4 = createLogger4("tutti-cli");
|
|
941
|
-
var OFFICIAL_VOICES =
|
|
942
|
-
filesystem
|
|
942
|
+
var OFFICIAL_VOICES = /* @__PURE__ */ new Map([
|
|
943
|
+
["filesystem", {
|
|
943
944
|
package: "@tuttiai/filesystem",
|
|
944
945
|
setup: ` Add to your score:
|
|
945
946
|
${chalk4.cyan('import { FilesystemVoice } from "@tuttiai/filesystem"')}
|
|
946
947
|
${chalk4.cyan("voices: [new FilesystemVoice()]")}`
|
|
947
|
-
},
|
|
948
|
-
github
|
|
948
|
+
}],
|
|
949
|
+
["github", {
|
|
949
950
|
package: "@tuttiai/github",
|
|
950
951
|
setup: ` Add ${chalk4.bold("GITHUB_TOKEN")} to your .env file:
|
|
951
952
|
${chalk4.cyan("GITHUB_TOKEN=ghp_your_token_here")}
|
|
@@ -953,8 +954,8 @@ var OFFICIAL_VOICES = {
|
|
|
953
954
|
Add to your score:
|
|
954
955
|
${chalk4.cyan('import { GitHubVoice } from "@tuttiai/github"')}
|
|
955
956
|
${chalk4.cyan("voices: [new GitHubVoice()]")}`
|
|
956
|
-
},
|
|
957
|
-
playwright
|
|
957
|
+
}],
|
|
958
|
+
["playwright", {
|
|
958
959
|
package: "@tuttiai/playwright",
|
|
959
960
|
setup: ` Install the browser:
|
|
960
961
|
${chalk4.cyan("npx playwright install chromium")}
|
|
@@ -962,8 +963,8 @@ var OFFICIAL_VOICES = {
|
|
|
962
963
|
Add to your score:
|
|
963
964
|
${chalk4.cyan('import { PlaywrightVoice } from "@tuttiai/playwright"')}
|
|
964
965
|
${chalk4.cyan("voices: [new PlaywrightVoice()]")}`
|
|
965
|
-
},
|
|
966
|
-
postgres
|
|
966
|
+
}],
|
|
967
|
+
["postgres", {
|
|
967
968
|
package: "pg",
|
|
968
969
|
setup: ` Add ${chalk4.bold("DATABASE_URL")} to your .env file:
|
|
969
970
|
${chalk4.cyan("DATABASE_URL=postgres://user:pass@localhost:5432/tutti")}
|
|
@@ -976,11 +977,12 @@ var OFFICIAL_VOICES = {
|
|
|
976
977
|
|
|
977
978
|
Use the async factory for initialization:
|
|
978
979
|
${chalk4.cyan("const tutti = await TuttiRuntime.create(score)")}`
|
|
979
|
-
}
|
|
980
|
-
|
|
980
|
+
}]
|
|
981
|
+
]);
|
|
981
982
|
function resolvePackageName(input) {
|
|
982
|
-
|
|
983
|
-
|
|
983
|
+
const voice = OFFICIAL_VOICES.get(input);
|
|
984
|
+
if (voice) {
|
|
985
|
+
return voice.package;
|
|
984
986
|
}
|
|
985
987
|
if (input.startsWith("@")) {
|
|
986
988
|
return input;
|
|
@@ -1023,7 +1025,7 @@ function addCommand(voiceName) {
|
|
|
1023
1025
|
logger4.error({ error: message, package: packageName }, "Installation failed");
|
|
1024
1026
|
process.exit(1);
|
|
1025
1027
|
}
|
|
1026
|
-
const official = OFFICIAL_VOICES
|
|
1028
|
+
const official = OFFICIAL_VOICES.get(voiceName);
|
|
1027
1029
|
if (official) {
|
|
1028
1030
|
console.log();
|
|
1029
1031
|
console.log(" Setup:");
|
|
@@ -1051,8 +1053,12 @@ import {
|
|
|
1051
1053
|
createLogger as createLogger5
|
|
1052
1054
|
} from "@tuttiai/core";
|
|
1053
1055
|
var logger5 = createLogger5("tutti-cli");
|
|
1054
|
-
var ok = (msg) =>
|
|
1055
|
-
|
|
1056
|
+
var ok = (msg) => {
|
|
1057
|
+
console.log(chalk5.green(" \u2714 " + msg));
|
|
1058
|
+
};
|
|
1059
|
+
var fail = (msg) => {
|
|
1060
|
+
console.log(chalk5.red(" \u2718 " + msg));
|
|
1061
|
+
};
|
|
1056
1062
|
async function checkCommand(scorePath) {
|
|
1057
1063
|
const file = resolve5(scorePath ?? "./tutti.score.ts");
|
|
1058
1064
|
console.log(chalk5.cyan(`
|
|
@@ -1101,10 +1107,10 @@ Checking ${file}...
|
|
|
1101
1107
|
for (const [agentKey, agent] of Object.entries(score.agents)) {
|
|
1102
1108
|
for (const voice of agent.voices) {
|
|
1103
1109
|
const voiceName = voice.name;
|
|
1104
|
-
const voiceEnvMap =
|
|
1105
|
-
github
|
|
1106
|
-
|
|
1107
|
-
const envVar = voiceEnvMap
|
|
1110
|
+
const voiceEnvMap = /* @__PURE__ */ new Map([
|
|
1111
|
+
["github", "GITHUB_TOKEN"]
|
|
1112
|
+
]);
|
|
1113
|
+
const envVar = voiceEnvMap.get(voiceName);
|
|
1108
1114
|
if (envVar) {
|
|
1109
1115
|
const key = SecretsManager3.optional(envVar);
|
|
1110
1116
|
if (key) {
|
|
@@ -1360,8 +1366,8 @@ async function fetchRegistry() {
|
|
|
1360
1366
|
}
|
|
1361
1367
|
}
|
|
1362
1368
|
function toolCount(name) {
|
|
1363
|
-
const counts =
|
|
1364
|
-
return counts
|
|
1369
|
+
const counts = /* @__PURE__ */ new Map([["filesystem", 7], ["github", 10], ["playwright", 12]]);
|
|
1370
|
+
return counts.get(name) ?? 0;
|
|
1365
1371
|
}
|
|
1366
1372
|
function matchesQuery(voice, query) {
|
|
1367
1373
|
const q = query.toLowerCase();
|
|
@@ -1464,7 +1470,9 @@ function fail2(msg) {
|
|
|
1464
1470
|
console.error(chalk8.red(" " + msg));
|
|
1465
1471
|
process.exit(1);
|
|
1466
1472
|
}
|
|
1467
|
-
var ok2 = (msg) =>
|
|
1473
|
+
var ok2 = (msg) => {
|
|
1474
|
+
console.log(chalk8.green(" \u2714 " + msg));
|
|
1475
|
+
};
|
|
1468
1476
|
async function publishCommand(opts) {
|
|
1469
1477
|
const cwd = process.cwd();
|
|
1470
1478
|
const pkg = readPkg(cwd);
|
|
@@ -1481,8 +1489,8 @@ async function publishCommand(opts) {
|
|
|
1481
1489
|
if (!pkg.license) missing.push("license");
|
|
1482
1490
|
if (!pkg.exports) missing.push("exports");
|
|
1483
1491
|
if (missing.length > 0) fail2("package.json is missing: " + missing.join(", "));
|
|
1484
|
-
const name = pkg.name;
|
|
1485
|
-
const version = pkg.version;
|
|
1492
|
+
const name = pkg.name ?? "";
|
|
1493
|
+
const version = pkg.version ?? "";
|
|
1486
1494
|
const validName = name.startsWith("@tuttiai/") || name.startsWith("tutti");
|
|
1487
1495
|
if (!validName) fail2("Package name must start with @tuttiai/ or tutti \u2014 got: " + name);
|
|
1488
1496
|
const src = readFileSync3(resolve8(cwd, "src/index.ts"), "utf-8");
|
|
@@ -1527,7 +1535,7 @@ async function publishCommand(opts) {
|
|
|
1527
1535
|
console.error(chalk8.dim(" " + msg));
|
|
1528
1536
|
process.exit(1);
|
|
1529
1537
|
}
|
|
1530
|
-
const fileLines = packOutput.split("\n").filter((l) => l.includes("npm notice") && /\d+(
|
|
1538
|
+
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*/, ""));
|
|
1531
1539
|
if (fileLines.length > 0) {
|
|
1532
1540
|
console.log(chalk8.dim(" Files:"));
|
|
1533
1541
|
for (const line of fileLines) {
|
|
@@ -1613,13 +1621,19 @@ async function openRegistryPR(packageName, version, description, token) {
|
|
|
1613
1621
|
author: isOfficial ? "tuttiai" : packageName.split("/")[0]?.replace("@", "") ?? "community",
|
|
1614
1622
|
tags: [shortName]
|
|
1615
1623
|
};
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
if (
|
|
1619
|
-
|
|
1620
|
-
registry[section][idx] = { ...registry[section][idx], ...entry };
|
|
1624
|
+
const sectionList = section === "official" ? registry.official : registry.community;
|
|
1625
|
+
const list = sectionList ?? [];
|
|
1626
|
+
if (section === "official") {
|
|
1627
|
+
registry.official = list;
|
|
1621
1628
|
} else {
|
|
1622
|
-
registry
|
|
1629
|
+
registry.community = list;
|
|
1630
|
+
}
|
|
1631
|
+
const existingIdx = list.findIndex((v) => v.package === packageName);
|
|
1632
|
+
if (existingIdx >= 0) {
|
|
1633
|
+
const existing = list.at(existingIdx);
|
|
1634
|
+
list.splice(existingIdx, 1, { ...existing, ...entry });
|
|
1635
|
+
} else {
|
|
1636
|
+
list.push(entry);
|
|
1623
1637
|
}
|
|
1624
1638
|
const updatedContent = Buffer.from(JSON.stringify(registry, null, 2) + "\n").toString("base64");
|
|
1625
1639
|
const mainRes = await fetch(
|
|
@@ -1764,7 +1778,7 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1764
1778
|
}
|
|
1765
1779
|
const agentNames = Object.keys(score.agents);
|
|
1766
1780
|
const agentName = options.agent ?? (typeof score.entry === "string" ? score.entry : void 0) ?? agentNames[0];
|
|
1767
|
-
if (!agentName || !score.agents
|
|
1781
|
+
if (!agentName || !Object.hasOwn(score.agents, agentName)) {
|
|
1768
1782
|
logger10.error(
|
|
1769
1783
|
{ requested: agentName, available: agentNames },
|
|
1770
1784
|
"Agent not found in score"
|
|
@@ -1785,7 +1799,8 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1785
1799
|
reactive.on("reloaded", () => {
|
|
1786
1800
|
void (async () => {
|
|
1787
1801
|
try {
|
|
1788
|
-
const nextScore = reactive
|
|
1802
|
+
const nextScore = reactive?.current;
|
|
1803
|
+
if (!nextScore) return;
|
|
1789
1804
|
const nextRuntime = buildRuntime2(nextScore, sharedSessions);
|
|
1790
1805
|
const nextApp = await buildApp(nextRuntime, agentName, port, host, options.apiKey);
|
|
1791
1806
|
await app.close();
|
|
@@ -1960,21 +1975,472 @@ async function scheduleCommand(scorePath) {
|
|
|
1960
1975
|
await new Promise(() => void 0);
|
|
1961
1976
|
}
|
|
1962
1977
|
|
|
1963
|
-
// src/commands/
|
|
1964
|
-
import {
|
|
1965
|
-
import {
|
|
1978
|
+
// src/commands/update.ts
|
|
1979
|
+
import { execSync as execSync3 } from "child_process";
|
|
1980
|
+
import { existsSync as existsSync12, readFileSync as readFileSync5 } from "fs";
|
|
1966
1981
|
import { resolve as resolve12 } from "path";
|
|
1967
|
-
import { createInterface as createInterface3 } from "readline/promises";
|
|
1968
1982
|
import chalk12 from "chalk";
|
|
1969
1983
|
import ora7 from "ora";
|
|
1984
|
+
var TUTTI_PACKAGES = [
|
|
1985
|
+
"@tuttiai/core",
|
|
1986
|
+
"@tuttiai/cli",
|
|
1987
|
+
"@tuttiai/types",
|
|
1988
|
+
"@tuttiai/server",
|
|
1989
|
+
"@tuttiai/filesystem",
|
|
1990
|
+
"@tuttiai/github",
|
|
1991
|
+
"@tuttiai/playwright",
|
|
1992
|
+
"@tuttiai/mcp",
|
|
1993
|
+
"@tuttiai/web",
|
|
1994
|
+
"@tuttiai/sandbox",
|
|
1995
|
+
"@tuttiai/rag"
|
|
1996
|
+
];
|
|
1997
|
+
function getInstalledVersion(pkg) {
|
|
1998
|
+
try {
|
|
1999
|
+
const out = execSync3(`npm list ${pkg} --depth=0 --json 2>/dev/null`, {
|
|
2000
|
+
encoding: "utf-8",
|
|
2001
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2002
|
+
});
|
|
2003
|
+
const data = JSON.parse(out);
|
|
2004
|
+
const deps = data.dependencies;
|
|
2005
|
+
if (!deps) return null;
|
|
2006
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
2007
|
+
if (name === pkg && info.version) return info.version;
|
|
2008
|
+
}
|
|
2009
|
+
return null;
|
|
2010
|
+
} catch {
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
function getLatestVersion(pkg) {
|
|
2015
|
+
try {
|
|
2016
|
+
return execSync3(`npm view ${pkg} version 2>/dev/null`, {
|
|
2017
|
+
encoding: "utf-8",
|
|
2018
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2019
|
+
}).trim();
|
|
2020
|
+
} catch {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
function detectPackageManager() {
|
|
2025
|
+
const cwd = process.cwd();
|
|
2026
|
+
if (existsSync12(resolve12(cwd, "yarn.lock"))) return "yarn";
|
|
2027
|
+
if (existsSync12(resolve12(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2028
|
+
return "npm";
|
|
2029
|
+
}
|
|
2030
|
+
function isGlobalInstall() {
|
|
2031
|
+
try {
|
|
2032
|
+
const globalPrefix = execSync3("npm prefix -g", {
|
|
2033
|
+
encoding: "utf-8",
|
|
2034
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2035
|
+
}).trim();
|
|
2036
|
+
return process.argv[1]?.startsWith(globalPrefix) ?? false;
|
|
2037
|
+
} catch {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
function updateCommand() {
|
|
2042
|
+
console.log();
|
|
2043
|
+
console.log(chalk12.cyan.bold(" Tutti Update"));
|
|
2044
|
+
console.log();
|
|
2045
|
+
const spinner = ora7("Checking for updates...").start();
|
|
2046
|
+
const cliCurrent = getInstalledVersion("@tuttiai/cli") ?? "unknown";
|
|
2047
|
+
const cliLatest = getLatestVersion("@tuttiai/cli");
|
|
2048
|
+
spinner.stop();
|
|
2049
|
+
if (cliLatest && cliCurrent !== cliLatest) {
|
|
2050
|
+
console.log(
|
|
2051
|
+
chalk12.yellow(" CLI update available: ") + chalk12.dim(cliCurrent) + " \u2192 " + chalk12.green(cliLatest)
|
|
2052
|
+
);
|
|
2053
|
+
if (isGlobalInstall()) {
|
|
2054
|
+
const updateSpinner2 = ora7("Updating global CLI...").start();
|
|
2055
|
+
try {
|
|
2056
|
+
execSync3("npm install -g tutti-ai@latest", { stdio: "pipe" });
|
|
2057
|
+
updateSpinner2.succeed("CLI updated to " + cliLatest);
|
|
2058
|
+
} catch {
|
|
2059
|
+
updateSpinner2.fail("Failed to update global CLI");
|
|
2060
|
+
console.log(chalk12.dim(" Run manually: npm install -g tutti-ai@latest"));
|
|
2061
|
+
}
|
|
2062
|
+
} else {
|
|
2063
|
+
console.log(chalk12.dim(" Global: npm install -g tutti-ai@latest"));
|
|
2064
|
+
}
|
|
2065
|
+
} else {
|
|
2066
|
+
console.log(chalk12.green(" CLI is up to date") + chalk12.dim(" (" + cliCurrent + ")"));
|
|
2067
|
+
}
|
|
2068
|
+
const pkgPath = resolve12(process.cwd(), "package.json");
|
|
2069
|
+
if (!existsSync12(pkgPath)) {
|
|
2070
|
+
console.log();
|
|
2071
|
+
console.log(chalk12.dim(" No package.json found \u2014 skipping project dependency check."));
|
|
2072
|
+
console.log();
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
let pkg;
|
|
2076
|
+
try {
|
|
2077
|
+
pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
2078
|
+
} catch {
|
|
2079
|
+
console.log(chalk12.dim(" Could not read package.json"));
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
2083
|
+
if (pkg.dependencies) {
|
|
2084
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
2085
|
+
allDeps.set(name, version);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
if (pkg.devDependencies) {
|
|
2089
|
+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
|
2090
|
+
allDeps.set(name, version);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
const installed = TUTTI_PACKAGES.filter((p) => allDeps.has(p));
|
|
2094
|
+
if (installed.length === 0) {
|
|
2095
|
+
console.log();
|
|
2096
|
+
console.log(chalk12.dim(" No @tuttiai packages found in this project."));
|
|
2097
|
+
console.log();
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
console.log();
|
|
2101
|
+
console.log(" " + chalk12.bold("Project packages:"));
|
|
2102
|
+
const toUpdate = [];
|
|
2103
|
+
for (const name of installed) {
|
|
2104
|
+
const current = allDeps.get(name) ?? "?";
|
|
2105
|
+
const latest = getLatestVersion(name);
|
|
2106
|
+
if (!latest) {
|
|
2107
|
+
console.log(" " + chalk12.dim(name) + " " + current + chalk12.dim(" (could not check)"));
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
const cleanCurrent = current.replace(/^[\^~]/, "");
|
|
2111
|
+
if (cleanCurrent === latest) {
|
|
2112
|
+
console.log(" " + chalk12.green("\u2714") + " " + name + " " + chalk12.dim(latest));
|
|
2113
|
+
} else {
|
|
2114
|
+
console.log(
|
|
2115
|
+
" " + chalk12.yellow("\u2191") + " " + name + " " + chalk12.dim(cleanCurrent) + " \u2192 " + chalk12.green(latest)
|
|
2116
|
+
);
|
|
2117
|
+
toUpdate.push(name + "@latest");
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (toUpdate.length === 0) {
|
|
2121
|
+
console.log();
|
|
2122
|
+
console.log(chalk12.green(" All packages are up to date."));
|
|
2123
|
+
console.log();
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
console.log();
|
|
2127
|
+
const pm = detectPackageManager();
|
|
2128
|
+
const installCmd = pm === "yarn" ? "yarn add " + toUpdate.join(" ") : pm === "pnpm" ? "pnpm add " + toUpdate.join(" ") : "npm install " + toUpdate.join(" ");
|
|
2129
|
+
const updateSpinner = ora7("Updating " + toUpdate.length + " package(s)...").start();
|
|
2130
|
+
try {
|
|
2131
|
+
execSync3(installCmd, { cwd: process.cwd(), stdio: "pipe" });
|
|
2132
|
+
updateSpinner.succeed("Updated " + toUpdate.length + " package(s)");
|
|
2133
|
+
} catch {
|
|
2134
|
+
updateSpinner.fail("Update failed");
|
|
2135
|
+
console.log(chalk12.dim(" Run manually: " + installCmd));
|
|
2136
|
+
}
|
|
2137
|
+
console.log();
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/commands/outdated.ts
|
|
2141
|
+
import { execSync as execSync4 } from "child_process";
|
|
2142
|
+
import { existsSync as existsSync13, readFileSync as readFileSync6 } from "fs";
|
|
2143
|
+
import { resolve as resolve13 } from "path";
|
|
2144
|
+
import chalk13 from "chalk";
|
|
2145
|
+
import ora8 from "ora";
|
|
2146
|
+
function getLatestVersion2(pkg) {
|
|
2147
|
+
try {
|
|
2148
|
+
return execSync4(`npm view ${pkg} version 2>/dev/null`, {
|
|
2149
|
+
encoding: "utf-8",
|
|
2150
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2151
|
+
}).trim();
|
|
2152
|
+
} catch {
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
function pad(s, len) {
|
|
2157
|
+
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
2158
|
+
}
|
|
2159
|
+
function outdatedCommand() {
|
|
2160
|
+
const pkgPath = resolve13(process.cwd(), "package.json");
|
|
2161
|
+
if (!existsSync13(pkgPath)) {
|
|
2162
|
+
console.error(chalk13.red("No package.json found in the current directory."));
|
|
2163
|
+
console.error(chalk13.dim('Run "tutti-ai init" to create a new project.'));
|
|
2164
|
+
process.exit(1);
|
|
2165
|
+
}
|
|
2166
|
+
let pkg;
|
|
2167
|
+
try {
|
|
2168
|
+
pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
2169
|
+
} catch {
|
|
2170
|
+
console.error(chalk13.red("Could not parse package.json"));
|
|
2171
|
+
process.exit(1);
|
|
2172
|
+
}
|
|
2173
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
2174
|
+
if (pkg.dependencies) {
|
|
2175
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
2176
|
+
allDeps.set(name, version);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
if (pkg.devDependencies) {
|
|
2180
|
+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
|
2181
|
+
allDeps.set(name, version);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
const tuttiDeps = [...allDeps.entries()].filter(([name]) => name.startsWith("@tuttiai/"));
|
|
2185
|
+
if (tuttiDeps.length === 0) {
|
|
2186
|
+
console.log(chalk13.dim("No @tuttiai packages found in this project."));
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
const spinner = ora8("Checking npm registry...").start();
|
|
2190
|
+
const results = [];
|
|
2191
|
+
for (const [name, version] of tuttiDeps) {
|
|
2192
|
+
const latest = getLatestVersion2(name);
|
|
2193
|
+
const current = version.replace(/^[\^~]/, "");
|
|
2194
|
+
results.push({
|
|
2195
|
+
name,
|
|
2196
|
+
current,
|
|
2197
|
+
latest: latest ?? "?",
|
|
2198
|
+
outdated: latest !== null && current !== latest
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
spinner.stop();
|
|
2202
|
+
console.log();
|
|
2203
|
+
console.log(
|
|
2204
|
+
chalk13.dim(
|
|
2205
|
+
" " + pad("PACKAGE", 28) + pad("CURRENT", 12) + pad("LATEST", 12) + "STATUS"
|
|
2206
|
+
)
|
|
2207
|
+
);
|
|
2208
|
+
console.log(chalk13.dim(" " + "\u2500".repeat(64)));
|
|
2209
|
+
let outdatedCount = 0;
|
|
2210
|
+
for (const r of results) {
|
|
2211
|
+
const status = r.outdated ? chalk13.yellow("update available") : chalk13.green("up to date");
|
|
2212
|
+
if (r.outdated) outdatedCount++;
|
|
2213
|
+
console.log(
|
|
2214
|
+
" " + pad(r.name, 28) + pad(r.current, 12) + pad(r.latest, 12) + status
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
console.log();
|
|
2218
|
+
if (outdatedCount > 0) {
|
|
2219
|
+
console.log(
|
|
2220
|
+
chalk13.yellow(" " + outdatedCount + " package(s) can be updated.") + chalk13.dim(" Run: tutti-ai update")
|
|
2221
|
+
);
|
|
2222
|
+
} else {
|
|
2223
|
+
console.log(chalk13.green(" All packages are up to date."));
|
|
2224
|
+
}
|
|
2225
|
+
console.log();
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// src/commands/info.ts
|
|
2229
|
+
import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
|
|
2230
|
+
import { resolve as resolve14 } from "path";
|
|
2231
|
+
import chalk14 from "chalk";
|
|
2232
|
+
import { ScoreLoader as ScoreLoader8, createLogger as createLogger12 } from "@tuttiai/core";
|
|
2233
|
+
var logger12 = createLogger12("tutti-cli");
|
|
2234
|
+
function pad2(s, len) {
|
|
2235
|
+
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
2236
|
+
}
|
|
2237
|
+
async function infoCommand(scorePath) {
|
|
2238
|
+
const pkgPath = resolve14(process.cwd(), "package.json");
|
|
2239
|
+
let projectName = "(unknown)";
|
|
2240
|
+
let projectVersion = "(unknown)";
|
|
2241
|
+
const installedDeps = /* @__PURE__ */ new Map();
|
|
2242
|
+
if (existsSync14(pkgPath)) {
|
|
2243
|
+
try {
|
|
2244
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
2245
|
+
projectName = pkg.name ?? "(unnamed)";
|
|
2246
|
+
projectVersion = pkg.version ?? "0.0.0";
|
|
2247
|
+
if (pkg.dependencies) {
|
|
2248
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
2249
|
+
installedDeps.set(name, version);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
if (pkg.devDependencies) {
|
|
2253
|
+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
|
2254
|
+
installedDeps.set(name, version);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
} catch {
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
console.log();
|
|
2261
|
+
console.log(chalk14.cyan.bold(" Tutti Project Info"));
|
|
2262
|
+
console.log();
|
|
2263
|
+
console.log(" " + chalk14.dim("Project:") + " " + chalk14.bold(projectName) + " " + chalk14.dim(projectVersion));
|
|
2264
|
+
const tuttiPkgs = [...installedDeps.entries()].filter(([name]) => name.startsWith("@tuttiai/"));
|
|
2265
|
+
if (tuttiPkgs.length > 0) {
|
|
2266
|
+
console.log();
|
|
2267
|
+
console.log(" " + chalk14.bold("Packages:"));
|
|
2268
|
+
for (const [name, version] of tuttiPkgs) {
|
|
2269
|
+
console.log(" " + pad2(name, 28) + chalk14.dim(version));
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const scoreFile = resolve14(scorePath ?? "./tutti.score.ts");
|
|
2273
|
+
if (!existsSync14(scoreFile)) {
|
|
2274
|
+
console.log();
|
|
2275
|
+
console.log(chalk14.dim(" No score file found at " + scoreFile));
|
|
2276
|
+
console.log(chalk14.dim(' Run "tutti-ai init" to create a new project.'));
|
|
2277
|
+
console.log();
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
let score;
|
|
2281
|
+
try {
|
|
2282
|
+
score = await ScoreLoader8.load(scoreFile);
|
|
2283
|
+
} catch (err) {
|
|
2284
|
+
logger12.error(
|
|
2285
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
2286
|
+
"Failed to load score"
|
|
2287
|
+
);
|
|
2288
|
+
console.log(chalk14.dim(" Score file found but failed to load."));
|
|
2289
|
+
console.log();
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
console.log(" " + chalk14.dim("Score:") + " " + (score.name ?? scoreFile));
|
|
2293
|
+
const agentEntries = Object.entries(score.agents);
|
|
2294
|
+
console.log();
|
|
2295
|
+
console.log(" " + chalk14.bold("Agents:") + chalk14.dim(" (" + agentEntries.length + ")"));
|
|
2296
|
+
for (const [id, agent] of agentEntries) {
|
|
2297
|
+
const voiceNames = agent.voices.map((v) => v.name).join(", ") || "none";
|
|
2298
|
+
const model = agent.model ?? score.default_model ?? "(default)";
|
|
2299
|
+
const flags = [];
|
|
2300
|
+
if (agent.streaming) flags.push("streaming");
|
|
2301
|
+
if (agent.allow_human_input) flags.push("hitl");
|
|
2302
|
+
if (agent.durable) flags.push("durable");
|
|
2303
|
+
if (agent.schedule) flags.push("scheduled");
|
|
2304
|
+
if (agent.outputSchema) flags.push("structured");
|
|
2305
|
+
if (agent.beforeRun ?? agent.afterRun) flags.push("guardrails");
|
|
2306
|
+
console.log();
|
|
2307
|
+
console.log(" " + chalk14.bold(id) + chalk14.dim(" (" + agent.name + ")"));
|
|
2308
|
+
console.log(" " + chalk14.dim("Model: ") + model);
|
|
2309
|
+
console.log(" " + chalk14.dim("Voices: ") + voiceNames);
|
|
2310
|
+
if (flags.length > 0) {
|
|
2311
|
+
console.log(" " + chalk14.dim("Flags: ") + flags.join(", "));
|
|
2312
|
+
}
|
|
2313
|
+
if (agent.schedule) {
|
|
2314
|
+
const sched = agent.schedule;
|
|
2315
|
+
const trigger = sched.cron ?? sched.every ?? sched.at ?? "?";
|
|
2316
|
+
console.log(" " + chalk14.dim("Schedule: ") + trigger);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
if (score.entry) {
|
|
2320
|
+
const entry = typeof score.entry === "string" ? score.entry : "parallel";
|
|
2321
|
+
console.log();
|
|
2322
|
+
console.log(" " + chalk14.dim("Entry:") + " " + entry);
|
|
2323
|
+
}
|
|
2324
|
+
console.log();
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/commands/upgrade.ts
|
|
2328
|
+
import { execSync as execSync5 } from "child_process";
|
|
2329
|
+
import { existsSync as existsSync15, readFileSync as readFileSync8 } from "fs";
|
|
2330
|
+
import { resolve as resolve15 } from "path";
|
|
2331
|
+
import chalk15 from "chalk";
|
|
2332
|
+
import ora9 from "ora";
|
|
2333
|
+
function detectPackageManager2() {
|
|
2334
|
+
const cwd = process.cwd();
|
|
2335
|
+
if (existsSync15(resolve15(cwd, "yarn.lock"))) return "yarn";
|
|
2336
|
+
if (existsSync15(resolve15(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2337
|
+
return "npm";
|
|
2338
|
+
}
|
|
2339
|
+
function resolvePackageName2(input) {
|
|
2340
|
+
const KNOWN = /* @__PURE__ */ new Map([
|
|
2341
|
+
["filesystem", "@tuttiai/filesystem"],
|
|
2342
|
+
["github", "@tuttiai/github"],
|
|
2343
|
+
["playwright", "@tuttiai/playwright"],
|
|
2344
|
+
["mcp", "@tuttiai/mcp"],
|
|
2345
|
+
["web", "@tuttiai/web"],
|
|
2346
|
+
["sandbox", "@tuttiai/sandbox"],
|
|
2347
|
+
["rag", "@tuttiai/rag"],
|
|
2348
|
+
["core", "@tuttiai/core"],
|
|
2349
|
+
["types", "@tuttiai/types"],
|
|
2350
|
+
["server", "@tuttiai/server"]
|
|
2351
|
+
]);
|
|
2352
|
+
return KNOWN.get(input) ?? (input.startsWith("@") ? input : `@tuttiai/${input}`);
|
|
2353
|
+
}
|
|
2354
|
+
function getInstalledTuttiPackages() {
|
|
2355
|
+
const pkgPath = resolve15(process.cwd(), "package.json");
|
|
2356
|
+
if (!existsSync15(pkgPath)) return /* @__PURE__ */ new Map();
|
|
2357
|
+
try {
|
|
2358
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
2359
|
+
const result = /* @__PURE__ */ new Map();
|
|
2360
|
+
if (pkg.dependencies) {
|
|
2361
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
2362
|
+
if (name.startsWith("@tuttiai/")) result.set(name, version);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
if (pkg.devDependencies) {
|
|
2366
|
+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
|
2367
|
+
if (name.startsWith("@tuttiai/")) result.set(name, version);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
return result;
|
|
2371
|
+
} catch {
|
|
2372
|
+
return /* @__PURE__ */ new Map();
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
function upgradeCommand(target) {
|
|
2376
|
+
const pkgPath = resolve15(process.cwd(), "package.json");
|
|
2377
|
+
if (!existsSync15(pkgPath)) {
|
|
2378
|
+
console.error(chalk15.red("No package.json found in the current directory."));
|
|
2379
|
+
console.error(chalk15.dim('Run "tutti-ai init" to create a new project.'));
|
|
2380
|
+
process.exit(1);
|
|
2381
|
+
}
|
|
2382
|
+
const pm = detectPackageManager2();
|
|
2383
|
+
const installed = getInstalledTuttiPackages();
|
|
2384
|
+
if (installed.size === 0) {
|
|
2385
|
+
console.log(chalk15.dim("No @tuttiai packages found in this project."));
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
let packages;
|
|
2389
|
+
if (target) {
|
|
2390
|
+
const resolved = resolvePackageName2(target);
|
|
2391
|
+
if (!installed.has(resolved)) {
|
|
2392
|
+
console.error(chalk15.red(resolved + " is not installed in this project."));
|
|
2393
|
+
console.error(chalk15.dim("Installed: " + [...installed.keys()].join(", ")));
|
|
2394
|
+
process.exit(1);
|
|
2395
|
+
}
|
|
2396
|
+
packages = [resolved + "@latest"];
|
|
2397
|
+
console.log(chalk15.cyan(" Upgrading " + resolved + "..."));
|
|
2398
|
+
} else {
|
|
2399
|
+
packages = [...installed.keys()].map((p) => p + "@latest");
|
|
2400
|
+
console.log(chalk15.cyan(" Upgrading all " + packages.length + " @tuttiai packages..."));
|
|
2401
|
+
}
|
|
2402
|
+
const installCmd = pm === "yarn" ? "yarn add " + packages.join(" ") : pm === "pnpm" ? "pnpm add " + packages.join(" ") : "npm install " + packages.join(" ");
|
|
2403
|
+
const spinner = ora9("Installing...").start();
|
|
2404
|
+
try {
|
|
2405
|
+
execSync5(installCmd, { cwd: process.cwd(), stdio: "pipe" });
|
|
2406
|
+
spinner.succeed("Upgraded " + packages.length + " package(s)");
|
|
2407
|
+
} catch {
|
|
2408
|
+
spinner.fail("Upgrade failed");
|
|
2409
|
+
console.log(chalk15.dim(" Run manually: " + installCmd));
|
|
2410
|
+
process.exit(1);
|
|
2411
|
+
}
|
|
2412
|
+
console.log();
|
|
2413
|
+
const newInstalled = getInstalledTuttiPackages();
|
|
2414
|
+
for (const [name, oldVersion] of installed) {
|
|
2415
|
+
const newVersion = newInstalled.get(name) ?? oldVersion;
|
|
2416
|
+
const oldClean = oldVersion.replace(/^[\^~]/, "");
|
|
2417
|
+
const newClean = newVersion.replace(/^[\^~]/, "");
|
|
2418
|
+
if (oldClean !== newClean) {
|
|
2419
|
+
console.log(" " + chalk15.green("\u2191") + " " + name + " " + chalk15.dim(oldClean) + " \u2192 " + chalk15.green(newClean));
|
|
2420
|
+
} else {
|
|
2421
|
+
console.log(" " + chalk15.dim("=") + " " + name + " " + chalk15.dim(newClean) + chalk15.dim(" (already latest)"));
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
console.log();
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/commands/replay.ts
|
|
2428
|
+
import { existsSync as existsSync16 } from "fs";
|
|
2429
|
+
import { writeFile } from "fs/promises";
|
|
2430
|
+
import { resolve as resolve16 } from "path";
|
|
2431
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
2432
|
+
import chalk17 from "chalk";
|
|
2433
|
+
import ora10 from "ora";
|
|
1970
2434
|
import {
|
|
1971
2435
|
PostgresSessionStore,
|
|
1972
|
-
ScoreLoader as
|
|
2436
|
+
ScoreLoader as ScoreLoader9,
|
|
1973
2437
|
TuttiRuntime as TuttiRuntime5,
|
|
1974
2438
|
SecretsManager as SecretsManager7,
|
|
1975
|
-
createLogger as
|
|
2439
|
+
createLogger as createLogger13
|
|
1976
2440
|
} from "@tuttiai/core";
|
|
1977
|
-
|
|
2441
|
+
|
|
2442
|
+
// src/commands/replay-render.ts
|
|
2443
|
+
import chalk16 from "chalk";
|
|
1978
2444
|
function messageToText2(msg) {
|
|
1979
2445
|
if (typeof msg.content === "string") return msg.content;
|
|
1980
2446
|
const parts = [];
|
|
@@ -1997,22 +2463,24 @@ function excerpt2(text, max) {
|
|
|
1997
2463
|
function renderList(messages) {
|
|
1998
2464
|
const lines = [];
|
|
1999
2465
|
for (let i = 0; i < messages.length; i++) {
|
|
2000
|
-
const msg = messages
|
|
2001
|
-
|
|
2466
|
+
const msg = messages.at(i);
|
|
2467
|
+
if (!msg) continue;
|
|
2468
|
+
const role = msg.role === "user" ? chalk16.blue("user ") : chalk16.green("assistant");
|
|
2002
2469
|
const text = excerpt2(messageToText2(msg), 80);
|
|
2003
2470
|
lines.push(
|
|
2004
|
-
|
|
2471
|
+
chalk16.dim(String(i).padStart(3)) + " " + role + " " + text
|
|
2005
2472
|
);
|
|
2006
2473
|
}
|
|
2007
2474
|
return lines.join("\n");
|
|
2008
2475
|
}
|
|
2009
2476
|
function renderShow(messages, index) {
|
|
2010
2477
|
if (index < 0 || index >= messages.length) {
|
|
2011
|
-
return
|
|
2478
|
+
return chalk16.red("Index out of range. Valid: 0\u2013" + (messages.length - 1));
|
|
2012
2479
|
}
|
|
2013
|
-
const msg = messages
|
|
2480
|
+
const msg = messages.at(index);
|
|
2481
|
+
if (!msg) return chalk16.red("Index out of range.");
|
|
2014
2482
|
const lines = [];
|
|
2015
|
-
lines.push(
|
|
2483
|
+
lines.push(chalk16.cyan.bold("Turn " + index) + " " + chalk16.dim("[" + msg.role + "]"));
|
|
2016
2484
|
lines.push("");
|
|
2017
2485
|
if (typeof msg.content === "string") {
|
|
2018
2486
|
lines.push(msg.content);
|
|
@@ -2021,13 +2489,13 @@ function renderShow(messages, index) {
|
|
|
2021
2489
|
if (block.type === "text") {
|
|
2022
2490
|
lines.push(block.text);
|
|
2023
2491
|
} else if (block.type === "tool_use") {
|
|
2024
|
-
lines.push(
|
|
2025
|
-
lines.push(
|
|
2026
|
-
lines.push(
|
|
2492
|
+
lines.push(chalk16.yellow(" tool_use: " + block.name));
|
|
2493
|
+
lines.push(chalk16.dim(" id: " + block.id));
|
|
2494
|
+
lines.push(chalk16.dim(" input: " + JSON.stringify(block.input, null, 2)));
|
|
2027
2495
|
} else if (block.type === "tool_result") {
|
|
2028
|
-
const label = block.is_error ?
|
|
2496
|
+
const label = block.is_error ? chalk16.red(" tool_result (error):") : chalk16.green(" tool_result:");
|
|
2029
2497
|
lines.push(label);
|
|
2030
|
-
lines.push(
|
|
2498
|
+
lines.push(chalk16.dim(" tool_use_id: " + block.tool_use_id));
|
|
2031
2499
|
lines.push(" " + block.content);
|
|
2032
2500
|
}
|
|
2033
2501
|
}
|
|
@@ -2036,9 +2504,9 @@ function renderShow(messages, index) {
|
|
|
2036
2504
|
}
|
|
2037
2505
|
function renderInspect(messages, index) {
|
|
2038
2506
|
if (index < 0 || index >= messages.length) {
|
|
2039
|
-
return
|
|
2507
|
+
return chalk16.red("Index out of range.");
|
|
2040
2508
|
}
|
|
2041
|
-
return JSON.stringify(messages
|
|
2509
|
+
return JSON.stringify(messages.at(index), null, 2);
|
|
2042
2510
|
}
|
|
2043
2511
|
function exportJSON(session) {
|
|
2044
2512
|
return JSON.stringify(
|
|
@@ -2063,7 +2531,8 @@ function exportMarkdown(session) {
|
|
|
2063
2531
|
lines.push("---");
|
|
2064
2532
|
lines.push("");
|
|
2065
2533
|
for (let i = 0; i < session.messages.length; i++) {
|
|
2066
|
-
const msg = session.messages
|
|
2534
|
+
const msg = session.messages.at(i);
|
|
2535
|
+
if (!msg) continue;
|
|
2067
2536
|
lines.push("## Turn " + i + " (" + msg.role + ")");
|
|
2068
2537
|
lines.push("");
|
|
2069
2538
|
if (typeof msg.content === "string") {
|
|
@@ -2086,25 +2555,28 @@ function exportMarkdown(session) {
|
|
|
2086
2555
|
}
|
|
2087
2556
|
return lines.join("\n");
|
|
2088
2557
|
}
|
|
2558
|
+
|
|
2559
|
+
// src/commands/replay.ts
|
|
2560
|
+
var logger13 = createLogger13("tutti-cli");
|
|
2089
2561
|
async function replayCommand(sessionId, opts = {}) {
|
|
2090
2562
|
const pgUrl = SecretsManager7.optional("TUTTI_PG_URL");
|
|
2091
2563
|
if (!pgUrl) {
|
|
2092
|
-
console.error(
|
|
2564
|
+
console.error(chalk17.red("TUTTI_PG_URL is not set."));
|
|
2093
2565
|
console.error(
|
|
2094
|
-
|
|
2566
|
+
chalk17.dim(
|
|
2095
2567
|
"The replay command requires PostgreSQL for session persistence.\nSet TUTTI_PG_URL=postgres://user:pass@host/db in your environment."
|
|
2096
2568
|
)
|
|
2097
2569
|
);
|
|
2098
2570
|
process.exit(1);
|
|
2099
2571
|
}
|
|
2100
2572
|
const store = new PostgresSessionStore(pgUrl);
|
|
2101
|
-
const spinner =
|
|
2573
|
+
const spinner = ora10({ color: "cyan" }).start("Loading session...");
|
|
2102
2574
|
let session;
|
|
2103
2575
|
try {
|
|
2104
2576
|
session = await store.getAsync(sessionId);
|
|
2105
2577
|
} catch (err) {
|
|
2106
2578
|
spinner.fail("Failed to load session");
|
|
2107
|
-
|
|
2579
|
+
logger13.error(
|
|
2108
2580
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
2109
2581
|
"Session store error"
|
|
2110
2582
|
);
|
|
@@ -2112,26 +2584,26 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2112
2584
|
}
|
|
2113
2585
|
spinner.stop();
|
|
2114
2586
|
if (!session) {
|
|
2115
|
-
console.error(
|
|
2116
|
-
console.error(
|
|
2587
|
+
console.error(chalk17.red("Session not found: " + sessionId));
|
|
2588
|
+
console.error(chalk17.dim("Check the session ID and ensure TUTTI_PG_URL points to the correct database."));
|
|
2117
2589
|
await store.close();
|
|
2118
2590
|
process.exit(1);
|
|
2119
2591
|
}
|
|
2120
2592
|
const messages = session.messages;
|
|
2121
2593
|
console.log("");
|
|
2122
|
-
console.log(
|
|
2123
|
-
console.log(
|
|
2124
|
-
console.log(
|
|
2125
|
-
console.log(
|
|
2126
|
-
console.log(
|
|
2594
|
+
console.log(chalk17.cyan.bold(" Tutti Replay"));
|
|
2595
|
+
console.log(chalk17.dim(" Session: " + session.id));
|
|
2596
|
+
console.log(chalk17.dim(" Agent: " + session.agent_name));
|
|
2597
|
+
console.log(chalk17.dim(" Created: " + session.created_at.toISOString()));
|
|
2598
|
+
console.log(chalk17.dim(" Messages: " + messages.length));
|
|
2127
2599
|
console.log("");
|
|
2128
|
-
console.log(
|
|
2600
|
+
console.log(chalk17.dim(" Commands: list, show <n>, next, prev, inspect, replay-from <n>, export <json|md>, quit"));
|
|
2129
2601
|
console.log("");
|
|
2130
2602
|
let cursor = 0;
|
|
2131
2603
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2132
2604
|
try {
|
|
2133
2605
|
while (true) {
|
|
2134
|
-
const prompt3 =
|
|
2606
|
+
const prompt3 = chalk17.cyan("replay [" + cursor + "/" + (messages.length - 1) + "]> ");
|
|
2135
2607
|
const raw = await rl.question(prompt3);
|
|
2136
2608
|
const input = raw.trim();
|
|
2137
2609
|
if (!input) continue;
|
|
@@ -2140,7 +2612,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2140
2612
|
case "quit":
|
|
2141
2613
|
case "exit":
|
|
2142
2614
|
case "q":
|
|
2143
|
-
console.log(
|
|
2615
|
+
console.log(chalk17.dim("Bye."));
|
|
2144
2616
|
return;
|
|
2145
2617
|
case "list":
|
|
2146
2618
|
console.log(renderList(messages));
|
|
@@ -2156,7 +2628,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2156
2628
|
cursor++;
|
|
2157
2629
|
console.log(renderShow(messages, cursor));
|
|
2158
2630
|
} else {
|
|
2159
|
-
console.log(
|
|
2631
|
+
console.log(chalk17.dim("Already at last message."));
|
|
2160
2632
|
}
|
|
2161
2633
|
break;
|
|
2162
2634
|
case "prev":
|
|
@@ -2164,7 +2636,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2164
2636
|
cursor--;
|
|
2165
2637
|
console.log(renderShow(messages, cursor));
|
|
2166
2638
|
} else {
|
|
2167
|
-
console.log(
|
|
2639
|
+
console.log(chalk17.dim("Already at first message."));
|
|
2168
2640
|
}
|
|
2169
2641
|
break;
|
|
2170
2642
|
case "inspect":
|
|
@@ -2173,7 +2645,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2173
2645
|
case "replay-from": {
|
|
2174
2646
|
const turn = parseInt(args[0] ?? "", 10);
|
|
2175
2647
|
if (isNaN(turn) || turn < 0 || turn >= messages.length) {
|
|
2176
|
-
console.log(
|
|
2648
|
+
console.log(chalk17.red("Usage: replay-from <turn-number>"));
|
|
2177
2649
|
break;
|
|
2178
2650
|
}
|
|
2179
2651
|
await handleReplayFrom(turn, session, rl, opts);
|
|
@@ -2184,19 +2656,19 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2184
2656
|
if (format === "json") {
|
|
2185
2657
|
const filename = "session-" + session.id.slice(0, 8) + ".json";
|
|
2186
2658
|
await writeFile(filename, exportJSON(session));
|
|
2187
|
-
console.log(
|
|
2659
|
+
console.log(chalk17.green("Exported to " + filename));
|
|
2188
2660
|
} else if (format === "md" || format === "markdown") {
|
|
2189
2661
|
const filename = "session-" + session.id.slice(0, 8) + ".md";
|
|
2190
2662
|
await writeFile(filename, exportMarkdown(session));
|
|
2191
|
-
console.log(
|
|
2663
|
+
console.log(chalk17.green("Exported to " + filename));
|
|
2192
2664
|
} else {
|
|
2193
|
-
console.log(
|
|
2665
|
+
console.log(chalk17.dim("Usage: export <json|md>"));
|
|
2194
2666
|
}
|
|
2195
2667
|
break;
|
|
2196
2668
|
}
|
|
2197
2669
|
default:
|
|
2198
2670
|
console.log(
|
|
2199
|
-
|
|
2671
|
+
chalk17.dim("Unknown command. Available: list, show <n>, next, prev, inspect, replay-from <n>, export <json|md>, quit")
|
|
2200
2672
|
);
|
|
2201
2673
|
}
|
|
2202
2674
|
}
|
|
@@ -2206,29 +2678,29 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2206
2678
|
}
|
|
2207
2679
|
}
|
|
2208
2680
|
async function handleReplayFrom(turn, session, rl, opts) {
|
|
2209
|
-
const scoreFile =
|
|
2210
|
-
if (!
|
|
2211
|
-
console.log(
|
|
2212
|
-
console.log(
|
|
2681
|
+
const scoreFile = resolve16(opts.score ?? "./tutti.score.ts");
|
|
2682
|
+
if (!existsSync16(scoreFile)) {
|
|
2683
|
+
console.log(chalk17.red("Score file not found: " + scoreFile));
|
|
2684
|
+
console.log(chalk17.dim("Use --score to specify the score file."));
|
|
2213
2685
|
return;
|
|
2214
2686
|
}
|
|
2215
|
-
const originalMsg = session.messages
|
|
2687
|
+
const originalMsg = session.messages.at(turn);
|
|
2216
2688
|
const originalInput = originalMsg ? messageToText2(originalMsg) : "";
|
|
2217
2689
|
const answer = await rl.question(
|
|
2218
|
-
|
|
2690
|
+
chalk17.cyan("Replay from turn " + turn + " with original input? ") + chalk17.dim("(y / enter new input) ")
|
|
2219
2691
|
);
|
|
2220
2692
|
const input = answer.trim().toLowerCase() === "y" || answer.trim() === "" ? originalInput : answer.trim();
|
|
2221
2693
|
if (!input) {
|
|
2222
|
-
console.log(
|
|
2694
|
+
console.log(chalk17.dim("No input provided. Cancelled."));
|
|
2223
2695
|
return;
|
|
2224
2696
|
}
|
|
2225
|
-
const spinnerLoad =
|
|
2697
|
+
const spinnerLoad = ora10({ color: "cyan" }).start("Loading score...");
|
|
2226
2698
|
let score;
|
|
2227
2699
|
try {
|
|
2228
|
-
score = await
|
|
2700
|
+
score = await ScoreLoader9.load(scoreFile);
|
|
2229
2701
|
} catch (err) {
|
|
2230
2702
|
spinnerLoad.fail("Failed to load score");
|
|
2231
|
-
|
|
2703
|
+
logger13.error({ error: err instanceof Error ? err.message : String(err) }, "Score load error");
|
|
2232
2704
|
return;
|
|
2233
2705
|
}
|
|
2234
2706
|
spinnerLoad.stop();
|
|
@@ -2245,12 +2717,13 @@ async function handleReplayFrom(turn, session, rl, opts) {
|
|
|
2245
2717
|
});
|
|
2246
2718
|
}
|
|
2247
2719
|
const agentName = session.agent_name;
|
|
2248
|
-
const
|
|
2720
|
+
const agentMap = new Map(Object.entries(score.agents));
|
|
2721
|
+
const agent = agentMap.get(agentName);
|
|
2249
2722
|
if (!agent) {
|
|
2250
|
-
console.log(
|
|
2723
|
+
console.log(chalk17.red('Agent "' + agentName + '" not found in score.'));
|
|
2251
2724
|
return;
|
|
2252
2725
|
}
|
|
2253
|
-
const spinnerRun =
|
|
2726
|
+
const spinnerRun = ora10({ color: "cyan" }).start("Running from turn " + turn + "...");
|
|
2254
2727
|
runtime.events.on("token:stream", (e) => {
|
|
2255
2728
|
spinnerRun.stop();
|
|
2256
2729
|
process.stdout.write(e.text);
|
|
@@ -2259,23 +2732,23 @@ async function handleReplayFrom(turn, session, rl, opts) {
|
|
|
2259
2732
|
const result = await runtime.run(agentName, input, session.id);
|
|
2260
2733
|
spinnerRun.stop();
|
|
2261
2734
|
console.log("");
|
|
2262
|
-
console.log(
|
|
2263
|
-
console.log(
|
|
2264
|
-
console.log(
|
|
2735
|
+
console.log(chalk17.green("Replay complete."));
|
|
2736
|
+
console.log(chalk17.dim(" Turns: " + result.turns));
|
|
2737
|
+
console.log(chalk17.dim(" Tokens: " + result.usage.input_tokens + " in / " + result.usage.output_tokens + " out"));
|
|
2265
2738
|
console.log("");
|
|
2266
2739
|
console.log(result.output);
|
|
2267
2740
|
} catch (err) {
|
|
2268
2741
|
spinnerRun.fail("Replay failed");
|
|
2269
|
-
|
|
2742
|
+
logger13.error({ error: err instanceof Error ? err.message : String(err) }, "Replay error");
|
|
2270
2743
|
}
|
|
2271
2744
|
}
|
|
2272
2745
|
|
|
2273
2746
|
// src/commands/schedules.ts
|
|
2274
|
-
import { existsSync as
|
|
2275
|
-
import { resolve as
|
|
2276
|
-
import
|
|
2747
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2748
|
+
import { resolve as resolve17 } from "path";
|
|
2749
|
+
import chalk18 from "chalk";
|
|
2277
2750
|
import {
|
|
2278
|
-
ScoreLoader as
|
|
2751
|
+
ScoreLoader as ScoreLoader10,
|
|
2279
2752
|
SchedulerEngine as SchedulerEngine2,
|
|
2280
2753
|
PostgresScheduleStore as PostgresScheduleStore2,
|
|
2281
2754
|
MemoryScheduleStore as MemoryScheduleStore2,
|
|
@@ -2283,15 +2756,15 @@ import {
|
|
|
2283
2756
|
EventBus as EventBus2,
|
|
2284
2757
|
InMemorySessionStore as InMemorySessionStore4,
|
|
2285
2758
|
SecretsManager as SecretsManager8,
|
|
2286
|
-
createLogger as
|
|
2759
|
+
createLogger as createLogger14
|
|
2287
2760
|
} from "@tuttiai/core";
|
|
2288
|
-
var
|
|
2761
|
+
var logger14 = createLogger14("tutti-cli");
|
|
2289
2762
|
function resolveStore2() {
|
|
2290
2763
|
const pgUrl = SecretsManager8.optional("TUTTI_PG_URL");
|
|
2291
2764
|
if (pgUrl) {
|
|
2292
2765
|
return new PostgresScheduleStore2({ connection_string: pgUrl });
|
|
2293
2766
|
}
|
|
2294
|
-
|
|
2767
|
+
logger14.warn("TUTTI_PG_URL not set \u2014 using in-memory store (schedules are ephemeral)");
|
|
2295
2768
|
return new MemoryScheduleStore2();
|
|
2296
2769
|
}
|
|
2297
2770
|
async function closeStore(store) {
|
|
@@ -2305,7 +2778,7 @@ function formatTrigger(r) {
|
|
|
2305
2778
|
if (r.config.at) return "at " + r.config.at;
|
|
2306
2779
|
return "?";
|
|
2307
2780
|
}
|
|
2308
|
-
function
|
|
2781
|
+
function pad3(s, len) {
|
|
2309
2782
|
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
2310
2783
|
}
|
|
2311
2784
|
async function schedulesListCommand() {
|
|
@@ -2313,22 +2786,22 @@ async function schedulesListCommand() {
|
|
|
2313
2786
|
try {
|
|
2314
2787
|
const records = await store.list();
|
|
2315
2788
|
if (records.length === 0) {
|
|
2316
|
-
console.log(
|
|
2317
|
-
console.log(
|
|
2789
|
+
console.log(chalk18.dim("No schedules found."));
|
|
2790
|
+
console.log(chalk18.dim('Run "tutti-ai schedule" to start the scheduler daemon.'));
|
|
2318
2791
|
return;
|
|
2319
2792
|
}
|
|
2320
2793
|
console.log("");
|
|
2321
2794
|
console.log(
|
|
2322
|
-
|
|
2323
|
-
" " +
|
|
2795
|
+
chalk18.dim(
|
|
2796
|
+
" " + pad3("ID", 20) + pad3("AGENT", 16) + pad3("TRIGGER", 22) + pad3("ENABLED", 10) + pad3("RUNS", 8) + "CREATED"
|
|
2324
2797
|
)
|
|
2325
2798
|
);
|
|
2326
|
-
console.log(
|
|
2799
|
+
console.log(chalk18.dim(" " + "\u2500".repeat(90)));
|
|
2327
2800
|
for (const r of records) {
|
|
2328
|
-
const enabled = r.enabled ?
|
|
2801
|
+
const enabled = r.enabled ? chalk18.green("yes") : chalk18.red("no") + " ";
|
|
2329
2802
|
const maxLabel = r.config.max_runs ? r.run_count + "/" + r.config.max_runs : String(r.run_count);
|
|
2330
2803
|
console.log(
|
|
2331
|
-
" " +
|
|
2804
|
+
" " + chalk18.bold(pad3(r.id, 20)) + pad3(r.agent_id, 16) + pad3(formatTrigger(r), 22) + pad3(enabled, 10) + pad3(maxLabel, 8) + chalk18.dim(r.created_at.toISOString().slice(0, 10))
|
|
2332
2805
|
);
|
|
2333
2806
|
}
|
|
2334
2807
|
console.log("");
|
|
@@ -2341,11 +2814,11 @@ async function schedulesEnableCommand(id) {
|
|
|
2341
2814
|
try {
|
|
2342
2815
|
const record = await store.get(id);
|
|
2343
2816
|
if (!record) {
|
|
2344
|
-
console.error(
|
|
2817
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2345
2818
|
process.exit(1);
|
|
2346
2819
|
}
|
|
2347
2820
|
await store.setEnabled(id, true);
|
|
2348
|
-
console.log(
|
|
2821
|
+
console.log(chalk18.green('Schedule "' + id + '" enabled.'));
|
|
2349
2822
|
} finally {
|
|
2350
2823
|
await closeStore(store);
|
|
2351
2824
|
}
|
|
@@ -2355,22 +2828,22 @@ async function schedulesDisableCommand(id) {
|
|
|
2355
2828
|
try {
|
|
2356
2829
|
const record = await store.get(id);
|
|
2357
2830
|
if (!record) {
|
|
2358
|
-
console.error(
|
|
2831
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2359
2832
|
process.exit(1);
|
|
2360
2833
|
}
|
|
2361
2834
|
await store.setEnabled(id, false);
|
|
2362
|
-
console.log(
|
|
2835
|
+
console.log(chalk18.yellow('Schedule "' + id + '" disabled.'));
|
|
2363
2836
|
} finally {
|
|
2364
2837
|
await closeStore(store);
|
|
2365
2838
|
}
|
|
2366
2839
|
}
|
|
2367
2840
|
async function schedulesTriggerCommand(id, scorePath) {
|
|
2368
|
-
const file =
|
|
2369
|
-
if (!
|
|
2370
|
-
console.error(
|
|
2841
|
+
const file = resolve17(scorePath ?? "./tutti.score.ts");
|
|
2842
|
+
if (!existsSync17(file)) {
|
|
2843
|
+
console.error(chalk18.red("Score file not found: " + file));
|
|
2371
2844
|
process.exit(1);
|
|
2372
2845
|
}
|
|
2373
|
-
const score = await
|
|
2846
|
+
const score = await ScoreLoader10.load(file);
|
|
2374
2847
|
const events = new EventBus2();
|
|
2375
2848
|
const sessions = new InMemorySessionStore4();
|
|
2376
2849
|
const runner = new AgentRunner2(score.provider, events, sessions);
|
|
@@ -2378,30 +2851,30 @@ async function schedulesTriggerCommand(id, scorePath) {
|
|
|
2378
2851
|
try {
|
|
2379
2852
|
const record = await store.get(id);
|
|
2380
2853
|
if (!record) {
|
|
2381
|
-
console.error(
|
|
2854
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2382
2855
|
process.exit(1);
|
|
2383
2856
|
}
|
|
2384
2857
|
const agent = score.agents[record.agent_id];
|
|
2385
2858
|
if (!agent) {
|
|
2386
|
-
console.error(
|
|
2859
|
+
console.error(chalk18.red('Agent "' + record.agent_id + '" not found in score.'));
|
|
2387
2860
|
process.exit(1);
|
|
2388
2861
|
}
|
|
2389
2862
|
const resolvedAgent = agent.model ? agent : { ...agent, model: score.default_model ?? "claude-sonnet-4-20250514" };
|
|
2390
2863
|
const engine = new SchedulerEngine2(store, runner, events);
|
|
2391
2864
|
await engine.schedule(id, resolvedAgent, record.config);
|
|
2392
2865
|
engine.start();
|
|
2393
|
-
console.log(
|
|
2866
|
+
console.log(chalk18.cyan('Triggering "' + id + '" (' + record.agent_id + ")..."));
|
|
2394
2867
|
const run2 = await engine.trigger(id);
|
|
2395
2868
|
engine.stop();
|
|
2396
2869
|
if (run2.error) {
|
|
2397
|
-
console.log(
|
|
2870
|
+
console.log(chalk18.red(" Error: " + run2.error));
|
|
2398
2871
|
process.exit(1);
|
|
2399
2872
|
}
|
|
2400
2873
|
const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() : 0;
|
|
2401
|
-
console.log(
|
|
2874
|
+
console.log(chalk18.green(" Completed in " + duration + "ms"));
|
|
2402
2875
|
if (run2.result) {
|
|
2403
2876
|
const preview = run2.result.length > 200 ? run2.result.slice(0, 200) + "..." : run2.result;
|
|
2404
|
-
console.log(
|
|
2877
|
+
console.log(chalk18.dim(" Output: " + preview));
|
|
2405
2878
|
}
|
|
2406
2879
|
} finally {
|
|
2407
2880
|
await closeStore(store);
|
|
@@ -2412,46 +2885,312 @@ async function schedulesRunsCommand(id) {
|
|
|
2412
2885
|
try {
|
|
2413
2886
|
const record = await store.get(id);
|
|
2414
2887
|
if (!record) {
|
|
2415
|
-
console.error(
|
|
2888
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2416
2889
|
process.exit(1);
|
|
2417
2890
|
}
|
|
2418
2891
|
if ("getRuns" in store && typeof store.getRuns === "function") {
|
|
2419
2892
|
const runs = store.getRuns(id);
|
|
2420
2893
|
if (runs.length === 0) {
|
|
2421
|
-
console.log(
|
|
2894
|
+
console.log(chalk18.dim("No runs recorded for this schedule."));
|
|
2422
2895
|
return;
|
|
2423
2896
|
}
|
|
2424
2897
|
const recent = runs.slice(-20);
|
|
2425
2898
|
console.log("");
|
|
2426
|
-
console.log(
|
|
2899
|
+
console.log(chalk18.dim(" Showing last " + recent.length + " of " + runs.length + " runs:"));
|
|
2427
2900
|
console.log("");
|
|
2428
2901
|
for (const run2 of recent) {
|
|
2429
2902
|
const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() + "ms" : "?";
|
|
2430
|
-
const status = run2.error ?
|
|
2903
|
+
const status = run2.error ? chalk18.red("error") : chalk18.green("ok");
|
|
2431
2904
|
const preview = run2.error ? run2.error.slice(0, 80) : (run2.result ?? "").slice(0, 80);
|
|
2432
2905
|
console.log(
|
|
2433
|
-
" " +
|
|
2906
|
+
" " + chalk18.dim(run2.triggered_at.toISOString()) + " " + status + " " + chalk18.dim(duration) + " " + preview
|
|
2434
2907
|
);
|
|
2435
2908
|
}
|
|
2436
2909
|
console.log("");
|
|
2437
2910
|
} else {
|
|
2438
|
-
console.log(
|
|
2439
|
-
console.log(
|
|
2911
|
+
console.log(chalk18.dim('Schedule "' + id + '" has completed ' + record.run_count + " runs."));
|
|
2912
|
+
console.log(chalk18.dim("Full run history requires the MemoryScheduleStore or a future tutti_schedule_runs table."));
|
|
2440
2913
|
}
|
|
2441
2914
|
} finally {
|
|
2442
2915
|
await closeStore(store);
|
|
2443
2916
|
}
|
|
2444
2917
|
}
|
|
2445
2918
|
|
|
2919
|
+
// src/commands/traces.ts
|
|
2920
|
+
import chalk20 from "chalk";
|
|
2921
|
+
import { SecretsManager as SecretsManager9 } from "@tuttiai/core";
|
|
2922
|
+
|
|
2923
|
+
// src/commands/traces-render.ts
|
|
2924
|
+
import chalk19 from "chalk";
|
|
2925
|
+
function visibleLen(s) {
|
|
2926
|
+
return s.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
2927
|
+
}
|
|
2928
|
+
function pad4(s, len) {
|
|
2929
|
+
const v = visibleLen(s);
|
|
2930
|
+
return v >= len ? s : s + " ".repeat(len - v);
|
|
2931
|
+
}
|
|
2932
|
+
function colorStatus(status) {
|
|
2933
|
+
if (status === "ok") return chalk19.green("ok");
|
|
2934
|
+
if (status === "error") return chalk19.red("error");
|
|
2935
|
+
return chalk19.yellow("running");
|
|
2936
|
+
}
|
|
2937
|
+
function formatCost(cost) {
|
|
2938
|
+
if (cost === null) return chalk19.dim("\u2014");
|
|
2939
|
+
if (cost === 0) return "$0";
|
|
2940
|
+
return "$" + cost.toFixed(6);
|
|
2941
|
+
}
|
|
2942
|
+
function formatTokens(n) {
|
|
2943
|
+
return n > 0 ? String(n) : chalk19.dim("\u2014");
|
|
2944
|
+
}
|
|
2945
|
+
function formatDuration(ms) {
|
|
2946
|
+
if (ms === null) return chalk19.dim("\u2014");
|
|
2947
|
+
return ms + "ms";
|
|
2948
|
+
}
|
|
2949
|
+
function renderTracesList(traces) {
|
|
2950
|
+
if (traces.length === 0) {
|
|
2951
|
+
return chalk19.dim("No traces found.");
|
|
2952
|
+
}
|
|
2953
|
+
const lines = [];
|
|
2954
|
+
lines.push("");
|
|
2955
|
+
lines.push(
|
|
2956
|
+
chalk19.dim(
|
|
2957
|
+
" " + pad4("TRACE", 10) + pad4("AGENT", 18) + pad4("STARTED", 12) + pad4("DURATION", 12) + pad4("STATUS", 12) + pad4("TOKENS", 10) + "COST"
|
|
2958
|
+
)
|
|
2959
|
+
);
|
|
2960
|
+
lines.push(chalk19.dim(" " + "\u2500".repeat(80)));
|
|
2961
|
+
for (const t of traces) {
|
|
2962
|
+
const traceShort = t.trace_id.slice(0, 8);
|
|
2963
|
+
const startedShort = t.started_at.slice(11, 19);
|
|
2964
|
+
lines.push(
|
|
2965
|
+
" " + chalk19.bold(pad4(traceShort, 10)) + pad4(t.agent_id ?? chalk19.dim("\u2014"), 18) + pad4(startedShort, 12) + pad4(formatDuration(t.duration_ms), 12) + pad4(colorStatus(t.status), 12) + pad4(formatTokens(t.total_tokens), 10) + formatCost(t.cost_usd)
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
lines.push("");
|
|
2969
|
+
return lines.join("\n");
|
|
2970
|
+
}
|
|
2971
|
+
var SPAN_ICONS = {
|
|
2972
|
+
agent: "\u25B6",
|
|
2973
|
+
llm: "\u25C6",
|
|
2974
|
+
tool: "\u2699",
|
|
2975
|
+
guardrail: "\u{1F6E1}",
|
|
2976
|
+
checkpoint: "\u{1F4BE}"
|
|
2977
|
+
};
|
|
2978
|
+
function renderSpanLine(span, indent) {
|
|
2979
|
+
const icon = SPAN_ICONS[span.kind];
|
|
2980
|
+
const indentStr = " ".repeat(indent);
|
|
2981
|
+
const dur = span.duration_ms !== void 0 ? chalk19.dim(" " + span.duration_ms + "ms ") : chalk19.dim(" (running) ");
|
|
2982
|
+
const status = colorStatus(span.status);
|
|
2983
|
+
const attrs = formatAttrs(span);
|
|
2984
|
+
const attrSuffix = attrs ? chalk19.dim(" \xB7 " + attrs) : "";
|
|
2985
|
+
return indentStr + icon + " " + chalk19.bold(span.name) + dur + status + attrSuffix;
|
|
2986
|
+
}
|
|
2987
|
+
function formatAttrs(span) {
|
|
2988
|
+
const a = span.attributes;
|
|
2989
|
+
const parts = [];
|
|
2990
|
+
if (span.kind === "agent") {
|
|
2991
|
+
if (a.agent_id !== void 0) parts.push("agent=" + a.agent_id);
|
|
2992
|
+
if (a.model !== void 0) parts.push("model=" + a.model);
|
|
2993
|
+
} else if (span.kind === "llm") {
|
|
2994
|
+
if (a.model !== void 0) parts.push("model=" + a.model);
|
|
2995
|
+
if (a.total_tokens !== void 0) parts.push(a.total_tokens + " tok");
|
|
2996
|
+
if (a.cost_usd !== void 0) parts.push(formatCost(a.cost_usd));
|
|
2997
|
+
} else if (span.kind === "tool") {
|
|
2998
|
+
if (a.tool_name !== void 0) parts.push(a.tool_name);
|
|
2999
|
+
} else if (span.kind === "guardrail") {
|
|
3000
|
+
if (a.guardrail_name !== void 0) parts.push(a.guardrail_name);
|
|
3001
|
+
if (a.guardrail_action !== void 0) parts.push("\u2192 " + a.guardrail_action);
|
|
3002
|
+
} else if (span.kind === "checkpoint") {
|
|
3003
|
+
if (a.session_id !== void 0) parts.push("session=" + a.session_id.slice(0, 8));
|
|
3004
|
+
}
|
|
3005
|
+
if (span.error?.message) {
|
|
3006
|
+
parts.push(chalk19.red("error: " + span.error.message));
|
|
3007
|
+
}
|
|
3008
|
+
return parts.join(" \xB7 ");
|
|
3009
|
+
}
|
|
3010
|
+
function renderTraceShow(spans) {
|
|
3011
|
+
if (spans.length === 0) {
|
|
3012
|
+
return chalk19.dim("No spans found for this trace.");
|
|
3013
|
+
}
|
|
3014
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
3015
|
+
const presentSpanIds = new Set(spans.map((s) => s.span_id));
|
|
3016
|
+
const roots = [];
|
|
3017
|
+
for (const span of spans) {
|
|
3018
|
+
const parent = span.parent_span_id;
|
|
3019
|
+
if (parent === void 0 || !presentSpanIds.has(parent)) {
|
|
3020
|
+
roots.push(span);
|
|
3021
|
+
continue;
|
|
3022
|
+
}
|
|
3023
|
+
const arr = childrenByParent.get(parent) ?? [];
|
|
3024
|
+
arr.push(span);
|
|
3025
|
+
childrenByParent.set(parent, arr);
|
|
3026
|
+
}
|
|
3027
|
+
roots.sort((a, b) => a.started_at.getTime() - b.started_at.getTime());
|
|
3028
|
+
const lines = [""];
|
|
3029
|
+
function walk(span, indent) {
|
|
3030
|
+
lines.push(renderSpanLine(span, indent));
|
|
3031
|
+
const kids = childrenByParent.get(span.span_id);
|
|
3032
|
+
if (!kids) return;
|
|
3033
|
+
for (const child of kids) {
|
|
3034
|
+
walk(child, indent + 1);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
for (const root of roots) walk(root, 0);
|
|
3038
|
+
let total_tokens = 0;
|
|
3039
|
+
let total_cost = 0;
|
|
3040
|
+
let any_cost = false;
|
|
3041
|
+
for (const s of spans) {
|
|
3042
|
+
if (s.name !== "llm.completion") continue;
|
|
3043
|
+
total_tokens += s.attributes.total_tokens ?? 0;
|
|
3044
|
+
if (s.attributes.cost_usd !== void 0) {
|
|
3045
|
+
total_cost += s.attributes.cost_usd;
|
|
3046
|
+
any_cost = true;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
const wall_ms = roots.map((r) => r.duration_ms).find((d) => d !== void 0);
|
|
3050
|
+
lines.push("");
|
|
3051
|
+
lines.push(chalk19.dim("\u2500".repeat(60)));
|
|
3052
|
+
lines.push(
|
|
3053
|
+
chalk19.dim("Total: ") + chalk19.bold(formatTokens(total_tokens)) + chalk19.dim(" tokens \xB7 ") + chalk19.bold(any_cost ? formatCost(total_cost) : chalk19.dim("\u2014")) + chalk19.dim(" cost \xB7 ") + chalk19.bold(wall_ms !== void 0 ? wall_ms + "ms" : chalk19.dim("\u2014")) + chalk19.dim(" wall")
|
|
3054
|
+
);
|
|
3055
|
+
lines.push("");
|
|
3056
|
+
return lines.join("\n");
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// src/commands/traces.ts
|
|
3060
|
+
var DEFAULT_SERVER_URL = "http://127.0.0.1:3847";
|
|
3061
|
+
function resolveUrl(opts) {
|
|
3062
|
+
return opts.url ?? SecretsManager9.optional("TUTTI_SERVER_URL") ?? DEFAULT_SERVER_URL;
|
|
3063
|
+
}
|
|
3064
|
+
function resolveAuthHeader(opts) {
|
|
3065
|
+
const key = opts.apiKey ?? SecretsManager9.optional("TUTTI_API_KEY");
|
|
3066
|
+
return key ? { Authorization: "Bearer " + key } : {};
|
|
3067
|
+
}
|
|
3068
|
+
function explainConnectionError(err, baseUrl) {
|
|
3069
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3070
|
+
console.error(chalk20.red("Failed to reach Tutti server at " + baseUrl));
|
|
3071
|
+
console.error(chalk20.dim(" " + msg));
|
|
3072
|
+
console.error(chalk20.dim(" Is `tutti-ai serve` running? Set --url or TUTTI_SERVER_URL to override."));
|
|
3073
|
+
process.exit(1);
|
|
3074
|
+
}
|
|
3075
|
+
async function tracesListCommand(opts) {
|
|
3076
|
+
const baseUrl = resolveUrl(opts);
|
|
3077
|
+
const url = baseUrl.replace(/\/$/, "") + "/traces";
|
|
3078
|
+
let res;
|
|
3079
|
+
try {
|
|
3080
|
+
res = await fetch(url, { headers: resolveAuthHeader(opts) });
|
|
3081
|
+
} catch (err) {
|
|
3082
|
+
explainConnectionError(err, baseUrl);
|
|
3083
|
+
}
|
|
3084
|
+
if (res.status === 401) {
|
|
3085
|
+
console.error(chalk20.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
3086
|
+
process.exit(1);
|
|
3087
|
+
}
|
|
3088
|
+
if (!res.ok) {
|
|
3089
|
+
console.error(chalk20.red("Server returned " + res.status + " " + res.statusText));
|
|
3090
|
+
process.exit(1);
|
|
3091
|
+
}
|
|
3092
|
+
const body = await res.json();
|
|
3093
|
+
console.log(renderTracesList(body.traces));
|
|
3094
|
+
}
|
|
3095
|
+
async function tracesShowCommand(traceId, opts) {
|
|
3096
|
+
const baseUrl = resolveUrl(opts);
|
|
3097
|
+
const url = baseUrl.replace(/\/$/, "") + "/traces/" + encodeURIComponent(traceId);
|
|
3098
|
+
let res;
|
|
3099
|
+
try {
|
|
3100
|
+
res = await fetch(url, { headers: resolveAuthHeader(opts) });
|
|
3101
|
+
} catch (err) {
|
|
3102
|
+
explainConnectionError(err, baseUrl);
|
|
3103
|
+
}
|
|
3104
|
+
if (res.status === 404) {
|
|
3105
|
+
console.error(chalk20.red('Trace "' + traceId + '" not found.'));
|
|
3106
|
+
process.exit(1);
|
|
3107
|
+
}
|
|
3108
|
+
if (res.status === 401) {
|
|
3109
|
+
console.error(chalk20.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
3110
|
+
process.exit(1);
|
|
3111
|
+
}
|
|
3112
|
+
if (!res.ok) {
|
|
3113
|
+
console.error(chalk20.red("Server returned " + res.status + " " + res.statusText));
|
|
3114
|
+
process.exit(1);
|
|
3115
|
+
}
|
|
3116
|
+
const body = await res.json();
|
|
3117
|
+
const spans = body.spans.map(reviveSpanDates);
|
|
3118
|
+
console.log(renderTraceShow(spans));
|
|
3119
|
+
}
|
|
3120
|
+
async function tracesTailCommand(opts) {
|
|
3121
|
+
const baseUrl = resolveUrl(opts);
|
|
3122
|
+
const url = baseUrl.replace(/\/$/, "") + "/traces/stream";
|
|
3123
|
+
console.error(chalk20.dim("Tailing traces from " + baseUrl + " \u2014 Ctrl+C to exit"));
|
|
3124
|
+
console.error("");
|
|
3125
|
+
const controller = new AbortController();
|
|
3126
|
+
process.once("SIGINT", () => {
|
|
3127
|
+
controller.abort();
|
|
3128
|
+
console.error("");
|
|
3129
|
+
console.error(chalk20.dim("Disconnected."));
|
|
3130
|
+
process.exit(0);
|
|
3131
|
+
});
|
|
3132
|
+
let res;
|
|
3133
|
+
try {
|
|
3134
|
+
res = await fetch(url, {
|
|
3135
|
+
headers: { ...resolveAuthHeader(opts), Accept: "text/event-stream" },
|
|
3136
|
+
signal: controller.signal
|
|
3137
|
+
});
|
|
3138
|
+
} catch (err) {
|
|
3139
|
+
if (controller.signal.aborted) return;
|
|
3140
|
+
explainConnectionError(err, baseUrl);
|
|
3141
|
+
}
|
|
3142
|
+
if (res.status === 401) {
|
|
3143
|
+
console.error(chalk20.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
3144
|
+
process.exit(1);
|
|
3145
|
+
}
|
|
3146
|
+
if (!res.ok || !res.body) {
|
|
3147
|
+
console.error(chalk20.red("Server returned " + res.status + " " + res.statusText));
|
|
3148
|
+
process.exit(1);
|
|
3149
|
+
}
|
|
3150
|
+
const reader = res.body.getReader();
|
|
3151
|
+
const decoder = new TextDecoder();
|
|
3152
|
+
let buffer = "";
|
|
3153
|
+
for (; ; ) {
|
|
3154
|
+
let chunk;
|
|
3155
|
+
try {
|
|
3156
|
+
chunk = await reader.read();
|
|
3157
|
+
} catch {
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
if (chunk.done) break;
|
|
3161
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
3162
|
+
let frameEnd;
|
|
3163
|
+
while ((frameEnd = buffer.indexOf("\n\n")) !== -1) {
|
|
3164
|
+
const frame = buffer.slice(0, frameEnd);
|
|
3165
|
+
buffer = buffer.slice(frameEnd + 2);
|
|
3166
|
+
const dataLine = frame.split("\n").find((l) => l.startsWith("data: "));
|
|
3167
|
+
if (!dataLine) continue;
|
|
3168
|
+
try {
|
|
3169
|
+
const span = reviveSpanDates(JSON.parse(dataLine.slice(6)));
|
|
3170
|
+
console.log(renderSpanLine(span, 0));
|
|
3171
|
+
} catch (err) {
|
|
3172
|
+
console.error(chalk20.red("Bad SSE frame: " + (err instanceof Error ? err.message : String(err))));
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
function reviveSpanDates(span) {
|
|
3178
|
+
return {
|
|
3179
|
+
...span,
|
|
3180
|
+
started_at: new Date(span.started_at),
|
|
3181
|
+
...span.ended_at !== void 0 ? { ended_at: new Date(span.ended_at) } : {}
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
|
|
2446
3185
|
// src/index.ts
|
|
2447
3186
|
config();
|
|
2448
|
-
var
|
|
3187
|
+
var logger15 = createLogger15("tutti-cli");
|
|
2449
3188
|
process.on("unhandledRejection", (reason) => {
|
|
2450
|
-
|
|
3189
|
+
logger15.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
|
|
2451
3190
|
process.exit(1);
|
|
2452
3191
|
});
|
|
2453
3192
|
process.on("uncaughtException", (err) => {
|
|
2454
|
-
|
|
3193
|
+
logger15.error({ error: err.message }, "Fatal error");
|
|
2455
3194
|
process.exit(1);
|
|
2456
3195
|
});
|
|
2457
3196
|
var program = new Command();
|
|
@@ -2511,6 +3250,18 @@ program.command("publish").description("Publish the current voice to npm and the
|
|
|
2511
3250
|
program.command("eval <suite-file>").description("Run an evaluation suite against a score").option("--ci", "Exit with code 1 if any case fails").option("-s, --score <path>", "Path to score file (default: ./tutti.score.ts)").action(async (suitePath, opts) => {
|
|
2512
3251
|
await evalCommand(suitePath, opts);
|
|
2513
3252
|
});
|
|
3253
|
+
program.command("update").description("Update @tuttiai packages to their latest versions").action(() => {
|
|
3254
|
+
updateCommand();
|
|
3255
|
+
});
|
|
3256
|
+
program.command("outdated").description("Check installed @tuttiai packages for newer versions").action(() => {
|
|
3257
|
+
outdatedCommand();
|
|
3258
|
+
});
|
|
3259
|
+
program.command("info [score]").description("Show project info \u2014 agents, voices, models, and package versions").action(async (score) => {
|
|
3260
|
+
await infoCommand(score);
|
|
3261
|
+
});
|
|
3262
|
+
program.command("upgrade [voice]").description("Upgrade a specific voice or all @tuttiai packages to latest").action((voice) => {
|
|
3263
|
+
upgradeCommand(voice);
|
|
3264
|
+
});
|
|
2514
3265
|
program.command("replay <session-id>").description("Time-travel debugger \u2014 navigate and replay a session from PostgreSQL").option("-s, --score <path>", "Path to score file for replay-from (default: ./tutti.score.ts)").action(async (sessionId, opts) => {
|
|
2515
3266
|
await replayCommand(sessionId, { score: opts.score });
|
|
2516
3267
|
});
|
|
@@ -2533,5 +3284,27 @@ schedulesCmd.command("trigger <id>").description("Manually trigger a scheduled r
|
|
|
2533
3284
|
schedulesCmd.command("runs <id>").description("Show run history for a schedule (last 20 runs)").action(async (id) => {
|
|
2534
3285
|
await schedulesRunsCommand(id);
|
|
2535
3286
|
});
|
|
3287
|
+
var tracesCmd = program.command("traces").description("Inspect spans emitted by a running tutti-ai serve process");
|
|
3288
|
+
tracesCmd.command("list").description("Show the last 20 traces (most recent first)").option("-u, --url <url>", "Server URL (default: http://127.0.0.1:3847)").option("-k, --api-key <key>", "Bearer token (default: TUTTI_API_KEY env)").action(async (opts) => {
|
|
3289
|
+
const resolved = {
|
|
3290
|
+
...opts.url !== void 0 ? { url: opts.url } : {},
|
|
3291
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {}
|
|
3292
|
+
};
|
|
3293
|
+
await tracesListCommand(resolved);
|
|
3294
|
+
});
|
|
3295
|
+
tracesCmd.command("show <trace-id>").description("Render every span in a trace as an indented tree").option("-u, --url <url>", "Server URL (default: http://127.0.0.1:3847)").option("-k, --api-key <key>", "Bearer token (default: TUTTI_API_KEY env)").action(async (traceId, opts) => {
|
|
3296
|
+
const resolved = {
|
|
3297
|
+
...opts.url !== void 0 ? { url: opts.url } : {},
|
|
3298
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {}
|
|
3299
|
+
};
|
|
3300
|
+
await tracesShowCommand(traceId, resolved);
|
|
3301
|
+
});
|
|
3302
|
+
tracesCmd.command("tail").description("Live-tail spans as they are emitted (Ctrl+C to exit)").option("-u, --url <url>", "Server URL (default: http://127.0.0.1:3847)").option("-k, --api-key <key>", "Bearer token (default: TUTTI_API_KEY env)").action(async (opts) => {
|
|
3303
|
+
const resolved = {
|
|
3304
|
+
...opts.url !== void 0 ? { url: opts.url } : {},
|
|
3305
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {}
|
|
3306
|
+
};
|
|
3307
|
+
await tracesTailCommand(resolved);
|
|
3308
|
+
});
|
|
2536
3309
|
program.parse();
|
|
2537
3310
|
//# sourceMappingURL=index.js.map
|