@funkai/prompts 0.1.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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +18 -0
  2. package/.turbo/turbo-test$colon$coverage.log +26 -0
  3. package/.turbo/turbo-test.log +26 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/CHANGELOG.md +14 -0
  6. package/LICENSE +21 -0
  7. package/README.md +101 -0
  8. package/banner.svg +100 -0
  9. package/coverage/lcov-report/base.css +224 -0
  10. package/coverage/lcov-report/block-navigation.js +87 -0
  11. package/coverage/lcov-report/clean.ts.html +160 -0
  12. package/coverage/lcov-report/engine.ts.html +196 -0
  13. package/coverage/lcov-report/favicon.png +0 -0
  14. package/coverage/lcov-report/index.html +161 -0
  15. package/coverage/lcov-report/partials-dir.ts.html +100 -0
  16. package/coverage/lcov-report/prettify.css +1 -0
  17. package/coverage/lcov-report/prettify.js +2 -0
  18. package/coverage/lcov-report/registry.ts.html +280 -0
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -0
  21. package/coverage/lcov.info +75 -0
  22. package/dist/lib/index.d.mts +99 -0
  23. package/dist/lib/index.d.mts.map +1 -0
  24. package/dist/lib/index.mjs +117 -0
  25. package/dist/lib/index.mjs.map +1 -0
  26. package/dist/prompts/constraints.prompt +20 -0
  27. package/dist/prompts/identity.prompt +6 -0
  28. package/dist/prompts/prompts/constraints.prompt +20 -0
  29. package/dist/prompts/prompts/identity.prompt +6 -0
  30. package/dist/prompts/prompts/tools.prompt +14 -0
  31. package/dist/prompts/tools.prompt +14 -0
  32. package/docs/cli/commands.md +73 -0
  33. package/docs/cli/overview.md +73 -0
  34. package/docs/codegen/overview.md +83 -0
  35. package/docs/file-format/frontmatter.md +55 -0
  36. package/docs/file-format/overview.md +67 -0
  37. package/docs/file-format/partials.md +87 -0
  38. package/docs/guides/add-partial.md +75 -0
  39. package/docs/guides/author-prompt.md +70 -0
  40. package/docs/guides/setup-project.md +75 -0
  41. package/docs/library/overview.md +64 -0
  42. package/docs/overview.md +102 -0
  43. package/docs/troubleshooting.md +37 -0
  44. package/logo.svg +20 -0
  45. package/package.json +53 -0
  46. package/src/clean.test.ts +44 -0
  47. package/src/clean.ts +25 -0
  48. package/src/engine.test.ts +44 -0
  49. package/src/engine.ts +37 -0
  50. package/src/index.ts +11 -0
  51. package/src/partials-dir.test.ts +15 -0
  52. package/src/partials-dir.ts +5 -0
  53. package/src/prompts/constraints.prompt +20 -0
  54. package/src/prompts/identity.prompt +6 -0
  55. package/src/prompts/tools.prompt +14 -0
  56. package/src/registry.test.ts +69 -0
  57. package/src/registry.ts +65 -0
  58. package/src/types.ts +62 -0
  59. package/tsconfig.json +25 -0
  60. package/tsdown.config.ts +12 -0
  61. package/vitest.config.ts +21 -0
