@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.
- package/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-test$colon$coverage.log +26 -0
- package/.turbo/turbo-test.log +26 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/banner.svg +100 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/clean.ts.html +160 -0
- package/coverage/lcov-report/engine.ts.html +196 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/partials-dir.ts.html +100 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/registry.ts.html +280 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +75 -0
- package/dist/lib/index.d.mts +99 -0
- package/dist/lib/index.d.mts.map +1 -0
- package/dist/lib/index.mjs +117 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/prompts/constraints.prompt +20 -0
- package/dist/prompts/identity.prompt +6 -0
- package/dist/prompts/prompts/constraints.prompt +20 -0
- package/dist/prompts/prompts/identity.prompt +6 -0
- package/dist/prompts/prompts/tools.prompt +14 -0
- package/dist/prompts/tools.prompt +14 -0
- package/docs/cli/commands.md +73 -0
- package/docs/cli/overview.md +73 -0
- package/docs/codegen/overview.md +83 -0
- package/docs/file-format/frontmatter.md +55 -0
- package/docs/file-format/overview.md +67 -0
- package/docs/file-format/partials.md +87 -0
- package/docs/guides/add-partial.md +75 -0
- package/docs/guides/author-prompt.md +70 -0
- package/docs/guides/setup-project.md +75 -0
- package/docs/library/overview.md +64 -0
- package/docs/overview.md +102 -0
- package/docs/troubleshooting.md +37 -0
- package/logo.svg +20 -0
- package/package.json +53 -0
- package/src/clean.test.ts +44 -0
- package/src/clean.ts +25 -0
- package/src/engine.test.ts +44 -0
- package/src/engine.ts +37 -0
- package/src/index.ts +11 -0
- package/src/partials-dir.test.ts +15 -0
- package/src/partials-dir.ts +5 -0
- package/src/prompts/constraints.prompt +20 -0
- package/src/prompts/identity.prompt +6 -0
- package/src/prompts/tools.prompt +14 -0
- package/src/registry.test.ts +69 -0
- package/src/registry.ts +65 -0
- package/src/types.ts +62 -0
- package/tsconfig.json +25 -0
- package/tsdown.config.ts +12 -0
- 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,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,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
|
+
});
|
package/src/registry.ts
ADDED
|
@@ -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
|
+
}
|
package/tsdown.config.ts
ADDED
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|