@makerbi/remodex 2.0.1 → 2.3.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.
@@ -2,12 +2,13 @@
2
2
  // Purpose: Serves safe Mac-local project folder discovery and creation requests from the iOS app.
3
3
  // Layer: Bridge handler
4
4
  // Exports: handleProjectRequest plus testable project filesystem helpers
5
- // Depends on: fs, os, path, ./codex-home
5
+ // Depends on: fs, os, path, ./codex-home, ./project-registry
6
6
 
7
7
  const fs = require("fs");
8
8
  const os = require("os");
9
9
  const path = require("path");
10
10
  const { resolveCodexHome } = require("./codex-home");
11
+ const { createProjectRegistry, getDefaultProjectRegistry } = require("./project-registry");
11
12
 
12
13
  const DEFAULT_DIRECTORY_LIMIT = 200;
13
14
  const DEFAULT_DIRECTORY_SEARCH_LIMIT = 80;
@@ -23,9 +24,9 @@ const ROOTLESS_CHAT_SLUG_MAX_LENGTH = 60;
23
24
  const ROOTLESS_CHAT_SLUG_FALLBACK = "new-chat";
24
25
  const ROOTLESS_CHAT_DEDUP_LIMIT = 50;
25
26
 
26
- // ─── ENTRY POINT ─────────────────────────────────────────────
27
+ // Entry point
27
28
 
