@rubytech/create-maxy-code 0.1.159 → 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.
Files changed (146) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +48 -17
  3. package/payload/platform/neo4j/schema.cypher +59 -36
  4. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  5. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
  6. package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +1 -1
  7. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -2
  8. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  9. package/payload/platform/plugins/email/PLUGIN.md +5 -1
  10. package/payload/platform/plugins/email/mcp/dist/index.js +25 -0
  11. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  12. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +8 -0
  13. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -0
  14. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +177 -0
  15. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -0
  16. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +6 -54
  17. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
  18. package/payload/platform/plugins/email/mcp/dist/lib/graph.js +12 -300
  19. package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
  20. package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts +6 -0
  21. package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts.map +1 -0
  22. package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js +89 -0
  23. package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js.map +1 -0
  24. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
  25. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +11 -6
  26. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
  27. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +10 -37
  28. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +1 -0
  29. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  30. package/payload/platform/plugins/memory/PLUGIN.md +10 -1
  31. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +139 -439
  32. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +15 -47
  33. package/payload/platform/plugins/memory/mcp/dist/index.js +168 -29
  34. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +43 -27
  36. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts +2 -0
  38. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts.map +1 -0
  39. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js +62 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js.map +1 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +5 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
  43. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js.map +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +42 -25
  46. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +23 -17
  48. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts +3 -0
  50. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts.map +1 -0
  51. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js +61 -0
  52. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js.map +1 -0
  53. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
  54. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
  55. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
  56. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
  57. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
  58. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
  59. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
  61. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts +2 -0
  62. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts.map +1 -1
  63. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js +9 -3
  64. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js.map +1 -1
  65. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts +41 -0
  66. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -0
  67. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +71 -0
  68. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -0
  69. package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts +18 -0
  70. package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts.map +1 -0
  71. package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js +31 -0
  72. package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js.map +1 -0
  73. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts +5 -0
  74. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +162 -78
  76. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
  77. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js +1 -1
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js.map +1 -1
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js +2 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js.map +1 -1
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +2 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -1
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js +6 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js.map +1 -1
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts +2 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js +141 -0
  88. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js.map +1 -0
  89. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts +2 -0
  90. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts.map +1 -0
  91. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js +86 -0
  92. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js.map +1 -0
  93. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +33 -34
  94. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
  95. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +94 -273
  96. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  97. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts +43 -0
  98. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts.map +1 -0
  99. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js +96 -0
  100. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js.map +1 -0
  101. package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts +6 -0
  102. package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts.map +1 -0
  103. package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js +73 -0
  104. package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js.map +1 -0
  105. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  106. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +0 -1
  107. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  108. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +6 -0
  109. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -1
  110. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +2 -0
  111. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -1
  112. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +8 -0
  113. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
  114. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +40 -5
  115. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
  116. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +7 -0
  117. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
  118. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +17 -1
  119. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  120. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +11 -0
  121. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  122. package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts +28 -0
  123. package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts.map +1 -0
  124. package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js +46 -0
  125. package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js.map +1 -0
  126. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  127. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +26 -0
  128. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  129. package/payload/platform/plugins/memory/mcp/vitest.config.ts +5 -4
  130. package/payload/platform/plugins/memory/references/schema-base.md +4 -6
  131. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +100 -116
  132. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +74 -24
  133. package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +1 -1
  134. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +1 -1
  135. package/payload/platform/plugins/substack-import/.claude-plugin/plugin.json +8 -0
  136. package/payload/platform/plugins/substack-import/PLUGIN.md +34 -0
  137. package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +182 -0
  138. package/payload/platform/plugins/substack-import/skills/substack-import/references/archive-shape.md +68 -0
  139. package/payload/platform/plugins/substack-import/skills/substack-import/references/attachments.md +72 -0
  140. package/payload/platform/plugins/substack-import/skills/substack-import/references/engagement.md +61 -0
  141. package/payload/platform/plugins/substack-import/skills/substack-import/references/posts.md +80 -0
  142. package/payload/platform/plugins/substack-import/skills/substack-import/references/subscribers.md +74 -0
  143. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  144. package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +45 -0
  145. package/payload/platform/templates/specialists/agents/database-operator.md +5 -17
  146. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
@@ -1,43 +1,29 @@
1
1
  #!/usr/bin/env bash
2
2
  # =============================================================================
