@rubytech/create-realagent 1.0.829 → 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.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/lib/oauth-llm/dist/index.d.ts +1 -1
- package/payload/platform/lib/oauth-llm/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/oauth-llm/dist/index.js +21 -0
- package/payload/platform/lib/oauth-llm/dist/index.js.map +1 -1
- package/payload/platform/lib/oauth-llm/src/index.ts +24 -0
- package/payload/platform/neo4j/migrations/007-conversation-archive-source.ts +116 -0
- package/payload/platform/neo4j/schema.cypher +12 -3
- package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +54 -39
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +64 -26
- package/payload/platform/plugins/contacts/mcp/dist/index.js +5 -5
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +29 -23
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +2 -1
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +541 -0
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +106 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +30 -16
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +11 -6
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +5 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +30 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +48 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +23 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.js +237 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/whatsapp-text.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.d.ts +11 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.js +21 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/delta-cursor.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts +16 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js +39 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts +17 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js +90 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sender-bind.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.d.ts +9 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.js +32 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/sessionize.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.js +27 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/to-turn-text.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.d.ts +45 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.js +125 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/document-chunker.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +24 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +266 -16
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js +9 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js +75 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.js +67 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-whatsapp-text.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +34 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +17 -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 +34 -13
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +18 -7
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +24 -8
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -2
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +133 -0
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +5 -2
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/scripts/seed-neo4j.sh +15 -15
- package/payload/platform/templates/specialists/agents/database-operator.md +8 -9
- package/payload/server/chunk-7BO5HDJC.js +10093 -0
- package/payload/server/chunk-EL4DZ56X.js +1116 -0
- package/payload/server/chunk-QOJ2D26Z.js +654 -0
- package/payload/server/chunk-RC46ZYGT.js +2305 -0
- package/payload/server/client-pool-7NTEFNVQ.js +32 -0
- package/payload/server/cloudflare-task-tracker-WE77WXSI.js +19 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/neo4j-migrations-4XPNJNM6.js +490 -0
- package/payload/server/server.js +6 -6
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// conversation-archive-ingest.mjs — in-process orchestrator for
|
|
4
|
+
// conversation-archive-ingest.sh (Task 894 — supersedes whatsapp-import bin).
|
|
5
|
+
//
|
|
6
|
+
// Source-agnostic. The same pipeline runs for every conversation source;
|
|
7
|
+
// `--source <enum>` selects the normaliser at the top of the pipeline:
|
|
8
|
+
//
|
|
9
|
+
// normalise → bind canonical senders → derive conversationIdentity
|
|
10
|
+
// → look up prior :ConversationArchive (delta cursor)
|
|
11
|
+
// → sessionize delta at gap-hours boundaries
|
|
12
|
+
// → for each session: classify (mode='chat') → collect chunks
|
|
13
|
+
// → memoryIngest(parentLabel='ConversationArchive', source=<enum>)
|
|
14
|
+
//
|
|
15
|
+
// Argv (positional): <archive-path>
|
|
16
|
+
// Argv (flags): --source <whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|other>
|
|
17
|
+
// --owner-element-id <id>
|
|
18
|
+
// --participant-person-ids <csv>
|
|
19
|
+
// --scope <admin|public>
|
|
20
|
+
// [--session-gap-hours <N>] (default 12)
|
|
21
|
+
// [--account-id <accountId>]
|
|
22
|
+
// [--timezone <iana>]
|
|
23
|
+
// [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>]
|
|
24
|
+
// [--session-id <id>]
|
|
25
|
+
//
|
|
26
|
+
// Stdout (success): one JSON line — counters the skill needs to formulate
|
|
27
|
+
// the three operator-facing messages. See SKILL.md for the shape.
|
|
28
|
+
// Stderr: one [conversation-archive] FAIL line on failure, exit non-zero.
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
32
|
+
import { join, resolve, dirname } from "node:path";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
|
|
37
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// 1. Resolve dist paths.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
const platformRoot =
|
|
43
|
+
process.env.MAXY_PLATFORM_ROOT?.trim() ||
|
|
44
|
+
resolve(__dirname, "..", "..", "..");
|
|
45
|
+
|
|
46
|
+
const NORMALISERS_PATH = resolve(
|
|
47
|
+
platformRoot,
|
|
48
|
+
"plugins",
|
|
49
|
+
"memory",
|
|
50
|
+
"mcp",
|
|
51
|
+
"dist",
|
|
52
|
+
"lib",
|
|
53
|
+
"conversation-normalisers",
|
|
54
|
+
"index.js",
|
|
55
|
+
);
|
|
56
|
+
const PIPELINE_PATH = resolve(
|
|
57
|
+
platformRoot,
|
|
58
|
+
"plugins",
|
|
59
|
+
"memory",
|
|
60
|
+
"mcp",
|
|
61
|
+
"dist",
|
|
62
|
+
"lib",
|
|
63
|
+
"conversation-pipeline",
|
|
64
|
+
);
|
|
65
|
+
const NEO4J_LIB_PATH = resolve(
|
|
66
|
+
platformRoot,
|
|
67
|
+
"plugins",
|
|
68
|
+
"memory",
|
|
69
|
+
"mcp",
|
|
70
|
+
"dist",
|
|
71
|
+
"lib",
|
|
72
|
+
"neo4j.js",
|
|
73
|
+
);
|
|
74
|
+
const LLM_CLASSIFIER_PATH = resolve(
|
|
75
|
+
platformRoot,
|
|
76
|
+
"plugins",
|
|
77
|
+
"memory",
|
|
78
|
+
"mcp",
|
|
79
|
+
"dist",
|
|
80
|
+
"lib",
|
|
81
|
+
"llm-classifier.js",
|
|
82
|
+
);
|
|
83
|
+
const MEMORY_INGEST_PATH = resolve(
|
|
84
|
+
platformRoot,
|
|
85
|
+
"plugins",
|
|
86
|
+
"memory",
|
|
87
|
+
"mcp",
|
|
88
|
+
"dist",
|
|
89
|
+
"tools",
|
|
90
|
+
"memory-ingest.js",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// 2. Logger
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
function log(line) {
|
|
97
|
+
process.stderr.write(`[conversation-archive] ${line}\n`);
|
|
98
|
+
}
|
|
99
|
+
function fail(phase, fields) {
|
|
100
|
+
const fieldStr = Object.entries(fields)
|
|
101
|
+
.map(([k, v]) =>
|
|
102
|
+
typeof v === "string" && (v.includes(" ") || v.includes("="))
|
|
103
|
+
? `${k}="${v.replace(/"/g, '\\"')}"`
|
|
104
|
+
: `${k}=${v ?? "-"}`,
|
|
105
|
+
)
|
|
106
|
+
.join(" ");
|
|
107
|
+
process.stderr.write(`[conversation-archive] FAIL phase=${phase} ${fieldStr}\n`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// 3. Argv parsing
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
const VALID_SOURCES = new Set([
|
|
115
|
+
"whatsapp",
|
|
116
|
+
"telegram",
|
|
117
|
+
"signal",
|
|
118
|
+
"linkedin-messages",
|
|
119
|
+
"zoom-transcript",
|
|
120
|
+
"meeting-minutes",
|
|
121
|
+
"imessage",
|
|
122
|
+
"slack",
|
|
123
|
+
"other",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
function parseArgv(argv) {
|
|
127
|
+
const args = argv.slice(2);
|
|
128
|
+
let archive = null;
|
|
129
|
+
const flags = {};
|
|
130
|
+
for (let i = 0; i < args.length; i++) {
|
|
131
|
+
const a = args[i];
|
|
132
|
+
if (!a.startsWith("--")) {
|
|
133
|
+
if (archive == null) archive = a;
|
|
134
|
+
else fail("argv", { reason: `unexpected positional argument "${a}"` });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const key = a.slice(2);
|
|
138
|
+
const v = args[++i];
|
|
139
|
+
if (v == null) fail("argv", { reason: `flag --${key} requires a value` });
|
|
140
|
+
flags[camelCase(key)] = v;
|
|
141
|
+
}
|
|
142
|
+
if (!archive) fail("argv", { reason: "missing positional <archive>" });
|
|
143
|
+
if (!flags.source) fail("argv", { reason: "missing --source" });
|
|
144
|
+
if (!VALID_SOURCES.has(flags.source)) {
|
|
145
|
+
fail("argv", { reason: `invalid --source "${flags.source}" (whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|other)` });
|
|
146
|
+
}
|
|
147
|
+
if (!flags.ownerElementId) fail("argv", { reason: "missing --owner-element-id" });
|
|
148
|
+
if (!flags.participantPersonIds) {
|
|
149
|
+
fail("argv", {
|
|
150
|
+
reason: "missing --participant-person-ids (csv of operator-confirmed :Person/:AdminUser elementIds, owner excluded)",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!flags.scope) fail("argv", { reason: "missing --scope" });
|
|
154
|
+
if (flags.scope !== "admin" && flags.scope !== "public") {
|
|
155
|
+
fail("argv", { reason: `invalid --scope "${flags.scope}" (admin|public)` });
|
|
156
|
+
}
|
|
157
|
+
return { archive, flags };
|
|
158
|
+
}
|
|
159
|
+
function camelCase(s) {
|
|
160
|
+
return s.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// 4. Archive resolution. WhatsApp ships zip-or-dir-or-_chat.txt; other
|
|
165
|
+
// sources pass a single file path that the normaliser interprets directly.
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
function resolveSourceFile(archivePath, source) {
|
|
168
|
+
const abs = resolve(archivePath);
|
|
169
|
+
if (!existsSync(abs)) fail("argv", { reason: `archive path not found: ${abs}` });
|
|
170
|
+
const st = statSync(abs);
|
|
171
|
+
|
|
172
|
+
if (source === "whatsapp") {
|
|
173
|
+
if (st.isFile() && abs.endsWith(".zip")) {
|
|
174
|
+
const tmp = mkdtempSync(join(tmpdir(), "conversation-archive-"));
|
|
175
|
+
const unzip = spawnSync("unzip", ["-q", "-o", abs, "-d", tmp], { encoding: "utf8" });
|
|
176
|
+
if (unzip.status !== 0) {
|
|
177
|
+
rmSync(tmp, { recursive: true });
|
|
178
|
+
fail("argv", {
|
|
179
|
+
reason: "unzip failed",
|
|
180
|
+
archive: abs,
|
|
181
|
+
stderr: (unzip.stderr || "").slice(0, 200),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const chat = findChatTxt(tmp);
|
|
185
|
+
if (!chat) {
|
|
186
|
+
rmSync(tmp, { recursive: true });
|
|
187
|
+
fail("argv", { reason: "_chat.txt not found in zip", archive: abs });
|
|
188
|
+
}
|
|
189
|
+
return { sourceFile: chat, cleanup: () => rmSync(tmp, { recursive: true }) };
|
|
190
|
+
}
|
|
191
|
+
if (st.isDirectory()) {
|
|
192
|
+
const chat = findChatTxt(abs);
|
|
193
|
+
if (!chat) fail("argv", { reason: "_chat.txt not found in directory", archive: abs });
|
|
194
|
+
return { sourceFile: chat, cleanup: () => {} };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (st.isFile()) {
|
|
199
|
+
return { sourceFile: abs, cleanup: () => {} };
|
|
200
|
+
}
|
|
201
|
+
fail("argv", { reason: `unsupported archive shape for source=${source}: ${abs}` });
|
|
202
|
+
return { sourceFile: abs, cleanup: () => {} };
|
|
203
|
+
}
|
|
204
|
+
function findChatTxt(dir) {
|
|
205
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
206
|
+
for (const e of entries) {
|
|
207
|
+
if (e.isFile() && e.name === "_chat.txt") return join(dir, e.name);
|
|
208
|
+
}
|
|
209
|
+
for (const e of entries) {
|
|
210
|
+
if (e.isDirectory()) {
|
|
211
|
+
const nested = findChatTxt(join(dir, e.name));
|
|
212
|
+
if (nested) return nested;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// 5. Account resolution (Phase 0 = single account)
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
function resolveAccountId(flags) {
|
|
222
|
+
if (flags.accountId && flags.accountId.trim()) return flags.accountId.trim();
|
|
223
|
+
const installDir = resolve(platformRoot, "..");
|
|
224
|
+
const accountsDir = join(installDir, "data", "accounts");
|
|
225
|
+
if (!existsSync(accountsDir)) {
|
|
226
|
+
fail("argv", { reason: `accounts dir not found: ${accountsDir}; pass --account-id explicitly` });
|
|
227
|
+
}
|
|
228
|
+
const dirs = readdirSync(accountsDir, { withFileTypes: true })
|
|
229
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
230
|
+
.map((d) => d.name);
|
|
231
|
+
if (dirs.length === 0) fail("argv", { reason: `no accounts found under ${accountsDir}` });
|
|
232
|
+
if (dirs.length > 1) {
|
|
233
|
+
fail("argv", {
|
|
234
|
+
reason: `multiple accounts under ${accountsDir} (${dirs.join(",")}); pass --account-id explicitly`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return dirs[0];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// 6. Main
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
async function main() {
|
|
244
|
+
const startedMs = Date.now();
|
|
245
|
+
const { archive, flags } = parseArgv(process.argv);
|
|
246
|
+
const source = flags.source;
|
|
247
|
+
const ownerElementId = flags.ownerElementId;
|
|
248
|
+
const participantElementIds = flags.participantPersonIds
|
|
249
|
+
.split(",")
|
|
250
|
+
.map((s) => s.trim())
|
|
251
|
+
.filter((s) => s.length > 0);
|
|
252
|
+
if (participantElementIds.length === 0) {
|
|
253
|
+
fail("argv", { reason: "--participant-person-ids must list at least one elementId" });
|
|
254
|
+
}
|
|
255
|
+
const scope = flags.scope;
|
|
256
|
+
const accountId = resolveAccountId(flags);
|
|
257
|
+
const timezone = flags.timezone || "Europe/London";
|
|
258
|
+
const dateFormat = flags.dateFormat;
|
|
259
|
+
const sessionGapHours = flags.sessionGapHours
|
|
260
|
+
? parseFloat(flags.sessionGapHours)
|
|
261
|
+
: 12;
|
|
262
|
+
if (!Number.isFinite(sessionGapHours) || sessionGapHours <= 0) {
|
|
263
|
+
fail("argv", { reason: `invalid --session-gap-hours "${flags.sessionGapHours}" (must be positive number)` });
|
|
264
|
+
}
|
|
265
|
+
const sessionId =
|
|
266
|
+
flags.sessionId ||
|
|
267
|
+
`conversation-archive:${source}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
|
|
268
|
+
|
|
269
|
+
// Imports — fail loudly if any compiled dist missing
|
|
270
|
+
let getNormaliser;
|
|
271
|
+
let sessionize, toTurnText, findDeltaCursor;
|
|
272
|
+
let normaliseSenderName, deriveConversationIdentity, deriveMessageContentHash;
|
|
273
|
+
let bindCanonicalSenders;
|
|
274
|
+
let getSession, classifyDocument, memoryIngest;
|
|
275
|
+
try {
|
|
276
|
+
({ getNormaliser } = await import(NORMALISERS_PATH));
|
|
277
|
+
({ sessionize } = await import(join(PIPELINE_PATH, "sessionize.js")));
|
|
278
|
+
({ toTurnText } = await import(join(PIPELINE_PATH, "to-turn-text.js")));
|
|
279
|
+
({ findDeltaCursor } = await import(join(PIPELINE_PATH, "delta-cursor.js")));
|
|
280
|
+
({ normaliseSenderName, deriveConversationIdentity, deriveMessageContentHash } =
|
|
281
|
+
await import(join(PIPELINE_PATH, "derive-keys.js")));
|
|
282
|
+
({ bindCanonicalSenders } = await import(join(PIPELINE_PATH, "sender-bind.js")));
|
|
283
|
+
({ getSession } = await import(NEO4J_LIB_PATH));
|
|
284
|
+
({ classifyDocument } = await import(LLM_CLASSIFIER_PATH));
|
|
285
|
+
({ memoryIngest } = await import(MEMORY_INGEST_PATH));
|
|
286
|
+
} catch (err) {
|
|
287
|
+
fail("import", {
|
|
288
|
+
reason: "failed to import compiled dist",
|
|
289
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 6a. Resolve source file
|
|
294
|
+
const { sourceFile, cleanup } = resolveSourceFile(archive, source);
|
|
295
|
+
|
|
296
|
+
// 6b. Run the source-specific normaliser
|
|
297
|
+
let normaliserResult;
|
|
298
|
+
try {
|
|
299
|
+
const normaliser = getNormaliser(source);
|
|
300
|
+
normaliserResult = await normaliser({
|
|
301
|
+
filePath: sourceFile,
|
|
302
|
+
accountId,
|
|
303
|
+
timezone,
|
|
304
|
+
opts: dateFormat ? { dateFormat } : undefined,
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
cleanup();
|
|
308
|
+
fail("parse", { reason: err instanceof Error ? err.message : String(err) });
|
|
309
|
+
}
|
|
310
|
+
const allLines = normaliserResult.parsedLines;
|
|
311
|
+
const archiveSha256 = normaliserResult.archiveSha256;
|
|
312
|
+
const archiveSourceFile = normaliserResult.archiveSourceFile;
|
|
313
|
+
log(
|
|
314
|
+
`source=${source} file=${archiveSourceFile} owner=${ownerElementId} participants=${participantElementIds.length} scope=${scope} accountId=${accountId} archiveSha256=${archiveSha256.slice(0, 12)} session-gap-hours=${sessionGapHours}`,
|
|
315
|
+
);
|
|
316
|
+
log(
|
|
317
|
+
`parsed lines=${normaliserResult.counters.parsed} media-skipped=${normaliserResult.counters.mediaSkipped} system-skipped=${normaliserResult.counters.systemSkipped}`,
|
|
318
|
+
);
|
|
319
|
+
if (allLines.length === 0) {
|
|
320
|
+
cleanup();
|
|
321
|
+
fail("parse", { reason: "zero parsed lines after walking archive" });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 6c. Bind canonical senders against the confirmed set
|
|
325
|
+
const distinctSenderNames = Array.from(new Set(allLines.map((l) => l.senderName)));
|
|
326
|
+
const senderHistogram = computeSenderHistogram(allLines);
|
|
327
|
+
let session = getSession();
|
|
328
|
+
try {
|
|
329
|
+
await bindCanonicalSenders({
|
|
330
|
+
session,
|
|
331
|
+
accountId,
|
|
332
|
+
ownerElementId,
|
|
333
|
+
participantElementIds,
|
|
334
|
+
senderNames: distinctSenderNames,
|
|
335
|
+
});
|
|
336
|
+
} catch (err) {
|
|
337
|
+
await session.close().catch(() => {});
|
|
338
|
+
cleanup();
|
|
339
|
+
if (err && err.userFacing) {
|
|
340
|
+
process.stderr.write(`[conversation-archive] FAIL ${err.message}\n`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
fail("argv", { reason: err instanceof Error ? err.message : String(err) });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 6d. Derive conversationIdentity and look up prior :ConversationArchive
|
|
347
|
+
const conversationIdentity = deriveConversationIdentity({
|
|
348
|
+
accountId,
|
|
349
|
+
participantElementIds: [ownerElementId, ...participantElementIds],
|
|
350
|
+
});
|
|
351
|
+
let priorArchive = null;
|
|
352
|
+
try {
|
|
353
|
+
const r = await session.run(
|
|
354
|
+
`MATCH (a:ConversationArchive { conversationIdentity: $cid })
|
|
355
|
+
RETURN elementId(a) AS elemId,
|
|
356
|
+
a.lastIngestedMessageHash AS lastHash,
|
|
357
|
+
a.lastIngestedMessageAt AS lastAt LIMIT 1`,
|
|
358
|
+
{ cid: conversationIdentity },
|
|
359
|
+
);
|
|
360
|
+
if (r.records[0]) {
|
|
361
|
+
priorArchive = {
|
|
362
|
+
elemId: r.records[0].get("elemId"),
|
|
363
|
+
lastHash: r.records[0].get("lastHash"),
|
|
364
|
+
lastAt: r.records[0].get("lastAt"),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
} catch (err) {
|
|
368
|
+
await session.close().catch(() => {});
|
|
369
|
+
cleanup();
|
|
370
|
+
fail("delta-cursor-missing", { reason: `conversationArchive lookup failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
371
|
+
}
|
|
372
|
+
await session.close().catch(() => {});
|
|
373
|
+
|
|
374
|
+
// 6e. Compute deltaStart
|
|
375
|
+
let deltaStart = 0;
|
|
376
|
+
let deltaKind = "first-ingest";
|
|
377
|
+
if (priorArchive && priorArchive.lastHash) {
|
|
378
|
+
const cursor = findDeltaCursor(allLines, priorArchive.lastHash);
|
|
379
|
+
if (cursor.kind === "missing") {
|
|
380
|
+
cleanup();
|
|
381
|
+
fail("delta-cursor-missing", {
|
|
382
|
+
reason: `prior cursor not found in re-export (operator deleted prior messages, or this is a different chat archive)`,
|
|
383
|
+
priorArchive: priorArchive.elemId,
|
|
384
|
+
lastIngestedMessageAt: priorArchive.lastAt,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (cursor.kind === "empty") {
|
|
388
|
+
log(`noop reason="no new messages since ${priorArchive.lastAt}"`);
|
|
389
|
+
cleanup();
|
|
390
|
+
const totalMs = Date.now() - startedMs;
|
|
391
|
+
process.stdout.write(JSON.stringify({
|
|
392
|
+
archiveElementId: priorArchive.elemId,
|
|
393
|
+
conversationIdentity,
|
|
394
|
+
archiveSha256,
|
|
395
|
+
archiveSourceFile,
|
|
396
|
+
source,
|
|
397
|
+
parsed: normaliserResult.counters.parsed,
|
|
398
|
+
mediaSkipped: normaliserResult.counters.mediaSkipped,
|
|
399
|
+
systemSkipped: normaliserResult.counters.systemSkipped,
|
|
400
|
+
delta: { kind: "empty-delta", deltaStart: allLines.length, deltaMessages: 0 },
|
|
401
|
+
sessions: 0,
|
|
402
|
+
chunks: 0,
|
|
403
|
+
nextEdgesCreated: 0,
|
|
404
|
+
participantsLinked: 0,
|
|
405
|
+
dateRange: { first: allLines[0].dateSent, last: allLines[allLines.length - 1].dateSent },
|
|
406
|
+
senderHistogram,
|
|
407
|
+
topicKeywords: [],
|
|
408
|
+
ms: totalMs,
|
|
409
|
+
priorLastIngestedMessageAt: priorArchive.lastAt,
|
|
410
|
+
}) + "\n");
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
deltaStart = cursor.deltaStart;
|
|
414
|
+
deltaKind = "delta";
|
|
415
|
+
}
|
|
416
|
+
const deltaLines = allLines.slice(deltaStart);
|
|
417
|
+
log(
|
|
418
|
+
`delta cursor=${priorArchive ? priorArchive.lastHash.slice(0, 12) : "(first-ingest)"} cursor-line=${deltaStart} delta-messages=${deltaLines.length}`,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// 6f. Sessionize delta
|
|
422
|
+
const sessions = sessionize(deltaLines, sessionGapHours);
|
|
423
|
+
log(
|
|
424
|
+
`sessionize source=${source} archiveSha256=${archiveSha256.slice(0, 12)} messages=${deltaLines.length} sessions=${sessions.length} gap-hours=${sessionGapHours}`,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// 6g. Classify each session via Haiku (mode='chat')
|
|
428
|
+
const allChunks = [];
|
|
429
|
+
const allKeywords = new Set();
|
|
430
|
+
for (const s of sessions) {
|
|
431
|
+
const sessionStart = Date.now();
|
|
432
|
+
const text = toTurnText(s);
|
|
433
|
+
const result = await classifyDocument({
|
|
434
|
+
accountId,
|
|
435
|
+
mode: "chat",
|
|
436
|
+
anchorDescription: `Conversation transcript (${[ownerElementId, ...participantElementIds].length} participants, session ${s.index + 1} of ${sessions.length})`,
|
|
437
|
+
ontologyLabels: new Set([]),
|
|
438
|
+
naturalEdgeMap: "",
|
|
439
|
+
documentText: text,
|
|
440
|
+
});
|
|
441
|
+
if (result.kind === "fallback") {
|
|
442
|
+
cleanup();
|
|
443
|
+
fail("classify", { reason: `Haiku fallback on session ${s.index}: ${result.reason}` });
|
|
444
|
+
}
|
|
445
|
+
const chunkCount = result.output.sections.length;
|
|
446
|
+
log(
|
|
447
|
+
`classify-session sessionIndex=${s.index + 1}/${sessions.length} messages=${s.messages.length} chars=${text.length} chunks=${chunkCount} ms=${Date.now() - sessionStart}`,
|
|
448
|
+
);
|
|
449
|
+
if (chunkCount === 0 && s.messages.length > 0) {
|
|
450
|
+
cleanup();
|
|
451
|
+
fail("classify", {
|
|
452
|
+
reason: `session ${s.index} of ${s.messages.length} messages produced zero chunks (classifier-prompt regression)`,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
for (const sec of result.output.sections) allChunks.push(sec);
|
|
456
|
+
for (const kw of result.output.documentKeywords) allKeywords.add(kw);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 6h. Compute lastIngestedMessageHash from the last delta line
|
|
460
|
+
const lastLine = deltaLines[deltaLines.length - 1];
|
|
461
|
+
const lastIngestedMessageHash = deriveMessageContentHash({
|
|
462
|
+
dateSent: lastLine.dateSent,
|
|
463
|
+
senderName: lastLine.senderName,
|
|
464
|
+
body: lastLine.body,
|
|
465
|
+
});
|
|
466
|
+
const lastIngestedMessageAt = lastLine.dateSent;
|
|
467
|
+
|
|
468
|
+
// 6i. Aggregate document-level summary across sessions
|
|
469
|
+
const documentSummary = sessions.length === 1
|
|
470
|
+
? `${deltaLines.length} messages in 1 session, ${allChunks.length} chunks.`
|
|
471
|
+
: `${deltaLines.length} messages in ${sessions.length} sessions, ${allChunks.length} chunks.`;
|
|
472
|
+
|
|
473
|
+
// 6j. Call memoryIngest with parentLabel='ConversationArchive'
|
|
474
|
+
let ingestResult;
|
|
475
|
+
const ingestStart = Date.now();
|
|
476
|
+
try {
|
|
477
|
+
ingestResult = await memoryIngest({
|
|
478
|
+
accountId,
|
|
479
|
+
attachmentId: conversationIdentity,
|
|
480
|
+
parentLabel: "ConversationArchive",
|
|
481
|
+
source,
|
|
482
|
+
documentSummary,
|
|
483
|
+
anchorNodeId: ownerElementId,
|
|
484
|
+
anchorLabel: "AdminUser",
|
|
485
|
+
sections: allChunks,
|
|
486
|
+
scope,
|
|
487
|
+
sessionId,
|
|
488
|
+
documentKeywords: Array.from(allKeywords),
|
|
489
|
+
archiveSha256,
|
|
490
|
+
archiveSourceFile,
|
|
491
|
+
lastIngestedMessageHash,
|
|
492
|
+
lastIngestedMessageAt,
|
|
493
|
+
participantElementIds: [ownerElementId, ...participantElementIds],
|
|
494
|
+
});
|
|
495
|
+
} catch (err) {
|
|
496
|
+
cleanup();
|
|
497
|
+
fail("memory-ingest", { reason: err instanceof Error ? err.message : String(err) });
|
|
498
|
+
}
|
|
499
|
+
log(
|
|
500
|
+
`source=${source} file=${archiveSourceFile} conversationIdentity=${conversationIdentity.slice(0, 12)} archiveElementId=${ingestResult.documentNodeId} chunks-written=${ingestResult.sectionCount} next-edges=${ingestResult.edgeBreakdown.NEXT ?? 0} participants=${ingestResult.edgeBreakdown.PARTICIPANT_IN ?? 0} ms=${Date.now() - ingestStart}`,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
cleanup();
|
|
504
|
+
const totalMs = Date.now() - startedMs;
|
|
505
|
+
log(`done source=${source} conversationIdentity=${conversationIdentity.slice(0, 12)} total-ms=${totalMs} exit=0`);
|
|
506
|
+
|
|
507
|
+
process.stdout.write(JSON.stringify({
|
|
508
|
+
archiveElementId: ingestResult.documentNodeId,
|
|
509
|
+
conversationIdentity,
|
|
510
|
+
archiveSha256,
|
|
511
|
+
archiveSourceFile,
|
|
512
|
+
source,
|
|
513
|
+
parsed: normaliserResult.counters.parsed,
|
|
514
|
+
mediaSkipped: normaliserResult.counters.mediaSkipped,
|
|
515
|
+
systemSkipped: normaliserResult.counters.systemSkipped,
|
|
516
|
+
delta: { kind: deltaKind, deltaStart, deltaMessages: deltaLines.length },
|
|
517
|
+
sessions: sessions.length,
|
|
518
|
+
chunks: ingestResult.sectionCount,
|
|
519
|
+
nextEdgesCreated: ingestResult.edgeBreakdown.NEXT ?? 0,
|
|
520
|
+
participantsLinked: ingestResult.edgeBreakdown.PARTICIPANT_IN ?? 0,
|
|
521
|
+
dateRange: { first: allLines[0].dateSent, last: allLines[allLines.length - 1].dateSent },
|
|
522
|
+
senderHistogram,
|
|
523
|
+
topicKeywords: Array.from(allKeywords),
|
|
524
|
+
ms: totalMs,
|
|
525
|
+
}) + "\n");
|
|
526
|
+
process.exit(0);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function computeSenderHistogram(lines) {
|
|
530
|
+
const counts = new Map();
|
|
531
|
+
for (const l of lines) {
|
|
532
|
+
counts.set(l.senderName, (counts.get(l.senderName) ?? 0) + 1);
|
|
533
|
+
}
|
|
534
|
+
return Array.from(counts.entries())
|
|
535
|
+
.map(([name, count]) => ({ name, count }))
|
|
536
|
+
.sort((a, b) => b.count - a.count);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
main().catch((err) => {
|
|
540
|
+
fail("uncaught", { reason: err instanceof Error ? err.message : String(err) });
|
|
541
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# conversation-archive-ingest.sh — single deterministic Bash entry for
|
|
4
|
+
# conversation-archive ingestion (Task 894 — supersedes whatsapp-ingest.sh).
|
|
5
|
+
#
|
|
6
|
+
# Source-agnostic: WhatsApp `_chat.txt`, Telegram, Signal, LinkedIn DMs, Zoom
|
|
7
|
+
# transcript, meeting minutes, iMessage, Slack — every source flows through
|
|
8
|
+
# the same pipeline with `--source` selecting the normaliser.
|
|
9
|
+
#
|
|
10
|
+
# Pipeline: normalise (per source) → bind canonical sender set →
|
|
11
|
+
# derive conversationIdentity → look up prior :ConversationArchive (delta
|
|
12
|
+
# cursor) → sessionize delta at gap-hours boundary → classify each session
|
|
13
|
+
# via Haiku (mode='chat') → memory-ingest with parentLabel='ConversationArchive'.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# bash conversation-archive-ingest.sh <archive-path>
|
|
17
|
+
# --source <whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|other>
|
|
18
|
+
# --owner-element-id <id>
|
|
19
|
+
# --participant-person-ids <csv>
|
|
20
|
+
# --scope <admin|public>
|
|
21
|
+
# [--session-gap-hours <N>] (default 12)
|
|
22
|
+
# [--account-id <accountId>]
|
|
23
|
+
# [--timezone <iana-zone>]
|
|
24
|
+
# [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>] (whatsapp only)
|
|
25
|
+
#
|
|
26
|
+
# `--owner-element-id` + `--participant-person-ids` form the closed sender
|
|
27
|
+
# set; any parsed senderName outside that set LOUD-FAILs with `parser-miss`
|
|
28
|
+
# and exits non-zero.
|
|
29
|
+
#
|
|
30
|
+
# Exit 0 + JSON summary on stdout on success.
|
|
31
|
+
# Exit !0 + one [conversation-archive] FAIL line on stderr on failure.
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
arg_fail() {
|
|
37
|
+
local reason="$1"
|
|
38
|
+
echo "[conversation-archive] FAIL phase=argv reason=\"${reason}\"" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
43
|
+
PLATFORM_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
44
|
+
INGEST_MJS="$SCRIPT_DIR/conversation-archive-ingest.mjs"
|
|
45
|
+
|
|
46
|
+
if [ ! -f "$INGEST_MJS" ]; then
|
|
47
|
+
arg_fail "conversation-archive-ingest.mjs not found at $INGEST_MJS — run from a built install"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
ARCHIVE=""
|
|
51
|
+
HAS_SOURCE=0
|
|
52
|
+
SOURCE_VAL=""
|
|
53
|
+
HAS_OWNER=0
|
|
54
|
+
OWNER_VAL=""
|
|
55
|
+
HAS_PARTICIPANTS=0
|
|
56
|
+
PARTICIPANTS_VAL=""
|
|
57
|
+
HAS_SCOPE=0
|
|
58
|
+
SCOPE_VAL=""
|
|
59
|
+
|
|
60
|
+
ARGS=("$@")
|
|
61
|
+
i=0
|
|
62
|
+
while [ $i -lt ${#ARGS[@]} ]; do
|
|
63
|
+
a="${ARGS[$i]}"
|
|
64
|
+
case "$a" in
|
|
65
|
+
--source) HAS_SOURCE=1; SOURCE_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
|
|
66
|
+
--owner-element-id) HAS_OWNER=1; OWNER_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
|
|
67
|
+
--participant-person-ids) HAS_PARTICIPANTS=1; PARTICIPANTS_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
|
|
68
|
+
--scope) HAS_SCOPE=1; SCOPE_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
|
|
69
|
+
--session-gap-hours|--account-id|--timezone|--date-format|--session-id) i=$((i + 2)); continue ;;
|
|
70
|
+
--*) i=$((i + 2)); continue ;;
|
|
71
|
+
*)
|
|
72
|
+
if [ -z "$ARCHIVE" ]; then ARCHIVE="$a"; fi
|
|
73
|
+
i=$((i + 1))
|
|
74
|
+
continue
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
[ -n "$ARCHIVE" ] || arg_fail "missing positional <archive>"
|
|
80
|
+
[ "$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_OWNER" -eq 1 ] && [ -n "$OWNER_VAL" ] || arg_fail "missing --owner-element-id (or empty value)"
|
|
82
|
+
[ "$HAS_PARTICIPANTS" -eq 1 ] && [ -n "$PARTICIPANTS_VAL" ] || arg_fail "missing --participant-person-ids (csv of operator-confirmed :Person/:AdminUser elementIds, owner excluded)"
|
|
83
|
+
[ "$HAS_SCOPE" -eq 1 ] && [ -n "$SCOPE_VAL" ] || arg_fail "missing --scope (or empty value)"
|
|
84
|
+
case "$SCOPE_VAL" in
|
|
85
|
+
admin|public) : ;;
|
|
86
|
+
*) arg_fail "invalid --scope \"$SCOPE_VAL\" (admin|public)" ;;
|
|
87
|
+
esac
|
|
88
|
+
|
|
89
|
+
if [ -z "${NEO4J_PASSWORD:-}" ]; then
|
|
90
|
+
NEO4J_PASSWORD_FILE="$PLATFORM_ROOT/config/.neo4j-password"
|
|
91
|
+
if [ -f "$NEO4J_PASSWORD_FILE" ]; then
|
|
92
|
+
NEO4J_PASSWORD="$(cat "$NEO4J_PASSWORD_FILE")"
|
|
93
|
+
export NEO4J_PASSWORD
|
|
94
|
+
else
|
|
95
|
+
arg_fail "NEO4J_PASSWORD not in env and $NEO4J_PASSWORD_FILE not found"
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [ -z "${NEO4J_URI:-}" ]; then
|
|
100
|
+
arg_fail "NEO4J_URI not set (no default — set in env)"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
export NEO4J_USER="${NEO4J_USER:-neo4j}"
|
|
104
|
+
export MAXY_PLATFORM_ROOT="$PLATFORM_ROOT"
|
|
105
|
+
|
|
106
|
+
exec node "$INGEST_MJS" "$@"
|