clementine-agent 1.18.43 → 1.18.45
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/agent/assistant.d.ts +7 -0
- package/dist/agent/assistant.js +9 -0
- package/dist/agent/run-agent.d.ts +11 -0
- package/dist/agent/run-agent.js +63 -1
- package/dist/cli/dashboard.js +2 -1
- package/dist/gateway/router.js +73 -0
- package/dist/tools/admin-tools.js +8 -141
- package/package.json +1 -1
|
@@ -277,6 +277,13 @@ export declare class PersonalAssistant {
|
|
|
277
277
|
private memoryExtractionKey;
|
|
278
278
|
private assessMemoryExtraction;
|
|
279
279
|
private logMemoryExtractionSkip;
|
|
280
|
+
/**
|
|
281
|
+
* Public entry point for triggering auto-memory extraction after an
|
|
282
|
+
* exchange. Used by the new runAgent chat path (Phase 2 migration)
|
|
283
|
+
* so it can keep the existing memory-extraction behavior without
|
|
284
|
+
* having to recreate the surrounding plumbing.
|
|
285
|
+
*/
|
|
286
|
+
triggerMemoryExtractionPostExchange(userMessage: string, assistantResponse: string, sessionKey?: string, profile?: AgentProfile): Promise<void>;
|
|
280
287
|
private spawnMemoryExtraction;
|
|
281
288
|
private static readonly MEMORY_TOOL_NAMES;
|
|
282
289
|
private extractMemory;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -4883,6 +4883,15 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4883
4883
|
}
|
|
4884
4884
|
catch { /* telemetry only */ }
|
|
4885
4885
|
}
|
|
4886
|
+
/**
|
|
4887
|
+
* Public entry point for triggering auto-memory extraction after an
|
|
4888
|
+
* exchange. Used by the new runAgent chat path (Phase 2 migration)
|
|
4889
|
+
* so it can keep the existing memory-extraction behavior without
|
|
4890
|
+
* having to recreate the surrounding plumbing.
|
|
4891
|
+
*/
|
|
4892
|
+
async triggerMemoryExtractionPostExchange(userMessage, assistantResponse, sessionKey, profile) {
|
|
4893
|
+
return this.spawnMemoryExtraction(userMessage, assistantResponse, sessionKey, profile);
|
|
4894
|
+
}
|
|
4886
4895
|
async spawnMemoryExtraction(userMessage, assistantResponse, sessionKey, profile) {
|
|
4887
4896
|
// Guard: skip memory extraction if the user message looks like injection
|
|
4888
4897
|
const memScan = scanner.scan(userMessage);
|
|
@@ -66,6 +66,17 @@ export interface RunAgentOptions {
|
|
|
66
66
|
allowedTools?: string[];
|
|
67
67
|
/** Optional CLAUDE.md / project setting source. Defaults to ['project']. */
|
|
68
68
|
settingSources?: ('project' | 'user' | 'local')[];
|
|
69
|
+
/** Additional MCP servers to merge with the always-on clementine-tools
|
|
70
|
+
* server. Use to wire Composio + claude.ai integrations on chat-path
|
|
71
|
+
* invocations that need Outlook/Salesforce/etc. */
|
|
72
|
+
extraMcpServers?: Record<string, {
|
|
73
|
+
type: 'stdio' | 'http' | 'sse';
|
|
74
|
+
command?: string;
|
|
75
|
+
args?: string[];
|
|
76
|
+
env?: Record<string, string>;
|
|
77
|
+
url?: string;
|
|
78
|
+
headers?: Record<string, string>;
|
|
79
|
+
}>;
|
|
69
80
|
}
|
|
70
81
|
export interface RunAgentResult {
|
|
71
82
|
/** Final text response from the agent. */
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -21,10 +21,46 @@
|
|
|
21
21
|
* 5. NO context-thrash recovery, NO manual session rotation, NO
|
|
22
22
|
* long-task preflight, NO mode=unleashed wrapper.
|
|
23
23
|
*/
|
|
24
|
+
import path from 'node:path';
|
|
24
25
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
25
26
|
import pino from 'pino';
|
|
26
|
-
import { BASE_DIR, normalizeClaudeSdkOptionsForOneMillionContext } from '../config.js';
|
|
27
|
+
import { BASE_DIR, PKG_DIR, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
27
28
|
import { buildAgentMap } from './agent-definitions.js';
|
|
29
|
+
const MCP_SERVER_SCRIPT = path.join(PKG_DIR, 'dist', 'tools', 'mcp-server.js');
|
|
30
|
+
const ASSISTANT_NAME = (process.env.ASSISTANT_NAME ?? 'Clementine').toLowerCase();
|
|
31
|
+
const TOOLS_SERVER = `${ASSISTANT_NAME}-tools`;
|
|
32
|
+
/**
|
|
33
|
+
* Build a minimal env for the SDK subprocess. Mirrors the existing
|
|
34
|
+
* SAFE_ENV pattern in assistant.ts but exposed here so runAgent can be
|
|
35
|
+
* its own thing without depending on the legacy assistant module.
|
|
36
|
+
*
|
|
37
|
+
* Priority: CLAUDE_CODE_OAUTH_TOKEN > ANTHROPIC_AUTH_TOKEN > ANTHROPIC_API_KEY.
|
|
38
|
+
* When all are absent, HOME lets the subprocess find Keychain OAuth.
|
|
39
|
+
*/
|
|
40
|
+
function buildRunAgentEnv() {
|
|
41
|
+
const env = {
|
|
42
|
+
PATH: process.env.PATH ?? '',
|
|
43
|
+
HOME: process.env.HOME ?? '',
|
|
44
|
+
LANG: process.env.LANG ?? 'en_US.UTF-8',
|
|
45
|
+
TERM: process.env.TERM ?? 'xterm-256color',
|
|
46
|
+
USER: process.env.USER ?? '',
|
|
47
|
+
SHELL: process.env.SHELL ?? '',
|
|
48
|
+
CLEMENTINE_HOME: BASE_DIR,
|
|
49
|
+
};
|
|
50
|
+
const oauthTok = CLAUDE_CODE_OAUTH_TOKEN || process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
51
|
+
const authTok = process.env.ANTHROPIC_AUTH_TOKEN;
|
|
52
|
+
const apiKey = CONFIG_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
53
|
+
if (oauthTok) {
|
|
54
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = oauthTok;
|
|
55
|
+
}
|
|
56
|
+
else if (authTok) {
|
|
57
|
+
env.ANTHROPIC_AUTH_TOKEN = authTok;
|
|
58
|
+
}
|
|
59
|
+
else if (apiKey) {
|
|
60
|
+
env.ANTHROPIC_API_KEY = apiKey;
|
|
61
|
+
}
|
|
62
|
+
return env;
|
|
63
|
+
}
|
|
28
64
|
const logger = pino({ name: 'clementine.run-agent' });
|
|
29
65
|
const DEFAULT_BUDGETS = {
|
|
30
66
|
chat: 0.50,
|
|
@@ -83,6 +119,28 @@ export async function runAgent(prompt, opts) {
|
|
|
83
119
|
// Allowed tools. Default to core + Clementine MCP. Per-subagent tool
|
|
84
120
|
// restrictions live on each AgentDefinition.tools field.
|
|
85
121
|
const allowedTools = opts.allowedTools ?? CORE_TOOLS_FOR_AGENT_PARENT;
|
|
122
|
+
// Wire the Clementine MCP server so the agent can reach memory/cron/
|
|
123
|
+
// broken-job tools. Without this, the cron-fixer subagent's `tools`
|
|
124
|
+
// list references mcp__clementine-tools__* that don't exist in the
|
|
125
|
+
// session, and the agent falls back to reading raw JSON files.
|
|
126
|
+
const subprocessEnv = buildRunAgentEnv();
|
|
127
|
+
// SDK accepts a Record<string, McpServerConfig> here. We cast on
|
|
128
|
+
// assignment because we mix the always-on Clementine stdio server
|
|
129
|
+
// with caller-supplied servers of various types.
|
|
130
|
+
const mcpServers = {
|
|
131
|
+
[TOOLS_SERVER]: {
|
|
132
|
+
type: 'stdio',
|
|
133
|
+
command: 'node',
|
|
134
|
+
args: [MCP_SERVER_SCRIPT],
|
|
135
|
+
env: {
|
|
136
|
+
...subprocessEnv,
|
|
137
|
+
CLEMENTINE_HOME: BASE_DIR,
|
|
138
|
+
...(opts.profile?.slug ? { CLEMENTINE_TEAM_AGENT: opts.profile.slug } : {}),
|
|
139
|
+
CLEMENTINE_INTERACTION_SOURCE: source === 'cron' || source === 'heartbeat' ? 'autonomous' : 'interactive',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
...(opts.extraMcpServers ?? {}),
|
|
143
|
+
};
|
|
86
144
|
// Apply 1M-context env normalization (existing infra)
|
|
87
145
|
const sdkOptionsRaw = {
|
|
88
146
|
systemPrompt: profileAppend
|
|
@@ -90,9 +148,13 @@ export async function runAgent(prompt, opts) {
|
|
|
90
148
|
: { type: 'preset', preset: 'claude_code' },
|
|
91
149
|
settingSources: opts.settingSources ?? ['project'],
|
|
92
150
|
agents,
|
|
151
|
+
// SDK's McpServerConfig is a union; cast at the boundary since
|
|
152
|
+
// callers can mix stdio + http + sse server shapes.
|
|
153
|
+
mcpServers: mcpServers,
|
|
93
154
|
allowedTools,
|
|
94
155
|
permissionMode: 'bypassPermissions',
|
|
95
156
|
cwd: BASE_DIR,
|
|
157
|
+
env: subprocessEnv,
|
|
96
158
|
maxBudgetUsd,
|
|
97
159
|
effort,
|
|
98
160
|
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -2347,7 +2347,8 @@ export async function cmdDashboard(opts) {
|
|
|
2347
2347
|
const isLongRunning = req.path.startsWith('/brain/')
|
|
2348
2348
|
|| req.path.endsWith('/stream')
|
|
2349
2349
|
|| req.path === '/chat'
|
|
2350
|
-
|| req.path === '/builder/chat'
|
|
2350
|
+
|| req.path === '/builder/chat'
|
|
2351
|
+
|| req.path === '/runagent/test';
|
|
2351
2352
|
const timeoutMs = isLongRunning ? 10 * 60 * 1000 : 8000;
|
|
2352
2353
|
const timeout = setTimeout(() => {
|
|
2353
2354
|
if (!res.headersSent) {
|
package/dist/gateway/router.js
CHANGED
|
@@ -2088,6 +2088,79 @@ export class Gateway {
|
|
|
2088
2088
|
delete sessState.pendingInterrupt;
|
|
2089
2089
|
}
|
|
2090
2090
|
try {
|
|
2091
|
+
// ── Phase 2: opt-in canonical SDK chat path ──────────────────
|
|
2092
|
+
// When CLEMENTINE_USE_RUNAGENT_CHAT=1 is set, route through
|
|
2093
|
+
// the new runAgent() wrapper instead of the legacy
|
|
2094
|
+
// assistant.chat path. This is the SDK-canonical pattern
|
|
2095
|
+
// (one query() call, agents map for subagents, no
|
|
2096
|
+
// wrapper layers). Today's Phase 2 connects only the
|
|
2097
|
+
// Clementine MCP server — Composio/external integrations
|
|
2098
|
+
// come in Phase 3. Useful for testing the new path on
|
|
2099
|
+
// tool-light sessions like cron-fix or memory queries.
|
|
2100
|
+
//
|
|
2101
|
+
// The legacy path (default) keeps full Composio/external
|
|
2102
|
+
// routing + all post-response handlers, so this flag is
|
|
2103
|
+
// safe to leave off until we're ready.
|
|
2104
|
+
if (process.env.CLEMENTINE_USE_RUNAGENT_CHAT === '1'
|
|
2105
|
+
&& this.isTrustedPersonalSession(sessionKey)
|
|
2106
|
+
&& !sessState.pendingInterrupt) {
|
|
2107
|
+
const { runAgent } = await import('../agent/run-agent.js');
|
|
2108
|
+
logger.info({
|
|
2109
|
+
sessionKey: effectiveSessionKey,
|
|
2110
|
+
profile: resolvedProfile?.slug,
|
|
2111
|
+
path: 'runagent_chat',
|
|
2112
|
+
}, 'Phase 2: routing chat through runAgent');
|
|
2113
|
+
try {
|
|
2114
|
+
const runAgentResult = await runAgent(originalText, {
|
|
2115
|
+
sessionKey: effectiveSessionKey,
|
|
2116
|
+
source: 'chat',
|
|
2117
|
+
profile: resolvedProfile,
|
|
2118
|
+
agentManager: this.getAgentManager(),
|
|
2119
|
+
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
2120
|
+
onText: wrappedOnText,
|
|
2121
|
+
onToolActivity: ({ tool, input }) => {
|
|
2122
|
+
toolActivityCount++;
|
|
2123
|
+
if (wrappedOnToolActivity) {
|
|
2124
|
+
return wrappedOnToolActivity(tool, input);
|
|
2125
|
+
}
|
|
2126
|
+
return undefined;
|
|
2127
|
+
},
|
|
2128
|
+
abortSignal: chatAc.signal,
|
|
2129
|
+
});
|
|
2130
|
+
clearTimeout(chatTimer);
|
|
2131
|
+
clearTimeout(hardWallTimer);
|
|
2132
|
+
// Mirror transcript so memory + recall continue working.
|
|
2133
|
+
const memoryStore = this.assistant.getMemoryStore?.();
|
|
2134
|
+
if (memoryStore) {
|
|
2135
|
+
try {
|
|
2136
|
+
memoryStore.saveTurn(effectiveSessionKey, 'user', originalText);
|
|
2137
|
+
memoryStore.saveTurn(effectiveSessionKey, 'assistant', runAgentResult.text);
|
|
2138
|
+
}
|
|
2139
|
+
catch (err) {
|
|
2140
|
+
logger.debug({ err }, 'runAgent chat: transcript mirror failed (non-fatal)');
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
// Fire auto-memory extraction in the background so
|
|
2144
|
+
// MEMORY.md continues to update like the legacy path.
|
|
2145
|
+
this.assistant
|
|
2146
|
+
.triggerMemoryExtractionPostExchange(originalText, runAgentResult.text, effectiveSessionKey, resolvedProfile)
|
|
2147
|
+
.catch(err => logger.debug({ err, sessionKey: effectiveSessionKey }, 'runAgent chat: auto-memory failed (non-fatal)'));
|
|
2148
|
+
logger.info({
|
|
2149
|
+
sessionKey: effectiveSessionKey,
|
|
2150
|
+
totalMs: Date.now() - tInnerStart,
|
|
2151
|
+
routedVia: 'runagent_chat',
|
|
2152
|
+
numTurns: runAgentResult.numTurns,
|
|
2153
|
+
cost: Number(runAgentResult.totalCostUsd.toFixed(4)),
|
|
2154
|
+
responseLen: runAgentResult.text.length,
|
|
2155
|
+
}, 'chat:latency');
|
|
2156
|
+
return runAgentResult.text;
|
|
2157
|
+
}
|
|
2158
|
+
catch (err) {
|
|
2159
|
+
logger.warn({ err, sessionKey: effectiveSessionKey }, 'runAgent chat path failed — falling back to legacy chat');
|
|
2160
|
+
// Fall through to the legacy chat path so the user
|
|
2161
|
+
// still gets a response.
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2091
2164
|
// ── Pre-LLM plan routing (Gap #3 from orchestration audit) ──
|
|
2092
2165
|
// When the user's text clearly maps to multi-step parallel
|
|
2093
2166
|
// work, route through the orchestrator BEFORE the main agent
|
|
@@ -1170,147 +1170,14 @@ export function registerAdminTools(server) {
|
|
|
1170
1170
|
return textResult(`Triggered "${job_name}" — the daemon will pick it up within a few seconds and run it in the background. ` +
|
|
1171
1171
|
`Results will be delivered via notifications when complete.`);
|
|
1172
1172
|
});
|
|
1173
|
-
// ── Workflow Tools
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
if (workflows.length === 0) {
|
|
1182
|
-
return textResult('No workflow files found in `vault/00-System/workflows/`.');
|
|
1183
|
-
}
|
|
1184
|
-
const lines = [];
|
|
1185
|
-
for (const wf of workflows) {
|
|
1186
|
-
const status = wf.enabled ? 'enabled' : 'disabled';
|
|
1187
|
-
const trigger = wf.trigger.schedule ? `schedule: \`${wf.trigger.schedule}\`` : 'manual only';
|
|
1188
|
-
lines.push(`**${wf.name}** [${status}]` +
|
|
1189
|
-
`\n ${wf.description || '(no description)'}` +
|
|
1190
|
-
`\n Trigger: ${trigger}` +
|
|
1191
|
-
`\n Steps (${wf.steps.length}): ${wf.steps.map(s => s.id).join(' → ')}` +
|
|
1192
|
-
(Object.keys(wf.inputs).length > 0
|
|
1193
|
-
? `\n Inputs: ${Object.entries(wf.inputs).map(([k, v]) => `${k}${v.default ? `="${v.default}"` : ''}`).join(', ')}`
|
|
1194
|
-
: ''));
|
|
1195
|
-
}
|
|
1196
|
-
return textResult(lines.join('\n\n'));
|
|
1197
|
-
});
|
|
1198
|
-
server.tool('workflow_create', 'Create a new multi-step workflow file. Validates dependencies and writes to vault/00-System/workflows/. The daemon auto-reloads on file change.', {
|
|
1199
|
-
name: z.string().describe('Workflow name (used as filename and identifier)'),
|
|
1200
|
-
description: z.string().describe('What the workflow does'),
|
|
1201
|
-
steps: z.array(z.object({
|
|
1202
|
-
id: z.string().describe('Unique step identifier'),
|
|
1203
|
-
prompt: z.string().describe('Prompt for the step (supports {{input.*}}, {{steps.*.output}}, {{date}} variables)'),
|
|
1204
|
-
dependsOn: z.array(z.string()).default([]).describe('Step IDs this depends on'),
|
|
1205
|
-
model: z.string().optional().describe('Model tier: haiku or sonnet'),
|
|
1206
|
-
tier: z.number().optional().default(1).describe('Security tier (1-3)'),
|
|
1207
|
-
maxTurns: z.number().optional().default(15).describe('Max agent turns'),
|
|
1208
|
-
})).describe('Workflow steps'),
|
|
1209
|
-
trigger_schedule: z.string().optional().describe('Cron expression for scheduled trigger'),
|
|
1210
|
-
inputs: z.record(z.string(), z.object({
|
|
1211
|
-
type: z.enum(['string', 'number']).default('string'),
|
|
1212
|
-
default: z.string().optional(),
|
|
1213
|
-
description: z.string().optional(),
|
|
1214
|
-
})).optional().default({}).describe('Input parameters with optional defaults'),
|
|
1215
|
-
synthesis_prompt: z.string().optional().describe('Prompt to synthesize final output from all step results'),
|
|
1216
|
-
}, async ({ name, description, steps, trigger_schedule, inputs, synthesis_prompt }) => {
|
|
1217
|
-
// Validate step IDs are unique
|
|
1218
|
-
const ids = new Set(steps.map(s => s.id));
|
|
1219
|
-
if (ids.size !== steps.length) {
|
|
1220
|
-
return textResult('Error: Duplicate step IDs found.');
|
|
1221
|
-
}
|
|
1222
|
-
// Validate dependencies exist
|
|
1223
|
-
for (const step of steps) {
|
|
1224
|
-
for (const dep of step.dependsOn) {
|
|
1225
|
-
if (!ids.has(dep)) {
|
|
1226
|
-
return textResult(`Error: Step "${step.id}" depends on unknown step "${dep}".`);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
// Validate cron expression if provided
|
|
1231
|
-
if (trigger_schedule) {
|
|
1232
|
-
const cronMod = await import('node-cron');
|
|
1233
|
-
if (!cronMod.default.validate(trigger_schedule)) {
|
|
1234
|
-
return textResult(`Invalid cron expression: "${trigger_schedule}".`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
// Build frontmatter
|
|
1238
|
-
const frontmatter = {
|
|
1239
|
-
type: 'workflow',
|
|
1240
|
-
name,
|
|
1241
|
-
description,
|
|
1242
|
-
enabled: true,
|
|
1243
|
-
trigger: {
|
|
1244
|
-
...(trigger_schedule ? { schedule: trigger_schedule } : {}),
|
|
1245
|
-
manual: true,
|
|
1246
|
-
},
|
|
1247
|
-
};
|
|
1248
|
-
if (Object.keys(inputs).length > 0) {
|
|
1249
|
-
frontmatter.inputs = inputs;
|
|
1250
|
-
}
|
|
1251
|
-
frontmatter.steps = steps.map(s => ({
|
|
1252
|
-
id: s.id,
|
|
1253
|
-
prompt: s.prompt,
|
|
1254
|
-
dependsOn: s.dependsOn,
|
|
1255
|
-
...(s.model ? { model: s.model } : {}),
|
|
1256
|
-
...(s.tier && s.tier !== 1 ? { tier: s.tier } : {}),
|
|
1257
|
-
...(s.maxTurns && s.maxTurns !== 15 ? { maxTurns: s.maxTurns } : {}),
|
|
1258
|
-
}));
|
|
1259
|
-
if (synthesis_prompt) {
|
|
1260
|
-
frontmatter.synthesis = { prompt: synthesis_prompt };
|
|
1261
|
-
}
|
|
1262
|
-
// Write file
|
|
1263
|
-
if (!existsSync(WORKFLOWS_DIR)) {
|
|
1264
|
-
mkdirSync(WORKFLOWS_DIR, { recursive: true });
|
|
1265
|
-
}
|
|
1266
|
-
const matterMod = await import('gray-matter');
|
|
1267
|
-
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
1268
|
-
const filePath = path.join(WORKFLOWS_DIR, `${safeName}.md`);
|
|
1269
|
-
if (existsSync(filePath)) {
|
|
1270
|
-
return textResult(`Workflow file already exists: ${safeName}.md. Delete or rename it first.`);
|
|
1271
|
-
}
|
|
1272
|
-
const body = `# ${name}\n\n${description}\n`;
|
|
1273
|
-
const output = matterMod.default.stringify(body, frontmatter);
|
|
1274
|
-
writeFileSync(filePath, output);
|
|
1275
|
-
logger.info({ name, steps: steps.length }, 'Created workflow via MCP tool');
|
|
1276
|
-
const goalHint = `\n\n💡 **Goal tracking:** What goal does this workflow serve? Consider creating a persistent goal (\`goal_create\`) and linking related cron jobs so self-improvement can optimize this workflow against measurable outcomes.`;
|
|
1277
|
-
return textResult(`Created workflow "${name}" with ${steps.length} steps.\n` +
|
|
1278
|
-
`File: vault/00-System/workflows/${safeName}.md\n` +
|
|
1279
|
-
`Steps: ${steps.map(s => s.id).join(' → ')}\n` +
|
|
1280
|
-
(trigger_schedule ? `Schedule: ${trigger_schedule}\n` : 'Trigger: manual\n') +
|
|
1281
|
-
'The daemon will auto-detect it via file watcher.' +
|
|
1282
|
-
goalHint);
|
|
1283
|
-
});
|
|
1284
|
-
server.tool('workflow_run', 'Trigger a workflow by name with optional input overrides. Returns the workflow result.', {
|
|
1285
|
-
name: z.string().describe('Workflow name'),
|
|
1286
|
-
inputs: z.record(z.string(), z.string()).optional().default({}).describe('Input overrides (key=value pairs)'),
|
|
1287
|
-
}, async ({ name: workflowName, inputs }) => {
|
|
1288
|
-
const { parseAllWorkflows } = await import('../agent/workflow-runner.js');
|
|
1289
|
-
const { WorkflowRunner } = await import('../agent/workflow-runner.js');
|
|
1290
|
-
const workflows = parseAllWorkflows(WORKFLOWS_DIR);
|
|
1291
|
-
const wf = workflows.find(w => w.name === workflowName);
|
|
1292
|
-
if (!wf) {
|
|
1293
|
-
const available = workflows.map(w => w.name).join(', ');
|
|
1294
|
-
return textResult(`Workflow "${workflowName}" not found. Available: ${available || 'none'}`);
|
|
1295
|
-
}
|
|
1296
|
-
if (!wf.enabled) {
|
|
1297
|
-
return textResult(`Workflow "${workflowName}" is disabled.`);
|
|
1298
|
-
}
|
|
1299
|
-
// Build a minimal assistant for standalone MCP execution
|
|
1300
|
-
// In daemon mode, the CronScheduler.runWorkflow() path is preferred
|
|
1301
|
-
// For MCP standalone, we need to create an assistant instance
|
|
1302
|
-
try {
|
|
1303
|
-
const { PersonalAssistant } = await import('../agent/assistant.js');
|
|
1304
|
-
const assistant = new PersonalAssistant();
|
|
1305
|
-
const runner = new WorkflowRunner(assistant);
|
|
1306
|
-
const result = await runner.run(wf, inputs);
|
|
1307
|
-
return textResult(`**Workflow: ${workflowName}** — ${result.status}\n\n${result.output.slice(0, 3000)}`);
|
|
1308
|
-
}
|
|
1309
|
-
catch (err) {
|
|
1310
|
-
logger.error({ err, workflow: workflowName }, 'Workflow execution failed');
|
|
1311
|
-
return textResult(`Workflow "${workflowName}" failed: ${err instanceof Error ? err.message : err}`);
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1173
|
+
// ── Workflow Tools moved to builder-tools.ts ────────────────────────────
|
|
1174
|
+
//
|
|
1175
|
+
// `workflow_list`, `workflow_create`, and `workflow_run` were duplicated
|
|
1176
|
+
// here AND in builder-tools.ts (the newer Trick Builder). The duplicate
|
|
1177
|
+
// registration was crashing the MCP server on startup with
|
|
1178
|
+
// "Tool X is already registered" — silently breaking every fresh MCP
|
|
1179
|
+
// subprocess and forcing fallback to manual file reads.
|
|
1180
|
+
// All three live in builder-tools.ts now.
|
|
1314
1181
|
// ── Analyze Image ───────────────────────────────────────────────────────
|
|
1315
1182
|
server.tool('analyze_image', 'Analyze an image by URL. Fetches the image, converts to base64, and uses Claude vision to describe it. Works with any image URL — channel attachments, email attachments, web images.', {
|
|
1316
1183
|
url: z.string().describe('URL of the image to analyze'),
|