@awareness-sdk/local 0.1.9 → 0.1.11

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.
@@ -237,6 +237,19 @@ async function cmdStart(flags) {
237
237
  console.log(` MCP endpoint: http://localhost:${port}/mcp`);
238
238
  console.log(` Dashboard: http://localhost:${port}/`);
239
239
  console.log(` Log file: ${logPath}`);
240
+
241
+ // Auto-open dashboard on first daemon start
242
+ const firstRunFlag = path.join(awarenessDir, '.first-run-done');
243
+ if (!fs.existsSync(firstRunFlag)) {
244
+ try {
245
+ fs.writeFileSync(firstRunFlag, new Date().toISOString());
246
+ const url = `http://localhost:${port}/`;
247
+ const { exec } = await import('node:child_process');
248
+ if (process.platform === 'darwin') exec(`open "${url}"`);
249
+ else if (process.platform === 'linux') exec(`xdg-open "${url}"`);
250
+ else if (process.platform === 'win32') exec(`start "" "${url}"`);
251
+ } catch { /* ignore open failures */ }
252
+ }
240
253
  } else {
241
254
  console.error('Failed to start daemon. Check log file:');
242
255
  console.error(` ${logPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awareness-sdk/local",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Local-first AI agent memory system. No account needed.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -644,6 +644,13 @@ export class CloudSync {
644
644
  });
645
645
 
646
646
  } catch (err) {
647
+ // SSE is optional (server may not support it yet) — silently fall back
648
+ if (err.message && err.message.includes('404')) {
649
+ if (!this._sseRetryCount) {
650
+ console.log(`${LOG_PREFIX} SSE not available on server — using periodic sync only`);
651
+ }
652
+ return; // Don't retry on 404; periodic sync is the fallback
653
+ }
647
654
  console.warn(`${LOG_PREFIX} SSE connection failed:`, err.message);
648
655
  this._scheduleSSEReconnect();
649
656
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import Database from 'better-sqlite3';
9
9
  import { createHash } from 'node:crypto';
10
+ import { readFileSync, existsSync } from 'node:fs';
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // Schema DDL
@@ -31,7 +32,7 @@ CREATE TABLE IF NOT EXISTS memories (
31
32
 
32
33
  CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
33
34
  id UNINDEXED, title, content, tags,
34
- tokenize='unicode61 remove_diacritics 2'
35
+ tokenize='trigram'
35
36
  );
36
37
 
37
38
  CREATE TABLE IF NOT EXISTS knowledge_cards (
@@ -50,7 +51,7 @@ CREATE TABLE IF NOT EXISTS knowledge_cards (
50
51
 
51
52
  CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
52
53
  id UNINDEXED, title, summary, content, tags,
53
- tokenize='unicode61 remove_diacritics 2'
54
+ tokenize='trigram'
54
55
  );
55
56
 
56
57
  CREATE TABLE IF NOT EXISTS tasks (
@@ -155,6 +156,26 @@ export class Indexer {
155
156
 
156
157
  this.initSchema();
157
158
  this._prepareStatements();
159
+ this._reindexFts();
160
+ this._checkFtsSyncHealth();
161
+ }
162
+
163
+ /**
164
+ * If FTS row count is less than memories row count, reindex missing entries.
165
+ * Handles cases where migration dropped FTS data or records were added without FTS.
166
+ */
167
+ _checkFtsSyncHealth() {
168
+ try {
169
+ const memCount = this.db.prepare('SELECT count(*) AS c FROM memories').get().c;
170
+ const ftsCount = this.db.prepare('SELECT count(*) AS c FROM memories_fts').get().c;
171
+ if (memCount > 0 && ftsCount < memCount) {
172
+ console.log(`[indexer] FTS out of sync (${ftsCount}/${memCount}) — rebuilding missing entries...`);
173
+ this._ftsNeedsReindex = true;
174
+ this._reindexFts();
175
+ }
176
+ } catch {
177
+ // Skip if tables don't exist yet
178
+ }
158
179
  }
159
180
 
160
181
  // -----------------------------------------------------------------------
@@ -166,9 +187,78 @@ export class Indexer {
166
187
  * Safe to call repeatedly — every statement uses IF NOT EXISTS.
167
188
  */
168
189
  initSchema() {
190
+ // Migrate FTS5 tables from unicode61 to trigram (CJK support)
191
+ this._migrateFtsTokenizer();
169
192
  this.db.exec(SCHEMA_SQL);
170
193
  }
171
194
 
195
+ /**
196
+ * If existing FTS5 tables use unicode61 tokenizer, recreate them with trigram.
197
+ * This enables Chinese/Japanese/Korean full-text search.
198
+ */
199
+ _migrateFtsTokenizer() {
200
+ let migrated = false;
201
+ for (const table of ['memories_fts', 'knowledge_fts']) {
202
+ try {
203
+ const row = this.db.prepare(
204
+ `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`
205
+ ).get(table);
206
+ if (row && row.sql && row.sql.includes('unicode61')) {
207
+ this.db.exec(`DROP TABLE IF EXISTS ${table}`);
208
+ migrated = true;
209
+ }
210
+ } catch {
211
+ // Table doesn't exist yet — will be created by SCHEMA_SQL
212
+ }
213
+ }
214
+ this._ftsNeedsReindex = migrated;
215
+ }
216
+
217
+ /**
218
+ * Rebuild FTS5 indexes from source tables after tokenizer migration.
219
+ * Called after schema init + prepared statements are ready.
220
+ */
221
+ _reindexFts() {
222
+ if (!this._ftsNeedsReindex) return;
223
+ console.log('[indexer] Rebuilding FTS indexes after tokenizer migration...');
224
+ // Repopulate memories_fts from memories table + markdown files
225
+ const memories = this.db.prepare('SELECT id, title, tags, filepath FROM memories').all();
226
+ for (const m of memories) {
227
+ try {
228
+ const content = m.filepath && existsSync(m.filepath)
229
+ ? readFileSync(m.filepath, 'utf-8')
230
+ : (m.title || '');
231
+ this._stmtDeleteFts.run(m.id);
232
+ this._stmtInsertFts.run({
233
+ id: m.id,
234
+ title: m.title || '',
235
+ content,
236
+ tags: m.tags || '',
237
+ });
238
+ } catch {
239
+ // Skip files that can't be read
240
+ }
241
+ }
242
+ // Repopulate knowledge_fts
243
+ const cards = this.db.prepare('SELECT id, title, summary, tags FROM knowledge_cards').all();
244
+ for (const c of cards) {
245
+ try {
246
+ this._stmtDeleteKnowledgeFts.run(c.id);
247
+ this._stmtInsertKnowledgeFts.run({
248
+ id: c.id,
249
+ title: c.title || '',
250
+ summary: c.summary || '',
251
+ content: c.summary || '',
252
+ tags: c.tags || '',
253
+ });
254
+ } catch {
255
+ // Skip
256
+ }
257
+ }
258
+ console.log(`[indexer] FTS reindex done: ${memories.length} memories, ${cards.length} cards`);
259
+ this._ftsNeedsReindex = false;
260
+ }
261
+
172
262
  // -----------------------------------------------------------------------
173
263
  // Prepared-statement cache (lazy, created once)
174
264
  // -----------------------------------------------------------------------
@@ -337,7 +337,7 @@ export class SearchEngine {
337
337
  .split(/\s+/)
338
338
  .filter((w) => w.length > 0);
339
339
  for (const w of words) {
340
- // Quote each word for safety (handles special chars)
340
+ // Quote each word trigram tokenizer handles CJK natively
341
341
  terms.push(`"${w.replace(/"/g, '')}"`);
