@awareness-sdk/local 0.1.8 → 0.1.10
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/package.json +1 -1
- package/src/core/cloud-sync.mjs +7 -0
- package/src/core/indexer.mjs +92 -2
- package/src/core/search.mjs +3 -3
- package/src/daemon.mjs +38 -6
- package/src/web/index.html +8 -2
package/package.json
CHANGED
package/src/core/cloud-sync.mjs
CHANGED
|
@@ -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
|
}
|
package/src/core/indexer.mjs
CHANGED
|
@@ -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='
|
|
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='
|
|
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
|
// -----------------------------------------------------------------------
|
package/src/core/search.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
514
|
-
const similarity = cosineSimilarity(queryVec, item.
|
|
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,7 +16,8 @@
|
|
|
16
16
|
import http from 'node:http';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
-
import {
|
|
19
|
+
import { execFile } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
21
|
|
|
21
22
|
import { MemoryStore } from './core/memory-store.mjs';
|
|
22
23
|
import { Indexer } from './core/indexer.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
|
-
//
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1784,7 +1816,7 @@ ${item.description || item.title || ''}
|
|
|
1784
1816
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1785
1817
|
const modPath = path.join(thisDir, 'core', 'search.mjs');
|
|
1786
1818
|
if (fs.existsSync(modPath)) {
|
|
1787
|
-
const mod = await import(modPath);
|
|
1819
|
+
const mod = await import(pathToFileURL(modPath).href);
|
|
1788
1820
|
const SearchEngine = mod.SearchEngine || mod.default;
|
|
1789
1821
|
if (SearchEngine) {
|
|
1790
1822
|
return new SearchEngine(this.indexer, this.memoryStore);
|
|
@@ -1802,7 +1834,7 @@ ${item.description || item.title || ''}
|
|
|
1802
1834
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
1803
1835
|
const modPath = path.join(thisDir, 'core', 'knowledge-extractor.mjs');
|
|
1804
1836
|
if (fs.existsSync(modPath)) {
|
|
1805
|
-
const mod = await import(modPath);
|
|
1837
|
+
const mod = await import(pathToFileURL(modPath).href);
|
|
1806
1838
|
const KnowledgeExtractor = mod.KnowledgeExtractor || mod.default;
|
|
1807
1839
|
if (KnowledgeExtractor) {
|
|
1808
1840
|
return new KnowledgeExtractor(this.memoryStore, this.indexer);
|
package/src/web/index.html
CHANGED
|
@@ -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
|
|
789
|
-
|
|
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', {
|