@tuttiai/cli 0.12.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 +1162 -76
- 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,28 +1975,796 @@ async function scheduleCommand(scorePath) {
|
|
|
1960
1975
|
await new Promise(() => void 0);
|
|
1961
1976
|
}
|
|
1962
1977
|
|
|
1963
|
-
// src/commands/
|
|
1964
|
-
import {
|
|
1978
|
+
// src/commands/update.ts
|
|
1979
|
+
import { execSync as execSync3 } from "child_process";
|
|
1980
|
+
import { existsSync as existsSync12, readFileSync as readFileSync5 } from "fs";
|
|
1965
1981
|
import { resolve as resolve12 } from "path";
|
|
1966
1982
|
import chalk12 from "chalk";
|
|
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";
|
|
2434
|
+
import {
|
|
2435
|
+
PostgresSessionStore,
|
|
2436
|
+
ScoreLoader as ScoreLoader9,
|
|
2437
|
+
TuttiRuntime as TuttiRuntime5,
|
|
2438
|
+
SecretsManager as SecretsManager7,
|
|
2439
|
+
createLogger as createLogger13
|
|
2440
|
+
} from "@tuttiai/core";
|
|
2441
|
+
|
|
2442
|
+
// src/commands/replay-render.ts
|
|
2443
|
+
import chalk16 from "chalk";
|
|
2444
|
+
function messageToText2(msg) {
|
|
2445
|
+
if (typeof msg.content === "string") return msg.content;
|
|
2446
|
+
const parts = [];
|
|
2447
|
+
for (const block of msg.content) {
|
|
2448
|
+
if (block.type === "text") {
|
|
2449
|
+
parts.push(block.text);
|
|
2450
|
+
} else if (block.type === "tool_use") {
|
|
2451
|
+
parts.push("[tool_use " + block.name + "]");
|
|
2452
|
+
} else if (block.type === "tool_result") {
|
|
2453
|
+
const preview = block.content.replace(/\s+/g, " ").trim();
|
|
2454
|
+
parts.push("[tool_result " + (preview.length > 60 ? preview.slice(0, 59) + "\u2026" : preview) + "]");
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return parts.join(" ");
|
|
2458
|
+
}
|
|
2459
|
+
function excerpt2(text, max) {
|
|
2460
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
2461
|
+
return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
|
|
2462
|
+
}
|
|
2463
|
+
function renderList(messages) {
|
|
2464
|
+
const lines = [];
|
|
2465
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2466
|
+
const msg = messages.at(i);
|
|
2467
|
+
if (!msg) continue;
|
|
2468
|
+
const role = msg.role === "user" ? chalk16.blue("user ") : chalk16.green("assistant");
|
|
2469
|
+
const text = excerpt2(messageToText2(msg), 80);
|
|
2470
|
+
lines.push(
|
|
2471
|
+
chalk16.dim(String(i).padStart(3)) + " " + role + " " + text
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
return lines.join("\n");
|
|
2475
|
+
}
|
|
2476
|
+
function renderShow(messages, index) {
|
|
2477
|
+
if (index < 0 || index >= messages.length) {
|
|
2478
|
+
return chalk16.red("Index out of range. Valid: 0\u2013" + (messages.length - 1));
|
|
2479
|
+
}
|
|
2480
|
+
const msg = messages.at(index);
|
|
2481
|
+
if (!msg) return chalk16.red("Index out of range.");
|
|
2482
|
+
const lines = [];
|
|
2483
|
+
lines.push(chalk16.cyan.bold("Turn " + index) + " " + chalk16.dim("[" + msg.role + "]"));
|
|
2484
|
+
lines.push("");
|
|
2485
|
+
if (typeof msg.content === "string") {
|
|
2486
|
+
lines.push(msg.content);
|
|
2487
|
+
} else {
|
|
2488
|
+
for (const block of msg.content) {
|
|
2489
|
+
if (block.type === "text") {
|
|
2490
|
+
lines.push(block.text);
|
|
2491
|
+
} else if (block.type === "tool_use") {
|
|
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)));
|
|
2495
|
+
} else if (block.type === "tool_result") {
|
|
2496
|
+
const label = block.is_error ? chalk16.red(" tool_result (error):") : chalk16.green(" tool_result:");
|
|
2497
|
+
lines.push(label);
|
|
2498
|
+
lines.push(chalk16.dim(" tool_use_id: " + block.tool_use_id));
|
|
2499
|
+
lines.push(" " + block.content);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
return lines.join("\n");
|
|
2504
|
+
}
|
|
2505
|
+
function renderInspect(messages, index) {
|
|
2506
|
+
if (index < 0 || index >= messages.length) {
|
|
2507
|
+
return chalk16.red("Index out of range.");
|
|
2508
|
+
}
|
|
2509
|
+
return JSON.stringify(messages.at(index), null, 2);
|
|
2510
|
+
}
|
|
2511
|
+
function exportJSON(session) {
|
|
2512
|
+
return JSON.stringify(
|
|
2513
|
+
{
|
|
2514
|
+
id: session.id,
|
|
2515
|
+
agent_name: session.agent_name,
|
|
2516
|
+
created_at: session.created_at,
|
|
2517
|
+
messages: session.messages
|
|
2518
|
+
},
|
|
2519
|
+
null,
|
|
2520
|
+
2
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
function exportMarkdown(session) {
|
|
2524
|
+
const lines = [];
|
|
2525
|
+
lines.push("# Session " + session.id);
|
|
2526
|
+
lines.push("");
|
|
2527
|
+
lines.push("**Agent:** " + session.agent_name);
|
|
2528
|
+
lines.push("**Created:** " + session.created_at.toISOString());
|
|
2529
|
+
lines.push("**Messages:** " + session.messages.length);
|
|
2530
|
+
lines.push("");
|
|
2531
|
+
lines.push("---");
|
|
2532
|
+
lines.push("");
|
|
2533
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
2534
|
+
const msg = session.messages.at(i);
|
|
2535
|
+
if (!msg) continue;
|
|
2536
|
+
lines.push("## Turn " + i + " (" + msg.role + ")");
|
|
2537
|
+
lines.push("");
|
|
2538
|
+
if (typeof msg.content === "string") {
|
|
2539
|
+
lines.push(msg.content);
|
|
2540
|
+
} else {
|
|
2541
|
+
for (const block of msg.content) {
|
|
2542
|
+
if (block.type === "text") {
|
|
2543
|
+
lines.push(block.text);
|
|
2544
|
+
} else if (block.type === "tool_use") {
|
|
2545
|
+
lines.push("**Tool call:** `" + block.name + "`");
|
|
2546
|
+
lines.push("```json\n" + JSON.stringify(block.input, null, 2) + "\n```");
|
|
2547
|
+
} else if (block.type === "tool_result") {
|
|
2548
|
+
const label = block.is_error ? "**Tool error:**" : "**Tool result:**";
|
|
2549
|
+
lines.push(label);
|
|
2550
|
+
lines.push("```\n" + block.content + "\n```");
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
lines.push("");
|
|
2555
|
+
}
|
|
2556
|
+
return lines.join("\n");
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/commands/replay.ts
|
|
2560
|
+
var logger13 = createLogger13("tutti-cli");
|
|
2561
|
+
async function replayCommand(sessionId, opts = {}) {
|
|
2562
|
+
const pgUrl = SecretsManager7.optional("TUTTI_PG_URL");
|
|
2563
|
+
if (!pgUrl) {
|
|
2564
|
+
console.error(chalk17.red("TUTTI_PG_URL is not set."));
|
|
2565
|
+
console.error(
|
|
2566
|
+
chalk17.dim(
|
|
2567
|
+
"The replay command requires PostgreSQL for session persistence.\nSet TUTTI_PG_URL=postgres://user:pass@host/db in your environment."
|
|
2568
|
+
)
|
|
2569
|
+
);
|
|
2570
|
+
process.exit(1);
|
|
2571
|
+
}
|
|
2572
|
+
const store = new PostgresSessionStore(pgUrl);
|
|
2573
|
+
const spinner = ora10({ color: "cyan" }).start("Loading session...");
|
|
2574
|
+
let session;
|
|
2575
|
+
try {
|
|
2576
|
+
session = await store.getAsync(sessionId);
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
spinner.fail("Failed to load session");
|
|
2579
|
+
logger13.error(
|
|
2580
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
2581
|
+
"Session store error"
|
|
2582
|
+
);
|
|
2583
|
+
process.exit(1);
|
|
2584
|
+
}
|
|
2585
|
+
spinner.stop();
|
|
2586
|
+
if (!session) {
|
|
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."));
|
|
2589
|
+
await store.close();
|
|
2590
|
+
process.exit(1);
|
|
2591
|
+
}
|
|
2592
|
+
const messages = session.messages;
|
|
2593
|
+
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));
|
|
2599
|
+
console.log("");
|
|
2600
|
+
console.log(chalk17.dim(" Commands: list, show <n>, next, prev, inspect, replay-from <n>, export <json|md>, quit"));
|
|
2601
|
+
console.log("");
|
|
2602
|
+
let cursor = 0;
|
|
2603
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2604
|
+
try {
|
|
2605
|
+
while (true) {
|
|
2606
|
+
const prompt3 = chalk17.cyan("replay [" + cursor + "/" + (messages.length - 1) + "]> ");
|
|
2607
|
+
const raw = await rl.question(prompt3);
|
|
2608
|
+
const input = raw.trim();
|
|
2609
|
+
if (!input) continue;
|
|
2610
|
+
const [cmd, ...args] = input.split(/\s+/);
|
|
2611
|
+
switch (cmd) {
|
|
2612
|
+
case "quit":
|
|
2613
|
+
case "exit":
|
|
2614
|
+
case "q":
|
|
2615
|
+
console.log(chalk17.dim("Bye."));
|
|
2616
|
+
return;
|
|
2617
|
+
case "list":
|
|
2618
|
+
console.log(renderList(messages));
|
|
2619
|
+
break;
|
|
2620
|
+
case "show": {
|
|
2621
|
+
const n = parseInt(args[0] ?? String(cursor), 10);
|
|
2622
|
+
console.log(renderShow(messages, n));
|
|
2623
|
+
if (n >= 0 && n < messages.length) cursor = n;
|
|
2624
|
+
break;
|
|
2625
|
+
}
|
|
2626
|
+
case "next":
|
|
2627
|
+
if (cursor < messages.length - 1) {
|
|
2628
|
+
cursor++;
|
|
2629
|
+
console.log(renderShow(messages, cursor));
|
|
2630
|
+
} else {
|
|
2631
|
+
console.log(chalk17.dim("Already at last message."));
|
|
2632
|
+
}
|
|
2633
|
+
break;
|
|
2634
|
+
case "prev":
|
|
2635
|
+
if (cursor > 0) {
|
|
2636
|
+
cursor--;
|
|
2637
|
+
console.log(renderShow(messages, cursor));
|
|
2638
|
+
} else {
|
|
2639
|
+
console.log(chalk17.dim("Already at first message."));
|
|
2640
|
+
}
|
|
2641
|
+
break;
|
|
2642
|
+
case "inspect":
|
|
2643
|
+
console.log(renderInspect(messages, cursor));
|
|
2644
|
+
break;
|
|
2645
|
+
case "replay-from": {
|
|
2646
|
+
const turn = parseInt(args[0] ?? "", 10);
|
|
2647
|
+
if (isNaN(turn) || turn < 0 || turn >= messages.length) {
|
|
2648
|
+
console.log(chalk17.red("Usage: replay-from <turn-number>"));
|
|
2649
|
+
break;
|
|
2650
|
+
}
|
|
2651
|
+
await handleReplayFrom(turn, session, rl, opts);
|
|
2652
|
+
break;
|
|
2653
|
+
}
|
|
2654
|
+
case "export": {
|
|
2655
|
+
const format = args[0];
|
|
2656
|
+
if (format === "json") {
|
|
2657
|
+
const filename = "session-" + session.id.slice(0, 8) + ".json";
|
|
2658
|
+
await writeFile(filename, exportJSON(session));
|
|
2659
|
+
console.log(chalk17.green("Exported to " + filename));
|
|
2660
|
+
} else if (format === "md" || format === "markdown") {
|
|
2661
|
+
const filename = "session-" + session.id.slice(0, 8) + ".md";
|
|
2662
|
+
await writeFile(filename, exportMarkdown(session));
|
|
2663
|
+
console.log(chalk17.green("Exported to " + filename));
|
|
2664
|
+
} else {
|
|
2665
|
+
console.log(chalk17.dim("Usage: export <json|md>"));
|
|
2666
|
+
}
|
|
2667
|
+
break;
|
|
2668
|
+
}
|
|
2669
|
+
default:
|
|
2670
|
+
console.log(
|
|
2671
|
+
chalk17.dim("Unknown command. Available: list, show <n>, next, prev, inspect, replay-from <n>, export <json|md>, quit")
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
} finally {
|
|
2676
|
+
rl.close();
|
|
2677
|
+
await store.close();
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
async function handleReplayFrom(turn, session, rl, opts) {
|
|
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."));
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
const originalMsg = session.messages.at(turn);
|
|
2688
|
+
const originalInput = originalMsg ? messageToText2(originalMsg) : "";
|
|
2689
|
+
const answer = await rl.question(
|
|
2690
|
+
chalk17.cyan("Replay from turn " + turn + " with original input? ") + chalk17.dim("(y / enter new input) ")
|
|
2691
|
+
);
|
|
2692
|
+
const input = answer.trim().toLowerCase() === "y" || answer.trim() === "" ? originalInput : answer.trim();
|
|
2693
|
+
if (!input) {
|
|
2694
|
+
console.log(chalk17.dim("No input provided. Cancelled."));
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const spinnerLoad = ora10({ color: "cyan" }).start("Loading score...");
|
|
2698
|
+
let score;
|
|
2699
|
+
try {
|
|
2700
|
+
score = await ScoreLoader9.load(scoreFile);
|
|
2701
|
+
} catch (err) {
|
|
2702
|
+
spinnerLoad.fail("Failed to load score");
|
|
2703
|
+
logger13.error({ error: err instanceof Error ? err.message : String(err) }, "Score load error");
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
spinnerLoad.stop();
|
|
2707
|
+
const restoredMessages = session.messages.slice(0, turn);
|
|
2708
|
+
const runtime = new TuttiRuntime5(score);
|
|
2709
|
+
const sessions = runtime.sessions;
|
|
2710
|
+
if ("save" in sessions && typeof sessions.save === "function") {
|
|
2711
|
+
sessions.save({
|
|
2712
|
+
id: session.id,
|
|
2713
|
+
agent_name: session.agent_name,
|
|
2714
|
+
messages: restoredMessages,
|
|
2715
|
+
created_at: session.created_at,
|
|
2716
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
const agentName = session.agent_name;
|
|
2720
|
+
const agentMap = new Map(Object.entries(score.agents));
|
|
2721
|
+
const agent = agentMap.get(agentName);
|
|
2722
|
+
if (!agent) {
|
|
2723
|
+
console.log(chalk17.red('Agent "' + agentName + '" not found in score.'));
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
const spinnerRun = ora10({ color: "cyan" }).start("Running from turn " + turn + "...");
|
|
2727
|
+
runtime.events.on("token:stream", (e) => {
|
|
2728
|
+
spinnerRun.stop();
|
|
2729
|
+
process.stdout.write(e.text);
|
|
2730
|
+
});
|
|
2731
|
+
try {
|
|
2732
|
+
const result = await runtime.run(agentName, input, session.id);
|
|
2733
|
+
spinnerRun.stop();
|
|
2734
|
+
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"));
|
|
2738
|
+
console.log("");
|
|
2739
|
+
console.log(result.output);
|
|
2740
|
+
} catch (err) {
|
|
2741
|
+
spinnerRun.fail("Replay failed");
|
|
2742
|
+
logger13.error({ error: err instanceof Error ? err.message : String(err) }, "Replay error");
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// src/commands/schedules.ts
|
|
2747
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2748
|
+
import { resolve as resolve17 } from "path";
|
|
2749
|
+
import chalk18 from "chalk";
|
|
1967
2750
|
import {
|
|
1968
|
-
ScoreLoader as
|
|
2751
|
+
ScoreLoader as ScoreLoader10,
|
|
1969
2752
|
SchedulerEngine as SchedulerEngine2,
|
|
1970
2753
|
PostgresScheduleStore as PostgresScheduleStore2,
|
|
1971
2754
|
MemoryScheduleStore as MemoryScheduleStore2,
|
|
1972
2755
|
AgentRunner as AgentRunner2,
|
|
1973
2756
|
EventBus as EventBus2,
|
|
1974
2757
|
InMemorySessionStore as InMemorySessionStore4,
|
|
1975
|
-
SecretsManager as
|
|
1976
|
-
createLogger as
|
|
2758
|
+
SecretsManager as SecretsManager8,
|
|
2759
|
+
createLogger as createLogger14
|
|
1977
2760
|
} from "@tuttiai/core";
|
|
1978
|
-
var
|
|
2761
|
+
var logger14 = createLogger14("tutti-cli");
|
|
1979
2762
|
function resolveStore2() {
|
|
1980
|
-
const pgUrl =
|
|
2763
|
+
const pgUrl = SecretsManager8.optional("TUTTI_PG_URL");
|
|
1981
2764
|
if (pgUrl) {
|
|
1982
2765
|
return new PostgresScheduleStore2({ connection_string: pgUrl });
|
|
1983
2766
|
}
|
|
1984
|
-
|
|
2767
|
+
logger14.warn("TUTTI_PG_URL not set \u2014 using in-memory store (schedules are ephemeral)");
|
|
1985
2768
|
return new MemoryScheduleStore2();
|
|
1986
2769
|
}
|
|
1987
2770
|
async function closeStore(store) {
|
|
@@ -1995,7 +2778,7 @@ function formatTrigger(r) {
|
|
|
1995
2778
|
if (r.config.at) return "at " + r.config.at;
|
|
1996
2779
|
return "?";
|
|
1997
2780
|
}
|
|
1998
|
-
function
|
|
2781
|
+
function pad3(s, len) {
|
|
1999
2782
|
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
2000
2783
|
}
|
|
2001
2784
|
async function schedulesListCommand() {
|
|
@@ -2003,22 +2786,22 @@ async function schedulesListCommand() {
|
|
|
2003
2786
|
try {
|
|
2004
2787
|
const records = await store.list();
|
|
2005
2788
|
if (records.length === 0) {
|
|
2006
|
-
console.log(
|
|
2007
|
-
console.log(
|
|
2789
|
+
console.log(chalk18.dim("No schedules found."));
|
|
2790
|
+
console.log(chalk18.dim('Run "tutti-ai schedule" to start the scheduler daemon.'));
|
|
2008
2791
|
return;
|
|
2009
2792
|
}
|
|
2010
2793
|
console.log("");
|
|
2011
2794
|
console.log(
|
|
2012
|
-
|
|
2013
|
-
" " +
|
|
2795
|
+
chalk18.dim(
|
|
2796
|
+
" " + pad3("ID", 20) + pad3("AGENT", 16) + pad3("TRIGGER", 22) + pad3("ENABLED", 10) + pad3("RUNS", 8) + "CREATED"
|
|
2014
2797
|
)
|
|
2015
2798
|
);
|
|
2016
|
-
console.log(
|
|
2799
|
+
console.log(chalk18.dim(" " + "\u2500".repeat(90)));
|
|
2017
2800
|
for (const r of records) {
|
|
2018
|
-
const enabled = r.enabled ?
|
|
2801
|
+
const enabled = r.enabled ? chalk18.green("yes") : chalk18.red("no") + " ";
|
|
2019
2802
|
const maxLabel = r.config.max_runs ? r.run_count + "/" + r.config.max_runs : String(r.run_count);
|
|
2020
2803
|
console.log(
|
|
2021
|
-
" " +
|
|
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))
|
|
2022
2805
|
);
|
|
2023
2806
|
}
|
|
2024
2807
|
console.log("");
|
|
@@ -2031,11 +2814,11 @@ async function schedulesEnableCommand(id) {
|
|
|
2031
2814
|
try {
|
|
2032
2815
|
const record = await store.get(id);
|
|
2033
2816
|
if (!record) {
|
|
2034
|
-
console.error(
|
|
2817
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2035
2818
|
process.exit(1);
|
|
2036
2819
|
}
|
|
2037
2820
|
await store.setEnabled(id, true);
|
|
2038
|
-
console.log(
|
|
2821
|
+
console.log(chalk18.green('Schedule "' + id + '" enabled.'));
|
|
2039
2822
|
} finally {
|
|
2040
2823
|
await closeStore(store);
|
|
2041
2824
|
}
|
|
@@ -2045,22 +2828,22 @@ async function schedulesDisableCommand(id) {
|
|
|
2045
2828
|
try {
|
|
2046
2829
|
const record = await store.get(id);
|
|
2047
2830
|
if (!record) {
|
|
2048
|
-
console.error(
|
|
2831
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2049
2832
|
process.exit(1);
|
|
2050
2833
|
}
|
|
2051
2834
|
await store.setEnabled(id, false);
|
|
2052
|
-
console.log(
|
|
2835
|
+
console.log(chalk18.yellow('Schedule "' + id + '" disabled.'));
|
|
2053
2836
|
} finally {
|
|
2054
2837
|
await closeStore(store);
|
|
2055
2838
|
}
|
|
2056
2839
|
}
|
|
2057
2840
|
async function schedulesTriggerCommand(id, scorePath) {
|
|
2058
|
-
const file =
|
|
2059
|
-
if (!
|
|
2060
|
-
console.error(
|
|
2841
|
+
const file = resolve17(scorePath ?? "./tutti.score.ts");
|
|
2842
|
+
if (!existsSync17(file)) {
|
|
2843
|
+
console.error(chalk18.red("Score file not found: " + file));
|
|
2061
2844
|
process.exit(1);
|
|
2062
2845
|
}
|
|
2063
|
-
const score = await
|
|
2846
|
+
const score = await ScoreLoader10.load(file);
|
|
2064
2847
|
const events = new EventBus2();
|
|
2065
2848
|
const sessions = new InMemorySessionStore4();
|
|
2066
2849
|
const runner = new AgentRunner2(score.provider, events, sessions);
|
|
@@ -2068,30 +2851,30 @@ async function schedulesTriggerCommand(id, scorePath) {
|
|
|
2068
2851
|
try {
|
|
2069
2852
|
const record = await store.get(id);
|
|
2070
2853
|
if (!record) {
|
|
2071
|
-
console.error(
|
|
2854
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2072
2855
|
process.exit(1);
|
|
2073
2856
|
}
|
|
2074
2857
|
const agent = score.agents[record.agent_id];
|
|
2075
2858
|
if (!agent) {
|
|
2076
|
-
console.error(
|
|
2859
|
+
console.error(chalk18.red('Agent "' + record.agent_id + '" not found in score.'));
|
|
2077
2860
|
process.exit(1);
|
|
2078
2861
|
}
|
|
2079
2862
|
const resolvedAgent = agent.model ? agent : { ...agent, model: score.default_model ?? "claude-sonnet-4-20250514" };
|
|
2080
2863
|
const engine = new SchedulerEngine2(store, runner, events);
|
|
2081
2864
|
await engine.schedule(id, resolvedAgent, record.config);
|
|
2082
2865
|
engine.start();
|
|
2083
|
-
console.log(
|
|
2866
|
+
console.log(chalk18.cyan('Triggering "' + id + '" (' + record.agent_id + ")..."));
|
|
2084
2867
|
const run2 = await engine.trigger(id);
|
|
2085
2868
|
engine.stop();
|
|
2086
2869
|
if (run2.error) {
|
|
2087
|
-
console.log(
|
|
2870
|
+
console.log(chalk18.red(" Error: " + run2.error));
|
|
2088
2871
|
process.exit(1);
|
|
2089
2872
|
}
|
|
2090
2873
|
const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() : 0;
|
|
2091
|
-
console.log(
|
|
2874
|
+
console.log(chalk18.green(" Completed in " + duration + "ms"));
|
|
2092
2875
|
if (run2.result) {
|
|
2093
2876
|
const preview = run2.result.length > 200 ? run2.result.slice(0, 200) + "..." : run2.result;
|
|
2094
|
-
console.log(
|
|
2877
|
+
console.log(chalk18.dim(" Output: " + preview));
|
|
2095
2878
|
}
|
|
2096
2879
|
} finally {
|
|
2097
2880
|
await closeStore(store);
|
|
@@ -2102,50 +2885,316 @@ async function schedulesRunsCommand(id) {
|
|
|
2102
2885
|
try {
|
|
2103
2886
|
const record = await store.get(id);
|
|
2104
2887
|
if (!record) {
|
|
2105
|
-
console.error(
|
|
2888
|
+
console.error(chalk18.red('Schedule "' + id + '" not found.'));
|
|
2106
2889
|
process.exit(1);
|
|
2107
2890
|
}
|
|
2108
2891
|
if ("getRuns" in store && typeof store.getRuns === "function") {
|
|
2109
2892
|
const runs = store.getRuns(id);
|
|
2110
2893
|
if (runs.length === 0) {
|
|
2111
|
-
console.log(
|
|
2894
|
+
console.log(chalk18.dim("No runs recorded for this schedule."));
|
|
2112
2895
|
return;
|
|
2113
2896
|
}
|
|
2114
2897
|
const recent = runs.slice(-20);
|
|
2115
2898
|
console.log("");
|
|
2116
|
-
console.log(
|
|
2899
|
+
console.log(chalk18.dim(" Showing last " + recent.length + " of " + runs.length + " runs:"));
|
|
2117
2900
|
console.log("");
|
|
2118
2901
|
for (const run2 of recent) {
|
|
2119
2902
|
const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() + "ms" : "?";
|
|
2120
|
-
const status = run2.error ?
|
|
2903
|
+
const status = run2.error ? chalk18.red("error") : chalk18.green("ok");
|
|
2121
2904
|
const preview = run2.error ? run2.error.slice(0, 80) : (run2.result ?? "").slice(0, 80);
|
|
2122
2905
|
console.log(
|
|
2123
|
-
" " +
|
|
2906
|
+
" " + chalk18.dim(run2.triggered_at.toISOString()) + " " + status + " " + chalk18.dim(duration) + " " + preview
|
|
2124
2907
|
);
|
|
2125
2908
|
}
|
|
2126
2909
|
console.log("");
|
|
2127
2910
|
} else {
|
|
2128
|
-
console.log(
|
|
2129
|
-
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."));
|
|
2130
2913
|
}
|
|
2131
2914
|
} finally {
|
|
2132
2915
|
await closeStore(store);
|
|
2133
2916
|
}
|
|
2134
2917
|
}
|
|
2135
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
|
+
|
|
2136
3185
|
// src/index.ts
|
|
2137
3186
|
config();
|
|
2138
|
-
var
|
|
3187
|
+
var logger15 = createLogger15("tutti-cli");
|
|
2139
3188
|
process.on("unhandledRejection", (reason) => {
|
|
2140
|
-
|
|
3189
|
+
logger15.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
|
|
2141
3190
|
process.exit(1);
|
|
2142
3191
|
});
|
|
2143
3192
|
process.on("uncaughtException", (err) => {
|
|
2144
|
-
|
|
3193
|
+
logger15.error({ error: err.message }, "Fatal error");
|
|
2145
3194
|
process.exit(1);
|
|
2146
3195
|
});
|
|
2147
3196
|
var program = new Command();
|
|
2148
|
-
program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.
|
|
3197
|
+
program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.13.0");
|
|
2149
3198
|
program.command("init [project-name]").description("Create a new Tutti project").option("-t, --template <id>", "Project template to use").action(async (projectName, opts) => {
|
|
2150
3199
|
await initCommand(projectName, opts.template);
|
|
2151
3200
|
});
|
|
@@ -2201,6 +3250,21 @@ program.command("publish").description("Publish the current voice to npm and the
|
|
|
2201
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) => {
|
|
2202
3251
|
await evalCommand(suitePath, opts);
|
|
2203
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
|
+
});
|
|
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) => {
|
|
3266
|
+
await replayCommand(sessionId, { score: opts.score });
|
|
3267
|
+
});
|
|
2204
3268
|
program.command("schedule [score]").description("Start the scheduler daemon \u2014 runs agents on their configured schedules").action(async (score) => {
|
|
2205
3269
|
await scheduleCommand(score);
|
|
2206
3270
|
});
|
|
@@ -2220,5 +3284,27 @@ schedulesCmd.command("trigger <id>").description("Manually trigger a scheduled r
|
|
|
2220
3284
|
schedulesCmd.command("runs <id>").description("Show run history for a schedule (last 20 runs)").action(async (id) => {
|
|
2221
3285
|
await schedulesRunsCommand(id);
|
|
2222
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
|
+
});
|
|
2223
3309
|
program.parse();
|
|
2224
3310
|
//# sourceMappingURL=index.js.map
|