@rubytech/create-realagent 1.0.828 → 1.0.830

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 (144) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -1
  3. package/payload/platform/lib/oauth-llm/dist/index.d.ts +1 -1
  4. package/payload/platform/lib/oauth-llm/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/oauth-llm/dist/index.js +21 -0
  6. package/payload/platform/lib/oauth-llm/dist/index.js.map +1 -1
  7. package/payload/platform/lib/oauth-llm/src/index.ts +24 -0
  8. package/payload/platform/neo4j/migrations/007-conversation-archive-source.ts +116 -0
  9. package/payload/platform/neo4j/schema.cypher +12 -2
  10. package/payload/platform/package.json +2 -2
  11. package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +6 -6
  12. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +14 -8
  13. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +2 -2
  14. package/payload/platform/plugins/contacts/mcp/dist/index.js +5 -5
  15. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +1 -1
  17. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
  18. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +29 -23
  19. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
  20. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  21. package/payload/platform/plugins/memory/PLUGIN.md +6 -5
  22. package/payload/platform/plugins/{whatsapp-import/bin/ingest.mjs → memory/bin/conversation-archive-ingest.mjs} +136 -212
  23. package/payload/platform/plugins/{whatsapp-import/bin/whatsapp-ingest.sh → memory/bin/conversation-archive-ingest.sh} +27 -19
  24. package/payload/platform/plugins/memory/mcp/dist/index.js +26 -212
  25. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +4 -3
  27. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +11 -6
  29. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +103 -0
  31. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +5 -0
  33. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -0
  34. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +30 -0
  35. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -0
  36. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +48 -0
  37. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -0
  38. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +23 -0
  39. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.d.ts +3 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.d.ts.map +1 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.js +237 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.js.map +1 -0
  44. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.d.ts +11 -0
  45. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.d.ts.map +1 -0
  46. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.js +21 -0
  47. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.js.map +1 -0
  48. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts +16 -0
  49. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts.map +1 -0
  50. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js +39 -0
  51. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js.map +1 -0
  52. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts +17 -0
  53. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts.map +1 -0
  54. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js +90 -0
  55. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js.map +1 -0
  56. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.d.ts +9 -0
  57. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.d.ts.map +1 -0
  58. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.js +32 -0
  59. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.js.map +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.d.ts +3 -0
  61. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.d.ts.map +1 -0
  62. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.js +27 -0
  63. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.js.map +1 -0
  64. package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.d.ts +45 -0
  65. package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.d.ts.map +1 -0
  66. package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.js +125 -0
  67. package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.js.map +1 -0
  68. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +24 -1
  69. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  70. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +293 -33
  71. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  72. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.d.ts.map +1 -1
  73. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js +9 -2
  74. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +16 -1
  76. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
  77. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +12 -3
  78. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.d.ts +2 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.d.ts.map +1 -0
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js +75 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js.map +1 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.d.ts +2 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.d.ts.map +1 -0
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.js +67 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.js.map +1 -0
  87. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js +2 -138
  88. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js.map +1 -1
  89. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +39 -3
  90. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
  91. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.d.ts +2 -0
  92. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.d.ts.map +1 -0
  93. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js +148 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js.map +1 -0
  95. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +1 -47
  96. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  97. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +9 -318
  98. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  99. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +7 -0
  100. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  101. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +14 -8
  102. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  103. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +21 -17
  104. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
  105. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +77 -37
  106. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  107. package/payload/platform/plugins/memory/references/schema-base.md +3 -1
  108. package/payload/platform/plugins/{whatsapp-import/skills/whatsapp-import → memory/skills/conversation-archive}/SKILL.md +45 -36
  109. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +59 -6
  110. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  111. package/payload/platform/scripts/seed-neo4j.sh +9 -8
  112. package/payload/platform/templates/specialists/agents/database-operator.md +7 -14
  113. package/payload/server/chunk-7BO5HDJC.js +10093 -0
  114. package/payload/server/chunk-CUSH3UXP.js +2305 -0
  115. package/payload/server/chunk-EL4DZ56X.js +1116 -0
  116. package/payload/server/chunk-IWNDVGKT.js +10077 -0
  117. package/payload/server/chunk-KC7NUABI.js +654 -0
  118. package/payload/server/chunk-QOJ2D26Z.js +654 -0
  119. package/payload/server/chunk-RC46ZYGT.js +2305 -0
  120. package/payload/server/chunk-WUVXPZIV.js +1116 -0
  121. package/payload/server/client-pool-3TM3SRIA.js +32 -0
  122. package/payload/server/client-pool-7NTEFNVQ.js +32 -0
  123. package/payload/server/cloudflare-task-tracker-4NIODMGL.js +19 -0
  124. package/payload/server/cloudflare-task-tracker-WE77WXSI.js +19 -0
  125. package/payload/server/maxy-edge.js +3 -3
  126. package/payload/server/neo4j-migrations-4XPNJNM6.js +490 -0
  127. package/payload/server/neo4j-migrations-XTQ4WEV6.js +428 -0
  128. package/payload/server/server.js +6 -6
  129. package/payload/platform/plugins/whatsapp-import/PLUGIN.md +0 -48
  130. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/delta-append.test.ts +0 -163
  131. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/parse-export-lrm.test.ts +0 -83
  132. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/parse-export.test.ts +0 -678
  133. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/sessionize.test.ts +0 -91
  134. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/to-classifier-input.test.ts +0 -59
  135. package/payload/platform/plugins/whatsapp-import/lib/src/delta-cursor.ts +0 -54
  136. package/payload/platform/plugins/whatsapp-import/lib/src/derive-keys.ts +0 -82
  137. package/payload/platform/plugins/whatsapp-import/lib/src/index.ts +0 -22
  138. package/payload/platform/plugins/whatsapp-import/lib/src/parse-export.ts +0 -471
  139. package/payload/platform/plugins/whatsapp-import/lib/src/sessionize.ts +0 -81
  140. package/payload/platform/plugins/whatsapp-import/lib/src/to-classifier-input.ts +0 -48
  141. package/payload/platform/plugins/whatsapp-import/lib/tsconfig.json +0 -9
  142. package/payload/platform/plugins/whatsapp-import/lib/vitest.config.ts +0 -9
  143. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/conversation-archive-shape.md +0 -143
  144. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/export-parse.md +0 -109
