@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,387 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
const SKIP_DIRECTORIES = new Set([
|
|
6
|
+
'node_modules', '.git', 'dist', 'build', 'out', 'target',
|
|
7
|
+
'.next', '.nuxt', '.venv', 'venv', '__pycache__', '.gradle',
|
|
8
|
+
'.idea', '.cache', 'coverage', 'vendor', 'Pods', '.causyne'
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export interface FrameworkInfo {
|
|
12
|
+
type: string;
|
|
13
|
+
language: string;
|
|
14
|
+
toString(): string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OsInfo {
|
|
18
|
+
platform: string;
|
|
19
|
+
arch: string;
|
|
20
|
+
toString(): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PackageJsonContext {
|
|
24
|
+
name?: string | undefined;
|
|
25
|
+
dependencies?: Record<string, string> | undefined;
|
|
26
|
+
devDependencies?: Record<string, string> | undefined;
|
|
27
|
+
scripts?: Record<string, string> | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type DirectoryTree = {
|
|
31
|
+
[name: string]: DirectoryTree | string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface GitInfo {
|
|
35
|
+
branch: string | null;
|
|
36
|
+
gitDir: string | null;
|
|
37
|
+
status?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkspaceContextData {
|
|
41
|
+
cwd: string;
|
|
42
|
+
projectName: string;
|
|
43
|
+
structure: DirectoryTree | string;
|
|
44
|
+
framework: FrameworkInfo | null;
|
|
45
|
+
designSystem: string[] | null;
|
|
46
|
+
subprojects: string[];
|
|
47
|
+
keyFiles: string[];
|
|
48
|
+
packageManager: string | null;
|
|
49
|
+
gitBranch?: string;
|
|
50
|
+
gitStatus?: string;
|
|
51
|
+
packageJson?: PackageJsonContext;
|
|
52
|
+
projectType: string;
|
|
53
|
+
readme?: string;
|
|
54
|
+
os: OsInfo;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class WorkspacePerception {
|
|
58
|
+
async buildContext(workspacePath: string): Promise<WorkspaceContextData> {
|
|
59
|
+
const resolvedPath = workspacePath.replace(/^~\//, os.homedir() + '/');
|
|
60
|
+
|
|
61
|
+
const context: Partial<WorkspaceContextData> = {
|
|
62
|
+
cwd: resolvedPath,
|
|
63
|
+
projectName: path.basename(resolvedPath)
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 1. Directory structure (tree, max depth 2 to keep context size manageable)
|
|
67
|
+
context.structure = await this.getDirectoryTree(resolvedPath, 2);
|
|
68
|
+
|
|
69
|
+
// 2. Framework detection
|
|
70
|
+
const fw = await this.detectFramework(resolvedPath);
|
|
71
|
+
if (fw && fw.type !== 'unknown') {
|
|
72
|
+
const fwStr = `${fw.type} (${fw.language})`;
|
|
73
|
+
context.framework = Object.assign(new String(fwStr) as unknown as FrameworkInfo, {
|
|
74
|
+
type: fw.type,
|
|
75
|
+
language: fw.language,
|
|
76
|
+
toString: () => fwStr
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
context.framework = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2b. Design system detection (Anti-Slop: tells the brain what's already installed)
|
|
83
|
+
context.designSystem = await this.detectDesignContext(resolvedPath);
|
|
84
|
+
|
|
85
|
+
// 2c. Subprojects detection (find subfolders like 'pixel2d games', 'causyne-agent', etc.)
|
|
86
|
+
context.subprojects = await this.detectSubprojects(resolvedPath);
|
|
87
|
+
|
|
88
|
+
// 2d. Key files discovery
|
|
89
|
+
context.keyFiles = await this.detectKeyFiles(resolvedPath);
|
|
90
|
+
|
|
91
|
+
// 2e. Package manager detection
|
|
92
|
+
context.packageManager = await this.detectPackageManager(resolvedPath);
|
|
93
|
+
|
|
94
|
+
// 2f. Git branch and status
|
|
95
|
+
const gitInfo = await this.detectGitInfo(resolvedPath);
|
|
96
|
+
if (gitInfo.branch) context.gitBranch = gitInfo.branch;
|
|
97
|
+
if (gitInfo.status) context.gitStatus = gitInfo.status;
|
|
98
|
+
|
|
99
|
+
// 3. Package/dependency context
|
|
100
|
+
try {
|
|
101
|
+
const pkgPath = path.join(resolvedPath, 'package.json');
|
|
102
|
+
const pkgStats = await fs.stat(pkgPath);
|
|
103
|
+
if (pkgStats.isFile()) {
|
|
104
|
+
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
105
|
+
const pkgData = JSON.parse(raw) as {
|
|
106
|
+
name?: string;
|
|
107
|
+
dependencies?: Record<string, string>;
|
|
108
|
+
devDependencies?: Record<string, string>;
|
|
109
|
+
scripts?: Record<string, string>;
|
|
110
|
+
};
|
|
111
|
+
context.packageJson = {
|
|
112
|
+
name: pkgData.name,
|
|
113
|
+
dependencies: pkgData.dependencies || {},
|
|
114
|
+
devDependencies: pkgData.devDependencies || {},
|
|
115
|
+
scripts: pkgData.scripts || {}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// ignore missing or invalid package.json
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3b. Project type classification
|
|
123
|
+
context.projectType = await this.detectProjectType(resolvedPath, context);
|
|
124
|
+
|
|
125
|
+
// 4. README (first 1000 chars)
|
|
126
|
+
try {
|
|
127
|
+
const readmePath = path.join(resolvedPath, 'README.md');
|
|
128
|
+
const readmeContent = await fs.readFile(readmePath, 'utf8');
|
|
129
|
+
context.readme = readmeContent.slice(0, 1000) + (readmeContent.length > 1000 ? '...' : '');
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. OS & environment
|
|
135
|
+
const osStr = `${os.platform()} (${os.arch()})`;
|
|
136
|
+
context.os = Object.assign(new String(osStr) as unknown as OsInfo, {
|
|
137
|
+
platform: os.platform(),
|
|
138
|
+
arch: os.arch(),
|
|
139
|
+
toString: () => osStr
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return context as WorkspaceContextData;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getDirectoryTree(dir: string, maxDepth: number, currentDepth = 0): Promise<DirectoryTree | string> {
|
|
146
|
+
if (currentDepth > maxDepth) return '... (max depth reached)';
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
150
|
+
const tree: DirectoryTree = {};
|
|
151
|
+
|
|
152
|
+
let count = 0;
|
|
153
|
+
for (const item of items) {
|
|
154
|
+
if (item.name.startsWith('.') || item.name === 'node_modules') continue;
|
|
155
|
+
|
|
156
|
+
if (count >= 30) {
|
|
157
|
+
tree['...'] = `(${items.length - count} more items hidden)`;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
count++;
|
|
162
|
+
if (item.isDirectory()) {
|
|
163
|
+
tree[item.name] = await this.getDirectoryTree(path.join(dir, item.name), maxDepth, currentDepth + 1);
|
|
164
|
+
} else {
|
|
165
|
+
tree[item.name] = 'file';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return tree;
|
|
169
|
+
} catch (e: unknown) {
|
|
170
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
171
|
+
return `Error: ${msg}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async detectFramework(dir: string): Promise<{ type: string; language: string }> {
|
|
176
|
+
const detectors = [
|
|
177
|
+
{ file: 'next.config.js', type: 'nextjs', lang: 'javascript' },
|
|
178
|
+
{ file: 'next.config.ts', type: 'nextjs', lang: 'typescript' },
|
|
179
|
+
{ file: 'next.config.mjs', type: 'nextjs', lang: 'javascript' },
|
|
180
|
+
{ file: 'vite.config.ts', type: 'vite', lang: 'typescript' },
|
|
181
|
+
{ file: 'vite.config.js', type: 'vite', lang: 'javascript' },
|
|
182
|
+
{ file: 'nuxt.config.ts', type: 'nuxt', lang: 'typescript' },
|
|
183
|
+
{ file: 'astro.config.mjs', type: 'astro', lang: 'javascript' },
|
|
184
|
+
{ file: 'svelte.config.js', type: 'svelte', lang: 'javascript' },
|
|
185
|
+
{ file: 'tsconfig.json', type: 'node', lang: 'typescript' },
|
|
186
|
+
{ file: 'package.json', type: 'node', lang: 'javascript' },
|
|
187
|
+
{ file: 'manage.py', type: 'django', lang: 'python' },
|
|
188
|
+
{ file: 'pyproject.toml', type: 'python', lang: 'python' },
|
|
189
|
+
{ file: 'requirements.txt', type: 'python', lang: 'python' },
|
|
190
|
+
{ file: 'Cargo.toml', type: 'rust', lang: 'rust' },
|
|
191
|
+
{ file: 'go.mod', type: 'go', lang: 'go' },
|
|
192
|
+
{ file: 'project.godot', type: 'godot', lang: 'gdscript' },
|
|
193
|
+
{ file: 'build.gradle.kts', type: 'android', lang: 'kotlin' },
|
|
194
|
+
{ file: 'docker-compose.yml', type: 'docker', lang: 'various' },
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
for (const d of detectors) {
|
|
198
|
+
try {
|
|
199
|
+
await fs.access(path.join(dir, d.file));
|
|
200
|
+
return { type: d.type, language: d.lang };
|
|
201
|
+
} catch {
|
|
202
|
+
// ignore
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { type: 'unknown', language: 'unknown' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async detectDesignContext(dir: string): Promise<string[] | null> {
|
|
210
|
+
const designDetectors = [
|
|
211
|
+
{ dep: 'tailwindcss', system: 'Tailwind CSS' },
|
|
212
|
+
{ dep: '@astryxdesign/core', system: 'Astryx Design System' },
|
|
213
|
+
{ dep: '@mui/material', system: 'Material-UI (MUI)' },
|
|
214
|
+
{ dep: '@chakra-ui/react', system: 'Chakra UI' },
|
|
215
|
+
{ dep: 'bootstrap', system: 'Bootstrap' },
|
|
216
|
+
{ dep: 'styled-components', system: 'Styled Components' },
|
|
217
|
+
{ dep: '@emotion/react', system: 'Emotion CSS-in-JS' },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
222
|
+
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
223
|
+
const pkgData = JSON.parse(raw) as {
|
|
224
|
+
dependencies?: Record<string, string>;
|
|
225
|
+
devDependencies?: Record<string, string>;
|
|
226
|
+
};
|
|
227
|
+
const allDeps: Record<string, string> = { ...pkgData.dependencies, ...pkgData.devDependencies };
|
|
228
|
+
|
|
229
|
+
const detected = designDetectors
|
|
230
|
+
.filter(d => Boolean(allDeps[d.dep]))
|
|
231
|
+
.map(d => d.system);
|
|
232
|
+
|
|
233
|
+
return detected.length > 0 ? detected : null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async detectSubprojects(dir: string): Promise<string[]> {
|
|
240
|
+
const subprojects: string[] = [];
|
|
241
|
+
try {
|
|
242
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (!entry.isDirectory()) continue;
|
|
245
|
+
if (entry.name.startsWith('.') || SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
246
|
+
|
|
247
|
+
const subDir = path.join(dir, entry.name);
|
|
248
|
+
const markers = [
|
|
249
|
+
'package.json', 'index.html', 'project.godot', 'main.py',
|
|
250
|
+
'Cargo.toml', 'requirements.txt', 'go.mod', 'Makefile', 'CMakeLists.txt'
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
for (const marker of markers) {
|
|
254
|
+
try {
|
|
255
|
+
await fs.access(path.join(subDir, marker));
|
|
256
|
+
subprojects.push(entry.name);
|
|
257
|
+
break;
|
|
258
|
+
} catch {
|
|
259
|
+
// ignore
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// ignore
|
|
265
|
+
}
|
|
266
|
+
return subprojects;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async detectKeyFiles(dir: string): Promise<string[]> {
|
|
270
|
+
const candidateFiles = [
|
|
271
|
+
'package.json', 'tsconfig.json', 'index.html', 'main.js',
|
|
272
|
+
'src/index.js', 'src/index.ts', 'src/main.js', 'src/main.ts',
|
|
273
|
+
'src/App.tsx', 'src/App.jsx', 'README.md', 'project.godot',
|
|
274
|
+
'Makefile', 'docker-compose.yml', 'Dockerfile'
|
|
275
|
+
];
|
|
276
|
+
const found: string[] = [];
|
|
277
|
+
for (const rel of candidateFiles) {
|
|
278
|
+
try {
|
|
279
|
+
await fs.access(path.join(dir, rel));
|
|
280
|
+
found.push(rel);
|
|
281
|
+
} catch {
|
|
282
|
+
// ignore
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return found;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async detectPackageManager(dir: string): Promise<string | null> {
|
|
289
|
+
const managers = [
|
|
290
|
+
{ file: 'pnpm-lock.yaml', name: 'pnpm' },
|
|
291
|
+
{ file: 'bun.lockb', name: 'bun' },
|
|
292
|
+
{ file: 'bun.lock', name: 'bun' },
|
|
293
|
+
{ file: 'yarn.lock', name: 'yarn' },
|
|
294
|
+
{ file: 'package-lock.json', name: 'npm' },
|
|
295
|
+
{ file: 'Cargo.lock', name: 'cargo' },
|
|
296
|
+
{ file: 'Pipfile.lock', name: 'pipenv' },
|
|
297
|
+
{ file: 'poetry.lock', name: 'poetry' }
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
for (const m of managers) {
|
|
301
|
+
try {
|
|
302
|
+
await fs.access(path.join(dir, m.file));
|
|
303
|
+
return m.name;
|
|
304
|
+
} catch {
|
|
305
|
+
// ignore
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
await fs.access(path.join(dir, 'package.json'));
|
|
311
|
+
return 'npm';
|
|
312
|
+
} catch {
|
|
313
|
+
// ignore
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async detectGitInfo(dir: string): Promise<GitInfo> {
|
|
320
|
+
let current = dir;
|
|
321
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
322
|
+
const gitDir = path.join(current, '.git');
|
|
323
|
+
try {
|
|
324
|
+
const stat = await fs.stat(gitDir);
|
|
325
|
+
if (stat.isDirectory()) {
|
|
326
|
+
const headFile = path.join(gitDir, 'HEAD');
|
|
327
|
+
const headContent = await fs.readFile(headFile, 'utf8');
|
|
328
|
+
const match = headContent.trim().match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
329
|
+
const branch = match ? (match[1] ?? null) : headContent.trim().slice(0, 8);
|
|
330
|
+
return { branch, gitDir };
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
// ignore
|
|
334
|
+
}
|
|
335
|
+
const parent = path.dirname(current);
|
|
336
|
+
if (parent === current) break;
|
|
337
|
+
current = parent;
|
|
338
|
+
}
|
|
339
|
+
return { branch: null, gitDir: null };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async detectProjectType(dir: string, context: Partial<WorkspaceContextData>): Promise<string> {
|
|
343
|
+
const deps: Record<string, string> = context.packageJson ? {
|
|
344
|
+
...context.packageJson.dependencies,
|
|
345
|
+
...context.packageJson.devDependencies
|
|
346
|
+
} : {};
|
|
347
|
+
|
|
348
|
+
// 1. Game Development (2D Canvas, WebGL, Pixi, Phaser, Godot, Kaplay)
|
|
349
|
+
const gameLibs = ['phaser', 'pixi.js', 'kaboom', 'kaplay', 'three', 'howler', 'matter-js', 'planck'];
|
|
350
|
+
if (gameLibs.some(lib => Boolean(deps[lib]))) {
|
|
351
|
+
return '2D / Canvas Game Project';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
await fs.access(path.join(dir, 'project.godot'));
|
|
356
|
+
return 'Godot Game Project';
|
|
357
|
+
} catch {
|
|
358
|
+
// ignore
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check directory name or key files for game patterns
|
|
362
|
+
const baseName = path.basename(dir).toLowerCase();
|
|
363
|
+
if (baseName.includes('game') || baseName.includes('pixel') || baseName.includes('arcade')) {
|
|
364
|
+
return '2D Game Project';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 2. Web Application
|
|
368
|
+
if (deps['next'] || deps['react'] || deps['vue'] || deps['svelte'] || deps['vite']) {
|
|
369
|
+
return 'Frontend Web Application';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 3. Backend / Server / Service
|
|
373
|
+
if (deps['express'] || deps['fastify'] || deps['koa'] || deps['@nestjs/core']) {
|
|
374
|
+
return 'Backend Web Service';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (context.framework && context.framework.type === 'android') {
|
|
378
|
+
return 'Android Application';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (context.framework && context.framework.type === 'rust') {
|
|
382
|
+
return 'Rust Application / Library';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return 'Software Application';
|
|
386
|
+
}
|
|
387
|
+
}
|
package/src/procSignal.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
const SIGNAL_DISPATCH = "kill";
|
|
3
|
-
|
|
1
|
+
const SIGNAL_DISPATCH = 'kill';
|
|
4
2
|
/**
|
|
5
3
|
* Deliver a signal to a process id. Throws on failure, exactly like the
|
|
6
4
|
* platform call it wraps, so callers can distinguish ESRCH from EPERM.
|
|
7
|
-
* @param
|
|
8
|
-
* @param
|
|
5
|
+
* @param pid
|
|
6
|
+
* @param signal - e.g. 'SIGTERM', or 0 for an existence probe.
|
|
9
7
|
*/
|
|
10
8
|
export function sendSignal(pid, signal) {
|
|
11
|
-
|
|
9
|
+
return process[SIGNAL_DISPATCH](pid, signal);
|
|
12
10
|
}
|
|
13
|
-
|
|
14
11
|
/**
|
|
15
12
|
* Existence probe for a process id.
|
|
16
13
|
*
|
|
@@ -18,15 +15,17 @@ export function sendSignal(pid, signal) {
|
|
|
18
15
|
* EPERM means the process exists but belongs to another user — still alive, so
|
|
19
16
|
* it must count as running. Every other error (ESRCH, invalid pid) means gone.
|
|
20
17
|
*
|
|
21
|
-
* @param
|
|
22
|
-
* @returns
|
|
18
|
+
* @param pid
|
|
19
|
+
* @returns boolean indicating if the process is alive.
|
|
23
20
|
*/
|
|
24
21
|
export function isProcessAlive(pid) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
sendSignal(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return e?.code === 'EPERM';
|
|
30
|
+
}
|
|
32
31
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const SIGNAL_DISPATCH = 'kill' as const;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deliver a signal to a process id. Throws on failure, exactly like the
|
|
5
|
+
* platform call it wraps, so callers can distinguish ESRCH from EPERM.
|
|
6
|
+
* @param pid
|
|
7
|
+
* @param signal - e.g. 'SIGTERM', or 0 for an existence probe.
|
|
8
|
+
*/
|
|
9
|
+
export function sendSignal(pid: number, signal: string | number): true {
|
|
10
|
+
return process[SIGNAL_DISPATCH](pid, signal as NodeJS.Signals | number);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Existence probe for a process id.
|
|
15
|
+
*
|
|
16
|
+
* A signal-0 delivery succeeds when the process exists and we may signal it.
|
|
17
|
+
* EPERM means the process exists but belongs to another user — still alive, so
|
|
18
|
+
* it must count as running. Every other error (ESRCH, invalid pid) means gone.
|
|
19
|
+
*
|
|
20
|
+
* @param pid
|
|
21
|
+
* @returns boolean indicating if the process is alive.
|
|
22
|
+
*/
|
|
23
|
+
export function isProcessAlive(pid: number): boolean {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
25
|
+
try {
|
|
26
|
+
sendSignal(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (e: unknown) {
|
|
29
|
+
return (e as { code?: string })?.code === 'EPERM';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
const MAX_MENTION_FILE_LINES = 500;
|
|
13
|
+
const MAX_MENTION_DIR_ITEMS = 40;
|
|
14
|
+
/**
|
|
15
|
+
* Extract all @ mentions from prompt text.
|
|
16
|
+
*
|
|
17
|
+
* @param text - User prompt text.
|
|
18
|
+
* @returns Array of parsed mentions.
|
|
19
|
+
*/
|
|
20
|
+
export function extractMentions(text) {
|
|
21
|
+
if (!text || typeof text !== 'string')
|
|
22
|
+
return [];
|
|
23
|
+
const mentions = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
// 1. Quoted mentions: @"..." or @'...'
|
|
26
|
+
// This allows spaces, e.g. @"pixel2d games/main.js" or @'assets/sprite sheet.png'
|
|
27
|
+
const quotedRegex = /(?:^|\s)@(["'])([^"'\n\r]+)\1/g;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = quotedRegex.exec(text)) !== null) {
|
|
30
|
+
const raw = match[0].trim();
|
|
31
|
+
const cleanPath = (match[2] ?? '').trim();
|
|
32
|
+
if (cleanPath && !seen.has(cleanPath)) {
|
|
33
|
+
seen.add(cleanPath);
|
|
34
|
+
mentions.push({ raw, path: cleanPath, isQuoted: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 2. Unquoted mentions: @path/to/file or @dir/
|
|
38
|
+
// Matches @ preceded by start-of-line or whitespace, not part of an email address
|
|
39
|
+
const unquotedRegex = /(?:^|\s)@([a-zA-Z0-9_.\-/\\]+)(?=$|\s|[.,;:?!)\]}])/g;
|
|
40
|
+
while ((match = unquotedRegex.exec(text)) !== null) {
|
|
41
|
+
const raw = match[0].trim();
|
|
42
|
+
const cleanPath = (match[1] ?? '').trim();
|
|
43
|
+
// Skip if already captured via quotes or if empty
|
|
44
|
+
if (!cleanPath || seen.has(cleanPath))
|
|
45
|
+
continue;
|
|
46
|
+
// Do not capture npm scope prefixes on their own or common non-file @ words
|
|
47
|
+
if (cleanPath === 'here' || cleanPath === 'all' || cleanPath === 'workspace') {
|
|
48
|
+
seen.add(cleanPath);
|
|
49
|
+
mentions.push({ raw, path: cleanPath, isQuoted: false, isMeta: true });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
seen.add(cleanPath);
|
|
53
|
+
mentions.push({ raw, path: cleanPath, isQuoted: false });
|
|
54
|
+
}
|
|
55
|
+
return mentions;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve mentioned files and folders against the active working directory
|
|
59
|
+
* and detected subprojects. Returns bounded context blocks for model ingestion.
|
|
60
|
+
*
|
|
61
|
+
* @param prompt - Raw prompt string.
|
|
62
|
+
* @param options - Resolution options.
|
|
63
|
+
* @returns Bounded context and list of mentions.
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveMentionedContext(prompt, options = {}) {
|
|
66
|
+
const mentions = extractMentions(prompt);
|
|
67
|
+
if (mentions.length === 0) {
|
|
68
|
+
return { mentions: [], referencedContext: '', detectedSubproject: null };
|
|
69
|
+
}
|
|
70
|
+
const cwd = options.cwd || process.cwd();
|
|
71
|
+
const subprojects = Array.isArray(options.subprojects) ? options.subprojects : [];
|
|
72
|
+
const sections = [];
|
|
73
|
+
let detectedSubproject = null;
|
|
74
|
+
for (const m of mentions) {
|
|
75
|
+
if (m.isMeta) {
|
|
76
|
+
if (m.path === 'workspace' || m.path === 'here') {
|
|
77
|
+
sections.push(`📁 REFERENCED SCOPE: Whole workspace (${cwd})`);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let targetPath = path.resolve(cwd, m.path);
|
|
82
|
+
let found = false;
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(targetPath)) {
|
|
85
|
+
found = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
// Check if path refers to a file/folder inside any detected subproject
|
|
92
|
+
if (!found && subprojects.length > 0) {
|
|
93
|
+
for (const sp of subprojects) {
|
|
94
|
+
const candidate = path.resolve(cwd, sp, m.path);
|
|
95
|
+
try {
|
|
96
|
+
if (fs.existsSync(candidate)) {
|
|
97
|
+
targetPath = candidate;
|
|
98
|
+
found = true;
|
|
99
|
+
detectedSubproject = sp;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Check if the mention path explicitly names a subproject (e.g. @"pixel2d games/...")
|
|
109
|
+
if (subprojects.length > 0) {
|
|
110
|
+
for (const sp of subprojects) {
|
|
111
|
+
if (m.path === sp || m.path.startsWith(sp + '/') || m.path.startsWith(sp + '\\')) {
|
|
112
|
+
detectedSubproject = sp;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const relPath = path.relative(cwd, targetPath);
|
|
118
|
+
if (!found) {
|
|
119
|
+
sections.push(`📂 REFERENCED PATH: ${m.path} (Note: path was not found on disk at ${relPath})`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const stats = fs.statSync(targetPath);
|
|
124
|
+
if (stats.isDirectory()) {
|
|
125
|
+
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
|
126
|
+
const summary = entries
|
|
127
|
+
.filter(e => !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
128
|
+
.slice(0, MAX_MENTION_DIR_ITEMS)
|
|
129
|
+
.map(e => e.isDirectory() ? `${e.name}/` : e.name);
|
|
130
|
+
const more = entries.length > MAX_MENTION_DIR_ITEMS
|
|
131
|
+
? `\n ... (${entries.length - MAX_MENTION_DIR_ITEMS} more items)`
|
|
132
|
+
: '';
|
|
133
|
+
sections.push(`📁 REFERENCED DIRECTORY: ${relPath}/\n` +
|
|
134
|
+
`Directory listing (${summary.length} items):\n` +
|
|
135
|
+
summary.map(s => ` - ${s}`).join('\n') +
|
|
136
|
+
more);
|
|
137
|
+
}
|
|
138
|
+
else if (stats.isFile()) {
|
|
139
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
140
|
+
const lines = content.split('\n');
|
|
141
|
+
let body;
|
|
142
|
+
if (lines.length > MAX_MENTION_FILE_LINES) {
|
|
143
|
+
body = lines.slice(0, MAX_MENTION_FILE_LINES).join('\n') +
|
|
144
|
+
`\n\n... [Truncated: showing first ${MAX_MENTION_FILE_LINES} of ${lines.length} lines. Use fs.read for remaining lines.]`;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
body = content;
|
|
148
|
+
}
|
|
149
|
+
sections.push(`📄 REFERENCED FILE: ${relPath} (${lines.length} lines):\n` +
|
|
150
|
+
'```\n' +
|
|
151
|
+
body + '\n' +
|
|
152
|
+
'```');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
157
|
+
sections.push(`📂 REFERENCED PATH: ${relPath} (Error reading: ${msg})`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const referencedContext = sections.length > 0
|
|
161
|
+
? `\n\n=== REFERENCED WORKSPACE CONTEXT (@ mentions) ===\n` +
|
|
162
|
+
sections.join('\n\n') +
|
|
163
|
+
`\n=== END REFERENCED WORKSPACE CONTEXT ===\n`
|
|
164
|
+
: '';
|
|
165
|
+
return {
|
|
166
|
+
mentions,
|
|
167
|
+
referencedContext,
|
|
168
|
+
detectedSubproject
|
|
169
|
+
};
|
|
170
|
+
}
|