@freesyntax/notch-cli 0.4.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/LICENSE +19 -0
- package/dist/auth-GTGBXOSH.js +16 -0
- package/dist/chunk-MWM5TFY4.js +142 -0
- package/dist/chunk-TJS4W4R5.js +176 -0
- package/dist/compression-AGHTZF7D.js +10 -0
- package/dist/index.js +4764 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4764 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
clearCredentials,
|
|
4
|
+
loadCredentials,
|
|
5
|
+
login
|
|
6
|
+
} from "./chunk-TJS4W4R5.js";
|
|
7
|
+
import {
|
|
8
|
+
autoCompress,
|
|
9
|
+
estimateTokens
|
|
10
|
+
} from "./chunk-MWM5TFY4.js";
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import chalk8 from "chalk";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import * as readline from "readline";
|
|
17
|
+
import * as nodePath from "path";
|
|
18
|
+
|
|
19
|
+
// src/config.ts
|
|
20
|
+
import fs from "fs/promises";
|
|
21
|
+
import path from "path";
|
|
22
|
+
|
|
23
|
+
// src/providers/registry.ts
|
|
24
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
25
|
+
var MODEL_CATALOG = {
|
|
26
|
+
"notch-cinder": {
|
|
27
|
+
id: "notch-cinder",
|
|
28
|
+
label: "Cinder",
|
|
29
|
+
size: "4B",
|
|
30
|
+
gpu: "L4",
|
|
31
|
+
contextWindow: 65536,
|
|
32
|
+
maxOutputTokens: 8192,
|
|
33
|
+
baseUrl: "https://cutmob--notch-serve-cinder-notchcinderserver-serve.modal.run/v1"
|
|
34
|
+
},
|
|
35
|
+
"notch-forge": {
|
|
36
|
+
id: "notch-forge",
|
|
37
|
+
label: "Forge",
|
|
38
|
+
size: "9B",
|
|
39
|
+
gpu: "L40S",
|
|
40
|
+
contextWindow: 131072,
|
|
41
|
+
maxOutputTokens: 16384,
|
|
42
|
+
baseUrl: "https://cutmob--notch-serve-forge-notchforgeserver-serve.modal.run/v1"
|
|
43
|
+
},
|
|
44
|
+
"notch-pyre": {
|
|
45
|
+
id: "notch-pyre",
|
|
46
|
+
label: "Pyre",
|
|
47
|
+
size: "24B",
|
|
48
|
+
gpu: "A100-80GB",
|
|
49
|
+
contextWindow: 131072,
|
|
50
|
+
maxOutputTokens: 16384,
|
|
51
|
+
baseUrl: "https://cutmob--notch-serve-pyre-notchpyreserver-serve.modal.run/v1"
|
|
52
|
+
},
|
|
53
|
+
"notch-ignis": {
|
|
54
|
+
id: "notch-ignis",
|
|
55
|
+
label: "Ignis",
|
|
56
|
+
size: "27B",
|
|
57
|
+
gpu: "A100-80GB",
|
|
58
|
+
contextWindow: 131072,
|
|
59
|
+
maxOutputTokens: 16384,
|
|
60
|
+
baseUrl: "https://cutmob--notch-serve-ignis-notchignisserver-serve.modal.run/v1"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var MODEL_IDS = Object.keys(MODEL_CATALOG);
|
|
64
|
+
function isValidModel(id) {
|
|
65
|
+
return id in MODEL_CATALOG;
|
|
66
|
+
}
|
|
67
|
+
function resolveModel(config) {
|
|
68
|
+
const info = MODEL_CATALOG[config.model];
|
|
69
|
+
if (!info) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Unknown model "${config.model}". Available: ${MODEL_IDS.join(", ")}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const baseUrl = config.baseUrl ?? process.env.NOTCH_BASE_URL ?? info.baseUrl;
|
|
75
|
+
const apiKey = config.apiKey ?? process.env.NOTCH_API_KEY;
|
|
76
|
+
if (!apiKey) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"NOTCH_API_KEY is not set. Set it via the NOTCH_API_KEY environment variable or --api-key flag."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const provider = createOpenAI({
|
|
82
|
+
apiKey,
|
|
83
|
+
baseURL: baseUrl,
|
|
84
|
+
headers: config.headers
|
|
85
|
+
});
|
|
86
|
+
return provider(config.model);
|
|
87
|
+
}
|
|
88
|
+
async function validateConfig(config) {
|
|
89
|
+
const info = MODEL_CATALOG[config.model];
|
|
90
|
+
if (!info) {
|
|
91
|
+
return { ok: false, error: `Unknown model "${config.model}". Available: ${MODEL_IDS.join(", ")}` };
|
|
92
|
+
}
|
|
93
|
+
const baseUrl = config.baseUrl ?? process.env.NOTCH_BASE_URL ?? info.baseUrl;
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${baseUrl.replace(/\/v1$/, "")}/health`, {
|
|
96
|
+
signal: AbortSignal.timeout(5e3)
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
return { ok: false, error: `Notch ${info.label} returned ${res.status} at ${baseUrl}` };
|
|
100
|
+
}
|
|
101
|
+
return { ok: true };
|
|
102
|
+
} catch {
|
|
103
|
+
return { ok: false, error: `Cannot reach Notch ${info.label} at ${baseUrl}` };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/config.ts
|
|
108
|
+
var DEFAULT_MODEL = {
|
|
109
|
+
model: "notch-forge",
|
|
110
|
+
temperature: 0.3
|
|
111
|
+
};
|
|
112
|
+
var DEFAULTS = {
|
|
113
|
+
models: { chat: DEFAULT_MODEL },
|
|
114
|
+
projectRoot: process.cwd(),
|
|
115
|
+
autoConfirm: false,
|
|
116
|
+
maxIterations: 25,
|
|
117
|
+
useRepoMap: true,
|
|
118
|
+
renderMarkdown: true,
|
|
119
|
+
enableMemory: true,
|
|
120
|
+
enableHooks: true,
|
|
121
|
+
permissionMode: "auto",
|
|
122
|
+
theme: "default"
|
|
123
|
+
};
|
|
124
|
+
async function loadConfig(overrides = {}) {
|
|
125
|
+
const config = { ...DEFAULTS, models: { chat: { ...DEFAULT_MODEL } } };
|
|
126
|
+
const configPath = path.resolve(config.projectRoot, ".notch.json");
|
|
127
|
+
try {
|
|
128
|
+
const raw = await fs.readFile(configPath, "utf-8");
|
|
129
|
+
const fileConfig = JSON.parse(raw);
|
|
130
|
+
if (fileConfig.model && isValidModel(fileConfig.model)) {
|
|
131
|
+
config.models.chat.model = fileConfig.model;
|
|
132
|
+
}
|
|
133
|
+
if (fileConfig.baseUrl) config.models.chat.baseUrl = fileConfig.baseUrl;
|
|
134
|
+
if (fileConfig.apiKey) config.models.chat.apiKey = fileConfig.apiKey;
|
|
135
|
+
if (fileConfig.maxIterations) config.maxIterations = fileConfig.maxIterations;
|
|
136
|
+
if (fileConfig.useRepoMap !== void 0) config.useRepoMap = fileConfig.useRepoMap;
|
|
137
|
+
if (fileConfig.temperature !== void 0) config.models.chat.temperature = fileConfig.temperature;
|
|
138
|
+
if (fileConfig.renderMarkdown !== void 0) config.renderMarkdown = fileConfig.renderMarkdown;
|
|
139
|
+
if (fileConfig.enableMemory !== void 0) config.enableMemory = fileConfig.enableMemory;
|
|
140
|
+
if (fileConfig.enableHooks !== void 0) config.enableHooks = fileConfig.enableHooks;
|
|
141
|
+
if (fileConfig.permissionMode) config.permissionMode = fileConfig.permissionMode;
|
|
142
|
+
if (fileConfig.shellTimeout) config.shellTimeout = fileConfig.shellTimeout;
|
|
143
|
+
if (fileConfig.theme) config.theme = fileConfig.theme;
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
const creds = await loadCredentials();
|
|
147
|
+
if (creds?.token) {
|
|
148
|
+
config.models.chat.apiKey = creds.token;
|
|
149
|
+
}
|
|
150
|
+
if (process.env.NOTCH_MODEL && isValidModel(process.env.NOTCH_MODEL)) {
|
|
151
|
+
config.models.chat.model = process.env.NOTCH_MODEL;
|
|
152
|
+
}
|
|
153
|
+
if (process.env.NOTCH_BASE_URL) {
|
|
154
|
+
config.models.chat.baseUrl = process.env.NOTCH_BASE_URL;
|
|
155
|
+
}
|
|
156
|
+
if (process.env.NOTCH_API_KEY) {
|
|
157
|
+
config.models.chat.apiKey = process.env.NOTCH_API_KEY;
|
|
158
|
+
}
|
|
159
|
+
if (config.models.chat.temperature !== void 0) {
|
|
160
|
+
config.models.chat.temperature = Math.max(0, Math.min(2, config.models.chat.temperature));
|
|
161
|
+
}
|
|
162
|
+
config.maxIterations = Math.max(1, Math.min(100, config.maxIterations));
|
|
163
|
+
return { ...config, ...overrides };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/agent/loop.ts
|
|
167
|
+
import { streamText } from "ai";
|
|
168
|
+
|
|
169
|
+
// src/tools/index.ts
|
|
170
|
+
import { tool } from "ai";
|
|
171
|
+
|
|
172
|
+
// src/tools/read.ts
|
|
173
|
+
import fs2 from "fs/promises";
|
|
174
|
+
import path2 from "path";
|
|
175
|
+
import { z } from "zod";
|
|
176
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
|
|
177
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".zip", ".tar", ".gz", ".wasm", ".exe", ".dll", ".so", ".dylib"]);
|
|
178
|
+
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
179
|
+
var MAX_LINES = 2e3;
|
|
180
|
+
var parameters = z.object({
|
|
181
|
+
path: z.string().describe("Relative or absolute path to the file to read"),
|
|
182
|
+
offset: z.number().optional().describe("Line number to start reading from (1-based)"),
|
|
183
|
+
limit: z.number().optional().describe("Maximum number of lines to read")
|
|
184
|
+
});
|
|
185
|
+
async function readImage(filePath, ext) {
|
|
186
|
+
try {
|
|
187
|
+
const stat = await fs2.stat(filePath);
|
|
188
|
+
const buf = await fs2.readFile(filePath);
|
|
189
|
+
const mimeType = ext === ".svg" ? "image/svg+xml" : ext === ".png" ? "image/png" : ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : ext === ".bmp" ? "image/bmp" : ext === ".ico" ? "image/x-icon" : "image/jpeg";
|
|
190
|
+
if (ext === ".svg") {
|
|
191
|
+
const svgText = buf.toString("utf-8");
|
|
192
|
+
return {
|
|
193
|
+
content: `Image: ${filePath} (${(stat.size / 1024).toFixed(1)} KB, SVG)
|
|
194
|
+
|
|
195
|
+
${svgText.slice(0, 1e4)}`
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const base64 = buf.toString("base64");
|
|
199
|
+
return {
|
|
200
|
+
content: `Image: ${filePath} (${(stat.size / 1024).toFixed(1)} KB, ${ext})
|
|
201
|
+
data:${mimeType};base64,${base64.slice(0, 200)}... [${base64.length} chars total]
|
|
202
|
+
|
|
203
|
+
(Image data is available. Describe what changes to make if needed.)`
|
|
204
|
+
};
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return { content: `Error reading image ${filePath}: ${err.message}`, isError: true };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function readPDF(filePath) {
|
|
210
|
+
try {
|
|
211
|
+
const stat = await fs2.stat(filePath);
|
|
212
|
+
const buf = await fs2.readFile(filePath);
|
|
213
|
+
const text = buf.toString("latin1");
|
|
214
|
+
const textChunks = [];
|
|
215
|
+
const streamRegex = /stream\r?\n([\s\S]*?)\r?\nendstream/g;
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = streamRegex.exec(text)) !== null) {
|
|
218
|
+
const chunk = match[1].replace(/[^\x20-\x7E\n\r\t]/g, " ").replace(/\s+/g, " ").trim();
|
|
219
|
+
if (chunk.length > 10) {
|
|
220
|
+
textChunks.push(chunk);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const tjRegex = /\(((?:[^)\\]|\\.)*)\)\s*Tj/g;
|
|
224
|
+
while ((match = tjRegex.exec(text)) !== null) {
|
|
225
|
+
const decoded = match[1].replace(/\\(.)/g, "$1");
|
|
226
|
+
if (decoded.trim().length > 0) {
|
|
227
|
+
textChunks.push(decoded.trim());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (textChunks.length === 0) {
|
|
231
|
+
return {
|
|
232
|
+
content: `PDF: ${filePath} (${(stat.size / 1024).toFixed(1)} KB)
|
|
233
|
+
|
|
234
|
+
(Could not extract text \u2014 the PDF may be image-based or encrypted. Consider using an external tool for OCR.)`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const extractedText = textChunks.join("\n").slice(0, 5e4);
|
|
238
|
+
return {
|
|
239
|
+
content: `PDF: ${filePath} (${(stat.size / 1024).toFixed(1)} KB)
|
|
240
|
+
|
|
241
|
+
Extracted text:
|
|
242
|
+
${extractedText}`
|
|
243
|
+
};
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return { content: `Error reading PDF ${filePath}: ${err.message}`, isError: true };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
var readTool = {
|
|
249
|
+
name: "read",
|
|
250
|
+
description: 'Read file contents with line numbers. Use offset/limit for large files. Supports text files, images (returns metadata + base64), and PDFs (basic text extraction). Returns numbered lines like " 1\\tline content".',
|
|
251
|
+
parameters,
|
|
252
|
+
async execute(params, ctx) {
|
|
253
|
+
const filePath = path2.isAbsolute(params.path) ? params.path : path2.resolve(ctx.cwd, params.path);
|
|
254
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
255
|
+
try {
|
|
256
|
+
await fs2.access(filePath);
|
|
257
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
258
|
+
return await readImage(filePath, ext);
|
|
259
|
+
}
|
|
260
|
+
if (ext === ".pdf") {
|
|
261
|
+
return await readPDF(filePath);
|
|
262
|
+
}
|
|
263
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
264
|
+
const stat2 = await fs2.stat(filePath);
|
|
265
|
+
return {
|
|
266
|
+
content: `Binary file: ${filePath} (${(stat2.size / 1024).toFixed(1)} KB, ${ext})`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const stat = await fs2.stat(filePath);
|
|
270
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
271
|
+
return {
|
|
272
|
+
content: `File too large: ${filePath} (${(stat.size / 1024 / 1024).toFixed(1)} MB). Use offset/limit to read portions.`,
|
|
273
|
+
isError: true
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const cacheKey = `${filePath}:${params.offset ?? 0}:${params.limit ?? 0}`;
|
|
277
|
+
if (ctx._readCache?.has(cacheKey)) {
|
|
278
|
+
return { content: ctx._readCache.get(cacheKey) };
|
|
279
|
+
}
|
|
280
|
+
const raw = await fs2.readFile(filePath, "utf-8");
|
|
281
|
+
const allLines = raw.split("\n");
|
|
282
|
+
const offset = Math.max(0, (params.offset ?? 1) - 1);
|
|
283
|
+
const limit = params.limit ?? MAX_LINES;
|
|
284
|
+
const lines = allLines.slice(offset, offset + limit);
|
|
285
|
+
const numbered = lines.map((line, i) => {
|
|
286
|
+
const lineNum = String(offset + i + 1).padStart(5);
|
|
287
|
+
return `${lineNum} ${line}`;
|
|
288
|
+
}).join("\n");
|
|
289
|
+
const result = allLines.length > offset + limit ? `${numbered}
|
|
290
|
+
|
|
291
|
+
(${allLines.length - offset - limit} more lines. Use offset=${offset + limit + 1} to continue.)` : numbered;
|
|
292
|
+
ctx._readCache?.set(cacheKey, result);
|
|
293
|
+
return { content: result };
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (err.code === "ENOENT") {
|
|
296
|
+
return { content: `File not found: ${filePath}`, isError: true };
|
|
297
|
+
}
|
|
298
|
+
return { content: `Error reading ${filePath}: ${err.message}`, isError: true };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/tools/write.ts
|
|
304
|
+
import fs3 from "fs/promises";
|
|
305
|
+
import path3 from "path";
|
|
306
|
+
import { z as z2 } from "zod";
|
|
307
|
+
var parameters2 = z2.object({
|
|
308
|
+
path: z2.string().describe("Relative or absolute path to write"),
|
|
309
|
+
content: z2.string().describe("Complete file content to write")
|
|
310
|
+
});
|
|
311
|
+
var writeTool = {
|
|
312
|
+
name: "write",
|
|
313
|
+
description: "Write a file (create or overwrite). Creates parent directories if needed. Always provide the COMPLETE file content.",
|
|
314
|
+
parameters: parameters2,
|
|
315
|
+
async execute(params, ctx) {
|
|
316
|
+
const filePath = path3.isAbsolute(params.path) ? params.path : path3.resolve(ctx.cwd, params.path);
|
|
317
|
+
try {
|
|
318
|
+
await fs3.mkdir(path3.dirname(filePath), { recursive: true });
|
|
319
|
+
let existed = false;
|
|
320
|
+
try {
|
|
321
|
+
await fs3.access(filePath);
|
|
322
|
+
existed = true;
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
await fs3.writeFile(filePath, params.content, "utf-8");
|
|
326
|
+
const lines = params.content.split("\n").length;
|
|
327
|
+
const action = existed ? "Updated" : "Created";
|
|
328
|
+
return { content: `${action} ${filePath} (${lines} lines)` };
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return { content: `Error writing ${filePath}: ${err.message}`, isError: true };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// src/tools/edit.ts
|
|
336
|
+
import fs4 from "fs/promises";
|
|
337
|
+
import path4 from "path";
|
|
338
|
+
import { z as z3 } from "zod";
|
|
339
|
+
var parameters3 = z3.object({
|
|
340
|
+
path: z3.string().describe("Relative or absolute path to the file"),
|
|
341
|
+
old_string: z3.string().describe("Exact string to find and replace (must be unique in file)"),
|
|
342
|
+
new_string: z3.string().describe("Replacement string"),
|
|
343
|
+
replace_all: z3.boolean().optional().default(false).describe("Replace all occurrences")
|
|
344
|
+
});
|
|
345
|
+
var editTool = {
|
|
346
|
+
name: "edit",
|
|
347
|
+
description: "Perform exact string replacement in a file. old_string must match exactly and be unique unless replace_all is true. Use for surgical edits.",
|
|
348
|
+
parameters: parameters3,
|
|
349
|
+
async execute(params, ctx) {
|
|
350
|
+
const filePath = path4.isAbsolute(params.path) ? params.path : path4.resolve(ctx.cwd, params.path);
|
|
351
|
+
try {
|
|
352
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
353
|
+
if (!content.includes(params.old_string)) {
|
|
354
|
+
return {
|
|
355
|
+
content: `old_string not found in ${filePath}. Make sure it matches exactly (including whitespace and indentation).`,
|
|
356
|
+
isError: true
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (!params.replace_all) {
|
|
360
|
+
const firstIdx = content.indexOf(params.old_string);
|
|
361
|
+
const secondIdx = content.indexOf(params.old_string, firstIdx + 1);
|
|
362
|
+
if (secondIdx !== -1) {
|
|
363
|
+
return {
|
|
364
|
+
content: `old_string appears multiple times in ${filePath}. Provide more surrounding context to make it unique, or set replace_all: true.`,
|
|
365
|
+
isError: true
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const newContent = params.replace_all ? content.replaceAll(params.old_string, params.new_string) : content.replace(params.old_string, params.new_string);
|
|
370
|
+
await fs4.writeFile(filePath, newContent, "utf-8");
|
|
371
|
+
const oldLines = params.old_string.split("\n").length;
|
|
372
|
+
const newLines = params.new_string.split("\n").length;
|
|
373
|
+
return {
|
|
374
|
+
content: `Edited ${filePath}: replaced ${oldLines} line(s) with ${newLines} line(s)`
|
|
375
|
+
};
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (err.code === "ENOENT") {
|
|
378
|
+
return { content: `File not found: ${filePath}`, isError: true };
|
|
379
|
+
}
|
|
380
|
+
return { content: `Error editing ${filePath}: ${err.message}`, isError: true };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/tools/shell.ts
|
|
386
|
+
import { execSync } from "child_process";
|
|
387
|
+
import { z as z4 } from "zod";
|
|
388
|
+
var BLOCKED_PATTERNS = [
|
|
389
|
+
/rm\s+-rf\s+\/(?!\S)/,
|
|
390
|
+
// rm -rf /
|
|
391
|
+
/mkfs\./,
|
|
392
|
+
/dd\s+if=.*of=\/dev/,
|
|
393
|
+
/:\(\)\s*\{.*:\|:.*\}/,
|
|
394
|
+
// fork bomb variants
|
|
395
|
+
/chmod\s+-R\s+777\s+\//
|
|
396
|
+
// recursive chmod on root
|
|
397
|
+
];
|
|
398
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
399
|
+
/rm\s+-rf/,
|
|
400
|
+
/rm\s+-r\s/,
|
|
401
|
+
/git\s+push\s+--force(?!\s+--with-lease)/,
|
|
402
|
+
/git\s+reset\s+--hard/,
|
|
403
|
+
/DROP\s+(TABLE|DATABASE)/i,
|
|
404
|
+
/TRUNCATE/i,
|
|
405
|
+
/>\s*\/dev\/sd/
|
|
406
|
+
];
|
|
407
|
+
var MAX_OUTPUT = 5e4;
|
|
408
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
409
|
+
var MAX_TIMEOUT = 6e5;
|
|
410
|
+
var parameters4 = z4.object({
|
|
411
|
+
command: z4.string().describe("Shell command to execute"),
|
|
412
|
+
timeout: z4.number().optional().describe("Timeout in ms (default 30s, max configurable up to 10m)")
|
|
413
|
+
});
|
|
414
|
+
function validateCommand(command, cwd) {
|
|
415
|
+
const absolutePathRegex = /(?:^|\s)(?:>|>>|cat|cp|mv|ln)\s+(\/(?!tmp|dev\/null)[^\s]+)/g;
|
|
416
|
+
let match;
|
|
417
|
+
while ((match = absolutePathRegex.exec(command)) !== null) {
|
|
418
|
+
const targetPath = match[1];
|
|
419
|
+
if (!targetPath.startsWith(cwd) && !targetPath.startsWith("/tmp")) {
|
|
420
|
+
return `Blocked: command targets path outside project root: ${targetPath}`;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
var shellTool = {
|
|
426
|
+
name: "shell",
|
|
427
|
+
description: "Execute a shell command in the project directory. Dangerous commands (rm -rf, DROP TABLE, git push --force) require confirmation. Some destructive system commands are blocked entirely.",
|
|
428
|
+
parameters: parameters4,
|
|
429
|
+
async execute(params, ctx) {
|
|
430
|
+
const { command } = params;
|
|
431
|
+
const maxTimeout = ctx.shellTimeout ?? MAX_TIMEOUT;
|
|
432
|
+
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, maxTimeout);
|
|
433
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
434
|
+
if (pattern.test(command)) {
|
|
435
|
+
return {
|
|
436
|
+
content: `Blocked: "${command}" is too dangerous to execute.`,
|
|
437
|
+
isError: true
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const pathError = validateCommand(command, ctx.cwd);
|
|
442
|
+
if (pathError) {
|
|
443
|
+
return { content: pathError, isError: true };
|
|
444
|
+
}
|
|
445
|
+
if (ctx.requireConfirm) {
|
|
446
|
+
for (const pattern of DESTRUCTIVE_PATTERNS) {
|
|
447
|
+
if (pattern.test(command)) {
|
|
448
|
+
const confirmed = await ctx.confirm(
|
|
449
|
+
`\u26A0 Destructive command: ${command}
|
|
450
|
+
Proceed?`
|
|
451
|
+
);
|
|
452
|
+
if (!confirmed) {
|
|
453
|
+
return { content: "Command cancelled by user.", isError: true };
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const output = execSync(command, {
|
|
461
|
+
cwd: ctx.cwd,
|
|
462
|
+
encoding: "utf-8",
|
|
463
|
+
timeout,
|
|
464
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
465
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
466
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
467
|
+
});
|
|
468
|
+
const trimmed = output.length > MAX_OUTPUT ? output.slice(0, MAX_OUTPUT) + `
|
|
469
|
+
... (truncated, ${output.length} chars total)` : output;
|
|
470
|
+
return { content: trimmed || "(no output)" };
|
|
471
|
+
} catch (err) {
|
|
472
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
473
|
+
const stdout = err.stdout?.toString() ?? "";
|
|
474
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
475
|
+
const trimmed = combined.length > MAX_OUTPUT ? combined.slice(0, MAX_OUTPUT) + "\n... (truncated)" : combined;
|
|
476
|
+
if (err.killed && err.signal === "SIGTERM") {
|
|
477
|
+
return {
|
|
478
|
+
content: `Command timed out after ${(timeout / 1e3).toFixed(0)}s: ${command}
|
|
479
|
+
|
|
480
|
+
Partial output:
|
|
481
|
+
${trimmed || "(none)"}`,
|
|
482
|
+
isError: true
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
content: `Command failed (exit ${err.status ?? "unknown"}):
|
|
487
|
+
${trimmed || err.message}`,
|
|
488
|
+
isError: true
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// src/tools/git.ts
|
|
495
|
+
import { simpleGit } from "simple-git";
|
|
496
|
+
import { z as z5 } from "zod";
|
|
497
|
+
var PROTECTED_BRANCHES = ["main", "master", "production", "release"];
|
|
498
|
+
var parameters5 = z5.object({
|
|
499
|
+
operation: z5.enum([
|
|
500
|
+
"status",
|
|
501
|
+
"diff",
|
|
502
|
+
"log",
|
|
503
|
+
"commit",
|
|
504
|
+
"branch",
|
|
505
|
+
"checkout",
|
|
506
|
+
"push",
|
|
507
|
+
"pull",
|
|
508
|
+
"stash",
|
|
509
|
+
"add",
|
|
510
|
+
"show"
|
|
511
|
+
]).describe("Git operation to perform"),
|
|
512
|
+
args: z5.string().optional().describe("Additional arguments (e.g., branch name, commit message, file paths)")
|
|
513
|
+
});
|
|
514
|
+
var gitTool = {
|
|
515
|
+
name: "git",
|
|
516
|
+
description: "Perform git operations with built-in safety. Always prefer this over shell for git commands. Blocks force-push to protected branches. Operations: status, diff, log, commit, branch, checkout, push, pull, stash, add, show.",
|
|
517
|
+
parameters: parameters5,
|
|
518
|
+
async execute(params, ctx) {
|
|
519
|
+
const git = simpleGit(ctx.cwd);
|
|
520
|
+
try {
|
|
521
|
+
switch (params.operation) {
|
|
522
|
+
case "status": {
|
|
523
|
+
const status = await git.status();
|
|
524
|
+
const lines = [
|
|
525
|
+
`Branch: ${status.current}`,
|
|
526
|
+
`Ahead: ${status.ahead} Behind: ${status.behind}`,
|
|
527
|
+
status.staged.length ? `Staged: ${status.staged.join(", ")}` : null,
|
|
528
|
+
status.modified.length ? `Modified: ${status.modified.join(", ")}` : null,
|
|
529
|
+
status.not_added.length ? `Untracked: ${status.not_added.join(", ")}` : null,
|
|
530
|
+
status.deleted.length ? `Deleted: ${status.deleted.join(", ")}` : null
|
|
531
|
+
].filter(Boolean);
|
|
532
|
+
return { content: lines.join("\n") };
|
|
533
|
+
}
|
|
534
|
+
case "diff": {
|
|
535
|
+
const diff = await git.diff(params.args?.split(" ") ?? []);
|
|
536
|
+
return { content: diff || "(no changes)" };
|
|
537
|
+
}
|
|
538
|
+
case "log": {
|
|
539
|
+
const log = await git.log({ maxCount: 10, ...params.args ? {} : {} });
|
|
540
|
+
const entries = log.all.map(
|
|
541
|
+
(c) => `${c.hash.slice(0, 8)} ${c.date} ${c.message}`
|
|
542
|
+
);
|
|
543
|
+
return { content: entries.join("\n") || "(no commits)" };
|
|
544
|
+
}
|
|
545
|
+
case "commit": {
|
|
546
|
+
if (!params.args) {
|
|
547
|
+
return { content: 'Commit requires a message. Use args: "your commit message"', isError: true };
|
|
548
|
+
}
|
|
549
|
+
const result = await git.commit(params.args);
|
|
550
|
+
return {
|
|
551
|
+
content: `Committed: ${result.commit} (${result.summary.changes} changes, ${result.summary.insertions} insertions, ${result.summary.deletions} deletions)`
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
case "add": {
|
|
555
|
+
const files = params.args?.split(" ") ?? ["."];
|
|
556
|
+
const sensitive = files.filter(
|
|
557
|
+
(f) => /\.(env|pem|key|credentials|secret)/i.test(f)
|
|
558
|
+
);
|
|
559
|
+
if (sensitive.length > 0) {
|
|
560
|
+
const confirmed = await ctx.confirm(
|
|
561
|
+
`\u26A0 Staging potentially sensitive files: ${sensitive.join(", ")}
|
|
562
|
+
Proceed?`
|
|
563
|
+
);
|
|
564
|
+
if (!confirmed) {
|
|
565
|
+
return { content: "Staging cancelled.", isError: true };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
await git.add(files);
|
|
569
|
+
return { content: `Staged: ${files.join(", ")}` };
|
|
570
|
+
}
|
|
571
|
+
case "branch": {
|
|
572
|
+
if (params.args) {
|
|
573
|
+
await git.branch(params.args.split(" "));
|
|
574
|
+
return { content: `Branch operation completed: ${params.args}` };
|
|
575
|
+
}
|
|
576
|
+
const branches = await git.branch();
|
|
577
|
+
return { content: branches.all.map((b) => `${b === branches.current ? "* " : " "}${b}`).join("\n") };
|
|
578
|
+
}
|
|
579
|
+
case "checkout": {
|
|
580
|
+
if (!params.args) {
|
|
581
|
+
return { content: "Checkout requires a branch or file path.", isError: true };
|
|
582
|
+
}
|
|
583
|
+
await git.checkout(params.args.split(" "));
|
|
584
|
+
return { content: `Checked out: ${params.args}` };
|
|
585
|
+
}
|
|
586
|
+
case "push": {
|
|
587
|
+
const currentBranch = (await git.status()).current ?? "";
|
|
588
|
+
const args = params.args?.split(" ") ?? [];
|
|
589
|
+
const isForce = args.includes("--force") || args.includes("-f");
|
|
590
|
+
if (isForce && PROTECTED_BRANCHES.includes(currentBranch)) {
|
|
591
|
+
return {
|
|
592
|
+
content: `Blocked: cannot force-push to protected branch "${currentBranch}". Use --force-with-lease instead.`,
|
|
593
|
+
isError: true
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
await git.push(args);
|
|
597
|
+
return { content: `Pushed ${currentBranch} successfully` };
|
|
598
|
+
}
|
|
599
|
+
case "pull": {
|
|
600
|
+
const pullResult = await git.pull(params.args?.split(" "));
|
|
601
|
+
return {
|
|
602
|
+
content: `Pulled: ${pullResult.summary.changes} changes, ${pullResult.summary.insertions} insertions, ${pullResult.summary.deletions} deletions`
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
case "stash": {
|
|
606
|
+
if (params.args === "pop") {
|
|
607
|
+
await git.stash(["pop"]);
|
|
608
|
+
return { content: "Stash popped" };
|
|
609
|
+
}
|
|
610
|
+
if (params.args === "list") {
|
|
611
|
+
const list = await git.stashList();
|
|
612
|
+
return { content: list.all.map((s) => `${s.hash} ${s.message}`).join("\n") || "(no stashes)" };
|
|
613
|
+
}
|
|
614
|
+
await git.stash(params.args?.split(" "));
|
|
615
|
+
return { content: "Changes stashed" };
|
|
616
|
+
}
|
|
617
|
+
case "show": {
|
|
618
|
+
const raw = await git.show(params.args?.split(" ") ?? []);
|
|
619
|
+
return { content: raw };
|
|
620
|
+
}
|
|
621
|
+
default:
|
|
622
|
+
return { content: `Unknown git operation: ${params.operation}`, isError: true };
|
|
623
|
+
}
|
|
624
|
+
} catch (err) {
|
|
625
|
+
return { content: `Git error: ${err.message}`, isError: true };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/tools/grep.ts
|
|
631
|
+
import { execSync as execSync2 } from "child_process";
|
|
632
|
+
import fs5 from "fs/promises";
|
|
633
|
+
import path5 from "path";
|
|
634
|
+
import { z as z6 } from "zod";
|
|
635
|
+
var parameters6 = z6.object({
|
|
636
|
+
pattern: z6.string().describe("Regex pattern to search for"),
|
|
637
|
+
path: z6.string().optional().describe("File or directory to search (defaults to project root)"),
|
|
638
|
+
glob: z6.string().optional().describe('Glob filter, e.g. "*.ts" or "**/*.tsx"'),
|
|
639
|
+
context: z6.number().optional().default(0).describe("Lines of context around each match"),
|
|
640
|
+
max_results: z6.number().optional().default(50).describe("Maximum number of matches to return")
|
|
641
|
+
});
|
|
642
|
+
var grepTool = {
|
|
643
|
+
name: "grep",
|
|
644
|
+
description: "Search file contents by regex pattern. Returns matching lines with file paths and line numbers. Supports glob filtering and context lines.",
|
|
645
|
+
parameters: parameters6,
|
|
646
|
+
async execute(params, ctx) {
|
|
647
|
+
const searchPath = params.path ? path5.isAbsolute(params.path) ? params.path : path5.resolve(ctx.cwd, params.path) : ctx.cwd;
|
|
648
|
+
try {
|
|
649
|
+
const rgArgs = [
|
|
650
|
+
"rg",
|
|
651
|
+
"--no-heading",
|
|
652
|
+
"--line-number",
|
|
653
|
+
"--color=never",
|
|
654
|
+
`--max-count=${params.max_results}`,
|
|
655
|
+
params.context ? `-C ${params.context}` : "",
|
|
656
|
+
params.glob ? `--glob "${params.glob}"` : "",
|
|
657
|
+
"--",
|
|
658
|
+
JSON.stringify(params.pattern),
|
|
659
|
+
searchPath
|
|
660
|
+
].filter(Boolean).join(" ");
|
|
661
|
+
const output = execSync2(rgArgs, {
|
|
662
|
+
cwd: ctx.cwd,
|
|
663
|
+
encoding: "utf-8",
|
|
664
|
+
timeout: 15e3,
|
|
665
|
+
maxBuffer: 5 * 1024 * 1024
|
|
666
|
+
});
|
|
667
|
+
return { content: output || "(no matches)" };
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
const regex = new RegExp(params.pattern, "g");
|
|
672
|
+
const results = [];
|
|
673
|
+
async function searchDir(dir) {
|
|
674
|
+
if (results.length >= (params.max_results ?? 50)) return;
|
|
675
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
676
|
+
for (const entry of entries) {
|
|
677
|
+
if (results.length >= (params.max_results ?? 50)) return;
|
|
678
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
679
|
+
const full = path5.join(dir, entry.name);
|
|
680
|
+
if (entry.isDirectory()) {
|
|
681
|
+
await searchDir(full);
|
|
682
|
+
} else if (entry.isFile()) {
|
|
683
|
+
if (params.glob) {
|
|
684
|
+
const ext = params.glob.replace(/^\*+\.?/, "");
|
|
685
|
+
if (ext && !entry.name.endsWith(ext)) continue;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
const content = await fs5.readFile(full, "utf-8");
|
|
689
|
+
const lines = content.split("\n");
|
|
690
|
+
for (let i = 0; i < lines.length; i++) {
|
|
691
|
+
if (regex.test(lines[i])) {
|
|
692
|
+
const rel = path5.relative(ctx.cwd, full);
|
|
693
|
+
results.push(`${rel}:${i + 1}:${lines[i]}`);
|
|
694
|
+
regex.lastIndex = 0;
|
|
695
|
+
if (results.length >= (params.max_results ?? 50)) return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const stat = await fs5.stat(searchPath);
|
|
704
|
+
if (stat.isFile()) {
|
|
705
|
+
const content = await fs5.readFile(searchPath, "utf-8");
|
|
706
|
+
const lines = content.split("\n");
|
|
707
|
+
const rel = path5.relative(ctx.cwd, searchPath);
|
|
708
|
+
for (let i = 0; i < lines.length; i++) {
|
|
709
|
+
if (regex.test(lines[i])) {
|
|
710
|
+
results.push(`${rel}:${i + 1}:${lines[i]}`);
|
|
711
|
+
regex.lastIndex = 0;
|
|
712
|
+
if (results.length >= (params.max_results ?? 50)) break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
await searchDir(searchPath);
|
|
717
|
+
}
|
|
718
|
+
return { content: results.join("\n") || "(no matches)" };
|
|
719
|
+
} catch (err) {
|
|
720
|
+
return { content: `Search error: ${err.message}`, isError: true };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// src/tools/glob.ts
|
|
726
|
+
import { glob as globFn } from "glob";
|
|
727
|
+
import path6 from "path";
|
|
728
|
+
import { z as z7 } from "zod";
|
|
729
|
+
var parameters7 = z7.object({
|
|
730
|
+
pattern: z7.string().describe('Glob pattern, e.g. "**/*.ts" or "src/**/*.tsx"'),
|
|
731
|
+
path: z7.string().optional().describe("Directory to search (defaults to project root)")
|
|
732
|
+
});
|
|
733
|
+
var globTool = {
|
|
734
|
+
name: "glob",
|
|
735
|
+
description: "Find files matching a glob pattern. Returns relative paths sorted by modification time. Ignores node_modules and .git by default.",
|
|
736
|
+
parameters: parameters7,
|
|
737
|
+
async execute(params, ctx) {
|
|
738
|
+
const searchDir = params.path ? path6.isAbsolute(params.path) ? params.path : path6.resolve(ctx.cwd, params.path) : ctx.cwd;
|
|
739
|
+
try {
|
|
740
|
+
const matches = await globFn(params.pattern, {
|
|
741
|
+
cwd: searchDir,
|
|
742
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
|
|
743
|
+
nodir: true,
|
|
744
|
+
dot: false
|
|
745
|
+
});
|
|
746
|
+
if (matches.length === 0) {
|
|
747
|
+
return { content: `No files matching "${params.pattern}" in ${path6.relative(ctx.cwd, searchDir) || "."}` };
|
|
748
|
+
}
|
|
749
|
+
const relative = matches.map((m) => path6.relative(ctx.cwd, path6.resolve(searchDir, m)));
|
|
750
|
+
return {
|
|
751
|
+
content: `Found ${relative.length} file(s):
|
|
752
|
+
${relative.join("\n")}`
|
|
753
|
+
};
|
|
754
|
+
} catch (err) {
|
|
755
|
+
return { content: `Glob error: ${err.message}`, isError: true };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// src/tools/web-fetch.ts
|
|
761
|
+
import { z as z8 } from "zod";
|
|
762
|
+
var MAX_CONTENT_LENGTH = 5e4;
|
|
763
|
+
var FETCH_TIMEOUT = 15e3;
|
|
764
|
+
var parameters8 = z8.object({
|
|
765
|
+
url: z8.string().describe("URL to fetch"),
|
|
766
|
+
max_length: z8.number().optional().default(MAX_CONTENT_LENGTH).describe("Max characters to return")
|
|
767
|
+
});
|
|
768
|
+
function htmlToText(html) {
|
|
769
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, "").replace(/<footer[\s\S]*?<\/footer>/gi, "").replace(/<header[\s\S]*?<\/header>/gi, "").replace(/<\/?(p|div|br|h[1-6]|li|tr|blockquote|pre|hr)[^>]*>/gi, "\n").replace(/<li[^>]*>/gi, "\n- ").replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/\n{3,}/g, "\n\n").replace(/[ \t]+/g, " ").trim();
|
|
770
|
+
}
|
|
771
|
+
var webFetchTool = {
|
|
772
|
+
name: "web_fetch",
|
|
773
|
+
description: "Fetch a web page and return its text content. Useful for reading documentation, API references, error lookups, and other web resources. Strips HTML to plain text.",
|
|
774
|
+
parameters: parameters8,
|
|
775
|
+
async execute(params, _ctx) {
|
|
776
|
+
const { url } = params;
|
|
777
|
+
const maxLen = params.max_length ?? MAX_CONTENT_LENGTH;
|
|
778
|
+
try {
|
|
779
|
+
new URL(url);
|
|
780
|
+
} catch {
|
|
781
|
+
return { content: `Invalid URL: ${url}`, isError: true };
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const response = await fetch(url, {
|
|
785
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
786
|
+
headers: {
|
|
787
|
+
"User-Agent": "Notch-CLI/0.1 (AI coding assistant)",
|
|
788
|
+
"Accept": "text/html,application/json,text/plain"
|
|
789
|
+
},
|
|
790
|
+
redirect: "follow"
|
|
791
|
+
});
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
return {
|
|
794
|
+
content: `HTTP ${response.status} ${response.statusText} for ${url}`,
|
|
795
|
+
isError: true
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
799
|
+
const raw = await response.text();
|
|
800
|
+
let text;
|
|
801
|
+
if (contentType.includes("application/json")) {
|
|
802
|
+
try {
|
|
803
|
+
text = JSON.stringify(JSON.parse(raw), null, 2);
|
|
804
|
+
} catch {
|
|
805
|
+
text = raw;
|
|
806
|
+
}
|
|
807
|
+
} else if (contentType.includes("text/html")) {
|
|
808
|
+
text = htmlToText(raw);
|
|
809
|
+
} else {
|
|
810
|
+
text = raw;
|
|
811
|
+
}
|
|
812
|
+
if (text.length > maxLen) {
|
|
813
|
+
text = text.slice(0, maxLen) + `
|
|
814
|
+
|
|
815
|
+
... (truncated, ${text.length} chars total)`;
|
|
816
|
+
}
|
|
817
|
+
return { content: `Fetched ${url} (${response.status}):
|
|
818
|
+
|
|
819
|
+
${text}` };
|
|
820
|
+
} catch (err) {
|
|
821
|
+
if (err.name === "AbortError" || err.name === "TimeoutError") {
|
|
822
|
+
return { content: `Timeout fetching ${url} (${FETCH_TIMEOUT / 1e3}s limit)`, isError: true };
|
|
823
|
+
}
|
|
824
|
+
return { content: `Fetch error for ${url}: ${err.message}`, isError: true };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// src/tools/index.ts
|
|
830
|
+
var ALL_TOOLS = [
|
|
831
|
+
readTool,
|
|
832
|
+
writeTool,
|
|
833
|
+
editTool,
|
|
834
|
+
shellTool,
|
|
835
|
+
gitTool,
|
|
836
|
+
grepTool,
|
|
837
|
+
globTool,
|
|
838
|
+
webFetchTool
|
|
839
|
+
];
|
|
840
|
+
function buildToolMap(ctx) {
|
|
841
|
+
const map = {};
|
|
842
|
+
for (const t of ALL_TOOLS) {
|
|
843
|
+
map[t.name] = tool({
|
|
844
|
+
description: t.description,
|
|
845
|
+
parameters: t.parameters,
|
|
846
|
+
execute: async (params) => {
|
|
847
|
+
if (ctx.checkPermission) {
|
|
848
|
+
const level = ctx.checkPermission(t.name, params);
|
|
849
|
+
if (level === "deny") {
|
|
850
|
+
return {
|
|
851
|
+
content: `Permission denied: ${t.name} is not allowed by your permission config.`,
|
|
852
|
+
isError: true
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
if (level === "prompt" && ctx.requireConfirm) {
|
|
856
|
+
const paramSummary = Object.entries(params).map(([k, v]) => `${k}=${String(v).slice(0, 80)}`).join(", ");
|
|
857
|
+
const confirmed = await ctx.confirm(
|
|
858
|
+
`Tool ${t.name}(${paramSummary}) requires approval. Proceed?`
|
|
859
|
+
);
|
|
860
|
+
if (!confirmed) {
|
|
861
|
+
return { content: "Cancelled by user.", isError: true };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (ctx.dryRun && ["write", "edit", "shell", "git"].includes(t.name)) {
|
|
866
|
+
const paramSummary = JSON.stringify(params, null, 2).slice(0, 500);
|
|
867
|
+
return {
|
|
868
|
+
content: `[DRY RUN] Would execute ${t.name}:
|
|
869
|
+
${paramSummary}`
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
await ctx.runHook?.("pre-tool", { tool: t.name, args: params });
|
|
873
|
+
const result = await t.execute(params, ctx);
|
|
874
|
+
await ctx.runHook?.("post-tool", {
|
|
875
|
+
tool: t.name,
|
|
876
|
+
args: params,
|
|
877
|
+
result: result.content.slice(0, 500),
|
|
878
|
+
isError: result.isError ?? false
|
|
879
|
+
});
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return map;
|
|
885
|
+
}
|
|
886
|
+
function describeTools() {
|
|
887
|
+
return ALL_TOOLS.map(
|
|
888
|
+
(t) => `- **${t.name}**: ${t.description}`
|
|
889
|
+
).join("\n");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/context/project-instructions.ts
|
|
893
|
+
import fs6 from "fs/promises";
|
|
894
|
+
import path7 from "path";
|
|
895
|
+
import os from "os";
|
|
896
|
+
var INSTRUCTION_FILES = [".notch.md", "NOTCH.md", ".notch/instructions.md"];
|
|
897
|
+
async function loadProjectInstructions(projectRoot) {
|
|
898
|
+
const sources = [];
|
|
899
|
+
const homeDir = os.homedir();
|
|
900
|
+
for (const file of INSTRUCTION_FILES) {
|
|
901
|
+
const globalPath = path7.join(homeDir, file);
|
|
902
|
+
const content = await safeRead(globalPath);
|
|
903
|
+
if (content) {
|
|
904
|
+
sources.push({ path: globalPath, content, scope: "global" });
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
for (const file of INSTRUCTION_FILES) {
|
|
909
|
+
const projectPath = path7.join(projectRoot, file);
|
|
910
|
+
const content = await safeRead(projectPath);
|
|
911
|
+
if (content) {
|
|
912
|
+
sources.push({ path: projectPath, content, scope: "project" });
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (sources.length === 0) return "";
|
|
917
|
+
const sections = sources.map((s) => {
|
|
918
|
+
const label = s.scope === "global" ? "Global Instructions" : "Project Instructions";
|
|
919
|
+
return `## ${label}
|
|
920
|
+
_Source: ${s.path}_
|
|
921
|
+
|
|
922
|
+
${s.content}`;
|
|
923
|
+
});
|
|
924
|
+
return `
|
|
925
|
+
# Custom Instructions
|
|
926
|
+
|
|
927
|
+
${sections.join("\n\n")}`;
|
|
928
|
+
}
|
|
929
|
+
async function safeRead(filePath) {
|
|
930
|
+
try {
|
|
931
|
+
const content = await fs6.readFile(filePath, "utf-8");
|
|
932
|
+
return content.trim() || null;
|
|
933
|
+
} catch {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/memory/store.ts
|
|
939
|
+
import fs7 from "fs/promises";
|
|
940
|
+
import path8 from "path";
|
|
941
|
+
import os2 from "os";
|
|
942
|
+
var MEMORY_DIR = path8.join(os2.homedir(), ".notch", "memory");
|
|
943
|
+
var INDEX_FILE = path8.join(MEMORY_DIR, "MEMORY.md");
|
|
944
|
+
async function ensureDir() {
|
|
945
|
+
await fs7.mkdir(MEMORY_DIR, { recursive: true });
|
|
946
|
+
}
|
|
947
|
+
async function saveMemory(memory) {
|
|
948
|
+
await ensureDir();
|
|
949
|
+
const slug = memory.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
950
|
+
const filename = `${memory.type}_${slug}.md`;
|
|
951
|
+
const filePath = path8.join(MEMORY_DIR, filename);
|
|
952
|
+
const fileContent = [
|
|
953
|
+
"---",
|
|
954
|
+
`name: ${memory.name}`,
|
|
955
|
+
`description: ${memory.description}`,
|
|
956
|
+
`type: ${memory.type}`,
|
|
957
|
+
`updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
958
|
+
"---",
|
|
959
|
+
"",
|
|
960
|
+
memory.content
|
|
961
|
+
].join("\n");
|
|
962
|
+
await fs7.writeFile(filePath, fileContent, "utf-8");
|
|
963
|
+
await updateIndex();
|
|
964
|
+
return filename;
|
|
965
|
+
}
|
|
966
|
+
async function loadMemories() {
|
|
967
|
+
await ensureDir();
|
|
968
|
+
const files = await fs7.readdir(MEMORY_DIR);
|
|
969
|
+
const memories = [];
|
|
970
|
+
for (const file of files) {
|
|
971
|
+
if (!file.endsWith(".md") || file === "MEMORY.md") continue;
|
|
972
|
+
try {
|
|
973
|
+
const content = await fs7.readFile(path8.join(MEMORY_DIR, file), "utf-8");
|
|
974
|
+
const memory = parseMemoryFile(content, file);
|
|
975
|
+
if (memory) memories.push(memory);
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return memories;
|
|
980
|
+
}
|
|
981
|
+
async function deleteMemory(filename) {
|
|
982
|
+
try {
|
|
983
|
+
await fs7.unlink(path8.join(MEMORY_DIR, filename));
|
|
984
|
+
await updateIndex();
|
|
985
|
+
return true;
|
|
986
|
+
} catch {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function searchMemories(query) {
|
|
991
|
+
const all = await loadMemories();
|
|
992
|
+
const q = query.toLowerCase();
|
|
993
|
+
return all.filter(
|
|
994
|
+
(m) => m.name.toLowerCase().includes(q) || m.description.toLowerCase().includes(q) || m.content.toLowerCase().includes(q)
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
async function formatMemoriesForPrompt() {
|
|
998
|
+
const memories = await loadMemories();
|
|
999
|
+
if (memories.length === 0) return "";
|
|
1000
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1001
|
+
for (const m of memories) {
|
|
1002
|
+
const list = byType.get(m.type) ?? [];
|
|
1003
|
+
list.push(m);
|
|
1004
|
+
byType.set(m.type, list);
|
|
1005
|
+
}
|
|
1006
|
+
const sections = ["\n## Memory (from previous sessions)\n"];
|
|
1007
|
+
const typeLabels = {
|
|
1008
|
+
user: "User Profile",
|
|
1009
|
+
feedback: "Behavioral Guidance",
|
|
1010
|
+
project: "Project Context",
|
|
1011
|
+
reference: "External References"
|
|
1012
|
+
};
|
|
1013
|
+
for (const [type, label] of Object.entries(typeLabels)) {
|
|
1014
|
+
const mems = byType.get(type);
|
|
1015
|
+
if (!mems || mems.length === 0) continue;
|
|
1016
|
+
sections.push(`### ${label}`);
|
|
1017
|
+
for (const m of mems) {
|
|
1018
|
+
sections.push(`- **${m.name}**: ${m.content.slice(0, 300)}`);
|
|
1019
|
+
}
|
|
1020
|
+
sections.push("");
|
|
1021
|
+
}
|
|
1022
|
+
return sections.join("\n");
|
|
1023
|
+
}
|
|
1024
|
+
function parseMemoryFile(raw, filename) {
|
|
1025
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1026
|
+
if (!fmMatch) return null;
|
|
1027
|
+
const frontmatter = fmMatch[1];
|
|
1028
|
+
const content = fmMatch[2].trim();
|
|
1029
|
+
const name = extractField(frontmatter, "name") ?? filename;
|
|
1030
|
+
const description = extractField(frontmatter, "description") ?? "";
|
|
1031
|
+
const type = extractField(frontmatter, "type") ?? "project";
|
|
1032
|
+
return { name, description, type, content, filename };
|
|
1033
|
+
}
|
|
1034
|
+
function extractField(frontmatter, field) {
|
|
1035
|
+
const match = frontmatter.match(new RegExp(`^${field}:\\s*(.+)$`, "m"));
|
|
1036
|
+
return match?.[1]?.trim() ?? null;
|
|
1037
|
+
}
|
|
1038
|
+
async function updateIndex() {
|
|
1039
|
+
const memories = await loadMemories();
|
|
1040
|
+
const lines = [
|
|
1041
|
+
"# Notch Memory Index",
|
|
1042
|
+
"",
|
|
1043
|
+
`_${memories.length} memories stored. Auto-generated \u2014 do not edit._`,
|
|
1044
|
+
""
|
|
1045
|
+
];
|
|
1046
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1047
|
+
for (const m of memories) {
|
|
1048
|
+
const list = byType.get(m.type) ?? [];
|
|
1049
|
+
list.push(m);
|
|
1050
|
+
byType.set(m.type, list);
|
|
1051
|
+
}
|
|
1052
|
+
for (const [type, mems] of byType) {
|
|
1053
|
+
lines.push(`## ${type}`);
|
|
1054
|
+
for (const m of mems) {
|
|
1055
|
+
lines.push(`- [${m.filename}](${m.filename}) \u2014 ${m.description}`);
|
|
1056
|
+
}
|
|
1057
|
+
lines.push("");
|
|
1058
|
+
}
|
|
1059
|
+
await fs7.writeFile(INDEX_FILE, lines.join("\n"), "utf-8");
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/agent/loop.ts
|
|
1063
|
+
async function runAgentLoop(messages, config) {
|
|
1064
|
+
const readCache = /* @__PURE__ */ new Map();
|
|
1065
|
+
const toolCtxWithCache = {
|
|
1066
|
+
...config.toolContext,
|
|
1067
|
+
_readCache: readCache
|
|
1068
|
+
};
|
|
1069
|
+
const tools = buildToolMap(toolCtxWithCache);
|
|
1070
|
+
const maxIter = config.maxIterations ?? 25;
|
|
1071
|
+
const contextWindow = config.contextWindow ?? 131072;
|
|
1072
|
+
let iterations = 0;
|
|
1073
|
+
let totalToolCalls = 0;
|
|
1074
|
+
let totalPromptTokens = 0;
|
|
1075
|
+
let totalCompletionTokens = 0;
|
|
1076
|
+
let wasCompressed = false;
|
|
1077
|
+
let history = [...messages];
|
|
1078
|
+
history = await autoCompress(history, config.model, contextWindow, () => {
|
|
1079
|
+
wasCompressed = true;
|
|
1080
|
+
config.onCompress?.();
|
|
1081
|
+
});
|
|
1082
|
+
while (iterations < maxIter) {
|
|
1083
|
+
iterations++;
|
|
1084
|
+
const result = streamText({
|
|
1085
|
+
model: config.model,
|
|
1086
|
+
system: config.systemPrompt,
|
|
1087
|
+
messages: history,
|
|
1088
|
+
tools,
|
|
1089
|
+
maxSteps: 1
|
|
1090
|
+
// We manage the loop ourselves for better control
|
|
1091
|
+
});
|
|
1092
|
+
let fullText = "";
|
|
1093
|
+
const toolCalls = [];
|
|
1094
|
+
const toolResults = [];
|
|
1095
|
+
for await (const event of result.fullStream) {
|
|
1096
|
+
if (event.type === "text-delta") {
|
|
1097
|
+
fullText += event.textDelta;
|
|
1098
|
+
config.onTextChunk?.(event.textDelta);
|
|
1099
|
+
} else if (event.type === "tool-call") {
|
|
1100
|
+
toolCalls.push({
|
|
1101
|
+
toolCallId: event.toolCallId,
|
|
1102
|
+
toolName: event.toolName,
|
|
1103
|
+
args: event.args
|
|
1104
|
+
});
|
|
1105
|
+
config.onToolCall?.(event.toolName, event.args);
|
|
1106
|
+
}
|
|
1107
|
+
const evt = event;
|
|
1108
|
+
if (evt.type === "tool-result") {
|
|
1109
|
+
const res = evt.result;
|
|
1110
|
+
toolResults.push({
|
|
1111
|
+
toolCallId: evt.toolCallId,
|
|
1112
|
+
result: evt.result
|
|
1113
|
+
});
|
|
1114
|
+
config.onToolResult?.(
|
|
1115
|
+
toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
|
|
1116
|
+
res?.content ?? String(evt.result),
|
|
1117
|
+
res?.isError ?? false
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const u = await result.usage;
|
|
1123
|
+
if (u) {
|
|
1124
|
+
totalPromptTokens += u.promptTokens ?? 0;
|
|
1125
|
+
totalCompletionTokens += u.completionTokens ?? 0;
|
|
1126
|
+
}
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
totalToolCalls += toolCalls.length;
|
|
1130
|
+
if (toolCalls.length > 0) {
|
|
1131
|
+
history.push({
|
|
1132
|
+
role: "assistant",
|
|
1133
|
+
content: [
|
|
1134
|
+
...fullText ? [{ type: "text", text: fullText }] : [],
|
|
1135
|
+
...toolCalls.map((tc) => ({
|
|
1136
|
+
type: "tool-call",
|
|
1137
|
+
toolCallId: tc.toolCallId,
|
|
1138
|
+
toolName: tc.toolName,
|
|
1139
|
+
args: tc.args
|
|
1140
|
+
}))
|
|
1141
|
+
]
|
|
1142
|
+
});
|
|
1143
|
+
history.push({
|
|
1144
|
+
role: "tool",
|
|
1145
|
+
content: toolResults.map((tr) => ({
|
|
1146
|
+
type: "tool-result",
|
|
1147
|
+
toolCallId: tr.toolCallId,
|
|
1148
|
+
toolName: toolCalls.find((tc) => tc.toolCallId === tr.toolCallId)?.toolName ?? "unknown",
|
|
1149
|
+
result: tr.result
|
|
1150
|
+
}))
|
|
1151
|
+
});
|
|
1152
|
+
if (iterations % 5 === 0) {
|
|
1153
|
+
history = await autoCompress(history, config.model, contextWindow, () => {
|
|
1154
|
+
wasCompressed = true;
|
|
1155
|
+
config.onCompress?.();
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
history.push({ role: "assistant", content: fullText });
|
|
1161
|
+
return {
|
|
1162
|
+
text: fullText,
|
|
1163
|
+
messages: history,
|
|
1164
|
+
iterations,
|
|
1165
|
+
toolCallCount: totalToolCalls,
|
|
1166
|
+
compressed: wasCompressed,
|
|
1167
|
+
usage: {
|
|
1168
|
+
promptTokens: totalPromptTokens,
|
|
1169
|
+
completionTokens: totalCompletionTokens,
|
|
1170
|
+
totalTokens: totalPromptTokens + totalCompletionTokens
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const finalText = "[Reached maximum tool iterations. Please continue or refine your request.]";
|
|
1175
|
+
history.push({ role: "assistant", content: finalText });
|
|
1176
|
+
return {
|
|
1177
|
+
text: finalText,
|
|
1178
|
+
messages: history,
|
|
1179
|
+
iterations,
|
|
1180
|
+
toolCallCount: totalToolCalls,
|
|
1181
|
+
compressed: wasCompressed,
|
|
1182
|
+
usage: {
|
|
1183
|
+
promptTokens: totalPromptTokens,
|
|
1184
|
+
completionTokens: totalCompletionTokens,
|
|
1185
|
+
totalTokens: totalPromptTokens + totalCompletionTokens
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
async function buildSystemPrompt(projectRoot) {
|
|
1190
|
+
const parts = [
|
|
1191
|
+
"You are Notch, an expert AI coding assistant built by Driftrail.",
|
|
1192
|
+
"You help developers write, debug, refactor, and understand code.",
|
|
1193
|
+
"You have access to tools for reading/writing files, running shell commands, searching code, and git operations.",
|
|
1194
|
+
"",
|
|
1195
|
+
"Guidelines:",
|
|
1196
|
+
"- Always read files before editing them.",
|
|
1197
|
+
"- Use exact string matching for edits \u2014 never guess at file contents.",
|
|
1198
|
+
"- Explain what you're doing before making changes.",
|
|
1199
|
+
"- If a task is complex, break it into steps.",
|
|
1200
|
+
"- When running shell commands, prefer non-destructive operations.",
|
|
1201
|
+
"- If you encounter an error, analyze it and suggest a fix."
|
|
1202
|
+
];
|
|
1203
|
+
try {
|
|
1204
|
+
const instructions = await loadProjectInstructions(projectRoot);
|
|
1205
|
+
if (instructions) {
|
|
1206
|
+
parts.push("", "## Project Instructions (.notch.md)", instructions);
|
|
1207
|
+
}
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
try {
|
|
1211
|
+
const memoryStr = await formatMemoriesForPrompt(projectRoot);
|
|
1212
|
+
if (memoryStr) {
|
|
1213
|
+
parts.push("", "## Saved Context (Memory)", memoryStr);
|
|
1214
|
+
}
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
parts.push("", "## Available Tools", describeTools());
|
|
1218
|
+
return parts.join("\n");
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/agent/checkpoints.ts
|
|
1222
|
+
import fs8 from "fs/promises";
|
|
1223
|
+
import path9 from "path";
|
|
1224
|
+
var CheckpointManager = class {
|
|
1225
|
+
checkpoints = [];
|
|
1226
|
+
nextId = 1;
|
|
1227
|
+
pendingFiles = /* @__PURE__ */ new Map();
|
|
1228
|
+
/** Call before a file is modified — records its current content */
|
|
1229
|
+
async recordBefore(filePath) {
|
|
1230
|
+
if (this.pendingFiles.has(filePath)) return;
|
|
1231
|
+
try {
|
|
1232
|
+
const content = await fs8.readFile(filePath, "utf-8");
|
|
1233
|
+
this.pendingFiles.set(filePath, content);
|
|
1234
|
+
} catch {
|
|
1235
|
+
this.pendingFiles.set(filePath, null);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/** Commit all pending file changes as a checkpoint */
|
|
1239
|
+
async commit(description) {
|
|
1240
|
+
const files = [];
|
|
1241
|
+
for (const [filePath, before] of this.pendingFiles) {
|
|
1242
|
+
let after = null;
|
|
1243
|
+
try {
|
|
1244
|
+
after = await fs8.readFile(filePath, "utf-8");
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
files.push({ path: filePath, before, after });
|
|
1248
|
+
}
|
|
1249
|
+
const checkpoint = {
|
|
1250
|
+
id: this.nextId++,
|
|
1251
|
+
timestamp: Date.now(),
|
|
1252
|
+
files,
|
|
1253
|
+
description
|
|
1254
|
+
};
|
|
1255
|
+
this.checkpoints.push(checkpoint);
|
|
1256
|
+
this.pendingFiles.clear();
|
|
1257
|
+
return checkpoint;
|
|
1258
|
+
}
|
|
1259
|
+
/** Undo the most recent checkpoint — restores all files to their before state */
|
|
1260
|
+
async undo() {
|
|
1261
|
+
const checkpoint = this.checkpoints.pop();
|
|
1262
|
+
if (!checkpoint) return null;
|
|
1263
|
+
for (const snap of checkpoint.files) {
|
|
1264
|
+
if (snap.before === null) {
|
|
1265
|
+
try {
|
|
1266
|
+
await fs8.unlink(snap.path);
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
} else {
|
|
1270
|
+
await fs8.mkdir(path9.dirname(snap.path), { recursive: true });
|
|
1271
|
+
await fs8.writeFile(snap.path, snap.before, "utf-8");
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return checkpoint;
|
|
1275
|
+
}
|
|
1276
|
+
/** Get the list of checkpoints (most recent last) */
|
|
1277
|
+
list() {
|
|
1278
|
+
return [...this.checkpoints];
|
|
1279
|
+
}
|
|
1280
|
+
/** How many checkpoints are available to undo */
|
|
1281
|
+
get undoCount() {
|
|
1282
|
+
return this.checkpoints.length;
|
|
1283
|
+
}
|
|
1284
|
+
/** Discard pending without committing */
|
|
1285
|
+
discard() {
|
|
1286
|
+
this.pendingFiles.clear();
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Get all diffs across all checkpoints (for /diff command).
|
|
1290
|
+
* Returns unique file paths with their first-seen "before" and latest "after" content.
|
|
1291
|
+
*/
|
|
1292
|
+
allDiffs() {
|
|
1293
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
1294
|
+
for (const checkpoint of this.checkpoints) {
|
|
1295
|
+
for (const snapshot of checkpoint.files) {
|
|
1296
|
+
if (!fileMap.has(snapshot.path)) {
|
|
1297
|
+
fileMap.set(snapshot.path, { before: snapshot.before, after: snapshot.after });
|
|
1298
|
+
} else {
|
|
1299
|
+
fileMap.get(snapshot.path).after = snapshot.after;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return Array.from(fileMap.entries()).map(([path19, { before, after }]) => ({
|
|
1304
|
+
path: path19,
|
|
1305
|
+
before,
|
|
1306
|
+
after
|
|
1307
|
+
}));
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
// src/agent/usage.ts
|
|
1312
|
+
import chalk from "chalk";
|
|
1313
|
+
var UsageTracker = class {
|
|
1314
|
+
turns = [];
|
|
1315
|
+
record(usage) {
|
|
1316
|
+
this.turns.push(usage);
|
|
1317
|
+
}
|
|
1318
|
+
get sessionTotal() {
|
|
1319
|
+
return this.turns.reduce(
|
|
1320
|
+
(acc, t) => ({
|
|
1321
|
+
promptTokens: acc.promptTokens + t.promptTokens,
|
|
1322
|
+
completionTokens: acc.completionTokens + t.completionTokens,
|
|
1323
|
+
totalTokens: acc.totalTokens + t.totalTokens,
|
|
1324
|
+
toolCalls: acc.toolCalls + t.toolCalls,
|
|
1325
|
+
iterations: acc.iterations + t.iterations
|
|
1326
|
+
}),
|
|
1327
|
+
{ promptTokens: 0, completionTokens: 0, totalTokens: 0, toolCalls: 0, iterations: 0 }
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
get turnCount() {
|
|
1331
|
+
return this.turns.length;
|
|
1332
|
+
}
|
|
1333
|
+
formatLast() {
|
|
1334
|
+
const last = this.turns[this.turns.length - 1];
|
|
1335
|
+
if (!last) return "";
|
|
1336
|
+
const t = last.totalTokens;
|
|
1337
|
+
const label = t > 1e4 ? chalk.yellow(`${(t / 1e3).toFixed(1)}K`) : chalk.gray(`${t}`);
|
|
1338
|
+
return chalk.gray(` [${label} tokens, ${last.toolCalls} tool calls, ${last.iterations} rounds]`);
|
|
1339
|
+
}
|
|
1340
|
+
formatSession() {
|
|
1341
|
+
const total = this.sessionTotal;
|
|
1342
|
+
return [
|
|
1343
|
+
chalk.gray(`
|
|
1344
|
+
Session usage (${this.turnCount} turns):`),
|
|
1345
|
+
chalk.gray(` Prompt: ${total.promptTokens.toLocaleString()} tokens`),
|
|
1346
|
+
chalk.gray(` Completion: ${total.completionTokens.toLocaleString()} tokens`),
|
|
1347
|
+
chalk.gray(` Total: ${total.totalTokens.toLocaleString()} tokens`),
|
|
1348
|
+
chalk.gray(` Tool calls: ${total.toolCalls}`)
|
|
1349
|
+
].join("\n");
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
// src/agent/retry.ts
|
|
1354
|
+
var DEFAULT_RETRYABLE = (err) => {
|
|
1355
|
+
const msg = err.message.toLowerCase();
|
|
1356
|
+
if (msg.includes("fetch") || msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("network") || msg.includes("socket")) {
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
if (msg.includes("429") || msg.includes("rate limit") || msg.includes("too many requests")) {
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
if (/\b5\d{2}\b/.test(msg)) {
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
return false;
|
|
1366
|
+
};
|
|
1367
|
+
async function withRetry(fn, options = {}) {
|
|
1368
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
1369
|
+
const baseDelay = options.baseDelay ?? 1e3;
|
|
1370
|
+
const maxDelay = options.maxDelay ?? 3e4;
|
|
1371
|
+
const factor = options.factor ?? 2;
|
|
1372
|
+
const jitter = options.jitter ?? true;
|
|
1373
|
+
const isRetryable = options.isRetryable ?? DEFAULT_RETRYABLE;
|
|
1374
|
+
let lastError;
|
|
1375
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1376
|
+
try {
|
|
1377
|
+
return await fn();
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
lastError = err;
|
|
1380
|
+
if (attempt === maxAttempts || !isRetryable(err)) {
|
|
1381
|
+
throw err;
|
|
1382
|
+
}
|
|
1383
|
+
let delay = Math.min(baseDelay * Math.pow(factor, attempt - 1), maxDelay);
|
|
1384
|
+
if (jitter) {
|
|
1385
|
+
delay += Math.random() * delay * 0.5;
|
|
1386
|
+
}
|
|
1387
|
+
options.onRetry?.(attempt, err, delay);
|
|
1388
|
+
await sleep(delay);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
throw lastError;
|
|
1392
|
+
}
|
|
1393
|
+
function sleep(ms) {
|
|
1394
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/agent/subagent.ts
|
|
1398
|
+
import { streamText as streamText2 } from "ai";
|
|
1399
|
+
var SUBAGENT_PROMPTS = {
|
|
1400
|
+
explore: `You are an exploration agent. Your job is to quickly search and analyze a codebase to answer questions.
|
|
1401
|
+
You have access to read, grep, and glob tools. Use them efficiently \u2014 search broadly first, then narrow down.
|
|
1402
|
+
Be thorough but concise. Return your findings as a structured summary.
|
|
1403
|
+
Do NOT modify any files. Only read and search.`,
|
|
1404
|
+
plan: `You are a planning agent. Your job is to design implementation strategies.
|
|
1405
|
+
Analyze the codebase, identify the files that need changes, and produce a step-by-step plan.
|
|
1406
|
+
Consider edge cases, existing patterns, and potential risks.
|
|
1407
|
+
Do NOT implement anything. Only plan and recommend.
|
|
1408
|
+
Output a structured plan with:
|
|
1409
|
+
1. Summary of approach
|
|
1410
|
+
2. Files to modify (with specific changes)
|
|
1411
|
+
3. New files to create (if any)
|
|
1412
|
+
4. Testing strategy
|
|
1413
|
+
5. Risks and considerations`,
|
|
1414
|
+
general: `You are a general-purpose worker agent. Complete the task assigned to you.
|
|
1415
|
+
You have full access to all tools. Work autonomously and return results when done.
|
|
1416
|
+
Be efficient \u2014 use grep/glob to find things before reading entire files.
|
|
1417
|
+
If you encounter errors, try to resolve them yourself before reporting back.`
|
|
1418
|
+
};
|
|
1419
|
+
var EXPLORE_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "web_fetch"]);
|
|
1420
|
+
var PLAN_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "git", "web_fetch"]);
|
|
1421
|
+
var subagentCounter = 0;
|
|
1422
|
+
async function spawnSubagent(config) {
|
|
1423
|
+
const { id, type, prompt, model, toolContext, maxIterations = 15 } = config;
|
|
1424
|
+
config.onStatus?.(id, `Starting ${type} agent...`);
|
|
1425
|
+
const allTools = buildToolMap(toolContext);
|
|
1426
|
+
const tools = {};
|
|
1427
|
+
if (type === "explore") {
|
|
1428
|
+
for (const [name, tool2] of Object.entries(allTools)) {
|
|
1429
|
+
if (EXPLORE_TOOL_FILTER.has(name)) tools[name] = tool2;
|
|
1430
|
+
}
|
|
1431
|
+
} else if (type === "plan") {
|
|
1432
|
+
for (const [name, tool2] of Object.entries(allTools)) {
|
|
1433
|
+
if (PLAN_TOOL_FILTER.has(name)) tools[name] = tool2;
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
Object.assign(tools, allTools);
|
|
1437
|
+
}
|
|
1438
|
+
const systemPrompt = `${SUBAGENT_PROMPTS[type]}
|
|
1439
|
+
|
|
1440
|
+
## Working Directory
|
|
1441
|
+
${toolContext.cwd}
|
|
1442
|
+
|
|
1443
|
+
## Available Tools
|
|
1444
|
+
${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
|
|
1445
|
+
const messages = [
|
|
1446
|
+
{ role: "user", content: prompt }
|
|
1447
|
+
];
|
|
1448
|
+
let iterations = 0;
|
|
1449
|
+
let totalToolCalls = 0;
|
|
1450
|
+
try {
|
|
1451
|
+
while (iterations < maxIterations) {
|
|
1452
|
+
iterations++;
|
|
1453
|
+
config.onStatus?.(id, `Iteration ${iterations}/${maxIterations}...`);
|
|
1454
|
+
const result = streamText2({
|
|
1455
|
+
model,
|
|
1456
|
+
system: systemPrompt,
|
|
1457
|
+
messages,
|
|
1458
|
+
tools,
|
|
1459
|
+
maxSteps: 1
|
|
1460
|
+
});
|
|
1461
|
+
let fullText = "";
|
|
1462
|
+
const toolCalls = [];
|
|
1463
|
+
const toolResults = [];
|
|
1464
|
+
for await (const event of result.fullStream) {
|
|
1465
|
+
if (event.type === "text-delta") {
|
|
1466
|
+
fullText += event.textDelta;
|
|
1467
|
+
} else if (event.type === "tool-call") {
|
|
1468
|
+
toolCalls.push({
|
|
1469
|
+
toolCallId: event.toolCallId,
|
|
1470
|
+
toolName: event.toolName,
|
|
1471
|
+
args: event.args
|
|
1472
|
+
});
|
|
1473
|
+
config.onStatus?.(id, `${event.toolName}(...)`);
|
|
1474
|
+
}
|
|
1475
|
+
const evt = event;
|
|
1476
|
+
if (evt.type === "tool-result") {
|
|
1477
|
+
toolResults.push({
|
|
1478
|
+
toolCallId: evt.toolCallId,
|
|
1479
|
+
toolName: toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
|
|
1480
|
+
result: evt.result
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
totalToolCalls += toolCalls.length;
|
|
1485
|
+
if (toolCalls.length > 0) {
|
|
1486
|
+
messages.push({
|
|
1487
|
+
role: "assistant",
|
|
1488
|
+
content: [
|
|
1489
|
+
...fullText ? [{ type: "text", text: fullText }] : [],
|
|
1490
|
+
...toolCalls.map((tc) => ({
|
|
1491
|
+
type: "tool-call",
|
|
1492
|
+
toolCallId: tc.toolCallId,
|
|
1493
|
+
toolName: tc.toolName,
|
|
1494
|
+
args: tc.args
|
|
1495
|
+
}))
|
|
1496
|
+
]
|
|
1497
|
+
});
|
|
1498
|
+
messages.push({
|
|
1499
|
+
role: "tool",
|
|
1500
|
+
content: toolResults.map((tr) => ({
|
|
1501
|
+
type: "tool-result",
|
|
1502
|
+
toolCallId: tr.toolCallId,
|
|
1503
|
+
toolName: tr.toolName,
|
|
1504
|
+
result: tr.result
|
|
1505
|
+
}))
|
|
1506
|
+
});
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
config.onStatus?.(id, "Complete");
|
|
1510
|
+
return {
|
|
1511
|
+
id,
|
|
1512
|
+
type,
|
|
1513
|
+
text: fullText,
|
|
1514
|
+
toolCalls: totalToolCalls,
|
|
1515
|
+
iterations
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
config.onStatus?.(id, "Max iterations reached");
|
|
1519
|
+
return {
|
|
1520
|
+
id,
|
|
1521
|
+
type,
|
|
1522
|
+
text: "[Subagent reached max iterations]",
|
|
1523
|
+
toolCalls: totalToolCalls,
|
|
1524
|
+
iterations
|
|
1525
|
+
};
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
config.onStatus?.(id, `Error: ${err.message}`);
|
|
1528
|
+
return {
|
|
1529
|
+
id,
|
|
1530
|
+
type,
|
|
1531
|
+
text: "",
|
|
1532
|
+
toolCalls: totalToolCalls,
|
|
1533
|
+
iterations,
|
|
1534
|
+
error: err.message
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function nextSubagentId(type) {
|
|
1539
|
+
return `${type}-${++subagentCounter}`;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/agent/planner.ts
|
|
1543
|
+
import { generateText as generateText2 } from "ai";
|
|
1544
|
+
import chalk2 from "chalk";
|
|
1545
|
+
var PLAN_SYSTEM_PROMPT = `You are a planning assistant. Given a task, produce a structured implementation plan.
|
|
1546
|
+
|
|
1547
|
+
Respond in EXACTLY this format (no other text):
|
|
1548
|
+
|
|
1549
|
+
SUMMARY: <one sentence summary of the approach>
|
|
1550
|
+
|
|
1551
|
+
STEPS:
|
|
1552
|
+
1. <action description> | FILES: <comma-separated file paths>
|
|
1553
|
+
2. <action description> | FILES: <comma-separated file paths>
|
|
1554
|
+
...
|
|
1555
|
+
|
|
1556
|
+
RISKS:
|
|
1557
|
+
- <risk or consideration>
|
|
1558
|
+
- <risk or consideration>
|
|
1559
|
+
|
|
1560
|
+
Rules:
|
|
1561
|
+
- Be specific about which files to modify and what changes to make
|
|
1562
|
+
- Order steps logically (read before edit, create before use)
|
|
1563
|
+
- Include testing steps
|
|
1564
|
+
- Keep it concise \u2014 no more than 10 steps
|
|
1565
|
+
- Files should be relative paths`;
|
|
1566
|
+
async function generatePlan(request, model, context) {
|
|
1567
|
+
const messages = [];
|
|
1568
|
+
if (context.history && context.history.length > 0) {
|
|
1569
|
+
const recentUser = context.history.filter((m) => m.role === "user" && typeof m.content === "string").slice(-3);
|
|
1570
|
+
if (recentUser.length > 0) {
|
|
1571
|
+
messages.push({
|
|
1572
|
+
role: "user",
|
|
1573
|
+
content: `Recent conversation context:
|
|
1574
|
+
${recentUser.map((m) => `- ${String(m.content).slice(0, 100)}`).join("\n")}`
|
|
1575
|
+
});
|
|
1576
|
+
messages.push({
|
|
1577
|
+
role: "assistant",
|
|
1578
|
+
content: "Understood, I have the conversation context."
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
let prompt = `Task: ${request}
|
|
1583
|
+
|
|
1584
|
+
Working directory: ${context.cwd}`;
|
|
1585
|
+
if (context.repoMap) {
|
|
1586
|
+
prompt += `
|
|
1587
|
+
|
|
1588
|
+
Repository structure:
|
|
1589
|
+
${context.repoMap.slice(0, 4e3)}`;
|
|
1590
|
+
}
|
|
1591
|
+
messages.push({ role: "user", content: prompt });
|
|
1592
|
+
const result = await generateText2({
|
|
1593
|
+
model,
|
|
1594
|
+
system: PLAN_SYSTEM_PROMPT,
|
|
1595
|
+
messages,
|
|
1596
|
+
maxTokens: 2048
|
|
1597
|
+
});
|
|
1598
|
+
return parsePlan(result.text);
|
|
1599
|
+
}
|
|
1600
|
+
function parsePlan(text) {
|
|
1601
|
+
const summary = text.match(/SUMMARY:\s*(.+)/)?.[1]?.trim() ?? "Implementation plan";
|
|
1602
|
+
const steps = [];
|
|
1603
|
+
const stepMatches = text.matchAll(/(\d+)\.\s*(.+?)(?:\s*\|\s*FILES:\s*(.+))?$/gm);
|
|
1604
|
+
let idx = 0;
|
|
1605
|
+
for (const match of stepMatches) {
|
|
1606
|
+
const action = match[2]?.trim() ?? "";
|
|
1607
|
+
const filesStr = match[3]?.trim() ?? "";
|
|
1608
|
+
const files = filesStr ? filesStr.split(",").map((f) => f.trim()).filter(Boolean) : [];
|
|
1609
|
+
steps.push({ index: idx++, action, files, status: "pending" });
|
|
1610
|
+
}
|
|
1611
|
+
if (steps.length === 0) {
|
|
1612
|
+
const lines = text.split("\n").filter((l) => l.trim() && !l.startsWith("SUMMARY") && !l.startsWith("RISKS"));
|
|
1613
|
+
for (const line of lines.slice(0, 10)) {
|
|
1614
|
+
steps.push({ index: idx++, action: line.trim(), files: [], status: "pending" });
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
const risks = [];
|
|
1618
|
+
const riskSection = text.match(/RISKS:\n([\s\S]*?)(?:\n\n|$)/);
|
|
1619
|
+
if (riskSection) {
|
|
1620
|
+
const riskLines = riskSection[1].split("\n").filter((l) => l.trim().startsWith("-"));
|
|
1621
|
+
for (const line of riskLines) {
|
|
1622
|
+
risks.push(line.replace(/^-\s*/, "").trim());
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
summary,
|
|
1627
|
+
steps,
|
|
1628
|
+
risks,
|
|
1629
|
+
currentStep: -1,
|
|
1630
|
+
approved: false
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function formatPlan(plan) {
|
|
1634
|
+
const lines = [];
|
|
1635
|
+
lines.push("");
|
|
1636
|
+
lines.push(chalk2.bold.white(` Plan: ${plan.summary}`));
|
|
1637
|
+
lines.push("");
|
|
1638
|
+
for (const step of plan.steps) {
|
|
1639
|
+
const icon = step.status === "done" ? chalk2.green("\u2713") : step.status === "in_progress" ? chalk2.yellow("\u25B6") : step.status === "failed" ? chalk2.red("\u2717") : step.status === "skipped" ? chalk2.gray("\u2013") : chalk2.gray("\u25CB");
|
|
1640
|
+
const num = chalk2.gray(`${step.index + 1}.`);
|
|
1641
|
+
const action = step.status === "done" ? chalk2.gray(step.action) : chalk2.white(step.action);
|
|
1642
|
+
const files = step.files.length > 0 ? chalk2.cyan(` [${step.files.join(", ")}]`) : "";
|
|
1643
|
+
lines.push(` ${icon} ${num} ${action}${files}`);
|
|
1644
|
+
}
|
|
1645
|
+
if (plan.risks.length > 0) {
|
|
1646
|
+
lines.push("");
|
|
1647
|
+
lines.push(chalk2.yellow(" Risks:"));
|
|
1648
|
+
for (const risk of plan.risks) {
|
|
1649
|
+
lines.push(chalk2.yellow(` \u26A0 ${risk}`));
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
lines.push("");
|
|
1653
|
+
if (!plan.approved) {
|
|
1654
|
+
lines.push(chalk2.gray(" Approve: /plan approve | Modify: /plan edit | Cancel: /plan cancel"));
|
|
1655
|
+
}
|
|
1656
|
+
lines.push("");
|
|
1657
|
+
return lines.join("\n");
|
|
1658
|
+
}
|
|
1659
|
+
function currentStepPrompt(plan) {
|
|
1660
|
+
const step = plan.steps[plan.currentStep];
|
|
1661
|
+
if (!step) return null;
|
|
1662
|
+
let prompt = `Execute step ${step.index + 1} of the plan: ${step.action}`;
|
|
1663
|
+
if (step.files.length > 0) {
|
|
1664
|
+
prompt += `
|
|
1665
|
+
Relevant files: ${step.files.join(", ")}`;
|
|
1666
|
+
}
|
|
1667
|
+
prompt += "\n\nComplete this step, then report what you did.";
|
|
1668
|
+
return prompt;
|
|
1669
|
+
}
|
|
1670
|
+
function advancePlan(plan) {
|
|
1671
|
+
if (plan.currentStep >= 0 && plan.steps[plan.currentStep]?.status === "in_progress") {
|
|
1672
|
+
plan.steps[plan.currentStep].status = "done";
|
|
1673
|
+
}
|
|
1674
|
+
const next = plan.steps.findIndex((s) => s.status === "pending");
|
|
1675
|
+
if (next === -1) return false;
|
|
1676
|
+
plan.currentStep = next;
|
|
1677
|
+
plan.steps[next].status = "in_progress";
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
function isPlanComplete(plan) {
|
|
1681
|
+
return plan.steps.every((s) => s.status === "done" || s.status === "skipped");
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// src/agent/cost.ts
|
|
1685
|
+
import chalk3 from "chalk";
|
|
1686
|
+
var MODEL_COSTS = {
|
|
1687
|
+
"notch-cinder": { input: 0.05, output: 0.15 },
|
|
1688
|
+
// L4 — cheapest
|
|
1689
|
+
"notch-forge": { input: 0.1, output: 0.3 },
|
|
1690
|
+
// L40S
|
|
1691
|
+
"notch-pyre": { input: 0.3, output: 0.9 },
|
|
1692
|
+
// A100
|
|
1693
|
+
"notch-ignis": { input: 0.4, output: 1.2 }
|
|
1694
|
+
// A100
|
|
1695
|
+
};
|
|
1696
|
+
var CostTracker = class {
|
|
1697
|
+
entries = [];
|
|
1698
|
+
record(model, promptTokens, completionTokens) {
|
|
1699
|
+
const rates = MODEL_COSTS[model] ?? MODEL_COSTS["notch-forge"];
|
|
1700
|
+
const inputCost = promptTokens / 1e6 * rates.input;
|
|
1701
|
+
const outputCost = completionTokens / 1e6 * rates.output;
|
|
1702
|
+
const cost = inputCost + outputCost;
|
|
1703
|
+
const entry = { model, promptTokens, completionTokens, cost };
|
|
1704
|
+
this.entries.push(entry);
|
|
1705
|
+
return entry;
|
|
1706
|
+
}
|
|
1707
|
+
get totalCost() {
|
|
1708
|
+
return this.entries.reduce((sum, e) => sum + e.cost, 0);
|
|
1709
|
+
}
|
|
1710
|
+
get totalTokens() {
|
|
1711
|
+
return this.entries.reduce(
|
|
1712
|
+
(acc, e) => ({
|
|
1713
|
+
prompt: acc.prompt + e.promptTokens,
|
|
1714
|
+
completion: acc.completion + e.completionTokens
|
|
1715
|
+
}),
|
|
1716
|
+
{ prompt: 0, completion: 0 }
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
formatLastCost() {
|
|
1720
|
+
const last = this.entries[this.entries.length - 1];
|
|
1721
|
+
if (!last) return "";
|
|
1722
|
+
return chalk3.gray(`$${last.cost.toFixed(4)}`);
|
|
1723
|
+
}
|
|
1724
|
+
formatSession() {
|
|
1725
|
+
if (this.entries.length === 0) return chalk3.gray(" No cost data.");
|
|
1726
|
+
const total = this.totalCost;
|
|
1727
|
+
const tokens = this.totalTokens;
|
|
1728
|
+
return [
|
|
1729
|
+
chalk3.gray(`
|
|
1730
|
+
Session cost estimate:`),
|
|
1731
|
+
chalk3.gray(` Input: ${tokens.prompt.toLocaleString()} tokens`),
|
|
1732
|
+
chalk3.gray(` Output: ${tokens.completion.toLocaleString()} tokens`),
|
|
1733
|
+
chalk3.gray(` Est. cost: `) + chalk3.white(`$${total.toFixed(4)}`),
|
|
1734
|
+
chalk3.gray(` (${this.entries.length} turns)`)
|
|
1735
|
+
].join("\n");
|
|
1736
|
+
}
|
|
1737
|
+
/** Alias for formatSession */
|
|
1738
|
+
formatTotal() {
|
|
1739
|
+
return this.formatSession();
|
|
1740
|
+
}
|
|
1741
|
+
/** Breakdown cost by model */
|
|
1742
|
+
formatByModel() {
|
|
1743
|
+
if (this.entries.length === 0) return "";
|
|
1744
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
1745
|
+
for (const e of this.entries) {
|
|
1746
|
+
const existing = byModel.get(e.model) ?? { prompt: 0, completion: 0, cost: 0, turns: 0 };
|
|
1747
|
+
existing.prompt += e.promptTokens;
|
|
1748
|
+
existing.completion += e.completionTokens;
|
|
1749
|
+
existing.cost += e.cost;
|
|
1750
|
+
existing.turns += 1;
|
|
1751
|
+
byModel.set(e.model, existing);
|
|
1752
|
+
}
|
|
1753
|
+
const lines = [chalk3.gray("\n Cost by model:")];
|
|
1754
|
+
for (const [modelId, data] of byModel) {
|
|
1755
|
+
lines.push(
|
|
1756
|
+
chalk3.gray(` ${modelId.padEnd(14)} ${data.turns} turns `) + chalk3.white(`$${data.cost.toFixed(4)}`) + chalk3.gray(` (${(data.prompt + data.completion).toLocaleString()} tokens)`)
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
return lines.join("\n");
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
// src/agent/ralph.ts
|
|
1764
|
+
import fs10 from "fs/promises";
|
|
1765
|
+
import path11 from "path";
|
|
1766
|
+
import chalk4 from "chalk";
|
|
1767
|
+
import { generateText as generateText3, streamText as streamText3 } from "ai";
|
|
1768
|
+
|
|
1769
|
+
// src/context/repo-map.ts
|
|
1770
|
+
import fs9 from "fs/promises";
|
|
1771
|
+
import path10 from "path";
|
|
1772
|
+
import { glob } from "glob";
|
|
1773
|
+
var PATTERNS = {
|
|
1774
|
+
ts: [
|
|
1775
|
+
/^export\s+(default\s+)?(async\s+)?function\s+(\w+)/gm,
|
|
1776
|
+
/^export\s+(default\s+)?class\s+(\w+)/gm,
|
|
1777
|
+
/^export\s+(const|let|var)\s+(\w+)/gm,
|
|
1778
|
+
/^export\s+(default\s+)?interface\s+(\w+)/gm,
|
|
1779
|
+
/^export\s+(default\s+)?type\s+(\w+)/gm,
|
|
1780
|
+
/^export\s+(default\s+)?enum\s+(\w+)/gm
|
|
1781
|
+
],
|
|
1782
|
+
js: [
|
|
1783
|
+
/^export\s+(default\s+)?(async\s+)?function\s+(\w+)/gm,
|
|
1784
|
+
/^export\s+(default\s+)?class\s+(\w+)/gm,
|
|
1785
|
+
/^export\s+(const|let|var)\s+(\w+)/gm,
|
|
1786
|
+
/^module\.exports\s*=\s*{([^}]+)}/gm
|
|
1787
|
+
],
|
|
1788
|
+
py: [
|
|
1789
|
+
/^def\s+(\w+)\s*\(/gm,
|
|
1790
|
+
/^class\s+(\w+)/gm,
|
|
1791
|
+
/^(\w+)\s*=\s*/gm
|
|
1792
|
+
],
|
|
1793
|
+
rs: [
|
|
1794
|
+
/^pub\s+(async\s+)?fn\s+(\w+)/gm,
|
|
1795
|
+
/^pub\s+struct\s+(\w+)/gm,
|
|
1796
|
+
/^pub\s+enum\s+(\w+)/gm,
|
|
1797
|
+
/^pub\s+trait\s+(\w+)/gm
|
|
1798
|
+
]
|
|
1799
|
+
};
|
|
1800
|
+
var IMPORT_PATTERN = /(?:import|from)\s+['"]([^'"]+)['"]/g;
|
|
1801
|
+
function getPatterns(filePath) {
|
|
1802
|
+
const ext = path10.extname(filePath).slice(1);
|
|
1803
|
+
if (["ts", "tsx", "mts", "cts"].includes(ext)) return PATTERNS.ts;
|
|
1804
|
+
if (["js", "jsx", "mjs", "cjs"].includes(ext)) return PATTERNS.js;
|
|
1805
|
+
if (ext === "py") return PATTERNS.py;
|
|
1806
|
+
if (ext === "rs") return PATTERNS.rs;
|
|
1807
|
+
return [];
|
|
1808
|
+
}
|
|
1809
|
+
function extractSymbols(content, patterns) {
|
|
1810
|
+
const symbols = [];
|
|
1811
|
+
for (const pattern of patterns) {
|
|
1812
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
1813
|
+
let match;
|
|
1814
|
+
while ((match = re.exec(content)) !== null) {
|
|
1815
|
+
const name = match[match.length - 1];
|
|
1816
|
+
if (name && name.length < 100) {
|
|
1817
|
+
symbols.push(name.trim());
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return [...new Set(symbols)];
|
|
1822
|
+
}
|
|
1823
|
+
function extractImports(content, projectRoot, filePath) {
|
|
1824
|
+
const imports = [];
|
|
1825
|
+
const re = new RegExp(IMPORT_PATTERN.source, IMPORT_PATTERN.flags);
|
|
1826
|
+
let match;
|
|
1827
|
+
while ((match = re.exec(content)) !== null) {
|
|
1828
|
+
const importPath = match[1];
|
|
1829
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
1830
|
+
imports.push(importPath);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return imports;
|
|
1834
|
+
}
|
|
1835
|
+
async function buildRepoMap(root) {
|
|
1836
|
+
const files = await glob("**/*.{ts,tsx,js,jsx,mjs,py,rs}", {
|
|
1837
|
+
cwd: root,
|
|
1838
|
+
ignore: [
|
|
1839
|
+
"**/node_modules/**",
|
|
1840
|
+
"**/.git/**",
|
|
1841
|
+
"**/dist/**",
|
|
1842
|
+
"**/build/**",
|
|
1843
|
+
"**/__pycache__/**",
|
|
1844
|
+
"**/target/**",
|
|
1845
|
+
"**/*.test.*",
|
|
1846
|
+
"**/*.spec.*",
|
|
1847
|
+
"**/*.d.ts"
|
|
1848
|
+
],
|
|
1849
|
+
nodir: true
|
|
1850
|
+
});
|
|
1851
|
+
const entries = [];
|
|
1852
|
+
for (const file of files.slice(0, 500)) {
|
|
1853
|
+
const fullPath = path10.resolve(root, file);
|
|
1854
|
+
try {
|
|
1855
|
+
const content = await fs9.readFile(fullPath, "utf-8");
|
|
1856
|
+
const lines = content.split("\n").length;
|
|
1857
|
+
const patterns = getPatterns(file);
|
|
1858
|
+
const symbols = extractSymbols(content, patterns);
|
|
1859
|
+
const imports = extractImports(content, root, file);
|
|
1860
|
+
if (symbols.length > 0 || imports.length > 0) {
|
|
1861
|
+
entries.push({ path: file, symbols, imports, lines });
|
|
1862
|
+
}
|
|
1863
|
+
} catch {
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const summaryLines = entries.map((e) => {
|
|
1867
|
+
const syms = e.symbols.length > 0 ? `: ${e.symbols.join(", ")}` : "";
|
|
1868
|
+
return ` ${e.path} (${e.lines}L)${syms}`;
|
|
1869
|
+
});
|
|
1870
|
+
const summary = [
|
|
1871
|
+
`Repository structure (${entries.length} source files):`,
|
|
1872
|
+
...summaryLines
|
|
1873
|
+
].join("\n");
|
|
1874
|
+
return { root, entries, summary };
|
|
1875
|
+
}
|
|
1876
|
+
function condensedRepoMap(map, maxChars = 8e3) {
|
|
1877
|
+
if (map.summary.length <= maxChars) return map.summary;
|
|
1878
|
+
const lines = [`Repository structure (${map.entries.length} source files):`];
|
|
1879
|
+
let charCount = lines[0].length;
|
|
1880
|
+
for (const entry of map.entries) {
|
|
1881
|
+
const line = ` ${entry.path} (${entry.lines}L)`;
|
|
1882
|
+
if (charCount + line.length + 1 > maxChars) {
|
|
1883
|
+
lines.push(` ... and ${map.entries.length - lines.length + 1} more files`);
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
lines.push(line);
|
|
1887
|
+
charCount += line.length + 1;
|
|
1888
|
+
}
|
|
1889
|
+
return lines.join("\n");
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// src/agent/ralph.ts
|
|
1893
|
+
var PLAN_FILE = ".notch-ralph-plan.json";
|
|
1894
|
+
var MAX_TASK_ATTEMPTS = 2;
|
|
1895
|
+
var PLAN_SYSTEM = `You are a senior software architect. Given a high-level goal, break it into discrete, ordered implementation tasks.
|
|
1896
|
+
|
|
1897
|
+
Output EXACTLY this JSON format (no markdown, no explanation):
|
|
1898
|
+
{
|
|
1899
|
+
"tasks": [
|
|
1900
|
+
{
|
|
1901
|
+
"title": "Short task name",
|
|
1902
|
+
"description": "Detailed description of what to implement and how",
|
|
1903
|
+
"files": ["path/to/file1.ts", "path/to/file2.ts"]
|
|
1904
|
+
}
|
|
1905
|
+
]
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
Rules:
|
|
1909
|
+
- Each task should be completable in one focused session (5-15 min of agent work)
|
|
1910
|
+
- Order tasks so dependencies come first (create before use)
|
|
1911
|
+
- Include a testing/verification task at the end
|
|
1912
|
+
- Be specific about file paths and changes
|
|
1913
|
+
- 3-12 tasks typical. No more than 15.
|
|
1914
|
+
- Each task should commit its own changes`;
|
|
1915
|
+
async function generateRalphPlan(goal, model, cwd) {
|
|
1916
|
+
let repoContext = "";
|
|
1917
|
+
try {
|
|
1918
|
+
const repoMap = await buildRepoMap(cwd);
|
|
1919
|
+
repoContext = condensedRepoMap(repoMap, 4e3);
|
|
1920
|
+
} catch {
|
|
1921
|
+
}
|
|
1922
|
+
const prompt = `Goal: ${goal}
|
|
1923
|
+
|
|
1924
|
+
Repository (${cwd}):
|
|
1925
|
+
${repoContext || "(empty project)"}`;
|
|
1926
|
+
const result = await generateText3({
|
|
1927
|
+
model,
|
|
1928
|
+
system: PLAN_SYSTEM,
|
|
1929
|
+
messages: [{ role: "user", content: prompt }],
|
|
1930
|
+
maxTokens: 4096
|
|
1931
|
+
});
|
|
1932
|
+
const jsonMatch = result.text.match(/\{[\s\S]*\}/);
|
|
1933
|
+
if (!jsonMatch) {
|
|
1934
|
+
throw new Error("Failed to generate structured plan");
|
|
1935
|
+
}
|
|
1936
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1937
|
+
const plan = {
|
|
1938
|
+
goal,
|
|
1939
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1940
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1941
|
+
tasks: parsed.tasks.map((t, i) => ({
|
|
1942
|
+
id: i + 1,
|
|
1943
|
+
title: t.title,
|
|
1944
|
+
description: t.description,
|
|
1945
|
+
files: t.files ?? [],
|
|
1946
|
+
status: "pending",
|
|
1947
|
+
attempts: 0
|
|
1948
|
+
})),
|
|
1949
|
+
completedCount: 0,
|
|
1950
|
+
totalCount: parsed.tasks.length
|
|
1951
|
+
};
|
|
1952
|
+
return plan;
|
|
1953
|
+
}
|
|
1954
|
+
async function savePlan(plan, cwd) {
|
|
1955
|
+
plan.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1956
|
+
plan.completedCount = plan.tasks.filter((t) => t.status === "done").length;
|
|
1957
|
+
await fs10.writeFile(path11.join(cwd, PLAN_FILE), JSON.stringify(plan, null, 2), "utf-8");
|
|
1958
|
+
}
|
|
1959
|
+
async function loadPlan(cwd) {
|
|
1960
|
+
try {
|
|
1961
|
+
const raw = await fs10.readFile(path11.join(cwd, PLAN_FILE), "utf-8");
|
|
1962
|
+
return JSON.parse(raw);
|
|
1963
|
+
} catch {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function deletePlan(cwd) {
|
|
1968
|
+
try {
|
|
1969
|
+
await fs10.unlink(path11.join(cwd, PLAN_FILE));
|
|
1970
|
+
} catch {
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
async function executeTask(task, plan, model, toolContext, callbacks) {
|
|
1974
|
+
const tools = buildToolMap(toolContext);
|
|
1975
|
+
const maxIter = 20;
|
|
1976
|
+
let repoContext = "";
|
|
1977
|
+
try {
|
|
1978
|
+
const repoMap = await buildRepoMap(toolContext.cwd);
|
|
1979
|
+
repoContext = condensedRepoMap(repoMap, 3e3);
|
|
1980
|
+
} catch {
|
|
1981
|
+
}
|
|
1982
|
+
const systemPrompt = `You are Notch, an AI coding agent executing a specific task from a plan.
|
|
1983
|
+
|
|
1984
|
+
## Your Task
|
|
1985
|
+
Task ${task.id}/${plan.totalCount}: ${task.title}
|
|
1986
|
+
|
|
1987
|
+
${task.description}
|
|
1988
|
+
|
|
1989
|
+
${task.files.length > 0 ? `Target files: ${task.files.join(", ")}` : ""}
|
|
1990
|
+
|
|
1991
|
+
## Full Plan Context
|
|
1992
|
+
Goal: ${plan.goal}
|
|
1993
|
+
Completed: ${plan.completedCount}/${plan.totalCount} tasks
|
|
1994
|
+
|
|
1995
|
+
Previous tasks (already done):
|
|
1996
|
+
${plan.tasks.filter((t) => t.status === "done").map((t) => ` \u2713 ${t.title}`).join("\n") || " (none yet)"}
|
|
1997
|
+
|
|
1998
|
+
## Working Directory
|
|
1999
|
+
${toolContext.cwd}
|
|
2000
|
+
|
|
2001
|
+
## Repository
|
|
2002
|
+
${repoContext}
|
|
2003
|
+
|
|
2004
|
+
## Available Tools
|
|
2005
|
+
${describeTools()}
|
|
2006
|
+
|
|
2007
|
+
## Instructions
|
|
2008
|
+
1. Read relevant files to understand the current state
|
|
2009
|
+
2. Implement the changes described in your task
|
|
2010
|
+
3. Verify your changes work (run tests if applicable)
|
|
2011
|
+
4. Commit your changes with a descriptive message
|
|
2012
|
+
5. Respond with a brief summary of what you did
|
|
2013
|
+
|
|
2014
|
+
Be focused. Only implement what this task requires. Do not scope-creep into other tasks.`;
|
|
2015
|
+
const messages = [
|
|
2016
|
+
{ role: "user", content: `Execute task ${task.id}: ${task.title}
|
|
2017
|
+
|
|
2018
|
+
${task.description}` }
|
|
2019
|
+
];
|
|
2020
|
+
let iterations = 0;
|
|
2021
|
+
let totalText = "";
|
|
2022
|
+
try {
|
|
2023
|
+
while (iterations < maxIter) {
|
|
2024
|
+
iterations++;
|
|
2025
|
+
const result = streamText3({
|
|
2026
|
+
model,
|
|
2027
|
+
system: systemPrompt,
|
|
2028
|
+
messages,
|
|
2029
|
+
tools,
|
|
2030
|
+
maxSteps: 1
|
|
2031
|
+
});
|
|
2032
|
+
let fullText = "";
|
|
2033
|
+
const toolCalls = [];
|
|
2034
|
+
const toolResults = [];
|
|
2035
|
+
for await (const event of result.fullStream) {
|
|
2036
|
+
if (event.type === "text-delta") {
|
|
2037
|
+
fullText += event.textDelta;
|
|
2038
|
+
callbacks.onText?.(event.textDelta);
|
|
2039
|
+
} else if (event.type === "tool-call") {
|
|
2040
|
+
toolCalls.push({
|
|
2041
|
+
toolCallId: event.toolCallId,
|
|
2042
|
+
toolName: event.toolName,
|
|
2043
|
+
args: event.args
|
|
2044
|
+
});
|
|
2045
|
+
callbacks.onToolCall?.(event.toolName, event.args);
|
|
2046
|
+
}
|
|
2047
|
+
const evt = event;
|
|
2048
|
+
if (evt.type === "tool-result") {
|
|
2049
|
+
const res = evt.result;
|
|
2050
|
+
toolResults.push({
|
|
2051
|
+
toolCallId: evt.toolCallId,
|
|
2052
|
+
toolName: toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
|
|
2053
|
+
result: evt.result
|
|
2054
|
+
});
|
|
2055
|
+
callbacks.onToolResult?.(
|
|
2056
|
+
toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
|
|
2057
|
+
res?.content ?? String(evt.result),
|
|
2058
|
+
res?.isError ?? false
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (toolCalls.length > 0) {
|
|
2063
|
+
messages.push({
|
|
2064
|
+
role: "assistant",
|
|
2065
|
+
content: [
|
|
2066
|
+
...fullText ? [{ type: "text", text: fullText }] : [],
|
|
2067
|
+
...toolCalls.map((tc) => ({
|
|
2068
|
+
type: "tool-call",
|
|
2069
|
+
toolCallId: tc.toolCallId,
|
|
2070
|
+
toolName: tc.toolName,
|
|
2071
|
+
args: tc.args
|
|
2072
|
+
}))
|
|
2073
|
+
]
|
|
2074
|
+
});
|
|
2075
|
+
messages.push({
|
|
2076
|
+
role: "tool",
|
|
2077
|
+
content: toolResults.map((tr) => ({
|
|
2078
|
+
type: "tool-result",
|
|
2079
|
+
toolCallId: tr.toolCallId,
|
|
2080
|
+
toolName: tr.toolName,
|
|
2081
|
+
result: tr.result
|
|
2082
|
+
}))
|
|
2083
|
+
});
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
totalText = fullText;
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
return { success: true, text: totalText };
|
|
2090
|
+
} catch (err) {
|
|
2091
|
+
return { success: false, error: err.message, text: totalText };
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
async function runRalphLoop(plan, model, toolContext, cwd, callbacks) {
|
|
2095
|
+
for (const task of plan.tasks) {
|
|
2096
|
+
if (task.status === "done" || task.status === "skipped") continue;
|
|
2097
|
+
task.status = "in_progress";
|
|
2098
|
+
task.attempts++;
|
|
2099
|
+
callbacks.onTaskStart?.(task);
|
|
2100
|
+
await savePlan(plan, cwd);
|
|
2101
|
+
const result = await executeTask(task, plan, model, toolContext, {
|
|
2102
|
+
onToolCall: callbacks.onToolCall,
|
|
2103
|
+
onToolResult: callbacks.onToolResult,
|
|
2104
|
+
onText: callbacks.onText
|
|
2105
|
+
});
|
|
2106
|
+
if (result.success) {
|
|
2107
|
+
task.status = "done";
|
|
2108
|
+
callbacks.onTaskEnd?.(task, result);
|
|
2109
|
+
} else {
|
|
2110
|
+
if (task.attempts >= MAX_TASK_ATTEMPTS) {
|
|
2111
|
+
task.status = "failed";
|
|
2112
|
+
task.error = result.error;
|
|
2113
|
+
callbacks.onTaskEnd?.(task, result);
|
|
2114
|
+
} else {
|
|
2115
|
+
task.status = "pending";
|
|
2116
|
+
task.error = result.error;
|
|
2117
|
+
callbacks.onTaskEnd?.(task, result);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
await savePlan(plan, cwd);
|
|
2121
|
+
}
|
|
2122
|
+
callbacks.onComplete?.(plan);
|
|
2123
|
+
return plan;
|
|
2124
|
+
}
|
|
2125
|
+
function formatRalphStatus(plan) {
|
|
2126
|
+
const lines = [];
|
|
2127
|
+
const done = plan.tasks.filter((t) => t.status === "done").length;
|
|
2128
|
+
const failed = plan.tasks.filter((t) => t.status === "failed").length;
|
|
2129
|
+
const pending = plan.tasks.filter((t) => t.status === "pending").length;
|
|
2130
|
+
const progress = plan.totalCount > 0 ? Math.round(done / plan.totalCount * 100) : 0;
|
|
2131
|
+
const barWidth = 25;
|
|
2132
|
+
const filled = Math.round(done / plan.totalCount * barWidth);
|
|
2133
|
+
const bar = chalk4.green("\u2588".repeat(filled)) + chalk4.gray("\u2591".repeat(barWidth - filled));
|
|
2134
|
+
lines.push("");
|
|
2135
|
+
lines.push(chalk4.bold.white(` Ralph Mode: ${plan.goal}`));
|
|
2136
|
+
lines.push(` [${bar}] ${progress}% (${done}/${plan.totalCount} tasks)`);
|
|
2137
|
+
if (failed > 0) lines.push(chalk4.red(` ${failed} failed`));
|
|
2138
|
+
lines.push("");
|
|
2139
|
+
for (const task of plan.tasks) {
|
|
2140
|
+
const icon = task.status === "done" ? chalk4.green("\u2713") : task.status === "failed" ? chalk4.red("\u2717") : task.status === "in_progress" ? chalk4.yellow("\u25B6") : task.status === "skipped" ? chalk4.gray("\u2013") : chalk4.gray("\u25CB");
|
|
2141
|
+
const title = task.status === "done" ? chalk4.gray(task.title) : task.status === "failed" ? chalk4.red(task.title) : chalk4.white(task.title);
|
|
2142
|
+
const files = task.files.length > 0 ? chalk4.cyan(` [${task.files.join(", ")}]`) : "";
|
|
2143
|
+
const err = task.error ? chalk4.red(` \u2014 ${task.error.slice(0, 60)}`) : "";
|
|
2144
|
+
const attempts = task.attempts > 1 ? chalk4.yellow(` (${task.attempts} attempts)`) : "";
|
|
2145
|
+
lines.push(` ${icon} ${chalk4.gray(`${task.id}.`)} ${title}${files}${attempts}${err}`);
|
|
2146
|
+
}
|
|
2147
|
+
lines.push("");
|
|
2148
|
+
return lines.join("\n");
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// src/context/references.ts
|
|
2152
|
+
import fs11 from "fs/promises";
|
|
2153
|
+
import path12 from "path";
|
|
2154
|
+
import { glob as glob2 } from "glob";
|
|
2155
|
+
async function resolveReferences(input, cwd) {
|
|
2156
|
+
const references = [];
|
|
2157
|
+
const refPattern = /@((?:https?:\/\/\S+)|(?:[^\s`]+))/g;
|
|
2158
|
+
const matches = [...input.matchAll(refPattern)];
|
|
2159
|
+
if (matches.length === 0) {
|
|
2160
|
+
return { cleanInput: input, references: [] };
|
|
2161
|
+
}
|
|
2162
|
+
let cleanInput = input;
|
|
2163
|
+
for (const match of matches) {
|
|
2164
|
+
const ref = match[1];
|
|
2165
|
+
cleanInput = cleanInput.replace(match[0], "").trim();
|
|
2166
|
+
if (ref.startsWith("http://") || ref.startsWith("https://")) {
|
|
2167
|
+
const resolved = await resolveUrl(ref);
|
|
2168
|
+
references.push(resolved);
|
|
2169
|
+
} else if (ref.includes("*")) {
|
|
2170
|
+
const resolved = await resolveGlob(ref, cwd);
|
|
2171
|
+
references.push(...resolved);
|
|
2172
|
+
} else {
|
|
2173
|
+
const resolved = await resolveFile(ref, cwd);
|
|
2174
|
+
references.push(resolved);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return { cleanInput, references };
|
|
2178
|
+
}
|
|
2179
|
+
function formatReferences(refs) {
|
|
2180
|
+
if (refs.length === 0) return "";
|
|
2181
|
+
const sections = [];
|
|
2182
|
+
for (const ref of refs) {
|
|
2183
|
+
if (ref.error) {
|
|
2184
|
+
sections.push(`<!-- @${ref.original}: ${ref.error} -->`);
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
const label = ref.type === "url" ? ref.original : ref.resolved;
|
|
2188
|
+
const truncated = ref.content.length > 3e4 ? ref.content.slice(0, 3e4) + "\n... (truncated)" : ref.content;
|
|
2189
|
+
sections.push(`<referenced_${ref.type} path="${label}">
|
|
2190
|
+
${truncated}
|
|
2191
|
+
</referenced_${ref.type}>`);
|
|
2192
|
+
}
|
|
2193
|
+
return sections.join("\n\n") + "\n\n";
|
|
2194
|
+
}
|
|
2195
|
+
async function resolveFile(ref, cwd) {
|
|
2196
|
+
const filePath = path12.isAbsolute(ref) ? ref : path12.resolve(cwd, ref);
|
|
2197
|
+
try {
|
|
2198
|
+
const content = await fs11.readFile(filePath, "utf-8");
|
|
2199
|
+
const lines = content.split("\n");
|
|
2200
|
+
const numbered = lines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
|
|
2201
|
+
return {
|
|
2202
|
+
type: "file",
|
|
2203
|
+
original: ref,
|
|
2204
|
+
resolved: filePath,
|
|
2205
|
+
content: `${filePath} (${lines.length} lines):
|
|
2206
|
+
${numbered}`
|
|
2207
|
+
};
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
return {
|
|
2210
|
+
type: "file",
|
|
2211
|
+
original: ref,
|
|
2212
|
+
resolved: filePath,
|
|
2213
|
+
content: "",
|
|
2214
|
+
error: err.code === "ENOENT" ? `File not found: ${filePath}` : err.message
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
async function resolveUrl(url) {
|
|
2219
|
+
try {
|
|
2220
|
+
const response = await fetch(url, {
|
|
2221
|
+
signal: AbortSignal.timeout(1e4),
|
|
2222
|
+
headers: { "User-Agent": "Notch-CLI/0.2 (AI coding assistant)" }
|
|
2223
|
+
});
|
|
2224
|
+
if (!response.ok) {
|
|
2225
|
+
return {
|
|
2226
|
+
type: "url",
|
|
2227
|
+
original: url,
|
|
2228
|
+
resolved: url,
|
|
2229
|
+
content: "",
|
|
2230
|
+
error: `HTTP ${response.status}`
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
const text = await response.text();
|
|
2234
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
2235
|
+
let content;
|
|
2236
|
+
if (contentType.includes("html")) {
|
|
2237
|
+
content = text.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, "").replace(/\s{3,}/g, "\n\n").trim();
|
|
2238
|
+
} else {
|
|
2239
|
+
content = text;
|
|
2240
|
+
}
|
|
2241
|
+
return { type: "url", original: url, resolved: url, content };
|
|
2242
|
+
} catch (err) {
|
|
2243
|
+
return {
|
|
2244
|
+
type: "url",
|
|
2245
|
+
original: url,
|
|
2246
|
+
resolved: url,
|
|
2247
|
+
content: "",
|
|
2248
|
+
error: err.message
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async function resolveGlob(pattern, cwd) {
|
|
2253
|
+
try {
|
|
2254
|
+
const matches = await glob2(pattern, {
|
|
2255
|
+
cwd,
|
|
2256
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
|
|
2257
|
+
nodir: true
|
|
2258
|
+
});
|
|
2259
|
+
if (matches.length === 0) {
|
|
2260
|
+
return [{
|
|
2261
|
+
type: "glob",
|
|
2262
|
+
original: pattern,
|
|
2263
|
+
resolved: pattern,
|
|
2264
|
+
content: "",
|
|
2265
|
+
error: `No files matching: ${pattern}`
|
|
2266
|
+
}];
|
|
2267
|
+
}
|
|
2268
|
+
const capped = matches.slice(0, 10);
|
|
2269
|
+
const results = [];
|
|
2270
|
+
for (const file of capped) {
|
|
2271
|
+
results.push(await resolveFile(file, cwd));
|
|
2272
|
+
}
|
|
2273
|
+
if (matches.length > 10) {
|
|
2274
|
+
results.push({
|
|
2275
|
+
type: "glob",
|
|
2276
|
+
original: pattern,
|
|
2277
|
+
resolved: pattern,
|
|
2278
|
+
content: `\u26A0 Warning: ${matches.length} files matched ${pattern}, but only the first 10 were injected. Narrow your glob pattern for more precise results.`
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
return results;
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
return [{
|
|
2284
|
+
type: "glob",
|
|
2285
|
+
original: pattern,
|
|
2286
|
+
resolved: pattern,
|
|
2287
|
+
content: "",
|
|
2288
|
+
error: err.message
|
|
2289
|
+
}];
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// src/ui/themes.ts
|
|
2294
|
+
import chalk5 from "chalk";
|
|
2295
|
+
var defaultTheme = {
|
|
2296
|
+
name: "Default",
|
|
2297
|
+
description: "Classic Notch \u2014 green mantis, blue wordmark",
|
|
2298
|
+
brand: chalk5.blueBright,
|
|
2299
|
+
mascot: chalk5.greenBright,
|
|
2300
|
+
mascotAccent: chalk5.yellowBright,
|
|
2301
|
+
tagline: chalk5.cyan,
|
|
2302
|
+
prompt: chalk5.blueBright,
|
|
2303
|
+
border: chalk5.gray,
|
|
2304
|
+
dim: chalk5.gray,
|
|
2305
|
+
text: chalk5.white,
|
|
2306
|
+
bold: chalk5.white.bold,
|
|
2307
|
+
success: chalk5.green,
|
|
2308
|
+
warning: chalk5.yellow,
|
|
2309
|
+
error: chalk5.red,
|
|
2310
|
+
info: chalk5.cyan,
|
|
2311
|
+
toolName: chalk5.gray,
|
|
2312
|
+
toolArgs: chalk5.gray,
|
|
2313
|
+
toolResult: chalk5.gray,
|
|
2314
|
+
diffAdd: chalk5.green,
|
|
2315
|
+
diffRemove: chalk5.red,
|
|
2316
|
+
diffHeader: chalk5.cyan,
|
|
2317
|
+
mdH1: chalk5.white.bold,
|
|
2318
|
+
mdH2: chalk5.blueBright,
|
|
2319
|
+
mdH3: chalk5.cyan,
|
|
2320
|
+
mdCode: chalk5.cyan,
|
|
2321
|
+
mdInlineCode: chalk5.bgGray.white,
|
|
2322
|
+
mdLink: chalk5.blue.underline,
|
|
2323
|
+
meterLow: chalk5.green,
|
|
2324
|
+
meterMid: chalk5.yellow,
|
|
2325
|
+
meterHigh: chalk5.red
|
|
2326
|
+
};
|
|
2327
|
+
var interstellarTheme = {
|
|
2328
|
+
name: "Interstellar",
|
|
2329
|
+
description: "Deep space amber \u2014 Endurance control panels",
|
|
2330
|
+
brand: chalk5.hex("#FFB347"),
|
|
2331
|
+
// warm amber (instrument readout)
|
|
2332
|
+
mascot: chalk5.hex("#4A90D9"),
|
|
2333
|
+
// pale blue (frozen clouds)
|
|
2334
|
+
mascotAccent: chalk5.hex("#FFD700"),
|
|
2335
|
+
// gold (Gargantua light)
|
|
2336
|
+
tagline: chalk5.hex("#708090"),
|
|
2337
|
+
// slate gray (ship hull)
|
|
2338
|
+
prompt: chalk5.hex("#FFB347"),
|
|
2339
|
+
border: chalk5.hex("#3A3A3A"),
|
|
2340
|
+
// dark hull metal
|
|
2341
|
+
dim: chalk5.hex("#5C5C5C"),
|
|
2342
|
+
text: chalk5.hex("#D4D4D4"),
|
|
2343
|
+
// cool white (display text)
|
|
2344
|
+
bold: chalk5.hex("#FFFFFF").bold,
|
|
2345
|
+
success: chalk5.hex("#7CFC00"),
|
|
2346
|
+
// "CASE: All systems nominal"
|
|
2347
|
+
warning: chalk5.hex("#FFB347"),
|
|
2348
|
+
// amber warning
|
|
2349
|
+
error: chalk5.hex("#FF4444"),
|
|
2350
|
+
// critical alert
|
|
2351
|
+
info: chalk5.hex("#4A90D9"),
|
|
2352
|
+
// blue data readout
|
|
2353
|
+
toolName: chalk5.hex("#FFB347"),
|
|
2354
|
+
toolArgs: chalk5.hex("#708090"),
|
|
2355
|
+
toolResult: chalk5.hex("#5C5C5C"),
|
|
2356
|
+
diffAdd: chalk5.hex("#7CFC00"),
|
|
2357
|
+
diffRemove: chalk5.hex("#FF4444"),
|
|
2358
|
+
diffHeader: chalk5.hex("#FFB347"),
|
|
2359
|
+
mdH1: chalk5.hex("#FFB347").bold,
|
|
2360
|
+
mdH2: chalk5.hex("#4A90D9"),
|
|
2361
|
+
mdH3: chalk5.hex("#708090"),
|
|
2362
|
+
mdCode: chalk5.hex("#FFD700"),
|
|
2363
|
+
mdInlineCode: chalk5.hex("#FFB347"),
|
|
2364
|
+
mdLink: chalk5.hex("#4A90D9").underline,
|
|
2365
|
+
meterLow: chalk5.hex("#4A90D9"),
|
|
2366
|
+
meterMid: chalk5.hex("#FFB347"),
|
|
2367
|
+
meterHigh: chalk5.hex("#FF4444")
|
|
2368
|
+
};
|
|
2369
|
+
var spaceGrayTheme = {
|
|
2370
|
+
name: "Space Gray",
|
|
2371
|
+
description: "Apple minimalism \u2014 silver, cool gray, ice blue",
|
|
2372
|
+
brand: chalk5.hex("#A2AAAD"),
|
|
2373
|
+
// apple silver
|
|
2374
|
+
mascot: chalk5.hex("#86868B"),
|
|
2375
|
+
// space gray
|
|
2376
|
+
mascotAccent: chalk5.hex("#0071E3"),
|
|
2377
|
+
// apple blue
|
|
2378
|
+
tagline: chalk5.hex("#6E6E73"),
|
|
2379
|
+
// secondary gray
|
|
2380
|
+
prompt: chalk5.hex("#A2AAAD"),
|
|
2381
|
+
border: chalk5.hex("#3A3A3C"),
|
|
2382
|
+
// dark separator
|
|
2383
|
+
dim: chalk5.hex("#6E6E73"),
|
|
2384
|
+
text: chalk5.hex("#E5E5EA"),
|
|
2385
|
+
// system gray 6
|
|
2386
|
+
bold: chalk5.hex("#F5F5F7").bold,
|
|
2387
|
+
// near white
|
|
2388
|
+
success: chalk5.hex("#30D158"),
|
|
2389
|
+
// apple green
|
|
2390
|
+
warning: chalk5.hex("#FFD60A"),
|
|
2391
|
+
// apple yellow
|
|
2392
|
+
error: chalk5.hex("#FF453A"),
|
|
2393
|
+
// apple red
|
|
2394
|
+
info: chalk5.hex("#0A84FF"),
|
|
2395
|
+
// apple blue
|
|
2396
|
+
toolName: chalk5.hex("#A2AAAD"),
|
|
2397
|
+
toolArgs: chalk5.hex("#6E6E73"),
|
|
2398
|
+
toolResult: chalk5.hex("#48484A"),
|
|
2399
|
+
diffAdd: chalk5.hex("#30D158"),
|
|
2400
|
+
diffRemove: chalk5.hex("#FF453A"),
|
|
2401
|
+
diffHeader: chalk5.hex("#0A84FF"),
|
|
2402
|
+
mdH1: chalk5.hex("#F5F5F7").bold,
|
|
2403
|
+
mdH2: chalk5.hex("#0A84FF"),
|
|
2404
|
+
mdH3: chalk5.hex("#A2AAAD"),
|
|
2405
|
+
mdCode: chalk5.hex("#BF5AF2"),
|
|
2406
|
+
// apple purple for code
|
|
2407
|
+
mdInlineCode: chalk5.hex("#BF5AF2"),
|
|
2408
|
+
mdLink: chalk5.hex("#0A84FF").underline,
|
|
2409
|
+
meterLow: chalk5.hex("#30D158"),
|
|
2410
|
+
meterMid: chalk5.hex("#FFD60A"),
|
|
2411
|
+
meterHigh: chalk5.hex("#FF453A")
|
|
2412
|
+
};
|
|
2413
|
+
var cyberpunkTheme = {
|
|
2414
|
+
name: "Cyberpunk",
|
|
2415
|
+
description: "Neon pink & cyan \u2014 Night City terminal",
|
|
2416
|
+
brand: chalk5.hex("#FF2079"),
|
|
2417
|
+
// hot pink
|
|
2418
|
+
mascot: chalk5.hex("#00FFFF"),
|
|
2419
|
+
// electric cyan
|
|
2420
|
+
mascotAccent: chalk5.hex("#FF2079"),
|
|
2421
|
+
// hot pink eyes
|
|
2422
|
+
tagline: chalk5.hex("#9D00FF"),
|
|
2423
|
+
// deep violet
|
|
2424
|
+
prompt: chalk5.hex("#FF2079"),
|
|
2425
|
+
border: chalk5.hex("#1A1A2E"),
|
|
2426
|
+
dim: chalk5.hex("#4A4A6A"),
|
|
2427
|
+
text: chalk5.hex("#E0E0FF"),
|
|
2428
|
+
bold: chalk5.hex("#FFFFFF").bold,
|
|
2429
|
+
success: chalk5.hex("#00FF41"),
|
|
2430
|
+
// matrix green
|
|
2431
|
+
warning: chalk5.hex("#FFFF00"),
|
|
2432
|
+
// neon yellow
|
|
2433
|
+
error: chalk5.hex("#FF0000"),
|
|
2434
|
+
info: chalk5.hex("#00FFFF"),
|
|
2435
|
+
toolName: chalk5.hex("#FF2079"),
|
|
2436
|
+
toolArgs: chalk5.hex("#9D00FF"),
|
|
2437
|
+
toolResult: chalk5.hex("#4A4A6A"),
|
|
2438
|
+
diffAdd: chalk5.hex("#00FF41"),
|
|
2439
|
+
diffRemove: chalk5.hex("#FF0000"),
|
|
2440
|
+
diffHeader: chalk5.hex("#FF2079"),
|
|
2441
|
+
mdH1: chalk5.hex("#FF2079").bold,
|
|
2442
|
+
mdH2: chalk5.hex("#00FFFF"),
|
|
2443
|
+
mdH3: chalk5.hex("#9D00FF"),
|
|
2444
|
+
mdCode: chalk5.hex("#00FF41"),
|
|
2445
|
+
mdInlineCode: chalk5.hex("#00FFFF"),
|
|
2446
|
+
mdLink: chalk5.hex("#9D00FF").underline,
|
|
2447
|
+
meterLow: chalk5.hex("#00FF41"),
|
|
2448
|
+
meterMid: chalk5.hex("#FFFF00"),
|
|
2449
|
+
meterHigh: chalk5.hex("#FF0000")
|
|
2450
|
+
};
|
|
2451
|
+
var auroraTheme = {
|
|
2452
|
+
name: "Aurora",
|
|
2453
|
+
description: "Northern lights \u2014 teal, violet, polar green",
|
|
2454
|
+
brand: chalk5.hex("#7FFFD4"),
|
|
2455
|
+
// aquamarine
|
|
2456
|
+
mascot: chalk5.hex("#00CED1"),
|
|
2457
|
+
// dark turquoise
|
|
2458
|
+
mascotAccent: chalk5.hex("#DA70D6"),
|
|
2459
|
+
// orchid
|
|
2460
|
+
tagline: chalk5.hex("#9370DB"),
|
|
2461
|
+
// medium purple
|
|
2462
|
+
prompt: chalk5.hex("#7FFFD4"),
|
|
2463
|
+
border: chalk5.hex("#2F4F4F"),
|
|
2464
|
+
// dark slate
|
|
2465
|
+
dim: chalk5.hex("#5F9EA0"),
|
|
2466
|
+
// cadet blue
|
|
2467
|
+
text: chalk5.hex("#E0FFFF"),
|
|
2468
|
+
// light cyan
|
|
2469
|
+
bold: chalk5.hex("#F0FFFF").bold,
|
|
2470
|
+
success: chalk5.hex("#00FA9A"),
|
|
2471
|
+
// medium spring green
|
|
2472
|
+
warning: chalk5.hex("#DDA0DD"),
|
|
2473
|
+
// plum
|
|
2474
|
+
error: chalk5.hex("#FF6B6B"),
|
|
2475
|
+
info: chalk5.hex("#87CEEB"),
|
|
2476
|
+
// sky blue
|
|
2477
|
+
toolName: chalk5.hex("#7FFFD4"),
|
|
2478
|
+
toolArgs: chalk5.hex("#5F9EA0"),
|
|
2479
|
+
toolResult: chalk5.hex("#2F4F4F"),
|
|
2480
|
+
diffAdd: chalk5.hex("#00FA9A"),
|
|
2481
|
+
diffRemove: chalk5.hex("#FF6B6B"),
|
|
2482
|
+
diffHeader: chalk5.hex("#DA70D6"),
|
|
2483
|
+
mdH1: chalk5.hex("#7FFFD4").bold,
|
|
2484
|
+
mdH2: chalk5.hex("#DA70D6"),
|
|
2485
|
+
mdH3: chalk5.hex("#87CEEB"),
|
|
2486
|
+
mdCode: chalk5.hex("#00CED1"),
|
|
2487
|
+
mdInlineCode: chalk5.hex("#7FFFD4"),
|
|
2488
|
+
mdLink: chalk5.hex("#DA70D6").underline,
|
|
2489
|
+
meterLow: chalk5.hex("#00FA9A"),
|
|
2490
|
+
meterMid: chalk5.hex("#DDA0DD"),
|
|
2491
|
+
meterHigh: chalk5.hex("#FF6B6B")
|
|
2492
|
+
};
|
|
2493
|
+
var solarizedTheme = {
|
|
2494
|
+
name: "Solarized",
|
|
2495
|
+
description: "Solarized Dark \u2014 warm, precise, classic dev palette",
|
|
2496
|
+
brand: chalk5.hex("#268BD2"),
|
|
2497
|
+
// blue
|
|
2498
|
+
mascot: chalk5.hex("#859900"),
|
|
2499
|
+
// green
|
|
2500
|
+
mascotAccent: chalk5.hex("#B58900"),
|
|
2501
|
+
// yellow
|
|
2502
|
+
tagline: chalk5.hex("#2AA198"),
|
|
2503
|
+
// cyan
|
|
2504
|
+
prompt: chalk5.hex("#268BD2"),
|
|
2505
|
+
border: chalk5.hex("#073642"),
|
|
2506
|
+
// base02
|
|
2507
|
+
dim: chalk5.hex("#586E75"),
|
|
2508
|
+
// base01
|
|
2509
|
+
text: chalk5.hex("#839496"),
|
|
2510
|
+
// base0
|
|
2511
|
+
bold: chalk5.hex("#93A1A1").bold,
|
|
2512
|
+
// base1
|
|
2513
|
+
success: chalk5.hex("#859900"),
|
|
2514
|
+
warning: chalk5.hex("#B58900"),
|
|
2515
|
+
error: chalk5.hex("#DC322F"),
|
|
2516
|
+
info: chalk5.hex("#2AA198"),
|
|
2517
|
+
toolName: chalk5.hex("#268BD2"),
|
|
2518
|
+
toolArgs: chalk5.hex("#586E75"),
|
|
2519
|
+
toolResult: chalk5.hex("#073642"),
|
|
2520
|
+
diffAdd: chalk5.hex("#859900"),
|
|
2521
|
+
diffRemove: chalk5.hex("#DC322F"),
|
|
2522
|
+
diffHeader: chalk5.hex("#268BD2"),
|
|
2523
|
+
mdH1: chalk5.hex("#93A1A1").bold,
|
|
2524
|
+
mdH2: chalk5.hex("#268BD2"),
|
|
2525
|
+
mdH3: chalk5.hex("#2AA198"),
|
|
2526
|
+
mdCode: chalk5.hex("#859900"),
|
|
2527
|
+
mdInlineCode: chalk5.hex("#2AA198"),
|
|
2528
|
+
mdLink: chalk5.hex("#268BD2").underline,
|
|
2529
|
+
meterLow: chalk5.hex("#859900"),
|
|
2530
|
+
meterMid: chalk5.hex("#B58900"),
|
|
2531
|
+
meterHigh: chalk5.hex("#DC322F")
|
|
2532
|
+
};
|
|
2533
|
+
var draculaTheme = {
|
|
2534
|
+
name: "Dracula",
|
|
2535
|
+
description: "Dracula \u2014 purple, pink, green on charcoal",
|
|
2536
|
+
brand: chalk5.hex("#BD93F9"),
|
|
2537
|
+
// purple
|
|
2538
|
+
mascot: chalk5.hex("#50FA7B"),
|
|
2539
|
+
// green
|
|
2540
|
+
mascotAccent: chalk5.hex("#FF79C6"),
|
|
2541
|
+
// pink
|
|
2542
|
+
tagline: chalk5.hex("#6272A4"),
|
|
2543
|
+
// comment gray
|
|
2544
|
+
prompt: chalk5.hex("#BD93F9"),
|
|
2545
|
+
border: chalk5.hex("#44475A"),
|
|
2546
|
+
// current line
|
|
2547
|
+
dim: chalk5.hex("#6272A4"),
|
|
2548
|
+
// comment
|
|
2549
|
+
text: chalk5.hex("#F8F8F2"),
|
|
2550
|
+
// foreground
|
|
2551
|
+
bold: chalk5.hex("#F8F8F2").bold,
|
|
2552
|
+
success: chalk5.hex("#50FA7B"),
|
|
2553
|
+
warning: chalk5.hex("#F1FA8C"),
|
|
2554
|
+
// yellow
|
|
2555
|
+
error: chalk5.hex("#FF5555"),
|
|
2556
|
+
info: chalk5.hex("#8BE9FD"),
|
|
2557
|
+
// cyan
|
|
2558
|
+
toolName: chalk5.hex("#BD93F9"),
|
|
2559
|
+
toolArgs: chalk5.hex("#6272A4"),
|
|
2560
|
+
toolResult: chalk5.hex("#44475A"),
|
|
2561
|
+
diffAdd: chalk5.hex("#50FA7B"),
|
|
2562
|
+
diffRemove: chalk5.hex("#FF5555"),
|
|
2563
|
+
diffHeader: chalk5.hex("#FF79C6"),
|
|
2564
|
+
mdH1: chalk5.hex("#FF79C6").bold,
|
|
2565
|
+
mdH2: chalk5.hex("#BD93F9"),
|
|
2566
|
+
mdH3: chalk5.hex("#8BE9FD"),
|
|
2567
|
+
mdCode: chalk5.hex("#50FA7B"),
|
|
2568
|
+
mdInlineCode: chalk5.hex("#F1FA8C"),
|
|
2569
|
+
mdLink: chalk5.hex("#8BE9FD").underline,
|
|
2570
|
+
meterLow: chalk5.hex("#50FA7B"),
|
|
2571
|
+
meterMid: chalk5.hex("#F1FA8C"),
|
|
2572
|
+
meterHigh: chalk5.hex("#FF5555")
|
|
2573
|
+
};
|
|
2574
|
+
var monokaiTheme = {
|
|
2575
|
+
name: "Monokai",
|
|
2576
|
+
description: "Monokai Pro \u2014 vivid on dark, a classic",
|
|
2577
|
+
brand: chalk5.hex("#A6E22E"),
|
|
2578
|
+
// green
|
|
2579
|
+
mascot: chalk5.hex("#66D9EF"),
|
|
2580
|
+
// blue
|
|
2581
|
+
mascotAccent: chalk5.hex("#F92672"),
|
|
2582
|
+
// pink
|
|
2583
|
+
tagline: chalk5.hex("#75715E"),
|
|
2584
|
+
// comment
|
|
2585
|
+
prompt: chalk5.hex("#A6E22E"),
|
|
2586
|
+
border: chalk5.hex("#3E3D32"),
|
|
2587
|
+
dim: chalk5.hex("#75715E"),
|
|
2588
|
+
text: chalk5.hex("#F8F8F2"),
|
|
2589
|
+
bold: chalk5.hex("#F8F8F2").bold,
|
|
2590
|
+
success: chalk5.hex("#A6E22E"),
|
|
2591
|
+
warning: chalk5.hex("#E6DB74"),
|
|
2592
|
+
// yellow
|
|
2593
|
+
error: chalk5.hex("#F92672"),
|
|
2594
|
+
info: chalk5.hex("#66D9EF"),
|
|
2595
|
+
toolName: chalk5.hex("#A6E22E"),
|
|
2596
|
+
toolArgs: chalk5.hex("#75715E"),
|
|
2597
|
+
toolResult: chalk5.hex("#3E3D32"),
|
|
2598
|
+
diffAdd: chalk5.hex("#A6E22E"),
|
|
2599
|
+
diffRemove: chalk5.hex("#F92672"),
|
|
2600
|
+
diffHeader: chalk5.hex("#66D9EF"),
|
|
2601
|
+
mdH1: chalk5.hex("#F92672").bold,
|
|
2602
|
+
mdH2: chalk5.hex("#A6E22E"),
|
|
2603
|
+
mdH3: chalk5.hex("#66D9EF"),
|
|
2604
|
+
mdCode: chalk5.hex("#E6DB74"),
|
|
2605
|
+
mdInlineCode: chalk5.hex("#A6E22E"),
|
|
2606
|
+
mdLink: chalk5.hex("#66D9EF").underline,
|
|
2607
|
+
meterLow: chalk5.hex("#A6E22E"),
|
|
2608
|
+
meterMid: chalk5.hex("#E6DB74"),
|
|
2609
|
+
meterHigh: chalk5.hex("#F92672")
|
|
2610
|
+
};
|
|
2611
|
+
var oceanTheme = {
|
|
2612
|
+
name: "Ocean",
|
|
2613
|
+
description: "Deep sea \u2014 midnight blue, bioluminescent glow",
|
|
2614
|
+
brand: chalk5.hex("#00B4D8"),
|
|
2615
|
+
// cerulean
|
|
2616
|
+
mascot: chalk5.hex("#0077B6"),
|
|
2617
|
+
// deep blue
|
|
2618
|
+
mascotAccent: chalk5.hex("#90E0EF"),
|
|
2619
|
+
// light blue (bioluminescent)
|
|
2620
|
+
tagline: chalk5.hex("#023E8A"),
|
|
2621
|
+
// navy
|
|
2622
|
+
prompt: chalk5.hex("#00B4D8"),
|
|
2623
|
+
border: chalk5.hex("#03045E"),
|
|
2624
|
+
// deep navy
|
|
2625
|
+
dim: chalk5.hex("#0077B6"),
|
|
2626
|
+
text: chalk5.hex("#CAF0F8"),
|
|
2627
|
+
// lightest blue
|
|
2628
|
+
bold: chalk5.hex("#CAF0F8").bold,
|
|
2629
|
+
success: chalk5.hex("#2DC653"),
|
|
2630
|
+
// kelp green
|
|
2631
|
+
warning: chalk5.hex("#F4A261"),
|
|
2632
|
+
// sandy
|
|
2633
|
+
error: chalk5.hex("#E76F51"),
|
|
2634
|
+
// coral
|
|
2635
|
+
info: chalk5.hex("#90E0EF"),
|
|
2636
|
+
toolName: chalk5.hex("#00B4D8"),
|
|
2637
|
+
toolArgs: chalk5.hex("#0077B6"),
|
|
2638
|
+
toolResult: chalk5.hex("#03045E"),
|
|
2639
|
+
diffAdd: chalk5.hex("#2DC653"),
|
|
2640
|
+
diffRemove: chalk5.hex("#E76F51"),
|
|
2641
|
+
diffHeader: chalk5.hex("#00B4D8"),
|
|
2642
|
+
mdH1: chalk5.hex("#90E0EF").bold,
|
|
2643
|
+
mdH2: chalk5.hex("#00B4D8"),
|
|
2644
|
+
mdH3: chalk5.hex("#0077B6"),
|
|
2645
|
+
mdCode: chalk5.hex("#90E0EF"),
|
|
2646
|
+
mdInlineCode: chalk5.hex("#00B4D8"),
|
|
2647
|
+
mdLink: chalk5.hex("#90E0EF").underline,
|
|
2648
|
+
meterLow: chalk5.hex("#00B4D8"),
|
|
2649
|
+
meterMid: chalk5.hex("#F4A261"),
|
|
2650
|
+
meterHigh: chalk5.hex("#E76F51")
|
|
2651
|
+
};
|
|
2652
|
+
var emberTheme = {
|
|
2653
|
+
name: "Ember",
|
|
2654
|
+
description: "Fire forged \u2014 charcoal, ember orange, flame red",
|
|
2655
|
+
brand: chalk5.hex("#FF6B35"),
|
|
2656
|
+
// flame orange
|
|
2657
|
+
mascot: chalk5.hex("#D62828"),
|
|
2658
|
+
// deep red
|
|
2659
|
+
mascotAccent: chalk5.hex("#FFD166"),
|
|
2660
|
+
// bright flame
|
|
2661
|
+
tagline: chalk5.hex("#8B4513"),
|
|
2662
|
+
// saddle brown
|
|
2663
|
+
prompt: chalk5.hex("#FF6B35"),
|
|
2664
|
+
border: chalk5.hex("#2B2B2B"),
|
|
2665
|
+
// charcoal
|
|
2666
|
+
dim: chalk5.hex("#6B3A2A"),
|
|
2667
|
+
text: chalk5.hex("#F4E4C1"),
|
|
2668
|
+
// warm parchment
|
|
2669
|
+
bold: chalk5.hex("#FFF5E1").bold,
|
|
2670
|
+
success: chalk5.hex("#FFD166"),
|
|
2671
|
+
// bright flame = success
|
|
2672
|
+
warning: chalk5.hex("#FF6B35"),
|
|
2673
|
+
error: chalk5.hex("#D62828"),
|
|
2674
|
+
info: chalk5.hex("#F4845F"),
|
|
2675
|
+
// soft coral
|
|
2676
|
+
toolName: chalk5.hex("#FF6B35"),
|
|
2677
|
+
toolArgs: chalk5.hex("#6B3A2A"),
|
|
2678
|
+
toolResult: chalk5.hex("#2B2B2B"),
|
|
2679
|
+
diffAdd: chalk5.hex("#FFD166"),
|
|
2680
|
+
diffRemove: chalk5.hex("#D62828"),
|
|
2681
|
+
diffHeader: chalk5.hex("#FF6B35"),
|
|
2682
|
+
mdH1: chalk5.hex("#FF6B35").bold,
|
|
2683
|
+
mdH2: chalk5.hex("#F4845F"),
|
|
2684
|
+
mdH3: chalk5.hex("#8B4513"),
|
|
2685
|
+
mdCode: chalk5.hex("#FFD166"),
|
|
2686
|
+
mdInlineCode: chalk5.hex("#FF6B35"),
|
|
2687
|
+
mdLink: chalk5.hex("#F4845F").underline,
|
|
2688
|
+
meterLow: chalk5.hex("#FFD166"),
|
|
2689
|
+
meterMid: chalk5.hex("#FF6B35"),
|
|
2690
|
+
meterHigh: chalk5.hex("#D62828")
|
|
2691
|
+
};
|
|
2692
|
+
var ghostTheme = {
|
|
2693
|
+
name: "Ghost",
|
|
2694
|
+
description: "Monochrome \u2014 pure grayscale minimalism",
|
|
2695
|
+
brand: chalk5.hex("#E0E0E0"),
|
|
2696
|
+
mascot: chalk5.hex("#AAAAAA"),
|
|
2697
|
+
mascotAccent: chalk5.hex("#FFFFFF"),
|
|
2698
|
+
tagline: chalk5.hex("#666666"),
|
|
2699
|
+
prompt: chalk5.hex("#CCCCCC"),
|
|
2700
|
+
border: chalk5.hex("#333333"),
|
|
2701
|
+
dim: chalk5.hex("#555555"),
|
|
2702
|
+
text: chalk5.hex("#CCCCCC"),
|
|
2703
|
+
bold: chalk5.hex("#FFFFFF").bold,
|
|
2704
|
+
success: chalk5.hex("#AAAAAA"),
|
|
2705
|
+
warning: chalk5.hex("#CCCCCC"),
|
|
2706
|
+
error: chalk5.hex("#FFFFFF"),
|
|
2707
|
+
info: chalk5.hex("#999999"),
|
|
2708
|
+
toolName: chalk5.hex("#AAAAAA"),
|
|
2709
|
+
toolArgs: chalk5.hex("#666666"),
|
|
2710
|
+
toolResult: chalk5.hex("#444444"),
|
|
2711
|
+
diffAdd: chalk5.hex("#CCCCCC"),
|
|
2712
|
+
diffRemove: chalk5.hex("#777777").strikethrough,
|
|
2713
|
+
diffHeader: chalk5.hex("#E0E0E0"),
|
|
2714
|
+
mdH1: chalk5.hex("#FFFFFF").bold,
|
|
2715
|
+
mdH2: chalk5.hex("#CCCCCC"),
|
|
2716
|
+
mdH3: chalk5.hex("#AAAAAA"),
|
|
2717
|
+
mdCode: chalk5.hex("#BBBBBB"),
|
|
2718
|
+
mdInlineCode: chalk5.hex("#CCCCCC"),
|
|
2719
|
+
mdLink: chalk5.hex("#E0E0E0").underline,
|
|
2720
|
+
meterLow: chalk5.hex("#AAAAAA"),
|
|
2721
|
+
meterMid: chalk5.hex("#CCCCCC"),
|
|
2722
|
+
meterHigh: chalk5.hex("#FFFFFF")
|
|
2723
|
+
};
|
|
2724
|
+
var THEME_CATALOG = {
|
|
2725
|
+
"default": defaultTheme,
|
|
2726
|
+
"interstellar": interstellarTheme,
|
|
2727
|
+
"space-gray": spaceGrayTheme,
|
|
2728
|
+
"cyberpunk": cyberpunkTheme,
|
|
2729
|
+
"aurora": auroraTheme,
|
|
2730
|
+
"solarized": solarizedTheme,
|
|
2731
|
+
"dracula": draculaTheme,
|
|
2732
|
+
"monokai": monokaiTheme,
|
|
2733
|
+
"ocean": oceanTheme,
|
|
2734
|
+
"ember": emberTheme,
|
|
2735
|
+
"ghost": ghostTheme
|
|
2736
|
+
};
|
|
2737
|
+
var THEME_IDS = Object.keys(THEME_CATALOG);
|
|
2738
|
+
function isValidTheme(id) {
|
|
2739
|
+
return id in THEME_CATALOG;
|
|
2740
|
+
}
|
|
2741
|
+
var _activeTheme = defaultTheme;
|
|
2742
|
+
var _activeThemeId = "default";
|
|
2743
|
+
function theme() {
|
|
2744
|
+
return _activeTheme;
|
|
2745
|
+
}
|
|
2746
|
+
function themeId() {
|
|
2747
|
+
return _activeThemeId;
|
|
2748
|
+
}
|
|
2749
|
+
function setTheme(id) {
|
|
2750
|
+
const t = THEME_CATALOG[id];
|
|
2751
|
+
if (!t) throw new Error(`Unknown theme: ${id}. Available: ${THEME_IDS.join(", ")}`);
|
|
2752
|
+
_activeTheme = t;
|
|
2753
|
+
_activeThemeId = id;
|
|
2754
|
+
return t;
|
|
2755
|
+
}
|
|
2756
|
+
function formatThemeList(activeId) {
|
|
2757
|
+
const lines = ["\n Available themes:\n"];
|
|
2758
|
+
for (const id of THEME_IDS) {
|
|
2759
|
+
const t = THEME_CATALOG[id];
|
|
2760
|
+
const active = id === activeId ? t.success(" \u25CF") : " ";
|
|
2761
|
+
const name = id === activeId ? t.bold(t.name) : chalk5.gray(t.name);
|
|
2762
|
+
const desc = chalk5.gray(t.description);
|
|
2763
|
+
lines.push(` ${active} ${chalk5.hex("#888")(id.padEnd(14))} ${name} ${desc}`);
|
|
2764
|
+
}
|
|
2765
|
+
lines.push(chalk5.gray(`
|
|
2766
|
+
Switch with: /theme <name>
|
|
2767
|
+
`));
|
|
2768
|
+
return lines.join("\n");
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// src/ui/banner.ts
|
|
2772
|
+
var MANTIS = [
|
|
2773
|
+
" \u25C9\u2588\u2580\u2580\u2580\u2580\u2588\u25C9",
|
|
2774
|
+
// head + eyes
|
|
2775
|
+
" \u2580\u2588\u2584\u2584\u2588\u2580",
|
|
2776
|
+
// jaw
|
|
2777
|
+
" \u2590\u258C",
|
|
2778
|
+
// thin neck
|
|
2779
|
+
" \u2588\u2580 \u2590\u258C \u2580\u2588",
|
|
2780
|
+
// forelegs out
|
|
2781
|
+
" \u2580\u2588\u2584\u2588\u2588\u2584\u2588\u2580",
|
|
2782
|
+
// forelegs folding (prayer)
|
|
2783
|
+
" \u2590\u2588\u2588\u258C",
|
|
2784
|
+
// thorax
|
|
2785
|
+
" \u2590\u2588\u2588\u258C",
|
|
2786
|
+
// abdomen
|
|
2787
|
+
" \u2590\u2588\u2588\u258C",
|
|
2788
|
+
// abdomen
|
|
2789
|
+
" \u2584\u2580 \u2580\u2584",
|
|
2790
|
+
// legs splay
|
|
2791
|
+
" \u2580 \u2580"
|
|
2792
|
+
// feet
|
|
2793
|
+
];
|
|
2794
|
+
var LOGO_INLINE = [
|
|
2795
|
+
" \u2588\u2588\u2584 \u2588 \u2584\u2580\u2580\u2584 \u2580\u2588\u2580 \u2584\u2580\u2580\u2584 \u2588 \u2588",
|
|
2796
|
+
" \u2588 \u2580\u2584 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2580\u2580\u2588",
|
|
2797
|
+
" \u2588 \u2580\u2588\u2588 \u2580\u2584\u2584\u2580 \u2588 \u2580\u2584\u2584\u2580 \u2588 \u2588"
|
|
2798
|
+
];
|
|
2799
|
+
function colorMantis(line) {
|
|
2800
|
+
const t = theme();
|
|
2801
|
+
return line.replace(/\u25c9/g, t.mascotAccent("\u25C9")).replace(/[\u2588]/g, (ch) => t.mascot(ch)).replace(/[\u2584\u2580]/g, (ch) => t.mascot(ch)).replace(/\u2590/g, t.mascot("\u2590")).replace(/\u258c/g, t.mascot("\u258C"));
|
|
2802
|
+
}
|
|
2803
|
+
function printBanner(version, modelLabel, modelId, modelSize, project) {
|
|
2804
|
+
const t = theme();
|
|
2805
|
+
const termWidth = process.stdout.columns || 80;
|
|
2806
|
+
console.log("");
|
|
2807
|
+
for (const line of MANTIS) {
|
|
2808
|
+
console.log(" " + colorMantis(line));
|
|
2809
|
+
}
|
|
2810
|
+
console.log("");
|
|
2811
|
+
for (const line of LOGO_INLINE) {
|
|
2812
|
+
console.log(" " + t.brand(line));
|
|
2813
|
+
}
|
|
2814
|
+
console.log("");
|
|
2815
|
+
const divWidth = Math.min(50, termWidth - 4);
|
|
2816
|
+
const divider = t.border(" " + "\u2500".repeat(divWidth));
|
|
2817
|
+
console.log(divider);
|
|
2818
|
+
console.log(
|
|
2819
|
+
t.dim(" ") + t.bold(modelLabel) + t.dim(` (${modelSize})`) + t.dim(" \u2502 v") + t.text(version) + t.dim(" \u2502 ") + t.dim("by ") + t.tagline("Driftrail")
|
|
2820
|
+
);
|
|
2821
|
+
console.log(t.dim(` ${project}`));
|
|
2822
|
+
console.log(divider);
|
|
2823
|
+
console.log("");
|
|
2824
|
+
}
|
|
2825
|
+
function printMantis() {
|
|
2826
|
+
const t = theme();
|
|
2827
|
+
console.log("");
|
|
2828
|
+
for (const line of MANTIS) {
|
|
2829
|
+
console.log(" " + colorMantis(line));
|
|
2830
|
+
}
|
|
2831
|
+
console.log("");
|
|
2832
|
+
console.log(t.dim(' "Precision. Patience. Execution."'));
|
|
2833
|
+
console.log("");
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// src/ui/context-meter.ts
|
|
2837
|
+
function renderContextMeter(usedTokens, maxTokens, width = 30) {
|
|
2838
|
+
const t = theme();
|
|
2839
|
+
const ratio = Math.min(usedTokens / maxTokens, 1);
|
|
2840
|
+
const percent = Math.round(ratio * 100);
|
|
2841
|
+
const filled = Math.round(ratio * width);
|
|
2842
|
+
const empty = width - filled;
|
|
2843
|
+
const barColor = ratio < 0.5 ? t.meterLow : ratio < 0.75 ? t.meterMid : t.meterHigh;
|
|
2844
|
+
const bar = barColor("\u2588".repeat(filled)) + t.border("\u2591".repeat(empty));
|
|
2845
|
+
const used = formatTokens(usedTokens);
|
|
2846
|
+
const max = formatTokens(maxTokens);
|
|
2847
|
+
return ` Context: [${bar}] ${percent}% (${used} / ${max} tokens)`;
|
|
2848
|
+
}
|
|
2849
|
+
function compactContextIndicator(usedTokens, maxTokens) {
|
|
2850
|
+
const t = theme();
|
|
2851
|
+
const ratio = Math.min(usedTokens / maxTokens, 1);
|
|
2852
|
+
const percent = Math.round(ratio * 100);
|
|
2853
|
+
if (ratio < 0.5) return t.meterLow(`[${percent}%]`);
|
|
2854
|
+
if (ratio < 0.75) return t.meterMid(`[${percent}%]`);
|
|
2855
|
+
return t.meterHigh(`[${percent}%]`);
|
|
2856
|
+
}
|
|
2857
|
+
function formatTokens(n) {
|
|
2858
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2859
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
2860
|
+
return String(n);
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/ui/update-checker.ts
|
|
2864
|
+
import fs12 from "fs/promises";
|
|
2865
|
+
import path13 from "path";
|
|
2866
|
+
import os3 from "os";
|
|
2867
|
+
import chalk6 from "chalk";
|
|
2868
|
+
var CACHE_FILE = path13.join(os3.homedir(), ".notch", "update-check.json");
|
|
2869
|
+
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
2870
|
+
var PACKAGE_NAME = "notch-cli";
|
|
2871
|
+
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
2872
|
+
async function checkForUpdates(currentVersion) {
|
|
2873
|
+
try {
|
|
2874
|
+
const cache = await loadCache();
|
|
2875
|
+
if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL) {
|
|
2876
|
+
if (cache.latestVersion && isNewer(cache.latestVersion, currentVersion)) {
|
|
2877
|
+
return formatUpdateMessage(currentVersion, cache.latestVersion);
|
|
2878
|
+
}
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
const response = await fetch(REGISTRY_URL, {
|
|
2882
|
+
signal: AbortSignal.timeout(3e3),
|
|
2883
|
+
// Fast timeout — don't slow down startup
|
|
2884
|
+
headers: { "Accept": "application/json" }
|
|
2885
|
+
});
|
|
2886
|
+
if (!response.ok) {
|
|
2887
|
+
await saveCache({ lastCheck: Date.now(), latestVersion: null });
|
|
2888
|
+
return null;
|
|
2889
|
+
}
|
|
2890
|
+
const data = await response.json();
|
|
2891
|
+
const latest = data.version;
|
|
2892
|
+
await saveCache({ lastCheck: Date.now(), latestVersion: latest });
|
|
2893
|
+
if (isNewer(latest, currentVersion)) {
|
|
2894
|
+
return formatUpdateMessage(currentVersion, latest);
|
|
2895
|
+
}
|
|
2896
|
+
return null;
|
|
2897
|
+
} catch {
|
|
2898
|
+
return null;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
function isNewer(latest, current) {
|
|
2902
|
+
const l = latest.split(".").map(Number);
|
|
2903
|
+
const c = current.split(".").map(Number);
|
|
2904
|
+
for (let i = 0; i < 3; i++) {
|
|
2905
|
+
if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
|
|
2906
|
+
if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
|
|
2907
|
+
}
|
|
2908
|
+
return false;
|
|
2909
|
+
}
|
|
2910
|
+
function formatUpdateMessage(current, latest) {
|
|
2911
|
+
return chalk6.yellow(` Update available: ${current} -> ${latest}. Run: npm update -g ${PACKAGE_NAME}
|
|
2912
|
+
`);
|
|
2913
|
+
}
|
|
2914
|
+
async function loadCache() {
|
|
2915
|
+
try {
|
|
2916
|
+
const raw = await fs12.readFile(CACHE_FILE, "utf-8");
|
|
2917
|
+
return JSON.parse(raw);
|
|
2918
|
+
} catch {
|
|
2919
|
+
return null;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
async function saveCache(cache) {
|
|
2923
|
+
try {
|
|
2924
|
+
await fs12.mkdir(path13.dirname(CACHE_FILE), { recursive: true });
|
|
2925
|
+
await fs12.writeFile(CACHE_FILE, JSON.stringify(cache), "utf-8");
|
|
2926
|
+
} catch {
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/permissions/index.ts
|
|
2931
|
+
import fs13 from "fs/promises";
|
|
2932
|
+
import path14 from "path";
|
|
2933
|
+
import os4 from "os";
|
|
2934
|
+
var DEFAULT_PERMISSIONS = {
|
|
2935
|
+
default: "prompt",
|
|
2936
|
+
rules: [
|
|
2937
|
+
// Safe read-only tools auto-allowed
|
|
2938
|
+
{ tool: "read", level: "allow" },
|
|
2939
|
+
{ tool: "grep", level: "allow" },
|
|
2940
|
+
{ tool: "glob", level: "allow" },
|
|
2941
|
+
{ tool: "web_fetch", level: "allow" },
|
|
2942
|
+
// Write tools prompt by default
|
|
2943
|
+
{ tool: "write", level: "prompt" },
|
|
2944
|
+
{ tool: "edit", level: "prompt" },
|
|
2945
|
+
{ tool: "shell", level: "prompt" },
|
|
2946
|
+
{ tool: "git", level: "prompt" }
|
|
2947
|
+
]
|
|
2948
|
+
};
|
|
2949
|
+
async function loadPermissions(projectRoot) {
|
|
2950
|
+
const projectPath = path14.join(projectRoot, ".notch.json");
|
|
2951
|
+
const globalPath = path14.join(os4.homedir(), ".notch", "permissions.json");
|
|
2952
|
+
let config = { ...DEFAULT_PERMISSIONS };
|
|
2953
|
+
try {
|
|
2954
|
+
const raw = await fs13.readFile(globalPath, "utf-8");
|
|
2955
|
+
const parsed = JSON.parse(raw);
|
|
2956
|
+
if (parsed.permissions) {
|
|
2957
|
+
config = mergePermissions(config, parsed.permissions);
|
|
2958
|
+
}
|
|
2959
|
+
} catch {
|
|
2960
|
+
}
|
|
2961
|
+
try {
|
|
2962
|
+
const raw = await fs13.readFile(projectPath, "utf-8");
|
|
2963
|
+
const parsed = JSON.parse(raw);
|
|
2964
|
+
if (parsed.permissions) {
|
|
2965
|
+
config = mergePermissions(config, parsed.permissions);
|
|
2966
|
+
}
|
|
2967
|
+
} catch {
|
|
2968
|
+
}
|
|
2969
|
+
return config;
|
|
2970
|
+
}
|
|
2971
|
+
function checkPermission(config, toolName, args) {
|
|
2972
|
+
const rule = config.rules.find((r) => {
|
|
2973
|
+
if (r.tool !== toolName) return false;
|
|
2974
|
+
if (r.pattern && args) {
|
|
2975
|
+
const pat = new RegExp(r.pattern);
|
|
2976
|
+
return Object.values(args).some((v) => pat.test(String(v)));
|
|
2977
|
+
}
|
|
2978
|
+
return true;
|
|
2979
|
+
});
|
|
2980
|
+
return rule?.level ?? config.default;
|
|
2981
|
+
}
|
|
2982
|
+
function formatPermissions(config) {
|
|
2983
|
+
const lines = [`Default: ${config.default}`, ""];
|
|
2984
|
+
for (const rule of config.rules) {
|
|
2985
|
+
const icon = rule.level === "allow" ? "\u2713" : rule.level === "deny" ? "\u2717" : "?";
|
|
2986
|
+
const pattern = rule.pattern ? ` (${rule.pattern})` : "";
|
|
2987
|
+
lines.push(` ${icon} ${rule.tool.padEnd(12)} ${rule.level}${pattern}`);
|
|
2988
|
+
}
|
|
2989
|
+
return lines.join("\n");
|
|
2990
|
+
}
|
|
2991
|
+
function mergePermissions(base, override) {
|
|
2992
|
+
const merged = { ...base };
|
|
2993
|
+
if (override.default) merged.default = override.default;
|
|
2994
|
+
if (override.rules) {
|
|
2995
|
+
for (const rule of override.rules) {
|
|
2996
|
+
const idx = merged.rules.findIndex((r) => r.tool === rule.tool && r.pattern === rule.pattern);
|
|
2997
|
+
if (idx >= 0) {
|
|
2998
|
+
merged.rules[idx] = rule;
|
|
2999
|
+
} else {
|
|
3000
|
+
merged.rules.push(rule);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
return merged;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// src/hooks/index.ts
|
|
3008
|
+
import { execSync as execSync3 } from "child_process";
|
|
3009
|
+
import fs14 from "fs/promises";
|
|
3010
|
+
import path15 from "path";
|
|
3011
|
+
import os5 from "os";
|
|
3012
|
+
import crypto from "crypto";
|
|
3013
|
+
var TRUST_STORE_PATH = path15.join(os5.homedir(), ".notch", "trusted-projects.json");
|
|
3014
|
+
async function isTrustedProject(projectRoot, raw) {
|
|
3015
|
+
const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
|
|
3016
|
+
const key = path15.resolve(projectRoot);
|
|
3017
|
+
try {
|
|
3018
|
+
const store = JSON.parse(await fs14.readFile(TRUST_STORE_PATH, "utf-8"));
|
|
3019
|
+
return store[key] === fingerprint;
|
|
3020
|
+
} catch {
|
|
3021
|
+
return false;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
async function trustProject(projectRoot, raw) {
|
|
3025
|
+
const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
|
|
3026
|
+
const key = path15.resolve(projectRoot);
|
|
3027
|
+
let store = {};
|
|
3028
|
+
try {
|
|
3029
|
+
store = JSON.parse(await fs14.readFile(TRUST_STORE_PATH, "utf-8"));
|
|
3030
|
+
} catch {
|
|
3031
|
+
}
|
|
3032
|
+
store[key] = fingerprint;
|
|
3033
|
+
await fs14.mkdir(path15.dirname(TRUST_STORE_PATH), { recursive: true });
|
|
3034
|
+
await fs14.writeFile(TRUST_STORE_PATH, JSON.stringify(store, null, 2));
|
|
3035
|
+
}
|
|
3036
|
+
async function loadHooks(projectRoot, promptTrust) {
|
|
3037
|
+
const hooks = [];
|
|
3038
|
+
const globalPath = path15.join(os5.homedir(), ".notch", "hooks.json");
|
|
3039
|
+
try {
|
|
3040
|
+
const raw = await fs14.readFile(globalPath, "utf-8");
|
|
3041
|
+
const parsed = JSON.parse(raw);
|
|
3042
|
+
if (Array.isArray(parsed.hooks)) {
|
|
3043
|
+
hooks.push(...parsed.hooks);
|
|
3044
|
+
}
|
|
3045
|
+
} catch {
|
|
3046
|
+
}
|
|
3047
|
+
const projectPath = path15.join(projectRoot, ".notch.json");
|
|
3048
|
+
try {
|
|
3049
|
+
const raw = await fs14.readFile(projectPath, "utf-8");
|
|
3050
|
+
const parsed = JSON.parse(raw);
|
|
3051
|
+
if (Array.isArray(parsed.hooks) && parsed.hooks.length > 0) {
|
|
3052
|
+
const alreadyTrusted = await isTrustedProject(projectRoot, raw);
|
|
3053
|
+
if (!alreadyTrusted) {
|
|
3054
|
+
const commands = parsed.hooks.map((h) => h.command);
|
|
3055
|
+
const allowed = promptTrust ? await promptTrust(commands) : false;
|
|
3056
|
+
if (!allowed) {
|
|
3057
|
+
console.warn("[notch] Project hooks skipped \u2014 not trusted. Run with --trust-hooks to allow.");
|
|
3058
|
+
} else {
|
|
3059
|
+
await trustProject(projectRoot, raw);
|
|
3060
|
+
hooks.push(...parsed.hooks);
|
|
3061
|
+
}
|
|
3062
|
+
} else {
|
|
3063
|
+
hooks.push(...parsed.hooks);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
} catch {
|
|
3067
|
+
}
|
|
3068
|
+
return { hooks };
|
|
3069
|
+
}
|
|
3070
|
+
async function runHooks(config, event, context) {
|
|
3071
|
+
const matching = config.hooks.filter((h) => {
|
|
3072
|
+
if (h.event !== event) return false;
|
|
3073
|
+
if (h.tool && h.tool !== context.tool) return false;
|
|
3074
|
+
if (h.pattern && context.file && !context.file.includes(h.pattern)) return false;
|
|
3075
|
+
return true;
|
|
3076
|
+
});
|
|
3077
|
+
const results = [];
|
|
3078
|
+
for (const hook of matching) {
|
|
3079
|
+
const result = await executeHook(hook, context);
|
|
3080
|
+
results.push(result);
|
|
3081
|
+
if (!result.ok) break;
|
|
3082
|
+
}
|
|
3083
|
+
return { results };
|
|
3084
|
+
}
|
|
3085
|
+
async function executeHook(hook, context) {
|
|
3086
|
+
const BLOCKED_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
3087
|
+
"NOTCH_API_KEY",
|
|
3088
|
+
"NOTCH_BASE_URL",
|
|
3089
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
3090
|
+
"SUPABASE_ANON_KEY",
|
|
3091
|
+
"OPENAI_API_KEY",
|
|
3092
|
+
"ANTHROPIC_API_KEY",
|
|
3093
|
+
"GOOGLE_API_KEY",
|
|
3094
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
3095
|
+
"AWS_SESSION_TOKEN",
|
|
3096
|
+
"STRIPE_SECRET_KEY",
|
|
3097
|
+
"STRIPE_WEBHOOK_SECRET"
|
|
3098
|
+
]);
|
|
3099
|
+
const safeEnv = {};
|
|
3100
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
3101
|
+
if (v !== void 0 && !BLOCKED_ENV_KEYS.has(k)) {
|
|
3102
|
+
safeEnv[k] = v;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
const env = {
|
|
3106
|
+
...safeEnv,
|
|
3107
|
+
NOTCH_EVENT: hook.event,
|
|
3108
|
+
NOTCH_TOOL: context.tool ?? "",
|
|
3109
|
+
NOTCH_FILE: context.file ?? "",
|
|
3110
|
+
NOTCH_ERROR: context.error ?? "",
|
|
3111
|
+
NOTCH_CWD: context.cwd
|
|
3112
|
+
};
|
|
3113
|
+
try {
|
|
3114
|
+
const output = execSync3(hook.command, {
|
|
3115
|
+
cwd: context.cwd,
|
|
3116
|
+
encoding: "utf-8",
|
|
3117
|
+
timeout: hook.timeout ?? 1e4,
|
|
3118
|
+
env,
|
|
3119
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3120
|
+
});
|
|
3121
|
+
return { hook, ok: true, output: output.trim() };
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
return {
|
|
3124
|
+
hook,
|
|
3125
|
+
ok: false,
|
|
3126
|
+
output: err.stdout?.toString() ?? "",
|
|
3127
|
+
error: err.stderr?.toString() ?? err.message
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
// src/session/index.ts
|
|
3133
|
+
import fs15 from "fs/promises";
|
|
3134
|
+
import path16 from "path";
|
|
3135
|
+
import os6 from "os";
|
|
3136
|
+
var SESSION_DIR = path16.join(os6.homedir(), ".notch", "sessions");
|
|
3137
|
+
var MAX_SESSIONS = 20;
|
|
3138
|
+
async function ensureDir2() {
|
|
3139
|
+
await fs15.mkdir(SESSION_DIR, { recursive: true });
|
|
3140
|
+
}
|
|
3141
|
+
function sessionPath(id) {
|
|
3142
|
+
return path16.join(SESSION_DIR, `${id}.json`);
|
|
3143
|
+
}
|
|
3144
|
+
function generateId() {
|
|
3145
|
+
const now = /* @__PURE__ */ new Date();
|
|
3146
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
3147
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
3148
|
+
return `${date}-${rand}`;
|
|
3149
|
+
}
|
|
3150
|
+
async function saveSession(messages, project, model, existingId) {
|
|
3151
|
+
await ensureDir2();
|
|
3152
|
+
const id = existingId ?? generateId();
|
|
3153
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3154
|
+
const firstUser = messages.find((m) => m.role === "user");
|
|
3155
|
+
const preview = firstUser && typeof firstUser.content === "string" ? firstUser.content.slice(0, 80) : "(no preview)";
|
|
3156
|
+
const userCount = messages.filter((m) => m.role === "user").length;
|
|
3157
|
+
const session = {
|
|
3158
|
+
meta: {
|
|
3159
|
+
id,
|
|
3160
|
+
project,
|
|
3161
|
+
model,
|
|
3162
|
+
created: existingId ? (await loadSession(existingId))?.meta.created ?? now : now,
|
|
3163
|
+
updated: now,
|
|
3164
|
+
turns: userCount,
|
|
3165
|
+
preview
|
|
3166
|
+
},
|
|
3167
|
+
messages
|
|
3168
|
+
};
|
|
3169
|
+
await fs15.writeFile(sessionPath(id), JSON.stringify(session, null, 2), "utf-8");
|
|
3170
|
+
await pruneOldSessions();
|
|
3171
|
+
return id;
|
|
3172
|
+
}
|
|
3173
|
+
async function loadSession(id) {
|
|
3174
|
+
try {
|
|
3175
|
+
const raw = await fs15.readFile(sessionPath(id), "utf-8");
|
|
3176
|
+
return JSON.parse(raw);
|
|
3177
|
+
} catch {
|
|
3178
|
+
return null;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
async function listSessions() {
|
|
3182
|
+
await ensureDir2();
|
|
3183
|
+
const files = await fs15.readdir(SESSION_DIR);
|
|
3184
|
+
const sessions = [];
|
|
3185
|
+
for (const file of files) {
|
|
3186
|
+
if (!file.endsWith(".json")) continue;
|
|
3187
|
+
try {
|
|
3188
|
+
const raw = await fs15.readFile(path16.join(SESSION_DIR, file), "utf-8");
|
|
3189
|
+
const session = JSON.parse(raw);
|
|
3190
|
+
sessions.push(session.meta);
|
|
3191
|
+
} catch {
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return sessions.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
3195
|
+
}
|
|
3196
|
+
async function loadLastSession(project) {
|
|
3197
|
+
const sessions = await listSessions();
|
|
3198
|
+
const match = sessions.find((s) => s.project === project);
|
|
3199
|
+
if (!match) return null;
|
|
3200
|
+
return loadSession(match.id);
|
|
3201
|
+
}
|
|
3202
|
+
async function deleteSession(id) {
|
|
3203
|
+
try {
|
|
3204
|
+
await fs15.unlink(sessionPath(id));
|
|
3205
|
+
return true;
|
|
3206
|
+
} catch {
|
|
3207
|
+
return false;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
async function pruneOldSessions() {
|
|
3211
|
+
const sessions = await listSessions();
|
|
3212
|
+
if (sessions.length <= MAX_SESSIONS) return;
|
|
3213
|
+
const toDelete = sessions.slice(MAX_SESSIONS);
|
|
3214
|
+
for (const s of toDelete) {
|
|
3215
|
+
await deleteSession(s.id);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async function exportSession(messages, outputPath, meta) {
|
|
3219
|
+
const lines = [];
|
|
3220
|
+
lines.push("# Notch Conversation Export");
|
|
3221
|
+
lines.push("");
|
|
3222
|
+
if (meta) {
|
|
3223
|
+
lines.push(`- **Model**: ${meta.model}`);
|
|
3224
|
+
lines.push(`- **Project**: ${meta.project}`);
|
|
3225
|
+
}
|
|
3226
|
+
lines.push(`- **Date**: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
3227
|
+
lines.push("");
|
|
3228
|
+
lines.push("---");
|
|
3229
|
+
lines.push("");
|
|
3230
|
+
for (const msg of messages) {
|
|
3231
|
+
if (msg.role === "user") {
|
|
3232
|
+
lines.push("## User");
|
|
3233
|
+
lines.push("");
|
|
3234
|
+
if (typeof msg.content === "string") {
|
|
3235
|
+
lines.push(msg.content);
|
|
3236
|
+
}
|
|
3237
|
+
lines.push("");
|
|
3238
|
+
} else if (msg.role === "assistant") {
|
|
3239
|
+
lines.push("## Notch");
|
|
3240
|
+
lines.push("");
|
|
3241
|
+
if (typeof msg.content === "string") {
|
|
3242
|
+
lines.push(msg.content);
|
|
3243
|
+
} else if (Array.isArray(msg.content)) {
|
|
3244
|
+
for (const part of msg.content) {
|
|
3245
|
+
if ("text" in part) {
|
|
3246
|
+
lines.push(part.text);
|
|
3247
|
+
} else if ("toolName" in part) {
|
|
3248
|
+
lines.push(`> Tool call: \`${part.toolName}\``);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
lines.push("");
|
|
3253
|
+
} else if (msg.role === "tool") {
|
|
3254
|
+
lines.push("> *Tool results omitted for brevity*");
|
|
3255
|
+
lines.push("");
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
await fs15.writeFile(outputPath, lines.join("\n"), "utf-8");
|
|
3259
|
+
return outputPath;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// src/init.ts
|
|
3263
|
+
import fs16 from "fs/promises";
|
|
3264
|
+
import path17 from "path";
|
|
3265
|
+
import chalk7 from "chalk";
|
|
3266
|
+
var DEFAULT_CONFIG = {
|
|
3267
|
+
model: "notch-forge",
|
|
3268
|
+
temperature: 0.3,
|
|
3269
|
+
maxIterations: 25,
|
|
3270
|
+
useRepoMap: true,
|
|
3271
|
+
renderMarkdown: true,
|
|
3272
|
+
permissionMode: "auto"
|
|
3273
|
+
};
|
|
3274
|
+
var DEFAULT_INSTRUCTIONS = `# Project Instructions for Notch
|
|
3275
|
+
|
|
3276
|
+
<!--
|
|
3277
|
+
These instructions are loaded into Notch's system prompt every session.
|
|
3278
|
+
Use them to customize how Notch behaves in this project.
|
|
3279
|
+
-->
|
|
3280
|
+
|
|
3281
|
+
## Project Overview
|
|
3282
|
+
<!-- Describe what this project is and what it does -->
|
|
3283
|
+
|
|
3284
|
+
## Code Conventions
|
|
3285
|
+
<!-- Add any coding style preferences, naming conventions, etc. -->
|
|
3286
|
+
- Follow existing patterns in the codebase
|
|
3287
|
+
- Use TypeScript strict mode
|
|
3288
|
+
|
|
3289
|
+
## Important Notes
|
|
3290
|
+
<!-- Any gotchas, special setup, or things Notch should know -->
|
|
3291
|
+
|
|
3292
|
+
## Off-Limits
|
|
3293
|
+
<!-- Files or areas Notch should NOT modify -->
|
|
3294
|
+
`;
|
|
3295
|
+
async function initProject(projectRoot) {
|
|
3296
|
+
const configPath = path17.join(projectRoot, ".notch.json");
|
|
3297
|
+
const instructionsPath = path17.join(projectRoot, ".notch.md");
|
|
3298
|
+
let configExists = false;
|
|
3299
|
+
let instructionsExist = false;
|
|
3300
|
+
try {
|
|
3301
|
+
await fs16.access(configPath);
|
|
3302
|
+
configExists = true;
|
|
3303
|
+
} catch {
|
|
3304
|
+
}
|
|
3305
|
+
try {
|
|
3306
|
+
await fs16.access(instructionsPath);
|
|
3307
|
+
instructionsExist = true;
|
|
3308
|
+
} catch {
|
|
3309
|
+
}
|
|
3310
|
+
if (!configExists) {
|
|
3311
|
+
await fs16.writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf-8");
|
|
3312
|
+
console.log(chalk7.green(` Created ${configPath}`));
|
|
3313
|
+
} else {
|
|
3314
|
+
console.log(chalk7.gray(` Skipped ${configPath} (already exists)`));
|
|
3315
|
+
}
|
|
3316
|
+
if (!instructionsExist) {
|
|
3317
|
+
await fs16.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS, "utf-8");
|
|
3318
|
+
console.log(chalk7.green(` Created ${instructionsPath}`));
|
|
3319
|
+
} else {
|
|
3320
|
+
console.log(chalk7.gray(` Skipped ${instructionsPath} (already exists)`));
|
|
3321
|
+
}
|
|
3322
|
+
const gitignorePath = path17.join(projectRoot, ".gitignore");
|
|
3323
|
+
try {
|
|
3324
|
+
const gitignore = await fs16.readFile(gitignorePath, "utf-8");
|
|
3325
|
+
const additions = [];
|
|
3326
|
+
if (!gitignore.includes(".notch.json")) additions.push(".notch.json");
|
|
3327
|
+
if (additions.length > 0) {
|
|
3328
|
+
const append = "\n# Notch CLI\n" + additions.join("\n") + "\n";
|
|
3329
|
+
await fs16.appendFile(gitignorePath, append, "utf-8");
|
|
3330
|
+
console.log(chalk7.green(` Updated .gitignore`));
|
|
3331
|
+
}
|
|
3332
|
+
} catch {
|
|
3333
|
+
}
|
|
3334
|
+
console.log("");
|
|
3335
|
+
console.log(chalk7.cyan(" Notch initialized! Edit .notch.md to customize behavior."));
|
|
3336
|
+
console.log(chalk7.gray(' Run "notch" to start.\n'));
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
// src/tools/diff-preview.ts
|
|
3340
|
+
function unifiedDiff(oldContent, newContent, filePath) {
|
|
3341
|
+
const t = theme();
|
|
3342
|
+
const oldLines = oldContent.split("\n");
|
|
3343
|
+
const newLines = newContent.split("\n");
|
|
3344
|
+
const hunks = computeHunks(oldLines, newLines);
|
|
3345
|
+
if (hunks.length === 0) return "(no changes)";
|
|
3346
|
+
const output = [
|
|
3347
|
+
t.diffHeader(`--- a/${filePath}`),
|
|
3348
|
+
t.diffHeader(`+++ b/${filePath}`)
|
|
3349
|
+
];
|
|
3350
|
+
for (const hunk of hunks) {
|
|
3351
|
+
output.push(t.diffHeader(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`));
|
|
3352
|
+
for (const line of hunk.lines) {
|
|
3353
|
+
if (line.startsWith("+")) {
|
|
3354
|
+
output.push(t.diffAdd(line));
|
|
3355
|
+
} else if (line.startsWith("-")) {
|
|
3356
|
+
output.push(t.diffRemove(line));
|
|
3357
|
+
} else {
|
|
3358
|
+
output.push(t.dim(line));
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
return output.join("\n");
|
|
3363
|
+
}
|
|
3364
|
+
function computeHunks(oldLines, newLines, context = 3) {
|
|
3365
|
+
const changes = [];
|
|
3366
|
+
let oi = 0;
|
|
3367
|
+
let ni = 0;
|
|
3368
|
+
while (oi < oldLines.length || ni < newLines.length) {
|
|
3369
|
+
if (oi < oldLines.length && ni < newLines.length && oldLines[oi] === newLines[ni]) {
|
|
3370
|
+
changes.push({ type: "same", oldIdx: oi, newIdx: ni, line: oldLines[oi] });
|
|
3371
|
+
oi++;
|
|
3372
|
+
ni++;
|
|
3373
|
+
} else {
|
|
3374
|
+
const syncPoint = findSync(oldLines, newLines, oi, ni, 10);
|
|
3375
|
+
if (syncPoint) {
|
|
3376
|
+
while (oi < syncPoint.oi) {
|
|
3377
|
+
changes.push({ type: "remove", oldIdx: oi, newIdx: ni, line: oldLines[oi] });
|
|
3378
|
+
oi++;
|
|
3379
|
+
}
|
|
3380
|
+
while (ni < syncPoint.ni) {
|
|
3381
|
+
changes.push({ type: "add", oldIdx: oi, newIdx: ni, line: newLines[ni] });
|
|
3382
|
+
ni++;
|
|
3383
|
+
}
|
|
3384
|
+
} else {
|
|
3385
|
+
while (oi < oldLines.length) {
|
|
3386
|
+
changes.push({ type: "remove", oldIdx: oi, newIdx: ni, line: oldLines[oi] });
|
|
3387
|
+
oi++;
|
|
3388
|
+
}
|
|
3389
|
+
while (ni < newLines.length) {
|
|
3390
|
+
changes.push({ type: "add", oldIdx: oi, newIdx: ni, line: newLines[ni] });
|
|
3391
|
+
ni++;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
const hunks = [];
|
|
3397
|
+
let currentHunk = null;
|
|
3398
|
+
let contextRemaining = 0;
|
|
3399
|
+
for (let i = 0; i < changes.length; i++) {
|
|
3400
|
+
const c = changes[i];
|
|
3401
|
+
if (c.type !== "same") {
|
|
3402
|
+
if (!currentHunk) {
|
|
3403
|
+
const start = Math.max(0, i - context);
|
|
3404
|
+
currentHunk = {
|
|
3405
|
+
oldStart: changes[start].oldIdx + 1,
|
|
3406
|
+
oldCount: 0,
|
|
3407
|
+
newStart: changes[start].newIdx + 1,
|
|
3408
|
+
newCount: 0,
|
|
3409
|
+
lines: []
|
|
3410
|
+
};
|
|
3411
|
+
for (let j = start; j < i; j++) {
|
|
3412
|
+
currentHunk.lines.push(` ${changes[j].line}`);
|
|
3413
|
+
currentHunk.oldCount++;
|
|
3414
|
+
currentHunk.newCount++;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
if (c.type === "remove") {
|
|
3418
|
+
currentHunk.lines.push(`-${c.line}`);
|
|
3419
|
+
currentHunk.oldCount++;
|
|
3420
|
+
} else {
|
|
3421
|
+
currentHunk.lines.push(`+${c.line}`);
|
|
3422
|
+
currentHunk.newCount++;
|
|
3423
|
+
}
|
|
3424
|
+
contextRemaining = context;
|
|
3425
|
+
} else if (currentHunk) {
|
|
3426
|
+
if (contextRemaining > 0) {
|
|
3427
|
+
currentHunk.lines.push(` ${c.line}`);
|
|
3428
|
+
currentHunk.oldCount++;
|
|
3429
|
+
currentHunk.newCount++;
|
|
3430
|
+
contextRemaining--;
|
|
3431
|
+
} else {
|
|
3432
|
+
hunks.push(currentHunk);
|
|
3433
|
+
currentHunk = null;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
3438
|
+
return hunks;
|
|
3439
|
+
}
|
|
3440
|
+
function findSync(oldLines, newLines, oi, ni, lookAhead) {
|
|
3441
|
+
for (let d = 1; d <= lookAhead; d++) {
|
|
3442
|
+
if (oi + d < oldLines.length && ni < newLines.length && oldLines[oi + d] === newLines[ni]) {
|
|
3443
|
+
return { oi: oi + d, ni };
|
|
3444
|
+
}
|
|
3445
|
+
if (oi < oldLines.length && ni + d < newLines.length && oldLines[oi] === newLines[ni + d]) {
|
|
3446
|
+
return { oi, ni: ni + d };
|
|
3447
|
+
}
|
|
3448
|
+
if (oi + d < oldLines.length && ni + d < newLines.length && oldLines[oi + d] === newLines[ni + d]) {
|
|
3449
|
+
return { oi: oi + d, ni: ni + d };
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
return null;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
// src/mcp/client.ts
|
|
3456
|
+
import { spawn } from "child_process";
|
|
3457
|
+
import { z as z9 } from "zod";
|
|
3458
|
+
var MCPClient = class {
|
|
3459
|
+
constructor(config, serverName) {
|
|
3460
|
+
this.config = config;
|
|
3461
|
+
this.serverName = serverName;
|
|
3462
|
+
}
|
|
3463
|
+
process = null;
|
|
3464
|
+
requestId = 0;
|
|
3465
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
3466
|
+
buffer = "";
|
|
3467
|
+
serverName;
|
|
3468
|
+
_tools = [];
|
|
3469
|
+
/**
|
|
3470
|
+
* Start the MCP server and initialize the connection.
|
|
3471
|
+
*/
|
|
3472
|
+
async connect() {
|
|
3473
|
+
this.process = spawn(this.config.command, this.config.args ?? [], {
|
|
3474
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3475
|
+
env: { ...process.env, ...this.config.env },
|
|
3476
|
+
cwd: this.config.cwd
|
|
3477
|
+
});
|
|
3478
|
+
this.process.stdout?.setEncoding("utf-8");
|
|
3479
|
+
this.process.stdout?.on("data", (data) => {
|
|
3480
|
+
this.buffer += data;
|
|
3481
|
+
this.processBuffer();
|
|
3482
|
+
});
|
|
3483
|
+
this.process.on("error", (err) => {
|
|
3484
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
3485
|
+
pending.reject(new Error(`MCP server ${this.serverName} error: ${err.message}`));
|
|
3486
|
+
this.pendingRequests.delete(id);
|
|
3487
|
+
}
|
|
3488
|
+
});
|
|
3489
|
+
this.process.on("exit", (code) => {
|
|
3490
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
3491
|
+
pending.reject(new Error(`MCP server ${this.serverName} exited with code ${code}`));
|
|
3492
|
+
this.pendingRequests.delete(id);
|
|
3493
|
+
}
|
|
3494
|
+
});
|
|
3495
|
+
await this.sendRequest("initialize", {
|
|
3496
|
+
protocolVersion: "2024-11-05",
|
|
3497
|
+
capabilities: {},
|
|
3498
|
+
clientInfo: { name: "notch-cli", version: "0.3.0" }
|
|
3499
|
+
});
|
|
3500
|
+
this.sendNotification("notifications/initialized", {});
|
|
3501
|
+
const result = await this.sendRequest("tools/list", {});
|
|
3502
|
+
this._tools = result.tools ?? [];
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Get discovered tools from this server.
|
|
3506
|
+
*/
|
|
3507
|
+
get tools() {
|
|
3508
|
+
return this._tools;
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* Check if the MCP server process is still alive.
|
|
3512
|
+
*/
|
|
3513
|
+
get isAlive() {
|
|
3514
|
+
return this.process !== null && this.process.exitCode === null && !this.process.killed;
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Call a tool on the MCP server. Auto-reconnects if the server has crashed.
|
|
3518
|
+
*/
|
|
3519
|
+
async callTool(name, args) {
|
|
3520
|
+
if (!this.isAlive) {
|
|
3521
|
+
try {
|
|
3522
|
+
await this.connect();
|
|
3523
|
+
} catch (err) {
|
|
3524
|
+
throw new Error(`MCP server ${this.serverName} is down and could not reconnect: ${err.message}`);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
const result = await this.sendRequest("tools/call", { name, arguments: args });
|
|
3528
|
+
return result;
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Disconnect from the MCP server.
|
|
3532
|
+
*/
|
|
3533
|
+
disconnect() {
|
|
3534
|
+
if (this.process) {
|
|
3535
|
+
this.process.stdin?.end();
|
|
3536
|
+
this.process.kill();
|
|
3537
|
+
this.process = null;
|
|
3538
|
+
}
|
|
3539
|
+
this.pendingRequests.clear();
|
|
3540
|
+
}
|
|
3541
|
+
sendRequest(method, params) {
|
|
3542
|
+
return new Promise((resolve2, reject) => {
|
|
3543
|
+
const id = ++this.requestId;
|
|
3544
|
+
const msg = {
|
|
3545
|
+
jsonrpc: "2.0",
|
|
3546
|
+
id,
|
|
3547
|
+
method,
|
|
3548
|
+
params
|
|
3549
|
+
};
|
|
3550
|
+
this.pendingRequests.set(id, { resolve: resolve2, reject });
|
|
3551
|
+
const data = JSON.stringify(msg);
|
|
3552
|
+
const header = `Content-Length: ${Buffer.byteLength(data)}\r
|
|
3553
|
+
\r
|
|
3554
|
+
`;
|
|
3555
|
+
this.process?.stdin?.write(header + data);
|
|
3556
|
+
setTimeout(() => {
|
|
3557
|
+
if (this.pendingRequests.has(id)) {
|
|
3558
|
+
this.pendingRequests.delete(id);
|
|
3559
|
+
reject(new Error(`MCP request ${method} timed out`));
|
|
3560
|
+
}
|
|
3561
|
+
}, 3e4);
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
sendNotification(method, params) {
|
|
3565
|
+
const msg = {
|
|
3566
|
+
jsonrpc: "2.0",
|
|
3567
|
+
method,
|
|
3568
|
+
params
|
|
3569
|
+
};
|
|
3570
|
+
const data = JSON.stringify(msg);
|
|
3571
|
+
const header = `Content-Length: ${Buffer.byteLength(data)}\r
|
|
3572
|
+
\r
|
|
3573
|
+
`;
|
|
3574
|
+
this.process?.stdin?.write(header + data);
|
|
3575
|
+
}
|
|
3576
|
+
processBuffer() {
|
|
3577
|
+
while (this.buffer.length > 0) {
|
|
3578
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
3579
|
+
if (headerEnd === -1) break;
|
|
3580
|
+
const header = this.buffer.slice(0, headerEnd);
|
|
3581
|
+
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
3582
|
+
if (!lengthMatch) {
|
|
3583
|
+
const nlIdx = this.buffer.indexOf("\n");
|
|
3584
|
+
if (nlIdx === -1) break;
|
|
3585
|
+
const line = this.buffer.slice(0, nlIdx).trim();
|
|
3586
|
+
this.buffer = this.buffer.slice(nlIdx + 1);
|
|
3587
|
+
if (line) this.handleMessage(line);
|
|
3588
|
+
continue;
|
|
3589
|
+
}
|
|
3590
|
+
const contentLength = parseInt(lengthMatch[1], 10);
|
|
3591
|
+
const messageStart = headerEnd + 4;
|
|
3592
|
+
if (this.buffer.length < messageStart + contentLength) break;
|
|
3593
|
+
const body = this.buffer.slice(messageStart, messageStart + contentLength);
|
|
3594
|
+
this.buffer = this.buffer.slice(messageStart + contentLength);
|
|
3595
|
+
this.handleMessage(body);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
handleMessage(raw) {
|
|
3599
|
+
try {
|
|
3600
|
+
const msg = JSON.parse(raw);
|
|
3601
|
+
if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
|
|
3602
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
3603
|
+
this.pendingRequests.delete(msg.id);
|
|
3604
|
+
if (msg.error) {
|
|
3605
|
+
pending.reject(new Error(`MCP error: ${msg.error.message}`));
|
|
3606
|
+
} else {
|
|
3607
|
+
pending.resolve(msg.result);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
} catch {
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
3614
|
+
function parseMCPConfig(config) {
|
|
3615
|
+
const servers = config?.mcpServers;
|
|
3616
|
+
if (!servers || typeof servers !== "object") return {};
|
|
3617
|
+
const result = {};
|
|
3618
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
3619
|
+
const c = cfg;
|
|
3620
|
+
if (c?.command) {
|
|
3621
|
+
result[name] = {
|
|
3622
|
+
command: c.command,
|
|
3623
|
+
args: c.args,
|
|
3624
|
+
env: c.env,
|
|
3625
|
+
cwd: c.cwd
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
return result;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
// src/ui/completions.ts
|
|
3633
|
+
import fs17 from "fs";
|
|
3634
|
+
import path18 from "path";
|
|
3635
|
+
var COMMANDS = [
|
|
3636
|
+
"/help",
|
|
3637
|
+
"/quit",
|
|
3638
|
+
"/exit",
|
|
3639
|
+
"/q",
|
|
3640
|
+
"/clear",
|
|
3641
|
+
"/model",
|
|
3642
|
+
"/models",
|
|
3643
|
+
"/status",
|
|
3644
|
+
"/undo",
|
|
3645
|
+
"/usage",
|
|
3646
|
+
"/compact",
|
|
3647
|
+
"/diff",
|
|
3648
|
+
"/export",
|
|
3649
|
+
"/save",
|
|
3650
|
+
"/sessions",
|
|
3651
|
+
"/resume",
|
|
3652
|
+
"/search",
|
|
3653
|
+
"/memory",
|
|
3654
|
+
"/permissions",
|
|
3655
|
+
"/plan",
|
|
3656
|
+
"/plan approve",
|
|
3657
|
+
"/plan cancel",
|
|
3658
|
+
"/plan edit",
|
|
3659
|
+
"/theme",
|
|
3660
|
+
"/themes",
|
|
3661
|
+
"/mascot",
|
|
3662
|
+
"/cost",
|
|
3663
|
+
"/branch",
|
|
3664
|
+
"/branches"
|
|
3665
|
+
];
|
|
3666
|
+
function buildCompleter(cwd) {
|
|
3667
|
+
return function completer(line) {
|
|
3668
|
+
if (line.startsWith("/")) {
|
|
3669
|
+
const matches = COMMANDS.filter((c) => c.startsWith(line));
|
|
3670
|
+
return [matches.length > 0 ? matches : COMMANDS, line];
|
|
3671
|
+
}
|
|
3672
|
+
if (line.startsWith("/model ")) {
|
|
3673
|
+
const partial = line.slice(7);
|
|
3674
|
+
const modelNames = MODEL_IDS.map((id) => id.replace("notch-", ""));
|
|
3675
|
+
const allNames = [...MODEL_IDS, ...modelNames];
|
|
3676
|
+
const matches = allNames.filter((m) => m.startsWith(partial));
|
|
3677
|
+
return [matches.map((m) => `/model ${m}`), line];
|
|
3678
|
+
}
|
|
3679
|
+
if (line.startsWith("/theme ")) {
|
|
3680
|
+
const partial = line.slice(7);
|
|
3681
|
+
const matches = THEME_IDS.filter((t) => t.startsWith(partial));
|
|
3682
|
+
return [matches.map((t) => `/theme ${t}`), line];
|
|
3683
|
+
}
|
|
3684
|
+
const atMatch = line.match(/@([^\s]*)$/);
|
|
3685
|
+
if (atMatch) {
|
|
3686
|
+
const partial = atMatch[1];
|
|
3687
|
+
const completions = completeFilePath(partial, cwd);
|
|
3688
|
+
return [completions.map((c) => line.slice(0, -partial.length) + c), line];
|
|
3689
|
+
}
|
|
3690
|
+
return [[], line];
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
function completeFilePath(partial, cwd) {
|
|
3694
|
+
try {
|
|
3695
|
+
const dir = partial.includes("/") ? path18.resolve(cwd, path18.dirname(partial)) : cwd;
|
|
3696
|
+
const prefix = partial.includes("/") ? path18.basename(partial) : partial;
|
|
3697
|
+
const entries = fs17.readdirSync(dir, { withFileTypes: true });
|
|
3698
|
+
const matches = [];
|
|
3699
|
+
for (const entry of entries) {
|
|
3700
|
+
if (entry.name.startsWith(".")) continue;
|
|
3701
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
3702
|
+
if (entry.name.startsWith(prefix)) {
|
|
3703
|
+
const relative = partial.includes("/") ? path18.dirname(partial) + "/" + entry.name : entry.name;
|
|
3704
|
+
if (entry.isDirectory()) {
|
|
3705
|
+
matches.push(relative + "/");
|
|
3706
|
+
} else {
|
|
3707
|
+
matches.push(relative);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
return matches.slice(0, 20);
|
|
3712
|
+
} catch {
|
|
3713
|
+
return [];
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
// src/index.ts
|
|
3718
|
+
import fs18 from "fs/promises";
|
|
3719
|
+
var VERSION = "0.4.0";
|
|
3720
|
+
var modelChoices = MODEL_IDS.join(", ");
|
|
3721
|
+
var program = new Command().name("notch").description("Notch CLI \u2014 AI-powered coding assistant by Driftrail").version(VERSION).argument("[prompt...]", "One-shot prompt (runs once and exits)").option(`-m, --model <model>`, `Notch model (${modelChoices})`).option("--base-url <url>", "Override Notch API base URL").option("--api-key <key>", "Notch API key (prefer NOTCH_API_KEY env var)").option("--no-repo-map", "Disable automatic repository mapping").option("--no-markdown", "Disable markdown rendering in output").option("--max-iterations <n>", "Max tool-call rounds per turn", "25").option("-y, --yes", "Auto-confirm destructive actions").option("--trust", "Trust mode \u2014 auto-allow all tool calls").option("--theme <theme>", `UI color theme (${THEME_IDS.join(", ")})`).option("--resume", "Resume the last session for this project").option("--session <id>", "Resume a specific session by ID").option("--cwd <dir>", "Set working directory").parse(process.argv);
|
|
3722
|
+
var opts = program.opts();
|
|
3723
|
+
var promptArgs = program.args;
|
|
3724
|
+
function printModelTable(activeModel) {
|
|
3725
|
+
const t = theme();
|
|
3726
|
+
console.log(t.dim("\n Available models:\n"));
|
|
3727
|
+
for (const id of MODEL_IDS) {
|
|
3728
|
+
const info = MODEL_CATALOG[id];
|
|
3729
|
+
const active = id === activeModel ? t.success(" \u25CF") : " ";
|
|
3730
|
+
const label = id === activeModel ? t.bold(`${info.label}`) : t.dim(`${info.label}`);
|
|
3731
|
+
const size = t.info(info.size.padEnd(4));
|
|
3732
|
+
const ctx = t.dim(`${(info.contextWindow / 1024).toFixed(0)}K ctx`);
|
|
3733
|
+
console.log(` ${active} ${t.brand(id.padEnd(14))} ${size} ${label} ${ctx}`);
|
|
3734
|
+
}
|
|
3735
|
+
console.log(t.dim(`
|
|
3736
|
+
Switch with: /model <name>
|
|
3737
|
+
`));
|
|
3738
|
+
}
|
|
3739
|
+
function printHelp() {
|
|
3740
|
+
console.log(chalk8.gray(`
|
|
3741
|
+
Commands:
|
|
3742
|
+
/model \u2014 Show available models
|
|
3743
|
+
/model <name> \u2014 Switch model (e.g., /model pyre)
|
|
3744
|
+
/status \u2014 Check Notch API endpoint health
|
|
3745
|
+
/undo \u2014 Undo last file changes
|
|
3746
|
+
/usage \u2014 Show token usage + context meter
|
|
3747
|
+
/cost \u2014 Show estimated session cost
|
|
3748
|
+
/compact \u2014 Compress conversation history
|
|
3749
|
+
/diff \u2014 Show all file changes this session
|
|
3750
|
+
/export [path] \u2014 Export conversation to markdown
|
|
3751
|
+
/save \u2014 Save current session
|
|
3752
|
+
/sessions \u2014 List saved sessions
|
|
3753
|
+
/resume [id] \u2014 Resume a saved session
|
|
3754
|
+
/search <query> \u2014 Search conversation history
|
|
3755
|
+
|
|
3756
|
+
Planning & Agents:
|
|
3757
|
+
/plan <task> \u2014 Generate a step-by-step plan
|
|
3758
|
+
/plan approve \u2014 Approve and execute the plan
|
|
3759
|
+
/plan cancel \u2014 Discard the current plan
|
|
3760
|
+
/agent <task> \u2014 Spawn a background subagent
|
|
3761
|
+
|
|
3762
|
+
Memory & Config:
|
|
3763
|
+
/memory \u2014 List saved memories
|
|
3764
|
+
/memory search <q> \u2014 Search memories
|
|
3765
|
+
/memory clear \u2014 Delete all memories
|
|
3766
|
+
/permissions \u2014 Show current permission config
|
|
3767
|
+
|
|
3768
|
+
Ralph Wiggum Mode (autonomous):
|
|
3769
|
+
/ralph plan <goal> \u2014 Generate task plan for a goal
|
|
3770
|
+
/ralph run \u2014 Execute tasks (fresh context per task)
|
|
3771
|
+
/ralph status \u2014 Show plan progress
|
|
3772
|
+
/ralph clear \u2014 Discard plan
|
|
3773
|
+
|
|
3774
|
+
Other:
|
|
3775
|
+
/branch \u2014 Fork conversation at this point
|
|
3776
|
+
/branches \u2014 List conversation branches
|
|
3777
|
+
/theme \u2014 List available themes
|
|
3778
|
+
/theme <name> \u2014 Switch color theme
|
|
3779
|
+
/mascot \u2014 Show the Notch mantis
|
|
3780
|
+
/clear \u2014 Clear conversation history
|
|
3781
|
+
/quit, /exit, /q \u2014 Exit Notch (shows session usage)
|
|
3782
|
+
/help \u2014 Show this help
|
|
3783
|
+
|
|
3784
|
+
Inline References:
|
|
3785
|
+
@path/to/file \u2014 Inject file contents into prompt
|
|
3786
|
+
@https://url.com \u2014 Fetch and inject URL contents
|
|
3787
|
+
@*.ts \u2014 Inject all matching files (glob)
|
|
3788
|
+
|
|
3789
|
+
Pipe Mode:
|
|
3790
|
+
cat file | notch "explain this"
|
|
3791
|
+
`));
|
|
3792
|
+
}
|
|
3793
|
+
async function readStdin() {
|
|
3794
|
+
if (process.stdin.isTTY) return null;
|
|
3795
|
+
return new Promise((resolve2) => {
|
|
3796
|
+
let data = "";
|
|
3797
|
+
process.stdin.setEncoding("utf-8");
|
|
3798
|
+
process.stdin.on("data", (chunk) => {
|
|
3799
|
+
data += chunk;
|
|
3800
|
+
});
|
|
3801
|
+
process.stdin.on("end", () => resolve2(data.trim() || null));
|
|
3802
|
+
setTimeout(() => {
|
|
3803
|
+
if (!data) resolve2(null);
|
|
3804
|
+
}, 100);
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
async function main() {
|
|
3808
|
+
if (promptArgs[0] === "login") {
|
|
3809
|
+
const spinner = ora("Opening browser...").start();
|
|
3810
|
+
try {
|
|
3811
|
+
spinner.stop();
|
|
3812
|
+
const creds = await login();
|
|
3813
|
+
console.log(chalk8.green(`
|
|
3814
|
+
\u2713 Signed in as ${creds.email}`));
|
|
3815
|
+
console.log(chalk8.gray(` API key stored in ${(await import("./auth-GTGBXOSH.js")).getCredentialsPath()}
|
|
3816
|
+
`));
|
|
3817
|
+
} catch (err) {
|
|
3818
|
+
spinner.stop();
|
|
3819
|
+
console.error(chalk8.red(`
|
|
3820
|
+
Login failed: ${err.message}
|
|
3821
|
+
`));
|
|
3822
|
+
process.exit(1);
|
|
3823
|
+
}
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
if (promptArgs[0] === "logout") {
|
|
3827
|
+
const creds = await loadCredentials();
|
|
3828
|
+
if (!creds) {
|
|
3829
|
+
console.log(chalk8.gray("\n Not signed in.\n"));
|
|
3830
|
+
} else {
|
|
3831
|
+
await clearCredentials();
|
|
3832
|
+
console.log(chalk8.green(`
|
|
3833
|
+
\u2713 Signed out (${creds.email})
|
|
3834
|
+
`));
|
|
3835
|
+
}
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
if (promptArgs[0] === "whoami") {
|
|
3839
|
+
const creds = await loadCredentials();
|
|
3840
|
+
if (!creds) {
|
|
3841
|
+
console.log(chalk8.gray("\n Not signed in. Run: notch login\n"));
|
|
3842
|
+
} else {
|
|
3843
|
+
const keyPreview = `${creds.token.slice(0, 12)}...`;
|
|
3844
|
+
console.log(chalk8.gray(`
|
|
3845
|
+
Signed in as ${chalk8.white(creds.email)}`));
|
|
3846
|
+
console.log(chalk8.gray(` Key: ${keyPreview}`));
|
|
3847
|
+
console.log(chalk8.gray(` Since: ${new Date(creds.createdAt).toLocaleDateString()}
|
|
3848
|
+
`));
|
|
3849
|
+
}
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
if (promptArgs[0] === "init") {
|
|
3853
|
+
await initProject(opts.cwd ?? process.cwd());
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
if (promptArgs[0] === "ralph") {
|
|
3857
|
+
await handleRalphSubcommand(promptArgs.slice(1), opts);
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
const configOverrides = {};
|
|
3861
|
+
if (opts.cwd) configOverrides.projectRoot = opts.cwd;
|
|
3862
|
+
if (opts.yes) configOverrides.autoConfirm = true;
|
|
3863
|
+
if (opts.trust) configOverrides.permissionMode = "trust";
|
|
3864
|
+
if (opts.maxIterations) configOverrides.maxIterations = parseInt(opts.maxIterations, 10);
|
|
3865
|
+
if (opts.noRepoMap === false) configOverrides.useRepoMap = false;
|
|
3866
|
+
if (opts.noMarkdown === false) configOverrides.renderMarkdown = false;
|
|
3867
|
+
if (opts.theme) configOverrides.theme = opts.theme;
|
|
3868
|
+
const config = await loadConfig(configOverrides);
|
|
3869
|
+
if (opts.model) {
|
|
3870
|
+
if (!isValidModel(opts.model)) {
|
|
3871
|
+
console.error(chalk8.red(` Unknown model: ${opts.model}`));
|
|
3872
|
+
console.error(chalk8.gray(` Available: ${modelChoices}`));
|
|
3873
|
+
process.exit(1);
|
|
3874
|
+
}
|
|
3875
|
+
config.models.chat.model = opts.model;
|
|
3876
|
+
}
|
|
3877
|
+
if (opts.baseUrl) config.models.chat.baseUrl = opts.baseUrl;
|
|
3878
|
+
if (opts.apiKey) config.models.chat.apiKey = opts.apiKey;
|
|
3879
|
+
if (config.theme && isValidTheme(config.theme)) {
|
|
3880
|
+
setTheme(config.theme);
|
|
3881
|
+
}
|
|
3882
|
+
let activeModelId = config.models.chat.model;
|
|
3883
|
+
let model = resolveModel(config.models.chat);
|
|
3884
|
+
const info = MODEL_CATALOG[activeModelId];
|
|
3885
|
+
printBanner(VERSION, info.label, info.id, info.size, config.projectRoot);
|
|
3886
|
+
checkForUpdates(VERSION).then((msg) => {
|
|
3887
|
+
if (msg) console.log(msg);
|
|
3888
|
+
});
|
|
3889
|
+
const hookTrustPrompt = async (commands) => {
|
|
3890
|
+
console.warn(chalk8.yellow("\n\u26A0 This project contains hooks in .notch.json that will run shell commands:"));
|
|
3891
|
+
commands.forEach((cmd) => console.warn(chalk8.gray(` \u2022 ${cmd}`)));
|
|
3892
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3893
|
+
return new Promise((resolve2) => {
|
|
3894
|
+
rl2.question(chalk8.yellow("\nAllow these hooks for this project? [y/N] "), (answer) => {
|
|
3895
|
+
rl2.close();
|
|
3896
|
+
resolve2(answer.trim().toLowerCase() === "y");
|
|
3897
|
+
});
|
|
3898
|
+
});
|
|
3899
|
+
};
|
|
3900
|
+
const [permissions, hookConfig] = await Promise.all([
|
|
3901
|
+
loadPermissions(config.projectRoot),
|
|
3902
|
+
config.enableHooks ? loadHooks(config.projectRoot, hookTrustPrompt) : Promise.resolve({ hooks: [] })
|
|
3903
|
+
]);
|
|
3904
|
+
let repoMapStr = "";
|
|
3905
|
+
if (config.useRepoMap) {
|
|
3906
|
+
const spinner = ora("Mapping repository...").start();
|
|
3907
|
+
try {
|
|
3908
|
+
const repoMap = await buildRepoMap(config.projectRoot);
|
|
3909
|
+
repoMapStr = condensedRepoMap(repoMap);
|
|
3910
|
+
spinner.succeed(`Mapped ${repoMap.entries.length} source files`);
|
|
3911
|
+
} catch {
|
|
3912
|
+
spinner.warn("Could not build repo map");
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
const baseSystemPrompt = await buildSystemPrompt(config.projectRoot);
|
|
3916
|
+
const systemPrompt = [
|
|
3917
|
+
baseSystemPrompt,
|
|
3918
|
+
repoMapStr ? `
|
|
3919
|
+
## Repository Map
|
|
3920
|
+
${repoMapStr}` : ""
|
|
3921
|
+
].join("");
|
|
3922
|
+
const checkpoints = new CheckpointManager();
|
|
3923
|
+
const usage = new UsageTracker();
|
|
3924
|
+
let sessionId;
|
|
3925
|
+
let activePlan = null;
|
|
3926
|
+
const branches = /* @__PURE__ */ new Map();
|
|
3927
|
+
let currentBranch = "main";
|
|
3928
|
+
const costTracker = new CostTracker();
|
|
3929
|
+
const mcpClients = [];
|
|
3930
|
+
try {
|
|
3931
|
+
const configRaw = await fs18.readFile(nodePath.resolve(config.projectRoot, ".notch.json"), "utf-8").catch(() => "{}");
|
|
3932
|
+
const mcpConfigs = parseMCPConfig(JSON.parse(configRaw));
|
|
3933
|
+
for (const [name, mcpConfig] of Object.entries(mcpConfigs)) {
|
|
3934
|
+
try {
|
|
3935
|
+
const client = new MCPClient(mcpConfig, name);
|
|
3936
|
+
await client.connect();
|
|
3937
|
+
mcpClients.push(client);
|
|
3938
|
+
console.log(chalk8.green(` MCP: Connected to ${name} (${client.tools.length} tools)`));
|
|
3939
|
+
} catch (err) {
|
|
3940
|
+
console.log(chalk8.yellow(` MCP: Could not connect to ${name}: ${err.message}`));
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
} catch {
|
|
3944
|
+
}
|
|
3945
|
+
const toolCtx = {
|
|
3946
|
+
cwd: config.projectRoot,
|
|
3947
|
+
requireConfirm: config.permissionMode !== "trust" && !config.autoConfirm,
|
|
3948
|
+
confirm: async (message) => {
|
|
3949
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3950
|
+
return new Promise((resolve2) => {
|
|
3951
|
+
rl2.question(`${message} (y/N) `, (answer) => {
|
|
3952
|
+
rl2.close();
|
|
3953
|
+
resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
3954
|
+
});
|
|
3955
|
+
});
|
|
3956
|
+
},
|
|
3957
|
+
log: (msg) => console.log(chalk8.gray(` ${msg}`)),
|
|
3958
|
+
checkPermission: config.permissionMode === "trust" ? () => "allow" : (toolName, args) => checkPermission(permissions, toolName, args),
|
|
3959
|
+
runHook: async (event, ctx) => {
|
|
3960
|
+
if (!config.enableHooks || hookConfig.hooks.length === 0) return;
|
|
3961
|
+
const { results } = await runHooks(hookConfig, event, {
|
|
3962
|
+
cwd: config.projectRoot,
|
|
3963
|
+
...ctx
|
|
3964
|
+
});
|
|
3965
|
+
for (const r of results) {
|
|
3966
|
+
if (!r.ok) {
|
|
3967
|
+
console.log(chalk8.yellow(` Hook failed (${r.hook.event}): ${r.error}`));
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
};
|
|
3972
|
+
await toolCtx.runHook?.("session-start", {});
|
|
3973
|
+
const messages = [];
|
|
3974
|
+
if (opts.resume || opts.session) {
|
|
3975
|
+
const session = opts.session ? await loadSession(opts.session) : await loadLastSession(config.projectRoot);
|
|
3976
|
+
if (session) {
|
|
3977
|
+
messages.push(...session.messages);
|
|
3978
|
+
sessionId = session.meta.id;
|
|
3979
|
+
console.log(chalk8.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
|
|
3980
|
+
`));
|
|
3981
|
+
} else {
|
|
3982
|
+
console.log(chalk8.gray(" No session to resume.\n"));
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
const pipedInput = await readStdin();
|
|
3986
|
+
const oneShot = promptArgs.join(" ").trim();
|
|
3987
|
+
if (oneShot || pipedInput) {
|
|
3988
|
+
let prompt = oneShot;
|
|
3989
|
+
if (pipedInput) {
|
|
3990
|
+
const stdinContext = `<stdin>
|
|
3991
|
+
${pipedInput.slice(0, 5e4)}
|
|
3992
|
+
</stdin>`;
|
|
3993
|
+
prompt = prompt ? `${stdinContext}
|
|
3994
|
+
|
|
3995
|
+
${prompt}` : `${stdinContext}
|
|
3996
|
+
|
|
3997
|
+
Analyze the above input.`;
|
|
3998
|
+
}
|
|
3999
|
+
const { cleanInput, references } = await resolveReferences(prompt, config.projectRoot);
|
|
4000
|
+
const refContext = formatReferences(references);
|
|
4001
|
+
const finalPrompt = refContext + cleanInput;
|
|
4002
|
+
messages.push({ role: "user", content: finalPrompt });
|
|
4003
|
+
console.log(chalk8.cyan(`> ${oneShot || "(piped input)"}
|
|
4004
|
+
`));
|
|
4005
|
+
if (references.length > 0) {
|
|
4006
|
+
console.log(chalk8.gray(` Injected ${references.length} reference(s)
|
|
4007
|
+
`));
|
|
4008
|
+
}
|
|
4009
|
+
const spinner = ora("Thinking...").start();
|
|
4010
|
+
try {
|
|
4011
|
+
const response = await withRetry(
|
|
4012
|
+
() => runAgentLoop(messages, {
|
|
4013
|
+
model,
|
|
4014
|
+
systemPrompt,
|
|
4015
|
+
toolContext: toolCtx,
|
|
4016
|
+
maxIterations: config.maxIterations,
|
|
4017
|
+
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4018
|
+
onTextChunk: (chunk) => {
|
|
4019
|
+
if (spinner.isSpinning) spinner.stop();
|
|
4020
|
+
process.stdout.write(chunk);
|
|
4021
|
+
},
|
|
4022
|
+
onToolCall: (name, args) => {
|
|
4023
|
+
if (spinner.isSpinning) spinner.stop();
|
|
4024
|
+
const argSummary = Object.entries(args).map(([k, v]) => `${k}=${String(v).slice(0, 60)}`).join(", ");
|
|
4025
|
+
console.log(chalk8.gray(`
|
|
4026
|
+
\u2192 ${name}(${argSummary})`));
|
|
4027
|
+
},
|
|
4028
|
+
onToolResult: (_name, result, isError) => {
|
|
4029
|
+
const preview = result.slice(0, 100).replace(/\n/g, " ");
|
|
4030
|
+
const icon = isError ? chalk8.red("\u2717") : chalk8.green("\u2713");
|
|
4031
|
+
console.log(chalk8.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
|
|
4032
|
+
}
|
|
4033
|
+
})
|
|
4034
|
+
);
|
|
4035
|
+
console.log("\n");
|
|
4036
|
+
if (response.usage) {
|
|
4037
|
+
usage.record({
|
|
4038
|
+
promptTokens: response.usage.promptTokens,
|
|
4039
|
+
completionTokens: response.usage.completionTokens,
|
|
4040
|
+
totalTokens: response.usage.totalTokens,
|
|
4041
|
+
model: activeModelId
|
|
4042
|
+
});
|
|
4043
|
+
costTracker.record(activeModelId, response.usage.promptTokens, response.usage.completionTokens);
|
|
4044
|
+
console.log(usage.formatLast());
|
|
4045
|
+
}
|
|
4046
|
+
} catch (err) {
|
|
4047
|
+
spinner.fail(`Error: ${err.message}`);
|
|
4048
|
+
}
|
|
4049
|
+
process.exit(0);
|
|
4050
|
+
}
|
|
4051
|
+
let ralphPlan = null;
|
|
4052
|
+
try {
|
|
4053
|
+
const savedPlan = await loadPlan(config.projectRoot);
|
|
4054
|
+
if (savedPlan) {
|
|
4055
|
+
ralphPlan = savedPlan;
|
|
4056
|
+
console.log(chalk8.gray(` Ralph plan loaded (${savedPlan.tasks.length} tasks)
|
|
4057
|
+
`));
|
|
4058
|
+
}
|
|
4059
|
+
} catch {
|
|
4060
|
+
}
|
|
4061
|
+
const completer = buildCompleter(config.projectRoot);
|
|
4062
|
+
const rl = readline.createInterface({
|
|
4063
|
+
input: process.stdin,
|
|
4064
|
+
output: process.stdout,
|
|
4065
|
+
prompt: theme().prompt("notch> "),
|
|
4066
|
+
completer: (line) => completer(line)
|
|
4067
|
+
});
|
|
4068
|
+
console.log(chalk8.gray(" Type your request, or /help for commands.\n"));
|
|
4069
|
+
rl.prompt();
|
|
4070
|
+
rl.on("line", async (line) => {
|
|
4071
|
+
const input = line.trim();
|
|
4072
|
+
if (!input) {
|
|
4073
|
+
rl.prompt();
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
if (input === "/quit" || input === "/exit" || input === "/q") {
|
|
4077
|
+
if (usage.turnCount > 0) {
|
|
4078
|
+
console.log(usage.formatSession());
|
|
4079
|
+
console.log(costTracker.formatTotal());
|
|
4080
|
+
}
|
|
4081
|
+
if (messages.length > 0) {
|
|
4082
|
+
try {
|
|
4083
|
+
const id = await saveSession(config.projectRoot, messages, { model: activeModelId });
|
|
4084
|
+
console.log(chalk8.gray(` Session saved: ${id}`));
|
|
4085
|
+
} catch {
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
for (const client of mcpClients) {
|
|
4089
|
+
try {
|
|
4090
|
+
await client.disconnect();
|
|
4091
|
+
} catch {
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
await toolCtx.runHook?.("session-end", {});
|
|
4095
|
+
console.log(chalk8.gray("\n Goodbye!\n"));
|
|
4096
|
+
process.exit(0);
|
|
4097
|
+
}
|
|
4098
|
+
if (input === "/clear") {
|
|
4099
|
+
messages.length = 0;
|
|
4100
|
+
console.log(chalk8.gray(" Conversation cleared.\n"));
|
|
4101
|
+
rl.prompt();
|
|
4102
|
+
return;
|
|
4103
|
+
}
|
|
4104
|
+
if (input === "/help") {
|
|
4105
|
+
printHelp();
|
|
4106
|
+
rl.prompt();
|
|
4107
|
+
return;
|
|
4108
|
+
}
|
|
4109
|
+
if (input === "/model" || input === "/models") {
|
|
4110
|
+
printModelTable(activeModelId);
|
|
4111
|
+
rl.prompt();
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
if (input.startsWith("/model ")) {
|
|
4115
|
+
let newModel = input.replace("/model ", "").trim();
|
|
4116
|
+
if (!newModel.startsWith("notch-")) {
|
|
4117
|
+
newModel = `notch-${newModel}`;
|
|
4118
|
+
}
|
|
4119
|
+
if (!isValidModel(newModel)) {
|
|
4120
|
+
console.log(chalk8.red(` Unknown model: ${newModel}`));
|
|
4121
|
+
console.log(chalk8.gray(` Available: ${modelChoices}`));
|
|
4122
|
+
rl.prompt();
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
activeModelId = newModel;
|
|
4126
|
+
config.models.chat.model = activeModelId;
|
|
4127
|
+
model = resolveModel(config.models.chat);
|
|
4128
|
+
const switchedInfo = MODEL_CATALOG[activeModelId];
|
|
4129
|
+
console.log(chalk8.green(` Switched to ${switchedInfo.label} (${switchedInfo.id}, ${switchedInfo.size})
|
|
4130
|
+
`));
|
|
4131
|
+
rl.prompt();
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
if (input === "/status") {
|
|
4135
|
+
const statusSpinner = ora("Checking Notch API...").start();
|
|
4136
|
+
const check = await validateConfig(config.models.chat);
|
|
4137
|
+
if (check.ok) {
|
|
4138
|
+
const statusInfo = MODEL_CATALOG[activeModelId];
|
|
4139
|
+
statusSpinner.succeed(`${statusInfo.label} (${activeModelId}) is reachable`);
|
|
4140
|
+
} else {
|
|
4141
|
+
statusSpinner.fail(check.error ?? "API unreachable");
|
|
4142
|
+
console.log(chalk8.gray(" Tip: Set NOTCH_API_KEY or use --api-key, and verify your Modal endpoint is running.\n"));
|
|
4143
|
+
}
|
|
4144
|
+
rl.prompt();
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if (input === "/undo") {
|
|
4148
|
+
if (checkpoints.undoCount === 0) {
|
|
4149
|
+
console.log(chalk8.gray(" Nothing to undo.\n"));
|
|
4150
|
+
rl.prompt();
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
const undone = await checkpoints.undo();
|
|
4154
|
+
if (undone) {
|
|
4155
|
+
const fileCount = undone.files.length;
|
|
4156
|
+
console.log(chalk8.yellow(` Undid "${undone.description}" (${fileCount} file${fileCount !== 1 ? "s" : ""} restored)`));
|
|
4157
|
+
console.log(chalk8.gray(` ${checkpoints.undoCount} checkpoint${checkpoints.undoCount !== 1 ? "s" : ""} remaining
|
|
4158
|
+
`));
|
|
4159
|
+
}
|
|
4160
|
+
rl.prompt();
|
|
4161
|
+
return;
|
|
4162
|
+
}
|
|
4163
|
+
if (input === "/usage") {
|
|
4164
|
+
if (usage.turnCount === 0) {
|
|
4165
|
+
console.log(chalk8.gray(" No usage yet.\n"));
|
|
4166
|
+
} else {
|
|
4167
|
+
console.log(usage.formatSession());
|
|
4168
|
+
const currentTokens = estimateTokens(messages);
|
|
4169
|
+
const ctxWindow = MODEL_CATALOG[activeModelId].contextWindow;
|
|
4170
|
+
console.log(renderContextMeter(currentTokens, ctxWindow));
|
|
4171
|
+
console.log("");
|
|
4172
|
+
}
|
|
4173
|
+
rl.prompt();
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
if (input === "/cost") {
|
|
4177
|
+
if (costTracker.totalCost === 0) {
|
|
4178
|
+
console.log(chalk8.gray(" No cost data yet.\n"));
|
|
4179
|
+
} else {
|
|
4180
|
+
console.log(costTracker.formatTotal());
|
|
4181
|
+
console.log(costTracker.formatByModel());
|
|
4182
|
+
console.log("");
|
|
4183
|
+
}
|
|
4184
|
+
rl.prompt();
|
|
4185
|
+
return;
|
|
4186
|
+
}
|
|
4187
|
+
if (input === "/compact") {
|
|
4188
|
+
const { autoCompress: autoCompress2 } = await import("./compression-AGHTZF7D.js");
|
|
4189
|
+
const before = messages.length;
|
|
4190
|
+
const compressed = await autoCompress2(messages, model, MODEL_CATALOG[activeModelId].contextWindow);
|
|
4191
|
+
messages.length = 0;
|
|
4192
|
+
messages.push(...compressed);
|
|
4193
|
+
console.log(chalk8.green(` Compressed: ${before} messages \u2192 ${messages.length} messages`));
|
|
4194
|
+
console.log("");
|
|
4195
|
+
rl.prompt();
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
if (input === "/diff") {
|
|
4199
|
+
const diffs = checkpoints.allDiffs();
|
|
4200
|
+
if (diffs.length === 0) {
|
|
4201
|
+
console.log(chalk8.gray(" No file changes this session.\n"));
|
|
4202
|
+
} else {
|
|
4203
|
+
for (const df of diffs) {
|
|
4204
|
+
console.log(chalk8.cyan(` ${df.path}:`));
|
|
4205
|
+
console.log(unifiedDiff(df.before, df.after, df.path));
|
|
4206
|
+
console.log("");
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
rl.prompt();
|
|
4210
|
+
return;
|
|
4211
|
+
}
|
|
4212
|
+
if (input.startsWith("/export")) {
|
|
4213
|
+
const exportPath = input.replace("/export", "").trim() || void 0;
|
|
4214
|
+
try {
|
|
4215
|
+
const ePath = await exportSession(messages, {
|
|
4216
|
+
model: activeModelId,
|
|
4217
|
+
projectRoot: config.projectRoot,
|
|
4218
|
+
outputPath: exportPath
|
|
4219
|
+
});
|
|
4220
|
+
console.log(chalk8.green(` Exported to ${ePath}
|
|
4221
|
+
`));
|
|
4222
|
+
} catch (err) {
|
|
4223
|
+
console.log(chalk8.red(` Export failed: ${err.message}
|
|
4224
|
+
`));
|
|
4225
|
+
}
|
|
4226
|
+
rl.prompt();
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
if (input === "/save") {
|
|
4230
|
+
try {
|
|
4231
|
+
const id = await saveSession(config.projectRoot, messages, { model: activeModelId });
|
|
4232
|
+
sessionId = id;
|
|
4233
|
+
console.log(chalk8.green(` Session saved: ${id}
|
|
4234
|
+
`));
|
|
4235
|
+
} catch (err) {
|
|
4236
|
+
console.log(chalk8.red(` Save failed: ${err.message}
|
|
4237
|
+
`));
|
|
4238
|
+
}
|
|
4239
|
+
rl.prompt();
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
if (input === "/sessions") {
|
|
4243
|
+
try {
|
|
4244
|
+
const sessions = await listSessions(config.projectRoot);
|
|
4245
|
+
if (sessions.length === 0) {
|
|
4246
|
+
console.log(chalk8.gray(" No saved sessions.\n"));
|
|
4247
|
+
} else {
|
|
4248
|
+
console.log(chalk8.gray("\n Saved sessions:\n"));
|
|
4249
|
+
for (const s of sessions.slice(0, 10)) {
|
|
4250
|
+
console.log(chalk8.gray(` ${s.id} ${s.turns} turns ${s.date} ${s.model}`));
|
|
4251
|
+
}
|
|
4252
|
+
console.log("");
|
|
4253
|
+
}
|
|
4254
|
+
} catch (err) {
|
|
4255
|
+
console.log(chalk8.red(` Error listing sessions: ${err.message}
|
|
4256
|
+
`));
|
|
4257
|
+
}
|
|
4258
|
+
rl.prompt();
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
if (input.startsWith("/resume")) {
|
|
4262
|
+
const resumeId = input.replace("/resume", "").trim() || void 0;
|
|
4263
|
+
try {
|
|
4264
|
+
const session = resumeId ? await loadSession(resumeId) : await loadLastSession(config.projectRoot);
|
|
4265
|
+
if (session) {
|
|
4266
|
+
messages.length = 0;
|
|
4267
|
+
messages.push(...session.messages);
|
|
4268
|
+
sessionId = session.meta.id;
|
|
4269
|
+
try {
|
|
4270
|
+
const savedPlan = await loadPlan(config.projectRoot);
|
|
4271
|
+
if (savedPlan) {
|
|
4272
|
+
ralphPlan = savedPlan;
|
|
4273
|
+
console.log(chalk8.green(` Ralph plan restored (${savedPlan.tasks.length} tasks)
|
|
4274
|
+
`));
|
|
4275
|
+
}
|
|
4276
|
+
} catch {
|
|
4277
|
+
}
|
|
4278
|
+
console.log(chalk8.green(` Resumed session ${session.meta.id} (${session.meta.turns} turns)
|
|
4279
|
+
`));
|
|
4280
|
+
} else {
|
|
4281
|
+
console.log(chalk8.gray(" No session found.\n"));
|
|
4282
|
+
}
|
|
4283
|
+
} catch (err) {
|
|
4284
|
+
console.log(chalk8.red(` Resume failed: ${err.message}
|
|
4285
|
+
`));
|
|
4286
|
+
}
|
|
4287
|
+
rl.prompt();
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
if (input.startsWith("/search ")) {
|
|
4291
|
+
const query = input.replace("/search ", "").trim().toLowerCase();
|
|
4292
|
+
if (!query) {
|
|
4293
|
+
console.log(chalk8.gray(" Usage: /search <query>\n"));
|
|
4294
|
+
rl.prompt();
|
|
4295
|
+
return;
|
|
4296
|
+
}
|
|
4297
|
+
const matches = [];
|
|
4298
|
+
for (let i = 0; i < messages.length; i++) {
|
|
4299
|
+
const msg = messages[i];
|
|
4300
|
+
const text = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.map((p) => p.text || p.result || "").join(" ") : "";
|
|
4301
|
+
if (text.toLowerCase().includes(query)) {
|
|
4302
|
+
matches.push({
|
|
4303
|
+
index: i,
|
|
4304
|
+
role: msg.role,
|
|
4305
|
+
preview: text.slice(0, 120).replace(/\n/g, " ")
|
|
4306
|
+
});
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
if (matches.length === 0) {
|
|
4310
|
+
console.log(chalk8.gray(` No matches for "${query}"
|
|
4311
|
+
`));
|
|
4312
|
+
} else {
|
|
4313
|
+
console.log(chalk8.gray(`
|
|
4314
|
+
${matches.length} match(es) for "${query}":
|
|
4315
|
+
`));
|
|
4316
|
+
for (const m of matches.slice(0, 10)) {
|
|
4317
|
+
console.log(chalk8.gray(` [${m.index}] ${m.role}: ${m.preview}`));
|
|
4318
|
+
}
|
|
4319
|
+
console.log("");
|
|
4320
|
+
}
|
|
4321
|
+
rl.prompt();
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4324
|
+
if (input.startsWith("/plan ") && !input.startsWith("/plan approve") && !input.startsWith("/plan cancel") && !input.startsWith("/plan edit")) {
|
|
4325
|
+
const task = input.replace("/plan ", "").trim();
|
|
4326
|
+
const planSpinner = ora("Generating plan...").start();
|
|
4327
|
+
try {
|
|
4328
|
+
activePlan = await generatePlan(task, model, systemPrompt);
|
|
4329
|
+
planSpinner.succeed("Plan generated");
|
|
4330
|
+
console.log(formatPlan(activePlan));
|
|
4331
|
+
console.log(chalk8.gray(" Use /plan approve to execute, /plan edit to modify, or /plan cancel to discard.\n"));
|
|
4332
|
+
} catch (err) {
|
|
4333
|
+
planSpinner.fail(`Plan failed: ${err.message}`);
|
|
4334
|
+
}
|
|
4335
|
+
rl.prompt();
|
|
4336
|
+
return;
|
|
4337
|
+
}
|
|
4338
|
+
if (input === "/plan approve") {
|
|
4339
|
+
if (!activePlan) {
|
|
4340
|
+
console.log(chalk8.gray(" No active plan. Use /plan <task> to create one.\n"));
|
|
4341
|
+
rl.prompt();
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
console.log(chalk8.green(" Executing plan...\n"));
|
|
4345
|
+
while (!isPlanComplete(activePlan)) {
|
|
4346
|
+
const stepPrompt = currentStepPrompt(activePlan);
|
|
4347
|
+
messages.push({ role: "user", content: stepPrompt });
|
|
4348
|
+
const planStepSpinner = ora(`Step ${activePlan.currentStep + 1}/${activePlan.steps.length}...`).start();
|
|
4349
|
+
try {
|
|
4350
|
+
const response = await runAgentLoop(messages, {
|
|
4351
|
+
model,
|
|
4352
|
+
systemPrompt,
|
|
4353
|
+
toolContext: toolCtx,
|
|
4354
|
+
maxIterations: config.maxIterations,
|
|
4355
|
+
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4356
|
+
onTextChunk: (chunk) => {
|
|
4357
|
+
if (planStepSpinner.isSpinning) planStepSpinner.stop();
|
|
4358
|
+
process.stdout.write(chunk);
|
|
4359
|
+
},
|
|
4360
|
+
onToolCall: (name) => {
|
|
4361
|
+
if (planStepSpinner.isSpinning) planStepSpinner.stop();
|
|
4362
|
+
console.log(chalk8.gray(` \u2192 ${name}`));
|
|
4363
|
+
},
|
|
4364
|
+
onToolResult: (_name, _result, isError) => {
|
|
4365
|
+
console.log(isError ? chalk8.red(" \u2717") : chalk8.green(" \u2713"));
|
|
4366
|
+
}
|
|
4367
|
+
});
|
|
4368
|
+
console.log("\n");
|
|
4369
|
+
messages.length = 0;
|
|
4370
|
+
messages.push(...response.messages);
|
|
4371
|
+
activePlan = advancePlan(activePlan);
|
|
4372
|
+
} catch (err) {
|
|
4373
|
+
planStepSpinner.fail(`Step failed: ${err.message}`);
|
|
4374
|
+
break;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
if (activePlan && isPlanComplete(activePlan)) {
|
|
4378
|
+
console.log(chalk8.green(" Plan completed!\n"));
|
|
4379
|
+
}
|
|
4380
|
+
activePlan = null;
|
|
4381
|
+
rl.prompt();
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
if (input === "/plan edit") {
|
|
4385
|
+
if (!activePlan) {
|
|
4386
|
+
console.log(chalk8.gray(" No active plan to edit.\n"));
|
|
4387
|
+
rl.prompt();
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
console.log(formatPlan(activePlan));
|
|
4391
|
+
console.log(chalk8.gray(" Type a modified plan description and use /plan <new task> to regenerate,"));
|
|
4392
|
+
console.log(chalk8.gray(" or /plan approve to proceed with the current plan.\n"));
|
|
4393
|
+
rl.prompt();
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
if (input === "/plan cancel") {
|
|
4397
|
+
activePlan = null;
|
|
4398
|
+
console.log(chalk8.gray(" Plan discarded.\n"));
|
|
4399
|
+
rl.prompt();
|
|
4400
|
+
return;
|
|
4401
|
+
}
|
|
4402
|
+
if (input.startsWith("/agent ")) {
|
|
4403
|
+
const task = input.replace("/agent ", "").trim();
|
|
4404
|
+
const agentId = nextSubagentId();
|
|
4405
|
+
console.log(chalk8.cyan(` Spawning subagent #${agentId}: ${task}
|
|
4406
|
+
`));
|
|
4407
|
+
spawnSubagent({
|
|
4408
|
+
id: agentId,
|
|
4409
|
+
task,
|
|
4410
|
+
model,
|
|
4411
|
+
systemPrompt,
|
|
4412
|
+
toolContext: toolCtx,
|
|
4413
|
+
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4414
|
+
onComplete: (result) => {
|
|
4415
|
+
console.log(chalk8.green(`
|
|
4416
|
+
Subagent #${agentId} finished:`));
|
|
4417
|
+
console.log(chalk8.gray(` ${result.slice(0, 200)}
|
|
4418
|
+
`));
|
|
4419
|
+
rl.prompt();
|
|
4420
|
+
},
|
|
4421
|
+
onError: (err) => {
|
|
4422
|
+
console.log(chalk8.red(`
|
|
4423
|
+
Subagent #${agentId} failed: ${err}
|
|
4424
|
+
`));
|
|
4425
|
+
rl.prompt();
|
|
4426
|
+
}
|
|
4427
|
+
});
|
|
4428
|
+
rl.prompt();
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
if (input === "/memory") {
|
|
4432
|
+
try {
|
|
4433
|
+
const memories = await loadMemories(config.projectRoot);
|
|
4434
|
+
if (memories.length === 0) {
|
|
4435
|
+
console.log(chalk8.gray(" No saved memories.\n"));
|
|
4436
|
+
} else {
|
|
4437
|
+
console.log(chalk8.gray(`
|
|
4438
|
+
${memories.length} saved memories:
|
|
4439
|
+
`));
|
|
4440
|
+
for (const m of memories) {
|
|
4441
|
+
console.log(chalk8.gray(` [${m.type}] ${m.content.slice(0, 80)}`));
|
|
4442
|
+
}
|
|
4443
|
+
console.log("");
|
|
4444
|
+
}
|
|
4445
|
+
} catch (err) {
|
|
4446
|
+
console.log(chalk8.red(` Error: ${err.message}
|
|
4447
|
+
`));
|
|
4448
|
+
}
|
|
4449
|
+
rl.prompt();
|
|
4450
|
+
return;
|
|
4451
|
+
}
|
|
4452
|
+
if (input.startsWith("/memory search ")) {
|
|
4453
|
+
const query = input.replace("/memory search ", "").trim();
|
|
4454
|
+
try {
|
|
4455
|
+
const results = await searchMemories(config.projectRoot, query);
|
|
4456
|
+
if (results.length === 0) {
|
|
4457
|
+
console.log(chalk8.gray(` No memories matching "${query}"
|
|
4458
|
+
`));
|
|
4459
|
+
} else {
|
|
4460
|
+
for (const m of results) {
|
|
4461
|
+
console.log(chalk8.gray(` [${m.type}] ${m.content.slice(0, 100)}`));
|
|
4462
|
+
}
|
|
4463
|
+
console.log("");
|
|
4464
|
+
}
|
|
4465
|
+
} catch (err) {
|
|
4466
|
+
console.log(chalk8.red(` Error: ${err.message}
|
|
4467
|
+
`));
|
|
4468
|
+
}
|
|
4469
|
+
rl.prompt();
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
if (input === "/memory clear") {
|
|
4473
|
+
try {
|
|
4474
|
+
const memories = await loadMemories(config.projectRoot);
|
|
4475
|
+
for (const m of memories) {
|
|
4476
|
+
await deleteMemory(config.projectRoot, m.id);
|
|
4477
|
+
}
|
|
4478
|
+
console.log(chalk8.yellow(` Cleared ${memories.length} memories.
|
|
4479
|
+
`));
|
|
4480
|
+
} catch (err) {
|
|
4481
|
+
console.log(chalk8.red(` Error: ${err.message}
|
|
4482
|
+
`));
|
|
4483
|
+
}
|
|
4484
|
+
rl.prompt();
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
4487
|
+
if (input === "/permissions") {
|
|
4488
|
+
console.log(formatPermissions(permissions));
|
|
4489
|
+
console.log("");
|
|
4490
|
+
rl.prompt();
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
if (input.startsWith("/ralph plan ")) {
|
|
4494
|
+
const goal = input.replace("/ralph plan ", "").trim();
|
|
4495
|
+
const planSpinner = ora("Ralph is planning...").start();
|
|
4496
|
+
try {
|
|
4497
|
+
ralphPlan = await generateRalphPlan(goal, model, systemPrompt);
|
|
4498
|
+
await savePlan(config.projectRoot, ralphPlan);
|
|
4499
|
+
planSpinner.succeed(`Ralph planned ${ralphPlan.tasks.length} tasks`);
|
|
4500
|
+
console.log(formatRalphStatus(ralphPlan));
|
|
4501
|
+
} catch (err) {
|
|
4502
|
+
planSpinner.fail(`Ralph planning failed: ${err.message}`);
|
|
4503
|
+
}
|
|
4504
|
+
rl.prompt();
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
if (input === "/ralph run") {
|
|
4508
|
+
if (!ralphPlan) {
|
|
4509
|
+
console.log(chalk8.gray(" No Ralph plan. Use /ralph plan <goal> first.\n"));
|
|
4510
|
+
rl.prompt();
|
|
4511
|
+
return;
|
|
4512
|
+
}
|
|
4513
|
+
console.log(chalk8.green(" Ralph is running...\n"));
|
|
4514
|
+
try {
|
|
4515
|
+
ralphPlan = await runRalphLoop(ralphPlan, {
|
|
4516
|
+
model,
|
|
4517
|
+
systemPrompt,
|
|
4518
|
+
toolContext: toolCtx,
|
|
4519
|
+
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4520
|
+
onTaskStart: (task) => console.log(chalk8.cyan(` \u25B6 Task: ${task.description}`)),
|
|
4521
|
+
onTaskComplete: (task) => console.log(chalk8.green(` \u2713 Done: ${task.description}
|
|
4522
|
+
`)),
|
|
4523
|
+
onTaskFail: (task, err) => console.log(chalk8.red(` \u2717 Failed: ${task.description} (${err})
|
|
4524
|
+
`))
|
|
4525
|
+
});
|
|
4526
|
+
await savePlan(config.projectRoot, ralphPlan);
|
|
4527
|
+
console.log(formatRalphStatus(ralphPlan));
|
|
4528
|
+
} catch (err) {
|
|
4529
|
+
console.log(chalk8.red(` Ralph error: ${err.message}
|
|
4530
|
+
`));
|
|
4531
|
+
}
|
|
4532
|
+
rl.prompt();
|
|
4533
|
+
return;
|
|
4534
|
+
}
|
|
4535
|
+
if (input === "/ralph status") {
|
|
4536
|
+
if (!ralphPlan) {
|
|
4537
|
+
console.log(chalk8.gray(" No Ralph plan active.\n"));
|
|
4538
|
+
} else {
|
|
4539
|
+
console.log(formatRalphStatus(ralphPlan));
|
|
4540
|
+
}
|
|
4541
|
+
rl.prompt();
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
if (input === "/ralph clear") {
|
|
4545
|
+
ralphPlan = null;
|
|
4546
|
+
await deletePlan(config.projectRoot).catch(() => {
|
|
4547
|
+
});
|
|
4548
|
+
console.log(chalk8.gray(" Ralph plan cleared.\n"));
|
|
4549
|
+
rl.prompt();
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
if (input === "/branch") {
|
|
4553
|
+
const branchId = `branch-${branches.size + 1}`;
|
|
4554
|
+
branches.set(branchId, [...messages]);
|
|
4555
|
+
console.log(chalk8.green(` Forked conversation as "${branchId}" (${messages.length} messages)
|
|
4556
|
+
`));
|
|
4557
|
+
rl.prompt();
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
if (input === "/branches") {
|
|
4561
|
+
if (branches.size === 0) {
|
|
4562
|
+
console.log(chalk8.gray(" No conversation branches. Use /branch to fork.\n"));
|
|
4563
|
+
} else {
|
|
4564
|
+
console.log(chalk8.gray("\n Branches:\n"));
|
|
4565
|
+
for (const [name, msgs] of branches) {
|
|
4566
|
+
const marker = name === currentBranch ? chalk8.green(" \u25CF") : " ";
|
|
4567
|
+
console.log(chalk8.gray(` ${marker} ${name} (${msgs.length} messages)`));
|
|
4568
|
+
}
|
|
4569
|
+
console.log("");
|
|
4570
|
+
}
|
|
4571
|
+
rl.prompt();
|
|
4572
|
+
return;
|
|
4573
|
+
}
|
|
4574
|
+
if (input === "/theme" || input === "/themes") {
|
|
4575
|
+
console.log(formatThemeList(themeId()));
|
|
4576
|
+
rl.prompt();
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
if (input.startsWith("/theme ")) {
|
|
4580
|
+
const newTheme = input.replace("/theme ", "").trim();
|
|
4581
|
+
if (!isValidTheme(newTheme)) {
|
|
4582
|
+
const t2 = theme();
|
|
4583
|
+
console.log(t2.error(` Unknown theme: ${newTheme}`));
|
|
4584
|
+
console.log(t2.dim(` Available: ${THEME_IDS.join(", ")}
|
|
4585
|
+
`));
|
|
4586
|
+
rl.prompt();
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
setTheme(newTheme);
|
|
4590
|
+
const t = theme();
|
|
4591
|
+
console.log(t.success(` Switched to "${t.name}" theme`));
|
|
4592
|
+
console.log(t.dim(` ${THEME_IDS.length} themes available. Use /theme to browse.
|
|
4593
|
+
`));
|
|
4594
|
+
rl.setPrompt(t.prompt("notch> "));
|
|
4595
|
+
rl.prompt();
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
if (input === "/mascot") {
|
|
4599
|
+
printMantis();
|
|
4600
|
+
rl.prompt();
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
if (input.startsWith("/")) {
|
|
4604
|
+
console.log(chalk8.red(` Unknown command: ${input}`));
|
|
4605
|
+
console.log(chalk8.gray(" Type /help for available commands.\n"));
|
|
4606
|
+
rl.prompt();
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
const { cleanInput, references } = await resolveReferences(input, config.projectRoot);
|
|
4610
|
+
const refContext = formatReferences(references);
|
|
4611
|
+
const finalPrompt = refContext + cleanInput;
|
|
4612
|
+
if (references.length > 0) {
|
|
4613
|
+
console.log(chalk8.gray(` Injected ${references.length} reference(s)`));
|
|
4614
|
+
}
|
|
4615
|
+
messages.push({ role: "user", content: finalPrompt });
|
|
4616
|
+
const spinner = ora("Thinking...").start();
|
|
4617
|
+
try {
|
|
4618
|
+
const response = await withRetry(
|
|
4619
|
+
() => runAgentLoop(messages, {
|
|
4620
|
+
model,
|
|
4621
|
+
systemPrompt,
|
|
4622
|
+
toolContext: toolCtx,
|
|
4623
|
+
maxIterations: config.maxIterations,
|
|
4624
|
+
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
4625
|
+
onTextChunk: (chunk) => {
|
|
4626
|
+
if (spinner.isSpinning) spinner.stop();
|
|
4627
|
+
process.stdout.write(chunk);
|
|
4628
|
+
},
|
|
4629
|
+
onToolCall: (name, args) => {
|
|
4630
|
+
if (spinner.isSpinning) spinner.stop();
|
|
4631
|
+
if ((name === "write" || name === "edit") && typeof args.path === "string") {
|
|
4632
|
+
const filePath = nodePath.isAbsolute(args.path) ? args.path : nodePath.resolve(toolCtx.cwd, args.path);
|
|
4633
|
+
checkpoints.recordBefore(filePath);
|
|
4634
|
+
}
|
|
4635
|
+
const argSummary = Object.entries(args).map(([k, v]) => {
|
|
4636
|
+
const val = String(v);
|
|
4637
|
+
return `${k}=${val.length > 60 ? val.slice(0, 60) + "..." : val}`;
|
|
4638
|
+
}).join(", ");
|
|
4639
|
+
console.log(chalk8.gray(`
|
|
4640
|
+
\u2192 ${name}(${argSummary})`));
|
|
4641
|
+
},
|
|
4642
|
+
onToolResult: (_name, result, isError) => {
|
|
4643
|
+
const preview = result.slice(0, 100).replace(/\n/g, " ");
|
|
4644
|
+
const icon = isError ? chalk8.red("\u2717") : chalk8.green("\u2713");
|
|
4645
|
+
console.log(chalk8.gray(` ${icon} ${preview}${result.length > 100 ? "..." : ""}`));
|
|
4646
|
+
},
|
|
4647
|
+
onCompress: () => {
|
|
4648
|
+
console.log(chalk8.yellow("\n [Context compressed to fit window]\n"));
|
|
4649
|
+
}
|
|
4650
|
+
})
|
|
4651
|
+
);
|
|
4652
|
+
console.log("\n");
|
|
4653
|
+
checkpoints.commit(`Turn ${usage.turnCount + 1}`);
|
|
4654
|
+
if (response.usage) {
|
|
4655
|
+
usage.record({
|
|
4656
|
+
promptTokens: response.usage.promptTokens,
|
|
4657
|
+
completionTokens: response.usage.completionTokens,
|
|
4658
|
+
totalTokens: response.usage.totalTokens,
|
|
4659
|
+
model: activeModelId
|
|
4660
|
+
});
|
|
4661
|
+
costTracker.record(activeModelId, response.usage.promptTokens, response.usage.completionTokens);
|
|
4662
|
+
console.log(usage.formatLast());
|
|
4663
|
+
const currentTokens = estimateTokens(messages);
|
|
4664
|
+
const ctxWindow = MODEL_CATALOG[activeModelId].contextWindow;
|
|
4665
|
+
if (currentTokens > ctxWindow * 0.5) {
|
|
4666
|
+
console.log(compactContextIndicator(currentTokens, ctxWindow));
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
console.log("");
|
|
4670
|
+
messages.length = 0;
|
|
4671
|
+
messages.push(...response.messages);
|
|
4672
|
+
const lastText = response.text;
|
|
4673
|
+
if (lastText.includes("[MEMORY:") || lastText.includes("[memory:")) {
|
|
4674
|
+
const memMatch = lastText.match(/\[(?:MEMORY|memory):\s*([^\]]+)\]/);
|
|
4675
|
+
if (memMatch) {
|
|
4676
|
+
try {
|
|
4677
|
+
await saveMemory(config.projectRoot, {
|
|
4678
|
+
type: "auto",
|
|
4679
|
+
content: memMatch[1]
|
|
4680
|
+
});
|
|
4681
|
+
console.log(chalk8.gray(" (Saved to memory)\n"));
|
|
4682
|
+
} catch {
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
} catch (err) {
|
|
4687
|
+
spinner.fail(`Error: ${err.message}`);
|
|
4688
|
+
checkpoints.discard();
|
|
4689
|
+
const msg = err.message?.toLowerCase() ?? "";
|
|
4690
|
+
if (msg.includes("fetch") || msg.includes("econnrefused") || msg.includes("network")) {
|
|
4691
|
+
console.log(chalk8.gray(" Tip: Check that your Notch endpoint is running. Use /status to verify.\n"));
|
|
4692
|
+
} else if (msg.includes("401") || msg.includes("unauthorized") || msg.includes("api key")) {
|
|
4693
|
+
console.log(chalk8.gray(" Tip: Set NOTCH_API_KEY env var or use --api-key flag.\n"));
|
|
4694
|
+
} else if (msg.includes("429") || msg.includes("rate limit")) {
|
|
4695
|
+
console.log(chalk8.gray(" Tip: Rate limited. Wait a moment and try again.\n"));
|
|
4696
|
+
} else {
|
|
4697
|
+
console.log(chalk8.gray(" (The conversation history is preserved. Try again.)\n"));
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
rl.prompt();
|
|
4701
|
+
});
|
|
4702
|
+
rl.on("close", () => {
|
|
4703
|
+
process.exit(0);
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
async function handleRalphSubcommand(args, cliOpts) {
|
|
4707
|
+
const config = await loadConfig(cliOpts.cwd ? { projectRoot: cliOpts.cwd } : {});
|
|
4708
|
+
if (cliOpts.model) config.models.chat.model = cliOpts.model;
|
|
4709
|
+
const model = resolveModel(config.models.chat);
|
|
4710
|
+
const systemPrompt = await buildSystemPrompt(config.projectRoot);
|
|
4711
|
+
const toolCtx = {
|
|
4712
|
+
cwd: config.projectRoot,
|
|
4713
|
+
requireConfirm: false,
|
|
4714
|
+
confirm: async () => true,
|
|
4715
|
+
log: (msg) => console.log(chalk8.gray(` ${msg}`))
|
|
4716
|
+
};
|
|
4717
|
+
const subcommand = args[0];
|
|
4718
|
+
if (subcommand === "plan") {
|
|
4719
|
+
const goal = args.slice(1).join(" ");
|
|
4720
|
+
if (!goal) {
|
|
4721
|
+
console.error(chalk8.red(" Usage: notch ralph plan <goal>"));
|
|
4722
|
+
process.exit(1);
|
|
4723
|
+
}
|
|
4724
|
+
const spinner = ora("Ralph is planning...").start();
|
|
4725
|
+
const plan = await generateRalphPlan(goal, model, systemPrompt);
|
|
4726
|
+
await savePlan(config.projectRoot, plan);
|
|
4727
|
+
spinner.succeed(`Planned ${plan.tasks.length} tasks`);
|
|
4728
|
+
console.log(formatRalphStatus(plan));
|
|
4729
|
+
} else if (subcommand === "run") {
|
|
4730
|
+
let plan = await loadPlan(config.projectRoot);
|
|
4731
|
+
if (!plan) {
|
|
4732
|
+
console.error(chalk8.red(" No plan found. Run: notch ralph plan <goal>"));
|
|
4733
|
+
process.exit(1);
|
|
4734
|
+
}
|
|
4735
|
+
plan = await runRalphLoop(plan, {
|
|
4736
|
+
model,
|
|
4737
|
+
systemPrompt,
|
|
4738
|
+
toolContext: toolCtx,
|
|
4739
|
+
contextWindow: MODEL_CATALOG[config.models.chat.model].contextWindow,
|
|
4740
|
+
onTaskStart: (t) => console.log(chalk8.cyan(` \u25B6 ${t.description}`)),
|
|
4741
|
+
onTaskComplete: (t) => console.log(chalk8.green(` \u2713 ${t.description}`)),
|
|
4742
|
+
onTaskFail: (t, e) => console.log(chalk8.red(` \u2717 ${t.description}: ${e}`))
|
|
4743
|
+
});
|
|
4744
|
+
await savePlan(config.projectRoot, plan);
|
|
4745
|
+
console.log(formatRalphStatus(plan));
|
|
4746
|
+
} else if (subcommand === "status") {
|
|
4747
|
+
const plan = await loadPlan(config.projectRoot);
|
|
4748
|
+
if (!plan) {
|
|
4749
|
+
console.log(chalk8.gray(" No Ralph plan found."));
|
|
4750
|
+
} else {
|
|
4751
|
+
console.log(formatRalphStatus(plan));
|
|
4752
|
+
}
|
|
4753
|
+
} else {
|
|
4754
|
+
console.error(chalk8.red(` Unknown: notch ralph ${subcommand}`));
|
|
4755
|
+
console.error(chalk8.gray(" Usage: notch ralph <plan|run|status>"));
|
|
4756
|
+
process.exit(1);
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
main().catch((err) => {
|
|
4760
|
+
console.error(chalk8.red(`
|
|
4761
|
+
Fatal: ${err.message}
|
|
4762
|
+
`));
|
|
4763
|
+
process.exit(1);
|
|
4764
|
+
});
|