clementine-agent 1.6.0 → 1.6.1
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/dist/cli/browser.d.ts +11 -0
- package/dist/cli/browser.js +103 -17
- package/dist/cli/dashboard.js +193 -51
- package/dist/cli/index.js +5 -1
- package/dist/dashboard/builder/serializer.d.ts +9 -9
- package/dist/dashboard/builder/serializer.js +162 -40
- package/dist/tools/builder-tools.js +2 -2
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli/browser.d.ts
CHANGED
|
@@ -20,5 +20,16 @@
|
|
|
20
20
|
export declare function cmdBrowserStatus(): Promise<void>;
|
|
21
21
|
export declare function cmdBrowserInstall(): Promise<void>;
|
|
22
22
|
export declare function cmdBrowserEnable(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Auto-prompt during `clementine update`. Stays silent unless there's
|
|
25
|
+
* something actionable — mirrors the keychain wizard's behavior.
|
|
26
|
+
*
|
|
27
|
+
* Skips prompting when:
|
|
28
|
+
* - Not in an interactive TTY
|
|
29
|
+
* - The MCP wrapper isn't shipped with this version
|
|
30
|
+
* - Browser harness is already installed AND enabled
|
|
31
|
+
* - User previously dismissed the prompt
|
|
32
|
+
*/
|
|
33
|
+
export declare function maybePromptBrowserHarness(): Promise<void>;
|
|
23
34
|
export declare function cmdBrowserDisable(): Promise<void>;
|
|
24
35
|
//# sourceMappingURL=browser.d.ts.map
|
package/dist/cli/browser.js
CHANGED
|
@@ -22,6 +22,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
22
22
|
import os from 'node:os';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { confirm } from '@inquirer/prompts';
|
|
25
26
|
const BOLD = '\x1b[1m';
|
|
26
27
|
const DIM = '\x1b[0;90m';
|
|
27
28
|
const GREEN = '\x1b[0;32m';
|
|
@@ -40,6 +41,7 @@ const VENV_PYTHON = path.join(VENV_DIR, 'bin', 'python3');
|
|
|
40
41
|
const MCP_SERVERS_FILE = path.join(BASE_DIR, 'mcp-servers.json');
|
|
41
42
|
const HARNESS_REPO = 'https://github.com/browser-use/browser-harness.git';
|
|
42
43
|
const SERVER_NAME = 'browser-harness';
|
|
44
|
+
const DISMISSED_MARKER = path.join(BASE_DIR, '.browser-harness-dismissed');
|
|
43
45
|
function commandExists(cmd) {
|
|
44
46
|
const result = spawnSync('which', [cmd], { stdio: 'pipe' });
|
|
45
47
|
return result.status === 0;
|
|
@@ -102,7 +104,11 @@ export async function cmdBrowserStatus() {
|
|
|
102
104
|
console.log();
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Core install logic. Returns true on success, false on any failure.
|
|
109
|
+
* Prints progress + errors to stdout/stderr but never calls process.exit.
|
|
110
|
+
*/
|
|
111
|
+
async function runInstall() {
|
|
106
112
|
console.log();
|
|
107
113
|
console.log(` ${BOLD}Installing browser-harness${RESET} ${DIM}(beta)${RESET}`);
|
|
108
114
|
console.log();
|
|
@@ -110,13 +116,13 @@ export async function cmdBrowserInstall() {
|
|
|
110
116
|
console.error(` ${RED}python3 not found.${RESET} Install Python 3.10+ first:`);
|
|
111
117
|
console.error(` ${CYAN}brew install python@3.12${RESET}`);
|
|
112
118
|
console.error();
|
|
113
|
-
|
|
119
|
+
return false;
|
|
114
120
|
}
|
|
115
121
|
if (!existsSync(MCP_SCRIPT)) {
|
|
116
122
|
console.error(` ${RED}MCP wrapper not found at:${RESET} ${MCP_SCRIPT}`);
|
|
117
123
|
console.error(` ${DIM}This means the package was installed without vendor/ files. Reinstall:${RESET}`);
|
|
118
124
|
console.error(` ${CYAN}npm install -g clementine-agent@latest${RESET}`);
|
|
119
|
-
|
|
125
|
+
return false;
|
|
120
126
|
}
|
|
121
127
|
if (!existsSync(BASE_DIR))
|
|
122
128
|
mkdirSync(BASE_DIR, { recursive: true });
|
|
@@ -124,7 +130,7 @@ export async function cmdBrowserInstall() {
|
|
|
124
130
|
if (!existsSync(HARNESS_HOME)) {
|
|
125
131
|
if (!commandExists('git')) {
|
|
126
132
|
console.error(` ${RED}git not found.${RESET} Install git, then re-run.`);
|
|
127
|
-
|
|
133
|
+
return false;
|
|
128
134
|
}
|
|
129
135
|
console.log(` ${DIM}→ cloning ${HARNESS_REPO}${RESET}`);
|
|
130
136
|
try {
|
|
@@ -132,7 +138,7 @@ export async function cmdBrowserInstall() {
|
|
|
132
138
|
}
|
|
133
139
|
catch {
|
|
134
140
|
console.error(` ${RED}Clone failed.${RESET} Check network / git access and try again.`);
|
|
135
|
-
|
|
141
|
+
return false;
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
else {
|
|
@@ -146,7 +152,7 @@ export async function cmdBrowserInstall() {
|
|
|
146
152
|
}
|
|
147
153
|
catch {
|
|
148
154
|
console.error(` ${RED}venv creation failed.${RESET}`);
|
|
149
|
-
|
|
155
|
+
return false;
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
else {
|
|
@@ -165,25 +171,21 @@ export async function cmdBrowserInstall() {
|
|
|
165
171
|
}
|
|
166
172
|
catch {
|
|
167
173
|
console.error(` ${RED}pip install failed.${RESET} Inspect output above and re-run when fixed.`);
|
|
168
|
-
|
|
174
|
+
return false;
|
|
169
175
|
}
|
|
170
176
|
console.log();
|
|
171
177
|
console.log(` ${GREEN}✓${RESET} Install complete.`);
|
|
172
|
-
|
|
173
|
-
console.log(` ${BOLD}Next steps:${RESET}`);
|
|
174
|
-
console.log(` 1. Enable Chrome remote debugging — open Chrome with:`);
|
|
175
|
-
console.log(` ${CYAN}/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome \\${RESET}`);
|
|
176
|
-
console.log(` ${CYAN}--remote-debugging-port=9222${RESET}`);
|
|
177
|
-
console.log(` 2. Enable the MCP server: ${BOLD}clementine browser enable${RESET}`);
|
|
178
|
-
console.log(` 3. Restart the daemon: ${BOLD}clementine restart${RESET}`);
|
|
179
|
-
console.log();
|
|
178
|
+
return true;
|
|
180
179
|
}
|
|
181
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Core enable logic. Returns true on success, false on any failure.
|
|
182
|
+
*/
|
|
183
|
+
function runEnable() {
|
|
182
184
|
if (!existsSync(VENV_PYTHON) || !existsSync(MCP_SCRIPT)) {
|
|
183
185
|
console.error();
|
|
184
186
|
console.error(` ${RED}Not installed yet.${RESET} Run ${BOLD}clementine browser install${RESET} first.`);
|
|
185
187
|
console.error();
|
|
186
|
-
|
|
188
|
+
return false;
|
|
187
189
|
}
|
|
188
190
|
const servers = loadMcpServers();
|
|
189
191
|
servers[SERVER_NAME] = {
|
|
@@ -201,9 +203,93 @@ export async function cmdBrowserEnable() {
|
|
|
201
203
|
saveMcpServers(servers);
|
|
202
204
|
console.log();
|
|
203
205
|
console.log(` ${GREEN}✓${RESET} Registered ${BOLD}${SERVER_NAME}${RESET} in mcp-servers.json`);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
export async function cmdBrowserInstall() {
|
|
209
|
+
const ok = await runInstall();
|
|
210
|
+
if (!ok)
|
|
211
|
+
process.exit(1);
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(` ${BOLD}Next steps:${RESET}`);
|
|
214
|
+
console.log(` 1. Enable Chrome remote debugging — open Chrome with:`);
|
|
215
|
+
console.log(` ${CYAN}/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome \\${RESET}`);
|
|
216
|
+
console.log(` ${CYAN}--remote-debugging-port=9222${RESET}`);
|
|
217
|
+
console.log(` 2. Enable the MCP server: ${BOLD}clementine browser enable${RESET}`);
|
|
218
|
+
console.log(` 3. Restart the daemon: ${BOLD}clementine restart${RESET}`);
|
|
219
|
+
console.log();
|
|
220
|
+
}
|
|
221
|
+
export async function cmdBrowserEnable() {
|
|
222
|
+
const ok = runEnable();
|
|
223
|
+
if (!ok)
|
|
224
|
+
process.exit(1);
|
|
204
225
|
console.log(` ${DIM}Restart the daemon to pick up the change: clementine restart${RESET}`);
|
|
205
226
|
console.log();
|
|
206
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Auto-prompt during `clementine update`. Stays silent unless there's
|
|
230
|
+
* something actionable — mirrors the keychain wizard's behavior.
|
|
231
|
+
*
|
|
232
|
+
* Skips prompting when:
|
|
233
|
+
* - Not in an interactive TTY
|
|
234
|
+
* - The MCP wrapper isn't shipped with this version
|
|
235
|
+
* - Browser harness is already installed AND enabled
|
|
236
|
+
* - User previously dismissed the prompt
|
|
237
|
+
*/
|
|
238
|
+
export async function maybePromptBrowserHarness() {
|
|
239
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
240
|
+
return;
|
|
241
|
+
if (!existsSync(MCP_SCRIPT))
|
|
242
|
+
return;
|
|
243
|
+
const servers = loadMcpServers();
|
|
244
|
+
const enabled = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);
|
|
245
|
+
const installed = existsSync(VENV_PYTHON);
|
|
246
|
+
if (enabled && installed)
|
|
247
|
+
return;
|
|
248
|
+
if (existsSync(DISMISSED_MARKER))
|
|
249
|
+
return;
|
|
250
|
+
console.log();
|
|
251
|
+
console.log(` ${BOLD}Browser Harness available${RESET} ${DIM}(beta, opt-in)${RESET}`);
|
|
252
|
+
console.log(` ${DIM}Lets Clementine drive your real Chrome — fill forms, post on LinkedIn,${RESET}`);
|
|
253
|
+
console.log(` ${DIM}book appointments — using your live browser session.${RESET}`);
|
|
254
|
+
console.log();
|
|
255
|
+
let answer;
|
|
256
|
+
try {
|
|
257
|
+
answer = await confirm({
|
|
258
|
+
message: 'Install Browser Harness now?',
|
|
259
|
+
default: false,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// User Ctrl+C'd or terminal closed — treat as decline, don't dismiss permanently
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!answer) {
|
|
267
|
+
try {
|
|
268
|
+
writeFileSync(DISMISSED_MARKER, new Date().toISOString() + '\n');
|
|
269
|
+
}
|
|
270
|
+
catch { /* non-fatal */ }
|
|
271
|
+
console.log(` ${DIM}Skipped. To install later: clementine browser install${RESET}`);
|
|
272
|
+
console.log();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// User said yes — run install + enable inline
|
|
276
|
+
const installOk = await runInstall();
|
|
277
|
+
if (!installOk) {
|
|
278
|
+
console.error(` ${YELLOW}Install failed.${RESET} ${DIM}You can retry with: clementine browser install${RESET}`);
|
|
279
|
+
console.log();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const enableOk = runEnable();
|
|
283
|
+
if (!enableOk) {
|
|
284
|
+
console.error(` ${YELLOW}Enable failed.${RESET} ${DIM}Retry with: clementine browser enable${RESET}`);
|
|
285
|
+
console.log();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
console.log();
|
|
289
|
+
console.log(` ${GREEN}✓${RESET} Browser Harness installed and enabled.`);
|
|
290
|
+
console.log(` ${DIM}Open Chrome with --remote-debugging-port=9222 to connect.${RESET}`);
|
|
291
|
+
console.log();
|
|
292
|
+
}
|
|
207
293
|
export async function cmdBrowserDisable() {
|
|
208
294
|
const servers = loadMcpServers();
|
|
209
295
|
if (!Object.prototype.hasOwnProperty.call(servers, SERVER_NAME)) {
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -3174,8 +3174,9 @@ export async function cmdDashboard(opts) {
|
|
|
3174
3174
|
maxTurns: 15,
|
|
3175
3175
|
}],
|
|
3176
3176
|
sourceFile: '',
|
|
3177
|
+
agentSlug: body.agentSlug || undefined,
|
|
3177
3178
|
};
|
|
3178
|
-
const id = workflowId(slug);
|
|
3179
|
+
const id = workflowId(slug, body.agentSlug || undefined);
|
|
3179
3180
|
const result = saveWorkflow(id, wf);
|
|
3180
3181
|
if (!result.ok) {
|
|
3181
3182
|
res.status(400).json({ error: result.error });
|
|
@@ -5620,6 +5621,55 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5620
5621
|
// ── Builder chat endpoint ──────────────────────────────────────────
|
|
5621
5622
|
// Track which builder sessions have received the full system prefix
|
|
5622
5623
|
const builderSessionInited = new Set();
|
|
5624
|
+
async function buildBuilderCapabilityContext(agentSlug) {
|
|
5625
|
+
const lines = [];
|
|
5626
|
+
try {
|
|
5627
|
+
const { discoverMcpServers, loadToolInventory } = await import('../agent/mcp-bridge.js');
|
|
5628
|
+
const servers = discoverMcpServers();
|
|
5629
|
+
const inv = loadToolInventory();
|
|
5630
|
+
const serverLines = servers.map((s) => {
|
|
5631
|
+
const tools = (inv?.tools ?? [])
|
|
5632
|
+
.filter((t) => t.startsWith(`mcp__${s.name}__`))
|
|
5633
|
+
.map((t) => t.split('__')[2])
|
|
5634
|
+
.filter(Boolean)
|
|
5635
|
+
.slice(0, 40);
|
|
5636
|
+
return `- ${s.name} [${s.enabled ? 'on' : 'off'}]: ${tools.length ? tools.join(', ') : '(no tools cached yet)'}`;
|
|
5637
|
+
});
|
|
5638
|
+
lines.push('[AVAILABLE MCP SERVERS AND TOOLS]');
|
|
5639
|
+
lines.push(serverLines.length ? serverLines.join('\n') : '- none configured');
|
|
5640
|
+
}
|
|
5641
|
+
catch {
|
|
5642
|
+
lines.push('[AVAILABLE MCP SERVERS AND TOOLS]\n- unavailable');
|
|
5643
|
+
}
|
|
5644
|
+
const readSkills = (dir, scope) => {
|
|
5645
|
+
if (!existsSync(dir))
|
|
5646
|
+
return [];
|
|
5647
|
+
return readdirSync(dir)
|
|
5648
|
+
.filter(f => f.endsWith('.md'))
|
|
5649
|
+
.slice(0, 80)
|
|
5650
|
+
.map(f => {
|
|
5651
|
+
try {
|
|
5652
|
+
const parsed = matter(readFileSync(path.join(dir, f), 'utf-8'));
|
|
5653
|
+
const name = f.replace(/\.md$/, '');
|
|
5654
|
+
const title = String(parsed.data.title ?? name);
|
|
5655
|
+
const triggers = Array.isArray(parsed.data.triggers) ? parsed.data.triggers.join(', ') : '';
|
|
5656
|
+
const tools = Array.isArray(parsed.data.toolsUsed) ? parsed.data.toolsUsed.join(', ') : '';
|
|
5657
|
+
return `- ${title} (${scope}${name !== title ? `/${name}` : ''})${triggers ? ` triggers: ${triggers}` : ''}${tools ? ` tools: ${tools}` : ''}`;
|
|
5658
|
+
}
|
|
5659
|
+
catch {
|
|
5660
|
+
return null;
|
|
5661
|
+
}
|
|
5662
|
+
})
|
|
5663
|
+
.filter((x) => Boolean(x));
|
|
5664
|
+
};
|
|
5665
|
+
const globalSkills = readSkills(path.join(VAULT_DIR, '00-System', 'skills'), 'global');
|
|
5666
|
+
const agentSkills = agentSlug
|
|
5667
|
+
? readSkills(path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'skills'), `agent:${agentSlug}`)
|
|
5668
|
+
: [];
|
|
5669
|
+
lines.push('[AVAILABLE SKILLS]');
|
|
5670
|
+
lines.push([...agentSkills, ...globalSkills].length ? [...agentSkills, ...globalSkills].join('\n') : '- none saved yet');
|
|
5671
|
+
return `\n${lines.join('\n\n')}\n`;
|
|
5672
|
+
}
|
|
5623
5673
|
app.post('/api/builder/chat', async (req, res) => {
|
|
5624
5674
|
const { message, artifactType, agentSlug, currentArtifact, attachments, linkedTools } = req.body;
|
|
5625
5675
|
if (!message || typeof message !== 'string') {
|
|
@@ -5686,7 +5736,8 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5686
5736
|
`Workflows are defined as markdown files with YAML frontmatter. Each step has an id, prompt, optional agent, and optional dependsOn array.\n` +
|
|
5687
5737
|
`When the user says "save" or approves, output the final artifact block.]\n\n`
|
|
5688
5738
|
: `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
|
|
5689
|
-
|
|
5739
|
+
const capabilityContext = await buildBuilderCapabilityContext(agentSlug);
|
|
5740
|
+
enrichedMessage = builderPrefix + capabilityContext + fileContext + toolContext + artifactContext + message;
|
|
5690
5741
|
builderSessionInited.add(sessionKey);
|
|
5691
5742
|
}
|
|
5692
5743
|
else {
|
|
@@ -5808,35 +5859,26 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5808
5859
|
}
|
|
5809
5860
|
else if (artifactType === 'cron') {
|
|
5810
5861
|
// Scope cron to agent if selected
|
|
5811
|
-
const
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
: { cronFile: path.join(VAULT_DIR, '00-System', 'CRON.md') };
|
|
5820
|
-
if (!existsSync(cronFile)) {
|
|
5821
|
-
res.status(500).json({ error: 'CRON.md not found' });
|
|
5822
|
-
return;
|
|
5823
|
-
}
|
|
5824
|
-
const matterMod = require('gray-matter');
|
|
5825
|
-
const parsed = matterMod(readFileSync(cronFile, 'utf-8'));
|
|
5826
|
-
const jobs = parsed.data.jobs || [];
|
|
5862
|
+
const rawName = String(artifact.name || 'new-cron').trim();
|
|
5863
|
+
const jobName = agentSlug && rawName.startsWith(agentSlug + ':')
|
|
5864
|
+
? rawName.slice(agentSlug.length + 1)
|
|
5865
|
+
: rawName;
|
|
5866
|
+
const cronFile = agentSlug
|
|
5867
|
+
? path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'CRON.md')
|
|
5868
|
+
: path.join(VAULT_DIR, '00-System', 'CRON.md');
|
|
5869
|
+
const { parsed, jobs } = readCronFileAt(cronFile);
|
|
5827
5870
|
jobs.push({
|
|
5828
5871
|
name: jobName,
|
|
5829
5872
|
schedule: artifact.schedule,
|
|
5830
5873
|
prompt: artifact.prompt,
|
|
5831
5874
|
tier: artifact.tier || 1,
|
|
5832
5875
|
enabled: artifact.enabled !== false,
|
|
5833
|
-
...(agentSlug ? { agent: agentSlug } : {}),
|
|
5834
5876
|
...(artifact.mode === 'unleashed' ? { mode: 'unleashed', max_hours: artifact.max_hours || 1 } : {}),
|
|
5835
5877
|
...(artifact.work_dir ? { work_dir: artifact.work_dir } : {}),
|
|
5836
5878
|
});
|
|
5837
|
-
parsed
|
|
5838
|
-
|
|
5839
|
-
res.json({ ok: true, name:
|
|
5879
|
+
writeCronFileAt(cronFile, parsed, jobs);
|
|
5880
|
+
const displayName = agentSlug ? `${agentSlug}:${jobName}` : jobName;
|
|
5881
|
+
res.json({ ok: true, name: displayName, message: `Cron job "${displayName}" saved` });
|
|
5840
5882
|
}
|
|
5841
5883
|
else if (artifactType === 'agent') {
|
|
5842
5884
|
// Create agent via the same mechanism as the manual form
|
|
@@ -5865,15 +5907,19 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5865
5907
|
}
|
|
5866
5908
|
else if (artifactType === 'workflow') {
|
|
5867
5909
|
// Save workflow as markdown file
|
|
5868
|
-
const wfDir =
|
|
5910
|
+
const wfDir = agentSlug
|
|
5911
|
+
? path.join(VAULT_DIR, '00-System', 'agents', agentSlug, 'workflows')
|
|
5912
|
+
: path.join(VAULT_DIR, '00-System', 'workflows');
|
|
5869
5913
|
if (!existsSync(wfDir))
|
|
5870
5914
|
mkdirSync(wfDir, { recursive: true });
|
|
5871
5915
|
const wfName = (artifact.name || 'new-workflow').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
|
5872
5916
|
const matterMod = require('gray-matter');
|
|
5873
5917
|
const meta = {
|
|
5918
|
+
type: 'workflow',
|
|
5874
5919
|
name: artifact.name || wfName,
|
|
5875
5920
|
description: artifact.description || '',
|
|
5876
5921
|
enabled: true,
|
|
5922
|
+
...(agentSlug ? { agentSlug } : {}),
|
|
5877
5923
|
...(artifact.schedule ? { trigger: { schedule: artifact.schedule } } : {}),
|
|
5878
5924
|
};
|
|
5879
5925
|
const content = matterMod.stringify(`\n# ${artifact.name || wfName}\n\n${artifact.description || ''}\n\n## Steps\n\n${artifact.steps || ''}\n`, meta);
|
|
@@ -12315,7 +12361,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
12315
12361
|
<option value="agent">agent</option>
|
|
12316
12362
|
<option value="workflow">workflow</option>
|
|
12317
12363
|
</select>
|
|
12318
|
-
<
|
|
12364
|
+
<label for="builder-agent-scope" style="font-size:11px;color:var(--text-muted);font-weight:600">Owner</label>
|
|
12365
|
+
<select id="builder-agent-scope" onchange="setBuilderAgentScope(this.value)" style="padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;max-width:220px">
|
|
12366
|
+
<option value="__all">All owners</option>
|
|
12367
|
+
<option value="__global">Clementine (global)</option>
|
|
12368
|
+
</select>
|
|
12369
|
+
<span id="builder-capability-summary" style="font-size:11px;color:var(--text-muted)"></span>
|
|
12370
|
+
<span id="builder-agent-label" style="display:none;padding:0;font-size:13px;color:var(--text-secondary);font-weight:500"></span>
|
|
12319
12371
|
<input type="hidden" id="builder-agent" value="">
|
|
12320
12372
|
<span style="flex:1"></span>
|
|
12321
12373
|
<button class="btn-sm btn-primary" onclick="newFromBuildHeader()" title="Create a new artifact for this tab" style="padding:4px 14px;border-radius:6px;cursor:pointer;font-size:12px">New</button>
|
|
@@ -15152,9 +15204,24 @@ async function newFromBuildHeader() {
|
|
|
15152
15204
|
var name = prompt('Name your new ' + noun + ':');
|
|
15153
15205
|
if (!name || !name.trim()) return;
|
|
15154
15206
|
try {
|
|
15155
|
-
var
|
|
15156
|
-
|
|
15157
|
-
|
|
15207
|
+
var agentSlug = getBuilderSelectedAgentSlug();
|
|
15208
|
+
var r;
|
|
15209
|
+
if (activeTab === 'crons') {
|
|
15210
|
+
r = await apiJson('POST', '/api/cron', {
|
|
15211
|
+
name: name.trim(),
|
|
15212
|
+
schedule: '0 9 * * *',
|
|
15213
|
+
prompt: 'Describe what this scheduled automation should do.',
|
|
15214
|
+
tier: 1,
|
|
15215
|
+
enabled: true,
|
|
15216
|
+
agent: agentSlug || undefined,
|
|
15217
|
+
});
|
|
15218
|
+
if (r && !r.error) r.id = builderScopedId('cron', name.trim(), agentSlug);
|
|
15219
|
+
} else {
|
|
15220
|
+
r = await apiJson('POST', '/api/builder/workflows', {
|
|
15221
|
+
name: name.trim(),
|
|
15222
|
+
agentSlug: agentSlug || undefined,
|
|
15223
|
+
});
|
|
15224
|
+
}
|
|
15158
15225
|
if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
|
|
15159
15226
|
if (r && r.id) {
|
|
15160
15227
|
await refreshBuilderCanvasPicker(activeTab === 'crons' ? 'cron' : 'workflow');
|
|
@@ -15209,12 +15276,26 @@ async function forkBuildTemplate(templateId) {
|
|
|
15209
15276
|
var name = prompt('Name for the new workflow:', tpl.name);
|
|
15210
15277
|
if (!name) return;
|
|
15211
15278
|
try {
|
|
15212
|
-
var
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15279
|
+
var agentSlug = getBuilderSelectedAgentSlug();
|
|
15280
|
+
var r;
|
|
15281
|
+
if (tpl.schedule) {
|
|
15282
|
+
r = await apiJson('POST', '/api/cron', {
|
|
15283
|
+
name: name,
|
|
15284
|
+
schedule: tpl.schedule,
|
|
15285
|
+
prompt: tpl.initialPrompt,
|
|
15286
|
+
tier: 1,
|
|
15287
|
+
enabled: true,
|
|
15288
|
+
agent: agentSlug || undefined,
|
|
15289
|
+
});
|
|
15290
|
+
if (r && !r.error) r.id = builderScopedId('cron', name, agentSlug);
|
|
15291
|
+
} else {
|
|
15292
|
+
r = await apiJson('POST', '/api/builder/workflows', {
|
|
15293
|
+
name: name,
|
|
15294
|
+
description: tpl.description,
|
|
15295
|
+
initialPrompt: tpl.initialPrompt,
|
|
15296
|
+
agentSlug: agentSlug || undefined,
|
|
15297
|
+
});
|
|
15298
|
+
}
|
|
15218
15299
|
if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
|
|
15219
15300
|
if (r && r.id) {
|
|
15220
15301
|
switchBuildTab(tpl.schedule ? 'crons' : 'workflows');
|
|
@@ -19351,6 +19432,26 @@ var _builderCanvasOpenId = null;
|
|
|
19351
19432
|
var _builderCanvasLastWorkflow = null;
|
|
19352
19433
|
var _builderDrawflowLoading = null;
|
|
19353
19434
|
|
|
19435
|
+
function getBuilderOwnerScope() {
|
|
19436
|
+
var sel = document.getElementById('builder-agent-scope');
|
|
19437
|
+
return (sel && sel.value) || '__all';
|
|
19438
|
+
}
|
|
19439
|
+
|
|
19440
|
+
function getBuilderSelectedAgentSlug() {
|
|
19441
|
+
var scope = getBuilderOwnerScope();
|
|
19442
|
+
return (scope && scope !== '__all' && scope !== '__global') ? scope : '';
|
|
19443
|
+
}
|
|
19444
|
+
|
|
19445
|
+
function builderScopedId(type, name, agentSlug) {
|
|
19446
|
+
var prefix = type === 'cron' ? 'cron:' : 'workflow:';
|
|
19447
|
+
return agentSlug ? (prefix + 'agent:' + agentSlug + ':' + name) : (prefix + 'global:' + name);
|
|
19448
|
+
}
|
|
19449
|
+
|
|
19450
|
+
function builderOwnerLabel(item) {
|
|
19451
|
+
if (!item || !item.agentSlug) return 'Clementine';
|
|
19452
|
+
return item.agentSlug;
|
|
19453
|
+
}
|
|
19454
|
+
|
|
19354
19455
|
function _ensureDrawflowLoaded() {
|
|
19355
19456
|
if (window.Drawflow) return Promise.resolve();
|
|
19356
19457
|
if (_builderDrawflowLoading) return _builderDrawflowLoading;
|
|
@@ -19370,11 +19471,17 @@ async function refreshBuilderCanvasPicker(type) {
|
|
|
19370
19471
|
try {
|
|
19371
19472
|
var r = await apiFetch('/api/builder/workflows');
|
|
19372
19473
|
var d = await r.json();
|
|
19373
|
-
var
|
|
19474
|
+
var ownerScope = getBuilderOwnerScope();
|
|
19475
|
+
var items = (d.workflows || []).filter(function(w) {
|
|
19476
|
+
if (w.origin !== type) return false;
|
|
19477
|
+
if (ownerScope === '__all') return true;
|
|
19478
|
+
if (ownerScope === '__global') return !w.agentSlug;
|
|
19479
|
+
return w.agentSlug === ownerScope;
|
|
19480
|
+
});
|
|
19374
19481
|
var opts = '<option value="">' + (items.length ? '— pick a ' + type + ' —' : '(none yet)') + '</option>';
|
|
19375
19482
|
for (var i = 0; i < items.length; i++) {
|
|
19376
19483
|
var w = items[i];
|
|
19377
|
-
var lbl = w.name + (w.schedule ? ' · ' + w.schedule : '') + (w.enabled ? '' : ' · off');
|
|
19484
|
+
var lbl = builderOwnerLabel(w) + ' · ' + w.name + (w.schedule ? ' · ' + w.schedule : '') + (w.enabled ? '' : ' · off');
|
|
19378
19485
|
opts += '<option value="' + esc(w.id) + '">' + esc(lbl) + '</option>';
|
|
19379
19486
|
}
|
|
19380
19487
|
picker.innerHTML = opts;
|
|
@@ -20031,7 +20138,8 @@ async function refreshBuilderSkills() {
|
|
|
20031
20138
|
var countEl = document.getElementById('builder-skills-count');
|
|
20032
20139
|
if (!container) return;
|
|
20033
20140
|
try {
|
|
20034
|
-
var
|
|
20141
|
+
var agentSlug = getBuilderSelectedAgentSlug();
|
|
20142
|
+
var r = await apiFetch(agentSlug ? ('/api/agents/' + encodeURIComponent(agentSlug) + '/skills') : '/api/skills');
|
|
20035
20143
|
var d = await r.json();
|
|
20036
20144
|
var skills = d.skills || [];
|
|
20037
20145
|
if (countEl) countEl.textContent = skills.length + ' skill' + (skills.length !== 1 ? 's' : '');
|
|
@@ -20041,12 +20149,13 @@ async function refreshBuilderSkills() {
|
|
|
20041
20149
|
}
|
|
20042
20150
|
var html = '';
|
|
20043
20151
|
for (var s of skills) {
|
|
20152
|
+
var scopeTag = s.scope === 'agent' ? '<span style="font-size:9px;background:var(--blue);color:white;padding:1px 5px;border-radius:3px">agent</span> ' : '';
|
|
20044
20153
|
var sourceTag = s.source === 'builder' ? '<span style="font-size:9px;background:var(--accent);color:white;padding:1px 5px;border-radius:3px">built</span>'
|
|
20045
20154
|
: s.source === 'manual' ? '<span style="font-size:9px;background:var(--blue);color:white;padding:1px 5px;border-radius:3px">taught</span>'
|
|
20046
20155
|
: '<span style="font-size:9px;background:var(--bg-tertiary);color:var(--text-muted);padding:1px 5px;border-radius:3px">' + esc(s.source || 'auto') + '</span>';
|
|
20047
20156
|
html += '<div style="display:flex;align-items:center;gap:6px;padding:6px 4px;border-bottom:1px solid var(--border);font-size:12px">'
|
|
20048
20157
|
+ '<div style="flex:1;min-width:0">'
|
|
20049
|
-
+ '<div style="font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(s.title) + ' ' + sourceTag + '</div>'
|
|
20158
|
+
+ '<div style="font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(s.title) + ' ' + scopeTag + sourceTag + '</div>'
|
|
20050
20159
|
+ '<div style="font-size:10px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(s.description || '') + '</div>'
|
|
20051
20160
|
+ '</div>'
|
|
20052
20161
|
+ '<button onclick="editSkillInBuilder(\\x27' + esc(s.name) + '\\x27)" style="background:none;border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-size:10px;color:var(--accent);cursor:pointer;white-space:nowrap">Edit</button>'
|
|
@@ -20094,6 +20203,17 @@ async function editSkillInBuilder(name, agentSlug) {
|
|
|
20094
20203
|
var agentLabel = document.getElementById('builder-agent-label');
|
|
20095
20204
|
if (agentHidden) agentHidden.value = agentSlug || '';
|
|
20096
20205
|
if (agentLabel) agentLabel.textContent = agentSlug ? 'Agent: ' + agentSlug : '';
|
|
20206
|
+
var ownerSel = document.getElementById('builder-agent-scope');
|
|
20207
|
+
if (ownerSel) {
|
|
20208
|
+
if (agentSlug && !Array.prototype.some.call(ownerSel.options, function(o) { return o.value === agentSlug; })) {
|
|
20209
|
+
var opt = document.createElement('option');
|
|
20210
|
+
opt.value = agentSlug;
|
|
20211
|
+
opt.textContent = agentSlug;
|
|
20212
|
+
ownerSel.appendChild(opt);
|
|
20213
|
+
}
|
|
20214
|
+
ownerSel.value = agentSlug || '__global';
|
|
20215
|
+
setBuilderAgentScope(ownerSel.value);
|
|
20216
|
+
}
|
|
20097
20217
|
|
|
20098
20218
|
// Reset session on server so next message gets full prefix
|
|
20099
20219
|
await apiJson('POST', '/api/builder/reset', { artifactType: 'skill', agentSlug: agentSlug || undefined }).catch(function(){});
|
|
@@ -20417,23 +20537,45 @@ async function saveBuilderArtifact() {
|
|
|
20417
20537
|
} catch(e) { toast('Error: ' + e, 'error'); }
|
|
20418
20538
|
}
|
|
20419
20539
|
|
|
20420
|
-
|
|
20421
|
-
function refreshBuilderAgents(preselect) {
|
|
20540
|
+
function setBuilderAgentScope(value) {
|
|
20422
20541
|
var hidden = document.getElementById('builder-agent');
|
|
20423
20542
|
var label = document.getElementById('builder-agent-label');
|
|
20424
|
-
|
|
20425
|
-
hidden.value =
|
|
20426
|
-
if (
|
|
20427
|
-
label.textContent =
|
|
20428
|
-
|
|
20543
|
+
var agentSlug = (value && value !== '__all' && value !== '__global') ? value : '';
|
|
20544
|
+
if (hidden) hidden.value = agentSlug;
|
|
20545
|
+
if (label) {
|
|
20546
|
+
label.textContent = value === '__all'
|
|
20547
|
+
? 'All owners'
|
|
20548
|
+
: value === '__global'
|
|
20549
|
+
? 'Clementine (global)'
|
|
20550
|
+
: agentSlug;
|
|
20429
20551
|
}
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
if (
|
|
20433
|
-
|
|
20434
|
-
|
|
20435
|
-
|
|
20552
|
+
var type = (document.getElementById('builder-type') || {}).value;
|
|
20553
|
+
if (type === 'cron' || type === 'workflow') refreshBuilderCanvasPicker(type);
|
|
20554
|
+
if (type === 'skill') refreshBuilderSkills();
|
|
20555
|
+
apiJson('POST', '/api/builder/reset', { artifactType: type, agentSlug: agentSlug || undefined }).catch(function(){});
|
|
20556
|
+
}
|
|
20557
|
+
|
|
20558
|
+
// Populate owner dropdown when builder page loads
|
|
20559
|
+
async function refreshBuilderAgents(preselect) {
|
|
20560
|
+
var hidden = document.getElementById('builder-agent');
|
|
20561
|
+
var label = document.getElementById('builder-agent-label');
|
|
20562
|
+
var select = document.getElementById('builder-agent-scope');
|
|
20563
|
+
if (!hidden || !label || !select) return;
|
|
20564
|
+
var current = preselect ? preselect : (select.value || '__all');
|
|
20565
|
+
try {
|
|
20566
|
+
var r = await apiFetch('/api/agents');
|
|
20567
|
+
var agents = await r.json();
|
|
20568
|
+
var opts = '<option value="__all">All owners</option><option value="__global">Clementine (global)</option>';
|
|
20569
|
+
(agents || []).forEach(function(a) {
|
|
20570
|
+
opts += '<option value="' + esc(a.slug) + '">' + esc(a.name || a.slug) + '</option>';
|
|
20571
|
+
});
|
|
20572
|
+
select.innerHTML = opts;
|
|
20573
|
+
} catch(e) {
|
|
20574
|
+
select.innerHTML = '<option value="__all">All owners</option><option value="__global">Clementine (global)</option>';
|
|
20436
20575
|
}
|
|
20576
|
+
var hasCurrent = Array.prototype.some.call(select.options, function(o) { return o.value === current; });
|
|
20577
|
+
select.value = hasCurrent ? current : '__all';
|
|
20578
|
+
setBuilderAgentScope(select.value);
|
|
20437
20579
|
}
|
|
20438
20580
|
|
|
20439
20581
|
// ── Builder Linked Tools ──────────────────
|
|
@@ -20804,7 +20946,7 @@ function openBuilderForNewWorkflow() {
|
|
|
20804
20946
|
if (typeSel) { typeSel.value = 'workflow'; updateBuilderMode(); }
|
|
20805
20947
|
var name = prompt('Name your new workflow:');
|
|
20806
20948
|
if (!name) return;
|
|
20807
|
-
apiJson('POST', '/api/builder/workflows', { name: name }).then(function(r) {
|
|
20949
|
+
apiJson('POST', '/api/builder/workflows', { name: name, agentSlug: getBuilderSelectedAgentSlug() || undefined }).then(function(r) {
|
|
20808
20950
|
if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
|
|
20809
20951
|
if (r && r.id) {
|
|
20810
20952
|
// Refresh picker, then open the new workflow
|
package/dist/cli/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { cmdCronList, cmdCronRun, cmdCronRunDue, cmdCronRuns, cmdCronAdd, cmdCro
|
|
|
24
24
|
import { cmdDashboard } from './dashboard.js';
|
|
25
25
|
import { cmdChat } from './chat.js';
|
|
26
26
|
import { cmdIngestSeed, cmdIngestRun, cmdIngestList, cmdIngestStatus } from './ingest.js';
|
|
27
|
-
import { cmdBrowserStatus, cmdBrowserInstall, cmdBrowserEnable, cmdBrowserDisable } from './browser.js';
|
|
27
|
+
import { cmdBrowserStatus, cmdBrowserInstall, cmdBrowserEnable, cmdBrowserDisable, maybePromptBrowserHarness } from './browser.js';
|
|
28
28
|
import { isSensitiveEnvKey } from '../secrets/sensitivity.js';
|
|
29
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
30
|
const __dirname = path.dirname(__filename);
|
|
@@ -3171,6 +3171,8 @@ async function cmdUpdate(options) {
|
|
|
3171
3171
|
console.log(` ${DIM}Restart your daemon to pick up the new code:${RESET}`);
|
|
3172
3172
|
console.log(` clementine restart`);
|
|
3173
3173
|
}
|
|
3174
|
+
// Surface new opt-in integrations (silent unless action needed)
|
|
3175
|
+
await maybePromptBrowserHarness();
|
|
3174
3176
|
return;
|
|
3175
3177
|
}
|
|
3176
3178
|
let step = 0;
|
|
@@ -3830,6 +3832,8 @@ async function cmdUpdate(options) {
|
|
|
3830
3832
|
}
|
|
3831
3833
|
console.log(` ${DIM}Config backup: ${backupDir}${RESET}`);
|
|
3832
3834
|
console.log();
|
|
3835
|
+
// Surface new opt-in integrations (silent unless action needed)
|
|
3836
|
+
await maybePromptBrowserHarness();
|
|
3833
3837
|
}
|
|
3834
3838
|
// ── Cron commands ───────────────────────────────────────────────────
|
|
3835
3839
|
const cronCmd = program
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
* unless edited through this module, and edits preserve unrelated fields.
|
|
16
16
|
*/
|
|
17
17
|
import type { WorkflowDefinition, CronJobDefinition, BuilderWorkflowSummary, WorkflowOriginKind } from '../../types.js';
|
|
18
|
-
export
|
|
19
|
-
export declare function workflowId(filename: string): string;
|
|
20
|
-
export declare function parseBuilderId(id: string): {
|
|
18
|
+
export interface ParsedBuilderId {
|
|
21
19
|
origin: WorkflowOriginKind;
|
|
22
20
|
key: string;
|
|
23
|
-
|
|
21
|
+
scope: 'global' | 'agent';
|
|
22
|
+
agentSlug?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function cronId(name: string, agentSlug?: string): string;
|
|
25
|
+
export declare function workflowId(filename: string, agentSlug?: string): string;
|
|
26
|
+
export declare function parseBuilderId(id: string): ParsedBuilderId | null;
|
|
24
27
|
export declare function listAllForBuilder(): BuilderWorkflowSummary[];
|
|
25
28
|
export declare function readWorkflow(id: string): WorkflowDefinition | null;
|
|
26
29
|
export declare function cronJobToWorkflow(job: CronJobDefinition): WorkflowDefinition;
|
|
@@ -32,11 +35,8 @@ export declare function saveWorkflow(id: string, wf: WorkflowDefinition): {
|
|
|
32
35
|
ok: false;
|
|
33
36
|
error: string;
|
|
34
37
|
};
|
|
35
|
-
/** Resolve the on-disk file path for a builder id
|
|
36
|
-
export declare function sourceFileForId(id: string, parsedHint?:
|
|
37
|
-
origin: WorkflowOriginKind;
|
|
38
|
-
key: string;
|
|
39
|
-
}): string | null;
|
|
38
|
+
/** Resolve the on-disk file path for a builder id. */
|
|
39
|
+
export declare function sourceFileForId(id: string, parsedHint?: ParsedBuilderId): string | null;
|
|
40
40
|
/** Drawflow node shape (subset we use). */
|
|
41
41
|
interface DrawflowNode {
|
|
42
42
|
id: number;
|
|
@@ -18,30 +18,86 @@ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import matter from 'gray-matter';
|
|
20
20
|
import yaml from 'js-yaml';
|
|
21
|
-
import { CRON_FILE, WORKFLOWS_DIR } from '../../config.js';
|
|
21
|
+
import { AGENTS_DIR, CRON_FILE, WORKFLOWS_DIR } from '../../config.js';
|
|
22
22
|
import { snapshotWorkflow } from './snapshots.js';
|
|
23
23
|
// ── ID scheme ───────────────────────────────────────────────────────
|
|
24
24
|
const CRON_ID_PREFIX = 'cron:';
|
|
25
25
|
const WORKFLOW_ID_PREFIX = 'workflow:';
|
|
26
|
-
export function cronId(name) {
|
|
27
|
-
return
|
|
26
|
+
export function cronId(name, agentSlug) {
|
|
27
|
+
return agentSlug
|
|
28
|
+
? `${CRON_ID_PREFIX}agent:${agentSlug}:${name}`
|
|
29
|
+
: `${CRON_ID_PREFIX}global:${name}`;
|
|
28
30
|
}
|
|
29
|
-
export function workflowId(filename) {
|
|
31
|
+
export function workflowId(filename, agentSlug) {
|
|
30
32
|
const base = filename.endsWith('.md') ? filename.slice(0, -3) : filename;
|
|
31
|
-
return
|
|
33
|
+
return agentSlug
|
|
34
|
+
? `${WORKFLOW_ID_PREFIX}agent:${agentSlug}:${base}`
|
|
35
|
+
: `${WORKFLOW_ID_PREFIX}global:${base}`;
|
|
32
36
|
}
|
|
33
37
|
export function parseBuilderId(id) {
|
|
34
38
|
if (id.startsWith(CRON_ID_PREFIX))
|
|
35
|
-
return
|
|
39
|
+
return parseScopedTail('cron', id.slice(CRON_ID_PREFIX.length));
|
|
36
40
|
if (id.startsWith(WORKFLOW_ID_PREFIX))
|
|
37
|
-
return
|
|
41
|
+
return parseScopedTail('workflow', id.slice(WORKFLOW_ID_PREFIX.length));
|
|
38
42
|
return null;
|
|
39
43
|
}
|
|
44
|
+
function parseScopedTail(origin, tail) {
|
|
45
|
+
if (tail.startsWith('global:')) {
|
|
46
|
+
return { origin, scope: 'global', key: tail.slice('global:'.length) };
|
|
47
|
+
}
|
|
48
|
+
if (tail.startsWith('agent:')) {
|
|
49
|
+
const rest = tail.slice('agent:'.length);
|
|
50
|
+
const idx = rest.indexOf(':');
|
|
51
|
+
if (idx > 0) {
|
|
52
|
+
return {
|
|
53
|
+
origin,
|
|
54
|
+
scope: 'agent',
|
|
55
|
+
agentSlug: rest.slice(0, idx),
|
|
56
|
+
key: rest.slice(idx + 1),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Backwards-compatible legacy ids: cron:job-name / workflow:file-name.
|
|
61
|
+
return { origin, scope: 'global', key: tail };
|
|
62
|
+
}
|
|
63
|
+
function listAgentSlugs() {
|
|
64
|
+
if (!existsSync(AGENTS_DIR))
|
|
65
|
+
return [];
|
|
66
|
+
try {
|
|
67
|
+
const entries = readdirSync(AGENTS_DIR, { withFileTypes: true });
|
|
68
|
+
return entries
|
|
69
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('_'))
|
|
70
|
+
.map((d) => d.name)
|
|
71
|
+
.sort();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function cronFileForParsed(parsed) {
|
|
78
|
+
if (parsed.scope === 'agent' && parsed.agentSlug) {
|
|
79
|
+
return path.join(AGENTS_DIR, parsed.agentSlug, 'CRON.md');
|
|
80
|
+
}
|
|
81
|
+
return CRON_FILE;
|
|
82
|
+
}
|
|
83
|
+
function workflowsDirForParsed(parsed) {
|
|
84
|
+
if (parsed.scope === 'agent' && parsed.agentSlug) {
|
|
85
|
+
return path.join(AGENTS_DIR, parsed.agentSlug, 'workflows');
|
|
86
|
+
}
|
|
87
|
+
return WORKFLOWS_DIR;
|
|
88
|
+
}
|
|
89
|
+
function bareNameForScope(name, agentSlug) {
|
|
90
|
+
if (!agentSlug)
|
|
91
|
+
return name;
|
|
92
|
+
const prefix = `${agentSlug}:`;
|
|
93
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
94
|
+
}
|
|
40
95
|
// ── List ────────────────────────────────────────────────────────────
|
|
41
96
|
export function listAllForBuilder() {
|
|
42
97
|
const out = [];
|
|
43
|
-
//
|
|
44
|
-
|
|
98
|
+
// Global crons from CRON.md. A global entry may still run as an agent via
|
|
99
|
+
// agentSlug; its source remains global so saves write back to the right file.
|
|
100
|
+
for (const job of readCronJobs(CRON_FILE)) {
|
|
45
101
|
out.push({
|
|
46
102
|
id: cronId(job.name),
|
|
47
103
|
origin: 'cron',
|
|
@@ -52,9 +108,30 @@ export function listAllForBuilder() {
|
|
|
52
108
|
stepCount: 1,
|
|
53
109
|
sourceFile: CRON_FILE,
|
|
54
110
|
agentSlug: job.agentSlug,
|
|
111
|
+
scope: 'global',
|
|
55
112
|
});
|
|
56
113
|
}
|
|
57
|
-
//
|
|
114
|
+
// Agent crons from agents/<slug>/CRON.md.
|
|
115
|
+
for (const slug of listAgentSlugs()) {
|
|
116
|
+
const cronFile = path.join(AGENTS_DIR, slug, 'CRON.md');
|
|
117
|
+
if (!existsSync(cronFile))
|
|
118
|
+
continue;
|
|
119
|
+
for (const job of readCronJobs(cronFile, slug)) {
|
|
120
|
+
out.push({
|
|
121
|
+
id: cronId(job.name, slug),
|
|
122
|
+
origin: 'cron',
|
|
123
|
+
name: job.name,
|
|
124
|
+
description: '',
|
|
125
|
+
enabled: job.enabled,
|
|
126
|
+
schedule: job.schedule,
|
|
127
|
+
stepCount: 1,
|
|
128
|
+
sourceFile: cronFile,
|
|
129
|
+
agentSlug: slug,
|
|
130
|
+
scope: 'agent',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Global workflows from workflows dir.
|
|
58
135
|
if (existsSync(WORKFLOWS_DIR)) {
|
|
59
136
|
for (const file of readdirSync(WORKFLOWS_DIR).filter(f => f.endsWith('.md'))) {
|
|
60
137
|
try {
|
|
@@ -69,6 +146,33 @@ export function listAllForBuilder() {
|
|
|
69
146
|
stepCount: wf.steps.length,
|
|
70
147
|
sourceFile: wf.sourceFile,
|
|
71
148
|
agentSlug: wf.agentSlug,
|
|
149
|
+
scope: 'global',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Skip unparseable workflow files
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Agent workflows from agents/<slug>/workflows.
|
|
158
|
+
for (const slug of listAgentSlugs()) {
|
|
159
|
+
const wfDir = path.join(AGENTS_DIR, slug, 'workflows');
|
|
160
|
+
if (!existsSync(wfDir))
|
|
161
|
+
continue;
|
|
162
|
+
for (const file of readdirSync(wfDir).filter(f => f.endsWith('.md'))) {
|
|
163
|
+
try {
|
|
164
|
+
const wf = parseWorkflowFile(path.join(wfDir, file), slug);
|
|
165
|
+
out.push({
|
|
166
|
+
id: workflowId(file, slug),
|
|
167
|
+
origin: 'workflow',
|
|
168
|
+
name: wf.name,
|
|
169
|
+
description: wf.description,
|
|
170
|
+
enabled: wf.enabled,
|
|
171
|
+
schedule: wf.trigger.schedule,
|
|
172
|
+
stepCount: wf.steps.length,
|
|
173
|
+
sourceFile: wf.sourceFile,
|
|
174
|
+
agentSlug: slug,
|
|
175
|
+
scope: 'agent',
|
|
72
176
|
});
|
|
73
177
|
}
|
|
74
178
|
catch {
|
|
@@ -76,7 +180,11 @@ export function listAllForBuilder() {
|
|
|
76
180
|
}
|
|
77
181
|
}
|
|
78
182
|
}
|
|
79
|
-
return out.sort((a, b) =>
|
|
183
|
+
return out.sort((a, b) => {
|
|
184
|
+
const ownerA = a.agentSlug ?? '';
|
|
185
|
+
const ownerB = b.agentSlug ?? '';
|
|
186
|
+
return ownerA.localeCompare(ownerB) || a.name.localeCompare(b.name);
|
|
187
|
+
});
|
|
80
188
|
}
|
|
81
189
|
// ── Read ────────────────────────────────────────────────────────────
|
|
82
190
|
export function readWorkflow(id) {
|
|
@@ -84,25 +192,28 @@ export function readWorkflow(id) {
|
|
|
84
192
|
if (!parsed)
|
|
85
193
|
return null;
|
|
86
194
|
if (parsed.origin === 'cron') {
|
|
87
|
-
const
|
|
195
|
+
const cronFile = cronFileForParsed(parsed);
|
|
196
|
+
const job = readCronJobs(cronFile, parsed.agentSlug).find(j => j.name === parsed.key);
|
|
88
197
|
if (!job)
|
|
89
198
|
return null;
|
|
90
|
-
|
|
199
|
+
const wf = cronJobToWorkflow(job);
|
|
200
|
+
wf.sourceFile = cronFile;
|
|
201
|
+
return wf;
|
|
91
202
|
}
|
|
92
|
-
const file = path.join(
|
|
203
|
+
const file = path.join(workflowsDirForParsed(parsed), parsed.key + '.md');
|
|
93
204
|
if (!existsSync(file))
|
|
94
205
|
return null;
|
|
95
206
|
try {
|
|
96
|
-
return parseWorkflowFile(file);
|
|
207
|
+
return parseWorkflowFile(file, parsed.agentSlug);
|
|
97
208
|
}
|
|
98
209
|
catch {
|
|
99
210
|
return null;
|
|
100
211
|
}
|
|
101
212
|
}
|
|
102
|
-
function readCronJobs() {
|
|
103
|
-
if (!existsSync(
|
|
213
|
+
function readCronJobs(cronFile = CRON_FILE, agentSlug) {
|
|
214
|
+
if (!existsSync(cronFile))
|
|
104
215
|
return [];
|
|
105
|
-
const raw = readFileSync(
|
|
216
|
+
const raw = readFileSync(cronFile, 'utf-8');
|
|
106
217
|
let parsed;
|
|
107
218
|
try {
|
|
108
219
|
parsed = matter(raw);
|
|
@@ -137,16 +248,16 @@ function readCronJobs() {
|
|
|
137
248
|
alwaysDeliver: job.always_deliver === true ? true : undefined,
|
|
138
249
|
context: job.context != null ? String(job.context) : undefined,
|
|
139
250
|
preCheck: job.pre_check != null ? String(job.pre_check) : undefined,
|
|
140
|
-
agentSlug: typeof job.agentSlug === 'string'
|
|
251
|
+
agentSlug: agentSlug ?? (typeof job.agentSlug === 'string'
|
|
141
252
|
? job.agentSlug
|
|
142
253
|
: typeof job.agent_slug === 'string'
|
|
143
254
|
? job.agent_slug
|
|
144
|
-
: undefined,
|
|
255
|
+
: undefined),
|
|
145
256
|
});
|
|
146
257
|
}
|
|
147
258
|
return jobs;
|
|
148
259
|
}
|
|
149
|
-
function parseWorkflowFile(filePath) {
|
|
260
|
+
function parseWorkflowFile(filePath, fallbackAgentSlug) {
|
|
150
261
|
const raw = readFileSync(filePath, 'utf-8');
|
|
151
262
|
const parsed = matter(raw);
|
|
152
263
|
const data = parsed.data;
|
|
@@ -215,7 +326,7 @@ function parseWorkflowFile(filePath) {
|
|
|
215
326
|
steps,
|
|
216
327
|
synthesis,
|
|
217
328
|
sourceFile: filePath,
|
|
218
|
-
agentSlug: typeof data.agentSlug === 'string' ? data.agentSlug :
|
|
329
|
+
agentSlug: typeof data.agentSlug === 'string' ? data.agentSlug : fallbackAgentSlug,
|
|
219
330
|
};
|
|
220
331
|
}
|
|
221
332
|
// ── Cron ⇄ Workflow ─────────────────────────────────────────────────
|
|
@@ -265,23 +376,25 @@ export function saveWorkflow(id, wf) {
|
|
|
265
376
|
if (!isCronShape(wf)) {
|
|
266
377
|
return { ok: false, error: 'Cron entry must remain a single prompt step with a cron schedule' };
|
|
267
378
|
}
|
|
268
|
-
return
|
|
379
|
+
return saveCronEntryAt(cronFileForParsed(parsed), parsed.key, wf, parsed.agentSlug);
|
|
269
380
|
}
|
|
270
|
-
return
|
|
381
|
+
return saveWorkflowFileAt(workflowsDirForParsed(parsed), parsed.key, wf, parsed.agentSlug);
|
|
271
382
|
}
|
|
272
|
-
/** Resolve the on-disk file path for a builder id
|
|
383
|
+
/** Resolve the on-disk file path for a builder id. */
|
|
273
384
|
export function sourceFileForId(id, parsedHint) {
|
|
274
385
|
const parsed = parsedHint ?? parseBuilderId(id);
|
|
275
386
|
if (!parsed)
|
|
276
387
|
return null;
|
|
277
388
|
if (parsed.origin === 'cron')
|
|
278
|
-
return
|
|
279
|
-
return path.join(
|
|
389
|
+
return cronFileForParsed(parsed);
|
|
390
|
+
return path.join(workflowsDirForParsed(parsed), parsed.key + '.md');
|
|
280
391
|
}
|
|
281
|
-
function
|
|
282
|
-
if (!existsSync(
|
|
283
|
-
|
|
284
|
-
|
|
392
|
+
function saveCronEntryAt(cronFile, originalName, wf, agentSlug) {
|
|
393
|
+
if (!existsSync(cronFile)) {
|
|
394
|
+
mkdirSync(path.dirname(cronFile), { recursive: true });
|
|
395
|
+
writeFileSync(cronFile, matter.stringify('', { jobs: [] }), 'utf-8');
|
|
396
|
+
}
|
|
397
|
+
const raw = readFileSync(cronFile, 'utf-8');
|
|
285
398
|
let parsed;
|
|
286
399
|
try {
|
|
287
400
|
parsed = matter(raw);
|
|
@@ -297,7 +410,7 @@ function saveCronEntry(originalName, wf) {
|
|
|
297
410
|
const prev = jobs[idx];
|
|
298
411
|
const updated = {
|
|
299
412
|
...prev,
|
|
300
|
-
name: wf.name,
|
|
413
|
+
name: bareNameForScope(wf.name, agentSlug),
|
|
301
414
|
schedule: wf.trigger.schedule,
|
|
302
415
|
prompt: step.prompt,
|
|
303
416
|
enabled: wf.enabled,
|
|
@@ -309,18 +422,27 @@ function saveCronEntry(originalName, wf) {
|
|
|
309
422
|
updated.model = step.model;
|
|
310
423
|
if (step.workDir != null)
|
|
311
424
|
updated.work_dir = step.workDir;
|
|
312
|
-
if (
|
|
425
|
+
if (agentSlug) {
|
|
426
|
+
delete updated.agentSlug;
|
|
427
|
+
delete updated.agent_slug;
|
|
428
|
+
}
|
|
429
|
+
else if (wf.agentSlug) {
|
|
313
430
|
updated.agentSlug = wf.agentSlug;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
delete updated.agentSlug;
|
|
434
|
+
delete updated.agent_slug;
|
|
435
|
+
}
|
|
314
436
|
jobs[idx] = updated;
|
|
315
437
|
parsed.data.jobs = jobs;
|
|
316
438
|
const out = matter.stringify(parsed.content ?? '', parsed.data);
|
|
317
|
-
writeFileSync(
|
|
439
|
+
writeFileSync(cronFile, out, 'utf-8');
|
|
318
440
|
return { ok: true };
|
|
319
441
|
}
|
|
320
|
-
function
|
|
321
|
-
if (!existsSync(
|
|
322
|
-
mkdirSync(
|
|
323
|
-
const file = path.join(
|
|
442
|
+
function saveWorkflowFileAt(workflowsDir, key, wf, agentSlug) {
|
|
443
|
+
if (!existsSync(workflowsDir))
|
|
444
|
+
mkdirSync(workflowsDir, { recursive: true });
|
|
445
|
+
const file = path.join(workflowsDir, key + '.md');
|
|
324
446
|
// Preserve body content if the file exists; otherwise empty body.
|
|
325
447
|
let body = '';
|
|
326
448
|
if (existsSync(file)) {
|
|
@@ -339,8 +461,8 @@ function saveWorkflowFile(key, wf) {
|
|
|
339
461
|
enabled: wf.enabled,
|
|
340
462
|
trigger: wf.trigger,
|
|
341
463
|
};
|
|
342
|
-
if (wf.agentSlug)
|
|
343
|
-
data.agentSlug = wf.agentSlug;
|
|
464
|
+
if (agentSlug || wf.agentSlug)
|
|
465
|
+
data.agentSlug = agentSlug ?? wf.agentSlug;
|
|
344
466
|
if (Object.keys(wf.inputs).length > 0)
|
|
345
467
|
data.inputs = wf.inputs;
|
|
346
468
|
data.steps = wf.steps.map(serializeStep);
|
|
@@ -34,7 +34,7 @@ const stepShape = z.object({
|
|
|
34
34
|
});
|
|
35
35
|
export function registerBuilderTools(server) {
|
|
36
36
|
// ── Discovery ──────────────────────────────────────────────────────────
|
|
37
|
-
server.tool('workflow_list', 'List all workflows and crons visible in the Builder. Returns one per line: id|name|origin|enabled|schedule|stepCount.', {
|
|
37
|
+
server.tool('workflow_list', 'List all workflows and crons visible in the Builder. Returns one per line: id|owner|name|origin|enabled|schedule|stepCount.', {
|
|
38
38
|
enabledOnly: z.boolean().optional().describe('If true, return only enabled workflows'),
|
|
39
39
|
verbose: z.boolean().optional(),
|
|
40
40
|
}, async ({ enabledOnly, verbose }) => {
|
|
@@ -43,7 +43,7 @@ export function registerBuilderTools(server) {
|
|
|
43
43
|
return textResult(JSON.stringify(items, null, 2));
|
|
44
44
|
if (items.length === 0)
|
|
45
45
|
return textResult('(no workflows or crons found)');
|
|
46
|
-
return textResult(items.map(i => `${i.id}|${i.name}|${i.origin}|${i.enabled ? 'on' : 'off'}|${i.schedule ?? '-'}|${i.stepCount}step${i.stepCount === 1 ? '' : 's'}`).join('\n'));
|
|
46
|
+
return textResult(items.map(i => `${i.id}|${i.agentSlug ?? 'global'}|${i.name}|${i.origin}|${i.enabled ? 'on' : 'off'}|${i.schedule ?? '-'}|${i.stepCount}step${i.stepCount === 1 ? '' : 's'}`).join('\n'));
|
|
47
47
|
});
|
|
48
48
|
server.tool('workflow_read', 'Read a workflow as canonical JSON. Use this before editing — patches reference current step ids.', {
|
|
49
49
|
id: z.string().describe('Builder id (e.g., cron:morning-briefing or workflow:daily-digest)'),
|
package/dist/types.d.ts
CHANGED