@zihanw/pi-forge 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +80 -2
  2. package/README.zh-CN.md +65 -2
  3. package/dist/compiler.d.ts +10 -0
  4. package/dist/compiler.d.ts.map +1 -0
  5. package/dist/compiler.js +490 -0
  6. package/dist/compiler.js.map +1 -0
  7. package/dist/extension-registry.d.ts +25 -0
  8. package/dist/extension-registry.d.ts.map +1 -0
  9. package/dist/extension-registry.js +6 -0
  10. package/dist/extension-registry.js.map +1 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +364 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lifecycle.d.ts +14 -0
  16. package/dist/lifecycle.d.ts.map +1 -0
  17. package/dist/lifecycle.js +139 -0
  18. package/dist/lifecycle.js.map +1 -0
  19. package/dist/loader.d.ts +8 -0
  20. package/dist/loader.d.ts.map +1 -0
  21. package/dist/loader.js +345 -0
  22. package/dist/loader.js.map +1 -0
  23. package/dist/macro-engine.d.ts +23 -0
  24. package/dist/macro-engine.d.ts.map +1 -0
  25. package/dist/macro-engine.js +262 -0
  26. package/dist/macro-engine.js.map +1 -0
  27. package/dist/payload-capture.d.ts +15 -0
  28. package/dist/payload-capture.d.ts.map +1 -0
  29. package/dist/payload-capture.js +86 -0
  30. package/dist/payload-capture.js.map +1 -0
  31. package/dist/payload-command.d.ts +10 -0
  32. package/dist/payload-command.d.ts.map +1 -0
  33. package/dist/payload-command.js +112 -0
  34. package/dist/payload-command.js.map +1 -0
  35. package/dist/policy.d.ts +6 -0
  36. package/dist/policy.d.ts.map +1 -0
  37. package/dist/policy.js +38 -0
  38. package/dist/policy.js.map +1 -0
  39. package/dist/preset-command.d.ts +14 -0
  40. package/dist/preset-command.d.ts.map +1 -0
  41. package/dist/preset-command.js +223 -0
  42. package/dist/preset-command.js.map +1 -0
  43. package/dist/preview.d.ts +12 -0
  44. package/dist/preview.d.ts.map +1 -0
  45. package/dist/preview.js +179 -0
  46. package/dist/preview.js.map +1 -0
  47. package/dist/regex.d.ts +7 -0
  48. package/dist/regex.d.ts.map +1 -0
  49. package/dist/regex.js +432 -0
  50. package/dist/regex.js.map +1 -0
  51. package/dist/render-helpers.d.ts +44 -0
  52. package/dist/render-helpers.d.ts.map +1 -0
  53. package/dist/render-helpers.js +128 -0
  54. package/dist/render-helpers.js.map +1 -0
  55. package/dist/runtime-state.d.ts +24 -0
  56. package/dist/runtime-state.d.ts.map +1 -0
  57. package/dist/runtime-state.js +13 -0
  58. package/dist/runtime-state.js.map +1 -0
  59. package/dist/sillytavern-importer/items.d.ts +3 -0
  60. package/dist/sillytavern-importer/items.d.ts.map +1 -0
  61. package/dist/sillytavern-importer/items.js +88 -0
  62. package/dist/sillytavern-importer/items.js.map +1 -0
  63. package/dist/sillytavern-importer/macros.d.ts +15 -0
  64. package/dist/sillytavern-importer/macros.d.ts.map +1 -0
  65. package/dist/sillytavern-importer/macros.js +141 -0
  66. package/dist/sillytavern-importer/macros.js.map +1 -0
  67. package/dist/sillytavern-importer/prompt-order.d.ts +6 -0
  68. package/dist/sillytavern-importer/prompt-order.d.ts.map +1 -0
  69. package/dist/sillytavern-importer/prompt-order.js +38 -0
  70. package/dist/sillytavern-importer/prompt-order.js.map +1 -0
  71. package/dist/sillytavern-importer/regex.d.ts +3 -0
  72. package/dist/sillytavern-importer/regex.d.ts.map +1 -0
  73. package/dist/sillytavern-importer/regex.js +275 -0
  74. package/dist/sillytavern-importer/regex.js.map +1 -0
  75. package/dist/sillytavern-importer/report.d.ts +21 -0
  76. package/dist/sillytavern-importer/report.d.ts.map +1 -0
  77. package/dist/sillytavern-importer/report.js +166 -0
  78. package/dist/sillytavern-importer/report.js.map +1 -0
  79. package/dist/sillytavern-importer/types.d.ts +106 -0
  80. package/dist/sillytavern-importer/types.d.ts.map +1 -0
  81. package/dist/sillytavern-importer/types.js +2 -0
  82. package/dist/sillytavern-importer/types.js.map +1 -0
  83. package/dist/sillytavern-importer.d.ts +5 -0
  84. package/dist/sillytavern-importer.d.ts.map +1 -0
  85. package/dist/sillytavern-importer.js +117 -0
  86. package/dist/sillytavern-importer.js.map +1 -0
  87. package/dist/slot-renderers.d.ts +25 -0
  88. package/dist/slot-renderers.d.ts.map +1 -0
  89. package/dist/slot-renderers.js +334 -0
  90. package/dist/slot-renderers.js.map +1 -0
  91. package/dist/stack-migration.d.ts +29 -0
  92. package/dist/stack-migration.d.ts.map +1 -0
  93. package/dist/stack-migration.js +126 -0
  94. package/dist/stack-migration.js.map +1 -0
  95. package/dist/storage.d.ts +6 -0
  96. package/dist/storage.d.ts.map +1 -0
  97. package/dist/storage.js +23 -0
  98. package/dist/storage.js.map +1 -0
  99. package/dist/types.d.ts +173 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +2 -0
  102. package/dist/types.js.map +1 -0
  103. package/dist/web-editor/index.d.ts +3 -0
  104. package/dist/web-editor/index.d.ts.map +1 -0
  105. package/dist/web-editor/index.js +2 -0
  106. package/dist/web-editor/index.js.map +1 -0
  107. package/dist/web-editor/page.d.ts +2 -0
  108. package/dist/web-editor/page.d.ts.map +1 -0
  109. package/dist/web-editor/page.js +3331 -0
  110. package/dist/web-editor/page.js.map +1 -0
  111. package/dist/web-editor/server.d.ts +4 -0
  112. package/dist/web-editor/server.d.ts.map +1 -0
  113. package/dist/web-editor/server.js +263 -0
  114. package/dist/web-editor/server.js.map +1 -0
  115. package/dist/web-editor/types.d.ts +125 -0
  116. package/dist/web-editor/types.d.ts.map +1 -0
  117. package/dist/web-editor/types.js +2 -0
  118. package/dist/web-editor/types.js.map +1 -0
  119. package/dist/web-host.d.ts +29 -0
  120. package/dist/web-host.d.ts.map +1 -0
  121. package/dist/web-host.js +174 -0
  122. package/dist/web-host.js.map +1 -0
  123. package/examples/custom-system-status-extension/README.md +78 -0
  124. package/examples/custom-system-status-extension/index.ts +176 -0
  125. package/examples/custom-system-status-extension/prompt-stack.json +39 -0
  126. package/examples/image-reader-prompt-stack.json +124 -0
  127. package/package.json +28 -2
  128. package/src/compiler.ts +2 -472
  129. package/src/extension-registry.ts +33 -0
  130. package/src/index.ts +192 -652
  131. package/src/lifecycle.ts +171 -0
  132. package/src/macro-engine.ts +358 -0
  133. package/src/payload-command.ts +138 -0
  134. package/src/preset-command.ts +271 -0
  135. package/src/preview.ts +226 -0
  136. package/src/render-helpers.ts +169 -0
  137. package/src/runtime-state.ts +36 -0
  138. package/src/sillytavern-importer/items.ts +98 -0
  139. package/src/sillytavern-importer/macros.ts +159 -0
  140. package/src/sillytavern-importer/prompt-order.ts +54 -0
  141. package/src/sillytavern-importer/regex.ts +270 -0
  142. package/src/sillytavern-importer/report.ts +202 -0
  143. package/src/sillytavern-importer/types.ts +120 -0
  144. package/src/sillytavern-importer.ts +55 -865
  145. package/src/slot-renderers.ts +414 -0
  146. package/src/types.ts +3 -14
  147. package/src/web-editor/page.ts +516 -30
  148. package/src/web-editor/server.ts +5 -0
  149. package/src/web-editor/types.ts +14 -0
  150. package/src/web-host.ts +3 -0
