forge-openclaw-plugin 0.2.23 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +13 -0
  2. package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
  3. package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
  4. package/dist/assets/index-CFCKDIMH.js +67 -0
  5. package/dist/assets/index-CFCKDIMH.js.map +1 -0
  6. package/dist/assets/index-ZPY6U1TU.css +1 -0
  7. package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
  8. package/dist/assets/motion-DvkU14p-.js.map +1 -0
  9. package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
  10. package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
  11. package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
  12. package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
  13. package/dist/assets/vendor-D9PTEPSB.js +824 -0
  14. package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
  15. package/dist/assets/viz-Cqb6s--o.js +34 -0
  16. package/dist/assets/viz-Cqb6s--o.js.map +1 -0
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/parity.d.ts +1 -1
  19. package/dist/openclaw/parity.js +29 -0
  20. package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
  21. package/dist/openclaw/plugin-entry-shared.js +7 -4
  22. package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
  23. package/dist/openclaw/routes.js +236 -0
  24. package/dist/openclaw/session-bootstrap.d.ts +78 -0
  25. package/dist/openclaw/session-bootstrap.js +240 -0
  26. package/dist/openclaw/tools.js +279 -3
  27. package/dist/server/app.js +855 -19
  28. package/dist/server/connectors/box-registry.js +257 -0
  29. package/dist/server/db.js +2 -0
  30. package/dist/server/discovery-advertiser.js +114 -0
  31. package/dist/server/health.js +39 -11
  32. package/dist/server/index.js +4 -0
  33. package/dist/server/managers/platform/llm-manager.js +40 -4
  34. package/dist/server/managers/platform/openai-responses-provider.js +129 -19
  35. package/dist/server/movement.js +2935 -0
  36. package/dist/server/openapi.js +628 -5
  37. package/dist/server/psyche-types.js +15 -1
  38. package/dist/server/questionnaire-flow.js +552 -0
  39. package/dist/server/questionnaire-seeds.js +853 -0
  40. package/dist/server/questionnaire-types.js +340 -0
  41. package/dist/server/repositories/ai-connectors.js +944 -0
  42. package/dist/server/repositories/ai-processors.js +547 -0
  43. package/dist/server/repositories/diagnostic-logs.js +57 -4
  44. package/dist/server/repositories/entity-ownership.js +9 -1
  45. package/dist/server/repositories/habits.js +77 -9
  46. package/dist/server/repositories/model-settings.js +216 -0
  47. package/dist/server/repositories/notes.js +57 -15
  48. package/dist/server/repositories/preferences.js +124 -0
  49. package/dist/server/repositories/questionnaires.js +1338 -0
  50. package/dist/server/repositories/rewards.js +2 -2
  51. package/dist/server/repositories/settings.js +108 -12
  52. package/dist/server/repositories/surface-layouts.js +76 -0
  53. package/dist/server/repositories/wiki-memory.js +5 -1
  54. package/dist/server/services/entity-crud.js +81 -2
  55. package/dist/server/services/openai-codex-oauth.js +153 -0
  56. package/dist/server/services/psyche-observation-calendar.js +46 -0
  57. package/dist/server/types.js +492 -3
  58. package/dist/server/watch-mobile.js +562 -0
  59. package/dist/server/web.js +9 -2
  60. package/openclaw.plugin.json +1 -1
  61. package/package.json +6 -1
  62. package/server/migrations/024_questionnaires.sql +96 -0
  63. package/server/migrations/025_ai_model_connections.sql +26 -0
  64. package/server/migrations/026_custom_theme_settings.sql +2 -0
  65. package/server/migrations/027_ai_processors.sql +31 -0
  66. package/server/migrations/028_movement_domain.sql +136 -0
  67. package/server/migrations/029_watch_micro_capture.sql +23 -0
  68. package/server/migrations/030_surface_layouts.sql +5 -0
  69. package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  70. package/server/migrations/032_ai_connectors.sql +44 -0
  71. package/server/migrations/033_movement_trip_point_sync.sql +36 -0
  72. package/server/migrations/034_movement_segment_sync.sql +49 -0
  73. package/skills/forge-openclaw/SKILL.md +12 -1
  74. package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
  75. package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
  76. package/dist/assets/index-Ch_xeZ2u.js +0 -63
  77. package/dist/assets/index-Ch_xeZ2u.js.map +0 -1
  78. package/dist/assets/index-DvVM7K6j.css +0 -1
  79. package/dist/assets/motion-D4sZgCHd.js.map +0 -1
  80. package/dist/assets/vendor-De38P6YR.js +0 -729
  81. package/dist/assets/vendor-De38P6YR.js.map +0 -1
  82. package/dist/assets/viz-C6hfyqzu.js +0 -34
  83. package/dist/assets/viz-C6hfyqzu.js.map +0 -1
  84. package/skills/forge-openclaw/cron_jobs.md +0 -395
@@ -1,27 +1,34 @@
1
1
  import Fastify from "fastify";
2
2
  import cors from "@fastify/cors";
3
3
  import multipart from "@fastify/multipart";
4
+ import { CronExpressionParser } from "cron-parser";
4
5
  import { ZodError } from "zod";
5
6
  import { configureDatabase, configureDatabaseSeeding, runInTransaction } from "./db.js";
6
7
  import { HttpError, isHttpError } from "./errors.js";
7
8
  import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
8
9
  import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
10
+ import { createAiConnector, deleteAiConnector, getAiConnectorById, getAiConnectorBySlug, getAiConnectorConversationForConnector, listAiConnectorRuns, listAiConnectors, runAiConnector, updateAiConnector } from "./repositories/ai-connectors.js";
11
+ import { createAiProcessor, createAiProcessorLink, deleteAiProcessor, deleteAiProcessorLink, getAiProcessorById, getAiProcessorBySlug, listAiProcessors, getSurfaceProcessorGraph, runAiProcessor, updateAiProcessor } from "./repositories/ai-processors.js";
9
12
  import { listEventLog } from "./repositories/event-log.js";
10
13
  import { createDiagnosticMessage, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS, enforceDiagnosticLogRetention, listDiagnosticLogs, normalizeDiagnosticSource, recordDiagnosticLog, serializeDiagnosticError } from "./repositories/diagnostic-logs.js";
11
14
  import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
