forge-openclaw-plugin 0.2.4 → 0.2.7

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 (117) hide show
  1. package/README.md +113 -5
  2. package/dist/assets/board-CzgvdLO8.js +6 -0
  3. package/dist/assets/board-CzgvdLO8.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-8d_oM8fL.js +27 -0
  6. package/dist/assets/index-8d_oM8fL.js.map +1 -0
  7. package/dist/assets/index-D4A_bq8m.css +1 -0
  8. package/dist/assets/motion-STUd1O46.js +10 -0
  9. package/dist/assets/motion-STUd1O46.js.map +1 -0
  10. package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  11. package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  12. package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  13. package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
  14. package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
  15. package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  16. package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  17. package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  18. package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  19. package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  20. package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  21. package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  22. package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  23. package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  24. package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  25. package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  26. package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  27. package/dist/assets/table-CtNlETLc.js +23 -0
  28. package/dist/assets/table-CtNlETLc.js.map +1 -0
  29. package/dist/assets/ui-ThzkR_oW.js +46 -0
  30. package/dist/assets/ui-ThzkR_oW.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-DyHAI6nk.js +423 -0
  33. package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
  34. package/dist/assets/viz-BJuBCz_G.js +34 -0
  35. package/dist/assets/viz-BJuBCz_G.js.map +1 -0
  36. package/dist/favicon.ico +0 -0
  37. package/dist/favicon.png +0 -0
  38. package/dist/index.html +29 -0
  39. package/dist/openclaw/api-client.d.ts +8 -0
  40. package/dist/openclaw/api-client.js +31 -4
  41. package/dist/openclaw/local-runtime.d.ts +3 -0
  42. package/dist/openclaw/local-runtime.js +135 -0
  43. package/dist/openclaw/parity.d.ts +4 -4
  44. package/dist/openclaw/parity.js +23 -33
  45. package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
  46. package/dist/openclaw/plugin-entry-shared.js +51 -9
  47. package/dist/openclaw/routes.d.ts +12 -3
  48. package/dist/openclaw/routes.js +156 -924
  49. package/dist/openclaw/tools.js +242 -1100
  50. package/dist/server/app.js +2450 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/e2e-server.js +20 -0
  53. package/dist/server/errors.js +15 -0
  54. package/dist/server/index.js +16 -0
  55. package/dist/server/managers/base.js +17 -0
  56. package/dist/server/managers/contracts.js +47 -0
  57. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  58. package/dist/server/managers/platform/audit-manager.js +15 -0
  59. package/dist/server/managers/platform/authentication-manager.js +56 -0
  60. package/dist/server/managers/platform/authorization-manager.js +56 -0
  61. package/dist/server/managers/platform/background-job-manager.js +10 -0
  62. package/dist/server/managers/platform/configuration-manager.js +33 -0
  63. package/dist/server/managers/platform/database-manager.js +14 -0
  64. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  65. package/dist/server/managers/platform/external-service-manager.js +11 -0
  66. package/dist/server/managers/platform/health-manager.js +7 -0
  67. package/dist/server/managers/platform/migration-manager.js +8 -0
  68. package/dist/server/managers/platform/search-index-manager.js +4 -0
  69. package/dist/server/managers/platform/secrets-manager.js +19 -0
  70. package/dist/server/managers/platform/session-manager.js +121 -0
  71. package/dist/server/managers/platform/storage-manager.js +16 -0
  72. package/dist/server/managers/platform/token-manager.js +37 -0
  73. package/dist/server/managers/platform/transaction-manager.js +8 -0
  74. package/dist/server/managers/platform/trusted-network.js +39 -0
  75. package/dist/server/managers/runtime.js +56 -0
  76. package/dist/server/managers/type-guards.js +4 -0
  77. package/dist/server/openapi.js +3512 -0
  78. package/dist/server/psyche-types.js +395 -0
  79. package/dist/server/repositories/activity-events.js +157 -0
  80. package/dist/server/repositories/collaboration.js +497 -0
  81. package/dist/server/repositories/comments.js +176 -0
  82. package/dist/server/repositories/deleted-entities.js +192 -0
  83. package/dist/server/repositories/domains.js +30 -0
  84. package/dist/server/repositories/event-log.js +64 -0
  85. package/dist/server/repositories/goals.js +159 -0
  86. package/dist/server/repositories/projects.js +214 -0
  87. package/dist/server/repositories/psyche.js +1356 -0
  88. package/dist/server/repositories/rewards.js +675 -0
  89. package/dist/server/repositories/settings.js +399 -0
  90. package/dist/server/repositories/tags.js +160 -0
  91. package/dist/server/repositories/task-runs.js +488 -0
  92. package/dist/server/repositories/tasks.js +413 -0
  93. package/dist/server/services/context.js +214 -0
  94. package/dist/server/services/dashboard.js +170 -0
  95. package/dist/server/services/entity-crud.js +576 -0
  96. package/dist/server/services/gamification.js +215 -0
  97. package/dist/server/services/insights.js +91 -0
  98. package/dist/server/services/projects.js +75 -0
  99. package/dist/server/services/psyche.js +63 -0
  100. package/dist/server/services/relations.js +28 -0
  101. package/dist/server/services/reviews.js +88 -0
  102. package/dist/server/services/run-recovery.js +13 -0
  103. package/dist/server/services/tagging.js +49 -0
  104. package/dist/server/services/task-run-watchdog.js +92 -0
  105. package/dist/server/services/work-time.js +176 -0
  106. package/dist/server/types.js +999 -0
  107. package/dist/server/web.js +91 -0
  108. package/openclaw.plugin.json +21 -9
  109. package/package.json +17 -4
  110. package/server/migrations/001_core.sql +333 -0
  111. package/server/migrations/002_psyche.sql +241 -0
  112. package/server/migrations/003_timer_execution.sql +18 -0
  113. package/server/migrations/004_psyche_linked_entities.sql +5 -0
  114. package/server/migrations/005_adaptive_schemas.sql +157 -0
  115. package/server/migrations/006_psyche_auth_setting.sql +4 -0
  116. package/server/migrations/007_deleted_entities.sql +16 -0
  117. package/skills/forge-openclaw/SKILL.md +189 -275