package/src/compiler.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
2
  import type {
5
3
  CompileMessageSource,
6
4
  CompileMessagesResult,
@@ -11,13 +9,12 @@ import type {
11
9
  PromptStackItem,
12
10
  PromptStackRole,
13
11
  PromptStackSlotItem,
14
- PromptStackSlotFormat,
15
12
  PromptVariableValue,
16
13
  PromptVariableStore,
17
14
  } from "./types.ts";
18
- import { applyResourcePolicy } from "./policy.ts";
19
15
  import { applyRegexRulesToMessages, applyRegexRulesToString } from "./regex.ts";
20
- import { SUPPORTED_SLOTS } from "./types.ts";
16
+ import { expandMacros } from "./macro-engine.ts";
17
+ import { renderSlotText } from "./slot-renderers.ts";
21
18
 
22
19
  const ZERO_USAGE = {
23
20
  input: 0,
@@ -28,8 +25,6 @@ const ZERO_USAGE = {
28
25
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
29
26
  };
30
27
 
31
- type PromptVariableScope = "static" | "session" | "turn";
32
-
33
28
  const SUMMARY_ROLES = new Set(["branchSummary", "compactionSummary"]);
34
29
 
35
30
  export function createPromptVariableStore(sessionVariables: Record<string, PromptVariableValue> = {}): PromptVariableStore {
@@ -528,448 +523,6 @@ function enabledItems(stack: PromptStack): PromptStackItem[] {
528
523
  return stack.items.filter((item) => item.enabled !== false);
529
524
  }
530
525
 
531
- function renderSlotText(
532
- item: PromptStackSlotItem,
533
- stack: PromptStack,
534
- runtime: PromptRuntime,
535
- diagnostics: PromptStackDiagnostic[],
536
- ): string {
537
- const slot = item.slot;
538
-
539
- if (!SUPPORTED_SLOTS.has(slot as any)) {
540
- diagnostics.push({ level: "warning", message: `Unsupported slot: ${slot}`, itemId: item.id });
541
- return "";
542
- }
543
-
544
- switch (slot) {
545
- case "tools":
546
- return renderTools(item, stack, runtime);
547
- case "tool-guidelines":
548
- return renderToolGuidelines(item, stack, runtime);
549
- case "skills":
550
- return renderSkills(item, stack, runtime);
551
- case "project-context":
552
- return renderProjectContext(item, runtime);
553
- case "append-system-prompt":
554
- return runtime.options.appendSystemPrompt ?? "";
555
- case "date":
556
- return `Current date: ${formatDate(runtime.now)}`;
557
- case "cwd":
558
- return `Current working directory: ${runtime.options.cwd.replace(/\\/g, "/")}`;
559
- case "date-cwd":
560
- return [`Current date: ${formatDate(runtime.now)}`, `Current working directory: ${runtime.options.cwd.replace(/\\/g, "/")}`].join("\n");
561
- case "active-model": {
562
- const model = runtime.ctx?.model;
563
- return model ? `Current model: ${model.provider}/${model.id}` : "Current model: (none)";
564
- }
565
- case "pi-docs":
566
- return renderPiDocsGuidance();
567
- case "variables":
568
- return renderVariables(item, stack, runtime);
569
- case "chat-history":
570
- return "";
571
- default:
572
- return "";
573
- }
574
- }
575
-
576
- function renderTools(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
577
- const snippets = runtime.options.toolSnippets ?? {};
578
- const tools = item.options?.onlyWithSnippets === true
579
- ? scopedToolNames(stack, runtime).filter((name) => !!snippets[name])
580
- : scopedToolNames(stack, runtime);
581
-
582
- if (slotTextFormat(item) === "plain") {
583
- const lines = ["Available tools:"];
584
- if (tools.length === 0) {
585
- lines.push(item.options?.onlyWithSnippets === true ? "(none)" : "- (none)");
586
- } else {
587
- for (const name of tools) {
588
- lines.push(plainBullet(name, snippets[name] ?? "No prompt snippet provided."));
589
- }
590
- }
591
- return lines.join("\n");
592
- }
593
-
594
- const lines = ["<available_tools>"];
595
-
596
- if (tools.length === 0) {
597
- lines.push(" (none)");
598
- } else {
599
- for (const name of tools) {
600
- const snippet = snippets[name] ?? "No prompt snippet provided.";
601
- lines.push(` <tool name=\"${escapeXml(name)}\">${escapeXml(snippet)}</tool>`);
602
- }
603
- }
604
-
605
- lines.push("</available_tools>");
606
- return lines.join("\n");
607
- }
608
-
609
- function renderToolGuidelines(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
610
- const tools = scopedToolNames(stack, runtime);
611
- const guidelines: string[] = [];
612
- const seen = new Set<string>();
613
- const add = (line: string) => {
614
- const normalized = line.trim();
615
- if (!normalized || seen.has(normalized)) return;
616
- seen.add(normalized);
617
- guidelines.push(normalized);
618
- };
619
-
620
- if (tools.includes("bash") && !tools.includes("grep") && !tools.includes("find") && !tools.includes("ls")) {
621
- add(item.options?.piStyle === true ? "Use bash for file operations like ls, rg, find" : "Use bash for file operations like ls, rg, find.");
622
- }
623
-
624
- for (const guideline of runtime.options.promptGuidelines ?? []) add(guideline);
625
- if (item.options?.includePiDefaultGuidelines === true) {
626
- add("Be concise in your responses");
627
- add("Show file paths clearly when working with files");
628
- }
629
-
630
- if (guidelines.length === 0) return "";
631
- if (slotTextFormat(item) === "plain") {
632
- const heading = typeof item.options?.heading === "string" ? item.options.heading.trim() : "Tool guidelines:";
633
- return [
634
- ...(heading ? [heading] : []),
635
- ...guidelines.map((line) => `- ${plainContinuation(line, " ")}`),
636
- ].join("\n");
637
- }
638
- return ["<tool_guidelines>", ...guidelines.map((line) => `- ${line}`), "</tool_guidelines>"].join("\n");
639
- }
640
-
641
- function renderSkills(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
642
- if (item.options?.requireReadTool === true && !scopedToolNames(stack, runtime).includes("read")) return "";
643
- const skills = (runtime.options.skills ?? [])
644
- .filter((skill) => !skill.disableModelInvocation)
645
- .filter((skill) => applyResourcePolicy([skill.name], stack.skills).length > 0);
646
- if (skills.length === 0) return "";
647
-
648
- const lines = [
649
- "The following skills provide specialized instructions for specific tasks.",
650
- "Use the read tool to load a skill's file when the task matches its description.",
651
- "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
652
- "",
653
- ];
654
-
655
- if (slotTextFormat(item) === "plain") {
656
- lines.push("Available skills:");
657
- for (const skill of skills) {
658
- lines.push(plainBullet(skill.name, skill.description));
659
- lines.push(` Location: ${plainContinuation(skill.filePath, " ")}`);
660
- }
661
- return lines.join("\n");
662
- }
663
-
664
- lines.push("<available_skills>");
665
- for (const skill of skills) {
666
- lines.push(" <skill>");
667
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
668
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
669
- lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
670
- lines.push(" </skill>");
671
- }
672
-
673
- lines.push("</available_skills>");
674
- return lines.join("\n");
675
- }
676
-
677
- function scopedToolNames(stack: PromptStack, runtime: PromptRuntime): string[] {
678
- return applyResourcePolicy(runtime.options.selectedTools ?? [], stack.tools);
679
- }
680
-
681
- function renderProjectContext(item: PromptStackSlotItem, runtime: PromptRuntime): string {
682
- const contextFiles = runtime.options.contextFiles ?? [];
683
- if (contextFiles.length === 0) return "";
684
-
685
- if (slotTextFormat(item) === "plain") {
686
- const lines = ["Project context:", "", "Project-specific instructions and guidelines:", ""];
687
- for (const file of contextFiles) {
688
- lines.push(`Path: ${file.path}`);
689
- lines.push(indentPlainBlock(file.content, " "), "");
690
- }
691
- return lines.join("\n").trimEnd();
692
- }
693
-
694
- const lines = ["<project_context>", "", "Project-specific instructions and guidelines:", ""];
695
- for (const file of contextFiles) {
696
- lines.push(`<project_instructions path=\"${escapeXml(file.path)}\">`);
697
- lines.push(file.content);
698
- lines.push("</project_instructions>", "");
699
- }
700
- lines.push("</project_context>");
701
- return lines.join("\n");
702
- }
703
-
704
- function slotTextFormat(item: PromptStackSlotItem, options: { allowJson?: boolean } = {}): PromptStackSlotFormat {
705
- const format = item.options?.format;
706
- if (format === "plain") return "plain";
707
- if (format === "json" && options.allowJson) return "json";
708
- return "xml";
709
- }
710
-
711
- function renderPiDocsGuidance(): string {
712
- const paths = piDocsPaths();
713
- return [
714
- "Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
715
- `- Main documentation: ${paths.readme}`,
716
- `- Additional docs: ${paths.docs}`,
717
- `- Examples: ${paths.examples} (extensions, custom tools, SDK)`,
718
- "- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory",
719
- "- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)",
720
- "- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing",
721
- "- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)",
722
- ].join("\n");
723
- }
724
-
725
- function piDocsPaths(): { readme: string; docs: string; examples: string } {
726
- const fallbackRoot = "@earendil-works/pi-coding-agent";
727
- try {
728
- const resolve = (import.meta as unknown as { resolve?: (specifier: string) => string }).resolve;
729
- if (!resolve) throw new Error("import.meta.resolve unavailable");
730
- const resolved = resolve("@earendil-works/pi-coding-agent");
731
- if (!resolved.startsWith("file:")) throw new Error("non-file package resolution");
732
- const packageRoot = dirname(dirname(fileURLToPath(resolved)));
733
- return {
734
- readme: join(packageRoot, "README.md"),
735
- docs: join(packageRoot, "docs"),
736
- examples: join(packageRoot, "examples"),
737
- };
738
- } catch {
739
- return {
740
- readme: `${fallbackRoot}/README.md`,
741
- docs: `${fallbackRoot}/docs`,
742
- examples: `${fallbackRoot}/examples`,
743
- };
744
- }
745
- }
746
-
747
- function renderVariables(
748
- item: PromptStackSlotItem,
749
- stack: PromptStack,
750
- runtime: PromptRuntime,
751
- ): string {
752
- const options = item.options ?? {};
753
- const scopes = selectedVariableScopes(options);
754
- const format = slotTextFormat(item);
755
- const store = runtime.variables;
756
- const grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>> = {
757
- static: {},
758
- session: {},
759
- turn: {},
760
- };
761
-
762
- if (scopes.includes("static")) grouped.static = collectStaticVariables(stack);
763
- if (scopes.includes("session")) grouped.session = { ...(store?.session ?? {}) };
764
- if (scopes.includes("turn")) grouped.turn = { ...(store?.turn ?? {}) };
765
-
766
- const hasVariables = Object.values(grouped).some((values) => Object.keys(values).length > 0);
767
- if (!hasVariables) return "";
768
-
769
- if (format === "plain") {
770
- return renderPlainVariables(grouped);
771
- }
772
-
773
- const parts: string[] = ["<variables>"];
774
-
775
- for (const scope of scopes) {
776
- const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
777
- if (entries.length === 0) continue;
778
- parts.push(` <${scope}>`);
779
- for (const [name, value] of entries) {
780
- parts.push(` <var name=\"${escapeXml(name)}\">${escapeXml(variableValueToPromptText(value))}</var>`);
781
- }
782
- parts.push(` </${scope}>`);
783
- }
784
-
785
- parts.push("</variables>");
786
- return parts.join("\n");
787
- }
788
-
789
- function renderPlainVariables(
790
- grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>>,
791
- ): string {
792
- const parts: string[] = ["Variables:"];
793
-
794
- for (const scope of ["static", "session", "turn"] as const) {
795
- const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
796
- if (entries.length === 0) continue;
797
- parts.push(`${scope}:`);
798
- for (const [name, value] of entries) {
799
- parts.push(plainBullet(name, variableValueToPromptText(value)));
800
- }
801
- }
802
-
803
- return parts.join("\n");
804
- }
805
-
806
- function plainBullet(label: string, value: string): string {
807
- return `- ${label}: ${plainContinuation(value, " ")}`;
808
- }
809
-
810
- function plainContinuation(value: string, indent: string): string {
811
- return value.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
812
- }
813
-
814
- function indentPlainBlock(value: string, indent: string): string {
815
- return value.split("\n").map((line) => `${indent}${line}`).join("\n");
816
- }
817
-
818
- function selectedVariableScopes(options: Record<string, unknown>): PromptVariableScope[] {
819
- const scopes: PromptVariableScope[] = [];
820
- if (options.includeStatic !== false) scopes.push("static");
821
- if (options.includeSession !== false) scopes.push("session");
822
- if (options.includeTurn !== false) scopes.push("turn");
823
- return scopes;
824
- }
825
-
826
- function collectStaticVariables(stack: PromptStack): Record<string, PromptVariableValue> {
827
- return { ...(stack.variables ?? {}) };
828
- }
829
-
830
- function variableValueToPromptText(value: PromptVariableValue): string {
831
- if (typeof value === "string") return value;
832
- return JSON.stringify(value, null, 2);
833
- }
834
-
835
- function variableValueToMacroText(value: PromptVariableValue | undefined): string {
836
- if (value === undefined) return "";
837
- if (typeof value === "string") return value;
838
- return JSON.stringify(value);
839
- }
840
-
841
- function expandMacros(
842
- text: string,
843
- stack: PromptStack,
844
- runtime: PromptRuntime,
845
- diagnostics: PromptStackDiagnostic[],
846
- itemId: string,
847
- ): string {
848
- const policy = stack.defaults?.unresolvedMacroPolicy ?? "warn";
849
- const unknown = new Set<string>();
850
-
851
- const result = text.replace(/\{\{([^{}]+)\}\}/g, (full, rawName: string) => {
852
- const expression = rawName.trim();
853
- const parts = expression.split("::");
854
- const command = parts[0]?.trim();
855
-
856
- if (!command) return full;
857
-
858
- // SillyTavern-style mutable variables. By default, setvar is turn-scoped and returns empty text.
859
- if (command === "setvar" || command === "setturnvar" || command === "setsessionvar") {
860
- const scoped = command === "setvar" && (parts[1] === "turn" || parts[1] === "session");
861
- const scope = command === "setsessionvar" || (scoped && parts[1] === "session") ? "session" : "turn";
862
- const nameIndex = scoped ? 2 : 1;
863
- const valueIndex = nameIndex + 1;
864
- const name = parts[nameIndex]?.trim();
865
- const value = parts.slice(valueIndex).join("::");
866
- if (!name) {
867
- unknown.add(expression);
868
- return full;
869
- }
870
- setRuntimeVariable(runtime, scope, name, value);
871
- return "";
872
- }
873
-
874
- if (command === "getvar" || command === "var" || command === "getturnvar" || command === "getsessionvar") {
875
- const name = parts[1]?.trim();
876
- if (!name) {
877
- unknown.add(expression);
878
- return full;
879
- }
880
- if (command === "getturnvar") return variableValueToMacroText(runtime.variables?.turn[name]);
881
- if (command === "getsessionvar") return variableValueToMacroText(runtime.variables?.session[name]);
882
- return variableValueToMacroText(getRuntimeVariable(runtime, stack, name));
883
- }
884
-
885
- if (command === "clearvar" || command === "clearturnvar" || command === "clearsessionvar") {
886
- const scoped = command === "clearvar" && (parts[1] === "turn" || parts[1] === "session");
887
- const scope = command === "clearsessionvar" || (scoped && parts[1] === "session") ? "session" : "turn";
888
- const name = parts[scoped ? 2 : 1]?.trim();
889
- if (!name) {
890
- unknown.add(expression);
891
- return full;
892
- }
893
- clearRuntimeVariable(runtime, scope, name);
894
- return "";
895
- }
896
-
897
- const dynamicValue = getBuiltinMacro(command, stack, runtime);
898
- if (dynamicValue !== undefined) return dynamicValue;
899
-
900
- const variableValue = getRuntimeVariable(runtime, stack, command);
901
- if (variableValue !== undefined) return variableValueToMacroText(variableValue);
902
-
903
- unknown.add(expression);
904
- return full;
905
- });
906
-
907
- for (const name of unknown) {
908
- if (policy === "keep") continue;
909
- diagnostics.push({
910
- level: policy === "error" ? "error" : "warning",
911
- message: `Unresolved macro: {{${name}}}`,
912
- itemId,
913
- });
914
- }
915
-
916
- return result;
917
- }
918
-
919
- function getBuiltinMacro(name: string, stack: PromptStack, runtime: PromptRuntime): string | undefined {
920
- switch (name) {
921
- case "cwd":
922
- return runtime.options.cwd;
923
- case "date":
924
- return formatDate(runtime.now);
925
- case "time":
926
- return formatTime(runtime.now);
927
- case "lastUserMessage":
928
- return runtime.latestUserMessage ?? "";
929
- case "selectedTools":
930
- case "tools":
931
- return scopedToolNames(stack, runtime).join(", ");
932
- case "activeModel": {
933
- const model = runtime.ctx?.model;
934
- return model ? `${model.provider}/${model.id}` : "";
935
- }
936
- default:
937
- return undefined;
938
- }
939
- }
940
-
941
- function getRuntimeVariable(runtime: PromptRuntime, stack: PromptStack, name: string): PromptVariableValue | undefined {
942
- if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.turn, name)) return runtime.variables.turn[name];
943
- if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) return runtime.variables.session[name];
944
- const staticVariables = collectStaticVariables(stack);
945
- if (Object.prototype.hasOwnProperty.call(staticVariables, name)) return staticVariables[name];
946
- return undefined;
947
- }
948
-
949
- function setRuntimeVariable(runtime: PromptRuntime, scope: "turn" | "session", name: string, value: string): void {
950
- if (!runtime.variables) return;
951
- if (scope === "session") {
952
- if (runtime.variables.session[name] !== value) {
953
- runtime.variables.session[name] = value;
954
- runtime.variables.sessionDirty = true;
955
- }
956
- return;
957
- }
958
- runtime.variables.turn[name] = value;
959
- }
960
-
961
- function clearRuntimeVariable(runtime: PromptRuntime, scope: "turn" | "session", name: string): void {
962
- if (!runtime.variables) return;
963
- if (scope === "session") {
964
- if (Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) {
965
- delete runtime.variables.session[name];
966
- runtime.variables.sessionDirty = true;
967
- }
968
- return;
969
- }
970
- delete runtime.variables.turn[name];
971
- }
972
-
973
526
  function createSyntheticMessage(role: Exclude<PromptStackRole, "system">, content: string, stack: PromptStack, runtime: PromptRuntime): AgentMessage {
974
527
  const timestamp = runtime.now.getTime();
975
528
  const visible = stack.defaults?.syntheticMessagesVisible ?? false;
@@ -1023,26 +576,3 @@ function contentToText(content: unknown): string {
1023
576
  .filter(Boolean)
1024
577
  .join("\n");
1025
578
  }
1026
-
1027
- function formatDate(now: Date): string {
1028
- const year = now.getFullYear();
1029
- const month = String(now.getMonth() + 1).padStart(2, "0");
1030
- const day = String(now.getDate()).padStart(2, "0");
1031
- return `${year}-${month}-${day}`;
1032
- }
1033
-
1034
- function formatTime(now: Date): string {
1035
- const hours = String(now.getHours()).padStart(2, "0");
1036
- const minutes = String(now.getMinutes()).padStart(2, "0");
1037
- const seconds = String(now.getSeconds()).padStart(2, "0");
1038
- return `${hours}:${minutes}:${seconds}`;
1039
- }
1040
-
1041
- function escapeXml(value: string): string {
1042
- return value
1043
- .replace(/&/g, "&amp;")
1044
- .replace(/</g, "&lt;")
1045
- .replace(/>/g, "&gt;")
1046
- .replace(/\"/g, "&quot;")
1047
- .replace(/'/g, "&apos;");
1048
- }
@@ -0,0 +1,33 @@
1
+ export type PromptExtensionOptionType = "boolean" | "string" | "number" | "enum" | "stringArray";
2
+
3
+ export interface PromptExtensionOptionDefinition {
4
+ type: PromptExtensionOptionType;
5
+ description?: string;
6
+ default?: string | number | boolean | readonly string[];
7
+ values?: readonly string[];
8
+ min?: number;
9
+ max?: number;
10
+ integer?: boolean;
11
+ }
12
+
13
+ export type PromptExtensionOptionsSchema = Record<string, PromptExtensionOptionDefinition>;
14
+
15
+ export interface PromptExtensionArgumentDefinition {
16
+ name: string;
17
+ description?: string;
18
+ required?: boolean;
19
+ variadic?: boolean;
20
+ expansion?: "eager" | "lazy";
21
+ }
22
+
23
+ export interface PromptRegistryEntry {
24
+ name: string;
25
+ description?: string;
26
+ source?: string;
27
+ }
28
+
29
+ export function assertRegistryName(kind: string, name: string): void {
30
+ if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(name)) {
31
+ throw new Error(`${kind} name must start with a letter and contain only letters, numbers, underscore, dot, or hyphen: ${name}`);
32
+ }
33
+ }