memax-cli 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (97) hide show
  1. package/assets/skills/memax-memory/SKILL.md +154 -0
  2. package/dist/commands/auth.d.ts +1 -0
  3. package/dist/commands/auth.d.ts.map +1 -1
  4. package/dist/commands/auth.js +14 -7
  5. package/dist/commands/auth.js.map +1 -1
  6. package/dist/commands/capture.d.ts +17 -0
  7. package/dist/commands/capture.d.ts.map +1 -0
  8. package/dist/commands/capture.js +60 -0
  9. package/dist/commands/capture.js.map +1 -0
  10. package/dist/commands/delete.d.ts +1 -1
  11. package/dist/commands/delete.d.ts.map +1 -1
  12. package/dist/commands/delete.js +22 -5
  13. package/dist/commands/delete.js.map +1 -1
  14. package/dist/commands/hub.d.ts +4 -0
  15. package/dist/commands/hub.d.ts.map +1 -0
  16. package/dist/commands/hub.js +53 -0
  17. package/dist/commands/hub.js.map +1 -0
  18. package/dist/commands/list.d.ts +2 -1
  19. package/dist/commands/list.d.ts.map +1 -1
  20. package/dist/commands/list.js +35 -8
  21. package/dist/commands/list.js.map +1 -1
  22. package/dist/commands/login.d.ts.map +1 -1
  23. package/dist/commands/login.js +32 -7
  24. package/dist/commands/login.js.map +1 -1
  25. package/dist/commands/mcp.d.ts.map +1 -1
  26. package/dist/commands/mcp.js +226 -26
  27. package/dist/commands/mcp.js.map +1 -1
  28. package/dist/commands/push.d.ts +2 -1
  29. package/dist/commands/push.d.ts.map +1 -1
  30. package/dist/commands/push.js +49 -11
  31. package/dist/commands/push.js.map +1 -1
  32. package/dist/commands/recall.d.ts +1 -0
  33. package/dist/commands/recall.d.ts.map +1 -1
  34. package/dist/commands/recall.js +30 -27
  35. package/dist/commands/recall.js.map +1 -1
  36. package/dist/commands/setup-hooks.d.ts +12 -0
  37. package/dist/commands/setup-hooks.d.ts.map +1 -0
  38. package/dist/commands/setup-hooks.js +184 -0
  39. package/dist/commands/setup-hooks.js.map +1 -0
  40. package/dist/commands/setup-instructions.d.ts +21 -0
  41. package/dist/commands/setup-instructions.d.ts.map +1 -0
  42. package/dist/commands/setup-instructions.js +172 -0
  43. package/dist/commands/setup-instructions.js.map +1 -0
  44. package/dist/commands/setup-mcp.d.ts +14 -0
  45. package/dist/commands/setup-mcp.d.ts.map +1 -0
  46. package/dist/commands/setup-mcp.js +276 -0
  47. package/dist/commands/setup-mcp.js.map +1 -0
  48. package/dist/commands/setup-types.d.ts +20 -0
  49. package/dist/commands/setup-types.d.ts.map +1 -0
  50. package/dist/commands/setup-types.js +60 -0
  51. package/dist/commands/setup-types.js.map +1 -0
  52. package/dist/commands/setup.d.ts +18 -0
  53. package/dist/commands/setup.d.ts.map +1 -0
  54. package/dist/commands/setup.js +371 -0
  55. package/dist/commands/setup.js.map +1 -0
  56. package/dist/commands/show.d.ts.map +1 -1
  57. package/dist/commands/show.js +10 -13
  58. package/dist/commands/show.js.map +1 -1
  59. package/dist/commands/sync.d.ts +7 -2
  60. package/dist/commands/sync.d.ts.map +1 -1
  61. package/dist/commands/sync.js +614 -107
  62. package/dist/commands/sync.js.map +1 -1
  63. package/dist/index.js +85 -14
  64. package/dist/index.js.map +1 -1
  65. package/dist/lib/client.d.ts +6 -0
  66. package/dist/lib/client.d.ts.map +1 -0
  67. package/dist/lib/client.js +69 -0
  68. package/dist/lib/client.js.map +1 -0
  69. package/dist/lib/project-context.d.ts +40 -0
  70. package/dist/lib/project-context.d.ts.map +1 -0
  71. package/dist/lib/project-context.js +157 -0
  72. package/dist/lib/project-context.js.map +1 -0
  73. package/dist/lib/prompt.d.ts +7 -0
  74. package/dist/lib/prompt.d.ts.map +1 -0
  75. package/dist/lib/prompt.js +41 -0
  76. package/dist/lib/prompt.js.map +1 -0
  77. package/package.json +17 -13
  78. package/dist/lib/api.d.ts +0 -4
  79. package/dist/lib/api.d.ts.map +0 -1
  80. package/dist/lib/api.js +0 -95
  81. package/dist/lib/api.js.map +0 -1
  82. package/src/commands/auth.ts +0 -92
  83. package/src/commands/config.ts +0 -27
  84. package/src/commands/delete.ts +0 -20
  85. package/src/commands/hook.ts +0 -243
  86. package/src/commands/list.ts +0 -38
  87. package/src/commands/login.ts +0 -159
  88. package/src/commands/mcp.ts +0 -282
  89. package/src/commands/push.ts +0 -82
  90. package/src/commands/recall.ts +0 -160
  91. package/src/commands/show.ts +0 -35
  92. package/src/commands/sync.ts +0 -403
  93. package/src/index.ts +0 -167
  94. package/src/lib/api.ts +0 -110
  95. package/src/lib/config.ts +0 -61
  96. package/src/lib/credentials.ts +0 -42
  97. package/tsconfig.json +0 -9
