granite-mem 0.1.4 → 0.1.5

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 (3) hide show
  1. package/README.md +38 -38
  2. package/dist/index.js +633 -50
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -81,48 +81,48 @@ npm link
81
81
  Create the default vault in `~/.granite` and start capturing:
82
82
 
83
83
  ```bash
84
- mem init
85
- mem add "Talked to Alice about local-first sync tradeoffs"
86
- mem new "Local-first sync tradeoffs" --type permanent
87
- mem list
88
- mem search "sync"
84
+ granite init
85
+ granite add "Talked to Alice about local-first sync tradeoffs"
86
+ granite new "Local-first sync tradeoffs" --type permanent
87
+ granite list
88
+ granite search "sync"
89
89
  ```
90
90
 
91
91
  Start one long-running interface when you need it:
92
92
 
93
93
  ```bash
94
- mem serve # local web UI
95
- mem mcp # MCP server for agent clients
94
+ granite serve # local web UI
95
+ granite mcp # MCP server for agent clients
96
96
  ```
97
97
 
98
- `mem new` does more than create a file. It can immediately suggest related links, tags, and the next note to create, which is the core of Granite's value loop.
98
+ `granite new` does more than create a file. It can immediately suggest related links, tags, and the next note to create, which is the core of Granite's value loop.
99
99
 
100
100
  ## Example Workflow
101
101
 
102
102
  Capture something quickly:
103
103
 
104
104
  ```bash
105
- mem add "Users want fewer note types, but stronger defaults."
105
+ granite add "Users want fewer note types, but stronger defaults."
106
106
  ```
107
107
 
108
108
  Turn it into a durable note:
109
109
 
110
110
  ```bash
111
- mem new "Strong defaults beat infinite flexibility" --type permanent
111
+ granite new "Strong defaults beat infinite flexibility" --type permanent
112
112
  ```
113
113
 
114
114
  Find connections:
115
115
 
116
116
  ```bash
117
- mem suggest-links strong-defaults-beat-infinite-flexibility
118
- mem recommend strong-defaults-beat-infinite-flexibility
119
- mem backlinks strong-defaults-beat-infinite-flexibility
117
+ granite suggest-links strong-defaults-beat-infinite-flexibility
118
+ granite recommend strong-defaults-beat-infinite-flexibility
119
+ granite backlinks strong-defaults-beat-infinite-flexibility
120
120
  ```
121
121
 
122
122
  Open the local UI:
123
123
 
124
124
  ```bash
125
- mem serve
125
+ granite serve
126
126
  ```
127
127
 
128
128
  Then browse notes, search the vault, inspect backlinks, and explore the graph locally.
@@ -172,12 +172,12 @@ This keeps the system transparent, portable, and inspectable.
172
172
  Many commands support JSON output:
173
173
 
174
174
  ```bash
175
- mem new "Sync constraints" --type permanent --json
176
- mem list --json
177
- mem show sync-constraints --json
178
- mem search "constraints" --json
179
- mem backlinks sync-constraints --json
180
- mem recommend sync-constraints --json
175
+ granite new "Sync constraints" --type permanent --json
176
+ granite list --json
177
+ granite show sync-constraints --json
178
+ granite search "constraints" --json
179
+ granite backlinks sync-constraints --json
180
+ granite recommend sync-constraints --json
181
181
  ```
182
182
 
183
183
  That makes Granite a useful substrate for local workflows, scripts, and agent memory.
@@ -189,13 +189,13 @@ Granite ships with an MCP server so LLM clients can control the vault directly t
189
189
  Start it over stdio for local MCP clients:
190
190
 
191
191
  ```bash
192
- mem mcp --vault /path/to/vault
192
+ granite mcp --vault /path/to/vault
193
193
  ```
194
194
 
195
195
  Start it over Streamable HTTP:
196
196
 
197
197
  ```bash
198
- mem mcp --transport http --host 127.0.0.1 --port 3321
198
+ granite mcp --transport http --host 127.0.0.1 --port 3321
199
199
  ```
200
200
 
201
201
  The server exposes:
@@ -208,7 +208,7 @@ Example stdio client configuration:
208
208
 
209
209
  ```json
210
210
  {
211
- "command": "mem",
211
+ "command": "granite",
212
212
  "args": ["mcp", "--vault", "/path/to/vault"]
213
213
  }
214
214
  ```
@@ -216,21 +216,21 @@ Example stdio client configuration:
216
216
  ## Commands
217
217
 
218
218
  ```bash
219
- mem init
220
- mem new <title> [--type <type>] [--json]
221
- mem add [text] [--json]
222
- mem list [--type <type>] [--json]
223
- mem edit <slug>
224
- mem open <slug>
225
- mem show <slug> [--json] [--body]
226
- mem search <query> [--json]
227
- mem backlinks <slug> [--json]
228
- mem suggest-links <slug> [--json]
229
- mem recommend <slug> [--json]
230
- mem types
231
- mem doctor
232
- mem serve [-p <port>]
233
- mem mcp [--vault <path>] [--transport <stdio|http>]
219
+ granite init
220
+ granite new <title> [--type <type>] [--json]
221
+ granite add [text] [--json]
222
+ granite list [--type <type>] [--json]
223
+ granite edit <slug>
224
+ granite open <slug>
225
+ granite show <slug> [--json] [--body]
226
+ granite search <query> [--json]
227
+ granite backlinks <slug> [--json]
228
+ granite suggest-links <slug> [--json]
229
+ granite recommend <slug> [--json]
230
+ granite types
231
+ granite doctor
232
+ granite serve [-p <port>]
233
+ granite mcp [--vault <path>] [--transport <stdio|http>]
234
234
  ```
235
235
 
236
236
  ## Development