3
- # conversation-archive-ingest.sh — single deterministic Bash entry for
4
- # conversation-archive ingestion
5
- # env-only identity, progress-file path on stdout).
3
+ # conversation-archive-ingest.sh — Bash wrapper around the
4
+ # conversation-archive prepare helper (Task 435).
6
5
  #
7
- # Source-agnostic: WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom
8
- # transcript, meeting minutes, iMessage, Slack every source flows through
9
- # the same pipeline with `--source` selecting the normaliser.
10
- #
11
- # Pipeline: normalise (per source) bind canonical sender set
12
- # derive conversationIdentity look up prior :ConversationArchive (delta
13
- # cursor) → sessionize delta at gap-hours boundary → for each session: classify
14
- # via Haiku (mode='chat') + memory-ingest immediately (per-session checkpoint).
6
+ # Post-Task-435 contract: the wrapper validates env (ACCOUNT_ID, USER_ID,
7
+ # NEO4J_URI, NEO4J_PASSWORD) and argv, then exec's the .mjs which runs the
8
+ # deterministic pipeline (normalise verify participants → derive
9
+ # conversationIdentity → look up prior archive cursor → slice delta →
10
+ # sessionize emit JSON). Classify + memory-ingest move into the dispatched
11
+ # specialist's per-turn work (driven by SKILL.md).
15
12
  #
16
13
  # Usage:
17
14
  # bash conversation-archive-ingest.sh <archive-path>
18
- # --source <whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|other>
15
+ # --source <whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|x-dm|other>
19
16
  # --participant-person-ids <csv>
20
17
  # --scope <admin|public>
21
- # [--rebuild] (opt-in destructive rebuild)
18
+ # [--rebuild]
22
19
  # [--timezone <iana-zone>]
23
- # [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>] (whatsapp only)
24
- # [--session-id <id>]
25
- #
26
- # Sessions split deterministically at 8h gap (fixed code constant).
27
- # `--session-gap-hours` is REMOVED; passing it FAILs at phase=argv.
20
+ # [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>]
28
21
  #
29
- # Identity: `ACCOUNT_ID` + `USER_ID` come from the caller's env
30
- # (plumbed by `spawn-env.ts` into every Bash subprocess). The owner :AdminUser
31
- # elementId is derived from (accountId, userId) inside the bin via Cypher.
32
- # `--account-id` and `--owner-element-id` flags are GONE — env-only identity
33
- # is doctrine. `--participant-person-ids` carries the operator-confirmed
34
- # non-owner set.
22
+ # Identity: ACCOUNT_ID + USER_ID come from the caller's env (plumbed by
23
+ # spawn-env.ts). Owner :AdminUser elementId is derived inside the .mjs.
35
24
  #
36
- # Exit 0 + JSON summary on stdout on success.
25
+ # Exit 0 + one JSON line on stdout on success.
37
26
  # Exit !0 + one [conversation-archive] FAIL line on stderr on failure.
38
- # Stdout (always, before exec node): `[conversation-archive] progress-file=<absolute-path>`
39
- # Mid-run progress: line-by-line writes to that progress-file (fsync per line);
40
- # the agent's heartbeat polls it via `tail -n <N> <path>` every 30 s.
41
27
  # =============================================================================
42
28
 
43
29
  set -euo pipefail
@@ -56,10 +42,6 @@ if [ ! -f "$INGEST_MJS" ]; then
56
42
  arg_fail "conversation-archive-ingest.mjs not found at $INGEST_MJS — run from a built install"
57
43
  fi
58
44
 
59
- # Identity is env-only. The mjs LOUD-FAILs on missing/malformed
60
- # values, but we mirror the cheap accountId presence check here so the
61
- # progress-file path can be computed and emitted before exec node — without
62
- # accountId, the path is undefined.
63
45
  if [ -z "${ACCOUNT_ID:-}" ]; then
64
46
  arg_fail "ACCOUNT_ID env missing — bin must be invoked under an authenticated agent context (set by spawn-env.ts)"
65
47
  fi
@@ -74,7 +56,6 @@ HAS_PARTICIPANTS=0
74
56
  PARTICIPANTS_VAL=""
75
57
  HAS_SCOPE=0
76
58
  SCOPE_VAL=""
77
- SESSION_ID_VAL=""
78
59
 
79
60
  ARGS=("$@")
80
61
  i=0
@@ -84,7 +65,6 @@ while [ $i -lt ${#ARGS[@]} ]; do
84
65
  --source) HAS_SOURCE=1; SOURCE_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
85
66
  --participant-person-ids) HAS_PARTICIPANTS=1; PARTICIPANTS_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
