forge-openclaw-plugin 0.2.26 → 0.2.27

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 (108) hide show
  1. package/README.md +59 -3
  2. package/dist/assets/{board-ta0rUHOf.js → board-C6jCchjI.js} +2 -2
  3. package/dist/assets/{board-ta0rUHOf.js.map → board-C6jCchjI.js.map} +1 -1
  4. package/dist/assets/index-DVvS8iiU.css +1 -0
  5. package/dist/assets/index-zYB-9Dfo.js +85 -0
  6. package/dist/assets/index-zYB-9Dfo.js.map +1 -0
  7. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
  8. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
  9. package/dist/assets/{motion-fBKPB6yw.js → motion-DFHrH2rd.js} +2 -2
  10. package/dist/assets/{motion-fBKPB6yw.js.map → motion-DFHrH2rd.js.map} +1 -1
  11. package/dist/assets/{table-C-IGTQni.js → table-ZL7Di_u3.js} +2 -2
  12. package/dist/assets/{table-C-IGTQni.js.map → table-ZL7Di_u3.js.map} +1 -1
  13. package/dist/assets/{ui-DInOpaYF.js → ui-CKNPpz7q.js} +2 -2
  14. package/dist/assets/{ui-DInOpaYF.js.map → ui-CKNPpz7q.js.map} +1 -1
  15. package/dist/assets/vendor-DoNZuFhn.js +1247 -0
  16. package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
  17. package/dist/index.html +7 -7
  18. package/dist/openclaw/local-runtime.js +16 -0
  19. package/dist/openclaw/routes.d.ts +27 -0
  20. package/dist/openclaw/routes.js +16 -12
  21. package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  22. package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
  23. package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
  24. package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
  25. package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
  26. package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
  27. package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  28. package/dist/server/server/src/app.js +1684 -117
  29. package/dist/server/server/src/connectors/box-registry.js +44 -9
  30. package/dist/server/server/src/data-management-types.js +107 -0
  31. package/dist/server/server/src/db.js +68 -4
  32. package/dist/server/server/src/demo-data.js +2 -2
  33. package/dist/server/server/src/health.js +702 -18
  34. package/dist/server/server/src/managers/platform/llm-manager.js +7 -4
  35. package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
  36. package/dist/server/server/src/managers/platform/secrets-manager.js +18 -1
  37. package/dist/server/server/src/managers/runtime.js +9 -0
  38. package/dist/server/server/src/movement.js +1971 -112
  39. package/dist/server/server/src/openapi.js +489 -1
  40. package/dist/server/server/src/psyche-types.js +9 -1
  41. package/dist/server/server/src/repositories/activity-events.js +8 -0
  42. package/dist/server/server/src/repositories/ai-connectors.js +522 -74
  43. package/dist/server/server/src/repositories/habits.js +37 -1
  44. package/dist/server/server/src/repositories/model-settings.js +13 -3
  45. package/dist/server/server/src/repositories/notes.js +3 -0
  46. package/dist/server/server/src/repositories/settings.js +380 -18
  47. package/dist/server/server/src/repositories/tasks.js +170 -10
  48. package/dist/server/server/src/runtime-data-root.js +82 -0
  49. package/dist/server/server/src/screen-time.js +802 -0
  50. package/dist/server/server/src/services/data-management.js +788 -0
  51. package/dist/server/server/src/services/entity-crud.js +205 -2
  52. package/dist/server/server/src/services/knowledge-graph.js +1455 -0
  53. package/dist/server/server/src/services/life-force-model.js +197 -0
  54. package/dist/server/server/src/services/life-force.js +1270 -0
  55. package/dist/server/server/src/services/psyche-observation-calendar.js +383 -16
  56. package/dist/server/server/src/types.js +286 -13
  57. package/dist/server/server/src/web.js +228 -13
  58. package/dist/server/src/components/customization/utility-widgets.js +136 -27
  59. package/dist/server/src/components/ui/info-tooltip.js +25 -0
  60. package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
  61. package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
  62. package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
  63. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +63 -8
  64. package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
  65. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +62 -54
  66. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +18 -8
  67. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +56 -38
  68. package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
  69. package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
  70. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +35 -30
  71. package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
  72. package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
  73. package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
  74. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +3 -1
  75. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +39 -3
  76. package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
  77. package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
  78. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +47 -32
  79. package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
  80. package/dist/server/src/lib/api.js +280 -21
  81. package/dist/server/src/lib/data-management-types.js +1 -0
  82. package/dist/server/src/lib/entity-visuals.js +279 -0
  83. package/dist/server/src/lib/knowledge-graph-types.js +276 -0
  84. package/dist/server/src/lib/knowledge-graph.js +470 -0
  85. package/dist/server/src/lib/schemas.js +4 -0
  86. package/dist/server/src/lib/snapshot-normalizer.js +43 -1
  87. package/dist/server/src/lib/workbench/contracts.js +229 -0
  88. package/dist/server/src/lib/workbench/nodes.js +200 -0
  89. package/dist/server/src/lib/workbench/registry.js +52 -5
  90. package/dist/server/src/lib/workbench/runtime.js +254 -38
  91. package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
  92. package/openclaw.plugin.json +1 -1
  93. package/package.json +1 -1
  94. package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  95. package/server/migrations/038_data_management_settings.sql +11 -0
  96. package/server/migrations/039_life_force_and_action_points.sql +114 -0
  97. package/server/migrations/040_screen_time_domain.sql +89 -0
  98. package/server/migrations/041_companion_source_states.sql +21 -0
  99. package/server/migrations/042_movement_boxes.sql +47 -0
  100. package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  101. package/skills/forge-openclaw/SKILL.md +24 -11
  102. package/skills/forge-openclaw/entity_conversation_playbooks.md +210 -34
  103. package/skills/forge-openclaw/psyche_entity_playbooks.md +113 -17
  104. package/dist/assets/index-Ro0ZF_az.css +0 -1
  105. package/dist/assets/index-ytlpSj23.js +0 -79
  106. package/dist/assets/index-ytlpSj23.js.map +0 -1
  107. package/dist/assets/vendor-lE3tZJcC.js +0 -876
  108. package/dist/assets/vendor-lE3tZJcC.js.map +0 -1