@@ -0,0 +1,44 @@
1
+ import { Liquid } from "liquidjs";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { createEngine, engine } from "@/engine.js";
5
+
6
+ describe("createEngine", () => {
7
+ it("should return a Liquid instance", () => {
8
+ const eng = createEngine("/tmp/test-partials");
9
+ expect(eng).toBeInstanceOf(Liquid);
10
+ });
11
+
12
+ it("should render basic variable expressions", () => {
13
+ const eng = createEngine("/tmp/test-partials");
14
+ const result = eng.parseAndRenderSync("Hello {{ name }}", { name: "World" });
15
+ expect(result).toBe("Hello World");
16
+ });
17
+
18
+ it("should merge custom options with defaults", () => {
19
+ const eng = createEngine("/tmp/test-partials", {
20
+ cache: false,
21
+ strictFilters: false,
22
+ });
23
+ // Should not throw on unknown filter when strictFilters is disabled
24
+ const result = eng.parseAndRenderSync("{{ name | nonexistent }}", { name: "test" });
25
+ expect(result).toBe("test");
26
+ });
27
+ });
28
+
29
+ describe("engine", () => {
30
+ it("should be a Liquid instance", () => {
31
+ expect(engine).toBeInstanceOf(Liquid);
32
+ });
33
+
34
+ it("should render variable expressions", () => {
35
+ const result = engine.parseAndRenderSync("Hello {{ name }}", { name: "World" });
36
+ expect(result).toBe("Hello World");
37
+ });
38
+
39
+ it("should throw on unknown filters (strictFilters)", () => {
40
+ expect(() => {
41
+ engine.parseAndRenderSync("{{ name | bogus }}", { name: "test" });
42
+ }).toThrow();
43
+ });
44
+ });
package/src/engine.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Liquid } from "liquidjs";
2
+
3
+ import type { CreateEngineOptions } from "./types.js";
4
+
5
+ /**
6
+ * Create a LiquidJS engine with custom options.
7
+ *
8
+ * The `partialsDir` is used as the root for `{% render %}` resolution.
9
+ * The `.prompt` extension is appended automatically.
10
+ *
11
+ * @param partialsDir - Root directory for `{% render %}` partial resolution.
12
+ * @param options - Optional overrides for the LiquidJS engine configuration.
13
+ * @returns A configured {@link Liquid} engine instance.
14
+ */
15
+ export function createEngine(partialsDir: string, options?: Partial<CreateEngineOptions>): Liquid {
16
+ return new Liquid({
17
+ root: [partialsDir],
18
+ partials: [partialsDir],
19
+ extname: ".prompt",
20
+ cache: true,
21
+ strictFilters: true,
22
+ ownPropertyOnly: true,
23
+ ...options,
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Shared LiquidJS engine for rendering prompt templates at runtime.
29
+ *
30
+ * Partials are flattened at codegen time by the CLI, so this engine
31
+ * only needs to handle `{{ var }}` expressions and basic Liquid
32
+ * control flow (`{% if %}`, `{% for %}`). No filesystem access required.
33
+ */
34
+ export const engine = new Liquid({
35
+ strictFilters: true,
36
+ ownPropertyOnly: true,
37
+ });
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { engine, createEngine } from "./engine.js";
2
+ export { clean } from "./clean.js";
3
+ export { PARTIALS_DIR } from "./partials-dir.js";
4
+ export { createPromptRegistry } from "./registry.js";
5
+ export type {
6
+ CreateEngineOptions,
7
+ Liquid,
8
+ PromptModule,
9
+ PromptNamespace,
10
+ PromptRegistry,
11
+ } from "./types.js";
@@ -0,0 +1,15 @@
1
+ import { isAbsolute } from "node:path";
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { PARTIALS_DIR } from "@/partials-dir.js";
6
+
7
+ describe("PARTIALS_DIR", () => {
8
+ it("should be an absolute path", () => {
9
+ expect(isAbsolute(PARTIALS_DIR)).toBe(true);
10
+ });
11
+
12
+ it("should point to the prompts directory", () => {
13
+ expect(PARTIALS_DIR).toMatch(/prompts$/);
14
+ });
15
+ });
@@ -0,0 +1,5 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ /** Absolute path to the SDK's built-in partials directory. */
5
+ export const PARTIALS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../prompts");
@@ -0,0 +1,20 @@
1
+ <constraints>
2
+ {% if in_scope %}
3
+ ## In Scope
4
+ {% for item in in_scope %}
5
+ - {{ item }}
6
+ {% endfor %}
7
+ {% endif %}
8
+ {% if out_of_scope %}
9
+ ## Out of Scope
10
+ {% for item in out_of_scope %}
11
+ - {{ item }}
12
+ {% endfor %}
13
+ {% endif %}
14
+ {% if rules %}
15
+ ## Rules
16
+ {% for rule in rules %}
17
+ - {{ rule }}
18
+ {% endfor %}
19
+ {% endif %}
20
+ </constraints>
@@ -0,0 +1,6 @@
1
+ <identity>
2
+ You are {{ role }}, {{ desc }}.
3
+ {% if context %}
4
+ {{ context }}
5
+ {% endif %}
6
+ </identity>
@@ -0,0 +1,14 @@
1
+ <tools>
2
+ You have access to the following tools:
3
+ {% if tools %}
4
+ {% for tool in tools %}
5
+ **{{ tool.name }}** -- {{ tool.description }}
6
+ {% for param in tool.params %}
7
+ - `{{ param.name }}` ({{ param.type }}{% if param.required %}, required{% endif %}) -- {{ param.description }}
8
+ {% endfor %}
9
+
10
+ {% endfor %}
11
+ {% else %}
12
+ No tools are configured for this agent.
13
+ {% endif %}
14
+ </tools>
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { z } from "zod";
3
+
4
+ import { createPromptRegistry } from "@/registry.js";
5
+
6
+ const mockPrompt = {
7
+ name: "test-prompt" as const,
8
+ group: "agents" as const,
9
+ schema: z.object({ name: z.string() }),
10
+ render(variables: { name: string }) {
11
+ return `Hello ${variables.name}`;
12
+ },
13
+ validate(variables: unknown) {
14
+ return z.object({ name: z.string() }).parse(variables);
15
+ },
16
+ };
17
+
18
+ const emptyPrompt = {
19
+ name: "empty" as const,
20
+ group: undefined,
21
+ schema: z.object({}),
22
+ render() {
23
+ return "static";
24
+ },
25
+ validate(variables: unknown) {
26
+ return z.object({}).parse(variables);
27
+ },
28
+ };
29
+
30
+ describe("createPromptRegistry", () => {
31
+ it("should provide dot-access to a registered prompt", () => {
32
+ const registry = createPromptRegistry({ testPrompt: mockPrompt });
33
+ expect(registry.testPrompt.name).toBe("test-prompt");
34
+ expect(registry.testPrompt.render({ name: "Alice" })).toBe("Hello Alice");
35
+ });
36
+
37
+ it("should provide nested dot-access for grouped prompts", () => {
38
+ const registry = createPromptRegistry({
39
+ agents: { testPrompt: mockPrompt },
40
+ });
41
+ expect(registry.agents.testPrompt.name).toBe("test-prompt");
42
+ expect(registry.agents.testPrompt.render({ name: "Bob" })).toBe("Hello Bob");
43
+ });
44
+
45
+ it("should freeze the top-level registry object", () => {
46
+ const registry = createPromptRegistry({ testPrompt: mockPrompt });
47
+ expect(Object.isFrozen(registry)).toBe(true);
48
+ });
49
+
50
+ it("should freeze nested namespace objects", () => {
51
+ const registry = createPromptRegistry({
52
+ agents: { testPrompt: mockPrompt },
53
+ });
54
+ expect(Object.isFrozen(registry.agents)).toBe(true);
55
+ });
56
+
57
+ it("should expose all keys via Object.keys", () => {
58
+ const registry = createPromptRegistry({
59
+ testPrompt: mockPrompt,
60
+ empty: emptyPrompt,
61
+ });
62
+ expect(Object.keys(registry)).toEqual(["testPrompt", "empty"]);
63
+ });
64
+
65
+ it("should work with an empty registry", () => {
66
+ const registry = createPromptRegistry({});
67
+ expect(Object.keys(registry)).toEqual([]);
68
+ });
69
+ });
@@ -0,0 +1,65 @@
1
+ import type { PromptModule, PromptNamespace, PromptRegistry } from "./types.js";
2
+
3
+ /**
4
+ * Check whether a value looks like a PromptModule leaf.
5
+ * Leaves have `name`, `schema`, and `render` — namespaces do not.
6
+ *
7
+ * @private
8
+ */
9
+ function isPromptModule(value: unknown): value is PromptModule {
10
+ return (
11
+ typeof value === "object" &&
12
+ value !== null &&
13
+ "render" in value &&
14
+ "schema" in value &&
15
+ "name" in value
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Recursively freeze a prompt namespace tree.
21
+ * Only recurses into plain namespace nodes — PromptModule leaves
22
+ * (which contain Zod schemas) are frozen shallowly.
23
+ *
24
+ * @param obj - The namespace object to freeze.
25
+ * @returns The frozen object cast to its deep-readonly type.
26
+ *
27
+ * @private
28
+ */
29
+ function deepFreeze<T extends PromptNamespace>(obj: T): PromptRegistry<T> {
30
+ Object.freeze(obj);
31
+ Object.values(obj).forEach((value) => {
32
+ if (
33
+ typeof value === "object" &&
34
+ value !== null &&
35
+ !Object.isFrozen(value) &&
36
+ !isPromptModule(value)
37
+ ) {
38
+ deepFreeze(value as PromptNamespace);
39
+ }
40
+ });
41
+ return obj as PromptRegistry<T>;
42
+ }
43
+
44
+ /**
45
+ * Create a typed, frozen prompt registry from a (possibly nested) map of prompt modules.
46
+ *
47
+ * The registry is typically created by generated code — the CLI produces
48
+ * an `index.ts` that calls `createPromptRegistry()` with all discovered
49
+ * prompt modules keyed by camelCase name, nested by group.
50
+ *
51
+ * @param modules - Record mapping camelCase prompt names (or group namespaces) to their modules.
52
+ * @returns A deep-frozen, typed record with direct property access.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const prompts = createPromptRegistry({
57
+ * agents: { coverageAssessor },
58
+ * greeting,
59
+ * })
60
+ * prompts.agents.coverageAssessor.render({ scope: 'full' })
61
+ * ```
62
+ */
63
+ export function createPromptRegistry<T extends PromptNamespace>(modules: T): PromptRegistry<T> {
64
+ return deepFreeze({ ...modules });
65
+ }
package/src/types.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { Liquid, LiquidOptions } from "liquidjs";
2
+ import type { ZodType } from "zod";
3
+
4
+ /**
5
+ * Options for creating a custom LiquidJS engine.
6
+ */
7
+ export type CreateEngineOptions = Pick<
8
+ LiquidOptions,
9
+ | "root"
10
+ | "partials"
11
+ | "extname"
12
+ | "cache"
13
+ | "strictFilters"
14
+ | "strictVariables"
15
+ | "ownPropertyOnly"
16
+ >;
17
+
18
+ /**
19
+ * A single prompt module produced by codegen.
20
+ *
21
+ * Each `.prompt` file generates a default export conforming to this shape.
22
+ */
23
+ export interface PromptModule<T = unknown> {
24
+ readonly name: string;
25
+ readonly group: string | undefined;
26
+ readonly schema: ZodType<T>;
27
+ render(variables: T): string;
28
+ validate(variables: unknown): T;
29
+ }
30
+
31
+ /**
32
+ * A nested namespace node in the prompt tree.
33
+ * Values are either PromptModule leaves or further nested namespaces.
34
+ */
35
+ export type PromptNamespace = {
36
+ readonly [key: string]: PromptModule | PromptNamespace;
37
+ };
38
+
39
+ /**
40
+ * Deep-readonly version of a prompt tree.
41
+ * Prevents reassignment at any nesting level.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * type MyRegistry = PromptRegistry<{
46
+ * agents: { coverageAssessor: PromptModule }
47
+ * greeting: PromptModule
48
+ * }>
49
+ * ```
50
+ */
51
+ export type PromptRegistry<T extends PromptNamespace> = {
52
+ readonly [K in keyof T]: T[K] extends PromptModule
53
+ ? T[K]
54
+ : T[K] extends PromptNamespace
55
+ ? PromptRegistry<T[K]>
56
+ : T[K];
57
+ };
58
+
59
+ /**
60
+ * Re-export the Liquid type for consumers that need to type the engine.
61
+ */
62
+ export type { Liquid };
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "target": "ES2024",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "lib": ["ES2024"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "outDir": "./dist",
18
+ "rootDir": ".",
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ }
22
+ },
23
+ "include": ["src"],
24
+ "exclude": ["node_modules", "dist"]
25
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ outDir: "dist/lib",
6
+ format: ["esm"],
7
+ dts: true,
8
+ clean: true,
9
+ unbundle: false,
10
+ platform: "node",
11
+ target: "node22",
12
+ });
@@ -0,0 +1,21 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import { defineConfig } from "vitest/config";
4
+
5
+ export default defineConfig({
6
+ test: {
7
+ include: ["src/**/*.test.{ts,tsx}"],
8
+ passWithNoTests: true,
9
+ coverage: {
10
+ provider: "v8",
11
+ include: ["src/**/*.ts"],
12
+ exclude: ["src/**/*.test.ts", "src/**/*.test-d.ts", "src/**/index.ts", "src/types.ts"],
13
+ reporter: ["text", "lcov"],
14
+ },
15
+ },
16
+ resolve: {
17
+ alias: {
18
+ "@": resolve(__dirname, "./src"),
19
+ },
20
+ },
21
+ });