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,547 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import { execFile as execFileCallback } from "node:child_process";
5
+ import path from "node:path";
6
+ import { getDatabase } from "../db.js";
7
+ import { aiProcessorLinkSchema, aiProcessorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiProcessorSchema, surfaceProcessorGraphPayloadSchema, updateAiProcessorSchema } from "../types.js";
8
+ import { FORGE_DEFAULT_AGENT_ID, getAiModelConnectionById, listAiModelConnections, readModelConnectionCredential } from "./model-settings.js";
9
+ import { getSettings } from "./settings.js";
10
+ const MAX_RUN_HISTORY = 12;
11
+ const MAX_TOOL_STEPS = 6;
12
+ const execFile = promisify(execFileCallback);
13
+ function parseJson(value, fallback) {
14
+ try {
15
+ return value ? JSON.parse(value) : fallback;
16
+ }
17
+ catch {
18
+ return fallback;
19
+ }
20
+ }
21
+ function slugifySegment(value) {
22
+ const normalized = value
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, "-")
26
+ .replace(/^-+|-+$/g, "");
27
+ return normalized || "processor";
28
+ }
29
+ function buildProcessorSlug(title, id) {
30
+ return `${slugifySegment(title)}-${id.slice(-6)}`;
31
+ }
32
+ function processorWidgetId(processorId) {
33
+ return `aiproc:${processorId}`;
34
+ }
35
+ function processorIdFromNodeId(nodeId) {
36
+ return nodeId.startsWith("aiproc:") ? nodeId.slice("aiproc:".length) : null;
37
+ }
38
+ function resolveAllowedPath(inputPath) {
39
+ const candidate = path.resolve(process.cwd(), inputPath);
40
+ const workspaceRoot = process.cwd();
41
+ if (candidate !== workspaceRoot &&
42
+ !candidate.startsWith(`${workspaceRoot}${path.sep}`)) {
43
+ throw new Error("Machine access is restricted to the Forge workspace root.");
44
+ }
45
+ return candidate;
46
+ }
47
+ function tryParseStructuredAgentResponse(value) {
48
+ try {
49
+ return JSON.parse(value);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function executeMachineTool(processor, tool, args) {
56
+ if (tool === "machine_read_file") {
57
+ if (!processor.machineAccess.read) {
58
+ throw new Error("Read access is disabled for this processor.");
59
+ }
60
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
61
+ if (!targetPath) {
62
+ throw new Error("machine_read_file requires a string path.");
63
+ }
64
+ const content = await readFile(targetPath, "utf8");
65
+ return {
66
+ path: targetPath,
67
+ content
68
+ };
69
+ }
70
+ if (tool === "machine_write_file") {
71
+ if (!processor.machineAccess.write) {
72
+ throw new Error("Write access is disabled for this processor.");
73
+ }
74
+ const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
75
+ if (!targetPath) {
76
+ throw new Error("machine_write_file requires a string path.");
77
+ }
78
+ if (typeof args.content !== "string") {
79
+ throw new Error("machine_write_file requires string content.");
80
+ }
81
+ await writeFile(targetPath, args.content, "utf8");
82
+ return {
83
+ path: targetPath,
84
+ bytesWritten: Buffer.byteLength(args.content, "utf8")
85
+ };
86
+ }
87
+ if (!processor.machineAccess.exec) {
88
+ throw new Error("Exec access is disabled for this processor.");
89
+ }
90
+ if (typeof args.command !== "string" || args.command.trim().length === 0) {
91
+ throw new Error("machine_exec requires a command string.");
92
+ }
93
+ const cwd = typeof args.cwd === "string" && args.cwd.trim().length > 0
94
+ ? resolveAllowedPath(args.cwd)
95
+ : process.cwd();
96
+ const result = await execFile("zsh", ["-lc", args.command], {
97
+ cwd,
98
+ timeout: 15_000,
99
+ maxBuffer: 256_000
100
+ });
101
+ return {
102
+ cwd,
103
+ stdout: result.stdout.trim(),
104
+ stderr: result.stderr.trim()
105
+ };
106
+ }
107
+ async function runProcessorAgent(processor, agent, fullPrompt, services) {
108
+ if (!agent.profile) {
109
+ return "No model connection is configured for this agent yet.";
110
+ }
111
+ const toolNames = [
112
+ processor.machineAccess.read ? "machine_read_file(path)" : null,
113
+ processor.machineAccess.write
114
+ ? "machine_write_file(path, content)"
115
+ : null,
116
+ processor.machineAccess.exec ? "machine_exec(command, cwd?)" : null
117
+ ].filter(Boolean);
118
+ if (toolNames.length === 0) {
119
+ const result = await services.llm.runTextPrompt(agent.profile, {
120
+ explicitApiKey: agent.explicitApiKey,
121
+ systemPrompt: "You are an AI processor inside Forge. Follow the prompt flow exactly, use the linked context carefully, and return only the final output for your assigned agent.",
122
+ prompt: fullPrompt
123
+ });
124
+ return result.outputText.trim();
125
+ }
126
+ const transcript = [];
127
+ for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
128
+ const result = await services.llm.runTextPrompt(agent.profile, {
129
+ explicitApiKey: agent.explicitApiKey,
130
+ systemPrompt: [
131
+ "You are an AI processor inside Forge.",
132
+ "You may use machine tools when they are enabled.",
133
+ `Available tools: ${toolNames.join(", ")}.`,
134
+ "Return strict JSON only.",
135
+ 'For a final answer, return {"action":"final","text":"..."}',
136
+ 'To call a tool, return {"action":"tool","tool":"machine_exec","args":{...}}'
137
+ ].join(" "),
138
+ prompt: [
139
+ fullPrompt,
140
+ transcript.length > 0
141
+ ? `Tool transcript:\n${transcript.join("\n\n")}`
142
+ : ""
143
+ ]
144
+ .filter(Boolean)
145
+ .join("\n\n")
146
+ });
147
+ const structured = tryParseStructuredAgentResponse(result.outputText.trim());
148
+ if (!structured || structured.action === "final") {
149
+ return structured?.text?.trim() || result.outputText.trim();
150
+ }
151
+ const toolResult = await executeMachineTool(processor, structured.tool, structured.args);
152
+ transcript.push(`Tool call ${structured.tool}: ${JSON.stringify(structured.args)}`, `Tool result: ${JSON.stringify(toolResult)}`);
153
+ }
154
+ return "Processor stopped after reaching the maximum tool step count.";
155
+ }
156
+ function mapProcessor(row) {
157
+ return aiProcessorSchema.parse({
158
+ id: row.id,
159
+ slug: row.slug,
160
+ surfaceId: row.surface_id,
161
+ title: row.title,
162
+ promptFlow: row.prompt_flow,
163
+ contextInput: row.context_input,
164
+ toolConfig: parseJson(row.tool_config_json, []),
165
+ agentIds: parseJson(row.agent_ids_json, []),
166
+ agentConfigs: parseJson(row.agent_config_json, []),
167
+ triggerMode: row.trigger_mode,
168
+ cronExpression: row.cron_expression,
169
+ machineAccess: parseJson(row.machine_access_json, {
170
+ read: false,
171
+ write: false,
172
+ exec: false
173
+ }),
174
+ endpointEnabled: row.endpoint_enabled === 1,
175
+ lastRunAt: row.last_run_at,
176
+ lastRunStatus: row.last_run_status,
177
+ lastRunOutput: parseJson(row.last_run_output_json, null),
178
+ runHistory: parseJson(row.run_history_json, []),
179
+ createdAt: row.created_at,
180
+ updatedAt: row.updated_at
181
+ });
182
+ }
183
+ function mapLink(row) {
184
+ return aiProcessorLinkSchema.parse({
185
+ id: row.id,
186
+ surfaceId: row.surface_id,
187
+ sourceWidgetId: row.source_widget_id,
188
+ targetProcessorId: row.target_processor_id,
189
+ accessMode: row.access_mode,
190
+ capabilityMode: row.capability_mode,
191
+ metadata: parseJson(row.metadata_json, {}),
192
+ createdAt: row.created_at,
193
+ updatedAt: row.updated_at
194
+ });
195
+ }
196
+ export function listAiProcessors(surfaceId) {
197
+ const rows = surfaceId
198
+ ? (getDatabase()
199
+ .prepare(`SELECT * FROM ai_processors WHERE surface_id = ? ORDER BY created_at ASC`)
200
+ .all(surfaceId) ?? [])
201
+ : (getDatabase()
202
+ .prepare(`SELECT * FROM ai_processors ORDER BY created_at ASC`)
203
+ .all() ?? []);
204
+ return rows.map(mapProcessor);
205
+ }
206
+ export function getAiProcessorById(processorId) {
207
+ const row = getDatabase()
208
+ .prepare(`SELECT * FROM ai_processors WHERE id = ?`)
209
+ .get(processorId);
210
+ return row ? mapProcessor(row) : null;
211
+ }
212
+ export function getAiProcessorBySlug(slug) {
213
+ const row = getDatabase()
214
+ .prepare(`SELECT * FROM ai_processors WHERE slug = ?`)
215
+ .get(slug);
216
+ return row ? mapProcessor(row) : null;
217
+ }
218
+ export function listAiProcessorLinks(surfaceId) {
219
+ const rows = surfaceId
220
+ ? (getDatabase()
221
+ .prepare(`SELECT * FROM ai_processor_links WHERE surface_id = ? ORDER BY created_at ASC`)
222
+ .all(surfaceId) ?? [])
223
+ : (getDatabase()
224
+ .prepare(`SELECT * FROM ai_processor_links ORDER BY created_at ASC`)
225
+ .all() ?? []);
226
+ return rows.map(mapLink);
227
+ }
228
+ export function getSurfaceProcessorGraph(surfaceId) {
229
+ return surfaceProcessorGraphPayloadSchema.parse({
230
+ surfaceId,
231
+ processors: listAiProcessors(surfaceId),
232
+ links: listAiProcessorLinks(surfaceId)
233
+ });
234
+ }
235
+ export function createAiProcessor(input) {
236
+ const parsed = createAiProcessorSchema.parse(input);
237
+ const now = new Date().toISOString();
238
+ const id = `aip_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
239
+ const slug = buildProcessorSlug(parsed.title, id);
240
+ getDatabase()
241
+ .prepare(`INSERT INTO ai_processors (
242
+ id, slug, surface_id, title, prompt_flow, context_input, tool_config_json, agent_ids_json, agent_config_json, trigger_mode, cron_expression, machine_access_json, endpoint_enabled, last_run_at, last_run_status, last_run_output_json, run_history_json, created_at, updated_at
243
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
244
+ .run(id, slug, parsed.surfaceId, parsed.title, parsed.promptFlow, parsed.contextInput, JSON.stringify(parsed.toolConfig), JSON.stringify(parsed.agentIds), JSON.stringify(parsed.agentConfigs), parsed.triggerMode, parsed.cronExpression, JSON.stringify(parsed.machineAccess), parsed.endpointEnabled ? 1 : 0, null, "idle", null, "[]", now, now);
245
+ return getAiProcessorById(id);
246
+ }
247
+ export function updateAiProcessor(processorId, patch) {
248
+ const current = getAiProcessorById(processorId);
249
+ if (!current) {
250
+ return null;
251
+ }
252
+ const parsed = updateAiProcessorSchema.parse(patch);
253
+ const next = {
254
+ ...current,
255
+ ...parsed,
256
+ slug: parsed.title && parsed.title !== current.title
257
+ ? buildProcessorSlug(parsed.title, current.id)
258
+ : current.slug,
259
+ machineAccess: {
260
+ ...current.machineAccess,
261
+ ...(parsed.machineAccess ?? {})
262
+ }
263
+ };
264
+ const now = new Date().toISOString();
265
+ getDatabase()
266
+ .prepare(`UPDATE ai_processors
267
+ SET slug = ?, title = ?, prompt_flow = ?, context_input = ?, tool_config_json = ?, agent_ids_json = ?, agent_config_json = ?, trigger_mode = ?, cron_expression = ?, machine_access_json = ?, endpoint_enabled = ?, updated_at = ?
268
+ WHERE id = ?`)
269
+ .run(next.slug, next.title, next.promptFlow, next.contextInput, JSON.stringify(next.toolConfig), JSON.stringify(next.agentIds), JSON.stringify(next.agentConfigs), next.triggerMode, next.cronExpression, JSON.stringify(next.machineAccess), next.endpointEnabled ? 1 : 0, now, processorId);
270
+ return getAiProcessorById(processorId);
271
+ }
272
+ export function deleteAiProcessor(processorId) {
273
+ const current = getAiProcessorById(processorId);
274
+ if (!current) {
275
+ return null;
276
+ }
277
+ getDatabase().prepare(`DELETE FROM ai_processors WHERE id = ?`).run(processorId);
278
+ return current;
279
+ }
280
+ function assertProcessorGraphEdgeIsValid(input) {
281
+ const sourceProcessorId = processorIdFromNodeId(input.sourceWidgetId);
282
+ if (!sourceProcessorId) {
283
+ return;
284
+ }
285
+ if (sourceProcessorId === input.targetProcessorId) {
286
+ throw new Error("AI processor links cannot point a processor to itself.");
287
+ }
288
+ const links = listAiProcessorLinks(input.surfaceId);
289
+ const adjacency = new Map();
290
+ for (const link of links) {
291
+ const upstreamProcessorId = processorIdFromNodeId(link.sourceWidgetId);
292
+ if (!upstreamProcessorId) {
293
+ continue;
294
+ }
295
+ const current = adjacency.get(upstreamProcessorId) ?? new Set();
296
+ current.add(link.targetProcessorId);
297
+ adjacency.set(upstreamProcessorId, current);
298
+ }
299
+ const nextTargets = adjacency.get(sourceProcessorId) ?? new Set();
300
+ nextTargets.add(input.targetProcessorId);
301
+ adjacency.set(sourceProcessorId, nextTargets);
302
+ const seen = new Set();
303
+ const stack = [input.targetProcessorId];
304
+ while (stack.length > 0) {
305
+ const current = stack.pop();
306
+ if (current === sourceProcessorId) {
307
+ throw new Error("This link would create a processor cycle.");
308
+ }
309
+ if (seen.has(current)) {
310
+ continue;
311
+ }
312
+ seen.add(current);
313
+ for (const next of adjacency.get(current) ?? []) {
314
+ stack.push(next);
315
+ }
316
+ }
317
+ }
318
+ export function createAiProcessorLink(input) {
319
+ const parsed = createAiProcessorLinkSchema.parse(input);
320
+ assertProcessorGraphEdgeIsValid(parsed);
321
+ const existing = listAiProcessorLinks(parsed.surfaceId).find((link) => link.sourceWidgetId === parsed.sourceWidgetId &&
322
+ link.targetProcessorId === parsed.targetProcessorId);
323
+ const now = new Date().toISOString();
324
+ const id = existing?.id ?? `ail_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
325
+ getDatabase()
326
+ .prepare(`INSERT INTO ai_processor_links (
327
+ id, surface_id, source_widget_id, target_processor_id, access_mode, capability_mode, metadata_json, created_at, updated_at
328
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
329
+ ON CONFLICT(id) DO UPDATE SET
330
+ access_mode = excluded.access_mode,
331
+ capability_mode = excluded.capability_mode,
332
+ metadata_json = excluded.metadata_json,
333
+ updated_at = excluded.updated_at`)
334
+ .run(id, parsed.surfaceId, parsed.sourceWidgetId, parsed.targetProcessorId, parsed.accessMode, parsed.capabilityMode, JSON.stringify(parsed.metadata), existing?.createdAt ?? now, now);
335
+ return listAiProcessorLinks(parsed.surfaceId).find((entry) => entry.id === id);
336
+ }
337
+ export function deleteAiProcessorLink(linkId) {
338
+ const existing = listAiProcessorLinks().find((entry) => entry.id === linkId);
339
+ if (!existing) {
340
+ return null;
341
+ }
342
+ getDatabase().prepare(`DELETE FROM ai_processor_links WHERE id = ?`).run(linkId);
343
+ return existing;
344
+ }
345
+ function resolveProcessorAgentProfiles(processor, secrets) {
346
+ const allConnections = listAiModelConnections();
347
+ const requestedAgentIds = processor.agentIds.length > 0 ? processor.agentIds : [FORGE_DEFAULT_AGENT_ID];
348
+ const configByAgentId = new Map(processor.agentConfigs.map((config) => [config.agentId, config]));
349
+ const settings = getSettings();
350
+ return requestedAgentIds.map((agentId) => {
351
+ const override = configByAgentId.get(agentId) ?? null;
352
+ let connection = (override?.connectionId
353
+ ? getAiModelConnectionById(override.connectionId)
354
+ : null) ??
355
+ allConnections.find((entry) => entry.agentId === agentId) ??
356
+ null;
357
+ if (agentId === FORGE_DEFAULT_AGENT_ID) {
358
+ const selected = settings.modelSettings.forgeAgent.basicChat.connectionId;
359
+ connection =
360
+ (override?.connectionId ? connection : null) ??
361
+ (selected ? getAiModelConnectionById(selected) : null);
362
+ }
363
+ if (!connection) {
364
+ return {
365
+ agentId,
366
+ agentLabel: agentId === FORGE_DEFAULT_AGENT_ID ? "Forge Agent" : agentId,
367
+ profile: null,
368
+ explicitApiKey: null
369
+ };
370
+ }
371
+ const credential = readModelConnectionCredential(connection.id, secrets);
372
+ const explicitApiKey = credential?.kind === "api_key"
373
+ ? credential.apiKey
374
+ : credential?.kind === "oauth"
375
+ ? credential.access
376
+ : null;
377
+ return {
378
+ agentId,
379
+ agentLabel: agentId === FORGE_DEFAULT_AGENT_ID
380
+ ? "Forge Agent"
381
+ : connection.agentLabel,
382
+ profile: {
383
+ provider: connection.provider,
384
+ baseUrl: connection.baseUrl,
385
+ model: override?.model?.trim() || connection.model,
386
+ systemPrompt: "",
387
+ secretId: null,
388
+ metadata: {}
389
+ },
390
+ explicitApiKey
391
+ };
392
+ });
393
+ }
394
+ function writeProcessorRunState(processor, input) {
395
+ const nextHistory = [
396
+ input.runEntry,
397
+ ...processor.runHistory.filter((entry) => entry.id !== input.runEntry.id)
398
+ ].slice(0, MAX_RUN_HISTORY);
399
+ getDatabase()
400
+ .prepare(`UPDATE ai_processors
401
+ SET last_run_at = ?, last_run_status = ?, last_run_output_json = ?, run_history_json = ?, updated_at = ?
402
+ WHERE id = ?`)
403
+ .run(input.lastRunAt, input.lastRunStatus, input.lastRunOutput ? JSON.stringify(input.lastRunOutput) : null, JSON.stringify(nextHistory), new Date().toISOString(), processor.id);
404
+ }
405
+ async function executeAiProcessor(processorId, input, services, state) {
406
+ if (state.cache.has(processorId)) {
407
+ return state.cache.get(processorId);
408
+ }
409
+ if (state.active.has(processorId)) {
410
+ throw new Error("Processor graph contains a cycle.");
411
+ }
412
+ const processor = getAiProcessorById(processorId);
413
+ if (!processor) {
414
+ throw new Error("AI processor not found.");
415
+ }
416
+ state.active.add(processorId);
417
+ const parsed = runAiProcessorSchema.parse(input);
418
+ const runEntryId = `air_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
419
+ const runStartedAt = new Date().toISOString();
420
+ writeProcessorRunState(processor, {
421
+ lastRunAt: runStartedAt,
422
+ lastRunStatus: "running",
423
+ lastRunOutput: processor.lastRunOutput,
424
+ runEntry: {
425
+ id: runEntryId,
426
+ trigger: state.trigger,
427
+ startedAt: runStartedAt,
428
+ completedAt: null,
429
+ status: "running",
430
+ input: parsed.input,
431
+ output: null,
432
+ error: null
433
+ }
434
+ });
435
+ try {
436
+ const links = listAiProcessorLinks(processor.surfaceId).filter((link) => link.targetProcessorId === processor.id);
437
+ const upstreamOutputs = [];
438
+ const linkedContext = [];
439
+ for (const link of links) {
440
+ const sourceProcessorId = processorIdFromNodeId(link.sourceWidgetId);
441
+ if (sourceProcessorId) {
442
+ const upstream = await executeAiProcessor(sourceProcessorId, {
443
+ input: parsed.input,
444
+ context: parsed.context,
445
+ widgetSnapshots: parsed.widgetSnapshots
446
+ }, services, state);
447
+ upstreamOutputs.push({
448
+ processorId: sourceProcessorId,
449
+ title: upstream.processor.title,
450
+ output: upstream.output
451
+ });
452
+ linkedContext.push(`Upstream processor ${upstream.processor.title} provided ${link.capabilityMode} access (${link.accessMode}).`);
453
+ continue;
454
+ }
455
+ const snapshot = parsed.widgetSnapshots[link.sourceWidgetId];
456
+ linkedContext.push([
457
+ `Linked widget ${link.sourceWidgetId} offers ${link.capabilityMode} access (${link.accessMode}).`,
458
+ snapshot !== undefined
459
+ ? `Snapshot: ${JSON.stringify(snapshot)}`
460
+ : `Metadata: ${JSON.stringify(link.metadata)}`
461
+ ].join(" "));
462
+ }
463
+ const fullPrompt = [
464
+ processor.promptFlow.trim(),
465
+ processor.contextInput.trim()
466
+ ? `Processor context:\n${processor.contextInput.trim()}`
467
+ : "",
468
+ linkedContext.length > 0
469
+ ? `Linked capabilities:\n${linkedContext.join("\n")}`
470
+ : "",
471
+ upstreamOutputs.length > 0
472
+ ? `Upstream processor outputs:\n${upstreamOutputs
473
+ .map((entry) => `${entry.title} (${entry.processorId})\n${entry.output.concatenated}`)
474
+ .join("\n\n")}`
475
+ : "",
476
+ parsed.input.trim() ? `Runtime input:\n${parsed.input.trim()}` : "",
477
+ Object.keys(parsed.context).length > 0
478
+ ? `Structured context:\n${JSON.stringify(parsed.context, null, 2)}`
479
+ : ""
480
+ ]
481
+ .filter(Boolean)
482
+ .join("\n\n");
483
+ const agents = resolveProcessorAgentProfiles(processor, services.secrets);
484
+ const outputsByAgent = {};
485
+ await Promise.all(agents.map(async (agent) => {
486
+ outputsByAgent[agent.agentLabel] = await runProcessorAgent(processor, agent, fullPrompt, {
487
+ llm: services.llm
488
+ });
489
+ }));
490
+ const output = {
491
+ concatenated: Object.entries(outputsByAgent)
492
+ .map(([agentLabel, text]) => `${agentLabel}\n${text}`.trim())
493
+ .join("\n\n"),
494
+ byAgent: outputsByAgent
495
+ };
496
+ const completedAt = new Date().toISOString();
497
+ writeProcessorRunState(processor, {
498
+ lastRunAt: completedAt,
499
+ lastRunStatus: "completed",
500
+ lastRunOutput: output,
501
+ runEntry: {
502
+ id: runEntryId,
503
+ trigger: state.trigger,
504
+ startedAt: runStartedAt,
505
+ completedAt,
506
+ status: "completed",
507
+ input: parsed.input,
508
+ output,
509
+ error: null
510
+ }
511
+ });
512
+ const result = {
513
+ processor: getAiProcessorById(processor.id),
514
+ output
515
+ };
516
+ state.cache.set(processorId, result);
517
+ state.active.delete(processorId);
518
+ return result;
519
+ }
520
+ catch (error) {
521
+ const failedAt = new Date().toISOString();
522
+ writeProcessorRunState(processor, {
523
+ lastRunAt: failedAt,
524
+ lastRunStatus: "failed",
525
+ lastRunOutput: processor.lastRunOutput,
526
+ runEntry: {
527
+ id: runEntryId,
528
+ trigger: state.trigger,
529
+ startedAt: runStartedAt,
530
+ completedAt: failedAt,
531
+ status: "failed",
532
+ input: parsed.input,
533
+ output: null,
534
+ error: error instanceof Error ? error.message : "Processor run failed."
535
+ }
536
+ });
537
+ state.active.delete(processorId);
538
+ throw error;
539
+ }
540
+ }
541
+ export async function runAiProcessor(processorId, input, services, options = {}) {
542
+ return await executeAiProcessor(processorId, input, services, {
543
+ cache: new Map(),
544
+ active: new Set(),
545
+ trigger: options.trigger ?? "manual"
546
+ });
547
+ }
@@ -1132,7 +1132,7 @@ export function getCalendarOverview(query) {
1132
1132
  provider: "google",
1133
1133
  label: "Google Calendar",
1134
1134
  supportsDedicatedForgeCalendar: true,
1135
- connectionHelp: "Use a Google refresh token plus client credentials to sync calendars and publish Forge-owned events and timeboxes."
1135
+ connectionHelp: "Forge uses a localhost Authorization Code + PKCE flow. Users sign in with Google from the same machine running Forge, Forge exchanges the code on the backend, and stores a per-user refresh token server-side."
1136
1136
  },
1137
1137
  {
1138
1138
  provider: "apple",
@@ -88,5 +88,13 @@ export function filterOwnedEntities(entityType, entities, userIds) {
88
88
  return decorated;
89
89
  }
90
90
  const allowed = new Set(userIds);
91
- return decorated.filter((entity) => entity.userId !== null && allowed.has(entity.userId));
91
+ return decorated.filter((entity) => {
92
+ if (entity.userId !== null && allowed.has(entity.userId)) {
93
+ return true;
94
+ }
95
+ const embeddedUserId = "userId" in entity && typeof entity.userId === "string"
96
+ ? entity.userId
97
+ : null;
98
+ return embeddedUserId !== null && allowed.has(embeddedUserId);
99
+ });
92
100
  }
@@ -77,13 +77,72 @@ function calculateCompletionRate(habit, checkIns) {
77
77
  const aligned = checkIns.filter((checkIn) => isAligned(habit, checkIn)).length;
78
78
  return Math.round((aligned / checkIns.length) * 100);
79
79
  }
80
- function calculateStreak(habit, checkIns) {
81
- let streak = 0;
80
+ function calculateStreak(habit, checkIns, now = new Date()) {
81
+ if (habit.frequency === "weekly" && habit.weekDays.length === 0) {
82
+ return 0;
83
+ }
84
+ const statusByDate = new Map();
82
85
  for (const checkIn of checkIns) {
83
- if (!isAligned(habit, checkIn)) {
84
- break;
86
+ if (!statusByDate.has(checkIn.dateKey)) {
87
+ statusByDate.set(checkIn.dateKey, checkIn.status);
85
88
  }
89
+ }
90
+ const isScheduledOn = (date) => habit.frequency === "daily" || habit.weekDays.includes(date.getUTCDay());
91
+ const toDateKey = (date) => date.toISOString().slice(0, 10);
92
+ const atUtcDayStart = (date) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
93
+ const previousScheduledDate = (date) => {
94
+ const cursor = atUtcDayStart(date);
95
+ do {
96
+ cursor.setUTCDate(cursor.getUTCDate() - 1);
97
+ } while (!isScheduledOn(cursor));
98
+ return cursor;
99
+ };
100
+ const startOfUtcWeek = (date) => {
101
+ const start = atUtcDayStart(date);
102
+ const offset = (start.getUTCDay() + 6) % 7;
103
+ start.setUTCDate(start.getUTCDate() - offset);
104
+ return start;
105
+ };
106
+ const previousUtcWeek = (date) => {
107
+ const start = startOfUtcWeek(date);
108
+ start.setUTCDate(start.getUTCDate() - 7);
109
+ return start;
110
+ };
111
+ const alignedStatusOn = (date) => {
112
+ const status = statusByDate.get(toDateKey(date));
113
+ return status ? isAligned(habit, { status }) : false;
114
+ };
115
+ if (habit.frequency === "daily") {
116
+ const today = atUtcDayStart(now);
117
+ let cursor = isScheduledOn(today) && !statusByDate.has(toDateKey(today))
118
+ ? previousScheduledDate(today)
119
+ : today;
120
+ let streak = 0;
121
+ while (alignedStatusOn(cursor)) {
122
+ streak += 1;
123
+ cursor = previousScheduledDate(cursor);
124
+ }
125
+ return streak;
126
+ }
127
+ const alignedCountForWeek = (weekStart) => {
128
+ let count = 0;
129
+ for (let offset = 0; offset < 7; offset += 1) {
130
+ const day = new Date(weekStart);
131
+ day.setUTCDate(weekStart.getUTCDate() + offset);
132
+ if (isScheduledOn(day) && alignedStatusOn(day)) {
133
+ count += 1;
134
+ }
135
+ }
136
+ return count;
137
+ };
138
+ const currentWeekStart = startOfUtcWeek(now);
139
+ let cursor = alignedCountForWeek(currentWeekStart) >= habit.targetCount
140
+ ? currentWeekStart
141
+ : previousUtcWeek(currentWeekStart);
142
+ let streak = 0;
143
+ while (alignedCountForWeek(cursor) >= habit.targetCount) {
86
144
  streak += 1;
145
+ cursor = previousUtcWeek(cursor);
87
146
  }
88
147
  return streak;
89
148
  }
@@ -137,7 +196,12 @@ function mapHabit(row, checkIns = listCheckInsForHabit(row.id)) {
137
196
  updatedAt: row.updated_at,
138
197
  lastCheckInAt: latestCheckIn?.createdAt ?? null,
139
198
  lastCheckInStatus: latestCheckIn?.status ?? null,
140
- streakCount: calculateStreak({ polarity: row.polarity }, checkIns),
199
+ streakCount: calculateStreak({
200
+ polarity: row.polarity,
201
+ frequency: row.frequency,
202
+ targetCount: row.target_count,
203
+ weekDays: parseWeekDays(row.week_days_json)
204
+ }, checkIns),
141
205
  completionRate: calculateCompletionRate({ polarity: row.polarity }, checkIns),
142
206
  dueToday: false,
143
207
  checkIns