@@ -1,5 +1,8 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
1
3
  import { access, readFile } from "node:fs/promises";
2
4
  import path from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
3
6
  const distDir = path.join(process.cwd(), "dist");
4
7
  const packagedRuntimeDistDir = path.join(process.cwd(), "plugins", "forge-codex", "runtime", "dist");
5
8
  const contentTypes = {
@@ -17,7 +20,9 @@ function normalizeBasePath(value) {
17
20
  return "/";
18
21
  }
19
22
  const withLeadingSlash = value.startsWith("/") ? value : `/${value}`;
20
- return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
23
+ return withLeadingSlash.endsWith("/")
24
+ ? withLeadingSlash
25
+ : `${withLeadingSlash}/`;
21
26
  }
22
27
  function normalizeAbsoluteUrl(value) {
23
28
  const url = new URL(value);
@@ -31,6 +36,56 @@ function getDevWebOrigin() {
31
36
  const value = process.env.FORGE_DEV_WEB_ORIGIN?.trim();
32
37
  return value && value.length > 0 ? value : null;
33
38
  }
39
+ function shouldAutostartDevWeb(env) {
40
+ const value = env.FORGE_DEV_WEB_AUTOSTART?.trim().toLowerCase();
41
+ return value !== "0" && value !== "false" && value !== "no";
42
+ }
43
+ function getDevWebCommand(env) {
44
+ const value = env.FORGE_DEV_WEB_COMMAND?.trim();
45
+ return value && value.length > 0 ? value : "npm run dev:web";
46
+ }
47
+ function getDefaultDevWebOriginPort(origin) {
48
+ if (origin?.port && origin.port.trim().length > 0) {
49
+ return origin.port;
50
+ }
51
+ if (origin?.protocol === "https:") {
52
+ return "443";
53
+ }
54
+ return "3027";
55
+ }
56
+ function getDefaultViteCliPath(cwd) {
57
+ const candidate = path.join(cwd, "node_modules", "vite", "bin", "vite.js");
58
+ return existsSync(candidate) ? candidate : null;
59
+ }
60
+ function buildManagedDevWebLaunch(input) {
61
+ const explicitCommand = input.env.FORGE_DEV_WEB_COMMAND?.trim();
62
+ if (explicitCommand && explicitCommand.length > 0) {
63
+ return {
64
+ command: explicitCommand,
65
+ env: input.env,
66
+ shell: true
67
+ };
68
+ }
69
+ const viteCliPath = getDefaultViteCliPath(input.cwd);
70
+ if (!viteCliPath) {
71
+ return {
72
+ command: getDevWebCommand(input.env),
73
+ env: input.env,
74
+ shell: true
75
+ };
76
+ }
77
+ const host = input.env.FORGE_DEV_WEB_HOST?.trim() || "127.0.0.1";
78
+ const port = input.env.FORGE_DEV_WEB_PORT?.trim() || getDefaultDevWebOriginPort(input.origin);
79
+ return {
80
+ command: process.execPath,
81
+ args: [viteCliPath, "--host", host, "--port", port],
82
+ env: {
83
+ ...input.env,
84
+ FORGE_BASE_PATH: getDefaultBasePath()
85
+ },
86
+ shell: false
87
+ };
88
+ }
34
89
  function stripBasePath(requestPath, basePath) {
35
90
  const normalizedBasePath = normalizeBasePath(basePath);
36
91
  if (normalizedBasePath === "/") {
@@ -63,19 +118,174 @@ async function getClientDir() {
63
118
  return packagedRuntimeDistDir;
64
119
  }
65
120
  }
66
- async function serveAsset(requestPath, reply) {
67
- if (requestPath.startsWith("/api")) {
121
+ function parseRequestTarget(requestPath) {
122
+ return new URL(requestPath, "http://forge.local");
123
+ }
124
+ function copyProxyHeaders(response, reply) {
125
+ for (const [name, value] of response.headers) {
126
+ const lowerName = name.toLowerCase();
127
+ if (lowerName === "connection" ||
128
+ lowerName === "content-length" ||
129
+ lowerName === "keep-alive" ||
130
+ lowerName === "transfer-encoding") {
131
+ continue;
132
+ }
133
+ reply.header(name, value);
134
+ }
135
+ }
136
+ async function proxyDevAsset(input) {
137
+ const target = new URL(input.pathname.startsWith("/") ? input.pathname.slice(1) : input.pathname, input.origin);
138
+ target.search = input.search;
139
+ const response = await input.fetchImpl(target, { redirect: "manual" });
140
+ input.reply.code(response.status);
141
+ copyProxyHeaders(response, input.reply);
142
+ if (!response.headers.has("cache-control")) {
143
+ input.reply.header("Cache-Control", "no-store, max-age=0, must-revalidate");
144
+ }
145
+ if (!response.body) {
146
+ return "";
147
+ }
148
+ return Buffer.from(await response.arrayBuffer());
149
+ }
150
+ async function waitForProcessExit(child, timeoutMs = 5_000) {
151
+ if (child.exitCode !== null) {
152
+ return;
153
+ }
154
+ await Promise.race([
155
+ new Promise((resolve) => {
156
+ child.once("exit", () => resolve());
157
+ child.once("close", () => resolve());
158
+ }),
159
+ delay(timeoutMs).then(() => { })
160
+ ]);
161
+ }
162
+ export function createManagedDevWebRuntime(options = {}) {
163
+ const env = options.env ?? process.env;
164
+ const originValue = env.FORGE_DEV_WEB_ORIGIN?.trim();
165
+ const origin = originValue ? normalizeAbsoluteUrl(originValue) : null;
166
+ const cwd = options.cwd ?? process.cwd();
167
+ const fetchImpl = options.fetchImpl ?? fetch;
168
+ const spawnImpl = options.spawnImpl ?? spawn;
169
+ const autostart = shouldAutostartDevWeb(env);
170
+ const waitTimeoutMs = Number(env.FORGE_DEV_WEB_START_TIMEOUT_MS ?? 30_000);
171
+ const pollIntervalMs = 500;
172
+ let child = null;
173
+ let startupPromise = null;
174
+ async function probe() {
175
+ if (!origin) {
176
+ return null;
177
+ }
178
+ const controller = new AbortController();
179
+ const timeout = setTimeout(() => controller.abort(), 1_500);
180
+ try {
181
+ const response = await fetchImpl(origin, {
182
+ method: "GET",
183
+ redirect: "manual",
184
+ signal: controller.signal
185
+ });
186
+ return response.status < 500 ? origin : null;
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ finally {
192
+ clearTimeout(timeout);
193
+ }
194
+ }
195
+ async function waitUntilReady(processRef) {
196
+ const startedAt = Date.now();
197
+ while (Date.now() - startedAt < waitTimeoutMs) {
198
+ const readyOrigin = await probe();
199
+ if (readyOrigin) {
200
+ return readyOrigin;
201
+ }
202
+ if (processRef.exitCode !== null) {
203
+ break;
204
+ }
205
+ await delay(pollIntervalMs);
206
+ }
207
+ return null;
208
+ }
209
+ async function ensureReady() {
210
+ if (!origin) {
211
+ return null;
212
+ }
213
+ const readyOrigin = await probe();
214
+ if (readyOrigin || !autostart) {
215
+ return readyOrigin;
216
+ }
217
+ if (!startupPromise) {
218
+ startupPromise = (async () => {
219
+ if (!child || child.exitCode !== null) {
220
+ const launch = buildManagedDevWebLaunch({ cwd, env, origin });
221
+ const nextChild = launch.shell
222
+ ? spawnImpl(launch.command, {
223
+ cwd,
224
+ env: launch.env,
225
+ shell: true,
226
+ stdio: "inherit"
227
+ })
228
+ : spawnImpl(launch.command, launch.args ?? [], {
229
+ cwd,
230
+ env: launch.env,
231
+ stdio: "inherit"
232
+ });
233
+ child = nextChild;
234
+ nextChild.once("exit", () => {
235
+ if (child === nextChild) {
236
+ child = null;
237
+ }
238
+ });
239
+ }
240
+ const startedOrigin = await waitUntilReady(child);
241
+ startupPromise = null;
242
+ return startedOrigin;
243
+ })().catch((error) => {
244
+ startupPromise = null;
245
+ throw error;
246
+ });
247
+ }
248
+ return startupPromise;
249
+ }
250
+ async function stop() {
251
+ if (!child || child.exitCode !== null) {
252
+ return;
253
+ }
254
+ const childRef = child;
255
+ childRef.kill("SIGTERM");
256
+ await waitForProcessExit(childRef);
257
+ if (childRef.exitCode === null) {
258
+ childRef.kill("SIGKILL");
259
+ await waitForProcessExit(childRef, 1_000);
260
+ }
261
+ child = null;
262
+ }
263
+ return {
264
+ ensureReady,
265
+ stop
266
+ };
267
+ }
268
+ async function serveAsset(requestPath, reply, options) {
269
+ const requestTarget = parseRequestTarget(requestPath);
270
+ if (requestTarget.pathname.startsWith("/api")) {
68
271
  reply.code(404);
69
272
  return { error: "Not found" };
70
273
  }
71
- const normalizedRequestPath = stripBasePath(requestPath, getDefaultBasePath());
72
- const devWebOrigin = getDevWebOrigin();
274
+ const normalizedRequestPath = stripBasePath(requestTarget.pathname, getDefaultBasePath());
275
+ const devWebOrigin = await options.devWebRuntime.ensureReady();
73
276
  if (devWebOrigin) {
74
- const target = new URL(normalizedRequestPath.startsWith("/")
75
- ? normalizedRequestPath.slice(1)
76
- : normalizedRequestPath, normalizeAbsoluteUrl(devWebOrigin));
77
- reply.code(307).redirect(target.toString());
78
- return reply;
277
+ try {
278
+ return await proxyDevAsset({
279
+ origin: devWebOrigin,
280
+ pathname: normalizedRequestPath,
281
+ search: requestTarget.search,
282
+ reply,
283
+ fetchImpl: options.fetchImpl
284
+ });
285
+ }
286
+ catch {
287
+ reply.header("X-Forge-Web-Fallback", "built");
288
+ }
79
289
  }
80
290
  const clientDir = await getClientDir();
81
291
  const assetPath = resolveAsset(clientDir, normalizedRequestPath);
@@ -111,7 +321,12 @@ async function serveAsset(requestPath, reply) {
111
321
  return { error: "Asset not found" };
112
322
  }
113
323
  }
114
- export async function registerWebRoutes(app) {
115
- app.get("/", async (_request, reply) => serveAsset("/", reply));
116
- app.get("/*", async (request, reply) => serveAsset(request.url, reply));
324
+ export async function registerWebRoutes(app, options = {}) {
325
+ const devWebRuntime = options.devWebRuntime ?? createManagedDevWebRuntime();
326
+ const fetchImpl = options.fetchImpl ?? fetch;
327
+ app.addHook("onClose", async () => {
328
+ await devWebRuntime.stop();
329
+ });
330
+ app.get("/", async (_request, reply) => serveAsset("/", reply, { devWebRuntime, fetchImpl }));
331
+ app.get("/*", async (request, reply) => serveAsset(request.url, reply, { devWebRuntime, fetchImpl }));
117
332
  }
@@ -4,6 +4,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
4
  import { CalendarDays, CloudSun, ExternalLink, Music4, NotebookPen, Save, TimerReset } from "lucide-react";
5
5
  import { createNote, createWikiPage } from "../../lib/api.js";
6
6
  import { buildStaticWorkbenchExecution } from "../../lib/workbench/runtime.js";
7
+ import { createContextOutput, createNoteTool, createSummaryOutput } from "../../lib/workbench/contracts.js";
7
8
  import { cn } from "../../lib/utils.js";
8
9
  import { createGenericWorkbenchNodeView } from "../workbench-boxes/shared/generic-node-view.js";
9
10
  function formatMonthGrid(baseDate) {
@@ -175,19 +176,43 @@ const timeWidgetDefinition = {
175
176
  tags: ["utility", "clock"],
176
177
  inputs: [],
177
178
  params: [],
178
- output: [{ key: "primary", label: "Current time", kind: "text" }],
179
+ output: [
180
+ createSummaryOutput({
181
+ label: "Current time",
182
+ description: "Formatted local time string published by the clock widget."
183
+ }),
184
+ createContextOutput({
185
+ key: "clock",
186
+ label: "Clock state",
187
+ description: "Structured clock state including the current ISO timestamp.",
188
+ modelName: "ForgeClockState"
189
+ })
190
+ ],
179
191
  tools: [],
180
192
  NodeView: createGenericWorkbenchNodeView({
181
193
  title: "Clock",
182
194
  description: "Live local time widget.",
183
195
  inputs: [],
184
196
  params: [],
185
- output: [{ key: "primary", label: "Current time", kind: "text" }],
197
+ output: [
198
+ createSummaryOutput({
199
+ label: "Current time",
200
+ description: "Formatted local time string published by the clock widget."
201
+ }),
202
+ createContextOutput({
203
+ key: "clock",
204
+ label: "Clock state",
205
+ description: "Structured clock state including the current ISO timestamp.",
206
+ modelName: "ForgeClockState"
207
+ })
208
+ ],
186
209
  tools: []
187
210
  }),
188
211
  WebView: TimeWidget,
189
212
  execute: (input) => buildStaticWorkbenchExecution(input, {
190
- now: input.context.now
213
+ clock: {
214
+ now: input.context.now
215
+ }
191
216
  }, new Intl.DateTimeFormat(undefined, {
192
217
  hour: "2-digit",
193
218
  minute: "2-digit"
@@ -205,19 +230,43 @@ const calendarWidgetDefinition = {
205
230
  tags: ["utility", "calendar"],
206
231
  inputs: [],
207
232
  params: [],
208
- output: [{ key: "primary", label: "Month view", kind: "object" }],
233
+ output: [
234
+ createSummaryOutput({
235
+ label: "Month view",
236
+ description: "Summary of the compact month calendar widget."
237
+ }),
238
+ createContextOutput({
239
+ key: "calendarView",
240
+ label: "Calendar view",
241
+ description: "Structured mini-calendar state including the current month anchor.",
242
+ modelName: "ForgeMiniCalendarView"
243
+ })
244
+ ],
209
245
  tools: [],
210
246
  NodeView: createGenericWorkbenchNodeView({
211
247
  title: "Mini calendar",
212
248
  description: "Compact month calendar widget.",
213
249
  inputs: [],
214
250
  params: [],
215
- output: [{ key: "primary", label: "Month view", kind: "object" }],
251
+ output: [
252
+ createSummaryOutput({
253
+ label: "Month view",
254
+ description: "Summary of the compact month calendar widget."
255
+ }),
256
+ createContextOutput({
257
+ key: "calendarView",
258
+ label: "Calendar view",
259
+ description: "Structured mini-calendar state including the current month anchor.",
260
+ modelName: "ForgeMiniCalendarView"
261
+ })
262
+ ],
216
263
  tools: []
217
264
  }),
218
265
  WebView: MiniCalendarWidget,
219
266
  execute: (input) => buildStaticWorkbenchExecution(input, {
220
- now: input.context.now
267
+ calendarView: {
268
+ now: input.context.now
269
+ }
221
270
  }, "Compact month calendar")
222
271
  };
223
272
  MiniCalendarWidget.workbench = calendarWidgetDefinition;
@@ -239,19 +288,43 @@ const spotifyWidgetDefinition = {
239
288
  }
240
289
  ],
241
290
  params: [],
242
- output: [{ key: "primary", label: "Spotify link", kind: "text" }],
291
+ output: [
292
+ createSummaryOutput({
293
+ label: "Spotify link",
294
+ description: "Summary of the pinned music link widget."
295
+ }),
296
+ createContextOutput({
297
+ key: "spotifyLink",
298
+ label: "Spotify state",
299
+ description: "Structured Spotify widget state and pinned surface context.",
300
+ modelName: "ForgeSpotifyWidgetState"
301
+ })
302
+ ],
243
303
  tools: [],
244
304
  NodeView: createGenericWorkbenchNodeView({
245
305
  title: "Spotify",
246
306
  description: "Pinned music link widget.",
247
307
  inputs: [{ key: "surfaceId", label: "Surface id", kind: "text" }],
248
308
  params: [],
249
- output: [{ key: "primary", label: "Spotify link", kind: "text" }],
309
+ output: [
310
+ createSummaryOutput({
311
+ label: "Spotify link",
312
+ description: "Summary of the pinned music link widget."
313
+ }),
314
+ createContextOutput({
315
+ key: "spotifyLink",
316
+ label: "Spotify state",
317
+ description: "Structured Spotify widget state and pinned surface context.",
318
+ modelName: "ForgeSpotifyWidgetState"
319
+ })
320
+ ],
250
321
  tools: []
251
322
  }),
252
323
  WebView: SpotifyWidget,
253
324
  execute: (input) => buildStaticWorkbenchExecution(input, {
254
- surfaceId: input.inputs.surfaceId ?? null
325
+ spotifyLink: {
326
+ surfaceId: input.inputs.surfaceId ?? null
327
+ }
255
328
  }, "Pinned Spotify link")
256
329
  };
257
330
  SpotifyWidget.workbench = spotifyWidgetDefinition;
@@ -266,18 +339,40 @@ const weatherWidgetDefinition = {
266
339
  tags: ["utility", "weather"],
267
340
  inputs: [],
268
341
  params: [],
269
- output: [{ key: "primary", label: "Weather", kind: "object" }],
342
+ output: [
343
+ createSummaryOutput({
344
+ label: "Weather summary",
345
+ description: "Summary of the weather widget state."
346
+ }),
347
+ createContextOutput({
348
+ key: "weather",
349
+ label: "Weather payload",
350
+ description: "Structured weather widget payload.",
351
+ modelName: "ForgeWeatherWidgetState"
352
+ })
353
+ ],
270
354
  tools: [],
271
355
  NodeView: createGenericWorkbenchNodeView({
272
356
  title: "Weather",
273
357
  description: "Location-aware weather widget.",
274
358
  inputs: [],
275
359
  params: [],
276
- output: [{ key: "primary", label: "Weather", kind: "object" }],
360
+ output: [
361
+ createSummaryOutput({
362
+ label: "Weather summary",
363
+ description: "Summary of the weather widget state."
364
+ }),
365
+ createContextOutput({
366
+ key: "weather",
367
+ label: "Weather payload",
368
+ description: "Structured weather widget payload.",
369
+ modelName: "ForgeWeatherWidgetState"
370
+ })
371
+ ],
277
372
  tools: []
278
373
  }),
279
374
  WebView: WeatherWidget,
280
- execute: (input) => buildStaticWorkbenchExecution(input, null, "Weather widget")
375
+ execute: (input) => buildStaticWorkbenchExecution(input, { weather: null }, "Weather widget")
281
376
  };
282
377
  WeatherWidget.workbench = weatherWidgetDefinition;
283
378
  const quickCaptureWidgetDefinition = {
@@ -298,33 +393,47 @@ const quickCaptureWidgetDefinition = {
298
393
  }
299
394
  ],
300
395
  params: [],
301
- output: [{ key: "primary", label: "Draft", kind: "content" }],
396
+ output: [
397
+ createSummaryOutput({
398
+ label: "Draft summary",
399
+ description: "Summary of the quick-capture draft state."
400
+ }),
401
+ createContextOutput({
402
+ key: "draft",
403
+ label: "Draft context",
404
+ description: "Structured quick-capture draft context.",
405
+ modelName: "ForgeQuickCaptureDraft"
406
+ })
407
+ ],
302
408
  tools: [
303
- {
304
- key: "forge.create_note",
305
- label: "Create note",
306
- description: "Create a Forge evidence note from captured markdown.",
307
- accessMode: "write"
308
- }
409
+ createNoteTool("Create a Forge evidence note from captured markdown.")
309
410
  ],
310
411
  NodeView: createGenericWorkbenchNodeView({
311
412
  title: "Quick capture",
312
413
  description: "Draft a quick note or wiki page.",
313
414
  inputs: [{ key: "defaultUserId", label: "Default user id", kind: "text" }],
314
415
  params: [],
315
- output: [{ key: "primary", label: "Draft", kind: "content" }],
416
+ output: [
417
+ createSummaryOutput({
418
+ label: "Draft summary",
419
+ description: "Summary of the quick-capture draft state."
420
+ }),
421
+ createContextOutput({
422
+ key: "draft",
423
+ label: "Draft context",
424
+ description: "Structured quick-capture draft context.",
425
+ modelName: "ForgeQuickCaptureDraft"
426
+ })
427
+ ],
316
428
  tools: [
317
- {
318
- key: "forge.create_note",
319
- label: "Create note",
320
- description: "Create a Forge evidence note from captured markdown.",
321
- accessMode: "write"
322
- }
429
+ createNoteTool("Create a Forge evidence note from captured markdown.")
323
430
  ]
324
431
  }),
325
432
  WebView: QuickCaptureWidget,
326
433
  execute: (input) => buildStaticWorkbenchExecution(input, {
327
- defaultUserId: input.inputs.defaultUserId ?? null
434
+ draft: {
435
+ defaultUserId: input.inputs.defaultUserId ?? null
436
+ }
328
437
  }, "Quick capture can draft notes and wiki pages.")
329
438
  };
330
439
  QuickCaptureWidget.workbench = quickCaptureWidgetDefinition;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ import { CircleHelp } from "lucide-react";
4
+ import { cn } from "@/lib/utils";
5
+ export function FieldHint({ children, className }) {
6
+ return _jsx("div", { className: cn("text-sm leading-6 text-white/50", className), children: children });
7
+ }
8
+ export function InfoTooltip({ content, label = "Explain this field", className }) {
9
+ const [open, setOpen] = useState(false);
10
+ const containerRef = useRef(null);
11
+ const tooltipId = useId();
12
+ useEffect(() => {
13
+ if (!open) {
14
+ return;
15
+ }
16
+ const handlePointerDown = (event) => {
17
+ if (!containerRef.current?.contains(event.target)) {
18
+ setOpen(false);
19
+ }
20
+ };
21
+ document.addEventListener("pointerdown", handlePointerDown);
22
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
23
+ }, [open]);
24
+ return (_jsxs("span", { ref: containerRef, className: cn("relative inline-flex items-center", className), onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false), children: [_jsx("button", { type: "button", "aria-label": label, "aria-describedby": open ? tooltipId : undefined, "aria-expanded": open, className: "inline-flex size-5 items-center justify-center rounded-full text-white/42 transition hover:bg-white/[0.06] hover:text-white/78 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[rgba(192,193,255,0.35)]", onFocus: () => setOpen(true), onBlur: () => setOpen(false), onClick: () => setOpen((current) => !current), children: _jsx(CircleHelp, { className: "size-3.5" }) }), _jsx("span", { id: tooltipId, role: "tooltip", className: cn("pointer-events-none absolute right-0 top-[calc(100%+0.55rem)] z-40 w-[min(16rem,calc(100vw-2.5rem))] max-w-[calc(100vw-2.5rem)] rounded-[18px] border border-white/8 bg-[rgba(12,17,30,0.96)] px-3 py-2.5 text-sm leading-6 text-white/74 shadow-[0_18px_48px_rgba(3,8,18,0.42)] transition", open ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0"), children: content })] }));
25
+ }
@@ -0,0 +1,78 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { buildSearchWorkbenchExecution, buildStaticWorkbenchExecution } from "../../../lib/workbench/runtime.js";
3
+ import { createSearchEntitiesTool, createSearchInputs, createSearchOutputs, createSearchParams, createSummaryOutput } from "../../../lib/workbench/contracts.js";
4
+ import { createGenericWorkbenchNodeView } from "../shared/generic-node-view.js";
5
+ import { defineWorkbenchBox } from "../shared/define-workbench-box.js";
6
+ function Slot({ children }) {
7
+ return _jsx(_Fragment, { children: children });
8
+ }
9
+ function defineCalendarBox(id, title, description, tags, execute, output, tools, options) {
10
+ const inputs = options?.inputs ?? [];
11
+ const params = options?.params ?? [];
12
+ return defineWorkbenchBox(Slot, {
13
+ id,
14
+ surfaceId: "calendar",
15
+ routePath: "/calendar",
16
+ title,
17
+ icon: "calendar",
18
+ description,
19
+ category: "Calendar",
20
+ tags,
21
+ inputs,
22
+ params,
23
+ output,
24
+ tools,
25
+ NodeView: createGenericWorkbenchNodeView({
26
+ title,
27
+ description,
28
+ inputs,
29
+ params,
30
+ output,
31
+ tools
32
+ }),
33
+ execute
34
+ });
35
+ }
36
+ export const CalendarOverviewBox = defineCalendarBox("surface:calendar:overview", "Calendar overview", "Main calendar surface for mirrored events, work blocks, and planned timeboxes.", ["calendar", "overview"], (input) => buildStaticWorkbenchExecution(input, {
37
+ surfaces: ["events", "work_blocks", "timeboxes"]
38
+ }, "Calendar overview spanning mirrored events, work blocks, and timeboxes."), [createSummaryOutput({ label: "Calendar summary", description: "Summary of mirrored events, work blocks, and timeboxes." })], []);
39
+ export const CalendarEventsBox = defineCalendarBox("surface:calendar:events", "Calendar events", "Mirrored calendar events and Forge-managed calendar records.", ["calendar", "events", "mirrored"], (input) => buildSearchWorkbenchExecution(input, {
40
+ query: "",
41
+ entityTypes: ["calendar_event"],
42
+ limit: 20
43
+ }), createSearchOutputs({
44
+ itemKind: "calendar_event",
45
+ itemLabel: "Calendar event"
46
+ }), [createSearchEntitiesTool("Search calendar-backed Forge entities and planning records.")], {
47
+ inputs: createSearchInputs({
48
+ itemKind: "calendar_event",
49
+ itemLabel: "Calendar event",
50
+ defaultEntityTypes: ["calendar_event"],
51
+ defaultLimit: 20
52
+ }),
53
+ params: createSearchParams({
54
+ itemKind: "calendar_event",
55
+ defaultEntityTypes: ["calendar_event"],
56
+ defaultLimit: 20
57
+ })
58
+ });
59
+ export const CalendarPlanningBox = defineCalendarBox("surface:calendar:planning", "Planning blocks", "Planned task timeboxes and reusable work block templates.", ["calendar", "planning", "timeboxes"], (input) => buildSearchWorkbenchExecution(input, {
60
+ query: "",
61
+ entityTypes: ["task_timebox", "work_block_template"],
62
+ limit: 20
63
+ }), createSearchOutputs({
64
+ itemKind: "calendar_plan",
65
+ itemLabel: "Planning record"
66
+ }), [createSearchEntitiesTool("Search calendar-backed Forge entities and planning records.")], {
67
+ inputs: createSearchInputs({
68
+ itemKind: "calendar_plan",
69
+ itemLabel: "Planning record",
70
+ defaultEntityTypes: ["task_timebox", "work_block_template"],
71
+ defaultLimit: 20
72
+ }),
73
+ params: createSearchParams({
74
+ itemKind: "calendar_plan",
75
+ defaultEntityTypes: ["task_timebox", "work_block_template"],
76
+ defaultLimit: 20
77
+ })
78
+ });