@tuttiai/cli 0.14.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 +685 -6
- 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 createLogger16 } from "@tuttiai/core";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -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
|
|
2607
|
-
const raw = await rl.question(
|
|
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+/);
|
|
@@ -3182,15 +3182,639 @@ function reviveSpanDates(span) {
|
|
|
3182
3182
|
};
|
|
3183
3183
|
}
|
|
3184
3184
|
|
|
3185
|
+
// src/commands/memory.ts
|
|
3186
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
3187
|
+
import chalk22 from "chalk";
|
|
3188
|
+
import Enquirer3 from "enquirer";
|
|
3189
|
+
import {
|
|
3190
|
+
MemoryUserMemoryStore,
|
|
3191
|
+
PostgresUserMemoryStore,
|
|
3192
|
+
SecretsManager as SecretsManager10,
|
|
3193
|
+
createLogger as createLogger15
|
|
3194
|
+
} from "@tuttiai/core";
|
|
3195
|
+
|
|
3196
|
+
// src/commands/memory-render.ts
|
|
3197
|
+
import chalk21 from "chalk";
|
|
3198
|
+
function visibleLen2(s) {
|
|
3199
|
+
return s.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
3200
|
+
}
|
|
3201
|
+
function pad5(s, len) {
|
|
3202
|
+
const v = visibleLen2(s);
|
|
3203
|
+
return v >= len ? s : s + " ".repeat(len - v);
|
|
3204
|
+
}
|
|
3205
|
+
function truncate(text, max) {
|
|
3206
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
3207
|
+
return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
|
|
3208
|
+
}
|
|
3209
|
+
function importanceStars(importance) {
|
|
3210
|
+
if (importance === 3) return "\u2605\u2605\u2605";
|
|
3211
|
+
if (importance === 2) return "\u2605\u2605\u2606";
|
|
3212
|
+
return "\u2605\u2606\u2606";
|
|
3213
|
+
}
|
|
3214
|
+
function formatDate(d) {
|
|
3215
|
+
const iso = d.toISOString();
|
|
3216
|
+
return iso.slice(0, 10) + " " + iso.slice(11, 16);
|
|
3217
|
+
}
|
|
3218
|
+
function sortMemoriesForList(memories) {
|
|
3219
|
+
return [...memories].sort((a, b) => {
|
|
3220
|
+
if (a.importance !== b.importance) return b.importance - a.importance;
|
|
3221
|
+
return b.created_at.getTime() - a.created_at.getTime();
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
function renderTable(memories, emptyMessage) {
|
|
3225
|
+
if (memories.length === 0) return chalk21.dim(emptyMessage);
|
|
3226
|
+
const lines = [];
|
|
3227
|
+
lines.push("");
|
|
3228
|
+
lines.push(
|
|
3229
|
+
chalk21.dim(
|
|
3230
|
+
" " + pad5("ID", 10) + pad5("CONTENT", 62) + pad5("SOURCE", 12) + pad5("IMPORTANCE", 14) + "CREATED"
|
|
3231
|
+
)
|
|
3232
|
+
);
|
|
3233
|
+
lines.push(chalk21.dim(" " + "\u2500".repeat(110)));
|
|
3234
|
+
for (const m of memories) {
|
|
3235
|
+
const idShort = m.id.slice(0, 8);
|
|
3236
|
+
const content = truncate(m.content, 60);
|
|
3237
|
+
const sourceColored = m.source === "explicit" ? chalk21.green("explicit") : chalk21.yellow("inferred");
|
|
3238
|
+
const importance = importanceStars(m.importance);
|
|
3239
|
+
const created = formatDate(m.created_at);
|
|
3240
|
+
lines.push(
|
|
3241
|
+
" " + chalk21.bold(pad5(idShort, 10)) + pad5(content, 62) + pad5(sourceColored, 12) + pad5(importance, 14) + chalk21.dim(created)
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
lines.push("");
|
|
3245
|
+
return lines.join("\n");
|
|
3246
|
+
}
|
|
3247
|
+
function renderMemoryList(memories, userId) {
|
|
3248
|
+
return renderTable(
|
|
3249
|
+
sortMemoriesForList(memories),
|
|
3250
|
+
'No memories stored for user "' + userId + '".'
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
function renderMemorySearch(memories, userId, query) {
|
|
3254
|
+
const header = chalk21.dim(
|
|
3255
|
+
"Search for " + chalk21.bold('"' + query + '"') + ' in user "' + userId + '" \u2014 ' + memories.length + " result" + (memories.length === 1 ? "" : "s")
|
|
3256
|
+
);
|
|
3257
|
+
const body = renderTable(
|
|
3258
|
+
memories,
|
|
3259
|
+
'No memories matching "' + query + '" for user "' + userId + '".'
|
|
3260
|
+
);
|
|
3261
|
+
return header + body;
|
|
3262
|
+
}
|
|
3263
|
+
function renderMemoryAdded(memory) {
|
|
3264
|
+
return chalk21.green("\u2713") + " Stored memory " + chalk21.bold(memory.id.slice(0, 8)) + chalk21.dim(" (" + memory.source + ", " + importanceStars(memory.importance) + ")");
|
|
3265
|
+
}
|
|
3266
|
+
function renderMemoryDeleted(memoryId) {
|
|
3267
|
+
return chalk21.green("\u2713") + " Deleted memory " + chalk21.bold(memoryId.slice(0, 8));
|
|
3268
|
+
}
|
|
3269
|
+
function renderMemoryCleared(userId, count) {
|
|
3270
|
+
return chalk21.green("\u2713") + " Deleted " + chalk21.bold(String(count)) + " memor" + (count === 1 ? "y" : "ies") + ' for user "' + userId + '"';
|
|
3271
|
+
}
|
|
3272
|
+
function exportMemoriesJson(memories) {
|
|
3273
|
+
return JSON.stringify(memories, null, 2) + "\n";
|
|
3274
|
+
}
|
|
3275
|
+
function exportMemoriesCsv(memories) {
|
|
3276
|
+
const headers = [
|
|
3277
|
+
"id",
|
|
3278
|
+
"user_id",
|
|
3279
|
+
"content",
|
|
3280
|
+
"source",
|
|
3281
|
+
"importance",
|
|
3282
|
+
"tags",
|
|
3283
|
+
"created_at",
|
|
3284
|
+
"last_accessed_at",
|
|
3285
|
+
"expires_at"
|
|
3286
|
+
];
|
|
3287
|
+
const rows = [headers.join(",")];
|
|
3288
|
+
for (const m of memories) {
|
|
3289
|
+
rows.push(
|
|
3290
|
+
[
|
|
3291
|
+
m.id,
|
|
3292
|
+
m.user_id,
|
|
3293
|
+
m.content,
|
|
3294
|
+
m.source,
|
|
3295
|
+
String(m.importance),
|
|
3296
|
+
m.tags?.join(";") ?? "",
|
|
3297
|
+
m.created_at.toISOString(),
|
|
3298
|
+
m.last_accessed_at?.toISOString() ?? "",
|
|
3299
|
+
m.expires_at?.toISOString() ?? ""
|
|
3300
|
+
].map(csvEscape).join(",")
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
return rows.join("\n") + "\n";
|
|
3304
|
+
}
|
|
3305
|
+
function csvEscape(field) {
|
|
3306
|
+
if (/[",\n\r]/.test(field)) {
|
|
3307
|
+
return '"' + field.replace(/"/g, '""') + '"';
|
|
3308
|
+
}
|
|
3309
|
+
return field;
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// src/commands/memory.ts
|
|
3313
|
+
var logger15 = createLogger15("tutti-cli");
|
|
3314
|
+
var { prompt: prompt3 } = Enquirer3;
|
|
3315
|
+
function resolveStore3() {
|
|
3316
|
+
const pgUrl = SecretsManager10.optional("TUTTI_PG_URL");
|
|
3317
|
+
if (pgUrl) {
|
|
3318
|
+
return new PostgresUserMemoryStore({ connection_string: pgUrl });
|
|
3319
|
+
}
|
|
3320
|
+
logger15.warn(
|
|
3321
|
+
"TUTTI_PG_URL not set \u2014 using in-memory store (memories are ephemeral; this command will appear to do nothing useful)"
|
|
3322
|
+
);
|
|
3323
|
+
return new MemoryUserMemoryStore();
|
|
3324
|
+
}
|
|
3325
|
+
async function closeStore2(store) {
|
|
3326
|
+
if (typeof store.close === "function") {
|
|
3327
|
+
await store.close();
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
function requireUser(opts) {
|
|
3331
|
+
if (!opts.user || opts.user.trim() === "") {
|
|
3332
|
+
console.error(chalk22.red("--user <user-id> is required"));
|
|
3333
|
+
process.exit(1);
|
|
3334
|
+
}
|
|
3335
|
+
return opts.user.trim();
|
|
3336
|
+
}
|
|
3337
|
+
function parseImportance(raw) {
|
|
3338
|
+
if (raw === void 0) return 2;
|
|
3339
|
+
if (raw === "1") return 1;
|
|
3340
|
+
if (raw === "2") return 2;
|
|
3341
|
+
if (raw === "3") return 3;
|
|
3342
|
+
console.error(chalk22.red("--importance must be 1, 2, or 3"));
|
|
3343
|
+
process.exit(1);
|
|
3344
|
+
}
|
|
3345
|
+
async function memoryListCommand(opts) {
|
|
3346
|
+
const userId = requireUser(opts);
|
|
3347
|
+
const store = resolveStore3();
|
|
3348
|
+
try {
|
|
3349
|
+
const memories = await store.list(userId);
|
|
3350
|
+
console.log(renderMemoryList(memories, userId));
|
|
3351
|
+
} finally {
|
|
3352
|
+
await closeStore2(store);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
async function memorySearchCommand(query, opts) {
|
|
3356
|
+
const userId = requireUser(opts);
|
|
3357
|
+
if (query.trim() === "") {
|
|
3358
|
+
console.error(chalk22.red("Search query is required"));
|
|
3359
|
+
process.exit(1);
|
|
3360
|
+
}
|
|
3361
|
+
const store = resolveStore3();
|
|
3362
|
+
try {
|
|
3363
|
+
const memories = await store.search(userId, query);
|
|
3364
|
+
console.log(renderMemorySearch(memories, userId, query));
|
|
3365
|
+
} finally {
|
|
3366
|
+
await closeStore2(store);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
async function memoryAddCommand(content, opts) {
|
|
3370
|
+
const userId = requireUser(opts);
|
|
3371
|
+
if (content.trim() === "") {
|
|
3372
|
+
console.error(chalk22.red("Memory content is required"));
|
|
3373
|
+
process.exit(1);
|
|
3374
|
+
}
|
|
3375
|
+
const importance = parseImportance(opts.importance);
|
|
3376
|
+
const store = resolveStore3();
|
|
3377
|
+
try {
|
|
3378
|
+
const stored = await store.store(userId, content.trim(), {
|
|
3379
|
+
source: "explicit",
|
|
3380
|
+
importance
|
|
3381
|
+
});
|
|
3382
|
+
console.log(renderMemoryAdded(stored));
|
|
3383
|
+
} finally {
|
|
3384
|
+
await closeStore2(store);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
async function memoryDeleteCommand(memoryId, opts) {
|
|
3388
|
+
requireUser(opts);
|
|
3389
|
+
const store = resolveStore3();
|
|
3390
|
+
try {
|
|
3391
|
+
await store.delete(memoryId);
|
|
3392
|
+
console.log(renderMemoryDeleted(memoryId));
|
|
3393
|
+
} finally {
|
|
3394
|
+
await closeStore2(store);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
async function memoryClearCommand(opts) {
|
|
3398
|
+
const userId = requireUser(opts);
|
|
3399
|
+
const store = resolveStore3();
|
|
3400
|
+
try {
|
|
3401
|
+
const memories = await store.list(userId);
|
|
3402
|
+
if (memories.length === 0) {
|
|
3403
|
+
console.log(chalk22.dim('No memories stored for user "' + userId + '".'));
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
const { confirm } = await prompt3({
|
|
3407
|
+
type: "confirm",
|
|
3408
|
+
name: "confirm",
|
|
3409
|
+
message: "Delete all " + memories.length + ' memories for user "' + userId + '"?',
|
|
3410
|
+
initial: false
|
|
3411
|
+
});
|
|
3412
|
+
if (!confirm) {
|
|
3413
|
+
console.log(chalk22.dim(" Cancelled."));
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
await store.deleteAll(userId);
|
|
3417
|
+
console.log(renderMemoryCleared(userId, memories.length));
|
|
3418
|
+
} finally {
|
|
3419
|
+
await closeStore2(store);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
async function memoryExportCommand(opts) {
|
|
3423
|
+
const userId = requireUser(opts);
|
|
3424
|
+
const format = (opts.format ?? "json").toLowerCase();
|
|
3425
|
+
if (format !== "json" && format !== "csv") {
|
|
3426
|
+
console.error(chalk22.red("--format must be 'json' or 'csv'"));
|
|
3427
|
+
process.exit(1);
|
|
3428
|
+
}
|
|
3429
|
+
const store = resolveStore3();
|
|
3430
|
+
try {
|
|
3431
|
+
const memories = await store.list(userId);
|
|
3432
|
+
const body = format === "json" ? exportMemoriesJson(memories) : exportMemoriesCsv(memories);
|
|
3433
|
+
if (opts.out) {
|
|
3434
|
+
writeFileSync2(opts.out, body, "utf8");
|
|
3435
|
+
console.log(
|
|
3436
|
+
chalk22.green("\u2713") + " Wrote " + chalk22.bold(String(memories.length)) + " memor" + (memories.length === 1 ? "y" : "ies") + " to " + chalk22.bold(opts.out)
|
|
3437
|
+
);
|
|
3438
|
+
} else {
|
|
3439
|
+
process.stdout.write(body);
|
|
3440
|
+
}
|
|
3441
|
+
} finally {
|
|
3442
|
+
await closeStore2(store);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
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
|
+
|
|
3185
3809
|
// src/index.ts
|
|
3186
3810
|
config();
|
|
3187
|
-
var
|
|
3811
|
+
var logger16 = createLogger16("tutti-cli");
|
|
3188
3812
|
process.on("unhandledRejection", (reason) => {
|
|
3189
|
-
|
|
3813
|
+
logger16.error({ error: reason instanceof Error ? reason.message : String(reason) }, "Unhandled rejection");
|
|
3190
3814
|
process.exit(1);
|
|
3191
3815
|
});
|
|
3192
3816
|
process.on("uncaughtException", (err) => {
|
|
3193
|
-
|
|
3817
|
+
logger16.error({ error: err.message }, "Fatal error");
|
|
3194
3818
|
process.exit(1);
|
|
3195
3819
|
});
|
|
3196
3820
|
var program = new Command();
|
|
@@ -3306,5 +3930,60 @@ tracesCmd.command("tail").description("Live-tail spans as they are emitted (Ctrl
|
|
|
3306
3930
|
};
|
|
3307
3931
|
await tracesTailCommand(resolved);
|
|
3308
3932
|
});
|
|
3933
|
+
var memoryCmd = program.command("memory").description("Manage per-user memories (uses TUTTI_PG_URL like the runtime)");
|
|
3934
|
+
memoryCmd.command("list").description("Show every memory for a user, sorted by importance + recency").requiredOption("--user <user-id>", "End-user identifier").action(async (opts) => {
|
|
3935
|
+
await memoryListCommand({ user: opts.user });
|
|
3936
|
+
});
|
|
3937
|
+
memoryCmd.command("search <query>").description("Search a user's memories by relevance").requiredOption("--user <user-id>", "End-user identifier").action(async (query, opts) => {
|
|
3938
|
+
await memorySearchCommand(query, { user: opts.user });
|
|
3939
|
+
});
|
|
3940
|
+
memoryCmd.command("add <content>").description("Manually store a memory for a user").requiredOption("--user <user-id>", "End-user identifier").option("--importance <n>", "Importance: 1 (low), 2 (normal), 3 (high)", "2").action(async (content, opts) => {
|
|
3941
|
+
await memoryAddCommand(content, {
|
|
3942
|
+
user: opts.user,
|
|
3943
|
+
...opts.importance !== void 0 ? { importance: opts.importance } : {}
|
|
3944
|
+
});
|
|
3945
|
+
});
|
|
3946
|
+
memoryCmd.command("delete <memory-id>").description("Delete a specific memory by id").requiredOption("--user <user-id>", "End-user identifier").action(async (memoryId, opts) => {
|
|
3947
|
+
await memoryDeleteCommand(memoryId, { user: opts.user });
|
|
3948
|
+
});
|
|
3949
|
+
memoryCmd.command("clear").description("Delete every memory for a user (prompts for confirmation)").requiredOption("--user <user-id>", "End-user identifier").action(async (opts) => {
|
|
3950
|
+
await memoryClearCommand({ user: opts.user });
|
|
3951
|
+
});
|
|
3952
|
+
memoryCmd.command("export").description("Export every memory for a user as JSON or CSV").requiredOption("--user <user-id>", "End-user identifier").option("--format <fmt>", "Output format: json | csv", "json").option("-o, --out <path>", "Write to file instead of stdout").action(async (opts) => {
|
|
3953
|
+
await memoryExportCommand({
|
|
3954
|
+
user: opts.user,
|
|
3955
|
+
...opts.format !== void 0 ? { format: opts.format } : {},
|
|
3956
|
+
...opts.out !== void 0 ? { out: opts.out } : {}
|
|
3957
|
+
});
|
|
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
|
+
});
|
|
3309
3988
|
program.parse();
|
|
3310
3989
|
//# sourceMappingURL=index.js.map
|