@rubytech/create-realagent 1.0.651 → 1.0.653

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 (79) hide show
  1. package/dist/index.js +115 -6
  2. package/dist/pinned-binaries.js +43 -0
  3. package/package.json +1 -1
  4. package/payload/platform/lib/graph-trash/dist/index.d.ts +91 -0
  5. package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -0
  6. package/payload/platform/lib/graph-trash/dist/index.js +238 -0
  7. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -0
  8. package/payload/platform/lib/graph-trash/src/index.ts +360 -0
  9. package/payload/platform/lib/graph-trash/tsconfig.json +8 -0
  10. package/payload/platform/neo4j/schema.cypher +19 -0
  11. package/payload/platform/package.json +2 -2
  12. package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +19 -4
  13. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +162 -63
  14. package/payload/platform/plugins/contacts/mcp/dist/index.js +9 -4
  15. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.d.ts +16 -1
  17. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.d.ts.map +1 -1
  18. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.js +23 -10
  19. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-delete.js.map +1 -1
  20. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts.map +1 -1
  21. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js +2 -1
  22. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js.map +1 -1
  23. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -1
  24. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +8 -4
  25. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -1
  26. package/payload/platform/plugins/docs/references/deployment.md +1 -1
  27. package/payload/platform/plugins/docs/references/platform.md +3 -1
  28. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  29. package/payload/platform/plugins/docs/references/troubleshooting.md +43 -0
  30. package/payload/platform/plugins/memory/PLUGIN.md +4 -2
  31. package/payload/platform/plugins/memory/mcp/dist/index.js +92 -28
  32. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +30 -15
  34. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +46 -84
  36. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +22 -0
  38. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -0
  39. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +36 -0
  40. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -0
  41. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +16 -0
  43. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.d.ts +2 -0
  45. package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.d.ts.map +1 -1
  46. package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.js +42 -3
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-list-attachments.js.map +1 -1
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts +24 -0
  49. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts.map +1 -0
  50. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +40 -0
  51. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -0
  52. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +35 -10
  54. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  55. package/payload/platform/plugins/memory/references/graph-primitives.md +49 -3
  56. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +4 -0
  57. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  58. package/payload/platform/scripts/lib/resolve-account-dir.sh +166 -0
  59. package/payload/platform/scripts/seed-neo4j.sh +7 -11
  60. package/payload/platform/templates/systemd/maxy-ttyd.service +6 -1
  61. package/payload/server/public/assets/{admin-S2KHPNe4.js → admin-DLp3geZN.js} +4 -4
  62. package/payload/server/public/assets/{data-C-b1pXeA.js → data-DPqrIvgB.js} +1 -1
  63. package/payload/server/public/assets/{file-CRrDfnO1.js → file-Buaz89w8.js} +1 -1
  64. package/payload/server/public/assets/{graph-CSBMZGGe.js → graph-B_TxtKJP.js} +2 -2
  65. package/payload/server/public/assets/{house-D1CBraxB.js → house-B5wS-2kc.js} +1 -1
  66. package/payload/server/public/assets/jsx-runtime-ChVPhhAG.css +1 -0
  67. package/payload/server/public/assets/{public-ukz9-gSe.js → public-BNEciseE.js} +1 -1
  68. package/payload/server/public/assets/{share-2-DkF8neDM.js → share-2-Z5v9aWZ2.js} +1 -1
  69. package/payload/server/public/assets/{trash-2-Dde58AAR.js → trash-2-BaLFnigq.js} +1 -1
  70. package/payload/server/public/assets/{useVoiceRecorder-Hz9X6luB.js → useVoiceRecorder-CgMo3FDt.js} +1 -1
  71. package/payload/server/public/assets/x-DYxtrMFK.js +1 -0
  72. package/payload/server/public/data.html +7 -7
  73. package/payload/server/public/graph.html +6 -6
  74. package/payload/server/public/index.html +8 -8
  75. package/payload/server/public/public.html +5 -5
  76. package/payload/server/server.js +33 -0
  77. package/payload/server/public/assets/jsx-runtime-CZtLX8NN.css +0 -1
  78. package/payload/server/public/assets/x-D52qhSya.js +0 -1
  79. /package/payload/server/public/assets/{jsx-runtime-Cb-WunFZ.js → jsx-runtime-WYScGBOd.js} +0 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Soft-delete primitive for the knowledge graph.
