@rubytech/create-maxy 1.0.705 → 1.0.706

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 (74) hide show
  1. package/dist/__tests__/apt-resolve.test.js +179 -0
  2. package/dist/apt-resolve.js +73 -0
  3. package/dist/index.js +48 -46
  4. package/package.json +3 -3
  5. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.d.ts +2 -0
  6. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.d.ts.map +1 -0
  7. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js +89 -0
  8. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js.map +1 -0
  9. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts +42 -0
  10. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts.map +1 -0
  11. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js +87 -0
  12. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js.map +1 -0
  13. package/payload/platform/lib/graph-mcp/src/__tests__/schema-cypher-parser.test.ts +99 -0
  14. package/payload/platform/lib/graph-mcp/src/schema-cypher-parser.ts +84 -0
  15. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  16. package/payload/platform/plugins/admin/mcp/dist/index.js +30 -0
  17. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +2 -2
  19. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +46 -5
  20. package/payload/platform/plugins/memory/PLUGIN.md +3 -1
  21. package/payload/platform/plugins/memory/mcp/dist/index.js +56 -6
  22. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.d.ts +2 -0
  24. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.d.ts.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js +92 -0
  26. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js.map +1 -0
  27. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.d.ts +2 -0
  28. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.d.ts.map +1 -0
  29. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +51 -0
  30. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -0
  31. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.d.ts +2 -0
  32. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.d.ts.map +1 -0
  33. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +222 -0
  34. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -0
  35. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +16 -0
  36. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +38 -11
  38. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts +136 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts.map +1 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js +180 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js.map +1 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +11 -2
  44. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +6 -3
  46. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +44 -22
  48. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +94 -57
  50. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
  51. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +7 -5
  52. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +2 -2
  54. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  55. package/payload/platform/plugins/memory/references/schema-base.md +4 -0
  56. package/payload/server/chunk-PE76FPYP.js +12040 -0
  57. package/payload/server/maxy-edge.js +1 -1
  58. package/payload/server/public/assets/{Checkbox-B2Lk8F4X.js → Checkbox-CjbS9JcG.js} +1 -1
  59. package/payload/server/public/assets/{admin-agtgi48Q.js → admin-Ce9DbUuu.js} +1 -1
  60. package/payload/server/public/assets/{data-B7nsyBTV.js → data-C-SxjLC9.js} +1 -1
  61. package/payload/server/public/assets/{file-DHWTu8LP.js → file-D4cbAAuo.js} +1 -1
  62. package/payload/server/public/assets/{graph-ChDwqqhJ.js → graph-D-Rqh0Md.js} +1 -1
  63. package/payload/server/public/assets/{house-CfjnRPO6.js → house-CYsVygEQ.js} +1 -1
  64. package/payload/server/public/assets/{jsx-runtime-81wg0w0Q.css → jsx-runtime-DPXE45W9.css} +1 -1
  65. package/payload/server/public/assets/{public-CE1kyVnz.js → public-BTOF98iO.js} +1 -1
  66. package/payload/server/public/assets/{share-2-CAd1beVT.js → share-2-B-sbkB36.js} +1 -1
  67. package/payload/server/public/assets/{useVoiceRecorder-LSAU68Eo.js → useVoiceRecorder-DLVFx3ms.js} +1 -1
  68. package/payload/server/public/assets/{x-B0xK3Aoq.js → x-BNidzSAn.js} +1 -1
  69. package/payload/server/public/data.html +6 -6
  70. package/payload/server/public/graph.html +7 -7
  71. package/payload/server/public/index.html +8 -8
  72. package/payload/server/public/public.html +5 -5
  73. package/payload/server/server.js +1 -1
  74. /package/payload/server/public/assets/{jsx-runtime-DhzH26q8.js → jsx-runtime-BUs3sHtV.js} +0 -0
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * Shared parser for `platform/neo4j/schema.cypher`.
4
+ *
5
+ * Two consumers today:
6
+ * 1. The admin SCHEMA prompt block (`platform/ui/app/lib/admin-schema-block.ts`,
7
+ * Task 703) renders labels declared in schema.cypher into the agent's
8
+ * system prompt.
9
+ * 2. The memory-plugin schema validator (Task 736) treats schema.cypher
10
+ * declarations as the "promise" half of the recognised-label set —
11
+ * labels that exist as constraints/indexes but may not yet have a node
12
+ * in the graph (fresh-install bootstrap).
13
+ *
14
+ * The previous home was admin-schema-block.ts; that owner was wrong as
15
+ * soon as a second consumer arrived. Lifting here keeps a single source of
16
+ * truth and removes the cross-package shape coupling.
17
+ *
18
+ * Also exports a compact `levenshtein` for "did you mean?" suggestions on
19
+ * unknown labels — used by the memory-plugin validator. The graph-mcp
20
+ * cypher validator has its own copy in schema-cache.ts (private to the
21
+ * stale-miss heuristic); the duplication is intentional, since this
22
+ * exported one targets human-readable error UX, not refresh debouncing.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.parseLabelsFromSchemaCypher = parseLabelsFromSchemaCypher;
26
+ exports.levenshtein = levenshtein;
27
+ exports.nearestLabel = nearestLabel;
28
+ const LABEL_PATTERN = /FOR\s*\(\s*\w+\s*:\s*(\w+)\s*\)/g;
29
+ /**
30
+ * Extract every Neo4j label declared in a `schema.cypher` file by scanning
31
+ * `FOR (alias:Label)` constraint and index forms. Returns a sorted, de-duped
32
+ * list. Pure function — no I/O.
33
+ */
34
+ function parseLabelsFromSchemaCypher(schemaCypher) {
35
+ const found = new Set();
36
+ for (const match of schemaCypher.matchAll(LABEL_PATTERN)) {
37
+ found.add(match[1]);
38
+ }
39
+ return [...found].sort();
40
+ }
41
+ /**
42
+ * Standard iterative-DP Levenshtein. Used to suggest a near-match when an
43
+ * agent submits an unknown label — small alphabets, short strings, runs in
44
+ * microseconds. Identical algorithm to the private copy in schema-cache.ts;
45
+ * exported here so callers outside graph-mcp don't have to roll their own.
46
+ */
47
+ function levenshtein(a, b) {
48
+ if (a === b)
49
+ return 0;
50
+ if (a.length === 0)
51
+ return b.length;
52
+ if (b.length === 0)
53
+ return a.length;
54
+ let prev = new Array(b.length + 1);
55
+ let curr = new Array(b.length + 1);
56
+ for (let j = 0; j <= b.length; j++)
57
+ prev[j] = j;
58
+ for (let i = 1; i <= a.length; i++) {
59
+ curr[0] = i;
60
+ for (let j = 1; j <= b.length; j++) {
61
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
62
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
63
+ }
64
+ [prev, curr] = [curr, prev];
65
+ }
66
+ return prev[b.length];
67
+ }
68
+ /**
69
+ * Find the closest label in `candidates` to `unknown` by edit distance.
70
+ * Returns null when the closest match is further than `maxDistance` (default
71
+ * 3) — beyond that a suggestion is more confusing than helpful.
72
+ */
73
+ function nearestLabel(unknown, candidates, maxDistance = 3) {
74
+ let best = null;
75
+ let bestDist = Infinity;
76
+ for (const candidate of candidates) {
77
+ const d = levenshtein(unknown, candidate);
78
+ if (d < bestDist) {
79
+ bestDist = d;
80
+ best = candidate;
81
+ }
82
+ }
83
+ if (best === null || bestDist > maxDistance)
84
+ return null;
85
+ return best;
86
+ }
87
+ //# sourceMappingURL=schema-cypher-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-cypher-parser.js","sourceRoot":"","sources":["../src/schema-cypher-parser.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;AASH,kEAMC;AAQD,kCAgBC;AAOD,oCAgBC;AA5DD,MAAM,aAAa,GAAG,kCAAkC,CAAC;AAEzD;;;;GAIG;AACH,SAAgB,2BAA2B,CAAC,YAAoB;IAC9D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACzD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;GAKG;AACH,SAAgB,WAAW,CAAC,CAAS,EAAE,CAAS;IAC9C,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,MAAM,CAAC;IACpC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,MAAM,CAAC;IACpC,IAAI,IAAI,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,IAAI,IAAI,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3C,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QACvE,CAAC;QACD,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAC1B,OAAe,EACf,UAA4B,EAC5B,WAAW,GAAG,CAAC;IAEf,IAAI,IAAI,GAAkB,IAAI,CAAC;IAC/B,IAAI,QAAQ,GAAG,QAAQ,CAAC;IACxB,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,QAAQ,EAAE,CAAC;YACjB,QAAQ,GAAG,CAAC,CAAC;YACb,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,IAAI,IAAI,KAAK,IAAI,IAAI,QAAQ,GAAG,WAAW;QAAE,OAAO,IAAI,CAAC;IACzD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,99 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ parseLabelsFromSchemaCypher,
5
+ levenshtein,
6
+ nearestLabel,
7
+ } from "../schema-cypher-parser.js";
8
+
9
+ test("parseLabelsFromSchemaCypher: extracts labels from constraint and index forms", () => {
10
+ const text = `
11
+ CREATE CONSTRAINT person_email IF NOT EXISTS
12
+ FOR (p:Person) REQUIRE p.email IS UNIQUE;
13
+
14
+ CREATE CONSTRAINT business_account IF NOT EXISTS
15
+ FOR (b:LocalBusiness) REQUIRE b.accountId IS UNIQUE;
16
+
17
+ CREATE INDEX admin_user_account IF NOT EXISTS
18
+ FOR (au:AdminUser) ON (au.accountId);
19
+
20
+ CREATE INDEX knowledge_doc IF NOT EXISTS
21
+ FOR (k:KnowledgeDocument) ON (k.embedding) OPTIONS { ... };
22
+ `;
23
+ const labels = parseLabelsFromSchemaCypher(text);
24
+ assert.deepEqual(labels, [
25
+ "AdminUser",
26
+ "KnowledgeDocument",
27
+ "LocalBusiness",
28
+ "Person",
29
+ ]);
30
+ });
31
+
32
+ test("parseLabelsFromSchemaCypher: deduplicates labels declared multiple times", () => {
33
+ const text = `
34
+ FOR (p:Person) REQUIRE p.email IS UNIQUE;
35
+ FOR (p:Person) REQUIRE p.telephone IS UNIQUE;
36
+ FOR (p:Person) ON (p.status);
37
+ `;
38
+ assert.deepEqual(parseLabelsFromSchemaCypher(text), ["Person"]);
39
+ });
40
+
41
+ test("parseLabelsFromSchemaCypher: returns sorted output", () => {
42
+ const text = `FOR (z:Zebra) ON (z.x); FOR (a:Aardvark) ON (a.x); FOR (m:Mongoose) ON (m.x);`;
43
+ assert.deepEqual(parseLabelsFromSchemaCypher(text), [
44
+ "Aardvark",
45
+ "Mongoose",
46
+ "Zebra",
47
+ ]);
48
+ });
49
+
50
+ test("parseLabelsFromSchemaCypher: ignores non-constraint cypher", () => {
51
+ const text = `
52
+ MATCH (n:Person) RETURN n;
53
+ CREATE (b:Business { name: 'X' });
54
+ FOR (p:Person) REQUIRE p.email IS UNIQUE;
55
+ `;
56
+ // MATCH/CREATE patterns don't include FOR, so they're ignored.
57
+ assert.deepEqual(parseLabelsFromSchemaCypher(text), ["Person"]);
58
+ });
59
+
60
+ test("parseLabelsFromSchemaCypher: empty input returns empty array", () => {
61
+ assert.deepEqual(parseLabelsFromSchemaCypher(""), []);
62
+ });
63
+
64
+ test("levenshtein: identical strings", () => {
65
+ assert.equal(levenshtein("Person", "Person"), 0);
66
+ });
67
+
68
+ test("levenshtein: substitution distance", () => {
69
+ // LocaIBusiness vs LocalBusiness — capital-I-as-l typo from the incident.
70
+ assert.equal(levenshtein("LocaIBusiness", "LocalBusiness"), 1);
71
+ });
72
+
73
+ test("levenshtein: insertion + deletion", () => {
74
+ assert.equal(levenshtein("Person", "Persons"), 1);
75
+ assert.equal(levenshtein("Persons", "Person"), 1);
76
+ });
77
+
78
+ test("levenshtein: handles empty strings", () => {
79
+ assert.equal(levenshtein("", "abc"), 3);
80
+ assert.equal(levenshtein("abc", ""), 3);
81
+ assert.equal(levenshtein("", ""), 0);
82
+ });
83
+
84
+ test("nearestLabel: finds the closest match", () => {
85
+ const candidates = ["Person", "LocalBusiness", "AdminUser"];
86
+ assert.equal(nearestLabel("LocaIBusiness", candidates), "LocalBusiness");
87
+ assert.equal(nearestLabel("AdminUsr", candidates), "AdminUser");
88
+ });
89
+
90
+ test("nearestLabel: returns null when no match within maxDistance", () => {
91
+ const candidates = ["Person", "LocalBusiness"];
92
+ assert.equal(nearestLabel("Quokka", candidates), null);
93
+ // Default maxDistance is 3; "abcde" is too far from any candidate.
94
+ assert.equal(nearestLabel("abcde", candidates, 1), null);
95
+ });
96
+
97
+ test("nearestLabel: empty candidates returns null", () => {
98
+ assert.equal(nearestLabel("Person", []), null);
99
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Shared parser for `platform/neo4j/schema.cypher`.
3
+ *
4
+ * Two consumers today:
5
+ * 1. The admin SCHEMA prompt block (`platform/ui/app/lib/admin-schema-block.ts`,
6
+ * Task 703) renders labels declared in schema.cypher into the agent's
7
+ * system prompt.
8
+ * 2. The memory-plugin schema validator (Task 736) treats schema.cypher
9
+ * declarations as the "promise" half of the recognised-label set —
10
+ * labels that exist as constraints/indexes but may not yet have a node
11
+ * in the graph (fresh-install bootstrap).
12
+ *
13
+ * The previous home was admin-schema-block.ts; that owner was wrong as
14
+ * soon as a second consumer arrived. Lifting here keeps a single source of
15
+ * truth and removes the cross-package shape coupling.
16
+ *
17
+ * Also exports a compact `levenshtein` for "did you mean?" suggestions on
18
+ * unknown labels — used by the memory-plugin validator. The graph-mcp
19
+ * cypher validator has its own copy in schema-cache.ts (private to the
20
+ * stale-miss heuristic); the duplication is intentional, since this
21
+ * exported one targets human-readable error UX, not refresh debouncing.
22
+ */
23
+
24
+ const LABEL_PATTERN = /FOR\s*\(\s*\w+\s*:\s*(\w+)\s*\)/g;
25
+
26
+ /**
27
+ * Extract every Neo4j label declared in a `schema.cypher` file by scanning
28
+ * `FOR (alias:Label)` constraint and index forms. Returns a sorted, de-duped
29
+ * list. Pure function — no I/O.
30
+ */
31
+ export function parseLabelsFromSchemaCypher(schemaCypher: string): string[] {
32
+ const found = new Set<string>();
33
+ for (const match of schemaCypher.matchAll(LABEL_PATTERN)) {
34
+ found.add(match[1]);
35
+ }
36
+ return [...found].sort();
37
+ }
38
+
39
+ /**
40
+ * Standard iterative-DP Levenshtein. Used to suggest a near-match when an
41
+ * agent submits an unknown label — small alphabets, short strings, runs in
42
+ * microseconds. Identical algorithm to the private copy in schema-cache.ts;
43
+ * exported here so callers outside graph-mcp don't have to roll their own.
44
+ */
45
+ export function levenshtein(a: string, b: string): number {
46
+ if (a === b) return 0;
47
+ if (a.length === 0) return b.length;
48
+ if (b.length === 0) return a.length;
49
+ let prev = new Array<number>(b.length + 1);
50
+ let curr = new Array<number>(b.length + 1);
51
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
52
+ for (let i = 1; i <= a.length; i++) {
53
+ curr[0] = i;
54
+ for (let j = 1; j <= b.length; j++) {
55
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
56
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
57
+ }
58
+ [prev, curr] = [curr, prev];
59
+ }
60
+ return prev[b.length];
61
+ }
62
+
63
+ /**
64
+ * Find the closest label in `candidates` to `unknown` by edit distance.
65
+ * Returns null when the closest match is further than `maxDistance` (default
66
+ * 3) — beyond that a suggestion is more confusing than helpful.
67
+ */
68
+ export function nearestLabel(
69
+ unknown: string,
70
+ candidates: Iterable<string>,
71
+ maxDistance = 3,
72
+ ): string | null {
73
+ let best: string | null = null;
74
+ let bestDist = Infinity;
75
+ for (const candidate of candidates) {
76
+ const d = levenshtein(unknown, candidate);
77
+ if (d < bestDist) {
78
+ bestDist = d;
79
+ best = candidate;
80
+ }
81
+ }
82
+ if (best === null || bestDist > maxDistance) return null;
83
+ return best;
84
+ }
@@ -36,6 +36,7 @@ hidden:
36
36
  - api-key-verify
37
37
  - onboarding-get
38
38
  - onboarding-complete-step
39
+ - onboarding-step9-mode
39
40
  - qr-generate
40
41
  - wifi
41
42
  - review-digest-compose
@@ -1709,6 +1709,36 @@ server.tool("onboarding-complete-step", "Mark an onboarding step as completed. S
1709
1709
  };
1710
1710
  }
1711
1711
  });
