memeloop 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anthropic.cjs +72 -0
- package/dist/anthropic.cjs.map +1 -0
- package/dist/anthropic.d.cts +21 -0
- package/dist/anthropic.d.ts +21 -0
- package/dist/anthropic.js +21 -0
- package/dist/anthropic.js.map +1 -0
- package/dist/azure.cjs +72 -0
- package/dist/azure.cjs.map +1 -0
- package/dist/azure.d.cts +21 -0
- package/dist/azure.d.ts +21 -0
- package/dist/azure.js +21 -0
- package/dist/azure.js.map +1 -0
- package/dist/chunk-3CN7DFBL.js +4894 -0
- package/dist/chunk-3CN7DFBL.js.map +1 -0
- package/dist/chunk-4AZ4D7U5.js +32 -0
- package/dist/chunk-4AZ4D7U5.js.map +1 -0
- package/dist/chunk-6ON4XSOV.js +63 -0
- package/dist/chunk-6ON4XSOV.js.map +1 -0
- package/dist/chunk-DJQNGIXX.js +4793 -0
- package/dist/chunk-DJQNGIXX.js.map +1 -0
- package/dist/chunk-FKQP6AOQ.js +32 -0
- package/dist/chunk-FKQP6AOQ.js.map +1 -0
- package/dist/chunk-OVJ7JJBA.js +35 -0
- package/dist/chunk-OVJ7JJBA.js.map +1 -0
- package/dist/chunk-P4UZ7IXC.js +42 -0
- package/dist/chunk-P4UZ7IXC.js.map +1 -0
- package/dist/chunk-YHJ2AEFR.js +4933 -0
- package/dist/chunk-YHJ2AEFR.js.map +1 -0
- package/dist/chunk-YVK7RYRO.js +4973 -0
- package/dist/chunk-YVK7RYRO.js.map +1 -0
- package/dist/code-assistant.json +114 -0
- package/dist/cohere.cjs +72 -0
- package/dist/cohere.cjs.map +1 -0
- package/dist/cohere.d.cts +21 -0
- package/dist/cohere.d.ts +21 -0
- package/dist/cohere.js +21 -0
- package/dist/cohere.js.map +1 -0
- package/dist/deepseek.cjs +72 -0
- package/dist/deepseek.cjs.map +1 -0
- package/dist/deepseek.d.cts +21 -0
- package/dist/deepseek.d.ts +21 -0
- package/dist/deepseek.js +21 -0
- package/dist/deepseek.js.map +1 -0
- package/dist/dist-65U7OYRG.js +1257 -0
- package/dist/dist-65U7OYRG.js.map +1 -0
- package/dist/fetchProvider-Cu4WkU5O.d.cts +819 -0
- package/dist/fetchProvider-Cu4WkU5O.d.ts +819 -0
- package/dist/frontend-ui-ux.json +105 -0
- package/dist/general-assistant.json +95 -0
- package/dist/git-master.json +51 -0
- package/dist/google-vertex.cjs +72 -0
- package/dist/google-vertex.cjs.map +1 -0
- package/dist/google-vertex.d.cts +21 -0
- package/dist/google-vertex.d.ts +21 -0
- package/dist/google-vertex.js +21 -0
- package/dist/google-vertex.js.map +1 -0
- package/dist/google.cjs +72 -0
- package/dist/google.cjs.map +1 -0
- package/dist/google.d.cts +21 -0
- package/dist/google.d.ts +21 -0
- package/dist/google.js +21 -0
- package/dist/google.js.map +1 -0
- package/dist/groq.cjs +72 -0
- package/dist/groq.cjs.map +1 -0
- package/dist/groq.d.cts +21 -0
- package/dist/groq.d.ts +21 -0
- package/dist/groq.js +21 -0
- package/dist/groq.js.map +1 -0
- package/dist/index.cjs +8916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1816 -0
- package/dist/index.d.mts +3203 -0
- package/dist/index.d.ts +1816 -0
- package/dist/index.js +4040 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +7064 -0
- package/dist/index.mjs.map +1 -0
- package/dist/llm-providers.cjs +311 -0
- package/dist/llm-providers.cjs.map +1 -0
- package/dist/llm-providers.d.cts +111 -0
- package/dist/llm-providers.d.ts +111 -0
- package/dist/llm-providers.js +232 -0
- package/dist/llm-providers.js.map +1 -0
- package/dist/loop-api.cjs +4924 -0
- package/dist/loop-api.cjs.map +1 -0
- package/dist/loop-api.d.cts +11 -0
- package/dist/loop-api.d.ts +11 -0
- package/dist/loop-api.js +189 -0
- package/dist/loop-api.js.map +1 -0
- package/dist/mistral.cjs +72 -0
- package/dist/mistral.cjs.map +1 -0
- package/dist/mistral.d.cts +21 -0
- package/dist/mistral.d.ts +21 -0
- package/dist/mistral.js +21 -0
- package/dist/mistral.js.map +1 -0
- package/dist/multipart-parser-EJCXIUHC.js +372 -0
- package/dist/multipart-parser-EJCXIUHC.js.map +1 -0
- package/dist/openai.cjs +72 -0
- package/dist/openai.cjs.map +1 -0
- package/dist/openai.d.cts +21 -0
- package/dist/openai.d.ts +21 -0
- package/dist/openai.js +21 -0
- package/dist/openai.js.map +1 -0
- package/dist/perplexity.cjs +72 -0
- package/dist/perplexity.cjs.map +1 -0
- package/dist/perplexity.d.cts +21 -0
- package/dist/perplexity.d.ts +21 -0
- package/dist/perplexity.js +21 -0
- package/dist/perplexity.js.map +1 -0
- package/dist/playwright.json +52 -0
- package/dist/promptConcat-BJdazTg5.d.cts +1832 -0
- package/dist/promptConcat-BJdazTg5.d.ts +1832 -0
- package/dist/promptConcat-CAkpni_8.d.cts +1834 -0
- package/dist/promptConcat-CAkpni_8.d.ts +1834 -0
- package/dist/promptConcat-CfH5-GAE.d.cts +1036 -0
- package/dist/promptConcat-ChcAZOOL.d.ts +1036 -0
- package/dist/promptConcat-G5TnJVRy.d.cts +1800 -0
- package/dist/promptConcat-G5TnJVRy.d.ts +1800 -0
- package/dist/promptConcat-L72eowzp.d.ts +1090 -0
- package/dist/promptConcat-eLHdN7K8.d.cts +1090 -0
- package/dist/src-VTSXKG72.js +1409 -0
- package/dist/src-VTSXKG72.js.map +1 -0
- package/dist/togetherai.cjs +72 -0
- package/dist/togetherai.cjs.map +1 -0
- package/dist/togetherai.d.cts +21 -0
- package/dist/togetherai.d.ts +21 -0
- package/dist/togetherai.js +21 -0
- package/dist/togetherai.js.map +1 -0
- package/dist/types-B0Jm2l38.d.cts +767 -0
- package/dist/types-B0Jm2l38.d.ts +767 -0
- package/dist/xai.cjs +72 -0
- package/dist/xai.cjs.map +1 -0
- package/dist/xai.d.cts +21 -0
- package/dist/xai.d.ts +21 -0
- package/dist/xai.js +21 -0
- package/dist/xai.js.map +1 -0
- package/package.json +87 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4040 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_AGENT_LOOP_ID,
|
|
3
|
+
AGENT_TOOL_LOOP_ID,
|
|
4
|
+
ALL_CATEGORIES,
|
|
5
|
+
ASK_QUESTION_TOOL_ID,
|
|
6
|
+
ASK_USER_QUESTION_TOOL_ID,
|
|
7
|
+
AgentProfileRegistry,
|
|
8
|
+
BUILTIN_AGENT_AGENT_LOOP_QUALITY_GATE_SCRIPT_ID,
|
|
9
|
+
BUILTIN_AGENT_PROFILES,
|
|
10
|
+
BUILTIN_AGENT_TOOL_LOOP_DEFAULT_SCRIPT_ID,
|
|
11
|
+
BUILTIN_TOOL_PLUGIN_IDS,
|
|
12
|
+
HookRegistry,
|
|
13
|
+
MEMELOOP_STRUCTURED_TOOL_KEY,
|
|
14
|
+
PERMISSIONS_TABLE_DDL,
|
|
15
|
+
PLUGIN_ASK_QUESTION,
|
|
16
|
+
PLUGIN_ASK_USER_QUESTION,
|
|
17
|
+
PLUGIN_MCP_CLIENT,
|
|
18
|
+
PLUGIN_MCP_FORWARD,
|
|
19
|
+
PLUGIN_REMOTE_AGENT,
|
|
20
|
+
PLUGIN_SPAWN_AGENT,
|
|
21
|
+
PLUGIN_TASK,
|
|
22
|
+
PLUGIN_TODO_WRITE,
|
|
23
|
+
ProviderRegistry,
|
|
24
|
+
TODO_WRITE_TOOL_ID,
|
|
25
|
+
TokenTracker,
|
|
26
|
+
__clearTodoStore,
|
|
27
|
+
askQuestionConfigSchema,
|
|
28
|
+
askQuestionImpl,
|
|
29
|
+
askUserQuestionConfigSchema,
|
|
30
|
+
askUserQuestionImpl,
|
|
31
|
+
autoCompact,
|
|
32
|
+
buildAgent,
|
|
33
|
+
builtinAgentAgentLoopSources,
|
|
34
|
+
builtinAgentToolLoopSources,
|
|
35
|
+
cancelPendingApprovals,
|
|
36
|
+
checkPermission,
|
|
37
|
+
clearHooks,
|
|
38
|
+
clearPluginRegistrations,
|
|
39
|
+
compactMessages,
|
|
40
|
+
createAgentAgentLoopDefinition,
|
|
41
|
+
createAgentFrameworkHooks,
|
|
42
|
+
createAgentInstanceFromDefinition,
|
|
43
|
+
createAgentLoopRunner,
|
|
44
|
+
createAgentToolLoopDefinition,
|
|
45
|
+
createAgentToolLoopRunner,
|
|
46
|
+
createChatMessage,
|
|
47
|
+
createHooksWithPlugins,
|
|
48
|
+
createMemeLoopRuntime,
|
|
49
|
+
createPluginAPI,
|
|
50
|
+
evaluateApproval,
|
|
51
|
+
executeHooks,
|
|
52
|
+
exploreAgent,
|
|
53
|
+
extractMemeloopStructuredToolPayload,
|
|
54
|
+
findPromptById,
|
|
55
|
+
flattenPrompts,
|
|
56
|
+
getActivePluginRegistry,
|
|
57
|
+
getAgentAgentLoopId,
|
|
58
|
+
getAgentProfileRegistry,
|
|
59
|
+
getBuiltinAgentAgentLoopSource,
|
|
60
|
+
getBuiltinAgentToolLoopSource,
|
|
61
|
+
getBuiltinLoopProfile,
|
|
62
|
+
getBuiltinLoopProfiles,
|
|
63
|
+
getBuiltinToolPlugins,
|
|
64
|
+
getCategoryConfig,
|
|
65
|
+
getDefaultHookRegistry,
|
|
66
|
+
getHookCount,
|
|
67
|
+
getLoadedPlugin,
|
|
68
|
+
getLoopRegistry,
|
|
69
|
+
getMcpClientToolId,
|
|
70
|
+
getMcpForwardToolId,
|
|
71
|
+
getPendingApprovals,
|
|
72
|
+
getPluginRegistrations,
|
|
73
|
+
getRemoteAgentToolId,
|
|
74
|
+
getSpawnAgentToolId,
|
|
75
|
+
getTaskToolId,
|
|
76
|
+
getToolMetadata,
|
|
77
|
+
getToolParameterSchema,
|
|
78
|
+
hasHooks,
|
|
79
|
+
isPluginLoaded,
|
|
80
|
+
isTaskCategory,
|
|
81
|
+
librarianAgent,
|
|
82
|
+
listPlugins,
|
|
83
|
+
listRegisteredHookTypes,
|
|
84
|
+
loadAgentAgentLoopScript,
|
|
85
|
+
loadAgentToolLoopScript,
|
|
86
|
+
loadPluginModule,
|
|
87
|
+
loadPluginModules,
|
|
88
|
+
loadUserPermissions,
|
|
89
|
+
matchAllToolCallings,
|
|
90
|
+
matchPattern,
|
|
91
|
+
matchToolCalling,
|
|
92
|
+
mcpClientConfigSchema,
|
|
93
|
+
mcpClientImpl,
|
|
94
|
+
mcpForwardConfigSchema,
|
|
95
|
+
mcpForwardImpl,
|
|
96
|
+
mergePermissionSets,
|
|
97
|
+
onApprovalRequest,
|
|
98
|
+
oracleAgent,
|
|
99
|
+
planAgent,
|
|
100
|
+
pluginRegistry,
|
|
101
|
+
promptConcatStream,
|
|
102
|
+
registerBuiltinLoops,
|
|
103
|
+
registerBuiltinPromptPlugins,
|
|
104
|
+
registerBuiltinToolPlugins,
|
|
105
|
+
registerHook,
|
|
106
|
+
registerPluginHooks,
|
|
107
|
+
registerPluginTools,
|
|
108
|
+
registerToolParameterSchema,
|
|
109
|
+
remoteAgentConfigSchema,
|
|
110
|
+
remoteAgentImpl,
|
|
111
|
+
remoteAgentListImpl,
|
|
112
|
+
requestApproval,
|
|
113
|
+
resetAgentProfileRegistry,
|
|
114
|
+
resetLoopRegistry,
|
|
115
|
+
resolveAgentToolLoopTerminalState,
|
|
116
|
+
resolveApproval,
|
|
117
|
+
resolveCategory,
|
|
118
|
+
resolvePromptPluginMap,
|
|
119
|
+
resolveQuestionAnswer,
|
|
120
|
+
runAgentToolLoopTurn,
|
|
121
|
+
runPostProcessHooks,
|
|
122
|
+
runProcessPromptsHooks,
|
|
123
|
+
runResponseCompleteHooks,
|
|
124
|
+
runToolExecutedHooks,
|
|
125
|
+
runWithPluginRegistry,
|
|
126
|
+
saveUserPermissions,
|
|
127
|
+
shouldCompact,
|
|
128
|
+
spawnAgentConfigSchema,
|
|
129
|
+
spawnAgentImpl,
|
|
130
|
+
taskToolConfigSchema,
|
|
131
|
+
taskToolImpl,
|
|
132
|
+
todoWriteConfigSchema,
|
|
133
|
+
todoWriteImpl,
|
|
134
|
+
truncateToolSummary,
|
|
135
|
+
unloadAllPlugins,
|
|
136
|
+
unloadPlugin,
|
|
137
|
+
unregisterHook,
|
|
138
|
+
validatePluginManifest
|
|
139
|
+
} from "./chunk-3CN7DFBL.js";
|
|
140
|
+
import {
|
|
141
|
+
createFetchLLMProvider
|
|
142
|
+
} from "./chunk-OVJ7JJBA.js";
|
|
143
|
+
|
|
144
|
+
// src/sync/attachmentRpcCodec.ts
|
|
145
|
+
function decodeAttachmentBlobRpc(result) {
|
|
146
|
+
if (result == null || typeof result !== "object") return null;
|
|
147
|
+
const r = result;
|
|
148
|
+
if (r.error || !r.found || typeof r.dataBase64 !== "string") return null;
|
|
149
|
+
const data = Buffer.from(r.dataBase64, "base64");
|
|
150
|
+
if (!data.length) return null;
|
|
151
|
+
return {
|
|
152
|
+
data: new Uint8Array(data),
|
|
153
|
+
filename: typeof r.filename === "string" ? r.filename : "attachment",
|
|
154
|
+
mimeType: typeof r.mimeType === "string" ? r.mimeType : "application/octet-stream",
|
|
155
|
+
size: typeof r.size === "number" && r.size > 0 ? r.size : data.length
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/sync/chatSyncEngine.ts
|
|
160
|
+
var ChatSyncEngine = class {
|
|
161
|
+
nodeId;
|
|
162
|
+
storage;
|
|
163
|
+
getPeers;
|
|
164
|
+
versionVector = {};
|
|
165
|
+
constructor(options) {
|
|
166
|
+
this.nodeId = options.nodeId;
|
|
167
|
+
this.storage = options.storage;
|
|
168
|
+
this.getPeers = options.peers;
|
|
169
|
+
this.versionVector[this.nodeId] = 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
|
|
173
|
+
*/
|
|
174
|
+
bumpLocalVersion() {
|
|
175
|
+
this.versionVector[this.nodeId] = (this.versionVector[this.nodeId] ?? 0) + 1;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 执行一次与所有 peer 的增量同步(metadata 级别),并尽力拉取缺失消息。
|
|
179
|
+
*/
|
|
180
|
+
async syncOnce() {
|
|
181
|
+
const peers = this.getPeers();
|
|
182
|
+
if (peers.length === 0) return;
|
|
183
|
+
const localVersion = { ...this.versionVector };
|
|
184
|
+
const pulledConversations = /* @__PURE__ */ new Set();
|
|
185
|
+
for (const peer of peers) {
|
|
186
|
+
const { remoteVersion, missingForRemote } = await peer.exchangeVersionVector(localVersion);
|
|
187
|
+
const missingForLocal = await peer.pullMissingMetadata(this.versionVector);
|
|
188
|
+
for (const meta of missingForLocal) {
|
|
189
|
+
await this.storage.upsertConversationMetadata(meta);
|
|
190
|
+
pulledConversations.add(meta.conversationId);
|
|
191
|
+
}
|
|
192
|
+
for (const [nodeId, clock] of Object.entries(remoteVersion)) {
|
|
193
|
+
const current = this.versionVector[nodeId] ?? 0;
|
|
194
|
+
if (clock > current) {
|
|
195
|
+
this.versionVector[nodeId] = clock;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (missingForRemote.length > 0) {
|
|
199
|
+
this.bumpLocalVersion();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const conversationId of pulledConversations) {
|
|
203
|
+
await this.pullMessagesForConversationFromPeers(conversationId, peers);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Anti-entropy:先 syncOnce,再对已知会话逐个向各 peer 拉取缺失消息(参考审查单「定期对账」)。
|
|
208
|
+
*/
|
|
209
|
+
async antiEntropyOnce() {
|
|
210
|
+
await this.syncOnce();
|
|
211
|
+
const list = await this.storage.listConversations({ limit: 500 });
|
|
212
|
+
const peers = this.getPeers();
|
|
213
|
+
for (const meta of list) {
|
|
214
|
+
await this.pullMessagesForConversationFromPeers(meta.conversationId, peers);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async pullMessagesForConversationFromPeers(conversationId, peers) {
|
|
218
|
+
const localMsgs = await this.storage.getMessages(conversationId, { mode: "full-content" });
|
|
219
|
+
const knownIds = localMsgs.map((m) => m.messageId);
|
|
220
|
+
for (const peer of peers) {
|
|
221
|
+
if (!peer.pullMissingMessages) continue;
|
|
222
|
+
try {
|
|
223
|
+
const incoming = await peer.pullMissingMessages(conversationId, knownIds);
|
|
224
|
+
if (incoming.length > 0) {
|
|
225
|
+
await this.storage.insertMessagesIfAbsent(incoming);
|
|
226
|
+
await this.ensureAttachmentsFromMessages(incoming, peers);
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** 从消息元数据收集 `attachments[].contentHash`,对本地缺失 BLOB 的哈希向各 peer 拉取并 `saveAttachment`。 */
|
|
233
|
+
async ensureAttachmentsFromMessages(messages, peers) {
|
|
234
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
235
|
+
for (const m of messages) {
|
|
236
|
+
for (const a of m.attachments ?? []) {
|
|
237
|
+
if (a.contentHash) hashes.add(a.contentHash);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const h of hashes) {
|
|
241
|
+
const reference = await this.storage.getAttachment(h);
|
|
242
|
+
if (reference) {
|
|
243
|
+
const reader = this.storage.readAttachmentData?.bind(this.storage);
|
|
244
|
+
if (!reader) continue;
|
|
245
|
+
const bytes = await reader(h);
|
|
246
|
+
if (bytes && bytes.length > 0) continue;
|
|
247
|
+
}
|
|
248
|
+
for (const peer of peers) {
|
|
249
|
+
const pull = peer.pullAttachmentBlob?.bind(peer);
|
|
250
|
+
if (!pull) continue;
|
|
251
|
+
try {
|
|
252
|
+
const blob = await pull(h);
|
|
253
|
+
if (blob?.data?.length) {
|
|
254
|
+
const ar = {
|
|
255
|
+
contentHash: h,
|
|
256
|
+
filename: blob.filename,
|
|
257
|
+
mimeType: blob.mimeType,
|
|
258
|
+
size: blob.size > 0 ? blob.size : blob.data.length
|
|
259
|
+
};
|
|
260
|
+
await this.storage.saveAttachment(ar, blob.data);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
getVersionVector() {
|
|
269
|
+
return { ...this.versionVector };
|
|
270
|
+
}
|
|
271
|
+
getStorage() {
|
|
272
|
+
return this.storage;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/sync/peerNodeAdapter.ts
|
|
277
|
+
var PeerNodeSyncAdapter = class {
|
|
278
|
+
nodeId;
|
|
279
|
+
transport;
|
|
280
|
+
constructor(nodeId, transport) {
|
|
281
|
+
this.nodeId = nodeId;
|
|
282
|
+
this.transport = transport;
|
|
283
|
+
}
|
|
284
|
+
exchangeVersionVector(localVersion) {
|
|
285
|
+
return this.transport.exchangeVersionVector(this.nodeId, localVersion);
|
|
286
|
+
}
|
|
287
|
+
pullMissingMetadata(sinceVersion) {
|
|
288
|
+
return this.transport.pullMissingMetadata(this.nodeId, sinceVersion);
|
|
289
|
+
}
|
|
290
|
+
pullMissingMessages(conversationId, knownMessageIds) {
|
|
291
|
+
const function_ = this.transport.pullMissingMessages?.bind(this.transport);
|
|
292
|
+
if (!function_) {
|
|
293
|
+
return Promise.resolve([]);
|
|
294
|
+
}
|
|
295
|
+
return function_(this.nodeId, conversationId, knownMessageIds);
|
|
296
|
+
}
|
|
297
|
+
pullAttachmentBlob(contentHash) {
|
|
298
|
+
const function_ = this.transport.pullAttachmentBlob?.bind(this.transport);
|
|
299
|
+
if (!function_) {
|
|
300
|
+
return Promise.resolve(null);
|
|
301
|
+
}
|
|
302
|
+
return function_(this.nodeId, contentHash);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/types.ts
|
|
307
|
+
function isUserInitiatedConversation(meta) {
|
|
308
|
+
return meta.isUserInitiated;
|
|
309
|
+
}
|
|
310
|
+
function createInstanceDeltaFromDefinition(definition, overrides) {
|
|
311
|
+
const delta = {};
|
|
312
|
+
for (const key of Object.keys(overrides)) {
|
|
313
|
+
const value = overrides[key];
|
|
314
|
+
if (value !== void 0 && value !== definition[key]) {
|
|
315
|
+
delta[key] = value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return delta;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/storage/sessionStorage.ts
|
|
322
|
+
function createCheckpointRecord(conversationId, messages, savedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
323
|
+
const lastMessage = messages[messages.length - 1];
|
|
324
|
+
const preview = lastMessage && typeof lastMessage.content === "string" ? lastMessage.content.slice(0, 200) : "";
|
|
325
|
+
return {
|
|
326
|
+
conversationId,
|
|
327
|
+
messages: [...messages],
|
|
328
|
+
savedAt,
|
|
329
|
+
messageCount: messages.length,
|
|
330
|
+
lastMessagePreview: preview
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function parseCheckpointRecord(raw) {
|
|
334
|
+
try {
|
|
335
|
+
const record = JSON.parse(raw);
|
|
336
|
+
return isCheckpointRecord(record) ? record : null;
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function serializeCheckpointRecord(record) {
|
|
342
|
+
return JSON.stringify(record, null, 2);
|
|
343
|
+
}
|
|
344
|
+
var InMemoryCheckpointStore = class {
|
|
345
|
+
records = /* @__PURE__ */ new Map();
|
|
346
|
+
constructor(options = {}) {
|
|
347
|
+
for (const record of options.records ?? []) {
|
|
348
|
+
this.records.set(record.conversationId, cloneCheckpointRecord(record));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async saveCheckpoint(conversationId, messages) {
|
|
352
|
+
const record = createCheckpointRecord(conversationId, messages);
|
|
353
|
+
this.records.set(conversationId, cloneCheckpointRecord(record));
|
|
354
|
+
return cloneCheckpointRecord(record);
|
|
355
|
+
}
|
|
356
|
+
async loadCheckpoint(conversationId) {
|
|
357
|
+
const record = this.records.get(conversationId);
|
|
358
|
+
return record ? cloneCheckpointRecord(record) : null;
|
|
359
|
+
}
|
|
360
|
+
async listCheckpoints() {
|
|
361
|
+
return [...this.records.values()].map(toCheckpointSummary).sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
362
|
+
}
|
|
363
|
+
async deleteCheckpoint(conversationId) {
|
|
364
|
+
return this.records.delete(conversationId);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var SessionStorage = class extends InMemoryCheckpointStore {
|
|
368
|
+
};
|
|
369
|
+
function isCheckpointRecord(record) {
|
|
370
|
+
if (!record || typeof record !== "object") return false;
|
|
371
|
+
const candidate = record;
|
|
372
|
+
return typeof candidate.conversationId === "string" && Array.isArray(candidate.messages) && typeof candidate.savedAt === "string" && typeof candidate.messageCount === "number" && typeof candidate.lastMessagePreview === "string";
|
|
373
|
+
}
|
|
374
|
+
function cloneCheckpointRecord(record) {
|
|
375
|
+
return {
|
|
376
|
+
...record,
|
|
377
|
+
messages: [...record.messages]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function toCheckpointSummary(record) {
|
|
381
|
+
return {
|
|
382
|
+
conversationId: record.conversationId,
|
|
383
|
+
savedAt: record.savedAt,
|
|
384
|
+
messageCount: record.messageCount,
|
|
385
|
+
lastMessagePreview: record.lastMessagePreview
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/agent/tiddlerTemplateConverter.ts
|
|
390
|
+
function tiddlerToAgentDefinition(tiddler, workspaceName) {
|
|
391
|
+
if (!tiddler || !tiddler.title || typeof tiddler.text !== "string") return null;
|
|
392
|
+
let agentFrameworkConfig;
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(tiddler.text);
|
|
395
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
agentFrameworkConfig = parsed;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const getString = (field, fallback = "") => typeof field === "string" ? field : field && typeof field === "object" ? JSON.stringify(field) : fallback;
|
|
403
|
+
const parseJSON = (field) => {
|
|
404
|
+
if (typeof field === "string") {
|
|
405
|
+
try {
|
|
406
|
+
const parsed = JSON.parse(field);
|
|
407
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
408
|
+
return parsed;
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return void 0;
|
|
414
|
+
};
|
|
415
|
+
const id = `wiki-template-${getString(tiddler.title).replace(/[^a-zA-Z0-9-_]/g, "-")}`;
|
|
416
|
+
const name = getString(tiddler.caption) || getString(tiddler.title);
|
|
417
|
+
const description = getString(tiddler.description) || `Agent template from ${workspaceName || "wiki"}`;
|
|
418
|
+
const modelConfigRaw = parseJSON(tiddler.ai_api_config);
|
|
419
|
+
const rawAsRecord = modelConfigRaw && typeof modelConfigRaw === "object" && !Array.isArray(modelConfigRaw) ? modelConfigRaw : void 0;
|
|
420
|
+
const modelConfig = rawAsRecord ? {
|
|
421
|
+
provider: typeof rawAsRecord.provider === "string" ? rawAsRecord.provider : "",
|
|
422
|
+
model: typeof rawAsRecord.model === "string" ? rawAsRecord.model : ""
|
|
423
|
+
} : void 0;
|
|
424
|
+
const toolsRaw = parseJSON(tiddler.agent_tools);
|
|
425
|
+
const toolNames = Array.isArray(toolsRaw) ? toolsRaw.filter((t) => typeof t?.toolId === "string").map((t) => String(t.toolId)) : [];
|
|
426
|
+
return {
|
|
427
|
+
id,
|
|
428
|
+
name,
|
|
429
|
+
description,
|
|
430
|
+
systemPrompt: "",
|
|
431
|
+
tools: toolNames,
|
|
432
|
+
modelConfig,
|
|
433
|
+
// Keep the original config as promptSchema for the UI editor
|
|
434
|
+
promptSchema: agentFrameworkConfig,
|
|
435
|
+
agentFrameworkConfig: {
|
|
436
|
+
prompts: Array.isArray(agentFrameworkConfig.prompts) ? agentFrameworkConfig.prompts : [],
|
|
437
|
+
plugins: Array.isArray(agentFrameworkConfig.plugins) ? agentFrameworkConfig.plugins : [],
|
|
438
|
+
response: Array.isArray(agentFrameworkConfig.response) ? agentFrameworkConfig.response : void 0
|
|
439
|
+
},
|
|
440
|
+
version: "1"
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/agent-management/AgentSessionController.ts
|
|
445
|
+
var AgentSessionController = class {
|
|
446
|
+
options;
|
|
447
|
+
listener = null;
|
|
448
|
+
snapshot = {
|
|
449
|
+
agent: null,
|
|
450
|
+
loading: false,
|
|
451
|
+
error: null,
|
|
452
|
+
messages: [],
|
|
453
|
+
orderedMessageIds: [],
|
|
454
|
+
streamingMessageIds: /* @__PURE__ */ new Set()
|
|
455
|
+
};
|
|
456
|
+
unsubAgentUpdates = null;
|
|
457
|
+
unsubMessages = null;
|
|
458
|
+
pollingTimer = null;
|
|
459
|
+
agentId = null;
|
|
460
|
+
constructor(options) {
|
|
461
|
+
this.options = {
|
|
462
|
+
pollInterval: 500,
|
|
463
|
+
...options
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
// ── Lifecycle ─────────────────────────────────────────────────
|
|
467
|
+
/**
|
|
468
|
+
* Start a session for the given agent ID.
|
|
469
|
+
* Fetches the agent, subscribes to live updates, and loads messages.
|
|
470
|
+
*/
|
|
471
|
+
async start(agentId) {
|
|
472
|
+
this.agentId = agentId;
|
|
473
|
+
this.emitPartial({ loading: true, error: null });
|
|
474
|
+
try {
|
|
475
|
+
const [agent, rawMessages] = await Promise.all([
|
|
476
|
+
this.options.agentInstanceClient.fetchAgent(agentId),
|
|
477
|
+
this.options.conversationClient.getMessages(agentId)
|
|
478
|
+
]);
|
|
479
|
+
const orderedMessageIds = rawMessages.map((m) => m.messageId);
|
|
480
|
+
const messagesMap = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const m of rawMessages) messagesMap.set(m.messageId, m);
|
|
482
|
+
this.emitPartial({
|
|
483
|
+
agent,
|
|
484
|
+
loading: false,
|
|
485
|
+
messages: rawMessages,
|
|
486
|
+
orderedMessageIds,
|
|
487
|
+
streamingMessageIds: /* @__PURE__ */ new Set()
|
|
488
|
+
});
|
|
489
|
+
this.unsubAgentUpdates = this.options.agentInstanceClient.subscribeToUpdates(
|
|
490
|
+
agentId,
|
|
491
|
+
this.handleAgentUpdate
|
|
492
|
+
);
|
|
493
|
+
this.unsubMessages = this.options.conversationClient.subscribeToMessages(
|
|
494
|
+
agentId,
|
|
495
|
+
this.handleNewMessage
|
|
496
|
+
);
|
|
497
|
+
this.startPolling(agentId);
|
|
498
|
+
} catch (error_) {
|
|
499
|
+
this.emitPartial({
|
|
500
|
+
loading: false,
|
|
501
|
+
error: error_ instanceof Error ? error_ : new Error(String(error_))
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/** Stop the session and clean up subscriptions. */
|
|
506
|
+
stop() {
|
|
507
|
+
this.stopPolling();
|
|
508
|
+
this.unsubAgentUpdates?.();
|
|
509
|
+
this.unsubAgentUpdates = null;
|
|
510
|
+
this.unsubMessages?.();
|
|
511
|
+
this.unsubMessages = null;
|
|
512
|
+
this.agentId = null;
|
|
513
|
+
}
|
|
514
|
+
// ── Actions ───────────────────────────────────────────────────
|
|
515
|
+
/** Send a user message. */
|
|
516
|
+
async sendMessage(content, file, wikiTiddlers) {
|
|
517
|
+
if (!this.agentId) return;
|
|
518
|
+
this.emitPartial({ error: null });
|
|
519
|
+
try {
|
|
520
|
+
await this.options.conversationClient.sendMessage(this.agentId, content, file, wikiTiddlers);
|
|
521
|
+
} catch (error_) {
|
|
522
|
+
this.emitPartial({
|
|
523
|
+
error: error_ instanceof Error ? error_ : new Error(String(error_))
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/** Cancel the current agent operation. */
|
|
528
|
+
async cancel() {
|
|
529
|
+
if (!this.agentId) return;
|
|
530
|
+
try {
|
|
531
|
+
await this.options.agentInstanceClient.cancelAgent(this.agentId);
|
|
532
|
+
} catch (error_) {
|
|
533
|
+
this.emitPartial({
|
|
534
|
+
error: error_ instanceof Error ? error_ : new Error(String(error_))
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/** Delete a turn starting at the given user message ID. */
|
|
539
|
+
async deleteTurn(userMessageId) {
|
|
540
|
+
return this.options.conversationClient.deleteTurn(userMessageId);
|
|
541
|
+
}
|
|
542
|
+
/** Retry a turn starting at the given user message ID. */
|
|
543
|
+
async retryTurn(userMessageId) {
|
|
544
|
+
return this.options.conversationClient.retryTurn(userMessageId);
|
|
545
|
+
}
|
|
546
|
+
// ── Subscription ──────────────────────────────────────────────
|
|
547
|
+
/** Subscribe to snapshot changes. Returns unsubscribe function. */
|
|
548
|
+
subscribe(listener) {
|
|
549
|
+
this.listener = listener;
|
|
550
|
+
listener({ ...this.snapshot, streamingMessageIds: new Set(this.snapshot.streamingMessageIds) });
|
|
551
|
+
return () => {
|
|
552
|
+
if (this.listener === listener) this.listener = null;
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/** Get current snapshot without subscribing. */
|
|
556
|
+
getSnapshot() {
|
|
557
|
+
return this.snapshot;
|
|
558
|
+
}
|
|
559
|
+
// ── Private helpers ───────────────────────────────────────────
|
|
560
|
+
emitPartial(partial) {
|
|
561
|
+
this.snapshot = { ...this.snapshot, ...partial };
|
|
562
|
+
if (this.listener) {
|
|
563
|
+
this.listener({
|
|
564
|
+
...this.snapshot,
|
|
565
|
+
streamingMessageIds: new Set(this.snapshot.streamingMessageIds)
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
handleAgentUpdate = (update) => {
|
|
570
|
+
if (this.snapshot.agent) {
|
|
571
|
+
this.emitPartial({ agent: { ...this.snapshot.agent, ...update } });
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
handleNewMessage = (message) => {
|
|
575
|
+
const messages = [...this.snapshot.messages.filter((m) => m.messageId !== message.messageId), message];
|
|
576
|
+
const orderedMessageIds = [.../* @__PURE__ */ new Set([...this.snapshot.orderedMessageIds, message.messageId])];
|
|
577
|
+
this.emitPartial({ messages, orderedMessageIds });
|
|
578
|
+
};
|
|
579
|
+
startPolling(agentId) {
|
|
580
|
+
this.stopPolling();
|
|
581
|
+
this.pollingTimer = setInterval(async () => {
|
|
582
|
+
try {
|
|
583
|
+
const agent = await this.options.agentInstanceClient.fetchAgent(agentId);
|
|
584
|
+
this.emitPartial({ agent });
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
}, this.options.pollInterval);
|
|
588
|
+
}
|
|
589
|
+
stopPolling() {
|
|
590
|
+
if (this.pollingTimer !== null) {
|
|
591
|
+
clearInterval(this.pollingTimer);
|
|
592
|
+
this.pollingTimer = null;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/agent-management/AgentDefinitionEditorController.ts
|
|
598
|
+
var AgentDefinitionEditorController = class {
|
|
599
|
+
options;
|
|
600
|
+
listener = null;
|
|
601
|
+
state = {
|
|
602
|
+
agentDefinition: null,
|
|
603
|
+
agentName: "",
|
|
604
|
+
previewAgentId: null,
|
|
605
|
+
isLoading: false,
|
|
606
|
+
isSaving: false,
|
|
607
|
+
promptSchema: null
|
|
608
|
+
};
|
|
609
|
+
autosaveTimer = null;
|
|
610
|
+
constructor(options) {
|
|
611
|
+
this.options = {
|
|
612
|
+
autosaveDebounceMs: 1e3,
|
|
613
|
+
...options
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
// ── Lifecycle ─────────────────────────────────────────────────
|
|
617
|
+
/** Load an agent definition and its prompt schema. */
|
|
618
|
+
async loadDefinition(definitionId) {
|
|
619
|
+
this.emit({ isLoading: true });
|
|
620
|
+
try {
|
|
621
|
+
const [definition, previewAgentId] = await Promise.all([
|
|
622
|
+
this.options.definitionRepository.getAgentDef(definitionId),
|
|
623
|
+
this.findOrCreatePreviewAgent(definitionId)
|
|
624
|
+
]);
|
|
625
|
+
if (!definition) {
|
|
626
|
+
this.emit({ isLoading: false });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
let promptSchema = null;
|
|
630
|
+
if (definition.agentFrameworkID) {
|
|
631
|
+
try {
|
|
632
|
+
promptSchema = await this.options.agentInstanceClient.getFrameworkConfigSchema(
|
|
633
|
+
definition.agentFrameworkID
|
|
634
|
+
);
|
|
635
|
+
} catch {
|
|
636
|
+
promptSchema = null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
this.state = {
|
|
640
|
+
agentDefinition: definition,
|
|
641
|
+
agentName: definition.name ?? "",
|
|
642
|
+
previewAgentId,
|
|
643
|
+
isLoading: false,
|
|
644
|
+
isSaving: false,
|
|
645
|
+
promptSchema
|
|
646
|
+
};
|
|
647
|
+
this.emit(this.state);
|
|
648
|
+
} catch (_error) {
|
|
649
|
+
this.emit({ isLoading: false });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/** Update the agent definition field. Triggers debounced autosave. */
|
|
653
|
+
updateDefinition(change) {
|
|
654
|
+
if (!this.state.agentDefinition) return;
|
|
655
|
+
const updated = { ...this.state.agentDefinition, ...change };
|
|
656
|
+
this.state = { ...this.state, agentDefinition: updated, agentName: updated.name ?? "" };
|
|
657
|
+
this.emit({ agentDefinition: updated, agentName: this.state.agentName });
|
|
658
|
+
this.scheduleAutosave(updated);
|
|
659
|
+
}
|
|
660
|
+
/** Force an immediate save to the repository. */
|
|
661
|
+
async saveNow() {
|
|
662
|
+
if (!this.state.agentDefinition) return;
|
|
663
|
+
this.emit({ isSaving: true });
|
|
664
|
+
try {
|
|
665
|
+
await this.options.definitionRepository.updateAgentDef({
|
|
666
|
+
id: this.state.agentDefinition.id,
|
|
667
|
+
name: this.state.agentName,
|
|
668
|
+
description: this.state.agentDefinition.description,
|
|
669
|
+
agentFrameworkConfig: this.state.agentDefinition.agentFrameworkConfig,
|
|
670
|
+
aiApiConfig: this.state.agentDefinition.aiApiConfig,
|
|
671
|
+
agentTools: this.state.agentDefinition.agentTools,
|
|
672
|
+
heartbeat: this.state.agentDefinition.heartbeat
|
|
673
|
+
});
|
|
674
|
+
} catch (_error) {
|
|
675
|
+
} finally {
|
|
676
|
+
this.emit({ isSaving: false });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** Reload the prompt schema for a framework ID. */
|
|
680
|
+
async reloadSchema(frameworkId) {
|
|
681
|
+
try {
|
|
682
|
+
const schema = await this.options.agentInstanceClient.getFrameworkConfigSchema(frameworkId);
|
|
683
|
+
this.emit({ promptSchema: schema });
|
|
684
|
+
} catch (_error) {
|
|
685
|
+
this.emit({ promptSchema: null });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// ── Preview agent lifecycle ───────────────────────────────────
|
|
689
|
+
/** Get or create a preview agent instance tied to this definition. */
|
|
690
|
+
async findOrCreatePreviewAgent(definitionId) {
|
|
691
|
+
try {
|
|
692
|
+
const result = await this.options.agentInstanceClient.createAgent(definitionId, {
|
|
693
|
+
preview: true
|
|
694
|
+
});
|
|
695
|
+
return result.id;
|
|
696
|
+
} catch (_error) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// ── Scheduled tasks ───────────────────────────────────────────
|
|
701
|
+
async loadScheduledTasks() {
|
|
702
|
+
if (!this.state.previewAgentId) return [];
|
|
703
|
+
return this.options.scheduledTaskClient.listScheduledTasksForAgent(
|
|
704
|
+
this.state.previewAgentId
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
async saveScheduledTask(input) {
|
|
708
|
+
return this.options.scheduledTaskClient.createScheduledTask(input);
|
|
709
|
+
}
|
|
710
|
+
async deleteScheduledTask(taskId) {
|
|
711
|
+
return this.options.scheduledTaskClient.deleteScheduledTask(taskId);
|
|
712
|
+
}
|
|
713
|
+
// ── Subscription ──────────────────────────────────────────────
|
|
714
|
+
subscribe(listener) {
|
|
715
|
+
this.listener = listener;
|
|
716
|
+
listener(this.state);
|
|
717
|
+
return () => {
|
|
718
|
+
if (this.listener === listener) this.listener = null;
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
getState() {
|
|
722
|
+
return this.state;
|
|
723
|
+
}
|
|
724
|
+
// ── Private ───────────────────────────────────────────────────
|
|
725
|
+
emit(change) {
|
|
726
|
+
this.listener?.(change);
|
|
727
|
+
}
|
|
728
|
+
scheduleAutosave(definition) {
|
|
729
|
+
if (this.autosaveTimer !== null) clearTimeout(this.autosaveTimer);
|
|
730
|
+
this.autosaveTimer = setTimeout(() => {
|
|
731
|
+
this.options.definitionRepository.updateAgentDef({
|
|
732
|
+
id: definition.id,
|
|
733
|
+
name: definition.name,
|
|
734
|
+
description: definition.description,
|
|
735
|
+
agentFrameworkConfig: definition.agentFrameworkConfig,
|
|
736
|
+
aiApiConfig: definition.aiApiConfig,
|
|
737
|
+
agentTools: definition.agentTools,
|
|
738
|
+
heartbeat: definition.heartbeat
|
|
739
|
+
}).catch(() => {
|
|
740
|
+
});
|
|
741
|
+
}, this.options.autosaveDebounceMs);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/agent-management/AgentCreationController.ts
|
|
746
|
+
var AgentCreationController = class {
|
|
747
|
+
options;
|
|
748
|
+
listener = null;
|
|
749
|
+
state = {
|
|
750
|
+
currentStep: 0,
|
|
751
|
+
agentName: "",
|
|
752
|
+
selectedTemplate: null,
|
|
753
|
+
temporaryAgentDefinition: null,
|
|
754
|
+
previewAgentId: null,
|
|
755
|
+
isLoading: false,
|
|
756
|
+
promptSchema: null
|
|
757
|
+
};
|
|
758
|
+
constructor(options) {
|
|
759
|
+
this.options = {
|
|
760
|
+
defaultFrameworkId: "memeloopAgentToolLoop",
|
|
761
|
+
...options
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
/** Initialize the creation wizard, optionally from a saved tab state. */
|
|
765
|
+
async start(definitionId, templateDefinitionId) {
|
|
766
|
+
this.emit({ isLoading: true });
|
|
767
|
+
try {
|
|
768
|
+
if (definitionId) {
|
|
769
|
+
const temporaryDefinition = await this.options.definitionRepository.getAgentDef(definitionId);
|
|
770
|
+
if (temporaryDefinition) {
|
|
771
|
+
this.state = {
|
|
772
|
+
...this.state,
|
|
773
|
+
temporaryAgentDefinition: temporaryDefinition,
|
|
774
|
+
agentName: temporaryDefinition.name ?? "",
|
|
775
|
+
isLoading: false
|
|
776
|
+
};
|
|
777
|
+
this.emit(this.state);
|
|
778
|
+
if (templateDefinitionId) {
|
|
779
|
+
const template = await this.options.definitionRepository.getAgentDef(templateDefinitionId);
|
|
780
|
+
if (template) {
|
|
781
|
+
this.state = { ...this.state, selectedTemplate: template };
|
|
782
|
+
this.emit(this.state);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
this.emit({ isLoading: false });
|
|
789
|
+
} catch {
|
|
790
|
+
this.emit({ isLoading: false });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/** Select a template for the new agent. */
|
|
794
|
+
selectTemplate(template) {
|
|
795
|
+
this.state = { ...this.state, selectedTemplate: template };
|
|
796
|
+
this.emit(this.state);
|
|
797
|
+
}
|
|
798
|
+
/** Update the agent name. */
|
|
799
|
+
setAgentName(name) {
|
|
800
|
+
this.state = { ...this.state, agentName: name };
|
|
801
|
+
this.emit(this.state);
|
|
802
|
+
}
|
|
803
|
+
/** Advance to the next wizard step. */
|
|
804
|
+
nextStep() {
|
|
805
|
+
this.state = { ...this.state, currentStep: Math.min(this.state.currentStep + 1, 2) };
|
|
806
|
+
this.emit(this.state);
|
|
807
|
+
}
|
|
808
|
+
/** Go back to the previous wizard step. */
|
|
809
|
+
previousStep() {
|
|
810
|
+
this.state = { ...this.state, currentStep: Math.max(this.state.currentStep - 1, 0) };
|
|
811
|
+
this.emit(this.state);
|
|
812
|
+
}
|
|
813
|
+
/** Set the current wizard step directly. */
|
|
814
|
+
setStep(step) {
|
|
815
|
+
this.state = { ...this.state, currentStep: Math.max(0, Math.min(step, 2)) };
|
|
816
|
+
this.emit(this.state);
|
|
817
|
+
}
|
|
818
|
+
/** Update the temporary agent definition. */
|
|
819
|
+
updateTemporaryDefinition(change) {
|
|
820
|
+
const updated = this.state.temporaryAgentDefinition ? { ...this.state.temporaryAgentDefinition, ...change } : null;
|
|
821
|
+
this.state = { ...this.state, temporaryAgentDefinition: updated };
|
|
822
|
+
this.emit(this.state);
|
|
823
|
+
}
|
|
824
|
+
/** Set the prompt schema for the selected framework. */
|
|
825
|
+
setPromptSchema(schema) {
|
|
826
|
+
this.state = { ...this.state, promptSchema: schema };
|
|
827
|
+
this.emit(this.state);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Create a preview agent instance for the chat-preview step.
|
|
831
|
+
* Should be called when entering step 2 (or the chat preview step).
|
|
832
|
+
*/
|
|
833
|
+
async createPreviewAgent(definitionId) {
|
|
834
|
+
this.emit({ isLoading: true });
|
|
835
|
+
try {
|
|
836
|
+
const result = await this.options.agentInstanceClient.createAgent(definitionId, {
|
|
837
|
+
preview: true
|
|
838
|
+
});
|
|
839
|
+
this.state = { ...this.state, previewAgentId: result.id, isLoading: false };
|
|
840
|
+
this.emit(this.state);
|
|
841
|
+
} catch {
|
|
842
|
+
this.emit({ isLoading: false });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Finalize creation: persist the temporary definition and create the actual
|
|
847
|
+
* agent instance. Returns the final agent instance ID, or null on failure.
|
|
848
|
+
*/
|
|
849
|
+
async finalize() {
|
|
850
|
+
if (!this.state.temporaryAgentDefinition) return null;
|
|
851
|
+
try {
|
|
852
|
+
const definition = await this.options.definitionRepository.createAgentDef(
|
|
853
|
+
this.state.temporaryAgentDefinition
|
|
854
|
+
);
|
|
855
|
+
const instance = await this.options.agentInstanceClient.createAgent(definition.id);
|
|
856
|
+
return instance.id;
|
|
857
|
+
} catch {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/** Clean up temporary resources. */
|
|
862
|
+
async cleanup() {
|
|
863
|
+
if (this.state.previewAgentId) {
|
|
864
|
+
try {
|
|
865
|
+
await this.options.agentInstanceClient.deleteAgent(this.state.previewAgentId);
|
|
866
|
+
} catch {
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// ── Subscription ──────────────────────────────────────────────
|
|
871
|
+
subscribe(listener) {
|
|
872
|
+
this.listener = listener;
|
|
873
|
+
listener({ ...this.state });
|
|
874
|
+
return () => {
|
|
875
|
+
if (this.listener === listener) this.listener = null;
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
getState() {
|
|
879
|
+
return this.state;
|
|
880
|
+
}
|
|
881
|
+
// ── Private ───────────────────────────────────────────────────
|
|
882
|
+
emit(change) {
|
|
883
|
+
this.state = { ...this.state, ...change };
|
|
884
|
+
this.listener?.({ ...this.state });
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// src/agent-management/PromptPreviewController.ts
|
|
889
|
+
var PromptPreviewController = class {
|
|
890
|
+
options;
|
|
891
|
+
listener = null;
|
|
892
|
+
state = {
|
|
893
|
+
open: false,
|
|
894
|
+
baseMode: "preview",
|
|
895
|
+
activeTab: "tree",
|
|
896
|
+
loading: false,
|
|
897
|
+
progress: 0,
|
|
898
|
+
currentStep: "",
|
|
899
|
+
currentPlugin: null,
|
|
900
|
+
result: null,
|
|
901
|
+
lastUpdated: null,
|
|
902
|
+
formFieldsToScrollTo: []
|
|
903
|
+
};
|
|
904
|
+
constructor(options) {
|
|
905
|
+
this.options = options;
|
|
906
|
+
}
|
|
907
|
+
/** Open the dialog with an optional base mode. */
|
|
908
|
+
open(baseMode) {
|
|
909
|
+
this.state = {
|
|
910
|
+
...this.state,
|
|
911
|
+
open: true,
|
|
912
|
+
baseMode: baseMode ?? "preview",
|
|
913
|
+
loading: false,
|
|
914
|
+
progress: 0,
|
|
915
|
+
currentStep: "Starting...",
|
|
916
|
+
currentPlugin: null,
|
|
917
|
+
result: null,
|
|
918
|
+
lastUpdated: null,
|
|
919
|
+
formFieldsToScrollTo: []
|
|
920
|
+
};
|
|
921
|
+
this.emit();
|
|
922
|
+
}
|
|
923
|
+
/** Close the dialog. */
|
|
924
|
+
close() {
|
|
925
|
+
this.state = {
|
|
926
|
+
...this.state,
|
|
927
|
+
open: false,
|
|
928
|
+
baseMode: "preview",
|
|
929
|
+
lastUpdated: null,
|
|
930
|
+
formFieldsToScrollTo: []
|
|
931
|
+
};
|
|
932
|
+
this.emit();
|
|
933
|
+
}
|
|
934
|
+
/** Set the active tab in the dialog. */
|
|
935
|
+
setActiveTab(tab) {
|
|
936
|
+
this.state = { ...this.state, activeTab: tab };
|
|
937
|
+
this.emit();
|
|
938
|
+
}
|
|
939
|
+
/** Set form fields to scroll to (for edit mode navigation). */
|
|
940
|
+
setFormFieldsToScrollTo(fieldPaths) {
|
|
941
|
+
this.state = { ...this.state, formFieldsToScrollTo: fieldPaths };
|
|
942
|
+
this.emit();
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Generate a preview of the prompt tree.
|
|
946
|
+
* @param agentFrameworkConfig The agent's framework configuration
|
|
947
|
+
* @param messages Current conversation messages
|
|
948
|
+
* @param inputText Optional input text to include in the preview
|
|
949
|
+
*/
|
|
950
|
+
async generate(agentFrameworkConfig, messages, inputText) {
|
|
951
|
+
this.state = {
|
|
952
|
+
...this.state,
|
|
953
|
+
loading: true,
|
|
954
|
+
progress: 0,
|
|
955
|
+
currentStep: "Preparing..."
|
|
956
|
+
};
|
|
957
|
+
this.emit();
|
|
958
|
+
try {
|
|
959
|
+
const result = await this.options.previewClient.generatePreview(
|
|
960
|
+
agentFrameworkConfig,
|
|
961
|
+
messages,
|
|
962
|
+
inputText,
|
|
963
|
+
(progress) => {
|
|
964
|
+
this.state = {
|
|
965
|
+
...this.state,
|
|
966
|
+
progress: progress.progress,
|
|
967
|
+
currentStep: progress.step,
|
|
968
|
+
currentPlugin: progress.currentPlugin ?? null
|
|
969
|
+
};
|
|
970
|
+
this.emit();
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
this.state = {
|
|
974
|
+
...this.state,
|
|
975
|
+
loading: false,
|
|
976
|
+
progress: 1,
|
|
977
|
+
currentStep: "Complete",
|
|
978
|
+
currentPlugin: null,
|
|
979
|
+
result,
|
|
980
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
981
|
+
};
|
|
982
|
+
this.emit();
|
|
983
|
+
return result;
|
|
984
|
+
} catch (_error) {
|
|
985
|
+
this.state = {
|
|
986
|
+
...this.state,
|
|
987
|
+
loading: false,
|
|
988
|
+
progress: 0,
|
|
989
|
+
currentStep: "Error occurred",
|
|
990
|
+
currentPlugin: null,
|
|
991
|
+
result: null
|
|
992
|
+
};
|
|
993
|
+
this.emit();
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// ── Subscription ──────────────────────────────────────────────
|
|
998
|
+
subscribe(listener) {
|
|
999
|
+
this.listener = listener;
|
|
1000
|
+
listener({ ...this.state });
|
|
1001
|
+
return () => {
|
|
1002
|
+
if (this.listener === listener) this.listener = null;
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
getState() {
|
|
1006
|
+
return this.state;
|
|
1007
|
+
}
|
|
1008
|
+
// ── Private ───────────────────────────────────────────────────
|
|
1009
|
+
emit() {
|
|
1010
|
+
this.listener?.({ ...this.state });
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// src/network/gitProxyUrlPolicy.ts
|
|
1015
|
+
function ipv4Parts(host) {
|
|
1016
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
1017
|
+
if (!m) return null;
|
|
1018
|
+
return m.slice(1, 5).map((x) => Number(x));
|
|
1019
|
+
}
|
|
1020
|
+
function isPrivateOrLoopbackIpv4(host) {
|
|
1021
|
+
const p = ipv4Parts(host);
|
|
1022
|
+
if (!p || p.some((n) => n > 255)) return false;
|
|
1023
|
+
const [a, b] = p;
|
|
1024
|
+
if (a === 127 || a === 0) return true;
|
|
1025
|
+
if (a === 10) return true;
|
|
1026
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1027
|
+
if (a === 192 && b === 168) return true;
|
|
1028
|
+
if (a === 169 && b === 254) return true;
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
function gitProxyTargetBlockReason(url) {
|
|
1032
|
+
if (url.username || url.password) {
|
|
1033
|
+
return "credentials_in_url";
|
|
1034
|
+
}
|
|
1035
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1036
|
+
return "unsupported_protocol";
|
|
1037
|
+
}
|
|
1038
|
+
const host = url.hostname.toLowerCase();
|
|
1039
|
+
if (host === "localhost" || host.endsWith(".localhost") || host === "0.0.0.0" || host === "[::]" || host === "::") {
|
|
1040
|
+
return "loopback_or_wildcard";
|
|
1041
|
+
}
|
|
1042
|
+
if (host.startsWith("127.")) {
|
|
1043
|
+
return "loopback_ipv4";
|
|
1044
|
+
}
|
|
1045
|
+
if (isPrivateOrLoopbackIpv4(host)) {
|
|
1046
|
+
return "private_ipv4";
|
|
1047
|
+
}
|
|
1048
|
+
if (host.includes(":") && !host.startsWith("[")) {
|
|
1049
|
+
const lower = host.toLowerCase();
|
|
1050
|
+
if (lower.startsWith("fe80:") || lower.startsWith("fc") || lower.startsWith("fd")) {
|
|
1051
|
+
return "private_ipv6";
|
|
1052
|
+
}
|
|
1053
|
+
if (lower === "::1") {
|
|
1054
|
+
return "loopback_ipv6";
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
1058
|
+
const inner = host.slice(1, -1).toLowerCase();
|
|
1059
|
+
if (inner === "::1" || inner.startsWith("fe80:") || inner.startsWith("fc") || inner.startsWith("fd")) {
|
|
1060
|
+
return "blocked_ipv6_literal";
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1064
|
+
if (h === "metadata.google.internal" || h.endsWith(".internal")) {
|
|
1065
|
+
return "internal_hostname";
|
|
1066
|
+
}
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/network/uri.ts
|
|
1071
|
+
var PREFIX = "memeloop://node/";
|
|
1072
|
+
function buildMemeloopFileUri(nodeId, filePath) {
|
|
1073
|
+
const normalized = filePath.replace(/^\/+/, "");
|
|
1074
|
+
const segments = normalized.split("/").filter(Boolean).map(encodeURIComponent);
|
|
1075
|
+
return `memeloop://node/${encodeURIComponent(nodeId)}/file/${segments.join("/")}`;
|
|
1076
|
+
}
|
|
1077
|
+
var buildMemeloopUri = buildMemeloopFileUri;
|
|
1078
|
+
function parseMemeloopUri(uri) {
|
|
1079
|
+
const u = uri.trim();
|
|
1080
|
+
if (!u.startsWith(PREFIX)) return null;
|
|
1081
|
+
const rest = u.slice(PREFIX.length);
|
|
1082
|
+
const slash = rest.indexOf("/");
|
|
1083
|
+
if (slash <= 0) return null;
|
|
1084
|
+
let nodeId;
|
|
1085
|
+
try {
|
|
1086
|
+
nodeId = decodeURIComponent(rest.slice(0, slash));
|
|
1087
|
+
} catch {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
const afterNode = rest.slice(slash + 1);
|
|
1091
|
+
if (!afterNode.startsWith("file/")) return null;
|
|
1092
|
+
const pathPart = afterNode.slice("file/".length);
|
|
1093
|
+
if (pathPart.length === 0) {
|
|
1094
|
+
return { scheme: "memeloop", kind: "file", nodeId, filePath: "" };
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
const filePath = pathPart.split("/").map(decodeURIComponent).join("/");
|
|
1098
|
+
return { scheme: "memeloop", kind: "file", nodeId, filePath };
|
|
1099
|
+
} catch {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/device-network/agentRuntimeRpcHandler.ts
|
|
1105
|
+
function createAgentRuntimeDeviceRpcHandler(options) {
|
|
1106
|
+
return async ({ method, parameters }) => {
|
|
1107
|
+
switch (method) {
|
|
1108
|
+
case "memeloop.agent.getDefinitions":
|
|
1109
|
+
return { definitions: await options.getAgentDefinitions?.() ?? [] };
|
|
1110
|
+
case "memeloop.agent.create": {
|
|
1111
|
+
const rpcParameters = objectParameter(parameters);
|
|
1112
|
+
return options.runtime.createAgent({
|
|
1113
|
+
definitionId: stringParameter(rpcParameters, "definitionId"),
|
|
1114
|
+
initialMessage: optionalStringParameter(rpcParameters, "initialMessage")
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
case "memeloop.agent.send": {
|
|
1118
|
+
const rpcParameters = objectParameter(parameters);
|
|
1119
|
+
await options.runtime.sendMessage({
|
|
1120
|
+
conversationId: stringParameter(rpcParameters, "conversationId"),
|
|
1121
|
+
message: stringParameter(rpcParameters, "message"),
|
|
1122
|
+
definitionId: optionalStringParameter(rpcParameters, "definitionId"),
|
|
1123
|
+
userMessage: chatMessageParameter(rpcParameters, "userMessage"),
|
|
1124
|
+
resumeSession: chatMessageArrayParameter(rpcParameters, "resumeSession")
|
|
1125
|
+
});
|
|
1126
|
+
return { ok: true };
|
|
1127
|
+
}
|
|
1128
|
+
case "memeloop.agent.runTurn": {
|
|
1129
|
+
const rpcParameters = objectParameter(parameters);
|
|
1130
|
+
const conversationId = stringParameter(rpcParameters, "conversationId");
|
|
1131
|
+
const definitionId = stringParameter(rpcParameters, "definitionId");
|
|
1132
|
+
await upsertConversationForRemoteTurn({
|
|
1133
|
+
storage: options.storage,
|
|
1134
|
+
conversationId,
|
|
1135
|
+
definitionId,
|
|
1136
|
+
conversation: conversationMetaParameter(rpcParameters, "conversation"),
|
|
1137
|
+
localNodeId: options.localNodeId
|
|
1138
|
+
});
|
|
1139
|
+
await options.runtime.sendMessage({
|
|
1140
|
+
conversationId,
|
|
1141
|
+
definitionId,
|
|
1142
|
+
message: stringParameter(rpcParameters, "message"),
|
|
1143
|
+
userMessage: chatMessageParameter(rpcParameters, "userMessage"),
|
|
1144
|
+
resumeSession: chatMessageArrayParameter(rpcParameters, "resumeSession")
|
|
1145
|
+
});
|
|
1146
|
+
return { ok: true, conversationId };
|
|
1147
|
+
}
|
|
1148
|
+
case "memeloop.agent.cancel": {
|
|
1149
|
+
const rpcParameters = objectParameter(parameters);
|
|
1150
|
+
await options.runtime.cancelAgent(stringParameter(rpcParameters, "conversationId"));
|
|
1151
|
+
return { ok: true };
|
|
1152
|
+
}
|
|
1153
|
+
case "memeloop.chat.pullAgentRunLog": {
|
|
1154
|
+
const rpcParameters = objectParameter(parameters);
|
|
1155
|
+
const conversationId = stringParameter(rpcParameters, "conversationId");
|
|
1156
|
+
const known = new Set(stringArrayParameter(rpcParameters, "knownMessageIds"));
|
|
1157
|
+
const messages = await options.storage.getMessages(conversationId, { mode: "full-content" });
|
|
1158
|
+
return { messages: messages.filter((message) => !known.has(message.messageId)) };
|
|
1159
|
+
}
|
|
1160
|
+
default:
|
|
1161
|
+
throw new Error(`rpc_method_not_found:${method}`);
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
async function upsertConversationForRemoteTurn(input) {
|
|
1166
|
+
if (input.conversation) {
|
|
1167
|
+
await input.storage.upsertConversationMetadata(input.conversation);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const existing = await input.storage.getConversationMeta(input.conversationId).catch(() => null);
|
|
1171
|
+
if (existing) return;
|
|
1172
|
+
const now = Date.now();
|
|
1173
|
+
await input.storage.upsertConversationMetadata({
|
|
1174
|
+
conversationId: input.conversationId,
|
|
1175
|
+
title: input.definitionId,
|
|
1176
|
+
lastMessagePreview: "",
|
|
1177
|
+
lastMessageTimestamp: now,
|
|
1178
|
+
messageCount: 0,
|
|
1179
|
+
originNodeId: input.localNodeId ?? "remote",
|
|
1180
|
+
definitionId: input.definitionId,
|
|
1181
|
+
isUserInitiated: true
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
function objectParameter(value) {
|
|
1185
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid_rpc_params");
|
|
1186
|
+
return value;
|
|
1187
|
+
}
|
|
1188
|
+
function stringParameter(parameters, key) {
|
|
1189
|
+
const value = parameters[key];
|
|
1190
|
+
if (typeof value !== "string" || value.length === 0) throw new Error("invalid_rpc_params");
|
|
1191
|
+
return value;
|
|
1192
|
+
}
|
|
1193
|
+
function optionalStringParameter(parameters, key) {
|
|
1194
|
+
const value = parameters[key];
|
|
1195
|
+
if (value === void 0) return void 0;
|
|
1196
|
+
if (typeof value !== "string") throw new Error("invalid_rpc_params");
|
|
1197
|
+
return value;
|
|
1198
|
+
}
|
|
1199
|
+
function stringArrayParameter(parameters, key) {
|
|
1200
|
+
const value = parameters[key];
|
|
1201
|
+
if (value === void 0) return [];
|
|
1202
|
+
if (!Array.isArray(value)) throw new Error("invalid_rpc_params");
|
|
1203
|
+
return value.filter((item) => typeof item === "string");
|
|
1204
|
+
}
|
|
1205
|
+
function chatMessageParameter(parameters, key) {
|
|
1206
|
+
const value = parameters[key];
|
|
1207
|
+
if (value === void 0) return void 0;
|
|
1208
|
+
if (!isChatMessage(value)) throw new Error("invalid_rpc_params");
|
|
1209
|
+
return value;
|
|
1210
|
+
}
|
|
1211
|
+
function chatMessageArrayParameter(parameters, key) {
|
|
1212
|
+
const value = parameters[key];
|
|
1213
|
+
if (value === void 0) return void 0;
|
|
1214
|
+
if (!Array.isArray(value)) throw new Error("invalid_rpc_params");
|
|
1215
|
+
return value.filter(isChatMessage);
|
|
1216
|
+
}
|
|
1217
|
+
function conversationMetaParameter(parameters, key) {
|
|
1218
|
+
const value = parameters[key];
|
|
1219
|
+
if (value === void 0) return void 0;
|
|
1220
|
+
if (!isConversationMetaLike(value)) throw new Error("invalid_rpc_params");
|
|
1221
|
+
return value;
|
|
1222
|
+
}
|
|
1223
|
+
function isChatMessage(value) {
|
|
1224
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1225
|
+
const record = value;
|
|
1226
|
+
return typeof record.messageId === "string" && typeof record.conversationId === "string" && typeof record.originNodeId === "string" && typeof record.timestamp === "number" && typeof record.lamportClock === "number" && typeof record.role === "string" && typeof record.content === "string";
|
|
1227
|
+
}
|
|
1228
|
+
function isConversationMetaLike(value) {
|
|
1229
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1230
|
+
const record = value;
|
|
1231
|
+
return typeof record.conversationId === "string" && typeof record.title === "string" && typeof record.lastMessageTimestamp === "number" && typeof record.messageCount === "number" && typeof record.originNodeId === "string" && typeof record.definitionId === "string" && typeof record.isUserInitiated === "boolean";
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/device-network/libp2pDeviceNetworkService.ts
|
|
1235
|
+
import { noise } from "@chainsafe/libp2p-noise";
|
|
1236
|
+
import { yamux } from "@chainsafe/libp2p-yamux";
|
|
1237
|
+
import { bootstrap } from "@libp2p/bootstrap";
|
|
1238
|
+
import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
|
|
1239
|
+
import { identify } from "@libp2p/identify";
|
|
1240
|
+
import { mdns } from "@libp2p/mdns";
|
|
1241
|
+
import { peerIdFromPrivateKey, peerIdFromPublicKey, peerIdFromString } from "@libp2p/peer-id";
|
|
1242
|
+
import { ping } from "@libp2p/ping";
|
|
1243
|
+
import { tcp } from "@libp2p/tcp";
|
|
1244
|
+
import { webSockets } from "@libp2p/websockets";
|
|
1245
|
+
import { multiaddr } from "@multiformats/multiaddr";
|
|
1246
|
+
import { createLibp2p } from "libp2p";
|
|
1247
|
+
|
|
1248
|
+
// src/device-network/libp2pSyncProtocol.ts
|
|
1249
|
+
var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v1";
|
|
1250
|
+
var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v1";
|
|
1251
|
+
var syncMethods = /* @__PURE__ */ new Set([
|
|
1252
|
+
"exchangeVersionVector",
|
|
1253
|
+
"pullMissingMetadata",
|
|
1254
|
+
"pullMissingMessages",
|
|
1255
|
+
"pullAttachmentBlob"
|
|
1256
|
+
]);
|
|
1257
|
+
function isLibp2pSyncRequest(value) {
|
|
1258
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1259
|
+
const record = value;
|
|
1260
|
+
return record.type === LIBP2P_SYNC_REQUEST_TYPE && typeof record.id === "string" && typeof record.method === "string" && syncMethods.has(record.method);
|
|
1261
|
+
}
|
|
1262
|
+
function isLibp2pSyncResponse(value) {
|
|
1263
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1264
|
+
const record = value;
|
|
1265
|
+
if (record.type !== LIBP2P_SYNC_RESPONSE_TYPE || typeof record.id !== "string" || typeof record.ok !== "boolean") return false;
|
|
1266
|
+
if (record.ok) return "result" in record;
|
|
1267
|
+
return typeof record.error === "string";
|
|
1268
|
+
}
|
|
1269
|
+
function isAttachmentBlobWire(value) {
|
|
1270
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1271
|
+
const record = value;
|
|
1272
|
+
return typeof record.dataBase64Url === "string" && typeof record.filename === "string" && typeof record.mimeType === "string" && typeof record.size === "number";
|
|
1273
|
+
}
|
|
1274
|
+
async function attachmentBlobFromWire(value) {
|
|
1275
|
+
if (value === null) return null;
|
|
1276
|
+
if (!isAttachmentBlobWire(value)) throw new Error("invalid_sync_response");
|
|
1277
|
+
const { fromString } = await import("uint8arrays");
|
|
1278
|
+
return {
|
|
1279
|
+
data: fromString(value.dataBase64Url, "base64url"),
|
|
1280
|
+
filename: value.filename,
|
|
1281
|
+
mimeType: value.mimeType,
|
|
1282
|
+
size: value.size
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/device-network/libp2pDeviceSyncTransport.ts
|
|
1287
|
+
var Libp2pDeviceSyncTransport = class {
|
|
1288
|
+
nodeId;
|
|
1289
|
+
deviceNetwork;
|
|
1290
|
+
grantProvider;
|
|
1291
|
+
constructor(options) {
|
|
1292
|
+
this.nodeId = options.nodeId;
|
|
1293
|
+
this.deviceNetwork = options.deviceNetwork;
|
|
1294
|
+
this.grantProvider = options.grantProvider;
|
|
1295
|
+
}
|
|
1296
|
+
listPeers() {
|
|
1297
|
+
return this.deviceNetwork.listDevices();
|
|
1298
|
+
}
|
|
1299
|
+
exchangeVersionVector(peerId, localVersion) {
|
|
1300
|
+
return this.request(peerId, "exchangeVersionVector", { localVersion });
|
|
1301
|
+
}
|
|
1302
|
+
pullMissingMetadata(peerId, sinceVersion) {
|
|
1303
|
+
return this.request(peerId, "pullMissingMetadata", { sinceVersion });
|
|
1304
|
+
}
|
|
1305
|
+
pullMissingMessages(peerId, conversationId, knownMessageIds) {
|
|
1306
|
+
return this.request(peerId, "pullMissingMessages", { conversationId, knownMessageIds });
|
|
1307
|
+
}
|
|
1308
|
+
async pullAttachmentBlob(peerId, contentHash) {
|
|
1309
|
+
const result = await this.request(peerId, "pullAttachmentBlob", { contentHash });
|
|
1310
|
+
return attachmentBlobFromWire(result);
|
|
1311
|
+
}
|
|
1312
|
+
async request(peerId, method, parameters) {
|
|
1313
|
+
const grant = await this.grantProvider?.(peerId);
|
|
1314
|
+
const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/1.0.0", grant);
|
|
1315
|
+
const request = {
|
|
1316
|
+
type: LIBP2P_SYNC_REQUEST_TYPE,
|
|
1317
|
+
id: crypto.randomUUID(),
|
|
1318
|
+
method,
|
|
1319
|
+
params: parameters,
|
|
1320
|
+
grant
|
|
1321
|
+
};
|
|
1322
|
+
await writeJsonToStream(stream, request);
|
|
1323
|
+
const response = await readJsonFromStream(stream);
|
|
1324
|
+
await stream.close();
|
|
1325
|
+
if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
|
|
1326
|
+
if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
|
|
1327
|
+
if (!response.ok) throw new Error(response.error);
|
|
1328
|
+
return response.result;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
async function writeJsonToStream(stream, message) {
|
|
1332
|
+
const payload = new TextEncoder().encode(JSON.stringify(message));
|
|
1333
|
+
await stream.sink((async function* () {
|
|
1334
|
+
yield payload;
|
|
1335
|
+
})());
|
|
1336
|
+
}
|
|
1337
|
+
async function readJsonFromStream(stream) {
|
|
1338
|
+
const reader = stream.source[Symbol.asyncIterator]();
|
|
1339
|
+
const result = await reader.next();
|
|
1340
|
+
if (result.done || !result.value) throw new Error("sync_response_missing");
|
|
1341
|
+
return JSON.parse(new TextDecoder().decode(result.value));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/device-network/libp2pRpcProtocol.ts
|
|
1345
|
+
var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v1";
|
|
1346
|
+
var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v1";
|
|
1347
|
+
function isLibp2pRpcRequest(value) {
|
|
1348
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1349
|
+
const record = value;
|
|
1350
|
+
return record.type === LIBP2P_RPC_REQUEST_TYPE && typeof record.id === "string" && typeof record.method === "string";
|
|
1351
|
+
}
|
|
1352
|
+
function isLibp2pRpcResponse(value) {
|
|
1353
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1354
|
+
const record = value;
|
|
1355
|
+
if (record.type !== LIBP2P_RPC_RESPONSE_TYPE || typeof record.id !== "string" || typeof record.ok !== "boolean") return false;
|
|
1356
|
+
if (record.ok) return "result" in record;
|
|
1357
|
+
return typeof record.error === "string";
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/device-network/localTrustDeviceAuthorizer.ts
|
|
1361
|
+
var PAIRING_PROTOCOL = "/memeloop/pairing/1.0.0";
|
|
1362
|
+
var LocalTrustDeviceAuthorizer = class {
|
|
1363
|
+
constructor(options = {}) {
|
|
1364
|
+
this.options = options;
|
|
1365
|
+
this.allowPairingProtocol = options.allowPairingProtocol ?? true;
|
|
1366
|
+
for (const record of options.trustedDevices ?? []) {
|
|
1367
|
+
this.trustedDevices.set(record.peerId, record);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
trustedDevices = /* @__PURE__ */ new Map();
|
|
1371
|
+
allowPairingProtocol;
|
|
1372
|
+
setTrustedDevice(record) {
|
|
1373
|
+
this.trustedDevices.set(record.peerId, record);
|
|
1374
|
+
}
|
|
1375
|
+
removeTrustedDevice(peerId) {
|
|
1376
|
+
this.trustedDevices.delete(peerId);
|
|
1377
|
+
}
|
|
1378
|
+
async canOpenProtocol(input) {
|
|
1379
|
+
const record = this.options.getTrustedDevice?.(input.remotePeerId) ?? this.trustedDevices.get(input.remotePeerId);
|
|
1380
|
+
if (record?.revokedAt) return false;
|
|
1381
|
+
if (input.protocol === PAIRING_PROTOCOL) return this.allowPairingProtocol;
|
|
1382
|
+
return record !== void 0;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// src/device-network/libp2pDeviceNetworkService.ts
|
|
1387
|
+
var emptyCapabilities = {
|
|
1388
|
+
tools: [],
|
|
1389
|
+
mcpServers: [],
|
|
1390
|
+
hasWiki: false,
|
|
1391
|
+
agentLoop: false,
|
|
1392
|
+
imChannels: [],
|
|
1393
|
+
wikis: []
|
|
1394
|
+
};
|
|
1395
|
+
var defaultListen = {
|
|
1396
|
+
addresses: ["/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/tcp/0/ws"]
|
|
1397
|
+
};
|
|
1398
|
+
var PAIRING_PROTOCOL2 = "/memeloop/pairing/1.0.0";
|
|
1399
|
+
var RPC_PROTOCOL = "/memeloop/rpc/1.0.0";
|
|
1400
|
+
var SYNC_PROTOCOL = "/memeloop/sync/1.0.0";
|
|
1401
|
+
var RELAY_ADMISSION_PROTOCOL = "/memeloop/relay-admission/1.0.0";
|
|
1402
|
+
var PAIRING_SESSION_TTL_MS = 5 * 6e4;
|
|
1403
|
+
var PAIRING_MESSAGE_MAX_BYTES = 64 * 1024;
|
|
1404
|
+
var RPC_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
|
|
1405
|
+
var SYNC_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
|
|
1406
|
+
var RELAY_ADMISSION_MESSAGE_MAX_BYTES = 64 * 1024;
|
|
1407
|
+
var RELAY_ADMISSION_REQUEST_TYPE = "memeloop-relay-admission-request-v1";
|
|
1408
|
+
var RELAY_ADMISSION_RESPONSE_TYPE = "memeloop-relay-admission-response-v1";
|
|
1409
|
+
var Libp2pDeviceNetworkService = class {
|
|
1410
|
+
constructor(options) {
|
|
1411
|
+
this.options = options;
|
|
1412
|
+
this.capabilities = options.capabilities ?? emptyCapabilities;
|
|
1413
|
+
for (const record of options.trustedDevices ?? []) {
|
|
1414
|
+
this.trustedDevices.set(record.peerId, record);
|
|
1415
|
+
}
|
|
1416
|
+
for (const address of options.bootstrapMultiaddrs ?? []) {
|
|
1417
|
+
const trimmed = address.trim();
|
|
1418
|
+
if (trimmed) this.bootstrapMultiaddrs.add(trimmed);
|
|
1419
|
+
}
|
|
1420
|
+
this.authorizer = options.authorizer ?? new LocalTrustDeviceAuthorizer({
|
|
1421
|
+
getTrustedDevice: (peerId) => this.trustedDevices.get(peerId)
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
libp2p;
|
|
1425
|
+
capabilities;
|
|
1426
|
+
discoveredDevices = /* @__PURE__ */ new Map();
|
|
1427
|
+
trustedDevices = /* @__PURE__ */ new Map();
|
|
1428
|
+
bootstrapMultiaddrs = /* @__PURE__ */ new Set();
|
|
1429
|
+
authorizer;
|
|
1430
|
+
pairingSessions = /* @__PURE__ */ new Map();
|
|
1431
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1432
|
+
pairingListeners = /* @__PURE__ */ new Set();
|
|
1433
|
+
async start() {
|
|
1434
|
+
if (this.libp2p) return;
|
|
1435
|
+
await this.loadTrustedDevicesFromStore();
|
|
1436
|
+
this.libp2p = await this.createNode();
|
|
1437
|
+
this.registerDiscoveryListeners(this.libp2p);
|
|
1438
|
+
await this.registerProtocolHandlers(this.libp2p);
|
|
1439
|
+
await this.libp2p.start();
|
|
1440
|
+
this.emitDevices();
|
|
1441
|
+
}
|
|
1442
|
+
async stop() {
|
|
1443
|
+
if (!this.libp2p) return;
|
|
1444
|
+
await this.libp2p.stop();
|
|
1445
|
+
this.libp2p = void 0;
|
|
1446
|
+
this.discoveredDevices.clear();
|
|
1447
|
+
this.emitDevices();
|
|
1448
|
+
}
|
|
1449
|
+
async getLocalDevice() {
|
|
1450
|
+
const node = this.requireNode();
|
|
1451
|
+
return {
|
|
1452
|
+
peerId: node.peerId.toString(),
|
|
1453
|
+
displayName: this.options.identity.deviceName,
|
|
1454
|
+
platform: this.options.identity.platform,
|
|
1455
|
+
trustMode: "local-pairing",
|
|
1456
|
+
reachability: {
|
|
1457
|
+
state: node.status === "started" ? "online" : "offline",
|
|
1458
|
+
paths: node.status === "started" ? ["lan"] : []
|
|
1459
|
+
},
|
|
1460
|
+
capabilities: this.capabilities,
|
|
1461
|
+
multiaddrs: this.getMultiaddrs(),
|
|
1462
|
+
lastSeen: Date.now()
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
async listDevices() {
|
|
1466
|
+
return this.toVisibleDevices();
|
|
1467
|
+
}
|
|
1468
|
+
observeDevices(listener) {
|
|
1469
|
+
this.listeners.add(listener);
|
|
1470
|
+
void this.listDevices().then(listener);
|
|
1471
|
+
return () => {
|
|
1472
|
+
this.listeners.delete(listener);
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
async listPairingSessions() {
|
|
1476
|
+
this.refreshPairingSessionExpiry();
|
|
1477
|
+
return [...this.pairingSessions.values()];
|
|
1478
|
+
}
|
|
1479
|
+
observePairingSessions(listener) {
|
|
1480
|
+
this.pairingListeners.add(listener);
|
|
1481
|
+
void this.listPairingSessions().then(listener);
|
|
1482
|
+
return () => {
|
|
1483
|
+
this.pairingListeners.delete(listener);
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
async requestLocalPairing(peerId, options = {}) {
|
|
1487
|
+
const requestNonce = randomPairingNonce();
|
|
1488
|
+
const createdAt = Date.now();
|
|
1489
|
+
const expiresAt = createdAt + PAIRING_SESSION_TTL_MS;
|
|
1490
|
+
const request = {
|
|
1491
|
+
type: "memeloop-local-pairing-request-v1",
|
|
1492
|
+
sessionId: `pairing-${this.options.identity.peerId}-${peerId}-${createdAt}-${requestNonce}`,
|
|
1493
|
+
requestNonce,
|
|
1494
|
+
createdAt,
|
|
1495
|
+
expiresAt,
|
|
1496
|
+
device: this.localPairingDevice()
|
|
1497
|
+
};
|
|
1498
|
+
const stream = await this.requireNode().dialProtocol(this.pairingDialTarget(peerId, options), PAIRING_PROTOCOL2);
|
|
1499
|
+
try {
|
|
1500
|
+
await writeJsonMessage(stream, request);
|
|
1501
|
+
const response = await readJsonMessage(stream);
|
|
1502
|
+
await stream.close();
|
|
1503
|
+
const session = await this.sessionFromPairingResponse(peerId, request, response);
|
|
1504
|
+
this.pairingSessions.set(session.sessionId, session);
|
|
1505
|
+
this.upsertDiscoveredDeviceFromPairing(session.remotePeerId, response.device);
|
|
1506
|
+
this.emitPairingSessions();
|
|
1507
|
+
this.emitDevices();
|
|
1508
|
+
return session;
|
|
1509
|
+
} catch (error2) {
|
|
1510
|
+
stream.abort(error2 instanceof Error ? error2 : new Error("pairing_request_failed"));
|
|
1511
|
+
throw error2;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async acceptPairing(sessionId) {
|
|
1515
|
+
const session = this.requirePairingSession(sessionId);
|
|
1516
|
+
if (session.expiresAt < Date.now()) {
|
|
1517
|
+
session.status = "expired";
|
|
1518
|
+
this.emitPairingSessions();
|
|
1519
|
+
throw new Error("pairing_session_expired");
|
|
1520
|
+
}
|
|
1521
|
+
if (session.status !== "pending") throw new Error("pairing_session_not_pending");
|
|
1522
|
+
const trustedDevice = {
|
|
1523
|
+
peerId: session.remotePeerId,
|
|
1524
|
+
publicKeyMultibase: session.remotePublicKeyMultibase,
|
|
1525
|
+
deviceName: session.remoteDeviceName,
|
|
1526
|
+
platform: session.remotePlatform,
|
|
1527
|
+
trustMode: "local-pairing",
|
|
1528
|
+
createdAt: Date.now(),
|
|
1529
|
+
lastSeen: Date.now()
|
|
1530
|
+
};
|
|
1531
|
+
this.trustedDevices.set(session.remotePeerId, trustedDevice);
|
|
1532
|
+
await this.options.trustStore?.saveTrustedDevice(trustedDevice);
|
|
1533
|
+
session.status = "accepted";
|
|
1534
|
+
this.updateDeviceTrust(session.remotePeerId, "local-pairing", true);
|
|
1535
|
+
this.emitPairingSessions();
|
|
1536
|
+
this.emitDevices();
|
|
1537
|
+
}
|
|
1538
|
+
async rejectPairing(sessionId) {
|
|
1539
|
+
const session = this.pairingSessions.get(sessionId);
|
|
1540
|
+
if (!session) return;
|
|
1541
|
+
session.status = "rejected";
|
|
1542
|
+
this.emitPairingSessions();
|
|
1543
|
+
}
|
|
1544
|
+
async removeTrustedDevice(peerId) {
|
|
1545
|
+
this.trustedDevices.delete(peerId);
|
|
1546
|
+
await this.options.trustStore?.removeTrustedDevice(peerId);
|
|
1547
|
+
this.updateDeviceTrust(peerId, "local-pairing", false);
|
|
1548
|
+
this.emitDevices();
|
|
1549
|
+
}
|
|
1550
|
+
async openStream(peerId, protocol, presentedGrant) {
|
|
1551
|
+
const authorized = await this.authorizer.canOpenProtocol({
|
|
1552
|
+
remotePeerId: peerId,
|
|
1553
|
+
protocol,
|
|
1554
|
+
direction: "outbound",
|
|
1555
|
+
presentedGrant
|
|
1556
|
+
});
|
|
1557
|
+
if (!authorized) throw new Error("device_not_trusted");
|
|
1558
|
+
const addresses = this.discoveredDevices.get(peerId)?.multiaddrs ?? [];
|
|
1559
|
+
const dialTarget = addresses.length > 0 ? addresses.map((address) => multiaddr(address)) : peerIdFromString(peerId);
|
|
1560
|
+
const stream = await this.requireNode().dialProtocol(dialTarget, protocol, {
|
|
1561
|
+
runOnLimitedConnection: true
|
|
1562
|
+
});
|
|
1563
|
+
return this.wrapStream(stream);
|
|
1564
|
+
}
|
|
1565
|
+
async sendRpc(peerId, method, parameters, presentedGrant) {
|
|
1566
|
+
const stream = await this.openStream(peerId, RPC_PROTOCOL, presentedGrant);
|
|
1567
|
+
const request = {
|
|
1568
|
+
type: LIBP2P_RPC_REQUEST_TYPE,
|
|
1569
|
+
id: crypto.randomUUID(),
|
|
1570
|
+
method,
|
|
1571
|
+
params: parameters,
|
|
1572
|
+
grant: presentedGrant
|
|
1573
|
+
};
|
|
1574
|
+
await writeStreamJson(stream, request);
|
|
1575
|
+
const response = await readStreamJson(stream);
|
|
1576
|
+
await stream.close();
|
|
1577
|
+
if (!isLibp2pRpcResponse(response)) throw new Error("invalid_rpc_response");
|
|
1578
|
+
if (response.id !== request.id) throw new Error("rpc_response_id_mismatch");
|
|
1579
|
+
if (!response.ok) throw new Error(response.error);
|
|
1580
|
+
return response.result;
|
|
1581
|
+
}
|
|
1582
|
+
async syncWithDevice(peerId, presentedGrant) {
|
|
1583
|
+
const authorized = await this.authorizer.canOpenProtocol({
|
|
1584
|
+
remotePeerId: peerId,
|
|
1585
|
+
protocol: "/memeloop/sync/1.0.0",
|
|
1586
|
+
direction: "outbound",
|
|
1587
|
+
presentedGrant
|
|
1588
|
+
});
|
|
1589
|
+
if (!authorized) throw new Error("device_not_trusted");
|
|
1590
|
+
if (this.options.syncStorage) {
|
|
1591
|
+
const transport = new Libp2pDeviceSyncTransport({
|
|
1592
|
+
nodeId: this.options.identity.peerId,
|
|
1593
|
+
deviceNetwork: this,
|
|
1594
|
+
grantProvider: async (remotePeerId) => remotePeerId === peerId ? presentedGrant : void 0
|
|
1595
|
+
});
|
|
1596
|
+
const peer = new PeerNodeSyncAdapter(peerId, transport);
|
|
1597
|
+
const engine = new ChatSyncEngine({
|
|
1598
|
+
nodeId: this.options.identity.peerId,
|
|
1599
|
+
storage: this.options.syncStorage,
|
|
1600
|
+
peers: () => [peer]
|
|
1601
|
+
});
|
|
1602
|
+
await engine.syncOnce();
|
|
1603
|
+
}
|
|
1604
|
+
return { ok: true, peerId, syncedAt: Date.now() };
|
|
1605
|
+
}
|
|
1606
|
+
getTrustedDevice(peerId) {
|
|
1607
|
+
return this.trustedDevices.get(peerId);
|
|
1608
|
+
}
|
|
1609
|
+
upsertTrustedDevice(record) {
|
|
1610
|
+
this.trustedDevices.set(record.peerId, record);
|
|
1611
|
+
this.updateDeviceTrust(record.peerId, record.trustMode, true);
|
|
1612
|
+
this.emitDevices();
|
|
1613
|
+
}
|
|
1614
|
+
upsertDiscoveredDevice(device) {
|
|
1615
|
+
const trustedRecord = this.trustedDevices.get(device.peerId);
|
|
1616
|
+
this.discoveredDevices.set(device.peerId, {
|
|
1617
|
+
...device,
|
|
1618
|
+
displayName: trustedRecord?.deviceName ?? device.displayName,
|
|
1619
|
+
platform: trustedRecord?.platform ?? device.platform,
|
|
1620
|
+
trustMode: trustedRecord?.trustMode ?? device.trustMode,
|
|
1621
|
+
trusted: device.trusted ?? trustedRecord !== void 0,
|
|
1622
|
+
lastSeen: device.lastSeen ?? trustedRecord?.lastSeen
|
|
1623
|
+
});
|
|
1624
|
+
this.emitDevices();
|
|
1625
|
+
}
|
|
1626
|
+
async configureRelayReservation(token) {
|
|
1627
|
+
for (const address of [...token.bootstrapMultiaddrs, ...token.relayMultiaddrs]) {
|
|
1628
|
+
const trimmed = address.trim();
|
|
1629
|
+
if (trimmed) this.bootstrapMultiaddrs.add(trimmed);
|
|
1630
|
+
}
|
|
1631
|
+
if (!this.libp2p) return;
|
|
1632
|
+
await this.admitRelayReservation(token);
|
|
1633
|
+
await this.reserveRelayListeners(token.relayMultiaddrs);
|
|
1634
|
+
await this.dialBootstrapPeers([...this.bootstrapMultiaddrs]);
|
|
1635
|
+
}
|
|
1636
|
+
getMultiaddrs() {
|
|
1637
|
+
return this.libp2p?.getMultiaddrs().map((address) => address.toString()) ?? [];
|
|
1638
|
+
}
|
|
1639
|
+
async createNode() {
|
|
1640
|
+
const privateKey = await privateKeyFromIdentity(this.options.identity);
|
|
1641
|
+
const listen = this.options.listen ?? defaultListen;
|
|
1642
|
+
const peerDiscovery = [
|
|
1643
|
+
...this.createBootstrapDiscovery(),
|
|
1644
|
+
...this.options.enableMdns === false ? [] : [mdns({ serviceTag: "memeloop" })]
|
|
1645
|
+
];
|
|
1646
|
+
const transports = this.options.enableCircuitRelay === false ? [tcp(), webSockets()] : [tcp(), webSockets(), circuitRelayTransport()];
|
|
1647
|
+
return createLibp2p({
|
|
1648
|
+
privateKey,
|
|
1649
|
+
addresses: {
|
|
1650
|
+
listen: this.options.enableCircuitRelay === false ? listen.addresses : addRelayListenAddress(listen.addresses),
|
|
1651
|
+
announce: listen.announce
|
|
1652
|
+
},
|
|
1653
|
+
transports,
|
|
1654
|
+
connectionEncrypters: [noise()],
|
|
1655
|
+
streamMuxers: [yamux()],
|
|
1656
|
+
peerDiscovery,
|
|
1657
|
+
services: {
|
|
1658
|
+
identify: identify(),
|
|
1659
|
+
ping: ping()
|
|
1660
|
+
},
|
|
1661
|
+
start: false
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
createBootstrapDiscovery() {
|
|
1665
|
+
const list = [...this.bootstrapMultiaddrs];
|
|
1666
|
+
if (list.length === 0) return [];
|
|
1667
|
+
return [bootstrap({ list, tagName: "memeloop-bootstrap", tagTTL: Infinity })];
|
|
1668
|
+
}
|
|
1669
|
+
async dialBootstrapPeers(addresses) {
|
|
1670
|
+
const node = this.requireNode();
|
|
1671
|
+
for (const address of addresses) {
|
|
1672
|
+
await node.dial(multiaddr(address)).catch(() => void 0);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async admitRelayReservation(token) {
|
|
1676
|
+
const relayAddresses = token.relayMultiaddrs.map((address) => address.trim()).filter((address) => address.length > 0);
|
|
1677
|
+
if (relayAddresses.length === 0) return;
|
|
1678
|
+
const node = this.requireNode();
|
|
1679
|
+
const errors = [];
|
|
1680
|
+
for (const address of relayAddresses) {
|
|
1681
|
+
let stream;
|
|
1682
|
+
try {
|
|
1683
|
+
stream = await node.dialProtocol(multiaddr(address), RELAY_ADMISSION_PROTOCOL);
|
|
1684
|
+
const request = {
|
|
1685
|
+
type: RELAY_ADMISSION_REQUEST_TYPE,
|
|
1686
|
+
token
|
|
1687
|
+
};
|
|
1688
|
+
await writeJsonMessage(stream, request, RELAY_ADMISSION_MESSAGE_MAX_BYTES);
|
|
1689
|
+
const response = await readJsonMessage(stream, RELAY_ADMISSION_MESSAGE_MAX_BYTES);
|
|
1690
|
+
await stream.close();
|
|
1691
|
+
if (isRelayAdmissionResponse(response) && response.ok) return;
|
|
1692
|
+
const reason = isRelayAdmissionResponse(response) ? response.reason : "invalid_relay_admission_response";
|
|
1693
|
+
errors.push(`${address}: ${reason}`);
|
|
1694
|
+
} catch (error2) {
|
|
1695
|
+
stream?.abort(error2 instanceof Error ? error2 : new Error("relay_admission_failed"));
|
|
1696
|
+
errors.push(`${address}: ${error2 instanceof Error ? error2.message : "relay_admission_failed"}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
throw new Error(`relay_admission_failed${errors.length > 0 ? `: ${errors.join("; ")}` : ""}`);
|
|
1700
|
+
}
|
|
1701
|
+
async reserveRelayListeners(relayMultiaddrs) {
|
|
1702
|
+
const relayAddresses = relayMultiaddrs.map((address) => address.trim()).filter((address) => address.length > 0);
|
|
1703
|
+
if (relayAddresses.length === 0) return;
|
|
1704
|
+
const transportManager = this.requireNode().components?.transportManager;
|
|
1705
|
+
if (!transportManager) throw new Error("relay_transport_manager_unavailable");
|
|
1706
|
+
const errors = [];
|
|
1707
|
+
for (const address of relayAddresses) {
|
|
1708
|
+
try {
|
|
1709
|
+
await transportManager.listen([relayCircuitMultiaddr(address)]);
|
|
1710
|
+
return;
|
|
1711
|
+
} catch (error2) {
|
|
1712
|
+
errors.push(`${address}: ${error2 instanceof Error ? error2.message : "relay_reservation_failed"}`);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
throw new Error(`relay_reservation_failed${errors.length > 0 ? `: ${errors.join("; ")}` : ""}`);
|
|
1716
|
+
}
|
|
1717
|
+
async loadTrustedDevicesFromStore() {
|
|
1718
|
+
const records = await this.options.trustStore?.loadTrustedDevices();
|
|
1719
|
+
for (const record of records ?? []) {
|
|
1720
|
+
this.trustedDevices.set(record.peerId, record);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
registerDiscoveryListeners(node) {
|
|
1724
|
+
node.addEventListener("peer:discovery", (event) => {
|
|
1725
|
+
const detail = event.detail;
|
|
1726
|
+
const peerId = detail.id.toString();
|
|
1727
|
+
const trustedRecord = this.trustedDevices.get(peerId);
|
|
1728
|
+
this.discoveredDevices.set(peerId, {
|
|
1729
|
+
peerId,
|
|
1730
|
+
displayName: trustedRecord?.deviceName ?? peerId,
|
|
1731
|
+
platform: trustedRecord?.platform ?? "cli",
|
|
1732
|
+
trustMode: trustedRecord?.trustMode ?? "local-pairing",
|
|
1733
|
+
trusted: trustedRecord !== void 0,
|
|
1734
|
+
reachability: {
|
|
1735
|
+
state: "nearby",
|
|
1736
|
+
paths: ["lan"]
|
|
1737
|
+
},
|
|
1738
|
+
capabilities: emptyCapabilities,
|
|
1739
|
+
multiaddrs: detail.multiaddrs.map((address) => address.toString()),
|
|
1740
|
+
lastSeen: Date.now()
|
|
1741
|
+
});
|
|
1742
|
+
this.emitDevices();
|
|
1743
|
+
if (this.options.autoDialDiscoveredPeers === true && detail.multiaddrs.length > 0) {
|
|
1744
|
+
void node.dial(detail.multiaddrs).catch(() => void 0);
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
node.addEventListener("peer:connect", (event) => {
|
|
1748
|
+
this.markPeerOnline(event.detail);
|
|
1749
|
+
});
|
|
1750
|
+
node.addEventListener("peer:disconnect", (event) => {
|
|
1751
|
+
this.markPeerOffline(event.detail);
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
async registerProtocolHandlers(node) {
|
|
1755
|
+
const protocols = [
|
|
1756
|
+
"/memeloop/pairing/1.0.0",
|
|
1757
|
+
"/memeloop/rpc/1.0.0",
|
|
1758
|
+
"/memeloop/sync/1.0.0",
|
|
1759
|
+
"/memeloop/agent/1.0.0"
|
|
1760
|
+
];
|
|
1761
|
+
await node.handle(protocols, async (stream, connection) => {
|
|
1762
|
+
const remotePeerId = connection.remotePeer.toString();
|
|
1763
|
+
if (stream.protocol === PAIRING_PROTOCOL2) {
|
|
1764
|
+
if (!await this.authorizer.canOpenProtocol({ remotePeerId, protocol: PAIRING_PROTOCOL2, direction: "inbound" })) {
|
|
1765
|
+
stream.abort(new Error("device_not_trusted"));
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
await this.handlePairingStream(stream, remotePeerId).catch((error2) => {
|
|
1769
|
+
stream.abort(error2 instanceof Error ? error2 : new Error("pairing_handler_failed"));
|
|
1770
|
+
});
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
if (stream.protocol === SYNC_PROTOCOL) {
|
|
1774
|
+
await this.handleSyncStream(stream, remotePeerId);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (stream.protocol === RPC_PROTOCOL) {
|
|
1778
|
+
await this.handleRpcStream(stream, remotePeerId);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (!await this.authorizer.canOpenProtocol({ remotePeerId, protocol: stream.protocol, direction: "inbound" })) {
|
|
1782
|
+
stream.abort(new Error("device_not_trusted"));
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
stream.abort(new Error("protocol_handler_not_registered"));
|
|
1786
|
+
}, {
|
|
1787
|
+
runOnLimitedConnection: true
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
async handleRpcStream(stream, remotePeerId) {
|
|
1791
|
+
let requestId = "unknown";
|
|
1792
|
+
try {
|
|
1793
|
+
const request = await readJsonMessage(stream, RPC_MESSAGE_MAX_BYTES);
|
|
1794
|
+
if (!isLibp2pRpcRequest(request)) throw new Error("invalid_rpc_request");
|
|
1795
|
+
requestId = request.id;
|
|
1796
|
+
const authorized = await this.authorizer.canOpenProtocol({
|
|
1797
|
+
remotePeerId,
|
|
1798
|
+
protocol: RPC_PROTOCOL,
|
|
1799
|
+
direction: "inbound",
|
|
1800
|
+
presentedGrant: request.grant
|
|
1801
|
+
});
|
|
1802
|
+
if (!authorized) throw new Error("device_not_trusted");
|
|
1803
|
+
if (!this.options.rpcHandler) throw new Error("rpc_handler_not_configured");
|
|
1804
|
+
const result = await this.options.rpcHandler({
|
|
1805
|
+
remotePeerId,
|
|
1806
|
+
method: request.method,
|
|
1807
|
+
parameters: request.params,
|
|
1808
|
+
presentedGrant: request.grant
|
|
1809
|
+
});
|
|
1810
|
+
await writeJsonMessage(stream, {
|
|
1811
|
+
type: LIBP2P_RPC_RESPONSE_TYPE,
|
|
1812
|
+
id: request.id,
|
|
1813
|
+
ok: true,
|
|
1814
|
+
result
|
|
1815
|
+
}, RPC_MESSAGE_MAX_BYTES);
|
|
1816
|
+
} catch (error2) {
|
|
1817
|
+
await writeJsonMessage(stream, {
|
|
1818
|
+
type: LIBP2P_RPC_RESPONSE_TYPE,
|
|
1819
|
+
id: requestId,
|
|
1820
|
+
ok: false,
|
|
1821
|
+
error: error2 instanceof Error ? error2.message : "rpc_handler_failed"
|
|
1822
|
+
}, RPC_MESSAGE_MAX_BYTES).catch(() => void 0);
|
|
1823
|
+
} finally {
|
|
1824
|
+
await stream.close().catch(() => void 0);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
async handleSyncStream(stream, remotePeerId) {
|
|
1828
|
+
let requestId = "unknown";
|
|
1829
|
+
try {
|
|
1830
|
+
const request = await readJsonMessage(stream, SYNC_MESSAGE_MAX_BYTES);
|
|
1831
|
+
if (!isLibp2pSyncRequest(request)) throw new Error("invalid_sync_request");
|
|
1832
|
+
requestId = request.id;
|
|
1833
|
+
const authorized = await this.authorizer.canOpenProtocol({
|
|
1834
|
+
remotePeerId,
|
|
1835
|
+
protocol: SYNC_PROTOCOL,
|
|
1836
|
+
direction: "inbound",
|
|
1837
|
+
presentedGrant: request.grant
|
|
1838
|
+
});
|
|
1839
|
+
if (!authorized) throw new Error("device_not_trusted");
|
|
1840
|
+
const result = await this.handleSyncRequest(request);
|
|
1841
|
+
await writeJsonMessage(stream, {
|
|
1842
|
+
type: LIBP2P_SYNC_RESPONSE_TYPE,
|
|
1843
|
+
id: request.id,
|
|
1844
|
+
ok: true,
|
|
1845
|
+
result
|
|
1846
|
+
}, SYNC_MESSAGE_MAX_BYTES);
|
|
1847
|
+
} catch (error2) {
|
|
1848
|
+
await writeJsonMessage(stream, {
|
|
1849
|
+
type: LIBP2P_SYNC_RESPONSE_TYPE,
|
|
1850
|
+
id: requestId,
|
|
1851
|
+
ok: false,
|
|
1852
|
+
error: error2 instanceof Error ? error2.message : "sync_handler_failed"
|
|
1853
|
+
}, SYNC_MESSAGE_MAX_BYTES).catch(() => void 0);
|
|
1854
|
+
} finally {
|
|
1855
|
+
await stream.close().catch(() => void 0);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async handleSyncRequest(request) {
|
|
1859
|
+
const storage = this.options.syncStorage;
|
|
1860
|
+
if (!storage) throw new Error("sync_storage_not_configured");
|
|
1861
|
+
switch (request.method) {
|
|
1862
|
+
case "exchangeVersionVector":
|
|
1863
|
+
return {
|
|
1864
|
+
remoteVersion: this.options.syncVersionVector?.() ?? {},
|
|
1865
|
+
missingForRemote: []
|
|
1866
|
+
};
|
|
1867
|
+
case "pullMissingMetadata": {
|
|
1868
|
+
const sinceVersion = versionVectorParameter(request.params, "sinceVersion");
|
|
1869
|
+
const conversations = await storage.listConversations({ limit: 500 });
|
|
1870
|
+
return conversations.filter((conversation) => shouldSendConversation(conversation, sinceVersion));
|
|
1871
|
+
}
|
|
1872
|
+
case "pullMissingMessages": {
|
|
1873
|
+
const parameters = objectParameter2(request.params);
|
|
1874
|
+
const conversationId = stringParameter2(parameters, "conversationId");
|
|
1875
|
+
const knownMessageIds = stringArrayParameter2(parameters, "knownMessageIds");
|
|
1876
|
+
const messages = await storage.getMessages(conversationId, { mode: "full-content" });
|
|
1877
|
+
const known = new Set(knownMessageIds);
|
|
1878
|
+
return messages.filter((message) => !known.has(message.messageId));
|
|
1879
|
+
}
|
|
1880
|
+
case "pullAttachmentBlob": {
|
|
1881
|
+
const parameters = objectParameter2(request.params);
|
|
1882
|
+
const contentHash = stringParameter2(parameters, "contentHash");
|
|
1883
|
+
return this.readAttachmentBlob(contentHash);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async readAttachmentBlob(contentHash) {
|
|
1888
|
+
const storage = this.options.syncStorage;
|
|
1889
|
+
if (!storage?.readAttachmentData) return null;
|
|
1890
|
+
const reference = await storage.getAttachment(contentHash);
|
|
1891
|
+
if (!reference) return null;
|
|
1892
|
+
const data = await storage.readAttachmentData(contentHash);
|
|
1893
|
+
if (!data) return null;
|
|
1894
|
+
const { toString } = await loadUint8arrays();
|
|
1895
|
+
return {
|
|
1896
|
+
dataBase64Url: toString(data, "base64url"),
|
|
1897
|
+
filename: reference.filename,
|
|
1898
|
+
mimeType: reference.mimeType,
|
|
1899
|
+
size: reference.size
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
pairingDialTarget(peerId, options) {
|
|
1903
|
+
const addresses = options.multiaddrs ?? this.discoveredDevices.get(peerId)?.multiaddrs ?? [];
|
|
1904
|
+
if (addresses.length === 0) return peerIdFromString(peerId);
|
|
1905
|
+
return addresses.map((address) => multiaddr(address));
|
|
1906
|
+
}
|
|
1907
|
+
localPairingDevice() {
|
|
1908
|
+
return {
|
|
1909
|
+
peerId: this.options.identity.peerId,
|
|
1910
|
+
publicKeyMultibase: this.options.identity.publicKeyMultibase,
|
|
1911
|
+
deviceName: this.options.identity.deviceName,
|
|
1912
|
+
platform: this.options.identity.platform,
|
|
1913
|
+
capabilities: this.capabilities,
|
|
1914
|
+
multiaddrs: this.getMultiaddrs()
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
async handlePairingStream(stream, remotePeerId) {
|
|
1918
|
+
const request = await readJsonMessage(stream);
|
|
1919
|
+
if (!isPairingRequest(request)) throw new Error("invalid_pairing_request");
|
|
1920
|
+
if (request.device.peerId !== remotePeerId) throw new Error("pairing_peer_id_mismatch");
|
|
1921
|
+
if (request.expiresAt <= Date.now()) throw new Error("pairing_request_expired");
|
|
1922
|
+
await assertPairingDeviceIdentity(request.device);
|
|
1923
|
+
const responseNonce = randomPairingNonce();
|
|
1924
|
+
const localDevice = this.localPairingDevice();
|
|
1925
|
+
const expiresAt = Math.min(request.expiresAt, Date.now() + PAIRING_SESSION_TTL_MS);
|
|
1926
|
+
const session = {
|
|
1927
|
+
sessionId: request.sessionId,
|
|
1928
|
+
localPeerId: this.options.identity.peerId,
|
|
1929
|
+
remotePeerId: request.device.peerId,
|
|
1930
|
+
remotePublicKeyMultibase: request.device.publicKeyMultibase,
|
|
1931
|
+
remoteDeviceName: request.device.deviceName,
|
|
1932
|
+
remotePlatform: request.device.platform,
|
|
1933
|
+
remoteCapabilities: request.device.capabilities,
|
|
1934
|
+
remoteMultiaddrs: request.device.multiaddrs,
|
|
1935
|
+
direction: "inbound",
|
|
1936
|
+
status: "pending",
|
|
1937
|
+
confirmCode: await buildPairingConfirmCode({
|
|
1938
|
+
initiator: request.device,
|
|
1939
|
+
responder: localDevice,
|
|
1940
|
+
requestNonce: request.requestNonce,
|
|
1941
|
+
responseNonce
|
|
1942
|
+
}),
|
|
1943
|
+
createdAt: Date.now(),
|
|
1944
|
+
expiresAt
|
|
1945
|
+
};
|
|
1946
|
+
this.pairingSessions.set(session.sessionId, session);
|
|
1947
|
+
this.upsertDiscoveredDeviceFromPairing(session.remotePeerId, request.device);
|
|
1948
|
+
await writeJsonMessage(stream, {
|
|
1949
|
+
type: "memeloop-local-pairing-response-v1",
|
|
1950
|
+
sessionId: request.sessionId,
|
|
1951
|
+
requestNonce: request.requestNonce,
|
|
1952
|
+
responseNonce,
|
|
1953
|
+
accepted: true,
|
|
1954
|
+
expiresAt,
|
|
1955
|
+
device: localDevice
|
|
1956
|
+
});
|
|
1957
|
+
await stream.close();
|
|
1958
|
+
this.emitPairingSessions();
|
|
1959
|
+
this.emitDevices();
|
|
1960
|
+
}
|
|
1961
|
+
async sessionFromPairingResponse(peerId, request, response) {
|
|
1962
|
+
if (!isPairingResponse(response)) throw new Error("invalid_pairing_response");
|
|
1963
|
+
if (response.sessionId !== request.sessionId) throw new Error("pairing_session_mismatch");
|
|
1964
|
+
if (response.requestNonce !== request.requestNonce) throw new Error("pairing_nonce_mismatch");
|
|
1965
|
+
if (response.device.peerId !== peerId) throw new Error("pairing_peer_id_mismatch");
|
|
1966
|
+
if (response.expiresAt <= Date.now()) throw new Error("pairing_response_expired");
|
|
1967
|
+
await assertPairingDeviceIdentity(response.device);
|
|
1968
|
+
return {
|
|
1969
|
+
sessionId: request.sessionId,
|
|
1970
|
+
localPeerId: this.options.identity.peerId,
|
|
1971
|
+
remotePeerId: response.device.peerId,
|
|
1972
|
+
remotePublicKeyMultibase: response.device.publicKeyMultibase,
|
|
1973
|
+
remoteDeviceName: response.device.deviceName,
|
|
1974
|
+
remotePlatform: response.device.platform,
|
|
1975
|
+
remoteCapabilities: response.device.capabilities,
|
|
1976
|
+
remoteMultiaddrs: response.device.multiaddrs,
|
|
1977
|
+
direction: "outbound",
|
|
1978
|
+
status: "pending",
|
|
1979
|
+
confirmCode: await buildPairingConfirmCode({
|
|
1980
|
+
initiator: request.device,
|
|
1981
|
+
responder: response.device,
|
|
1982
|
+
requestNonce: request.requestNonce,
|
|
1983
|
+
responseNonce: response.responseNonce
|
|
1984
|
+
}),
|
|
1985
|
+
createdAt: request.createdAt,
|
|
1986
|
+
expiresAt: Math.min(request.expiresAt, response.expiresAt)
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
requirePairingSession(sessionId) {
|
|
1990
|
+
const session = this.pairingSessions.get(sessionId);
|
|
1991
|
+
if (!session) throw new Error("pairing_session_not_found");
|
|
1992
|
+
return session;
|
|
1993
|
+
}
|
|
1994
|
+
refreshPairingSessionExpiry() {
|
|
1995
|
+
const now = Date.now();
|
|
1996
|
+
let changed = false;
|
|
1997
|
+
for (const session of this.pairingSessions.values()) {
|
|
1998
|
+
if (session.status === "pending" && session.expiresAt <= now) {
|
|
1999
|
+
session.status = "expired";
|
|
2000
|
+
changed = true;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (changed) this.emitPairingSessions();
|
|
2004
|
+
}
|
|
2005
|
+
upsertDiscoveredDeviceFromPairing(peerId, device) {
|
|
2006
|
+
const current = this.discoveredDevices.get(peerId);
|
|
2007
|
+
this.discoveredDevices.set(peerId, {
|
|
2008
|
+
peerId,
|
|
2009
|
+
displayName: device.deviceName,
|
|
2010
|
+
platform: device.platform,
|
|
2011
|
+
trustMode: current?.trustMode ?? "local-pairing",
|
|
2012
|
+
trusted: current?.trusted ?? this.trustedDevices.has(peerId),
|
|
2013
|
+
reachability: current?.reachability ?? {
|
|
2014
|
+
state: "nearby",
|
|
2015
|
+
paths: device.multiaddrs.length > 0 ? ["lan"] : []
|
|
2016
|
+
},
|
|
2017
|
+
capabilities: device.capabilities,
|
|
2018
|
+
multiaddrs: device.multiaddrs,
|
|
2019
|
+
lastSeen: Date.now()
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
wrapStream(stream) {
|
|
2023
|
+
return {
|
|
2024
|
+
source: this.streamSource(stream),
|
|
2025
|
+
async sink(source) {
|
|
2026
|
+
for await (const chunk of source) {
|
|
2027
|
+
stream.send(chunk);
|
|
2028
|
+
}
|
|
2029
|
+
await stream.close();
|
|
2030
|
+
},
|
|
2031
|
+
close: async () => {
|
|
2032
|
+
await stream.close();
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
async *streamSource(stream) {
|
|
2037
|
+
for await (const chunk of stream) {
|
|
2038
|
+
if (chunk instanceof Uint8Array) {
|
|
2039
|
+
yield chunk;
|
|
2040
|
+
} else {
|
|
2041
|
+
yield chunk.subarray();
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
markPeerOnline(peerId) {
|
|
2046
|
+
const id = peerId.toString();
|
|
2047
|
+
const current = this.discoveredDevices.get(id);
|
|
2048
|
+
if (current) {
|
|
2049
|
+
current.reachability = { state: "online", paths: current.reachability.paths };
|
|
2050
|
+
current.lastSeen = Date.now();
|
|
2051
|
+
this.emitDevices();
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
markPeerOffline(peerId) {
|
|
2055
|
+
const id = peerId.toString();
|
|
2056
|
+
const current = this.discoveredDevices.get(id);
|
|
2057
|
+
if (current) {
|
|
2058
|
+
current.reachability = { state: "offline", paths: [] };
|
|
2059
|
+
current.lastSeen = Date.now();
|
|
2060
|
+
this.emitDevices();
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
updateDeviceTrust(peerId, trustMode, trusted) {
|
|
2064
|
+
const current = this.discoveredDevices.get(peerId);
|
|
2065
|
+
if (current) {
|
|
2066
|
+
this.discoveredDevices.set(peerId, {
|
|
2067
|
+
...current,
|
|
2068
|
+
trustMode,
|
|
2069
|
+
trusted
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
toVisibleDevices() {
|
|
2074
|
+
const visible = new Map(this.discoveredDevices);
|
|
2075
|
+
for (const record of this.trustedDevices.values()) {
|
|
2076
|
+
const current = visible.get(record.peerId);
|
|
2077
|
+
if (current) {
|
|
2078
|
+
visible.set(record.peerId, {
|
|
2079
|
+
...current,
|
|
2080
|
+
displayName: record.deviceName,
|
|
2081
|
+
platform: record.platform,
|
|
2082
|
+
trustMode: record.trustMode,
|
|
2083
|
+
trusted: !record.revokedAt,
|
|
2084
|
+
lastSeen: current.lastSeen ?? record.lastSeen
|
|
2085
|
+
});
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
if (record.revokedAt) continue;
|
|
2089
|
+
visible.set(record.peerId, {
|
|
2090
|
+
peerId: record.peerId,
|
|
2091
|
+
displayName: record.deviceName,
|
|
2092
|
+
platform: record.platform,
|
|
2093
|
+
trustMode: record.trustMode,
|
|
2094
|
+
trusted: true,
|
|
2095
|
+
reachability: {
|
|
2096
|
+
state: "offline",
|
|
2097
|
+
paths: []
|
|
2098
|
+
},
|
|
2099
|
+
capabilities: emptyCapabilities,
|
|
2100
|
+
lastSeen: record.lastSeen
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
return [...visible.values()];
|
|
2104
|
+
}
|
|
2105
|
+
emitDevices() {
|
|
2106
|
+
const devices = this.toVisibleDevices();
|
|
2107
|
+
for (const listener of this.listeners) listener(devices);
|
|
2108
|
+
}
|
|
2109
|
+
emitPairingSessions() {
|
|
2110
|
+
const sessions = [...this.pairingSessions.values()];
|
|
2111
|
+
for (const listener of this.pairingListeners) listener(sessions);
|
|
2112
|
+
}
|
|
2113
|
+
requireNode() {
|
|
2114
|
+
if (!this.libp2p) throw new Error("device_network_not_started");
|
|
2115
|
+
return this.libp2p;
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
var PUBLIC_KEY_MULTIBASE_PREFIX = "libp2p-pub:";
|
|
2119
|
+
async function loadCryptoKeys() {
|
|
2120
|
+
return import("@libp2p/crypto/keys");
|
|
2121
|
+
}
|
|
2122
|
+
async function loadUint8arrays() {
|
|
2123
|
+
return import("uint8arrays");
|
|
2124
|
+
}
|
|
2125
|
+
function randomPairingNonce() {
|
|
2126
|
+
const bytes = new Uint8Array(16);
|
|
2127
|
+
crypto.getRandomValues(bytes);
|
|
2128
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2129
|
+
}
|
|
2130
|
+
async function writeJsonMessage(stream, message, maxBytes = PAIRING_MESSAGE_MAX_BYTES) {
|
|
2131
|
+
const payload = new TextEncoder().encode(JSON.stringify(message));
|
|
2132
|
+
if (payload.byteLength > maxBytes) throw new Error("json_message_too_large");
|
|
2133
|
+
stream.send(payload);
|
|
2134
|
+
}
|
|
2135
|
+
async function readJsonMessage(stream, maxBytes = PAIRING_MESSAGE_MAX_BYTES) {
|
|
2136
|
+
const reader = stream[Symbol.asyncIterator]();
|
|
2137
|
+
const result = await reader.next();
|
|
2138
|
+
if (result.done || !result.value) throw new Error("pairing_message_missing");
|
|
2139
|
+
const chunk = result.value instanceof Uint8Array ? result.value : result.value.subarray();
|
|
2140
|
+
if (chunk.byteLength > maxBytes) throw new Error("json_message_too_large");
|
|
2141
|
+
return JSON.parse(new TextDecoder().decode(chunk));
|
|
2142
|
+
}
|
|
2143
|
+
async function writeStreamJson(stream, message) {
|
|
2144
|
+
const payload = new TextEncoder().encode(JSON.stringify(message));
|
|
2145
|
+
await stream.sink((async function* () {
|
|
2146
|
+
yield payload;
|
|
2147
|
+
})());
|
|
2148
|
+
}
|
|
2149
|
+
async function readStreamJson(stream) {
|
|
2150
|
+
const reader = stream.source[Symbol.asyncIterator]();
|
|
2151
|
+
const result = await reader.next();
|
|
2152
|
+
if (result.done || !result.value) throw new Error("rpc_response_missing");
|
|
2153
|
+
return JSON.parse(new TextDecoder().decode(result.value));
|
|
2154
|
+
}
|
|
2155
|
+
function isRelayAdmissionResponse(value) {
|
|
2156
|
+
if (value === null || typeof value !== "object") return false;
|
|
2157
|
+
const record = value;
|
|
2158
|
+
return record.type === RELAY_ADMISSION_RESPONSE_TYPE && typeof record.ok === "boolean";
|
|
2159
|
+
}
|
|
2160
|
+
function relayCircuitMultiaddr(address) {
|
|
2161
|
+
const parsed = multiaddr(address);
|
|
2162
|
+
return address.includes("/p2p-circuit") ? parsed : parsed.encapsulate("/p2p-circuit");
|
|
2163
|
+
}
|
|
2164
|
+
function objectParameter2(value) {
|
|
2165
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid_sync_params");
|
|
2166
|
+
return value;
|
|
2167
|
+
}
|
|
2168
|
+
function stringParameter2(parameters, key) {
|
|
2169
|
+
const value = parameters[key];
|
|
2170
|
+
if (typeof value !== "string" || value.length === 0) throw new Error("invalid_sync_params");
|
|
2171
|
+
return value;
|
|
2172
|
+
}
|
|
2173
|
+
function stringArrayParameter2(parameters, key) {
|
|
2174
|
+
const value = parameters[key];
|
|
2175
|
+
if (!Array.isArray(value)) throw new Error("invalid_sync_params");
|
|
2176
|
+
return value.filter((item) => typeof item === "string");
|
|
2177
|
+
}
|
|
2178
|
+
function versionVectorParameter(parameters, key) {
|
|
2179
|
+
const raw = objectParameter2(parameters)[key];
|
|
2180
|
+
if (raw === void 0) return {};
|
|
2181
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid_sync_params");
|
|
2182
|
+
const version = {};
|
|
2183
|
+
for (const [nodeId, clock] of Object.entries(raw)) {
|
|
2184
|
+
if (typeof clock === "number" && Number.isFinite(clock) && clock >= 0) version[nodeId] = clock;
|
|
2185
|
+
}
|
|
2186
|
+
return version;
|
|
2187
|
+
}
|
|
2188
|
+
function shouldSendConversation(conversation, sinceVersion) {
|
|
2189
|
+
const clock = Math.max(1, conversation.messageCount);
|
|
2190
|
+
return (sinceVersion[conversation.originNodeId] ?? 0) < clock;
|
|
2191
|
+
}
|
|
2192
|
+
function isDevicePlatform(value) {
|
|
2193
|
+
return value === "desktop" || value === "mobile" || value === "cli";
|
|
2194
|
+
}
|
|
2195
|
+
function normalizePairingCapabilities(value) {
|
|
2196
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2197
|
+
return {
|
|
2198
|
+
tools: Array.isArray(raw.tools) ? raw.tools.filter((item) => typeof item === "string") : [],
|
|
2199
|
+
mcpServers: Array.isArray(raw.mcpServers) ? raw.mcpServers.filter((item) => typeof item === "string") : [],
|
|
2200
|
+
hasWiki: raw.hasWiki === true,
|
|
2201
|
+
agentLoop: raw.agentLoop === true,
|
|
2202
|
+
imChannels: Array.isArray(raw.imChannels) ? raw.imChannels.filter((item) => typeof item === "string") : [],
|
|
2203
|
+
wikis: Array.isArray(raw.wikis) ? raw.wikis.filter((item) => item !== null && typeof item === "object") : []
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
function normalizePairingDevice(value) {
|
|
2207
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2208
|
+
if (!raw) return void 0;
|
|
2209
|
+
const peerId = typeof raw.peerId === "string" ? raw.peerId.trim() : "";
|
|
2210
|
+
const publicKeyMultibase = typeof raw.publicKeyMultibase === "string" ? raw.publicKeyMultibase.trim() : "";
|
|
2211
|
+
const deviceName = typeof raw.deviceName === "string" ? raw.deviceName.trim() : "";
|
|
2212
|
+
if (!peerId || !publicKeyMultibase || !deviceName || !isDevicePlatform(raw.platform)) return void 0;
|
|
2213
|
+
return {
|
|
2214
|
+
peerId,
|
|
2215
|
+
publicKeyMultibase,
|
|
2216
|
+
deviceName,
|
|
2217
|
+
platform: raw.platform,
|
|
2218
|
+
capabilities: normalizePairingCapabilities(raw.capabilities),
|
|
2219
|
+
multiaddrs: Array.isArray(raw.multiaddrs) ? raw.multiaddrs.filter((item) => typeof item === "string") : []
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
function isPairingRequest(value) {
|
|
2223
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2224
|
+
if (!raw || raw.type !== "memeloop-local-pairing-request-v1") return false;
|
|
2225
|
+
return typeof raw.sessionId === "string" && typeof raw.requestNonce === "string" && typeof raw.createdAt === "number" && typeof raw.expiresAt === "number" && normalizePairingDevice(raw.device) !== void 0;
|
|
2226
|
+
}
|
|
2227
|
+
function isPairingResponse(value) {
|
|
2228
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2229
|
+
if (!raw || raw.type !== "memeloop-local-pairing-response-v1") return false;
|
|
2230
|
+
return typeof raw.sessionId === "string" && typeof raw.requestNonce === "string" && typeof raw.responseNonce === "string" && raw.accepted === true && typeof raw.expiresAt === "number" && normalizePairingDevice(raw.device) !== void 0;
|
|
2231
|
+
}
|
|
2232
|
+
async function assertPairingDeviceIdentity(device) {
|
|
2233
|
+
const publicKey = await decodePublicKeyMultibase(device.publicKeyMultibase);
|
|
2234
|
+
if (peerIdFromPublicKey(publicKey).toString() !== device.peerId) throw new Error("pairing_public_key_peer_id_mismatch");
|
|
2235
|
+
}
|
|
2236
|
+
async function buildPairingConfirmCode(input) {
|
|
2237
|
+
const text = [
|
|
2238
|
+
"memeloop-local-pairing-confirm-v1",
|
|
2239
|
+
`initiatorPeerId=${input.initiator.peerId}`,
|
|
2240
|
+
`initiatorPublicKey=${input.initiator.publicKeyMultibase}`,
|
|
2241
|
+
`responderPeerId=${input.responder.peerId}`,
|
|
2242
|
+
`responderPublicKey=${input.responder.publicKeyMultibase}`,
|
|
2243
|
+
`requestNonce=${input.requestNonce}`,
|
|
2244
|
+
`responseNonce=${input.responseNonce}`
|
|
2245
|
+
].join("\n");
|
|
2246
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
2247
|
+
const bytes = new Uint8Array(digest);
|
|
2248
|
+
const value = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
|
|
2249
|
+
return (value % 1e6).toString().padStart(6, "0");
|
|
2250
|
+
}
|
|
2251
|
+
async function encodePublicKeyMultibase(publicKey) {
|
|
2252
|
+
const { publicKeyToProtobuf } = await loadCryptoKeys();
|
|
2253
|
+
const { toString } = await loadUint8arrays();
|
|
2254
|
+
const protobuf = publicKeyToProtobuf(publicKey);
|
|
2255
|
+
return `${PUBLIC_KEY_MULTIBASE_PREFIX}${toString(protobuf, "base64url")}`;
|
|
2256
|
+
}
|
|
2257
|
+
async function decodePublicKeyMultibase(multibase) {
|
|
2258
|
+
if (!multibase.startsWith(PUBLIC_KEY_MULTIBASE_PREFIX)) {
|
|
2259
|
+
throw new Error("unsupported_public_key_multibase_prefix");
|
|
2260
|
+
}
|
|
2261
|
+
const { publicKeyFromProtobuf } = await loadCryptoKeys();
|
|
2262
|
+
const { fromString } = await loadUint8arrays();
|
|
2263
|
+
const encoded = multibase.slice(PUBLIC_KEY_MULTIBASE_PREFIX.length);
|
|
2264
|
+
const protobuf = fromString(encoded, "base64url");
|
|
2265
|
+
return publicKeyFromProtobuf(protobuf);
|
|
2266
|
+
}
|
|
2267
|
+
async function createDeviceIdentity(platform, deviceName) {
|
|
2268
|
+
const { generateKeyPair } = await loadCryptoKeys();
|
|
2269
|
+
const { toString } = await loadUint8arrays();
|
|
2270
|
+
const privateKey = await generateKeyPair("Ed25519");
|
|
2271
|
+
const publicKey = privateKey.publicKey;
|
|
2272
|
+
return {
|
|
2273
|
+
peerId: peerIdFromPrivateKey(privateKey).toString(),
|
|
2274
|
+
publicKeyMultibase: await encodePublicKeyMultibase(publicKey),
|
|
2275
|
+
privateKeyRef: "libp2p-raw-seed",
|
|
2276
|
+
privateKeyRawSeedBase64Url: toString(privateKey.raw, "base64url"),
|
|
2277
|
+
createdAt: Date.now(),
|
|
2278
|
+
deviceName,
|
|
2279
|
+
platform
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
async function privateKeyFromIdentity(identity) {
|
|
2283
|
+
if (identity.privateKeyRawSeedBase64Url) {
|
|
2284
|
+
const { privateKeyFromRaw } = await loadCryptoKeys();
|
|
2285
|
+
const { fromString } = await loadUint8arrays();
|
|
2286
|
+
const raw = fromString(identity.privateKeyRawSeedBase64Url, "base64url");
|
|
2287
|
+
return privateKeyFromRaw(raw);
|
|
2288
|
+
}
|
|
2289
|
+
throw new Error("unsupported_private_key_format");
|
|
2290
|
+
}
|
|
2291
|
+
function buildDeviceBindingMessage(input) {
|
|
2292
|
+
const message = [
|
|
2293
|
+
"memeloop-device-binding-v1",
|
|
2294
|
+
`accountId=${input.accountId}`,
|
|
2295
|
+
`peerId=${input.peerId}`,
|
|
2296
|
+
`publicKey=${input.publicKeyMultibase}`,
|
|
2297
|
+
`nonce=${input.nonce}`
|
|
2298
|
+
].join("\n");
|
|
2299
|
+
return new TextEncoder().encode(message);
|
|
2300
|
+
}
|
|
2301
|
+
function buildDeviceConnectionGrantMessage(grant) {
|
|
2302
|
+
const message = [
|
|
2303
|
+
"memeloop-device-connection-grant-v1",
|
|
2304
|
+
`issuer=${grant.issuer}`,
|
|
2305
|
+
`accountId=${grant.accountId}`,
|
|
2306
|
+
`subjectPeerId=${grant.subjectPeerId}`,
|
|
2307
|
+
`allowedPeerIds=${grant.allowedPeerIds.join(",")}`,
|
|
2308
|
+
`issuedAt=${grant.issuedAt}`,
|
|
2309
|
+
`expiresAt=${grant.expiresAt}`
|
|
2310
|
+
].join("\n");
|
|
2311
|
+
return new TextEncoder().encode(message);
|
|
2312
|
+
}
|
|
2313
|
+
function buildDeviceRelayReservationTokenMessage(token) {
|
|
2314
|
+
const message = [
|
|
2315
|
+
"memeloop-device-relay-admission-v1",
|
|
2316
|
+
`issuer=${token.issuer}`,
|
|
2317
|
+
`accountId=${token.accountId}`,
|
|
2318
|
+
`peerId=${token.peerId}`,
|
|
2319
|
+
`relayMultiaddrs=${token.relayMultiaddrs.join(",")}`,
|
|
2320
|
+
`bootstrapMultiaddrs=${token.bootstrapMultiaddrs.join(",")}`,
|
|
2321
|
+
`issuedAt=${token.issuedAt}`,
|
|
2322
|
+
`expiresAt=${token.expiresAt}`
|
|
2323
|
+
].join("\n");
|
|
2324
|
+
return new TextEncoder().encode(message);
|
|
2325
|
+
}
|
|
2326
|
+
async function signDeviceBinding(input) {
|
|
2327
|
+
const { toString } = await loadUint8arrays();
|
|
2328
|
+
const privateKey = await privateKeyFromIdentity(input.identity);
|
|
2329
|
+
const publicKey = privateKey.publicKey;
|
|
2330
|
+
if (await encodePublicKeyMultibase(publicKey) !== input.identity.publicKeyMultibase) {
|
|
2331
|
+
throw new Error("device_identity_public_key_mismatch");
|
|
2332
|
+
}
|
|
2333
|
+
if (peerIdFromPrivateKey(privateKey).toString() !== input.identity.peerId) {
|
|
2334
|
+
throw new Error("device_identity_peer_id_mismatch");
|
|
2335
|
+
}
|
|
2336
|
+
const message = buildDeviceBindingMessage({
|
|
2337
|
+
accountId: input.accountId,
|
|
2338
|
+
peerId: input.identity.peerId,
|
|
2339
|
+
publicKeyMultibase: input.identity.publicKeyMultibase,
|
|
2340
|
+
nonce: input.nonce
|
|
2341
|
+
});
|
|
2342
|
+
const signature = await privateKey.sign(message);
|
|
2343
|
+
return toString(signature, "base64url");
|
|
2344
|
+
}
|
|
2345
|
+
async function verifyDeviceBinding(input) {
|
|
2346
|
+
try {
|
|
2347
|
+
const { fromString } = await loadUint8arrays();
|
|
2348
|
+
const publicKey = await decodePublicKeyMultibase(input.publicKeyMultibase);
|
|
2349
|
+
if (peerIdFromPublicKey(publicKey).toString() !== input.peerId) {
|
|
2350
|
+
return false;
|
|
2351
|
+
}
|
|
2352
|
+
const message = buildDeviceBindingMessage({
|
|
2353
|
+
accountId: input.accountId,
|
|
2354
|
+
peerId: input.peerId,
|
|
2355
|
+
publicKeyMultibase: input.publicKeyMultibase,
|
|
2356
|
+
nonce: input.cloudNonce
|
|
2357
|
+
});
|
|
2358
|
+
const signature = fromString(input.signature, "base64url");
|
|
2359
|
+
return await publicKey.verify(message, signature);
|
|
2360
|
+
} catch {
|
|
2361
|
+
return false;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
async function verifyDeviceConnectionGrant(input) {
|
|
2365
|
+
try {
|
|
2366
|
+
const now = input.now ?? Date.now();
|
|
2367
|
+
const { grant } = input;
|
|
2368
|
+
if (grant.issuer !== "memeloop-cloud") return false;
|
|
2369
|
+
if (grant.issuedAt > grant.expiresAt) return false;
|
|
2370
|
+
if (grant.expiresAt <= now) return false;
|
|
2371
|
+
if (input.subjectPeerId && grant.subjectPeerId !== input.subjectPeerId) return false;
|
|
2372
|
+
if (input.allowedPeerId && !grant.allowedPeerIds.includes(input.allowedPeerId)) return false;
|
|
2373
|
+
const { fromString } = await loadUint8arrays();
|
|
2374
|
+
const publicKey = await decodePublicKeyMultibase(input.verificationPublicKeyMultibase);
|
|
2375
|
+
const message = buildDeviceConnectionGrantMessage({
|
|
2376
|
+
issuer: grant.issuer,
|
|
2377
|
+
accountId: grant.accountId,
|
|
2378
|
+
subjectPeerId: grant.subjectPeerId,
|
|
2379
|
+
allowedPeerIds: grant.allowedPeerIds,
|
|
2380
|
+
issuedAt: grant.issuedAt,
|
|
2381
|
+
expiresAt: grant.expiresAt
|
|
2382
|
+
});
|
|
2383
|
+
const signature = fromString(grant.signature, "base64url");
|
|
2384
|
+
return await publicKey.verify(message, signature);
|
|
2385
|
+
} catch {
|
|
2386
|
+
return false;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
async function verifyDeviceRelayReservationToken(input) {
|
|
2390
|
+
try {
|
|
2391
|
+
const now = input.now ?? Date.now();
|
|
2392
|
+
const { token } = input;
|
|
2393
|
+
if (token.issuer !== "memeloop-cloud") return false;
|
|
2394
|
+
if (token.issuedAt > token.expiresAt) return false;
|
|
2395
|
+
if (token.expiresAt <= now) return false;
|
|
2396
|
+
if (input.peerId && token.peerId !== input.peerId) return false;
|
|
2397
|
+
const { fromString } = await loadUint8arrays();
|
|
2398
|
+
const publicKey = await decodePublicKeyMultibase(input.verificationPublicKeyMultibase);
|
|
2399
|
+
const message = buildDeviceRelayReservationTokenMessage({
|
|
2400
|
+
issuer: token.issuer,
|
|
2401
|
+
accountId: token.accountId,
|
|
2402
|
+
peerId: token.peerId,
|
|
2403
|
+
relayMultiaddrs: token.relayMultiaddrs,
|
|
2404
|
+
bootstrapMultiaddrs: token.bootstrapMultiaddrs,
|
|
2405
|
+
issuedAt: token.issuedAt,
|
|
2406
|
+
expiresAt: token.expiresAt
|
|
2407
|
+
});
|
|
2408
|
+
const signature = fromString(token.signature, "base64url");
|
|
2409
|
+
return await publicKey.verify(message, signature);
|
|
2410
|
+
} catch {
|
|
2411
|
+
return false;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
function addRelayListenAddress(addresses) {
|
|
2415
|
+
return addresses.includes("/p2p-circuit") ? addresses : [...addresses, "/p2p-circuit"];
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// src/device-network/cloudDeviceAuthorizer.ts
|
|
2419
|
+
var PAIRING_PROTOCOL3 = "/memeloop/pairing/1.0.0";
|
|
2420
|
+
var CloudDeviceAuthorizer = class {
|
|
2421
|
+
constructor(options) {
|
|
2422
|
+
this.options = options;
|
|
2423
|
+
this.allowPairingProtocol = options.allowPairingProtocol ?? true;
|
|
2424
|
+
}
|
|
2425
|
+
allowPairingProtocol;
|
|
2426
|
+
async canOpenProtocol(input) {
|
|
2427
|
+
const record = this.options.getTrustedDevice?.(input.remotePeerId);
|
|
2428
|
+
if (record?.revokedAt) return false;
|
|
2429
|
+
if (input.protocol === PAIRING_PROTOCOL3) return this.allowPairingProtocol;
|
|
2430
|
+
if (record) return true;
|
|
2431
|
+
if (!input.presentedGrant) return false;
|
|
2432
|
+
const direction = input.direction ?? "inbound";
|
|
2433
|
+
return verifyDeviceConnectionGrant({
|
|
2434
|
+
grant: input.presentedGrant,
|
|
2435
|
+
verificationPublicKeyMultibase: this.options.grantVerificationPublicKeyMultibase,
|
|
2436
|
+
subjectPeerId: direction === "outbound" ? this.options.localPeerId : input.remotePeerId,
|
|
2437
|
+
allowedPeerId: direction === "outbound" ? input.remotePeerId : this.options.localPeerId,
|
|
2438
|
+
now: this.options.now?.()
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
// src/device-network/memoryDeviceNetworkService.ts
|
|
2444
|
+
var emptyCapabilities2 = {
|
|
2445
|
+
tools: [],
|
|
2446
|
+
mcpServers: [],
|
|
2447
|
+
hasWiki: false,
|
|
2448
|
+
agentLoop: false,
|
|
2449
|
+
imChannels: [],
|
|
2450
|
+
wikis: []
|
|
2451
|
+
};
|
|
2452
|
+
function toDevice(record, capabilities) {
|
|
2453
|
+
return {
|
|
2454
|
+
peerId: record.peerId,
|
|
2455
|
+
displayName: record.deviceName,
|
|
2456
|
+
platform: record.platform,
|
|
2457
|
+
trustMode: record.trustMode,
|
|
2458
|
+
trusted: !record.revokedAt,
|
|
2459
|
+
reachability: {
|
|
2460
|
+
state: record.revokedAt ? "offline" : "nearby",
|
|
2461
|
+
paths: record.revokedAt ? [] : ["lan"]
|
|
2462
|
+
},
|
|
2463
|
+
capabilities,
|
|
2464
|
+
lastSeen: record.lastSeen
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
var MemoryDeviceNetworkService = class {
|
|
2468
|
+
constructor(options) {
|
|
2469
|
+
this.options = options;
|
|
2470
|
+
this.capabilities = options.capabilities ?? emptyCapabilities2;
|
|
2471
|
+
for (const record of options.trustedDevices ?? []) {
|
|
2472
|
+
this.trustedDevices.set(record.peerId, record);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
started = false;
|
|
2476
|
+
capabilities;
|
|
2477
|
+
trustedDevices = /* @__PURE__ */ new Map();
|
|
2478
|
+
pairingSessions = /* @__PURE__ */ new Map();
|
|
2479
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2480
|
+
pairingListeners = /* @__PURE__ */ new Set();
|
|
2481
|
+
async start() {
|
|
2482
|
+
this.started = true;
|
|
2483
|
+
this.emitDevices();
|
|
2484
|
+
}
|
|
2485
|
+
async stop() {
|
|
2486
|
+
this.started = false;
|
|
2487
|
+
this.emitDevices();
|
|
2488
|
+
}
|
|
2489
|
+
async getLocalDevice() {
|
|
2490
|
+
return {
|
|
2491
|
+
peerId: this.options.identity.peerId,
|
|
2492
|
+
displayName: this.options.identity.deviceName,
|
|
2493
|
+
platform: this.options.identity.platform,
|
|
2494
|
+
trustMode: "local-pairing",
|
|
2495
|
+
reachability: {
|
|
2496
|
+
state: this.started ? "online" : "offline",
|
|
2497
|
+
paths: this.started ? ["lan"] : []
|
|
2498
|
+
},
|
|
2499
|
+
capabilities: this.capabilities,
|
|
2500
|
+
lastSeen: Date.now()
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
async listDevices() {
|
|
2504
|
+
return [...this.trustedDevices.values()].map((record) => toDevice(record, emptyCapabilities2));
|
|
2505
|
+
}
|
|
2506
|
+
observeDevices(listener) {
|
|
2507
|
+
this.listeners.add(listener);
|
|
2508
|
+
void this.listDevices().then(listener);
|
|
2509
|
+
return () => {
|
|
2510
|
+
this.listeners.delete(listener);
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
async listPairingSessions() {
|
|
2514
|
+
return [...this.pairingSessions.values()];
|
|
2515
|
+
}
|
|
2516
|
+
observePairingSessions(listener) {
|
|
2517
|
+
this.pairingListeners.add(listener);
|
|
2518
|
+
void this.listPairingSessions().then(listener);
|
|
2519
|
+
return () => {
|
|
2520
|
+
this.pairingListeners.delete(listener);
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
async requestLocalPairing(peerId, _options = {}) {
|
|
2524
|
+
const createdAt = Date.now();
|
|
2525
|
+
const session = {
|
|
2526
|
+
sessionId: `pairing-${this.options.identity.peerId}-${peerId}-${createdAt}`,
|
|
2527
|
+
localPeerId: this.options.identity.peerId,
|
|
2528
|
+
remotePeerId: peerId,
|
|
2529
|
+
remotePublicKeyMultibase: "",
|
|
2530
|
+
remoteDeviceName: peerId,
|
|
2531
|
+
remotePlatform: "cli",
|
|
2532
|
+
remoteCapabilities: emptyCapabilities2,
|
|
2533
|
+
remoteMultiaddrs: [],
|
|
2534
|
+
direction: "outbound",
|
|
2535
|
+
status: "pending",
|
|
2536
|
+
confirmCode: this.confirmCode(peerId),
|
|
2537
|
+
createdAt,
|
|
2538
|
+
expiresAt: createdAt + 5 * 6e4
|
|
2539
|
+
};
|
|
2540
|
+
this.pairingSessions.set(session.sessionId, session);
|
|
2541
|
+
this.emitPairingSessions();
|
|
2542
|
+
return session;
|
|
2543
|
+
}
|
|
2544
|
+
async acceptPairing(sessionId) {
|
|
2545
|
+
const session = this.pairingSessions.get(sessionId);
|
|
2546
|
+
if (!session) throw new Error("pairing_session_not_found");
|
|
2547
|
+
if (session.expiresAt < Date.now()) throw new Error("pairing_session_expired");
|
|
2548
|
+
if (session.status !== "pending") throw new Error("pairing_session_not_pending");
|
|
2549
|
+
this.trustedDevices.set(session.remotePeerId, {
|
|
2550
|
+
peerId: session.remotePeerId,
|
|
2551
|
+
publicKeyMultibase: session.remotePublicKeyMultibase,
|
|
2552
|
+
deviceName: session.remoteDeviceName,
|
|
2553
|
+
platform: session.remotePlatform,
|
|
2554
|
+
trustMode: "local-pairing",
|
|
2555
|
+
createdAt: Date.now(),
|
|
2556
|
+
lastSeen: Date.now()
|
|
2557
|
+
});
|
|
2558
|
+
session.status = "accepted";
|
|
2559
|
+
this.emitPairingSessions();
|
|
2560
|
+
this.emitDevices();
|
|
2561
|
+
}
|
|
2562
|
+
async rejectPairing(sessionId) {
|
|
2563
|
+
const session = this.pairingSessions.get(sessionId);
|
|
2564
|
+
if (!session) return;
|
|
2565
|
+
session.status = "rejected";
|
|
2566
|
+
this.emitPairingSessions();
|
|
2567
|
+
}
|
|
2568
|
+
async removeTrustedDevice(peerId) {
|
|
2569
|
+
this.trustedDevices.delete(peerId);
|
|
2570
|
+
this.emitDevices();
|
|
2571
|
+
}
|
|
2572
|
+
async openStream(peerId, _protocol, _presentedGrant) {
|
|
2573
|
+
if (!this.trustedDevices.has(peerId)) throw new Error("device_not_trusted");
|
|
2574
|
+
return {
|
|
2575
|
+
source: (async function* emptySource() {
|
|
2576
|
+
})(),
|
|
2577
|
+
async sink() {
|
|
2578
|
+
},
|
|
2579
|
+
async close() {
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
async sendRpc(peerId, _method, _parameters, _presentedGrant) {
|
|
2584
|
+
if (!this.trustedDevices.has(peerId)) throw new Error("device_not_trusted");
|
|
2585
|
+
throw new Error("rpc_handler_not_registered");
|
|
2586
|
+
}
|
|
2587
|
+
async syncWithDevice(peerId, _presentedGrant) {
|
|
2588
|
+
if (!this.trustedDevices.has(peerId)) throw new Error("device_not_trusted");
|
|
2589
|
+
return { ok: true, peerId, syncedAt: Date.now() };
|
|
2590
|
+
}
|
|
2591
|
+
confirmCode(peerId) {
|
|
2592
|
+
const input = `${this.options.identity.peerId}:${peerId}`;
|
|
2593
|
+
let hash = 0;
|
|
2594
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
2595
|
+
hash = hash * 31 + input.charCodeAt(index) >>> 0;
|
|
2596
|
+
}
|
|
2597
|
+
return (hash % 1e6).toString().padStart(6, "0");
|
|
2598
|
+
}
|
|
2599
|
+
emitDevices() {
|
|
2600
|
+
void this.listDevices().then((devices) => {
|
|
2601
|
+
for (const listener of this.listeners) listener(devices);
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
emitPairingSessions() {
|
|
2605
|
+
void this.listPairingSessions().then((sessions) => {
|
|
2606
|
+
for (const listener of this.pairingListeners) listener(sessions);
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
// src/device-network/syncCloudDevices.ts
|
|
2612
|
+
async function syncCloudDevices(input) {
|
|
2613
|
+
const cloudDevices = await input.cloudClient.listDevices();
|
|
2614
|
+
if (cloudDevices.length === 0) return [];
|
|
2615
|
+
const existingRecords = await input.trustStore.loadTrustedDevices();
|
|
2616
|
+
const existingByPeerId = new Map(existingRecords.map((r) => [r.peerId, r]));
|
|
2617
|
+
for (const cloudDevice of cloudDevices) {
|
|
2618
|
+
const existing = existingByPeerId.get(cloudDevice.peerId);
|
|
2619
|
+
const record = {
|
|
2620
|
+
peerId: cloudDevice.peerId,
|
|
2621
|
+
publicKeyMultibase: cloudDevice.publicKeyMultibase,
|
|
2622
|
+
deviceName: cloudDevice.deviceName,
|
|
2623
|
+
platform: cloudDevice.platform,
|
|
2624
|
+
trustMode: "cloud-account",
|
|
2625
|
+
accountId: cloudDevice.accountId,
|
|
2626
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
2627
|
+
lastSeen: cloudDevice.lastSeen,
|
|
2628
|
+
revokedAt: cloudDevice.revokedAt
|
|
2629
|
+
};
|
|
2630
|
+
if (existing) {
|
|
2631
|
+
existing.lastSeen = cloudDevice.lastSeen;
|
|
2632
|
+
existing.revokedAt = cloudDevice.revokedAt;
|
|
2633
|
+
if (existing.publicKeyMultibase !== record.publicKeyMultibase || existing.deviceName !== record.deviceName || existing.platform !== record.platform) {
|
|
2634
|
+
await input.trustStore.saveTrustedDevice(record);
|
|
2635
|
+
}
|
|
2636
|
+
} else {
|
|
2637
|
+
await input.trustStore.saveTrustedDevice(record);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return cloudDevices;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/im/channelManager.ts
|
|
2644
|
+
var IMChannelManager = class {
|
|
2645
|
+
constructor(storage) {
|
|
2646
|
+
this.storage = storage;
|
|
2647
|
+
}
|
|
2648
|
+
bindings = /* @__PURE__ */ new Map();
|
|
2649
|
+
key(channelId, imUserId) {
|
|
2650
|
+
return `${channelId}::${imUserId}`;
|
|
2651
|
+
}
|
|
2652
|
+
async getBinding(channelId, imUserId) {
|
|
2653
|
+
const k = this.key(channelId, imUserId);
|
|
2654
|
+
if (this.storage?.getImBinding) {
|
|
2655
|
+
const row = await this.storage.getImBinding(channelId, imUserId);
|
|
2656
|
+
if (row) {
|
|
2657
|
+
this.bindings.set(k, row);
|
|
2658
|
+
return row;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return this.bindings.get(k);
|
|
2662
|
+
}
|
|
2663
|
+
async setBinding(record) {
|
|
2664
|
+
this.bindings.set(this.key(record.channelId, record.imUserId), record);
|
|
2665
|
+
if (this.storage?.setImBinding) {
|
|
2666
|
+
await this.storage.setImBinding(record);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* 处理入站文本:无 binding 时创建会话并发送首条用户消息。
|
|
2671
|
+
*/
|
|
2672
|
+
async dispatchInbound(message, driver, options) {
|
|
2673
|
+
const existing = await this.getBinding(message.channelId, message.imUserId);
|
|
2674
|
+
if (existing) {
|
|
2675
|
+
await driver.sendMessage({
|
|
2676
|
+
conversationId: existing.activeConversationId,
|
|
2677
|
+
message: message.text
|
|
2678
|
+
});
|
|
2679
|
+
return { conversationId: existing.activeConversationId };
|
|
2680
|
+
}
|
|
2681
|
+
const definitionId = options.defaultDefinitionId;
|
|
2682
|
+
const { conversationId } = await driver.createAgent({
|
|
2683
|
+
definitionId,
|
|
2684
|
+
initialMessage: message.text
|
|
2685
|
+
});
|
|
2686
|
+
await this.setBinding({
|
|
2687
|
+
channelId: message.channelId,
|
|
2688
|
+
imUserId: message.imUserId,
|
|
2689
|
+
activeConversationId: conversationId,
|
|
2690
|
+
createdAt: Date.now(),
|
|
2691
|
+
defaultDefinitionId: definitionId
|
|
2692
|
+
});
|
|
2693
|
+
return { conversationId };
|
|
2694
|
+
}
|
|
2695
|
+
async switchConversation(channelId, imUserId, conversationId) {
|
|
2696
|
+
const current = await this.getBinding(channelId, imUserId);
|
|
2697
|
+
if (current) {
|
|
2698
|
+
await this.setBinding({ ...current, activeConversationId: conversationId });
|
|
2699
|
+
} else {
|
|
2700
|
+
await this.setBinding({
|
|
2701
|
+
channelId,
|
|
2702
|
+
imUserId,
|
|
2703
|
+
activeConversationId: conversationId,
|
|
2704
|
+
createdAt: Date.now()
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
function pickAdapter(adapters, platform) {
|
|
2710
|
+
return adapters.find((a) => a.platform === platform);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
// src/im/imPlatformLimits.ts
|
|
2714
|
+
function imPlatformMaxMessageChars(platform) {
|
|
2715
|
+
switch (platform) {
|
|
2716
|
+
case "telegram":
|
|
2717
|
+
return 4096;
|
|
2718
|
+
case "discord":
|
|
2719
|
+
return 2e3;
|
|
2720
|
+
case "lark":
|
|
2721
|
+
return 4096;
|
|
2722
|
+
case "wecom":
|
|
2723
|
+
return 2048;
|
|
2724
|
+
case "slack":
|
|
2725
|
+
return 4e3;
|
|
2726
|
+
case "qq":
|
|
2727
|
+
return 4500;
|
|
2728
|
+
default:
|
|
2729
|
+
return 3500;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// src/im/slashCommands.ts
|
|
2734
|
+
async function tryHandleImSlashCommand(context) {
|
|
2735
|
+
const t = context.rawText.trim();
|
|
2736
|
+
if (!t.startsWith("/") || t.startsWith("//") || t.length < 2) {
|
|
2737
|
+
return { handled: false, messages: [] };
|
|
2738
|
+
}
|
|
2739
|
+
const parts = t.slice(1).split(/\s+/).filter(Boolean);
|
|
2740
|
+
const cmd = (parts[0] ?? "").toLowerCase();
|
|
2741
|
+
const rest = parts.slice(1).join(" ").trim();
|
|
2742
|
+
const binding = await context.manager.getBinding(context.channelId, context.imUserId);
|
|
2743
|
+
const ok = (lines) => ({ handled: true, messages: lines });
|
|
2744
|
+
if (cmd === "list") {
|
|
2745
|
+
const list = await context.storage.listConversations({ limit: 50 });
|
|
2746
|
+
if (list.length === 0) return ok(["\uFF08\u6682\u65E0\u4F1A\u8BDD\uFF09"]);
|
|
2747
|
+
const body = list.map(
|
|
2748
|
+
(c, index) => `${index + 1}. ${c.title || c.conversationId} \u2014 ${c.conversationId.slice(0, 14)}\u2026`
|
|
2749
|
+
);
|
|
2750
|
+
return ok(["\u53EF\u5207\u6362\u4F1A\u8BDD\uFF1A", ...body.map((l) => `\u2022 ${l}`)]);
|
|
2751
|
+
}
|
|
2752
|
+
if (cmd === "switch") {
|
|
2753
|
+
if (!binding) return ok(["\u5F53\u524D\u65E0\u7ED1\u5B9A\u4F1A\u8BDD\uFF0C\u5148\u7528 /new \u521B\u5EFA\u3002"]);
|
|
2754
|
+
const list = await context.storage.listConversations({ limit: 50 });
|
|
2755
|
+
const argument = rest.trim();
|
|
2756
|
+
if (!argument) return ok(["\u7528\u6CD5\uFF1A/switch <\u7F16\u53F7\u6216 conversationId>"]);
|
|
2757
|
+
const n = parseInt(argument, 10);
|
|
2758
|
+
let targetId;
|
|
2759
|
+
if (!Number.isNaN(n) && n >= 1 && n <= list.length) {
|
|
2760
|
+
targetId = list[n - 1]?.conversationId;
|
|
2761
|
+
} else {
|
|
2762
|
+
targetId = list.find(
|
|
2763
|
+
(c) => c.conversationId === argument || c.conversationId.includes(argument)
|
|
2764
|
+
)?.conversationId;
|
|
2765
|
+
}
|
|
2766
|
+
if (!targetId) return ok([`\u672A\u627E\u5230\u4F1A\u8BDD\uFF1A${argument}`]);
|
|
2767
|
+
await context.manager.switchConversation(context.channelId, context.imUserId, targetId);
|
|
2768
|
+
return ok([`\u5DF2\u5207\u6362\u5230\uFF1A${targetId}`]);
|
|
2769
|
+
}
|
|
2770
|
+
if (cmd === "new" || cmd === "agent") {
|
|
2771
|
+
const definitionId = rest || context.defaultDefinitionId;
|
|
2772
|
+
const { conversationId } = await context.driver.createAgent({ definitionId });
|
|
2773
|
+
await context.manager.setBinding({
|
|
2774
|
+
channelId: context.channelId,
|
|
2775
|
+
imUserId: context.imUserId,
|
|
2776
|
+
activeConversationId: conversationId,
|
|
2777
|
+
createdAt: Date.now(),
|
|
2778
|
+
defaultDefinitionId: definitionId
|
|
2779
|
+
});
|
|
2780
|
+
return ok([`\u5DF2\u65B0\u5EFA\u5E76\u7ED1\u5B9A\u4F1A\u8BDD\u3002
|
|
2781
|
+
ID\uFF1A\`${conversationId}\`
|
|
2782
|
+
\u53D1\u9001\u6D88\u606F\u5373\u53EF\u5F00\u59CB\u3002`]);
|
|
2783
|
+
}
|
|
2784
|
+
if (cmd === "status") {
|
|
2785
|
+
if (!binding) return ok(["\u65E0\u6D3B\u8DC3 IM \u7ED1\u5B9A\u3002"]);
|
|
2786
|
+
const meta = await context.storage.getConversationMeta(binding.activeConversationId);
|
|
2787
|
+
return ok([
|
|
2788
|
+
`\u4F1A\u8BDD\uFF1A${binding.activeConversationId}`,
|
|
2789
|
+
`\u5B9A\u4E49\uFF1A${meta?.definitionId ?? "?"}`,
|
|
2790
|
+
`\u6D88\u606F\u6570\uFF1A${meta?.messageCount ?? "?"}`
|
|
2791
|
+
]);
|
|
2792
|
+
}
|
|
2793
|
+
if (cmd === "cancel") {
|
|
2794
|
+
if (!binding) return ok(["\u65E0\u4F1A\u8BDD\u53EF\u53D6\u6D88\u3002"]);
|
|
2795
|
+
await context.runtime.cancelAgent(binding.activeConversationId);
|
|
2796
|
+
return ok(["\u5DF2\u8BF7\u6C42\u53D6\u6D88\u5F53\u524D\u4EFB\u52A1\u3002"]);
|
|
2797
|
+
}
|
|
2798
|
+
return { handled: false, messages: [] };
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// src/im/streamAgentReplyToIm.ts
|
|
2802
|
+
function stringifyImValue(value) {
|
|
2803
|
+
if (value == null) return "";
|
|
2804
|
+
if (typeof value === "string") return value;
|
|
2805
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
2806
|
+
return String(value);
|
|
2807
|
+
}
|
|
2808
|
+
try {
|
|
2809
|
+
return JSON.stringify(value);
|
|
2810
|
+
} catch {
|
|
2811
|
+
return "[unserializable value]";
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
async function streamRuntimeAgentReplyToIm(options) {
|
|
2815
|
+
const max = imPlatformMaxMessageChars(options.platform);
|
|
2816
|
+
let buf = "";
|
|
2817
|
+
const sendSlice = async (force) => {
|
|
2818
|
+
if (buf.length === 0) return;
|
|
2819
|
+
if (force) {
|
|
2820
|
+
while (buf.length > 0) {
|
|
2821
|
+
const take = Math.min(max, buf.length);
|
|
2822
|
+
const chunk = buf.slice(0, take);
|
|
2823
|
+
buf = buf.slice(take);
|
|
2824
|
+
if (chunk.trim().length > 0) await options.flush(chunk);
|
|
2825
|
+
}
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
while (buf.length >= max) {
|
|
2829
|
+
const chunk = buf.slice(0, max);
|
|
2830
|
+
buf = buf.slice(max);
|
|
2831
|
+
await options.flush(chunk);
|
|
2832
|
+
}
|
|
2833
|
+
};
|
|
2834
|
+
await new Promise((resolve, reject) => {
|
|
2835
|
+
const off = options.runtime.subscribeToUpdates(options.conversationId, (u) => {
|
|
2836
|
+
const o = u;
|
|
2837
|
+
if (o.type === "agent-done") {
|
|
2838
|
+
void sendSlice(true).then(() => {
|
|
2839
|
+
off();
|
|
2840
|
+
resolve();
|
|
2841
|
+
}).catch(reject);
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
if (o.type === "agent-error") {
|
|
2845
|
+
off();
|
|
2846
|
+
reject(new Error(o.error ?? "agent-error"));
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
if (o.type === "agent-step" && o.step?.type === "message") {
|
|
2850
|
+
const stepData = o.step.data;
|
|
2851
|
+
let piece = "";
|
|
2852
|
+
if (typeof stepData === "string") piece = stepData;
|
|
2853
|
+
else if (stepData != null && typeof stepData === "object" && "content" in stepData) {
|
|
2854
|
+
const content = stepData.content;
|
|
2855
|
+
piece = stringifyImValue(content);
|
|
2856
|
+
} else piece = stringifyImValue(stepData);
|
|
2857
|
+
buf += piece;
|
|
2858
|
+
void sendSlice(false);
|
|
2859
|
+
}
|
|
2860
|
+
if (o.type === "agent-step" && o.step?.type === "tool") {
|
|
2861
|
+
const td = o.step.data;
|
|
2862
|
+
const id = td.toolId;
|
|
2863
|
+
if (!id) return;
|
|
2864
|
+
if (id === "ask-question") {
|
|
2865
|
+
const p = td.parameters;
|
|
2866
|
+
const q = typeof p?.question === "string" ? p.question : "";
|
|
2867
|
+
const optLabels = p?.options?.map((o2) => o2.label).filter((x) => typeof x === "string");
|
|
2868
|
+
const askText = options.renderer.renderAskQuestion(q, optLabels);
|
|
2869
|
+
buf += (buf && !buf.endsWith("\n") ? "\n" : "") + askText + "\n";
|
|
2870
|
+
void sendSlice(true);
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
if (td.isError) {
|
|
2874
|
+
buf += (buf && !buf.endsWith("\n") ? "\n" : "") + options.renderer.renderError(stringifyImValue(td.result) || "tool error") + "\n";
|
|
2875
|
+
void sendSlice(false);
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const callLine = options.renderer.renderToolCallSummary(id, td.parameters ?? {});
|
|
2879
|
+
const resultLine = options.renderer.renderToolResultSummary(id, td.result ?? null);
|
|
2880
|
+
buf += (buf && !buf.endsWith("\n") ? "\n" : "") + callLine + (resultLine ? `
|
|
2881
|
+
${resultLine}` : "") + "\n";
|
|
2882
|
+
void sendSlice(false);
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/im/textRenderer.ts
|
|
2889
|
+
var TextMessageRenderer = class {
|
|
2890
|
+
renderPlainText(content) {
|
|
2891
|
+
return content.trim();
|
|
2892
|
+
}
|
|
2893
|
+
renderToolCallSummary(toolName, arguments_) {
|
|
2894
|
+
const s = (() => {
|
|
2895
|
+
try {
|
|
2896
|
+
const index = JSON.stringify(arguments_);
|
|
2897
|
+
if (!index || index === "{}") return "";
|
|
2898
|
+
return index.length > 120 ? `${index.slice(0, 117)}\u2026` : index;
|
|
2899
|
+
} catch {
|
|
2900
|
+
return "";
|
|
2901
|
+
}
|
|
2902
|
+
})();
|
|
2903
|
+
return s ? `\u{1F527} ${toolName}: ${s}` : `\u{1F527} ${toolName}`;
|
|
2904
|
+
}
|
|
2905
|
+
renderToolResultSummary(toolName, result) {
|
|
2906
|
+
if (result == null) return `\u2705 ${toolName}: done`;
|
|
2907
|
+
if (typeof result === "string") {
|
|
2908
|
+
const t = result.trim();
|
|
2909
|
+
if (!t) return `\u2705 ${toolName}: done`;
|
|
2910
|
+
return `\u2705 ${toolName}: ${t.length > 120 ? `${t.slice(0, 117)}\u2026` : t}`;
|
|
2911
|
+
}
|
|
2912
|
+
if (typeof result === "object") {
|
|
2913
|
+
const keys = Object.keys(result);
|
|
2914
|
+
if (keys.length === 0) return `\u2705 ${toolName}: done`;
|
|
2915
|
+
return `\u2705 ${toolName}: {${keys.slice(0, 6).join(", ")}${keys.length > 6 ? ", \u2026" : ""}}`;
|
|
2916
|
+
}
|
|
2917
|
+
if (typeof result === "symbol") return `\u2705 ${toolName}: ${result.toString()}`;
|
|
2918
|
+
if (typeof result === "function") return `\u2705 ${toolName}: [function]`;
|
|
2919
|
+
if (typeof result === "number" || typeof result === "boolean" || typeof result === "bigint") {
|
|
2920
|
+
return `\u2705 ${toolName}: ${String(result)}`;
|
|
2921
|
+
}
|
|
2922
|
+
return `\u2705 ${toolName}: done`;
|
|
2923
|
+
}
|
|
2924
|
+
renderToolApproval(toolName, arguments_) {
|
|
2925
|
+
return `\u23F8 \u9700\u8981\u5BA1\u6279\u6267\u884C\u5DE5\u5177\uFF1A${toolName}
|
|
2926
|
+
\u53C2\u6570\uFF1A
|
|
2927
|
+
\`\`\`json
|
|
2928
|
+
${JSON.stringify(arguments_, null, 2)}
|
|
2929
|
+
\`\`\`
|
|
2930
|
+
\u56DE\u590D Y \u540C\u610F / N \u62D2\u7EDD`;
|
|
2931
|
+
}
|
|
2932
|
+
renderAskQuestion(question, options) {
|
|
2933
|
+
const lines = options?.length ? [`\u2753 ${question}`, ...options.map((o, index) => `${index + 1}. ${o}`)] : [`\u2753 ${question}`];
|
|
2934
|
+
return lines.join("\n");
|
|
2935
|
+
}
|
|
2936
|
+
renderThinking(_content) {
|
|
2937
|
+
return null;
|
|
2938
|
+
}
|
|
2939
|
+
renderError(error2) {
|
|
2940
|
+
return `\u26A0\uFE0F \u9519\u8BEF\uFF1A${error2}`;
|
|
2941
|
+
}
|
|
2942
|
+
renderTodoList(todos) {
|
|
2943
|
+
return ["\u{1F4CB} Todo", ...todos.map((t) => `${t.done ? "[x]" : "[ ]"} ${t.text}`)].join("\n");
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2946
|
+
|
|
2947
|
+
// src/im/tools.ts
|
|
2948
|
+
var imToolDefinitions = [
|
|
2949
|
+
{
|
|
2950
|
+
name: "im.listConversations",
|
|
2951
|
+
description: "\u5217\u51FA\u53EF\u5207\u6362\u7684\u4F1A\u8BDD\uFF08\u7531 memeloop-cli registerImSessionTools \u6CE8\u518C\uFF0C\u4EC5 IM \u6765\u6E90\u4F1A\u8BDD\u53EF\u7528\uFF09\u3002"
|
|
2952
|
+
},
|
|
2953
|
+
{
|
|
2954
|
+
name: "im.switchConversation",
|
|
2955
|
+
description: "\u5207\u6362\u5F53\u524D IM \u7528\u6237\u7ED1\u5B9A\u7684\u6D3B\u8DC3\u4F1A\u8BDD\u3002\u53C2\u6570\uFF1A{ conversationId: string }"
|
|
2956
|
+
},
|
|
2957
|
+
{
|
|
2958
|
+
name: "im.newConversation",
|
|
2959
|
+
description: "\u521B\u5EFA\u65B0\u4F1A\u8BDD\u5E76\u5207\u6362\u7ED1\u5B9A\u3002\u53C2\u6570\uFF1A{ definitionId?: string }"
|
|
2960
|
+
},
|
|
2961
|
+
{
|
|
2962
|
+
name: "im.summarizeHistory",
|
|
2963
|
+
description: "\u8FD4\u56DE\u5F53\u524D\u4F1A\u8BDD\u6700\u8FD1\u6D88\u606F\u7684\u7EAF\u6587\u672C\u6458\u8981\uFF08\u89C4\u5219\u62FC\u63A5\uFF0C\u975E LLM\uFF09\u3002\u53C2\u6570\uFF1A{ maxMessages?: number }"
|
|
2964
|
+
}
|
|
2965
|
+
];
|
|
2966
|
+
async function imSwitchConversation(rt, conversationId) {
|
|
2967
|
+
await rt.manager.switchConversation(rt.channelId, rt.imUserId, conversationId);
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
// src/sync/protocol.ts
|
|
2971
|
+
function isConversationMeta(value) {
|
|
2972
|
+
if (value === null || typeof value !== "object") return false;
|
|
2973
|
+
const o = value;
|
|
2974
|
+
return typeof o.conversationId === "string" && typeof o.title === "string" && typeof o.originNodeId === "string" && typeof o.definitionId === "string" && typeof o.messageCount === "number" && typeof o.lastMessageTimestamp === "number" && typeof o.isUserInitiated === "boolean";
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
// src/tools/builtins/imBuiltinTools.ts
|
|
2978
|
+
import { z } from "zod";
|
|
2979
|
+
var IM_SESSION_TOOL_IDS = [
|
|
2980
|
+
"im.listConversations",
|
|
2981
|
+
"im.switchConversation",
|
|
2982
|
+
"im.newConversation",
|
|
2983
|
+
"im.summarizeHistory"
|
|
2984
|
+
];
|
|
2985
|
+
var listSchema = z.object({});
|
|
2986
|
+
var switchSchema = z.object({
|
|
2987
|
+
conversationId: z.string().min(1)
|
|
2988
|
+
});
|
|
2989
|
+
var newSchema = z.object({
|
|
2990
|
+
definitionId: z.string().min(1).optional()
|
|
2991
|
+
});
|
|
2992
|
+
var summarizeSchema = z.object({
|
|
2993
|
+
maxMessages: z.number().int().positive().max(200).optional()
|
|
2994
|
+
});
|
|
2995
|
+
function error(message) {
|
|
2996
|
+
return { error: message };
|
|
2997
|
+
}
|
|
2998
|
+
async function requireImSource(context, conversationId) {
|
|
2999
|
+
const meta = await context.storage.getConversationMeta(conversationId);
|
|
3000
|
+
const sc = meta?.sourceChannel;
|
|
3001
|
+
if (!sc) {
|
|
3002
|
+
return { error: "im_tools_only_in_im_session" };
|
|
3003
|
+
}
|
|
3004
|
+
return { channelId: sc.channelId, imUserId: sc.imUserId, platform: sc.platform };
|
|
3005
|
+
}
|
|
3006
|
+
async function imListConversationsImpl(arguments_, context, _reg) {
|
|
3007
|
+
const parsed = listSchema.safeParse(arguments_);
|
|
3008
|
+
if (!parsed.success) return error("invalid_args");
|
|
3009
|
+
const cid = context.activeToolConversationId;
|
|
3010
|
+
if (!cid) return error("no_active_conversation");
|
|
3011
|
+
const source = await requireImSource(context, cid);
|
|
3012
|
+
if ("error" in source) return source;
|
|
3013
|
+
const all = await context.storage.listConversations({});
|
|
3014
|
+
if (all.length === 0) return { result: "\uFF08\u6682\u65E0\u4F1A\u8BDD\uFF09" };
|
|
3015
|
+
const sorted = [...all].sort((a, b) => b.lastMessageTimestamp - a.lastMessageTimestamp);
|
|
3016
|
+
const lines = sorted.map((m, index) => {
|
|
3017
|
+
const title = m.title || m.conversationId;
|
|
3018
|
+
const t = new Date(m.lastMessageTimestamp).toISOString();
|
|
3019
|
+
return `${index + 1}. ${title}
|
|
3020
|
+
id: ${m.conversationId}
|
|
3021
|
+
def: ${m.definitionId} | ${t}`;
|
|
3022
|
+
});
|
|
3023
|
+
return { result: `\u53EF\u5207\u6362\u7684\u4F1A\u8BDD\uFF1A
|
|
3024
|
+
${lines.join("\n")}` };
|
|
3025
|
+
}
|
|
3026
|
+
async function imSwitchConversationImpl(arguments_, context, reg) {
|
|
3027
|
+
const parsed = switchSchema.safeParse(arguments_);
|
|
3028
|
+
if (!parsed.success) return error("invalid_args");
|
|
3029
|
+
const cid = context.activeToolConversationId;
|
|
3030
|
+
if (!cid) return error("no_active_conversation");
|
|
3031
|
+
const source = await requireImSource(context, cid);
|
|
3032
|
+
if ("error" in source) return source;
|
|
3033
|
+
await reg.imChannelManager.switchConversation(
|
|
3034
|
+
source.channelId,
|
|
3035
|
+
source.imUserId,
|
|
3036
|
+
parsed.data.conversationId
|
|
3037
|
+
);
|
|
3038
|
+
return { result: `\u5DF2\u5207\u6362\u5230\u4F1A\u8BDD\uFF1A${parsed.data.conversationId}` };
|
|
3039
|
+
}
|
|
3040
|
+
async function imNewConversationImpl(arguments_, context, reg) {
|
|
3041
|
+
const parsed = newSchema.safeParse(arguments_);
|
|
3042
|
+
if (!parsed.success) return error("invalid_args");
|
|
3043
|
+
const cid = context.activeToolConversationId;
|
|
3044
|
+
if (!cid) return error("no_active_conversation");
|
|
3045
|
+
const source = await requireImSource(context, cid);
|
|
3046
|
+
if ("error" in source) return source;
|
|
3047
|
+
const current = await reg.imChannelManager.getBinding(source.channelId, source.imUserId);
|
|
3048
|
+
const definitionId = parsed.data.definitionId?.trim() || current?.defaultDefinitionId || "memeloop:general-assistant";
|
|
3049
|
+
const rt = reg.getMemeLoopRuntime();
|
|
3050
|
+
const { conversationId } = await rt.createAgent({ definitionId, initialMessage: "" });
|
|
3051
|
+
await reg.imChannelManager.setBinding({
|
|
3052
|
+
channelId: source.channelId,
|
|
3053
|
+
imUserId: source.imUserId,
|
|
3054
|
+
activeConversationId: conversationId,
|
|
3055
|
+
createdAt: Date.now(),
|
|
3056
|
+
defaultDefinitionId: definitionId
|
|
3057
|
+
});
|
|
3058
|
+
const meta = await context.storage.getConversationMeta(conversationId);
|
|
3059
|
+
if (meta) {
|
|
3060
|
+
await context.storage.upsertConversationMetadata({
|
|
3061
|
+
...meta,
|
|
3062
|
+
sourceChannel: {
|
|
3063
|
+
channelId: source.channelId,
|
|
3064
|
+
platform: source.platform,
|
|
3065
|
+
imUserId: source.imUserId
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
return {
|
|
3070
|
+
result: `\u5DF2\u65B0\u5EFA\u5E76\u5207\u6362\u5230\u4F1A\u8BDD\uFF1A${conversationId}
|
|
3071
|
+
definition: ${definitionId}
|
|
3072
|
+
\u8BF7\u8BA9\u7528\u6237\u53D1\u9001\u4E0B\u4E00\u6761\u6D88\u606F\u4EE5\u5F00\u59CB\u5BF9\u8BDD\u3002`
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
async function imSummarizeHistoryImpl(arguments_, context, _reg) {
|
|
3076
|
+
const parsed = summarizeSchema.safeParse(arguments_);
|
|
3077
|
+
if (!parsed.success) return error("invalid_args");
|
|
3078
|
+
const cid = context.activeToolConversationId;
|
|
3079
|
+
if (!cid) return error("no_active_conversation");
|
|
3080
|
+
const source = await requireImSource(context, cid);
|
|
3081
|
+
if ("error" in source) return source;
|
|
3082
|
+
void source;
|
|
3083
|
+
const max = parsed.data.maxMessages ?? 40;
|
|
3084
|
+
const msgs = await context.storage.getMessages(cid, { mode: "full-content" });
|
|
3085
|
+
const tail = msgs.slice(-max);
|
|
3086
|
+
if (tail.length === 0) return { result: "\uFF08\u5F53\u524D\u4F1A\u8BDD\u5C1A\u65E0\u6D88\u606F\uFF09" };
|
|
3087
|
+
const lines = tail.map(
|
|
3088
|
+
(m) => `[${m.role}] ${m.content.slice(0, 2e3)}${m.content.length > 2e3 ? "\u2026" : ""}`
|
|
3089
|
+
);
|
|
3090
|
+
return {
|
|
3091
|
+
result: `\u6700\u8FD1 ${tail.length} \u6761\u6D88\u606F\u6458\u8981\uFF08\u4F9B IM \u4E0A\u4E0B\u6587\uFF09\uFF1A
|
|
3092
|
+
${lines.join("\n---\n")}`
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
function registerImSessionBuiltinTools(registry, context, reg) {
|
|
3096
|
+
const bound = (function_) => (arguments_) => function_(arguments_, context, reg);
|
|
3097
|
+
registry.registerTool("im.listConversations", bound(imListConversationsImpl));
|
|
3098
|
+
registry.registerTool("im.switchConversation", bound(imSwitchConversationImpl));
|
|
3099
|
+
registry.registerTool("im.newConversation", bound(imNewConversationImpl));
|
|
3100
|
+
registry.registerTool("im.summarizeHistory", bound(imSummarizeHistoryImpl));
|
|
3101
|
+
registerToolParameterSchema("im.listConversations", listSchema, {
|
|
3102
|
+
displayName: "IM: list conversations",
|
|
3103
|
+
description: "List conversations the user can switch to (IM sessions only)."
|
|
3104
|
+
});
|
|
3105
|
+
registerToolParameterSchema("im.switchConversation", switchSchema, {
|
|
3106
|
+
displayName: "IM: switch conversation",
|
|
3107
|
+
description: "Switch this IM user binding to another conversationId."
|
|
3108
|
+
});
|
|
3109
|
+
registerToolParameterSchema("im.newConversation", newSchema, {
|
|
3110
|
+
displayName: "IM: new conversation",
|
|
3111
|
+
description: "Create a new agent conversation and bind this IM user to it."
|
|
3112
|
+
});
|
|
3113
|
+
registerToolParameterSchema("im.summarizeHistory", summarizeSchema, {
|
|
3114
|
+
displayName: "IM: summarize history",
|
|
3115
|
+
description: "Return a plain-text digest of recent messages in the current conversation."
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/tools/builtins/index.ts
|
|
3120
|
+
function registerBuiltinTools(registry, context) {
|
|
3121
|
+
const promptDestination = registry.getPromptPlugins?.();
|
|
3122
|
+
if (promptDestination) {
|
|
3123
|
+
for (const [id, tool] of pluginRegistry) {
|
|
3124
|
+
promptDestination.set(id, tool);
|
|
3125
|
+
}
|
|
3126
|
+
registerBuiltinPromptPlugins(promptDestination);
|
|
3127
|
+
} else {
|
|
3128
|
+
registerBuiltinPromptPlugins();
|
|
3129
|
+
}
|
|
3130
|
+
registerBuiltinToolPlugins();
|
|
3131
|
+
getLoopRegistry().installPluginsForLoop("*", { ...context, toolRegistry: registry }, [
|
|
3132
|
+
...BUILTIN_TOOL_PLUGIN_IDS
|
|
3133
|
+
]);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
// src/tools/parallelExecution.ts
|
|
3137
|
+
var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
|
|
3138
|
+
var DEFAULT_BATCH_TIMEOUT_MS = 12e4;
|
|
3139
|
+
async function executeWithTimeout(entry) {
|
|
3140
|
+
const timeoutMs = entry.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
3141
|
+
return new Promise((resolve) => {
|
|
3142
|
+
let settled = false;
|
|
3143
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
3144
|
+
if (!settled) {
|
|
3145
|
+
settled = true;
|
|
3146
|
+
resolve({
|
|
3147
|
+
call: entry.call,
|
|
3148
|
+
status: "timeout",
|
|
3149
|
+
error: `Tool "${entry.call.toolId}" timed out after ${timeoutMs}ms`
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
}, timeoutMs) : void 0;
|
|
3153
|
+
entry.executor(entry.call.parameters ?? {}).then((result) => {
|
|
3154
|
+
if (!settled) {
|
|
3155
|
+
settled = true;
|
|
3156
|
+
if (timer) clearTimeout(timer);
|
|
3157
|
+
resolve({ call: entry.call, status: "fulfilled", result });
|
|
3158
|
+
}
|
|
3159
|
+
}).catch((error2) => {
|
|
3160
|
+
if (!settled) {
|
|
3161
|
+
settled = true;
|
|
3162
|
+
if (timer) clearTimeout(timer);
|
|
3163
|
+
resolve({
|
|
3164
|
+
call: entry.call,
|
|
3165
|
+
status: "rejected",
|
|
3166
|
+
result: {
|
|
3167
|
+
success: false,
|
|
3168
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
3169
|
+
},
|
|
3170
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
async function executeToolCallsParallel(entries, batchTimeoutMs = DEFAULT_BATCH_TIMEOUT_MS) {
|
|
3177
|
+
if (entries.length === 0) return [];
|
|
3178
|
+
if (entries.length === 1) {
|
|
3179
|
+
return [await executeWithTimeout(entries[0])];
|
|
3180
|
+
}
|
|
3181
|
+
const promises = entries.map((entry) => executeWithTimeout(entry));
|
|
3182
|
+
if (batchTimeoutMs > 0) {
|
|
3183
|
+
const batchTimer = new Promise((resolve) => {
|
|
3184
|
+
setTimeout(() => {
|
|
3185
|
+
resolve(
|
|
3186
|
+
entries.map((entry) => ({
|
|
3187
|
+
call: entry.call,
|
|
3188
|
+
status: "timeout",
|
|
3189
|
+
error: `Batch timeout: ${batchTimeoutMs}ms exceeded`
|
|
3190
|
+
}))
|
|
3191
|
+
);
|
|
3192
|
+
}, batchTimeoutMs);
|
|
3193
|
+
});
|
|
3194
|
+
return Promise.race([Promise.all(promises), batchTimer]);
|
|
3195
|
+
}
|
|
3196
|
+
return Promise.all(promises);
|
|
3197
|
+
}
|
|
3198
|
+
async function executeToolCallsSequential(entries) {
|
|
3199
|
+
const results = [];
|
|
3200
|
+
for (const entry of entries) {
|
|
3201
|
+
results.push(await executeWithTimeout(entry));
|
|
3202
|
+
}
|
|
3203
|
+
return results;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// src/tools/schemaToToolContent.ts
|
|
3207
|
+
function schemaToToolContent(schema) {
|
|
3208
|
+
const jsonSchema = schema.toJSONSchema?.() ?? {};
|
|
3209
|
+
const meta = Array.isArray(schema.meta) ? schema.meta?.[0] : void 0;
|
|
3210
|
+
const title = meta?.title || jsonSchema.title || "tool";
|
|
3211
|
+
const description = meta?.description || jsonSchema.description || "";
|
|
3212
|
+
const examples = meta?.examples || jsonSchema.examples || [];
|
|
3213
|
+
const props = jsonSchema.properties;
|
|
3214
|
+
const requiredArray = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
|
|
3215
|
+
let parameterLines = "";
|
|
3216
|
+
if (props) {
|
|
3217
|
+
parameterLines = Object.entries(props).map(([key, value]) => {
|
|
3218
|
+
const p = value;
|
|
3219
|
+
const type = p?.type || "string";
|
|
3220
|
+
const desc = p?.description || p?.title || "";
|
|
3221
|
+
const required = requiredArray.includes(key) ? "required" : "optional";
|
|
3222
|
+
return `- ${key} (${type}, ${required}): ${desc}`;
|
|
3223
|
+
}).join("\n");
|
|
3224
|
+
}
|
|
3225
|
+
const exampleSection = examples.map((example) => `- <tool_use name="${title}">${JSON.stringify(example)}</tool_use>`).join("\n");
|
|
3226
|
+
return `
|
|
3227
|
+
## ${title}
|
|
3228
|
+
**Description**: ${description}
|
|
3229
|
+
**Parameters**:
|
|
3230
|
+
${parameterLines}
|
|
3231
|
+
|
|
3232
|
+
**Examples**:
|
|
3233
|
+
${exampleSection}
|
|
3234
|
+
`;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// src/tools/defineTool.ts
|
|
3238
|
+
var MAX_TOOL_RESULT_CHARS = 32e3;
|
|
3239
|
+
var logger = {
|
|
3240
|
+
debug: (..._a) => {
|
|
3241
|
+
},
|
|
3242
|
+
warn: (...a) => {
|
|
3243
|
+
console.warn("[memeloop.defineTool]", ...a);
|
|
3244
|
+
},
|
|
3245
|
+
error: (...a) => {
|
|
3246
|
+
console.error("[memeloop.defineTool]", ...a);
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
function defineTool(definition) {
|
|
3250
|
+
const {
|
|
3251
|
+
toolId,
|
|
3252
|
+
configSchema,
|
|
3253
|
+
llmToolSchemas,
|
|
3254
|
+
onProcessPrompts,
|
|
3255
|
+
onResponseComplete,
|
|
3256
|
+
onPostProcess
|
|
3257
|
+
} = definition;
|
|
3258
|
+
const parameterKey = `${toolId}Param`;
|
|
3259
|
+
const tool = (hooks) => {
|
|
3260
|
+
if (onProcessPrompts) {
|
|
3261
|
+
hooks.processPrompts.tapAsync(`${toolId}-processPrompts`, async (context, callback) => {
|
|
3262
|
+
try {
|
|
3263
|
+
const { toolConfig, prompts, messages, agentFrameworkContext } = context;
|
|
3264
|
+
if (toolConfig.toolId !== toolId) {
|
|
3265
|
+
callback();
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
if (toolConfig.enabled === false) {
|
|
3269
|
+
callback();
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
const rawConfig = toolConfig[parameterKey];
|
|
3273
|
+
if (!rawConfig) {
|
|
3274
|
+
callback();
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
const config = configSchema.parse(rawConfig);
|
|
3278
|
+
const handlerContext = {
|
|
3279
|
+
config,
|
|
3280
|
+
toolConfig,
|
|
3281
|
+
prompts,
|
|
3282
|
+
messages,
|
|
3283
|
+
agentFrameworkContext,
|
|
3284
|
+
findPrompt: (id) => findPromptById(prompts, id),
|
|
3285
|
+
injectToolList: (options) => {
|
|
3286
|
+
const target = findPromptById(prompts, options.targetId);
|
|
3287
|
+
if (!target) {
|
|
3288
|
+
logger.warn(`Target prompt not found for tool list injection`, {
|
|
3289
|
+
targetId: options.targetId,
|
|
3290
|
+
toolId
|
|
3291
|
+
});
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
const schemas = options.toolSchemas ?? (llmToolSchemas ? Object.values(llmToolSchemas) : []);
|
|
3295
|
+
const toolContent = schemas.map((schema) => schemaToToolContent(schema)).join("\n\n");
|
|
3296
|
+
const pluginIndex = context.pluginIndex;
|
|
3297
|
+
const source = pluginIndex !== void 0 ? ["plugins", toolConfig.id] : void 0;
|
|
3298
|
+
const toolPrompt = {
|
|
3299
|
+
id: `${toolId}-tool-list-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
3300
|
+
text: toolContent,
|
|
3301
|
+
caption: options.caption ?? `${definition.displayName} Tools`,
|
|
3302
|
+
enabled: true,
|
|
3303
|
+
source
|
|
3304
|
+
};
|
|
3305
|
+
if (options.position === "child") {
|
|
3306
|
+
if (!target.prompt.children) {
|
|
3307
|
+
target.prompt.children = [];
|
|
3308
|
+
}
|
|
3309
|
+
target.prompt.children.push(toolPrompt);
|
|
3310
|
+
} else if (options.position === "before") {
|
|
3311
|
+
target.parent.splice(target.index, 0, toolPrompt);
|
|
3312
|
+
} else {
|
|
3313
|
+
target.parent.splice(target.index + 1, 0, toolPrompt);
|
|
3314
|
+
}
|
|
3315
|
+
},
|
|
3316
|
+
injectContent: (options) => {
|
|
3317
|
+
const target = findPromptById(prompts, options.targetId);
|
|
3318
|
+
if (!target) {
|
|
3319
|
+
logger.warn(`Target prompt not found for content injection`, {
|
|
3320
|
+
targetId: options.targetId,
|
|
3321
|
+
toolId
|
|
3322
|
+
});
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
const pluginIndex = context.pluginIndex;
|
|
3326
|
+
const source = pluginIndex !== void 0 ? ["plugins", toolConfig.id] : void 0;
|
|
3327
|
+
const contentPrompt = {
|
|
3328
|
+
id: options.id ?? `${toolId}-content-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
3329
|
+
text: options.content,
|
|
3330
|
+
caption: options.caption ?? "Injected Content",
|
|
3331
|
+
enabled: true,
|
|
3332
|
+
source
|
|
3333
|
+
};
|
|
3334
|
+
if (options.position === "child") {
|
|
3335
|
+
if (!target.prompt.children) {
|
|
3336
|
+
target.prompt.children = [];
|
|
3337
|
+
}
|
|
3338
|
+
target.prompt.children.push(contentPrompt);
|
|
3339
|
+
} else if (options.position === "before") {
|
|
3340
|
+
target.parent.splice(target.index, 0, contentPrompt);
|
|
3341
|
+
} else {
|
|
3342
|
+
target.parent.splice(target.index + 1, 0, contentPrompt);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
};
|
|
3346
|
+
await onProcessPrompts(handlerContext);
|
|
3347
|
+
callback();
|
|
3348
|
+
} catch (error2) {
|
|
3349
|
+
logger.error(`Error in ${toolId} processPrompts handler`, error2);
|
|
3350
|
+
callback();
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
if (onResponseComplete) {
|
|
3355
|
+
hooks.responseComplete.tapAsync(`${toolId}-responseComplete`, async (context, callback) => {
|
|
3356
|
+
try {
|
|
3357
|
+
const {
|
|
3358
|
+
agentFrameworkContext,
|
|
3359
|
+
response,
|
|
3360
|
+
agentFrameworkConfig,
|
|
3361
|
+
requestId,
|
|
3362
|
+
toolConfig: directToolConfig
|
|
3363
|
+
} = context;
|
|
3364
|
+
const configuredToolConfig = agentFrameworkConfig?.plugins?.find(
|
|
3365
|
+
(p) => p.toolId === toolId
|
|
3366
|
+
);
|
|
3367
|
+
const ourToolConfig = configuredToolConfig ?? (directToolConfig?.toolId === toolId ? directToolConfig : void 0);
|
|
3368
|
+
if (!ourToolConfig) {
|
|
3369
|
+
callback();
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
if (ourToolConfig.enabled === false) {
|
|
3373
|
+
callback();
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
if (response.status !== "done" || !response.content) {
|
|
3377
|
+
callback();
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
const { calls: allCalls, parallel: isParallel } = matchAllToolCallings(response.content);
|
|
3381
|
+
const toolCall = allCalls.length > 0 ? allCalls[0] : null;
|
|
3382
|
+
const rawConfig = ourToolConfig[parameterKey];
|
|
3383
|
+
let config;
|
|
3384
|
+
if (rawConfig) {
|
|
3385
|
+
try {
|
|
3386
|
+
config = configSchema.parse(rawConfig);
|
|
3387
|
+
} catch (parseError) {
|
|
3388
|
+
logger.warn(`Failed to parse config for ${toolId}`, parseError);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
const persist = agentFrameworkContext.persistAgentMessage;
|
|
3392
|
+
const handlerContext = {
|
|
3393
|
+
config,
|
|
3394
|
+
toolConfig: ourToolConfig,
|
|
3395
|
+
messages: agentFrameworkContext.agent.messages,
|
|
3396
|
+
agentFrameworkContext,
|
|
3397
|
+
response,
|
|
3398
|
+
toolCall,
|
|
3399
|
+
allToolCalls: allCalls,
|
|
3400
|
+
isParallel,
|
|
3401
|
+
agentFrameworkConfig,
|
|
3402
|
+
hooks,
|
|
3403
|
+
requestId,
|
|
3404
|
+
findPrompt: () => void 0,
|
|
3405
|
+
injectToolList: () => {
|
|
3406
|
+
logger.warn("injectToolList is not available in response phase");
|
|
3407
|
+
},
|
|
3408
|
+
injectContent: () => {
|
|
3409
|
+
logger.warn("injectContent is not available in response phase");
|
|
3410
|
+
},
|
|
3411
|
+
executeToolCall: async (toolName, executor) => {
|
|
3412
|
+
const toolNameString = String(toolName);
|
|
3413
|
+
if (!toolCall || toolCall.toolId !== toolNameString) {
|
|
3414
|
+
return false;
|
|
3415
|
+
}
|
|
3416
|
+
const toolSchema = llmToolSchemas?.[toolName];
|
|
3417
|
+
if (!toolSchema) {
|
|
3418
|
+
logger.error(`No schema found for tool: ${toolNameString}`);
|
|
3419
|
+
return false;
|
|
3420
|
+
}
|
|
3421
|
+
try {
|
|
3422
|
+
const validatedParameters = toolSchema.parse(toolCall.parameters);
|
|
3423
|
+
const approvalConfig = ourToolConfig.approval;
|
|
3424
|
+
const decision = evaluateApproval(
|
|
3425
|
+
approvalConfig,
|
|
3426
|
+
toolNameString,
|
|
3427
|
+
validatedParameters
|
|
3428
|
+
);
|
|
3429
|
+
if (decision === "deny") {
|
|
3430
|
+
handlerContext.addToolResult({
|
|
3431
|
+
toolName: toolNameString,
|
|
3432
|
+
parameters: validatedParameters,
|
|
3433
|
+
result: "Tool execution denied by approval policy.",
|
|
3434
|
+
isError: true,
|
|
3435
|
+
duration: 2
|
|
3436
|
+
});
|
|
3437
|
+
handlerContext.yieldToSelf();
|
|
3438
|
+
return true;
|
|
3439
|
+
}
|
|
3440
|
+
if (decision === "pending") {
|
|
3441
|
+
const approvalId = `approval-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
3442
|
+
const userDecision = await requestApproval({
|
|
3443
|
+
approvalId,
|
|
3444
|
+
agentId: agentFrameworkContext.agent.id,
|
|
3445
|
+
toolName: toolNameString,
|
|
3446
|
+
parameters: validatedParameters,
|
|
3447
|
+
originalText: toolCall.originalText,
|
|
3448
|
+
created: /* @__PURE__ */ new Date()
|
|
3449
|
+
});
|
|
3450
|
+
if (userDecision === "deny") {
|
|
3451
|
+
handlerContext.addToolResult({
|
|
3452
|
+
toolName: toolNameString,
|
|
3453
|
+
parameters: validatedParameters,
|
|
3454
|
+
result: "Tool execution denied by user.",
|
|
3455
|
+
isError: true,
|
|
3456
|
+
duration: 2
|
|
3457
|
+
});
|
|
3458
|
+
handlerContext.yieldToSelf();
|
|
3459
|
+
return true;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
const result = await executor(validatedParameters);
|
|
3463
|
+
const toolResultDuration = config?.toolResultDuration ?? 1;
|
|
3464
|
+
handlerContext.addToolResult({
|
|
3465
|
+
toolName: toolNameString,
|
|
3466
|
+
parameters: validatedParameters,
|
|
3467
|
+
result: result.success ? result.data ?? "Success" : result.error ?? "Unknown error",
|
|
3468
|
+
isError: !result.success,
|
|
3469
|
+
duration: toolResultDuration
|
|
3470
|
+
});
|
|
3471
|
+
handlerContext.yieldToSelf();
|
|
3472
|
+
await hooks.toolExecuted.promise({
|
|
3473
|
+
agentFrameworkContext,
|
|
3474
|
+
toolResult: result,
|
|
3475
|
+
toolInfo: {
|
|
3476
|
+
toolId: toolNameString,
|
|
3477
|
+
parameters: validatedParameters,
|
|
3478
|
+
originalText: toolCall.originalText
|
|
3479
|
+
},
|
|
3480
|
+
requestId
|
|
3481
|
+
});
|
|
3482
|
+
return true;
|
|
3483
|
+
} catch (error2) {
|
|
3484
|
+
logger.error(`Tool execution failed: ${toolNameString}`, error2);
|
|
3485
|
+
handlerContext.addToolResult({
|
|
3486
|
+
toolName: toolNameString,
|
|
3487
|
+
parameters: toolCall.parameters,
|
|
3488
|
+
result: error2 instanceof Error ? error2.message : String(error2),
|
|
3489
|
+
isError: true,
|
|
3490
|
+
duration: 2
|
|
3491
|
+
});
|
|
3492
|
+
handlerContext.yieldToSelf();
|
|
3493
|
+
await hooks.toolExecuted.promise({
|
|
3494
|
+
agentFrameworkContext,
|
|
3495
|
+
toolResult: {
|
|
3496
|
+
success: false,
|
|
3497
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
3498
|
+
},
|
|
3499
|
+
toolInfo: {
|
|
3500
|
+
toolId: toolNameString,
|
|
3501
|
+
parameters: toolCall.parameters || {}
|
|
3502
|
+
}
|
|
3503
|
+
});
|
|
3504
|
+
return true;
|
|
3505
|
+
}
|
|
3506
|
+
},
|
|
3507
|
+
addToolResult: (options) => {
|
|
3508
|
+
const now = /* @__PURE__ */ new Date();
|
|
3509
|
+
let resultContent = options.result;
|
|
3510
|
+
if (resultContent.length > MAX_TOOL_RESULT_CHARS) {
|
|
3511
|
+
const truncated = resultContent.slice(0, MAX_TOOL_RESULT_CHARS);
|
|
3512
|
+
resultContent = `${truncated}
|
|
3513
|
+
|
|
3514
|
+
[... truncated \u2014 result was ${options.result.length} chars, showing first ${MAX_TOOL_RESULT_CHARS}]`;
|
|
3515
|
+
}
|
|
3516
|
+
const toolResultText = `<functions_result>
|
|
3517
|
+
Tool: ${options.toolName}
|
|
3518
|
+
Parameters: ${JSON.stringify(options.parameters)}
|
|
3519
|
+
${options.isError ? "Error" : "Result"}: ${resultContent}
|
|
3520
|
+
</functions_result>`;
|
|
3521
|
+
const toolResultMessage = {
|
|
3522
|
+
messageId: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
3523
|
+
conversationId: agentFrameworkContext.agent.id,
|
|
3524
|
+
originNodeId: "local",
|
|
3525
|
+
timestamp: now.getTime(),
|
|
3526
|
+
lamportClock: 0,
|
|
3527
|
+
role: "tool",
|
|
3528
|
+
content: toolResultText,
|
|
3529
|
+
duration: options.duration ?? 1,
|
|
3530
|
+
metadata: {
|
|
3531
|
+
isToolResult: true,
|
|
3532
|
+
isError: options.isError ?? false,
|
|
3533
|
+
toolId: options.toolName,
|
|
3534
|
+
toolParameters: options.parameters,
|
|
3535
|
+
isPersisted: false,
|
|
3536
|
+
isComplete: true
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
agentFrameworkContext.agent.messages.push(toolResultMessage);
|
|
3540
|
+
const aiMessages = agentFrameworkContext.agent.messages.filter(
|
|
3541
|
+
(m) => m.role === "assistant"
|
|
3542
|
+
);
|
|
3543
|
+
if (aiMessages.length > 0) {
|
|
3544
|
+
const latestAiMessage = aiMessages[aiMessages.length - 1];
|
|
3545
|
+
if (latestAiMessage.content === response.content && !latestAiMessage.metadata?.containsToolCall) {
|
|
3546
|
+
latestAiMessage.duration = 1;
|
|
3547
|
+
latestAiMessage.metadata = {
|
|
3548
|
+
...latestAiMessage.metadata,
|
|
3549
|
+
containsToolCall: true,
|
|
3550
|
+
toolId: options.toolName,
|
|
3551
|
+
isPersisted: true
|
|
3552
|
+
};
|
|
3553
|
+
if (persist) {
|
|
3554
|
+
void (async () => {
|
|
3555
|
+
try {
|
|
3556
|
+
await persist(latestAiMessage);
|
|
3557
|
+
} catch (error2) {
|
|
3558
|
+
logger.warn("Failed to persist AI message with tool call", {
|
|
3559
|
+
error: error2,
|
|
3560
|
+
messageId: latestAiMessage.messageId
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
})();
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
if (persist) {
|
|
3568
|
+
void (async () => {
|
|
3569
|
+
try {
|
|
3570
|
+
await persist(toolResultMessage);
|
|
3571
|
+
toolResultMessage.metadata = {
|
|
3572
|
+
...toolResultMessage.metadata,
|
|
3573
|
+
isPersisted: true
|
|
3574
|
+
};
|
|
3575
|
+
} catch (error2) {
|
|
3576
|
+
logger.warn("Failed to persist tool result", {
|
|
3577
|
+
error: error2,
|
|
3578
|
+
messageId: toolResultMessage.messageId
|
|
3579
|
+
});
|
|
3580
|
+
}
|
|
3581
|
+
})();
|
|
3582
|
+
}
|
|
3583
|
+
},
|
|
3584
|
+
yieldToSelf: () => {
|
|
3585
|
+
const context_ = context;
|
|
3586
|
+
if (!context_.actions) {
|
|
3587
|
+
context_.actions = {};
|
|
3588
|
+
}
|
|
3589
|
+
context_.actions.yieldNextRoundTo = "self";
|
|
3590
|
+
},
|
|
3591
|
+
yieldToHuman: () => {
|
|
3592
|
+
const context_ = context;
|
|
3593
|
+
if (!context_.actions) {
|
|
3594
|
+
context_.actions = {};
|
|
3595
|
+
}
|
|
3596
|
+
context_.actions.yieldNextRoundTo = "human";
|
|
3597
|
+
},
|
|
3598
|
+
executeAllMatchingToolCalls: async (toolName, executor, options) => {
|
|
3599
|
+
const toolNameString = String(toolName);
|
|
3600
|
+
const matchingCalls = allCalls.filter((call) => call.toolId === toolNameString);
|
|
3601
|
+
if (matchingCalls.length === 0) return 0;
|
|
3602
|
+
const toolSchema = llmToolSchemas?.[toolName];
|
|
3603
|
+
if (!toolSchema) {
|
|
3604
|
+
logger.error(`No schema found for tool: ${toolNameString}`);
|
|
3605
|
+
return 0;
|
|
3606
|
+
}
|
|
3607
|
+
const toolResultDuration = config?.toolResultDuration ?? 1;
|
|
3608
|
+
const entries = [];
|
|
3609
|
+
const approvalConfig = ourToolConfig.approval;
|
|
3610
|
+
const batchDecision = evaluateApproval(
|
|
3611
|
+
approvalConfig,
|
|
3612
|
+
toolNameString,
|
|
3613
|
+
matchingCalls[0]?.parameters ?? {}
|
|
3614
|
+
);
|
|
3615
|
+
if (batchDecision === "deny") {
|
|
3616
|
+
for (const call of matchingCalls) {
|
|
3617
|
+
handlerContext.addToolResult({
|
|
3618
|
+
toolName: toolNameString,
|
|
3619
|
+
parameters: call.parameters,
|
|
3620
|
+
result: "Tool execution denied by approval policy.",
|
|
3621
|
+
isError: true,
|
|
3622
|
+
duration: toolResultDuration
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
handlerContext.yieldToSelf();
|
|
3626
|
+
return matchingCalls.length;
|
|
3627
|
+
}
|
|
3628
|
+
if (batchDecision === "pending") {
|
|
3629
|
+
const approvalId = `approval-batch-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
3630
|
+
const userDecision = await requestApproval({
|
|
3631
|
+
approvalId,
|
|
3632
|
+
agentId: agentFrameworkContext.agent.id,
|
|
3633
|
+
toolName: toolNameString,
|
|
3634
|
+
parameters: {
|
|
3635
|
+
_batchSize: matchingCalls.length,
|
|
3636
|
+
_firstCallParams: matchingCalls[0]?.parameters
|
|
3637
|
+
},
|
|
3638
|
+
created: /* @__PURE__ */ new Date()
|
|
3639
|
+
});
|
|
3640
|
+
if (userDecision === "deny") {
|
|
3641
|
+
for (const call of matchingCalls) {
|
|
3642
|
+
handlerContext.addToolResult({
|
|
3643
|
+
toolName: toolNameString,
|
|
3644
|
+
parameters: call.parameters,
|
|
3645
|
+
result: "Tool execution denied by user.",
|
|
3646
|
+
isError: true,
|
|
3647
|
+
duration: toolResultDuration
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
handlerContext.yieldToSelf();
|
|
3651
|
+
return matchingCalls.length;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
for (const call of matchingCalls) {
|
|
3655
|
+
try {
|
|
3656
|
+
const validatedParameters = toolSchema.parse(call.parameters);
|
|
3657
|
+
entries.push({
|
|
3658
|
+
call,
|
|
3659
|
+
executor: async () => executor(validatedParameters),
|
|
3660
|
+
timeoutMs: options?.timeoutMs
|
|
3661
|
+
});
|
|
3662
|
+
} catch (validationError) {
|
|
3663
|
+
handlerContext.addToolResult({
|
|
3664
|
+
toolName: toolNameString,
|
|
3665
|
+
parameters: call.parameters,
|
|
3666
|
+
result: `Parameter validation failed: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
|
|
3667
|
+
isError: true,
|
|
3668
|
+
duration: toolResultDuration
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
if (entries.length === 0) return matchingCalls.length;
|
|
3673
|
+
let results;
|
|
3674
|
+
if (isParallel) {
|
|
3675
|
+
results = await executeToolCallsParallel(entries);
|
|
3676
|
+
} else {
|
|
3677
|
+
results = await executeToolCallsSequential(entries);
|
|
3678
|
+
}
|
|
3679
|
+
for (const result of results) {
|
|
3680
|
+
const isError = result.status !== "fulfilled" || result.result !== void 0 && !result.result.success;
|
|
3681
|
+
const resultText = result.status === "timeout" ? result.error ?? "Tool execution timed out" : result.status === "rejected" ? result.error ?? "Tool execution failed" : result.result?.success ? result.result.data ?? "Success" : result.result?.error ?? "Unknown error";
|
|
3682
|
+
handlerContext.addToolResult({
|
|
3683
|
+
toolName: toolNameString,
|
|
3684
|
+
parameters: result.call.parameters,
|
|
3685
|
+
result: resultText,
|
|
3686
|
+
isError,
|
|
3687
|
+
duration: toolResultDuration
|
|
3688
|
+
});
|
|
3689
|
+
await hooks.toolExecuted.promise({
|
|
3690
|
+
agentFrameworkContext,
|
|
3691
|
+
toolResult: result.result ?? { success: false, error: resultText },
|
|
3692
|
+
toolInfo: {
|
|
3693
|
+
toolId: toolNameString,
|
|
3694
|
+
parameters: result.call.parameters ?? {},
|
|
3695
|
+
originalText: result.call.originalText
|
|
3696
|
+
},
|
|
3697
|
+
requestId
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
handlerContext.yieldToSelf();
|
|
3701
|
+
return matchingCalls.length;
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
await onResponseComplete(handlerContext);
|
|
3705
|
+
callback();
|
|
3706
|
+
} catch (error2) {
|
|
3707
|
+
logger.error(`Error in ${toolId} responseComplete handler`, error2);
|
|
3708
|
+
callback();
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
if (onPostProcess) {
|
|
3713
|
+
hooks.postProcess.tapAsync(
|
|
3714
|
+
`${toolId}-postProcess`,
|
|
3715
|
+
async (context, callback) => {
|
|
3716
|
+
try {
|
|
3717
|
+
const { toolConfig, prompts, messages, agentFrameworkContext, llmResponse, responses } = context;
|
|
3718
|
+
if (toolConfig.toolId !== toolId) {
|
|
3719
|
+
callback();
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
if (toolConfig.enabled === false) {
|
|
3723
|
+
callback();
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
const rawConfig = toolConfig[parameterKey];
|
|
3727
|
+
if (!rawConfig) {
|
|
3728
|
+
callback();
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
const config = configSchema.parse(rawConfig);
|
|
3732
|
+
const handlerContext = {
|
|
3733
|
+
config,
|
|
3734
|
+
toolConfig,
|
|
3735
|
+
prompts,
|
|
3736
|
+
messages,
|
|
3737
|
+
agentFrameworkContext,
|
|
3738
|
+
llmResponse,
|
|
3739
|
+
responses,
|
|
3740
|
+
findPrompt: (id) => findPromptById(prompts, id),
|
|
3741
|
+
injectToolList: () => {
|
|
3742
|
+
logger.warn("injectToolList is not recommended in postProcess phase");
|
|
3743
|
+
},
|
|
3744
|
+
injectContent: () => {
|
|
3745
|
+
logger.warn("injectContent is not recommended in postProcess phase");
|
|
3746
|
+
}
|
|
3747
|
+
};
|
|
3748
|
+
await onPostProcess(handlerContext);
|
|
3749
|
+
callback();
|
|
3750
|
+
} catch (error2) {
|
|
3751
|
+
logger.error(`Error in ${toolId} postProcess handler`, error2);
|
|
3752
|
+
callback();
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
};
|
|
3758
|
+
getActivePluginRegistry().set(toolId, tool);
|
|
3759
|
+
return {
|
|
3760
|
+
tool,
|
|
3761
|
+
toolId,
|
|
3762
|
+
configSchema,
|
|
3763
|
+
llmToolSchemas,
|
|
3764
|
+
displayName: definition.displayName,
|
|
3765
|
+
description: definition.description
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
// src/tools/hostAgentTools.ts
|
|
3770
|
+
function isPluginConfig(value) {
|
|
3771
|
+
return typeof value === "object" && value !== null;
|
|
3772
|
+
}
|
|
3773
|
+
function mergeAgentToolsIntoFrameworkConfig(frameworkConfig, agentTools) {
|
|
3774
|
+
const baseConfig = { ...frameworkConfig ?? {} };
|
|
3775
|
+
const rawPlugins = Array.isArray(baseConfig.plugins) ? baseConfig.plugins : [];
|
|
3776
|
+
const pluginByToolId = /* @__PURE__ */ new Map();
|
|
3777
|
+
const pluginWithoutToolId = [];
|
|
3778
|
+
for (const plugin of rawPlugins) {
|
|
3779
|
+
if (isPluginConfig(plugin) && typeof plugin.toolId === "string" && plugin.toolId.length > 0) {
|
|
3780
|
+
pluginByToolId.set(plugin.toolId, plugin);
|
|
3781
|
+
} else {
|
|
3782
|
+
pluginWithoutToolId.push(plugin);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
for (const tool of agentTools ?? []) {
|
|
3786
|
+
if (!tool.toolId) continue;
|
|
3787
|
+
pluginByToolId.set(tool.toolId, {
|
|
3788
|
+
id: `${tool.toolId}-agent-tool`,
|
|
3789
|
+
toolId: tool.toolId,
|
|
3790
|
+
enabled: tool.enabled ?? true,
|
|
3791
|
+
...tool.parameters ?? {}
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
return {
|
|
3795
|
+
...baseConfig,
|
|
3796
|
+
prompts: Array.isArray(baseConfig.prompts) ? baseConfig.prompts : [],
|
|
3797
|
+
response: Array.isArray(baseConfig.response) ? baseConfig.response : void 0,
|
|
3798
|
+
plugins: [...pluginWithoutToolId, ...pluginByToolId.values()]
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/tools/toolRegistry.ts
|
|
3803
|
+
var ToolDefinitionRegistry = class {
|
|
3804
|
+
registry = /* @__PURE__ */ new Map();
|
|
3805
|
+
registerToolDefinition(definition) {
|
|
3806
|
+
const toolDefinition = defineTool(definition);
|
|
3807
|
+
registerToolParameterSchema(toolDefinition.toolId, toolDefinition.configSchema, {
|
|
3808
|
+
displayName: toolDefinition.displayName,
|
|
3809
|
+
description: toolDefinition.description
|
|
3810
|
+
});
|
|
3811
|
+
this.registry.set(toolDefinition.toolId, toolDefinition);
|
|
3812
|
+
return toolDefinition;
|
|
3813
|
+
}
|
|
3814
|
+
getAllToolDefinitions() {
|
|
3815
|
+
return this.registry;
|
|
3816
|
+
}
|
|
3817
|
+
getToolDefinition(toolId) {
|
|
3818
|
+
return this.registry.get(toolId);
|
|
3819
|
+
}
|
|
3820
|
+
};
|
|
3821
|
+
var defaultToolDefinitionRegistry = new ToolDefinitionRegistry();
|
|
3822
|
+
function getDefaultToolDefinitionRegistry() {
|
|
3823
|
+
return defaultToolDefinitionRegistry;
|
|
3824
|
+
}
|
|
3825
|
+
function registerToolDefinition(definition) {
|
|
3826
|
+
return defaultToolDefinitionRegistry.registerToolDefinition(definition);
|
|
3827
|
+
}
|
|
3828
|
+
function getAllToolDefinitions() {
|
|
3829
|
+
return defaultToolDefinitionRegistry.getAllToolDefinitions();
|
|
3830
|
+
}
|
|
3831
|
+
function getToolDefinition(toolId) {
|
|
3832
|
+
return defaultToolDefinitionRegistry.getToolDefinition(toolId);
|
|
3833
|
+
}
|
|
3834
|
+
export {
|
|
3835
|
+
AGENT_AGENT_LOOP_ID,
|
|
3836
|
+
AGENT_TOOL_LOOP_ID,
|
|
3837
|
+
ALL_CATEGORIES,
|
|
3838
|
+
ASK_QUESTION_TOOL_ID,
|
|
3839
|
+
ASK_USER_QUESTION_TOOL_ID,
|
|
3840
|
+
AgentCreationController,
|
|
3841
|
+
AgentDefinitionEditorController,
|
|
3842
|
+
AgentProfileRegistry,
|
|
3843
|
+
AgentSessionController,
|
|
3844
|
+
BUILTIN_AGENT_AGENT_LOOP_QUALITY_GATE_SCRIPT_ID,
|
|
3845
|
+
BUILTIN_AGENT_PROFILES,
|
|
3846
|
+
BUILTIN_AGENT_TOOL_LOOP_DEFAULT_SCRIPT_ID,
|
|
3847
|
+
BUILTIN_TOOL_PLUGIN_IDS,
|
|
3848
|
+
ChatSyncEngine,
|
|
3849
|
+
CloudDeviceAuthorizer,
|
|
3850
|
+
HookRegistry,
|
|
3851
|
+
IMChannelManager,
|
|
3852
|
+
IM_SESSION_TOOL_IDS,
|
|
3853
|
+
LIBP2P_RPC_REQUEST_TYPE,
|
|
3854
|
+
LIBP2P_RPC_RESPONSE_TYPE,
|
|
3855
|
+
LIBP2P_SYNC_REQUEST_TYPE,
|
|
3856
|
+
LIBP2P_SYNC_RESPONSE_TYPE,
|
|
3857
|
+
Libp2pDeviceNetworkService,
|
|
3858
|
+
Libp2pDeviceSyncTransport,
|
|
3859
|
+
LocalTrustDeviceAuthorizer,
|
|
3860
|
+
MEMELOOP_STRUCTURED_TOOL_KEY,
|
|
3861
|
+
MemoryDeviceNetworkService,
|
|
3862
|
+
PERMISSIONS_TABLE_DDL,
|
|
3863
|
+
PLUGIN_ASK_QUESTION,
|
|
3864
|
+
PLUGIN_ASK_USER_QUESTION,
|
|
3865
|
+
PLUGIN_MCP_CLIENT,
|
|
3866
|
+
PLUGIN_MCP_FORWARD,
|
|
3867
|
+
PLUGIN_REMOTE_AGENT,
|
|
3868
|
+
PLUGIN_SPAWN_AGENT,
|
|
3869
|
+
PLUGIN_TASK,
|
|
3870
|
+
PLUGIN_TODO_WRITE,
|
|
3871
|
+
PeerNodeSyncAdapter,
|
|
3872
|
+
PromptPreviewController,
|
|
3873
|
+
ProviderRegistry,
|
|
3874
|
+
SessionStorage,
|
|
3875
|
+
TODO_WRITE_TOOL_ID,
|
|
3876
|
+
TextMessageRenderer,
|
|
3877
|
+
TokenTracker,
|
|
3878
|
+
ToolDefinitionRegistry,
|
|
3879
|
+
__clearTodoStore,
|
|
3880
|
+
askQuestionConfigSchema,
|
|
3881
|
+
askQuestionImpl,
|
|
3882
|
+
askUserQuestionConfigSchema,
|
|
3883
|
+
askUserQuestionImpl,
|
|
3884
|
+
attachmentBlobFromWire,
|
|
3885
|
+
autoCompact,
|
|
3886
|
+
buildAgent,
|
|
3887
|
+
buildDeviceBindingMessage,
|
|
3888
|
+
buildDeviceConnectionGrantMessage,
|
|
3889
|
+
buildDeviceRelayReservationTokenMessage,
|
|
3890
|
+
buildMemeloopFileUri,
|
|
3891
|
+
buildMemeloopUri,
|
|
3892
|
+
builtinAgentAgentLoopSources,
|
|
3893
|
+
builtinAgentToolLoopSources,
|
|
3894
|
+
cancelPendingApprovals,
|
|
3895
|
+
checkPermission,
|
|
3896
|
+
clearHooks,
|
|
3897
|
+
clearPluginRegistrations,
|
|
3898
|
+
compactMessages,
|
|
3899
|
+
createAgentAgentLoopDefinition,
|
|
3900
|
+
createAgentFrameworkHooks,
|
|
3901
|
+
createAgentInstanceFromDefinition,
|
|
3902
|
+
createAgentLoopRunner,
|
|
3903
|
+
createAgentRuntimeDeviceRpcHandler,
|
|
3904
|
+
createAgentToolLoopDefinition,
|
|
3905
|
+
createAgentToolLoopRunner,
|
|
3906
|
+
createChatMessage,
|
|
3907
|
+
createCheckpointRecord,
|
|
3908
|
+
createDeviceIdentity,
|
|
3909
|
+
createFetchLLMProvider,
|
|
3910
|
+
createHooksWithPlugins,
|
|
3911
|
+
createInstanceDeltaFromDefinition,
|
|
3912
|
+
createMemeLoopRuntime,
|
|
3913
|
+
createPluginAPI,
|
|
3914
|
+
decodeAttachmentBlobRpc,
|
|
3915
|
+
decodePublicKeyMultibase,
|
|
3916
|
+
defineTool,
|
|
3917
|
+
encodePublicKeyMultibase,
|
|
3918
|
+
evaluateApproval,
|
|
3919
|
+
executeHooks,
|
|
3920
|
+
executeToolCallsParallel,
|
|
3921
|
+
executeToolCallsSequential,
|
|
3922
|
+
exploreAgent,
|
|
3923
|
+
extractMemeloopStructuredToolPayload,
|
|
3924
|
+
findPromptById,
|
|
3925
|
+
flattenPrompts,
|
|
3926
|
+
getActivePluginRegistry,
|
|
3927
|
+
getAgentAgentLoopId,
|
|
3928
|
+
getAgentProfileRegistry,
|
|
3929
|
+
getAllToolDefinitions,
|
|
3930
|
+
getBuiltinAgentAgentLoopSource,
|
|
3931
|
+
getBuiltinAgentToolLoopSource,
|
|
3932
|
+
getBuiltinLoopProfile,
|
|
3933
|
+
getBuiltinLoopProfiles,
|
|
3934
|
+
getBuiltinToolPlugins,
|
|
3935
|
+
getCategoryConfig,
|
|
3936
|
+
getDefaultHookRegistry,
|
|
3937
|
+
getDefaultToolDefinitionRegistry,
|
|
3938
|
+
getHookCount,
|
|
3939
|
+
getLoadedPlugin,
|
|
3940
|
+
getLoopRegistry,
|
|
3941
|
+
getMcpClientToolId,
|
|
3942
|
+
getMcpForwardToolId,
|
|
3943
|
+
getPendingApprovals,
|
|
3944
|
+
getPluginRegistrations,
|
|
3945
|
+
getRemoteAgentToolId,
|
|
3946
|
+
getSpawnAgentToolId,
|
|
3947
|
+
getTaskToolId,
|
|
3948
|
+
getToolDefinition,
|
|
3949
|
+
getToolMetadata,
|
|
3950
|
+
getToolParameterSchema,
|
|
3951
|
+
gitProxyTargetBlockReason,
|
|
3952
|
+
hasHooks,
|
|
3953
|
+
imPlatformMaxMessageChars,
|
|
3954
|
+
imSwitchConversation,
|
|
3955
|
+
imToolDefinitions,
|
|
3956
|
+
isConversationMeta,
|
|
3957
|
+
isLibp2pRpcRequest,
|
|
3958
|
+
isLibp2pRpcResponse,
|
|
3959
|
+
isLibp2pSyncRequest,
|
|
3960
|
+
isLibp2pSyncResponse,
|
|
3961
|
+
isPluginLoaded,
|
|
3962
|
+
isTaskCategory,
|
|
3963
|
+
isUserInitiatedConversation,
|
|
3964
|
+
librarianAgent,
|
|
3965
|
+
listPlugins,
|
|
3966
|
+
listRegisteredHookTypes,
|
|
3967
|
+
loadAgentAgentLoopScript,
|
|
3968
|
+
loadAgentToolLoopScript,
|
|
3969
|
+
loadPluginModule,
|
|
3970
|
+
loadPluginModules,
|
|
3971
|
+
loadUserPermissions,
|
|
3972
|
+
matchAllToolCallings,
|
|
3973
|
+
matchPattern,
|
|
3974
|
+
matchToolCalling,
|
|
3975
|
+
mcpClientConfigSchema,
|
|
3976
|
+
mcpClientImpl,
|
|
3977
|
+
mcpForwardConfigSchema,
|
|
3978
|
+
mcpForwardImpl,
|
|
3979
|
+
mergeAgentToolsIntoFrameworkConfig,
|
|
3980
|
+
mergePermissionSets,
|
|
3981
|
+
onApprovalRequest,
|
|
3982
|
+
oracleAgent,
|
|
3983
|
+
parseCheckpointRecord,
|
|
3984
|
+
parseMemeloopUri,
|
|
3985
|
+
pickAdapter,
|
|
3986
|
+
planAgent,
|
|
3987
|
+
pluginRegistry,
|
|
3988
|
+
promptConcatStream,
|
|
3989
|
+
registerBuiltinLoops,
|
|
3990
|
+
registerBuiltinPromptPlugins,
|
|
3991
|
+
registerBuiltinToolPlugins,
|
|
3992
|
+
registerBuiltinTools,
|
|
3993
|
+
registerHook,
|
|
3994
|
+
registerImSessionBuiltinTools,
|
|
3995
|
+
registerPluginHooks,
|
|
3996
|
+
registerPluginTools,
|
|
3997
|
+
registerToolDefinition,
|
|
3998
|
+
registerToolParameterSchema,
|
|
3999
|
+
remoteAgentConfigSchema,
|
|
4000
|
+
remoteAgentImpl,
|
|
4001
|
+
remoteAgentListImpl,
|
|
4002
|
+
requestApproval,
|
|
4003
|
+
resetAgentProfileRegistry,
|
|
4004
|
+
resetLoopRegistry,
|
|
4005
|
+
resolveAgentToolLoopTerminalState,
|
|
4006
|
+
resolveApproval,
|
|
4007
|
+
resolveCategory,
|
|
4008
|
+
resolvePromptPluginMap,
|
|
4009
|
+
resolveQuestionAnswer,
|
|
4010
|
+
runAgentToolLoopTurn,
|
|
4011
|
+
runPostProcessHooks,
|
|
4012
|
+
runProcessPromptsHooks,
|
|
4013
|
+
runResponseCompleteHooks,
|
|
4014
|
+
runToolExecutedHooks,
|
|
4015
|
+
runWithPluginRegistry,
|
|
4016
|
+
saveUserPermissions,
|
|
4017
|
+
schemaToToolContent,
|
|
4018
|
+
serializeCheckpointRecord,
|
|
4019
|
+
shouldCompact,
|
|
4020
|
+
signDeviceBinding,
|
|
4021
|
+
spawnAgentConfigSchema,
|
|
4022
|
+
spawnAgentImpl,
|
|
4023
|
+
streamRuntimeAgentReplyToIm,
|
|
4024
|
+
syncCloudDevices,
|
|
4025
|
+
taskToolConfigSchema,
|
|
4026
|
+
taskToolImpl,
|
|
4027
|
+
tiddlerToAgentDefinition,
|
|
4028
|
+
todoWriteConfigSchema,
|
|
4029
|
+
todoWriteImpl,
|
|
4030
|
+
truncateToolSummary,
|
|
4031
|
+
tryHandleImSlashCommand,
|
|
4032
|
+
unloadAllPlugins,
|
|
4033
|
+
unloadPlugin,
|
|
4034
|
+
unregisterHook,
|
|
4035
|
+
validatePluginManifest,
|
|
4036
|
+
verifyDeviceBinding,
|
|
4037
|
+
verifyDeviceConnectionGrant,
|
|
4038
|
+
verifyDeviceRelayReservationToken
|
|
4039
|
+
};
|
|
4040
|
+
//# sourceMappingURL=index.js.map
|