86
67
  --scope) HAS_SCOPE=1; SCOPE_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
87
- --session-id) SESSION_ID_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
88
68
  --rebuild) i=$((i + 1)); continue ;;
89
69
  --session-gap-hours) arg_fail "--session-gap-hours is no longer supported; gap is fixed at 8h. Use --rebuild on the prior export bytes if you need to re-classify." ;;
90
70
  --timezone|--date-format) i=$((i + 2)); continue ;;
@@ -98,7 +78,7 @@ while [ $i -lt ${#ARGS[@]} ]; do
98
78
  done
99
79
 
100
80
  [ -n "$ARCHIVE" ] || arg_fail "missing positional <archive>"
101
- [ "$HAS_SOURCE" -eq 1 ] && [ -n "$SOURCE_VAL" ] || arg_fail "missing --source (whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|other)"
81
+ [ "$HAS_SOURCE" -eq 1 ] && [ -n "$SOURCE_VAL" ] || arg_fail "missing --source (whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|x-dm|other)"
102
82
  [ "$HAS_PARTICIPANTS" -eq 1 ] && [ -n "$PARTICIPANTS_VAL" ] || arg_fail "missing --participant-person-ids (csv of operator-confirmed :Person/:AdminUser elementIds, owner excluded)"
103
83
  [ "$HAS_SCOPE" -eq 1 ] && [ -n "$SCOPE_VAL" ] || arg_fail "missing --scope (or empty value)"
104
84
  case "$SCOPE_VAL" in
@@ -123,16 +103,4 @@ fi
123
103
  export NEO4J_USER="${NEO4J_USER:-neo4j}"
124
104
  export MAXY_PLATFORM_ROOT="$PLATFORM_ROOT"
125
105
 
126
- # Progress-file path must match the path the mjs opens (deterministic from
127
- # (ACCOUNT_ID, sessionId)). When the operator omits --session-id, the mjs
128
- # mints one — the wrapper passes through the same value via env so both
129
- # sides converge on one path.
130
- if [ -z "$SESSION_ID_VAL" ]; then
131
- SESSION_ID_VAL="conversation-archive:${SOURCE_VAL}:$(date +%s%N)"
132
- ARGS+=(--session-id "$SESSION_ID_VAL")
133
- fi
134
- INSTALL_DIR="$(cd "$PLATFORM_ROOT/.." && pwd)"
135
- PROGRESS_FILE="$INSTALL_DIR/data/accounts/$ACCOUNT_ID/logs/conversation-archive-$SESSION_ID_VAL.log"
136
- echo "[conversation-archive] progress-file=$PROGRESS_FILE"
137
-
138
106
  exec node "$INGEST_MJS" "${ARGS[@]}"
@@ -12,6 +12,8 @@ import { buildLiveSchemaSource, defaultSchemaCypherPath, } from "./lib/live-sche
12
12
  import { memoryReindex } from "./tools/memory-reindex.js";
13
13
  import { memoryIngestExtract } from "./tools/memory-ingest-extract.js";
14
14
  import { memoryIngest } from "./tools/memory-ingest.js";
15
+ import { CONVERSATION_SOURCES } from "./lib/conversation-normalisers/types.js";
16
+ import { kdClassify } from "./tools/kd-classify.js";
15
17
  import { memoryArchiveWrite } from "./tools/memory-archive-write.js";
16
18
  import { obsidianVaultImport, } from "./tools/obsidian-vault-import.js";
17
19
  // whatsapp-export-{parse,preview,insight-write} deleted. Conversation
@@ -25,6 +27,7 @@ import { memoryIngestWeb } from "./tools/memory-ingest-web.js";
25
27
  import { memoryUpdate } from "./tools/memory-update.js";
26
28
  import { memoryDelete } from "./tools/memory-delete.js";
27
29
  import { memoryFindCandidates } from "./tools/memory-find-candidates.js";
30
+ import { memoryNodeExists, ALLOWED_LABELS as MEMORY_NODE_EXISTS_LABELS } from "./tools/memory-node-exists.js";
28
31
  import { memoryRestore } from "./tools/memory-restore.js";
29
32
  import { memoryEmptyTrash } from "./tools/memory-empty-trash.js";
30
33
  import { memoryListAttachments } from "./tools/memory-list-attachments.js";
@@ -39,7 +42,8 @@ import { graphPruneDenylistAdd } from "./tools/graph-prune-denylist-add.js";
39
42
  import { graphPruneDenylistList } from "./tools/graph-prune-denylist-list.js";
40
43
  import { graphPruneDenylistRemove } from "./tools/graph-prune-denylist-remove.js";
41
44
  import { conversationMemoryExpunge } from "./tools/conversation-memory-expunge.js";
42
- import { conversationArchiveDeriveInsights, } from "./tools/conversation-archive-derive-insights.js";
45
+ import { conversationArchiveDeriveInsights, PROPOSAL_KINDS, } from "./tools/conversation-archive-derive-insights.js";
46
+ import { conversationArchiveListChunks } from "./tools/conversation-archive-list-chunks.js";
43
47
  import { conversationArchiveEnrichRejection } from "./tools/conversation-archive-enrich-rejection.js";
44
48
  import { fetchImageAsContent } from "./tools/image-fetch.js";
45
49
  import { memoryTypedEdgePass } from "./tools/memory-typed-edge-pass.js";
@@ -295,16 +299,12 @@ eagerTool(server, "memory-search", "Search the knowledge graph using semantic ve
295
299
  }
296
300
  });
