@tuttiai/cli 0.15.0 → 0.16.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 CHANGED
@@ -2603,8 +2603,8 @@ async function replayCommand(sessionId, opts = {}) {
2603
2603
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
2604
2604
  try {
2605
2605
  while (true) {
2606
- const prompt4 = chalk17.cyan("replay [" + cursor + "/" + (messages.length - 1) + "]> ");
2607
- const raw = await rl.question(prompt4);
2606
+ const prompt5 = chalk17.cyan("replay [" + cursor + "/" + (messages.length - 1) + "]> ");
2607
+ const raw = await rl.question(prompt5);
2608
2608
  const input = raw.trim();
2609
2609
  if (!input) continue;
2610
2610
  const [cmd, ...args] = input.split(/\s+/);
@@ -3443,6 +3443,369 @@ async function memoryExportCommand(opts) {
3443
3443
  }
3444
3444
  }
3445
3445
 
3446
+ // src/commands/interrupts.ts
3447
+ import chalk24 from "chalk";
3448
+ import Enquirer4 from "enquirer";
3449
+ import { SecretsManager as SecretsManager11 } from "@tuttiai/core";
3450
+
3451
+ // src/commands/interrupts-render.ts
3452
+ import chalk23 from "chalk";
3453
+ function visibleLen3(s) {
3454
+ return s.replace(/\u001b\[[0-9;]*m/g, "").length;
3455
+ }
3456
+ function pad6(s, len) {
3457
+ const v = visibleLen3(s);
3458
+ return v >= len ? s : s + " ".repeat(len - v);
3459
+ }
3460
+ function truncate2(text, max) {
3461
+ const oneLine = text.replace(/\s+/g, " ").trim();
3462
+ return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
3463
+ }
3464
+ function formatIsoShort(d) {
3465
+ const iso = d.toISOString();
3466
+ return iso.slice(0, 10) + " " + iso.slice(11, 19);
3467
+ }
3468
+ function formatRelativeTime(requested_at, now = /* @__PURE__ */ new Date()) {
3469
+ const diffMs = now.getTime() - requested_at.getTime();
3470
+ if (diffMs < 0) return "now";
3471
+ const s = Math.floor(diffMs / 1e3);
3472
+ if (s < 60) return s + "s ago";
3473
+ const m = Math.floor(s / 60);
3474
+ if (m < 60) return m + "m ago";
3475
+ const h = Math.floor(m / 60);
3476
+ if (h < 24) return h + "h ago";
3477
+ const d = Math.floor(h / 24);
3478
+ return d + "d ago";
3479
+ }
3480
+ function truncateArgs(tool_args, max = 80) {
3481
+ let json;
3482
+ try {
3483
+ json = JSON.stringify(tool_args);
3484
+ } catch {
3485
+ json = String(tool_args);
3486
+ }
3487
+ if (json === void 0) return "";
3488
+ return truncate2(json, max);
3489
+ }
3490
+ function renderInterruptsList(interrupts, now = /* @__PURE__ */ new Date()) {
3491
+ if (interrupts.length === 0) {
3492
+ return chalk23.dim("No pending interrupts.");
3493
+ }
3494
+ const lines = [];
3495
+ lines.push("");
3496
+ lines.push(
3497
+ chalk23.dim(
3498
+ " " + pad6("ID", 10) + pad6("SESSION", 14) + pad6("TOOL", 22) + pad6("ARGS", 52) + "AGE"
3499
+ )
3500
+ );
3501
+ lines.push(chalk23.dim(" " + "\u2500".repeat(110)));
3502
+ for (const r of interrupts) {
3503
+ const idShort = r.interrupt_id.slice(0, 8);
3504
+ const sessionShort = r.session_id.slice(0, 12);
3505
+ const toolName = truncate2(r.tool_name, 20);
3506
+ const argsPreview = truncateArgs(r.tool_args, 50);
3507
+ const age = formatRelativeTime(r.requested_at, now);
3508
+ lines.push(
3509
+ " " + chalk23.bold(pad6(idShort, 10)) + pad6(sessionShort, 14) + pad6(chalk23.cyan(toolName), 22) + pad6(chalk23.dim(argsPreview), 52) + chalk23.dim(age)
3510
+ );
3511
+ }
3512
+ lines.push("");
3513
+ return lines.join("\n");
3514
+ }
3515
+ function renderInterruptDetail(interrupt, now = /* @__PURE__ */ new Date()) {
3516
+ const lines = [];
3517
+ lines.push("");
3518
+ lines.push(chalk23.bold("Interrupt ") + chalk23.dim(interrupt.interrupt_id));
3519
+ lines.push(chalk23.dim("\u2500".repeat(60)));
3520
+ lines.push(chalk23.dim("Session: ") + interrupt.session_id);
3521
+ lines.push(chalk23.dim("Tool: ") + chalk23.cyan(interrupt.tool_name));
3522
+ lines.push(
3523
+ chalk23.dim("Requested: ") + formatIsoShort(interrupt.requested_at) + chalk23.dim(" (" + formatRelativeTime(interrupt.requested_at, now) + ")")
3524
+ );
3525
+ lines.push(chalk23.dim("Status: ") + colorStatus2(interrupt.status));
3526
+ if (interrupt.resolved_at) {
3527
+ lines.push(chalk23.dim("Resolved: ") + formatIsoShort(interrupt.resolved_at));
3528
+ }
3529
+ if (interrupt.resolved_by) {
3530
+ lines.push(chalk23.dim("Resolved by: ") + interrupt.resolved_by);
3531
+ }
3532
+ if (interrupt.denial_reason) {
3533
+ lines.push(chalk23.dim("Reason: ") + interrupt.denial_reason);
3534
+ }
3535
+ lines.push("");
3536
+ lines.push(chalk23.dim("Arguments:"));
3537
+ lines.push(prettyJson(interrupt.tool_args));
3538
+ lines.push("");
3539
+ return lines.join("\n");
3540
+ }
3541
+ function prettyJson(value) {
3542
+ try {
3543
+ return JSON.stringify(value, null, 2);
3544
+ } catch {
3545
+ return String(value);
3546
+ }
3547
+ }
3548
+ function colorStatus2(status) {
3549
+ if (status === "approved") return chalk23.green("approved");
3550
+ if (status === "denied") return chalk23.red("denied");
3551
+ return chalk23.yellow("pending");
3552
+ }
3553
+ function renderApproved(interrupt) {
3554
+ return chalk23.green("\u2713") + " Approved " + chalk23.bold(interrupt.interrupt_id.slice(0, 8)) + chalk23.dim(" (" + interrupt.tool_name + ")") + (interrupt.resolved_by ? chalk23.dim(" by " + interrupt.resolved_by) : "");
3555
+ }
3556
+ function renderDenied(interrupt) {
3557
+ return chalk23.red("\u2717") + " Denied " + chalk23.bold(interrupt.interrupt_id.slice(0, 8)) + chalk23.dim(" (" + interrupt.tool_name + ")") + (interrupt.denial_reason ? chalk23.dim(' \u2014 "' + interrupt.denial_reason + '"') : "");
3558
+ }
3559
+
3560
+ // src/commands/interrupts.ts
3561
+ var { prompt: prompt4 } = Enquirer4;
3562
+ var DEFAULT_SERVER_URL2 = "http://127.0.0.1:3847";
3563
+ var POLL_INTERVAL_MS = 2e3;
3564
+ function resolveUrl2(opts) {
3565
+ return opts.url ?? SecretsManager11.optional("TUTTI_SERVER_URL") ?? DEFAULT_SERVER_URL2;
3566
+ }
3567
+ function resolveAuthHeader2(opts) {
3568
+ const key = opts.apiKey ?? SecretsManager11.optional("TUTTI_API_KEY");
3569
+ return key ? { Authorization: "Bearer " + key } : {};
3570
+ }
3571
+ function explainConnectionError2(err, baseUrl) {
3572
+ const msg = err instanceof Error ? err.message : String(err);
3573
+ console.error(chalk24.red("Failed to reach Tutti server at " + baseUrl));
3574
+ console.error(chalk24.dim(" " + msg));
3575
+ console.error(
3576
+ chalk24.dim(" Is `tutti-ai serve` running? Set --url or TUTTI_SERVER_URL to override.")
3577
+ );
3578
+ process.exit(1);
3579
+ }
3580
+ function reviveInterrupt(wire) {
3581
+ const req = {
3582
+ interrupt_id: wire["interrupt_id"],
3583
+ session_id: wire["session_id"],
3584
+ tool_name: wire["tool_name"],
3585
+ tool_args: wire["tool_args"],
3586
+ requested_at: new Date(wire["requested_at"]),
3587
+ status: wire["status"]
3588
+ };
3589
+ if (typeof wire["resolved_at"] === "string") {
3590
+ req.resolved_at = new Date(wire["resolved_at"]);
3591
+ }
3592
+ if (typeof wire["resolved_by"] === "string") {
3593
+ req.resolved_by = wire["resolved_by"];
3594
+ }
3595
+ if (typeof wire["denial_reason"] === "string") {
3596
+ req.denial_reason = wire["denial_reason"];
3597
+ }
3598
+ return req;
3599
+ }
3600
+ async function httpJson(opts, method, path, body) {
3601
+ const baseUrl = resolveUrl2(opts);
3602
+ const url = baseUrl.replace(/\/$/, "") + path;
3603
+ let res;
3604
+ try {
3605
+ res = await fetch(url, {
3606
+ method,
3607
+ headers: {
3608
+ "Content-Type": "application/json",
3609
+ ...resolveAuthHeader2(opts)
3610
+ },
3611
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
3612
+ });
3613
+ } catch (err) {
3614
+ explainConnectionError2(err, baseUrl);
3615
+ }
3616
+ const text = await res.text();
3617
+ const parsed = text === "" ? null : JSON.parse(text);
3618
+ return { status: res.status, body: parsed };
3619
+ }
3620
+ async function fetchPending(opts) {
3621
+ const { status, body } = await httpJson(
3622
+ opts,
3623
+ "GET",
3624
+ "/interrupts/pending"
3625
+ );
3626
+ if (status === 401) {
3627
+ console.error(chalk24.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
3628
+ process.exit(1);
3629
+ }
3630
+ if (status === 503) {
3631
+ console.error(
3632
+ chalk24.red(
3633
+ "The server has no InterruptStore configured. Start `tutti-ai serve` with an interrupt store attached."
3634
+ )
3635
+ );
3636
+ process.exit(1);
3637
+ }
3638
+ if (status < 200 || status >= 300 || !("interrupts" in body)) {
3639
+ console.error(chalk24.red("Unexpected server response: " + status));
3640
+ process.exit(1);
3641
+ }
3642
+ return body.interrupts.map(reviveInterrupt);
3643
+ }
3644
+ async function postResolve(opts, interruptId, action, payload) {
3645
+ const { status, body } = await httpJson(
3646
+ opts,
3647
+ "POST",
3648
+ "/interrupts/" + encodeURIComponent(interruptId) + "/" + action,
3649
+ payload
3650
+ );
3651
+ if (status === 401) {
3652
+ console.error(chalk24.red("Unauthorized \u2014 set --api-key or TUTTI_API_KEY."));
3653
+ process.exit(1);
3654
+ }
3655
+ if (status === 404) {
3656
+ console.error(chalk24.red('Interrupt "' + interruptId + '" not found.'));
3657
+ process.exit(1);
3658
+ }
3659
+ if (status === 409) {
3660
+ const current = body.current;
3661
+ const currentStatus = current && typeof current["status"] === "string" ? current["status"] : "resolved";
3662
+ console.error(
3663
+ chalk24.red("Interrupt already " + currentStatus + " \u2014 refusing to override.")
3664
+ );
3665
+ process.exit(1);
3666
+ }
3667
+ if (status < 200 || status >= 300) {
3668
+ console.error(chalk24.red("Unexpected server response: " + status));
3669
+ process.exit(1);
3670
+ }
3671
+ return reviveInterrupt(body);
3672
+ }
3673
+ async function interruptsListCommand(opts) {
3674
+ const pending = await fetchPending(opts);
3675
+ console.log(renderInterruptsList(pending));
3676
+ }
3677
+ async function interruptsApproveCommand(interruptId, opts) {
3678
+ const resolved = await postResolve(opts, interruptId, "approve", {
3679
+ ...opts.resolvedBy !== void 0 ? { resolved_by: opts.resolvedBy } : {}
3680
+ });
3681
+ console.log(renderApproved(resolved));
3682
+ }
3683
+ async function interruptsDenyCommand(interruptId, opts) {
3684
+ const resolved = await postResolve(opts, interruptId, "deny", {
3685
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {},
3686
+ ...opts.resolvedBy !== void 0 ? { resolved_by: opts.resolvedBy } : {}
3687
+ });
3688
+ console.log(renderDenied(resolved));
3689
+ }
3690
+ function clearScreen() {
3691
+ process.stdout.write("\x1B[2J\x1B[H");
3692
+ }
3693
+ function readKey() {
3694
+ const input = process.stdin;
3695
+ if (!input.isTTY) return Promise.resolve(null);
3696
+ return new Promise((resolve18) => {
3697
+ input.setRawMode(true);
3698
+ input.resume();
3699
+ input.setEncoding("utf8");
3700
+ const onData = (data) => {
3701
+ input.removeListener("data", onData);
3702
+ input.setRawMode(false);
3703
+ input.pause();
3704
+ resolve18(data);
3705
+ };
3706
+ input.on("data", onData);
3707
+ });
3708
+ }
3709
+ async function readKeyOrTimeout(ms) {
3710
+ let timer;
3711
+ const timeout = new Promise((resolve18) => {
3712
+ timer = setTimeout(() => resolve18(null), ms);
3713
+ });
3714
+ try {
3715
+ const winner = await Promise.race([readKey(), timeout]);
3716
+ return winner;
3717
+ } finally {
3718
+ if (timer) clearTimeout(timer);
3719
+ }
3720
+ }
3721
+ async function interruptsTUICommand(opts) {
3722
+ if (!process.stdin.isTTY) {
3723
+ await interruptsListCommand(opts);
3724
+ return;
3725
+ }
3726
+ const sigint = () => {
3727
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
3728
+ process.stdout.write("\n");
3729
+ process.exit(0);
3730
+ };
3731
+ process.on("SIGINT", sigint);
3732
+ try {
3733
+ for (; ; ) {
3734
+ const pending = await fetchPending(opts);
3735
+ clearScreen();
3736
+ console.log(
3737
+ chalk24.bold("Tutti \u2014 pending interrupts") + chalk24.dim(" (auto-refresh every " + POLL_INTERVAL_MS / 1e3 + "s)")
3738
+ );
3739
+ console.log(renderInterruptsList(pending));
3740
+ if (pending.length > 0) {
3741
+ console.log(
3742
+ chalk24.dim(
3743
+ "Press a number to inspect, 'r' to refresh, 'q' to quit."
3744
+ )
3745
+ );
3746
+ } else {
3747
+ console.log(chalk24.dim("Press 'r' to refresh, 'q' to quit."));
3748
+ }
3749
+ const indexed = pending.slice(0, 9);
3750
+ const key = await readKeyOrTimeout(POLL_INTERVAL_MS);
3751
+ if (key === null) continue;
3752
+ if (key === "q" || key === "") return;
3753
+ if (key === "r") continue;
3754
+ const digit = parseInt(key, 10);
3755
+ if (!Number.isNaN(digit) && digit >= 1 && digit <= indexed.length) {
3756
+ const chosen = indexed[digit - 1];
3757
+ const shouldContinue = await runDetailView(opts, chosen);
3758
+ if (!shouldContinue) return;
3759
+ }
3760
+ }
3761
+ } finally {
3762
+ process.off("SIGINT", sigint);
3763
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
3764
+ }
3765
+ }
3766
+ async function runDetailView(opts, interrupt) {
3767
+ clearScreen();
3768
+ console.log(renderInterruptDetail(interrupt));
3769
+ console.log(
3770
+ chalk24.dim(
3771
+ "Press 'a' to approve, 'd' to deny, 'q' to go back to the list."
3772
+ )
3773
+ );
3774
+ const key = await readKey();
3775
+ if (key === null || key === "q" || key === "") return true;
3776
+ if (key === "a") {
3777
+ const resolved = await postResolve(opts, interrupt.interrupt_id, "approve", {});
3778
+ clearScreen();
3779
+ console.log(renderApproved(resolved));
3780
+ await pause();
3781
+ return true;
3782
+ }
3783
+ if (key === "d") {
3784
+ const { reason } = await prompt4({
3785
+ type: "input",
3786
+ name: "reason",
3787
+ message: "Reason (optional):"
3788
+ });
3789
+ const payload = {};
3790
+ if (reason && reason.trim() !== "") payload["reason"] = reason.trim();
3791
+ const resolved = await postResolve(
3792
+ opts,
3793
+ interrupt.interrupt_id,
3794
+ "deny",
3795
+ payload
3796
+ );
3797
+ clearScreen();
3798
+ console.log(renderDenied(resolved));
3799
+ await pause();
3800
+ return true;
3801
+ }
3802
+ return true;
3803
+ }
3804
+ async function pause() {
3805
+ console.log(chalk24.dim("\nPress any key to continue..."));
3806
+ await readKey();
3807
+ }
3808
+
3446
3809
  // src/index.ts
3447
3810
  config();
3448
3811
  var logger16 = createLogger16("tutti-cli");
@@ -3593,5 +3956,34 @@ memoryCmd.command("export").description("Export every memory for a user as JSON
3593
3956
  ...opts.out !== void 0 ? { out: opts.out } : {}
3594
3957
  });
3595
3958
  });
3959
+ function toInterruptsOptions(raw) {
3960
+ return {
3961
+ ...raw.url !== void 0 ? { url: raw.url } : {},
3962
+ ...raw.apiKey !== void 0 ? { apiKey: raw.apiKey } : {}
3963
+ };
3964
+ }
3965
+ var interruptsCmd = program.command("interrupts").description("Review and resolve approval-gated tool calls");
3966
+ 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) => {
3967
+ await interruptsTUICommand(toInterruptsOptions(opts));
3968
+ });
3969
+ 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) => {
3970
+ await interruptsListCommand(toInterruptsOptions(opts));
3971
+ });
3972
+ 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) => {
3973
+ await interruptsApproveCommand(id, {
3974
+ ...toInterruptsOptions(opts),
3975
+ ...opts.by !== void 0 ? { resolvedBy: opts.by } : {}
3976
+ });
3977
+ });
3978
+ 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) => {
3979
+ await interruptsDenyCommand(id, {
3980
+ ...toInterruptsOptions(opts),
3981
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {},
3982
+ ...opts.by !== void 0 ? { resolvedBy: opts.by } : {}
3983
+ });
3984
+ });
3985
+ 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) => {
3986
+ await interruptsTUICommand(toInterruptsOptions(opts));
3987
+ });
3596
3988
  program.parse();
3597
3989
  //# sourceMappingURL=index.js.map