@rubytech/create-realagent 1.0.771 → 1.0.773

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.771",
3
+ "version": "1.0.773",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: admin
3
- description: "Platform administration plugin. Provides system-status, brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, agent-list, agent-config-read, logs-read, plugin-read, render-component, session-reset, session-resume, file-attach, wifi, review-cadence tools (review-rules-list, review-rules-suppress, review-rules-unsuppress, review-rules-add, review-rules-remove, review-alerts-recent), adherence-read (attention-weighted adherence ledger), and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform."
3
+ description: "Platform administration plugin. Provides system-status, brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, render-component, session-reset, session-resume, file-attach, wifi, review-cadence tools (review-rules-list, review-rules-suppress, review-rules-unsuppress, review-rules-add, review-rules-remove, review-alerts-recent), adherence-read (attention-weighted adherence ledger), and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform."
4
4
  tools:
5
5
  - system-status
6
6
  - brand-settings
@@ -9,6 +9,7 @@ tools:
9
9
  - admin-add
10
10
  - admin-remove
11
11
  - admin-list
12
+ - admin-update-pin
12
13
  - agent-list
13
14
  - agent-config-read
14
15
  - logs-read
@@ -545,9 +545,9 @@ server.tool("account-update", "Update a user-configurable setting in account.jso
545
545
  // ===================================================================
546
546
  // Admin user management tools
547
547
  // ===================================================================
548
- server.tool("admin-add", "Add a new admin user to this account. Creates a device-level user entry (users.json) and adds them to this account's admins list (account.json). Generates a unique 4-digit PIN unless one is specified. Returns the userId and PIN to share with the new admin.", {
549
- name: z.string().describe("Display name for the new admin"),
550
- pin: z.string().optional().describe("Optional 4-digit PIN. If omitted, a unique PIN is generated."),
548
+ server.tool("admin-add", "Add a new admin user to this account. Creates a device-level user entry (users.json) and adds them to this account's admins list (account.json). PIN must be at least 4 digits. If no PIN is provided, a unique 4-digit PIN is generated. Returns the userId and PIN to share with the new admin.", {
549
+ name: z.string().describe("Display name for the new admin (stored on the AdminUser node in Neo4j)."),
550
+ pin: z.string().optional().describe("Optional PIN (minimum 4 digits). If omitted, a unique 4-digit PIN is generated."),
551
551
  }, async ({ name, pin: rawPin }) => {
552
552
  const TAG = "[admin]";
553
553
  if (!name.trim()) {
@@ -577,7 +577,7 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
577
577
  }
578
578
  // Resolve the calling user's identity from the session environment
579
579
  const callerUserId = process.env.USER_ID;
580
- // PIN: use provided or generate
580
+ // PIN: use provided or generate. Constraint: minimum 4 digits, no upper bound.
581
581
  let plaintextPin;
582
582
  if (rawPin) {
583
583
  if (rawPin.length < 4) {
@@ -599,8 +599,9 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
599
599
  }
600
600
  const pinHash = hashPin(plaintextPin);
601
601
  const userId = crypto.randomUUID();
602
- // 1. Write to users.json (device-level)
603
- users.push({ userId, name: name.trim(), pin: pinHash });
602
+ // 1. Write to users.json (device-level). Auth fields only — `name` lives
603
+ // on the AdminUser node in Neo4j (Task 829).
604
+ users.push({ userId, pin: pinHash });
604
605
  try {
605
606
  writeUsersJson(users);
606
607
  }
@@ -622,20 +623,58 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
622
623
  console.error(`${TAG} account.json write failed: ${err instanceof Error ? err.message : String(err)}`);
623
624
  return { content: [{ type: "text", text: `${TAG} User created in users.json but failed to add to account: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
624
625
  }
625
- // 3. Write to Neo4j (graph-level) partial failure is a warning, not an error
626
+ // 3. Write to Neo4j (graph-level): AdminUser + Person + OWNS atomically,
627
+ // plus ADMIN_OF edge to the LocalBusiness for this account.
628
+ // Task 830 — deterministic identity creation: never delegate node
629
+ // creation to the LLM. Person reuse rule mirrors writeAdminUserAndPerson
630
+ // in platform/ui/app/lib/neo4j-store.ts (case-insensitive exact match
631
+ // on givenName + familyName; partial-name ambiguity does NOT match —
632
+ // rationalisation is a separate agent-mediated concern).
626
633
  let neo4jWarning = "";
634
+ let personReused = false;
627
635
  try {
628
636
  const session = getSession();
629
637
  try {
630
638
  const createdAt = new Date().toISOString();
631
639
  const trimmedName = name.trim();
632
- await session.run(`MERGE (au:AdminUser {userId: $userId})
640
+ const firstSpace = trimmedName.search(/\s/);
641
+ const givenName = firstSpace === -1 ? trimmedName : trimmedName.slice(0, firstSpace).trim();
642
+ const familyName = firstSpace === -1 ? null : (trimmedName.slice(firstSpace + 1).trim() || null);
643
+ const result = await session.run(`MERGE (au:AdminUser {userId: $userId})
633
644
  ON CREATE SET au.name = $name, au.createdAt = $createdAt
634
- ON MATCH SET au.name = $name
645
+ ON MATCH SET au.name = $name, au.updatedAt = $createdAt
635
646
  WITH au
636
647
  MATCH (b:LocalBusiness {accountId: $accountId})
637
648
  MERGE (au)-[r:ADMIN_OF]->(b)
638
- ON CREATE SET r.role = 'admin', r.grantedAt = $createdAt`, { userId, name: trimmedName, createdAt, accountId: ACCOUNT_ID });
649
+ ON CREATE SET r.role = 'admin', r.grantedAt = $createdAt
650
+ WITH au
651
+ OPTIONAL MATCH (existingPerson:Person {accountId: $accountId})
652
+ WHERE toLower(existingPerson.givenName) = toLower($givenName)
653
+ AND coalesce(toLower(existingPerson.familyName), '') = coalesce(toLower($familyName), '')
654
+ WITH au, existingPerson
655
+ CALL {
656
+ WITH au, existingPerson
657
+ WITH au, existingPerson WHERE existingPerson IS NOT NULL
658
+ MERGE (au)-[:OWNS]->(existingPerson)
659
+ RETURN true AS reused
660
+ UNION
661
+ WITH au, existingPerson
662
+ WITH au WHERE existingPerson IS NULL
663
+ CREATE (newPerson:Person {
664
+ accountId: $accountId,
665
+ givenName: $givenName,
666
+ familyName: $familyName,
667
+ role: 'admin-personal',
668
+ scope: 'admin',
669
+ createdAt: $createdAt
670
+ })
671
+ MERGE (au)-[:OWNS]->(newPerson)
672
+ RETURN false AS reused
673
+ }
674
+ RETURN reused`, { userId, name: trimmedName, createdAt, accountId: ACCOUNT_ID, givenName, familyName });
675
+ if (result.records.length > 0) {
676
+ personReused = result.records[0].get("reused");
677
+ }
639
678
  }
640
679
  finally {
641
680
  await session.close();
@@ -646,6 +685,7 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
646
685
  console.error(`${TAG} Neo4j sync failed during admin-add: userId=${userId} error=${errMsg} — files written, graph pending`);
647
686
  neo4jWarning = ` Note: Neo4j sync failed (${errMsg}) — the admin user is functional but the graph will need reconciliation on next seed.`;
648
687
  }
688
+ console.error(`${TAG} [admin-identity] adminuser-bound userId=${userId.slice(0, 8)} name=${name.trim()} personReused=${personReused}`);
649
689
  console.error(`${TAG} admin added: userId=${userId} userName=${name.trim()} accountId=${ACCOUNT_ID} role=admin addedBy=${callerUserId ?? "unknown"}`);
650
690
  return {
651
691
  content: [{
@@ -676,13 +716,22 @@ server.tool("admin-remove", "Remove an admin from this account. Removes them fro
676
716
  if (admins.length <= 1) {
677
717
  return { content: [{ type: "text", text: `${TAG} Cannot remove the last admin. At least one admin must remain on the account.` }], isError: true };
678
718
  }
679
- // Resolve the admin's name for the confirmation message
719
+ // Resolve the admin's name from Neo4j (canonical) for the confirmation
720
+ // message. Best-effort — fall back to userId if the graph is unreachable.
680
721
  let removedName = userId;
681
722
  try {
682
- const users = readUsersJson();
683
- const user = users.find(u => u.userId === userId);
684
- if (user)
685
- removedName = user.name;
723
+ const session = getSession();
724
+ try {
725
+ const result = await session.run(`MATCH (au:AdminUser {userId: $userId}) RETURN au.name AS name LIMIT 1`, { userId });
726
+ if (result.records.length > 0) {
727
+ const name = result.records[0].get("name");
728
+ if (name && name.trim())
729
+ removedName = name.trim();
730
+ }
731
+ }
732
+ finally {
733
+ await session.close();
734
+ }
686
735
  }
687
736
  catch { /* name lookup is best-effort */ }
688
737
  // 1. Remove from account.json
@@ -733,13 +782,27 @@ server.tool("admin-list", "List all admins for this account with their names and
733
782
  if (admins.length === 0) {
734
783
  return { content: [{ type: "text", text: `${TAG} No admins configured for this account.` }] };
735
784
  }
736
- // Enrich with names from users.json
737
- let users = [];
785
+ // Enrich with names from Neo4j AdminUser (canonical, Task 829). Best
786
+ // effort render "(unknown)" for any userId without a graph entry.
787
+ const userMap = new Map();
738
788
  try {
739
- users = readUsersJson();
789
+ const session = getSession();
790
+ try {
791
+ const result = await session.run(`UNWIND $userIds AS uid
792
+ MATCH (au:AdminUser {userId: uid})
793
+ RETURN au.userId AS userId, au.name AS name`, { userIds: admins.map(a => a.userId) });
794
+ for (const record of result.records) {
795
+ const uid = record.get("userId");
796
+ const name = record.get("name");
797
+ if (name && name.trim())
798
+ userMap.set(uid, name.trim());
799
+ }
800
+ }
801
+ finally {
802
+ await session.close();
803
+ }
740
804
  }
741
- catch { /* name lookup is best-effort — show userIds if users.json is unavailable */ }
742
- const userMap = new Map(users.map(u => [u.userId, u.name]));
805
+ catch { /* name lookup is best-effort — userIds shown when graph is unreachable */ }
743
806
  const lines = admins.map(a => {
744
807
  const name = userMap.get(a.userId) ?? "(unknown)";
745
808
  return `- **${name}** — role: ${a.role}, userId: ${a.userId}`;
@@ -748,6 +811,54 @@ server.tool("admin-list", "List all admins for this account with their names and
748
811
  content: [{ type: "text", text: `Admins for this account:\n\n${lines.join("\n")}` }],
749
812
  };
750
813
  });
814
+ server.tool("admin-update-pin", "Update an existing admin user's PIN. Defaults to the calling admin if no userId is given. PIN must be at least 4 digits and unique across all users on the device. PINs are device-level: updating another admin's PIN does not require shared account membership — any admin on the device can rotate any other admin's PIN, matching the existing trust model used by admin-remove.", {
815
+ userId: z.string().optional().describe("The userId of the admin whose PIN to update. Defaults to the caller (the admin invoking this tool)."),
816
+ newPin: z.string().describe("The new PIN. Minimum 4 digits, no upper bound."),
817
+ }, async ({ userId: targetUserId, newPin }) => {
818
+ const TAG = "[admin-update-pin]";
819
+ const callerUserId = process.env.USER_ID;
820
+ const userId = targetUserId ?? callerUserId;
821
+ const userIdLabel = userId ? userId.slice(0, 8) : "unknown";
822
+ if (!userId) {
823
+ console.error(`${TAG} userId=${userIdLabel} result=user-not-found reason=no-caller-context`);
824
+ return { content: [{ type: "text", text: `${TAG} No userId supplied and no caller context — cannot update.` }], isError: true };
825
+ }
826
+ if (newPin.length < 4) {
827
+ console.error(`${TAG} userId=${userIdLabel} result=too-short`);
828
+ return { content: [{ type: "text", text: `${TAG} PIN must be at least 4 digits.` }], isError: true };
829
+ }
830
+ let users;
831
+ try {
832
+ users = readUsersJson();
833
+ }
834
+ catch (err) {
835
+ console.error(`${TAG} userId=${userIdLabel} result=user-not-found reason=users-json-read-failed`);
836
+ return { content: [{ type: "text", text: `${TAG} ${err instanceof Error ? err.message : String(err)}` }], isError: true };
837
+ }
838
+ const targetIndex = users.findIndex(u => u.userId === userId);
839
+ if (targetIndex === -1) {
840
+ console.error(`${TAG} userId=${userIdLabel} result=user-not-found`);
841
+ return { content: [{ type: "text", text: `${TAG} User ${userId} not found in users.json.` }], isError: true };
842
+ }
843
+ const newHash = hashPin(newPin);
844
+ const collidesWithOther = users.some((u, i) => i !== targetIndex && u.pin === newHash);
845
+ if (collidesWithOther) {
846
+ console.error(`${TAG} userId=${userIdLabel} result=collision`);
847
+ return { content: [{ type: "text", text: `${TAG} That PIN is already in use by another user. Choose a different PIN.` }], isError: true };
848
+ }
849
+ users[targetIndex].pin = newHash;
850
+ try {
851
+ writeUsersJson(users);
852
+ }
853
+ catch (err) {
854
+ return { content: [{ type: "text", text: `${TAG} Failed to write users.json: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
855
+ }
856
+ console.error(`${TAG} userId=${userIdLabel} result=ok`);
857
+ const self = userId === callerUserId;
858
+ return {
859
+ content: [{ type: "text", text: `PIN updated${self ? "" : ` for userId ${userId}`}. The new PIN takes effect on the next login.` }],
860
+ };
861
+ });
751
862
  server.tool("agent-image", "Upload, update, or remove an agent's image. action=set: copy the file at filePath to the agent's assets directory and update config.json with image URL and shape. action=remove: delete the image file and clear the image fields from config.json. imageShape: 'circle' for avatars/icons, 'rounded' for logos.", {
752
863
  agentSlug: z.string().describe("Agent slug (e.g. 'coaching', 'sales')"),
753
864
  action: z.enum(["set", "remove"]),