@awareness-sdk/local 0.1.6 → 0.1.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awareness-sdk/local",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Local-first AI agent memory system. No account needed.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -437,14 +437,155 @@ export class CloudSync {
437
437
  }
438
438
 
439
439
  /**
440
- * Full bidirectional sync: push then pull.
441
- * @returns {Promise<{ pushed: number, pulled: number }>}
440
+ * Push unsynced knowledge cards to the cloud.
441
+ * Uses POST /memories/{id}/insights/submit with action:"new".
442
+ *
443
+ * @returns {Promise<{ synced: number, errors: number }>}
444
+ */
445
+ async syncInsightsToCloud() {
446
+ if (!this.isEnabled()) return { synced: 0, errors: 0 };
447
+
448
+ let synced = 0;
449
+ let errors = 0;
450
+
451
+ try {
452
+ const unsynced = this.indexer.db
453
+ .prepare("SELECT * FROM knowledge_cards WHERE synced_to_cloud = 0 AND status = 'active' ORDER BY created_at")
454
+ .all();
455
+
456
+ if (!unsynced.length) return { synced: 0, errors: 0 };
457
+
458
+ // Batch cards in groups of 10 to reduce API calls
459
+ const batchSize = 10;
460
+ for (let i = 0; i < unsynced.length; i += batchSize) {
461
+ const batch = unsynced.slice(i, i + batchSize);
462
+ const cards = batch.map((card) => ({
463
+ title: card.title,
464
+ summary: card.summary || '',
465
+ category: card.category,
466
+ confidence: card.confidence || 0.8,
467
+ tags: this._parseTags(card.tags),
468
+ action: 'new',
469
+ }));
470
+
471
+ try {
472
+ const result = await this._post(
473
+ `/memories/${this.memoryId}/insights/submit`,
474
+ {
475
+ session_id: `local-sync-${this.deviceId}`,
476
+ knowledge_cards: cards,
477
+ metadata: { device_id: this.deviceId, source: 'awareness-local' },
478
+ }
479
+ );
480
+
481
+ if (result) {
482
+ // Mark batch as synced
483
+ const markStmt = this.indexer.db.prepare(
484
+ 'UPDATE knowledge_cards SET synced_to_cloud = 1 WHERE id = ?'
485
+ );
486
+ for (const card of batch) {
487
+ markStmt.run(card.id);
488
+ }
489
+ synced += batch.length;
490
+ } else {
491
+ errors += batch.length;
492
+ }
493
+ } catch (err) {
494
+ console.warn(`${LOG_PREFIX} Failed to push insight batch:`, err.message);
495
+ errors += batch.length;
496
+ }
497
+ }
498
+
499
+ if (synced > 0) {
500
+ console.log(`${LOG_PREFIX} Pushed ${synced} knowledge cards to cloud` + (errors ? ` (${errors} errors)` : ''));
501
+ }
502
+ } catch (err) {
503
+ console.error(`${LOG_PREFIX} syncInsightsToCloud failed:`, err.message);
504
+ }
505
+
506
+ return { synced, errors };
507
+ }
508
+
509
+ /**
510
+ * Push unsynced tasks (action items) to the cloud.
511
+ * Uses POST /memories/{id}/insights/submit with action_items.
512
+ *
513
+ * @returns {Promise<{ synced: number, errors: number }>}
514
+ */
515
+ async syncTasksToCloud() {
516
+ if (!this.isEnabled()) return { synced: 0, errors: 0 };
517
+
518
+ let synced = 0;
519
+ let errors = 0;
520
+
521
+ try {
522
+ const unsynced = this.indexer.db
523
+ .prepare("SELECT * FROM tasks WHERE synced_to_cloud = 0 ORDER BY created_at")
524
+ .all();
525
+
526
+ if (!unsynced.length) return { synced: 0, errors: 0 };
527
+
528
+ const batchSize = 10;
529
+ for (let i = 0; i < unsynced.length; i += batchSize) {
530
+ const batch = unsynced.slice(i, i + batchSize);
531
+ const items = batch.map((task) => ({
532
+ title: task.title,
533
+ detail: task.description || '',
534
+ priority: task.priority || 'medium',
535
+ status: task.status || 'open',
536
+ agent_role: task.agent_role || '',
537
+ }));
538
+
539
+ try {
540
+ const result = await this._post(
541
+ `/memories/${this.memoryId}/insights/submit`,
542
+ {
543
+ session_id: `local-sync-${this.deviceId}`,
544
+ action_items: items,
545
+ metadata: { device_id: this.deviceId, source: 'awareness-local' },
546
+ }
547
+ );
548
+
549
+ if (result) {
550
+ const markStmt = this.indexer.db.prepare(
551
+ 'UPDATE tasks SET synced_to_cloud = 1 WHERE id = ?'
552
+ );
553
+ for (const task of batch) {
554
+ markStmt.run(task.id);
555
+ }
556
+ synced += batch.length;
557
+ } else {
558
+ errors += batch.length;
559
+ }
560
+ } catch (err) {
561
+ console.warn(`${LOG_PREFIX} Failed to push task batch:`, err.message);
562
+ errors += batch.length;
563
+ }
564
+ }
565
+
566
+ if (synced > 0) {
567
+ console.log(`${LOG_PREFIX} Pushed ${synced} tasks to cloud` + (errors ? ` (${errors} errors)` : ''));
568
+ }
569
+ } catch (err) {
570
+ console.error(`${LOG_PREFIX} syncTasksToCloud failed:`, err.message);
571
+ }
572
+
573
+ return { synced, errors };
574
+ }
575
+
576
+ /**
577
+ * Full bidirectional sync: push memories + insights + tasks, then pull.
578
+ * @returns {Promise<{ pushed: number, pulled: number, insights_pushed: number, tasks_pushed: number }>}
442
579
  */