3
+ *
4
+ * MCP-tool callers must NEVER `DETACH DELETE` user-domain nodes. Use
5
+ * `trashNode` to mark a node `:Trashed`; `restoreNode` removes the label;
6
+ * `emptyTrash` hard-deletes nodes whose `trashedAt < now - graceDays`. The
7
+ * 2026-04-20 incident wiped 19 nodes via a single autonomous `DETACH DELETE`
8
+ * — Neo4j Community has no PITR, so properties were unrecoverable. This
9
+ * primitive contains the blast radius for every caller.
10
+ *
11
+ * Unique-constraint handling: when the trashed node's labels carry single-
12
+ * key UNIQUE constraints (e.g. `OnboardingState.accountId`), the live values
13
+ * are snapshotted into `_trashedKeys` (JSON) and nulled on the node, so
14
+ * MERGE against the same key won't collide. `restoreNode` writes them back
15
+ * and fails loudly when an active node already occupies the slot.
16
+ */
17
+
18
+ import type { Session } from "neo4j-driver";
19
+
20
+ /**
21
+ * Single-key UNIQUE properties per label, mirroring `platform/neo4j/schema.cypher`.
22
+ *
23
+ * Composite UNIQUEs (UserProfile, Email composite, AccessGrant) are
24
+ * represented by a single nullable component — Neo4j enforces a composite
25
+ * constraint only when every component is non-null, so nulling one frees it.
26
+ */
27
+ const UNIQUE_KEYS_BY_LABEL: Record<string, string[]> = {
28
+ Person: ["email", "telephone"],
29
+ Service: ["serviceId"],
30
+ LocalBusiness: ["accountId"],
31
+ Task: ["taskId"],
32
+ Event: ["eventId"],
33
+ KnowledgeDocument: ["attachmentId"],
34
+ DigitalDocument: ["attachmentId"],
35
+ Conversation: ["conversationId", "sessionKey"],
36
+ Message: ["messageId"],
37
+ OnboardingState: ["accountId"],
38
+ Workflow: ["workflowId"],
39
+ WorkflowStep: ["stepId"],
40
+ WorkflowRun: ["runId"],
41
+ Preference: ["preferenceId"],
42
+ Email: ["emailId", "messageId"],
43
+ AdminUser: ["userId"],
44
+ ToolCall: ["callId"],
45
+ // Composite component nulls — frees the composite constraint:
46
+ AccessGrant: ["contactValue"], // composite (contactValue, agentSlug, accountId)
47
+ UserProfile: ["userId"], // composite (accountId, userId)
48
+ };
49
+
50
+ const TRASH_PROP_NAMES = [
51
+ "trashedAt",
52
+ "trashedBy",
53
+ "trashReason",
54
+ "_trashedKeys",
55
+ ] as const;
56
+
57
+ export interface TrashParams {
58
+ session: Session;
59
+ accountId: string;
60
+ elementId: string;
61
+ /** Provenance marker — appears verbatim in `trashedBy` and `[trash:marked] by=`. */
62
+ by: string;
63
+ reason?: string;
64
+ }
65
+
66
+ export interface TrashResult {
67
+ trashed: boolean;
68
+ alreadyTrashed: boolean;
69
+ nodeId: string;
70
+ /** Labels excluding `:Trashed`. */
71
+ labels: string[];
72
+ trashedAt: string;
73
+ originalKeys: Record<string, unknown>;
74
+ }
75
+
76
+ export async function trashNode(params: TrashParams): Promise<TrashResult> {
77
+ const { session, accountId, elementId, by, reason } = params;
78
+
79
+ const lookup = await session.run(
80
+ `MATCH (n) WHERE elementId(n) = $eid AND n.accountId = $accountId
81
+ RETURN labels(n) AS labels, properties(n) AS props`,
82
+ { eid: elementId, accountId },
83
+ );
84
+ if (lookup.records.length === 0) {
85
+ throw new Error(
86
+ `trashNode: node not found (elementId=${elementId} accountId=${accountId.slice(0, 8)}…)`,
87
+ );
88
+ }
89
+
90
+ const allLabels = lookup.records[0].get("labels") as string[];
91
+ const props = lookup.records[0].get("props") as Record<string, unknown>;
92
+ const baseLabels = allLabels.filter((l) => l !== "Trashed");
93
+
94
+ if (allLabels.includes("Trashed")) {
95
+ return {
96
+ trashed: false,
97
+ alreadyTrashed: true,
98
+ nodeId: elementId,
99
+ labels: baseLabels,
100
+ trashedAt: String(props.trashedAt ?? ""),
101
+ originalKeys: {},
102
+ };
103
+ }
104
+
105
+ const uniqueKeys = new Set<string>();
106
+ for (const label of baseLabels) {
107
+ for (const key of UNIQUE_KEYS_BY_LABEL[label] ?? []) uniqueKeys.add(key);
108
+ }
109
+
110
+ const originalKeys: Record<string, unknown> = {};
111
+ for (const k of uniqueKeys) {
112
+ if (props[k] !== undefined && props[k] !== null) originalKeys[k] = props[k];
113
+ }
114
+
115
+ const setNullClauses = Object.keys(originalKeys)
116
+ .map((k) => `n.\`${k}\` = null`)
117
+ .join(", ");
118
+ const setNullSuffix = setNullClauses ? `, ${setNullClauses}` : "";
119
+
120
+ const trashedAt = new Date().toISOString();
121
+ await session.run(
122
+ `MATCH (n) WHERE elementId(n) = $eid
123
+ SET n:Trashed,
124
+ n.trashedAt = datetime($trashedAt),
125
+ n.trashedBy = $by,
126
+ n.trashReason = $reason,
127
+ n._trashedKeys = $trashedKeysJson${setNullSuffix}`,
128
+ {
129
+ eid: elementId,
130
+ trashedAt,
131
+ by,
132
+ reason: reason ?? null,
133
+ trashedKeysJson: JSON.stringify(originalKeys),
134
+ },
135
+ );
136
+
137
+ process.stderr.write(
138
+ `[trash:marked] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")} by=${by} reason=${reason ?? "null"}\n`,
139
+ );
140
+
141
+ return {
142
+ trashed: true,
143
+ alreadyTrashed: false,
144
+ nodeId: elementId,
145
+ labels: baseLabels,
146
+ trashedAt,
147
+ originalKeys,
148
+ };
149
+ }
150
+
151
+ export interface RestoreParams {
152
+ session: Session;
153
+ accountId: string;
154
+ elementId: string;
155
+ }
156
+
157
+ export interface RestoreResult {
158
+ restored: boolean;
159
+ nodeId: string;
160
+ labels: string[];
161
+ restoredKeys: Record<string, unknown>;
162
+ }
163
+
164
+ export async function restoreNode(params: RestoreParams): Promise<RestoreResult> {
165
+ const { session, accountId, elementId } = params;
166
+
167
+ const lookup = await session.run(
168
+ `MATCH (n:Trashed) WHERE elementId(n) = $eid
169
+ RETURN labels(n) AS labels, n._trashedKeys AS keysJson`,
170
+ { eid: elementId },
171
+ );
172
+ if (lookup.records.length === 0) {
173
+ throw new Error(
174
+ `restoreNode: trashed node not found (elementId=${elementId})`,
175
+ );
176
+ }
177
+
178
+ const allLabels = lookup.records[0].get("labels") as string[];
179
+ const baseLabels = allLabels.filter((l) => l !== "Trashed");
180
+ const keysJson = lookup.records[0].get("keysJson") as string | null;
181
+ const originalKeys: Record<string, unknown> = keysJson ? JSON.parse(keysJson) : {};
182
+
183
+ // Conflict check: an active node already holds the unique slot we want back?
184
+ for (const label of baseLabels) {
185
+ const uniqueKeys = UNIQUE_KEYS_BY_LABEL[label] ?? [];
186
+ for (const k of uniqueKeys) {
187
+ const v = originalKeys[k];
188
+ if (v === undefined || v === null) continue;
189
+ const conflict = await session.run(
190
+ `MATCH (other:\`${label}\`)
191
+ WHERE elementId(other) <> $eid
192
+ AND NOT other:Trashed
193
+ AND other.\`${k}\` = $val
194
+ RETURN elementId(other) AS otherId LIMIT 1`,
195
+ { eid: elementId, val: v },
196
+ );
197
+ if (conflict.records.length > 0) {
198
+ const otherId = conflict.records[0].get("otherId") as string;
199
+ throw new Error(
200
+ `restoreNode: cannot restore ${label} elementId=${elementId} — active node elementId=${otherId} already holds ${k}=${JSON.stringify(v)}`,
201
+ );
202
+ }
203
+ }
204
+ }
205
+
206
+ const setClauses = Object.keys(originalKeys)
207
+ .map((k) => `n.\`${k}\` = $val_${k}`)
208
+ .join(", ");
209
+ const setSuffix = setClauses ? `, ${setClauses}` : "";
210
+ const setParams: Record<string, unknown> = { eid: elementId };
211
+ for (const [k, v] of Object.entries(originalKeys)) setParams[`val_${k}`] = v;
212
+
213
+ await session.run(
214
+ `MATCH (n:Trashed) WHERE elementId(n) = $eid
215
+ REMOVE n:Trashed, n.trashedAt, n.trashedBy, n.trashReason, n._trashedKeys
216
+ SET n.restoredAt = datetime()${setSuffix}`,
217
+ setParams,
218
+ );
219
+
220
+ process.stderr.write(
221
+ `[trash:restored] accountId=${accountId} elementId=${elementId} labels=${baseLabels.join(",")}\n`,
222
+ );
223
+
224
+ return {
225
+ restored: true,
226
+ nodeId: elementId,
227
+ labels: baseLabels,
228
+ restoredKeys: originalKeys,
229
+ };
230
+ }
231
+
232
+ export interface EmptyTrashParams {
233
+ session: Session;
234
+ accountId: string;
235
+ /** Default 30. */
236
+ graceDays?: number;
237
+ dryRun?: boolean;
238
+ /** Optional label whitelist — only nodes carrying any of these labels are eligible. */
239
+ labels?: string[];
240
+ /** Side-effect callback invoked per emptied node, e.g. to clean disk attachments. */
241
+ onEmpty?: (node: TrashCandidate) => Promise<void>;
242
+ }
243
+
244
+ export interface TrashCandidate {
245
+ elementId: string;
246
+ labels: string[];
247
+ trashedAt: string;
248
+ trashedBy: string | null;
249
+ /** Original unique-key snapshot — useful for KnowledgeDocument disk cleanup. */
250
+ trashedKeys: Record<string, unknown>;
251
+ }
252
+
253
+ export interface EmptyTrashResult {
254
+ graceDays: number;
255
+ dryRun: boolean;
256
+ candidates: TrashCandidate[];
257
+ emptied: number;
258
+ }
259
+
260
+ export async function emptyTrash(params: EmptyTrashParams): Promise<EmptyTrashResult> {
261
+ const t0 = Date.now();
262
+ const { session, accountId, graceDays = 30, dryRun = false, labels: labelFilter, onEmpty } = params;
263
+
264
+ const cutoff = new Date(Date.now() - graceDays * 24 * 60 * 60 * 1000).toISOString();
265
+
266
+ const labelClause = labelFilter && labelFilter.length > 0
267
+ ? `AND ANY(l IN labels(n) WHERE l IN $labelFilter)`
268
+ : "";
269
+
270
+ const candidatesResult = await session.run(
271
+ `MATCH (n:Trashed)
272
+ WHERE n.trashedAt < datetime($cutoff)
273
+ ${labelClause}
274
+ RETURN elementId(n) AS eid,
275
+ labels(n) AS labels,
276
+ toString(n.trashedAt) AS trashedAt,
277
+ n.trashedBy AS trashedBy,
278
+ n._trashedKeys AS keysJson
279
+ ORDER BY n.trashedAt ASC`,
280
+ { cutoff, ...(labelFilter ? { labelFilter } : {}) },
281
+ );
282
+
283
+ const candidates: TrashCandidate[] = candidatesResult.records.map((r) => {
284
+ const keysJson = r.get("keysJson") as string | null;
285
+ return {
286
+ elementId: r.get("eid") as string,
287
+ labels: (r.get("labels") as string[]).filter((l) => l !== "Trashed"),
288
+ trashedAt: String(r.get("trashedAt")),
289
+ trashedBy: (r.get("trashedBy") as string | null) ?? null,
290
+ trashedKeys: keysJson ? JSON.parse(keysJson) : {},
291
+ };
292
+ });
293
+
294
+ process.stderr.write(
295
+ `[trash:empty-run] accountId=${accountId} graceDays=${graceDays} dryRun=${dryRun} candidates=${candidates.length}\n`,
296
+ );
297
+
298
+ if (dryRun || candidates.length === 0) {
299
+ process.stderr.write(
300
+ `[graph:trash:summary] accountId=${accountId} trashedNow=0 emptiedThisRun=0 graceDays=${graceDays} dryRun=${dryRun} elapsedMs=${Date.now() - t0}\n`,
301
+ );
302
+ return { graceDays, dryRun, candidates, emptied: 0 };
303
+ }
304
+
305
+ let emptied = 0;
306
+ for (const c of candidates) {
307
+ if (onEmpty) {
308
+ try {
309
+ await onEmpty(c);
310
+ } catch (err) {
311
+ process.stderr.write(
312
+ `[trash:empty-run] onEmpty side-effect failed for elementId=${c.elementId}: ${err instanceof Error ? err.message : String(err)}\n`,
313
+ );
314
+ }
315
+ }
316
+
317
+ await session.run(
318
+ `MATCH (n) WHERE elementId(n) = $eid DETACH DELETE n`,
319
+ { eid: c.elementId },
320
+ );
321
+
322
+ const ageDays = Math.floor((Date.now() - new Date(c.trashedAt).getTime()) / (24 * 60 * 60 * 1000));
323
+ process.stderr.write(
324
+ `[trash:emptied] accountId=${accountId} elementId=${c.elementId} labels=${c.labels.join(",")} trashedAt=${c.trashedAt} ageDays=${ageDays}\n`,
325
+ );
326
+ emptied++;
327
+ }
328
+
329
+ process.stderr.write(
330
+ `[graph:trash:summary] accountId=${accountId} trashedNow=0 emptiedThisRun=${emptied} graceDays=${graceDays} dryRun=${dryRun} elapsedMs=${Date.now() - t0}\n`,
331
+ );
332
+
333
+ return { graceDays, dryRun, candidates, emptied };
334
+ }
335
+
336
+ /**
337
+ * Read-filter clause excluding trashed nodes for the given Cypher alias.
338
+ *
339
+ * Filters both the `:Trashed` label (Task 576 primitive) and the legacy
340
+ * `deletedAt` property (KnowledgeDocument soft-delete pre-Task 576). The
341
+ * `deletedAt` arm is a transitional belt-and-braces — production graphs may
342
+ * still hold pre-migration soft-deletes — and stays until those are confirmed
343
+ * empty, then is dropped.
344
+ *
345
+ * notTrashed("node") → "(NOT node:Trashed AND node.deletedAt IS NULL)"
346
+ * notTrashed("related") → "(NOT related:Trashed AND related.deletedAt IS NULL)"
347
+ */
348
+ export function notTrashed(alias: string): string {
349
+ return `(NOT \`${alias}\`:Trashed AND \`${alias}\`.deletedAt IS NULL)`;
350
+ }
351
+
352
+ /** Runtime accessor for the static unique-keys map (for tests + sibling tooling). */
353
+ export function uniqueKeysForLabels(labels: string[]): string[] {
354
+ const out = new Set<string>();
355
+ for (const l of labels) for (const k of UNIQUE_KEYS_BY_LABEL[l] ?? []) out.add(k);
356
+ return [...out];
357
+ }
358
+
359
+ /** Property names trashNode writes — exposed so callers can re-MERGE without colliding. */
360
+ export const TRASH_METADATA_PROPS: readonly string[] = TRASH_PROP_NAMES;
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -613,3 +613,22 @@ FOR (tc:ToolCall) REQUIRE tc.callId IS UNIQUE;
613
613
 
