@awareness-sdk/local 0.1.0
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/bin/awareness-local.mjs +489 -0
- package/package.json +31 -0
- package/src/api.mjs +122 -0
- package/src/core/cloud-sync.mjs +970 -0
- package/src/core/config.mjs +303 -0
- package/src/core/embedder.mjs +239 -0
- package/src/core/index.mjs +34 -0
- package/src/core/indexer.mjs +726 -0
- package/src/core/knowledge-extractor.mjs +629 -0
- package/src/core/memory-store.mjs +665 -0
- package/src/core/search.mjs +633 -0
- package/src/daemon.mjs +1720 -0
- package/src/mcp-server.mjs +335 -0
- package/src/spec/awareness-spec.json +393 -0
- package/src/web/index.html +1015 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for Awareness Local daemon.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* start [--project <dir>] [--port <port>] [--foreground] — start daemon
|
|
8
|
+
* stop [--project <dir>] — stop daemon
|
|
9
|
+
* status [--project <dir>] — show daemon status + stats
|
|
10
|
+
* reindex [--project <dir>] — rebuild FTS5 + embedding index
|
|
11
|
+
*
|
|
12
|
+
* Uses process.argv parsing (no dependencies).
|
|
13
|
+
* For `start` without `--foreground`, spawns self as a detached child process.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import http from 'node:http';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Argv parsing
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse CLI arguments into { command, flags }.
|
|
28
|
+
* Supports: --flag value, --flag=value, --boolean-flag
|
|
29
|
+
* @param {string[]} argv — typically process.argv.slice(2)
|
|
30
|
+
* @returns {{ command: string, flags: Record<string, string|boolean> }}
|
|
31
|
+
*/
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const positional = [];
|
|
34
|
+
const flags = {};
|
|
35
|
+
let i = 0;
|
|
36
|
+
|
|
37
|
+
while (i < argv.length) {
|
|
38
|
+
const arg = argv[i];
|
|
39
|
+
|
|
40
|
+
if (arg.startsWith('--')) {
|
|
41
|
+
const eqIdx = arg.indexOf('=');
|
|
42
|
+
if (eqIdx !== -1) {
|
|
43
|
+
// --key=value
|
|
44
|
+
const key = arg.slice(2, eqIdx);
|
|
45
|
+
flags[key] = arg.slice(eqIdx + 1);
|
|
46
|
+
} else {
|
|
47
|
+
const key = arg.slice(2);
|
|
48
|
+
const next = argv[i + 1];
|
|
49
|
+
if (next && !next.startsWith('--')) {
|
|
50
|
+
flags[key] = next;
|
|
51
|
+
i++;
|
|
52
|
+
} else {
|
|
53
|
+
flags[key] = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
positional.push(arg);
|
|
58
|
+
}
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
command: positional[0] || 'start',
|
|
64
|
+
flags,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Helpers
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const AWARENESS_DIR = '.awareness';
|
|
73
|
+
const PID_FILENAME = 'daemon.pid';
|
|
74
|
+
const LOG_FILENAME = 'daemon.log';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the project directory from flags or cwd.
|
|
78
|
+
* @param {Record<string, string|boolean>} flags
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function resolveProjectDir(flags) {
|
|
82
|
+
const dir = typeof flags.project === 'string' ? flags.project : process.cwd();
|
|
83
|
+
return path.resolve(dir);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the daemon port from flags or default.
|
|
88
|
+
* @param {Record<string, string|boolean>} flags
|
|
89
|
+
* @returns {number}
|
|
90
|
+
*/
|
|
91
|
+
function resolvePort(flags) {
|
|
92
|
+
if (typeof flags.port === 'string') {
|
|
93
|
+
const p = parseInt(flags.port, 10);
|
|
94
|
+
if (!isNaN(p) && p > 0 && p < 65536) return p;
|
|
95
|
+
}
|
|
96
|
+
return 37800;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* HTTP GET to localhost — returns response body as string or null on error.
|
|
101
|
+
* @param {number} port
|
|
102
|
+
* @param {string} urlPath
|
|
103
|
+
* @param {number} [timeoutMs=3000]
|
|
104
|
+
* @returns {Promise<{ status: number, body: string }|null>}
|
|
105
|
+
*/
|
|
106
|
+
function httpGet(port, urlPath, timeoutMs = 3000) {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const req = http.get(
|
|
109
|
+
{ hostname: '127.0.0.1', port, path: urlPath, timeout: timeoutMs },
|
|
110
|
+
(res) => {
|
|
111
|
+
const chunks = [];
|
|
112
|
+
res.on('data', (c) => chunks.push(c));
|
|
113
|
+
res.on('end', () => {
|
|
114
|
+
resolve({
|
|
115
|
+
status: res.statusCode,
|
|
116
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
req.on('error', () => resolve(null));
|
|
122
|
+
req.on('timeout', () => {
|
|
123
|
+
req.destroy();
|
|
124
|
+
resolve(null);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Read PID from .awareness/daemon.pid.
|
|
131
|
+
* @param {string} projectDir
|
|
132
|
+
* @returns {number|null}
|
|
133
|
+
*/
|
|
134
|
+
function readPid(projectDir) {
|
|
135
|
+
const pidPath = path.join(projectDir, AWARENESS_DIR, PID_FILENAME);
|
|
136
|
+
try {
|
|
137
|
+
const content = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
138
|
+
const pid = parseInt(content, 10);
|
|
139
|
+
return isNaN(pid) ? null : pid;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a process with the given PID exists.
|
|
147
|
+
* @param {number} pid
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
function processExists(pid) {
|
|
151
|
+
try {
|
|
152
|
+
process.kill(pid, 0);
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Commands
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Start the daemon.
|
|
165
|
+
* Without --foreground: spawns a new detached process with --foreground flag.
|
|
166
|
+
* With --foreground: imports and runs the daemon in-process.
|
|
167
|
+
*/
|
|
168
|
+
async function cmdStart(flags) {
|
|
169
|
+
const projectDir = resolveProjectDir(flags);
|
|
170
|
+
const port = resolvePort(flags);
|
|
171
|
+
const foreground = flags.foreground === true;
|
|
172
|
+
|
|
173
|
+
// Ensure .awareness directory exists
|
|
174
|
+
const awarenessDir = path.join(projectDir, AWARENESS_DIR);
|
|
175
|
+
fs.mkdirSync(awarenessDir, { recursive: true });
|
|
176
|
+
|
|
177
|
+
// Check if already running
|
|
178
|
+
const pid = readPid(projectDir);
|
|
179
|
+
if (pid && processExists(pid)) {
|
|
180
|
+
const resp = await httpGet(port, '/healthz');
|
|
181
|
+
if (resp && resp.status === 200) {
|
|
182
|
+
console.log(`Awareness Local daemon already running (PID ${pid}, port ${port})`);
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (foreground) {
|
|
188
|
+
// Run in foreground — import daemon and start
|
|
189
|
+
const { AwarenessLocalDaemon } = await import('../src/daemon.mjs');
|
|
190
|
+
const daemon = new AwarenessLocalDaemon({ port, projectDir });
|
|
191
|
+
|
|
192
|
+
// Handle termination signals
|
|
193
|
+
const shutdown = async () => {
|
|
194
|
+
console.log('\n[awareness-local] shutting down...');
|
|
195
|
+
await daemon.stop();
|
|
196
|
+
process.exit(0);
|
|
197
|
+
};
|
|
198
|
+
process.on('SIGINT', shutdown);
|
|
199
|
+
process.on('SIGTERM', shutdown);
|
|
200
|
+
|
|
201
|
+
await daemon.start();
|
|
202
|
+
} else {
|
|
203
|
+
// Background mode: spawn self with --foreground
|
|
204
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
205
|
+
const logPath = path.join(awarenessDir, LOG_FILENAME);
|
|
206
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
207
|
+
|
|
208
|
+
const child = spawn(
|
|
209
|
+
process.execPath,
|
|
210
|
+
[thisFile, 'start', '--foreground', '--project', projectDir, '--port', String(port)],
|
|
211
|
+
{
|
|
212
|
+
detached: true,
|
|
213
|
+
stdio: ['ignore', logFd, logFd],
|
|
214
|
+
cwd: projectDir,
|
|
215
|
+
env: { ...process.env },
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
child.unref();
|
|
220
|
+
fs.closeSync(logFd);
|
|
221
|
+
|
|
222
|
+
// Wait for daemon to become healthy (up to 15 seconds)
|
|
223
|
+
console.log('Starting Awareness Local daemon...');
|
|
224
|
+
let healthy = false;
|
|
225
|
+
for (let i = 0; i < 30; i++) {
|
|
226
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
227
|
+
const resp = await httpGet(port, '/healthz');
|
|
228
|
+
if (resp && resp.status === 200) {
|
|
229
|
+
healthy = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (healthy) {
|
|
235
|
+
const newPid = readPid(projectDir);
|
|
236
|
+
console.log(`Awareness Local daemon started (PID ${newPid || child.pid}, port ${port})`);
|
|
237
|
+
console.log(` MCP endpoint: http://localhost:${port}/mcp`);
|
|
238
|
+
console.log(` Dashboard: http://localhost:${port}/`);
|
|
239
|
+
console.log(` Log file: ${logPath}`);
|
|
240
|
+
} else {
|
|
241
|
+
console.error('Failed to start daemon. Check log file:');
|
|
242
|
+
console.error(` ${logPath}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Stop the daemon.
|
|
250
|
+
*/
|
|
251
|
+
async function cmdStop(flags) {
|
|
252
|
+
const projectDir = resolveProjectDir(flags);
|
|
253
|
+
const pid = readPid(projectDir);
|
|
254
|
+
|
|
255
|
+
if (!pid) {
|
|
256
|
+
console.log('Awareness Local daemon is not running (no PID file found)');
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!processExists(pid)) {
|
|
261
|
+
// Stale PID file — clean up
|
|
262
|
+
const pidPath = path.join(projectDir, AWARENESS_DIR, PID_FILENAME);
|
|
263
|
+
try { fs.unlinkSync(pidPath); } catch { /* ignore */ }
|
|
264
|
+
console.log('Awareness Local daemon is not running (stale PID file removed)');
|
|
265
|
+
process.exit(0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Send SIGTERM
|
|
269
|
+
try {
|
|
270
|
+
process.kill(pid, 'SIGTERM');
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(`Failed to stop daemon (PID ${pid}): ${err.message}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Wait for process to exit (up to 5 seconds)
|
|
277
|
+
for (let i = 0; i < 10; i++) {
|
|
278
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
279
|
+
if (!processExists(pid)) break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Force kill if still alive
|
|
283
|
+
if (processExists(pid)) {
|
|
284
|
+
try {
|
|
285
|
+
process.kill(pid, 'SIGKILL');
|
|
286
|
+
} catch {
|
|
287
|
+
// ignore
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Clean PID file
|
|
292
|
+
const pidPath = path.join(projectDir, AWARENESS_DIR, PID_FILENAME);
|
|
293
|
+
try { fs.unlinkSync(pidPath); } catch { /* ignore */ }
|
|
294
|
+
|
|
295
|
+
console.log(`Awareness Local daemon stopped (was PID ${pid})`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Show daemon status and stats.
|
|
300
|
+
*/
|
|
301
|
+
async function cmdStatus(flags) {
|
|
302
|
+
const projectDir = resolveProjectDir(flags);
|
|
303
|
+
const port = resolvePort(flags);
|
|
304
|
+
const pid = readPid(projectDir);
|
|
305
|
+
|
|
306
|
+
if (!pid || !processExists(pid)) {
|
|
307
|
+
console.log('Awareness Local: not running');
|
|
308
|
+
if (pid) {
|
|
309
|
+
// Clean stale PID file
|
|
310
|
+
const pidPath = path.join(projectDir, AWARENESS_DIR, PID_FILENAME);
|
|
311
|
+
try { fs.unlinkSync(pidPath); } catch { /* ignore */ }
|
|
312
|
+
}
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Fetch health info
|
|
317
|
+
const resp = await httpGet(port, '/healthz');
|
|
318
|
+
if (!resp || resp.status !== 200) {
|
|
319
|
+
console.log(`Awareness Local: PID ${pid} exists but HTTP not responding on port ${port}`);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const data = JSON.parse(resp.body);
|
|
325
|
+
const uptime = data.uptime || 0;
|
|
326
|
+
const hours = Math.floor(uptime / 3600);
|
|
327
|
+
const minutes = Math.floor((uptime % 3600) / 60);
|
|
328
|
+
const uptimeStr = hours > 0
|
|
329
|
+
? `${hours}h ${minutes}m`
|
|
330
|
+
: `${minutes}m ${uptime % 60}s`;
|
|
331
|
+
|
|
332
|
+
console.log(`Awareness Local: running (PID ${pid}, port ${port})`);
|
|
333
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
334
|
+
console.log(` Project: ${data.project_dir || projectDir}`);
|
|
335
|
+
|
|
336
|
+
if (data.stats) {
|
|
337
|
+
const s = data.stats;
|
|
338
|
+
console.log(` Memories: ${s.totalMemories || 0}`);
|
|
339
|
+
console.log(` Knowledge Cards: ${s.totalKnowledge || 0}`);
|
|
340
|
+
console.log(` Open Tasks: ${s.totalTasks || 0}`);
|
|
341
|
+
console.log(` Sessions: ${s.totalSessions || 0}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check cloud sync status
|
|
345
|
+
const awarenessDir = path.join(projectDir, AWARENESS_DIR);
|
|
346
|
+
const configPath = path.join(awarenessDir, 'config.json');
|
|
347
|
+
if (fs.existsSync(configPath)) {
|
|
348
|
+
try {
|
|
349
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
350
|
+
if (config.cloud?.enabled) {
|
|
351
|
+
console.log(` Cloud Sync: enabled (${config.cloud.api_base || 'awareness.market'})`);
|
|
352
|
+
} else {
|
|
353
|
+
console.log(' Cloud Sync: not configured');
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
console.log(' Cloud Sync: unknown');
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
console.log(' Cloud Sync: not configured');
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
console.log(`Awareness Local: running (PID ${pid})`);
|
|
363
|
+
console.log(` Raw response: ${resp.body}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Rebuild the FTS5 + embedding index.
|
|
369
|
+
*/
|
|
370
|
+
async function cmdReindex(flags) {
|
|
371
|
+
const projectDir = resolveProjectDir(flags);
|
|
372
|
+
const port = resolvePort(flags);
|
|
373
|
+
|
|
374
|
+
// Check if daemon is running — if so, it holds a lock on index.db
|
|
375
|
+
const pid = readPid(projectDir);
|
|
376
|
+
const daemonRunning = pid && processExists(pid);
|
|
377
|
+
|
|
378
|
+
if (daemonRunning) {
|
|
379
|
+
console.log('Daemon is running — stopping it first for safe reindex...');
|
|
380
|
+
await cmdStop(flags);
|
|
381
|
+
// Brief pause for SQLite lock release
|
|
382
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log('Rebuilding index...');
|
|
386
|
+
|
|
387
|
+
const awarenessDir = path.join(projectDir, AWARENESS_DIR);
|
|
388
|
+
const dbPath = path.join(awarenessDir, 'index.db');
|
|
389
|
+
|
|
390
|
+
// Remove existing database to force full rebuild
|
|
391
|
+
for (const ext of ['', '-journal', '-wal', '-shm']) {
|
|
392
|
+
const p = dbPath + ext;
|
|
393
|
+
if (fs.existsSync(p)) {
|
|
394
|
+
fs.unlinkSync(p);
|
|
395
|
+
console.log(` Removed: ${path.basename(p)}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Import and run indexer
|
|
400
|
+
try {
|
|
401
|
+
const { Indexer } = await import('../src/core/indexer.mjs');
|
|
402
|
+
const { MemoryStore } = await import('../src/core/memory-store.mjs');
|
|
403
|
+
|
|
404
|
+
const store = new MemoryStore(projectDir);
|
|
405
|
+
const indexer = new Indexer(dbPath);
|
|
406
|
+
|
|
407
|
+
const result = await indexer.incrementalIndex(store);
|
|
408
|
+
console.log(`Reindex complete: ${result.indexed} files indexed, ${result.skipped} skipped`);
|
|
409
|
+
|
|
410
|
+
indexer.close();
|
|
411
|
+
} catch (err) {
|
|
412
|
+
console.error(`Reindex failed: ${err.message}`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Restart daemon if it was running
|
|
417
|
+
if (daemonRunning) {
|
|
418
|
+
console.log('Restarting daemon...');
|
|
419
|
+
await cmdStart({ ...flags, foreground: undefined });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Help
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
function printHelp() {
|
|
428
|
+
console.log(`
|
|
429
|
+
Awareness Local — AI agent memory daemon
|
|
430
|
+
|
|
431
|
+
Usage:
|
|
432
|
+
awareness-local <command> [options]
|
|
433
|
+
|
|
434
|
+
Commands:
|
|
435
|
+
start Start the daemon (default)
|
|
436
|
+
stop Stop the daemon
|
|
437
|
+
status Show daemon status and stats
|
|
438
|
+
reindex Rebuild the search index
|
|
439
|
+
|
|
440
|
+
Options:
|
|
441
|
+
--project <dir> Project directory (default: current directory)
|
|
442
|
+
--port <port> HTTP port (default: 37800)
|
|
443
|
+
--foreground Run in foreground (don't detach)
|
|
444
|
+
--help Show this help message
|
|
445
|
+
|
|
446
|
+
Examples:
|
|
447
|
+
npx @awareness-sdk/local start
|
|
448
|
+
npx @awareness-sdk/local status
|
|
449
|
+
npx @awareness-sdk/local stop
|
|
450
|
+
npx @awareness-sdk/local reindex --project /path/to/project
|
|
451
|
+
`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Main
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
async function main() {
|
|
459
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
460
|
+
|
|
461
|
+
if (flags.help || command === 'help') {
|
|
462
|
+
printHelp();
|
|
463
|
+
process.exit(0);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
switch (command) {
|
|
467
|
+
case 'start':
|
|
468
|
+
await cmdStart(flags);
|
|
469
|
+
break;
|
|
470
|
+
case 'stop':
|
|
471
|
+
await cmdStop(flags);
|
|
472
|
+
break;
|
|
473
|
+
case 'status':
|
|
474
|
+
await cmdStatus(flags);
|
|
475
|
+
break;
|
|
476
|
+
case 'reindex':
|
|
477
|
+
await cmdReindex(flags);
|
|
478
|
+
break;
|
|
479
|
+
default:
|
|
480
|
+
console.error(`Unknown command: ${command}`);
|
|
481
|
+
printHelp();
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
main().catch((err) => {
|
|
487
|
+
console.error(`Fatal error: ${err.message}`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@awareness-sdk/local",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local-first AI agent memory system. No account needed.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"awareness-local": "./bin/awareness-local.mjs"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/api.mjs",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/api.mjs",
|
|
13
|
+
"./api": "./src/api.mjs"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"better-sqlite3": "^11.0.0",
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.27.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/better-sqlite3": "^7.6.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["ai", "agent", "memory", "local", "mcp", "claude", "cursor", "windsurf", "awareness"],
|
|
26
|
+
"files": ["bin/", "src/", "LICENSE", "README.md"],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test test/*.test.mjs",
|
|
29
|
+
"start": "node bin/awareness-local.mjs start"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/api.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for @awareness-sdk/local
|
|
3
|
+
*
|
|
4
|
+
* Exports high-level functions for external callers (setup-cli, plugins, etc.).
|
|
5
|
+
* This is the package's main entry point (package.json "main" / "exports").
|
|
6
|
+
*
|
|
7
|
+
* Two categories:
|
|
8
|
+
* 1. Re-exports from core/ — directory management & config
|
|
9
|
+
* 2. Daemon management — lightweight HTTP-based checks (no heavy imports)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import http from 'node:http';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Re-exports from core modules
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
ensureLocalDirs,
|
|
20
|
+
initLocalConfig,
|
|
21
|
+
loadLocalConfig,
|
|
22
|
+
saveCloudConfig,
|
|
23
|
+
getConfigPath,
|
|
24
|
+
generateDeviceId,
|
|
25
|
+
} from './core/config.mjs';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Daemon management (HTTP-based, no import of daemon internals)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the daemon base URL.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} [port=37800]
|
|
35
|
+
* @returns {string} e.g. "http://localhost:37800"
|
|
36
|
+
*/
|
|
37
|
+
export function getDaemonUrl(port = 37800) {
|
|
38
|
+
return `http://localhost:${port}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if the local daemon is healthy.
|
|
43
|
+
* Performs a GET to localhost:{port}/healthz and returns true if status 200.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} [port=37800]
|
|
46
|
+
* @param {number} [timeoutMs=2000]
|
|
47
|
+
* @returns {Promise<boolean>}
|
|
48
|
+
*/
|
|
49
|
+
export async function checkDaemonHealth(port = 37800, timeoutMs = 2000) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const req = http.get(
|
|
52
|
+
{
|
|
53
|
+
hostname: '127.0.0.1',
|
|
54
|
+
port,
|
|
55
|
+
path: '/healthz',
|
|
56
|
+
timeout: timeoutMs,
|
|
57
|
+
},
|
|
58
|
+
(res) => {
|
|
59
|
+
// Drain the response body
|
|
60
|
+
res.resume();
|
|
61
|
+
resolve(res.statusCode === 200);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
req.on('error', () => resolve(false));
|
|
65
|
+
req.on('timeout', () => {
|
|
66
|
+
req.destroy();
|
|
67
|
+
resolve(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the daemon status including stats.
|
|
74
|
+
* Returns the parsed /healthz JSON, or null if the daemon is not reachable.
|
|
75
|
+
*
|
|
76
|
+
* @param {number} [port=37800]
|
|
77
|
+
* @param {number} [timeoutMs=3000]
|
|
78
|
+
* @returns {Promise<object|null>} — { status, mode, version, uptime, pid, port, project_dir, stats } or null
|
|
79
|
+
*/
|
|
80
|
+
export async function getDaemonStatus(port = 37800, timeoutMs = 3000) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const req = http.get(
|
|
83
|
+
{
|
|
84
|
+
hostname: '127.0.0.1',
|
|
85
|
+
port,
|
|
86
|
+
path: '/healthz',
|
|
87
|
+
timeout: timeoutMs,
|
|
88
|
+
},
|
|
89
|
+
(res) => {
|
|
90
|
+
const chunks = [];
|
|
91
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
92
|
+
res.on('end', () => {
|
|
93
|
+
if (res.statusCode !== 200) {
|
|
94
|
+
resolve(null);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
99
|
+
resolve(JSON.parse(body));
|
|
100
|
+
} catch {
|
|
101
|
+
resolve(null);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
req.on('error', () => resolve(null));
|
|
107
|
+
req.on('timeout', () => {
|
|
108
|
+
req.destroy();
|
|
109
|
+
resolve(null);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get the MCP endpoint URL for the local daemon.
|
|
116
|
+
*
|
|
117
|
+
* @param {number} [port=37800]
|
|
118
|
+
* @returns {string} e.g. "http://localhost:37800/mcp"
|
|
119
|
+
*/
|
|
120
|
+
export function getMcpUrl(port = 37800) {
|
|
121
|
+
return `http://localhost:${port}/mcp`;
|
|
122
|
+
}
|