forge-openclaw-plugin 0.2.24 → 0.2.26

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 (208) hide show
  1. package/README.md +13 -0
  2. package/dist/assets/{board-_C6oMy5w.js → board-ta0rUHOf.js} +3 -3
  3. package/dist/assets/{board-_C6oMy5w.js.map → board-ta0rUHOf.js.map} +1 -1
  4. package/dist/assets/index-Ro0ZF_az.css +1 -0
  5. package/dist/assets/index-ytlpSj23.js +79 -0
  6. package/dist/assets/index-ytlpSj23.js.map +1 -0
  7. package/dist/assets/{motion-D4sZgCHd.js → motion-fBKPB6yw.js} +3 -3
  8. package/dist/assets/motion-fBKPB6yw.js.map +1 -0
  9. package/dist/assets/{table-BWzTaky1.js → table-C-IGTQni.js} +2 -2
  10. package/dist/assets/{table-BWzTaky1.js.map → table-C-IGTQni.js.map} +1 -1
  11. package/dist/assets/{ui-BzK4azQb.js → ui-DInOpaYF.js} +2 -2
  12. package/dist/assets/{ui-BzK4azQb.js.map → ui-DInOpaYF.js.map} +1 -1
  13. package/dist/assets/vendor-lE3tZJcC.js +876 -0
  14. package/dist/assets/vendor-lE3tZJcC.js.map +1 -0
  15. package/dist/index.html +7 -8
  16. package/dist/openclaw/local-runtime.d.ts +3 -1
  17. package/dist/openclaw/local-runtime.js +51 -15
  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 +31 -6
  22. package/dist/openclaw/plugin-sdk-types.d.ts +29 -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 -6
  27. package/dist/server/server/migrations/001_core.sql +411 -0
  28. package/dist/server/server/migrations/002_psyche.sql +392 -0
  29. package/dist/server/server/migrations/003_habits.sql +30 -0
  30. package/dist/server/server/migrations/004_habit_links.sql +8 -0
  31. package/dist/server/server/migrations/005_habit_psyche_links.sql +24 -0
  32. package/dist/server/server/migrations/006_work_adjustments.sql +14 -0
  33. package/dist/server/server/migrations/007_weekly_review_closures.sql +17 -0
  34. package/dist/server/server/migrations/008_calendar_execution.sql +147 -0
  35. package/dist/server/server/migrations/009_true_calendar_events.sql +195 -0
  36. package/dist/server/server/migrations/010_calendar_selection_state.sql +6 -0
  37. package/dist/server/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  38. package/dist/server/server/migrations/012_work_block_ranges.sql +7 -0
  39. package/dist/server/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  40. package/dist/server/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  41. package/dist/server/server/migrations/015_multi_user_and_strategies.sql +244 -0
  42. package/dist/server/server/migrations/016_health_companion.sql +158 -0
  43. package/dist/server/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  44. package/dist/server/server/migrations/017_preferences.sql +131 -0
  45. package/dist/server/server/migrations/018_preference_catalogs.sql +31 -0
  46. package/dist/server/server/migrations/019_wiki_memory.sql +255 -0
  47. package/dist/server/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  48. package/dist/server/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  49. package/dist/server/server/migrations/022_wiki_ingest_background.sql +85 -0
  50. package/dist/server/server/migrations/023_diagnostic_logs.sql +28 -0
  51. package/dist/server/server/migrations/024_questionnaires.sql +96 -0
  52. package/dist/server/server/migrations/025_ai_model_connections.sql +26 -0
  53. package/dist/server/server/migrations/026_custom_theme_settings.sql +2 -0
  54. package/dist/server/server/migrations/027_ai_processors.sql +31 -0
  55. package/dist/server/server/migrations/028_movement_domain.sql +136 -0
  56. package/dist/server/server/migrations/029_watch_micro_capture.sql +23 -0
  57. package/dist/server/server/migrations/030_surface_layouts.sql +5 -0
  58. package/dist/server/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  59. package/dist/server/server/migrations/032_ai_connectors.sql +44 -0
  60. package/dist/server/server/migrations/033_movement_trip_point_sync.sql +36 -0
  61. package/dist/server/server/migrations/034_movement_segment_sync.sql +49 -0
  62. package/dist/server/server/migrations/035_google_local_auth_settings.sql +2 -0
  63. package/dist/server/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  64. package/dist/server/{app.js → server/src/app.js} +992 -25
  65. package/dist/server/server/src/connectors/box-registry.js +188 -0
  66. package/dist/server/{db.js → server/src/db.js} +6 -0
  67. package/dist/server/server/src/debug.js +19 -0
  68. package/dist/server/server/src/discovery-advertiser.js +114 -0
  69. package/dist/server/{health.js → server/src/health.js} +39 -11
  70. package/dist/server/{index.js → server/src/index.js} +4 -0
  71. package/dist/server/{managers → server/src/managers}/platform/llm-manager.js +40 -4
  72. package/dist/server/{managers → server/src/managers}/platform/openai-responses-provider.js +129 -19
  73. package/dist/server/server/src/movement.js +2935 -0
  74. package/dist/server/{openapi.js → server/src/openapi.js} +628 -5
  75. package/dist/server/{psyche-types.js → server/src/psyche-types.js} +15 -1
  76. package/dist/server/server/src/questionnaire-flow.js +552 -0
  77. package/dist/server/server/src/questionnaire-seeds.js +853 -0
  78. package/dist/server/server/src/questionnaire-types.js +340 -0
  79. package/dist/server/server/src/repositories/ai-connectors.js +1207 -0
  80. package/dist/server/server/src/repositories/ai-processors.js +547 -0
  81. package/dist/server/{repositories → server/src/repositories}/calendar.js +1 -1
  82. package/dist/server/{repositories → server/src/repositories}/entity-ownership.js +9 -1
  83. package/dist/server/{repositories → server/src/repositories}/habits.js +69 -5
  84. package/dist/server/server/src/repositories/model-settings.js +216 -0
  85. package/dist/server/{repositories → server/src/repositories}/notes.js +57 -15
  86. package/dist/server/{repositories → server/src/repositories}/preferences.js +124 -0
  87. package/dist/server/server/src/repositories/questionnaires.js +1338 -0
  88. package/dist/server/{repositories → server/src/repositories}/settings.js +156 -12
  89. package/dist/server/server/src/repositories/surface-layouts.js +76 -0
  90. package/dist/server/{repositories → server/src/repositories}/wiki-memory.js +5 -1
  91. package/dist/server/{services → server/src/services}/calendar-runtime.js +775 -58
  92. package/dist/server/{services → server/src/services}/entity-crud.js +81 -2
  93. package/dist/server/server/src/services/google-calendar-oauth-config.js +176 -0
  94. package/dist/server/server/src/services/openai-codex-oauth.js +153 -0
  95. package/dist/server/server/src/services/psyche-observation-calendar.js +46 -0
  96. package/dist/server/{types.js → server/src/types.js} +621 -14
  97. package/dist/server/server/src/watch-mobile.js +562 -0
  98. package/dist/server/{web.js → server/src/web.js} +30 -4
  99. package/dist/server/src/components/customization/utility-widgets.js +330 -0
  100. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +92 -0
  101. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +128 -0
  102. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +37 -0
  103. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +114 -0
  104. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +57 -0
  105. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +4 -0
  106. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +13 -0
  107. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +63 -0
  108. package/dist/server/src/lib/api-error.js +37 -0
  109. package/dist/server/src/lib/api.js +1859 -0
  110. package/dist/server/src/lib/calendar-name-deduper.js +144 -0
  111. package/dist/server/src/lib/diagnostics.js +67 -0
  112. package/dist/server/src/lib/psyche-types.js +1 -0
  113. package/dist/server/src/lib/questionnaire-types.js +1 -0
  114. package/dist/server/src/lib/runtime-paths.js +24 -0
  115. package/dist/server/src/lib/schemas.js +234 -0
  116. package/dist/server/src/lib/snapshot-normalizer.js +374 -0
  117. package/dist/server/src/lib/theme-system.js +319 -0
  118. package/dist/server/src/lib/types.js +1 -0
  119. package/dist/server/src/lib/utils.js +22 -0
  120. package/dist/server/src/lib/workbench/boxes.js +16 -0
  121. package/dist/server/src/lib/workbench/nodes.js +15 -0
  122. package/dist/server/src/lib/workbench/registry.js +73 -0
  123. package/dist/server/src/lib/workbench/runtime.js +181 -0
  124. package/openclaw.plugin.json +1 -1
  125. package/package.json +6 -1
  126. package/server/index.js +68 -0
  127. package/server/migrations/024_questionnaires.sql +96 -0
  128. package/server/migrations/025_ai_model_connections.sql +26 -0
  129. package/server/migrations/026_custom_theme_settings.sql +2 -0
  130. package/server/migrations/027_ai_processors.sql +31 -0
  131. package/server/migrations/028_movement_domain.sql +136 -0
  132. package/server/migrations/029_watch_micro_capture.sql +23 -0
  133. package/server/migrations/030_surface_layouts.sql +5 -0
  134. package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  135. package/server/migrations/032_ai_connectors.sql +44 -0
  136. package/server/migrations/033_movement_trip_point_sync.sql +36 -0
  137. package/server/migrations/034_movement_segment_sync.sql +49 -0
  138. package/server/migrations/035_google_local_auth_settings.sql +2 -0
  139. package/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  140. package/skills/forge-openclaw/SKILL.md +15 -1
  141. package/skills/forge-openclaw/entity_conversation_playbooks.md +523 -87
  142. package/skills/forge-openclaw/psyche_entity_playbooks.md +331 -221
  143. package/dist/assets/index-DTCwBWAs.js +0 -65
  144. package/dist/assets/index-DTCwBWAs.js.map +0 -1
  145. package/dist/assets/index-DttXlAgi.css +0 -1
  146. package/dist/assets/motion-D4sZgCHd.js.map +0 -1
  147. package/dist/assets/vendor-De38P6YR.js +0 -729
  148. package/dist/assets/vendor-De38P6YR.js.map +0 -1
  149. package/dist/assets/viz-C6hfyqzu.js +0 -34
  150. package/dist/assets/viz-C6hfyqzu.js.map +0 -1
  151. package/skills/forge-openclaw/cron_jobs.md +0 -395
  152. /package/dist/server/{demo-data.js → server/src/demo-data.js} +0 -0
  153. /package/dist/server/{e2e-server.js → server/src/e2e-server.js} +0 -0
  154. /package/dist/server/{errors.js → server/src/errors.js} +0 -0
  155. /package/dist/server/{managers → server/src/managers}/base.js +0 -0
  156. /package/dist/server/{managers → server/src/managers}/contracts.js +0 -0
  157. /package/dist/server/{managers → server/src/managers}/platform/api-gateway-manager.js +0 -0
  158. /package/dist/server/{managers → server/src/managers}/platform/audit-manager.js +0 -0
  159. /package/dist/server/{managers → server/src/managers}/platform/authentication-manager.js +0 -0
  160. /package/dist/server/{managers → server/src/managers}/platform/authorization-manager.js +0 -0
  161. /package/dist/server/{managers → server/src/managers}/platform/background-job-manager.js +0 -0
  162. /package/dist/server/{managers → server/src/managers}/platform/configuration-manager.js +0 -0
  163. /package/dist/server/{managers → server/src/managers}/platform/database-manager.js +0 -0
  164. /package/dist/server/{managers → server/src/managers}/platform/event-bus-manager.js +0 -0
  165. /package/dist/server/{managers → server/src/managers}/platform/external-service-manager.js +0 -0
  166. /package/dist/server/{managers → server/src/managers}/platform/health-manager.js +0 -0
  167. /package/dist/server/{managers → server/src/managers}/platform/migration-manager.js +0 -0
  168. /package/dist/server/{managers → server/src/managers}/platform/search-index-manager.js +0 -0
  169. /package/dist/server/{managers → server/src/managers}/platform/secrets-manager.js +0 -0
  170. /package/dist/server/{managers → server/src/managers}/platform/session-manager.js +0 -0
  171. /package/dist/server/{managers → server/src/managers}/platform/storage-manager.js +0 -0
  172. /package/dist/server/{managers → server/src/managers}/platform/token-manager.js +0 -0
  173. /package/dist/server/{managers → server/src/managers}/platform/transaction-manager.js +0 -0
  174. /package/dist/server/{managers → server/src/managers}/platform/trusted-network.js +0 -0
  175. /package/dist/server/{managers → server/src/managers}/runtime.js +0 -0
  176. /package/dist/server/{managers → server/src/managers}/type-guards.js +0 -0
  177. /package/dist/server/{preferences-seeds.js → server/src/preferences-seeds.js} +0 -0
  178. /package/dist/server/{preferences-types.js → server/src/preferences-types.js} +0 -0
  179. /package/dist/server/{repositories → server/src/repositories}/activity-events.js +0 -0
  180. /package/dist/server/{repositories → server/src/repositories}/collaboration.js +0 -0
  181. /package/dist/server/{repositories → server/src/repositories}/deleted-entities.js +0 -0
  182. /package/dist/server/{repositories → server/src/repositories}/diagnostic-logs.js +0 -0
  183. /package/dist/server/{repositories → server/src/repositories}/domains.js +0 -0
  184. /package/dist/server/{repositories → server/src/repositories}/event-log.js +0 -0
  185. /package/dist/server/{repositories → server/src/repositories}/goals.js +0 -0
  186. /package/dist/server/{repositories → server/src/repositories}/projects.js +0 -0
  187. /package/dist/server/{repositories → server/src/repositories}/psyche.js +0 -0
  188. /package/dist/server/{repositories → server/src/repositories}/rewards.js +0 -0
  189. /package/dist/server/{repositories → server/src/repositories}/strategies.js +0 -0
  190. /package/dist/server/{repositories → server/src/repositories}/tags.js +0 -0
  191. /package/dist/server/{repositories → server/src/repositories}/task-runs.js +0 -0
  192. /package/dist/server/{repositories → server/src/repositories}/tasks.js +0 -0
  193. /package/dist/server/{repositories → server/src/repositories}/users.js +0 -0
  194. /package/dist/server/{repositories → server/src/repositories}/weekly-reviews.js +0 -0
  195. /package/dist/server/{repositories → server/src/repositories}/work-adjustments.js +0 -0
  196. /package/dist/server/{seed-demo.js → server/src/seed-demo.js} +0 -0
  197. /package/dist/server/{services → server/src/services}/context.js +0 -0
  198. /package/dist/server/{services → server/src/services}/dashboard.js +0 -0
  199. /package/dist/server/{services → server/src/services}/gamification.js +0 -0
  200. /package/dist/server/{services → server/src/services}/insights.js +0 -0
  201. /package/dist/server/{services → server/src/services}/projects.js +0 -0
  202. /package/dist/server/{services → server/src/services}/psyche.js +0 -0
  203. /package/dist/server/{services → server/src/services}/relations.js +0 -0
  204. /package/dist/server/{services → server/src/services}/reviews.js +0 -0
  205. /package/dist/server/{services → server/src/services}/run-recovery.js +0 -0
  206. /package/dist/server/{services → server/src/services}/tagging.js +0 -0
  207. /package/dist/server/{services → server/src/services}/task-run-watchdog.js +0 -0
  208. /package/dist/server/{services → server/src/services}/work-time.js +0 -0