443
580
  async fullSync() {
444
581
  const pushResult = await this.syncToCloud();
582
+ const insightsResult = await this.syncInsightsToCloud();
583
+ const tasksResult = await this.syncTasksToCloud();
445
584
  const pullResult = await this.pullFromCloud();
446
585
  return {
447
586
  pushed: pushResult.synced,
587
+ insights_pushed: insightsResult.synced,
588
+ tasks_pushed: tasksResult.synced,
448
589
  pulled: pullResult.pulled,
449
590
  };
450
591
  }
@@ -518,9 +659,9 @@ export class CloudSync {
518
659
  this._periodicTimer = setInterval(async () => {
519
660
  try {
520
661
  const result = await this.fullSync();
521
- if (result.pushed > 0 || result.pulled > 0) {
662
+ if (result.pushed > 0 || result.pulled > 0 || result.insights_pushed > 0 || result.tasks_pushed > 0) {
522
663
  console.log(
523
- `${LOG_PREFIX} Periodic sync: pushed ${result.pushed}, pulled ${result.pulled}`
664
+ `${LOG_PREFIX} Periodic sync: memories=${result.pushed}, insights=${result.insights_pushed}, tasks=${result.tasks_pushed}, pulled=${result.pulled}`
524
665
  );
525
666
  }
526
667
  } catch (err) {
@@ -552,8 +693,10 @@ export class CloudSync {
552
693
  }
553
694
 
554
695
  try {
555
- // Push unsynced
696
+ // Push unsynced memories, knowledge cards, and tasks
556
697
  await this.syncToCloud();
698
+ await this.syncInsightsToCloud();
699
+ await this.syncTasksToCloud();
557
700
  } catch (err) {
558
701
  console.warn(`${LOG_PREFIX} Initial push failed (will retry):`, err.message);
559
702
  }
@@ -637,7 +780,8 @@ export class CloudSync {
637
780
  break;
638
781
  }
639
782
 
640
- case 'knowledge_extracted': {
783
+ case 'knowledge_extracted':
784
+ case 'insight_submitted': {
641
785
  if (data.device_id === this.deviceId) return;
642
786
  try {
643
787
  await this._pullKnowledgeCard(data);
@@ -647,6 +791,17 @@ export class CloudSync {
647
791
  break;
648
792
  }
649
793
 
794
+ case 'task_created':
795
+ case 'task_updated': {
796
+ if (data.device_id === this.deviceId) return;
797
+ try {
798
+ await this._pullTask(data);
799
+ } catch (err) {
800
+ console.warn(`${LOG_PREFIX} SSE task pull failed:`, err.message);
801
+ }
802
+ break;
803
+ }
804
+
650
805
  case 'heartbeat':
651
806
  // Keepalive — no action needed
652
807
  break;
@@ -790,8 +945,8 @@ export class CloudSync {
790
945
  this.indexer.db
791
946
  .prepare(
792
947
  `INSERT OR IGNORE INTO knowledge_cards
793
- (id, category, title, summary, source_memories, confidence, status, tags, created_at, filepath)
794
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
948
+ (id, category, title, summary, source_memories, confidence, status, tags, created_at, filepath, synced_to_cloud)
949
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
795
950
  )
796
951
  .run(
797
952
  kcId,
@@ -815,6 +970,57 @@ export class CloudSync {
815
970
  }
816
971
  }
817
972
 
973
+ /**
974
+ * Pull a task from cloud SSE event data and store locally.
975
+ * @param {object} data — { id, title, detail, priority, status, agent_role, ... }
976
+ */
977
+ async _pullTask(data) {
978
+ if (!data.title) return;
979
+
980
+ // Check if we already have this task
981
+ const existing = this._getSyncState(`cloud_task:${data.id}`);
982
+ if (existing) {
983
+ // Task update — update status/priority if changed
984
+ try {
985
+ this.indexer.db
986
+ .prepare('UPDATE tasks SET status = ?, priority = ?, updated_at = ? WHERE id = ?')
987
+ .run(data.status || 'open', data.priority || 'medium', new Date().toISOString(), existing);
988
+ } catch {
989
+ // ignore update failures
990
+ }
991
+ return;
992
+ }
993
+
994
+ const taskId = `task_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
995
+ const now = new Date().toISOString();
996
+
997
+ try {
998
+ this.indexer.db
999
+ .prepare(
1000
+ `INSERT OR IGNORE INTO tasks
1001
+ (id, title, description, status, priority, agent_role, created_at, updated_at, filepath, synced_to_cloud)
1002
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
1003
+ )
1004
+ .run(
1005
+ taskId,
1006
+ data.title,
1007
+ data.detail || data.description || '',
1008
+ data.status || 'open',
1009
+ data.priority || 'medium',
1010
+ data.agent_role || '',
1011
+ now,
1012
+ now,
1013
+ `cloud-pull:${data.id}`
1014
+ );
1015
+
1016
+ this._setSyncState(`cloud_task:${data.id}`, taskId);
1017
+ } catch (err) {
1018
+ if (!err.message?.includes('UNIQUE')) {
1019
+ throw err;
1020
+ }
1021
+ }
1022
+ }
1023
+
818
1024
  // =========================================================================
819
1025
  // Internal — HTTP helpers
820
1026
  // =========================================================================
@@ -909,6 +1115,20 @@ export class CloudSync {
909
1115
  } catch {
910
1116
  // Table likely already exists
911
1117
  }
1118
+
1119
+ // Migrate existing tables: add synced_to_cloud column if missing.
1120
+ // DEFAULT 0 means all existing records are marked as unsynced → they'll be
1121
+ // pushed on the next sync cycle, ensuring old data reaches the cloud.
1122
+ for (const table of ['knowledge_cards', 'tasks']) {
1123
+ try {
1124
+ this.indexer.db
1125
+ .prepare(`ALTER TABLE ${table} ADD COLUMN synced_to_cloud INTEGER DEFAULT 0`)
1126
+ .run();
1127
+ console.log(`${LOG_PREFIX} Migrated ${table}: added synced_to_cloud column`);
1128
+ } catch {
1129
+ // Column already exists — expected for fresh installs
1130
+ }
1131
+ }
912
1132
  }
913
1133
 
914
1134
  /**
@@ -44,7 +44,8 @@ CREATE TABLE IF NOT EXISTS knowledge_cards (
44
44
  status TEXT DEFAULT 'active',
45
45
  tags TEXT,
46
46
  created_at TEXT NOT NULL,
47
- filepath TEXT NOT NULL UNIQUE
47
+ filepath TEXT NOT NULL UNIQUE,
48
+ synced_to_cloud INTEGER DEFAULT 0
48
49
  );
49
50
 
50
51
  CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
@@ -61,7 +62,8 @@ CREATE TABLE IF NOT EXISTS tasks (
61
62
  agent_role TEXT,
62
63
  created_at TEXT NOT NULL,
63
64
  updated_at TEXT NOT NULL,
64
- filepath TEXT NOT NULL UNIQUE
65
+ filepath TEXT NOT NULL UNIQUE,
66
+ synced_to_cloud INTEGER DEFAULT 0
65
67
  );
66
68
 
67
69
  CREATE TABLE IF NOT EXISTS sessions (