forge-openclaw-plugin 0.2.47 → 0.2.49

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 (32) hide show
  1. package/README.md +6 -5
  2. package/dist/assets/index-2_tuemtU.css +1 -0
  3. package/dist/assets/index-BAmEvOXb.js +91 -0
  4. package/dist/assets/index-BAmEvOXb.js.map +1 -0
  5. package/dist/index.html +2 -2
  6. package/dist/openclaw/api-client.js +15 -1
  7. package/dist/openclaw/session-registry.js +17 -0
  8. package/dist/openclaw/tools.js +1 -1
  9. package/dist/server/server/migrations/052_agent_identity_tightening.sql +307 -0
  10. package/dist/server/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
  11. package/dist/server/server/src/app.js +42 -12
  12. package/dist/server/server/src/health-workout-adapters.js +465 -0
  13. package/dist/server/server/src/health.js +134 -9
  14. package/dist/server/server/src/openapi.js +33 -0
  15. package/dist/server/server/src/repositories/agent-runtime-sessions.js +122 -16
  16. package/dist/server/server/src/repositories/habits.js +62 -25
  17. package/dist/server/server/src/repositories/model-settings.js +5 -0
  18. package/dist/server/server/src/repositories/settings.js +101 -13
  19. package/dist/server/server/src/repositories/users.js +23 -0
  20. package/dist/server/server/src/types.js +22 -6
  21. package/dist/server/server/src/watch-mobile.js +33 -21
  22. package/dist/server/src/lib/date-keys.js +21 -0
  23. package/openclaw.plugin.json +1 -1
  24. package/package.json +5 -2
  25. package/server/migrations/052_agent_identity_tightening.sql +307 -0
  26. package/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
  27. package/skills/forge-openclaw/SKILL.md +3 -1
  28. package/skills/forge-openclaw/entity_conversation_playbooks.md +45 -8
  29. package/skills/forge-openclaw/psyche_entity_playbooks.md +14 -0
  30. package/dist/assets/index-BejDHw1R.js +0 -91
  31. package/dist/assets/index-BejDHw1R.js.map +0 -1
  32. package/dist/assets/index-DtEvFzXp.css +0 -1
@@ -11,8 +11,9 @@ import { recordActivityEvent } from "./activity-events.js";
11
11
  import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
12
12
  import { recordHabitCheckInReward, reverseLatestHabitCheckInReward } from "./rewards.js";
13
13
  import { createHabitCheckInSchema, createHabitSchema, habitCheckInSchema, habitSchema, updateHabitSchema } from "../types.js";
14
+ import { formatLocalDateKey } from "../../../src/lib/date-keys.js";
14
15
  function todayKey(now = new Date()) {
15
- return now.toISOString().slice(0, 10);
16
+ return formatLocalDateKey(now);
16
17
  }
17
18
  function parseWeekDays(raw) {
18
19
  const parsed = JSON.parse(raw);
@@ -87,25 +88,25 @@ function calculateStreak(habit, checkIns, now = new Date()) {
87
88
  statusByDate.set(checkIn.dateKey, checkIn.status);
88
89
  }
89
90
  }
90
- const isScheduledOn = (date) => habit.frequency === "daily" || habit.weekDays.includes(date.getUTCDay());
91
- const toDateKey = (date) => date.toISOString().slice(0, 10);
92
- const atUtcDayStart = (date) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
91
+ const isScheduledOn = (date) => habit.frequency === "daily" || habit.weekDays.includes(date.getDay());
92
+ const toDateKey = (date) => formatLocalDateKey(date);
93
+ const atLocalDayStart = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
93
94
  const previousScheduledDate = (date) => {
94
- const cursor = atUtcDayStart(date);
95
+ const cursor = atLocalDayStart(date);
95
96
  do {
96
- cursor.setUTCDate(cursor.getUTCDate() - 1);
97
+ cursor.setDate(cursor.getDate() - 1);
97
98
  } while (!isScheduledOn(cursor));
98
99
  return cursor;
99
100
  };