614
614
  CREATE INDEX tool_call_account_started IF NOT EXISTS
615
615
  FOR (tc:ToolCall) ON (tc.accountId, tc.startedAt);
616
+
617
+ // ----------------------------------------------------------
618
+ // :Trashed — Task 576 soft-delete primitive.
619
+ //
620
+ // Any node soft-deleted via memory-delete / contact-delete carries this
621
+ // label plus trashedAt / trashedBy / trashReason / _trashedKeys properties.
622
+ // memory-empty-trash hard-deletes by `MATCH (n:Trashed) WHERE n.trashedAt < cutoff`,
623
+ // and the trash-census + read-filter probes in memory-search hit the same
624
+ // label. The accountId index supports per-tenant census reads — accountId-as-
625
+ // unique-key labels (LocalBusiness, OnboardingState) have their accountId
626
+ // nulled on trash and are missed by accountId-scoped reads, but those are
627
+ // 1-per-account labels and rare in trash.
628
+ // ----------------------------------------------------------
629
+
630
+ CREATE INDEX trashed_account IF NOT EXISTS
631
+ FOR (n:Trashed) ON (n.accountId);
632
+
633
+ CREATE INDEX trashed_at IF NOT EXISTS
634
+ FOR (n:Trashed) ON (n.trashedAt);
@@ -6,8 +6,8 @@
6
6
  "plugins/*/mcp"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -6,13 +6,28 @@
