synabun 1.3.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/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as ni from '../services/neural-interface.js';
|
|
3
|
+
import { text } from './response.js';
|
|
4
|
+
|
|
5
|
+
// ── browser_evaluate ──
|
|
6
|
+
|
|
7
|
+
export const browserEvaluateSchema = {
|
|
8
|
+
script: z.string().optional().describe('JavaScript code to execute in the browser page context. The return value will be serialized to JSON.'),
|
|
9
|
+
expression: z.string().optional().describe('Alias for "script" — accepted for compatibility with Playwright conventions.'),
|
|
10
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const browserEvaluateDescription =
|
|
14
|
+
'Execute JavaScript in the browser page context and return the result. Useful for reading DOM state, extracting data, or performing actions not covered by other tools.';
|
|
15
|
+
|
|
16
|
+
export async function handleBrowserEvaluate(args: { script?: string; expression?: string; sessionId?: string }) {
|
|
17
|
+
const code = args.script || args.expression;
|
|
18
|
+
if (!code) return text('Either "script" or "expression" is required.');
|
|
19
|
+
|
|
20
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
21
|
+
if ('error' in resolved) return text(resolved.error);
|
|
22
|
+
|
|
23
|
+
const result = await ni.evaluate(resolved.sessionId, code);
|
|
24
|
+
if (result.error) return text(`Evaluate failed: ${result.error}`);
|
|
25
|
+
|
|
26
|
+
let msg: string;
|
|
27
|
+
try {
|
|
28
|
+
// Guard against undefined — JSON.stringify(undefined) returns undefined (not a string)
|
|
29
|
+
const val = result.result ?? null;
|
|
30
|
+
msg = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
|
|
31
|
+
} catch {
|
|
32
|
+
msg = String(result.result ?? 'undefined');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return text(msg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── browser_wait ──
|
|
39
|
+
|
|
40
|
+
export const browserWaitSchema = {
|
|
41
|
+
selector: z.string().optional().describe('CSS selector to wait for. If omitted and loadState also omitted, waits for timeout.'),
|
|
42
|
+
state: z.enum(['visible', 'hidden', 'attached', 'detached']).optional().describe('Element state to wait for (default: "visible"). Only used with selector.'),
|
|
43
|
+
loadState: z.enum(['load', 'domcontentloaded', 'networkidle']).optional().describe(
|
|
44
|
+
'Page load state to wait for. Use "networkidle" after posting a tweet to confirm XHR requests settled. Takes priority over selector when provided.'
|
|
45
|
+
),
|
|
46
|
+
timeout: z.coerce.number().optional().describe('Timeout in ms (default: 10000 for element waits, 15000 for loadState).'),
|
|
47
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const browserWaitDescription =
|
|
51
|
+
'Wait for an element to reach a certain state, a page load state, or a fixed timeout. Useful after navigation or clicks that trigger async content loading. Use loadState="networkidle" after posting on Twitter/X or after navigation to ensure all async requests have settled.';
|
|
52
|
+
|
|
53
|
+
export async function handleBrowserWait(args: {
|
|
54
|
+
selector?: string;
|
|
55
|
+
state?: string;
|
|
56
|
+
loadState?: string;
|
|
57
|
+
timeout?: number;
|
|
58
|
+
sessionId?: string;
|
|
59
|
+
}) {
|
|
60
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
61
|
+
if ('error' in resolved) return text(resolved.error);
|
|
62
|
+
|
|
63
|
+
const result = await ni.waitFor(resolved.sessionId, {
|
|
64
|
+
selector: args.selector,
|
|
65
|
+
state: args.state,
|
|
66
|
+
loadState: args.loadState,
|
|
67
|
+
timeout: args.timeout,
|
|
68
|
+
});
|
|
69
|
+
if (result.error) return text(`Wait failed: ${result.error}`);
|
|
70
|
+
|
|
71
|
+
if (args.loadState) {
|
|
72
|
+
return text(`Page reached "${args.loadState}" — now at ${result.url} "${result.title}"`);
|
|
73
|
+
}
|
|
74
|
+
if (args.selector) {
|
|
75
|
+
return text(`"${args.selector}" is now ${result.state}`);
|
|
76
|
+
}
|
|
77
|
+
return text(`Waited ${args.timeout || 1000}ms — now at ${result.url} "${result.title}"`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── browser_session ──
|
|
81
|
+
|
|
82
|
+
export const browserSessionSchema = {
|
|
83
|
+
action: z.enum(['list', 'create', 'close']).describe('"list" = list open sessions, "create" = open a new browser, "close" = close a session.'),
|
|
84
|
+
url: z.string().optional().describe('Starting URL for "create" action (default: about:blank).'),
|
|
85
|
+
sessionId: z.string().optional().describe('Session ID for "close" action. Required for close when multiple sessions exist.'),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const browserSessionDescription =
|
|
89
|
+
'Manage browser sessions — list open sessions, create a new browser, or close an existing one.';
|
|
90
|
+
|
|
91
|
+
export async function handleBrowserSession(args: {
|
|
92
|
+
action: string;
|
|
93
|
+
url?: string;
|
|
94
|
+
sessionId?: string;
|
|
95
|
+
}) {
|
|
96
|
+
if (args.action === 'list') {
|
|
97
|
+
const result = await ni.listSessions();
|
|
98
|
+
if (result.error) return text(`List failed: ${result.error}`);
|
|
99
|
+
const sessions = (result.sessions || []) as Array<{ id: string; url: string; title: string; createdAt: number }>;
|
|
100
|
+
if (sessions.length === 0) {
|
|
101
|
+
return text('No browser sessions open.');
|
|
102
|
+
}
|
|
103
|
+
const lines = sessions.map(s => ` ${s.id} — ${s.title || s.url} (opened ${new Date(s.createdAt).toLocaleTimeString()})`);
|
|
104
|
+
return text(`${sessions.length} session(s):\n${lines.join('\n')}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (args.action === 'create') {
|
|
108
|
+
const result = await ni.createSession(args.url);
|
|
109
|
+
if (result.error) return text(`Create failed: ${result.error}`);
|
|
110
|
+
return text(`Created session ${result.sessionId} at ${args.url || 'about:blank'}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (args.action === 'close') {
|
|
114
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
115
|
+
if ('error' in resolved) return text(resolved.error);
|
|
116
|
+
|
|
117
|
+
const result = await ni.closeSession(resolved.sessionId);
|
|
118
|
+
if (result.error) return text(`Close failed: ${result.error}`);
|
|
119
|
+
return text(`Closed session ${resolved.sessionId}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return text(`Unknown action "${args.action}". Use "list", "create", or "close".`);
|
|
123
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as ni from '../services/neural-interface.js';
|
|
3
|
+
import { text } from './response.js';
|
|
4
|
+
|
|
5
|
+
function formatClickHints(result: Record<string, unknown>): string {
|
|
6
|
+
const hints = result.hints as Array<{ role: string; text: string; ariaLabel: string; placeholder: string }> | undefined;
|
|
7
|
+
if (!hints?.length) return '';
|
|
8
|
+
let msg = '\n\nVisible interactive elements on page:';
|
|
9
|
+
for (const h of hints) {
|
|
10
|
+
const label = h.ariaLabel || h.text || h.placeholder || '(unnamed)';
|
|
11
|
+
msg += `\n- ${h.role}: "${label.substring(0, 60)}"`;
|
|
12
|
+
}
|
|
13
|
+
msg += '\n\nUse browser_snapshot for full page structure.';
|
|
14
|
+
return msg;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── browser_click ──
|
|
18
|
+
|
|
19
|
+
export const browserClickSchema = {
|
|
20
|
+
selector: z.string().describe('Playwright selector for the element to click. Use CSS selectors (e.g. "button.submit", "#login"), text selectors (e.g. \'text="Sign in"\'), or Playwright extensions like \'button:has-text("Retry")\'. NEVER use :contains() — use :has-text() instead.'),
|
|
21
|
+
nthMatch: z.coerce.number().int().min(0).optional().describe('If the selector matches multiple elements, click the Nth one (0-indexed). Facebook: composer trigger appears in sidebar + main — use nthMatch: 0 to click the first match.'),
|
|
22
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const browserClickDescription =
|
|
26
|
+
'Click an element on the page. Accepts Playwright selectors: CSS selectors, text="...", :has-text("..."), role=button[name="..."], or [data-testid="..."]. Use browser_snapshot first to identify interactive elements. ' +
|
|
27
|
+
'Twitter/X stable selectors: [data-testid="tweetButton"] (post), [data-testid="like"], [data-testid="retweet"], [data-testid="reply"], [data-testid="SideNav_NewTweet_Button"] (compose FAB). ' +
|
|
28
|
+
'Facebook: [role="button"][aria-label="Curtir"] (PT like), [role="button"][aria-label="Like"] (EN), [role="button"][aria-label="Beğen"] (TR), [role="button"][aria-label*="Compartilhar"] (PT share), [role="button"][aria-label*="Share"] (EN share). ' +
|
|
29
|
+
'Facebook composer trigger: scope to [role="main"] div[role="button"]:has-text("Escreva algo") (PT) / :has-text("Write something") (EN) / :has-text("Bir şeyler yaz") (TR), or pass nthMatch: 0 on the unscoped selector. ' +
|
|
30
|
+
'Facebook post submit: [role="dialog"] [role="button"]:has-text("Publicar") (PT) / :has-text("Post") (EN) / :has-text("Paylaş") (TR). ' +
|
|
31
|
+
'TikTok main app — Like: button[aria-label*="Curtir vídeo"] (PT) / button[aria-label*="Like video"] (EN). ' +
|
|
32
|
+
'Comments: button[aria-label*="comentário"] (PT) / button[aria-label*="Read or add comments"] (EN) / button[aria-label*="comment"] (EN fallback). NOTE: On For You feed multiple articles are loaded — use nthMatch: 0 to target the first visible video. Prefer navigating directly to tiktok.com/@user/video/ID to avoid CAPTCHA. ' +
|
|
33
|
+
'Share: button[aria-label*="Compartilhar vídeo"] (PT) / button[aria-label*="Share video"] (EN). ' +
|
|
34
|
+
'Upload nav: [data-e2e="nav-upload"]. ' +
|
|
35
|
+
'Follow from For You feed: [data-e2e="feed-follow"] (the + icon on avatar — no nthMatch needed). ' +
|
|
36
|
+
'Follow from profile page: button:has-text("Seguir") nthMatch:0 (matches many sidebar items — always use nthMatch:0). ' +
|
|
37
|
+
'NOTE: button[data-e2e="follow-button"] does NOT work on video pages — use feed-follow or navigate to profile. ' +
|
|
38
|
+
'Like a comment: use browser_evaluate to call row.querySelector("[class*=DivLikeContainer]").click() — standard browser_click on [data-e2e="like-icon"] targets video likes and may navigate away. ' +
|
|
39
|
+
'TikTok Studio — edit post: [data-tt="components_ActionCell_Clickable"] (first in row = edit pencil). ' +
|
|
40
|
+
'Privacy dropdown: [data-tt="components_PrivacyCell_TUXButton"]. ' +
|
|
41
|
+
'Studio upload: button:has-text("Selecionar vídeo") then use browser_upload for file. ' +
|
|
42
|
+
'TikTok open comment panel: click [data-e2e="comment-input"] (outer container — activates the panel). Comment submit: [data-e2e="comment-post"]. Notifications: [data-e2e="nav-activity"]. Profile nav: [data-e2e="nav-profile"]. ' +
|
|
43
|
+
'WhatsApp Web — open chat: click span[title="Chat Name"] in the sidebar. Send message: button[aria-label="Enviar"] (appears after typing). ' +
|
|
44
|
+
'Attach menu: button[aria-label="Anexar"] → then [role="menuitem"][aria-label="Documento"] / [aria-label="Fotos e vídeos"] / [aria-label="Câmera"] / [aria-label="Contato"] / [aria-label="Enquete"]. ' +
|
|
45
|
+
'Emoji: button[aria-label="Emojis, GIFs, figurinhas"]. Voice: button[aria-label="Mensagem de voz"]. Search in chat: button[aria-label="Pesquisar"]. ' +
|
|
46
|
+
'Nav: button[aria-label="Conversas"] / button[aria-label="Atualizações no status"] / button[aria-label="Canais"] / button[aria-label="Configurações"]. ' +
|
|
47
|
+
'New chat: button[aria-label="Nova conversa"]. Header more options: button[aria-label="Mais opções"][data-tab="6"]. ' +
|
|
48
|
+
'Status — add text: button[aria-label="Add Status"] → [role="menuitem"][aria-label="Texto"]. Status send: [aria-label="Enviar"]. Close status: button[aria-label="Fechar janela de post de status"]. ' +
|
|
49
|
+
'Instagram — Like: [role="button"]:has(svg[aria-label="Curtir"]) (PT) / [role="button"]:has(svg[aria-label="Like"]) (EN). ' +
|
|
50
|
+
'Comment button: [role="button"]:has(svg[aria-label="Comentar"]) (PT) / [role="button"]:has(svg[aria-label="Comment"]) (EN). ' +
|
|
51
|
+
'Share: [role="button"]:has(svg[aria-label="Compartilhar"]) (PT) / [role="button"]:has(svg[aria-label="Share"]) (EN). ' +
|
|
52
|
+
'Save: [role="button"]:has(svg[aria-label="Salvar"]) (PT) / [role="button"]:has(svg[aria-label="Save"]) (EN). ' +
|
|
53
|
+
'Follow: div:has-text("Seguir") (PT) / div:has-text("Follow") (EN) — use nthMatch:0 as multiple matches exist. ' +
|
|
54
|
+
'More options: [role="button"]:has(svg[aria-label="Mais opções"]) (PT) / [role="button"]:has(svg[aria-label="More options"]) (EN). ' +
|
|
55
|
+
'Sidebar nav: svg[aria-label="Pesquisa"] (search), svg[aria-label="Novo post"] (new post), svg[aria-label="Notificações"] (notifications), svg[aria-label="Mensagens"] (DM). ' +
|
|
56
|
+
'On single post pages: comment input is a textarea inside form, not contenteditable — submit comment by pressing Enter after typing. ' +
|
|
57
|
+
'Reels: scroll down to advance to next reel. Engagement buttons on the right sidebar. ' +
|
|
58
|
+
'Emoji picker: [role="button"]:has(svg[aria-label="Emoji"]). ' +
|
|
59
|
+
'LinkedIn — Like: button[aria-label="Reagir com gostei"] (PT) / button[aria-label="React with Like"] (EN). ' +
|
|
60
|
+
'Reactions menu: button[aria-label="Abrir menu de reações"] (PT) / button[aria-label="Open reactions menu"] (EN). ' +
|
|
61
|
+
'Comment: button[aria-label="Comentar"] (PT) / button[aria-label="Comment"] (EN). ' +
|
|
62
|
+
'Share dropdown: the Compartilhar/Share button is an artdeco-dropdown trigger. ' +
|
|
63
|
+
'Send via DM: button[aria-label="Enviar em mensagem privada"] (PT) / button[aria-label="Send in private message"] (EN). ' +
|
|
64
|
+
'Post composer trigger: button:has-text("Comece uma publicação") (PT) / button:has-text("Start a post") (EN). ' +
|
|
65
|
+
'Post composer editor: [role="dialog"] .ql-editor[role="textbox"] — use browser_fill to enter text. ' +
|
|
66
|
+
'Post submit: [role="dialog"] .share-actions__primary-action (Publicar/Post button — disabled until text entered). ' +
|
|
67
|
+
'Schedule post: [role="dialog"] button[aria-label="Agendar publicação"] (PT) / button[aria-label="Schedule post"] (EN). ' +
|
|
68
|
+
'Add media: [role="dialog"] button[aria-label="Adicionar mídia"] (PT) / button[aria-label="Add media"] (EN). ' +
|
|
69
|
+
'Close dialog: button[aria-label="Fechar"] nthMatch:0. ' +
|
|
70
|
+
'Comment input on post page: .ql-editor[role="textbox"] — use browser_fill then browser_press Enter to submit. ' +
|
|
71
|
+
'Follow: button:has-text("+ Seguir") (PT) / button:has-text("+ Follow") (EN) — use nthMatch:0. ' +
|
|
72
|
+
'Connect: button:has-text("Conectar") (PT) / button:has-text("Connect") (EN). ' +
|
|
73
|
+
'Messaging compose: .msg-form__contenteditable — use browser_fill to type, then click .msg-form__send-button to send. ' +
|
|
74
|
+
'Nav: a[href*="/feed/"] (Home), a[href*="/mynetwork/"] (Network), a[href*="/jobs/"] (Jobs), a[href*="/messaging/"] (Messages), a[href*="/notifications/"] (Notifications).'
|
|
75
|
+
|
|
76
|
+
export async function handleBrowserClick(args: { selector: string; nthMatch?: number; sessionId?: string }) {
|
|
77
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
78
|
+
if ('error' in resolved) return text(resolved.error);
|
|
79
|
+
|
|
80
|
+
const result = await ni.click(resolved.sessionId, args.selector, args.nthMatch);
|
|
81
|
+
if (result.error) return text(`Click failed: ${result.error}${formatClickHints(result)}`);
|
|
82
|
+
|
|
83
|
+
return text(`Clicked "${args.selector}" — now at ${result.url} "${result.title}"`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── browser_fill ──
|
|
87
|
+
|
|
88
|
+
export const browserFillSchema = {
|
|
89
|
+
selector: z.string().describe('Playwright selector for the input/textarea to fill (e.g. "input[name=\'email\']", "#search-box", \'input:has-text("Search")\').'),
|
|
90
|
+
value: z.string().describe('The text value to fill into the element. Clears existing content first.'),
|
|
91
|
+
nthMatch: z.coerce.number().int().min(0).optional().describe('If the selector matches multiple elements, fill the Nth one (0-indexed).'),
|
|
92
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const browserFillDescription =
|
|
96
|
+
'Clear an input field and fill it with new text. Accepts Playwright selectors: CSS, text="...", :has-text("..."), [data-testid="..."]. Twitter/X: compose box is [data-testid="tweetTextarea_0"], search is [data-testid="SearchBox_Search_Input"]. TikTok search: [data-e2e="search-user-input"]. WhatsApp search: input[aria-label="Pesquisar ou começar uma nova conversa"]. ' +
|
|
97
|
+
'Instagram: comment box is textarea[aria-label="Adicione um comentário..."] (PT) / textarea[aria-label="Add a comment…"] (EN). Use browser_fill for comment text, then press Enter to submit.';
|
|
98
|
+
|
|
99
|
+
export async function handleBrowserFill(args: { selector: string; value: string; nthMatch?: number; sessionId?: string }) {
|
|
100
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
101
|
+
if ('error' in resolved) return text(resolved.error);
|
|
102
|
+
|
|
103
|
+
const result = await ni.fill(resolved.sessionId, args.selector, args.value, args.nthMatch);
|
|
104
|
+
if (result.error) return text(`Fill failed: ${result.error}${formatClickHints(result)}`);
|
|
105
|
+
|
|
106
|
+
const location = result.url ? ` — ${result.url} "${result.title}"` : '';
|
|
107
|
+
return text(`Filled "${args.selector}" with "${args.value.slice(0, 100)}"${location}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── browser_type ──
|
|
111
|
+
|
|
112
|
+
export const browserTypeSchema = {
|
|
113
|
+
selector: z.string().optional().describe('Playwright selector for the element to type into. If omitted, types into the currently focused element using keyboard events.'),
|
|
114
|
+
text: z.string().describe('The text to type character by character (appends to existing content).'),
|
|
115
|
+
nthMatch: z.coerce.number().int().min(0).optional().describe('If the selector matches multiple elements, type into the Nth one (0-indexed). Twitter/X reply dialog has 2 tweetTextarea_0 elements — use nthMatch to disambiguate.'),
|
|
116
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const browserTypeDescription =
|
|
120
|
+
'Type text character by character (simulates real keystrokes, appends to existing content). Provide a selector to target an element, or omit to type into whatever is currently focused. ' +
|
|
121
|
+
'Prefer browser_type over browser_fill for contenteditable editors — both Twitter/X ([data-testid="tweetTextarea_0"]) and Facebook use them. ' +
|
|
122
|
+
'Facebook post composer (after opening dialog): [role="dialog"] [role="textbox"]. ' +
|
|
123
|
+
'Facebook comment boxes: [contenteditable][aria-label*="Comente como"] (PT), [contenteditable][aria-label*="Comment as"] (EN), [contenteditable][aria-label*="Yorum yap"] (TR). ' +
|
|
124
|
+
'TikTok comment flow: 1) Navigate directly to tiktok.com/@user/video/ID (avoids CAPTCHA from feed). 2) Click [data-e2e="comment-input"] to open/activate the comment panel. 3) Type into [data-e2e="comment-text"] div[contenteditable="true"] (DraftJS editor — NOT [data-e2e="comment-input"] which is a non-editable wrapper; use browser_evaluate with execCommand for longer text). 4) Submit with [data-e2e="comment-post"]. ' +
|
|
125
|
+
'From For You feed: comment button needs nthMatch: 0 (multiple articles loaded). Direct video URL is safer to avoid CAPTCHA. ' +
|
|
126
|
+
'WhatsApp message compose: div[role="textbox"][contenteditable="true"] (aria-label is "Digitar na conversa com [Name]" or "Digitar no grupo [Name]"). ' +
|
|
127
|
+
'WhatsApp status text composer: .lexical-rich-text-input div[contenteditable="true"]. ' +
|
|
128
|
+
'Instagram: Comment on post pages uses a native textarea (NOT contenteditable) — use browser_fill instead of browser_type. For DMs, navigate to instagram.com/direct/inbox/ first. Search: click svg[aria-label="Pesquisa"] in sidebar to open search panel, then type in the input that appears.';
|
|
129
|
+
|
|
130
|
+
export async function handleBrowserType(args: { selector?: string; text: string; nthMatch?: number; sessionId?: string }) {
|
|
131
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
132
|
+
if ('error' in resolved) return text(resolved.error);
|
|
133
|
+
|
|
134
|
+
const result = await ni.type(resolved.sessionId, args.selector ?? null, args.text, args.nthMatch);
|
|
135
|
+
if (result.error) return text(`Type failed: ${result.error}${formatClickHints(result)}`);
|
|
136
|
+
|
|
137
|
+
const target = args.selector ? `"${args.selector}"` : 'focused element';
|
|
138
|
+
const location = result.url ? ` — ${result.url} "${result.title}"` : '';
|
|
139
|
+
return text(`Typed "${args.text.slice(0, 100)}" into ${target}${location}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── browser_hover ──
|
|
143
|
+
|
|
144
|
+
export const browserHoverSchema = {
|
|
145
|
+
selector: z.string().describe('Playwright selector for the element to hover over (CSS, text="...", :has-text("...")).'),
|
|
146
|
+
nthMatch: z.coerce.number().int().min(0).optional().describe('If the selector matches multiple elements, hover over the Nth one (0-indexed).'),
|
|
147
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const browserHoverDescription =
|
|
151
|
+
'Hover over an element. Useful for revealing dropdowns, tooltips, or hover-triggered content. Accepts Playwright selectors.';
|
|
152
|
+
|
|
153
|
+
export async function handleBrowserHover(args: { selector: string; nthMatch?: number; sessionId?: string }) {
|
|
154
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
155
|
+
if ('error' in resolved) return text(resolved.error);
|
|
156
|
+
|
|
157
|
+
const result = await ni.hover(resolved.sessionId, args.selector, args.nthMatch);
|
|
158
|
+
if (result.error) return text(`Hover failed: ${result.error}${formatClickHints(result)}`);
|
|
159
|
+
|
|
160
|
+
const location = result.url ? ` — ${result.url} "${result.title}"` : '';
|
|
161
|
+
return text(`Hovered over "${args.selector}"${location}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── browser_select ──
|
|
165
|
+
|
|
166
|
+
export const browserSelectSchema = {
|
|
167
|
+
selector: z.string().describe('Playwright selector for the <select> element.'),
|
|
168
|
+
value: z.string().describe('The option value to select.'),
|
|
169
|
+
nthMatch: z.coerce.number().int().min(0).optional().describe('If the selector matches multiple elements, select from the Nth one (0-indexed).'),
|
|
170
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const browserSelectDescription =
|
|
174
|
+
'Select an option from a <select> dropdown by CSS selector and option value.';
|
|
175
|
+
|
|
176
|
+
export async function handleBrowserSelect(args: { selector: string; value: string; nthMatch?: number; sessionId?: string }) {
|
|
177
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
178
|
+
if ('error' in resolved) return text(resolved.error);
|
|
179
|
+
|
|
180
|
+
const result = await ni.selectOption(resolved.sessionId, args.selector, args.value, args.nthMatch);
|
|
181
|
+
if (result.error) return text(`Select failed: ${result.error}${formatClickHints(result)}`);
|
|
182
|
+
|
|
183
|
+
const location = result.url ? ` — ${result.url} "${result.title}"` : '';
|
|
184
|
+
return text(`Selected "${args.value}" in "${args.selector}"${location}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── browser_press ──
|
|
188
|
+
|
|
189
|
+
export const browserPressSchema = {
|
|
190
|
+
key: z.string().describe('Key or key combo to press (e.g. "Enter", "Tab", "Control+A", "Escape", "ArrowDown").'),
|
|
191
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const browserPressDescription =
|
|
195
|
+
'Press a keyboard key or key combination. Supports modifiers like Control+A, Shift+Enter, etc.';
|
|
196
|
+
|
|
197
|
+
export async function handleBrowserPress(args: { key: string; sessionId?: string }) {
|
|
198
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
199
|
+
if ('error' in resolved) return text(resolved.error);
|
|
200
|
+
|
|
201
|
+
const result = await ni.pressKey(resolved.sessionId, args.key);
|
|
202
|
+
if (result.error) return text(`Press failed: ${result.error}`);
|
|
203
|
+
|
|
204
|
+
const location = result.url ? ` — ${result.url} "${result.title}"` : '';
|
|
205
|
+
return text(`Pressed "${args.key}"${location}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── browser_scroll ──
|
|
209
|
+
|
|
210
|
+
export const browserScrollSchema = {
|
|
211
|
+
direction: z.enum(['up', 'down', 'left', 'right']).describe('Direction to scroll.'),
|
|
212
|
+
distance: z.coerce.number().optional().describe('Pixels to scroll (default: 500).'),
|
|
213
|
+
selector: z.string().optional().describe('Scroll within a specific element instead of the page. Twitter: [data-testid="primaryColumn"] for main feed. Facebook: [role="feed"] for group/page post feed. TikTok: [data-e2e="comment-list"] for comment panel, [data-tt="components_PostTable_Container"] for Studio content table. WhatsApp: [aria-label="Lista de conversas"] for chat list sidebar, [role="application"] or the main chat panel for message history.'),
|
|
214
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const browserScrollDescription =
|
|
218
|
+
'Scroll the page or a specific element. Essential for infinite scroll feeds. Twitter/X: direction "down" with distance 800–1500 to load more tweets. Facebook: use selector "[role=\\"feed\\"]" to scroll within the post feed. TikTok feed: scroll "down" 800–1000px to advance to the next video. TikTok comment panel: use selector "[data-e2e=\\"comment-list\\"]". TikTok Studio table: use selector "[data-tt=\\"components_PostTable_Container\\"]". WhatsApp chat list: use selector "[aria-label=\\"Lista de conversas\\"]". WhatsApp message history: scroll "up" to load older messages. ' +
|
|
219
|
+
'Instagram: Home feed — scroll down 800–1200px to load more posts. Profile grid — scroll down to load more posts. Reels — scroll down 800px to advance to next reel. Post page comments — scroll within the comment panel to load older comments. Explore page — scroll down to load more grid posts. ' +
|
|
220
|
+
'LinkedIn: Feed — scroll down 800–1200px to load more posts. Profile — scroll down to load more sections. Notifications — scroll down for older notifications. Messaging conversations sidebar — scroll within .msg-conversations-container. Search results — scroll down for more results. My Network suggestions — scroll down for more people to follow/connect.';
|
|
221
|
+
|
|
222
|
+
export async function handleBrowserScroll(args: { direction: string; distance?: number; selector?: string; sessionId?: string }) {
|
|
223
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
224
|
+
if ('error' in resolved) return text(resolved.error);
|
|
225
|
+
|
|
226
|
+
const result = await ni.scroll(resolved.sessionId, { direction: args.direction, distance: args.distance, selector: args.selector });
|
|
227
|
+
if (result.error) return text(`Scroll failed: ${result.error}`);
|
|
228
|
+
|
|
229
|
+
const target = args.selector ? `"${args.selector}"` : 'page';
|
|
230
|
+
return text(`Scrolled ${args.direction} ${args.distance ?? 500}px in ${target} — now at ${result.url} "${result.title}"`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── browser_upload ──
|
|
234
|
+
|
|
235
|
+
export const browserUploadSchema = {
|
|
236
|
+
selector: z.string().describe('Playwright selector for the file input element. Twitter: \'input[data-testid="fileInput"]\'.'),
|
|
237
|
+
filePaths: z.array(z.string()).describe('Absolute file paths to upload. Twitter supports up to 4 images or 1 video per tweet.'),
|
|
238
|
+
nthMatch: z.coerce.number().int().min(0).optional().describe('If the selector matches multiple file inputs, upload to the Nth one (0-indexed).'),
|
|
239
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const browserUploadDescription =
|
|
243
|
+
'Upload one or more files via a file input element. For Twitter/X: click the media button first to reveal the hidden file input, then use selector \'input[data-testid="fileInput"]\'. ' +
|
|
244
|
+
'TikTok Studio: click button:has-text("Selecionar vídeo") first, then use selector \'input[type="file"]\' — supports a single video file. ' +
|
|
245
|
+
'Instagram: Click svg[aria-label="Novo post"] in sidebar to open the create dialog, then use browser_upload on the file input that appears. Supports images and videos.';
|
|
246
|
+
|
|
247
|
+
export async function handleBrowserUpload(args: { selector: string; filePaths: string[]; nthMatch?: number; sessionId?: string }) {
|
|
248
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
249
|
+
if ('error' in resolved) return text(resolved.error);
|
|
250
|
+
|
|
251
|
+
const result = await ni.upload(resolved.sessionId, args.selector, args.filePaths, args.nthMatch);
|
|
252
|
+
if (result.error) return text(`Upload failed: ${result.error}${formatClickHints(result)}`);
|
|
253
|
+
|
|
254
|
+
return text(`Uploaded ${args.filePaths.length} file(s) via "${args.selector}" — now at ${result.url} "${result.title}"`);
|
|
255
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as ni from '../services/neural-interface.js';
|
|
3
|
+
import { text } from './response.js';
|
|
4
|
+
|
|
5
|
+
// ── browser_navigate ──
|
|
6
|
+
|
|
7
|
+
export const browserNavigateSchema = {
|
|
8
|
+
url: z.string().describe('The URL to navigate to.'),
|
|
9
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session or creates a new one.'),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const browserNavigateDescription =
|
|
13
|
+
'Navigate the browser to a URL. If no browser session exists, one is created automatically.';
|
|
14
|
+
|
|
15
|
+
export async function handleBrowserNavigate(args: { url: string; sessionId?: string }) {
|
|
16
|
+
const resolved = await ni.resolveSession(args.sessionId, { url: args.url });
|
|
17
|
+
if ('error' in resolved) return text(resolved.error);
|
|
18
|
+
|
|
19
|
+
const result = await ni.navigate(resolved.sessionId, args.url);
|
|
20
|
+
if (result.error) return text(`Navigation failed: ${result.error}`);
|
|
21
|
+
|
|
22
|
+
return text(`Navigated to ${result.url} — "${result.title}"`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── browser_go_back ──
|
|
26
|
+
|
|
27
|
+
export const browserGoBackSchema = {
|
|
28
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const browserGoBackDescription = 'Go back to the previous page in browser history.';
|
|
32
|
+
|
|
33
|
+
export async function handleBrowserGoBack(args: { sessionId?: string }) {
|
|
34
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
35
|
+
if ('error' in resolved) return text(resolved.error);
|
|
36
|
+
|
|
37
|
+
const result = await ni.goBack(resolved.sessionId);
|
|
38
|
+
if (result.error) return text(`Go back failed: ${result.error}`);
|
|
39
|
+
|
|
40
|
+
return text(`Went back to ${result.url} — "${result.title}"`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── browser_go_forward ──
|
|
44
|
+
|
|
45
|
+
export const browserGoForwardSchema = {
|
|
46
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const browserGoForwardDescription = 'Go forward to the next page in browser history.';
|
|
50
|
+
|
|
51
|
+
export async function handleBrowserGoForward(args: { sessionId?: string }) {
|
|
52
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
53
|
+
if ('error' in resolved) return text(resolved.error);
|
|
54
|
+
|
|
55
|
+
const result = await ni.goForward(resolved.sessionId);
|
|
56
|
+
if (result.error) return text(`Go forward failed: ${result.error}`);
|
|
57
|
+
|
|
58
|
+
return text(`Went forward to ${result.url} — "${result.title}"`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── browser_reload ──
|
|
62
|
+
|
|
63
|
+
export const browserReloadSchema = {
|
|
64
|
+
sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const browserReloadDescription = 'Reload the current page. Useful after making changes or when content is stale.';
|
|
68
|
+
|
|
69
|
+
export async function handleBrowserReload(args: { sessionId?: string }) {
|
|
70
|
+
const resolved = await ni.resolveSession(args.sessionId);
|
|
71
|
+
if ('error' in resolved) return text(resolved.error);
|
|
72
|
+
|
|
73
|
+
const result = await ni.reload(resolved.sessionId);
|
|
74
|
+
if (result.error) return text(`Reload failed: ${result.error}`);
|
|
75
|
+
|
|
76
|
+
return text(`Reloaded — now at ${result.url} "${result.title}"`);
|
|
77
|
+
}
|