forge-openclaw-plugin 0.2.44 → 0.2.45

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/dist/index.html CHANGED
@@ -13,7 +13,7 @@
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-s24CefIb.js"></script>
16
+ <script type="module" crossorigin src="/forge/assets/index-BejDHw1R.js"></script>
17
17
  <link rel="modulepreload" crossorigin href="/forge/assets/vendor-D_NZFJze.js">
18
18
  <link rel="modulepreload" crossorigin href="/forge/assets/board-CAszQU7Y.js">
19
19
  <link rel="modulepreload" crossorigin href="/forge/assets/ui-B5MjRjKe.js">
@@ -9,6 +9,7 @@ export type ForgePluginConfig = {
9
9
  dataRoot: string;
10
10
  apiToken: string;
11
11
  actorLabel: string;
12
+ injectBootstrapContext: boolean;
12
13
  timeoutMs: number;
13
14
  };
14
15
  export type CallForgeApiArgs = {
@@ -55,6 +55,9 @@ function normalizeDataRoot(value) {
55
55
  function normalizeOptionalString(value) {
56
56
  return typeof value === "string" ? value.trim() : "";
57
57
  }
58
+ function normalizeBoolean(value, fallback) {
59
+ return typeof value === "boolean" ? value : fallback;
60
+ }
58
61
  function isLocalOrigin(origin) {
59
62
  try {
60
63
  return LOCAL_HOSTNAMES.has(new URL(origin).hostname.toLowerCase());
@@ -98,6 +101,7 @@ export function resolveForgePluginConfig(pluginConfig) {
98
101
  dataRoot: normalizeDataRoot(raw.dataRoot),
99
102
  apiToken: typeof raw.apiToken === "string" ? raw.apiToken.trim() : "",
100
103
  actorLabel: normalizeOptionalString(raw.actorLabel),
104
+ injectBootstrapContext: normalizeBoolean(raw.injectBootstrapContext, true),
101
105
  timeoutMs: normalizeTimeout(raw.timeoutMs, 15_000)
102
106
  };
103
107
  }
@@ -136,6 +140,11 @@ export const forgePluginConfigSchema = {
136
140
  default: DEFAULT_OPENCLAW_ACTOR_LABEL,
137
141
  description: "Optional acting user label recorded in Forge provenance headers. Leave blank to inherit the local operator session label automatically."
138
142
  },
143
+ injectBootstrapContext: {
144
+ type: "boolean",
145
+ default: true,
146
+ description: "Whether OpenClaw should inject a preseeded Forge BOOTSTRAP.md file into new agent sessions. Disable this to conserve context or model-token budget."
147
+ },
139
148
  timeoutMs: {
140
149
  type: "integer",
141
150
  default: 15000,
@@ -173,6 +182,10 @@ export const forgePluginConfigSchema = {
173
182
  help: "Optional acting user label for provenance. Leave blank to inherit the local operator session label, or set one when a child agent should announce itself under a specific user.",
174
183
  placeholder: "Inherited from Forge operator session"
175
184
  },
185
+ injectBootstrapContext: {
186
+ label: "Inject Bootstrap Context",
187
+ help: "Enabled by default. Turn this off when you want OpenClaw sessions to start without a preseeded Forge BOOTSTRAP.md context file to conserve token budget."
188
+ },
176
189
  timeoutMs: {
177
190
  label: "Request Timeout (ms)",
178
191
  help: "Maximum time to wait before the plugin aborts an upstream Forge request.",
@@ -62,7 +62,18 @@ type ForgeOperatorOverview = {
62
62
  warnings?: string[];
63
63
  operator?: ForgeOperatorContext | null;
64
64
  };
65
+ type ForgeBootstrapPolicy = {
66
+ mode: "disabled" | "active_only" | "scoped" | "full";
67
+ goalsLimit: number;
68
+ projectsLimit: number;
69
+ tasksLimit: number;
70
+ habitsLimit: number;
71
+ strategiesLimit: number;
72
+ peoplePageLimit: number;
73
+ includePeoplePages: boolean;
74
+ };
65
75
  type ForgeSessionBootstrapPayload = {
76
+ bootstrapPolicy: ForgeBootstrapPolicy;
66
77
  overview: ForgeOperatorOverview | null;
67
78
  goals: ForgeGoalRecord[];
68
79
  projects: ForgeProjectRecord[];
@@ -3,6 +3,16 @@ import { isAgentBootstrapEvent } from "openclaw/plugin-sdk/hook-runtime";
3
3
  import { callConfiguredForgeApi, expectForgeSuccess } from "./api-client.js";
4
4
  const FORGE_SESSION_BOOTSTRAP_PATH = ".forge/generated/FORGE_SESSION_BOOTSTRAP.md";
5
5
  const FORGE_SESSION_BOOTSTRAP_NAME = "forge-session-bootstrap";
6
+ const DEFAULT_BOOTSTRAP_POLICY = {
7
+ mode: "active_only",
8
+ goalsLimit: 5,
9
+ projectsLimit: 8,
10
+ tasksLimit: 10,
11
+ habitsLimit: 6,
12
+ strategiesLimit: 4,
13
+ peoplePageLimit: 4,
14
+ includePeoplePages: true
15
+ };
6
16
  function isRecord(value) {
7
17
  return typeof value === "object" && value !== null;
8
18
  }
@@ -12,6 +22,33 @@ function asArray(value) {
12
22
  function cleanInline(value) {
13
23
  return (value ?? "").replace(/\s+/g, " ").trim();
14
24
  }
25
+ function clampBudget(value, fallback, max) {
26
+ const numeric = typeof value === "number" ? value : Number(value);
27
+ return Math.min(Math.max(Number.isFinite(numeric) ? numeric : fallback, 0), max);
28
+ }
29
+ function sanitizeBootstrapPolicy(value) {
30
+ if (!isRecord(value)) {
31
+ return { ...DEFAULT_BOOTSTRAP_POLICY };
32
+ }
33
+ const mode = value.mode === "disabled" ||
34
+ value.mode === "active_only" ||
35
+ value.mode === "scoped" ||
36
+ value.mode === "full"
37
+ ? value.mode
38
+ : DEFAULT_BOOTSTRAP_POLICY.mode;
39
+ return {
40
+ mode,
41
+ goalsLimit: clampBudget(value.goalsLimit, DEFAULT_BOOTSTRAP_POLICY.goalsLimit, 100),
42
+ projectsLimit: clampBudget(value.projectsLimit, DEFAULT_BOOTSTRAP_POLICY.projectsLimit, 100),
43
+ tasksLimit: clampBudget(value.tasksLimit, DEFAULT_BOOTSTRAP_POLICY.tasksLimit, 100),
44
+ habitsLimit: clampBudget(value.habitsLimit, DEFAULT_BOOTSTRAP_POLICY.habitsLimit, 100),
45
+ strategiesLimit: clampBudget(value.strategiesLimit, DEFAULT_BOOTSTRAP_POLICY.strategiesLimit, 100),
46
+ peoplePageLimit: clampBudget(value.peoplePageLimit, DEFAULT_BOOTSTRAP_POLICY.peoplePageLimit, 50),
47
+ includePeoplePages: typeof value.includePeoplePages === "boolean"
48
+ ? value.includePeoplePages
49
+ : DEFAULT_BOOTSTRAP_POLICY.includePeoplePages
50
+ };
51
+ }
15
52
  function excerpt(value, maxLength) {
16
53
  const normalized = cleanInline(value);
17
54
  if (!normalized) {
@@ -105,6 +142,7 @@ export function buildForgeSessionBootstrapContext(payload) {
105
142
  if (payload.overview?.generatedAt) {
106
143
  lines.push(`Generated at: ${payload.overview.generatedAt}`, "");
107
144
  }
145
+ lines.push(`Bootstrap mode: ${payload.bootstrapPolicy.mode.replaceAll("_", " ")}`, "");
108
146
  if (overview) {
109
147
  lines.push("## Current Forge Snapshot", "");
110
148
  lines.push(`- Active projects in operator view: ${overview.activeProjects?.length ?? 0}`);
@@ -177,38 +215,138 @@ async function readForgePayload(config, pathName) {
177
215
  });
178
216
  return expectForgeSuccess(result);
179
217
  }
218
+ function withQuery(pathName, query) {
219
+ const search = new URLSearchParams();
220
+ for (const [key, value] of Object.entries(query)) {
221
+ if (value === undefined) {
222
+ continue;
223
+ }
224
+ search.set(key, String(value));
225
+ }
226
+ const encoded = search.toString();
227
+ return encoded ? `${pathName}?${encoded}` : pathName;
228
+ }
229
+ function resolveBootstrapPolicy(onboardingResponse) {
230
+ const onboarding = onboardingResponse && isRecord(onboardingResponse) && "onboarding" in onboardingResponse
231
+ ? onboardingResponse.onboarding
232
+ : null;
233
+ if (isRecord(onboarding) && isRecord(onboarding.effectiveBootstrapPolicy)) {
234
+ return sanitizeBootstrapPolicy(onboarding.effectiveBootstrapPolicy);
235
+ }
236
+ if (isRecord(onboarding) && isRecord(onboarding.defaultBootstrapPolicy)) {
237
+ return sanitizeBootstrapPolicy(onboarding.defaultBootstrapPolicy);
238
+ }
239
+ return { ...DEFAULT_BOOTSTRAP_POLICY };
240
+ }
180
241
  async function loadForgeSessionBootstrapPayload(config) {
242
+ const onboardingResponse = await readForgePayload(config, "/api/v1/agents/onboarding");
243
+ const bootstrapPolicy = resolveBootstrapPolicy(onboardingResponse);
244
+ if (bootstrapPolicy.mode === "disabled") {
245
+ return {
246
+ bootstrapPolicy,
247
+ overview: null,
248
+ goals: [],
249
+ projects: [],
250
+ tasks: [],
251
+ habits: [],
252
+ strategies: [],
253
+ peoplePages: []
254
+ };
255
+ }
256
+ const goalsPath = bootstrapPolicy.mode === "full"
257
+ ? withQuery("/api/v1/goals", { limit: 100 })
258
+ : withQuery("/api/v1/goals", {
259
+ status: bootstrapPolicy.mode === "active_only" ? "active" : undefined,
260
+ limit: bootstrapPolicy.goalsLimit
261
+ });
262
+ const projectsPath = bootstrapPolicy.mode === "full"
263
+ ? withQuery("/api/v1/projects", { limit: 100 })
264
+ : withQuery("/api/v1/projects", {
265
+ status: bootstrapPolicy.mode === "active_only" ? "active" : undefined,
266
+ limit: bootstrapPolicy.projectsLimit
267
+ });
268
+ const tasksPath = bootstrapPolicy.mode === "full"
269
+ ? withQuery("/api/v1/tasks", { limit: 100 })
270
+ : withQuery("/api/v1/tasks", {
271
+ status: bootstrapPolicy.mode === "active_only" ? "focus" : undefined,
272
+ limit: bootstrapPolicy.tasksLimit
273
+ });
274
+ const habitsPath = bootstrapPolicy.mode === "full"
275
+ ? withQuery("/api/v1/habits", { limit: 100 })
276
+ : withQuery("/api/v1/habits", {
277
+ dueToday: bootstrapPolicy.mode === "active_only" ? true : undefined,
278
+ limit: bootstrapPolicy.habitsLimit
279
+ });
280
+ const strategiesPath = bootstrapPolicy.mode === "full"
281
+ ? withQuery("/api/v1/strategies", { limit: 100 })
282
+ : withQuery("/api/v1/strategies", {
283
+ status: bootstrapPolicy.mode === "active_only" ? "active" : undefined,
284
+ limit: bootstrapPolicy.strategiesLimit
285
+ });
286
+ const wikiPagesPath = bootstrapPolicy.includePeoplePages && bootstrapPolicy.peoplePageLimit > 0
287
+ ? withQuery("/api/v1/wiki/pages", {
288
+ kind: "wiki",
289
+ limit: bootstrapPolicy.mode === "full"
290
+ ? 200
291
+ : Math.max(bootstrapPolicy.peoplePageLimit * 4, 25)
292
+ })
293
+ : null;
181
294
  const [overviewResponse, goalsResponse, projectsResponse, tasksResponse, habitsResponse, strategiesResponse, wikiPagesResponse] = await Promise.all([
182
295
  readForgePayload(config, "/api/v1/operator/overview"),
183
- readForgePayload(config, "/api/v1/goals"),
184
- readForgePayload(config, "/api/v1/projects"),
185
- readForgePayload(config, "/api/v1/tasks"),
186
- readForgePayload(config, "/api/v1/habits"),
187
- readForgePayload(config, "/api/v1/strategies"),
188
- readForgePayload(config, "/api/v1/wiki/pages")
296
+ readForgePayload(config, goalsPath),
297
+ readForgePayload(config, projectsPath),
298
+ readForgePayload(config, tasksPath),
299
+ readForgePayload(config, habitsPath),
300
+ readForgePayload(config, strategiesPath),
301
+ wikiPagesPath
302
+ ? readForgePayload(config, wikiPagesPath)
303
+ : Promise.resolve({ pages: [] })
189
304
  ]);
190
305
  const wikiPages = asArray(wikiPagesResponse.pages).filter((page) => isRecord(page) &&
191
306
  typeof page.slug === "string" &&
192
307
  typeof page.title === "string");
193
308
  return {
309
+ bootstrapPolicy,
194
310
  overview: overviewResponse && isRecord(overviewResponse) && "overview" in overviewResponse
195
311
  ? (overviewResponse.overview ?? null)
196
312
  : null,
197
- goals: asArray(goalsResponse.goals),
198
- projects: asArray(projectsResponse.projects),
199
- tasks: asArray(tasksResponse.tasks),
200
- habits: asArray(habitsResponse.habits),
201
- strategies: asArray(strategiesResponse.strategies),
202
- peoplePages: listPeopleBranchPages(wikiPages)
313
+ goals: bootstrapPolicy.mode === "full"
314
+ ? asArray(goalsResponse.goals)
315
+ : asArray(goalsResponse.goals).slice(0, bootstrapPolicy.goalsLimit),
316
+ projects: bootstrapPolicy.mode === "full"
317
+ ? asArray(projectsResponse.projects)
318
+ : asArray(projectsResponse.projects).slice(0, bootstrapPolicy.projectsLimit),
319
+ tasks: bootstrapPolicy.mode === "full"
320
+ ? asArray(tasksResponse.tasks)
321
+ : asArray(tasksResponse.tasks).slice(0, bootstrapPolicy.tasksLimit),
322
+ habits: bootstrapPolicy.mode === "full"
323
+ ? asArray(habitsResponse.habits)
324
+ : asArray(habitsResponse.habits).slice(0, bootstrapPolicy.habitsLimit),
325
+ strategies: bootstrapPolicy.mode === "full"
326
+ ? asArray(strategiesResponse.strategies)
327
+ : asArray(strategiesResponse.strategies).slice(0, bootstrapPolicy.strategiesLimit),
328
+ peoplePages: bootstrapPolicy.includePeoplePages
329
+ ? listPeopleBranchPages(wikiPages).slice(0, bootstrapPolicy.peoplePageLimit)
330
+ : []
203
331
  };
204
332
  }
205
333
  export async function buildLiveForgeSessionBootstrapContext(config) {
206
- return buildForgeSessionBootstrapContext(await loadForgeSessionBootstrapPayload(config));
334
+ if (!config.injectBootstrapContext) {
335
+ return "";
336
+ }
337
+ const payload = await loadForgeSessionBootstrapPayload(config);
338
+ if (payload.bootstrapPolicy.mode === "disabled") {
339
+ return "";
340
+ }
341
+ return buildForgeSessionBootstrapContext(payload);
207
342
  }
208
343
  export function registerForgeSessionBootstrapHook(api, config) {
209
344
  if (!api.registerHook) {
210
345
  return;
211
346
  }
347
+ if (!config.injectBootstrapContext) {
348
+ return;
349
+ }
212
350
  api.registerHook("agent:bootstrap", async (event) => {
213
351
  if (!isAgentBootstrapEvent(event)) {
214
352
  return;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE agent_tokens
2
+ ADD COLUMN bootstrap_policy_json TEXT NOT NULL DEFAULT '{"mode":"full","goalsLimit":25,"projectsLimit":25,"tasksLimit":25,"habitsLimit":20,"strategiesLimit":20,"peoplePageLimit":12,"includePeoplePages":true}';
@@ -0,0 +1,2 @@
1
+ ALTER TABLE agent_tokens
2
+ ADD COLUMN scope_policy_json TEXT NOT NULL DEFAULT '{"userIds":[],"projectIds":[],"tagIds":[]}';