@rubytech/create-realagent 1.0.709 → 1.0.712

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 (41) hide show
  1. package/dist/index.js +38 -3
  2. package/package.json +2 -2
  3. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +53 -0
  4. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/mcp-spawn-tee/dist/index.js +132 -0
  6. package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -0
  7. package/payload/platform/lib/mcp-spawn-tee/src/index.ts +134 -0
  8. package/payload/platform/lib/mcp-spawn-tee/tsconfig.json +8 -0
  9. package/payload/platform/package.json +2 -2
  10. package/payload/platform/plugins/docs/references/plugins-guide.md +12 -4
  11. package/payload/platform/plugins/linkedin-import/PLUGIN.md +1 -0
  12. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +26 -5
  13. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +53 -82
  14. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +42 -49
  15. package/payload/platform/plugins/memory/PLUGIN.md +1 -0
  16. package/payload/platform/plugins/memory/mcp/dist/index.js +48 -0
  17. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +34 -1
  19. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  20. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +10 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +22 -3
  23. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +33 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +229 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -0
  28. package/payload/platform/plugins/memory/mcp/package.json +3 -1
  29. package/payload/platform/plugins/memory/mcp/scripts/boot-smoke.sh +69 -0
  30. package/payload/platform/plugins/memory/references/graph-primitives.md +22 -0
  31. package/payload/platform/plugins/memory/references/schema-base.md +1 -1
  32. package/payload/platform/scripts/redact-install-logs.sh +85 -0
  33. package/payload/platform/scripts/setup.sh +20 -3
  34. package/payload/platform/scripts/verify-skill-tool-surface.sh +255 -0
  35. package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
  36. package/payload/server/chunk-A5K3CFMI.js +12297 -0
  37. package/payload/server/chunk-U5JPRUYZ.js +12298 -0
  38. package/payload/server/maxy-edge.js +1 -1
  39. package/payload/server/public/assets/{graph-BRD96pKD.js → graph-DJ7IfYHV.js} +12 -12
  40. package/payload/server/public/graph.html +1 -1
  41. package/payload/server/server.js +49 -28
