@rubytech/create-realagent 1.0.842 → 1.0.843

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.842",
3
+ "version": "1.0.843",
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, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, render-component, session-reset, session-resume, file-attach, wifi, 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, store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), render-component, session-reset, session-resume, file-attach, wifi, 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
@@ -14,6 +14,7 @@ tools:
14
14
  - agent-config-read
15
15
  - logs-read
16
16
  - plugin-read
17
+ - store-skill
17
18
  - render-component
18
19
  - session-reset
19
20
  - session-resume
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
  import { readFile, writeFile } from "node:fs/promises";
7
7
  import { resolve, join } from "node:path";
8
8
  import { execFileSync } from "node:child_process";
9
- import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
9
+ import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
10
10
  import { writeKey, validateKey, hasKey, keyFilePath, deleteKey } from "../../../../lib/anthropic-key/dist/index.js";
11
11
  import { writeAdminEntry, removeAdminFromAccount } from "../../../../lib/admins-write/dist/index.js";
12
12
  import { deviceUrlBlock } from "../../../../lib/device-url/dist/index.js";
@@ -1421,6 +1421,138 @@ server.tool("logs-read", "Read recent logs. Stream logs (type=agent-stream/error
1421
1421
  return { content: [{ type: "text", text: `Failed to read logs: ${err instanceof Error ? err.message : String(err)}` }] };
1422
1422
  }
1423
1423
  });
