@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.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/lib/brand-templating/dist/index.d.ts +13 -7
- package/payload/platform/lib/brand-templating/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/brand-templating/dist/index.js +44 -29
- package/payload/platform/lib/brand-templating/dist/index.js.map +1 -1
- package/payload/platform/lib/brand-templating/src/__tests__/brand-templating.test.ts +103 -0
- package/payload/platform/lib/brand-templating/src/index.ts +45 -29
- package/payload/platform/lib/brand-templating/tsconfig.json +2 -1
- package/payload/platform/lib/brand-templating/vitest.config.ts +9 -0
- package/payload/platform/lib/telegram-reach/dist/index.d.ts +6 -0
- package/payload/platform/lib/telegram-reach/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/telegram-reach/dist/index.js +38 -3
- package/payload/platform/lib/telegram-reach/dist/index.js.map +1 -1
- package/payload/platform/lib/telegram-reach/src/__tests__/telegram-reach.test.ts +54 -2
- package/payload/platform/lib/telegram-reach/src/index.ts +38 -4
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -1
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/docs/references/platform.md +2 -0
- package/payload/platform/plugins/docs/references/telegram-guide.md +4 -0
- 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 +0 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +11 -5
- package/payload/server/{chunk-CV6AEMKH.js → chunk-34FPJYHN.js} +1 -1
- package/payload/server/{chunk-SKYZG6OT.js → chunk-6AQYMT6C.js} +27 -21
- package/payload/server/{chunk-BTXRBXMU.js → chunk-GO3EPIXU.js} +1 -1
- package/payload/server/{manager-FQSQSSJ3.js → manager-7BXVB76I.js} +2 -2
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/server.js +305 -297
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
6
|
-
* substitutes at read time from `brand.json`. Same
|
|
7
|
-
* as Tasks 787/788: there is no fallback; missing
|
|
8
|
-
* hard-fails so the wrong brand can never reach the system
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
7
|
-
* substitutes at read time from `brand.json`. Same
|
|
8
|
-
* as Tasks 787/788: there is no fallback; missing
|
|
9
|
-
* hard-fails so the wrong brand can never reach the system
|
|
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.
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
55
|
+
throw new Error(`[skill-loader] brand.json at ${path} has missing or empty ${field}`);
|
|
48
56
|
}
|
|
49
|
-
|
|
57
|
+
cache.set(key, value);
|
|
50
58
|
return value;
|
|
51
59
|
}
|
|
52
60
|
function substituteBrandPlaceholders(content, sourcePath) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
6
|
-
* substitutes at read time from `brand.json`. Same
|
|
7
|
-
* as Tasks 787/788: there is no fallback; missing
|
|
8
|
-
* hard-fails so the wrong brand can never reach the system
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
60
|
+
`[skill-loader] brand.json at ${path} has missing or empty ${field}`,
|
|
52
61
|
);
|
|
53
62
|
}
|
|
54
|
-
|
|
63
|
+
cache.set(key, value);
|
|
55
64
|
return value;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
export function substituteBrandPlaceholders(content: string, sourcePath: string): string {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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":"
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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":";;
|
|
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
|
+
});
|