15
+ import { getSurfaceLayout, resetSurfaceLayout, saveSurfaceLayout } from "./repositories/surface-layouts.js";
16
+ import { listForgeBoxCatalog } from "./connectors/box-registry.js";
12
17
  import { createHabit, createHabitCheckIn, deleteHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
13
18
  import { listDomains } from "./repositories/domains.js";
14
19
  import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
15
20
  import { createWikiIngestJobSchema, createUploadedWikiIngestJob, createWikiSpace, createWikiSpaceSchema, deleteWikiIngestJob, deleteWikiProfile, getWikiHealth, getWikiIngestJob, getWikiHomePageDetail, getWikiPageDetail, getWikiPageDetailBySlug, getWikiSettingsPayload, ingestWikiSource, listWikiIngestJobs, listWikiLlmProfiles, listWikiPageTree, listWikiPages, listWikiSpaces, processWikiIngestJob, reindexWikiEmbeddings, reindexWikiEmbeddingsSchema, rerunWikiIngestJob, reviewWikiIngestJob, reviewWikiIngestJobSchema, searchWikiPages, syncWikiVaultFromDisk, syncWikiVaultSchema, testWikiLlmProfileSchema, upsertWikiEmbeddingProfile, upsertWikiEmbeddingProfileSchema, upsertWikiLlmProfile, upsertWikiLlmProfileSchema, wikiSearchQuerySchema } from "./repositories/wiki-memory.js";
16
21
  import { filterOwnedEntities, setEntityOwner } from "./repositories/entity-ownership.js";
17
22
  import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
23
+ import { cloneQuestionnaireInstrument, completeQuestionnaireRun, createQuestionnaireInstrument, ensureQuestionnaireDraftVersion, getQuestionnaireInstrumentDetail, getQuestionnaireRunDetail, listQuestionnaireInstruments, publishQuestionnaireDraftVersion, startQuestionnaireRun, updateQuestionnaireDraftVersion, updateQuestionnaireRun } from "./repositories/questionnaires.js";
18
24
  import { createProject, updateProject } from "./repositories/projects.js";
19
25
  import { createPreferenceCatalog, createPreferenceCatalogItem, createPreferenceContext, createPreferenceItem, createPreferenceItemFromEntity, deletePreferenceCatalog, deletePreferenceCatalogItem, getPreferenceWorkspace, mergePreferenceContexts, startPreferenceGame, submitAbsoluteSignal, submitPairwiseJudgment, updatePreferenceCatalog, updatePreferenceCatalogItem, updatePreferenceContext, updatePreferenceItem, updatePreferenceScore } from "./repositories/preferences.js";
20
26
  import { createStrategy, getStrategyById, listStrategies, updateStrategy } from "./repositories/strategies.js";
21
27
  import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordWorkAdjustmentReward, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
22
28
  import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
29
+ import { deleteAiModelConnection, getAiModelConnectionById, readModelConnectionCredential, upsertAiModelConnection } from "./repositories/model-settings.js";
23
30
  import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
24
- import { createUser, ensureSystemUsers, getUserById, listUserAccessGrants, listUserOwnershipSummaries, listUserXpSummaries, listUsers, resolveUserForMutation, updateUserAccessGrant, updateUser } from "./repositories/users.js";
31
+ import { createUser, ensureSystemUsers, getDefaultUser, getUserById, listUserAccessGrants, listUserOwnershipSummaries, listUserXpSummaries, listUsers, resolveUserForMutation, updateUserAccessGrant, updateUser } from "./repositories/users.js";
25
32
  import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
26
33
  import { createTask, createTaskWithIdempotency, getTaskById, listTasks, uncompleteTask, updateTask } from "./repositories/tasks.js";
27
34
  import { createWorkAdjustment } from "./repositories/work-adjustments.js";
@@ -32,20 +39,25 @@ import { buildGamificationOverview, buildGamificationProfile, buildXpMomentumPul
32
39
  import { getInsightsPayload } from "./services/insights.js";
33
40
  import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
34
41
  import { getPsycheOverview } from "./services/psyche.js";
42
+ import { getPsycheObservationCalendar } from "./services/psyche-observation-calendar.js";
35
43
  import { getProjectBoard, getProjectSummary, listProjectSummaries } from "./services/projects.js";
36
44
  import { getWeeklyReviewPayload } from "./services/reviews.js";
37
45
  import { finalizeWeeklyReviewClosure } from "./repositories/weekly-reviews.js";
38
46
  import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
39
47
  import { suggestTags } from "./services/tagging.js";
40
48
  import { CalendarConnectionConflictError, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
49
+ import { consumeOpenAiCodexOauthCredentials, getOpenAiCodexOauthSession, startOpenAiCodexOauthSession, submitOpenAiCodexOauthManualInput } from "./services/openai-codex-oauth.js";
41
50
  import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
51
+ import { createQuestionnaireInstrumentSchema, publishQuestionnaireVersionSchema, startQuestionnaireRunSchema, updateQuestionnaireRunSchema, updateQuestionnaireVersionSchema } from "./questionnaire-types.js";
42
52
  import { createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "./preferences-types.js";
43
- import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
53
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, createAiConnectorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiConnectorSchema, writeSurfaceLayoutSchema, upsertAiModelConnectionSchema, testAiModelConnectionSchema, submitOpenAiCodexOauthManualCodeSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, updateAiConnectorSchema, updateAiProcessorSchema, runAiProcessorSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
44
54
  import { buildOpenApiDocument } from "./openapi.js";
45
55
  import { registerWebRoutes } from "./web.js";
46
56
  import { createManagerRuntime } from "./managers/runtime.js";
47
57
  import { isManagerError } from "./managers/type-guards.js";
48
- import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
58
+ import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
59
+ import { createMovementPlace, getMovementAllTimeSummary, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, listMovementPlaces, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileStayPatchSchema, movementMobileTimelineSchema, movementMobileTripPatchSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, movementTripPatchSchema, deleteMovementTripPoint, deleteMovementStay, deleteMovementTrip, updateMovementPlace, updateMovementStay, updateMovementSettings, updateMovementTrip, updateMovementTripPoint } from "./movement.js";
60
+ import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
49
61
  const COMPATIBILITY_SUNSET = "transitional-node";
50
62
  function markCompatibilityRoute(reply) {
51
63
  reply.header("Deprecation", "true");
@@ -116,8 +128,7 @@ function buildApiBaseUrl(request) {
116
128
  if (referer) {
117
129
  try {
118
130
  const url = new URL(referer);
119
- const forgeMounted = url.pathname.startsWith("/forge/");
120
- return `${url.origin}${forgeMounted ? "/forge" : ""}/api/v1`;
131
+ return `${url.origin}/api/v1`;
121
132
  }
122
133
  catch {
123
134
  // Fall through to host-based resolution.
@@ -1881,13 +1892,15 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
1881
1892
  "Ask only for what is missing or unclear instead of walking the user through every optional field.",
1882
1893
  "Use a progression of concrete example or intent, working name, purpose or meaning, placement in Forge, operational details, and linked context.",
1883
1894
  "Ask one to three focused questions at a time. One is usually best when the user is uncertain or emotionally loaded.",
1895
+ "If the user already answered the normal opening question, do not repeat it. Move to the next missing clarification.",
1896
+ "Do not over-therapize logistical entities. For tasks, calendar events, work blocks, timeboxes, and task runs, one brief confirming sentence plus one question is usually enough.",
1884
1897
  "Before saving, briefly summarize the working formulation in the user's own language when that would reduce ambiguity.",
1885
1898
  "When updating an entity, start with what is changing, what should stay true, and what prompted the update now."
1886
1899
  ];
1887
1900
  const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
1888
1901
  {
1889
1902
  focus: "goal",
1890
- openingQuestion: "What direction are you trying to hold onto here?",
1903
+ openingQuestion: "What direction here feels important enough that you want to keep it in view?",
1891
1904
  coachingGoal: "Clarify the direction and why it matters, not just produce a title.",
1892
1905
  askSequence: [
1893
1906
  "Ask what direction or outcome the user wants to keep in view.",
@@ -1898,12 +1911,13 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
1898
1911
  },
1899
1912
  {
1900
1913
  focus: "project",
1901
- openingQuestion: "If this becomes a project, what would you want it to be called and what should it accomplish?",
1914
+ openingQuestion: "If this became a real project, what would you be trying to make true?",
1902
1915
  coachingGoal: "Turn an intention into a bounded workstream with a clear outcome.",
1903
1916
  askSequence: [
1904
- "Ask what this piece of work should be called.",
1917
+ "Ask what this piece of work is trying to make true.",
1905
1918
  "Ask what outcome would make the project feel real or complete for now.",
1906
1919
  "Ask which goal it belongs under.",
1920
+ "Land on a working name once the scope is clear.",
1907
1921
  "Clarify status, owner, and notes only after the scope is clear."
1908
1922
  ]
1909
1923
  },
@@ -1930,18 +1944,18 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
1930
1944
  },
1931
1945
  {
1932
1946
  focus: "habit",
1933
- openingQuestion: "What is the recurring behavior you want Forge to keep track of?",
1947
+ openingQuestion: "What recurring move are you trying to strengthen or loosen?",
1934
1948
  coachingGoal: "Define the recurring behavior and cadence clearly enough for honest later check-ins.",
1935
1949
  askSequence: [
1936
1950
  "Ask what the recurring behavior is in plain language.",
1937
1951
  "Ask whether doing it is aligned or a slip.",
1938
- "Ask about cadence and what counts as success in practice.",
1952
+ "Ask about cadence and what counts as an honest check-in in practice.",
1939
1953
  "Ask about links only if they will help later review."
1940
1954
  ]
1941
1955
  },
1942
1956
  {
1943
1957
  focus: "note",
1944
- openingQuestion: "What do you want this note to preserve, and what should it stay attached to?",
1958
+ openingQuestion: "What feels important to preserve from this?",
1945
1959
  coachingGoal: "Preserve the useful context and link it to the right places without turning the note into a dump.",
1946
1960
  askSequence: [
1947
1961
  "Ask what the note needs to preserve.",
@@ -1994,7 +2008,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
1994
2008
  },
1995
2009
  {
1996
2010
  focus: "event_type",
1997
- openingQuestion: "What kind of incident should this category stand for?",
2011
+ openingQuestion: "When this kind of moment happens, what would you want to call it so future reports stay consistent?",
1998
2012
  coachingGoal: "Create a reusable incident category that will actually help future reports stay consistent.",
1999
2013
  askSequence: [
2000
2014
  "Ask what category the label should capture.",
@@ -2004,7 +2018,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
2004
2018
  },
2005
2019
  {
2006
2020
  focus: "emotion_definition",
2007
- openingQuestion: "What emotion label do you want to keep reusable in Forge?",
2021
+ openingQuestion: "What emotion do you want Forge to help you name clearly and reuse later?",
2008
2022
  coachingGoal: "Create a reusable emotion label with enough clarity to use consistently later.",
2009
2023
  askSequence: [
2010
2024
  "Ask what emotion label the user wants to preserve.",
@@ -2086,7 +2100,8 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2086
2100
  "A pattern is usually the best Psyche container for functional analysis.",
2087
2101
  "If the user is describing one specific episode rather than a repeated loop, prefer a trigger report.",
2088
2102
  "Reflect before the next question, and avoid interrogating through the schema fields in order.",
2089
- "If the user asks to understand the loop first, do not lead with a finished working diagnosis or title before asking at least one clarifying question."
2103
+ "If the user asks to understand the loop first, do not lead with a finished working diagnosis or title before asking at least one clarifying question.",
2104
+ "Before you ask how to change the loop, ask what it is protecting, preventing, or managing for the user."
2090
2105
  ]
2091
2106
  },
2092
2107
  {
@@ -2127,7 +2142,8 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2127
2142
  notes: [
2128
2143
  "Keep the user close to observable behavior rather than jumping straight to labels.",
2129
2144
  "When the behavior clearly belongs inside a larger loop, suggest linking or also mapping the related behavior_pattern.",
2130
- "If the user asks for understanding before storage, ask about the recent example and function of the move before classifying it."
2145
+ "If the user asks for understanding before storage, ask about the recent example and function of the move before classifying it.",
2146
+ "Ask what the move is trying to do for the user before moving into replacement planning."
2131
2147
  ]
2132
2148
  },
2133
2149
  {
@@ -2157,6 +2173,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2157
2173
  ],
2158
2174
  exampleQuestions: [
2159
2175
  "If we turned that reaction into one sentence, what would it sound like?",
2176
+ "When that reaction hits, what does it start telling you?",
2160
2177
  "Is it more of an always/never belief, or an if-then rule?",
2161
2178
  "How true does it feel right now from 0 to 100?",
2162
2179
  "What seems to support it, and what weakens it?",
@@ -2166,7 +2183,8 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2166
2183
  notes: [
2167
2184
  "Schema catalog entries are reference concepts; belief_entry is the user-owned record.",
2168
2185
  "If no schema catalog match is known, omit schemaId rather than inventing one.",
2169
- "Do not argue the user out of the belief. Reflect it, understand its function, and then collaboratively test for flexibility."
2186
+ "Do not argue the user out of the belief. Reflect it, understand its function, and then collaboratively test for flexibility.",
2187
+ "When the wording is nearly there, ask whether it feels true enough before you move into confidence, evidence, or alternative-belief details."
2170
2188
  ]
2171
2189
  },
2172
2190
  {
@@ -2199,6 +2217,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2199
2217
  ],
2200
2218
  exampleQuestions: [
2201
2219
  "When this part shows up, what is it like from the inside?",
2220
+ "When this part takes over, what is it trying to protect?",
2202
2221
  "What kind of part does this feel like: coping, child, critic-parent, healthy-adult, or happy-child?",
2203
2222
  "If you gave this mode a name, what would it be?",
2204
2223
  "What is it afraid would happen if it stopped doing its job?",
@@ -2227,6 +2246,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2227
2246
  highValueOptionalFields: [],
2228
2247
  exampleQuestions: [
2229
2248
  "What just happened that brought this up right now?",
2249
+ "What just happened before this part came online?",
2230
2250
  "If this part had a voice, what would it be saying?",
2231
2251
  "What is it trying to protect you from?",
2232
2252
  "What does it seem to need from you or from someone else?",
@@ -2322,7 +2342,8 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2322
2342
  "entityType alone is never enough; full data is required.",
2323
2343
  "Batch multiple related creates together when they come from one user ask.",
2324
2344
  "Goal, project, and task creates can include notes: [{ contentMarkdown, author?, tags?, destroyAt?, links? }] and Forge will auto-link those notes to the newly created entity.",
2325
- "The same batch create route also handles calendar_event, work_block_template, and task_timebox. Calendar-event creates still trigger downstream projection sync when a writable provider calendar is selected."
2345
+ "The same batch create route also handles calendar_event, work_block_template, task_timebox, preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument.",
2346
+ "Calendar-event creates still trigger downstream projection sync when a writable provider calendar is selected."
2326
2347
  ],
2327
2348
  example: '{"operations":[{"entityType":"task","data":{"title":"Write the public release notes","projectId":"project_123","status":"focus","notes":[{"contentMarkdown":"Starting from the changelog draft and the last QA pass."}]},"clientRef":"task-1"}]}'
2328
2349
  },
@@ -2342,7 +2363,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2342
2363
  "Project lifecycle is status-driven: patch project.status to active, paused, or completed instead of looking for separate suspend, restart, or finish routes.",
2343
2364
  "Setting project.status to completed finishes the project and auto-completes linked unfinished tasks through the normal task completion path.",
2344
2365
  "Task and project scheduling rules stay on these same entity patches. Update task.schedulingRules, task.plannedDurationSeconds, or project.schedulingRules here.",
2345
- "Use this same route to move or relink calendar_event records and to edit work_block_template or task_timebox records without switching to narrower calendar CRUD tools."
2366
+ "Use this same route to move or relink calendar_event records, edit work_block_template or task_timebox records, and do normal field updates on preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument."
2346
2367
  ],
2347
2368
  example: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"completed"},"clientRef":"project-finish-1"}]}'
2348
2369
  },
@@ -2777,6 +2798,9 @@ function buildAgentOnboardingPayload(request) {
2777
2798
  wiki: "Forge Wiki is the file-first memory layer: local Markdown pages plus media, backlinks, optional embeddings, explicit spaces, and structured links back to Forge entities.",
2778
2799
  sleepSession: "A sleep session is a first-class health record with timing, sleep and bed duration, stage breakdown, recovery metrics, annotations, and Forge links back to planning or Psyche context.",
2779
2800
  workoutSession: "A workout session is a first-class sports record imported from HealthKit or generated from a habit. It holds workout type, timing, energy or distance when available, subjective effort, narrative context, and Forge links.",
2801
+ preferences: "Forge Preferences is the explicit taste-modeling domain. It has workspaces, contexts, concept libraries, direct items, pairwise judgments, direct signals, and inferred scores.",
2802
+ questionnaire: "Forge Psyche questionnaires are structured reusable instruments with provenance, scoring, draft and published versions, and user-owned answer runs.",
2803
+ selfObservation: "Forge self-observation is a dedicated Psyche calendar view backed by observed notes timestamped by frontmatter.observedAt, including deliberate reflection notes and rolling movement notes from the companion.",
2780
2804
  insight: "An agent-authored observation or recommendation grounded in Forge data.",
2781
2805
  calendar: "A connected calendar source mirrored into Forge. Calendar state combines provider events, recurring work blocks, and task timeboxes.",
2782
2806
  workBlock: "A recurring half-day or custom time window such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or Custom. Work blocks can allow or block work by default, can define active date bounds, and remain editable through the calendar surface.",
@@ -2814,6 +2838,94 @@ function buildAgentOnboardingPayload(request) {
2814
2838
  "Behavior patterns, behaviors, beliefs, modes, and trigger reports cross-link to describe one reflective model rather than isolated records.",
2815
2839
  "Insights can point at one entity, but they exist to capture interpretation or advice rather than raw work items."
2816
2840
  ],
2841
+ entityRouteModel: {
2842
+ batchCrudEntities: [
2843
+ "goal",
2844
+ "project",
2845
+ "task",
2846
+ "strategy",
2847
+ "habit",
2848
+ "tag",
2849
+ "note",
2850
+ "insight",
2851
+ "calendar_event",
2852
+ "work_block_template",
2853
+ "task_timebox",
2854
+ "psyche_value",
2855
+ "behavior_pattern",
2856
+ "behavior",
2857
+ "belief_entry",
2858
+ "mode_profile",
2859
+ "mode_guide_session",
2860
+ "event_type",
2861
+ "emotion_definition",
2862
+ "trigger_report",
2863
+ "preference_catalog",
2864
+ "preference_catalog_item",
2865
+ "preference_context",
2866
+ "preference_item",
2867
+ "questionnaire_instrument"
2868
+ ],
2869
+ batchRoutes: {
2870
+ search: "/api/v1/entities/search",
2871
+ create: "/api/v1/entities/create",
2872
+ update: "/api/v1/entities/update",
2873
+ delete: "/api/v1/entities/delete",
2874
+ restore: "/api/v1/entities/restore"
2875
+ },
2876
+ actionEntities: {
2877
+ task_run: {
2878
+ readModel: "/api/v1/operator/context",
2879
+ actions: {
2880
+ start: "/api/v1/tasks/:taskId/runs",
2881
+ heartbeat: "/api/v1/task-runs/:id/heartbeat",
2882
+ focus: "/api/v1/task-runs/:id/focus",
2883
+ complete: "/api/v1/task-runs/:id/complete",
2884
+ release: "/api/v1/task-runs/:id/release"
2885
+ }
2886
+ },
2887
+ questionnaire_run: {
2888
+ read: "/api/v1/psyche/questionnaire-runs/:id",
2889
+ actions: {
2890
+ start: "/api/v1/psyche/questionnaires/:id/runs",
2891
+ update: "/api/v1/psyche/questionnaire-runs/:id",
2892
+ complete: "/api/v1/psyche/questionnaire-runs/:id/complete"
2893
+ }
2894
+ },
2895
+ preferences: {
2896
+ workspace: "/api/v1/preferences/workspace",
2897
+ actions: {
2898
+ startGame: "/api/v1/preferences/game/start",
2899
+ mergeContexts: "/api/v1/preferences/contexts/merge",
2900
+ enqueueFromEntity: "/api/v1/preferences/items/from-entity",
2901
+ submitJudgment: "/api/v1/preferences/judgments",
2902
+ submitSignal: "/api/v1/preferences/signals",
2903
+ overrideScore: "/api/v1/preferences/items/:id/score"
2904
+ }
2905
+ },
2906
+ questionnaires: {
2907
+ list: "/api/v1/psyche/questionnaires",
2908
+ detail: "/api/v1/psyche/questionnaires/:id",
2909
+ actions: {
2910
+ clone: "/api/v1/psyche/questionnaires/:id/clone",
2911
+ ensureDraft: "/api/v1/psyche/questionnaires/:id/draft",
2912
+ publishDraft: "/api/v1/psyche/questionnaires/:id/publish"
2913
+ }
2914
+ },
2915
+ selfObservation: {
2916
+ read: "/api/v1/psyche/self-observation/calendar",
2917
+ writeModel: "Create or update an observed note with frontmatter.observedAt. Manual reflections usually carry the Self-observation tag, while movement sync can also publish rolling observed notes tagged movement."
2918
+ },
2919
+ sleep_session: {
2920
+ read: "/api/v1/health/sleep",
2921
+ update: "/api/v1/health/sleep/:id"
2922
+ },
2923
+ workout_session: {
2924
+ read: "/api/v1/health/fitness",
2925
+ update: "/api/v1/health/workouts/:id"
2926
+ }
2927
+ }
2928
+ },
2817
2929
  multiUserModel: {
2818
2930
  summary: "Forge is multi-user by default. Humans and bots share one entity graph, with explicit ownership on every record and directional relationship settings between every pair of users.",
2819
2931
  defaultUserScopeBehavior: "If no user scope is provided, Forge returns all visible users. Use userId or repeated userIds when an agent should focus on one owner namespace or on a specific human/bot slice.",
@@ -2858,6 +2970,7 @@ function buildAgentOnboardingPayload(request) {
2858
2970
  ],
2859
2971
  configNotes: [
2860
2972
  "Localhost and Tailscale targets can usually use the operator-session path without a long-lived token.",
2973
+ "Use a distinct actor label such as Albert (claw) so OpenClaw-originated work stays obvious in Forge provenance.",
2861
2974
  "Create each agent as a Forge bot user, then use userId or userIds in tool inputs whenever the agent should focus on one human, one bot, or a specific collaboration slice."
2862
2975
  ]
2863
2976
  },
@@ -2875,6 +2988,7 @@ function buildAgentOnboardingPayload(request) {
2875
2988
  ],
2876
2989
  configNotes: [
2877
2990
  "Hermes keeps its durable Forge config under ~/.hermes/forge/config.json.",
2991
+ "Use a distinct actor label such as Albert (hermes) so Hermes-originated work stays obvious in Forge provenance.",
2878
2992
  "Hermes uses the same multi-user scoping rules and should pass userIds intentionally when working across humans and bots.",
2879
2993
  "The Forge relationship graph still decides whether Hermes may see, message, plan for, or affect another owner."
2880
2994
  ]
@@ -3528,8 +3642,43 @@ export async function buildServer(options = {}) {
3528
3642
  }
3529
3643
  }, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS);
3530
3644
  diagnosticRetentionTimer.unref?.();
3645
+ const activeCronRuns = new Set();
3646
+ const cronSchedulerTimer = setInterval(() => {
3647
+ const now = new Date();
3648
+ for (const processor of listAiProcessors()) {
3649
+ if (processor.triggerMode !== "cron" ||
3650
+ !processor.endpointEnabled ||
3651
+ !processor.cronExpression.trim() ||
3652
+ activeCronRuns.has(processor.id)) {
3653
+ continue;
3654
+ }
3655
+ try {
3656
+ const interval = CronExpressionParser.parse(processor.cronExpression, {
3657
+ currentDate: processor.lastRunAt && Number.isFinite(Date.parse(processor.lastRunAt))
3658
+ ? processor.lastRunAt
3659
+ : new Date(now.getTime() - 60_000).toISOString()
3660
+ });
3661
+ const nextDueAt = interval.next().toDate();
3662
+ if (nextDueAt.getTime() > now.getTime()) {
3663
+ continue;
3664
+ }
3665
+ activeCronRuns.add(processor.id);
3666
+ void runAiProcessor(processor.id, { input: "", context: {}, widgetSnapshots: {} }, {
3667
+ llm: managers.llm,
3668
+ secrets: managers.secrets
3669
+ }, { trigger: "cron" }).finally(() => {
3670
+ activeCronRuns.delete(processor.id);
3671
+ });
3672
+ }
3673
+ catch {
3674
+ continue;
3675
+ }
3676
+ }
3677
+ }, 30_000);
3678
+ cronSchedulerTimer.unref?.();
3531
3679
  app.addHook("onClose", async () => {
3532
3680
  clearInterval(diagnosticRetentionTimer);
3681
+ clearInterval(cronSchedulerTimer);
3533
3682
  taskRunWatchdog?.stop();
3534
3683
  await managers.backgroundJobs.stop();
3535
3684
  });
@@ -3858,6 +4007,147 @@ export async function buildServer(options = {}) {
3858
4007
  app.get("/api/v1/health/fitness", async (request) => ({
3859
4008
  fitness: getFitnessViewData(resolveScopedUserIds(request.query))
3860
4009
  }));
4010
+ app.get("/api/v1/movement/day", async (request) => {
4011
+ const query = request.query;
4012
+ return {
4013
+ movement: getMovementDayDetail({
4014
+ date: typeof query.date === "string" ? query.date : undefined,
4015
+ userIds: resolveScopedUserIds(query)
4016
+ })
4017
+ };
4018
+ });
4019
+ app.get("/api/v1/movement/month", async (request) => {
4020
+ const query = request.query;
4021
+ return {
4022
+ movement: getMovementMonthSummary({
4023
+ month: typeof query.month === "string" ? query.month : undefined,
4024
+ userIds: resolveScopedUserIds(query)
4025
+ })
4026
+ };
4027
+ });
4028
+ app.get("/api/v1/movement/all-time", async (request) => ({
4029
+ movement: getMovementAllTimeSummary(resolveScopedUserIds(request.query))
4030
+ }));
4031
+ app.get("/api/v1/movement/timeline", async (request) => {
4032
+ const parsed = movementTimelineQuerySchema.parse(request.query ?? {});
4033
+ return {
4034
+ movement: getMovementTimeline({
4035
+ ...parsed,
4036
+ userIds: parsed.userIds.length > 0
4037
+ ? parsed.userIds
4038
+ : (resolveScopedUserIds(request.query) ?? [])
4039
+ })
4040
+ };
4041
+ });
4042
+ app.get("/api/v1/movement/settings", async (request) => ({
4043
+ settings: getMovementSettings(resolveScopedUserIds(request.query))
4044
+ }));
4045
+ app.patch("/api/v1/movement/settings", async (request) => {
4046
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/settings" });
4047
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
4048
+ getDefaultUser().id;
4049
+ return {
4050
+ settings: updateMovementSettings(userId, movementSettingsPatchSchema.parse(request.body ?? {}), toActivityContext(auth))
4051
+ };
4052
+ });
4053
+ app.get("/api/v1/movement/places", async (request) => ({
4054
+ places: listMovementPlaces(resolveScopedUserIds(request.query))
4055
+ }));
4056
+ app.post("/api/v1/movement/places", async (request, reply) => {
4057
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/places" });
4058
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
4059
+ getDefaultUser().id;
4060
+ reply.code(201);
4061
+ return {
4062
+ place: createMovementPlace({
4063
+ ...movementPlaceMutationSchema.parse(request.body ?? {}),
4064
+ userId,
4065
+ source: "user"
4066
+ }, toActivityContext(auth))
4067
+ };
4068
+ });
4069
+ app.patch("/api/v1/movement/places/:id", async (request, reply) => {
4070
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/places/:id" });
4071
+ const { id } = request.params;
4072
+ const place = updateMovementPlace(id, movementPlacePatchSchema.parse(request.body ?? {}), toActivityContext(auth));
4073
+ if (!place) {
4074
+ reply.code(404);
4075
+ return { error: "Movement place not found" };
4076
+ }
4077
+ return { place };
4078
+ });
4079
+ app.patch("/api/v1/movement/stays/:id", async (request, reply) => {
4080
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/stays/:id" });
4081
+ const { id } = request.params;
4082
+ const stay = updateMovementStay(id, movementStayPatchSchema.parse(request.body ?? {}), toActivityContext(auth));
4083
+ if (!stay) {
4084
+ reply.code(404);
4085
+ return { error: "Movement stay not found" };
4086
+ }
4087
+ return { stay };
4088
+ });
4089
+ app.delete("/api/v1/movement/stays/:id", async (request, reply) => {
4090
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/stays/:id" });
4091
+ const { id } = request.params;
4092
+ const result = deleteMovementStay(id, toActivityContext(auth));
4093
+ if (!result) {
4094
+ reply.code(404);
4095
+ return { error: "Movement stay not found" };
4096
+ }
4097
+ return result;
4098
+ });
4099
+ app.patch("/api/v1/movement/trips/:id", async (request, reply) => {
4100
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/trips/:id" });
4101
+ const { id } = request.params;
4102
+ const trip = updateMovementTrip(id, movementTripPatchSchema.parse(request.body ?? {}), toActivityContext(auth));
4103
+ if (!trip) {
4104
+ reply.code(404);
4105
+ return { error: "Movement trip not found" };
4106
+ }
4107
+ return { trip };
4108
+ });
4109
+ app.delete("/api/v1/movement/trips/:id", async (request, reply) => {
4110
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/trips/:id" });
4111
+ const { id } = request.params;
4112
+ const result = deleteMovementTrip(id, toActivityContext(auth));
4113
+ if (!result) {
4114
+ reply.code(404);
4115
+ return { error: "Movement trip not found" };
4116
+ }
4117
+ return result;
4118
+ });
4119
+ app.patch("/api/v1/movement/trips/:id/points/:pointId", async (request, reply) => {
4120
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/trips/:id/points/:pointId" });
4121
+ const { id, pointId } = request.params;
4122
+ const result = updateMovementTripPoint(id, pointId, movementTripPointPatchSchema.parse(request.body ?? {}), toActivityContext(auth));
4123
+ if (!result) {
4124
+ reply.code(404);
4125
+ return { error: "Movement datapoint not found" };
4126
+ }
4127
+ return result;
4128
+ });
4129
+ app.delete("/api/v1/movement/trips/:id/points/:pointId", async (request, reply) => {
4130
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/trips/:id/points/:pointId" });
4131
+ const { id, pointId } = request.params;
4132
+ const result = deleteMovementTripPoint(id, pointId, toActivityContext(auth));
4133
+ if (!result) {
4134
+ reply.code(404);
4135
+ return { error: "Movement datapoint not found" };
4136
+ }
4137
+ return result;
4138
+ });
4139
+ app.get("/api/v1/movement/trips/:id", async (request, reply) => {
4140
+ const { id } = request.params;
4141
+ const movement = getMovementTripDetail(id);
4142
+ if (!movement) {
4143
+ reply.code(404);
4144
+ return { error: "Movement trip not found" };
4145
+ }
4146
+ return { movement };
4147
+ });
4148
+ app.post("/api/v1/movement/selection", async (request) => ({
4149
+ movement: getMovementSelectionAggregate(movementSelectionAggregateSchema.parse(request.body ?? {}))
4150
+ }));
3861
4151
  app.post("/api/v1/health/pairing-sessions", async (request, reply) => {
3862
4152
  requireOperatorSession(request.headers, {
3863
4153
  route: "/api/v1/health/pairing-sessions"
@@ -3895,6 +4185,106 @@ export async function buildServer(options = {}) {
3895
4185
  app.post("/api/v1/mobile/pairing/verify", async (request) => ({
3896
4186
  pairing: verifyCompanionPairing(verifyCompanionPairingSchema.parse(request.body ?? {}))
3897
4187
  }));
4188
+ app.post("/api/v1/mobile/movement/bootstrap", async (request) => {
4189
+ const parsed = movementMobileBootstrapSchema.parse(request.body ?? {});
4190
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4191
+ return {
4192
+ movement: getMovementMobileBootstrap(pairing)
4193
+ };
4194
+ });
4195
+ app.post("/api/v1/mobile/movement/places", async (request, reply) => {
4196
+ const parsed = movementMobilePlaceMutationSchema.parse(request.body ?? {});
4197
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4198
+ reply.code(201);
4199
+ return {
4200
+ place: createMovementPlace({
4201
+ ...parsed.place,
4202
+ userId: pairing.user_id,
4203
+ source: "companion"
4204
+ }, {
4205
+ actor: "Forge Companion",
4206
+ source: "system"
4207
+ })
4208
+ };
4209
+ });
4210
+ app.post("/api/v1/mobile/movement/timeline", async (request) => {
4211
+ const parsed = movementMobileTimelineSchema.parse(request.body ?? {});
4212
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4213
+ return {
4214
+ movement: getMovementTimeline({
4215
+ before: parsed.before,
4216
+ limit: parsed.limit,
4217
+ userIds: [pairing.user_id]
4218
+ })
4219
+ };
4220
+ });
4221
+ app.patch("/api/v1/mobile/movement/stays/:id", async (request, reply) => {
4222
+ const parsed = movementMobileStayPatchSchema.parse(request.body ?? {});
4223
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4224
+ const { id } = request.params;
4225
+ const stay = updateMovementStay(id, parsed.patch, {
4226
+ actor: "Forge Companion",
4227
+ source: "system"
4228
+ }, { userId: pairing.user_id });
4229
+ if (!stay) {
4230
+ reply.code(404);
4231
+ return { error: "Movement stay not found" };
4232
+ }
4233
+ return { stay };
4234
+ });
4235
+ app.patch("/api/v1/mobile/movement/trips/:id", async (request, reply) => {
4236
+ const parsed = movementMobileTripPatchSchema.parse(request.body ?? {});
4237
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4238
+ const { id } = request.params;
4239
+ const trip = updateMovementTrip(id, parsed.patch, {
4240
+ actor: "Forge Companion",
4241
+ source: "system"
4242
+ }, { userId: pairing.user_id });
4243
+ if (!trip) {
4244
+ reply.code(404);
4245
+ return { error: "Movement trip not found" };
4246
+ }
4247
+ return { trip };
4248
+ });
4249
+ app.post("/api/v1/mobile/watch/bootstrap", async (request) => {
4250
+ const parsed = mobileWatchBootstrapSchema.parse(request.body ?? {});
4251
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4252
+ assertWatchReady(pairing);
4253
+ return {
4254
+ watch: buildWatchBootstrap(pairing)
4255
+ };
4256
+ });
4257
+ app.post("/api/v1/mobile/watch/habits/:id/check-ins", async (request, reply) => {
4258
+ const parsed = mobileWatchHabitCheckInSchema.parse(request.body ?? {});
4259
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4260
+ assertWatchReady(pairing);
4261
+ const { id } = request.params;
4262
+ const habit = createHabitCheckIn(id, {
4263
+ dateKey: parsed.dateKey,
4264
+ status: parsed.status,
4265
+ note: parsed.note
4266
+ }, { source: "system", actor: `watch:${parsed.dedupeKey}` });
4267
+ if (!habit) {
4268
+ reply.code(404);
4269
+ return { error: "Habit not found" };
4270
+ }
4271
+ return {
4272
+ habit,
4273
+ metrics: buildXpMetricsPayload(),
4274
+ watch: buildWatchBootstrap(pairing, {
4275
+ anchorDateKey: parsed.dateKey
4276
+ })
4277
+ };
4278
+ });
4279
+ app.post("/api/v1/mobile/watch/capture-events:batch", async (request) => {
4280
+ const parsed = mobileWatchCaptureBatchSchema.parse(request.body ?? {});
4281
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4282
+ assertWatchReady(pairing);
4283
+ return {
4284
+ receipt: ingestWatchCaptureBatch(pairing, parsed),
4285
+ watch: buildWatchBootstrap(pairing)
4286
+ };
4287
+ });
3898
4288
  app.post("/api/v1/mobile/healthkit/sync", async (request) => ({
3899
4289
  sync: ingestMobileHealthSync(mobileHealthSyncSchema.parse(request.body ?? {}))
3900
4290
  }));
@@ -3948,6 +4338,88 @@ export async function buildServer(options = {}) {
3948
4338
  const userIds = resolveScopedUserIds(request.query);
3949
4339
  return { overview: getPsycheOverview(userIds) };
3950
4340
  });
4341
+ app.get("/api/v1/psyche/questionnaires", async (request) => {
4342
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/questionnaires" });
4343
+ const userIds = resolveScopedUserIds(request.query);
4344
+ return listQuestionnaireInstruments({ userIds });
4345
+ });
4346
+ app.post("/api/v1/psyche/questionnaires", async (request, reply) => {
4347
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires" });
4348
+ const result = createQuestionnaireInstrument(createQuestionnaireInstrumentSchema.parse(request.body ?? {}), toActivityContext(auth));
4349
+ reply.code(201);
4350
+ return result;
4351
+ });
4352
+ app.get("/api/v1/psyche/questionnaires/:id", async (request) => {
4353
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/questionnaires/:id" });
4354
+ const { id } = request.params;
4355
+ const userIds = resolveScopedUserIds(request.query);
4356
+ return getQuestionnaireInstrumentDetail(id, { userIds });
4357
+ });
4358
+ app.post("/api/v1/psyche/questionnaires/:id/clone", async (request, reply) => {
4359
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id/clone" });
4360
+ const { id } = request.params;
4361
+ const body = (request.body ?? {});
4362
+ const userId = typeof body.userId === "string" && body.userId.trim().length > 0
4363
+ ? body.userId.trim()
4364
+ : null;
4365
+ const result = cloneQuestionnaireInstrument(id, { userId }, toActivityContext(auth));
4366
+ reply.code(201);
4367
+ return result;
4368
+ });
4369
+ app.post("/api/v1/psyche/questionnaires/:id/draft", async (request) => {
4370
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id/draft" });
4371
+ const { id } = request.params;
4372
+ return ensureQuestionnaireDraftVersion(id, toActivityContext(auth));
4373
+ });
4374
+ app.patch("/api/v1/psyche/questionnaires/:id/draft", async (request) => {
4375
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id/draft" });
4376
+ const { id } = request.params;
4377
+ return updateQuestionnaireDraftVersion(id, updateQuestionnaireVersionSchema.parse(request.body ?? {}), toActivityContext(auth));
4378
+ });
4379
+ app.post("/api/v1/psyche/questionnaires/:id/publish", async (request) => {
4380
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id/publish" });
4381
+ const { id } = request.params;
4382
+ return publishQuestionnaireDraftVersion(id, publishQuestionnaireVersionSchema.parse(request.body ?? {}), toActivityContext(auth));
4383
+ });
4384
+ app.post("/api/v1/psyche/questionnaires/:id/runs", async (request, reply) => {
4385
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write", "psyche.read"], { route: "/api/v1/psyche/questionnaires/:id/runs" });
4386
+ const { id } = request.params;
4387
+ const result = startQuestionnaireRun(id, startQuestionnaireRunSchema.parse(request.body ?? {}), toActivityContext(auth));
4388
+ reply.code(201);
4389
+ return result;
4390
+ });
4391
+ app.get("/api/v1/psyche/questionnaire-runs/:id", async (request) => {
4392
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/questionnaire-runs/:id" });
4393
+ const { id } = request.params;
4394
+ const userIds = resolveScopedUserIds(request.query);
4395
+ return getQuestionnaireRunDetail(id, { userIds });
4396
+ });
4397
+ app.patch("/api/v1/psyche/questionnaire-runs/:id", async (request) => {
4398
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaire-runs/:id" });
4399
+ const { id } = request.params;
4400
+ return updateQuestionnaireRun(id, updateQuestionnaireRunSchema.parse(request.body ?? {}), toActivityContext(auth));
4401
+ });
4402
+ app.post("/api/v1/psyche/questionnaire-runs/:id/complete", async (request) => {
4403
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaire-runs/:id/complete" });
4404
+ const { id } = request.params;
4405
+ return completeQuestionnaireRun(id, toActivityContext(auth));
4406
+ });
4407
+ app.get("/api/v1/psyche/self-observation/calendar", async (request) => {
4408
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/self-observation/calendar" });
4409
+ const query = calendarOverviewQuerySchema.parse(request.query ?? {});
4410
+ const now = new Date();
4411
+ const from = query.from ??
4412
+ new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
4413
+ const to = query.to ??
4414
+ new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
4415
+ return {
4416
+ calendar: getPsycheObservationCalendar({
4417
+ from,
4418
+ to,
4419
+ userIds: query.userIds
4420
+ })
4421
+ };
4422
+ });
3951
4423
  app.get("/api/v1/psyche/values", async (request) => {
3952
4424
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/values" });
3953
4425
  const userIds = resolveScopedUserIds(request.query);
@@ -6247,9 +6719,373 @@ export async function buildServer(options = {}) {
6247
6719
  app.patch("/api/v1/settings", async (request) => {
6248
6720
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings" });
6249
6721
  return {
6250
- settings: updateSettings(updateSettingsSchema.parse(request.body ?? {}), toActivityContext(auth))
6722
+ settings: updateSettings(updateSettingsSchema.parse(request.body ?? {}), {
6723
+ activity: toActivityContext(auth),
6724
+ secrets: managers.secrets
6725
+ })
6726
+ };
6727
+ });
6728
+ app.post("/api/v1/settings/models/connections", async (request, reply) => {
6729
+ requireScopedAccess(request.headers, ["write"], {
6730
+ route: "/api/v1/settings/models/connections"
6731
+ });
6732
+ const parsed = upsertAiModelConnectionSchema.parse(request.body ?? {});
6733
+ const oauthCredential = parsed.oauthSessionId?.trim()
6734
+ ? consumeOpenAiCodexOauthCredentials(parsed.oauthSessionId.trim())
6735
+ : null;
6736
+ const connection = upsertAiModelConnection(parsed, managers.secrets, {
6737
+ oauthCredential
6738
+ });
6739
+ const currentSettings = getSettings();
6740
+ const selectedWikiConnectionId = currentSettings.modelSettings.forgeAgent.wiki.connectionId;
6741
+ if (selectedWikiConnectionId === connection.id) {
6742
+ updateSettings({
6743
+ modelSettings: {
6744
+ forgeAgent: {
6745
+ wiki: {
6746
+ connectionId: connection.id,
6747
+ model: parsed.model
6748
+ }
6749
+ }
6750
+ }
6751
+ }, { secrets: managers.secrets });
6752
+ }
6753
+ reply.code(201);
6754
+ return { connection };
6755
+ });
6756
+ app.delete("/api/v1/settings/models/connections/:id", async (request, reply) => {
6757
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings/models/connections/:id" });
6758
+ const deletedId = deleteAiModelConnection(request.params.id, managers.secrets);
6759
+ if (!deletedId) {
6760
+ reply.code(404);
6761
+ return { error: "AI model connection not found" };
6762
+ }
6763
+ return { deletedId };
6764
+ });
6765
+ app.post("/api/v1/settings/models/connections/test", async (request, reply) => {
6766
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings/models/connections/test" });
6767
+ const parsed = testAiModelConnectionSchema.parse(request.body ?? {});
6768
+ const existing = parsed.connectionId
6769
+ ? getAiModelConnectionById(parsed.connectionId)
6770
+ : null;
6771
+ const credential = parsed.connectionId
6772
+ ? readModelConnectionCredential(parsed.connectionId, managers.secrets)
6773
+ : null;
6774
+ const explicitApiKey = parsed.apiKey?.trim() ||
6775
+ (credential?.kind === "api_key"
6776
+ ? credential.apiKey
6777
+ : credential?.kind === "oauth"
6778
+ ? credential.access
6779
+ : null);
6780
+ const result = await managers.llm.testWikiConnection({
6781
+ provider: parsed.provider ?? existing?.provider ?? "openai-api",
6782
+ baseUrl: parsed.baseUrl?.trim() ||
6783
+ existing?.baseUrl ||
6784
+ "https://api.openai.com/v1",
6785
+ model: parsed.model,
6786
+ systemPrompt: "",
6787
+ secretId: null,
6788
+ metadata: {}
6789
+ }, explicitApiKey, ({ level, message, details = {} }) => {
6790
+ recordDiagnosticLog({
6791
+ level,
6792
+ source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
6793
+ scope: typeof details.scope === "string" ? details.scope : "model_settings",
6794
+ eventKey: typeof details.eventKey === "string"
6795
+ ? details.eventKey
6796
+ : "model_connection_test",
6797
+ message,
6798
+ route: "/api/v1/settings/models/connections/test",
6799
+ functionName: "testModelConnection",
6800
+ details
6801
+ });
6802
+ });
6803
+ reply.code(200);
6804
+ return { result };
6805
+ });
6806
+ app.post("/api/v1/settings/models/oauth/openai-codex/start", async (request) => {
6807
+ requireScopedAccess(request.headers, ["write"], {
6808
+ route: "/api/v1/settings/models/oauth/openai-codex/start"
6809
+ });
6810
+ return { session: await startOpenAiCodexOauthSession() };
6811
+ });
6812
+ app.get("/api/v1/settings/models/oauth/openai-codex/session/:id", async (request, reply) => {
6813
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings/models/oauth/openai-codex/session/:id" });
6814
+ try {
6815
+ return {
6816
+ session: getOpenAiCodexOauthSession(request.params.id)
6817
+ };
6818
+ }
6819
+ catch (error) {
6820
+ if (error instanceof Error &&
6821
+ error.message.startsWith("Unknown OpenAI Codex OAuth session")) {
6822
+ reply.code(404);
6823
+ return { error: "OpenAI Codex OAuth session not found" };
6824
+ }
6825
+ throw error;
6826
+ }
6827
+ });
6828
+ app.post("/api/v1/settings/models/oauth/openai-codex/session/:id/manual", async (request) => {
6829
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings/models/oauth/openai-codex/session/:id/manual" });
6830
+ return {
6831
+ session: submitOpenAiCodexOauthManualInput(request.params.id, submitOpenAiCodexOauthManualCodeSchema.parse(request.body ?? {})
6832
+ .codeOrUrl)
6833
+ };
6834
+ });
6835
+ app.get("/api/v1/surfaces/:surfaceId/ai-processors", async (request) => {
6836
+ requireScopedAccess(request.headers, ["read"], {
6837
+ route: "/api/v1/surfaces/:surfaceId/ai-processors"
6838
+ });
6839
+ return {
6840
+ graph: getSurfaceProcessorGraph(request.params.surfaceId)
6841
+ };
6842
+ });
6843
+ app.get("/api/v1/surfaces/:surfaceId/layout", async (request) => {
6844
+ requireScopedAccess(request.headers, ["read"], {
6845
+ route: "/api/v1/surfaces/:surfaceId/layout"
6846
+ });
6847
+ return {
6848
+ layout: getSurfaceLayout(request.params.surfaceId)
6849
+ };
6850
+ });
6851
+ app.put("/api/v1/surfaces/:surfaceId/layout", async (request) => {
6852
+ requireScopedAccess(request.headers, ["write"], {
6853
+ route: "/api/v1/surfaces/:surfaceId/layout"
6854
+ });
6855
+ const surfaceId = request.params.surfaceId;
6856
+ return {
6857
+ layout: saveSurfaceLayout(surfaceId, writeSurfaceLayoutSchema.parse(request.body ?? {}))
6858
+ };
6859
+ });
6860
+ app.post("/api/v1/surfaces/:surfaceId/layout/reset", async (request) => {
6861
+ requireScopedAccess(request.headers, ["write"], {
6862
+ route: "/api/v1/surfaces/:surfaceId/layout/reset"
6863
+ });
6864
+ return {
6865
+ layout: resetSurfaceLayout(request.params.surfaceId)
6866
+ };
6867
+ });
6868
+ app.post("/api/v1/surfaces/:surfaceId/ai-processors", async (request, reply) => {
6869
+ requireScopedAccess(request.headers, ["write"], {
6870
+ route: "/api/v1/surfaces/:surfaceId/ai-processors"
6871
+ });
6872
+ const body = createAiProcessorSchema.parse({
6873
+ ...(request.body ?? {}),
6874
+ surfaceId: request.params.surfaceId
6875
+ });
6876
+ const processor = createAiProcessor({
6877
+ ...body
6878
+ });
6879
+ reply.code(201);
6880
+ return { processor };
6881
+ });
6882
+ app.patch("/api/v1/ai-processors/:id", async (request, reply) => {
6883
+ requireScopedAccess(request.headers, ["write"], {
6884
+ route: "/api/v1/ai-processors/:id"
6885
+ });
6886
+ const processor = updateAiProcessor(request.params.id, updateAiProcessorSchema.parse(request.body ?? {}));
6887
+ if (!processor) {
6888
+ reply.code(404);
6889
+ return { error: "AI processor not found" };
6890
+ }
6891
+ return { processor };
6892
+ });
6893
+ app.delete("/api/v1/ai-processors/:id", async (request, reply) => {
6894
+ requireScopedAccess(request.headers, ["write"], {
6895
+ route: "/api/v1/ai-processors/:id"
6896
+ });
6897
+ const processor = deleteAiProcessor(request.params.id);
6898
+ if (!processor) {
6899
+ reply.code(404);
6900
+ return { error: "AI processor not found" };
6901
+ }
6902
+ return { processor };
6903
+ });
6904
+ app.post("/api/v1/ai-processor-links", async (request, reply) => {
6905
+ requireScopedAccess(request.headers, ["write"], {
6906
+ route: "/api/v1/ai-processor-links"
6907
+ });
6908
+ const link = createAiProcessorLink(createAiProcessorLinkSchema.parse(request.body ?? {}));
6909
+ reply.code(201);
6910
+ return { link };
6911
+ });
6912
+ app.delete("/api/v1/ai-processor-links/:id", async (request, reply) => {
6913
+ requireScopedAccess(request.headers, ["write"], {
6914
+ route: "/api/v1/ai-processor-links/:id"
6915
+ });
6916
+ const link = deleteAiProcessorLink(request.params.id);
6917
+ if (!link) {
6918
+ reply.code(404);
6919
+ return { error: "AI processor link not found" };
6920
+ }
6921
+ return { link };
6922
+ });
6923
+ app.post("/api/v1/ai-processors/:id/run", async (request, reply) => {
6924
+ requireScopedAccess(request.headers, ["write"], {
6925
+ route: "/api/v1/ai-processors/:id/run"
6926
+ });
6927
+ const processor = getAiProcessorById(request.params.id);
6928
+ if (!processor) {
6929
+ reply.code(404);
6930
+ return { error: "AI processor not found" };
6931
+ }
6932
+ return await runAiProcessor(processor.id, runAiProcessorSchema.parse(request.body ?? {}), {
6933
+ llm: managers.llm,
6934
+ secrets: managers.secrets
6935
+ }, { trigger: "manual" });
6936
+ });
6937
+ app.get("/api/v1/aiproc/:slug", async (request, reply) => {
6938
+ requireScopedAccess(request.headers, ["read"], {
6939
+ route: "/api/v1/aiproc/:slug"
6940
+ });
6941
+ const processor = getAiProcessorBySlug(request.params.slug);
6942
+ if (!processor) {
6943
+ reply.code(404);
6944
+ return { error: "AI processor not found" };
6945
+ }
6946
+ return { processor };
6947
+ });
6948
+ app.post("/api/v1/aiproc/:slug/run", async (request, reply) => {
6949
+ requireScopedAccess(request.headers, ["write"], {
6950
+ route: "/api/v1/aiproc/:slug/run"
6951
+ });
6952
+ const processor = getAiProcessorBySlug(request.params.slug);
6953
+ if (!processor) {
6954
+ reply.code(404);
6955
+ return { error: "AI processor not found" };
6956
+ }
6957
+ return await runAiProcessor(processor.id, runAiProcessorSchema.parse(request.body ?? {}), {
6958
+ llm: managers.llm,
6959
+ secrets: managers.secrets
6960
+ }, { trigger: "route" });
6961
+ });
6962
+ app.get("/api/v1/ai-connectors/catalog/boxes", async (request) => {
6963
+ requireScopedAccess(request.headers, ["read"], {
6964
+ route: "/api/v1/ai-connectors/catalog/boxes"
6965
+ });
6966
+ return {
6967
+ boxes: listForgeBoxCatalog()
6968
+ };
6969
+ });
6970
+ app.get("/api/v1/ai-connectors", async (request) => {
6971
+ requireScopedAccess(request.headers, ["read"], {
6972
+ route: "/api/v1/ai-connectors"
6973
+ });
6974
+ return {
6975
+ connectors: listAiConnectors()
6251
6976
  };
6252
6977
  });
6978
+ app.post("/api/v1/ai-connectors", async (request, reply) => {
6979
+ requireScopedAccess(request.headers, ["write"], {
6980
+ route: "/api/v1/ai-connectors"
6981
+ });
6982
+ const connector = createAiConnector(createAiConnectorSchema.parse(request.body ?? {}));
6983
+ reply.code(201);
6984
+ return { connector };
6985
+ });
6986
+ app.get("/api/v1/ai-connectors/:id", async (request, reply) => {
6987
+ requireScopedAccess(request.headers, ["read"], {
6988
+ route: "/api/v1/ai-connectors/:id"
6989
+ });
6990
+ const connector = getAiConnectorById(request.params.id);
6991
+ if (!connector) {
6992
+ reply.code(404);
6993
+ return { error: "AI connector not found" };
6994
+ }
6995
+ return {
6996
+ connector,
6997
+ runs: listAiConnectorRuns(connector.id),
6998
+ conversation: getAiConnectorConversationForConnector(connector.id)
6999
+ };
7000
+ });
7001
+ app.patch("/api/v1/ai-connectors/:id", async (request, reply) => {
7002
+ requireScopedAccess(request.headers, ["write"], {
7003
+ route: "/api/v1/ai-connectors/:id"
7004
+ });
7005
+ const connector = updateAiConnector(request.params.id, updateAiConnectorSchema.parse(request.body ?? {}));
7006
+ if (!connector) {
7007
+ reply.code(404);
7008
+ return { error: "AI connector not found" };
7009
+ }
7010
+ return { connector };
7011
+ });
7012
+ app.delete("/api/v1/ai-connectors/:id", async (request, reply) => {
7013
+ requireScopedAccess(request.headers, ["write"], {
7014
+ route: "/api/v1/ai-connectors/:id"
7015
+ });
7016
+ const connector = deleteAiConnector(request.params.id);
7017
+ if (!connector) {
7018
+ reply.code(404);
7019
+ return { error: "AI connector not found" };
7020
+ }
7021
+ return { connector };
7022
+ });
7023
+ app.post("/api/v1/ai-connectors/:id/run", async (request, reply) => {
7024
+ requireScopedAccess(request.headers, ["write"], {
7025
+ route: "/api/v1/ai-connectors/:id/run"
7026
+ });
7027
+ const connector = getAiConnectorById(request.params.id);
7028
+ if (!connector) {
7029
+ reply.code(404);
7030
+ return { error: "AI connector not found" };
7031
+ }
7032
+ return await runAiConnector(connector.id, runAiConnectorSchema.parse(request.body ?? {}), {
7033
+ llm: managers.llm,
7034
+ secrets: managers.secrets
7035
+ }, "run");
7036
+ });
7037
+ app.post("/api/v1/ai-connectors/:id/chat", async (request, reply) => {
7038
+ requireScopedAccess(request.headers, ["write"], {
7039
+ route: "/api/v1/ai-connectors/:id/chat"
7040
+ });
7041
+ const connector = getAiConnectorById(request.params.id);
7042
+ if (!connector) {
7043
+ reply.code(404);
7044
+ return { error: "AI connector not found" };
7045
+ }
7046
+ return await runAiConnector(connector.id, runAiConnectorSchema.parse(request.body ?? {}), {
7047
+ llm: managers.llm,
7048
+ secrets: managers.secrets
7049
+ }, "chat");
7050
+ });
7051
+ app.get("/api/v1/ai-connectors/:id/output", async (request, reply) => {
7052
+ requireScopedAccess(request.headers, ["read"], {
7053
+ route: "/api/v1/ai-connectors/:id/output"
7054
+ });
7055
+ const connector = getAiConnectorById(request.params.id);
7056
+ if (!connector) {
7057
+ reply.code(404);
7058
+ return { error: "AI connector not found" };
7059
+ }
7060
+ return {
7061
+ connector,
7062
+ output: connector.lastRun?.result ?? null
7063
+ };
7064
+ });
7065
+ app.get("/api/v1/ai-connectors/:id/runs", async (request, reply) => {
7066
+ requireScopedAccess(request.headers, ["read"], {
7067
+ route: "/api/v1/ai-connectors/:id/runs"
7068
+ });
7069
+ const connector = getAiConnectorById(request.params.id);
7070
+ if (!connector) {
7071
+ reply.code(404);
7072
+ return { error: "AI connector not found" };
7073
+ }
7074
+ return {
7075
+ runs: listAiConnectorRuns(connector.id)
7076
+ };
7077
+ });
7078
+ app.get("/api/v1/ai-connectors/by-slug/:slug", async (request, reply) => {
7079
+ requireScopedAccess(request.headers, ["read"], {
7080
+ route: "/api/v1/ai-connectors/by-slug/:slug"
7081
+ });
7082
+ const connector = getAiConnectorBySlug(request.params.slug);
7083
+ if (!connector) {
7084
+ reply.code(404);
7085
+ return { error: "AI connector not found" };
7086
+ }
7087
+ return { connector };
7088
+ });
6253
7089
  app.post("/api/v1/settings/tokens", async (request, reply) => {
6254
7090
  const auth = requireOperatorSession(request.headers, { route: "/api/v1/settings/tokens" });
6255
7091
  const token = managers.token.issueLocalAgentToken(createAgentTokenSchema.parse(request.body ?? {}), auth);