@@ -0,0 +1,1207 @@
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.id.replace(/^connector-output:/, ""),
197
+ nodeId: "node_output",
198
+ label: entry.title,
199
+ apiPath: `/api/v1/workbench/flows/${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/workbench/flows/${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
+ function tryParseJsonObject(value) {
326
+ try {
327
+ const parsed = JSON.parse(value);
328
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
329
+ return parsed;
330
+ }
331
+ return null;
332
+ }
333
+ catch {
334
+ return null;
335
+ }
336
+ }
337
+ function coerceText(value) {
338
+ if (typeof value === "string") {
339
+ return value;
340
+ }
341
+ if (value == null) {
342
+ return "";
343
+ }
344
+ if (typeof value === "number" || typeof value === "boolean") {
345
+ return String(value);
346
+ }
347
+ try {
348
+ return JSON.stringify(value);
349
+ }
350
+ catch {
351
+ return "";
352
+ }
353
+ }
354
+ function buildOutputMap(primaryText, primaryJson, outputKeys = []) {
355
+ const outputMap = {
356
+ primary: {
357
+ text: primaryText,
358
+ json: primaryJson
359
+ }
360
+ };
361
+ for (const key of outputKeys) {
362
+ if (!primaryJson || !(key in primaryJson)) {
363
+ continue;
364
+ }
365
+ const value = primaryJson[key];
366
+ outputMap[key] = {
367
+ text: coerceText(value),
368
+ json: value && typeof value === "object" && !Array.isArray(value)
369
+ ? value
370
+ : null
371
+ };
372
+ }
373
+ return outputMap;
374
+ }
375
+ function readOutputSelection(value, handle) {
376
+ if (!handle || handle === "primary") {
377
+ return { text: value.text, json: value.json };
378
+ }
379
+ const selected = value.outputMap[handle];
380
+ if (selected) {
381
+ return selected;
382
+ }
383
+ if (value.json && handle in value.json) {
384
+ const raw = value.json[handle];
385
+ return {
386
+ text: coerceText(raw),
387
+ json: raw && typeof raw === "object" && !Array.isArray(raw)
388
+ ? raw
389
+ : null
390
+ };
391
+ }
392
+ return { text: value.text, json: value.json };
393
+ }
394
+ async function executeMachineTool(tool, args) {
395
+ if (tool === "machine_read_file") {
396
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
397
+ if (!targetPath) {
398
+ throw new Error("machine_read_file requires a string path.");
399
+ }
400
+ const content = await readFile(targetPath, "utf8");
401
+ return { path: targetPath, content };
402
+ }
403
+ if (tool === "machine_write_file") {
404
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
405
+ if (!targetPath || typeof args.content !== "string") {
406
+ throw new Error("machine_write_file requires { path, content }.");
407
+ }
408
+ await writeFile(targetPath, args.content, "utf8");
409
+ return { path: targetPath, bytesWritten: Buffer.byteLength(args.content, "utf8") };
410
+ }
411
+ if (typeof args.command !== "string" || args.command.trim().length === 0) {
412
+ throw new Error("machine_exec requires a command string.");
413
+ }
414
+ const cwd = typeof args.cwd === "string" && args.cwd.trim().length > 0
415
+ ? resolveAllowedPath(args.cwd)
416
+ : process.cwd();
417
+ const result = await execFile("zsh", ["-lc", args.command], {
418
+ cwd,
419
+ timeout: 15_000,
420
+ maxBuffer: 256_000
421
+ });
422
+ return {
423
+ cwd,
424
+ stdout: result.stdout.trim(),
425
+ stderr: result.stderr.trim()
426
+ };
427
+ }
428
+ function getConversationBasePrompt(input) {
429
+ return [
430
+ input.node.data.prompt?.trim() || "",
431
+ input.userInput ? `User input:\n${input.userInput}` : "",
432
+ input.upstream.length > 0
433
+ ? `Linked inputs:\n${input.upstream
434
+ .map((entry, index) => `Input ${index + 1}:\n${entry.text}${entry.json ? `\nJSON: ${JSON.stringify(entry.json)}` : ""}`)
435
+ .join("\n\n")}`
436
+ : "",
437
+ input.transcript.length > 0 ? `Tool transcript:\n${input.transcript.join("\n\n")}` : ""
438
+ ]
439
+ .filter(Boolean)
440
+ .join("\n\n");
441
+ }
442
+ async function createOpenAiConversation(profile, apiKey) {
443
+ const response = await fetch(buildConversationsUrl(profile), {
444
+ method: "POST",
445
+ headers: buildRequestHeaders(profile, apiKey),
446
+ body: JSON.stringify({})
447
+ });
448
+ if (!response.ok) {
449
+ const message = await response.text();
450
+ throw new Error(`OpenAI conversation creation failed (${response.status})${message ? `: ${message}` : ""}`);
451
+ }
452
+ const payload = (await response.json());
453
+ const conversationId = typeof payload.id === "string" ? payload.id : null;
454
+ if (!conversationId) {
455
+ throw new Error("OpenAI conversation creation did not return an id.");
456
+ }
457
+ return conversationId;
458
+ }
459
+ async function runOpenAiConversationPrompt(input) {
460
+ const conversationId = input.conversationId ?? (await createOpenAiConversation(input.profile, input.apiKey));
461
+ const response = await fetch(buildResponsesUrl(input.profile), {
462
+ method: "POST",
463
+ headers: buildRequestHeaders(input.profile, input.apiKey),
464
+ body: JSON.stringify({
465
+ model: input.profile.model,
466
+ conversation: { id: conversationId },
467
+ input: [
468
+ ...(input.systemPrompt?.trim()
469
+ ? [
470
+ {
471
+ role: "system",
472
+ content: [{ type: "input_text", text: input.systemPrompt.trim() }]
473
+ }
474
+ ]
475
+ : []),
476
+ {
477
+ role: "user",
478
+ content: [{ type: "input_text", text: input.prompt }]
479
+ }
480
+ ],
481
+ reasoning: typeof input.profile.metadata.reasoningEffort === "string"
482
+ ? { effort: input.profile.metadata.reasoningEffort }
483
+ : undefined,
484
+ text: typeof input.profile.metadata.verbosity === "string"
485
+ ? { verbosity: input.profile.metadata.verbosity }
486
+ : undefined,
487
+ max_output_tokens: 1200
488
+ })
489
+ });
490
+ if (!response.ok) {
491
+ const message = await response.text();
492
+ throw new Error(`OpenAI connector prompt failed (${response.status})${message ? `: ${message}` : ""}`);
493
+ }
494
+ const payload = (await response.json());
495
+ return {
496
+ text: parseOutputText(payload)?.trim() || "",
497
+ conversationId
498
+ };
499
+ }
500
+ function resolveConnectorModelProfile(node, secrets) {
501
+ const requestedConnectionId = node.data.modelConfig?.connectionId;
502
+ const fallbackConnection = (requestedConnectionId
503
+ ? getAiModelConnectionById(requestedConnectionId)
504
+ : null) ??
505
+ getAiModelConnectionById(FORGE_DEFAULT_AGENT_ID) ??
506
+ listAiModelConnections()[0] ??
507
+ null;
508
+ if (!fallbackConnection) {
509
+ throw new Error("No model connection is configured for this connector node.");
510
+ }
511
+ const credential = readModelConnectionCredential(fallbackConnection.id, secrets);
512
+ const explicitApiKey = credential?.kind === "api_key"
513
+ ? credential.apiKey
514
+ : credential?.kind === "oauth"
515
+ ? credential.access
516
+ : null;
517
+ if (!explicitApiKey) {
518
+ throw new Error("The selected connector model connection is missing a credential.");
519
+ }
520
+ const profile = {
521
+ provider: fallbackConnection.provider,
522
+ baseUrl: node.data.modelConfig?.baseUrl?.trim() ||
523
+ fallbackConnection.baseUrl ||
524
+ DEFAULT_OPENAI_BASE_URL,
525
+ model: node.data.modelConfig?.model?.trim() || fallbackConnection.model || "",
526
+ systemPrompt: "",
527
+ secretId: null,
528
+ metadata: {
529
+ reasoningEffort: node.data.modelConfig?.thinking ?? null,
530
+ verbosity: node.data.modelConfig?.verbosity ?? null
531
+ }
532
+ };
533
+ return {
534
+ profile,
535
+ apiKey: explicitApiKey
536
+ };
537
+ }
538
+ async function runModelNode(input) {
539
+ const { profile, apiKey } = resolveConnectorModelProfile(input.node, input.services.secrets);
540
+ const availableTools = input.upstream.flatMap((entry) => entry.tools);
541
+ const enabledKeys = new Set(input.node.data.enabledToolKeys ?? []);
542
+ const activeTools = enabledKeys.size > 0
543
+ ? availableTools.filter((tool) => enabledKeys.has(tool.key))
544
+ : availableTools;
545
+ const transcript = [];
546
+ const conversationAware = input.node.type === "chat";
547
+ let conversationId = input.conversation?.externalConversationId ?? null;
548
+ for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
549
+ const systemPrompt = [
550
+ input.node.data.systemPrompt?.trim() || "",
551
+ activeTools.length > 0
552
+ ? [
553
+ "You may call available tools when needed.",
554
+ "Return strict JSON only.",
555
+ 'For a final answer return {"action":"final","text":"..."}',
556
+ 'For a tool call return {"action":"tool","tool":"tool_key","args":{...}}',
557
+ `Available tools: ${activeTools
558
+ .map((tool) => `${tool.key} (${tool.description})`)
559
+ .join("; ")}.`
560
+ ].join(" ")
561
+ : "Return only the final answer text."
562
+ ]
563
+ .filter(Boolean)
564
+ .join("\n\n");
565
+ const prompt = getConversationBasePrompt({
566
+ connector: input.connector,
567
+ node: input.node,
568
+ userInput: input.userInput,
569
+ upstream: input.upstream,
570
+ transcript
571
+ });
572
+ let rawText = "";
573
+ if (conversationAware && isOpenAiFamily(profile)) {
574
+ const result = await runOpenAiConversationPrompt({
575
+ profile,
576
+ apiKey,
577
+ systemPrompt,
578
+ prompt,
579
+ conversationId
580
+ });
581
+ rawText = result.text;
582
+ conversationId = result.conversationId;
583
+ }
584
+ else {
585
+ rawText = (await input.services.llm.runTextPrompt(profile, {
586
+ explicitApiKey: apiKey,
587
+ systemPrompt,
588
+ prompt
589
+ })).outputText.trim();
590
+ }
591
+ if (activeTools.length === 0) {
592
+ return {
593
+ text: rawText.trim(),
594
+ json: tryParseJsonObject(rawText.trim()),
595
+ conversationId,
596
+ logs: transcript
597
+ };
598
+ }
599
+ const structured = tryParseStructuredAgentResponse(rawText.trim());
600
+ if (!structured || structured.action === "final") {
601
+ return {
602
+ text: structured?.text?.trim() || rawText.trim(),
603
+ json: tryParseJsonObject(structured?.text?.trim() || rawText.trim()),
604
+ conversationId,
605
+ logs: transcript
606
+ };
607
+ }
608
+ const toolResult = structured.tool.startsWith("machine_")
609
+ ? await executeMachineTool(structured.tool, structured.args)
610
+ : await executeForgeBoxTool(activeTools.find((tool) => tool.key === structured.tool)?.boxId ?? "", structured.tool, structured.args, {
611
+ actor: {
612
+ userIds: null,
613
+ source: "agent"
614
+ }
615
+ });
616
+ transcript.push(`Tool call ${structured.tool}: ${JSON.stringify(structured.args)}`, `Tool result: ${JSON.stringify(toolResult)}`);
617
+ }
618
+ return {
619
+ text: "Connector stopped after reaching the maximum tool step count.",
620
+ json: null,
621
+ conversationId,
622
+ logs: transcript
623
+ };
624
+ }
625
+ function validateConnectorGraph(graph) {
626
+ const nodeIds = new Set(graph.nodes.map((node) => node.id));
627
+ for (const edge of graph.edges) {
628
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
629
+ throw new Error("Connector graph edge references a missing node.");
630
+ }
631
+ }
632
+ const adjacency = new Map();
633
+ for (const edge of graph.edges) {
634
+ const current = adjacency.get(edge.source) ?? [];
635
+ current.push(edge.target);
636
+ adjacency.set(edge.source, current);
637
+ }
638
+ const visiting = new Set();
639
+ const visited = new Set();
640
+ const visit = (nodeId) => {
641
+ if (visiting.has(nodeId)) {
642
+ throw new Error("Connector graphs cannot contain cycles.");
643
+ }
644
+ if (visited.has(nodeId)) {
645
+ return;
646
+ }
647
+ visiting.add(nodeId);
648
+ for (const target of adjacency.get(nodeId) ?? []) {
649
+ visit(target);
650
+ }
651
+ visiting.delete(nodeId);
652
+ visited.add(nodeId);
653
+ };
654
+ for (const node of graph.nodes) {
655
+ visit(node.id);
656
+ }
657
+ }
658
+ function buildOutputResult(connector, resolvedNodeValues) {
659
+ const outputs = Object.fromEntries(connector.publishedOutputs.map((output) => {
660
+ const nodeValue = resolvedNodeValues.get(output.nodeId);
661
+ return [
662
+ output.id,
663
+ {
664
+ label: output.label,
665
+ text: nodeValue?.text ?? "",
666
+ json: nodeValue?.json ?? null
667
+ }
668
+ ];
669
+ }));
670
+ const first = connector.publishedOutputs[0];
671
+ return aiConnectorRunResultSchema.parse({
672
+ primaryText: first ? outputs[first.id]?.text ?? "" : "",
673
+ outputs
674
+ });
675
+ }
676
+ function parseValueLiteral(valueType, valueLiteral) {
677
+ if (valueType === "null") {
678
+ return null;
679
+ }
680
+ if (valueType === "boolean") {
681
+ return valueLiteral.trim().toLowerCase() === "true";
682
+ }
683
+ if (valueType === "number") {
684
+ const parsed = Number(valueLiteral);
685
+ return Number.isFinite(parsed) ? parsed : 0;
686
+ }
687
+ if (valueType === "array" || valueType === "object") {
688
+ try {
689
+ return JSON.parse(valueLiteral || (valueType === "array" ? "[]" : "{}"));
690
+ }
691
+ catch {
692
+ return valueType === "array" ? [] : {};
693
+ }
694
+ }
695
+ return valueLiteral;
696
+ }
697
+ function createConversationRecord(input) {
698
+ const now = new Date().toISOString();
699
+ return saveAiConnectorConversation(aiConnectorConversationSchema.parse({
700
+ id: input.existing?.id ?? `aicv_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
701
+ connectorId: input.connectorId,
702
+ provider: input.provider,
703
+ externalConversationId: input.externalConversationId,
704
+ transcript: input.transcript,
705
+ createdAt: input.existing?.createdAt ?? now,
706
+ updatedAt: now
707
+ }));
708
+ }
709
+ async function executeConnector(connector, rawInput, services) {
710
+ validateConnectorGraph(connector.graph);
711
+ const parsedInput = runAiConnectorSchema.parse(rawInput);
712
+ const incoming = new Map();
713
+ for (const edge of connector.graph.edges) {
714
+ const list = incoming.get(edge.target) ?? [];
715
+ list.push(edge);
716
+ incoming.set(edge.target, list);
717
+ }
718
+ const values = new Map();
719
+ const debugNodes = [];
720
+ const debugErrors = [];
721
+ const outputNodes = connector.graph.nodes.filter((node) => node.type === "output");
722
+ const activeConversation = parsedInput.conversationId
723
+ ? getAiConnectorConversationById(parsedInput.conversationId)
724
+ : getAiConnectorConversationForConnector(connector.id);
725
+ const evaluateNode = async (nodeId) => {
726
+ const existing = values.get(nodeId);
727
+ if (existing) {
728
+ return existing;
729
+ }
730
+ const node = connector.graph.nodes.find((entry) => entry.id === nodeId);
731
+ if (!node) {
732
+ throw new Error(`Missing connector node ${nodeId}.`);
733
+ }
734
+ const upstreamEdges = incoming.get(nodeId) ?? [];
735
+ const upstream = await Promise.all(upstreamEdges.map(async (edge) => {
736
+ const upstreamValue = await evaluateNode(edge.source);
737
+ const selected = readOutputSelection(upstreamValue, edge.sourceHandle);
738
+ return {
739
+ edge,
740
+ sourceValue: upstreamValue,
741
+ selected
742
+ };
743
+ }));
744
+ let resolved;
745
+ if (node.type === "box" || node.type === "box_input") {
746
+ const boxId = node.data.boxId?.trim() || "";
747
+ const resolvedInputs = Object.fromEntries(upstream.map(({ edge, selected }, index) => [
748
+ edge.targetHandle ?? edge.sourceHandle ?? `input_${index + 1}`,
749
+ selected.json ?? selected.text
750
+ ]));
751
+ const resolvedParams = node.data.paramValues && typeof node.data.paramValues === "object"
752
+ ? node.data.paramValues
753
+ : {};
754
+ const providedSnapshot = boxId ? parsedInput.boxSnapshots[boxId] : null;
755
+ const snapshot = providedSnapshot && typeof providedSnapshot === "object"
756
+ ? {
757
+ ...resolveForgeBoxSnapshot(boxId, {
758
+ actor: {
759
+ userIds: null,
760
+ source: "agent"
761
+ }
762
+ }, {
763
+ inputs: resolvedInputs,
764
+ params: resolvedParams
765
+ }),
766
+ contentJson: providedSnapshot
767
+ }
768
+ : boxId
769
+ ? resolveForgeBoxSnapshot(boxId, {
770
+ actor: {
771
+ userIds: null,
772
+ source: "agent"
773
+ }
774
+ }, {
775
+ inputs: resolvedInputs,
776
+ params: resolvedParams
777
+ })
778
+ : {
779
+ boxId: "",
780
+ label: node.data.label,
781
+ capturedAt: new Date().toISOString(),
782
+ contentText: "No box is configured for this node yet.",
783
+ contentJson: null,
784
+ tools: []
785
+ };
786
+ const outputKeys = [
787
+ ...(node.data.outputs ?? []).map((port) => port.key),
788
+ ...Object.keys(snapshot.contentJson ?? {})
789
+ ];
790
+ resolved = {
791
+ text: snapshot.contentText,
792
+ json: snapshot.contentJson,
793
+ tools: snapshot.tools.map((tool) => ({
794
+ boxId: snapshot.boxId,
795
+ key: tool.key,
796
+ label: tool.label,
797
+ description: tool.description
798
+ })),
799
+ conversationId: null,
800
+ outputMap: buildOutputMap(snapshot.contentText, snapshot.contentJson, outputKeys),
801
+ logs: []
802
+ };
803
+ }
804
+ else if (node.type === "value") {
805
+ const parsedValue = parseValueLiteral(node.data.valueType ?? "string", node.data.valueLiteral ?? "");
806
+ const jsonValue = parsedValue && typeof parsedValue === "object" && !Array.isArray(parsedValue)
807
+ ? parsedValue
808
+ : null;
809
+ const textValue = parsedValue === null
810
+ ? "null"
811
+ : typeof parsedValue === "string"
812
+ ? parsedValue
813
+ : JSON.stringify(parsedValue, null, 2);
814
+ resolved = {
815
+ text: textValue,
816
+ json: jsonValue,
817
+ tools: [],
818
+ conversationId: null,
819
+ outputMap: buildOutputMap(textValue, jsonValue, ["primary"]),
820
+ logs: []
821
+ };
822
+ }
823
+ else if (node.type === "user_input") {
824
+ resolved = {
825
+ text: parsedInput.userInput || "",
826
+ json: Object.keys(parsedInput.context).length > 0 ? parsedInput.context : null,
827
+ tools: [],
828
+ conversationId: activeConversation?.id ?? null,
829
+ outputMap: buildOutputMap(parsedInput.userInput || "", Object.keys(parsedInput.context).length > 0 ? parsedInput.context : null, Object.keys(parsedInput.context ?? {})),
830
+ logs: []
831
+ };
832
+ }
833
+ else if (node.type === "merge") {
834
+ const mergedText = upstream
835
+ .map((entry) => entry.selected.text)
836
+ .filter(Boolean)
837
+ .join("\n\n");
838
+ const mergedJson = Object.assign({}, ...upstream
839
+ .map((entry) => entry.selected.json)
840
+ .filter((entry) => Boolean(entry) && typeof entry === "object"));
841
+ resolved = {
842
+ text: mergedText,
843
+ json: Object.keys(mergedJson).length > 0 ? mergedJson : null,
844
+ tools: upstream.flatMap((entry) => entry.sourceValue.tools),
845
+ conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
846
+ .conversationId ?? null,
847
+ outputMap: buildOutputMap(mergedText, Object.keys(mergedJson).length > 0 ? mergedJson : null, Object.keys(mergedJson)),
848
+ logs: []
849
+ };
850
+ }
851
+ else if (node.type === "template") {
852
+ const primary = upstream[0]?.selected ?? { text: "", json: null };
853
+ const rendered = (node.data.template ?? node.data.promptTemplate ?? "")
854
+ .replaceAll("{{input}}", primary.text)
855
+ .replaceAll("{{json}}", primary.json ? JSON.stringify(primary.json) : "");
856
+ resolved = {
857
+ text: rendered,
858
+ json: tryParseJsonObject(rendered),
859
+ tools: [],
860
+ conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
861
+ .conversationId ?? null,
862
+ outputMap: buildOutputMap(rendered, tryParseJsonObject(rendered)),
863
+ logs: []
864
+ };
865
+ }
866
+ else if (node.type === "pick_key") {
867
+ const primary = upstream[0]?.selected ?? { text: "", json: null };
868
+ const selectedKey = node.data.selectedKey?.trim() || "";
869
+ const selectedValue = primary.json && selectedKey in primary.json ? primary.json[selectedKey] : null;
870
+ const selectedJson = selectedValue &&
871
+ typeof selectedValue === "object" &&
872
+ !Array.isArray(selectedValue)
873
+ ? selectedValue
874
+ : null;
875
+ resolved = {
876
+ text: coerceText(selectedValue),
877
+ json: selectedJson,
878
+ tools: [],
879
+ conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
880
+ .conversationId ?? null,
881
+ outputMap: buildOutputMap(coerceText(selectedValue), selectedJson),
882
+ logs: []
883
+ };
884
+ }
885
+ else if (node.type === "output") {
886
+ const mergedText = upstream
887
+ .map((entry) => entry.selected.text)
888
+ .filter(Boolean)
889
+ .join("\n\n");
890
+ resolved = {
891
+ text: mergedText,
892
+ json: upstream[0]?.selected.json ?? null,
893
+ tools: [],
894
+ conversationId: upstream.find((entry) => entry.sourceValue.conversationId)?.sourceValue
895
+ .conversationId ?? null,
896
+ outputMap: buildOutputMap(mergedText, upstream[0]?.selected.json ?? null, Object.keys(upstream[0]?.selected.json ?? {})),
897
+ logs: []
898
+ };
899
+ }
900
+ else {
901
+ const modelResult = await runModelNode({
902
+ connector,
903
+ node,
904
+ userInput: parsedInput.userInput,
905
+ upstream: upstream.map((entry) => ({
906
+ text: entry.selected.text,
907
+ json: entry.selected.json,
908
+ tools: entry.sourceValue.tools,
909
+ conversationId: entry.sourceValue.conversationId,
910
+ outputMap: entry.sourceValue.outputMap,
911
+ logs: entry.sourceValue.logs
912
+ })),
913
+ services,
914
+ conversation: activeConversation
915
+ });
916
+ const outputKeys = (node.data.outputs ?? []).map((port) => port.key);
917
+ resolved = {
918
+ text: modelResult.text,
919
+ json: modelResult.json,
920
+ tools: [],
921
+ conversationId: modelResult.conversationId,
922
+ outputMap: buildOutputMap(modelResult.text, modelResult.json, outputKeys),
923
+ logs: modelResult.logs
924
+ };
925
+ }
926
+ values.set(nodeId, resolved);
927
+ debugNodes.push({
928
+ nodeId: node.id,
929
+ nodeType: node.type,
930
+ label: node.data.label,
931
+ input: upstream.map((entry) => ({
932
+ sourceNodeId: entry.edge.source,
933
+ sourceHandle: entry.edge.sourceHandle ?? null,
934
+ targetHandle: entry.edge.targetHandle ?? null,
935
+ text: entry.selected.text,
936
+ json: entry.selected.json
937
+ })),
938
+ output: {
939
+ text: resolved.text,
940
+ json: resolved.json
941
+ },
942
+ tools: resolved.tools.map((tool) => tool.key),
943
+ logs: resolved.logs,
944
+ error: null
945
+ });
946
+ return resolved;
947
+ };
948
+ try {
949
+ for (const outputNode of outputNodes) {
950
+ await evaluateNode(outputNode.id);
951
+ }
952
+ }
953
+ catch (error) {
954
+ debugErrors.push(error instanceof Error ? error.message : "Flow execution failed");
955
+ throw error;
956
+ }
957
+ const result = aiConnectorRunResultSchema.parse({
958
+ ...buildOutputResult(connector, values),
959
+ debugTrace: parsedInput.debug
960
+ ? {
961
+ nodes: debugNodes,
962
+ errors: debugErrors
963
+ }
964
+ : undefined
965
+ });
966
+ const conversationProviderNode = connector.graph.nodes.find((node) => node.type === "chat");
967
+ const resolvedConversationId = [...values.values()].find((entry) => entry.conversationId)?.conversationId ?? null;
968
+ const nextConversation = conversationProviderNode
969
+ ? createConversationRecord({
970
+ connectorId: connector.id,
971
+ provider: conversationProviderNode.data.modelConfig?.provider ?? null,
972
+ externalConversationId: conversationProviderNode.data.modelConfig?.provider &&
973
+ isOpenAiFamily({
974
+ provider: conversationProviderNode.data.modelConfig.provider,
975
+ baseUrl: conversationProviderNode.data.modelConfig.baseUrl ?? DEFAULT_OPENAI_BASE_URL,
976
+ model: conversationProviderNode.data.modelConfig.model,
977
+ systemPrompt: "",
978
+ secretId: null,
979
+ metadata: {}
980
+ })
981
+ ? resolvedConversationId
982
+ : null,
983
+ transcript: [
984
+ ...(activeConversation?.transcript ?? []),
985
+ ...(parsedInput.userInput
986
+ ? [
987
+ {
988
+ role: "user",
989
+ text: parsedInput.userInput,
990
+ createdAt: new Date().toISOString()
991
+ }
992
+ ]
993
+ : []),
994
+ {
995
+ role: "assistant",
996
+ text: result.primaryText,
997
+ createdAt: new Date().toISOString()
998
+ }
999
+ ],
1000
+ existing: activeConversation
1001
+ })
1002
+ : null;
1003
+ return {
1004
+ result,
1005
+ conversation: nextConversation
1006
+ };
1007
+ }
1008
+ function migrateLegacyProcessor(processorId) {
1009
+ const processor = getAiProcessorById(processorId);
1010
+ if (!processor) {
1011
+ return null;
1012
+ }
1013
+ const existing = getDatabase()
1014
+ .prepare(`SELECT * FROM ai_connectors WHERE legacy_processor_id = ?`)
1015
+ .get(processorId);
1016
+ if (existing) {
1017
+ return mapConnector(existing);
1018
+ }
1019
+ const sourceLinks = listAiProcessorLinks(processor.surfaceId).filter((link) => link.targetProcessorId === processor.id);
1020
+ const inputNodes = sourceLinks.map((link, index) => ({
1021
+ id: `legacy_input_${index + 1}`,
1022
+ type: "box_input",
1023
+ position: { x: 60, y: 80 + index * 120 },
1024
+ data: {
1025
+ label: `Legacy input ${index + 1}`,
1026
+ description: `Imported from ${link.sourceWidgetId}`,
1027
+ boxId: `legacy:${link.sourceWidgetId}`,
1028
+ enabledToolKeys: []
1029
+ }
1030
+ }));
1031
+ const modelNode = {
1032
+ id: "legacy_functor",
1033
+ type: "functor",
1034
+ position: { x: 360, y: 160 },
1035
+ data: {
1036
+ label: processor.title,
1037
+ description: "Imported from a legacy AI processor.",
1038
+ prompt: processor.promptFlow,
1039
+ systemPrompt: processor.contextInput,
1040
+ enabledToolKeys: processor.toolConfig.map((tool) => tool.key),
1041
+ modelConfig: {
1042
+ connectionId: processor.agentConfigs[0]?.connectionId ?? null,
1043
+ provider: null,
1044
+ baseUrl: null,
1045
+ model: processor.agentConfigs[0]?.model ?? "",
1046
+ thinking: null,
1047
+ verbosity: null
1048
+ }
1049
+ }
1050
+ };
1051
+ const outputNode = {
1052
+ id: "legacy_output",
1053
+ type: "output",
1054
+ position: { x: 700, y: 160 },
1055
+ data: {
1056
+ label: "Output",
1057
+ description: "Imported legacy output.",
1058
+ outputKey: "primary",
1059
+ enabledToolKeys: []
1060
+ }
1061
+ };
1062
+ const graph = {
1063
+ nodes: [...inputNodes, modelNode, outputNode],
1064
+ edges: [
1065
+ ...inputNodes.map((node, index) => ({
1066
+ id: `legacy_edge_input_${index + 1}`,
1067
+ source: node.id,
1068
+ target: modelNode.id
1069
+ })),
1070
+ {
1071
+ id: "legacy_edge_output",
1072
+ source: modelNode.id,
1073
+ target: outputNode.id
1074
+ }
1075
+ ]
1076
+ };
1077
+ return createAiConnector({
1078
+ title: processor.title,
1079
+ description: "Migrated from a legacy AI processor.",
1080
+ kind: "functor",
1081
+ homeSurfaceId: processor.surfaceId,
1082
+ endpointEnabled: processor.endpointEnabled,
1083
+ graph,
1084
+ legacyProcessorId: processor.id
1085
+ });
1086
+ }
1087
+ export function ensureLegacyProcessorsMigrated() {
1088
+ for (const processor of listAiProcessors()) {
1089
+ migrateLegacyProcessor(processor.id);
1090
+ }
1091
+ }
1092
+ export function listAiConnectors() {
1093
+ ensureLegacyProcessorsMigrated();
1094
+ const rows = getDatabase()
1095
+ .prepare(`SELECT * FROM ai_connectors ORDER BY created_at ASC`)
1096
+ .all();
1097
+ return rows.map(mapConnector);
1098
+ }
1099
+ export function getAiConnectorById(connectorId) {
1100
+ ensureLegacyProcessorsMigrated();
1101
+ const row = getDatabase()
1102
+ .prepare(`SELECT * FROM ai_connectors WHERE id = ?`)
1103
+ .get(connectorId);
1104
+ return row ? mapConnector(row) : null;
1105
+ }
1106
+ export function getAiConnectorBySlug(slug) {
1107
+ ensureLegacyProcessorsMigrated();
1108
+ const row = getDatabase()
1109
+ .prepare(`SELECT * FROM ai_connectors WHERE slug = ?`)
1110
+ .get(slug);
1111
+ return row ? mapConnector(row) : null;
1112
+ }
1113
+ export function createAiConnector(input) {
1114
+ const parsed = createAiConnectorSchema.parse(input);
1115
+ const now = new Date().toISOString();
1116
+ const id = `aic_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
1117
+ const slug = buildConnectorSlug(parsed.title, id);
1118
+ const graph = parsed.graph.nodes.length > 0 ? parsed.graph : buildDefaultGraph(parsed.kind, parsed.title);
1119
+ const publishedOutputs = ensurePublishedOutputs(id, graph);
1120
+ getDatabase()
1121
+ .prepare(`INSERT INTO ai_connectors (
1122
+ 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
1123
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1124
+ .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);
1125
+ return getAiConnectorById(id);
1126
+ }
1127
+ export function updateAiConnector(connectorId, patch) {
1128
+ const current = getAiConnectorById(connectorId);
1129
+ if (!current) {
1130
+ return null;
1131
+ }
1132
+ const parsed = updateAiConnectorSchema.parse(patch);
1133
+ const nextGraph = parsed.graph ?? current.graph;
1134
+ validateConnectorGraph(nextGraph);
1135
+ const nextTitle = parsed.title ?? current.title;
1136
+ const next = {
1137
+ ...current,
1138
+ ...parsed,
1139
+ title: nextTitle,
1140
+ slug: parsed.title && parsed.title !== current.title
1141
+ ? buildConnectorSlug(parsed.title, current.id)
1142
+ : current.slug,
1143
+ graph: nextGraph,
1144
+ publishedOutputs: ensurePublishedOutputs(current.id, nextGraph)
1145
+ };
1146
+ const now = new Date().toISOString();
1147
+ getDatabase()
1148
+ .prepare(`UPDATE ai_connectors
1149
+ SET slug = ?, title = ?, description = ?, kind = ?, home_surface_id = ?, endpoint_enabled = ?, graph_json = ?, published_outputs_json = ?, updated_at = ?
1150
+ WHERE id = ?`)
1151
+ .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);
1152
+ return getAiConnectorById(connectorId);
1153
+ }
1154
+ export function deleteAiConnector(connectorId) {
1155
+ const current = getAiConnectorById(connectorId);
1156
+ if (!current) {
1157
+ return null;
1158
+ }
1159
+ getDatabase().prepare(`DELETE FROM ai_connectors WHERE id = ?`).run(connectorId);
1160
+ return current;
1161
+ }
1162
+ export async function runAiConnector(connectorId, input, services, mode = "run") {
1163
+ const connector = getAiConnectorById(connectorId);
1164
+ if (!connector) {
1165
+ throw new Error(`Connector ${connectorId} was not found.`);
1166
+ }
1167
+ const pendingRun = aiConnectorRunSchema.parse({
1168
+ id: `aicr_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
1169
+ connectorId,
1170
+ mode,
1171
+ status: "running",
1172
+ userInput: input.userInput ?? "",
1173
+ context: input.context ?? {},
1174
+ conversationId: input.conversationId ?? null,
1175
+ result: null,
1176
+ error: null,
1177
+ createdAt: new Date().toISOString(),
1178
+ completedAt: null
1179
+ });
1180
+ insertRun(pendingRun);
1181
+ try {
1182
+ const execution = await executeConnector(connector, input, services);
1183
+ const completedRun = aiConnectorRunSchema.parse({
1184
+ ...pendingRun,
1185
+ status: "completed",
1186
+ result: execution.result,
1187
+ conversationId: execution.conversation?.id ?? pendingRun.conversationId,
1188
+ completedAt: new Date().toISOString()
1189
+ });
1190
+ insertRun(completedRun);
1191
+ return {
1192
+ connector: getAiConnectorById(connectorId),
1193
+ run: completedRun,
1194
+ conversation: execution.conversation
1195
+ };
1196
+ }
1197
+ catch (error) {
1198
+ const failedRun = aiConnectorRunSchema.parse({
1199
+ ...pendingRun,
1200
+ status: "failed",
1201
+ error: error instanceof Error ? error.message : "Connector run failed",
1202
+ completedAt: new Date().toISOString()
1203
+ });
1204
+ insertRun(failedRun);
1205
+ throw error;
1206
+ }
1207
+ }