@@ -0,0 +1,229 @@
1
+ import { getSession } from "../lib/neo4j.js";
2
+ /**
3
+ * memory-archive-write — deterministic bulk-archive write surface (Task 744).
4
+ *
5
+ * Writes a flat dataset (today: LinkedIn Connections.csv parsed into typed
6
+ * rows) into the graph as first-class entity nodes plus their natural edges.
7
+ * Distinct from memory-write (per-node, adjacency-required) and memory-ingest
8
+ * (narrative document → KnowledgeDocument + Section + HAS_SECTION + NEXT).
9
+ *
10
+ * Doctrine:
11
+ * - The Cypher body for each archiveType is fixed inside this tool. The
12
+ * agent never supplies Cypher; only parsed rows + the archiveType
13
+ * discriminant. Determinism here means "the Cypher is not in the tool
14
+ * input" — not "be careful with the Cypher".
15
+ * - 500-row UNWIND batches per executeWrite transaction. Larger batches
16
+ * risk Neo4j Community heap pressure on a Pi 5; smaller batches multiply
17
+ * round-trip cost.
18
+ * - Per-row provenance stamps (createdByAgent, createdBySession, source,
19
+ * createdAt) come from this server, not the agent.
20
+ * - Owner is matched by elementId so both AdminUser and external-Person
21
+ * archive owners flow through one path.
22
+ * - Counters come from result.summary.counters (nodesCreated /
23
+ * relationshipsCreated) so we never invent stamp-based detection.
24
+ */
25
+ const BATCH_SIZE = 500;
26
+ const HANDLERS = {
27
+ "linkedin-connections": linkedinConnectionsHandler(),
28
+ };
29
+ export async function memoryArchiveWrite(params) {
30
+ const { archiveType, ownerNodeId, accountId, rows, sessionId } = params;
31
+ if (!accountId || !accountId.trim()) {
32
+ throw new Error("memory-archive-write: accountId is required.");
33
+ }
34
+ if (!ownerNodeId || !ownerNodeId.trim()) {
35
+ throw new Error("memory-archive-write: ownerNodeId is required.");
36
+ }
37
+ if (!Array.isArray(rows) || rows.length === 0) {
38
+ throw new Error("memory-archive-write: rows must be a non-empty array.");
39
+ }
40
+ const handler = HANDLERS[archiveType];
41
+ if (!handler) {
42
+ throw new Error(`memory-archive-write: unknown archiveType "${archiveType}". Known: ${Object.keys(HANDLERS).join(", ")}`);
43
+ }
44
+ const session = getSession();
45
+ const startedMs = Date.now();
46
+ const errors = [];
47
+ const totals = {
48
+ createdPersons: 0,
49
+ mergedPersons: 0,
50
+ createdOrganizations: 0,
51
+ mergedOrganizations: 0,
52
+ createdEdges: 0,
53
+ };
54
+ try {
55
+ await handler.verifyOwner(session, ownerNodeId, accountId);
56
+ for (let offset = 0; offset < rows.length; offset += BATCH_SIZE) {
57
+ const batch = rows.slice(offset, offset + BATCH_SIZE);
58
+ try {
59
+ const counters = await handler.writeBatch(session, {
60
+ ownerNodeId,
61
+ accountId,
62
+ rows: batch,
63
+ sessionId: sessionId?.trim() || null,
64
+ });
65
+ totals.createdPersons += counters.createdPersons;
66
+ totals.mergedPersons += counters.mergedPersons;
67
+ totals.createdOrganizations += counters.createdOrganizations;
68
+ totals.mergedOrganizations += counters.mergedOrganizations;
69
+ totals.createdEdges += counters.createdEdges;
70
+ }
71
+ catch (err) {
72
+ const reason = err instanceof Error ? err.message : String(err);
73
+ errors.push({ rowIndex: offset, reason: `batch starting at row ${offset}: ${reason}` });
74
+ process.stderr.write(`[memory-archive-write] batch-failed archiveType=${archiveType} offset=${offset} reason=${reason}\n`);
75
+ }
76
+ }
77
+ }
78
+ finally {
79
+ await session.close();
80
+ }
81
+ const elapsedMs = Date.now() - startedMs;
82
+ process.stderr.write(`[memory-archive-write] archiveType=${archiveType} rows=${rows.length} ` +
83
+ `createdPersons=${totals.createdPersons} mergedPersons=${totals.mergedPersons} ` +
84
+ `createdOrganizations=${totals.createdOrganizations} mergedOrganizations=${totals.mergedOrganizations} ` +
85
+ `createdEdges=${totals.createdEdges} errors=${errors.length} ms=${elapsedMs}\n`);
86
+ return {
87
+ archiveType,
88
+ processedRows: rows.length,
89
+ ...totals,
90
+ errors,
91
+ };
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // linkedin-connections handler
95
+ // ---------------------------------------------------------------------------
96
+ //
97
+ // Owner anchor: AdminUser or Person (elementId match — operator-confirmed
98
+ // during the linkedin-import skill's owner-confirmation flow).
99
+ //
100
+ // Per row the CSV expresses two relationships:
101
+ // 1. (owner)-[:CONNECTED_ON_LINKEDIN {connectedOn}]->(:Person)
102
+ // 2. (:Person)-[:WORKS_FOR {title, current=true}]->(:Organization) — when company present
103
+ //
104
+ // Natural keys:
105
+ // :Person — linkedinUrl (indexed in platform/neo4j/schema.cypher)
106
+ // :Organization — (accountId, name)
107
+ //
108
+ // Provenance stamped on every node:
109
+ // createdByAgent='linkedin-import', createdBySource='linkedin-import',
110
+ // createdBySession=$sessionId (when provided), createdAt=datetime(),
111
+ // source='linkedin'
112
+ //
113
+ // We split each batch into two tx.run calls inside one executeWrite so each
114
+ // statement's summary counters give a clean per-label breakdown.
115
+ // ---------------------------------------------------------------------------
116
+ const PERSON_AND_CONNECTED_EDGE_CYPHER = `
117
+ MATCH (owner) WHERE elementId(owner) = $ownerNodeId
118
+ WITH owner, $rows AS rows
119
+ UNWIND rows AS row
120
+ MERGE (p:Person {linkedinUrl: row.linkedinUrl})
121
+ ON CREATE SET
122
+ p.accountId = $accountId,
123
+ p.source = 'linkedin',
124
+ p.createdByAgent = 'linkedin-import',
125
+ p.createdBySource = 'linkedin-import',
126
+ p.createdBySession = $sessionId,
127
+ p.createdAt = datetime(),
128
+ p.scope = 'admin'
129
+ SET
130
+ p.givenName = row.givenName,
131
+ p.familyName= row.familyName,
132
+ p.name = trim(coalesce(row.givenName,'') + ' ' + coalesce(row.familyName,''))
133
+ FOREACH (_ IN CASE WHEN row.email IS NOT NULL AND row.email <> '' THEN [1] ELSE [] END |
134
+ SET p.email = row.email
135
+ )
136
+ MERGE (owner)-[c:CONNECTED_ON_LINKEDIN]->(p)
137
+ ON CREATE SET
138
+ c.connectedOn = date(row.connectedOn),
139
+ c.source = 'linkedin',
140
+ c.createdAt = datetime()
141
+ RETURN count(*) AS processed
142
+ `;
143
+ const ORG_AND_WORKS_FOR_CYPHER = `
144
+ UNWIND $rows AS row
145
+ WITH row WHERE row.company IS NOT NULL AND row.company <> ''
146
+ MATCH (p:Person {linkedinUrl: row.linkedinUrl})
147
+ MERGE (o:Organization {accountId: $accountId, name: row.company})
148
+ ON CREATE SET
149
+ o.source = 'linkedin',
150
+ o.createdByAgent = 'linkedin-import',
151
+ o.createdBySource = 'linkedin-import',
152
+ o.createdBySession = $sessionId,
153
+ o.createdAt = datetime(),
154
+ o.scope = 'admin'
155
+ MERGE (p)-[w:WORKS_FOR]->(o)
156
+ ON CREATE SET
157
+ w.title = row.title,
158
+ w.source = 'linkedin',
159
+ w.current = true,
160
+ w.createdAt = datetime()
161
+ ON MATCH SET
162
+ w.title = coalesce(row.title, w.title)
163
+ RETURN count(*) AS processed
164
+ `;
165
+ function linkedinConnectionsHandler() {
166
+ return {
167
+ async verifyOwner(session, ownerNodeId, accountId) {
168
+ const result = await session.run(`MATCH (n) WHERE elementId(n) = $ownerNodeId
169
+ RETURN labels(n) AS labels, n.accountId AS accountId LIMIT 1`, { ownerNodeId });
170
+ if (result.records.length === 0) {
171
+ throw new Error(`ownerNodeId ${ownerNodeId} not found. Re-run the archive-owner confirmation flow before invoking memory-archive-write.`);
172
+ }
173
+ const labels = result.records[0].get("labels") ?? [];
174
+ const ownerAccountId = result.records[0].get("accountId");
175
+ const isAdminUser = labels.includes("AdminUser");
176
+ const isPerson = labels.includes("Person");
177
+ if (!isAdminUser && !isPerson) {
178
+ throw new Error(`ownerNodeId ${ownerNodeId} has labels [${labels.join(", ")}]; expected :AdminUser or :Person.`);
179
+ }
180
+ if (ownerAccountId && ownerAccountId !== accountId) {
181
+ throw new Error(`ownerNodeId ${ownerNodeId} belongs to account ${ownerAccountId}, not ${accountId}. Cross-account archive ingest refused.`);
182
+ }
183
+ },
184
+ async writeBatch(session, { ownerNodeId, accountId, rows, sessionId }) {
185
+ const params = {
186
+ ownerNodeId,
187
+ accountId,
188
+ sessionId,
189
+ rows: rows.map((r) => normaliseLinkedinRow(r)),
190
+ };
191
+ return await session.executeWrite(async (tx) => {
192
+ const personRes = await tx.run(PERSON_AND_CONNECTED_EDGE_CYPHER, params);
193
+ const personCounters = personRes.summary.counters.updates();
194
+ const orgRes = await tx.run(ORG_AND_WORKS_FOR_CYPHER, params);
195
+ const orgCounters = orgRes.summary.counters.updates();
196
+ const createdPersonNodes = personCounters.nodesCreated;
197
+ const createdOrgNodes = orgCounters.nodesCreated;
198
+ const personsTouched = params.rows.length;
199
+ const personsCreated = createdPersonNodes;
200
+ const personsMerged = personsTouched - personsCreated;
201
+ return {
202
+ createdPersons: personsCreated,
203
+ mergedPersons: personsMerged,
204
+ createdOrganizations: createdOrgNodes,
205
+ mergedOrganizations: 0,
206
+ createdEdges: personCounters.relationshipsCreated + orgCounters.relationshipsCreated,
207
+ };
208
+ });
209
+ },
210
+ };
211
+ }
212
+ function normaliseLinkedinRow(row) {
213
+ if (!row.linkedinUrl || !row.linkedinUrl.trim()) {
214
+ throw new Error("linkedin-connections row missing linkedinUrl (natural key).");
215
+ }
216
+ if (!row.connectedOn || !/^\d{4}-\d{2}-\d{2}$/.test(row.connectedOn)) {
217
+ throw new Error(`linkedin-connections row connectedOn="${row.connectedOn}" is not ISO 8601 (YYYY-MM-DD). Parser must convert "23 Apr 2026" before calling.`);
218
+ }
219
+ return {
220
+ givenName: (row.givenName ?? "").trim(),
221
+ familyName: (row.familyName ?? "").trim(),
222
+ linkedinUrl: row.linkedinUrl.trim(),
223
+ email: row.email && row.email.trim() ? row.email.trim() : null,
224
+ company: row.company && row.company.trim() ? row.company.trim() : null,
225
+ title: row.title && row.title.trim() ? row.title.trim() : null,
226
+ connectedOn: row.connectedOn,
227
+ };
228
+ }
229
+ //# sourceMappingURL=memory-archive-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-archive-write.js","sourceRoot":"","sources":["../../src/tools/memory-archive-write.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,UAAU,GAAG,GAAG,CAAC;AAoDvB,MAAM,QAAQ,GAAmC;IAC/C,sBAAsB,EAAE,0BAA0B,EAAE;CACrD,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAA0B;IAE1B,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAExE,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IACtC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,8CAA8C,WAAW,aAAa,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACzG,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,MAAM,GAAiC,EAAE,CAAC;IAChD,MAAM,MAAM,GAAkB;QAC5B,cAAc,EAAE,CAAC;QACjB,aAAa,EAAE,CAAC;QAChB,oBAAoB,EAAE,CAAC;QACvB,mBAAmB,EAAE,CAAC;QACtB,YAAY,EAAE,CAAC;KAChB,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;QAE3D,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,UAAU,EAAE,CAAC;YAChE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC;YACtD,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE;oBACjD,WAAW;oBACX,SAAS;oBACT,IAAI,EAAE,KAAK;oBACX,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,IAAI;iBACrC,CAAC,CAAC;gBACH,MAAM,CAAC,cAAc,IAAI,QAAQ,CAAC,cAAc,CAAC;gBACjD,MAAM,CAAC,aAAa,IAAI,QAAQ,CAAC,aAAa,CAAC;gBAC/C,MAAM,CAAC,oBAAoB,IAAI,QAAQ,CAAC,oBAAoB,CAAC;gBAC7D,MAAM,CAAC,mBAAmB,IAAI,QAAQ,CAAC,mBAAmB,CAAC;gBAC3D,MAAM,CAAC,YAAY,IAAI,QAAQ,CAAC,YAAY,CAAC;YAC/C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAChE,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,MAAM,KAAK,MAAM,EAAE,EAAE,CAAC,CAAC;gBACxF,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,mDAAmD,WAAW,WAAW,MAAM,WAAW,MAAM,IAAI,CACrG,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,sCAAsC,WAAW,SAAS,IAAI,CAAC,MAAM,GAAG;QACtE,kBAAkB,MAAM,CAAC,cAAc,kBAAkB,MAAM,CAAC,aAAa,GAAG;QAChF,wBAAwB,MAAM,CAAC,oBAAoB,wBAAwB,MAAM,CAAC,mBAAmB,GAAG;QACxG,gBAAgB,MAAM,CAAC,YAAY,WAAW,MAAM,CAAC,MAAM,OAAO,SAAS,IAAI,CAClF,CAAC;IAEF,OAAO;QACL,WAAW;QACX,aAAa,EAAE,IAAI,CAAC,MAAM;QAC1B,GAAG,MAAM;QACT,MAAM;KACP,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,+BAA+B;AAC/B,8EAA8E;AAC9E,EAAE;AACF,0EAA0E;AAC1E,+DAA+D;AAC/D,EAAE;AACF,+CAA+C;AAC/C,iEAAiE;AACjE,4FAA4F;AAC5F,EAAE;AACF,gBAAgB;AAChB,0EAA0E;AAC1E,sCAAsC;AACtC,EAAE;AACF,oCAAoC;AACpC,yEAAyE;AACzE,uEAAuE;AACvE,sBAAsB;AACtB,EAAE;AACF,4EAA4E;AAC5E,iEAAiE;AACjE,8EAA8E;AAE9E,MAAM,gCAAgC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BxC,CAAC;AAEF,MAAM,wBAAwB,GAAG;;;;;;;;;;;;;;;;;;;;;CAqBhC,CAAC;AAEF,SAAS,0BAA0B;IACjC,OAAO;QACL,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS;YAC/C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B;sEAC8D,EAC9D,EAAE,WAAW,EAAE,CAChB,CAAC;YACF,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CACb,eAAe,WAAW,8FAA8F,CACzH,CAAC;YACJ,CAAC;YACD,MAAM,MAAM,GAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAc,IAAI,EAAE,CAAC;YACnE,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAkB,CAAC;YAC3E,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACjD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC3C,IAAI,CAAC,WAAW,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CACb,eAAe,WAAW,gBAAgB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,oCAAoC,CAChG,CAAC;YACJ,CAAC;YACD,IAAI,cAAc,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;gBACnD,MAAM,IAAI,KAAK,CACb,eAAe,WAAW,uBAAuB,cAAc,SAAS,SAAS,yCAAyC,CAC3H,CAAC;YACJ,CAAC;QACH,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;YACnE,MAAM,MAAM,GAAG;gBACb,WAAW;gBACX,SAAS;gBACT,SAAS;gBACT,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAA0B,CAAC,CAAC;aACxE,CAAC;YACF,OAAO,MAAM,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;gBAC7C,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,gCAAgC,EAAE,MAAM,CAAC,CAAC;gBACzE,MAAM,cAAc,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBAE5D,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;gBAC9D,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBAEtD,MAAM,kBAAkB,GAAG,cAAc,CAAC,YAAY,CAAC;gBACvD,MAAM,eAAe,GAAG,WAAW,CAAC,YAAY,CAAC;gBACjD,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC1C,MAAM,cAAc,GAAG,kBAAkB,CAAC;gBAC1C,MAAM,aAAa,GAAG,cAAc,GAAG,cAAc,CAAC;gBAEtD,OAAO;oBACL,cAAc,EAAE,cAAc;oBAC9B,aAAa,EAAE,aAAa;oBAC5B,oBAAoB,EAAE,eAAe;oBACrC,mBAAmB,EAAE,CAAC;oBACtB,YAAY,EAAE,cAAc,CAAC,oBAAoB,GAAG,WAAW,CAAC,oBAAoB;iBACrF,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,GAA0B;IACtD,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CACb,yCAAyC,GAAG,CAAC,WAAW,mFAAmF,CAC5I,CAAC;IACJ,CAAC;IACD,OAAO;QACL,SAAS,EAAE,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;QACvC,UAAU,EAAE,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;QACzC,WAAW,EAAE,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE;QACnC,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;QAC9D,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;QACtE,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;QAC9D,WAAW,EAAE,GAAG,CAAC,WAAW;KAC7B,CAAC;AACJ,CAAC"}
@@ -6,7 +6,9 @@
6
6
  "main": "dist/index.js",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
- "start": "node dist/index.js"
9
+ "start": "node dist/index.js",
10
+ "smoke": "bash ./scripts/boot-smoke.sh",
11
+ "prepublish": "bash ./scripts/boot-smoke.sh"
10
12
  },
