@artflo-ai/artflo-openclaw-plugin 0.0.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.
Files changed (59) hide show
  1. package/README.md +102 -0
  2. package/dist/index.js +73 -0
  3. package/dist/logs/ws-traffic-traces/2026-03-27T20-30-00-216Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  4. package/dist/logs/ws-traffic-traces/2026-03-27T20-30-00-217Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  5. package/dist/logs/ws-traffic-traces/2026-03-27T20-30-05-727Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  6. package/dist/logs/ws-traffic-traces/2026-03-27T20-30-30-218Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  7. package/dist/logs/ws-traffic-traces/2026-03-27T20-30-30-218Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  8. package/dist/logs/ws-traffic-traces/2026-03-27T20-30-35-728Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  9. package/dist/logs/ws-traffic-traces/2026-03-27T20-31-00-218Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  10. package/dist/logs/ws-traffic-traces/2026-03-27T20-31-00-219Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  11. package/dist/logs/ws-traffic-traces/2026-03-27T20-31-05-729Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  12. package/dist/logs/ws-traffic-traces/2026-03-27T20-31-30-220Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  13. package/dist/logs/ws-traffic-traces/2026-03-27T20-31-30-220Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  14. package/dist/logs/ws-traffic-traces/2026-03-27T20-31-35-729Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  15. package/dist/logs/ws-traffic-traces/2026-03-27T20-32-00-221Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  16. package/dist/logs/ws-traffic-traces/2026-03-27T20-32-00-221Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  17. package/dist/logs/ws-traffic-traces/2026-03-27T20-32-05-730Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  18. package/dist/logs/ws-traffic-traces/2026-03-27T20-32-30-222Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  19. package/dist/logs/ws-traffic-traces/2026-03-27T20-32-30-222Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  20. package/dist/logs/ws-traffic-traces/2026-03-27T20-32-35-731Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  21. package/dist/logs/ws-traffic-traces/2026-03-27T20-33-00-223Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  22. package/dist/logs/ws-traffic-traces/2026-03-27T20-33-00-223Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  23. package/dist/logs/ws-traffic-traces/2026-03-27T20-33-05-732Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  24. package/dist/logs/ws-traffic-traces/2026-03-27T20-33-30-223Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  25. package/dist/logs/ws-traffic-traces/2026-03-27T20-33-30-223Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  26. package/dist/logs/ws-traffic-traces/2026-03-27T20-33-35-734Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
  27. package/dist/logs/ws-traffic-traces/2026-03-27T20-34-00-228Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
  28. package/dist/logs/ws-traffic-traces/2026-03-27T20-34-00-229Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
  29. package/dist/src/config.js +57 -0
  30. package/dist/src/constants.js +35 -0
  31. package/dist/src/core/api/api-base.js +12 -0
  32. package/dist/src/core/api/upload-file.js +59 -0
  33. package/dist/src/core/canvas/canvas-session-manager.js +189 -0
  34. package/dist/src/core/canvas/canvas-websocket-client.js +453 -0
  35. package/dist/src/core/canvas/create-canvas.js +37 -0
  36. package/dist/src/core/canvas/types.js +23 -0
  37. package/dist/src/core/canvas/ws-trace.js +42 -0
  38. package/dist/src/core/config/fetch-client-params.js +20 -0
  39. package/dist/src/core/config/fetch-vip-info.js +30 -0
  40. package/dist/src/core/config/model-config-transformer.js +104 -0
  41. package/dist/src/core/executor/element-builders.js +216 -0
  42. package/dist/src/core/executor/execute-plan.js +1221 -0
  43. package/dist/src/core/executor/execution-trace.js +34 -0
  44. package/dist/src/core/layout/layout-service.js +366 -0
  45. package/dist/src/core/plan/analyze-plan-groups.js +71 -0
  46. package/dist/src/core/plan/types.js +1 -0
  47. package/dist/src/core/plan/validate-plan.js +159 -0
  48. package/dist/src/paths.js +16 -0
  49. package/dist/src/services/canvas-session-registry.js +57 -0
  50. package/dist/src/tools/register-tools.js +669 -0
  51. package/dist/src/tools/tool-trace.js +19 -0
  52. package/openclaw.plugin.json +33 -0
  53. package/package.json +42 -0
  54. package/skills/artflo-canvas/SKILL.md +118 -0
  55. package/skills/artflo-canvas/references/graph-rules.md +53 -0
  56. package/skills/artflo-canvas/references/layout-notes.md +31 -0
  57. package/skills/artflo-canvas/references/node-schema.json +948 -0
  58. package/skills/artflo-canvas/references/node-schema.md +188 -0
  59. package/skills/artflo-canvas/references/planning-guide.md +321 -0
