@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.
Files changed (55) hide show
  1. package/native-skills/media/professional-document-publishing/SKILL.md +44 -0
  2. package/native-skills/media/screenplay-and-shotlist-forge/SKILL.md +42 -0
  3. package/package.json +1 -1
  4. package/src/acp/client.js +235 -0
  5. package/src/acp/client.ts +282 -0
  6. package/src/acp/peerProfiles.js +122 -0
  7. package/src/acp/peerProfiles.ts +128 -0
  8. package/src/acp/protocol.js +54 -0
  9. package/src/acp/protocol.ts +434 -0
  10. package/src/acp/server.js +609 -0
  11. package/src/acp/server.ts +676 -0
  12. package/src/agentLoop.d.ts +43 -0
  13. package/src/agentLoop.js +142 -8
  14. package/src/commands.js +203 -0
  15. package/src/compiledSkills.js +10 -0
  16. package/src/documentEngine/antiSlopAuditor.js +156 -0
  17. package/src/documentEngine/antiSlopAuditor.ts +221 -0
  18. package/src/documentEngine/documentForge.js +443 -0
  19. package/src/documentEngine/documentForge.ts +519 -0
  20. package/src/documentEngine/screenplayFormatter.js +336 -0
  21. package/src/documentEngine/screenplayFormatter.ts +407 -0
  22. package/src/entrypoint.js +13 -0
  23. package/src/execSafe.js +10 -13
  24. package/src/execSafe.ts +25 -0
  25. package/src/modelRouter.js +56 -123
  26. package/src/modelRouter.ts +134 -0
  27. package/src/observer/advisory.js +164 -0
  28. package/src/observer/advisory.ts +171 -0
  29. package/src/observer/coldObserver.js +462 -0
  30. package/src/observer/coldObserver.ts +540 -0
  31. package/src/observer/evidence.js +435 -0
  32. package/src/observer/evidence.ts +499 -0
  33. package/src/perception.js +303 -118
  34. package/src/perception.ts +387 -0
  35. package/src/procSignal.js +15 -16
  36. package/src/procSignal.ts +31 -0
  37. package/src/promptMentions.js +170 -0
  38. package/src/promptMentions.ts +212 -0
  39. package/src/sandbox.js +1 -1
  40. package/src/textDiff.js +117 -109
  41. package/src/textDiff.ts +169 -0
  42. package/src/toolPacks.js +5 -2
  43. package/src/tools/documentForge.js +215 -0
  44. package/src/tools/documentForge.ts +265 -0
  45. package/src/tools/peerAgent.js +102 -0
  46. package/src/tools/peerAgent.ts +116 -0
  47. package/src/tools/registry.js +3 -1
  48. package/src/tui/completion.js +97 -41
  49. package/src/tui/prompt.js +17 -17
  50. package/src/uncertainty.js +93 -205
  51. package/src/uncertainty.ts +255 -0
  52. package/src/utils/paths.js +15 -18
  53. package/src/utils/paths.ts +36 -0
  54. package/src/vfeTracker.js +122 -181
  55. 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 {number} pid
8
- * @param {string|number} signal - e.g. 'SIGTERM', or 0 for an existence probe.
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
- return process[SIGNAL_DISPATCH](pid, signal);
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 {number} pid
22
- * @returns {boolean}
18
+ * @param pid
19
+ * @returns boolean indicating if the process is alive.
23
20
  */
24
21
  export function isProcessAlive(pid) {
25
- if (!Number.isInteger(pid) || pid <= 0) return false;
26
- try {
27
- sendSignal(pid, 0);
28
- return true;
29
- } catch (e) {
30
- return e.code === 'EPERM';
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
+ }