memax-cli 0.1.0-alpha.5 → 0.1.0-alpha.50

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