@tuttiai/cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1258 -311
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { config } from "dotenv";
|
|
5
|
-
import { createLogger as
|
|
5
|
+
import { createLogger as createLogger18 } from "@tuttiai/core";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -1729,34 +1729,568 @@ async function evalCommand(suitePath, opts) {
|
|
|
1729
1729
|
}
|
|
1730
1730
|
}
|
|
1731
1731
|
|
|
1732
|
+
// src/commands/eval-record.ts
|
|
1733
|
+
import { readFile } from "fs/promises";
|
|
1734
|
+
import { join as join2, resolve as resolve10 } from "path";
|
|
1735
|
+
import chalk11 from "chalk";
|
|
1736
|
+
import Enquirer3 from "enquirer";
|
|
1737
|
+
import {
|
|
1738
|
+
JsonFileGoldenStore,
|
|
1739
|
+
PostgresSessionStore,
|
|
1740
|
+
SecretsManager as SecretsManager5,
|
|
1741
|
+
createLogger as createLogger10
|
|
1742
|
+
} from "@tuttiai/core";
|
|
1743
|
+
|
|
1744
|
+
// src/commands/eval-record-render.ts
|
|
1745
|
+
import chalk10 from "chalk";
|
|
1746
|
+
function extractSessionDraft(session) {
|
|
1747
|
+
const firstUser = session.messages.find((m) => m.role === "user");
|
|
1748
|
+
const lastAssistant = [...session.messages].reverse().find((m) => m.role === "assistant");
|
|
1749
|
+
return {
|
|
1750
|
+
input: firstUser ? messageText(firstUser) : "",
|
|
1751
|
+
output: lastAssistant ? messageText(lastAssistant) : "",
|
|
1752
|
+
tool_sequence: collectToolSequence(session.messages)
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
function messageText(msg) {
|
|
1756
|
+
if (typeof msg.content === "string") return msg.content;
|
|
1757
|
+
return msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1758
|
+
}
|
|
1759
|
+
function collectToolSequence(messages) {
|
|
1760
|
+
const seq = [];
|
|
1761
|
+
for (const msg of messages) {
|
|
1762
|
+
if (msg.role !== "assistant" || typeof msg.content === "string") continue;
|
|
1763
|
+
for (const block of msg.content) {
|
|
1764
|
+
if (block.type === "tool_use") seq.push(block.name);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return seq;
|
|
1768
|
+
}
|
|
1769
|
+
function truncate200(text) {
|
|
1770
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
1771
|
+
return compact.length > 200 ? compact.slice(0, 199) + "\u2026" : compact;
|
|
1772
|
+
}
|
|
1773
|
+
function renderSessionSummary(session, draft, tokens) {
|
|
1774
|
+
const lines = [];
|
|
1775
|
+
lines.push(chalk10.cyan.bold(" Session summary"));
|
|
1776
|
+
lines.push(chalk10.dim(" Session: ") + session.id);
|
|
1777
|
+
lines.push(chalk10.dim(" Agent: ") + session.agent_name);
|
|
1778
|
+
lines.push(chalk10.dim(" Created: ") + session.created_at.toISOString());
|
|
1779
|
+
lines.push(chalk10.dim(" Tokens: ") + (tokens !== void 0 ? String(tokens) : "\u2014"));
|
|
1780
|
+
lines.push("");
|
|
1781
|
+
lines.push(chalk10.dim(" Input: ") + truncate200(draft.input || "(empty)"));
|
|
1782
|
+
lines.push(chalk10.dim(" Output: ") + truncate200(draft.output || "(empty)"));
|
|
1783
|
+
lines.push(
|
|
1784
|
+
chalk10.dim(" Tools: ") + (draft.tool_sequence.length === 0 ? chalk10.dim("(none)") : draft.tool_sequence.map((t) => chalk10.cyan(t)).join(chalk10.dim(" \u2192 ")))
|
|
1785
|
+
);
|
|
1786
|
+
return lines.join("\n");
|
|
1787
|
+
}
|
|
1788
|
+
function deriveDefaultCaseName(input) {
|
|
1789
|
+
const compact = input.replace(/\s+/g, " ").trim();
|
|
1790
|
+
return compact.length > 40 ? compact.slice(0, 40) : compact;
|
|
1791
|
+
}
|
|
1792
|
+
function parseTagInput(raw) {
|
|
1793
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1794
|
+
}
|
|
1795
|
+
function parseToolSequenceInput(raw) {
|
|
1796
|
+
return raw.split(/[,\u2192]|->/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1797
|
+
}
|
|
1798
|
+
function buildGoldenCase(session, draft, answers, now = /* @__PURE__ */ new Date()) {
|
|
1799
|
+
const expected_output = resolveExpectedOutput(draft, answers);
|
|
1800
|
+
return {
|
|
1801
|
+
id: "",
|
|
1802
|
+
name: answers.name,
|
|
1803
|
+
agent_id: session.agent_name,
|
|
1804
|
+
input: draft.input,
|
|
1805
|
+
...expected_output !== void 0 ? { expected_output } : {},
|
|
1806
|
+
...answers.tool_sequence.length > 0 ? { expected_tool_sequence: answers.tool_sequence } : {},
|
|
1807
|
+
scorers: answers.scorers,
|
|
1808
|
+
...answers.tags.length > 0 ? { tags: answers.tags } : {},
|
|
1809
|
+
promoted_from_session: session.id,
|
|
1810
|
+
created_at: now
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
function resolveExpectedOutput(draft, answers) {
|
|
1814
|
+
if (answers.expected_mode === "actual") return draft.output;
|
|
1815
|
+
if (answers.expected_mode === "custom") return answers.expected_output_custom ?? "";
|
|
1816
|
+
return void 0;
|
|
1817
|
+
}
|
|
1818
|
+
function renderRecordedConfirmation(stored) {
|
|
1819
|
+
return chalk10.green("\u2713") + " Golden case saved: " + chalk10.bold(stored.name) + chalk10.dim(" (" + stored.id + "). Run `tutti-ai eval run` to test against it.");
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// src/commands/eval-record.ts
|
|
1823
|
+
var logger10 = createLogger10("tutti-cli");
|
|
1824
|
+
var { prompt: prompt3 } = Enquirer3;
|
|
1825
|
+
var LOCAL_SESSION_DIR = ".tutti/sessions";
|
|
1826
|
+
async function evalRecordCommand(sessionId) {
|
|
1827
|
+
const { session, close } = await resolveSession(sessionId);
|
|
1828
|
+
try {
|
|
1829
|
+
const draft = extractSessionDraft(session);
|
|
1830
|
+
console.log(renderSessionSummary(session, draft, void 0));
|
|
1831
|
+
console.log("");
|
|
1832
|
+
const answers = await collectAnswers(draft);
|
|
1833
|
+
const goldenCase = buildGoldenCase(session, draft, answers);
|
|
1834
|
+
const store = new JsonFileGoldenStore();
|
|
1835
|
+
const stored = await store.saveCase(goldenCase);
|
|
1836
|
+
console.log(renderRecordedConfirmation(stored));
|
|
1837
|
+
} finally {
|
|
1838
|
+
await close();
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
async function resolveSession(sessionId) {
|
|
1842
|
+
const pgUrl = SecretsManager5.optional("TUTTI_PG_URL");
|
|
1843
|
+
if (pgUrl) {
|
|
1844
|
+
return loadFromPostgres(sessionId, pgUrl);
|
|
1845
|
+
}
|
|
1846
|
+
return loadFromLocalLog(sessionId);
|
|
1847
|
+
}
|
|
1848
|
+
async function loadFromPostgres(sessionId, pgUrl) {
|
|
1849
|
+
const store = new PostgresSessionStore(pgUrl);
|
|
1850
|
+
let session;
|
|
1851
|
+
try {
|
|
1852
|
+
session = await store.getAsync(sessionId);
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
await store.close();
|
|
1855
|
+
logger10.error(
|
|
1856
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
1857
|
+
"Session store error"
|
|
1858
|
+
);
|
|
1859
|
+
process.exit(1);
|
|
1860
|
+
}
|
|
1861
|
+
if (!session) {
|
|
1862
|
+
await store.close();
|
|
1863
|
+
exitSessionNotFound(sessionId, "postgres");
|
|
1864
|
+
}
|
|
1865
|
+
return { session, close: () => store.close() };
|
|
1866
|
+
}
|
|
1867
|
+
async function loadFromLocalLog(sessionId) {
|
|
1868
|
+
const path = resolve10(join2(LOCAL_SESSION_DIR, sessionId + ".json"));
|
|
1869
|
+
let raw;
|
|
1870
|
+
try {
|
|
1871
|
+
raw = await readFile(path, "utf8");
|
|
1872
|
+
} catch {
|
|
1873
|
+
exitSessionNotFound(sessionId, "local");
|
|
1874
|
+
}
|
|
1875
|
+
const parsed = JSON.parse(raw);
|
|
1876
|
+
const session = reviveLocalSession(parsed);
|
|
1877
|
+
return { session, close: () => Promise.resolve() };
|
|
1878
|
+
}
|
|
1879
|
+
function reviveLocalSession(raw) {
|
|
1880
|
+
const obj = raw;
|
|
1881
|
+
return {
|
|
1882
|
+
...obj,
|
|
1883
|
+
created_at: new Date(obj.created_at),
|
|
1884
|
+
updated_at: new Date(obj.updated_at)
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
function exitSessionNotFound(sessionId, source) {
|
|
1888
|
+
console.error(chalk11.red("Session not found: " + sessionId));
|
|
1889
|
+
if (source === "postgres") {
|
|
1890
|
+
console.error(
|
|
1891
|
+
chalk11.dim(
|
|
1892
|
+
" Checked the Postgres session store at TUTTI_PG_URL. Verify the id and that the env var points at the right database."
|
|
1893
|
+
)
|
|
1894
|
+
);
|
|
1895
|
+
} else {
|
|
1896
|
+
console.error(
|
|
1897
|
+
chalk11.dim(
|
|
1898
|
+
" TUTTI_PG_URL is unset and no local log exists at " + LOCAL_SESSION_DIR + "/" + sessionId + ".json.\n Set TUTTI_PG_URL to pull the session from Postgres."
|
|
1899
|
+
)
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
process.exit(1);
|
|
1903
|
+
}
|
|
1904
|
+
async function collectAnswers(draft) {
|
|
1905
|
+
const { name } = await prompt3({
|
|
1906
|
+
type: "input",
|
|
1907
|
+
name: "name",
|
|
1908
|
+
message: "Case name?",
|
|
1909
|
+
initial: deriveDefaultCaseName(draft.input) || "unnamed-case"
|
|
1910
|
+
});
|
|
1911
|
+
const expected_mode = await promptExpectedMode();
|
|
1912
|
+
const expected_output_custom = expected_mode === "custom" ? await promptCustomExpected() : void 0;
|
|
1913
|
+
const tool_sequence = await promptToolSequence(draft.tool_sequence);
|
|
1914
|
+
const scorers = await promptScorers(expected_mode, tool_sequence.length > 0);
|
|
1915
|
+
const { tagsRaw } = await prompt3({
|
|
1916
|
+
type: "input",
|
|
1917
|
+
name: "tagsRaw",
|
|
1918
|
+
message: "Tags (comma-separated, optional)?",
|
|
1919
|
+
initial: ""
|
|
1920
|
+
});
|
|
1921
|
+
return {
|
|
1922
|
+
name: name.trim() === "" ? deriveDefaultCaseName(draft.input) : name.trim(),
|
|
1923
|
+
expected_mode,
|
|
1924
|
+
...expected_output_custom !== void 0 ? { expected_output_custom } : {},
|
|
1925
|
+
tool_sequence,
|
|
1926
|
+
scorers,
|
|
1927
|
+
tags: parseTagInput(tagsRaw)
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
async function promptExpectedMode() {
|
|
1931
|
+
const { choice } = await prompt3({
|
|
1932
|
+
type: "select",
|
|
1933
|
+
name: "choice",
|
|
1934
|
+
message: "Expected output?",
|
|
1935
|
+
choices: [
|
|
1936
|
+
{ name: "actual", message: "Use the actual output from this run (exact match)" },
|
|
1937
|
+
{ name: "custom", message: "Enter a custom expected output" },
|
|
1938
|
+
{ name: "skip", message: "Skip \u2014 rely on tool-sequence / custom scorers" }
|
|
1939
|
+
]
|
|
1940
|
+
});
|
|
1941
|
+
if (choice === "actual" || choice === "custom" || choice === "skip") return choice;
|
|
1942
|
+
return "skip";
|
|
1943
|
+
}
|
|
1944
|
+
async function promptCustomExpected() {
|
|
1945
|
+
const { expected } = await prompt3({
|
|
1946
|
+
type: "input",
|
|
1947
|
+
name: "expected",
|
|
1948
|
+
message: "Custom expected output:"
|
|
1949
|
+
});
|
|
1950
|
+
return expected;
|
|
1951
|
+
}
|
|
1952
|
+
async function promptToolSequence(actual) {
|
|
1953
|
+
const initial = actual.join(", ");
|
|
1954
|
+
const { edited } = await prompt3({
|
|
1955
|
+
type: "input",
|
|
1956
|
+
name: "edited",
|
|
1957
|
+
message: "Expected tool sequence (comma-separated, empty to skip)?",
|
|
1958
|
+
initial
|
|
1959
|
+
});
|
|
1960
|
+
return parseToolSequenceInput(edited);
|
|
1961
|
+
}
|
|
1962
|
+
async function promptScorers(expectedMode, hasToolSequence) {
|
|
1963
|
+
const defaults = [];
|
|
1964
|
+
if (expectedMode !== "skip") defaults.push("exact");
|
|
1965
|
+
if (hasToolSequence) defaults.push("tool-sequence");
|
|
1966
|
+
const multiselectOpts = {
|
|
1967
|
+
type: "multiselect",
|
|
1968
|
+
name: "picked",
|
|
1969
|
+
message: "Scorers to attach?",
|
|
1970
|
+
choices: [
|
|
1971
|
+
{ name: "exact", message: "Exact output match" },
|
|
1972
|
+
{ name: "similarity", message: "Cosine similarity" },
|
|
1973
|
+
{ name: "tool-sequence", message: "Tool sequence match" },
|
|
1974
|
+
{ name: "custom", message: "Custom scorer module" }
|
|
1975
|
+
],
|
|
1976
|
+
initial: defaults
|
|
1977
|
+
};
|
|
1978
|
+
const { picked } = await prompt3(multiselectOpts);
|
|
1979
|
+
const refs = [];
|
|
1980
|
+
for (const kind of picked) {
|
|
1981
|
+
if (kind === "custom") {
|
|
1982
|
+
const { path } = await prompt3({
|
|
1983
|
+
type: "input",
|
|
1984
|
+
name: "path",
|
|
1985
|
+
message: "Custom scorer module path (relative to CWD)?"
|
|
1986
|
+
});
|
|
1987
|
+
refs.push({ type: "custom", path: path.trim() });
|
|
1988
|
+
} else if (kind === "exact" || kind === "similarity" || kind === "tool-sequence") {
|
|
1989
|
+
refs.push({ type: kind });
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return refs;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// src/commands/eval-list.ts
|
|
1996
|
+
import { JsonFileGoldenStore as JsonFileGoldenStore2 } from "@tuttiai/core";
|
|
1997
|
+
|
|
1998
|
+
// src/commands/eval-list-render.ts
|
|
1999
|
+
import chalk12 from "chalk";
|
|
2000
|
+
function visibleLen(s) {
|
|
2001
|
+
return s.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
2002
|
+
}
|
|
2003
|
+
function pad(s, len) {
|
|
2004
|
+
const v = visibleLen(s);
|
|
2005
|
+
return v >= len ? s : s + " ".repeat(len - v);
|
|
2006
|
+
}
|
|
2007
|
+
function truncate(text, max) {
|
|
2008
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
2009
|
+
return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
|
|
2010
|
+
}
|
|
2011
|
+
function formatIsoShort(d) {
|
|
2012
|
+
const iso = d.toISOString();
|
|
2013
|
+
return iso.slice(0, 10) + " " + iso.slice(11, 16);
|
|
2014
|
+
}
|
|
2015
|
+
function scorersCell(scorers) {
|
|
2016
|
+
if (scorers.length === 0) return chalk12.dim("(none)");
|
|
2017
|
+
return scorers.map((s) => s.type).join(",");
|
|
2018
|
+
}
|
|
2019
|
+
function renderStatus(status) {
|
|
2020
|
+
if (status === "pass") return chalk12.green("pass");
|
|
2021
|
+
if (status === "fail") return chalk12.red("FAIL");
|
|
2022
|
+
return chalk12.dim("never");
|
|
2023
|
+
}
|
|
2024
|
+
function deriveLastRunStatus(latest) {
|
|
2025
|
+
if (!latest) return "never";
|
|
2026
|
+
return latest.passed ? "pass" : "fail";
|
|
2027
|
+
}
|
|
2028
|
+
function renderGoldenCasesTable(cases, latestByCaseId) {
|
|
2029
|
+
if (cases.length === 0) {
|
|
2030
|
+
return chalk12.dim(
|
|
2031
|
+
"No golden cases recorded. Run `tutti-ai eval record <session-id>` to pin one."
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
const rows = cases.map((c) => ({
|
|
2035
|
+
id: c.id.slice(0, 8),
|
|
2036
|
+
name: truncate(c.name, 36),
|
|
2037
|
+
agent: truncate(c.agent_id, 16),
|
|
2038
|
+
scorers: truncate(scorersCell(c.scorers), 32),
|
|
2039
|
+
status: deriveLastRunStatus(latestByCaseId.get(c.id)),
|
|
2040
|
+
created: formatIsoShort(c.created_at)
|
|
2041
|
+
}));
|
|
2042
|
+
const widths = {
|
|
2043
|
+
id: 8,
|
|
2044
|
+
name: Math.max(4, ...rows.map((r) => visibleLen(r.name))),
|
|
2045
|
+
agent: Math.max(5, ...rows.map((r) => visibleLen(r.agent))),
|
|
2046
|
+
scorers: Math.max(7, ...rows.map((r) => visibleLen(r.scorers))),
|
|
2047
|
+
status: 6,
|
|
2048
|
+
created: 16
|
|
2049
|
+
};
|
|
2050
|
+
const header = chalk12.dim.bold(
|
|
2051
|
+
pad("ID", widths.id) + " " + pad("NAME", widths.name) + " " + pad("AGENT", widths.agent) + " " + pad("SCORERS", widths.scorers) + " " + pad("STATUS", widths.status) + " " + pad("CREATED", widths.created)
|
|
2052
|
+
);
|
|
2053
|
+
const body = rows.map(
|
|
2054
|
+
(r) => pad(chalk12.dim(r.id), widths.id) + " " + pad(r.name, widths.name) + " " + pad(chalk12.cyan(r.agent), widths.agent) + " " + pad(r.scorers, widths.scorers) + " " + pad(renderStatus(r.status), widths.status) + " " + pad(chalk12.dim(r.created), widths.created)
|
|
2055
|
+
).join("\n");
|
|
2056
|
+
return header + "\n" + body;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/commands/eval-list.ts
|
|
2060
|
+
async function evalListCommand() {
|
|
2061
|
+
const store = new JsonFileGoldenStore2();
|
|
2062
|
+
const cases = await store.listCases();
|
|
2063
|
+
const latest = await Promise.all(
|
|
2064
|
+
cases.map(async (c) => [c.id, await store.latestRun(c.id)])
|
|
2065
|
+
);
|
|
2066
|
+
const latestByCaseId = new Map(latest);
|
|
2067
|
+
console.log(renderGoldenCasesTable(cases, latestByCaseId));
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/commands/eval-run.ts
|
|
2071
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2072
|
+
import { dirname as dirname2, resolve as resolve11 } from "path";
|
|
2073
|
+
import chalk14 from "chalk";
|
|
2074
|
+
import ora7 from "ora";
|
|
2075
|
+
import {
|
|
2076
|
+
GoldenRunner,
|
|
2077
|
+
JsonFileGoldenStore as JsonFileGoldenStore3,
|
|
2078
|
+
ScoreLoader as ScoreLoader6,
|
|
2079
|
+
createLogger as createLogger11
|
|
2080
|
+
} from "@tuttiai/core";
|
|
2081
|
+
|
|
2082
|
+
// src/commands/eval-run-render.ts
|
|
2083
|
+
import chalk13 from "chalk";
|
|
2084
|
+
var DIFF_PREVIEW_LINES = 20;
|
|
2085
|
+
function filterCases(cases, filters) {
|
|
2086
|
+
return cases.filter((c) => {
|
|
2087
|
+
if (filters.case !== void 0 && !c.id.startsWith(filters.case)) return false;
|
|
2088
|
+
if (filters.tag !== void 0) {
|
|
2089
|
+
const tags = c.tags ?? [];
|
|
2090
|
+
if (!tags.includes(filters.tag)) return false;
|
|
2091
|
+
}
|
|
2092
|
+
return true;
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
function scoreSummary(scores) {
|
|
2096
|
+
const parts = Object.values(scores).map(
|
|
2097
|
+
(s) => s.scorer + ":" + s.score.toFixed(2)
|
|
2098
|
+
);
|
|
2099
|
+
return parts.length > 0 ? parts.join(", ") : "(no scorers)";
|
|
2100
|
+
}
|
|
2101
|
+
function firstFailure(scores) {
|
|
2102
|
+
return Object.values(scores).find((s) => !s.passed);
|
|
2103
|
+
}
|
|
2104
|
+
function truncateDiffPreview(diff, maxLines = DIFF_PREVIEW_LINES) {
|
|
2105
|
+
const lines = diff.split("\n");
|
|
2106
|
+
if (lines.length <= maxLines) return diff;
|
|
2107
|
+
const kept = lines.slice(0, maxLines);
|
|
2108
|
+
const dropped = lines.length - maxLines;
|
|
2109
|
+
return kept.join("\n") + "\n\u2026 (" + dropped + " more lines)";
|
|
2110
|
+
}
|
|
2111
|
+
function renderCaseLine(goldenCase, run2) {
|
|
2112
|
+
if (run2.passed) {
|
|
2113
|
+
return chalk13.green("\u2713 ") + chalk13.bold(goldenCase.name) + chalk13.dim(" \u2014 " + scoreSummary(run2.scores));
|
|
2114
|
+
}
|
|
2115
|
+
const failure = firstFailure(run2.scores);
|
|
2116
|
+
const why = failure ? failure.scorer + (failure.detail ? ": " + failure.detail : " failed") : "run failed";
|
|
2117
|
+
const head = chalk13.red("\u2717 ") + chalk13.bold(goldenCase.name) + chalk13.dim(" \u2014 " + why);
|
|
2118
|
+
if (run2.diff) {
|
|
2119
|
+
return head + "\n" + chalk13.dim(truncateDiffPreview(run2.diff));
|
|
2120
|
+
}
|
|
2121
|
+
return head;
|
|
2122
|
+
}
|
|
2123
|
+
function renderCaseLineCI(goldenCase, run2) {
|
|
2124
|
+
const status = run2.passed ? "PASS" : "FAIL";
|
|
2125
|
+
const id8 = goldenCase.id.slice(0, 8);
|
|
2126
|
+
const scoreStr = scoreSummary(run2.scores);
|
|
2127
|
+
const tokens = "tokens=" + run2.tokens;
|
|
2128
|
+
const cost = run2.cost_usd !== void 0 ? " cost=$" + run2.cost_usd.toFixed(4) : "";
|
|
2129
|
+
const why = !run2.passed && firstFailure(run2.scores) ? " why=" + (firstFailure(run2.scores)?.scorer ?? "") : "";
|
|
2130
|
+
return status + " " + id8 + " " + goldenCase.name + " [" + scoreStr + "] " + tokens + cost + why;
|
|
2131
|
+
}
|
|
2132
|
+
function computeStats(runs) {
|
|
2133
|
+
const passed = runs.filter((r) => r.passed).length;
|
|
2134
|
+
const totalTokens = runs.reduce((sum, r) => sum + r.tokens, 0);
|
|
2135
|
+
const totalCostUsd = runs.reduce((sum, r) => sum + (r.cost_usd ?? 0), 0);
|
|
2136
|
+
return {
|
|
2137
|
+
passed,
|
|
2138
|
+
failed: runs.length - passed,
|
|
2139
|
+
total: runs.length,
|
|
2140
|
+
totalTokens,
|
|
2141
|
+
totalCostUsd
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
function renderSummary(stats, colors) {
|
|
2145
|
+
const avgTokens = stats.total > 0 ? Math.round(stats.totalTokens / stats.total) : 0;
|
|
2146
|
+
const passStr = stats.passed + " passed";
|
|
2147
|
+
const failStr = stats.failed + " failed";
|
|
2148
|
+
const passOut = colors ? chalk13.green(passStr) : passStr;
|
|
2149
|
+
const failOut = colors && stats.failed > 0 ? chalk13.red(failStr) : failStr;
|
|
2150
|
+
return passOut + ", " + failOut + " out of " + stats.total + " cases | avg tokens: " + avgTokens + " | total cost: $" + stats.totalCostUsd.toFixed(2);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/commands/eval-run-junit.ts
|
|
2154
|
+
function escapeXmlText(s) {
|
|
2155
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2156
|
+
}
|
|
2157
|
+
function escapeXmlAttr(s) {
|
|
2158
|
+
return escapeXmlText(s).replace(/"/g, """).replace(/\n/g, " ");
|
|
2159
|
+
}
|
|
2160
|
+
function firstFailure2(scores) {
|
|
2161
|
+
return Object.values(scores).find((s) => !s.passed);
|
|
2162
|
+
}
|
|
2163
|
+
function toJunitXml(rows, suiteName = "tutti-eval") {
|
|
2164
|
+
const totalTimeSec = rows.reduce((s, r) => s + r.durationMs / 1e3, 0);
|
|
2165
|
+
const failures = rows.filter((r) => !r.run.passed).length;
|
|
2166
|
+
const attrs = 'name="' + escapeXmlAttr(suiteName) + '" tests="' + rows.length + '" failures="' + failures + '" errors="0" time="' + totalTimeSec.toFixed(3) + '"';
|
|
2167
|
+
const body = rows.map(renderTestCase).join("");
|
|
2168
|
+
return '<?xml version="1.0" encoding="UTF-8"?>\n<testsuites ' + attrs + ">\n <testsuite " + attrs + ">\n" + body + " </testsuite>\n</testsuites>\n";
|
|
2169
|
+
}
|
|
2170
|
+
function renderTestCase({ goldenCase, run: run2, durationMs }) {
|
|
2171
|
+
const timeSec = (durationMs / 1e3).toFixed(3);
|
|
2172
|
+
const caseAttrs = ' classname="' + escapeXmlAttr(goldenCase.agent_id) + '" name="' + escapeXmlAttr(goldenCase.name) + '" time="' + timeSec + '"';
|
|
2173
|
+
if (run2.passed) {
|
|
2174
|
+
return " <testcase" + caseAttrs + "/>\n";
|
|
2175
|
+
}
|
|
2176
|
+
const failure = firstFailure2(run2.scores);
|
|
2177
|
+
const message = failure ? failure.scorer + (failure.detail ? ": " + failure.detail : " failed") : "case failed";
|
|
2178
|
+
const parts = [];
|
|
2179
|
+
if (failure?.detail) parts.push(failure.detail);
|
|
2180
|
+
if (run2.diff) parts.push(run2.diff);
|
|
2181
|
+
parts.push("---- output ----\n" + run2.output);
|
|
2182
|
+
const bodyRaw = parts.join("\n\n").replace(/]]>/g, "]]]]><![CDATA[>");
|
|
2183
|
+
return " <testcase" + caseAttrs + '>\n <failure message="' + escapeXmlAttr(message) + '" type="ScorerFailed"><![CDATA[' + bodyRaw + "]]></failure>\n </testcase>\n";
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/commands/eval-run.ts
|
|
2187
|
+
var logger11 = createLogger11("tutti-cli");
|
|
2188
|
+
var DEFAULT_JUNIT_PATH = ".tutti/eval-results.xml";
|
|
2189
|
+
async function runEvalRun(opts, deps = {}) {
|
|
2190
|
+
const store = deps.store ?? new JsonFileGoldenStore3();
|
|
2191
|
+
const all = await store.listCases();
|
|
2192
|
+
const cases = filterCases(all, {
|
|
2193
|
+
...opts.case !== void 0 ? { case: opts.case } : {},
|
|
2194
|
+
...opts.tag !== void 0 ? { tag: opts.tag } : {}
|
|
2195
|
+
});
|
|
2196
|
+
if (cases.length === 0) {
|
|
2197
|
+
console.log(
|
|
2198
|
+
chalk14.dim(
|
|
2199
|
+
"No golden cases match the filter. Run `tutti-ai eval record <session-id>` first."
|
|
2200
|
+
)
|
|
2201
|
+
);
|
|
2202
|
+
return { passed: 0, failed: 0, total: 0, totalTokens: 0, totalCostUsd: 0 };
|
|
2203
|
+
}
|
|
2204
|
+
const score = deps.score ?? await loadScore(opts.score);
|
|
2205
|
+
const runner = new GoldenRunner({ score, store });
|
|
2206
|
+
const rows = [];
|
|
2207
|
+
for (const c of cases) {
|
|
2208
|
+
const start = Date.now();
|
|
2209
|
+
const run2 = await runner.runGoldenCase(c);
|
|
2210
|
+
const durationMs = Date.now() - start;
|
|
2211
|
+
rows.push({ goldenCase: c, run: run2, durationMs });
|
|
2212
|
+
printCaseResult(c, run2, opts.ci === true);
|
|
2213
|
+
}
|
|
2214
|
+
const runs = rows.map((r) => r.run);
|
|
2215
|
+
const stats = computeStats(runs);
|
|
2216
|
+
console.log(renderSummary(stats, !opts.ci));
|
|
2217
|
+
let xmlPath;
|
|
2218
|
+
if (opts.ci) {
|
|
2219
|
+
xmlPath = resolve11(deps.junitPath ?? DEFAULT_JUNIT_PATH);
|
|
2220
|
+
await writeJunitFile(xmlPath, rows);
|
|
2221
|
+
}
|
|
2222
|
+
return {
|
|
2223
|
+
passed: stats.passed,
|
|
2224
|
+
failed: stats.failed,
|
|
2225
|
+
total: stats.total,
|
|
2226
|
+
totalTokens: stats.totalTokens,
|
|
2227
|
+
totalCostUsd: stats.totalCostUsd,
|
|
2228
|
+
...xmlPath !== void 0 ? { xmlPath } : {}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
async function evalRunCommand(opts) {
|
|
2232
|
+
const result = await runEvalRun(opts);
|
|
2233
|
+
if (opts.ci && result.failed > 0) {
|
|
2234
|
+
process.exit(1);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
async function loadScore(scorePath) {
|
|
2238
|
+
const path = resolve11(scorePath ?? "./tutti.score.ts");
|
|
2239
|
+
const spinner = ora7({ color: "cyan" }).start("Loading score...");
|
|
2240
|
+
try {
|
|
2241
|
+
const score = await ScoreLoader6.load(path);
|
|
2242
|
+
spinner.stop();
|
|
2243
|
+
return score;
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
spinner.fail("Failed to load score");
|
|
2246
|
+
logger11.error(
|
|
2247
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
2248
|
+
"Score load error"
|
|
2249
|
+
);
|
|
2250
|
+
process.exit(1);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function printCaseResult(goldenCase, run2, ci) {
|
|
2254
|
+
if (ci) {
|
|
2255
|
+
console.log(renderCaseLineCI(goldenCase, run2));
|
|
2256
|
+
} else {
|
|
2257
|
+
console.log(renderCaseLine(goldenCase, run2));
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
async function writeJunitFile(xmlPath, rows) {
|
|
2261
|
+
const xml = toJunitXml(rows);
|
|
2262
|
+
await mkdir(dirname2(xmlPath), { recursive: true });
|
|
2263
|
+
await writeFile(xmlPath, xml, "utf8");
|
|
2264
|
+
}
|
|
2265
|
+
|
|
1732
2266
|
// src/commands/serve.ts
|
|
1733
2267
|
import { existsSync as existsSync10 } from "fs";
|
|
1734
|
-
import { resolve as
|
|
1735
|
-
import
|
|
2268
|
+
import { resolve as resolve12 } from "path";
|
|
2269
|
+
import chalk15 from "chalk";
|
|
1736
2270
|
import {
|
|
1737
2271
|
TuttiRuntime as TuttiRuntime4,
|
|
1738
|
-
ScoreLoader as
|
|
2272
|
+
ScoreLoader as ScoreLoader7,
|
|
1739
2273
|
AnthropicProvider as AnthropicProvider4,
|
|
1740
2274
|
OpenAIProvider as OpenAIProvider4,
|
|
1741
2275
|
GeminiProvider as GeminiProvider4,
|
|
1742
|
-
SecretsManager as
|
|
2276
|
+
SecretsManager as SecretsManager6,
|
|
1743
2277
|
InMemorySessionStore as InMemorySessionStore2,
|
|
1744
|
-
createLogger as
|
|
2278
|
+
createLogger as createLogger12
|
|
1745
2279
|
} from "@tuttiai/core";
|
|
1746
2280
|
import { createServer, DEFAULT_PORT, SERVER_VERSION } from "@tuttiai/server";
|
|
1747
|
-
var
|
|
2281
|
+
var logger12 = createLogger12("tutti-serve");
|
|
1748
2282
|
async function serveCommand(scorePath, options = {}) {
|
|
1749
|
-
const file =
|
|
2283
|
+
const file = resolve12(scorePath ?? "./tutti.score.ts");
|
|
1750
2284
|
if (!existsSync10(file)) {
|
|
1751
|
-
|
|
1752
|
-
console.error(
|
|
2285
|
+
logger12.error({ file }, "Score file not found");
|
|
2286
|
+
console.error(chalk15.dim('Run "tutti-ai init" to create a new project.'));
|
|
1753
2287
|
process.exit(1);
|
|
1754
2288
|
}
|
|
1755
2289
|
let score;
|
|
1756
2290
|
try {
|
|
1757
|
-
score = await
|
|
2291
|
+
score = await ScoreLoader7.load(file);
|
|
1758
2292
|
} catch (err) {
|
|
1759
|
-
|
|
2293
|
+
logger12.error(
|
|
1760
2294
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
1761
2295
|
"Failed to load score"
|
|
1762
2296
|
);
|
|
@@ -1769,9 +2303,9 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1769
2303
|
];
|
|
1770
2304
|
for (const [ProviderClass, envVar] of providerKeyMap) {
|
|
1771
2305
|
if (score.provider instanceof ProviderClass) {
|
|
1772
|
-
const key =
|
|
2306
|
+
const key = SecretsManager6.optional(envVar);
|
|
1773
2307
|
if (!key) {
|
|
1774
|
-
|
|
2308
|
+
logger12.error({ envVar }, "Missing API key");
|
|
1775
2309
|
process.exit(1);
|
|
1776
2310
|
}
|
|
1777
2311
|
}
|
|
@@ -1779,7 +2313,7 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1779
2313
|
const agentNames = Object.keys(score.agents);
|
|
1780
2314
|
const agentName = options.agent ?? (typeof score.entry === "string" ? score.entry : void 0) ?? agentNames[0];
|
|
1781
2315
|
if (!agentName || !Object.hasOwn(score.agents, agentName)) {
|
|
1782
|
-
|
|
2316
|
+
logger12.error(
|
|
1783
2317
|
{ requested: agentName, available: agentNames },
|
|
1784
2318
|
"Agent not found in score"
|
|
1785
2319
|
);
|
|
@@ -1794,7 +2328,7 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1794
2328
|
if (options.watch) {
|
|
1795
2329
|
reactive = new ReactiveScore(score, file);
|
|
1796
2330
|
reactive.on("file-change", () => {
|
|
1797
|
-
console.log(
|
|
2331
|
+
console.log(chalk15.cyan("\n[tutti] Score changed, reloading..."));
|
|
1798
2332
|
});
|
|
1799
2333
|
reactive.on("reloaded", () => {
|
|
1800
2334
|
void (async () => {
|
|
@@ -1807,9 +2341,9 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1807
2341
|
runtime = nextRuntime;
|
|
1808
2342
|
app = nextApp;
|
|
1809
2343
|
await app.listen({ port, host });
|
|
1810
|
-
console.log(
|
|
2344
|
+
console.log(chalk15.green("[tutti] Score reloaded. Server restarted."));
|
|
1811
2345
|
} catch (err) {
|
|
1812
|
-
|
|
2346
|
+
logger12.error(
|
|
1813
2347
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
1814
2348
|
"[tutti] Reload failed \u2014 server continues with previous config"
|
|
1815
2349
|
);
|
|
@@ -1817,7 +2351,7 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1817
2351
|
})();
|
|
1818
2352
|
});
|
|
1819
2353
|
reactive.on("reload-failed", (err) => {
|
|
1820
|
-
|
|
2354
|
+
logger12.error(
|
|
1821
2355
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
1822
2356
|
"[tutti] Reload failed \u2014 server continues with previous config"
|
|
1823
2357
|
);
|
|
@@ -1826,7 +2360,7 @@ async function serveCommand(scorePath, options = {}) {
|
|
|
1826
2360
|
await app.listen({ port, host });
|
|
1827
2361
|
printBanner(port, host, agentName, score, file, options.watch);
|
|
1828
2362
|
const shutdown = async (signal) => {
|
|
1829
|
-
console.log(
|
|
2363
|
+
console.log(chalk15.dim("\n" + signal + " received \u2014 shutting down..."));
|
|
1830
2364
|
if (reactive) await reactive.close();
|
|
1831
2365
|
await app.close();
|
|
1832
2366
|
process.exit(0);
|
|
@@ -1838,7 +2372,7 @@ function parsePort(raw) {
|
|
|
1838
2372
|
if (raw === void 0) return DEFAULT_PORT;
|
|
1839
2373
|
const n = Number.parseInt(raw, 10);
|
|
1840
2374
|
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
1841
|
-
|
|
2375
|
+
logger12.error({ port: raw }, "Invalid port number");
|
|
1842
2376
|
process.exit(1);
|
|
1843
2377
|
}
|
|
1844
2378
|
return n;
|
|
@@ -1862,56 +2396,56 @@ function printBanner(port, host, agentName, score, file, watch) {
|
|
|
1862
2396
|
const display = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
1863
2397
|
const url = "http://" + display + ":" + port;
|
|
1864
2398
|
console.log();
|
|
1865
|
-
console.log(
|
|
1866
|
-
console.log(
|
|
2399
|
+
console.log(chalk15.bold(" Tutti Server v" + SERVER_VERSION));
|
|
2400
|
+
console.log(chalk15.dim(" " + url));
|
|
1867
2401
|
console.log();
|
|
1868
|
-
console.log(
|
|
1869
|
-
console.log(
|
|
1870
|
-
console.log(
|
|
2402
|
+
console.log(chalk15.dim(" Score: ") + (score.name ?? file));
|
|
2403
|
+
console.log(chalk15.dim(" Agent: ") + agentName);
|
|
2404
|
+
console.log(chalk15.dim(" Agents: ") + Object.keys(score.agents).join(", "));
|
|
1871
2405
|
if (watch) {
|
|
1872
|
-
console.log(
|
|
2406
|
+
console.log(chalk15.dim(" Watch: ") + chalk15.cyan("enabled"));
|
|
1873
2407
|
}
|
|
1874
2408
|
console.log();
|
|
1875
|
-
console.log(
|
|
1876
|
-
console.log(
|
|
1877
|
-
console.log(
|
|
1878
|
-
console.log(
|
|
1879
|
-
console.log(
|
|
2409
|
+
console.log(chalk15.dim(" Endpoints:"));
|
|
2410
|
+
console.log(chalk15.dim(" POST ") + url + "/run");
|
|
2411
|
+
console.log(chalk15.dim(" POST ") + url + "/run/stream");
|
|
2412
|
+
console.log(chalk15.dim(" GET ") + url + "/sessions/:id");
|
|
2413
|
+
console.log(chalk15.dim(" GET ") + url + "/health");
|
|
1880
2414
|
console.log();
|
|
1881
2415
|
}
|
|
1882
2416
|
|
|
1883
2417
|
// src/commands/schedule.ts
|
|
1884
2418
|
import { existsSync as existsSync11 } from "fs";
|
|
1885
|
-
import { resolve as
|
|
1886
|
-
import
|
|
2419
|
+
import { resolve as resolve13 } from "path";
|
|
2420
|
+
import chalk16 from "chalk";
|
|
1887
2421
|
import {
|
|
1888
|
-
ScoreLoader as
|
|
2422
|
+
ScoreLoader as ScoreLoader8,
|
|
1889
2423
|
SchedulerEngine,
|
|
1890
2424
|
PostgresScheduleStore,
|
|
1891
2425
|
MemoryScheduleStore,
|
|
1892
2426
|
AgentRunner,
|
|
1893
2427
|
EventBus,
|
|
1894
2428
|
InMemorySessionStore as InMemorySessionStore3,
|
|
1895
|
-
SecretsManager as
|
|
1896
|
-
createLogger as
|
|
2429
|
+
SecretsManager as SecretsManager7,
|
|
2430
|
+
createLogger as createLogger13
|
|
1897
2431
|
} from "@tuttiai/core";
|
|
1898
|
-
var
|
|
2432
|
+
var logger13 = createLogger13("tutti-cli");
|
|
1899
2433
|
function resolveStore() {
|
|
1900
|
-
const pgUrl =
|
|
2434
|
+
const pgUrl = SecretsManager7.optional("TUTTI_PG_URL");
|
|
1901
2435
|
if (pgUrl) {
|
|
1902
2436
|
return new PostgresScheduleStore({ connection_string: pgUrl });
|
|
1903
2437
|
}
|
|
1904
|
-
|
|
2438
|
+
logger13.warn("TUTTI_PG_URL not set \u2014 using in-memory store (not durable across restarts)");
|
|
1905
2439
|
return new MemoryScheduleStore();
|
|
1906
2440
|
}
|
|
1907
2441
|
async function scheduleCommand(scorePath) {
|
|
1908
|
-
const file =
|
|
2442
|
+
const file = resolve13(scorePath ?? "./tutti.score.ts");
|
|
1909
2443
|
if (!existsSync11(file)) {
|
|
1910
|
-
console.error(
|
|
1911
|
-
console.error(
|
|
2444
|
+
console.error(chalk16.red("Score file not found: " + file));
|
|
2445
|
+
console.error(chalk16.dim('Run "tutti-ai init" to create a new project.'));
|
|
1912
2446
|
process.exit(1);
|
|
1913
2447
|
}
|
|
1914
|
-
const score = await
|
|
2448
|
+
const score = await ScoreLoader8.load(file);
|
|
1915
2449
|
const events = new EventBus();
|
|
1916
2450
|
const sessions = new InMemorySessionStore3();
|
|
1917
2451
|
const runner = new AgentRunner(
|
|
@@ -1929,41 +2463,41 @@ async function scheduleCommand(scorePath) {
|
|
|
1929
2463
|
registered++;
|
|
1930
2464
|
}
|
|
1931
2465
|
if (registered === 0) {
|
|
1932
|
-
console.log(
|
|
1933
|
-
console.log(
|
|
2466
|
+
console.log(chalk16.yellow("No agents have a schedule config. Nothing to run."));
|
|
2467
|
+
console.log(chalk16.dim("Add schedule: { cron: '...', input: '...' } to an agent in your score."));
|
|
1934
2468
|
process.exit(0);
|
|
1935
2469
|
}
|
|
1936
2470
|
events.onAny((e) => {
|
|
1937
2471
|
if (e.type === "schedule:triggered") {
|
|
1938
2472
|
const ev = e;
|
|
1939
2473
|
console.log(
|
|
1940
|
-
|
|
2474
|
+
chalk16.dim((/* @__PURE__ */ new Date()).toISOString()) + " " + chalk16.cyan("triggered") + " " + chalk16.bold(ev.schedule_id) + " \u2192 " + ev.agent_name
|
|
1941
2475
|
);
|
|
1942
2476
|
}
|
|
1943
2477
|
if (e.type === "schedule:completed") {
|
|
1944
2478
|
const ev = e;
|
|
1945
2479
|
console.log(
|
|
1946
|
-
|
|
2480
|
+
chalk16.dim((/* @__PURE__ */ new Date()).toISOString()) + " " + chalk16.green("completed") + " " + chalk16.bold(ev.schedule_id) + " " + chalk16.dim("(" + ev.duration_ms + "ms)")
|
|
1947
2481
|
);
|
|
1948
2482
|
}
|
|
1949
2483
|
if (e.type === "schedule:error") {
|
|
1950
2484
|
const ev = e;
|
|
1951
2485
|
console.log(
|
|
1952
|
-
|
|
2486
|
+
chalk16.dim((/* @__PURE__ */ new Date()).toISOString()) + " " + chalk16.red("error") + " " + chalk16.bold(ev.schedule_id) + " \u2014 " + ev.error.message
|
|
1953
2487
|
);
|
|
1954
2488
|
}
|
|
1955
2489
|
});
|
|
1956
2490
|
engine.start();
|
|
1957
2491
|
console.log("");
|
|
1958
|
-
console.log(
|
|
1959
|
-
console.log(
|
|
1960
|
-
console.log(
|
|
1961
|
-
console.log(
|
|
2492
|
+
console.log(chalk16.cyan.bold(" Tutti Scheduler"));
|
|
2493
|
+
console.log(chalk16.dim(" Score: " + (score.name ?? file)));
|
|
2494
|
+
console.log(chalk16.dim(" Schedules: " + registered));
|
|
2495
|
+
console.log(chalk16.dim(" Store: " + (SecretsManager7.optional("TUTTI_PG_URL") ? "postgres" : "memory")));
|
|
1962
2496
|
console.log("");
|
|
1963
|
-
console.log(
|
|
2497
|
+
console.log(chalk16.dim(" Press Ctrl+C to stop."));
|
|
1964
2498
|
console.log("");
|
|
1965
2499
|
const shutdown = () => {
|
|
1966
|
-
console.log(
|
|
2500
|
+
console.log(chalk16.dim("\n Shutting down scheduler..."));
|
|
1967
2501
|
engine.stop();
|
|
1968
2502
|
if ("close" in store && typeof store.close === "function") {
|
|
1969
2503
|
void store.close();
|
|
@@ -1978,9 +2512,9 @@ async function scheduleCommand(scorePath) {
|
|
|
1978
2512
|
// src/commands/update.ts
|
|
1979
2513
|
import { execSync as execSync3 } from "child_process";
|
|
1980
2514
|
import { existsSync as existsSync12, readFileSync as readFileSync5 } from "fs";
|
|
1981
|
-
import { resolve as
|
|
1982
|
-
import
|
|
1983
|
-
import
|
|
2515
|
+
import { resolve as resolve14 } from "path";
|
|
2516
|
+
import chalk17 from "chalk";
|
|
2517
|
+
import ora8 from "ora";
|
|
1984
2518
|
var TUTTI_PACKAGES = [
|
|
1985
2519
|
"@tuttiai/core",
|
|
1986
2520
|
"@tuttiai/cli",
|
|
@@ -2023,8 +2557,8 @@ function getLatestVersion(pkg) {
|
|
|
2023
2557
|
}
|
|
2024
2558
|
function detectPackageManager() {
|
|
2025
2559
|
const cwd = process.cwd();
|
|
2026
|
-
if (existsSync12(
|
|
2027
|
-
if (existsSync12(
|
|
2560
|
+
if (existsSync12(resolve14(cwd, "yarn.lock"))) return "yarn";
|
|
2561
|
+
if (existsSync12(resolve14(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2028
2562
|
return "npm";
|
|
2029
2563
|
}
|
|
2030
2564
|
function isGlobalInstall() {
|
|
@@ -2040,35 +2574,35 @@ function isGlobalInstall() {
|
|
|
2040
2574
|
}
|
|
2041
2575
|
function updateCommand() {
|
|
2042
2576
|
console.log();
|
|
2043
|
-
console.log(
|
|
2577
|
+
console.log(chalk17.cyan.bold(" Tutti Update"));
|
|
2044
2578
|
console.log();
|
|
2045
|
-
const spinner =
|
|
2579
|
+
const spinner = ora8("Checking for updates...").start();
|
|
2046
2580
|
const cliCurrent = getInstalledVersion("@tuttiai/cli") ?? "unknown";
|
|
2047
2581
|
const cliLatest = getLatestVersion("@tuttiai/cli");
|
|
2048
2582
|
spinner.stop();
|
|
2049
2583
|
if (cliLatest && cliCurrent !== cliLatest) {
|
|
2050
2584
|
console.log(
|
|
2051
|
-
|
|
2585
|
+
chalk17.yellow(" CLI update available: ") + chalk17.dim(cliCurrent) + " \u2192 " + chalk17.green(cliLatest)
|
|
2052
2586
|
);
|
|
2053
2587
|
if (isGlobalInstall()) {
|
|
2054
|
-
const updateSpinner2 =
|
|
2588
|
+
const updateSpinner2 = ora8("Updating global CLI...").start();
|
|
2055
2589
|
try {
|
|
2056
2590
|
execSync3("npm install -g tutti-ai@latest", { stdio: "pipe" });
|
|
2057
2591
|
updateSpinner2.succeed("CLI updated to " + cliLatest);
|
|
2058
2592
|
} catch {
|
|
2059
2593
|
updateSpinner2.fail("Failed to update global CLI");
|
|
2060
|
-
console.log(
|
|
2594
|
+
console.log(chalk17.dim(" Run manually: npm install -g tutti-ai@latest"));
|
|
2061
2595
|
}
|
|
2062
2596
|
} else {
|
|
2063
|
-
console.log(
|
|
2597
|
+
console.log(chalk17.dim(" Global: npm install -g tutti-ai@latest"));
|
|
2064
2598
|
}
|
|
2065
2599
|
} else {
|
|
2066
|
-
console.log(
|
|
2600
|
+
console.log(chalk17.green(" CLI is up to date") + chalk17.dim(" (" + cliCurrent + ")"));
|
|
2067
2601
|
}
|
|
2068
|
-
const pkgPath =
|
|
2602
|
+
const pkgPath = resolve14(process.cwd(), "package.json");
|
|
2069
2603
|
if (!existsSync12(pkgPath)) {
|
|
2070
2604
|
console.log();
|
|
2071
|
-
console.log(
|
|
2605
|
+
console.log(chalk17.dim(" No package.json found \u2014 skipping project dependency check."));
|
|
2072
2606
|
console.log();
|
|
2073
2607
|
return;
|
|
2074
2608
|
}
|
|
@@ -2076,7 +2610,7 @@ function updateCommand() {
|
|
|
2076
2610
|
try {
|
|
2077
2611
|
pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
2078
2612
|
} catch {
|
|
2079
|
-
console.log(
|
|
2613
|
+
console.log(chalk17.dim(" Could not read package.json"));
|
|
2080
2614
|
return;
|
|
2081
2615
|
}
|
|
2082
2616
|
const allDeps = /* @__PURE__ */ new Map();
|
|
@@ -2093,46 +2627,46 @@ function updateCommand() {
|
|
|
2093
2627
|
const installed = TUTTI_PACKAGES.filter((p) => allDeps.has(p));
|
|
2094
2628
|
if (installed.length === 0) {
|
|
2095
2629
|
console.log();
|
|
2096
|
-
console.log(
|
|
2630
|
+
console.log(chalk17.dim(" No @tuttiai packages found in this project."));
|
|
2097
2631
|
console.log();
|
|
2098
2632
|
return;
|
|
2099
2633
|
}
|
|
2100
2634
|
console.log();
|
|
2101
|
-
console.log(" " +
|
|
2635
|
+
console.log(" " + chalk17.bold("Project packages:"));
|
|
2102
2636
|
const toUpdate = [];
|
|
2103
2637
|
for (const name of installed) {
|
|
2104
2638
|
const current = allDeps.get(name) ?? "?";
|
|
2105
2639
|
const latest = getLatestVersion(name);
|
|
2106
2640
|
if (!latest) {
|
|
2107
|
-
console.log(" " +
|
|
2641
|
+
console.log(" " + chalk17.dim(name) + " " + current + chalk17.dim(" (could not check)"));
|
|
2108
2642
|
continue;
|
|
2109
2643
|
}
|
|
2110
2644
|
const cleanCurrent = current.replace(/^[\^~]/, "");
|
|
2111
2645
|
if (cleanCurrent === latest) {
|
|
2112
|
-
console.log(" " +
|
|
2646
|
+
console.log(" " + chalk17.green("\u2714") + " " + name + " " + chalk17.dim(latest));
|
|
2113
2647
|
} else {
|
|
2114
2648
|
console.log(
|
|
2115
|
-
" " +
|
|
2649
|
+
" " + chalk17.yellow("\u2191") + " " + name + " " + chalk17.dim(cleanCurrent) + " \u2192 " + chalk17.green(latest)
|
|
2116
2650
|
);
|
|
2117
2651
|
toUpdate.push(name + "@latest");
|
|
2118
2652
|
}
|
|
2119
2653
|
}
|
|
2120
2654
|
if (toUpdate.length === 0) {
|
|
2121
2655
|
console.log();
|
|
2122
|
-
console.log(
|
|
2656
|
+
console.log(chalk17.green(" All packages are up to date."));
|
|
2123
2657
|
console.log();
|
|
2124
2658
|
return;
|
|
2125
2659
|
}
|
|
2126
2660
|
console.log();
|
|
2127
2661
|
const pm = detectPackageManager();
|
|
2128
2662
|
const installCmd = pm === "yarn" ? "yarn add " + toUpdate.join(" ") : pm === "pnpm" ? "pnpm add " + toUpdate.join(" ") : "npm install " + toUpdate.join(" ");
|
|
2129
|
-
const updateSpinner =
|
|
2663
|
+
const updateSpinner = ora8("Updating " + toUpdate.length + " package(s)...").start();
|
|
2130
2664
|
try {
|
|
2131
2665
|
execSync3(installCmd, { cwd: process.cwd(), stdio: "pipe" });
|
|
2132
2666
|
updateSpinner.succeed("Updated " + toUpdate.length + " package(s)");
|
|
2133
2667
|
} catch {
|
|
2134
2668
|
updateSpinner.fail("Update failed");
|
|
2135
|
-
console.log(
|
|
2669
|
+
console.log(chalk17.dim(" Run manually: " + installCmd));
|
|
2136
2670
|
}
|
|
2137
2671
|
console.log();
|
|
2138
2672
|
}
|
|
@@ -2140,9 +2674,9 @@ function updateCommand() {
|
|
|
2140
2674
|
// src/commands/outdated.ts
|
|
2141
2675
|
import { execSync as execSync4 } from "child_process";
|
|
2142
2676
|
import { existsSync as existsSync13, readFileSync as readFileSync6 } from "fs";
|
|
2143
|
-
import { resolve as
|
|
2144
|
-
import
|
|
2145
|
-
import
|
|
2677
|
+
import { resolve as resolve15 } from "path";
|
|
2678
|
+
import chalk18 from "chalk";
|
|
2679
|
+
import ora9 from "ora";
|
|
2146
2680
|
function getLatestVersion2(pkg) {
|
|
2147
2681
|
try {
|
|
2148
2682
|
return execSync4(`npm view ${pkg} version 2>/dev/null`, {
|
|
@@ -2153,21 +2687,21 @@ function getLatestVersion2(pkg) {
|
|
|
2153
2687
|
return null;
|
|
2154
2688
|
}
|
|
2155
2689
|
}
|
|
2156
|
-
function
|
|
2690
|
+
function pad2(s, len) {
|
|
2157
2691
|
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
2158
2692
|
}
|
|
2159
2693
|
function outdatedCommand() {
|
|
2160
|
-
const pkgPath =
|
|
2694
|
+
const pkgPath = resolve15(process.cwd(), "package.json");
|
|
2161
2695
|
if (!existsSync13(pkgPath)) {
|
|
2162
|
-
console.error(
|
|
2163
|
-
console.error(
|
|
2696
|
+
console.error(chalk18.red("No package.json found in the current directory."));
|
|
2697
|
+
console.error(chalk18.dim('Run "tutti-ai init" to create a new project.'));
|
|
2164
2698
|
process.exit(1);
|
|
2165
2699
|
}
|
|
2166
2700
|
let pkg;
|
|
2167
2701
|
try {
|
|
2168
2702
|
pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
2169
2703
|
} catch {
|
|
2170
|
-
console.error(
|
|
2704
|
+
console.error(chalk18.red("Could not parse package.json"));
|
|
2171
2705
|
process.exit(1);
|
|
2172
2706
|
}
|
|
2173
2707
|
const allDeps = /* @__PURE__ */ new Map();
|
|
@@ -2183,10 +2717,10 @@ function outdatedCommand() {
|
|
|
2183
2717
|
}
|
|
2184
2718
|
const tuttiDeps = [...allDeps.entries()].filter(([name]) => name.startsWith("@tuttiai/"));
|
|
2185
2719
|
if (tuttiDeps.length === 0) {
|
|
2186
|
-
console.log(
|
|
2720
|
+
console.log(chalk18.dim("No @tuttiai packages found in this project."));
|
|
2187
2721
|
return;
|
|
2188
2722
|
}
|
|
2189
|
-
const spinner =
|
|
2723
|
+
const spinner = ora9("Checking npm registry...").start();
|
|
2190
2724
|
const results = [];
|
|
2191
2725
|
for (const [name, version] of tuttiDeps) {
|
|
2192
2726
|
const latest = getLatestVersion2(name);
|
|
@@ -2201,41 +2735,41 @@ function outdatedCommand() {
|
|
|
2201
2735
|
spinner.stop();
|
|
2202
2736
|
console.log();
|
|
2203
2737
|
console.log(
|
|
2204
|
-
|
|
2205
|
-
" " +
|
|
2738
|
+
chalk18.dim(
|
|
2739
|
+
" " + pad2("PACKAGE", 28) + pad2("CURRENT", 12) + pad2("LATEST", 12) + "STATUS"
|
|
2206
2740
|
)
|
|
2207
2741
|
);
|
|
2208
|
-
console.log(
|
|
2742
|
+
console.log(chalk18.dim(" " + "\u2500".repeat(64)));
|
|
2209
2743
|
let outdatedCount = 0;
|
|
2210
2744
|
for (const r of results) {
|
|
2211
|
-
const status = r.outdated ?
|
|
2745
|
+
const status = r.outdated ? chalk18.yellow("update available") : chalk18.green("up to date");
|
|
2212
2746
|
if (r.outdated) outdatedCount++;
|
|
2213
2747
|
console.log(
|
|
2214
|
-
" " +
|
|
2748
|
+
" " + pad2(r.name, 28) + pad2(r.current, 12) + pad2(r.latest, 12) + status
|
|
2215
2749
|
);
|
|
2216
2750
|
}
|
|
2217
2751
|
console.log();
|
|
2218
2752
|
if (outdatedCount > 0) {
|
|
2219
2753
|
console.log(
|
|
2220
|
-
|
|
2754
|
+
chalk18.yellow(" " + outdatedCount + " package(s) can be updated.") + chalk18.dim(" Run: tutti-ai update")
|
|
2221
2755
|
);
|
|
2222
2756
|
} else {
|
|
2223
|
-
console.log(
|
|
2757
|
+
console.log(chalk18.green(" All packages are up to date."));
|
|
2224
2758
|
}
|
|
2225
2759
|
console.log();
|
|
2226
2760
|
}
|
|
2227
2761
|
|
|
2228
2762
|
// src/commands/info.ts
|
|
2229
2763
|
import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
|
|
2230
|
-
import { resolve as
|
|
2231
|
-
import
|
|
2232
|
-
import { ScoreLoader as
|
|
2233
|
-
var
|
|
2234
|
-
function
|
|
2764
|
+
import { resolve as resolve16 } from "path";
|
|
2765
|
+
import chalk19 from "chalk";
|
|
2766
|
+
import { ScoreLoader as ScoreLoader9, createLogger as createLogger14 } from "@tuttiai/core";
|
|
2767
|
+
var logger14 = createLogger14("tutti-cli");
|
|
2768
|
+
function pad3(s, len) {
|
|
2235
2769
|
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
2236
2770
|
}
|
|
2237
2771
|
async function infoCommand(scorePath) {
|
|
2238
|
-
const pkgPath =
|
|
2772
|
+
const pkgPath = resolve16(process.cwd(), "package.json");
|
|
2239
2773
|
let projectName = "(unknown)";
|
|
2240
2774
|
let projectVersion = "(unknown)";
|
|
2241
2775
|
const installedDeps = /* @__PURE__ */ new Map();
|
|
@@ -2258,41 +2792,41 @@ async function infoCommand(scorePath) {
|
|
|
2258
2792
|
}
|
|
2259
2793
|
}
|
|
2260
2794
|
console.log();
|
|
2261
|
-
console.log(
|
|
2795
|
+
console.log(chalk19.cyan.bold(" Tutti Project Info"));
|
|
2262
2796
|
console.log();
|
|
2263
|
-
console.log(" " +
|
|
2797
|
+
console.log(" " + chalk19.dim("Project:") + " " + chalk19.bold(projectName) + " " + chalk19.dim(projectVersion));
|
|
2264
2798
|
const tuttiPkgs = [...installedDeps.entries()].filter(([name]) => name.startsWith("@tuttiai/"));
|
|
2265
2799
|
if (tuttiPkgs.length > 0) {
|
|
2266
2800
|
console.log();
|
|
2267
|
-
console.log(" " +
|
|
2801
|
+
console.log(" " + chalk19.bold("Packages:"));
|
|
2268
2802
|
for (const [name, version] of tuttiPkgs) {
|
|
2269
|
-
console.log(" " +
|
|
2803
|
+
console.log(" " + pad3(name, 28) + chalk19.dim(version));
|
|
2270
2804
|
}
|
|
2271
2805
|
}
|
|
2272
|
-
const scoreFile =
|
|
2806
|
+
const scoreFile = resolve16(scorePath ?? "./tutti.score.ts");
|
|
2273
2807
|
if (!existsSync14(scoreFile)) {
|
|
2274
2808
|
console.log();
|
|
2275
|
-
console.log(
|
|
2276
|
-
console.log(
|
|
2809
|
+
console.log(chalk19.dim(" No score file found at " + scoreFile));
|
|
2810
|
+
console.log(chalk19.dim(' Run "tutti-ai init" to create a new project.'));
|
|
2277
2811
|
console.log();
|
|
2278
2812
|
return;
|
|
2279
2813
|
}
|
|
2280
2814
|
let score;
|
|
2281
2815
|
try {
|
|
2282
|
-
score = await
|
|
2816
|
+
score = await ScoreLoader9.load(scoreFile);
|
|
2283
2817
|
} catch (err) {
|
|
2284
|
-
|
|
2818
|
+
logger14.error(
|
|
2285
2819
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
2286
2820
|
"Failed to load score"
|
|
2287
2821
|
);
|
|
2288
|
-
console.log(
|
|
2822
|
+
console.log(chalk19.dim(" Score file found but failed to load."));
|
|
2289
2823
|
console.log();
|
|
2290
2824
|
return;
|
|
2291
2825
|
}
|
|
2292
|
-
console.log(" " +
|
|
2826
|
+
console.log(" " + chalk19.dim("Score:") + " " + (score.name ?? scoreFile));
|
|
2293
2827
|
const agentEntries = Object.entries(score.agents);
|
|
2294
2828
|
console.log();
|
|
2295
|
-
console.log(" " +
|
|
2829
|
+
console.log(" " + chalk19.bold("Agents:") + chalk19.dim(" (" + agentEntries.length + ")"));
|
|
2296
2830
|
for (const [id, agent] of agentEntries) {
|
|
2297
2831
|
const voiceNames = agent.voices.map((v) => v.name).join(", ") || "none";
|
|
2298
2832
|
const model = agent.model ?? score.default_model ?? "(default)";
|
|
@@ -2304,22 +2838,22 @@ async function infoCommand(scorePath) {
|
|
|
2304
2838
|
if (agent.outputSchema) flags.push("structured");
|
|
2305
2839
|
if (agent.beforeRun ?? agent.afterRun) flags.push("guardrails");
|
|
2306
2840
|
console.log();
|
|
2307
|
-
console.log(" " +
|
|
2308
|
-
console.log(" " +
|
|
2309
|
-
console.log(" " +
|
|
2841
|
+
console.log(" " + chalk19.bold(id) + chalk19.dim(" (" + agent.name + ")"));
|
|
2842
|
+
console.log(" " + chalk19.dim("Model: ") + model);
|
|
2843
|
+
console.log(" " + chalk19.dim("Voices: ") + voiceNames);
|
|
2310
2844
|
if (flags.length > 0) {
|
|
2311
|
-
console.log(" " +
|
|
2845
|
+
console.log(" " + chalk19.dim("Flags: ") + flags.join(", "));
|
|
2312
2846
|
}
|
|
2313
2847
|
if (agent.schedule) {
|
|
2314
2848
|
const sched = agent.schedule;
|
|
2315
2849
|
const trigger = sched.cron ?? sched.every ?? sched.at ?? "?";
|
|
2316
|
-
console.log(" " +
|
|
2850
|
+
console.log(" " + chalk19.dim("Schedule: ") + trigger);
|
|
2317
2851
|
}
|
|
2318
2852
|
}
|
|
2319
2853
|
if (score.entry) {
|
|
2320
2854
|
const entry = typeof score.entry === "string" ? score.entry : "parallel";
|
|
2321
2855
|
console.log();
|
|
2322
|
-
console.log(" " +
|
|
2856
|
+
console.log(" " + chalk19.dim("Entry:") + " " + entry);
|
|
2323
2857
|
}
|
|
2324
2858
|
console.log();
|
|
2325
2859
|
}
|
|
@@ -2327,13 +2861,13 @@ async function infoCommand(scorePath) {
|
|
|
2327
2861
|
// src/commands/upgrade.ts
|
|
2328
2862
|
import { execSync as execSync5 } from "child_process";
|
|
2329
2863
|
import { existsSync as existsSync15, readFileSync as readFileSync8 } from "fs";
|
|
2330
|
-
import { resolve as
|
|
2331
|
-
import
|
|
2332
|
-
import
|
|
2864
|
+
import { resolve as resolve17 } from "path";
|
|
2865
|
+
import chalk20 from "chalk";
|
|
2866
|
+
import ora10 from "ora";
|
|
2333
2867
|
function detectPackageManager2() {
|
|
2334
2868
|
const cwd = process.cwd();
|
|
2335
|
-
if (existsSync15(
|
|
2336
|
-
if (existsSync15(
|
|
2869
|
+
if (existsSync15(resolve17(cwd, "yarn.lock"))) return "yarn";
|
|
2870
|
+
if (existsSync15(resolve17(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2337
2871
|
return "npm";
|
|
2338
2872
|
}
|
|
2339
2873
|
function resolvePackageName2(input) {
|
|
@@ -2352,7 +2886,7 @@ function resolvePackageName2(input) {
|
|
|
2352
2886
|
return KNOWN.get(input) ?? (input.startsWith("@") ? input : `@tuttiai/${input}`);
|
|
2353
2887
|
}
|
|
2354
2888
|
function getInstalledTuttiPackages() {
|
|
2355
|
-
const pkgPath =
|
|
2889
|
+
const pkgPath = resolve17(process.cwd(), "package.json");
|
|
2356
2890
|
if (!existsSync15(pkgPath)) return /* @__PURE__ */ new Map();
|
|
2357
2891
|
try {
|
|
2358
2892
|
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
@@ -2373,40 +2907,40 @@ function getInstalledTuttiPackages() {
|
|
|
2373
2907
|
}
|
|
2374
2908
|
}
|
|
2375
2909
|
function upgradeCommand(target) {
|
|
2376
|
-
const pkgPath =
|
|
2910
|
+
const pkgPath = resolve17(process.cwd(), "package.json");
|
|
2377
2911
|
if (!existsSync15(pkgPath)) {
|
|
2378
|
-
console.error(
|
|
2379
|
-
console.error(
|
|
2912
|
+
console.error(chalk20.red("No package.json found in the current directory."));
|
|
2913
|
+
console.error(chalk20.dim('Run "tutti-ai init" to create a new project.'));
|
|
2380
2914
|
process.exit(1);
|
|
2381
2915
|
}
|
|
2382
2916
|
const pm = detectPackageManager2();
|
|
2383
2917
|
const installed = getInstalledTuttiPackages();
|
|
2384
2918
|
if (installed.size === 0) {
|
|
2385
|
-
console.log(
|
|
2919
|
+
console.log(chalk20.dim("No @tuttiai packages found in this project."));
|
|
2386
2920
|
return;
|
|
2387
2921
|
}
|
|
2388
2922
|
let packages;
|
|
2389
2923
|
if (target) {
|
|
2390
2924
|
const resolved = resolvePackageName2(target);
|
|
2391
2925
|
if (!installed.has(resolved)) {
|
|
2392
|
-
console.error(
|
|
2393
|
-
console.error(
|
|
2926
|
+
console.error(chalk20.red(resolved + " is not installed in this project."));
|
|
2927
|
+
console.error(chalk20.dim("Installed: " + [...installed.keys()].join(", ")));
|
|
2394
2928
|
process.exit(1);
|
|
2395
2929
|
}
|
|
2396
2930
|
packages = [resolved + "@latest"];
|
|
2397
|
-
console.log(
|
|
2931
|
+
console.log(chalk20.cyan(" Upgrading " + resolved + "..."));
|
|
2398
2932
|
} else {
|
|
2399
2933
|
packages = [...installed.keys()].map((p) => p + "@latest");
|
|
2400
|
-
console.log(
|
|
2934
|
+
console.log(chalk20.cyan(" Upgrading all " + packages.length + " @tuttiai packages..."));
|
|
2401
2935
|
}
|
|
2402
2936
|
const installCmd = pm === "yarn" ? "yarn add " + packages.join(" ") : pm === "pnpm" ? "pnpm add " + packages.join(" ") : "npm install " + packages.join(" ");
|
|
2403
|
-
const spinner =
|
|
2937
|
+
const spinner = ora10("Installing...").start();
|
|
2404
2938
|
try {
|
|
2405
2939
|
execSync5(installCmd, { cwd: process.cwd(), stdio: "pipe" });
|
|
2406
2940
|
spinner.succeed("Upgraded " + packages.length + " package(s)");
|
|
2407
2941
|
} catch {
|
|
2408
2942
|
spinner.fail("Upgrade failed");
|
|
2409
|
-
console.log(
|
|
2943
|
+
console.log(chalk20.dim(" Run manually: " + installCmd));
|
|
2410
2944
|
process.exit(1);
|
|
2411
2945
|
}
|
|
2412
2946
|
console.log();
|
|
@@ -2416,9 +2950,9 @@ function upgradeCommand(target) {
|
|
|
2416
2950
|
const oldClean = oldVersion.replace(/^[\^~]/, "");
|
|
2417
2951
|
const newClean = newVersion.replace(/^[\^~]/, "");
|
|
2418
2952
|
if (oldClean !== newClean) {
|
|
2419
|
-
console.log(" " +
|
|
2953
|
+
console.log(" " + chalk20.green("\u2191") + " " + name + " " + chalk20.dim(oldClean) + " \u2192 " + chalk20.green(newClean));
|
|
2420
2954
|
} else {
|
|
2421
|
-
console.log(" " +
|
|
2955
|
+
console.log(" " + chalk20.dim("=") + " " + name + " " + chalk20.dim(newClean) + chalk20.dim(" (already latest)"));
|
|
2422
2956
|
}
|
|
2423
2957
|
}
|
|
2424
2958
|
console.log();
|
|
@@ -2426,21 +2960,21 @@ function upgradeCommand(target) {
|
|
|
2426
2960
|
|
|
2427
2961
|
// src/commands/replay.ts
|
|
2428
2962
|
import { existsSync as existsSync16 } from "fs";
|
|
2429
|
-
import { writeFile } from "fs/promises";
|
|
2430
|
-
import { resolve as
|
|
2963
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
2964
|
+
import { resolve as resolve18 } from "path";
|
|
2431
2965
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
2432
|
-
import
|
|
2433
|
-
import
|
|
2966
|
+
import chalk22 from "chalk";
|
|
2967
|
+
import ora11 from "ora";
|
|
2434
2968
|
import {
|
|
2435
|
-
PostgresSessionStore,
|
|
2436
|
-
ScoreLoader as
|
|
2969
|
+
PostgresSessionStore as PostgresSessionStore2,
|
|
2970
|
+
ScoreLoader as ScoreLoader10,
|
|
2437
2971
|
TuttiRuntime as TuttiRuntime5,
|
|
2438
|
-
SecretsManager as
|
|
2439
|
-
createLogger as
|
|
2972
|
+
SecretsManager as SecretsManager8,
|
|
2973
|
+
createLogger as createLogger15
|
|
2440
2974
|
} from "@tuttiai/core";
|
|
2441
2975
|
|
|
2442
2976
|
// src/commands/replay-render.ts
|
|
2443
|
-
import
|
|
2977
|
+
import chalk21 from "chalk";
|
|
2444
2978
|
function messageToText2(msg) {
|
|
2445
2979
|
if (typeof msg.content === "string") return msg.content;
|
|
2446
2980
|
const parts = [];
|
|
@@ -2465,22 +2999,22 @@ function renderList(messages) {
|
|
|
2465
2999
|
for (let i = 0; i < messages.length; i++) {
|
|
2466
3000
|
const msg = messages.at(i);
|
|
2467
3001
|
if (!msg) continue;
|
|
2468
|
-
const role = msg.role === "user" ?
|
|
3002
|
+
const role = msg.role === "user" ? chalk21.blue("user ") : chalk21.green("assistant");
|
|
2469
3003
|
const text = excerpt2(messageToText2(msg), 80);
|
|
2470
3004
|
lines.push(
|
|
2471
|
-
|
|
3005
|
+
chalk21.dim(String(i).padStart(3)) + " " + role + " " + text
|
|
2472
3006
|
);
|
|
2473
3007
|
}
|
|
2474
3008
|
return lines.join("\n");
|
|
2475
3009
|
}
|
|
2476
3010
|
function renderShow(messages, index) {
|
|
2477
3011
|
if (index < 0 || index >= messages.length) {
|
|
2478
|
-
return
|
|
3012
|
+
return chalk21.red("Index out of range. Valid: 0\u2013" + (messages.length - 1));
|
|
2479
3013
|
}
|
|
2480
3014
|
const msg = messages.at(index);
|
|
2481
|
-
if (!msg) return
|
|
3015
|
+
if (!msg) return chalk21.red("Index out of range.");
|
|
2482
3016
|
const lines = [];
|
|
2483
|
-
lines.push(
|
|
3017
|
+
lines.push(chalk21.cyan.bold("Turn " + index) + " " + chalk21.dim("[" + msg.role + "]"));
|
|
2484
3018
|
lines.push("");
|
|
2485
3019
|
if (typeof msg.content === "string") {
|
|
2486
3020
|
lines.push(msg.content);
|
|
@@ -2489,13 +3023,13 @@ function renderShow(messages, index) {
|
|
|
2489
3023
|
if (block.type === "text") {
|
|
2490
3024
|
lines.push(block.text);
|
|
2491
3025
|
} else if (block.type === "tool_use") {
|
|
2492
|
-
lines.push(
|
|
2493
|
-
lines.push(
|
|
2494
|
-
lines.push(
|
|
3026
|
+
lines.push(chalk21.yellow(" tool_use: " + block.name));
|
|
3027
|
+
lines.push(chalk21.dim(" id: " + block.id));
|
|
3028
|
+
lines.push(chalk21.dim(" input: " + JSON.stringify(block.input, null, 2)));
|
|
2495
3029
|
} else if (block.type === "tool_result") {
|
|
2496
|
-
const label = block.is_error ?
|
|
3030
|
+
const label = block.is_error ? chalk21.red(" tool_result (error):") : chalk21.green(" tool_result:");
|
|
2497
3031
|
lines.push(label);
|
|
2498
|
-
lines.push(
|
|
3032
|
+
lines.push(chalk21.dim(" tool_use_id: " + block.tool_use_id));
|
|
2499
3033
|
lines.push(" " + block.content);
|
|
2500
3034
|
}
|
|
2501
3035
|
}
|
|
@@ -2504,7 +3038,7 @@ function renderShow(messages, index) {
|
|
|
2504
3038
|
}
|
|
2505
3039
|
function renderInspect(messages, index) {
|
|
2506
3040
|
if (index < 0 || index >= messages.length) {
|
|
2507
|
-
return
|
|
3041
|
+
return chalk21.red("Index out of range.");
|
|
2508
3042
|
}
|
|
2509
3043
|
return JSON.stringify(messages.at(index), null, 2);
|
|
2510
3044
|
}
|
|
@@ -2557,26 +3091,26 @@ function exportMarkdown(session) {
|
|
|
2557
3091
|
}
|
|
2558
3092
|
|
|
2559
3093
|
// src/commands/replay.ts
|
|
2560
|
-
var
|
|
3094
|
+
var logger15 = createLogger15("tutti-cli");
|
|
2561
3095
|
async function replayCommand(sessionId, opts = {}) {
|
|
2562
|
-
const pgUrl =
|
|
3096
|
+
const pgUrl = SecretsManager8.optional("TUTTI_PG_URL");
|
|
2563
3097
|
if (!pgUrl) {
|
|
2564
|
-
console.error(
|
|
3098
|
+
console.error(chalk22.red("TUTTI_PG_URL is not set."));
|
|
2565
3099
|
console.error(
|
|
2566
|
-
|
|
3100
|
+
chalk22.dim(
|
|
2567
3101
|
"The replay command requires PostgreSQL for session persistence.\nSet TUTTI_PG_URL=postgres://user:pass@host/db in your environment."
|
|
2568
3102
|
)
|
|
2569
3103
|
);
|
|
2570
3104
|
process.exit(1);
|
|
2571
3105
|
}
|
|
2572
|
-
const store = new
|
|
2573
|
-
const spinner =
|
|
3106
|
+
const store = new PostgresSessionStore2(pgUrl);
|
|
3107
|
+
const spinner = ora11({ color: "cyan" }).start("Loading session...");
|
|
2574
3108
|
let session;
|
|
2575
3109
|
try {
|
|
2576
3110
|
session = await store.getAsync(sessionId);
|
|
2577
3111
|
} catch (err) {
|
|
2578
3112
|
spinner.fail("Failed to load session");
|
|
2579
|
-
|
|
3113
|
+
logger15.error(
|
|
2580
3114
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
2581
3115
|
"Session store error"
|
|
2582
3116
|
);
|
|
@@ -2584,27 +3118,27 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2584
3118
|
}
|
|
2585
3119
|
spinner.stop();
|
|
2586
3120
|
if (!session) {
|
|
2587
|
-
console.error(
|
|
2588
|
-
console.error(
|
|
3121
|
+
console.error(chalk22.red("Session not found: " + sessionId));
|
|
3122
|
+
console.error(chalk22.dim("Check the session ID and ensure TUTTI_PG_URL points to the correct database."));
|
|
2589
3123
|
await store.close();
|
|
2590
3124
|
process.exit(1);
|
|
2591
3125
|
}
|
|
2592
3126
|
const messages = session.messages;
|
|
2593
3127
|
console.log("");
|
|
2594
|
-
console.log(
|
|
2595
|
-
console.log(
|
|
2596
|
-
console.log(
|
|
2597
|
-
console.log(
|
|
2598
|
-
console.log(
|
|
3128
|
+
console.log(chalk22.cyan.bold(" Tutti Replay"));
|
|
3129
|
+
console.log(chalk22.dim(" Session: " + session.id));
|
|
3130
|
+
console.log(chalk22.dim(" Agent: " + session.agent_name));
|
|
3131
|
+
console.log(chalk22.dim(" Created: " + session.created_at.toISOString()));
|
|
3132
|
+
console.log(chalk22.dim(" Messages: " + messages.length));
|
|
2599
3133
|
console.log("");
|
|
2600
|
-
console.log(
|
|
3134
|
+
console.log(chalk22.dim(" Commands: list, show <n>, next, prev, inspect, replay-from <n>, export <json|md>, quit"));
|
|
2601
3135
|
console.log("");
|
|
2602
3136
|
let cursor = 0;
|
|
2603
3137
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2604
3138
|
try {
|
|
2605
3139
|
while (true) {
|
|
2606
|
-
const
|
|
2607
|
-
const raw = await rl.question(
|
|
3140
|
+
const prompt6 = chalk22.cyan("replay [" + cursor + "/" + (messages.length - 1) + "]> ");
|
|
3141
|
+
const raw = await rl.question(prompt6);
|
|
2608
3142
|
const input = raw.trim();
|
|
2609
3143
|
if (!input) continue;
|
|
2610
3144
|
const [cmd, ...args] = input.split(/\s+/);
|
|
@@ -2612,7 +3146,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2612
3146
|
case "quit":
|
|
2613
3147
|
case "exit":
|
|
2614
3148
|
case "q":
|
|
2615
|
-
console.log(
|
|
3149
|
+
console.log(chalk22.dim("Bye."));
|
|
2616
3150
|
return;
|
|
2617
3151
|
case "list":
|
|
2618
3152
|
console.log(renderList(messages));
|
|
@@ -2628,7 +3162,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2628
3162
|
cursor++;
|
|
2629
3163
|
console.log(renderShow(messages, cursor));
|
|
2630
3164
|
} else {
|
|
2631
|
-
console.log(
|
|
3165
|
+
console.log(chalk22.dim("Already at last message."));
|
|
2632
3166
|
}
|
|
2633
3167
|
break;
|
|
2634
3168
|
case "prev":
|
|
@@ -2636,7 +3170,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2636
3170
|
cursor--;
|
|
2637
3171
|
console.log(renderShow(messages, cursor));
|
|
2638
3172
|
} else {
|
|
2639
|
-
console.log(
|
|
3173
|
+
console.log(chalk22.dim("Already at first message."));
|
|
2640
3174
|
}
|
|
2641
3175
|
break;
|
|
2642
3176
|
case "inspect":
|
|
@@ -2645,7 +3179,7 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2645
3179
|
case "replay-from": {
|
|
2646
3180
|
const turn = parseInt(args[0] ?? "", 10);
|
|
2647
3181
|
if (isNaN(turn) || turn < 0 || turn >= messages.length) {
|
|
2648
|
-
console.log(
|
|
3182
|
+
console.log(chalk22.red("Usage: replay-from <turn-number>"));
|
|
2649
3183
|
break;
|
|
2650
3184
|
}
|
|
2651
3185
|
await handleReplayFrom(turn, session, rl, opts);
|
|
@@ -2655,20 +3189,20 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2655
3189
|
const format = args[0];
|
|
2656
3190
|
if (format === "json") {
|
|
2657
3191
|
const filename = "session-" + session.id.slice(0, 8) + ".json";
|
|
2658
|
-
await
|
|
2659
|
-
console.log(
|
|
3192
|
+
await writeFile2(filename, exportJSON(session));
|
|
3193
|
+
console.log(chalk22.green("Exported to " + filename));
|
|
2660
3194
|
} else if (format === "md" || format === "markdown") {
|
|
2661
3195
|
const filename = "session-" + session.id.slice(0, 8) + ".md";
|
|
2662
|
-
await
|
|
2663
|
-
console.log(
|
|
3196
|
+
await writeFile2(filename, exportMarkdown(session));
|
|
3197
|
+
console.log(chalk22.green("Exported to " + filename));
|
|
2664
3198
|
} else {
|
|
2665
|
-
console.log(
|
|
3199
|
+
console.log(chalk22.dim("Usage: export <json|md>"));
|
|
2666
3200
|
}
|
|
2667
3201
|
break;
|
|
2668
3202
|
}
|
|
2669
3203
|
default:
|
|
2670
3204
|
console.log(
|
|
2671
|
-
|
|
3205
|
+
chalk22.dim("Unknown command. Available: list, show <n>, next, prev, inspect, replay-from <n>, export <json|md>, quit")
|
|
2672
3206
|
);
|
|
2673
3207
|
}
|
|
2674
3208
|
}
|
|
@@ -2678,29 +3212,29 @@ async function replayCommand(sessionId, opts = {}) {
|
|
|
2678
3212
|
}
|
|
2679
3213
|
}
|
|
2680
3214
|
async function handleReplayFrom(turn, session, rl, opts) {
|
|
2681
|
-
const scoreFile =
|
|
3215
|
+
const scoreFile = resolve18(opts.score ?? "./tutti.score.ts");
|
|
2682
3216
|
if (!existsSync16(scoreFile)) {
|
|
2683
|
-
console.log(
|
|
2684
|
-
console.log(
|
|
3217
|
+
console.log(chalk22.red("Score file not found: " + scoreFile));
|
|
3218
|
+
console.log(chalk22.dim("Use --score to specify the score file."));
|
|
2685
3219
|
return;
|
|
2686
3220
|
}
|
|
2687
3221
|
const originalMsg = session.messages.at(turn);
|
|
2688
3222
|
const originalInput = originalMsg ? messageToText2(originalMsg) : "";
|
|
2689
3223
|
const answer = await rl.question(
|
|
2690
|
-
|
|
3224
|
+
chalk22.cyan("Replay from turn " + turn + " with original input? ") + chalk22.dim("(y / enter new input) ")
|
|
2691
3225
|
);
|
|
2692
3226
|
const input = answer.trim().toLowerCase() === "y" || answer.trim() === "" ? originalInput : answer.trim();
|
|
2693
3227
|
if (!input) {
|
|
2694
|
-
console.log(
|
|
3228
|
+
console.log(chalk22.dim("No input provided. Cancelled."));
|
|
2695
3229
|
return;
|
|
2696
3230
|
}
|
|
2697
|
-
const spinnerLoad =
|
|
3231
|
+
const spinnerLoad = ora11({ color: "cyan" }).start("Loading score...");
|
|
2698
3232
|
let score;
|
|
2699
3233
|
try {
|
|
2700
|
-
score = await
|
|
3234
|
+
score = await ScoreLoader10.load(scoreFile);
|
|
2701
3235
|
} catch (err) {
|
|
2702
3236
|
spinnerLoad.fail("Failed to load score");
|
|
2703
|
-
|
|
3237
|
+
logger15.error({ error: err instanceof Error ? err.message : String(err) }, "Score load error");
|
|
2704
3238
|
return;
|
|
2705
3239
|
}
|
|
2706
3240
|
spinnerLoad.stop();
|
|
@@ -2720,10 +3254,10 @@ async function handleReplayFrom(turn, session, rl, opts) {
|
|
|
2720
3254
|
const agentMap = new Map(Object.entries(score.agents));
|
|
2721
3255
|
const agent = agentMap.get(agentName);
|
|
2722
3256
|
if (!agent) {
|
|
2723
|
-
console.log(
|
|
3257
|
+
console.log(chalk22.red('Agent "' + agentName + '" not found in score.'));
|
|
2724
3258
|
return;
|
|
2725
3259
|
}
|
|
2726
|
-
const spinnerRun =
|
|
3260
|
+
const spinnerRun = ora11({ color: "cyan" }).start("Running from turn " + turn + "...");
|
|
2727
3261
|
runtime.events.on("token:stream", (e) => {
|
|
2728
3262
|
spinnerRun.stop();
|
|
2729
3263
|
process.stdout.write(e.text);
|
|
@@ -2732,39 +3266,39 @@ async function handleReplayFrom(turn, session, rl, opts) {
|
|
|
2732
3266
|
const result = await runtime.run(agentName, input, session.id);
|
|
2733
3267
|
spinnerRun.stop();
|
|
2734
3268
|
console.log("");
|
|
2735
|
-
console.log(
|
|
2736
|
-
console.log(
|
|
2737
|
-
console.log(
|
|
3269
|
+
console.log(chalk22.green("Replay complete."));
|
|
3270
|
+
console.log(chalk22.dim(" Turns: " + result.turns));
|
|
3271
|
+
console.log(chalk22.dim(" Tokens: " + result.usage.input_tokens + " in / " + result.usage.output_tokens + " out"));
|
|
2738
3272
|
console.log("");
|
|
2739
3273
|
console.log(result.output);
|
|
2740
3274
|
} catch (err) {
|
|
2741
3275
|
spinnerRun.fail("Replay failed");
|
|
2742
|
-
|
|
3276
|
+
logger15.error({ error: err instanceof Error ? err.message : String(err) }, "Replay error");
|
|
2743
3277
|
}
|
|
2744
3278
|
}
|
|
2745
3279
|
|
|
2746
3280
|
// src/commands/schedules.ts
|
|
2747
3281
|
import { existsSync as existsSync17 } from "fs";
|
|
2748
|
-
import { resolve as
|
|
2749
|
-
import
|
|
3282
|
+
import { resolve as resolve19 } from "path";
|
|
3283
|
+
import chalk23 from "chalk";
|
|
2750
3284
|
import {
|
|
2751
|
-
ScoreLoader as
|
|
3285
|
+
ScoreLoader as ScoreLoader11,
|
|
2752
3286
|
SchedulerEngine as SchedulerEngine2,
|
|
2753
3287
|
PostgresScheduleStore as PostgresScheduleStore2,
|
|
2754
3288
|
MemoryScheduleStore as MemoryScheduleStore2,
|
|
2755
3289
|
AgentRunner as AgentRunner2,
|
|
2756
3290
|
EventBus as EventBus2,
|
|
2757
3291
|
InMemorySessionStore as InMemorySessionStore4,
|
|
2758
|
-
SecretsManager as
|
|
2759
|
-
createLogger as
|
|
3292
|
+
SecretsManager as SecretsManager9,
|
|
3293
|
+
createLogger as createLogger16
|
|
2760
3294
|
} from "@tuttiai/core";
|
|
2761
|
-
var
|
|
3295
|
+
var logger16 = createLogger16("tutti-cli");
|
|
2762
3296
|
function resolveStore2() {
|
|
2763
|
-
const pgUrl =
|
|
3297
|
+
const pgUrl = SecretsManager9.optional("TUTTI_PG_URL");
|
|
2764
3298
|
if (pgUrl) {
|
|
2765
3299
|
return new PostgresScheduleStore2({ connection_string: pgUrl });
|
|
2766
3300
|
}
|
|
2767
|
-
|
|
3301
|
+
logger16.warn("TUTTI_PG_URL not set \u2014 using in-memory store (schedules are ephemeral)");
|
|
2768
3302
|
return new MemoryScheduleStore2();
|
|
2769
3303
|
}
|
|
2770
3304
|
async function closeStore(store) {
|
|
@@ -2778,7 +3312,7 @@ function formatTrigger(r) {
|
|
|
2778
3312
|
if (r.config.at) return "at " + r.config.at;
|
|
2779
3313
|
return "?";
|
|
2780
3314
|
}
|
|
2781
|
-
function
|
|
3315
|
+
function pad4(s, len) {
|
|
2782
3316
|
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
2783
3317
|
}
|
|
2784
3318
|
async function schedulesListCommand() {
|
|
@@ -2786,22 +3320,22 @@ async function schedulesListCommand() {
|
|
|
2786
3320
|
try {
|
|
2787
3321
|
const records = await store.list();
|
|
2788
3322
|
if (records.length === 0) {
|
|
2789
|
-
console.log(
|
|
2790
|
-
console.log(
|
|
3323
|
+
console.log(chalk23.dim("No schedules found."));
|
|
3324
|
+
console.log(chalk23.dim('Run "tutti-ai schedule" to start the scheduler daemon.'));
|
|
2791
3325
|
return;
|
|
2792
3326
|
}
|
|
2793
3327
|
console.log("");
|
|
2794
3328
|
console.log(
|
|
2795
|
-
|
|
2796
|
-
" " +
|
|
3329
|
+
chalk23.dim(
|
|
3330
|
+
" " + pad4("ID", 20) + pad4("AGENT", 16) + pad4("TRIGGER", 22) + pad4("ENABLED", 10) + pad4("RUNS", 8) + "CREATED"
|
|
2797
3331
|
)
|
|
2798
3332
|
);
|
|
2799
|
-
console.log(
|
|
3333
|
+
console.log(chalk23.dim(" " + "\u2500".repeat(90)));
|
|
2800
3334
|
for (const r of records) {
|
|
2801
|
-
const enabled = r.enabled ?
|
|
3335
|
+
const enabled = r.enabled ? chalk23.green("yes") : chalk23.red("no") + " ";
|
|
2802
3336
|
const maxLabel = r.config.max_runs ? r.run_count + "/" + r.config.max_runs : String(r.run_count);
|
|
2803
3337
|
console.log(
|
|
2804
|
-
" " +
|
|
3338
|
+
" " + chalk23.bold(pad4(r.id, 20)) + pad4(r.agent_id, 16) + pad4(formatTrigger(r), 22) + pad4(enabled, 10) + pad4(maxLabel, 8) + chalk23.dim(r.created_at.toISOString().slice(0, 10))
|
|
2805
3339
|
);
|
|
2806
3340
|
}
|
|
2807
3341
|
console.log("");
|
|
@@ -2814,11 +3348,11 @@ async function schedulesEnableCommand(id) {
|
|
|
2814
3348
|
try {
|
|
2815
3349
|
const record = await store.get(id);
|
|
2816
3350
|
if (!record) {
|
|
2817
|
-
console.error(
|
|
3351
|
+
console.error(chalk23.red('Schedule "' + id + '" not found.'));
|
|
2818
3352
|
process.exit(1);
|
|
2819
3353
|
}
|
|
2820
3354
|
await store.setEnabled(id, true);
|
|
2821
|
-
console.log(
|
|
3355
|
+
console.log(chalk23.green('Schedule "' + id + '" enabled.'));
|
|
2822
3356
|
} finally {
|
|
2823
3357
|
await closeStore(store);
|
|
2824
3358
|
}
|
|
@@ -2828,22 +3362,22 @@ async function schedulesDisableCommand(id) {
|
|
|
2828
3362
|
try {
|
|
2829
3363
|
const record = await store.get(id);
|
|
2830
3364
|
if (!record) {
|
|
2831
|
-
console.error(
|
|
3365
|
+
console.error(chalk23.red('Schedule "' + id + '" not found.'));
|
|
2832
3366
|
process.exit(1);
|
|
2833
3367
|
}
|
|
2834
3368
|
await store.setEnabled(id, false);
|
|
2835
|
-
console.log(
|
|
3369
|
+
console.log(chalk23.yellow('Schedule "' + id + '" disabled.'));
|
|
2836
3370
|
} finally {
|
|
2837
3371
|
await closeStore(store);
|
|
2838
3372
|
}
|
|
2839
3373
|
}
|
|
2840
3374
|
async function schedulesTriggerCommand(id, scorePath) {
|
|
2841
|
-
const file =
|
|
3375
|
+
const file = resolve19(scorePath ?? "./tutti.score.ts");
|
|
2842
3376
|
if (!existsSync17(file)) {
|
|
2843
|
-
console.error(
|
|
3377
|
+
console.error(chalk23.red("Score file not found: " + file));
|
|
2844
3378
|
process.exit(1);
|
|
2845
3379
|
}
|
|
2846
|
-
const score = await
|
|
3380
|
+
const score = await ScoreLoader11.load(file);
|
|
2847
3381
|
const events = new EventBus2();
|
|
2848
3382
|
const sessions = new InMemorySessionStore4();
|
|
2849
3383
|
const runner = new AgentRunner2(score.provider, events, sessions);
|
|
@@ -2851,30 +3385,30 @@ async function schedulesTriggerCommand(id, scorePath) {
|
|
|
2851
3385
|
try {
|
|
2852
3386
|
const record = await store.get(id);
|
|
2853
3387
|
if (!record) {
|
|
2854
|
-
console.error(
|
|
3388
|
+
console.error(chalk23.red('Schedule "' + id + '" not found.'));
|
|
2855
3389
|
process.exit(1);
|
|
2856
3390
|
}
|
|
2857
3391
|
const agent = score.agents[record.agent_id];
|
|
2858
3392
|
if (!agent) {
|
|
2859
|
-
console.error(
|
|
3393
|
+
console.error(chalk23.red('Agent "' + record.agent_id + '" not found in score.'));
|
|
2860
3394
|
process.exit(1);
|
|
2861
3395
|
}
|
|
2862
3396
|
const resolvedAgent = agent.model ? agent : { ...agent, model: score.default_model ?? "claude-sonnet-4-20250514" };
|
|
2863
3397
|
const engine = new SchedulerEngine2(store, runner, events);
|
|
2864
3398
|
await engine.schedule(id, resolvedAgent, record.config);
|
|
2865
3399
|
engine.start();
|
|
2866
|
-
console.log(
|
|
3400
|
+
console.log(chalk23.cyan('Triggering "' + id + '" (' + record.agent_id + ")..."));
|
|
2867
3401
|
const run2 = await engine.trigger(id);
|
|
2868
3402
|
engine.stop();
|
|
2869
3403
|
if (run2.error) {
|
|
2870
|
-
console.log(
|
|
3404
|
+
console.log(chalk23.red(" Error: " + run2.error));
|
|
2871
3405
|
process.exit(1);
|
|
2872
3406
|
}
|
|
2873
3407
|
const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() : 0;
|
|
2874
|
-
console.log(
|
|
3408
|
+
console.log(chalk23.green(" Completed in " + duration + "ms"));
|
|
2875
3409
|
if (run2.result) {
|
|
2876
3410
|
const preview = run2.result.length > 200 ? run2.result.slice(0, 200) + "..." : run2.result;
|
|
2877
|
-
console.log(
|
|
3411
|
+
console.log(chalk23.dim(" Output: " + preview));
|
|
2878
3412
|
}
|
|
2879
3413
|
} finally {
|
|
2880
3414
|
await closeStore(store);
|
|
@@ -2885,31 +3419,31 @@ async function schedulesRunsCommand(id) {
|
|
|
2885
3419
|
try {
|
|
2886
3420
|
const record = await store.get(id);
|
|
2887
3421
|
if (!record) {
|
|
2888
|
-
console.error(
|
|
3422
|
+
console.error(chalk23.red('Schedule "' + id + '" not found.'));
|
|
2889
3423
|
process.exit(1);
|
|
2890
3424
|
}
|
|
2891
3425
|
if ("getRuns" in store && typeof store.getRuns === "function") {
|
|
2892
3426
|
const runs = store.getRuns(id);
|
|
2893
3427
|
if (runs.length === 0) {
|
|
2894
|
-
console.log(
|
|
3428
|
+
console.log(chalk23.dim("No runs recorded for this schedule."));
|
|
2895
3429
|
return;
|
|
2896
3430
|
}
|
|
2897
3431
|
const recent = runs.slice(-20);
|
|
2898
3432
|
console.log("");
|
|
2899
|
-
console.log(
|
|
3433
|
+
console.log(chalk23.dim(" Showing last " + recent.length + " of " + runs.length + " runs:"));
|
|
2900
3434
|
console.log("");
|
|
2901
3435
|
for (const run2 of recent) {
|
|
2902
3436
|
const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() + "ms" : "?";
|
|
2903
|
-
const status = run2.error ?
|
|
3437
|
+
const status = run2.error ? chalk23.red("error") : chalk23.green("ok");
|
|
2904
3438
|
const preview = run2.error ? run2.error.slice(0, 80) : (run2.result ?? "").slice(0, 80);
|
|
2905
3439
|
console.log(
|
|
2906
|
-
" " +
|
|
3440
|
+
" " + chalk23.dim(run2.triggered_at.toISOString()) + " " + status + " " + chalk23.dim(duration) + " " + preview
|
|
2907
3441
|
);
|
|
2908
3442
|
}
|
|
2909
3443
|
console.log("");
|
|
2910
3444
|
} else {
|
|
2911
|
-
console.log(
|
|
2912
|
-
console.log(
|
|
3445
|
+
console.log(chalk23.dim('Schedule "' + id + '" has completed ' + record.run_count + " runs."));
|
|
3446
|
+
console.log(chalk23.dim("Full run history requires the MemoryScheduleStore or a future tutti_schedule_runs table."));
|
|
2913
3447
|
}
|
|
2914
3448
|
} finally {
|
|
2915
3449
|
await closeStore(store);
|
|
@@ -2917,52 +3451,52 @@ async function schedulesRunsCommand(id) {
|
|
|
2917
3451
|
}
|
|
2918
3452
|
|
|
2919
3453
|
// src/commands/traces.ts
|
|
2920
|
-
import
|
|
2921
|
-
import { SecretsManager as
|
|
3454
|
+
import chalk25 from "chalk";
|
|
3455
|
+
import { SecretsManager as SecretsManager10 } from "@tuttiai/core";
|
|
2922
3456
|
|
|
2923
3457
|
// src/commands/traces-render.ts
|
|
2924
|
-
import
|
|
2925
|
-
function
|
|
3458
|
+
import chalk24 from "chalk";
|
|
3459
|
+
function visibleLen2(s) {
|
|
2926
3460
|
return s.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
2927
3461
|
}
|
|
2928
|
-
function
|
|
2929
|
-
const v =
|
|
3462
|
+
function pad5(s, len) {
|
|
3463
|
+
const v = visibleLen2(s);
|
|
2930
3464
|
return v >= len ? s : s + " ".repeat(len - v);
|
|
2931
3465
|
}
|
|
2932
3466
|
function colorStatus(status) {
|
|
2933
|
-
if (status === "ok") return
|
|
2934
|
-
if (status === "error") return
|
|
2935
|
-
return
|
|
3467
|
+
if (status === "ok") return chalk24.green("ok");
|
|
3468
|
+
if (status === "error") return chalk24.red("error");
|
|
3469
|
+
return chalk24.yellow("running");
|
|
2936
3470
|
}
|
|
2937
3471
|
function formatCost(cost) {
|
|
2938
|
-
if (cost === null) return
|
|
3472
|
+
if (cost === null) return chalk24.dim("\u2014");
|
|
2939
3473
|
if (cost === 0) return "$0";
|
|
2940
3474
|
return "$" + cost.toFixed(6);
|
|
2941
3475
|
}
|
|
2942
3476
|
function formatTokens(n) {
|
|
2943
|
-
return n > 0 ? String(n) :
|
|
3477
|
+
return n > 0 ? String(n) : chalk24.dim("\u2014");
|
|
2944
3478
|
}
|
|
2945
3479
|
function formatDuration(ms) {
|
|
2946
|
-
if (ms === null) return
|
|
3480
|
+
if (ms === null) return chalk24.dim("\u2014");
|
|
2947
3481
|
return ms + "ms";
|
|
2948
3482
|
}
|
|
2949
3483
|
function renderTracesList(traces) {
|
|
2950
3484
|
if (traces.length === 0) {
|
|
2951
|
-
return
|
|
3485
|
+
return chalk24.dim("No traces found.");
|
|
2952
3486
|
}
|
|
2953
3487
|
const lines = [];
|
|
2954
3488
|
lines.push("");
|
|
2955
3489
|
lines.push(
|
|
2956
|
-
|
|
2957
|
-
" " +
|
|
3490
|
+
chalk24.dim(
|
|
3491
|
+
" " + pad5("TRACE", 10) + pad5("AGENT", 18) + pad5("STARTED", 12) + pad5("DURATION", 12) + pad5("STATUS", 12) + pad5("TOKENS", 10) + "COST"
|
|
2958
3492
|
)
|
|
2959
3493
|
);
|
|
2960
|
-
lines.push(
|
|
3494
|
+
lines.push(chalk24.dim(" " + "\u2500".repeat(80)));
|
|
2961
3495
|
for (const t of traces) {
|
|
2962
3496
|
const traceShort = t.trace_id.slice(0, 8);
|
|
2963
3497
|
const startedShort = t.started_at.slice(11, 19);
|
|
2964
3498
|
lines.push(
|
|
2965
|
-
" " +
|
|
3499
|
+
" " + chalk24.bold(pad5(traceShort, 10)) + pad5(t.agent_id ?? chalk24.dim("\u2014"), 18) + pad5(startedShort, 12) + pad5(formatDuration(t.duration_ms), 12) + pad5(colorStatus(t.status), 12) + pad5(formatTokens(t.total_tokens), 10) + formatCost(t.cost_usd)
|
|
2966
3500
|
);
|
|
2967
3501
|
}
|
|
2968
3502
|
lines.push("");
|
|
@@ -2978,11 +3512,11 @@ var SPAN_ICONS = {
|
|
|
2978
3512
|
function renderSpanLine(span, indent) {
|
|
2979
3513
|
const icon = SPAN_ICONS[span.kind];
|
|
2980
3514
|
const indentStr = " ".repeat(indent);
|
|
2981
|
-
const dur = span.duration_ms !== void 0 ?
|
|
3515
|
+
const dur = span.duration_ms !== void 0 ? chalk24.dim(" " + span.duration_ms + "ms ") : chalk24.dim(" (running) ");
|
|
2982
3516
|
const status = colorStatus(span.status);
|
|
2983
3517
|
const attrs = formatAttrs(span);
|
|
2984
|
-
const attrSuffix = attrs ?
|
|
2985
|
-
return indentStr + icon + " " +
|
|
3518
|
+
const attrSuffix = attrs ? chalk24.dim(" \xB7 " + attrs) : "";
|
|
3519
|
+
return indentStr + icon + " " + chalk24.bold(span.name) + dur + status + attrSuffix;
|
|
2986
3520
|
}
|
|
2987
3521
|
function formatAttrs(span) {
|
|
2988
3522
|
const a = span.attributes;
|
|
@@ -3003,13 +3537,13 @@ function formatAttrs(span) {
|
|
|
3003
3537
|
if (a.session_id !== void 0) parts.push("session=" + a.session_id.slice(0, 8));
|
|
3004
3538
|
}
|
|
3005
3539
|
if (span.error?.message) {
|
|
3006
|
-
parts.push(
|
|
3540
|
+
parts.push(chalk24.red("error: " + span.error.message));
|
|
3007
3541
|
}
|
|
3008
3542
|
return parts.join(" \xB7 ");
|
|
3009
3543
|
}
|
|
3010
3544
|
function renderTraceShow(spans) {
|
|
3011
3545
|
if (spans.length === 0) {
|
|
3012
|
-
return
|
|
3546
|
+
return chalk24.dim("No spans found for this trace.");
|
|
3013
3547
|
}
|
|
3014
3548
|
const childrenByParent = /* @__PURE__ */ new Map();
|
|
3015
3549
|
const presentSpanIds = new Set(spans.map((s) => s.span_id));
|
|
@@ -3048,9 +3582,9 @@ function renderTraceShow(spans) {
|
|
|
3048
3582
|
}
|
|
3049
3583
|
const wall_ms = roots.map((r) => r.duration_ms).find((d) => d !== void 0);
|
|
3050
3584
|
lines.push("");
|
|
3051
|
-
lines.push(
|
|
3585
|
+
lines.push(chalk24.dim("\u2500".repeat(60)));
|
|
3052
3586
|
lines.push(
|
|
3053
|
-
|
|
3587
|
+
chalk24.dim("Total: ") + chalk24.bold(formatTokens(total_tokens)) + chalk24.dim(" tokens \xB7 ") + chalk24.bold(any_cost ? formatCost(total_cost) : chalk24.dim("\u2014")) + chalk24.dim(" cost \xB7 ") + chalk24.bold(wall_ms !== void 0 ? wall_ms + "ms" : chalk24.dim("\u2014")) + chalk24.dim(" wall")
|
|
3054
3588
|
);
|
|
3055
3589
|
lines.push("");
|
|
3056
3590
|
return lines.join("\n");
|
|
@@ -3059,17 +3593,17 @@ function renderTraceShow(spans) {
|
|
|
3059
3593
|
// src/commands/traces.ts
|
|
3060
3594
|
var DEFAULT_SERVER_URL = "http://127.0.0.1:3847";
|
|
3061
3595
|
function resolveUrl(opts) {
|
|
3062
|
-
return opts.url ??
|
|
3596
|
+
return opts.url ?? SecretsManager10.optional("TUTTI_SERVER_URL") ?? DEFAULT_SERVER_URL;
|
|
3063
3597
|
}
|
|
3064
3598
|
function resolveAuthHeader(opts) {
|
|
3065
|
-
const key = opts.apiKey ??
|
|
3599
|
+
const key = opts.apiKey ?? SecretsManager10.optional("TUTTI_API_KEY");
|
|
3066
3600
|
return key ? { Authorization: "Bearer " + key } : {};
|
|
3067
3601
|
}
|
|
3068
3602
|
function explainConnectionError(err, baseUrl) {
|
|
3069
3603
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3070
|
-
console.error(
|
|
3071
|
-
console.error(
|
|
3072
|
-
console.error(
|
|
3604
|
+
console.error(chalk25.red("Failed to reach Tutti server at " + baseUrl));
|
|
3605
|
+
console.error(chalk25.dim(" " + msg));
|
|
3606
|
+
console.error(chalk25.dim(" Is `tutti-ai serve` running? Set --url or TUTTI_SERVER_URL to override."));
|
|
3073
3607
|
process.exit(1);
|
|
3074
3608
|
}
|
|
3075
3609
|
async function tracesListCommand(opts) {
|
|
@@ -3082,11 +3616,11 @@ async function tracesListCommand(opts) {
|
|
|
3082
3616
|
explainConnectionError(err, baseUrl);
|
|
3083
3617
|
}
|
|
3084
3618
|
if (res.status === 401) {
|
|
3085
|
-
console.error(
|
|
3619
|
+
console.error(chalk25.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
3086
3620
|
process.exit(1);
|
|
3087
3621
|
}
|
|
3088
3622
|
if (!res.ok) {
|
|
3089
|
-
console.error(
|
|
3623
|
+
console.error(chalk25.red("Server returned " + res.status + " " + res.statusText));
|
|
3090
3624
|
process.exit(1);
|
|
3091
3625
|
}
|
|
3092
3626
|
const body = await res.json();
|
|
@@ -3102,15 +3636,15 @@ async function tracesShowCommand(traceId, opts) {
|
|
|
3102
3636
|
explainConnectionError(err, baseUrl);
|
|
3103
3637
|
}
|
|
3104
3638
|
if (res.status === 404) {
|
|
3105
|
-
console.error(
|
|
3639
|
+
console.error(chalk25.red('Trace "' + traceId + '" not found.'));
|
|
3106
3640
|
process.exit(1);
|
|
3107
3641
|
}
|
|
3108
3642
|
if (res.status === 401) {
|
|
3109
|
-
console.error(
|
|
3643
|
+
console.error(chalk25.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
3110
3644
|
process.exit(1);
|
|
3111
3645
|
}
|
|
3112
3646
|
if (!res.ok) {
|
|
3113
|
-
console.error(
|
|
3647
|
+
console.error(chalk25.red("Server returned " + res.status + " " + res.statusText));
|
|
3114
3648
|
process.exit(1);
|
|
3115
3649
|
}
|
|
3116
3650
|
const body = await res.json();
|
|
@@ -3120,13 +3654,13 @@ async function tracesShowCommand(traceId, opts) {
|
|
|
3120
3654
|
async function tracesTailCommand(opts) {
|
|
3121
3655
|
const baseUrl = resolveUrl(opts);
|
|
3122
3656
|
const url = baseUrl.replace(/\/$/, "") + "/traces/stream";
|
|
3123
|
-
console.error(
|
|
3657
|
+
console.error(chalk25.dim("Tailing traces from " + baseUrl + " \u2014 Ctrl+C to exit"));
|
|
3124
3658
|
console.error("");
|
|
3125
3659
|
const controller = new AbortController();
|
|
3126
3660
|
process.once("SIGINT", () => {
|
|
3127
3661
|
controller.abort();
|
|
3128
3662
|
console.error("");
|
|
3129
|
-
console.error(
|
|
3663
|
+
console.error(chalk25.dim("Disconnected."));
|
|
3130
3664
|
process.exit(0);
|
|
3131
3665
|
});
|
|
3132
3666
|
let res;
|
|
@@ -3140,11 +3674,11 @@ async function tracesTailCommand(opts) {
|
|
|
3140
3674
|
explainConnectionError(err, baseUrl);
|
|
3141
3675
|
}
|
|
3142
3676
|
if (res.status === 401) {
|
|
3143
|
-
console.error(
|
|
3677
|
+
console.error(chalk25.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
3144
3678
|
process.exit(1);
|
|
3145
3679
|
}
|
|
3146
3680
|
if (!res.ok || !res.body) {
|
|
3147
|
-
console.error(
|
|
3681
|
+
console.error(chalk25.red("Server returned " + res.status + " " + res.statusText));
|
|
3148
3682
|
process.exit(1);
|
|
3149
3683
|
}
|
|
3150
3684
|
const reader = res.body.getReader();
|
|
@@ -3169,7 +3703,7 @@ async function tracesTailCommand(opts) {
|
|
|
3169
3703
|
const span = reviveSpanDates(JSON.parse(dataLine.slice(6)));
|
|
3170
3704
|
console.log(renderSpanLine(span, 0));
|
|
3171
3705
|
} catch (err) {
|
|
3172
|
-
console.error(
|
|
3706
|
+
console.error(chalk25.red("Bad SSE frame: " + (err instanceof Error ? err.message : String(err))));
|
|
3173
3707
|
}
|
|
3174
3708
|
}
|
|
3175
3709
|
}
|
|
@@ -3184,25 +3718,25 @@ function reviveSpanDates(span) {
|
|
|
3184
3718
|
|
|
3185
3719
|
// src/commands/memory.ts
|
|
3186
3720
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
3187
|
-
import
|
|
3188
|
-
import
|
|
3721
|
+
import chalk27 from "chalk";
|
|
3722
|
+
import Enquirer4 from "enquirer";
|
|
3189
3723
|
import {
|
|
3190
3724
|
MemoryUserMemoryStore,
|
|
3191
3725
|
PostgresUserMemoryStore,
|
|
3192
|
-
SecretsManager as
|
|
3193
|
-
createLogger as
|
|
3726
|
+
SecretsManager as SecretsManager11,
|
|
3727
|
+
createLogger as createLogger17
|
|
3194
3728
|
} from "@tuttiai/core";
|
|
3195
3729
|
|
|
3196
3730
|
// src/commands/memory-render.ts
|
|
3197
|
-
import
|
|
3198
|
-
function
|
|
3731
|
+
import chalk26 from "chalk";
|
|
3732
|
+
function visibleLen3(s) {
|
|
3199
3733
|
return s.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
3200
3734
|
}
|
|
3201
|
-
function
|
|
3202
|
-
const v =
|
|
3735
|
+
function pad6(s, len) {
|
|
3736
|
+
const v = visibleLen3(s);
|
|
3203
3737
|
return v >= len ? s : s + " ".repeat(len - v);
|
|
3204
3738
|
}
|
|
3205
|
-
function
|
|
3739
|
+
function truncate2(text, max) {
|
|
3206
3740
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
3207
3741
|
return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
|
|
3208
3742
|
}
|
|
@@ -3222,23 +3756,23 @@ function sortMemoriesForList(memories) {
|
|
|
3222
3756
|
});
|
|
3223
3757
|
}
|
|
3224
3758
|
function renderTable(memories, emptyMessage) {
|
|
3225
|
-
if (memories.length === 0) return
|
|
3759
|
+
if (memories.length === 0) return chalk26.dim(emptyMessage);
|
|
3226
3760
|
const lines = [];
|
|
3227
3761
|
lines.push("");
|
|
3228
3762
|
lines.push(
|
|
3229
|
-
|
|
3230
|
-
" " +
|
|
3763
|
+
chalk26.dim(
|
|
3764
|
+
" " + pad6("ID", 10) + pad6("CONTENT", 62) + pad6("SOURCE", 12) + pad6("IMPORTANCE", 14) + "CREATED"
|
|
3231
3765
|
)
|
|
3232
3766
|
);
|
|
3233
|
-
lines.push(
|
|
3767
|
+
lines.push(chalk26.dim(" " + "\u2500".repeat(110)));
|
|
3234
3768
|
for (const m of memories) {
|
|
3235
3769
|
const idShort = m.id.slice(0, 8);
|
|
3236
|
-
const content =
|
|
3237
|
-
const sourceColored = m.source === "explicit" ?
|
|
3770
|
+
const content = truncate2(m.content, 60);
|
|
3771
|
+
const sourceColored = m.source === "explicit" ? chalk26.green("explicit") : chalk26.yellow("inferred");
|
|
3238
3772
|
const importance = importanceStars(m.importance);
|
|
3239
3773
|
const created = formatDate(m.created_at);
|
|
3240
3774
|
lines.push(
|
|
3241
|
-
" " +
|
|
3775
|
+
" " + chalk26.bold(pad6(idShort, 10)) + pad6(content, 62) + pad6(sourceColored, 12) + pad6(importance, 14) + chalk26.dim(created)
|
|
3242
3776
|
);
|
|
3243
3777
|
}
|
|
3244
3778
|
lines.push("");
|
|
@@ -3251,8 +3785,8 @@ function renderMemoryList(memories, userId) {
|
|
|
3251
3785
|
);
|
|
3252
3786
|
}
|
|
3253
3787
|
function renderMemorySearch(memories, userId, query) {
|
|
3254
|
-
const header =
|
|
3255
|
-
"Search for " +
|
|
3788
|
+
const header = chalk26.dim(
|
|
3789
|
+
"Search for " + chalk26.bold('"' + query + '"') + ' in user "' + userId + '" \u2014 ' + memories.length + " result" + (memories.length === 1 ? "" : "s")
|
|
3256
3790
|
);
|
|
3257
3791
|
const body = renderTable(
|
|
3258
3792
|
memories,
|
|
@@ -3261,13 +3795,13 @@ function renderMemorySearch(memories, userId, query) {
|
|
|
3261
3795
|
return header + body;
|
|
3262
3796
|
}
|
|
3263
3797
|
function renderMemoryAdded(memory) {
|
|
3264
|
-
return
|
|
3798
|
+
return chalk26.green("\u2713") + " Stored memory " + chalk26.bold(memory.id.slice(0, 8)) + chalk26.dim(" (" + memory.source + ", " + importanceStars(memory.importance) + ")");
|
|
3265
3799
|
}
|
|
3266
3800
|
function renderMemoryDeleted(memoryId) {
|
|
3267
|
-
return
|
|
3801
|
+
return chalk26.green("\u2713") + " Deleted memory " + chalk26.bold(memoryId.slice(0, 8));
|
|
3268
3802
|
}
|
|
3269
3803
|
function renderMemoryCleared(userId, count) {
|
|
3270
|
-
return
|
|
3804
|
+
return chalk26.green("\u2713") + " Deleted " + chalk26.bold(String(count)) + " memor" + (count === 1 ? "y" : "ies") + ' for user "' + userId + '"';
|
|
3271
3805
|
}
|
|
3272
3806
|
function exportMemoriesJson(memories) {
|
|
3273
3807
|
return JSON.stringify(memories, null, 2) + "\n";
|
|
@@ -3310,14 +3844,14 @@ function csvEscape(field) {
|
|
|
3310
3844
|
}
|
|
3311
3845
|
|
|
3312
3846
|
// src/commands/memory.ts
|
|
3313
|
-
var
|
|
3314
|
-
var { prompt:
|
|
3847
|
+
var logger17 = createLogger17("tutti-cli");
|
|
3848
|
+
var { prompt: prompt4 } = Enquirer4;
|
|
3315
3849
|
function resolveStore3() {
|
|
3316
|
-
const pgUrl =
|
|
3850
|
+
const pgUrl = SecretsManager11.optional("TUTTI_PG_URL");
|
|
3317
3851
|
if (pgUrl) {
|
|
3318
3852
|
return new PostgresUserMemoryStore({ connection_string: pgUrl });
|
|
3319
3853
|
}
|
|
3320
|
-
|
|
3854
|
+
logger17.warn(
|
|
3321
3855
|
"TUTTI_PG_URL not set \u2014 using in-memory store (memories are ephemeral; this command will appear to do nothing useful)"
|
|
3322
3856
|
);
|
|
3323
3857
|
return new MemoryUserMemoryStore();
|
|
@@ -3329,7 +3863,7 @@ async function closeStore2(store) {
|
|
|
3329
3863
|
}
|
|
3330
3864
|
function requireUser(opts) {
|
|
3331
3865
|
if (!opts.user || opts.user.trim() === "") {
|
|
3332
|
-
console.error(
|
|
3866
|
+
console.error(chalk27.red("--user <user-id> is required"));
|
|
3333
3867
|
process.exit(1);
|
|
3334
3868
|
}
|
|
3335
3869
|
return opts.user.trim();
|
|
@@ -3339,7 +3873,7 @@ function parseImportance(raw) {
|
|
|
3339
3873
|
if (raw === "1") return 1;
|
|
3340
3874
|
if (raw === "2") return 2;
|
|
3341
3875
|
if (raw === "3") return 3;
|
|
3342
|
-
console.error(
|
|
3876
|
+
console.error(chalk27.red("--importance must be 1, 2, or 3"));
|
|
3343
3877
|
process.exit(1);
|
|
3344
3878
|
}
|
|
3345
3879
|
async function memoryListCommand(opts) {
|
|
@@ -3355,7 +3889,7 @@ async function memoryListCommand(opts) {
|
|
|
3355
3889
|
async function memorySearchCommand(query, opts) {
|
|
3356
3890
|
const userId = requireUser(opts);
|
|
3357
3891
|
if (query.trim() === "") {
|
|
3358
|
-
console.error(
|
|
3892
|
+
console.error(chalk27.red("Search query is required"));
|
|
3359
3893
|
process.exit(1);
|
|
3360
3894
|
}
|
|
3361
3895
|
const store = resolveStore3();
|
|
@@ -3369,7 +3903,7 @@ async function memorySearchCommand(query, opts) {
|
|
|
3369
3903
|
async function memoryAddCommand(content, opts) {
|
|
3370
3904
|
const userId = requireUser(opts);
|
|
3371
3905
|
if (content.trim() === "") {
|
|
3372
|
-
console.error(
|
|
3906
|
+
console.error(chalk27.red("Memory content is required"));
|
|
3373
3907
|
process.exit(1);
|
|
3374
3908
|
}
|
|
3375
3909
|
const importance = parseImportance(opts.importance);
|
|
@@ -3400,17 +3934,17 @@ async function memoryClearCommand(opts) {
|
|
|
3400
3934
|
try {
|
|
3401
3935
|
const memories = await store.list(userId);
|
|
3402
3936
|
if (memories.length === 0) {
|
|
3403
|
-
console.log(
|
|
3937
|
+
console.log(chalk27.dim('No memories stored for user "' + userId + '".'));
|
|
3404
3938
|
return;
|
|
3405
3939
|
}
|
|
3406
|
-
const { confirm } = await
|
|
3940
|
+
const { confirm } = await prompt4({
|
|
3407
3941
|
type: "confirm",
|
|
3408
3942
|
name: "confirm",
|
|
3409
3943
|
message: "Delete all " + memories.length + ' memories for user "' + userId + '"?',
|
|
3410
3944
|
initial: false
|
|
3411
3945
|
});
|
|
3412
3946
|
if (!confirm) {
|
|
3413
|
-
console.log(
|
|
3947
|
+
console.log(chalk27.dim(" Cancelled."));
|
|
3414
3948
|
return;
|
|
3415
3949
|
}
|
|
3416
3950
|
await store.deleteAll(userId);
|
|
@@ -3423,7 +3957,7 @@ async function memoryExportCommand(opts) {
|
|
|
3423
3957
|
const userId = requireUser(opts);
|
|
3424
3958
|
const format = (opts.format ?? "json").toLowerCase();
|
|
3425
3959
|
if (format !== "json" && format !== "csv") {
|
|
3426
|
-
console.error(
|
|
3960
|
+
console.error(chalk27.red("--format must be 'json' or 'csv'"));
|
|
3427
3961
|
process.exit(1);
|
|
3428
3962
|
}
|
|
3429
3963
|
const store = resolveStore3();
|
|
@@ -3433,7 +3967,7 @@ async function memoryExportCommand(opts) {
|
|
|
3433
3967
|
if (opts.out) {
|
|
3434
3968
|
writeFileSync2(opts.out, body, "utf8");
|
|
3435
3969
|
console.log(
|
|
3436
|
-
|
|
3970
|
+
chalk27.green("\u2713") + " Wrote " + chalk27.bold(String(memories.length)) + " memor" + (memories.length === 1 ? "y" : "ies") + " to " + chalk27.bold(opts.out)
|
|
3437
3971
|
);
|
|
3438
3972
|
} else {
|
|
3439
3973
|
process.stdout.write(body);
|
|
@@ -3443,15 +3977,378 @@ async function memoryExportCommand(opts) {
|
|
|
3443
3977
|
}
|
|
3444
3978
|
}
|
|
3445
3979
|
|
|
3980
|
+
// src/commands/interrupts.ts
|
|
3981
|
+
import chalk29 from "chalk";
|
|
3982
|
+
import Enquirer5 from "enquirer";
|
|
3983
|
+
import { SecretsManager as SecretsManager12 } from "@tuttiai/core";
|
|
3984
|
+
|
|
3985
|
+
// src/commands/interrupts-render.ts
|
|
3986
|
+
import chalk28 from "chalk";
|
|
3987
|
+
function visibleLen4(s) {
|
|
3988
|
+
return s.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
3989
|
+
}
|
|
3990
|
+
function pad7(s, len) {
|
|
3991
|
+
const v = visibleLen4(s);
|
|
3992
|
+
return v >= len ? s : s + " ".repeat(len - v);
|
|
3993
|
+
}
|
|
3994
|
+
function truncate3(text, max) {
|
|
3995
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
3996
|
+
return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
|
|
3997
|
+
}
|
|
3998
|
+
function formatIsoShort2(d) {
|
|
3999
|
+
const iso = d.toISOString();
|
|
4000
|
+
return iso.slice(0, 10) + " " + iso.slice(11, 19);
|
|
4001
|
+
}
|
|
4002
|
+
function formatRelativeTime(requested_at, now = /* @__PURE__ */ new Date()) {
|
|
4003
|
+
const diffMs = now.getTime() - requested_at.getTime();
|
|
4004
|
+
if (diffMs < 0) return "now";
|
|
4005
|
+
const s = Math.floor(diffMs / 1e3);
|
|
4006
|
+
if (s < 60) return s + "s ago";
|
|
4007
|
+
const m = Math.floor(s / 60);
|
|
4008
|
+
if (m < 60) return m + "m ago";
|
|
4009
|
+
const h = Math.floor(m / 60);
|
|
4010
|
+
if (h < 24) return h + "h ago";
|
|
4011
|
+
const d = Math.floor(h / 24);
|
|
4012
|
+
return d + "d ago";
|
|
4013
|
+
}
|
|
4014
|
+
function truncateArgs(tool_args, max = 80) {
|
|
4015
|
+
let json;
|
|
4016
|
+
try {
|
|
4017
|
+
json = JSON.stringify(tool_args);
|
|
4018
|
+
} catch {
|
|
4019
|
+
json = String(tool_args);
|
|
4020
|
+
}
|
|
4021
|
+
if (json === void 0) return "";
|
|
4022
|
+
return truncate3(json, max);
|
|
4023
|
+
}
|
|
4024
|
+
function renderInterruptsList(interrupts, now = /* @__PURE__ */ new Date()) {
|
|
4025
|
+
if (interrupts.length === 0) {
|
|
4026
|
+
return chalk28.dim("No pending interrupts.");
|
|
4027
|
+
}
|
|
4028
|
+
const lines = [];
|
|
4029
|
+
lines.push("");
|
|
4030
|
+
lines.push(
|
|
4031
|
+
chalk28.dim(
|
|
4032
|
+
" " + pad7("ID", 10) + pad7("SESSION", 14) + pad7("TOOL", 22) + pad7("ARGS", 52) + "AGE"
|
|
4033
|
+
)
|
|
4034
|
+
);
|
|
4035
|
+
lines.push(chalk28.dim(" " + "\u2500".repeat(110)));
|
|
4036
|
+
for (const r of interrupts) {
|
|
4037
|
+
const idShort = r.interrupt_id.slice(0, 8);
|
|
4038
|
+
const sessionShort = r.session_id.slice(0, 12);
|
|
4039
|
+
const toolName = truncate3(r.tool_name, 20);
|
|
4040
|
+
const argsPreview = truncateArgs(r.tool_args, 50);
|
|
4041
|
+
const age = formatRelativeTime(r.requested_at, now);
|
|
4042
|
+
lines.push(
|
|
4043
|
+
" " + chalk28.bold(pad7(idShort, 10)) + pad7(sessionShort, 14) + pad7(chalk28.cyan(toolName), 22) + pad7(chalk28.dim(argsPreview), 52) + chalk28.dim(age)
|
|
4044
|
+
);
|
|
4045
|
+
}
|
|
4046
|
+
lines.push("");
|
|
4047
|
+
return lines.join("\n");
|
|
4048
|
+
}
|
|
4049
|
+
function renderInterruptDetail(interrupt, now = /* @__PURE__ */ new Date()) {
|
|
4050
|
+
const lines = [];
|
|
4051
|
+
lines.push("");
|
|
4052
|
+
lines.push(chalk28.bold("Interrupt ") + chalk28.dim(interrupt.interrupt_id));
|
|
4053
|
+
lines.push(chalk28.dim("\u2500".repeat(60)));
|
|
4054
|
+
lines.push(chalk28.dim("Session: ") + interrupt.session_id);
|
|
4055
|
+
lines.push(chalk28.dim("Tool: ") + chalk28.cyan(interrupt.tool_name));
|
|
4056
|
+
lines.push(
|
|
4057
|
+
chalk28.dim("Requested: ") + formatIsoShort2(interrupt.requested_at) + chalk28.dim(" (" + formatRelativeTime(interrupt.requested_at, now) + ")")
|
|
4058
|
+
);
|
|
4059
|
+
lines.push(chalk28.dim("Status: ") + colorStatus2(interrupt.status));
|
|
4060
|
+
if (interrupt.resolved_at) {
|
|
4061
|
+
lines.push(chalk28.dim("Resolved: ") + formatIsoShort2(interrupt.resolved_at));
|
|
4062
|
+
}
|
|
4063
|
+
if (interrupt.resolved_by) {
|
|
4064
|
+
lines.push(chalk28.dim("Resolved by: ") + interrupt.resolved_by);
|
|
4065
|
+
}
|
|
4066
|
+
if (interrupt.denial_reason) {
|
|
4067
|
+
lines.push(chalk28.dim("Reason: ") + interrupt.denial_reason);
|
|
4068
|
+
}
|
|
4069
|
+
lines.push("");
|
|
4070
|
+
lines.push(chalk28.dim("Arguments:"));
|
|
4071
|
+
lines.push(prettyJson(interrupt.tool_args));
|
|
4072
|
+
lines.push("");
|
|
4073
|
+
return lines.join("\n");
|
|
4074
|
+
}
|
|
4075
|
+
function prettyJson(value) {
|
|
4076
|
+
try {
|
|
4077
|
+
return JSON.stringify(value, null, 2);
|
|
4078
|
+
} catch {
|
|
4079
|
+
return String(value);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
function colorStatus2(status) {
|
|
4083
|
+
if (status === "approved") return chalk28.green("approved");
|
|
4084
|
+
if (status === "denied") return chalk28.red("denied");
|
|
4085
|
+
return chalk28.yellow("pending");
|
|
4086
|
+
}
|
|
4087
|
+
function renderApproved(interrupt) {
|
|
4088
|
+
return chalk28.green("\u2713") + " Approved " + chalk28.bold(interrupt.interrupt_id.slice(0, 8)) + chalk28.dim(" (" + interrupt.tool_name + ")") + (interrupt.resolved_by ? chalk28.dim(" by " + interrupt.resolved_by) : "");
|
|
4089
|
+
}
|
|
4090
|
+
function renderDenied(interrupt) {
|
|
4091
|
+
return chalk28.red("\u2717") + " Denied " + chalk28.bold(interrupt.interrupt_id.slice(0, 8)) + chalk28.dim(" (" + interrupt.tool_name + ")") + (interrupt.denial_reason ? chalk28.dim(' \u2014 "' + interrupt.denial_reason + '"') : "");
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
// src/commands/interrupts.ts
|
|
4095
|
+
var { prompt: prompt5 } = Enquirer5;
|
|
4096
|
+
var DEFAULT_SERVER_URL2 = "http://127.0.0.1:3847";
|
|
4097
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
4098
|
+
function resolveUrl2(opts) {
|
|
4099
|
+
return opts.url ?? SecretsManager12.optional("TUTTI_SERVER_URL") ?? DEFAULT_SERVER_URL2;
|
|
4100
|
+
}
|
|
4101
|
+
function resolveAuthHeader2(opts) {
|
|
4102
|
+
const key = opts.apiKey ?? SecretsManager12.optional("TUTTI_API_KEY");
|
|
4103
|
+
return key ? { Authorization: "Bearer " + key } : {};
|
|
4104
|
+
}
|
|
4105
|
+
function explainConnectionError2(err, baseUrl) {
|
|
4106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4107
|
+
console.error(chalk29.red("Failed to reach Tutti server at " + baseUrl));
|
|
4108
|
+
console.error(chalk29.dim(" " + msg));
|
|
4109
|
+
console.error(
|
|
4110
|
+
chalk29.dim(" Is `tutti-ai serve` running? Set --url or TUTTI_SERVER_URL to override.")
|
|
4111
|
+
);
|
|
4112
|
+
process.exit(1);
|
|
4113
|
+
}
|
|
4114
|
+
function reviveInterrupt(wire) {
|
|
4115
|
+
const req = {
|
|
4116
|
+
interrupt_id: wire["interrupt_id"],
|
|
4117
|
+
session_id: wire["session_id"],
|
|
4118
|
+
tool_name: wire["tool_name"],
|
|
4119
|
+
tool_args: wire["tool_args"],
|
|
4120
|
+
requested_at: new Date(wire["requested_at"]),
|
|
4121
|
+
status: wire["status"]
|
|
4122
|
+
};
|
|
4123
|
+
if (typeof wire["resolved_at"] === "string") {
|
|
4124
|
+
req.resolved_at = new Date(wire["resolved_at"]);
|
|
4125
|
+
}
|
|
4126
|
+
if (typeof wire["resolved_by"] === "string") {
|
|
4127
|
+
req.resolved_by = wire["resolved_by"];
|
|
4128
|
+
}
|
|
4129
|
+
if (typeof wire["denial_reason"] === "string") {
|
|
4130
|
+
req.denial_reason = wire["denial_reason"];
|
|
4131
|
+
}
|
|
4132
|
+
return req;
|
|
4133
|
+
}
|
|
4134
|
+
async function httpJson(opts, method, path, body) {
|
|
4135
|
+
const baseUrl = resolveUrl2(opts);
|
|
4136
|
+
const url = baseUrl.replace(/\/$/, "") + path;
|
|
4137
|
+
let res;
|
|
4138
|
+
try {
|
|
4139
|
+
res = await fetch(url, {
|
|
4140
|
+
method,
|
|
4141
|
+
headers: {
|
|
4142
|
+
"Content-Type": "application/json",
|
|
4143
|
+
...resolveAuthHeader2(opts)
|
|
4144
|
+
},
|
|
4145
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
4146
|
+
});
|
|
4147
|
+
} catch (err) {
|
|
4148
|
+
explainConnectionError2(err, baseUrl);
|
|
4149
|
+
}
|
|
4150
|
+
const text = await res.text();
|
|
4151
|
+
const parsed = text === "" ? null : JSON.parse(text);
|
|
4152
|
+
return { status: res.status, body: parsed };
|
|
4153
|
+
}
|
|
4154
|
+
async function fetchPending(opts) {
|
|
4155
|
+
const { status, body } = await httpJson(
|
|
4156
|
+
opts,
|
|
4157
|
+
"GET",
|
|
4158
|
+
"/interrupts/pending"
|
|
4159
|
+
);
|
|
4160
|
+
if (status === 401) {
|
|
4161
|
+
console.error(chalk29.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
4162
|
+
process.exit(1);
|
|
4163
|
+
}
|
|
4164
|
+
if (status === 503) {
|
|
4165
|
+
console.error(
|
|
4166
|
+
chalk29.red(
|
|
4167
|
+
"The server has no InterruptStore configured. Start `tutti-ai serve` with an interrupt store attached."
|
|
4168
|
+
)
|
|
4169
|
+
);
|
|
4170
|
+
process.exit(1);
|
|
4171
|
+
}
|
|
4172
|
+
if (status < 200 || status >= 300 || !("interrupts" in body)) {
|
|
4173
|
+
console.error(chalk29.red("Unexpected server response: " + status));
|
|
4174
|
+
process.exit(1);
|
|
4175
|
+
}
|
|
4176
|
+
return body.interrupts.map(reviveInterrupt);
|
|
4177
|
+
}
|
|
4178
|
+
async function postResolve(opts, interruptId, action, payload) {
|
|
4179
|
+
const { status, body } = await httpJson(
|
|
4180
|
+
opts,
|
|
4181
|
+
"POST",
|
|
4182
|
+
"/interrupts/" + encodeURIComponent(interruptId) + "/" + action,
|
|
4183
|
+
payload
|
|
4184
|
+
);
|
|
4185
|
+
if (status === 401) {
|
|
4186
|
+
console.error(chalk29.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
|
|
4187
|
+
process.exit(1);
|
|
4188
|
+
}
|
|
4189
|
+
if (status === 404) {
|
|
4190
|
+
console.error(chalk29.red('Interrupt "' + interruptId + '" not found.'));
|
|
4191
|
+
process.exit(1);
|
|
4192
|
+
}
|
|
4193
|
+
if (status === 409) {
|
|
4194
|
+
const current = body.current;
|
|
4195
|
+
const currentStatus = current && typeof current["status"] === "string" ? current["status"] : "resolved";
|
|
4196
|
+
console.error(
|
|
4197
|
+
chalk29.red("Interrupt already " + currentStatus + " \u2014 refusing to override.")
|
|
4198
|
+
);
|
|
4199
|
+
process.exit(1);
|
|
4200
|
+
}
|
|
4201
|
+
if (status < 200 || status >= 300) {
|
|
4202
|
+
console.error(chalk29.red("Unexpected server response: " + status));
|
|
4203
|
+
process.exit(1);
|
|
4204
|
+
}
|
|
4205
|
+
return reviveInterrupt(body);
|
|
4206
|
+
}
|
|
4207
|
+
async function interruptsListCommand(opts) {
|
|
4208
|
+
const pending = await fetchPending(opts);
|
|
4209
|
+
console.log(renderInterruptsList(pending));
|
|
4210
|
+
}
|
|
4211
|
+
async function interruptsApproveCommand(interruptId, opts) {
|
|
4212
|
+
const resolved = await postResolve(opts, interruptId, "approve", {
|
|
4213
|
+
...opts.resolvedBy !== void 0 ? { resolved_by: opts.resolvedBy } : {}
|
|
4214
|
+
});
|
|
4215
|
+
console.log(renderApproved(resolved));
|
|
4216
|
+
}
|
|
4217
|
+
async function interruptsDenyCommand(interruptId, opts) {
|
|
4218
|
+
const resolved = await postResolve(opts, interruptId, "deny", {
|
|
4219
|
+
...opts.reason !== void 0 ? { reason: opts.reason } : {},
|
|
4220
|
+
...opts.resolvedBy !== void 0 ? { resolved_by: opts.resolvedBy } : {}
|
|
4221
|
+
});
|
|
4222
|
+
console.log(renderDenied(resolved));
|
|
4223
|
+
}
|
|
4224
|
+
function clearScreen() {
|
|
4225
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
4226
|
+
}
|
|
4227
|
+
function readKey() {
|
|
4228
|
+
const input = process.stdin;
|
|
4229
|
+
if (!input.isTTY) return Promise.resolve(null);
|
|
4230
|
+
return new Promise((resolve20) => {
|
|
4231
|
+
input.setRawMode(true);
|
|
4232
|
+
input.resume();
|
|
4233
|
+
input.setEncoding("utf8");
|
|
4234
|
+
const onData = (data) => {
|
|
4235
|
+
input.removeListener("data", onData);
|
|
4236
|
+
input.setRawMode(false);
|
|
4237
|
+
input.pause();
|
|
4238
|
+
resolve20(data);
|
|
4239
|
+
};
|
|
4240
|
+
input.on("data", onData);
|
|
4241
|
+
});
|
|
4242
|
+
}
|
|
4243
|
+
async function readKeyOrTimeout(ms) {
|
|
4244
|
+
let timer;
|
|
4245
|
+
const timeout = new Promise((resolve20) => {
|
|
4246
|
+
timer = setTimeout(() => resolve20(null), ms);
|
|
4247
|
+
});
|
|
4248
|
+
try {
|
|
4249
|
+
const winner = await Promise.race([readKey(), timeout]);
|
|
4250
|
+
return winner;
|
|
4251
|
+
} finally {
|
|
4252
|
+
if (timer) clearTimeout(timer);
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
async function interruptsTUICommand(opts) {
|
|
4256
|
+
if (!process.stdin.isTTY) {
|
|
4257
|
+
await interruptsListCommand(opts);
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
const sigint = () => {
|
|
4261
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
4262
|
+
process.stdout.write("\n");
|
|
4263
|
+
process.exit(0);
|
|
4264
|
+
};
|
|
4265
|
+
process.on("SIGINT", sigint);
|
|
4266
|
+
try {
|
|
4267
|
+
for (; ; ) {
|
|
4268
|
+
const pending = await fetchPending(opts);
|
|
4269
|
+
clearScreen();
|
|
4270
|
+
console.log(
|
|
4271
|
+
chalk29.bold("Tutti \u2014 pending interrupts") + chalk29.dim(" (auto-refresh every " + POLL_INTERVAL_MS / 1e3 + "s)")
|
|
4272
|
+
);
|
|
4273
|
+
console.log(renderInterruptsList(pending));
|
|
4274
|
+
if (pending.length > 0) {
|
|
4275
|
+
console.log(
|
|
4276
|
+
chalk29.dim(
|
|
4277
|
+
"Press a number to inspect, 'r' to refresh, 'q' to quit."
|
|
4278
|
+
)
|
|
4279
|
+
);
|
|
4280
|
+
} else {
|
|
4281
|
+
console.log(chalk29.dim("Press 'r' to refresh, 'q' to quit."));
|
|
4282
|
+
}
|
|
4283
|
+
const indexed = pending.slice(0, 9);
|
|
4284
|
+
const key = await readKeyOrTimeout(POLL_INTERVAL_MS);
|
|
4285
|
+
if (key === null) continue;
|
|
4286
|
+
if (key === "q" || key === "") return;
|
|
4287
|
+
if (key === "r") continue;
|
|
4288
|
+
const digit = parseInt(key, 10);
|
|
4289
|
+
if (!Number.isNaN(digit) && digit >= 1 && digit <= indexed.length) {
|
|
4290
|
+
const chosen = indexed[digit - 1];
|
|
4291
|
+
const shouldContinue = await runDetailView(opts, chosen);
|
|
4292
|
+
if (!shouldContinue) return;
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
} finally {
|
|
4296
|
+
process.off("SIGINT", sigint);
|
|
4297
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
async function runDetailView(opts, interrupt) {
|
|
4301
|
+
clearScreen();
|
|
4302
|
+
console.log(renderInterruptDetail(interrupt));
|
|
4303
|
+
console.log(
|
|
4304
|
+
chalk29.dim(
|
|
4305
|
+
"Press 'a' to approve, 'd' to deny, 'q' to go back to the list."
|
|
4306
|
+
)
|
|
4307
|
+
);
|
|
4308
|
+
const key = await readKey();
|
|
4309
|
+
if (key === null || key === "q" || key === "") return true;
|
|
4310
|
+
if (key === "a") {
|
|
4311
|
+
const resolved = await postResolve(opts, interrupt.interrupt_id, "approve", {});
|
|
4312
|
+
clearScreen();
|
|
4313
|
+
console.log(renderApproved(resolved));
|
|
4314
|
+
await pause();
|
|
4315
|
+
return true;
|
|
4316
|
+
}
|
|
4317
|
+
if (key === "d") {
|
|
4318
|
+
const { reason } = await prompt5({
|
|
4319
|
+
type: "input",
|
|
4320
|
+
name: "reason",
|
|
4321
|
+
message: "Reason (optional):"
|
|
4322
|
+
});
|
|
4323
|
+
const payload = {};
|
|
4324
|
+
if (reason && reason.trim() !== "") payload["reason"] = reason.trim();
|
|
4325
|
+
const resolved = await postResolve(
|
|
4326
|
+
opts,
|
|
4327
|
+
interrupt.interrupt_id,
|
|
4328
|
+
"deny",
|
|
4329
|
+
payload
|
|
4330
|
+
);
|
|
4331
|
+
clearScreen();
|
|
4332
|
+
console.log(renderDenied(resolved));
|
|
4333
|
+
await pause();
|
|
4334
|
+
return true;
|
|
4335
|
+
}
|
|
4336
|
+
return true;
|
|
4337
|
+
}
|
|
4338
|
+
async function pause() {
|
|
4339
|
+
console.log(chalk29.dim("\nPress any key to continue..."));
|
|
4340
|
+
await readKey();
|
|
4341
|
+
}
|
|
4342
|
+
|
|
3446
4343
|
// src/index.ts
|
|
3447
4344
|
config();
|
|
3448
|
-
var
|
|
4345
|
+
var logger18 = createLogger18("tutti-cli");
|
|
3449
4346
|
process.on("unhandledRejection", (reason) => {
|
|
3450
|
-
|
|
4347
|
+
logger18.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
|
|
3451
4348
|
process.exit(1);
|
|
3452
4349
|
});
|
|
3453
4350
|
process.on("uncaughtException", (err) => {
|
|
3454
|
-
|
|
4351
|
+
logger18.error({ error: err.message }, "Fatal error");
|
|
3455
4352
|
process.exit(1);
|
|
3456
4353
|
});
|
|
3457
4354
|
var program = new Command();
|
|
@@ -3508,9 +4405,30 @@ program.command("voices").description("List all available official voices and in
|
|
|
3508
4405
|
program.command("publish").description("Publish the current voice to npm and the voice registry").option("--dry-run", "Run all checks without publishing").action(async (opts) => {
|
|
3509
4406
|
await publishCommand(opts);
|
|
3510
4407
|
});
|
|
3511
|
-
program.command("eval
|
|
4408
|
+
var evalCmd = 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) => {
|
|
4409
|
+
if (!suitePath) {
|
|
4410
|
+
console.error(
|
|
4411
|
+
"Usage: tutti-ai eval <suite-file>\n tutti-ai eval record <session-id>\n tutti-ai eval list"
|
|
4412
|
+
);
|
|
4413
|
+
process.exit(1);
|
|
4414
|
+
}
|
|
3512
4415
|
await evalCommand(suitePath, opts);
|
|
3513
4416
|
});
|
|
4417
|
+
evalCmd.command("record <session-id>").description("Promote a past session run to a golden eval case").action(async (sessionId) => {
|
|
4418
|
+
await evalRecordCommand(sessionId);
|
|
4419
|
+
});
|
|
4420
|
+
evalCmd.command("list").description("Show every golden case + latest-run status").action(async () => {
|
|
4421
|
+
await evalListCommand();
|
|
4422
|
+
});
|
|
4423
|
+
evalCmd.command("run").description("Run every golden case (optionally filtered) and report pass/fail").option("--case <id>", "Run only the case with this id (full or 8-char prefix)").option("--tag <tag>", "Run only cases carrying this tag").option("--ci", "Write JUnit XML + exit 1 on failure + drop colors").option("-s, --score <path>", "Path to score file (default: ./tutti.score.ts)").action(async (opts) => {
|
|
4424
|
+
const resolved = {
|
|
4425
|
+
...opts.case !== void 0 ? { case: opts.case } : {},
|
|
4426
|
+
...opts.tag !== void 0 ? { tag: opts.tag } : {},
|
|
4427
|
+
...opts.ci !== void 0 ? { ci: opts.ci } : {},
|
|
4428
|
+
...opts.score !== void 0 ? { score: opts.score } : {}
|
|
4429
|
+
};
|
|
4430
|
+
await evalRunCommand(resolved);
|
|
4431
|
+
});
|
|
3514
4432
|
program.command("update").description("Update @tuttiai packages to their latest versions").action(() => {
|
|
3515
4433
|
updateCommand();
|
|
3516
4434
|
});
|
|
@@ -3593,5 +4511,34 @@ memoryCmd.command("export").description("Export every memory for a user as JSON
|
|
|
3593
4511
|
...opts.out !== void 0 ? { out: opts.out } : {}
|
|
3594
4512
|
});
|
|
3595
4513
|
});
|
|
4514
|
+
function toInterruptsOptions(raw) {
|
|
4515
|
+
return {
|
|
4516
|
+
...raw.url !== void 0 ? { url: raw.url } : {},
|
|
4517
|
+
...raw.apiKey !== void 0 ? { apiKey: raw.apiKey } : {}
|
|
4518
|
+
};
|
|
4519
|
+
}
|
|
4520
|
+
var interruptsCmd = program.command("interrupts").description("Review and resolve approval-gated tool calls");
|
|
4521
|
+
interruptsCmd.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) => {
|
|
4522
|
+
await interruptsTUICommand(toInterruptsOptions(opts));
|
|
4523
|
+
});
|
|
4524
|
+
interruptsCmd.command("list").description("Print pending interrupts as a table and exit (script-friendly)").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) => {
|
|
4525
|
+
await interruptsListCommand(toInterruptsOptions(opts));
|
|
4526
|
+
});
|
|
4527
|
+
interruptsCmd.command("approve <interrupt-id>").description("Approve an interrupt directly").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)").option("--by <name>", "Reviewer identifier (resolved_by field)").action(async (id, opts) => {
|
|
4528
|
+
await interruptsApproveCommand(id, {
|
|
4529
|
+
...toInterruptsOptions(opts),
|
|
4530
|
+
...opts.by !== void 0 ? { resolvedBy: opts.by } : {}
|
|
4531
|
+
});
|
|
4532
|
+
});
|
|
4533
|
+
interruptsCmd.command("deny <interrupt-id>").description("Deny an interrupt directly").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)").option("--reason <text>", "Free-text denial reason").option("--by <name>", "Reviewer identifier (resolved_by field)").action(async (id, opts) => {
|
|
4534
|
+
await interruptsDenyCommand(id, {
|
|
4535
|
+
...toInterruptsOptions(opts),
|
|
4536
|
+
...opts.reason !== void 0 ? { reason: opts.reason } : {},
|
|
4537
|
+
...opts.by !== void 0 ? { resolvedBy: opts.by } : {}
|
|
4538
|
+
});
|
|
4539
|
+
});
|
|
4540
|
+
program.command("approve").description("Alias for `tutti-ai interrupts` \u2014 interactive approval TUI").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) => {
|
|
4541
|
+
await interruptsTUICommand(toInterruptsOptions(opts));
|
|
4542
|
+
});
|
|
3596
4543
|
program.parse();
|
|
3597
4544
|
//# sourceMappingURL=index.js.map
|