100
- const startOfUtcWeek = (date) => {
101
- const start = atUtcDayStart(date);
102
- const offset = (start.getUTCDay() + 6) % 7;
103
- start.setUTCDate(start.getUTCDate() - offset);
101
+ const startOfLocalWeek = (date) => {
102
+ const start = atLocalDayStart(date);
103
+ const offset = (start.getDay() + 6) % 7;
104
+ start.setDate(start.getDate() - offset);
104
105
  return start;
105
106
  };
106
- const previousUtcWeek = (date) => {
107
- const start = startOfUtcWeek(date);
108
- start.setUTCDate(start.getUTCDate() - 7);
107
+ const previousLocalWeek = (date) => {
108
+ const start = startOfLocalWeek(date);
109
+ start.setDate(start.getDate() - 7);
109
110
  return start;
110
111
  };
111
112
  const alignedStatusOn = (date) => {
@@ -113,7 +114,7 @@ function calculateStreak(habit, checkIns, now = new Date()) {
113
114
  return status ? isAligned(habit, { status }) : false;
114
115
  };
115
116
  if (habit.frequency === "daily") {
116
- const today = atUtcDayStart(now);
117
+ const today = atLocalDayStart(now);
117
118
  let cursor = isScheduledOn(today) && !statusByDate.has(toDateKey(today))
118
119
  ? previousScheduledDate(today)
119
120
  : today;
@@ -128,21 +129,21 @@ function calculateStreak(habit, checkIns, now = new Date()) {
128
129
  let count = 0;
129
130
  for (let offset = 0; offset < 7; offset += 1) {
130
131
  const day = new Date(weekStart);
131
- day.setUTCDate(weekStart.getUTCDate() + offset);
132
+ day.setDate(weekStart.getDate() + offset);
132
133
  if (isScheduledOn(day) && alignedStatusOn(day)) {
133
134
  count += 1;
134
135
  }
135
136
  }
136
137
  return count;
137
138
  };
138
- const currentWeekStart = startOfUtcWeek(now);
139
+ const currentWeekStart = startOfLocalWeek(now);
139
140
  let cursor = alignedCountForWeek(currentWeekStart) >= habit.targetCount
140
141
  ? currentWeekStart
141
- : previousUtcWeek(currentWeekStart);
142
+ : previousLocalWeek(currentWeekStart);
142
143
  let streak = 0;
143
144
  while (alignedCountForWeek(cursor) >= habit.targetCount) {
144
145
  streak += 1;
145
- cursor = previousUtcWeek(cursor);
146
+ cursor = previousLocalWeek(cursor);
146
147
  }
147
148
  return streak;
148
149
  }
@@ -157,7 +158,7 @@ function isHabitDueToday(habit, latestCheckIn, now = new Date()) {
157
158
  if (habit.frequency === "daily") {
158
159
  return true;
159
160
  }
160
- return habit.weekDays.includes(now.getUTCDay());
161
+ return habit.weekDays.includes(now.getDay());
161
162
  }
162
163
  function mapHabit(row, checkIns = listCheckInsForHabit(row.id)) {
163
164
  const latestCheckIn = checkIns[0] ?? null;
@@ -235,21 +236,28 @@ function sortHabits(habits, orderBy) {
235
236
  const nextHabits = [...habits];
236
237
  nextHabits.sort((left, right) => {
237
238
  if (orderBy === "name") {
238
- return (left.title.localeCompare(right.title, undefined, { sensitivity: "base" }) ||
239
- compareDateDesc(left.createdAt, right.createdAt));
239
+ return (left.title.localeCompare(right.title, undefined, {
240
+ sensitivity: "base"
241
+ }) || compareDateDesc(left.createdAt, right.createdAt));
240
242
  }
241
243
  if (orderBy === "streak") {
242
244
  return (right.streakCount - left.streakCount ||
243
245
  Number(right.dueToday) - Number(left.dueToday) ||
244
- left.title.localeCompare(right.title, undefined, { sensitivity: "base" }));
246
+ left.title.localeCompare(right.title, undefined, {
247
+ sensitivity: "base"
248
+ }));
245
249
  }
246
250
  if (orderBy === "created_at") {
247
251
  return (compareDateDesc(left.createdAt, right.createdAt) ||
248
- left.title.localeCompare(right.title, undefined, { sensitivity: "base" }));
252
+ left.title.localeCompare(right.title, undefined, {
253
+ sensitivity: "base"
254
+ }));
249
255
  }
250
256
  if (orderBy === "updated_at") {
251
257
  return (compareDateDesc(left.updatedAt, right.updatedAt) ||
252
- left.title.localeCompare(right.title, undefined, { sensitivity: "base" }));
258
+ left.title.localeCompare(right.title, undefined, {
259
+ sensitivity: "base"
260
+ }));
253
261
  }
254
262
  return (Number(right.dueToday) - Number(left.dueToday) ||
255
263
  compareDateAsc(left.lastCheckInAt, right.lastCheckInAt) ||
@@ -367,6 +375,32 @@ export function updateHabit(habitId, input, activity) {
367
375
  return undefined;
368
376
  }
369
377
  const parsed = updateHabitSchema.parse(input);
378
+ const shouldApplyEntityPatch = parsed.title !== undefined ||
379
+ parsed.description !== undefined ||
380
+ parsed.status !== undefined ||
381
+ parsed.polarity !== undefined ||
382
+ parsed.frequency !== undefined ||
383
+ parsed.targetCount !== undefined ||
384
+ parsed.weekDays !== undefined ||
385
+ parsed.linkedGoalIds !== undefined ||
386
+ parsed.linkedProjectIds !== undefined ||
387
+ parsed.linkedTaskIds !== undefined ||
388
+ parsed.linkedValueIds !== undefined ||
389
+ parsed.linkedPatternIds !== undefined ||
390
+ parsed.linkedBehaviorIds !== undefined ||
391
+ parsed.linkedBeliefIds !== undefined ||
392
+ parsed.linkedModeIds !== undefined ||
393
+ parsed.linkedReportIds !== undefined ||
394
+ parsed.linkedBehaviorId !== undefined ||
395
+ parsed.rewardXp !== undefined ||
396
+ parsed.penaltyXp !== undefined ||
397
+ parsed.generatedHealthEventTemplate !== undefined ||
398
+ parsed.userId !== undefined;
399
+ if (!shouldApplyEntityPatch) {
400
+ return parsed.checkIn
401
+ ? createHabitCheckIn(habitId, parsed.checkIn, activity)
402
+ : current;
403
+ }
370
404
  const nextLinkedBehaviorIds = parsed.linkedBehaviorIds !== undefined ||
371
405
  parsed.linkedBehaviorId !== undefined
372
406
  ? normalizeLinkedBehaviorIds({
@@ -385,7 +419,7 @@ export function updateHabit(habitId, input, activity) {
385
419
  validateExistingIds(parsed.linkedBeliefIds ?? current.linkedBeliefIds, getBeliefEntryById, "belief_not_found", "Belief");
386
420
  validateExistingIds(parsed.linkedModeIds ?? current.linkedModeIds, getModeProfileById, "mode_not_found", "Mode");
387
421
  validateExistingIds(parsed.linkedReportIds ?? current.linkedReportIds, getTriggerReportById, "report_not_found", "Report");
388
- return runInTransaction(() => {
422
+ const updatedHabit = runInTransaction(() => {
389
423
  const updatedAt = new Date().toISOString();
390
424
  getDatabase()
391
425
  .prepare(`UPDATE habits
@@ -419,6 +453,9 @@ export function updateHabit(habitId, input, activity) {
419
453
  }
420
454
  return habit;
421
455
  });
456
+ return parsed.checkIn
457
+ ? createHabitCheckIn(habitId, parsed.checkIn, activity) ?? updatedHabit
458
+ : updatedHabit;
422
459
  }
423
460
  export function deleteHabit(habitId, activity) {
424
461
  const current = getHabitById(habitId);
@@ -47,6 +47,11 @@ export function buildConnectionAgentIdentity(connection) {
47
47
  id: connection.agentId,
48
48
  label: connection.agentLabel,
49
49
  agentType: connection.provider,
50
+ identityKey: `model:${connection.id}`,
51
+ provider: null,
52
+ machineKey: null,
53
+ personaKey: connection.provider,
54
+ linkedUsers: [],
50
55
  trustLevel: "trusted",
51
56
  autonomyMode: "approval_required",
52
57
  approvalMode: "approval_by_default",
@@ -7,6 +7,7 @@ import { recordActivityEvent } from "./activity-events.js";
7
7
  import { recordEventLog } from "./event-log.js";
8
8
  import { resolveGoogleCalendarOauthPublicConfig } from "../services/google-calendar-oauth-config.js";
9
9
  import { buildConnectionAgentIdentity, FORGE_DEFAULT_AGENT_ID, listAiModelConnections, syncForgeManagedWikiProfile } from "./model-settings.js";
10
+ import { listUsersByIds } from "./users.js";
10
11
  import { agentBootstrapPolicySchema, agentScopePolicySchema, createAgentTokenSchema, legacyAgentBootstrapPolicy, defaultAgentScopePolicy, agentIdentitySchema, customThemeSchema, settingsPayloadSchema, updateSettingsSchema } from "../types.js";
11
12
  const settingsFileSchema = settingsPayloadSchema.deepPartial();
12
13
  let settingsFileSyncDepth = 0;
@@ -303,11 +304,40 @@ function pickComparableOverrideSubset(source, template) {
303
304
  }
304
305
  return picked;
305
306
  }
306
- function mapAgent(row) {
307
+ function listAgentIdentityUserLinks(agentIds) {
308
+ if (agentIds.length === 0) {
309
+ return new Map();
310
+ }
311
+ const placeholders = agentIds.map(() => "?").join(",");
312
+ const rows = getDatabase()
313
+ .prepare(`SELECT agent_id, user_id, role
314
+ FROM agent_identity_users
315
+ WHERE agent_id IN (${placeholders})
316
+ ORDER BY role = 'primary' DESC, created_at ASC`)
317
+ .all(...agentIds);
318
+ const usersById = new Map(listUsersByIds(rows.map((row) => row.user_id)).map((user) => [user.id, user]));
319
+ const linksByAgentId = new Map();
320
+ for (const row of rows) {
321
+ const current = linksByAgentId.get(row.agent_id) ?? [];
322
+ current.push({
323
+ userId: row.user_id,
324
+ role: row.role,
325
+ user: usersById.get(row.user_id) ?? null
326
+ });
327
+ linksByAgentId.set(row.agent_id, current);
328
+ }
329
+ return linksByAgentId;
330
+ }
331
+ function mapAgent(row, linkedUsers = []) {
307
332
  return agentIdentitySchema.parse({
308
333
  id: row.id,
309
334
  label: row.label,
310
335
  agentType: row.agent_type,
336
+ identityKey: row.identity_key,
337
+ provider: row.provider,
338
+ machineKey: row.machine_key,
339
+ personaKey: row.persona_key,
340
+ linkedUsers,
311
341
  trustLevel: row.trust_level,
312
342
  autonomyMode: row.autonomy_mode,
313
343
  approvalMode: row.approval_mode,
@@ -351,6 +381,10 @@ function findAgentIdentity(agentId) {
351
381
  agent_identities.id,
352
382
  agent_identities.label,
353
383
  agent_identities.agent_type,
384
+ agent_identities.identity_key,
385
+ agent_identities.provider,
386
+ agent_identities.machine_key,
387
+ agent_identities.persona_key,
354
388
  agent_identities.trust_level,
355
389
  agent_identities.autonomy_mode,
356
390
  agent_identities.approval_mode,
@@ -358,36 +392,76 @@ function findAgentIdentity(agentId) {
358
392
  agent_identities.created_at,
359
393
  agent_identities.updated_at,
360
394
  COUNT(agent_tokens.id) AS token_count,
361
- COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
395
+ COALESCE(SUM(CASE WHEN agent_tokens.id IS NOT NULL AND agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
362
396
  FROM agent_identities
363
397
  LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
364
398
  WHERE agent_identities.id = ?
365
399
  GROUP BY agent_identities.id`)
366
400
  .get(agentId);
367
- return row ? mapAgent(row) : undefined;
401
+ const links = row ? listAgentIdentityUserLinks([row.id]) : new Map();
402
+ return row ? mapAgent(row, links.get(row.id) ?? []) : undefined;
403
+ }
404
+ function normalizeAgentIdentityPart(value) {
405
+ return value
406
+ ?.trim()
407
+ .toLowerCase()
408
+ .replace(/[^a-z0-9._:]+/g, "_")
409
+ .replace(/^_+|_+$/g, "") || "";
410
+ }
411
+ function runtimeProviderFromAgentType(agentType) {
412
+ const normalized = normalizeAgentIdentityPart(agentType);
413
+ if (normalized === "openclaw" ||
414
+ normalized === "hermes" ||
415
+ normalized === "codex") {
416
+ return normalized;
417
+ }
418
+ return null;
419
+ }
420
+ function deriveTokenAgentIdentityFields(input) {
421
+ const provider = runtimeProviderFromAgentType(input.agentType);
422
+ if (!provider) {
423
+ return {
424
+ identityKey: null,
425
+ provider: null,
426
+ machineKey: null,
427
+ personaKey: null
428
+ };
429
+ }
430
+ return {
431
+ identityKey: `runtime:${provider}:token:default`,
432
+ provider,
433
+ machineKey: "token",
434
+ personaKey: "default"
435
+ };
368
436
  }
369
437
  function upsertAgentIdentity(input) {
370
438
  const now = new Date().toISOString();
439
+ const identityFields = deriveTokenAgentIdentityFields(input);
371
440
  const existing = getDatabase()
372
441
  .prepare(`SELECT id
373
442
  FROM agent_identities
374
- WHERE lower(label) = lower(?)
443
+ WHERE (? IS NOT NULL AND identity_key = ?)
444
+ OR lower(label) = lower(?)
375
445
  LIMIT 1`)
376
- .get(input.agentLabel);
446
+ .get(identityFields.identityKey, identityFields.identityKey, input.agentLabel);
377
447
  if (existing) {
378
448
  getDatabase()
379
449
  .prepare(`UPDATE agent_identities
380
- SET agent_type = ?, trust_level = ?, autonomy_mode = ?, approval_mode = ?, description = ?, updated_at = ?
450
+ SET agent_type = ?, identity_key = COALESCE(identity_key, ?),
451
+ provider = COALESCE(provider, ?), machine_key = COALESCE(machine_key, ?),
452
+ persona_key = COALESCE(persona_key, ?), trust_level = ?,
453
+ autonomy_mode = ?, approval_mode = ?, description = ?, updated_at = ?
381
454
  WHERE id = ?`)
382
- .run(input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, existing.id);
455
+ .run(input.agentType, identityFields.identityKey, identityFields.provider, identityFields.machineKey, identityFields.personaKey, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, existing.id);
383
456
  return findAgentIdentity(existing.id);
384
457
  }
385
458
  const agentId = `agt_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
386
459
  getDatabase()
387
460
  .prepare(`INSERT INTO agent_identities (
388
- id, label, agent_type, trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
389
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
390
- .run(agentId, input.agentLabel, input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, now);
461
+ id, label, agent_type, identity_key, provider, machine_key, persona_key,
462
+ trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
463
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
464
+ .run(agentId, input.agentLabel, input.agentType, identityFields.identityKey, identityFields.provider, identityFields.machineKey, identityFields.personaKey, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, now);
391
465
  return findAgentIdentity(agentId);
392
466
  }
393
467
  function ensureSettingsRow(now = new Date().toISOString()) {
@@ -441,6 +515,10 @@ export function listAgentIdentities() {
441
515
  agent_identities.id,
442
516
  agent_identities.label,
443
517
  agent_identities.agent_type,
518
+ agent_identities.identity_key,
519
+ agent_identities.provider,
520
+ agent_identities.machine_key,
521
+ agent_identities.persona_key,
444
522
  agent_identities.trust_level,
445
523
  agent_identities.autonomy_mode,
446
524
  agent_identities.approval_mode,
@@ -448,19 +526,25 @@ export function listAgentIdentities() {
448
526
  agent_identities.created_at,
449
527
  agent_identities.updated_at,
450
528
  COUNT(agent_tokens.id) AS token_count,
451
- COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
529
+ COALESCE(SUM(CASE WHEN agent_tokens.id IS NOT NULL AND agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
452
530
  FROM agent_identities
453
531
  LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
454
532
  GROUP BY agent_identities.id
455
533
  ORDER BY agent_identities.created_at DESC`)
456
534
  .all();
457
- const manualAgents = rows.map(mapAgent);
535
+ const links = listAgentIdentityUserLinks(rows.map((row) => row.id));
536
+ const manualAgents = rows.map((row) => mapAgent(row, links.get(row.id) ?? []));
458
537
  const modelAgents = listAiModelConnections().map(buildConnectionAgentIdentity);
459
538
  const settings = readSettingsRow();
460
539
  const forgeAgent = agentIdentitySchema.parse({
461
540
  id: FORGE_DEFAULT_AGENT_ID,
462
541
  label: "Forge Agent",
463
542
  agentType: "forge_default",
543
+ identityKey: "forge:default",
544
+ provider: null,
545
+ machineKey: null,
546
+ personaKey: "default",
547
+ linkedUsers: [],
464
548
  trustLevel: "trusted",
465
549
  autonomyMode: "approval_required",
466
550
  approvalMode: "approval_by_default",
@@ -470,7 +554,11 @@ export function listAgentIdentities() {
470
554
  createdAt: settings.created_at,
471
555
  updatedAt: settings.updated_at
472
556
  });
473
- return [forgeAgent, ...modelAgents, ...manualAgents];
557
+ const deduped = new Map();
558
+ for (const agent of [forgeAgent, ...modelAgents, ...manualAgents]) {
559
+ deduped.set(agent.identityKey ?? agent.id, agent);
560
+ }
561
+ return Array.from(deduped.values());
474
562
  }
475
563
  export function isPsycheAuthRequired() {
476
564
  ensureSettingsRow();
@@ -153,6 +153,29 @@ export function ensureSystemUsers() {
153
153
  }
154
154
  }
155
155
  }
156
+ export function ensureBotUser(input) {
157
+ const parsed = createUserSchema.parse({
158
+ kind: "bot",
159
+ handle: normalizeHandle(input.handle),
160
+ displayName: input.displayName,
161
+ description: input.description,
162
+ accentColor: input.accentColor
163
+ });
164
+ const now = new Date().toISOString();
165
+ getDatabase()
166
+ .prepare(`INSERT INTO users (id, kind, handle, display_name, description, accent_color, created_at, updated_at)
167
+ VALUES (?, 'bot', ?, ?, ?, ?, ?, ?)
168
+ ON CONFLICT(id) DO UPDATE SET
169
+ kind = 'bot',
170
+ handle = excluded.handle,
171
+ display_name = excluded.display_name,
172
+ description = excluded.description,
173
+ accent_color = excluded.accent_color,
174
+ updated_at = excluded.updated_at`)
175
+ .run(input.id, parsed.handle, parsed.displayName, parsed.description, parsed.accentColor, now, now);
176
+ ensurePermissiveGrantsForUser(input.id, now);
177
+ return getUserById(input.id);
178
+ }
156
179
  function ensurePermissiveGrantsForUser(userId, now) {
157
180
  const existingUsers = listUsers();
158
181
  for (const otherUser of existingUsers) {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { LEGACY_WORKBENCH_PORT_KINDS, WORKBENCH_PORT_KINDS, normalizeWorkbenchPortKind } from "../../src/lib/workbench/nodes.js";
3
+ import { formatLocalDateKey } from "../../src/lib/date-keys.js";
3
4
  export const taskStatusSchema = z.enum([
4
5
  "backlog",
5
6
  "focus",
@@ -2103,6 +2104,15 @@ export const agentIdentitySchema = z.object({
2103
2104
  id: z.string(),
2104
2105
  label: z.string(),
2105
2106
  agentType: z.string(),
2107
+ identityKey: z.string().nullable().default(null),
2108
+ provider: agentRuntimeProviderSchema.nullable().default(null),
2109
+ machineKey: z.string().nullable().default(null),
2110
+ personaKey: z.string().nullable().default(null),
2111
+ linkedUsers: z.array(z.object({
2112
+ userId: z.string(),
2113
+ role: z.string(),
2114
+ user: userSummarySchema.nullable().default(null)
2115
+ })).default([]),
2106
2116
  trustLevel: agentTrustLevelSchema,
2107
2117
  autonomyMode: autonomyModeSchema,
2108
2118
  approvalMode: approvalModeSchema,
@@ -2187,6 +2197,10 @@ export const createAgentRuntimeSessionSchema = z.object({
2187
2197
  provider: agentRuntimeProviderSchema,
2188
2198
  agentLabel: nonEmptyTrimmedString,
2189
2199
  agentType: trimmedString.default("assistant"),
2200
+ agentIdentityKey: trimmedString.optional(),
2201
+ machineKey: trimmedString.optional(),
2202
+ personaKey: trimmedString.optional(),
2203
+ linkedUserIds: uniqueStringArraySchema.default([]),
2190
2204
  actorLabel: nonEmptyTrimmedString,
2191
2205
  sessionKey: nonEmptyTrimmedString,
2192
2206
  sessionLabel: trimmedString.default(""),
@@ -3120,6 +3134,12 @@ export const taskMutationShape = {
3120
3134
  notes: z.array(nestedCreateNoteSchema).default([])
3121
3135
  };
3122
3136
  export const createTaskSchema = z.object(taskMutationShape);
3137
+ const habitCheckInWriteSchema = z.object({
3138
+ dateKey: dateOnlySchema.default(() => formatLocalDateKey()),
3139
+ status: habitCheckInStatusSchema,
3140
+ note: trimmedString.default(""),
3141
+ description: trimmedString.optional()
3142
+ });
3123
3143
  const habitMutationShape = {
3124
3144
  title: nonEmptyTrimmedString,
3125
3145
  description: trimmedString.default(""),
@@ -3206,6 +3226,7 @@ export const updateHabitSchema = z
3206
3226
  linkedBehaviorId: nonEmptyTrimmedString.nullable().optional(),
3207
3227
  rewardXp: z.number().int().min(1).max(100).optional(),
3208
3228
  penaltyXp: z.number().int().min(1).max(100).optional(),
3229
+ checkIn: habitCheckInWriteSchema.optional(),
3209
3230
  generatedHealthEventTemplate: z
3210
3231
  .object({
3211
3232
  enabled: z.boolean().optional(),
@@ -3371,12 +3392,7 @@ export const taskRunFinishSchema = z.object({
3371
3392
  export const taskRunFocusSchema = z.object({
3372
3393
  actor: nonEmptyTrimmedString.optional()
3373
3394
  });
3374
- export const createHabitCheckInSchema = z.object({
3375
- dateKey: dateOnlySchema.default(new Date().toISOString().slice(0, 10)),
3376
- status: habitCheckInStatusSchema,
3377
- note: trimmedString.default(""),
3378
- description: trimmedString.optional()
3379
- });
3395
+ export const createHabitCheckInSchema = habitCheckInWriteSchema;
3380
3396
  export const updateSettingsSchema = z.object({
3381
3397
  profile: z
3382
3398
  .object({
@@ -5,6 +5,7 @@ import { HttpError } from "./errors.js";
5
5
  import { updateWorkoutMetadata } from "./health.js";
6
6
  import { canonicalizeMovementCategoryTags, listMovementPlaces, normalizeMovementCategoryTag, updateMovementPlace } from "./movement.js";
7
7
  import { listHabits } from "./repositories/habits.js";
8
+ import { formatLocalDateKey } from "../../src/lib/date-keys.js";
8
9
  const watchCapability = "watch-ready";
9
10
  const watchHistoryStateSchema = z.enum(["aligned", "unaligned", "unknown"]);
10
11
  const watchPromptKindSchema = z.enum([
@@ -56,7 +57,11 @@ export const mobileWatchHabitCheckInSchema = z.object({
56
57
  sessionId: z.string().trim().min(1),
57
58
  pairingToken: z.string().trim().min(1),
58
59
  dedupeKey: z.string().trim().min(1),
59
- dateKey: z.string().trim().min(1).default(new Date().toISOString().slice(0, 10)),
60
+ dateKey: z
61
+ .string()
62
+ .trim()
63
+ .min(1)
64
+ .default(() => formatLocalDateKey()),
60
65
  status: z.enum(["done", "missed"]),
61
66
  note: z.string().trim().default(""),
62
67
  description: z.string().trim().optional()
@@ -82,24 +87,24 @@ function nowIso() {
82
87
  return new Date().toISOString();
83
88
  }
84
89
  function formatDateKey(date) {
85
- return date.toISOString().slice(0, 10);
90
+ return formatLocalDateKey(date);
86
91
  }
87
92
  function parseDateKey(dateKey) {
88
93
  const [year, month, day] = dateKey.split("-").map(Number);
89
- return new Date(Date.UTC(year, month - 1, day));
94
+ return new Date(year, month - 1, day);
90
95
  }
91
- function startOfUtcDay(date) {
92
- return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
96
+ function startOfLocalDay(date) {
97
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
93
98
  }
94
- function addUtcDays(date, days) {
99
+ function addLocalDays(date, days) {
95
100
  const next = new Date(date);
96
- next.setUTCDate(next.getUTCDate() + days);
101
+ next.setDate(next.getDate() + days);
97
102
  return next;
98
103
  }
99
- function startOfUtcWeek(date) {
100
- const start = startOfUtcDay(date);
101
- const offset = (start.getUTCDay() + 6) % 7;
102
- start.setUTCDate(start.getUTCDate() - offset);
104
+ function startOfLocalWeek(date) {
105
+ const start = startOfLocalDay(date);
106
+ const offset = (start.getDay() + 6) % 7;
107
+ start.setDate(start.getDate() - offset);
103
108
  return start;
104
109
  }
105
110
  function isAlignedCheckIn(habit, status) {
@@ -125,15 +130,15 @@ function buildHabitHistory(habit, options) {
125
130
  ? parseDateKey(options.anchorDateKey)
126
131
  : new Date();
127
132
  if (habit.frequency === "daily") {
128
- const today = startOfUtcDay(now);
133
+ const today = startOfLocalDay(now);
129
134
  return Array.from({ length: 7 }, (_, index) => {
130
135
  const offset = index - 6;
131
- const date = addUtcDays(today, offset);
136
+ const date = addLocalDays(today, offset);
132
137
  const dateKey = formatDateKey(date);
133
138
  const checkIn = habit.checkIns.find((entry) => entry.dateKey === dateKey) ?? null;
134
139
  return {
135
140
  id: dateKey,
136
- label: ["S", "M", "T", "W", "T", "F", "S"][date.getUTCDay()],
141
+ label: ["S", "M", "T", "W", "T", "F", "S"][date.getDay()],
137
142
  periodKey: dateKey,
138
143
  current: offset === 0,
139
144
  state: checkIn
@@ -144,13 +149,13 @@ function buildHabitHistory(habit, options) {
144
149
  };
145
150
  });
146
151
  }
147
- const thisWeek = startOfUtcWeek(now);
152
+ const thisWeek = startOfLocalWeek(now);
148
153
  return Array.from({ length: 7 }, (_, index) => {
149
154
  const offset = index - 6;
150
- const weekStart = addUtcDays(thisWeek, offset * 7);
155
+ const weekStart = addLocalDays(thisWeek, offset * 7);
151
156
  const weekKey = formatDateKey(weekStart);
152
157
  const weekEntries = habit.checkIns.filter((entry) => {
153
- const entryWeek = formatDateKey(startOfUtcWeek(parseDateKey(entry.dateKey)));
158
+ const entryWeek = formatDateKey(startOfLocalWeek(parseDateKey(entry.dateKey)));
154
159
  return entryWeek === weekKey;
155
160
  });
156
161
  const alignedCount = weekEntries.filter((entry) => isAlignedCheckIn(habit, entry.status)).length;
@@ -375,7 +380,9 @@ function projectionForStoredEvent(event) {
375
380
  const categoryCandidate = Array.isArray(event.payload.categoryTags)
376
381
  ? event.payload.categoryTags
377
382
  : typeof event.payload.category === "string"
378
- ? watchCategoryMap.get(event.payload.category) ?? [event.payload.category]
383
+ ? (watchCategoryMap.get(event.payload.category) ?? [
384
+ event.payload.category
385
+ ])
379
386
  : [];
380
387
  const categoryTags = canonicalizeMovementCategoryTags(categoryCandidate.flatMap((value) => typeof value === "string" ? [normalizeMovementCategoryTag(value)] : []));
381
388
  try {
@@ -403,12 +410,15 @@ function projectionForStoredEvent(event) {
403
410
  status: "projection_failed",
404
411
  details: {
405
412
  reason: "place_update_failed",
406
- message: error instanceof Error ? error.message : "Unknown place update error"
413
+ message: error instanceof Error
414
+ ? error.message
415
+ : "Unknown place update error"
407
416
  }
408
417
  };
409
418
  }
410
419
  }
411
- if (event.eventType === "workout_annotation" && event.linkedContext.workoutId) {
420
+ if (event.eventType === "workout_annotation" &&
421
+ event.linkedContext.workoutId) {
412
422
  try {
413
423
  const workout = updateWorkoutMetadata(event.linkedContext.workoutId, {
414
424
  subjectiveEffort: typeof event.payload.subjectiveEffort === "number"
@@ -449,7 +459,9 @@ function projectionForStoredEvent(event) {
449
459
  status: "projection_failed",
450
460
  details: {
451
461
  reason: "workout_update_failed",
452
- message: error instanceof Error ? error.message : "Unknown workout update error"
462
+ message: error instanceof Error
463
+ ? error.message
464
+ : "Unknown workout update error"
453
465
  }
454
466
  };
455
467
  }
@@ -0,0 +1,21 @@
1
+ function readPart(parts, type) {
2
+ return parts.find((part) => part.type === type)?.value ?? "";
3
+ }
4
+ export function getRuntimeTimeZone() {
5
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
6
+ }
7
+ export function formatDateKeyInTimeZone(date, timeZone) {
8
+ const parts = new Intl.DateTimeFormat("en-CA", {
9
+ timeZone,
10
+ year: "numeric",
11
+ month: "2-digit",
12
+ day: "2-digit"
13
+ }).formatToParts(date);
14
+ const year = readPart(parts, "year");
15
+ const month = readPart(parts, "month");
16
+ const day = readPart(parts, "day");
17
+ return `${year}-${month}-${day}`;
18
+ }
19
+ export function formatLocalDateKey(date = new Date()) {
20
+ return formatDateKeyInTimeZone(date, getRuntimeTimeZone());
21
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.47",
5
+ "version": "0.2.49",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],