evolutiondb-notes-sync 0.1.0__tar.gz → 0.1.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evolutiondb-notes-sync
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Sync macOS Apple Notes into EvolutionDB long-term memory.
5
5
  Author-email: alptekin topal <topal.alptekin@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evolutiondb-notes-sync
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Sync macOS Apple Notes into EvolutionDB long-term memory.
5
5
  Author-email: alptekin topal <topal.alptekin@gmail.com>
6
6
  License: MIT
@@ -110,7 +110,23 @@ class MemoryStore:
110
110
  f"('{_e(self.namespace)}','{_e(WATERMARK_KEY)}','{_e(value)}')"))
111
111
 
112
112
  def put_record(self, key: str, record: Dict) -> None:
113
+ # MEMORY PUT acts as upsert for small payloads but reports a
114
+ # spurious "duplicate key" error for larger ones; clearing the
115
+ # existing row first sidesteps that path. The DELETE is cheap
116
+ # and harmless when no row exists yet.
113
117
  value = json.dumps(record, ensure_ascii=False)
114
- self._retry(lambda cur: cur.execute(
115
- f"MEMORY PUT INTO {self.store} VALUES "
116
- f"('{_e(self.namespace)}','{_e(key)}','{_e(value)}')"))
118
+ def run(cur):
119
+ cur.execute(
120
+ f"DELETE FROM __mem_{self.store} "
121
+ f"WHERE mem_namespace = '{_e(self.namespace)}' "
122
+ f"AND mem_key = '{_e(key)}'")
123
+ cur.execute(
124
+ f"MEMORY PUT INTO {self.store} VALUES "
125
+ f"('{_e(self.namespace)}','{_e(key)}','{_e(value)}')")
126
+ try:
127
+ self._retry(run)
128
+ except Exception as exc:
129
+ print(f"[notes-sync] put_record FAILED key={key!r} "
130
+ f"value_len={len(value)} err={exc}",
131
+ file=sys.stderr, flush=True)
132
+ raise
@@ -95,11 +95,18 @@ def _html_to_text(body_html: str) -> str:
95
95
 
96
96
 
97
97
  def iter_notes() -> Iterable[Dict]:
98
- """Spawn osascript and stream JSON notes line by line."""
98
+ """Spawn osascript and stream JSON notes line by line.
99
+
100
+ JXA's `console.log` writes to stderr, not stdout, so we merge the
101
+ two streams. Any non-JSON line (genuine osascript errors) falls
102
+ through the JSONDecodeError branch and is ignored. Without the
103
+ merge, the stderr pipe fills (~64KB) on the first oversized note
104
+ and osascript blocks forever.
105
+ """
99
106
  try:
100
107
  proc = subprocess.Popen(
101
108
  ["/usr/bin/osascript", "-l", "JavaScript", "-e", _JXA],
102
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
109
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
103
110
  text=True, bufsize=1)
104
111
  except FileNotFoundError:
105
112
  print("[notes-sync] /usr/bin/osascript not found — Notes "
@@ -126,12 +133,7 @@ def iter_notes() -> Iterable[Dict]:
126
133
  proc.stdout.close()
127
134
  except Exception:
128
135
  pass
129
- rc = proc.wait(timeout=600)
130
- if rc != 0 and proc.stderr is not None:
131
- err = proc.stderr.read().strip()
132
- if err:
133
- print(f"[notes-sync] osascript stderr: {err[:400]}",
134
- file=sys.stderr, flush=True)
136
+ proc.wait(timeout=600)
135
137
 
136
138
 
137
139
  # ---------------------------------------------------------------- #
@@ -161,12 +163,27 @@ class Config:
161
163
  "mcp_mem")
162
164
 
163
165
 
166
+ # EvolutionDB caps a single SQL statement at 8KB. The MEMORY PUT
167
+ # wrapper, JSON envelope (title/folder/timestamps/tags), and SQL
168
+ # single-quote doubling can roughly double the on-wire byte count
169
+ # against the raw text. We leave generous headroom so a note full
170
+ # of quotes or multi-byte UTF-8 still fits in one statement.
171
+ _MAX_TEXT_CHARS = 3000
172
+
173
+
164
174
  def _build_record(n: Dict) -> Optional[Dict]:
165
175
  title = (n.get("name") or "").strip() or "(untitled)"
166
176
  text = _html_to_text(n.get("body") or "")
167
177
  if not text and title == "(untitled)":
168
178
  return None
179
+ truncated = False
180
+ if len(text) > _MAX_TEXT_CHARS:
181
+ text = text[:_MAX_TEXT_CHARS]
182
+ truncated = True
169
183
  modified = n.get("modified") or n.get("created") or ""
184
+ tags = ["notes", "apple-notes"]
185
+ if truncated:
186
+ tags.append("truncated")
170
187
  return {
171
188
  "fact": f"Note \"{title}\": {text[:240]}",
172
189
  "source": "notes",
@@ -177,7 +194,8 @@ def _build_record(n: Dict) -> Optional[Dict]:
177
194
  "note_id": n.get("id") or "",
178
195
  "modified_at": modified,
179
196
  "created_at": n.get("created") or "",
180
- "tags": ["notes", "apple-notes"],
197
+ "truncated": truncated,
198
+ "tags": tags,
181
199
  }
182
200
 
183
201
 
@@ -202,23 +220,27 @@ def sync_once(cfg: Config, *, since_iso: Optional[str],
202
220
  floor = wm or since_iso
203
221
  highest = floor or ""
204
222
 
205
- try:
206
- for raw in iter_notes():
207
- rec = _build_record(raw)
208
- if not rec:
209
- counters["skipped"] += 1
210
- continue
211
- if floor and rec["modified_at"] <= floor:
212
- continue
213
- if store:
223
+ for raw in iter_notes():
224
+ rec = _build_record(raw)
225
+ if not rec:
226
+ counters["skipped"] += 1
227
+ continue
228
+ if floor and rec["modified_at"] <= floor:
229
+ continue
230
+ if store:
231
+ try:
214
232
  store.put_record(_note_key(rec), rec)
215
- counters["notes"] += 1
216
- if rec["modified_at"] > highest:
217
- highest = rec["modified_at"]
218
- except Exception as exc: # noqa: BLE001
219
- print(f"[notes-sync] read failed: {exc}",
220
- file=sys.stderr, flush=True)
221
- counters["errors"] += 1
233
+ except Exception as exc: # noqa: BLE001
234
+ # One bad row should not abort the whole run; the next
235
+ # invocation will retry. Track and move on.
236
+ print(f"[notes-sync] note write failed "
237
+ f"(title={rec['title'][:60]!r}): {exc}",
238
+ file=sys.stderr, flush=True)
239
+ counters["errors"] += 1
240
+ continue
241
+ counters["notes"] += 1
242
+ if rec["modified_at"] > highest:
243
+ highest = rec["modified_at"]
222
244
 
223
245
  if store and highest and highest != (wm or ""):
224
246
  store.set_watermark_iso(highest)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "evolutiondb-notes-sync"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Sync macOS Apple Notes into EvolutionDB long-term memory."
9
9
  requires-python = ">=3.9"
10
10
  license = {text = "MIT"}