@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.
- package/dist/index.js +2956 -694
- 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
|
|
18
|
-
import
|
|
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(
|
|
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}${
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
constructor(config, serverName) {
|
|
1530
|
+
var StdioTransport = class {
|
|
1531
|
+
constructor(config, name) {
|
|
1039
1532
|
this.config = config;
|
|
1040
|
-
this.
|
|
1533
|
+
this.name = name;
|
|
1041
1534
|
}
|
|
1042
1535
|
process = null;
|
|
1043
1536
|
requestId = 0;
|
|
1044
1537
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
1045
1538
|
buffer = "";
|
|
1046
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
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/
|
|
1212
|
-
var
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
|
|
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
|
|
1281
|
-
import
|
|
1282
|
-
import
|
|
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 =
|
|
2197
|
+
const homeDir = os2.homedir();
|
|
1287
2198
|
for (const file of INSTRUCTION_FILES) {
|
|
1288
|
-
const globalPath =
|
|
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 =
|
|
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
|
|
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
|
|
1327
|
-
import
|
|
1328
|
-
import
|
|
1329
|
-
var MEMORY_DIR =
|
|
1330
|
-
var INDEX_FILE =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1627
|
-
import
|
|
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
|
|
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
|
|
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
|
|
2590
|
+
await fs11.unlink(snap.path);
|
|
1671
2591
|
} catch {
|
|
1672
2592
|
}
|
|
1673
2593
|
} else {
|
|
1674
|
-
await
|
|
1675
|
-
await
|
|
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(([
|
|
1708
|
-
path:
|
|
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
|
|
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 ?
|
|
1742
|
-
return
|
|
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
|
-
|
|
2667
|
+
chalk2.gray(`
|
|
1748
2668
|
Session usage (${this.turnCount} turns):`),
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
|
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(
|
|
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" ?
|
|
2044
|
-
const num =
|
|
2045
|
-
const action = step.status === "done" ?
|
|
2046
|
-
const files = step.files.length > 0 ?
|
|
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(
|
|
2971
|
+
lines.push(chalk3.yellow(" Risks:"));
|
|
2052
2972
|
for (const risk of plan.risks) {
|
|
2053
|
-
lines.push(
|
|
2973
|
+
lines.push(chalk3.yellow(` \u26A0 ${risk}`));
|
|
2054
2974
|
}
|
|
2055
2975
|
}
|
|
2056
2976
|
lines.push("");
|
|
2057
2977
|
if (!plan.approved) {
|
|
2058
|
-
lines.push(
|
|
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
|
|
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
|
|
3052
|
+
return chalk4.gray(`$${last.cost.toFixed(4)}`);
|
|
2133
3053
|
}
|
|
2134
3054
|
formatSession() {
|
|
2135
|
-
if (this.entries.length === 0) return
|
|
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
|
-
|
|
3059
|
+
chalk4.gray(`
|
|
2140
3060
|
Session cost estimate:`),
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
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 = [
|
|
3083
|
+
const lines = [chalk4.gray("\n Cost by model:")];
|
|
2164
3084
|
for (const [modelId, data] of byModel) {
|
|
2165
3085
|
lines.push(
|
|
2166
|
-
|
|
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
|
|
2175
|
-
import
|
|
2176
|
-
import
|
|
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
|
|
2181
|
-
import
|
|
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 =
|
|
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 =
|
|
3183
|
+
const fullPath = path14.resolve(root, file);
|
|
2264
3184
|
try {
|
|
2265
|
-
const content = await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
3463
|
+
const bar = chalk5.green("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(barWidth - filled));
|
|
2544
3464
|
lines.push("");
|
|
2545
|
-
lines.push(
|
|
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(
|
|
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" ?
|
|
2551
|
-
const title = task.status === "done" ?
|
|
2552
|
-
const files = task.files.length > 0 ?
|
|
2553
|
-
const err = task.error ?
|
|
2554
|
-
const attempts = task.attempts > 1 ?
|
|
2555
|
-
lines.push(` ${icon} ${
|
|
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
|
|
2563
|
-
import
|
|
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 =
|
|
3526
|
+
const filePath = path16.isAbsolute(ref) ? ref : path16.resolve(cwd, ref);
|
|
2607
3527
|
try {
|
|
2608
|
-
const content = await
|
|
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
|
|
3624
|
+
import chalk7 from "chalk";
|
|
2705
3625
|
|
|
2706
3626
|
// src/ui/themes.ts
|
|
2707
|
-
import
|
|
3627
|
+
import chalk6 from "chalk";
|
|
2708
3628
|
var defaultTheme = {
|
|
2709
3629
|
name: "Default",
|
|
2710
3630
|
description: "FreeSyntax \u2014 silver, white, monochrome",
|
|
2711
|
-
brand:
|
|
3631
|
+
brand: chalk6.hex("#D4D4D4"),
|
|
2712
3632
|
// silver (banner uses gradient override)
|
|
2713
|
-
mascot:
|
|
3633
|
+
mascot: chalk6.hex("#AAAAAA"),
|
|
2714
3634
|
// medium gray mantis
|
|
2715
|
-
mascotAccent:
|
|
3635
|
+
mascotAccent: chalk6.hex("#FFFFFF"),
|
|
2716
3636
|
// white eyes
|
|
2717
|
-
tagline:
|
|
3637
|
+
tagline: chalk6.hex("#777777"),
|
|
2718
3638
|
// muted gray
|
|
2719
|
-
prompt:
|
|
3639
|
+
prompt: chalk6.hex("#CCCCCC"),
|
|
2720
3640
|
// silver prompt
|
|
2721
|
-
border:
|
|
3641
|
+
border: chalk6.hex("#444444"),
|
|
2722
3642
|
// dark border
|
|
2723
|
-
dim:
|
|
3643
|
+
dim: chalk6.hex("#666666"),
|
|
2724
3644
|
// muted text
|
|
2725
|
-
text:
|
|
3645
|
+
text: chalk6.hex("#D4D4D4"),
|
|
2726
3646
|
// silver body text
|
|
2727
|
-
bold:
|
|
3647
|
+
bold: chalk6.hex("#FFFFFF").bold,
|
|
2728
3648
|
// pure white emphasis
|
|
2729
|
-
success:
|
|
2730
|
-
warning:
|
|
2731
|
-
error:
|
|
2732
|
-
info:
|
|
3649
|
+
success: chalk6.green,
|
|
3650
|
+
warning: chalk6.yellow,
|
|
3651
|
+
error: chalk6.red,
|
|
3652
|
+
info: chalk6.hex("#BBBBBB"),
|
|
2733
3653
|
// light gray info
|
|
2734
|
-
toolName:
|
|
2735
|
-
toolArgs:
|
|
2736
|
-
toolResult:
|
|
2737
|
-
diffAdd:
|
|
2738
|
-
diffRemove:
|
|
2739
|
-
diffHeader:
|
|
2740
|
-
mdH1:
|
|
2741
|
-
mdH2:
|
|
2742
|
-
mdH3:
|
|
2743
|
-
mdCode:
|
|
2744
|
-
mdInlineCode:
|
|
2745
|
-
mdLink:
|
|
2746
|
-
meterLow:
|
|
2747
|
-
meterMid:
|
|
2748
|
-
meterHigh:
|
|
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:
|
|
3673
|
+
brand: chalk6.hex("#FFB347"),
|
|
2754
3674
|
// warm amber (instrument readout)
|
|
2755
|
-
mascot:
|
|
3675
|
+
mascot: chalk6.hex("#4A90D9"),
|
|
2756
3676
|
// pale blue (frozen clouds)
|
|
2757
|
-
mascotAccent:
|
|
3677
|
+
mascotAccent: chalk6.hex("#FFD700"),
|
|
2758
3678
|
// gold (Gargantua light)
|
|
2759
|
-
tagline:
|
|
3679
|
+
tagline: chalk6.hex("#708090"),
|
|
2760
3680
|
// slate gray (ship hull)
|
|
2761
|
-
prompt:
|
|
2762
|
-
border:
|
|
3681
|
+
prompt: chalk6.hex("#FFB347"),
|
|
3682
|
+
border: chalk6.hex("#3A3A3A"),
|
|
2763
3683
|
// dark hull metal
|
|
2764
|
-
dim:
|
|
2765
|
-
text:
|
|
3684
|
+
dim: chalk6.hex("#5C5C5C"),
|
|
3685
|
+
text: chalk6.hex("#D4D4D4"),
|
|
2766
3686
|
// cool white (display text)
|
|
2767
|
-
bold:
|
|
2768
|
-
success:
|
|
3687
|
+
bold: chalk6.hex("#FFFFFF").bold,
|
|
3688
|
+
success: chalk6.hex("#7CFC00"),
|
|
2769
3689
|
// "CASE: All systems nominal"
|
|
2770
|
-
warning:
|
|
3690
|
+
warning: chalk6.hex("#FFB347"),
|
|
2771
3691
|
// amber warning
|
|
2772
|
-
error:
|
|
3692
|
+
error: chalk6.hex("#FF4444"),
|
|
2773
3693
|
// critical alert
|
|
2774
|
-
info:
|
|
3694
|
+
info: chalk6.hex("#4A90D9"),
|
|
2775
3695
|
// blue data readout
|
|
2776
|
-
toolName:
|
|
2777
|
-
toolArgs:
|
|
2778
|
-
toolResult:
|
|
2779
|
-
diffAdd:
|
|
2780
|
-
diffRemove:
|
|
2781
|
-
diffHeader:
|
|
2782
|
-
mdH1:
|
|
2783
|
-
mdH2:
|
|
2784
|
-
mdH3:
|
|
2785
|
-
mdCode:
|
|
2786
|
-
mdInlineCode:
|
|
2787
|
-
mdLink:
|
|
2788
|
-
meterLow:
|
|
2789
|
-
meterMid:
|
|
2790
|
-
meterHigh:
|
|
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:
|
|
3715
|
+
brand: chalk6.hex("#A2AAAD"),
|
|
2796
3716
|
// apple silver
|
|
2797
|
-
mascot:
|
|
3717
|
+
mascot: chalk6.hex("#86868B"),
|
|
2798
3718
|
// space gray
|
|
2799
|
-
mascotAccent:
|
|
3719
|
+
mascotAccent: chalk6.hex("#0071E3"),
|
|
2800
3720
|
// apple blue
|
|
2801
|
-
tagline:
|
|
3721
|
+
tagline: chalk6.hex("#6E6E73"),
|
|
2802
3722
|
// secondary gray
|
|
2803
|
-
prompt:
|
|
2804
|
-
border:
|
|
3723
|
+
prompt: chalk6.hex("#A2AAAD"),
|
|
3724
|
+
border: chalk6.hex("#3A3A3C"),
|
|
2805
3725
|
// dark separator
|
|
2806
|
-
dim:
|
|
2807
|
-
text:
|
|
3726
|
+
dim: chalk6.hex("#6E6E73"),
|
|
3727
|
+
text: chalk6.hex("#E5E5EA"),
|
|
2808
3728
|
// system gray 6
|
|
2809
|
-
bold:
|
|
3729
|
+
bold: chalk6.hex("#F5F5F7").bold,
|
|
2810
3730
|
// near white
|
|
2811
|
-
success:
|
|
3731
|
+
success: chalk6.hex("#30D158"),
|
|
2812
3732
|
// apple green
|
|
2813
|
-
warning:
|
|
3733
|
+
warning: chalk6.hex("#FFD60A"),
|
|
2814
3734
|
// apple yellow
|
|
2815
|
-
error:
|
|
3735
|
+
error: chalk6.hex("#FF453A"),
|
|
2816
3736
|
// apple red
|
|
2817
|
-
info:
|
|
3737
|
+
info: chalk6.hex("#0A84FF"),
|
|
2818
3738
|
// apple blue
|
|
2819
|
-
toolName:
|
|
2820
|
-
toolArgs:
|
|
2821
|
-
toolResult:
|
|
2822
|
-
diffAdd:
|
|
2823
|
-
diffRemove:
|
|
2824
|
-
diffHeader:
|
|
2825
|
-
mdH1:
|
|
2826
|
-
mdH2:
|
|
2827
|
-
mdH3:
|
|
2828
|
-
mdCode:
|
|
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:
|
|
2831
|
-
mdLink:
|
|
2832
|
-
meterLow:
|
|
2833
|
-
meterMid:
|
|
2834
|
-
meterHigh:
|
|
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:
|
|
3759
|
+
brand: chalk6.hex("#FF2079"),
|
|
2840
3760
|
// hot pink
|
|
2841
|
-
mascot:
|
|
3761
|
+
mascot: chalk6.hex("#00FFFF"),
|
|
2842
3762
|
// electric cyan
|
|
2843
|
-
mascotAccent:
|
|
3763
|
+
mascotAccent: chalk6.hex("#FF2079"),
|
|
2844
3764
|
// hot pink eyes
|
|
2845
|
-
tagline:
|
|
3765
|
+
tagline: chalk6.hex("#9D00FF"),
|
|
2846
3766
|
// deep violet
|
|
2847
|
-
prompt:
|
|
2848
|
-
border:
|
|
2849
|
-
dim:
|
|
2850
|
-
text:
|
|
2851
|
-
bold:
|
|
2852
|
-
success:
|
|
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:
|
|
3774
|
+
warning: chalk6.hex("#FFFF00"),
|
|
2855
3775
|
// neon yellow
|
|
2856
|
-
error:
|
|
2857
|
-
info:
|
|
2858
|
-
toolName:
|
|
2859
|
-
toolArgs:
|
|
2860
|
-
toolResult:
|
|
2861
|
-
diffAdd:
|
|
2862
|
-
diffRemove:
|
|
2863
|
-
diffHeader:
|
|
2864
|
-
mdH1:
|
|
2865
|
-
mdH2:
|
|
2866
|
-
mdH3:
|
|
2867
|
-
mdCode:
|
|
2868
|
-
mdInlineCode:
|
|
2869
|
-
mdLink:
|
|
2870
|
-
meterLow:
|
|
2871
|
-
meterMid:
|
|
2872
|
-
meterHigh:
|
|
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:
|
|
3797
|
+
brand: chalk6.hex("#7FFFD4"),
|
|
2878
3798
|
// aquamarine
|
|
2879
|
-
mascot:
|
|
3799
|
+
mascot: chalk6.hex("#00CED1"),
|
|
2880
3800
|
// dark turquoise
|
|
2881
|
-
mascotAccent:
|
|
3801
|
+
mascotAccent: chalk6.hex("#DA70D6"),
|
|
2882
3802
|
// orchid
|
|
2883
|
-
tagline:
|
|
3803
|
+
tagline: chalk6.hex("#9370DB"),
|
|
2884
3804
|
// medium purple
|
|
2885
|
-
prompt:
|
|
2886
|
-
border:
|
|
3805
|
+
prompt: chalk6.hex("#7FFFD4"),
|
|
3806
|
+
border: chalk6.hex("#2F4F4F"),
|
|
2887
3807
|
// dark slate
|
|
2888
|
-
dim:
|
|
3808
|
+
dim: chalk6.hex("#5F9EA0"),
|
|
2889
3809
|
// cadet blue
|
|
2890
|
-
text:
|
|
3810
|
+
text: chalk6.hex("#E0FFFF"),
|
|
2891
3811
|
// light cyan
|
|
2892
|
-
bold:
|
|
2893
|
-
success:
|
|
3812
|
+
bold: chalk6.hex("#F0FFFF").bold,
|
|
3813
|
+
success: chalk6.hex("#00FA9A"),
|
|
2894
3814
|
// medium spring green
|
|
2895
|
-
warning:
|
|
3815
|
+
warning: chalk6.hex("#DDA0DD"),
|
|
2896
3816
|
// plum
|
|
2897
|
-
error:
|
|
2898
|
-
info:
|
|
3817
|
+
error: chalk6.hex("#FF6B6B"),
|
|
3818
|
+
info: chalk6.hex("#87CEEB"),
|
|
2899
3819
|
// sky blue
|
|
2900
|
-
toolName:
|
|
2901
|
-
toolArgs:
|
|
2902
|
-
toolResult:
|
|
2903
|
-
diffAdd:
|
|
2904
|
-
diffRemove:
|
|
2905
|
-
diffHeader:
|
|
2906
|
-
mdH1:
|
|
2907
|
-
mdH2:
|
|
2908
|
-
mdH3:
|
|
2909
|
-
mdCode:
|
|
2910
|
-
mdInlineCode:
|
|
2911
|
-
mdLink:
|
|
2912
|
-
meterLow:
|
|
2913
|
-
meterMid:
|
|
2914
|
-
meterHigh:
|
|
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:
|
|
3839
|
+
brand: chalk6.hex("#268BD2"),
|
|
2920
3840
|
// blue
|
|
2921
|
-
mascot:
|
|
3841
|
+
mascot: chalk6.hex("#859900"),
|
|
2922
3842
|
// green
|
|
2923
|
-
mascotAccent:
|
|
3843
|
+
mascotAccent: chalk6.hex("#B58900"),
|
|
2924
3844
|
// yellow
|
|
2925
|
-
tagline:
|
|
3845
|
+
tagline: chalk6.hex("#2AA198"),
|
|
2926
3846
|
// cyan
|
|
2927
|
-
prompt:
|
|
2928
|
-
border:
|
|
3847
|
+
prompt: chalk6.hex("#268BD2"),
|
|
3848
|
+
border: chalk6.hex("#073642"),
|
|
2929
3849
|
// base02
|
|
2930
|
-
dim:
|
|
3850
|
+
dim: chalk6.hex("#586E75"),
|
|
2931
3851
|
// base01
|
|
2932
|
-
text:
|
|
3852
|
+
text: chalk6.hex("#839496"),
|
|
2933
3853
|
// base0
|
|
2934
|
-
bold:
|
|
3854
|
+
bold: chalk6.hex("#93A1A1").bold,
|
|
2935
3855
|
// base1
|
|
2936
|
-
success:
|
|
2937
|
-
warning:
|
|
2938
|
-
error:
|
|
2939
|
-
info:
|
|
2940
|
-
toolName:
|
|
2941
|
-
toolArgs:
|
|
2942
|
-
toolResult:
|
|
2943
|
-
diffAdd:
|
|
2944
|
-
diffRemove:
|
|
2945
|
-
diffHeader:
|
|
2946
|
-
mdH1:
|
|
2947
|
-
mdH2:
|
|
2948
|
-
mdH3:
|
|
2949
|
-
mdCode:
|
|
2950
|
-
mdInlineCode:
|
|
2951
|
-
mdLink:
|
|
2952
|
-
meterLow:
|
|
2953
|
-
meterMid:
|
|
2954
|
-
meterHigh:
|
|
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:
|
|
3879
|
+
brand: chalk6.hex("#BD93F9"),
|
|
2960
3880
|
// purple
|
|
2961
|
-
mascot:
|
|
3881
|
+
mascot: chalk6.hex("#50FA7B"),
|
|
2962
3882
|
// green
|
|
2963
|
-
mascotAccent:
|
|
3883
|
+
mascotAccent: chalk6.hex("#FF79C6"),
|
|
2964
3884
|
// pink
|
|
2965
|
-
tagline:
|
|
3885
|
+
tagline: chalk6.hex("#6272A4"),
|
|
2966
3886
|
// comment gray
|
|
2967
|
-
prompt:
|
|
2968
|
-
border:
|
|
3887
|
+
prompt: chalk6.hex("#BD93F9"),
|
|
3888
|
+
border: chalk6.hex("#44475A"),
|
|
2969
3889
|
// current line
|
|
2970
|
-
dim:
|
|
3890
|
+
dim: chalk6.hex("#6272A4"),
|
|
2971
3891
|
// comment
|
|
2972
|
-
text:
|
|
3892
|
+
text: chalk6.hex("#F8F8F2"),
|
|
2973
3893
|
// foreground
|
|
2974
|
-
bold:
|
|
2975
|
-
success:
|
|
2976
|
-
warning:
|
|
3894
|
+
bold: chalk6.hex("#F8F8F2").bold,
|
|
3895
|
+
success: chalk6.hex("#50FA7B"),
|
|
3896
|
+
warning: chalk6.hex("#F1FA8C"),
|
|
2977
3897
|
// yellow
|
|
2978
|
-
error:
|
|
2979
|
-
info:
|
|
3898
|
+
error: chalk6.hex("#FF5555"),
|
|
3899
|
+
info: chalk6.hex("#8BE9FD"),
|
|
2980
3900
|
// cyan
|
|
2981
|
-
toolName:
|
|
2982
|
-
toolArgs:
|
|
2983
|
-
toolResult:
|
|
2984
|
-
diffAdd:
|
|
2985
|
-
diffRemove:
|
|
2986
|
-
diffHeader:
|
|
2987
|
-
mdH1:
|
|
2988
|
-
mdH2:
|
|
2989
|
-
mdH3:
|
|
2990
|
-
mdCode:
|
|
2991
|
-
mdInlineCode:
|
|
2992
|
-
mdLink:
|
|
2993
|
-
meterLow:
|
|
2994
|
-
meterMid:
|
|
2995
|
-
meterHigh:
|
|
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:
|
|
3920
|
+
brand: chalk6.hex("#A6E22E"),
|
|
3001
3921
|
// green
|
|
3002
|
-
mascot:
|
|
3922
|
+
mascot: chalk6.hex("#66D9EF"),
|
|
3003
3923
|
// blue
|
|
3004
|
-
mascotAccent:
|
|
3924
|
+
mascotAccent: chalk6.hex("#F92672"),
|
|
3005
3925
|
// pink
|
|
3006
|
-
tagline:
|
|
3926
|
+
tagline: chalk6.hex("#75715E"),
|
|
3007
3927
|
// comment
|
|
3008
|
-
prompt:
|
|
3009
|
-
border:
|
|
3010
|
-
dim:
|
|
3011
|
-
text:
|
|
3012
|
-
bold:
|
|
3013
|
-
success:
|
|
3014
|
-
warning:
|
|
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:
|
|
3017
|
-
info:
|
|
3018
|
-
toolName:
|
|
3019
|
-
toolArgs:
|
|
3020
|
-
toolResult:
|
|
3021
|
-
diffAdd:
|
|
3022
|
-
diffRemove:
|
|
3023
|
-
diffHeader:
|
|
3024
|
-
mdH1:
|
|
3025
|
-
mdH2:
|
|
3026
|
-
mdH3:
|
|
3027
|
-
mdCode:
|
|
3028
|
-
mdInlineCode:
|
|
3029
|
-
mdLink:
|
|
3030
|
-
meterLow:
|
|
3031
|
-
meterMid:
|
|
3032
|
-
meterHigh:
|
|
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:
|
|
3957
|
+
brand: chalk6.hex("#00B4D8"),
|
|
3038
3958
|
// cerulean
|
|
3039
|
-
mascot:
|
|
3959
|
+
mascot: chalk6.hex("#0077B6"),
|
|
3040
3960
|
// deep blue
|
|
3041
|
-
mascotAccent:
|
|
3961
|
+
mascotAccent: chalk6.hex("#90E0EF"),
|
|
3042
3962
|
// light blue (bioluminescent)
|
|
3043
|
-
tagline:
|
|
3963
|
+
tagline: chalk6.hex("#023E8A"),
|
|
3044
3964
|
// navy
|
|
3045
|
-
prompt:
|
|
3046
|
-
border:
|
|
3965
|
+
prompt: chalk6.hex("#00B4D8"),
|
|
3966
|
+
border: chalk6.hex("#03045E"),
|
|
3047
3967
|
// deep navy
|
|
3048
|
-
dim:
|
|
3049
|
-
text:
|
|
3968
|
+
dim: chalk6.hex("#0077B6"),
|
|
3969
|
+
text: chalk6.hex("#CAF0F8"),
|
|
3050
3970
|
// lightest blue
|
|
3051
|
-
bold:
|
|
3052
|
-
success:
|
|
3971
|
+
bold: chalk6.hex("#CAF0F8").bold,
|
|
3972
|
+
success: chalk6.hex("#2DC653"),
|
|
3053
3973
|
// kelp green
|
|
3054
|
-
warning:
|
|
3974
|
+
warning: chalk6.hex("#F4A261"),
|
|
3055
3975
|
// sandy
|
|
3056
|
-
error:
|
|
3976
|
+
error: chalk6.hex("#E76F51"),
|
|
3057
3977
|
// coral
|
|
3058
|
-
info:
|
|
3059
|
-
toolName:
|
|
3060
|
-
toolArgs:
|
|
3061
|
-
toolResult:
|
|
3062
|
-
diffAdd:
|
|
3063
|
-
diffRemove:
|
|
3064
|
-
diffHeader:
|
|
3065
|
-
mdH1:
|
|
3066
|
-
mdH2:
|
|
3067
|
-
mdH3:
|
|
3068
|
-
mdCode:
|
|
3069
|
-
mdInlineCode:
|
|
3070
|
-
mdLink:
|
|
3071
|
-
meterLow:
|
|
3072
|
-
meterMid:
|
|
3073
|
-
meterHigh:
|
|
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:
|
|
3998
|
+
brand: chalk6.hex("#FF6B35"),
|
|
3079
3999
|
// flame orange
|
|
3080
|
-
mascot:
|
|
4000
|
+
mascot: chalk6.hex("#D62828"),
|
|
3081
4001
|
// deep red
|
|
3082
|
-
mascotAccent:
|
|
4002
|
+
mascotAccent: chalk6.hex("#FFD166"),
|
|
3083
4003
|
// bright flame
|
|
3084
|
-
tagline:
|
|
4004
|
+
tagline: chalk6.hex("#8B4513"),
|
|
3085
4005
|
// saddle brown
|
|
3086
|
-
prompt:
|
|
3087
|
-
border:
|
|
4006
|
+
prompt: chalk6.hex("#FF6B35"),
|
|
4007
|
+
border: chalk6.hex("#2B2B2B"),
|
|
3088
4008
|
// charcoal
|
|
3089
|
-
dim:
|
|
3090
|
-
text:
|
|
4009
|
+
dim: chalk6.hex("#6B3A2A"),
|
|
4010
|
+
text: chalk6.hex("#F4E4C1"),
|
|
3091
4011
|
// warm parchment
|
|
3092
|
-
bold:
|
|
3093
|
-
success:
|
|
4012
|
+
bold: chalk6.hex("#FFF5E1").bold,
|
|
4013
|
+
success: chalk6.hex("#FFD166"),
|
|
3094
4014
|
// bright flame = success
|
|
3095
|
-
warning:
|
|
3096
|
-
error:
|
|
3097
|
-
info:
|
|
4015
|
+
warning: chalk6.hex("#FF6B35"),
|
|
4016
|
+
error: chalk6.hex("#D62828"),
|
|
4017
|
+
info: chalk6.hex("#F4845F"),
|
|
3098
4018
|
// soft coral
|
|
3099
|
-
toolName:
|
|
3100
|
-
toolArgs:
|
|
3101
|
-
toolResult:
|
|
3102
|
-
diffAdd:
|
|
3103
|
-
diffRemove:
|
|
3104
|
-
diffHeader:
|
|
3105
|
-
mdH1:
|
|
3106
|
-
mdH2:
|
|
3107
|
-
mdH3:
|
|
3108
|
-
mdCode:
|
|
3109
|
-
mdInlineCode:
|
|
3110
|
-
mdLink:
|
|
3111
|
-
meterLow:
|
|
3112
|
-
meterMid:
|
|
3113
|
-
meterHigh:
|
|
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:
|
|
3119
|
-
mascot:
|
|
3120
|
-
mascotAccent:
|
|
3121
|
-
tagline:
|
|
3122
|
-
prompt:
|
|
3123
|
-
border:
|
|
3124
|
-
dim:
|
|
3125
|
-
text:
|
|
3126
|
-
bold:
|
|
3127
|
-
success:
|
|
3128
|
-
warning:
|
|
3129
|
-
error:
|
|
3130
|
-
info:
|
|
3131
|
-
toolName:
|
|
3132
|
-
toolArgs:
|
|
3133
|
-
toolResult:
|
|
3134
|
-
diffAdd:
|
|
3135
|
-
diffRemove:
|
|
3136
|
-
diffHeader:
|
|
3137
|
-
mdH1:
|
|
3138
|
-
mdH2:
|
|
3139
|
-
mdH3:
|
|
3140
|
-
mdCode:
|
|
3141
|
-
mdInlineCode:
|
|
3142
|
-
mdLink:
|
|
3143
|
-
meterLow:
|
|
3144
|
-
meterMid:
|
|
3145
|
-
meterHigh:
|
|
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) :
|
|
3185
|
-
const desc =
|
|
3186
|
-
lines.push(` ${active} ${
|
|
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(
|
|
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
|
|
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
|
|
3333
|
-
import
|
|
3334
|
-
import
|
|
3335
|
-
import { execSync as
|
|
3336
|
-
import
|
|
3337
|
-
var CACHE_FILE =
|
|
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(
|
|
4290
|
+
console.log(chalk8.cyan(`
|
|
3371
4291
|
\u2B06 Updating Notch CLI: ${current} \u2192 ${latest}...
|
|
3372
4292
|
`));
|
|
3373
4293
|
try {
|
|
3374
|
-
|
|
4294
|
+
execSync4(`npm install -g ${PACKAGE_NAME}@${latest}`, {
|
|
3375
4295
|
stdio: "inherit",
|
|
3376
4296
|
timeout: 6e4
|
|
3377
4297
|
});
|
|
3378
|
-
console.log(
|
|
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
|
|
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
|
|
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
|
|
3410
|
-
await
|
|
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
|
|
3417
|
-
import
|
|
3418
|
-
import
|
|
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 =
|
|
3436
|
-
const globalPath =
|
|
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
|
|
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
|
|
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
|
|
3494
|
-
import
|
|
3495
|
-
import
|
|
3496
|
-
import
|
|
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 =
|
|
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 =
|
|
4422
|
+
const key = path19.resolve(projectRoot);
|
|
3502
4423
|
try {
|
|
3503
|
-
const store = JSON.parse(await
|
|
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 =
|
|
4432
|
+
const key = path19.resolve(projectRoot);
|
|
3512
4433
|
let store = {};
|
|
3513
4434
|
try {
|
|
3514
|
-
store = JSON.parse(await
|
|
4435
|
+
store = JSON.parse(await fs17.readFile(TRUST_STORE_PATH, "utf-8"));
|
|
3515
4436
|
} catch {
|
|
3516
4437
|
}
|
|
3517
4438
|
store[key] = fingerprint;
|
|
3518
|
-
await
|
|
3519
|
-
await
|
|
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 =
|
|
4444
|
+
const globalPath = path19.join(os6.homedir(), ".notch", "hooks.json");
|
|
3524
4445
|
try {
|
|
3525
|
-
const raw = await
|
|
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 =
|
|
4453
|
+
const projectPath = path19.join(projectRoot, ".notch.json");
|
|
3533
4454
|
try {
|
|
3534
|
-
const raw = await
|
|
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
|
|
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 =
|
|
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
|
|
3619
|
-
import
|
|
3620
|
-
import
|
|
3621
|
-
var SESSION_DIR =
|
|
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
|
|
4576
|
+
await fs18.mkdir(SESSION_DIR, { recursive: true });
|
|
3625
4577
|
}
|
|
3626
4578
|
function sessionPath(id) {
|
|
3627
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3749
|
-
import
|
|
3750
|
-
import
|
|
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 =
|
|
3782
|
-
const instructionsPath =
|
|
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
|
|
4738
|
+
await fs19.access(configPath);
|
|
3787
4739
|
configExists = true;
|
|
3788
4740
|
} catch {
|
|
3789
4741
|
}
|
|
3790
4742
|
try {
|
|
3791
|
-
await
|
|
4743
|
+
await fs19.access(instructionsPath);
|
|
3792
4744
|
instructionsExist = true;
|
|
3793
4745
|
} catch {
|
|
3794
4746
|
}
|
|
3795
4747
|
if (!configExists) {
|
|
3796
|
-
await
|
|
3797
|
-
console.log(
|
|
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(
|
|
4751
|
+
console.log(chalk9.gray(` Skipped ${configPath} (already exists)`));
|
|
3800
4752
|
}
|
|
3801
4753
|
if (!instructionsExist) {
|
|
3802
|
-
await
|
|
3803
|
-
console.log(
|
|
4754
|
+
await fs19.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS, "utf-8");
|
|
4755
|
+
console.log(chalk9.green(` Created ${instructionsPath}`));
|
|
3804
4756
|
} else {
|
|
3805
|
-
console.log(
|
|
4757
|
+
console.log(chalk9.gray(` Skipped ${instructionsPath} (already exists)`));
|
|
3806
4758
|
}
|
|
3807
|
-
const gitignorePath =
|
|
4759
|
+
const gitignorePath = path21.join(projectRoot, ".gitignore");
|
|
3808
4760
|
try {
|
|
3809
|
-
const gitignore = await
|
|
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
|
|
3815
|
-
console.log(
|
|
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(
|
|
3821
|
-
console.log(
|
|
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
|
|
3942
|
-
import
|
|
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("/") ?
|
|
4004
|
-
const prefix = partial.includes("/") ?
|
|
4005
|
-
const entries =
|
|
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("/") ?
|
|
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
|
|
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(
|
|
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 =
|
|
6315
|
+
const spinner = ora4("Opening browser...").start();
|
|
4119
6316
|
try {
|
|
4120
6317
|
spinner.stop();
|
|
4121
6318
|
const creds = await login();
|
|
4122
|
-
console.log(
|
|
6319
|
+
console.log(chalk26.green(`
|
|
4123
6320
|
\u2713 Signed in as ${creds.email}`));
|
|
4124
|
-
console.log(
|
|
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(
|
|
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(
|
|
6335
|
+
console.log(chalk26.gray("\n Not signed in.\n"));
|
|
4139
6336
|
} else {
|
|
4140
6337
|
await clearCredentials();
|
|
4141
|
-
console.log(
|
|
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(
|
|
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(
|
|
4154
|
-
Signed in as ${
|
|
4155
|
-
console.log(
|
|
4156
|
-
console.log(
|
|
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(
|
|
4181
|
-
console.error(
|
|
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(
|
|
4221
|
-
commands.forEach((cmd) => console.warn(
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
6467
|
+
console.log(chalk26.green(` MCP: Connected to ${name} (${client.tools.length} tools)`));
|
|
4271
6468
|
} catch (err) {
|
|
4272
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
6526
|
+
console.log(chalk26.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
|
|
4312
6527
|
`));
|
|
4313
6528
|
} else {
|
|
4314
|
-
console.log(
|
|
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(
|
|
6550
|
+
console.log(chalk26.cyan(`> ${oneShot || "(piped input)"}
|
|
4336
6551
|
`));
|
|
4337
6552
|
if (references.length > 0) {
|
|
4338
|
-
console.log(
|
|
6553
|
+
console.log(chalk26.gray(` Injected ${references.length} reference(s)
|
|
4339
6554
|
`));
|
|
4340
6555
|
}
|
|
4341
|
-
const spinner =
|
|
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(
|
|
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 ?
|
|
4363
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
4453
|
-
console.log(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
4489
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
6752
|
+
console.log(chalk26.gray(" No file changes this session.\n"));
|
|
4534
6753
|
} else {
|
|
4535
6754
|
for (const df of diffs) {
|
|
4536
|
-
console.log(
|
|
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(
|
|
6771
|
+
console.log(chalk26.green(` Exported to ${ePath}
|
|
4553
6772
|
`));
|
|
4554
6773
|
} catch (err) {
|
|
4555
|
-
console.log(
|
|
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(
|
|
6784
|
+
console.log(chalk26.green(` Session saved: ${id}
|
|
4566
6785
|
`));
|
|
4567
6786
|
} catch (err) {
|
|
4568
|
-
console.log(
|
|
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(
|
|
6797
|
+
console.log(chalk26.gray(" No saved sessions.\n"));
|
|
4579
6798
|
} else {
|
|
4580
|
-
console.log(
|
|
6799
|
+
console.log(chalk26.gray("\n Saved sessions:\n"));
|
|
4581
6800
|
for (const s of sessions.slice(0, 10)) {
|
|
4582
|
-
console.log(
|
|
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(
|
|
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(
|
|
6824
|
+
console.log(chalk26.green(` Ralph plan restored (${savedPlan.tasks.length} tasks)
|
|
4606
6825
|
`));
|
|
4607
6826
|
}
|
|
4608
6827
|
} catch {
|
|
4609
6828
|
}
|
|
4610
|
-
console.log(
|
|
6829
|
+
console.log(chalk26.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
|
|
4611
6830
|
`));
|
|
4612
6831
|
} else {
|
|
4613
|
-
console.log(
|
|
6832
|
+
console.log(chalk26.gray(" No session found.\n"));
|
|
4614
6833
|
}
|
|
4615
6834
|
} catch (err) {
|
|
4616
|
-
console.log(
|
|
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(
|
|
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(
|
|
6861
|
+
console.log(chalk26.gray(` No matches for "${query}"
|
|
4643
6862
|
`));
|
|
4644
6863
|
} else {
|
|
4645
|
-
console.log(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
6913
|
+
console.log(chalk26.gray(` \u2192 ${name}`));
|
|
4695
6914
|
},
|
|
4696
6915
|
onToolResult: (_name, _result, isError) => {
|
|
4697
|
-
console.log(isError ?
|
|
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(
|
|
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(
|
|
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(
|
|
4724
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
6966
|
+
console.log(chalk26.green(`
|
|
4748
6967
|
Subagent #${agentId} finished:`));
|
|
4749
|
-
console.log(
|
|
6968
|
+
console.log(chalk26.gray(` ${result.slice(0, 200)}
|
|
4750
6969
|
`));
|
|
4751
6970
|
rl.prompt();
|
|
4752
6971
|
},
|
|
4753
6972
|
onError: (err) => {
|
|
4754
|
-
console.log(
|
|
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(
|
|
6986
|
+
console.log(chalk26.gray(" No saved memories.\n"));
|
|
4768
6987
|
} else {
|
|
4769
|
-
console.log(
|
|
6988
|
+
console.log(chalk26.gray(`
|
|
4770
6989
|
${memories.length} saved memories:
|
|
4771
6990
|
`));
|
|
4772
6991
|
for (const m of memories) {
|
|
4773
|
-
console.log(
|
|
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(
|
|
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(
|
|
7008
|
+
console.log(chalk26.gray(` No memories matching "${query}"
|
|
4790
7009
|
`));
|
|
4791
7010
|
} else {
|
|
4792
7011
|
for (const m of results) {
|
|
4793
|
-
console.log(
|
|
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(
|
|
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(
|
|
7029
|
+
console.log(chalk26.yellow(` Cleared ${memories.length} memories.
|
|
4811
7030
|
`));
|
|
4812
7031
|
} catch (err) {
|
|
4813
|
-
console.log(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
4853
|
-
onTaskComplete: (task) => console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
7113
|
+
console.log(chalk26.gray(" No conversation branches. Use /branch to fork.\n"));
|
|
4895
7114
|
} else {
|
|
4896
|
-
console.log(
|
|
7115
|
+
console.log(chalk26.gray("\n Branches:\n"));
|
|
4897
7116
|
for (const [name, msgs] of branches) {
|
|
4898
|
-
const marker = name === currentBranch ?
|
|
4899
|
-
console.log(
|
|
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
|
-
|
|
4937
|
-
|
|
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(
|
|
7206
|
+
console.log(chalk26.gray(` Injected ${references.length} reference(s)`));
|
|
4946
7207
|
}
|
|
4947
7208
|
messages.push({ role: "user", content: finalPrompt });
|
|
4948
|
-
const spinner =
|
|
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(
|
|
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 ?
|
|
4977
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
7289
|
+
console.log(chalk26.gray(" Tip: Rate limited. Wait a moment and try again.\n"));
|
|
5028
7290
|
} else {
|
|
5029
|
-
console.log(
|
|
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(
|
|
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(
|
|
7315
|
+
console.error(chalk26.red(" Usage: notch ralph plan <goal>"));
|
|
5054
7316
|
process.exit(1);
|
|
5055
7317
|
}
|
|
5056
|
-
const spinner =
|
|
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(
|
|
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(
|
|
5073
|
-
onTaskComplete: (t) => console.log(
|
|
5074
|
-
onTaskFail: (t, e) => console.log(
|
|
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(
|
|
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(
|
|
5087
|
-
console.error(
|
|
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(
|
|
7354
|
+
console.error(chalk26.red(`
|
|
5093
7355
|
Fatal: ${err.message}
|
|
5094
7356
|
`));
|
|
5095
7357
|
process.exit(1);
|