memax-cli 0.1.0-alpha.35 → 0.1.0-alpha.37
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 +13 -0
- package/dist/commands/agent-sessions.d.ts.map +1 -1
- package/dist/commands/agent-sessions.js +359 -11
- package/dist/commands/agent-sessions.js.map +1 -1
- package/dist/commands/agent-sessions.test.js +123 -1
- package/dist/commands/agent-sessions.test.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +12 -2
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/recall.d.ts.map +1 -1
- package/dist/commands/recall.js +28 -9
- package/dist/commands/recall.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +9 -1
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +163 -7
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/topic.d.ts +4 -3
- package/dist/commands/topic.d.ts.map +1 -1
- package/dist/commands/topic.js +24 -5
- package/dist/commands/topic.js.map +1 -1
- package/dist/commands/topic.test.js +46 -1
- package/dist/commands/topic.test.js.map +1 -1
- package/dist/index.js +33 -10
- package/dist/index.js.map +1 -1
- package/dist/lib/trash.d.ts +6 -0
- package/dist/lib/trash.d.ts.map +1 -0
- package/dist/lib/trash.js +28 -0
- package/dist/lib/trash.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSession, SessionSyncPlanAction } from "memax-sdk";
|
|
1
2
|
interface AgentSessionLocation {
|
|
2
3
|
agent: string;
|
|
3
4
|
path: string;
|
|
@@ -17,6 +18,13 @@ interface SessionProjectContext {
|
|
|
17
18
|
projectRoot?: string;
|
|
18
19
|
}
|
|
19
20
|
export declare function hashPortableSessionContent(agent: string, content: Buffer, projectRoot?: string): string;
|
|
21
|
+
export declare function computeSessionSyncHash(agent: string, scope: string, content: Buffer, currentProjectRootPath: string): string;
|
|
22
|
+
export declare function isLegacyGlobalSessionShadowed(action: SessionSyncPlanAction, projectScopedKeys: Set<string>): boolean;
|
|
23
|
+
interface ShadowedGlobalSessionPair {
|
|
24
|
+
global: AgentSession;
|
|
25
|
+
project: AgentSession;
|
|
26
|
+
}
|
|
27
|
+
export declare function findShadowedGlobalSessions(sessions: AgentSession[]): ShadowedGlobalSessionPair[];
|
|
20
28
|
export declare function computeSessionProjectContext(agent: string, path: string, fallbackProjectRoot?: string | null): SessionProjectContext;
|
|
21
29
|
export declare function materializeAgentSessionContent(agent: string, content: Buffer, options: {
|
|
22
30
|
scope: string;
|
|
@@ -28,7 +36,12 @@ export declare function syncAgentSessionsCommand(options?: {
|
|
|
28
36
|
pull?: boolean;
|
|
29
37
|
}): Promise<void>;
|
|
30
38
|
export declare function listAgentSessionsCommand(): Promise<void>;
|
|
39
|
+
export declare function listDeletedAgentSessionsCommand(): Promise<void>;
|
|
40
|
+
export declare function restoreDeletedAgentSessionsCommand(): Promise<void>;
|
|
31
41
|
export declare function deleteAgentSessionsCommand(): Promise<void>;
|
|
42
|
+
export declare function cleanupAgentSessionsCommand(options?: {
|
|
43
|
+
yes?: boolean;
|
|
44
|
+
}): Promise<void>;
|
|
32
45
|
export declare function doctorAgentSessionsCommand(): Promise<void>;
|
|
33
46
|
export declare function resolveAgentSessionWritePath(agent: string, filePath: string, scope: string, options?: {
|
|
34
47
|
cwd?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-sessions.d.ts","sourceRoot":"","sources":["../../src/commands/agent-sessions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent-sessions.d.ts","sourceRoot":"","sources":["../../src/commands/agent-sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync,
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { getClient } from "../lib/client.js";
|
|
7
7
|
import { getProjectScope, resolveClaudeProjectPath, resolveClaudeProjectFolder, resolveProjectRootPath, resolveProjectScope, normalizeFilePath, } from "../lib/project-context.js";
|
|
8
8
|
import { getOrCreateDeviceID, loadConfig } from "../lib/config.js";
|
|
9
9
|
import { ask, confirmDefault } from "../lib/prompt.js";
|
|
10
|
+
import { moveFileToTrash } from "../lib/trash.js";
|
|
10
11
|
const PROJECT_ROOT_PLACEHOLDER = "__MEMAX_PROJECT_ROOT__";
|
|
12
|
+
const PORTABLE_SESSION_AGENTS = new Set(["claude-code", "codex", "gemini"]);
|
|
11
13
|
function isWithinRoot(candidate, root) {
|
|
12
14
|
return candidate === root || candidate.startsWith(`${root}/`);
|
|
13
15
|
}
|
|
@@ -93,6 +95,61 @@ export function hashPortableSessionContent(agent, content, projectRoot) {
|
|
|
93
95
|
const canonical = canonicalizeSessionContent(agent, content, projectRoot);
|
|
94
96
|
return createHash("sha256").update(canonical).digest("hex");
|
|
95
97
|
}
|
|
98
|
+
export function computeSessionSyncHash(agent, scope, content, currentProjectRootPath) {
|
|
99
|
+
return hashPortableSessionContent(agent, content, scope.startsWith("project:") ? currentProjectRootPath : undefined);
|
|
100
|
+
}
|
|
101
|
+
export function isLegacyGlobalSessionShadowed(action, projectScopedKeys) {
|
|
102
|
+
if (action.scope !== "global")
|
|
103
|
+
return false;
|
|
104
|
+
if (action.reason !== "cloud_only" &&
|
|
105
|
+
action.reason !== "deleted_everywhere") {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (!PORTABLE_SESSION_AGENTS.has(action.agent))
|
|
109
|
+
return false;
|
|
110
|
+
if (action.agent === "codex" &&
|
|
111
|
+
normalizeFilePath(action.file_path) === "history.jsonl") {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return projectScopedKeys.has(`${action.agent}|${normalizeFilePath(action.file_path)}`);
|
|
115
|
+
}
|
|
116
|
+
export function findShadowedGlobalSessions(sessions) {
|
|
117
|
+
const projectScopedByKey = new Map();
|
|
118
|
+
for (const session of sessions) {
|
|
119
|
+
if (!PORTABLE_SESSION_AGENTS.has(session.agent))
|
|
120
|
+
continue;
|
|
121
|
+
if (!session.scope.startsWith("project:"))
|
|
122
|
+
continue;
|
|
123
|
+
const normalized = normalizeFilePath(session.file_path);
|
|
124
|
+
if (session.agent === "codex" && normalized === "history.jsonl")
|
|
125
|
+
continue;
|
|
126
|
+
const key = `${session.agent}|${normalized}`;
|
|
127
|
+
const group = projectScopedByKey.get(key) ?? [];
|
|
128
|
+
group.push(session);
|
|
129
|
+
projectScopedByKey.set(key, group);
|
|
130
|
+
}
|
|
131
|
+
const pairs = [];
|
|
132
|
+
for (const session of sessions) {
|
|
133
|
+
if (!PORTABLE_SESSION_AGENTS.has(session.agent))
|
|
134
|
+
continue;
|
|
135
|
+
if (session.scope !== "global")
|
|
136
|
+
continue;
|
|
137
|
+
const normalized = normalizeFilePath(session.file_path);
|
|
138
|
+
if (session.agent === "codex" && normalized === "history.jsonl")
|
|
139
|
+
continue;
|
|
140
|
+
const siblings = projectScopedByKey.get(`${session.agent}|${normalized}`);
|
|
141
|
+
if (!siblings)
|
|
142
|
+
continue;
|
|
143
|
+
for (const sibling of siblings) {
|
|
144
|
+
pairs.push({ global: session, project: sibling });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return pairs;
|
|
148
|
+
}
|
|
149
|
+
function computeDownloadedPortableHash(agent, content) {
|
|
150
|
+
const projectRoot = readStructuredSessionRootFromContent(agent, content);
|
|
151
|
+
return hashPortableSessionContent(agent, content, projectRoot ?? undefined);
|
|
152
|
+
}
|
|
96
153
|
function readStructuredSessionRootFromContent(agent, content) {
|
|
97
154
|
try {
|
|
98
155
|
const raw = content.toString("utf-8");
|
|
@@ -232,6 +289,10 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
232
289
|
return false;
|
|
233
290
|
return true;
|
|
234
291
|
});
|
|
292
|
+
const projectScopedKeys = new Set(actions
|
|
293
|
+
.filter((action) => action.scope.startsWith("project:"))
|
|
294
|
+
.map((action) => `${action.agent}|${normalizeFilePath(action.file_path)}`));
|
|
295
|
+
actions = actions.filter((action) => !isLegacyGlobalSessionShadowed(action, projectScopedKeys));
|
|
235
296
|
const localByKey = new Map();
|
|
236
297
|
for (const session of localSessions) {
|
|
237
298
|
localByKey.set(`${session.loc.agent}|${session.loc.filePath}|${session.loc.scope}`, session);
|
|
@@ -254,6 +315,7 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
254
315
|
let pushed = 0;
|
|
255
316
|
let pulled = 0;
|
|
256
317
|
let deletedLocal = 0;
|
|
318
|
+
let reconciled = 0;
|
|
257
319
|
let unchanged = 0;
|
|
258
320
|
let skipped = 0;
|
|
259
321
|
let errors = 0;
|
|
@@ -343,17 +405,18 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
343
405
|
const session = await getClient().agentSessions.get(action.session_id);
|
|
344
406
|
const bytes = await downloadAgentSession(action.session_id);
|
|
345
407
|
mkdirSync(dirname(writePath), { recursive: true });
|
|
346
|
-
|
|
408
|
+
const materialized = materializeAgentSessionContent(action.agent, bytes, {
|
|
347
409
|
scope: action.scope,
|
|
348
410
|
currentProjectRootPath,
|
|
349
411
|
writePath,
|
|
350
|
-
})
|
|
412
|
+
});
|
|
413
|
+
writeFileSync(writePath, materialized);
|
|
351
414
|
if (action.version) {
|
|
352
415
|
ackSessions.push({
|
|
353
416
|
agent: action.agent,
|
|
354
417
|
file_path: action.file_path,
|
|
355
418
|
scope: action.scope,
|
|
356
|
-
content_hash:
|
|
419
|
+
content_hash: computeSessionSyncHash(action.agent, action.scope, materialized, currentProjectRootPath),
|
|
357
420
|
version: action.version,
|
|
358
421
|
local_path: writePath,
|
|
359
422
|
});
|
|
@@ -368,10 +431,65 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
368
431
|
continue;
|
|
369
432
|
}
|
|
370
433
|
if (action.action === "delete_local") {
|
|
434
|
+
if (isLegacyGlobalSessionShadowed(action, projectScopedKeys)) {
|
|
435
|
+
if (action.version) {
|
|
436
|
+
ackSessions.push({
|
|
437
|
+
agent: action.agent,
|
|
438
|
+
file_path: action.file_path,
|
|
439
|
+
scope: action.scope,
|
|
440
|
+
version: action.version,
|
|
441
|
+
deleted: true,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
console.log(chalk.gray(` = ${action.file_path}`), chalk.gray("legacy global duplicate removed"));
|
|
445
|
+
reconciled++;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (!options.pull) {
|
|
449
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
450
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("cloud deleted this session artifact — skipped in non-interactive mode"));
|
|
451
|
+
skipped++;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const resolution = await promptSessionCloudDeletion(action.file_path);
|
|
455
|
+
if (resolution === "skip") {
|
|
456
|
+
console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
|
|
457
|
+
skipped++;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (resolution === "local") {
|
|
461
|
+
const local = localByKey.get(key);
|
|
462
|
+
if (!local) {
|
|
463
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("local file missing — skipped"));
|
|
464
|
+
skipped++;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
const fileRef = await uploadLocalFile(local.loc.path, local.content);
|
|
469
|
+
await getClient().agentSessions.upsert({
|
|
470
|
+
agent: action.agent,
|
|
471
|
+
file_path: action.file_path,
|
|
472
|
+
scope: action.scope,
|
|
473
|
+
session_type: local.loc.sessionType,
|
|
474
|
+
content_hash: local.hash,
|
|
475
|
+
device_id: deviceID,
|
|
476
|
+
local_path: local.loc.path,
|
|
477
|
+
file_ref: fileRef,
|
|
478
|
+
});
|
|
479
|
+
console.log(chalk.green(` ↑ ${action.file_path}`), chalk.gray("kept local and restored to cloud"));
|
|
480
|
+
pushed++;
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
console.log(chalk.red(` ✗ ${action.file_path}`), chalk.gray(err.message));
|
|
484
|
+
errors++;
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
371
489
|
try {
|
|
372
490
|
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
373
491
|
if (writePath && existsSync(writePath)) {
|
|
374
|
-
|
|
492
|
+
moveFileToTrash(writePath, "agent-sessions");
|
|
375
493
|
}
|
|
376
494
|
if (action.version) {
|
|
377
495
|
ackSessions.push({
|
|
@@ -383,7 +501,7 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
383
501
|
deleted: true,
|
|
384
502
|
});
|
|
385
503
|
}
|
|
386
|
-
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("deleted locally (
|
|
504
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("deleted locally (moved to Memax trash)"));
|
|
387
505
|
deletedLocal++;
|
|
388
506
|
}
|
|
389
507
|
catch (err) {
|
|
@@ -392,6 +510,11 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
392
510
|
}
|
|
393
511
|
continue;
|
|
394
512
|
}
|
|
513
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
514
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("conflict skipped in non-interactive mode"));
|
|
515
|
+
skipped++;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
395
518
|
const resolution = await promptSessionConflict(action.file_path);
|
|
396
519
|
if (resolution === "local") {
|
|
397
520
|
const local = localByKey.get(key);
|
|
@@ -429,17 +552,18 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
429
552
|
const session = await getClient().agentSessions.get(action.session_id);
|
|
430
553
|
const bytes = await downloadAgentSession(action.session_id);
|
|
431
554
|
mkdirSync(dirname(writePath), { recursive: true });
|
|
432
|
-
|
|
555
|
+
const materialized = materializeAgentSessionContent(action.agent, bytes, {
|
|
433
556
|
scope: action.scope,
|
|
434
557
|
currentProjectRootPath,
|
|
435
558
|
writePath,
|
|
436
|
-
})
|
|
559
|
+
});
|
|
560
|
+
writeFileSync(writePath, materialized);
|
|
437
561
|
if (action.version) {
|
|
438
562
|
ackSessions.push({
|
|
439
563
|
agent: action.agent,
|
|
440
564
|
file_path: action.file_path,
|
|
441
565
|
scope: action.scope,
|
|
442
|
-
content_hash:
|
|
566
|
+
content_hash: computeSessionSyncHash(action.agent, action.scope, materialized, currentProjectRootPath),
|
|
443
567
|
version: action.version,
|
|
444
568
|
local_path: writePath,
|
|
445
569
|
});
|
|
@@ -476,6 +600,8 @@ export async function syncAgentSessionsCommand(options = {}) {
|
|
|
476
600
|
parts.push(`${pulled} restored`);
|
|
477
601
|
if (deletedLocal > 0)
|
|
478
602
|
parts.push(`${deletedLocal} deleted locally`);
|
|
603
|
+
if (reconciled > 0)
|
|
604
|
+
parts.push(`${reconciled} reconciled`);
|
|
479
605
|
if (unchanged > 0)
|
|
480
606
|
parts.push(`${unchanged} unchanged`);
|
|
481
607
|
if (skipped > 0)
|
|
@@ -510,6 +636,120 @@ export async function listAgentSessionsCommand() {
|
|
|
510
636
|
console.error(chalk.red(` Failed to fetch session artifacts: ${err.message}\n`));
|
|
511
637
|
}
|
|
512
638
|
}
|
|
639
|
+
export async function listDeletedAgentSessionsCommand() {
|
|
640
|
+
try {
|
|
641
|
+
const result = await getClient().agentSessions.listDeleted();
|
|
642
|
+
const sessions = result.sessions;
|
|
643
|
+
if (sessions.length === 0) {
|
|
644
|
+
console.log(chalk.gray(" No recoverable deleted session artifacts.\n"));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
console.log(chalk.bold("\n Recoverable Deleted Session Artifacts\n"));
|
|
648
|
+
for (const [index, session] of sessions.entries()) {
|
|
649
|
+
const scopeTag = session.scope === "global"
|
|
650
|
+
? chalk.dim("global")
|
|
651
|
+
: chalk.dim(session.scope.replace(/^project:/, ""));
|
|
652
|
+
console.log(` ${chalk.bold(String(index + 1).padStart(2, " "))}. ${chalk.cyan(formatAgentName(session.agent))} ${session.file_path} ${scopeTag}`);
|
|
653
|
+
console.log(chalk.gray(` deleted ${formatAge(session.deleted_at)} · recoverable until ${session.content_expires_at ? new Date(session.content_expires_at).toLocaleString() : "expired"}`));
|
|
654
|
+
}
|
|
655
|
+
console.log();
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
console.error(chalk.red(` Failed to fetch deleted session artifacts: ${err.message}\n`));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
export async function restoreDeletedAgentSessionsCommand() {
|
|
662
|
+
let deleted;
|
|
663
|
+
try {
|
|
664
|
+
deleted = await getClient().agentSessions.listDeleted();
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
console.error(chalk.red(` Failed to fetch deleted session artifacts: ${err.message}\n`));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (deleted.sessions.length === 0) {
|
|
671
|
+
console.log(chalk.gray(" No recoverable deleted session artifacts.\n"));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
console.log(chalk.bold("\n Recover Deleted Session Artifacts\n"));
|
|
675
|
+
deleted.sessions.forEach((session, index) => {
|
|
676
|
+
const scopeTag = session.scope === "global"
|
|
677
|
+
? chalk.dim("global")
|
|
678
|
+
: chalk.dim(session.scope.replace(/^project:/, ""));
|
|
679
|
+
console.log(` ${chalk.dim(`${index + 1}.`)} ${chalk.cyan(formatAgentName(session.agent))} ${session.file_path} ${scopeTag}`);
|
|
680
|
+
});
|
|
681
|
+
console.log();
|
|
682
|
+
const answer = await ask(" Select session artifacts to restore (comma-separated numbers, or 'q' to quit): ");
|
|
683
|
+
if (!answer || answer.trim().toLowerCase() === "q") {
|
|
684
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const indexes = answer
|
|
688
|
+
.split(",")
|
|
689
|
+
.map((part) => Number.parseInt(part.trim(), 10))
|
|
690
|
+
.filter((idx) => Number.isInteger(idx) && idx >= 1 && idx <= deleted.sessions.length);
|
|
691
|
+
if (indexes.length === 0) {
|
|
692
|
+
console.log(chalk.gray(" No valid selection.\n"));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const cwd = process.cwd();
|
|
696
|
+
const deviceID = getOrCreateDeviceID();
|
|
697
|
+
const currentProjectScope = getProjectScope(cwd);
|
|
698
|
+
const currentProjectRootPath = resolveProjectRootPath(cwd) ?? cwd;
|
|
699
|
+
let restored = 0;
|
|
700
|
+
for (const index of indexes) {
|
|
701
|
+
const sessionInfo = deleted.sessions[index - 1];
|
|
702
|
+
try {
|
|
703
|
+
const writePath = resolveAgentSessionWritePath(sessionInfo.agent, sessionInfo.file_path, sessionInfo.scope, {
|
|
704
|
+
cwd,
|
|
705
|
+
home: homedir(),
|
|
706
|
+
currentProjectScope,
|
|
707
|
+
});
|
|
708
|
+
const session = await getClient().agentSessions.restore({
|
|
709
|
+
agent: sessionInfo.agent,
|
|
710
|
+
file_path: sessionInfo.file_path,
|
|
711
|
+
scope: sessionInfo.scope,
|
|
712
|
+
device_id: deviceID,
|
|
713
|
+
local_path: writePath ?? undefined,
|
|
714
|
+
});
|
|
715
|
+
if (writePath && !existsSync(writePath)) {
|
|
716
|
+
const bytes = await downloadAgentSession(session.id);
|
|
717
|
+
mkdirSync(dirname(writePath), { recursive: true });
|
|
718
|
+
const materialized = materializeAgentSessionContent(session.agent, bytes, {
|
|
719
|
+
scope: session.scope,
|
|
720
|
+
currentProjectRootPath,
|
|
721
|
+
writePath,
|
|
722
|
+
});
|
|
723
|
+
writeFileSync(writePath, materialized);
|
|
724
|
+
await getClient().agentSessions.ack({
|
|
725
|
+
device_id: deviceID,
|
|
726
|
+
sessions: [
|
|
727
|
+
{
|
|
728
|
+
agent: session.agent,
|
|
729
|
+
file_path: session.file_path,
|
|
730
|
+
scope: session.scope,
|
|
731
|
+
content_hash: computeSessionSyncHash(session.agent, session.scope, materialized, currentProjectRootPath),
|
|
732
|
+
version: session.version,
|
|
733
|
+
local_path: writePath,
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
});
|
|
737
|
+
console.log(chalk.green(` ✓ ${session.file_path}`), chalk.gray("restored to cloud and local machine"));
|
|
738
|
+
}
|
|
739
|
+
else if (writePath && existsSync(writePath)) {
|
|
740
|
+
console.log(chalk.yellow(` - ${session.file_path}`), chalk.gray("restored to cloud; local file already exists"));
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
console.log(chalk.yellow(` - ${session.file_path}`), chalk.gray("restored to cloud; no safe local path on this machine"));
|
|
744
|
+
}
|
|
745
|
+
restored++;
|
|
746
|
+
}
|
|
747
|
+
catch (err) {
|
|
748
|
+
console.log(chalk.red(` ✗ ${sessionInfo.file_path}`), chalk.gray(err.message));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
console.log(chalk.gray(`\n ${restored} session artifact${restored === 1 ? "" : "s"} restored.\n`));
|
|
752
|
+
}
|
|
513
753
|
export async function deleteAgentSessionsCommand() {
|
|
514
754
|
let sessions;
|
|
515
755
|
try {
|
|
@@ -566,13 +806,13 @@ export async function deleteAgentSessionsCommand() {
|
|
|
566
806
|
local_path: localPath ?? undefined,
|
|
567
807
|
});
|
|
568
808
|
if (localPath && existsSync(localPath)) {
|
|
569
|
-
|
|
809
|
+
moveFileToTrash(localPath, "agent-sessions");
|
|
570
810
|
}
|
|
571
811
|
}
|
|
572
812
|
else {
|
|
573
813
|
await getClient().agentSessions.delete(session.id);
|
|
574
814
|
if (localPath && existsSync(localPath)) {
|
|
575
|
-
|
|
815
|
+
moveFileToTrash(localPath, "agent-sessions");
|
|
576
816
|
}
|
|
577
817
|
await getClient().agentSessions.ack({
|
|
578
818
|
device_id: deviceID,
|
|
@@ -596,6 +836,94 @@ export async function deleteAgentSessionsCommand() {
|
|
|
596
836
|
}
|
|
597
837
|
console.log();
|
|
598
838
|
}
|
|
839
|
+
export async function cleanupAgentSessionsCommand(options) {
|
|
840
|
+
let sessions;
|
|
841
|
+
try {
|
|
842
|
+
sessions = (await getClient().agentSessions.list()).sessions;
|
|
843
|
+
}
|
|
844
|
+
catch (err) {
|
|
845
|
+
console.error(chalk.red(` Failed to fetch session artifacts: ${err.message}\n`));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const pairs = findShadowedGlobalSessions(sessions);
|
|
849
|
+
if (pairs.length === 0) {
|
|
850
|
+
console.log(chalk.gray(" No legacy global session duplicates found.\n"));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const safePairs = [];
|
|
854
|
+
const divergedPairs = [];
|
|
855
|
+
for (const pair of pairs) {
|
|
856
|
+
if (pair.global.content_hash === pair.project.content_hash) {
|
|
857
|
+
safePairs.push(pair);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
const [globalBytes, projectBytes] = await Promise.all([
|
|
862
|
+
downloadAgentSession(pair.global.id),
|
|
863
|
+
downloadAgentSession(pair.project.id),
|
|
864
|
+
]);
|
|
865
|
+
if (computeDownloadedPortableHash(pair.global.agent, globalBytes) ===
|
|
866
|
+
computeDownloadedPortableHash(pair.project.agent, projectBytes)) {
|
|
867
|
+
safePairs.push(pair);
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
divergedPairs.push(pair);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
divergedPairs.push(pair);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
console.log(chalk.bold("\n Session Duplicate Cleanup\n"));
|
|
878
|
+
if (safePairs.length > 0) {
|
|
879
|
+
console.log(chalk.white(" Safe To Remove"));
|
|
880
|
+
for (const pair of safePairs) {
|
|
881
|
+
console.log(` ${chalk.cyan(formatAgentName(pair.global.agent))} ${pair.global.file_path}`);
|
|
882
|
+
console.log(` ${chalk.gray("delete legacy global copy; project-scoped copy remains")}`);
|
|
883
|
+
}
|
|
884
|
+
console.log();
|
|
885
|
+
}
|
|
886
|
+
if (divergedPairs.length > 0) {
|
|
887
|
+
console.log(chalk.yellow(" Needs Manual Review"));
|
|
888
|
+
for (const pair of divergedPairs) {
|
|
889
|
+
console.log(` ${chalk.yellow(formatAgentName(pair.global.agent))} ${pair.global.file_path}`);
|
|
890
|
+
console.log(` ${chalk.gray(`global hash ${pair.global.content_hash.slice(0, 8)}… differs from project ${pair.project.scope.replace(/^project:/, "")} hash ${pair.project.content_hash.slice(0, 8)}…`)}`);
|
|
891
|
+
}
|
|
892
|
+
console.log();
|
|
893
|
+
}
|
|
894
|
+
if (safePairs.length === 0) {
|
|
895
|
+
console.log(chalk.gray(" No identical legacy global duplicates can be removed safely.\n"));
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (!options?.yes) {
|
|
899
|
+
const proceed = await confirmDefault(` Delete ${safePairs.length} safe global duplicate${safePairs.length === 1 ? "" : "s"} from cloud? [Y/n] `);
|
|
900
|
+
if (!proceed) {
|
|
901
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
let deleted = 0;
|
|
906
|
+
let errors = 0;
|
|
907
|
+
for (const pair of safePairs) {
|
|
908
|
+
try {
|
|
909
|
+
await getClient().agentSessions.delete(pair.global.id);
|
|
910
|
+
console.log(chalk.green(` ✓ ${pair.global.file_path}`), chalk.gray("deleted legacy global copy"));
|
|
911
|
+
deleted++;
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
console.log(chalk.red(` ✗ ${pair.global.file_path}`), chalk.gray(err.message));
|
|
915
|
+
errors++;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const summary = [];
|
|
919
|
+
if (deleted > 0)
|
|
920
|
+
summary.push(`${deleted} deleted`);
|
|
921
|
+
if (divergedPairs.length > 0)
|
|
922
|
+
summary.push(`${divergedPairs.length} need review`);
|
|
923
|
+
if (errors > 0)
|
|
924
|
+
summary.push(`${errors} errors`);
|
|
925
|
+
console.log(chalk.bold(`\n Done: ${summary.join(", ")}\n`));
|
|
926
|
+
}
|
|
599
927
|
export async function doctorAgentSessionsCommand() {
|
|
600
928
|
const cwd = process.cwd();
|
|
601
929
|
const project = resolveProjectScope(cwd);
|
|
@@ -854,6 +1182,15 @@ async function promptSessionConflict(filePath) {
|
|
|
854
1182
|
return "cloud";
|
|
855
1183
|
return "skip";
|
|
856
1184
|
}
|
|
1185
|
+
async function promptSessionCloudDeletion(filePath) {
|
|
1186
|
+
const answer = await ask(` ${filePath} was deleted in cloud. [d]elete local, [k]eep local and restore cloud, or [s]kip? `);
|
|
1187
|
+
const normalized = answer.trim().toLowerCase();
|
|
1188
|
+
if (normalized === "d")
|
|
1189
|
+
return "delete";
|
|
1190
|
+
if (normalized === "k")
|
|
1191
|
+
return "local";
|
|
1192
|
+
return "skip";
|
|
1193
|
+
}
|
|
857
1194
|
function readProjectRootMarker(path) {
|
|
858
1195
|
if (!existsSync(path))
|
|
859
1196
|
return null;
|
|
@@ -933,6 +1270,17 @@ function formatBytes(size) {
|
|
|
933
1270
|
return `${(size / 1024).toFixed(1)} KB`;
|
|
934
1271
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
935
1272
|
}
|
|
1273
|
+
function formatAge(dateStr) {
|
|
1274
|
+
const ms = Date.now() - new Date(dateStr).getTime();
|
|
1275
|
+
const mins = Math.floor(ms / 60000);
|
|
1276
|
+
if (mins < 60)
|
|
1277
|
+
return `${mins}m ago`;
|
|
1278
|
+
const hours = Math.floor(mins / 60);
|
|
1279
|
+
if (hours < 24)
|
|
1280
|
+
return `${hours}h ago`;
|
|
1281
|
+
const days = Math.floor(hours / 24);
|
|
1282
|
+
return `${days}d ago`;
|
|
1283
|
+
}
|
|
936
1284
|
function basenameSafe(path) {
|
|
937
1285
|
return normalizeFilePath(path).split("/").pop() ?? "artifact";
|
|
938
1286
|
}
|