297
301
  // ---------------------------------------------------------------------------
298
- // conversation-archive Phase 2: chunk-anchored insight derivation.
299
- // Read-only tool. Walks the :Section chunks of one named conversation
300
- // archive (`:ConversationArchive` keyed on `conversationIdentity` — Task
301
- // 397) in pages and asks Haiku (via OAuth) for the high-confidence claims.
302
- // Returns operator-facing proposals with the cypher needed to wire them;
303
- // the conversation-archive-enrich skill drives the per-row operator gate.
304
- // The tool itself NEVER writes; idempotency lives on the MERGE-key shape
305
- // (chunkElementId, kind, contentHash) encoded in each proposal's mergeCypher.
302
+ // conversation-archive Phase 2 chunk page reader (Task 433).
303
+ // Returns one page of :Section chunk bodies for the named :ConversationArchive
304
+ // so the conversation-archive-enrich skill can drive the dispatched
305
+ // specialist to read each chunk and emit claims in-turn. Read-only.
306
306
  // ---------------------------------------------------------------------------
307
- server.tool("conversation-archive-derive-insights", "Walk the :Section chunks of one named :ConversationArchive (conversation-archive walker) and emit high-confidence claim proposals (mention, task, preference, observed-relationship). Read-only: the caller drives a per-row operator gate; this tool never writes. Paginates via chunkOffset + chunkLimit (default 5). Operator-triggered, never automatic.", {
307
+ server.tool("conversation-archive-list-chunks", "Page through the :Section chunks of one named :ConversationArchive and return each chunk's elementId, ordered index, summary, and full body. The conversation-archive-enrich skill calls this per page, then prompts the dispatched specialist to read each chunk's body in-turn and emit claims; the skill hands those claims back to `conversation-archive-derive-insights` for per-kind cypher emission. Read-only.", {
308
308
  archiveElementId: z
309
309
  .string()
310
310
  .describe("elementId of the :ConversationArchive parent (Task 397 — keyed on conversationIdentity) the operator named (required)."),
@@ -313,23 +313,100 @@ server.tool("conversation-archive-derive-insights", "Walk the :Section chunks of
313
313
  .int()
314
314
  .nonnegative()
315
315
  .optional()
316
- .describe("Zero-based chunk offset. Default 0. Increase per page."),
316
+ .describe("Zero-based chunk offset. Default 0. Advance by chunkLimit per page."),
317
317
  chunkLimit: z
318
318
  .number()
319
319
  .int()
320
320
  .positive()
321
321
  .max(20)
322
322
  .optional()
323
- .describe("Chunks per page (1..20). Default 5 — small pages keep MCP calls under SDK timeout windows."),
323
+ .describe("Chunks per page (1..20). Default 5."),
324
324
  }, async ({ archiveElementId, chunkOffset, chunkLimit }) => {
325
325
  if (!accountId)
326
- return refuseNoAccount("conversation-archive-derive-insights");
326
+ return refuseNoAccount("conversation-archive-list-chunks");
327
327
  try {
328
- const result = await conversationArchiveDeriveInsights({
328
+ const result = await conversationArchiveListChunks({
329
329
  accountId,
330
330
  archiveElementId,
331
331
  chunkOffset,
332
332
  chunkLimit,
333
+ });
334
+ const header = `archiveElementId=${result.archiveElementId} title=${JSON.stringify(result.archiveTitle ?? "")} totalChunks=${result.totalChunks} walked=${result.walkedFrom}..${result.walkedTo} chunks=${result.chunks.length} chunksRemaining=${result.chunksRemaining}`;
335
+ const chunkBlocks = result.chunks
336
+ .map((c) => [
337
+ `Chunk ${c.index + 1}/${result.totalChunks}:`,
338
+ ` elementId=${c.elementId}`,
339
+ ` summary=${JSON.stringify(c.summary)}`,
340
+ ` body=${JSON.stringify(c.body)}`,
341
+ ].join("\n"))
342
+ .join("\n\n");
343
+ return {
344
+ content: [
345
+ {
346
+ type: "text",
347
+ text: result.chunks.length === 0
348
+ ? `${header}\n(no chunks at this offset)`
349
+ : `${header}\n\n${chunkBlocks}`,
350
+ },
351
+ ],
352
+ };
353
+ }
354
+ catch (err) {
355
+ const msg = err instanceof Error ? err.message : String(err);
356
+ return {
357
+ content: [{ type: "text", text: msg }],
358
+ isError: true,
359
+ };
360
+ }
361
+ });
362
+ process.stderr.write("[memory-mcp] registered tool=conversation-archive-list-chunks\n");
363
+ // ---------------------------------------------------------------------------
364
+ // conversation-archive Phase 2: per-chunk claim → cypher proposal emitter.
365
+ // Task 433: accepts the agent-produced `claims` array for one chunk (read
366
+ // from `conversation-archive-list-chunks`) and returns operator-facing
367
+ // proposals carrying the per-kind mergeCypher. Rejection sidecar filter is
368
+ // applied here. The tool itself NEVER writes; idempotency lives on the
369
+ // MERGE-key (chunkElementId, kind, contentHash) baked into each mergeCypher.
370
+ // ---------------------------------------------------------------------------
371
+ const claimSchema = z.object({
372
+ kind: z.enum([...PROPOSAL_KINDS]).describe("One of: mention, task, preference, observed-relationship."),
373
+ evidenceSnippet: z
374
+ .string()
375
+ .describe("Near-verbatim quote from the chunk body (≤80 chars enforced server-side)."),
376
+ subject: z
377
+ .string()
378
+ .nullable()
379
+ .optional()
380
+ .describe("Display name of the primary entity. Required for mention + observed-relationship."),
381
+ object: z
382
+ .string()
383
+ .nullable()
384
+ .optional()
385
+ .describe("Display name of the related entity. Required for observed-relationship only."),
386
+ payload: z
387
+ .record(z.string(), z.unknown())
388
+ .optional()
389
+ .describe("Kind-specific structured fields (taskTitle/taskDueHint for task; preferenceCategory/preferenceKey/preferenceValue for preference; relationshipType for observed-relationship)."),
390
+ });
391
+ server.tool("conversation-archive-derive-insights", "Convert one chunk's agent-produced claims into operator-facing claim proposals (mention, task, preference, observed-relationship). The conversation-archive-enrich skill reads chunk bodies via `conversation-archive-list-chunks`, drives the dispatched specialist to emit claims in-turn, and passes them here for per-kind cypher emission. Applies the durable rejection sidecar so already-triaged claims do not re-surface. Read-only: the per-row operator gate runs in the skill, this tool never writes.", {
392
+ archiveElementId: z
393
+ .string()
394
+ .describe("elementId of the :ConversationArchive parent (Task 397 — keyed on conversationIdentity) the operator named (required)."),
395
+ chunkElementId: z
396
+ .string()
397
+ .describe("elementId of the single :Section chunk the agent just read (required)."),
398
+ claims: z
399
+ .array(claimSchema)
400
+ .describe("Claims the dispatched specialist emitted in-turn from the chunk body. Zero is valid — pass [] to record an empty-chunk pass."),
401
+ }, async ({ archiveElementId, chunkElementId, claims }) => {
402
+ if (!accountId)
403
+ return refuseNoAccount("conversation-archive-derive-insights");
404
+ try {
405
+ const result = await conversationArchiveDeriveInsights({
406
+ accountId,
407
+ archiveElementId,
408
+ chunkElementId,
409
+ claims: claims,
333
410
  sessionId: resolveSessionId(),
334
411
  });
335
412
  const proposalLines = result.proposals
@@ -349,13 +426,13 @@ server.tool("conversation-archive-derive-insights", "Walk the :Section chunks of
349
426
  ].join("\n");
350
427
  })
351
428
  .join("\n\n");
352
- const header = `archiveElementId=${result.archiveElementId} title=${JSON.stringify(result.archiveTitle ?? "")} totalChunks=${result.totalChunks} walked=${result.walkedFrom}..${result.walkedTo} proposals=${result.proposals.length} proposalsRemaining=${result.proposalsRemaining} emptyChunks=${result.emptyChunkElementIds.length}`;
429
+ const header = `archiveElementId=${result.archiveElementId} chunkElementId=${result.chunkElementId} proposals=${result.proposals.length} rejectionsFiltered=${result.rejectionsFiltered} invalidClaims=${result.invalidClaims}`;
353
430
  return {
354
431
  content: [
355
432
  {
356
433
  type: "text",
357
434
  text: result.proposals.length === 0
358
- ? `${header}\n(no high-confidence claims in this page; advance chunkOffset to walk further)`
435
+ ? `${header}\n(no proposals from this chunk's claims)`
359
436
  : `${header}\n\n${proposalLines}`,
360
437
  },
361
438
  ],
@@ -888,20 +965,29 @@ if (!readOnly) {
888
965
  };
889
966
  }
890
967
  });
891
- server.tool("memory-ingest", "Write a classified document into the graph as typed nodes anchored to the document subject. " +
892
- "Requires a prior memory-ingest-extract call for the text cache; the typed-section JSON in the `sections` argument is produced by the dispatched specialist in-turn from the loaded ontology. " +
968
+ server.tool("memory-ingest", "Write a classified document or conversation-archive session into the graph as typed nodes anchored to a subject. " +
969
+ "Two mutually-exclusive parent paths exactly one MUST be set: " +
970
+ "(1) **Document path** — set `attachmentId` (requires a prior memory-ingest-extract call for the text cache). MERGEs under `:KnowledgeDocument` keyed on attachmentId. The typed-section JSON in `sections` is produced by the dispatched specialist in-turn from the loaded ontology. " +
971
+ "(2) **Conversation-archive path** — set `conversationIdentity` along with the conversation-document fields (`source`, `archiveSha256`, `archiveSourceFile`, `participantElementIds`, `lastIngestedMessageHash`, `lastIngestedMessageAt`, optionally `archiveTitle` and `cleanupPriorChunks`). MERGEs under `:ConversationArchive` keyed on conversationIdentity. The specialist invokes this once per session prepared by `conversation-archive-ingest.sh`, advancing the cursor atomically with each session's chunks. " +
893
972
  "Each section becomes either a typed graph node (Position, Service, Credential, etc.) anchored to the named " +
894
- "subject via the natural ontology edge plus a (KnowledgeDocument)-[:REFERENCES]->(typed) link, or — for " +
895
- "UNMAPPED sections — a generic :Section node hanging off the document via HAS_SECTION (legacy fallback). " +
896
- "Re-ingesting with the same attachmentId replaces the document's typed and untyped children safe to call " +
897
- "on already-ingested documents. Shared related entities (Organizations, Persons referenced by typed nodes) " +
973
+ "subject via the natural ontology edge plus a (parent)-[:REFERENCES]->(typed) link, or — for " +
974
+ "UNMAPPED sections — a generic :Section node hanging off the parent via HAS_SECTION (legacy fallback). " +
975
+ "Document re-ingest replaces the document's typed and untyped children (same attachmentId). Conversation-archive re-ingest is delta-append: prior chunks are never touched unless `cleanupPriorChunks=true` is set (only legitimate on the FIRST session of a `--rebuild` run). " +
976
+ "Shared related entities (Organizations, Persons referenced by typed nodes) " +
898
977
  "are MERGEd by identifying property and never deleted on re-ingest.", {
899
- attachmentId: z.string().describe("UUID of the file attachment — must match a prior memory-ingest-extract call"),
900
- documentSummary: z.string().describe("Classifier-produced summary of the entire document (1-3 sentences)"),
901
- anchorNodeId: z.string().describe("Element ID of the anchor node (UserProfile/LocalBusiness/Person/Organization). The document subject " +
902
- "the operator confirmed during the document-ingest skill's first step."),
903
- anchorLabel: z.string().describe("Primary label of the anchor node (e.g. 'UserProfile', 'LocalBusiness'). Used in the per-section MATCH " +
904
- "for anchor-edge creation."),
978
+ attachmentId: z.string().optional().describe("UUID of the file attachment — must match a prior memory-ingest-extract call. Document path only — mutually exclusive with conversationIdentity."),
979
+ conversationIdentity: z.string().optional().describe("sha256(accountId + ':' + sortedParticipantElementIds.join(',')) produced by `conversation-archive-ingest.sh`. Conversation-archive path only — mutually exclusive with attachmentId."),
980
+ source: z.enum(CONVERSATION_SOURCES).optional().describe("Conversation source enum. Required on the conversation-archive path."),
981
+ archiveSha256: z.string().optional().describe("sha256 of the export bytes. Required on the conversation-archive path; stamped on every :Section chunk so cleanup-by-archiveSha256 is exact."),
982
+ archiveSourceFile: z.string().optional().describe("Basename of the source file (e.g. `_chat.txt`). Required on the conversation-archive path."),
983
+ participantElementIds: z.array(z.string()).optional().describe("Owner + operator-confirmed participants (elementIds). Required on the conversation-archive path. Owner is included first; :PARTICIPANT_IN edges land for every entry. Order is irrelevant for conversationIdentity, which is derived from a sorted set."),
984
+ lastIngestedMessageHash: z.string().optional().describe("sha256 of the last message in THIS session. Required on the conversation-archive path; written to :ConversationArchive.lastIngestedMessageHash atomically with this session's chunks for delta-append on the next ingest."),
985
+ lastIngestedMessageAt: z.string().optional().describe("ISO timestamp of the last message in THIS session. Required on the conversation-archive path; written to :ConversationArchive.lastIngestedMessageAt."),
986
+ archiveTitle: z.string().optional().describe("Stable archive title (`<source> · <owner> ↔ <others> · <YYYY-MM-DD>→<YYYY-MM-DD>`) computed by `conversation-archive-ingest.sh`. Written ON CREATE and COALESCE-on-MATCH so the UI label is stable across re-ingests. Conversation-archive path only."),
987
+ cleanupPriorChunks: z.boolean().optional().describe("When true, the writer deletes prior chunks for the matching archiveSha256 before MERGE. Set this ONLY on the FIRST session of a `--rebuild` run; subsequent sessions in the same run MUST set it false or the writer drops chunks the earlier sessions just wrote. Defaults to false."),
988
+ documentSummary: z.string().describe("Classifier-produced summary (1-3 sentences) for the document path. On the conversation-archive path, pass the stable `archiveTitle` so the parent's summary matches the title."),
989
+ anchorNodeId: z.string().describe("Element ID of the anchor node. Document path: the operator-confirmed document subject (UserProfile/LocalBusiness/Person/Organization). Conversation-archive path: the owner :AdminUser elementId (or :Person for a third-party archive)."),
990
+ anchorLabel: z.string().describe("Primary label of the anchor node (e.g. 'UserProfile', 'LocalBusiness', 'AdminUser'). Used in the per-section MATCH for anchor-edge creation."),
905
991
  sections: z.array(z.object({
906
992
  kind: z.string().describe("Section kind from the closed enumeration (Position/Education/Chapter/Parties/.../Other), or a standalone-node kind like 'Project'."),
907
993
  title: z.string(),
@@ -945,13 +1031,22 @@ if (!readOnly) {
945
1031
  documentKeywords: z.array(z.string()).optional().describe("Classifier-extracted topic keywords."),
946
1032
  userKeywords: z.array(z.string()).optional().describe("Keywords explicitly provided by the user. Merged additively with documentKeywords."),
947
1033
  sessionId: z.string().optional().describe("Session UUID for provenance stamping. Optional override; the server falls back to the SESSION_ID env var set by the admin agent at MCP spawn. The agent only needs to pass this when calling outside the normal admin-session context."),
948
- }, async ({ attachmentId, documentSummary, anchorNodeId, anchorLabel, sections, documentEdges, orphanCandidates, scope, sourceUrl, sourceType, documentKeywords, userKeywords, sessionId: sessionIdOverride }) => {
1034
+ }, async ({ attachmentId, conversationIdentity, source, archiveSha256, archiveSourceFile, participantElementIds, lastIngestedMessageHash, lastIngestedMessageAt, archiveTitle, cleanupPriorChunks, documentSummary, anchorNodeId, anchorLabel, sections, documentEdges, orphanCandidates, scope, sourceUrl, sourceType, documentKeywords, userKeywords, sessionId: sessionIdOverride }) => {
949
1035
  if (!accountId)
950
1036
  return refuseNoAccount("memory-ingest");
951
1037
  try {
952
1038
  const result = await memoryIngest({
953
1039
  accountId,
954
1040
  attachmentId,
1041
+ conversationIdentity,
1042
+ source,
1043
+ archiveSha256,
1044
+ archiveSourceFile,
1045
+ participantElementIds,
1046
+ lastIngestedMessageHash,
1047
+ lastIngestedMessageAt,
1048
+ archiveTitle,
1049
+ cleanupPriorChunks,
955
1050
  documentSummary,
956
1051
  anchorNodeId,
957
1052
  anchorLabel,
@@ -978,6 +1073,7 @@ if (!readOnly) {
978
1073
  orphanCandidates: result.orphanCandidates,
979
1074
  documentSummary: result.documentSummary,
980
1075
  keywords: result.keywords,
1076
+ cleanedPriorChunks: result.cleanedPriorChunks,
981
1077
  }),
982
1078
  }],
983
1079
  };
@@ -992,6 +1088,25 @@ if (!readOnly) {
992
1088
  };
993
1089
  }
994
1090
  });
1091
+ eagerTool(server, "kd-classify", "Operator-solicited reclassify for any :KnowledgeDocument that carries a `body` property (today: email-thread KDs; future: any thread-shaped source). Reads the KD by attachmentId, decides via a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under $ACCOUNT_DIR/tmp/, and returns a dispatch envelope the admin agent hands to the `librarian` specialist. Librarian runs the document-ingest skill against the temp file; memory-ingest MERGEs by attachmentId, replacing any prior :Section children, and stamps lastClassifiedAt + lastClassifiedBodyLength on the KD so the next gate call has a baseline. Bodyless attachment KDs (PDFs etc.) are not supported here — they have no growth signal.", {
1092
+ attachmentId: z.string().describe("attachmentId of the :KnowledgeDocument to classify. For email threads, this is sha256(`email:<accountId>:<rootMessageId>`); use the email-classify tool when you only have the root Message-ID."),
1093
+ }, async ({ attachmentId }) => {
1094
+ if (!accountId)
1095
+ return refuseNoAccount("kd-classify");
1096
+ try {
1097
+ const text = await kdClassify({ accountId, attachmentId });
1098
+ return { content: [{ type: "text", text }] };
1099
+ }
1100
+ catch (err) {
1101
+ return {
1102
+ content: [{
1103
+ type: "text",
1104
+ text: `kd-classify failed: ${err instanceof Error ? err.message : String(err)}`,
1105
+ }],
1106
+ isError: true,
1107
+ };
1108
+ }
1109
+ });
995
1110
  eagerTool(server, "memory-archive-write", "Bulk-archive write surface for flat datasets. " +
996
1111
  "Writes typed entities + natural edges in 500-row UNWIND batches. The Cypher body is fixed " +
997
1112
  "server-side per `archiveType`; the agent supplies parsed rows + the discriminant, never " +
@@ -1223,6 +1338,30 @@ if (!readOnly) {
1223
1338
  };
1224
1339
  }
1225
1340
  });
1341
+ server.tool("memory-node-exists", "Deterministic label-existence check for installer-seeded singleton roots. " +
1342
+ "Returns { exists, elementId, labels } for a single label/accountId lookup, " +
1343
+ "bypassing the memory-search ranking stack so an installer-seeded node with " +
1344
+ "empty content properties (e.g. a freshly seeded :LocalBusiness with no name yet) " +
1345
+ "is still reported as exists=true. Allow-list of labels is fixed: LocalBusiness, " +
1346
+ "AdminUser, UserProfile, Person. Refuses any other label — use memory-search for " +
1347
+ "ranked discovery on general labels. Use this before any branch that claims a " +
1348
+ "singleton root is missing.", {
1349
+ label: z.enum(MEMORY_NODE_EXISTS_LABELS).describe("Installer-seeded singleton label. One of: LocalBusiness, AdminUser, UserProfile, Person."),
1350
+ }, async ({ label }) => {
1351
+ if (!accountId)
1352
+ return refuseNoAccount("memory-node-exists");
1353
+ try {
1354
+ const result = await memoryNodeExists({ accountId, label });
1355
+ const payload = JSON.stringify(result);
1356
+ return { content: [{ type: "text", text: payload }] };
1357
+ }
1358
+ catch (err) {
1359
+ return {
1360
+ content: [{ type: "text", text: `memory-node-exists failed: ${err instanceof Error ? err.message : String(err)}` }],
1361
+ isError: true,
1362
+ };
1363
+ }
1364
+ });
1226
1365
  server.tool("memory-delete", "Soft-delete a single graph node by elementId. Adds the :Trashed label, preserves relationships, " +
1227
1366
  "and excludes the node from memory-search and any read filtered via notTrashed. For KnowledgeDocument, " +
1228
1367
  "cascades to linked Section and Chunk children. For Conversation, cascades to attached :PART_OF Messages " +