@rubytech/create-maxy 1.0.881 → 1.0.884

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 (67) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-mcp/dist/index.js +45 -0
  3. package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
  4. package/payload/platform/lib/graph-mcp/src/index.ts +47 -0
  5. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +57 -9
  6. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -1
  7. package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts +26 -0
  8. package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts.map +1 -0
  9. package/payload/platform/lib/graph-write/dist/conversation-provenance.js +81 -0
  10. package/payload/platform/lib/graph-write/dist/conversation-provenance.js.map +1 -0
  11. package/payload/platform/lib/graph-write/dist/index.d.ts +38 -16
  12. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  13. package/payload/platform/lib/graph-write/dist/index.js +75 -35
  14. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  15. package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +59 -9
  16. package/payload/platform/lib/graph-write/src/conversation-provenance.ts +140 -0
  17. package/payload/platform/lib/graph-write/src/index.ts +76 -35
  18. package/payload/platform/lib/mcp-eager/dist/index.d.ts +61 -0
  19. package/payload/platform/lib/mcp-eager/dist/index.d.ts.map +1 -0
  20. package/payload/platform/lib/mcp-eager/dist/index.js +49 -0
  21. package/payload/platform/lib/mcp-eager/dist/index.js.map +1 -0
  22. package/payload/platform/lib/mcp-eager/src/index.ts +78 -0
  23. package/payload/platform/lib/mcp-eager/tsconfig.json +8 -0
  24. package/payload/platform/package.json +2 -2
  25. package/payload/platform/plugins/admin/mcp/dist/__tests__/plugin-read-skill-resolution.test.js +26 -2
  26. package/payload/platform/plugins/admin/mcp/dist/__tests__/plugin-read-skill-resolution.test.js.map +1 -1
  27. package/payload/platform/plugins/admin/mcp/dist/index.js +36 -33
  28. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  29. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts +5 -1
  30. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
  31. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +24 -8
  32. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
  33. package/payload/platform/plugins/contacts/PLUGIN.md +8 -0
  34. package/payload/platform/plugins/contacts/mcp/dist/index.js +10 -9
  35. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  36. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
  37. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +17 -2
  38. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
  39. package/payload/platform/plugins/docs/references/internals.md +15 -2
  40. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
  41. package/payload/platform/plugins/email/mcp/dist/index.js +10 -9
  42. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  43. package/payload/platform/plugins/memory/PLUGIN.md +5 -3
  44. package/payload/platform/plugins/memory/mcp/dist/index.js +10 -9
  45. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  46. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +18 -1
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  49. package/payload/platform/plugins/scheduling/mcp/dist/index.js +9 -8
  50. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  51. package/payload/platform/plugins/tasks/mcp/dist/index.js +15 -14
  52. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  53. package/payload/platform/plugins/telegram/mcp/dist/index.js +4 -3
  54. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  55. package/payload/platform/plugins/workflows/mcp/dist/index.js +9 -8
  56. package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
  57. package/payload/platform/scripts/__tests__/logs-read-prefix.sh +341 -0
  58. package/payload/platform/scripts/logs-read.sh +108 -41
  59. package/payload/platform/scripts/logs-read.test.sh +6 -2
  60. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  61. package/payload/premium-plugins/real-agency/BUNDLE.md +1 -1
  62. package/payload/server/chunk-5PQU2HW2.js +11672 -0
  63. package/payload/server/chunk-ECAQVMRA.js +759 -0
  64. package/payload/server/chunk-K7S5T4VG.js +11534 -0
  65. package/payload/server/cloudflare-task-tracker-JNZXLW32.js +22 -0
  66. package/payload/server/maxy-edge.js +2 -2
  67. package/payload/server/server.js +38 -6
