@stelis/say-ur-intent 0.0.0 → 0.0.2

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 (234) hide show
  1. package/README.md +4 -39
  2. package/dist/adapters/adapterLifecycleValidators.js +7 -0
  3. package/dist/adapters/adapterPromptSurfaces.js +71 -0
  4. package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +175 -0
  5. package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
  6. package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
  7. package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
  8. package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
  9. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +269 -0
  10. package/dist/adapters/flowx/flowxSwapHumanReviewProducer.js +176 -0
  11. package/dist/adapters/flowx/flowxSwapIntent.js +79 -0
  12. package/dist/adapters/flowx/flowxSwapQuotePolicy.js +104 -0
  13. package/dist/adapters/flowx/flowxSwapReviewEvidence.js +468 -0
  14. package/dist/adapters/flowx/flowxSwapReviewLifecycle.js +85 -0
  15. package/dist/adapters/flowx/flowxSwapTransactionMaterialProducer.js +362 -0
  16. package/dist/adapters/intentPlanFactories.js +59 -0
  17. package/dist/adapters/reviewAdapters.js +81 -0
  18. package/dist/core/action/adapterLifecycleValidation.js +12 -0
  19. package/dist/core/action/forbiddenFields.js +43 -0
  20. package/dist/core/action/humanReadableReviewEvidence.js +203 -0
  21. package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
  22. package/dist/core/action/ptbVisualizationProducer.js +66 -0
  23. package/dist/core/action/reviewCheckResults.js +6 -0
  24. package/dist/core/action/reviewStateValidation.js +11 -0
  25. package/dist/core/action/reviewTimeSimulationEvidence.js +471 -0
  26. package/dist/core/action/schemas.js +529 -0
  27. package/dist/core/action/signableAdapterContract.js +993 -0
  28. package/dist/core/action/swapHumanReadableReviewProjection.js +124 -0
  29. package/dist/core/action/swapQuotePolicyEvidence.js +278 -0
  30. package/dist/core/action/transactionObjectOwnershipEvidence.js +247 -0
  31. package/dist/core/action/transactionObjectOwnershipProducer.js +329 -0
  32. package/dist/core/action/types.js +35 -0
  33. package/dist/core/action/walletReviewContractAssembler.js +282 -0
  34. package/dist/core/activity/activityStore.js +15 -0
  35. package/dist/core/activity/localDataService.js +258 -0
  36. package/dist/core/activity/localDataTypes.js +11 -0
  37. package/dist/core/activity/localDataValidation.js +396 -0
  38. package/dist/core/activity/schemaVersion.js +1 -0
  39. package/dist/core/activity/sqliteActivityStore.js +820 -0
  40. package/dist/core/activity/sqliteActivityStoreRows.js +430 -0
  41. package/dist/core/activity/sqliteActivityStoreSchema.js +258 -0
  42. package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
  43. package/dist/core/activity/suiFunctionTarget.js +43 -0
  44. package/dist/core/activity/transactionActivityAccountEffects.js +189 -0
  45. package/dist/core/activity/transactionActivityAnalysis.js +295 -0
  46. package/dist/core/activity/transactionActivityClassifier.js +306 -0
  47. package/dist/core/activity/transactionActivityDetails.js +229 -0
  48. package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
  49. package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
  50. package/dist/core/activity/transactionActivityService.js +379 -0
  51. package/dist/core/activity/transactionActivityTypes.js +18 -0
  52. package/dist/core/eventlog/sink.js +35 -0
  53. package/dist/core/evidence/settlementFamilies.js +87 -0
  54. package/dist/core/evidence/userAnswerUse.js +1 -0
  55. package/dist/core/numeric/rawU64.js +63 -0
  56. package/dist/core/preferences/preferencesStore.js +26 -0
  57. package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
  58. package/dist/core/proposal/externalProposalReview.js +347 -0
  59. package/dist/core/proposal/schemas.js +208 -0
  60. package/dist/core/proposal/types.js +35 -0
  61. package/dist/core/read/amounts.js +14 -0
  62. package/dist/core/read/coinMetadata.js +60 -0
  63. package/dist/core/read/deepbookRawQuoteClient.js +86 -0
  64. package/dist/core/read/deepbookReadHelpers.js +265 -0
  65. package/dist/core/read/deepbookRegistry.js +133 -0
  66. package/dist/core/read/flowxQuoteClient.js +117 -0
  67. package/dist/core/read/flowxReadHelpers.js +145 -0
  68. package/dist/core/read/flowxRegistry.js +174 -0
  69. package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
  70. package/dist/core/read/readResponseGuidance.js +451 -0
  71. package/dist/core/read/readService.js +1164 -0
  72. package/dist/core/read/readServiceTypes.js +59 -0
  73. package/dist/core/read/settlementParityFormatting.js +82 -0
  74. package/dist/core/read/walletReadHelpers.js +99 -0
  75. package/dist/core/review/reviewChecks.js +54 -0
  76. package/dist/core/review/reviewComputation.js +38 -0
  77. package/dist/core/review/reviewComputationResult.js +87 -0
  78. package/dist/core/session/localSession.js +31 -0
  79. package/dist/core/session/privateReviewArtifacts.js +73 -0
  80. package/dist/core/session/sessionErrors.js +9 -0
  81. package/dist/core/session/sessionStore.js +821 -0
  82. package/dist/core/session/settingsSession.js +1 -0
  83. package/dist/core/session/settingsSessions.js +43 -0
  84. package/dist/core/session/status.js +86 -0
  85. package/dist/core/session/transactionMaterialStore.js +205 -0
  86. package/dist/core/session/wait.js +102 -0
  87. package/dist/core/session/walletIdentity.js +103 -0
  88. package/dist/core/session/walletIdentitySessions.js +189 -0
  89. package/dist/core/suiAddress.js +18 -0
  90. package/dist/core/suiEndpoint.js +72 -0
  91. package/dist/mcp/activeAccountResponse.js +24 -0
  92. package/dist/mcp/prompts.js +146 -0
  93. package/dist/mcp/registerTool.js +19 -0
  94. package/dist/mcp/resources.js +72 -0
  95. package/dist/mcp/responseGuidance.js +381 -0
  96. package/dist/mcp/result.js +17 -0
  97. package/dist/mcp/schemas.js +8 -0
  98. package/dist/mcp/server.js +30 -0
  99. package/dist/mcp/serverInfo.js +123 -0
  100. package/dist/mcp/toolErrors.js +105 -0
  101. package/dist/mcp/toolNames.js +50 -0
  102. package/dist/mcp/tools/account/index.js +44 -0
  103. package/dist/mcp/tools/action/prepareSuiActionReview.js +120 -0
  104. package/dist/mcp/tools/read/commonSchemas.js +43 -0
  105. package/dist/mcp/tools/read/deepbookReadTools.js +453 -0
  106. package/dist/mcp/tools/read/flowxReadTools.js +135 -0
  107. package/dist/mcp/tools/read/index.js +16 -0
  108. package/dist/mcp/tools/read/readToolHelpers.js +68 -0
  109. package/dist/mcp/tools/read/reviewActivityTools.js +176 -0
  110. package/dist/mcp/tools/read/serverStatusTools.js +103 -0
  111. package/dist/mcp/tools/read/transactionActivityOutput.js +300 -0
  112. package/dist/mcp/tools/read/transactionActivityTools.js +544 -0
  113. package/dist/mcp/tools/read/walletReadTools.js +733 -0
  114. package/dist/mcp/tools/session/executionResultTools.js +92 -0
  115. package/dist/mcp/tools/session/index.js +8 -0
  116. package/dist/mcp/tools/session/shared.js +79 -0
  117. package/dist/mcp/tools/session/statusTools.js +134 -0
  118. package/dist/mcp/tools/session/walletIdentityTools.js +119 -0
  119. package/dist/mcp/tools/settings/index.js +64 -0
  120. package/dist/review-app/analysis.css +1 -0
  121. package/dist/review-app/analysis.js +1 -0
  122. package/dist/review-app/arc-BjIacwQm.js +1 -0
  123. package/dist/review-app/architecture-U656AL7Q-aSB9x1OK.js +1 -0
  124. package/dist/review-app/architectureDiagram-VXUJARFQ-C5W6re2I.js +36 -0
  125. package/dist/review-app/array-BmXUUrU6.js +1 -0
  126. package/dist/review-app/blockDiagram-VD42YOAC-20MLNcUm.js +122 -0
  127. package/dist/review-app/c4Diagram-YG6GDRKO-BZXRrcck.js +10 -0
  128. package/dist/review-app/channel-lk2p_CUu.js +1 -0
  129. package/dist/review-app/chunk-4BX2VUAB-BPITOdjX.js +1 -0
  130. package/dist/review-app/chunk-55IACEB6-Dz-pyw5k.js +1 -0
  131. package/dist/review-app/chunk-76Q3JFCE-cK_X1P_l.js +1 -0
  132. package/dist/review-app/chunk-ABZYJK2D-Dt4W53JI.js +81 -0
  133. package/dist/review-app/chunk-ATLVNIR6-fZHLXURb.js +1 -0
  134. package/dist/review-app/chunk-B4BG7PRW-BbgcjusC.js +165 -0
  135. package/dist/review-app/chunk-BJD4TVEz.js +1 -0
  136. package/dist/review-app/chunk-CVBHYZKI-CViawAKX.js +1 -0
  137. package/dist/review-app/chunk-DI55MBZ5-C5aoul-d.js +220 -0
  138. package/dist/review-app/chunk-FMBD7UC4-Chxmw62A.js +15 -0
  139. package/dist/review-app/chunk-FPAJGGOC-DDHjQ09H.js +80 -0
  140. package/dist/review-app/chunk-FWNWRKHM-CVVQUptk.js +1 -0
  141. package/dist/review-app/chunk-HN2XXSSU-yzNpjaSZ.js +1 -0
  142. package/dist/review-app/chunk-JA3XYJ7Z-C5ZJdU01.js +70 -0
  143. package/dist/review-app/chunk-JZLCHNYA-BBST4Cnk.js +54 -0
  144. package/dist/review-app/chunk-LBM3YZW2-CdwAPuHr.js +1 -0
  145. package/dist/review-app/chunk-LHMN2FUI-BtB5uDcp.js +1 -0
  146. package/dist/review-app/chunk-O7ZBX7Z2-pxdK4Sa3.js +1 -0
  147. package/dist/review-app/chunk-QN33PNHL-CbVv3uGK.js +1 -0
  148. package/dist/review-app/chunk-QXUST7PY-DKM2-t2c.js +7 -0
  149. package/dist/review-app/chunk-QZHKN3VN-C5ni2pN_.js +1 -0
  150. package/dist/review-app/chunk-S3R3BYOJ-BWvOhDs0.js +2 -0
  151. package/dist/review-app/chunk-S6J4BHB3-D9Fk0YeD.js +1 -0
  152. package/dist/review-app/chunk-T53DSG4Q-C1qEyzyV.js +1 -0
  153. package/dist/review-app/chunk-TZMSLE5B-B--7eU69.js +1 -0
  154. package/dist/review-app/classDiagram-2ON5EDUG-DlL1m2bp.js +1 -0
  155. package/dist/review-app/classDiagram-v2-WZHVMYZB-FXRskT1j.js +1 -0
  156. package/dist/review-app/clone-BZZb7gpZ.js +1 -0
  157. package/dist/review-app/cose-bilkent-S5V4N54A-CRIb8XEO.js +1 -0
  158. package/dist/review-app/cytoscape.esm-C7jYqDP5.js +321 -0
  159. package/dist/review-app/dagre-6UL2VRFP-FNCAXbdE.js +4 -0
  160. package/dist/review-app/dagre-Be46QtUd.js +1 -0
  161. package/dist/review-app/defaultLocale-BaWNtAUL.js +1 -0
  162. package/dist/review-app/diagram-PSM6KHXK-ylLWjiNM.js +24 -0
  163. package/dist/review-app/diagram-QEK2KX5R-BCDcESxs.js +43 -0
  164. package/dist/review-app/diagram-S2PKOQOG-Vdrc-vrO.js +24 -0
  165. package/dist/review-app/dist-WPc74x_f.js +1 -0
  166. package/dist/review-app/erDiagram-Q2GNP2WA-E5ZsUbDF.js +60 -0
  167. package/dist/review-app/flatten-DHf9IeNI.js +1 -0
  168. package/dist/review-app/flowDiagram-NV44I4VS-DBSQuj6x.js +162 -0
  169. package/dist/review-app/ganttDiagram-LVOFAZNH-CKUOsqwl.js +267 -0
  170. package/dist/review-app/gitGraph-F6HP7TQM-DsAD6qK1.js +1 -0
  171. package/dist/review-app/gitGraphDiagram-NY62KEGX-BCeIMWdl.js +65 -0
  172. package/dist/review-app/graphlib-CiX5CXxR.js +1 -0
  173. package/dist/review-app/http-DMvwuuFk.js +1 -0
  174. package/dist/review-app/identity-DY8PXc6t.js +1 -0
  175. package/dist/review-app/info-NVLQJR56-Dlx1nZic.js +1 -0
  176. package/dist/review-app/infoDiagram-F6ZHWCRC-CAuANIrz.js +2 -0
  177. package/dist/review-app/init-BvqephKz.js +1 -0
  178. package/dist/review-app/journeyDiagram-XKPGCS4Q-C-Z9phnx.js +139 -0
  179. package/dist/review-app/kanban-definition-3W4ZIXB7-DufgZABq.js +89 -0
  180. package/dist/review-app/katex-B-Z-NXXN.js +257 -0
  181. package/dist/review-app/line-DiIv3Jgw.js +1 -0
  182. package/dist/review-app/linear-Cv-UPvo1.js +1 -0
  183. package/dist/review-app/math-kmyYrkHL.js +1 -0
  184. package/dist/review-app/mermaid-parser.core-DkwUYTPl.js +4 -0
  185. package/dist/review-app/mindmap-definition-VGOIOE7T-TM_CqdmV.js +68 -0
  186. package/dist/review-app/ordinal-BliTlkoG.js +1 -0
  187. package/dist/review-app/packet-BFZMPI3H-DqbnU92v.js +1 -0
  188. package/dist/review-app/path-AEo9W6mQ.js +1 -0
  189. package/dist/review-app/pie-7BOR55EZ-LJzaLkgr.js +1 -0
  190. package/dist/review-app/pieDiagram-ADFJNKIX-BAs8OfRS.js +30 -0
  191. package/dist/review-app/quadrantDiagram-AYHSOK5B-CyUDZP5S.js +7 -0
  192. package/dist/review-app/radar-NHE76QYJ-DBpHc8_Y.js +1 -0
  193. package/dist/review-app/reduce-B-HuPpdd.js +1 -0
  194. package/dist/review-app/requirementDiagram-UZGBJVZJ-BEHix78P.js +64 -0
  195. package/dist/review-app/review.css +1 -0
  196. package/dist/review-app/review.js +43 -0
  197. package/dist/review-app/sankeyDiagram-TZEHDZUN-B2bKbmsm.js +10 -0
  198. package/dist/review-app/sequenceDiagram-WL72ISMW-DVLOORFJ.js +145 -0
  199. package/dist/review-app/settings.css +1 -0
  200. package/dist/review-app/settings.js +1 -0
  201. package/dist/review-app/src-Buml7cM5.js +1 -0
  202. package/dist/review-app/stateDiagram-FKZM4ZOC-sFGGp2kV.js +1 -0
  203. package/dist/review-app/stateDiagram-v2-4FDKWEC3-BHfCF4dX.js +1 -0
  204. package/dist/review-app/timeline-definition-IT6M3QCI-BESnBijC.js +61 -0
  205. package/dist/review-app/treemap-KMMF4GRG-wnVLBDeQ.js +1 -0
  206. package/dist/review-app/walletStatus-CcojOdGy.js +7 -0
  207. package/dist/review-app/xychartDiagram-PRI3JC2R-BGWVfCx4.js +7 -0
  208. package/dist/review-server/assets.js +48 -0
  209. package/dist/review-server/html.js +66 -0
  210. package/dist/review-server/http.js +47 -0
  211. package/dist/review-server/middleware/hostOrigin.js +48 -0
  212. package/dist/review-server/middleware/reviewToken.js +7 -0
  213. package/dist/review-server/reviewServerPolicy.js +10 -0
  214. package/dist/review-server/server.js +568 -0
  215. package/dist/review-server/settingsApi.js +182 -0
  216. package/dist/review-server/walletIdentityResponse.js +13 -0
  217. package/dist/runtime/config.js +103 -0
  218. package/dist/runtime/localSettingsService.js +198 -0
  219. package/dist/runtime/logger.js +50 -0
  220. package/dist/runtime/reviewServerAcquire.js +128 -0
  221. package/dist/runtime/smokeMainnetRead.js +529 -0
  222. package/dist/runtime/smokeMainnetReadAssertions.js +308 -0
  223. package/dist/runtime/start.js +295 -0
  224. package/dist/runtime/suiEndpoint.js +97 -0
  225. package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
  226. package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
  227. package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
  228. package/docs/AGENT_BEHAVIOR.md +1 -1
  229. package/docs/AGENT_DEVELOPMENT_POLICY.md +20 -0
  230. package/docs/FRONTEND_POLICY.md +4 -3
  231. package/docs/MCP_SETUP.md +59 -7
  232. package/docs/MCP_TOOLS.md +1 -1
  233. package/docs/SDK_API.md +5 -1
  234. package/package.json +3 -2