6
6
  # helpers to emit phase lines and tee subprocess output into the same
7
7
  # per-conversation file the chat UI's server-side tailer reads.
8
8
  #
9
- # Contract (read by platform/ui/app/api/admin/chat/route.ts tailer and
9
+ # Contract (read by platform/ui/app/lib/script-stream-tailer.ts tailer and
10
10
  # .docs/platform.md):
11
11
  # [<ISO-ts>] [<scope>] <kv …>
12
12
  # [<ISO-ts>] [<scope>:<subprocess-tag>] <raw line>
13
- # where <scope> {setup-tunnel, reset-tunnel}. The tailer regex is
14
- # ^\[[^]]+\] \[(setup-tunnel|reset-tunnel)(:[^]]+)?\]
15
- # so any prefix change must be made on both sides atomically.
13
+ # Canonical regex SCRIPT_STREAM_RE at platform/ui/app/lib/script-stream-tailer.ts:51:
14
+ # ^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$
15
+ # <scope> is any `[a-z][a-z0-9-]*` token (Task 592 generalised from the
16
+ # pre-592 enum `setup-tunnel|reset-tunnel`, which silently filtered out the
17
+ # `[list-cf-domains]` lines Task 589 emitted). <subprocess-tag> may contain
18
+ # lowercase, digits, `-`, `_`, `:`. Adding a new <scope> requires no edit to
19
+ # the regex — the shape is the only contract. Any prefix-shape change must
20
+ # be made on both sides (this helper + script-stream-tailer.ts) atomically.
21
+ #
22
+ # Inner layers (e.g. a node/python helper a .sh wrapper spawns — Task 598's
23
+ # `list-cf-domains.sh` → `list-cf-domains.ts` pattern) must write phase lines
24
+ # directly to STREAM_LOG_PATH with the same prefix shape: stderr alone is
25
+ # silently discarded by runFormSpawn on exit 0. The build-gate
26
+ # `platform/ui/scripts/check-stream-log-contract.mjs` (Task 600) enforces this
27
+ # by rejecting any .sh under `platform/plugins/*/scripts/` that sources this
28
+ # helper and invokes an interpreter subprocess whose target does not pair a
29
+ # STREAM_LOG_PATH env read with an append/write call. Opt out per invocation
30
+ # with `# stream-log-contract: stderr-only (reason: <prose>)`.
16
31
 
17
32
  # Exit 1 loudly with the variable name and the invoking scope so direct-SSH
18
33
  # invocations fail fast and the operator reads exactly what to set. No