11
13
  "dependencies": {
12
14
  "@anthropic-ai/sdk": "^0.81.0",
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Memory MCP boot-smoke gate (Task 743).
3
+ #
4
+ # Spawns dist/index.js with a stub env, holds stdin open via `tail -f /dev/null`
5
+ # (the MCP SDK's StdioServerTransport exits on EOF — closing stdin would mask a
6
+ # clean boot as a failure), sleeps 2s, then asserts the process is still alive.
7
+ # A schema-loader throw, an import failure, or any other module-load fault
8
+ # would have killed the child by now and `kill -0` would return non-zero.
9
+ #
10
+ # Wired to the memory MCP `prepublish` script in package.json so a regression
11
+ # can never reach npm publish without the gate firing first. Local matrix:
12
+ #
13
+ # $ ./scripts/boot-smoke.sh
14
+ # [boot-smoke] memory ok
15
+ #
16
+ # $ # break the schema content, rebuild, retry
17
+ # $ ./scripts/boot-smoke.sh
18
+ # [boot-smoke] memory FAILED tail=<schema-loader throw>
19
+ # exit 1
20
+ set -euo pipefail
21
+
22
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
23
+ MCP_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
24
+ ENTRY="$MCP_DIR/dist/index.js"
25
+ PLATFORM_ROOT=$(cd "$MCP_DIR/../../.." && pwd)
26
+
27
+ if [ ! -f "$ENTRY" ]; then
28
+ echo "[boot-smoke] memory FAILED tail=\"dist/index.js missing — run npm run build first\"" >&2
29
+ exit 1
30
+ fi
31
+
32
+ LOG_DIR=$(mktemp -d -t mcp-boot-smoke.XXXXXX)
33
+ trap 'rm -rf "$LOG_DIR"' EXIT
34
+
35
+ # stdin from a `tail -f /dev/null` keeps the StdioServerTransport open. The
36
+ # pipeline's left side is a tiny long-running process; the right side is the
37
+ # MCP server we are smoke-testing. Both are killed below.
38
+ # Disable pipefail for this block — when bash forks the pipeline, $! is the
39
+ # PID of the LAST command (the node process), which is what we test below.
40
+ set +o pipefail
41
+
42
+ tail -f /dev/null | env \
43
+ ACCOUNT_ID=boot-smoke-test \
44
+ PLATFORM_ROOT="$PLATFORM_ROOT" \
45
+ LOG_DIR="$LOG_DIR" \
46
+ STREAM_LOG_PATH="$LOG_DIR/stream.log" \
47
+ NEO4J_URI=bolt://localhost:7687 \
48
+ SESSION_ID=boot-smoke \
49
+ node "$ENTRY" > /dev/null 2>"$LOG_DIR/stderr-direct.log" &
50
+
51
+ NODE_PID=$!
52
+ sleep 2
53
+
54
+ if ! kill -0 "$NODE_PID" 2>/dev/null; then
55
+ TAIL=$(tail -n 5 "$LOG_DIR/stderr-direct.log" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g')
56
+ echo "[boot-smoke] memory FAILED tail=\"$TAIL\"" >&2
57
+ # Reap the tail process by killing its parent group (the pipeline subshell).
58
+ kill 0 2>/dev/null || true
59
+ exit 1
60
+ fi
61
+
62
+ echo "[boot-smoke] memory ok"
63
+ kill "$NODE_PID" 2>/dev/null || true
64
+ # Reap the still-running `tail -f /dev/null` so the script exits cleanly.
65
+ # `disown` first so bash does not print a job-control "Terminated" line
66
+ # when the pipeline subshell is killed by exit-trap.
67
+ { disown -a 2>/dev/null; pkill -P $$ -f "tail -f /dev/null"; } 2>/dev/null || true
68
+ wait 2>/dev/null || true
69
+ exit 0
@@ -38,6 +38,28 @@ deterministic path through the shim is the only supported way to read
38
38
  the graph, and any substitute path loses the read-only + namespace +
39
39
  token-limit discipline the upstream server enforces.
40
40
 
41
+ ## When the memory tools are absent
42
+
43
+ If `mcp__memory__memory-write`, `mcp__memory__memory-search`,
44
+ `mcp__memory__memory-rank`, `mcp__memory__memory-reindex`,
45
+ `mcp__memory__session-compact`, `mcp__memory__session-compact-status`, or
46
+ `mcp__memory__conversation-search` is missing from your tool list, the
47
+ memory MCP server failed to start on this device. Reply once with
48
+ exactly:
49
+
50
+ > The memory MCP server failed to start on this device. Run the admin
51
+ > system-status check to diagnose — do not retry by other routes.
52
+
53
+ Then stop. Do not search for a similarly-named tool via `ToolSearch`, do
54
+ not improvise via `maxy-graph-read_neo4j_cypher` (it is read-only — every
55
+ write goes through the schema-aware memory tools), do not paraphrase.
56
+ A missing memory tool is a deterministic failure of the per-account
57
+ memory server: the platform's `[mcp-init-error] server=memory` line in
58
+ the per-conversation stream log already names it; the user-facing reply
59
+ exists so the operator knows the session cannot proceed without an
60
+ operator-side fix (Task 743 propagates Task 560's loud-fail contract
61
+ from graph to memory).
62
+
41
63
  ## Mutation intents
42
64
 
43
65
  The graph tools above are read-only. Every write intent has a schema-aware
@@ -165,7 +165,7 @@ The classifier returns `kind` strings from the closed enumeration above. `kind`
165
165
  | `CREATED` | `UserProfile` → `Project` | Standalone-node anchor. |
166
166
  | `UNDER` | `Project` → `Organization` | Optional Project context. |
167
167
  | `AT` | `Section:Position` → `Organization` | Position's employer. |
168
- | `PARTY` | `KnowledgeDocument` → `Person|Organization` | Contract Parties — written from `documentEdges`. |
168
+ | `PARTY` | `KnowledgeDocument` → `Person` or `Organization` | Contract Parties — written from `documentEdges`. |
169
169
  | `DEFINES` | `Section:Definitions` → `DefinedTerm` | Contract definitions — written from per-section `related`. |
170
170
 
171
171
  **Ontology-growth review query.** When a document accumulates several `:Section:Other` nodes, the operator (or admin agent) can run the following Cypher to surface candidate ontology additions:
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # Existing-pi install-log redaction (Task 744).
3
+ #
4
+ # Idempotent one-shot remediation for Pis that completed installation BEFORE
5
+ # the install-log redaction landed at index.ts:152 / setup.sh:94. Scans every
6
+ # `install-*.log` in the configured logs directory and replaces every literal
7
+ # `set-initial-password ...<secret>` payload with `set-initial-password
8
+ # [REDACTED]`. Re-running the script is safe — already-redacted lines do not
9
+ # match the source pattern, so no further edits occur.
10
+ #
11
+ # Source patterns covered:
12
+ # 1. TS installer (packages/create-maxy/src/index.ts:152) — "[ISO] > sudo
13
+ # neo4j-admin dbms set-initial-password -- <secret>" or any args after
14
+ # "set-initial-password" (positional or "--" delimited).
15
+ # 2. Shell installer (platform/scripts/setup.sh, pre-fix variant) — "+ sudo
16
+ # neo4j-admin dbms set-initial-password <secret>" if bash -x had been on.
17
+ #
18
+ # A trailing marker line `[redact-install-logs] redacted=<n> file=<path>` is
19
+ # appended to each modified log so subsequent reads can identify which logs
20
+ # went through the remediation. Files with zero matches are left untouched.
21
+ #
22
+ # Default scan location: $HOME/.maxy/logs (the installer's LOG_DIR). Override
23
+ # with --dir <path> for non-default deployments.
24
+
25
+ set -euo pipefail
26
+
27
+ LOG_DIR="${HOME}/.maxy/logs"
28
+
29
+ while [ $# -gt 0 ]; do
30
+ case "$1" in
31
+ --dir) LOG_DIR="$2"; shift 2 ;;
32
+ --help|-h)
33
+ cat <<USAGE
34
+ Usage: redact-install-logs.sh [--dir <log-dir>]
35
+
36
+ Redacts neo4j set-initial-password secrets from install-*.log files.
37
+ Default --dir: \$HOME/.maxy/logs
38
+
39
+ Idempotent — safe to re-run.
40
+ USAGE
41
+ exit 0 ;;
42
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
43
+ esac
44
+ done
45
+
46
+ if [ ! -d "$LOG_DIR" ]; then
47
+ echo "[redact-install-logs] log dir not found: $LOG_DIR (nothing to do)"
48
+ exit 0
49
+ fi
50
+
51
+ shopt -s nullglob
52
+ TOTAL_FILES=0
53
+ TOTAL_REDACTIONS=0
54
+
55
+ for f in "$LOG_DIR"/install-*.log; do
56
+ [ -f "$f" ] || continue
57
+ TOTAL_FILES=$((TOTAL_FILES + 1))
58
+
59
+ # Pattern: any "set-initial-password" line followed by one or more args.
60
+ # The replacement keeps the leading prefix (timestamp + cmd up through
61
+ # set-initial-password and an optional "--") and substitutes everything
62
+ # after with [REDACTED]. We anchor the replacement only when the remaining
63
+ # tail is non-empty AND not already "[REDACTED]" — making the script idempotent.
64
+ REDACTED_THIS_FILE=$(
65
+ perl -ne '
66
+ if (/set-initial-password(\s+--)?\s+(\S.*)$/ && $2 ne "[REDACTED]") {
67
+ print STDOUT "1\n";
68
+ }
69
+ ' "$f" | wc -l | tr -d ' '
70
+ )
71
+
72
+ if [ "$REDACTED_THIS_FILE" -gt 0 ]; then
73
+ perl -i -pe '
74
+ if (/set-initial-password(\s+--)?\s+(\S.*)$/ && $2 ne "[REDACTED]") {
75
+ s/set-initial-password(\s+--)?\s+\S.*$/set-initial-password${1} [REDACTED]/;
76
+ }
77
+ ' "$f"
78
+ printf "[redact-install-logs] redacted=%d file=%s\n" "$REDACTED_THIS_FILE" "$f" >> "$f"
79
+ echo "[redact-install-logs] redacted=$REDACTED_THIS_FILE file=$f"
80
+ TOTAL_REDACTIONS=$((TOTAL_REDACTIONS + REDACTED_THIS_FILE))
81
+ fi
82
+ done
83
+
84
+ echo "[redact-install-logs] summary files_scanned=$TOTAL_FILES total_redactions=$TOTAL_REDACTIONS"
85
+ exit 0
@@ -86,12 +86,20 @@ else
86
86
  # Configure Neo4j for local use
87
87
  sudo sed -i 's/#server.default_listen_address=0.0.0.0/server.default_listen_address=127.0.0.1/' /etc/neo4j/neo4j.conf
88
88
 
89
- # Generate a strong random password and store it
89
+ # Generate a strong random password and store it.
90
+ # Password handling block is set +x bracketed so even bash -x setup.sh
91
+ # cannot print the substituted secret. The password is written to
92
+ # platform/config/.neo4j-password (chmod 600) — the only readable source.
93
+ # set-initial-password reads the secret via $(cat ...) so the literal
94
+ # never appears on the parent shell's command line, and stdout is
95
+ # discarded so neo4j-admin's own echo cannot leak it either (Task 744).
96
+ { set +x; } 2>/dev/null
90
97
  NEO4J_GENERATED_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
91
98
  mkdir -p "$INSTALL_DIR/platform/config"
92
- echo "$NEO4J_GENERATED_PASSWORD" > "$INSTALL_DIR/platform/config/.neo4j-password"
99
+ printf '%s' "$NEO4J_GENERATED_PASSWORD" > "$INSTALL_DIR/platform/config/.neo4j-password"
93
100
  chmod 600 "$INSTALL_DIR/platform/config/.neo4j-password"
94
- sudo neo4j-admin dbms set-initial-password "$NEO4J_GENERATED_PASSWORD"
101
+ unset NEO4J_GENERATED_PASSWORD
102
+ sudo neo4j-admin dbms set-initial-password "$(cat "$INSTALL_DIR/platform/config/.neo4j-password")" >/dev/null 2>&1
95
103
 
96
104
  # Start and enable
97
105
  sudo systemctl enable neo4j
@@ -139,6 +147,15 @@ else
139
147
  cd "$INSTALL_DIR"
140
148
  fi
141
149
 
150
+ # ------------------------------------------------------------------
151
+ # 6.5. Redact install-log credential leaks (Task 744 — idempotent).
152
+ # Pre-fix logs may contain plaintext neo4j passwords; this script scrubs
153
+ # every install-*.log to "[REDACTED]". Safe on already-clean logs.
154
+ # ------------------------------------------------------------------
155
+ if [ -x "$INSTALL_DIR/platform/scripts/redact-install-logs.sh" ]; then
156
+ bash "$INSTALL_DIR/platform/scripts/redact-install-logs.sh" || true
157
+ fi
158
+
142
159
  # ------------------------------------------------------------------
143
160
  # 7. Install dependencies and build
144
161
  # ------------------------------------------------------------------