28
- function handleProjectRequest(rawMessage, sendResponse) {
29
+ function handleProjectRequest(rawMessage, sendResponse, options = {}) {
29
30
  let parsed;
30
31
  try {
31
32
  parsed = JSON.parse(rawMessage);
@@ -41,7 +42,7 @@ function handleProjectRequest(rawMessage, sendResponse) {
41
42
  const id = parsed.id;
42
43
  const params = parsed.params || {};
43
44
 
44
- handleProjectMethod(method, params)
45
+ handleProjectMethod(method, params, options)
45
46
  .then((result) => {
46
47
  sendResponse(JSON.stringify({ id, result }));
47
48
  })
@@ -69,6 +70,10 @@ async function handleProjectMethod(method, params, options = {}) {
69
70
  return projectQuickLocations(options);
70
71
  case "project/projectlessRoots":
71
72
  return projectProjectlessRoots(options);
73
+ case "project/knownProjects":
74
+ return projectKnownProjects(params, options);
75
+ case "project/rememberKnownProject":
76
+ return projectRememberKnownProject(params, options);
72
77
  case "project/listDirectory":
73
78
  return projectListDirectory(params, options);
74
79
  case "project/searchDirectories":
@@ -84,7 +89,7 @@ async function handleProjectMethod(method, params, options = {}) {
84
89
  }
85
90
  }
86
91
 
87
- // ─── Project Methods ─────────────────────────────────────────
92
+ // Project methods
88
93
 
89
94
  async function projectQuickLocations(options = {}) {
90
95
  const homeDir = resolveHomeDir(options);
@@ -130,6 +135,32 @@ async function projectProjectlessRoots(options = {}) {
130
135
  };
131
136
  }
132
137
 
138
+ async function projectKnownProjects(params = {}, options = {}) {
139
+ const registry = resolveProjectRegistry(options);
140
+ return {
141
+ projects: registry.listProjects(params),
142
+ };
143
+ }
144
+
145
+ async function projectRememberKnownProject(params = {}, options = {}) {
146
+ const requestedPath = readString(params.path || params.projectPath || params.cwd);
147
+ if (!requestedPath) {
148
+ throw projectError("missing_path", "A folder path is required.");
149
+ }
150
+
151
+ const directory = await requireUsableDirectory(requestedPath, options);
152
+ const registry = resolveProjectRegistry(options);
153
+ const project = registry.rememberProjectPath(directory.path, {
154
+ source: readString(params.source) || "manual",
155
+ provider: readString(params.provider || params.modelProvider || params.model_provider),
156
+ });
157
+ if (!project) {
158
+ throw projectError("not_project_folder", "That folder is reserved for projectless chats.");
159
+ }
160
+
161
+ return { project };
162
+ }
163
+
133
164
  async function projectListDirectory(params, options = {}) {
134
165
  const requestedPath = readString(params.path) || resolveHomeDir(options);
135
166
  const directory = await requireUsableDirectory(requestedPath, options);
@@ -287,7 +318,7 @@ function rootlessChatSlugFromPromptHint(rawPromptHint) {
287
318
  return slug || ROOTLESS_CHAT_SLUG_FALLBACK;
288
319
  }
289
320
 
290
- // Appends "-2", "-3", so two rapid-fire chats with the same first words
321
+ // Appends "-2", "-3", etc. so two rapid-fire chats with the same first words
291
322
  // keep distinct folders instead of fighting over the same path.
292
323
  async function reserveUniqueRootlessChatPath(parentDirectory, slugBase) {
293
324
  for (let attempt = 0; attempt < ROOTLESS_CHAT_DEDUP_LIMIT; attempt += 1) {
@@ -325,7 +356,7 @@ async function safeRealpath(candidatePath) {
325
356
  }
326
357
  }
327
358
 
328
- // ─── Filesystem Helpers ──────────────────────────────────────
359
+ // Filesystem helpers
329
360
 
330
361
  async function readDirectoryEntries(directoryPath, options = {}) {
331
362
  let dirents;
@@ -603,6 +634,16 @@ function resolveHomeDir(options = {}) {
603
634
  return options.homeDir || os.homedir();
604
635
  }
605
636
 
637
+ function resolveProjectRegistry(options = {}) {
638
+ if (options.projectRegistry) {
639
+ return options.projectRegistry;
640
+ }
641
+ if (options.storagePath || options.homeDir || options.codexHome) {
642
+ return createProjectRegistry(options);
643
+ }
644
+ return getDefaultProjectRegistry();
645
+ }
646
+
606
647
  function uniqueExistingOrCandidatePaths(paths) {
607
648
  const seen = new Set();
608
649
  const result = [];
@@ -644,6 +685,8 @@ module.exports = {
644
685
  handleProjectMethod,
645
686
  projectQuickLocations,
646
687
  projectProjectlessRoots,
688
+ projectKnownProjects,
689
+ projectRememberKnownProject,
647
690
  projectListDirectory,
648
691
  projectSearchDirectories,
649
692
  projectValidatePath,
@@ -0,0 +1,466 @@
1
+ // FILE: project-registry.js
2
+ // Purpose: Persists provider-neutral local project folders discovered by Codex, OpenCode, or manual picks.
3
+ // Layer: Bridge service
4
+ // Exports: createProjectRegistry plus pure path helpers used by tests
5
+ // Depends on: fs, os, path, ./codex-home
6
+
7
+ const fs = require("fs");
8
+ const os = require("os");
9
+ const path = require("path");
10
+ const { resolveCodexHome } = require("./codex-home");
11
+
12
+ const REGISTRY_SCHEMA_VERSION = 1;
13
+ const REGISTRY_DIRECTORY_NAME = "remodex";
14
+ const REGISTRY_FILE_NAME = "known-projects.json";
15
+
16
+ let defaultProjectRegistry = null;
17
+
18
+ // Entry point
19
+
20
+ function createProjectRegistry(options = {}) {
21
+ const storagePath = resolveRegistryStoragePath(options);
22
+ return {
23
+ storagePath,
24
+ listProjects(params = {}) {
25
+ return listKnownProjects(storagePath, params, options);
26
+ },
27
+ rememberProjectPath(candidatePath, metadata = {}) {
28
+ return rememberKnownProjectPath(storagePath, candidatePath, metadata, options);
29
+ },
30
+ rememberProjectsFromThreads(threads, metadata = {}) {
31
+ return rememberKnownProjectsFromThreads(storagePath, threads, metadata, options);
32
+ },
33
+ };
34
+ }
35
+
36
+ function getDefaultProjectRegistry() {
37
+ if (!defaultProjectRegistry) {
38
+ defaultProjectRegistry = createProjectRegistry();
39
+ }
40
+ return defaultProjectRegistry;
41
+ }
42
+
43
+ // Registry operations
44
+
45
+ function listKnownProjects(storagePath, params = {}, options = {}) {
46
+ const state = readRegistryState(storagePath);
47
+ const includeUnavailable = params.includeUnavailable === true || params.include_unavailable === true;
48
+ const seenKeys = new Set();
49
+
50
+ return state.projects
51
+ .map((entry) => normalizeStoredEntry(entry, options))
52
+ .filter(Boolean)
53
+ .filter((entry) => {
54
+ const key = projectIdentityKey(entry.path);
55
+ if (!key || seenKeys.has(key)) {
56
+ return false;
57
+ }
58
+ seenKeys.add(key);
59
+
60
+ if (isGeneratedProjectlessPath(entry.path, options)) {
61
+ return false;
62
+ }
63
+ if (includeUnavailable) {
64
+ return true;
65
+ }
66
+ return directoryExists(entry.path);
67
+ })
68
+ .sort(compareKnownProjects)
69
+ .map(publicKnownProject);
70
+ }
71
+
72
+ function rememberKnownProjectPath(storagePath, candidatePath, metadata = {}, options = {}) {
73
+ return rememberKnownProjectEntries(storagePath, [{
74
+ path: candidatePath,
75
+ metadata,
76
+ }], options)[0] || null;
77
+ }
78
+
79
+ function rememberKnownProjectsFromThreads(storagePath, threads, metadata = {}, options = {}) {
80
+ if (!Array.isArray(threads) || threads.length === 0) {
81
+ return [];
82
+ }
83
+
84
+ const candidates = [];
85
+ for (const thread of threads) {
86
+ const cwd = readThreadProjectPath(thread);
87
+ if (!cwd) {
88
+ continue;
89
+ }
90
+
91
+ candidates.push({
92
+ path: cwd,
93
+ metadata: {
94
+ ...metadata,
95
+ provider: readString(metadata.provider || thread.modelProvider || thread.model_provider || thread.provider),
96
+ lastSeenAt: readThreadLastSeenAt(thread) || metadata.lastSeenAt,
97
+ },
98
+ });
99
+ }
100
+ return rememberKnownProjectEntries(storagePath, candidates, options);
101
+ }
102
+
103
+ // Batches thread-list updates into one registry read/write so provider sync
104
+ // stays cheap even when a page contains many threads from the same project.
105
+ function rememberKnownProjectEntries(storagePath, candidates, options = {}) {
106
+ if (!Array.isArray(candidates) || candidates.length === 0) {
107
+ return [];
108
+ }
109
+
110
+ const now = currentTimestamp(options);
111
+ const state = readRegistryState(storagePath);
112
+ const indexByKey = new Map();
113
+ state.projects.forEach((entry, index) => {
114
+ const key = projectIdentityKey(entry?.path);
115
+ if (key && !indexByKey.has(key)) {
116
+ indexByKey.set(key, index);
117
+ }
118
+ });
119
+ const remembered = [];
120
+ let didChange = false;
121
+
122
+ for (const candidate of candidates) {
123
+ const metadata = candidate?.metadata || {};
124
+ const normalizedPath = normalizeProjectPath(candidate?.path, options);
125
+ if (!normalizedPath || isGeneratedProjectlessPath(normalizedPath, options)) {
126
+ continue;
127
+ }
128
+
129
+ const key = projectIdentityKey(normalizedPath);
130
+ const existingIndex = indexByKey.has(key) ? indexByKey.get(key) : -1;
131
+ const previous = existingIndex >= 0 ? state.projects[existingIndex] : null;
132
+ const nextEntry = mergeProjectEntry(previous, {
133
+ path: normalizedPath,
134
+ label: readString(metadata.label) || projectLabelForPath(normalizedPath),
135
+ source: readString(metadata.source) || "unknown",
136
+ provider: readString(metadata.provider || metadata.providerHint || metadata.provider_hint),
137
+ firstSeenAt: previous?.firstSeenAt || now,
138
+ lastSeenAt: readString(metadata.lastSeenAt || metadata.last_seen_at) || now,
139
+ });
140
+
141
+ if (existingIndex >= 0) {
142
+ state.projects[existingIndex] = nextEntry;
143
+ } else {
144
+ indexByKey.set(key, state.projects.length);
145
+ state.projects.push(nextEntry);
146
+ }
147
+ didChange = true;
148
+ remembered.push(publicKnownProject(nextEntry));
149
+ }
150
+
151
+ if (didChange) {
152
+ writeRegistryState(storagePath, state);
153
+ }
154
+ return remembered;
155
+ }
156
+
157
+ // Entry normalization
158
+
159
+ function mergeProjectEntry(previous, next) {
160
+ const previousSources = Array.isArray(previous?.sources) ? previous.sources : [];
161
+ const previousProviderHints = Array.isArray(previous?.providerHints) ? previous.providerHints : [];
162
+ const sources = appendUniqueString(previousSources, next.source);
163
+ const providerHints = appendUniqueString(previousProviderHints, next.provider);
164
+
165
+ return {
166
+ path: next.path,
167
+ label: next.label || previous?.label || projectLabelForPath(next.path),
168
+ source: next.source || previous?.source || "unknown",
169
+ sources,
170
+ providerHints,
171
+ firstSeenAt: previous?.firstSeenAt || next.firstSeenAt,
172
+ lastSeenAt: mostRecentTimestamp(previous?.lastSeenAt, next.lastSeenAt),
173
+ };
174
+ }
175
+
176
+ function normalizeStoredEntry(entry, options = {}) {
177
+ if (!entry || typeof entry !== "object") {
178
+ return null;
179
+ }
180
+
181
+ const normalizedPath = normalizeProjectPath(entry.path, options);
182
+ if (!normalizedPath) {
183
+ return null;
184
+ }
185
+
186
+ return {
187
+ path: normalizedPath,
188
+ label: readString(entry.label) || projectLabelForPath(normalizedPath),
189
+ source: readString(entry.source) || firstString(entry.sources) || "unknown",
190
+ sources: uniqueStrings(entry.sources),
191
+ providerHints: uniqueStrings(entry.providerHints || entry.provider_hints),
192
+ firstSeenAt: readString(entry.firstSeenAt || entry.first_seen_at) || "",
193
+ lastSeenAt: readString(entry.lastSeenAt || entry.last_seen_at) || "",
194
+ };
195
+ }
196
+
197
+ function publicKnownProject(entry) {
198
+ return {
199
+ id: entry.path,
200
+ path: entry.path,
201
+ label: entry.label || projectLabelForPath(entry.path),
202
+ source: entry.source || "unknown",
203
+ sources: uniqueStrings(entry.sources),
204
+ providerHints: uniqueStrings(entry.providerHints),
205
+ firstSeenAt: entry.firstSeenAt || "",
206
+ lastSeenAt: entry.lastSeenAt || "",
207
+ };
208
+ }
209
+
210
+ // Storage
211
+
212
+ function resolveRegistryStoragePath(options = {}) {
213
+ if (readString(options.storagePath)) {
214
+ return path.resolve(options.storagePath);
215
+ }
216
+
217
+ const codexHome = path.resolve(readString(options.codexHome) || resolveCodexHome());
218
+ return path.join(codexHome, REGISTRY_DIRECTORY_NAME, REGISTRY_FILE_NAME);
219
+ }
220
+
221
+ function readRegistryState(storagePath) {
222
+ try {
223
+ const raw = fs.readFileSync(storagePath, "utf8");
224
+ const parsed = JSON.parse(raw);
225
+ return normalizeRegistryState(parsed);
226
+ } catch {
227
+ return emptyRegistryState();
228
+ }
229
+ }
230
+
231
+ function writeRegistryState(storagePath, state) {
232
+ const normalizedState = normalizeRegistryState(state);
233
+ const directory = path.dirname(storagePath);
234
+ fs.mkdirSync(directory, { recursive: true });
235
+ const tempPath = `${storagePath}.${process.pid}.${Date.now()}.tmp`;
236
+ fs.writeFileSync(tempPath, `${JSON.stringify(normalizedState, null, 2)}\n`, "utf8");
237
+ fs.renameSync(tempPath, storagePath);
238
+ }
239
+
240
+ function normalizeRegistryState(state) {
241
+ return {
242
+ version: REGISTRY_SCHEMA_VERSION,
243
+ projects: Array.isArray(state?.projects) ? state.projects.filter(Boolean) : [],
244
+ };
245
+ }
246
+
247
+ function emptyRegistryState() {
248
+ return {
249
+ version: REGISTRY_SCHEMA_VERSION,
250
+ projects: [],
251
+ };
252
+ }
253
+
254
+ // Path helpers
255
+
256
+ function normalizeProjectPath(candidatePath, options = {}) {
257
+ const rawPath = readString(candidatePath);
258
+ if (!rawPath || !isLikelyFilesystemPath(rawPath)) {
259
+ return null;
260
+ }
261
+
262
+ const expandedPath = expandHomePath(rawPath, options);
263
+ if (!path.isAbsolute(expandedPath)) {
264
+ return null;
265
+ }
266
+
267
+ const resolvedPath = path.resolve(expandedPath);
268
+ return realpathSyncIfAvailable(resolvedPath) || resolvedPath;
269
+ }
270
+
271
+ function isGeneratedProjectlessPath(candidatePath, options = {}) {
272
+ const normalizedPath = normalizeProjectPath(candidatePath, options);
273
+ if (!normalizedPath) {
274
+ return false;
275
+ }
276
+
277
+ const homeDir = path.resolve(readString(options.homeDir) || os.homedir());
278
+ const codexHome = path.resolve(readString(options.codexHome) || resolveCodexHome());
279
+ const knownRootlessRoots = [
280
+ path.join(codexHome, "threads"),
281
+ path.join(homeDir, "Documents", "Codex"),
282
+ ];
283
+
284
+ return knownRootlessRoots.some((rootPath) => samePathOrDescendant(normalizedPath, rootPath))
285
+ || hasGeneratedProjectlessComponents(normalizedPath);
286
+ }
287
+
288
+ function hasGeneratedProjectlessComponents(candidatePath) {
289
+ const components = projectPathComponents(candidatePath);
290
+ for (let index = 0; index < components.length; index += 1) {
291
+ if (
292
+ components[index] === ".codex"
293
+ && components[index + 1] === "threads"
294
+ && readString(components[index + 2])
295
+ ) {
296
+ return true;
297
+ }
298
+
299
+ if (
300
+ components[index] === "Documents"
301
+ && components[index + 1] === "Codex"
302
+ && isISODateFolderName(components[index + 2])
303
+ && readString(components[index + 3])
304
+ ) {
305
+ return true;
306
+ }
307
+ }
308
+ return false;
309
+ }
310
+
311
+ function samePathOrDescendant(candidatePath, rootPath) {
312
+ const normalizedCandidate = path.resolve(candidatePath);
313
+ const normalizedRoot = realpathSyncIfAvailable(path.resolve(rootPath)) || path.resolve(rootPath);
314
+ const relative = path.relative(normalizedRoot, normalizedCandidate);
315
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
316
+ }
317
+
318
+ function projectIdentityKey(candidatePath) {
319
+ const normalizedPath = readString(candidatePath);
320
+ if (!normalizedPath) {
321
+ return "";
322
+ }
323
+ return process.platform === "win32" || process.platform === "darwin"
324
+ ? normalizedPath.toLowerCase()
325
+ : normalizedPath;
326
+ }
327
+
328
+ function projectLabelForPath(candidatePath) {
329
+ const baseName = path.basename(candidatePath);
330
+ return baseName || candidatePath;
331
+ }
332
+
333
+ function readThreadProjectPath(thread) {
334
+ if (
335
+ thread?.metadata?.projectCwdSource === "fallback"
336
+ || thread?.metadata?.projectRegistrySkipCwd === true
337
+ ) {
338
+ return "";
339
+ }
340
+ return readString(thread?.cwd || thread?.current_working_directory || thread?.workingDirectory || thread?.directory);
341
+ }
342
+
343
+ function readThreadLastSeenAt(thread) {
344
+ return readString(thread?.updatedAt || thread?.updated_at || thread?.createdAt || thread?.created_at);
345
+ }
346
+
347
+ function expandHomePath(candidatePath, options = {}) {
348
+ const homeDir = readString(options.homeDir) || os.homedir();
349
+ if (candidatePath === "~") {
350
+ return homeDir;
351
+ }
352
+ if (candidatePath.startsWith("~/")) {
353
+ return path.join(homeDir, candidatePath.slice(2));
354
+ }
355
+ return candidatePath;
356
+ }
357
+
358
+ function isLikelyFilesystemPath(value) {
359
+ return value === "~"
360
+ || value === "/"
361
+ || value.startsWith("/")
362
+ || value.startsWith("~/")
363
+ || /^[A-Za-z]:[\\/]/u.test(value)
364
+ || value.startsWith("\\\\");
365
+ }
366
+
367
+ function directoryExists(candidatePath) {
368
+ try {
369
+ return fs.statSync(candidatePath).isDirectory();
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+
375
+ function realpathSyncIfAvailable(candidatePath) {
376
+ try {
377
+ return fs.realpathSync.native(candidatePath);
378
+ } catch {
379
+ try {
380
+ return fs.realpathSync(candidatePath);
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+ }
386
+
387
+ // Value helpers
388
+
389
+ function compareKnownProjects(left, right) {
390
+ const leftTime = Date.parse(left.lastSeenAt || left.firstSeenAt || 0) || 0;
391
+ const rightTime = Date.parse(right.lastSeenAt || right.firstSeenAt || 0) || 0;
392
+ if (leftTime !== rightTime) {
393
+ return rightTime - leftTime;
394
+ }
395
+
396
+ const labelOrder = left.label.localeCompare(right.label, undefined, { sensitivity: "base" });
397
+ if (labelOrder !== 0) {
398
+ return labelOrder;
399
+ }
400
+ return left.path.localeCompare(right.path, undefined, { sensitivity: "base" });
401
+ }
402
+
403
+ function mostRecentTimestamp(left, right) {
404
+ const leftTime = Date.parse(left || 0) || 0;
405
+ const rightTime = Date.parse(right || 0) || 0;
406
+ if (!leftTime) {
407
+ return right || left || "";
408
+ }
409
+ if (!rightTime) {
410
+ return left || right || "";
411
+ }
412
+ return rightTime >= leftTime ? right : left;
413
+ }
414
+
415
+ function currentTimestamp(options = {}) {
416
+ if (typeof options.now === "function") {
417
+ return new Date(options.now()).toISOString();
418
+ }
419
+ return new Date().toISOString();
420
+ }
421
+
422
+ function appendUniqueString(values, value) {
423
+ return uniqueStrings([...values, value]);
424
+ }
425
+
426
+ function uniqueStrings(values) {
427
+ if (!Array.isArray(values)) {
428
+ return [];
429
+ }
430
+
431
+ const seen = new Set();
432
+ const result = [];
433
+ for (const value of values) {
434
+ const normalized = readString(value);
435
+ if (!normalized || seen.has(normalized)) {
436
+ continue;
437
+ }
438
+ seen.add(normalized);
439
+ result.push(normalized);
440
+ }
441
+ return result;
442
+ }
443
+
444
+ function firstString(values) {
445
+ return uniqueStrings(values)[0] || "";
446
+ }
447
+
448
+ function projectPathComponents(candidatePath) {
449
+ return candidatePath.replace(/\\/g, "/").split("/").filter(Boolean);
450
+ }
451
+
452
+ function isISODateFolderName(value) {
453
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/u.test(value);
454
+ }
455
+
456
+ function readString(value) {
457
+ return typeof value === "string" && value.trim() ? value.trim() : "";
458
+ }
459
+
460
+ module.exports = {
461
+ createProjectRegistry,
462
+ getDefaultProjectRegistry,
463
+ isGeneratedProjectlessPath,
464
+ normalizeProjectPath,
465
+ projectLabelForPath,
466
+ };
@@ -27,8 +27,8 @@ function createPushNotificationTracker({
27
27
 
28
28
  // ─── ENTRY POINT ─────────────────────────────────────────────
29
29
 
30
- function handleOutbound(rawMessage) {
31
- const message = parseOutboundMessage(rawMessage);
30
+ function handleOutbound(rawMessage, parsedMessage = null) {
31
+ const message = parseOutboundMessage(rawMessage, parsedMessage);
32
32
  if (!message) {
33
33
  return;
34
34
  }
@@ -274,8 +274,8 @@ function createPushNotificationTracker({
274
274
  }
275
275
 
276
276
  // Normalizes the message envelope once so downstream helpers can share the same parsed view.
277
- function parseOutboundMessage(rawMessage) {
278
- const parsed = safeParseJSON(rawMessage);
277
+ function parseOutboundMessage(rawMessage, parsedMessage = null) {
278
+ const parsed = parsedMessage ?? safeParseJSON(rawMessage);
279
279
  if (!parsed || typeof parsed.method !== "string") {
280
280
  return null;
281
281
  }