forge-openclaw-plugin 0.2.24 → 0.2.25

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 (82) hide show
  1. package/README.md +13 -0
  2. package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
  3. package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
  4. package/dist/assets/index-CFCKDIMH.js +67 -0
  5. package/dist/assets/index-CFCKDIMH.js.map +1 -0
  6. package/dist/assets/index-ZPY6U1TU.css +1 -0
  7. package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
  8. package/dist/assets/motion-DvkU14p-.js.map +1 -0
  9. package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
  10. package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
  11. package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
  12. package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
  13. package/dist/assets/vendor-D9PTEPSB.js +824 -0
  14. package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
  15. package/dist/assets/viz-Cqb6s--o.js +34 -0
  16. package/dist/assets/viz-Cqb6s--o.js.map +1 -0
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/parity.d.ts +1 -1
  19. package/dist/openclaw/parity.js +29 -0
  20. package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
  21. package/dist/openclaw/plugin-entry-shared.js +7 -4
  22. package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
  23. package/dist/openclaw/routes.js +236 -0
  24. package/dist/openclaw/session-bootstrap.d.ts +78 -0
  25. package/dist/openclaw/session-bootstrap.js +240 -0
  26. package/dist/openclaw/tools.js +279 -3
  27. package/dist/server/app.js +855 -19
  28. package/dist/server/connectors/box-registry.js +257 -0
  29. package/dist/server/db.js +2 -0
  30. package/dist/server/discovery-advertiser.js +114 -0
  31. package/dist/server/health.js +39 -11
  32. package/dist/server/index.js +4 -0
  33. package/dist/server/managers/platform/llm-manager.js +40 -4
  34. package/dist/server/managers/platform/openai-responses-provider.js +129 -19
  35. package/dist/server/movement.js +2935 -0
  36. package/dist/server/openapi.js +628 -5
  37. package/dist/server/psyche-types.js +15 -1
  38. package/dist/server/questionnaire-flow.js +552 -0
  39. package/dist/server/questionnaire-seeds.js +853 -0
  40. package/dist/server/questionnaire-types.js +340 -0
  41. package/dist/server/repositories/ai-connectors.js +944 -0
  42. package/dist/server/repositories/ai-processors.js +547 -0
  43. package/dist/server/repositories/entity-ownership.js +9 -1
  44. package/dist/server/repositories/habits.js +69 -5
  45. package/dist/server/repositories/model-settings.js +216 -0
  46. package/dist/server/repositories/notes.js +57 -15
  47. package/dist/server/repositories/preferences.js +124 -0
  48. package/dist/server/repositories/questionnaires.js +1338 -0
  49. package/dist/server/repositories/settings.js +108 -12
  50. package/dist/server/repositories/surface-layouts.js +76 -0
  51. package/dist/server/repositories/wiki-memory.js +5 -1
  52. package/dist/server/services/entity-crud.js +81 -2
  53. package/dist/server/services/openai-codex-oauth.js +153 -0
  54. package/dist/server/services/psyche-observation-calendar.js +46 -0
  55. package/dist/server/types.js +492 -3
  56. package/dist/server/watch-mobile.js +562 -0
  57. package/dist/server/web.js +9 -2
  58. package/openclaw.plugin.json +1 -1
  59. package/package.json +6 -1
  60. package/server/migrations/024_questionnaires.sql +96 -0
  61. package/server/migrations/025_ai_model_connections.sql +26 -0
  62. package/server/migrations/026_custom_theme_settings.sql +2 -0
  63. package/server/migrations/027_ai_processors.sql +31 -0
  64. package/server/migrations/028_movement_domain.sql +136 -0
  65. package/server/migrations/029_watch_micro_capture.sql +23 -0
  66. package/server/migrations/030_surface_layouts.sql +5 -0
  67. package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  68. package/server/migrations/032_ai_connectors.sql +44 -0
  69. package/server/migrations/033_movement_trip_point_sync.sql +36 -0
  70. package/server/migrations/034_movement_segment_sync.sql +49 -0
  71. package/skills/forge-openclaw/SKILL.md +12 -1
  72. package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
  73. package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
  74. package/dist/assets/index-DTCwBWAs.js +0 -65
  75. package/dist/assets/index-DTCwBWAs.js.map +0 -1
  76. package/dist/assets/index-DttXlAgi.css +0 -1
  77. package/dist/assets/motion-D4sZgCHd.js.map +0 -1
  78. package/dist/assets/vendor-De38P6YR.js +0 -729
  79. package/dist/assets/vendor-De38P6YR.js.map +0 -1
  80. package/dist/assets/viz-C6hfyqzu.js +0 -34
  81. package/dist/assets/viz-C6hfyqzu.js.map +0 -1
  82. package/skills/forge-openclaw/cron_jobs.md +0 -395