342
342
  }
343
343
  }
@@ -510,8 +510,8 @@ export class SearchEngine {
510
510
  // Compute cosine similarity for each
511
511
  const scored = [];
512
512
  for (const item of allEmbeddings) {
513
- if (!item.embedding) continue;
514
- const similarity = cosineSimilarity(queryVec, item.embedding);
513
+ if (!item.vector) continue;
514
+ const similarity = cosineSimilarity(queryVec, item.vector);
515
515
  if (similarity > 0.1) {
516
516
  scored.push({
517
517
  ...item,
package/src/daemon.mjs CHANGED
@@ -16,6 +16,7 @@
16
16
  import http from 'node:http';
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
+ import { execFile } from 'node:child_process';
19
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
21
 
21
22
  import { MemoryStore } from './core/memory-store.mjs';
@@ -886,6 +887,11 @@ export class AwarenessLocalDaemon {
886
887
  return await this._apiCloudAuthPoll(req, res);
887
888
  }
888
889
 
890
+ // POST /api/v1/cloud/auth/open-browser — open URL in system browser
891
+ if (route === '/cloud/auth/open-browser' && req.method === 'POST') {
892
+ return await this._apiCloudAuthOpenBrowser(req, res);
893
+ }
894
+
889
895
  // GET /api/v1/cloud/memories — list memories (after auth)
890
896
  if (route.startsWith('/cloud/memories') && req.method === 'GET') {
891
897
  return await this._apiCloudListMemories(req, res, url);
@@ -1137,6 +1143,27 @@ export class AwarenessLocalDaemon {
1137
1143
  // Cloud Auth API (device-auth flow from Dashboard)
1138
1144
  // -----------------------------------------------------------------------
1139
1145
 
1146
+ async _apiCloudAuthOpenBrowser(req, res) {
1147
+ const body = await readBody(req);
1148
+ let params;
1149
+ try { params = JSON.parse(body); } catch { return jsonResponse(res, { error: 'Invalid JSON' }, 400); }
1150
+ const { url: targetUrl } = params;
1151
+ if (!targetUrl || typeof targetUrl !== 'string') {
1152
+ return jsonResponse(res, { error: 'url required' }, 400);
1153
+ }
1154
+ // Only allow opening our own auth URLs
1155
+ if (!targetUrl.startsWith('https://awareness.market/')) {
1156
+ return jsonResponse(res, { error: 'URL not allowed' }, 403);
1157
+ }
1158
+ const cmd = process.platform === 'darwin' ? 'open'
1159
+ : process.platform === 'win32' ? 'start'
1160
+ : 'xdg-open';
1161
+ execFile(cmd, [targetUrl], (err) => {
1162
+ if (err) console.warn('[awareness-local] failed to open browser:', err.message);
1163
+ });
1164
+ return jsonResponse(res, { status: 'ok' });
1165
+ }
1166
+
1140
1167
  async _apiCloudAuthStart(_req, res) {
1141
1168
  const apiBase = this.config?.cloud?.api_base || 'https://awareness.market/api/v1';
1142
1169
  try {
@@ -1178,9 +1205,10 @@ export class AwarenessLocalDaemon {
1178
1205
  }
1179
1206
 
1180
1207
  async _apiCloudListMemories(req, res, url) {
1181
- // SECURITY C3: Use cloud config API key instead of query param (avoid log/referer leaks)
1208
+ // Accept api_key from query param (during auth flow, before config is saved)
1209
+ // or fall back to saved config (for subsequent calls)
1182
1210
  const config = this._loadConfig();
1183
- const apiKey = config?.cloud?.api_key;
1211
+ const apiKey = url.searchParams.get('api_key') || config?.cloud?.api_key;
1184
1212
  if (!apiKey) return jsonResponse(res, { error: 'Cloud not configured. Connect via /api/v1/cloud/connect first.' }, 400);
1185
1213
 
1186
1214
  const apiBase = this.config?.cloud?.api_base || 'https://awareness.market/api/v1';
@@ -1370,7 +1398,11 @@ export class AwarenessLocalDaemon {
1370
1398
 
1371
1399
  // Cloud sync (fire-and-forget — don't block the response)
1372
1400
  if (this.cloudSync?.isEnabled()) {
1373
- this.cloudSync.syncToCloud().catch((err) => {
1401
+ Promise.all([
1402
+ this.cloudSync.syncToCloud(),
1403
+ this.cloudSync.syncInsightsToCloud(),
1404
+ this.cloudSync.syncTasksToCloud(),
1405
+ ]).catch((err) => {
1374
1406
  console.warn('[awareness-local] cloud sync after remember failed:', err.message);
1375
1407
  });
1376
1408
  }
@@ -1390,14 +1422,19 @@ export class AwarenessLocalDaemon {
1390
1422
  return { error: 'items array is required for remember_batch' };
1391
1423
  }
1392
1424
 
1425
+ // Batch-level insights go to the last item (summary item)
1426
+ const batchInsights = params.insights || null;
1427
+
1393
1428
  const results = [];
1394
- for (const item of items) {
1429
+ for (let i = 0; i < items.length; i++) {
1430
+ const item = items[i];
1431
+ const isLast = i === items.length - 1;
1395
1432
  const result = await this._remember({
1396
1433
  content: item.content,
1397
1434
  title: item.title,
1398
1435
  event_type: item.event_type,
1399
1436
  tags: item.tags,
1400
- insights: item.insights,
1437
+ insights: item.insights || (isLast ? batchInsights : null),
1401
1438
  session_id: params.session_id,
1402
1439
  agent_role: params.agent_role,
1403
1440
  });
@@ -785,8 +785,14 @@ async function startDeviceAuth() {
785
785
  link.href = data.verification_uri + '?code=' + data.user_code;
786
786
  link.textContent = data.verification_uri;
787
787
 
788
- // Open browser automatically
789
- window.open(link.href, '_blank');
788
+ // Open browser via daemon (bypasses popup blockers)
789
+ api('/cloud/auth/open-browser', {
790
+ method: 'POST',
791
+ body: JSON.stringify({ url: link.href })
792
+ }).catch(function() {
793
+ // Fallback to window.open if daemon endpoint fails
794
+ window.open(link.href, '_blank');
795
+ });
790
796
 
791
797
  // Poll for authorization
792
798
  var result = await api('/cloud/auth/poll', {