@rubytech/create-realagent-code 0.1.583 → 0.1.585

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 (30) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -0
  3. package/payload/platform/lib/brand-templating/dist/index.d.ts +13 -7
  4. package/payload/platform/lib/brand-templating/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/brand-templating/dist/index.js +44 -29
  6. package/payload/platform/lib/brand-templating/dist/index.js.map +1 -1
  7. package/payload/platform/lib/brand-templating/src/__tests__/brand-templating.test.ts +103 -0
  8. package/payload/platform/lib/brand-templating/src/index.ts +45 -29
  9. package/payload/platform/lib/brand-templating/tsconfig.json +2 -1
  10. package/payload/platform/lib/brand-templating/vitest.config.ts +9 -0
  11. package/payload/platform/lib/telegram-reach/dist/index.d.ts +6 -0
  12. package/payload/platform/lib/telegram-reach/dist/index.d.ts.map +1 -1
  13. package/payload/platform/lib/telegram-reach/dist/index.js +38 -3
  14. package/payload/platform/lib/telegram-reach/dist/index.js.map +1 -1
  15. package/payload/platform/lib/telegram-reach/src/__tests__/telegram-reach.test.ts +54 -2
  16. package/payload/platform/lib/telegram-reach/src/index.ts +38 -4
  17. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -1
  18. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  19. package/payload/platform/plugins/docs/references/platform.md +2 -0
  20. package/payload/platform/plugins/docs/references/telegram-guide.md +4 -0
  21. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -1
  23. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  24. package/payload/platform/templates/agents/admin/IDENTITY.md +11 -5
  25. package/payload/server/{chunk-CV6AEMKH.js → chunk-34FPJYHN.js} +1 -1
  26. package/payload/server/{chunk-SKYZG6OT.js → chunk-6AQYMT6C.js} +27 -21
  27. package/payload/server/{chunk-BTXRBXMU.js → chunk-GO3EPIXU.js} +1 -1
  28. package/payload/server/{manager-FQSQSSJ3.js → manager-7BXVB76I.js} +2 -2
  29. package/payload/server/maxy-edge.js +2 -2
  30. package/payload/server/server.js +305 -297
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent-code",
3
- "version": "0.1.583",
3
+ "version": "0.1.585",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent-code": "./dist/index.js"
@@ -5,6 +5,7 @@
5
5
  "installDir": "realagent-code",
6
6
  "configDir": ".realagent-code",
7
7
  "tagline": "Built for agents. By agents.",
8
+ "productMandate": "You are the back office of an estate agency. Your work is the administration behind properties, listings, viewings, offers and applicants: records kept straight, correspondence answered, progress chased, deadlines met.",
8
9
  "marketingUrl": "https://realagent.network",
9
10
  "neo4jPort": 7688,
10
11
  "ttydPort": 7682,
@@ -1,18 +1,24 @@
1
1
  /**
2
- * Brand templating — single source of truth for the operator-visible brand name.
2
+ * Brand templating — single source of truth for the operator-visible brand
3
+ * name and the admin agent's stated purpose.
3
4
  *
4
5
  * Skills, agent templates, plugin manifests, and reference content reference
5
- * the brand only via the literal `{{productName}}` placeholder. This module
6
- * substitutes at read time from `brand.json`. Same shadow-source defect class
7
- * as Tasks 787/788: there is no fallback; missing or empty `productName`
8
- * hard-fails so the wrong brand can never reach the system prompt or the
9
- * operator UI silently.
6
+ * the brand only via literal `{{field}}` placeholders, one per entry in
7
+ * `BRAND_FIELDS`. This module substitutes at read time from `brand.json`. Same
8
+ * shadow-source defect class as Tasks 787/788: there is no fallback; a missing
9
+ * or empty value hard-fails so the wrong brand can never reach the system
10
+ * prompt or the operator UI silently.
10
11
  *
11
12
  * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
12
13
  * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
13
14
  * pass the absolute file path as `sourcePath` so the diagnostic line names
14
15
  * exactly which file was substituted.
15
16
  */