package/dist/index.js CHANGED
@@ -109,7 +109,7 @@ function writeDefaultConfig(dir) {
109
109
  function loadConfig(vaultRoot) {
110
110
  const configPath = path.join(vaultRoot, CONFIG_FILENAME);
111
111
  if (!fs.existsSync(configPath)) {
112
- throw new Error(`No ${CONFIG_FILENAME} found in ${vaultRoot}. Run "mem init" first.`);
112
+ throw new Error(`No ${CONFIG_FILENAME} found in ${vaultRoot}. Run "granite init" first.`);
113
113
  }
114
114
  const raw = fs.readFileSync(configPath, "utf-8");
115
115
  const parsed = yaml.load(raw);
@@ -143,7 +143,7 @@ function findVaultRoot(from) {
143
143
  function requireVaultRoot(from) {
144
144
  const root = findVaultRoot(from);
145
145
  if (!root) {
146
- throw new Error(`No Granite vault found. Run "mem init" to create ${getDefaultVaultRoot()}.`);
146
+ throw new Error(`No Granite vault found. Run "granite init" to create ${getDefaultVaultRoot()}.`);
147
147
  }
148
148
  return root;
149
149
  }
@@ -180,11 +180,11 @@ function initVault(dir = getDefaultVaultRoot()) {
180
180
  console.log(` ${typeConfig.folder}/ (${name})`);
181
181
  }
182
182
  console.log("");
183
- console.log('Next: mem new "My first note" --type fleeting');
183
+ console.log('Next: granite new "My first note" --type fleeting');
184
184
  }
185
185
 
186
186
  // src/commands/new.ts
187
- import fs6 from "fs";
187
+ import fs10 from "fs";
188
188
 
189
189
  // src/core/note.ts
190
190
  import fs4 from "fs";
@@ -1200,6 +1200,489 @@ function parseJsonArray(raw) {
1200
1200
  }
1201
1201
  }
1202
1202
 
1203
+ // src/core/sync/manager.ts
1204
+ import fs9 from "fs";
1205
+ import path9 from "path";
1206
+
1207
+ // src/core/sync/client.ts
1208
+ var SyncClient = class {
1209
+ baseUrl;
1210
+ apiKey;
1211
+ timeoutMs;
1212
+ constructor(config, timeoutMs = 5e3) {
1213
+ this.baseUrl = config.server.replace(/\/$/, "");
1214
+ this.apiKey = config.api_key;
1215
+ this.timeoutMs = timeoutMs;
1216
+ }
1217
+ async request(method, endpoint, body) {
1218
+ const controller = new AbortController();
1219
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1220
+ try {
1221
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
1222
+ method,
1223
+ headers: {
1224
+ "Authorization": `Bearer ${this.apiKey}`,
1225
+ "Content-Type": "application/json"
1226
+ },
1227
+ body: body ? JSON.stringify(body) : void 0,
1228
+ signal: controller.signal
1229
+ });
1230
+ if (!response.ok) {
1231
+ const text = await response.text().catch(() => "");
1232
+ throw new SyncApiError(response.status, text || response.statusText);
1233
+ }
1234
+ return await response.json();
1235
+ } finally {
1236
+ clearTimeout(timer);
1237
+ }
1238
+ }
1239
+ async push(payload) {
1240
+ return this.request("POST", "/v1/sync/push", payload);
1241
+ }
1242
+ async pull(sinceSeq, deviceId) {
1243
+ return this.request("GET", `/v1/sync/pull?since_seq=${sinceSeq}&device_id=${deviceId}`);
1244
+ }
1245
+ async quickPull(sinceSeq, deviceId, timeoutMs = 200) {
1246
+ const oldTimeout = this.timeoutMs;
1247
+ this.timeoutMs = timeoutMs;
1248
+ try {
1249
+ return await this.pull(sinceSeq, deviceId);
1250
+ } catch {
1251
+ return null;
1252
+ } finally {
1253
+ this.timeoutMs = oldTimeout;
1254
+ }
1255
+ }
1256
+ async listDevices() {
1257
+ return this.request("GET", "/v1/sync/devices");
1258
+ }
1259
+ async ping() {
1260
+ try {
1261
+ await this.request("GET", "/v1/sync/ping");
1262
+ return true;
1263
+ } catch {
1264
+ return false;
1265
+ }
1266
+ }
1267
+ };
1268
+ var SyncApiError = class extends Error {
1269
+ constructor(statusCode, body) {
1270
+ super(`Sync API error ${statusCode}: ${body}`);
1271
+ this.statusCode = statusCode;
1272
+ this.body = body;
1273
+ this.name = "SyncApiError";
1274
+ }
1275
+ };
1276
+
1277
+ // src/core/sync/device.ts
1278
+ import fs6 from "fs";
1279
+ import path6 from "path";
1280
+ import os2 from "os";
1281
+ import { v4 as uuidv42 } from "uuid";
1282
+ var DEVICE_FILE = "device.json";
1283
+ function getDeviceFilePath(vaultRoot) {
1284
+ const graniteDir = path6.join(vaultRoot, ".granite");
1285
+ fs6.mkdirSync(graniteDir, { recursive: true });
1286
+ return path6.join(graniteDir, DEVICE_FILE);
1287
+ }
1288
+ function getOrCreateDeviceId(vaultRoot, deviceName) {
1289
+ const filePath = getDeviceFilePath(vaultRoot);
1290
+ if (fs6.existsSync(filePath)) {
1291
+ const raw = fs6.readFileSync(filePath, "utf-8");
1292
+ return JSON.parse(raw);
1293
+ }
1294
+ const info = {
1295
+ device_id: uuidv42(),
1296
+ device_name: deviceName || os2.hostname(),
1297
+ created: (/* @__PURE__ */ new Date()).toISOString()
1298
+ };
1299
+ fs6.writeFileSync(filePath, JSON.stringify(info, null, 2), "utf-8");
1300
+ return info;
1301
+ }
1302
+ function getDeviceId(vaultRoot) {
1303
+ return getOrCreateDeviceId(vaultRoot).device_id;
1304
+ }
1305
+
1306
+ // src/core/sync/changelog.ts
1307
+ import Database2 from "better-sqlite3";
1308
+ import fs7 from "fs";
1309
+ import path7 from "path";
1310
+ import crypto from "crypto";
1311
+ var SYNC_DB_FILE = "sync.db";
1312
+ var SYNC_SCHEMA_SQL = `
1313
+ CREATE TABLE IF NOT EXISTS changelog (
1314
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
1315
+ note_id TEXT NOT NULL,
1316
+ operation TEXT NOT NULL,
1317
+ timestamp TEXT NOT NULL,
1318
+ device_id TEXT NOT NULL,
1319
+ checksum TEXT NOT NULL,
1320
+ synced INTEGER NOT NULL DEFAULT 0
1321
+ );
1322
+
1323
+ CREATE INDEX IF NOT EXISTS idx_changelog_note_id ON changelog(note_id);
1324
+ CREATE INDEX IF NOT EXISTS idx_changelog_synced ON changelog(synced);
1325
+
1326
+ CREATE TABLE IF NOT EXISTS sync_meta (
1327
+ key TEXT PRIMARY KEY,
1328
+ value TEXT
1329
+ );
1330
+ `;
1331
+ function getSyncDbPath(vaultRoot) {
1332
+ const graniteDir = path7.join(vaultRoot, ".granite");
1333
+ fs7.mkdirSync(graniteDir, { recursive: true });
1334
+ return path7.join(graniteDir, SYNC_DB_FILE);
1335
+ }
1336
+ function openSyncDb(vaultRoot) {
1337
+ const dbPath = getSyncDbPath(vaultRoot);
1338
+ const db = new Database2(dbPath);
1339
+ db.pragma("journal_mode = WAL");
1340
+ db.exec(SYNC_SCHEMA_SQL);
1341
+ return db;
1342
+ }
1343
+ function computeChecksum(content) {
1344
+ return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
1345
+ }
1346
+ function recordChange(db, noteId, operation, deviceId, fileContent) {
1347
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1348
+ const checksum = computeChecksum(fileContent);
1349
+ const stmt = db.prepare(`
1350
+ INSERT INTO changelog (note_id, operation, timestamp, device_id, checksum, synced)
1351
+ VALUES (?, ?, ?, ?, ?, 0)
1352
+ `);
1353
+ const result = stmt.run(noteId, operation, now, deviceId, checksum);
1354
+ return {
1355
+ seq: result.lastInsertRowid,
1356
+ note_id: noteId,
1357
+ operation,
1358
+ timestamp: now,
1359
+ device_id: deviceId,
1360
+ checksum,
1361
+ synced: false
1362
+ };
1363
+ }
1364
+ function getPendingChanges(db) {
1365
+ return db.prepare("SELECT * FROM changelog WHERE synced = 0 ORDER BY seq ASC").all();
1366
+ }
1367
+ function markChangesSynced(db, upToSeq) {
1368
+ db.prepare("UPDATE changelog SET synced = 1 WHERE seq <= ?").run(upToSeq);
1369
+ }
1370
+ function getLastServerSeq(db) {
1371
+ const row = db.prepare("SELECT value FROM sync_meta WHERE key = 'last_server_seq'").get();
1372
+ return row ? Number(row.value) : 0;
1373
+ }
1374
+ function setLastServerSeq(db, seq) {
1375
+ db.prepare("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_server_seq', ?)").run(String(seq));
1376
+ }
1377
+ function getLastSyncTime(db) {
1378
+ const row = db.prepare("SELECT value FROM sync_meta WHERE key = 'last_sync'").get();
1379
+ return row?.value ?? null;
1380
+ }
1381
+ function setLastSyncTime(db) {
1382
+ db.prepare("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_sync', ?)").run((/* @__PURE__ */ new Date()).toISOString());
1383
+ }
1384
+ function getPendingCount(db) {
1385
+ const row = db.prepare("SELECT COUNT(*) as c FROM changelog WHERE synced = 0").get();
1386
+ return row.c;
1387
+ }
1388
+
1389
+ // src/core/sync/conflict.ts
1390
+ import fs8 from "fs";
1391
+ import path8 from "path";
1392
+ var CONFLICTS_DIR = ".granite/conflicts";
1393
+ function getConflictsDir(vaultRoot) {
1394
+ const dir = path8.join(vaultRoot, CONFLICTS_DIR);
1395
+ fs8.mkdirSync(dir, { recursive: true });
1396
+ return dir;
1397
+ }
1398
+ function detectConflict(localNote, remoteChange) {
1399
+ if (remoteChange.operation === "delete") return false;
1400
+ if (!remoteChange.frontmatter) return false;
1401
+ const localModified = new Date(localNote.frontmatter.modified).getTime();
1402
+ const remoteModified = new Date(remoteChange.frontmatter.modified).getTime();
1403
+ return localModified !== remoteModified && remoteChange.checksum !== "";
1404
+ }
1405
+ function resolveConflict(vaultRoot, localNote, remoteChange, deviceId) {
1406
+ if (!remoteChange.frontmatter || remoteChange.body === void 0) return null;
1407
+ const localModified = new Date(localNote.frontmatter.modified).getTime();
1408
+ const remoteModified = new Date(remoteChange.frontmatter.modified).getTime();
1409
+ const remoteWins = remoteModified > localModified;
1410
+ if (remoteWins) {
1411
+ const conflictFile2 = saveConflictBackup(vaultRoot, localNote, deviceId);
1412
+ return {
1413
+ note_id: localNote.frontmatter.id,
1414
+ local_modified: localNote.frontmatter.modified,
1415
+ remote_modified: remoteChange.frontmatter.modified,
1416
+ resolved: true,
1417
+ conflict_file: conflictFile2
1418
+ };
1419
+ }
1420
+ const conflictFile = saveRemoteConflictBackup(vaultRoot, remoteChange);
1421
+ return {
1422
+ note_id: localNote.frontmatter.id,
1423
+ local_modified: localNote.frontmatter.modified,
1424
+ remote_modified: remoteChange.frontmatter.modified,
1425
+ resolved: true,
1426
+ conflict_file: conflictFile
1427
+ };
1428
+ }
1429
+ function saveConflictBackup(vaultRoot, note, deviceId) {
1430
+ const conflictsDir = getConflictsDir(vaultRoot);
1431
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1432
+ const filename = `${note.frontmatter.id}_${deviceId}_${timestamp}.md`;
1433
+ const filePath = path8.join(conflictsDir, filename);
1434
+ const content = serializeFrontmatter(note.frontmatter, note.body);
1435
+ fs8.writeFileSync(filePath, content, "utf-8");
1436
+ return filePath;
1437
+ }
1438
+ function saveRemoteConflictBackup(vaultRoot, change) {
1439
+ const conflictsDir = getConflictsDir(vaultRoot);
1440
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1441
+ const filename = `${change.note_id}_remote_${timestamp}.md`;
1442
+ const filePath = path8.join(conflictsDir, filename);
1443
+ if (change.frontmatter && change.body !== void 0) {
1444
+ const content = serializeFrontmatter(change.frontmatter, change.body);
1445
+ fs8.writeFileSync(filePath, content, "utf-8");
1446
+ }
1447
+ return filePath;
1448
+ }
1449
+ function listConflicts(vaultRoot) {
1450
+ const conflictsDir = path8.join(vaultRoot, CONFLICTS_DIR);
1451
+ if (!fs8.existsSync(conflictsDir)) return [];
1452
+ return fs8.readdirSync(conflictsDir).filter((f) => f.endsWith(".md")).map((f) => path8.join(conflictsDir, f));
1453
+ }
1454
+ function clearResolvedConflicts(vaultRoot) {
1455
+ const files = listConflicts(vaultRoot);
1456
+ for (const f of files) {
1457
+ fs8.unlinkSync(f);
1458
+ }
1459
+ return files.length;
1460
+ }
1461
+
1462
+ // src/core/sync/manager.ts
1463
+ var SyncManager = class {
1464
+ vaultRoot;
1465
+ config;
1466
+ syncConfig;
1467
+ client;
1468
+ deviceId;
1469
+ constructor(vaultRoot, config) {
1470
+ if (!config.sync?.enabled) {
1471
+ throw new Error("Sync is not enabled. Add a sync section to granite.yml.");
1472
+ }
1473
+ this.vaultRoot = vaultRoot;
1474
+ this.config = config;
1475
+ this.syncConfig = config.sync;
1476
+ this.client = new SyncClient(this.syncConfig);
1477
+ this.deviceId = getDeviceId(vaultRoot);
1478
+ }
1479
+ /**
1480
+ * Track a local mutation and push it in the background (fire-and-forget).
1481
+ * This is the hook called after every note create/update/delete.
1482
+ */
1483
+ trackAndPush(note, operation) {
1484
+ const db = openSyncDb(this.vaultRoot);
1485
+ try {
1486
+ const content = fs9.existsSync(note.filepath) ? fs9.readFileSync(note.filepath, "utf-8") : "";
1487
+ recordChange(db, note.frontmatter.id, operation, this.deviceId, content);
1488
+ } finally {
1489
+ db.close();
1490
+ }
1491
+ this.push().catch(() => {
1492
+ });
1493
+ }
1494
+ /**
1495
+ * Push all pending local changes to the relay server.
1496
+ */
1497
+ async push() {
1498
+ const db = openSyncDb(this.vaultRoot);
1499
+ try {
1500
+ const pending = getPendingChanges(db);
1501
+ if (pending.length === 0) {
1502
+ return { pushed: 0, server_seq: getLastServerSeq(db) };
1503
+ }
1504
+ const changes = [];
1505
+ for (const entry of pending) {
1506
+ const note = this.findNoteById(entry.note_id);
1507
+ changes.push({
1508
+ note_id: entry.note_id,
1509
+ operation: entry.operation,
1510
+ timestamp: entry.timestamp,
1511
+ checksum: entry.checksum,
1512
+ slug: note?.slug ?? "",
1513
+ frontmatter: note?.frontmatter,
1514
+ body: note?.body
1515
+ });
1516
+ }
1517
+ const lastSeq = getLastServerSeq(db);
1518
+ const result = await this.client.push({
1519
+ device_id: this.deviceId,
1520
+ last_server_seq: lastSeq,
1521
+ changes
1522
+ });
1523
+ const maxSeq = Math.max(...pending.map((p) => p.seq));
1524
+ markChangesSynced(db, maxSeq);
1525
+ setLastServerSeq(db, result.server_seq);
1526
+ setLastSyncTime(db);
1527
+ return { pushed: result.accepted, server_seq: result.server_seq };
1528
+ } finally {
1529
+ db.close();
1530
+ }
1531
+ }
1532
+ /**
1533
+ * Pull remote changes and apply them locally.
1534
+ */
1535
+ async pull() {
1536
+ const db = openSyncDb(this.vaultRoot);
1537
+ try {
1538
+ const lastSeq = getLastServerSeq(db);
1539
+ const response = await this.client.pull(lastSeq, this.deviceId);
1540
+ let applied = 0;
1541
+ let conflicts = 0;
1542
+ for (const change of response.changes) {
1543
+ const result = this.applyRemoteChange(change);
1544
+ if (result === "applied") applied++;
1545
+ if (result === "conflict") conflicts++;
1546
+ }
1547
+ setLastServerSeq(db, response.server_seq);
1548
+ setLastSyncTime(db);
1549
+ return { applied, conflicts };
1550
+ } finally {
1551
+ db.close();
1552
+ }
1553
+ }
1554
+ /**
1555
+ * Quick pull with a tight timeout — used before read commands.
1556
+ * Returns silently if server is unreachable.
1557
+ */
1558
+ async quickPull() {
1559
+ const db = openSyncDb(this.vaultRoot);
1560
+ try {
1561
+ const lastSeq = getLastServerSeq(db);
1562
+ const response = await this.client.quickPull(lastSeq, this.deviceId);
1563
+ if (!response) return;
1564
+ for (const change of response.changes) {
1565
+ this.applyRemoteChange(change);
1566
+ }
1567
+ setLastServerSeq(db, response.server_seq);
1568
+ setLastSyncTime(db);
1569
+ } finally {
1570
+ db.close();
1571
+ }
1572
+ }
1573
+ /**
1574
+ * Full bidirectional sync: push then pull.
1575
+ */
1576
+ async sync() {
1577
+ const pushResult = await this.push();
1578
+ const pullResult = await this.pull();
1579
+ return {
1580
+ pushed: pushResult.pushed,
1581
+ pulled: pullResult.applied,
1582
+ conflicts: pullResult.conflicts
1583
+ };
1584
+ }
1585
+ /**
1586
+ * Get current sync status.
1587
+ */
1588
+ status() {
1589
+ const db = openSyncDb(this.vaultRoot);
1590
+ try {
1591
+ const deviceInfo = getOrCreateDeviceId(this.vaultRoot);
1592
+ return {
1593
+ device_id: deviceInfo.device_id,
1594
+ device_name: deviceInfo.device_name,
1595
+ last_sync: getLastSyncTime(db),
1596
+ pending_changes: getPendingCount(db),
1597
+ server_seq: getLastServerSeq(db)
1598
+ };
1599
+ } finally {
1600
+ db.close();
1601
+ }
1602
+ }
1603
+ /**
1604
+ * List connected devices.
1605
+ */
1606
+ async devices() {
1607
+ return this.client.listDevices();
1608
+ }
1609
+ /**
1610
+ * List unresolved conflict files.
1611
+ */
1612
+ conflicts() {
1613
+ return listConflicts(this.vaultRoot);
1614
+ }
1615
+ applyRemoteChange(change) {
1616
+ switch (change.operation) {
1617
+ case "create":
1618
+ return this.applyRemoteCreate(change);
1619
+ case "update":
1620
+ return this.applyRemoteUpdate(change);
1621
+ case "delete":
1622
+ return this.applyRemoteDelete(change);
1623
+ default:
1624
+ return "skipped";
1625
+ }
1626
+ }
1627
+ applyRemoteCreate(change) {
1628
+ if (!change.frontmatter || change.body === void 0) return "skipped";
1629
+ const existing = this.findNoteById(change.note_id);
1630
+ if (existing) return "skipped";
1631
+ const typeConfig = this.config.note_types[change.frontmatter.type];
1632
+ if (!typeConfig) return "skipped";
1633
+ const folder = path9.join(this.vaultRoot, typeConfig.folder);
1634
+ fs9.mkdirSync(folder, { recursive: true });
1635
+ const slug = change.slug || change.note_id;
1636
+ const filepath = path9.join(folder, `${slug}.md`);
1637
+ const content = serializeFrontmatter(change.frontmatter, change.body);
1638
+ fs9.writeFileSync(filepath, content, "utf-8");
1639
+ return "applied";
1640
+ }
1641
+ applyRemoteUpdate(change) {
1642
+ if (!change.frontmatter || change.body === void 0) return "skipped";
1643
+ const localNote = this.findNoteById(change.note_id);
1644
+ if (!localNote) {
1645
+ return this.applyRemoteCreate(change);
1646
+ }
1647
+ const localContent = fs9.readFileSync(localNote.filepath, "utf-8");
1648
+ const localChecksum = computeChecksum(localContent);
1649
+ if (localChecksum === change.checksum) return "skipped";
1650
+ if (detectConflict(localNote, change)) {
1651
+ resolveConflict(this.vaultRoot, localNote, change, this.deviceId);
1652
+ const localTime = new Date(localNote.frontmatter.modified).getTime();
1653
+ const remoteTime = new Date(change.frontmatter.modified).getTime();
1654
+ if (remoteTime > localTime) {
1655
+ const content2 = serializeFrontmatter(change.frontmatter, change.body);
1656
+ fs9.writeFileSync(localNote.filepath, content2, "utf-8");
1657
+ }
1658
+ return "conflict";
1659
+ }
1660
+ const content = serializeFrontmatter(change.frontmatter, change.body);
1661
+ fs9.writeFileSync(localNote.filepath, content, "utf-8");
1662
+ return "applied";
1663
+ }
1664
+ applyRemoteDelete(change) {
1665
+ const localNote = this.findNoteById(change.note_id);
1666
+ if (!localNote) return "skipped";
1667
+ if (fs9.existsSync(localNote.filepath)) {
1668
+ fs9.unlinkSync(localNote.filepath);
1669
+ }
1670
+ return "applied";
1671
+ }
1672
+ findNoteById(noteId) {
1673
+ const allNotes = listNotes(this.vaultRoot, this.config);
1674
+ return allNotes.find((n) => n.frontmatter.id === noteId) ?? null;
1675
+ }
1676
+ };
1677
+ function getSyncManager(vaultRoot, config) {
1678
+ if (!config.sync?.enabled) return null;
1679
+ try {
1680
+ return new SyncManager(vaultRoot, config);
1681
+ } catch {
1682
+ return null;
1683
+ }
1684
+ }
1685
+
1203
1686
  // src/commands/new.ts