@@ -1,8 +1,11 @@
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, resolveClaudeProjectFolder, normalizeFilePath, } from "../lib/project-context.js";
8
+ import { confirm, ask, confirmDefault } from "../lib/prompt.js";
6
9
  const DEFAULT_IGNORE = new Set([
7
10
  "node_modules",
8
11
  ".git",
@@ -37,14 +40,136 @@ const SUPPORTED_EXTENSIONS = new Set([
37
40
  ".proto",
38
41
  ".dockerfile",
39
42
  ]);
40
- export async function syncAgentMemoryCommand() {
41
- await syncAgentMemory();
43
+ export async function syncAgentMemoryCommand(options = {}) {
44
+ await syncAgentMemory(options);
42
45
  }
43
- export async function syncCommand(directory, options) {
44
- if (options.agentMemory || directory === "agents") {
45
- await syncAgentMemory();
46
+ export async function listAgentConfigsCommand() {
47
+ let configs;
48
+ try {
49
+ const result = await getClient().configs.list();
50
+ configs = result.configs;
51
+ }
52
+ catch (err) {
53
+ console.error(chalk.red(` Failed to fetch configs: ${err.message}\n`));
54
+ return;
55
+ }
56
+ if (!configs || configs.length === 0) {
57
+ console.log(chalk.yellow(" No synced configs. Run: memax agent-configs sync\n"));
46
58
  return;
47
59
  }
60
+ // Group by agent
61
+ const byAgent = new Map();
62
+ for (const c of configs) {
63
+ const list = byAgent.get(c.agent) ?? [];
64
+ list.push({
65
+ id: c.id,
66
+ filePath: c.file_path,
67
+ scope: c.scope,
68
+ updatedAt: c.updated_at,
69
+ });
70
+ byAgent.set(c.agent, list);
71
+ }
72
+ console.log();
73
+ for (const [agent, files] of byAgent) {
74
+ console.log(` ${chalk.cyan(agent)}`);
75
+ for (const f of files) {
76
+ const scopeTag = f.scope === "global"
77
+ ? chalk.dim("global")
78
+ : chalk.dim(f.scope.replace("project:", ""));
79
+ const age = formatAge(f.updatedAt);
80
+ console.log(` ${f.filePath} ${scopeTag} ${chalk.dim(age)} ${chalk.dim(f.id.slice(0, 8))}`);
81
+ }
82
+ console.log();
83
+ }
84
+ console.log(chalk.gray(` ${configs.length} config${configs.length > 1 ? "s" : ""} synced to cloud.\n`));
85
+ }
86
+ export async function deleteAgentConfigsCommand() {
87
+ let configs;
88
+ try {
89
+ const result = await getClient().configs.list();
90
+ configs = result.configs;
91
+ }
92
+ catch (err) {
93
+ console.error(chalk.red(` Failed to fetch configs: ${err.message}\n`));
94
+ return;
95
+ }
96
+ if (!configs || configs.length === 0) {
97
+ console.log(chalk.yellow(" No synced configs to delete.\n"));
98
+ return;
99
+ }
100
+ // Display numbered list grouped by agent
101
+ const items = [];
102
+ let currentAgent = "";
103
+ for (const c of configs) {
104
+ if (c.agent !== currentAgent) {
105
+ if (currentAgent)
106
+ console.log();
107
+ console.log(` ${chalk.cyan(c.agent)}`);
108
+ currentAgent = c.agent;
109
+ }
110
+ items.push({
111
+ id: c.id,
112
+ agent: c.agent,
113
+ filePath: c.file_path,
114
+ scope: c.scope,
115
+ });
116
+ const idx = chalk.dim(`${items.length}.`);
117
+ const scopeTag = c.scope === "global"
118
+ ? chalk.dim("global")
119
+ : chalk.dim(c.scope.replace("project:", ""));
120
+ console.log(` ${idx} ${c.file_path} ${scopeTag}`);
121
+ }
122
+ console.log();
123
+ const answer = await ask(" Select configs to delete (comma-separated numbers, or 'q' to quit): ");
124
+ if (!answer || answer.trim().toLowerCase() === "q") {
125
+ console.log(chalk.gray(" Cancelled.\n"));
126
+ return;
127
+ }
128
+ const indices = answer
129
+ .split(",")
130
+ .map((s) => parseInt(s.trim(), 10))
131
+ .filter((n) => !isNaN(n) && n >= 1 && n <= items.length);
132
+ if (indices.length === 0) {
133
+ console.log(chalk.gray(" No valid selections.\n"));
134
+ return;
135
+ }
136
+ // Confirm
137
+ console.log();
138
+ for (const i of indices) {
139
+ const item = items[i - 1];
140
+ console.log(chalk.yellow(` ${item.agent}/${item.filePath}`));
141
+ }
142
+ const ok = await confirm(`\n Delete ${indices.length} config${indices.length > 1 ? "s" : ""}? (y/N) `);
143
+ if (!ok) {
144
+ console.log(chalk.gray(" Cancelled.\n"));
145
+ return;
146
+ }
147
+ let deleted = 0;
148
+ for (const i of indices) {
149
+ const item = items[i - 1];
150
+ try {
151
+ await getClient().configs.delete(item.id);
152
+ console.log(chalk.green(` \u2713 ${item.agent}/${item.filePath}`), chalk.gray("deleted"));
153
+ deleted++;
154
+ }
155
+ catch (err) {
156
+ console.log(chalk.red(` \u2717 ${item.agent}/${item.filePath}`), chalk.gray(err.message));
157
+ }
158
+ }
159
+ console.log(chalk.gray(`\n ${deleted} config${deleted > 1 ? "s" : ""} deleted.\n`));
160
+ }
161
+ function formatAge(dateStr) {
162
+ const ms = Date.now() - new Date(dateStr).getTime();
163
+ const mins = Math.floor(ms / 60000);
164
+ if (mins < 60)
165
+ return `${mins}m ago`;
166
+ const hours = Math.floor(mins / 60);
167
+ if (hours < 24)
168
+ return `${hours}h ago`;
169
+ const days = Math.floor(hours / 24);
170
+ return `${days}d ago`;
171
+ }
172
+ export async function syncCommand(directory, options) {
48
173
  const dir = directory ?? ".";
49
174
  const customIgnore = options.ignore
50
175
  ? new Set(options.ignore.split(",").map((s) => s.trim()))
@@ -57,6 +182,15 @@ export async function syncCommand(directory, options) {
57
182
  return;
58
183
  }
59
184
  console.log(chalk.gray(`Found ${files.length} files to sync`));
185
+ // Confirm if many files (>10) unless -y is passed
186
+ if (files.length > 10 && !options.yes) {
187
+ console.log(chalk.yellow(`\n This will push ${files.length} files. Continue? (y/N) `));
188
+ const confirmed = await confirm(" ");
189
+ if (!confirmed) {
190
+ console.log(chalk.gray(" Cancelled.\n"));
191
+ return;
192
+ }
193
+ }
60
194
  console.log();
61
195
  let pushed = 0;
62
196
  let errors = 0;
@@ -108,15 +242,14 @@ async function pushFile(file, options) {
108
242
  : ext === ".json" || ext === ".yaml" || ext === ".yml"
109
243
  ? "structured"
110
244
  : "code";
111
- const note = await apiPost("/v1/notes", {
112
- content,
245
+ const memory = await getClient().push(content, {
113
246
  title: relPath,
114
247
  category: options.category ?? guessCategory(relPath),
115
248
  source: "sync",
116
- source_path: relPath,
117
- content_type: contentType,
249
+ sourcePath: relPath,
250
+ contentType,
118
251
  });
119
- console.log(chalk.green(" +"), relPath, chalk.gray(`[${note.category}]`));
252
+ console.log(chalk.green(" +"), relPath, chalk.gray(`[${memory.category}]`));
120
253
  return "pushed";
121
254
  }
122
255
  catch (err) {
@@ -179,143 +312,517 @@ function guessCategory(path) {
179
312
  return "reference/config";
180
313
  return "daily/note";
181
314
  }
182
- function discoverAgentMemoryFiles() {
315
+ function discoverAgentConfigs() {
183
316
  const home = homedir();
184
317
  const cwd = process.cwd();
318
+ const projectScope = getProjectScope(cwd);
185
319
  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
320
+ const add = (agent, label, path, filePath, scope = "global") => locations.push({
321
+ agent,
322
+ label,
323
+ path,
324
+ filePath: normalizeFilePath(filePath),
325
+ scope,
326
+ });
327
+ // Claude Code — global
328
+ add("claude-code", "~/.claude/CLAUDE.md", join(home, ".claude", "CLAUDE.md"), "CLAUDE.md");
329
+ add("claude-code", "~/.claude/MEMORY.md", join(home, ".claude", "MEMORY.md"), "MEMORY.md");
330
+ // Claude Code — per-project memories: ~/.claude/projects/*/memory/*.md
331
+ // The folder name is the absolute project path with "/" replaced by "-"
332
+ // (e.g., "-workspaces-memax"). We resolve it to a git repo URL so the
333
+ // same project's memories match across machines regardless of clone path.
190
334
  const claudeProjectsDir = join(home, ".claude", "projects");
191
335
  if (existsSync(claudeProjectsDir)) {
192
336
  try {
193
337
  for (const project of readdirSync(claudeProjectsDir)) {
194
338
  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
- }
339
+ if (!existsSync(memoryDir))
340
+ continue;
341
+ // Try to resolve mangled folder → git repo → canonical scope
342
+ const repoUrl = resolveClaudeProjectFolder(project);
343
+ const memoryScope = repoUrl ? `project:${repoUrl}` : undefined;
344
+ try {
345
+ for (const file of readdirSync(memoryDir)) {
346
+ if (!file.endsWith(".md"))
347
+ continue;
348
+ if (memoryScope) {
349
+ // Canonical: filePath is just "memory/<file>", scope identifies the project
350
+ add("claude-code", `~/.claude/projects/${project}/memory/${file}`, join(memoryDir, file), `memory/${file}`, memoryScope);
351
+ }
352
+ else {
353
+ // Fallback: can't resolve project → keep legacy format with folder name
354
+ add("claude-code", `~/.claude/projects/${project}/memory/${file}`, join(memoryDir, file), `projects/${project}/memory/${file}`);
204
355
  }
205
356
  }
206
- catch {
207
- // Permission denied or other read error — skip
208
- }
357
+ }
358
+ catch {
359
+ // Permission denied — skip
209
360
  }
210
361
  }
211
362
  }
212
363
  catch {
213
- // Permission denied or other read error — skip
364
+ // Permission denied — skip
214
365
  }
215
366
  }
216
- // Cursor rules (project-level)
217
- locations.push({ label: "./.cursorrules", path: join(cwd, ".cursorrules") });
218
- // Cursor scoped rules: .cursor/rules/*.mdc
367
+ // Claude Code project-level
368
+ add("claude-code", "./.claude/CLAUDE.md", join(cwd, ".claude", "CLAUDE.md"), "CLAUDE.md", projectScope);
369
+ // Cursor (project-level)
370
+ add("cursor", "./.cursorrules", join(cwd, ".cursorrules"), ".cursorrules", projectScope);
219
371
  const cursorRulesDir = join(cwd, ".cursor", "rules");
220
372
  if (existsSync(cursorRulesDir)) {
221
373
  try {
222
374
  for (const file of readdirSync(cursorRulesDir)) {
223
375
  if (file.endsWith(".mdc")) {
224
- locations.push({
225
- label: `./.cursor/rules/${file}`,
226
- path: join(cursorRulesDir, file),
227
- });
376
+ add("cursor", `./.cursor/rules/${file}`, join(cursorRulesDir, file), `.cursor/rules/${file}`, projectScope);
228
377
  }
229
378
  }
230
379
  }
231
380
  catch {
232
- // Skip on error
381
+ /* skip */
233
382
  }
234
383
  }
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)) {
258
- 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);
384
+ // Codex
385
+ add("codex", "./.codex/instructions.md", join(cwd, ".codex", "instructions.md"), "instructions.md", projectScope);
386
+ add("codex", "~/.codex/AGENTS.md", join(home, ".codex", "AGENTS.md"), "AGENTS.md");
387
+ // Gemini CLI
388
+ add("gemini", "~/.gemini/GEMINI.md", join(home, ".gemini", "GEMINI.md"), "GEMINI.md");
389
+ add("gemini", "./GEMINI.md", join(cwd, "GEMINI.md"), "GEMINI.md", projectScope);
390
+ // GitHub Copilot
391
+ add("copilot", "./.github/copilot-instructions.md", join(cwd, ".github", "copilot-instructions.md"), "copilot-instructions.md", projectScope);
392
+ // Windsurf
393
+ add("windsurf", "./.windsurfrules", join(cwd, ".windsurfrules"), ".windsurfrules", projectScope);
394
+ const windsurfRulesDir = join(cwd, ".windsurf", "rules");
395
+ if (existsSync(windsurfRulesDir)) {
396
+ try {
397
+ for (const file of readdirSync(windsurfRulesDir)) {
398
+ if (file.endsWith(".md")) {
399
+ add("windsurf", `./.windsurf/rules/${file}`, join(windsurfRulesDir, file), `.windsurf/rules/${file}`, projectScope);
265
400
  }
266
401
  }
267
- catch {
268
- notFound.push(loc.label);
269
- }
270
402
  }
271
- else {
272
- notFound.push(loc.label);
403
+ catch {
404
+ /* skip */
273
405
  }
274
406
  }
275
- if (found.length === 0) {
276
- console.log(chalk.yellow("No agent memory files found."));
277
- return;
278
- }
279
- console.log("Found:");
280
- for (const f of found) {
281
- console.log(chalk.green(" \u2713"), f.label, chalk.gray(`(${formatFileSize(f.size)})`));
407
+ // OpenClaw
408
+ const openclawMemoryDir = join(home, ".openclaw", "memory");
409
+ if (existsSync(openclawMemoryDir)) {
410
+ try {
411
+ for (const file of readdirSync(openclawMemoryDir)) {
412
+ if (file.endsWith(".md") || file.endsWith(".json")) {
413
+ add("openclaw", `~/.openclaw/memory/${file}`, join(openclawMemoryDir, file), `memory/${file}`);
414
+ }
415
+ }
416
+ }
417
+ catch {
418
+ /* skip */
419
+ }
282
420
  }
283
- for (const label of notFound) {
284
- console.log(chalk.gray(" \u2717"), chalk.gray(label), chalk.gray("(not found)"));
421
+ // OpenCode (project-level)
422
+ const opencodePath = join(cwd, ".opencode");
423
+ if (existsSync(opencodePath)) {
424
+ try {
425
+ for (const file of readdirSync(opencodePath)) {
426
+ if (file.endsWith(".md")) {
427
+ add("opencode", `./.opencode/${file}`, join(opencodePath, file), file, projectScope);
428
+ }
429
+ }
430
+ }
431
+ catch {
432
+ /* skip */
433
+ }
285
434
  }
286
- console.log();
287
- let synced = 0;
288
- let unchanged = 0;
289
- let errors = 0;
290
- for (const f of found) {
435
+ // Generic project-level agent files
436
+ add("generic", "./AGENTS.md", join(cwd, "AGENTS.md"), "AGENTS.md", projectScope);
437
+ add("generic", "./CLAUDE.md", join(cwd, "CLAUDE.md"), "CLAUDE.md", projectScope);
438
+ return locations;
439
+ }
440
+ async function syncAgentMemory(options = {}) {
441
+ console.log(chalk.bold("\n Memax Config Sync\n"));
442
+ // Discover local config files
443
+ const locations = discoverAgentConfigs();
444
+ const localConfigs = [];
445
+ for (const loc of locations) {
446
+ if (!existsSync(loc.path))
447
+ continue;
291
448
  try {
292
- const content = readFileSync(f.path, "utf-8");
449
+ const stat = statSync(loc.path);
450
+ if (!stat.isFile() || stat.size === 0)
451
+ continue;
452
+ const content = readFileSync(loc.path, "utf-8");
293
453
  if (!content.trim())
294
454
  continue;
295
- const title = basename(f.path);
296
- await apiPost("/v1/notes", {
455
+ const hash = createHash("sha256").update(content).digest("hex");
456
+ localConfigs.push({
457
+ loc,
297
458
  content,
298
- title,
299
- source: "sync",
300
- source_path: f.path,
301
- content_type: "markdown",
302
- category: "",
459
+ hash,
460
+ updatedAt: stat.mtime.toISOString(),
303
461
  });
304
- synced++;
305
462
  }
306
- catch (err) {
307
- const msg = err.message;
308
- if (msg.includes("unchanged") ||
309
- msg.includes("duplicate") ||
310
- msg.includes("exists")) {
311
- unchanged++;
463
+ catch {
464
+ // Skip unreadable files
465
+ }
466
+ }
467
+ const isBootstrap = localConfigs.length === 0;
468
+ if (isBootstrap) {
469
+ console.log(chalk.gray(" No local agent configs found. Checking cloud for backups...\n"));
470
+ }
471
+ else {
472
+ console.log(chalk.gray(` Found ${localConfigs.length} local config${localConfigs.length > 1 ? "s" : ""}. Syncing with cloud...\n`));
473
+ }
474
+ // Build manifest — may be empty on a new device (that's fine, we'll pull from cloud)
475
+ const manifest = localConfigs.map((c) => ({
476
+ agent: c.loc.agent,
477
+ file_path: c.loc.filePath,
478
+ scope: c.loc.scope,
479
+ content_hash: c.hash,
480
+ updated_at: c.updatedAt,
481
+ }));
482
+ let actions;
483
+ try {
484
+ const plan = await getClient().configs.sync(manifest);
485
+ actions = plan.actions;
486
+ }
487
+ catch (err) {
488
+ console.error(chalk.red(` Sync failed: ${err.message}\n`));
489
+ return;
490
+ }
491
+ // Force modes override the plan
492
+ if (options.push) {
493
+ actions = actions.map((a) => a.action === "pull" || a.action === "conflict"
494
+ ? { ...a, action: "push" }
495
+ : a);
496
+ }
497
+ else if (options.pull) {
498
+ actions = actions.map((a) => a.action === "push" || a.action === "conflict"
499
+ ? { ...a, action: "pull" }
500
+ : a);
501
+ }
502
+ // Filter out project-scoped cloud-only configs that don't belong to the
503
+ // current project. Without this, running `memax sync agents` from ~/
504
+ // would dump project configs (like .cursorrules from repo X) into the
505
+ // home directory.
506
+ const currentProjectScope = getProjectScope();
507
+ actions = actions.filter((a) => {
508
+ if (!a.scope.startsWith("project:"))
509
+ return true; // global → always sync
510
+ if (a.scope === "project")
511
+ return true; // legacy → keep for compat
512
+ // Project-scoped: only sync if it matches the current project
513
+ if (a.scope === currentProjectScope)
514
+ return true;
515
+ // Cloud-only configs for other projects → skip silently
516
+ if (a.action === "pull" && a.reason === "cloud_only")
517
+ return false;
518
+ // Conflict/push for configs we have locally → keep (user is in the project)
519
+ return true;
520
+ });
521
+ // Index local configs by (agent, file_path, scope) for quick lookup
522
+ const localByKey = new Map();
523
+ for (const c of localConfigs) {
524
+ localByKey.set(`${c.loc.agent}|${c.loc.filePath}|${c.loc.scope}`, c);
525
+ }
526
+ // Index locations by (agent, file_path, scope) for pull path resolution
527
+ const locByKey = new Map();
528
+ for (const loc of locations) {
529
+ locByKey.set(`${loc.agent}|${loc.filePath}|${loc.scope}`, loc);
530
+ }
531
+ // Resolve a local write path for any config — even ones not discovered locally.
532
+ // This enables pulling configs to a brand-new device where agent dirs don't exist yet.
533
+ const resolveWritePath = (agent, filePath, scope) => {
534
+ // First check if we have a known location from local discovery
535
+ const loc = locByKey.get(`${agent}|${filePath}|${scope}`);
536
+ if (loc)
537
+ return loc.path;
538
+ const home = homedir();
539
+ // Global configs: reconstruct from agent dir + filePath
540
+ if (scope === "global") {
541
+ const agentDirs = {
542
+ "claude-code": join(home, ".claude"),
543
+ cursor: join(home, ".cursor"),
544
+ codex: join(home, ".codex"),
545
+ gemini: join(home, ".gemini"),
546
+ copilot: join(home, ".copilot"),
547
+ windsurf: join(home, ".windsurf"),
548
+ openclaw: join(home, ".openclaw"),
549
+ opencode: join(home, ".opencode"),
550
+ };
551
+ const dir = agentDirs[agent];
552
+ if (dir)
553
+ return join(dir, filePath);
554
+ }
555
+ // Project-scoped configs — ONLY write if we're in the matching project.
556
+ // This is the safety net: never write project files to the wrong directory.
557
+ if (scope.startsWith("project")) {
558
+ // Verify this scope matches the current project
559
+ if (scope !== "project" && scope !== currentProjectScope) {
560
+ return null; // Wrong project — refuse to write
561
+ }
562
+ // Claude per-project memories: filePath like "memory/feedback.md"
563
+ if (agent === "claude-code" && filePath.startsWith("memory/")) {
564
+ const projectDir = findClaudeProjectDir(scope);
565
+ if (projectDir)
566
+ return join(projectDir, filePath);
567
+ // Fallback: use mangled cwd path
568
+ const mangledCwd = process.cwd().replace(/\//g, "-");
569
+ return join(home, ".claude", "projects", mangledCwd, filePath);
570
+ }
571
+ // Regular project configs: write relative to cwd
572
+ return join(process.cwd(), filePath);
573
+ }
574
+ return null;
575
+ };
576
+ /**
577
+ * Find the local ~/.claude/projects/<mangled> directory that corresponds
578
+ * to a given project scope (e.g., "project:github.com/memaxlabs/memax").
579
+ */
580
+ function findClaudeProjectDir(scope) {
581
+ const home = homedir();
582
+ const claudeProjectsDir = join(home, ".claude", "projects");
583
+ if (!existsSync(claudeProjectsDir))
584
+ return null;
585
+ try {
586
+ for (const project of readdirSync(claudeProjectsDir)) {
587
+ const repoUrl = resolveClaudeProjectFolder(project);
588
+ if (repoUrl && scope === `project:${repoUrl}`) {
589
+ return join(claudeProjectsDir, project);
590
+ }
591
+ }
592
+ }
593
+ catch {
594
+ // Permission denied — skip
595
+ }
596
+ return null;
597
+ }
598
+ // Execute sync plan
599
+ let pushed = 0;
600
+ let pulled = 0;
601
+ let unchangedCount = 0;
602
+ let skipped = 0;
603
+ let errors = 0;
604
+ // Group actions by agent for display
605
+ const byAgent = new Map();
606
+ for (const action of actions) {
607
+ const group = byAgent.get(action.agent) ?? [];
608
+ group.push(action);
609
+ byAgent.set(action.agent, group);
610
+ }
611
+ for (const [agent, agentActions] of byAgent) {
612
+ console.log(chalk.white(` ${formatAgentName(agent)}`));
613
+ for (const action of agentActions) {
614
+ const key = `${action.agent}|${action.file_path}|${action.scope}`;
615
+ if (action.action === "unchanged") {
616
+ console.log(chalk.gray(` = ${action.file_path}`), chalk.gray("unchanged"));
617
+ unchangedCount++;
618
+ continue;
619
+ }
620
+ if (action.action === "push") {
621
+ const local = localByKey.get(key);
622
+ if (!local) {
623
+ errors++;
624
+ continue;
625
+ }
626
+ try {
627
+ await getClient().configs.upsert({
628
+ agent: action.agent,
629
+ file_path: action.file_path,
630
+ scope: action.scope,
631
+ content: local.content,
632
+ });
633
+ console.log(chalk.green(` \u2191 ${action.file_path}`), chalk.gray(action.reason === "local_only"
634
+ ? "pushing (new)"
635
+ : "pushing (local newer)"));
636
+ pushed++;
637
+ }
638
+ catch (err) {
639
+ console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
640
+ errors++;
641
+ }
642
+ continue;
643
+ }
644
+ if (action.action === "pull") {
645
+ if (!action.config_id) {
646
+ errors++;
647
+ continue;
648
+ }
649
+ try {
650
+ const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
651
+ if (!writePath) {
652
+ console.log(chalk.yellow(` ? ${action.file_path}`), chalk.gray(action.scope !== "global" &&
653
+ action.scope !== currentProjectScope
654
+ ? "different project \u2014 skipped"
655
+ : "unknown agent \u2014 skipped"));
656
+ skipped++;
657
+ continue;
658
+ }
659
+ // For new files (not updates), ask user before writing
660
+ const isNewLocally = action.reason === "cloud_only" && !existsSync(writePath);
661
+ if (isNewLocally && !options.pull) {
662
+ console.log(chalk.cyan(` New file: ${action.file_path}`));
663
+ console.log(chalk.gray(` → ${writePath}`));
664
+ const accept = await confirmDefault(` Download? [Y/n] `);
665
+ if (!accept) {
666
+ console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
667
+ skipped++;
668
+ continue;
669
+ }
670
+ }
671
+ const config = await getClient().configs.get(action.config_id);
672
+ mkdirSync(dirname(writePath), { recursive: true });
673
+ writeFileSync(writePath, config.content);
674
+ console.log(chalk.cyan(` \u2193 ${action.file_path}`), chalk.gray(isNewLocally ? "restored" : "pulling (cloud newer)"));
675
+ pulled++;
676
+ }
677
+ catch (err) {
678
+ console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
679
+ errors++;
680
+ }
681
+ continue;
312
682
  }
313
- else {
314
- errors++;
315
- console.log(chalk.red(" Error syncing"), f.label, chalk.gray(msg));
683
+ if (action.action === "conflict") {
684
+ const resolution = await promptConflict(agent, action.file_path);
685
+ if (resolution === "local") {
686
+ const local = localByKey.get(key);
687
+ if (local) {
688
+ try {
689
+ await getClient().configs.upsert({
690
+ agent: action.agent,
691
+ file_path: action.file_path,
692
+ scope: action.scope,
693
+ content: local.content,
694
+ });
695
+ console.log(chalk.green(` \u2191 ${action.file_path}`), chalk.gray("kept local"));
696
+ pushed++;
697
+ }
698
+ catch (err) {
699
+ console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
700
+ errors++;
701
+ }
702
+ }
703
+ }
704
+ else if (resolution === "cloud" && action.config_id) {
705
+ try {
706
+ const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
707
+ if (writePath) {
708
+ const config = await getClient().configs.get(action.config_id);
709
+ mkdirSync(dirname(writePath), { recursive: true });
710
+ writeFileSync(writePath, config.content);
711
+ console.log(chalk.cyan(` \u2193 ${action.file_path}`), chalk.gray("used cloud"));
712
+ pulled++;
713
+ }
714
+ }
715
+ catch (err) {
716
+ console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
717
+ errors++;
718
+ }
719
+ }
720
+ else if (resolution === "merge" && action.config_id) {
721
+ const local = localByKey.get(key);
722
+ if (local) {
723
+ try {
724
+ const cloudConfig = await getClient().configs.get(action.config_id);
725
+ console.log(chalk.gray(` Merging with LLM...`));
726
+ const merged = await mergeConfigs(action.agent, action.file_path, local.content, cloudConfig.content);
727
+ if (merged) {
728
+ // Write merged to local
729
+ const writePath = resolveWritePath(action.agent, action.file_path, action.scope);
730
+ if (writePath) {
731
+ mkdirSync(dirname(writePath), { recursive: true });
732
+ writeFileSync(writePath, merged);
733
+ }
734
+ // Push merged to cloud
735
+ await getClient().configs.upsert({
736
+ agent: action.agent,
737
+ file_path: action.file_path,
738
+ scope: action.scope,
739
+ content: merged,
740
+ });
741
+ console.log(chalk.magenta(` \u2194 ${action.file_path}`), chalk.gray("merged (LLM)"));
742
+ pushed++;
743
+ }
744
+ else {
745
+ skipped++;
746
+ }
747
+ }
748
+ catch (err) {
749
+ console.log(chalk.red(` \u2717 ${action.file_path}`), chalk.gray(err.message));
750
+ errors++;
751
+ }
752
+ }
753
+ }
754
+ else {
755
+ console.log(chalk.gray(` - ${action.file_path}`), chalk.gray("skipped"));
756
+ skipped++;
757
+ }
316
758
  }
317
759
  }
318
760
  }
319
- console.log(chalk.blue(`Synced ${found.length} files to Memax`), chalk.gray(`(${synced} new, ${unchanged} unchanged${errors > 0 ? `, ${errors} errors` : ""})`));
761
+ // Summary
762
+ if (pushed === 0 &&
763
+ pulled === 0 &&
764
+ unchangedCount === 0 &&
765
+ skipped === 0 &&
766
+ errors === 0) {
767
+ console.log(chalk.gray(" No configs in cloud yet. Push some first from a device that has them.\n"));
768
+ return;
769
+ }
770
+ const parts = [];
771
+ if (pushed > 0)
772
+ parts.push(`${pushed} pushed`);
773
+ if (pulled > 0)
774
+ parts.push(`${pulled} restored`);
775
+ if (unchangedCount > 0)
776
+ parts.push(`${unchangedCount} unchanged`);
777
+ if (skipped > 0)
778
+ parts.push(`${skipped} skipped`);
779
+ if (errors > 0)
780
+ parts.push(`${errors} errors`);
781
+ console.log(chalk.bold(`\n Done: ${parts.join(", ")}`));
782
+ if (pulled > 0) {
783
+ console.log(chalk.gray(" Restart your agents for restored configs to take effect."));
784
+ }
785
+ console.log();
786
+ }
787
+ function formatAgentName(id) {
788
+ const names = {
789
+ "claude-code": "Claude Code",
790
+ cursor: "Cursor",
791
+ codex: "Codex",
792
+ gemini: "Gemini CLI",
793
+ copilot: "GitHub Copilot",
794
+ windsurf: "Windsurf",
795
+ openclaw: "OpenClaw",
796
+ opencode: "OpenCode",
797
+ generic: "Generic",
798
+ };
799
+ return names[id] ?? id;
800
+ }
801
+ async function promptConflict(_agent, filePath) {
802
+ const answer = await ask(chalk.yellow(`\n ${filePath} has changes on both sides.\n` +
803
+ ` [l] Keep local [c] Use cloud [m] Merge (LLM) [s] Skip: `));
804
+ const a = answer.toLowerCase();
805
+ if (a === "l")
806
+ return "local";
807
+ if (a === "c")
808
+ return "cloud";
809
+ if (a === "m")
810
+ return "merge";
811
+ return "skip";
812
+ }
813
+ async function mergeConfigs(agent, filePath, localContent, cloudContent) {
814
+ try {
815
+ const result = await getClient().configs.merge({
816
+ local_content: localContent,
817
+ cloud_content: cloudContent,
818
+ file_path: filePath,
819
+ agent,
820
+ });
821
+ return result.merged_content;
822
+ }
823
+ catch (err) {
824
+ console.log(chalk.red(` Merge failed: ${err.message}`));
825
+ return null;
826
+ }
320
827
  }
321
828
  //# sourceMappingURL=sync.js.map