@freesyntax/notch-cli 0.4.8 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +2956 -694
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,8 +14,8 @@ import {
14
14
 
15
15
  // src/index.ts
16
16
  import { Command } from "commander";
17
- import chalk9 from "chalk";
18
- import ora from "ora";
17
+ import chalk26 from "chalk";
18
+ import ora4 from "ora";
19
19
  import * as readline from "readline";
20
20
  import * as nodePath from "path";
21
21
 
@@ -866,7 +866,7 @@ var GITHUB_API = "https://api.github.com";
866
866
  function getToken() {
867
867
  return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || null;
868
868
  }
869
- async function ghFetch(path19, opts2 = {}) {
869
+ async function ghFetch(path25, opts2 = {}) {
870
870
  const token = getToken();
871
871
  const headers = {
872
872
  "Accept": "application/vnd.github+json",
@@ -874,7 +874,7 @@ async function ghFetch(path19, opts2 = {}) {
874
874
  ...opts2.headers ?? {}
875
875
  };
876
876
  if (token) headers["Authorization"] = `Bearer ${token}`;
877
- return fetch(`${GITHUB_API}${path19}`, { ...opts2, headers });
877
+ return fetch(`${GITHUB_API}${path25}`, { ...opts2, headers });
878
878
  }
879
879
  var parameters9 = z9.object({
880
880
  action: z9.enum([
@@ -1031,24 +1031,516 @@ var githubTool = {
1031
1031
  execute
1032
1032
  };
1033
1033
 
1034
+ // src/tools/lsp.ts
1035
+ import { execSync as execSync3, execFileSync } from "child_process";
1036
+ import fs6 from "fs/promises";
1037
+ import path7 from "path";
1038
+ import { z as z10 } from "zod";
1039
+ var parameters10 = z10.object({
1040
+ action: z10.enum(["definition", "references", "diagnostics", "hover"]).describe("LSP action: definition (go-to-def), references (find usages), diagnostics (type errors), hover (type info)"),
1041
+ file: z10.string().describe("File path (relative or absolute)"),
1042
+ line: z10.number().optional().describe("Line number (1-based) \u2014 required for definition, references, hover"),
1043
+ character: z10.number().optional().describe("Column number (0-based) \u2014 required for definition, references, hover"),
1044
+ symbol: z10.string().optional().describe("Symbol name to search for (alternative to line/character)")
1045
+ });
1046
+ function detectLanguage(filePath) {
1047
+ const ext = path7.extname(filePath).toLowerCase();
1048
+ switch (ext) {
1049
+ case ".ts":
1050
+ case ".tsx":
1051
+ case ".js":
1052
+ case ".jsx":
1053
+ case ".mjs":
1054
+ case ".cjs":
1055
+ return { lang: "typescript", server: "typescript-language-server", tsconfig: true };
1056
+ case ".py":
1057
+ return { lang: "python", server: "pyright", tsconfig: false };
1058
+ case ".go":
1059
+ return { lang: "go", server: "gopls", tsconfig: false };
1060
+ case ".rs":
1061
+ return { lang: "rust", server: "rust-analyzer", tsconfig: false };
1062
+ default:
1063
+ return { lang: "unknown", server: null, tsconfig: false };
1064
+ }
1065
+ }
1066
+ function isAvailable(cmd) {
1067
+ try {
1068
+ const which = process.platform === "win32" ? "where" : "which";
1069
+ execFileSync(which, [cmd], { stdio: "pipe", timeout: 3e3 });
1070
+ return true;
1071
+ } catch {
1072
+ return false;
1073
+ }
1074
+ }
1075
+ async function tsDiagnostics(filePath, cwd) {
1076
+ try {
1077
+ const result = execSync3(
1078
+ `npx tsc --noEmit --pretty false "${filePath}" 2>&1`,
1079
+ { cwd, encoding: "utf-8", timeout: 3e4 }
1080
+ );
1081
+ return result.trim() || "No type errors found.";
1082
+ } catch (err) {
1083
+ const output = err.stdout || err.stderr || "";
1084
+ if (output.includes("error TS")) {
1085
+ const lines = output.split("\n").filter((l) => l.includes("error TS"));
1086
+ return lines.slice(0, 20).join("\n") || "No type errors found.";
1087
+ }
1088
+ try {
1089
+ const result = execSync3(
1090
+ `npx tsc --noEmit --pretty false 2>&1 | head -30`,
1091
+ { cwd, encoding: "utf-8", timeout: 3e4, shell: true }
1092
+ );
1093
+ return result.trim() || "No type errors found.";
1094
+ } catch (err2) {
1095
+ const out = err2.stdout || err2.stderr || "";
1096
+ const errLines = out.split("\n").filter((l) => l.includes("error TS"));
1097
+ return errLines.slice(0, 20).join("\n") || "Could not run type checker.";
1098
+ }
1099
+ }
1100
+ }
1101
+ async function findDefinition(symbol, cwd) {
1102
+ const patterns = [
1103
+ `(function|const|let|var|class|interface|type|enum)\\s+${symbol}\\b`,
1104
+ `export\\s+(default\\s+)?(function|const|let|var|class|interface|type|enum)\\s+${symbol}\\b`,
1105
+ `def\\s+${symbol}\\s*\\(`,
1106
+ // Python
1107
+ `func\\s+${symbol}\\s*\\(`,
1108
+ // Go
1109
+ `fn\\s+${symbol}\\s*[<(]`
1110
+ // Rust
1111
+ ];
1112
+ const combinedPattern = patterns.join("|");
1113
+ try {
1114
+ const result = execSync3(
1115
+ `rg --no-heading --line-number -e "${combinedPattern}" --type-add "code:*.{ts,tsx,js,jsx,py,go,rs}" --type code -m 10`,
1116
+ { cwd, encoding: "utf-8", timeout: 1e4, maxBuffer: 1024 * 1024 }
1117
+ );
1118
+ return result.trim() || `No definition found for "${symbol}"`;
1119
+ } catch {
1120
+ try {
1121
+ const result = execSync3(
1122
+ `grep -rn -E "${combinedPattern}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" --include="*.rs" . | head -10`,
1123
+ { cwd, encoding: "utf-8", timeout: 1e4, shell: true, maxBuffer: 1024 * 1024 }
1124
+ );
1125
+ return result.trim() || `No definition found for "${symbol}"`;
1126
+ } catch {
1127
+ return `No definition found for "${symbol}"`;
1128
+ }
1129
+ }
1130
+ }
1131
+ async function findReferences(symbol, cwd) {
1132
+ try {
1133
+ const result = execSync3(
1134
+ `rg --no-heading --line-number -w "${symbol}" --type-add "code:*.{ts,tsx,js,jsx,py,go,rs}" --type code -m 30`,
1135
+ { cwd, encoding: "utf-8", timeout: 1e4, maxBuffer: 1024 * 1024 }
1136
+ );
1137
+ const lines = result.trim().split("\n");
1138
+ return `Found ${lines.length} reference(s) for "${symbol}":
1139
+
1140
+ ${lines.join("\n")}`;
1141
+ } catch {
1142
+ try {
1143
+ const result = execSync3(
1144
+ `grep -rn -w "${symbol}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" --include="*.rs" . | head -30`,
1145
+ { cwd, encoding: "utf-8", timeout: 1e4, shell: true, maxBuffer: 1024 * 1024 }
1146
+ );
1147
+ const lines = result.trim().split("\n");
1148
+ return `Found ${lines.length} reference(s) for "${symbol}":
1149
+
1150
+ ${lines.join("\n")}`;
1151
+ } catch {
1152
+ return `No references found for "${symbol}"`;
1153
+ }
1154
+ }
1155
+ }
1156
+ async function getHoverInfo(symbol, filePath, line, cwd) {
1157
+ try {
1158
+ const absPath = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
1159
+ const content = await fs6.readFile(absPath, "utf-8");
1160
+ const lines = content.split("\n");
1161
+ const startLine = Math.max(0, line - 3);
1162
+ const endLine = Math.min(lines.length, line + 2);
1163
+ const context = lines.slice(startLine, endLine).map((l, i) => `${startLine + i + 1}: ${l}`).join("\n");
1164
+ const def = await findDefinition(symbol, cwd);
1165
+ return `## Context at ${filePath}:${line}
1166
+ \`\`\`
1167
+ ${context}
1168
+ \`\`\`
1169
+
1170
+ ## Definition
1171
+ ${def}`;
1172
+ } catch (err) {
1173
+ return `Could not get hover info: ${err.message}`;
1174
+ }
1175
+ }
1176
+ async function getDiagnostics(filePath, cwd) {
1177
+ const absPath = path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
1178
+ const { lang } = detectLanguage(absPath);
1179
+ switch (lang) {
1180
+ case "typescript":
1181
+ return tsDiagnostics(absPath, cwd);
1182
+ case "python": {
1183
+ if (isAvailable("pyright")) {
1184
+ try {
1185
+ const result = execFileSync("pyright", [absPath], {
1186
+ cwd,
1187
+ encoding: "utf-8",
1188
+ timeout: 3e4
1189
+ });
1190
+ return result.trim() || "No errors found.";
1191
+ } catch (err) {
1192
+ return err.stdout || err.stderr || "Pyright check failed.";
1193
+ }
1194
+ }
1195
+ try {
1196
+ execFileSync("python", ["-m", "py_compile", absPath], {
1197
+ cwd,
1198
+ encoding: "utf-8",
1199
+ timeout: 1e4
1200
+ });
1201
+ return "No syntax errors found.";
1202
+ } catch (err) {
1203
+ return err.stderr || err.stdout || "Syntax check failed.";
1204
+ }
1205
+ }
1206
+ case "go": {
1207
+ try {
1208
+ const result = execFileSync("go", ["vet", absPath], {
1209
+ cwd,
1210
+ encoding: "utf-8",
1211
+ timeout: 15e3
1212
+ });
1213
+ return result.trim() || "No issues found.";
1214
+ } catch (err) {
1215
+ return err.stderr || err.stdout || "Go vet failed.";
1216
+ }
1217
+ }
1218
+ case "rust": {
1219
+ try {
1220
+ const result = execSync3(`cargo check --message-format short 2>&1 | head -30`, {
1221
+ cwd,
1222
+ encoding: "utf-8",
1223
+ timeout: 6e4,
1224
+ shell: true
1225
+ });
1226
+ return result.trim() || "No issues found.";
1227
+ } catch (err) {
1228
+ return err.stderr || err.stdout || "Cargo check failed.";
1229
+ }
1230
+ }
1231
+ default:
1232
+ return `No language server available for ${lang} files. Use grep to search for symbols.`;
1233
+ }
1234
+ }
1235
+ var lspTool = {
1236
+ name: "lsp",
1237
+ description: "Code intelligence: find definitions, references, type errors, and hover info. Uses TypeScript compiler, pyright, gopls, or grep-based heuristics. Actions: definition (go-to-def), references (find usages), diagnostics (type errors), hover (type info). Provide either line/character position or a symbol name.",
1238
+ parameters: parameters10,
1239
+ async execute(params, ctx) {
1240
+ const filePath = params.file;
1241
+ const absPath = path7.isAbsolute(filePath) ? filePath : path7.resolve(ctx.cwd, filePath);
1242
+ try {
1243
+ switch (params.action) {
1244
+ case "definition": {
1245
+ const symbol = params.symbol || await extractSymbol(absPath, params.line, params.character);
1246
+ if (!symbol) {
1247
+ return { content: "Provide a symbol name or line/character position.", isError: true };
1248
+ }
1249
+ const result = await findDefinition(symbol, ctx.cwd);
1250
+ return { content: result };
1251
+ }
1252
+ case "references": {
1253
+ const symbol = params.symbol || await extractSymbol(absPath, params.line, params.character);
1254
+ if (!symbol) {
1255
+ return { content: "Provide a symbol name or line/character position.", isError: true };
1256
+ }
1257
+ const result = await findReferences(symbol, ctx.cwd);
1258
+ return { content: result };
1259
+ }
1260
+ case "diagnostics": {
1261
+ const result = await getDiagnostics(filePath, ctx.cwd);
1262
+ return { content: result };
1263
+ }
1264
+ case "hover": {
1265
+ const symbol = params.symbol || await extractSymbol(absPath, params.line, params.character);
1266
+ if (!symbol) {
1267
+ return { content: "Provide a symbol name or line/character position.", isError: true };
1268
+ }
1269
+ const result = await getHoverInfo(symbol, filePath, params.line ?? 1, ctx.cwd);
1270
+ return { content: result };
1271
+ }
1272
+ default:
1273
+ return { content: `Unknown action: ${params.action}`, isError: true };
1274
+ }
1275
+ } catch (err) {
1276
+ return { content: `LSP error: ${err.message}`, isError: true };
1277
+ }
1278
+ }
1279
+ };
1280
+ async function extractSymbol(filePath, line, character) {
1281
+ if (!line) return null;
1282
+ try {
1283
+ const content = await fs6.readFile(filePath, "utf-8");
1284
+ const lines = content.split("\n");
1285
+ const lineText = lines[line - 1];
1286
+ if (!lineText) return null;
1287
+ const col = character ?? 0;
1288
+ const wordRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*/g;
1289
+ let match;
1290
+ while ((match = wordRegex.exec(lineText)) !== null) {
1291
+ const start = match.index;
1292
+ const end = start + match[0].length;
1293
+ if (col >= start && col <= end) {
1294
+ return match[0];
1295
+ }
1296
+ }
1297
+ const firstMatch = lineText.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/);
1298
+ return firstMatch?.[0] ?? null;
1299
+ } catch {
1300
+ return null;
1301
+ }
1302
+ }
1303
+
1304
+ // src/tools/notebook.ts
1305
+ import fs7 from "fs/promises";
1306
+ import path8 from "path";
1307
+ import { z as z11 } from "zod";
1308
+ var parameters11 = z11.object({
1309
+ action: z11.enum(["read", "edit", "add", "remove"]).describe("Action: read (view cells), edit (modify cell source), add (insert new cell), remove (delete cell)"),
1310
+ path: z11.string().describe("Path to the .ipynb notebook file"),
1311
+ cell_index: z11.number().optional().describe("Cell index (0-based) \u2014 for edit/remove actions"),
1312
+ cell_type: z11.enum(["code", "markdown", "raw"]).optional().describe("Cell type \u2014 for add action"),
1313
+ content: z11.string().optional().describe("New cell content \u2014 for edit/add actions"),
1314
+ insert_after: z11.number().optional().describe("Insert after this cell index \u2014 for add action (-1 for beginning)")
1315
+ });
1316
+ function formatNotebook(nb, filePath) {
1317
+ const parts = [
1318
+ `# ${path8.basename(filePath)}`,
1319
+ `# ${nb.cells.length} cells | nbformat ${nb.nbformat}.${nb.nbformat_minor}`,
1320
+ ""
1321
+ ];
1322
+ for (let i = 0; i < nb.cells.length; i++) {
1323
+ const cell = nb.cells[i];
1324
+ const source = cell.source.join("");
1325
+ const header = cell.cell_type === "code" ? `[${i}] Code (exec: ${cell.execution_count ?? "-"})` : `[${i}] ${cell.cell_type.charAt(0).toUpperCase() + cell.cell_type.slice(1)}`;
1326
+ parts.push(`--- ${header} ---`);
1327
+ parts.push(source);
1328
+ if (cell.cell_type === "code" && cell.outputs && cell.outputs.length > 0) {
1329
+ parts.push(" Output:");
1330
+ for (const output of cell.outputs) {
1331
+ if (output.text) {
1332
+ parts.push(" " + output.text.join("").slice(0, 500));
1333
+ } else if (output.data?.["text/plain"]) {
1334
+ parts.push(" " + output.data["text/plain"].join("").slice(0, 500));
1335
+ } else if (output.ename) {
1336
+ parts.push(` Error: ${output.ename}: ${output.evalue}`);
1337
+ }
1338
+ }
1339
+ }
1340
+ parts.push("");
1341
+ }
1342
+ return parts.join("\n");
1343
+ }
1344
+ var notebookTool = {
1345
+ name: "notebook",
1346
+ description: "Read and edit Jupyter .ipynb notebook files. Actions: read (view all cells with outputs), edit (modify a cell), add (insert a new cell), remove (delete a cell).",
1347
+ parameters: parameters11,
1348
+ async execute(params, ctx) {
1349
+ const filePath = path8.isAbsolute(params.path) ? params.path : path8.resolve(ctx.cwd, params.path);
1350
+ try {
1351
+ switch (params.action) {
1352
+ case "read": {
1353
+ const raw = await fs7.readFile(filePath, "utf-8");
1354
+ const nb = JSON.parse(raw);
1355
+ return { content: formatNotebook(nb, filePath) };
1356
+ }
1357
+ case "edit": {
1358
+ if (params.cell_index === void 0) {
1359
+ return { content: "cell_index is required for edit action.", isError: true };
1360
+ }
1361
+ if (params.content === void 0) {
1362
+ return { content: "content is required for edit action.", isError: true };
1363
+ }
1364
+ const raw = await fs7.readFile(filePath, "utf-8");
1365
+ const nb = JSON.parse(raw);
1366
+ if (params.cell_index < 0 || params.cell_index >= nb.cells.length) {
1367
+ return { content: `Cell index ${params.cell_index} out of range (0-${nb.cells.length - 1}).`, isError: true };
1368
+ }
1369
+ const lines = params.content.split("\n").map(
1370
+ (l, i, arr) => i < arr.length - 1 ? l + "\n" : l
1371
+ );
1372
+ nb.cells[params.cell_index].source = lines;
1373
+ await fs7.writeFile(filePath, JSON.stringify(nb, null, 1) + "\n");
1374
+ return { content: `Cell [${params.cell_index}] updated in ${path8.basename(filePath)}.` };
1375
+ }
1376
+ case "add": {
1377
+ if (params.content === void 0) {
1378
+ return { content: "content is required for add action.", isError: true };
1379
+ }
1380
+ const raw = await fs7.readFile(filePath, "utf-8");
1381
+ const nb = JSON.parse(raw);
1382
+ const cellType = params.cell_type ?? "code";
1383
+ const lines = params.content.split("\n").map(
1384
+ (l, i, arr) => i < arr.length - 1 ? l + "\n" : l
1385
+ );
1386
+ const newCell = {
1387
+ cell_type: cellType,
1388
+ source: lines,
1389
+ metadata: {},
1390
+ ...cellType === "code" ? { outputs: [], execution_count: null } : {}
1391
+ };
1392
+ const insertIdx = params.insert_after !== void 0 ? params.insert_after + 1 : nb.cells.length;
1393
+ nb.cells.splice(insertIdx, 0, newCell);
1394
+ await fs7.writeFile(filePath, JSON.stringify(nb, null, 1) + "\n");
1395
+ return { content: `Added ${cellType} cell at index [${insertIdx}] in ${path8.basename(filePath)}.` };
1396
+ }
1397
+ case "remove": {
1398
+ if (params.cell_index === void 0) {
1399
+ return { content: "cell_index is required for remove action.", isError: true };
1400
+ }
1401
+ const raw = await fs7.readFile(filePath, "utf-8");
1402
+ const nb = JSON.parse(raw);
1403
+ if (params.cell_index < 0 || params.cell_index >= nb.cells.length) {
1404
+ return { content: `Cell index ${params.cell_index} out of range (0-${nb.cells.length - 1}).`, isError: true };
1405
+ }
1406
+ const removed = nb.cells.splice(params.cell_index, 1)[0];
1407
+ await fs7.writeFile(filePath, JSON.stringify(nb, null, 1) + "\n");
1408
+ return { content: `Removed ${removed.cell_type} cell [${params.cell_index}] from ${path8.basename(filePath)}. ${nb.cells.length} cells remaining.` };
1409
+ }
1410
+ default:
1411
+ return { content: `Unknown action: ${params.action}`, isError: true };
1412
+ }
1413
+ } catch (err) {
1414
+ return { content: `Notebook error: ${err.message}`, isError: true };
1415
+ }
1416
+ }
1417
+ };
1418
+
1419
+ // src/tools/task.ts
1420
+ import { z as z12 } from "zod";
1421
+ import chalk from "chalk";
1422
+ var tasks = [];
1423
+ var nextId = 1;
1424
+ var parameters12 = z12.object({
1425
+ action: z12.enum(["create", "update", "list", "get"]).describe("Action: create (new task), update (change status/subject), list (show all), get (show one)"),
1426
+ subject: z12.string().optional().describe("Task title \u2014 for create action"),
1427
+ description: z12.string().optional().describe("Task details \u2014 for create action"),
1428
+ task_id: z12.number().int().positive().optional().describe("Task ID \u2014 for update/get actions"),
1429
+ status: z12.enum(["pending", "in_progress", "completed", "failed"]).optional().describe("New status \u2014 for update action")
1430
+ });
1431
+ function formatTask(t) {
1432
+ const icons = {
1433
+ pending: "\u25CB",
1434
+ in_progress: "\u25D0",
1435
+ completed: "\u25CF",
1436
+ failed: "\u2717"
1437
+ };
1438
+ const colors = {
1439
+ pending: chalk.gray,
1440
+ in_progress: chalk.yellow,
1441
+ completed: chalk.green,
1442
+ failed: chalk.red
1443
+ };
1444
+ return `${colors[t.status](icons[t.status])} #${t.id} ${colors[t.status](t.subject)} [${t.status}]`;
1445
+ }
1446
+ function formatTaskList() {
1447
+ if (tasks.length === 0) return "No tasks.";
1448
+ return tasks.map(formatTask).join("\n");
1449
+ }
1450
+ var taskTool = {
1451
+ name: "task",
1452
+ description: "Manage tasks within the current session. Use this to break complex work into tracked steps. Actions: create (new task), update (change status), list (show all tasks), get (show one task). Mark tasks as in_progress when starting and completed when done.",
1453
+ parameters: parameters12,
1454
+ async execute(params, _ctx) {
1455
+ switch (params.action) {
1456
+ case "create": {
1457
+ if (!params.subject) {
1458
+ return { content: "subject is required for create action.", isError: true };
1459
+ }
1460
+ const task = {
1461
+ id: nextId++,
1462
+ subject: params.subject,
1463
+ description: params.description,
1464
+ status: "pending",
1465
+ createdAt: Date.now(),
1466
+ updatedAt: Date.now()
1467
+ };
1468
+ tasks.push(task);
1469
+ console.log(chalk.gray(` ${formatTask(task)}`));
1470
+ return { content: `Task #${task.id} created: ${task.subject}` };
1471
+ }
1472
+ case "update": {
1473
+ if (params.task_id === void 0) {
1474
+ return { content: "task_id is required for update action.", isError: true };
1475
+ }
1476
+ const task = tasks.find((t) => t.id === params.task_id);
1477
+ if (!task) {
1478
+ return { content: `Task #${params.task_id} not found.`, isError: true };
1479
+ }
1480
+ if (params.status) task.status = params.status;
1481
+ if (params.subject) task.subject = params.subject;
1482
+ task.updatedAt = Date.now();
1483
+ console.log(chalk.gray(` ${formatTask(task)}`));
1484
+ return { content: `Task #${task.id} updated: ${task.subject} [${task.status}]` };
1485
+ }
1486
+ case "list": {
1487
+ const output = formatTaskList();
1488
+ return { content: output };
1489
+ }
1490
+ case "get": {
1491
+ if (params.task_id === void 0) {
1492
+ return { content: "task_id is required for get action.", isError: true };
1493
+ }
1494
+ const task = tasks.find((t) => t.id === params.task_id);
1495
+ if (!task) {
1496
+ return { content: `Task #${params.task_id} not found.`, isError: true };
1497
+ }
1498
+ const details = [
1499
+ formatTask(task),
1500
+ task.description ? ` ${task.description}` : "",
1501
+ ` Created: ${new Date(task.createdAt).toLocaleTimeString()}`,
1502
+ ` Updated: ${new Date(task.updatedAt).toLocaleTimeString()}`
1503
+ ].filter(Boolean).join("\n");
1504
+ return { content: details };
1505
+ }
1506
+ default:
1507
+ return { content: `Unknown action: ${params.action}`, isError: true };
1508
+ }
1509
+ }
1510
+ };
1511
+ function getAllTasks() {
1512
+ return [...tasks];
1513
+ }
1514
+ function formatTasksDisplay() {
1515
+ return formatTaskList();
1516
+ }
1517
+
1034
1518
  // src/mcp/client.ts
1519
+ import { z as z13 } from "zod";
1520
+
1521
+ // src/mcp/transport.ts
1522
+ function detectTransport(config) {
1523
+ if (config.transport) return config.transport;
1524
+ if (config.url) return "http";
1525
+ return "stdio";
1526
+ }
1527
+
1528
+ // src/mcp/stdio-transport.ts
1035
1529
  import { spawn } from "child_process";
1036
- import { z as z10 } from "zod";
1037
- var MCPClient = class {
1038
- constructor(config, serverName) {
1530
+ var StdioTransport = class {
1531
+ constructor(config, name) {
1039
1532
  this.config = config;
1040
- this.serverName = serverName;
1533
+ this.name = name;
1041
1534
  }
1042
1535
  process = null;
1043
1536
  requestId = 0;
1044
1537
  pendingRequests = /* @__PURE__ */ new Map();
1045
1538
  buffer = "";
1046
- serverName;
1047
- _tools = [];
1048
- /**
1049
- * Start the MCP server and initialize the connection.
1050
- */
1539
+ name;
1051
1540
  async connect() {
1541
+ if (!this.config.command) {
1542
+ throw new Error(`Stdio transport requires 'command' in config for server ${this.name}`);
1543
+ }
1052
1544
  this.process = spawn(this.config.command, this.config.args ?? [], {
1053
1545
  stdio: ["pipe", "pipe", "pipe"],
1054
1546
  env: { ...process.env, ...this.config.env },
@@ -1061,54 +1553,17 @@ var MCPClient = class {
1061
1553
  });
1062
1554
  this.process.on("error", (err) => {
1063
1555
  for (const [id, pending] of this.pendingRequests) {
1064
- pending.reject(new Error(`MCP server ${this.serverName} error: ${err.message}`));
1556
+ pending.reject(new Error(`MCP server ${this.name} error: ${err.message}`));
1065
1557
  this.pendingRequests.delete(id);
1066
1558
  }
1067
1559
  });
1068
1560
  this.process.on("exit", (code) => {
1069
1561
  for (const [id, pending] of this.pendingRequests) {
1070
- pending.reject(new Error(`MCP server ${this.serverName} exited with code ${code}`));
1562
+ pending.reject(new Error(`MCP server ${this.name} exited with code ${code}`));
1071
1563
  this.pendingRequests.delete(id);
1072
1564
  }
1073
1565
  });
1074
- await this.sendRequest("initialize", {
1075
- protocolVersion: "2024-11-05",
1076
- capabilities: {},
1077
- clientInfo: { name: "notch-cli", version: "0.3.0" }
1078
- });
1079
- this.sendNotification("notifications/initialized", {});
1080
- const result = await this.sendRequest("tools/list", {});
1081
- this._tools = result.tools ?? [];
1082
- }
1083
- /**
1084
- * Get discovered tools from this server.
1085
- */
1086
- get tools() {
1087
- return this._tools;
1088
- }
1089
- /**
1090
- * Check if the MCP server process is still alive.
1091
- */
1092
- get isAlive() {
1093
- return this.process !== null && this.process.exitCode === null && !this.process.killed;
1094
- }
1095
- /**
1096
- * Call a tool on the MCP server. Auto-reconnects if the server has crashed.
1097
- */
1098
- async callTool(name, args) {
1099
- if (!this.isAlive) {
1100
- try {
1101
- await this.connect();
1102
- } catch (err) {
1103
- throw new Error(`MCP server ${this.serverName} is down and could not reconnect: ${err.message}`);
1104
- }
1105
- }
1106
- const result = await this.sendRequest("tools/call", { name, arguments: args });
1107
- return result;
1108
1566
  }
1109
- /**
1110
- * Disconnect from the MCP server.
1111
- */
1112
1567
  disconnect() {
1113
1568
  if (this.process) {
1114
1569
  this.process.stdin?.end();
@@ -1117,15 +1572,13 @@ var MCPClient = class {
1117
1572
  }
1118
1573
  this.pendingRequests.clear();
1119
1574
  }
1575
+ get isAlive() {
1576
+ return this.process !== null && this.process.exitCode === null && !this.process.killed;
1577
+ }
1120
1578
  sendRequest(method, params) {
1121
1579
  return new Promise((resolve2, reject) => {
1122
1580
  const id = ++this.requestId;
1123
- const msg = {
1124
- jsonrpc: "2.0",
1125
- id,
1126
- method,
1127
- params
1128
- };
1581
+ const msg = { jsonrpc: "2.0", id, method, params };
1129
1582
  this.pendingRequests.set(id, { resolve: resolve2, reject });
1130
1583
  const data = JSON.stringify(msg);
1131
1584
  const header = `Content-Length: ${Buffer.byteLength(data)}\r
@@ -1141,11 +1594,7 @@ var MCPClient = class {
1141
1594
  });
1142
1595
  }
1143
1596
  sendNotification(method, params) {
1144
- const msg = {
1145
- jsonrpc: "2.0",
1146
- method,
1147
- params
1148
- };
1597
+ const msg = { jsonrpc: "2.0", method, params };
1149
1598
  const data = JSON.stringify(msg);
1150
1599
  const header = `Content-Length: ${Buffer.byteLength(data)}\r
1151
1600
  \r
@@ -1190,41 +1639,500 @@ var MCPClient = class {
1190
1639
  }
1191
1640
  }
1192
1641
  };
1193
- function parseMCPConfig(config) {
1194
- const servers = config?.mcpServers;
1195
- if (!servers || typeof servers !== "object") return {};
1196
- const result = {};
1197
- for (const [name, cfg] of Object.entries(servers)) {
1198
- const c = cfg;
1199
- if (c?.command) {
1200
- result[name] = {
1201
- command: c.command,
1202
- args: c.args,
1203
- env: c.env,
1204
- cwd: c.cwd
1205
- };
1642
+
1643
+ // src/mcp/http-transport.ts
1644
+ var HttpTransport = class {
1645
+ requestId = 0;
1646
+ connected = false;
1647
+ name;
1648
+ baseUrl;
1649
+ headers;
1650
+ constructor(config, name) {
1651
+ this.name = name;
1652
+ if (!config.url) {
1653
+ throw new Error(`HTTP transport requires 'url' in config for server ${name}`);
1654
+ }
1655
+ this.baseUrl = config.url.replace(/\/$/, "");
1656
+ this.headers = {
1657
+ "Content-Type": "application/json",
1658
+ ...config.headers
1659
+ };
1660
+ }
1661
+ async connect() {
1662
+ try {
1663
+ const response = await fetch(`${this.baseUrl}`, {
1664
+ method: "POST",
1665
+ headers: this.headers,
1666
+ body: JSON.stringify({
1667
+ jsonrpc: "2.0",
1668
+ id: ++this.requestId,
1669
+ method: "initialize",
1670
+ params: {
1671
+ protocolVersion: "2024-11-05",
1672
+ capabilities: {},
1673
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1674
+ }
1675
+ }),
1676
+ signal: AbortSignal.timeout(15e3)
1677
+ });
1678
+ if (!response.ok) {
1679
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1680
+ }
1681
+ this.sendNotification("notifications/initialized", {});
1682
+ this.connected = true;
1683
+ } catch (err) {
1684
+ throw new Error(`MCP HTTP server ${this.name} unreachable: ${err.message}`);
1206
1685
  }
1207
1686
  }
1208
- return result;
1209
- }
1687
+ disconnect() {
1688
+ this.connected = false;
1689
+ }
1690
+ get isAlive() {
1691
+ return this.connected;
1692
+ }
1693
+ async sendRequest(method, params) {
1694
+ const id = ++this.requestId;
1695
+ const body = { jsonrpc: "2.0", id, method, params };
1696
+ const response = await fetch(this.baseUrl, {
1697
+ method: "POST",
1698
+ headers: this.headers,
1699
+ body: JSON.stringify(body),
1700
+ signal: AbortSignal.timeout(3e4)
1701
+ });
1702
+ if (!response.ok) {
1703
+ throw new Error(`MCP HTTP error (${this.name}): ${response.status} ${response.statusText}`);
1704
+ }
1705
+ const json = await response.json();
1706
+ if (json.error) {
1707
+ throw new Error(`MCP error (${this.name}): ${json.error.message}`);
1708
+ }
1709
+ return json.result;
1710
+ }
1711
+ sendNotification(method, params) {
1712
+ void fetch(this.baseUrl, {
1713
+ method: "POST",
1714
+ headers: this.headers,
1715
+ body: JSON.stringify({ jsonrpc: "2.0", method, params }),
1716
+ signal: AbortSignal.timeout(1e4)
1717
+ }).catch(() => {
1718
+ });
1719
+ }
1720
+ };
1210
1721
 
1211
- // src/tools/index.ts
1212
- var BUILTIN_TOOLS = [
1213
- readTool,
1214
- writeTool,
1215
- editTool,
1216
- shellTool,
1217
- gitTool,
1218
- githubTool,
1219
- grepTool,
1220
- globTool,
1221
- webFetchTool
1222
- ];
1223
- var mcpTools = [];
1224
- function getAllTools() {
1225
- return [...BUILTIN_TOOLS, ...mcpTools];
1226
- }
1227
- function buildToolMap(ctx) {
1722
+ // src/mcp/sse-transport.ts
1723
+ var SSETransport = class {
1724
+ requestId = 0;
1725
+ pendingRequests = /* @__PURE__ */ new Map();
1726
+ abortController = null;
1727
+ connected = false;
1728
+ name;
1729
+ baseUrl;
1730
+ messageEndpoint;
1731
+ sseEndpoint;
1732
+ headers;
1733
+ constructor(config, name) {
1734
+ this.name = name;
1735
+ if (!config.url) {
1736
+ throw new Error(`SSE transport requires 'url' in config for server ${name}`);
1737
+ }
1738
+ this.baseUrl = config.url.replace(/\/$/, "");
1739
+ this.sseEndpoint = `${this.baseUrl}/sse`;
1740
+ this.messageEndpoint = `${this.baseUrl}/message`;
1741
+ this.headers = {
1742
+ "Content-Type": "application/json",
1743
+ ...config.headers
1744
+ };
1745
+ }
1746
+ async connect() {
1747
+ this.abortController = new AbortController();
1748
+ const ssePromise = this.startSSEListener();
1749
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
1750
+ const initResult = await this.sendRequest("initialize", {
1751
+ protocolVersion: "2024-11-05",
1752
+ capabilities: {},
1753
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1754
+ });
1755
+ if (!initResult) {
1756
+ throw new Error(`MCP SSE server ${this.name} failed to initialize`);
1757
+ }
1758
+ this.sendNotification("notifications/initialized", {});
1759
+ this.connected = true;
1760
+ ssePromise.catch(() => {
1761
+ this.connected = false;
1762
+ });
1763
+ }
1764
+ disconnect() {
1765
+ this.abortController?.abort();
1766
+ this.abortController = null;
1767
+ this.connected = false;
1768
+ for (const [id, pending] of this.pendingRequests) {
1769
+ pending.reject(new Error(`MCP SSE transport disconnected`));
1770
+ this.pendingRequests.delete(id);
1771
+ }
1772
+ }
1773
+ get isAlive() {
1774
+ return this.connected;
1775
+ }
1776
+ async sendRequest(method, params) {
1777
+ const id = ++this.requestId;
1778
+ const body = { jsonrpc: "2.0", id, method, params };
1779
+ const resultPromise = new Promise((resolve2, reject) => {
1780
+ this.pendingRequests.set(id, { resolve: resolve2, reject });
1781
+ setTimeout(() => {
1782
+ if (this.pendingRequests.has(id)) {
1783
+ this.pendingRequests.delete(id);
1784
+ reject(new Error(`MCP SSE request ${method} timed out`));
1785
+ }
1786
+ }, 3e4);
1787
+ });
1788
+ const response = await fetch(this.messageEndpoint, {
1789
+ method: "POST",
1790
+ headers: this.headers,
1791
+ body: JSON.stringify(body),
1792
+ signal: AbortSignal.timeout(1e4)
1793
+ });
1794
+ if (!response.ok) {
1795
+ this.pendingRequests.delete(id);
1796
+ throw new Error(`MCP SSE POST error (${this.name}): ${response.status} ${response.statusText}`);
1797
+ }
1798
+ return resultPromise;
1799
+ }
1800
+ sendNotification(method, params) {
1801
+ const body = { jsonrpc: "2.0", method, params };
1802
+ void fetch(this.messageEndpoint, {
1803
+ method: "POST",
1804
+ headers: this.headers,
1805
+ body: JSON.stringify(body),
1806
+ signal: AbortSignal.timeout(1e4)
1807
+ }).catch(() => {
1808
+ });
1809
+ }
1810
+ /**
1811
+ * Start listening for Server-Sent Events.
1812
+ * Parses the SSE stream and resolves pending requests.
1813
+ */
1814
+ async startSSEListener() {
1815
+ const response = await fetch(this.sseEndpoint, {
1816
+ headers: {
1817
+ Accept: "text/event-stream",
1818
+ ...this.headers
1819
+ },
1820
+ signal: this.abortController?.signal
1821
+ });
1822
+ if (!response.ok || !response.body) {
1823
+ throw new Error(`SSE connection failed: ${response.status}`);
1824
+ }
1825
+ const reader = response.body.getReader();
1826
+ const decoder = new TextDecoder();
1827
+ let buffer = "";
1828
+ try {
1829
+ while (true) {
1830
+ const { done, value } = await reader.read();
1831
+ if (done) break;
1832
+ buffer += decoder.decode(value, { stream: true });
1833
+ const events = buffer.split("\n\n");
1834
+ buffer = events.pop() ?? "";
1835
+ for (const event of events) {
1836
+ const lines = event.split("\n");
1837
+ let data = "";
1838
+ for (const line of lines) {
1839
+ if (line.startsWith("data: ")) {
1840
+ data += line.slice(6);
1841
+ }
1842
+ }
1843
+ if (data) {
1844
+ this.handleSSEMessage(data);
1845
+ }
1846
+ }
1847
+ }
1848
+ } catch (err) {
1849
+ if (err.name !== "AbortError") {
1850
+ this.connected = false;
1851
+ }
1852
+ }
1853
+ }
1854
+ handleSSEMessage(raw) {
1855
+ try {
1856
+ const msg = JSON.parse(raw);
1857
+ if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
1858
+ const pending = this.pendingRequests.get(msg.id);
1859
+ this.pendingRequests.delete(msg.id);
1860
+ if (msg.error) {
1861
+ pending.reject(new Error(`MCP SSE error: ${msg.error.message}`));
1862
+ } else {
1863
+ pending.resolve(msg.result);
1864
+ }
1865
+ }
1866
+ } catch {
1867
+ }
1868
+ }
1869
+ };
1870
+
1871
+ // src/mcp/client.ts
1872
+ function createTransport(config, name) {
1873
+ const type = detectTransport(config);
1874
+ switch (type) {
1875
+ case "http":
1876
+ return new HttpTransport(config, name);
1877
+ case "sse":
1878
+ return new SSETransport(config, name);
1879
+ case "stdio":
1880
+ default:
1881
+ return new StdioTransport(config, name);
1882
+ }
1883
+ }
1884
+ var MCPClient = class {
1885
+ transport;
1886
+ serverName;
1887
+ _tools = [];
1888
+ constructor(config, serverName) {
1889
+ this.serverName = serverName;
1890
+ this.transport = createTransport(config, serverName);
1891
+ }
1892
+ /**
1893
+ * Start the MCP server and initialize the connection.
1894
+ */
1895
+ async connect() {
1896
+ await this.transport.connect();
1897
+ await this.transport.sendRequest("initialize", {
1898
+ protocolVersion: "2024-11-05",
1899
+ capabilities: {},
1900
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1901
+ });
1902
+ this.transport.sendNotification("notifications/initialized", {});
1903
+ const result = await this.transport.sendRequest("tools/list", {});
1904
+ this._tools = result.tools ?? [];
1905
+ }
1906
+ /**
1907
+ * Get discovered tools from this server.
1908
+ */
1909
+ get tools() {
1910
+ return this._tools;
1911
+ }
1912
+ /**
1913
+ * Check if the MCP server is still alive.
1914
+ */
1915
+ get isAlive() {
1916
+ return this.transport.isAlive;
1917
+ }
1918
+ /**
1919
+ * Call a tool on the MCP server. Auto-reconnects if the server has crashed.
1920
+ */
1921
+ async callTool(name, args) {
1922
+ if (!this.isAlive) {
1923
+ try {
1924
+ await this.transport.connect();
1925
+ } catch (err) {
1926
+ throw new Error(`MCP server ${this.serverName} is down and could not reconnect: ${err.message}`);
1927
+ }
1928
+ }
1929
+ return this.transport.sendRequest("tools/call", { name, arguments: args });
1930
+ }
1931
+ /**
1932
+ * Disconnect from the MCP server.
1933
+ */
1934
+ disconnect() {
1935
+ this.transport.disconnect();
1936
+ }
1937
+ };
1938
+ function parseMCPConfig(config) {
1939
+ const servers = config?.mcpServers;
1940
+ if (!servers || typeof servers !== "object") return {};
1941
+ const result = {};
1942
+ for (const [name, cfg] of Object.entries(servers)) {
1943
+ const c = cfg;
1944
+ if (c?.command || c?.url) {
1945
+ result[name] = {
1946
+ command: c.command,
1947
+ args: c.args,
1948
+ env: c.env,
1949
+ cwd: c.cwd,
1950
+ transport: c.transport,
1951
+ url: c.url,
1952
+ headers: c.headers
1953
+ };
1954
+ }
1955
+ }
1956
+ return result;
1957
+ }
1958
+
1959
+ // src/plugins/discovery.ts
1960
+ import fs8 from "fs/promises";
1961
+ import path9 from "path";
1962
+ import os from "os";
1963
+ async function discoverPlugins(projectRoot) {
1964
+ const globalDir = path9.join(os.homedir(), ".notch", "plugins");
1965
+ const projectDir = path9.join(projectRoot, ".notch", "plugins");
1966
+ const plugins = /* @__PURE__ */ new Map();
1967
+ for (const plugin of await scanDirectory(globalDir)) {
1968
+ plugins.set(plugin.name, plugin);
1969
+ }
1970
+ for (const plugin of await scanDirectory(projectDir)) {
1971
+ plugins.set(plugin.name, plugin);
1972
+ }
1973
+ return [...plugins.values()];
1974
+ }
1975
+ async function scanDirectory(dir) {
1976
+ const results = [];
1977
+ try {
1978
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1979
+ for (const entry of entries) {
1980
+ if (!entry.isDirectory()) continue;
1981
+ const pluginDir = path9.join(dir, entry.name);
1982
+ const pkgPath = path9.join(pluginDir, "package.json");
1983
+ try {
1984
+ const raw = await fs8.readFile(pkgPath, "utf-8");
1985
+ const pkg = JSON.parse(raw);
1986
+ if (pkg["notch-plugin"] && pkg.name) {
1987
+ results.push({
1988
+ name: pkg.name,
1989
+ version: pkg.version ?? "0.0.0",
1990
+ description: pkg.description ?? "",
1991
+ path: pluginDir,
1992
+ manifest: pkg["notch-plugin"]
1993
+ });
1994
+ }
1995
+ } catch {
1996
+ }
1997
+ }
1998
+ } catch {
1999
+ }
2000
+ return results;
2001
+ }
2002
+
2003
+ // src/plugins/loader.ts
2004
+ import path10 from "path";
2005
+ import { pathToFileURL } from "url";
2006
+
2007
+ // src/commands/registry.ts
2008
+ var registry = /* @__PURE__ */ new Map();
2009
+ function registerCommand(name, handler) {
2010
+ registry.set(name, handler);
2011
+ }
2012
+ async function dispatchCommand(input, ctx) {
2013
+ for (const [name, handler] of registry) {
2014
+ if (input === name || input.startsWith(name + " ")) {
2015
+ const args = input.slice(name.length).trim();
2016
+ await handler(args, ctx);
2017
+ return true;
2018
+ }
2019
+ }
2020
+ return false;
2021
+ }
2022
+
2023
+ // src/plugins/loader.ts
2024
+ async function loadPlugin(discovered, projectRoot, log) {
2025
+ const entryPath = path10.resolve(discovered.path, discovered.manifest.main);
2026
+ const tools = [];
2027
+ const hooks = [];
2028
+ const ctx = {
2029
+ cwd: projectRoot,
2030
+ log,
2031
+ registerTool(tool2) {
2032
+ tools.push(tool2);
2033
+ },
2034
+ registerCommand(name, handler) {
2035
+ registerCommand(name, handler);
2036
+ },
2037
+ registerHook(hook) {
2038
+ hooks.push(hook);
2039
+ }
2040
+ };
2041
+ try {
2042
+ const moduleUrl = pathToFileURL(entryPath).href;
2043
+ const mod = await import(moduleUrl);
2044
+ const plugin = mod.default ?? mod;
2045
+ if (typeof plugin.activate !== "function") {
2046
+ log(`Plugin ${discovered.name}: no activate() function \u2014 skipping`);
2047
+ return null;
2048
+ }
2049
+ await plugin.activate(ctx);
2050
+ return {
2051
+ name: discovered.name,
2052
+ version: discovered.version,
2053
+ description: discovered.description,
2054
+ path: discovered.path,
2055
+ instance: plugin,
2056
+ tools,
2057
+ hooks
2058
+ };
2059
+ } catch (err) {
2060
+ log(`Plugin ${discovered.name} failed to load: ${err.message}`);
2061
+ return null;
2062
+ }
2063
+ }
2064
+
2065
+ // src/plugins/index.ts
2066
+ var PluginManager = class {
2067
+ plugins = [];
2068
+ initialized = false;
2069
+ /**
2070
+ * Discover and load all plugins. Call once at startup.
2071
+ */
2072
+ async init(projectRoot, log) {
2073
+ if (this.initialized) return this.plugins.length;
2074
+ const discovered = await discoverPlugins(projectRoot);
2075
+ if (discovered.length === 0) {
2076
+ this.initialized = true;
2077
+ return 0;
2078
+ }
2079
+ for (const d of discovered) {
2080
+ const loaded = await loadPlugin(d, projectRoot, log);
2081
+ if (loaded) {
2082
+ this.plugins.push(loaded);
2083
+ }
2084
+ }
2085
+ this.initialized = true;
2086
+ return this.plugins.length;
2087
+ }
2088
+ /** All tools registered by plugins */
2089
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2090
+ getTools() {
2091
+ return this.plugins.flatMap((p) => p.tools);
2092
+ }
2093
+ /** All hooks registered by plugins */
2094
+ getHooks() {
2095
+ return this.plugins.flatMap((p) => p.hooks);
2096
+ }
2097
+ /** List all loaded plugins */
2098
+ list() {
2099
+ return [...this.plugins];
2100
+ }
2101
+ /** Shut down all plugins gracefully */
2102
+ async shutdown() {
2103
+ for (const p of this.plugins) {
2104
+ try {
2105
+ await p.instance.deactivate?.();
2106
+ } catch (err) {
2107
+ console.warn(` Plugin ${p.name} deactivation failed: ${err.message}`);
2108
+ }
2109
+ }
2110
+ this.plugins = [];
2111
+ this.initialized = false;
2112
+ }
2113
+ };
2114
+ var pluginManager = new PluginManager();
2115
+
2116
+ // src/tools/index.ts
2117
+ var BUILTIN_TOOLS = [
2118
+ readTool,
2119
+ writeTool,
2120
+ editTool,
2121
+ shellTool,
2122
+ gitTool,
2123
+ githubTool,
2124
+ grepTool,
2125
+ globTool,
2126
+ webFetchTool,
2127
+ lspTool,
2128
+ notebookTool,
2129
+ taskTool
2130
+ ];
2131
+ var mcpTools = [];
2132
+ function getAllTools() {
2133
+ return [...BUILTIN_TOOLS, ...mcpTools, ...pluginManager.getTools()];
2134
+ }
2135
+ function buildToolMap(ctx) {
1228
2136
  const map = {};
1229
2137
  for (const t of getAllTools()) {
1230
2138
  map[t.name] = tool({
@@ -1232,8 +2140,10 @@ function buildToolMap(ctx) {
1232
2140
  parameters: t.parameters,
1233
2141
  execute: async (params) => {
1234
2142
  if (ctx.checkPermission) {
2143
+ await ctx.runHook?.("permission-request", { tool: t.name, args: params });
1235
2144
  const level = ctx.checkPermission(t.name, params);
1236
2145
  if (level === "deny") {
2146
+ await ctx.runHook?.("permission-denied", { tool: t.name, args: params, reason: "denied-by-config" });
1237
2147
  return {
1238
2148
  content: `Permission denied: ${t.name} is not allowed by your permission config.`,
1239
2149
  isError: true
@@ -1245,6 +2155,7 @@ function buildToolMap(ctx) {
1245
2155
  `Tool ${t.name}(${paramSummary}) requires approval. Proceed?`
1246
2156
  );
1247
2157
  if (!confirmed) {
2158
+ await ctx.runHook?.("permission-denied", { tool: t.name, args: params, reason: "cancelled-by-user" });
1248
2159
  return { content: "Cancelled by user.", isError: true };
1249
2160
  }
1250
2161
  }
@@ -1277,15 +2188,15 @@ function describeTools() {
1277
2188
  }
1278
2189
 
1279
2190
  // src/context/project-instructions.ts
1280
- import fs6 from "fs/promises";
1281
- import path7 from "path";
1282
- import os from "os";
2191
+ import fs9 from "fs/promises";
2192
+ import path11 from "path";
2193
+ import os2 from "os";
1283
2194
  var INSTRUCTION_FILES = [".notch.md", "NOTCH.md", ".notch/instructions.md"];
1284
2195
  async function loadProjectInstructions(projectRoot) {
1285
2196
  const sources = [];
1286
- const homeDir = os.homedir();
2197
+ const homeDir = os2.homedir();
1287
2198
  for (const file of INSTRUCTION_FILES) {
1288
- const globalPath = path7.join(homeDir, file);
2199
+ const globalPath = path11.join(homeDir, file);
1289
2200
  const content = await safeRead(globalPath);
1290
2201
  if (content) {
1291
2202
  sources.push({ path: globalPath, content, scope: "global" });
@@ -1293,7 +2204,7 @@ async function loadProjectInstructions(projectRoot) {
1293
2204
  }
1294
2205
  }
1295
2206
  for (const file of INSTRUCTION_FILES) {
1296
- const projectPath = path7.join(projectRoot, file);
2207
+ const projectPath = path11.join(projectRoot, file);
1297
2208
  const content = await safeRead(projectPath);
1298
2209
  if (content) {
1299
2210
  sources.push({ path: projectPath, content, scope: "project" });
@@ -1315,7 +2226,7 @@ ${sections.join("\n\n")}`;
1315
2226
  }
1316
2227
  async function safeRead(filePath) {
1317
2228
  try {
1318
- const content = await fs6.readFile(filePath, "utf-8");
2229
+ const content = await fs9.readFile(filePath, "utf-8");
1319
2230
  return content.trim() || null;
1320
2231
  } catch {
1321
2232
  return null;
@@ -1323,19 +2234,19 @@ async function safeRead(filePath) {
1323
2234
  }
1324
2235
 
1325
2236
  // src/memory/store.ts
1326
- import fs7 from "fs/promises";
1327
- import path8 from "path";
1328
- import os2 from "os";
1329
- var MEMORY_DIR = path8.join(os2.homedir(), ".notch", "memory");
1330
- var INDEX_FILE = path8.join(MEMORY_DIR, "MEMORY.md");
2237
+ import fs10 from "fs/promises";
2238
+ import path12 from "path";
2239
+ import os3 from "os";
2240
+ var MEMORY_DIR = path12.join(os3.homedir(), ".notch", "memory");
2241
+ var INDEX_FILE = path12.join(MEMORY_DIR, "MEMORY.md");
1331
2242
  async function ensureDir() {
1332
- await fs7.mkdir(MEMORY_DIR, { recursive: true });
2243
+ await fs10.mkdir(MEMORY_DIR, { recursive: true });
1333
2244
  }
1334
2245
  async function saveMemory(memory) {
1335
2246
  await ensureDir();
1336
2247
  const slug = memory.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1337
2248
  const filename = `${memory.type}_${slug}.md`;
1338
- const filePath = path8.join(MEMORY_DIR, filename);
2249
+ const filePath = path12.join(MEMORY_DIR, filename);
1339
2250
  const fileContent = [
1340
2251
  "---",
1341
2252
  `name: ${memory.name}`,
@@ -1346,18 +2257,18 @@ async function saveMemory(memory) {
1346
2257
  "",
1347
2258
  memory.content
1348
2259
  ].join("\n");
1349
- await fs7.writeFile(filePath, fileContent, "utf-8");
2260
+ await fs10.writeFile(filePath, fileContent, "utf-8");
1350
2261
  await updateIndex();
1351
2262
  return filename;
1352
2263
  }
1353
2264
  async function loadMemories() {
1354
2265
  await ensureDir();
1355
- const files = await fs7.readdir(MEMORY_DIR);
2266
+ const files = await fs10.readdir(MEMORY_DIR);
1356
2267
  const memories = [];
1357
2268
  for (const file of files) {
1358
2269
  if (!file.endsWith(".md") || file === "MEMORY.md") continue;
1359
2270
  try {
1360
- const content = await fs7.readFile(path8.join(MEMORY_DIR, file), "utf-8");
2271
+ const content = await fs10.readFile(path12.join(MEMORY_DIR, file), "utf-8");
1361
2272
  const memory = parseMemoryFile(content, file);
1362
2273
  if (memory) memories.push(memory);
1363
2274
  } catch {
@@ -1367,7 +2278,7 @@ async function loadMemories() {
1367
2278
  }
1368
2279
  async function deleteMemory(filename) {
1369
2280
  try {
1370
- await fs7.unlink(path8.join(MEMORY_DIR, filename));
2281
+ await fs10.unlink(path12.join(MEMORY_DIR, filename));
1371
2282
  await updateIndex();
1372
2283
  return true;
1373
2284
  } catch {
@@ -1443,7 +2354,7 @@ async function updateIndex() {
1443
2354
  }
1444
2355
  lines.push("");
1445
2356
  }
1446
- await fs7.writeFile(INDEX_FILE, lines.join("\n"), "utf-8");
2357
+ await fs10.writeFile(INDEX_FILE, lines.join("\n"), "utf-8");
1447
2358
  }
1448
2359
 
1449
2360
  // src/agent/loop.ts
@@ -1462,10 +2373,14 @@ async function runAgentLoop(messages, config) {
1462
2373
  let totalCompletionTokens = 0;
1463
2374
  let wasCompressed = false;
1464
2375
  let history = [...messages];
2376
+ await config.toolContext.runHook?.("pre-compact", { messageCount: history.length });
1465
2377
  history = await autoCompress(history, config.model, contextWindow, () => {
1466
2378
  wasCompressed = true;
1467
2379
  config.onCompress?.();
1468
2380
  });
2381
+ if (wasCompressed) {
2382
+ await config.toolContext.runHook?.("post-compact", { messageCount: history.length });
2383
+ }
1469
2384
  while (iterations < maxIter) {
1470
2385
  iterations++;
1471
2386
  const result = streamText({
@@ -1537,10 +2452,15 @@ async function runAgentLoop(messages, config) {
1537
2452
  }))
1538
2453
  });
1539
2454
  if (iterations % 5 === 0) {
2455
+ const prevLen = history.length;
2456
+ await config.toolContext.runHook?.("pre-compact", { messageCount: prevLen });
1540
2457
  history = await autoCompress(history, config.model, contextWindow, () => {
1541
2458
  wasCompressed = true;
1542
2459
  config.onCompress?.();
1543
2460
  });
2461
+ if (history.length < prevLen) {
2462
+ await config.toolContext.runHook?.("post-compact", { messageCount: history.length });
2463
+ }
1544
2464
  }
1545
2465
  continue;
1546
2466
  }
@@ -1623,8 +2543,8 @@ async function buildSystemPrompt(projectRoot, modelId) {
1623
2543
  }
1624
2544
 
1625
2545
  // src/agent/checkpoints.ts
1626
- import fs8 from "fs/promises";
1627
- import path9 from "path";
2546
+ import fs11 from "fs/promises";
2547
+ import path13 from "path";
1628
2548
  var CheckpointManager = class {
1629
2549
  checkpoints = [];
1630
2550
  nextId = 1;
@@ -1633,7 +2553,7 @@ var CheckpointManager = class {
1633
2553
  async recordBefore(filePath) {
1634
2554
  if (this.pendingFiles.has(filePath)) return;
1635
2555
  try {
1636
- const content = await fs8.readFile(filePath, "utf-8");
2556
+ const content = await fs11.readFile(filePath, "utf-8");
1637
2557
  this.pendingFiles.set(filePath, content);
1638
2558
  } catch {
1639
2559
  this.pendingFiles.set(filePath, null);
@@ -1645,7 +2565,7 @@ var CheckpointManager = class {
1645
2565
  for (const [filePath, before] of this.pendingFiles) {
1646
2566
  let after = null;
1647
2567
  try {
1648
- after = await fs8.readFile(filePath, "utf-8");
2568
+ after = await fs11.readFile(filePath, "utf-8");
1649
2569
  } catch {
1650
2570
  }
1651
2571
  files.push({ path: filePath, before, after });
@@ -1667,12 +2587,12 @@ var CheckpointManager = class {
1667
2587
  for (const snap of checkpoint.files) {
1668
2588
  if (snap.before === null) {
1669
2589
  try {
1670
- await fs8.unlink(snap.path);
2590
+ await fs11.unlink(snap.path);
1671
2591
  } catch {
1672
2592
  }
1673
2593
  } else {
1674
- await fs8.mkdir(path9.dirname(snap.path), { recursive: true });
1675
- await fs8.writeFile(snap.path, snap.before, "utf-8");
2594
+ await fs11.mkdir(path13.dirname(snap.path), { recursive: true });
2595
+ await fs11.writeFile(snap.path, snap.before, "utf-8");
1676
2596
  }
1677
2597
  }
1678
2598
  return checkpoint;
@@ -1704,8 +2624,8 @@ var CheckpointManager = class {
1704
2624
  }
1705
2625
  }
1706
2626
  }
1707
- return Array.from(fileMap.entries()).map(([path19, { before, after }]) => ({
1708
- path: path19,
2627
+ return Array.from(fileMap.entries()).map(([path25, { before, after }]) => ({
2628
+ path: path25,
1709
2629
  before,
1710
2630
  after
1711
2631
  }));
@@ -1713,7 +2633,7 @@ var CheckpointManager = class {
1713
2633
  };
1714
2634
 
1715
2635
  // src/agent/usage.ts
1716
- import chalk from "chalk";
2636
+ import chalk2 from "chalk";
1717
2637
  var UsageTracker = class {
1718
2638
  turns = [];
1719
2639
  record(usage) {
@@ -1738,18 +2658,18 @@ var UsageTracker = class {
1738
2658
  const last = this.turns[this.turns.length - 1];
1739
2659
  if (!last) return "";
1740
2660
  const t = last.totalTokens;
1741
- const label = t > 1e4 ? chalk.yellow(`${(t / 1e3).toFixed(1)}K`) : chalk.gray(`${t}`);
1742
- return chalk.gray(` [${label} tokens, ${last.toolCalls} tool calls, ${last.iterations} rounds]`);
2661
+ const label = t > 1e4 ? chalk2.yellow(`${(t / 1e3).toFixed(1)}K`) : chalk2.gray(`${t}`);
2662
+ return chalk2.gray(` [${label} tokens, ${last.toolCalls} tool calls, ${last.iterations} rounds]`);
1743
2663
  }
1744
2664
  formatSession() {
1745
2665
  const total = this.sessionTotal;
1746
2666
  return [
1747
- chalk.gray(`
2667
+ chalk2.gray(`
1748
2668
  Session usage (${this.turnCount} turns):`),
1749
- chalk.gray(` Prompt: ${total.promptTokens.toLocaleString()} tokens`),
1750
- chalk.gray(` Completion: ${total.completionTokens.toLocaleString()} tokens`),
1751
- chalk.gray(` Total: ${total.totalTokens.toLocaleString()} tokens`),
1752
- chalk.gray(` Tool calls: ${total.toolCalls}`)
2669
+ chalk2.gray(` Prompt: ${total.promptTokens.toLocaleString()} tokens`),
2670
+ chalk2.gray(` Completion: ${total.completionTokens.toLocaleString()} tokens`),
2671
+ chalk2.gray(` Total: ${total.totalTokens.toLocaleString()} tokens`),
2672
+ chalk2.gray(` Tool calls: ${total.toolCalls}`)
1753
2673
  ].join("\n");
1754
2674
  }
1755
2675
  };
@@ -1945,7 +2865,7 @@ function nextSubagentId(type) {
1945
2865
 
1946
2866
  // src/agent/planner.ts
1947
2867
  import { generateText as generateText2 } from "ai";
1948
- import chalk2 from "chalk";
2868
+ import chalk3 from "chalk";
1949
2869
  var PLAN_SYSTEM_PROMPT = `You are a planning assistant. Given a task, produce a structured implementation plan.
1950
2870
 
1951
2871
  Respond in EXACTLY this format (no other text):
@@ -2037,25 +2957,25 @@ function parsePlan(text) {
2037
2957
  function formatPlan(plan) {
2038
2958
  const lines = [];
2039
2959
  lines.push("");
2040
- lines.push(chalk2.bold.white(` Plan: ${plan.summary}`));
2960
+ lines.push(chalk3.bold.white(` Plan: ${plan.summary}`));
2041
2961
  lines.push("");
2042
2962
  for (const step of plan.steps) {
2043
- const icon = step.status === "done" ? chalk2.green("\u2713") : step.status === "in_progress" ? chalk2.yellow("\u25B6") : step.status === "failed" ? chalk2.red("\u2717") : step.status === "skipped" ? chalk2.gray("\u2013") : chalk2.gray("\u25CB");
2044
- const num = chalk2.gray(`${step.index + 1}.`);
2045
- const action = step.status === "done" ? chalk2.gray(step.action) : chalk2.white(step.action);
2046
- const files = step.files.length > 0 ? chalk2.cyan(` [${step.files.join(", ")}]`) : "";
2963
+ const icon = step.status === "done" ? chalk3.green("\u2713") : step.status === "in_progress" ? chalk3.yellow("\u25B6") : step.status === "failed" ? chalk3.red("\u2717") : step.status === "skipped" ? chalk3.gray("\u2013") : chalk3.gray("\u25CB");
2964
+ const num = chalk3.gray(`${step.index + 1}.`);
2965
+ const action = step.status === "done" ? chalk3.gray(step.action) : chalk3.white(step.action);
2966
+ const files = step.files.length > 0 ? chalk3.cyan(` [${step.files.join(", ")}]`) : "";
2047
2967
  lines.push(` ${icon} ${num} ${action}${files}`);
2048
2968
  }
2049
2969
  if (plan.risks.length > 0) {
2050
2970
  lines.push("");
2051
- lines.push(chalk2.yellow(" Risks:"));
2971
+ lines.push(chalk3.yellow(" Risks:"));
2052
2972
  for (const risk of plan.risks) {
2053
- lines.push(chalk2.yellow(` \u26A0 ${risk}`));
2973
+ lines.push(chalk3.yellow(` \u26A0 ${risk}`));
2054
2974
  }
2055
2975
  }
2056
2976
  lines.push("");
2057
2977
  if (!plan.approved) {
2058
- lines.push(chalk2.gray(" Approve: /plan approve | Modify: /plan edit | Cancel: /plan cancel"));
2978
+ lines.push(chalk3.gray(" Approve: /plan approve | Modify: /plan edit | Cancel: /plan cancel"));
2059
2979
  }
2060
2980
  lines.push("");
2061
2981
  return lines.join("\n");
@@ -2086,7 +3006,7 @@ function isPlanComplete(plan) {
2086
3006
  }
2087
3007
 
2088
3008
  // src/agent/cost.ts
2089
- import chalk3 from "chalk";
3009
+ import chalk4 from "chalk";
2090
3010
  var MODEL_COSTS = {
2091
3011
  "notch-cinder": { input: 0.05, output: 0.15 },
2092
3012
  // L4 — cheapest
@@ -2129,19 +3049,19 @@ var CostTracker = class {
2129
3049
  formatLastCost() {
2130
3050
  const last = this.entries[this.entries.length - 1];
2131
3051
  if (!last) return "";
2132
- return chalk3.gray(`$${last.cost.toFixed(4)}`);
3052
+ return chalk4.gray(`$${last.cost.toFixed(4)}`);
2133
3053
  }
2134
3054
  formatSession() {
2135
- if (this.entries.length === 0) return chalk3.gray(" No cost data.");
3055
+ if (this.entries.length === 0) return chalk4.gray(" No cost data.");
2136
3056
  const total = this.totalCost;
2137
3057
  const tokens = this.totalTokens;
2138
3058
  return [
2139
- chalk3.gray(`
3059
+ chalk4.gray(`
2140
3060
  Session cost estimate:`),
2141
- chalk3.gray(` Input: ${tokens.prompt.toLocaleString()} tokens`),
2142
- chalk3.gray(` Output: ${tokens.completion.toLocaleString()} tokens`),
2143
- chalk3.gray(` Est. cost: `) + chalk3.white(`$${total.toFixed(4)}`),
2144
- chalk3.gray(` (${this.entries.length} turns)`)
3061
+ chalk4.gray(` Input: ${tokens.prompt.toLocaleString()} tokens`),
3062
+ chalk4.gray(` Output: ${tokens.completion.toLocaleString()} tokens`),
3063
+ chalk4.gray(` Est. cost: `) + chalk4.white(`$${total.toFixed(4)}`),
3064
+ chalk4.gray(` (${this.entries.length} turns)`)
2145
3065
  ].join("\n");
2146
3066
  }
2147
3067
  /** Alias for formatSession */
@@ -2160,10 +3080,10 @@ var CostTracker = class {
2160
3080
  existing.turns += 1;
2161
3081
  byModel.set(e.model, existing);
2162
3082
  }
2163
- const lines = [chalk3.gray("\n Cost by model:")];
3083
+ const lines = [chalk4.gray("\n Cost by model:")];
2164
3084
  for (const [modelId, data] of byModel) {
2165
3085
  lines.push(
2166
- chalk3.gray(` ${modelId.padEnd(14)} ${data.turns} turns `) + chalk3.white(`$${data.cost.toFixed(4)}`) + chalk3.gray(` (${(data.prompt + data.completion).toLocaleString()} tokens)`)
3086
+ chalk4.gray(` ${modelId.padEnd(14)} ${data.turns} turns `) + chalk4.white(`$${data.cost.toFixed(4)}`) + chalk4.gray(` (${(data.prompt + data.completion).toLocaleString()} tokens)`)
2167
3087
  );
2168
3088
  }
2169
3089
  return lines.join("\n");
@@ -2171,14 +3091,14 @@ var CostTracker = class {
2171
3091
  };
2172
3092
 
2173
3093
  // src/agent/ralph.ts
2174
- import fs10 from "fs/promises";
2175
- import path11 from "path";
2176
- import chalk4 from "chalk";
3094
+ import fs13 from "fs/promises";
3095
+ import path15 from "path";
3096
+ import chalk5 from "chalk";
2177
3097
  import { generateText as generateText3, streamText as streamText3 } from "ai";
2178
3098
 
2179
3099
  // src/context/repo-map.ts
2180
- import fs9 from "fs/promises";
2181
- import path10 from "path";
3100
+ import fs12 from "fs/promises";
3101
+ import path14 from "path";
2182
3102
  import { glob } from "glob";
2183
3103
  var PATTERNS = {
2184
3104
  ts: [
@@ -2209,7 +3129,7 @@ var PATTERNS = {
2209
3129
  };
2210
3130
  var IMPORT_PATTERN = /(?:import|from)\s+['"]([^'"]+)['"]/g;
2211
3131
  function getPatterns(filePath) {
2212
- const ext = path10.extname(filePath).slice(1);
3132
+ const ext = path14.extname(filePath).slice(1);
2213
3133
  if (["ts", "tsx", "mts", "cts"].includes(ext)) return PATTERNS.ts;
2214
3134
  if (["js", "jsx", "mjs", "cjs"].includes(ext)) return PATTERNS.js;
2215
3135
  if (ext === "py") return PATTERNS.py;
@@ -2260,9 +3180,9 @@ async function buildRepoMap(root) {
2260
3180
  });
2261
3181
  const entries = [];
2262
3182
  for (const file of files.slice(0, 500)) {
2263
- const fullPath = path10.resolve(root, file);
3183
+ const fullPath = path14.resolve(root, file);
2264
3184
  try {
2265
- const content = await fs9.readFile(fullPath, "utf-8");
3185
+ const content = await fs12.readFile(fullPath, "utf-8");
2266
3186
  const lines = content.split("\n").length;
2267
3187
  const patterns = getPatterns(file);
2268
3188
  const symbols = extractSymbols(content, patterns);
@@ -2364,11 +3284,11 @@ ${repoContext || "(empty project)"}`;
2364
3284
  async function savePlan(plan, cwd) {
2365
3285
  plan.updated = (/* @__PURE__ */ new Date()).toISOString();
2366
3286
  plan.completedCount = plan.tasks.filter((t) => t.status === "done").length;
2367
- await fs10.writeFile(path11.join(cwd, PLAN_FILE), JSON.stringify(plan, null, 2), "utf-8");
3287
+ await fs13.writeFile(path15.join(cwd, PLAN_FILE), JSON.stringify(plan, null, 2), "utf-8");
2368
3288
  }
2369
3289
  async function loadPlan(cwd) {
2370
3290
  try {
2371
- const raw = await fs10.readFile(path11.join(cwd, PLAN_FILE), "utf-8");
3291
+ const raw = await fs13.readFile(path15.join(cwd, PLAN_FILE), "utf-8");
2372
3292
  return JSON.parse(raw);
2373
3293
  } catch {
2374
3294
  return null;
@@ -2376,7 +3296,7 @@ async function loadPlan(cwd) {
2376
3296
  }
2377
3297
  async function deletePlan(cwd) {
2378
3298
  try {
2379
- await fs10.unlink(path11.join(cwd, PLAN_FILE));
3299
+ await fs13.unlink(path15.join(cwd, PLAN_FILE));
2380
3300
  } catch {
2381
3301
  }
2382
3302
  }
@@ -2540,27 +3460,27 @@ function formatRalphStatus(plan) {
2540
3460
  const progress = plan.totalCount > 0 ? Math.round(done / plan.totalCount * 100) : 0;
2541
3461
  const barWidth = 25;
2542
3462
  const filled = Math.round(done / plan.totalCount * barWidth);
2543
- const bar = chalk4.green("\u2588".repeat(filled)) + chalk4.gray("\u2591".repeat(barWidth - filled));
3463
+ const bar = chalk5.green("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(barWidth - filled));
2544
3464
  lines.push("");
2545
- lines.push(chalk4.bold.white(` Ralph Mode: ${plan.goal}`));
3465
+ lines.push(chalk5.bold.white(` Ralph Mode: ${plan.goal}`));
2546
3466
  lines.push(` [${bar}] ${progress}% (${done}/${plan.totalCount} tasks)`);
2547
- if (failed > 0) lines.push(chalk4.red(` ${failed} failed`));
3467
+ if (failed > 0) lines.push(chalk5.red(` ${failed} failed`));
2548
3468
  lines.push("");
2549
3469
  for (const task of plan.tasks) {
2550
- const icon = task.status === "done" ? chalk4.green("\u2713") : task.status === "failed" ? chalk4.red("\u2717") : task.status === "in_progress" ? chalk4.yellow("\u25B6") : task.status === "skipped" ? chalk4.gray("\u2013") : chalk4.gray("\u25CB");
2551
- const title = task.status === "done" ? chalk4.gray(task.title) : task.status === "failed" ? chalk4.red(task.title) : chalk4.white(task.title);
2552
- const files = task.files.length > 0 ? chalk4.cyan(` [${task.files.join(", ")}]`) : "";
2553
- const err = task.error ? chalk4.red(` \u2014 ${task.error.slice(0, 60)}`) : "";
2554
- const attempts = task.attempts > 1 ? chalk4.yellow(` (${task.attempts} attempts)`) : "";
2555
- lines.push(` ${icon} ${chalk4.gray(`${task.id}.`)} ${title}${files}${attempts}${err}`);
3470
+ const icon = task.status === "done" ? chalk5.green("\u2713") : task.status === "failed" ? chalk5.red("\u2717") : task.status === "in_progress" ? chalk5.yellow("\u25B6") : task.status === "skipped" ? chalk5.gray("\u2013") : chalk5.gray("\u25CB");
3471
+ const title = task.status === "done" ? chalk5.gray(task.title) : task.status === "failed" ? chalk5.red(task.title) : chalk5.white(task.title);
3472
+ const files = task.files.length > 0 ? chalk5.cyan(` [${task.files.join(", ")}]`) : "";
3473
+ const err = task.error ? chalk5.red(` \u2014 ${task.error.slice(0, 60)}`) : "";
3474
+ const attempts = task.attempts > 1 ? chalk5.yellow(` (${task.attempts} attempts)`) : "";
3475
+ lines.push(` ${icon} ${chalk5.gray(`${task.id}.`)} ${title}${files}${attempts}${err}`);
2556
3476
  }
2557
3477
  lines.push("");
2558
3478
  return lines.join("\n");
2559
3479
  }
2560
3480
 
2561
3481
  // src/context/references.ts
2562
- import fs11 from "fs/promises";
2563
- import path12 from "path";
3482
+ import fs14 from "fs/promises";
3483
+ import path16 from "path";
2564
3484
  import { glob as glob2 } from "glob";
2565
3485
  async function resolveReferences(input, cwd) {
2566
3486
  const references = [];
@@ -2603,9 +3523,9 @@ ${truncated}
2603
3523
  return sections.join("\n\n") + "\n\n";
2604
3524
  }
2605
3525
  async function resolveFile(ref, cwd) {
2606
- const filePath = path12.isAbsolute(ref) ? ref : path12.resolve(cwd, ref);
3526
+ const filePath = path16.isAbsolute(ref) ? ref : path16.resolve(cwd, ref);
2607
3527
  try {
2608
- const content = await fs11.readFile(filePath, "utf-8");
3528
+ const content = await fs14.readFile(filePath, "utf-8");
2609
3529
  const lines = content.split("\n");
2610
3530
  const numbered = lines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
2611
3531
  return {
@@ -2701,448 +3621,448 @@ async function resolveGlob(pattern, cwd) {
2701
3621
  }
2702
3622
 
2703
3623
  // src/ui/banner.ts
2704
- import chalk6 from "chalk";
3624
+ import chalk7 from "chalk";
2705
3625
 
2706
3626
  // src/ui/themes.ts
2707
- import chalk5 from "chalk";
3627
+ import chalk6 from "chalk";
2708
3628
  var defaultTheme = {
2709
3629
  name: "Default",
2710
3630
  description: "FreeSyntax \u2014 silver, white, monochrome",
2711
- brand: chalk5.hex("#D4D4D4"),
3631
+ brand: chalk6.hex("#D4D4D4"),
2712
3632
  // silver (banner uses gradient override)
2713
- mascot: chalk5.hex("#AAAAAA"),
3633
+ mascot: chalk6.hex("#AAAAAA"),
2714
3634
  // medium gray mantis
2715
- mascotAccent: chalk5.hex("#FFFFFF"),
3635
+ mascotAccent: chalk6.hex("#FFFFFF"),
2716
3636
  // white eyes
2717
- tagline: chalk5.hex("#777777"),
3637
+ tagline: chalk6.hex("#777777"),
2718
3638
  // muted gray
2719
- prompt: chalk5.hex("#CCCCCC"),
3639
+ prompt: chalk6.hex("#CCCCCC"),
2720
3640
  // silver prompt
2721
- border: chalk5.hex("#444444"),
3641
+ border: chalk6.hex("#444444"),
2722
3642
  // dark border
2723
- dim: chalk5.hex("#666666"),
3643
+ dim: chalk6.hex("#666666"),
2724
3644
  // muted text
2725
- text: chalk5.hex("#D4D4D4"),
3645
+ text: chalk6.hex("#D4D4D4"),
2726
3646
  // silver body text
2727
- bold: chalk5.hex("#FFFFFF").bold,
3647
+ bold: chalk6.hex("#FFFFFF").bold,
2728
3648
  // pure white emphasis
2729
- success: chalk5.green,
2730
- warning: chalk5.yellow,
2731
- error: chalk5.red,
2732
- info: chalk5.hex("#BBBBBB"),
3649
+ success: chalk6.green,
3650
+ warning: chalk6.yellow,
3651
+ error: chalk6.red,
3652
+ info: chalk6.hex("#BBBBBB"),
2733
3653
  // light gray info
2734
- toolName: chalk5.hex("#AAAAAA"),
2735
- toolArgs: chalk5.hex("#777777"),
2736
- toolResult: chalk5.hex("#555555"),
2737
- diffAdd: chalk5.green,
2738
- diffRemove: chalk5.red,
2739
- diffHeader: chalk5.hex("#CCCCCC"),
2740
- mdH1: chalk5.hex("#FFFFFF").bold,
2741
- mdH2: chalk5.hex("#D4D4D4"),
2742
- mdH3: chalk5.hex("#AAAAAA"),
2743
- mdCode: chalk5.hex("#BBBBBB"),
2744
- mdInlineCode: chalk5.hex("#CCCCCC"),
2745
- mdLink: chalk5.hex("#D4D4D4").underline,
2746
- meterLow: chalk5.green,
2747
- meterMid: chalk5.yellow,
2748
- meterHigh: chalk5.red
3654
+ toolName: chalk6.hex("#AAAAAA"),
3655
+ toolArgs: chalk6.hex("#777777"),
3656
+ toolResult: chalk6.hex("#555555"),
3657
+ diffAdd: chalk6.green,
3658
+ diffRemove: chalk6.red,
3659
+ diffHeader: chalk6.hex("#CCCCCC"),
3660
+ mdH1: chalk6.hex("#FFFFFF").bold,
3661
+ mdH2: chalk6.hex("#D4D4D4"),
3662
+ mdH3: chalk6.hex("#AAAAAA"),
3663
+ mdCode: chalk6.hex("#BBBBBB"),
3664
+ mdInlineCode: chalk6.hex("#CCCCCC"),
3665
+ mdLink: chalk6.hex("#D4D4D4").underline,
3666
+ meterLow: chalk6.green,
3667
+ meterMid: chalk6.yellow,
3668
+ meterHigh: chalk6.red
2749
3669
  };
2750
3670
  var interstellarTheme = {
2751
3671
  name: "Interstellar",
2752
3672
  description: "Deep space amber \u2014 Endurance control panels",
2753
- brand: chalk5.hex("#FFB347"),
3673
+ brand: chalk6.hex("#FFB347"),
2754
3674
  // warm amber (instrument readout)
2755
- mascot: chalk5.hex("#4A90D9"),
3675
+ mascot: chalk6.hex("#4A90D9"),
2756
3676
  // pale blue (frozen clouds)
2757
- mascotAccent: chalk5.hex("#FFD700"),
3677
+ mascotAccent: chalk6.hex("#FFD700"),
2758
3678
  // gold (Gargantua light)
2759
- tagline: chalk5.hex("#708090"),
3679
+ tagline: chalk6.hex("#708090"),
2760
3680
  // slate gray (ship hull)
2761
- prompt: chalk5.hex("#FFB347"),
2762
- border: chalk5.hex("#3A3A3A"),
3681
+ prompt: chalk6.hex("#FFB347"),
3682
+ border: chalk6.hex("#3A3A3A"),
2763
3683
  // dark hull metal
2764
- dim: chalk5.hex("#5C5C5C"),
2765
- text: chalk5.hex("#D4D4D4"),
3684
+ dim: chalk6.hex("#5C5C5C"),
3685
+ text: chalk6.hex("#D4D4D4"),
2766
3686
  // cool white (display text)
2767
- bold: chalk5.hex("#FFFFFF").bold,
2768
- success: chalk5.hex("#7CFC00"),
3687
+ bold: chalk6.hex("#FFFFFF").bold,
3688
+ success: chalk6.hex("#7CFC00"),
2769
3689
  // "CASE: All systems nominal"
2770
- warning: chalk5.hex("#FFB347"),
3690
+ warning: chalk6.hex("#FFB347"),
2771
3691
  // amber warning
2772
- error: chalk5.hex("#FF4444"),
3692
+ error: chalk6.hex("#FF4444"),
2773
3693
  // critical alert
2774
- info: chalk5.hex("#4A90D9"),
3694
+ info: chalk6.hex("#4A90D9"),
2775
3695
  // blue data readout
2776
- toolName: chalk5.hex("#FFB347"),
2777
- toolArgs: chalk5.hex("#708090"),
2778
- toolResult: chalk5.hex("#5C5C5C"),
2779
- diffAdd: chalk5.hex("#7CFC00"),
2780
- diffRemove: chalk5.hex("#FF4444"),
2781
- diffHeader: chalk5.hex("#FFB347"),
2782
- mdH1: chalk5.hex("#FFB347").bold,
2783
- mdH2: chalk5.hex("#4A90D9"),
2784
- mdH3: chalk5.hex("#708090"),
2785
- mdCode: chalk5.hex("#FFD700"),
2786
- mdInlineCode: chalk5.hex("#FFB347"),
2787
- mdLink: chalk5.hex("#4A90D9").underline,
2788
- meterLow: chalk5.hex("#4A90D9"),
2789
- meterMid: chalk5.hex("#FFB347"),
2790
- meterHigh: chalk5.hex("#FF4444")
3696
+ toolName: chalk6.hex("#FFB347"),
3697
+ toolArgs: chalk6.hex("#708090"),
3698
+ toolResult: chalk6.hex("#5C5C5C"),
3699
+ diffAdd: chalk6.hex("#7CFC00"),
3700
+ diffRemove: chalk6.hex("#FF4444"),
3701
+ diffHeader: chalk6.hex("#FFB347"),
3702
+ mdH1: chalk6.hex("#FFB347").bold,
3703
+ mdH2: chalk6.hex("#4A90D9"),
3704
+ mdH3: chalk6.hex("#708090"),
3705
+ mdCode: chalk6.hex("#FFD700"),
3706
+ mdInlineCode: chalk6.hex("#FFB347"),
3707
+ mdLink: chalk6.hex("#4A90D9").underline,
3708
+ meterLow: chalk6.hex("#4A90D9"),
3709
+ meterMid: chalk6.hex("#FFB347"),
3710
+ meterHigh: chalk6.hex("#FF4444")
2791
3711
  };
2792
3712
  var spaceGrayTheme = {
2793
3713
  name: "Space Gray",
2794
3714
  description: "Apple minimalism \u2014 silver, cool gray, ice blue",
2795
- brand: chalk5.hex("#A2AAAD"),
3715
+ brand: chalk6.hex("#A2AAAD"),
2796
3716
  // apple silver
2797
- mascot: chalk5.hex("#86868B"),
3717
+ mascot: chalk6.hex("#86868B"),
2798
3718
  // space gray
2799
- mascotAccent: chalk5.hex("#0071E3"),
3719
+ mascotAccent: chalk6.hex("#0071E3"),
2800
3720
  // apple blue
2801
- tagline: chalk5.hex("#6E6E73"),
3721
+ tagline: chalk6.hex("#6E6E73"),
2802
3722
  // secondary gray
2803
- prompt: chalk5.hex("#A2AAAD"),
2804
- border: chalk5.hex("#3A3A3C"),
3723
+ prompt: chalk6.hex("#A2AAAD"),
3724
+ border: chalk6.hex("#3A3A3C"),
2805
3725
  // dark separator
2806
- dim: chalk5.hex("#6E6E73"),
2807
- text: chalk5.hex("#E5E5EA"),
3726
+ dim: chalk6.hex("#6E6E73"),
3727
+ text: chalk6.hex("#E5E5EA"),
2808
3728
  // system gray 6
2809
- bold: chalk5.hex("#F5F5F7").bold,
3729
+ bold: chalk6.hex("#F5F5F7").bold,
2810
3730
  // near white
2811
- success: chalk5.hex("#30D158"),
3731
+ success: chalk6.hex("#30D158"),
2812
3732
  // apple green
2813
- warning: chalk5.hex("#FFD60A"),
3733
+ warning: chalk6.hex("#FFD60A"),
2814
3734
  // apple yellow
2815
- error: chalk5.hex("#FF453A"),
3735
+ error: chalk6.hex("#FF453A"),
2816
3736
  // apple red
2817
- info: chalk5.hex("#0A84FF"),
3737
+ info: chalk6.hex("#0A84FF"),
2818
3738
  // apple blue
2819
- toolName: chalk5.hex("#A2AAAD"),
2820
- toolArgs: chalk5.hex("#6E6E73"),
2821
- toolResult: chalk5.hex("#48484A"),
2822
- diffAdd: chalk5.hex("#30D158"),
2823
- diffRemove: chalk5.hex("#FF453A"),
2824
- diffHeader: chalk5.hex("#0A84FF"),
2825
- mdH1: chalk5.hex("#F5F5F7").bold,
2826
- mdH2: chalk5.hex("#0A84FF"),
2827
- mdH3: chalk5.hex("#A2AAAD"),
2828
- mdCode: chalk5.hex("#BF5AF2"),
3739
+ toolName: chalk6.hex("#A2AAAD"),
3740
+ toolArgs: chalk6.hex("#6E6E73"),
3741
+ toolResult: chalk6.hex("#48484A"),
3742
+ diffAdd: chalk6.hex("#30D158"),
3743
+ diffRemove: chalk6.hex("#FF453A"),
3744
+ diffHeader: chalk6.hex("#0A84FF"),
3745
+ mdH1: chalk6.hex("#F5F5F7").bold,
3746
+ mdH2: chalk6.hex("#0A84FF"),
3747
+ mdH3: chalk6.hex("#A2AAAD"),
3748
+ mdCode: chalk6.hex("#BF5AF2"),
2829
3749
  // apple purple for code
2830
- mdInlineCode: chalk5.hex("#BF5AF2"),
2831
- mdLink: chalk5.hex("#0A84FF").underline,
2832
- meterLow: chalk5.hex("#30D158"),
2833
- meterMid: chalk5.hex("#FFD60A"),
2834
- meterHigh: chalk5.hex("#FF453A")
3750
+ mdInlineCode: chalk6.hex("#BF5AF2"),
3751
+ mdLink: chalk6.hex("#0A84FF").underline,
3752
+ meterLow: chalk6.hex("#30D158"),
3753
+ meterMid: chalk6.hex("#FFD60A"),
3754
+ meterHigh: chalk6.hex("#FF453A")
2835
3755
  };
2836
3756
  var cyberpunkTheme = {
2837
3757
  name: "Cyberpunk",
2838
3758
  description: "Neon pink & cyan \u2014 Night City terminal",
2839
- brand: chalk5.hex("#FF2079"),
3759
+ brand: chalk6.hex("#FF2079"),
2840
3760
  // hot pink
2841
- mascot: chalk5.hex("#00FFFF"),
3761
+ mascot: chalk6.hex("#00FFFF"),
2842
3762
  // electric cyan
2843
- mascotAccent: chalk5.hex("#FF2079"),
3763
+ mascotAccent: chalk6.hex("#FF2079"),
2844
3764
  // hot pink eyes
2845
- tagline: chalk5.hex("#9D00FF"),
3765
+ tagline: chalk6.hex("#9D00FF"),
2846
3766
  // deep violet
2847
- prompt: chalk5.hex("#FF2079"),
2848
- border: chalk5.hex("#1A1A2E"),
2849
- dim: chalk5.hex("#4A4A6A"),
2850
- text: chalk5.hex("#E0E0FF"),
2851
- bold: chalk5.hex("#FFFFFF").bold,
2852
- success: chalk5.hex("#00FF41"),
3767
+ prompt: chalk6.hex("#FF2079"),
3768
+ border: chalk6.hex("#1A1A2E"),
3769
+ dim: chalk6.hex("#4A4A6A"),
3770
+ text: chalk6.hex("#E0E0FF"),
3771
+ bold: chalk6.hex("#FFFFFF").bold,
3772
+ success: chalk6.hex("#00FF41"),
2853
3773
  // matrix green
2854
- warning: chalk5.hex("#FFFF00"),
3774
+ warning: chalk6.hex("#FFFF00"),
2855
3775
  // neon yellow
2856
- error: chalk5.hex("#FF0000"),
2857
- info: chalk5.hex("#00FFFF"),
2858
- toolName: chalk5.hex("#FF2079"),
2859
- toolArgs: chalk5.hex("#9D00FF"),
2860
- toolResult: chalk5.hex("#4A4A6A"),
2861
- diffAdd: chalk5.hex("#00FF41"),
2862
- diffRemove: chalk5.hex("#FF0000"),
2863
- diffHeader: chalk5.hex("#FF2079"),
2864
- mdH1: chalk5.hex("#FF2079").bold,
2865
- mdH2: chalk5.hex("#00FFFF"),
2866
- mdH3: chalk5.hex("#9D00FF"),
2867
- mdCode: chalk5.hex("#00FF41"),
2868
- mdInlineCode: chalk5.hex("#00FFFF"),
2869
- mdLink: chalk5.hex("#9D00FF").underline,
2870
- meterLow: chalk5.hex("#00FF41"),
2871
- meterMid: chalk5.hex("#FFFF00"),
2872
- meterHigh: chalk5.hex("#FF0000")
3776
+ error: chalk6.hex("#FF0000"),
3777
+ info: chalk6.hex("#00FFFF"),
3778
+ toolName: chalk6.hex("#FF2079"),
3779
+ toolArgs: chalk6.hex("#9D00FF"),
3780
+ toolResult: chalk6.hex("#4A4A6A"),
3781
+ diffAdd: chalk6.hex("#00FF41"),
3782
+ diffRemove: chalk6.hex("#FF0000"),
3783
+ diffHeader: chalk6.hex("#FF2079"),
3784
+ mdH1: chalk6.hex("#FF2079").bold,
3785
+ mdH2: chalk6.hex("#00FFFF"),
3786
+ mdH3: chalk6.hex("#9D00FF"),
3787
+ mdCode: chalk6.hex("#00FF41"),
3788
+ mdInlineCode: chalk6.hex("#00FFFF"),
3789
+ mdLink: chalk6.hex("#9D00FF").underline,
3790
+ meterLow: chalk6.hex("#00FF41"),
3791
+ meterMid: chalk6.hex("#FFFF00"),
3792
+ meterHigh: chalk6.hex("#FF0000")
2873
3793
  };
2874
3794
  var auroraTheme = {
2875
3795
  name: "Aurora",
2876
3796
  description: "Northern lights \u2014 teal, violet, polar green",
2877
- brand: chalk5.hex("#7FFFD4"),
3797
+ brand: chalk6.hex("#7FFFD4"),
2878
3798
  // aquamarine
2879
- mascot: chalk5.hex("#00CED1"),
3799
+ mascot: chalk6.hex("#00CED1"),
2880
3800
  // dark turquoise
2881
- mascotAccent: chalk5.hex("#DA70D6"),
3801
+ mascotAccent: chalk6.hex("#DA70D6"),
2882
3802
  // orchid
2883
- tagline: chalk5.hex("#9370DB"),
3803
+ tagline: chalk6.hex("#9370DB"),
2884
3804
  // medium purple
2885
- prompt: chalk5.hex("#7FFFD4"),
2886
- border: chalk5.hex("#2F4F4F"),
3805
+ prompt: chalk6.hex("#7FFFD4"),
3806
+ border: chalk6.hex("#2F4F4F"),
2887
3807
  // dark slate
2888
- dim: chalk5.hex("#5F9EA0"),
3808
+ dim: chalk6.hex("#5F9EA0"),
2889
3809
  // cadet blue
2890
- text: chalk5.hex("#E0FFFF"),
3810
+ text: chalk6.hex("#E0FFFF"),
2891
3811
  // light cyan
2892
- bold: chalk5.hex("#F0FFFF").bold,
2893
- success: chalk5.hex("#00FA9A"),
3812
+ bold: chalk6.hex("#F0FFFF").bold,
3813
+ success: chalk6.hex("#00FA9A"),
2894
3814
  // medium spring green
2895
- warning: chalk5.hex("#DDA0DD"),
3815
+ warning: chalk6.hex("#DDA0DD"),
2896
3816
  // plum
2897
- error: chalk5.hex("#FF6B6B"),
2898
- info: chalk5.hex("#87CEEB"),
3817
+ error: chalk6.hex("#FF6B6B"),
3818
+ info: chalk6.hex("#87CEEB"),
2899
3819
  // sky blue
2900
- toolName: chalk5.hex("#7FFFD4"),
2901
- toolArgs: chalk5.hex("#5F9EA0"),
2902
- toolResult: chalk5.hex("#2F4F4F"),
2903
- diffAdd: chalk5.hex("#00FA9A"),
2904
- diffRemove: chalk5.hex("#FF6B6B"),
2905
- diffHeader: chalk5.hex("#DA70D6"),
2906
- mdH1: chalk5.hex("#7FFFD4").bold,
2907
- mdH2: chalk5.hex("#DA70D6"),
2908
- mdH3: chalk5.hex("#87CEEB"),
2909
- mdCode: chalk5.hex("#00CED1"),
2910
- mdInlineCode: chalk5.hex("#7FFFD4"),
2911
- mdLink: chalk5.hex("#DA70D6").underline,
2912
- meterLow: chalk5.hex("#00FA9A"),
2913
- meterMid: chalk5.hex("#DDA0DD"),
2914
- meterHigh: chalk5.hex("#FF6B6B")
3820
+ toolName: chalk6.hex("#7FFFD4"),
3821
+ toolArgs: chalk6.hex("#5F9EA0"),
3822
+ toolResult: chalk6.hex("#2F4F4F"),
3823
+ diffAdd: chalk6.hex("#00FA9A"),
3824
+ diffRemove: chalk6.hex("#FF6B6B"),
3825
+ diffHeader: chalk6.hex("#DA70D6"),
3826
+ mdH1: chalk6.hex("#7FFFD4").bold,
3827
+ mdH2: chalk6.hex("#DA70D6"),
3828
+ mdH3: chalk6.hex("#87CEEB"),
3829
+ mdCode: chalk6.hex("#00CED1"),
3830
+ mdInlineCode: chalk6.hex("#7FFFD4"),
3831
+ mdLink: chalk6.hex("#DA70D6").underline,
3832
+ meterLow: chalk6.hex("#00FA9A"),
3833
+ meterMid: chalk6.hex("#DDA0DD"),
3834
+ meterHigh: chalk6.hex("#FF6B6B")
2915
3835
  };
2916
3836
  var solarizedTheme = {
2917
3837
  name: "Solarized",
2918
3838
  description: "Solarized Dark \u2014 warm, precise, classic dev palette",
2919
- brand: chalk5.hex("#268BD2"),
3839
+ brand: chalk6.hex("#268BD2"),
2920
3840
  // blue
2921
- mascot: chalk5.hex("#859900"),
3841
+ mascot: chalk6.hex("#859900"),
2922
3842
  // green
2923
- mascotAccent: chalk5.hex("#B58900"),
3843
+ mascotAccent: chalk6.hex("#B58900"),
2924
3844
  // yellow
2925
- tagline: chalk5.hex("#2AA198"),
3845
+ tagline: chalk6.hex("#2AA198"),
2926
3846
  // cyan
2927
- prompt: chalk5.hex("#268BD2"),
2928
- border: chalk5.hex("#073642"),
3847
+ prompt: chalk6.hex("#268BD2"),
3848
+ border: chalk6.hex("#073642"),
2929
3849
  // base02
2930
- dim: chalk5.hex("#586E75"),
3850
+ dim: chalk6.hex("#586E75"),
2931
3851
  // base01
2932
- text: chalk5.hex("#839496"),
3852
+ text: chalk6.hex("#839496"),
2933
3853
  // base0
2934
- bold: chalk5.hex("#93A1A1").bold,
3854
+ bold: chalk6.hex("#93A1A1").bold,
2935
3855
  // base1
2936
- success: chalk5.hex("#859900"),
2937
- warning: chalk5.hex("#B58900"),
2938
- error: chalk5.hex("#DC322F"),
2939
- info: chalk5.hex("#2AA198"),
2940
- toolName: chalk5.hex("#268BD2"),
2941
- toolArgs: chalk5.hex("#586E75"),
2942
- toolResult: chalk5.hex("#073642"),
2943
- diffAdd: chalk5.hex("#859900"),
2944
- diffRemove: chalk5.hex("#DC322F"),
2945
- diffHeader: chalk5.hex("#268BD2"),
2946
- mdH1: chalk5.hex("#93A1A1").bold,
2947
- mdH2: chalk5.hex("#268BD2"),
2948
- mdH3: chalk5.hex("#2AA198"),
2949
- mdCode: chalk5.hex("#859900"),
2950
- mdInlineCode: chalk5.hex("#2AA198"),
2951
- mdLink: chalk5.hex("#268BD2").underline,
2952
- meterLow: chalk5.hex("#859900"),
2953
- meterMid: chalk5.hex("#B58900"),
2954
- meterHigh: chalk5.hex("#DC322F")
3856
+ success: chalk6.hex("#859900"),
3857
+ warning: chalk6.hex("#B58900"),
3858
+ error: chalk6.hex("#DC322F"),
3859
+ info: chalk6.hex("#2AA198"),
3860
+ toolName: chalk6.hex("#268BD2"),
3861
+ toolArgs: chalk6.hex("#586E75"),
3862
+ toolResult: chalk6.hex("#073642"),
3863
+ diffAdd: chalk6.hex("#859900"),
3864
+ diffRemove: chalk6.hex("#DC322F"),
3865
+ diffHeader: chalk6.hex("#268BD2"),
3866
+ mdH1: chalk6.hex("#93A1A1").bold,
3867
+ mdH2: chalk6.hex("#268BD2"),
3868
+ mdH3: chalk6.hex("#2AA198"),
3869
+ mdCode: chalk6.hex("#859900"),
3870
+ mdInlineCode: chalk6.hex("#2AA198"),
3871
+ mdLink: chalk6.hex("#268BD2").underline,
3872
+ meterLow: chalk6.hex("#859900"),
3873
+ meterMid: chalk6.hex("#B58900"),
3874
+ meterHigh: chalk6.hex("#DC322F")
2955
3875
  };
2956
3876
  var draculaTheme = {
2957
3877
  name: "Dracula",
2958
3878
  description: "Dracula \u2014 purple, pink, green on charcoal",
2959
- brand: chalk5.hex("#BD93F9"),
3879
+ brand: chalk6.hex("#BD93F9"),
2960
3880
  // purple
2961
- mascot: chalk5.hex("#50FA7B"),
3881
+ mascot: chalk6.hex("#50FA7B"),
2962
3882
  // green
2963
- mascotAccent: chalk5.hex("#FF79C6"),
3883
+ mascotAccent: chalk6.hex("#FF79C6"),
2964
3884
  // pink
2965
- tagline: chalk5.hex("#6272A4"),
3885
+ tagline: chalk6.hex("#6272A4"),
2966
3886
  // comment gray
2967
- prompt: chalk5.hex("#BD93F9"),
2968
- border: chalk5.hex("#44475A"),
3887
+ prompt: chalk6.hex("#BD93F9"),
3888
+ border: chalk6.hex("#44475A"),
2969
3889
  // current line
2970
- dim: chalk5.hex("#6272A4"),
3890
+ dim: chalk6.hex("#6272A4"),
2971
3891
  // comment
2972
- text: chalk5.hex("#F8F8F2"),
3892
+ text: chalk6.hex("#F8F8F2"),
2973
3893
  // foreground
2974
- bold: chalk5.hex("#F8F8F2").bold,
2975
- success: chalk5.hex("#50FA7B"),
2976
- warning: chalk5.hex("#F1FA8C"),
3894
+ bold: chalk6.hex("#F8F8F2").bold,
3895
+ success: chalk6.hex("#50FA7B"),
3896
+ warning: chalk6.hex("#F1FA8C"),
2977
3897
  // yellow
2978
- error: chalk5.hex("#FF5555"),
2979
- info: chalk5.hex("#8BE9FD"),
3898
+ error: chalk6.hex("#FF5555"),
3899
+ info: chalk6.hex("#8BE9FD"),
2980
3900
  // cyan
2981
- toolName: chalk5.hex("#BD93F9"),
2982
- toolArgs: chalk5.hex("#6272A4"),
2983
- toolResult: chalk5.hex("#44475A"),
2984
- diffAdd: chalk5.hex("#50FA7B"),
2985
- diffRemove: chalk5.hex("#FF5555"),
2986
- diffHeader: chalk5.hex("#FF79C6"),
2987
- mdH1: chalk5.hex("#FF79C6").bold,
2988
- mdH2: chalk5.hex("#BD93F9"),
2989
- mdH3: chalk5.hex("#8BE9FD"),
2990
- mdCode: chalk5.hex("#50FA7B"),
2991
- mdInlineCode: chalk5.hex("#F1FA8C"),
2992
- mdLink: chalk5.hex("#8BE9FD").underline,
2993
- meterLow: chalk5.hex("#50FA7B"),
2994
- meterMid: chalk5.hex("#F1FA8C"),
2995
- meterHigh: chalk5.hex("#FF5555")
3901
+ toolName: chalk6.hex("#BD93F9"),
3902
+ toolArgs: chalk6.hex("#6272A4"),
3903
+ toolResult: chalk6.hex("#44475A"),
3904
+ diffAdd: chalk6.hex("#50FA7B"),
3905
+ diffRemove: chalk6.hex("#FF5555"),
3906
+ diffHeader: chalk6.hex("#FF79C6"),
3907
+ mdH1: chalk6.hex("#FF79C6").bold,
3908
+ mdH2: chalk6.hex("#BD93F9"),
3909
+ mdH3: chalk6.hex("#8BE9FD"),
3910
+ mdCode: chalk6.hex("#50FA7B"),
3911
+ mdInlineCode: chalk6.hex("#F1FA8C"),
3912
+ mdLink: chalk6.hex("#8BE9FD").underline,
3913
+ meterLow: chalk6.hex("#50FA7B"),
3914
+ meterMid: chalk6.hex("#F1FA8C"),
3915
+ meterHigh: chalk6.hex("#FF5555")
2996
3916
  };
2997
3917
  var monokaiTheme = {
2998
3918
  name: "Monokai",
2999
3919
  description: "Monokai Pro \u2014 vivid on dark, a classic",
3000
- brand: chalk5.hex("#A6E22E"),
3920
+ brand: chalk6.hex("#A6E22E"),
3001
3921
  // green
3002
- mascot: chalk5.hex("#66D9EF"),
3922
+ mascot: chalk6.hex("#66D9EF"),
3003
3923
  // blue
3004
- mascotAccent: chalk5.hex("#F92672"),
3924
+ mascotAccent: chalk6.hex("#F92672"),
3005
3925
  // pink
3006
- tagline: chalk5.hex("#75715E"),
3926
+ tagline: chalk6.hex("#75715E"),
3007
3927
  // comment
3008
- prompt: chalk5.hex("#A6E22E"),
3009
- border: chalk5.hex("#3E3D32"),
3010
- dim: chalk5.hex("#75715E"),
3011
- text: chalk5.hex("#F8F8F2"),
3012
- bold: chalk5.hex("#F8F8F2").bold,
3013
- success: chalk5.hex("#A6E22E"),
3014
- warning: chalk5.hex("#E6DB74"),
3928
+ prompt: chalk6.hex("#A6E22E"),
3929
+ border: chalk6.hex("#3E3D32"),
3930
+ dim: chalk6.hex("#75715E"),
3931
+ text: chalk6.hex("#F8F8F2"),
3932
+ bold: chalk6.hex("#F8F8F2").bold,
3933
+ success: chalk6.hex("#A6E22E"),
3934
+ warning: chalk6.hex("#E6DB74"),
3015
3935
  // yellow
3016
- error: chalk5.hex("#F92672"),
3017
- info: chalk5.hex("#66D9EF"),
3018
- toolName: chalk5.hex("#A6E22E"),
3019
- toolArgs: chalk5.hex("#75715E"),
3020
- toolResult: chalk5.hex("#3E3D32"),
3021
- diffAdd: chalk5.hex("#A6E22E"),
3022
- diffRemove: chalk5.hex("#F92672"),
3023
- diffHeader: chalk5.hex("#66D9EF"),
3024
- mdH1: chalk5.hex("#F92672").bold,
3025
- mdH2: chalk5.hex("#A6E22E"),
3026
- mdH3: chalk5.hex("#66D9EF"),
3027
- mdCode: chalk5.hex("#E6DB74"),
3028
- mdInlineCode: chalk5.hex("#A6E22E"),
3029
- mdLink: chalk5.hex("#66D9EF").underline,
3030
- meterLow: chalk5.hex("#A6E22E"),
3031
- meterMid: chalk5.hex("#E6DB74"),
3032
- meterHigh: chalk5.hex("#F92672")
3936
+ error: chalk6.hex("#F92672"),
3937
+ info: chalk6.hex("#66D9EF"),
3938
+ toolName: chalk6.hex("#A6E22E"),
3939
+ toolArgs: chalk6.hex("#75715E"),
3940
+ toolResult: chalk6.hex("#3E3D32"),
3941
+ diffAdd: chalk6.hex("#A6E22E"),
3942
+ diffRemove: chalk6.hex("#F92672"),
3943
+ diffHeader: chalk6.hex("#66D9EF"),
3944
+ mdH1: chalk6.hex("#F92672").bold,
3945
+ mdH2: chalk6.hex("#A6E22E"),
3946
+ mdH3: chalk6.hex("#66D9EF"),
3947
+ mdCode: chalk6.hex("#E6DB74"),
3948
+ mdInlineCode: chalk6.hex("#A6E22E"),
3949
+ mdLink: chalk6.hex("#66D9EF").underline,
3950
+ meterLow: chalk6.hex("#A6E22E"),
3951
+ meterMid: chalk6.hex("#E6DB74"),
3952
+ meterHigh: chalk6.hex("#F92672")
3033
3953
  };
3034
3954
  var oceanTheme = {
3035
3955
  name: "Ocean",
3036
3956
  description: "Deep sea \u2014 midnight blue, bioluminescent glow",
3037
- brand: chalk5.hex("#00B4D8"),
3957
+ brand: chalk6.hex("#00B4D8"),
3038
3958
  // cerulean
3039
- mascot: chalk5.hex("#0077B6"),
3959
+ mascot: chalk6.hex("#0077B6"),
3040
3960
  // deep blue
3041
- mascotAccent: chalk5.hex("#90E0EF"),
3961
+ mascotAccent: chalk6.hex("#90E0EF"),
3042
3962
  // light blue (bioluminescent)
3043
- tagline: chalk5.hex("#023E8A"),
3963
+ tagline: chalk6.hex("#023E8A"),
3044
3964
  // navy
3045
- prompt: chalk5.hex("#00B4D8"),
3046
- border: chalk5.hex("#03045E"),
3965
+ prompt: chalk6.hex("#00B4D8"),
3966
+ border: chalk6.hex("#03045E"),
3047
3967
  // deep navy
3048
- dim: chalk5.hex("#0077B6"),
3049
- text: chalk5.hex("#CAF0F8"),
3968
+ dim: chalk6.hex("#0077B6"),
3969
+ text: chalk6.hex("#CAF0F8"),
3050
3970
  // lightest blue
3051
- bold: chalk5.hex("#CAF0F8").bold,
3052
- success: chalk5.hex("#2DC653"),
3971
+ bold: chalk6.hex("#CAF0F8").bold,
3972
+ success: chalk6.hex("#2DC653"),
3053
3973
  // kelp green
3054
- warning: chalk5.hex("#F4A261"),
3974
+ warning: chalk6.hex("#F4A261"),
3055
3975
  // sandy
3056
- error: chalk5.hex("#E76F51"),
3976
+ error: chalk6.hex("#E76F51"),
3057
3977
  // coral
3058
- info: chalk5.hex("#90E0EF"),
3059
- toolName: chalk5.hex("#00B4D8"),
3060
- toolArgs: chalk5.hex("#0077B6"),
3061
- toolResult: chalk5.hex("#03045E"),
3062
- diffAdd: chalk5.hex("#2DC653"),
3063
- diffRemove: chalk5.hex("#E76F51"),
3064
- diffHeader: chalk5.hex("#00B4D8"),
3065
- mdH1: chalk5.hex("#90E0EF").bold,
3066
- mdH2: chalk5.hex("#00B4D8"),
3067
- mdH3: chalk5.hex("#0077B6"),
3068
- mdCode: chalk5.hex("#90E0EF"),
3069
- mdInlineCode: chalk5.hex("#00B4D8"),
3070
- mdLink: chalk5.hex("#90E0EF").underline,
3071
- meterLow: chalk5.hex("#00B4D8"),
3072
- meterMid: chalk5.hex("#F4A261"),
3073
- meterHigh: chalk5.hex("#E76F51")
3978
+ info: chalk6.hex("#90E0EF"),
3979
+ toolName: chalk6.hex("#00B4D8"),
3980
+ toolArgs: chalk6.hex("#0077B6"),
3981
+ toolResult: chalk6.hex("#03045E"),
3982
+ diffAdd: chalk6.hex("#2DC653"),
3983
+ diffRemove: chalk6.hex("#E76F51"),
3984
+ diffHeader: chalk6.hex("#00B4D8"),
3985
+ mdH1: chalk6.hex("#90E0EF").bold,
3986
+ mdH2: chalk6.hex("#00B4D8"),
3987
+ mdH3: chalk6.hex("#0077B6"),
3988
+ mdCode: chalk6.hex("#90E0EF"),
3989
+ mdInlineCode: chalk6.hex("#00B4D8"),
3990
+ mdLink: chalk6.hex("#90E0EF").underline,
3991
+ meterLow: chalk6.hex("#00B4D8"),
3992
+ meterMid: chalk6.hex("#F4A261"),
3993
+ meterHigh: chalk6.hex("#E76F51")
3074
3994
  };
3075
3995
  var emberTheme = {
3076
3996
  name: "Ember",
3077
3997
  description: "Fire forged \u2014 charcoal, ember orange, flame red",
3078
- brand: chalk5.hex("#FF6B35"),
3998
+ brand: chalk6.hex("#FF6B35"),
3079
3999
  // flame orange
3080
- mascot: chalk5.hex("#D62828"),
4000
+ mascot: chalk6.hex("#D62828"),
3081
4001
  // deep red
3082
- mascotAccent: chalk5.hex("#FFD166"),
4002
+ mascotAccent: chalk6.hex("#FFD166"),
3083
4003
  // bright flame
3084
- tagline: chalk5.hex("#8B4513"),
4004
+ tagline: chalk6.hex("#8B4513"),
3085
4005
  // saddle brown
3086
- prompt: chalk5.hex("#FF6B35"),
3087
- border: chalk5.hex("#2B2B2B"),
4006
+ prompt: chalk6.hex("#FF6B35"),
4007
+ border: chalk6.hex("#2B2B2B"),
3088
4008
  // charcoal
3089
- dim: chalk5.hex("#6B3A2A"),
3090
- text: chalk5.hex("#F4E4C1"),
4009
+ dim: chalk6.hex("#6B3A2A"),
4010
+ text: chalk6.hex("#F4E4C1"),
3091
4011
  // warm parchment
3092
- bold: chalk5.hex("#FFF5E1").bold,
3093
- success: chalk5.hex("#FFD166"),
4012
+ bold: chalk6.hex("#FFF5E1").bold,
4013
+ success: chalk6.hex("#FFD166"),
3094
4014
  // bright flame = success
3095
- warning: chalk5.hex("#FF6B35"),
3096
- error: chalk5.hex("#D62828"),
3097
- info: chalk5.hex("#F4845F"),
4015
+ warning: chalk6.hex("#FF6B35"),
4016
+ error: chalk6.hex("#D62828"),
4017
+ info: chalk6.hex("#F4845F"),
3098
4018
  // soft coral
3099
- toolName: chalk5.hex("#FF6B35"),
3100
- toolArgs: chalk5.hex("#6B3A2A"),
3101
- toolResult: chalk5.hex("#2B2B2B"),
3102
- diffAdd: chalk5.hex("#FFD166"),
3103
- diffRemove: chalk5.hex("#D62828"),
3104
- diffHeader: chalk5.hex("#FF6B35"),
3105
- mdH1: chalk5.hex("#FF6B35").bold,
3106
- mdH2: chalk5.hex("#F4845F"),
3107
- mdH3: chalk5.hex("#8B4513"),
3108
- mdCode: chalk5.hex("#FFD166"),
3109
- mdInlineCode: chalk5.hex("#FF6B35"),
3110
- mdLink: chalk5.hex("#F4845F").underline,
3111
- meterLow: chalk5.hex("#FFD166"),
3112
- meterMid: chalk5.hex("#FF6B35"),
3113
- meterHigh: chalk5.hex("#D62828")
4019
+ toolName: chalk6.hex("#FF6B35"),
4020
+ toolArgs: chalk6.hex("#6B3A2A"),
4021
+ toolResult: chalk6.hex("#2B2B2B"),
4022
+ diffAdd: chalk6.hex("#FFD166"),
4023
+ diffRemove: chalk6.hex("#D62828"),
4024
+ diffHeader: chalk6.hex("#FF6B35"),
4025
+ mdH1: chalk6.hex("#FF6B35").bold,
4026
+ mdH2: chalk6.hex("#F4845F"),
4027
+ mdH3: chalk6.hex("#8B4513"),
4028
+ mdCode: chalk6.hex("#FFD166"),
4029
+ mdInlineCode: chalk6.hex("#FF6B35"),
4030
+ mdLink: chalk6.hex("#F4845F").underline,
4031
+ meterLow: chalk6.hex("#FFD166"),
4032
+ meterMid: chalk6.hex("#FF6B35"),
4033
+ meterHigh: chalk6.hex("#D62828")
3114
4034
  };
3115
4035
  var ghostTheme = {
3116
4036
  name: "Ghost",
3117
4037
  description: "Monochrome \u2014 pure grayscale minimalism",
3118
- brand: chalk5.hex("#E0E0E0"),
3119
- mascot: chalk5.hex("#AAAAAA"),
3120
- mascotAccent: chalk5.hex("#FFFFFF"),
3121
- tagline: chalk5.hex("#666666"),
3122
- prompt: chalk5.hex("#CCCCCC"),
3123
- border: chalk5.hex("#333333"),
3124
- dim: chalk5.hex("#555555"),
3125
- text: chalk5.hex("#CCCCCC"),
3126
- bold: chalk5.hex("#FFFFFF").bold,
3127
- success: chalk5.hex("#AAAAAA"),
3128
- warning: chalk5.hex("#CCCCCC"),
3129
- error: chalk5.hex("#FFFFFF"),
3130
- info: chalk5.hex("#999999"),
3131
- toolName: chalk5.hex("#AAAAAA"),
3132
- toolArgs: chalk5.hex("#666666"),
3133
- toolResult: chalk5.hex("#444444"),
3134
- diffAdd: chalk5.hex("#CCCCCC"),
3135
- diffRemove: chalk5.hex("#777777").strikethrough,
3136
- diffHeader: chalk5.hex("#E0E0E0"),
3137
- mdH1: chalk5.hex("#FFFFFF").bold,
3138
- mdH2: chalk5.hex("#CCCCCC"),
3139
- mdH3: chalk5.hex("#AAAAAA"),
3140
- mdCode: chalk5.hex("#BBBBBB"),
3141
- mdInlineCode: chalk5.hex("#CCCCCC"),
3142
- mdLink: chalk5.hex("#E0E0E0").underline,
3143
- meterLow: chalk5.hex("#AAAAAA"),
3144
- meterMid: chalk5.hex("#CCCCCC"),
3145
- meterHigh: chalk5.hex("#FFFFFF")
4038
+ brand: chalk6.hex("#E0E0E0"),
4039
+ mascot: chalk6.hex("#AAAAAA"),
4040
+ mascotAccent: chalk6.hex("#FFFFFF"),
4041
+ tagline: chalk6.hex("#666666"),
4042
+ prompt: chalk6.hex("#CCCCCC"),
4043
+ border: chalk6.hex("#333333"),
4044
+ dim: chalk6.hex("#555555"),
4045
+ text: chalk6.hex("#CCCCCC"),
4046
+ bold: chalk6.hex("#FFFFFF").bold,
4047
+ success: chalk6.hex("#AAAAAA"),
4048
+ warning: chalk6.hex("#CCCCCC"),
4049
+ error: chalk6.hex("#FFFFFF"),
4050
+ info: chalk6.hex("#999999"),
4051
+ toolName: chalk6.hex("#AAAAAA"),
4052
+ toolArgs: chalk6.hex("#666666"),
4053
+ toolResult: chalk6.hex("#444444"),
4054
+ diffAdd: chalk6.hex("#CCCCCC"),
4055
+ diffRemove: chalk6.hex("#777777").strikethrough,
4056
+ diffHeader: chalk6.hex("#E0E0E0"),
4057
+ mdH1: chalk6.hex("#FFFFFF").bold,
4058
+ mdH2: chalk6.hex("#CCCCCC"),
4059
+ mdH3: chalk6.hex("#AAAAAA"),
4060
+ mdCode: chalk6.hex("#BBBBBB"),
4061
+ mdInlineCode: chalk6.hex("#CCCCCC"),
4062
+ mdLink: chalk6.hex("#E0E0E0").underline,
4063
+ meterLow: chalk6.hex("#AAAAAA"),
4064
+ meterMid: chalk6.hex("#CCCCCC"),
4065
+ meterHigh: chalk6.hex("#FFFFFF")
3146
4066
  };
3147
4067
  var THEME_CATALOG = {
3148
4068
  "default": defaultTheme,
@@ -3181,11 +4101,11 @@ function formatThemeList(activeId) {
3181
4101
  for (const id of THEME_IDS) {
3182
4102
  const t = THEME_CATALOG[id];
3183
4103
  const active = id === activeId ? t.success(" \u25CF") : " ";
3184
- const name = id === activeId ? t.bold(t.name) : chalk5.gray(t.name);
3185
- const desc = chalk5.gray(t.description);
3186
- lines.push(` ${active} ${chalk5.hex("#888")(id.padEnd(14))} ${name} ${desc}`);
4104
+ const name = id === activeId ? t.bold(t.name) : chalk6.gray(t.name);
4105
+ const desc = chalk6.gray(t.description);
4106
+ lines.push(` ${active} ${chalk6.hex("#888")(id.padEnd(14))} ${name} ${desc}`);
3187
4107
  }
3188
- lines.push(chalk5.gray(`
4108
+ lines.push(chalk6.gray(`
3189
4109
  Switch with: /theme <name>
3190
4110
  `));
3191
4111
  return lines.join("\n");
@@ -3223,7 +4143,7 @@ function colorBannerLine(line, rowIndex) {
3223
4143
  return t.brand(line);
3224
4144
  }
3225
4145
  const color = ROW_COLORS[rowIndex] ?? "#CCCCCC";
3226
- return chalk6.hex(color)(line);
4146
+ return chalk7.hex(color)(line);
3227
4147
  }
3228
4148
  var MANTIS = [
3229
4149
  " \u2571\u25C9\u25C9\u2572",
@@ -3329,12 +4249,12 @@ function formatTokens(n) {
3329
4249
  }
3330
4250
 
3331
4251
  // src/ui/update-checker.ts
3332
- import fs12 from "fs/promises";
3333
- import path13 from "path";
3334
- import os3 from "os";
3335
- import { execSync as execSync3 } from "child_process";
3336
- import chalk7 from "chalk";
3337
- var CACHE_FILE = path13.join(os3.homedir(), ".notch", "update-check.json");
4252
+ import fs15 from "fs/promises";
4253
+ import path17 from "path";
4254
+ import os4 from "os";
4255
+ import { execSync as execSync4 } from "child_process";
4256
+ import chalk8 from "chalk";
4257
+ var CACHE_FILE = path17.join(os4.homedir(), ".notch", "update-check.json");
3338
4258
  var CHECK_INTERVAL = 60 * 60 * 1e3;
3339
4259
  var PACKAGE_NAME = "@freesyntax/notch-cli";
3340
4260
  var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
@@ -3367,15 +4287,15 @@ async function checkForUpdates(currentVersion) {
3367
4287
  }
3368
4288
  }
3369
4289
  function autoUpdate(current, latest) {
3370
- console.log(chalk7.cyan(`
4290
+ console.log(chalk8.cyan(`
3371
4291
  \u2B06 Updating Notch CLI: ${current} \u2192 ${latest}...
3372
4292
  `));
3373
4293
  try {
3374
- execSync3(`npm install -g ${PACKAGE_NAME}@${latest}`, {
4294
+ execSync4(`npm install -g ${PACKAGE_NAME}@${latest}`, {
3375
4295
  stdio: "inherit",
3376
4296
  timeout: 6e4
3377
4297
  });
3378
- console.log(chalk7.green(`
4298
+ console.log(chalk8.green(`
3379
4299
  \u2713 Updated to v${latest}. Restarting...
3380
4300
  `));
3381
4301
  const args = process.argv.slice(2);
@@ -3383,7 +4303,7 @@ function autoUpdate(current, latest) {
3383
4303
  const result = spawnSync("notch", args, { stdio: "inherit" });
3384
4304
  process.exit(result.status ?? 0);
3385
4305
  } catch {
3386
- return chalk7.yellow(` Update available: ${current} \u2192 ${latest}. Run: npm install -g ${PACKAGE_NAME}@latest
4306
+ return chalk8.yellow(` Update available: ${current} \u2192 ${latest}. Run: npm install -g ${PACKAGE_NAME}@latest
3387
4307
  `);
3388
4308
  }
3389
4309
  }
@@ -3398,7 +4318,7 @@ function isNewer(latest, current) {
3398
4318
  }
3399
4319
  async function loadCache() {
3400
4320
  try {
3401
- const raw = await fs12.readFile(CACHE_FILE, "utf-8");
4321
+ const raw = await fs15.readFile(CACHE_FILE, "utf-8");
3402
4322
  return JSON.parse(raw);
3403
4323
  } catch {
3404
4324
  return null;
@@ -3406,16 +4326,16 @@ async function loadCache() {
3406
4326
  }
3407
4327
  async function saveCache(cache) {
3408
4328
  try {
3409
- await fs12.mkdir(path13.dirname(CACHE_FILE), { recursive: true });
3410
- await fs12.writeFile(CACHE_FILE, JSON.stringify(cache), "utf-8");
4329
+ await fs15.mkdir(path17.dirname(CACHE_FILE), { recursive: true });
4330
+ await fs15.writeFile(CACHE_FILE, JSON.stringify(cache), "utf-8");
3411
4331
  } catch {
3412
4332
  }
3413
4333
  }
3414
4334
 
3415
4335
  // src/permissions/index.ts
3416
- import fs13 from "fs/promises";
3417
- import path14 from "path";
3418
- import os4 from "os";
4336
+ import fs16 from "fs/promises";
4337
+ import path18 from "path";
4338
+ import os5 from "os";
3419
4339
  var DEFAULT_PERMISSIONS = {
3420
4340
  default: "prompt",
3421
4341
  rules: [
@@ -3432,11 +4352,11 @@ var DEFAULT_PERMISSIONS = {
3432
4352
  ]
3433
4353
  };
3434
4354
  async function loadPermissions(projectRoot) {
3435
- const projectPath = path14.join(projectRoot, ".notch.json");
3436
- const globalPath = path14.join(os4.homedir(), ".notch", "permissions.json");
4355
+ const projectPath = path18.join(projectRoot, ".notch.json");
4356
+ const globalPath = path18.join(os5.homedir(), ".notch", "permissions.json");
3437
4357
  let config = { ...DEFAULT_PERMISSIONS };
3438
4358
  try {
3439
- const raw = await fs13.readFile(globalPath, "utf-8");
4359
+ const raw = await fs16.readFile(globalPath, "utf-8");
3440
4360
  const parsed = JSON.parse(raw);
3441
4361
  if (parsed.permissions) {
3442
4362
  config = mergePermissions(config, parsed.permissions);
@@ -3444,7 +4364,7 @@ async function loadPermissions(projectRoot) {
3444
4364
  } catch {
3445
4365
  }
3446
4366
  try {
3447
- const raw = await fs13.readFile(projectPath, "utf-8");
4367
+ const raw = await fs16.readFile(projectPath, "utf-8");
3448
4368
  const parsed = JSON.parse(raw);
3449
4369
  if (parsed.permissions) {
3450
4370
  config = mergePermissions(config, parsed.permissions);
@@ -3490,17 +4410,18 @@ function mergePermissions(base, override) {
3490
4410
  }
3491
4411
 
3492
4412
  // src/hooks/index.ts
3493
- import { execSync as execSync4 } from "child_process";
3494
- import fs14 from "fs/promises";
3495
- import path15 from "path";
3496
- import os5 from "os";
4413
+ import { execSync as execSync5 } from "child_process";
4414
+ import fs17 from "fs/promises";
4415
+ import { watch } from "fs";
4416
+ import path19 from "path";
4417
+ import os6 from "os";
3497
4418
  import crypto from "crypto";
3498
- var TRUST_STORE_PATH = path15.join(os5.homedir(), ".notch", "trusted-projects.json");
4419
+ var TRUST_STORE_PATH = path19.join(os6.homedir(), ".notch", "trusted-projects.json");
3499
4420
  async function isTrustedProject(projectRoot, raw) {
3500
4421
  const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
3501
- const key = path15.resolve(projectRoot);
4422
+ const key = path19.resolve(projectRoot);
3502
4423
  try {
3503
- const store = JSON.parse(await fs14.readFile(TRUST_STORE_PATH, "utf-8"));
4424
+ const store = JSON.parse(await fs17.readFile(TRUST_STORE_PATH, "utf-8"));
3504
4425
  return store[key] === fingerprint;
3505
4426
  } catch {
3506
4427
  return false;
@@ -3508,30 +4429,30 @@ async function isTrustedProject(projectRoot, raw) {
3508
4429
  }
3509
4430
  async function trustProject(projectRoot, raw) {
3510
4431
  const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
3511
- const key = path15.resolve(projectRoot);
4432
+ const key = path19.resolve(projectRoot);
3512
4433
  let store = {};
3513
4434
  try {
3514
- store = JSON.parse(await fs14.readFile(TRUST_STORE_PATH, "utf-8"));
4435
+ store = JSON.parse(await fs17.readFile(TRUST_STORE_PATH, "utf-8"));
3515
4436
  } catch {
3516
4437
  }
3517
4438
  store[key] = fingerprint;
3518
- await fs14.mkdir(path15.dirname(TRUST_STORE_PATH), { recursive: true });
3519
- await fs14.writeFile(TRUST_STORE_PATH, JSON.stringify(store, null, 2));
4439
+ await fs17.mkdir(path19.dirname(TRUST_STORE_PATH), { recursive: true });
4440
+ await fs17.writeFile(TRUST_STORE_PATH, JSON.stringify(store, null, 2));
3520
4441
  }
3521
4442
  async function loadHooks(projectRoot, promptTrust) {
3522
4443
  const hooks = [];
3523
- const globalPath = path15.join(os5.homedir(), ".notch", "hooks.json");
4444
+ const globalPath = path19.join(os6.homedir(), ".notch", "hooks.json");
3524
4445
  try {
3525
- const raw = await fs14.readFile(globalPath, "utf-8");
4446
+ const raw = await fs17.readFile(globalPath, "utf-8");
3526
4447
  const parsed = JSON.parse(raw);
3527
4448
  if (Array.isArray(parsed.hooks)) {
3528
4449
  hooks.push(...parsed.hooks);
3529
4450
  }
3530
4451
  } catch {
3531
4452
  }
3532
- const projectPath = path15.join(projectRoot, ".notch.json");
4453
+ const projectPath = path19.join(projectRoot, ".notch.json");
3533
4454
  try {
3534
- const raw = await fs14.readFile(projectPath, "utf-8");
4455
+ const raw = await fs17.readFile(projectPath, "utf-8");
3535
4456
  const parsed = JSON.parse(raw);
3536
4457
  if (Array.isArray(parsed.hooks) && parsed.hooks.length > 0) {
3537
4458
  const alreadyTrusted = await isTrustedProject(projectRoot, raw);
@@ -3553,7 +4474,8 @@ async function loadHooks(projectRoot, promptTrust) {
3553
4474
  return { hooks };
3554
4475
  }
3555
4476
  async function runHooks(config, event, context) {
3556
- const matching = config.hooks.filter((h) => {
4477
+ const allHooks = [...config.hooks, ...pluginManager.getHooks()];
4478
+ const matching = allHooks.filter((h) => {
3557
4479
  if (h.event !== event) return false;
3558
4480
  if (h.tool && h.tool !== context.tool) return false;
3559
4481
  if (h.pattern && context.file && !context.file.includes(h.pattern)) return false;
@@ -3596,7 +4518,7 @@ async function executeHook(hook, context) {
3596
4518
  NOTCH_CWD: context.cwd
3597
4519
  };
3598
4520
  try {
3599
- const output = execSync4(hook.command, {
4521
+ const output = execSync5(hook.command, {
3600
4522
  cwd: context.cwd,
3601
4523
  encoding: "utf-8",
3602
4524
  timeout: hook.timeout ?? 1e4,
@@ -3613,18 +4535,48 @@ async function executeHook(hook, context) {
3613
4535
  };
3614
4536
  }
3615
4537
  }
4538
+ function startFileWatcher(projectRoot, hookConfig, onHookResult) {
4539
+ const debounceMs = 150;
4540
+ const pending = /* @__PURE__ */ new Map();
4541
+ let watcher = null;
4542
+ try {
4543
+ watcher = watch(projectRoot, { recursive: true }, (_eventType, filename) => {
4544
+ if (!filename) return;
4545
+ if (filename.startsWith(".git/") || filename.startsWith(".git\\") || filename.includes("node_modules") || filename.startsWith("dist/") || filename.startsWith("dist\\")) return;
4546
+ const existing = pending.get(filename);
4547
+ if (existing) clearTimeout(existing);
4548
+ pending.set(filename, setTimeout(async () => {
4549
+ pending.delete(filename);
4550
+ const filePath = path19.join(projectRoot, filename);
4551
+ const context = { cwd: projectRoot, file: filePath };
4552
+ const { results } = await runHooks(hookConfig, "file-changed", context);
4553
+ onHookResult?.("file-changed", results);
4554
+ if (filename === ".notch.json") {
4555
+ const { results: cfgResults } = await runHooks(hookConfig, "config-change", context);
4556
+ onHookResult?.("config-change", cfgResults);
4557
+ }
4558
+ }, debounceMs));
4559
+ });
4560
+ } catch {
4561
+ }
4562
+ return () => {
4563
+ watcher?.close();
4564
+ for (const timer of pending.values()) clearTimeout(timer);
4565
+ pending.clear();
4566
+ };
4567
+ }
3616
4568
 
3617
4569
  // src/session/index.ts
3618
- import fs15 from "fs/promises";
3619
- import path16 from "path";
3620
- import os6 from "os";
3621
- var SESSION_DIR = path16.join(os6.homedir(), ".notch", "sessions");
4570
+ import fs18 from "fs/promises";
4571
+ import path20 from "path";
4572
+ import os7 from "os";
4573
+ var SESSION_DIR = path20.join(os7.homedir(), ".notch", "sessions");
3622
4574
  var MAX_SESSIONS = 20;
3623
4575
  async function ensureDir2() {
3624
- await fs15.mkdir(SESSION_DIR, { recursive: true });
4576
+ await fs18.mkdir(SESSION_DIR, { recursive: true });
3625
4577
  }
3626
4578
  function sessionPath(id) {
3627
- return path16.join(SESSION_DIR, `${id}.json`);
4579
+ return path20.join(SESSION_DIR, `${id}.json`);
3628
4580
  }
3629
4581
  function generateId() {
3630
4582
  const now = /* @__PURE__ */ new Date();
@@ -3651,13 +4603,13 @@ async function saveSession(messages, project, model, existingId) {
3651
4603
  },
3652
4604
  messages
3653
4605
  };
3654
- await fs15.writeFile(sessionPath(id), JSON.stringify(session, null, 2), "utf-8");
4606
+ await fs18.writeFile(sessionPath(id), JSON.stringify(session, null, 2), "utf-8");
3655
4607
  await pruneOldSessions();
3656
4608
  return id;
3657
4609
  }
3658
4610
  async function loadSession(id) {
3659
4611
  try {
3660
- const raw = await fs15.readFile(sessionPath(id), "utf-8");
4612
+ const raw = await fs18.readFile(sessionPath(id), "utf-8");
3661
4613
  return JSON.parse(raw);
3662
4614
  } catch {
3663
4615
  return null;
@@ -3665,12 +4617,12 @@ async function loadSession(id) {
3665
4617
  }
3666
4618
  async function listSessions() {
3667
4619
  await ensureDir2();
3668
- const files = await fs15.readdir(SESSION_DIR);
4620
+ const files = await fs18.readdir(SESSION_DIR);
3669
4621
  const sessions = [];
3670
4622
  for (const file of files) {
3671
4623
  if (!file.endsWith(".json")) continue;
3672
4624
  try {
3673
- const raw = await fs15.readFile(path16.join(SESSION_DIR, file), "utf-8");
4625
+ const raw = await fs18.readFile(path20.join(SESSION_DIR, file), "utf-8");
3674
4626
  const session = JSON.parse(raw);
3675
4627
  sessions.push(session.meta);
3676
4628
  } catch {
@@ -3686,7 +4638,7 @@ async function loadLastSession(project) {
3686
4638
  }
3687
4639
  async function deleteSession(id) {
3688
4640
  try {
3689
- await fs15.unlink(sessionPath(id));
4641
+ await fs18.unlink(sessionPath(id));
3690
4642
  return true;
3691
4643
  } catch {
3692
4644
  return false;
@@ -3740,14 +4692,14 @@ async function exportSession(messages, outputPath, meta) {
3740
4692
  lines.push("");
3741
4693
  }
3742
4694
  }
3743
- await fs15.writeFile(outputPath, lines.join("\n"), "utf-8");
4695
+ await fs18.writeFile(outputPath, lines.join("\n"), "utf-8");
3744
4696
  return outputPath;
3745
4697
  }
3746
4698
 
3747
4699
  // src/init.ts
3748
- import fs16 from "fs/promises";
3749
- import path17 from "path";
3750
- import chalk8 from "chalk";
4700
+ import fs19 from "fs/promises";
4701
+ import path21 from "path";
4702
+ import chalk9 from "chalk";
3751
4703
  var DEFAULT_CONFIG = {
3752
4704
  model: "notch-forge",
3753
4705
  temperature: 0.3,
@@ -3778,47 +4730,47 @@ var DEFAULT_INSTRUCTIONS = `# Project Instructions for Notch
3778
4730
  <!-- Files or areas Notch should NOT modify -->
3779
4731
  `;
3780
4732
  async function initProject(projectRoot) {
3781
- const configPath = path17.join(projectRoot, ".notch.json");
3782
- const instructionsPath = path17.join(projectRoot, ".notch.md");
4733
+ const configPath = path21.join(projectRoot, ".notch.json");
4734
+ const instructionsPath = path21.join(projectRoot, ".notch.md");
3783
4735
  let configExists = false;
3784
4736
  let instructionsExist = false;
3785
4737
  try {
3786
- await fs16.access(configPath);
4738
+ await fs19.access(configPath);
3787
4739
  configExists = true;
3788
4740
  } catch {
3789
4741
  }
3790
4742
  try {
3791
- await fs16.access(instructionsPath);
4743
+ await fs19.access(instructionsPath);
3792
4744
  instructionsExist = true;
3793
4745
  } catch {
3794
4746
  }
3795
4747
  if (!configExists) {
3796
- await fs16.writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf-8");
3797
- console.log(chalk8.green(` Created ${configPath}`));
4748
+ await fs19.writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf-8");
4749
+ console.log(chalk9.green(` Created ${configPath}`));
3798
4750
  } else {
3799
- console.log(chalk8.gray(` Skipped ${configPath} (already exists)`));
4751
+ console.log(chalk9.gray(` Skipped ${configPath} (already exists)`));
3800
4752
  }
3801
4753
  if (!instructionsExist) {
3802
- await fs16.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS, "utf-8");
3803
- console.log(chalk8.green(` Created ${instructionsPath}`));
4754
+ await fs19.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS, "utf-8");
4755
+ console.log(chalk9.green(` Created ${instructionsPath}`));
3804
4756
  } else {
3805
- console.log(chalk8.gray(` Skipped ${instructionsPath} (already exists)`));
4757
+ console.log(chalk9.gray(` Skipped ${instructionsPath} (already exists)`));
3806
4758
  }
3807
- const gitignorePath = path17.join(projectRoot, ".gitignore");
4759
+ const gitignorePath = path21.join(projectRoot, ".gitignore");
3808
4760
  try {
3809
- const gitignore = await fs16.readFile(gitignorePath, "utf-8");
4761
+ const gitignore = await fs19.readFile(gitignorePath, "utf-8");
3810
4762
  const additions = [];
3811
4763
  if (!gitignore.includes(".notch.json")) additions.push(".notch.json");
3812
4764
  if (additions.length > 0) {
3813
4765
  const append = "\n# Notch CLI\n" + additions.join("\n") + "\n";
3814
- await fs16.appendFile(gitignorePath, append, "utf-8");
3815
- console.log(chalk8.green(` Updated .gitignore`));
4766
+ await fs19.appendFile(gitignorePath, append, "utf-8");
4767
+ console.log(chalk9.green(` Updated .gitignore`));
3816
4768
  }
3817
4769
  } catch {
3818
4770
  }
3819
4771
  console.log("");
3820
- console.log(chalk8.cyan(" Notch initialized! Edit .notch.md to customize behavior."));
3821
- console.log(chalk8.gray(' Run "notch" to start.\n'));
4772
+ console.log(chalk9.cyan(" Notch initialized! Edit .notch.md to customize behavior."));
4773
+ console.log(chalk9.gray(' Run "notch" to start.\n'));
3822
4774
  }
3823
4775
 
3824
4776
  // src/tools/diff-preview.ts
@@ -3937,9 +4889,1198 @@ function findSync(oldLines, newLines, oi, ni, lookAhead) {
3937
4889
  return null;
3938
4890
  }
3939
4891
 
4892
+ // src/commands/doctor.ts
4893
+ import { execSync as execSync6 } from "child_process";
4894
+ import fs20 from "fs/promises";
4895
+ import path22 from "path";
4896
+ import os8 from "os";
4897
+ import chalk10 from "chalk";
4898
+ async function runDiagnostics(cwd) {
4899
+ const results = [];
4900
+ const nodeVersion = process.versions.node;
4901
+ const major = parseInt(nodeVersion.split(".")[0], 10);
4902
+ if (major >= 20) {
4903
+ results.push({ name: "Node.js", status: "ok", message: `v${nodeVersion}` });
4904
+ } else if (major >= 18) {
4905
+ results.push({ name: "Node.js", status: "warn", message: `v${nodeVersion} (recommend >= 20)` });
4906
+ } else {
4907
+ results.push({ name: "Node.js", status: "fail", message: `v${nodeVersion} (requires >= 18)` });
4908
+ }
4909
+ try {
4910
+ const gitVersion = execSync6("git --version", { encoding: "utf-8", timeout: 5e3 }).trim();
4911
+ results.push({ name: "Git", status: "ok", message: gitVersion });
4912
+ } catch {
4913
+ results.push({ name: "Git", status: "fail", message: "Not found. Install git to use git tools." });
4914
+ }
4915
+ try {
4916
+ const config = await loadConfig({ projectRoot: cwd });
4917
+ const apiKey = config.models.chat.apiKey;
4918
+ if (apiKey) {
4919
+ results.push({ name: "API Key", status: "ok", message: `Set (${apiKey.slice(0, 8)}...)` });
4920
+ } else {
4921
+ results.push({ name: "API Key", status: "fail", message: "Not set. Run: notch login" });
4922
+ }
4923
+ try {
4924
+ const check = await validateConfig(config.models.chat);
4925
+ if (check.ok) {
4926
+ results.push({ name: "API Endpoint", status: "ok", message: `${config.models.chat.model} reachable` });
4927
+ } else {
4928
+ results.push({ name: "API Endpoint", status: "fail", message: check.error ?? "Unreachable" });
4929
+ }
4930
+ } catch (err) {
4931
+ results.push({ name: "API Endpoint", status: "fail", message: err.message });
4932
+ }
4933
+ } catch (err) {
4934
+ results.push({ name: "Config", status: "fail", message: `Could not load: ${err.message}` });
4935
+ }
4936
+ try {
4937
+ await fs20.access(path22.join(cwd, ".notch.json"));
4938
+ results.push({ name: ".notch.json", status: "ok", message: "Found" });
4939
+ } catch {
4940
+ results.push({ name: ".notch.json", status: "warn", message: "Not found. Run: notch init" });
4941
+ }
4942
+ const notchDir = path22.join(os8.homedir(), ".notch");
4943
+ try {
4944
+ await fs20.access(notchDir);
4945
+ results.push({ name: "~/.notch/", status: "ok", message: "Exists" });
4946
+ } catch {
4947
+ results.push({ name: "~/.notch/", status: "warn", message: "Not found (will be created on first use)" });
4948
+ }
4949
+ try {
4950
+ await fs20.access(path22.join(cwd, ".notch.md"));
4951
+ results.push({ name: ".notch.md", status: "ok", message: "Found" });
4952
+ } catch {
4953
+ results.push({ name: ".notch.md", status: "warn", message: "Not found. Run: notch init" });
4954
+ }
4955
+ try {
4956
+ const configRaw = await fs20.readFile(path22.join(cwd, ".notch.json"), "utf-8").catch(() => "{}");
4957
+ const mcpConfigs = parseMCPConfig(JSON.parse(configRaw));
4958
+ const serverNames = Object.keys(mcpConfigs);
4959
+ if (serverNames.length === 0) {
4960
+ results.push({ name: "MCP Servers", status: "ok", message: "None configured" });
4961
+ } else {
4962
+ for (const name of serverNames) {
4963
+ try {
4964
+ const client = new MCPClient(mcpConfigs[name], name);
4965
+ await client.connect();
4966
+ results.push({ name: `MCP: ${name}`, status: "ok", message: `${client.tools.length} tools` });
4967
+ client.disconnect();
4968
+ } catch (err) {
4969
+ results.push({ name: `MCP: ${name}`, status: "fail", message: err.message });
4970
+ }
4971
+ }
4972
+ }
4973
+ } catch {
4974
+ results.push({ name: "MCP Servers", status: "ok", message: "No config to check" });
4975
+ }
4976
+ try {
4977
+ const sessionsDir = path22.join(notchDir, "sessions");
4978
+ const entries = await fs20.readdir(sessionsDir).catch(() => []);
4979
+ results.push({ name: "Sessions", status: "ok", message: `${entries.length} saved` });
4980
+ } catch {
4981
+ results.push({ name: "Sessions", status: "ok", message: "0 saved" });
4982
+ }
4983
+ return results;
4984
+ }
4985
+ function formatDiagnostics(results) {
4986
+ const icons = { ok: chalk10.green("\u2713"), warn: chalk10.yellow("!"), fail: chalk10.red("\u2717") };
4987
+ const lines = results.map((r) => {
4988
+ const icon = icons[r.status];
4989
+ const name = r.name.padEnd(16);
4990
+ return ` ${icon} ${name} ${chalk10.gray(r.message)}`;
4991
+ });
4992
+ return "\n" + lines.join("\n") + "\n";
4993
+ }
4994
+ registerCommand("/doctor", async (_args, ctx) => {
4995
+ ctx.log("Running diagnostics...");
4996
+ const results = await runDiagnostics(ctx.cwd);
4997
+ console.log(formatDiagnostics(results));
4998
+ const failCount = results.filter((r) => r.status === "fail").length;
4999
+ const warnCount = results.filter((r) => r.status === "warn").length;
5000
+ if (failCount > 0) {
5001
+ console.log(chalk10.red(` ${failCount} issue(s) need attention.
5002
+ `));
5003
+ } else if (warnCount > 0) {
5004
+ console.log(chalk10.yellow(` All clear with ${warnCount} suggestion(s).
5005
+ `));
5006
+ } else {
5007
+ console.log(chalk10.green(" Everything looks good!\n"));
5008
+ }
5009
+ });
5010
+
5011
+ // src/commands/copy.ts
5012
+ import { execSync as execSync7 } from "child_process";
5013
+ import chalk11 from "chalk";
5014
+ function copyToClipboard(text) {
5015
+ try {
5016
+ const platform = process.platform;
5017
+ if (platform === "win32") {
5018
+ execSync7("clip.exe", { input: text, timeout: 5e3 });
5019
+ } else if (platform === "darwin") {
5020
+ execSync7("pbcopy", { input: text, timeout: 5e3 });
5021
+ } else {
5022
+ try {
5023
+ execSync7("xclip -selection clipboard", { input: text, timeout: 5e3 });
5024
+ } catch {
5025
+ try {
5026
+ execSync7("xsel --clipboard --input", { input: text, timeout: 5e3 });
5027
+ } catch {
5028
+ execSync7("wl-copy", { input: text, timeout: 5e3 });
5029
+ }
5030
+ }
5031
+ }
5032
+ return true;
5033
+ } catch {
5034
+ return false;
5035
+ }
5036
+ }
5037
+ registerCommand("/copy", async (_args, ctx) => {
5038
+ if (!ctx.lastResponse) {
5039
+ console.log(chalk11.gray(" Nothing to copy yet.\n"));
5040
+ return;
5041
+ }
5042
+ if (copyToClipboard(ctx.lastResponse)) {
5043
+ const preview = ctx.lastResponse.slice(0, 60).replace(/\n/g, " ");
5044
+ console.log(chalk11.green(` \u2713 Copied to clipboard`) + chalk11.gray(` (${ctx.lastResponse.length} chars)`));
5045
+ console.log(chalk11.gray(` "${preview}${ctx.lastResponse.length > 60 ? "..." : ""}"
5046
+ `));
5047
+ } else {
5048
+ console.log(chalk11.red(" Clipboard not available."));
5049
+ console.log(chalk11.gray(" Install xclip (Linux) or ensure clip.exe/pbcopy is available.\n"));
5050
+ }
5051
+ });
5052
+
5053
+ // src/commands/btw.ts
5054
+ import chalk12 from "chalk";
5055
+ registerCommand("/btw", async (args, ctx) => {
5056
+ if (!args) {
5057
+ console.log(chalk12.gray(" Usage: /btw <question>\n"));
5058
+ return;
5059
+ }
5060
+ console.log(chalk12.gray(" (Side question \u2014 won't affect main conversation)\n"));
5061
+ const tempMessages = [
5062
+ { role: "user", content: args }
5063
+ ];
5064
+ try {
5065
+ await ctx.runPrompt(args, tempMessages);
5066
+ console.log(chalk12.gray("\n (Side question complete \u2014 context unchanged)\n"));
5067
+ } catch (err) {
5068
+ console.log(chalk12.red(` Side question failed: ${err.message}
5069
+ `));
5070
+ }
5071
+ });
5072
+
5073
+ // src/commands/security-review.ts
5074
+ import { execFileSync as execFileSync2, execSync as execSync8 } from "child_process";
5075
+ import chalk13 from "chalk";
5076
+ function isValidGitRange(range) {
5077
+ return /^[a-zA-Z0-9._~^\/\-]+(\.\.[a-zA-Z0-9._~^\/\-]+)?$/.test(range);
5078
+ }
5079
+ registerCommand("/security-review", async (args, ctx) => {
5080
+ const range = args || "HEAD~5..HEAD";
5081
+ if (!isValidGitRange(range)) {
5082
+ console.log(chalk13.red(" Invalid git range. Use formats like: HEAD~5..HEAD, main..feature, abc123\n"));
5083
+ return;
5084
+ }
5085
+ let diff;
5086
+ let stat;
5087
+ try {
5088
+ stat = execFileSync2("git", ["diff", range, "--stat"], {
5089
+ cwd: ctx.cwd,
5090
+ encoding: "utf-8",
5091
+ timeout: 1e4
5092
+ }).trim();
5093
+ diff = execFileSync2("git", ["diff", range], {
5094
+ cwd: ctx.cwd,
5095
+ encoding: "utf-8",
5096
+ timeout: 1e4,
5097
+ maxBuffer: 1024 * 1024 * 5
5098
+ // 5MB
5099
+ }).trim();
5100
+ } catch {
5101
+ try {
5102
+ stat = execSync8("git diff --stat", {
5103
+ cwd: ctx.cwd,
5104
+ encoding: "utf-8",
5105
+ timeout: 1e4
5106
+ }).trim();
5107
+ diff = execSync8("git diff", {
5108
+ cwd: ctx.cwd,
5109
+ encoding: "utf-8",
5110
+ timeout: 1e4,
5111
+ maxBuffer: 1024 * 1024 * 5
5112
+ }).trim();
5113
+ } catch (err) {
5114
+ console.log(chalk13.red(` Could not get diff: ${err.message}
5115
+ `));
5116
+ return;
5117
+ }
5118
+ }
5119
+ if (!diff) {
5120
+ console.log(chalk13.gray(" No changes to review.\n"));
5121
+ return;
5122
+ }
5123
+ console.log(chalk13.gray(` Reviewing changes...
5124
+ `));
5125
+ console.log(chalk13.gray(` ${stat}
5126
+ `));
5127
+ const maxDiffLen = 5e4;
5128
+ const truncatedDiff = diff.length > maxDiffLen ? diff.slice(0, maxDiffLen) + "\n... (truncated)" : diff;
5129
+ const prompt = `Perform a thorough security review of these recent code changes. Use your tools to read full files if you need more context.
5130
+
5131
+ ## Changed Files
5132
+ ${stat}
5133
+
5134
+ ## Diff
5135
+ \`\`\`diff
5136
+ ${truncatedDiff}
5137
+ \`\`\`
5138
+
5139
+ ## Review Checklist
5140
+ Analyze for these vulnerability categories:
5141
+ 1. **Secrets & Credentials** \u2014 Hardcoded API keys, tokens, passwords, or connection strings
5142
+ 2. **Injection** \u2014 SQL injection, command injection, XSS, template injection
5143
+ 3. **Authentication & Authorization** \u2014 Broken auth, missing access controls, session issues
5144
+ 4. **Input Validation** \u2014 Missing or insufficient input validation at system boundaries
5145
+ 5. **Path Traversal** \u2014 Unsanitized file paths that could escape the project root
5146
+ 6. **Dependency Issues** \u2014 New dependencies with known vulnerabilities
5147
+ 7. **Cryptography** \u2014 Weak algorithms, hardcoded IVs/salts, insecure random
5148
+ 8. **Information Disclosure** \u2014 Verbose errors, stack traces in production, debug endpoints
5149
+ 9. **Race Conditions** \u2014 TOCTOU, unsafe concurrent access to shared state
5150
+ 10. **Configuration** \u2014 Insecure defaults, overly permissive CORS/CSP/permissions
5151
+
5152
+ For each finding, provide:
5153
+ - **Severity**: Critical / High / Medium / Low
5154
+ - **File and line**: Where the issue is
5155
+ - **Description**: What the vulnerability is
5156
+ - **Fix**: How to remediate it
5157
+
5158
+ If no issues are found, say so explicitly. Do not fabricate issues.`;
5159
+ ctx.messages.push({ role: "user", content: prompt });
5160
+ try {
5161
+ await ctx.runPrompt(prompt, ctx.messages);
5162
+ } catch (err) {
5163
+ console.log(chalk13.red(` Security review failed: ${err.message}
5164
+ `));
5165
+ }
5166
+ });
5167
+
5168
+ // src/commands/loop.ts
5169
+ import chalk14 from "chalk";
5170
+ var activeLoop = null;
5171
+ function stopActiveLoop() {
5172
+ if (activeLoop) {
5173
+ clearInterval(activeLoop.timer);
5174
+ activeLoop = null;
5175
+ }
5176
+ }
5177
+ function parseDuration(s) {
5178
+ const match = s.match(/^(\d+)(s|m|h)$/);
5179
+ if (!match) return null;
5180
+ const n = parseInt(match[1], 10);
5181
+ switch (match[2]) {
5182
+ case "s":
5183
+ return n * 1e3;
5184
+ case "m":
5185
+ return n * 6e4;
5186
+ case "h":
5187
+ return n * 36e5;
5188
+ default:
5189
+ return null;
5190
+ }
5191
+ }
5192
+ function formatDuration(ms) {
5193
+ if (ms >= 36e5) return `${ms / 36e5}h`;
5194
+ if (ms >= 6e4) return `${ms / 6e4}m`;
5195
+ return `${ms / 1e3}s`;
5196
+ }
5197
+ registerCommand("/loop", async (args, ctx) => {
5198
+ if (args === "stop" || args === "cancel") {
5199
+ if (activeLoop) {
5200
+ clearInterval(activeLoop.timer);
5201
+ console.log(chalk14.gray(` Stopped loop: ${activeLoop.command} (was every ${formatDuration(activeLoop.intervalMs)})
5202
+ `));
5203
+ activeLoop = null;
5204
+ } else {
5205
+ console.log(chalk14.gray(" No active loop.\n"));
5206
+ }
5207
+ return;
5208
+ }
5209
+ if (args === "status" || !args) {
5210
+ if (activeLoop) {
5211
+ console.log(chalk14.gray(` Active loop: ${chalk14.white(activeLoop.command)} every ${formatDuration(activeLoop.intervalMs)}
5212
+ `));
5213
+ console.log(chalk14.gray(" Stop with: /loop stop\n"));
5214
+ } else {
5215
+ console.log(chalk14.gray(" No active loop.\n"));
5216
+ console.log(chalk14.gray(" Usage: /loop <interval> <command>"));
5217
+ console.log(chalk14.gray(" Example: /loop 5m /status\n"));
5218
+ }
5219
+ return;
5220
+ }
5221
+ const parts = args.split(/\s+/);
5222
+ if (parts.length < 2) {
5223
+ console.log(chalk14.gray(" Usage: /loop <interval> <command>"));
5224
+ console.log(chalk14.gray(" Example: /loop 5m /status"));
5225
+ console.log(chalk14.gray(" Intervals: 30s, 5m, 1h\n"));
5226
+ return;
5227
+ }
5228
+ const intervalMs = parseDuration(parts[0]);
5229
+ if (!intervalMs) {
5230
+ console.log(chalk14.red(` Invalid interval: ${parts[0]}`));
5231
+ console.log(chalk14.gray(" Use formats like: 30s, 5m, 1h\n"));
5232
+ return;
5233
+ }
5234
+ if (intervalMs < 1e4) {
5235
+ console.log(chalk14.red(" Minimum interval is 10s.\n"));
5236
+ return;
5237
+ }
5238
+ const command = parts.slice(1).join(" ");
5239
+ if (!command.startsWith("/")) {
5240
+ console.log(chalk14.red(" Command must start with /\n"));
5241
+ return;
5242
+ }
5243
+ if (activeLoop) {
5244
+ clearInterval(activeLoop.timer);
5245
+ }
5246
+ const timer = setInterval(async () => {
5247
+ console.log(chalk14.gray(`
5248
+ [loop] Running: ${command}`));
5249
+ try {
5250
+ const handled = await dispatchCommand(command, ctx);
5251
+ if (!handled) {
5252
+ console.log(chalk14.gray(` [loop] Unknown command: ${command}`));
5253
+ }
5254
+ } catch (err) {
5255
+ console.log(chalk14.red(` [loop] Error: ${err.message}`));
5256
+ }
5257
+ }, intervalMs);
5258
+ activeLoop = { timer, command, intervalMs };
5259
+ console.log(chalk14.green(` \u2713 Loop started: ${chalk14.white(command)} every ${formatDuration(intervalMs)}`));
5260
+ console.log(chalk14.gray(" Stop with: /loop stop\n"));
5261
+ });
5262
+
5263
+ // src/commands/batch.ts
5264
+ import chalk15 from "chalk";
5265
+ import ora from "ora";
5266
+ var MAX_CONCURRENT = 3;
5267
+ async function pLimit(tasks2, limit) {
5268
+ const results = [];
5269
+ let idx = 0;
5270
+ async function runNext() {
5271
+ while (idx < tasks2.length) {
5272
+ const i = idx++;
5273
+ try {
5274
+ const value = await tasks2[i]();
5275
+ results[i] = { status: "fulfilled", value };
5276
+ } catch (reason) {
5277
+ results[i] = { status: "rejected", reason };
5278
+ }
5279
+ }
5280
+ }
5281
+ const workers = Array.from({ length: Math.min(limit, tasks2.length) }, () => runNext());
5282
+ await Promise.all(workers);
5283
+ return results;
5284
+ }
5285
+ registerCommand("/batch", async (args, ctx) => {
5286
+ if (!args) {
5287
+ console.log(chalk15.gray(" Usage: /batch <task description>"));
5288
+ console.log(chalk15.gray(" Example: /batch add JSDoc to all exported functions"));
5289
+ console.log(chalk15.gray(" Example: /batch convert var to const in src/**/*.js\n"));
5290
+ return;
5291
+ }
5292
+ const spinner = ora("Identifying target files...").start();
5293
+ const discoveryPrompt = `I need to apply this change across multiple files: "${args}"
5294
+
5295
+ List ONLY the file paths that need this change, one per line. Use the glob and grep tools to find them. Output the final list as a simple newline-separated list of file paths with no other text, wrapped in <files> tags like:
5296
+
5297
+ <files>
5298
+ src/foo.ts
5299
+ src/bar.ts
5300
+ </files>`;
5301
+ const tempMessages = [
5302
+ { role: "user", content: discoveryPrompt }
5303
+ ];
5304
+ let fileList = [];
5305
+ try {
5306
+ const responseText = await ctx.runPrompt(discoveryPrompt, tempMessages);
5307
+ spinner.stop();
5308
+ const match = responseText.match(/<files>\s*([\s\S]*?)\s*<\/files>/);
5309
+ if (match) {
5310
+ fileList = match[1].split("\n").map((f) => f.trim()).filter(Boolean);
5311
+ }
5312
+ if (fileList.length === 0) {
5313
+ console.log(chalk15.yellow(" No target files identified.\n"));
5314
+ return;
5315
+ }
5316
+ } catch (err) {
5317
+ spinner.fail(`Failed to identify files: ${err.message}`);
5318
+ return;
5319
+ }
5320
+ console.log(chalk15.gray(`
5321
+ Found ${fileList.length} file(s) to process:
5322
+ `));
5323
+ for (const f of fileList) {
5324
+ console.log(chalk15.gray(` \u2022 ${f}`));
5325
+ }
5326
+ console.log("");
5327
+ const batchSpinner = ora(`Processing 0/${fileList.length} files...`).start();
5328
+ let completed = 0;
5329
+ const tasks2 = fileList.map((file) => async () => {
5330
+ const fileMessages = [
5331
+ {
5332
+ role: "user",
5333
+ content: `Apply this change to the file "${file}": ${args}
5334
+
5335
+ Read the file first, then make the change. Only modify this one file.`
5336
+ }
5337
+ ];
5338
+ try {
5339
+ await ctx.runPrompt(fileMessages[0].content, fileMessages);
5340
+ completed++;
5341
+ batchSpinner.text = `Processing ${completed}/${fileList.length} files...`;
5342
+ return { file, ok: true };
5343
+ } catch (err) {
5344
+ completed++;
5345
+ batchSpinner.text = `Processing ${completed}/${fileList.length} files...`;
5346
+ return { file, ok: false, error: err.message };
5347
+ }
5348
+ });
5349
+ const results = await pLimit(tasks2, MAX_CONCURRENT);
5350
+ batchSpinner.stop();
5351
+ console.log(chalk15.gray("\n Batch Results:\n"));
5352
+ let successes = 0;
5353
+ let failures = 0;
5354
+ for (const r of results) {
5355
+ if (r.status === "fulfilled") {
5356
+ if (r.value.ok) {
5357
+ console.log(chalk15.green(` \u2713 ${r.value.file}`));
5358
+ successes++;
5359
+ } else {
5360
+ console.log(chalk15.red(` \u2717 ${r.value.file}: ${r.value.error}`));
5361
+ failures++;
5362
+ }
5363
+ } else {
5364
+ console.log(chalk15.red(` \u2717 Unknown: ${r.reason?.message ?? "failed"}`));
5365
+ failures++;
5366
+ }
5367
+ }
5368
+ console.log("");
5369
+ if (failures === 0) {
5370
+ console.log(chalk15.green(` \u2713 All ${successes} file(s) processed successfully.
5371
+ `));
5372
+ } else {
5373
+ console.log(chalk15.yellow(` ${successes} succeeded, ${failures} failed.
5374
+ `));
5375
+ }
5376
+ });
5377
+
5378
+ // src/commands/plugin.ts
5379
+ import { execSync as execSync9, execFileSync as execFileSync3 } from "child_process";
5380
+ import fs21 from "fs/promises";
5381
+ import path23 from "path";
5382
+ import os9 from "os";
5383
+ import chalk16 from "chalk";
5384
+ var GLOBAL_PLUGINS_DIR = path23.join(os9.homedir(), ".notch", "plugins");
5385
+ registerCommand("/plugin", async (args, ctx) => {
5386
+ const parts = args.split(/\s+/);
5387
+ const subcommand = parts[0] || "list";
5388
+ const target = parts.slice(1).join(" ");
5389
+ switch (subcommand) {
5390
+ case "list":
5391
+ case "ls": {
5392
+ const plugins = pluginManager.list();
5393
+ if (plugins.length === 0) {
5394
+ console.log(chalk16.gray(" No plugins installed.\n"));
5395
+ console.log(chalk16.gray(" Install one: /plugin install <npm-package-or-git-url>\n"));
5396
+ return;
5397
+ }
5398
+ console.log(chalk16.gray("\n Installed plugins:\n"));
5399
+ for (const p of plugins) {
5400
+ const toolCount = p.tools.length;
5401
+ const hookCount = p.hooks.length;
5402
+ const meta = [
5403
+ toolCount > 0 ? `${toolCount} tool${toolCount > 1 ? "s" : ""}` : "",
5404
+ hookCount > 0 ? `${hookCount} hook${hookCount > 1 ? "s" : ""}` : ""
5405
+ ].filter(Boolean).join(", ");
5406
+ console.log(` ${chalk16.green("\u25CF")} ${chalk16.white(p.name)} ${chalk16.gray(`v${p.version}`)}${meta ? chalk16.gray(` (${meta})`) : ""}`);
5407
+ if (p.description) {
5408
+ console.log(chalk16.gray(` ${p.description}`));
5409
+ }
5410
+ }
5411
+ console.log("");
5412
+ return;
5413
+ }
5414
+ case "install":
5415
+ case "add": {
5416
+ if (!target) {
5417
+ console.log(chalk16.gray(" Usage: /plugin install <npm-package-or-git-url>\n"));
5418
+ return;
5419
+ }
5420
+ await fs21.mkdir(GLOBAL_PLUGINS_DIR, { recursive: true });
5421
+ const pluginDir = path23.join(GLOBAL_PLUGINS_DIR, path23.basename(target).replace(/\.git$/, ""));
5422
+ console.log(chalk16.gray(` Installing ${target}...`));
5423
+ try {
5424
+ if (target.includes("/") && !target.startsWith("@")) {
5425
+ execFileSync3("git", ["clone", "--depth", "1", target, pluginDir], {
5426
+ encoding: "utf-8",
5427
+ timeout: 6e4,
5428
+ stdio: "pipe"
5429
+ });
5430
+ } else {
5431
+ await fs21.mkdir(pluginDir, { recursive: true });
5432
+ execFileSync3("npm", ["init", "-y"], {
5433
+ cwd: pluginDir,
5434
+ encoding: "utf-8",
5435
+ timeout: 3e4,
5436
+ stdio: "pipe"
5437
+ });
5438
+ execFileSync3("npm", ["install", target], {
5439
+ cwd: pluginDir,
5440
+ encoding: "utf-8",
5441
+ timeout: 12e4,
5442
+ stdio: "pipe"
5443
+ });
5444
+ }
5445
+ try {
5446
+ const pkgExists = await fs21.access(path23.join(pluginDir, "package.json")).then(() => true).catch(() => false);
5447
+ if (pkgExists) {
5448
+ execSync9("npm install --production", {
5449
+ cwd: pluginDir,
5450
+ encoding: "utf-8",
5451
+ timeout: 12e4,
5452
+ stdio: "pipe"
5453
+ });
5454
+ }
5455
+ } catch {
5456
+ }
5457
+ console.log(chalk16.green(` \u2713 Installed to ${pluginDir}`));
5458
+ console.log(chalk16.gray(" Restart notch to load the plugin.\n"));
5459
+ } catch (err) {
5460
+ console.log(chalk16.red(` Install failed: ${err.message}
5461
+ `));
5462
+ }
5463
+ return;
5464
+ }
5465
+ case "remove":
5466
+ case "rm":
5467
+ case "uninstall": {
5468
+ if (!target) {
5469
+ console.log(chalk16.gray(" Usage: /plugin remove <plugin-name>\n"));
5470
+ return;
5471
+ }
5472
+ const pluginDir = path23.join(GLOBAL_PLUGINS_DIR, target);
5473
+ try {
5474
+ await fs21.access(pluginDir);
5475
+ await fs21.rm(pluginDir, { recursive: true, force: true });
5476
+ console.log(chalk16.green(` \u2713 Removed ${target}`));
5477
+ console.log(chalk16.gray(" Restart notch to apply.\n"));
5478
+ } catch {
5479
+ console.log(chalk16.red(` Plugin not found: ${target}
5480
+ `));
5481
+ }
5482
+ return;
5483
+ }
5484
+ default:
5485
+ console.log(chalk16.gray(" Usage: /plugin <list|install|remove> [name]\n"));
5486
+ }
5487
+ });
5488
+
5489
+ // src/commands/commit.ts
5490
+ import { execFileSync as execFileSync4 } from "child_process";
5491
+ import chalk17 from "chalk";
5492
+ import ora2 from "ora";
5493
+ function gitCommit(message, cwd) {
5494
+ execFileSync4("git", ["commit", "-m", message], {
5495
+ cwd,
5496
+ encoding: "utf-8",
5497
+ timeout: 3e4
5498
+ });
5499
+ }
5500
+ registerCommand("/commit", async (args, ctx) => {
5501
+ let diff;
5502
+ let stat;
5503
+ try {
5504
+ const staged = execFileSync4("git", ["diff", "--cached", "--stat"], { cwd: ctx.cwd, encoding: "utf-8" }).trim();
5505
+ if (!staged) {
5506
+ execFileSync4("git", ["add", "-A"], { cwd: ctx.cwd, encoding: "utf-8" });
5507
+ }
5508
+ stat = execFileSync4("git", ["diff", "--cached", "--stat"], { cwd: ctx.cwd, encoding: "utf-8" }).trim();
5509
+ diff = execFileSync4("git", ["diff", "--cached"], { cwd: ctx.cwd, encoding: "utf-8", maxBuffer: 5 * 1024 * 1024 }).trim();
5510
+ } catch (err) {
5511
+ console.log(chalk17.red(` Git error: ${err.message}
5512
+ `));
5513
+ return;
5514
+ }
5515
+ if (!diff) {
5516
+ console.log(chalk17.gray(" No changes to commit.\n"));
5517
+ return;
5518
+ }
5519
+ console.log(chalk17.gray(`
5520
+ Staged changes:
5521
+ ${stat}
5522
+ `));
5523
+ if (args) {
5524
+ try {
5525
+ gitCommit(args, ctx.cwd);
5526
+ console.log(chalk17.green(` \u2713 Committed: ${args}
5527
+ `));
5528
+ } catch (err) {
5529
+ console.log(chalk17.red(` Commit failed: ${err.message}
5530
+ `));
5531
+ }
5532
+ return;
5533
+ }
5534
+ const spinner = ora2("Generating commit message...").start();
5535
+ const truncatedDiff = diff.length > 3e4 ? diff.slice(0, 3e4) + "\n... (truncated)" : diff;
5536
+ const prompt = `Generate a concise git commit message for these changes. Follow conventional commit format (feat:, fix:, refactor:, docs:, chore:, etc.). Keep the first line under 72 characters. Add a blank line then a brief body if needed.
5537
+
5538
+ Changes:
5539
+ ${stat}
5540
+
5541
+ Diff:
5542
+ \`\`\`diff
5543
+ ${truncatedDiff}
5544
+ \`\`\`
5545
+
5546
+ Reply with ONLY the commit message, nothing else. No markdown, no explanation.`;
5547
+ const tempMessages = [
5548
+ { role: "user", content: prompt }
5549
+ ];
5550
+ try {
5551
+ const message = await ctx.runPrompt(prompt, tempMessages);
5552
+ spinner.stop();
5553
+ const cleanMessage = message.replace(/^```\n?/gm, "").replace(/\n?```$/gm, "").replace(/^["'`]/g, "").replace(/["'`]$/g, "").trim();
5554
+ console.log(chalk17.white(`
5555
+ ${cleanMessage.split("\n")[0]}
5556
+ `));
5557
+ if (cleanMessage.includes("\n")) {
5558
+ const body = cleanMessage.split("\n").slice(1).join("\n").trim();
5559
+ if (body) console.log(chalk17.gray(` ${body}
5560
+ `));
5561
+ }
5562
+ const rl = await import("readline");
5563
+ const confirm = rl.createInterface({ input: process.stdin, output: process.stdout });
5564
+ const answer = await new Promise((resolve2) => {
5565
+ confirm.question(chalk17.yellow(" Commit with this message? [Y/n/e(dit)] "), resolve2);
5566
+ });
5567
+ confirm.close();
5568
+ const choice = answer.trim().toLowerCase();
5569
+ if (choice === "n" || choice === "no") {
5570
+ console.log(chalk17.gray(" Commit cancelled.\n"));
5571
+ return;
5572
+ }
5573
+ let finalMessage = cleanMessage;
5574
+ if (choice === "e" || choice === "edit") {
5575
+ const editRl = rl.createInterface({ input: process.stdin, output: process.stdout });
5576
+ finalMessage = await new Promise((resolve2) => {
5577
+ editRl.question(chalk17.gray(" Enter commit message: "), resolve2);
5578
+ });
5579
+ editRl.close();
5580
+ if (!finalMessage.trim()) {
5581
+ console.log(chalk17.gray(" Commit cancelled.\n"));
5582
+ return;
5583
+ }
5584
+ }
5585
+ try {
5586
+ gitCommit(finalMessage, ctx.cwd);
5587
+ console.log(chalk17.green(` \u2713 Committed: ${finalMessage.split("\n")[0]}
5588
+ `));
5589
+ } catch (err) {
5590
+ console.log(chalk17.red(` Commit failed: ${err.message}
5591
+ `));
5592
+ }
5593
+ } catch (err) {
5594
+ spinner.fail(`Failed: ${err.message}`);
5595
+ }
5596
+ });
5597
+
5598
+ // src/commands/pr.ts
5599
+ import { execSync as execSync11, execFileSync as execFileSync5 } from "child_process";
5600
+ import chalk18 from "chalk";
5601
+ import ora3 from "ora";
5602
+ function tryExec(cmd, cwd) {
5603
+ try {
5604
+ return execSync11(cmd, { cwd, encoding: "utf-8", timeout: 15e3 }).trim();
5605
+ } catch {
5606
+ return null;
5607
+ }
5608
+ }
5609
+ registerCommand("/pr", async (args, ctx) => {
5610
+ const branch = tryExec("git branch --show-current", ctx.cwd);
5611
+ if (!branch) {
5612
+ console.log(chalk18.red(" Not a git repository or not on a branch.\n"));
5613
+ return;
5614
+ }
5615
+ const baseBranch = tryExec("git rev-parse --abbrev-ref origin/HEAD", ctx.cwd)?.replace("origin/", "") || tryExec("git symbolic-ref refs/remotes/origin/HEAD", ctx.cwd)?.replace("refs/remotes/origin/", "") || "main";
5616
+ if (branch === baseBranch) {
5617
+ console.log(chalk18.red(` You're on ${baseBranch}. Switch to a feature branch first.
5618
+ `));
5619
+ return;
5620
+ }
5621
+ if (!tryExec("gh --version", ctx.cwd)) {
5622
+ console.log(chalk18.red(" GitHub CLI (gh) not found. Install it: https://cli.github.com\n"));
5623
+ return;
5624
+ }
5625
+ const stat = tryExec(`git diff ${baseBranch}...HEAD --stat`, ctx.cwd) || "";
5626
+ const log = tryExec(`git log ${baseBranch}..HEAD --oneline`, ctx.cwd) || "";
5627
+ const diff = tryExec(`git diff ${baseBranch}...HEAD`, ctx.cwd) || "";
5628
+ if (!log) {
5629
+ console.log(chalk18.gray(" No commits ahead of base branch.\n"));
5630
+ return;
5631
+ }
5632
+ console.log(chalk18.gray(`
5633
+ Branch: ${chalk18.white(branch)} \u2192 ${baseBranch}`));
5634
+ console.log(chalk18.gray(` Commits:
5635
+ ${log.split("\n").map((l) => ` ${l}`).join("\n")}
5636
+ `));
5637
+ console.log(chalk18.gray(` Files:
5638
+ ${stat}
5639
+ `));
5640
+ const hasRemote = tryExec(`git rev-parse --abbrev-ref ${branch}@{upstream}`, ctx.cwd);
5641
+ if (!hasRemote) {
5642
+ console.log(chalk18.gray(" Pushing branch to origin..."));
5643
+ try {
5644
+ execFileSync5("git", ["push", "-u", "origin", branch], { cwd: ctx.cwd, encoding: "utf-8", timeout: 3e4 });
5645
+ console.log(chalk18.green(" \u2713 Pushed\n"));
5646
+ } catch (err) {
5647
+ console.log(chalk18.red(` Push failed: ${err.message}
5648
+ `));
5649
+ return;
5650
+ }
5651
+ }
5652
+ if (args) {
5653
+ try {
5654
+ const result = execFileSync5("gh", ["pr", "create", "--title", args, "--body", "Auto-generated by Notch CLI", "--base", baseBranch], {
5655
+ cwd: ctx.cwd,
5656
+ encoding: "utf-8",
5657
+ timeout: 3e4
5658
+ });
5659
+ console.log(chalk18.green(` \u2713 ${result.trim()}
5660
+ `));
5661
+ } catch (err) {
5662
+ console.log(chalk18.red(` PR creation failed: ${err.message}
5663
+ `));
5664
+ }
5665
+ return;
5666
+ }
5667
+ const spinner = ora3("Generating PR description...").start();
5668
+ const truncatedDiff = diff.length > 3e4 ? diff.slice(0, 3e4) + "\n... (truncated)" : diff;
5669
+ const prompt = `Generate a GitHub pull request title and body for these changes.
5670
+
5671
+ Branch: ${branch} \u2192 ${baseBranch}
5672
+
5673
+ Commits:
5674
+ ${log}
5675
+
5676
+ Files changed:
5677
+ ${stat}
5678
+
5679
+ Diff:
5680
+ \`\`\`diff
5681
+ ${truncatedDiff}
5682
+ \`\`\`
5683
+
5684
+ Reply in this exact format (no other text):
5685
+ TITLE: <short PR title, under 72 chars>
5686
+ BODY:
5687
+ ## Summary
5688
+ <2-3 bullet points describing the changes>
5689
+
5690
+ ## Changes
5691
+ <brief list of what was changed and why>`;
5692
+ const tempMessages = [
5693
+ { role: "user", content: prompt }
5694
+ ];
5695
+ try {
5696
+ const response = await ctx.runPrompt(prompt, tempMessages);
5697
+ spinner.stop();
5698
+ const titleMatch = response.match(/TITLE:\s*(.+)/);
5699
+ const bodyMatch = response.match(/BODY:\s*([\s\S]+)/);
5700
+ const title = titleMatch?.[1]?.trim() || `${branch}: changes from Notch`;
5701
+ const body = bodyMatch?.[1]?.trim() || response;
5702
+ console.log(chalk18.white(`
5703
+ Title: ${title}
5704
+ `));
5705
+ console.log(chalk18.gray(` ${body.slice(0, 200)}${body.length > 200 ? "..." : ""}
5706
+ `));
5707
+ const rl = await import("readline");
5708
+ const confirm = rl.createInterface({ input: process.stdin, output: process.stdout });
5709
+ const answer = await new Promise((resolve2) => {
5710
+ confirm.question(chalk18.yellow(" Create PR? [Y/n] "), resolve2);
5711
+ });
5712
+ confirm.close();
5713
+ if (answer.trim().toLowerCase() === "n") {
5714
+ console.log(chalk18.gray(" PR cancelled.\n"));
5715
+ return;
5716
+ }
5717
+ try {
5718
+ const result = execFileSync5("gh", ["pr", "create", "--title", title, "--body", body, "--base", baseBranch], {
5719
+ cwd: ctx.cwd,
5720
+ encoding: "utf-8",
5721
+ timeout: 3e4
5722
+ });
5723
+ console.log(chalk18.green(` \u2713 ${result.trim()}
5724
+ `));
5725
+ } catch (err) {
5726
+ console.log(chalk18.red(` PR creation failed: ${err.message}
5727
+ `));
5728
+ }
5729
+ } catch (err) {
5730
+ spinner.fail(`Failed: ${err.message}`);
5731
+ }
5732
+ });
5733
+
5734
+ // src/commands/worktree.ts
5735
+ import { execSync as execSync12, execFileSync as execFileSync6 } from "child_process";
5736
+ import chalk19 from "chalk";
5737
+ function tryExec2(cmd, cwd) {
5738
+ try {
5739
+ return execSync12(cmd, { cwd, encoding: "utf-8", timeout: 15e3 }).trim();
5740
+ } catch {
5741
+ return null;
5742
+ }
5743
+ }
5744
+ registerCommand("/worktree", async (args, ctx) => {
5745
+ const parts = args.split(/\s+/);
5746
+ const subcommand = parts[0] || "list";
5747
+ switch (subcommand) {
5748
+ case "list":
5749
+ case "ls": {
5750
+ const result = tryExec2("git worktree list", ctx.cwd);
5751
+ if (!result) {
5752
+ console.log(chalk19.red(" Not a git repository.\n"));
5753
+ return;
5754
+ }
5755
+ console.log(chalk19.gray("\n Git Worktrees:\n"));
5756
+ for (const line of result.split("\n")) {
5757
+ const isCurrent = line.includes(ctx.cwd);
5758
+ console.log(isCurrent ? chalk19.green(` \u25CF ${line}`) : chalk19.gray(` ${line}`));
5759
+ }
5760
+ console.log("");
5761
+ return;
5762
+ }
5763
+ case "create":
5764
+ case "new": {
5765
+ const branchName = parts[1];
5766
+ if (!branchName) {
5767
+ console.log(chalk19.gray(" Usage: /worktree create <branch-name> [base-branch]\n"));
5768
+ return;
5769
+ }
5770
+ const baseBranch = parts[2] || "HEAD";
5771
+ const worktreePath = `../${branchName}`;
5772
+ try {
5773
+ execFileSync6("git", ["worktree", "add", worktreePath, "-b", branchName, baseBranch], {
5774
+ cwd: ctx.cwd,
5775
+ encoding: "utf-8",
5776
+ timeout: 15e3
5777
+ });
5778
+ const absPath = tryExec2(`cd "${worktreePath}" && pwd`, ctx.cwd);
5779
+ console.log(chalk19.green(` \u2713 Created worktree: ${branchName}`));
5780
+ console.log(chalk19.gray(` Path: ${absPath || worktreePath}`));
5781
+ console.log(chalk19.gray(` Base: ${baseBranch}`));
5782
+ console.log(chalk19.gray(`
5783
+ Run: /worktree switch ${branchName}
5784
+ `));
5785
+ } catch (err) {
5786
+ console.log(chalk19.red(` Failed: ${err.message}
5787
+ `));
5788
+ }
5789
+ return;
5790
+ }
5791
+ case "remove":
5792
+ case "rm": {
5793
+ const target = parts[1];
5794
+ if (!target) {
5795
+ console.log(chalk19.gray(" Usage: /worktree remove <branch-or-path>\n"));
5796
+ return;
5797
+ }
5798
+ try {
5799
+ execFileSync6("git", ["worktree", "remove", target, "--force"], {
5800
+ cwd: ctx.cwd,
5801
+ encoding: "utf-8",
5802
+ timeout: 15e3
5803
+ });
5804
+ try {
5805
+ execFileSync6("git", ["branch", "-D", target], { cwd: ctx.cwd, encoding: "utf-8", timeout: 5e3 });
5806
+ } catch {
5807
+ }
5808
+ console.log(chalk19.green(` \u2713 Removed worktree: ${target}
5809
+ `));
5810
+ } catch (err) {
5811
+ console.log(chalk19.red(` Failed: ${err.message}
5812
+ `));
5813
+ }
5814
+ return;
5815
+ }
5816
+ case "switch": {
5817
+ const target = parts[1];
5818
+ if (!target) {
5819
+ console.log(chalk19.gray(" Usage: /worktree switch <branch-name>\n"));
5820
+ return;
5821
+ }
5822
+ const list = tryExec2("git worktree list --porcelain", ctx.cwd);
5823
+ if (!list) {
5824
+ console.log(chalk19.red(" Not a git repository.\n"));
5825
+ return;
5826
+ }
5827
+ const worktrees = list.split("\n\n").filter((block) => block.trim().length > 0).map((block) => {
5828
+ const lines = block.split("\n");
5829
+ const wtPath = lines.find((l) => l.startsWith("worktree "))?.slice(9)?.trim();
5830
+ const branch = lines.find((l) => l.startsWith("branch "))?.slice(7)?.replace("refs/heads/", "")?.trim();
5831
+ return { path: wtPath, branch };
5832
+ }).filter((wt) => wt.path);
5833
+ const match = worktrees.find(
5834
+ (wt) => wt.branch === target || wt.path?.includes(target)
5835
+ );
5836
+ if (!match?.path) {
5837
+ console.log(chalk19.red(` Worktree not found: ${target}
5838
+ `));
5839
+ return;
5840
+ }
5841
+ console.log(chalk19.green(` \u2713 Switched context to: ${match.branch || target}`));
5842
+ console.log(chalk19.gray(` Path: ${match.path}`));
5843
+ console.log(chalk19.gray(`
5844
+ Note: This changes the working directory for Notch tools.
5845
+ `));
5846
+ return;
5847
+ }
5848
+ case "prune": {
5849
+ try {
5850
+ execSync12("git worktree prune", { cwd: ctx.cwd, encoding: "utf-8" });
5851
+ console.log(chalk19.green(" \u2713 Pruned stale worktrees.\n"));
5852
+ } catch (err) {
5853
+ console.log(chalk19.red(` Failed: ${err.message}
5854
+ `));
5855
+ }
5856
+ return;
5857
+ }
5858
+ default:
5859
+ console.log(chalk19.gray(" Usage: /worktree <list|create|remove|switch|prune>\n"));
5860
+ console.log(chalk19.gray(" Commands:"));
5861
+ console.log(chalk19.gray(" list \u2014 List all worktrees"));
5862
+ console.log(chalk19.gray(" create <branch> [base] \u2014 Create new worktree"));
5863
+ console.log(chalk19.gray(" remove <branch> \u2014 Remove a worktree"));
5864
+ console.log(chalk19.gray(" switch <branch> \u2014 Switch Notch context"));
5865
+ console.log(chalk19.gray(" prune \u2014 Clean up stale worktrees\n"));
5866
+ }
5867
+ });
5868
+
5869
+ // src/commands/tasks.ts
5870
+ import chalk20 from "chalk";
5871
+ registerCommand("/tasks", async (_args, _ctx) => {
5872
+ const tasks2 = getAllTasks();
5873
+ if (tasks2.length === 0) {
5874
+ console.log(chalk20.gray(" No tasks. The agent will create tasks automatically during complex work.\n"));
5875
+ return;
5876
+ }
5877
+ console.log(chalk20.gray("\n Task List:\n"));
5878
+ console.log(" " + formatTasksDisplay().split("\n").join("\n "));
5879
+ const completed = tasks2.filter((t) => t.status === "completed").length;
5880
+ const total = tasks2.length;
5881
+ const pct = total > 0 ? Math.round(completed / total * 100) : 0;
5882
+ console.log(chalk20.gray(`
5883
+ Progress: ${completed}/${total} (${pct}%)
5884
+ `));
5885
+ });
5886
+
5887
+ // src/commands/debug.ts
5888
+ import chalk21 from "chalk";
5889
+ var debugEnabled = false;
5890
+ registerCommand("/debug", async (args, ctx) => {
5891
+ if (args === "off") {
5892
+ debugEnabled = false;
5893
+ console.log(chalk21.gray(" Debug logging disabled.\n"));
5894
+ return;
5895
+ }
5896
+ if (args === "on" || !args) {
5897
+ debugEnabled = !debugEnabled;
5898
+ console.log(debugEnabled ? chalk21.yellow(" Debug logging enabled. Verbose output will be shown.\n") : chalk21.gray(" Debug logging disabled.\n"));
5899
+ return;
5900
+ }
5901
+ if (args === "status") {
5902
+ console.log(chalk21.gray("\n Debug Status:"));
5903
+ console.log(chalk21.gray(` Model: ${ctx.modelId}`));
5904
+ console.log(chalk21.gray(` Messages: ${ctx.messages.length}`));
5905
+ console.log(chalk21.gray(` CWD: ${ctx.cwd}`));
5906
+ console.log(chalk21.gray(` Debug: ${debugEnabled ? "on" : "off"}`));
5907
+ console.log(chalk21.gray(` Last response: ${ctx.lastResponse ? ctx.lastResponse.length + " chars" : "none"}`));
5908
+ const charCount = ctx.messages.reduce((sum, m) => {
5909
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
5910
+ return sum + content.length;
5911
+ }, 0);
5912
+ console.log(chalk21.gray(` Estimated tokens: ~${Math.round(charCount / 4)}`));
5913
+ console.log("");
5914
+ return;
5915
+ }
5916
+ console.log(chalk21.gray(" Usage: /debug [on|off|status]\n"));
5917
+ });
5918
+
5919
+ // src/commands/rewind.ts
5920
+ import chalk22 from "chalk";
5921
+ registerCommand("/rewind", async (args, ctx) => {
5922
+ if (ctx.messages.length === 0) {
5923
+ console.log(chalk22.gray(" No conversation to rewind.\n"));
5924
+ return;
5925
+ }
5926
+ const userTurns = [];
5927
+ for (let i = 0; i < ctx.messages.length; i++) {
5928
+ const msg = ctx.messages[i];
5929
+ if (msg.role === "user") {
5930
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5931
+ const preview = content.slice(0, 60).replace(/\n/g, " ");
5932
+ userTurns.push({ index: i, preview });
5933
+ }
5934
+ }
5935
+ if (userTurns.length === 0) {
5936
+ console.log(chalk22.gray(" No turns to rewind to.\n"));
5937
+ return;
5938
+ }
5939
+ if (args) {
5940
+ const turnNum = parseInt(args, 10);
5941
+ if (isNaN(turnNum) || turnNum < 1 || turnNum > userTurns.length) {
5942
+ console.log(chalk22.red(` Invalid turn number: ${args}`));
5943
+ console.log(chalk22.gray(` Valid range: 1-${userTurns.length}
5944
+ `));
5945
+ return;
5946
+ }
5947
+ const target = userTurns[turnNum - 1];
5948
+ const kept = ctx.messages.slice(0, target.index);
5949
+ ctx.messages.length = 0;
5950
+ ctx.messages.push(...kept);
5951
+ console.log(chalk22.green(` \u2713 Rewound to before turn ${turnNum}`));
5952
+ console.log(chalk22.gray(` "${target.preview}"`));
5953
+ console.log(chalk22.gray(` ${ctx.messages.length} messages remaining.
5954
+ `));
5955
+ return;
5956
+ }
5957
+ console.log(chalk22.gray("\n Conversation turns:\n"));
5958
+ for (let i = 0; i < userTurns.length; i++) {
5959
+ console.log(chalk22.gray(` ${i + 1}. ${userTurns[i].preview}${userTurns[i].preview.length >= 60 ? "..." : ""}`));
5960
+ }
5961
+ console.log(chalk22.gray(`
5962
+ Rewind to a turn: /rewind <turn-number>
5963
+ `));
5964
+ });
5965
+
5966
+ // src/commands/insights.ts
5967
+ import chalk23 from "chalk";
5968
+ registerCommand("/insights", async (_args, ctx) => {
5969
+ if (ctx.messages.length === 0) {
5970
+ console.log(chalk23.gray(" No conversation data to analyze.\n"));
5971
+ return;
5972
+ }
5973
+ let userMsgs = 0;
5974
+ let assistantMsgs = 0;
5975
+ let toolCalls = 0;
5976
+ const toolUsage = /* @__PURE__ */ new Map();
5977
+ const filesReferenced = /* @__PURE__ */ new Set();
5978
+ let totalChars = 0;
5979
+ for (const msg of ctx.messages) {
5980
+ if (msg.role === "user") {
5981
+ userMsgs++;
5982
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5983
+ totalChars += content.length;
5984
+ } else if (msg.role === "assistant") {
5985
+ assistantMsgs++;
5986
+ if (Array.isArray(msg.content)) {
5987
+ for (const part of msg.content) {
5988
+ if (typeof part === "object" && "type" in part && part.type === "tool-call") {
5989
+ toolCalls++;
5990
+ const name = part.toolName || "unknown";
5991
+ toolUsage.set(name, (toolUsage.get(name) || 0) + 1);
5992
+ const args = part.args;
5993
+ if (args?.path) filesReferenced.add(String(args.path));
5994
+ if (args?.file) filesReferenced.add(String(args.file));
5995
+ }
5996
+ }
5997
+ }
5998
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5999
+ totalChars += content.length;
6000
+ }
6001
+ }
6002
+ const estimatedTokens = Math.round(totalChars / 4);
6003
+ console.log(chalk23.gray("\n \u2550\u2550\u2550 Session Insights \u2550\u2550\u2550\n"));
6004
+ console.log(chalk23.gray(" Conversation:"));
6005
+ console.log(chalk23.gray(` ${userMsgs} user turns, ${assistantMsgs} assistant responses`));
6006
+ console.log(chalk23.gray(` ${ctx.messages.length} total messages`));
6007
+ console.log(chalk23.gray(` ~${estimatedTokens.toLocaleString()} tokens estimated`));
6008
+ console.log("");
6009
+ if (toolCalls > 0) {
6010
+ console.log(chalk23.gray(" Tool Usage:"));
6011
+ const sorted = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
6012
+ for (const [name, count] of sorted) {
6013
+ const bar = "\u2588".repeat(Math.min(count, 20));
6014
+ console.log(chalk23.gray(` ${name.padEnd(14)} ${chalk23.cyan(bar)} ${count}`));
6015
+ }
6016
+ console.log(chalk23.gray(` ${"Total".padEnd(14)} ${toolCalls} calls`));
6017
+ console.log("");
6018
+ }
6019
+ if (filesReferenced.size > 0) {
6020
+ console.log(chalk23.gray(" Files Referenced:"));
6021
+ const files = [...filesReferenced].slice(0, 15);
6022
+ for (const f of files) {
6023
+ console.log(chalk23.gray(` \u2022 ${f}`));
6024
+ }
6025
+ if (filesReferenced.size > 15) {
6026
+ console.log(chalk23.gray(` ... and ${filesReferenced.size - 15} more`));
6027
+ }
6028
+ console.log("");
6029
+ }
6030
+ const avgToolsPerTurn = userMsgs > 0 ? (toolCalls / userMsgs).toFixed(1) : "0";
6031
+ console.log(chalk23.gray(" Efficiency:"));
6032
+ console.log(chalk23.gray(` Avg tools per turn: ${avgToolsPerTurn}`));
6033
+ console.log(chalk23.gray(` Files per session: ${filesReferenced.size}`));
6034
+ console.log(chalk23.gray(` Tool diversity: ${toolUsage.size} unique tools used`));
6035
+ console.log("");
6036
+ });
6037
+
6038
+ // src/commands/rename.ts
6039
+ import chalk24 from "chalk";
6040
+ var sessionName = "";
6041
+ function getSessionName() {
6042
+ return sessionName;
6043
+ }
6044
+ registerCommand("/rename", async (args, _ctx) => {
6045
+ if (!args) {
6046
+ if (sessionName) {
6047
+ console.log(chalk24.gray(` Current session name: ${chalk24.white(sessionName)}
6048
+ `));
6049
+ } else {
6050
+ console.log(chalk24.gray(" Usage: /rename <name>\n"));
6051
+ }
6052
+ return;
6053
+ }
6054
+ sessionName = args.trim();
6055
+ console.log(chalk24.green(` \u2713 Session named: "${sessionName}"
6056
+ `));
6057
+ });
6058
+
6059
+ // src/commands/sandbox.ts
6060
+ import chalk25 from "chalk";
6061
+ var sandboxEnabled = false;
6062
+ function isSandboxEnabled() {
6063
+ return sandboxEnabled;
6064
+ }
6065
+ registerCommand("/sandbox", async (args, _ctx) => {
6066
+ if (args === "on") {
6067
+ sandboxEnabled = true;
6068
+ } else if (args === "off") {
6069
+ sandboxEnabled = false;
6070
+ } else {
6071
+ sandboxEnabled = !sandboxEnabled;
6072
+ }
6073
+ if (sandboxEnabled) {
6074
+ console.log(chalk25.yellow(" \u26A0 Sandbox mode ON \u2014 write/edit/shell tools will run in dry-run mode."));
6075
+ console.log(chalk25.gray(" Only read, grep, glob, and web-fetch will execute normally.\n"));
6076
+ } else {
6077
+ console.log(chalk25.green(" \u2713 Sandbox mode OFF \u2014 all tools enabled.\n"));
6078
+ }
6079
+ });
6080
+
3940
6081
  // src/ui/completions.ts
3941
- import fs17 from "fs";
3942
- import path18 from "path";
6082
+ import fs22 from "fs";
6083
+ import path24 from "path";
3943
6084
  var COMMANDS = [
3944
6085
  "/help",
3945
6086
  "/quit",
@@ -3969,7 +6110,39 @@ var COMMANDS = [
3969
6110
  "/mascot",
3970
6111
  "/cost",
3971
6112
  "/branch",
3972
- "/branches"
6113
+ "/branches",
6114
+ // Utility commands
6115
+ "/doctor",
6116
+ "/copy",
6117
+ "/btw",
6118
+ "/security-review",
6119
+ "/loop",
6120
+ "/loop stop",
6121
+ "/batch",
6122
+ "/plugin",
6123
+ "/plugin list",
6124
+ "/plugin install",
6125
+ "/plugin remove",
6126
+ // Git workflow
6127
+ "/commit",
6128
+ "/pr",
6129
+ // Worktree
6130
+ "/worktree",
6131
+ "/worktree list",
6132
+ "/worktree create",
6133
+ "/worktree remove",
6134
+ "/worktree switch",
6135
+ // Task management
6136
+ "/tasks",
6137
+ // UX commands
6138
+ "/debug",
6139
+ "/debug on",
6140
+ "/debug off",
6141
+ "/debug status",
6142
+ "/rewind",
6143
+ "/insights",
6144
+ "/rename",
6145
+ "/sandbox"
3973
6146
  ];
3974
6147
  function buildCompleter(cwd) {
3975
6148
  return function completer(line) {
@@ -4000,15 +6173,15 @@ function buildCompleter(cwd) {
4000
6173
  }
4001
6174
  function completeFilePath(partial, cwd) {
4002
6175
  try {
4003
- const dir = partial.includes("/") ? path18.resolve(cwd, path18.dirname(partial)) : cwd;
4004
- const prefix = partial.includes("/") ? path18.basename(partial) : partial;
4005
- const entries = fs17.readdirSync(dir, { withFileTypes: true });
6176
+ const dir = partial.includes("/") ? path24.resolve(cwd, path24.dirname(partial)) : cwd;
6177
+ const prefix = partial.includes("/") ? path24.basename(partial) : partial;
6178
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
4006
6179
  const matches = [];
4007
6180
  for (const entry of entries) {
4008
6181
  if (entry.name.startsWith(".")) continue;
4009
6182
  if (entry.name === "node_modules" || entry.name === ".git") continue;
4010
6183
  if (entry.name.startsWith(prefix)) {
4011
- const relative = partial.includes("/") ? path18.dirname(partial) + "/" + entry.name : entry.name;
6184
+ const relative = partial.includes("/") ? path24.dirname(partial) + "/" + entry.name : entry.name;
4012
6185
  if (entry.isDirectory()) {
4013
6186
  matches.push(relative + "/");
4014
6187
  } else {
@@ -4023,7 +6196,7 @@ function completeFilePath(partial, cwd) {
4023
6196
  }
4024
6197
 
4025
6198
  // src/index.ts
4026
- import fs18 from "fs/promises";
6199
+ import fs23 from "fs/promises";
4027
6200
  import { createRequire } from "module";
4028
6201
  var _require = createRequire(import.meta.url);
4029
6202
  var VERSION = _require("../package.json").version;
@@ -4046,7 +6219,7 @@ function printModelTable(activeModel) {
4046
6219
  `));
4047
6220
  }
4048
6221
  function printHelp() {
4049
- console.log(chalk9.gray(`
6222
+ console.log(chalk26.gray(`
4050
6223
  Commands:
4051
6224
  /model \u2014 Show available models
4052
6225
  /model <name> \u2014 Switch model (e.g., /model pyre)
@@ -4080,6 +6253,30 @@ function printHelp() {
4080
6253
  /ralph status \u2014 Show plan progress
4081
6254
  /ralph clear \u2014 Discard plan
4082
6255
 
6256
+ Git Workflow:
6257
+ /commit [msg] \u2014 Smart commit (AI-generated message)
6258
+ /pr [title] \u2014 Create PR with AI-generated summary
6259
+ /worktree <cmd> \u2014 Manage git worktrees (create/switch/remove)
6260
+
6261
+ Utilities:
6262
+ /doctor \u2014 Diagnose installation health
6263
+ /copy \u2014 Copy last response to clipboard
6264
+ /btw <question> \u2014 Side question (won't affect context)
6265
+ /security-review \u2014 Review recent changes for vulnerabilities
6266
+ /loop <int> <cmd> \u2014 Run a command on interval (e.g. /loop 5m /status)
6267
+ /batch <task> \u2014 Apply a task across multiple files in parallel
6268
+ /tasks \u2014 Show agent task list
6269
+ /insights \u2014 Session analytics and tool usage
6270
+ /rewind [turn] \u2014 Rewind conversation to a previous turn
6271
+ /rename <name> \u2014 Name this session
6272
+ /debug [on|off] \u2014 Toggle debug logging
6273
+ /sandbox \u2014 Toggle sandbox mode (read-only)
6274
+
6275
+ Plugins:
6276
+ /plugin list \u2014 List installed plugins
6277
+ /plugin install <n> \u2014 Install a plugin (npm package or git URL)
6278
+ /plugin remove <n> \u2014 Remove a plugin
6279
+
4083
6280
  Other:
4084
6281
  /branch \u2014 Fork conversation at this point
4085
6282
  /branches \u2014 List conversation branches
@@ -4115,17 +6312,17 @@ async function readStdin() {
4115
6312
  }
4116
6313
  async function main() {
4117
6314
  if (promptArgs[0] === "login") {
4118
- const spinner = ora("Opening browser...").start();
6315
+ const spinner = ora4("Opening browser...").start();
4119
6316
  try {
4120
6317
  spinner.stop();
4121
6318
  const creds = await login();
4122
- console.log(chalk9.green(`
6319
+ console.log(chalk26.green(`
4123
6320
  \u2713 Signed in as ${creds.email}`));
4124
- console.log(chalk9.gray(` API key stored in ${(await import("./auth-S3FIB42I.js")).getCredentialsPath()}
6321
+ console.log(chalk26.gray(` API key stored in ${(await import("./auth-S3FIB42I.js")).getCredentialsPath()}
4125
6322
  `));
4126
6323
  } catch (err) {
4127
6324
  spinner.stop();
4128
- console.error(chalk9.red(`
6325
+ console.error(chalk26.red(`
4129
6326
  Login failed: ${err.message}
4130
6327
  `));
4131
6328
  process.exit(1);
@@ -4135,10 +6332,10 @@ async function main() {
4135
6332
  if (promptArgs[0] === "logout") {
4136
6333
  const creds = await loadCredentials();
4137
6334
  if (!creds) {
4138
- console.log(chalk9.gray("\n Not signed in.\n"));
6335
+ console.log(chalk26.gray("\n Not signed in.\n"));
4139
6336
  } else {
4140
6337
  await clearCredentials();
4141
- console.log(chalk9.green(`
6338
+ console.log(chalk26.green(`
4142
6339
  \u2713 Signed out (${creds.email})
4143
6340
  `));
4144
6341
  }
@@ -4147,13 +6344,13 @@ async function main() {
4147
6344
  if (promptArgs[0] === "whoami") {
4148
6345
  const creds = await loadCredentials();
4149
6346
  if (!creds) {
4150
- console.log(chalk9.gray("\n Not signed in. Run: notch login\n"));
6347
+ console.log(chalk26.gray("\n Not signed in. Run: notch login\n"));
4151
6348
  } else {
4152
6349
  const keyPreview = `${creds.token.slice(0, 12)}...`;
4153
- console.log(chalk9.gray(`
4154
- Signed in as ${chalk9.white(creds.email)}`));
4155
- console.log(chalk9.gray(` Key: ${keyPreview}`));
4156
- console.log(chalk9.gray(` Since: ${new Date(creds.createdAt).toLocaleDateString()}
6350
+ console.log(chalk26.gray(`
6351
+ Signed in as ${chalk26.white(creds.email)}`));
6352
+ console.log(chalk26.gray(` Key: ${keyPreview}`));
6353
+ console.log(chalk26.gray(` Since: ${new Date(creds.createdAt).toLocaleDateString()}
4157
6354
  `));
4158
6355
  }
4159
6356
  return;
@@ -4177,8 +6374,8 @@ async function main() {
4177
6374
  const config = await loadConfig(configOverrides);
4178
6375
  if (opts.model) {
4179
6376
  if (!isValidModel(opts.model)) {
4180
- console.error(chalk9.red(` Unknown model: ${opts.model}`));
4181
- console.error(chalk9.gray(` Available: ${modelChoices}`));
6377
+ console.error(chalk26.red(` Unknown model: ${opts.model}`));
6378
+ console.error(chalk26.gray(` Available: ${modelChoices}`));
4182
6379
  process.exit(1);
4183
6380
  }
4184
6381
  config.models.chat.model = opts.model;
@@ -4217,11 +6414,11 @@ async function main() {
4217
6414
  const updateMsg = await checkForUpdates(VERSION);
4218
6415
  if (updateMsg) console.log(updateMsg);
4219
6416
  const hookTrustPrompt = async (commands) => {
4220
- console.warn(chalk9.yellow("\n\u26A0 This project contains hooks in .notch.json that will run shell commands:"));
4221
- commands.forEach((cmd) => console.warn(chalk9.gray(` \u2022 ${cmd}`)));
6417
+ console.warn(chalk26.yellow("\n\u26A0 This project contains hooks in .notch.json that will run shell commands:"));
6418
+ commands.forEach((cmd) => console.warn(chalk26.gray(` \u2022 ${cmd}`)));
4222
6419
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
4223
6420
  return new Promise((resolve2) => {
4224
- rl2.question(chalk9.yellow("\nAllow these hooks for this project? [y/N] "), (answer) => {
6421
+ rl2.question(chalk26.yellow("\nAllow these hooks for this project? [y/N] "), (answer) => {
4225
6422
  rl2.close();
4226
6423
  resolve2(answer.trim().toLowerCase() === "y");
4227
6424
  });
@@ -4233,7 +6430,7 @@ async function main() {
4233
6430
  ]);
4234
6431
  let repoMapStr = "";
4235
6432
  if (config.useRepoMap) {
4236
- const spinner = ora("Mapping repository...").start();
6433
+ const spinner = ora4("Mapping repository...").start();
4237
6434
  try {
4238
6435
  const repoMap = await buildRepoMap(config.projectRoot);
4239
6436
  repoMapStr = condensedRepoMap(repoMap);
@@ -4260,20 +6457,30 @@ ${repoMapStr}` : ""
4260
6457
  const costTracker = new CostTracker();
4261
6458
  const mcpClients = [];
4262
6459
  try {
4263
- const configRaw = await fs18.readFile(nodePath.resolve(config.projectRoot, ".notch.json"), "utf-8").catch(() => "{}");
6460
+ const configRaw = await fs23.readFile(nodePath.resolve(config.projectRoot, ".notch.json"), "utf-8").catch(() => "{}");
4264
6461
  const mcpConfigs = parseMCPConfig(JSON.parse(configRaw));
4265
6462
  for (const [name, mcpConfig] of Object.entries(mcpConfigs)) {
4266
6463
  try {
4267
6464
  const client = new MCPClient(mcpConfig, name);
4268
6465
  await client.connect();
4269
6466
  mcpClients.push(client);
4270
- console.log(chalk9.green(` MCP: Connected to ${name} (${client.tools.length} tools)`));
6467
+ console.log(chalk26.green(` MCP: Connected to ${name} (${client.tools.length} tools)`));
4271
6468
  } catch (err) {
4272
- console.log(chalk9.yellow(` MCP: Could not connect to ${name}: ${err.message}`));
6469
+ console.log(chalk26.yellow(` MCP: Could not connect to ${name}: ${err.message}`));
4273
6470
  }
4274
6471
  }
4275
6472
  } catch {
4276
6473
  }
6474
+ try {
6475
+ const pluginCount = await pluginManager.init(config.projectRoot, (msg) => {
6476
+ console.log(chalk26.gray(` ${msg}`));
6477
+ });
6478
+ if (pluginCount > 0) {
6479
+ console.log(chalk26.green(` Plugins: ${pluginCount} loaded`));
6480
+ }
6481
+ } catch (err) {
6482
+ console.log(chalk26.yellow(` Plugin init failed: ${err.message}`));
6483
+ }
4277
6484
  const toolCtx = {
4278
6485
  cwd: config.projectRoot,
4279
6486
  requireConfirm: config.permissionMode !== "trust" && !config.autoConfirm,
@@ -4286,7 +6493,7 @@ ${repoMapStr}` : ""
4286
6493
  });
4287
6494
  });
4288
6495
  },
4289
- log: (msg) => console.log(chalk9.gray(` ${msg}`)),
6496
+ log: (msg) => console.log(chalk26.gray(` ${msg}`)),
4290
6497
  checkPermission: config.permissionMode === "trust" ? () => "allow" : (toolName, args) => checkPermission(permissions, toolName, args),
4291
6498
  runHook: async (event, ctx) => {
4292
6499
  if (!config.enableHooks || hookConfig.hooks.length === 0) return;
@@ -4296,22 +6503,30 @@ ${repoMapStr}` : ""
4296
6503
  });
4297
6504
  for (const r of results) {
4298
6505
  if (!r.ok) {
4299
- console.log(chalk9.yellow(` Hook failed (${r.hook.event}): ${r.error}`));
6506
+ console.log(chalk26.yellow(` Hook failed (${r.hook.event}): ${r.error}`));
4300
6507
  }
4301
6508
  }
4302
6509
  }
4303
6510
  };
4304
6511
  await toolCtx.runHook?.("session-start", {});
6512
+ const stopFileWatcher = startFileWatcher(config.projectRoot, hookConfig, (event, results) => {
6513
+ for (const r of results) {
6514
+ if (!r.ok) {
6515
+ console.log(chalk26.yellow(` Hook failed (${event}): ${r.error}`));
6516
+ }
6517
+ }
6518
+ });
4305
6519
  const messages = [];
6520
+ let lastAssistantResponse = "";
4306
6521
  if (opts.resume || opts.session) {
4307
6522
  const session = opts.session ? await loadSession(opts.session) : await loadLastSession(config.projectRoot);
4308
6523
  if (session) {
4309
6524
  messages.push(...session.messages);
4310
6525
  sessionId = session.meta.id;
4311
- console.log(chalk9.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
6526
+ console.log(chalk26.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
4312
6527
  `));
4313
6528
  } else {
4314
- console.log(chalk9.gray(" No session to resume.\n"));
6529
+ console.log(chalk26.gray(" No session to resume.\n"));
4315
6530
  }
4316
6531
  }
4317
6532
  const pipedInput = await readStdin();
@@ -4332,13 +6547,13 @@ Analyze the above input.`;
4332
6547
  const refContext = formatReferences(references);
4333
6548
  const finalPrompt = refContext + cleanInput;
4334
6549
  messages.push({ role: "user", content: finalPrompt });
4335
- console.log(chalk9.cyan(`> ${oneShot || "(piped input)"}
6550
+ console.log(chalk26.cyan(`> ${oneShot || "(piped input)"}
4336
6551
  `));
4337
6552
  if (references.length > 0) {
4338
- console.log(chalk9.gray(` Injected ${references.length} reference(s)
6553
+ console.log(chalk26.gray(` Injected ${references.length} reference(s)
4339
6554
  `));
4340
6555
  }
4341
- const spinner = ora("Thinking...").start();
6556
+ const spinner = ora4("Thinking...").start();
4342
6557
  try {
4343
6558
  const response = await withRetry(
4344
6559
  () => runAgentLoop(messages, {
@@ -4354,13 +6569,13 @@ Analyze the above input.`;
4354
6569
  onToolCall: (name, args) => {
4355
6570
  if (spinner.isSpinning) spinner.stop();
4356
6571
  const argSummary = Object.entries(args).map(([k, v]) => `${k}=${String(v).slice(0, 60)}`).join(", ");
4357
- console.log(chalk9.gray(`
6572
+ console.log(chalk26.gray(`
4358
6573
  \u2192 ${name}(${argSummary})`));
4359
6574
  },
4360
6575
  onToolResult: (_name, result, isError) => {
4361
6576
  const preview = result.slice(0, 100).replace(/\n/g, " ");
4362
- const icon = isError ? chalk9.red("\u2717") : chalk9.green("\u2713");
4363
- console.log(chalk9.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
6577
+ const icon = isError ? chalk26.red("\u2717") : chalk26.green("\u2713");
6578
+ console.log(chalk26.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
4364
6579
  }
4365
6580
  })
4366
6581
  );
@@ -4385,7 +6600,7 @@ Analyze the above input.`;
4385
6600
  const savedPlan = await loadPlan(config.projectRoot);
4386
6601
  if (savedPlan) {
4387
6602
  ralphPlan = savedPlan;
4388
- console.log(chalk9.gray(` Ralph plan loaded (${savedPlan.tasks.length} tasks)
6603
+ console.log(chalk26.gray(` Ralph plan loaded (${savedPlan.tasks.length} tasks)
4389
6604
  `));
4390
6605
  }
4391
6606
  } catch {
@@ -4397,7 +6612,7 @@ Analyze the above input.`;
4397
6612
  prompt: theme().prompt("notch> "),
4398
6613
  completer: (line) => completer(line)
4399
6614
  });
4400
- console.log(chalk9.gray(" Type your request, or /help for commands.\n"));
6615
+ console.log(chalk26.gray(" Type your request, or /help for commands.\n"));
4401
6616
  rl.prompt();
4402
6617
  rl.on("line", async (line) => {
4403
6618
  const input = line.trim();
@@ -4412,8 +6627,9 @@ Analyze the above input.`;
4412
6627
  }
4413
6628
  if (messages.length > 0) {
4414
6629
  try {
6630
+ const name = getSessionName();
4415
6631
  const id = await saveSession(config.projectRoot, messages, { model: activeModelId });
4416
- console.log(chalk9.gray(` Session saved: ${id}`));
6632
+ console.log(chalk26.gray(` Session saved: ${id}${name ? ` (${name})` : ""}`));
4417
6633
  } catch {
4418
6634
  }
4419
6635
  }
@@ -4423,13 +6639,16 @@ Analyze the above input.`;
4423
6639
  } catch {
4424
6640
  }
4425
6641
  }
6642
+ stopActiveLoop();
6643
+ stopFileWatcher();
6644
+ await pluginManager.shutdown();
4426
6645
  await toolCtx.runHook?.("session-end", {});
4427
- console.log(chalk9.gray("\n Goodbye!\n"));
6646
+ console.log(chalk26.gray("\n Goodbye!\n"));
4428
6647
  process.exit(0);
4429
6648
  }
4430
6649
  if (input === "/clear") {
4431
6650
  messages.length = 0;
4432
- console.log(chalk9.gray(" Conversation cleared.\n"));
6651
+ console.log(chalk26.gray(" Conversation cleared.\n"));
4433
6652
  rl.prompt();
4434
6653
  return;
4435
6654
  }
@@ -4449,8 +6668,8 @@ Analyze the above input.`;
4449
6668
  newModel = `notch-${newModel}`;
4450
6669
  }
4451
6670
  if (!isValidModel(newModel)) {
4452
- console.log(chalk9.red(` Unknown model: ${newModel}`));
4453
- console.log(chalk9.gray(` Available: ${modelChoices}`));
6671
+ console.log(chalk26.red(` Unknown model: ${newModel}`));
6672
+ console.log(chalk26.gray(` Available: ${modelChoices}`));
4454
6673
  rl.prompt();
4455
6674
  return;
4456
6675
  }
@@ -4458,35 +6677,35 @@ Analyze the above input.`;
4458
6677
  config.models.chat.model = activeModelId;
4459
6678
  model = resolveModel(config.models.chat);
4460
6679
  const switchedInfo = MODEL_CATALOG[activeModelId];
4461
- console.log(chalk9.green(` Switched to ${switchedInfo.label} (${switchedInfo.id})
6680
+ console.log(chalk26.green(` Switched to ${switchedInfo.label} (${switchedInfo.id})
4462
6681
  `));
4463
6682
  rl.prompt();
4464
6683
  return;
4465
6684
  }
4466
6685
  if (input === "/status") {
4467
- const statusSpinner = ora("Checking Notch API...").start();
6686
+ const statusSpinner = ora4("Checking Notch API...").start();
4468
6687
  const check = await validateConfig(config.models.chat);
4469
6688
  if (check.ok) {
4470
6689
  const statusInfo = MODEL_CATALOG[activeModelId];
4471
6690
  statusSpinner.succeed(`${statusInfo.label} (${activeModelId}) is reachable`);
4472
6691
  } else {
4473
6692
  statusSpinner.fail(check.error ?? "API unreachable");
4474
- console.log(chalk9.gray(" Tip: Set NOTCH_API_KEY or use --api-key, and verify your Modal endpoint is running.\n"));
6693
+ console.log(chalk26.gray(" Tip: Set NOTCH_API_KEY or use --api-key, and verify your Modal endpoint is running.\n"));
4475
6694
  }
4476
6695
  rl.prompt();
4477
6696
  return;
4478
6697
  }
4479
6698
  if (input === "/undo") {
4480
6699
  if (checkpoints.undoCount === 0) {
4481
- console.log(chalk9.gray(" Nothing to undo.\n"));
6700
+ console.log(chalk26.gray(" Nothing to undo.\n"));
4482
6701
  rl.prompt();
4483
6702
  return;
4484
6703
  }
4485
6704
  const undone = await checkpoints.undo();
4486
6705
  if (undone) {
4487
6706
  const fileCount = undone.files.length;
4488
- console.log(chalk9.yellow(` Undid "${undone.description}" (${fileCount} file${fileCount !== 1 ? "s" : ""} restored)`));
4489
- console.log(chalk9.gray(` ${checkpoints.undoCount} checkpoint${checkpoints.undoCount !== 1 ? "s" : ""} remaining
6707
+ console.log(chalk26.yellow(` Undid "${undone.description}" (${fileCount} file${fileCount !== 1 ? "s" : ""} restored)`));
6708
+ console.log(chalk26.gray(` ${checkpoints.undoCount} checkpoint${checkpoints.undoCount !== 1 ? "s" : ""} remaining
4490
6709
  `));
4491
6710
  }
4492
6711
  rl.prompt();
@@ -4494,7 +6713,7 @@ Analyze the above input.`;
4494
6713
  }
4495
6714
  if (input === "/usage") {
4496
6715
  if (usage.turnCount === 0) {
4497
- console.log(chalk9.gray(" No usage yet.\n"));
6716
+ console.log(chalk26.gray(" No usage yet.\n"));
4498
6717
  } else {
4499
6718
  console.log(usage.formatSession());
4500
6719
  const currentTokens = estimateTokens(messages);
@@ -4507,7 +6726,7 @@ Analyze the above input.`;
4507
6726
  }
4508
6727
  if (input === "/cost") {
4509
6728
  if (costTracker.totalCost === 0) {
4510
- console.log(chalk9.gray(" No cost data yet.\n"));
6729
+ console.log(chalk26.gray(" No cost data yet.\n"));
4511
6730
  } else {
4512
6731
  console.log(costTracker.formatTotal());
4513
6732
  console.log(costTracker.formatByModel());
@@ -4522,7 +6741,7 @@ Analyze the above input.`;
4522
6741
  const compressed = await autoCompress2(messages, model, MODEL_CATALOG[activeModelId].contextWindow);
4523
6742
  messages.length = 0;
4524
6743
  messages.push(...compressed);
4525
- console.log(chalk9.green(` Compressed: ${before} messages \u2192 ${messages.length} messages`));
6744
+ console.log(chalk26.green(` Compressed: ${before} messages \u2192 ${messages.length} messages`));
4526
6745
  console.log("");
4527
6746
  rl.prompt();
4528
6747
  return;
@@ -4530,10 +6749,10 @@ Analyze the above input.`;
4530
6749
  if (input === "/diff") {
4531
6750
  const diffs = checkpoints.allDiffs();
4532
6751
  if (diffs.length === 0) {
4533
- console.log(chalk9.gray(" No file changes this session.\n"));
6752
+ console.log(chalk26.gray(" No file changes this session.\n"));
4534
6753
  } else {
4535
6754
  for (const df of diffs) {
4536
- console.log(chalk9.cyan(` ${df.path}:`));
6755
+ console.log(chalk26.cyan(` ${df.path}:`));
4537
6756
  console.log(unifiedDiff(df.before, df.after, df.path));
4538
6757
  console.log("");
4539
6758
  }
@@ -4549,10 +6768,10 @@ Analyze the above input.`;
4549
6768
  projectRoot: config.projectRoot,
4550
6769
  outputPath: exportPath
4551
6770
  });
4552
- console.log(chalk9.green(` Exported to ${ePath}
6771
+ console.log(chalk26.green(` Exported to ${ePath}
4553
6772
  `));
4554
6773
  } catch (err) {
4555
- console.log(chalk9.red(` Export failed: ${err.message}
6774
+ console.log(chalk26.red(` Export failed: ${err.message}
4556
6775
  `));
4557
6776
  }
4558
6777
  rl.prompt();
@@ -4562,10 +6781,10 @@ Analyze the above input.`;
4562
6781
  try {
4563
6782
  const id = await saveSession(config.projectRoot, messages, { model: activeModelId });
4564
6783
  sessionId = id;
4565
- console.log(chalk9.green(` Session saved: ${id}
6784
+ console.log(chalk26.green(` Session saved: ${id}
4566
6785
  `));
4567
6786
  } catch (err) {
4568
- console.log(chalk9.red(` Save failed: ${err.message}
6787
+ console.log(chalk26.red(` Save failed: ${err.message}
4569
6788
  `));
4570
6789
  }
4571
6790
  rl.prompt();
@@ -4575,16 +6794,16 @@ Analyze the above input.`;
4575
6794
  try {
4576
6795
  const sessions = await listSessions(config.projectRoot);
4577
6796
  if (sessions.length === 0) {
4578
- console.log(chalk9.gray(" No saved sessions.\n"));
6797
+ console.log(chalk26.gray(" No saved sessions.\n"));
4579
6798
  } else {
4580
- console.log(chalk9.gray("\n Saved sessions:\n"));
6799
+ console.log(chalk26.gray("\n Saved sessions:\n"));
4581
6800
  for (const s of sessions.slice(0, 10)) {
4582
- console.log(chalk9.gray(` ${s.id} ${s.turns} turns ${s.date} ${s.model}`));
6801
+ console.log(chalk26.gray(` ${s.id} ${s.turns} turns ${s.date} ${s.model}`));
4583
6802
  }
4584
6803
  console.log("");
4585
6804
  }
4586
6805
  } catch (err) {
4587
- console.log(chalk9.red(` Error listing sessions: ${err.message}
6806
+ console.log(chalk26.red(` Error listing sessions: ${err.message}
4588
6807
  `));
4589
6808
  }
4590
6809
  rl.prompt();
@@ -4602,18 +6821,18 @@ Analyze the above input.`;
4602
6821
  const savedPlan = await loadPlan(config.projectRoot);
4603
6822
  if (savedPlan) {
4604
6823
  ralphPlan = savedPlan;
4605
- console.log(chalk9.green(` Ralph plan restored (${savedPlan.tasks.length} tasks)
6824
+ console.log(chalk26.green(` Ralph plan restored (${savedPlan.tasks.length} tasks)
4606
6825
  `));
4607
6826
  }
4608
6827
  } catch {
4609
6828
  }
4610
- console.log(chalk9.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
6829
+ console.log(chalk26.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
4611
6830
  `));
4612
6831
  } else {
4613
- console.log(chalk9.gray(" No session found.\n"));
6832
+ console.log(chalk26.gray(" No session found.\n"));
4614
6833
  }
4615
6834
  } catch (err) {
4616
- console.log(chalk9.red(` Resume failed: ${err.message}
6835
+ console.log(chalk26.red(` Resume failed: ${err.message}
4617
6836
  `));
4618
6837
  }
4619
6838
  rl.prompt();
@@ -4622,7 +6841,7 @@ Analyze the above input.`;
4622
6841
  if (input.startsWith("/search ")) {
4623
6842
  const query = input.replace("/search ", "").trim().toLowerCase();
4624
6843
  if (!query) {
4625
- console.log(chalk9.gray(" Usage: /search <query>\n"));
6844
+ console.log(chalk26.gray(" Usage: /search <query>\n"));
4626
6845
  rl.prompt();
4627
6846
  return;
4628
6847
  }
@@ -4639,14 +6858,14 @@ Analyze the above input.`;
4639
6858
  }
4640
6859
  }
4641
6860
  if (matches.length === 0) {
4642
- console.log(chalk9.gray(` No matches for "${query}"
6861
+ console.log(chalk26.gray(` No matches for "${query}"
4643
6862
  `));
4644
6863
  } else {
4645
- console.log(chalk9.gray(`
6864
+ console.log(chalk26.gray(`
4646
6865
  ${matches.length} match(es) for "${query}":
4647
6866
  `));
4648
6867
  for (const m of matches.slice(0, 10)) {
4649
- console.log(chalk9.gray(` [${m.index}] ${m.role}: ${m.preview}`));
6868
+ console.log(chalk26.gray(` [${m.index}] ${m.role}: ${m.preview}`));
4650
6869
  }
4651
6870
  console.log("");
4652
6871
  }
@@ -4655,12 +6874,12 @@ Analyze the above input.`;
4655
6874
  }
4656
6875
  if (input.startsWith("/plan ") && !input.startsWith("/plan approve") && !input.startsWith("/plan cancel") && !input.startsWith("/plan edit")) {
4657
6876
  const task = input.replace("/plan ", "").trim();
4658
- const planSpinner = ora("Generating plan...").start();
6877
+ const planSpinner = ora4("Generating plan...").start();
4659
6878
  try {
4660
6879
  activePlan = await generatePlan(task, model, systemPrompt);
4661
6880
  planSpinner.succeed("Plan generated");
4662
6881
  console.log(formatPlan(activePlan));
4663
- console.log(chalk9.gray(" Use /plan approve to execute, /plan edit to modify, or /plan cancel to discard.\n"));
6882
+ console.log(chalk26.gray(" Use /plan approve to execute, /plan edit to modify, or /plan cancel to discard.\n"));
4664
6883
  } catch (err) {
4665
6884
  planSpinner.fail(`Plan failed: ${err.message}`);
4666
6885
  }
@@ -4669,15 +6888,15 @@ Analyze the above input.`;
4669
6888
  }
4670
6889
  if (input === "/plan approve") {
4671
6890
  if (!activePlan) {
4672
- console.log(chalk9.gray(" No active plan. Use /plan <task> to create one.\n"));
6891
+ console.log(chalk26.gray(" No active plan. Use /plan <task> to create one.\n"));
4673
6892
  rl.prompt();
4674
6893
  return;
4675
6894
  }
4676
- console.log(chalk9.green(" Executing plan...\n"));
6895
+ console.log(chalk26.green(" Executing plan...\n"));
4677
6896
  while (!isPlanComplete(activePlan)) {
4678
6897
  const stepPrompt = currentStepPrompt(activePlan);
4679
6898
  messages.push({ role: "user", content: stepPrompt });
4680
- const planStepSpinner = ora(`Step ${activePlan.currentStep + 1}/${activePlan.steps.length}...`).start();
6899
+ const planStepSpinner = ora4(`Step ${activePlan.currentStep + 1}/${activePlan.steps.length}...`).start();
4681
6900
  try {
4682
6901
  const response = await runAgentLoop(messages, {
4683
6902
  model,
@@ -4691,10 +6910,10 @@ Analyze the above input.`;
4691
6910
  },
4692
6911
  onToolCall: (name) => {
4693
6912
  if (planStepSpinner.isSpinning) planStepSpinner.stop();
4694
- console.log(chalk9.gray(` \u2192 ${name}`));
6913
+ console.log(chalk26.gray(` \u2192 ${name}`));
4695
6914
  },
4696
6915
  onToolResult: (_name, _result, isError) => {
4697
- console.log(isError ? chalk9.red(" \u2717") : chalk9.green(" \u2713"));
6916
+ console.log(isError ? chalk26.red(" \u2717") : chalk26.green(" \u2713"));
4698
6917
  }
4699
6918
  });
4700
6919
  console.log("\n");
@@ -4707,7 +6926,7 @@ Analyze the above input.`;
4707
6926
  }
4708
6927
  }
4709
6928
  if (activePlan && isPlanComplete(activePlan)) {
4710
- console.log(chalk9.green(" Plan completed!\n"));
6929
+ console.log(chalk26.green(" Plan completed!\n"));
4711
6930
  }
4712
6931
  activePlan = null;
4713
6932
  rl.prompt();
@@ -4715,26 +6934,26 @@ Analyze the above input.`;
4715
6934
  }
4716
6935
  if (input === "/plan edit") {
4717
6936
  if (!activePlan) {
4718
- console.log(chalk9.gray(" No active plan to edit.\n"));
6937
+ console.log(chalk26.gray(" No active plan to edit.\n"));
4719
6938
  rl.prompt();
4720
6939
  return;
4721
6940
  }
4722
6941
  console.log(formatPlan(activePlan));
4723
- console.log(chalk9.gray(" Type a modified plan description and use /plan <new task> to regenerate,"));
4724
- console.log(chalk9.gray(" or /plan approve to proceed with the current plan.\n"));
6942
+ console.log(chalk26.gray(" Type a modified plan description and use /plan <new task> to regenerate,"));
6943
+ console.log(chalk26.gray(" or /plan approve to proceed with the current plan.\n"));
4725
6944
  rl.prompt();
4726
6945
  return;
4727
6946
  }
4728
6947
  if (input === "/plan cancel") {
4729
6948
  activePlan = null;
4730
- console.log(chalk9.gray(" Plan discarded.\n"));
6949
+ console.log(chalk26.gray(" Plan discarded.\n"));
4731
6950
  rl.prompt();
4732
6951
  return;
4733
6952
  }
4734
6953
  if (input.startsWith("/agent ")) {
4735
6954
  const task = input.replace("/agent ", "").trim();
4736
6955
  const agentId = nextSubagentId();
4737
- console.log(chalk9.cyan(` Spawning subagent #${agentId}: ${task}
6956
+ console.log(chalk26.cyan(` Spawning subagent #${agentId}: ${task}
4738
6957
  `));
4739
6958
  spawnSubagent({
4740
6959
  id: agentId,
@@ -4744,14 +6963,14 @@ Analyze the above input.`;
4744
6963
  toolContext: toolCtx,
4745
6964
  contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
4746
6965
  onComplete: (result) => {
4747
- console.log(chalk9.green(`
6966
+ console.log(chalk26.green(`
4748
6967
  Subagent #${agentId} finished:`));
4749
- console.log(chalk9.gray(` ${result.slice(0, 200)}
6968
+ console.log(chalk26.gray(` ${result.slice(0, 200)}
4750
6969
  `));
4751
6970
  rl.prompt();
4752
6971
  },
4753
6972
  onError: (err) => {
4754
- console.log(chalk9.red(`
6973
+ console.log(chalk26.red(`
4755
6974
  Subagent #${agentId} failed: ${err}
4756
6975
  `));
4757
6976
  rl.prompt();
@@ -4764,18 +6983,18 @@ Analyze the above input.`;
4764
6983
  try {
4765
6984
  const memories = await loadMemories(config.projectRoot);
4766
6985
  if (memories.length === 0) {
4767
- console.log(chalk9.gray(" No saved memories.\n"));
6986
+ console.log(chalk26.gray(" No saved memories.\n"));
4768
6987
  } else {
4769
- console.log(chalk9.gray(`
6988
+ console.log(chalk26.gray(`
4770
6989
  ${memories.length} saved memories:
4771
6990
  `));
4772
6991
  for (const m of memories) {
4773
- console.log(chalk9.gray(` [${m.type}] ${m.content.slice(0, 80)}`));
6992
+ console.log(chalk26.gray(` [${m.type}] ${m.content.slice(0, 80)}`));
4774
6993
  }
4775
6994
  console.log("");
4776
6995
  }
4777
6996
  } catch (err) {
4778
- console.log(chalk9.red(` Error: ${err.message}
6997
+ console.log(chalk26.red(` Error: ${err.message}
4779
6998
  `));
4780
6999
  }
4781
7000
  rl.prompt();
@@ -4786,16 +7005,16 @@ Analyze the above input.`;
4786
7005
  try {
4787
7006
  const results = await searchMemories(config.projectRoot, query);
4788
7007
  if (results.length === 0) {
4789
- console.log(chalk9.gray(` No memories matching "${query}"
7008
+ console.log(chalk26.gray(` No memories matching "${query}"
4790
7009
  `));
4791
7010
  } else {
4792
7011
  for (const m of results) {
4793
- console.log(chalk9.gray(` [${m.type}] ${m.content.slice(0, 100)}`));
7012
+ console.log(chalk26.gray(` [${m.type}] ${m.content.slice(0, 100)}`));
4794
7013
  }
4795
7014
  console.log("");
4796
7015
  }
4797
7016
  } catch (err) {
4798
- console.log(chalk9.red(` Error: ${err.message}
7017
+ console.log(chalk26.red(` Error: ${err.message}
4799
7018
  `));
4800
7019
  }
4801
7020
  rl.prompt();
@@ -4807,10 +7026,10 @@ Analyze the above input.`;
4807
7026
  for (const m of memories) {
4808
7027
  await deleteMemory(config.projectRoot, m.id);
4809
7028
  }
4810
- console.log(chalk9.yellow(` Cleared ${memories.length} memories.
7029
+ console.log(chalk26.yellow(` Cleared ${memories.length} memories.
4811
7030
  `));
4812
7031
  } catch (err) {
4813
- console.log(chalk9.red(` Error: ${err.message}
7032
+ console.log(chalk26.red(` Error: ${err.message}
4814
7033
  `));
4815
7034
  }
4816
7035
  rl.prompt();
@@ -4824,7 +7043,7 @@ Analyze the above input.`;
4824
7043
  }
4825
7044
  if (input.startsWith("/ralph plan ")) {
4826
7045
  const goal = input.replace("/ralph plan ", "").trim();
4827
- const planSpinner = ora("Ralph is planning...").start();
7046
+ const planSpinner = ora4("Ralph is planning...").start();
4828
7047
  try {
4829
7048
  ralphPlan = await generateRalphPlan(goal, model, systemPrompt);
4830
7049
  await savePlan(config.projectRoot, ralphPlan);
@@ -4838,27 +7057,27 @@ Analyze the above input.`;
4838
7057
  }
4839
7058
  if (input === "/ralph run") {
4840
7059
  if (!ralphPlan) {
4841
- console.log(chalk9.gray(" No Ralph plan. Use /ralph plan <goal> first.\n"));
7060
+ console.log(chalk26.gray(" No Ralph plan. Use /ralph plan <goal> first.\n"));
4842
7061
  rl.prompt();
4843
7062
  return;
4844
7063
  }
4845
- console.log(chalk9.green(" Ralph is running...\n"));
7064
+ console.log(chalk26.green(" Ralph is running...\n"));
4846
7065
  try {
4847
7066
  ralphPlan = await runRalphLoop(ralphPlan, {
4848
7067
  model,
4849
7068
  systemPrompt,
4850
7069
  toolContext: toolCtx,
4851
7070
  contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
4852
- onTaskStart: (task) => console.log(chalk9.cyan(` \u25B6 Task: ${task.description}`)),
4853
- onTaskComplete: (task) => console.log(chalk9.green(` \u2713 Done: ${task.description}
7071
+ onTaskStart: (task) => console.log(chalk26.cyan(` \u25B6 Task: ${task.description}`)),
7072
+ onTaskComplete: (task) => console.log(chalk26.green(` \u2713 Done: ${task.description}
4854
7073
  `)),
4855
- onTaskFail: (task, err) => console.log(chalk9.red(` \u2717 Failed: ${task.description} (${err})
7074
+ onTaskFail: (task, err) => console.log(chalk26.red(` \u2717 Failed: ${task.description} (${err})
4856
7075
  `))
4857
7076
  });
4858
7077
  await savePlan(config.projectRoot, ralphPlan);
4859
7078
  console.log(formatRalphStatus(ralphPlan));
4860
7079
  } catch (err) {
4861
- console.log(chalk9.red(` Ralph error: ${err.message}
7080
+ console.log(chalk26.red(` Ralph error: ${err.message}
4862
7081
  `));
4863
7082
  }
4864
7083
  rl.prompt();
@@ -4866,7 +7085,7 @@ Analyze the above input.`;
4866
7085
  }
4867
7086
  if (input === "/ralph status") {
4868
7087
  if (!ralphPlan) {
4869
- console.log(chalk9.gray(" No Ralph plan active.\n"));
7088
+ console.log(chalk26.gray(" No Ralph plan active.\n"));
4870
7089
  } else {
4871
7090
  console.log(formatRalphStatus(ralphPlan));
4872
7091
  }
@@ -4877,26 +7096,26 @@ Analyze the above input.`;
4877
7096
  ralphPlan = null;
4878
7097
  await deletePlan(config.projectRoot).catch(() => {
4879
7098
  });
4880
- console.log(chalk9.gray(" Ralph plan cleared.\n"));
7099
+ console.log(chalk26.gray(" Ralph plan cleared.\n"));
4881
7100
  rl.prompt();
4882
7101
  return;
4883
7102
  }
4884
7103
  if (input === "/branch") {
4885
7104
  const branchId = `branch-${branches.size + 1}`;
4886
7105
  branches.set(branchId, [...messages]);
4887
- console.log(chalk9.green(` Forked conversation as "${branchId}" (${messages.length} messages)
7106
+ console.log(chalk26.green(` Forked conversation as "${branchId}" (${messages.length} messages)
4888
7107
  `));
4889
7108
  rl.prompt();
4890
7109
  return;
4891
7110
  }
4892
7111
  if (input === "/branches") {
4893
7112
  if (branches.size === 0) {
4894
- console.log(chalk9.gray(" No conversation branches. Use /branch to fork.\n"));
7113
+ console.log(chalk26.gray(" No conversation branches. Use /branch to fork.\n"));
4895
7114
  } else {
4896
- console.log(chalk9.gray("\n Branches:\n"));
7115
+ console.log(chalk26.gray("\n Branches:\n"));
4897
7116
  for (const [name, msgs] of branches) {
4898
- const marker = name === currentBranch ? chalk9.green(" \u25CF") : " ";
4899
- console.log(chalk9.gray(` ${marker} ${name} (${msgs.length} messages)`));
7117
+ const marker = name === currentBranch ? chalk26.green(" \u25CF") : " ";
7118
+ console.log(chalk26.gray(` ${marker} ${name} (${msgs.length} messages)`));
4900
7119
  }
4901
7120
  console.log("");
4902
7121
  }
@@ -4933,8 +7152,50 @@ Analyze the above input.`;
4933
7152
  return;
4934
7153
  }
4935
7154
  if (input.startsWith("/")) {
4936
- console.log(chalk9.red(` Unknown command: ${input}`));
4937
- console.log(chalk9.gray(" Type /help for available commands.\n"));
7155
+ const cmdCtx = {
7156
+ cwd: config.projectRoot,
7157
+ modelId: activeModelId,
7158
+ messages,
7159
+ lastResponse: lastAssistantResponse,
7160
+ log: (msg) => console.log(chalk26.gray(` ${msg}`)),
7161
+ runPrompt: async (prompt, msgs) => {
7162
+ const spinner2 = ora4("Thinking...").start();
7163
+ const response = await withRetry(
7164
+ () => runAgentLoop(msgs, {
7165
+ model,
7166
+ systemPrompt,
7167
+ toolContext: { ...toolCtx, dryRun: isSandboxEnabled() || toolCtx.dryRun },
7168
+ maxIterations: config.maxIterations,
7169
+ contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
7170
+ onTextChunk: (chunk) => {
7171
+ if (spinner2.isSpinning) spinner2.stop();
7172
+ process.stdout.write(chunk);
7173
+ },
7174
+ onToolCall: (name, args) => {
7175
+ if (spinner2.isSpinning) spinner2.stop();
7176
+ const argSummary = Object.entries(args).map(([k, v]) => `${k}=${String(v).slice(0, 60)}`).join(", ");
7177
+ console.log(chalk26.gray(`
7178
+ \u2192 ${name}(${argSummary})`));
7179
+ },
7180
+ onToolResult: (_name, result, isError) => {
7181
+ const preview = result.slice(0, 100).replace(/\n/g, " ");
7182
+ const icon = isError ? chalk26.red("\u2717") : chalk26.green("\u2713");
7183
+ console.log(chalk26.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
7184
+ }
7185
+ })
7186
+ );
7187
+ if (spinner2.isSpinning) spinner2.stop();
7188
+ lastAssistantResponse = response.text;
7189
+ return response.text;
7190
+ }
7191
+ };
7192
+ const handled = await dispatchCommand(input, cmdCtx);
7193
+ if (handled) {
7194
+ rl.prompt();
7195
+ return;
7196
+ }
7197
+ console.log(chalk26.red(` Unknown command: ${input}`));
7198
+ console.log(chalk26.gray(" Type /help for available commands.\n"));
4938
7199
  rl.prompt();
4939
7200
  return;
4940
7201
  }
@@ -4942,16 +7203,16 @@ Analyze the above input.`;
4942
7203
  const refContext = formatReferences(references);
4943
7204
  const finalPrompt = refContext + cleanInput;
4944
7205
  if (references.length > 0) {
4945
- console.log(chalk9.gray(` Injected ${references.length} reference(s)`));
7206
+ console.log(chalk26.gray(` Injected ${references.length} reference(s)`));
4946
7207
  }
4947
7208
  messages.push({ role: "user", content: finalPrompt });
4948
- const spinner = ora("Thinking...").start();
7209
+ const spinner = ora4("Thinking...").start();
4949
7210
  try {
4950
7211
  const response = await withRetry(
4951
7212
  () => runAgentLoop(messages, {
4952
7213
  model,
4953
7214
  systemPrompt,
4954
- toolContext: toolCtx,
7215
+ toolContext: { ...toolCtx, dryRun: isSandboxEnabled() || toolCtx.dryRun },
4955
7216
  maxIterations: config.maxIterations,
4956
7217
  contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
4957
7218
  onTextChunk: (chunk) => {
@@ -4968,20 +7229,21 @@ Analyze the above input.`;
4968
7229
  const val = String(v);
4969
7230
  return `${k}=${val.length > 60 ? val.slice(0, 60) + "..." : val}`;
4970
7231
  }).join(", ");
4971
- console.log(chalk9.gray(`
7232
+ console.log(chalk26.gray(`
4972
7233
  \u2192 ${name}(${argSummary})`));
4973
7234
  },
4974
7235
  onToolResult: (_name, result, isError) => {
4975
7236
  const preview = result.slice(0, 100).replace(/\n/g, " ");
4976
- const icon = isError ? chalk9.red("\u2717") : chalk9.green("\u2713");
4977
- console.log(chalk9.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
7237
+ const icon = isError ? chalk26.red("\u2717") : chalk26.green("\u2713");
7238
+ console.log(chalk26.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
4978
7239
  },
4979
7240
  onCompress: () => {
4980
- console.log(chalk9.yellow("\n [Context compressed to fit window]\n"));
7241
+ console.log(chalk26.yellow("\n [Context compressed to fit window]\n"));
4981
7242
  }
4982
7243
  })
4983
7244
  );
4984
7245
  console.log("\n");
7246
+ lastAssistantResponse = response.text;
4985
7247
  checkpoints.commit(`Turn ${usage.turnCount + 1}`);
4986
7248
  if (response.usage) {
4987
7249
  usage.record({
@@ -5010,7 +7272,7 @@ Analyze the above input.`;
5010
7272
  type: "auto",
5011
7273
  content: memMatch[1]
5012
7274
  });
5013
- console.log(chalk9.gray(" (Saved to memory)\n"));
7275
+ console.log(chalk26.gray(" (Saved to memory)\n"));
5014
7276
  } catch {
5015
7277
  }
5016
7278
  }
@@ -5020,13 +7282,13 @@ Analyze the above input.`;
5020
7282
  checkpoints.discard();
5021
7283
  const msg = err.message?.toLowerCase() ?? "";
5022
7284
  if (msg.includes("fetch") || msg.includes("econnrefused") || msg.includes("network")) {
5023
- console.log(chalk9.gray(" Tip: Check that your Notch endpoint is running. Use /status to verify.\n"));
7285
+ console.log(chalk26.gray(" Tip: Check that your Notch endpoint is running. Use /status to verify.\n"));
5024
7286
  } else if (msg.includes("401") || msg.includes("unauthorized") || msg.includes("api key")) {
5025
- console.log(chalk9.gray(" Tip: Set NOTCH_API_KEY env var or use --api-key flag.\n"));
7287
+ console.log(chalk26.gray(" Tip: Set NOTCH_API_KEY env var or use --api-key flag.\n"));
5026
7288
  } else if (msg.includes("429") || msg.includes("rate limit")) {
5027
- console.log(chalk9.gray(" Tip: Rate limited. Wait a moment and try again.\n"));
7289
+ console.log(chalk26.gray(" Tip: Rate limited. Wait a moment and try again.\n"));
5028
7290
  } else {
5029
- console.log(chalk9.gray(" (The conversation history is preserved. Try again.)\n"));
7291
+ console.log(chalk26.gray(" (The conversation history is preserved. Try again.)\n"));
5030
7292
  }
5031
7293
  }
5032
7294
  rl.prompt();
@@ -5044,16 +7306,16 @@ async function handleRalphSubcommand(args, cliOpts) {
5044
7306
  cwd: config.projectRoot,
5045
7307
  requireConfirm: false,
5046
7308
  confirm: async () => true,
5047
- log: (msg) => console.log(chalk9.gray(` ${msg}`))
7309
+ log: (msg) => console.log(chalk26.gray(` ${msg}`))
5048
7310
  };
5049
7311
  const subcommand = args[0];
5050
7312
  if (subcommand === "plan") {
5051
7313
  const goal = args.slice(1).join(" ");
5052
7314
  if (!goal) {
5053
- console.error(chalk9.red(" Usage: notch ralph plan <goal>"));
7315
+ console.error(chalk26.red(" Usage: notch ralph plan <goal>"));
5054
7316
  process.exit(1);
5055
7317
  }
5056
- const spinner = ora("Ralph is planning...").start();
7318
+ const spinner = ora4("Ralph is planning...").start();
5057
7319
  const plan = await generateRalphPlan(goal, model, systemPrompt);
5058
7320
  await savePlan(config.projectRoot, plan);
5059
7321
  spinner.succeed(`Planned ${plan.tasks.length} tasks`);
@@ -5061,7 +7323,7 @@ async function handleRalphSubcommand(args, cliOpts) {
5061
7323
  } else if (subcommand === "run") {
5062
7324
  let plan = await loadPlan(config.projectRoot);
5063
7325
  if (!plan) {
5064
- console.error(chalk9.red(" No plan found. Run: notch ralph plan <goal>"));
7326
+ console.error(chalk26.red(" No plan found. Run: notch ralph plan <goal>"));
5065
7327
  process.exit(1);
5066
7328
  }
5067
7329
  plan = await runRalphLoop(plan, {
@@ -5069,27 +7331,27 @@ async function handleRalphSubcommand(args, cliOpts) {
5069
7331
  systemPrompt,
5070
7332
  toolContext: toolCtx,
5071
7333
  contextWindow: MODEL_CATALOG[config.models.chat.model].contextWindow,
5072
- onTaskStart: (t) => console.log(chalk9.cyan(` \u25B6 ${t.description}`)),
5073
- onTaskComplete: (t) => console.log(chalk9.green(` \u2713 ${t.description}`)),
5074
- onTaskFail: (t, e) => console.log(chalk9.red(` \u2717 ${t.description}: ${e}`))
7334
+ onTaskStart: (t) => console.log(chalk26.cyan(` \u25B6 ${t.description}`)),
7335
+ onTaskComplete: (t) => console.log(chalk26.green(` \u2713 ${t.description}`)),
7336
+ onTaskFail: (t, e) => console.log(chalk26.red(` \u2717 ${t.description}: ${e}`))
5075
7337
  });
5076
7338
  await savePlan(config.projectRoot, plan);
5077
7339
  console.log(formatRalphStatus(plan));
5078
7340
  } else if (subcommand === "status") {
5079
7341
  const plan = await loadPlan(config.projectRoot);
5080
7342
  if (!plan) {
5081
- console.log(chalk9.gray(" No Ralph plan found."));
7343
+ console.log(chalk26.gray(" No Ralph plan found."));
5082
7344
  } else {
5083
7345
  console.log(formatRalphStatus(plan));
5084
7346
  }
5085
7347
  } else {
5086
- console.error(chalk9.red(` Unknown: notch ralph ${subcommand}`));
5087
- console.error(chalk9.gray(" Usage: notch ralph <plan|run|status>"));
7348
+ console.error(chalk26.red(` Unknown: notch ralph ${subcommand}`));
7349
+ console.error(chalk26.gray(" Usage: notch ralph <plan|run|status>"));
5088
7350
  process.exit(1);
5089
7351
  }
5090
7352
  }
5091
7353
  main().catch((err) => {
5092
- console.error(chalk9.red(`
7354
+ console.error(chalk26.red(`
5093
7355
  Fatal: ${err.message}
5094
7356
  `));
5095
7357
  process.exit(1);