@rubytech/create-maxy-code 0.1.138 → 0.1.142

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.
Files changed (108) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -1
  3. package/payload/platform/lib/obsidian-parser/dist/index.d.ts +98 -0
  4. package/payload/platform/lib/obsidian-parser/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/obsidian-parser/dist/index.js +480 -0
  6. package/payload/platform/lib/obsidian-parser/dist/index.js.map +1 -0
  7. package/payload/platform/lib/obsidian-parser/src/index.ts +572 -0
  8. package/payload/platform/lib/obsidian-parser/tsconfig.json +8 -0
  9. package/payload/platform/neo4j/schema.cypher +48 -67
  10. package/payload/platform/package.json +2 -2
  11. package/payload/platform/plugins/.claude-plugin/marketplace.json +15 -0
  12. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.d.ts.map +1 -1
  13. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js +14 -8
  14. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js.map +1 -1
  15. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts +1 -1
  16. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts.map +1 -1
  17. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js +10 -7
  18. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js.map +1 -1
  19. package/payload/platform/plugins/docs/references/internals.md +1 -1
  20. package/payload/platform/plugins/docs/references/neo4j.md +3 -3
  21. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -0
  22. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  23. package/payload/platform/plugins/email/PLUGIN.md +3 -7
  24. package/payload/platform/plugins/email/mcp/dist/index.js +7 -46
  25. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  26. package/payload/platform/plugins/email/mcp/dist/lib/credentials.d.ts.map +1 -1
  27. package/payload/platform/plugins/email/mcp/dist/lib/credentials.js +4 -15
  28. package/payload/platform/plugins/email/mcp/dist/lib/credentials.js.map +1 -1
  29. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +51 -43
  30. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
  31. package/payload/platform/plugins/email/mcp/dist/lib/graph.js +223 -229
  32. package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
  33. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js +7 -4
  34. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js.map +1 -1
  35. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +14 -4
  36. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
  37. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +69 -84
  38. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
  39. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  40. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +49 -64
  41. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  42. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
  43. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +1 -2
  44. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
  45. package/payload/platform/plugins/memory/PLUGIN.md +3 -0
  46. package/payload/platform/plugins/memory/mcp/dist/index.js +89 -2
  47. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  48. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
  49. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
  50. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
  51. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
  52. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
  54. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +1 -0
  55. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
  56. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts +3 -0
  57. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.d.ts.map +1 -0
  58. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js +101 -0
  59. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js.map +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
  61. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +7 -4
  62. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
  63. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +122 -1
  64. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  65. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +580 -0
  66. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  67. package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.d.ts +127 -0
  68. package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.d.ts.map +1 -0
  69. package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.js +477 -0
  70. package/payload/platform/plugins/memory/mcp/dist/tools/obsidian-vault-import.js.map +1 -0
  71. package/payload/platform/plugins/memory/references/schema-base.md +4 -4
  72. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +2 -1
  73. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +1 -0
  74. package/payload/platform/plugins/notion-import/.claude-plugin/plugin.json +8 -0
  75. package/payload/platform/plugins/notion-import/PLUGIN.md +27 -0
  76. package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +114 -0
  77. package/payload/platform/plugins/notion-import/skills/notion-import/references/attachments.md +55 -0
  78. package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +83 -0
  79. package/payload/platform/plugins/notion-import/skills/notion-import/references/page-tree.md +61 -0
  80. package/payload/platform/plugins/notion-import/skills/notion-import/references/workspace-export.md +41 -0
  81. package/payload/platform/plugins/obsidian-import/.claude-plugin/plugin.json +8 -0
  82. package/payload/platform/plugins/obsidian-import/PLUGIN.md +39 -0
  83. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +92 -0
  84. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/attachments.md +80 -0
  85. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/daily-notes.md +31 -0
  86. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/vault-structure.md +46 -0
  87. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/wikilinks.md +70 -0
  88. package/payload/platform/plugins/scheduling/PLUGIN.md +18 -0
  89. package/payload/platform/plugins/scheduling/mcp/dist/index.js +93 -0
  90. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  91. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts +80 -0
  92. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.d.ts.map +1 -0
  93. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js +395 -0
  94. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics-graph-ingest.js.map +1 -0
  95. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts +16 -0
  96. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.d.ts.map +1 -1
  97. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js +111 -2
  98. package/payload/platform/plugins/scheduling/mcp/dist/lib/ics.js.map +1 -1
  99. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts +20 -0
  100. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.d.ts.map +1 -0
  101. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js +63 -0
  102. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-archive-ics.js.map +1 -0
  103. package/payload/platform/plugins/x-import/.claude-plugin/plugin.json +8 -0
  104. package/payload/platform/plugins/x-import/PLUGIN.md +33 -0
  105. package/payload/platform/plugins/x-import/references/archive-shape.md +124 -0
  106. package/payload/platform/plugins/x-import/references/transcript-shape.md +121 -0
  107. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +123 -0
  108. package/payload/platform/templates/specialists/agents/database-operator.md +18 -2
@@ -40,7 +40,7 @@ When loading this reference, confirm which schema files were consulted by noting
40
40
  | WhatsApp Conversation (legacy) | `WhatsAppConversation` | extends `schema:Conversation` | — | `accountId`, `conversationId`, `archiveSourceFile`, `firstMessageAt`, `lastMessageAt`, `participantCount`, `messageCount`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
41
41
  | WhatsApp Message (legacy) | `WhatsAppMessage` | extends `schema:Message` | — | `accountId`, `conversationId`, `messageId`, `dateSent`, `body`, `senderName`, `sequenceIndex`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
42
42
  | ConversationArchive | `ConversationArchive` | platform-native (chunked conversation archive parent) | — | `accountId`, `conversationIdentity`, `archiveSourceFile`, `summary`, `keywords`, `lastIngestedMessageHash`, `lastIngestedMessageAt`, `lastIngestedArchiveSha256`, `scope`, `createdByAgent`, `createdBySession`, `createdAt` |
43
- | Email | `Email` | extends `schema:Message` | RFC 5322 | `accountId`, `messageId`, `dateSent`, `sender`, `recipient`, `subject`, `body`, `authorshipMode` |
43
+ | Email thread | `KnowledgeDocument {source:'email'}` | Per-thread document (Task 321 retired the `:Email` label) | RFC 5322 | `accountId`, `attachmentId`, `subject`, `body` (chronological transcript), `messageIds` (array), `messages` (JSON-serialized per-message envelopes), `firstReceivedAt`, `lastReceivedAt`, `direction`, `authorshipMode` |
44
44
  | SocialPost | `SocialPost` | `schema:SocialMediaPosting` | — | `accountId`, `platform` (linkedin/twitter/instagram/…), `body`, `datePublished`, `authorshipMode` |
45
45
  | VoiceProfile | `VoiceProfile` | platform-native (Task 356 — authorial-voice style card) | — | `accountId`, `adminUserId`, `styleCard`, `generatedAt`, `corpusSize`, `feedbackEntries` |
46
46
  | VoiceEdit | `VoiceEdit` | platform-native (Task 356 — operator edit on an agent draft) | — | `accountId`, `adminUserId`, `originalText`, `editedText`, `intent`, `occurredAt` |
@@ -62,9 +62,9 @@ When loading this reference, confirm which schema files were consulted by noting
62
62
 
