@tuttiai/cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -231,6 +231,96 @@ Resume from turn 2? (y/n) y
231
231
  Final turn: 3
232
232
  ```
233
233
 
234
+ ### `tutti-ai schedule [score]`
235
+
236
+ Start the scheduler daemon — reads a score file, registers all agents
237
+ that have a `schedule` config, and runs on their configured triggers
238
+ (cron, interval, or one-shot datetime) until the process is killed.
239
+
240
+ ```bash
241
+ tutti-ai schedule # defaults to ./tutti.score.ts
242
+ tutti-ai schedule ./custom-score.ts # specify a score file
243
+ ```
244
+
245
+ Score file example:
246
+
247
+ ```ts
248
+ const score = defineScore({
249
+ provider: new AnthropicProvider(),
250
+ agents: {
251
+ reporter: {
252
+ name: "Reporter",
253
+ system_prompt: "Generate a daily status report.",
254
+ voices: [],
255
+ schedule: {
256
+ cron: "0 9 * * *", // 9 AM daily
257
+ input: "Generate the daily status report",
258
+ max_runs: 30, // auto-disable after 30 runs
259
+ },
260
+ },
261
+ },
262
+ });
263
+ ```
264
+
265
+ Environment:
266
+
267
+ | Variable | Required | Description |
268
+ |---|---|---|
269
+ | `TUTTI_PG_URL` | Recommended | PostgreSQL URL for durable schedule persistence. Falls back to in-memory (lost on restart). |
270
+
271
+ The daemon logs `schedule:triggered`, `schedule:completed`, and
272
+ `schedule:error` events to stdout with timestamps.
273
+
274
+ ### `tutti-ai schedules list`
275
+
276
+ Show all registered schedules:
277
+
278
+ ```bash
279
+ tutti-ai schedules list
280
+ ```
281
+
282
+ Output:
283
+
284
+ ```
285
+ ID AGENT TRIGGER ENABLED RUNS CREATED
286
+ ──────────────────────────────────────────────────────────────────────────────────────
287
+ nightly-report reporter cron: 0 9 * * * yes 12 2026-04-14
288
+ health-check monitor every 30m yes 48/100 2026-04-14
289
+ ```
290
+
291
+ ### `tutti-ai schedules enable <id>`
292
+
293
+ Re-enable a disabled schedule:
294
+
295
+ ```bash
296
+ tutti-ai schedules enable nightly-report
297
+ ```
298
+
299
+ ### `tutti-ai schedules disable <id>`
300
+
301
+ Disable a schedule without deleting it:
302
+
303
+ ```bash
304
+ tutti-ai schedules disable nightly-report
305
+ ```
306
+
307
+ ### `tutti-ai schedules trigger <id>`
308
+
309
+ Manually trigger a scheduled run immediately (useful for testing):
310
+
311
+ ```bash
312
+ tutti-ai schedules trigger nightly-report
313
+ tutti-ai schedules trigger nightly-report --score ./custom-score.ts
314
+ ```
315
+
316
+ ### `tutti-ai schedules runs <id>`
317
+
318
+ Show run history for a schedule (last 20 runs):
319
+
320
+ ```bash
321
+ tutti-ai schedules runs nightly-report
322
+ ```
323
+
234
324
  ## Links
235
325
 
236
326
  - [Tutti](https://tutti-ai.com)
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 createLogger11 } from "@tuttiai/core";
5
+ import { createLogger as createLogger13 } from "@tuttiai/core";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/init.ts
@@ -1782,22 +1782,24 @@ async function serveCommand(scorePath, options = {}) {
1782
1782
  reactive.on("file-change", () => {
1783
1783
  console.log(chalk10.cyan("\n[tutti] Score changed, reloading..."));
1784
1784
  });
1785
- reactive.on("reloaded", async () => {
1786
- try {
1787
- const nextScore = reactive.current;
1788
- const nextRuntime = buildRuntime2(nextScore, sharedSessions);
1789
- const nextApp = await buildApp(nextRuntime, agentName, port, host, options.apiKey);
1790
- await app.close();
1791
- runtime = nextRuntime;
1792
- app = nextApp;
1793
- await app.listen({ port, host });
1794
- console.log(chalk10.green("[tutti] Score reloaded. Server restarted."));
1795
- } catch (err) {
1796
- logger10.error(
1797
- { error: err instanceof Error ? err.message : String(err) },
1798
- "[tutti] Reload failed \u2014 server continues with previous config"
1799
- );
1800
- }
1785
+ reactive.on("reloaded", () => {
1786
+ void (async () => {
1787
+ try {
1788
+ const nextScore = reactive.current;
1789
+ const nextRuntime = buildRuntime2(nextScore, sharedSessions);
1790
+ const nextApp = await buildApp(nextRuntime, agentName, port, host, options.apiKey);
1791
+ await app.close();
1792
+ runtime = nextRuntime;
1793
+ app = nextApp;
1794
+ await app.listen({ port, host });
1795
+ console.log(chalk10.green("[tutti] Score reloaded. Server restarted."));
1796
+ } catch (err) {
1797
+ logger10.error(
1798
+ { error: err instanceof Error ? err.message : String(err) },
1799
+ "[tutti] Reload failed \u2014 server continues with previous config"
1800
+ );
1801
+ }
1802
+ })();
1801
1803
  });
1802
1804
  reactive.on("reload-failed", (err) => {
1803
1805
  logger10.error(
@@ -1863,19 +1865,287 @@ function printBanner(port, host, agentName, score, file, watch) {
1863
1865
  console.log();
1864
1866
  }
1865
1867
 
1868
+ // src/commands/schedule.ts
1869
+ import { existsSync as existsSync11 } from "fs";
1870
+ import { resolve as resolve11 } from "path";
1871
+ import chalk11 from "chalk";
1872
+ import {
1873
+ ScoreLoader as ScoreLoader7,
1874
+ SchedulerEngine,
1875
+ PostgresScheduleStore,
1876
+ MemoryScheduleStore,
1877
+ AgentRunner,
1878
+ EventBus,
1879
+ InMemorySessionStore as InMemorySessionStore3,
1880
+ SecretsManager as SecretsManager6,
1881
+ createLogger as createLogger11
1882
+ } from "@tuttiai/core";
1883
+ var logger11 = createLogger11("tutti-cli");
1884
+ function resolveStore() {
1885
+ const pgUrl = SecretsManager6.optional("TUTTI_PG_URL");
1886
+ if (pgUrl) {
1887
+ return new PostgresScheduleStore({ connection_string: pgUrl });
1888
+ }
1889
+ logger11.warn("TUTTI_PG_URL not set \u2014 using in-memory store (not durable across restarts)");
1890
+ return new MemoryScheduleStore();
1891
+ }
1892
+ async function scheduleCommand(scorePath) {
1893
+ const file = resolve11(scorePath ?? "./tutti.score.ts");
1894
+ if (!existsSync11(file)) {
1895
+ console.error(chalk11.red("Score file not found: " + file));
1896
+ console.error(chalk11.dim('Run "tutti-ai init" to create a new project.'));
1897
+ process.exit(1);
1898
+ }
1899
+ const score = await ScoreLoader7.load(file);
1900
+ const events = new EventBus();
1901
+ const sessions = new InMemorySessionStore3();
1902
+ const runner = new AgentRunner(
1903
+ score.provider,
1904
+ events,
1905
+ sessions
1906
+ );
1907
+ const store = resolveStore();
1908
+ const engine = new SchedulerEngine(store, runner, events);
1909
+ let registered = 0;
1910
+ for (const [agentId, agent] of Object.entries(score.agents)) {
1911
+ if (!agent.schedule) continue;
1912
+ const resolvedAgent = agent.model ? agent : { ...agent, model: score.default_model ?? "claude-sonnet-4-20250514" };
1913
+ await engine.schedule(agentId, resolvedAgent, agent.schedule);
1914
+ registered++;
1915
+ }
1916
+ if (registered === 0) {
1917
+ console.log(chalk11.yellow("No agents have a schedule config. Nothing to run."));
1918
+ console.log(chalk11.dim("Add schedule: { cron: '...', input: '...' } to an agent in your score."));
1919
+ process.exit(0);
1920
+ }
1921
+ events.onAny((e) => {
1922
+ if (e.type === "schedule:triggered") {
1923
+ const ev = e;
1924
+ console.log(
1925
+ chalk11.dim((/* @__PURE__ */ new Date()).toISOString()) + " " + chalk11.cyan("triggered") + " " + chalk11.bold(ev.schedule_id) + " \u2192 " + ev.agent_name
1926
+ );
1927
+ }
1928
+ if (e.type === "schedule:completed") {
1929
+ const ev = e;
1930
+ console.log(
1931
+ chalk11.dim((/* @__PURE__ */ new Date()).toISOString()) + " " + chalk11.green("completed") + " " + chalk11.bold(ev.schedule_id) + " " + chalk11.dim("(" + ev.duration_ms + "ms)")
1932
+ );
1933
+ }
1934
+ if (e.type === "schedule:error") {
1935
+ const ev = e;
1936
+ console.log(
1937
+ chalk11.dim((/* @__PURE__ */ new Date()).toISOString()) + " " + chalk11.red("error") + " " + chalk11.bold(ev.schedule_id) + " \u2014 " + ev.error.message
1938
+ );
1939
+ }
1940
+ });
1941
+ engine.start();
1942
+ console.log("");
1943
+ console.log(chalk11.cyan.bold(" Tutti Scheduler"));
1944
+ console.log(chalk11.dim(" Score: " + (score.name ?? file)));
1945
+ console.log(chalk11.dim(" Schedules: " + registered));
1946
+ console.log(chalk11.dim(" Store: " + (SecretsManager6.optional("TUTTI_PG_URL") ? "postgres" : "memory")));
1947
+ console.log("");
1948
+ console.log(chalk11.dim(" Press Ctrl+C to stop."));
1949
+ console.log("");
1950
+ const shutdown = () => {
1951
+ console.log(chalk11.dim("\n Shutting down scheduler..."));
1952
+ engine.stop();
1953
+ if ("close" in store && typeof store.close === "function") {
1954
+ void store.close();
1955
+ }
1956
+ process.exit(0);
1957
+ };
1958
+ process.on("SIGINT", shutdown);
1959
+ process.on("SIGTERM", shutdown);
1960
+ await new Promise(() => void 0);
1961
+ }
1962
+
1963
+ // src/commands/schedules.ts
1964
+ import { existsSync as existsSync12 } from "fs";
1965
+ import { resolve as resolve12 } from "path";
1966
+ import chalk12 from "chalk";
1967
+ import {
1968
+ ScoreLoader as ScoreLoader8,
1969
+ SchedulerEngine as SchedulerEngine2,
1970
+ PostgresScheduleStore as PostgresScheduleStore2,
1971
+ MemoryScheduleStore as MemoryScheduleStore2,
1972
+ AgentRunner as AgentRunner2,
1973
+ EventBus as EventBus2,
1974
+ InMemorySessionStore as InMemorySessionStore4,
1975
+ SecretsManager as SecretsManager7,
1976
+ createLogger as createLogger12
1977
+ } from "@tuttiai/core";
1978
+ var logger12 = createLogger12("tutti-cli");
1979
+ function resolveStore2() {
1980
+ const pgUrl = SecretsManager7.optional("TUTTI_PG_URL");
1981
+ if (pgUrl) {
1982
+ return new PostgresScheduleStore2({ connection_string: pgUrl });
1983
+ }
1984
+ logger12.warn("TUTTI_PG_URL not set \u2014 using in-memory store (schedules are ephemeral)");
1985
+ return new MemoryScheduleStore2();
1986
+ }
1987
+ async function closeStore(store) {
1988
+ if ("close" in store && typeof store.close === "function") {
1989
+ await store.close();
1990
+ }
1991
+ }
1992
+ function formatTrigger(r) {
1993
+ if (r.config.cron) return "cron: " + r.config.cron;
1994
+ if (r.config.every) return "every " + r.config.every;
1995
+ if (r.config.at) return "at " + r.config.at;
1996
+ return "?";
1997
+ }
1998
+ function pad(s, len) {
1999
+ return s.length >= len ? s : s + " ".repeat(len - s.length);
2000
+ }
2001
+ async function schedulesListCommand() {
2002
+ const store = resolveStore2();
2003
+ try {
2004
+ const records = await store.list();
2005
+ if (records.length === 0) {
2006
+ console.log(chalk12.dim("No schedules found."));
2007
+ console.log(chalk12.dim('Run "tutti-ai schedule" to start the scheduler daemon.'));
2008
+ return;
2009
+ }
2010
+ console.log("");
2011
+ console.log(
2012
+ chalk12.dim(
2013
+ " " + pad("ID", 20) + pad("AGENT", 16) + pad("TRIGGER", 22) + pad("ENABLED", 10) + pad("RUNS", 8) + "CREATED"
2014
+ )
2015
+ );
2016
+ console.log(chalk12.dim(" " + "\u2500".repeat(90)));
2017
+ for (const r of records) {
2018
+ const enabled = r.enabled ? chalk12.green("yes") : chalk12.red("no") + " ";
2019
+ const maxLabel = r.config.max_runs ? r.run_count + "/" + r.config.max_runs : String(r.run_count);
2020
+ console.log(
2021
+ " " + chalk12.bold(pad(r.id, 20)) + pad(r.agent_id, 16) + pad(formatTrigger(r), 22) + pad(enabled, 10) + pad(maxLabel, 8) + chalk12.dim(r.created_at.toISOString().slice(0, 10))
2022
+ );
2023
+ }
2024
+ console.log("");
2025
+ } finally {
2026
+ await closeStore(store);
2027
+ }
2028
+ }
2029
+ async function schedulesEnableCommand(id) {
2030
+ const store = resolveStore2();
2031
+ try {
2032
+ const record = await store.get(id);
2033
+ if (!record) {
2034
+ console.error(chalk12.red('Schedule "' + id + '" not found.'));
2035
+ process.exit(1);
2036
+ }
2037
+ await store.setEnabled(id, true);
2038
+ console.log(chalk12.green('Schedule "' + id + '" enabled.'));
2039
+ } finally {
2040
+ await closeStore(store);
2041
+ }
2042
+ }
2043
+ async function schedulesDisableCommand(id) {
2044
+ const store = resolveStore2();
2045
+ try {
2046
+ const record = await store.get(id);
2047
+ if (!record) {
2048
+ console.error(chalk12.red('Schedule "' + id + '" not found.'));
2049
+ process.exit(1);
2050
+ }
2051
+ await store.setEnabled(id, false);
2052
+ console.log(chalk12.yellow('Schedule "' + id + '" disabled.'));
2053
+ } finally {
2054
+ await closeStore(store);
2055
+ }
2056
+ }
2057
+ async function schedulesTriggerCommand(id, scorePath) {
2058
+ const file = resolve12(scorePath ?? "./tutti.score.ts");
2059
+ if (!existsSync12(file)) {
2060
+ console.error(chalk12.red("Score file not found: " + file));
2061
+ process.exit(1);
2062
+ }
2063
+ const score = await ScoreLoader8.load(file);
2064
+ const events = new EventBus2();
2065
+ const sessions = new InMemorySessionStore4();
2066
+ const runner = new AgentRunner2(score.provider, events, sessions);
2067
+ const store = resolveStore2();
2068
+ try {
2069
+ const record = await store.get(id);
2070
+ if (!record) {
2071
+ console.error(chalk12.red('Schedule "' + id + '" not found.'));
2072
+ process.exit(1);
2073
+ }
2074
+ const agent = score.agents[record.agent_id];
2075
+ if (!agent) {
2076
+ console.error(chalk12.red('Agent "' + record.agent_id + '" not found in score.'));
2077
+ process.exit(1);
2078
+ }
2079
+ const resolvedAgent = agent.model ? agent : { ...agent, model: score.default_model ?? "claude-sonnet-4-20250514" };
2080
+ const engine = new SchedulerEngine2(store, runner, events);
2081
+ await engine.schedule(id, resolvedAgent, record.config);
2082
+ engine.start();
2083
+ console.log(chalk12.cyan('Triggering "' + id + '" (' + record.agent_id + ")..."));
2084
+ const run2 = await engine.trigger(id);
2085
+ engine.stop();
2086
+ if (run2.error) {
2087
+ console.log(chalk12.red(" Error: " + run2.error));
2088
+ process.exit(1);
2089
+ }
2090
+ const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() : 0;
2091
+ console.log(chalk12.green(" Completed in " + duration + "ms"));
2092
+ if (run2.result) {
2093
+ const preview = run2.result.length > 200 ? run2.result.slice(0, 200) + "..." : run2.result;
2094
+ console.log(chalk12.dim(" Output: " + preview));
2095
+ }
2096
+ } finally {
2097
+ await closeStore(store);
2098
+ }
2099
+ }
2100
+ async function schedulesRunsCommand(id) {
2101
+ const store = resolveStore2();
2102
+ try {
2103
+ const record = await store.get(id);
2104
+ if (!record) {
2105
+ console.error(chalk12.red('Schedule "' + id + '" not found.'));
2106
+ process.exit(1);
2107
+ }
2108
+ if ("getRuns" in store && typeof store.getRuns === "function") {
2109
+ const runs = store.getRuns(id);
2110
+ if (runs.length === 0) {
2111
+ console.log(chalk12.dim("No runs recorded for this schedule."));
2112
+ return;
2113
+ }
2114
+ const recent = runs.slice(-20);
2115
+ console.log("");
2116
+ console.log(chalk12.dim(" Showing last " + recent.length + " of " + runs.length + " runs:"));
2117
+ console.log("");
2118
+ for (const run2 of recent) {
2119
+ const duration = run2.completed_at && run2.triggered_at ? run2.completed_at.getTime() - run2.triggered_at.getTime() + "ms" : "?";
2120
+ const status = run2.error ? chalk12.red("error") : chalk12.green("ok");
2121
+ const preview = run2.error ? run2.error.slice(0, 80) : (run2.result ?? "").slice(0, 80);
2122
+ console.log(
2123
+ " " + chalk12.dim(run2.triggered_at.toISOString()) + " " + status + " " + chalk12.dim(duration) + " " + preview
2124
+ );
2125
+ }
2126
+ console.log("");
2127
+ } else {
2128
+ console.log(chalk12.dim('Schedule "' + id + '" has completed ' + record.run_count + " runs."));
2129
+ console.log(chalk12.dim("Full run history requires the MemoryScheduleStore or a future tutti_schedule_runs table."));
2130
+ }
2131
+ } finally {
2132
+ await closeStore(store);
2133
+ }
2134
+ }
2135
+
1866
2136
  // src/index.ts
1867
2137
  config();
1868
- var logger11 = createLogger11("tutti-cli");
2138
+ var logger13 = createLogger13("tutti-cli");
1869
2139
  process.on("unhandledRejection", (reason) => {
1870
- logger11.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
2140
+ logger13.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
1871
2141
  process.exit(1);
1872
2142
  });
1873
2143
  process.on("uncaughtException", (err) => {
1874
- logger11.error({ error: err.message }, "Fatal error");
2144
+ logger13.error({ error: err.message }, "Fatal error");
1875
2145
  process.exit(1);
1876
2146
  });
1877
2147
  var program = new Command();
1878
- program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.11.0");
2148
+ program.name("tutti-ai").description("Tutti \u2014 multi-agent orchestration. All agents. All together.").version("0.12.0");
1879
2149
  program.command("init [project-name]").description("Create a new Tutti project").option("-t, --template <id>", "Project template to use").action(async (projectName, opts) => {
1880
2150
  await initCommand(projectName, opts.template);
1881
2151
  });
@@ -1931,5 +2201,24 @@ program.command("publish").description("Publish the current voice to npm and the
1931
2201
  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) => {
1932
2202
  await evalCommand(suitePath, opts);
1933
2203
  });
2204
+ program.command("schedule [score]").description("Start the scheduler daemon \u2014 runs agents on their configured schedules").action(async (score) => {
2205
+ await scheduleCommand(score);
2206
+ });
2207
+ var schedulesCmd = program.command("schedules").description("Manage scheduled agents");
2208
+ schedulesCmd.command("list").description("Show all registered schedules").action(async () => {
2209
+ await schedulesListCommand();
2210
+ });
2211
+ schedulesCmd.command("enable <id>").description("Enable a disabled schedule").action(async (id) => {
2212
+ await schedulesEnableCommand(id);
2213
+ });
2214
+ schedulesCmd.command("disable <id>").description("Disable a schedule without deleting it").action(async (id) => {
2215
+ await schedulesDisableCommand(id);
2216
+ });
2217
+ schedulesCmd.command("trigger <id>").description("Manually trigger a scheduled run immediately").option("-s, --score <path>", "Path to score file (default: ./tutti.score.ts)").action(async (id, opts) => {
2218
+ await schedulesTriggerCommand(id, opts.score);
2219
+ });
2220
+ schedulesCmd.command("runs <id>").description("Show run history for a schedule (last 20 runs)").action(async (id) => {
2221
+ await schedulesRunsCommand(id);
2222
+ });
1934
2223
  program.parse();
1935
2224
  //# sourceMappingURL=index.js.map