@causyne/agent 1.10.13 → 1.10.18

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 (76) hide show
  1. package/docs/ARCHITECTURE.md +118 -17
  2. package/docs/SECURITY_REVIEW.md +13 -1
  3. package/docs/THIRD_PARTY_NOTICES.md +46 -10
  4. package/package.json +10 -4
  5. package/src/agentLoop.js +123 -21
  6. package/src/agentProfiles.js +135 -0
  7. package/src/agentProfiles.ts +143 -0
  8. package/src/approvalGrants.js +17 -35
  9. package/src/approvalGrants.ts +54 -0
  10. package/src/auditLog.d.ts +3 -0
  11. package/src/browser/browserRuntime.js +88 -0
  12. package/src/browser/browserRuntime.ts +107 -0
  13. package/src/browser/causyneBrowser.js +495 -0
  14. package/src/browser/causyneBrowser.ts +594 -0
  15. package/src/browser/searchProviders.js +156 -0
  16. package/src/browser/searchProviders.ts +156 -0
  17. package/src/browser/snapshotWindow.js +28 -0
  18. package/src/browser/snapshotWindow.ts +40 -0
  19. package/src/browser/urlPolicy.js +89 -0
  20. package/src/browser/urlPolicy.ts +89 -0
  21. package/src/checkpoints.js +19 -6
  22. package/src/cliMode.js +13 -1
  23. package/src/commands.js +37 -8
  24. package/src/config.d.ts +2 -0
  25. package/src/decisionPolicy.js +68 -0
  26. package/src/decisionPolicy.ts +106 -0
  27. package/src/hooks.js +121 -107
  28. package/src/hooks.ts +193 -0
  29. package/src/implementationPlan.js +50 -0
  30. package/src/implementationPlan.ts +59 -0
  31. package/src/mcp/mcpClient.js +520 -132
  32. package/src/mcp/mcpClient.ts +584 -0
  33. package/src/mcp/mcpRegistry.js +399 -185
  34. package/src/mcp/mcpRegistry.ts +451 -0
  35. package/src/planCache.js +473 -450
  36. package/src/planCache.ts +840 -0
  37. package/src/plugins/pluginManager.js +40 -5
  38. package/src/plugins/pluginSkills.js +84 -0
  39. package/src/plugins/pluginSkills.ts +106 -0
  40. package/src/provisioning/capabilityProvisioner.d.ts +10 -0
  41. package/src/provisioning/capabilityProvisioner.js +5 -0
  42. package/src/provisioning/capabilityTools.js +2 -1
  43. package/src/sandbox.js +32 -2
  44. package/src/sessionJournal.js +471 -0
  45. package/src/sessionJournal.ts +496 -0
  46. package/src/sessionStore.js +3 -279
  47. package/src/skillsManager.js +18 -0
  48. package/src/subagentJournal.js +215 -0
  49. package/src/subagentJournal.ts +254 -0
  50. package/src/taskShape.js +179 -198
  51. package/src/taskShape.ts +259 -0
  52. package/src/toolPacks.js +7 -5
  53. package/src/tools/browserAutomator.js +45 -241
  54. package/src/tools/browserAutomator.ts +57 -0
  55. package/src/tools/manageLocalAiDependencies.js +1 -1
  56. package/src/tools/registry.js +4 -3
  57. package/src/tools/requestDecision.js +87 -0
  58. package/src/tools/requestDecision.ts +107 -0
  59. package/src/tools/subagentOrchestrator.js +73 -12
  60. package/src/tools/updatePlan.js +37 -84
  61. package/src/tools/updatePlan.ts +48 -0
  62. package/src/tools/webFetch.js +183 -68
  63. package/src/tools/webFetch.ts +185 -0
  64. package/src/tools/webSearch.js +50 -75
  65. package/src/tools/webSearch.ts +56 -0
  66. package/src/tui/ask.js +41 -1
  67. package/src/tui/chatRenderer.js +28 -0
  68. package/src/tui/dialogs.js +91 -0
  69. package/src/tui/prompt.js +32 -3
  70. package/src/tui/toolDisplay.js +0 -1
  71. package/src/types/contracts.js +8 -0
  72. package/src/types/contracts.ts +263 -0
  73. package/src/utils/paths.d.ts +3 -0
  74. package/src/version.d.ts +1 -0
  75. package/src/mcp/jsonRpc.js +0 -165
  76. package/src/tools/todoWrite.js +0 -75
