echonodesync
Version:
Autonomous, pluggable, secure memory bridge for agent/human/ritual ecosystems (DreamWeaver, EchoThreads, etc.)
119 lines (108 loc) • 3.69 kB
JavaScript
// 🧠🌸 EchoNodeSync: scanKeys.js
// Scans memory keys via /api/scan, supporting pattern/regex/limit, and groups by category.
const fetch = require('node-fetch');
const path = require('path');
const os = require('os');
const fs = require('fs');
function stripQuotes(str) {
if (!str) return str;
return str.replace(/^['"]+|['"]+$/g, '').trim();
}
function resolveApiUrl() {
if (process.env.ECHONODESYNC_API_URL) return stripQuotes(process.env.ECHONODESYNC_API_URL);
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]);
}
}
}
return 'http://localhost:3000/api/memory';
}
function resolveSecret() {
if (process.env.ECHONODESYNC_SECRET) return stripQuotes(process.env.ECHONODESYNC_SECRET);
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().replace(/\/+$/, '');
const SECRET = resolveSecret();
function parseArgs(argv) {
const args = { pattern: null, regex: null, limit: null, plain: false };
for (let i = 0; i < argv.length; ++i) {
if (argv[i] === '--pattern') args.pattern = argv[++i];
else if (argv[i] === '--regex') args.regex = argv[++i];
else if (argv[i] === '--limit') args.limit = parseInt(argv[++i], 10);
else if (argv[i] === '-S') args.plain = true;
}
return args;
}
function groupByCategory(keys) {
const groups = {};
for (const key of keys) {
const [category] = key.split(':');
if (!groups[category]) groups[category] = [];
groups[category].push(key);
}
return groups;
}
async function scanKeys({ pattern, regex, limit, plain }) {
const url = new URL(BASE_URL.replace(/\/api\/memory$/, '/api/scan'));
if (pattern) url.searchParams.append('pattern', pattern);
if (regex) url.searchParams.append('regex', regex);
if (limit) url.searchParams.append('limit', limit);
const headers = {};
if (SECRET) headers['Authorization'] = `Bearer ${SECRET}`;
try {
const res = await fetch(url.toString(), { headers });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!data.keys || !Array.isArray(data.keys)) {
console.error('No keys found or invalid response.');
process.exit(1);
}
if (plain) {
for (const key of data.keys) {
console.log(key);
}
} else {
const grouped = groupByCategory(data.keys);
for (const [cat, keys] of Object.entries(grouped)) {
console.log(`\n[${cat}]`);
for (const key of keys) {
console.log(` ${key}`);
}
}
}
} catch (err) {
console.error('Failed to scan keys:', err.message);
process.exit(1);
}
}
if (require.main === module) {
const args = parseArgs(process.argv.slice(2));
if (!args.pattern && !args.regex) {
console.error('Usage: scan-keys --pattern <pattern> [--limit N] [-S]\n or: scan-keys --regex <regex> [--limit N] [-S]');
process.exit(1);
}
scanKeys(args);
}
module.exports = scanKeys;