@agimon-ai/po-core 0.0.0-alpha.106 → 0.0.0-alpha.108

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.
@@ -0,0 +1,5 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_index = require("./src/services/inputTemplate/index.cjs");
3
+ exports.hasInputTemplate = require_index.hasInputTemplate;
4
+ exports.parseInputTemplate = require_index.parseInputTemplate;
5
+ exports.renderInputTemplate = require_index.renderInputTemplate;
@@ -0,0 +1,3 @@
1
+ import { InputTemplateOptions, InputTemplatePart } from "./src/services/inputTemplate/type.mjs";
2
+ import { hasInputTemplate, parseInputTemplate, renderInputTemplate } from "./src/services/inputTemplate/index.mjs";
3
+ export { type InputTemplateOptions, type InputTemplatePart, hasInputTemplate, parseInputTemplate, renderInputTemplate };
@@ -0,0 +1 @@
1
+ export { hasInputTemplate, parseInputTemplate, renderInputTemplate } from "./src/services/inputTemplate/index.mjs";
@@ -0,0 +1,120 @@
1
+ let liquidjs = require("liquidjs");
2
+ //#region src/services/inputTemplate/index.ts
3
+ const MAX_BYTES = 65536;
4
+ const MAX_REFERENCES = 16;
5
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
6
+ const FORBIDDEN = /* @__PURE__ */ new Set([
7
+ "__proto__",
8
+ "prototype",
9
+ "constructor"
10
+ ]);
11
+ const liquid = new liquidjs.Liquid({
12
+ strictVariables: true,
13
+ strictFilters: true,
14
+ ownPropertyOnly: true
15
+ });
16
+ function invalid() {
17
+ return /* @__PURE__ */ new Error("Unsupported input template");
18
+ }
19
+ function validName(name) {
20
+ return NAME.test(name) && !FORBIDDEN.has(name);
21
+ }
22
+ /** Includes unsupported template openings so they fail closed rather than being typed literally. */
23
+ function hasInputTemplate(value) {
24
+ return /\$\{|\{\{|\{%/u.test(value);
25
+ }
26
+ /** Interpolation only. No tags, filters, dynamic keys, expressions or recursive evaluation. */
27
+ function parseInputTemplate(value) {
28
+ if (typeof value !== "string" || new TextEncoder().encode(value).length > MAX_BYTES) throw invalid();
29
+ const parts = [];
30
+ const openings = /\$\{|\{\{|\{%/gu;
31
+ let cursor = 0;
32
+ let references = 0;
33
+ for (let match = openings.exec(value); match; match = openings.exec(value)) {
34
+ const open = match.index;
35
+ if (open > cursor) parts.push({
36
+ kind: "text",
37
+ text: value.slice(cursor, open)
38
+ });
39
+ if (match[0] === "{%") throw invalid();
40
+ const closing = match[0] === "${" ? "}" : "}}";
41
+ const close = value.indexOf(closing, open + 2);
42
+ if (close === -1) throw invalid();
43
+ const reference = /^(env|secret|secrets)\.([A-Za-z_][A-Za-z0-9_]*)$/u.exec(value.slice(open + 2, close).trim());
44
+ if (!reference || !validName(reference[2]) || ++references > MAX_REFERENCES) throw invalid();
45
+ parts.push({
46
+ kind: reference[1] === "env" ? "env" : "secret",
47
+ name: reference[2]
48
+ });
49
+ cursor = close + closing.length;
50
+ openings.lastIndex = cursor;
51
+ }
52
+ if (references === 0) throw invalid();
53
+ if (cursor < value.length) parts.push({
54
+ kind: "text",
55
+ text: value.slice(cursor)
56
+ });
57
+ return parts;
58
+ }
59
+ async function abortable(work, signal) {
60
+ signal.throwIfAborted();
61
+ let cancel = () => {};
62
+ const aborted = new Promise((_resolve, reject) => {
63
+ cancel = () => reject(invalid());
64
+ signal.addEventListener("abort", cancel, { once: true });
65
+ });
66
+ try {
67
+ return await Promise.race([work, aborted]);
68
+ } finally {
69
+ signal.removeEventListener("abort", cancel);
70
+ }
71
+ }
72
+ /** Resolves host-declared values, then renders them as Liquid data, never as Liquid source. */
73
+ async function renderInputTemplate(parts, options) {
74
+ try {
75
+ let references = 0;
76
+ let bytes = 0;
77
+ for (const part of parts) if (part.kind === "text" && typeof part.text === "string") bytes += new TextEncoder().encode(part.text).length;
78
+ else if ((part.kind === "env" || part.kind === "secret") && validName(part.name)) references += 1;
79
+ else throw invalid();
80
+ if (references > MAX_REFERENCES || bytes > MAX_BYTES || parts.length > 33) throw invalid();
81
+ const values = Object.create(null);
82
+ const cache = /* @__PURE__ */ new Map();
83
+ let source = "";
84
+ for (const [index, part] of parts.entries()) {
85
+ options.signal.throwIfAborted();
86
+ let value;
87
+ if (part.kind === "text") value = part.text;
88
+ else {
89
+ const key = `${part.kind}.${part.name}`;
90
+ if (cache.has(key)) value = cache.get(key);
91
+ else {
92
+ const resolved = part.kind === "env" ? options.readEnv(part.name) : await abortable(Promise.resolve().then(() => {
93
+ options.signal.throwIfAborted();
94
+ return options.resolveSecret(part.name);
95
+ }), options.signal);
96
+ if (typeof resolved !== "string" || part.kind === "secret" && !resolved) throw invalid();
97
+ value = resolved;
98
+ if (part.kind === "secret") options.onResolved(value);
99
+ cache.set(key, value);
100
+ }
101
+ }
102
+ bytes += part.kind === "text" ? 0 : new TextEncoder().encode(value).length;
103
+ if (bytes > MAX_BYTES) throw invalid();
104
+ values[`v${index}`] = value;
105
+ source += `{{ values.v${index} }}`;
106
+ }
107
+ options.signal.throwIfAborted();
108
+ const rendered = await liquid.parseAndRender(source, { values });
109
+ options.signal.throwIfAborted();
110
+ return rendered;
111
+ } catch {
112
+ throw new Error("Input template resolution failed");
113
+ }
114
+ }
115
+ //#endregion
116
+ exports.hasInputTemplate = hasInputTemplate;
117
+ exports.parseInputTemplate = parseInputTemplate;
118
+ exports.renderInputTemplate = renderInputTemplate;
119
+
120
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["Liquid"],"sources":["../../../../src/services/inputTemplate/index.ts"],"sourcesContent":["import { Liquid } from 'liquidjs';\n\nimport type { InputTemplateOptions, InputTemplatePart } from './type';\n\nconst MAX_BYTES = 65_536;\nconst MAX_REFERENCES = 16;\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;\nconst FORBIDDEN = new Set(['__proto__', 'prototype', 'constructor']);\nconst liquid = new Liquid({ strictVariables: true, strictFilters: true, ownPropertyOnly: true });\n\nfunction invalid(): Error {\n return new Error('Unsupported input template');\n}\n\nfunction validName(name: string): boolean {\n return NAME.test(name) && !FORBIDDEN.has(name);\n}\n\n/** Includes unsupported template openings so they fail closed rather than being typed literally. */\nexport function hasInputTemplate(value: string): boolean {\n return /\\$\\{|\\{\\{|\\{%/u.test(value);\n}\n\n/** Interpolation only. No tags, filters, dynamic keys, expressions or recursive evaluation. */\nexport function parseInputTemplate(value: string): InputTemplatePart[] {\n if (typeof value !== 'string' || new TextEncoder().encode(value).length > MAX_BYTES) throw invalid();\n const parts: InputTemplatePart[] = [];\n const openings = /\\$\\{|\\{\\{|\\{%/gu;\n let cursor = 0;\n let references = 0;\n for (let match = openings.exec(value); match; match = openings.exec(value)) {\n const open = match.index;\n if (open > cursor) parts.push({ kind: 'text', text: value.slice(cursor, open) });\n if (match[0] === '{%') throw invalid();\n const closing = match[0] === '${' ? '}' : '}}';\n const close = value.indexOf(closing, open + 2);\n if (close === -1) throw invalid();\n const reference = /^(env|secret|secrets)\\.([A-Za-z_][A-Za-z0-9_]*)$/u.exec(value.slice(open + 2, close).trim());\n if (!reference || !validName(reference[2]!) || ++references > MAX_REFERENCES) throw invalid();\n parts.push({ kind: reference[1] === 'env' ? 'env' : 'secret', name: reference[2]! });\n cursor = close + closing.length;\n openings.lastIndex = cursor;\n }\n if (references === 0) throw invalid();\n if (cursor < value.length) parts.push({ kind: 'text', text: value.slice(cursor) });\n return parts;\n}\n\nasync function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {\n signal.throwIfAborted();\n let cancel = () => {};\n const aborted = new Promise<never>((_resolve, reject) => {\n cancel = () => reject(invalid());\n signal.addEventListener('abort', cancel, { once: true });\n });\n try {\n return await Promise.race([work, aborted]);\n } finally {\n signal.removeEventListener('abort', cancel);\n }\n}\n\n/** Resolves host-declared values, then renders them as Liquid data, never as Liquid source. */\nexport async function renderInputTemplate(\n parts: readonly InputTemplatePart[],\n options: InputTemplateOptions,\n): Promise<string> {\n try {\n let references = 0;\n let bytes = 0;\n // Validate the whole input before the first lookup, including caller-constructed parts.\n for (const part of parts) {\n if (part.kind === 'text' && typeof part.text === 'string') bytes += new TextEncoder().encode(part.text).length;\n else if ((part.kind === 'env' || part.kind === 'secret') && validName(part.name)) references += 1;\n else throw invalid();\n }\n if (references > MAX_REFERENCES || bytes > MAX_BYTES || parts.length > MAX_REFERENCES * 2 + 1) throw invalid();\n const values: Record<string, string> = Object.create(null);\n const cache = new Map<string, string>();\n let source = '';\n for (const [index, part] of parts.entries()) {\n options.signal.throwIfAborted();\n let value: string;\n if (part.kind === 'text') value = part.text;\n else {\n const key = `${part.kind}.${part.name}`;\n if (cache.has(key)) value = cache.get(key)!;\n else {\n const resolved =\n part.kind === 'env'\n ? options.readEnv(part.name)\n : await abortable(\n Promise.resolve().then(() => {\n options.signal.throwIfAborted();\n return options.resolveSecret(part.name);\n }),\n options.signal,\n );\n if (typeof resolved !== 'string' || (part.kind === 'secret' && !resolved)) throw invalid();\n value = resolved;\n if (part.kind === 'secret') options.onResolved(value);\n cache.set(key, value);\n }\n }\n bytes += part.kind === 'text' ? 0 : new TextEncoder().encode(value).length;\n if (bytes > MAX_BYTES) throw invalid();\n values[`v${index}`] = value;\n source += `{{ values.v${index} }}`;\n }\n options.signal.throwIfAborted();\n const rendered = await liquid.parseAndRender(source, { values });\n options.signal.throwIfAborted();\n return rendered;\n } catch {\n throw new Error('Input template resolution failed');\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { InputTemplateOptions, InputTemplatePart } from "./type.mjs";
2
+ //#region src/services/inputTemplate/index.d.ts
3
+ /** Includes unsupported template openings so they fail closed rather than being typed literally. */
4
+ export declare function hasInputTemplate(value: string): boolean;
5
+ /** Interpolation only. No tags, filters, dynamic keys, expressions or recursive evaluation. */
6
+ export declare function parseInputTemplate(value: string): InputTemplatePart[];
7
+ /** Resolves host-declared values, then renders them as Liquid data, never as Liquid source. */
8
+ export declare function renderInputTemplate(parts: readonly InputTemplatePart[], options: InputTemplateOptions): Promise<string>;
9
+ //#endregion
10
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/inputTemplate/index.ts"],"mappings":";;;wBAmBgB,iBAAiB;;wBAKjB,mBAAmB,gBAAgB;;wBAuC7B,oBACpB,gBAAgB,qBAChB,SAAS,uBACR"}
@@ -0,0 +1,118 @@
1
+ import { Liquid } from "liquidjs";
2
+ //#region src/services/inputTemplate/index.ts
3
+ const MAX_BYTES = 65536;
4
+ const MAX_REFERENCES = 16;
5
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
6
+ const FORBIDDEN = /* @__PURE__ */ new Set([
7
+ "__proto__",
8
+ "prototype",
9
+ "constructor"
10
+ ]);
11
+ const liquid = new Liquid({
12
+ strictVariables: true,
13
+ strictFilters: true,
14
+ ownPropertyOnly: true
15
+ });
16
+ function invalid() {
17
+ return /* @__PURE__ */ new Error("Unsupported input template");
18
+ }
19
+ function validName(name) {
20
+ return NAME.test(name) && !FORBIDDEN.has(name);
21
+ }
22
+ /** Includes unsupported template openings so they fail closed rather than being typed literally. */
23
+ function hasInputTemplate(value) {
24
+ return /\$\{|\{\{|\{%/u.test(value);
25
+ }
26
+ /** Interpolation only. No tags, filters, dynamic keys, expressions or recursive evaluation. */
27
+ function parseInputTemplate(value) {
28
+ if (typeof value !== "string" || new TextEncoder().encode(value).length > MAX_BYTES) throw invalid();
29
+ const parts = [];
30
+ const openings = /\$\{|\{\{|\{%/gu;
31
+ let cursor = 0;
32
+ let references = 0;
33
+ for (let match = openings.exec(value); match; match = openings.exec(value)) {
34
+ const open = match.index;
35
+ if (open > cursor) parts.push({
36
+ kind: "text",
37
+ text: value.slice(cursor, open)
38
+ });
39
+ if (match[0] === "{%") throw invalid();
40
+ const closing = match[0] === "${" ? "}" : "}}";
41
+ const close = value.indexOf(closing, open + 2);
42
+ if (close === -1) throw invalid();
43
+ const reference = /^(env|secret|secrets)\.([A-Za-z_][A-Za-z0-9_]*)$/u.exec(value.slice(open + 2, close).trim());
44
+ if (!reference || !validName(reference[2]) || ++references > MAX_REFERENCES) throw invalid();
45
+ parts.push({
46
+ kind: reference[1] === "env" ? "env" : "secret",
47
+ name: reference[2]
48
+ });
49
+ cursor = close + closing.length;
50
+ openings.lastIndex = cursor;
51
+ }
52
+ if (references === 0) throw invalid();
53
+ if (cursor < value.length) parts.push({
54
+ kind: "text",
55
+ text: value.slice(cursor)
56
+ });
57
+ return parts;
58
+ }
59
+ async function abortable(work, signal) {
60
+ signal.throwIfAborted();
61
+ let cancel = () => {};
62
+ const aborted = new Promise((_resolve, reject) => {
63
+ cancel = () => reject(invalid());
64
+ signal.addEventListener("abort", cancel, { once: true });
65
+ });
66
+ try {
67
+ return await Promise.race([work, aborted]);
68
+ } finally {
69
+ signal.removeEventListener("abort", cancel);
70
+ }
71
+ }
72
+ /** Resolves host-declared values, then renders them as Liquid data, never as Liquid source. */
73
+ async function renderInputTemplate(parts, options) {
74
+ try {
75
+ let references = 0;
76
+ let bytes = 0;
77
+ for (const part of parts) if (part.kind === "text" && typeof part.text === "string") bytes += new TextEncoder().encode(part.text).length;
78
+ else if ((part.kind === "env" || part.kind === "secret") && validName(part.name)) references += 1;
79
+ else throw invalid();
80
+ if (references > MAX_REFERENCES || bytes > MAX_BYTES || parts.length > 33) throw invalid();
81
+ const values = Object.create(null);
82
+ const cache = /* @__PURE__ */ new Map();
83
+ let source = "";
84
+ for (const [index, part] of parts.entries()) {
85
+ options.signal.throwIfAborted();
86
+ let value;
87
+ if (part.kind === "text") value = part.text;
88
+ else {
89
+ const key = `${part.kind}.${part.name}`;
90
+ if (cache.has(key)) value = cache.get(key);
91
+ else {
92
+ const resolved = part.kind === "env" ? options.readEnv(part.name) : await abortable(Promise.resolve().then(() => {
93
+ options.signal.throwIfAborted();
94
+ return options.resolveSecret(part.name);
95
+ }), options.signal);
96
+ if (typeof resolved !== "string" || part.kind === "secret" && !resolved) throw invalid();
97
+ value = resolved;
98
+ if (part.kind === "secret") options.onResolved(value);
99
+ cache.set(key, value);
100
+ }
101
+ }
102
+ bytes += part.kind === "text" ? 0 : new TextEncoder().encode(value).length;
103
+ if (bytes > MAX_BYTES) throw invalid();
104
+ values[`v${index}`] = value;
105
+ source += `{{ values.v${index} }}`;
106
+ }
107
+ options.signal.throwIfAborted();
108
+ const rendered = await liquid.parseAndRender(source, { values });
109
+ options.signal.throwIfAborted();
110
+ return rendered;
111
+ } catch {
112
+ throw new Error("Input template resolution failed");
113
+ }
114
+ }
115
+ //#endregion
116
+ export { hasInputTemplate, parseInputTemplate, renderInputTemplate };
117
+
118
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/inputTemplate/index.ts"],"sourcesContent":["import { Liquid } from 'liquidjs';\n\nimport type { InputTemplateOptions, InputTemplatePart } from './type';\n\nconst MAX_BYTES = 65_536;\nconst MAX_REFERENCES = 16;\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;\nconst FORBIDDEN = new Set(['__proto__', 'prototype', 'constructor']);\nconst liquid = new Liquid({ strictVariables: true, strictFilters: true, ownPropertyOnly: true });\n\nfunction invalid(): Error {\n return new Error('Unsupported input template');\n}\n\nfunction validName(name: string): boolean {\n return NAME.test(name) && !FORBIDDEN.has(name);\n}\n\n/** Includes unsupported template openings so they fail closed rather than being typed literally. */\nexport function hasInputTemplate(value: string): boolean {\n return /\\$\\{|\\{\\{|\\{%/u.test(value);\n}\n\n/** Interpolation only. No tags, filters, dynamic keys, expressions or recursive evaluation. */\nexport function parseInputTemplate(value: string): InputTemplatePart[] {\n if (typeof value !== 'string' || new TextEncoder().encode(value).length > MAX_BYTES) throw invalid();\n const parts: InputTemplatePart[] = [];\n const openings = /\\$\\{|\\{\\{|\\{%/gu;\n let cursor = 0;\n let references = 0;\n for (let match = openings.exec(value); match; match = openings.exec(value)) {\n const open = match.index;\n if (open > cursor) parts.push({ kind: 'text', text: value.slice(cursor, open) });\n if (match[0] === '{%') throw invalid();\n const closing = match[0] === '${' ? '}' : '}}';\n const close = value.indexOf(closing, open + 2);\n if (close === -1) throw invalid();\n const reference = /^(env|secret|secrets)\\.([A-Za-z_][A-Za-z0-9_]*)$/u.exec(value.slice(open + 2, close).trim());\n if (!reference || !validName(reference[2]!) || ++references > MAX_REFERENCES) throw invalid();\n parts.push({ kind: reference[1] === 'env' ? 'env' : 'secret', name: reference[2]! });\n cursor = close + closing.length;\n openings.lastIndex = cursor;\n }\n if (references === 0) throw invalid();\n if (cursor < value.length) parts.push({ kind: 'text', text: value.slice(cursor) });\n return parts;\n}\n\nasync function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {\n signal.throwIfAborted();\n let cancel = () => {};\n const aborted = new Promise<never>((_resolve, reject) => {\n cancel = () => reject(invalid());\n signal.addEventListener('abort', cancel, { once: true });\n });\n try {\n return await Promise.race([work, aborted]);\n } finally {\n signal.removeEventListener('abort', cancel);\n }\n}\n\n/** Resolves host-declared values, then renders them as Liquid data, never as Liquid source. */\nexport async function renderInputTemplate(\n parts: readonly InputTemplatePart[],\n options: InputTemplateOptions,\n): Promise<string> {\n try {\n let references = 0;\n let bytes = 0;\n // Validate the whole input before the first lookup, including caller-constructed parts.\n for (const part of parts) {\n if (part.kind === 'text' && typeof part.text === 'string') bytes += new TextEncoder().encode(part.text).length;\n else if ((part.kind === 'env' || part.kind === 'secret') && validName(part.name)) references += 1;\n else throw invalid();\n }\n if (references > MAX_REFERENCES || bytes > MAX_BYTES || parts.length > MAX_REFERENCES * 2 + 1) throw invalid();\n const values: Record<string, string> = Object.create(null);\n const cache = new Map<string, string>();\n let source = '';\n for (const [index, part] of parts.entries()) {\n options.signal.throwIfAborted();\n let value: string;\n if (part.kind === 'text') value = part.text;\n else {\n const key = `${part.kind}.${part.name}`;\n if (cache.has(key)) value = cache.get(key)!;\n else {\n const resolved =\n part.kind === 'env'\n ? options.readEnv(part.name)\n : await abortable(\n Promise.resolve().then(() => {\n options.signal.throwIfAborted();\n return options.resolveSecret(part.name);\n }),\n options.signal,\n );\n if (typeof resolved !== 'string' || (part.kind === 'secret' && !resolved)) throw invalid();\n value = resolved;\n if (part.kind === 'secret') options.onResolved(value);\n cache.set(key, value);\n }\n }\n bytes += part.kind === 'text' ? 0 : new TextEncoder().encode(value).length;\n if (bytes > MAX_BYTES) throw invalid();\n values[`v${index}`] = value;\n source += `{{ values.v${index} }}`;\n }\n options.signal.throwIfAborted();\n const rendered = await liquid.parseAndRender(source, { values });\n options.signal.throwIfAborted();\n return rendered;\n } catch {\n throw new Error('Input template resolution failed');\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ //#region src/services/inputTemplate/type.d.ts
2
+ export type InputTemplatePart = {
3
+ readonly kind: 'text';
4
+ readonly text: string;
5
+ } | {
6
+ readonly kind: 'env' | 'secret';
7
+ readonly name: string;
8
+ };
9
+ /** Host-owned lookups only. Never expose this renderer as a secret-reading model tool. */
10
+ export interface InputTemplateOptions {
11
+ readEnv(name: string): string | undefined;
12
+ resolveSecret(name: string): Promise<string>;
13
+ onResolved(value: string): void;
14
+ readonly signal: AbortSignal;
15
+ }
16
+ //#endregion
17
+ //# sourceMappingURL=type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type.d.mts","names":[],"sources":["../../../../src/services/inputTemplate/type.ts"],"mappings":";YAAY;WACG;WAAuB;;WACvB;WAAiC;;;iBAG/B;EACf,QAAQ;EACR,cAAc,eAAe;EAC7B,WAAW;WACF,QAAQ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/po-core",
3
- "version": "0.0.0-alpha.106",
3
+ "version": "0.0.0-alpha.108",
4
4
  "description": "Po runtime systems and reusable core services.",
5
5
  "keywords": [
6
6
  "ai",
@@ -568,6 +568,11 @@
568
568
  "import": "./dist/telemetry.mjs",
569
569
  "require": "./dist/telemetry.cjs"
570
570
  },
571
+ "./inputTemplate": {
572
+ "types": "./dist/inputTemplate.d.mts",
573
+ "import": "./dist/inputTemplate.mjs",
574
+ "require": "./dist/inputTemplate.cjs"
575
+ },
571
576
  "./testMode": {
572
577
  "types": "./dist/testMode.d.mts",
573
578
  "import": "./dist/testMode.mjs",
@@ -579,8 +584,8 @@
579
584
  "registry": "https://registry.npmjs.org/"
580
585
  },
581
586
  "dependencies": {
582
- "@agimon-ai/po-logsink": "0.0.1-alpha.88",
583
- "@agimon-ai/po-web-security": "0.0.0-alpha.106",
587
+ "@agimon-ai/po-logsink": "0.0.1-alpha.90",
588
+ "@agimon-ai/po-web-security": "0.0.0-alpha.108",
584
589
  "@deepseek-ai/cordis": "4.0.5",
585
590
  "@earendil-works/chord": "1.5.0",
586
591
  "@earendil-works/pi-agent-core": "1.5.0",
@@ -597,6 +602,7 @@
597
602
  "gpt-tokenizer": "4.1.0",
598
603
  "hono": "4.13.13",
599
604
  "ignore": "7.0.12",
605
+ "liquidjs": "10.30.0",
600
606
  "minimatch": "10.2.6",
601
607
  "protobufjs": "8.8.1",
602
608
  "typebox": "1.4.2",
@@ -605,7 +611,7 @@
605
611
  "yaml": "2.9.1"
606
612
  },
607
613
  "devDependencies": {
608
- "@agimon-ai/vibe-lint-plugin-po-core": "0.0.0-alpha.106",
614
+ "@agimon-ai/vibe-lint-plugin-po-core": "0.0.0-alpha.108",
609
615
  "@apidevtools/swagger-parser": "^13.1.0",
610
616
  "@asyncapi/parser": "^3.6.4",
611
617
  "@earendil-works/pi-ai": "1.5.0",