@@ -0,0 +1,259 @@
1
+ import type {
2
+ TaskPackScore,
3
+ TaskPhase,
4
+ TaskShape,
5
+ WorkspaceContext
6
+ } from './types/contracts.js';
7
+
8
+ interface SignalSet {
9
+ strong: string[];
10
+ weak: string[];
11
+ }
12
+
13
+ interface ArchetypeDefinition {
14
+ label: string;
15
+ match: SignalSet;
16
+ phases: TaskPhase[];
17
+ }
18
+
19
+ const PACK_SIGNALS: Record<string, SignalSet> = {
20
+ game: {
21
+ strong: ['game', 'gamedev', 'permainan', 'godot', 'unity', 'unreal', 'rpg', 'platformer', 'roguelike', 'arcade', 'voxel', 'minecraft', 'tilemap', 'spritesheet', 'boss fight', 'level design'],
22
+ weak: ['sprite', 'player', 'enemy', 'quest', 'npc', 'gameplay', 'multiplayer', 'leaderboard']
23
+ },
24
+ web: {
25
+ strong: ['web app', 'webapp', 'website', 'situs', 'landing page', 'homepage', 'frontend', 'front-end', 'dashboard', 'portfolio', 'react', 'vue', 'svelte', 'angular', 'nextjs', 'next.js', 'nuxt', 'astro', 'tailwind', 'html', 'css'],
26
+ weak: ['web', 'ui', 'ux', 'page', 'halaman', 'responsive', 'browser', 'dom', 'component', 'layout', 'design', 'desain', 'tampilan']
27
+ },
28
+ media: {
29
+ strong: ['generate image', 'generate video', 'generate music', 'text to speech', 'voiceover', 'blender', 'davinci', 'resolve', 'render', '3d model', 'animation', 'animasi'],
30
+ weak: ['image', 'gambar', 'photo', 'foto', 'video', 'audio', 'sound', 'suara', 'music', 'musik', 'lagu', 'thumbnail', 'poster', 'logo', 'illustration']
31
+ },
32
+ system: {
33
+ strong: ['kill process', 'port', 'zip', 'unzip', 'tar', 'archive', 'clipboard', 'printer', 'print', 'cetak'],
34
+ weak: ['process', 'proses', 'compress', 'extract', 'ekstrak', 'open app', 'launch']
35
+ },
36
+ advanced: {
37
+ strong: ['pdf', 'forge a tool', 'custom tool', 'plugin', 'plugins', 'custom plugin', 'subagent', 'sub-agent', 'parallel agent'],
38
+ weak: ['symbols', 'ast', 'refactor', 'orchestrate', 'swarm']
39
+ }
40
+ };
41
+
42
+ const STRONG_WEIGHT = 3;
43
+ const WEAK_WEIGHT = 1;
44
+ const PACK_THRESHOLD = 3;
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Task archetypes. Each one carries the phase plan the agent should follow and
48
+ // the tools that matter in each phase.
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const ARCHETYPES: Record<string, ArchetypeDefinition> = {
52
+ integrate: {
53
+ label: 'Build an application that integrates live external data',
54
+ match: {
55
+ strong: ['realtime', 'real-time', 'real time', 'api', 'rest', 'webhook', 'integration', 'integrasi', 'sync', 'sinkron', 'automation', 'automasi', 'otomasi', 'automate', 'otomatis', 'booking', 'tiket', 'ticket', 'calendar', 'kalender', 'jadwal', 'schedule', 'payment', 'pembayaran'],
56
+ weak: ['live', 'stream', 'fetch', 'data', 'third party', 'oauth', 'token']
57
+ },
58
+ phases: [
59
+ { name: 'Research the data sources FIRST', tools: ['web_search', 'web_fetch'], note: 'Identify the exact official REST APIs, their auth model, rate limits and response shape before writing any code. Prefer documented JSON APIs over browser scraping — reading JSON always beats reading DOM.' },
60
+ { name: 'Verify access and versions', tools: ['sysRun', 'web_fetch'], note: 'Probe one real endpoint and check current package versions with `npm view <pkg> version` before pinning anything. Never assume a version from memory.' },
61
+ { name: 'Scaffold', tools: ['fs_write', 'sysRun'], note: 'Create the project structure and dependency manifest.' },
62
+ { name: 'Implement', tools: ['fs_read', 'fs_edit', 'MultiEdit'], note: 'Build the integration layer first, the UI second. Handle auth failures and rate limits explicitly.' },
63
+ { name: 'Verify', tools: ['code_verify_syntax', 'sysRun'], note: 'Run the build, lint and type-check. Exercise the live data path end to end, not just the happy path.' }
64
+ ]
65
+ },
66
+ build_app: {
67
+ label: 'Build a new application from scratch',
68
+ match: {
69
+ strong: ['build me', 'buatkan', 'buat aku', 'create a', 'make me', 'bikin', 'develop a', 'new app', 'from scratch'],
70
+ weak: ['app', 'aplikasi', 'project', 'proyek', 'tool', 'system', 'sistem']
71
+ },
72
+ phases: [
73
+ { name: 'Plan', tools: ['update_plan'], note: 'Create a visible atomic implementation plan before touching the filesystem.' },
74
+ { name: 'Scaffold', tools: ['fs_write', 'sysRun'], note: 'Create structure and install dependencies at verified current versions.' },
75
+ { name: 'Implement', tools: ['fs_write', 'fs_edit', 'MultiEdit'], note: 'Write runnable code with every import and dependency present.' },
76
+ { name: 'Verify', tools: ['code_verify_syntax', 'sysRun'], note: 'Build, lint and run it. Never assume code works.' }
77
+ ]
78
+ },
79
+ debug: {
80
+ label: 'Diagnose and fix a defect in existing code',
81
+ match: {
82
+ strong: ['bug', 'error', 'crash', 'fix', 'perbaiki', 'benerin', 'broken', 'rusak', 'not working', 'gagal', 'failing', 'exception', 'stack trace', 'regression'],
83
+ weak: ['issue', 'masalah', 'problem', 'wrong', 'salah', 'debug']
84
+ },
85
+ phases: [
86
+ { name: 'Reproduce', tools: ['sysRun', 'fs_read'], note: 'Reproduce the failure and capture the exact error before changing anything.' },
87
+ { name: 'Locate', tools: ['code_search_ripgrep', 'codebase_search', 'fs_read'], note: 'Search several phrasings in parallel. Read the file before patching it.' },
88
+ { name: 'Fix', tools: ['fs_edit', 'MultiEdit'], note: 'Make the smallest correct change. Do not touch unrelated code.' },
89
+ { name: 'Verify', tools: ['sysRun', 'code_verify_syntax'], note: 'Confirm the original reproduction now passes.' }
90
+ ]
91
+ },
92
+ research: {
93
+ label: 'Investigate and report, without changing the system',
94
+ match: {
95
+ strong: ['explain', 'jelaskan', 'analyse', 'analyze', 'analisa', 'research', 'riset', 'compare', 'bandingkan', 'what is', 'apa itu', 'how does', 'bagaimana', 'review', 'audit'],
96
+ weak: ['understand', 'pahami', 'find out', 'cari tahu', 'summarize', 'ringkas']
97
+ },
98
+ phases: [
99
+ { name: 'Gather', tools: ['code_search_ripgrep', 'fs_read', 'web_search', 'web_fetch'], note: 'Search broadly with several phrasings in parallel before concluding.' },
100
+ { name: 'Report', tools: [], note: 'Answer concisely and cite exact file paths or URLs. Do not modify anything unless asked.' }
101
+ ]
102
+ },
103
+ maintain: {
104
+ label: 'Operate on files, processes or the machine itself',
105
+ match: {
106
+ strong: ['organize', 'rapikan', 'clean up', 'bersihkan', 'rename', 'ganti nama', 'move', 'pindahkan', 'backup', 'cadangkan', 'delete', 'hapus', 'install', 'deploy', 'commit', 'merge'],
107
+ weak: ['folder', 'directory', 'file', 'berkas', 'desktop', 'downloads']
108
+ },
109
+ phases: [
110
+ { name: 'Survey', tools: ['fs_list', 'fs_find'], note: 'List and count what you are about to touch, and report it, before acting.' },
111
+ { name: 'Act', tools: ['fs_move', 'fs_write', 'sysRun'], note: 'Prefer reversible operations. Flag anything destructive for approval.' },
112
+ { name: 'Confirm', tools: ['fs_list'], note: 'Verify the end state matches what the user asked for.' }
113
+ ]
114
+ }
115
+ };
116
+
117
+ const ARCHETYPE_NAMES = Object.keys(ARCHETYPES);
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Scoring
121
+ // ---------------------------------------------------------------------------
122
+
123
+ function scoreSignals(text: string, signals: SignalSet): { score: number; hits: string[] } {
124
+ let score = 0;
125
+ const hits: string[] = [];
126
+
127
+ for (const term of signals.strong || []) {
128
+ if (text.includes(term)) {
129
+ score += STRONG_WEIGHT;
130
+ hits.push(term);
131
+ }
132
+ }
133
+ for (const term of signals.weak || []) {
134
+ // Word-boundary check for short weak terms so 'ui' cannot match 'build'
135
+ // and 'race' cannot match 'trace' — the exact class of false positive that
136
+ // used to drag ordinary tasks into game and web enforcement.
137
+ const pattern = new RegExp(`(^|[^a-z0-9])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^a-z0-9])`, 'i');
138
+ if (pattern.test(text)) {
139
+ score += WEAK_WEIGHT;
140
+ hits.push(term);
141
+ }
142
+ }
143
+
144
+ return { score, hits };
145
+ }
146
+
147
+ /**
148
+ * Analyse a request into the shape of the work it implies.
149
+ *
150
+ */
151
+ export function analyzeTask(prompt: string, workspaceContext: WorkspaceContext | null = null): TaskShape {
152
+ const text = String(prompt || '').toLowerCase();
153
+
154
+ // Which capability packs are worth their token cost for this request.
155
+ const packScores: TaskPackScore[] = [];
156
+ for (const packName of Object.keys(PACK_SIGNALS)) {
157
+ const signals = PACK_SIGNALS[packName];
158
+ if (!signals) continue;
159
+ const { score } = scoreSignals(text, signals);
160
+ if (score >= PACK_THRESHOLD) {
161
+ packScores.push({ name: packName, score });
162
+ }
163
+ }
164
+
165
+ // Workspace evidence outranks keywords: a Godot project on disk is far more
166
+ // reliable than any phrasing in the prompt.
167
+ if (workspaceContext) {
168
+ const signals = [
169
+ workspaceContext.framework,
170
+ workspaceContext.projectType,
171
+ workspaceContext.designSystem,
172
+ Array.isArray(workspaceContext.keyFiles) ? workspaceContext.keyFiles.join(' ') : ''
173
+ ].filter(Boolean).join(' ').toLowerCase();
174
+
175
+ const bump = (name: string, amount: number): void => {
176
+ const existing = packScores.find(p => p.name === name);
177
+ if (existing) existing.score += amount;
178
+ else packScores.push({ name, score: amount });
179
+ };
180
+
181
+ if (/godot|project\.godot|unity|\.tscn/.test(signals)) bump('game', 5);
182
+ if (/react|vue|svelte|next|nuxt|astro|angular|tailwind|vite|index\.html/.test(signals)) bump('web', 4);
183
+ }
184
+
185
+ packScores.sort((a, b) => b.score - a.score);
186
+
187
+ // Which archetype the work follows.
188
+ let bestArchetype: string | null = null;
189
+ let bestScore = 0;
190
+ for (const name of ARCHETYPE_NAMES) {
191
+ const definition = ARCHETYPES[name];
192
+ if (!definition) continue;
193
+ const { score } = scoreSignals(text, definition.match);
194
+ if (score > bestScore) {
195
+ bestScore = score;
196
+ bestArchetype = name;
197
+ }
198
+ }
199
+
200
+ // A build request that also talks about live external data is an integration
201
+ // job first and a build job second — that ordering is what stops the agent
202
+ // scaffolding a UI before it knows whether the API even exists.
203
+ const integrate = ARCHETYPES.integrate;
204
+ if (!integrate) throw new Error('integrate task archetype is unavailable');
205
+ const integrationScore = scoreSignals(text, integrate.match).score;
206
+ if (integrationScore >= STRONG_WEIGHT && bestArchetype === 'build_app') {
207
+ bestArchetype = 'integrate';
208
+ bestScore = integrationScore;
209
+ }
210
+
211
+ const archetype = bestArchetype || 'build_app';
212
+ const confidence = bestScore >= 6 ? 'high' : (bestScore >= 3 ? 'medium' : 'low');
213
+
214
+ const definition = ARCHETYPES[archetype];
215
+ if (!definition) throw new Error(`task archetype ${archetype} is unavailable`);
216
+ return {
217
+ archetype,
218
+ archetypeLabel: definition.label,
219
+ packs: packScores,
220
+ phases: definition.phases,
221
+ confidence
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Render the analysis as a compact brief for the system prompt.
227
+ *
228
+ * This is the piece that turns "here are 28 tools" into "here is the order to
229
+ * use them in". It costs roughly 150-200 tokens and routinely saves whole
230
+ * exploratory steps, each of which costs thousands.
231
+ */
232
+ export function buildTaskBrief(
233
+ shape: TaskShape | null | undefined,
234
+ availableToolNames: readonly string[] | null | undefined
235
+ ): string {
236
+ if (!shape) return '';
237
+
238
+ const available = new Set(availableToolNames || []);
239
+ const lines: string[] = [];
240
+
241
+ lines.push('# TASK SHAPE (analysed from your request)');
242
+ lines.push(`Work type: ${shape.archetypeLabel}.`);
243
+ lines.push('Follow this sequence. Do not skip ahead — each phase de-risks the next.');
244
+
245
+ shape.phases.forEach((phase, index) => {
246
+ const tools = (phase.tools || []).filter(name => available.has(name));
247
+ const toolList = tools.length > 0 ? ` [${tools.join(', ')}]` : '';
248
+ lines.push(`${index + 1}. ${phase.name}${toolList} — ${phase.note}`);
249
+ });
250
+
251
+ lines.push('Tools outside this list remain fully available; search for them with tool_search when a phase genuinely needs one.');
252
+
253
+ return lines.join('\n');
254
+ }
255
+
256
+ /** Pack names above threshold, highest score first. */
257
+ export function packsFromShape(shape: TaskShape | null | undefined): string[] {
258
+ return shape && Array.isArray(shape.packs) ? shape.packs.map(p => p.name) : [];
259
+ }
package/src/toolPacks.js CHANGED
@@ -36,8 +36,8 @@ export const CORE_TOOLS = [
36
36
  'code_verify_syntax',
37
37
  'web_search',
38
38
  'web_fetch',
39
- 'TodoWrite',
40
39
  'update_plan',
40
+ 'request_decision',
41
41
  'memory_write',
42
42
  'fs_read_skill',
43
43
  'skill_create',
@@ -130,9 +130,11 @@ export const PACK_NAMES = Object.keys(PACKS);
130
130
  *
131
131
  * WHAT IS IN, AND WHY EXACTLY THESE: read, write, edit, list and search cover
132
132
  * every file operation a first turn can need; `sysRun` is the escape hatch that
133
- * can reach anything the other seven cannot; `TodoWrite` is how a multi-step
134
- * task announces its plan on step one; `tool_search` is the door to the other
135
- * 62. With this set the agent is never stuck — worst case it spends one call
133
+ * can reach anything the other resident tools cannot; `update_plan` is how a
134
+ * multi-step task announces its plan on step one; `tool_search` is the door to
135
+ * deferred tools. `request_decision` is injected on the first turn only when
136
+ * task policy detects an unresolved durable choice. With this set the agent is
137
+ * never stuck — worst case it spends one call
136
138
  * discovering the tool it wants, and the tool it discovers stays loaded for the
137
139
  * rest of the task.
138
140
  *
@@ -147,7 +149,7 @@ export const RESIDENT_TOOLS = [
147
149
  'fs_list',
148
150
  'fs_search',
149
151
  'sysRun',
150
- 'TodoWrite',
152
+ 'update_plan',
151
153
  'tool_search'
152
154
  ];
153
155
 
@@ -1,252 +1,56 @@
1
- import os from 'os';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { ensureCapability, loadCapabilityPackage } from '../provisioning/capabilityProvisioner.js';
5
- import { causyneHome } from '../utils/paths.js';
6
-
7
- function getChromeExecutablePath() {
8
- const platform = os.platform();
9
- if (platform === 'win32') {
10
- return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
11
- } else if (platform === 'darwin') {
12
- return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
13
- } else {
14
- return '/usr/bin/google-chrome';
15
- }
16
- }
17
-
18
- function getChromeUserDataDir() {
19
- const platform = os.platform();
20
- const home = os.homedir();
21
- if (platform === 'win32') {
22
- return `${home}\\AppData\\Local\\Google\\Chrome\\User Data`;
23
- } else if (platform === 'darwin') {
24
- return `${home}/Library/Application Support/Google/Chrome`;
25
- } else {
26
- return `${home}/.config/google-chrome`;
27
- }
28
- }
29
-
30
- let activeBrowser = null;
31
- let activePage = null;
32
-
1
+ import { BrowserCapabilityError, causyneBrowser } from '../browser/causyneBrowser.js';
33
2
  export const browserAct = {
34
- schema: {
35
- name: 'browser.act',
36
- description: 'Automate Google Chrome using local profile to scrape sites or perform actions. IMPORTANT: First use "scrape" to get the page mapped to numerical indices. Then use "clickIndex" or "typeIndex" for flawless precision. Defaults to ~/.causyne/chrome_profile to protect your personal accounts from logout.',
37
- parameters: {
38
- type: 'object',
39
- properties: {
40
- usePersonalProfile: {
41
- type: 'boolean',
42
- description: 'Set to true ONLY if you explicitly want to use your personal Google Chrome profile. Defaults to false (uses safe dedicated profile ~/.causyne/chrome_profile).'
43
- },
44
- profileDirectory: {
45
- type: 'string',
46
- description: 'Specific profile sub-folder when usePersonalProfile is true (e.g. "Profile 1", "Default", "Profile 2").'
47
- },
48
- actions: {
49
- type: 'array',
50
- description: 'List of actions to perform in order',
51
- items: {
3
+ schema: {
4
+ name: 'browser.act',
5
+ description: 'Use the dedicated Causyne Browser for resilient search, navigation, accessible snapshots, interaction, extraction, and workspace-scoped downloads. Take a snapshot before interacting by ref.',
6
+ parameters: {
52
7
  type: 'object',
53
8
  properties: {
54
- type: { type: 'string', enum: ['goto', 'click', 'type', 'clickIndex', 'typeIndex', 'waitFor', 'scrape', 'selectOption', 'keyPress', 'scroll'] },
55
- url: { type: 'string' },
56
- selector: { type: 'string', description: 'CSS selector (legacy)' },
57
- index: { type: 'number', description: 'Element index from the scrape map' },
58
- text: { type: 'string' },
59
- value: { type: 'string', description: 'Option value to select in dropdowns' },
60
- key: { type: 'string', description: 'Key to press (Enter, Tab, ArrowDown, etc.)' },
61
- humanDelayMs: { type: 'number', description: 'Keystroke delay in ms for natural human-like typing (default 45ms)' },
62
- timeoutMs: { type: 'number' }
9
+ actions: {
10
+ type: 'array',
11
+ minItems: 1,
12
+ maxItems: 40,
13
+ items: {
14
+ type: 'object',
15
+ properties: {
16
+ type: {
17
+ type: 'string',
18
+ enum: ['newTab', 'switchTab', 'listTabs', 'closeTab', 'search', 'goto', 'snapshot', 'scrape', 'click', 'clickIndex', 'type', 'typeIndex', 'selectOption', 'keyPress', 'scroll', 'back', 'forward', 'reload', 'extract', 'download', 'screenshot']
19
+ },
20
+ tabId: { type: 'string', description: 'Tab id returned by newTab or listTabs.' },
21
+ url: { type: 'string', description: 'Public http(s) URL for goto or newTab.' },
22
+ query: { type: 'string', description: 'Search query.' },
23
+ ref: { type: 'string', description: 'Current accessibility ref, for example e12.' },
24
+ index: { type: 'number', description: 'Legacy numeric form of an accessibility ref.' },
25
+ selector: { type: 'string', description: 'CSS selector when a current accessibility ref is unavailable.' },
26
+ text: { type: 'string' },
27
+ value: { type: 'string' },
28
+ key: { type: 'string' },
29
+ direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
30
+ amount: { type: 'number' },
31
+ offset: { type: 'number', description: 'Character offset for a paged accessibility snapshot.' },
32
+ outputFile: { type: 'string', description: 'Download or screenshot destination inside the selected workspace.' },
33
+ fullPage: { type: 'boolean', description: 'Capture the complete page for a screenshot instead of the visible viewport.' }
34
+ },
35
+ required: ['type'],
36
+ additionalProperties: false
37
+ }
38
+ }
63
39
  },
64
- required: ['type']
65
- }
40
+ required: ['actions'],
41
+ additionalProperties: false
66
42
  }
67
- },
68
- required: ['actions']
69
- }
70
- },
71
- async execute(args) {
72
- const vision = await ensureCapability('vision');
73
- if (!vision.ok) return { success: false, error: vision.reason, requiresConsent: vision.requiresConsent };
74
- const puppeteer = await loadCapabilityPackage('puppeteer-core');
75
-
76
- let results = [];
77
-
78
- try {
79
- if (!activeBrowser || !activeBrowser.isConnected()) {
80
- const executablePath = getChromeExecutablePath();
81
- const primaryUserDataDir = getChromeUserDataDir();
82
- let connectedOrLaunched = false;
83
-
84
- // Layer 1: Attempt connection to active Chrome Remote Debugging port (9222) for zero-interruption live session reuse
43
+ },
44
+ async execute(args) {
85
45
  try {
86
- const controller = new AbortController();
87
- const timeoutId = setTimeout(() => controller.abort(), 600);
88
- const resp = await fetch('http://127.0.0.1:9222/json/version', { signal: controller.signal });
89
- clearTimeout(timeoutId);
90
- if (resp.ok) {
91
- const data = await resp.json();
92
- if (data.webSocketDebuggerUrl) {
93
- activeBrowser = await puppeteer.connect({
94
- browserWSEndpoint: data.webSocketDebuggerUrl,
95
- defaultViewport: null
96
- });
97
- connectedOrLaunched = true;
98
- results.push('[Sovereign Browser] Attached directly to active Chrome session via port 9222 (Layer 1).');
99
- }
100
- }
101
- } catch (debugErr) {
102
- // Port 9222 not active, proceed to Layer 2 / Layer 3 decision
46
+ const results = await causyneBrowser().run(args.actions ?? []);
47
+ return { success: true, output: JSON.stringify(results, null, 2), data: results };
103
48
  }
104
-
105
- // Sovereign Guard: Decide whether to launch dedicated profile (Layer 2) or personal profile (Layer 3)
106
- if (!connectedOrLaunched) {
107
- const fallbackDir = path.join(causyneHome(), 'chrome_profile');
108
- if (!fs.existsSync(fallbackDir)) fs.mkdirSync(fallbackDir, { recursive: true });
109
-
110
- if (!args.usePersonalProfile) {
111
- // Layer 2: Sovereign Dedicated Agent Profile (100% Safe - Never risks logging out personal Chrome accounts)
112
- activeBrowser = await puppeteer.launch({
113
- executablePath,
114
- userDataDir: fallbackDir,
115
- headless: false,
116
- defaultViewport: null,
117
- args: ['--no-sandbox', '--disable-setuid-sandbox']
118
- });
119
- connectedOrLaunched = true;
120
- results.push('[Sovereign Browser] Launched Chrome using Sovereign Dedicated Profile (~/.causyne/chrome_profile). Your personal Google Chrome accounts remain 100% untouched and protected.');
121
- } else {
122
- // Layer 3: Explicit Personal Profile Guard (Strictly when usePersonalProfile: true is requested)
123
- try {
124
- const profileArgs = ['--no-sandbox', '--disable-setuid-sandbox'];
125
- if (args.profileDirectory) {
126
- profileArgs.push(`--profile-directory=${args.profileDirectory}`);
127
- }
128
- activeBrowser = await puppeteer.launch({
129
- executablePath,
130
- userDataDir: primaryUserDataDir,
131
- headless: false,
132
- defaultViewport: null,
133
- args: profileArgs
134
- });
135
- connectedOrLaunched = true;
136
- results.push(`[Sovereign Browser] Launched primary profile (${args.profileDirectory || 'Default'}) explicitly requested via usePersonalProfile.`);
137
- } catch (launchErr) {
138
- const isProfileLock = launchErr.message.includes('already running') ||
139
- launchErr.message.includes('SingletonLock') ||
140
- launchErr.message.includes('EBUSY') ||
141
- launchErr.message.includes('lock') ||
142
- launchErr.message.includes('user data directory is already in use');
143
- if (isProfileLock) {
144
- // Safe fallback to dedicated profile so task never fails and never corrupts personal cookies
145
- results.push(`[Sovereign Profile Guard] Your personal Chrome profile (${args.profileDirectory || 'Default'}) is currently running and locked without remote debugging port 9222. Automatically switched to Sovereign Dedicated Profile (~/.causyne/chrome_profile) to protect your personal account session from logout.`);
146
- activeBrowser = await puppeteer.launch({
147
- executablePath,
148
- userDataDir: fallbackDir,
149
- headless: false,
150
- defaultViewport: null,
151
- args: ['--no-sandbox', '--disable-setuid-sandbox']
152
- });
153
- connectedOrLaunched = true;
154
- } else {
155
- throw launchErr;
156
- }
49
+ catch (error) {
50
+ if (error instanceof BrowserCapabilityError) {
51
+ return { success: false, error: error.message, requiresConsent: error.requiresConsent };
157
52
  }
158
- }
159
- }
160
-
161
- const pages = await activeBrowser.pages();
162
- activePage = pages[0] || (await activeBrowser.newPage());
163
- }
164
-
165
- for (const action of args.actions) {
166
- if (action.type === 'goto') {
167
- await activePage.goto(action.url, { waitUntil: 'networkidle2' });
168
- results.push(`Navigated to ${action.url}`);
169
- }
170
- else if (action.type === 'click') {
171
- await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 5000 });
172
- await activePage.click(action.selector);
173
- results.push(`Clicked ${action.selector}`);
174
- }
175
- else if (action.type === 'type') {
176
- await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 5000 });
177
- await activePage.type(action.selector, action.text, { delay: action.humanDelayMs || 45 });
178
- results.push(`Typed text into ${action.selector} with human keystroke delay`);
179
- }
180
- // Phase 1 AGI Upgrade: DOM-to-Markdown Interaction
181
- else if (action.type === 'clickIndex') {
182
- const selector = `[data-causyne-id="${action.index}"]`;
183
- await activePage.waitForSelector(selector, { timeout: action.timeoutMs || 5000 });
184
- await activePage.click(selector);
185
- results.push(`Clicked element mapped to index [${action.index}]`);
186
- }
187
- else if (action.type === 'typeIndex') {
188
- const selector = `[data-causyne-id="${action.index}"]`;
189
- await activePage.waitForSelector(selector, { timeout: action.timeoutMs || 5000 });
190
- await activePage.type(selector, action.text, { delay: action.humanDelayMs || 45 });
191
- results.push(`Typed text into element mapped to index [${action.index}] with human keystroke delay`);
192
- }
193
- else if (action.type === 'selectOption') {
194
- if (action.index !== undefined) {
195
- const selector = `[data-causyne-id="${action.index}"]`;
196
- await activePage.waitForSelector(selector, { timeout: action.timeoutMs || 5000 });
197
- await activePage.select(selector, action.value || action.text);
198
- } else if (action.selector) {
199
- await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 5000 });
200
- await activePage.select(action.selector, action.value || action.text);
201
- }
202
- results.push(`Selected option [${action.value || action.text}] from dropdown`);
203
- }
204
- else if (action.type === 'keyPress') {
205
- await activePage.keyboard.press(action.key || 'Enter');
206
- results.push(`Pressed keyboard key [${action.key || 'Enter'}]`);
207
- }
208
- else if (action.type === 'scroll') {
209
- await activePage.evaluate(() => {
210
- window.scrollBy({ top: window.innerHeight * 0.75, behavior: 'smooth' });
211
- });
212
- results.push(`Scrolled page smoothly`);
213
- }
214
- else if (action.type === 'waitFor') {
215
- await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 10000 });
216
- results.push(`Element ${action.selector} appeared`);
217
- }
218
- else if (action.type === 'scrape') {
219
- // Phase 1 AGI Upgrade: AXTree Mapper injection
220
- const mappedMarkdown = await activePage.evaluate(() => {
221
- let idx = 1;
222
- let lines = [];
223
- const elements = document.querySelectorAll('a, button, input, textarea, [role="button"], select');
224
-
225
- elements.forEach(el => {
226
- // Ignore hidden elements
227
- const style = window.getComputedStyle(el);
228
- if (style.display === 'none' || style.visibility === 'hidden' || el.offsetWidth === 0) return;
229
-
230
- let label = el.innerText || el.value || el.placeholder || el.getAttribute('aria-label') || el.name || 'unlabeled';
231
- label = label.trim().replace(/\n/g, ' ');
232
- if (!label) return; // Skip empty elements
233
-
234
- const tagName = el.tagName.toLowerCase();
235
- lines.push(`[${idx}] ${tagName}: ${label}`);
236
-
237
- el.setAttribute('data-causyne-id', idx.toString());
238
- idx++;
239
- });
240
- return lines.join('\n');
241
- });
242
-
243
- results.push(`Scraped DOM Map:\n${mappedMarkdown}`);
53
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
244
54
  }
245
- }
246
-
247
- return { success: true, log: results };
248
- } catch (e) {
249
- return { success: false, error: e.message };
250
55
  }
251
- }
252
56
  };
