memax-cli 0.1.0-alpha.28 → 0.1.0-alpha.29
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/commands/agent-sessions.d.ts +32 -0
- package/dist/commands/agent-sessions.d.ts.map +1 -0
- package/dist/commands/agent-sessions.js +761 -0
- package/dist/commands/agent-sessions.js.map +1 -0
- package/dist/commands/agent-sessions.test.d.ts +2 -0
- package/dist/commands/agent-sessions.test.d.ts.map +1 -0
- package/dist/commands/agent-sessions.test.js +82 -0
- package/dist/commands/agent-sessions.test.js.map +1 -0
- package/dist/commands/recall.d.ts +1 -0
- package/dist/commands/recall.d.ts.map +1 -1
- package/dist/commands/recall.js +71 -31
- package/dist/commands/recall.js.map +1 -1
- package/dist/commands/recall.test.js +9 -1
- package/dist/commands/recall.test.js.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +1 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +3 -0
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { getAuthHeaders, getClient } from "../lib/client.js";
|
|
7
|
+
import { getProjectScope, resolveClaudeProjectFolder, resolveProjectScope, normalizeFilePath, } from "../lib/project-context.js";
|
|
8
|
+
import { getOrCreateDeviceID, loadConfig } from "../lib/config.js";
|
|
9
|
+
import { ask, confirmDefault } from "../lib/prompt.js";
|
|
10
|
+
export async function syncAgentSessionsCommand(options = {}) {
|
|
11
|
+
console.log(chalk.bold("\n Memax Session Sync\n"));
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const home = homedir();
|
|
14
|
+
const deviceID = getOrCreateDeviceID();
|
|
15
|
+
const projectScopeResolution = resolveProjectScope(cwd);
|
|
16
|
+
const currentProjectScope = projectScopeResolution.scope;
|
|
17
|
+
const locations = discoverAgentSessions();
|
|
18
|
+
const localSessions = locations
|
|
19
|
+
.filter((loc) => existsSync(loc.path))
|
|
20
|
+
.map((loc) => {
|
|
21
|
+
const content = readFileSync(loc.path);
|
|
22
|
+
return {
|
|
23
|
+
loc,
|
|
24
|
+
content,
|
|
25
|
+
hash: createHash("sha256").update(content).digest("hex"),
|
|
26
|
+
size: statSync(loc.path).size,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const manifest = localSessions.map((session) => ({
|
|
30
|
+
agent: session.loc.agent,
|
|
31
|
+
file_path: session.loc.filePath,
|
|
32
|
+
scope: session.loc.scope,
|
|
33
|
+
content_hash: session.hash,
|
|
34
|
+
local_path: session.loc.path,
|
|
35
|
+
}));
|
|
36
|
+
let actions;
|
|
37
|
+
try {
|
|
38
|
+
const plan = await getClient().agentSessions.sync({
|
|
39
|
+
device_id: deviceID,
|
|
40
|
+
sessions: manifest,
|
|
41
|
+
});
|
|
42
|
+
actions = plan.actions;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error(chalk.red(` Sync failed: ${err.message}\n`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (options.push) {
|
|
49
|
+
actions = actions.map((a) => a.action === "conflict" ? { ...a, action: "push" } : a);
|
|
50
|
+
}
|
|
51
|
+
else if (options.pull) {
|
|
52
|
+
actions = actions.map((a) => a.action === "conflict" ? { ...a, action: "pull" } : a);
|
|
53
|
+
}
|
|
54
|
+
actions = actions.filter((action) => {
|
|
55
|
+
if (!action.scope.startsWith("project:"))
|
|
56
|
+
return true;
|
|
57
|
+
if (action.scope === currentProjectScope)
|
|
58
|
+
return true;
|
|
59
|
+
if (action.action === "pull" && action.reason === "cloud_only")
|
|
60
|
+
return false;
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
63
|
+
const localByKey = new Map();
|
|
64
|
+
for (const session of localSessions) {
|
|
65
|
+
localByKey.set(`${session.loc.agent}|${session.loc.filePath}|${session.loc.scope}`, session);
|
|
66
|
+
}
|
|
67
|
+
const locationByKey = new Map();
|
|
68
|
+
for (const location of locations) {
|
|
69
|
+
locationByKey.set(`${location.agent}|${location.filePath}|${location.scope}`, location);
|
|
70
|
+
}
|
|
71
|
+
const resolveWritePath = (agent, filePath, scope) => {
|
|
72
|
+
const existing = locationByKey.get(`${agent}|${filePath}|${scope}`);
|
|
73
|
+
if (existing)
|
|
74
|
+
return existing.path;
|
|
75
|
+
return resolveAgentSessionWritePath(agent, filePath, scope, {
|
|
76
|
+
cwd,
|
|
77
|
+
home,
|
|
78
|
+
currentProjectScope,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
let pushed = 0;
|
|
82
|
+
let pulled = 0;
|
|
83
|
+
let deletedLocal = 0;
|
|
84
|
+
let unchanged = 0;
|
|
85
|
+
let skipped = 0;
|
|
86
|
+
let errors = 0;
|
|
87
|
+
const ackSessions = [];
|
|
88
|
+
const byAgent = new Map();
|
|
89
|
+
for (const action of actions) {
|
|
90
|
+
const group = byAgent.get(action.agent) ?? [];
|
|
91
|
+
group.push(action);
|
|
92
|
+
byAgent.set(action.agent, group);
|
|
93
|
+
}
|
|
94
|
+
for (const [agent, agentActions] of byAgent) {
|
|
95
|
+
console.log(chalk.white(` ${formatAgentName(agent)}`));
|
|
96
|
+
for (const action of agentActions) {
|
|
97
|
+
const key = `${action.agent}|${action.file_path}|${action.scope}`;
|
|
98
|
+
if (action.action === "unchanged") {
|
|
99
|
+
const local = localByKey.get(key);
|
|
100
|
+
console.log(chalk.gray(` = ${action.file_path}`), chalk.gray("unchanged"));
|
|
101
|
+
if (local && action.version) {
|
|
102
|
+
ackSessions.push({
|
|
103
|
+
agent: action.agent,
|
|
104
|
+
file_path: action.file_path,
|
|
105
|
+
scope: action.scope,
|
|
106
|
+
content_hash: local.hash,
|
|
107
|
+
version: action.version,
|
|
108
|
+
local_path: local.loc.path,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
unchanged++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (action.action === "push") {
|
|
115
|
+
const local = localByKey.get(key);
|
|
116
|
+
if (!local) {
|
|
117
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray("local file not found for push"));
|
|
118
|
+
errors++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const fileRef = await uploadLocalFile(local.loc.path, local.content, local.hash);
|
|
123
|
+
await getClient().agentSessions.upsert({
|
|
124
|
+
agent: action.agent,
|
|
125
|
+
file_path: action.file_path,
|
|
126
|
+
scope: action.scope,
|
|
127
|
+
session_type: local.loc.sessionType,
|
|
128
|
+
device_id: deviceID,
|
|
129
|
+
local_path: local.loc.path,
|
|
130
|
+
file_ref: fileRef,
|
|
131
|
+
});
|
|
132
|
+
console.log(chalk.green(` ↑ ${action.file_path}`), chalk.gray(action.reason === "local_only"
|
|
133
|
+
? "pushing (new)"
|
|
134
|
+
: "pushing (local newer)"));
|
|
135
|
+
pushed++;
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray(err.message));
|
|
139
|
+
errors++;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (action.action === "pull") {
|
|
144
|
+
if (!action.session_id) {
|
|
145
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray("missing session ID from server"));
|
|
146
|
+
errors++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
150
|
+
if (!writePath) {
|
|
151
|
+
console.log(chalk.yellow(` ? ${action.file_path}`), chalk.gray(action.scope !== "global" && action.scope !== currentProjectScope
|
|
152
|
+
? "different project — skipped"
|
|
153
|
+
: "no safe restore path on this machine"));
|
|
154
|
+
skipped++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const isNewLocally = action.reason === "cloud_only" && !existsSync(writePath);
|
|
159
|
+
if (isNewLocally && !options.pull) {
|
|
160
|
+
console.log(chalk.cyan(` New file: ${action.file_path}`));
|
|
161
|
+
console.log(chalk.gray(` → ${writePath}`));
|
|
162
|
+
const accept = await confirmDefault(` Download? [Y/n] `);
|
|
163
|
+
if (!accept) {
|
|
164
|
+
console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
|
|
165
|
+
skipped++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const session = await getClient().agentSessions.get(action.session_id);
|
|
170
|
+
const bytes = await downloadAgentSession(action.session_id);
|
|
171
|
+
mkdirSync(dirname(writePath), { recursive: true });
|
|
172
|
+
writeFileSync(writePath, bytes);
|
|
173
|
+
if (action.version) {
|
|
174
|
+
ackSessions.push({
|
|
175
|
+
agent: action.agent,
|
|
176
|
+
file_path: action.file_path,
|
|
177
|
+
scope: action.scope,
|
|
178
|
+
content_hash: session.content_hash,
|
|
179
|
+
version: action.version,
|
|
180
|
+
local_path: writePath,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
console.log(chalk.cyan(` ↓ ${action.file_path}`), chalk.gray(isNewLocally ? "restored" : "pulling (cloud newer)"));
|
|
184
|
+
pulled++;
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray(err.message));
|
|
188
|
+
errors++;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (action.action === "delete_local") {
|
|
193
|
+
try {
|
|
194
|
+
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
195
|
+
if (writePath && existsSync(writePath)) {
|
|
196
|
+
rmSync(writePath);
|
|
197
|
+
}
|
|
198
|
+
if (action.version) {
|
|
199
|
+
ackSessions.push({
|
|
200
|
+
agent: action.agent,
|
|
201
|
+
file_path: action.file_path,
|
|
202
|
+
scope: action.scope,
|
|
203
|
+
version: action.version,
|
|
204
|
+
local_path: writePath ?? undefined,
|
|
205
|
+
deleted: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("deleted locally (cloud removed everywhere)"));
|
|
209
|
+
deletedLocal++;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray(err.message));
|
|
213
|
+
errors++;
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const resolution = await promptSessionConflict(action.file_path);
|
|
218
|
+
if (resolution === "local") {
|
|
219
|
+
const local = localByKey.get(key);
|
|
220
|
+
if (!local) {
|
|
221
|
+
skipped++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const fileRef = await uploadLocalFile(local.loc.path, local.content, local.hash);
|
|
226
|
+
await getClient().agentSessions.upsert({
|
|
227
|
+
agent: action.agent,
|
|
228
|
+
file_path: action.file_path,
|
|
229
|
+
scope: action.scope,
|
|
230
|
+
session_type: local.loc.sessionType,
|
|
231
|
+
device_id: deviceID,
|
|
232
|
+
local_path: local.loc.path,
|
|
233
|
+
file_ref: fileRef,
|
|
234
|
+
});
|
|
235
|
+
console.log(chalk.green(` ↑ ${action.file_path}`), chalk.gray("kept local"));
|
|
236
|
+
pushed++;
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray(err.message));
|
|
240
|
+
errors++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (resolution === "cloud" && action.session_id) {
|
|
244
|
+
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
245
|
+
if (!writePath) {
|
|
246
|
+
skipped++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const session = await getClient().agentSessions.get(action.session_id);
|
|
251
|
+
const bytes = await downloadAgentSession(action.session_id);
|
|
252
|
+
mkdirSync(dirname(writePath), { recursive: true });
|
|
253
|
+
writeFileSync(writePath, bytes);
|
|
254
|
+
if (action.version) {
|
|
255
|
+
ackSessions.push({
|
|
256
|
+
agent: action.agent,
|
|
257
|
+
file_path: action.file_path,
|
|
258
|
+
scope: action.scope,
|
|
259
|
+
content_hash: session.content_hash,
|
|
260
|
+
version: action.version,
|
|
261
|
+
local_path: writePath,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
console.log(chalk.cyan(` ↓ ${action.file_path}`), chalk.gray("used cloud"));
|
|
265
|
+
pulled++;
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray(err.message));
|
|
269
|
+
errors++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
|
|
274
|
+
skipped++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (ackSessions.length > 0) {
|
|
279
|
+
try {
|
|
280
|
+
await getClient().agentSessions.ack({
|
|
281
|
+
device_id: deviceID,
|
|
282
|
+
sessions: ackSessions,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
console.log(chalk.yellow("\n Warning: failed to persist session sync state"), chalk.gray(err.message));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const parts = [];
|
|
290
|
+
if (pushed > 0)
|
|
291
|
+
parts.push(`${pushed} pushed`);
|
|
292
|
+
if (pulled > 0)
|
|
293
|
+
parts.push(`${pulled} restored`);
|
|
294
|
+
if (deletedLocal > 0)
|
|
295
|
+
parts.push(`${deletedLocal} deleted locally`);
|
|
296
|
+
if (unchanged > 0)
|
|
297
|
+
parts.push(`${unchanged} unchanged`);
|
|
298
|
+
if (skipped > 0)
|
|
299
|
+
parts.push(`${skipped} skipped`);
|
|
300
|
+
if (errors > 0)
|
|
301
|
+
parts.push(`${errors} errors`);
|
|
302
|
+
if (parts.length === 0) {
|
|
303
|
+
console.log(chalk.gray(" No session artifacts discovered.\n"));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
console.log(chalk.bold(`\n Done: ${parts.join(", ")}`));
|
|
307
|
+
console.log(chalk.gray(" Session sync preserves raw artifacts. Knowledge extraction remains a separate workflow.\n"));
|
|
308
|
+
}
|
|
309
|
+
export async function listAgentSessionsCommand() {
|
|
310
|
+
try {
|
|
311
|
+
const result = await getClient().agentSessions.list();
|
|
312
|
+
const sessions = result.sessions;
|
|
313
|
+
if (sessions.length === 0) {
|
|
314
|
+
console.log(chalk.yellow(" No synced session artifacts.\n"));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log();
|
|
318
|
+
for (const session of sessions) {
|
|
319
|
+
const scopeTag = session.scope === "global"
|
|
320
|
+
? chalk.dim("global")
|
|
321
|
+
: chalk.dim(session.scope.replace(/^project:/, ""));
|
|
322
|
+
console.log(` ${chalk.cyan(formatAgentName(session.agent))} ${session.file_path} ${scopeTag} ${chalk.dim(formatBytes(session.size_bytes))}`);
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
console.error(chalk.red(` Failed to fetch session artifacts: ${err.message}\n`));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export async function deleteAgentSessionsCommand() {
|
|
331
|
+
let sessions;
|
|
332
|
+
try {
|
|
333
|
+
sessions = (await getClient().agentSessions.list()).sessions;
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
console.error(chalk.red(` Failed to fetch session artifacts: ${err.message}\n`));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (sessions.length === 0) {
|
|
340
|
+
console.log(chalk.yellow(" No synced session artifacts to delete.\n"));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
sessions.forEach((session, index) => {
|
|
344
|
+
const scopeTag = session.scope === "global"
|
|
345
|
+
? chalk.dim("global")
|
|
346
|
+
: chalk.dim(session.scope.replace(/^project:/, ""));
|
|
347
|
+
console.log(` ${chalk.dim(`${index + 1}.`)} ${chalk.cyan(formatAgentName(session.agent))} ${session.file_path} ${scopeTag}`);
|
|
348
|
+
});
|
|
349
|
+
console.log();
|
|
350
|
+
const answer = await ask(" Select session artifacts to delete (comma-separated numbers, or 'q' to quit): ");
|
|
351
|
+
if (!answer || answer.trim().toLowerCase() === "q") {
|
|
352
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const indices = answer
|
|
356
|
+
.split(",")
|
|
357
|
+
.map((value) => Number.parseInt(value.trim(), 10))
|
|
358
|
+
.filter((value) => Number.isFinite(value) && value >= 1 && value <= sessions.length);
|
|
359
|
+
if (indices.length === 0) {
|
|
360
|
+
console.log(chalk.gray(" No valid selections.\n"));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const mode = (await ask(" Delete from [l] this device only, [e] everywhere, or [s] skip? "))
|
|
364
|
+
.trim()
|
|
365
|
+
.toLowerCase();
|
|
366
|
+
if (mode !== "l" && mode !== "e") {
|
|
367
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const deviceID = getOrCreateDeviceID();
|
|
371
|
+
const cwd = process.cwd();
|
|
372
|
+
const currentProjectScope = getProjectScope(cwd);
|
|
373
|
+
for (const index of indices) {
|
|
374
|
+
const session = sessions[index - 1];
|
|
375
|
+
const localPath = resolveAgentSessionWritePath(session.agent, session.file_path, session.scope, { cwd, home: homedir(), currentProjectScope });
|
|
376
|
+
try {
|
|
377
|
+
if (mode === "l") {
|
|
378
|
+
await getClient().agentSessions.localDelete({
|
|
379
|
+
device_id: deviceID,
|
|
380
|
+
agent: session.agent,
|
|
381
|
+
file_path: session.file_path,
|
|
382
|
+
scope: session.scope,
|
|
383
|
+
local_path: localPath ?? undefined,
|
|
384
|
+
});
|
|
385
|
+
if (localPath && existsSync(localPath)) {
|
|
386
|
+
rmSync(localPath);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
await getClient().agentSessions.delete(session.id);
|
|
391
|
+
if (localPath && existsSync(localPath)) {
|
|
392
|
+
rmSync(localPath);
|
|
393
|
+
}
|
|
394
|
+
await getClient().agentSessions.ack({
|
|
395
|
+
device_id: deviceID,
|
|
396
|
+
sessions: [
|
|
397
|
+
{
|
|
398
|
+
agent: session.agent,
|
|
399
|
+
file_path: session.file_path,
|
|
400
|
+
scope: session.scope,
|
|
401
|
+
version: session.version + 1,
|
|
402
|
+
local_path: localPath ?? undefined,
|
|
403
|
+
deleted: true,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
console.log(chalk.green(` ✓ ${session.file_path}`), chalk.gray(mode === "e" ? "deleted everywhere" : "removed from this device"));
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
console.log(chalk.red(` ✗ ${session.file_path}`), chalk.gray(err.message));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
console.log();
|
|
415
|
+
}
|
|
416
|
+
export async function doctorAgentSessionsCommand() {
|
|
417
|
+
const cwd = process.cwd();
|
|
418
|
+
const project = resolveProjectScope(cwd);
|
|
419
|
+
const deviceID = getOrCreateDeviceID();
|
|
420
|
+
const locations = discoverAgentSessions();
|
|
421
|
+
const localByKey = new Map();
|
|
422
|
+
for (const loc of locations) {
|
|
423
|
+
if (!existsSync(loc.path))
|
|
424
|
+
continue;
|
|
425
|
+
localByKey.set(`${loc.agent}|${loc.filePath}|${loc.scope}`, loc);
|
|
426
|
+
}
|
|
427
|
+
console.log(chalk.bold("\n Memax Agent Session Doctor\n"));
|
|
428
|
+
console.log(` Device ${chalk.bold(deviceID)}`);
|
|
429
|
+
console.log(` CWD ${chalk.gray(cwd)}`);
|
|
430
|
+
console.log(` Scope ${chalk.bold(project.scope)}`);
|
|
431
|
+
if (project.warning) {
|
|
432
|
+
console.log(` Warning ${chalk.yellow(project.warning)}`);
|
|
433
|
+
}
|
|
434
|
+
console.log();
|
|
435
|
+
console.log(chalk.white(" Local Discovery"));
|
|
436
|
+
if (localByKey.size === 0) {
|
|
437
|
+
console.log(` ${chalk.gray("No supported local session artifacts discovered.")}`);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
for (const loc of localByKey.values()) {
|
|
441
|
+
console.log(` ${chalk.cyan(formatAgentName(loc.agent))} ${loc.filePath} ${chalk.gray(loc.scope)} ${chalk.gray(loc.path)}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
console.log();
|
|
445
|
+
try {
|
|
446
|
+
const cloud = await getClient().agentSessions.list();
|
|
447
|
+
const placements = cloud.sessions.map((session) => ({
|
|
448
|
+
session,
|
|
449
|
+
placement: classifyAgentSessionPlacement(session.agent, session.file_path, session.scope, {
|
|
450
|
+
cwd,
|
|
451
|
+
home: homedir(),
|
|
452
|
+
currentProjectScope: project.scope,
|
|
453
|
+
localByKey,
|
|
454
|
+
}),
|
|
455
|
+
}));
|
|
456
|
+
printSessionPlacementSection(" Restorable Here", chalk.cyan, placements.filter((item) => item.placement.kind === "restorable"));
|
|
457
|
+
printSessionPlacementSection(" Different Project", chalk.yellow, placements.filter((item) => item.placement.kind === "different_project"));
|
|
458
|
+
printSessionPlacementSection(" Unresolved", chalk.magenta, placements.filter((item) => item.placement.kind === "unresolved"));
|
|
459
|
+
console.log(chalk.gray(" Session sync restores only when placement is safe. Ambiguous session stores are skipped.\n"));
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
console.error(chalk.red(` Failed to fetch cloud session artifacts: ${err.message}\n`));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function discoverAgentSessions() {
|
|
466
|
+
const home = homedir();
|
|
467
|
+
const config = loadConfig();
|
|
468
|
+
const locations = [];
|
|
469
|
+
const add = (agent, path, filePath, scope, sessionType) => {
|
|
470
|
+
locations.push({
|
|
471
|
+
agent,
|
|
472
|
+
path,
|
|
473
|
+
filePath: normalizeFilePath(filePath),
|
|
474
|
+
scope,
|
|
475
|
+
sessionType,
|
|
476
|
+
});
|
|
477
|
+
};
|
|
478
|
+
const claudeProjectsDir = join(home, ".claude", "projects");
|
|
479
|
+
if (existsSync(claudeProjectsDir)) {
|
|
480
|
+
for (const project of safeListDir(claudeProjectsDir)) {
|
|
481
|
+
const repoUrl = resolveClaudeProjectFolder(project);
|
|
482
|
+
if (!repoUrl)
|
|
483
|
+
continue;
|
|
484
|
+
const projectDir = join(claudeProjectsDir, project);
|
|
485
|
+
for (const file of safeListDir(projectDir)) {
|
|
486
|
+
if (!file.endsWith(".jsonl"))
|
|
487
|
+
continue;
|
|
488
|
+
add("claude-code", join(projectDir, file), `sessions/${file}`, `project:${repoUrl}`, "transcript");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const codexHistory = join(home, ".codex", "history.jsonl");
|
|
493
|
+
add("codex", codexHistory, "history.jsonl", "global", "history");
|
|
494
|
+
const codexSessionsRoot = join(home, ".codex", "sessions");
|
|
495
|
+
if (existsSync(codexSessionsRoot)) {
|
|
496
|
+
for (const file of walkFiles(codexSessionsRoot, (entry) => entry.endsWith(".jsonl"))) {
|
|
497
|
+
add("codex", file, join("sessions", relative(codexSessionsRoot, file)), "global", "transcript");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const geminiTmpRoot = join(home, ".gemini", "tmp");
|
|
501
|
+
if (existsSync(geminiTmpRoot)) {
|
|
502
|
+
for (const projectDirName of safeListDir(geminiTmpRoot)) {
|
|
503
|
+
const projectDir = join(geminiTmpRoot, projectDirName);
|
|
504
|
+
const projectRootPath = readProjectRootMarker(join(projectDir, ".project_root"));
|
|
505
|
+
if (!projectRootPath)
|
|
506
|
+
continue;
|
|
507
|
+
const scope = getProjectScope(projectRootPath);
|
|
508
|
+
if (!scope.startsWith("project:"))
|
|
509
|
+
continue;
|
|
510
|
+
const chatsDir = join(projectDir, "chats");
|
|
511
|
+
if (!existsSync(chatsDir))
|
|
512
|
+
continue;
|
|
513
|
+
for (const file of safeListDir(chatsDir)) {
|
|
514
|
+
if (!file.endsWith(".json"))
|
|
515
|
+
continue;
|
|
516
|
+
add("gemini", join(chatsDir, file), `chats/${file}`, scope, "session");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const root of config.agent_session_roots ?? []) {
|
|
521
|
+
const normalizedScope = (root.scope || "").trim();
|
|
522
|
+
if (normalizedScope === "")
|
|
523
|
+
continue;
|
|
524
|
+
const rootPath = root.root_path ? resolveHome(root.root_path) : "";
|
|
525
|
+
if (!rootPath || !existsSync(rootPath))
|
|
526
|
+
continue;
|
|
527
|
+
const includeExtensions = root.include_extensions && root.include_extensions.length > 0
|
|
528
|
+
? new Set(root.include_extensions.map((value) => value.toLowerCase()))
|
|
529
|
+
: new Set([".jsonl", ".json", ".md", ".txt"]);
|
|
530
|
+
const sessionType = root.session_type?.trim() || "artifact";
|
|
531
|
+
for (const file of walkFiles(rootPath, (entry) => includeExtensions.has(extension(entry)))) {
|
|
532
|
+
add(root.agent, file, relative(rootPath, file), normalizedScope, sessionType);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return locations;
|
|
536
|
+
}
|
|
537
|
+
function findGeminiProjectDir(home, scope) {
|
|
538
|
+
const geminiTmpRoot = join(home, ".gemini", "tmp");
|
|
539
|
+
if (!existsSync(geminiTmpRoot))
|
|
540
|
+
return null;
|
|
541
|
+
for (const projectDirName of safeListDir(geminiTmpRoot)) {
|
|
542
|
+
const projectDir = join(geminiTmpRoot, projectDirName);
|
|
543
|
+
const projectRootPath = readProjectRootMarker(join(projectDir, ".project_root"));
|
|
544
|
+
if (!projectRootPath)
|
|
545
|
+
continue;
|
|
546
|
+
if (getProjectScope(projectRootPath) === scope) {
|
|
547
|
+
return projectDir;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
export function resolveAgentSessionWritePath(agent, filePath, scope, options = {}) {
|
|
553
|
+
const cwd = options.cwd ?? process.cwd();
|
|
554
|
+
const home = options.home ?? homedir();
|
|
555
|
+
const currentProjectScope = options.currentProjectScope ?? getProjectScope(cwd);
|
|
556
|
+
const normalized = normalizeFilePath(filePath);
|
|
557
|
+
if (scope === "global") {
|
|
558
|
+
switch (agent) {
|
|
559
|
+
case "codex":
|
|
560
|
+
if (normalized === "history.jsonl") {
|
|
561
|
+
return join(home, ".codex", "history.jsonl");
|
|
562
|
+
}
|
|
563
|
+
if (normalized.startsWith("sessions/")) {
|
|
564
|
+
return join(home, ".codex", normalized);
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
default:
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!scope.startsWith("project:") || scope !== currentProjectScope) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
switch (agent) {
|
|
575
|
+
case "claude-code": {
|
|
576
|
+
if (!normalized.startsWith("sessions/"))
|
|
577
|
+
return null;
|
|
578
|
+
const claudeProjectsDir = join(home, ".claude", "projects");
|
|
579
|
+
for (const project of safeListDir(claudeProjectsDir)) {
|
|
580
|
+
const repoUrl = resolveClaudeProjectFolder(project);
|
|
581
|
+
if (repoUrl && `project:${repoUrl}` === scope) {
|
|
582
|
+
return join(claudeProjectsDir, project, normalized.replace(/^sessions\//, ""));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
case "gemini": {
|
|
588
|
+
if (!normalized.startsWith("chats/"))
|
|
589
|
+
return null;
|
|
590
|
+
const projectDir = findGeminiProjectDir(home, scope);
|
|
591
|
+
if (!projectDir)
|
|
592
|
+
return null;
|
|
593
|
+
return join(projectDir, normalized);
|
|
594
|
+
}
|
|
595
|
+
default:
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
export function classifyAgentSessionPlacement(agent, filePath, scope, options = {}) {
|
|
600
|
+
const key = `${agent}|${normalizeFilePath(filePath)}|${scope}`;
|
|
601
|
+
const existing = options.localByKey?.get(key);
|
|
602
|
+
if (existing) {
|
|
603
|
+
return { kind: "present", path: existing.path, reason: "present locally" };
|
|
604
|
+
}
|
|
605
|
+
const cwd = options.cwd ?? process.cwd();
|
|
606
|
+
const currentProjectScope = options.currentProjectScope ?? getProjectScope(cwd);
|
|
607
|
+
if (scope.startsWith("project:") && scope !== currentProjectScope) {
|
|
608
|
+
return {
|
|
609
|
+
kind: "different_project",
|
|
610
|
+
reason: `belongs to ${scope.replace(/^project:/, "")}`,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const path = resolveAgentSessionWritePath(agent, filePath, scope, options);
|
|
614
|
+
if (path) {
|
|
615
|
+
return { kind: "restorable", path, reason: "safe restore path available" };
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
kind: "unresolved",
|
|
619
|
+
reason: "no safe restore path on this machine",
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
async function uploadLocalFile(path, content, sha256) {
|
|
623
|
+
const stat = statSync(path);
|
|
624
|
+
const contentType = inferContentType(path);
|
|
625
|
+
const intent = await getClient().uploads.create({
|
|
626
|
+
filename: basenameSafe(path),
|
|
627
|
+
content_type: contentType,
|
|
628
|
+
size_bytes: stat.size,
|
|
629
|
+
});
|
|
630
|
+
await putUpload(intent, content);
|
|
631
|
+
return {
|
|
632
|
+
object_key: intent.object_key,
|
|
633
|
+
filename: basenameSafe(path),
|
|
634
|
+
content_type: contentType,
|
|
635
|
+
size_bytes: stat.size,
|
|
636
|
+
sha256,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async function putUpload(intent, content) {
|
|
640
|
+
const res = await fetch(intent.upload_url, {
|
|
641
|
+
method: "PUT",
|
|
642
|
+
headers: intent.headers,
|
|
643
|
+
body: content,
|
|
644
|
+
});
|
|
645
|
+
if (!res.ok) {
|
|
646
|
+
throw new Error(`upload failed with status ${res.status}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async function downloadAgentSession(id) {
|
|
650
|
+
const headers = await getAuthHeaders();
|
|
651
|
+
const res = await fetch(`${loadConfig().api_url}/v1/agent-sessions/${id}/download`, {
|
|
652
|
+
headers,
|
|
653
|
+
});
|
|
654
|
+
if (!res.ok) {
|
|
655
|
+
throw new Error(`download failed with status ${res.status}`);
|
|
656
|
+
}
|
|
657
|
+
return Buffer.from(await res.arrayBuffer());
|
|
658
|
+
}
|
|
659
|
+
async function promptSessionConflict(filePath) {
|
|
660
|
+
const answer = await ask(` Conflict for ${filePath}. Use [l]ocal, [c]loud, or [s]kip? `);
|
|
661
|
+
const normalized = answer.trim().toLowerCase();
|
|
662
|
+
if (normalized === "l")
|
|
663
|
+
return "local";
|
|
664
|
+
if (normalized === "c")
|
|
665
|
+
return "cloud";
|
|
666
|
+
return "skip";
|
|
667
|
+
}
|
|
668
|
+
function readProjectRootMarker(path) {
|
|
669
|
+
if (!existsSync(path))
|
|
670
|
+
return null;
|
|
671
|
+
try {
|
|
672
|
+
const value = readFileSync(path, "utf-8").trim();
|
|
673
|
+
return value || null;
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function walkFiles(root, include) {
|
|
680
|
+
const files = [];
|
|
681
|
+
const visit = (dir) => {
|
|
682
|
+
for (const entry of safeListDir(dir)) {
|
|
683
|
+
const fullPath = join(dir, entry);
|
|
684
|
+
let stat;
|
|
685
|
+
try {
|
|
686
|
+
stat = statSync(fullPath);
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (stat.isDirectory()) {
|
|
692
|
+
visit(fullPath);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (stat.isFile() && include(fullPath)) {
|
|
696
|
+
files.push(fullPath);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
visit(root);
|
|
701
|
+
return files;
|
|
702
|
+
}
|
|
703
|
+
function safeListDir(dir) {
|
|
704
|
+
try {
|
|
705
|
+
return readdirSync(dir);
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function printSessionPlacementSection(title, color, items) {
|
|
712
|
+
if (items.length === 0)
|
|
713
|
+
return;
|
|
714
|
+
console.log(chalk.white(title));
|
|
715
|
+
for (const item of items) {
|
|
716
|
+
console.log(` ${color(formatAgentName(item.session.agent))} ${item.session.file_path}`);
|
|
717
|
+
console.log(` ${chalk.gray(item.placement.reason)}`);
|
|
718
|
+
}
|
|
719
|
+
console.log();
|
|
720
|
+
}
|
|
721
|
+
function formatAgentName(agent) {
|
|
722
|
+
const labels = {
|
|
723
|
+
"claude-code": "Claude Code",
|
|
724
|
+
codex: "Codex",
|
|
725
|
+
gemini: "Gemini CLI",
|
|
726
|
+
openclaw: "OpenClaw",
|
|
727
|
+
opencode: "OpenCode",
|
|
728
|
+
};
|
|
729
|
+
return labels[agent] ?? agent;
|
|
730
|
+
}
|
|
731
|
+
function inferContentType(path) {
|
|
732
|
+
if (path.endsWith(".jsonl"))
|
|
733
|
+
return "application/x-ndjson";
|
|
734
|
+
if (path.endsWith(".json"))
|
|
735
|
+
return "application/json";
|
|
736
|
+
if (path.endsWith(".md"))
|
|
737
|
+
return "text/markdown";
|
|
738
|
+
return "text/plain";
|
|
739
|
+
}
|
|
740
|
+
function formatBytes(size) {
|
|
741
|
+
if (size < 1024)
|
|
742
|
+
return `${size} B`;
|
|
743
|
+
if (size < 1024 * 1024)
|
|
744
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
745
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
746
|
+
}
|
|
747
|
+
function basenameSafe(path) {
|
|
748
|
+
return normalizeFilePath(path).split("/").pop() ?? "artifact";
|
|
749
|
+
}
|
|
750
|
+
function resolveHome(path) {
|
|
751
|
+
if (path.startsWith("~/")) {
|
|
752
|
+
return join(homedir(), path.slice(2));
|
|
753
|
+
}
|
|
754
|
+
return path;
|
|
755
|
+
}
|
|
756
|
+
function extension(path) {
|
|
757
|
+
const normalized = normalizeFilePath(path).toLowerCase();
|
|
758
|
+
const dot = normalized.lastIndexOf(".");
|
|
759
|
+
return dot >= 0 ? normalized.slice(dot) : "";
|
|
760
|
+
}
|
|
761
|
+
//# sourceMappingURL=agent-sessions.js.map
|