@zihanw/pi-forge 0.3.2 → 0.4.0
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/CHANGELOG.md +196 -0
- package/PUBLIC_API.md +5 -0
- package/README.md +93 -478
- package/README.zh-CN.md +88 -461
- package/SUBAGENT_ADAPTER_CONTRACT.md +5 -0
- package/dist/agent-profile.d.ts +71 -0
- package/dist/agent-profile.d.ts.map +1 -0
- package/dist/agent-profile.js +303 -0
- package/dist/agent-profile.js.map +1 -0
- package/dist/forge-config.d.ts +88 -0
- package/dist/forge-config.d.ts.map +1 -0
- package/dist/forge-config.js +290 -0
- package/dist/forge-config.js.map +1 -0
- package/dist/forge-extensions.d.ts.map +1 -1
- package/dist/forge-extensions.js +19 -3
- package/dist/forge-extensions.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -358
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +11 -3
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +90 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +116 -5
- package/dist/loader.js.map +1 -1
- package/dist/payload-capture.d.ts.map +1 -1
- package/dist/payload-capture.js +27 -0
- package/dist/payload-capture.js.map +1 -1
- package/dist/payload-command.d.ts +3 -3
- package/dist/payload-command.d.ts.map +1 -1
- package/dist/payload-command.js.map +1 -1
- package/dist/preview.d.ts +2 -2
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +2 -3
- package/dist/preview.js.map +1 -1
- package/dist/profile-command.d.ts +12 -0
- package/dist/profile-command.d.ts.map +1 -0
- package/dist/profile-command.js +291 -0
- package/dist/profile-command.js.map +1 -0
- package/dist/profile-service.d.ts +103 -0
- package/dist/profile-service.d.ts.map +1 -0
- package/dist/profile-service.js +220 -0
- package/dist/profile-service.js.map +1 -0
- package/dist/runtime/profile-runtime.d.ts +13 -0
- package/dist/runtime/profile-runtime.d.ts.map +1 -0
- package/dist/runtime/profile-runtime.js +47 -0
- package/dist/runtime/profile-runtime.js.map +1 -0
- package/dist/runtime/prompt-stack-runtime.d.ts +22 -0
- package/dist/runtime/prompt-stack-runtime.d.ts.map +1 -0
- package/dist/runtime/prompt-stack-runtime.js +104 -0
- package/dist/runtime/prompt-stack-runtime.js.map +1 -0
- package/dist/runtime/subagent-runtime.d.ts +45 -0
- package/dist/runtime/subagent-runtime.d.ts.map +1 -0
- package/dist/runtime/subagent-runtime.js +335 -0
- package/dist/runtime/subagent-runtime.js.map +1 -0
- package/dist/runtime/tool-policy-runtime.d.ts +15 -0
- package/dist/runtime/tool-policy-runtime.d.ts.map +1 -0
- package/dist/runtime/tool-policy-runtime.js +170 -0
- package/dist/runtime/tool-policy-runtime.js.map +1 -0
- package/dist/runtime/web-editor-runtime.d.ts +9 -0
- package/dist/runtime/web-editor-runtime.d.ts.map +1 -0
- package/dist/runtime/web-editor-runtime.js +131 -0
- package/dist/runtime/web-editor-runtime.js.map +1 -0
- package/dist/runtime-state.d.ts +4 -0
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +2 -0
- package/dist/runtime-state.js.map +1 -1
- package/dist/storage.d.ts +4 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +24 -1
- package/dist/storage.js.map +1 -1
- package/dist/subagent/canonical.d.ts +22 -0
- package/dist/subagent/canonical.d.ts.map +1 -0
- package/dist/subagent/canonical.js +24 -0
- package/dist/subagent/canonical.js.map +1 -0
- package/dist/subagent/context.d.ts +8 -0
- package/dist/subagent/context.d.ts.map +1 -0
- package/dist/subagent/context.js +125 -0
- package/dist/subagent/context.js.map +1 -0
- package/dist/subagent/contract.d.ts +10 -0
- package/dist/subagent/contract.d.ts.map +1 -0
- package/dist/subagent/contract.js +10 -0
- package/dist/subagent/contract.js.map +1 -0
- package/dist/subagent/index.d.ts +14 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +14 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/plan.d.ts +18 -0
- package/dist/subagent/plan.d.ts.map +1 -0
- package/dist/subagent/plan.js +157 -0
- package/dist/subagent/plan.js.map +1 -0
- package/dist/subagent/preflight.d.ts +4 -0
- package/dist/subagent/preflight.d.ts.map +1 -0
- package/dist/subagent/preflight.js +108 -0
- package/dist/subagent/preflight.js.map +1 -0
- package/dist/subagent/request.d.ts +4 -0
- package/dist/subagent/request.d.ts.map +1 -0
- package/dist/subagent/request.js +122 -0
- package/dist/subagent/request.js.map +1 -0
- package/dist/subagent/response.d.ts +8 -0
- package/dist/subagent/response.d.ts.map +1 -0
- package/dist/subagent/response.js +155 -0
- package/dist/subagent/response.js.map +1 -0
- package/dist/subagent/tools.d.ts +4 -0
- package/dist/subagent/tools.d.ts.map +1 -0
- package/dist/subagent/tools.js +42 -0
- package/dist/subagent/tools.js.map +1 -0
- package/dist/subagent/types.d.ts +268 -0
- package/dist/subagent/types.d.ts.map +1 -0
- package/dist/subagent/types.js +3 -0
- package/dist/subagent/types.js.map +1 -0
- package/dist/subagent/validation.d.ts +35 -0
- package/dist/subagent/validation.d.ts.map +1 -0
- package/dist/subagent/validation.js +314 -0
- package/dist/subagent/validation.js.map +1 -0
- package/dist/subagent-command.d.ts +4 -0
- package/dist/subagent-command.d.ts.map +1 -0
- package/dist/subagent-command.js +246 -0
- package/dist/subagent-command.js.map +1 -0
- package/dist/subagent-host.d.ts +44 -0
- package/dist/subagent-host.d.ts.map +1 -0
- package/dist/subagent-host.js +292 -0
- package/dist/subagent-host.js.map +1 -0
- package/dist/subagent-profile-tool.d.ts +49 -0
- package/dist/subagent-profile-tool.d.ts.map +1 -0
- package/dist/subagent-profile-tool.js +124 -0
- package/dist/subagent-profile-tool.js.map +1 -0
- package/dist/subagent-tool.d.ts +53 -0
- package/dist/subagent-tool.d.ts.map +1 -0
- package/dist/subagent-tool.js +456 -0
- package/dist/subagent-tool.js.map +1 -0
- package/dist/web-editor/client-script.d.ts +2 -0
- package/dist/web-editor/client-script.d.ts.map +1 -0
- package/dist/web-editor/client-script.generated.d.ts +2 -0
- package/dist/web-editor/client-script.generated.d.ts.map +1 -0
- package/dist/web-editor/client-script.generated.js +3 -0
- package/dist/web-editor/client-script.generated.js.map +1 -0
- package/dist/web-editor/client-script.js +2 -0
- package/dist/web-editor/client-script.js.map +1 -0
- package/dist/web-editor/client-styles.d.ts +2 -0
- package/dist/web-editor/client-styles.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.d.ts +2 -0
- package/dist/web-editor/client-styles.generated.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.js +3 -0
- package/dist/web-editor/client-styles.generated.js.map +1 -0
- package/dist/web-editor/client-styles.js +2 -0
- package/dist/web-editor/client-styles.js.map +1 -0
- package/dist/web-editor/page.d.ts +2 -0
- package/dist/web-editor/page.d.ts.map +1 -1
- package/dist/web-editor/page.js +14 -3319
- package/dist/web-editor/page.js.map +1 -1
- package/dist/web-editor/server.d.ts.map +1 -1
- package/dist/web-editor/server.js +148 -0
- package/dist/web-editor/server.js.map +1 -1
- package/dist/web-editor/styles.d.ts +2 -0
- package/dist/web-editor/styles.d.ts.map +1 -0
- package/dist/web-editor/styles.js +1053 -0
- package/dist/web-editor/styles.js.map +1 -0
- package/dist/web-editor/types.d.ts +79 -0
- package/dist/web-editor/types.d.ts.map +1 -1
- package/dist/web-host.d.ts +16 -5
- package/dist/web-host.d.ts.map +1 -1
- package/dist/web-host.js +307 -0
- package/dist/web-host.js.map +1 -1
- package/docs/README.md +41 -0
- package/docs/concepts/agent-profiles.md +60 -0
- package/docs/concepts/prompt-stacks.md +90 -0
- package/docs/design/README.md +17 -0
- package/docs/design/roadmap-0.4-archive.md +216 -0
- package/docs/design/subagents/design-review.md +220 -0
- package/docs/design/subagents/interface-design.md +274 -0
- package/docs/design/subagents/sdk-spike-findings.md +117 -0
- package/docs/development/complexity-review.md +86 -0
- package/docs/development/release.md +31 -0
- package/docs/development/roadmap.md +42 -0
- package/docs/development/setup.md +75 -0
- package/docs/getting-started.md +93 -0
- package/docs/guides/custom-macros-and-slots.md +68 -0
- package/docs/guides/debugging.md +39 -0
- package/docs/guides/delegation.md +99 -0
- package/docs/guides/sillytavern-import.md +47 -0
- package/docs/guides/use-cases.md +65 -0
- package/docs/guides/web-editor.md +75 -0
- package/docs/reference/commands.md +60 -0
- package/docs/reference/configuration.md +64 -0
- package/docs/reference/features.md +279 -0
- package/docs/reference/macros-and-slots.md +82 -0
- package/docs/reference/public-api.md +28 -0
- package/docs/reference/stack-schema.md +167 -0
- package/docs/reference/subagent-adapter.md +204 -0
- package/docs/zh-CN/README.md +37 -0
- package/docs/zh-CN/concepts/agent-profiles.md +44 -0
- package/docs/zh-CN/concepts/prompt-stacks.md +40 -0
- package/docs/zh-CN/getting-started.md +79 -0
- package/docs/zh-CN/guides/delegation.md +66 -0
- package/docs/zh-CN/guides/web-editor.md +45 -0
- package/docs/zh-CN/reference/commands.md +58 -0
- package/package.json +55 -13
- package/src/compiler.ts +0 -578
- package/src/extension-registry.ts +0 -33
- package/src/forge-extensions.ts +0 -223
- package/src/index.ts +0 -445
- package/src/lifecycle.ts +0 -171
- package/src/loader.ts +0 -394
- package/src/macro-engine.ts +0 -358
- package/src/payload-capture.ts +0 -85
- package/src/payload-command.ts +0 -138
- package/src/policy.ts +0 -42
- package/src/preset-command.ts +0 -280
- package/src/preview.ts +0 -226
- package/src/regex.ts +0 -500
- package/src/render-helpers.ts +0 -169
- package/src/runtime-state.ts +0 -40
- package/src/sillytavern-importer/items.ts +0 -98
- package/src/sillytavern-importer/macros.ts +0 -159
- package/src/sillytavern-importer/prompt-order.ts +0 -54
- package/src/sillytavern-importer/regex.ts +0 -270
- package/src/sillytavern-importer/report.ts +0 -202
- package/src/sillytavern-importer/types.ts +0 -120
- package/src/sillytavern-importer.ts +0 -152
- package/src/slot-renderers.ts +0 -414
- package/src/stack-migration.ts +0 -159
- package/src/storage.ts +0 -45
- package/src/types.ts +0 -209
- package/src/web-editor/index.ts +0 -2
- package/src/web-editor/page.ts +0 -3330
- package/src/web-editor/server.ts +0 -294
- package/src/web-editor/types.ts +0 -98
- package/src/web-host.ts +0 -232
package/src/runtime-state.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { BuildSystemPromptOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { WebEditorPayloadCapture } from "./web-editor/index.ts";
|
|
3
|
-
import type { LoadedPromptStack, PromptStackDiagnostic, PromptVariableStore, PromptVariableValue } from "./types.ts";
|
|
4
|
-
|
|
5
|
-
export const STATE_ENTRY_TYPE = "pi-forge-prompt-stack-state";
|
|
6
|
-
export const VARIABLE_ENTRY_TYPE = "pi-forge-variable-state";
|
|
7
|
-
|
|
8
|
-
export type PayloadDisplayTarget = "editor" | "web";
|
|
9
|
-
|
|
10
|
-
export interface PiForgeRuntimeState {
|
|
11
|
-
stacks: LoadedPromptStack[];
|
|
12
|
-
active?: LoadedPromptStack;
|
|
13
|
-
currentSystemPromptOptions?: BuildSystemPromptOptions;
|
|
14
|
-
currentLatestUserMessage?: string;
|
|
15
|
-
currentVariableStore?: PromptVariableStore;
|
|
16
|
-
contextRewritePending: boolean;
|
|
17
|
-
sessionVariables: Record<string, PromptVariableValue>;
|
|
18
|
-
lastPersistedActiveId?: string;
|
|
19
|
-
latestCompileDiagnostics: PromptStackDiagnostic[];
|
|
20
|
-
forgeExtensionDiagnostics: PromptStackDiagnostic[];
|
|
21
|
-
forgeExtensionPaths: string[];
|
|
22
|
-
interceptNextProviderPayload: boolean;
|
|
23
|
-
interceptPayloadSavePath?: string;
|
|
24
|
-
interceptPayloadDisplayTarget: PayloadDisplayTarget;
|
|
25
|
-
payloadCaptureArmedAt?: string;
|
|
26
|
-
latestProviderPayloadCapture?: WebEditorPayloadCapture;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function createRuntimeState(): PiForgeRuntimeState {
|
|
30
|
-
return {
|
|
31
|
-
stacks: [],
|
|
32
|
-
contextRewritePending: false,
|
|
33
|
-
sessionVariables: {},
|
|
34
|
-
latestCompileDiagnostics: [],
|
|
35
|
-
forgeExtensionDiagnostics: [],
|
|
36
|
-
forgeExtensionPaths: [],
|
|
37
|
-
interceptNextProviderPayload: false,
|
|
38
|
-
interceptPayloadDisplayTarget: "editor",
|
|
39
|
-
};
|
|
40
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { PromptStackItem } from "../types.ts";
|
|
2
|
-
import { COMMENT_MACRO_RE, TRIM_MACRO_RE } from "./macros.ts";
|
|
3
|
-
import type { PromptConversionResult, StConversionItem } from "./types.ts";
|
|
4
|
-
|
|
5
|
-
const MARKER_CHAT_HISTORY = "chatHistory";
|
|
6
|
-
const MARKER_SKIP = new Set([
|
|
7
|
-
"worldInfoBefore",
|
|
8
|
-
"worldInfoAfter",
|
|
9
|
-
"charDescription",
|
|
10
|
-
"charPersonality",
|
|
11
|
-
"personaDescription",
|
|
12
|
-
"scenario",
|
|
13
|
-
"dialogueExamples",
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
export function convertPromptItems(conversionItems: StConversionItem[]): PromptConversionResult {
|
|
17
|
-
let nextId = 1;
|
|
18
|
-
const items: PromptStackItem[] = [];
|
|
19
|
-
let usesLastUserMessage = false;
|
|
20
|
-
const disabledCount = { marker: 0, empty: 0, orderDisable: 0 };
|
|
21
|
-
|
|
22
|
-
for (const { def, orderEnabled, orderIndex } of conversionItems) {
|
|
23
|
-
const id = String(nextId++);
|
|
24
|
-
|
|
25
|
-
if (def.marker) {
|
|
26
|
-
if (def.identifier === MARKER_CHAT_HISTORY) {
|
|
27
|
-
items.push({
|
|
28
|
-
kind: "slot",
|
|
29
|
-
id,
|
|
30
|
-
name: def.name || "Chat History",
|
|
31
|
-
enabled: orderEnabled,
|
|
32
|
-
slot: "chat-history",
|
|
33
|
-
source: { previousId: def.identifier, name: def.name },
|
|
34
|
-
});
|
|
35
|
-
} else if (MARKER_SKIP.has(def.identifier)) {
|
|
36
|
-
disabledCount.marker++;
|
|
37
|
-
}
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let content = def.content ?? "";
|
|
42
|
-
if (!content.trim()) {
|
|
43
|
-
disabledCount.empty++;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
content = content.replace(COMMENT_MACRO_RE, "");
|
|
48
|
-
content = content.replace(TRIM_MACRO_RE, "");
|
|
49
|
-
|
|
50
|
-
if (!content.trim()) {
|
|
51
|
-
disabledCount.empty++;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (/\{\{\s*lastUserMessage\b/i.test(content)) {
|
|
56
|
-
usesLastUserMessage = true;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const role = normalizeRole(def.role);
|
|
60
|
-
if (!role) {
|
|
61
|
-
disabledCount.empty++;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!orderEnabled) {
|
|
66
|
-
disabledCount.orderDisable++;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
items.push({
|
|
70
|
-
kind: "block",
|
|
71
|
-
id,
|
|
72
|
-
name: def.name,
|
|
73
|
-
enabled: orderEnabled,
|
|
74
|
-
role,
|
|
75
|
-
content: cleanContent(content),
|
|
76
|
-
source: {
|
|
77
|
-
previousId: def.identifier,
|
|
78
|
-
previousName: def.name,
|
|
79
|
-
orderIndex,
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { items, usesLastUserMessage, disabledCount };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function cleanContent(content: string): string {
|
|
88
|
-
return content.replace(/\n{3,}/g, "\n\n").trim();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function normalizeRole(role: string | undefined): "system" | "user" | "assistant" | undefined {
|
|
92
|
-
if (!role) return undefined;
|
|
93
|
-
const lower = role.trim().toLowerCase();
|
|
94
|
-
if (lower === "system") return "system";
|
|
95
|
-
if (lower === "user") return "user";
|
|
96
|
-
if (lower === "assistant") return "assistant";
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import type { StConversionItem } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
export const NATIVE_MACROS = new Set([
|
|
4
|
-
"char",
|
|
5
|
-
"user",
|
|
6
|
-
"lastusermessage",
|
|
7
|
-
"cwd",
|
|
8
|
-
"date",
|
|
9
|
-
"time",
|
|
10
|
-
"selectedtools",
|
|
11
|
-
"tools",
|
|
12
|
-
"activemodel",
|
|
13
|
-
"setvar",
|
|
14
|
-
"setturnvar",
|
|
15
|
-
"setsessionvar",
|
|
16
|
-
"getvar",
|
|
17
|
-
"var",
|
|
18
|
-
"getturnvar",
|
|
19
|
-
"getsessionvar",
|
|
20
|
-
"clearvar",
|
|
21
|
-
"clearturnvar",
|
|
22
|
-
"clearsessionvar",
|
|
23
|
-
"trim",
|
|
24
|
-
"upper",
|
|
25
|
-
"lower",
|
|
26
|
-
"json",
|
|
27
|
-
"xml",
|
|
28
|
-
"ifvar",
|
|
29
|
-
"ifeq",
|
|
30
|
-
"iftools",
|
|
31
|
-
"ifslot",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
const MACRO_DISPLAY_NAMES: Record<string, string> = {
|
|
35
|
-
lastusermessage: "lastUserMessage",
|
|
36
|
-
selectedtools: "selectedTools",
|
|
37
|
-
activemodel: "activeModel",
|
|
38
|
-
groupnotmuted: "groupNotMuted",
|
|
39
|
-
notchar: "notChar",
|
|
40
|
-
charifnotgroup: "charIfNotGroup",
|
|
41
|
-
mesexamples: "mesExamples",
|
|
42
|
-
mesexamplesraw: "mesExamplesRaw",
|
|
43
|
-
charprompt: "charPrompt",
|
|
44
|
-
charinstruction: "charInstruction",
|
|
45
|
-
charversion: "charVersion",
|
|
46
|
-
charfirstmessage: "charFirstMessage",
|
|
47
|
-
wibefore: "wiBefore",
|
|
48
|
-
wiafter: "wiAfter",
|
|
49
|
-
lorebefore: "loreBefore",
|
|
50
|
-
loreafter: "loreAfter",
|
|
51
|
-
anchorbefore: "anchorBefore",
|
|
52
|
-
anchorafter: "anchorAfter",
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const MACRO_NEEDS_MIGRATION: Record<string, string> = {
|
|
56
|
-
addvar: "use pi-forge {{getvar::name}} + math, or setvar with computed value",
|
|
57
|
-
getglobalvar: "use pi-forge session variables via {{getsessionvar::name}}",
|
|
58
|
-
setglobalvar: "use pi-forge {{setsessionvar::name::value}}",
|
|
59
|
-
random: "precompute or rewrite manually with simple pi-forge conditionals when applicable",
|
|
60
|
-
pick: "precompute or rewrite manually with simple pi-forge conditionals when applicable",
|
|
61
|
-
roll: "precompute or rewrite manually with simple pi-forge conditionals when applicable",
|
|
62
|
-
if: "rewrite as {{ifvar::...}}, {{ifeq::...}}, {{iftools::...}}, or {{ifslot::...}} when applicable",
|
|
63
|
-
original: "SillyTavern-specific — no direct equivalent; set as session variable if needed",
|
|
64
|
-
outlet: "SillyTavern extension outlet — no pi-forge equivalent",
|
|
65
|
-
group: "SillyTavern group chat — no pi-forge equivalent",
|
|
66
|
-
groupnotmuted: "SillyTavern group chat — no pi-forge equivalent",
|
|
67
|
-
notchar: "SillyTavern group chat — no pi-forge equivalent",
|
|
68
|
-
charifnotgroup: "SillyTavern group chat — no pi-forge equivalent",
|
|
69
|
-
description: "ST character card field — set as session variable or static variable if needed",
|
|
70
|
-
personality: "ST character card field — set as session variable or static variable if needed",
|
|
71
|
-
scenario: "ST character card field — set as session variable or static variable if needed",
|
|
72
|
-
persona: "ST persona field — set as session variable or static variable if needed",
|
|
73
|
-
mesexamples: "ST character dialogue examples — add inline or as context file if needed",
|
|
74
|
-
mesexamplesraw: "ST character dialogue examples — add inline or as context file if needed",
|
|
75
|
-
charprompt: "ST character prompt override — merge into system prompt blocks if needed",
|
|
76
|
-
charinstruction: "ST character instruction override — merge into post-history blocks if needed",
|
|
77
|
-
charversion: "ST character version — set as static variable if needed",
|
|
78
|
-
charfirstmessage: "ST character first message — add as static variable if needed",
|
|
79
|
-
system: "ST context template system prompt — handled by pi-forge system prompt replacement",
|
|
80
|
-
wibefore: "ST world info — no pi-forge equivalent; merge relevant lore into static blocks",
|
|
81
|
-
wiafter: "ST world info — no pi-forge equivalent; merge relevant lore into static blocks",
|
|
82
|
-
lorebefore: "ST world info — no pi-forge equivalent; merge relevant lore into static blocks",
|
|
83
|
-
loreafter: "ST world info — no pi-forge equivalent; merge relevant lore into static blocks",
|
|
84
|
-
anchorbefore: "ST extension injection point — no pi-forge equivalent",
|
|
85
|
-
anchorafter: "ST extension injection point — no pi-forge equivalent",
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const NATIVE_MACRO_NOTES: Record<string, string> = {
|
|
89
|
-
setvar: "handled as a turn variable by default, with {{setvar::session::name::value}} for session scope",
|
|
90
|
-
setturnvar: "handled as a turn variable",
|
|
91
|
-
setsessionvar: "handled as a session variable",
|
|
92
|
-
getvar: "handled with turn -> session -> static lookup",
|
|
93
|
-
var: "handled as an alias of getvar",
|
|
94
|
-
getturnvar: "handled as a turn-only lookup",
|
|
95
|
-
getsessionvar: "handled as a session-only lookup",
|
|
96
|
-
clearvar: "handled as a turn variable clear by default, with {{clearvar::session::name}} for session scope",
|
|
97
|
-
clearturnvar: "handled as a turn variable clear",
|
|
98
|
-
clearsessionvar: "handled as a session variable clear",
|
|
99
|
-
selectedtools: "handled as comma-separated active tool names after stack tool policy",
|
|
100
|
-
tools: "handled as an alias of selectedTools",
|
|
101
|
-
activemodel: "handled as provider/model when available",
|
|
102
|
-
trim: "handled as an expanded-text trim filter",
|
|
103
|
-
upper: "handled as an expanded-text uppercase filter",
|
|
104
|
-
lower: "handled as an expanded-text lowercase filter",
|
|
105
|
-
json: "handled as JSON string escaping for expanded text",
|
|
106
|
-
xml: "handled as XML escaping for expanded text",
|
|
107
|
-
ifvar: "handled as a lazy variable-existence conditional",
|
|
108
|
-
ifeq: "handled as a lazy variable equality conditional",
|
|
109
|
-
iftools: "handled as a lazy active-tool conditional",
|
|
110
|
-
ifslot: "handled as a lazy enabled-slot conditional",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
export const COMMENT_MACRO_RE = /\{\{\s*\/\/[\s\S]*?\}\}/g;
|
|
114
|
-
export const TRIM_MACRO_RE = /\{\{\s*trim\s*\}\}/gi;
|
|
115
|
-
|
|
116
|
-
export interface MacroDetection {
|
|
117
|
-
detected: Set<string>;
|
|
118
|
-
commentsStripped: number;
|
|
119
|
-
trimStripped: number;
|
|
120
|
-
migrationNeeded: Record<string, number>;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function detectMacros(conversionItems: StConversionItem[]): MacroDetection {
|
|
124
|
-
const detected = new Set<string>();
|
|
125
|
-
let commentsStripped = 0;
|
|
126
|
-
let trimStripped = 0;
|
|
127
|
-
const migrationNeeded: Record<string, number> = {};
|
|
128
|
-
const macroFindRe = /\{\{(?!\/\/)([a-zA-Z_][a-zA-Z0-9_-]*)/gi;
|
|
129
|
-
|
|
130
|
-
for (const { def } of conversionItems) {
|
|
131
|
-
const content = def.content ?? "";
|
|
132
|
-
if (!content) continue;
|
|
133
|
-
|
|
134
|
-
const commentMatches = content.match(COMMENT_MACRO_RE);
|
|
135
|
-
if (commentMatches) commentsStripped += commentMatches.length;
|
|
136
|
-
|
|
137
|
-
const trimMatches = content.match(TRIM_MACRO_RE);
|
|
138
|
-
if (trimMatches) trimStripped += trimMatches.length;
|
|
139
|
-
|
|
140
|
-
const cleaned = content.replace(COMMENT_MACRO_RE, "").replace(TRIM_MACRO_RE, "");
|
|
141
|
-
|
|
142
|
-
let match: RegExpExecArray | null;
|
|
143
|
-
macroFindRe.lastIndex = 0;
|
|
144
|
-
while ((match = macroFindRe.exec(cleaned)) !== null) {
|
|
145
|
-
const name = match[1].toLowerCase();
|
|
146
|
-
detected.add(name);
|
|
147
|
-
|
|
148
|
-
if (MACRO_NEEDS_MIGRATION[name]) {
|
|
149
|
-
migrationNeeded[name] = (migrationNeeded[name] ?? 0) + 1;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return { detected, commentsStripped, trimStripped, migrationNeeded };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function displayMacroName(name: string): string {
|
|
158
|
-
return MACRO_DISPLAY_NAMES[name] ?? name;
|
|
159
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { StConversionItem, StPromptDef, StPromptOrderEntry } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
export function selectCharacterEntry(
|
|
4
|
-
order: StPromptOrderEntry[],
|
|
5
|
-
preferredId?: number,
|
|
6
|
-
): StPromptOrderEntry | undefined {
|
|
7
|
-
if (order.length === 0) return undefined;
|
|
8
|
-
|
|
9
|
-
if (preferredId !== undefined) {
|
|
10
|
-
return order.find((entry) => entry.character_id === preferredId);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (order.length === 1) return order[0];
|
|
14
|
-
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function buildPromptMap(prompts: StPromptDef[]): Map<string, StPromptDef> {
|
|
19
|
-
const promptMap = new Map<string, StPromptDef>();
|
|
20
|
-
for (const def of prompts) {
|
|
21
|
-
promptMap.set(def.identifier, def);
|
|
22
|
-
}
|
|
23
|
-
return promptMap;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildConversionItems(
|
|
27
|
-
selectedEntry: StPromptOrderEntry,
|
|
28
|
-
promptMap: Map<string, StPromptDef>,
|
|
29
|
-
): StConversionItem[] {
|
|
30
|
-
const conversionItems: StConversionItem[] = [];
|
|
31
|
-
let orderIndex = 0;
|
|
32
|
-
|
|
33
|
-
for (const orderItem of selectedEntry.order ?? []) {
|
|
34
|
-
const def = promptMap.get(orderItem.identifier);
|
|
35
|
-
if (def) {
|
|
36
|
-
conversionItems.push({ def, orderEnabled: orderItem.enabled, orderIndex: orderIndex++ });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return conversionItems;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function findMissingIdentifiers(
|
|
44
|
-
selectedEntry: StPromptOrderEntry,
|
|
45
|
-
promptMap: Map<string, StPromptDef>,
|
|
46
|
-
): string[] {
|
|
47
|
-
const missingIdentifiers: string[] = [];
|
|
48
|
-
for (const orderItem of selectedEntry.order ?? []) {
|
|
49
|
-
if (!promptMap.has(orderItem.identifier)) {
|
|
50
|
-
missingIdentifiers.push(orderItem.identifier);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return missingIdentifiers;
|
|
54
|
-
}
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import type { PromptRegexRule } from "../types.ts";
|
|
2
|
-
import type { RegexScriptMode, RegexScriptReport, RegexScriptReportEntry, StPreset, StRegexScript } from "./types.ts";
|
|
3
|
-
|
|
4
|
-
const ST_REGEX_PLACEMENT_LABELS: Record<number, string> = {
|
|
5
|
-
0: "MD Display",
|
|
6
|
-
1: "User Input",
|
|
7
|
-
2: "AI Output",
|
|
8
|
-
3: "Slash Commands",
|
|
9
|
-
5: "World Info",
|
|
10
|
-
6: "Reasoning",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function summarizeRegexScripts(preset: StPreset): RegexScriptReport | undefined {
|
|
14
|
-
const rawScripts = preset.extensions?.regex_scripts;
|
|
15
|
-
if (!Array.isArray(rawScripts)) return undefined;
|
|
16
|
-
|
|
17
|
-
const scripts = rawScripts
|
|
18
|
-
.map((raw, index) => classifyRegexScript(raw, index))
|
|
19
|
-
.filter((script): script is RegexScriptReportEntry => script !== undefined);
|
|
20
|
-
|
|
21
|
-
const report: RegexScriptReport = {
|
|
22
|
-
total: scripts.length,
|
|
23
|
-
enabled: 0,
|
|
24
|
-
disabled: 0,
|
|
25
|
-
promptOnly: 0,
|
|
26
|
-
markdownOnly: 0,
|
|
27
|
-
mixed: 0,
|
|
28
|
-
unspecified: 0,
|
|
29
|
-
converted: 0,
|
|
30
|
-
scripts,
|
|
31
|
-
rules: [],
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const seenRuleIds = new Set<string>();
|
|
35
|
-
for (const [index, script] of scripts.entries()) {
|
|
36
|
-
const conversion = convertPromptOnlyRegexScript(script, index, seenRuleIds);
|
|
37
|
-
if (conversion.rule) {
|
|
38
|
-
report.rules.push(conversion.rule);
|
|
39
|
-
report.converted++;
|
|
40
|
-
script.convertedRuleId = conversion.rule.id;
|
|
41
|
-
} else if (conversion.note) {
|
|
42
|
-
script.conversionNote = conversion.note;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!script.enabled) {
|
|
46
|
-
report.disabled++;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
report.enabled++;
|
|
50
|
-
if (script.mode === "prompt-only") report.promptOnly++;
|
|
51
|
-
else if (script.mode === "markdown-only") report.markdownOnly++;
|
|
52
|
-
else if (script.mode === "prompt+markdown") report.mixed++;
|
|
53
|
-
else if (script.mode === "unspecified") report.unspecified++;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return report;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function classifyRegexScript(raw: unknown, index: number): RegexScriptReportEntry | undefined {
|
|
60
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
61
|
-
const script = raw as StRegexScript;
|
|
62
|
-
const enabled = script.disabled !== true;
|
|
63
|
-
const promptOnly = script.promptOnly === true;
|
|
64
|
-
const markdownOnly = script.markdownOnly === true;
|
|
65
|
-
const mode: RegexScriptMode = enabled
|
|
66
|
-
? promptOnly && markdownOnly
|
|
67
|
-
? "prompt+markdown"
|
|
68
|
-
: promptOnly
|
|
69
|
-
? "prompt-only"
|
|
70
|
-
: markdownOnly
|
|
71
|
-
? "markdown-only"
|
|
72
|
-
: "unspecified"
|
|
73
|
-
: "disabled";
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
name: firstString(script.script_name, script.scriptName, script.name) ?? `regex-${index + 1}`,
|
|
77
|
-
mode,
|
|
78
|
-
enabled,
|
|
79
|
-
promptOnly,
|
|
80
|
-
markdownOnly,
|
|
81
|
-
findRegex: firstString(script.findRegex),
|
|
82
|
-
replaceString: firstString(script.replaceString),
|
|
83
|
-
trimStrings: normalizeTrimStrings(script.trimStrings),
|
|
84
|
-
placement: normalizeNumberArray(script.placement),
|
|
85
|
-
substituteRegex: normalizeNumber(script.substituteRegex),
|
|
86
|
-
minDepth: normalizeDepth(script.minDepth, -1),
|
|
87
|
-
maxDepth: normalizeDepth(script.maxDepth, 0),
|
|
88
|
-
conversionWarnings: [],
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function convertPromptOnlyRegexScript(
|
|
93
|
-
entry: RegexScriptReportEntry,
|
|
94
|
-
index: number,
|
|
95
|
-
seenRuleIds: Set<string>,
|
|
96
|
-
): { rule?: PromptRegexRule; note?: string } {
|
|
97
|
-
if (!entry.promptOnly) return { note: entry.enabled ? "not prompt-only" : "disabled non-prompt script" };
|
|
98
|
-
if (entry.markdownOnly) return { note: "mixed prompt/display script requires review" };
|
|
99
|
-
if (!entry.findRegex) return { note: "missing findRegex" };
|
|
100
|
-
|
|
101
|
-
const parsed = parseSillyTavernRegex(entry.findRegex);
|
|
102
|
-
if ("error" in parsed) return { note: parsed.error };
|
|
103
|
-
if (entry.placement && entry.placement.length > 0 && !entry.placement.some((placement) => placement === 1 || placement === 2)) {
|
|
104
|
-
return { note: "placement has no pi-forge runtime mapping" };
|
|
105
|
-
}
|
|
106
|
-
const replacement = normalizeSillyTavernReplacement(entry.replaceString ?? "", entry.conversionWarnings);
|
|
107
|
-
const roles = rolesForSillyTavernPlacement(entry.placement, entry.conversionWarnings);
|
|
108
|
-
if (entry.substituteRegex && entry.substituteRegex !== 0) {
|
|
109
|
-
entry.conversionWarnings.push("substituteRegex is preserved as metadata but dynamic find-regex macro substitution is not executed");
|
|
110
|
-
}
|
|
111
|
-
for (const placement of entry.placement ?? []) {
|
|
112
|
-
if (placement !== 1 && placement !== 2) {
|
|
113
|
-
entry.conversionWarnings.push(`placement ${placementLabel(placement)} is preserved as metadata but not directly mapped`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const id = uniqueRegexRuleId(`st-${entry.name || `regex-${index + 1}`}`, seenRuleIds);
|
|
118
|
-
const rule: PromptRegexRule = {
|
|
119
|
-
id,
|
|
120
|
-
name: entry.name,
|
|
121
|
-
enabled: entry.enabled,
|
|
122
|
-
stage: "history",
|
|
123
|
-
effect: "outgoing",
|
|
124
|
-
pattern: parsed.pattern,
|
|
125
|
-
replace: replacement,
|
|
126
|
-
source: {
|
|
127
|
-
sillytavern: {
|
|
128
|
-
scriptName: entry.name,
|
|
129
|
-
findRegex: entry.findRegex,
|
|
130
|
-
replaceString: entry.replaceString ?? "",
|
|
131
|
-
trimStrings: entry.trimStrings ?? [],
|
|
132
|
-
placement: entry.placement ?? [],
|
|
133
|
-
placementNames: (entry.placement ?? []).map(placementLabel),
|
|
134
|
-
promptOnly: entry.promptOnly,
|
|
135
|
-
markdownOnly: entry.markdownOnly,
|
|
136
|
-
substituteRegex: entry.substituteRegex ?? 0,
|
|
137
|
-
minDepth: entry.minDepth,
|
|
138
|
-
maxDepth: entry.maxDepth,
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
if (parsed.flags) rule.flags = parsed.flags;
|
|
143
|
-
if (entry.trimStrings && entry.trimStrings.length > 0) rule.trimStrings = entry.trimStrings;
|
|
144
|
-
if (roles && roles.length > 0) rule.roles = roles;
|
|
145
|
-
if (isNonNegativeInteger(entry.minDepth)) rule.minDepth = entry.minDepth;
|
|
146
|
-
if (isNonNegativeInteger(entry.maxDepth)) rule.maxDepth = entry.maxDepth;
|
|
147
|
-
return {
|
|
148
|
-
rule,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function parseSillyTavernRegex(value: string): { pattern: string; flags?: string } | { error: string } {
|
|
153
|
-
const trimmed = value.trim();
|
|
154
|
-
if (!trimmed) return { error: "empty findRegex" };
|
|
155
|
-
|
|
156
|
-
if (!trimmed.startsWith("/")) return validateParsedRegex(trimmed, "");
|
|
157
|
-
|
|
158
|
-
const closingSlash = findRegexLiteralClosingSlash(trimmed);
|
|
159
|
-
if (closingSlash <= 0) return { error: "could not parse regex literal" };
|
|
160
|
-
|
|
161
|
-
const pattern = trimmed.slice(1, closingSlash);
|
|
162
|
-
const flags = trimmed.slice(closingSlash + 1);
|
|
163
|
-
return validateParsedRegex(pattern, flags);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function normalizeSillyTavernReplacement(value: string, warnings: string[]): string {
|
|
167
|
-
let result = value;
|
|
168
|
-
if (/\{\{\s*match\s*\}\}/i.test(result)) {
|
|
169
|
-
result = result.replace(/\{\{\s*match\s*\}\}/gi, () => "$&");
|
|
170
|
-
warnings.push("converted SillyTavern {{match}} to JavaScript $&");
|
|
171
|
-
}
|
|
172
|
-
if (/\$0(?!\d)/.test(result)) {
|
|
173
|
-
result = result.replace(/\$0(?!\d)/g, () => "$&");
|
|
174
|
-
warnings.push("converted SillyTavern-style $0 full match to JavaScript $&");
|
|
175
|
-
}
|
|
176
|
-
return result;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function rolesForSillyTavernPlacement(placement: number[] | undefined, warnings: string[]): string[] | undefined {
|
|
180
|
-
if (!placement || placement.length === 0) return undefined;
|
|
181
|
-
const roles: string[] = [];
|
|
182
|
-
if (placement.includes(1)) roles.push("user");
|
|
183
|
-
if (placement.includes(2)) roles.push("assistant");
|
|
184
|
-
if (placement.includes(6)) warnings.push("reasoning placement is preserved as metadata; use stripAssistantThinking or manual rules for thinking content");
|
|
185
|
-
return roles.length > 0 ? roles : undefined;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function placementLabel(value: number): string {
|
|
189
|
-
return ST_REGEX_PLACEMENT_LABELS[value] ? `${ST_REGEX_PLACEMENT_LABELS[value]} (${value})` : `unknown (${value})`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function normalizeTrimStrings(value: unknown): string[] | undefined {
|
|
193
|
-
if (typeof value === "string") {
|
|
194
|
-
const strings = value.split(/\r?\n/).filter((entry) => entry.length > 0);
|
|
195
|
-
return strings.length > 0 ? strings : undefined;
|
|
196
|
-
}
|
|
197
|
-
if (!Array.isArray(value)) return undefined;
|
|
198
|
-
const strings = value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
|
199
|
-
return strings.length > 0 ? strings : undefined;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function normalizeNumberArray(value: unknown): number[] | undefined {
|
|
203
|
-
if (!Array.isArray(value)) return undefined;
|
|
204
|
-
const numbers = value
|
|
205
|
-
.map(normalizeNumber)
|
|
206
|
-
.filter((entry): entry is number => entry !== undefined);
|
|
207
|
-
return numbers.length > 0 ? numbers : undefined;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function normalizeNumber(value: unknown): number | undefined {
|
|
211
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
212
|
-
if (typeof value === "string" && value.trim()) {
|
|
213
|
-
const parsed = Number(value);
|
|
214
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
215
|
-
}
|
|
216
|
-
if (typeof value === "boolean") return value ? 1 : 0;
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function normalizeDepth(value: unknown, minimum: number): number | undefined {
|
|
221
|
-
const number = normalizeNumber(value);
|
|
222
|
-
if (number === undefined || !Number.isInteger(number) || number < minimum) return undefined;
|
|
223
|
-
return number;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function isNonNegativeInteger(value: unknown): value is number {
|
|
227
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function validateParsedRegex(pattern: string, flags: string): { pattern: string; flags?: string } | { error: string } {
|
|
231
|
-
if (!pattern) return { error: "empty regex pattern" };
|
|
232
|
-
const seen = new Set<string>();
|
|
233
|
-
for (const flag of flags) {
|
|
234
|
-
if (!["g", "i", "m", "s", "u"].includes(flag)) return { error: `unsupported regex flag: ${flag}` };
|
|
235
|
-
if (seen.has(flag)) return { error: `duplicate regex flag: ${flag}` };
|
|
236
|
-
seen.add(flag);
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
new RegExp(pattern, flags);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
return { error: `regex failed to compile: ${error instanceof Error ? error.message : String(error)}` };
|
|
242
|
-
}
|
|
243
|
-
return { pattern, flags: flags || undefined };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function findRegexLiteralClosingSlash(value: string): number {
|
|
247
|
-
for (let index = value.length - 1; index > 0; index--) {
|
|
248
|
-
if (value[index] !== "/") continue;
|
|
249
|
-
let slashCount = 0;
|
|
250
|
-
for (let backslash = index - 1; backslash >= 0 && value[backslash] === "\\"; backslash--) slashCount++;
|
|
251
|
-
if (slashCount % 2 === 0) return index;
|
|
252
|
-
}
|
|
253
|
-
return -1;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function uniqueRegexRuleId(base: string, seen: Set<string>): string {
|
|
257
|
-
const normalized = base.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase() || "st-regex";
|
|
258
|
-
let candidate = normalized;
|
|
259
|
-
let suffix = 2;
|
|
260
|
-
while (seen.has(candidate)) candidate = `${normalized}-${suffix++}`;
|
|
261
|
-
seen.add(candidate);
|
|
262
|
-
return candidate;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function firstString(...values: unknown[]): string | undefined {
|
|
266
|
-
for (const value of values) {
|
|
267
|
-
if (typeof value === "string" && value.trim()) return value;
|
|
268
|
-
}
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|