@@ -0,0 +1,820 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, resolve } from "node:path";
4
+ import Database from "better-sqlite3";
5
+ import { assertNoForbiddenMcpFields } from "../action/forbiddenFields.js";
6
+ import { parseLifecycleValidatedReviewState } from "../action/reviewStateValidation.js";
7
+ import { actionPlanSchema, executionResultSchema } from "../action/schemas.js";
8
+ import { parseSuiAddress } from "../suiAddress.js";
9
+ import { SqlitePreferencesRepository } from "../preferences/sqlitePreferencesRepository.js";
10
+ import { SqliteLocalDataService } from "./localDataService.js";
11
+ import { ActivityStoreReadError, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS, REVIEW_ACTIVITY_LOW_SAMPLE_THRESHOLD } from "./activityStore.js";
12
+ import { configureDatabase, initializeDatabase } from "./sqliteActivityStoreSchema.js";
13
+ import { ActivityStoreError } from "./sqliteActivityStoreTypes.js";
14
+ import { EXTERNAL_ACTIVITY_RELATIONSHIPS, EXTERNAL_ACTIVITY_STATUSES, INTERNAL_SESSION_STATUSES, REVIEW_STATE_STATUSES, REVIEW_TRANSITION_EVENTS, asAccountSource, asInternalSessionStatus, asReviewTransitionEvent, asString, assertDateRange, canAdvanceReviewExecution, coinMetadataCacheRecordFromRow, countMap, emptyExternalActivitySummaryStats, emptyReviewFunnelSummary, externalActivityScanFromRow, externalActivitySummaryResult, externalActivityTransactionFromRow, extractRequestedIntent, isSameReviewExecution, normalizeExternalActivityLimit, normalizeListLimit, nullableSeconds, parseActionPlanEvidence, parseEvidenceJson, parseIsoTimestamp, parseOptionalIsoTimestamp, reasonForReviewState, reviewActivityListResult, reviewActivityRowFromStorage, reviewFunnelResult, reviewSessionWhere, serializeExternalActivityTransactionDetail, serializeJson, serializeOptionalJson } from "./sqliteActivityStoreRows.js";
15
+ export { ActivityStoreError };
16
+ const ACTIVE_ACCOUNT_SINGLETON_ID = 1;
17
+ export const DATA_DIR_ENV = "SAY_UR_INTENT_DATA_DIR";
18
+ export const ACTIVITY_DATABASE_FILENAME = "say-ur-intent.sqlite";
19
+ export class SqliteActivityStore {
20
+ db;
21
+ validateAdapterLifecycle;
22
+ constructor(options) {
23
+ this.validateAdapterLifecycle = options.validateAdapterLifecycle;
24
+ try {
25
+ mkdirSync(dirname(options.databasePath), { recursive: true });
26
+ }
27
+ catch {
28
+ throw new ActivityStoreError(`Could not create the local activity data directory. Check directory permissions or set ${DATA_DIR_ENV}.`);
29
+ }
30
+ this.db = new Database(options.databasePath);
31
+ try {
32
+ configureDatabase(this.db);
33
+ initializeDatabase(this.db);
34
+ }
35
+ catch (error) {
36
+ this.db.close();
37
+ throw error;
38
+ }
39
+ }
40
+ async upsertAccount(address, source, now = new Date()) {
41
+ return this.upsertAccountSync(address, source, now.toISOString());
42
+ }
43
+ async getKnownAccount(address) {
44
+ const normalized = parseSuiAddress(address);
45
+ if (!normalized) {
46
+ throw new ActivityStoreReadError("input_invalid", "Invalid account address", { field: "account" });
47
+ }
48
+ return this.getAccountByAddressSync(normalized);
49
+ }
50
+ async setActiveAccount(address, source, now = new Date(), wallet) {
51
+ const timestamp = now.toISOString();
52
+ return this.db.transaction(() => {
53
+ const account = this.upsertAccountSync(address, source, timestamp);
54
+ this.db
55
+ .prepare(`INSERT INTO active_account_context (id, account_id, source, set_at, wallet_name, wallet_id)
56
+ VALUES (?, ?, ?, ?, ?, ?)
57
+ ON CONFLICT(id) DO UPDATE SET
58
+ account_id = excluded.account_id,
59
+ source = excluded.source,
60
+ set_at = excluded.set_at,
61
+ wallet_name = excluded.wallet_name,
62
+ wallet_id = excluded.wallet_id`)
63
+ .run(ACTIVE_ACCOUNT_SINGLETON_ID, account.id, source, timestamp, wallet?.name ?? null, wallet?.id ?? null);
64
+ return {
65
+ accountId: account.id,
66
+ address: account.address,
67
+ source,
68
+ setAt: timestamp,
69
+ ...(wallet?.name ? { walletName: wallet.name } : {}),
70
+ ...(wallet?.id ? { walletId: wallet.id } : {})
71
+ };
72
+ })();
73
+ }
74
+ async getActiveAccount() {
75
+ return this.getActiveAccountSync();
76
+ }
77
+ async clearActiveAccount(now = new Date()) {
78
+ this.db
79
+ .prepare(`INSERT INTO active_account_context (id, account_id, source, set_at)
80
+ VALUES (?, NULL, 'cleared', ?)
81
+ ON CONFLICT(id) DO UPDATE SET
82
+ account_id = NULL,
83
+ source = 'cleared',
84
+ set_at = excluded.set_at`)
85
+ .run(ACTIVE_ACCOUNT_SINGLETON_ID, now.toISOString());
86
+ }
87
+ async recordReviewSession(input) {
88
+ const plan = parseActionPlanEvidence(input.plan);
89
+ const planJson = serializeJson(plan);
90
+ const intentJson = serializeOptionalJson(extractRequestedIntent(plan));
91
+ this.db
92
+ .transaction(() => {
93
+ this.db
94
+ .prepare(`INSERT INTO review_sessions
95
+ (id, plan_id, action_kind, adapter_id, protocol, current_status,
96
+ plan_json, intent_json, created_at, updated_at)
97
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
98
+ .run(input.reviewSessionId, plan.id, plan.actionKind, plan.adapterId, plan.protocol, input.currentStatus, planJson, intentJson, input.createdAt, input.createdAt);
99
+ this.insertReviewTransition({
100
+ reviewSessionId: input.reviewSessionId,
101
+ event: "created",
102
+ toStatus: input.currentStatus,
103
+ transitionedAt: input.createdAt
104
+ });
105
+ })();
106
+ }
107
+ async recordReviewTransition(input) {
108
+ this.db
109
+ .transaction(() => {
110
+ const accountId = input.account
111
+ ? this.upsertAccountSync(input.account, "review_execution", input.transitionedAt).id
112
+ : null;
113
+ if (accountId !== null) {
114
+ this.assertReviewSessionAccount(input.reviewSessionId, accountId);
115
+ }
116
+ this.insertReviewTransition({ ...input, accountId });
117
+ this.db
118
+ .prepare(`UPDATE review_sessions
119
+ SET current_status = ?, account_id = COALESCE(account_id, ?), updated_at = ?
120
+ WHERE id = ?`)
121
+ .run(input.toStatus, accountId, input.transitionedAt, input.reviewSessionId);
122
+ })();
123
+ }
124
+ async recordReviewStateSnapshot(input) {
125
+ const parsedState = parseLifecycleValidatedReviewState(input.state, this.validateAdapterLifecycle);
126
+ const stateJson = serializeJson(parsedState);
127
+ this.db
128
+ .transaction(() => {
129
+ const account = this.upsertAccountSync(parsedState.account, "review_execution", input.recordedAt);
130
+ this.assertReviewSessionAccount(input.reviewSessionId, account.id);
131
+ this.db
132
+ .prepare(`INSERT INTO review_state_snapshots
133
+ (review_session_id, plan_id, account_id, status, blocked_reason, refresh_reason,
134
+ state_json, updated_at, recorded_at)
135
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
136
+ .run(input.reviewSessionId, parsedState.planId, account.id, parsedState.status, "blockedReason" in parsedState ? parsedState.blockedReason : null, "refreshReason" in parsedState ? parsedState.refreshReason : null, stateJson, parsedState.updatedAt, input.recordedAt);
137
+ this.insertReviewTransition({
138
+ reviewSessionId: input.reviewSessionId,
139
+ event: "state_computed",
140
+ fromStatus: input.fromStatus,
141
+ toStatus: parsedState.status,
142
+ accountId: account.id,
143
+ reason: reasonForReviewState(parsedState),
144
+ transitionedAt: input.recordedAt
145
+ });
146
+ this.db
147
+ .prepare(`UPDATE review_sessions
148
+ SET current_status = ?, account_id = ?, updated_at = ?
149
+ WHERE id = ?`)
150
+ .run(parsedState.status, account.id, input.recordedAt, input.reviewSessionId);
151
+ })();
152
+ }
153
+ async recordReviewExecution(input) {
154
+ const resultJson = serializeJson(input.result);
155
+ this.db
156
+ .transaction(() => {
157
+ const account = this.upsertAccountSync(input.account, "review_execution", input.recordedAt);
158
+ this.assertReviewSessionAccount(input.reviewSessionId, account.id);
159
+ const existing = this.getReviewExecutionStorageRow(input.reviewSessionId);
160
+ if (existing) {
161
+ if (isSameReviewExecution(existing, account.id, input)) {
162
+ return;
163
+ }
164
+ if (!canAdvanceReviewExecution(existing, account.id, input)) {
165
+ throw new ActivityStoreError(`Conflicting review execution evidence: ${input.reviewSessionId}`);
166
+ }
167
+ }
168
+ this.db
169
+ .prepare(`INSERT INTO review_executions
170
+ (review_session_id, plan_id, account_id, status, tx_digest, explorer_url,
171
+ failure_reason, result_json, recorded_at, updated_at)
172
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
173
+ ON CONFLICT(review_session_id) DO UPDATE SET
174
+ plan_id = excluded.plan_id,
175
+ account_id = excluded.account_id,
176
+ status = excluded.status,
177
+ tx_digest = excluded.tx_digest,
178
+ explorer_url = excluded.explorer_url,
179
+ failure_reason = excluded.failure_reason,
180
+ result_json = excluded.result_json,
181
+ updated_at = excluded.updated_at`)
182
+ .run(input.reviewSessionId, input.planId, account.id, input.status, input.txDigest ?? null, input.explorerUrl ?? null, input.failureReason ?? null, resultJson, input.recordedAt, input.recordedAt);
183
+ this.insertReviewTransition({
184
+ reviewSessionId: input.reviewSessionId,
185
+ event: "result_recorded",
186
+ fromStatus: input.fromStatus,
187
+ toStatus: input.status,
188
+ accountId: account.id,
189
+ reason: input.failureReason,
190
+ transitionedAt: input.recordedAt
191
+ });
192
+ this.db
193
+ .prepare(`UPDATE review_sessions
194
+ SET current_status = ?, account_id = ?, updated_at = ?
195
+ WHERE id = ?`)
196
+ .run(input.status, account.id, input.recordedAt, input.reviewSessionId);
197
+ })();
198
+ const recorded = await this.getReviewExecution(input.reviewSessionId);
199
+ if (!recorded) {
200
+ throw new ActivityStoreError(`Review execution was not recorded: ${input.reviewSessionId}`);
201
+ }
202
+ return recorded;
203
+ }
204
+ getReviewExecutionStorageRow(reviewSessionId) {
205
+ return this.db
206
+ .prepare(`SELECT review_session_id, plan_id, account_id, status, tx_digest, explorer_url, failure_reason
207
+ FROM review_executions
208
+ WHERE review_session_id = ?`)
209
+ .get(reviewSessionId);
210
+ }
211
+ async getReviewExecution(reviewSessionId) {
212
+ const row = this.db
213
+ .prepare(`SELECT r.review_session_id, r.plan_id, r.account_id, a.sui_address AS account,
214
+ r.status, r.tx_digest, r.explorer_url, r.failure_reason, r.recorded_at, r.updated_at
215
+ FROM review_executions r
216
+ JOIN accounts a ON a.id = r.account_id
217
+ WHERE r.review_session_id = ?`)
218
+ .get(reviewSessionId);
219
+ return row
220
+ ? {
221
+ reviewSessionId: asString(row.review_session_id),
222
+ planId: asString(row.plan_id),
223
+ accountId: row.account_id,
224
+ account: asString(row.account),
225
+ status: asString(row.status),
226
+ txDigest: row.tx_digest === null ? undefined : asString(row.tx_digest),
227
+ explorerUrl: row.explorer_url === null ? undefined : asString(row.explorer_url),
228
+ failureReason: row.failure_reason === null ? undefined : asString(row.failure_reason),
229
+ recordedAt: asString(row.recorded_at),
230
+ updatedAt: asString(row.updated_at)
231
+ }
232
+ : undefined;
233
+ }
234
+ async listReviewActivity(filter) {
235
+ const from = parseOptionalIsoTimestamp(filter.from, "from");
236
+ const to = parseOptionalIsoTimestamp(filter.to, "to");
237
+ assertDateRange(from, to);
238
+ const limit = normalizeListLimit(filter.limit);
239
+ const scope = this.resolveReviewActivityScope(filter);
240
+ if (scope.accountId === undefined) {
241
+ return reviewActivityListResult(scope, from, to, [], false, 0);
242
+ }
243
+ const { whereSql, params } = reviewSessionWhere(scope.accountId, from, to, filter.status);
244
+ const totalRow = this.db
245
+ .prepare(`SELECT COUNT(*) AS count FROM review_sessions rs ${whereSql}`)
246
+ .get(...params);
247
+ const rows = this.db
248
+ .prepare(`SELECT rs.id AS review_session_id, rs.plan_id, rs.action_kind, rs.adapter_id, rs.protocol,
249
+ rs.current_status, a.sui_address AS account, rs.created_at, rs.updated_at,
250
+ re.status AS execution_status, re.tx_digest,
251
+ (SELECT COUNT(*) FROM review_state_snapshots s WHERE s.review_session_id = rs.id) AS snapshot_count,
252
+ (SELECT COUNT(*) FROM review_status_transitions t WHERE t.review_session_id = rs.id) AS transition_count
253
+ FROM review_sessions rs
254
+ JOIN accounts a ON a.id = rs.account_id
255
+ LEFT JOIN review_executions re ON re.review_session_id = rs.id
256
+ ${whereSql}
257
+ ORDER BY rs.created_at DESC, rs.id DESC
258
+ LIMIT ?`)
259
+ .all(...params, limit + 1);
260
+ const truncated = rows.length > limit;
261
+ const activities = rows.slice(0, limit).map(reviewActivityRowFromStorage);
262
+ return reviewActivityListResult(scope, from, to, activities, truncated, totalRow.count);
263
+ }
264
+ async summarizeReviewFunnel(filter) {
265
+ const from = parseOptionalIsoTimestamp(filter.from, "from");
266
+ const to = parseOptionalIsoTimestamp(filter.to, "to");
267
+ assertDateRange(from, to);
268
+ const scope = this.resolveReviewActivityScope(filter);
269
+ if (scope.accountId === undefined) {
270
+ return reviewFunnelResult(scope, from, to, emptyReviewFunnelSummary(), 0);
271
+ }
272
+ const { whereSql, params } = reviewSessionWhere(scope.accountId, from, to);
273
+ const total = this.db
274
+ .prepare(`SELECT COUNT(*) AS count FROM review_sessions rs ${whereSql}`)
275
+ .get(...params).count;
276
+ const eventCounts = this.db
277
+ .prepare(`SELECT t.event AS key, COUNT(DISTINCT t.review_session_id) AS count
278
+ FROM review_status_transitions t
279
+ JOIN review_sessions rs ON rs.id = t.review_session_id
280
+ ${whereSql}
281
+ GROUP BY t.event`)
282
+ .all(...params);
283
+ const currentStatusCounts = this.db
284
+ .prepare(`SELECT rs.current_status AS key, COUNT(*) AS count
285
+ FROM review_sessions rs
286
+ ${whereSql}
287
+ GROUP BY rs.current_status`)
288
+ .all(...params);
289
+ const reachedStateCounts = this.db
290
+ .prepare(`SELECT t.to_status AS key, COUNT(DISTINCT t.review_session_id) AS count
291
+ FROM review_status_transitions t
292
+ JOIN review_sessions rs ON rs.id = t.review_session_id
293
+ ${whereSql}
294
+ AND t.to_status IN ('ready_for_wallet_review', 'blocked', 'refresh_required')
295
+ GROUP BY t.to_status`)
296
+ .all(...params);
297
+ const signedPending = this.db
298
+ .prepare(`SELECT COUNT(DISTINCT t.review_session_id) AS count
299
+ FROM review_status_transitions t
300
+ JOIN review_sessions rs ON rs.id = t.review_session_id
301
+ ${whereSql}
302
+ AND t.event = 'result_recorded'
303
+ AND t.to_status = 'signed_pending_result'`)
304
+ .get(...params).count;
305
+ const expiredBeforeResult = this.db
306
+ .prepare(`SELECT COUNT(*) AS count
307
+ FROM review_sessions rs
308
+ LEFT JOIN review_executions re ON re.review_session_id = rs.id
309
+ ${whereSql}
310
+ AND rs.current_status = 'expired'
311
+ AND re.review_session_id IS NULL`)
312
+ .get(...params).count;
313
+ const timing = this.db
314
+ .prepare(`WITH scoped AS (
315
+ SELECT rs.id, rs.created_at
316
+ FROM review_sessions rs
317
+ ${whereSql}
318
+ ),
319
+ signed AS (
320
+ SELECT review_session_id, MIN(transitioned_at) AS signed_at
321
+ FROM review_status_transitions
322
+ WHERE event = 'result_recorded' AND to_status = 'signed_pending_result'
323
+ GROUP BY review_session_id
324
+ ),
325
+ opened AS (
326
+ SELECT review_session_id, MIN(transitioned_at) AS opened_at
327
+ FROM review_status_transitions
328
+ WHERE event = 'opened'
329
+ GROUP BY review_session_id
330
+ )
331
+ SELECT
332
+ AVG((julianday(signed.signed_at) - julianday(scoped.created_at)) * 86400.0) AS avg_created_to_signed,
333
+ AVG(
334
+ CASE
335
+ WHEN opened.opened_at IS NULL THEN NULL
336
+ ELSE (julianday(signed.signed_at) - julianday(opened.opened_at)) * 86400.0
337
+ END
338
+ ) AS avg_opened_to_signed
339
+ FROM scoped
340
+ JOIN signed ON signed.review_session_id = scoped.id
341
+ LEFT JOIN opened ON opened.review_session_id = scoped.id`)
342
+ .get(...params);
343
+ const currentStatus = countMap(INTERNAL_SESSION_STATUSES, currentStatusCounts);
344
+ const reachedStates = countMap(REVIEW_STATE_STATUSES, reachedStateCounts);
345
+ const events = countMap(REVIEW_TRANSITION_EVENTS, eventCounts);
346
+ const summary = {
347
+ total,
348
+ opened: events.opened,
349
+ walletConnected: events.wallet_connected,
350
+ stateComputed: events.state_computed,
351
+ currentStatusCounts: currentStatus,
352
+ everReachedReviewStateCounts: reachedStates,
353
+ signedPending,
354
+ success: currentStatus.success,
355
+ failure: currentStatus.failure,
356
+ expiredBeforeResult,
357
+ avgCreatedToSignedSeconds: nullableSeconds(timing.avg_created_to_signed),
358
+ avgOpenedToSignedSeconds: nullableSeconds(timing.avg_opened_to_signed)
359
+ };
360
+ return reviewFunnelResult(scope, from, to, summary, total);
361
+ }
362
+ async getReviewSessionDetail(input) {
363
+ const scope = this.resolveReviewActivityScope({ account: input.account });
364
+ if (scope.accountId === undefined) {
365
+ throw new ActivityStoreReadError("session_not_found", "Review session not found", {
366
+ reviewSessionId: input.reviewSessionId
367
+ });
368
+ }
369
+ const row = this.db
370
+ .prepare(`SELECT rs.id AS review_session_id, rs.plan_id, rs.action_kind, rs.adapter_id, rs.protocol,
371
+ rs.current_status, a.sui_address AS account, rs.created_at, rs.updated_at,
372
+ rs.plan_json, rs.intent_json,
373
+ re.status AS execution_status, re.tx_digest, re.explorer_url, re.failure_reason,
374
+ re.recorded_at AS execution_recorded_at, re.updated_at AS execution_updated_at,
375
+ re.result_json
376
+ FROM review_sessions rs
377
+ JOIN accounts a ON a.id = rs.account_id
378
+ LEFT JOIN review_executions re ON re.review_session_id = rs.id
379
+ WHERE rs.id = ? AND rs.account_id = ?`)
380
+ .get(input.reviewSessionId, scope.accountId);
381
+ if (!row) {
382
+ throw new ActivityStoreReadError("session_not_found", "Review session not found", {
383
+ reviewSessionId: input.reviewSessionId
384
+ });
385
+ }
386
+ const snapshotRows = this.db
387
+ .prepare(`SELECT s.id, s.plan_id, a.sui_address AS account, s.status, s.blocked_reason, s.refresh_reason,
388
+ s.state_json, s.updated_at, s.recorded_at
389
+ FROM review_state_snapshots s
390
+ JOIN accounts a ON a.id = s.account_id
391
+ WHERE s.review_session_id = ? AND s.account_id = ?
392
+ ORDER BY s.recorded_at ASC, s.id ASC
393
+ LIMIT ?`)
394
+ .all(input.reviewSessionId, scope.accountId, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS + 1);
395
+ const transitionRows = this.db
396
+ .prepare(`SELECT t.id, t.event, t.from_status, t.to_status, a.sui_address AS account,
397
+ t.reason, t.transitioned_at
398
+ FROM review_status_transitions t
399
+ LEFT JOIN accounts a ON a.id = t.account_id
400
+ WHERE t.review_session_id = ? AND (t.account_id IS NULL OR t.account_id = ?)
401
+ ORDER BY t.transitioned_at ASC, t.id ASC
402
+ LIMIT ?`)
403
+ .all(input.reviewSessionId, scope.accountId, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS + 1);
404
+ const recordCount = this.db
405
+ .prepare(`SELECT COUNT(*) AS count FROM review_sessions rs WHERE rs.account_id = ?`)
406
+ .get(scope.accountId).count;
407
+ const snapshotsTruncated = snapshotRows.length > REVIEW_ACTIVITY_DETAIL_MAX_ITEMS;
408
+ const transitionsTruncated = transitionRows.length > REVIEW_ACTIVITY_DETAIL_MAX_ITEMS;
409
+ const planJson = parseEvidenceJson(row.plan_json, input.reviewSessionId, "plan_json", actionPlanSchema);
410
+ const intentJson = row.intent_json === null
411
+ ? undefined
412
+ : parseEvidenceJson(row.intent_json, input.reviewSessionId, "intent_json");
413
+ const execution = row.execution_status === null
414
+ ? undefined
415
+ : {
416
+ reviewSessionId: asString(row.review_session_id),
417
+ planId: asString(row.plan_id),
418
+ accountId: scope.accountId,
419
+ account: asString(row.account),
420
+ status: asString(row.execution_status),
421
+ txDigest: row.tx_digest === null ? undefined : asString(row.tx_digest),
422
+ explorerUrl: row.explorer_url === null ? undefined : asString(row.explorer_url),
423
+ failureReason: row.failure_reason === null ? undefined : asString(row.failure_reason),
424
+ recordedAt: asString(row.execution_recorded_at),
425
+ updatedAt: asString(row.execution_updated_at),
426
+ resultJson: parseEvidenceJson(row.result_json, input.reviewSessionId, "result_json", executionResultSchema)
427
+ };
428
+ return {
429
+ dataScope: {
430
+ account: scope.account,
431
+ recordCount
432
+ },
433
+ accountSource: scope.accountSource,
434
+ lowSampleWarning: recordCount < REVIEW_ACTIVITY_LOW_SAMPLE_THRESHOLD,
435
+ lowSampleThreshold: REVIEW_ACTIVITY_LOW_SAMPLE_THRESHOLD,
436
+ session: {
437
+ reviewSessionId: asString(row.review_session_id),
438
+ planId: asString(row.plan_id),
439
+ actionKind: asString(row.action_kind),
440
+ adapterId: asString(row.adapter_id),
441
+ protocol: asString(row.protocol),
442
+ currentStatus: asInternalSessionStatus(row.current_status),
443
+ account: asString(row.account),
444
+ createdAt: asString(row.created_at),
445
+ updatedAt: asString(row.updated_at)
446
+ },
447
+ planJson,
448
+ intentJson,
449
+ stateSnapshots: snapshotRows.slice(0, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS).map((snapshot) => ({
450
+ id: snapshot.id,
451
+ planId: asString(snapshot.plan_id),
452
+ account: asString(snapshot.account),
453
+ status: asString(snapshot.status),
454
+ blockedReason: snapshot.blocked_reason === null ? undefined : asString(snapshot.blocked_reason),
455
+ refreshReason: snapshot.refresh_reason === null ? undefined : asString(snapshot.refresh_reason),
456
+ stateJson: this.parseReviewStateEvidenceJson(snapshot.state_json, input.reviewSessionId, "state_json"),
457
+ updatedAt: asString(snapshot.updated_at),
458
+ recordedAt: asString(snapshot.recorded_at)
459
+ })),
460
+ transitions: transitionRows.slice(0, REVIEW_ACTIVITY_DETAIL_MAX_ITEMS).map((transitionRow) => ({
461
+ id: transitionRow.id,
462
+ event: asReviewTransitionEvent(transitionRow.event),
463
+ fromStatus: transitionRow.from_status === null ? undefined : asString(transitionRow.from_status),
464
+ toStatus: asString(transitionRow.to_status),
465
+ isNoOp: transitionRow.from_status !== null && asString(transitionRow.from_status) === asString(transitionRow.to_status),
466
+ account: transitionRow.account === null ? undefined : asString(transitionRow.account),
467
+ reason: transitionRow.reason === null ? undefined : asString(transitionRow.reason),
468
+ transitionedAt: asString(transitionRow.transitioned_at)
469
+ })),
470
+ execution,
471
+ truncated: {
472
+ activities: false,
473
+ snapshots: snapshotsTruncated,
474
+ transitions: transitionsTruncated
475
+ }
476
+ };
477
+ }
478
+ async recordExternalActivityScan(input) {
479
+ try {
480
+ assertNoForbiddenMcpFields(input);
481
+ }
482
+ catch {
483
+ throw new ActivityStoreReadError("input_invalid", "External activity scan contains forbidden fields", {
484
+ reason: "forbidden_field"
485
+ });
486
+ }
487
+ const account = await this.getKnownAccount(input.account);
488
+ if (!account) {
489
+ throw new ActivityStoreReadError("input_invalid", "External activity scan account is not a known wallet", {
490
+ reason: "account_not_known"
491
+ });
492
+ }
493
+ const fetchedAt = parseIsoTimestamp(input.fetchedAt, "fetchedAt");
494
+ const fromTimestamp = parseOptionalIsoTimestamp(input.fromTimestamp, "from");
495
+ const toTimestamp = parseOptionalIsoTimestamp(input.toTimestamp, "to");
496
+ assertDateRange(fromTimestamp, toTimestamp);
497
+ const record = this.db.transaction(() => {
498
+ this.db
499
+ .prepare(`INSERT INTO external_activity_scans
500
+ (scan_id, kind, account_id, relationship, input_digest, from_checkpoint, to_checkpoint,
501
+ from_timestamp, to_timestamp, limit_count, request_cursor, response_cursor, endpoint_host,
502
+ chain_identifier, fetched_at, stored_count, skipped_count, has_more, window_complete,
503
+ incomplete_reason)
504
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
505
+ .run(input.scanId, input.kind, account.id, input.relationship, input.inputDigest ?? null, input.fromCheckpoint ?? null, input.toCheckpoint ?? null, fromTimestamp ?? null, toTimestamp ?? null, input.limit, input.requestCursor ?? null, input.responseCursor ?? null, input.endpointHost, input.chainIdentifier, fetchedAt, 0, input.transactions.length, input.hasMore ? 1 : 0, input.windowComplete === null ? null : input.windowComplete ? 1 : 0, input.incompleteReason ?? null);
506
+ let storedCount = 0;
507
+ const skippedCount = input.skippedCount ?? 0;
508
+ const upsert = this.db.prepare(`INSERT INTO external_activity_transactions
509
+ (account_id, digest, relationship, checkpoint, timestamp, status, known_sender_account_id,
510
+ first_scan_id, last_scan_id, first_fetched_at, last_fetched_at, detail_json)
511
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
512
+ ON CONFLICT(account_id, digest, relationship) DO UPDATE SET
513
+ checkpoint = COALESCE(excluded.checkpoint, external_activity_transactions.checkpoint),
514
+ timestamp = COALESCE(excluded.timestamp, external_activity_transactions.timestamp),
515
+ status = excluded.status,
516
+ known_sender_account_id = COALESCE(excluded.known_sender_account_id, external_activity_transactions.known_sender_account_id),
517
+ last_scan_id = excluded.last_scan_id,
518
+ last_fetched_at = excluded.last_fetched_at,
519
+ detail_json = COALESCE(excluded.detail_json, external_activity_transactions.detail_json)`);
520
+ for (const transaction of input.transactions) {
521
+ if (transaction.knownSenderAccountId !== undefined && !this.accountIdExists(transaction.knownSenderAccountId)) {
522
+ throw new ActivityStoreReadError("input_invalid", "External activity sender account is not known", {
523
+ reason: "sender_account_not_known"
524
+ });
525
+ }
526
+ upsert.run(account.id, transaction.digest, transaction.relationship, transaction.checkpoint ?? null, transaction.timestamp ? parseIsoTimestamp(transaction.timestamp, "transaction.timestamp") : null, transaction.status, transaction.knownSenderAccountId ?? null, input.scanId, input.scanId, fetchedAt, fetchedAt, transaction.details === undefined
527
+ ? null
528
+ : serializeExternalActivityTransactionDetail(transaction.details, account.address));
529
+ storedCount += 1;
530
+ }
531
+ this.db
532
+ .prepare(`UPDATE external_activity_scans
533
+ SET stored_count = ?, skipped_count = ?
534
+ WHERE scan_id = ?`)
535
+ .run(storedCount, skippedCount + input.transactions.length - storedCount, input.scanId);
536
+ return this.externalActivityScanById(input.scanId);
537
+ })();
538
+ if (!record) {
539
+ throw new ActivityStoreReadError("internal_error", "External activity scan was not recorded", {
540
+ scanId: input.scanId
541
+ });
542
+ }
543
+ return record;
544
+ }
545
+ async summarizeExternalActivity(filter) {
546
+ const from = parseOptionalIsoTimestamp(filter.from, "from");
547
+ const to = parseOptionalIsoTimestamp(filter.to, "to");
548
+ assertDateRange(from, to);
549
+ const limit = normalizeExternalActivityLimit(filter.limit);
550
+ const scope = this.resolveReviewActivityScope(filter);
551
+ if (scope.accountId === undefined) {
552
+ return externalActivitySummaryResult(scope, from, to, [], false, emptyExternalActivitySummaryStats());
553
+ }
554
+ const where = ["eat.account_id = ?"];
555
+ const params = [scope.accountId];
556
+ if (from !== undefined) {
557
+ where.push("eat.timestamp >= ?");
558
+ params.push(from);
559
+ }
560
+ if (to !== undefined) {
561
+ where.push("eat.timestamp <= ?");
562
+ params.push(to);
563
+ }
564
+ const whereSql = `WHERE ${where.join(" AND ")}`;
565
+ const total = this.db
566
+ .prepare(`SELECT COUNT(*) AS count FROM external_activity_transactions eat ${whereSql}`)
567
+ .get(...params).count;
568
+ const statusCounts = countMap(EXTERNAL_ACTIVITY_STATUSES, this.db
569
+ .prepare(`SELECT eat.status AS key, COUNT(*) AS count FROM external_activity_transactions eat ${whereSql} GROUP BY eat.status`)
570
+ .all(...params));
571
+ const relationshipCounts = countMap(EXTERNAL_ACTIVITY_RELATIONSHIPS, this.db
572
+ .prepare(`SELECT eat.relationship AS key, COUNT(*) AS count FROM external_activity_transactions eat ${whereSql} GROUP BY eat.relationship`)
573
+ .all(...params));
574
+ const timestampRow = this.db
575
+ .prepare(`SELECT MIN(eat.timestamp) AS earliest_timestamp, MAX(eat.timestamp) AS latest_timestamp
576
+ FROM external_activity_transactions eat
577
+ ${whereSql}`)
578
+ .get(...params);
579
+ const rows = this.db
580
+ .prepare(`SELECT eat.account_id, a.sui_address AS account, eat.digest, eat.relationship,
581
+ eat.checkpoint, eat.timestamp, eat.status, eat.known_sender_account_id,
582
+ eat.first_scan_id, eat.last_scan_id, eat.first_fetched_at, eat.last_fetched_at,
583
+ last_scan.incomplete_reason AS last_scan_incomplete_reason,
584
+ eat.detail_json
585
+ FROM external_activity_transactions eat
586
+ JOIN accounts a ON a.id = eat.account_id
587
+ LEFT JOIN external_activity_scans last_scan ON last_scan.scan_id = eat.last_scan_id
588
+ ${whereSql}
589
+ ORDER BY
590
+ CASE WHEN eat.checkpoint IS NULL THEN 0 ELSE 1 END DESC,
591
+ CAST(eat.checkpoint AS INTEGER) DESC,
592
+ COALESCE(eat.timestamp, '') DESC,
593
+ eat.digest DESC
594
+ LIMIT ?`)
595
+ .all(...params, limit + 1);
596
+ return externalActivitySummaryResult(scope, from, to, rows.slice(0, limit).map(externalActivityTransactionFromRow), rows.length > limit, {
597
+ transactionCount: total,
598
+ statusCounts,
599
+ relationshipCounts,
600
+ earliestTimestamp: timestampRow.earliest_timestamp ?? undefined,
601
+ latestTimestamp: timestampRow.latest_timestamp ?? undefined
602
+ });
603
+ }
604
+ close() {
605
+ this.db.close();
606
+ }
607
+ createPreferencesRepository() {
608
+ return new SqlitePreferencesRepository(this.db);
609
+ }
610
+ createLocalDataService(options) {
611
+ return new SqliteLocalDataService(this.db, options, this.validateAdapterLifecycle);
612
+ }
613
+ createCoinMetadataCache() {
614
+ return new SqliteCoinMetadataCache(this.db);
615
+ }
616
+ upsertAccountSync(address, source, timestamp) {
617
+ const normalized = parseSuiAddress(address);
618
+ if (!normalized) {
619
+ throw new ActivityStoreError("Invalid Sui account address");
620
+ }
621
+ this.db
622
+ .prepare(`INSERT INTO accounts (sui_address, first_seen_at, last_used_at, first_source, last_source)
623
+ VALUES (?, ?, ?, ?, ?)
624
+ ON CONFLICT(sui_address) DO UPDATE SET
625
+ last_used_at = excluded.last_used_at,
626
+ last_source = excluded.last_source`)
627
+ .run(normalized, timestamp, timestamp, source, source);
628
+ const row = this.db
629
+ .prepare(`SELECT id, sui_address, first_seen_at, last_used_at, first_source, last_source
630
+ FROM accounts
631
+ WHERE sui_address = ?`)
632
+ .get(normalized);
633
+ if (!row) {
634
+ throw new ActivityStoreError(`Account was not recorded: ${normalized}`);
635
+ }
636
+ return {
637
+ id: row.id,
638
+ address: asString(row.sui_address),
639
+ firstSeenAt: asString(row.first_seen_at),
640
+ lastUsedAt: asString(row.last_used_at),
641
+ firstSource: asAccountSource(row.first_source),
642
+ lastSource: asAccountSource(row.last_source)
643
+ };
644
+ }
645
+ parseReviewStateEvidenceJson(value, reviewSessionId, evidenceField) {
646
+ const parsed = parseEvidenceJson(value, reviewSessionId, evidenceField);
647
+ try {
648
+ return parseLifecycleValidatedReviewState(parsed, this.validateAdapterLifecycle);
649
+ }
650
+ catch {
651
+ throw new ActivityStoreReadError("internal_error", "Malformed activity JSON evidence", {
652
+ reviewSessionId,
653
+ evidenceField
654
+ });
655
+ }
656
+ }
657
+ getAccountByAddressSync(address) {
658
+ const row = this.db
659
+ .prepare(`SELECT id, sui_address, first_seen_at, last_used_at, first_source, last_source
660
+ FROM accounts
661
+ WHERE sui_address = ?`)
662
+ .get(address);
663
+ return row
664
+ ? {
665
+ id: row.id,
666
+ address: asString(row.sui_address),
667
+ firstSeenAt: asString(row.first_seen_at),
668
+ lastUsedAt: asString(row.last_used_at),
669
+ firstSource: asAccountSource(row.first_source),
670
+ lastSource: asAccountSource(row.last_source)
671
+ }
672
+ : undefined;
673
+ }
674
+ accountIdExists(accountId) {
675
+ const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
676
+ return row !== undefined;
677
+ }
678
+ externalActivityScanById(scanId) {
679
+ const row = this.db
680
+ .prepare(`SELECT eas.scan_id, eas.kind, eas.account_id, a.sui_address AS account, eas.relationship,
681
+ eas.input_digest, eas.from_checkpoint, eas.to_checkpoint, eas.from_timestamp,
682
+ eas.to_timestamp, eas.limit_count, eas.request_cursor, eas.response_cursor,
683
+ eas.endpoint_host, eas.chain_identifier, eas.fetched_at, eas.stored_count,
684
+ eas.skipped_count, eas.has_more, eas.window_complete, eas.incomplete_reason
685
+ FROM external_activity_scans eas
686
+ JOIN accounts a ON a.id = eas.account_id
687
+ WHERE eas.scan_id = ?`)
688
+ .get(scanId);
689
+ return row ? externalActivityScanFromRow(row) : undefined;
690
+ }
691
+ insertReviewTransition(input) {
692
+ this.db
693
+ .prepare(`INSERT INTO review_status_transitions
694
+ (review_session_id, event, from_status, to_status, account_id, reason, transitioned_at)
695
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
696
+ .run(input.reviewSessionId, input.event, input.fromStatus ?? null, input.toStatus, input.accountId ?? null, input.reason ?? null, input.transitionedAt);
697
+ }
698
+ resolveReviewActivityScope(filter) {
699
+ if (filter.account) {
700
+ const normalized = parseSuiAddress(filter.account);
701
+ if (!normalized) {
702
+ throw new ActivityStoreReadError("input_invalid", "Invalid account filter", { field: "account" });
703
+ }
704
+ const row = this.db
705
+ .prepare("SELECT id FROM accounts WHERE sui_address = ?")
706
+ .get(normalized);
707
+ return {
708
+ account: normalized,
709
+ accountId: row?.id,
710
+ accountSource: "explicit_filter"
711
+ };
712
+ }
713
+ const active = this.getActiveAccountSync();
714
+ if (!active) {
715
+ throw new ActivityStoreReadError("active_account_not_set", "Active account read context is not set", {
716
+ action: "connect_wallet_identity"
717
+ });
718
+ }
719
+ return {
720
+ account: active.address,
721
+ accountId: active.accountId,
722
+ accountSource: "active_account_context"
723
+ };
724
+ }
725
+ getActiveAccountSync() {
726
+ const row = this.db
727
+ .prepare(`SELECT a.id AS account_id, a.sui_address AS address, c.source AS source, c.set_at AS set_at,
728
+ c.wallet_name AS wallet_name, c.wallet_id AS wallet_id
729
+ FROM active_account_context c
730
+ JOIN accounts a ON a.id = c.account_id
731
+ WHERE c.id = ? AND c.account_id IS NOT NULL`)
732
+ .get(ACTIVE_ACCOUNT_SINGLETON_ID);
733
+ return row
734
+ ? {
735
+ accountId: row.account_id,
736
+ address: asString(row.address),
737
+ source: "wallet_identity",
738
+ setAt: asString(row.set_at),
739
+ ...(row.wallet_name ? { walletName: row.wallet_name } : {}),
740
+ ...(row.wallet_id ? { walletId: row.wallet_id } : {})
741
+ }
742
+ : undefined;
743
+ }
744
+ assertReviewSessionAccount(reviewSessionId, accountId) {
745
+ const row = this.db
746
+ .prepare("SELECT account_id FROM review_sessions WHERE id = ?")
747
+ .get(reviewSessionId);
748
+ if (!row) {
749
+ throw new ActivityStoreError(`Review session not found: ${reviewSessionId}`);
750
+ }
751
+ if (row.account_id !== null && row.account_id !== accountId) {
752
+ throw new ActivityStoreError(`Review session already belongs to a different account: ${reviewSessionId}`);
753
+ }
754
+ }
755
+ }
756
+ class SqliteCoinMetadataCache {
757
+ db;
758
+ constructor(db) {
759
+ this.db = db;
760
+ }
761
+ async getCoinMetadata(input) {
762
+ const row = this.db
763
+ .prepare(`SELECT coin_type, chain_identifier, decimals, symbol, name, fetched_at, expires_at
764
+ FROM coin_metadata_cache
765
+ WHERE coin_type = ? AND chain_identifier = ?`)
766
+ .get(input.coinType, input.chainIdentifier);
767
+ if (!row) {
768
+ return { status: "miss" };
769
+ }
770
+ const record = coinMetadataCacheRecordFromRow(row);
771
+ return record.expiresAt > input.now.toISOString()
772
+ ? { status: "hit", record }
773
+ : { status: "expired", record };
774
+ }
775
+ async setCoinMetadata(record) {
776
+ this.db
777
+ .prepare(`INSERT INTO coin_metadata_cache
778
+ (coin_type, chain_identifier, decimals, symbol, name, fetched_at, expires_at)
779
+ VALUES (?, ?, ?, ?, ?, ?, ?)
780
+ ON CONFLICT(coin_type, chain_identifier) DO UPDATE SET
781
+ decimals = excluded.decimals,
782
+ symbol = excluded.symbol,
783
+ name = excluded.name,
784
+ fetched_at = excluded.fetched_at,
785
+ expires_at = excluded.expires_at`)
786
+ .run(record.coinType, record.chainIdentifier, record.decimals, record.symbol, record.name, record.fetchedAt, record.expiresAt);
787
+ }
788
+ }
789
+ export function resolveActivityDatabasePath(env = process.env, platform = process.platform) {
790
+ const configured = env[DATA_DIR_ENV];
791
+ const dataDir = configured && configured.trim() ? configured : defaultDataDir(env, platform);
792
+ if (dataDir.includes("\0")) {
793
+ throw new ActivityStoreError(`${DATA_DIR_ENV} must not contain null bytes`);
794
+ }
795
+ return resolve(dataDir, ACTIVITY_DATABASE_FILENAME);
796
+ }
797
+ export function assertSqliteEngineAvailable() {
798
+ const db = new Database(":memory:");
799
+ try {
800
+ db.exec("CREATE TABLE engine_check (id INTEGER PRIMARY KEY)");
801
+ db.prepare("INSERT INTO engine_check (id) VALUES (?)").run(1);
802
+ const row = db.prepare("SELECT id FROM engine_check").get();
803
+ if (row?.id !== 1) {
804
+ throw new ActivityStoreError("better-sqlite3 smoke query failed");
805
+ }
806
+ }
807
+ finally {
808
+ db.close();
809
+ }
810
+ }
811
+ function defaultDataDir(env, platform) {
812
+ const home = homedir();
813
+ if (platform === "darwin") {
814
+ return resolve(home, "Library", "Application Support", "say-ur-intent");
815
+ }
816
+ if (platform === "win32") {
817
+ return resolve(env.APPDATA ?? resolve(home, "AppData", "Roaming"), "say-ur-intent");
818
+ }
819
+ return resolve(env.XDG_DATA_HOME ?? resolve(home, ".local", "share"), "say-ur-intent");
820
+ }