forge-openclaw-plugin 0.2.7 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -1
- package/dist/assets/{board-CzgvdLO8.js → board-C_m78kvK.js} +2 -2
- package/dist/assets/{board-CzgvdLO8.js.map → board-C_m78kvK.js.map} +1 -1
- package/dist/assets/index-BWtLtXwb.js +36 -0
- package/dist/assets/index-BWtLtXwb.js.map +1 -0
- package/dist/assets/index-Dp5GXY_z.css +1 -0
- package/dist/assets/{motion-STUd1O46.js → motion-CpZvZumD.js} +2 -2
- package/dist/assets/{motion-STUd1O46.js.map → motion-CpZvZumD.js.map} +1 -1
- package/dist/assets/{table-CtNlETLc.js → table-DtyXTw03.js} +2 -2
- package/dist/assets/{table-CtNlETLc.js.map → table-DtyXTw03.js.map} +1 -1
- package/dist/assets/{ui-ThzkR_oW.js → ui-BXbpiKyS.js} +2 -2
- package/dist/assets/{ui-ThzkR_oW.js.map → ui-BXbpiKyS.js.map} +1 -1
- package/dist/assets/{vendor-DyHAI6nk.js → vendor-QBH6qVEe.js} +84 -74
- package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
- package/dist/assets/{viz-BJuBCz_G.js → viz-w-IMeueL.js} +2 -2
- package/dist/assets/{viz-BJuBCz_G.js.map → viz-w-IMeueL.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/api-client.d.ts +1 -0
- package/dist/openclaw/local-runtime.d.ts +8 -0
- package/dist/openclaw/local-runtime.js +123 -1
- package/dist/openclaw/plugin-entry-shared.js +12 -0
- package/dist/openclaw/routes.js +4 -0
- package/dist/server/app.js +104 -67
- package/dist/server/demo-data.js +49 -0
- package/dist/server/openapi.js +84 -43
- package/dist/server/psyche-types.js +1 -30
- package/dist/server/repositories/deleted-entities.js +60 -26
- package/dist/server/repositories/goals.js +2 -5
- package/dist/server/repositories/notes.js +359 -0
- package/dist/server/repositories/projects.js +2 -5
- package/dist/server/repositories/psyche.js +11 -14
- package/dist/server/repositories/task-runs.js +2 -0
- package/dist/server/repositories/tasks.js +21 -10
- package/dist/server/seed-demo.js +11 -0
- package/dist/server/services/dashboard.js +4 -1
- package/dist/server/services/entity-crud.js +27 -30
- package/dist/server/services/insights.js +5 -5
- package/dist/server/services/projects.js +3 -1
- package/dist/server/services/psyche.js +4 -4
- package/dist/server/types.js +70 -11
- package/openclaw.plugin.json +12 -1
- package/package.json +1 -1
- package/server/migrations/001_core.sql +78 -0
- package/server/migrations/002_psyche.sql +164 -13
- package/skills/forge-openclaw/SKILL.md +19 -5
- package/dist/assets/index-8d_oM8fL.js +0 -27
- package/dist/assets/index-8d_oM8fL.js.map +0 -1
- package/dist/assets/index-D4A_bq8m.css +0 -1
- package/dist/assets/vendor-DyHAI6nk.js.map +0 -1
- package/dist/server/repositories/comments.js +0 -176
- package/server/migrations/003_timer_execution.sql +0 -18
- package/server/migrations/004_psyche_linked_entities.sql +0 -5
- package/server/migrations/005_adaptive_schemas.sql +0 -157
- package/server/migrations/006_psyche_auth_setting.sql +0 -4
- package/server/migrations/007_deleted_entities.sql +0 -16
package/dist/index.html
CHANGED
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
/>
|
|
14
14
|
<link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
15
15
|
<link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
16
|
-
<script type="module" crossorigin src="/forge/assets/index-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/motion-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/ui-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/table-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/viz-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/board-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-BWtLtXwb.js"></script>
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-QBH6qVEe.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/motion-CpZvZumD.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/ui-BXbpiKyS.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/table-DtyXTw03.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/viz-w-IMeueL.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/board-C_m78kvK.js">
|
|
23
23
|
<link rel="stylesheet" crossorigin href="/forge/assets/vendor-CRS-psbw.css">
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/forge/assets/index-
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/forge/assets/index-Dp5GXY_z.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body class="bg-canvas text-ink antialiased">
|
|
27
27
|
<div id="root"></div>
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { ForgePluginConfig } from "./api-client.js";
|
|
2
|
+
export type ForgeRuntimeStopResult = {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
stopped: boolean;
|
|
5
|
+
managed: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
pid: number | null;
|
|
8
|
+
};
|
|
2
9
|
export declare function ensureForgeRuntimeReady(config: ForgePluginConfig): Promise<void>;
|
|
3
10
|
export declare function primeForgeRuntime(config: ForgePluginConfig): void;
|
|
11
|
+
export declare function stopForgeRuntime(config: ForgePluginConfig): Promise<ForgeRuntimeStopResult>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
6
8
|
const STARTUP_TIMEOUT_MS = 15_000;
|
|
@@ -12,6 +14,63 @@ let startupPromise = null;
|
|
|
12
14
|
function runtimeKey(config) {
|
|
13
15
|
return `${config.origin}:${config.port}`;
|
|
14
16
|
}
|
|
17
|
+
function getRuntimeStatePath(config) {
|
|
18
|
+
const origin = new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
19
|
+
return path.join(homedir(), ".openclaw", "run", "forge-openclaw-plugin", `${origin}-${config.port}.json`);
|
|
20
|
+
}
|
|
21
|
+
async function writeRuntimeState(config, pid) {
|
|
22
|
+
const statePath = getRuntimeStatePath(config);
|
|
23
|
+
await mkdir(path.dirname(statePath), { recursive: true });
|
|
24
|
+
const payload = {
|
|
25
|
+
pid,
|
|
26
|
+
origin: config.origin,
|
|
27
|
+
port: config.port,
|
|
28
|
+
baseUrl: config.baseUrl,
|
|
29
|
+
startedAt: new Date().toISOString()
|
|
30
|
+
};
|
|
31
|
+
await writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
32
|
+
}
|
|
33
|
+
async function clearRuntimeState(config) {
|
|
34
|
+
await rm(getRuntimeStatePath(config), { force: true });
|
|
35
|
+
}
|
|
36
|
+
async function readRuntimeState(config) {
|
|
37
|
+
try {
|
|
38
|
+
const payload = await readFile(getRuntimeStatePath(config), "utf8");
|
|
39
|
+
const parsed = JSON.parse(payload);
|
|
40
|
+
if (typeof parsed.pid !== "number" || !Number.isFinite(parsed.pid)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
pid: Math.trunc(parsed.pid),
|
|
45
|
+
origin: typeof parsed.origin === "string" ? parsed.origin : config.origin,
|
|
46
|
+
port: typeof parsed.port === "number" ? parsed.port : config.port,
|
|
47
|
+
baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : config.baseUrl,
|
|
48
|
+
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : new Date(0).toISOString()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function processExists(pid) {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return !(error instanceof Error) || !("code" in error) || error.code !== "ESRCH";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
|
+
while (Date.now() < deadline) {
|
|
67
|
+
if (!processExists(pid)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, HEALTHCHECK_INTERVAL_MS));
|
|
71
|
+
}
|
|
72
|
+
return !processExists(pid);
|
|
73
|
+
}
|
|
15
74
|
function isLocalOrigin(origin) {
|
|
16
75
|
try {
|
|
17
76
|
return LOCAL_HOSTNAMES.has(new URL(origin).hostname.toLowerCase());
|
|
@@ -75,7 +134,8 @@ function spawnManagedRuntime(config, plan) {
|
|
|
75
134
|
...process.env,
|
|
76
135
|
HOST: "127.0.0.1",
|
|
77
136
|
PORT: String(config.port),
|
|
78
|
-
FORGE_BASE_PATH: "/forge/"
|
|
137
|
+
FORGE_BASE_PATH: "/forge/",
|
|
138
|
+
...(config.dataRoot ? { FORGE_DATA_ROOT: config.dataRoot } : {})
|
|
79
139
|
},
|
|
80
140
|
stdio: "ignore",
|
|
81
141
|
detached: true
|
|
@@ -86,9 +146,13 @@ function spawnManagedRuntime(config, plan) {
|
|
|
86
146
|
managedRuntimeChild = null;
|
|
87
147
|
managedRuntimeKey = null;
|
|
88
148
|
}
|
|
149
|
+
void clearRuntimeState(config);
|
|
89
150
|
});
|
|
90
151
|
managedRuntimeChild = child;
|
|
91
152
|
managedRuntimeKey = runtimeKey(config);
|
|
153
|
+
void writeRuntimeState(config, child.pid).catch(() => {
|
|
154
|
+
// State tracking is best effort. Runtime health checks remain authoritative.
|
|
155
|
+
});
|
|
92
156
|
}
|
|
93
157
|
async function waitForRuntime(config, timeoutMs) {
|
|
94
158
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -133,3 +197,61 @@ export function primeForgeRuntime(config) {
|
|
|
133
197
|
// Keep plugin registration non-blocking. Failures surface on first real call.
|
|
134
198
|
});
|
|
135
199
|
}
|
|
200
|
+
export async function stopForgeRuntime(config) {
|
|
201
|
+
if (!isLocalOrigin(config.origin)) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
stopped: false,
|
|
205
|
+
managed: false,
|
|
206
|
+
message: "Forge stop only supports local plugin-managed runtimes. Remote Forge targets must be stopped where they are hosted.",
|
|
207
|
+
pid: null
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const state = await readRuntimeState(config);
|
|
211
|
+
if (!state) {
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
stopped: false,
|
|
215
|
+
managed: false,
|
|
216
|
+
message: (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS))
|
|
217
|
+
? "Forge is running, but it does not look like a plugin-managed runtime. Stop it where it was started."
|
|
218
|
+
: "Forge is not running through the plugin-managed local runtime.",
|
|
219
|
+
pid: null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (!processExists(state.pid)) {
|
|
223
|
+
await clearRuntimeState(config);
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
stopped: false,
|
|
227
|
+
managed: true,
|
|
228
|
+
message: "The saved Forge runtime PID was stale. The plugin-managed runtime is already stopped.",
|
|
229
|
+
pid: state.pid
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
process.kill(state.pid, "SIGTERM");
|
|
233
|
+
if (!(await waitForProcessExit(state.pid, 5_000))) {
|
|
234
|
+
process.kill(state.pid, "SIGKILL");
|
|
235
|
+
if (!(await waitForProcessExit(state.pid, 2_000))) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
stopped: false,
|
|
239
|
+
managed: true,
|
|
240
|
+
message: `Forge runtime pid ${state.pid} did not stop cleanly.`,
|
|
241
|
+
pid: state.pid
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (managedRuntimeChild?.pid === state.pid) {
|
|
246
|
+
managedRuntimeChild = null;
|
|
247
|
+
managedRuntimeKey = null;
|
|
248
|
+
}
|
|
249
|
+
await clearRuntimeState(config);
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
stopped: true,
|
|
253
|
+
managed: true,
|
|
254
|
+
message: `Stopped the plugin-managed Forge runtime on ${config.baseUrl}.`,
|
|
255
|
+
pid: state.pid
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -45,6 +45,7 @@ export function resolveForgePluginConfig(pluginConfig) {
|
|
|
45
45
|
port,
|
|
46
46
|
baseUrl: buildForgeBaseUrl(origin, port),
|
|
47
47
|
webAppUrl: buildForgeWebAppUrl(origin, port),
|
|
48
|
+
dataRoot: typeof raw.dataRoot === "string" ? raw.dataRoot.trim() : "",
|
|
48
49
|
apiToken: typeof raw.apiToken === "string" ? raw.apiToken.trim() : "",
|
|
49
50
|
actorLabel: normalizeString(raw.actorLabel, "aurel"),
|
|
50
51
|
timeoutMs: normalizeTimeout(raw.timeoutMs, 15_000)
|
|
@@ -70,6 +71,11 @@ export const forgePluginConfigSchema = {
|
|
|
70
71
|
maximum: 65535,
|
|
71
72
|
description: "Forge server port. Override this when your local machine uses a different port."
|
|
72
73
|
},
|
|
74
|
+
dataRoot: {
|
|
75
|
+
type: "string",
|
|
76
|
+
default: "",
|
|
77
|
+
description: "Optional absolute path for the Forge data folder root. Leave blank to use the runtime working directory."
|
|
78
|
+
},
|
|
73
79
|
apiToken: {
|
|
74
80
|
type: "string",
|
|
75
81
|
default: "",
|
|
@@ -100,6 +106,12 @@ export const forgePluginConfigSchema = {
|
|
|
100
106
|
help: "Forge server port. Change this if your local machine uses another port.",
|
|
101
107
|
placeholder: "4317"
|
|
102
108
|
},
|
|
109
|
+
dataRoot: {
|
|
110
|
+
label: "Forge Data Root",
|
|
111
|
+
help: "Optional absolute folder path for Forge data. Use this when you want Forge to read and write a specific data directory instead of the runtime working directory.",
|
|
112
|
+
placeholder: "/Users/you/forge-data",
|
|
113
|
+
advanced: true
|
|
114
|
+
},
|
|
103
115
|
apiToken: {
|
|
104
116
|
label: "Forge API Token",
|
|
105
117
|
help: "Optional bearer token. Leave blank for one-step localhost or Tailscale operator-session bootstrap.",
|
package/dist/openclaw/routes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canBootstrapOperatorSession, callConfiguredForgeApi, expectForgeSuccess, readJsonRequestBody, readSingleHeaderValue, requireApiToken, writeForgeProxyResponse, writePluginError, writeRedirectResponse } from "./api-client.js";
|
|
2
|
+
import { stopForgeRuntime } from "./local-runtime.js";
|
|
2
3
|
import { collectSupportedPluginApiRouteKeys, makeApiRouteKey } from "./parity.js";
|
|
3
4
|
function passthroughSearch(path, url) {
|
|
4
5
|
return `${path}${url.search}`;
|
|
@@ -374,6 +375,9 @@ export function registerForgePluginCli(api, config) {
|
|
|
374
375
|
command.command("ui").description("Print the Forge UI entrypoint").action(async () => {
|
|
375
376
|
console.log(JSON.stringify({ webAppUrl: await resolveForgeUiUrl(config), pluginUiRoute: "/forge/v1/ui" }, null, 2));
|
|
376
377
|
});
|
|
378
|
+
command.command("stop").description("Stop the local Forge runtime when it was auto-started by the OpenClaw plugin").action(async () => {
|
|
379
|
+
console.log(JSON.stringify(await stopForgeRuntime(config), null, 2));
|
|
380
|
+
});
|
|
377
381
|
command.command("doctor").description("Run plugin connectivity and curated route diagnostics").action(async () => {
|
|
378
382
|
console.log(JSON.stringify(await runDoctor(config), null, 2));
|
|
379
383
|
});
|
package/dist/server/app.js
CHANGED
|
@@ -7,8 +7,8 @@ import { listActivityEvents, listActivityEventsForTask, removeActivityEvent } fr
|
|
|
7
7
|
import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
|
|
8
8
|
import { listEventLog } from "./repositories/event-log.js";
|
|
9
9
|
import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
|
|
10
|
-
import { createComment, getCommentById, listComments, updateComment } from "./repositories/comments.js";
|
|
11
10
|
import { listDomains } from "./repositories/domains.js";
|
|
11
|
+
import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
|
|
12
12
|
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";
|
|
13
13
|
import { createProject, updateProject } from "./repositories/projects.js";
|
|
14
14
|
import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
|
|
@@ -26,8 +26,8 @@ import { getProjectBoard, listProjectSummaries } from "./services/projects.js";
|
|
|
26
26
|
import { getWeeklyReviewPayload } from "./services/reviews.js";
|
|
27
27
|
import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
|
|
28
28
|
import { suggestTags } from "./services/tagging.js";
|
|
29
|
-
import { PSYCHE_ENTITY_TYPES, createBehaviorSchema,
|
|
30
|
-
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
|
|
29
|
+
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";
|
|
30
|
+
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
|
|
31
31
|
import { buildOpenApiDocument } from "./openapi.js";
|
|
32
32
|
import { registerWebRoutes } from "./web.js";
|
|
33
33
|
import { createManagerRuntime } from "./managers/runtime.js";
|
|
@@ -133,7 +133,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
133
133
|
{ name: "status", type: "active|paused|completed", required: false, description: "Current lifecycle state for the goal.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
|
|
134
134
|
{ name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the goal.", defaultValue: 400 },
|
|
135
135
|
{ name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c8a46b" },
|
|
136
|
-
{ name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the goal.", defaultValue: [] }
|
|
136
|
+
{ name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the goal.", defaultValue: [] },
|
|
137
|
+
{ name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new goal.", defaultValue: [] }
|
|
137
138
|
]
|
|
138
139
|
},
|
|
139
140
|
{
|
|
@@ -153,7 +154,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
153
154
|
{ name: "description", type: "string", required: false, description: "Desired outcome or scope.", defaultValue: "" },
|
|
154
155
|
{ name: "status", type: "active|paused|completed", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
|
|
155
156
|
{ name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the project.", defaultValue: 240 },
|
|
156
|
-
{ name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c0c1ff" }
|
|
157
|
+
{ name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c0c1ff" },
|
|
158
|
+
{ name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new project.", defaultValue: [] }
|
|
157
159
|
]
|
|
158
160
|
},
|
|
159
161
|
{
|
|
@@ -180,7 +182,27 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
180
182
|
{ name: "energy", type: "low|steady|high", required: false, description: "Energy demand.", enumValues: ["low", "steady", "high"], defaultValue: "steady" },
|
|
181
183
|
{ name: "points", type: "integer", required: false, description: "Reward value for the task.", defaultValue: 40 },
|
|
182
184
|
{ name: "sortOrder", type: "integer", required: false, description: "Lane ordering hint when set explicitly." },
|
|
183
|
-
{ name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the task.", defaultValue: [] }
|
|
185
|
+
{ name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the task.", defaultValue: [] },
|
|
186
|
+
{ name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new task.", defaultValue: [] }
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
entityType: "note",
|
|
191
|
+
purpose: "A Markdown note that can link to one or many Forge entities.",
|
|
192
|
+
minimumCreateFields: ["contentMarkdown", "links"],
|
|
193
|
+
relationshipRules: [
|
|
194
|
+
"Notes can link to goals, projects, tasks, Psyche records, and other supported Forge entities.",
|
|
195
|
+
"When nested under another create flow, notes auto-link to that new entity and can optionally include extra links."
|
|
196
|
+
],
|
|
197
|
+
searchHints: ["Search by Markdown content, author, or linked entity before creating a duplicate note."],
|
|
198
|
+
examples: [
|
|
199
|
+
'{"contentMarkdown":"Finished the review pass and captured the remaining edge cases.","links":[{"entityType":"task","entityId":"task_123"}]}',
|
|
200
|
+
'{"contentMarkdown":"Observed a stronger protector response after the meeting.","author":"forge-agent","links":[{"entityType":"trigger_report","entityId":"report_123"},{"entityType":"behavior_pattern","entityId":"pattern_123"}]}'
|
|
201
|
+
],
|
|
202
|
+
fieldGuide: [
|
|
203
|
+
{ name: "contentMarkdown", type: "string", required: true, description: "Markdown body of the note." },
|
|
204
|
+
{ name: "author", type: "string|null", required: false, description: "Optional display author for the note.", defaultValue: null, nullable: true },
|
|
205
|
+
{ name: "links", type: "Array<{ entityType, entityId, anchorKey? }>", required: true, description: "Entities this note should link to." }
|
|
184
206
|
]
|
|
185
207
|
},
|
|
186
208
|
{
|
|
@@ -540,9 +562,10 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
540
562
|
requiredFields: ["operations", "operations[].entityType", "operations[].data"],
|
|
541
563
|
notes: [
|
|
542
564
|
"entityType alone is never enough; full data is required.",
|
|
543
|
-
"Batch multiple related creates together when they come from one user ask."
|
|
565
|
+
"Batch multiple related creates together when they come from one user ask.",
|
|
566
|
+
"Goal, project, and task creates can include notes: [{ contentMarkdown, author?, links? }] and Forge will auto-link those notes to the newly created entity."
|
|
544
567
|
],
|
|
545
|
-
example: '{"operations":[{"entityType":"
|
|
568
|
+
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"}]}'
|
|
546
569
|
},
|
|
547
570
|
{
|
|
548
571
|
toolName: "forge_update_entities",
|
|
@@ -584,10 +607,10 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
584
607
|
toolName: "forge_log_work",
|
|
585
608
|
summary: "Log work that already happened.",
|
|
586
609
|
whenToUse: "Use for retroactive work, not for starting a live session.",
|
|
587
|
-
inputShape: "{ taskId?: string, title?: string, description?: string, summary?: string, goalId?: string|null, projectId?: string|null, owner?: string, status?: TaskStatus, priority?: TaskPriority, dueDate?: string|null, effort?: TaskEffort, energy?: TaskEnergy, points?: number, tagIds?: string[] }",
|
|
610
|
+
inputShape: "{ taskId?: string, title?: string, description?: string, summary?: string, goalId?: string|null, projectId?: string|null, owner?: string, status?: TaskStatus, priority?: TaskPriority, dueDate?: string|null, effort?: TaskEffort, energy?: TaskEnergy, points?: number, tagIds?: string[], closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
588
611
|
requiredFields: ["taskId or title"],
|
|
589
|
-
notes: ["Use taskId when logging work against an existing task.", "Use title when a new completed work item should be created and logged."],
|
|
590
|
-
example: '{"taskId":"task_123","summary":"Finished the review draft and cleaned the notes.","points":40}'
|
|
612
|
+
notes: ["Use taskId when logging work against an existing task.", "Use title when a new completed work item should be created and logged.", "closeoutNote persists the work summary as a real linked note."],
|
|
613
|
+
example: '{"taskId":"task_123","summary":"Finished the review draft and cleaned the notes.","points":40,"closeoutNote":{"contentMarkdown":"Finished the review draft, cleaned the note structure, and left one follow-up for QA."}}'
|
|
591
614
|
},
|
|
592
615
|
{
|
|
593
616
|
toolName: "forge_start_task_run",
|
|
@@ -620,19 +643,19 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
620
643
|
toolName: "forge_complete_task_run",
|
|
621
644
|
summary: "Finish an active run as completed work.",
|
|
622
645
|
whenToUse: "Use when the user has finished the live work block.",
|
|
623
|
-
inputShape: "{ taskRunId: string, actor?: string, note?: string }",
|
|
646
|
+
inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
624
647
|
requiredFields: ["taskRunId"],
|
|
625
|
-
notes: ["This is the truthful way to finish live work and award completion effects."],
|
|
626
|
-
example: '{"taskRunId":"run_123","actor":"aurel","note":"Finished the review draft"}'
|
|
648
|
+
notes: ["This is the truthful way to finish live work and award completion effects.", "closeoutNote persists a real linked note instead of only updating the transient run note."],
|
|
649
|
+
example: '{"taskRunId":"run_123","actor":"aurel","note":"Finished the review draft","closeoutNote":{"contentMarkdown":"Completed the draft review and listed the follow-up fixes."}}'
|
|
627
650
|
},
|
|
628
651
|
{
|
|
629
652
|
toolName: "forge_release_task_run",
|
|
630
653
|
summary: "Stop an active run without marking the task complete.",
|
|
631
654
|
whenToUse: "Use when the user is stopping or pausing work without completion.",
|
|
632
|
-
inputShape: "{ taskRunId: string, actor?: string, note?: string }",
|
|
655
|
+
inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
633
656
|
requiredFields: ["taskRunId"],
|
|
634
|
-
notes: ["Use this instead of faking a stop by only changing task status."],
|
|
635
|
-
example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback"}'
|
|
657
|
+
notes: ["Use this instead of faking a stop by only changing task status.", "closeoutNote is useful for documenting blockers or handoff context."],
|
|
658
|
+
example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback","closeoutNote":{"contentMarkdown":"Blocked on feedback from design before I can continue."}}'
|
|
636
659
|
}
|
|
637
660
|
];
|
|
638
661
|
function buildAgentOnboardingPayload(request) {
|
|
@@ -656,7 +679,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
656
679
|
"rewards.manage",
|
|
657
680
|
"psyche.read",
|
|
658
681
|
"psyche.write",
|
|
659
|
-
"psyche.
|
|
682
|
+
"psyche.note",
|
|
660
683
|
"psyche.insight",
|
|
661
684
|
"psyche.mode"
|
|
662
685
|
],
|
|
@@ -692,6 +715,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
692
715
|
project: "A multi-step workstream under one goal. Projects organize related tasks.",
|
|
693
716
|
task: "A concrete actionable work item. Task status is board state, not proof of live work.",
|
|
694
717
|
taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
|
|
718
|
+
note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
|
|
695
719
|
insight: "An agent-authored observation or recommendation grounded in Forge data.",
|
|
696
720
|
psyche: "Forge Psyche is the reflective domain for values, patterns, behaviors, beliefs, modes, and trigger reports. It is sensitive and should be handled deliberately."
|
|
697
721
|
},
|
|
@@ -713,6 +737,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
713
737
|
"Projects belong to one goal through goalId.",
|
|
714
738
|
"Tasks can belong to a goal, a project, both, or neither.",
|
|
715
739
|
"Task runs represent live work sessions on tasks and are separate from task status.",
|
|
740
|
+
"Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
|
|
716
741
|
"Psyche values can link to goals, projects, and tasks.",
|
|
717
742
|
"Behavior patterns, behaviors, beliefs, modes, and trigger reports cross-link to describe one reflective model rather than isolated records.",
|
|
718
743
|
"Insights can point at one entity, but they exist to capture interpretation or advice rather than raw work items."
|
|
@@ -1173,12 +1198,12 @@ export async function buildServer(options = {}) {
|
|
|
1173
1198
|
}
|
|
1174
1199
|
return context;
|
|
1175
1200
|
};
|
|
1176
|
-
const
|
|
1201
|
+
const requireNoteAccess = (headers, entityType, detail) => {
|
|
1177
1202
|
const context = authenticateRequest(headers);
|
|
1178
1203
|
if (isPsycheEntityType(entityType)) {
|
|
1179
1204
|
if (isPsycheAuthRequired()) {
|
|
1180
1205
|
managers.authorization.requireAuthenticatedActor(context, detail);
|
|
1181
|
-
managers.authorization.
|
|
1206
|
+
managers.authorization.requireAnyTokenScope(context, ["psyche.note"], {
|
|
1182
1207
|
entityType,
|
|
1183
1208
|
...(detail ?? {})
|
|
1184
1209
|
});
|
|
@@ -1581,7 +1606,7 @@ export async function buildServer(options = {}) {
|
|
|
1581
1606
|
}
|
|
1582
1607
|
return {
|
|
1583
1608
|
report,
|
|
1584
|
-
|
|
1609
|
+
notes: listNotes({ linkedEntityType: "trigger_report", linkedEntityId: id, limit: 50 }),
|
|
1585
1610
|
insights: listInsights({ entityType: "trigger_report", entityId: id, limit: 50 })
|
|
1586
1611
|
};
|
|
1587
1612
|
});
|
|
@@ -1605,65 +1630,73 @@ export async function buildServer(options = {}) {
|
|
|
1605
1630
|
}
|
|
1606
1631
|
return { report };
|
|
1607
1632
|
});
|
|
1608
|
-
app.get("/api/v1/
|
|
1609
|
-
const query =
|
|
1610
|
-
if (isPsycheEntityType(query.
|
|
1611
|
-
requirePsycheScopedAccess(request.headers, ["psyche.read"], {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1633
|
+
app.get("/api/v1/notes", async (request) => {
|
|
1634
|
+
const query = notesListQuerySchema.parse(request.query ?? {});
|
|
1635
|
+
if (isPsycheEntityType(query.linkedEntityType)) {
|
|
1636
|
+
requirePsycheScopedAccess(request.headers, ["psyche.read"], {
|
|
1637
|
+
route: "/api/v1/notes",
|
|
1638
|
+
entityType: query.linkedEntityType
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
return { notes: listNotes(query) };
|
|
1642
|
+
});
|
|
1643
|
+
app.post("/api/v1/notes", async (request, reply) => {
|
|
1644
|
+
const input = createNoteSchema.parse(request.body ?? {});
|
|
1645
|
+
const firstLinkedEntityType = input.links[0]?.entityType;
|
|
1646
|
+
const auth = requireNoteAccess(request.headers, firstLinkedEntityType, {
|
|
1647
|
+
route: "/api/v1/notes",
|
|
1648
|
+
entityType: firstLinkedEntityType ?? null
|
|
1620
1649
|
});
|
|
1621
|
-
const
|
|
1650
|
+
const note = createNote(input, toActivityContext(auth));
|
|
1622
1651
|
reply.code(201);
|
|
1623
|
-
return {
|
|
1652
|
+
return { note };
|
|
1624
1653
|
});
|
|
1625
|
-
app.get("/api/v1/
|
|
1654
|
+
app.get("/api/v1/notes/:id", async (request, reply) => {
|
|
1626
1655
|
const { id } = request.params;
|
|
1627
|
-
const current =
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1656
|
+
const current = getNoteById(id);
|
|
1657
|
+
const psycheEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))?.entityType;
|
|
1658
|
+
if (psycheEntityType) {
|
|
1659
|
+
requirePsycheScopedAccess(request.headers, ["psyche.read"], {
|
|
1660
|
+
route: "/api/v1/notes/:id",
|
|
1661
|
+
entityType: psycheEntityType
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1633
1664
|
if (!current) {
|
|
1634
1665
|
reply.code(404);
|
|
1635
|
-
return { error: "
|
|
1666
|
+
return { error: "Note not found" };
|
|
1636
1667
|
}
|
|
1637
|
-
return {
|
|
1668
|
+
return { note: current };
|
|
1638
1669
|
});
|
|
1639
|
-
app.patch("/api/v1/
|
|
1670
|
+
app.patch("/api/v1/notes/:id", async (request, reply) => {
|
|
1640
1671
|
const { id } = request.params;
|
|
1641
|
-
const patch =
|
|
1642
|
-
const current =
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1645
|
-
|
|
1672
|
+
const patch = updateNoteSchema.parse(request.body ?? {});
|
|
1673
|
+
const current = getNoteById(id);
|
|
1674
|
+
const linkedEntityType = current?.links[0]?.entityType ?? patch.links?.[0]?.entityType ?? null;
|
|
1675
|
+
const auth = requireNoteAccess(request.headers, linkedEntityType, {
|
|
1676
|
+
route: "/api/v1/notes/:id",
|
|
1677
|
+
entityType: linkedEntityType
|
|
1646
1678
|
});
|
|
1647
|
-
const
|
|
1648
|
-
if (!
|
|
1679
|
+
const note = updateNote(id, patch, toActivityContext(auth));
|
|
1680
|
+
if (!note) {
|
|
1649
1681
|
reply.code(404);
|
|
1650
|
-
return { error: "
|
|
1682
|
+
return { error: "Note not found" };
|
|
1651
1683
|
}
|
|
1652
|
-
return {
|
|
1684
|
+
return { note };
|
|
1653
1685
|
});
|
|
1654
|
-
app.delete("/api/v1/
|
|
1686
|
+
app.delete("/api/v1/notes/:id", async (request, reply) => {
|
|
1655
1687
|
const { id } = request.params;
|
|
1656
|
-
const current =
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
|
|
1688
|
+
const current = getNoteById(id);
|
|
1689
|
+
const linkedEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))?.entityType ?? current?.links[0]?.entityType ?? null;
|
|
1690
|
+
const auth = requireNoteAccess(request.headers, linkedEntityType, {
|
|
1691
|
+
route: "/api/v1/notes/:id",
|
|
1692
|
+
entityType: linkedEntityType
|
|
1660
1693
|
});
|
|
1661
|
-
const
|
|
1662
|
-
if (!
|
|
1694
|
+
const note = deleteEntity("note", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
|
|
1695
|
+
if (!note) {
|
|
1663
1696
|
reply.code(404);
|
|
1664
|
-
return { error: "
|
|
1697
|
+
return { error: "Note not found" };
|
|
1665
1698
|
}
|
|
1666
|
-
return {
|
|
1699
|
+
return { note };
|
|
1667
1700
|
});
|
|
1668
1701
|
app.get("/api/v1/projects", async (request) => {
|
|
1669
1702
|
const query = projectListQuerySchema.parse(request.query ?? {});
|
|
@@ -2116,7 +2149,8 @@ export async function buildServer(options = {}) {
|
|
|
2116
2149
|
project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
|
|
2117
2150
|
activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
|
|
2118
2151
|
taskRuns,
|
|
2119
|
-
activity: listActivityEventsForTask(id, 20)
|
|
2152
|
+
activity: listActivityEventsForTask(id, 20),
|
|
2153
|
+
notesSummaryByEntity: buildNotesSummaryByEntity()
|
|
2120
2154
|
});
|
|
2121
2155
|
});
|
|
2122
2156
|
app.get("/api/v1/tasks/:id/context", async (request, reply) => {
|
|
@@ -2133,7 +2167,8 @@ export async function buildServer(options = {}) {
|
|
|
2133
2167
|
project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
|
|
2134
2168
|
activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
|
|
2135
2169
|
taskRuns,
|
|
2136
|
-
activity: listActivityEventsForTask(id, 20)
|
|
2170
|
+
activity: listActivityEventsForTask(id, 20),
|
|
2171
|
+
notesSummaryByEntity: buildNotesSummaryByEntity()
|
|
2137
2172
|
});
|
|
2138
2173
|
});
|
|
2139
2174
|
app.post("/api/goals", async (request, reply) => {
|
|
@@ -2320,7 +2355,8 @@ export async function buildServer(options = {}) {
|
|
|
2320
2355
|
effort: input.effort,
|
|
2321
2356
|
energy: input.energy,
|
|
2322
2357
|
points: input.points,
|
|
2323
|
-
tagIds: input.tagIds
|
|
2358
|
+
tagIds: input.tagIds,
|
|
2359
|
+
notes: input.closeoutNote ? [input.closeoutNote] : undefined
|
|
2324
2360
|
}, toActivityContext(auth));
|
|
2325
2361
|
if (!task) {
|
|
2326
2362
|
reply.code(404);
|
|
@@ -2344,7 +2380,8 @@ export async function buildServer(options = {}) {
|
|
|
2344
2380
|
effort: input.effort ?? "deep",
|
|
2345
2381
|
energy: input.energy ?? "steady",
|
|
2346
2382
|
points: input.points ?? 40,
|
|
2347
|
-
tagIds: input.tagIds ?? []
|
|
2383
|
+
tagIds: input.tagIds ?? [],
|
|
2384
|
+
notes: input.closeoutNote ? [input.closeoutNote] : []
|
|
2348
2385
|
}), toActivityContext(auth));
|
|
2349
2386
|
reply.code(201);
|
|
2350
2387
|
return { task, xp: buildXpMetricsPayload() };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { closeDatabase, configureDatabase, configureDatabaseSeeding, getDatabase, initializeDatabase } from "./db.js";
|
|
3
|
+
const PERSONAL_CONTENT_TABLES = [
|
|
4
|
+
"goals",
|
|
5
|
+
"projects",
|
|
6
|
+
"tasks",
|
|
7
|
+
"task_runs",
|
|
8
|
+
"notes",
|
|
9
|
+
"insights",
|
|
10
|
+
"psyche_values",
|
|
11
|
+
"belief_entries",
|
|
12
|
+
"psyche_behaviors",
|
|
13
|
+
"behavior_patterns",
|
|
14
|
+
"mode_profiles",
|
|
15
|
+
"trigger_reports"
|
|
16
|
+
];
|
|
17
|
+
function countRows(tableName) {
|
|
18
|
+
const row = getDatabase().prepare(`SELECT COUNT(*) AS count FROM ${tableName}`).get();
|
|
19
|
+
return row.count;
|
|
20
|
+
}
|
|
21
|
+
function getCounts() {
|
|
22
|
+
return Object.fromEntries(PERSONAL_CONTENT_TABLES.map((tableName) => [tableName, countRows(tableName)]));
|
|
23
|
+
}
|
|
24
|
+
export async function seedDemoDataIntoRuntime(dataRoot = process.env.FORGE_DATA_ROOT ?? process.cwd()) {
|
|
25
|
+
const resolvedDataRoot = path.resolve(dataRoot);
|
|
26
|
+
const databasePath = path.join(resolvedDataRoot, "data", "forge.sqlite");
|
|
27
|
+
closeDatabase();
|
|
28
|
+
configureDatabase({ dataRoot: resolvedDataRoot });
|
|
29
|
+
configureDatabaseSeeding(false);
|
|
30
|
+
await initializeDatabase();
|
|
31
|
+
const existingCounts = getCounts();
|
|
32
|
+
const hasPersonalContent = Object.values(existingCounts).some((count) => count > 0);
|
|
33
|
+
if (hasPersonalContent) {
|
|
34
|
+
closeDatabase();
|
|
35
|
+
throw new Error(`Refusing to seed demo data into a non-empty Forge runtime at ${databasePath}. Use a fresh FORGE_DATA_ROOT instead.`);
|
|
36
|
+
}
|
|
37
|
+
closeDatabase();
|
|
38
|
+
configureDatabase({ dataRoot: resolvedDataRoot });
|
|
39
|
+
configureDatabaseSeeding(true);
|
|
40
|
+
await initializeDatabase();
|
|
41
|
+
const counts = getCounts();
|
|
42
|
+
closeDatabase();
|
|
43
|
+
configureDatabaseSeeding(false);
|
|
44
|
+
return {
|
|
45
|
+
dataRoot: resolvedDataRoot,
|
|
46
|
+
databasePath,
|
|
47
|
+
counts
|
|
48
|
+
};
|
|
49
|
+
}
|