1204
1687
  function newNote(title, options) {
1205
1688
  const vaultRoot = requireVaultRoot();
@@ -1211,17 +1694,19 @@ function newNote(title, options) {
1211
1694
  if (options.source || options.status) {
1212
1695
  if (options.source) validateSource(options.source);
1213
1696
  if (options.status) validateStatus(options.status);
1214
- const raw = fs6.readFileSync(note.filepath, "utf-8");
1697
+ const raw = fs10.readFileSync(note.filepath, "utf-8");
1215
1698
  const { frontmatter, body } = parseFrontmatter(raw);
1216
1699
  if (options.source) frontmatter.source = options.source;
1217
1700
  if (options.status) frontmatter.status = options.status;
1218
- fs6.writeFileSync(note.filepath, serializeFrontmatter(frontmatter, body), "utf-8");
1701
+ fs10.writeFileSync(note.filepath, serializeFrontmatter(frontmatter, body), "utf-8");
1219
1702
  note.frontmatter = frontmatter;
1220
1703
  }
1221
1704
  const recommendationStrategy = typeConfig?.slug_format === "date" ? "incremental" : "rebuild";
1222
1705
  const recommendations = recommendNote(vaultRoot, config, note, { strategy: recommendationStrategy });
1223
1706
  const lines = note.body.split("\n").length;
1224
1707
  const overLimit = typeConfig && typeConfig.line_limit && lines > typeConfig.line_limit;
1708
+ const sync = getSyncManager(vaultRoot, config);
1709
+ sync?.trackAndPush(note, "create");
1225
1710
  if (options.json) {
1226
1711
  console.log(jsonSuccess({
1227
1712
  slug: note.slug,
@@ -1259,7 +1744,7 @@ function newNote(title, options) {
1259
1744
  }
1260
1745
 
1261
1746
  // src/commands/add.ts
1262
- import fs7 from "fs";
1747
+ import fs11 from "fs";
1263
1748
  function addNote(text, options = {}) {
1264
1749
  const vaultRoot = requireVaultRoot();
1265
1750
  const config = loadConfig(vaultRoot);
@@ -1268,9 +1753,9 @@ function addNote(text, options = {}) {
1268
1753
  if (text) {
1269
1754
  content = text;
1270
1755
  } else if (!process.stdin.isTTY) {
1271
- content = fs7.readFileSync(0, "utf-8").trim();
1756
+ content = fs11.readFileSync(0, "utf-8").trim();
1272
1757
  } else {
1273
- console.error('Usage: mem add "some text" or echo "text" | mem add');
1758
+ console.error('Usage: granite add "some text" or echo "text" | granite add');
1274
1759
  process.exit(1);
1275
1760
  }
1276
1761
  if (!content) {
@@ -1281,6 +1766,8 @@ function addNote(text, options = {}) {
1281
1766
  const title = firstLine.length > 60 ? firstLine.slice(0, 60).trim() + "..." : firstLine;
1282
1767
  const note = createNote(vaultRoot, config, typeName, title, content + "\n");
1283
1768
  const recommendations = recommendNote(vaultRoot, config, note, { strategy: "incremental" });
1769
+ const sync = getSyncManager(vaultRoot, config);
1770
+ sync?.trackAndPush(note, "create");
1284
1771
  if (options.json) {
1285
1772
  console.log(jsonSuccess({
1286
1773
  slug: note.slug,
@@ -1317,7 +1804,7 @@ function openNote(slug) {
1317
1804
  }
1318
1805
 
1319
1806
  // src/commands/show.ts
1320
- import fs8 from "fs";
1807
+ import fs12 from "fs";
1321
1808
  function showCommand(slug, options) {
1322
1809
  const vaultRoot = requireVaultRoot();
1323
1810
  const config = loadConfig(vaultRoot);
@@ -1353,7 +1840,7 @@ function showCommand(slug, options) {
1353
1840
  console.log(`# ${note.frontmatter.title} (${note.slug})`);
1354
1841
  console.log(`# type: ${note.frontmatter.type} | modified: ${note.frontmatter.modified.slice(0, 10)}`);
1355
1842
  console.log("");
1356
- const content = fs8.readFileSync(note.filepath, "utf-8");
1843
+ const content = fs12.readFileSync(note.filepath, "utf-8");
1357
1844
  console.log(content);
1358
1845
  }
1359
1846
 
@@ -1511,14 +1998,14 @@ function recommendCommand(slug, options) {
1511
1998
  }
1512
1999
 
1513
2000
  // src/core/doctor.ts
1514
- import fs9 from "fs";
1515
- import path6 from "path";
2001
+ import fs13 from "fs";
2002
+ import path10 from "path";
1516
2003
  function runDoctor(vaultRoot, config, db) {
1517
2004
  const issues = [];
1518
2005
  const notes = listNotes(vaultRoot, config);
1519
2006
  for (const [name, typeConfig] of Object.entries(config.note_types)) {
1520
- const folder = path6.join(vaultRoot, typeConfig.folder);
1521
- if (!fs9.existsSync(folder)) {
2007
+ const folder = path10.join(vaultRoot, typeConfig.folder);
2008
+ if (!fs13.existsSync(folder)) {
1522
2009
  issues.push({
1523
2010
  level: "error",
1524
2011
  file: `granite.yml`,
@@ -1653,8 +2140,8 @@ ${errors.length} error(s), ${warnings.length} warning(s), ${infos.length} info(s
1653
2140
  // src/web/server.ts
1654
2141
  import { Hono } from "hono";
1655
2142
  import { serve } from "@hono/node-server";
1656
- import path7 from "path";
1657
- import fs10 from "fs";
2143
+ import path11 from "path";
2144
+ import fs14 from "fs";
1658
2145
  function createApp(vaultRoot, config) {
1659
2146
  const app = new Hono();
1660
2147
  app.get("/api/notes", (c) => {
@@ -1723,6 +2210,8 @@ function createApp(vaultRoot, config) {
1723
2210
  }
1724
2211
  try {
1725
2212
  const note = createNote(vaultRoot, config, type, title, noteBody || void 0);
2213
+ const sync = getSyncManager(vaultRoot, config);
2214
+ sync?.trackAndPush(note, "create");
1726
2215
  return c.json({
1727
2216
  slug: note.slug,
1728
2217
  title: note.frontmatter.title,
@@ -1740,17 +2229,17 @@ function createApp(vaultRoot, config) {
1740
2229
  db.close();
1741
2230
  return c.json({ nodes, edges });
1742
2231
  });
1743
- const publicDir = path7.join(import.meta.dirname, "public");
2232
+ const publicDir = path11.join(import.meta.dirname, "public");
1744
2233
  app.get("/*", (c) => {
1745
2234
  const reqPath = c.req.path === "/" ? "/index.html" : c.req.path;
1746
- const filePath = path7.join(publicDir, reqPath);
1747
- if (!fs10.existsSync(filePath)) {
1748
- const indexPath = path7.join(publicDir, "index.html");
1749
- const html = fs10.readFileSync(indexPath, "utf-8");
2235
+ const filePath = path11.join(publicDir, reqPath);
2236
+ if (!fs14.existsSync(filePath)) {
2237
+ const indexPath = path11.join(publicDir, "index.html");
2238
+ const html = fs14.readFileSync(indexPath, "utf-8");
1750
2239
  return c.html(html);
1751
2240
  }
1752
- const content = fs10.readFileSync(filePath);
1753
- const ext = path7.extname(filePath);
2241
+ const content = fs14.readFileSync(filePath);
2242
+ const ext = path11.extname(filePath);
1754
2243
  const mimeTypes = {
1755
2244
  ".html": "text/html",
1756
2245
  ".css": "text/css",
@@ -1796,7 +2285,7 @@ function typesCommand() {
1796
2285
  }
1797
2286
  console.log(`Default type: ${config.defaults.note_type}`);
1798
2287
  console.log(`
1799
- Usage: mem new <type> <title>`);
2288
+ Usage: granite new <type> <title>`);
1800
2289
  }
1801
2290
 
1802
2291
  // src/commands/list.ts
@@ -1886,7 +2375,7 @@ ${notes.length} note(s)`);
1886
2375
  }
1887
2376
 
1888
2377
  // src/commands/edit.ts
1889
- import fs11 from "fs";
2378
+ import fs15 from "fs";
1890
2379
  import { execSync as execSync2 } from "child_process";
1891
2380
  function editCommand(slug, options) {
1892
2381
  const vaultRoot = requireVaultRoot();
@@ -1898,7 +2387,7 @@ function editCommand(slug, options) {
1898
2387
  }
1899
2388
  const hasFlags = options.body !== void 0 || options.append !== void 0 || options.title !== void 0 || options.tag !== void 0 || options.alias !== void 0 || options.status !== void 0 || options.source !== void 0;
1900
2389
  if (hasFlags) {
1901
- let { frontmatter, body } = parseFrontmatter(fs11.readFileSync(note.filepath, "utf-8"));
2390
+ let { frontmatter, body } = parseFrontmatter(fs15.readFileSync(note.filepath, "utf-8"));
1902
2391
  if (options.title) {
1903
2392
  frontmatter.title = options.title;
1904
2393
  }
@@ -1929,24 +2418,30 @@ function editCommand(slug, options) {
1929
2418
  body = body.trimEnd() + "\n" + options.append + "\n";
1930
2419
  }
1931
2420
  frontmatter.modified = (/* @__PURE__ */ new Date()).toISOString();
1932
- fs11.writeFileSync(note.filepath, serializeFrontmatter(frontmatter, body), "utf-8");
2421
+ fs15.writeFileSync(note.filepath, serializeFrontmatter(frontmatter, body), "utf-8");
2422
+ const sync = getSyncManager(vaultRoot, config);
2423
+ const updatedForSync = readNote(note.filepath);
2424
+ sync?.trackAndPush(updatedForSync, "update");
1933
2425
  console.log(note.filepath);
1934
2426
  const recommendationStrategy = options.title !== void 0 || options.alias !== void 0 ? "rebuild" : "incremental";
1935
2427
  printRecommendations(vaultRoot, config, note.filepath, recommendationStrategy);
1936
2428
  } else {
1937
2429
  const editor = process.env.EDITOR || "vi";
1938
- const statBefore = fs11.statSync(note.filepath).mtimeMs;
2430
+ const statBefore = fs15.statSync(note.filepath).mtimeMs;
1939
2431
  try {
1940
2432
  execSync2(`${editor} "${note.filepath}"`, { stdio: "inherit" });
1941
2433
  } catch {
1942
2434
  console.error(`Failed to open editor: ${editor}`);
1943
2435
  process.exit(1);
1944
2436
  }
1945
- const statAfter = fs11.statSync(note.filepath).mtimeMs;
2437
+ const statAfter = fs15.statSync(note.filepath).mtimeMs;
1946
2438
  if (statAfter !== statBefore) {
1947
- const { frontmatter, body } = parseFrontmatter(fs11.readFileSync(note.filepath, "utf-8"));
2439
+ const { frontmatter, body } = parseFrontmatter(fs15.readFileSync(note.filepath, "utf-8"));
1948
2440
  frontmatter.modified = (/* @__PURE__ */ new Date()).toISOString();
1949
- fs11.writeFileSync(note.filepath, serializeFrontmatter(frontmatter, body), "utf-8");
2441
+ fs15.writeFileSync(note.filepath, serializeFrontmatter(frontmatter, body), "utf-8");
2442
+ const sync = getSyncManager(vaultRoot, config);
2443
+ const updatedForSync = readNote(note.filepath);
2444
+ sync?.trackAndPush(updatedForSync, "update");
1950
2445
  console.log(`Updated: ${note.filepath}`);
1951
2446
  printRecommendations(vaultRoot, config, note.filepath, "rebuild");
1952
2447
  }
@@ -1965,7 +2460,7 @@ function printRecommendations(vaultRoot, config, filepath, strategy) {
1965
2460
  }
1966
2461
 
1967
2462
  // src/commands/mcp.ts
1968
- import path9 from "path";
2463
+ import path13 from "path";
1969
2464
 
1970
2465
  // src/mcp/server.ts
1971
2466
  import { serve as serve2 } from "@hono/node-server";
@@ -2280,7 +2775,7 @@ function registerTools(server, runtime) {
2280
2775
  });
2281
2776
  server.registerTool("granite_capture_note", {
2282
2777
  title: "Capture Granite Note",
2283
- description: "Quick-capture a note from free-form text, similar to mem add.",
2778
+ description: "Quick-capture a note from free-form text, similar to granite add.",
2284
2779
  inputSchema: {
2285
2780
  text: z.string().describe("Raw capture text."),
2286
2781
  type: z.string().optional().describe("Optional note type override. Defaults to the vault default type."),
@@ -2663,8 +3158,8 @@ function corsHeaders(origin, allowedOrigins) {
2663
3158
  }
2664
3159
 
2665
3160
  // src/mcp/runtime.ts
2666
- import fs12 from "fs";
2667
- import path8 from "path";
3161
+ import fs16 from "fs";
3162
+ import path12 from "path";
2668
3163
  var GraniteMcpRuntime = class {
2669
3164
  vaultRoot;
2670
3165
  config;
@@ -2673,7 +3168,7 @@ var GraniteMcpRuntime = class {
2673
3168
  lastSignature;
2674
3169
  lastIndexCheckAt = 0;
2675
3170
  constructor(vaultRoot, options = {}) {
2676
- this.vaultRoot = path8.resolve(vaultRoot);
3171
+ this.vaultRoot = path12.resolve(vaultRoot);
2677
3172
  this.config = loadConfig(this.vaultRoot);
2678
3173
  this.db = openDatabase(this.vaultRoot);
2679
3174
  this.indexCheckIntervalMs = options.indexCheckIntervalMs ?? 1500;
@@ -2826,7 +3321,7 @@ var GraniteMcpRuntime = class {
2826
3321
  };
2827
3322
  }
2828
3323
  readVaultConfigRaw() {
2829
- return fs12.readFileSync(path8.join(this.vaultRoot, CONFIG_FILENAME), "utf-8");
3324
+ return fs16.readFileSync(path12.join(this.vaultRoot, CONFIG_FILENAME), "utf-8");
2830
3325
  }
2831
3326
  readVaultTypesJson() {
2832
3327
  return JSON.stringify({
@@ -2839,7 +3334,7 @@ var GraniteMcpRuntime = class {
2839
3334
  }
2840
3335
  readNoteMarkdown(slug) {
2841
3336
  const note = this.requireNote(slug);
2842
- return fs12.readFileSync(note.filepath, "utf-8");
3337
+ return fs16.readFileSync(note.filepath, "utf-8");
2843
3338
  }
2844
3339
  completeSlugs(prefix = "") {
2845
3340
  const normalizedPrefix = prefix.toLowerCase();
@@ -2900,7 +3395,7 @@ var GraniteMcpRuntime = class {
2900
3395
  };
2901
3396
  }
2902
3397
  applyMutations(filepath, input) {
2903
- const raw = fs12.readFileSync(filepath, "utf-8");
3398
+ const raw = fs16.readFileSync(filepath, "utf-8");
2904
3399
  const { frontmatter, body: existingBody } = parseFrontmatter(raw);
2905
3400
  let body = existingBody;
2906
3401
  if (input.title !== void 0) {
@@ -2929,7 +3424,7 @@ ${input.append}
2929
3424
  `;
2930
3425
  }
2931
3426
  frontmatter.modified = (/* @__PURE__ */ new Date()).toISOString();
2932
- fs12.writeFileSync(filepath, serializeFrontmatter(frontmatter, body), "utf-8");
3427
+ fs16.writeFileSync(filepath, serializeFrontmatter(frontmatter, body), "utf-8");
2933
3428
  }
2934
3429
  afterWrite(slug, fullRebuild) {
2935
3430
  if (fullRebuild) {
@@ -2954,16 +3449,16 @@ ${input.append}
2954
3449
  let noteCount = 0;
2955
3450
  let latestNoteMtimeMs = 0;
2956
3451
  for (const typeConfig of Object.values(this.config.note_types)) {
2957
- const folder = path8.join(this.vaultRoot, typeConfig.folder);
2958
- if (!fs12.existsSync(folder)) {
3452
+ const folder = path12.join(this.vaultRoot, typeConfig.folder);
3453
+ if (!fs16.existsSync(folder)) {
2959
3454
  continue;
2960
3455
  }
2961
- for (const entry of fs12.readdirSync(folder)) {
3456
+ for (const entry of fs16.readdirSync(folder)) {
2962
3457
  if (!entry.endsWith(".md")) {
2963
3458
  continue;
2964
3459
  }
2965
3460
  noteCount += 1;
2966
- const stat = fs12.statSync(path8.join(folder, entry));
3461
+ const stat = fs16.statSync(path12.join(folder, entry));
2967
3462
  latestNoteMtimeMs = Math.max(latestNoteMtimeMs, stat.mtimeMs);
2968
3463
  }
2969
3464
  }
@@ -2975,8 +3470,8 @@ ${input.append}
2975
3470
  };
2976
3471
  }
2977
3472
  getConfigMtimeMs() {
2978
- const configPath = path8.join(this.vaultRoot, CONFIG_FILENAME);
2979
- return fs12.existsSync(configPath) ? fs12.statSync(configPath).mtimeMs : 0;
3473
+ const configPath = path12.join(this.vaultRoot, CONFIG_FILENAME);
3474
+ return fs16.existsSync(configPath) ? fs16.statSync(configPath).mtimeMs : 0;
2980
3475
  }
2981
3476
  getLastRebuildMs() {
2982
3477
  const value = this.getMetaValue("last_rebuild");
@@ -3038,10 +3533,10 @@ async function mcpCommand(options) {
3038
3533
  function resolveVaultRoot(explicitVault) {
3039
3534
  const fromEnv = process.env.GRANITE_VAULT;
3040
3535
  if (explicitVault) {
3041
- return path9.resolve(explicitVault);
3536
+ return path13.resolve(explicitVault);
3042
3537
  }
3043
3538
  if (fromEnv) {
3044
- return path9.resolve(fromEnv);
3539
+ return path13.resolve(fromEnv);
3045
3540
  }
3046
3541
  return requireVaultRoot();
3047
3542
  }
@@ -3064,9 +3559,85 @@ function parsePort(value) {
3064
3559
  return parsed;
3065
3560
  }
3066
3561
 
3562
+ // src/commands/sync.ts
3563
+ async function syncCommand(options) {
3564
+ const vaultRoot = requireVaultRoot();
3565
+ const config = loadConfig(vaultRoot);
3566
+ const manager = new SyncManager(vaultRoot, config);
3567
+ const result = await manager.sync();
3568
+ if (options.json) {
3569
+ console.log(JSON.stringify(result));
3570
+ return;
3571
+ }
3572
+ console.log(`Sync complete: ${result.pushed} pushed, ${result.pulled} pulled`);
3573
+ if (result.conflicts > 0) {
3574
+ console.log(` ${result.conflicts} conflict(s) resolved (backups in .granite/conflicts/)`);
3575
+ }
3576
+ }
3577
+ async function syncStatusCommand(options) {
3578
+ const vaultRoot = requireVaultRoot();
3579
+ const config = loadConfig(vaultRoot);
3580
+ const manager = new SyncManager(vaultRoot, config);
3581
+ const status = manager.status();
3582
+ const conflictFiles = manager.conflicts();
3583
+ if (options.json) {
3584
+ console.log(JSON.stringify({ ...status, conflicts: conflictFiles.length }));
3585
+ return;
3586
+ }
3587
+ console.log(`Device: ${status.device_name} (${status.device_id.slice(0, 8)}...)`);
3588
+ console.log(`Last sync: ${status.last_sync ?? "never"}`);
3589
+ console.log(`Pending changes: ${status.pending_changes}`);
3590
+ console.log(`Server seq: ${status.server_seq}`);
3591
+ if (conflictFiles.length > 0) {
3592
+ console.log(`Conflicts: ${conflictFiles.length} file(s) in .granite/conflicts/`);
3593
+ }
3594
+ }
3595
+ async function syncDevicesCommand(options) {
3596
+ const vaultRoot = requireVaultRoot();
3597
+ const config = loadConfig(vaultRoot);
3598
+ const manager = new SyncManager(vaultRoot, config);
3599
+ const devices = await manager.devices();
3600
+ if (options.json) {
3601
+ console.log(JSON.stringify(devices));
3602
+ return;
3603
+ }
3604
+ if (devices.length === 0) {
3605
+ console.log("No devices registered yet.");
3606
+ return;
3607
+ }
3608
+ for (const d of devices) {
3609
+ console.log(` ${d.device_name} (${d.device_id.slice(0, 8)}...) \u2014 last seen ${d.last_seen}`);
3610
+ }
3611
+ }
3612
+ function syncConflictsCommand(options) {
3613
+ const vaultRoot = requireVaultRoot();
3614
+ if (options.clear) {
3615
+ const cleared = clearResolvedConflicts(vaultRoot);
3616
+ if (options.json) {
3617
+ console.log(JSON.stringify({ cleared }));
3618
+ } else {
3619
+ console.log(`Cleared ${cleared} conflict file(s).`);
3620
+ }
3621
+ return;
3622
+ }
3623
+ const files = listConflicts(vaultRoot);
3624
+ if (options.json) {
3625
+ console.log(JSON.stringify({ conflicts: files }));
3626
+ return;
3627
+ }
3628
+ if (files.length === 0) {
3629
+ console.log("No conflicts.");
3630
+ return;
3631
+ }
3632
+ console.log(`${files.length} conflict file(s):`);
3633
+ for (const f of files) {
3634
+ console.log(` ${f}`);
3635
+ }
3636
+ }
3637
+
3067
3638
  // src/index.ts
3068
3639
  var program = new Command();
3069
- program.name("mem").description("Granite \u2014 a local-first markdown memory system").version(GRANITE_VERSION);
3640
+ program.name("granite").description("Granite \u2014 a local-first markdown memory system").version(GRANITE_VERSION);
3070
3641
  program.command("init").description("Initialize the default vault in ~/.granite").action(() => {
3071
3642
  initVault();
3072
3643
  });
@@ -3112,6 +3683,18 @@ program.command("serve").description("Start the local web UI").option("-p, --por
3112
3683
  program.command("mcp").description("Start Granite as an MCP server").option("--vault <path>", "Vault root. Defaults to the current Granite vault or $GRANITE_VAULT").option("--transport <transport>", "Transport to use: stdio or http", parseTransportOption, "stdio").option("--host <host>", "Host for HTTP transport", "127.0.0.1").option("--port <port>", "Port for HTTP transport", "3321").option("--allow-origin <origin>", "Allow an HTTP Origin for browser-based HTTP clients", collectValues, []).option("--json-response", "Prefer JSON HTTP responses instead of SSE streams").action(async (options) => {
3113
3684
  await mcpCommand(options);
3114
3685
  });
3686
+ var syncCmd = program.command("sync").description("Sync vault across devices").option("--json", "Output as JSON").action(async (options) => {
3687
+ await syncCommand(options);
3688
+ });
3689
+ syncCmd.command("status").description("Show sync status").option("--json", "Output as JSON").action(async (options) => {
3690
+ await syncStatusCommand(options);
3691
+ });
3692
+ syncCmd.command("devices").description("List connected devices").option("--json", "Output as JSON").action(async (options) => {
3693
+ await syncDevicesCommand(options);
3694
+ });
3695
+ syncCmd.command("conflicts").description("List or clear conflict files").option("--clear", "Remove all conflict backup files").option("--json", "Output as JSON").action((options) => {
3696
+ syncConflictsCommand(options);
3697
+ });
3115
3698
  await program.parseAsync();
3116
3699
  function collectValues(value, previous) {
3117
3700
  return [...previous, value];
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "granite-mem",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A local-first markdown memory system for humans and agents",
5
5
  "type": "module",
6
6
  "bin": {
7
- "mem": "./dist/index.js"
7
+ "granite": "./dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"