@causyne/agent 1.10.21 → 1.11.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/native-skills/media/professional-document-publishing/SKILL.md +44 -0
- package/native-skills/media/screenplay-and-shotlist-forge/SKILL.md +42 -0
- package/package.json +1 -1
- package/src/acp/client.js +235 -0
- package/src/acp/client.ts +282 -0
- package/src/acp/peerProfiles.js +122 -0
- package/src/acp/peerProfiles.ts +128 -0
- package/src/acp/protocol.js +54 -0
- package/src/acp/protocol.ts +434 -0
- package/src/acp/server.js +609 -0
- package/src/acp/server.ts +676 -0
- package/src/agentLoop.d.ts +43 -0
- package/src/agentLoop.js +142 -8
- package/src/commands.js +203 -0
- package/src/compiledSkills.js +10 -0
- package/src/documentEngine/antiSlopAuditor.js +156 -0
- package/src/documentEngine/antiSlopAuditor.ts +221 -0
- package/src/documentEngine/documentForge.js +443 -0
- package/src/documentEngine/documentForge.ts +519 -0
- package/src/documentEngine/screenplayFormatter.js +336 -0
- package/src/documentEngine/screenplayFormatter.ts +407 -0
- package/src/entrypoint.js +13 -0
- package/src/execSafe.js +10 -13
- package/src/execSafe.ts +25 -0
- package/src/modelRouter.js +56 -123
- package/src/modelRouter.ts +134 -0
- package/src/observer/advisory.js +164 -0
- package/src/observer/advisory.ts +171 -0
- package/src/observer/coldObserver.js +462 -0
- package/src/observer/coldObserver.ts +540 -0
- package/src/observer/evidence.js +435 -0
- package/src/observer/evidence.ts +499 -0
- package/src/perception.js +303 -118
- package/src/perception.ts +387 -0
- package/src/procSignal.js +15 -16
- package/src/procSignal.ts +31 -0
- package/src/promptMentions.js +170 -0
- package/src/promptMentions.ts +212 -0
- package/src/sandbox.js +1 -1
- package/src/textDiff.js +117 -109
- package/src/textDiff.ts +169 -0
- package/src/toolPacks.js +5 -2
- package/src/tools/documentForge.js +215 -0
- package/src/tools/documentForge.ts +265 -0
- package/src/tools/peerAgent.js +102 -0
- package/src/tools/peerAgent.ts +116 -0
- package/src/tools/registry.js +3 -1
- package/src/tui/completion.js +97 -41
- package/src/tui/prompt.js +17 -17
- package/src/uncertainty.js +93 -205
- package/src/uncertainty.ts +255 -0
- package/src/utils/paths.js +15 -18
- package/src/utils/paths.ts +36 -0
- package/src/vfeTracker.js +122 -181
- package/src/vfeTracker.ts +242 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Resolution of `@` mentions for files and directories in user prompts.
|
|
2
|
+
//
|
|
3
|
+
// Pure Node.js. Zero dependencies. No external shell commands.
|
|
4
|
+
//
|
|
5
|
+
// Extracts both quoted (`@"path with spaces/file.js"`, `@'path'`) and
|
|
6
|
+
// unquoted (`@path/to/file.js`) mentions, resolves them against the active
|
|
7
|
+
// working directory and known subprojects, and formats bounded context
|
|
8
|
+
// representations so the agent immediately possesses referenced files and
|
|
9
|
+
// folder trees without needing exploratory tool calls.
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
const MAX_MENTION_FILE_LINES = 500;
|
|
15
|
+
const MAX_MENTION_DIR_ITEMS = 40;
|
|
16
|
+
|
|
17
|
+
export interface Mention {
|
|
18
|
+
raw: string;
|
|
19
|
+
path: string;
|
|
20
|
+
isQuoted: boolean;
|
|
21
|
+
isMeta?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResolveMentionOptions {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
subprojects?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ResolvedMentionContext {
|
|
30
|
+
mentions: Mention[];
|
|
31
|
+
referencedContext: string;
|
|
32
|
+
detectedSubproject: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract all @ mentions from prompt text.
|
|
37
|
+
*
|
|
38
|
+
* @param text - User prompt text.
|
|
39
|
+
* @returns Array of parsed mentions.
|
|
40
|
+
*/
|
|
41
|
+
export function extractMentions(text: string): Mention[] {
|
|
42
|
+
if (!text || typeof text !== 'string') return [];
|
|
43
|
+
const mentions: Mention[] = [];
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
|
|
46
|
+
// 1. Quoted mentions: @"..." or @'...'
|
|
47
|
+
// This allows spaces, e.g. @"pixel2d games/main.js" or @'assets/sprite sheet.png'
|
|
48
|
+
const quotedRegex = /(?:^|\s)@(["'])([^"'\n\r]+)\1/g;
|
|
49
|
+
let match: RegExpExecArray | null;
|
|
50
|
+
while ((match = quotedRegex.exec(text)) !== null) {
|
|
51
|
+
const raw = match[0].trim();
|
|
52
|
+
const cleanPath = (match[2] ?? '').trim();
|
|
53
|
+
if (cleanPath && !seen.has(cleanPath)) {
|
|
54
|
+
seen.add(cleanPath);
|
|
55
|
+
mentions.push({ raw, path: cleanPath, isQuoted: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Unquoted mentions: @path/to/file or @dir/
|
|
60
|
+
// Matches @ preceded by start-of-line or whitespace, not part of an email address
|
|
61
|
+
const unquotedRegex = /(?:^|\s)@([a-zA-Z0-9_.\-/\\]+)(?=$|\s|[.,;:?!)\]}])/g;
|
|
62
|
+
while ((match = unquotedRegex.exec(text)) !== null) {
|
|
63
|
+
const raw = match[0].trim();
|
|
64
|
+
const cleanPath = (match[1] ?? '').trim();
|
|
65
|
+
|
|
66
|
+
// Skip if already captured via quotes or if empty
|
|
67
|
+
if (!cleanPath || seen.has(cleanPath)) continue;
|
|
68
|
+
|
|
69
|
+
// Do not capture npm scope prefixes on their own or common non-file @ words
|
|
70
|
+
if (cleanPath === 'here' || cleanPath === 'all' || cleanPath === 'workspace') {
|
|
71
|
+
seen.add(cleanPath);
|
|
72
|
+
mentions.push({ raw, path: cleanPath, isQuoted: false, isMeta: true });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
seen.add(cleanPath);
|
|
77
|
+
mentions.push({ raw, path: cleanPath, isQuoted: false });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return mentions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve mentioned files and folders against the active working directory
|
|
85
|
+
* and detected subprojects. Returns bounded context blocks for model ingestion.
|
|
86
|
+
*
|
|
87
|
+
* @param prompt - Raw prompt string.
|
|
88
|
+
* @param options - Resolution options.
|
|
89
|
+
* @returns Bounded context and list of mentions.
|
|
90
|
+
*/
|
|
91
|
+
export async function resolveMentionedContext(
|
|
92
|
+
prompt: string,
|
|
93
|
+
options: ResolveMentionOptions = {}
|
|
94
|
+
): Promise<ResolvedMentionContext> {
|
|
95
|
+
const mentions = extractMentions(prompt);
|
|
96
|
+
if (mentions.length === 0) {
|
|
97
|
+
return { mentions: [], referencedContext: '', detectedSubproject: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const cwd = options.cwd || process.cwd();
|
|
101
|
+
const subprojects = Array.isArray(options.subprojects) ? options.subprojects : [];
|
|
102
|
+
const sections: string[] = [];
|
|
103
|
+
let detectedSubproject: string | null = null;
|
|
104
|
+
|
|
105
|
+
for (const m of mentions) {
|
|
106
|
+
if (m.isMeta) {
|
|
107
|
+
if (m.path === 'workspace' || m.path === 'here') {
|
|
108
|
+
sections.push(`📁 REFERENCED SCOPE: Whole workspace (${cwd})`);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let targetPath = path.resolve(cwd, m.path);
|
|
114
|
+
let found = false;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (fs.existsSync(targetPath)) {
|
|
118
|
+
found = true;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if path refers to a file/folder inside any detected subproject
|
|
125
|
+
if (!found && subprojects.length > 0) {
|
|
126
|
+
for (const sp of subprojects) {
|
|
127
|
+
const candidate = path.resolve(cwd, sp, m.path);
|
|
128
|
+
try {
|
|
129
|
+
if (fs.existsSync(candidate)) {
|
|
130
|
+
targetPath = candidate;
|
|
131
|
+
found = true;
|
|
132
|
+
detectedSubproject = sp;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// ignore
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if the mention path explicitly names a subproject (e.g. @"pixel2d games/...")
|
|
142
|
+
if (subprojects.length > 0) {
|
|
143
|
+
for (const sp of subprojects) {
|
|
144
|
+
if (m.path === sp || m.path.startsWith(sp + '/') || m.path.startsWith(sp + '\\')) {
|
|
145
|
+
detectedSubproject = sp;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const relPath = path.relative(cwd, targetPath);
|
|
152
|
+
|
|
153
|
+
if (!found) {
|
|
154
|
+
sections.push(`📂 REFERENCED PATH: ${m.path} (Note: path was not found on disk at ${relPath})`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const stats = fs.statSync(targetPath);
|
|
160
|
+
if (stats.isDirectory()) {
|
|
161
|
+
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
|
162
|
+
const summary = entries
|
|
163
|
+
.filter(e => !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
164
|
+
.slice(0, MAX_MENTION_DIR_ITEMS)
|
|
165
|
+
.map(e => e.isDirectory() ? `${e.name}/` : e.name);
|
|
166
|
+
|
|
167
|
+
const more = entries.length > MAX_MENTION_DIR_ITEMS
|
|
168
|
+
? `\n ... (${entries.length - MAX_MENTION_DIR_ITEMS} more items)`
|
|
169
|
+
: '';
|
|
170
|
+
|
|
171
|
+
sections.push(
|
|
172
|
+
`📁 REFERENCED DIRECTORY: ${relPath}/\n` +
|
|
173
|
+
`Directory listing (${summary.length} items):\n` +
|
|
174
|
+
summary.map(s => ` - ${s}`).join('\n') +
|
|
175
|
+
more
|
|
176
|
+
);
|
|
177
|
+
} else if (stats.isFile()) {
|
|
178
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
179
|
+
const lines = content.split('\n');
|
|
180
|
+
let body: string;
|
|
181
|
+
if (lines.length > MAX_MENTION_FILE_LINES) {
|
|
182
|
+
body = lines.slice(0, MAX_MENTION_FILE_LINES).join('\n') +
|
|
183
|
+
`\n\n... [Truncated: showing first ${MAX_MENTION_FILE_LINES} of ${lines.length} lines. Use fs.read for remaining lines.]`;
|
|
184
|
+
} else {
|
|
185
|
+
body = content;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
sections.push(
|
|
189
|
+
`📄 REFERENCED FILE: ${relPath} (${lines.length} lines):\n` +
|
|
190
|
+
'```\n' +
|
|
191
|
+
body + '\n' +
|
|
192
|
+
'```'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} catch (err: unknown) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
sections.push(`📂 REFERENCED PATH: ${relPath} (Error reading: ${msg})`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const referencedContext = sections.length > 0
|
|
202
|
+
? `\n\n=== REFERENCED WORKSPACE CONTEXT (@ mentions) ===\n` +
|
|
203
|
+
sections.join('\n\n') +
|
|
204
|
+
`\n=== END REFERENCED WORKSPACE CONTEXT ===\n`
|
|
205
|
+
: '';
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
mentions,
|
|
209
|
+
referencedContext,
|
|
210
|
+
detectedSubproject
|
|
211
|
+
};
|
|
212
|
+
}
|
package/src/sandbox.js
CHANGED
|
@@ -360,7 +360,7 @@ export class Sandbox {
|
|
|
360
360
|
if (toolName === 'desktop_act' || toolName === 'computer_use') {
|
|
361
361
|
return { allowed: true, risk: 'extreme' };
|
|
362
362
|
}
|
|
363
|
-
if (toolName === 'ui_forge') {
|
|
363
|
+
if (toolName === 'ui_forge' || toolName === 'document_forge') {
|
|
364
364
|
return { allowed: true, risk: 'safe' };
|
|
365
365
|
}
|
|
366
366
|
if (toolName === 'fs_write' || toolName === 'fs_move' || toolName === 'fs_edit' || toolName === 'MultiEdit') {
|
package/src/textDiff.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A unified diff, in pure JavaScript.
|
|
1
|
+
// A unified diff, in pure JavaScript / TypeScript.
|
|
2
2
|
//
|
|
3
3
|
// The git backend gets patches from git itself. This exists so the NATIVE
|
|
4
4
|
// backend — the fallback for a machine with no git, or a workspace that is not
|
|
@@ -9,13 +9,10 @@
|
|
|
9
9
|
// No dependency: this is a standard LCS over lines, which is what `diff -u`
|
|
10
10
|
// computes. Bounded, because a checkpoint diff is a UI affordance and must
|
|
11
11
|
// never become the reason a request times out.
|
|
12
|
-
|
|
13
12
|
/** Above this many lines on either side, fall back to counts without a patch. */
|
|
14
13
|
const MAX_DIFF_LINES = 4000;
|
|
15
|
-
|
|
16
14
|
/** Context lines around each change, matching `diff -u` and git's default. */
|
|
17
15
|
const CONTEXT = 3;
|
|
18
|
-
|
|
19
16
|
/**
|
|
20
17
|
* Longest common subsequence over two line arrays.
|
|
21
18
|
*
|
|
@@ -24,127 +21,138 @@ const CONTEXT = 3;
|
|
|
24
21
|
* second, and anything larger is not something a human reads in a dialog.
|
|
25
22
|
*/
|
|
26
23
|
function lcsTable(a, b) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
const table = [];
|
|
25
|
+
for (let i = 0; i <= a.length; i++)
|
|
26
|
+
table.push(new Uint32Array(b.length + 1));
|
|
27
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
28
|
+
const tableI = table[i];
|
|
29
|
+
const tableNext = table[i + 1];
|
|
30
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
31
|
+
tableI[j] = a[i] === b[j]
|
|
32
|
+
? tableNext[j + 1] + 1
|
|
33
|
+
: Math.max(tableNext[j], tableI[j + 1]);
|
|
34
|
+
}
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
return table;
|
|
36
|
+
return table;
|
|
37
37
|
}
|
|
38
|
-
|
|
39
38
|
/** Walk the table into a flat edit script: ' ' keep, '-' remove, '+' add. */
|
|
40
39
|
function editScript(a, b) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
const table = lcsTable(a, b);
|
|
41
|
+
const ops = [];
|
|
42
|
+
let i = 0;
|
|
43
|
+
let j = 0;
|
|
44
|
+
while (i < a.length && j < b.length) {
|
|
45
|
+
if (a[i] === b[j]) {
|
|
46
|
+
ops.push([' ', a[i]]);
|
|
47
|
+
i++;
|
|
48
|
+
j++;
|
|
49
|
+
}
|
|
50
|
+
else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
51
|
+
ops.push(['-', a[i]]);
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
ops.push(['+', b[j]]);
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
while (i < a.length)
|
|
60
|
+
ops.push(['-', a[i++]]);
|
|
61
|
+
while (j < b.length)
|
|
62
|
+
ops.push(['+', b[j++]]);
|
|
63
|
+
return ops;
|
|
53
64
|
}
|
|
54
|
-
|
|
55
65
|
function splitLines(text) {
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
if (text === '')
|
|
67
|
+
return [];
|
|
68
|
+
return text.replace(/\n$/, '').split('\n');
|
|
58
69
|
}
|
|
59
|
-
|
|
60
70
|
/**
|
|
61
71
|
* A unified diff of `before` → `after`.
|
|
62
|
-
*
|
|
63
|
-
* @returns {{patch: string, additions: number, deletions: number, truncated: boolean}}
|
|
64
72
|
*/
|
|
65
73
|
export function unifiedDiff(before, after, pathLabel = 'file') {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const ops = editScript(a, b);
|
|
81
|
-
let additions = 0;
|
|
82
|
-
let deletions = 0;
|
|
83
|
-
for (const [kind] of ops) {
|
|
84
|
-
if (kind === '+') additions++;
|
|
85
|
-
else if (kind === '-') deletions++;
|
|
86
|
-
}
|
|
87
|
-
if (additions === 0 && deletions === 0) {
|
|
88
|
-
return { patch: '', additions: 0, deletions: 0, truncated: false };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Group changes into hunks with CONTEXT lines of surrounding text, so the
|
|
92
|
-
// output is readable rather than the whole file with markers.
|
|
93
|
-
const lines = [`--- a/${pathLabel}`, `+++ b/${pathLabel}`];
|
|
94
|
-
let index = 0;
|
|
95
|
-
let oldLine = 1;
|
|
96
|
-
let newLine = 1;
|
|
97
|
-
|
|
98
|
-
while (index < ops.length) {
|
|
99
|
-
if (ops[index][0] === ' ') {
|
|
100
|
-
if (ops[index][0] === ' ') { oldLine++; newLine++; }
|
|
101
|
-
index++;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Walk back for leading context, then forward to the end of the change.
|
|
106
|
-
const start = Math.max(0, index - CONTEXT);
|
|
107
|
-
let end = index;
|
|
108
|
-
let quiet = 0;
|
|
109
|
-
while (end < ops.length && quiet <= CONTEXT * 2) {
|
|
110
|
-
quiet = ops[end][0] === ' ' ? quiet + 1 : 0;
|
|
111
|
-
end++;
|
|
74
|
+
const a = splitLines(before);
|
|
75
|
+
const b = splitLines(after);
|
|
76
|
+
if (a.length > MAX_DIFF_LINES || b.length > MAX_DIFF_LINES) {
|
|
77
|
+
// Counting without the table keeps a huge file cheap: the exact per-line
|
|
78
|
+
// patch is not worth an O(n*m) allocation nobody will read.
|
|
79
|
+
return {
|
|
80
|
+
patch: '',
|
|
81
|
+
additions: b.length,
|
|
82
|
+
deletions: a.length,
|
|
83
|
+
truncated: true
|
|
84
|
+
};
|
|
112
85
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
86
|
+
const ops = editScript(a, b);
|
|
87
|
+
let additions = 0;
|
|
88
|
+
let deletions = 0;
|
|
89
|
+
for (const [kind] of ops) {
|
|
90
|
+
if (kind === '+')
|
|
91
|
+
additions++;
|
|
92
|
+
else if (kind === '-')
|
|
93
|
+
deletions++;
|
|
120
94
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
let newCount = 0;
|
|
124
|
-
const body = [];
|
|
125
|
-
for (let k = start; k < end; k++) {
|
|
126
|
-
const [kind, text] = ops[k];
|
|
127
|
-
if (kind !== '+') oldCount++;
|
|
128
|
-
if (kind !== '-') newCount++;
|
|
129
|
-
body.push(`${kind}${text}`);
|
|
95
|
+
if (additions === 0 && deletions === 0) {
|
|
96
|
+
return { patch: '', additions: 0, deletions: 0, truncated: false };
|
|
130
97
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
lines
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
98
|
+
// Group changes into hunks with CONTEXT lines of surrounding text, so the
|
|
99
|
+
// output is readable rather than the whole file with markers.
|
|
100
|
+
const lines = [`--- a/${pathLabel}`, `+++ b/${pathLabel}`];
|
|
101
|
+
let index = 0;
|
|
102
|
+
let oldLine = 1;
|
|
103
|
+
let newLine = 1;
|
|
104
|
+
while (index < ops.length) {
|
|
105
|
+
const opIndex = ops[index];
|
|
106
|
+
if (opIndex[0] === ' ') {
|
|
107
|
+
oldLine++;
|
|
108
|
+
newLine++;
|
|
109
|
+
index++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Walk back for leading context, then forward to the end of the change.
|
|
113
|
+
const start = Math.max(0, index - CONTEXT);
|
|
114
|
+
let end = index;
|
|
115
|
+
let quiet = 0;
|
|
116
|
+
while (end < ops.length && quiet <= CONTEXT * 2) {
|
|
117
|
+
quiet = ops[end][0] === ' ' ? quiet + 1 : 0;
|
|
118
|
+
end++;
|
|
119
|
+
}
|
|
120
|
+
end = Math.min(ops.length, end);
|
|
121
|
+
let oldStart = oldLine;
|
|
122
|
+
let newStart = newLine;
|
|
123
|
+
for (let k = start; k < index; k++) {
|
|
124
|
+
if (ops[k][0] !== '+')
|
|
125
|
+
oldStart--;
|
|
126
|
+
if (ops[k][0] !== '-')
|
|
127
|
+
newStart--;
|
|
128
|
+
}
|
|
129
|
+
let oldCount = 0;
|
|
130
|
+
let newCount = 0;
|
|
131
|
+
const body = [];
|
|
132
|
+
for (let k = start; k < end; k++) {
|
|
133
|
+
const [kind, text] = ops[k];
|
|
134
|
+
if (kind !== '+')
|
|
135
|
+
oldCount++;
|
|
136
|
+
if (kind !== '-')
|
|
137
|
+
newCount++;
|
|
138
|
+
body.push(`${kind}${text}`);
|
|
139
|
+
}
|
|
140
|
+
lines.push(`@@ -${Math.max(1, oldStart)},${oldCount} +${Math.max(1, newStart)},${newCount} @@`);
|
|
141
|
+
lines.push(...body);
|
|
142
|
+
for (let k = index; k < end; k++) {
|
|
143
|
+
if (ops[k][0] !== '+')
|
|
144
|
+
oldLine++;
|
|
145
|
+
if (ops[k][0] !== '-')
|
|
146
|
+
newLine++;
|
|
147
|
+
}
|
|
148
|
+
index = end;
|
|
138
149
|
}
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { patch: lines.join('\n'), additions, deletions, truncated: false };
|
|
150
|
+
return { patch: lines.join('\n'), additions, deletions, truncated: false };
|
|
143
151
|
}
|
|
144
|
-
|
|
145
152
|
/** Binary content has no useful line diff — detected the way git detects it. */
|
|
146
153
|
export function looksBinary(buffer) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
if (!buffer || buffer.length === 0)
|
|
155
|
+
return false;
|
|
156
|
+
const sample = buffer.subarray(0, Math.min(8000, buffer.length));
|
|
157
|
+
return sample.includes(0);
|
|
150
158
|
}
|
package/src/textDiff.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// A unified diff, in pure JavaScript / TypeScript.
|
|
2
|
+
//
|
|
3
|
+
// The git backend gets patches from git itself. This exists so the NATIVE
|
|
4
|
+
// backend — the fallback for a machine with no git, or a workspace that is not
|
|
5
|
+
// a repository — can answer the same question. Without it, REVIEW would show
|
|
6
|
+
// line counts on one machine and real changes on another, which is the kind of
|
|
7
|
+
// inconsistency that makes a feature feel broken rather than degraded.
|
|
8
|
+
//
|
|
9
|
+
// No dependency: this is a standard LCS over lines, which is what `diff -u`
|
|
10
|
+
// computes. Bounded, because a checkpoint diff is a UI affordance and must
|
|
11
|
+
// never become the reason a request times out.
|
|
12
|
+
|
|
13
|
+
/** Above this many lines on either side, fall back to counts without a patch. */
|
|
14
|
+
const MAX_DIFF_LINES = 4000;
|
|
15
|
+
|
|
16
|
+
/** Context lines around each change, matching `diff -u` and git's default. */
|
|
17
|
+
const CONTEXT = 3;
|
|
18
|
+
|
|
19
|
+
export interface UnifiedDiffResult {
|
|
20
|
+
patch: string;
|
|
21
|
+
additions: number;
|
|
22
|
+
deletions: number;
|
|
23
|
+
truncated: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Longest common subsequence over two line arrays.
|
|
28
|
+
*
|
|
29
|
+
* Classic dynamic programming. O(n*m) in time and memory, which is why
|
|
30
|
+
* MAX_DIFF_LINES exists — at the cap this is 16M cells, still well under a
|
|
31
|
+
* second, and anything larger is not something a human reads in a dialog.
|
|
32
|
+
*/
|
|
33
|
+
function lcsTable(a: string[], b: string[]): Uint32Array[] {
|
|
34
|
+
const table: Uint32Array[] = [];
|
|
35
|
+
for (let i = 0; i <= a.length; i++) table.push(new Uint32Array(b.length + 1));
|
|
36
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
37
|
+
const tableI = table[i]!;
|
|
38
|
+
const tableNext = table[i + 1]!;
|
|
39
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
40
|
+
tableI[j] = a[i] === b[j]
|
|
41
|
+
? tableNext[j + 1]! + 1
|
|
42
|
+
: Math.max(tableNext[j]!, tableI[j + 1]!);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return table;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type EditOp = [' ' | '-' | '+', string];
|
|
49
|
+
|
|
50
|
+
/** Walk the table into a flat edit script: ' ' keep, '-' remove, '+' add. */
|
|
51
|
+
function editScript(a: string[], b: string[]): EditOp[] {
|
|
52
|
+
const table = lcsTable(a, b);
|
|
53
|
+
const ops: EditOp[] = [];
|
|
54
|
+
let i = 0;
|
|
55
|
+
let j = 0;
|
|
56
|
+
while (i < a.length && j < b.length) {
|
|
57
|
+
if (a[i] === b[j]) {
|
|
58
|
+
ops.push([' ', a[i]!]);
|
|
59
|
+
i++;
|
|
60
|
+
j++;
|
|
61
|
+
} else if (table[i + 1]![j]! >= table[i]![j + 1]!) {
|
|
62
|
+
ops.push(['-', a[i]!]);
|
|
63
|
+
i++;
|
|
64
|
+
} else {
|
|
65
|
+
ops.push(['+', b[j]!]);
|
|
66
|
+
j++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
while (i < a.length) ops.push(['-', a[i++]!]);
|
|
70
|
+
while (j < b.length) ops.push(['+', b[j++]!]);
|
|
71
|
+
return ops;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function splitLines(text: string): string[] {
|
|
75
|
+
if (text === '') return [];
|
|
76
|
+
return text.replace(/\n$/, '').split('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A unified diff of `before` → `after`.
|
|
81
|
+
*/
|
|
82
|
+
export function unifiedDiff(before: string, after: string, pathLabel = 'file'): UnifiedDiffResult {
|
|
83
|
+
const a = splitLines(before);
|
|
84
|
+
const b = splitLines(after);
|
|
85
|
+
|
|
86
|
+
if (a.length > MAX_DIFF_LINES || b.length > MAX_DIFF_LINES) {
|
|
87
|
+
// Counting without the table keeps a huge file cheap: the exact per-line
|
|
88
|
+
// patch is not worth an O(n*m) allocation nobody will read.
|
|
89
|
+
return {
|
|
90
|
+
patch: '',
|
|
91
|
+
additions: b.length,
|
|
92
|
+
deletions: a.length,
|
|
93
|
+
truncated: true
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ops = editScript(a, b);
|
|
98
|
+
let additions = 0;
|
|
99
|
+
let deletions = 0;
|
|
100
|
+
for (const [kind] of ops) {
|
|
101
|
+
if (kind === '+') additions++;
|
|
102
|
+
else if (kind === '-') deletions++;
|
|
103
|
+
}
|
|
104
|
+
if (additions === 0 && deletions === 0) {
|
|
105
|
+
return { patch: '', additions: 0, deletions: 0, truncated: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Group changes into hunks with CONTEXT lines of surrounding text, so the
|
|
109
|
+
// output is readable rather than the whole file with markers.
|
|
110
|
+
const lines = [`--- a/${pathLabel}`, `+++ b/${pathLabel}`];
|
|
111
|
+
let index = 0;
|
|
112
|
+
let oldLine = 1;
|
|
113
|
+
let newLine = 1;
|
|
114
|
+
|
|
115
|
+
while (index < ops.length) {
|
|
116
|
+
const opIndex = ops[index]!;
|
|
117
|
+
if (opIndex[0] === ' ') {
|
|
118
|
+
oldLine++;
|
|
119
|
+
newLine++;
|
|
120
|
+
index++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Walk back for leading context, then forward to the end of the change.
|
|
125
|
+
const start = Math.max(0, index - CONTEXT);
|
|
126
|
+
let end = index;
|
|
127
|
+
let quiet = 0;
|
|
128
|
+
while (end < ops.length && quiet <= CONTEXT * 2) {
|
|
129
|
+
quiet = ops[end]![0] === ' ' ? quiet + 1 : 0;
|
|
130
|
+
end++;
|
|
131
|
+
}
|
|
132
|
+
end = Math.min(ops.length, end);
|
|
133
|
+
|
|
134
|
+
let oldStart = oldLine;
|
|
135
|
+
let newStart = newLine;
|
|
136
|
+
for (let k = start; k < index; k++) {
|
|
137
|
+
if (ops[k]![0] !== '+') oldStart--;
|
|
138
|
+
if (ops[k]![0] !== '-') newStart--;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let oldCount = 0;
|
|
142
|
+
let newCount = 0;
|
|
143
|
+
const body: string[] = [];
|
|
144
|
+
for (let k = start; k < end; k++) {
|
|
145
|
+
const [kind, text] = ops[k]!;
|
|
146
|
+
if (kind !== '+') oldCount++;
|
|
147
|
+
if (kind !== '-') newCount++;
|
|
148
|
+
body.push(`${kind}${text}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lines.push(`@@ -${Math.max(1, oldStart)},${oldCount} +${Math.max(1, newStart)},${newCount} @@`);
|
|
152
|
+
lines.push(...body);
|
|
153
|
+
|
|
154
|
+
for (let k = index; k < end; k++) {
|
|
155
|
+
if (ops[k]![0] !== '+') oldLine++;
|
|
156
|
+
if (ops[k]![0] !== '-') newLine++;
|
|
157
|
+
}
|
|
158
|
+
index = end;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { patch: lines.join('\n'), additions, deletions, truncated: false };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Binary content has no useful line diff — detected the way git detects it. */
|
|
165
|
+
export function looksBinary(buffer: Buffer | Uint8Array | null | undefined): boolean {
|
|
166
|
+
if (!buffer || buffer.length === 0) return false;
|
|
167
|
+
const sample = buffer.subarray(0, Math.min(8000, buffer.length));
|
|
168
|
+
return sample.includes(0);
|
|
169
|
+
}
|
package/src/toolPacks.js
CHANGED
|
@@ -87,7 +87,8 @@ export const PACKS = {
|
|
|
87
87
|
'blender_act',
|
|
88
88
|
'creative_apps',
|
|
89
89
|
'resolve_act',
|
|
90
|
-
'palmier_act'
|
|
90
|
+
'palmier_act',
|
|
91
|
+
'document_forge'
|
|
91
92
|
],
|
|
92
93
|
system: [
|
|
93
94
|
// Channel B of the Three-Channel Doctrine: reading the screen as structured
|
|
@@ -110,7 +111,9 @@ export const PACKS = {
|
|
|
110
111
|
advanced: [
|
|
111
112
|
'tool_forge',
|
|
112
113
|
'plugin_manager',
|
|
114
|
+
'document_forge',
|
|
113
115
|
'agent_subagent_orchestrate',
|
|
116
|
+
'peer_agent_delegate',
|
|
114
117
|
'code_search_ast',
|
|
115
118
|
'code_list_symbols',
|
|
116
119
|
'pdf_read'
|
|
@@ -370,7 +373,7 @@ const MEDIA_INTENT = /\b(image|images|picture|photo|gambar|render|3d model|blend
|
|
|
370
373
|
|
|
371
374
|
const SYSTEM_INTENT = /\b(process|processes|pid|kill|port|zip|unzip|tar|archive|compress|extract|clipboard|copy to clipboard|print|printer|cetak|open in browser|launch app|computer use|vision grounding|desktop vision|gui agent|coordinate click|desktop control)\b/i;
|
|
372
375
|
|
|
373
|
-
const ADVANCED_INTENT = /\b(pdf|forge a tool|custom tool|new tool|plugin|plugins|custom plugin|subagent|sub-agent|parallel agent|swarm|ast|symbols|refactor across)\b/i;
|
|
376
|
+
const ADVANCED_INTENT = /\b(pdf|document|proposal|dokumen|screenplay|naskah|shotlist|shot list|voiceover|dubbing|forge a tool|custom tool|new tool|plugin|plugins|custom plugin|subagent|sub-agent|parallel agent|swarm|ast|symbols|refactor across)\b/i;
|
|
374
377
|
|
|
375
378
|
const INTENT_MATCHERS = {
|
|
376
379
|
game: GAME_INTENT,
|