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.
- package/assets/skills/memax-memory/SKILL.md +154 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +14 -7
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/capture.d.ts +17 -0
- package/dist/commands/capture.d.ts.map +1 -0
- package/dist/commands/capture.js +60 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/delete.d.ts +1 -1
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +22 -5
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/hub.d.ts +4 -0
- package/dist/commands/hub.d.ts.map +1 -0
- package/dist/commands/hub.js +53 -0
- package/dist/commands/hub.js.map +1 -0
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +35 -8
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +32 -7
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +226 -26
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/push.d.ts +2 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +49 -11
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/recall.d.ts +1 -0
- package/dist/commands/recall.d.ts.map +1 -1
- package/dist/commands/recall.js +30 -27
- package/dist/commands/recall.js.map +1 -1
- package/dist/commands/setup-hooks.d.ts +12 -0
- package/dist/commands/setup-hooks.d.ts.map +1 -0
- package/dist/commands/setup-hooks.js +184 -0
- package/dist/commands/setup-hooks.js.map +1 -0
- package/dist/commands/setup-instructions.d.ts +21 -0
- package/dist/commands/setup-instructions.d.ts.map +1 -0
- package/dist/commands/setup-instructions.js +172 -0
- package/dist/commands/setup-instructions.js.map +1 -0
- package/dist/commands/setup-mcp.d.ts +14 -0
- package/dist/commands/setup-mcp.d.ts.map +1 -0
- package/dist/commands/setup-mcp.js +276 -0
- package/dist/commands/setup-mcp.js.map +1 -0
- package/dist/commands/setup-types.d.ts +20 -0
- package/dist/commands/setup-types.d.ts.map +1 -0
- package/dist/commands/setup-types.js +60 -0
- package/dist/commands/setup-types.js.map +1 -0
- package/dist/commands/setup.d.ts +18 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +371 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +10 -13
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/sync.d.ts +7 -2
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +614 -107
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.js +85 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +6 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +69 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/project-context.d.ts +40 -0
- package/dist/lib/project-context.d.ts.map +1 -0
- package/dist/lib/project-context.js +157 -0
- package/dist/lib/project-context.js.map +1 -0
- package/dist/lib/prompt.d.ts +7 -0
- package/dist/lib/prompt.d.ts.map +1 -0
- package/dist/lib/prompt.js +41 -0
- package/dist/lib/prompt.js.map +1 -0
- package/package.json +17 -13
- package/dist/lib/api.d.ts +0 -4
- package/dist/lib/api.d.ts.map +0 -1
- package/dist/lib/api.js +0 -95
- package/dist/lib/api.js.map +0 -1
- package/src/commands/auth.ts +0 -92
- package/src/commands/config.ts +0 -27
- package/src/commands/delete.ts +0 -20
- package/src/commands/hook.ts +0 -243
- package/src/commands/list.ts +0 -38
- package/src/commands/login.ts +0 -159
- package/src/commands/mcp.ts +0 -282
- package/src/commands/push.ts +0 -82
- package/src/commands/recall.ts +0 -160
- package/src/commands/show.ts +0 -35
- package/src/commands/sync.ts +0 -403
- package/src/index.ts +0 -167
- package/src/lib/api.ts +0 -110
- package/src/lib/config.ts +0 -61
- package/src/lib/credentials.ts +0 -42
- package/tsconfig.json +0 -9
package/dist/commands/sync.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
249
|
+
sourcePath: relPath,
|
|
250
|
+
contentType,
|
|
118
251
|
});
|
|
119
|
-
console.log(chalk.green(" +"), relPath, chalk.gray(`[${
|
|
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
|
|
315
|
+
function discoverAgentConfigs() {
|
|
183
316
|
const home = homedir();
|
|
184
317
|
const cwd = process.cwd();
|
|
318
|
+
const projectScope = getProjectScope(cwd);
|
|
185
319
|
const locations = [];
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Permission denied — skip
|
|
209
360
|
}
|
|
210
361
|
}
|
|
211
362
|
}
|
|
212
363
|
catch {
|
|
213
|
-
// Permission denied
|
|
364
|
+
// Permission denied — skip
|
|
214
365
|
}
|
|
215
366
|
}
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
// Cursor
|
|
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
|
-
|
|
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
|
-
|
|
381
|
+
/* skip */
|
|
233
382
|
}
|
|
234
383
|
}
|
|
235
|
-
// Codex
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
272
|
-
|
|
403
|
+
catch {
|
|
404
|
+
/* skip */
|
|
273
405
|
}
|
|
274
406
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
|
296
|
-
|
|
455
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
456
|
+
localConfigs.push({
|
|
457
|
+
loc,
|
|
297
458
|
content,
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|