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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leonardo.ai Browser MCP tools.
|
|
3
|
+
* 100% browser-based — all interaction happens via SynaBun's browser automation.
|
|
4
|
+
* No API key required.
|
|
5
|
+
*
|
|
6
|
+
* Tools: leonardo_browser_navigate, leonardo_browser_generate, leonardo_browser_library, leonardo_browser_download, leonardo_browser_reference
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import * as ni from '../services/neural-interface.js';
|
|
11
|
+
import { text } from './response.js';
|
|
12
|
+
|
|
13
|
+
const LEO_BASE = 'https://app.leonardo.ai';
|
|
14
|
+
|
|
15
|
+
// ── Helper: resolve or create session ─────────────────────────
|
|
16
|
+
|
|
17
|
+
async function getSession(): Promise<string | null> {
|
|
18
|
+
const resolved = await ni.resolveSession(undefined, { url: LEO_BASE });
|
|
19
|
+
if ('error' in resolved) return null;
|
|
20
|
+
return resolved.sessionId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function wait(ms: number): Promise<void> {
|
|
24
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Browser Navigate ──────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export const browserNavigateSchema = {
|
|
30
|
+
page: z.enum([
|
|
31
|
+
'home', 'library', 'image', 'video', 'upscaler',
|
|
32
|
+
'blueprints', 'flow-state', 'models',
|
|
33
|
+
] as const).describe('Leonardo.ai page to navigate to.'),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const browserNavigateDescription =
|
|
37
|
+
'Navigate the browser to a specific Leonardo.ai page. Always use this as the first step before any browser-based generation.';
|
|
38
|
+
|
|
39
|
+
export async function handleBrowserNavigate(args: { page: string }) {
|
|
40
|
+
const sessionId = await getSession();
|
|
41
|
+
if (!sessionId) return text('No browser session available. Open Leonardo.ai in the browser first using browser_navigate or the Apps menu.');
|
|
42
|
+
|
|
43
|
+
const paths: Record<string, string> = {
|
|
44
|
+
'home': '/',
|
|
45
|
+
'library': '/library',
|
|
46
|
+
'image': '/image-generation',
|
|
47
|
+
'video': '/image-generation/video',
|
|
48
|
+
'upscaler': '/universal-upscaler',
|
|
49
|
+
'blueprints': '/blueprints',
|
|
50
|
+
'flow-state': '/flow-state',
|
|
51
|
+
'models': '/models-training',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const path = paths[args.page] || '/';
|
|
55
|
+
const res = await ni.navigate(sessionId, `${LEO_BASE}${path}`);
|
|
56
|
+
if (res.error) return text(`Navigation failed: ${res.error}`);
|
|
57
|
+
|
|
58
|
+
// Wait for page to settle
|
|
59
|
+
await wait(3000);
|
|
60
|
+
|
|
61
|
+
return text(`Navigated to Leonardo.ai ${args.page} page (${LEO_BASE}${path}). Use browser_snapshot to see the current UI state.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Browser Generate (full UI automation) ─────────────────────
|
|
65
|
+
|
|
66
|
+
export const browserGenerateSchema = {
|
|
67
|
+
prompt: z.string().describe('The generation prompt to type into Leonardo.ai.'),
|
|
68
|
+
type: z.enum(['image', 'video'] as const).optional().describe('Generation type. Default: image.'),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const browserGenerateDescription =
|
|
72
|
+
'Fill the prompt field and click Generate on the current Leonardo.ai page. Set all other settings (model, style, dimensions, motion controls) BEFORE calling this — use browser_click, browser_fill, and browser_snapshot to configure the UI first. This tool only handles: navigate to correct page if needed → clear & fill prompt → click Generate.';
|
|
73
|
+
|
|
74
|
+
export async function handleBrowserGenerate(args: {
|
|
75
|
+
prompt: string;
|
|
76
|
+
type?: string;
|
|
77
|
+
}) {
|
|
78
|
+
const sessionId = await getSession();
|
|
79
|
+
if (!sessionId) return text('No browser session available. Open Leonardo.ai first.');
|
|
80
|
+
|
|
81
|
+
const genType = args.type || 'image';
|
|
82
|
+
|
|
83
|
+
// Check current URL — navigate only if not already on the right page
|
|
84
|
+
const snapshot = await ni.snapshot(sessionId);
|
|
85
|
+
const currentUrl = (snapshot as any)?.url || '';
|
|
86
|
+
const targetPath = genType === 'video' ? '/image-generation/video' : '/image-generation';
|
|
87
|
+
|
|
88
|
+
if (!currentUrl.includes(targetPath)) {
|
|
89
|
+
const navRes = await ni.navigate(sessionId, `${LEO_BASE}${targetPath}`);
|
|
90
|
+
if (navRes.error) return text(`Navigation failed: ${navRes.error}`);
|
|
91
|
+
await wait(3000);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clear and fill the prompt textbox
|
|
95
|
+
const fillRes = await ni.fill(sessionId, '[aria-label="Prompt"], textbox[name="Prompt"], textarea', args.prompt);
|
|
96
|
+
if (fillRes.error) return text(`Failed to fill prompt: ${fillRes.error}`);
|
|
97
|
+
|
|
98
|
+
await wait(500);
|
|
99
|
+
|
|
100
|
+
// Click the Generate button
|
|
101
|
+
const clickRes = await ni.click(sessionId, 'button[aria-label="Generate"], button:has-text("Generate")');
|
|
102
|
+
if (clickRes.error) return text(`Failed to click Generate: ${clickRes.error}`);
|
|
103
|
+
|
|
104
|
+
return text(`Generation started (${genType}).\n\nPrompt: "${args.prompt.substring(0, 120)}${args.prompt.length > 120 ? '...' : ''}"\n\nThe generation is processing in Leonardo.ai. Use browser_snapshot or browser_screenshot to check progress and see results. Results also appear in the library.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Browser Library ───────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export const browserLibrarySchema = {
|
|
110
|
+
action: z.enum(['view', 'search'] as const).describe('Action: view (open library) or search (search for a term).'),
|
|
111
|
+
query: z.string().optional().describe('Search query (for search action).'),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const browserLibraryDescription =
|
|
115
|
+
'Open or search Leonardo.ai\'s library to view past generations. Use browser_snapshot after to see results.';
|
|
116
|
+
|
|
117
|
+
export async function handleBrowserLibrary(args: { action: string; query?: string }) {
|
|
118
|
+
const sessionId = await getSession();
|
|
119
|
+
if (!sessionId) return text('No browser session available.');
|
|
120
|
+
|
|
121
|
+
const navRes = await ni.navigate(sessionId, `${LEO_BASE}/library`);
|
|
122
|
+
if (navRes.error) return text(`Navigation failed: ${navRes.error}`);
|
|
123
|
+
|
|
124
|
+
await wait(2000);
|
|
125
|
+
|
|
126
|
+
if (args.action === 'search' && args.query) {
|
|
127
|
+
const fillRes = await ni.fill(sessionId, 'input[placeholder*="Search" i]', args.query);
|
|
128
|
+
if (fillRes.error) return text(`Failed to search: ${fillRes.error}`);
|
|
129
|
+
await ni.pressKey(sessionId, 'Enter');
|
|
130
|
+
return text(`Searched Leonardo.ai library for: "${args.query}". Use browser_snapshot to see results.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return text('Leonardo.ai library opened. Use browser_snapshot to see recent generations.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Browser Download / Screenshot ─────────────────────────────
|
|
137
|
+
|
|
138
|
+
export const browserDownloadSchema = {
|
|
139
|
+
action: z.enum(['screenshot'] as const).describe('Action: screenshot (capture current Leonardo.ai page state).'),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const browserDownloadDescription =
|
|
143
|
+
'Capture a screenshot of the current Leonardo.ai page. Use to verify generation results or UI state.';
|
|
144
|
+
|
|
145
|
+
export async function handleBrowserDownload(args: { action: string }) {
|
|
146
|
+
const sessionId = await getSession();
|
|
147
|
+
if (!sessionId) return text('No browser session available.');
|
|
148
|
+
|
|
149
|
+
if (args.action === 'screenshot') {
|
|
150
|
+
const res = await ni.screenshot(sessionId);
|
|
151
|
+
if (res.error) return text(`Screenshot failed: ${res.error}`);
|
|
152
|
+
return text('Screenshot captured of the current Leonardo.ai page.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return text(`Unknown action: ${args.action}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Browser Reference (upload reference images) ──────────────
|
|
159
|
+
|
|
160
|
+
export const browserReferenceSchema = {
|
|
161
|
+
type: z.enum([
|
|
162
|
+
'image_ref', 'style_ref', 'content_ref', 'character_ref',
|
|
163
|
+
'image_to_image', 'start_frame', 'end_frame',
|
|
164
|
+
] as const).describe(
|
|
165
|
+
'Reference type. Image generation: image_ref, style_ref, content_ref, character_ref, image_to_image. ' +
|
|
166
|
+
'Video generation: start_frame, end_frame. Availability depends on the selected model — check the model advisor.',
|
|
167
|
+
),
|
|
168
|
+
filePaths: z.array(z.string()).describe(
|
|
169
|
+
'Absolute file paths of images to upload as reference. Get paths from image_staged tool or from user-attached images.',
|
|
170
|
+
),
|
|
171
|
+
autoClear: z.boolean().optional().describe(
|
|
172
|
+
'Auto-delete the source images from data/images/ after successful upload. Default: true.',
|
|
173
|
+
),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const browserReferenceDescription =
|
|
177
|
+
'Upload reference image(s) to Leonardo.ai for guided generation. Supports image references, style/content/character references, ' +
|
|
178
|
+
'image-to-image, and video start/end frames. Must be called AFTER selecting a model (reference type availability is model-dependent) ' +
|
|
179
|
+
'and BEFORE calling leonardo_browser_generate. Uses browser automation to click the reference panel, upload the file(s), and confirm. ' +
|
|
180
|
+
'By default, auto-clears uploaded images from the SynaBun image store after success.';
|
|
181
|
+
|
|
182
|
+
export async function handleBrowserReference(args: {
|
|
183
|
+
type: string;
|
|
184
|
+
filePaths: string[];
|
|
185
|
+
autoClear?: boolean;
|
|
186
|
+
}) {
|
|
187
|
+
const sessionId = await getSession();
|
|
188
|
+
if (!sessionId) return text('No browser session available. Open Leonardo.ai first.');
|
|
189
|
+
|
|
190
|
+
const { type, filePaths, autoClear = true } = args;
|
|
191
|
+
const isVideo = type === 'start_frame' || type === 'end_frame';
|
|
192
|
+
|
|
193
|
+
// Step 1: Open the reference panel
|
|
194
|
+
let openResult: { error?: string };
|
|
195
|
+
|
|
196
|
+
if (isVideo) {
|
|
197
|
+
// Video: click "Add Image Guidance to generation" button
|
|
198
|
+
openResult = await ni.evaluate(sessionId, `(() => {
|
|
199
|
+
const btns = document.querySelectorAll('button');
|
|
200
|
+
for (const b of btns) {
|
|
201
|
+
if (b.textContent.includes('Image Guidance') || b.textContent.includes('Add Image')) {
|
|
202
|
+
b.click(); return 'opened';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return 'not found';
|
|
206
|
+
})()`);
|
|
207
|
+
} else {
|
|
208
|
+
// Image: click "Add elements" button
|
|
209
|
+
openResult = await ni.evaluate(sessionId, `(() => {
|
|
210
|
+
const btns = document.querySelectorAll('button');
|
|
211
|
+
for (const b of btns) {
|
|
212
|
+
if (b.textContent.includes('Add elements') || b.textContent.includes('Add Elements')) {
|
|
213
|
+
b.click(); return 'opened';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return 'not found';
|
|
217
|
+
})()`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (openResult.error) return text(`Failed to open reference panel: ${openResult.error}`);
|
|
221
|
+
await wait(1500);
|
|
222
|
+
|
|
223
|
+
// Step 2: For image references, click the specific reference type tab/button
|
|
224
|
+
if (!isVideo) {
|
|
225
|
+
const typeLabels: Record<string, string[]> = {
|
|
226
|
+
'image_ref': ['Image Reference', 'Image Ref', 'Image Input'],
|
|
227
|
+
'style_ref': ['Style Reference', 'Style Ref'],
|
|
228
|
+
'content_ref': ['Content Reference', 'Content Ref'],
|
|
229
|
+
'character_ref': ['Character Reference', 'Character Ref'],
|
|
230
|
+
'image_to_image': ['Image to Image', 'Img2Img'],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const labels = typeLabels[type] || [type];
|
|
234
|
+
const labelSearch = labels.map(l => `b.textContent.includes('${l}')`).join(' || ');
|
|
235
|
+
|
|
236
|
+
const selectResult = await ni.evaluate(sessionId, `(() => {
|
|
237
|
+
const btns = document.querySelectorAll('button, [role="tab"], [role="menuitem"]');
|
|
238
|
+
for (const b of btns) {
|
|
239
|
+
if (${labelSearch}) { b.click(); return 'selected'; }
|
|
240
|
+
}
|
|
241
|
+
return 'not found — use browser_snapshot to see available options';
|
|
242
|
+
})()`);
|
|
243
|
+
|
|
244
|
+
if (selectResult.error) return text(`Failed to select reference type: ${selectResult.error}. Use browser_snapshot to see the panel.`);
|
|
245
|
+
await wait(1000);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Step 3: Find file input and upload
|
|
249
|
+
const uploadResult = await ni.upload(sessionId, 'input[type="file"]', filePaths);
|
|
250
|
+
if (uploadResult.error) {
|
|
251
|
+
// Fallback: try broader selector
|
|
252
|
+
const fallback = await ni.upload(sessionId, 'input[accept*="image"]', filePaths);
|
|
253
|
+
if (fallback.error) {
|
|
254
|
+
return text(
|
|
255
|
+
`Failed to upload reference image(s): ${uploadResult.error}. ` +
|
|
256
|
+
`The file input may not be visible yet — try browser_snapshot to inspect the panel, ` +
|
|
257
|
+
`then use browser_upload manually with the correct selector.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await wait(1000);
|
|
263
|
+
|
|
264
|
+
// Step 4: Auto-clear source images if requested
|
|
265
|
+
if (autoClear) {
|
|
266
|
+
const { basename } = await import('path');
|
|
267
|
+
for (const fp of filePaths) {
|
|
268
|
+
const filename = basename(fp);
|
|
269
|
+
try {
|
|
270
|
+
await ni.deleteImage(filename);
|
|
271
|
+
} catch { /* ignore cleanup errors */ }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const typeLabel = type.replace(/_/g, ' ');
|
|
276
|
+
return text(
|
|
277
|
+
`Reference uploaded (${typeLabel}): ${filePaths.length} image(s).\n` +
|
|
278
|
+
`${autoClear ? 'Source images auto-cleared from store.\n' : ''}` +
|
|
279
|
+
`Use browser_snapshot to verify the reference is applied, then proceed with leonardo_browser_generate.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leonardo.ai MCP tool registration barrel.
|
|
3
|
+
* 100% browser-based — no API key required.
|
|
4
|
+
* Registers 4 Leonardo browser tools on the given server instance.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
browserNavigateSchema, browserNavigateDescription, handleBrowserNavigate,
|
|
11
|
+
browserGenerateSchema, browserGenerateDescription, handleBrowserGenerate,
|
|
12
|
+
browserLibrarySchema, browserLibraryDescription, handleBrowserLibrary,
|
|
13
|
+
browserDownloadSchema, browserDownloadDescription, handleBrowserDownload,
|
|
14
|
+
browserReferenceSchema, browserReferenceDescription, handleBrowserReference,
|
|
15
|
+
} from './leonardo-browser-tools.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register all Leonardo.ai MCP tools on the given server instance.
|
|
19
|
+
* These are browser-based tools that automate the Leonardo.ai web UI.
|
|
20
|
+
* Use the /leonardo skill for the full guided creation experience.
|
|
21
|
+
*/
|
|
22
|
+
export function registerLeonardoTools(server: McpServer) {
|
|
23
|
+
server.tool('leonardo_browser_navigate', browserNavigateDescription, browserNavigateSchema, handleBrowserNavigate);
|
|
24
|
+
server.tool('leonardo_browser_generate', browserGenerateDescription, browserGenerateSchema, handleBrowserGenerate);
|
|
25
|
+
server.tool('leonardo_browser_library', browserLibraryDescription, browserLibrarySchema, handleBrowserLibrary);
|
|
26
|
+
server.tool('leonardo_browser_download', browserDownloadDescription, browserDownloadSchema, handleBrowserDownload);
|
|
27
|
+
server.tool('leonardo_browser_reference', browserReferenceDescription, browserReferenceSchema, handleBrowserReference);
|
|
28
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync, readdirSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { text } from './response.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const DATA_DIR = join(__dirname, '..', '..', 'data');
|
|
9
|
+
// Loop state files and templates live in the project root data dir (Synabun/data/)
|
|
10
|
+
const PROJECT_ROOT = process.env.SYNABUN_ROOT || join(__dirname, '..', '..', '..');
|
|
11
|
+
const NI_DATA_DIR = join(PROJECT_ROOT, 'data');
|
|
12
|
+
const LOOP_DIR = join(NI_DATA_DIR, 'loop');
|
|
13
|
+
const LOOP_TEMPLATES_PATH = join(NI_DATA_DIR, 'loop-templates.json');
|
|
14
|
+
|
|
15
|
+
const MAX_ITERATIONS = 50;
|
|
16
|
+
const DEFAULT_ITERATIONS = 10;
|
|
17
|
+
const MAX_MINUTES = 480;
|
|
18
|
+
const DEFAULT_MINUTES = 60;
|
|
19
|
+
|
|
20
|
+
function ensureLoopDir() {
|
|
21
|
+
if (!existsSync(LOOP_DIR)) {
|
|
22
|
+
mkdirSync(LOOP_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the session ID for loop state file.
|
|
28
|
+
* Priority: explicit param > CLAUDE_SESSION_ID env > SYNABUN_TERMINAL_SESSION env > scan.
|
|
29
|
+
*
|
|
30
|
+
* When SYNABUN_TERMINAL_SESSION is set (server-launched loop), find the loop file
|
|
31
|
+
* by matching the terminalSessionId field. This survives /clear (which changes
|
|
32
|
+
* CLAUDE_SESSION_ID) and prevents cross-loop contamination with multiple automations.
|
|
33
|
+
*/
|
|
34
|
+
function resolveSessionId(explicit?: string): string | null {
|
|
35
|
+
if (explicit) return explicit;
|
|
36
|
+
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
37
|
+
|
|
38
|
+
ensureLoopDir();
|
|
39
|
+
|
|
40
|
+
// Priority: find loop file owned by this terminal session (survives /clear)
|
|
41
|
+
const terminalSessionEnv = process.env.SYNABUN_TERMINAL_SESSION;
|
|
42
|
+
if (terminalSessionEnv) {
|
|
43
|
+
try {
|
|
44
|
+
const files = readdirSync(LOOP_DIR).filter(f => f.endsWith('.json') && !f.startsWith('pending-'));
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(readFileSync(join(LOOP_DIR, file), 'utf-8'));
|
|
48
|
+
if (data?.active && data.terminalSessionId === terminalSessionEnv) {
|
|
49
|
+
return file.replace('.json', '');
|
|
50
|
+
}
|
|
51
|
+
} catch { /* skip corrupt files */ }
|
|
52
|
+
}
|
|
53
|
+
} catch { /* dir read failed */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback: scan for any active loop file (legacy — no terminal scoping)
|
|
57
|
+
try {
|
|
58
|
+
const files = readdirSync(LOOP_DIR).filter(f => f.endsWith('.json'));
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
try {
|
|
61
|
+
const data = JSON.parse(readFileSync(join(LOOP_DIR, file), 'utf-8'));
|
|
62
|
+
if (data?.active) return file.replace('.json', '');
|
|
63
|
+
} catch { /* skip corrupt files */ }
|
|
64
|
+
}
|
|
65
|
+
} catch { /* dir read failed */ }
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getLoopPath(sessionId: string): string {
|
|
71
|
+
return join(LOOP_DIR, `${sessionId}.json`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const loopSchema = {
|
|
75
|
+
action: z
|
|
76
|
+
.enum(['start', 'stop', 'status', 'update'] as const)
|
|
77
|
+
.describe('start: begin autonomous loop. stop: end loop. status: check current loop state. update: write iteration journal/progress.'),
|
|
78
|
+
task: z
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('What to do each iteration (required for start).'),
|
|
82
|
+
iterations: z
|
|
83
|
+
.number()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe(`Max iterations (1-${MAX_ITERATIONS}, default ${DEFAULT_ITERATIONS}).`),
|
|
86
|
+
max_minutes: z
|
|
87
|
+
.number()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe(`Time cap in minutes (1-${MAX_MINUTES}, default ${DEFAULT_MINUTES}).`),
|
|
90
|
+
context: z
|
|
91
|
+
.string()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('Extra context injected each iteration.'),
|
|
94
|
+
session_id: z
|
|
95
|
+
.string()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('Claude Code session ID. Auto-detected if omitted.'),
|
|
98
|
+
template: z
|
|
99
|
+
.string()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Load a saved template by name or id. Template values are used as defaults; explicit params override.'),
|
|
102
|
+
summary: z
|
|
103
|
+
.string()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Brief summary of what this iteration accomplished (1-2 sentences, for journal). Used with action "update".'),
|
|
106
|
+
progress: z
|
|
107
|
+
.string()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Rolling progress summary replacing the previous one. Used with action "update".'),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const loopDescription =
|
|
113
|
+
'Autonomous loop control. Start a repeating task loop, check status, update progress journal, or stop it. The Stop hook drives iteration. Call "update" after each iteration with a brief summary to maintain context across compactions.';
|
|
114
|
+
|
|
115
|
+
// ── Start ──────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function loadTemplate(nameOrId: string): Record<string, unknown> | null {
|
|
118
|
+
try {
|
|
119
|
+
if (!existsSync(LOOP_TEMPLATES_PATH)) return null;
|
|
120
|
+
const templates = JSON.parse(readFileSync(LOOP_TEMPLATES_PATH, 'utf-8'));
|
|
121
|
+
if (!Array.isArray(templates)) return null;
|
|
122
|
+
const lower = nameOrId.toLowerCase();
|
|
123
|
+
return templates.find((t: Record<string, unknown>) =>
|
|
124
|
+
(t.id as string) === nameOrId ||
|
|
125
|
+
(t.name as string)?.toLowerCase() === lower
|
|
126
|
+
) || null;
|
|
127
|
+
} catch { return null; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function handleStart(args: {
|
|
131
|
+
task?: string;
|
|
132
|
+
iterations?: number;
|
|
133
|
+
max_minutes?: number;
|
|
134
|
+
context?: string;
|
|
135
|
+
session_id?: string;
|
|
136
|
+
template?: string;
|
|
137
|
+
}) {
|
|
138
|
+
// Load template defaults if specified
|
|
139
|
+
if (args.template) {
|
|
140
|
+
const tpl = loadTemplate(args.template);
|
|
141
|
+
if (!tpl) {
|
|
142
|
+
return text(`Error: Template "${args.template}" not found. Check Settings → Automations for available templates.`);
|
|
143
|
+
}
|
|
144
|
+
// Template values as defaults; explicit params override
|
|
145
|
+
if (!args.task) args.task = tpl.task as string;
|
|
146
|
+
if (args.iterations === undefined) args.iterations = tpl.iterations as number;
|
|
147
|
+
if (args.max_minutes === undefined) args.max_minutes = tpl.maxMinutes as number;
|
|
148
|
+
if (!args.context && tpl.context) args.context = tpl.context as string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!args.task?.trim()) {
|
|
152
|
+
return text('Error: "task" is required for start action. Describe what to do each iteration.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sessionId = resolveSessionId(args.session_id);
|
|
156
|
+
if (!sessionId) {
|
|
157
|
+
return text('Error: Could not determine session ID. Pass session_id explicitly or ensure CLAUDE_SESSION_ID is set.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const iterations = Math.min(Math.max(args.iterations || DEFAULT_ITERATIONS, 1), MAX_ITERATIONS);
|
|
161
|
+
const maxMinutes = Math.min(Math.max(args.max_minutes || DEFAULT_MINUTES, 1), MAX_MINUTES);
|
|
162
|
+
|
|
163
|
+
ensureLoopDir();
|
|
164
|
+
|
|
165
|
+
// Check for existing active loop
|
|
166
|
+
const loopPath = getLoopPath(sessionId);
|
|
167
|
+
if (existsSync(loopPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const existing = JSON.parse(readFileSync(loopPath, 'utf-8'));
|
|
170
|
+
if (existing?.active) {
|
|
171
|
+
return text(`Error: Loop already active (iteration ${existing.currentIteration}/${existing.totalIterations}). Stop it first with action "stop".`);
|
|
172
|
+
}
|
|
173
|
+
} catch { /* corrupt file, overwrite */ }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const state = {
|
|
177
|
+
active: true,
|
|
178
|
+
task: args.task.trim(),
|
|
179
|
+
totalIterations: iterations,
|
|
180
|
+
currentIteration: 0,
|
|
181
|
+
maxMinutes,
|
|
182
|
+
startedAt: new Date().toISOString(),
|
|
183
|
+
lastIterationAt: null as string | null,
|
|
184
|
+
context: args.context?.trim() || null,
|
|
185
|
+
retries: 0,
|
|
186
|
+
journal: [] as { iteration: number; summary: string; timestamp: string }[],
|
|
187
|
+
lastMemoryAt: 0,
|
|
188
|
+
memoryInterval: 5,
|
|
189
|
+
progressSummary: null as string | null,
|
|
190
|
+
awaitingNext: false,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
writeFileSync(loopPath, JSON.stringify(state, null, 2));
|
|
194
|
+
|
|
195
|
+
return text([
|
|
196
|
+
`Loop started for session ${sessionId}.`,
|
|
197
|
+
`Task: ${state.task}`,
|
|
198
|
+
`Iterations: ${iterations} | Time cap: ${maxMinutes} min`,
|
|
199
|
+
state.context ? `Context: ${state.context}` : '',
|
|
200
|
+
'',
|
|
201
|
+
'The Stop hook will now drive autonomous iteration. Begin your first iteration.',
|
|
202
|
+
].filter(Boolean).join('\n'));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Stop ───────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function handleStop(args: { session_id?: string }) {
|
|
208
|
+
const sessionId = resolveSessionId(args.session_id);
|
|
209
|
+
if (!sessionId) {
|
|
210
|
+
return text('No active loop found to stop.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const loopPath = getLoopPath(sessionId);
|
|
214
|
+
if (!existsSync(loopPath)) {
|
|
215
|
+
return text('No active loop found for this session.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let summary = '';
|
|
219
|
+
try {
|
|
220
|
+
const state = JSON.parse(readFileSync(loopPath, 'utf-8'));
|
|
221
|
+
const elapsed = Math.round((Date.now() - new Date(state.startedAt).getTime()) / 60000);
|
|
222
|
+
summary = ` Completed ${state.currentIteration}/${state.totalIterations} iterations in ${elapsed} min.`;
|
|
223
|
+
} catch { /* ok */ }
|
|
224
|
+
|
|
225
|
+
try { unlinkSync(loopPath); } catch { /* ok */ }
|
|
226
|
+
|
|
227
|
+
return text(`Loop stopped.${summary}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Status ─────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
function handleStatus(args: { session_id?: string }) {
|
|
233
|
+
const sessionId = resolveSessionId(args.session_id);
|
|
234
|
+
if (!sessionId) {
|
|
235
|
+
return text('No active loop.');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const loopPath = getLoopPath(sessionId);
|
|
239
|
+
if (!existsSync(loopPath)) {
|
|
240
|
+
return text('No active loop for this session.');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let state;
|
|
244
|
+
try {
|
|
245
|
+
state = JSON.parse(readFileSync(loopPath, 'utf-8'));
|
|
246
|
+
} catch {
|
|
247
|
+
return text('Loop state file is corrupt.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!state.active) {
|
|
251
|
+
const finishedAt = state.finishedAt ? ` Finished at ${state.finishedAt}.` : '';
|
|
252
|
+
return text(`Loop inactive (completed).${finishedAt} ${state.currentIteration}/${state.totalIterations} iterations done.`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const elapsed = Math.round((Date.now() - new Date(state.startedAt).getTime()) / 60000);
|
|
256
|
+
const iterationsRemaining = state.totalIterations - (state.currentIteration || 0);
|
|
257
|
+
|
|
258
|
+
return text([
|
|
259
|
+
`Loop active: iteration ${state.currentIteration}/${state.totalIterations} (${iterationsRemaining} remaining)`,
|
|
260
|
+
`Task: ${state.task}`,
|
|
261
|
+
`Elapsed: ${elapsed} min`,
|
|
262
|
+
state.context ? `Context: ${state.context}` : '',
|
|
263
|
+
state.lastIterationAt ? `Last iteration: ${state.lastIterationAt}` : '',
|
|
264
|
+
].filter(Boolean).join('\n'));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Update (journal + progress) ───────────────────────────────
|
|
268
|
+
|
|
269
|
+
function handleUpdate(args: {
|
|
270
|
+
session_id?: string;
|
|
271
|
+
summary?: string;
|
|
272
|
+
progress?: string;
|
|
273
|
+
}) {
|
|
274
|
+
if (!args.summary && !args.progress) {
|
|
275
|
+
return text('Error: Provide "summary" (iteration journal) and/or "progress" (rolling summary) for update.');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const sessionId = resolveSessionId(args.session_id);
|
|
279
|
+
if (!sessionId) {
|
|
280
|
+
return text('No active loop to update.');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const loopPath = getLoopPath(sessionId);
|
|
284
|
+
if (!existsSync(loopPath)) {
|
|
285
|
+
return text('No loop state file found.');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let state: Record<string, unknown>;
|
|
289
|
+
try {
|
|
290
|
+
state = JSON.parse(readFileSync(loopPath, 'utf-8'));
|
|
291
|
+
} catch {
|
|
292
|
+
return text('Loop state file is corrupt.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!state.active) {
|
|
296
|
+
return text('Loop is not active.');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Append journal entry
|
|
300
|
+
if (args.summary) {
|
|
301
|
+
if (!Array.isArray(state.journal)) state.journal = [];
|
|
302
|
+
(state.journal as { iteration: number; summary: string; timestamp: string }[]).push({
|
|
303
|
+
iteration: (state.currentIteration as number) || 0,
|
|
304
|
+
summary: args.summary.slice(0, 200),
|
|
305
|
+
timestamp: new Date().toISOString(),
|
|
306
|
+
});
|
|
307
|
+
// Keep only last 10 entries
|
|
308
|
+
if ((state.journal as unknown[]).length > 10) {
|
|
309
|
+
state.journal = (state.journal as unknown[]).slice(-10);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Update rolling progress summary
|
|
314
|
+
if (args.progress) {
|
|
315
|
+
state.progressSummary = args.progress.slice(0, 500);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
writeFileSync(loopPath, JSON.stringify(state, null, 2));
|
|
319
|
+
|
|
320
|
+
return text(`Loop journal updated (iteration ${state.currentIteration}).`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Main dispatcher ────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
export async function handleLoop(args: {
|
|
326
|
+
action: string;
|
|
327
|
+
task?: string;
|
|
328
|
+
iterations?: number;
|
|
329
|
+
max_minutes?: number;
|
|
330
|
+
context?: string;
|
|
331
|
+
session_id?: string;
|
|
332
|
+
template?: string;
|
|
333
|
+
summary?: string;
|
|
334
|
+
progress?: string;
|
|
335
|
+
}) {
|
|
336
|
+
switch (args.action) {
|
|
337
|
+
case 'start':
|
|
338
|
+
return handleStart(args);
|
|
339
|
+
case 'stop':
|
|
340
|
+
return handleStop(args);
|
|
341
|
+
case 'status':
|
|
342
|
+
return handleStatus(args);
|
|
343
|
+
case 'update':
|
|
344
|
+
return handleUpdate(args);
|
|
345
|
+
default:
|
|
346
|
+
return text(`Unknown action "${args.action}". Use: start, stop, status, update.`);
|
|
347
|
+
}
|
|
348
|
+
}
|