@@ -0,0 +1,57 @@
1
+ import { BrowserCapabilityError, causyneBrowser, type BrowserAction } from '../browser/causyneBrowser.js';
2
+
3
+ export const browserAct = {
4
+ schema: {
5
+ name: 'browser.act',
6
+ description: 'Use the dedicated Causyne Browser for resilient search, navigation, accessible snapshots, interaction, extraction, and workspace-scoped downloads. Take a snapshot before interacting by ref.',
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ actions: {
11
+ type: 'array',
12
+ minItems: 1,
13
+ maxItems: 40,
14
+ items: {
15
+ type: 'object',
16
+ properties: {
17
+ type: {
18
+ type: 'string',
19
+ enum: ['newTab', 'switchTab', 'listTabs', 'closeTab', 'search', 'goto', 'snapshot', 'scrape', 'click', 'clickIndex', 'type', 'typeIndex', 'selectOption', 'keyPress', 'scroll', 'back', 'forward', 'reload', 'extract', 'download', 'screenshot']
20
+ },
21
+ tabId: { type: 'string', description: 'Tab id returned by newTab or listTabs.' },
22
+ url: { type: 'string', description: 'Public http(s) URL for goto or newTab.' },
23
+ query: { type: 'string', description: 'Search query.' },
24
+ ref: { type: 'string', description: 'Current accessibility ref, for example e12.' },
25
+ index: { type: 'number', description: 'Legacy numeric form of an accessibility ref.' },
26
+ selector: { type: 'string', description: 'CSS selector when a current accessibility ref is unavailable.' },
27
+ text: { type: 'string' },
28
+ value: { type: 'string' },
29
+ key: { type: 'string' },
30
+ direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
31
+ amount: { type: 'number' },
32
+ offset: { type: 'number', description: 'Character offset for a paged accessibility snapshot.' },
33
+ outputFile: { type: 'string', description: 'Download or screenshot destination inside the selected workspace.' },
34
+ fullPage: { type: 'boolean', description: 'Capture the complete page for a screenshot instead of the visible viewport.' }
35
+ },
36
+ required: ['type'],
37
+ additionalProperties: false
38
+ }
39
+ }
40
+ },
41
+ required: ['actions'],
42
+ additionalProperties: false
43
+ }
44
+ },
45
+
46
+ async execute(args: { actions?: BrowserAction[] }) {
47
+ try {
48
+ const results = await causyneBrowser().run(args.actions ?? []);
49
+ return { success: true, output: JSON.stringify(results, null, 2), data: results };
50
+ } catch (error) {
51
+ if (error instanceof BrowserCapabilityError) {
52
+ return { success: false, error: error.message, requiresConsent: error.requiresConsent };
53
+ }
54
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
55
+ }
56
+ }
57
+ };
@@ -55,7 +55,7 @@ export const manageLocalDependencies = {
55
55
  type: 'object',
56
56
  properties: {
57
57
  action: { type: 'string', enum: ['setup_venv', 'pipInst', 'check_and_setupEng', 'grant_capability', 'list_capabilities', 'grant_mcp_server', 'list_mcp_servers'] },
58
- group: { type: 'string', description: 'Capability group for grant_capability: vision (browser + screen capture), ocr (read text from images), system (process and load inspection), desktop (clipboard, open, recycle bin), archive (zip and unzip).' },
58
+ group: { type: 'string', description: 'Capability group for grant_capability: browser (dedicated Causyne Browser runtime), vision (screen capture), ocr (read text from images), system (process and load inspection), desktop (clipboard, open, recycle bin), archive (zip and unzip).' },
59
59
  engine: { type: 'string', enum: ['godot', 'blender'], description: 'Engine to locate when action is check_and_setupEng' },
60
60
  simulateMissing: { type: 'boolean', description: 'Simulate missing binary to verify sovereign fallback cascade' },
61
61
  packages: { type: 'array', items: { type: 'string' }, description: 'Exact PyPI pins only, for example pillow==11.3.0. URLs, paths, VCS references and pip options are rejected.' }