63
63
  **Position** carries a single role held by a `UserProfile` at an `Organization` — its title, dates, and per-role properties (e.g. achievement bullets in `description`, `endDate` for a finished role). Each role is its own node so it can carry its own structure; multiple Positions per UserProfile (career history); one Organization may host many Positions across many UserProfiles. Written by the `document-ingest` skill when a CV section classifies as `Position`. The natural pattern is `(:UserProfile)-[:HAS_POSITION]->(:Position)-[:AT]->(:Organization)` — never set the employer name as a property on Position; always create the AT edge to the (MERGEd) Organization.
64
64
 
65
- **`authorshipMode` on content labels (Task 356).** Every `:KnowledgeDocument`, `:Email`, `:Message`, `:SocialPost`, and `:Section:Conversation` write carries an `authorshipMode` enum drawn from `human-only | human-led-agent-assisted | agent-led-human-reviewed | agent-only | unknown`. The value records who actually wrote the content body, not who triggered the write. `human-only` means every word of the body came from the operator; `human-led-agent-assisted` is operator-authored with light agent polish (the operator chose every claim and structure); `agent-led-human-reviewed` is agent-drafted then operator-edited before send; `agent-only` is pure agent output the operator did not touch. The voice-mirror distillation walks the corpus where `authorshipMode IN ['human-only', 'human-led-agent-assisted']` — both modes carry voice signal. `:Section:Conversation` chat-archive chunks (WhatsApp / Telegram / Signal exports ingested under `:ConversationArchive`-[:HAS_SECTION]->`) are first-class voice corpus and the highest-leverage signal source because chat captures the unguarded voice in a way email rarely does. Legacy nodes default to `unknown` until reclassified via the voice-mirror backfill flow. The per-label authorshipMode index makes the distillation walk filter cheap; the `:Section` index covers `:Section:Conversation` via the base label. Writers that cannot determine the mode (e.g. bulk archive imports) write `unknown` rather than guessing.
65
+ **`authorshipMode` on content labels (Task 356).** Every `:KnowledgeDocument` (including email-source threads), `:Message`, `:SocialPost`, and `:Section:Conversation` write carries an `authorshipMode` enum drawn from `human-only | human-led-agent-assisted | agent-led-human-reviewed | agent-only | unknown`. The value records who actually wrote the content body, not who triggered the write. `human-only` means every word of the body came from the operator; `human-led-agent-assisted` is operator-authored with light agent polish (the operator chose every claim and structure); `agent-led-human-reviewed` is agent-drafted then operator-edited before send; `agent-only` is pure agent output the operator did not touch. The voice-mirror distillation walks the corpus where `authorshipMode IN ['human-only', 'human-led-agent-assisted']` — both modes carry voice signal. `:Section:Conversation` chat-archive chunks (WhatsApp / Telegram / Signal exports ingested under `:ConversationArchive`-[:HAS_SECTION]->`) are first-class voice corpus and the highest-leverage signal source because chat captures the unguarded voice in a way email rarely does. Legacy nodes default to `unknown` until reclassified via the voice-mirror backfill flow. The per-label authorshipMode index makes the distillation walk filter cheap; the `:Section` index covers `:Section:Conversation` via the base label. Writers that cannot determine the mode (e.g. bulk archive imports) write `unknown` rather than guessing.
66
66
 
67
- **Voice profile pattern (Task 356).** `(:AdminUser)-[:HAS_VOICE_PROFILE]->(:VoiceProfile)`. The styleCard is YAML serialized to a string property containing sentence-length distribution, register, favoured/avoided constructions and words, taboos, opening/closing patterns, and exemplar sentences. `(:VoiceProfile)-[:LEARNED_FROM]->(:KnowledgeDocument|:Email|:Message|:SocialPost|:Section)` links the profile to every node in its source corpus so the operator can audit what drove the distillation. Each operator edit on an agent draft becomes `(:VoiceEdit)-[:FEEDBACK_FOR]->(:VoiceProfile)` with the original/edited bodies plus a Haiku-summarised `intent` so the next distillation knows what to adjust.
67
+ **Voice profile pattern (Task 356).** `(:AdminUser)-[:HAS_VOICE_PROFILE]->(:VoiceProfile)`. The styleCard is YAML serialized to a string property containing sentence-length distribution, register, favoured/avoided constructions and words, taboos, opening/closing patterns, and exemplar sentences. `(:VoiceProfile)-[:LEARNED_FROM]->(:KnowledgeDocument|:Message|:SocialPost|:Section)` links the profile to every node in its source corpus so the operator can audit what drove the distillation. Each operator edit on an agent draft becomes `(:VoiceEdit)-[:FEEDBACK_FOR]->(:VoiceProfile)` with the original/edited bodies plus a Haiku-summarised `intent` so the next distillation knows what to adjust.
68
68
 