16
- export declare function getBrandProductName(): string;
17
+ /** Every brand.json field a template may reference as `{{field}}`.
18
+ * Task 2646 added `productMandate`, the admin agent's stated purpose. */
19
+ declare const BRAND_FIELDS: readonly ["productName", "productMandate"];
20
+ export type BrandField = (typeof BRAND_FIELDS)[number];
21
+ export declare function getBrandValue(field: BrandField): string;
17
22
  export declare function substituteBrandPlaceholders(content: string, sourcePath: string): string;
23
+ export {};
18
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAmBH,wBAAgB,mBAAmB,IAAI,MAAM,CAsB5C;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkBvF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH;0EAC0E;AAC1E,QAAA,MAAM,YAAY,4CAA6C,CAAC;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAiBvD,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAwBvD;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAyBvF"}
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  /**
3
- * Brand templating — single source of truth for the operator-visible brand name.
3
+ * Brand templating — single source of truth for the operator-visible brand
4
+ * name and the admin agent's stated purpose.
4
5
  *
5
6
  * Skills, agent templates, plugin manifests, and reference content reference
6
- * the brand only via the literal `{{productName}}` placeholder. This module
7
- * substitutes at read time from `brand.json`. Same shadow-source defect class
8
- * as Tasks 787/788: there is no fallback; missing or empty `productName`
9
- * hard-fails so the wrong brand can never reach the system prompt or the
10
- * operator UI silently.
7
+ * the brand only via literal `{{field}}` placeholders, one per entry in
8
+ * `BRAND_FIELDS`. This module substitutes at read time from `brand.json`. Same
9
+ * shadow-source defect class as Tasks 787/788: there is no fallback; a missing
10
+ * or empty value hard-fails so the wrong brand can never reach the system
11
+ * prompt or the operator UI silently.
11
12
  *
12
13
  * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
13
14
  * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
@@ -15,12 +16,17 @@
15
16
  * exactly which file was substituted.
16
17
  */
17
18
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.getBrandProductName = getBrandProductName;
19
+ exports.getBrandValue = getBrandValue;
19
20
  exports.substituteBrandPlaceholders = substituteBrandPlaceholders;
20
21
  const node_path_1 = require("node:path");
21
22
  const node_fs_1 = require("node:fs");
