memax-cli 0.1.0-alpha.8 → 0.1.0
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/assets/skills/memax-memory/SKILL.md +173 -0
- package/dist/commands/agent-configs.d.ts +41 -0
- package/dist/commands/agent-configs.d.ts.map +1 -0
- package/dist/commands/agent-configs.js +1290 -0
- package/dist/commands/agent-configs.js.map +1 -0
- package/dist/commands/agent-configs.test.d.ts +2 -0
- package/dist/commands/agent-configs.test.d.ts.map +1 -0
- package/dist/commands/agent-configs.test.js +122 -0
- package/dist/commands/agent-configs.test.js.map +1 -0
- package/dist/commands/agent-sessions.d.ts +74 -0
- package/dist/commands/agent-sessions.d.ts.map +1 -0
- package/dist/commands/agent-sessions.js +1513 -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 +255 -0
- package/dist/commands/agent-sessions.test.js.map +1 -0
- package/dist/commands/agents.d.ts +3 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +36 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/ask.d.ts +15 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +483 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +103 -8
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/capture.d.ts +19 -0
- package/dist/commands/capture.d.ts.map +1 -0
- package/dist/commands/capture.js +69 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +13 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/delete.d.ts +2 -0
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +15 -18
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/dreams.d.ts +22 -0
- package/dist/commands/dreams.d.ts.map +1 -0
- package/dist/commands/dreams.js +251 -0
- package/dist/commands/dreams.js.map +1 -0
- package/dist/commands/dreams.test.d.ts +2 -0
- package/dist/commands/dreams.test.d.ts.map +1 -0
- package/dist/commands/dreams.test.js +39 -0
- package/dist/commands/dreams.test.js.map +1 -0
- package/dist/commands/hook.d.ts +2 -0
- package/dist/commands/hook.d.ts.map +1 -1
- package/dist/commands/hook.js +25 -103
- package/dist/commands/hook.js.map +1 -1
- package/dist/commands/hub.d.ts +37 -0
- package/dist/commands/hub.d.ts.map +1 -0
- package/dist/commands/hub.js +347 -0
- package/dist/commands/hub.js.map +1 -0
- package/dist/commands/hub.test.d.ts +2 -0
- package/dist/commands/hub.test.d.ts.map +1 -0
- package/dist/commands/hub.test.js +62 -0
- package/dist/commands/hub.test.js.map +1 -0
- package/dist/commands/import.d.ts +13 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +257 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/import.test.d.ts +2 -0
- package/dist/commands/import.test.d.ts.map +1 -0
- package/dist/commands/import.test.js +11 -0
- package/dist/commands/import.test.js.map +1 -0
- package/dist/commands/list.d.ts +9 -2
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +118 -9
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.d.ts +2 -0
- package/dist/commands/list.test.d.ts.map +1 -0
- package/dist/commands/list.test.js +20 -0
- package/dist/commands/list.test.js.map +1 -0
- package/dist/commands/login.d.ts +7 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +81 -20
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +386 -65
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/push.d.ts +6 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +42 -8
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/recall.d.ts +11 -1
- package/dist/commands/recall.d.ts.map +1 -1
- package/dist/commands/recall.js +228 -41
- package/dist/commands/recall.js.map +1 -1
- package/dist/commands/recall.test.d.ts +2 -0
- package/dist/commands/recall.test.d.ts.map +1 -0
- package/dist/commands/recall.test.js +31 -0
- package/dist/commands/recall.test.js.map +1 -0
- package/dist/commands/setup-hooks.d.ts +13 -0
- package/dist/commands/setup-hooks.d.ts.map +1 -0
- package/dist/commands/setup-hooks.js +193 -0
- package/dist/commands/setup-hooks.js.map +1 -0
- package/dist/commands/setup-instructions.d.ts +21 -0
- package/dist/commands/setup-instructions.d.ts.map +1 -0
- package/dist/commands/setup-instructions.js +172 -0
- package/dist/commands/setup-instructions.js.map +1 -0
- package/dist/commands/setup-mcp.d.ts +40 -0
- package/dist/commands/setup-mcp.d.ts.map +1 -0
- package/dist/commands/setup-mcp.js +414 -0
- package/dist/commands/setup-mcp.js.map +1 -0
- package/dist/commands/setup-types.d.ts +33 -0
- package/dist/commands/setup-types.d.ts.map +1 -0
- package/dist/commands/setup-types.js +60 -0
- package/dist/commands/setup-types.js.map +1 -0
- package/dist/commands/setup.d.ts +10 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +216 -532
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/show.d.ts +5 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +36 -14
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/topic.d.ts +32 -0
- package/dist/commands/topic.d.ts.map +1 -0
- package/dist/commands/topic.js +265 -0
- package/dist/commands/topic.js.map +1 -0
- package/dist/commands/topic.test.d.ts +2 -0
- package/dist/commands/topic.test.d.ts.map +1 -0
- package/dist/commands/topic.test.js +114 -0
- package/dist/commands/topic.test.js.map +1 -0
- package/dist/index.js +35 -144
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +10 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +104 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/client.test.d.ts +2 -0
- package/dist/lib/client.test.d.ts.map +1 -0
- package/dist/lib/client.test.js +44 -0
- package/dist/lib/client.test.js.map +1 -0
- package/dist/lib/config.d.ts +43 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +72 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/credentials.d.ts +3 -0
- package/dist/lib/credentials.d.ts.map +1 -1
- package/dist/lib/credentials.js +24 -2
- package/dist/lib/credentials.js.map +1 -1
- package/dist/lib/hubs.d.ts +7 -0
- package/dist/lib/hubs.d.ts.map +1 -0
- package/dist/lib/hubs.js +33 -0
- package/dist/lib/hubs.js.map +1 -0
- package/dist/lib/hubs.test.d.ts +2 -0
- package/dist/lib/hubs.test.d.ts.map +1 -0
- package/dist/lib/hubs.test.js +58 -0
- package/dist/lib/hubs.test.js.map +1 -0
- package/dist/lib/project-context.d.ts +56 -0
- package/dist/lib/project-context.d.ts.map +1 -0
- package/dist/lib/project-context.js +225 -0
- package/dist/lib/project-context.js.map +1 -0
- package/dist/lib/project-context.test.d.ts +2 -0
- package/dist/lib/project-context.test.d.ts.map +1 -0
- package/dist/lib/project-context.test.js +75 -0
- package/dist/lib/project-context.test.js.map +1 -0
- package/dist/lib/prompt.d.ts +7 -0
- package/dist/lib/prompt.d.ts.map +1 -0
- package/dist/lib/prompt.js +41 -0
- package/dist/lib/prompt.js.map +1 -0
- 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 +17 -13
- package/.vscode/mcp.json +0 -8
- package/dist/commands/sync.d.ts +0 -12
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js +0 -414
- package/dist/commands/sync.js.map +0 -1
- package/dist/lib/api.d.ts +0 -4
- package/dist/lib/api.d.ts.map +0 -1
- package/dist/lib/api.js +0 -95
- package/dist/lib/api.js.map +0 -1
- package/src/commands/auth.ts +0 -92
- package/src/commands/config.ts +0 -27
- package/src/commands/delete.ts +0 -58
- package/src/commands/hook.ts +0 -243
- package/src/commands/list.ts +0 -38
- package/src/commands/login.ts +0 -164
- package/src/commands/mcp.ts +0 -405
- package/src/commands/push.ts +0 -137
- package/src/commands/recall.ts +0 -163
- package/src/commands/setup.ts +0 -1075
- package/src/commands/show.ts +0 -35
- package/src/commands/sync.ts +0 -506
- package/src/index.ts +0 -213
- package/src/lib/api.ts +0 -110
- package/src/lib/config.ts +0 -61
- package/src/lib/credentials.ts +0 -42
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { getClient } from "../lib/client.js";
|
|
7
|
+
import { getProjectScope, resolveProjectScope, resolveClaudeProjectFolder, normalizeFilePath, readMemaxYmlConfig, isCanonicalProjectScope, } from "../lib/project-context.js";
|
|
8
|
+
import { getOrCreateDeviceID } from "../lib/config.js";
|
|
9
|
+
import { confirm, ask, confirmDefault } from "../lib/prompt.js";
|
|
10
|
+
import { moveFileToTrash } from "../lib/trash.js";
|
|
11
|
+
export async function syncAgentMemoryCommand(options = {}) {
|
|
12
|
+
await syncAgentMemory(options);
|
|
13
|
+
}
|
|
14
|
+
export async function listAgentConfigsCommand() {
|
|
15
|
+
let configs;
|
|
16
|
+
try {
|
|
17
|
+
const result = await getClient().configs.list();
|
|
18
|
+
configs = result.configs;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error(chalk.red(` Failed to fetch configs: ${err.message}\n`));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!configs || configs.length === 0) {
|
|
25
|
+
console.log(chalk.yellow(" No synced configs. Run: memax agents configs sync\n"));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Group by agent
|
|
29
|
+
const byAgent = new Map();
|
|
30
|
+
for (const c of configs) {
|
|
31
|
+
const list = byAgent.get(c.agent) ?? [];
|
|
32
|
+
list.push({
|
|
33
|
+
id: c.id,
|
|
34
|
+
filePath: c.file_path,
|
|
35
|
+
scope: c.scope,
|
|
36
|
+
updatedAt: c.updated_at,
|
|
37
|
+
});
|
|
38
|
+
byAgent.set(c.agent, list);
|
|
39
|
+
}
|
|
40
|
+
console.log();
|
|
41
|
+
for (const [agent, files] of byAgent) {
|
|
42
|
+
console.log(` ${chalk.cyan(agent)}`);
|
|
43
|
+
for (const f of files) {
|
|
44
|
+
const scopeTag = f.scope === "global"
|
|
45
|
+
? chalk.dim("global")
|
|
46
|
+
: chalk.dim(f.scope.replace("project:", ""));
|
|
47
|
+
const age = formatAge(f.updatedAt);
|
|
48
|
+
console.log(` ${f.filePath} ${scopeTag} ${chalk.dim(age)} ${chalk.dim(f.id.slice(0, 8))}`);
|
|
49
|
+
}
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.gray(` ${configs.length} config${configs.length > 1 ? "s" : ""} synced to cloud.\n`));
|
|
53
|
+
}
|
|
54
|
+
export async function listDeletedAgentConfigsCommand() {
|
|
55
|
+
let deleted;
|
|
56
|
+
try {
|
|
57
|
+
deleted = await getClient().configs.listDeleted();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(chalk.red(` Failed to fetch deleted configs: ${err.message}\n`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!deleted.configs || deleted.configs.length === 0) {
|
|
64
|
+
console.log(chalk.gray(" No recoverable deleted configs.\n"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk.bold("\n Recoverable Deleted Configs\n"));
|
|
68
|
+
deleted.configs.forEach((item, index) => {
|
|
69
|
+
const scopeLabel = item.scope === "global"
|
|
70
|
+
? chalk.dim("global")
|
|
71
|
+
: chalk.dim(item.scope.replace("project:", ""));
|
|
72
|
+
console.log(` ${chalk.bold(String(index + 1).padStart(2, " "))}. ${chalk.cyan(item.agent)} ${item.file_path} ${scopeLabel}`);
|
|
73
|
+
console.log(chalk.gray(` deleted ${formatAge(item.deleted_at)} · recoverable until ${item.content_expires_at ? new Date(item.content_expires_at).toLocaleString() : "expired"}`));
|
|
74
|
+
});
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
export async function restoreDeletedAgentConfigsCommand() {
|
|
78
|
+
let deleted;
|
|
79
|
+
try {
|
|
80
|
+
deleted = await getClient().configs.listDeleted();
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.error(chalk.red(` Failed to fetch deleted configs: ${err.message}\n`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!deleted.configs || deleted.configs.length === 0) {
|
|
87
|
+
console.log(chalk.gray(" No recoverable deleted configs.\n"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(chalk.bold("\n Recover Deleted Configs\n"));
|
|
91
|
+
deleted.configs.forEach((item, index) => {
|
|
92
|
+
const scopeLabel = item.scope === "global" ? "global" : item.scope.replace("project:", "");
|
|
93
|
+
console.log(` ${index + 1}. ${item.agent}/${item.file_path} ${chalk.gray(scopeLabel)}`);
|
|
94
|
+
});
|
|
95
|
+
const raw = await ask(chalk.gray("\n Enter indexes to restore (comma-separated): "));
|
|
96
|
+
const indexes = raw
|
|
97
|
+
.split(",")
|
|
98
|
+
.map((value) => Number.parseInt(value.trim(), 10))
|
|
99
|
+
.filter((value) => Number.isInteger(value) && value > 0 && value <= deleted.configs.length);
|
|
100
|
+
if (indexes.length === 0) {
|
|
101
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const cwd = process.cwd();
|
|
105
|
+
const deviceID = getOrCreateDeviceID();
|
|
106
|
+
const currentProjectScope = getProjectScope(cwd);
|
|
107
|
+
let restored = 0;
|
|
108
|
+
for (const index of indexes) {
|
|
109
|
+
const item = deleted.configs[index - 1];
|
|
110
|
+
try {
|
|
111
|
+
const writePath = resolveAgentConfigWritePath(item.agent, item.file_path, item.scope, {
|
|
112
|
+
cwd,
|
|
113
|
+
home: homedir(),
|
|
114
|
+
currentProjectScope,
|
|
115
|
+
findClaudeProjectDir,
|
|
116
|
+
});
|
|
117
|
+
const config = await getClient().configs.restore({
|
|
118
|
+
agent: item.agent,
|
|
119
|
+
file_path: item.file_path,
|
|
120
|
+
scope: item.scope,
|
|
121
|
+
device_id: deviceID,
|
|
122
|
+
local_path: writePath ?? undefined,
|
|
123
|
+
});
|
|
124
|
+
if (writePath && !existsSync(writePath)) {
|
|
125
|
+
mkdirSync(dirname(writePath), { recursive: true });
|
|
126
|
+
writeFileSync(writePath, config.content);
|
|
127
|
+
await getClient().configs.ack({
|
|
128
|
+
device_id: deviceID,
|
|
129
|
+
configs: [
|
|
130
|
+
{
|
|
131
|
+
agent: item.agent,
|
|
132
|
+
file_path: item.file_path,
|
|
133
|
+
scope: item.scope,
|
|
134
|
+
content_hash: config.content_hash,
|
|
135
|
+
version: config.version,
|
|
136
|
+
local_path: writePath,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
console.log(chalk.green(` ✓ ${item.file_path}`), chalk.gray("restored to cloud and local machine"));
|
|
141
|
+
}
|
|
142
|
+
else if (writePath && existsSync(writePath)) {
|
|
143
|
+
console.log(chalk.yellow(` - ${item.file_path}`), chalk.gray("restored to cloud; local file already exists"));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(chalk.yellow(` - ${item.file_path}`), chalk.gray("restored to cloud; no safe local path on this machine"));
|
|
147
|
+
}
|
|
148
|
+
restored++;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.log(chalk.red(` ✗ ${item.file_path}`), chalk.gray(err.message));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log(chalk.gray(`\n ${restored} config${restored === 1 ? "" : "s"} restored.\n`));
|
|
155
|
+
}
|
|
156
|
+
export function classifyAgentConfigPlacement(agent, filePath, scope, options = {}) {
|
|
157
|
+
const key = `${agent}|${normalizeFilePath(filePath)}|${scope}`;
|
|
158
|
+
const existing = options.localByKey?.get(key);
|
|
159
|
+
if (existing) {
|
|
160
|
+
return {
|
|
161
|
+
kind: "present",
|
|
162
|
+
path: existing.path,
|
|
163
|
+
reason: "present locally",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const cwd = options.cwd ?? process.cwd();
|
|
167
|
+
const currentProjectScope = options.currentProjectScope ?? getProjectScope(cwd);
|
|
168
|
+
if (scope.startsWith("project:") && scope !== currentProjectScope) {
|
|
169
|
+
return {
|
|
170
|
+
kind: "different_project",
|
|
171
|
+
reason: `belongs to ${scope.replace(/^project:/, "")}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const path = resolveAgentConfigWritePath(agent, filePath, scope, options);
|
|
175
|
+
if (path) {
|
|
176
|
+
return {
|
|
177
|
+
kind: "restorable",
|
|
178
|
+
path,
|
|
179
|
+
reason: "safe restore path available",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
kind: "unresolved",
|
|
184
|
+
reason: "no safe restore path on this machine",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export async function doctorAgentConfigsCommand() {
|
|
188
|
+
const cwd = process.cwd();
|
|
189
|
+
const home = homedir();
|
|
190
|
+
const deviceID = getOrCreateDeviceID();
|
|
191
|
+
const project = resolveProjectScope(cwd);
|
|
192
|
+
const memaxYml = readMemaxYmlConfig(cwd);
|
|
193
|
+
const locations = discoverAgentConfigs();
|
|
194
|
+
const localConfigs = locations.filter((loc) => {
|
|
195
|
+
if (!existsSync(loc.path))
|
|
196
|
+
return false;
|
|
197
|
+
try {
|
|
198
|
+
const stat = statSync(loc.path);
|
|
199
|
+
return stat.isFile() && stat.size > 0;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const localByKey = new Map();
|
|
206
|
+
for (const loc of localConfigs) {
|
|
207
|
+
localByKey.set(`${loc.agent}|${loc.filePath}|${loc.scope}`, loc);
|
|
208
|
+
}
|
|
209
|
+
let cloudConfigs = null;
|
|
210
|
+
try {
|
|
211
|
+
cloudConfigs = await getClient().configs.list();
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
console.error(chalk.red(` Failed to fetch cloud configs: ${err.message}\n`));
|
|
215
|
+
}
|
|
216
|
+
const scopeSource = project.source === "memax_yml"
|
|
217
|
+
? ".memax.yml project_id"
|
|
218
|
+
: project.source === "git_remote"
|
|
219
|
+
? "git origin"
|
|
220
|
+
: "no canonical project identity";
|
|
221
|
+
console.log(chalk.bold("\n Memax Agent Config Doctor\n"));
|
|
222
|
+
console.log(chalk.white(" Device"));
|
|
223
|
+
console.log(` ID ${chalk.bold(deviceID)}`);
|
|
224
|
+
console.log(` Home ${chalk.gray(home)}`);
|
|
225
|
+
console.log();
|
|
226
|
+
console.log(chalk.white(" Project"));
|
|
227
|
+
console.log(` CWD ${chalk.gray(cwd)}`);
|
|
228
|
+
console.log(` Scope ${chalk.bold(project.scope)}`);
|
|
229
|
+
console.log(` Source ${chalk.gray(scopeSource)}`);
|
|
230
|
+
if (memaxYml?.hub) {
|
|
231
|
+
console.log(` Hub ${chalk.gray(memaxYml.hub)}`);
|
|
232
|
+
}
|
|
233
|
+
if (memaxYml?.project_id) {
|
|
234
|
+
console.log(` project_id ${chalk.gray(memaxYml.project_id)}`);
|
|
235
|
+
}
|
|
236
|
+
if (project.warning) {
|
|
237
|
+
console.log(` Warning ${chalk.yellow(project.warning)}`);
|
|
238
|
+
}
|
|
239
|
+
if (project.scope === "project") {
|
|
240
|
+
console.log(` Note ${chalk.yellow("project-scoped cross-device restore is disabled until git origin or .memax.yml project_id is available")}`);
|
|
241
|
+
}
|
|
242
|
+
console.log();
|
|
243
|
+
const byAgent = new Map();
|
|
244
|
+
for (const loc of localConfigs) {
|
|
245
|
+
const group = byAgent.get(loc.agent) ?? [];
|
|
246
|
+
group.push(loc);
|
|
247
|
+
byAgent.set(loc.agent, group);
|
|
248
|
+
}
|
|
249
|
+
console.log(chalk.white(" Local Discovery"));
|
|
250
|
+
if (localConfigs.length === 0) {
|
|
251
|
+
console.log(` ${chalk.gray("No local agent configs discovered.")}`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
for (const [agent, group] of byAgent) {
|
|
255
|
+
console.log(` ${chalk.cyan(formatAgentName(agent))}`);
|
|
256
|
+
for (const loc of group) {
|
|
257
|
+
const scopeLabel = loc.scope === "global"
|
|
258
|
+
? "global"
|
|
259
|
+
: loc.scope.replace(/^project:/, "");
|
|
260
|
+
console.log(` • ${loc.filePath} ${chalk.gray(scopeLabel)} ${chalk.gray(loc.path)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
console.log();
|
|
265
|
+
if (!cloudConfigs) {
|
|
266
|
+
console.log(chalk.gray(" Cloud inspection skipped because cloud config fetch failed.\n"));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const placements = cloudConfigs.configs.map((config) => ({
|
|
270
|
+
config,
|
|
271
|
+
placement: classifyAgentConfigPlacement(config.agent, config.file_path, config.scope, {
|
|
272
|
+
cwd,
|
|
273
|
+
home,
|
|
274
|
+
currentProjectScope: project.scope,
|
|
275
|
+
localByKey,
|
|
276
|
+
findClaudeProjectDir,
|
|
277
|
+
}),
|
|
278
|
+
}));
|
|
279
|
+
const present = placements.filter((p) => p.placement.kind === "present");
|
|
280
|
+
const restorable = placements.filter((p) => p.placement.kind === "restorable");
|
|
281
|
+
const differentProject = placements.filter((p) => p.placement.kind === "different_project");
|
|
282
|
+
const unresolved = placements.filter((p) => p.placement.kind === "unresolved");
|
|
283
|
+
console.log(chalk.white(" Cloud Coverage"));
|
|
284
|
+
console.log(` Present locally ${chalk.bold(String(present.length))}`);
|
|
285
|
+
console.log(` Restorable here ${chalk.bold(String(restorable.length))}`);
|
|
286
|
+
console.log(` Other project ${chalk.bold(String(differentProject.length))}`);
|
|
287
|
+
console.log(` Unresolved here ${chalk.bold(String(unresolved.length))}`);
|
|
288
|
+
console.log();
|
|
289
|
+
printPlacementSection(" Restorable Here", chalk.cyan, restorable, (item) => [
|
|
290
|
+
item.config.agent,
|
|
291
|
+
item.config.file_path,
|
|
292
|
+
item.placement.path ?? "",
|
|
293
|
+
]);
|
|
294
|
+
printPlacementSection(" Different Project", chalk.yellow, differentProject, (item) => [item.config.agent, item.config.file_path, item.placement.reason]);
|
|
295
|
+
printPlacementSection(" Unresolved", chalk.magenta, unresolved, (item) => [
|
|
296
|
+
item.config.agent,
|
|
297
|
+
item.config.file_path,
|
|
298
|
+
item.placement.reason,
|
|
299
|
+
]);
|
|
300
|
+
console.log(chalk.gray(" Use this command to verify what sync can restore safely on this machine.\n"));
|
|
301
|
+
}
|
|
302
|
+
export function registerAgentConfigCommands(agentsCmd) {
|
|
303
|
+
const agentConfigsCmd = agentsCmd
|
|
304
|
+
.command("configs")
|
|
305
|
+
.description("Manage synced agent config files");
|
|
306
|
+
agentConfigsCmd
|
|
307
|
+
.command("sync")
|
|
308
|
+
.description("Sync agent config files bidirectionally with Memax cloud")
|
|
309
|
+
.option("--push", "Force push local configs to cloud (overwrite)")
|
|
310
|
+
.option("--pull", "Force pull cloud configs to local (overwrite)")
|
|
311
|
+
.action(syncAgentMemoryCommand);
|
|
312
|
+
agentConfigsCmd
|
|
313
|
+
.command("list")
|
|
314
|
+
.description("List all synced agent configs in the cloud")
|
|
315
|
+
.action(listAgentConfigsCommand);
|
|
316
|
+
agentConfigsCmd
|
|
317
|
+
.command("deleted")
|
|
318
|
+
.description("List recoverable deleted configs retained in cloud")
|
|
319
|
+
.action(listDeletedAgentConfigsCommand);
|
|
320
|
+
agentConfigsCmd
|
|
321
|
+
.command("restore")
|
|
322
|
+
.description("Restore deleted configs retained in cloud")
|
|
323
|
+
.action(restoreDeletedAgentConfigsCommand);
|
|
324
|
+
agentConfigsCmd
|
|
325
|
+
.command("delete")
|
|
326
|
+
.description("Interactively select and delete synced configs")
|
|
327
|
+
.action(deleteAgentConfigsCommand);
|
|
328
|
+
agentConfigsCmd
|
|
329
|
+
.command("doctor")
|
|
330
|
+
.description("Explain config sync identity, discovery, and safe restore behavior on this machine")
|
|
331
|
+
.action(doctorAgentConfigsCommand);
|
|
332
|
+
}
|
|
333
|
+
export async function deleteAgentConfigsCommand() {
|
|
334
|
+
let configs;
|
|
335
|
+
try {
|
|
336
|
+
const result = await getClient().configs.list();
|
|
337
|
+
configs = result.configs;
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
console.error(chalk.red(` Failed to fetch configs: ${err.message}\n`));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!configs || configs.length === 0) {
|
|
344
|
+
console.log(chalk.yellow(" No synced configs to delete.\n"));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
// Display numbered list grouped by agent
|
|
348
|
+
const items = [];
|
|
349
|
+
let currentAgent = "";
|
|
350
|
+
for (const c of configs) {
|
|
351
|
+
if (c.agent !== currentAgent) {
|
|
352
|
+
if (currentAgent)
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(` ${chalk.cyan(c.agent)}`);
|
|
355
|
+
currentAgent = c.agent;
|
|
356
|
+
}
|
|
357
|
+
items.push({
|
|
358
|
+
id: c.id,
|
|
359
|
+
agent: c.agent,
|
|
360
|
+
filePath: c.file_path,
|
|
361
|
+
scope: c.scope,
|
|
362
|
+
version: c.version,
|
|
363
|
+
});
|
|
364
|
+
const idx = chalk.dim(`${items.length}.`);
|
|
365
|
+
const scopeTag = c.scope === "global"
|
|
366
|
+
? chalk.dim("global")
|
|
367
|
+
: chalk.dim(c.scope.replace("project:", ""));
|
|
368
|
+
console.log(` ${idx} ${c.file_path} ${scopeTag}`);
|
|
369
|
+
}
|
|
370
|
+
console.log();
|
|
371
|
+
const answer = await ask(" Select configs to delete (comma-separated numbers, or 'q' to quit): ");
|
|
372
|
+
if (!answer || answer.trim().toLowerCase() === "q") {
|
|
373
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const indices = answer
|
|
377
|
+
.split(",")
|
|
378
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
379
|
+
.filter((n) => !isNaN(n) && n >= 1 && n <= items.length);
|
|
380
|
+
if (indices.length === 0) {
|
|
381
|
+
console.log(chalk.gray(" No valid selections.\n"));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const modeAnswer = await ask(" Delete from [l] this device only, [e] everywhere, or [s] skip? ");
|
|
385
|
+
const mode = modeAnswer.trim().toLowerCase();
|
|
386
|
+
if (mode !== "l" && mode !== "e") {
|
|
387
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
console.log();
|
|
391
|
+
for (const i of indices) {
|
|
392
|
+
const item = items[i - 1];
|
|
393
|
+
console.log(chalk.yellow(` ${item.agent}/${item.filePath}`));
|
|
394
|
+
}
|
|
395
|
+
const ok = await confirm(mode === "e"
|
|
396
|
+
? `\n Delete ${indices.length} config${indices.length > 1 ? "s" : ""} everywhere? (y/N) `
|
|
397
|
+
: `\n Remove ${indices.length} config${indices.length > 1 ? "s" : ""} from this device only? (y/N) `);
|
|
398
|
+
if (!ok) {
|
|
399
|
+
console.log(chalk.gray(" Cancelled.\n"));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const deviceID = getOrCreateDeviceID();
|
|
403
|
+
const cwd = process.cwd();
|
|
404
|
+
const currentProjectScope = getProjectScope(cwd);
|
|
405
|
+
const locations = discoverAgentConfigs();
|
|
406
|
+
const locByKey = new Map();
|
|
407
|
+
for (const loc of locations) {
|
|
408
|
+
locByKey.set(`${loc.agent}|${loc.filePath}|${loc.scope}`, loc);
|
|
409
|
+
}
|
|
410
|
+
const resolveDeletePath = (agent, filePath, scope) => {
|
|
411
|
+
const loc = locByKey.get(`${agent}|${filePath}|${scope}`);
|
|
412
|
+
if (loc)
|
|
413
|
+
return loc.path;
|
|
414
|
+
return resolveAgentConfigWritePath(agent, filePath, scope, {
|
|
415
|
+
cwd,
|
|
416
|
+
home: homedir(),
|
|
417
|
+
currentProjectScope,
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
let deleted = 0;
|
|
421
|
+
let failed = 0;
|
|
422
|
+
if (mode === "l") {
|
|
423
|
+
// Local-only path: per-item call is still correct because localDelete
|
|
424
|
+
// is a single-device sync-state write, not a cloud delete. No batch
|
|
425
|
+
// endpoint needed.
|
|
426
|
+
for (const i of indices) {
|
|
427
|
+
const item = items[i - 1];
|
|
428
|
+
try {
|
|
429
|
+
const localPath = resolveDeletePath(item.agent, item.filePath, item.scope);
|
|
430
|
+
await getClient().configs.localDelete({
|
|
431
|
+
device_id: deviceID,
|
|
432
|
+
agent: item.agent,
|
|
433
|
+
file_path: item.filePath,
|
|
434
|
+
scope: item.scope,
|
|
435
|
+
local_path: localPath ?? undefined,
|
|
436
|
+
});
|
|
437
|
+
if (localPath && existsSync(localPath)) {
|
|
438
|
+
moveFileToTrash(localPath, "agent-configs");
|
|
439
|
+
}
|
|
440
|
+
console.log(chalk.green(` \u2713 ${item.agent}/${item.filePath}`), chalk.gray("removed from this device (moved to Memax trash)"));
|
|
441
|
+
deleted++;
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
console.log(chalk.red(` \u2717 ${item.agent}/${item.filePath}`), chalk.gray(err.message));
|
|
445
|
+
failed++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Cloud-delete path: single batchDelete call commits what it can,
|
|
451
|
+
// then we run per-item local cleanup (trash + ack + print) only for
|
|
452
|
+
// ids the server actually removed.
|
|
453
|
+
//
|
|
454
|
+
// Classification rule (mirrors the algorithm in the B3 plan):
|
|
455
|
+
// not_found → server row already gone. Counts as committed for
|
|
456
|
+
// local cleanup: the user's target state is reached,
|
|
457
|
+
// so trash the local file and ack so the device
|
|
458
|
+
// manifest reflects the deletion.
|
|
459
|
+
// delete_failed → server row still present. Skip local cleanup
|
|
460
|
+
// (retry on next sync), surface a red line.
|
|
461
|
+
// success → standard committed path.
|
|
462
|
+
const requestedIDs = indices.map((i) => items[i - 1].id);
|
|
463
|
+
const itemByID = new Map(items.map((item) => [item.id, item]));
|
|
464
|
+
let result;
|
|
465
|
+
try {
|
|
466
|
+
result = await getClient().configs.batchDelete(requestedIDs);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
console.error(chalk.red(`\n Batch delete failed: ${err.message}\n`));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Index skipped ids by reason so the loop can branch per-item.
|
|
473
|
+
const skipReason = new Map();
|
|
474
|
+
for (const s of result.skipped) {
|
|
475
|
+
skipReason.set(s.id, s.reason);
|
|
476
|
+
}
|
|
477
|
+
for (const id of requestedIDs) {
|
|
478
|
+
const item = itemByID.get(id);
|
|
479
|
+
if (!item)
|
|
480
|
+
continue;
|
|
481
|
+
const reason = skipReason.get(id);
|
|
482
|
+
if (reason === "delete_failed") {
|
|
483
|
+
console.log(chalk.red(` \u2717 ${item.agent}/${item.filePath}`), chalk.gray("server delete failed (will retry on next sync)"));
|
|
484
|
+
failed++;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// Committed path: real success OR not_found (idempotent — row
|
|
488
|
+
// already gone on the server). Both paths trash the local file
|
|
489
|
+
// and ack so the device manifest records the deletion.
|
|
490
|
+
const localPath = resolveDeletePath(item.agent, item.filePath, item.scope);
|
|
491
|
+
try {
|
|
492
|
+
if (localPath && existsSync(localPath)) {
|
|
493
|
+
moveFileToTrash(localPath, "agent-configs");
|
|
494
|
+
}
|
|
495
|
+
await getClient().configs.ack({
|
|
496
|
+
device_id: deviceID,
|
|
497
|
+
configs: [
|
|
498
|
+
{
|
|
499
|
+
agent: item.agent,
|
|
500
|
+
file_path: item.filePath,
|
|
501
|
+
scope: item.scope,
|
|
502
|
+
version: item.version + 1,
|
|
503
|
+
local_path: localPath ?? undefined,
|
|
504
|
+
deleted: true,
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
});
|
|
508
|
+
deleted++;
|
|
509
|
+
console.log(chalk.green(` \u2713 ${item.agent}/${item.filePath}`), chalk.gray(reason === "not_found"
|
|
510
|
+
? "already deleted on server (local copy moved to Memax trash)"
|
|
511
|
+
: "deleted everywhere (local copy moved to Memax trash)"));
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
// Local cleanup or ack failed — the server row is already
|
|
515
|
+
// gone, so count as deleted for the summary but warn the
|
|
516
|
+
// user their local state may be inconsistent.
|
|
517
|
+
deleted++;
|
|
518
|
+
console.log(chalk.yellow(` ! ${item.agent}/${item.filePath}`), chalk.gray(`server deleted but local cleanup failed: ${err.message}`));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const summary = mode === "e"
|
|
523
|
+
? `\n ${deleted} config${deleted === 1 ? "" : "s"} deleted everywhere${failed > 0 ? `, ${failed} failed` : ""}.\n`
|
|
524
|
+
: `\n ${deleted} config${deleted === 1 ? "" : "s"} removed from this device${failed > 0 ? `, ${failed} failed` : ""}.\n`;
|
|
525
|
+
console.log(chalk.gray(summary));
|
|
526
|
+
}
|
|
527
|
+
function formatAge(dateStr) {
|
|
528
|
+
const ms = Date.now() - new Date(dateStr).getTime();
|
|
529
|
+
const mins = Math.floor(ms / 60000);
|
|
530
|
+
if (mins < 60)
|
|
531
|
+
return `${mins}m ago`;
|
|
532
|
+
const hours = Math.floor(mins / 60);
|
|
533
|
+
if (hours < 24)
|
|
534
|
+
return `${hours}h ago`;
|
|
535
|
+
const days = Math.floor(hours / 24);
|
|
536
|
+
return `${days}d ago`;
|
|
537
|
+
}
|
|
538
|
+
function findClaudeProjectDir(scope) {
|
|
539
|
+
const home = homedir();
|
|
540
|
+
const claudeProjectsDir = join(home, ".claude", "projects");
|
|
541
|
+
if (!existsSync(claudeProjectsDir))
|
|
542
|
+
return null;
|
|
543
|
+
try {
|
|
544
|
+
for (const project of readdirSync(claudeProjectsDir)) {
|
|
545
|
+
const repoUrl = resolveClaudeProjectFolder(project);
|
|
546
|
+
if (repoUrl && scope === `project:${repoUrl}`) {
|
|
547
|
+
return join(claudeProjectsDir, project);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// Permission denied — skip
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
function printPlacementSection(title, color, items, format) {
|
|
557
|
+
if (items.length === 0)
|
|
558
|
+
return;
|
|
559
|
+
console.log(chalk.white(title));
|
|
560
|
+
for (const item of items) {
|
|
561
|
+
const [agent, filePath, detail] = format(item);
|
|
562
|
+
console.log(` ${color(formatAgentName(agent))} ${filePath}`);
|
|
563
|
+
console.log(` ${chalk.gray(detail)}`);
|
|
564
|
+
}
|
|
565
|
+
console.log();
|
|
566
|
+
}
|
|
567
|
+
export function resolveAgentConfigWritePath(agent, filePath, scope, options = {}) {
|
|
568
|
+
const cwd = options.cwd ?? process.cwd();
|
|
569
|
+
const home = options.home ?? homedir();
|
|
570
|
+
const currentProjectScope = options.currentProjectScope ?? getProjectScope(cwd);
|
|
571
|
+
const normalizedFilePath = normalizeFilePath(filePath);
|
|
572
|
+
if (scope === "global") {
|
|
573
|
+
switch (agent) {
|
|
574
|
+
case "claude-code":
|
|
575
|
+
return join(home, ".claude", normalizedFilePath);
|
|
576
|
+
case "codex":
|
|
577
|
+
return join(home, ".codex", normalizedFilePath);
|
|
578
|
+
case "gemini":
|
|
579
|
+
return join(home, ".gemini", normalizedFilePath);
|
|
580
|
+
case "openclaw":
|
|
581
|
+
return join(home, ".openclaw", normalizedFilePath);
|
|
582
|
+
case "opencode":
|
|
583
|
+
return join(home, ".opencode", normalizedFilePath);
|
|
584
|
+
default:
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (!scope.startsWith("project:") || scope !== currentProjectScope) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
switch (agent) {
|
|
592
|
+
case "claude-code":
|
|
593
|
+
if (normalizedFilePath === "CLAUDE.md" ||
|
|
594
|
+
normalizedFilePath === "MEMORY.md") {
|
|
595
|
+
return join(cwd, ".claude", normalizedFilePath);
|
|
596
|
+
}
|
|
597
|
+
if (normalizedFilePath.startsWith(".claude/")) {
|
|
598
|
+
return join(cwd, normalizedFilePath);
|
|
599
|
+
}
|
|
600
|
+
if (normalizedFilePath.startsWith("memory/")) {
|
|
601
|
+
const projectDir = options.findClaudeProjectDir?.(scope);
|
|
602
|
+
if (projectDir) {
|
|
603
|
+
return join(projectDir, normalizedFilePath);
|
|
604
|
+
}
|
|
605
|
+
const mangledCwd = cwd.replace(/\//g, "-");
|
|
606
|
+
return join(home, ".claude", "projects", mangledCwd, normalizedFilePath);
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
case "cursor":
|
|
610
|
+
if (normalizedFilePath === ".cursorrules" ||
|
|
611
|
+
normalizedFilePath.startsWith(".cursor/")) {
|
|
612
|
+
return join(cwd, normalizedFilePath);
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
case "codex":
|
|
616
|
+
if (normalizedFilePath === "instructions.md") {
|
|
617
|
+
return join(cwd, ".codex", "instructions.md");
|
|
618
|
+
}
|
|
619
|
+
if (normalizedFilePath.startsWith(".codex/")) {
|
|
620
|
+
return join(cwd, normalizedFilePath);
|
|
621
|
+
}
|
|
622
|
+
return null;
|
|
623
|
+
case "gemini":
|
|
624
|
+
if (normalizedFilePath === "GEMINI.md") {
|
|
625
|
+
return join(cwd, "GEMINI.md");
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
case "copilot":
|
|
629
|
+
if (normalizedFilePath === "copilot-instructions.md") {
|
|
630
|
+
return join(cwd, ".github", "copilot-instructions.md");
|
|
631
|
+
}
|
|
632
|
+
if (normalizedFilePath.startsWith(".github/")) {
|
|
633
|
+
return join(cwd, normalizedFilePath);
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
case "windsurf":
|
|
637
|
+
if (normalizedFilePath === ".windsurfrules" ||
|
|
638
|
+
normalizedFilePath.startsWith(".windsurf/")) {
|
|
639
|
+
return join(cwd, normalizedFilePath);
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
case "opencode":
|
|
643
|
+
if (normalizedFilePath.startsWith(".opencode/")) {
|
|
644
|
+
return join(cwd, normalizedFilePath);
|
|
645
|
+
}
|
|
646
|
+
return join(cwd, ".opencode", normalizedFilePath);
|
|
647
|
+
case "generic":
|
|
648
|
+
if (normalizedFilePath === "AGENTS.md" ||
|
|
649
|
+
normalizedFilePath === "CLAUDE.md" ||
|
|
650
|
+
normalizedFilePath === "GEMINI.md") {
|
|
651
|
+
return join(cwd, normalizedFilePath);
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
default:
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function discoverAgentConfigs() {
|
|
659
|
+
const home = homedir();
|
|
660
|
+
const cwd = process.cwd();
|
|
661
|
+
const projectScope = getProjectScope(cwd);
|
|
662
|
+
const canonicalProjectScope = isCanonicalProjectScope(projectScope)
|
|
663
|
+
? projectScope
|
|
664
|
+
: null;
|
|
665
|
+
const locations = [];
|
|
666
|
+
const add = (agent, label, path, filePath, scope = "global") => locations.push({
|
|
667
|
+
agent,
|
|
668
|
+
label,
|
|
669
|
+
path,
|
|
670
|
+
filePath: normalizeFilePath(filePath),
|
|
671
|
+
scope,
|
|
672
|
+
});
|
|
673
|
+
// Claude Code — global
|
|
674
|
+
add("claude-code", "~/.claude/CLAUDE.md", join(home, ".claude", "CLAUDE.md"), "CLAUDE.md");
|
|
675
|
+
add("claude-code", "~/.claude/MEMORY.md", join(home, ".claude", "MEMORY.md"), "MEMORY.md");
|
|
676
|
+
// Claude Code — per-project memories: ~/.claude/projects/*/memory/*.md
|
|
677
|
+
// The folder name is the absolute project path with "/" replaced by "-"
|
|
678
|
+
// (e.g., "-workspaces-memax"). We resolve it to a git repo URL so the
|
|
679
|
+
// same project's memories match across machines regardless of clone path.
|
|
680
|
+
const claudeProjectsDir = join(home, ".claude", "projects");
|
|
681
|
+
if (existsSync(claudeProjectsDir)) {
|
|
682
|
+
try {
|
|
683
|
+
for (const project of readdirSync(claudeProjectsDir)) {
|
|
684
|
+
const memoryDir = join(claudeProjectsDir, project, "memory");
|
|
685
|
+
if (!existsSync(memoryDir))
|
|
686
|
+
continue;
|
|
687
|
+
// Try to resolve mangled folder → git repo → canonical scope
|
|
688
|
+
const repoUrl = resolveClaudeProjectFolder(project);
|
|
689
|
+
const memoryScope = repoUrl
|
|
690
|
+
? `project:${repoUrl}`
|
|
691
|
+
: undefined;
|
|
692
|
+
try {
|
|
693
|
+
for (const file of readdirSync(memoryDir)) {
|
|
694
|
+
if (!file.endsWith(".md"))
|
|
695
|
+
continue;
|
|
696
|
+
if (memoryScope) {
|
|
697
|
+
// Canonical: filePath is just "memory/<file>", scope identifies the project
|
|
698
|
+
add("claude-code", `~/.claude/projects/${project}/memory/${file}`, join(memoryDir, file), `memory/${file}`, memoryScope);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
// Fallback: can't resolve project → keep legacy format with folder name
|
|
702
|
+
add("claude-code", `~/.claude/projects/${project}/memory/${file}`, join(memoryDir, file), `projects/${project}/memory/${file}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// Permission denied — skip
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// Permission denied — skip
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// --- Project-scoped configs (only when inside a git repo) ---
|
|
716
|
+
if (canonicalProjectScope) {
|
|
717
|
+
// Claude Code — project-level
|
|
718
|
+
add("claude-code", "./.claude/CLAUDE.md", join(cwd, ".claude", "CLAUDE.md"), "CLAUDE.md", canonicalProjectScope);
|
|
719
|
+
// Cursor (project-level)
|
|
720
|
+
add("cursor", "./.cursorrules", join(cwd, ".cursorrules"), ".cursorrules", canonicalProjectScope);
|
|
721
|
+
const cursorRulesDir = join(cwd, ".cursor", "rules");
|
|
722
|
+
if (existsSync(cursorRulesDir)) {
|
|
723
|
+
try {
|
|
724
|
+
for (const file of readdirSync(cursorRulesDir)) {
|
|
725
|
+
if (file.endsWith(".mdc")) {
|
|
726
|
+
add("cursor", `./.cursor/rules/${file}`, join(cursorRulesDir, file), `.cursor/rules/${file}`, canonicalProjectScope);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
/* skip */
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Codex (project-level)
|
|
735
|
+
add("codex", "./.codex/instructions.md", join(cwd, ".codex", "instructions.md"), "instructions.md", canonicalProjectScope);
|
|
736
|
+
}
|
|
737
|
+
add("codex", "~/.codex/AGENTS.md", join(home, ".codex", "AGENTS.md"), "AGENTS.md");
|
|
738
|
+
// Gemini CLI — global
|
|
739
|
+
add("gemini", "~/.gemini/GEMINI.md", join(home, ".gemini", "GEMINI.md"), "GEMINI.md");
|
|
740
|
+
if (canonicalProjectScope) {
|
|
741
|
+
// Gemini CLI — project-level
|
|
742
|
+
add("gemini", "./GEMINI.md", join(cwd, "GEMINI.md"), "GEMINI.md", canonicalProjectScope);
|
|
743
|
+
// GitHub Copilot
|
|
744
|
+
add("copilot", "./.github/copilot-instructions.md", join(cwd, ".github", "copilot-instructions.md"), "copilot-instructions.md", canonicalProjectScope);
|
|
745
|
+
// Windsurf
|
|
746
|
+
add("windsurf", "./.windsurfrules", join(cwd, ".windsurfrules"), ".windsurfrules", canonicalProjectScope);
|
|
747
|
+
const windsurfRulesDir = join(cwd, ".windsurf", "rules");
|
|
748
|
+
if (existsSync(windsurfRulesDir)) {
|
|
749
|
+
try {
|
|
750
|
+
for (const file of readdirSync(windsurfRulesDir)) {
|
|
751
|
+
if (file.endsWith(".md")) {
|
|
752
|
+
add("windsurf", `./.windsurf/rules/${file}`, join(windsurfRulesDir, file), `.windsurf/rules/${file}`, canonicalProjectScope);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
/* skip */
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// OpenClaw
|
|
762
|
+
const openclawMemoryDir = join(home, ".openclaw", "memory");
|
|
763
|
+
if (existsSync(openclawMemoryDir)) {
|
|
764
|
+
try {
|
|
765
|
+
for (const file of readdirSync(openclawMemoryDir)) {
|
|
766
|
+
if (file.endsWith(".md") || file.endsWith(".json")) {
|
|
767
|
+
add("openclaw", `~/.openclaw/memory/${file}`, join(openclawMemoryDir, file), `memory/${file}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
/* skip */
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (canonicalProjectScope) {
|
|
776
|
+
// OpenCode (project-level)
|
|
777
|
+
const opencodePath = join(cwd, ".opencode");
|
|
778
|
+
if (existsSync(opencodePath)) {
|
|
779
|
+
try {
|
|
780
|
+
for (const file of readdirSync(opencodePath)) {
|
|
781
|
+
if (file.endsWith(".md")) {
|
|
782
|
+
add("opencode", `./.opencode/${file}`, join(opencodePath, file), file, canonicalProjectScope);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
/* skip */
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Generic project-level agent files
|
|
791
|
+
add("generic", "./AGENTS.md", join(cwd, "AGENTS.md"), "AGENTS.md", canonicalProjectScope);
|
|
792
|
+
add("generic", "./CLAUDE.md", join(cwd, "CLAUDE.md"), "CLAUDE.md", canonicalProjectScope);
|
|
793
|
+
}
|
|
794
|
+
return locations;
|
|
795
|
+
}
|
|
796
|
+
async function syncAgentMemory(options = {}) {
|
|
797
|
+
console.log(chalk.bold("\n Memax Config Sync\n"));
|
|
798
|
+
const projectScopeResolution = resolveProjectScope();
|
|
799
|
+
if (projectScopeResolution.warning) {
|
|
800
|
+
console.log(chalk.yellow(` Warning: ${projectScopeResolution.warning}`));
|
|
801
|
+
console.log(chalk.gray(" Using .memax.yml project_id as the canonical project identity.\n"));
|
|
802
|
+
}
|
|
803
|
+
// Discover local config files
|
|
804
|
+
const locations = discoverAgentConfigs();
|
|
805
|
+
const localConfigs = [];
|
|
806
|
+
for (const loc of locations) {
|
|
807
|
+
if (!existsSync(loc.path))
|
|
808
|
+
continue;
|
|
809
|
+
try {
|
|
810
|
+
const stat = statSync(loc.path);
|
|
811
|
+
if (!stat.isFile() || stat.size === 0)
|
|
812
|
+
continue;
|
|
813
|
+
const content = readFileSync(loc.path, "utf-8");
|
|
814
|
+
if (!content.trim())
|
|
815
|
+
continue;
|
|
816
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
817
|
+
localConfigs.push({
|
|
818
|
+
loc,
|
|
819
|
+
content,
|
|
820
|
+
hash,
|
|
821
|
+
updatedAt: stat.mtime.toISOString(),
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
// Skip unreadable files
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const isBootstrap = localConfigs.length === 0;
|
|
829
|
+
const deviceID = getOrCreateDeviceID();
|
|
830
|
+
if (isBootstrap) {
|
|
831
|
+
console.log(chalk.gray(" No local agent configs found. Checking cloud for backups...\n"));
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
console.log(chalk.gray(` Found ${localConfigs.length} local config${localConfigs.length > 1 ? "s" : ""}. Syncing with cloud...\n`));
|
|
835
|
+
}
|
|
836
|
+
// Build manifest — may be empty on a new device (that's fine, we'll pull from cloud)
|
|
837
|
+
const manifest = localConfigs.map((c) => ({
|
|
838
|
+
agent: c.loc.agent,
|
|
839
|
+
file_path: c.loc.filePath,
|
|
840
|
+
scope: c.loc.scope,
|
|
841
|
+
content_hash: c.hash,
|
|
842
|
+
updated_at: c.updatedAt,
|
|
843
|
+
local_path: c.loc.path,
|
|
844
|
+
}));
|
|
845
|
+
let actions;
|
|
846
|
+
try {
|
|
847
|
+
const plan = await getClient().configs.sync({
|
|
848
|
+
device_id: deviceID,
|
|
849
|
+
configs: manifest,
|
|
850
|
+
});
|
|
851
|
+
actions = plan.actions;
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
console.error(chalk.red(` Sync failed: ${err.message}\n`));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
// Force modes: resolve ALL ambiguous actions in one direction.
|
|
858
|
+
// This includes both "conflict" AND "delete_local" (tombstone-driven
|
|
859
|
+
// deletions where the server decided local should be removed).
|
|
860
|
+
// One-sided actions (local_only push, cloud_only pull) stay as-is.
|
|
861
|
+
if (options.push) {
|
|
862
|
+
actions = actions.map((a) => {
|
|
863
|
+
if (a.action === "conflict")
|
|
864
|
+
return { ...a, action: "push" };
|
|
865
|
+
if (a.action === "delete_local")
|
|
866
|
+
return { ...a, action: "push" };
|
|
867
|
+
return a;
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
else if (options.pull) {
|
|
871
|
+
actions = actions.map((a) => {
|
|
872
|
+
if (a.action !== "conflict")
|
|
873
|
+
return a;
|
|
874
|
+
if (a.config_id)
|
|
875
|
+
return { ...a, action: "pull" };
|
|
876
|
+
return { ...a, action: "delete_local" };
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
// Skip conflicts mode (used by setup): only process one-sided actions
|
|
880
|
+
// (local_only push, cloud_only pull), skip all ambiguous/destructive actions.
|
|
881
|
+
if (options.skipConflicts) {
|
|
882
|
+
const skippedCount = actions.filter((a) => a.action === "conflict" || a.action === "delete_local").length;
|
|
883
|
+
actions = actions.filter((a) => a.action !== "conflict" && a.action !== "delete_local");
|
|
884
|
+
if (skippedCount > 0) {
|
|
885
|
+
console.log(chalk.gray(` ${skippedCount} conflict${skippedCount > 1 ? "s" : ""} skipped (resolve with: memax agents sync)\n`));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Filter out project-scoped cloud-only configs that don't belong to the
|
|
889
|
+
// current project. Without this, running `memax agents sync` from ~/
|
|
890
|
+
// would dump project configs (like .cursorrules from repo X) into the
|
|
891
|
+
// home directory.
|
|
892
|
+
const currentProjectScope = projectScopeResolution.scope;
|
|
893
|
+
actions = actions.filter((a) => {
|
|
894
|
+
if (!a.scope.startsWith("project:"))
|
|
895
|
+
return true; // global → always sync
|
|
896
|
+
// Project-scoped: only sync if it matches the current project
|
|
897
|
+
if (a.scope === currentProjectScope)
|
|
898
|
+
return true;
|
|
899
|
+
// Cloud-only configs for other projects → skip silently
|
|
900
|
+
if (a.action === "pull" && a.reason === "cloud_only")
|
|
901
|
+
return false;
|
|
902
|
+
// Conflict/push for configs we have locally → keep (user is in the project)
|
|
903
|
+
return true;
|
|
904
|
+
});
|
|
905
|
+
// Index local configs by (agent, file_path, scope) for quick lookup
|
|
906
|
+
const localByKey = new Map();
|
|
907
|
+
for (const c of localConfigs) {
|
|
908
|
+
localByKey.set(`${c.loc.agent}|${c.loc.filePath}|${c.loc.scope}`, c);
|
|
909
|
+
}
|
|
910
|
+
// Index locations by (agent, file_path, scope) for pull path resolution
|
|
911
|
+
const locByKey = new Map();
|
|
912
|
+
for (const loc of locations) {
|
|
913
|
+
locByKey.set(`${loc.agent}|${loc.filePath}|${loc.scope}`, loc);
|
|
914
|
+
}
|
|
915
|
+
// Resolve a local write path for any config — even ones not discovered locally.
|
|
916
|
+
// This enables pulling configs to a brand-new device where agent dirs don't exist yet.
|
|
917
|
+
const resolveWritePath = (agent, filePath, scope) => {
|
|
918
|
+
const loc = locByKey.get(`${agent}|${filePath}|${scope}`);
|
|
919
|
+
if (loc)
|
|
920
|
+
return loc.path;
|
|
921
|
+
return resolveAgentConfigWritePath(agent, filePath, scope, {
|
|
922
|
+
cwd: process.cwd(),
|
|
923
|
+
home: homedir(),
|
|
924
|
+
currentProjectScope,
|
|
925
|
+
findClaudeProjectDir,
|
|
926
|
+
});
|
|
927
|
+
};
|
|
928
|
+
// Execute sync plan
|
|
929
|
+
let pushed = 0;
|
|
930
|
+
let pulled = 0;
|
|
931
|
+
let deletedLocal = 0;
|
|
932
|
+
let unchangedCount = 0;
|
|
933
|
+
let skipped = 0;
|
|
934
|
+
let errors = 0;
|
|
935
|
+
const ackConfigs = [];
|
|
936
|
+
// Group actions by agent for display
|
|
937
|
+
const byAgent = new Map();
|
|
938
|
+
for (const action of actions) {
|
|
939
|
+
const group = byAgent.get(action.agent) ?? [];
|
|
940
|
+
group.push(action);
|
|
941
|
+
byAgent.set(action.agent, group);
|
|
942
|
+
}
|
|
943
|
+
for (const [agent, agentActions] of byAgent) {
|
|
944
|
+
console.log(chalk.white(` ${formatAgentName(agent)}`));
|
|
945
|
+
for (const action of agentActions) {
|
|
946
|
+
const key = `${action.agent}|${action.file_path}|${action.scope}`;
|
|
947
|
+
if (action.action === "unchanged") {
|
|
948
|
+
const local = localByKey.get(key);
|
|
949
|
+
console.log(chalk.gray(` = ${action.file_path}`), chalk.gray("unchanged"));
|
|
950
|
+
if (local && action.version) {
|
|
951
|
+
ackConfigs.push({
|
|
952
|
+
agent: action.agent,
|
|
953
|
+
file_path: action.file_path,
|
|
954
|
+
scope: action.scope,
|
|
955
|
+
content_hash: local.hash,
|
|
956
|
+
version: action.version,
|
|
957
|
+
local_path: local.loc.path,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
unchangedCount++;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (action.action === "push") {
|
|
964
|
+
const local = localByKey.get(key);
|
|
965
|
+
if (!local) {
|
|
966
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray("local file not found for push"));
|
|
967
|
+
errors++;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
await getClient().configs.upsert({
|
|
972
|
+
agent: action.agent,
|
|
973
|
+
file_path: action.file_path,
|
|
974
|
+
scope: action.scope,
|
|
975
|
+
content: local.content,
|
|
976
|
+
device_id: deviceID,
|
|
977
|
+
local_path: local.loc.path,
|
|
978
|
+
});
|
|
979
|
+
console.log(chalk.green(` \u2191 ${action.file_path}`), chalk.gray(action.reason === "local_only"
|
|
980
|
+
? "pushing (new)"
|
|
981
|
+
: "pushing (local newer)"));
|
|
982
|
+
pushed++;
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
|
|
986
|
+
errors++;
|
|
987
|
+
}
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (action.action === "pull") {
|
|
991
|
+
if (!action.config_id) {
|
|
992
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray("missing config ID from server"));
|
|
993
|
+
errors++;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
998
|
+
if (!writePath) {
|
|
999
|
+
console.log(chalk.yellow(` ? ${action.file_path}`), chalk.gray(action.scope !== "global" &&
|
|
1000
|
+
action.scope !== currentProjectScope
|
|
1001
|
+
? "different project \u2014 skipped"
|
|
1002
|
+
: "unknown agent \u2014 skipped"));
|
|
1003
|
+
skipped++;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
// For new files (not updates), ask user before writing
|
|
1007
|
+
const isNewLocally = action.reason === "cloud_only" && !existsSync(writePath);
|
|
1008
|
+
if (isNewLocally && !options.pull) {
|
|
1009
|
+
console.log(chalk.cyan(` New file: ${action.file_path}`));
|
|
1010
|
+
console.log(chalk.gray(` → ${writePath}`));
|
|
1011
|
+
const accept = await confirmDefault(` Download? [Y/n] `);
|
|
1012
|
+
if (!accept) {
|
|
1013
|
+
console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
|
|
1014
|
+
skipped++;
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const config = await getClient().configs.get(action.config_id);
|
|
1019
|
+
mkdirSync(dirname(writePath), { recursive: true });
|
|
1020
|
+
writeFileSync(writePath, config.content);
|
|
1021
|
+
if (action.version) {
|
|
1022
|
+
ackConfigs.push({
|
|
1023
|
+
agent: action.agent,
|
|
1024
|
+
file_path: action.file_path,
|
|
1025
|
+
scope: action.scope,
|
|
1026
|
+
content_hash: config.content_hash,
|
|
1027
|
+
version: action.version,
|
|
1028
|
+
local_path: writePath,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
console.log(chalk.cyan(` \u2193 ${action.file_path}`), chalk.gray(isNewLocally ? "restored" : "pulling (cloud newer)"));
|
|
1032
|
+
pulled++;
|
|
1033
|
+
}
|
|
1034
|
+
catch (err) {
|
|
1035
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
|
|
1036
|
+
errors++;
|
|
1037
|
+
}
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (action.action === "delete_local") {
|
|
1041
|
+
if (!options.pull) {
|
|
1042
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1043
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("cloud deleted this config \u2014 skipped in non-interactive mode"));
|
|
1044
|
+
skipped++;
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const resolution = await promptCloudDeletion(action.file_path);
|
|
1048
|
+
if (resolution === "skip") {
|
|
1049
|
+
console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
|
|
1050
|
+
skipped++;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (resolution === "local") {
|
|
1054
|
+
const local = localByKey.get(key);
|
|
1055
|
+
if (!local) {
|
|
1056
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("local file missing \u2014 skipped"));
|
|
1057
|
+
skipped++;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
try {
|
|
1061
|
+
await getClient().configs.upsert({
|
|
1062
|
+
agent: action.agent,
|
|
1063
|
+
file_path: action.file_path,
|
|
1064
|
+
scope: action.scope,
|
|
1065
|
+
content: local.content,
|
|
1066
|
+
device_id: deviceID,
|
|
1067
|
+
local_path: local.loc.path,
|
|
1068
|
+
});
|
|
1069
|
+
console.log(chalk.green(` \u2191 ${action.file_path}`), chalk.gray("kept local and restored to cloud"));
|
|
1070
|
+
pushed++;
|
|
1071
|
+
}
|
|
1072
|
+
catch (err) {
|
|
1073
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
|
|
1074
|
+
errors++;
|
|
1075
|
+
}
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
1081
|
+
if (writePath && existsSync(writePath)) {
|
|
1082
|
+
moveFileToTrash(writePath, "agent-configs");
|
|
1083
|
+
}
|
|
1084
|
+
if (action.version) {
|
|
1085
|
+
ackConfigs.push({
|
|
1086
|
+
agent: action.agent,
|
|
1087
|
+
file_path: action.file_path,
|
|
1088
|
+
scope: action.scope,
|
|
1089
|
+
version: action.version,
|
|
1090
|
+
local_path: writePath ?? undefined,
|
|
1091
|
+
deleted: true,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("deleted locally (moved to Memax trash)"));
|
|
1095
|
+
deletedLocal++;
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
|
|
1099
|
+
errors++;
|
|
1100
|
+
}
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (action.action === "conflict") {
|
|
1104
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1105
|
+
console.log(chalk.yellow(` - ${action.file_path}`), chalk.gray("conflict skipped in non-interactive mode"));
|
|
1106
|
+
skipped++;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const resolution = await promptConflict(agent, action.file_path);
|
|
1110
|
+
if (resolution === "local") {
|
|
1111
|
+
const local = localByKey.get(key);
|
|
1112
|
+
if (local) {
|
|
1113
|
+
try {
|
|
1114
|
+
await getClient().configs.upsert({
|
|
1115
|
+
agent: action.agent,
|
|
1116
|
+
file_path: action.file_path,
|
|
1117
|
+
scope: action.scope,
|
|
1118
|
+
content: local.content,
|
|
1119
|
+
device_id: deviceID,
|
|
1120
|
+
local_path: local.loc.path,
|
|
1121
|
+
});
|
|
1122
|
+
console.log(chalk.green(` \u2191 ${action.file_path}`), chalk.gray("kept local"));
|
|
1123
|
+
pushed++;
|
|
1124
|
+
}
|
|
1125
|
+
catch (err) {
|
|
1126
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
|
|
1127
|
+
errors++;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
else if (resolution === "cloud" && action.config_id) {
|
|
1132
|
+
try {
|
|
1133
|
+
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
1134
|
+
if (writePath) {
|
|
1135
|
+
const config = await getClient().configs.get(action.config_id);
|
|
1136
|
+
mkdirSync(dirname(writePath), { recursive: true });
|
|
1137
|
+
writeFileSync(writePath, config.content);
|
|
1138
|
+
if (action.version) {
|
|
1139
|
+
ackConfigs.push({
|
|
1140
|
+
agent: action.agent,
|
|
1141
|
+
file_path: action.file_path,
|
|
1142
|
+
scope: action.scope,
|
|
1143
|
+
content_hash: config.content_hash,
|
|
1144
|
+
version: action.version,
|
|
1145
|
+
local_path: writePath,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
console.log(chalk.cyan(` \u2193 ${action.file_path}`), chalk.gray("used cloud"));
|
|
1149
|
+
pulled++;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
catch (err) {
|
|
1153
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
|
|
1154
|
+
errors++;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
else if (resolution === "merge" && action.config_id) {
|
|
1158
|
+
const local = localByKey.get(key);
|
|
1159
|
+
if (local) {
|
|
1160
|
+
try {
|
|
1161
|
+
const cloudConfig = await getClient().configs.get(action.config_id);
|
|
1162
|
+
console.log(chalk.gray(` Merging with LLM...`));
|
|
1163
|
+
const merged = await mergeConfigs(action.agent, action.file_path, local.content, cloudConfig.content);
|
|
1164
|
+
if (merged) {
|
|
1165
|
+
// Write merged to local
|
|
1166
|
+
const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
|
|
1167
|
+
if (writePath) {
|
|
1168
|
+
mkdirSync(dirname(writePath), { recursive: true });
|
|
1169
|
+
writeFileSync(writePath, merged);
|
|
1170
|
+
}
|
|
1171
|
+
// Push merged to cloud
|
|
1172
|
+
await getClient().configs.upsert({
|
|
1173
|
+
agent: action.agent,
|
|
1174
|
+
file_path: action.file_path,
|
|
1175
|
+
scope: action.scope,
|
|
1176
|
+
content: merged,
|
|
1177
|
+
device_id: deviceID,
|
|
1178
|
+
local_path: writePath ?? undefined,
|
|
1179
|
+
});
|
|
1180
|
+
console.log(chalk.magenta(` \u2194 ${action.file_path}`), chalk.gray("merged (LLM)"));
|
|
1181
|
+
pushed++;
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
skipped++;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
|
|
1189
|
+
errors++;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
|
|
1195
|
+
skipped++;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (ackConfigs.length > 0) {
|
|
1201
|
+
try {
|
|
1202
|
+
await getClient().configs.ack({
|
|
1203
|
+
device_id: deviceID,
|
|
1204
|
+
configs: ackConfigs,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
console.log(chalk.yellow("\n Warning: failed to persist sync state"), chalk.gray(err.message));
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// Summary
|
|
1212
|
+
if (pushed === 0 &&
|
|
1213
|
+
pulled === 0 &&
|
|
1214
|
+
unchangedCount === 0 &&
|
|
1215
|
+
skipped === 0 &&
|
|
1216
|
+
errors === 0) {
|
|
1217
|
+
console.log(chalk.gray(" No configs in cloud yet. Push some first from a device that has them.\n"));
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const parts = [];
|
|
1221
|
+
if (pushed > 0)
|
|
1222
|
+
parts.push(`${pushed} pushed`);
|
|
1223
|
+
if (pulled > 0)
|
|
1224
|
+
parts.push(`${pulled} restored`);
|
|
1225
|
+
if (deletedLocal > 0)
|
|
1226
|
+
parts.push(`${deletedLocal} deleted locally`);
|
|
1227
|
+
if (unchangedCount > 0)
|
|
1228
|
+
parts.push(`${unchangedCount} unchanged`);
|
|
1229
|
+
if (skipped > 0)
|
|
1230
|
+
parts.push(`${skipped} skipped`);
|
|
1231
|
+
if (errors > 0)
|
|
1232
|
+
parts.push(`${errors} errors`);
|
|
1233
|
+
console.log(chalk.bold(`\n Done: ${parts.join(", ")}`));
|
|
1234
|
+
if (pulled > 0) {
|
|
1235
|
+
console.log(chalk.gray(" Restart your agents for restored configs to take effect."));
|
|
1236
|
+
}
|
|
1237
|
+
console.log();
|
|
1238
|
+
}
|
|
1239
|
+
function formatAgentName(id) {
|
|
1240
|
+
const names = {
|
|
1241
|
+
"claude-code": "Claude Code",
|
|
1242
|
+
cursor: "Cursor",
|
|
1243
|
+
codex: "Codex",
|
|
1244
|
+
gemini: "Gemini CLI",
|
|
1245
|
+
copilot: "GitHub Copilot",
|
|
1246
|
+
windsurf: "Windsurf",
|
|
1247
|
+
openclaw: "OpenClaw",
|
|
1248
|
+
opencode: "OpenCode",
|
|
1249
|
+
generic: "Generic",
|
|
1250
|
+
};
|
|
1251
|
+
return names[id] ?? id;
|
|
1252
|
+
}
|
|
1253
|
+
async function promptConflict(_agent, filePath) {
|
|
1254
|
+
const answer = await ask(chalk.yellow(`\n ${filePath} has changes on both sides.\n` +
|
|
1255
|
+
` [l] Keep local [c] Use cloud [m] Merge (LLM) [s] Skip: `));
|
|
1256
|
+
const a = answer.toLowerCase();
|
|
1257
|
+
if (a === "l")
|
|
1258
|
+
return "local";
|
|
1259
|
+
if (a === "c")
|
|
1260
|
+
return "cloud";
|
|
1261
|
+
if (a === "m")
|
|
1262
|
+
return "merge";
|
|
1263
|
+
return "skip";
|
|
1264
|
+
}
|
|
1265
|
+
async function promptCloudDeletion(filePath) {
|
|
1266
|
+
const answer = await ask(chalk.yellow(`\n ${filePath} was deleted in cloud.\n` +
|
|
1267
|
+
` [d] Delete local copy [k] Keep local and restore cloud [s] Skip: `));
|
|
1268
|
+
const normalized = answer.toLowerCase();
|
|
1269
|
+
if (normalized === "d")
|
|
1270
|
+
return "delete";
|
|
1271
|
+
if (normalized === "k")
|
|
1272
|
+
return "local";
|
|
1273
|
+
return "skip";
|
|
1274
|
+
}
|
|
1275
|
+
async function mergeConfigs(agent, filePath, localContent, cloudContent) {
|
|
1276
|
+
try {
|
|
1277
|
+
const result = await getClient().configs.merge({
|
|
1278
|
+
local_content: localContent,
|
|
1279
|
+
cloud_content: cloudContent,
|
|
1280
|
+
file_path: filePath,
|
|
1281
|
+
agent,
|
|
1282
|
+
});
|
|
1283
|
+
return result.merged_content;
|
|
1284
|
+
}
|
|
1285
|
+
catch (err) {
|
|
1286
|
+
console.log(chalk.red(` Merge failed: ${err.message}`));
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
//# sourceMappingURL=agent-configs.js.map
|