1424
+ // Task 918 — operator-authored skills are written to disk as a plugin so the
1425
+ // existing plugin-manifest loader picks them up (parsePluginFrontmatter +
1426
+ // assemblePublicPluginContent + loadEmbeddedPlugins all read from
1427
+ // <PLATFORM_ROOT>/plugins/<dir>/). Canonical = <accountDir>/plugins/<plugin>
1428
+ // survives installer rmSync (data/ excluded); the mirror under
1429
+ // <PLATFORM_ROOT>/plugins/<plugin> is rehydrated at admin session start by
1430
+ // autoDeliverUserPlugins. The agent supplies pluginName/skillName/body —
1431
+ // path is computed by this tool from ACCOUNT_ID. Symmetric write counterpart
1432
+ // to plugin-read (Task 916).
1433
+ server.tool("store-skill", "Save an operator-authored skill on disk as part of an admin-managed plugin. " +
1434
+ "The skill becomes immediately discoverable by the admin agent (and the public agent if publicEmbed=true). " +
1435
+ "Path is computed internally from the active account; the agent supplies content + names only. " +
1436
+ "Re-running for the same skillName overwrites in place (drops orphan reference files).", {
1437
+ pluginName: z.string().describe("Plugin directory name in lowercase kebab-case (e.g. 'beacons-skills'). Must not collide with a shipped plugin name."),
1438
+ skillName: z.string().describe("Skill directory name in lowercase kebab-case (e.g. 'lead-capture')."),
1439
+ description: z.string().describe("One-sentence skill description used in SKILL.md frontmatter and as the trigger hint for the agent."),
1440
+ publicEmbed: z.boolean().describe("True to embed this skill in the public agent's system prompt; false for admin-only."),
1441
+ body: z.string().describe("SKILL.md body content WITHOUT frontmatter. Frontmatter (name, description, publicEmbed) is composed by the tool."),
1442
+ references: z.array(z.object({
1443
+ filename: z.string().describe("Reference filename matching ^[a-z0-9-]+\\.md$ (no path separators)."),
1444
+ content: z.string().describe("Reference file body content."),
1445
+ })).default([]).describe("Optional reference files written under skills/<skillName>/references/."),
1446
+ }, async ({ pluginName, skillName, description, publicEmbed, body, references }) => {
1447
+ const KEBAB_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
1448
+ const REF_RE = /^[a-z0-9-]+\.md$/;
1449
+ if (!KEBAB_RE.test(pluginName)) {
1450
+ console.error(`[store-skill] ERROR pluginName=${pluginName} reason=invalid-kebab path=-`);
1451
+ return { content: [{ type: "text", text: `pluginName must be lowercase kebab-case (1-64 chars, no leading/trailing hyphen): "${pluginName}"` }], isError: true };
1452
+ }
1453
+ if (!KEBAB_RE.test(skillName)) {
1454
+ console.error(`[store-skill] ERROR pluginName=${pluginName} skillName=${skillName} reason=invalid-kebab path=-`);
1455
+ return { content: [{ type: "text", text: `skillName must be lowercase kebab-case (1-64 chars, no leading/trailing hyphen): "${skillName}"` }], isError: true };
1456
+ }
1457
+ for (const ref of references) {
1458
+ if (!REF_RE.test(ref.filename)) {
1459
+ console.error(`[store-skill] ERROR pluginName=${pluginName} skillName=${skillName} reason=invalid-ref-filename ref=${ref.filename} path=-`);
1460
+ return { content: [{ type: "text", text: `reference filename must match ^[a-z0-9-]+\\.md$: "${ref.filename}"` }], isError: true };
1461
+ }
1462
+ }
1463
+ const accountDir = getAccountDir();
1464
+ const canonicalPluginDir = resolve(accountDir, "plugins", pluginName);
1465
+ const mirrorPluginDir = resolve(PLATFORM_ROOT, "plugins", pluginName);
1466
+ const canonicalPluginMd = resolve(canonicalPluginDir, "PLUGIN.md");
1467
+ const mirrorPluginMd = resolve(mirrorPluginDir, "PLUGIN.md");
1468
+ // Collision: mirror dir exists from a shipped plugin (no canonical mirror).
1469
+ // Refuse rather than silently shadow.
1470
+ if (existsSync(mirrorPluginMd) && !existsSync(canonicalPluginMd)) {
1471
+ console.error(`[store-skill] ERROR pluginName=${pluginName} skillName=${skillName} reason=collision-with-shipped path=${mirrorPluginDir}`);
1472
+ return { content: [{ type: "text", text: `pluginName "${pluginName}" collides with a shipped plugin. Choose a different pluginName.` }], isError: true };
1473
+ }
1474
+ const skillDir = resolve(canonicalPluginDir, "skills", skillName);
1475
+ const action = existsSync(resolve(skillDir, "SKILL.md")) ? "overwrite" : "create";
1476
+ try {
1477
+ mkdirSync(canonicalPluginDir, { recursive: true });
1478
+ // Compose PLUGIN.md only on first write. embed=["admin","public"] +
1479
+ // optional=false matches assemblePublicPluginContent / loadEmbeddedPlugins
1480
+ // expectations. metadata is single-line JSON (parsePluginFrontmatter at
1481
+ // plugin-manifest.ts:67 expects single-line) — multi-line silently falls
1482
+ // back to platform={}.
1483
+ if (!existsSync(canonicalPluginMd)) {
1484
+ const pluginMd = `---
1485
+ name: ${pluginName}
1486
+ description: "Operator-authored plugin"
1487
+ tools: []
1488
+ hidden: []
1489
+ requires: []
1490
+ metadata: {"platform":{"embed":["admin","public"],"optional":false}}
1491
+ ---
1492
+
1493
+ # ${pluginName}
1494
+
1495
+ Operator-authored plugin. Skills under \`skills/\` were created via the admin skill-builder.
1496
+ `;
1497
+ writeFileSync(canonicalPluginMd, pluginMd);
1498
+ }
1499
+ // Reset skill dir before write so removed reference files don't linger.
1500
+ if (existsSync(skillDir)) {
1501
+ rmSync(skillDir, { recursive: true, force: true });
1502
+ }
1503
+ mkdirSync(skillDir, { recursive: true });
1504
+ // Normalise description — strip newlines and tabs to keep YAML
1505
+ // frontmatter on a single line. parsePluginFrontmatter uses a
1506
+ // single-line regex; an embedded newline (or `\n---\n`) would
1507
+ // otherwise corrupt the frontmatter parse and silently drop body
1508
+ // content from the agent prompt.
1509
+ const singleLineDescription = description.replace(/[\r\n\t]+/g, " ").trim();
1510
+ const escapedDescription = singleLineDescription.replace(/"/g, '\\"');
1511
+ const skillMd = `---
1512
+ name: ${skillName}
1513
+ description: "${escapedDescription}"
1514
+ publicEmbed: ${publicEmbed}
1515
+ ---
1516
+
1517
+ ${body}
1518
+ `;
1519
+ writeFileSync(resolve(skillDir, "SKILL.md"), skillMd);
1520
+ let refCount = 0;
1521
+ if (references.length > 0) {
1522
+ const refsDir = resolve(skillDir, "references");
1523
+ mkdirSync(refsDir, { recursive: true });
1524
+ for (const ref of references) {
1525
+ writeFileSync(resolve(refsDir, ref.filename), ref.content);
1526
+ refCount++;
1527
+ }
1528
+ }
1529
+ // Write the `.user-mirror` marker to CANONICAL first — cpSync will
1530
+ // carry it into the platform-side mirror as part of the same recursive
1531
+ // copy. This eliminates the post-cpSync writeFileSync race: if cpSync
1532
+ // succeeds, the marker is already in target; if cpSync fails partway,
1533
+ // canonical's marker is intact and the next autoDeliverUserPlugins
1534
+ // call retries the copy. autoDeliverUserPlugins relies on the marker
1535
+ // arriving with the source tree — never writes its own marker.
1536
+ writeFileSync(resolve(canonicalPluginDir, ".user-mirror"), `pluginName=${pluginName}\nsource=${canonicalPluginDir}\n`);
1537
+ // Mirror to <PLATFORM_ROOT>/plugins/<pluginName>/ so the change is
1538
+ // visible to the loader without a session restart.
1539
+ mkdirSync(mirrorPluginDir, { recursive: true });
1540
+ cpSync(canonicalPluginDir, mirrorPluginDir, { recursive: true, force: true });
1541
+ const bodyBytes = Buffer.byteLength(body, "utf-8");
1542
+ console.log(`[store-skill] write pluginName=${pluginName} skillName=${skillName} publicEmbed=${publicEmbed} path=${canonicalPluginDir} bodyBytes=${bodyBytes} referenceCount=${refCount} action=${action}`);
1543
+ return {
1544
+ content: [{
1545
+ type: "text",
1546
+ text: `Skill "${skillName}" saved to plugin "${pluginName}" (${action}). publicEmbed=${publicEmbed}, references=${refCount}. Active immediately for the admin agent${publicEmbed ? "; surfaces in the public agent on next session start" : ""}.`,
1547
+ }],
1548
+ };
1549
+ }
1550
+ catch (err) {
1551
+ const msg = err instanceof Error ? err.message : String(err);
1552
+ console.error(`[store-skill] ERROR pluginName=${pluginName} skillName=${skillName} reason=${msg.slice(0, 120)} path=${canonicalPluginDir}`);
1553
+ return { content: [{ type: "text", text: `Failed to write skill: ${msg}` }], isError: true };
1554
+ }
1555
+ });
1424
1556
  server.tool("plugin-read", "Read a plugin definition (PLUGIN.md) or one of its reference files.", {
1425
1557
  pluginName: z.string().describe("Name of the plugin directory (e.g. 'sales', 'business-assistant')"),
1426
1558
  file: z.string().optional().describe("Specific file to read (e.g. 'references/pricing.md'). Defaults to PLUGIN.md."),