@freesyntax/notch-cli 0.4.7 → 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
CHANGED
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
autoCompress,
|
|
9
9
|
estimateTokens
|
|
10
10
|
} from "./chunk-MWM5TFY4.js";
|
|
11
|
+
import {
|
|
12
|
+
__require
|
|
13
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
11
14
|
|
|
12
15
|
// src/index.ts
|
|
13
16
|
import { Command } from "commander";
|
|
14
|
-
import
|
|
15
|
-
import
|
|
17
|
+
import chalk26 from "chalk";
|
|
18
|
+
import ora4 from "ora";
|
|
16
19
|
import * as readline from "readline";
|
|
17
20
|
import * as nodePath from "path";
|
|
18
21
|
|
|
@@ -863,7 +866,7 @@ var GITHUB_API = "https://api.github.com";
|
|
|
863
866
|
function getToken() {
|
|
864
867
|
return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || null;
|
|
865
868
|
}
|
|
866
|
-
async function ghFetch(
|
|
869
|
+
async function ghFetch(path25, opts2 = {}) {
|
|
867
870
|
const token = getToken();
|
|
868
871
|
const headers = {
|
|
869
872
|
"Accept": "application/vnd.github+json",
|
|
@@ -871,7 +874,7 @@ async function ghFetch(path19, opts2 = {}) {
|
|
|
871
874
|
...opts2.headers ?? {}
|
|
872
875
|
};
|
|
873
876
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
874
|
-
return fetch(`${GITHUB_API}${
|
|
877
|
+
return fetch(`${GITHUB_API}${path25}`, { ...opts2, headers });
|
|
875
878
|
}
|
|
876
879
|
var parameters9 = z9.object({
|
|
877
880
|
action: z9.enum([
|
|
@@ -1028,24 +1031,516 @@ var githubTool = {
|
|
|
1028
1031
|
execute
|
|
1029
1032
|
};
|
|
1030
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
|
+
|
|
1031
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
|
|
1032
1529
|
import { spawn } from "child_process";
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
constructor(config, serverName) {
|
|
1530
|
+
var StdioTransport = class {
|
|
1531
|
+
constructor(config, name) {
|
|
1036
1532
|
this.config = config;
|
|
1037
|
-
this.
|
|
1533
|
+
this.name = name;
|
|
1038
1534
|
}
|
|
1039
1535
|
process = null;
|
|
1040
1536
|
requestId = 0;
|
|
1041
1537
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
1042
1538
|
buffer = "";
|
|
1043
|
-
|
|
1044
|
-
_tools = [];
|
|
1045
|
-
/**
|
|
1046
|
-
* Start the MCP server and initialize the connection.
|
|
1047
|
-
*/
|
|
1539
|
+
name;
|
|
1048
1540
|
async connect() {
|
|
1541
|
+
if (!this.config.command) {
|
|
1542
|
+
throw new Error(`Stdio transport requires 'command' in config for server ${this.name}`);
|
|
1543
|
+
}
|
|
1049
1544
|
this.process = spawn(this.config.command, this.config.args ?? [], {
|
|
1050
1545
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1051
1546
|
env: { ...process.env, ...this.config.env },
|
|
@@ -1058,54 +1553,17 @@ var MCPClient = class {
|
|
|
1058
1553
|
});
|
|
1059
1554
|
this.process.on("error", (err) => {
|
|
1060
1555
|
for (const [id, pending] of this.pendingRequests) {
|
|
1061
|
-
pending.reject(new Error(`MCP server ${this.
|
|
1556
|
+
pending.reject(new Error(`MCP server ${this.name} error: ${err.message}`));
|
|
1062
1557
|
this.pendingRequests.delete(id);
|
|
1063
1558
|
}
|
|
1064
1559
|
});
|
|
1065
1560
|
this.process.on("exit", (code) => {
|
|
1066
1561
|
for (const [id, pending] of this.pendingRequests) {
|
|
1067
|
-
pending.reject(new Error(`MCP server ${this.
|
|
1562
|
+
pending.reject(new Error(`MCP server ${this.name} exited with code ${code}`));
|
|
1068
1563
|
this.pendingRequests.delete(id);
|
|
1069
1564
|
}
|
|
1070
1565
|
});
|
|
1071
|
-
await this.sendRequest("initialize", {
|
|
1072
|
-
protocolVersion: "2024-11-05",
|
|
1073
|
-
capabilities: {},
|
|
1074
|
-
clientInfo: { name: "notch-cli", version: "0.3.0" }
|
|
1075
|
-
});
|
|
1076
|
-
this.sendNotification("notifications/initialized", {});
|
|
1077
|
-
const result = await this.sendRequest("tools/list", {});
|
|
1078
|
-
this._tools = result.tools ?? [];
|
|
1079
|
-
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Get discovered tools from this server.
|
|
1082
|
-
*/
|
|
1083
|
-
get tools() {
|
|
1084
|
-
return this._tools;
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Check if the MCP server process is still alive.
|
|
1088
|
-
*/
|
|
1089
|
-
get isAlive() {
|
|
1090
|
-
return this.process !== null && this.process.exitCode === null && !this.process.killed;
|
|
1091
|
-
}
|
|
1092
|
-
/**
|
|
1093
|
-
* Call a tool on the MCP server. Auto-reconnects if the server has crashed.
|
|
1094
|
-
*/
|
|
1095
|
-
async callTool(name, args) {
|
|
1096
|
-
if (!this.isAlive) {
|
|
1097
|
-
try {
|
|
1098
|
-
await this.connect();
|
|
1099
|
-
} catch (err) {
|
|
1100
|
-
throw new Error(`MCP server ${this.serverName} is down and could not reconnect: ${err.message}`);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
const result = await this.sendRequest("tools/call", { name, arguments: args });
|
|
1104
|
-
return result;
|
|
1105
1566
|
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Disconnect from the MCP server.
|
|
1108
|
-
*/
|
|
1109
1567
|
disconnect() {
|
|
1110
1568
|
if (this.process) {
|
|
1111
1569
|
this.process.stdin?.end();
|
|
@@ -1114,15 +1572,13 @@ var MCPClient = class {
|
|
|
1114
1572
|
}
|
|
1115
1573
|
this.pendingRequests.clear();
|
|
1116
1574
|
}
|
|
1575
|
+
get isAlive() {
|
|
1576
|
+
return this.process !== null && this.process.exitCode === null && !this.process.killed;
|
|
1577
|
+
}
|
|
1117
1578
|
sendRequest(method, params) {
|
|
1118
1579
|
return new Promise((resolve2, reject) => {
|
|
1119
1580
|
const id = ++this.requestId;
|
|
1120
|
-
const msg = {
|
|
1121
|
-
jsonrpc: "2.0",
|
|
1122
|
-
id,
|
|
1123
|
-
method,
|
|
1124
|
-
params
|
|
1125
|
-
};
|
|
1581
|
+
const msg = { jsonrpc: "2.0", id, method, params };
|
|
1126
1582
|
this.pendingRequests.set(id, { resolve: resolve2, reject });
|
|
1127
1583
|
const data = JSON.stringify(msg);
|
|
1128
1584
|
const header = `Content-Length: ${Buffer.byteLength(data)}\r
|
|
@@ -1138,11 +1594,7 @@ var MCPClient = class {
|
|
|
1138
1594
|
});
|
|
1139
1595
|
}
|
|
1140
1596
|
sendNotification(method, params) {
|
|
1141
|
-
const msg = {
|
|
1142
|
-
jsonrpc: "2.0",
|
|
1143
|
-
method,
|
|
1144
|
-
params
|
|
1145
|
-
};
|
|
1597
|
+
const msg = { jsonrpc: "2.0", method, params };
|
|
1146
1598
|
const data = JSON.stringify(msg);
|
|
1147
1599
|
const header = `Content-Length: ${Buffer.byteLength(data)}\r
|
|
1148
1600
|
\r
|
|
@@ -1187,40 +1639,499 @@ var MCPClient = class {
|
|
|
1187
1639
|
}
|
|
1188
1640
|
}
|
|
1189
1641
|
};
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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}`);
|
|
1203
1685
|
}
|
|
1204
1686
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
+
};
|
|
1207
1721
|
|
|
1208
|
-
// src/
|
|
1209
|
-
var
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}
|
|
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
|
+
}
|
|
1224
2135
|
function buildToolMap(ctx) {
|
|
1225
2136
|
const map = {};
|
|
1226
2137
|
for (const t of getAllTools()) {
|
|
@@ -1229,8 +2140,10 @@ function buildToolMap(ctx) {
|
|
|
1229
2140
|
parameters: t.parameters,
|
|
1230
2141
|
execute: async (params) => {
|
|
1231
2142
|
if (ctx.checkPermission) {
|
|
2143
|
+
await ctx.runHook?.("permission-request", { tool: t.name, args: params });
|
|
1232
2144
|
const level = ctx.checkPermission(t.name, params);
|
|
1233
2145
|
if (level === "deny") {
|
|
2146
|
+
await ctx.runHook?.("permission-denied", { tool: t.name, args: params, reason: "denied-by-config" });
|
|
1234
2147
|
return {
|
|
1235
2148
|
content: `Permission denied: ${t.name} is not allowed by your permission config.`,
|
|
1236
2149
|
isError: true
|
|
@@ -1242,6 +2155,7 @@ function buildToolMap(ctx) {
|
|
|
1242
2155
|
`Tool ${t.name}(${paramSummary}) requires approval. Proceed?`
|
|
1243
2156
|
);
|
|
1244
2157
|
if (!confirmed) {
|
|
2158
|
+
await ctx.runHook?.("permission-denied", { tool: t.name, args: params, reason: "cancelled-by-user" });
|
|
1245
2159
|
return { content: "Cancelled by user.", isError: true };
|
|
1246
2160
|
}
|
|
1247
2161
|
}
|
|
@@ -1274,15 +2188,15 @@ function describeTools() {
|
|
|
1274
2188
|
}
|
|
1275
2189
|
|
|
1276
2190
|
// src/context/project-instructions.ts
|
|
1277
|
-
import
|
|
1278
|
-
import
|
|
1279
|
-
import
|
|
2191
|
+
import fs9 from "fs/promises";
|
|
2192
|
+
import path11 from "path";
|
|
2193
|
+
import os2 from "os";
|
|
1280
2194
|
var INSTRUCTION_FILES = [".notch.md", "NOTCH.md", ".notch/instructions.md"];
|
|
1281
2195
|
async function loadProjectInstructions(projectRoot) {
|
|
1282
2196
|
const sources = [];
|
|
1283
|
-
const homeDir =
|
|
2197
|
+
const homeDir = os2.homedir();
|
|
1284
2198
|
for (const file of INSTRUCTION_FILES) {
|
|
1285
|
-
const globalPath =
|
|
2199
|
+
const globalPath = path11.join(homeDir, file);
|
|
1286
2200
|
const content = await safeRead(globalPath);
|
|
1287
2201
|
if (content) {
|
|
1288
2202
|
sources.push({ path: globalPath, content, scope: "global" });
|
|
@@ -1290,7 +2204,7 @@ async function loadProjectInstructions(projectRoot) {
|
|
|
1290
2204
|
}
|
|
1291
2205
|
}
|
|
1292
2206
|
for (const file of INSTRUCTION_FILES) {
|
|
1293
|
-
const projectPath =
|
|
2207
|
+
const projectPath = path11.join(projectRoot, file);
|
|
1294
2208
|
const content = await safeRead(projectPath);
|
|
1295
2209
|
if (content) {
|
|
1296
2210
|
sources.push({ path: projectPath, content, scope: "project" });
|
|
@@ -1312,7 +2226,7 @@ ${sections.join("\n\n")}`;
|
|
|
1312
2226
|
}
|
|
1313
2227
|
async function safeRead(filePath) {
|
|
1314
2228
|
try {
|
|
1315
|
-
const content = await
|
|
2229
|
+
const content = await fs9.readFile(filePath, "utf-8");
|
|
1316
2230
|
return content.trim() || null;
|
|
1317
2231
|
} catch {
|
|
1318
2232
|
return null;
|
|
@@ -1320,19 +2234,19 @@ async function safeRead(filePath) {
|
|
|
1320
2234
|
}
|
|
1321
2235
|
|
|
1322
2236
|
// src/memory/store.ts
|
|
1323
|
-
import
|
|
1324
|
-
import
|
|
1325
|
-
import
|
|
1326
|
-
var MEMORY_DIR =
|
|
1327
|
-
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");
|
|
1328
2242
|
async function ensureDir() {
|
|
1329
|
-
await
|
|
2243
|
+
await fs10.mkdir(MEMORY_DIR, { recursive: true });
|
|
1330
2244
|
}
|
|
1331
2245
|
async function saveMemory(memory) {
|
|
1332
2246
|
await ensureDir();
|
|
1333
2247
|
const slug = memory.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
1334
2248
|
const filename = `${memory.type}_${slug}.md`;
|
|
1335
|
-
const filePath =
|
|
2249
|
+
const filePath = path12.join(MEMORY_DIR, filename);
|
|
1336
2250
|
const fileContent = [
|
|
1337
2251
|
"---",
|
|
1338
2252
|
`name: ${memory.name}`,
|
|
@@ -1343,18 +2257,18 @@ async function saveMemory(memory) {
|
|
|
1343
2257
|
"",
|
|
1344
2258
|
memory.content
|
|
1345
2259
|
].join("\n");
|
|
1346
|
-
await
|
|
2260
|
+
await fs10.writeFile(filePath, fileContent, "utf-8");
|
|
1347
2261
|
await updateIndex();
|
|
1348
2262
|
return filename;
|
|
1349
2263
|
}
|
|
1350
2264
|
async function loadMemories() {
|
|
1351
2265
|
await ensureDir();
|
|
1352
|
-
const files = await
|
|
2266
|
+
const files = await fs10.readdir(MEMORY_DIR);
|
|
1353
2267
|
const memories = [];
|
|
1354
2268
|
for (const file of files) {
|
|
1355
2269
|
if (!file.endsWith(".md") || file === "MEMORY.md") continue;
|
|
1356
2270
|
try {
|
|
1357
|
-
const content = await
|
|
2271
|
+
const content = await fs10.readFile(path12.join(MEMORY_DIR, file), "utf-8");
|
|
1358
2272
|
const memory = parseMemoryFile(content, file);
|
|
1359
2273
|
if (memory) memories.push(memory);
|
|
1360
2274
|
} catch {
|
|
@@ -1364,7 +2278,7 @@ async function loadMemories() {
|
|
|
1364
2278
|
}
|
|
1365
2279
|
async function deleteMemory(filename) {
|
|
1366
2280
|
try {
|
|
1367
|
-
await
|
|
2281
|
+
await fs10.unlink(path12.join(MEMORY_DIR, filename));
|
|
1368
2282
|
await updateIndex();
|
|
1369
2283
|
return true;
|
|
1370
2284
|
} catch {
|
|
@@ -1440,7 +2354,7 @@ async function updateIndex() {
|
|
|
1440
2354
|
}
|
|
1441
2355
|
lines.push("");
|
|
1442
2356
|
}
|
|
1443
|
-
await
|
|
2357
|
+
await fs10.writeFile(INDEX_FILE, lines.join("\n"), "utf-8");
|
|
1444
2358
|
}
|
|
1445
2359
|
|
|
1446
2360
|
// src/agent/loop.ts
|
|
@@ -1459,10 +2373,14 @@ async function runAgentLoop(messages, config) {
|
|
|
1459
2373
|
let totalCompletionTokens = 0;
|
|
1460
2374
|
let wasCompressed = false;
|
|
1461
2375
|
let history = [...messages];
|
|
2376
|
+
await config.toolContext.runHook?.("pre-compact", { messageCount: history.length });
|
|
1462
2377
|
history = await autoCompress(history, config.model, contextWindow, () => {
|
|
1463
2378
|
wasCompressed = true;
|
|
1464
2379
|
config.onCompress?.();
|
|
1465
2380
|
});
|
|
2381
|
+
if (wasCompressed) {
|
|
2382
|
+
await config.toolContext.runHook?.("post-compact", { messageCount: history.length });
|
|
2383
|
+
}
|
|
1466
2384
|
while (iterations < maxIter) {
|
|
1467
2385
|
iterations++;
|
|
1468
2386
|
const result = streamText({
|
|
@@ -1534,10 +2452,15 @@ async function runAgentLoop(messages, config) {
|
|
|
1534
2452
|
}))
|
|
1535
2453
|
});
|
|
1536
2454
|
if (iterations % 5 === 0) {
|
|
2455
|
+
const prevLen = history.length;
|
|
2456
|
+
await config.toolContext.runHook?.("pre-compact", { messageCount: prevLen });
|
|
1537
2457
|
history = await autoCompress(history, config.model, contextWindow, () => {
|
|
1538
2458
|
wasCompressed = true;
|
|
1539
2459
|
config.onCompress?.();
|
|
1540
2460
|
});
|
|
2461
|
+
if (history.length < prevLen) {
|
|
2462
|
+
await config.toolContext.runHook?.("post-compact", { messageCount: history.length });
|
|
2463
|
+
}
|
|
1541
2464
|
}
|
|
1542
2465
|
continue;
|
|
1543
2466
|
}
|
|
@@ -1620,8 +2543,8 @@ async function buildSystemPrompt(projectRoot, modelId) {
|
|
|
1620
2543
|
}
|
|
1621
2544
|
|
|
1622
2545
|
// src/agent/checkpoints.ts
|
|
1623
|
-
import
|
|
1624
|
-
import
|
|
2546
|
+
import fs11 from "fs/promises";
|
|
2547
|
+
import path13 from "path";
|
|
1625
2548
|
var CheckpointManager = class {
|
|
1626
2549
|
checkpoints = [];
|
|
1627
2550
|
nextId = 1;
|
|
@@ -1630,7 +2553,7 @@ var CheckpointManager = class {
|
|
|
1630
2553
|
async recordBefore(filePath) {
|
|
1631
2554
|
if (this.pendingFiles.has(filePath)) return;
|
|
1632
2555
|
try {
|
|
1633
|
-
const content = await
|
|
2556
|
+
const content = await fs11.readFile(filePath, "utf-8");
|
|
1634
2557
|
this.pendingFiles.set(filePath, content);
|
|
1635
2558
|
} catch {
|
|
1636
2559
|
this.pendingFiles.set(filePath, null);
|
|
@@ -1642,7 +2565,7 @@ var CheckpointManager = class {
|
|
|
1642
2565
|
for (const [filePath, before] of this.pendingFiles) {
|
|
1643
2566
|
let after = null;
|
|
1644
2567
|
try {
|
|
1645
|
-
after = await
|
|
2568
|
+
after = await fs11.readFile(filePath, "utf-8");
|
|
1646
2569
|
} catch {
|
|
1647
2570
|
}
|
|
1648
2571
|
files.push({ path: filePath, before, after });
|
|
@@ -1664,12 +2587,12 @@ var CheckpointManager = class {
|
|
|
1664
2587
|
for (const snap of checkpoint.files) {
|
|
1665
2588
|
if (snap.before === null) {
|
|
1666
2589
|
try {
|
|
1667
|
-
await
|
|
2590
|
+
await fs11.unlink(snap.path);
|
|
1668
2591
|
} catch {
|
|
1669
2592
|
}
|
|
1670
2593
|
} else {
|
|
1671
|
-
await
|
|
1672
|
-
await
|
|
2594
|
+
await fs11.mkdir(path13.dirname(snap.path), { recursive: true });
|
|
2595
|
+
await fs11.writeFile(snap.path, snap.before, "utf-8");
|
|
1673
2596
|
}
|
|
1674
2597
|
}
|
|
1675
2598
|
return checkpoint;
|
|
@@ -1701,8 +2624,8 @@ var CheckpointManager = class {
|
|
|
1701
2624
|
}
|
|
1702
2625
|
}
|
|
1703
2626
|
}
|
|
1704
|
-
return Array.from(fileMap.entries()).map(([
|
|
1705
|
-
path:
|
|
2627
|
+
return Array.from(fileMap.entries()).map(([path25, { before, after }]) => ({
|
|
2628
|
+
path: path25,
|
|
1706
2629
|
before,
|
|
1707
2630
|
after
|
|
1708
2631
|
}));
|
|
@@ -1710,7 +2633,7 @@ var CheckpointManager = class {
|
|
|
1710
2633
|
};
|
|
1711
2634
|
|
|
1712
2635
|
// src/agent/usage.ts
|
|
1713
|
-
import
|
|
2636
|
+
import chalk2 from "chalk";
|
|
1714
2637
|
var UsageTracker = class {
|
|
1715
2638
|
turns = [];
|
|
1716
2639
|
record(usage) {
|
|
@@ -1735,18 +2658,18 @@ var UsageTracker = class {
|
|
|
1735
2658
|
const last = this.turns[this.turns.length - 1];
|
|
1736
2659
|
if (!last) return "";
|
|
1737
2660
|
const t = last.totalTokens;
|
|
1738
|
-
const label = t > 1e4 ?
|
|
1739
|
-
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]`);
|
|
1740
2663
|
}
|
|
1741
2664
|
formatSession() {
|
|
1742
2665
|
const total = this.sessionTotal;
|
|
1743
2666
|
return [
|
|
1744
|
-
|
|
2667
|
+
chalk2.gray(`
|
|
1745
2668
|
Session usage (${this.turnCount} turns):`),
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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}`)
|
|
1750
2673
|
].join("\n");
|
|
1751
2674
|
}
|
|
1752
2675
|
};
|
|
@@ -1942,7 +2865,7 @@ function nextSubagentId(type) {
|
|
|
1942
2865
|
|
|
1943
2866
|
// src/agent/planner.ts
|
|
1944
2867
|
import { generateText as generateText2 } from "ai";
|
|
1945
|
-
import
|
|
2868
|
+
import chalk3 from "chalk";
|
|
1946
2869
|
var PLAN_SYSTEM_PROMPT = `You are a planning assistant. Given a task, produce a structured implementation plan.
|
|
1947
2870
|
|
|
1948
2871
|
Respond in EXACTLY this format (no other text):
|
|
@@ -2034,25 +2957,25 @@ function parsePlan(text) {
|
|
|
2034
2957
|
function formatPlan(plan) {
|
|
2035
2958
|
const lines = [];
|
|
2036
2959
|
lines.push("");
|
|
2037
|
-
lines.push(
|
|
2960
|
+
lines.push(chalk3.bold.white(` Plan: ${plan.summary}`));
|
|
2038
2961
|
lines.push("");
|
|
2039
2962
|
for (const step of plan.steps) {
|
|
2040
|
-
const icon = step.status === "done" ?
|
|
2041
|
-
const num =
|
|
2042
|
-
const action = step.status === "done" ?
|
|
2043
|
-
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(", ")}]`) : "";
|
|
2044
2967
|
lines.push(` ${icon} ${num} ${action}${files}`);
|
|
2045
2968
|
}
|
|
2046
2969
|
if (plan.risks.length > 0) {
|
|
2047
2970
|
lines.push("");
|
|
2048
|
-
lines.push(
|
|
2971
|
+
lines.push(chalk3.yellow(" Risks:"));
|
|
2049
2972
|
for (const risk of plan.risks) {
|
|
2050
|
-
lines.push(
|
|
2973
|
+
lines.push(chalk3.yellow(` \u26A0 ${risk}`));
|
|
2051
2974
|
}
|
|
2052
2975
|
}
|
|
2053
2976
|
lines.push("");
|
|
2054
2977
|
if (!plan.approved) {
|
|
2055
|
-
lines.push(
|
|
2978
|
+
lines.push(chalk3.gray(" Approve: /plan approve | Modify: /plan edit | Cancel: /plan cancel"));
|
|
2056
2979
|
}
|
|
2057
2980
|
lines.push("");
|
|
2058
2981
|
return lines.join("\n");
|
|
@@ -2083,7 +3006,7 @@ function isPlanComplete(plan) {
|
|
|
2083
3006
|
}
|
|
2084
3007
|
|
|
2085
3008
|
// src/agent/cost.ts
|
|
2086
|
-
import
|
|
3009
|
+
import chalk4 from "chalk";
|
|
2087
3010
|
var MODEL_COSTS = {
|
|
2088
3011
|
"notch-cinder": { input: 0.05, output: 0.15 },
|
|
2089
3012
|
// L4 — cheapest
|
|
@@ -2126,19 +3049,19 @@ var CostTracker = class {
|
|
|
2126
3049
|
formatLastCost() {
|
|
2127
3050
|
const last = this.entries[this.entries.length - 1];
|
|
2128
3051
|
if (!last) return "";
|
|
2129
|
-
return
|
|
3052
|
+
return chalk4.gray(`$${last.cost.toFixed(4)}`);
|
|
2130
3053
|
}
|
|
2131
3054
|
formatSession() {
|
|
2132
|
-
if (this.entries.length === 0) return
|
|
3055
|
+
if (this.entries.length === 0) return chalk4.gray(" No cost data.");
|
|
2133
3056
|
const total = this.totalCost;
|
|
2134
3057
|
const tokens = this.totalTokens;
|
|
2135
3058
|
return [
|
|
2136
|
-
|
|
3059
|
+
chalk4.gray(`
|
|
2137
3060
|
Session cost estimate:`),
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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)`)
|
|
2142
3065
|
].join("\n");
|
|
2143
3066
|
}
|
|
2144
3067
|
/** Alias for formatSession */
|
|
@@ -2157,10 +3080,10 @@ var CostTracker = class {
|
|
|
2157
3080
|
existing.turns += 1;
|
|
2158
3081
|
byModel.set(e.model, existing);
|
|
2159
3082
|
}
|
|
2160
|
-
const lines = [
|
|
3083
|
+
const lines = [chalk4.gray("\n Cost by model:")];
|
|
2161
3084
|
for (const [modelId, data] of byModel) {
|
|
2162
3085
|
lines.push(
|
|
2163
|
-
|
|
3086
|
+
chalk4.gray(` ${modelId.padEnd(14)} ${data.turns} turns `) + chalk4.white(`$${data.cost.toFixed(4)}`) + chalk4.gray(` (${(data.prompt + data.completion).toLocaleString()} tokens)`)
|
|
2164
3087
|
);
|
|
2165
3088
|
}
|
|
2166
3089
|
return lines.join("\n");
|
|
@@ -2168,14 +3091,14 @@ var CostTracker = class {
|
|
|
2168
3091
|
};
|
|
2169
3092
|
|
|
2170
3093
|
// src/agent/ralph.ts
|
|
2171
|
-
import
|
|
2172
|
-
import
|
|
2173
|
-
import
|
|
3094
|
+
import fs13 from "fs/promises";
|
|
3095
|
+
import path15 from "path";
|
|
3096
|
+
import chalk5 from "chalk";
|
|
2174
3097
|
import { generateText as generateText3, streamText as streamText3 } from "ai";
|
|
2175
3098
|
|
|
2176
3099
|
// src/context/repo-map.ts
|
|
2177
|
-
import
|
|
2178
|
-
import
|
|
3100
|
+
import fs12 from "fs/promises";
|
|
3101
|
+
import path14 from "path";
|
|
2179
3102
|
import { glob } from "glob";
|
|
2180
3103
|
var PATTERNS = {
|
|
2181
3104
|
ts: [
|
|
@@ -2206,7 +3129,7 @@ var PATTERNS = {
|
|
|
2206
3129
|
};
|
|
2207
3130
|
var IMPORT_PATTERN = /(?:import|from)\s+['"]([^'"]+)['"]/g;
|
|
2208
3131
|
function getPatterns(filePath) {
|
|
2209
|
-
const ext =
|
|
3132
|
+
const ext = path14.extname(filePath).slice(1);
|
|
2210
3133
|
if (["ts", "tsx", "mts", "cts"].includes(ext)) return PATTERNS.ts;
|
|
2211
3134
|
if (["js", "jsx", "mjs", "cjs"].includes(ext)) return PATTERNS.js;
|
|
2212
3135
|
if (ext === "py") return PATTERNS.py;
|
|
@@ -2257,9 +3180,9 @@ async function buildRepoMap(root) {
|
|
|
2257
3180
|
});
|
|
2258
3181
|
const entries = [];
|
|
2259
3182
|
for (const file of files.slice(0, 500)) {
|
|
2260
|
-
const fullPath =
|
|
3183
|
+
const fullPath = path14.resolve(root, file);
|
|
2261
3184
|
try {
|
|
2262
|
-
const content = await
|
|
3185
|
+
const content = await fs12.readFile(fullPath, "utf-8");
|
|
2263
3186
|
const lines = content.split("\n").length;
|
|
2264
3187
|
const patterns = getPatterns(file);
|
|
2265
3188
|
const symbols = extractSymbols(content, patterns);
|
|
@@ -2361,11 +3284,11 @@ ${repoContext || "(empty project)"}`;
|
|
|
2361
3284
|
async function savePlan(plan, cwd) {
|
|
2362
3285
|
plan.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2363
3286
|
plan.completedCount = plan.tasks.filter((t) => t.status === "done").length;
|
|
2364
|
-
await
|
|
3287
|
+
await fs13.writeFile(path15.join(cwd, PLAN_FILE), JSON.stringify(plan, null, 2), "utf-8");
|
|
2365
3288
|
}
|
|
2366
3289
|
async function loadPlan(cwd) {
|
|
2367
3290
|
try {
|
|
2368
|
-
const raw = await
|
|
3291
|
+
const raw = await fs13.readFile(path15.join(cwd, PLAN_FILE), "utf-8");
|
|
2369
3292
|
return JSON.parse(raw);
|
|
2370
3293
|
} catch {
|
|
2371
3294
|
return null;
|
|
@@ -2373,7 +3296,7 @@ async function loadPlan(cwd) {
|
|
|
2373
3296
|
}
|
|
2374
3297
|
async function deletePlan(cwd) {
|
|
2375
3298
|
try {
|
|
2376
|
-
await
|
|
3299
|
+
await fs13.unlink(path15.join(cwd, PLAN_FILE));
|
|
2377
3300
|
} catch {
|
|
2378
3301
|
}
|
|
2379
3302
|
}
|
|
@@ -2537,27 +3460,27 @@ function formatRalphStatus(plan) {
|
|
|
2537
3460
|
const progress = plan.totalCount > 0 ? Math.round(done / plan.totalCount * 100) : 0;
|
|
2538
3461
|
const barWidth = 25;
|
|
2539
3462
|
const filled = Math.round(done / plan.totalCount * barWidth);
|
|
2540
|
-
const bar =
|
|
3463
|
+
const bar = chalk5.green("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(barWidth - filled));
|
|
2541
3464
|
lines.push("");
|
|
2542
|
-
lines.push(
|
|
3465
|
+
lines.push(chalk5.bold.white(` Ralph Mode: ${plan.goal}`));
|
|
2543
3466
|
lines.push(` [${bar}] ${progress}% (${done}/${plan.totalCount} tasks)`);
|
|
2544
|
-
if (failed > 0) lines.push(
|
|
3467
|
+
if (failed > 0) lines.push(chalk5.red(` ${failed} failed`));
|
|
2545
3468
|
lines.push("");
|
|
2546
3469
|
for (const task of plan.tasks) {
|
|
2547
|
-
const icon = task.status === "done" ?
|
|
2548
|
-
const title = task.status === "done" ?
|
|
2549
|
-
const files = task.files.length > 0 ?
|
|
2550
|
-
const err = task.error ?
|
|
2551
|
-
const attempts = task.attempts > 1 ?
|
|
2552
|
-
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}`);
|
|
2553
3476
|
}
|
|
2554
3477
|
lines.push("");
|
|
2555
3478
|
return lines.join("\n");
|
|
2556
3479
|
}
|
|
2557
3480
|
|
|
2558
3481
|
// src/context/references.ts
|
|
2559
|
-
import
|
|
2560
|
-
import
|
|
3482
|
+
import fs14 from "fs/promises";
|
|
3483
|
+
import path16 from "path";
|
|
2561
3484
|
import { glob as glob2 } from "glob";
|
|
2562
3485
|
async function resolveReferences(input, cwd) {
|
|
2563
3486
|
const references = [];
|
|
@@ -2600,9 +3523,9 @@ ${truncated}
|
|
|
2600
3523
|
return sections.join("\n\n") + "\n\n";
|
|
2601
3524
|
}
|
|
2602
3525
|
async function resolveFile(ref, cwd) {
|
|
2603
|
-
const filePath =
|
|
3526
|
+
const filePath = path16.isAbsolute(ref) ? ref : path16.resolve(cwd, ref);
|
|
2604
3527
|
try {
|
|
2605
|
-
const content = await
|
|
3528
|
+
const content = await fs14.readFile(filePath, "utf-8");
|
|
2606
3529
|
const lines = content.split("\n");
|
|
2607
3530
|
const numbered = lines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
|
|
2608
3531
|
return {
|
|
@@ -2698,448 +3621,448 @@ async function resolveGlob(pattern, cwd) {
|
|
|
2698
3621
|
}
|
|
2699
3622
|
|
|
2700
3623
|
// src/ui/banner.ts
|
|
2701
|
-
import
|
|
3624
|
+
import chalk7 from "chalk";
|
|
2702
3625
|
|
|
2703
3626
|
// src/ui/themes.ts
|
|
2704
|
-
import
|
|
3627
|
+
import chalk6 from "chalk";
|
|
2705
3628
|
var defaultTheme = {
|
|
2706
3629
|
name: "Default",
|
|
2707
3630
|
description: "FreeSyntax \u2014 silver, white, monochrome",
|
|
2708
|
-
brand:
|
|
3631
|
+
brand: chalk6.hex("#D4D4D4"),
|
|
2709
3632
|
// silver (banner uses gradient override)
|
|
2710
|
-
mascot:
|
|
3633
|
+
mascot: chalk6.hex("#AAAAAA"),
|
|
2711
3634
|
// medium gray mantis
|
|
2712
|
-
mascotAccent:
|
|
3635
|
+
mascotAccent: chalk6.hex("#FFFFFF"),
|
|
2713
3636
|
// white eyes
|
|
2714
|
-
tagline:
|
|
3637
|
+
tagline: chalk6.hex("#777777"),
|
|
2715
3638
|
// muted gray
|
|
2716
|
-
prompt:
|
|
3639
|
+
prompt: chalk6.hex("#CCCCCC"),
|
|
2717
3640
|
// silver prompt
|
|
2718
|
-
border:
|
|
3641
|
+
border: chalk6.hex("#444444"),
|
|
2719
3642
|
// dark border
|
|
2720
|
-
dim:
|
|
3643
|
+
dim: chalk6.hex("#666666"),
|
|
2721
3644
|
// muted text
|
|
2722
|
-
text:
|
|
3645
|
+
text: chalk6.hex("#D4D4D4"),
|
|
2723
3646
|
// silver body text
|
|
2724
|
-
bold:
|
|
3647
|
+
bold: chalk6.hex("#FFFFFF").bold,
|
|
2725
3648
|
// pure white emphasis
|
|
2726
|
-
success:
|
|
2727
|
-
warning:
|
|
2728
|
-
error:
|
|
2729
|
-
info:
|
|
3649
|
+
success: chalk6.green,
|
|
3650
|
+
warning: chalk6.yellow,
|
|
3651
|
+
error: chalk6.red,
|
|
3652
|
+
info: chalk6.hex("#BBBBBB"),
|
|
2730
3653
|
// light gray info
|
|
2731
|
-
toolName:
|
|
2732
|
-
toolArgs:
|
|
2733
|
-
toolResult:
|
|
2734
|
-
diffAdd:
|
|
2735
|
-
diffRemove:
|
|
2736
|
-
diffHeader:
|
|
2737
|
-
mdH1:
|
|
2738
|
-
mdH2:
|
|
2739
|
-
mdH3:
|
|
2740
|
-
mdCode:
|
|
2741
|
-
mdInlineCode:
|
|
2742
|
-
mdLink:
|
|
2743
|
-
meterLow:
|
|
2744
|
-
meterMid:
|
|
2745
|
-
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
|
|
2746
3669
|
};
|
|
2747
3670
|
var interstellarTheme = {
|
|
2748
3671
|
name: "Interstellar",
|
|
2749
3672
|
description: "Deep space amber \u2014 Endurance control panels",
|
|
2750
|
-
brand:
|
|
3673
|
+
brand: chalk6.hex("#FFB347"),
|
|
2751
3674
|
// warm amber (instrument readout)
|
|
2752
|
-
mascot:
|
|
3675
|
+
mascot: chalk6.hex("#4A90D9"),
|
|
2753
3676
|
// pale blue (frozen clouds)
|
|
2754
|
-
mascotAccent:
|
|
3677
|
+
mascotAccent: chalk6.hex("#FFD700"),
|
|
2755
3678
|
// gold (Gargantua light)
|
|
2756
|
-
tagline:
|
|
3679
|
+
tagline: chalk6.hex("#708090"),
|
|
2757
3680
|
// slate gray (ship hull)
|
|
2758
|
-
prompt:
|
|
2759
|
-
border:
|
|
3681
|
+
prompt: chalk6.hex("#FFB347"),
|
|
3682
|
+
border: chalk6.hex("#3A3A3A"),
|
|
2760
3683
|
// dark hull metal
|
|
2761
|
-
dim:
|
|
2762
|
-
text:
|
|
3684
|
+
dim: chalk6.hex("#5C5C5C"),
|
|
3685
|
+
text: chalk6.hex("#D4D4D4"),
|
|
2763
3686
|
// cool white (display text)
|
|
2764
|
-
bold:
|
|
2765
|
-
success:
|
|
3687
|
+
bold: chalk6.hex("#FFFFFF").bold,
|
|
3688
|
+
success: chalk6.hex("#7CFC00"),
|
|
2766
3689
|
// "CASE: All systems nominal"
|
|
2767
|
-
warning:
|
|
3690
|
+
warning: chalk6.hex("#FFB347"),
|
|
2768
3691
|
// amber warning
|
|
2769
|
-
error:
|
|
3692
|
+
error: chalk6.hex("#FF4444"),
|
|
2770
3693
|
// critical alert
|
|
2771
|
-
info:
|
|
3694
|
+
info: chalk6.hex("#4A90D9"),
|
|
2772
3695
|
// blue data readout
|
|
2773
|
-
toolName:
|
|
2774
|
-
toolArgs:
|
|
2775
|
-
toolResult:
|
|
2776
|
-
diffAdd:
|
|
2777
|
-
diffRemove:
|
|
2778
|
-
diffHeader:
|
|
2779
|
-
mdH1:
|
|
2780
|
-
mdH2:
|
|
2781
|
-
mdH3:
|
|
2782
|
-
mdCode:
|
|
2783
|
-
mdInlineCode:
|
|
2784
|
-
mdLink:
|
|
2785
|
-
meterLow:
|
|
2786
|
-
meterMid:
|
|
2787
|
-
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")
|
|
2788
3711
|
};
|
|
2789
3712
|
var spaceGrayTheme = {
|
|
2790
3713
|
name: "Space Gray",
|
|
2791
3714
|
description: "Apple minimalism \u2014 silver, cool gray, ice blue",
|
|
2792
|
-
brand:
|
|
3715
|
+
brand: chalk6.hex("#A2AAAD"),
|
|
2793
3716
|
// apple silver
|
|
2794
|
-
mascot:
|
|
3717
|
+
mascot: chalk6.hex("#86868B"),
|
|
2795
3718
|
// space gray
|
|
2796
|
-
mascotAccent:
|
|
3719
|
+
mascotAccent: chalk6.hex("#0071E3"),
|
|
2797
3720
|
// apple blue
|
|
2798
|
-
tagline:
|
|
3721
|
+
tagline: chalk6.hex("#6E6E73"),
|
|
2799
3722
|
// secondary gray
|
|
2800
|
-
prompt:
|
|
2801
|
-
border:
|
|
3723
|
+
prompt: chalk6.hex("#A2AAAD"),
|
|
3724
|
+
border: chalk6.hex("#3A3A3C"),
|
|
2802
3725
|
// dark separator
|
|
2803
|
-
dim:
|
|
2804
|
-
text:
|
|
3726
|
+
dim: chalk6.hex("#6E6E73"),
|
|
3727
|
+
text: chalk6.hex("#E5E5EA"),
|
|
2805
3728
|
// system gray 6
|
|
2806
|
-
bold:
|
|
3729
|
+
bold: chalk6.hex("#F5F5F7").bold,
|
|
2807
3730
|
// near white
|
|
2808
|
-
success:
|
|
3731
|
+
success: chalk6.hex("#30D158"),
|
|
2809
3732
|
// apple green
|
|
2810
|
-
warning:
|
|
3733
|
+
warning: chalk6.hex("#FFD60A"),
|
|
2811
3734
|
// apple yellow
|
|
2812
|
-
error:
|
|
3735
|
+
error: chalk6.hex("#FF453A"),
|
|
2813
3736
|
// apple red
|
|
2814
|
-
info:
|
|
3737
|
+
info: chalk6.hex("#0A84FF"),
|
|
2815
3738
|
// apple blue
|
|
2816
|
-
toolName:
|
|
2817
|
-
toolArgs:
|
|
2818
|
-
toolResult:
|
|
2819
|
-
diffAdd:
|
|
2820
|
-
diffRemove:
|
|
2821
|
-
diffHeader:
|
|
2822
|
-
mdH1:
|
|
2823
|
-
mdH2:
|
|
2824
|
-
mdH3:
|
|
2825
|
-
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"),
|
|
2826
3749
|
// apple purple for code
|
|
2827
|
-
mdInlineCode:
|
|
2828
|
-
mdLink:
|
|
2829
|
-
meterLow:
|
|
2830
|
-
meterMid:
|
|
2831
|
-
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")
|
|
2832
3755
|
};
|
|
2833
3756
|
var cyberpunkTheme = {
|
|
2834
3757
|
name: "Cyberpunk",
|
|
2835
3758
|
description: "Neon pink & cyan \u2014 Night City terminal",
|
|
2836
|
-
brand:
|
|
3759
|
+
brand: chalk6.hex("#FF2079"),
|
|
2837
3760
|
// hot pink
|
|
2838
|
-
mascot:
|
|
3761
|
+
mascot: chalk6.hex("#00FFFF"),
|
|
2839
3762
|
// electric cyan
|
|
2840
|
-
mascotAccent:
|
|
3763
|
+
mascotAccent: chalk6.hex("#FF2079"),
|
|
2841
3764
|
// hot pink eyes
|
|
2842
|
-
tagline:
|
|
3765
|
+
tagline: chalk6.hex("#9D00FF"),
|
|
2843
3766
|
// deep violet
|
|
2844
|
-
prompt:
|
|
2845
|
-
border:
|
|
2846
|
-
dim:
|
|
2847
|
-
text:
|
|
2848
|
-
bold:
|
|
2849
|
-
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"),
|
|
2850
3773
|
// matrix green
|
|
2851
|
-
warning:
|
|
3774
|
+
warning: chalk6.hex("#FFFF00"),
|
|
2852
3775
|
// neon yellow
|
|
2853
|
-
error:
|
|
2854
|
-
info:
|
|
2855
|
-
toolName:
|
|
2856
|
-
toolArgs:
|
|
2857
|
-
toolResult:
|
|
2858
|
-
diffAdd:
|
|
2859
|
-
diffRemove:
|
|
2860
|
-
diffHeader:
|
|
2861
|
-
mdH1:
|
|
2862
|
-
mdH2:
|
|
2863
|
-
mdH3:
|
|
2864
|
-
mdCode:
|
|
2865
|
-
mdInlineCode:
|
|
2866
|
-
mdLink:
|
|
2867
|
-
meterLow:
|
|
2868
|
-
meterMid:
|
|
2869
|
-
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")
|
|
2870
3793
|
};
|
|
2871
3794
|
var auroraTheme = {
|
|
2872
3795
|
name: "Aurora",
|
|
2873
3796
|
description: "Northern lights \u2014 teal, violet, polar green",
|
|
2874
|
-
brand:
|
|
3797
|
+
brand: chalk6.hex("#7FFFD4"),
|
|
2875
3798
|
// aquamarine
|
|
2876
|
-
mascot:
|
|
3799
|
+
mascot: chalk6.hex("#00CED1"),
|
|
2877
3800
|
// dark turquoise
|
|
2878
|
-
mascotAccent:
|
|
3801
|
+
mascotAccent: chalk6.hex("#DA70D6"),
|
|
2879
3802
|
// orchid
|
|
2880
|
-
tagline:
|
|
3803
|
+
tagline: chalk6.hex("#9370DB"),
|
|
2881
3804
|
// medium purple
|
|
2882
|
-
prompt:
|
|
2883
|
-
border:
|
|
3805
|
+
prompt: chalk6.hex("#7FFFD4"),
|
|
3806
|
+
border: chalk6.hex("#2F4F4F"),
|
|
2884
3807
|
// dark slate
|
|
2885
|
-
dim:
|
|
3808
|
+
dim: chalk6.hex("#5F9EA0"),
|
|
2886
3809
|
// cadet blue
|
|
2887
|
-
text:
|
|
3810
|
+
text: chalk6.hex("#E0FFFF"),
|
|
2888
3811
|
// light cyan
|
|
2889
|
-
bold:
|
|
2890
|
-
success:
|
|
3812
|
+
bold: chalk6.hex("#F0FFFF").bold,
|
|
3813
|
+
success: chalk6.hex("#00FA9A"),
|
|
2891
3814
|
// medium spring green
|
|
2892
|
-
warning:
|
|
3815
|
+
warning: chalk6.hex("#DDA0DD"),
|
|
2893
3816
|
// plum
|
|
2894
|
-
error:
|
|
2895
|
-
info:
|
|
3817
|
+
error: chalk6.hex("#FF6B6B"),
|
|
3818
|
+
info: chalk6.hex("#87CEEB"),
|
|
2896
3819
|
// sky blue
|
|
2897
|
-
toolName:
|
|
2898
|
-
toolArgs:
|
|
2899
|
-
toolResult:
|
|
2900
|
-
diffAdd:
|
|
2901
|
-
diffRemove:
|
|
2902
|
-
diffHeader:
|
|
2903
|
-
mdH1:
|
|
2904
|
-
mdH2:
|
|
2905
|
-
mdH3:
|
|
2906
|
-
mdCode:
|
|
2907
|
-
mdInlineCode:
|
|
2908
|
-
mdLink:
|
|
2909
|
-
meterLow:
|
|
2910
|
-
meterMid:
|
|
2911
|
-
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")
|
|
2912
3835
|
};
|
|
2913
3836
|
var solarizedTheme = {
|
|
2914
3837
|
name: "Solarized",
|
|
2915
3838
|
description: "Solarized Dark \u2014 warm, precise, classic dev palette",
|
|
2916
|
-
brand:
|
|
3839
|
+
brand: chalk6.hex("#268BD2"),
|
|
2917
3840
|
// blue
|
|
2918
|
-
mascot:
|
|
3841
|
+
mascot: chalk6.hex("#859900"),
|
|
2919
3842
|
// green
|
|
2920
|
-
mascotAccent:
|
|
3843
|
+
mascotAccent: chalk6.hex("#B58900"),
|
|
2921
3844
|
// yellow
|
|
2922
|
-
tagline:
|
|
3845
|
+
tagline: chalk6.hex("#2AA198"),
|
|
2923
3846
|
// cyan
|
|
2924
|
-
prompt:
|
|
2925
|
-
border:
|
|
3847
|
+
prompt: chalk6.hex("#268BD2"),
|
|
3848
|
+
border: chalk6.hex("#073642"),
|
|
2926
3849
|
// base02
|
|
2927
|
-
dim:
|
|
3850
|
+
dim: chalk6.hex("#586E75"),
|
|
2928
3851
|
// base01
|
|
2929
|
-
text:
|
|
3852
|
+
text: chalk6.hex("#839496"),
|
|
2930
3853
|
// base0
|
|
2931
|
-
bold:
|
|
3854
|
+
bold: chalk6.hex("#93A1A1").bold,
|
|
2932
3855
|
// base1
|
|
2933
|
-
success:
|
|
2934
|
-
warning:
|
|
2935
|
-
error:
|
|
2936
|
-
info:
|
|
2937
|
-
toolName:
|
|
2938
|
-
toolArgs:
|
|
2939
|
-
toolResult:
|
|
2940
|
-
diffAdd:
|
|
2941
|
-
diffRemove:
|
|
2942
|
-
diffHeader:
|
|
2943
|
-
mdH1:
|
|
2944
|
-
mdH2:
|
|
2945
|
-
mdH3:
|
|
2946
|
-
mdCode:
|
|
2947
|
-
mdInlineCode:
|
|
2948
|
-
mdLink:
|
|
2949
|
-
meterLow:
|
|
2950
|
-
meterMid:
|
|
2951
|
-
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")
|
|
2952
3875
|
};
|
|
2953
3876
|
var draculaTheme = {
|
|
2954
3877
|
name: "Dracula",
|
|
2955
3878
|
description: "Dracula \u2014 purple, pink, green on charcoal",
|
|
2956
|
-
brand:
|
|
3879
|
+
brand: chalk6.hex("#BD93F9"),
|
|
2957
3880
|
// purple
|
|
2958
|
-
mascot:
|
|
3881
|
+
mascot: chalk6.hex("#50FA7B"),
|
|
2959
3882
|
// green
|
|
2960
|
-
mascotAccent:
|
|
3883
|
+
mascotAccent: chalk6.hex("#FF79C6"),
|
|
2961
3884
|
// pink
|
|
2962
|
-
tagline:
|
|
3885
|
+
tagline: chalk6.hex("#6272A4"),
|
|
2963
3886
|
// comment gray
|
|
2964
|
-
prompt:
|
|
2965
|
-
border:
|
|
3887
|
+
prompt: chalk6.hex("#BD93F9"),
|
|
3888
|
+
border: chalk6.hex("#44475A"),
|
|
2966
3889
|
// current line
|
|
2967
|
-
dim:
|
|
3890
|
+
dim: chalk6.hex("#6272A4"),
|
|
2968
3891
|
// comment
|
|
2969
|
-
text:
|
|
3892
|
+
text: chalk6.hex("#F8F8F2"),
|
|
2970
3893
|
// foreground
|
|
2971
|
-
bold:
|
|
2972
|
-
success:
|
|
2973
|
-
warning:
|
|
3894
|
+
bold: chalk6.hex("#F8F8F2").bold,
|
|
3895
|
+
success: chalk6.hex("#50FA7B"),
|
|
3896
|
+
warning: chalk6.hex("#F1FA8C"),
|
|
2974
3897
|
// yellow
|
|
2975
|
-
error:
|
|
2976
|
-
info:
|
|
3898
|
+
error: chalk6.hex("#FF5555"),
|
|
3899
|
+
info: chalk6.hex("#8BE9FD"),
|
|
2977
3900
|
// cyan
|
|
2978
|
-
toolName:
|
|
2979
|
-
toolArgs:
|
|
2980
|
-
toolResult:
|
|
2981
|
-
diffAdd:
|
|
2982
|
-
diffRemove:
|
|
2983
|
-
diffHeader:
|
|
2984
|
-
mdH1:
|
|
2985
|
-
mdH2:
|
|
2986
|
-
mdH3:
|
|
2987
|
-
mdCode:
|
|
2988
|
-
mdInlineCode:
|
|
2989
|
-
mdLink:
|
|
2990
|
-
meterLow:
|
|
2991
|
-
meterMid:
|
|
2992
|
-
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")
|
|
2993
3916
|
};
|
|
2994
3917
|
var monokaiTheme = {
|
|
2995
3918
|
name: "Monokai",
|
|
2996
3919
|
description: "Monokai Pro \u2014 vivid on dark, a classic",
|
|
2997
|
-
brand:
|
|
3920
|
+
brand: chalk6.hex("#A6E22E"),
|
|
2998
3921
|
// green
|
|
2999
|
-
mascot:
|
|
3922
|
+
mascot: chalk6.hex("#66D9EF"),
|
|
3000
3923
|
// blue
|
|
3001
|
-
mascotAccent:
|
|
3924
|
+
mascotAccent: chalk6.hex("#F92672"),
|
|
3002
3925
|
// pink
|
|
3003
|
-
tagline:
|
|
3926
|
+
tagline: chalk6.hex("#75715E"),
|
|
3004
3927
|
// comment
|
|
3005
|
-
prompt:
|
|
3006
|
-
border:
|
|
3007
|
-
dim:
|
|
3008
|
-
text:
|
|
3009
|
-
bold:
|
|
3010
|
-
success:
|
|
3011
|
-
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"),
|
|
3012
3935
|
// yellow
|
|
3013
|
-
error:
|
|
3014
|
-
info:
|
|
3015
|
-
toolName:
|
|
3016
|
-
toolArgs:
|
|
3017
|
-
toolResult:
|
|
3018
|
-
diffAdd:
|
|
3019
|
-
diffRemove:
|
|
3020
|
-
diffHeader:
|
|
3021
|
-
mdH1:
|
|
3022
|
-
mdH2:
|
|
3023
|
-
mdH3:
|
|
3024
|
-
mdCode:
|
|
3025
|
-
mdInlineCode:
|
|
3026
|
-
mdLink:
|
|
3027
|
-
meterLow:
|
|
3028
|
-
meterMid:
|
|
3029
|
-
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")
|
|
3030
3953
|
};
|
|
3031
3954
|
var oceanTheme = {
|
|
3032
3955
|
name: "Ocean",
|
|
3033
3956
|
description: "Deep sea \u2014 midnight blue, bioluminescent glow",
|
|
3034
|
-
brand:
|
|
3957
|
+
brand: chalk6.hex("#00B4D8"),
|
|
3035
3958
|
// cerulean
|
|
3036
|
-
mascot:
|
|
3959
|
+
mascot: chalk6.hex("#0077B6"),
|
|
3037
3960
|
// deep blue
|
|
3038
|
-
mascotAccent:
|
|
3961
|
+
mascotAccent: chalk6.hex("#90E0EF"),
|
|
3039
3962
|
// light blue (bioluminescent)
|
|
3040
|
-
tagline:
|
|
3963
|
+
tagline: chalk6.hex("#023E8A"),
|
|
3041
3964
|
// navy
|
|
3042
|
-
prompt:
|
|
3043
|
-
border:
|
|
3965
|
+
prompt: chalk6.hex("#00B4D8"),
|
|
3966
|
+
border: chalk6.hex("#03045E"),
|
|
3044
3967
|
// deep navy
|
|
3045
|
-
dim:
|
|
3046
|
-
text:
|
|
3968
|
+
dim: chalk6.hex("#0077B6"),
|
|
3969
|
+
text: chalk6.hex("#CAF0F8"),
|
|
3047
3970
|
// lightest blue
|
|
3048
|
-
bold:
|
|
3049
|
-
success:
|
|
3971
|
+
bold: chalk6.hex("#CAF0F8").bold,
|
|
3972
|
+
success: chalk6.hex("#2DC653"),
|
|
3050
3973
|
// kelp green
|
|
3051
|
-
warning:
|
|
3974
|
+
warning: chalk6.hex("#F4A261"),
|
|
3052
3975
|
// sandy
|
|
3053
|
-
error:
|
|
3976
|
+
error: chalk6.hex("#E76F51"),
|
|
3054
3977
|
// coral
|
|
3055
|
-
info:
|
|
3056
|
-
toolName:
|
|
3057
|
-
toolArgs:
|
|
3058
|
-
toolResult:
|
|
3059
|
-
diffAdd:
|
|
3060
|
-
diffRemove:
|
|
3061
|
-
diffHeader:
|
|
3062
|
-
mdH1:
|
|
3063
|
-
mdH2:
|
|
3064
|
-
mdH3:
|
|
3065
|
-
mdCode:
|
|
3066
|
-
mdInlineCode:
|
|
3067
|
-
mdLink:
|
|
3068
|
-
meterLow:
|
|
3069
|
-
meterMid:
|
|
3070
|
-
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")
|
|
3071
3994
|
};
|
|
3072
3995
|
var emberTheme = {
|
|
3073
3996
|
name: "Ember",
|
|
3074
3997
|
description: "Fire forged \u2014 charcoal, ember orange, flame red",
|
|
3075
|
-
brand:
|
|
3998
|
+
brand: chalk6.hex("#FF6B35"),
|
|
3076
3999
|
// flame orange
|
|
3077
|
-
mascot:
|
|
4000
|
+
mascot: chalk6.hex("#D62828"),
|
|
3078
4001
|
// deep red
|
|
3079
|
-
mascotAccent:
|
|
4002
|
+
mascotAccent: chalk6.hex("#FFD166"),
|
|
3080
4003
|
// bright flame
|
|
3081
|
-
tagline:
|
|
4004
|
+
tagline: chalk6.hex("#8B4513"),
|
|
3082
4005
|
// saddle brown
|
|
3083
|
-
prompt:
|
|
3084
|
-
border:
|
|
4006
|
+
prompt: chalk6.hex("#FF6B35"),
|
|
4007
|
+
border: chalk6.hex("#2B2B2B"),
|
|
3085
4008
|
// charcoal
|
|
3086
|
-
dim:
|
|
3087
|
-
text:
|
|
4009
|
+
dim: chalk6.hex("#6B3A2A"),
|
|
4010
|
+
text: chalk6.hex("#F4E4C1"),
|
|
3088
4011
|
// warm parchment
|
|
3089
|
-
bold:
|
|
3090
|
-
success:
|
|
4012
|
+
bold: chalk6.hex("#FFF5E1").bold,
|
|
4013
|
+
success: chalk6.hex("#FFD166"),
|
|
3091
4014
|
// bright flame = success
|
|
3092
|
-
warning:
|
|
3093
|
-
error:
|
|
3094
|
-
info:
|
|
4015
|
+
warning: chalk6.hex("#FF6B35"),
|
|
4016
|
+
error: chalk6.hex("#D62828"),
|
|
4017
|
+
info: chalk6.hex("#F4845F"),
|
|
3095
4018
|
// soft coral
|
|
3096
|
-
toolName:
|
|
3097
|
-
toolArgs:
|
|
3098
|
-
toolResult:
|
|
3099
|
-
diffAdd:
|
|
3100
|
-
diffRemove:
|
|
3101
|
-
diffHeader:
|
|
3102
|
-
mdH1:
|
|
3103
|
-
mdH2:
|
|
3104
|
-
mdH3:
|
|
3105
|
-
mdCode:
|
|
3106
|
-
mdInlineCode:
|
|
3107
|
-
mdLink:
|
|
3108
|
-
meterLow:
|
|
3109
|
-
meterMid:
|
|
3110
|
-
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")
|
|
3111
4034
|
};
|
|
3112
4035
|
var ghostTheme = {
|
|
3113
4036
|
name: "Ghost",
|
|
3114
4037
|
description: "Monochrome \u2014 pure grayscale minimalism",
|
|
3115
|
-
brand:
|
|
3116
|
-
mascot:
|
|
3117
|
-
mascotAccent:
|
|
3118
|
-
tagline:
|
|
3119
|
-
prompt:
|
|
3120
|
-
border:
|
|
3121
|
-
dim:
|
|
3122
|
-
text:
|
|
3123
|
-
bold:
|
|
3124
|
-
success:
|
|
3125
|
-
warning:
|
|
3126
|
-
error:
|
|
3127
|
-
info:
|
|
3128
|
-
toolName:
|
|
3129
|
-
toolArgs:
|
|
3130
|
-
toolResult:
|
|
3131
|
-
diffAdd:
|
|
3132
|
-
diffRemove:
|
|
3133
|
-
diffHeader:
|
|
3134
|
-
mdH1:
|
|
3135
|
-
mdH2:
|
|
3136
|
-
mdH3:
|
|
3137
|
-
mdCode:
|
|
3138
|
-
mdInlineCode:
|
|
3139
|
-
mdLink:
|
|
3140
|
-
meterLow:
|
|
3141
|
-
meterMid:
|
|
3142
|
-
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")
|
|
3143
4066
|
};
|
|
3144
4067
|
var THEME_CATALOG = {
|
|
3145
4068
|
"default": defaultTheme,
|
|
@@ -3178,11 +4101,11 @@ function formatThemeList(activeId) {
|
|
|
3178
4101
|
for (const id of THEME_IDS) {
|
|
3179
4102
|
const t = THEME_CATALOG[id];
|
|
3180
4103
|
const active = id === activeId ? t.success(" \u25CF") : " ";
|
|
3181
|
-
const name = id === activeId ? t.bold(t.name) :
|
|
3182
|
-
const desc =
|
|
3183
|
-
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}`);
|
|
3184
4107
|
}
|
|
3185
|
-
lines.push(
|
|
4108
|
+
lines.push(chalk6.gray(`
|
|
3186
4109
|
Switch with: /theme <name>
|
|
3187
4110
|
`));
|
|
3188
4111
|
return lines.join("\n");
|
|
@@ -3220,7 +4143,7 @@ function colorBannerLine(line, rowIndex) {
|
|
|
3220
4143
|
return t.brand(line);
|
|
3221
4144
|
}
|
|
3222
4145
|
const color = ROW_COLORS[rowIndex] ?? "#CCCCCC";
|
|
3223
|
-
return
|
|
4146
|
+
return chalk7.hex(color)(line);
|
|
3224
4147
|
}
|
|
3225
4148
|
var MANTIS = [
|
|
3226
4149
|
" \u2571\u25C9\u25C9\u2572",
|
|
@@ -3326,12 +4249,13 @@ function formatTokens(n) {
|
|
|
3326
4249
|
}
|
|
3327
4250
|
|
|
3328
4251
|
// src/ui/update-checker.ts
|
|
3329
|
-
import
|
|
3330
|
-
import
|
|
3331
|
-
import
|
|
3332
|
-
import
|
|
3333
|
-
|
|
3334
|
-
var
|
|
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");
|
|
4258
|
+
var CHECK_INTERVAL = 60 * 60 * 1e3;
|
|
3335
4259
|
var PACKAGE_NAME = "@freesyntax/notch-cli";
|
|
3336
4260
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
3337
4261
|
async function checkForUpdates(currentVersion) {
|
|
@@ -3339,13 +4263,12 @@ async function checkForUpdates(currentVersion) {
|
|
|
3339
4263
|
const cache = await loadCache();
|
|
3340
4264
|
if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL) {
|
|
3341
4265
|
if (cache.latestVersion && isNewer(cache.latestVersion, currentVersion)) {
|
|
3342
|
-
return
|
|
4266
|
+
return autoUpdate(currentVersion, cache.latestVersion);
|
|
3343
4267
|
}
|
|
3344
4268
|
return null;
|
|
3345
4269
|
}
|
|
3346
4270
|
const response = await fetch(REGISTRY_URL, {
|
|
3347
|
-
signal: AbortSignal.timeout(
|
|
3348
|
-
// Fast timeout — don't slow down startup
|
|
4271
|
+
signal: AbortSignal.timeout(5e3),
|
|
3349
4272
|
headers: { "Accept": "application/json" }
|
|
3350
4273
|
});
|
|
3351
4274
|
if (!response.ok) {
|
|
@@ -3356,13 +4279,34 @@ async function checkForUpdates(currentVersion) {
|
|
|
3356
4279
|
const latest = data.version;
|
|
3357
4280
|
await saveCache({ lastCheck: Date.now(), latestVersion: latest });
|
|
3358
4281
|
if (isNewer(latest, currentVersion)) {
|
|
3359
|
-
return
|
|
4282
|
+
return autoUpdate(currentVersion, latest);
|
|
3360
4283
|
}
|
|
3361
4284
|
return null;
|
|
3362
4285
|
} catch {
|
|
3363
4286
|
return null;
|
|
3364
4287
|
}
|
|
3365
4288
|
}
|
|
4289
|
+
function autoUpdate(current, latest) {
|
|
4290
|
+
console.log(chalk8.cyan(`
|
|
4291
|
+
\u2B06 Updating Notch CLI: ${current} \u2192 ${latest}...
|
|
4292
|
+
`));
|
|
4293
|
+
try {
|
|
4294
|
+
execSync4(`npm install -g ${PACKAGE_NAME}@${latest}`, {
|
|
4295
|
+
stdio: "inherit",
|
|
4296
|
+
timeout: 6e4
|
|
4297
|
+
});
|
|
4298
|
+
console.log(chalk8.green(`
|
|
4299
|
+
\u2713 Updated to v${latest}. Restarting...
|
|
4300
|
+
`));
|
|
4301
|
+
const args = process.argv.slice(2);
|
|
4302
|
+
const { spawnSync } = __require("child_process");
|
|
4303
|
+
const result = spawnSync("notch", args, { stdio: "inherit" });
|
|
4304
|
+
process.exit(result.status ?? 0);
|
|
4305
|
+
} catch {
|
|
4306
|
+
return chalk8.yellow(` Update available: ${current} \u2192 ${latest}. Run: npm install -g ${PACKAGE_NAME}@latest
|
|
4307
|
+
`);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
3366
4310
|
function isNewer(latest, current) {
|
|
3367
4311
|
const l = latest.split(".").map(Number);
|
|
3368
4312
|
const c = current.split(".").map(Number);
|
|
@@ -3372,13 +4316,9 @@ function isNewer(latest, current) {
|
|
|
3372
4316
|
}
|
|
3373
4317
|
return false;
|
|
3374
4318
|
}
|
|
3375
|
-
function formatUpdateMessage(current, latest) {
|
|
3376
|
-
return chalk7.yellow(` Update available: ${current} \u2192 ${latest}. Run: npm install -g ${PACKAGE_NAME}@latest
|
|
3377
|
-
`);
|
|
3378
|
-
}
|
|
3379
4319
|
async function loadCache() {
|
|
3380
4320
|
try {
|
|
3381
|
-
const raw = await
|
|
4321
|
+
const raw = await fs15.readFile(CACHE_FILE, "utf-8");
|
|
3382
4322
|
return JSON.parse(raw);
|
|
3383
4323
|
} catch {
|
|
3384
4324
|
return null;
|
|
@@ -3386,16 +4326,16 @@ async function loadCache() {
|
|
|
3386
4326
|
}
|
|
3387
4327
|
async function saveCache(cache) {
|
|
3388
4328
|
try {
|
|
3389
|
-
await
|
|
3390
|
-
await
|
|
4329
|
+
await fs15.mkdir(path17.dirname(CACHE_FILE), { recursive: true });
|
|
4330
|
+
await fs15.writeFile(CACHE_FILE, JSON.stringify(cache), "utf-8");
|
|
3391
4331
|
} catch {
|
|
3392
4332
|
}
|
|
3393
4333
|
}
|
|
3394
4334
|
|
|
3395
4335
|
// src/permissions/index.ts
|
|
3396
|
-
import
|
|
3397
|
-
import
|
|
3398
|
-
import
|
|
4336
|
+
import fs16 from "fs/promises";
|
|
4337
|
+
import path18 from "path";
|
|
4338
|
+
import os5 from "os";
|
|
3399
4339
|
var DEFAULT_PERMISSIONS = {
|
|
3400
4340
|
default: "prompt",
|
|
3401
4341
|
rules: [
|
|
@@ -3412,11 +4352,11 @@ var DEFAULT_PERMISSIONS = {
|
|
|
3412
4352
|
]
|
|
3413
4353
|
};
|
|
3414
4354
|
async function loadPermissions(projectRoot) {
|
|
3415
|
-
const projectPath =
|
|
3416
|
-
const globalPath =
|
|
4355
|
+
const projectPath = path18.join(projectRoot, ".notch.json");
|
|
4356
|
+
const globalPath = path18.join(os5.homedir(), ".notch", "permissions.json");
|
|
3417
4357
|
let config = { ...DEFAULT_PERMISSIONS };
|
|
3418
4358
|
try {
|
|
3419
|
-
const raw = await
|
|
4359
|
+
const raw = await fs16.readFile(globalPath, "utf-8");
|
|
3420
4360
|
const parsed = JSON.parse(raw);
|
|
3421
4361
|
if (parsed.permissions) {
|
|
3422
4362
|
config = mergePermissions(config, parsed.permissions);
|
|
@@ -3424,7 +4364,7 @@ async function loadPermissions(projectRoot) {
|
|
|
3424
4364
|
} catch {
|
|
3425
4365
|
}
|
|
3426
4366
|
try {
|
|
3427
|
-
const raw = await
|
|
4367
|
+
const raw = await fs16.readFile(projectPath, "utf-8");
|
|
3428
4368
|
const parsed = JSON.parse(raw);
|
|
3429
4369
|
if (parsed.permissions) {
|
|
3430
4370
|
config = mergePermissions(config, parsed.permissions);
|
|
@@ -3470,17 +4410,18 @@ function mergePermissions(base, override) {
|
|
|
3470
4410
|
}
|
|
3471
4411
|
|
|
3472
4412
|
// src/hooks/index.ts
|
|
3473
|
-
import { execSync as
|
|
3474
|
-
import
|
|
3475
|
-
import
|
|
3476
|
-
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";
|
|
3477
4418
|
import crypto from "crypto";
|
|
3478
|
-
var TRUST_STORE_PATH =
|
|
4419
|
+
var TRUST_STORE_PATH = path19.join(os6.homedir(), ".notch", "trusted-projects.json");
|
|
3479
4420
|
async function isTrustedProject(projectRoot, raw) {
|
|
3480
4421
|
const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
|
|
3481
|
-
const key =
|
|
4422
|
+
const key = path19.resolve(projectRoot);
|
|
3482
4423
|
try {
|
|
3483
|
-
const store = JSON.parse(await
|
|
4424
|
+
const store = JSON.parse(await fs17.readFile(TRUST_STORE_PATH, "utf-8"));
|
|
3484
4425
|
return store[key] === fingerprint;
|
|
3485
4426
|
} catch {
|
|
3486
4427
|
return false;
|
|
@@ -3488,30 +4429,30 @@ async function isTrustedProject(projectRoot, raw) {
|
|
|
3488
4429
|
}
|
|
3489
4430
|
async function trustProject(projectRoot, raw) {
|
|
3490
4431
|
const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
|
|
3491
|
-
const key =
|
|
4432
|
+
const key = path19.resolve(projectRoot);
|
|
3492
4433
|
let store = {};
|
|
3493
4434
|
try {
|
|
3494
|
-
store = JSON.parse(await
|
|
4435
|
+
store = JSON.parse(await fs17.readFile(TRUST_STORE_PATH, "utf-8"));
|
|
3495
4436
|
} catch {
|
|
3496
4437
|
}
|
|
3497
4438
|
store[key] = fingerprint;
|
|
3498
|
-
await
|
|
3499
|
-
await
|
|
4439
|
+
await fs17.mkdir(path19.dirname(TRUST_STORE_PATH), { recursive: true });
|
|
4440
|
+
await fs17.writeFile(TRUST_STORE_PATH, JSON.stringify(store, null, 2));
|
|
3500
4441
|
}
|
|
3501
4442
|
async function loadHooks(projectRoot, promptTrust) {
|
|
3502
4443
|
const hooks = [];
|
|
3503
|
-
const globalPath =
|
|
4444
|
+
const globalPath = path19.join(os6.homedir(), ".notch", "hooks.json");
|
|
3504
4445
|
try {
|
|
3505
|
-
const raw = await
|
|
4446
|
+
const raw = await fs17.readFile(globalPath, "utf-8");
|
|
3506
4447
|
const parsed = JSON.parse(raw);
|
|
3507
4448
|
if (Array.isArray(parsed.hooks)) {
|
|
3508
4449
|
hooks.push(...parsed.hooks);
|
|
3509
4450
|
}
|
|
3510
4451
|
} catch {
|
|
3511
4452
|
}
|
|
3512
|
-
const projectPath =
|
|
4453
|
+
const projectPath = path19.join(projectRoot, ".notch.json");
|
|
3513
4454
|
try {
|
|
3514
|
-
const raw = await
|
|
4455
|
+
const raw = await fs17.readFile(projectPath, "utf-8");
|
|
3515
4456
|
const parsed = JSON.parse(raw);
|
|
3516
4457
|
if (Array.isArray(parsed.hooks) && parsed.hooks.length > 0) {
|
|
3517
4458
|
const alreadyTrusted = await isTrustedProject(projectRoot, raw);
|
|
@@ -3533,7 +4474,8 @@ async function loadHooks(projectRoot, promptTrust) {
|
|
|
3533
4474
|
return { hooks };
|
|
3534
4475
|
}
|
|
3535
4476
|
async function runHooks(config, event, context) {
|
|
3536
|
-
const
|
|
4477
|
+
const allHooks = [...config.hooks, ...pluginManager.getHooks()];
|
|
4478
|
+
const matching = allHooks.filter((h) => {
|
|
3537
4479
|
if (h.event !== event) return false;
|
|
3538
4480
|
if (h.tool && h.tool !== context.tool) return false;
|
|
3539
4481
|
if (h.pattern && context.file && !context.file.includes(h.pattern)) return false;
|
|
@@ -3576,7 +4518,7 @@ async function executeHook(hook, context) {
|
|
|
3576
4518
|
NOTCH_CWD: context.cwd
|
|
3577
4519
|
};
|
|
3578
4520
|
try {
|
|
3579
|
-
const output =
|
|
4521
|
+
const output = execSync5(hook.command, {
|
|
3580
4522
|
cwd: context.cwd,
|
|
3581
4523
|
encoding: "utf-8",
|
|
3582
4524
|
timeout: hook.timeout ?? 1e4,
|
|
@@ -3593,18 +4535,48 @@ async function executeHook(hook, context) {
|
|
|
3593
4535
|
};
|
|
3594
4536
|
}
|
|
3595
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
|
+
}
|
|
3596
4568
|
|
|
3597
4569
|
// src/session/index.ts
|
|
3598
|
-
import
|
|
3599
|
-
import
|
|
3600
|
-
import
|
|
3601
|
-
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");
|
|
3602
4574
|
var MAX_SESSIONS = 20;
|
|
3603
4575
|
async function ensureDir2() {
|
|
3604
|
-
await
|
|
4576
|
+
await fs18.mkdir(SESSION_DIR, { recursive: true });
|
|
3605
4577
|
}
|
|
3606
4578
|
function sessionPath(id) {
|
|
3607
|
-
return
|
|
4579
|
+
return path20.join(SESSION_DIR, `${id}.json`);
|
|
3608
4580
|
}
|
|
3609
4581
|
function generateId() {
|
|
3610
4582
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -3631,13 +4603,13 @@ async function saveSession(messages, project, model, existingId) {
|
|
|
3631
4603
|
},
|
|
3632
4604
|
messages
|
|
3633
4605
|
};
|
|
3634
|
-
await
|
|
4606
|
+
await fs18.writeFile(sessionPath(id), JSON.stringify(session, null, 2), "utf-8");
|
|
3635
4607
|
await pruneOldSessions();
|
|
3636
4608
|
return id;
|
|
3637
4609
|
}
|
|
3638
4610
|
async function loadSession(id) {
|
|
3639
4611
|
try {
|
|
3640
|
-
const raw = await
|
|
4612
|
+
const raw = await fs18.readFile(sessionPath(id), "utf-8");
|
|
3641
4613
|
return JSON.parse(raw);
|
|
3642
4614
|
} catch {
|
|
3643
4615
|
return null;
|
|
@@ -3645,12 +4617,12 @@ async function loadSession(id) {
|
|
|
3645
4617
|
}
|
|
3646
4618
|
async function listSessions() {
|
|
3647
4619
|
await ensureDir2();
|
|
3648
|
-
const files = await
|
|
4620
|
+
const files = await fs18.readdir(SESSION_DIR);
|
|
3649
4621
|
const sessions = [];
|
|
3650
4622
|
for (const file of files) {
|
|
3651
4623
|
if (!file.endsWith(".json")) continue;
|
|
3652
4624
|
try {
|
|
3653
|
-
const raw = await
|
|
4625
|
+
const raw = await fs18.readFile(path20.join(SESSION_DIR, file), "utf-8");
|
|
3654
4626
|
const session = JSON.parse(raw);
|
|
3655
4627
|
sessions.push(session.meta);
|
|
3656
4628
|
} catch {
|
|
@@ -3666,7 +4638,7 @@ async function loadLastSession(project) {
|
|
|
3666
4638
|
}
|
|
3667
4639
|
async function deleteSession(id) {
|
|
3668
4640
|
try {
|
|
3669
|
-
await
|
|
4641
|
+
await fs18.unlink(sessionPath(id));
|
|
3670
4642
|
return true;
|
|
3671
4643
|
} catch {
|
|
3672
4644
|
return false;
|
|
@@ -3720,14 +4692,14 @@ async function exportSession(messages, outputPath, meta) {
|
|
|
3720
4692
|
lines.push("");
|
|
3721
4693
|
}
|
|
3722
4694
|
}
|
|
3723
|
-
await
|
|
4695
|
+
await fs18.writeFile(outputPath, lines.join("\n"), "utf-8");
|
|
3724
4696
|
return outputPath;
|
|
3725
4697
|
}
|
|
3726
4698
|
|
|
3727
4699
|
// src/init.ts
|
|
3728
|
-
import
|
|
3729
|
-
import
|
|
3730
|
-
import
|
|
4700
|
+
import fs19 from "fs/promises";
|
|
4701
|
+
import path21 from "path";
|
|
4702
|
+
import chalk9 from "chalk";
|
|
3731
4703
|
var DEFAULT_CONFIG = {
|
|
3732
4704
|
model: "notch-forge",
|
|
3733
4705
|
temperature: 0.3,
|
|
@@ -3758,47 +4730,47 @@ var DEFAULT_INSTRUCTIONS = `# Project Instructions for Notch
|
|
|
3758
4730
|
<!-- Files or areas Notch should NOT modify -->
|
|
3759
4731
|
`;
|
|
3760
4732
|
async function initProject(projectRoot) {
|
|
3761
|
-
const configPath =
|
|
3762
|
-
const instructionsPath =
|
|
4733
|
+
const configPath = path21.join(projectRoot, ".notch.json");
|
|
4734
|
+
const instructionsPath = path21.join(projectRoot, ".notch.md");
|
|
3763
4735
|
let configExists = false;
|
|
3764
4736
|
let instructionsExist = false;
|
|
3765
4737
|
try {
|
|
3766
|
-
await
|
|
4738
|
+
await fs19.access(configPath);
|
|
3767
4739
|
configExists = true;
|
|
3768
4740
|
} catch {
|
|
3769
4741
|
}
|
|
3770
4742
|
try {
|
|
3771
|
-
await
|
|
4743
|
+
await fs19.access(instructionsPath);
|
|
3772
4744
|
instructionsExist = true;
|
|
3773
4745
|
} catch {
|
|
3774
4746
|
}
|
|
3775
4747
|
if (!configExists) {
|
|
3776
|
-
await
|
|
3777
|
-
console.log(
|
|
4748
|
+
await fs19.writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf-8");
|
|
4749
|
+
console.log(chalk9.green(` Created ${configPath}`));
|
|
3778
4750
|
} else {
|
|
3779
|
-
console.log(
|
|
4751
|
+
console.log(chalk9.gray(` Skipped ${configPath} (already exists)`));
|
|
3780
4752
|
}
|
|
3781
4753
|
if (!instructionsExist) {
|
|
3782
|
-
await
|
|
3783
|
-
console.log(
|
|
4754
|
+
await fs19.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS, "utf-8");
|
|
4755
|
+
console.log(chalk9.green(` Created ${instructionsPath}`));
|
|
3784
4756
|
} else {
|
|
3785
|
-
console.log(
|
|
4757
|
+
console.log(chalk9.gray(` Skipped ${instructionsPath} (already exists)`));
|
|
3786
4758
|
}
|
|
3787
|
-
const gitignorePath =
|
|
4759
|
+
const gitignorePath = path21.join(projectRoot, ".gitignore");
|
|
3788
4760
|
try {
|
|
3789
|
-
const gitignore = await
|
|
4761
|
+
const gitignore = await fs19.readFile(gitignorePath, "utf-8");
|
|
3790
4762
|
const additions = [];
|
|
3791
4763
|
if (!gitignore.includes(".notch.json")) additions.push(".notch.json");
|
|
3792
4764
|
if (additions.length > 0) {
|
|
3793
4765
|
const append = "\n# Notch CLI\n" + additions.join("\n") + "\n";
|
|
3794
|
-
await
|
|
3795
|
-
console.log(
|
|
4766
|
+
await fs19.appendFile(gitignorePath, append, "utf-8");
|
|
4767
|
+
console.log(chalk9.green(` Updated .gitignore`));
|
|
3796
4768
|
}
|
|
3797
4769
|
} catch {
|
|
3798
4770
|
}
|
|
3799
4771
|
console.log("");
|
|
3800
|
-
console.log(
|
|
3801
|
-
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'));
|
|
3802
4774
|
}
|
|
3803
4775
|
|
|
3804
4776
|
// src/tools/diff-preview.ts
|
|
@@ -3917,9 +4889,1198 @@ function findSync(oldLines, newLines, oi, ni, lookAhead) {
|
|
|
3917
4889
|
return null;
|
|
3918
4890
|
}
|
|
3919
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
|
+
|
|
3920
6081
|
// src/ui/completions.ts
|
|
3921
|
-
import
|
|
3922
|
-
import
|
|
6082
|
+
import fs22 from "fs";
|
|
6083
|
+
import path24 from "path";
|
|
3923
6084
|
var COMMANDS = [
|
|
3924
6085
|
"/help",
|
|
3925
6086
|
"/quit",
|
|
@@ -3949,7 +6110,39 @@ var COMMANDS = [
|
|
|
3949
6110
|
"/mascot",
|
|
3950
6111
|
"/cost",
|
|
3951
6112
|
"/branch",
|
|
3952
|
-
"/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"
|
|
3953
6146
|
];
|
|
3954
6147
|
function buildCompleter(cwd) {
|
|
3955
6148
|
return function completer(line) {
|
|
@@ -3980,15 +6173,15 @@ function buildCompleter(cwd) {
|
|
|
3980
6173
|
}
|
|
3981
6174
|
function completeFilePath(partial, cwd) {
|
|
3982
6175
|
try {
|
|
3983
|
-
const dir = partial.includes("/") ?
|
|
3984
|
-
const prefix = partial.includes("/") ?
|
|
3985
|
-
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 });
|
|
3986
6179
|
const matches = [];
|
|
3987
6180
|
for (const entry of entries) {
|
|
3988
6181
|
if (entry.name.startsWith(".")) continue;
|
|
3989
6182
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
3990
6183
|
if (entry.name.startsWith(prefix)) {
|
|
3991
|
-
const relative = partial.includes("/") ?
|
|
6184
|
+
const relative = partial.includes("/") ? path24.dirname(partial) + "/" + entry.name : entry.name;
|
|
3992
6185
|
if (entry.isDirectory()) {
|
|
3993
6186
|
matches.push(relative + "/");
|
|
3994
6187
|
} else {
|
|
@@ -4003,7 +6196,7 @@ function completeFilePath(partial, cwd) {
|
|
|
4003
6196
|
}
|
|
4004
6197
|
|
|
4005
6198
|
// src/index.ts
|
|
4006
|
-
import
|
|
6199
|
+
import fs23 from "fs/promises";
|
|
4007
6200
|
import { createRequire } from "module";
|
|
4008
6201
|
var _require = createRequire(import.meta.url);
|
|
4009
6202
|
var VERSION = _require("../package.json").version;
|
|
@@ -4026,7 +6219,7 @@ function printModelTable(activeModel) {
|
|
|
4026
6219
|
`));
|
|
4027
6220
|
}
|
|
4028
6221
|
function printHelp() {
|
|
4029
|
-
console.log(
|
|
6222
|
+
console.log(chalk26.gray(`
|
|
4030
6223
|
Commands:
|
|
4031
6224
|
/model \u2014 Show available models
|
|
4032
6225
|
/model <name> \u2014 Switch model (e.g., /model pyre)
|
|
@@ -4060,6 +6253,30 @@ function printHelp() {
|
|
|
4060
6253
|
/ralph status \u2014 Show plan progress
|
|
4061
6254
|
/ralph clear \u2014 Discard plan
|
|
4062
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
|
+
|
|
4063
6280
|
Other:
|
|
4064
6281
|
/branch \u2014 Fork conversation at this point
|
|
4065
6282
|
/branches \u2014 List conversation branches
|
|
@@ -4095,17 +6312,17 @@ async function readStdin() {
|
|
|
4095
6312
|
}
|
|
4096
6313
|
async function main() {
|
|
4097
6314
|
if (promptArgs[0] === "login") {
|
|
4098
|
-
const spinner =
|
|
6315
|
+
const spinner = ora4("Opening browser...").start();
|
|
4099
6316
|
try {
|
|
4100
6317
|
spinner.stop();
|
|
4101
6318
|
const creds = await login();
|
|
4102
|
-
console.log(
|
|
6319
|
+
console.log(chalk26.green(`
|
|
4103
6320
|
\u2713 Signed in as ${creds.email}`));
|
|
4104
|
-
console.log(
|
|
6321
|
+
console.log(chalk26.gray(` API key stored in ${(await import("./auth-S3FIB42I.js")).getCredentialsPath()}
|
|
4105
6322
|
`));
|
|
4106
6323
|
} catch (err) {
|
|
4107
6324
|
spinner.stop();
|
|
4108
|
-
console.error(
|
|
6325
|
+
console.error(chalk26.red(`
|
|
4109
6326
|
Login failed: ${err.message}
|
|
4110
6327
|
`));
|
|
4111
6328
|
process.exit(1);
|
|
@@ -4115,10 +6332,10 @@ async function main() {
|
|
|
4115
6332
|
if (promptArgs[0] === "logout") {
|
|
4116
6333
|
const creds = await loadCredentials();
|
|
4117
6334
|
if (!creds) {
|
|
4118
|
-
console.log(
|
|
6335
|
+
console.log(chalk26.gray("\n Not signed in.\n"));
|
|
4119
6336
|
} else {
|
|
4120
6337
|
await clearCredentials();
|
|
4121
|
-
console.log(
|
|
6338
|
+
console.log(chalk26.green(`
|
|
4122
6339
|
\u2713 Signed out (${creds.email})
|
|
4123
6340
|
`));
|
|
4124
6341
|
}
|
|
@@ -4127,13 +6344,13 @@ async function main() {
|
|
|
4127
6344
|
if (promptArgs[0] === "whoami") {
|
|
4128
6345
|
const creds = await loadCredentials();
|
|
4129
6346
|
if (!creds) {
|
|
4130
|
-
console.log(
|
|
6347
|
+
console.log(chalk26.gray("\n Not signed in. Run: notch login\n"));
|
|
4131
6348
|
} else {
|
|
4132
6349
|
const keyPreview = `${creds.token.slice(0, 12)}...`;
|
|
4133
|
-
console.log(
|
|
4134
|
-
Signed in as ${
|
|
4135
|
-
console.log(
|
|
4136
|
-
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()}
|
|
4137
6354
|
`));
|
|
4138
6355
|
}
|
|
4139
6356
|
return;
|
|
@@ -4157,8 +6374,8 @@ async function main() {
|
|
|
4157
6374
|
const config = await loadConfig(configOverrides);
|
|
4158
6375
|
if (opts.model) {
|
|
4159
6376
|
if (!isValidModel(opts.model)) {
|
|
4160
|
-
console.error(
|
|
4161
|
-
console.error(
|
|
6377
|
+
console.error(chalk26.red(` Unknown model: ${opts.model}`));
|
|
6378
|
+
console.error(chalk26.gray(` Available: ${modelChoices}`));
|
|
4162
6379
|
process.exit(1);
|
|
4163
6380
|
}
|
|
4164
6381
|
config.models.chat.model = opts.model;
|
|
@@ -4194,15 +6411,14 @@ async function main() {
|
|
|
4194
6411
|
}
|
|
4195
6412
|
const info = MODEL_CATALOG[activeModelId];
|
|
4196
6413
|
printBanner(VERSION, info.label, info.id, info.size, config.projectRoot);
|
|
4197
|
-
checkForUpdates(VERSION)
|
|
4198
|
-
|
|
4199
|
-
});
|
|
6414
|
+
const updateMsg = await checkForUpdates(VERSION);
|
|
6415
|
+
if (updateMsg) console.log(updateMsg);
|
|
4200
6416
|
const hookTrustPrompt = async (commands) => {
|
|
4201
|
-
console.warn(
|
|
4202
|
-
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}`)));
|
|
4203
6419
|
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4204
6420
|
return new Promise((resolve2) => {
|
|
4205
|
-
rl2.question(
|
|
6421
|
+
rl2.question(chalk26.yellow("\nAllow these hooks for this project? [y/N] "), (answer) => {
|
|
4206
6422
|
rl2.close();
|
|
4207
6423
|
resolve2(answer.trim().toLowerCase() === "y");
|
|
4208
6424
|
});
|
|
@@ -4214,7 +6430,7 @@ async function main() {
|
|
|
4214
6430
|
]);
|
|
4215
6431
|
let repoMapStr = "";
|
|
4216
6432
|
if (config.useRepoMap) {
|
|
4217
|
-
const spinner =
|
|
6433
|
+
const spinner = ora4("Mapping repository...").start();
|
|
4218
6434
|
try {
|
|
4219
6435
|
const repoMap = await buildRepoMap(config.projectRoot);
|
|
4220
6436
|
repoMapStr = condensedRepoMap(repoMap);
|
|
@@ -4241,20 +6457,30 @@ ${repoMapStr}` : ""
|
|
|
4241
6457
|
const costTracker = new CostTracker();
|
|
4242
6458
|
const mcpClients = [];
|
|
4243
6459
|
try {
|
|
4244
|
-
const configRaw = await
|
|
6460
|
+
const configRaw = await fs23.readFile(nodePath.resolve(config.projectRoot, ".notch.json"), "utf-8").catch(() => "{}");
|
|
4245
6461
|
const mcpConfigs = parseMCPConfig(JSON.parse(configRaw));
|
|
4246
6462
|
for (const [name, mcpConfig] of Object.entries(mcpConfigs)) {
|
|
4247
6463
|
try {
|
|
4248
6464
|
const client = new MCPClient(mcpConfig, name);
|
|
4249
6465
|
await client.connect();
|
|
4250
6466
|
mcpClients.push(client);
|
|
4251
|
-
console.log(
|
|
6467
|
+
console.log(chalk26.green(` MCP: Connected to ${name} (${client.tools.length} tools)`));
|
|
4252
6468
|
} catch (err) {
|
|
4253
|
-
console.log(
|
|
6469
|
+
console.log(chalk26.yellow(` MCP: Could not connect to ${name}: ${err.message}`));
|
|
4254
6470
|
}
|
|
4255
6471
|
}
|
|
4256
6472
|
} catch {
|
|
4257
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
|
+
}
|
|
4258
6484
|
const toolCtx = {
|
|
4259
6485
|
cwd: config.projectRoot,
|
|
4260
6486
|
requireConfirm: config.permissionMode !== "trust" && !config.autoConfirm,
|
|
@@ -4267,7 +6493,7 @@ ${repoMapStr}` : ""
|
|
|
4267
6493
|
});
|
|
4268
6494
|
});
|
|
4269
6495
|
},
|
|
4270
|
-
log: (msg) => console.log(
|
|
6496
|
+
log: (msg) => console.log(chalk26.gray(` ${msg}`)),
|
|
4271
6497
|
checkPermission: config.permissionMode === "trust" ? () => "allow" : (toolName, args) => checkPermission(permissions, toolName, args),
|
|
4272
6498
|
runHook: async (event, ctx) => {
|
|
4273
6499
|
if (!config.enableHooks || hookConfig.hooks.length === 0) return;
|
|
@@ -4277,22 +6503,30 @@ ${repoMapStr}` : ""
|
|
|
4277
6503
|
});
|
|
4278
6504
|
for (const r of results) {
|
|
4279
6505
|
if (!r.ok) {
|
|
4280
|
-
console.log(
|
|
6506
|
+
console.log(chalk26.yellow(` Hook failed (${r.hook.event}): ${r.error}`));
|
|
4281
6507
|
}
|
|
4282
6508
|
}
|
|
4283
6509
|
}
|
|
4284
6510
|
};
|
|
4285
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
|
+
});
|
|
4286
6519
|
const messages = [];
|
|
6520
|
+
let lastAssistantResponse = "";
|
|
4287
6521
|
if (opts.resume || opts.session) {
|
|
4288
6522
|
const session = opts.session ? await loadSession(opts.session) : await loadLastSession(config.projectRoot);
|
|
4289
6523
|
if (session) {
|
|
4290
6524
|
messages.push(...session.messages);
|
|
4291
6525
|
sessionId = session.meta.id;
|
|
4292
|
-
console.log(
|
|
6526
|
+
console.log(chalk26.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
|
|
4293
6527
|
`));
|
|
4294
6528
|
} else {
|
|
4295
|
-
console.log(
|
|
6529
|
+
console.log(chalk26.gray(" No session to resume.\n"));
|
|
4296
6530
|
}
|
|
4297
6531
|
}
|
|
4298
6532
|
const pipedInput = await readStdin();
|
|
@@ -4313,13 +6547,13 @@ Analyze the above input.`;
|
|
|
4313
6547
|
const refContext = formatReferences(references);
|
|
4314
6548
|
const finalPrompt = refContext + cleanInput;
|
|
4315
6549
|
messages.push({ role: "user", content: finalPrompt });
|
|
4316
|
-
console.log(
|
|
6550
|
+
console.log(chalk26.cyan(`> ${oneShot || "(piped input)"}
|
|
4317
6551
|
`));
|
|
4318
6552
|
if (references.length > 0) {
|
|
4319
|
-
console.log(
|
|
6553
|
+
console.log(chalk26.gray(` Injected ${references.length} reference(s)
|
|
4320
6554
|
`));
|
|
4321
6555
|
}
|
|
4322
|
-
const spinner =
|
|
6556
|
+
const spinner = ora4("Thinking...").start();
|
|
4323
6557
|
try {
|
|
4324
6558
|
const response = await withRetry(
|
|
4325
6559
|
() => runAgentLoop(messages, {
|
|
@@ -4335,13 +6569,13 @@ Analyze the above input.`;
|
|
|
4335
6569
|
onToolCall: (name, args) => {
|
|
4336
6570
|
if (spinner.isSpinning) spinner.stop();
|
|
4337
6571
|
const argSummary = Object.entries(args).map(([k, v]) => `${k}=${String(v).slice(0, 60)}`).join(", ");
|
|
4338
|
-
console.log(
|
|
6572
|
+
console.log(chalk26.gray(`
|
|
4339
6573
|
\u2192 ${name}(${argSummary})`));
|
|
4340
6574
|
},
|
|
4341
6575
|
onToolResult: (_name, result, isError) => {
|
|
4342
6576
|
const preview = result.slice(0, 100).replace(/\n/g, " ");
|
|
4343
|
-
const icon = isError ?
|
|
4344
|
-
console.log(
|
|
6577
|
+
const icon = isError ? chalk26.red("\u2717") : chalk26.green("\u2713");
|
|
6578
|
+
console.log(chalk26.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
|
|
4345
6579
|
}
|
|
4346
6580
|
})
|
|
4347
6581
|
);
|
|
@@ -4366,7 +6600,7 @@ Analyze the above input.`;
|
|
|
4366
6600
|
const savedPlan = await loadPlan(config.projectRoot);
|
|
4367
6601
|
if (savedPlan) {
|
|
4368
6602
|
ralphPlan = savedPlan;
|
|
4369
|
-
console.log(
|
|
6603
|
+
console.log(chalk26.gray(` Ralph plan loaded (${savedPlan.tasks.length} tasks)
|
|
4370
6604
|
`));
|
|
4371
6605
|
}
|
|
4372
6606
|
} catch {
|
|
@@ -4378,7 +6612,7 @@ Analyze the above input.`;
|
|
|
4378
6612
|
prompt: theme().prompt("notch> "),
|
|
4379
6613
|
completer: (line) => completer(line)
|
|
4380
6614
|
});
|
|
4381
|
-
console.log(
|
|
6615
|
+
console.log(chalk26.gray(" Type your request, or /help for commands.\n"));
|
|
4382
6616
|
rl.prompt();
|
|
4383
6617
|
rl.on("line", async (line) => {
|
|
4384
6618
|
const input = line.trim();
|
|
@@ -4393,8 +6627,9 @@ Analyze the above input.`;
|
|
|
4393
6627
|
}
|
|
4394
6628
|
if (messages.length > 0) {
|
|
4395
6629
|
try {
|
|
6630
|
+
const name = getSessionName();
|
|
4396
6631
|
const id = await saveSession(config.projectRoot, messages, { model: activeModelId });
|
|
4397
|
-
console.log(
|
|
6632
|
+
console.log(chalk26.gray(` Session saved: ${id}${name ? ` (${name})` : ""}`));
|
|
4398
6633
|
} catch {
|
|
4399
6634
|
}
|
|
4400
6635
|
}
|
|
@@ -4404,13 +6639,16 @@ Analyze the above input.`;
|
|
|
4404
6639
|
} catch {
|
|
4405
6640
|
}
|
|
4406
6641
|
}
|
|
6642
|
+
stopActiveLoop();
|
|
6643
|
+
stopFileWatcher();
|
|
6644
|
+
await pluginManager.shutdown();
|
|
4407
6645
|
await toolCtx.runHook?.("session-end", {});
|
|
4408
|
-
console.log(
|
|
6646
|
+
console.log(chalk26.gray("\n Goodbye!\n"));
|
|
4409
6647
|
process.exit(0);
|
|
4410
6648
|
}
|
|
4411
6649
|
if (input === "/clear") {
|
|
4412
6650
|
messages.length = 0;
|
|
4413
|
-
console.log(
|
|
6651
|
+
console.log(chalk26.gray(" Conversation cleared.\n"));
|
|
4414
6652
|
rl.prompt();
|
|
4415
6653
|
return;
|
|
4416
6654
|
}
|
|
@@ -4430,8 +6668,8 @@ Analyze the above input.`;
|
|
|
4430
6668
|
newModel = `notch-${newModel}`;
|
|
4431
6669
|
}
|
|
4432
6670
|
if (!isValidModel(newModel)) {
|
|
4433
|
-
console.log(
|
|
4434
|
-
console.log(
|
|
6671
|
+
console.log(chalk26.red(` Unknown model: ${newModel}`));
|
|
6672
|
+
console.log(chalk26.gray(` Available: ${modelChoices}`));
|
|
4435
6673
|
rl.prompt();
|
|
4436
6674
|
return;
|
|
4437
6675
|
}
|
|
@@ -4439,35 +6677,35 @@ Analyze the above input.`;
|
|
|
4439
6677
|
config.models.chat.model = activeModelId;
|
|
4440
6678
|
model = resolveModel(config.models.chat);
|
|
4441
6679
|
const switchedInfo = MODEL_CATALOG[activeModelId];
|
|
4442
|
-
console.log(
|
|
6680
|
+
console.log(chalk26.green(` Switched to ${switchedInfo.label} (${switchedInfo.id})
|
|
4443
6681
|
`));
|
|
4444
6682
|
rl.prompt();
|
|
4445
6683
|
return;
|
|
4446
6684
|
}
|
|
4447
6685
|
if (input === "/status") {
|
|
4448
|
-
const statusSpinner =
|
|
6686
|
+
const statusSpinner = ora4("Checking Notch API...").start();
|
|
4449
6687
|
const check = await validateConfig(config.models.chat);
|
|
4450
6688
|
if (check.ok) {
|
|
4451
6689
|
const statusInfo = MODEL_CATALOG[activeModelId];
|
|
4452
6690
|
statusSpinner.succeed(`${statusInfo.label} (${activeModelId}) is reachable`);
|
|
4453
6691
|
} else {
|
|
4454
6692
|
statusSpinner.fail(check.error ?? "API unreachable");
|
|
4455
|
-
console.log(
|
|
6693
|
+
console.log(chalk26.gray(" Tip: Set NOTCH_API_KEY or use --api-key, and verify your Modal endpoint is running.\n"));
|
|
4456
6694
|
}
|
|
4457
6695
|
rl.prompt();
|
|
4458
6696
|
return;
|
|
4459
6697
|
}
|
|
4460
6698
|
if (input === "/undo") {
|
|
4461
6699
|
if (checkpoints.undoCount === 0) {
|
|
4462
|
-
console.log(
|
|
6700
|
+
console.log(chalk26.gray(" Nothing to undo.\n"));
|
|
4463
6701
|
rl.prompt();
|
|
4464
6702
|
return;
|
|
4465
6703
|
}
|
|
4466
6704
|
const undone = await checkpoints.undo();
|
|
4467
6705
|
if (undone) {
|
|
4468
6706
|
const fileCount = undone.files.length;
|
|
4469
|
-
console.log(
|
|
4470
|
-
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
|
|
4471
6709
|
`));
|
|
4472
6710
|
}
|
|
4473
6711
|
rl.prompt();
|
|
@@ -4475,7 +6713,7 @@ Analyze the above input.`;
|
|
|
4475
6713
|
}
|
|
4476
6714
|
if (input === "/usage") {
|
|
4477
6715
|
if (usage.turnCount === 0) {
|
|
4478
|
-
console.log(
|
|
6716
|
+
console.log(chalk26.gray(" No usage yet.\n"));
|
|
4479
6717
|
} else {
|
|
4480
6718
|
console.log(usage.formatSession());
|
|
4481
6719
|
const currentTokens = estimateTokens(messages);
|
|
@@ -4488,7 +6726,7 @@ Analyze the above input.`;
|
|
|
4488
6726
|
}
|
|
4489
6727
|
if (input === "/cost") {
|
|
4490
6728
|
if (costTracker.totalCost === 0) {
|
|
4491
|
-
console.log(
|
|
6729
|
+
console.log(chalk26.gray(" No cost data yet.\n"));
|
|
4492
6730
|
} else {
|
|
4493
6731
|
console.log(costTracker.formatTotal());
|
|
4494
6732
|
console.log(costTracker.formatByModel());
|
|
@@ -4498,12 +6736,12 @@ Analyze the above input.`;
|
|
|
4498
6736
|
return;
|
|
4499
6737
|
}
|
|
4500
6738
|
if (input === "/compact") {
|
|
4501
|
-
const { autoCompress: autoCompress2 } = await import("./compression-
|
|
6739
|
+
const { autoCompress: autoCompress2 } = await import("./compression-CXJN2ZYN.js");
|
|
4502
6740
|
const before = messages.length;
|
|
4503
6741
|
const compressed = await autoCompress2(messages, model, MODEL_CATALOG[activeModelId].contextWindow);
|
|
4504
6742
|
messages.length = 0;
|
|
4505
6743
|
messages.push(...compressed);
|
|
4506
|
-
console.log(
|
|
6744
|
+
console.log(chalk26.green(` Compressed: ${before} messages \u2192 ${messages.length} messages`));
|
|
4507
6745
|
console.log("");
|
|
4508
6746
|
rl.prompt();
|
|
4509
6747
|
return;
|
|
@@ -4511,10 +6749,10 @@ Analyze the above input.`;
|
|
|
4511
6749
|
if (input === "/diff") {
|
|
4512
6750
|
const diffs = checkpoints.allDiffs();
|
|
4513
6751
|
if (diffs.length === 0) {
|
|
4514
|
-
console.log(
|
|
6752
|
+
console.log(chalk26.gray(" No file changes this session.\n"));
|
|
4515
6753
|
} else {
|
|
4516
6754
|
for (const df of diffs) {
|
|
4517
|
-
console.log(
|
|
6755
|
+
console.log(chalk26.cyan(` ${df.path}:`));
|
|
4518
6756
|
console.log(unifiedDiff(df.before, df.after, df.path));
|
|
4519
6757
|
console.log("");
|
|
4520
6758
|
}
|
|
@@ -4530,10 +6768,10 @@ Analyze the above input.`;
|
|
|
4530
6768
|
projectRoot: config.projectRoot,
|
|
4531
6769
|
outputPath: exportPath
|
|
4532
6770
|
});
|
|
4533
|
-
console.log(
|
|
6771
|
+
console.log(chalk26.green(` Exported to ${ePath}
|
|
4534
6772
|
`));
|
|
4535
6773
|
} catch (err) {
|
|
4536
|
-
console.log(
|
|
6774
|
+
console.log(chalk26.red(` Export failed: ${err.message}
|
|
4537
6775
|
`));
|
|
4538
6776
|
}
|
|
4539
6777
|
rl.prompt();
|
|
@@ -4543,10 +6781,10 @@ Analyze the above input.`;
|
|
|
4543
6781
|
try {
|
|
4544
6782
|
const id = await saveSession(config.projectRoot, messages, { model: activeModelId });
|
|
4545
6783
|
sessionId = id;
|
|
4546
|
-
console.log(
|
|
6784
|
+
console.log(chalk26.green(` Session saved: ${id}
|
|
4547
6785
|
`));
|
|
4548
6786
|
} catch (err) {
|
|
4549
|
-
console.log(
|
|
6787
|
+
console.log(chalk26.red(` Save failed: ${err.message}
|
|
4550
6788
|
`));
|
|
4551
6789
|
}
|
|
4552
6790
|
rl.prompt();
|
|
@@ -4556,16 +6794,16 @@ Analyze the above input.`;
|
|
|
4556
6794
|
try {
|
|
4557
6795
|
const sessions = await listSessions(config.projectRoot);
|
|
4558
6796
|
if (sessions.length === 0) {
|
|
4559
|
-
console.log(
|
|
6797
|
+
console.log(chalk26.gray(" No saved sessions.\n"));
|
|
4560
6798
|
} else {
|
|
4561
|
-
console.log(
|
|
6799
|
+
console.log(chalk26.gray("\n Saved sessions:\n"));
|
|
4562
6800
|
for (const s of sessions.slice(0, 10)) {
|
|
4563
|
-
console.log(
|
|
6801
|
+
console.log(chalk26.gray(` ${s.id} ${s.turns} turns ${s.date} ${s.model}`));
|
|
4564
6802
|
}
|
|
4565
6803
|
console.log("");
|
|
4566
6804
|
}
|
|
4567
6805
|
} catch (err) {
|
|
4568
|
-
console.log(
|
|
6806
|
+
console.log(chalk26.red(` Error listing sessions: ${err.message}
|
|
4569
6807
|
`));
|
|
4570
6808
|
}
|
|
4571
6809
|
rl.prompt();
|
|
@@ -4583,18 +6821,18 @@ Analyze the above input.`;
|
|
|
4583
6821
|
const savedPlan = await loadPlan(config.projectRoot);
|
|
4584
6822
|
if (savedPlan) {
|
|
4585
6823
|
ralphPlan = savedPlan;
|
|
4586
|
-
console.log(
|
|
6824
|
+
console.log(chalk26.green(` Ralph plan restored (${savedPlan.tasks.length} tasks)
|
|
4587
6825
|
`));
|
|
4588
6826
|
}
|
|
4589
6827
|
} catch {
|
|
4590
6828
|
}
|
|
4591
|
-
console.log(
|
|
6829
|
+
console.log(chalk26.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
|
|
4592
6830
|
`));
|
|
4593
6831
|
} else {
|
|
4594
|
-
console.log(
|
|
6832
|
+
console.log(chalk26.gray(" No session found.\n"));
|
|
4595
6833
|
}
|
|
4596
6834
|
} catch (err) {
|
|
4597
|
-
console.log(
|
|
6835
|
+
console.log(chalk26.red(` Resume failed: ${err.message}
|
|
4598
6836
|
`));
|
|
4599
6837
|
}
|
|
4600
6838
|
rl.prompt();
|
|
@@ -4603,7 +6841,7 @@ Analyze the above input.`;
|
|
|
4603
6841
|
if (input.startsWith("/search ")) {
|
|
4604
6842
|
const query = input.replace("/search ", "").trim().toLowerCase();
|
|
4605
6843
|
if (!query) {
|
|
4606
|
-
console.log(
|
|
6844
|
+
console.log(chalk26.gray(" Usage: /search <query>\n"));
|
|
4607
6845
|
rl.prompt();
|
|
4608
6846
|
return;
|
|
4609
6847
|
}
|
|
@@ -4620,14 +6858,14 @@ Analyze the above input.`;
|
|
|
4620
6858
|
}
|
|
4621
6859
|
}
|
|
4622
6860
|
if (matches.length === 0) {
|
|
4623
|
-
console.log(
|
|
6861
|
+
console.log(chalk26.gray(` No matches for "${query}"
|
|
4624
6862
|
`));
|
|
4625
6863
|
} else {
|
|
4626
|
-
console.log(
|
|
6864
|
+
console.log(chalk26.gray(`
|
|
4627
6865
|
${matches.length} match(es) for "${query}":
|
|
4628
6866
|
`));
|
|
4629
6867
|
for (const m of matches.slice(0, 10)) {
|
|
4630
|
-
console.log(
|
|
6868
|
+
console.log(chalk26.gray(` [${m.index}] ${m.role}: ${m.preview}`));
|
|
4631
6869
|
}
|
|
4632
6870
|
console.log("");
|
|
4633
6871
|
}
|
|
@@ -4636,12 +6874,12 @@ Analyze the above input.`;
|
|
|
4636
6874
|
}
|
|
4637
6875
|
if (input.startsWith("/plan ") && !input.startsWith("/plan approve") && !input.startsWith("/plan cancel") && !input.startsWith("/plan edit")) {
|
|
4638
6876
|
const task = input.replace("/plan ", "").trim();
|
|
4639
|
-
const planSpinner =
|
|
6877
|
+
const planSpinner = ora4("Generating plan...").start();
|
|
4640
6878
|
try {
|
|
4641
6879
|
activePlan = await generatePlan(task, model, systemPrompt);
|
|
4642
6880
|
planSpinner.succeed("Plan generated");
|
|
4643
6881
|
console.log(formatPlan(activePlan));
|
|
4644
|
-
console.log(
|
|
6882
|
+
console.log(chalk26.gray(" Use /plan approve to execute, /plan edit to modify, or /plan cancel to discard.\n"));
|
|
4645
6883
|
} catch (err) {
|
|
4646
6884
|
planSpinner.fail(`Plan failed: ${err.message}`);
|
|
4647
6885
|
}
|
|
@@ -4650,15 +6888,15 @@ Analyze the above input.`;
|
|
|
4650
6888
|
}
|
|
4651
6889
|
if (input === "/plan approve") {
|
|
4652
6890
|
if (!activePlan) {
|
|
4653
|
-
console.log(
|
|
6891
|
+
console.log(chalk26.gray(" No active plan. Use /plan <task> to create one.\n"));
|
|
4654
6892
|
rl.prompt();
|
|
4655
6893
|
return;
|
|
4656
6894
|
}
|
|
4657
|
-
console.log(
|
|
6895
|
+
console.log(chalk26.green(" Executing plan...\n"));
|
|
4658
6896
|
while (!isPlanComplete(activePlan)) {
|
|
4659
6897
|
const stepPrompt = currentStepPrompt(activePlan);
|
|
4660
6898
|
messages.push({ role: "user", content: stepPrompt });
|
|
4661
|
-
const planStepSpinner =
|
|
6899
|
+
const planStepSpinner = ora4(`Step ${activePlan.currentStep + 1}/${activePlan.steps.length}...`).start();
|
|
4662
6900
|
try {
|
|
4663
6901
|
const response = await runAgentLoop(messages, {
|
|
4664
6902
|
model,
|
|
@@ -4672,10 +6910,10 @@ Analyze the above input.`;
|
|
|
4672
6910
|
},
|
|
4673
6911
|
onToolCall: (name) => {
|
|
4674
6912
|
if (planStepSpinner.isSpinning) planStepSpinner.stop();
|
|
4675
|
-
console.log(
|
|
6913
|
+
console.log(chalk26.gray(` \u2192 ${name}`));
|
|
4676
6914
|
},
|
|
4677
6915
|
onToolResult: (_name, _result, isError) => {
|
|
4678
|
-
console.log(isError ?
|
|
6916
|
+
console.log(isError ? chalk26.red(" \u2717") : chalk26.green(" \u2713"));
|
|
4679
6917
|
}
|
|
4680
6918
|
});
|
|
4681
6919
|
console.log("\n");
|
|
@@ -4688,7 +6926,7 @@ Analyze the above input.`;
|
|
|
4688
6926
|
}
|
|
4689
6927
|
}
|
|
4690
6928
|
if (activePlan && isPlanComplete(activePlan)) {
|
|
4691
|
-
console.log(
|
|
6929
|
+
console.log(chalk26.green(" Plan completed!\n"));
|
|
4692
6930
|
}
|
|
4693
6931
|
activePlan = null;
|
|
4694
6932
|
rl.prompt();
|
|
@@ -4696,26 +6934,26 @@ Analyze the above input.`;
|
|
|
4696
6934
|
}
|
|
4697
6935
|
if (input === "/plan edit") {
|
|
4698
6936
|
if (!activePlan) {
|
|
4699
|
-
console.log(
|
|
6937
|
+
console.log(chalk26.gray(" No active plan to edit.\n"));
|
|
4700
6938
|
rl.prompt();
|
|
4701
6939
|
return;
|
|
4702
6940
|
}
|
|
4703
6941
|
console.log(formatPlan(activePlan));
|
|
4704
|
-
console.log(
|
|
4705
|
-
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"));
|
|
4706
6944
|
rl.prompt();
|
|
4707
6945
|
return;
|
|
4708
6946
|
}
|
|
4709
6947
|
if (input === "/plan cancel") {
|
|
4710
6948
|
activePlan = null;
|
|
4711
|
-
console.log(
|
|
6949
|
+
console.log(chalk26.gray(" Plan discarded.\n"));
|
|
4712
6950
|
rl.prompt();
|
|
4713
6951
|
return;
|
|
4714
6952
|
}
|
|
4715
6953
|
if (input.startsWith("/agent ")) {
|
|
4716
6954
|
const task = input.replace("/agent ", "").trim();
|
|
4717
6955
|
const agentId = nextSubagentId();
|
|
4718
|
-
console.log(
|
|
6956
|
+
console.log(chalk26.cyan(` Spawning subagent #${agentId}: ${task}
|
|
4719
6957
|
`));
|
|
4720
6958
|
spawnSubagent({
|
|
4721
6959
|
id: agentId,
|
|
@@ -4725,14 +6963,14 @@ Analyze the above input.`;
|
|
|
4725
6963
|
toolContext: toolCtx,
|
|
4726
6964
|
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4727
6965
|
onComplete: (result) => {
|
|
4728
|
-
console.log(
|
|
6966
|
+
console.log(chalk26.green(`
|
|
4729
6967
|
Subagent #${agentId} finished:`));
|
|
4730
|
-
console.log(
|
|
6968
|
+
console.log(chalk26.gray(` ${result.slice(0, 200)}
|
|
4731
6969
|
`));
|
|
4732
6970
|
rl.prompt();
|
|
4733
6971
|
},
|
|
4734
6972
|
onError: (err) => {
|
|
4735
|
-
console.log(
|
|
6973
|
+
console.log(chalk26.red(`
|
|
4736
6974
|
Subagent #${agentId} failed: ${err}
|
|
4737
6975
|
`));
|
|
4738
6976
|
rl.prompt();
|
|
@@ -4745,18 +6983,18 @@ Analyze the above input.`;
|
|
|
4745
6983
|
try {
|
|
4746
6984
|
const memories = await loadMemories(config.projectRoot);
|
|
4747
6985
|
if (memories.length === 0) {
|
|
4748
|
-
console.log(
|
|
6986
|
+
console.log(chalk26.gray(" No saved memories.\n"));
|
|
4749
6987
|
} else {
|
|
4750
|
-
console.log(
|
|
6988
|
+
console.log(chalk26.gray(`
|
|
4751
6989
|
${memories.length} saved memories:
|
|
4752
6990
|
`));
|
|
4753
6991
|
for (const m of memories) {
|
|
4754
|
-
console.log(
|
|
6992
|
+
console.log(chalk26.gray(` [${m.type}] ${m.content.slice(0, 80)}`));
|
|
4755
6993
|
}
|
|
4756
6994
|
console.log("");
|
|
4757
6995
|
}
|
|
4758
6996
|
} catch (err) {
|
|
4759
|
-
console.log(
|
|
6997
|
+
console.log(chalk26.red(` Error: ${err.message}
|
|
4760
6998
|
`));
|
|
4761
6999
|
}
|
|
4762
7000
|
rl.prompt();
|
|
@@ -4767,16 +7005,16 @@ Analyze the above input.`;
|
|
|
4767
7005
|
try {
|
|
4768
7006
|
const results = await searchMemories(config.projectRoot, query);
|
|
4769
7007
|
if (results.length === 0) {
|
|
4770
|
-
console.log(
|
|
7008
|
+
console.log(chalk26.gray(` No memories matching "${query}"
|
|
4771
7009
|
`));
|
|
4772
7010
|
} else {
|
|
4773
7011
|
for (const m of results) {
|
|
4774
|
-
console.log(
|
|
7012
|
+
console.log(chalk26.gray(` [${m.type}] ${m.content.slice(0, 100)}`));
|
|
4775
7013
|
}
|
|
4776
7014
|
console.log("");
|
|
4777
7015
|
}
|
|
4778
7016
|
} catch (err) {
|
|
4779
|
-
console.log(
|
|
7017
|
+
console.log(chalk26.red(` Error: ${err.message}
|
|
4780
7018
|
`));
|
|
4781
7019
|
}
|
|
4782
7020
|
rl.prompt();
|
|
@@ -4788,10 +7026,10 @@ Analyze the above input.`;
|
|
|
4788
7026
|
for (const m of memories) {
|
|
4789
7027
|
await deleteMemory(config.projectRoot, m.id);
|
|
4790
7028
|
}
|
|
4791
|
-
console.log(
|
|
7029
|
+
console.log(chalk26.yellow(` Cleared ${memories.length} memories.
|
|
4792
7030
|
`));
|
|
4793
7031
|
} catch (err) {
|
|
4794
|
-
console.log(
|
|
7032
|
+
console.log(chalk26.red(` Error: ${err.message}
|
|
4795
7033
|
`));
|
|
4796
7034
|
}
|
|
4797
7035
|
rl.prompt();
|
|
@@ -4805,7 +7043,7 @@ Analyze the above input.`;
|
|
|
4805
7043
|
}
|
|
4806
7044
|
if (input.startsWith("/ralph plan ")) {
|
|
4807
7045
|
const goal = input.replace("/ralph plan ", "").trim();
|
|
4808
|
-
const planSpinner =
|
|
7046
|
+
const planSpinner = ora4("Ralph is planning...").start();
|
|
4809
7047
|
try {
|
|
4810
7048
|
ralphPlan = await generateRalphPlan(goal, model, systemPrompt);
|
|
4811
7049
|
await savePlan(config.projectRoot, ralphPlan);
|
|
@@ -4819,27 +7057,27 @@ Analyze the above input.`;
|
|
|
4819
7057
|
}
|
|
4820
7058
|
if (input === "/ralph run") {
|
|
4821
7059
|
if (!ralphPlan) {
|
|
4822
|
-
console.log(
|
|
7060
|
+
console.log(chalk26.gray(" No Ralph plan. Use /ralph plan <goal> first.\n"));
|
|
4823
7061
|
rl.prompt();
|
|
4824
7062
|
return;
|
|
4825
7063
|
}
|
|
4826
|
-
console.log(
|
|
7064
|
+
console.log(chalk26.green(" Ralph is running...\n"));
|
|
4827
7065
|
try {
|
|
4828
7066
|
ralphPlan = await runRalphLoop(ralphPlan, {
|
|
4829
7067
|
model,
|
|
4830
7068
|
systemPrompt,
|
|
4831
7069
|
toolContext: toolCtx,
|
|
4832
7070
|
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4833
|
-
onTaskStart: (task) => console.log(
|
|
4834
|
-
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}
|
|
4835
7073
|
`)),
|
|
4836
|
-
onTaskFail: (task, err) => console.log(
|
|
7074
|
+
onTaskFail: (task, err) => console.log(chalk26.red(` \u2717 Failed: ${task.description} (${err})
|
|
4837
7075
|
`))
|
|
4838
7076
|
});
|
|
4839
7077
|
await savePlan(config.projectRoot, ralphPlan);
|
|
4840
7078
|
console.log(formatRalphStatus(ralphPlan));
|
|
4841
7079
|
} catch (err) {
|
|
4842
|
-
console.log(
|
|
7080
|
+
console.log(chalk26.red(` Ralph error: ${err.message}
|
|
4843
7081
|
`));
|
|
4844
7082
|
}
|
|
4845
7083
|
rl.prompt();
|
|
@@ -4847,7 +7085,7 @@ Analyze the above input.`;
|
|
|
4847
7085
|
}
|
|
4848
7086
|
if (input === "/ralph status") {
|
|
4849
7087
|
if (!ralphPlan) {
|
|
4850
|
-
console.log(
|
|
7088
|
+
console.log(chalk26.gray(" No Ralph plan active.\n"));
|
|
4851
7089
|
} else {
|
|
4852
7090
|
console.log(formatRalphStatus(ralphPlan));
|
|
4853
7091
|
}
|
|
@@ -4858,26 +7096,26 @@ Analyze the above input.`;
|
|
|
4858
7096
|
ralphPlan = null;
|
|
4859
7097
|
await deletePlan(config.projectRoot).catch(() => {
|
|
4860
7098
|
});
|
|
4861
|
-
console.log(
|
|
7099
|
+
console.log(chalk26.gray(" Ralph plan cleared.\n"));
|
|
4862
7100
|
rl.prompt();
|
|
4863
7101
|
return;
|
|
4864
7102
|
}
|
|
4865
7103
|
if (input === "/branch") {
|
|
4866
7104
|
const branchId = `branch-${branches.size + 1}`;
|
|
4867
7105
|
branches.set(branchId, [...messages]);
|
|
4868
|
-
console.log(
|
|
7106
|
+
console.log(chalk26.green(` Forked conversation as "${branchId}" (${messages.length} messages)
|
|
4869
7107
|
`));
|
|
4870
7108
|
rl.prompt();
|
|
4871
7109
|
return;
|
|
4872
7110
|
}
|
|
4873
7111
|
if (input === "/branches") {
|
|
4874
7112
|
if (branches.size === 0) {
|
|
4875
|
-
console.log(
|
|
7113
|
+
console.log(chalk26.gray(" No conversation branches. Use /branch to fork.\n"));
|
|
4876
7114
|
} else {
|
|
4877
|
-
console.log(
|
|
7115
|
+
console.log(chalk26.gray("\n Branches:\n"));
|
|
4878
7116
|
for (const [name, msgs] of branches) {
|
|
4879
|
-
const marker = name === currentBranch ?
|
|
4880
|
-
console.log(
|
|
7117
|
+
const marker = name === currentBranch ? chalk26.green(" \u25CF") : " ";
|
|
7118
|
+
console.log(chalk26.gray(` ${marker} ${name} (${msgs.length} messages)`));
|
|
4881
7119
|
}
|
|
4882
7120
|
console.log("");
|
|
4883
7121
|
}
|
|
@@ -4914,8 +7152,50 @@ Analyze the above input.`;
|
|
|
4914
7152
|
return;
|
|
4915
7153
|
}
|
|
4916
7154
|
if (input.startsWith("/")) {
|
|
4917
|
-
|
|
4918
|
-
|
|
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"));
|
|
4919
7199
|
rl.prompt();
|
|
4920
7200
|
return;
|
|
4921
7201
|
}
|
|
@@ -4923,16 +7203,16 @@ Analyze the above input.`;
|
|
|
4923
7203
|
const refContext = formatReferences(references);
|
|
4924
7204
|
const finalPrompt = refContext + cleanInput;
|
|
4925
7205
|
if (references.length > 0) {
|
|
4926
|
-
console.log(
|
|
7206
|
+
console.log(chalk26.gray(` Injected ${references.length} reference(s)`));
|
|
4927
7207
|
}
|
|
4928
7208
|
messages.push({ role: "user", content: finalPrompt });
|
|
4929
|
-
const spinner =
|
|
7209
|
+
const spinner = ora4("Thinking...").start();
|
|
4930
7210
|
try {
|
|
4931
7211
|
const response = await withRetry(
|
|
4932
7212
|
() => runAgentLoop(messages, {
|
|
4933
7213
|
model,
|
|
4934
7214
|
systemPrompt,
|
|
4935
|
-
toolContext: toolCtx,
|
|
7215
|
+
toolContext: { ...toolCtx, dryRun: isSandboxEnabled() || toolCtx.dryRun },
|
|
4936
7216
|
maxIterations: config.maxIterations,
|
|
4937
7217
|
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4938
7218
|
onTextChunk: (chunk) => {
|
|
@@ -4949,20 +7229,21 @@ Analyze the above input.`;
|
|
|
4949
7229
|
const val = String(v);
|
|
4950
7230
|
return `${k}=${val.length > 60 ? val.slice(0, 60) + "..." : val}`;
|
|
4951
7231
|
}).join(", ");
|
|
4952
|
-
console.log(
|
|
7232
|
+
console.log(chalk26.gray(`
|
|
4953
7233
|
\u2192 ${name}(${argSummary})`));
|
|
4954
7234
|
},
|
|
4955
7235
|
onToolResult: (_name, result, isError) => {
|
|
4956
7236
|
const preview = result.slice(0, 100).replace(/\n/g, " ");
|
|
4957
|
-
const icon = isError ?
|
|
4958
|
-
console.log(
|
|
7237
|
+
const icon = isError ? chalk26.red("\u2717") : chalk26.green("\u2713");
|
|
7238
|
+
console.log(chalk26.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
|
|
4959
7239
|
},
|
|
4960
7240
|
onCompress: () => {
|
|
4961
|
-
console.log(
|
|
7241
|
+
console.log(chalk26.yellow("\n [Context compressed to fit window]\n"));
|
|
4962
7242
|
}
|
|
4963
7243
|
})
|
|
4964
7244
|
);
|
|
4965
7245
|
console.log("\n");
|
|
7246
|
+
lastAssistantResponse = response.text;
|
|
4966
7247
|
checkpoints.commit(`Turn ${usage.turnCount + 1}`);
|
|
4967
7248
|
if (response.usage) {
|
|
4968
7249
|
usage.record({
|
|
@@ -4991,7 +7272,7 @@ Analyze the above input.`;
|
|
|
4991
7272
|
type: "auto",
|
|
4992
7273
|
content: memMatch[1]
|
|
4993
7274
|
});
|
|
4994
|
-
console.log(
|
|
7275
|
+
console.log(chalk26.gray(" (Saved to memory)\n"));
|
|
4995
7276
|
} catch {
|
|
4996
7277
|
}
|
|
4997
7278
|
}
|
|
@@ -5001,13 +7282,13 @@ Analyze the above input.`;
|
|
|
5001
7282
|
checkpoints.discard();
|
|
5002
7283
|
const msg = err.message?.toLowerCase() ?? "";
|
|
5003
7284
|
if (msg.includes("fetch") || msg.includes("econnrefused") || msg.includes("network")) {
|
|
5004
|
-
console.log(
|
|
7285
|
+
console.log(chalk26.gray(" Tip: Check that your Notch endpoint is running. Use /status to verify.\n"));
|
|
5005
7286
|
} else if (msg.includes("401") || msg.includes("unauthorized") || msg.includes("api key")) {
|
|
5006
|
-
console.log(
|
|
7287
|
+
console.log(chalk26.gray(" Tip: Set NOTCH_API_KEY env var or use --api-key flag.\n"));
|
|
5007
7288
|
} else if (msg.includes("429") || msg.includes("rate limit")) {
|
|
5008
|
-
console.log(
|
|
7289
|
+
console.log(chalk26.gray(" Tip: Rate limited. Wait a moment and try again.\n"));
|
|
5009
7290
|
} else {
|
|
5010
|
-
console.log(
|
|
7291
|
+
console.log(chalk26.gray(" (The conversation history is preserved. Try again.)\n"));
|
|
5011
7292
|
}
|
|
5012
7293
|
}
|
|
5013
7294
|
rl.prompt();
|
|
@@ -5025,16 +7306,16 @@ async function handleRalphSubcommand(args, cliOpts) {
|
|
|
5025
7306
|
cwd: config.projectRoot,
|
|
5026
7307
|
requireConfirm: false,
|
|
5027
7308
|
confirm: async () => true,
|
|
5028
|
-
log: (msg) => console.log(
|
|
7309
|
+
log: (msg) => console.log(chalk26.gray(` ${msg}`))
|
|
5029
7310
|
};
|
|
5030
7311
|
const subcommand = args[0];
|
|
5031
7312
|
if (subcommand === "plan") {
|
|
5032
7313
|
const goal = args.slice(1).join(" ");
|
|
5033
7314
|
if (!goal) {
|
|
5034
|
-
console.error(
|
|
7315
|
+
console.error(chalk26.red(" Usage: notch ralph plan <goal>"));
|
|
5035
7316
|
process.exit(1);
|
|
5036
7317
|
}
|
|
5037
|
-
const spinner =
|
|
7318
|
+
const spinner = ora4("Ralph is planning...").start();
|
|
5038
7319
|
const plan = await generateRalphPlan(goal, model, systemPrompt);
|
|
5039
7320
|
await savePlan(config.projectRoot, plan);
|
|
5040
7321
|
spinner.succeed(`Planned ${plan.tasks.length} tasks`);
|
|
@@ -5042,7 +7323,7 @@ async function handleRalphSubcommand(args, cliOpts) {
|
|
|
5042
7323
|
} else if (subcommand === "run") {
|
|
5043
7324
|
let plan = await loadPlan(config.projectRoot);
|
|
5044
7325
|
if (!plan) {
|
|
5045
|
-
console.error(
|
|
7326
|
+
console.error(chalk26.red(" No plan found. Run: notch ralph plan <goal>"));
|
|
5046
7327
|
process.exit(1);
|
|
5047
7328
|
}
|
|
5048
7329
|
plan = await runRalphLoop(plan, {
|
|
@@ -5050,27 +7331,27 @@ async function handleRalphSubcommand(args, cliOpts) {
|
|
|
5050
7331
|
systemPrompt,
|
|
5051
7332
|
toolContext: toolCtx,
|
|
5052
7333
|
contextWindow: MODEL_CATALOG[config.models.chat.model].contextWindow,
|
|
5053
|
-
onTaskStart: (t) => console.log(
|
|
5054
|
-
onTaskComplete: (t) => console.log(
|
|
5055
|
-
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}`))
|
|
5056
7337
|
});
|
|
5057
7338
|
await savePlan(config.projectRoot, plan);
|
|
5058
7339
|
console.log(formatRalphStatus(plan));
|
|
5059
7340
|
} else if (subcommand === "status") {
|
|
5060
7341
|
const plan = await loadPlan(config.projectRoot);
|
|
5061
7342
|
if (!plan) {
|
|
5062
|
-
console.log(
|
|
7343
|
+
console.log(chalk26.gray(" No Ralph plan found."));
|
|
5063
7344
|
} else {
|
|
5064
7345
|
console.log(formatRalphStatus(plan));
|
|
5065
7346
|
}
|
|
5066
7347
|
} else {
|
|
5067
|
-
console.error(
|
|
5068
|
-
console.error(
|
|
7348
|
+
console.error(chalk26.red(` Unknown: notch ralph ${subcommand}`));
|
|
7349
|
+
console.error(chalk26.gray(" Usage: notch ralph <plan|run|status>"));
|
|
5069
7350
|
process.exit(1);
|
|
5070
7351
|
}
|
|
5071
7352
|
}
|
|
5072
7353
|
main().catch((err) => {
|
|
5073
|
-
console.error(
|
|
7354
|
+
console.error(chalk26.red(`
|
|
5074
7355
|
Fatal: ${err.message}
|
|
5075
7356
|
`));
|
|
5076
7357
|
process.exit(1);
|