22
- const PLACEHOLDER = "{{productName}}";
23
- let cachedProductName = null;
23
+ /** Every brand.json field a template may reference as `{{field}}`.
24
+ * Task 2646 added `productMandate`, the admin agent's stated purpose. */
25
+ const BRAND_FIELDS = ["productName", "productMandate"];
26
+ /** Keyed by manifest path and field, not by field alone: a process that
27
+ * changes MAXY_PLATFORM_ROOT (only tests do) must not read a previous root's
28
+ * value back out of the cache. */
29
+ const cache = new Map();
24
30
  function brandJsonPath() {
25
31
  const platformRoot = process.env.MAXY_PLATFORM_ROOT;
26
32
  if (!platformRoot) {
@@ -28,10 +34,12 @@ function brandJsonPath() {
28
34
  }
29
35
  return (0, node_path_1.join)(platformRoot, "config", "brand.json");
30
36
  }
31
- function getBrandProductName() {
32
- if (cachedProductName !== null)
33
- return cachedProductName;
37
+ function getBrandValue(field) {
34
38
  const path = brandJsonPath();
39
+ const key = `${path}::${field}`;
40
+ const cached = cache.get(key);
41
+ if (cached !== undefined)
42
+ return cached;
35
43
  if (!(0, node_fs_1.existsSync)(path)) {
36
44
  throw new Error(`[skill-loader] brand.json missing at ${path}`);
37
45
  }
@@ -42,28 +50,35 @@ function getBrandProductName() {
42
50
  catch (err) {
43
51
  throw new Error(`[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`);
44
52
  }
45
- const value = parsed.productName;
53
+ const value = parsed[field];
46
54
  if (typeof value !== "string" || value.trim() === "") {
47
- throw new Error(`[skill-loader] brand.json at ${path} has missing or empty productName`);
55
+ throw new Error(`[skill-loader] brand.json at ${path} has missing or empty ${field}`);
48
56
  }
49
- cachedProductName = value;
57
+ cache.set(key, value);
50
58
  return value;
51
59
  }
52
60
  function substituteBrandPlaceholders(content, sourcePath) {
53
- if (!content.includes(PLACEHOLDER))
54
- return content;
55
- let productName;
56
- try {
57
- productName = getBrandProductName();
58
- }
59
- catch (err) {
60
- console.error(`[skill-loader] ERROR: brand.json missing — cannot resolve productName for skill ${sourcePath}`);
61
- throw err;
61
+ let out = content;
62
+ for (const field of BRAND_FIELDS) {
63
+ const placeholder = `{{${field}}}`;
64
+ // Per-placeholder, not per-call: a file using only one placeholder must
65
+ // never read the other field, so a brand.json predating a field keeps
66
+ // rendering every file that does not use it.
67
+ if (!out.includes(placeholder))
68
+ continue;
69
+ let value;
70
+ try {
71
+ value = getBrandValue(field);
72
+ }
73
+ catch (err) {
74
+ console.error(`[skill-loader] ERROR: brand.json missing — cannot resolve ${field} for skill ${sourcePath}`);
75
+ throw err;
76
+ }
77
+ // split-length counts non-overlapping literal matches without regex-escape risk.
78
+ const occurrences = out.split(placeholder).length - 1;
79
+ out = out.split(placeholder).join(value);
80
+ console.log(`[skill-loader] brand-substituted ${field}=${value} skill=${sourcePath} occurrences=${occurrences}`);
62
81
  }
63
- // split-length counts non-overlapping literal matches without regex-escape risk.
64
- const occurrences = content.split(PLACEHOLDER).length - 1;
65
- const substituted = content.split(PLACEHOLDER).join(productName);
66
- console.log(`[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`);
67
- return substituted;
82
+ return out;
68
83
  }
69
84
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAmBH,kDAsBC;AAED,kEAkBC;AA3DD,yCAAiC;AACjC,qCAAmD;AAEnD,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC,IAAI,iBAAiB,GAAkB,IAAI,CAAC;AAE5C,SAAS,aAAa;IACpB,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACpD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;IACJ,CAAC;IACD,OAAO,IAAA,gBAAI,EAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,mBAAmB;IACjC,IAAI,iBAAiB,KAAK,IAAI;QAAE,OAAO,iBAAiB,CAAC;IACzD,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,IAAI,CAAC,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,MAAiC,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,2CAA2C,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACvG,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,gCAAgC,IAAI,mCAAmC,CACxE,CAAC;IACJ,CAAC;IACD,iBAAiB,GAAG,KAAK,CAAC;IAC1B,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAgB,2BAA2B,CAAC,OAAe,EAAE,UAAkB;IAC7E,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,OAAO,CAAC;IACnD,IAAI,WAAmB,CAAC;IACxB,IAAI,CAAC;QACH,WAAW,GAAG,mBAAmB,EAAE,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CACX,mFAAmF,UAAU,EAAE,CAChG,CAAC;QACF,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,iFAAiF;IACjF,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CACT,gDAAgD,WAAW,UAAU,UAAU,gBAAgB,WAAW,EAAE,CAC7G,CAAC;IACF,OAAO,WAAW,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ // Task 2646 — the admin identity carries a per-brand purpose, so brand.json now
2
+ // supplies two fields rather than one. These cases pin the property that makes
3
+ // that safe: each placeholder resolves independently, so a manifest predating a
4
+ // field keeps rendering every file that does not use it.
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { getBrandValue, substituteBrandPlaceholders } from "../index";
10
+
11
+ const roots: string[] = [];
12
+ const previousRoot = process.env.MAXY_PLATFORM_ROOT;
13
+
14
+ /** Seed a fresh platform root holding one brand.json and point the env at it.
15
+ * Fresh per test, so the path-keyed cache never carries a value across. */
16
+ function seedBrand(brand: Record<string, unknown>): string {
17
+ const root = mkdtempSync(join(tmpdir(), "brand-templating-"));
18
+ roots.push(root);
19
+ mkdirSync(join(root, "config"), { recursive: true });
20
+ writeFileSync(join(root, "config", "brand.json"), JSON.stringify(brand));
21
+ process.env.MAXY_PLATFORM_ROOT = root;
22
+ return root;
23
+ }
24
+
25
+ afterEach(() => {
26
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
27
+ if (previousRoot === undefined) delete process.env.MAXY_PLATFORM_ROOT;
28
+ else process.env.MAXY_PLATFORM_ROOT = previousRoot;
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ describe("substituteBrandPlaceholders", () => {
33
+ it("substitutes both placeholders in one pass", () => {
34
+ seedBrand({ productName: "Real Agent", productMandate: "You are the back office." });
35
+ const out = substituteBrandPlaceholders(
36
+ "{{productName}} — {{productMandate}}",
37
+ "/tmp/IDENTITY.md",
38
+ );
39
+ expect(out).toBe("Real Agent — You are the back office.");
40
+ });
41
+
42
+ it("never reads productMandate for content that only uses productName", () => {
43
+ // The regression this guards: a brand.json predating the new field must keep
44
+ // rendering every existing productName-only file.
45
+ seedBrand({ productName: "Maxy" });
46
+ expect(substituteBrandPlaceholders("Hello from {{productName}}", "/tmp/x.md")).toBe(
47
+ "Hello from Maxy",
48
+ );
49
+ });
50
+
51
+ it("returns content unchanged and logs nothing when no placeholder is present", () => {
52
+ seedBrand({ productName: "Maxy", productMandate: "m" });
53
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
54
+ const content = "no placeholders here";
55
+ expect(substituteBrandPlaceholders(content, "/tmp/x.md")).toBe(content);
56
+ expect(log).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it("replaces every occurrence of the mandate placeholder", () => {
60
+ seedBrand({ productName: "Maxy", productMandate: "M" });
61
+ expect(substituteBrandPlaceholders("{{productMandate}}/{{productMandate}}", "/tmp/x.md")).toBe(
62
+ "M/M",
63
+ );
64
+ });
65
+
66
+ it("logs the substitution with field, value and occurrence count", () => {
67
+ seedBrand({ productName: "Maxy", productMandate: "M" });
68
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
69
+ substituteBrandPlaceholders("{{productMandate}} {{productMandate}}", "/tmp/IDENTITY.md");
70
+ expect(log).toHaveBeenCalledWith(
71
+ "[skill-loader] brand-substituted productMandate=M skill=/tmp/IDENTITY.md occurrences=2",
72
+ );
73
+ });
74
+
75
+ it("throws when the mandate placeholder is used and the field is missing", () => {
76
+ seedBrand({ productName: "Maxy" });
77
+ expect(() => substituteBrandPlaceholders("{{productMandate}}", "/tmp/x.md")).toThrow(
78
+ /missing or empty productMandate/,
79
+ );
80
+ });
81
+
82
+ it("throws when the mandate field is not a string", () => {
83
+ seedBrand({ productName: "Maxy", productMandate: 42 });
84
+ expect(() => substituteBrandPlaceholders("{{productMandate}}", "/tmp/x.md")).toThrow(
85
+ /missing or empty productMandate/,
86
+ );
87
+ });
88
+
89
+ it("throws when the mandate field is whitespace only", () => {
90
+ seedBrand({ productName: "Maxy", productMandate: " " });
91
+ expect(() => substituteBrandPlaceholders("{{productMandate}}", "/tmp/x.md")).toThrow(
92
+ /missing or empty productMandate/,
93
+ );
94
+ });
95
+ });
96
+
97
+ describe("getBrandValue", () => {
98
+ it("reads each field independently from the same manifest", () => {
99
+ seedBrand({ productName: "SiteDesk", productMandate: "jobs" });
100
+ expect(getBrandValue("productName")).toBe("SiteDesk");
101
+ expect(getBrandValue("productMandate")).toBe("jobs");
102
+ });
103
+ });
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Brand templating — single source of truth for the operator-visible brand name.
2
+ * Brand templating — single source of truth for the operator-visible brand
3
+ * name and the admin agent's stated purpose.
3
4
  *
4
5
  * Skills, agent templates, plugin manifests, and reference content reference
5
- * the brand only via the literal `{{productName}}` placeholder. This module
6
- * substitutes at read time from `brand.json`. Same shadow-source defect class
7
- * as Tasks 787/788: there is no fallback; missing or empty `productName`
8
- * hard-fails so the wrong brand can never reach the system prompt or the
9
- * operator UI silently.
6
+ * the brand only via literal `{{field}}` placeholders, one per entry in
7
+ * `BRAND_FIELDS`. This module substitutes at read time from `brand.json`. Same
8
+ * shadow-source defect class as Tasks 787/788: there is no fallback; a missing
9
+ * or empty value hard-fails so the wrong brand can never reach the system
10
+ * prompt or the operator UI silently.
10
11
  *
11
12
  * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
12
13
  * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
@@ -17,9 +18,15 @@
17
18
  import { join } from "node:path";
18
19
  import { existsSync, readFileSync } from "node:fs";
19
20
 
20
- const PLACEHOLDER = "{{productName}}";
21
+ /** Every brand.json field a template may reference as `{{field}}`.
22
+ * Task 2646 added `productMandate`, the admin agent's stated purpose. */
23
+ const BRAND_FIELDS = ["productName", "productMandate"] as const;
24
+ export type BrandField = (typeof BRAND_FIELDS)[number];
21
25
 
22
- let cachedProductName: string | null = null;
26
+ /** Keyed by manifest path and field, not by field alone: a process that
27
+ * changes MAXY_PLATFORM_ROOT (only tests do) must not read a previous root's
28
+ * value back out of the cache. */
29
+ const cache = new Map<string, string>();
23
30
 
24
31
  function brandJsonPath(): string {
25
32
  const platformRoot = process.env.MAXY_PLATFORM_ROOT;
@@ -31,13 +38,15 @@ function brandJsonPath(): string {
31
38
  return join(platformRoot, "config", "brand.json");
32
39
  }
33
40
 
34
- export function getBrandProductName(): string {
35
- if (cachedProductName !== null) return cachedProductName;
41
+ export function getBrandValue(field: BrandField): string {
36
42
  const path = brandJsonPath();
43
+ const key = `${path}::${field}`;
44
+ const cached = cache.get(key);
45
+ if (cached !== undefined) return cached;
37
46
  if (!existsSync(path)) {
38
47
  throw new Error(`[skill-loader] brand.json missing at ${path}`);
39
48
  }
40
- let parsed: { productName?: unknown };
49
+ let parsed: Record<string, unknown>;
41
50
  try {
42
51
  parsed = JSON.parse(readFileSync(path, "utf-8"));
43
52
  } catch (err) {
@@ -45,32 +54,39 @@ export function getBrandProductName(): string {
45
54
  `[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`,
46
55
  );
47
56
  }
48
- const value = parsed.productName;
57
+ const value = parsed[field];
49
58
  if (typeof value !== "string" || value.trim() === "") {
50
59
  throw new Error(
51
- `[skill-loader] brand.json at ${path} has missing or empty productName`,
60
+ `[skill-loader] brand.json at ${path} has missing or empty ${field}`,
52
61
  );
53
62
  }
54
- cachedProductName = value;
63
+ cache.set(key, value);
55
64
  return value;
56
65
  }
57
66
 
58
67
  export function substituteBrandPlaceholders(content: string, sourcePath: string): string {
59
- if (!content.includes(PLACEHOLDER)) return content;
60
- let productName: string;
61
- try {
62
- productName = getBrandProductName();
63
- } catch (err) {
64
- console.error(
65
- `[skill-loader] ERROR: brand.json missing — cannot resolve productName for skill ${sourcePath}`,
68
+ let out = content;
69
+ for (const field of BRAND_FIELDS) {
70
+ const placeholder = `{{${field}}}`;
71
+ // Per-placeholder, not per-call: a file using only one placeholder must
72
+ // never read the other field, so a brand.json predating a field keeps
73
+ // rendering every file that does not use it.
74
+ if (!out.includes(placeholder)) continue;
75
+ let value: string;
76
+ try {
77
+ value = getBrandValue(field);
78
+ } catch (err) {
79
+ console.error(
80
+ `[skill-loader] ERROR: brand.json missing — cannot resolve ${field} for skill ${sourcePath}`,
81
+ );
82
+ throw err;
83
+ }
84
+ // split-length counts non-overlapping literal matches without regex-escape risk.
85
+ const occurrences = out.split(placeholder).length - 1;
86
+ out = out.split(placeholder).join(value);
87
+ console.log(
88
+ `[skill-loader] brand-substituted ${field}=${value} skill=${sourcePath} occurrences=${occurrences}`,
66
89
  );
67
- throw err;
68
90
  }
69
- // split-length counts non-overlapping literal matches without regex-escape risk.
70
- const occurrences = content.split(PLACEHOLDER).length - 1;
71
- const substituted = content.split(PLACEHOLDER).join(productName);
72
- console.log(
73
- `[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`,
74
- );
75
- return substituted;
91
+ return out;
76
92
  }
@@ -4,5 +4,6 @@
4
4
  "outDir": "dist",
5
5
  "rootDir": "src"
6
6
  },
7
- "include": ["src"]
7
+ "include": ["src"],
8
+ "exclude": ["src/__tests__"]
8
9
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ globals: false,
7
+ include: ["src/__tests__/**/*.test.ts"],
8
+ },
9
+ });
@@ -77,4 +77,10 @@ export declare function collectTelegramReach(input: {
77
77
  storeDir: string;
78
78
  bots: ReachBotInput[];
79
79
  }): ReachResult;
80
+ /** The brand's Telegram message store root, for a given platform root.
81
+ *
82
+ * The ONE derivation of this path. `ui/app/lib/telegram/message-store.ts`
83
+ * writes under it and `plugins/telegram/mcp` reads under it, both through this
84
+ * function, so the writer and the reader cannot name different directories. */
85
+ export declare function telegramStoreRoot(platformRoot: string): string;
80
86
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;;6EAK6E;AAC7E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;;kEAKkE;AAClE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;;WAKW;AACX,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD;AAED;4DAC4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB;+CAC2C;IAC3C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qEAAqE;IACrE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;4EACwE;IACxE,KAAK,EAAE,OAAO,CAAC;IACf,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gFAAgF;IAChF,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB;;;;4BAIwB;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AA+FD;iEACiE;AACjE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,aAAa,EAAE,CAAC;CACvB,GAAG,WAAW,CAsCd"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;;6EAK6E;AAC7E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;;kEAKkE;AAClE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;;WAKW;AACX,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD;AAED;4DAC4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB;+CAC2C;IAC3C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qEAAqE;IACrE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;4EACwE;IACxE,KAAK,EAAE,OAAO,CAAC;IACf,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gFAAgF;IAChF,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB;;;;4BAIwB;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AA+FD;iEACiE;AACjE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,aAAa,EAAE,CAAC;CACvB,GAAG,WAAW,CAsCd;AAqBD;;;;gFAIgF;AAChF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG9D"}
@@ -5,6 +5,7 @@ exports.isBusinessChannelKey = isBusinessChannelKey;
5
5
  exports.channelKeyToFileStem = channelKeyToFileStem;
6
6
  exports.fileStemToChannelKey = fileStemToChannelKey;
7
7
  exports.collectTelegramReach = collectTelegramReach;
8
+ exports.telegramStoreRoot = telegramStoreRoot;
8
9
  /**
9
10
  * Task 2625 — which Telegram chats a registered bot can already message.
10
11
  *
@@ -22,11 +23,16 @@ exports.collectTelegramReach = collectTelegramReach;
22
23
  * re-exports it, so the store that writes a filename and the walk that reads one
23
24
  * cannot disagree.
24
25
  *
25
- * Path-free by construction: the caller passes `storeDir`. The plugin derives it
26
- * from PLATFORM_ROOT and the server from the brand config dir, and neither
27
- * derivation belongs in a library that must not know what a brand is.
26
+ * `collectTelegramReach` takes `storeDir` from its caller, but the DERIVATION of
27
+ * that directory lives here too, in `telegramStoreRoot`. Task 2625 left it to
28
+ * each caller on the grounds that a library must not know what a brand is, and
29
+ * the two callers promptly disagreed: the webhook wrote the brand config dir
30
+ * while `telegram-reach` read the brand install dir, so the tool reported every
31
+ * chat unreachable and named its own live sender as never having messaged
32
+ * (Task 2644). One derivation, for the same reason the filename rule is here.
28
33
  */
29
34
  const node_fs_1 = require("node:fs");
35
+ const node_os_1 = require("node:os");
30
36
  const node_path_1 = require("node:path");
31
37
  /**
32
38
  * Is this a channel key that can safely become a path segment?
@@ -208,4 +214,33 @@ function collectTelegramReach(input) {
208
214
  });
209
215
  return { bots, storeRows };
210
216
  }
217
+ /** The brand's config directory NAME, from its own brand.json.
218
+ *
219
+ * Resolved WITHOUT the platform's `paths.ts`, which throws at module init on a
220
+ * brand.json missing a port field. The inbound Telegram webhook imports this
221
+ * path, so a throw here takes delivery down; a brand.json that cannot be read
222
+ * falls back to `.maxy` instead. */
223
+ function configDirName(platformRoot) {
224
+ const brandPath = (0, node_path_1.join)(platformRoot, "config", "brand.json");
225
+ if ((0, node_fs_1.existsSync)(brandPath)) {
226
+ try {
227
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(brandPath, "utf8"));
228
+ if (typeof parsed.configDir === "string")
229
+ return parsed.configDir;
230
+ }
231
+ catch {
232
+ // A brand.json that will not parse falls back rather than throwing.
233
+ }
234
+ }
235
+ return ".maxy";
236
+ }
237
+ /** The brand's Telegram message store root, for a given platform root.
238
+ *
239
+ * The ONE derivation of this path. `ui/app/lib/telegram/message-store.ts`
240
+ * writes under it and `plugins/telegram/mcp` reads under it, both through this
241
+ * function, so the writer and the reader cannot name different directories. */
242
+ function telegramStoreRoot(platformRoot) {
243
+ const brandDir = process.env.MAXY_DIR_OVERRIDE ?? (0, node_path_1.resolve)((0, node_os_1.homedir)(), configDirName(platformRoot));
244
+ return (0, node_path_1.join)(brandDir, "data", "telegram-messages");
245
+ }
211
246
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir, homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
6
6
  collectTelegramReach,
@@ -8,6 +8,7 @@ import {
8
8
  isBusinessChannelKey,
9
9
  channelKeyToFileStem,
10
10
  fileStemToChannelKey,
11
+ telegramStoreRoot,
11
12
  } from "../index.js";
12
13
 
13
14
  const BOT = "8801383568";
@@ -214,3 +215,54 @@ describe("collectTelegramReach", () => {
214
215
  expect(out.bots[0].chats.map((c) => c.senderId)).toEqual([NEO, KIT, "5555555555"]);
215
216
  });
216
217
  });
218
+
219
+ describe("telegramStoreRoot", () => {
220
+ let root: string;
221
+
222
+ beforeEach(() => {
223
+ root = mkdtempSync(join(tmpdir(), "tg-store-root-"));
224
+ delete process.env.MAXY_DIR_OVERRIDE;
225
+ });
226
+
227
+ afterEach(() => {
228
+ delete process.env.MAXY_DIR_OVERRIDE;
229
+ rmSync(root, { recursive: true, force: true });
230
+ });
231
+
232
+ function writeBrand(configDir: unknown): string {
233
+ const platformRoot = join(root, "platform");
234
+ mkdirSync(join(platformRoot, "config"), { recursive: true });
235
+ writeFileSync(
236
+ join(platformRoot, "config", "brand.json"),
237
+ typeof configDir === "string" ? JSON.stringify({ configDir }) : "{ not json",
238
+ "utf8",
239
+ );
240
+ return platformRoot;
241
+ }
242
+
243
+ it("reads configDir from brand.json", () => {
244
+ expect(telegramStoreRoot(writeBrand(".maxy-code"))).toBe(
245
+ join(homedir(), ".maxy-code", "data", "telegram-messages"),
246
+ );
247
+ });
248
+
249
+ it("falls back to .maxy when brand.json is absent", () => {
250
+ const platformRoot = join(root, "platform");
251
+ mkdirSync(platformRoot, { recursive: true });
252
+ expect(telegramStoreRoot(platformRoot)).toBe(
253
+ join(homedir(), ".maxy", "data", "telegram-messages"),
254
+ );
255
+ });
256
+
257
+ it("falls back to .maxy when brand.json is unparseable, without throwing", () => {
258
+ expect(telegramStoreRoot(writeBrand(null))).toBe(
259
+ join(homedir(), ".maxy", "data", "telegram-messages"),
260
+ );
261
+ });
262
+
263
+ it("MAXY_DIR_OVERRIDE wins over brand.json", () => {
264
+ const platformRoot = writeBrand(".maxy-code");
265
+ process.env.MAXY_DIR_OVERRIDE = root;
266
+ expect(telegramStoreRoot(platformRoot)).toBe(join(root, "data", "telegram-messages"));
267
+ });
268
+ });