synapse-causal-memory 0.2.2 → 0.2.3

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/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env bun
1
2
  // @bun
2
3
  var __esm = (fn, res, err) => () => {
3
4
  if (fn)
@@ -23,6 +24,8 @@ var init_causal_config = __esm(() => {
23
24
  LOCAL_DB_PATH: resolve(import.meta.dir, "causal-memory.json"),
24
25
  DEFAULT_HOST: process.env.SYNAPSE_MEMORY_HOST || "127.0.0.1",
25
26
  DEFAULT_PORT: parseInt(process.env.SYNAPSE_MEMORY_PORT || "4242", 10),
27
+ REMOTE_URL: process.env.SYNAPSE_MEMORY_REMOTE_URL || "",
28
+ REMOTE_TIMEOUT_MS: parseInt(process.env.SYNAPSE_MEMORY_REMOTE_TIMEOUT || "3000", 10),
26
29
  MCP_PROTOCOL_VERSION: process.env.SYNAPSE_MCP_VERSION || "2024-11-05",
27
30
  MCP_SERVER_NAME: "synapse-causal-memory",
28
31
  MAX_CONTEXT_PREVIEW_LINES: parseInt(process.env.SYNAPSE_MEMORY_MAX_PREVIEW || "5", 10)
@@ -181,6 +184,42 @@ class CausalMemoryStore {
181
184
  }
182
185
  }
183
186
  }
187
+ static getRuleDomainPriority(rule) {
188
+ const titleLower = rule.title.toLowerCase();
189
+ const dom = rule.domain;
190
+ if (titleLower.includes("biophys") && dom === "User_Preferences_And_Rules") {
191
+ return 0;
192
+ }
193
+ if (dom === "Knowledge_Base") {
194
+ return 1;
195
+ }
196
+ return 2;
197
+ }
198
+ static deduplicateRulesByTitle(rules) {
199
+ const byTitle = new Map;
200
+ for (const rule of rules) {
201
+ if (!rule.title)
202
+ continue;
203
+ const key = rule.title.toLowerCase();
204
+ const existing = byTitle.get(key);
205
+ if (!existing) {
206
+ byTitle.set(key, rule);
207
+ } else {
208
+ const existingPrio = CausalMemoryStore.getRuleDomainPriority(existing);
209
+ const incomingPrio = CausalMemoryStore.getRuleDomainPriority(rule);
210
+ if (incomingPrio > existingPrio) {
211
+ byTitle.set(key, rule);
212
+ } else if (incomingPrio === existingPrio) {
213
+ const existingDate = existing.lastVerified || "";
214
+ const incomingDate = rule.lastVerified || "";
215
+ if (incomingDate >= existingDate) {
216
+ byTitle.set(key, rule);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ return Array.from(byTitle.values());
222
+ }
184
223
  loadDatabase() {
185
224
  try {
186
225
  if (existsSync(this.dbPath)) {
@@ -188,7 +227,8 @@ class CausalMemoryStore {
188
227
  this.lastMtimeMs = stat.mtimeMs;
189
228
  const raw = readFileSync(this.dbPath, "utf-8");
190
229
  const parsed = JSON.parse(raw);
191
- const rules = Array.isArray(parsed.rules) ? parsed.rules : [];
230
+ const rawRules = Array.isArray(parsed.rules) ? parsed.rules : [];
231
+ const rules = CausalMemoryStore.deduplicateRulesByTitle(rawRules);
192
232
  const relations = Array.isArray(parsed.relations) ? parsed.relations : [];
193
233
  return {
194
234
  version: parsed.version || CAUSAL_MEMORY_CONFIG.SCHEMA_VERSION,
@@ -637,6 +677,34 @@ class CausalMemoryStore {
637
677
  }
638
678
  return created;
639
679
  }
680
+ deleteRule(ruleId, persistToDisk = true) {
681
+ const targetId = ruleId.toLowerCase();
682
+ const removedTitles = new Set;
683
+ const filteredRules = this.database.rules.filter((r) => {
684
+ if (r.id.toLowerCase() === targetId) {
685
+ removedTitles.add(r.title.toLowerCase());
686
+ return false;
687
+ }
688
+ return true;
689
+ });
690
+ if (removedTitles.size === 0) {
691
+ return false;
692
+ }
693
+ const filteredRelations = this.database.relations.filter((rel) => !removedTitles.has(rel.from.toLowerCase()) && !removedTitles.has(rel.to.toLowerCase()) && rel.from.toLowerCase() !== targetId && rel.to.toLowerCase() !== targetId);
694
+ this.database = {
695
+ ...this.database,
696
+ updatedAt: new Date().toISOString(),
697
+ totalRules: filteredRules.length,
698
+ totalRelations: filteredRelations.length,
699
+ rules: filteredRules,
700
+ relations: filteredRelations
701
+ };
702
+ this.rebuildIndices();
703
+ if (persistToDisk) {
704
+ this.saveDatabase();
705
+ }
706
+ return true;
707
+ }
640
708
  deleteEntities(entityNames, persistToDisk = true) {
641
709
  const nameSet = new Set(entityNames.map((n) => n.toLowerCase()));
642
710
  const filteredRules = this.database.rules.filter((r) => !nameSet.has(r.title.toLowerCase()) && !nameSet.has(r.id.toLowerCase()));
@@ -701,7 +769,7 @@ class CausalMemoryStore {
701
769
  const invariants = [];
702
770
  const actions = [];
703
771
  const context = [];
704
- const isTrueUserPreference = typeLower === "preference" || typeLower === "user_constraint" || typeLower === "forbidden_rules" || nameLower === "userpreferences" || nameLower === "user_web_navigation_preference" || nameLower === "python_strictly_forbidden" || nameLower === "forbidden_topics_until_mentioned";
772
+ const isTrueUserPreference = !nameLower.includes("biophys") && (typeLower === "preference" || typeLower === "user_constraint" || typeLower === "forbidden_rules" || nameLower === "userpreferences" || nameLower === "user_web_navigation_preference" || nameLower === "python_strictly_forbidden" || nameLower === "forbidden_topics_until_mentioned");
705
773
  if (isTrueUserPreference) {
706
774
  domain = "User_Preferences_And_Rules";
707
775
  triggers.alwaysActive = true;
@@ -763,6 +831,7 @@ class CausalMemoryStore {
763
831
  audit() {
764
832
  const issues = [];
765
833
  const seenIds = new Set;
834
+ const seenTitles = new Set;
766
835
  let alwaysActiveCount = 0;
767
836
  for (const rule of this.database.rules) {
768
837
  if (rule.triggers.alwaysActive) {
@@ -775,6 +844,14 @@ class CausalMemoryStore {
775
844
  } else {
776
845
  seenIds.add(rule.id);
777
846
  }
847
+ const titleLower = (rule.title || "").toLowerCase();
848
+ if (titleLower) {
849
+ if (seenTitles.has(titleLower)) {
850
+ issues.push(`\uC911\uBCF5 \uC81C\uBAA9(Title) \uBC1C\uACAC: ${rule.title}`);
851
+ } else {
852
+ seenTitles.add(titleLower);
853
+ }
854
+ }
778
855
  if (!rule.domain) {
779
856
  issues.push(`\uB3C4\uBA54\uC778 \uB204\uB77D \uADDC\uCE59: ${rule.id}`);
780
857
  }
@@ -790,6 +867,76 @@ class CausalMemoryStore {
790
867
  issues
791
868
  };
792
869
  }
870
+ getRawDatabase() {
871
+ this.reloadIfNeeded();
872
+ return this.database;
873
+ }
874
+ mergeDatabase(incomingRules, incomingRelations = []) {
875
+ this.reloadIfNeeded();
876
+ let addedRules = 0;
877
+ let updatedRules = 0;
878
+ let addedRelations = 0;
879
+ const byTitle = new Map;
880
+ for (const r of this.database.rules) {
881
+ byTitle.set(r.title.toLowerCase(), r);
882
+ }
883
+ for (const incoming of incomingRules) {
884
+ if (!incoming.id || !incoming.title)
885
+ continue;
886
+ const titleKey = incoming.title.toLowerCase();
887
+ const existing = byTitle.get(titleKey);
888
+ if (!existing) {
889
+ byTitle.set(titleKey, incoming);
890
+ addedRules++;
891
+ } else {
892
+ const existingPrio = CausalMemoryStore.getRuleDomainPriority(existing);
893
+ const incomingPrio = CausalMemoryStore.getRuleDomainPriority(incoming);
894
+ const existingDate = existing.lastVerified || "";
895
+ const incomingDate = incoming.lastVerified || "";
896
+ if (incomingPrio > existingPrio) {
897
+ byTitle.set(titleKey, incoming);
898
+ updatedRules++;
899
+ } else if (incomingPrio === existingPrio && incoming.id !== existing.id && incomingDate > existingDate) {
900
+ byTitle.set(titleKey, incoming);
901
+ updatedRules++;
902
+ } else if (incomingPrio === existingPrio && incoming.id === existing.id && incomingDate > existingDate) {
903
+ byTitle.set(titleKey, incoming);
904
+ updatedRules++;
905
+ }
906
+ }
907
+ }
908
+ const relationKey = (rel) => `${rel.from.toLowerCase()}::${rel.relationType.toLowerCase()}::${rel.to.toLowerCase()}`;
909
+ const relMap = new Map;
910
+ for (const rel of this.database.relations) {
911
+ relMap.set(relationKey(rel), rel);
912
+ }
913
+ for (const incomingRel of incomingRelations) {
914
+ const key = relationKey(incomingRel);
915
+ if (!relMap.has(key)) {
916
+ relMap.set(key, incomingRel);
917
+ addedRelations++;
918
+ }
919
+ }
920
+ const mergedRules = Array.from(byTitle.values());
921
+ const mergedRelations = Array.from(relMap.values());
922
+ this.database = {
923
+ ...this.database,
924
+ updatedAt: new Date().toISOString(),
925
+ totalRules: mergedRules.length,
926
+ totalRelations: mergedRelations.length,
927
+ rules: mergedRules,
928
+ relations: mergedRelations
929
+ };
930
+ this.rebuildIndices();
931
+ this.saveDatabase();
932
+ return {
933
+ addedRules,
934
+ updatedRules,
935
+ addedRelations,
936
+ totalRules: mergedRules.length,
937
+ totalRelations: mergedRelations.length
938
+ };
939
+ }
793
940
  }
794
941
  var init_causal_store = __esm(() => {
795
942
  init_causal_config();
@@ -1129,6 +1276,17 @@ function startCausalServer(options = {}) {
1129
1276
  const auditResult = store.audit();
1130
1277
  return new Response(JSON.stringify(auditResult), { headers });
1131
1278
  }
1279
+ if (url.pathname === "/sync" && req.method === "GET") {
1280
+ const rawDb = store.getRawDatabase();
1281
+ return new Response(JSON.stringify(rawDb), { headers });
1282
+ }
1283
+ if (url.pathname === "/sync" && req.method === "POST") {
1284
+ const body = await req.json();
1285
+ const incomingRules = Array.isArray(body.rules) ? body.rules : [];
1286
+ const incomingRelations = Array.isArray(body.relations) ? body.relations : [];
1287
+ const mergeResult = store.mergeDatabase(incomingRules, incomingRelations);
1288
+ return new Response(JSON.stringify({ ok: true, ...mergeResult }), { headers });
1289
+ }
1132
1290
  if (url.pathname === "/mcp" && req.method === "POST") {
1133
1291
  const rpc = await req.json();
1134
1292
  const responseObj = await handleMcpJsonRpcRequest(rpc, store, options.dbPath);
@@ -1145,9 +1303,12 @@ function startCausalServer(options = {}) {
1145
1303
  });
1146
1304
  return server;
1147
1305
  }
1148
- async function startCausalMcpStdioServer(dbPath) {
1306
+ async function startCausalMcpStdioServer(dbPath, remoteUrl) {
1149
1307
  const store = new CausalMemoryStore(dbPath);
1150
- const daemonUrl = `http://${CAUSAL_MEMORY_CONFIG.DEFAULT_HOST}:${CAUSAL_MEMORY_CONFIG.DEFAULT_PORT}/mcp`;
1308
+ const targetRemote = remoteUrl || CAUSAL_MEMORY_CONFIG.REMOTE_URL;
1309
+ const isRemoteMode = Boolean(targetRemote && targetRemote.trim().length > 0);
1310
+ const activeDaemonUrl = isRemoteMode ? targetRemote.endsWith("/mcp") ? targetRemote : `${targetRemote.replace(/\/+$/, "")}/mcp` : `http://${CAUSAL_MEMORY_CONFIG.DEFAULT_HOST}:${CAUSAL_MEMORY_CONFIG.DEFAULT_PORT}/mcp`;
1311
+ const timeoutMs = isRemoteMode ? CAUSAL_MEMORY_CONFIG.REMOTE_TIMEOUT_MS : 150;
1151
1312
  const rl = createInterface({
1152
1313
  input: process.stdin,
1153
1314
  terminal: false
@@ -1161,11 +1322,11 @@ async function startCausalMcpStdioServer(dbPath) {
1161
1322
  let responseObj = null;
1162
1323
  if (rpc.method === "tools/call") {
1163
1324
  try {
1164
- const daemonRes = await fetch(daemonUrl, {
1325
+ const daemonRes = await fetch(activeDaemonUrl, {
1165
1326
  method: "POST",
1166
1327
  headers: { "Content-Type": "application/json" },
1167
1328
  body: trimmed,
1168
- signal: AbortSignal.timeout(150)
1329
+ signal: AbortSignal.timeout(timeoutMs)
1169
1330
  });
1170
1331
  if (daemonRes.ok) {
1171
1332
  responseObj = await daemonRes.json();
@@ -1930,6 +2091,177 @@ function migrateLegacyMemoryGraph(sourceBackupPath = CAUSAL_MEMORY_CONFIG.LEGACY
1930
2091
  };
1931
2092
  }
1932
2093
 
2094
+ // causal-peer.ts
2095
+ init_causal_config();
2096
+ init_causal_store();
2097
+ async function getTailscalePeers() {
2098
+ try {
2099
+ const proc = Bun.spawn(["tailscale", "status", "--json"], {
2100
+ stdout: "pipe",
2101
+ stderr: "pipe"
2102
+ });
2103
+ const output = await new Response(proc.stdout).text();
2104
+ const data = JSON.parse(output);
2105
+ const selfIp = data.Self?.TailscaleIPs?.[0];
2106
+ const selfHost = data.Self?.HostName;
2107
+ const peers = [];
2108
+ if (data.Self) {
2109
+ peers.push({
2110
+ id: data.Self.ID || "self",
2111
+ hostName: data.Self.HostName || "localhost",
2112
+ ip: selfIp || "127.0.0.1",
2113
+ os: data.Self.OS || process.platform,
2114
+ online: data.Self.Online ?? true,
2115
+ active: true,
2116
+ isSelf: true
2117
+ });
2118
+ }
2119
+ if (data.Peer) {
2120
+ for (const key of Object.keys(data.Peer)) {
2121
+ const p = data.Peer[key];
2122
+ const peerIp = p.TailscaleIPs?.[0] || "";
2123
+ peers.push({
2124
+ id: p.ID || key,
2125
+ hostName: p.HostName || "",
2126
+ ip: peerIp,
2127
+ os: p.OS || "unknown",
2128
+ online: Boolean(p.Online),
2129
+ active: Boolean(p.Active),
2130
+ isSelf: false
2131
+ });
2132
+ }
2133
+ }
2134
+ return { selfIp, selfHost, peers };
2135
+ } catch (error) {
2136
+ return {
2137
+ peers: []
2138
+ };
2139
+ }
2140
+ }
2141
+ async function pingPeer(host, port = CAUSAL_MEMORY_CONFIG.DEFAULT_PORT) {
2142
+ const url = `http://${host}:${port}/health`;
2143
+ const startTime = performance.now();
2144
+ try {
2145
+ const res = await fetch(url, {
2146
+ method: "GET",
2147
+ signal: AbortSignal.timeout(CAUSAL_MEMORY_CONFIG.REMOTE_TIMEOUT_MS)
2148
+ });
2149
+ const rttMs = Math.round((performance.now() - startTime) * 100) / 100;
2150
+ if (!res.ok) {
2151
+ return {
2152
+ host,
2153
+ port,
2154
+ url,
2155
+ reachable: false,
2156
+ rttMs,
2157
+ error: `HTTP ${res.status} ${res.statusText}`
2158
+ };
2159
+ }
2160
+ const data = await res.json();
2161
+ return {
2162
+ host,
2163
+ port,
2164
+ url,
2165
+ reachable: true,
2166
+ rttMs,
2167
+ version: data.status?.version,
2168
+ totalRules: data.status?.totalRules,
2169
+ totalRelations: data.status?.totalRelations
2170
+ };
2171
+ } catch (error) {
2172
+ const rttMs = Math.round((performance.now() - startTime) * 100) / 100;
2173
+ return {
2174
+ host,
2175
+ port,
2176
+ url,
2177
+ reachable: false,
2178
+ rttMs,
2179
+ error: error instanceof Error ? error.message : String(error)
2180
+ };
2181
+ }
2182
+ }
2183
+ async function queryRemotePeer(remoteUrl, options) {
2184
+ const cleanUrl = remoteUrl.replace(/\/+$/, "");
2185
+ const endpoint = cleanUrl.endsWith("/query") ? cleanUrl : `${cleanUrl}/query`;
2186
+ const res = await fetch(endpoint, {
2187
+ method: "POST",
2188
+ headers: { "Content-Type": "application/json" },
2189
+ body: JSON.stringify(options),
2190
+ signal: AbortSignal.timeout(CAUSAL_MEMORY_CONFIG.REMOTE_TIMEOUT_MS)
2191
+ });
2192
+ if (!res.ok) {
2193
+ throw new Error(`\uC6D0\uACA9 \uC778\uACFC\uC728 \uCFFC\uB9AC \uC2E4\uD328: HTTP ${res.status} ${res.statusText}`);
2194
+ }
2195
+ return await res.json();
2196
+ }
2197
+ async function syncWithPeer(remoteHost, port = CAUSAL_MEMORY_CONFIG.DEFAULT_PORT, direction = "pull", dbPath) {
2198
+ const store = new CausalMemoryStore(dbPath);
2199
+ const cleanHost = remoteHost.replace(/^https?:\/\//, "").split(":")[0];
2200
+ const targetPort = remoteHost.includes(":") ? parseInt(remoteHost.split(":")[1].replace(/\/.*/, ""), 10) || port : port;
2201
+ const baseUrl = `http://${cleanHost}:${targetPort}`;
2202
+ const startTime = performance.now();
2203
+ try {
2204
+ if (direction === "pull") {
2205
+ const res = await fetch(`${baseUrl}/sync`, {
2206
+ method: "GET",
2207
+ signal: AbortSignal.timeout(CAUSAL_MEMORY_CONFIG.REMOTE_TIMEOUT_MS)
2208
+ });
2209
+ if (!res.ok) {
2210
+ throw new Error(`\uC6D0\uACA9 \uB370\uC774\uD130 \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: HTTP ${res.status}`);
2211
+ }
2212
+ const remoteDb = await res.json();
2213
+ const rules = Array.isArray(remoteDb.rules) ? remoteDb.rules : [];
2214
+ const relations = Array.isArray(remoteDb.relations) ? remoteDb.relations : [];
2215
+ const mergeResult = store.mergeDatabase(rules, relations);
2216
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
2217
+ return {
2218
+ targetHost: cleanHost,
2219
+ direction: "pull",
2220
+ success: true,
2221
+ durationMs,
2222
+ addedRules: mergeResult.addedRules,
2223
+ updatedRules: mergeResult.updatedRules,
2224
+ addedRelations: mergeResult.addedRelations,
2225
+ totalRules: mergeResult.totalRules,
2226
+ totalRelations: mergeResult.totalRelations
2227
+ };
2228
+ } else {
2229
+ const rawDb = store.getRawDatabase();
2230
+ const res = await fetch(`${baseUrl}/sync`, {
2231
+ method: "POST",
2232
+ headers: { "Content-Type": "application/json" },
2233
+ body: JSON.stringify(rawDb),
2234
+ signal: AbortSignal.timeout(CAUSAL_MEMORY_CONFIG.REMOTE_TIMEOUT_MS)
2235
+ });
2236
+ if (!res.ok) {
2237
+ throw new Error(`\uC6D0\uACA9 \uB370\uC774\uD130 \uD478\uC2DC \uC2E4\uD328: HTTP ${res.status}`);
2238
+ }
2239
+ const pushResult = await res.json();
2240
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
2241
+ return {
2242
+ targetHost: cleanHost,
2243
+ direction: "push",
2244
+ success: true,
2245
+ durationMs,
2246
+ addedRules: pushResult.addedRules,
2247
+ updatedRules: pushResult.updatedRules,
2248
+ addedRelations: pushResult.addedRelations,
2249
+ totalRules: pushResult.totalRules,
2250
+ totalRelations: pushResult.totalRelations
2251
+ };
2252
+ }
2253
+ } catch (error) {
2254
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
2255
+ return {
2256
+ targetHost: cleanHost,
2257
+ direction,
2258
+ success: false,
2259
+ durationMs,
2260
+ error: error instanceof Error ? error.message : String(error)
2261
+ };
2262
+ }
2263
+ }
2264
+
1933
2265
  // cli.ts
1934
2266
  var RESET2 = "\x1B[0m";
1935
2267
  var BOLD2 = "\x1B[1m";
@@ -1950,6 +2282,7 @@ ${BOLD2}\uC11C\uBE0C \uBA85\uB839\uC5B4:${RESET2}
1950
2282
  list \uB4F1\uB85D\uB41C \uB3C4\uBA54\uC778, \uC778\uACFC \uADDC\uCE59 \uBC0F \uC778\uACFC \uAC04\uC120(Relations) \uD1B5\uACC4 \uD655\uC778
1951
2283
  audit \uC778\uACFC\uC728 \uBA54\uBAA8\uB9AC \uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uBB34\uACB0\uC131 \uBC0F \uD1A0\uD070 \uC808\uAC10 \uC0C1\uD0DC \uAC10\uC0AC
1952
2284
  assert \uC2E0\uADDC \uC778\uACFC\uC728 \uADDC\uCE59 \uB4F1\uB85D \uB610\uB294 \uAC31\uC2E0 (--json '<json>')
2285
+ peer Tailscale \uC0AC\uC124 \uBA54\uC26C \uD53C\uC5B4 \uD0D0\uC0C9, \uD551(RTT) \uBC0F \uBA54\uBAA8\uB9AC \uC6D0\uACA9 \uB3D9\uAE30\uD654
1953
2286
  migrate \uAD6C\uD615 \uC9C0\uC2DD \uADF8\uB798\uD504 \uBC31\uC5C5\uBCF8\uC744 \uC778\uACFC\uC728 \uBA54\uBAA8\uB9AC \uB370\uC774\uD130\uBCA0\uC774\uC2A4\uB85C \uBB34\uC190\uC2E4 \uBCC0\uD658
1954
2287
  benchmark 4\uB300 \uCC28\uC6D0 \uACFC\uBD80\uD558 \uC2A4\uD2B8\uB808\uC2A4 \uBCA4\uCE58\uB9C8\uD06C \uC2E4\uD589 (--scale 10000 --concurrency 50)
1955
2288
  serve \uB3C5\uB9BD \uB85C\uCEEC \uBC31\uADF8\uB77C\uC6B4\uB4DC \uC11C\uBC84/\uB370\uBAAC \uAD6C\uB3D9 (0ms \uC778\uBA54\uBAA8\uB9AC HTTP & MCP API)
@@ -1960,11 +2293,21 @@ ${BOLD2}query \uC635\uC158:${RESET2}
1960
2293
  --keywords <words...> \uD2B8\uB9AC\uAC70 \uAC80\uC0C9 \uD0A4\uC6CC\uB4DC
1961
2294
  --command <cmd> \uD2B8\uB9AC\uAC70 \uC5F0\uB3D9 \uBA85\uB839\uC5B4 (\uC608: release, build, sign)
1962
2295
  --domain <domain> \uD2B9\uC815 \uB3C4\uBA54\uC778\uC73C\uB85C \uAC80\uC0C9 \uC81C\uD55C
2296
+ --remote <url> \uC6D0\uACA9 \uB370\uBAAC\uC73C\uB85C \uACF5\uBA85 \uCFFC\uB9AC \uC694\uCCAD (\uC608: http://100.82.92.122:4242)
1963
2297
  --format <json|md|compact> \uCD9C\uB825 \uD3EC\uB9F7 \uC9C0\uC815
1964
2298
 
1965
2299
  ${BOLD2}serve \uC635\uC158:${RESET2}
1966
2300
  --port <number> \uC11C\uBC84 \uD3EC\uD2B8 \uBC88\uD638 (\uAE30\uBCF8: ${CAUSAL_MEMORY_CONFIG.DEFAULT_PORT})
1967
2301
  --host <string> \uBC14\uC778\uB529 \uD638\uC2A4\uD2B8 (\uAE30\uBCF8: ${CAUSAL_MEMORY_CONFIG.DEFAULT_HOST})
2302
+ --tailscale Tailscale \uC0AC\uC124 IP\uB85C \uC790\uB3D9 \uBC14\uC778\uB529 (\uBA54\uC26C \uACF5\uC720 \uBAA8\uB4DC)
2303
+
2304
+ ${BOLD2}peer \uC0AC\uC6A9\uBC95:${RESET2}
2305
+ synapse memory peer list Tailscale \uBA54\uC26C \uD53C\uC5B4 \uBC0F \uB370\uBAAC \uAC00\uB3D9 \uC0C1\uD0DC \uC870\uD68C
2306
+ synapse memory peer ping <ip> \uB300\uC0C1 \uD53C\uC5B4\uC758 Causal Memory \uD5EC\uC2A4\uCCB4\uD06C \uBC0F RTT \uCE21\uC815
2307
+ synapse memory peer sync <ip> [--push] \uB300\uC0C1 \uD53C\uC5B4\uC640 \uC778\uACFC\uC728 \uC9C0\uC2DD \uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC591\uBC29\uD5A5 \uB3D9\uAE30\uD654
2308
+
2309
+ ${BOLD2}mcp-stdio \uC635\uC158:${RESET2}
2310
+ --remote <url> \uC6D0\uACA9 \uB370\uBAAC \uD504\uB85D\uC2DC \uBAA8\uB4DC \uAD6C\uB3D9 (\uC608: http://100.82.92.122:4242)
1968
2311
 
1969
2312
  ${BOLD2}\uACF5\uD1B5 \uD50C\uB798\uADF8:${RESET2}
1970
2313
  --agent, --compact AI \uC5D0\uC774\uC804\uD2B8 \uC804\uC6A9 \uCD08\uACBD\uB7C9 JSON \uBAA8\uB4DC
@@ -1979,7 +2322,14 @@ async function handleMemoryCommand(args, options) {
1979
2322
  return 0;
1980
2323
  }
1981
2324
  if (subCommand === "mcp-stdio") {
1982
- await startCausalMcpStdioServer();
2325
+ let remoteUrl;
2326
+ for (let i = 0;i < subArgs.length; i++) {
2327
+ if (subArgs[i] === "--remote" && subArgs[i + 1]) {
2328
+ remoteUrl = subArgs[i + 1];
2329
+ break;
2330
+ }
2331
+ }
2332
+ await startCausalMcpStdioServer(undefined, remoteUrl);
1983
2333
  return 0;
1984
2334
  }
1985
2335
  if (subCommand === "benchmark") {
@@ -2051,6 +2401,7 @@ ${BOLD2}${GREEN2}\u2728 [Causal Memory \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC6
2051
2401
  const keywords = [];
2052
2402
  let command;
2053
2403
  let domain;
2404
+ let remoteUrl;
2054
2405
  let format = options.compactMode ? "compact" : options.agentMode ? "json" : "text";
2055
2406
  for (let i = 0;i < subArgs.length; i++) {
2056
2407
  const arg = subArgs[i];
@@ -2072,11 +2423,13 @@ ${BOLD2}${GREEN2}\u2728 [Causal Memory \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC6
2072
2423
  command = subArgs[++i];
2073
2424
  } else if (arg === "--domain" || arg === "-d") {
2074
2425
  domain = subArgs[++i];
2426
+ } else if (arg === "--remote") {
2427
+ remoteUrl = subArgs[++i];
2075
2428
  } else if (arg === "--format") {
2076
2429
  format = subArgs[++i];
2077
2430
  }
2078
2431
  }
2079
- const result = await runCausalQuery({
2432
+ const result = remoteUrl ? await queryRemotePeer(remoteUrl, { files, keywords, command, domain }) : await runCausalQuery({
2080
2433
  files,
2081
2434
  keywords,
2082
2435
  command,
@@ -2194,9 +2547,160 @@ ${BOLD2}${RED2}\uD83D\uDEA8 [Causal Memory Audit \uACB0\uD568 \uBC1C\uACAC] \uCD
2194
2547
  return 1;
2195
2548
  }
2196
2549
  }
2550
+ if (subCommand === "peer") {
2551
+ const peerAction = subArgs.find((a) => a !== "peer" && !a.startsWith("-")) || "list";
2552
+ if (peerAction === "list") {
2553
+ const { selfIp, selfHost, peers } = await getTailscalePeers();
2554
+ if (peers.length === 0) {
2555
+ if (options.agentMode) {
2556
+ process.stdout.write(JSON.stringify({ ok: false, message: "No Tailscale peers detected" }) + `
2557
+ `);
2558
+ } else {
2559
+ process.stdout.write(`${YELLOW2}\u26A0\uFE0F Tailscale\uC774 \uC2E4\uD589 \uC911\uC774\uC9C0 \uC54A\uAC70\uB098 \uAC10\uC9C0\uB41C \uD53C\uC5B4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.${RESET2}
2560
+ `);
2561
+ }
2562
+ return 1;
2563
+ }
2564
+ const healthPromises = peers.map(async (p) => {
2565
+ if (!p.ip) {
2566
+ return {
2567
+ ...p,
2568
+ reachable: false,
2569
+ rttMs: -1,
2570
+ totalRules: undefined,
2571
+ totalRelations: undefined
2572
+ };
2573
+ }
2574
+ const health = await pingPeer(p.ip);
2575
+ return {
2576
+ ...p,
2577
+ reachable: health.reachable,
2578
+ rttMs: health.rttMs,
2579
+ totalRules: health.totalRules,
2580
+ totalRelations: health.totalRelations
2581
+ };
2582
+ });
2583
+ const checkedPeers = await Promise.all(healthPromises);
2584
+ if (options.agentMode) {
2585
+ process.stdout.write(JSON.stringify({ ok: true, selfIp, selfHost, peers: checkedPeers }) + `
2586
+ `);
2587
+ return 0;
2588
+ }
2589
+ process.stdout.write(`
2590
+ ${BOLD2}${CYAN2}\uD83C\uDF10 [Causal Memory] Tailscale \uC0AC\uC124 \uBA54\uC26C \uD53C\uC5B4 \uD1A0\uD3F4\uB85C\uC9C0${RESET2}
2591
+ `);
2592
+ process.stdout.write(` \u2022 \uB85C\uCEEC \uB178\uB4DC: ${BOLD2}${GREEN2}${selfHost || "localhost"}${RESET2} (${selfIp || "127.0.0.1"})
2593
+
2594
+ `);
2595
+ for (const p of checkedPeers) {
2596
+ const statusBadge = p.isSelf ? `${BOLD2}${CYAN2}[LOCAL HOST]${RESET2}` : p.reachable ? `${BOLD2}${GREEN2}[DAEMON ONLINE]${RESET2}` : p.online ? `${YELLOW2}[PEER UP (NO DAEMON)]${RESET2}` : `${GRAY2}[OFFLINE]${RESET2}`;
2597
+ const rttText = p.rttMs >= 0 ? `${p.rttMs}ms` : "-";
2598
+ const memoryInfo = p.reachable ? `(\uADDC\uCE59 ${p.totalRules}\uAC1C, \uAC04\uC120 ${p.totalRelations}\uAC1C)` : "";
2599
+ process.stdout.write(` \u2022 ${BOLD2}${p.hostName.padEnd(24)}${RESET2} ${GRAY2}${p.ip.padEnd(16)}${RESET2} ${statusBadge} ${GRAY2}${rttText}${RESET2} ${memoryInfo}
2600
+ `);
2601
+ }
2602
+ process.stdout.write(`
2603
+ `);
2604
+ return 0;
2605
+ }
2606
+ if (peerAction === "ping") {
2607
+ const targetHost = subArgs.filter((a) => a !== "peer" && a !== "ping" && !a.startsWith("-"))[0];
2608
+ if (!targetHost) {
2609
+ process.stderr.write(`${RED2}\u274C \uB300\uC0C1 IP \uB610\uB294 \uD638\uC2A4\uD2B8\uBA85\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (\uC608: synapse memory peer ping 100.73.80.80)${RESET2}
2610
+ `);
2611
+ return 1;
2612
+ }
2613
+ const health = await pingPeer(targetHost);
2614
+ if (options.agentMode) {
2615
+ process.stdout.write(JSON.stringify(health) + `
2616
+ `);
2617
+ return health.reachable ? 0 : 1;
2618
+ }
2619
+ if (health.reachable) {
2620
+ process.stdout.write(`
2621
+ ${BOLD2}${GREEN2}\u2714 [Causal Memory Peer \uC751\uB2F5 \uC131\uACF5]${RESET2}
2622
+ `);
2623
+ process.stdout.write(` \u2022 \uD53C\uC5B4 \uC8FC\uC18C : ${CYAN2}${health.url}${RESET2}
2624
+ `);
2625
+ process.stdout.write(` \u2022 \uC751\uB2F5 \uC9C0\uC5F0 : ${BOLD2}${health.rttMs}ms${RESET2}
2626
+ `);
2627
+ process.stdout.write(` \u2022 \uC5D4\uC9C4 \uBC84\uC804 : v${health.version}
2628
+ `);
2629
+ process.stdout.write(` \u2022 \uBCF4\uC720 \uADDC\uCE59 : ${BOLD2}${health.totalRules}\uAC1C${RESET2} (\uAC04\uC120: ${health.totalRelations}\uAC1C)
2630
+
2631
+ `);
2632
+ return 0;
2633
+ } else {
2634
+ process.stderr.write(`
2635
+ ${BOLD2}${RED2}\u2716 [Causal Memory Peer \uC751\uB2F5 \uC5C6\uC74C]${RESET2}
2636
+ `);
2637
+ process.stderr.write(` \u2022 \uD53C\uC5B4 \uC8FC\uC18C : ${health.url}
2638
+ `);
2639
+ process.stderr.write(` \u2022 \uB808\uC774\uD134\uC2DC : ${health.rttMs}ms
2640
+ `);
2641
+ process.stderr.write(` \u2022 \uC5D0\uB7EC \uB0B4\uC6A9 : ${health.error || "\uC5F0\uACB0 \uAC70\uBD80 \uB610\uB294 \uD0C0\uC784\uC544\uC6C3"}
2642
+ `);
2643
+ process.stderr.write(` \u2022 \uC870\uCE58 \uD301 : \uC0C1\uB300\uBC29 PC\uC5D0\uC11C 'synapse memory serve --tailscale' \uB370\uBAAC\uC774 \uC2E4\uD589 \uC911\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694.
2644
+
2645
+ `);
2646
+ return 1;
2647
+ }
2648
+ }
2649
+ if (peerAction === "sync") {
2650
+ const targetHost = subArgs.filter((a) => a !== "peer" && a !== "sync" && !a.startsWith("-"))[0];
2651
+ if (!targetHost) {
2652
+ process.stderr.write(`${RED2}\u274C \uB3D9\uAE30\uD654 \uB300\uC0C1 IP\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. (\uC608: synapse memory peer sync 100.73.80.80)${RESET2}
2653
+ `);
2654
+ return 1;
2655
+ }
2656
+ const isPush = subArgs.includes("--push");
2657
+ const direction = isPush ? "push" : "pull";
2658
+ const syncResult = await syncWithPeer(targetHost, CAUSAL_MEMORY_CONFIG.DEFAULT_PORT, direction);
2659
+ if (options.agentMode) {
2660
+ process.stdout.write(JSON.stringify(syncResult) + `
2661
+ `);
2662
+ return syncResult.success ? 0 : 1;
2663
+ }
2664
+ if (syncResult.success) {
2665
+ process.stdout.write(`
2666
+ ${BOLD2}${GREEN2}\u2728 [Causal Memory \uB3D9\uAE30\uD654 \uC131\uACF5 (${direction.toUpperCase()})]${RESET2}
2667
+ `);
2668
+ process.stdout.write(` \u2022 \uB300\uC0C1 \uD53C\uC5B4 : ${CYAN2}${syncResult.targetHost}${RESET2}
2669
+ `);
2670
+ process.stdout.write(` \u2022 \uCD94\uAC00\uB41C \uADDC\uCE59: ${BOLD2}${syncResult.addedRules}\uAC1C${RESET2} (\uAC31\uC2E0: ${syncResult.updatedRules}\uAC1C)
2671
+ `);
2672
+ process.stdout.write(` \u2022 \uCD94\uAC00\uB41C \uAC04\uC120: ${BOLD2}${syncResult.addedRelations}\uAC1C${RESET2}
2673
+ `);
2674
+ process.stdout.write(` \u2022 \uCD1D \uC9C0\uC2DD \uC218 : \uADDC\uCE59 ${syncResult.totalRules}\uAC1C, \uAC04\uC120 ${syncResult.totalRelations}\uAC1C
2675
+ `);
2676
+ process.stdout.write(` \u2022 \uC18C\uC694 \uC2DC\uAC04 : ${syncResult.durationMs}ms
2677
+
2678
+ `);
2679
+ return 0;
2680
+ } else {
2681
+ process.stderr.write(`
2682
+ ${BOLD2}${RED2}\uD83D\uDEA8 [Causal Memory \uB3D9\uAE30\uD654 \uC2E4\uD328]${RESET2}
2683
+ `);
2684
+ process.stderr.write(` \u2022 \uB300\uC0C1 \uD53C\uC5B4 : ${syncResult.targetHost}
2685
+ `);
2686
+ process.stderr.write(` \u2022 \uC5D0\uB7EC \uC0AC\uC720 : ${syncResult.error}
2687
+
2688
+ `);
2689
+ return 1;
2690
+ }
2691
+ }
2692
+ process.stderr.write(`${RED2}\u274C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 peer \uC11C\uBE0C \uBA85\uB839\uC5B4: '${peerAction}' (list, ping, sync)${RESET2}
2693
+ `);
2694
+ return 1;
2695
+ }
2197
2696
  if (subCommand === "serve") {
2198
2697
  let port = CAUSAL_MEMORY_CONFIG.DEFAULT_PORT;
2199
2698
  let host = CAUSAL_MEMORY_CONFIG.DEFAULT_HOST;
2699
+ const isTailscale = subArgs.includes("--tailscale");
2700
+ if (isTailscale) {
2701
+ const { selfIp } = await getTailscalePeers();
2702
+ host = selfIp || "0.0.0.0";
2703
+ }
2200
2704
  for (let i = 0;i < subArgs.length; i++) {
2201
2705
  if (subArgs[i] === "--port" && subArgs[i + 1]) {
2202
2706
  port = parseInt(subArgs[i + 1], 10);
@@ -2205,14 +2709,15 @@ ${BOLD2}${RED2}\uD83D\uDEA8 [Causal Memory Audit \uACB0\uD568 \uBC1C\uACAC] \uCD
2205
2709
  }
2206
2710
  }
2207
2711
  const server = startCausalServer({ port, host });
2208
- if (options.agentMode) {
2209
- process.stdout.write(JSON.stringify({ ok: true, port: server.port, host, url: `http://${host}:${server.port}` }) + `
2712
+ if (options?.agentMode) {
2713
+ process.stdout.write(JSON.stringify({ ok: true, port: server.port, host, url: `http://${host}:${server.port}`, tailscale: isTailscale }) + `
2210
2714
  `);
2211
2715
  } else {
2212
2716
  process.stdout.write(`
2213
2717
  ${BOLD2}${GREEN2}\uD83D\uDE80 [Causal Memory Daemon \uAC00\uB3D9 \uC644\uB8CC]${RESET2}
2214
- \u2022 \uC8FC\uC18C: ${CYAN2}http://${host}:${server.port}${RESET2}
2215
- \u2022 MCP API: ${GRAY2}http://${host}:${server.port}/mcp${RESET2}
2718
+ \u2022 \uB85C\uCEEC \uBC14\uC778\uB529: ${CYAN2}http://${host}:${server.port}${RESET2}
2719
+ \u2022 MCP API URL: ${GRAY2}http://${host}:${server.port}/mcp${RESET2}
2720
+ \u2022 \uBAA8\uB4DC : ${isTailscale ? `${BOLD2}${GREEN2}Tailscale \uC0AC\uC124 \uBA54\uC26C \uBAA8\uB4DC (\uC678\uBD80 \uD53C\uC5B4 \uC811\uADFC \uD5C8\uC6A9)${RESET2}` : "\uB85C\uCEEC \uB8E8\uD504\uBC31 \uBAA8\uB4DC"}
2216
2721
  \u2022 0ms \uC778\uBA54\uBAA8\uB9AC \uACF5\uBA85 \uB300\uAE30 \uC911... (\uC885\uB8CC: Ctrl+C)
2217
2722
 
2218
2723
  `);
@@ -2225,6 +2730,21 @@ ${BOLD2}${GREEN2}\uD83D\uDE80 [Causal Memory Daemon \uAC00\uB3D9 \uC644\uB8CC]${
2225
2730
  printMemoryHelp();
2226
2731
  return 1;
2227
2732
  }
2733
+ if (import.meta.main) {
2734
+ const rawArgs = process.argv.slice(2);
2735
+ const options = {
2736
+ agentMode: rawArgs.includes("--agent") || rawArgs.includes("--compact"),
2737
+ compact: rawArgs.includes("--compact"),
2738
+ force: rawArgs.includes("--force") || rawArgs.includes("-f")
2739
+ };
2740
+ handleMemoryCommand(rawArgs, options).then((code) => {
2741
+ process.exit(code);
2742
+ }).catch((err) => {
2743
+ process.stderr.write(`\u274C Error: ${err instanceof Error ? err.message : String(err)}
2744
+ `);
2745
+ process.exit(1);
2746
+ });
2747
+ }
2228
2748
  export {
2229
2749
  handleMemoryCommand
2230
2750
  };