@@ -0,0 +1,428 @@
1
+ import {
2
+ ACCOUNTS_DIR
3
+ } from "./chunk-5OG7TUQL.js";
4
+ import {
5
+ getSession,
6
+ projectAgent
7
+ } from "./chunk-CUSH3UXP.js";
8
+
9
+ // app/lib/neo4j-migrations.ts
10
+ import { readFileSync as readFileSync2 } from "fs";
11
+ import { resolve as resolve4 } from "path";
12
+
13
+ // ../neo4j/migrations/004-prune-alien-accounts.ts
14
+ import { readFileSync, readdirSync } from "fs";
15
+ import { resolve } from "path";
16
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ async function pruneAlienAccounts(driver, platformRoot) {
18
+ const accountsDir = resolve(platformRoot, "..", "data", "accounts");
19
+ const validIds = enumerateValidAccountIds(accountsDir);
20
+ if (validIds.size === 0) {
21
+ throw new Error(
22
+ `refusing to prune: no valid accounts found under ${accountsDir} \u2014 corrupt install? not deleting anything to avoid wiping the entire graph.`
23
+ );
24
+ }
25
+ const valid = Array.from(validIds);
26
+ const session = driver.session();
27
+ try {
28
+ const peek = await session.run(
29
+ `MATCH (n)
30
+ WHERE n.accountId IS NOT NULL AND NOT n.accountId IN $valid
31
+ RETURN DISTINCT n.accountId AS aid`,
32
+ { valid }
33
+ );
34
+ const alienIds = [];
35
+ for (const record of peek.records) {
36
+ const aid = record.get("aid");
37
+ if (typeof aid === "string") alienIds.push(aid);
38
+ }
39
+ if (alienIds.length === 0) return;
40
+ const result = await session.run(
41
+ `MATCH (n)
42
+ WHERE n.accountId IS NOT NULL AND NOT n.accountId IN $valid
43
+ DETACH DELETE n
44
+ RETURN count(n) AS pruned`,
45
+ { valid }
46
+ );
47
+ const prunedRaw = result.records[0]?.get("pruned");
48
+ const pruned = typeof prunedRaw === "number" ? prunedRaw : prunedRaw?.toNumber?.() ?? 0;
49
+ console.error(
50
+ `[graph-invariant] alien-accounts pruned=${pruned} accountIds=${alienIds.join(",")}`
51
+ );
52
+ } finally {
53
+ await session.close();
54
+ }
55
+ }
56
+ function enumerateValidAccountIds(accountsDir) {
57
+ const valid = /* @__PURE__ */ new Set();
58
+ let names;
59
+ try {
60
+ names = readdirSync(accountsDir);
61
+ } catch (err) {
62
+ if (err.code === "ENOENT") return valid;
63
+ throw err;
64
+ }
65
+ for (const name of names) {
66
+ if (!UUID_RE.test(name)) continue;
67
+ const configPath = resolve(accountsDir, name, "account.json");
68
+ try {
69
+ JSON.parse(readFileSync(configPath, "utf-8"));
70
+ valid.add(name);
71
+ } catch (err) {
72
+ const code = err.code ?? "parse-error";
73
+ if (code === "ENOENT") continue;
74
+ console.error(
75
+ `[graph-invariant] account-json-skip uuid=${name} reason=${code}`
76
+ );
77
+ }
78
+ }
79
+ return valid;
80
+ }
81
+
82
+ // ../neo4j/migrations/004-project-admin-agent.ts
83
+ import { existsSync, readdirSync as readdirSync2 } from "fs";
84
+ import { resolve as resolve2 } from "path";
85
+ async function projectAccountAdmin(accountId, accountDir) {
86
+ const adminDir = resolve2(accountDir, "agents", "admin");
87
+ const configPath = resolve2(adminDir, "config.json");
88
+ if (!existsSync(adminDir) || !existsSync(configPath)) {
89
+ return { projected: 0, failed: 0 };
90
+ }
91
+ try {
92
+ await projectAgent(accountId, accountDir, "admin");
93
+ return { projected: 1, failed: 0 };
94
+ } catch (err) {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ console.error(
97
+ `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} project FAILED error="${msg}"`
98
+ );
99
+ return { projected: 0, failed: 1 };
100
+ }
101
+ }
102
+ async function backfillAdminHandledBy(driver, accountId) {
103
+ const session = driver.session();
104
+ try {
105
+ const result = await session.run(
106
+ `MATCH (c:AdminConversation {accountId: $accountId})
107
+ WHERE NOT EXISTS((c)-[:HANDLED_BY]->(:Agent))
108
+ OPTIONAL MATCH (a:Agent {accountId: $accountId, slug: 'admin'})
109
+ FOREACH (_ IN CASE WHEN a IS NULL THEN [] ELSE [1] END | MERGE (c)-[:HANDLED_BY]->(a))
110
+ RETURN
111
+ count(c) AS candidates,
112
+ sum(CASE WHEN a IS NULL THEN 0 ELSE 1 END) AS edges`,
113
+ { accountId }
114
+ );
115
+ const toNum = (v) => {
116
+ if (typeof v === "number") return v;
117
+ if (v && typeof v.toNumber === "function") {
118
+ return v.toNumber();
119
+ }
120
+ return 0;
121
+ };
122
+ return {
123
+ candidates: toNum(result.records[0]?.get("candidates")),
124
+ edges: toNum(result.records[0]?.get("edges"))
125
+ };
126
+ } finally {
127
+ await session.close();
128
+ }
129
+ }
130
+ async function backfillChannel(driver, accountId) {
131
+ const session = driver.session();
132
+ try {
133
+ const result = await session.run(
134
+ `MATCH (c:Conversation {accountId: $accountId})
135
+ WHERE c.channel IS NULL
136
+ SET c.channel = 'webchat'
137
+ RETURN count(c) AS backfilled`,
138
+ { accountId }
139
+ );
140
+ const raw = result.records[0]?.get("backfilled");
141
+ if (typeof raw === "number") return raw;
142
+ if (raw && typeof raw.toNumber === "function") {
143
+ return raw.toNumber();
144
+ }
145
+ return 0;
146
+ } finally {
147
+ await session.close();
148
+ }
149
+ }
150
+ async function applyAdminAgentBackfill(driver, platformRoot) {
151
+ const accountsDir = resolve2(platformRoot, "..", "data", "accounts");
152
+ const start = Date.now();
153
+ if (!existsSync(accountsDir)) {
154
+ console.error(
155
+ `[admin-agent-graph-backfill] accounts-dir missing at ${accountsDir} \u2014 nothing to do`
156
+ );
157
+ return;
158
+ }
159
+ const accountEntries = readdirSync2(accountsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
160
+ console.error(
161
+ `[admin-agent-graph-backfill] start accounts=${accountEntries.length}`
162
+ );
163
+ let totalProjected = 0;
164
+ let totalFailed = 0;
165
+ let totalHandledByCandidates = 0;
166
+ let totalHandledByEdges = 0;
167
+ let totalChannelBackfilled = 0;
168
+ const perAccount = [];
169
+ for (const entry of accountEntries) {
170
+ const accountDir = resolve2(accountsDir, entry.name);
171
+ const accountId = entry.name;
172
+ const accountStart = Date.now();
173
+ const { projected, failed } = await projectAccountAdmin(
174
+ accountId,
175
+ accountDir
176
+ );
177
+ totalProjected += projected;
178
+ totalFailed += failed;
179
+ let handledByStats = { candidates: 0, edges: 0 };
180
+ let channelBackfilled = 0;
181
+ try {
182
+ handledByStats = await backfillAdminHandledBy(driver, accountId);
183
+ totalHandledByCandidates += handledByStats.candidates;
184
+ totalHandledByEdges += handledByStats.edges;
185
+ } catch (err) {
186
+ const msg = err instanceof Error ? err.message : String(err);
187
+ console.error(
188
+ `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} handled-by-backfill FAILED error="${msg}"`
189
+ );
190
+ }
191
+ try {
192
+ channelBackfilled = await backfillChannel(driver, accountId);
193
+ totalChannelBackfilled += channelBackfilled;
194
+ } catch (err) {
195
+ const msg = err instanceof Error ? err.message : String(err);
196
+ console.error(
197
+ `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} channel-backfill FAILED error="${msg}"`
198
+ );
199
+ }
200
+ perAccount.push({
201
+ accountId,
202
+ projected,
203
+ failed,
204
+ handledByCandidates: handledByStats.candidates,
205
+ handledByEdges: handledByStats.edges,
206
+ channelBackfilled
207
+ });
208
+ const ms2 = Date.now() - accountStart;
209
+ console.error(
210
+ `[admin-agent-graph-backfill] account=${accountId.slice(0, 8)} projected=${projected} failed=${failed} handled-by-candidates=${handledByStats.candidates} handled-by-edges=${handledByStats.edges} channel-backfilled=${channelBackfilled} ms=${ms2}`
211
+ );
212
+ }
213
+ const ms = Date.now() - start;
214
+ console.error(
215
+ `[admin-agent-graph-backfill] done totals: projected=${totalProjected} failed=${totalFailed} handled-by-candidates=${totalHandledByCandidates} handled-by-edges=${totalHandledByEdges} channel-backfilled=${totalChannelBackfilled} ms=${ms}`
216
+ );
217
+ }
218
+ async function main() {
219
+ if (!existsSync(ACCOUNTS_DIR)) {
220
+ console.error(
221
+ `[admin-agent-graph-backfill] ACCOUNTS_DIR missing at ${ACCOUNTS_DIR} \u2014 nothing to do`
222
+ );
223
+ process.exit(0);
224
+ }
225
+ const driverShim = { session: () => getSession() };
226
+ const platformRoot = resolve2(ACCOUNTS_DIR, "..", "..", "platform");
227
+ try {
228
+ await applyAdminAgentBackfill(driverShim, platformRoot);
229
+ process.exit(0);
230
+ } catch (err) {
231
+ const msg = err instanceof Error ? err.message : String(err);
232
+ console.error(`[admin-agent-graph-backfill] fatal error="${msg}"`);
233
+ process.exit(2);
234
+ }
235
+ }
236
+ if (process.argv[1]?.endsWith("004-project-admin-agent.ts")) {
237
+ main().catch((err) => {
238
+ const msg = err instanceof Error ? err.message : String(err);
239
+ console.error(`[admin-agent-graph-backfill] fatal error="${msg}"`);
240
+ process.exit(2);
241
+ });
242
+ }
243
+
244
+ // ../neo4j/migrations/005-removed-review-feature.ts
245
+ import { resolve as resolve3 } from "path";
246
+ import { unlinkSync } from "fs";
247
+ import { homedir } from "os";
248
+ import { basename, dirname } from "path";
249
+ async function removeReviewFeature(driver, platformRoot) {
250
+ const session = driver.session();
251
+ let eventsDeleted = 0;
252
+ let alertsDeleted = 0;
253
+ try {
254
+ const eventResult = await session.run(
255
+ `MATCH (e:Event)
256
+ WHERE e.actionTool = "review-digest-compose"
257
+ OR e.eventId STARTS WITH "review-digest-"
258
+ DETACH DELETE e
259
+ RETURN count(e) AS deleted`
260
+ );
261
+ eventsDeleted = toNumber(eventResult.records[0]?.get("deleted"));
262
+ const alertResult = await session.run(
263
+ `MATCH (a:ReviewAlert)
264
+ DETACH DELETE a
265
+ RETURN count(a) AS deleted`
266
+ );
267
+ alertsDeleted = toNumber(alertResult.records[0]?.get("deleted"));
268
+ } finally {
269
+ await session.close();
270
+ }
271
+ const installDirName = basename(dirname(platformRoot));
272
+ const configDir = resolve3(homedir(), `.${installDirName}`);
273
+ const artefacts = [
274
+ resolve3(configDir, "logs", "review.log"),
275
+ resolve3(configDir, "review-rules.json"),
276
+ resolve3(configDir, "review-state.json"),
277
+ resolve3(configDir, "review-pending-alerts.jsonl")
278
+ ];
279
+ for (const path of artefacts) {
280
+ try {
281
+ unlinkSync(path);
282
+ } catch (err) {
283
+ const code = err.code;
284
+ if (code === "ENOENT") continue;
285
+ console.error(
286
+ `[migration] removed-review-feature unlink-skip path=${path} reason=${code ?? "unknown"}`
287
+ );
288
+ }
289
+ }
290
+ console.error(
291
+ `[migration] removed-review-feature events=${eventsDeleted} alerts=${alertsDeleted}`
292
+ );
293
+ }
294
+ function toNumber(v) {
295
+ if (typeof v === "number") return v;
296
+ if (v && typeof v.toNumber === "function") {
297
+ return v.toNumber();
298
+ }
299
+ return 0;
300
+ }
301
+
302
+ // ../neo4j/migrations/006-prune-bogus-whatsapp-persons.ts
303
+ var POLLUTED_NAME_REGEX = "(?s).*[\\n\\[].*";
304
+ async function pruneBogusWhatsappPersons(driver) {
305
+ const session = driver.session();
306
+ try {
307
+ const nonAutoRes = await session.run(
308
+ `MATCH (p:Person {source: 'whatsapp'})
309
+ WHERE coalesce(p.participantStatus, '') <> 'auto-created'
310
+ AND p.name =~ $regex
311
+ RETURN elementId(p) AS elementId, p.name AS name`,
312
+ { regex: POLLUTED_NAME_REGEX }
313
+ );
314
+ const skippedNonAuto = nonAutoRes.records.length;
315
+ for (const record of nonAutoRes.records) {
316
+ const elementId = record.get("elementId");
317
+ const name = record.get("name");
318
+ console.error(
319
+ `[migration:whatsapp-bogus-person-prune] non-auto-match elementId=${elementId} name=${JSON.stringify(name)}`
320
+ );
321
+ }
322
+ const matchedRes = await session.run(
323
+ `MATCH (p:Person {source: 'whatsapp', participantStatus: 'auto-created'})
324
+ WHERE p.name =~ $regex
325
+ RETURN count(p) AS matched`,
326
+ { regex: POLLUTED_NAME_REGEX }
327
+ );
328
+ const matched = toNumber2(matchedRes.records[0]?.get("matched"));
329
+ if (matched === 0) {
330
+ console.error(
331
+ `[migration:whatsapp-bogus-person-prune] matched=0 deleted=0 skipped-non-auto=${skippedNonAuto}`
332
+ );
333
+ return;
334
+ }
335
+ const deleteRes = await session.run(
336
+ `MATCH (p:Person {source: 'whatsapp', participantStatus: 'auto-created'})
337
+ WHERE p.name =~ $regex
338
+ DETACH DELETE p
339
+ RETURN count(p) AS deleted`,
340
+ { regex: POLLUTED_NAME_REGEX }
341
+ );
342
+ const deleted = toNumber2(deleteRes.records[0]?.get("deleted"));
343
+ console.error(
344
+ `[migration:whatsapp-bogus-person-prune] matched=${matched} deleted=${deleted} skipped-non-auto=${skippedNonAuto}`
345
+ );
346
+ } finally {
347
+ await session.close();
348
+ }
349
+ }
350
+ function toNumber2(v) {
351
+ if (typeof v === "number") return v;
352
+ if (v && typeof v.toNumber === "function") {
353
+ return v.toNumber();
354
+ }
355
+ return 0;
356
+ }
357
+
358
+ // app/lib/neo4j-migrations.ts
359
+ var BOOT_MIGRATIONS = [
360
+ { fileName: "003-person-name-eradicate.cypher", slug: "person-name-eradicate" }
361
+ ];
362
+ async function applyBootMigrations(driver, platformRoot) {
363
+ for (const migration of BOOT_MIGRATIONS) {
364
+ await applyOne(driver, platformRoot, migration);
365
+ }
366
+ try {
367
+ await pruneAlienAccounts(driver, platformRoot);
368
+ } catch (err) {
369
+ const msg = err instanceof Error ? err.message : String(err);
370
+ console.error(`[migration] failed prune-alien-accounts error="${msg}"`);
371
+ }
372
+ try {
373
+ await applyAdminAgentBackfill(driver, platformRoot);
374
+ } catch (err) {
375
+ const msg = err instanceof Error ? err.message : String(err);
376
+ console.error(`[migration] failed admin-agent-graph-backfill error="${msg}"`);
377
+ }
378
+ try {
379
+ await removeReviewFeature(driver, platformRoot);
380
+ } catch (err) {
381
+ const msg = err instanceof Error ? err.message : String(err);
382
+ console.error(`[migration] failed removed-review-feature error="${msg}"`);
383
+ }
384
+ try {
385
+ await pruneBogusWhatsappPersons(driver);
386
+ } catch (err) {
387
+ const msg = err instanceof Error ? err.message : String(err);
388
+ console.error(`[migration] failed prune-bogus-whatsapp-persons error="${msg}"`);
389
+ }
390
+ }
391
+ async function applyOne(driver, platformRoot, migration) {
392
+ const path = resolve4(platformRoot, "neo4j/migrations", migration.fileName);
393
+ let cypher;
394
+ try {
395
+ cypher = readFileSync2(path, "utf-8");
396
+ } catch (err) {
397
+ const msg = err instanceof Error ? err.message : String(err);
398
+ console.error(
399
+ `[migration] failed ${migration.slug} error=cannot-read-file path=${path} msg=${msg}`
400
+ );
401
+ return;
402
+ }
403
+ const session = driver.session();
404
+ try {
405
+ const result = await session.run(cypher);
406
+ const record = result.records[0];
407
+ const removedRaw = record?.get("removed");
408
+ const removed = toNumber3(removedRaw);
409
+ console.error(
410
+ `[migration] applied ${migration.slug} removed=${removed}`
411
+ );
412
+ } catch (err) {
413
+ const msg = err instanceof Error ? err.message : String(err);
414
+ console.error(`[migration] failed ${migration.slug} error="${msg}"`);
415
+ } finally {
416
+ await session.close();
417
+ }
418
+ }
419
+ function toNumber3(v) {
420
+ if (typeof v === "number") return v;
421
+ if (v && typeof v.toNumber === "function") {
422
+ return v.toNumber();
423
+ }
424
+ return 0;
425
+ }
426
+ export {
427
+ applyBootMigrations
428
+ };
@@ -51,7 +51,7 @@ import {
51
51
  vncLog,
52
52
  waitForExit,
53
53
  writeChromiumWrapper
54
- } from "./chunk-HAXOJNAM.js";
54
+ } from "./chunk-7BO5HDJC.js";
55
55
  import {
56
56
  agentLogStream,
57
57
  clearSessionHistory,
@@ -79,7 +79,7 @@ import {
79
79
  sigtermFlushStreamLogs,
80
80
  unregisterSession,
81
81
  validateSession
82
- } from "./chunk-F5QBVHLS.js";
82
+ } from "./chunk-EL4DZ56X.js";
83
83
  import {
84
84
  ACCOUNTS_DIR,
85
85
  PLATFORM_ROOT,
@@ -97,7 +97,7 @@ import {
97
97
  openCloudflareTask,
98
98
  readTunnelState,
99
99
  resolveUnitGoneVerdict
100
- } from "./chunk-T2OPNP3L.js";
100
+ } from "./chunk-QOJ2D26Z.js";
101
101
  import {
102
102
  GREETING_DIRECTIVE,
103
103
  HAIKU_MODEL,
@@ -129,7 +129,7 @@ import {
129
129
  verifyAndGetConversationUpdatedAt,
130
130
  verifyConversationOwnership,
131
131
  writeAdminUserAndPerson
132
- } from "./chunk-AEHTLEC3.js";
132
+ } from "./chunk-RC46ZYGT.js";
133
133
 