@@ -0,0 +1,497 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase } from "../db.js";
3
+ import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
4
+ import { recordActivityEvent } from "./activity-events.js";
5
+ import { recordEventLog } from "./event-log.js";
6
+ import { createProject } from "./projects.js";
7
+ import { createTask } from "./tasks.js";
8
+ import { recordInsightAppliedReward } from "./rewards.js";
9
+ import { agentActionSchema, approvalRequestSchema, createAgentActionSchema, createInsightFeedbackSchema, createInsightSchema, insightFeedbackSchema, insightSchema, updateInsightSchema } from "../types.js";
10
+ function mapInsight(row) {
11
+ return insightSchema.parse({
12
+ id: row.id,
13
+ originType: row.origin_type,
14
+ originAgentId: row.origin_agent_id,
15
+ originLabel: row.origin_label,
16
+ visibility: row.visibility,
17
+ status: row.status,
18
+ entityType: row.entity_type,
19
+ entityId: row.entity_id,
20
+ timeframeLabel: row.timeframe_label,
21
+ title: row.title,
22
+ summary: row.summary,
23
+ recommendation: row.recommendation,
24
+ rationale: row.rationale,
25
+ confidence: row.confidence,
26
+ ctaLabel: row.cta_label,
27
+ evidence: JSON.parse(row.evidence_json),
28
+ createdAt: row.created_at,
29
+ updatedAt: row.updated_at
30
+ });
31
+ }
32
+ function mapFeedback(row) {
33
+ return insightFeedbackSchema.parse({
34
+ id: row.id,
35
+ insightId: row.insight_id,
36
+ actor: row.actor,
37
+ feedbackType: row.feedback_type,
38
+ note: row.note,
39
+ createdAt: row.created_at
40
+ });
41
+ }
42
+ function mapApproval(row) {
43
+ return approvalRequestSchema.parse({
44
+ id: row.id,
45
+ actionType: row.action_type,
46
+ status: row.status,
47
+ title: row.title,
48
+ summary: row.summary,
49
+ entityType: row.entity_type,
50
+ entityId: row.entity_id,
51
+ requestedByAgentId: row.requested_by_agent_id,
52
+ requestedByTokenId: row.requested_by_token_id,
53
+ requestedPayload: JSON.parse(row.requested_payload_json),
54
+ approvedBy: row.approved_by,
55
+ approvedAt: row.approved_at,
56
+ rejectedBy: row.rejected_by,
57
+ rejectedAt: row.rejected_at,
58
+ resolutionNote: row.resolution_note,
59
+ createdAt: row.created_at,
60
+ updatedAt: row.updated_at
61
+ });
62
+ }
63
+ function mapAction(row) {
64
+ return agentActionSchema.parse({
65
+ id: row.id,
66
+ agentId: row.agent_id,
67
+ tokenId: row.token_id,
68
+ actionType: row.action_type,
69
+ riskLevel: row.risk_level,
70
+ status: row.status,
71
+ title: row.title,
72
+ summary: row.summary,
73
+ payload: JSON.parse(row.payload_json),
74
+ idempotencyKey: row.idempotency_key,
75
+ approvalRequestId: row.approval_request_id,
76
+ outcome: JSON.parse(row.outcome_json),
77
+ createdAt: row.created_at,
78
+ updatedAt: row.updated_at,
79
+ completedAt: row.completed_at
80
+ });
81
+ }
82
+ function shouldRequireApproval(input, token) {
83
+ if (!token) {
84
+ return input.riskLevel !== "low";
85
+ }
86
+ if (token.autonomyMode === "approval_required") {
87
+ return true;
88
+ }
89
+ if (input.riskLevel === "high") {
90
+ return token.approvalMode !== "none";
91
+ }
92
+ if (token.approvalMode === "approval_by_default") {
93
+ return true;
94
+ }
95
+ return false;
96
+ }
97
+ function getInsightRowById(insightId) {
98
+ return getDatabase()
99
+ .prepare(`SELECT
100
+ id, origin_type, origin_agent_id, origin_label, visibility, status, entity_type, entity_id,
101
+ timeframe_label, title, summary, recommendation, rationale, confidence, cta_label, evidence_json, created_at, updated_at
102
+ FROM insights
103
+ WHERE id = ?`)
104
+ .get(insightId);
105
+ }
106
+ export function listInsights(filters = {}) {
107
+ const whereClauses = [];
108
+ const params = [];
109
+ if (filters.entityType) {
110
+ whereClauses.push("entity_type = ?");
111
+ params.push(filters.entityType);
112
+ }
113
+ if (filters.entityId) {
114
+ whereClauses.push("entity_id = ?");
115
+ params.push(filters.entityId);
116
+ }
117
+ if (filters.status) {
118
+ whereClauses.push("status = ?");
119
+ params.push(filters.status);
120
+ }
121
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
122
+ const limitSql = filters.limit ? "LIMIT ?" : "";
123
+ if (filters.limit) {
124
+ params.push(filters.limit);
125
+ }
126
+ const rows = getDatabase()
127
+ .prepare(`SELECT
128
+ id, origin_type, origin_agent_id, origin_label, visibility, status, entity_type, entity_id,
129
+ timeframe_label, title, summary, recommendation, rationale, confidence, cta_label, evidence_json, created_at, updated_at
130
+ FROM insights
131
+ ${whereSql}
132
+ ORDER BY created_at DESC
133
+ ${limitSql}`)
134
+ .all(...params);
135
+ return filterDeletedEntities("insight", rows.map(mapInsight));
136
+ }
137
+ export function getInsightById(insightId) {
138
+ if (isEntityDeleted("insight", insightId)) {
139
+ return undefined;
140
+ }
141
+ const row = getInsightRowById(insightId);
142
+ return row ? mapInsight(row) : undefined;
143
+ }
144
+ export function createInsight(input, context) {
145
+ const parsed = createInsightSchema.parse(input);
146
+ const now = new Date().toISOString();
147
+ const insightId = `ins_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
148
+ getDatabase()
149
+ .prepare(`INSERT INTO insights (
150
+ id, origin_type, origin_agent_id, origin_label, visibility, status, entity_type, entity_id, timeframe_label,
151
+ title, summary, recommendation, rationale, confidence, cta_label, evidence_json, created_at, updated_at
152
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
153
+ .run(insightId, parsed.originType, parsed.originAgentId, parsed.originLabel, parsed.visibility, parsed.status, parsed.entityType, parsed.entityId, parsed.timeframeLabel, parsed.title, parsed.summary, parsed.recommendation, parsed.rationale, parsed.confidence, parsed.ctaLabel, JSON.stringify(parsed.evidence), now, now);
154
+ recordActivityEvent({
155
+ entityType: "insight",
156
+ entityId: insightId,
157
+ eventType: "insight_created",
158
+ title: `Insight captured: ${parsed.title}`,
159
+ description: parsed.summary,
160
+ actor: context.actor ?? null,
161
+ source: context.source,
162
+ metadata: {
163
+ originType: parsed.originType,
164
+ entityType: parsed.entityType ?? "",
165
+ entityId: parsed.entityId ?? ""
166
+ }
167
+ });
168
+ recordEventLog({
169
+ eventKind: "insight.created",
170
+ entityType: "insight",
171
+ entityId: insightId,
172
+ actor: context.actor ?? null,
173
+ source: context.source,
174
+ metadata: {
175
+ originType: parsed.originType,
176
+ confidence: parsed.confidence
177
+ }
178
+ });
179
+ return getInsightById(insightId);
180
+ }
181
+ export function updateInsight(insightId, input, context) {
182
+ const current = getInsightById(insightId);
183
+ if (!current) {
184
+ return undefined;
185
+ }
186
+ const parsed = updateInsightSchema.parse(input);
187
+ const updatedAt = new Date().toISOString();
188
+ getDatabase()
189
+ .prepare(`UPDATE insights
190
+ SET visibility = ?, status = ?, entity_type = ?, entity_id = ?, timeframe_label = ?, title = ?, summary = ?,
191
+ recommendation = ?, rationale = ?, confidence = ?, cta_label = ?, evidence_json = ?, updated_at = ?
192
+ WHERE id = ?`)
193
+ .run(parsed.visibility ?? current.visibility, parsed.status ?? current.status, parsed.entityType === undefined ? current.entityType : parsed.entityType, parsed.entityId === undefined ? current.entityId : parsed.entityId, parsed.timeframeLabel === undefined ? current.timeframeLabel : parsed.timeframeLabel, parsed.title ?? current.title, parsed.summary ?? current.summary, parsed.recommendation ?? current.recommendation, parsed.rationale ?? current.rationale, parsed.confidence ?? current.confidence, parsed.ctaLabel ?? current.ctaLabel, JSON.stringify(parsed.evidence ?? current.evidence), updatedAt, insightId);
194
+ recordEventLog({
195
+ eventKind: "insight.updated",
196
+ entityType: "insight",
197
+ entityId: insightId,
198
+ actor: context.actor ?? null,
199
+ source: context.source
200
+ });
201
+ return getInsightById(insightId);
202
+ }
203
+ export function deleteInsight(insightId, context) {
204
+ const existing = getInsightRowById(insightId);
205
+ if (!existing) {
206
+ return undefined;
207
+ }
208
+ getDatabase().prepare(`DELETE FROM insight_feedback WHERE insight_id = ?`).run(insightId);
209
+ getDatabase().prepare(`DELETE FROM insights WHERE id = ?`).run(insightId);
210
+ recordActivityEvent({
211
+ entityType: "insight",
212
+ entityId: insightId,
213
+ eventType: "insight_deleted",
214
+ title: `Insight deleted: ${existing.title}`,
215
+ description: existing.summary,
216
+ actor: context.actor ?? null,
217
+ source: context.source,
218
+ metadata: {
219
+ entityType: existing.entity_type ?? "",
220
+ entityId: existing.entity_id ?? ""
221
+ }
222
+ });
223
+ recordEventLog({
224
+ eventKind: "insight.deleted",
225
+ entityType: "insight",
226
+ entityId: insightId,
227
+ actor: context.actor ?? null,
228
+ source: context.source
229
+ });
230
+ return mapInsight(existing);
231
+ }
232
+ export function listInsightFeedback(insightId) {
233
+ const rows = getDatabase()
234
+ .prepare(`SELECT id, insight_id, actor, feedback_type, note, created_at
235
+ FROM insight_feedback
236
+ WHERE insight_id = ?
237
+ ORDER BY created_at DESC`)
238
+ .all(insightId);
239
+ return rows.map(mapFeedback);
240
+ }
241
+ export function createInsightFeedback(insightId, input, context) {
242
+ const insight = getInsightById(insightId);
243
+ if (!insight) {
244
+ return undefined;
245
+ }
246
+ const parsed = createInsightFeedbackSchema.parse(input);
247
+ const feedbackId = `fbk_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
248
+ const createdAt = new Date().toISOString();
249
+ getDatabase()
250
+ .prepare(`INSERT INTO insight_feedback (id, insight_id, actor, feedback_type, note, created_at) VALUES (?, ?, ?, ?, ?, ?)`)
251
+ .run(feedbackId, insightId, parsed.actor ?? context.actor ?? null, parsed.feedbackType, parsed.note, createdAt);
252
+ const nextStatus = parsed.feedbackType === "accepted"
253
+ ? "accepted"
254
+ : parsed.feedbackType === "dismissed"
255
+ ? "dismissed"
256
+ : parsed.feedbackType === "applied"
257
+ ? "applied"
258
+ : "snoozed";
259
+ getDatabase().prepare(`UPDATE insights SET status = ?, updated_at = ? WHERE id = ?`).run(nextStatus, createdAt, insightId);
260
+ if (parsed.feedbackType === "applied" && insight.entityType && insight.entityId) {
261
+ recordInsightAppliedReward(insightId, insight.entityType, insight.entityId, {
262
+ actor: parsed.actor ?? context.actor ?? null,
263
+ source: context.source
264
+ });
265
+ }
266
+ recordActivityEvent({
267
+ entityType: "insight",
268
+ entityId: insightId,
269
+ eventType: "insight_feedback_recorded",
270
+ title: `Insight ${nextStatus}: ${insight.title}`,
271
+ description: parsed.note || `Insight marked ${nextStatus}.`,
272
+ actor: parsed.actor ?? context.actor ?? null,
273
+ source: context.source,
274
+ metadata: {
275
+ feedbackType: parsed.feedbackType
276
+ }
277
+ });
278
+ return listInsightFeedback(insightId)[0];
279
+ }
280
+ function insertApprovalRequest(input) {
281
+ const approvalId = `apr_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
282
+ const now = new Date().toISOString();
283
+ getDatabase()
284
+ .prepare(`INSERT INTO approval_requests (
285
+ id, action_type, status, title, summary, entity_type, entity_id, requested_by_agent_id, requested_by_token_id,
286
+ requested_payload_json, resolution_note, created_at, updated_at
287
+ ) VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, '', ?, ?)`)
288
+ .run(approvalId, input.actionType, input.title, input.summary, input.entityType ?? null, input.entityId ?? null, input.requestedByAgentId ?? null, input.requestedByTokenId ?? null, JSON.stringify(input.requestedPayload), now, now);
289
+ recordEventLog({
290
+ eventKind: "approval.requested",
291
+ entityType: "approval_request",
292
+ entityId: approvalId,
293
+ actor: null,
294
+ source: "agent",
295
+ metadata: {
296
+ actionType: input.actionType
297
+ }
298
+ });
299
+ return listApprovalRequests().find((entry) => entry.id === approvalId);
300
+ }
301
+ export function listApprovalRequests(status) {
302
+ const rows = getDatabase()
303
+ .prepare(`SELECT
304
+ id, action_type, status, title, summary, entity_type, entity_id, requested_by_agent_id, requested_by_token_id,
305
+ requested_payload_json, approved_by, approved_at, rejected_by, rejected_at, resolution_note, created_at, updated_at
306
+ FROM approval_requests
307
+ ${status ? "WHERE status = ?" : ""}
308
+ ORDER BY created_at DESC`)
309
+ .all(...(status ? [status] : []));
310
+ return rows.map(mapApproval);
311
+ }
312
+ function getApprovalRequestRow(id) {
313
+ return getDatabase()
314
+ .prepare(`SELECT
315
+ id, action_type, status, title, summary, entity_type, entity_id, requested_by_agent_id, requested_by_token_id,
316
+ requested_payload_json, approved_by, approved_at, rejected_by, rejected_at, resolution_note, created_at, updated_at
317
+ FROM approval_requests
318
+ WHERE id = ?`)
319
+ .get(id);
320
+ }
321
+ function executeAgentAction(action, context) {
322
+ if (action.actionType === "create_insight") {
323
+ const insight = createInsight(createInsightSchema.parse(action.payload), context);
324
+ return { insightId: insight.id };
325
+ }
326
+ if (action.actionType === "create_task") {
327
+ const task = createTask(action.payload, { source: context.source, actor: context.actor ?? null });
328
+ return { taskId: task.id };
329
+ }
330
+ if (action.actionType === "create_project") {
331
+ const project = createProject(action.payload, { source: context.source, actor: context.actor ?? null });
332
+ return { projectId: project.id };
333
+ }
334
+ return { deferred: true };
335
+ }
336
+ export function createAgentAction(input, context, idempotencyKey) {
337
+ const parsed = createAgentActionSchema.parse(input);
338
+ if (idempotencyKey) {
339
+ const existing = getDatabase()
340
+ .prepare(`SELECT
341
+ id, agent_id, token_id, action_type, risk_level, status, title, summary, payload_json, idempotency_key,
342
+ approval_request_id, outcome_json, created_at, updated_at, completed_at
343
+ FROM agent_actions
344
+ WHERE idempotency_key = ?`)
345
+ .get(idempotencyKey);
346
+ if (existing) {
347
+ return {
348
+ action: mapAction(existing),
349
+ approvalRequest: existing.approval_request_id
350
+ ? listApprovalRequests().find((entry) => entry.id === existing.approval_request_id) ?? null
351
+ : null
352
+ };
353
+ }
354
+ }
355
+ const now = new Date().toISOString();
356
+ const actionId = `act_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
357
+ const requiresApproval = shouldRequireApproval(parsed, context.token);
358
+ const approvalRequest = requiresApproval
359
+ ? insertApprovalRequest({
360
+ actionType: parsed.actionType,
361
+ title: parsed.title,
362
+ summary: parsed.summary,
363
+ entityType: typeof parsed.payload.entityType === "string" ? parsed.payload.entityType : null,
364
+ entityId: typeof parsed.payload.entityId === "string" ? parsed.payload.entityId : null,
365
+ requestedByAgentId: parsed.agentId ?? context.token?.agentId ?? null,
366
+ requestedByTokenId: parsed.tokenId ?? context.token?.id ?? null,
367
+ requestedPayload: parsed.payload
368
+ })
369
+ : null;
370
+ const status = requiresApproval ? "pending_approval" : "executed";
371
+ const actionSeed = agentActionSchema.parse({
372
+ id: actionId,
373
+ agentId: parsed.agentId ?? context.token?.agentId ?? null,
374
+ tokenId: parsed.tokenId ?? context.token?.id ?? null,
375
+ actionType: parsed.actionType,
376
+ riskLevel: parsed.riskLevel,
377
+ status,
378
+ title: parsed.title,
379
+ summary: parsed.summary,
380
+ payload: parsed.payload,
381
+ idempotencyKey: idempotencyKey ?? null,
382
+ approvalRequestId: approvalRequest?.id ?? null,
383
+ outcome: {},
384
+ createdAt: now,
385
+ updatedAt: now,
386
+ completedAt: requiresApproval ? null : now
387
+ });
388
+ const outcome = requiresApproval ? {} : executeAgentAction(actionSeed, context);
389
+ getDatabase()
390
+ .prepare(`INSERT INTO agent_actions (
391
+ id, agent_id, token_id, action_type, risk_level, status, title, summary, payload_json, idempotency_key,
392
+ approval_request_id, outcome_json, created_at, updated_at, completed_at
393
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
394
+ .run(actionId, actionSeed.agentId, actionSeed.tokenId, actionSeed.actionType, actionSeed.riskLevel, actionSeed.status, actionSeed.title, actionSeed.summary, JSON.stringify(actionSeed.payload), actionSeed.idempotencyKey, actionSeed.approvalRequestId, JSON.stringify(outcome), actionSeed.createdAt, actionSeed.updatedAt, actionSeed.completedAt);
395
+ recordActivityEvent({
396
+ entityType: "agent_action",
397
+ entityId: actionId,
398
+ eventType: requiresApproval ? "agent_action_requested" : "agent_action_executed",
399
+ title: requiresApproval ? `Approval requested: ${parsed.title}` : `Agent action executed: ${parsed.title}`,
400
+ description: parsed.summary,
401
+ actor: context.actor ?? null,
402
+ source: context.source,
403
+ metadata: {
404
+ actionType: parsed.actionType,
405
+ riskLevel: parsed.riskLevel
406
+ }
407
+ });
408
+ const actionRow = getDatabase()
409
+ .prepare(`SELECT
410
+ id, agent_id, token_id, action_type, risk_level, status, title, summary, payload_json, idempotency_key,
411
+ approval_request_id, outcome_json, created_at, updated_at, completed_at
412
+ FROM agent_actions
413
+ WHERE id = ?`)
414
+ .get(actionId);
415
+ return { action: mapAction(actionRow), approvalRequest };
416
+ }
417
+ export function listAgentActions(agentId) {
418
+ const rows = getDatabase()
419
+ .prepare(`SELECT
420
+ id, agent_id, token_id, action_type, risk_level, status, title, summary, payload_json, idempotency_key,
421
+ approval_request_id, outcome_json, created_at, updated_at, completed_at
422
+ FROM agent_actions
423
+ WHERE agent_id = ?
424
+ ORDER BY created_at DESC`)
425
+ .all(agentId);
426
+ return rows.map(mapAction);
427
+ }
428
+ export function approveApprovalRequest(approvalId, note, actor) {
429
+ const row = getApprovalRequestRow(approvalId);
430
+ if (!row || row.status !== "pending") {
431
+ return row ? mapApproval(row) : undefined;
432
+ }
433
+ const approvedAt = new Date().toISOString();
434
+ getDatabase()
435
+ .prepare(`UPDATE approval_requests
436
+ SET status = 'approved', approved_by = ?, approved_at = ?, resolution_note = ?, updated_at = ?
437
+ WHERE id = ?`)
438
+ .run(actor, approvedAt, note, approvedAt, approvalId);
439
+ const actionRow = getDatabase()
440
+ .prepare(`SELECT
441
+ id, agent_id, token_id, action_type, risk_level, status, title, summary, payload_json, idempotency_key,
442
+ approval_request_id, outcome_json, created_at, updated_at, completed_at
443
+ FROM agent_actions
444
+ WHERE approval_request_id = ?`)
445
+ .get(approvalId);
446
+ if (actionRow) {
447
+ const action = mapAction(actionRow);
448
+ const outcome = executeAgentAction(action, {
449
+ actor,
450
+ source: "ui",
451
+ token: null
452
+ });
453
+ getDatabase()
454
+ .prepare(`UPDATE agent_actions
455
+ SET status = 'executed', outcome_json = ?, updated_at = ?, completed_at = ?
456
+ WHERE id = ?`)
457
+ .run(JSON.stringify(outcome), approvedAt, approvedAt, action.id);
458
+ getDatabase().prepare(`UPDATE approval_requests SET status = 'executed', updated_at = ? WHERE id = ?`).run(approvedAt, approvalId);
459
+ }
460
+ recordActivityEvent({
461
+ entityType: "approval_request",
462
+ entityId: approvalId,
463
+ eventType: "approval_request_approved",
464
+ title: `Approval request approved`,
465
+ description: note || "A pending agent action was approved.",
466
+ actor,
467
+ source: "ui",
468
+ metadata: {}
469
+ });
470
+ return mapApproval(getApprovalRequestRow(approvalId));
471
+ }
472
+ export function rejectApprovalRequest(approvalId, note, actor) {
473
+ const row = getApprovalRequestRow(approvalId);
474
+ if (!row || row.status !== "pending") {
475
+ return row ? mapApproval(row) : undefined;
476
+ }
477
+ const rejectedAt = new Date().toISOString();
478
+ getDatabase()
479
+ .prepare(`UPDATE approval_requests
480
+ SET status = 'rejected', rejected_by = ?, rejected_at = ?, resolution_note = ?, updated_at = ?
481
+ WHERE id = ?`)
482
+ .run(actor, rejectedAt, note, rejectedAt, approvalId);
483
+ getDatabase()
484
+ .prepare(`UPDATE agent_actions SET status = 'rejected', updated_at = ? WHERE approval_request_id = ?`)
485
+ .run(rejectedAt, approvalId);
486
+ recordActivityEvent({
487
+ entityType: "approval_request",
488
+ entityId: approvalId,
489
+ eventType: "approval_request_rejected",
490
+ title: `Approval request rejected`,
491
+ description: note || "A pending agent action was rejected.",
492
+ actor,
493
+ source: "ui",
494
+ metadata: {}
495
+ });
496
+ return mapApproval(getApprovalRequestRow(approvalId));
497
+ }
@@ -0,0 +1,176 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase } from "../db.js";
3
+ import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
4
+ import { recordActivityEvent } from "./activity-events.js";
5
+ import { recordEventLog } from "./event-log.js";
6
+ import { commentSchema, commentsListQuerySchema, createCommentSchema, updateCommentSchema } from "../psyche-types.js";
7
+ import { activityEntityTypeSchema } from "../types.js";
8
+ function mapComment(row) {
9
+ return commentSchema.parse({
10
+ id: row.id,
11
+ entityType: row.entity_type,
12
+ entityId: row.entity_id,
13
+ anchorKey: row.anchor_key,
14
+ body: row.body,
15
+ author: row.author,
16
+ source: row.source,
17
+ createdAt: row.created_at,
18
+ updatedAt: row.updated_at
19
+ });
20
+ }
21
+ function getCommentRow(commentId) {
22
+ return getDatabase()
23
+ .prepare(`SELECT id, entity_type, entity_id, anchor_key, body, author, source, created_at, updated_at
24
+ FROM entity_comments
25
+ WHERE id = ?`)
26
+ .get(commentId);
27
+ }
28
+ export function getCommentById(commentId) {
29
+ if (isEntityDeleted("comment", commentId)) {
30
+ return undefined;
31
+ }
32
+ const row = getCommentRow(commentId);
33
+ return row ? mapComment(row) : undefined;
34
+ }
35
+ export function listComments(query = {}) {
36
+ const parsed = commentsListQuerySchema.parse(query);
37
+ const whereClauses = [];
38
+ const params = [];
39
+ if (parsed.entityType) {
40
+ whereClauses.push("entity_type = ?");
41
+ params.push(parsed.entityType);
42
+ }
43
+ if (parsed.entityId) {
44
+ whereClauses.push("entity_id = ?");
45
+ params.push(parsed.entityId);
46
+ }
47
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
48
+ const limitSql = parsed.limit ? "LIMIT ?" : "";
49
+ if (parsed.limit) {
50
+ params.push(parsed.limit);
51
+ }
52
+ const rows = getDatabase()
53
+ .prepare(`SELECT id, entity_type, entity_id, anchor_key, body, author, source, created_at, updated_at
54
+ FROM entity_comments
55
+ ${whereSql}
56
+ ORDER BY created_at DESC
57
+ ${limitSql}`)
58
+ .all(...params);
59
+ return filterDeletedEntities("comment", rows.map(mapComment));
60
+ }
61
+ export function createComment(input, context) {
62
+ const parsed = createCommentSchema.parse(input);
63
+ const now = new Date().toISOString();
64
+ const comment = commentSchema.parse({
65
+ id: `cmt_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
66
+ entityType: parsed.entityType,
67
+ entityId: parsed.entityId,
68
+ anchorKey: parsed.anchorKey,
69
+ body: parsed.body,
70
+ author: parsed.author ?? context.actor ?? null,
71
+ source: context.source,
72
+ createdAt: now,
73
+ updatedAt: now
74
+ });
75
+ const entityType = activityEntityTypeSchema.parse(comment.entityType);
76
+ getDatabase()
77
+ .prepare(`INSERT INTO entity_comments (id, entity_type, entity_id, anchor_key, body, author, source, created_at, updated_at)
78
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
79
+ .run(comment.id, comment.entityType, comment.entityId, comment.anchorKey, comment.body, comment.author, comment.source, comment.createdAt, comment.updatedAt);
80
+ recordActivityEvent({
81
+ entityType,
82
+ entityId: comment.entityId,
83
+ eventType: "comment.created",
84
+ title: "Comment added",
85
+ description: comment.body,
86
+ actor: comment.author ?? null,
87
+ source: context.source,
88
+ metadata: {
89
+ commentId: comment.id,
90
+ anchorKey: comment.anchorKey ?? ""
91
+ }
92
+ });
93
+ recordEventLog({
94
+ eventKind: "comment.created",
95
+ entityType,
96
+ entityId: comment.entityId,
97
+ actor: comment.author ?? null,
98
+ source: context.source,
99
+ metadata: {
100
+ commentId: comment.id,
101
+ anchorKey: comment.anchorKey ?? ""
102
+ }
103
+ });
104
+ return comment;
105
+ }
106
+ export function updateComment(commentId, input, context) {
107
+ const existing = getCommentRow(commentId);
108
+ if (!existing) {
109
+ return undefined;
110
+ }
111
+ const patch = updateCommentSchema.parse(input);
112
+ const updatedAt = new Date().toISOString();
113
+ const author = patch.author ?? existing.author ?? context.actor ?? null;
114
+ const entityType = activityEntityTypeSchema.parse(existing.entity_type);
115
+ getDatabase()
116
+ .prepare(`UPDATE entity_comments
117
+ SET body = ?, author = ?, updated_at = ?
118
+ WHERE id = ?`)
119
+ .run(patch.body, author, updatedAt, commentId);
120
+ recordActivityEvent({
121
+ entityType,
122
+ entityId: existing.entity_id,
123
+ eventType: "comment.updated",
124
+ title: "Comment updated",
125
+ description: patch.body,
126
+ actor: author,
127
+ source: context.source,
128
+ metadata: {
129
+ commentId
130
+ }
131
+ });
132
+ recordEventLog({
133
+ eventKind: "comment.updated",
134
+ entityType,
135
+ entityId: existing.entity_id,
136
+ actor: author,
137
+ source: context.source,
138
+ metadata: {
139
+ commentId
140
+ }
141
+ });
142
+ return mapComment(getCommentRow(commentId));
143
+ }
144
+ export function deleteComment(commentId, context) {
145
+ const existing = getCommentRow(commentId);
146
+ if (!existing) {
147
+ return undefined;
148
+ }
149
+ const entityType = activityEntityTypeSchema.parse(existing.entity_type);
150
+ getDatabase()
151
+ .prepare(`DELETE FROM entity_comments WHERE id = ?`)
152
+ .run(commentId);
153
+ recordActivityEvent({
154
+ entityType,
155
+ entityId: existing.entity_id,
156
+ eventType: "comment.deleted",
157
+ title: "Comment deleted",
158
+ description: existing.body,
159
+ actor: context.actor ?? existing.author ?? null,
160
+ source: context.source,
161
+ metadata: {
162
+ commentId
163
+ }
164
+ });
165
+ recordEventLog({
166
+ eventKind: "comment.deleted",
167
+ entityType,
168
+ entityId: existing.entity_id,
169
+ actor: context.actor ?? existing.author ?? null,
170
+ source: context.source,
171
+ metadata: {
172
+ commentId
173
+ }
174
+ });
175
+ return mapComment(existing);
176
+ }