echonodesync
Version:
Autonomous, pluggable, secure memory bridge for agent/human/ritual ecosystems (DreamWeaver, EchoThreads, etc.)
139 lines (130 loc) • 4.21 kB
JavaScript
// 🧠🌸 EchoNodeSync: postMemory implementation (HTTP backend only)
// Posts a memory value for a given key to the /api/memory HTTP backend.
// Accepts either CLI args or a JSON file with { key, value }.
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const os = require('os');
function stripQuotes(str) {
if (!str) return str;
return str.replace(/^['"]+|['"]+$/g, '').trim();
}
function resolveApiUrl() {
// 1. Ritual: Check process.env
if (process.env.ECHONODESYNC_API_URL) return stripQuotes(process.env.ECHONODESYNC_API_URL);
// 2. Ritual: Check .env in cwd, then $HOME
const envPaths = [
path.resolve(process.cwd(), '.env'),
path.resolve(os.homedir(), '.env')
];
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
for (const line of lines) {
const match = line.match(/^\s*ECHONODESYNC_API_URL\s*=\s*(.+)\s*$/);
if (match) return stripQuotes(match[1]);
}
}
}
// 3. Ritual: Default
return 'http://localhost:3000/api/memory';
}
function resolveSecret() {
// 1. Ritual: Check process.env
if (process.env.ECHONODESYNC_SECRET) return stripQuotes(process.env.ECHONODESYNC_SECRET);
// 2. Ritual: Check .env in cwd, then $HOME
const envPaths = [
path.resolve(process.cwd(), '.env'),
path.resolve(os.homedir(), '.env')
];
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
for (const line of lines) {
const match = line.match(/^\s*ECHONODESYNC_SECRET\s*=\s*(.+)\s*$/);
if (match) return stripQuotes(match[1]);
}
}
}
return null;
}
const BASE_URL = resolveApiUrl();
const SECRET = resolveSecret();
/**
* Store a memory value by key.
* @param {string} key - The memory key to store.
* @param {string} value - The memory content to store.
* @returns {Promise<void>}
*/
async function postMemory(key, value) {
const url = `${BASE_URL}`;
try {
const headers = { 'Content-Type': 'application/json' };
if (SECRET) headers['Authorization'] = `Bearer ${SECRET}`;
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ key, value })
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(`Memory posted for key '${key}'.`);
return data;
} catch (err) {
console.error(`Failed to post memory for key '${key}':`, err.message);
process.exit(1);
}
}
function isAbsoluteUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
// CLI support
if (require.main === module) {
let key, value;
// Support --key and --value flags
const argv = process.argv.slice(2);
if (argv.length === 1 && argv[0].endsWith('.json')) {
// Read from JSON file
const filePath = path.resolve(argv[0]);
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
process.exit(1);
}
const obj = JSON.parse(fs.readFileSync(filePath, 'utf8'));
key = obj.key;
value = obj.value;
if (!key || value === undefined) {
console.error('JSON file must contain { "key": ..., "value": ... }');
process.exit(1);
}
} else if (argv.includes('--key') && argv.includes('--value')) {
// Parse --key and --value flags
const keyIdx = argv.indexOf('--key');
const valueIdx = argv.indexOf('--value');
key = argv[keyIdx + 1];
value = argv[valueIdx + 1];
if (!key || value === undefined) {
console.error('Usage: post-memory --key <key> --value <value>');
process.exit(1);
}
} else if (argv.length === 2) {
// Positional: <key> <value>
key = argv[0];
value = argv[1];
} else {
console.error('Usage: post-memory <key> <value> | post-memory --key <key> --value <value> | post-memory <file.json>');
process.exit(1);
}
if (!isAbsoluteUrl(BASE_URL)) {
console.error(`BASE_URL is not absolute: ${BASE_URL}`);
process.exit(1);
}
postMemory(key, value);
}
module.exports = postMemory;