134
134
  // ../lib/graph-trash/dist/index.js
135
135
  var require_dist = __commonJS({
@@ -7285,7 +7285,7 @@ var app11 = new Hono();
7285
7285
  app11.post("/cancel", requireAdminSession, async (c) => {
7286
7286
  const session_key = c.var.sessionKey;
7287
7287
  try {
7288
- const { interruptClient: interruptClient2 } = await import("./client-pool-FXCFSUXR.js");
7288
+ const { interruptClient: interruptClient2 } = await import("./client-pool-7NTEFNVQ.js");
7289
7289
  await interruptClient2(session_key);
7290
7290
  return c.json({ ok: true });
7291
7291
  } catch (err) {
@@ -12832,7 +12832,7 @@ autoDeliverPremiumPlugins(bootEntitlement?.purchasedPlugins ?? void 0);
12832
12832
  (async () => {
12833
12833
  if (!bootAccount) return;
12834
12834
  try {
12835
- const { recoverRunningCloudflareTasks } = await import("./cloudflare-task-tracker-CR6TL4VL.js");
12835
+ const { recoverRunningCloudflareTasks } = await import("./cloudflare-task-tracker-WE77WXSI.js");
12836
12836
  const result = await recoverRunningCloudflareTasks(
12837
12837
  bootAccount.accountId,
12838
12838
  configDirForWhatsApp,
@@ -1,48 +0,0 @@
1
- ---
2
- name: whatsapp-import
3
- description: "Import a WhatsApp `_chat.txt` export into the {{productName}} Neo4j graph as a `:ConversationArchive` parent + chunked `:Section:Conversation` children (the chunked-archive contract). Single-phase ingest: parse → operator-confirms owner + every participant → sessionize at gap-hours boundary → Haiku-chunk each session into topic-bounded :Section:Conversation rows with summary + topic keywords. Re-imports are delta-append: prior chunks never touched. Insight derivation (`:Observation` / `:Task` / `:Preference`) is deferred to a separate follow-up task. Skill-only plugin owned by the database-operator specialist. Opt-in per brand — not enabled by default. Distinct from the live `whatsapp` plugin (Baileys QR pairing + in-memory store)."
4
- tools: []
5
- always: false
6
- embed: false
7
- specialist: database-operator
8
- metadata: {"platform":{"optional":true,"pluginKey":"whatsapp-import"}}
9
- ---
10
-
11
- # WhatsApp Import
12
-
13
- Ingests a WhatsApp "Export Chat" archive (the `_chat.txt` file plus media attachments) into the {{productName}} Neo4j graph as a single document-shaped `:ConversationArchive` with chunked `:Section:Conversation` children. Skill-only plugin — no MCP server, no admin tools added. The skill runs under the `database-operator` specialist, which owns external-archive ingestion and ad-hoc graph operations.
14
-
15
- ## Single-phase contract (the chunked-archive contract)
16
-
17
- Pre-Task-891 the plugin shipped two skills (`whatsapp-import` for raw load + `whatsapp-import-enrich` for Haiku-driven `:Observation` extraction). The split worked but operated at per-message granularity: insights were attached to individual `:Message` rows, missing intent that spans multiple turns, and the auto-`:Person` participant pattern leaked entities on every parser miss. the chunked-archive contract retires both skills and the per-message writer in favour of a single-phase contract:
18
-
19
- 1. **Parse** — deterministic `_chat.txt` parser (unchanged from the deterministic parser contract).
20
- 2. **Operator confirmation** — every distinct senderName must resolve to an existing `:AdminUser` or `:Person` elementId. No auto-creation.
21
- 3. **Sessionize** — split parsed messages at gap-hours boundaries (default 12h).
22
- 4. **Classify** — each session goes to Haiku via `memory-classify` with `mode='chat'`; returns one or more `:Section:Conversation` chunks with topic-bounded `summary`, `keywords`, `firstMessageAt`, `lastMessageAt`, `participantNames`, `messageCount`, and verbatim turn-attributed `body`.
23
- 5. **Write** — `memory-ingest` with `parentLabel='ConversationArchive'` MERGEs the parent on `conversationIdentity = sha256(accountId + ":" + sortedParticipantElementIds)`, MERGEs `:PARTICIPANT_IN` edges, drops any prior chunks for THIS export's SHA-256 (idempotency), CREATEs new chunks, and extends the existing `:NEXT` chain from its tail.
24
-
25
- Re-imports are delta-append: prior chunks are never touched, only messages after `lastIngestedMessageHash` flow through the pipeline. See [skills/whatsapp-import/references/conversation-archive-shape.md](skills/whatsapp-import/references/conversation-archive-shape.md) for the full schema.
26
-
27
- Insight derivation (`:Observation` / `:Task` / `:Preference` / `:MENTIONS`, anchored to chunks) is deferred to a separate follow-up task with its own skill name.
28
-
29
- ## When this applies
30
-
31
- The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat. The specialist runs the skill's owner + all-participants confirmation flow before any line is written, then invokes the deterministic Bash entry (`bin/whatsapp-ingest.sh`) once: parse → sessionize → in-process Haiku classify → in-process memory-ingest all run in one Node process.
32
-
33
- ## Accepted export shapes
34
-
35
- WhatsApp's "Export Chat" emits `[DD/MM/YYYY, HH:MM:SS]` prefixes by default in modern releases and `[DD/MM/YY, HH:MM:SS]` in legacy exports. The parser accepts both year shapes (2-digit mapped `2000+yy`, 4-digit passed through) within a single file and auto-detects DD/MM vs MM/DD ordering from the first matched line — see [skills/whatsapp-import/references/export-parse.md](skills/whatsapp-import/references/export-parse.md) for the full grammar contract.
36
-
37
- ## What this is not
38
-
39
- - **Not** the live `whatsapp` plugin. That plugin (Baileys QR pairing) holds messages in an in-memory store cleared on restart. This plugin imports historical exports into Neo4j as persistent graph nodes. Names are deliberately distinct (`whatsapp-import` vs `whatsapp`) so the boundary is mechanical.
40
- - **Not** a media-transcription pipeline. Voice notes (`.opus`), photos, PDFs are skipped at parse with a counter logged. Transcription / OCR is a separate plugin decision.
41
- - **Not** a generic chat-export importer. Each messaging channel (Telegram, iMessage, Signal, SMS dump) ships as its own opt-in plugin under the same shape when requested.
42
- - **Not** an insight-extraction pass. `:Observation` / `:Task` / `:Preference` derivation against chunks is a separate, follow-up task.
43
-
44
- ## Relationship to other plugins
45
-
46
- - **memory** — provides `memory-classify` (with `mode='chat'`) and `memory-ingest` (with `parentLabel='ConversationArchive'`). The whatsapp-import bin imports both in-process — no MCP envelope between steps. All writes carry `source='whatsapp'` + `createdByAgent='whatsapp-import'` provenance. The legacy `mcp__memory__whatsapp-export-parse` / `whatsapp-export-insight-write` MCP tools and the `memory-archive-write` MCP path with `archiveType=whatsapp-export` are blocked at the harness — the Bash entry is the only supported invocation surface.
47
- - **database-operator specialist** — owns execution. See [admin/IDENTITY.md](../../../platform/templates/agents/admin/IDENTITY.md) delegation clause and [database-operator.md](../../../platform/templates/specialists/agents/database-operator.md) per-source archive list (which now names the chunked-archive shape under the WhatsApp entry).
48
- - **linkedin-import** — sister plugin under the same opt-in pattern (LinkedIn Basic Data Export). Reading [linkedin-import/PLUGIN.md](../linkedin-import/PLUGIN.md) is the fastest way to understand the load-phase shape that complements this plugin's chunked-archive approach.
@@ -1,163 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { findDeltaCursor } from "../delta-cursor.js";
3
- import {
4
- deriveConversationIdentity,
5
- deriveMessageContentHash,
6
- normaliseSenderName,
7
- } from "../derive-keys.js";
8
- import type { ParsedLine } from "../parse-export.js";
9
-
10
- function mk(dateSent: string, senderName: string, body: string): ParsedLine {
11
- return { dateSent, senderName, body, sequenceIndex: 0 };
12
- }
13
-
14
- describe("deriveConversationIdentity", () => {
15
- it("is stable across participant order (sorted internally)", () => {
16
- const a = deriveConversationIdentity({
17
- accountId: "acct-1",
18
- participantElementIds: ["1:abc:1", "1:abc:2"],
19
- });
20
- const b = deriveConversationIdentity({
21
- accountId: "acct-1",
22
- participantElementIds: ["1:abc:2", "1:abc:1"],
23
- });
24
- expect(a).toBe(b);
25
- });
26
-
27
- it("changes when accountId differs", () => {
28
- const a = deriveConversationIdentity({
29
- accountId: "acct-1",
30
- participantElementIds: ["1:abc:1", "1:abc:2"],
31
- });
32
- const b = deriveConversationIdentity({
33
- accountId: "acct-2",
34
- participantElementIds: ["1:abc:1", "1:abc:2"],
35
- });
36
- expect(a).not.toBe(b);
37
- });
38
-
39
- it("rejects empty participant array", () => {
40
- expect(() =>
41
- deriveConversationIdentity({
42
- accountId: "acct-1",
43
- participantElementIds: [],
44
- }),
45
- ).toThrow(/non-empty/);
46
- });
47
-
48
- it("rejects empty accountId", () => {
49
- expect(() =>
50
- deriveConversationIdentity({
51
- accountId: "",
52
- participantElementIds: ["1:abc:1"],
53
- }),
54
- ).toThrow(/accountId/);
55
- });
56
-
57
- it("treats DM and group identically (same formula, different array length)", () => {
58
- const dm = deriveConversationIdentity({
59
- accountId: "acct-1",
60
- participantElementIds: ["1:a:1", "1:a:2"],
61
- });
62
- const group = deriveConversationIdentity({
63
- accountId: "acct-1",
64
- participantElementIds: ["1:a:1", "1:a:2", "1:a:3"],
65
- });
66
- expect(dm).not.toBe(group);
67
- // Both must hex-decode to a sha256 (64 hex chars).
68
- expect(dm).toMatch(/^[a-f0-9]{64}$/);
69
- expect(group).toMatch(/^[a-f0-9]{64}$/);
70
- });
71
- });
72
-
73
- describe("deriveMessageContentHash", () => {
74
- it("is content-only — no archive bytes contribute", () => {
75
- const a = deriveMessageContentHash({
76
- dateSent: "2026-03-14T10:00:00+00:00",
77
- senderName: "Joel",
78
- body: "hi",
79
- });
80
- const b = deriveMessageContentHash({
81
- dateSent: "2026-03-14T10:00:00+00:00",
82
- senderName: "Joel",
83
- body: "hi",
84
- });
85
- expect(a).toBe(b);
86
- });
87
-
88
- it("changes when any field changes", () => {
89
- const base = {
90
- dateSent: "2026-03-14T10:00:00+00:00",
91
- senderName: "Joel",
92
- body: "hi",
93
- };
94
- expect(deriveMessageContentHash(base)).not.toBe(
95
- deriveMessageContentHash({ ...base, body: "hello" }),
96
- );
97
- expect(deriveMessageContentHash(base)).not.toBe(
98
- deriveMessageContentHash({ ...base, senderName: "Adam" }),
99
- );
100
- expect(deriveMessageContentHash(base)).not.toBe(
101
- deriveMessageContentHash({ ...base, dateSent: "2026-03-14T10:00:01+00:00" }),
102
- );
103
- });
104
-
105
- it("collapses NFKC-equivalent sender names to one hash", () => {
106
- const a = deriveMessageContentHash({
107
- dateSent: "2026-03-14T10:00:00+00:00",
108
- senderName: " ADAM Mackay ",
109
- body: "hi",
110
- });
111
- const b = deriveMessageContentHash({
112
- dateSent: "2026-03-14T10:00:00+00:00",
113
- senderName: "adam mackay",
114
- body: "hi",
115
- });
116
- expect(a).toBe(b);
117
- });
118
- });
119
-
120
- describe("normaliseSenderName", () => {
121
- it("returns NFKC-trim-lower form", () => {
122
- expect(normaliseSenderName(" Adam Mackay ")).toBe("adam mackay");
123
- });
124
- });
125
-
126
- describe("findDeltaCursor", () => {
127
- const lines: ParsedLine[] = [
128
- mk("2026-03-14T10:00:00+00:00", "Joel", "first"),
129
- mk("2026-03-14T10:01:00+00:00", "Adam", "second"),
130
- mk("2026-03-14T10:02:00+00:00", "Joel", "third"),
131
- ];
132
- const hashFor = (l: ParsedLine) =>
133
- deriveMessageContentHash({
134
- dateSent: l.dateSent,
135
- senderName: l.senderName,
136
- body: l.body,
137
- });
138
-
139
- it("returns 'found' with deltaStart = cursor + 1 when the hash matches a prior line", () => {
140
- const result = findDeltaCursor(lines, hashFor(lines[0]));
141
- expect(result).toEqual({ kind: "found", deltaStart: 1 });
142
- });
143
-
144
- it("returns 'empty' when the cursor is the last line (no new messages)", () => {
145
- const result = findDeltaCursor(lines, hashFor(lines[2]));
146
- expect(result).toEqual({ kind: "empty" });
147
- });
148
-
149
- it("returns 'missing' when no parsed line matches (LOUD-FAIL upstream)", () => {
150
- const result = findDeltaCursor(lines, "deadbeef".repeat(8));
151
- expect(result).toEqual({ kind: "missing" });
152
- });
153
-
154
- it("returns 'missing' for an empty parsed sequence (cursor cannot exist)", () => {
155
- const result = findDeltaCursor([], hashFor(lines[0]));
156
- expect(result).toEqual({ kind: "missing" });
157
- });
158
-
159
- it("rejects empty cursor hash", () => {
160
- expect(() => findDeltaCursor(lines, "")).toThrow(/non-empty/);
161
- expect(() => findDeltaCursor(lines, " ")).toThrow(/non-empty/);
162
- });
163
- });