diff --git a/docs/adr/002-authgate-stays.md b/docs/adr/002-authgate-stays.md new file mode 100644 index 00000000..669878d9 --- /dev/null +++ b/docs/adr/002-authgate-stays.md @@ -0,0 +1,22 @@ +# ADR 002: AuthGate stays + +**Status:** Accepted +**Date:** 2026-08-22 +**Deciders:** Tomas Maritano + +## Context + +Dripnex is local-first SQLite. An earlier write-up treated first-run login as a bug ("Inkdrop is offline, so skip the account"). + +## Decision + +Keep AuthGate. Matching Inkdrop official docs: + +- [Get started](https://docs.inkdrop.app/start-guide): create account, then "You'll see a login screen." +- [Privacy 5.4](https://docs.inkdrop.app/privacy): account required to use the client apps. + +Offline and Don't Sync are **after** login. See #545. + +## Consequences + +First-run is account + login. Do not ship a guest editor as v1. Mobile, when it exists, uses the same account. diff --git a/docs/adr/003-sqlite-not-couchdb.md b/docs/adr/003-sqlite-not-couchdb.md new file mode 100644 index 00000000..39bd1766 --- /dev/null +++ b/docs/adr/003-sqlite-not-couchdb.md @@ -0,0 +1,17 @@ +# ADR 003: SQLite, not CouchDB + +**Status:** Accepted +**Date:** 2026-08-22 +**Deciders:** Tomas Maritano + +## Context + +Early Inkdrop posts (2017-18) used CouchDB/PouchDB+Cloudant. Planning CouchDB self-host as "Inkdrop parity" was wrong. + +## Decision + +Local store is SQLite (Inkdrop v6 replaced LevelDB with SQLite). Dripnex already uses better-sqlite3. Sync docs that mention Advanced CouchDB are leftover. Do not plan CouchDB self-host. + +## Consequences + +Mobile and MCP read/write the same note model (SQLite on device, optional E2E sync). No second database product. diff --git a/docs/adr/004-mcp-over-local-http.md b/docs/adr/004-mcp-over-local-http.md new file mode 100644 index 00000000..c95cdd9e --- /dev/null +++ b/docs/adr/004-mcp-over-local-http.md @@ -0,0 +1,25 @@ +# ADR 004: MCP over Local HTTP + +**Status:** Accepted +**Date:** 2026-08-22 +**Deciders:** Tomas Maritano + +## Context + +`packages/mcp-server` opened the SQLite file via `DRIPNEX_DB_PATH`. The desktop already has a loopback API (Settings > Integrations, default `127.0.0.1:29168`, Bearer). Settings snippets still copied the SQLite path. + +## Decision + +One MCP for every agent (Grok Bot, Claude, Cursor, Codex). Prefer Local HTTP: + +- `DRIPNEX_LOCAL_SERVER_URL` + `DRIPNEX_LOCAL_TOKEN` +- URL must be loopback +- SQLite path is fallback when HTTP env is unset +- Writes stay a separate toggle (`mcp.json` / `DRIPNEX_MCP_WRITES`). Enabling MCP/HTTP is not a write grant. +- Never commit the token. Rotate if it was pasted in chat. + +Settings Claude/Codex snippets copy URL+token when MCP/HTTP is on. See PR 554. + +## Consequences + +Agents talk to the running app, not a second writer on the DB file. Clipper and Raycast can use the same HTTP. Grok Bot cannot attach that loopback as a hosted connector (127.0.0.1 is the user's machine). diff --git a/docs/adr/005-mobile-own-repo.md b/docs/adr/005-mobile-own-repo.md new file mode 100644 index 00000000..ea69bd08 --- /dev/null +++ b/docs/adr/005-mobile-own-repo.md @@ -0,0 +1,22 @@ +# ADR 005: Mobile is its own repo + +**Status:** Accepted +**Date:** 2026-08-22 +**Deciders:** Tomas Maritano + +## Context + +Inkdrop has iOS/Android. Dripnex desktop is the product until write path + sync are proven. Putting UIKit/Swift inside `dripnex/app` (Electron monorepo) couples two release trains. + +## Decision + +- Do not put mobile in `dripnex/app`. +- Repo `dripnex/ios` (iPhone + iPad). Android later, not the first client. +- Same AuthGate + account as desktop. Same note model. Sync via existing `api.dripnex.app` after login. +- Two-profile desktop sync is a P3 risk, not a P1 blocker. Issue #551. Full plan: [`docs/mobile/PLAN.md`](../mobile/PLAN.md). Contract: [`docs/mobile/CONTRACT.md`](../mobile/CONTRACT.md). + +## Consequences + +Desktop CI/releases stay Electron-only. iOS has its own repo, signing, and TestFlight. Shared pieces are HTTP contracts and markdown rules, not the Electron shell. + +`dripnex/ios` created 2026-08-22 (private). P1 skeleton starts there. iOS copies `NoteSnapshot` + `sync-core` wire fields; see [`docs/mobile/CONTRACT.md`](../mobile/CONTRACT.md). diff --git a/docs/ai/README.md b/docs/ai/README.md new file mode 100644 index 00000000..9333dd31 --- /dev/null +++ b/docs/ai/README.md @@ -0,0 +1,10 @@ +# Technical decisions + +Accepted product calls live as ADRs. Dated log: [updates](./updates.md). Mobile plan: [`docs/mobile/PLAN.md`](../mobile/PLAN.md). + +| ADR | Decision | +| ---------------------------------------- | -------------------------------------- | +| [002](../adr/002-authgate-stays.md) | AuthGate stays (match Inkdrop) | +| [003](../adr/003-sqlite-not-couchdb.md) | Local store is SQLite | +| [004](../adr/004-mcp-over-local-http.md) | MCP prefers Local HTTP | +| [005](../adr/005-mobile-own-repo.md) | Mobile is dripnex/ios, not dripnex/app | diff --git a/docs/ai/updates.md b/docs/ai/updates.md new file mode 100644 index 00000000..f764b24d --- /dev/null +++ b/docs/ai/updates.md @@ -0,0 +1,13 @@ +# Updates + +Newest first. Not release notes (`docs/releases/`). + +## 2026-08-22 + +- Mobile **started**. Tomás said go. `dripnex/ios` created (private). Reuse is `NoteSnapshot` + `sync-core` wire. Contract: `docs/mobile/CONTRACT.md`. Two-profile desktop sync stays a P3 risk, not a P1 blocker. +- AuthGate stays. Inkdrop start guide + privacy 5.4. ADR 002. +- SQLite, not CouchDB. ADR 003. +- MCP over Local HTTP (Bearer, 127.0.0.1:29168). Settings snippets must copy URL+token. ADR 004. Code: PR 554. +- Technical decisions go in `docs/adr` + this folder. Mobile plan: `docs/mobile/PLAN.md`. ADR 005. Issue #551. +- #541 e2e CM merged. #552 Dependabot HTTPS rewrite merged. No release for CI-only. +- GitHub Project: https://github.com/orgs/dripnex/projects/3 (not Linear). diff --git a/docs/mobile/CONTRACT.md b/docs/mobile/CONTRACT.md new file mode 100644 index 00000000..57f0a4da --- /dev/null +++ b/docs/mobile/CONTRACT.md @@ -0,0 +1,102 @@ +# Mobile contract + +Canonical types live in `dripnex/app`. iOS copies these fields. Do not invent new ones. + +| Source | What iOS copies | +| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| [`packages/core/src/contracts/NoteSnapshot.ts`](../../packages/core/src/contracts/NoteSnapshot.ts) | Note shape that leaves core (IPC / UI / storage) | +| [`packages/core/src/domain/types.ts`](../../packages/core/src/domain/types.ts) | `NoteStatus`, `INBOX_NOTEBOOK_ID`, `TEMPLATES_NOTEBOOK_ID` | +| [`packages/sync-core/src/types.ts`](../../packages/sync-core/src/types.ts) | Zod wire schemas for push / pull | + +Plan: [PLAN.md](./PLAN.md). Decision: [ADR 005](../adr/005-mobile-own-repo.md). + +## NoteSnapshot + +From `packages/core/src/contracts/NoteSnapshot.ts`. Plain serializable fields (no branded ids). + +| Field | Type | +| ------------------ | ---------------- | +| `id` | `string` | +| `notebookId` | `string` | +| `content` | `string` | +| `title` | `string` | +| `createdAt` | `string` | +| `updatedAt` | `string` | +| `tags` | `string[]` | +| `wordCount` | `number` | +| `taskCount` | `number` | +| `checkedTaskCount` | `number` | +| `archivedAt` | `string \| null` | +| `isArchived` | `boolean` | +| `isPinned` | `boolean` | +| `isDeleted` | `boolean` | +| `status` | `NoteStatus` | + +`NoteSummary` in the same file is the list-row subset (no `content`; adds `excerpt`). Same identity and status fields. Do not add a second note model. + +## Domain constants + +From `packages/core/src/domain/types.ts`: + +| Name | Value | +| ----------------------- | --------------------------------------------------- | +| `NoteStatus` | `'active' \| 'on_hold' \| 'completed' \| 'dropped'` | +| `NOTE_STATUSES` | the four values above | +| `DEFAULT_NOTE_STATUS` | `'active'` | +| `INBOX_NOTEBOOK_ID` | `'inbox'` | +| `TEMPLATES_NOTEBOOK_ID` | `'templates'` | +| `MAX_NOTEBOOK_DEPTH` | `2` (levels 0, 1, 2) | + +Inbox is the default notebook. Templates is reserved. Do not invent other reserved ids. + +## Sync wire (Zod) + +From `packages/sync-core/src/types.ts`. These schemas are the live wire. iOS must send and accept the same keys. + +`NoteOperationSchema`: `'create' | 'update' | 'delete'`. + +**LocalNotePushSchema** (plaintext content, renderer → main): + +| Field | Rule | +| -------------- | ------------------------------------ | +| `noteId` | `string`, min 1, max 128 | +| `operation` | `NoteOperationSchema` | +| `content` | `string`, max 10 MiB, optional | +| `localVersion` | `number`, int, nonnegative, optional | + +**EncryptedNotePushSchema** (content already encrypted, main → API): + +| Field | Rule | +| --------------- | -------------------------- | +| `noteId` | `string`, min 1, max 128 | +| `operation` | `NoteOperationSchema` | +| `encryptedData` | `string \| null`, optional | +| `localVersion` | `number`, int, optional | + +**EncryptedNotePushRequestSchema**: `changes` (1–100 `EncryptedNotePushSchema`), `deviceId` (UUID string). + +**RemoteNoteChangeSchema** (API → client pull item): + +| Field | Type | +| --------------- | --------------------- | +| `id` | `string` | +| `noteId` | `string` | +| `version` | `number` (int) | +| `operation` | `NoteOperationSchema` | +| `encryptedData` | `string \| null` | +| `deviceId` | `string` | +| `createdAt` | `string` | + +**NotePushResultSchema**: `noteId`, `version` (int), `status` (`'applied' | 'conflict'`), optional `serverVersion` (int). + +**NotePushResponseSchema**: `results` (`NotePushResultSchema[]`), `cursor` (int). + +**NotePullResponseSchema**: `changes` (`RemoteNoteChangeSchema[]`), `cursor` (int), `hasMore` (boolean). + +TypeScript names (`LocalNotePush`, `EncryptedNotePush`, …) are `z.infer` of those schemas. Copy the fields, not new ones. + +## Rules + +- Markdown is the note body (`content`). Do not persist a second AST or rich-text format. +- Same account / AuthGate as desktop. Sync is `api.dripnex.app` after login. +- If a field is not in these files, it is not in the contract. diff --git a/docs/mobile/PLAN.md b/docs/mobile/PLAN.md new file mode 100644 index 00000000..0d1bef5c --- /dev/null +++ b/docs/mobile/PLAN.md @@ -0,0 +1,99 @@ +# Mobile plan + +Status: **started**. Tomás said go on 2026-08-22. P1 is [`dripnex/ios`](https://github.com/dripnex/ios) (private, created the same day). No iOS code in `dripnex/app`. +Issue: [#551](https://github.com/dripnex/app/issues/551). Decision: [ADR 005](../adr/005-mobile-own-repo.md). Contract: [CONTRACT.md](./CONTRACT.md). + +Inkdrop ships iOS/Android after the same account login. Copy that product shape, not a guest-only phone editor. + +## Why not a daily driver yet + +Desktop still has to prove the write path and sync (two desktop profiles / two machines). That is a **P3 risk**, not a P1 blocker: if people write on the phone before merge is proven, mobile is a second offline silo. Clipper stays Later, not a side quest. + +```mermaid +flowchart TD + repo[dripnex/ios P1 skeleton] + auth[AuthGate + magic link] + read[Read notes + notebooks] + write[Editor write path] + sync[E2E sync with desktop] + ipad[iPad layout] + android[Android later] + repo --> auth --> read --> write --> sync --> ipad --> android +``` + +## Gate + +Satisfied for P1 (2026-08-22): + +1. AuthGate stays on desktop (ADR 002). +2. Local HTTP + MCP are the agent path on desktop (ADR 004). Phone does not need MCP v1. +3. Tomás said go. Ask only for release / signing / TestFlight. + +**P3 risk (not a skeleton blocker):** two desktop profiles can edit the same account and merge without data loss (#549). Keep it on the P3 dogfood phase. Do not wait on it to start P1 in `dripnex/ios`. + +## Repo and stack + +| | Choice | Why | +| -------- | --------------------------------------------------------------------------------------- | ---------------------------------------------- | +| Repo | `dripnex/ios` (private, created 2026-08-22) | Never inside `dripnex/app` | +| First OS | iPhone, then iPad | Inkdrop-shaped; Android after dogfood | +| UI | SwiftUI | Native, one store, no Electron in a phone | +| Notes DB | SQLite on device | ADR 003. Same fields as desktop | +| Editor | GFM in a WKWebView CodeMirror 6 shell, or a native markdown TextView if CM is too heavy | Editor is the product. Do not invent a WYSIWYG | +| Auth | Same account / magic link as desktop | AuthGate on first launch | +| Sync | `api.dripnex.app` after login, Don't Sync valid | Same as desktop | +| Plugins | Not v1 | Desktop plugin path (#547) first | + +Do not use Capacitor wrapping the desktop app. Do not put React Native inside `dripnex/app`. iOS copies [CONTRACT.md](./CONTRACT.md); it does not invent note or sync fields. + +## Product shape (v1 phone) + +- Login, then three panes collapsed to list + editor (Inkdrop mobile). +- Inbox default. Statuses: hide completed/dropped in the main list. +- GFM only. One note per issue when that workflow exists on desktop. +- Offline after login. Local SQLite is source of truth. +- Share sheet / clipper: **not** phone v1 (same Later bucket as #551). + +```mermaid +flowchart LR + subgraph phone [dripnex/ios] + login[AuthGate] + db[(SQLite)] + ed[GFM editor] + login --> db --> ed + end + api[api.dripnex.app] + desk[dripnex/app desktop] + db <--> api + desk <--> api +``` + +## Phases + +**P0 — desktop (still in dripnex/app)** +Prove sync merge + attachments (#549). That work stays here. It does not block the P1 skeleton. + +**P1 — skeleton (`dripnex/ios`, started)** +Xcode project, AuthGate, empty Inbox, local SQLite. Read-only notes when sync exists; do not block the skeleton on two-profile desktop sync. No iOS sources in this repo. TestFlight only with Tomás. + +**P2 — write path** +Create / edit / trash. Titles = first non-empty line. Templates if desktop templates already sync. + +**P3 — sync dogfood** +Same account: type on phone, see it on desktop, and the reverse. Conflict UI: keep this / keep other / open both (desktop already has this). Two-profile desktop merge (#549) is the quality risk in this phase. + +**P4 — iPad** +Three-pane optional. Not a new app. + +**P5 — Android / clipper** +Only after iOS is a daily driver. Clipper can be a share extension on iOS first, still Later. + +## Out of scope + +- Marketplace, graph, AI-notetaker, hosted note MCP on the phone +- Building inside `dripnex/app` +- Treating two-profile desktop sync as a P1 gate + +## Done when (later) + +Tomas can jot a note on iPhone after login, open it on desktop, and the markdown is the same. Until then P1 is the skeleton in `dripnex/ios`, not a daily-driver ticket. diff --git a/docs/mobile/README.md b/docs/mobile/README.md new file mode 100644 index 00000000..a47bb594 --- /dev/null +++ b/docs/mobile/README.md @@ -0,0 +1,3 @@ +# Mobile + +Plan: [PLAN.md](./PLAN.md). Contract: [CONTRACT.md](./CONTRACT.md). Decision: [ADR 005](../adr/005-mobile-own-repo.md). Issue: [#551](https://github.com/dripnex/app/issues/551).