1712
+ // Task 704: pin the operator's persona (personal/business-owner/employee) at
1713
+ // step 9 BEFORE any graph write happens. The agent calls this immediately on
1714
+ // the user's single-select submission. The tool's job is twofold:
1715
+ // 1. Emit a deterministic [onboarding-step9-mode] log line for the
1716
+ // diagnostic-grep path on the Pi (matches [onboarding-step-complete]
1717
+ // format — same accountId-prefix slicing, same surrounding code).
1718
+ // 2. Return the next-action prose so the agent's branching is anchored to
1719
+ // the tool surface, not free-text inference.
1720
+ // No persistence — the graph state (LocalBusiness exists vs Person with
1721
+ // role=admin-personal exists) remains the source of truth for gate
1722
+ // satisfaction. Re-calling this tool with a different mode on the same
1723
+ // session is allowed; the most recent log line wins for diagnostic purposes.
1724
+ server.tool("onboarding-step9-mode", "Step 9 onboarding fork: record the operator's persona (personal, business-owner, " +
1725
+ "or employee) before any graph write. Logs the choice for diagnostic grep and " +
1726
+ "returns the deterministic next-action prose for the agent.", {
1727
+ mode: z
1728
+ .enum(["personal", "business-owner", "employee"])
1729
+ .describe("The operator's persona: 'personal' for an individual user with no business, 'business-owner' for someone setting up Maxy as the operations agent for their company, or 'employee' for an employee whose employer is NOT being registered."),
1730
+ }, async ({ mode }) => {
1731
+ console.log(`[onboarding-step9-mode] accountId=${ACCOUNT_ID.slice(0, 8)}… mode=${mode}`);
1732
+ const nextAction = mode === "business-owner"
1733
+ ? "Invoke the `business-profile` skill to create AdminUser + LocalBusiness and capture the business identity. Mark step 9 complete only after both nodes exist in the graph."
1734
+ : "Ask the user for their email (one sentence). Use their name from `admin-identity` in the system prompt; split into givenName + familyName. Call `memory-write` to create the AdminUser node, then call `memory-write` to create a Person node with `role: \"admin-personal\"`, the givenName, familyName, email, and an OWNS edge from the AdminUser. Then call `onboarding-complete-step` with step 9. Do not invoke the `business-profile` skill — personal/employee mode does not register a LocalBusiness.";
1735
+ return {
1736
+ content: [{
1737
+ type: "text",
1738
+ text: JSON.stringify({ mode, nextAction }, null, 2),
1739
+ }],
1740
+ };
1741
+ });
1712
1742
  // ===================================================================
1713
1743
  // Utility tools
1714
1744
  // ===================================================================