@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.
- package/README.md +4 -39
- package/dist/adapters/adapterLifecycleValidators.js +7 -0
- package/dist/adapters/adapterPromptSurfaces.js +71 -0
- package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +175 -0
- package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
- package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
- package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
- package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +269 -0
- package/dist/adapters/flowx/flowxSwapHumanReviewProducer.js +176 -0
- package/dist/adapters/flowx/flowxSwapIntent.js +79 -0
- package/dist/adapters/flowx/flowxSwapQuotePolicy.js +104 -0
- package/dist/adapters/flowx/flowxSwapReviewEvidence.js +468 -0
- package/dist/adapters/flowx/flowxSwapReviewLifecycle.js +85 -0
- package/dist/adapters/flowx/flowxSwapTransactionMaterialProducer.js +362 -0
- package/dist/adapters/intentPlanFactories.js +59 -0
- package/dist/adapters/reviewAdapters.js +81 -0
- package/dist/core/action/adapterLifecycleValidation.js +12 -0
- package/dist/core/action/forbiddenFields.js +43 -0
- package/dist/core/action/humanReadableReviewEvidence.js +203 -0
- package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
- package/dist/core/action/ptbVisualizationProducer.js +66 -0
- package/dist/core/action/reviewCheckResults.js +6 -0
- package/dist/core/action/reviewStateValidation.js +11 -0
- package/dist/core/action/reviewTimeSimulationEvidence.js +471 -0
- package/dist/core/action/schemas.js +529 -0
- package/dist/core/action/signableAdapterContract.js +993 -0
- package/dist/core/action/swapHumanReadableReviewProjection.js +124 -0
- package/dist/core/action/swapQuotePolicyEvidence.js +278 -0
- package/dist/core/action/transactionObjectOwnershipEvidence.js +247 -0
- package/dist/core/action/transactionObjectOwnershipProducer.js +329 -0
- package/dist/core/action/types.js +35 -0
- package/dist/core/action/walletReviewContractAssembler.js +282 -0
- package/dist/core/activity/activityStore.js +15 -0
- package/dist/core/activity/localDataService.js +258 -0
- package/dist/core/activity/localDataTypes.js +11 -0
- package/dist/core/activity/localDataValidation.js +396 -0
- package/dist/core/activity/schemaVersion.js +1 -0
- package/dist/core/activity/sqliteActivityStore.js +820 -0
- package/dist/core/activity/sqliteActivityStoreRows.js +430 -0
- package/dist/core/activity/sqliteActivityStoreSchema.js +258 -0
- package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
- package/dist/core/activity/suiFunctionTarget.js +43 -0
- package/dist/core/activity/transactionActivityAccountEffects.js +189 -0
- package/dist/core/activity/transactionActivityAnalysis.js +295 -0
- package/dist/core/activity/transactionActivityClassifier.js +306 -0
- package/dist/core/activity/transactionActivityDetails.js +229 -0
- package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
- package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
- package/dist/core/activity/transactionActivityService.js +379 -0
- package/dist/core/activity/transactionActivityTypes.js +18 -0
- package/dist/core/eventlog/sink.js +35 -0
- package/dist/core/evidence/settlementFamilies.js +87 -0
- package/dist/core/evidence/userAnswerUse.js +1 -0
- package/dist/core/numeric/rawU64.js +63 -0
- package/dist/core/preferences/preferencesStore.js +26 -0
- package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
- package/dist/core/proposal/externalProposalReview.js +347 -0
- package/dist/core/proposal/schemas.js +208 -0
- package/dist/core/proposal/types.js +35 -0
- package/dist/core/read/amounts.js +14 -0
- package/dist/core/read/coinMetadata.js +60 -0
- package/dist/core/read/deepbookRawQuoteClient.js +86 -0
- package/dist/core/read/deepbookReadHelpers.js +265 -0
- package/dist/core/read/deepbookRegistry.js +133 -0
- package/dist/core/read/flowxQuoteClient.js +117 -0
- package/dist/core/read/flowxReadHelpers.js +145 -0
- package/dist/core/read/flowxRegistry.js +174 -0
- package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
- package/dist/core/read/readResponseGuidance.js +451 -0
- package/dist/core/read/readService.js +1164 -0
- package/dist/core/read/readServiceTypes.js +59 -0
- package/dist/core/read/settlementParityFormatting.js +82 -0
- package/dist/core/read/walletReadHelpers.js +99 -0
- package/dist/core/review/reviewChecks.js +54 -0
- package/dist/core/review/reviewComputation.js +38 -0
- package/dist/core/review/reviewComputationResult.js +87 -0
- package/dist/core/session/localSession.js +31 -0
- package/dist/core/session/privateReviewArtifacts.js +73 -0
- package/dist/core/session/sessionErrors.js +9 -0
- package/dist/core/session/sessionStore.js +821 -0
- package/dist/core/session/settingsSession.js +1 -0
- package/dist/core/session/settingsSessions.js +43 -0
- package/dist/core/session/status.js +86 -0
- package/dist/core/session/transactionMaterialStore.js +205 -0
- package/dist/core/session/wait.js +102 -0
- package/dist/core/session/walletIdentity.js +103 -0
- package/dist/core/session/walletIdentitySessions.js +189 -0
- package/dist/core/suiAddress.js +18 -0
- package/dist/core/suiEndpoint.js +72 -0
- package/dist/mcp/activeAccountResponse.js +24 -0
- package/dist/mcp/prompts.js +146 -0
- package/dist/mcp/registerTool.js +19 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/responseGuidance.js +381 -0
- package/dist/mcp/result.js +17 -0
- package/dist/mcp/schemas.js +8 -0
- package/dist/mcp/server.js +30 -0
- package/dist/mcp/serverInfo.js +123 -0
- package/dist/mcp/toolErrors.js +105 -0
- package/dist/mcp/toolNames.js +50 -0
- package/dist/mcp/tools/account/index.js +44 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +120 -0
- package/dist/mcp/tools/read/commonSchemas.js +43 -0
- package/dist/mcp/tools/read/deepbookReadTools.js +453 -0
- package/dist/mcp/tools/read/flowxReadTools.js +135 -0
- package/dist/mcp/tools/read/index.js +16 -0
- package/dist/mcp/tools/read/readToolHelpers.js +68 -0
- package/dist/mcp/tools/read/reviewActivityTools.js +176 -0
- package/dist/mcp/tools/read/serverStatusTools.js +103 -0
- package/dist/mcp/tools/read/transactionActivityOutput.js +300 -0
- package/dist/mcp/tools/read/transactionActivityTools.js +544 -0
- package/dist/mcp/tools/read/walletReadTools.js +733 -0
- package/dist/mcp/tools/session/executionResultTools.js +92 -0
- package/dist/mcp/tools/session/index.js +8 -0
- package/dist/mcp/tools/session/shared.js +79 -0
- package/dist/mcp/tools/session/statusTools.js +134 -0
- package/dist/mcp/tools/session/walletIdentityTools.js +119 -0
- package/dist/mcp/tools/settings/index.js +64 -0
- package/dist/review-app/analysis.css +1 -0
- package/dist/review-app/analysis.js +1 -0
- package/dist/review-app/arc-BjIacwQm.js +1 -0
- package/dist/review-app/architecture-U656AL7Q-aSB9x1OK.js +1 -0
- package/dist/review-app/architectureDiagram-VXUJARFQ-C5W6re2I.js +36 -0
- package/dist/review-app/array-BmXUUrU6.js +1 -0
- package/dist/review-app/blockDiagram-VD42YOAC-20MLNcUm.js +122 -0
- package/dist/review-app/c4Diagram-YG6GDRKO-BZXRrcck.js +10 -0
- package/dist/review-app/channel-lk2p_CUu.js +1 -0
- package/dist/review-app/chunk-4BX2VUAB-BPITOdjX.js +1 -0
- package/dist/review-app/chunk-55IACEB6-Dz-pyw5k.js +1 -0
- package/dist/review-app/chunk-76Q3JFCE-cK_X1P_l.js +1 -0
- package/dist/review-app/chunk-ABZYJK2D-Dt4W53JI.js +81 -0
- package/dist/review-app/chunk-ATLVNIR6-fZHLXURb.js +1 -0
- package/dist/review-app/chunk-B4BG7PRW-BbgcjusC.js +165 -0
- package/dist/review-app/chunk-BJD4TVEz.js +1 -0
- package/dist/review-app/chunk-CVBHYZKI-CViawAKX.js +1 -0
- package/dist/review-app/chunk-DI55MBZ5-C5aoul-d.js +220 -0
- package/dist/review-app/chunk-FMBD7UC4-Chxmw62A.js +15 -0
- package/dist/review-app/chunk-FPAJGGOC-DDHjQ09H.js +80 -0
- package/dist/review-app/chunk-FWNWRKHM-CVVQUptk.js +1 -0
- package/dist/review-app/chunk-HN2XXSSU-yzNpjaSZ.js +1 -0
- package/dist/review-app/chunk-JA3XYJ7Z-C5ZJdU01.js +70 -0
- package/dist/review-app/chunk-JZLCHNYA-BBST4Cnk.js +54 -0
- package/dist/review-app/chunk-LBM3YZW2-CdwAPuHr.js +1 -0
- package/dist/review-app/chunk-LHMN2FUI-BtB5uDcp.js +1 -0
- package/dist/review-app/chunk-O7ZBX7Z2-pxdK4Sa3.js +1 -0
- package/dist/review-app/chunk-QN33PNHL-CbVv3uGK.js +1 -0
- package/dist/review-app/chunk-QXUST7PY-DKM2-t2c.js +7 -0
- package/dist/review-app/chunk-QZHKN3VN-C5ni2pN_.js +1 -0
- package/dist/review-app/chunk-S3R3BYOJ-BWvOhDs0.js +2 -0
- package/dist/review-app/chunk-S6J4BHB3-D9Fk0YeD.js +1 -0
- package/dist/review-app/chunk-T53DSG4Q-C1qEyzyV.js +1 -0
- package/dist/review-app/chunk-TZMSLE5B-B--7eU69.js +1 -0
- package/dist/review-app/classDiagram-2ON5EDUG-DlL1m2bp.js +1 -0
- package/dist/review-app/classDiagram-v2-WZHVMYZB-FXRskT1j.js +1 -0
- package/dist/review-app/clone-BZZb7gpZ.js +1 -0
- package/dist/review-app/cose-bilkent-S5V4N54A-CRIb8XEO.js +1 -0
- package/dist/review-app/cytoscape.esm-C7jYqDP5.js +321 -0
- package/dist/review-app/dagre-6UL2VRFP-FNCAXbdE.js +4 -0
- package/dist/review-app/dagre-Be46QtUd.js +1 -0
- package/dist/review-app/defaultLocale-BaWNtAUL.js +1 -0
- package/dist/review-app/diagram-PSM6KHXK-ylLWjiNM.js +24 -0
- package/dist/review-app/diagram-QEK2KX5R-BCDcESxs.js +43 -0
- package/dist/review-app/diagram-S2PKOQOG-Vdrc-vrO.js +24 -0
- package/dist/review-app/dist-WPc74x_f.js +1 -0
- package/dist/review-app/erDiagram-Q2GNP2WA-E5ZsUbDF.js +60 -0
- package/dist/review-app/flatten-DHf9IeNI.js +1 -0
- package/dist/review-app/flowDiagram-NV44I4VS-DBSQuj6x.js +162 -0
- package/dist/review-app/ganttDiagram-LVOFAZNH-CKUOsqwl.js +267 -0
- package/dist/review-app/gitGraph-F6HP7TQM-DsAD6qK1.js +1 -0
- package/dist/review-app/gitGraphDiagram-NY62KEGX-BCeIMWdl.js +65 -0
- package/dist/review-app/graphlib-CiX5CXxR.js +1 -0
- package/dist/review-app/http-DMvwuuFk.js +1 -0
- package/dist/review-app/identity-DY8PXc6t.js +1 -0
- package/dist/review-app/info-NVLQJR56-Dlx1nZic.js +1 -0
- package/dist/review-app/infoDiagram-F6ZHWCRC-CAuANIrz.js +2 -0
- package/dist/review-app/init-BvqephKz.js +1 -0
- package/dist/review-app/journeyDiagram-XKPGCS4Q-C-Z9phnx.js +139 -0
- package/dist/review-app/kanban-definition-3W4ZIXB7-DufgZABq.js +89 -0
- package/dist/review-app/katex-B-Z-NXXN.js +257 -0
- package/dist/review-app/line-DiIv3Jgw.js +1 -0
- package/dist/review-app/linear-Cv-UPvo1.js +1 -0
- package/dist/review-app/math-kmyYrkHL.js +1 -0
- package/dist/review-app/mermaid-parser.core-DkwUYTPl.js +4 -0
- package/dist/review-app/mindmap-definition-VGOIOE7T-TM_CqdmV.js +68 -0
- package/dist/review-app/ordinal-BliTlkoG.js +1 -0
- package/dist/review-app/packet-BFZMPI3H-DqbnU92v.js +1 -0
- package/dist/review-app/path-AEo9W6mQ.js +1 -0
- package/dist/review-app/pie-7BOR55EZ-LJzaLkgr.js +1 -0
- package/dist/review-app/pieDiagram-ADFJNKIX-BAs8OfRS.js +30 -0
- package/dist/review-app/quadrantDiagram-AYHSOK5B-CyUDZP5S.js +7 -0
- package/dist/review-app/radar-NHE76QYJ-DBpHc8_Y.js +1 -0
- package/dist/review-app/reduce-B-HuPpdd.js +1 -0
- package/dist/review-app/requirementDiagram-UZGBJVZJ-BEHix78P.js +64 -0
- package/dist/review-app/review.css +1 -0
- package/dist/review-app/review.js +43 -0
- package/dist/review-app/sankeyDiagram-TZEHDZUN-B2bKbmsm.js +10 -0
- package/dist/review-app/sequenceDiagram-WL72ISMW-DVLOORFJ.js +145 -0
- package/dist/review-app/settings.css +1 -0
- package/dist/review-app/settings.js +1 -0
- package/dist/review-app/src-Buml7cM5.js +1 -0
- package/dist/review-app/stateDiagram-FKZM4ZOC-sFGGp2kV.js +1 -0
- package/dist/review-app/stateDiagram-v2-4FDKWEC3-BHfCF4dX.js +1 -0
- package/dist/review-app/timeline-definition-IT6M3QCI-BESnBijC.js +61 -0
- package/dist/review-app/treemap-KMMF4GRG-wnVLBDeQ.js +1 -0
- package/dist/review-app/walletStatus-CcojOdGy.js +7 -0
- package/dist/review-app/xychartDiagram-PRI3JC2R-BGWVfCx4.js +7 -0
- package/dist/review-server/assets.js +48 -0
- package/dist/review-server/html.js +66 -0
- package/dist/review-server/http.js +47 -0
- package/dist/review-server/middleware/hostOrigin.js +48 -0
- package/dist/review-server/middleware/reviewToken.js +7 -0
- package/dist/review-server/reviewServerPolicy.js +10 -0
- package/dist/review-server/server.js +568 -0
- package/dist/review-server/settingsApi.js +182 -0
- package/dist/review-server/walletIdentityResponse.js +13 -0
- package/dist/runtime/config.js +103 -0
- package/dist/runtime/localSettingsService.js +198 -0
- package/dist/runtime/logger.js +50 -0
- package/dist/runtime/reviewServerAcquire.js +128 -0
- package/dist/runtime/smokeMainnetRead.js +529 -0
- package/dist/runtime/smokeMainnetReadAssertions.js +308 -0
- package/dist/runtime/start.js +295 -0
- package/dist/runtime/suiEndpoint.js +97 -0
- package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
- package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
- package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
- package/docs/AGENT_BEHAVIOR.md +1 -1
- package/docs/AGENT_DEVELOPMENT_POLICY.md +20 -0
- package/docs/FRONTEND_POLICY.md +4 -3
- package/docs/MCP_SETUP.md +59 -7
- package/docs/MCP_TOOLS.md +1 -1
- package/docs/SDK_API.md +5 -1
- package/package.json +3 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { hashEventValue } from "../eventlog/sink.js";
|
|
2
|
+
import { cloneLocalSession, createLocalSessionBase, isLocalSessionExpired, tokenMatchesHash } from "./localSession.js";
|
|
3
|
+
import { SessionStoreError } from "./sessionErrors.js";
|
|
4
|
+
import { isTerminalWalletIdentityStatus, walletIdentityResultInputSchema, walletIdentitySessionSchema } from "./walletIdentity.js";
|
|
5
|
+
const ALLOWED_WALLET_IDENTITY_TRANSITIONS = {
|
|
6
|
+
pending: ["opened", "expired"],
|
|
7
|
+
opened: ["connecting", "failed", "expired"],
|
|
8
|
+
connecting: ["connected", "rejected", "failed", "expired"],
|
|
9
|
+
connected: [],
|
|
10
|
+
rejected: [],
|
|
11
|
+
failed: [],
|
|
12
|
+
expired: []
|
|
13
|
+
};
|
|
14
|
+
export function transitionWalletIdentity(session, next) {
|
|
15
|
+
if (session.status === next) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const allowed = ALLOWED_WALLET_IDENTITY_TRANSITIONS[session.status] ?? [];
|
|
19
|
+
if (!allowed.includes(next)) {
|
|
20
|
+
throw new SessionStoreError("invalid_session_transition", `Invalid wallet identity transition: ${session.status} -> ${next}`);
|
|
21
|
+
}
|
|
22
|
+
session.status = next;
|
|
23
|
+
}
|
|
24
|
+
function parseWalletIdentitySession(session) {
|
|
25
|
+
const parsed = walletIdentitySessionSchema.safeParse(session);
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
throw new SessionStoreError("input_invalid", "Invalid wallet identity session shape");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class WalletIdentitySessionManager {
|
|
31
|
+
options;
|
|
32
|
+
sessions = new Map();
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
}
|
|
36
|
+
async create(now) {
|
|
37
|
+
const { base, token } = createLocalSessionBase(now, this.options.ttlMs);
|
|
38
|
+
const session = {
|
|
39
|
+
...base,
|
|
40
|
+
status: "pending"
|
|
41
|
+
};
|
|
42
|
+
this.sessions.set(session.id, session);
|
|
43
|
+
await this.options.appendEventLog({
|
|
44
|
+
type: "wallet_identity.created",
|
|
45
|
+
sessionId: session.id,
|
|
46
|
+
status: session.status,
|
|
47
|
+
at: now.toISOString()
|
|
48
|
+
});
|
|
49
|
+
return { session: cloneLocalSession(session), token };
|
|
50
|
+
}
|
|
51
|
+
async get(id, now) {
|
|
52
|
+
const session = this.sessions.get(id);
|
|
53
|
+
if (!session) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (isLocalSessionExpired(session, now) && !isTerminalWalletIdentityStatus(session.status)) {
|
|
57
|
+
return cloneLocalSession(await this.expire(id, session, now));
|
|
58
|
+
}
|
|
59
|
+
return cloneLocalSession(session);
|
|
60
|
+
}
|
|
61
|
+
async list(now) {
|
|
62
|
+
const sessions = [];
|
|
63
|
+
for (const id of this.sessions.keys()) {
|
|
64
|
+
const session = await this.get(id, now);
|
|
65
|
+
if (session) {
|
|
66
|
+
sessions.push(session);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return sessions;
|
|
70
|
+
}
|
|
71
|
+
async validateToken(id, token) {
|
|
72
|
+
const session = this.sessions.get(id);
|
|
73
|
+
if (!session) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
// Wallet identity sessions expose terminal lifecycle states through read APIs after token validation.
|
|
77
|
+
// Mutable methods own expiry transitions and return lifecycle-specific errors.
|
|
78
|
+
return tokenMatchesHash(session.tokenHash, token);
|
|
79
|
+
}
|
|
80
|
+
async recordOpened(id, now) {
|
|
81
|
+
const session = this.sessions.get(id);
|
|
82
|
+
if (!session) {
|
|
83
|
+
throw new SessionStoreError("session_not_found", `Wallet identity session not found: ${id}`);
|
|
84
|
+
}
|
|
85
|
+
if (isLocalSessionExpired(session, now) && !isTerminalWalletIdentityStatus(session.status)) {
|
|
86
|
+
return cloneLocalSession(await this.expire(id, session, now));
|
|
87
|
+
}
|
|
88
|
+
if (isTerminalWalletIdentityStatus(session.status)) {
|
|
89
|
+
return cloneLocalSession(session);
|
|
90
|
+
}
|
|
91
|
+
const nextSession = cloneLocalSession(session);
|
|
92
|
+
if (nextSession.status === "pending") {
|
|
93
|
+
transitionWalletIdentity(nextSession, "opened");
|
|
94
|
+
}
|
|
95
|
+
nextSession.lastActivityAt = now.toISOString();
|
|
96
|
+
await this.options.appendEventLog({
|
|
97
|
+
type: "wallet_identity.opened",
|
|
98
|
+
sessionId: id,
|
|
99
|
+
status: nextSession.status,
|
|
100
|
+
at: now.toISOString()
|
|
101
|
+
});
|
|
102
|
+
this.sessions.set(id, nextSession);
|
|
103
|
+
return cloneLocalSession(nextSession);
|
|
104
|
+
}
|
|
105
|
+
async recordConnecting(id, now) {
|
|
106
|
+
const session = await this.requireMutable(id, now);
|
|
107
|
+
const nextSession = cloneLocalSession(session);
|
|
108
|
+
if (nextSession.status !== "connecting") {
|
|
109
|
+
transitionWalletIdentity(nextSession, "connecting");
|
|
110
|
+
}
|
|
111
|
+
nextSession.lastActivityAt = now.toISOString();
|
|
112
|
+
await this.options.appendEventLog({
|
|
113
|
+
type: "wallet_identity.connecting",
|
|
114
|
+
sessionId: id,
|
|
115
|
+
status: nextSession.status,
|
|
116
|
+
at: now.toISOString()
|
|
117
|
+
});
|
|
118
|
+
this.sessions.set(id, nextSession);
|
|
119
|
+
return cloneLocalSession(nextSession);
|
|
120
|
+
}
|
|
121
|
+
async recordResult(id, result, now) {
|
|
122
|
+
const session = await this.requireMutable(id, now);
|
|
123
|
+
const parsed = walletIdentityResultInputSchema.safeParse(result);
|
|
124
|
+
if (!parsed.success) {
|
|
125
|
+
throw new SessionStoreError("input_invalid", `Invalid wallet identity result: ${id}`);
|
|
126
|
+
}
|
|
127
|
+
if (session.status === "opened" &&
|
|
128
|
+
parsed.data.status === "failed" &&
|
|
129
|
+
parsed.data.failureReason !== "no_compatible_wallet") {
|
|
130
|
+
throw new SessionStoreError("invalid_session_transition", "Only no_compatible_wallet may fail before a connection attempt");
|
|
131
|
+
}
|
|
132
|
+
const nextSession = cloneLocalSession(session);
|
|
133
|
+
transitionWalletIdentity(nextSession, parsed.data.status);
|
|
134
|
+
Object.assign(nextSession, parsed.data);
|
|
135
|
+
nextSession.lastActivityAt = now.toISOString();
|
|
136
|
+
parseWalletIdentitySession(nextSession);
|
|
137
|
+
if (parsed.data.status === "connected") {
|
|
138
|
+
await this.options.setActiveAccount(parsed.data.account, now, {
|
|
139
|
+
name: parsed.data.walletName,
|
|
140
|
+
id: parsed.data.walletId
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
this.sessions.set(id, nextSession);
|
|
144
|
+
const eventType = parsed.data.status === "connected"
|
|
145
|
+
? "wallet_identity.connected"
|
|
146
|
+
: parsed.data.status === "rejected"
|
|
147
|
+
? "wallet_identity.rejected"
|
|
148
|
+
: "wallet_identity.failed";
|
|
149
|
+
const event = {
|
|
150
|
+
type: eventType,
|
|
151
|
+
sessionId: id,
|
|
152
|
+
status: nextSession.status,
|
|
153
|
+
at: now.toISOString()
|
|
154
|
+
};
|
|
155
|
+
await this.options.appendEventLog(parsed.data.status === "connected"
|
|
156
|
+
? { ...event, walletAddressHash: hashEventValue(parsed.data.account) }
|
|
157
|
+
: event);
|
|
158
|
+
return cloneLocalSession(nextSession);
|
|
159
|
+
}
|
|
160
|
+
clear() {
|
|
161
|
+
this.sessions.clear();
|
|
162
|
+
}
|
|
163
|
+
async requireMutable(id, now) {
|
|
164
|
+
const session = this.sessions.get(id);
|
|
165
|
+
if (!session) {
|
|
166
|
+
throw new SessionStoreError("session_not_found", `Wallet identity session not found: ${id}`);
|
|
167
|
+
}
|
|
168
|
+
if (isLocalSessionExpired(session, now) && !isTerminalWalletIdentityStatus(session.status)) {
|
|
169
|
+
await this.expire(id, session, now);
|
|
170
|
+
throw new SessionStoreError("session_expired", `Wallet identity session expired: ${id}`);
|
|
171
|
+
}
|
|
172
|
+
if (isTerminalWalletIdentityStatus(session.status)) {
|
|
173
|
+
throw new SessionStoreError("invalid_session_transition", `Wallet identity session already terminal: ${id}`);
|
|
174
|
+
}
|
|
175
|
+
return session;
|
|
176
|
+
}
|
|
177
|
+
async expire(id, session, now) {
|
|
178
|
+
const nextSession = cloneLocalSession(session);
|
|
179
|
+
transitionWalletIdentity(nextSession, "expired");
|
|
180
|
+
await this.options.appendEventLog({
|
|
181
|
+
type: "wallet_identity.expired",
|
|
182
|
+
sessionId: id,
|
|
183
|
+
status: nextSession.status,
|
|
184
|
+
at: now.toISOString()
|
|
185
|
+
});
|
|
186
|
+
this.sessions.set(id, nextSession);
|
|
187
|
+
return nextSession;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isValidSuiAddress, isValidTransactionDigest, normalizeSuiAddress } from "@mysten/sui/utils";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// Use the string-only schema at MCP/HTTP output boundaries because zod transforms
|
|
4
|
+
// cannot be represented in MCP JSON Schema. Use the normalized schema for values
|
|
5
|
+
// before storing them as local session state.
|
|
6
|
+
export const suiAddressStringSchema = z
|
|
7
|
+
.string()
|
|
8
|
+
.refine(isValidSuiAddress, "Expected a 32-byte Sui address");
|
|
9
|
+
// Shared Sui transaction digest schema (pinned SDK source of truth). Used at
|
|
10
|
+
// MCP/HTTP boundaries and by the signable-adapter commitment fields.
|
|
11
|
+
export const suiTransactionDigestSchema = z
|
|
12
|
+
.string()
|
|
13
|
+
.refine(isValidTransactionDigest, "Expected a Sui transaction digest");
|
|
14
|
+
export const normalizedSuiAddressSchema = suiAddressStringSchema.transform((value) => normalizeSuiAddress(value));
|
|
15
|
+
export function parseSuiAddress(value) {
|
|
16
|
+
const parsed = normalizedSuiAddressSchema.safeParse(value);
|
|
17
|
+
return parsed.success ? parsed.data : undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export class SuiEndpointError extends Error {
|
|
2
|
+
kind;
|
|
3
|
+
details;
|
|
4
|
+
constructor(kind, message, details = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.kind = kind;
|
|
7
|
+
this.details = details;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export const MAX_SUI_GRPC_URL_LENGTH = 512;
|
|
11
|
+
export const MAX_SUI_GRAPHQL_URL_LENGTH = 512;
|
|
12
|
+
export function parseGrpcUrl(value) {
|
|
13
|
+
let parsed;
|
|
14
|
+
if (value.length > MAX_SUI_GRPC_URL_LENGTH) {
|
|
15
|
+
throw new SuiEndpointError("invalid_url", `Sui gRPC URL must be ${MAX_SUI_GRPC_URL_LENGTH} characters or fewer`);
|
|
16
|
+
}
|
|
17
|
+
const authority = getUrlAuthority(value);
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(value);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new SuiEndpointError("invalid_url", "Sui gRPC URL must be a valid http(s) URL with an explicit port");
|
|
23
|
+
}
|
|
24
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
25
|
+
throw new SuiEndpointError("invalid_url", "Sui gRPC URL must use http or https");
|
|
26
|
+
}
|
|
27
|
+
if (!authority || !hasExplicitPort(authority)) {
|
|
28
|
+
throw new SuiEndpointError("invalid_url", "Sui gRPC URL must include an explicit port, for example :443 or :9000");
|
|
29
|
+
}
|
|
30
|
+
if (authority.includes("@")) {
|
|
31
|
+
throw new SuiEndpointError("invalid_url", "Sui gRPC URL must not include credentials");
|
|
32
|
+
}
|
|
33
|
+
if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
34
|
+
throw new SuiEndpointError("invalid_url", "Sui gRPC URL must include only scheme, host, and explicit port");
|
|
35
|
+
}
|
|
36
|
+
return `${parsed.protocol}//${authority}`;
|
|
37
|
+
}
|
|
38
|
+
// GraphQL endpoints commonly include a path such as /graphql and may omit an
|
|
39
|
+
// explicit port. gRPC endpoints stay stricter because the pinned gRPC client
|
|
40
|
+
// runtime expects only scheme, host, and explicit port.
|
|
41
|
+
export function parseGraphqlUrl(value) {
|
|
42
|
+
let parsed;
|
|
43
|
+
if (value.length > MAX_SUI_GRAPHQL_URL_LENGTH) {
|
|
44
|
+
throw new SuiEndpointError("invalid_url", `Sui GraphQL URL must be ${MAX_SUI_GRAPHQL_URL_LENGTH} characters or fewer`);
|
|
45
|
+
}
|
|
46
|
+
const authority = getUrlAuthority(value);
|
|
47
|
+
try {
|
|
48
|
+
parsed = new URL(value);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new SuiEndpointError("invalid_url", "Sui GraphQL URL must be a valid https URL");
|
|
52
|
+
}
|
|
53
|
+
if (parsed.protocol !== "https:") {
|
|
54
|
+
throw new SuiEndpointError("invalid_url", "Sui GraphQL URL must use https");
|
|
55
|
+
}
|
|
56
|
+
if (!authority) {
|
|
57
|
+
throw new SuiEndpointError("invalid_url", "Sui GraphQL URL must include a host");
|
|
58
|
+
}
|
|
59
|
+
if (authority.includes("@")) {
|
|
60
|
+
throw new SuiEndpointError("invalid_url", "Sui GraphQL URL must not include credentials");
|
|
61
|
+
}
|
|
62
|
+
if (parsed.search || parsed.hash) {
|
|
63
|
+
throw new SuiEndpointError("invalid_url", "Sui GraphQL URL must not include a query string or fragment");
|
|
64
|
+
}
|
|
65
|
+
return parsed.toString();
|
|
66
|
+
}
|
|
67
|
+
function getUrlAuthority(value) {
|
|
68
|
+
return value.match(/^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i)?.[1];
|
|
69
|
+
}
|
|
70
|
+
function hasExplicitPort(authority) {
|
|
71
|
+
return /\]:\d+$/.test(authority) || /^[^[]*:\d+$/.test(authority);
|
|
72
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const activeAccountResponseSchema = z.discriminatedUnion("status", [
|
|
3
|
+
z.object({
|
|
4
|
+
status: z.literal("set"),
|
|
5
|
+
account: z.string(),
|
|
6
|
+
source: z.literal("wallet_identity"),
|
|
7
|
+
setAt: z.string(),
|
|
8
|
+
boundary: z.literal("read_context_only_not_signing_authorization")
|
|
9
|
+
}),
|
|
10
|
+
z.object({
|
|
11
|
+
status: z.literal("none")
|
|
12
|
+
})
|
|
13
|
+
]);
|
|
14
|
+
export function activeAccountResponse(active) {
|
|
15
|
+
return active
|
|
16
|
+
? {
|
|
17
|
+
status: "set",
|
|
18
|
+
account: active.address,
|
|
19
|
+
source: active.source,
|
|
20
|
+
setAt: active.setAt,
|
|
21
|
+
boundary: "read_context_only_not_signing_authorization"
|
|
22
|
+
}
|
|
23
|
+
: { status: "none" };
|
|
24
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { actionGroups, promptNameFor } from "../adapters/adapterPromptSurfaces.js";
|
|
4
|
+
export const MCP_PROMPTS = [
|
|
5
|
+
{
|
|
6
|
+
name: "inspect-supported-sui-actions",
|
|
7
|
+
title: "Inspect Supported Sui Actions",
|
|
8
|
+
description: "Review current supported mainnet surfaces and tool status.",
|
|
9
|
+
text: [
|
|
10
|
+
"Inspect the Say Ur Intent MCP server status and supported mainnet Sui protocol surfaces.",
|
|
11
|
+
"Use read.get_server_status first, then read.list_supported_protocols.",
|
|
12
|
+
"Report implemented tools separately from unavailable or blocked surfaces."
|
|
13
|
+
].join("\n")
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "prepare-reviewable-sui-action",
|
|
17
|
+
title: "Prepare Reviewable Sui Action",
|
|
18
|
+
description: "Prepare a local review session for a supported action proposal.",
|
|
19
|
+
text: [
|
|
20
|
+
"Prepare a reviewable Sui action only through Say Ur Intent's review-session flow.",
|
|
21
|
+
"Use action.prepare_external_proposal_review when the input is a structured external payment or Sui action proposal.",
|
|
22
|
+
"External proposals get read-only local review and never become signing material. Use action.prepare_sui_action_review for a natural-language swap intent; after a wallet account is connected, account-bound review can reach ready_for_wallet_review, where the local review page offers digest-gated, user-controlled wallet signing.",
|
|
23
|
+
"Show the reviewUrl and summarize the review checks. This MCP response never contains signing data, transaction bytes, or signing readiness; signing and execution receipts happen on the local review page."
|
|
24
|
+
].join("\n")
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
// Platform-owned boundary language appended to every adapter prompt surface.
|
|
28
|
+
// Adapters contribute copy about their own action only; they cannot weaken
|
|
29
|
+
// or omit these lines.
|
|
30
|
+
const PLATFORM_PROMPT_BOUNDARY_LINES = [
|
|
31
|
+
"Show the reviewUrl and summarize the review checks.",
|
|
32
|
+
"This MCP response never contains signing data, transaction bytes, or signing readiness; signing and execution receipts happen on the local review page."
|
|
33
|
+
];
|
|
34
|
+
function surfacePromptText(surface, intent) {
|
|
35
|
+
return [
|
|
36
|
+
`Prepare a reviewable Sui mainnet ${surface.action} for this intent: "${intent}".`,
|
|
37
|
+
`Parse the source amount, source symbol, and target symbol from the intent (any language) and call ${surface.toolName} with them and intent.protocol set to "${surface.protocolSlug}".`,
|
|
38
|
+
...PLATFORM_PROMPT_BOUNDARY_LINES
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
function registerSurfacePrompt(server, name, surface, description) {
|
|
42
|
+
server.registerPrompt(name, {
|
|
43
|
+
title: surface.title,
|
|
44
|
+
description,
|
|
45
|
+
argsSchema: {
|
|
46
|
+
intent: z.string().describe(surface.intentArgDescription)
|
|
47
|
+
}
|
|
48
|
+
}, async ({ intent }) => ({
|
|
49
|
+
description,
|
|
50
|
+
messages: [
|
|
51
|
+
{
|
|
52
|
+
role: "user",
|
|
53
|
+
content: {
|
|
54
|
+
type: "text",
|
|
55
|
+
text: surfacePromptText(surface, intent)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
export function registerMcpPrompts(server, surfaces = []) {
|
|
62
|
+
for (const prompt of MCP_PROMPTS) {
|
|
63
|
+
server.registerPrompt(prompt.name, {
|
|
64
|
+
title: prompt.title,
|
|
65
|
+
description: prompt.description
|
|
66
|
+
}, async () => ({
|
|
67
|
+
description: prompt.description,
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
role: "user",
|
|
71
|
+
content: {
|
|
72
|
+
type: "text",
|
|
73
|
+
text: prompt.text
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
// Adapter prompt surfaces: action-first names (`swap-deep`) with one
|
|
80
|
+
// free-text intent argument so clients can pass the whole request in one
|
|
81
|
+
// line (Claude Code: /mcp__say-ur-intent__swap-deep 10 sui to usdc;
|
|
82
|
+
// Claude Desktop shows a single input field). The model parses the
|
|
83
|
+
// intent; this server never does.
|
|
84
|
+
for (const surface of surfaces) {
|
|
85
|
+
registerSurfacePrompt(server, promptNameFor(surface), surface, surface.description);
|
|
86
|
+
}
|
|
87
|
+
// Bare action prompts are always registered. One protocol: straight to it.
|
|
88
|
+
// Several protocols: an optional `protocol` argument (completion suggests
|
|
89
|
+
// the slugs) and explicit instructions to ask the user - the venue is the
|
|
90
|
+
// user's choice, never the server's.
|
|
91
|
+
for (const [action, group] of actionGroups(surfaces)) {
|
|
92
|
+
registerBareActionPrompt(server, action, group);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function bareActionPromptText(action, group, intent, protocol) {
|
|
96
|
+
const chosen = group.length === 1 ? group[0] : group.find((surface) => surface.protocolSlug === (protocol ?? "").trim());
|
|
97
|
+
if (chosen) {
|
|
98
|
+
return surfacePromptText(chosen, intent);
|
|
99
|
+
}
|
|
100
|
+
const options = group
|
|
101
|
+
.map((surface) => `${surface.protocolSlug} (${surface.title}, tool: ${surface.toolName})`)
|
|
102
|
+
.join("; ");
|
|
103
|
+
return [
|
|
104
|
+
`Several protocols support the ${action} action: ${options}.`,
|
|
105
|
+
`The user's intent: "${intent}".`,
|
|
106
|
+
"List these protocol options to the user and ask which one to use. Do not pick a protocol on your own.",
|
|
107
|
+
"Once the user names a protocol, parse the source amount, source symbol, and target symbol from the intent (any language) and call that protocol's tool with them and intent.protocol set to the chosen slug.",
|
|
108
|
+
...PLATFORM_PROMPT_BOUNDARY_LINES
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
111
|
+
function registerBareActionPrompt(server, action, group) {
|
|
112
|
+
const single = group.length === 1 ? group[0] : undefined;
|
|
113
|
+
const first = group[0];
|
|
114
|
+
if (!first) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const description = single
|
|
118
|
+
? `${single.description} Shorthand for ${promptNameFor(single)}.`
|
|
119
|
+
: `Prepare a reviewable Sui ${action} - several protocols support it (${group
|
|
120
|
+
.map((surface) => surface.protocolSlug)
|
|
121
|
+
.join(", ")}); you pick which.`;
|
|
122
|
+
const slugs = group.map((surface) => surface.protocolSlug);
|
|
123
|
+
server.registerPrompt(action, {
|
|
124
|
+
title: single ? single.title : `${action} (choose protocol)`,
|
|
125
|
+
description,
|
|
126
|
+
argsSchema: {
|
|
127
|
+
intent: completable(z.string().describe(first.intentArgDescription), (value) => value ? [] : group.flatMap((surface) => surface.exampleIntents)),
|
|
128
|
+
...(single
|
|
129
|
+
? {}
|
|
130
|
+
: {
|
|
131
|
+
protocol: completable(z.string().optional().describe(`Protocol slug (optional): ${slugs.join(" | ")}`), (value) => slugs.filter((slug) => slug.startsWith((value ?? "").toLowerCase())))
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}, async (args) => ({
|
|
135
|
+
description,
|
|
136
|
+
messages: [
|
|
137
|
+
{
|
|
138
|
+
role: "user",
|
|
139
|
+
content: {
|
|
140
|
+
type: "text",
|
|
141
|
+
text: bareActionPromptText(action, group, String(args.intent ?? ""), args.protocol)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { assertValidToolName } from "./toolNames.js";
|
|
2
|
+
const DEFAULT_TOOL_ANNOTATIONS = {
|
|
3
|
+
openWorldHint: false
|
|
4
|
+
};
|
|
5
|
+
function mergedAnnotations(defaultAnnotations, annotations) {
|
|
6
|
+
return {
|
|
7
|
+
...DEFAULT_TOOL_ANNOTATIONS,
|
|
8
|
+
...(defaultAnnotations ?? {}),
|
|
9
|
+
...(annotations ?? {})
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function registerSayUrIntentTool(server, name, config, callback) {
|
|
13
|
+
assertValidToolName(name);
|
|
14
|
+
const { defaultAnnotations, annotations, ...sdkConfig } = config;
|
|
15
|
+
return server.registerTool(name, {
|
|
16
|
+
...sdkConfig,
|
|
17
|
+
annotations: mergedAnnotations(defaultAnnotations, annotations)
|
|
18
|
+
}, callback);
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
5
|
+
export const MCP_RESOURCES = [
|
|
6
|
+
{
|
|
7
|
+
name: "readme",
|
|
8
|
+
uri: "sayurintent://docs/readme",
|
|
9
|
+
title: "README",
|
|
10
|
+
description: "Public entry document: product purpose, current release boundary, setup path, and documentation map.",
|
|
11
|
+
path: "README.md"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "mcp-setup",
|
|
15
|
+
uri: "sayurintent://docs/mcp-setup",
|
|
16
|
+
title: "MCP Setup",
|
|
17
|
+
description: "Setup guide: installation, MCP client connection, first-use flow, settings, and troubleshooting.",
|
|
18
|
+
path: "docs/MCP_SETUP.md"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "mcp-tools",
|
|
22
|
+
uri: "sayurintent://docs/mcp-tools",
|
|
23
|
+
title: "MCP Tools",
|
|
24
|
+
description: "API reference: tool contracts, response fields, statuses, follow-up fields, and output boundaries.",
|
|
25
|
+
path: "docs/MCP_TOOLS.md"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "wallet-identity",
|
|
29
|
+
uri: "sayurintent://docs/wallet-identity",
|
|
30
|
+
title: "Wallet Identity",
|
|
31
|
+
description: "Wallet identity reference: active-account read context and same-machine capture boundaries.",
|
|
32
|
+
path: "docs/WALLET_IDENTITY.md"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "agent-behavior",
|
|
36
|
+
uri: "sayurintent://docs/agent-behavior",
|
|
37
|
+
title: "Agent Behavior Reference",
|
|
38
|
+
description: "Answer playbook: user-question flows, tool selection, and response wording boundaries.",
|
|
39
|
+
path: "docs/AGENT_BEHAVIOR.md"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "deepbook-v3",
|
|
43
|
+
uri: "sayurintent://protocols/deepbook-v3",
|
|
44
|
+
title: "DeepBookV3",
|
|
45
|
+
description: "Protocol reference only; use MCP tool responses and read.list_supported_protocols for current support.",
|
|
46
|
+
path: "protocols/deepbook-v3.md"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "deepbook-margin",
|
|
50
|
+
uri: "sayurintent://protocols/deepbook-margin",
|
|
51
|
+
title: "DeepBook Margin",
|
|
52
|
+
description: "Protocol reference only; no margin MCP read tools or signable actions are exposed in this release.",
|
|
53
|
+
path: "protocols/deepbook-margin.md"
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
export function registerMcpResources(server) {
|
|
57
|
+
for (const resource of MCP_RESOURCES) {
|
|
58
|
+
server.registerResource(resource.name, resource.uri, {
|
|
59
|
+
title: resource.title,
|
|
60
|
+
description: resource.description,
|
|
61
|
+
mimeType: "text/markdown"
|
|
62
|
+
}, async () => ({
|
|
63
|
+
contents: [
|
|
64
|
+
{
|
|
65
|
+
uri: resource.uri,
|
|
66
|
+
mimeType: "text/markdown",
|
|
67
|
+
text: await readFile(resolve(packageRoot, resource.path), "utf8")
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|