@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.
- package/package.json +2 -2
- package/src/account-status.js +5 -4
- package/src/bridge.js +1813 -670
- package/src/codex-desktop-refresher.js +35 -7
- package/src/cursor-acp-client.js +242 -0
- package/src/cursor-models.js +134 -0
- package/src/cursor-provider.js +1197 -0
- package/src/desktop-ipc-action-follower.js +2331 -964
- package/src/desktop-ipc-conversation-adapter.js +1132 -0
- package/src/desktop-ipc-conversation-projector.js +1169 -0
- package/src/desktop-ipc-live-owner.js +1790 -0
- package/src/desktop-ipc-owner-transport.js +750 -0
- package/src/desktop-ipc-shared.js +473 -0
- package/src/desktop-ipc-state-patches.js +218 -0
- package/src/opencode-models.js +108 -0
- package/src/opencode-provider.js +1151 -0
- package/src/project-handler.js +50 -7
- package/src/project-registry.js +466 -0
- package/src/push-notification-tracker.js +4 -4
- package/src/rollout-live-mirror.js +930 -218
- package/src/rollout-turn-semantics.js +20 -0
- package/src/runtime-provider-models.js +164 -0
- package/src/runtime-provider-router.js +365 -0
- package/src/scripts/codex-refresh.applescript +26 -15
- package/src/secure-transport.js +204 -9
- package/src/session-jsonl-history.js +429 -39
- package/src/thread-context-handler.js +8 -6
- package/src/thread-runtime-settings-store.js +247 -0
- package/src/voice-audio.js +344 -0
- package/src/voice-handler.js +363 -173
package/src/project-handler.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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",
|
|
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
|
-
//
|
|
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
|
}
|