@@ -0,0 +1,669 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ /**
3
+ * Format a tool result payload for the openclaw agent runtime.
4
+ *
5
+ * This is a local implementation identical to `jsonResult` from
6
+ * `openclaw/plugin-sdk`. We define it here because the openclaw plugin
7
+ * loader may use a CJS interop layer that cannot resolve the named ESM
8
+ * export at runtime (error: `_pluginSdk.jsonResult is not a function`).
9
+ */
10
+ function jsonResult(payload) {
11
+ return {
12
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
13
+ details: payload,
14
+ };
15
+ }
16
+ import { ARTFLO_TOOL_NAMES } from '../constants.js';
17
+ import { createCanvas } from '../core/canvas/create-canvas.js';
18
+ import { uploadFile } from '../core/api/upload-file.js';
19
+ import { fetchVipInfo } from '../core/config/fetch-vip-info.js';
20
+ import { transformCanvasConfig } from '../core/config/model-config-transformer.js';
21
+ import { executePlan as runExecutePlan } from '../core/executor/execute-plan.js';
22
+ import { ExecutionTraceWriter } from '../core/executor/execution-trace.js';
23
+ import { parsePlanV2, validateAndFixPlan } from '../core/plan/validate-plan.js';
24
+ import { writeToolTrace } from './tool-trace.js';
25
+ export function registerArtfloTools(api, context) {
26
+ const registerOptionalTool = (tool) => {
27
+ api.registerTool(tool, { optional: true });
28
+ };
29
+ registerOptionalTool({
30
+ name: ARTFLO_TOOL_NAMES.getNode,
31
+ label: 'Artflo Canvas Get Node',
32
+ description: 'Read one node or edge from the current Artflo canvas session by id.',
33
+ parameters: Type.Object({
34
+ canvasId: Type.String({ minLength: 1 }),
35
+ id: Type.String({ minLength: 1 }),
36
+ }),
37
+ async execute(_id, params) {
38
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.getNode, params, async () => {
39
+ const { canvasId, id } = params;
40
+ const session = context.sessions.getOrCreate(canvasId);
41
+ await session.connect({ canvasId });
42
+ const element = session.getElement(id) ?? null;
43
+ return jsonResult({
44
+ ok: true,
45
+ label: ARTFLO_TOOL_NAMES.getNode,
46
+ data: {
47
+ canvasId,
48
+ element,
49
+ },
50
+ });
51
+ });
52
+ },
53
+ });
54
+ registerOptionalTool({
55
+ name: ARTFLO_TOOL_NAMES.connect,
56
+ label: 'Artflo Canvas Connect',
57
+ description: 'Register or warm a direct Artflo canvas WebSocket session for a canvas id using preconfigured plugin credentials.',
58
+ parameters: Type.Object({
59
+ canvasId: Type.String({ minLength: 1 }),
60
+ }),
61
+ async execute(_id, params) {
62
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.connect, params, async () => {
63
+ const canvasId = params.canvasId;
64
+ const session = context.sessions.getOrCreate(canvasId);
65
+ const elements = await session.connect({
66
+ canvasId,
67
+ });
68
+ return jsonResult({
69
+ ok: true,
70
+ label: ARTFLO_TOOL_NAMES.connect,
71
+ data: {
72
+ session: session.getConnectionState(),
73
+ elementCount: elements.length,
74
+ configuredApiBaseUrl: context.config.apiBaseUrl,
75
+ hasApiKey: context.config.apiKey.length > 0,
76
+ },
77
+ });
78
+ });
79
+ },
80
+ });
81
+ registerOptionalTool({
82
+ name: ARTFLO_TOOL_NAMES.connectionStatus,
83
+ label: 'Artflo Canvas Connection Status',
84
+ description: 'Inspect current plugin-level Artflo WebSocket session state and config readiness.',
85
+ parameters: Type.Object({
86
+ canvasId: Type.Optional(Type.String()),
87
+ }),
88
+ async execute(_id, params) {
89
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.connectionStatus, params, async () => {
90
+ const canvasId = params.canvasId;
91
+ return jsonResult({
92
+ ok: true,
93
+ label: ARTFLO_TOOL_NAMES.connectionStatus,
94
+ data: canvasId
95
+ ? {
96
+ session: context.sessions.get(canvasId)?.getConnectionState() ?? null,
97
+ status: context.sessions.getStatus(),
98
+ }
99
+ : {
100
+ sessions: context.sessions
101
+ .getAll()
102
+ .map((session) => session.getConnectionState()),
103
+ status: context.sessions.getStatus(),
104
+ },
105
+ });
106
+ });
107
+ },
108
+ });
109
+ registerOptionalTool({
110
+ name: ARTFLO_TOOL_NAMES.getState,
111
+ label: 'Artflo Canvas State',
112
+ description: 'Return scaffold plugin state and planned runtime capabilities for Artflo execution.',
113
+ parameters: Type.Object({
114
+ canvasId: Type.Optional(Type.String()),
115
+ }),
116
+ async execute(_id, params) {
117
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.getState, params, async () => {
118
+ const canvasId = params.canvasId;
119
+ const session = canvasId ? context.sessions.get(canvasId) : null;
120
+ const elements = session?.getElements() ?? [];
121
+ return jsonResult({
122
+ ok: true,
123
+ label: ARTFLO_TOOL_NAMES.getState,
124
+ data: {
125
+ stage: 'canvas-runtime-connected',
126
+ capabilities: [
127
+ 'direct_websocket_runtime_ready',
128
+ 'layout_engine_planned',
129
+ 'plan_validation_planned',
130
+ 'execute_plan_planned',
131
+ ],
132
+ status: context.sessions.getStatus(),
133
+ canvasId: canvasId ?? null,
134
+ elementCount: elements.length,
135
+ elementsPreview: elements.slice(0, 10).map((element) => ({
136
+ id: element.id,
137
+ type: element.type,
138
+ position: element.position ?? null,
139
+ })),
140
+ },
141
+ });
142
+ });
143
+ },
144
+ });
145
+ registerOptionalTool({
146
+ name: ARTFLO_TOOL_NAMES.getConfig,
147
+ label: 'Artflo Canvas Get Config',
148
+ description: 'Return available models, subscription status, and prompt refine categories for the connected canvas. Call this before planning a workflow so you know which models and resolutions are available.',
149
+ parameters: Type.Object({
150
+ canvasId: Type.String({ minLength: 1 }),
151
+ }),
152
+ async execute(_id, params) {
153
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.getConfig, params, async () => {
154
+ const canvasId = params.canvasId;
155
+ const session = context.sessions.getOrCreate(canvasId);
156
+ await session.connect({ canvasId });
157
+ const canvasConfig = session.getCanvasConfig();
158
+ const vipInfo = await fetchVipInfo(context.config);
159
+ const llmConfig = canvasConfig
160
+ ? transformCanvasConfig(canvasConfig, vipInfo.isVip)
161
+ : null;
162
+ return jsonResult({
163
+ ok: true,
164
+ label: ARTFLO_TOOL_NAMES.getConfig,
165
+ data: {
166
+ canvasId,
167
+ subscription: {
168
+ isVip: vipInfo.isVip,
169
+ membershipLevel: vipInfo.membershipLevel,
170
+ membershipName: vipInfo.membershipName,
171
+ },
172
+ models: llmConfig?.categories ?? [],
173
+ outputTools: llmConfig?.outputTools ?? [],
174
+ promptRefineCategories: llmConfig?.promptRefineCategories ?? [],
175
+ hasCanvasConfig: canvasConfig !== null,
176
+ },
177
+ });
178
+ });
179
+ },
180
+ });
181
+ registerOptionalTool({
182
+ name: ARTFLO_TOOL_NAMES.findNodes,
183
+ label: 'Artflo Canvas Find Nodes',
184
+ description: 'Find nodes or edges in the connected Artflo canvas by ids, type, status, label text, or runnable state.',
185
+ parameters: Type.Object({
186
+ canvasId: Type.String({ minLength: 1 }),
187
+ ids: Type.Optional(Type.Array(Type.String())),
188
+ type: Type.Optional(Type.Number()),
189
+ labelIncludes: Type.Optional(Type.String()),
190
+ status: Type.Optional(Type.Number()),
191
+ runnable: Type.Optional(Type.Boolean()),
192
+ }),
193
+ async execute(_id, params) {
194
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.findNodes, params, async () => {
195
+ const payload = params;
196
+ const session = context.sessions.getOrCreate(payload.canvasId);
197
+ await session.connect({ canvasId: payload.canvasId });
198
+ const matched = session.getElements().filter((element) => {
199
+ if (payload.ids?.length && !payload.ids.includes(element.id)) {
200
+ return false;
201
+ }
202
+ if (typeof payload.type === 'number' && element.type !== payload.type) {
203
+ return false;
204
+ }
205
+ if (typeof payload.status === 'number') {
206
+ const currentStatus = typeof element.data.status === 'number'
207
+ ? Number(element.data.status)
208
+ : undefined;
209
+ if (currentStatus !== payload.status) {
210
+ return false;
211
+ }
212
+ }
213
+ if (typeof payload.runnable === 'boolean') {
214
+ const runnable = isRunnableElement(element);
215
+ if (runnable !== payload.runnable) {
216
+ return false;
217
+ }
218
+ }
219
+ if (payload.labelIncludes) {
220
+ const label = readElementLabel(element).toLowerCase();
221
+ if (!label.includes(payload.labelIncludes.toLowerCase())) {
222
+ return false;
223
+ }
224
+ }
225
+ return true;
226
+ });
227
+ return jsonResult({
228
+ ok: true,
229
+ label: ARTFLO_TOOL_NAMES.findNodes,
230
+ data: {
231
+ canvasId: payload.canvasId,
232
+ count: matched.length,
233
+ elements: matched,
234
+ },
235
+ });
236
+ });
237
+ },
238
+ });
239
+ registerOptionalTool({
240
+ name: ARTFLO_TOOL_NAMES.changeElements,
241
+ label: 'Artflo Canvas Change Elements',
242
+ description: 'Apply direct structured element changes to an existing Artflo canvas session.',
243
+ parameters: Type.Object({
244
+ canvasId: Type.String({ minLength: 1 }),
245
+ changes: Type.Array(Type.Any(), { minItems: 1 }),
246
+ }),
247
+ async execute(_id, params) {
248
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.changeElements, params, async () => {
249
+ const payload = params;
250
+ const session = context.sessions.getOrCreate(payload.canvasId);
251
+ await session.connect({ canvasId: payload.canvasId });
252
+ await session.changeElements(payload.changes);
253
+ return jsonResult({
254
+ ok: true,
255
+ label: ARTFLO_TOOL_NAMES.changeElements,
256
+ data: {
257
+ canvasId: payload.canvasId,
258
+ changedIds: payload.changes
259
+ .map((change) => change.id)
260
+ .filter((id) => typeof id === 'string'),
261
+ },
262
+ });
263
+ });
264
+ },
265
+ });
266
+ registerOptionalTool({
267
+ name: ARTFLO_TOOL_NAMES.deleteElements,
268
+ label: 'Artflo Canvas Delete Elements',
269
+ description: 'Delete nodes or edges from the current Artflo canvas session.',
270
+ parameters: Type.Object({
271
+ canvasId: Type.String({ minLength: 1 }),
272
+ ids: Type.Array(Type.String(), { minItems: 1 }),
273
+ }),
274
+ async execute(_id, params) {
275
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.deleteElements, params, async () => {
276
+ const payload = params;
277
+ const session = context.sessions.getOrCreate(payload.canvasId);
278
+ await session.connect({ canvasId: payload.canvasId });
279
+ await session.deleteElements(payload.ids);
280
+ return jsonResult({
281
+ ok: true,
282
+ label: ARTFLO_TOOL_NAMES.deleteElements,
283
+ data: {
284
+ canvasId: payload.canvasId,
285
+ deletedIds: payload.ids,
286
+ },
287
+ });
288
+ });
289
+ },
290
+ });
291
+ registerOptionalTool({
292
+ name: ARTFLO_TOOL_NAMES.runNodes,
293
+ label: 'Artflo Canvas Run Nodes',
294
+ description: 'Run one or more executable Artflo nodes by id.',
295
+ parameters: Type.Object({
296
+ canvasId: Type.String({ minLength: 1 }),
297
+ ids: Type.Array(Type.String(), { minItems: 1 }),
298
+ }),
299
+ async execute(_id, params) {
300
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.runNodes, params, async () => {
301
+ const payload = params;
302
+ const session = context.sessions.getOrCreate(payload.canvasId);
303
+ await session.connect({ canvasId: payload.canvasId });
304
+ await session.executeNodes(payload.ids);
305
+ return jsonResult({
306
+ ok: true,
307
+ label: ARTFLO_TOOL_NAMES.runNodes,
308
+ data: {
309
+ canvasId: payload.canvasId,
310
+ executedNodeIds: payload.ids,
311
+ },
312
+ });
313
+ });
314
+ },
315
+ });
316
+ registerOptionalTool({
317
+ name: ARTFLO_TOOL_NAMES.waitForCompletion,
318
+ label: 'Artflo Canvas Wait For Completion',
319
+ description: 'Wait for one or more executable nodes to reach completion status.',
320
+ parameters: Type.Object({
321
+ canvasId: Type.String({ minLength: 1 }),
322
+ ids: Type.Array(Type.String(), { minItems: 1 }),
323
+ timeoutMs: Type.Optional(Type.Number()),
324
+ }),
325
+ async execute(_id, params) {
326
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.waitForCompletion, params, async () => {
327
+ const payload = params;
328
+ const session = context.sessions.getOrCreate(payload.canvasId);
329
+ await session.connect({ canvasId: payload.canvasId });
330
+ const completed = await session.waitForCompletion(payload.ids, payload.timeoutMs ?? 300_000);
331
+ return jsonResult({
332
+ ok: true,
333
+ label: ARTFLO_TOOL_NAMES.waitForCompletion,
334
+ data: {
335
+ canvasId: payload.canvasId,
336
+ ids: payload.ids,
337
+ completed,
338
+ },
339
+ });
340
+ });
341
+ },
342
+ });
343
+ registerOptionalTool({
344
+ name: ARTFLO_TOOL_NAMES.executePlan,
345
+ label: 'Artflo Execute Plan',
346
+ description: 'Execute a structured Artflo plan through deterministic runtime logic. In scaffold phase, validates request shape and returns the next integration milestone.',
347
+ parameters: Type.Object({
348
+ canvasId: Type.String({ minLength: 1 }),
349
+ plan: Type.Object({
350
+ nodes: Type.Array(Type.Any()),
351
+ edges: Type.Array(Type.Any()),
352
+ description: Type.Optional(Type.String()),
353
+ planId: Type.Optional(Type.String()),
354
+ version: Type.Optional(Type.String()),
355
+ }),
356
+ selectedNodeIds: Type.Optional(Type.Array(Type.String())),
357
+ draft: Type.Optional(Type.Boolean()),
358
+ }),
359
+ async execute(_id, params) {
360
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.executePlan, params, async () => {
361
+ const payload = params;
362
+ const session = context.sessions.getOrCreate(payload.canvasId);
363
+ await session.connect({
364
+ canvasId: payload.canvasId,
365
+ });
366
+ const parsedPlan = parsePlanV2(payload.plan);
367
+ const validated = validateAndFixPlan(parsedPlan);
368
+ const traceWriter = await ExecutionTraceWriter.create({
369
+ planId: validated.plan.planId,
370
+ canvasId: payload.canvasId,
371
+ });
372
+ const execution = payload.draft === true
373
+ ? null
374
+ : await runExecutePlan({
375
+ plan: validated.plan,
376
+ session,
377
+ traceWriter,
378
+ });
379
+ if (payload.draft === true) {
380
+ await traceWriter.writeMeta('draft-only', {
381
+ canvasId: payload.canvasId,
382
+ fixes: validated.fixes,
383
+ selectedNodeIds: payload.selectedNodeIds ?? [],
384
+ draft: true,
385
+ plan: validated.plan,
386
+ });
387
+ }
388
+ return jsonResult({
389
+ ok: true,
390
+ label: ARTFLO_TOOL_NAMES.executePlan,
391
+ data: {
392
+ accepted: true,
393
+ stage: 'canvas-runtime-connected',
394
+ canvasId: payload.canvasId,
395
+ session: session.getConnectionState(),
396
+ nodeCount: validated.plan.nodes.length,
397
+ edgeCount: validated.plan.edges.length,
398
+ selectedNodeIds: payload.selectedNodeIds ?? [],
399
+ draft: payload.draft ?? false,
400
+ planId: validated.plan.planId,
401
+ description: validated.plan.description,
402
+ fixes: validated.fixes,
403
+ execution,
404
+ traceDirectory: execution?.traceDirectory ?? traceWriter.directory,
405
+ ok: execution ? execution.executionCompleted !== false : true,
406
+ nextStep: 'Next: add smarter handles, executable node runs, and advanced batch/layout orchestration.',
407
+ },
408
+ });
409
+ });
410
+ },
411
+ });
412
+ api.registerTool({
413
+ name: ARTFLO_TOOL_NAMES.createCanvas,
414
+ label: 'Artflo Canvas Create',
415
+ description: 'Create a new Artflo canvas project. Returns the canvas id and project URL. Use this when no canvas id is available or the user requests a new canvas.',
416
+ parameters: Type.Object({
417
+ name: Type.Optional(Type.String({ description: 'Canvas name, defaults to "Untitled"' })),
418
+ }),
419
+ async execute(_id, params) {
420
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.createCanvas, params, async () => {
421
+ const name = params.name || 'Untitled';
422
+ const result = await createCanvas(context.config, name);
423
+ return jsonResult({
424
+ ok: true,
425
+ label: ARTFLO_TOOL_NAMES.createCanvas,
426
+ data: {
427
+ canvasId: result.id,
428
+ name: result.name,
429
+ url: result.url,
430
+ },
431
+ });
432
+ });
433
+ },
434
+ });
435
+ api.registerTool({
436
+ name: ARTFLO_TOOL_NAMES.listModels,
437
+ label: 'Artflo Canvas List Models',
438
+ description: 'List all available AI models with their names, subscription requirements, default status, and credit costs. Video model credits are per-second. Use this to help users understand what models are available before planning a workflow.',
439
+ parameters: Type.Object({
440
+ canvasId: Type.String({ minLength: 1 }),
441
+ category: Type.Optional(Type.String({
442
+ description: 'Filter by category: "text2image", "image2image", "text2video", "image2video", "image23d". Omit to list all.',
443
+ })),
444
+ }),
445
+ async execute(_id, params) {
446
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.listModels, params, async () => {
447
+ const payload = params;
448
+ const session = context.sessions.getOrCreate(payload.canvasId);
449
+ await session.connect({ canvasId: payload.canvasId });
450
+ const canvasConfig = session.getCanvasConfig();
451
+ const vipInfo = await fetchVipInfo(context.config);
452
+ if (!canvasConfig) {
453
+ return jsonResult({
454
+ ok: false,
455
+ label: ARTFLO_TOOL_NAMES.listModels,
456
+ error: 'Canvas config not available',
457
+ });
458
+ }
459
+ const VIDEO_TOOL_TYPES = new Set([3, 4]);
460
+ const CATEGORY_MAP = {
461
+ text2image: { key: 'text_to_image_model', toolType: 1, label: 'Text to Image' },
462
+ image2image: { key: 'image_to_image_model', toolType: 2, label: 'Image to Image' },
463
+ text2video: { key: 'text_to_video_model', toolType: 3, label: 'Text to Video' },
464
+ image2video: { key: 'image_to_video_model', toolType: 4, label: 'Image to Video' },
465
+ image23d: { key: 'image_to_3d_model', toolType: 8, label: 'Image to 3D' },
466
+ };
467
+ const categoriesToList = payload.category
468
+ ? { [payload.category.toLowerCase()]: CATEGORY_MAP[payload.category.toLowerCase()] }
469
+ : CATEGORY_MAP;
470
+ const result = [];
471
+ for (const [, info] of Object.entries(categoriesToList)) {
472
+ if (!info)
473
+ continue;
474
+ const models = canvasConfig[info.key];
475
+ if (!Array.isArray(models) || models.length === 0)
476
+ continue;
477
+ const isVideo = VIDEO_TOOL_TYPES.has(info.toolType);
478
+ const mapped = models.map((m) => {
479
+ const modelAvailable = !m.disable &&
480
+ (!m.subscribe_available || vipInfo.isVip || m.is_free_limit);
481
+ // Build per-resolution pricing
482
+ const resolutionPricing = m.multi_config_price && Object.keys(m.multi_config_price).length > 0
483
+ ? Object.fromEntries(Object.entries(m.multi_config_price).map(([key, price]) => {
484
+ const label = key.replace('resolution_', '').toUpperCase();
485
+ const needsSub = m.multi_subscribe?.[key] === true;
486
+ return [
487
+ label,
488
+ {
489
+ credits: isVideo ? `${price} credits/sec` : `${price} credits`,
490
+ ...(needsSub ? { requiresSubscription: true } : {}),
491
+ },
492
+ ];
493
+ }))
494
+ : undefined;
495
+ const item = {
496
+ name: m.model_name,
497
+ modelKey: m.model_key,
498
+ isDefault: m.is_default,
499
+ requiresSubscription: m.subscribe_available,
500
+ available: modelAvailable,
501
+ credits: isVideo
502
+ ? `${m.price} credits/sec`
503
+ : `${m.price} credits`,
504
+ };
505
+ if (m.support_resolution?.length) {
506
+ item.supportedResolutions = m.support_resolution;
507
+ if (m.default_resolution)
508
+ item.defaultResolution = m.default_resolution;
509
+ }
510
+ if (m.support_ratio?.length)
511
+ item.supportedRatios = m.support_ratio;
512
+ if (m.support_duration?.length)
513
+ item.supportedDurations = m.support_duration;
514
+ if (resolutionPricing)
515
+ item.resolutionPricing = resolutionPricing;
516
+ return item;
517
+ });
518
+ result.push({ category: info.label, models: mapped });
519
+ }
520
+ return jsonResult({
521
+ ok: true,
522
+ label: ARTFLO_TOOL_NAMES.listModels,
523
+ data: {
524
+ subscription: {
525
+ isVip: vipInfo.isVip,
526
+ membershipLevel: vipInfo.membershipLevel,
527
+ pricingUrl: 'https://artflo.ai/pricing',
528
+ },
529
+ hint: vipInfo.isVip
530
+ ? undefined
531
+ : 'User is not subscribed. Models or resolutions marked requiresSubscription need a subscription. Visit https://artflo.ai/pricing to subscribe, or use free models.',
532
+ categories: result,
533
+ },
534
+ });
535
+ });
536
+ },
537
+ });
538
+ api.registerTool({
539
+ name: ARTFLO_TOOL_NAMES.uploadFile,
540
+ label: 'Artflo Upload File',
541
+ description: 'Upload a local file or a remote URL to Artflo OBS storage. Returns the CDN URL and object key. Use this to upload reference images before creating workflows with image-to-image or image-to-video nodes.',
542
+ parameters: Type.Object({
543
+ source: Type.String({
544
+ description: 'Local file path or remote https URL to upload.',
545
+ }),
546
+ filename: Type.Optional(Type.String({ description: 'Override filename for the upload.' })),
547
+ }),
548
+ async execute(_id, params) {
549
+ return runWithToolTrace(ARTFLO_TOOL_NAMES.uploadFile, params, async () => {
550
+ const payload = params;
551
+ const result = await uploadFile(context.config, payload.source, payload.filename);
552
+ return jsonResult({
553
+ ok: true,
554
+ label: ARTFLO_TOOL_NAMES.uploadFile,
555
+ data: {
556
+ url: result.url,
557
+ key: result.key,
558
+ },
559
+ });
560
+ });
561
+ },
562
+ });
563
+ api.registerTool({
564
+ name: ARTFLO_TOOL_NAMES.setApiKey,
565
+ label: 'Artflo Set API Key',
566
+ description: 'Save the user\'s Artflo API Key to the plugin config and restart the gateway. Call this when the user provides their API Key in chat. After success, the gateway will restart and all Artflo tools will be functional.',
567
+ parameters: Type.Object({
568
+ apiKey: Type.String({ minLength: 1, description: 'The Artflo API Key provided by the user.' }),
569
+ }),
570
+ async execute(_id, params) {
571
+ const { apiKey } = params;
572
+ try {
573
+ const cfg = api.runtime.config.loadConfig();
574
+ const pluginEntry = cfg.plugins?.entries?.['artflo-openclaw-plugin'] ?? {};
575
+ const existingConfig = pluginEntry.config ?? {};
576
+ const nextConfig = {
577
+ ...cfg,
578
+ plugins: {
579
+ ...cfg.plugins,
580
+ entries: {
581
+ ...cfg.plugins?.entries,
582
+ 'artflo-openclaw-plugin': {
583
+ ...pluginEntry,
584
+ enabled: true,
585
+ config: {
586
+ ...existingConfig,
587
+ apiKey,
588
+ },
589
+ },
590
+ },
591
+ },
592
+ };
593
+ await api.runtime.config.writeConfigFile(nextConfig);
594
+ return jsonResult({
595
+ ok: true,
596
+ label: ARTFLO_TOOL_NAMES.setApiKey,
597
+ data: {
598
+ message: 'API Key saved. Please restart gateway with: openclaw gateway restart',
599
+ saved: true,
600
+ },
601
+ });
602
+ }
603
+ catch (err) {
604
+ return jsonResult({
605
+ ok: false,
606
+ label: ARTFLO_TOOL_NAMES.setApiKey,
607
+ error: err instanceof Error ? err.message : String(err),
608
+ });
609
+ }
610
+ },
611
+ });
612
+ }
613
+ async function runWithToolTrace(toolName, params, run) {
614
+ const canvasId = typeof params === 'object' &&
615
+ params !== null &&
616
+ 'canvasId' in params &&
617
+ typeof params.canvasId === 'string'
618
+ ? params.canvasId
619
+ : undefined;
620
+ await writeToolTrace({
621
+ toolName,
622
+ stage: 'start',
623
+ payload: { params },
624
+ canvasId,
625
+ });
626
+ try {
627
+ const result = await run();
628
+ await writeToolTrace({
629
+ toolName,
630
+ stage: 'success',
631
+ payload: { params, result },
632
+ canvasId,
633
+ });
634
+ return result;
635
+ }
636
+ catch (error) {
637
+ await writeToolTrace({
638
+ toolName,
639
+ stage: 'error',
640
+ payload: {
641
+ params,
642
+ error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error),
643
+ },
644
+ canvasId,
645
+ });
646
+ throw error;
647
+ }
648
+ }
649
+ function isRunnableElement(element) {
650
+ return (element.type === 130000 ||
651
+ element.type === 130500 ||
652
+ element.type === 190000);
653
+ }
654
+ function readElementLabel(element) {
655
+ const data = element.data;
656
+ if (typeof data.label === 'string') {
657
+ return data.label;
658
+ }
659
+ if (typeof data.prompt === 'string') {
660
+ return data.prompt;
661
+ }
662
+ if (typeof data.current_model === 'string') {
663
+ return data.current_model;
664
+ }
665
+ if (typeof data.model === 'string') {
666
+ return data.model;
667
+ }
668
+ return '';
669
+ }
@@ -0,0 +1,19 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { resolvePluginLogDir, localTimestamp } from '../paths.js';
4
+ function sanitizeSegment(value) {
5
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 80) || 'trace';
6
+ }
7
+ export async function writeToolTrace(args) {
8
+ const timestamp = localTimestamp();
9
+ const baseDir = resolvePluginLogDir('tool-call-traces');
10
+ await mkdir(baseDir, { recursive: true });
11
+ const filePath = path.join(baseDir, `${timestamp}-${sanitizeSegment(args.toolName)}${args.canvasId ? `-${sanitizeSegment(args.canvasId)}` : ''}-${args.stage}.json`);
12
+ await writeFile(filePath, JSON.stringify({
13
+ toolName: args.toolName,
14
+ stage: args.stage,
15
+ createdAt: new Date().toISOString(),
16
+ payload: args.payload,
17
+ }, null, 2), 'utf8');
18
+ return filePath;
19
+ }