@rubytech/create-maxy-code 0.1.330 → 0.1.332
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +19 -2
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/lib/account-enumeration/dist/index.d.ts +33 -0
- package/payload/platform/lib/account-enumeration/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/account-enumeration/dist/index.js +76 -0
- package/payload/platform/lib/account-enumeration/dist/index.js.map +1 -1
- package/payload/platform/lib/account-enumeration/src/index.ts +78 -0
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +24 -24
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js +109 -75
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +56 -19
- package/payload/platform/lib/mcp-spawn-tee/src/index.ts +112 -77
- package/payload/platform/neo4j/schema.cypher +58 -4
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +13 -1
- package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/admin/mcp/dist/index.js +102 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.js +57 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/__tests__/account-lifecycle.test.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts +34 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js +78 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js.map +1 -0
- package/payload/platform/plugins/admin/skills/plainly/SKILL.md +7 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/contacts/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.js +71 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.js +112 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-create.test.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.js +102 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/group-manage.test.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +10 -3
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +7 -2
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js +5 -4
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/package.json +4 -2
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/memory/mcp/dist/index.js +3 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-lookup-by-name.test.js +39 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-lookup-by-name.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-lookup-by-name.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-lookup-by-name.js +13 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-lookup-by-name.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +17 -5
- package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/telegram/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/platform/scripts/__tests__/provision-role-stamp.test.sh +40 -0
- package/payload/platform/scripts/__tests__/sweep-durability.test.sh +103 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +319 -0
- package/payload/platform/scripts/lib/resolve-account-dir.sh +79 -132
- package/payload/platform/scripts/seed-neo4j.sh +18 -0
- package/payload/platform/scripts/setup-account.sh +28 -361
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +18 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +63 -7
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +30 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +16 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +7 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +8 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.d.ts +6 -0
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.js +17 -5
- package/payload/platform/services/claude-session-manager/dist/stuck-turn.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +25 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +17 -0
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +14 -0
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.js +66 -2
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts +28 -0
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -0
- package/payload/platform/services/webchat-channel/dist/turn-follow.js +188 -0
- package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +24 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/templates/account.json +2 -0
- package/payload/premium-plugins/writer-craft/lib/mcp-spawn-tee/index.js +109 -75
- package/payload/server/public/assets/AdminLoginScreens-DexSvtjD.js +1 -0
- package/payload/server/public/assets/AdminShell-Bf9tndNg.js +1 -0
- package/payload/server/public/assets/{Checkbox-1flpB9H0.js → Checkbox-D58GsKoQ.js} +1 -1
- package/payload/server/public/assets/{OperatorConversations-DrPqB9Ym.css → OperatorConversations-CDdp2nVn.css} +1 -1
- package/payload/server/public/assets/OperatorConversations-RmqANYz8.js +9 -0
- package/payload/server/public/assets/{admin-BWJTvagN.js → admin-CH5Vkkt4.js} +1 -1
- package/payload/server/public/assets/{browser-XM5e-fn1.js → browser-BwDtzSlr.js} +1 -1
- package/payload/server/public/assets/chat-3GY0LmYH.js +1 -0
- package/payload/server/public/assets/data-CttrzhfL.js +1 -0
- package/payload/server/public/assets/{graph-CIIz9Dmh.js → graph-CZSdF2jv.js} +3 -3
- package/payload/server/public/assets/{graph-labels-BRKvhE0e.js → graph-labels-Bi0fu8Ns.js} +1 -1
- package/payload/server/public/assets/operator-BEivxmRy.js +1 -0
- package/payload/server/public/assets/page-DUvT07SB.js +30 -0
- package/payload/server/public/assets/{public-BPlZnopr.js → public-B3eIlVbd.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +605 -435
- package/payload/server/public/assets/AdminLoginScreens-BnuaXvxN.js +0 -1
- package/payload/server/public/assets/AdminShell-Br0DJQNu.js +0 -1
- package/payload/server/public/assets/OperatorConversations-C8sITz2_.js +0 -9
- package/payload/server/public/assets/chat-COFMxaoC.js +0 -1
- package/payload/server/public/assets/data-BHtJozT2.js +0 -1
- package/payload/server/public/assets/operator-9hbxrXKm.js +0 -1
- package/payload/server/public/assets/page-CXkBfOhG.js +0 -30
package/payload/platform/plugins/contacts/mcp/dist/tools/__tests__/contact-lookup.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contact-lookup.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/contact-lookup.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Task 985 — contact-lookup by name must recompose the split givenName +
|
|
2
|
+
// familyName pair. A Person stored givenName:"Jane" familyName:"Smith" was
|
|
3
|
+
// invisible to a search for "Jane Smith" because the old predicate tested the
|
|
4
|
+
// whole query against each field with CONTAINS ("jane smith" is a substring of
|
|
5
|
+
// neither "jane" nor "smith"). These tests lock the composed-substring predicate
|
|
6
|
+
// and the query-whitespace normalization. The live Neo4j CONTAINS filtering is
|
|
7
|
+
// exercised on the Pi; the predicate correctness is locked here.
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
9
|
+
const mockSession = {
|
|
10
|
+
run: vi.fn(),
|
|
11
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
};
|
|
13
|
+
vi.mock("../../lib/neo4j.js", () => ({
|
|
14
|
+
getSession: () => mockSession,
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("../../../../../../lib/graph-trash/dist/index.js", () => ({
|
|
17
|
+
notTrashed: (alias) => `(NOT ${alias}:Trashed)`,
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("neo4j-driver", () => ({
|
|
20
|
+
default: { int: (v) => v },
|
|
21
|
+
}));
|
|
22
|
+
import { contactLookup, normalizeNameQuery } from "../contact-lookup.js";
|
|
23
|
+
const ACCOUNT_ID = "20028fb5-db26-4de7-bff6-c5ec2d4fc766";
|
|
24
|
+
function personRecord(props, nodeId = "4:person:9") {
|
|
25
|
+
return {
|
|
26
|
+
get: (k) => (k === "nodeId" ? nodeId : { properties: props }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
mockSession.run.mockReset();
|
|
31
|
+
mockSession.close.mockClear();
|
|
32
|
+
});
|
|
33
|
+
describe("normalizeNameQuery", () => {
|
|
34
|
+
it("trims and collapses internal whitespace to single spaces", () => {
|
|
35
|
+
expect(normalizeNameQuery(" Jane Smith ")).toBe("Jane Smith");
|
|
36
|
+
});
|
|
37
|
+
it("leaves a single token unchanged", () => {
|
|
38
|
+
expect(normalizeNameQuery("Jane")).toBe("Jane");
|
|
39
|
+
});
|
|
40
|
+
it("collapses a tab/newline run between tokens", () => {
|
|
41
|
+
expect(normalizeNameQuery("Jane\t\nSmith")).toBe("Jane Smith");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("contact-lookup — composed full-name match (Task 985)", () => {
|
|
45
|
+
it("matches the composed givenName+familyName for a full-name query", async () => {
|
|
46
|
+
mockSession.run.mockResolvedValueOnce({
|
|
47
|
+
records: [personRecord({ givenName: "Jane", familyName: "Smith", source: "manual", status: "active", createdOn: "2026-01-01" })],
|
|
48
|
+
});
|
|
49
|
+
const hits = await contactLookup({ name: "Jane Smith", accountId: ACCOUNT_ID });
|
|
50
|
+
const [cypher, params] = mockSession.run.mock.calls[0];
|
|
51
|
+
expect(cypher).toContain("toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) CONTAINS toLower($name)");
|
|
52
|
+
expect(params.name).toBe("Jane Smith");
|
|
53
|
+
expect(hits).toHaveLength(1);
|
|
54
|
+
expect(hits[0].givenName).toBe("Jane");
|
|
55
|
+
expect(hits[0].familyName).toBe("Smith");
|
|
56
|
+
});
|
|
57
|
+
it("uses the same composed predicate for a single-token query (no per-field branch)", async () => {
|
|
58
|
+
mockSession.run.mockResolvedValueOnce({ records: [] });
|
|
59
|
+
await contactLookup({ name: "Jane", accountId: ACCOUNT_ID });
|
|
60
|
+
const [cypher, params] = mockSession.run.mock.calls[0];
|
|
61
|
+
expect(cypher).toContain("toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) CONTAINS toLower($name)");
|
|
62
|
+
expect(params.name).toBe("Jane");
|
|
63
|
+
});
|
|
64
|
+
it("normalizes whitespace in the threaded $name param", async () => {
|
|
65
|
+
mockSession.run.mockResolvedValueOnce({ records: [] });
|
|
66
|
+
await contactLookup({ name: " Jane Smith ", accountId: ACCOUNT_ID });
|
|
67
|
+
const [, params] = mockSession.run.mock.calls[0];
|
|
68
|
+
expect(params.name).toBe("Jane Smith");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=contact-lookup.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contact-lookup.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/contact-lookup.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-create.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/group-create.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Task 988 — group-create resolves a participant by name. The old predicate
|
|
2
|
+
// composed the display name with `p.givenName + COALESCE(' ' + p.familyName,
|
|
3
|
+
// '')`, which is null in Cypher whenever givenName is null, so a familyName-only
|
|
4
|
+
// Person could never be matched by name. These tests lock the null-safe,
|
|
5
|
+
// case-insensitive composed-display-name equality (parity with
|
|
6
|
+
// memory-lookup-by-name's Person branch) and the query-whitespace normalization.
|
|
7
|
+
// Live Neo4j matching is exercised on the Pi; predicate correctness is locked here.
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
9
|
+
const mockSession = {
|
|
10
|
+
run: vi.fn(),
|
|
11
|
+
executeWrite: vi.fn(),
|
|
12
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
};
|
|
14
|
+
vi.mock("../../lib/neo4j.js", () => ({
|
|
15
|
+
getSession: () => mockSession,
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("../../../../../../lib/graph-trash/dist/index.js", () => ({
|
|
18
|
+
notTrashed: (alias) => `(NOT ${alias}:Trashed)`,
|
|
19
|
+
}));
|
|
20
|
+
vi.mock("../../../../../../lib/graph-write/dist/index.js", () => ({
|
|
21
|
+
stampCreatedBy: (props) => props,
|
|
22
|
+
}));
|
|
23
|
+
import { groupCreate } from "../group-create.js";
|
|
24
|
+
const ACCOUNT_ID = "20028fb5-db26-4de7-bff6-c5ec2d4fc766";
|
|
25
|
+
const COMPOSED_PREDICATE = "toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) = toLower($identifier)";
|
|
26
|
+
// elementId + p.givenName record shape returned by the participant-resolve query.
|
|
27
|
+
function resolveRecord(id, name) {
|
|
28
|
+
return { get: (k) => (k === "id" ? id : name) };
|
|
29
|
+
}
|
|
30
|
+
function countRecord(value) {
|
|
31
|
+
return { get: () => ({ toNumber: () => value }) };
|
|
32
|
+
}
|
|
33
|
+
// Resolve every participant to a distinct person; the slug check is unique, the
|
|
34
|
+
// write is a no-op, and the brand lookup returns nothing (default base URL).
|
|
35
|
+
function wireHappyPath() {
|
|
36
|
+
let person = 0;
|
|
37
|
+
mockSession.run.mockImplementation((cypher) => {
|
|
38
|
+
if (cypher.includes("count(c) AS cnt")) {
|
|
39
|
+
return Promise.resolve({ records: [countRecord(0)] });
|
|
40
|
+
}
|
|
41
|
+
if (cypher.includes("p:Person")) {
|
|
42
|
+
person += 1;
|
|
43
|
+
return Promise.resolve({
|
|
44
|
+
records: [resolveRecord(`4:person:${person}`, person === 1 ? null : "Jane")],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return Promise.resolve({ records: [] }); // LocalBusiness brand lookup
|
|
48
|
+
});
|
|
49
|
+
mockSession.executeWrite.mockImplementation(async (cb) => cb({ run: vi.fn().mockResolvedValue({ records: [] }) }));
|
|
50
|
+
}
|
|
51
|
+
function participantResolveCalls() {
|
|
52
|
+
return mockSession.run.mock.calls.filter(([cypher]) => cypher.includes("p:Person"));
|
|
53
|
+
}
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockSession.run.mockReset();
|
|
56
|
+
mockSession.executeWrite.mockReset();
|
|
57
|
+
mockSession.close.mockClear();
|
|
58
|
+
});
|
|
59
|
+
describe("group-create — composed by-name participant match (Task 988)", () => {
|
|
60
|
+
it("matches participants on the null-safe composed display name", async () => {
|
|
61
|
+
wireHappyPath();
|
|
62
|
+
await groupCreate({
|
|
63
|
+
groupName: "Smiths",
|
|
64
|
+
agentSlug: "default",
|
|
65
|
+
participants: ["Smith", "Jane Smith"],
|
|
66
|
+
accountId: ACCOUNT_ID,
|
|
67
|
+
createdBy: {},
|
|
68
|
+
});
|
|
69
|
+
const [cypher] = participantResolveCalls()[0];
|
|
70
|
+
expect(cypher).toContain(COMPOSED_PREDICATE);
|
|
71
|
+
// The old non-null-safe composition must be gone.
|
|
72
|
+
expect(cypher).not.toContain("p.givenName + COALESCE");
|
|
73
|
+
});
|
|
74
|
+
it("resolves a familyName-only participant rather than dropping it as not-found", async () => {
|
|
75
|
+
wireHappyPath();
|
|
76
|
+
const result = await groupCreate({
|
|
77
|
+
groupName: "Smiths",
|
|
78
|
+
agentSlug: "default",
|
|
79
|
+
participants: ["Smith", "Jane Smith"],
|
|
80
|
+
accountId: ACCOUNT_ID,
|
|
81
|
+
createdBy: {},
|
|
82
|
+
});
|
|
83
|
+
expect(result.notFound).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
it("normalizes the threaded participant query whitespace", async () => {
|
|
86
|
+
wireHappyPath();
|
|
87
|
+
await groupCreate({
|
|
88
|
+
groupName: "Smiths",
|
|
89
|
+
agentSlug: "default",
|
|
90
|
+
participants: [" Jane Smith ", "Smith"],
|
|
91
|
+
accountId: ACCOUNT_ID,
|
|
92
|
+
createdBy: {},
|
|
93
|
+
});
|
|
94
|
+
const [, params] = participantResolveCalls()[0];
|
|
95
|
+
expect(params.identifier).toBe("Jane Smith");
|
|
96
|
+
});
|
|
97
|
+
it("reports the original identifier (not the normalized form) when nothing resolves", async () => {
|
|
98
|
+
mockSession.run.mockImplementation((cypher) => {
|
|
99
|
+
if (cypher.includes("count(c) AS cnt"))
|
|
100
|
+
return Promise.resolve({ records: [countRecord(0)] });
|
|
101
|
+
return Promise.resolve({ records: [] }); // no participant resolves
|
|
102
|
+
});
|
|
103
|
+
await expect(groupCreate({
|
|
104
|
+
groupName: "Smiths",
|
|
105
|
+
agentSlug: "default",
|
|
106
|
+
participants: [" Nobody Here ", "Ghost"],
|
|
107
|
+
accountId: ACCOUNT_ID,
|
|
108
|
+
createdBy: {},
|
|
109
|
+
})).rejects.toThrow("Nobody Here");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=group-create.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-create.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/group-create.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-manage.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/group-manage.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Task 988 — group-manage adds and removes a participant by name. Both the add
|
|
2
|
+
// and remove queries composed the display name with `p.givenName + COALESCE(' '
|
|
3
|
+
// + p.familyName, '')`, which is null whenever givenName is null, so a
|
|
4
|
+
// familyName-only Person could be neither added to nor removed from a group by
|
|
5
|
+
// name. These tests lock the null-safe, case-insensitive composed-display-name
|
|
6
|
+
// equality (parity with memory-lookup-by-name's Person branch) and the
|
|
7
|
+
// query-whitespace normalization. Live Neo4j matching is exercised on the Pi.
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
9
|
+
const mockSession = {
|
|
10
|
+
run: vi.fn(),
|
|
11
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
};
|
|
13
|
+
vi.mock("../../lib/neo4j.js", () => ({
|
|
14
|
+
getSession: () => mockSession,
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("../../../../../../lib/graph-trash/dist/index.js", () => ({
|
|
17
|
+
notTrashed: (alias) => `(NOT ${alias}:Trashed)`,
|
|
18
|
+
}));
|
|
19
|
+
import { groupManage } from "../group-manage.js";
|
|
20
|
+
const ACCOUNT_ID = "20028fb5-db26-4de7-bff6-c5ec2d4fc766";
|
|
21
|
+
const GROUP_SLUG = "smiths";
|
|
22
|
+
const COMPOSED_PREDICATE = "toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) = toLower($participant)";
|
|
23
|
+
function record(values) {
|
|
24
|
+
return { get: (k) => values[k] };
|
|
25
|
+
}
|
|
26
|
+
function countRecord(value) {
|
|
27
|
+
return { get: () => ({ toNumber: () => value }) };
|
|
28
|
+
}
|
|
29
|
+
// Group lookup succeeds; add returns a resolved name; remove deletes one edge;
|
|
30
|
+
// the trailing participant list is empty (irrelevant to these predicate locks).
|
|
31
|
+
function wire({ removed = 1 } = {}) {
|
|
32
|
+
mockSession.run.mockImplementation((cypher) => {
|
|
33
|
+
if (cypher.includes("groupSlug: $groupSlug")) {
|
|
34
|
+
return Promise.resolve({ records: [record({ sessionId: "sess-1", groupName: "Smiths" })] });
|
|
35
|
+
}
|
|
36
|
+
if (cypher.includes("MERGE (p)-[r:PARTICIPATES_IN")) {
|
|
37
|
+
return Promise.resolve({ records: [record({ name: "Smith" })] });
|
|
38
|
+
}
|
|
39
|
+
if (cypher.includes("DELETE r")) {
|
|
40
|
+
return Promise.resolve({ records: [countRecord(removed)] });
|
|
41
|
+
}
|
|
42
|
+
return Promise.resolve({ records: [] }); // trailing participant list
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function callContaining(fragment) {
|
|
46
|
+
return mockSession.run.mock.calls.find(([cypher]) => cypher.includes(fragment));
|
|
47
|
+
}
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
mockSession.run.mockReset();
|
|
50
|
+
mockSession.close.mockClear();
|
|
51
|
+
});
|
|
52
|
+
describe("group-manage add — composed by-name match (Task 988)", () => {
|
|
53
|
+
it("matches the participant on the null-safe composed display name", async () => {
|
|
54
|
+
wire();
|
|
55
|
+
await groupManage({
|
|
56
|
+
groupSlug: GROUP_SLUG,
|
|
57
|
+
action: "add",
|
|
58
|
+
participant: "Smith",
|
|
59
|
+
accountId: ACCOUNT_ID,
|
|
60
|
+
});
|
|
61
|
+
const [cypher] = callContaining("MERGE (p)-[r:PARTICIPATES_IN");
|
|
62
|
+
expect(cypher).toContain(COMPOSED_PREDICATE);
|
|
63
|
+
expect(cypher).not.toContain("p.givenName + COALESCE");
|
|
64
|
+
});
|
|
65
|
+
it("normalizes the threaded participant query whitespace", async () => {
|
|
66
|
+
wire();
|
|
67
|
+
await groupManage({
|
|
68
|
+
groupSlug: GROUP_SLUG,
|
|
69
|
+
action: "add",
|
|
70
|
+
participant: " Jane Smith ",
|
|
71
|
+
accountId: ACCOUNT_ID,
|
|
72
|
+
});
|
|
73
|
+
const [, params] = callContaining("MERGE (p)-[r:PARTICIPATES_IN");
|
|
74
|
+
expect(params.participant).toBe("Jane Smith");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("group-manage remove — composed by-name match (Task 988)", () => {
|
|
78
|
+
it("matches the participant on the null-safe composed display name", async () => {
|
|
79
|
+
wire();
|
|
80
|
+
await groupManage({
|
|
81
|
+
groupSlug: GROUP_SLUG,
|
|
82
|
+
action: "remove",
|
|
83
|
+
participant: "Smith",
|
|
84
|
+
accountId: ACCOUNT_ID,
|
|
85
|
+
});
|
|
86
|
+
const [cypher] = callContaining("DELETE r");
|
|
87
|
+
expect(cypher).toContain(COMPOSED_PREDICATE);
|
|
88
|
+
expect(cypher).not.toContain("p.givenName + COALESCE");
|
|
89
|
+
});
|
|
90
|
+
it("normalizes the threaded participant query whitespace", async () => {
|
|
91
|
+
wire();
|
|
92
|
+
await groupManage({
|
|
93
|
+
groupSlug: GROUP_SLUG,
|
|
94
|
+
action: "remove",
|
|
95
|
+
participant: " Jane Smith ",
|
|
96
|
+
accountId: ACCOUNT_ID,
|
|
97
|
+
});
|
|
98
|
+
const [, params] = callContaining("DELETE r");
|
|
99
|
+
expect(params.participant).toBe("Jane Smith");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=group-manage.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-manage.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/group-manage.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contact-lookup.d.ts","sourceRoot":"","sources":["../../src/tools/contact-lookup.ts"],"names":[],"mappings":"AAIA,UAAU,mBAAmB;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,aAAa,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"contact-lookup.d.ts","sourceRoot":"","sources":["../../src/tools/contact-lookup.ts"],"names":[],"mappings":"AAIA,UAAU,mBAAmB;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,aAAa,EAAE,CAAC,CAiE1B"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import neo4j from "neo4j-driver";
|
|
2
2
|
import { getSession } from "../lib/neo4j.js";
|
|
3
3
|
import { notTrashed } from "../../../../../lib/graph-trash/dist/index.js";
|
|
4
|
+
// A by-name query is matched against the read-time-composed display name
|
|
5
|
+
// (givenName + ' ' + familyName), so the full name the UI rendered resolves the
|
|
6
|
+
// person even though the schema forbids a canonical Person.name. Trim and
|
|
7
|
+
// collapse internal whitespace so " Jane Smith " matches the single-spaced
|
|
8
|
+
// composition; a single token is left unchanged and still matches either field.
|
|
9
|
+
export function normalizeNameQuery(name) {
|
|
10
|
+
return name.trim().replace(/\s+/g, " ");
|
|
11
|
+
}
|
|
4
12
|
export async function contactLookup(params) {
|
|
5
13
|
const { email, telephone, name, accountId, limit = 10 } = params;
|
|
6
14
|
const session = getSession();
|
|
@@ -29,13 +37,12 @@ export async function contactLookup(params) {
|
|
|
29
37
|
else if (name) {
|
|
30
38
|
query = `
|
|
31
39
|
MATCH (p:Person {accountId: $accountId})
|
|
32
|
-
WHERE
|
|
33
|
-
OR toLower(p.familyName) CONTAINS toLower($name))
|
|
40
|
+
WHERE toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) CONTAINS toLower($name)
|
|
34
41
|
AND ${trashFilter}
|
|
35
42
|
RETURN p, elementId(p) AS nodeId
|
|
36
43
|
LIMIT $limit
|
|
37
44
|
`;
|
|
38
|
-
queryParams = { name, accountId, limit: neo4j.int(limit) };
|
|
45
|
+
queryParams = { name: normalizeNameQuery(name), accountId, limit: neo4j.int(limit) };
|
|
39
46
|
}
|
|
40
47
|
else {
|
|
41
48
|
query = `
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contact-lookup.js","sourceRoot":"","sources":["../../src/tools/contact-lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,cAAc,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,8CAA8C,CAAC;
|
|
1
|
+
{"version":3,"file":"contact-lookup.js","sourceRoot":"","sources":["../../src/tools/contact-lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,cAAc,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,8CAA8C,CAAC;AAU1E,yEAAyE;AACzE,gFAAgF;AAChF,0EAA0E;AAC1E,8EAA8E;AAC9E,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAcD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAA2B;IAE3B,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,GAAG,EAAE,EAAE,GAAG,MAAM,CAAC;IAEjE,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,IAAI,CAAC;QACH,IAAI,KAAa,CAAC;QAClB,IAAI,WAAoC,CAAC;QAEzC,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,GAAG;;qCAEuB,WAAW;;;OAGzC,CAAC;YACF,WAAW,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACnF,CAAC;aAAM,IAAI,SAAS,EAAE,CAAC;YACrB,KAAK,GAAG;;6CAE+B,WAAW;;;OAGjD,CAAC;YACF,WAAW,GAAG,EAAE,SAAS,EAAE,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACpF,CAAC;aAAM,IAAI,IAAI,EAAE,CAAC;YAChB,KAAK,GAAG;;;iBAGG,WAAW;;;OAGrB,CAAC;YACF,WAAW,GAAG,EAAE,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACvF,CAAC;aAAM,CAAC;YACN,KAAK,GAAG;;gBAEE,WAAW;;;;OAIpB,CAAC;YACF,WAAW,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAErD,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACnC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC;YACrC,OAAO;gBACL,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAW;gBACtC,SAAS,EAAE,CAAC,CAAC,SAAmB;gBAChC,UAAU,EAAE,CAAC,CAAC,UAAgC;gBAC9C,KAAK,EAAE,CAAC,CAAC,KAA2B;gBACpC,SAAS,EAAE,CAAC,CAAC,SAA+B;gBAC5C,QAAQ,EAAE,CAAC,CAAC,QAA8B;gBAC1C,MAAM,EAAE,CAAC,CAAC,MAAgB;gBAC1B,MAAM,EAAE,CAAC,CAAC,MAAgB;gBAC1B,SAAS,EAAE,CAAC,CAAC,SAAmB;aACjC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-create.d.ts","sourceRoot":"","sources":["../../src/tools/group-create.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,8CAA8C,CAAC;
|
|
1
|
+
{"version":3,"file":"group-create.d.ts","sourceRoot":"","sources":["../../src/tools/group-create.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,8CAA8C,CAAC;AAY9F,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiHvF"}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { getSession } from "../lib/neo4j.js";
|
|
3
3
|
import { notTrashed } from "../../../../../lib/graph-trash/dist/index.js";
|
|
4
4
|
import { stampCreatedBy } from "../../../../../lib/graph-write/dist/index.js";
|
|
5
|
+
import { normalizeNameQuery } from "./contact-lookup.js";
|
|
5
6
|
function generateGroupSlug(name) {
|
|
6
7
|
const base = name
|
|
7
8
|
.toLowerCase()
|
|
@@ -36,12 +37,16 @@ export async function groupCreate(params) {
|
|
|
36
37
|
const resolvedParticipants = [];
|
|
37
38
|
const notFound = [];
|
|
38
39
|
for (const identifier of participants) {
|
|
40
|
+
// Match the read-time-composed display name (null-safe, case-insensitive)
|
|
41
|
+
// so a familyName-only Person resolves by name, exactly as the lookup
|
|
42
|
+
// tools do; the original identifier is still reported in notFound. The
|
|
43
|
+
// email/telephone branches keep their existing exact match.
|
|
39
44
|
const r = await session.run(`MATCH (p:Person {accountId: $accountId})
|
|
40
|
-
WHERE (p.givenName +
|
|
45
|
+
WHERE (toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) = toLower($identifier)
|
|
41
46
|
OR p.email = $identifier
|
|
42
47
|
OR p.telephone = $identifier)
|
|
43
48
|
AND ${notTrashed("p")}
|
|
44
|
-
RETURN elementId(p) AS id, p.givenName AS name LIMIT 1`, { accountId, identifier });
|
|
49
|
+
RETURN elementId(p) AS id, p.givenName AS name LIMIT 1`, { accountId, identifier: normalizeNameQuery(identifier) });
|
|
45
50
|
if (r.records.length > 0) {
|
|
46
51
|
resolvedParticipants.push({
|
|
47
52
|
identifier,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-create.js","sourceRoot":"","sources":["../../src/tools/group-create.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,8CAA8C,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAkB,MAAM,8CAA8C,CAAC;
|
|
1
|
+
{"version":3,"file":"group-create.js","sourceRoot":"","sources":["../../src/tools/group-create.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-manage.d.ts","sourceRoot":"","sources":["../../src/tools/group-manage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"group-manage.d.ts","sourceRoot":"","sources":["../../src/tools/group-manage.ts"],"names":[],"mappings":"AAIA,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,iBAAiB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA2FvF"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getSession } from "../lib/neo4j.js";
|
|
2
2
|
import { notTrashed } from "../../../../../lib/graph-trash/dist/index.js";
|
|
3
|
+
import { normalizeNameQuery } from "./contact-lookup.js";
|
|
3
4
|
export async function groupManage(params) {
|
|
4
5
|
const { groupSlug, action, participant, accountId } = params;
|
|
5
6
|
const session = getSession();
|
|
@@ -18,7 +19,7 @@ export async function groupManage(params) {
|
|
|
18
19
|
if (!participant)
|
|
19
20
|
throw new Error("participant is required for add action");
|
|
20
21
|
const addResult = await session.run(`MATCH (p:Person {accountId: $accountId})
|
|
21
|
-
WHERE (p.givenName +
|
|
22
|
+
WHERE (toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) = toLower($participant)
|
|
22
23
|
OR p.email = $participant
|
|
23
24
|
OR p.telephone = $participant)
|
|
24
25
|
AND ${notTrashed("p")}
|
|
@@ -26,7 +27,7 @@ export async function groupManage(params) {
|
|
|
26
27
|
MATCH (c:Conversation {sessionId: $sessionId})
|
|
27
28
|
MERGE (p)-[r:PARTICIPATES_IN]->(c)
|
|
28
29
|
ON CREATE SET r.joinedAt = datetime(), r.displayName = p.givenName
|
|
29
|
-
RETURN p.givenName AS name`, { accountId, participant, sessionId });
|
|
30
|
+
RETURN p.givenName AS name`, { accountId, participant: normalizeNameQuery(participant), sessionId });
|
|
30
31
|
const name = addResult.records[0]?.get("name");
|
|
31
32
|
if (!name) {
|
|
32
33
|
throw new Error(`Contact not found: ${participant}`);
|
|
@@ -37,12 +38,12 @@ export async function groupManage(params) {
|
|
|
37
38
|
if (!participant)
|
|
38
39
|
throw new Error("participant is required for remove action");
|
|
39
40
|
const removeResult = await session.run(`MATCH (p:Person {accountId: $accountId})-[r:PARTICIPATES_IN]->(c:Conversation {sessionId: $sessionId})
|
|
40
|
-
WHERE (p.givenName +
|
|
41
|
+
WHERE (toLower(trim(coalesce(p.givenName, '') + ' ' + coalesce(p.familyName, ''))) = toLower($participant)
|
|
41
42
|
OR p.email = $participant
|
|
42
43
|
OR p.telephone = $participant)
|
|
43
44
|
AND ${notTrashed("p")}
|
|
44
45
|
DELETE r
|
|
45
|
-
RETURN count(r) AS removed`, { accountId, participant, sessionId });
|
|
46
|
+
RETURN count(r) AS removed`, { accountId, participant: normalizeNameQuery(participant), sessionId });
|
|
46
47
|
const removed = removeResult.records[0]?.get("removed")?.toNumber?.() ?? 0;
|
|
47
48
|
if (removed === 0) {
|
|
48
49
|
throw new Error(`Participant not found in group: ${participant}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-manage.js","sourceRoot":"","sources":["../../src/tools/group-manage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,8CAA8C,CAAC;
|
|
1
|
+
{"version":3,"file":"group-manage.js","sourceRoot":"","sources":["../../src/tools/group-manage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,8CAA8C,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAgBzD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAyB;IACzD,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAE7D,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,IAAI,CAAC;QACH,mEAAmE;QACnE,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACnC;eACS,UAAU,CAAC,GAAG,CAAC;iEACmC,EAC3D,EAAE,SAAS,EAAE,SAAS,EAAE,CACzB,CAAC;QAEF,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,SAAS,EAAE,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,CAAW,CAAC;QACpD,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,CAAW,CAAC;QAEpD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;YAE5E,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC;;;;iBAIS,UAAU,CAAC,GAAG,CAAC;;;;;oCAKI,EAC5B,EAAE,SAAS,EAAE,WAAW,EAAE,kBAAkB,CAAC,WAAW,CAAC,EAAE,SAAS,EAAE,CACvE,CAAC;YAEF,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAuB,CAAC;YACrE,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,sBAAsB,WAAW,EAAE,CAAC,CAAC;YACvD,CAAC;YAED,OAAO,CAAC,KAAK,CAAC,gCAAgC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;QACvF,CAAC;aAAM,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;YAE/E,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,GAAG,CACpC;;;;iBAIS,UAAU,CAAC,GAAG,CAAC;;oCAEI,EAC5B,EAAE,SAAS,EAAE,WAAW,EAAE,kBAAkB,CAAC,WAAW,CAAC,EAAE,SAAS,EAAE,CACvE,CAAC;YAEF,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,CAAC;YAC3E,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,mCAAmC,WAAW,EAAE,CAAC,CAAC;YACpE,CAAC;YAED,OAAO,CAAC,KAAK,CAAC,kCAAkC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,WAAW,EAAE,CAAC,CAAC;QAChG,CAAC;QAED,uEAAuE;QACvE,0EAA0E;QAC1E,wEAAwE;QACxE,QAAQ;QACR,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,GAAG,CAClC;eACS,UAAU,CAAC,GAAG,CAAC;;+BAEC,EACzB,EAAE,SAAS,EAAE,CACd,CAAC;QAEF,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CACzC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAW,CACtC,CAAC;QAEF,OAAO;YACL,MAAM;YACN,SAAS;YACT,WAAW,EAAE,WAAW,IAAI,SAAS;YACrC,YAAY;SACb,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC"}
|
|
@@ -6,7 +6,8 @@
|
|
|
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
|
+
"test": "vitest run"
|
|
10
11
|
},
|
|
11
12
|
"dependencies": {
|
|
12
13
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"typescript": "^5.7.0",
|
|
17
|
-
"@types/node": "^22.0.0"
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"vitest": "^4.1.2"
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -336,7 +336,7 @@ authoritative. This section names the surfaces and what backs each.
|
|
|
336
336
|
|---|---|---|
|
|
337
337
|
| `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
|
|
338
338
|
| Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
|
|
339
|
-
| Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
|
|
339
|
+
| Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. **The "Projects" row is brand-aware:** its visible term and the graph label it filters to both come from the injected `window.__BRAND__.primaryContainer` (`brand.json` → `server/index.ts` inject → `brand.ts` `brandPrimaryContainer()`), so Maxy reads "Projects" → `:Project`, SiteOffice "Jobs" → `:Job`, Real Agent "Properties" → `:Property`. A brand JSON missing `primaryContainer` falls back to Project/Projects (server logs `[brand] op=primaryContainer-default reason=absent brand=<hostname>`; the populated case logs `[brand] op=inject primaryContainer=<label>/<term>`). | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
|
|
340
340
|
| Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
|
|
341
341
|
| Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". **Re-seat:** each channel conversation row carries a `SessionRowActions` cluster whose sole action is Re-seat (the row is a `conv-with-actions` shell whose open click moved onto an inner button so the cluster can sit beside it), so a WhatsApp/Telegram session's model/mode/effort is re-seatable from the dashboard exactly like a webchat row — wide it is the inline sliders icon, and below the 400 px width it folds into the `…` overflow menu identically to the Sessions rows; the channel reader row carries the same `model` field for the picker default. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
|
|
342
342
|
| Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover between consecutive turns so a thread spanning midnight is unambiguous. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
|