@@ -0,0 +1,944 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { getDatabase } from "../db.js";
7
+ import { createAiConnectorSchema, aiConnectorConversationSchema, aiConnectorRunResultSchema, aiConnectorRunSchema, aiConnectorSchema, runAiConnectorSchema, updateAiConnectorSchema } from "../types.js";
8
+ import { FORGE_DEFAULT_AGENT_ID, getAiModelConnectionById, listAiModelConnections, readModelConnectionCredential } from "./model-settings.js";
9
+ import { getAiProcessorById, listAiProcessorLinks, listAiProcessors } from "./ai-processors.js";
10
+ import { buildConnectorOutputCatalogEntry, executeForgeBoxTool, resolveForgeBoxSnapshot } from "../connectors/box-registry.js";
11
+ const execFile = promisify(execFileCallback);
12
+ const MAX_TOOL_STEPS = 6;
13
+ const MAX_RUN_HISTORY = 20;
14
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
15
+ function parseJson(value, fallback) {
16
+ try {
17
+ return value ? JSON.parse(value) : fallback;
18
+ }
19
+ catch {
20
+ return fallback;
21
+ }
22
+ }
23
+ function slugifySegment(value) {
24
+ const normalized = value
25
+ .trim()
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, "-")
28
+ .replace(/^-+|-+$/g, "");
29
+ return normalized || "connector";
30
+ }
31
+ function buildConnectorSlug(title, id) {
32
+ return `${slugifySegment(title)}-${id.slice(-6)}`;
33
+ }
34
+ function normalizeBaseUrl(profile) {
35
+ const trimmed = profile.baseUrl.trim();
36
+ return trimmed.length > 0 ? trimmed.replace(/\/$/, "") : DEFAULT_OPENAI_BASE_URL;
37
+ }
38
+ function isOpenAiFamily(profile) {
39
+ return (profile.provider === "openai-api" ||
40
+ profile.provider === "openai-compatible" ||
41
+ profile.provider === "openai-codex");
42
+ }
43
+ function isCodexProfile(profile) {
44
+ return profile.provider === "openai-codex";
45
+ }
46
+ function extractCodexAccountId(accessToken) {
47
+ const parts = accessToken.split(".");
48
+ if (parts.length !== 3) {
49
+ throw new Error("Failed to extract accountId from OpenAI Codex token.");
50
+ }
51
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
52
+ const auth = payload["https://api.openai.com/auth"];
53
+ if (!auth || typeof auth !== "object") {
54
+ throw new Error("Failed to extract accountId from OpenAI Codex token.");
55
+ }
56
+ const accountId = auth.chatgpt_account_id;
57
+ if (typeof accountId !== "string" || accountId.trim().length === 0) {
58
+ throw new Error("Failed to extract accountId from OpenAI Codex token.");
59
+ }
60
+ return accountId;
61
+ }
62
+ function buildRequestHeaders(profile, apiKey) {
63
+ const headers = {
64
+ authorization: `Bearer ${apiKey}`,
65
+ "content-type": "application/json"
66
+ };
67
+ if (!isCodexProfile(profile)) {
68
+ return headers;
69
+ }
70
+ headers["OpenAI-Beta"] = "responses=experimental";
71
+ headers.originator = "pi";
72
+ headers["chatgpt-account-id"] = extractCodexAccountId(apiKey);
73
+ return headers;
74
+ }
75
+ function buildResponsesUrl(profile) {
76
+ const baseUrl = normalizeBaseUrl(profile);
77
+ if (isCodexProfile(profile)) {
78
+ if (baseUrl.endsWith("/codex/responses")) {
79
+ return baseUrl;
80
+ }
81
+ if (baseUrl.endsWith("/codex")) {
82
+ return `${baseUrl}/responses`;
83
+ }
84
+ return `${baseUrl}/codex/responses`;
85
+ }
86
+ return baseUrl.endsWith("/responses") ? baseUrl : `${baseUrl}/responses`;
87
+ }
88
+ function buildConversationsUrl(profile) {
89
+ const baseUrl = normalizeBaseUrl(profile);
90
+ if (isCodexProfile(profile)) {
91
+ if (baseUrl.endsWith("/codex")) {
92
+ return `${baseUrl}/conversations`;
93
+ }
94
+ if (baseUrl.endsWith("/codex/responses")) {
95
+ return baseUrl.replace(/\/responses$/, "/conversations");
96
+ }
97
+ return `${baseUrl}/codex/conversations`;
98
+ }
99
+ return baseUrl.endsWith("/v1") ? `${baseUrl}/conversations` : `${baseUrl}/conversations`;
100
+ }
101
+ function parseOutputText(payload) {
102
+ const output = Array.isArray(payload.output) ? payload.output : [];
103
+ for (const item of output) {
104
+ if (!item || typeof item !== "object") {
105
+ continue;
106
+ }
107
+ const content = Array.isArray(item.content)
108
+ ? item.content
109
+ : [];
110
+ for (const part of content) {
111
+ if (part &&
112
+ typeof part === "object" &&
113
+ part.type === "output_text" &&
114
+ typeof part.text === "string") {
115
+ return part.text;
116
+ }
117
+ }
118
+ }
119
+ return "";
120
+ }
121
+ function buildDefaultGraph(kind, title) {
122
+ const modelNodeId = "node_model";
123
+ const outputNodeId = "node_output";
124
+ return {
125
+ nodes: [
126
+ {
127
+ id: "node_input",
128
+ type: "user_input",
129
+ position: { x: 60, y: 160 },
130
+ data: {
131
+ label: "User input",
132
+ description: "Manual runtime input.",
133
+ enabledToolKeys: []
134
+ }
135
+ },
136
+ {
137
+ id: modelNodeId,
138
+ type: kind === "chat" ? "chat" : "functor",
139
+ position: { x: 340, y: 150 },
140
+ data: {
141
+ label: title,
142
+ description: kind === "chat"
143
+ ? "Chat connector node."
144
+ : "Functor node.",
145
+ prompt: kind === "chat"
146
+ ? "Respond helpfully using the linked inputs and available tools."
147
+ : "Transform the linked inputs and return the best final answer.",
148
+ systemPrompt: "",
149
+ enabledToolKeys: [],
150
+ modelConfig: {
151
+ connectionId: null,
152
+ provider: null,
153
+ baseUrl: null,
154
+ model: "",
155
+ thinking: null,
156
+ verbosity: null
157
+ }
158
+ }
159
+ },
160
+ {
161
+ id: outputNodeId,
162
+ type: "output",
163
+ position: { x: 660, y: 150 },
164
+ data: {
165
+ label: "Output",
166
+ description: "Published connector output.",
167
+ outputKey: "primary",
168
+ enabledToolKeys: []
169
+ }
170
+ }
171
+ ],
172
+ edges: [
173
+ {
174
+ id: "edge_input_model",
175
+ source: "node_input",
176
+ target: modelNodeId
177
+ },
178
+ {
179
+ id: "edge_model_output",
180
+ source: modelNodeId,
181
+ target: outputNodeId
182
+ }
183
+ ]
184
+ };
185
+ }
186
+ function ensurePublishedOutputs(connectorId, graph) {
187
+ const outputNodes = graph.nodes.filter((node) => node.type === "output");
188
+ if (outputNodes.length === 0) {
189
+ return [
190
+ buildConnectorOutputCatalogEntry({
191
+ connectorId,
192
+ title: "Connector",
193
+ outputId: "primary"
194
+ })
195
+ ].map((entry) => ({
196
+ id: entry.boxId.replace(/^connector-output:/, ""),
197
+ nodeId: "node_output",
198
+ label: entry.label,
199
+ apiPath: `/api/v1/ai-connectors/${connectorId}/output`
200
+ }));
201
+ }
202
+ return outputNodes.map((node, index) => ({
203
+ id: `${connectorId}_out_${index + 1}`,
204
+ nodeId: node.id,
205
+ label: node.data.label || `Output ${index + 1}`,
206
+ apiPath: `/api/v1/ai-connectors/${connectorId}/output`
207
+ }));
208
+ }
209
+ function mapRun(row) {
210
+ return aiConnectorRunSchema.parse({
211
+ id: row.id,
212
+ connectorId: row.connector_id,
213
+ mode: row.mode,
214
+ status: row.status,
215
+ userInput: row.user_input,
216
+ context: parseJson(row.context_json, {}),
217
+ conversationId: row.conversation_id,
218
+ result: parseJson(row.result_json, null),
219
+ error: row.error,
220
+ createdAt: row.created_at,
221
+ completedAt: row.completed_at
222
+ });
223
+ }
224
+ function mapConversation(row) {
225
+ return aiConnectorConversationSchema.parse({
226
+ id: row.id,
227
+ connectorId: row.connector_id,
228
+ provider: row.provider,
229
+ externalConversationId: row.external_conversation_id,
230
+ transcript: parseJson(row.transcript_json, []),
231
+ createdAt: row.created_at,
232
+ updatedAt: row.updated_at
233
+ });
234
+ }
235
+ function mapConnector(row) {
236
+ return aiConnectorSchema.parse({
237
+ id: row.id,
238
+ slug: row.slug,
239
+ title: row.title,
240
+ description: row.description,
241
+ kind: row.kind,
242
+ homeSurfaceId: row.home_surface_id,
243
+ endpointEnabled: row.endpoint_enabled === 1,
244
+ graph: parseJson(row.graph_json, { nodes: [], edges: [] }),
245
+ publishedOutputs: parseJson(row.published_outputs_json, []),
246
+ lastRun: parseJson(row.last_run_json, null),
247
+ legacyProcessorId: row.legacy_processor_id,
248
+ createdAt: row.created_at,
249
+ updatedAt: row.updated_at
250
+ });
251
+ }
252
+ export function listAiConnectorRuns(connectorId) {
253
+ const rows = getDatabase()
254
+ .prepare(`SELECT * FROM ai_connector_runs WHERE connector_id = ? ORDER BY created_at DESC LIMIT ?`)
255
+ .all(connectorId, MAX_RUN_HISTORY);
256
+ return rows.map(mapRun);
257
+ }
258
+ export function getAiConnectorConversationById(conversationId) {
259
+ const row = getDatabase()
260
+ .prepare(`SELECT * FROM ai_connector_conversations WHERE id = ?`)
261
+ .get(conversationId);
262
+ return row ? mapConversation(row) : null;
263
+ }
264
+ export function getAiConnectorConversationForConnector(connectorId) {
265
+ const row = getDatabase()
266
+ .prepare(`SELECT * FROM ai_connector_conversations WHERE connector_id = ?`)
267
+ .get(connectorId);
268
+ return row ? mapConversation(row) : null;
269
+ }
270
+ function saveAiConnectorConversation(input) {
271
+ getDatabase()
272
+ .prepare(`INSERT INTO ai_connector_conversations (
273
+ id, connector_id, provider, external_conversation_id, transcript_json, created_at, updated_at
274
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
275
+ ON CONFLICT(connector_id) DO UPDATE SET
276
+ provider = excluded.provider,
277
+ external_conversation_id = excluded.external_conversation_id,
278
+ transcript_json = excluded.transcript_json,
279
+ updated_at = excluded.updated_at`)
280
+ .run(input.id, input.connectorId, input.provider, input.externalConversationId, JSON.stringify(input.transcript), input.createdAt, input.updatedAt);
281
+ return getAiConnectorConversationById(input.id);
282
+ }
283
+ function updateConnectorLastRun(connectorId, run) {
284
+ getDatabase()
285
+ .prepare(`UPDATE ai_connectors SET last_run_json = ?, updated_at = ? WHERE id = ?`)
286
+ .run(JSON.stringify(run), new Date().toISOString(), connectorId);
287
+ }
288
+ function insertRun(input) {
289
+ getDatabase()
290
+ .prepare(`INSERT INTO ai_connector_runs (
291
+ id, connector_id, mode, status, user_input, context_json, conversation_id, result_json, error, created_at, completed_at
292
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
293
+ ON CONFLICT(id) DO UPDATE SET
294
+ connector_id = excluded.connector_id,
295
+ mode = excluded.mode,
296
+ status = excluded.status,
297
+ user_input = excluded.user_input,
298
+ context_json = excluded.context_json,
299
+ conversation_id = excluded.conversation_id,
300
+ result_json = excluded.result_json,
301
+ error = excluded.error,
302
+ created_at = excluded.created_at,
303
+ completed_at = excluded.completed_at`)
304
+ .run(input.id, input.connectorId, input.mode, input.status, input.userInput, JSON.stringify(input.context), input.conversationId, input.result ? JSON.stringify(input.result) : null, input.error, input.createdAt, input.completedAt);
305
+ updateConnectorLastRun(input.connectorId, input);
306
+ return input;
307
+ }
308
+ function resolveAllowedPath(inputPath) {
309
+ const candidate = path.resolve(process.cwd(), inputPath);
310
+ const workspaceRoot = process.cwd();
311
+ if (candidate !== workspaceRoot &&
312
+ !candidate.startsWith(`${workspaceRoot}${path.sep}`)) {
313
+ throw new Error("Machine access is restricted to the Forge workspace root.");
314
+ }
315
+ return candidate;
316
+ }
317
+ function tryParseStructuredAgentResponse(value) {
318
+ try {
319
+ return JSON.parse(value);
320
+ }
321
+ catch {
322
+ return null;
323
+ }
324
+ }
325
+ async function executeMachineTool(tool, args) {
326
+ if (tool === "machine_read_file") {
327
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
328
+ if (!targetPath) {
329
+ throw new Error("machine_read_file requires a string path.");
330
+ }
331
+ const content = await readFile(targetPath, "utf8");
332
+ return { path: targetPath, content };
333
+ }
334
+ if (tool === "machine_write_file") {
335
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
336
+ if (!targetPath || typeof args.content !== "string") {
337
+ throw new Error("machine_write_file requires { path, content }.");
338
+ }
339
+ await writeFile(targetPath, args.content, "utf8");
340
+ return { path: targetPath, bytesWritten: Buffer.byteLength(args.content, "utf8") };
341
+ }
342
+ if (typeof args.command !== "string" || args.command.trim().length === 0) {
343
+ throw new Error("machine_exec requires a command string.");
344
+ }
345
+ const cwd = typeof args.cwd === "string" && args.cwd.trim().length > 0
346
+ ? resolveAllowedPath(args.cwd)
347
+ : process.cwd();
348
+ const result = await execFile("zsh", ["-lc", args.command], {
349
+ cwd,
350
+ timeout: 15_000,
351
+ maxBuffer: 256_000
352
+ });
353
+ return {
354
+ cwd,
355
+ stdout: result.stdout.trim(),
356
+ stderr: result.stderr.trim()
357
+ };
358
+ }
359
+ function getConversationBasePrompt(input) {
360
+ return [
361
+ input.node.data.prompt?.trim() || "",
362
+ input.userInput ? `User input:\n${input.userInput}` : "",
363
+ input.upstream.length > 0
364
+ ? `Linked inputs:\n${input.upstream
365
+ .map((entry, index) => `Input ${index + 1}:\n${entry.text}${entry.json ? `\nJSON: ${JSON.stringify(entry.json)}` : ""}`)
366
+ .join("\n\n")}`
367
+ : "",
368
+ input.transcript.length > 0 ? `Tool transcript:\n${input.transcript.join("\n\n")}` : ""
369
+ ]
370
+ .filter(Boolean)
371
+ .join("\n\n");
372
+ }
373
+ async function createOpenAiConversation(profile, apiKey) {
374
+ const response = await fetch(buildConversationsUrl(profile), {
375
+ method: "POST",
376
+ headers: buildRequestHeaders(profile, apiKey),
377
+ body: JSON.stringify({})
378
+ });
379
+ if (!response.ok) {
380
+ const message = await response.text();
381
+ throw new Error(`OpenAI conversation creation failed (${response.status})${message ? `: ${message}` : ""}`);
382
+ }
383
+ const payload = (await response.json());
384
+ const conversationId = typeof payload.id === "string" ? payload.id : null;
385
+ if (!conversationId) {
386
+ throw new Error("OpenAI conversation creation did not return an id.");
387
+ }
388
+ return conversationId;
389
+ }
390
+ async function runOpenAiConversationPrompt(input) {
391
+ const conversationId = input.conversationId ?? (await createOpenAiConversation(input.profile, input.apiKey));
392
+ const response = await fetch(buildResponsesUrl(input.profile), {
393
+ method: "POST",
394
+ headers: buildRequestHeaders(input.profile, input.apiKey),
395
+ body: JSON.stringify({
396
+ model: input.profile.model,
397
+ conversation: { id: conversationId },
398
+ input: [
399
+ ...(input.systemPrompt?.trim()
400
+ ? [
401
+ {
402
+ role: "system",
403
+ content: [{ type: "input_text", text: input.systemPrompt.trim() }]
404
+ }
405
+ ]
406
+ : []),
407
+ {
408
+ role: "user",
409
+ content: [{ type: "input_text", text: input.prompt }]
410
+ }
411
+ ],
412
+ reasoning: typeof input.profile.metadata.reasoningEffort === "string"
413
+ ? { effort: input.profile.metadata.reasoningEffort }
414
+ : undefined,
415
+ text: typeof input.profile.metadata.verbosity === "string"
416
+ ? { verbosity: input.profile.metadata.verbosity }
417
+ : undefined,
418
+ max_output_tokens: 1200
419
+ })
420
+ });
421
+ if (!response.ok) {
422
+ const message = await response.text();
423
+ throw new Error(`OpenAI connector prompt failed (${response.status})${message ? `: ${message}` : ""}`);
424
+ }
425
+ const payload = (await response.json());
426
+ return {
427
+ text: parseOutputText(payload)?.trim() || "",
428
+ conversationId
429
+ };
430
+ }
431
+ function resolveConnectorModelProfile(node, secrets) {
432
+ const requestedConnectionId = node.data.modelConfig?.connectionId;
433
+ const fallbackConnection = (requestedConnectionId
434
+ ? getAiModelConnectionById(requestedConnectionId)
435
+ : null) ??
436
+ getAiModelConnectionById(FORGE_DEFAULT_AGENT_ID) ??
437
+ listAiModelConnections()[0] ??
438
+ null;
439
+ if (!fallbackConnection) {
440
+ throw new Error("No model connection is configured for this connector node.");
441
+ }
442
+ const credential = readModelConnectionCredential(fallbackConnection.id, secrets);
443
+ const explicitApiKey = credential?.kind === "api_key"
444
+ ? credential.apiKey
445
+ : credential?.kind === "oauth"
446
+ ? credential.access
447
+ : null;
448
+ if (!explicitApiKey) {
449
+ throw new Error("The selected connector model connection is missing a credential.");
450
+ }
451
+ const profile = {
452
+ provider: fallbackConnection.provider,
453
+ baseUrl: node.data.modelConfig?.baseUrl?.trim() ||
454
+ fallbackConnection.baseUrl ||
455
+ DEFAULT_OPENAI_BASE_URL,
456
+ model: node.data.modelConfig?.model?.trim() || fallbackConnection.model || "",
457
+ systemPrompt: "",
458
+ secretId: null,
459
+ metadata: {
460
+ reasoningEffort: node.data.modelConfig?.thinking ?? null,
461
+ verbosity: node.data.modelConfig?.verbosity ?? null
462
+ }
463
+ };
464
+ return {
465
+ profile,
466
+ apiKey: explicitApiKey
467
+ };
468
+ }
469
+ async function runModelNode(input) {
470
+ const { profile, apiKey } = resolveConnectorModelProfile(input.node, input.services.secrets);
471
+ const availableTools = input.upstream.flatMap((entry) => entry.tools);
472
+ const enabledKeys = new Set(input.node.data.enabledToolKeys ?? []);
473
+ const activeTools = enabledKeys.size > 0
474
+ ? availableTools.filter((tool) => enabledKeys.has(tool.key))
475
+ : availableTools;
476
+ const transcript = [];
477
+ const conversationAware = input.node.type === "chat";
478
+ let conversationId = input.conversation?.externalConversationId ?? null;
479
+ for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
480
+ const systemPrompt = [
481
+ input.node.data.systemPrompt?.trim() || "",
482
+ activeTools.length > 0
483
+ ? [
484
+ "You may call available tools when needed.",
485
+ "Return strict JSON only.",
486
+ 'For a final answer return {"action":"final","text":"..."}',
487
+ 'For a tool call return {"action":"tool","tool":"tool_key","args":{...}}',
488
+ `Available tools: ${activeTools
489
+ .map((tool) => `${tool.key} (${tool.description})`)
490
+ .join("; ")}.`
491
+ ].join(" ")
492
+ : "Return only the final answer text."
493
+ ]
494
+ .filter(Boolean)
495
+ .join("\n\n");
496
+ const prompt = getConversationBasePrompt({
497
+ connector: input.connector,
498
+ node: input.node,
499
+ userInput: input.userInput,
500
+ upstream: input.upstream,
501
+ transcript
502
+ });
503
+ let rawText = "";
504
+ if (conversationAware && isOpenAiFamily(profile)) {
505
+ const result = await runOpenAiConversationPrompt({
506
+ profile,
507
+ apiKey,
508
+ systemPrompt,
509
+ prompt,
510
+ conversationId
511
+ });
512
+ rawText = result.text;
513
+ conversationId = result.conversationId;
514
+ }
515
+ else {
516
+ rawText = (await input.services.llm.runTextPrompt(profile, {
517
+ explicitApiKey: apiKey,
518
+ systemPrompt,
519
+ prompt
520
+ })).outputText.trim();
521
+ }
522
+ if (activeTools.length === 0) {
523
+ return {
524
+ text: rawText.trim(),
525
+ conversationId
526
+ };
527
+ }
528
+ const structured = tryParseStructuredAgentResponse(rawText.trim());
529
+ if (!structured || structured.action === "final") {
530
+ return {
531
+ text: structured?.text?.trim() || rawText.trim(),
532
+ conversationId
533
+ };
534
+ }
535
+ const toolResult = structured.tool.startsWith("machine_")
536
+ ? await executeMachineTool(structured.tool, structured.args)
537
+ : await executeForgeBoxTool(activeTools.find((tool) => tool.key === structured.tool)?.boxId ?? "", structured.tool, structured.args);
538
+ transcript.push(`Tool call ${structured.tool}: ${JSON.stringify(structured.args)}`, `Tool result: ${JSON.stringify(toolResult)}`);
539
+ }
540
+ return {
541
+ text: "Connector stopped after reaching the maximum tool step count.",
542
+ conversationId
543
+ };
544
+ }
545
+ function validateConnectorGraph(graph) {
546
+ const nodeIds = new Set(graph.nodes.map((node) => node.id));
547
+ for (const edge of graph.edges) {
548
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
549
+ throw new Error("Connector graph edge references a missing node.");
550
+ }
551
+ }
552
+ const adjacency = new Map();
553
+ for (const edge of graph.edges) {
554
+ const current = adjacency.get(edge.source) ?? [];
555
+ current.push(edge.target);
556
+ adjacency.set(edge.source, current);
557
+ }
558
+ const visiting = new Set();
559
+ const visited = new Set();
560
+ const visit = (nodeId) => {
561
+ if (visiting.has(nodeId)) {
562
+ throw new Error("Connector graphs cannot contain cycles.");
563
+ }
564
+ if (visited.has(nodeId)) {
565
+ return;
566
+ }
567
+ visiting.add(nodeId);
568
+ for (const target of adjacency.get(nodeId) ?? []) {
569
+ visit(target);
570
+ }
571
+ visiting.delete(nodeId);
572
+ visited.add(nodeId);
573
+ };
574
+ for (const node of graph.nodes) {
575
+ visit(node.id);
576
+ }
577
+ }
578
+ function buildOutputResult(connector, resolvedNodeValues) {
579
+ const outputs = Object.fromEntries(connector.publishedOutputs.map((output) => {
580
+ const nodeValue = resolvedNodeValues.get(output.nodeId);
581
+ return [
582
+ output.id,
583
+ {
584
+ label: output.label,
585
+ text: nodeValue?.text ?? "",
586
+ json: nodeValue?.json ?? null
587
+ }
588
+ ];
589
+ }));
590
+ const first = connector.publishedOutputs[0];
591
+ return aiConnectorRunResultSchema.parse({
592
+ primaryText: first ? outputs[first.id]?.text ?? "" : "",
593
+ outputs
594
+ });
595
+ }
596
+ function createConversationRecord(input) {
597
+ const now = new Date().toISOString();
598
+ return saveAiConnectorConversation(aiConnectorConversationSchema.parse({
599
+ id: input.existing?.id ?? `aicv_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
600
+ connectorId: input.connectorId,
601
+ provider: input.provider,
602
+ externalConversationId: input.externalConversationId,
603
+ transcript: input.transcript,
604
+ createdAt: input.existing?.createdAt ?? now,
605
+ updatedAt: now
606
+ }));
607
+ }
608
+ async function executeConnector(connector, rawInput, services) {
609
+ validateConnectorGraph(connector.graph);
610
+ const parsedInput = runAiConnectorSchema.parse(rawInput);
611
+ const incoming = new Map();
612
+ for (const edge of connector.graph.edges) {
613
+ const list = incoming.get(edge.target) ?? [];
614
+ list.push(edge);
615
+ incoming.set(edge.target, list);
616
+ }
617
+ const values = new Map();
618
+ const outputNodes = connector.graph.nodes.filter((node) => node.type === "output");
619
+ const activeConversation = parsedInput.conversationId
620
+ ? getAiConnectorConversationById(parsedInput.conversationId)
621
+ : getAiConnectorConversationForConnector(connector.id);
622
+ const evaluateNode = async (nodeId) => {
623
+ const existing = values.get(nodeId);
624
+ if (existing) {
625
+ return existing;
626
+ }
627
+ const node = connector.graph.nodes.find((entry) => entry.id === nodeId);
628
+ if (!node) {
629
+ throw new Error(`Missing connector node ${nodeId}.`);
630
+ }
631
+ const upstream = await Promise.all((incoming.get(nodeId) ?? []).map((edge) => evaluateNode(edge.source)));
632
+ let resolved;
633
+ if (node.type === "box_input") {
634
+ const boxId = node.data.boxId?.trim() || "";
635
+ const providedSnapshot = boxId ? parsedInput.boxSnapshots[boxId] : null;
636
+ const snapshot = providedSnapshot && typeof providedSnapshot === "object"
637
+ ? {
638
+ ...resolveForgeBoxSnapshot(boxId),
639
+ contentJson: providedSnapshot
640
+ }
641
+ : boxId
642
+ ? resolveForgeBoxSnapshot(boxId)
643
+ : {
644
+ boxId: "",
645
+ label: node.data.label,
646
+ capturedAt: new Date().toISOString(),
647
+ contentText: "No box is configured for this node yet.",
648
+ contentJson: null,
649
+ tools: []
650
+ };
651
+ resolved = {
652
+ text: snapshot.contentText,
653
+ json: snapshot.contentJson,
654
+ tools: snapshot.tools.map((tool) => ({
655
+ boxId: snapshot.boxId,
656
+ key: tool.key,
657
+ label: tool.label,
658
+ description: tool.description
659
+ })),
660
+ conversationId: null
661
+ };
662
+ }
663
+ else if (node.type === "user_input") {
664
+ resolved = {
665
+ text: parsedInput.userInput || "",
666
+ json: Object.keys(parsedInput.context).length > 0 ? parsedInput.context : null,
667
+ tools: [],
668
+ conversationId: activeConversation?.id ?? null
669
+ };
670
+ }
671
+ else if (node.type === "output") {
672
+ const mergedText = upstream.map((entry) => entry.text).filter(Boolean).join("\n\n");
673
+ resolved = {
674
+ text: mergedText,
675
+ json: upstream[0]?.json ?? null,
676
+ tools: [],
677
+ conversationId: upstream.find((entry) => entry.conversationId)?.conversationId ?? null
678
+ };
679
+ }
680
+ else {
681
+ const modelResult = await runModelNode({
682
+ connector,
683
+ node,
684
+ userInput: parsedInput.userInput,
685
+ upstream,
686
+ services,
687
+ conversation: activeConversation
688
+ });
689
+ resolved = {
690
+ text: modelResult.text,
691
+ json: null,
692
+ tools: [],
693
+ conversationId: modelResult.conversationId
694
+ };
695
+ }
696
+ values.set(nodeId, resolved);
697
+ return resolved;
698
+ };
699
+ for (const outputNode of outputNodes) {
700
+ await evaluateNode(outputNode.id);
701
+ }
702
+ const result = buildOutputResult(connector, values);
703
+ const conversationProviderNode = connector.graph.nodes.find((node) => node.type === "chat");
704
+ const resolvedConversationId = [...values.values()].find((entry) => entry.conversationId)?.conversationId ?? null;
705
+ const nextConversation = conversationProviderNode
706
+ ? createConversationRecord({
707
+ connectorId: connector.id,
708
+ provider: conversationProviderNode.data.modelConfig?.provider ?? null,
709
+ externalConversationId: conversationProviderNode.data.modelConfig?.provider &&
710
+ isOpenAiFamily({
711
+ provider: conversationProviderNode.data.modelConfig.provider,
712
+ baseUrl: conversationProviderNode.data.modelConfig.baseUrl ?? DEFAULT_OPENAI_BASE_URL,
713
+ model: conversationProviderNode.data.modelConfig.model,
714
+ systemPrompt: "",
715
+ secretId: null,
716
+ metadata: {}
717
+ })
718
+ ? resolvedConversationId
719
+ : null,
720
+ transcript: [
721
+ ...(activeConversation?.transcript ?? []),
722
+ ...(parsedInput.userInput
723
+ ? [
724
+ {
725
+ role: "user",
726
+ text: parsedInput.userInput,
727
+ createdAt: new Date().toISOString()
728
+ }
729
+ ]
730
+ : []),
731
+ {
732
+ role: "assistant",
733
+ text: result.primaryText,
734
+ createdAt: new Date().toISOString()
735
+ }
736
+ ],
737
+ existing: activeConversation
738
+ })
739
+ : null;
740
+ return {
741
+ result,
742
+ conversation: nextConversation
743
+ };
744
+ }
745
+ function migrateLegacyProcessor(processorId) {
746
+ const processor = getAiProcessorById(processorId);
747
+ if (!processor) {
748
+ return null;
749
+ }
750
+ const existing = getDatabase()
751
+ .prepare(`SELECT * FROM ai_connectors WHERE legacy_processor_id = ?`)
752
+ .get(processorId);
753
+ if (existing) {
754
+ return mapConnector(existing);
755
+ }
756
+ const sourceLinks = listAiProcessorLinks(processor.surfaceId).filter((link) => link.targetProcessorId === processor.id);
757
+ const inputNodes = sourceLinks.map((link, index) => ({
758
+ id: `legacy_input_${index + 1}`,
759
+ type: "box_input",
760
+ position: { x: 60, y: 80 + index * 120 },
761
+ data: {
762
+ label: `Legacy input ${index + 1}`,
763
+ description: `Imported from ${link.sourceWidgetId}`,
764
+ boxId: `legacy:${link.sourceWidgetId}`,
765
+ enabledToolKeys: []
766
+ }
767
+ }));
768
+ const modelNode = {
769
+ id: "legacy_functor",
770
+ type: "functor",
771
+ position: { x: 360, y: 160 },
772
+ data: {
773
+ label: processor.title,
774
+ description: "Imported from a legacy AI processor.",
775
+ prompt: processor.promptFlow,
776
+ systemPrompt: processor.contextInput,
777
+ enabledToolKeys: processor.toolConfig.map((tool) => tool.key),
778
+ modelConfig: {
779
+ connectionId: processor.agentConfigs[0]?.connectionId ?? null,
780
+ provider: null,
781
+ baseUrl: null,
782
+ model: processor.agentConfigs[0]?.model ?? "",
783
+ thinking: null,
784
+ verbosity: null
785
+ }
786
+ }
787
+ };
788
+ const outputNode = {
789
+ id: "legacy_output",
790
+ type: "output",
791
+ position: { x: 700, y: 160 },
792
+ data: {
793
+ label: "Output",
794
+ description: "Imported legacy output.",
795
+ outputKey: "primary",
796
+ enabledToolKeys: []
797
+ }
798
+ };
799
+ const graph = {
800
+ nodes: [...inputNodes, modelNode, outputNode],
801
+ edges: [
802
+ ...inputNodes.map((node, index) => ({
803
+ id: `legacy_edge_input_${index + 1}`,
804
+ source: node.id,
805
+ target: modelNode.id
806
+ })),
807
+ {
808
+ id: "legacy_edge_output",
809
+ source: modelNode.id,
810
+ target: outputNode.id
811
+ }
812
+ ]
813
+ };
814
+ return createAiConnector({
815
+ title: processor.title,
816
+ description: "Migrated from a legacy AI processor.",
817
+ kind: "functor",
818
+ homeSurfaceId: processor.surfaceId,
819
+ endpointEnabled: processor.endpointEnabled,
820
+ graph,
821
+ legacyProcessorId: processor.id
822
+ });
823
+ }
824
+ export function ensureLegacyProcessorsMigrated() {
825
+ for (const processor of listAiProcessors()) {
826
+ migrateLegacyProcessor(processor.id);
827
+ }
828
+ }
829
+ export function listAiConnectors() {
830
+ ensureLegacyProcessorsMigrated();
831
+ const rows = getDatabase()
832
+ .prepare(`SELECT * FROM ai_connectors ORDER BY created_at ASC`)
833
+ .all();
834
+ return rows.map(mapConnector);
835
+ }
836
+ export function getAiConnectorById(connectorId) {
837
+ ensureLegacyProcessorsMigrated();
838
+ const row = getDatabase()
839
+ .prepare(`SELECT * FROM ai_connectors WHERE id = ?`)
840
+ .get(connectorId);
841
+ return row ? mapConnector(row) : null;
842
+ }
843
+ export function getAiConnectorBySlug(slug) {
844
+ ensureLegacyProcessorsMigrated();
845
+ const row = getDatabase()
846
+ .prepare(`SELECT * FROM ai_connectors WHERE slug = ?`)
847
+ .get(slug);
848
+ return row ? mapConnector(row) : null;
849
+ }
850
+ export function createAiConnector(input) {
851
+ const parsed = createAiConnectorSchema.parse(input);
852
+ const now = new Date().toISOString();
853
+ const id = `aic_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
854
+ const slug = buildConnectorSlug(parsed.title, id);
855
+ const graph = parsed.graph.nodes.length > 0 ? parsed.graph : buildDefaultGraph(parsed.kind, parsed.title);
856
+ const publishedOutputs = ensurePublishedOutputs(id, graph);
857
+ getDatabase()
858
+ .prepare(`INSERT INTO ai_connectors (
859
+ id, slug, title, description, kind, home_surface_id, endpoint_enabled, graph_json, published_outputs_json, last_run_json, legacy_processor_id, created_at, updated_at
860
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
861
+ .run(id, slug, parsed.title, parsed.description, parsed.kind, parsed.homeSurfaceId, parsed.endpointEnabled ? 1 : 0, JSON.stringify(graph), JSON.stringify(publishedOutputs), null, input.legacyProcessorId ?? null, now, now);
862
+ return getAiConnectorById(id);
863
+ }
864
+ export function updateAiConnector(connectorId, patch) {
865
+ const current = getAiConnectorById(connectorId);
866
+ if (!current) {
867
+ return null;
868
+ }
869
+ const parsed = updateAiConnectorSchema.parse(patch);
870
+ const nextGraph = parsed.graph ?? current.graph;
871
+ validateConnectorGraph(nextGraph);
872
+ const nextTitle = parsed.title ?? current.title;
873
+ const next = {
874
+ ...current,
875
+ ...parsed,
876
+ title: nextTitle,
877
+ slug: parsed.title && parsed.title !== current.title
878
+ ? buildConnectorSlug(parsed.title, current.id)
879
+ : current.slug,
880
+ graph: nextGraph,
881
+ publishedOutputs: ensurePublishedOutputs(current.id, nextGraph)
882
+ };
883
+ const now = new Date().toISOString();
884
+ getDatabase()
885
+ .prepare(`UPDATE ai_connectors
886
+ SET slug = ?, title = ?, description = ?, kind = ?, home_surface_id = ?, endpoint_enabled = ?, graph_json = ?, published_outputs_json = ?, updated_at = ?
887
+ WHERE id = ?`)
888
+ .run(next.slug, next.title, next.description, next.kind, next.homeSurfaceId, next.endpointEnabled ? 1 : 0, JSON.stringify(next.graph), JSON.stringify(next.publishedOutputs), now, connectorId);
889
+ return getAiConnectorById(connectorId);
890
+ }
891
+ export function deleteAiConnector(connectorId) {
892
+ const current = getAiConnectorById(connectorId);
893
+ if (!current) {
894
+ return null;
895
+ }
896
+ getDatabase().prepare(`DELETE FROM ai_connectors WHERE id = ?`).run(connectorId);
897
+ return current;
898
+ }
899
+ export async function runAiConnector(connectorId, input, services, mode = "run") {
900
+ const connector = getAiConnectorById(connectorId);
901
+ if (!connector) {
902
+ throw new Error(`Connector ${connectorId} was not found.`);
903
+ }
904
+ const pendingRun = aiConnectorRunSchema.parse({
905
+ id: `aicr_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
906
+ connectorId,
907
+ mode,
908
+ status: "running",
909
+ userInput: input.userInput ?? "",
910
+ context: input.context ?? {},
911
+ conversationId: input.conversationId ?? null,
912
+ result: null,
913
+ error: null,
914
+ createdAt: new Date().toISOString(),
915
+ completedAt: null
916
+ });
917
+ insertRun(pendingRun);
918
+ try {
919
+ const execution = await executeConnector(connector, input, services);
920
+ const completedRun = aiConnectorRunSchema.parse({
921
+ ...pendingRun,
922
+ status: "completed",
923
+ result: execution.result,
924
+ conversationId: execution.conversation?.id ?? pendingRun.conversationId,
925
+ completedAt: new Date().toISOString()
926
+ });
927
+ insertRun(completedRun);
928
+ return {
929
+ connector: getAiConnectorById(connectorId),
930
+ run: completedRun,
931
+ conversation: execution.conversation
932
+ };
933
+ }
934
+ catch (error) {
935
+ const failedRun = aiConnectorRunSchema.parse({
936
+ ...pendingRun,
937
+ status: "failed",
938
+ error: error instanceof Error ? error.message : "Connector run failed",
939
+ completedAt: new Date().toISOString()
940
+ });
941
+ insertRun(failedRun);
942
+ throw error;
943
+ }
944
+ }