@rubytech/create-maxy-code 0.1.158 → 0.1.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts +1 -1
- package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admin-conversation-purge/dist/index.js +4 -4
- package/payload/platform/lib/admin-conversation-purge/dist/index.js.map +1 -1
- package/payload/platform/lib/admin-conversation-purge/src/index.ts +5 -5
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +48 -17
- package/payload/platform/lib/graph-style/dist/index.js +2 -2
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +1 -1
- package/payload/platform/lib/graph-style/src/index.ts +2 -2
- package/payload/platform/lib/graph-trash/dist/index.js +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +1 -1
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +1 -1
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -1
- package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts +3 -3
- package/payload/platform/lib/graph-write/dist/conversation-provenance.js +5 -5
- package/payload/platform/lib/graph-write/dist/conversation-provenance.js.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +1 -1
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +1 -1
- package/payload/platform/lib/graph-write/src/conversation-provenance.ts +8 -8
- package/payload/platform/lib/graph-write/src/index.ts +1 -1
- package/payload/platform/neo4j/schema.cypher +86 -46
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +8 -4
- package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +361 -0
- package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +13 -13
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +1 -1
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
- package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +102 -0
- package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +106 -0
- package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +15 -8
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +4 -4
- package/payload/platform/plugins/admin/mcp/dist/index.js +65 -24
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +1 -1
- package/payload/platform/plugins/admin/skills/session-management/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +5 -5
- package/payload/platform/plugins/admin/skills/stream-log-review/references/analysis-patterns.md +6 -6
- package/payload/platform/plugins/business-assistant/references/profiling.md +1 -1
- package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +2 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js +3 -3
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js +2 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +4 -4
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js +10 -10
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js.map +1 -1
- package/payload/platform/plugins/docs/references/admin-session.md +6 -6
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/docs/references/internals.md +5 -5
- package/payload/platform/plugins/docs/references/plugins-guide.md +6 -5
- package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +19 -2
- package/payload/platform/plugins/email/mcp/dist/index.js +78 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +8 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +177 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/embedding.js +2 -2
- package/payload/platform/plugins/email/mcp/dist/lib/embedding.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +6 -54
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +12 -300
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.d.ts +23 -0
- package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.js +15 -0
- package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts +6 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js +89 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts +15 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js +49 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts +20 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +91 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +32 -52
- package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +19 -83
- package/payload/platform/plugins/email/skills/email-ingest/SKILL.md +87 -0
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +10 -37
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +1 -0
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +11 -2
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +139 -439
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +15 -47
- package/payload/platform/plugins/memory/mcp/dist/index.js +197 -58
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +43 -27
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js +62 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +42 -25
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +23 -17
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js +61 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js +9 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.d.ts +3 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.js +6 -6
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts +41 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +71 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js +31 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +2 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +162 -78
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.js +3 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js +141 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js +8 -8
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js +86 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +33 -34
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +94 -273
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts +43 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js +96 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js +73 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +0 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +4 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +40 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +8 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +25 -9
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +11 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts +28 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js +46 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +26 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +11 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/scripts/backfill-typed-edges.ts +1 -1
- package/payload/platform/plugins/memory/mcp/scripts/graph/fixture.cypher +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +5 -4
- package/payload/platform/plugins/memory/references/graph-primitives.md +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +8 -10
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +100 -116
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +74 -24
- package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +1 -1
- package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +1 -1
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +1 -1
- package/payload/platform/plugins/substack-import/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/substack-import/PLUGIN.md +34 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +182 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/archive-shape.md +68 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/attachments.md +72 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/engagement.md +61 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/posts.md +80 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/subscribers.md +74 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/work/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/work/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/work/mcp/dist/tools/project-create.js +6 -6
- package/payload/platform/plugins/work/mcp/dist/tools/project-create.js.map +1 -1
- package/payload/platform/plugins/work/mcp/dist/tools/session-list.d.ts +1 -1
- package/payload/platform/plugins/work/mcp/dist/tools/session-list.d.ts.map +1 -1
- package/payload/platform/plugins/work/mcp/dist/tools/session-list.js +1 -1
- package/payload/platform/plugins/work/mcp/dist/tools/session-list.js.map +1 -1
- package/payload/platform/plugins/x-import/references/archive-shape.md +2 -2
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +4 -4
- package/payload/platform/scripts/admin-persist-audit.ts +18 -18
- package/payload/platform/scripts/identity-forbidden-token-check.mjs +2 -0
- package/payload/platform/scripts/seed-neo4j.sh +1 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js +3 -3
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +3 -3
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +4 -4
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +2 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +45 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +5 -17
- package/payload/platform/templates/specialists/agents/librarian.md +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-AXOL2PPG.js → chunk-3TRIXQSJ.js} +101 -101
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{admin-3fLFD5h_.js → admin-Dal5FWaD.js} +2 -2
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Breg9-99.js → architectureDiagram-Q4EWVU46-Do2zYONG.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DXf1_673.js → blockDiagram-DXYQGD6D-DpIX_bRu.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CSZ36W4-.js → c4Diagram-AHTNJAMY-Cz335ZSY.js} +1 -1
- package/payload/server/public/assets/channel-PC4_IjI1.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-DsrLk2eK.js → chunk-336JU56O-Dmknn3Wo.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-JpNfNf81.js → chunk-426QAEUC-ClfGjizN.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-DmClEAD0.js → chunk-4TB4RGXK-BPKctJ5c.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-BI9blW3N.js → chunk-5FUZZQ4R-BWDCO9zw.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-BbgttEFO.js → chunk-5PVQY5BW-BE34_Db0.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-BX_LCFOx.js → chunk-EDXVE4YY-BysA-_6v.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-Bb7EoD0w.js → chunk-ENJZ2VHE-BMVyMsqA.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-DH8uQk-s.js → chunk-ICPOFSXX-C5KRdLV4.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-FH5kgXsw.js → chunk-OYMX7WX6-DVZD-AVc.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-D_ljlkHr.js → chunk-U2HBQHQK-DYmQdx8x.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-BX3EmZvm.js → chunk-X2U36JSP-DrZenIl3.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-Vu7-E93h.js → chunk-YZCP3GAM-CDuGbvMq.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-CwjhNPX5.js → chunk-ZZ45TVLE-COztbiu5.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-B_pMa-SX.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BdBAgF20.js +1 -0
- package/payload/server/public/assets/clone-CkNt9L9r.js +1 -0
- package/payload/server/public/assets/{dagre-Ce6gsj0C.js → dagre-Cv9Ib2OM.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-BaMGpsoy.js → dagre-KV5264BT-CoEd61KZ.js} +1 -1
- package/payload/server/public/assets/{data-BHASh0_Y.js → data-DbSORCl4.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-CHEE9rjq.js → diagram-5BDNPKRD-WXNzw3uW.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-C98sZLW1.js → diagram-G4DWMVQ6-C6IuQ2VV.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-DNvMGsTZ.js → diagram-MMDJMWI5-CdJdir5A.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-Cdte4IBa.js → diagram-TYMM5635-DxWKQH5T.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-C9VZsWXK.js → erDiagram-SMLLAGMA-C9fvs33K.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-tuP5fzzR.js → flowDiagram-DWJPFMVM-BO7Yrbqv.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-SJgRPUwx.js → ganttDiagram-T4ZO3ILL-CQJyWBZq.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-DU_cI_50.js → gitGraphDiagram-UUTBAWPF-DZfQHrYw.js} +1 -1
- package/payload/server/public/assets/{graph-labels-DXlmm1og.js → graph-labels-BBB5jsZk.js} +1 -1
- package/payload/server/public/assets/{graph-BhEI09tF.js → graph-ys_joP2o.js} +1 -1
- package/payload/server/public/assets/{graphlib-D_nAsvmx.js → graphlib-C40H-Q5E.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-Cj2Y19m6.js → infoDiagram-42DDH7IO-CakfV5f9.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DMc1xq1y.js → ishikawaDiagram-UXIWVN3A-CPuBPyfa.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-C4CRAQma.js → journeyDiagram-VCZTEJTY-zpZm2Mmo.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CTr4AldU.js → kanban-definition-6JOO6SKY-DfJduQCo.js} +1 -1
- package/payload/server/public/assets/{line-Btlm_IAl.js → line-BpYQqO5R.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-DXFdj_y1.js → mermaid-parser.core-C5Mwi14D.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-DFBhoCr5.js → mermaid.core-D8qBtvvT.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-r9gVQybo.js → mindmap-definition-QFDTVHPH-Bli9JhIN.js} +1 -1
- package/payload/server/public/assets/{page-CDWu1Aft.js → page--bl5Y7ZI.js} +2 -2
- package/payload/server/public/assets/page-CwmZ76U7.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CHKZjSaH.js → pieDiagram-DEJITSTG-MsLXTSLg.js} +1 -1
- package/payload/server/public/assets/{public-DyGWgfaZ.js → public-DltgEfA9.js} +2 -2
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CSGR1HJr.js → quadrantDiagram-34T5L4WZ-De1sh2K0.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DZ6hX9D6.js → requirementDiagram-MS252O5E-D5Sfg1t_.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DbZBKh0m.js → sankeyDiagram-XADWPNL6-DHA3LIbv.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BHHpb8dt.js → sequenceDiagram-FGHM5R23-BE_QlG1B.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DQ1jiLB4.js → stateDiagram-FHFEXIEX-NIwp3F4u.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-pfk2F6st.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BlGiEU7p.js → timeline-definition-GMOUNBTQ-CPE9jyDF.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DmEH-hsB.js → vennDiagram-DHZGUBPP-CKfoyYmH.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C7KJJ40d.js → wardleyDiagram-NUSXRM2D-B9yJek-o.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-JDnusCaa.js → xychartDiagram-5P7HB3ND-DuCcSLqX.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/public/public.html +1 -1
- package/payload/server/server.js +133 -133
- package/payload/server/public/assets/channel-BmZnt3PP.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-kmMLFUpV.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BV7Ae8ct.js +0 -1
- package/payload/server/public/assets/clone-B5eqoN9I.js +0 -1
- package/payload/server/public/assets/page-cyvshX4Z.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CByT2RzF.js +0 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: substack-import
|
|
3
|
+
description: Ingest an extracted Substack "Export your data" archive into the graph as one `:KnowledgeDocument {kind:'substack-post'}` per essay plus one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` carrying `:MENTIONS` edges to each subscriber `:Person`. Triggers when the operator drops a Substack archive directory or `<publication>.zip` path into chat. Owner + publication name confirm against the importing `:AdminUser` before any read. Idempotent on `substackPostId` and `(accountId, email)`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Substack Import
|
|
7
|
+
|
|
8
|
+
**Invoked from `specialists:archive-ingest-operator`** — the admin agent dispatches the archive-ingest-operator when the operator hands over a Substack archive, and the specialist loads this skill. The admin agent never calls this skill directly.
|
|
9
|
+
|
|
10
|
+
One Substack archive becomes:
|
|
11
|
+
|
|
12
|
+
- **N `:KnowledgeDocument` nodes** (`source='substack'`, `kind='substack-post'`) — one per essay, MERGEd on the synthetic stable key `attachmentId = "substack-post-${substackPostId}"`. Substack post IDs survive edits, so re-importing after an edit replaces the prior KD's sections under the same `sourceDocumentId`. `document-ingest`'s in-turn typed-section classification chunks the body into `:Section` children.
|
|
13
|
+
- **One `:KnowledgeDocument`** (`source='substack'`, `kind='substack-subscriber-roster'`) per import run, MERGEd on `(accountId, kind, publicationName, importId)`, carrying the subscriber count and one `:MENTIONS` edge per subscriber.
|
|
14
|
+
- **N `:Person` nodes** — one per subscriber row, MERGEd on `(accountId, email)`. Existing `:Person` nodes are property-update no-ops on re-import.
|
|
15
|
+
- **N `:DigitalDocument` nodes** (`kind='substack-image'`) — one per image referenced from any post body, MERGEd on `(accountId, sha256)`, copied to `{accountDir}/archive/substack/<importId>/`.
|
|
16
|
+
|
|
17
|
+
## Anchor model
|
|
18
|
+
|
|
19
|
+
A Substack archive has one operator-side identity (the publication's author / owner) and N subscribers. Provenance anchors on the importing `:AdminUser` via `[:IMPORTED]` to every node this skill writes. Subscribers are not "owned by" the importer — they are `:Person` nodes scoped by `accountId`, with the subscription expressed as a `:MENTIONS` edge from the roster KD to each `:Person`. The `publicationName` short string lives as a property on every post KD and the roster KD; there is no `:SubstackPublication` anchor node.
|
|
20
|
+
|
|
21
|
+
Every node this skill writes (or that document-ingest writes on this skill's dispatch) carries `importId` (UUID per skill run) so a future dream-cycle pass can detect orphans or selectively re-ingest.
|
|
22
|
+
|
|
23
|
+
## Archive shape
|
|
24
|
+
|
|
25
|
+
See [archive-shape.md](references/archive-shape.md) for the directory layout, the `posts.csv` columns this skill consumes, the subscriber CSV columns, and the engagement-events filename detection list. Substack export filenames vary by export version — the skill probes a known list and continues with engagement properties absent if none match.
|
|
26
|
+
|
|
27
|
+
## Pipeline (deterministic, four phases)
|
|
28
|
+
|
|
29
|
+
### Phase 0 — Provenance confirmation
|
|
30
|
+
|
|
31
|
+
Before any file body is read:
|
|
32
|
+
|
|
33
|
+
1. **Identify the importing AdminUser.** Default to the live operator (`$USER_ID`). Surface to the operator: `"Substack archive: importing as :AdminUser <name> ({userId}). Confirm (yes/no)."`. If the operator is importing on behalf of someone else, accept either an existing AdminUser userId or a new `:Person` natural-key (`givenName` + `familyName` + at least one of `email` / `telephone`). Persist as `$importerNodeId`.
|
|
34
|
+
|
|
35
|
+
2. **Confirm the publication label.** Ask: `"What's the publication's display name? (short string, e.g. 'Stories from the Field')"`. Persist as `$publicationName` — used as a property on every post KD and the roster KD. **No `:SubstackPublication` node is MERGEd** — the label is a property, not an anchor.
|
|
36
|
+
|
|
37
|
+
3. **Persist run identity.** Generate `$importId = randomUUID()` and `$sessionId` (the writer-session for this skill run). Both flow into every write.
|
|
38
|
+
|
|
39
|
+
Refuse to read the archive until both `$importerNodeId` and `$publicationName` are echo-confirmed.
|
|
40
|
+
|
|
41
|
+
### Phase 1 — Walk + engagement detection
|
|
42
|
+
|
|
43
|
+
1. **Walk the archive directory.** Classify entries per [archive-shape.md](references/archive-shape.md):
|
|
44
|
+
- `posts.csv` — post metadata index.
|
|
45
|
+
- `posts/<slug>.html` — per-post body files.
|
|
46
|
+
- `subscribers.csv` — subscriber roster.
|
|
47
|
+
- Engagement events file — try in order: `email_activity.csv`, `subscriber_activity.csv`, `emails.csv`. Persist the matched filename as `$engagementFile` or `null`.
|
|
48
|
+
- `images/` (or any directory referenced from post-body `<img src="…">`) — image enclosures.
|
|
49
|
+
|
|
50
|
+
2. **Count candidates.** `postCount = posts.csv row count`, `subscriberCount = subscribers.csv row count`, `imageCount = distinct image src refs across all posts/*.html`.
|
|
51
|
+
|
|
52
|
+
3. **Emit walk-complete log line.** `[substack-import] start importId=<id> publication=<name> exportPath=<path> posts=<n> subscribers=<n> images=<n> engagementFile=<name|none>`. This line is the operator's grep-able run-shape signal.
|
|
53
|
+
|
|
54
|
+
### Phase 2 — Bulk-import gate
|
|
55
|
+
|
|
56
|
+
If `postCount > 200` OR `subscriberCount > 2000`:
|
|
57
|
+
|
|
58
|
+
1. Emit `[substack-import:gate] threshold=posts>200|subscribers>2000 reason=bulk awaiting=filter`.
|
|
59
|
+
2. Surface to the operator: `"This archive has <n> posts and <n> subscribers. Filter before write? Available axes: publishedAt date range (e.g. 'since 2024-01-01'), post tag (e.g. 'only tag X'), subscriber tier ('free' | 'paid' | 'comp'). Reply with one or more, or 'all' to bypass the gate."`.
|
|
60
|
+
3. Apply the operator's filter to the candidate set before any write begins.
|
|
61
|
+
4. Re-emit a fresh `start` line with the reduced counts.
|
|
62
|
+
|
|
63
|
+
Below threshold, skip this phase entirely.
|
|
64
|
+
|
|
65
|
+
### Phase 3 — Writes (three sub-passes, in this order)
|
|
66
|
+
|
|
67
|
+
**3a. Per-post `:KnowledgeDocument` via librarian/document-ingest.**
|
|
68
|
+
|
|
69
|
+
For each post (after gate filter), dispatch `librarian` via the Task tool. The dispatch brief carries:
|
|
70
|
+
|
|
71
|
+
- **Anchor:** the confirmed `$importerNodeId` (`:AdminUser` if the importer was the operator, else the named `:Person`).
|
|
72
|
+
- **`attachmentId`:** `"substack-post-${substackPostId}"` — synthetic stable key. Substack post IDs survive edits; re-imports of an edited post replace prior sections under the same `sourceDocumentId`.
|
|
73
|
+
- **`storagePath`:** `{accountDir}/archive/substack/<importId>/posts/<slug>.html` (copy the source file there before dispatch so the storage path is stable).
|
|
74
|
+
- **`mimeType`:** `text/html`.
|
|
75
|
+
- **`sourceUrl`:** `https://<publication-domain>/p/<slug>` if the export carries it, else absent.
|
|
76
|
+
- **`sourceType`:** `'substack'` (carried into `:KnowledgeDocument.source`).
|
|
77
|
+
- **`scope`:** `'admin'` (the post is the operator's authored content).
|
|
78
|
+
- **Additional KD properties** (set by document-ingest from the brief's `extraProperties` block): `kind: 'substack-post'`, `substackPostId`, `slug`, `publishedAt`, `isPaid`, `publicationName`, `bodyTruncated` (true if the body is a paywall stub — detected by length < 200 chars on `isPaid=true` posts), `importId`, `createdByAgent: 'substack-import'`, `createdBySession: $sessionId`.
|
|
79
|
+
|
|
80
|
+
Per-post outcomes (created / updated / skipped) are returned by document-ingest and accumulated into the per-pass counter — no per-post log line is emitted.
|
|
81
|
+
|
|
82
|
+
See [posts.md](references/posts.md) for HTML-to-markdown body conversion rules and inline-mention handling.
|
|
83
|
+
|
|
84
|
+
**3b. Roster KD + per-subscriber `:Person` + `:MENTIONS` edges.**
|
|
85
|
+
|
|
86
|
+
`archive-ingest-operator` runs these writes directly using its `memory-write` tool — no dispatch needed.
|
|
87
|
+
|
|
88
|
+
1. **Read engagement aggregates** (once, before the per-subscriber loop). If `$engagementFile` is non-null, parse it per [engagement.md](references/engagement.md) and build an in-memory map `emailToAggregates: { email -> { totalOpens, totalClicks, lastOpenedAt, lastClickedAt, engagementWindowDays } }`. If `$engagementFile` is null, emit `[substack-import:warn] no-engagement-file` once and use an empty map.
|
|
89
|
+
|
|
90
|
+
2. **MERGE the roster KD** via `memory-write`:
|
|
91
|
+
```cypher
|
|
92
|
+
MERGE (r:KnowledgeDocument {accountId: $accountId, kind: 'substack-subscriber-roster', publicationName: $publicationName, importId: $importId})
|
|
93
|
+
ON CREATE SET r.source = 'substack', r.subscriberCount = $subscriberCount,
|
|
94
|
+
r.createdByAgent = 'substack-import', r.createdBySession = $sessionId,
|
|
95
|
+
r.createdAt = datetime()
|
|
96
|
+
```
|
|
97
|
+
Persist the returned elementId as `$rosterNodeId`.
|
|
98
|
+
|
|
99
|
+
3. **Write the `:IMPORTED` edge** from `$importerNodeId` to `$rosterNodeId` carrying `importId` and `createdAt`.
|
|
100
|
+
|
|
101
|
+
4. **Per-subscriber loop.** For each row in `subscribers.csv` (after gate filter), parse per [subscribers.md](references/subscribers.md), look up `emailToAggregates[row.email]`, then one `memory-write` MERGE:
|
|
102
|
+
```cypher
|
|
103
|
+
MERGE (p:Person {accountId: $accountId, email: $email})
|
|
104
|
+
ON CREATE SET p.givenName = $givenName, p.familyName = $familyName,
|
|
105
|
+
p.source = 'substack', p.createdByAgent = 'substack-import',
|
|
106
|
+
p.createdBySession = $sessionId, p.createdAt = datetime(),
|
|
107
|
+
p.importId = $importId
|
|
108
|
+
WITH p
|
|
109
|
+
MATCH (r:KnowledgeDocument) WHERE elementId(r) = $rosterNodeId
|
|
110
|
+
MERGE (r)-[m:MENTIONS {mentionContext: 'substack-subscription'}]->(p)
|
|
111
|
+
ON CREATE SET m.tier = $tier, m.subscribedAt = $subscribedAt,
|
|
112
|
+
m.createdByAgent = 'substack-import', m.createdAt = datetime()
|
|
113
|
+
SET m.unsubscribedAt = $unsubscribedAt,
|
|
114
|
+
m.totalOpens = $totalOpens, m.totalClicks = $totalClicks,
|
|
115
|
+
m.lastOpenedAt = $lastOpenedAt, m.lastClickedAt = $lastClickedAt,
|
|
116
|
+
m.engagementWindowDays = $engagementWindowDays
|
|
117
|
+
```
|
|
118
|
+
Engagement properties OVERWRITE on re-import (the export carries totals-to-date, not deltas). Absent engagement (no row in the events file) sets the properties to `null`, which Neo4j removes — so missing aggregates are absent, not zero.
|
|
119
|
+
|
|
120
|
+
Per-row outcomes accumulate into the per-pass counter — no per-row log line is emitted.
|
|
121
|
+
|
|
122
|
+
**3c. Image enclosures.**
|
|
123
|
+
|
|
124
|
+
For each distinct image referenced from any imported post body:
|
|
125
|
+
|
|
126
|
+
1. Copy file to `{accountDir}/archive/substack/<importId>/<sha256><ext>` (perms 0o600).
|
|
127
|
+
2. `memory-write` MERGE:
|
|
128
|
+
```cypher
|
|
129
|
+
MERGE (d:DigitalDocument {accountId: $accountId, sha256: $sha256})
|
|
130
|
+
ON CREATE SET d.kind = 'substack-image', d.source = 'substack',
|
|
131
|
+
d.storagePath = $storagePath, d.mimeType = $mimeType,
|
|
132
|
+
d.createdByAgent = 'substack-import', d.createdBySession = $sessionId,
|
|
133
|
+
d.createdAt = datetime(), d.importId = $importId
|
|
134
|
+
```
|
|
135
|
+
3. For each post KD that references this image, attach the canonical enclosure edge:
|
|
136
|
+
```cypher
|
|
137
|
+
MATCH (k:KnowledgeDocument), (d:DigitalDocument)
|
|
138
|
+
WHERE k.attachmentId = $postAttachmentId AND elementId(d) = $imageElementId
|
|
139
|
+
MERGE (k)-[e:HAS_ENCLOSURE]->(d)
|
|
140
|
+
ON CREATE SET e.createdByAgent = 'substack-import', e.createdAt = datetime()
|
|
141
|
+
```
|
|
142
|
+
`:HAS_ENCLOSURE` is the canonical attachment edge for `:KnowledgeDocument → :DigitalDocument` declared in `schema-base.md`. If at skill-load the canonical edge is absent from the schema, fall back to `:MENTIONS {mentionContext: 'substack-image-enclosure'}` — the documented closed-edge fallback.
|
|
143
|
+
|
|
144
|
+
See [attachments.md](references/attachments.md) for the image-reference discovery walk and storage-path conventions.
|
|
145
|
+
|
|
146
|
+
### Phase 4 — Completion log
|
|
147
|
+
|
|
148
|
+
Emit one grep-able summary line:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
[substack-import] importId=<id> posts=<created>/<updated>/<skipped> subscribers=<created>/<updated>/<skipped> images=<copied>/<skipped> engagementSubscribers=<n> ms=<elapsed>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`engagementSubscribers` counts subscribers whose `:MENTIONS` edge received engagement properties (non-null). A `start` line that named an engagement file followed by `engagementSubscribers=0` at completion is a parse defect — investigate the events-file shape.
|
|
155
|
+
|
|
156
|
+
## Idempotency
|
|
157
|
+
|
|
158
|
+
- Re-importing identical archive bytes is a no-op: post KDs match on the synthetic `attachmentId = "substack-post-${substackPostId}"`; `document-ingest` skips identical content. Subscribers match on `(accountId, email)`; the `:Person` MERGE is a property-update no-op. Images match on `(accountId, sha256)`.
|
|
159
|
+
- Re-importing an archive with edited posts: the synthetic `attachmentId` is stable, so the same KD is updated in place (`document-ingest` regenerates sections under the same `sourceDocumentId`).
|
|
160
|
+
- Re-importing an archive with newer engagement data: engagement properties OVERWRITE via `ON MATCH SET`. The export carries totals-to-date, not deltas; the latest export's totals are authoritative.
|
|
161
|
+
- Re-importing with an additive subscriber list: existing subscribers' `:MENTIONS` edges keep their `subscribedAt`; new subscribers get a fresh edge. Unsubscribes set `unsubscribedAt` (and do not delete the edge).
|
|
162
|
+
- The roster KD is one-per-import-run (keyed on `importId`), so each import leaves an auditable trail of which run produced which `:MENTIONS` set.
|
|
163
|
+
|
|
164
|
+
## Closed-edge defence
|
|
165
|
+
|
|
166
|
+
The skill never invents an edge type. Every edge written:
|
|
167
|
+
|
|
168
|
+
- `:IMPORTED` (AdminUser → roster KD) — provenance, schema-declared.
|
|
169
|
+
- `:MENTIONS {mentionContext:'substack-subscription', ...engagement}` (roster KD → Person) — documented catch-all for any relation not in the closed enumeration.
|
|
170
|
+
- `:HAS_ENCLOSURE` (post KD → DigitalDocument) — canonical attachment edge if schema-base declares it; `:MENTIONS {mentionContext:'substack-image-enclosure'}` fallback.
|
|
171
|
+
|
|
172
|
+
Audit query: `MATCH ()-[r]->() WHERE r.createdByAgent='substack-import' RETURN DISTINCT type(r)` must return only the above. Any other edge type is a defect.
|
|
173
|
+
|
|
174
|
+
## What this skill does not do
|
|
175
|
+
|
|
176
|
+
- **New labels.** No `:SubstackPublication`, `:Subscriber`, `:Subscription`, `:Post`. Everything maps to existing `:KnowledgeDocument` + `:Person` + `:DigitalDocument`.
|
|
177
|
+
- **New edge types.** No `:SUBSCRIBED_TO`, `:OPENED`, `:CLICKED`. Subscription is `:MENTIONS`; engagement is properties on that edge.
|
|
178
|
+
- **Per-event engagement detail.** Per-subscriber-per-post events are aggregated to per-edge totals; the individual open/click events are not preserved.
|
|
179
|
+
- **Comments.** Substack export does not include reader comments.
|
|
180
|
+
- **Inbound newsletter ingestion.** A Substack post landing in the operator's Gmail goes through `conversation-archive --source email` (Task 426), not this plugin.
|
|
181
|
+
- **Live API or RSS poll.** Operator-dropped export only.
|
|
182
|
+
- **Audio / video enclosures.** Substack podcast / video posts use additional file shapes the v1 plugin does not handle; only images referenced from post-body `<img>` tags are enclosed.
|
package/payload/platform/plugins/substack-import/skills/substack-import/references/archive-shape.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Substack archive shape
|
|
2
|
+
|
|
3
|
+
The "Export your data" download from Substack arrives as a zip. Once extracted, the top-level directory contains the following entries. Filenames have varied across export versions — the skill probes the listed candidates in order and binds to the first match.
|
|
4
|
+
|
|
5
|
+
## Directory layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
<archive-root>/
|
|
9
|
+
├── posts.csv # post metadata index (required)
|
|
10
|
+
├── posts/
|
|
11
|
+
│ ├── <post-id>.<slug>.html # one HTML body per post (filename = "<post_id>.<slug>.html")
|
|
12
|
+
│ └── …
|
|
13
|
+
├── subscribers.csv # subscriber roster (required)
|
|
14
|
+
├── email_activity.csv # engagement events (one of these names, optional)
|
|
15
|
+
│ OR subscriber_activity.csv
|
|
16
|
+
│ OR emails.csv
|
|
17
|
+
├── images/ # image enclosures referenced from post bodies
|
|
18
|
+
│ └── <files>
|
|
19
|
+
└── (other files — manifest.json, etc.) ignored by this skill
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Required: `posts.csv` and `subscribers.csv` must exist. If either is missing, abort the walk with `[substack-import:error] file=<archive-root> reason="missing required file: <name>"`.
|
|
23
|
+
|
|
24
|
+
Optional: engagement events file. If none of the three candidate names exist, emit `[substack-import:warn] no-engagement-file` once and continue with engagement properties absent on every `:MENTIONS` edge.
|
|
25
|
+
|
|
26
|
+
## `posts.csv` columns this skill consumes
|
|
27
|
+
|
|
28
|
+
| Column | Used as | Notes |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `post_id` | `substackPostId` (stable across edits) | Required. MERGE key via synthetic `attachmentId = "substack-post-${post_id}"`. |
|
|
31
|
+
| `post_date` | `publishedAt` (ISO 8601) | Required. |
|
|
32
|
+
| `is_published` | filter | Only `true` rows are imported; drafts are dropped at walk. |
|
|
33
|
+
| `type` | filter | Only `newsletter` / `podcast` / `thread` rows imported; system rows dropped. |
|
|
34
|
+
| `audience` | `isPaid` | `everyone` → `isPaid=false`; `only_paid` / `founding` → `isPaid=true`. |
|
|
35
|
+
| `slug` | `slug` | Used to compose the post body filename `posts/<post_id>.<slug>.html`. |
|
|
36
|
+
| `title` | `title` | KD title. |
|
|
37
|
+
| `subtitle` | `subtitle` | Optional KD property. |
|
|
38
|
+
| `email_sent_at` | optional context | Recorded if present. |
|
|
39
|
+
|
|
40
|
+
Columns the skill does not read (`url`, `webRecommendations`, etc.) are ignored — adding them later does not require a parser change.
|
|
41
|
+
|
|
42
|
+
## `subscribers.csv` columns this skill consumes
|
|
43
|
+
|
|
44
|
+
| Column | Used as | Notes |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `email` | `:Person.email` (MERGE key) | Required. Lowercased. |
|
|
47
|
+
| `name` | `:Person.givenName` + `:Person.familyName` | Split on first whitespace; surname = remainder. Single-token names use the token as `givenName`. |
|
|
48
|
+
| `type` (or `membership_state` / `subscription_state`) | `:MENTIONS.tier` | `free` / `paid` / `comp` / `gift`. Unknown values pass through verbatim. |
|
|
49
|
+
| `created_at` (or `subscribed_at`) | `:MENTIONS.subscribedAt` | ISO 8601. |
|
|
50
|
+
| `expiry` / `unsubscribed_at` | `:MENTIONS.unsubscribedAt` | Optional. Present only for unsubscribed rows. |
|
|
51
|
+
|
|
52
|
+
Columns the skill does not read (e.g. `stripe_customer_id`) are ignored.
|
|
53
|
+
|
|
54
|
+
## Engagement-events filename detection
|
|
55
|
+
|
|
56
|
+
The skill probes for the engagement events file in this order. The first match binds `$engagementFile`:
|
|
57
|
+
|
|
58
|
+
1. `email_activity.csv`
|
|
59
|
+
2. `subscriber_activity.csv`
|
|
60
|
+
3. `emails.csv`
|
|
61
|
+
|
|
62
|
+
If a new export version uses a different name, add it to this list — the skill does not auto-discover.
|
|
63
|
+
|
|
64
|
+
## Hard limits
|
|
65
|
+
|
|
66
|
+
- `posts.csv` row count > 200: bulk-gate fires.
|
|
67
|
+
- `subscribers.csv` row count > 2000: bulk-gate fires.
|
|
68
|
+
- Individual post body > 50 MB: skipped with `[substack-import:error] file=<…> reason="body exceeds 50MB"`. (Substack does not generally produce bodies this large; the limit is a defence against malformed archives.)
|
package/payload/platform/plugins/substack-import/skills/substack-import/references/attachments.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Image enclosures
|
|
2
|
+
|
|
3
|
+
Image files referenced from post bodies become `:DigitalDocument` nodes attached to the parent post KD via the canonical `:HAS_ENCLOSURE` edge (or `:MENTIONS {mentionContext:'substack-image-enclosure'}` as the documented closed-edge fallback if `schema-base.md` does not declare `:HAS_ENCLOSURE` for `:KnowledgeDocument → :DigitalDocument` at skill-load).
|
|
4
|
+
|
|
5
|
+
## Discovery walk
|
|
6
|
+
|
|
7
|
+
After Phase 3a (post writes) but before Phase 3c (image writes), build a per-post image-reference map:
|
|
8
|
+
|
|
9
|
+
1. For each successfully-imported post HTML body (the operator's source file, before HTML→markdown), parse `<img src="…">` tags.
|
|
10
|
+
2. Resolve the `src` relative to the archive root. Substack export sources are typically `images/<filename>` or `../images/<filename>` from a post-body directory; resolve both to the absolute path under `<archive-root>/images/`.
|
|
11
|
+
3. Record `{ postAttachmentId → [imageAbsolutePath, …] }`.
|
|
12
|
+
|
|
13
|
+
Image files referenced from `<img>` are the only enclosures considered. Audio (`<audio>`) and video (`<video>`) embeds are out of scope per SKILL.md.
|
|
14
|
+
|
|
15
|
+
## Per-image write
|
|
16
|
+
|
|
17
|
+
For each distinct image path (one MERGE per file, regardless of how many posts reference it):
|
|
18
|
+
|
|
19
|
+
1. **Compute content hash.** SHA-256 of the file bytes. Persist as `$sha256`.
|
|
20
|
+
2. **Copy file.** From the archive's `images/<filename>` to `{accountDir}/archive/substack/<importId>/<sha256><ext>` (perms 0o600). The destination filename is content-addressed so the same image referenced from multiple imports overlays cleanly.
|
|
21
|
+
3. **Detect mime type.** From the file extension first (`.jpg`/`.jpeg` → `image/jpeg`, `.png` → `image/png`, `.gif` → `image/gif`, `.webp` → `image/webp`). If the extension is missing or unrecognised, sniff the first 16 bytes against known image magic numbers. Fallback: `application/octet-stream`.
|
|
22
|
+
4. **MERGE the `:DigitalDocument`.**
|
|
23
|
+
```cypher
|
|
24
|
+
MERGE (d:DigitalDocument {accountId: $accountId, sha256: $sha256})
|
|
25
|
+
ON CREATE SET d.kind = 'substack-image',
|
|
26
|
+
d.source = 'substack',
|
|
27
|
+
d.storagePath = $storagePath,
|
|
28
|
+
d.mimeType = $mimeType,
|
|
29
|
+
d.originalFilename = $originalFilename,
|
|
30
|
+
d.createdByAgent = 'substack-import',
|
|
31
|
+
d.createdBySession = $sessionId,
|
|
32
|
+
d.createdAt = datetime(),
|
|
33
|
+
d.importId = $importId
|
|
34
|
+
```
|
|
35
|
+
Re-imports of the same image are property-update no-ops.
|
|
36
|
+
|
|
37
|
+
## Per-post attach
|
|
38
|
+
|
|
39
|
+
For each `(postAttachmentId, imageElementId)` pair from the discovery map:
|
|
40
|
+
|
|
41
|
+
```cypher
|
|
42
|
+
MATCH (k:KnowledgeDocument {accountId: $accountId, attachmentId: $postAttachmentId}),
|
|
43
|
+
(d:DigitalDocument) WHERE elementId(d) = $imageElementId
|
|
44
|
+
MERGE (k)-[e:HAS_ENCLOSURE]->(d)
|
|
45
|
+
ON CREATE SET e.createdByAgent = 'substack-import',
|
|
46
|
+
e.createdAt = datetime()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If at skill-load the canonical `:HAS_ENCLOSURE` edge is absent from `schema-base.md` for `:KnowledgeDocument → :DigitalDocument`, swap to:
|
|
50
|
+
|
|
51
|
+
```cypher
|
|
52
|
+
MERGE (k)-[e:MENTIONS {mentionContext: 'substack-image-enclosure'}]->(d)
|
|
53
|
+
ON CREATE SET e.createdByAgent = 'substack-import',
|
|
54
|
+
e.createdAt = datetime()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The choice is fixed at skill-load (one read of `schema-base.md`), not per-image — so every image enclosure in this run uses the same edge type. The closed-edge audit query distinguishes the two.
|
|
58
|
+
|
|
59
|
+
## Missing image files
|
|
60
|
+
|
|
61
|
+
If an `<img src="…">` reference points at a file that does not exist in the archive's `images/` directory:
|
|
62
|
+
|
|
63
|
+
- Emit `[substack-import:error] file=<postFilename> reason="image reference missing: <src>"`.
|
|
64
|
+
- The post KD is unaffected (it already wrote in Phase 3a); the enclosure is simply absent.
|
|
65
|
+
- Continue with remaining images.
|
|
66
|
+
|
|
67
|
+
## Counter
|
|
68
|
+
|
|
69
|
+
The per-pass image counter (in the completion log) accumulates:
|
|
70
|
+
|
|
71
|
+
- `copied`: count of files where the destination at `{accountDir}/archive/substack/<importId>/<sha256><ext>` did not exist before (first-time content). Re-imports of the same image bytes do not advance this counter.
|
|
72
|
+
- `skipped`: count of files where the destination already existed (re-import of identical content).
|
package/payload/platform/plugins/substack-import/skills/substack-import/references/engagement.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Engagement events aggregation
|
|
2
|
+
|
|
3
|
+
Substack engagement export carries per-subscriber-per-post events (opens, clicks). This skill aggregates them to per-subscriber totals folded onto the `:MENTIONS` edge between the roster KD and each `:Person` — per-event detail is intentionally dropped.
|
|
4
|
+
|
|
5
|
+
## Detection
|
|
6
|
+
|
|
7
|
+
Per [archive-shape.md](archive-shape.md), the skill probes for the events file in this order:
|
|
8
|
+
|
|
9
|
+
1. `email_activity.csv`
|
|
10
|
+
2. `subscriber_activity.csv`
|
|
11
|
+
3. `emails.csv`
|
|
12
|
+
|
|
13
|
+
The first match binds `$engagementFile`. None match: emit `[substack-import:warn] no-engagement-file` once and proceed with an empty aggregates map.
|
|
14
|
+
|
|
15
|
+
## Column tolerance
|
|
16
|
+
|
|
17
|
+
Substack has used at least three column-naming schemes across export versions. Probe for each value in the order listed; first match wins:
|
|
18
|
+
|
|
19
|
+
| Aggregate field | Candidate column names |
|
|
20
|
+
|---|---|
|
|
21
|
+
| subscriber email | `email`, `subscriber_email`, `user_email` |
|
|
22
|
+
| event type | `event`, `event_type`, `action` |
|
|
23
|
+
| event timestamp | `event_at`, `timestamp`, `time` |
|
|
24
|
+
| post identifier (optional, for cross-check) | `post_id`, `email_id` |
|
|
25
|
+
|
|
26
|
+
If the email or event-type column cannot be resolved, emit `[substack-import:error] file=<engagementFile> reason="cannot resolve email or event-type column"` and proceed with empty aggregates — do not abort the whole import.
|
|
27
|
+
|
|
28
|
+
## Aggregation
|
|
29
|
+
|
|
30
|
+
Walk the file once, accumulating per email:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
emailToAggregates: Map<email-lowercased, {
|
|
34
|
+
totalOpens: int, // count of rows where event matches /^open$/i
|
|
35
|
+
totalClicks: int, // count of rows where event matches /^click$/i
|
|
36
|
+
lastOpenedAt: ISO|null, // max timestamp where event matched open
|
|
37
|
+
lastClickedAt: ISO|null, // max timestamp where event matched click
|
|
38
|
+
engagementWindowDays: int // (now - earliest event timestamp for this email) in days, ceil
|
|
39
|
+
}>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Event types other than open / click are tallied separately and reported in the completion log as `otherEvents=<n>` if non-zero, but are not folded onto the edge (no `:MENTIONS.totalForwards` etc. — would inflate the property set without a concrete query motivating it).
|
|
43
|
+
|
|
44
|
+
## Cardinality bound
|
|
45
|
+
|
|
46
|
+
A worst-case substack archive: 2000 subscribers × 500 posts × 2 event types = 2,000,000 events. Reading this twice (once to count, once to aggregate) wastes I/O; the aggregation pass runs streaming with a single map allocation. Map size is bounded by distinct emails (≤ subscriber count), not by event count — so the in-memory cost is O(subscribers), not O(events).
|
|
47
|
+
|
|
48
|
+
If the events file exceeds 500 MB, emit `[substack-import:warn] engagement-file-large size=<bytes>` so the operator knows the parse pass will take a few minutes; proceed anyway.
|
|
49
|
+
|
|
50
|
+
## Re-import semantics
|
|
51
|
+
|
|
52
|
+
Each Substack export carries totals-to-date, not deltas. On re-import:
|
|
53
|
+
|
|
54
|
+
- The aggregation walk recomputes the per-email totals from the new file.
|
|
55
|
+
- The per-subscriber MERGE's unconditional `SET` block overwrites engagement properties on the edge.
|
|
56
|
+
- Subscriber 1 with 3 opens in import A and 5 opens in import B: edge ends with `totalOpens=5` (the latest export's total), not 8.
|
|
57
|
+
- A subscriber who appears in import A's events file but not import B's: their edge's engagement properties are set to `null` in import B (Neo4j removes them), so the edge stops showing engagement. This is correct — if Substack's export omits a subscriber, they have no recorded engagement and the edge should reflect that.
|
|
58
|
+
|
|
59
|
+
## Why not per-event KDs
|
|
60
|
+
|
|
61
|
+
Per-event detail (which specific day of which specific open) is high-volume and low-query-value at v1. The aggregates answer the "warm vs cold" question that the operator actually asks. If a downstream feature needs per-event detail (e.g., "show me a timeline of when subscriber X engaged with my posts"), it lands as a separate task — likely as `:KnowledgeDocument {kind:'substack-email-event'}` children of the per-post KD with `:MENTIONS` to the `:Person`. That schema choice is deferred until a concrete query motivates it.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Post body handling
|
|
2
|
+
|
|
3
|
+
Per-post writes route through `librarian` / `document-ingest`. This reference documents how the body is prepared and what the dispatch brief carries.
|
|
4
|
+
|
|
5
|
+
## File pairing
|
|
6
|
+
|
|
7
|
+
For each row in `posts.csv`, the body file is at `posts/${post_id}.${slug}.html`. If the file is missing:
|
|
8
|
+
|
|
9
|
+
- If `is_published=true` and the post is `paid_only` and the operator's export does not include paid content: write the post KD anyway with `bodyTruncated=true` and an empty body (so the metadata + subscription-derivation work; the body lands when the operator re-exports with paid-content entitlement).
|
|
10
|
+
- Otherwise: emit `[substack-import:error] file=posts/<filename> reason="body file missing"` and skip the post.
|
|
11
|
+
|
|
12
|
+
## HTML → markdown conversion
|
|
13
|
+
|
|
14
|
+
Substack post bodies use a constrained HTML subset. Convert before dispatch so `document-ingest`'s in-turn classification sees clean markdown.
|
|
15
|
+
|
|
16
|
+
Block-level elements:
|
|
17
|
+
|
|
18
|
+
| HTML | Markdown |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `<h1>` … `<h6>` | `#` … `######` |
|
|
21
|
+
| `<p>` | paragraph |
|
|
22
|
+
| `<blockquote>` | `> ` prefix per line |
|
|
23
|
+
| `<ul>` / `<ol>` | `-` / `1.` per item |
|
|
24
|
+
| `<pre><code>` | fenced ``` block |
|
|
25
|
+
| `<hr>` | `---` |
|
|
26
|
+
|
|
27
|
+
Inline elements:
|
|
28
|
+
|
|
29
|
+
| HTML | Markdown |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `<a href="…">text</a>` | `[text](href)` |
|
|
32
|
+
| `<strong>` / `<b>` | `**text**` |
|
|
33
|
+
| `<em>` / `<i>` | `*text*` |
|
|
34
|
+
| `<code>` | `` `text` `` |
|
|
35
|
+
| `<img src="…" alt="…">` | `` — `src` is preserved verbatim for the attachment walk to resolve |
|
|
36
|
+
|
|
37
|
+
Substack-specific embeds (`<div class="subscription-widget">`, paywall stubs, audio player blocks): replace with a single placeholder line `[Substack embed: <type>]` and continue. The embed-detail-loss is documented; full embed preservation would require re-rendering the player on read which is out of scope.
|
|
38
|
+
|
|
39
|
+
## Paywall stub detection
|
|
40
|
+
|
|
41
|
+
A paywall stub is a body where:
|
|
42
|
+
|
|
43
|
+
- `audience` (from `posts.csv`) is `only_paid` or `founding`, AND
|
|
44
|
+
- the converted-markdown body length is < 200 characters, AND
|
|
45
|
+
- the body contains a recognisable paywall marker (e.g., "This post is for paid subscribers", `<div class="paywall">`, etc.).
|
|
46
|
+
|
|
47
|
+
When detected, set `bodyTruncated=true` on the dispatch brief's `extraProperties`. The KD still writes; downstream "this post body is incomplete" handling can branch on the property.
|
|
48
|
+
|
|
49
|
+
## Inline `@person` mentions
|
|
50
|
+
|
|
51
|
+
Substack post bodies occasionally reference `@handle`-style mentions. `document-ingest`'s in-turn typed-section classification handles these — the dispatch brief does not pre-resolve them. Per `archive-ingest-operator.md`'s account-isolation rule, mentions resolve against `:Person {accountId}` only; no auto-create from mention text.
|
|
52
|
+
|
|
53
|
+
## Dispatch brief shape (the Task-tool input the operator builds)
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Anchor: <importerNodeId> (:AdminUser or :Person)
|
|
57
|
+
attachmentId: "substack-post-<post_id>"
|
|
58
|
+
storagePath: {accountDir}/archive/substack/<importId>/posts/<post_id>.<slug>.html
|
|
59
|
+
mimeType: text/html
|
|
60
|
+
sourceUrl: https://<publication-domain>/p/<slug> (omit if unknown)
|
|
61
|
+
sourceType: 'substack'
|
|
62
|
+
scope: 'admin'
|
|
63
|
+
extraProperties:
|
|
64
|
+
kind: 'substack-post'
|
|
65
|
+
substackPostId: <post_id>
|
|
66
|
+
slug: <slug>
|
|
67
|
+
publishedAt: <ISO 8601>
|
|
68
|
+
isPaid: <bool>
|
|
69
|
+
publicationName: $publicationName
|
|
70
|
+
bodyTruncated: <bool> (only when paywall-stub)
|
|
71
|
+
importId: $importId
|
|
72
|
+
createdByAgent: 'substack-import'
|
|
73
|
+
createdBySession: $sessionId
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The librarian dispatched against this brief writes the `:KnowledgeDocument` and classified `:Section` children, then returns `{ elementId, outcome: 'created' | 'updated' | 'skipped' }` which feeds the per-pass counter.
|
|
77
|
+
|
|
78
|
+
## Storage path
|
|
79
|
+
|
|
80
|
+
Before dispatch, copy `posts/${post_id}.${slug}.html` from the operator's export directory to `{accountDir}/archive/substack/<importId>/posts/${post_id}.${slug}.html` (perms 0o600). This is the stable on-disk location the KD's `storagePath` property references — the operator may delete the export directory after import; the archived copy survives.
|
package/payload/platform/plugins/substack-import/skills/substack-import/references/subscribers.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Subscriber roster handling
|
|
2
|
+
|
|
3
|
+
The subscribers pass writes one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` and one `:Person` per row, with a `:MENTIONS` edge between them carrying subscription tier and engagement aggregates.
|
|
4
|
+
|
|
5
|
+
## Per-row parse
|
|
6
|
+
|
|
7
|
+
For each row in `subscribers.csv` (after bulk-gate filter):
|
|
8
|
+
|
|
9
|
+
1. **Email — required.** Lowercase. If missing or malformed, emit `[substack-import:error] file=subscribers.csv reason="missing email at row <n>"` and skip.
|
|
10
|
+
2. **Name — optional.**
|
|
11
|
+
- Split on first whitespace.
|
|
12
|
+
- First token → `givenName`.
|
|
13
|
+
- Remainder (if any) → `familyName`.
|
|
14
|
+
- Single-token names: `givenName=token, familyName=null`.
|
|
15
|
+
- Empty name: both null. The `:Person` writes with email only.
|
|
16
|
+
3. **Tier — required.** Read from `type` / `membership_state` / `subscription_state` (first present). Pass through verbatim — `free`, `paid`, `comp`, `gift`, anything else. Validation is downstream's responsibility.
|
|
17
|
+
4. **Subscribed-at — required.** Read from `created_at` / `subscribed_at`. ISO 8601.
|
|
18
|
+
5. **Unsubscribed-at — optional.** Present only on unsubscribed rows. ISO 8601.
|
|
19
|
+
|
|
20
|
+
## Per-row write
|
|
21
|
+
|
|
22
|
+
The MERGE shape (one `memory-write` Cypher call per subscriber row):
|
|
23
|
+
|
|
24
|
+
```cypher
|
|
25
|
+
MERGE (p:Person {accountId: $accountId, email: $email})
|
|
26
|
+
ON CREATE SET p.givenName = $givenName,
|
|
27
|
+
p.familyName = $familyName,
|
|
28
|
+
p.source = 'substack',
|
|
29
|
+
p.createdByAgent = 'substack-import',
|
|
30
|
+
p.createdBySession = $sessionId,
|
|
31
|
+
p.createdAt = datetime(),
|
|
32
|
+
p.importId = $importId
|
|
33
|
+
WITH p
|
|
34
|
+
MATCH (r:KnowledgeDocument) WHERE elementId(r) = $rosterNodeId
|
|
35
|
+
MERGE (r)-[m:MENTIONS {mentionContext: 'substack-subscription'}]->(p)
|
|
36
|
+
ON CREATE SET m.tier = $tier,
|
|
37
|
+
m.subscribedAt = $subscribedAt,
|
|
38
|
+
m.createdByAgent = 'substack-import',
|
|
39
|
+
m.createdAt = datetime()
|
|
40
|
+
SET m.unsubscribedAt = $unsubscribedAt,
|
|
41
|
+
m.totalOpens = $totalOpens,
|
|
42
|
+
m.totalClicks = $totalClicks,
|
|
43
|
+
m.lastOpenedAt = $lastOpenedAt,
|
|
44
|
+
m.lastClickedAt = $lastClickedAt,
|
|
45
|
+
m.engagementWindowDays = $engagementWindowDays
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Notes:
|
|
49
|
+
|
|
50
|
+
- The `ON CREATE` block on `:Person` sets `givenName` / `familyName` only on first write. Existing `:Person` nodes (e.g., a contact already imported from LinkedIn) keep their operator-set names; the subscription edge still attaches.
|
|
51
|
+
- The `ON CREATE` block on `:MENTIONS` sets `tier` and `subscribedAt` only once (the original subscription). Subsequent imports overwriting tier (e.g., free → paid upgrade) need `SET m.tier = $tier` explicitly — see "tier updates on re-import" below.
|
|
52
|
+
- The unconditional `SET` block at the bottom overwrites engagement properties and `unsubscribedAt`. The export carries totals-to-date, not deltas — overwrite is correct.
|
|
53
|
+
- Engagement properties from `$totalOpens` etc. set to Cypher `null` (because the events file lacked an entry for this email) remove the properties from the edge; Neo4j does not distinguish "property=null" from "property absent". This is the intended semantic — absent engagement is absent, not zero.
|
|
54
|
+
|
|
55
|
+
## Tier updates on re-import
|
|
56
|
+
|
|
57
|
+
Substack tier can change (free → paid upgrade, paid → comp, etc.). The current MERGE keeps the original `tier` from the first import. To overwrite on re-import, the writer adds `SET m.tier = $tier` to the unconditional `SET` block. This sacrifices "first-subscribed-as" auditability — if the operator needs that, the original tier can be queried from the per-import roster KD's `createdAt` lineage.
|
|
58
|
+
|
|
59
|
+
For v1, the unconditional `SET m.tier = $tier` is the right call (engagement-warm signal depends on current tier, not historical). The `subscribedAt` stays `ON CREATE` because that is the immutable first-subscription timestamp.
|
|
60
|
+
|
|
61
|
+
## Anchor-edge from the importing operator
|
|
62
|
+
|
|
63
|
+
After the per-row loop, write one `:IMPORTED` edge from the importing operator to the roster KD:
|
|
64
|
+
|
|
65
|
+
```cypher
|
|
66
|
+
MATCH (a) WHERE elementId(a) = $importerNodeId
|
|
67
|
+
MATCH (r:KnowledgeDocument) WHERE elementId(r) = $rosterNodeId
|
|
68
|
+
MERGE (a)-[i:IMPORTED]->(r)
|
|
69
|
+
ON CREATE SET i.importId = $importId,
|
|
70
|
+
i.source = 'substack',
|
|
71
|
+
i.createdAt = datetime()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This edge is the provenance trail: "who pushed the button for this subscription roster."
|
|
@@ -79,7 +79,7 @@ Four tools for reading WhatsApp conversation context. The first two read from th
|
|
|
79
79
|
|------|---------|
|
|
80
80
|
| `whatsapp-conversations` | JIDs with recent message activity, conversation type (group/DM), last message timestamp, message count |
|
|
81
81
|
| `whatsapp-messages` | In-memory ring-buffer messages for a specific JID — sender, body, timestamp, reply context. Supports optional `limit` for most recent N messages |
|
|
82
|
-
| `whatsapp-conversation-graph-state` | Persisted `:Message:WhatsAppMessage` rows attached to the `:Conversation` (sessionKey-keyed) plus the resolved `
|
|
82
|
+
| `whatsapp-conversation-graph-state` | Persisted `:Message:WhatsAppMessage` rows attached to the `:Conversation` (sessionKey-keyed) plus the resolved `sessionId`, the cypher string, and a divergence header reporting graph-row count vs in-memory count. Use when the question is "what got persisted" — never compose the cypher manually |
|
|
83
83
|
| `whatsapp-group-info` | Group metadata from WhatsApp servers — subject, description, participants with admin flags, creation date. Requires active connection |
|
|
84
84
|
|
|
85
85
|
## Group access — three-way mention recognition
|
|
@@ -379,7 +379,7 @@ server.tool("whatsapp-messages", "Retrieve stored WhatsApp messages for a specif
|
|
|
379
379
|
// graph-row vs in-memory-store count drift as an operator-visible signal.
|
|
380
380
|
server.tool("whatsapp-conversation-graph-state", [
|
|
381
381
|
"Read persisted graph state for a WhatsApp conversation deterministically.",
|
|
382
|
-
"Returns the conversation's `
|
|
382
|
+
"Returns the conversation's `sessionId` and every `:Message:WhatsAppMessage` row attached via `(:PART_OF)->`, plus the in-memory store's row count for divergence visibility.",
|
|
383
383
|
"Use this whenever the question is 'what messages got persisted to <jid> / <sessionKey>?'. Do not compose cypher by hand — the cypher in the platform uses the writer-known filter (sessionKey on the Conversation node, not jid match on the Message node).",
|
|
384
384
|
"Input: pass `jid` (e.g. group `120363...@g.us` or DM `44...@s.whatsapp.net`) — the platform derives the public-agent sessionKey. For admin-agent sessionKeys, pass `sessionKey` directly.",
|
|
385
385
|
].join("\n"), {
|
|
@@ -417,7 +417,7 @@ server.tool("whatsapp-conversation-graph-state", [
|
|
|
417
417
|
const rows = result.graphRows ?? [];
|
|
418
418
|
const headerParts = [
|
|
419
419
|
`sessionKey=${result.sessionKey}`,
|
|
420
|
-
`
|
|
420
|
+
`sessionId=${result.sessionId ?? "null"}`,
|
|
421
421
|
`graphRows=${rows.length}`,
|
|
422
422
|
];
|
|
423
423
|
if (memoryCount !== null)
|