69
69
  **Idea** is a forward-only write surface for original-thinking candidates extracted from admin-side conversations by the `signal-detector` specialist (dispatched only by the admin agent's Stop hook). `body` is the verbatim operator statement; `conversationId` denormalises the source conversation; `scope` is always `"admin"`. Never written by any other path. No dedup or clustering at write time — let the brain accumulate first.
70
70
 
@@ -189,7 +189,7 @@ Do not write placeholder values ("TBD", "unknown", empty strings) for properties
189
189
  (:KnowledgeDocument)-[:REFERENCES]->(:*)
190
190
  (:KnowledgeDocument)-[:HAS_SECTION]->(:Section)
191
191
  (:AdminUser)-[:HAS_VOICE_PROFILE]->(:VoiceProfile)
192
- (:VoiceProfile)-[:LEARNED_FROM]->(:KnowledgeDocument|:Email|:Message|:SocialPost|:Section)
192
+ (:VoiceProfile)-[:LEARNED_FROM]->(:KnowledgeDocument|:Message|:SocialPost|:Section)
193
193
  (:VoiceEdit)-[:FEEDBACK_FOR]->(:VoiceProfile)
194
194
  (:KnowledgeDocument)-[:HAS_AEO_AUDIT]->(:AEOAudit)
195
195
  (:Message)-[:PART_OF]->(:Conversation)
@@ -13,7 +13,7 @@ One skill, one bash entry, one writer. The pipeline is identical for every conve
13
13
 
14
14
  ## Confirmed sources (Phase 0)
15
15
 
16
- `whatsapp` ships today (relocated grammar from the retired `whatsapp-import` plugin). Other sources land as needed:
16
+ `whatsapp` ships today (relocated grammar from the retired `whatsapp-import` plugin). `x-dm` ships alongside the `x-import` plugin (Task 323) — driven by the x-import skill, one invocation per dmConversation slice. Other sources land as needed:
17
17
  - `telegram` — Telegram JSON export
18
18
  - `signal` — Signal text export
19
19
  - `linkedin-messages` — LinkedIn DM thread export (LinkedIn *connections* stay flat-dataset; only DMs route here)
@@ -21,6 +21,7 @@ One skill, one bash entry, one writer. The pipeline is identical for every conve
21
21
  - `meeting-minutes` — operator-supplied meeting notes
22
22
  - `imessage` — iMessage backup export
23
23
  - `slack` — Slack channel export
24
+ - `x-dm` — X (Twitter) Direct Messages, dispatched by the `x-import` skill from `data/direct-messages.js` / `data/direct-messages-group.js` — one bash invocation per dmConversation slice; senders carry numeric X senderIds resolved to `:Person` / `:AdminUser` upstream in the x-import skill
24
25
 
25
26
  Until a normaliser ships, the bash entry loud-fails with the implemented set listed in the error.
26
27
 
@@ -15,6 +15,7 @@ Ingests any unstructured input — documents (PDF, text, transcript, web page) a
15
15
  |---|---|---|---|---|
16
16
  | PDF / text / web (default) | `:KnowledgeDocument` | `:Section:<Kind>` from closed enumeration (`Position`, `Chapter`, `Parties`, …) | `attachmentId` | `document` |
17
17
  | Chat archive (`_chat.txt`) | `:ConversationArchive` | `:Section:Conversation` | `conversationIdentity` | `chat` |
18
+ | X (Twitter) tweet-stream transcript (rendered by `x-import`) | `:KnowledgeDocument` (`source='x'`) | `:Section:<Kind>` from closed enumeration | `attachmentId` | `document` |
18
19
 
19
20
  The classifier in `memory-classify` decides which section kinds each section maps to (document mode) or chunks the archive into topic-bounded `:Section:Conversation` nodes (chat mode). The skill orchestrates the pipeline; the classifier reads the loaded ontology; the writer enforces the validator. **Classifier failure is terminal — the ingest aborts entirely; nothing is written. Loud failures, never silent landfill.**
20
21
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "notion-import",
3
+ "description": "Import a Notion workspace export (markdown + CSV directory) into the Maxy Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: notion-import
3
+ description: "Import a Notion workspace export (markdown + CSV directory) into the Maxy Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default."
4
+ tools: []
5
+ always: false
6
+ embed: false
7
+ specialist: database-operator
8
+ metadata: {"platform":{"optional":true,"pluginKey":"notion-import"}}
9
+ ---
10
+
11
+ # Notion Import
12
+
13
+ Ingests a Notion workspace export (the native "Export all workspace content" markdown + CSV dump) into the Maxy Neo4j graph. Skill-only plugin — no MCP server, no admin tools added. The skill runs under `specialists:database-operator`, which owns graph-write delegation from admin.
14
+
15
+ ## When this applies
16
+
17
+ The admin agent delegates to `specialists:database-operator` when the operator drops a Notion export directory (or zip path) into chat and asks to import it. The specialist runs the skill's import-provenance confirmation flow before any page or row is read, then walks the export.
18
+
19
+ ## Intra-plugin growth
20
+
21
+ A Notion export contains pages (`.md`), databases (CSV pairs — schema CSV plus rows CSV), nested page subdirectories, and an `attachments/` tree. The skill ships four references covering the four shapes the export expresses — workspace traversal, database parsing, page-tree hierarchy, attachment copy. Per-database-type references land here as new files under `references/` as operators surface new shapes; they do not become new plugins.
22
+
23
+ ## Relationship to other plugins
24
+
25
+ - **memory** — the underlying Cypher-write surface used by the skill (`memory-write`, `memory-update`, `memory-search`). Every node carries `source='notion'` + `createdByAgent='notion-import'` for provenance.
26
+ - **database-operator specialist** — owns execution. See the delegation clause in [database-operator.md](../../templates/specialists/agents/database-operator.md).
27
+ - **linkedin-import** — sibling skill-only import plugin (different specialist owner — `librarian` predates the database-operator delegation doctrine). The two plugins share no code.
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: notion-import
3
+ description: Import a Notion workspace export (markdown + CSV) into a {{productName}} Neo4j graph. Triggers when the user asks to import a Notion workspace, ingest a Notion export, or mentions a `Export-*.zip` / unzipped Notion export directory. Pages become `:KnowledgeDocument` nodes, database rows become typed entities (or `:KnowledgeDocument` if no schema mapping fits), database schemas become `:KnowledgeDocument` siblings, hierarchy is preserved via `:HAS_SECTION`, attachments are copied locally and written as `:DigitalDocument`.
4
+ ---
5
+
6
+ # Notion Import
7
+
8
+ **Invoked from `specialists:database-operator`** when the operator drops a Notion workspace export directory (or `Export-*.zip` path) and asks to import it. The admin agent dispatches the database-operator; the database-operator loads this skill.
9
+
10
+ **Anchor model.** A Notion export does not have a single subject the way a LinkedIn archive does — pages have multiple authors, databases are shared workspaces, mentions span the whole org. The skill anchors **import provenance** on the importing `:AdminUser` via `IMPORTED_BY` (so every node in the import can be traced back to who pushed the button); page authorship is captured separately via `AUTHORED` when the export records an author. The `:AdminUser` is identity, not subject — pages are not "owned" by the importer.
11
+
12
+ Every node this skill writes carries an `importId` property (UUID per skill run) so a future dream-cycle pass can detect orphans or selectively re-ingest.
13
+
14
+ ## Import-provenance confirmation (mandatory first step)
15
+
16
+ A Notion export is dropped by one operator on behalf of one workspace. Before any file is read, the skill confirms two things:
17
+
18
+ 1. **Importing AdminUser.** The `:AdminUser` who pushed the button. Default is the live operator (the session's userId); confirm by echoing `userId=<id> name=<name>` and accepting yes/no. If the operator is importing on behalf of someone else — a partner, a client whose workspace they're staging — accept either an existing AdminUser userId or a new `:Person` natural-key (`givenName` + `familyName` + at least one of `email` / `telephone`) as the importing identity. Persist as `$importerNodeId`.
19
+ 2. **Workspace label.** A short human label (`"Joel's personal workspace"`, `"Rubytech ops"`) used as the value of `:NotionWorkspace.name`. One `:NotionWorkspace` node is MERGEd per `(accountId, name)` natural key; re-imports of the same workspace match it.
20
+
21
+ The skill refuses to read the export directory until both values are echo-confirmed.
22
+
23
+ ## Anchor shape
24
+
25
+ ```
26
+ (:AdminUser {userId: $importerUserId})
27
+ -[:IMPORTED { importId, source: 'notion', startedAt }]->
28
+ (:NotionWorkspace { accountId, name })
29
+ -[:HAS_SECTION]-> (:KnowledgeDocument { kind: 'notion-page', notionId, importId })
30
+ -[:HAS_SECTION]-> (:KnowledgeDocument { kind: 'notion-database', notionId, importId })
31
+ ```
32
+
33
+ `:NotionWorkspace` is the only new label the skill needs; everything below it reuses {{productName}} canonical labels (`:KnowledgeDocument`, `:Person`, `:Organization`, `:DigitalDocument`). Page hierarchy from Notion's parent-child relation is written as `:HAS_SECTION` between `:KnowledgeDocument` pages.
34
+
35
+ ## Invariants
36
+
37
+ 1. **Schema first.** No new constraints or indexes are required; the canonical schema in [`platform/plugins/memory/references/schema-base.md`](../../../../plugins/memory/references/schema-base.md) covers every label this skill writes. If the operator's workspace surfaces a database shape no canonical label fits, the rows are written as `:KnowledgeDocument` with `kind: 'notion-row'` and the gap is reported to admin — the skill does not mint new labels at runtime.
38
+ 2. **Provenance confirmed first.** No file is read until `$importerNodeId` (AdminUser or external Person) and `$workspaceName` are persisted and echo-confirmed.
39
+ 3. **Closed edge vocabulary.** Edges written are drawn from `schema-base.md` only. Unmapped Notion relations (a custom DB-to-DB link the operator named `sponsored_by`, say) land as `:MENTIONS` from the source page's `:KnowledgeDocument`, with the Notion relation label stored as a property (`mentionContext: '<relation-name>'`). `RELATES_TO` is **not** a schema edge and is not used.
40
+ 4. **Account isolation on fuzzy resolves.** Every inline `@person` mention resolves against `:Person {accountId: $accountId}` — never label-only. A "John Smith" mention in the importing account never matches a `:Person` in another account, even if the names are identical. When no in-account match is found, the mention is written as a new `:Person` with `givenName` / `familyName` parsed from the mention text and `source='notion'`.
41
+ 5. **Idempotent MERGE on Notion IDs.** Every Notion page and row carries a stable Notion UUID. Pages MERGE on `(accountId, notionId)`; database rows MERGE on `(accountId, notionId)`; attachments MERGE on `(accountId, sha256)`. Re-import of the same export is a property-update no-op on existing nodes — `skipped` counter advances, no duplicates.
42
+ 6. **Provenance stamps.** Every new node: `createdByAgent='notion-import'`, `createdBySource='notion'`, `createdBySession=$sessionId` (UUID per skill run), `createdAt=datetime()`, `importId=$importId`, plus `source='notion'`.
43
+ 7. **Bulk writes route through `memory-archive-write`; per-node `memory-write` is only used for the workspace anchor.** The walk produces three buffered payloads (pages, database rows per label, deferred relations), each submitted as one `mcp__plugin_memory_memory__memory-archive-write` call with the matching `archiveType`. The server batches 500 rows per transaction, returns aggregate counters, and gives the import per-chunk atomicity. `mcp__plugin_memory_memory__memory-search` is still used for inline-mention and author resolution (per-mention lookup happens during the walk, before any write). No raw Cypher, no Bash improvisation. If a write shape the skill needs is missing from the archive-write surface, the gap is filed as a follow-up task — never improvised.
44
+
45
+ ## Execution model
46
+
47
+ 1. Run the provenance-confirmation flow. Persist `$importerNodeId`, `$workspaceName`, `$importId` (fresh UUID), `$sessionId`.
48
+ 2. MERGE the `:NotionWorkspace { accountId, name }` and the `(:AdminUser)-[:IMPORTED]->(:NotionWorkspace)` edge via `memory-write`. Persist the resulting `:NotionWorkspace` `elementId` as `$workspaceNodeId` — every subsequent archive-write call passes it as `ownerNodeId`.
49
+ 3. Walk the export directory per [workspace-export.md](references/workspace-export.md) — classify each entry as page (`.md`), database schema (`<db-name>.csv` paired with row CSVs), database rows, attachment subdirectory, or nested page subdirectory. The walk **buffers** parsed output into three lists: `pageBuffer`, `dbRowBufferByLabel` (keyed by canonical label + databaseNotionId), and `deferredRelations`. No writes are issued during the walk except inline-mention `memory-search` lookups.
50
+ 4. For each shape, follow the matching reference to fill the buffers:
51
+ - Pages → [page-tree.md](references/page-tree.md). Each parsed page becomes one `NotionPageRow` in `pageBuffer`. Top-level pages set `parentNotionId: null`; nested pages keep the enclosing page's `notionId`. Inline `@person` mentions resolved per Invariant 4 — each match appends a `MENTIONS` entry to `deferredRelations`. Page-to-page `HAS_SECTION` (every non-workspace parent) is appended to `deferredRelations` with `edgeType: 'HAS_SECTION'`, regardless of in-batch ordering — the server resolves them once every node is MERGEd in pass-2.
52
+ - Databases → [databases.md](references/databases.md). The database itself is MERGEd via `memory-write` as `:KnowledgeDocument {kind: 'notion-database', notionId}` before any rows are queued (the row handler verifies the parent exists). Each row joins `dbRowBufferByLabel[label][databaseNotionId]`. Notion's typed relation columns become entries in `deferredRelations` (canonical edge type when the schema maps, `MENTIONS` with `mentionContext` otherwise).
53
+ - Attachments → [attachments.md](references/attachments.md). Copied to `{accountDir}/archive/notion/<importId>/`, content-hashed, written as `:DigitalDocument {kind: 'notion-attachment', sha256}` via `memory-write` (one per attachment — attachments are not bulk-batched because the file copy itself is the long pole). Each attachment appends a `MENTIONS` or canonical `HAS_ATTACHMENT` entry to `deferredRelations` when wired into the typed-edge schema.
54
+ 5. **Flush phase — three batched archive-write calls.** After the walk completes:
55
+ 1. `memory-archive-write archiveType='notion-pages' ownerNodeId=$workspaceNodeId importId=$importId rows=pageBuffer` — server MERGEs every page and attaches workspace-root `HAS_SECTION` for `parentNotionId: null` rows. Optional `AUTHORED` edges write when `authorPersonNodeId` is set.
56
+ 2. For each `(label, databaseNotionId)` key: `memory-archive-write archiveType='notion-database-rows' ownerNodeId=$workspaceNodeId importId=$importId databaseNotionId=<…> label=<…> rows=<bufferedRows>`.
57
+ 3. `memory-archive-write archiveType='notion-relations-pass-2' ownerNodeId=$workspaceNodeId rows=deferredRelations` — server buckets relations by `edgeType` from the closed allowlist (`MENTIONS`, `WORKS_FOR`, `OWNS`, `ASSIGNED_TO`, `PART_OF`, `ATTENDED`, `HAS_SECTION`, `AUTHORED`). Unknown edge types and missing endpoints are counted, not thrown.
58
+ 6. Emit `[notion-import] importId=<id> pages=<n> rows=<n> relations=<n> mentions=<n> attachments=<n> skipped=<n> ms=<elapsed>` at completion, aggregating the counters returned by each archive-write call. Per-file errors emit `[notion-import:error] file=<path> reason=<…>` during the walk; the skill continues past parse failures so a single bad file does not abort the entire import. Per-batch transaction failures surface in the archive-write `errors[]` array — log each as `[notion-import:batch-error] archiveType=<…> offset=<…> reason=<…>` and continue with subsequent batches; the same `importId` re-run is idempotent and replays only the chunks that did not commit.
59
+
60
+ ## Selective-ingest gate (bulk imports)
61
+
62
+ A Notion workspace typically contains 200–5,000 pages. Writing all of them in one shot defeats compression-on-write — most pages will never be queried, and the noise compounds with every subsequent import.
63
+
64
+ **Threshold:** when the export walk discovers more than **200 pages or 500 database rows**, pause and ask the operator to filter before any write begins. Natural filter axes for Notion:
65
+
66
+ - **Top-level page subtree** — "only pages under `Engineering/`", "only `Personal/Journal/`".
67
+ - **Database name** — "only the `People` and `Projects` DBs", "skip the `Daily Standup` DB".
68
+ - **Last-edited date** — "only pages edited since 2024-01-01".
69
+
70
+ The operator picks one axis or a combination. The skill applies the filter to the walk's candidate set before issuing any write. Re-importing later with a wider filter hits the same `(accountId, notionId)` natural keys; existing nodes are property-updated, only the new-only delta is created.
71
+
72
+ ## File-shape roster
73
+
74
+ The Notion export is a directory of:
75
+
76
+ | Shape | Detection | Reference |
77
+ |-------|-----------|-----------|
78
+ | Page (markdown) | `*.md` file at any depth, with a Notion UUID suffix in the filename | [page-tree.md](references/page-tree.md) |
79
+ | Database schema | `<name>.csv` with a sibling directory of the same name containing per-row `.md` files | [databases.md](references/databases.md) |
80
+ | Database row | `<row-name>.md` inside a database subdirectory, with the parent CSV's column set as front-matter properties | [databases.md](references/databases.md) |
81
+ | Attachment | Any file under an `<image>/`, `<pdf>/`, or other media-bearing subdirectory adjacent to its referencing page | [attachments.md](references/attachments.md) |
82
+ | Nested workspace subtree | A directory whose name carries a Notion UUID and contains both `.md` pages and CSV databases | [workspace-export.md](references/workspace-export.md) |
83
+
84
+ ## Explicitly out of scope
85
+
86
+ - **Notion API live sync.** This is one-shot from a manual export only. Live API sync (incremental delta, webhook listening, two-way write-back) is a separate task if commissioned.
87
+ - **Formulas, rollups, synced blocks.** The export materialises computed values as text; the skill imports the materialised text but not the formula source. Re-computing rollups against the graph is downstream work.
88
+ - **Notion Web Clipper saved pages.** Clipper pages live in a different export shape (HTML, not markdown) and have different semantics (third-party source ingest). They land in a future task, not here.
89
+ - **Back-sync to Notion.** Writing changes from the graph back into the Notion workspace is not in scope.
90
+ - **Streaming progress events from the archive-write server.** Counters are returned only at completion of each archive-write call; per-row progress is a separate observability task if commissioned.
91
+
92
+ ## Schema-mapping table
93
+
94
+ The skill maps Notion shapes to canonical {{productName}} labels and edges. Schema additions are last resort — every row below uses an existing label from `schema-base.md`.
95
+
96
+ | Notion shape | Canonical label | Notes |
97
+ |--------------|-----------------|-------|
98
+ | Workspace | `:NotionWorkspace` | New label, single property `name` plus `accountId`. Only new label introduced by this skill. |
99
+ | Page | `:KnowledgeDocument { kind: 'notion-page' }` | Body markdown stored as `text`. Hierarchy via `:HAS_SECTION`. |
100
+ | Page authored-by | `(:Person)-[:AUTHORED]->(:KnowledgeDocument)` | When the export records an author, resolve to in-account `:Person`. |
101
+ | Database (the table itself) | `:KnowledgeDocument { kind: 'notion-database' }` | Schema preserved as `schema` (JSON-stringified property list). |
102
+ | Database row, canonical-fit | `:Person`, `:Organization`, `:Project`, `:Task`, `:Event` | Database name + columns drive label selection. A "People" DB with `email`/`telephone` columns → `:Person`. |
103
+ | Database row, no fit | `:KnowledgeDocument { kind: 'notion-row' }` | Properties preserved on the node; surface to admin in the completion log if any DB lands here entirely. |
104
+ | Notion relation, schema-mapped | Canonical edge from `schema-base.md` | A relation column named `works_at` between People and Companies → `(:Person)-[:WORKS_FOR]->(:Organization)`. |
105
+ | Notion relation, unmapped | `:MENTIONS { mentionContext: '<relation-name>' }` | Source page `:KnowledgeDocument` → target. Preserves the Notion relation name for later audit. |
106
+ | Inline `@person` mention | `(:KnowledgeDocument)-[:MENTIONS]->(:Person)` | Account-filtered fuzzy resolve per Invariant 4. |
107
+ | Attachment | `:DigitalDocument { kind: 'notion-attachment', sha256 }` | Files copied to `{accountDir}/archive/notion/<importId>/`. |
108
+ | Page hierarchy | `(:KnowledgeDocument)-[:HAS_SECTION]->(:KnowledgeDocument)` | Parent-of-child per Notion's nesting. |
109
+
110
+ If the operator's workspace introduces a database that fits none of the canonical labels and is not satisfied by the `:KnowledgeDocument` fallback, surface the gap to admin in the completion summary — never mint a new label at runtime.
111
+
112
+ ## Natural-edge discipline (why this matters)
113
+
114
+ Every edge written corresponds to a relationship the Notion export actually expresses. `:HAS_SECTION` is real — Notion explicitly records page parents. `:AUTHORED` is real when the export records it. `:WORKS_FOR` is real when a People DB has a Companies-relation column. Synthetic "attach-everything-to-the-importer" edges are forbidden — the importer is provenance (via `IMPORTED`), not the subject of every page. A workspace with thousands of unrelated pages does not get thousands of edges manufactured to "make it reachable"; the workspace-level `IMPORTED` edge already makes them reachable through the importer.
@@ -0,0 +1,55 @@
1
+ # attachments
2
+
3
+ Reference for copying Notion attachments — images, PDFs, and other media — into the account's local archive and recording them as graph nodes.
4
+
5
+ ## Shape on disk
6
+
7
+ Notion exports media files alongside the page that references them. The export's directory layout varies — small media may live in the same directory as the referencing `.md`, larger media in a sibling subdirectory named after the page. The skill does not assume a single layout; it follows attachment references **from the page markdown**, not by enumerating media files.
8
+
9
+ Referenced shapes:
10
+
11
+ - `![alt](Image%20Name%20<32-hex-notionId>.png)` — image embed.
12
+ - `[Doc](Doc%20Name%20<32-hex-notionId>.pdf)` — file link.
13
+ - HTML `<img src="...">` tags inside the markdown (older exports) — also captured.
14
+
15
+ The referenced filename is URL-decoded to get the relative path on disk.
16
+
17
+ ## Where they go
18
+
19
+ Attachments copy to `{accountDir}/archive/notion/<importId>/<sha256>.<ext>`:
20
+
21
+ - `{accountDir}` is the account's directory (the same directory that holds `account.json`), resolved per the platform's existing `accountDir` convention used by `platform/lib/admins-write` and elsewhere.
22
+ - `<importId>` is the UUID for this skill run — keeps a single import's attachments grouped on disk.
23
+ - `<sha256>` is the content hash of the file, computed at copy time. Filename collisions on the same hash skip the copy (already present from a prior import of the same file).
24
+ - `<ext>` is the original extension preserved from the source filename.
25
+
26
+ The path scheme is deliberately content-hash-keyed so multiple pages referencing the same file (a logo embedded across many pages, say) share one copy on disk and one `:DigitalDocument` node in the graph.
27
+
28
+ ## What each attachment becomes
29
+
30
+ One `:DigitalDocument { kind: 'notion-attachment' }` node per content hash:
31
+
32
+ - `sha256` — the content hash; the MERGE natural key together with `accountId`.
33
+ - `name` — original filename (without the Notion ID suffix where present).
34
+ - `mimeType` — inferred from extension.
35
+ - `bytes` — file size in bytes.
36
+ - `localPath` — absolute path under `{accountDir}/archive/notion/<importId>/`.
37
+ - `accountId`, `importId`, `source='notion'`, plus standard provenance per SKILL invariant 6.
38
+
39
+ Page-to-attachment edge: `(:KnowledgeDocument)-[:HAS_ATTACHMENT]->(:DigitalDocument)`. One edge per referencing page; if a single page references the same attachment three times, one edge is MERGEd (idempotent).
40
+
41
+ ## Idempotency
42
+
43
+ `:DigitalDocument` MERGEs on `(accountId, sha256)`. Re-import of the same file by content hash matches the existing node — `localPath` updates if the file was copied into a fresh `<importId>` directory, every other property stays. The `:HAS_ATTACHMENT` edge also MERGEs, so re-imports do not duplicate.
44
+
45
+ ## What the attachment step does NOT do
46
+
47
+ - **Does not** OCR scanned PDFs or extract text from images. That belongs to a separate ingest step (a future task can chain `memory-ingest` to derive text from `:DigitalDocument` nodes by mime type).
48
+ - **Does not** fetch from Notion's CDN. Notion exports are self-contained — every referenced file is in the zip. If a markdown reference points at an absolute Notion URL (not a relative export path), it is treated as an external link and a `:MENTIONS` edge to a `:KnowledgeDocument { kind: 'external-link', url }` is written instead, with no file copy.
49
+ - **Does not** delete attachments on re-import when the page no longer references the file. Orphan attachment detection is a graph-prune concern, not an importer concern.
50
+
51
+ ## Failure modes
52
+
53
+ - `[notion-import:error] reason=attachment-not-on-disk path=<rel-path>` — the page markdown references a file the walker cannot locate. Skip the copy, skip the edge, log.
54
+ - `[notion-import:error] reason=copy-failed path=<rel-path> errno=<…>` — filesystem error during copy. Log and continue; the page itself is still written, only the attachment is missing.
55
+ - `[notion-import:error] reason=hash-collision-different-content sha256=<hash>` — two files in the same import produced the same hash but the filenames disagree on extension. Should be impossible in practice; if observed, log and write both as separate `:DigitalDocument` nodes with disambiguated extensions.
@@ -0,0 +1,83 @@
1
+ # databases
2
+
3
+ Reference for ingesting Notion databases — both the database (schema) and its rows.
4
+
5
+ ## Shape on disk
6
+
7
+ A Notion database lands as a `.csv` file plus a sibling directory of the same basename. The CSV is the table-view export; the directory contains one `.md` per row, with the row's properties as YAML front-matter and the row body as markdown below.
8
+
9
+ ```
10
+ Projects 7a1b2c…/
11
+ Projects 7a1b2c….csv ← schema + row table
12
+ Projects 7a1b2c…/ ← row markdowns
13
+ Phoenix Migration 9f3a…md
14
+ Q3 Strategy d4e5…md
15
+ ```
16
+
17
+ The CSV columns are the database's properties. Column types are not encoded in the CSV directly — they are inferred from the values (date-shaped, URL-shaped, person-mention-shaped, relation-shaped). A relation column's cells contain the human title plus Notion ID of the linked row.
18
+
19
+ ## What the database itself becomes
20
+
21
+ Each Notion database is written as one `:KnowledgeDocument { kind: 'notion-database' }` node:
22
+
23
+ - `notionId` — the 32-hex ID from the CSV basename.
24
+ - `name` — human title from the CSV basename.
25
+ - `schema` — JSON-stringified list of `{ name, inferredType }` per column.
26
+ - `parentPageNotionId` — the page that contains the database, if any.
27
+
28
+ The database is attached to the workspace root via `(:NotionWorkspace)-[:HAS_SECTION]->(:KnowledgeDocument)` and, when nested under a page, also via `(:KnowledgeDocument {page-parent})-[:HAS_SECTION]->(:KnowledgeDocument {database})`.
29
+
30
+ ## Label selection for rows
31
+
32
+ Per-row label is decided by the database's name and column set together — not by either alone.
33
+
34
+ - **`:Person`** when the database name matches `^(people|contacts|crm|team|members?)$` (case-insensitive) AND the columns contain at least one of `email`, `phone`, `telephone`, `linkedin`. Properties map onto schema-base camelCase: `givenName` / `familyName` from a single `name` column split on first space (preserve `name` only if no canonical Person property fits); `email`, `telephone`, `jobTitle`, etc.
35
+ - **`:Organization`** when the name matches `^(companies|orgs|organizations?|accounts)$` AND the columns contain `website` or `domain`.
36
+ - **`:Project`** when the name matches `^projects?$` AND the columns contain at least one of `status`, `startDate`, `dueDate`, `owner`.
37
+ - **`:Task`** when the name matches `^(tasks?|todo|to-?dos?|action items?)$` AND the columns contain at least one of `done`, `status`, `assignee`, `dueDate`.
38
+ - **`:Event`** when the name matches `^(events?|meetings?|calendar)$` AND the columns contain `startDate` / `start` / `date`.
39
+ - **`:KnowledgeDocument { kind: 'notion-row' }`** — fallback. The CSV columns are preserved on the node as properties. Surface to admin in the completion log if any database falls entirely into this bucket — it usually means the workspace has a custom DB shape worth handling explicitly in a future iteration.
40
+
41
+ When admin's brief is ambiguous about which label a database should land in, the database-operator's `{ambiguity, proposed}` protocol applies — ask, don't guess.
42
+
43
+ ## Row write recipe
44
+
45
+ For each row in the parsed CSV:
46
+
47
+ 1. **Confirm the parent database exists.** The database itself (`:KnowledgeDocument { kind: 'notion-database', notionId }`) is MERGEd via `memory-write` once, before any of its rows are queued. The `notion-database-rows` handler refuses the batch if the parent is missing.
48
+ 2. **Select the label** per the rules above. Allowlist: `Person`, `Organization`, `Project`, `Task`, `Event`, `KnowledgeDocument`.
49
+ 3. **Map CSV columns to schema-base properties.** Use canonical camelCase. Unknown columns land as properties on the node with their original column name preserved.
50
+ 4. **Buffer the row.** Append `{ notionId, properties }` to `dbRowBufferByLabel[label][databaseNotionId]`. The row carries node properties only; the HAS_SECTION edge to the parent database is written server-side at flush time.
51
+ 5. **Bucket relation columns into `deferredRelations`.** A relation column cell is `"Linked Row Title <32-hex-notionId>"`. For each cell, append a relation entry with `sourceNotionId` = this row's `notionId`, `targetNotionId` = the linked row's `notionId`. When the relation name maps to a canonical edge (table below), set `edgeType` accordingly; otherwise set `edgeType: 'MENTIONS'` and `properties: { mentionContext: '<relation-column-name>' }`. Pass-2 resolves targets — links to rows outside the import set are counted as `skippedUnknownTarget`, not errors.
52
+
53
+ The actual writes happen at flush time: one `memory-archive-write` call per `(label, databaseNotionId)` key, then one final `memory-archive-write archiveType='notion-relations-pass-2'` call carrying every relation across all databases and pages.
54
+
55
+ ## Notion-relation → canonical-edge map
56
+
57
+ Examples — the table is extensible per workspace, but the principle is fixed: a relation maps to a canonical edge only when the schema vocabulary in `schema-base.md` (Relationship Patterns + Edges introduced by document-ingest, plus any vertical-schema edges loaded at runtime) names a matching one.
58
+
59
+ | Notion relation column name | Canonical edge |
60
+ |-----------------------------|----------------|
61
+ | `works_at`, `company`, `employer` | `(:Person)-[:WORKS_FOR]->(:Organization)` |
62
+ | `assignee`, `owner` | `(:Person)-[:OWNS]->(:Project)` / `(:Person)-[:ASSIGNED_TO]->(:Task)` per row label |
63
+ | `project`, `belongs_to_project` | `(:Task)-[:PART_OF]->(:Project)` |
64
+ | `attended`, `participants` | `(:Person)-[:ATTENDED]->(:Event)` |
65
+ | `mentions` (Notion's own typed mention relation) | `:MENTIONS` |
66
+ | anything else | `:MENTIONS { mentionContext: '<relation-column-name>' }` |
67
+
68
+ The mapping table lives in this reference — not in a server-side enum — so a workspace with custom relation names is handled by extending this file, not by code change.
69
+
70
+ ## Forward-reference resolution
71
+
72
+ A row's relation column may point at a row not yet written (later in the walk or in a later batch). The buffered execution model handles this naturally:
73
+
74
+ - **Pass 1 — node writes.** Pages flush via `notion-pages`; database rows flush via one `notion-database-rows` call per `(label, databaseNotionId)` key. No relations are written in pass 1.
75
+ - **Pass 2 — relations.** Once every page and row is MERGEd, `notion-relations-pass-2` flushes `deferredRelations`. Source and target are located by `(accountId, notionId)`; missing endpoints are counted as `skippedUnknownTarget`, not errors.
76
+
77
+ Two passes are deterministic and keep every write idempotent. The completion log reports the `relationsCreated` / `relationsMatched` counters from pass 2.
78
+
79
+ ## Failure modes
80
+
81
+ - `[notion-import:error] reason=row-schema-mismatch file=<path>` — the row's front-matter has properties not in the parent CSV's schema. Write the row with the union of properties; log the mismatch; continue.
82
+ - `[notion-import:error] reason=relation-target-not-found notionId=<id>` — pass 2 reported `skippedUnknownTarget > 0` (a row outside the operator's filter, or an externally-linked workspace row). The pass-2 counter captures the total; per-edge detail is not logged because the server only sees the bucketed list, not the source manifest.
83
+ - `[notion-import:error] reason=label-ambiguous db=<name>` — the name + column heuristic matched no canonical label and the operator's brief did not specify. Fall back to `:KnowledgeDocument { kind: 'notion-row' }` and surface the gap in the completion log.
@@ -0,0 +1,61 @@
1
+ # page-tree
2
+
3
+ Reference for ingesting Notion pages — the `.md` files at every depth of the export.
4
+
5
+ ## Shape on disk
6
+
7
+ A Notion page is a markdown file with a 32-hex Notion ID suffix in its basename. Front-matter at the top (when present) carries page-level properties: `Created`, `Created by`, `Last edited`, `Last edited by`, tags. The body is markdown — headings, paragraphs, lists, embedded media references, inline `@person` mentions, and inline `@page` mentions (page-to-page links).
8
+
9
+ A page with children lives alongside a directory of the same basename containing the child pages and their own subdirectories.
10
+
11
+ ## What each page becomes
12
+
13
+ One `NotionPageRow` appended to `pageBuffer`; the server-side `notion-pages` handler MERGEs it as a `:KnowledgeDocument { kind: 'notion-page' }` at flush time. Row fields:
14
+
15
+ - `notionId` — 32-hex from the basename.
16
+ - `title` — human title from the basename.
17
+ - `text` — full markdown body (front-matter stripped; the front-matter fields are mapped to row fields below).
18
+ - `parentNotionId` — `null` for workspace-root pages; otherwise the basename `notionId` of the enclosing directory.
19
+ - `notionCreatedAt`, `notionLastEditedAt` — ISO 8601 strings from front-matter `Created` / `Last edited`, or `null`.
20
+ - `authorPersonNodeId` — elementId of an in-account `:Person` resolved per the Authorship section below, or `null`.
21
+
22
+ Provenance (`accountId`, `importId`, `source='notion'`, `createdByAgent='notion-import'`, `createdBySession`, `createdAt`) is stamped server-side; the row does not carry it.
23
+
24
+ ## Hierarchy
25
+
26
+ Page hierarchy is written as `:HAS_SECTION` between `:KnowledgeDocument` nodes:
27
+
28
+ - **Top-level pages** — `parentNotionId: null` in the row. The `notion-pages` handler attaches `(:NotionWorkspace)-[:HAS_SECTION]->(:KnowledgeDocument)` at insert time.
29
+ - **Nested pages** — every non-null `parentNotionId` is appended to `deferredRelations` with `edgeType: 'HAS_SECTION'`. The `notion-relations-pass-2` flush resolves them once every page is MERGEd, including parents that land in a later batch than their children.
30
+
31
+ The parent's `notionId` is the basename of the enclosing directory.
32
+
33
+ ## Authorship
34
+
35
+ When the front-matter records `Created by` (or `Author`), resolve the named user against in-account `:Person` per SKILL invariant 4 (account-filtered fuzzy match on `givenName + familyName`). Write `(:Person)-[:AUTHORED]->(:KnowledgeDocument)` when a match is found or a new `:Person` is created. If the front-matter only carries a Notion display name that resolves to no in-account `:Person` and the import flow already created a `:Person` for an earlier mention with the same display name, MERGE on `(accountId, displayName)` so a Notion user named once in front-matter and once inline becomes one node, not two.
36
+
37
+ `Last edited by` is not currently written as an edge — page-level edit provenance is downstream work (a future task can add `:LAST_EDITED_BY` if needed).
38
+
39
+ ## Inline `@person` mentions
40
+
41
+ Notion exports inline person mentions as plain text in the markdown body — typically the user's display name without an `@` prefix in the markdown source, but operators sometimes preserve `@`-prefixed text. The skill matches both shapes:
42
+
43
+ 1. Detect candidate mentions — bracketed names from Notion's link syntax `[Person Name](https://www.notion.so/...)`, plain `@`-prefixed tokens, and front-matter `Mentioned` lists (when present).
44
+ 2. For each candidate, run `mcp__plugin_memory_memory__memory-search` with `accountId` filter and the candidate's display name as the search term. Use the search's existing fuzzy semantics — do not invent a new matcher.
45
+ 3. On match, append a `MENTIONS` entry to `deferredRelations` (`sourceNotionId` = the page's `notionId`, `targetNotionId` requires the matched `:Person` to itself carry a `notionId` — if the `:Person` was created by this import the skill assigns one; if the match is an existing in-account `:Person` without a `notionId`, the mention is written via `memory-write` at walk time instead, because pass-2 endpoint resolution is by `(accountId, notionId)` only). On miss, the new `:Person { givenName, familyName, notionId: <fresh-uuid>, source='notion', accountId }` is created via `memory-write` immediately and the mention queued to pass-2 — the mention is preserved either way.
46
+
47
+ The account-filter step is load-bearing. A "John Smith" mention in the importing account never matches a `:Person` in another account, even if the names collide. See SKILL invariant 4.
48
+
49
+ ## Inline `@page` mentions (page-to-page links)
50
+
51
+ A page-to-page link in Notion exports as `[Other Page Title](Other%20Page%20Title%20<32-hex-notionId>.md)`. Append a `MENTIONS` entry to `deferredRelations` with `sourceNotionId` = the current page, `targetNotionId` = the linked page, and `properties: { mentionContext: 'page-link' }`. Pass-2 handles resolution — links to pages outside the import set are counted as `skippedUnknownTarget` in the pass-2 counters, not errors.
52
+
53
+ ## Idempotency
54
+
55
+ Pages MERGE on `(accountId, notionId)`. Re-importing the same export with no changes is a no-op — every property update writes the same value back, every edge MERGE matches existing. The `skipped` counter advances on each unchanged page. Property-level updates (a page edited in Notion between exports) overwrite without versioning; the previous body is lost. A future task could write `:KnowledgeDocumentRevision` audit nodes if version history becomes a need.
56
+
57
+ ## Failure modes
58
+
59
+ - `[notion-import:error] reason=front-matter-parse file=<path>` — front-matter is malformed YAML. Treat as no front-matter; write the page with body-only.
60
+ - `[notion-import:error] reason=mention-search-failed name=<name>` — `memory-search` raised. Log and continue; the mention is dropped (no fallback Person creation when the search itself failed, only when it returned no match).
61
+ - `[notion-import:error] reason=parent-not-found notionId=<id>` — the enclosing directory's `notionId` did not resolve to a `:KnowledgeDocument`. Anchor the orphan page to the `:NotionWorkspace` and log.
@@ -0,0 +1,41 @@
1
+ # workspace-export
2
+
3
+ Reference for walking a Notion "Export all workspace content" directory.
4
+
5
+ ## What a Notion export looks like
6
+
7
+ Notion's native export ("Settings → Workspace → Export all workspace content" with "Everything", "Markdown & CSV" format) produces a zip. Unzipped, the top level is a single directory named after the workspace plus a Notion UUID; nested inside is the page tree as the operator structured it.
8
+
9
+ Two file shapes occur at every depth:
10
+
11
+ - **Pages** — `Page Title <32-hex-notionId>.md`. The basename is the human title followed by the 32-character hex Notion ID (no dashes). The file body is markdown.
12
+ - **Databases** — `Database Name <32-hex-notionId>.csv` paired with `Database Name <32-hex-notionId>/` directory. The CSV is the table view; the directory contains one `.md` per row plus an attachments tree.
13
+
14
+ Nested page subtrees: a page with children becomes a directory `Page Title <notionId>/` alongside its `.md` file; the directory holds the child pages and their own subdirectories recursively.
15
+
16
+ ## What the walker does
17
+
18
+ 1. **Read the root.** First-level entries are the top-level pages and databases of the workspace.
19
+ 2. **Recurse depth-first** into each subdirectory, classifying entries on each pass.
20
+ 3. **Build the candidate manifest** — a flat list of `{kind, path, notionId, parentNotionId, depth}` records, one per page or row. This is the manifest the selective-ingest gate runs against, and the manifest the write loop consumes.
21
+ 4. **Apply the operator's filter** (top-level subtree, database name, last-edited date) by predicate-matching the manifest before any write begins.
22
+ 5. **Hand off** the filtered manifest to [page-tree.md](page-tree.md) for pages and [databases.md](databases.md) for database tables and rows. Attachments are handed off as referenced from pages, not walked separately — see [attachments.md](attachments.md).
23
+
24
+ ## Notion ID extraction
25
+
26
+ The Notion ID is the last 32 hex characters of the basename, after the trailing space. Strip the `.md` / `.csv` suffix and the trailing space, then take the last 32 chars; the remainder is the human title. Notion IDs are stable across exports; they are the natural key the skill MERGEs on.
27
+
28
+ ## What the walker does NOT do
29
+
30
+ - **Does not** read page or row body content. That happens at write time, per the page/database references.
31
+ - **Does not** decide labels. Label selection happens in [databases.md](databases.md).
32
+ - **Does not** open attachments. Hashing happens at the attachment-copy step.
33
+ - **Does not** retry on partial exports. If a referenced child directory is missing, the parent page is still imported and the gap is logged.
34
+
35
+ ## Failure modes
36
+
37
+ - `[notion-import:error] reason=basename-no-notion-id file=<path>` — basename does not end in a 32-hex Notion ID. Skip the file, continue the walk.
38
+ - `[notion-import:error] reason=orphan-row file=<path>` — a row markdown lives inside a directory whose parent CSV is missing. Skip; the row cannot be typed without the schema.
39
+ - `[notion-import:error] reason=zip-not-extracted path=<path>` — the operator pointed at a `.zip`. Unzip first; the skill does not call `unzip`.
40
+
41
+ The walker never aborts on a single file's failure. Counts of skipped files appear in the completion log.
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "obsidian-import",
3
+ "description": "Import an Obsidian vault (extracted directory of markdown notes + assets) into the Maxy Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: obsidian-import
3
+ description: "Import an Obsidian vault (extracted directory of markdown notes + assets) into the Maxy Neo4j graph. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default."
4
+ tools: []
5
+ always: false
6
+ embed: false
7
+ specialist: database-operator
8
+ metadata: {"platform":{"optional":true,"pluginKey":"obsidian-import"}}
9
+ ---
10
+
11
+ # Obsidian Import
12
+
13
+ Ingests an extracted Obsidian vault (a directory tree of `.md` notes, frontmatter, wikilinks, tags, and embedded attachments) into the Maxy Neo4j graph. Skill-only plugin — no MCP server, no admin tools added. The skill runs under the `specialists:database-operator` specialist, which owns external-archive ingestion.
14
+
15
+ ## When this applies
16
+
17
+ The admin agent delegates to `specialists:database-operator` when the operator drops an extracted Obsidian vault directory (or references one by path) into chat. The specialist runs the skill's archive-owner confirmation flow before any markdown is read, then orchestrates the two-phase server-side import (dry-run → operator disambiguation → commit).
18
+
19
+ ## How parsing happens
20
+
21
+ The skill does not parse markdown. Vault traversal, frontmatter YAML, wikilink regex, tag scanning, daily-notes detection, and attachment resolution all happen server-side inside `mcp__memory__obsidian-vault-import` (memory plugin). The skill is the operator-facing conversation surface only: confirm owner → select filters → invoke the tool with `mode='dry-run'` → present ambiguities → invoke with `mode='commit'` and the operator's resolutions.
22
+
23
+ ## Intra-plugin growth
24
+
25
+ Reference docs land here as the parser surface grows — Canvas, Bases, Dataview queries each get a reference file when the parser supports them. They are not separate plugins.
26
+
27
+ ## Relationship to other plugins
28
+
29
+ - **memory** — owns the parser (`platform/lib/obsidian-parser/`), the `obsidian-vault-import` MCP tool, and the `obsidian-vault` archiveType handler inside `memory-archive-write`. All graph writes happen through that surface.
30
+ - **database-operator specialist** — owns execution. See [database-operator.md](../../templates/specialists/agents/database-operator.md) delegation clause.
31
+
32
+ ## Out of scope
33
+
34
+ - Obsidian plugins, Dataview queries, Canvas, and Bases — text markdown only.
35
+ - Realtime vault sync. One-shot import per upload.
36
+ - Reverse export back to Obsidian.
37
+ - Automatic compiled-truth backfill on imported entities — that happens via subsequent `memory-update` calls, not at import time.
38
+
39
+ Each of the above can be added later as its own reference inside this plugin once a parser extension lands. Nothing silently flows in.