@@ -0,0 +1,759 @@
1
+ import {
2
+ getSession
3
+ } from "./chunk-DOIAYD3J.js";
4
+ import {
5
+ __commonJS,
6
+ __toESM
7
+ } from "./chunk-JSBRDJBE.js";
8
+
9
+ // ../lib/graph-write/dist/audit.js
10
+ var require_audit = __commonJS({
11
+ "../lib/graph-write/dist/audit.js"(exports) {
12
+ "use strict";
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.auditCypherWrite = auditCypherWrite;
15
+ exports.formatAuditLine = formatAuditLine;
16
+ var EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
17
+ var CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
18
+ var PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
19
+ function stripStringLiterals(cypher) {
20
+ return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
21
+ }
22
+ function extractEdgeTypes(cleaned) {
23
+ const out = /* @__PURE__ */ new Set();
24
+ for (const m of cleaned.matchAll(EDGE_PATTERN)) {
25
+ for (const t of m[1].split("|")) {
26
+ const clean = t.trim();
27
+ if (clean)
28
+ out.add(clean);
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ function countCreateOrMergeNodes(cleaned) {
34
+ const matches = cleaned.match(CREATE_OR_MERGE_NODE);
35
+ return matches ? matches.length : 0;
36
+ }
37
+ function countProvenanceStamps(cleaned) {
38
+ const matches = cleaned.match(PROVENANCE_TOKEN);
39
+ return matches ? matches.length : 0;
40
+ }
41
+ function auditCypherWrite(input) {
42
+ const warnings = [];
43
+ const cleaned = stripStringLiterals(input.cypher);
44
+ const referencedTypes = extractEdgeTypes(cleaned);
45
+ for (const t of referencedTypes) {
46
+ if (!input.schema.relationshipTypes.has(t)) {
47
+ warnings.push({ kind: "unknown-type-warning", type: t });
48
+ }
49
+ }
50
+ if (input.nodesCreated > 0) {
51
+ const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
52
+ if (createOrMergeNodes > 0) {
53
+ const stamps = countProvenanceStamps(cleaned);
54
+ if (stamps < createOrMergeNodes) {
55
+ warnings.push({
56
+ kind: "missing-provenance-warning",
57
+ created: createOrMergeNodes,
58
+ stamped: stamps
59
+ });
60
+ }
61
+ }
62
+ }
63
+ if (input.orphanIds.length > 0) {
64
+ warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
65
+ }
66
+ return warnings;
67
+ }
68
+ function formatAuditLine(line) {
69
+ const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
70
+ switch (line.kind) {
71
+ case "accepted":
72
+ return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
73
+ case "orphan-warning":
74
+ return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
75
+ case "unknown-type-warning":
76
+ return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
77
+ case "missing-provenance-warning":
78
+ return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
79
+ }
80
+ }
81
+ }
82
+ });
83
+
84
+ // ../lib/graph-write/dist/conversation-provenance.js
85
+ var require_conversation_provenance = __commonJS({
86
+ "../lib/graph-write/dist/conversation-provenance.js"(exports) {
87
+ "use strict";
88
+ Object.defineProperty(exports, "__esModule", { value: true });
89
+ exports.injectConversationProvenance = injectConversationProvenance;
90
+ var index_js_1 = require_dist();
91
+ async function injectConversationProvenance(params) {
92
+ const { session, relationships, accountId, writeLabels, conversationNodeId, logNamespace, tool } = params;
93
+ const original = [...relationships];
94
+ if (!writeLabels.some((l) => index_js_1.ACTION_PROVENANCE_LABELS.has(l)))
95
+ return original;
96
+ if (!conversationNodeId)
97
+ return original;
98
+ if (hasInboundProducedEdge(original))
99
+ return original;
100
+ let lookup;
101
+ try {
102
+ lookup = await session.run(`MATCH (n) WHERE elementId(n) = $id RETURN labels(n) AS labels, n.accountId AS accountId LIMIT 1`, { id: conversationNodeId });
103
+ } catch (err) {
104
+ process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=driver-error message=${err instanceof Error ? err.message : String(err)} conversationNodeId=${conversationNodeId}
105
+ `);
106
+ return original;
107
+ }
108
+ if (lookup.records.length === 0) {
109
+ process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=node-not-found conversationNodeId=${conversationNodeId}
110
+ `);
111
+ return original;
112
+ }
113
+ const labels = lookup.records[0].get("labels");
114
+ const sourceAccountId = lookup.records[0].get("accountId");
115
+ const sourceLabel = labels.find((l) => index_js_1.PROVENANCE_SOURCE_LABELS.has(l));
116
+ if (!sourceLabel) {
117
+ process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=wrong-source-label labels=${labels.join(",")} conversationNodeId=${conversationNodeId}
118
+ `);
119
+ return original;
120
+ }
121
+ if (sourceAccountId !== accountId) {
122
+ process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=account-mismatch sourceAccountId=${sourceAccountId.slice(0, 8)} writeAccountId=${accountId.slice(0, 8)} conversationNodeId=${conversationNodeId}
123
+ `);
124
+ return original;
125
+ }
126
+ process.stderr.write(`[${logNamespace}] [provenance-inject] tool=${tool} from=${sourceLabel}:${conversationNodeId}
127
+ `);
128
+ return [
129
+ {
130
+ type: "PRODUCED",
131
+ direction: "incoming",
132
+ targetNodeId: conversationNodeId
133
+ },
134
+ ...original
135
+ ];
136
+ }
137
+ function hasInboundProducedEdge(relationships) {
138
+ return relationships.some((r) => r.type === "PRODUCED" && r.direction === "incoming");
139
+ }
140
+ }
141
+ });
142
+
143
+ // ../lib/graph-write/dist/index.js
144
+ var require_dist = __commonJS({
145
+ "../lib/graph-write/dist/index.js"(exports) {
146
+ "use strict";
147
+ var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
148
+ if (k2 === void 0) k2 = k;
149
+ var desc = Object.getOwnPropertyDescriptor(m, k);
150
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
151
+ desc = { enumerable: true, get: function() {
152
+ return m[k];
153
+ } };
154
+ }
155
+ Object.defineProperty(o, k2, desc);
156
+ }) : (function(o, m, k, k2) {
157
+ if (k2 === void 0) k2 = k;
158
+ o[k2] = m[k];
159
+ }));
160
+ var __exportStar = exports && exports.__exportStar || function(m, exports2) {
161
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
162
+ };
163
+ Object.defineProperty(exports, "__esModule", { value: true });
164
+ exports.PROVENANCE_SOURCE_LABELS = exports.ACTION_PROVENANCE_LABELS = void 0;
165
+ exports.stampCreatedBy = stampCreatedBy;
166
+ exports.writeNodeWithEdges = writeNodeWithEdges2;
167
+ __exportStar(require_audit(), exports);
168
+ __exportStar(require_conversation_provenance(), exports);
169
+ exports.ACTION_PROVENANCE_LABELS = /* @__PURE__ */ new Set([
170
+ "Person",
171
+ "UserProfile",
172
+ "AdminUser",
173
+ "Organization",
174
+ "LocalBusiness",
175
+ "CloudflareTunnel",
176
+ "CloudflareHostname"
177
+ ]);
178
+ exports.PROVENANCE_SOURCE_LABELS = /* @__PURE__ */ new Set([
179
+ "Task",
180
+ "Conversation",
181
+ "Message"
182
+ ]);
183
+ function requiresActionProvenance(labels) {
184
+ for (const label of labels) {
185
+ if (exports.ACTION_PROVENANCE_LABELS.has(label))
186
+ return true;
187
+ }
188
+ return false;
189
+ }
190
+ function findProvenanceCandidates(relationships) {
191
+ return relationships.filter((r) => r.type === "PRODUCED" && r.direction === "incoming");
192
+ }
193
+ function stampCreatedBy(props, createdBy) {
194
+ return {
195
+ ...props,
196
+ createdByAgent: createdBy.agent ?? "unknown",
197
+ createdBySession: createdBy.session ?? "unknown",
198
+ createdByTool: createdBy.tool ?? null,
199
+ createdBySource: createdBy.source ?? null
200
+ };
201
+ }
202
+ async function writeNodeWithEdges2(params) {
203
+ const { session, labels, props, relationships, createdBy } = params;
204
+ const agentLabel = createdBy.agent ?? createdBy.source ?? "unknown";
205
+ const labelCsv = labels.join(",");
206
+ const isSystemBootstrap = (createdBy.agent ?? "") === "system";
207
+ if (!isSystemBootstrap) {
208
+ const accountId = props.accountId;
209
+ const expectedAccountId = params.expectedAccountId ?? process.env.ACCOUNT_ID;
210
+ if (typeof accountId !== "string" || !expectedAccountId || accountId !== expectedAccountId) {
211
+ const slice = typeof accountId === "string" ? accountId.slice(0, 8) : "missing";
212
+ process.stderr.write(`[graph-write] reject reason=invalid-account-id accountId=${slice} writer=${agentLabel}
213
+ `);
214
+ throw new Error(`Write doctrine violated: invalid-account-id (${slice}) \u2014 accountId must equal ACCOUNT_ID set by the spawning process. See .docs/neo4j.md "Account isolation invariant".`);
215
+ }
216
+ }
217
+ const reviewDigestActionTool = typeof props.actionTool === "string" && props.actionTool === "review-digest-compose";
218
+ if (labels.includes("ReviewAlert") || reviewDigestActionTool) {
219
+ const actionToolField = reviewDigestActionTool ? "review-digest-compose" : "n/a";
220
+ process.stderr.write(`[graph-write] reject reason=removed-feature labels=${labelCsv} actionTool=${actionToolField} agent=${agentLabel}
221
+ `);
222
+ throw new Error("Write doctrine violated: review-detector feature removed (Task 884) \u2014 `:ReviewAlert` and `:Event {actionTool:'review-digest-compose'}` writes are not allowed.");
223
+ }
224
+ if (!relationships || relationships.length < 1) {
225
+ process.stderr.write(`[graph-write] reject reason=zero-relationships labels=${labelCsv} agent=${agentLabel}
226
+ `);
227
+ throw new Error("Write doctrine violated: a node must be created with at least one relationship. See .docs/neo4j.md (Write doctrine).");
228
+ }
229
+ const labelStr = labels.map((l) => `\`${l.replace(/`/g, "")}\``).join(":");
230
+ const nodeProps = stampCreatedBy(props, createdBy);
231
+ return await session.executeWrite(async (tx) => {
232
+ const targetIds = relationships.map((r) => r.targetNodeId);
233
+ const check = await tx.run(`UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN elementId(t) AS id, labels(t) AS labels`, { ids: targetIds });
234
+ const labelsByTarget = /* @__PURE__ */ new Map();
235
+ for (const rec of check.records) {
236
+ labelsByTarget.set(rec.get("id"), rec.get("labels"));
237
+ }
238
+ const found = labelsByTarget.size;
239
+ const uniqueRequested = new Set(targetIds).size;
240
+ if (found !== uniqueRequested) {
241
+ process.stderr.write(`[graph-write] reject reason=unresolved-target labels=${labelCsv} agent=${agentLabel} requested=${uniqueRequested} found=${found}
242
+ `);
243
+ throw new Error(`Write doctrine violated: ${uniqueRequested - found} of ${uniqueRequested} relationship target(s) did not resolve (elementId mismatch). No node created.`);
244
+ }
245
+ let provenanceSourceId = null;
246
+ let provenanceSourceLabel = null;
247
+ if (requiresActionProvenance(labels) && (createdBy.agent ?? "") !== "system") {
248
+ const candidates = findProvenanceCandidates(relationships);
249
+ const matched = candidates.map((r) => {
250
+ const lbls = labelsByTarget.get(r.targetNodeId);
251
+ if (!Array.isArray(lbls))
252
+ return null;
253
+ const sourceLabel = lbls.find((l) => exports.PROVENANCE_SOURCE_LABELS.has(l));
254
+ return sourceLabel ? { rel: r, sourceLabel } : null;
255
+ }).filter((m) => m !== null);
256
+ if (matched.length === 0) {
257
+ process.stderr.write(`[graph-write] reject reason=missing-provenance labels=${labelCsv} agent=${agentLabel}
258
+ `);
259
+ throw new Error(`missing-provenance: write to ${labelCsv} requires an inbound :PRODUCED edge from a :Task, :Conversation, or :Message (createdBy.agent='${agentLabel}'). Either pass producedByTaskId on the write (autonomous workflows: call task-create at the start of the flow and thread the returned taskId), or rely on the MCP wrapper's CONVERSATION_NODE_ID env-stamp injection (direct admin asks).`);
260
+ }
261
+ provenanceSourceId = matched[0].rel.targetNodeId;
262
+ provenanceSourceLabel = matched[0].sourceLabel;
263
+ }
264
+ let nodeRes;
265
+ try {
266
+ nodeRes = await tx.run(`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`, { props: nodeProps });
267
+ } catch (err) {
268
+ const code = err?.code ?? "";
269
+ if (code === "Neo.ClientError.Schema.ConstraintValidationFailed" && labels.includes("UserProfile")) {
270
+ const accountIdProp = nodeProps.accountId;
271
+ const userIdProp = nodeProps.userId;
272
+ const acctSlice = typeof accountIdProp === "string" ? accountIdProp.slice(0, 8) : "unknown";
273
+ const userSlice = typeof userIdProp === "string" ? userIdProp.slice(0, 8) : "unknown";
274
+ process.stderr.write(`[graph-write] reject reason=user-profile-uniqueness-violation accountId=${acctSlice} userId=${userSlice} writer=${agentLabel}
275
+ `);
276
+ }
277
+ throw err;
278
+ }
279
+ const nodeId = nodeRes.records[0].get("nodeId");
280
+ const nodeLabels = nodeRes.records[0].get("nodeLabels");
281
+ let edgesCreated = 0;
282
+ for (const rel of relationships) {
283
+ const type = rel.type.replace(/`/g, "");
284
+ const q = rel.direction === "outgoing" ? `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (a)-[:\`${type}\`]->(b)` : `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (b)-[:\`${type}\`]->(a)`;
285
+ const r = await tx.run(q, { from: nodeId, to: rel.targetNodeId });
286
+ const created = r.summary.counters.updates().relationshipsCreated;
287
+ if (created === 0) {
288
+ process.stderr.write(`[graph-write] reject reason=unresolved-target-on-create labels=${labelCsv} agent=${agentLabel} relType=${rel.type} targetId=${rel.targetNodeId}
289
+ `);
290
+ throw new Error(`Write doctrine violated: relationship CREATE to target ${rel.targetNodeId} produced 0 edges (target likely deleted concurrently after pre-check). Transaction rolled back.`);
291
+ }
292
+ edgesCreated += created;
293
+ }
294
+ if (edgesCreated !== relationships.length) {
295
+ process.stderr.write(`[graph-write] reject reason=edge-count-mismatch labels=${labelCsv} agent=${agentLabel} requested=${relationships.length} created=${edgesCreated}
296
+ `);
297
+ throw new Error(`Write doctrine violated: expected ${relationships.length} edges, created ${edgesCreated}. Transaction rolled back.`);
298
+ }
299
+ process.stderr.write(`[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"} producedBy=${provenanceSourceLabel ?? "none"}:${provenanceSourceId ?? "none"}
300
+ `);
301
+ return { nodeId, labels: nodeLabels, edgesCreated };
302
+ });
303
+ }
304
+ }
305
+ });
306
+
307
+ // ../lib/task-secrets/dist/index.js
308
+ var require_dist2 = __commonJS({
309
+ "../lib/task-secrets/dist/index.js"(exports) {
310
+ "use strict";
311
+ Object.defineProperty(exports, "__esModule", { value: true });
312
+ exports.redactSecrets = redactSecrets2;
313
+ function redactSecrets2(payload, schema) {
314
+ if (!payload) {
315
+ return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
316
+ }
317
+ const secrets = new Set(schema?.secretFields ?? []);
318
+ const redacted = {};
319
+ const dropped = [];
320
+ for (const [key, value] of Object.entries(payload)) {
321
+ if (secrets.has(key)) {
322
+ dropped.push(key);
323
+ continue;
324
+ }
325
+ redacted[key] = value;
326
+ }
327
+ dropped.sort();
328
+ return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
329
+ }
330
+ }
331
+ });
332
+
333
+ // app/lib/cloudflare-task-tracker.ts
334
+ import { readFileSync, existsSync } from "fs";
335
+ import { randomUUID } from "crypto";
336
+ var import_dist = __toESM(require_dist(), 1);
337
+ var import_dist2 = __toESM(require_dist2(), 1);
338
+ var CREATED_BY_AGENT = "cloudflare-setup-endpoint";
339
+ var TASK_KIND = "cloudflare-tunnel-login";
340
+ async function openCloudflareTask(params) {
341
+ const { accountId, conversationId, inputsProvided, inputs, inputSchema, messageId } = params;
342
+ const taskId = randomUUID();
343
+ const now = (/* @__PURE__ */ new Date()).toISOString();
344
+ const session = getSession();
345
+ try {
346
+ const conv = await session.run(
347
+ `MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
348
+ { conversationId, accountId }
349
+ );
350
+ if (conv.records.length === 0) {
351
+ throw new Error(
352
+ `cloudflare-task-tracker: conversationId=${conversationId.slice(0, 8)} has no :Conversation node \u2014 invariant violated, ensureConversation missed at session boot`
353
+ );
354
+ }
355
+ const conversationElementId = conv.records[0].get("id");
356
+ let messageElementId = null;
357
+ let raisedDuringTag = `raisedDuringConversation=${conversationId.slice(0, 8)}`;
358
+ if (messageId) {
359
+ const msgRow = await session.run(
360
+ `MATCH (m:Message {messageId: $messageId, accountId: $accountId, conversationId: $conversationId}) RETURN elementId(m) AS id LIMIT 1`,
361
+ { messageId, accountId, conversationId }
362
+ );
363
+ if (msgRow.records.length > 0) {
364
+ messageElementId = msgRow.records[0].get("id");
365
+ raisedDuringTag = `raisedDuringMessage=${messageId.slice(0, 8)}`;
366
+ }
367
+ }
368
+ const adminLabelStr = typeof inputs?.adminLabel === "string" ? inputs.adminLabel : "?";
369
+ const adminDomainStr = typeof inputs?.adminDomain === "string" ? inputs.adminDomain : "?";
370
+ const publicLabelStr = typeof inputs?.publicLabel === "string" ? inputs.publicLabel : "";
371
+ const publicDomainStr = typeof inputs?.publicDomain === "string" ? inputs.publicDomain : "";
372
+ const publicSegment = publicDomainStr ? `, public=${publicLabelStr}.${publicDomainStr}` : "";
373
+ const description = `Cloudflare setup for admin=${adminLabelStr}.${adminDomainStr}${publicSegment}`;
374
+ const props = {
375
+ taskId,
376
+ accountId,
377
+ name: "Cloudflare tunnel login + setup",
378
+ description,
379
+ status: "running",
380
+ priority: "normal",
381
+ kind: TASK_KIND,
382
+ inputsProvided,
383
+ startedAt: now,
384
+ createdAt: now,
385
+ updatedAt: now
386
+ };
387
+ if (inputs) {
388
+ if (!inputSchema) {
389
+ process.stderr.write(
390
+ `[task] redact-no-schema kind=${TASK_KIND} taskId=${taskId} fieldsCount=${Object.keys(inputs).length}
391
+ `
392
+ );
393
+ }
394
+ const { redacted, droppedFields, droppedFieldNames } = (0, import_dist2.redactSecrets)(inputs, inputSchema);
395
+ for (const [key, value] of Object.entries(redacted)) {
396
+ if (value === void 0 || value === null || value === "") continue;
397
+ props[`inputs.${key}`] = value;
398
+ }
399
+ if (droppedFields > 0) {
400
+ process.stderr.write(
401
+ `[task] redacted kind=${TASK_KIND} taskId=${taskId} droppedFields=${droppedFields} fields=${droppedFieldNames.join(",")}
402
+ `
403
+ );
404
+ }
405
+ }
406
+ const relationships = [
407
+ {
408
+ type: "RAISED_DURING",
409
+ direction: "outgoing",
410
+ targetNodeId: messageElementId ?? conversationElementId
411
+ }
412
+ ];
413
+ const result = await (0, import_dist.writeNodeWithEdges)({
414
+ session,
415
+ labels: ["Task"],
416
+ props,
417
+ relationships,
418
+ createdBy: {
419
+ agent: CREATED_BY_AGENT,
420
+ session: conversationId,
421
+ tool: "cloudflare-setup-endpoint"
422
+ }
423
+ });
424
+ process.stderr.write(
425
+ `[task] action-start kind=${TASK_KIND} taskId=${taskId} ${raisedDuringTag}
426
+ `
427
+ );
428
+ return { taskId, taskElementId: result.nodeId };
429
+ } finally {
430
+ await session.close();
431
+ }
432
+ }
433
+ async function appendCloudflareSteps(taskId, accountId, streamLogPath) {
434
+ if (!existsSync(streamLogPath)) return [];
435
+ let content;
436
+ try {
437
+ content = readFileSync(streamLogPath, "utf-8");
438
+ } catch {
439
+ return [];
440
+ }
441
+ const steps = [];
442
+ for (const line of content.split(/\r?\n/)) {
443
+ const m = line.match(/\bphase_line\s+setup-tunnel\s+step=(\S+)/);
444
+ if (m) steps.push(m[1]);
445
+ }
446
+ if (steps.length === 0) return [];
447
+ const session = getSession();
448
+ try {
449
+ await session.run(
450
+ `MATCH (t:Task {taskId: $taskId, accountId: $accountId})
451
+ SET t.steps = coalesce(t.steps, []) + $steps,
452
+ t.updatedAt = $updatedAt`,
453
+ { taskId, accountId, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
454
+ );
455
+ for (const step of steps) {
456
+ process.stderr.write(
457
+ `[task] action-step kind=${TASK_KIND} taskId=${taskId} step=${step}
458
+ `
459
+ );
460
+ }
461
+ return steps;
462
+ } finally {
463
+ await session.close();
464
+ }
465
+ }
466
+ async function completeCloudflareTask(params) {
467
+ const { taskId, taskElementId, accountId, conversationId, tunnelId, tunnelName, hostnames, status, errorMessage } = params;
468
+ const now = (/* @__PURE__ */ new Date()).toISOString();
469
+ if (status === "failed" && (!errorMessage || errorMessage.trim().length === 0)) {
470
+ throw new Error(
471
+ "cloudflare-task-tracker: errorMessage is required when status='failed' (Task 885 process-provenance contract)."
472
+ );
473
+ }
474
+ const session = getSession();
475
+ try {
476
+ if (status === "completed" && tunnelId && tunnelName) {
477
+ const conv = await session.run(
478
+ `MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
479
+ { conversationId, accountId }
480
+ );
481
+ const conversationElementId = conv.records.length > 0 ? conv.records[0].get("id") : null;
482
+ const tunnelProps = {
483
+ accountId,
484
+ tunnelId,
485
+ tunnelName,
486
+ createdAt: now,
487
+ updatedAt: now
488
+ };
489
+ const tunnelRels = [
490
+ { type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId }
491
+ ];
492
+ if (conversationElementId) {
493
+ tunnelRels.push({
494
+ type: "RAISED_DURING",
495
+ direction: "outgoing",
496
+ targetNodeId: conversationElementId
497
+ });
498
+ }
499
+ const tunnelWrite = await (0, import_dist.writeNodeWithEdges)({
500
+ session,
501
+ labels: ["CloudflareTunnel"],
502
+ props: tunnelProps,
503
+ relationships: tunnelRels,
504
+ createdBy: {
505
+ agent: CREATED_BY_AGENT,
506
+ session: conversationId,
507
+ tool: "cloudflare-setup-endpoint"
508
+ }
509
+ });
510
+ for (const h of hostnames ?? []) {
511
+ const hostRels = [
512
+ { type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId },
513
+ {
514
+ type: "ROUTES_TO",
515
+ direction: "outgoing",
516
+ targetNodeId: tunnelWrite.nodeId
517
+ }
518
+ ];
519
+ await (0, import_dist.writeNodeWithEdges)({
520
+ session,
521
+ labels: ["CloudflareHostname"],
522
+ props: {
523
+ accountId,
524
+ hostnameValue: h.hostnameValue,
525
+ tunnelId,
526
+ isApex: h.isApex,
527
+ createdAt: now,
528
+ updatedAt: now
529
+ },
530
+ relationships: hostRels,
531
+ createdBy: {
532
+ agent: CREATED_BY_AGENT,
533
+ session: conversationId,
534
+ tool: "cloudflare-setup-endpoint"
535
+ }
536
+ });
537
+ }
538
+ }
539
+ const setClauses = [
540
+ "t.status = $status",
541
+ "t.completedAt = $now",
542
+ "t.updatedAt = $now"
543
+ ];
544
+ const queryParams = { taskId, accountId, status, now };
545
+ if (status === "failed" && errorMessage) {
546
+ setClauses.push("t.errorMessage = $errorMessage");
547
+ queryParams.errorMessage = errorMessage;
548
+ }
549
+ if (status === "completed") {
550
+ setClauses.push("t.errorMessage = null");
551
+ if (tunnelId) {
552
+ setClauses.push("t.tunnelId = $tunnelId");
553
+ queryParams.tunnelId = tunnelId;
554
+ }
555
+ if (tunnelName) {
556
+ setClauses.push("t.tunnelName = $tunnelName");
557
+ queryParams.tunnelName = tunnelName;
558
+ }
559
+ if (hostnames && hostnames.length > 0) {
560
+ setClauses.push("t.hostnames = $hostnamesList");
561
+ queryParams.hostnamesList = hostnames.map((h) => h.hostnameValue);
562
+ }
563
+ }
564
+ const updateRes = await session.run(
565
+ `MATCH (t:Task {taskId: $taskId, accountId: $accountId})
566
+ SET ${setClauses.join(", ")}
567
+ RETURN size(coalesce(t.steps, [])) AS stepsCount, count(t) AS affected`,
568
+ queryParams
569
+ );
570
+ const stepsCount = updateRes.records[0]?.get("stepsCount")?.toNumber?.() ?? 0;
571
+ const affected = updateRes.records[0]?.get("affected")?.toNumber?.() ?? 0;
572
+ if (affected !== 1) {
573
+ throw new Error(
574
+ `cloudflare-task-tracker: completeCloudflareTask MATCH count=${affected} for taskId=${taskId} \u2014 Task missing or accountId mismatch`
575
+ );
576
+ }
577
+ process.stderr.write(
578
+ `[task] action-done kind=${TASK_KIND} taskId=${taskId} status=${status} stepsCount=${stepsCount}
579
+ `
580
+ );
581
+ } finally {
582
+ await session.close();
583
+ }
584
+ }
585
+ async function findMostRecentTerminalCloudflareTask(accountId, conversationId) {
586
+ const session = getSession();
587
+ try {
588
+ const res = await session.run(
589
+ `MATCH (c:Conversation {conversationId: $conversationId, accountId: $accountId})<-[:RAISED_DURING]-(t:Task {kind: $kind})
590
+ WHERE t.status IN ['completed', 'failed'] AND t.completedAt IS NOT NULL
591
+ RETURN t.taskId AS taskId, t.status AS status, t.completedAt AS completedAt, t.errorMessage AS errorMessage
592
+ ORDER BY t.completedAt DESC LIMIT 1`,
593
+ { conversationId, accountId, kind: TASK_KIND }
594
+ );
595
+ if (res.records.length === 0) return null;
596
+ const row = res.records[0];
597
+ const taskId = row.get("taskId");
598
+ const status = row.get("status");
599
+ const completedAt = row.get("completedAt");
600
+ const errorMessage = row.get("errorMessage");
601
+ if (status !== "completed" && status !== "failed") return null;
602
+ return { taskId, outcome: status, completedAt, errorMessage };
603
+ } catch (err) {
604
+ process.stderr.write(
605
+ `[cf-task-tracker] findMostRecentTerminalCloudflareTask failed: ${err instanceof Error ? err.message : String(err)}
606
+ `
607
+ );
608
+ return null;
609
+ } finally {
610
+ await session.close();
611
+ }
612
+ }
613
+ function readTunnelState(brandConfigDir) {
614
+ const statePath = `${process.env.HOME ?? ""}/${brandConfigDir}/cloudflared/tunnel.state`;
615
+ if (!existsSync(statePath)) return null;
616
+ try {
617
+ const parsed = JSON.parse(readFileSync(statePath, "utf-8"));
618
+ const tunnelId = typeof parsed.tunnelId === "string" ? parsed.tunnelId : null;
619
+ const tunnelName = typeof parsed.tunnelName === "string" ? parsed.tunnelName : null;
620
+ const domain = typeof parsed.domain === "string" ? parsed.domain : null;
621
+ if (!tunnelId || !tunnelName || !domain) return null;
622
+ return { tunnelId, tunnelName, domain };
623
+ } catch {
624
+ return null;
625
+ }
626
+ }
627
+ var CLOUDFLARE_TASK_DIAGNOSTICS = {
628
+ scriptExitedNonzero: "script-exited-nonzero",
629
+ noTunnelStateOnDisk: "no-tunnel-state-on-disk",
630
+ endpointDiedPreReconcile: "endpoint-died-pre-reconcile"
631
+ };
632
+ function resolveUnitGoneVerdict(args) {
633
+ const { tunnelState, expected } = args;
634
+ const tunnelStateFound = tunnelState !== null;
635
+ const nameSet = typeof expected.tunnelName === "string" && expected.tunnelName.length > 0;
636
+ const idSet = typeof expected.tunnelId === "string" && expected.tunnelId.length > 0;
637
+ const identityMatch = tunnelState !== null && (nameSet && tunnelState.tunnelName === expected.tunnelName || idSet && tunnelState.tunnelId === expected.tunnelId);
638
+ if (tunnelStateFound && identityMatch) {
639
+ return { kind: "close-completed", tunnelStateFound: true, identityMatch: true };
640
+ }
641
+ return { kind: "close-failed", tunnelStateFound, identityMatch };
642
+ }
643
+ var RECONCILE_DELAY_BUDGET_MS = 9e4;
644
+ var RECONCILE_HARD_AGE_MS = 60 * 60 * 1e3;
645
+ async function recoverRunningCloudflareTasks(accountId, brandConfigDir, conversationIdForState) {
646
+ const session = getSession();
647
+ try {
648
+ const cutoff = new Date(Date.now() - RECONCILE_DELAY_BUDGET_MS).toISOString();
649
+ const stale = await session.run(
650
+ `MATCH (t:Task {accountId: $accountId, kind: $kind, status: 'running'})
651
+ WHERE t.startedAt < $cutoff
652
+ RETURN t.taskId AS taskId, elementId(t) AS taskElementId, t.startedAt AS startedAt
653
+ ORDER BY t.startedAt ASC`,
654
+ { accountId, kind: TASK_KIND, cutoff }
655
+ );
656
+ const tunnelState = readTunnelState(brandConfigDir);
657
+ const tunnelStateFound = tunnelState !== null;
658
+ const resolved = [];
659
+ for (const record of stale.records) {
660
+ const taskId = record.get("taskId");
661
+ const taskElementId = record.get("taskElementId");
662
+ const startedAt = record.get("startedAt");
663
+ const ageMs = Date.now() - new Date(startedAt).getTime();
664
+ try {
665
+ if (tunnelStateFound && tunnelState) {
666
+ const tunnelExistsRow = await session.run(
667
+ `MATCH (t:Task {taskId: $taskId, accountId: $accountId})-[:PRODUCED]->(tn:CloudflareTunnel {tunnelId: $tunnelId})
668
+ RETURN count(tn) AS existsCount`,
669
+ { taskId, accountId, tunnelId: tunnelState.tunnelId }
670
+ );
671
+ const existsCountRaw = tunnelExistsRow.records[0]?.get("existsCount");
672
+ const tunnelAlreadyExists = (typeof existsCountRaw === "number" ? existsCountRaw : existsCountRaw?.toNumber?.() ?? 0) > 0;
673
+ if (tunnelAlreadyExists) {
674
+ const now = (/* @__PURE__ */ new Date()).toISOString();
675
+ const closeRes = await session.run(
676
+ `MATCH (t:Task {taskId: $taskId, accountId: $accountId})
677
+ SET t.status = 'completed', t.completedAt = $now, t.updatedAt = $now,
678
+ t.errorMessage = null,
679
+ t.tunnelId = $tunnelId, t.tunnelName = $tunnelName
680
+ RETURN count(t) AS affected`,
681
+ { taskId, accountId, now, tunnelId: tunnelState.tunnelId, tunnelName: tunnelState.tunnelName }
682
+ );
683
+ const aff = closeRes.records[0]?.get("affected");
684
+ const affN = typeof aff === "number" ? aff : aff?.toNumber?.() ?? 0;
685
+ if (affN !== 1) {
686
+ throw new Error(`reconciler close-out MATCH count=${affN} for taskId=${taskId}`);
687
+ }
688
+ resolved.push({ taskId, resolution: "completed-existing-tunnel" });
689
+ process.stderr.write(
690
+ `[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=true tunnel-already-written=true resolution=completed
691
+ `
692
+ );
693
+ continue;
694
+ }
695
+ const convRow = await session.run(
696
+ `MATCH (t:Task {taskId: $taskId, accountId: $accountId})-[:RAISED_DURING]->(target)
697
+ OPTIONAL MATCH (target)-[:PART_OF]->(c:Conversation)
698
+ RETURN coalesce(c.conversationId, target.conversationId) AS conversationId LIMIT 1`,
699
+ { taskId, accountId }
700
+ );
701
+ const resolvedConversationId = convRow.records[0]?.get("conversationId");
702
+ await completeCloudflareTask({
703
+ taskId,
704
+ taskElementId,
705
+ accountId,
706
+ // Empty string when the edge resolution found no Conversation
707
+ // (rare — would require a malformed Task with no RAISED_DURING).
708
+ // The live close-out will produce a Tunnel without provenance
709
+ // edges in that case; better than silently dropping the close-out.
710
+ conversationId: resolvedConversationId ?? conversationIdForState ?? "",
711
+ tunnelId: tunnelState.tunnelId,
712
+ tunnelName: tunnelState.tunnelName,
713
+ hostnames: void 0,
714
+ status: "completed"
715
+ });
716
+ resolved.push({ taskId, resolution: "completed" });
717
+ process.stderr.write(
718
+ `[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=true tunnel-already-written=false resolution=completed
719
+ `
720
+ );
721
+ } else {
722
+ const diagnostic = ageMs > RECONCILE_HARD_AGE_MS ? CLOUDFLARE_TASK_DIAGNOSTICS.noTunnelStateOnDisk : CLOUDFLARE_TASK_DIAGNOSTICS.endpointDiedPreReconcile;
723
+ await completeCloudflareTask({
724
+ taskId,
725
+ taskElementId,
726
+ accountId,
727
+ conversationId: conversationIdForState ?? "",
728
+ status: "failed",
729
+ errorMessage: diagnostic
730
+ });
731
+ resolved.push({ taskId, resolution: `failed-${diagnostic}` });
732
+ process.stderr.write(
733
+ `[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=false resolution=failed errorMessage=${diagnostic}
734
+ `
735
+ );
736
+ }
737
+ } catch (err) {
738
+ process.stderr.write(
739
+ `[task] action-recover kind=${TASK_KIND} taskId=${taskId} resolution=error reason="${err instanceof Error ? err.message : String(err)}"
740
+ `
741
+ );
742
+ }
743
+ }
744
+ return { scanned: stale.records.length, resolved };
745
+ } finally {
746
+ await session.close();
747
+ }
748
+ }
749
+
750
+ export {
751
+ openCloudflareTask,
752
+ appendCloudflareSteps,
753
+ completeCloudflareTask,
754
+ findMostRecentTerminalCloudflareTask,
755
+ readTunnelState,
756
+ CLOUDFLARE_TASK_DIAGNOSTICS,
757
+ resolveUnitGoneVerdict,
758
+ recoverRunningCloudflareTasks
759
+ };