@freesyntax/notch-cli 0.5.14 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-6M6CXXWR.js +213 -0
- package/dist/compression-LPFNGAV6.js +17 -0
- package/dist/index.js +291 -112
- package/package.json +1 -1
- package/dist/chunk-MWM5TFY4.js +0 -142
- package/dist/compression-CXJN2ZYN.js +0 -11
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// src/agent/compression.ts
|
|
2
|
+
import { generateText } from "ai";
|
|
3
|
+
function estimateTokens(messages) {
|
|
4
|
+
let chars = 0;
|
|
5
|
+
for (const msg of messages) {
|
|
6
|
+
if (typeof msg.content === "string") {
|
|
7
|
+
chars += msg.content.length;
|
|
8
|
+
} else if (Array.isArray(msg.content)) {
|
|
9
|
+
for (const part of msg.content) {
|
|
10
|
+
if ("text" in part) chars += part.text.length;
|
|
11
|
+
else if ("result" in part) chars += JSON.stringify(part.result).length;
|
|
12
|
+
else if ("args" in part) chars += JSON.stringify(part.args).length;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return Math.ceil(chars / 4);
|
|
17
|
+
}
|
|
18
|
+
var TEXT_BLOCK_MAX = 8e3;
|
|
19
|
+
function microCompact(messages) {
|
|
20
|
+
const result = [];
|
|
21
|
+
for (let idx = 0; idx < messages.length; idx++) {
|
|
22
|
+
const msg = messages[idx];
|
|
23
|
+
if (idx === 0 || idx >= messages.length - 4) {
|
|
24
|
+
result.push(msg);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (typeof msg.content === "string" && msg.content.length > TEXT_BLOCK_MAX && msg.role === "assistant") {
|
|
28
|
+
result.push({
|
|
29
|
+
...msg,
|
|
30
|
+
content: msg.content.slice(0, TEXT_BLOCK_MAX) + "\n... [truncated]"
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
result.push(msg);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
var RESERVE_BUFFER_TOKENS = 13e3;
|
|
39
|
+
var MAX_SUMMARY_TOKENS = 2e4;
|
|
40
|
+
var MAX_COMPRESSION_FAILURES = 3;
|
|
41
|
+
var compressionFailures = 0;
|
|
42
|
+
async function autoCompactSummarize(messages, opts) {
|
|
43
|
+
const threshold = opts.contextWindow - RESERVE_BUFFER_TOKENS;
|
|
44
|
+
const currentTokens = estimateTokens(messages);
|
|
45
|
+
if (currentTokens < threshold * 0.75 || messages.length < 6) {
|
|
46
|
+
return { messages, compressed: false };
|
|
47
|
+
}
|
|
48
|
+
if (compressionFailures >= MAX_COMPRESSION_FAILURES) {
|
|
49
|
+
return deterministicCompress(messages, opts.keepRecent ?? 4);
|
|
50
|
+
}
|
|
51
|
+
const keepRecent = opts.keepRecent ?? 4;
|
|
52
|
+
const keepStart = 1;
|
|
53
|
+
const head = messages.slice(0, keepStart);
|
|
54
|
+
const middle = messages.slice(keepStart, -keepRecent);
|
|
55
|
+
const tail = messages.slice(-keepRecent);
|
|
56
|
+
if (middle.length === 0) {
|
|
57
|
+
return { messages, compressed: false };
|
|
58
|
+
}
|
|
59
|
+
const middleText = summarizeMessages(middle);
|
|
60
|
+
let summaryText;
|
|
61
|
+
try {
|
|
62
|
+
const result = await generateText({
|
|
63
|
+
model: opts.model,
|
|
64
|
+
system: "You are a conversation summarizer. Condense the following conversation history into a brief summary. Preserve: files modified, key decisions, errors encountered, and the current task state. Be concise but thorough. Output only the summary, no preamble.",
|
|
65
|
+
messages: [{ role: "user", content: middleText }],
|
|
66
|
+
maxTokens: Math.min(1024, Math.floor(MAX_SUMMARY_TOKENS / 4))
|
|
67
|
+
});
|
|
68
|
+
summaryText = result.text;
|
|
69
|
+
compressionFailures = 0;
|
|
70
|
+
} catch {
|
|
71
|
+
compressionFailures++;
|
|
72
|
+
return deterministicCompress(messages, keepRecent);
|
|
73
|
+
}
|
|
74
|
+
return buildCompressedHistory(head, summaryText, tail);
|
|
75
|
+
}
|
|
76
|
+
async function fullCompact(messages, model) {
|
|
77
|
+
if (messages.length < 4) {
|
|
78
|
+
return { messages, compressed: false };
|
|
79
|
+
}
|
|
80
|
+
const allButLast2 = messages.slice(0, -2);
|
|
81
|
+
const last2 = messages.slice(-2);
|
|
82
|
+
const middleText = summarizeMessages(allButLast2);
|
|
83
|
+
let summaryText;
|
|
84
|
+
try {
|
|
85
|
+
const result = await generateText({
|
|
86
|
+
model,
|
|
87
|
+
system: "Compress this entire conversation into a dense summary. Include: the original task, all files created/modified, key decisions, current state, and any unresolved issues. Max 500 words.",
|
|
88
|
+
messages: [{ role: "user", content: middleText }],
|
|
89
|
+
maxTokens: 2048
|
|
90
|
+
});
|
|
91
|
+
summaryText = result.text;
|
|
92
|
+
} catch {
|
|
93
|
+
summaryText = buildDeterministicSummary(allButLast2);
|
|
94
|
+
}
|
|
95
|
+
return buildCompressedHistory([], summaryText, last2);
|
|
96
|
+
}
|
|
97
|
+
function deterministicCompress(messages, keepRecent) {
|
|
98
|
+
const head = messages.slice(0, 1);
|
|
99
|
+
const middle = messages.slice(1, -keepRecent);
|
|
100
|
+
const tail = messages.slice(-keepRecent);
|
|
101
|
+
if (middle.length === 0) return { messages, compressed: false };
|
|
102
|
+
const summaryText = buildDeterministicSummary(middle);
|
|
103
|
+
return buildCompressedHistory(head, summaryText, tail);
|
|
104
|
+
}
|
|
105
|
+
function buildDeterministicSummary(messages) {
|
|
106
|
+
const filesModified = /* @__PURE__ */ new Set();
|
|
107
|
+
const toolsUsed = /* @__PURE__ */ new Set();
|
|
108
|
+
const userRequests = [];
|
|
109
|
+
let errorCount = 0;
|
|
110
|
+
for (const msg of messages) {
|
|
111
|
+
if (msg.role === "user" && typeof msg.content === "string") {
|
|
112
|
+
userRequests.push(msg.content.slice(0, 100));
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(msg.content)) {
|
|
115
|
+
for (const part of msg.content) {
|
|
116
|
+
if ("toolName" in part) {
|
|
117
|
+
const p = part;
|
|
118
|
+
toolsUsed.add(String(p.toolName));
|
|
119
|
+
const args = p.args;
|
|
120
|
+
if (args?.path) filesModified.add(String(args.path));
|
|
121
|
+
}
|
|
122
|
+
if ("result" in part) {
|
|
123
|
+
const r = part;
|
|
124
|
+
const res = r.result;
|
|
125
|
+
if (res?.isError) errorCount++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const lines = ["Summary of previous conversation:"];
|
|
131
|
+
if (userRequests.length > 0) lines.push(`- User requests: ${userRequests.join("; ")}`);
|
|
132
|
+
if (toolsUsed.size > 0) lines.push(`- Tools used: ${[...toolsUsed].join(", ")}`);
|
|
133
|
+
if (filesModified.size > 0) lines.push(`- Files touched: ${[...filesModified].join(", ")}`);
|
|
134
|
+
if (errorCount > 0) lines.push(`- Errors encountered: ${errorCount}`);
|
|
135
|
+
lines.push(`- Total messages summarized: ${messages.length}`);
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function summarizeMessages(messages) {
|
|
139
|
+
const lines = [];
|
|
140
|
+
for (const msg of messages) {
|
|
141
|
+
const role = msg.role.toUpperCase();
|
|
142
|
+
if (typeof msg.content === "string") {
|
|
143
|
+
lines.push(`${role}: ${msg.content.slice(0, 500)}`);
|
|
144
|
+
} else if (Array.isArray(msg.content)) {
|
|
145
|
+
const parts = [];
|
|
146
|
+
for (const part of msg.content) {
|
|
147
|
+
if ("text" in part) parts.push(part.text.slice(0, 200));
|
|
148
|
+
else if ("toolName" in part) parts.push(`[tool: ${part.toolName}]`);
|
|
149
|
+
else if ("result" in part) parts.push(`[result: ${JSON.stringify(part.result).slice(0, 100)}]`);
|
|
150
|
+
}
|
|
151
|
+
lines.push(`${role}: ${parts.join(" | ")}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
function buildCompressedHistory(head, summaryText, tail) {
|
|
157
|
+
const compressed = [...head];
|
|
158
|
+
const summaryContent = `[Previous conversation context]
|
|
159
|
+
${summaryText}
|
|
160
|
+
[End of context]`;
|
|
161
|
+
if (tail.length > 0 && tail[0].role === "user") {
|
|
162
|
+
const firstContent = typeof tail[0].content === "string" ? tail[0].content : "";
|
|
163
|
+
compressed.push({
|
|
164
|
+
role: "user",
|
|
165
|
+
content: `${summaryContent}
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
${firstContent}`
|
|
170
|
+
});
|
|
171
|
+
compressed.push(...tail.slice(1));
|
|
172
|
+
} else {
|
|
173
|
+
compressed.push({ role: "user", content: summaryContent });
|
|
174
|
+
compressed.push({
|
|
175
|
+
role: "assistant",
|
|
176
|
+
content: "Understood. I have the context from our previous conversation. Continuing."
|
|
177
|
+
});
|
|
178
|
+
compressed.push(...tail);
|
|
179
|
+
}
|
|
180
|
+
return { messages: compressed, compressed: true };
|
|
181
|
+
}
|
|
182
|
+
async function autoCompress(messages, model, contextWindow, onCompress) {
|
|
183
|
+
let result = microCompact(messages);
|
|
184
|
+
const threshold = (contextWindow - RESERVE_BUFFER_TOKENS) * 0.75;
|
|
185
|
+
let tokens = estimateTokens(result);
|
|
186
|
+
if (tokens < threshold) return result;
|
|
187
|
+
const auto = await autoCompactSummarize(result, { model, contextWindow });
|
|
188
|
+
if (auto.compressed) {
|
|
189
|
+
onCompress?.();
|
|
190
|
+
result = auto.messages;
|
|
191
|
+
tokens = estimateTokens(result);
|
|
192
|
+
}
|
|
193
|
+
if (tokens < threshold) return result;
|
|
194
|
+
const full = await fullCompact(result, model);
|
|
195
|
+
if (full.compressed) {
|
|
196
|
+
onCompress?.();
|
|
197
|
+
result = full.messages;
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
async function compressHistory(messages, opts) {
|
|
202
|
+
const result = await autoCompactSummarize(messages, opts);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
estimateTokens,
|
|
208
|
+
microCompact,
|
|
209
|
+
autoCompactSummarize,
|
|
210
|
+
fullCompact,
|
|
211
|
+
autoCompress,
|
|
212
|
+
compressHistory
|
|
213
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
autoCompactSummarize,
|
|
3
|
+
autoCompress,
|
|
4
|
+
compressHistory,
|
|
5
|
+
estimateTokens,
|
|
6
|
+
fullCompact,
|
|
7
|
+
microCompact
|
|
8
|
+
} from "./chunk-6M6CXXWR.js";
|
|
9
|
+
import "./chunk-3RG5ZIWI.js";
|
|
10
|
+
export {
|
|
11
|
+
autoCompactSummarize,
|
|
12
|
+
autoCompress,
|
|
13
|
+
compressHistory,
|
|
14
|
+
estimateTokens,
|
|
15
|
+
fullCompact,
|
|
16
|
+
microCompact
|
|
17
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
autoCompress,
|
|
9
9
|
estimateTokens
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-6M6CXXWR.js";
|
|
11
11
|
import {
|
|
12
12
|
__require
|
|
13
13
|
} from "./chunk-3RG5ZIWI.js";
|
|
@@ -77,15 +77,6 @@ var MODEL_CATALOG = {
|
|
|
77
77
|
maxOutputTokens: 16384,
|
|
78
78
|
baseUrl: "https://cutmob--notch-serve-solace-notchsolaceserver-serve.modal.run/v1"
|
|
79
79
|
},
|
|
80
|
-
"notch-forge-lite": {
|
|
81
|
-
id: "notch-forge-lite",
|
|
82
|
-
label: "Forge Lite",
|
|
83
|
-
size: "9B",
|
|
84
|
-
gpu: "L4",
|
|
85
|
-
contextWindow: 131072,
|
|
86
|
-
maxOutputTokens: 16384,
|
|
87
|
-
baseUrl: "https://cutmob--notch-serve-forge-lite-notchforgeliteserver-serve.modal.run/v1"
|
|
88
|
-
},
|
|
89
80
|
"notch-solace-lite": {
|
|
90
81
|
id: "notch-solace-lite",
|
|
91
82
|
label: "Solace Lite",
|
|
@@ -417,36 +408,94 @@ var editTool = {
|
|
|
417
408
|
};
|
|
418
409
|
|
|
419
410
|
// src/tools/shell.ts
|
|
420
|
-
import {
|
|
411
|
+
import { execFile, exec } from "child_process";
|
|
412
|
+
import { promisify } from "util";
|
|
421
413
|
import path5 from "path";
|
|
422
414
|
import { z as z4 } from "zod";
|
|
415
|
+
var execFileAsync = promisify(execFile);
|
|
416
|
+
var execAsync = promisify(exec);
|
|
423
417
|
var BLOCKED_PATTERNS = [
|
|
424
418
|
/rm\s+-rf\s+\/(?!\S)/,
|
|
425
419
|
// rm -rf /
|
|
426
420
|
/mkfs\./,
|
|
421
|
+
// format filesystem
|
|
427
422
|
/dd\s+if=.*of=\/dev/,
|
|
423
|
+
// raw disk write
|
|
428
424
|
/:\(\)\s*\{.*:\|:.*\}/,
|
|
429
|
-
// fork bomb
|
|
430
|
-
/chmod\s+-R\s+777\s
|
|
425
|
+
// fork bomb
|
|
426
|
+
/chmod\s+-R\s+777\s+\//,
|
|
431
427
|
// recursive chmod on root
|
|
428
|
+
/curl\s.*\|\s*(?:ba)?sh/,
|
|
429
|
+
// curl | sh (remote code execution)
|
|
430
|
+
/wget\s.*\|\s*(?:ba)?sh/,
|
|
431
|
+
// wget | sh
|
|
432
|
+
/>\s*\/dev\/sd[a-z]/,
|
|
433
|
+
// overwrite disk device
|
|
434
|
+
/shutdown|reboot|init\s+[06]/,
|
|
435
|
+
// system shutdown/reboot
|
|
436
|
+
/rm\s+-rf\s+~\//
|
|
437
|
+
// rm -rf ~/
|
|
432
438
|
];
|
|
433
439
|
var DESTRUCTIVE_PATTERNS = [
|
|
434
440
|
/rm\s+-rf/,
|
|
435
441
|
/rm\s+-r\s/,
|
|
436
442
|
/git\s+push\s+--force(?!\s+--with-lease)/,
|
|
437
443
|
/git\s+reset\s+--hard/,
|
|
444
|
+
/git\s+clean\s+-f/,
|
|
445
|
+
/git\s+checkout\s+--\s*\./,
|
|
438
446
|
/DROP\s+(TABLE|DATABASE)/i,
|
|
439
447
|
/TRUNCATE/i,
|
|
440
|
-
/>\s*\/dev\/sd
|
|
448
|
+
/>\s*\/dev\/sd/,
|
|
449
|
+
/docker\s+(rm|rmi|system\s+prune)/,
|
|
450
|
+
/kubectl\s+delete/,
|
|
451
|
+
/npm\s+unpublish/
|
|
452
|
+
];
|
|
453
|
+
var BLOCKED_ENV_PATTERNS = [
|
|
454
|
+
/\bLD_PRELOAD=/,
|
|
455
|
+
/\bLD_LIBRARY_PATH=/,
|
|
456
|
+
/\bDYLD_INSERT_LIBRARIES=/,
|
|
457
|
+
/\bPATH=\//,
|
|
458
|
+
// Setting PATH to absolute (could shadow binaries)
|
|
459
|
+
/\bHOME=\//,
|
|
460
|
+
/\bSHELL=/
|
|
441
461
|
];
|
|
442
462
|
var MAX_OUTPUT = 5e4;
|
|
443
463
|
var DEFAULT_TIMEOUT = 3e4;
|
|
444
464
|
var MAX_TIMEOUT = 6e5;
|
|
445
465
|
var parameters4 = z4.object({
|
|
446
466
|
command: z4.string().describe("Shell command to execute"),
|
|
447
|
-
timeout: z4.number().optional().describe("Timeout in ms (default 30s, max
|
|
467
|
+
timeout: z4.number().optional().describe("Timeout in ms (default 30s, max 10m)")
|
|
448
468
|
});
|
|
449
469
|
function validateCommand(command, cwd) {
|
|
470
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
471
|
+
if (pattern.test(command)) {
|
|
472
|
+
return `Blocked: this command is too dangerous to execute.`;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
for (const pattern of BLOCKED_ENV_PATTERNS) {
|
|
476
|
+
if (pattern.test(command)) {
|
|
477
|
+
return `Blocked: command attempts to override a protected environment variable.`;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const pipeSegments = command.split(/\s*\|\s*/);
|
|
481
|
+
for (const segment of pipeSegments) {
|
|
482
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
483
|
+
if (pattern.test(segment.trim())) {
|
|
484
|
+
return `Blocked: a pipe segment contains a dangerous command.`;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const subCommands = [
|
|
489
|
+
...command.matchAll(/\$\(([^)]+)\)/g),
|
|
490
|
+
...command.matchAll(/`([^`]+)`/g)
|
|
491
|
+
];
|
|
492
|
+
for (const match2 of subCommands) {
|
|
493
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
494
|
+
if (pattern.test(match2[1])) {
|
|
495
|
+
return `Blocked: command substitution contains a dangerous command.`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
450
499
|
const fileOpRegex = /(?:^|\s)(?:>|>>|cat|cp|mv|ln|tee|tar|zip|scp|rsync|chmod|chown|rm)\s+(\/(?!tmp\b|dev\/null\b)[^\s]+)/g;
|
|
451
500
|
let match;
|
|
452
501
|
while ((match = fileOpRegex.exec(command)) !== null) {
|
|
@@ -456,10 +505,19 @@ function validateCommand(command, cwd) {
|
|
|
456
505
|
}
|
|
457
506
|
}
|
|
458
507
|
if (/(?:^|\s)(?:\.\.\/){3,}/.test(command)) {
|
|
459
|
-
return "Blocked: deep path traversal detected";
|
|
508
|
+
return "Blocked: deep path traversal detected.";
|
|
460
509
|
}
|
|
461
510
|
return null;
|
|
462
511
|
}
|
|
512
|
+
function isDestructive(command) {
|
|
513
|
+
const segments = [command, ...command.split(/\s*\|\s*/)];
|
|
514
|
+
for (const segment of segments) {
|
|
515
|
+
for (const pattern of DESTRUCTIVE_PATTERNS) {
|
|
516
|
+
if (pattern.test(segment)) return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
463
521
|
var shellTool = {
|
|
464
522
|
name: "shell",
|
|
465
523
|
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.",
|
|
@@ -468,50 +526,38 @@ var shellTool = {
|
|
|
468
526
|
const { command } = params;
|
|
469
527
|
const maxTimeout = ctx.shellTimeout ?? MAX_TIMEOUT;
|
|
470
528
|
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, maxTimeout);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
content: `Blocked: "${command}" is too dangerous to execute.`,
|
|
475
|
-
isError: true
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
const pathError = validateCommand(command, ctx.cwd);
|
|
480
|
-
if (pathError) {
|
|
481
|
-
return { content: pathError, isError: true };
|
|
529
|
+
const validationError = validateCommand(command, ctx.cwd);
|
|
530
|
+
if (validationError) {
|
|
531
|
+
return { content: validationError, isError: true };
|
|
482
532
|
}
|
|
483
|
-
if (ctx.requireConfirm) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const confirmed = await ctx.confirm(
|
|
487
|
-
`\u26A0 Destructive command: ${command}
|
|
533
|
+
if (ctx.requireConfirm && isDestructive(command)) {
|
|
534
|
+
const confirmed = await ctx.confirm(
|
|
535
|
+
`\u26A0 Destructive command: ${command}
|
|
488
536
|
Proceed?`
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
537
|
+
);
|
|
538
|
+
if (!confirmed) {
|
|
539
|
+
return { content: "Command cancelled by user.", isError: true };
|
|
495
540
|
}
|
|
496
541
|
}
|
|
497
542
|
try {
|
|
498
|
-
const
|
|
543
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
499
544
|
cwd: ctx.cwd,
|
|
500
545
|
encoding: "utf-8",
|
|
501
546
|
timeout,
|
|
502
547
|
maxBuffer: 10 * 1024 * 1024,
|
|
503
|
-
env: { ...process.env, FORCE_COLOR: "0" }
|
|
504
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
548
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
505
549
|
});
|
|
506
|
-
const
|
|
507
|
-
|
|
550
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
551
|
+
const trimmed = combined.length > MAX_OUTPUT ? combined.slice(0, MAX_OUTPUT) + `
|
|
552
|
+
... (truncated, ${combined.length} chars total)` : combined;
|
|
508
553
|
return { content: trimmed || "(no output)" };
|
|
509
554
|
} catch (err) {
|
|
510
|
-
const
|
|
511
|
-
const
|
|
555
|
+
const e = err;
|
|
556
|
+
const stderr = e.stderr ?? "";
|
|
557
|
+
const stdout = e.stdout ?? "";
|
|
512
558
|
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
513
559
|
const trimmed = combined.length > MAX_OUTPUT ? combined.slice(0, MAX_OUTPUT) + "\n... (truncated)" : combined;
|
|
514
|
-
if (
|
|
560
|
+
if (e.killed && e.signal === "SIGTERM") {
|
|
515
561
|
return {
|
|
516
562
|
content: `Command timed out after ${(timeout / 1e3).toFixed(0)}s: ${command}
|
|
517
563
|
|
|
@@ -521,8 +567,8 @@ ${trimmed || "(none)"}`,
|
|
|
521
567
|
};
|
|
522
568
|
}
|
|
523
569
|
return {
|
|
524
|
-
content: `Command failed (exit ${
|
|
525
|
-
${trimmed ||
|
|
570
|
+
content: `Command failed (exit ${e.status ?? e.code ?? "unknown"}):
|
|
571
|
+
${trimmed || e.message || "Unknown error"}`,
|
|
526
572
|
isError: true
|
|
527
573
|
};
|
|
528
574
|
}
|
|
@@ -666,7 +712,7 @@ Proceed?`
|
|
|
666
712
|
};
|
|
667
713
|
|
|
668
714
|
// src/tools/grep.ts
|
|
669
|
-
import { execSync
|
|
715
|
+
import { execSync } from "child_process";
|
|
670
716
|
import fs5 from "fs/promises";
|
|
671
717
|
import path6 from "path";
|
|
672
718
|
import { z as z6 } from "zod";
|
|
@@ -696,7 +742,7 @@ var grepTool = {
|
|
|
696
742
|
JSON.stringify(params.pattern),
|
|
697
743
|
searchPath
|
|
698
744
|
].filter(Boolean).join(" ");
|
|
699
|
-
const output =
|
|
745
|
+
const output = execSync(rgArgs, {
|
|
700
746
|
cwd: ctx.cwd,
|
|
701
747
|
encoding: "utf-8",
|
|
702
748
|
timeout: 15e3,
|
|
@@ -2404,6 +2450,15 @@ async function updateIndex() {
|
|
|
2404
2450
|
}
|
|
2405
2451
|
|
|
2406
2452
|
// src/agent/loop.ts
|
|
2453
|
+
function getErrorSignature(toolName, result) {
|
|
2454
|
+
return {
|
|
2455
|
+
toolName,
|
|
2456
|
+
errorPrefix: result.slice(0, 120)
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function signaturesMatch(a, b) {
|
|
2460
|
+
return a.toolName === b.toolName && a.errorPrefix === b.errorPrefix;
|
|
2461
|
+
}
|
|
2407
2462
|
async function runAgentLoop(messages, config) {
|
|
2408
2463
|
const readCache = /* @__PURE__ */ new Map();
|
|
2409
2464
|
const toolCtxWithCache = {
|
|
@@ -2418,6 +2473,8 @@ async function runAgentLoop(messages, config) {
|
|
|
2418
2473
|
let totalPromptTokens = 0;
|
|
2419
2474
|
let totalCompletionTokens = 0;
|
|
2420
2475
|
let wasCompressed = false;
|
|
2476
|
+
const recentErrors = [];
|
|
2477
|
+
const MAX_REPEATED_ERRORS = 3;
|
|
2421
2478
|
let history = [...messages];
|
|
2422
2479
|
await config.toolContext.runHook?.("pre-compact", { messageCount: history.length });
|
|
2423
2480
|
history = await autoCompress(history, config.model, contextWindow, () => {
|
|
@@ -2429,17 +2486,17 @@ async function runAgentLoop(messages, config) {
|
|
|
2429
2486
|
}
|
|
2430
2487
|
while (iterations < maxIter) {
|
|
2431
2488
|
iterations++;
|
|
2489
|
+
let fullText = "";
|
|
2490
|
+
const toolCalls = [];
|
|
2491
|
+
const toolResults = [];
|
|
2492
|
+
let streamUsage = null;
|
|
2432
2493
|
const result = streamText({
|
|
2433
2494
|
model: config.model,
|
|
2434
2495
|
system: config.systemPrompt,
|
|
2435
2496
|
messages: history,
|
|
2436
2497
|
tools,
|
|
2437
2498
|
maxSteps: 1
|
|
2438
|
-
// We manage the loop ourselves for better control
|
|
2439
2499
|
});
|
|
2440
|
-
let fullText = "";
|
|
2441
|
-
const toolCalls = [];
|
|
2442
|
-
const toolResults = [];
|
|
2443
2500
|
for await (const event of result.fullStream) {
|
|
2444
2501
|
if (event.type === "text-delta") {
|
|
2445
2502
|
fullText += event.textDelta;
|
|
@@ -2468,14 +2525,28 @@ async function runAgentLoop(messages, config) {
|
|
|
2468
2525
|
}
|
|
2469
2526
|
try {
|
|
2470
2527
|
const u = await result.usage;
|
|
2471
|
-
if (u)
|
|
2472
|
-
totalPromptTokens += u.promptTokens ?? 0;
|
|
2473
|
-
totalCompletionTokens += u.completionTokens ?? 0;
|
|
2474
|
-
}
|
|
2528
|
+
if (u) streamUsage = u;
|
|
2475
2529
|
} catch {
|
|
2476
2530
|
}
|
|
2531
|
+
if (streamUsage) {
|
|
2532
|
+
totalPromptTokens += streamUsage.promptTokens ?? 0;
|
|
2533
|
+
totalCompletionTokens += streamUsage.completionTokens ?? 0;
|
|
2534
|
+
}
|
|
2477
2535
|
totalToolCalls += toolCalls.length;
|
|
2478
2536
|
if (toolCalls.length > 0) {
|
|
2537
|
+
let hasRepeatedError = false;
|
|
2538
|
+
for (const tr of toolResults) {
|
|
2539
|
+
const res = tr.result;
|
|
2540
|
+
if (res?.isError) {
|
|
2541
|
+
const toolName = toolCalls.find((tc) => tc.toolCallId === tr.toolCallId)?.toolName ?? "unknown";
|
|
2542
|
+
const sig = getErrorSignature(toolName, res.content ?? "");
|
|
2543
|
+
const repeated = recentErrors.filter((e) => signaturesMatch(e, sig)).length;
|
|
2544
|
+
recentErrors.push(sig);
|
|
2545
|
+
if (repeated >= MAX_REPEATED_ERRORS - 1) {
|
|
2546
|
+
hasRepeatedError = true;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2479
2550
|
history.push({
|
|
2480
2551
|
role: "assistant",
|
|
2481
2552
|
content: [
|
|
@@ -2497,6 +2568,13 @@ async function runAgentLoop(messages, config) {
|
|
|
2497
2568
|
result: tr.result
|
|
2498
2569
|
}))
|
|
2499
2570
|
});
|
|
2571
|
+
if (hasRepeatedError) {
|
|
2572
|
+
history.push({
|
|
2573
|
+
role: "user",
|
|
2574
|
+
content: "[System: You have repeated the same failing tool call multiple times. Stop and try a different approach. If the tool is unavailable or the command keeps failing, explain the issue to the user instead of retrying.]"
|
|
2575
|
+
});
|
|
2576
|
+
recentErrors.length = 0;
|
|
2577
|
+
}
|
|
2500
2578
|
if (iterations % 5 === 0) {
|
|
2501
2579
|
const prevLen = history.length;
|
|
2502
2580
|
await config.toolContext.runHook?.("pre-compact", { messageCount: prevLen });
|
|
@@ -2566,7 +2644,8 @@ async function buildSystemPrompt(projectRoot, modelId) {
|
|
|
2566
2644
|
"- Explain what you're doing before making changes.",
|
|
2567
2645
|
"- If a task is complex, break it into steps.",
|
|
2568
2646
|
"- When running shell commands, prefer non-destructive operations.",
|
|
2569
|
-
"- If you encounter an error, analyze it and suggest a fix."
|
|
2647
|
+
"- If you encounter an error, analyze it and suggest a fix.",
|
|
2648
|
+
"- If the same tool call keeps failing, stop retrying and try a different approach."
|
|
2570
2649
|
];
|
|
2571
2650
|
try {
|
|
2572
2651
|
const instructions = await loadProjectInstructions(projectRoot);
|
|
@@ -3054,20 +3133,18 @@ function isPlanComplete(plan) {
|
|
|
3054
3133
|
// src/agent/cost.ts
|
|
3055
3134
|
import chalk4 from "chalk";
|
|
3056
3135
|
var MODEL_COSTS = {
|
|
3057
|
-
"notch-cinder": { input:
|
|
3058
|
-
// L4
|
|
3059
|
-
"notch-forge": { input:
|
|
3060
|
-
// L40S
|
|
3061
|
-
"notch-pyre": { input:
|
|
3062
|
-
// A100
|
|
3063
|
-
"notch-ignis": { input:
|
|
3064
|
-
// A100
|
|
3065
|
-
"notch-solace": { input:
|
|
3066
|
-
// A100
|
|
3067
|
-
"notch-
|
|
3068
|
-
// L4
|
|
3069
|
-
"notch-solace-lite": { input: 0.04, output: 0.12 }
|
|
3070
|
-
// L4 — Gemma 4 E4B
|
|
3136
|
+
"notch-cinder": { input: 1.59, output: 7.66 },
|
|
3137
|
+
// L4 (benchmarked 2026-04-02)
|
|
3138
|
+
"notch-forge": { input: 3.17, output: 12.32 },
|
|
3139
|
+
// L40S (benchmarked 2026-04-02)
|
|
3140
|
+
"notch-pyre": { input: 4.34, output: 15.42 },
|
|
3141
|
+
// A100-80GB (benchmarked 2026-04-02)
|
|
3142
|
+
"notch-ignis": { input: 2.86, output: 25.7 },
|
|
3143
|
+
// A100-80GB (benchmarked 2026-04-02)
|
|
3144
|
+
"notch-solace": { input: 4.63, output: 36.15 },
|
|
3145
|
+
// A100-80GB (benchmarked 2026-04-06)
|
|
3146
|
+
"notch-solace-lite": { input: 1.11, output: 10.28 }
|
|
3147
|
+
// L4 (benchmarked 2026-04-05)
|
|
3071
3148
|
};
|
|
3072
3149
|
var CostTracker = class {
|
|
3073
3150
|
entries = [];
|
|
@@ -3216,16 +3293,45 @@ async function buildRepoMap(root) {
|
|
|
3216
3293
|
"**/.git/**",
|
|
3217
3294
|
"**/dist/**",
|
|
3218
3295
|
"**/build/**",
|
|
3296
|
+
"**/out/**",
|
|
3297
|
+
"**/.next/**",
|
|
3298
|
+
"**/.nuxt/**",
|
|
3299
|
+
"**/.output/**",
|
|
3219
3300
|
"**/__pycache__/**",
|
|
3220
3301
|
"**/target/**",
|
|
3302
|
+
"**/.venv/**",
|
|
3303
|
+
"**/venv/**",
|
|
3221
3304
|
"**/*.test.*",
|
|
3222
3305
|
"**/*.spec.*",
|
|
3223
|
-
"**/*.d.ts"
|
|
3306
|
+
"**/*.d.ts",
|
|
3307
|
+
"**/*.config.*",
|
|
3308
|
+
// vite.config, tailwind.config, etc.
|
|
3309
|
+
"**/*.setup.*",
|
|
3310
|
+
// vitest.setup, jest.setup
|
|
3311
|
+
"**/postcss.config.*",
|
|
3312
|
+
"**/tsconfig.*",
|
|
3313
|
+
"**/.eslintrc.*",
|
|
3314
|
+
"**/.prettierrc.*",
|
|
3315
|
+
"**/env.d.ts",
|
|
3316
|
+
"**/vite-env.d.ts",
|
|
3317
|
+
"**/global.d.ts",
|
|
3318
|
+
"**/globals.d.ts",
|
|
3319
|
+
"**/next-env.d.ts",
|
|
3320
|
+
"**/migrations/**",
|
|
3321
|
+
"**/generated/**",
|
|
3322
|
+
"**/*.min.*",
|
|
3323
|
+
"**/vendor/**",
|
|
3324
|
+
"**/public/**",
|
|
3325
|
+
"**/coverage/**",
|
|
3326
|
+
"**/.cache/**",
|
|
3327
|
+
"**/lock.*",
|
|
3328
|
+
"**/*-lock.*"
|
|
3224
3329
|
],
|
|
3225
3330
|
nodir: true
|
|
3226
3331
|
});
|
|
3227
3332
|
const entries = [];
|
|
3228
|
-
|
|
3333
|
+
files.sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
|
|
3334
|
+
for (const file of files.slice(0, 300)) {
|
|
3229
3335
|
const fullPath = path15.resolve(root, file);
|
|
3230
3336
|
try {
|
|
3231
3337
|
const content = await fs12.readFile(fullPath, "utf-8");
|
|
@@ -4298,7 +4404,7 @@ function formatTokens(n) {
|
|
|
4298
4404
|
import fs15 from "fs/promises";
|
|
4299
4405
|
import path18 from "path";
|
|
4300
4406
|
import os4 from "os";
|
|
4301
|
-
import { execSync as
|
|
4407
|
+
import { execSync as execSync2 } from "child_process";
|
|
4302
4408
|
import chalk8 from "chalk";
|
|
4303
4409
|
var CACHE_FILE = path18.join(os4.homedir(), ".notch", "update-check.json");
|
|
4304
4410
|
var CHECK_INTERVAL = 0;
|
|
@@ -4337,7 +4443,7 @@ function autoUpdate(current, latest) {
|
|
|
4337
4443
|
\u2B06 Updating Notch CLI: ${current} \u2192 ${latest}...
|
|
4338
4444
|
`));
|
|
4339
4445
|
try {
|
|
4340
|
-
|
|
4446
|
+
execSync2(`npm install -g ${PACKAGE_NAME}@${latest}`, {
|
|
4341
4447
|
stdio: "inherit",
|
|
4342
4448
|
timeout: 6e4
|
|
4343
4449
|
});
|
|
@@ -4456,7 +4562,7 @@ function mergePermissions(base, override) {
|
|
|
4456
4562
|
}
|
|
4457
4563
|
|
|
4458
4564
|
// src/hooks/index.ts
|
|
4459
|
-
import { execSync as
|
|
4565
|
+
import { execSync as execSync3 } from "child_process";
|
|
4460
4566
|
import fs17 from "fs/promises";
|
|
4461
4567
|
import { watch } from "fs";
|
|
4462
4568
|
import path20 from "path";
|
|
@@ -4564,7 +4670,7 @@ async function executeHook(hook, context) {
|
|
|
4564
4670
|
NOTCH_CWD: context.cwd
|
|
4565
4671
|
};
|
|
4566
4672
|
try {
|
|
4567
|
-
const output =
|
|
4673
|
+
const output = execSync3(hook.command, {
|
|
4568
4674
|
cwd: context.cwd,
|
|
4569
4675
|
encoding: "utf-8",
|
|
4570
4676
|
timeout: hook.timeout ?? 1e4,
|
|
@@ -4936,7 +5042,7 @@ function findSync(oldLines, newLines, oi, ni, lookAhead) {
|
|
|
4936
5042
|
}
|
|
4937
5043
|
|
|
4938
5044
|
// src/commands/doctor.ts
|
|
4939
|
-
import { execSync as
|
|
5045
|
+
import { execSync as execSync4 } from "child_process";
|
|
4940
5046
|
import fs20 from "fs/promises";
|
|
4941
5047
|
import path23 from "path";
|
|
4942
5048
|
import os8 from "os";
|
|
@@ -4953,7 +5059,7 @@ async function runDiagnostics(cwd) {
|
|
|
4953
5059
|
results.push({ name: "Node.js", status: "fail", message: `v${nodeVersion} (requires >= 18)` });
|
|
4954
5060
|
}
|
|
4955
5061
|
try {
|
|
4956
|
-
const gitVersion =
|
|
5062
|
+
const gitVersion = execSync4("git --version", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
4957
5063
|
results.push({ name: "Git", status: "ok", message: gitVersion });
|
|
4958
5064
|
} catch {
|
|
4959
5065
|
results.push({ name: "Git", status: "fail", message: "Not found. Install git to use git tools." });
|
|
@@ -5055,23 +5161,23 @@ registerCommand("/doctor", async (_args, ctx) => {
|
|
|
5055
5161
|
});
|
|
5056
5162
|
|
|
5057
5163
|
// src/commands/copy.ts
|
|
5058
|
-
import { execSync as
|
|
5164
|
+
import { execSync as execSync5 } from "child_process";
|
|
5059
5165
|
import chalk11 from "chalk";
|
|
5060
5166
|
function copyToClipboard(text) {
|
|
5061
5167
|
try {
|
|
5062
5168
|
const platform = process.platform;
|
|
5063
5169
|
if (platform === "win32") {
|
|
5064
|
-
|
|
5170
|
+
execSync5("clip.exe", { input: text, timeout: 5e3 });
|
|
5065
5171
|
} else if (platform === "darwin") {
|
|
5066
|
-
|
|
5172
|
+
execSync5("pbcopy", { input: text, timeout: 5e3 });
|
|
5067
5173
|
} else {
|
|
5068
5174
|
try {
|
|
5069
|
-
|
|
5175
|
+
execSync5("xclip -selection clipboard", { input: text, timeout: 5e3 });
|
|
5070
5176
|
} catch {
|
|
5071
5177
|
try {
|
|
5072
|
-
|
|
5178
|
+
execSync5("xsel --clipboard --input", { input: text, timeout: 5e3 });
|
|
5073
5179
|
} catch {
|
|
5074
|
-
|
|
5180
|
+
execSync5("wl-copy", { input: text, timeout: 5e3 });
|
|
5075
5181
|
}
|
|
5076
5182
|
}
|
|
5077
5183
|
}
|
|
@@ -5117,7 +5223,7 @@ registerCommand("/btw", async (args, ctx) => {
|
|
|
5117
5223
|
});
|
|
5118
5224
|
|
|
5119
5225
|
// src/commands/security-review.ts
|
|
5120
|
-
import { execFileSync as execFileSync2, execSync as
|
|
5226
|
+
import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
|
|
5121
5227
|
import chalk13 from "chalk";
|
|
5122
5228
|
function isValidGitRange(range) {
|
|
5123
5229
|
return /^[a-zA-Z0-9._~^\/\-]+(\.\.[a-zA-Z0-9._~^\/\-]+)?$/.test(range);
|
|
@@ -5145,12 +5251,12 @@ registerCommand("/security-review", async (args, ctx) => {
|
|
|
5145
5251
|
}).trim();
|
|
5146
5252
|
} catch {
|
|
5147
5253
|
try {
|
|
5148
|
-
stat =
|
|
5254
|
+
stat = execSync6("git diff --stat", {
|
|
5149
5255
|
cwd: ctx.cwd,
|
|
5150
5256
|
encoding: "utf-8",
|
|
5151
5257
|
timeout: 1e4
|
|
5152
5258
|
}).trim();
|
|
5153
|
-
diff =
|
|
5259
|
+
diff = execSync6("git diff", {
|
|
5154
5260
|
cwd: ctx.cwd,
|
|
5155
5261
|
encoding: "utf-8",
|
|
5156
5262
|
timeout: 1e4,
|
|
@@ -5422,7 +5528,7 @@ Read the file first, then make the change. Only modify this one file.`
|
|
|
5422
5528
|
});
|
|
5423
5529
|
|
|
5424
5530
|
// src/commands/plugin.ts
|
|
5425
|
-
import { execSync as
|
|
5531
|
+
import { execSync as execSync7, execFileSync as execFileSync3 } from "child_process";
|
|
5426
5532
|
import fs21 from "fs/promises";
|
|
5427
5533
|
import path24 from "path";
|
|
5428
5534
|
import os9 from "os";
|
|
@@ -5491,7 +5597,7 @@ registerCommand("/plugin", async (args, ctx) => {
|
|
|
5491
5597
|
try {
|
|
5492
5598
|
const pkgExists = await fs21.access(path24.join(pluginDir, "package.json")).then(() => true).catch(() => false);
|
|
5493
5599
|
if (pkgExists) {
|
|
5494
|
-
|
|
5600
|
+
execSync7("npm install --production", {
|
|
5495
5601
|
cwd: pluginDir,
|
|
5496
5602
|
encoding: "utf-8",
|
|
5497
5603
|
timeout: 12e4,
|
|
@@ -5642,12 +5748,12 @@ Reply with ONLY the commit message, nothing else. No markdown, no explanation.`;
|
|
|
5642
5748
|
});
|
|
5643
5749
|
|
|
5644
5750
|
// src/commands/pr.ts
|
|
5645
|
-
import { execSync as
|
|
5751
|
+
import { execSync as execSync9, execFileSync as execFileSync5 } from "child_process";
|
|
5646
5752
|
import chalk18 from "chalk";
|
|
5647
5753
|
import ora3 from "ora";
|
|
5648
5754
|
function tryExec(cmd, cwd) {
|
|
5649
5755
|
try {
|
|
5650
|
-
return
|
|
5756
|
+
return execSync9(cmd, { cwd, encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5651
5757
|
} catch {
|
|
5652
5758
|
return null;
|
|
5653
5759
|
}
|
|
@@ -5778,11 +5884,11 @@ BODY:
|
|
|
5778
5884
|
});
|
|
5779
5885
|
|
|
5780
5886
|
// src/commands/worktree.ts
|
|
5781
|
-
import { execSync as
|
|
5887
|
+
import { execSync as execSync10, execFileSync as execFileSync6 } from "child_process";
|
|
5782
5888
|
import chalk19 from "chalk";
|
|
5783
5889
|
function tryExec2(cmd, cwd) {
|
|
5784
5890
|
try {
|
|
5785
|
-
return
|
|
5891
|
+
return execSync10(cmd, { cwd, encoding: "utf-8", timeout: 15e3 }).trim();
|
|
5786
5892
|
} catch {
|
|
5787
5893
|
return null;
|
|
5788
5894
|
}
|
|
@@ -5893,7 +5999,7 @@ registerCommand("/worktree", async (args, ctx) => {
|
|
|
5893
5999
|
}
|
|
5894
6000
|
case "prune": {
|
|
5895
6001
|
try {
|
|
5896
|
-
|
|
6002
|
+
execSync10("git worktree prune", { cwd: ctx.cwd, encoding: "utf-8" });
|
|
5897
6003
|
console.log(chalk19.green(" \u2713 Pruned stale worktrees.\n"));
|
|
5898
6004
|
} catch (err) {
|
|
5899
6005
|
console.log(chalk19.red(` Failed: ${err.message}
|
|
@@ -6515,19 +6621,60 @@ var modelChoices = MODEL_IDS.join(", ");
|
|
|
6515
6621
|
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);
|
|
6516
6622
|
var opts = program.opts();
|
|
6517
6623
|
var promptArgs = program.args;
|
|
6518
|
-
function
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
const
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6624
|
+
function interactiveModelPicker(activeModel) {
|
|
6625
|
+
return new Promise((resolve2) => {
|
|
6626
|
+
const t = theme();
|
|
6627
|
+
let cursor = MODEL_IDS.indexOf(activeModel);
|
|
6628
|
+
if (cursor < 0) cursor = 0;
|
|
6629
|
+
const render = () => {
|
|
6630
|
+
process.stdout.write(`\x1B[${MODEL_IDS.length + 2}A\x1B[J`);
|
|
6631
|
+
draw();
|
|
6632
|
+
};
|
|
6633
|
+
const draw = () => {
|
|
6634
|
+
console.log(t.dim(" Select a model (\u2191\u2193 to move, Enter to select, Esc to cancel)\n"));
|
|
6635
|
+
for (let i = 0; i < MODEL_IDS.length; i++) {
|
|
6636
|
+
const id = MODEL_IDS[i];
|
|
6637
|
+
const info = MODEL_CATALOG[id];
|
|
6638
|
+
const isCurrent = id === activeModel;
|
|
6639
|
+
const isSelected = i === cursor;
|
|
6640
|
+
const pointer = isSelected ? t.brand("\u276F") : " ";
|
|
6641
|
+
const dot = isCurrent ? t.success("\u25CF") : " ";
|
|
6642
|
+
const label = isSelected ? t.bold(info.label) : t.dim(info.label);
|
|
6643
|
+
const size = t.dim(info.size);
|
|
6644
|
+
const gpu = t.dim(info.gpu);
|
|
6645
|
+
const ctx = t.dim(`${(info.contextWindow / 1024).toFixed(0)}K`);
|
|
6646
|
+
console.log(` ${pointer} ${dot} ${t.brand(id.replace("notch-", "").padEnd(12))} ${label.padEnd(20)} ${size.padEnd(6)} ${gpu.padEnd(12)} ${ctx}`);
|
|
6647
|
+
}
|
|
6648
|
+
};
|
|
6649
|
+
console.log("");
|
|
6650
|
+
draw();
|
|
6651
|
+
const stdin = process.stdin;
|
|
6652
|
+
const wasRaw = stdin.isRaw;
|
|
6653
|
+
stdin.setRawMode(true);
|
|
6654
|
+
stdin.resume();
|
|
6655
|
+
const onKey = (key) => {
|
|
6656
|
+
const s = key.toString();
|
|
6657
|
+
if (s === "\x1B[A") {
|
|
6658
|
+
cursor = (cursor - 1 + MODEL_IDS.length) % MODEL_IDS.length;
|
|
6659
|
+
render();
|
|
6660
|
+
} else if (s === "\x1B[B") {
|
|
6661
|
+
cursor = (cursor + 1) % MODEL_IDS.length;
|
|
6662
|
+
render();
|
|
6663
|
+
} else if (s === "\r" || s === "\n") {
|
|
6664
|
+
cleanup();
|
|
6665
|
+
resolve2(MODEL_IDS[cursor] ?? null);
|
|
6666
|
+
} else if (s === "\x1B" || s === "") {
|
|
6667
|
+
cleanup();
|
|
6668
|
+
resolve2(null);
|
|
6669
|
+
}
|
|
6670
|
+
};
|
|
6671
|
+
const cleanup = () => {
|
|
6672
|
+
stdin.removeListener("data", onKey);
|
|
6673
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
6674
|
+
process.stdout.write(`\x1B[${MODEL_IDS.length + 2}A\x1B[J`);
|
|
6675
|
+
};
|
|
6676
|
+
stdin.on("data", onKey);
|
|
6677
|
+
});
|
|
6531
6678
|
}
|
|
6532
6679
|
function printHelp() {
|
|
6533
6680
|
console.log(chalk27.gray(`
|
|
@@ -6973,7 +7120,23 @@ Analyze the above input.`;
|
|
|
6973
7120
|
return;
|
|
6974
7121
|
}
|
|
6975
7122
|
if (input === "/model" || input === "/models") {
|
|
6976
|
-
|
|
7123
|
+
rl.pause();
|
|
7124
|
+
const picked = await interactiveModelPicker(activeModelId);
|
|
7125
|
+
if (picked && picked !== activeModelId) {
|
|
7126
|
+
activeModelId = picked;
|
|
7127
|
+
config.models.chat.model = activeModelId;
|
|
7128
|
+
model = resolveModel(config.models.chat);
|
|
7129
|
+
const switchedInfo = MODEL_CATALOG[activeModelId];
|
|
7130
|
+
console.log(chalk27.green(` \u2713 Switched to ${switchedInfo.label} (${switchedInfo.id})
|
|
7131
|
+
`));
|
|
7132
|
+
} else if (picked) {
|
|
7133
|
+
console.log(chalk27.gray(` Already using ${MODEL_CATALOG[activeModelId].label}
|
|
7134
|
+
`));
|
|
7135
|
+
} else {
|
|
7136
|
+
console.log(chalk27.gray(` Cancelled
|
|
7137
|
+
`));
|
|
7138
|
+
}
|
|
7139
|
+
rl.resume();
|
|
6977
7140
|
rl.prompt();
|
|
6978
7141
|
return;
|
|
6979
7142
|
}
|
|
@@ -7051,7 +7214,7 @@ Analyze the above input.`;
|
|
|
7051
7214
|
return;
|
|
7052
7215
|
}
|
|
7053
7216
|
if (input === "/compact") {
|
|
7054
|
-
const { autoCompress: autoCompress2 } = await import("./compression-
|
|
7217
|
+
const { autoCompress: autoCompress2 } = await import("./compression-LPFNGAV6.js");
|
|
7055
7218
|
const before = messages.length;
|
|
7056
7219
|
const compressed = await autoCompress2(messages, model, MODEL_CATALOG[activeModelId].contextWindow);
|
|
7057
7220
|
messages.length = 0;
|
|
@@ -7517,6 +7680,19 @@ Analyze the above input.`;
|
|
|
7517
7680
|
}
|
|
7518
7681
|
messages.push({ role: "user", content: finalPrompt });
|
|
7519
7682
|
const spinner = ora4("Thinking...").start();
|
|
7683
|
+
const spinnerStart = Date.now();
|
|
7684
|
+
const spinnerTimer = setInterval(() => {
|
|
7685
|
+
if (!spinner.isSpinning) {
|
|
7686
|
+
clearInterval(spinnerTimer);
|
|
7687
|
+
return;
|
|
7688
|
+
}
|
|
7689
|
+
const elapsed = Math.floor((Date.now() - spinnerStart) / 1e3);
|
|
7690
|
+
if (elapsed >= 30) {
|
|
7691
|
+
spinner.text = `Waiting for model... (${elapsed}s \u2014 endpoint may be cold-starting)`;
|
|
7692
|
+
} else if (elapsed >= 10) {
|
|
7693
|
+
spinner.text = `Thinking... (${elapsed}s)`;
|
|
7694
|
+
}
|
|
7695
|
+
}, 2e3);
|
|
7520
7696
|
try {
|
|
7521
7697
|
const response = await withRetry(
|
|
7522
7698
|
() => runAgentLoop(messages, {
|
|
@@ -7526,10 +7702,12 @@ Analyze the above input.`;
|
|
|
7526
7702
|
maxIterations: config.maxIterations,
|
|
7527
7703
|
contextWindow: MODEL_CATALOG[activeModelId].contextWindow,
|
|
7528
7704
|
onTextChunk: (chunk) => {
|
|
7705
|
+
clearInterval(spinnerTimer);
|
|
7529
7706
|
if (spinner.isSpinning) spinner.stop();
|
|
7530
7707
|
process.stdout.write(chunk);
|
|
7531
7708
|
},
|
|
7532
7709
|
onToolCall: (name, args) => {
|
|
7710
|
+
clearInterval(spinnerTimer);
|
|
7533
7711
|
if (spinner.isSpinning) spinner.stop();
|
|
7534
7712
|
if ((name === "write" || name === "edit") && typeof args.path === "string") {
|
|
7535
7713
|
const filePath = nodePath.isAbsolute(args.path) ? args.path : nodePath.resolve(toolCtx.cwd, args.path);
|
|
@@ -7591,6 +7769,7 @@ Analyze the above input.`;
|
|
|
7591
7769
|
}
|
|
7592
7770
|
}
|
|
7593
7771
|
} catch (err) {
|
|
7772
|
+
clearInterval(spinnerTimer);
|
|
7594
7773
|
spinner.fail(`Error: ${err.message}`);
|
|
7595
7774
|
checkpoints.discard();
|
|
7596
7775
|
const msg = err.message?.toLowerCase() ?? "";
|
package/package.json
CHANGED
package/dist/chunk-MWM5TFY4.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
// src/agent/compression.ts
|
|
2
|
-
import { generateText } from "ai";
|
|
3
|
-
function estimateTokens(messages) {
|
|
4
|
-
let chars = 0;
|
|
5
|
-
for (const msg of messages) {
|
|
6
|
-
if (typeof msg.content === "string") {
|
|
7
|
-
chars += msg.content.length;
|
|
8
|
-
} else if (Array.isArray(msg.content)) {
|
|
9
|
-
for (const part of msg.content) {
|
|
10
|
-
if ("text" in part) chars += part.text.length;
|
|
11
|
-
else if ("result" in part) chars += JSON.stringify(part.result).length;
|
|
12
|
-
else if ("args" in part) chars += JSON.stringify(part.args).length;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return Math.ceil(chars / 4);
|
|
17
|
-
}
|
|
18
|
-
async function compressHistory(messages, opts) {
|
|
19
|
-
const threshold = opts.contextWindow * 0.75;
|
|
20
|
-
const currentTokens = estimateTokens(messages);
|
|
21
|
-
if (currentTokens < threshold || messages.length < 6) {
|
|
22
|
-
return { messages, compressed: false };
|
|
23
|
-
}
|
|
24
|
-
const keepRecent = opts.keepRecent ?? 4;
|
|
25
|
-
const keepStart = 1;
|
|
26
|
-
const head = messages.slice(0, keepStart);
|
|
27
|
-
const middle = messages.slice(keepStart, -keepRecent);
|
|
28
|
-
const tail = messages.slice(-keepRecent);
|
|
29
|
-
if (middle.length === 0) {
|
|
30
|
-
return { messages, compressed: false };
|
|
31
|
-
}
|
|
32
|
-
const middleSummary = summarizeMessages(middle);
|
|
33
|
-
let summaryText;
|
|
34
|
-
try {
|
|
35
|
-
const result = await generateText({
|
|
36
|
-
model: opts.model,
|
|
37
|
-
system: "You are a conversation summarizer. Condense the following conversation history into a brief summary that preserves all important context, decisions made, files modified, and any errors encountered. Be concise but thorough. Output only the summary.",
|
|
38
|
-
messages: [{ role: "user", content: middleSummary }],
|
|
39
|
-
maxTokens: 1024
|
|
40
|
-
});
|
|
41
|
-
summaryText = result.text;
|
|
42
|
-
} catch {
|
|
43
|
-
summaryText = buildDeterministicSummary(middle);
|
|
44
|
-
}
|
|
45
|
-
const compressedMessages = [...head];
|
|
46
|
-
const summaryContent = `[Previous conversation context]
|
|
47
|
-
${summaryText}
|
|
48
|
-
[End of context]`;
|
|
49
|
-
if (tail.length > 0 && tail[0].role === "user") {
|
|
50
|
-
const firstContent = typeof tail[0].content === "string" ? tail[0].content : "";
|
|
51
|
-
compressedMessages.push({
|
|
52
|
-
role: "user",
|
|
53
|
-
content: `${summaryContent}
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
${firstContent}`
|
|
58
|
-
});
|
|
59
|
-
compressedMessages.push(...tail.slice(1));
|
|
60
|
-
} else {
|
|
61
|
-
compressedMessages.push({ role: "user", content: summaryContent });
|
|
62
|
-
compressedMessages.push({
|
|
63
|
-
role: "assistant",
|
|
64
|
-
content: "Understood. I have the context from our previous conversation. Continuing."
|
|
65
|
-
});
|
|
66
|
-
compressedMessages.push(...tail);
|
|
67
|
-
}
|
|
68
|
-
return { messages: compressedMessages, compressed: true };
|
|
69
|
-
}
|
|
70
|
-
function summarizeMessages(messages) {
|
|
71
|
-
const lines = [];
|
|
72
|
-
for (const msg of messages) {
|
|
73
|
-
const role = msg.role.toUpperCase();
|
|
74
|
-
if (typeof msg.content === "string") {
|
|
75
|
-
lines.push(`${role}: ${msg.content.slice(0, 500)}`);
|
|
76
|
-
} else if (Array.isArray(msg.content)) {
|
|
77
|
-
const parts = [];
|
|
78
|
-
for (const part of msg.content) {
|
|
79
|
-
if ("text" in part) parts.push(part.text.slice(0, 200));
|
|
80
|
-
else if ("toolName" in part) parts.push(`[tool: ${part.toolName}]`);
|
|
81
|
-
else if ("result" in part) parts.push(`[result: ${JSON.stringify(part.result).slice(0, 100)}]`);
|
|
82
|
-
}
|
|
83
|
-
lines.push(`${role}: ${parts.join(" | ")}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return lines.join("\n");
|
|
87
|
-
}
|
|
88
|
-
function buildDeterministicSummary(messages) {
|
|
89
|
-
const filesModified = /* @__PURE__ */ new Set();
|
|
90
|
-
const toolsUsed = /* @__PURE__ */ new Set();
|
|
91
|
-
const userRequests = [];
|
|
92
|
-
let errorCount = 0;
|
|
93
|
-
for (const msg of messages) {
|
|
94
|
-
if (msg.role === "user" && typeof msg.content === "string") {
|
|
95
|
-
userRequests.push(msg.content.slice(0, 100));
|
|
96
|
-
}
|
|
97
|
-
if (Array.isArray(msg.content)) {
|
|
98
|
-
for (const part of msg.content) {
|
|
99
|
-
if ("toolName" in part) {
|
|
100
|
-
const p = part;
|
|
101
|
-
toolsUsed.add(p.toolName);
|
|
102
|
-
if (p.args?.path) filesModified.add(String(p.args.path));
|
|
103
|
-
}
|
|
104
|
-
if ("result" in part) {
|
|
105
|
-
const r = part;
|
|
106
|
-
if (r.result?.isError) errorCount++;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const lines = ["Summary of previous conversation:"];
|
|
112
|
-
if (userRequests.length > 0) {
|
|
113
|
-
lines.push(`- User requests: ${userRequests.join("; ")}`);
|
|
114
|
-
}
|
|
115
|
-
if (toolsUsed.size > 0) {
|
|
116
|
-
lines.push(`- Tools used: ${[...toolsUsed].join(", ")}`);
|
|
117
|
-
}
|
|
118
|
-
if (filesModified.size > 0) {
|
|
119
|
-
lines.push(`- Files touched: ${[...filesModified].join(", ")}`);
|
|
120
|
-
}
|
|
121
|
-
if (errorCount > 0) {
|
|
122
|
-
lines.push(`- Errors encountered: ${errorCount}`);
|
|
123
|
-
}
|
|
124
|
-
lines.push(`- Total messages summarized: ${messages.length}`);
|
|
125
|
-
return lines.join("\n");
|
|
126
|
-
}
|
|
127
|
-
async function autoCompress(messages, model, contextWindow, onCompress) {
|
|
128
|
-
const result = await compressHistory(messages, {
|
|
129
|
-
model,
|
|
130
|
-
contextWindow
|
|
131
|
-
});
|
|
132
|
-
if (result.compressed) {
|
|
133
|
-
onCompress?.();
|
|
134
|
-
}
|
|
135
|
-
return result.messages;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export {
|
|
139
|
-
estimateTokens,
|
|
140
|
-
compressHistory,
|
|
141
|
-
autoCompress
|
|
142
|
-
};
|