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.
- package/README.md +13 -0
- package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
- package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
- package/dist/assets/index-CFCKDIMH.js +67 -0
- package/dist/assets/index-CFCKDIMH.js.map +1 -0
- package/dist/assets/index-ZPY6U1TU.css +1 -0
- package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
- package/dist/assets/motion-DvkU14p-.js.map +1 -0
- package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
- package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
- package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
- package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
- package/dist/assets/vendor-D9PTEPSB.js +824 -0
- package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
- package/dist/assets/viz-Cqb6s--o.js +34 -0
- package/dist/assets/viz-Cqb6s--o.js.map +1 -0
- package/dist/index.html +8 -8
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -0
- package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
- package/dist/openclaw/plugin-entry-shared.js +7 -4
- package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
- package/dist/openclaw/routes.js +236 -0
- package/dist/openclaw/session-bootstrap.d.ts +78 -0
- package/dist/openclaw/session-bootstrap.js +240 -0
- package/dist/openclaw/tools.js +279 -3
- package/dist/server/app.js +855 -19
- package/dist/server/connectors/box-registry.js +257 -0
- package/dist/server/db.js +2 -0
- package/dist/server/discovery-advertiser.js +114 -0
- package/dist/server/health.js +39 -11
- package/dist/server/index.js +4 -0
- package/dist/server/managers/platform/llm-manager.js +40 -4
- package/dist/server/managers/platform/openai-responses-provider.js +129 -19
- package/dist/server/movement.js +2935 -0
- package/dist/server/openapi.js +628 -5
- package/dist/server/psyche-types.js +15 -1
- package/dist/server/questionnaire-flow.js +552 -0
- package/dist/server/questionnaire-seeds.js +853 -0
- package/dist/server/questionnaire-types.js +340 -0
- package/dist/server/repositories/ai-connectors.js +944 -0
- package/dist/server/repositories/ai-processors.js +547 -0
- package/dist/server/repositories/entity-ownership.js +9 -1
- package/dist/server/repositories/habits.js +69 -5
- package/dist/server/repositories/model-settings.js +216 -0
- package/dist/server/repositories/notes.js +57 -15
- package/dist/server/repositories/preferences.js +124 -0
- package/dist/server/repositories/questionnaires.js +1338 -0
- package/dist/server/repositories/settings.js +108 -12
- package/dist/server/repositories/surface-layouts.js +76 -0
- package/dist/server/repositories/wiki-memory.js +5 -1
- package/dist/server/services/entity-crud.js +81 -2
- package/dist/server/services/openai-codex-oauth.js +153 -0
- package/dist/server/services/psyche-observation-calendar.js +46 -0
- package/dist/server/types.js +492 -3
- package/dist/server/watch-mobile.js +562 -0
- package/dist/server/web.js +9 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -1
- package/server/migrations/024_questionnaires.sql +96 -0
- package/server/migrations/025_ai_model_connections.sql +26 -0
- package/server/migrations/026_custom_theme_settings.sql +2 -0
- package/server/migrations/027_ai_processors.sql +31 -0
- package/server/migrations/028_movement_domain.sql +136 -0
- package/server/migrations/029_watch_micro_capture.sql +23 -0
- package/server/migrations/030_surface_layouts.sql +5 -0
- package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/server/migrations/032_ai_connectors.sql +44 -0
- package/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/server/migrations/034_movement_segment_sync.sql +49 -0
- package/skills/forge-openclaw/SKILL.md +12 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
- package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
- package/dist/assets/index-DTCwBWAs.js +0 -65
- package/dist/assets/index-DTCwBWAs.js.map +0 -1
- package/dist/assets/index-DttXlAgi.css +0 -1
- package/dist/assets/motion-D4sZgCHd.js.map +0 -1
- package/dist/assets/vendor-De38P6YR.js +0 -729
- package/dist/assets/vendor-De38P6YR.js.map +0 -1
- package/dist/assets/viz-C6hfyqzu.js +0 -34
- package/dist/assets/viz-C6hfyqzu.js.map +0 -1
- 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
|
+
}
|