@funkai/cli 0.2.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.
package/dist/index.mjs CHANGED
@@ -10,21 +10,23 @@ import * as k$2 from "node:readline";
10
10
  import ot from "node:readline";
11
11
  import { ReadStream } from "node:tty";
12
12
  import fs, { existsSync, lstatSync, mkdirSync, promises, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
13
- import path, { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
14
- import { attempt, attemptAsync } from "es-toolkit";
13
+ import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
14
+ import { attempt, attemptAsync, isNil } from "es-toolkit";
15
15
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
16
16
  import { URL as URL$1, fileURLToPath, pathToFileURL } from "node:url";
17
17
  import { homedir } from "node:os";
18
18
  import assert from "node:assert";
19
19
  import v8 from "node:v8";
20
20
  import { readFileSync as readFileSync$1, readdirSync as readdirSync$1, statSync as statSync$1, writeFile as writeFile$1 } from "fs";
21
- import { basename as basename$1, dirname as dirname$1, extname as extname$1, join as join$1, normalize, relative, resolve as resolve$1 } from "path";
21
+ import { basename as basename$1, dirname as dirname$1, extname as extname$1, join as join$1, normalize, relative as relative$1, resolve as resolve$1 } from "path";
22
22
  import { createHash } from "node:crypto";
23
23
  import { parse, stringify } from "yaml";
24
24
  import { notStrictEqual, strictEqual } from "assert";
25
25
  import { format as format$1, inspect as inspect$1 } from "util";
26
26
  import { fileURLToPath as fileURLToPath$1 } from "url";
27
+ import { configSchema } from "@funkai/config";
27
28
  import { PARTIALS_DIR, clean } from "@funkai/prompts/cli";
29
+ import picomatch from "picomatch";
28
30
  import { Liquid } from "liquidjs";
29
31
 
30
32
  //#region \0rolldown/runtime.js
@@ -140,7 +142,7 @@ var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
140
142
  }));
141
143
 
142
144
  //#endregion
143
- //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_h5jt7i6rdfwaknd5di57zdj5zy/node_modules/@kidd-cli/core/dist/tally-KfEitTrZ.js
145
+ //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_ac2wgu34upx3xtuhxg7cvmcnvm/node_modules/@kidd-cli/core/dist/tally-KfEitTrZ.js
144
146
  /**
145
147
  * Format a duration in milliseconds to a human-readable string.
146
148
  *
@@ -2152,7 +2154,7 @@ ${r ? styleText("cyan", x$2) : ""}
2152
2154
  }));
2153
2155
 
2154
2156
  //#endregion
2155
- //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_h5jt7i6rdfwaknd5di57zdj5zy/node_modules/@kidd-cli/core/dist/lib/logger.js
2157
+ //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_ac2wgu34upx3xtuhxg7cvmcnvm/node_modules/@kidd-cli/core/dist/lib/logger.js
2156
2158
  /**
2157
2159
  * Create a new {@link CliLogger} instance.
2158
2160
  *
@@ -2308,7 +2310,7 @@ var init_json$1 = __esmMin((() => {
2308
2310
  }));
2309
2311
 
2310
2312
  //#endregion
2311
- //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_h5jt7i6rdfwaknd5di57zdj5zy/node_modules/@kidd-cli/core/dist/create-context-vWwSL8R5.js
2313
+ //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_ac2wgu34upx3xtuhxg7cvmcnvm/node_modules/@kidd-cli/core/dist/create-context-vWwSL8R5.js
2312
2314
  /**
2313
2315
  * Create a ContextError with an exit code and optional error code.
2314
2316
  *
@@ -12621,7 +12623,7 @@ var init_dist$1 = __esmMin((() => {
12621
12623
  }));
12622
12624
 
12623
12625
  //#endregion
12624
- //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_h5jt7i6rdfwaknd5di57zdj5zy/node_modules/@kidd-cli/core/dist/config-BSyREvk7.js
12626
+ //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_ac2wgu34upx3xtuhxg7cvmcnvm/node_modules/@kidd-cli/core/dist/config-BSyREvk7.js
12625
12627
  /**
12626
12628
  * Determine the config format from a file path's extension.
12627
12629
  *
@@ -15337,7 +15339,7 @@ var init_esm = __esmMin((() => {
15337
15339
  basename: basename$1,
15338
15340
  dirname: dirname$1,
15339
15341
  extname: extname$1,
15340
- relative,
15342
+ relative: relative$1,
15341
15343
  resolve: resolve$1,
15342
15344
  join: join$1
15343
15345
  },
@@ -18295,6 +18297,28 @@ var init_yargs = __esmMin((() => {
18295
18297
  Yargs = YargsFactory(esm_default);
18296
18298
  }));
18297
18299
 
18300
+ //#endregion
18301
+ //#region src/config.ts
18302
+ /**
18303
+ * Extract the typed funkai config from a command context.
18304
+ *
18305
+ * kidd-cli's `Merge<CliConfig, TConfig>` erases augmented keys when
18306
+ * `TConfig` defaults to `Record<string, unknown>`, so we cast here.
18307
+ *
18308
+ * @param ctx - The CLI context.
18309
+ * @returns The typed funkai configuration.
18310
+ *
18311
+ * @example
18312
+ * ```ts
18313
+ * const config = getConfig(ctx);
18314
+ * const promptsConfig = config.prompts;
18315
+ * ```
18316
+ */
18317
+ function getConfig(ctx) {
18318
+ return ctx.config;
18319
+ }
18320
+ var init_config = __esmMin((() => {}));
18321
+
18298
18322
  //#endregion
18299
18323
  //#region src/lib/prompts/codegen.ts
18300
18324
  /**
@@ -18336,6 +18360,11 @@ function formatGroupValue(group) {
18336
18360
  return "undefined";
18337
18361
  }
18338
18362
  /** @private */
18363
+ function formatGroupJsdoc(group) {
18364
+ if (group) return [" *", ` * @group ${group}`];
18365
+ return [];
18366
+ }
18367
+ /** @private */
18339
18368
  function parseGroupSegments(group) {
18340
18369
  if (group) return group.split("/").map(toCamelCase);
18341
18370
  return [];
@@ -18353,22 +18382,68 @@ function generateSchemaExpression(vars) {
18353
18382
  return ` ${v.name}: ${expr},`;
18354
18383
  }).join("\n")}\n})`;
18355
18384
  }
18385
+ /** @private */
18386
+ function formatHeader(sourcePath) {
18387
+ return [
18388
+ "// ─── AUTO-GENERATED ────────────────────────────────────────",
18389
+ `${match(sourcePath).with(void 0, () => "").otherwise((p) => `// Source: ${p}\n`)}// Regenerate: funkai prompts generate`,
18390
+ "// ───────────────────────────────────────────────────────────"
18391
+ ].join("\n");
18392
+ }
18393
+ /**
18394
+ * Derive a unique file slug from group + name.
18395
+ *
18396
+ * Ungrouped prompts use the name alone. Grouped prompts
18397
+ * join group segments and name with hyphens.
18398
+ *
18399
+ * @param name - The prompt name (kebab-case).
18400
+ * @param group - Optional group path (e.g., 'core/agent').
18401
+ * @returns The file slug string.
18402
+ *
18403
+ * @example
18404
+ * ```ts
18405
+ * toFileSlug('system', 'core/agent') // => 'core-agent-system'
18406
+ * toFileSlug('greeting', undefined) // => 'greeting'
18407
+ * ```
18408
+ */
18409
+ function toFileSlug(name, group) {
18410
+ if (group) return `${group.replaceAll("/", "-")}-${name}`;
18411
+ return name;
18412
+ }
18413
+ /**
18414
+ * Derive a unique import name (camelCase) from group + name.
18415
+ *
18416
+ * @param name - The prompt name (kebab-case).
18417
+ * @param group - Optional group path (e.g., 'core/agent').
18418
+ * @returns The camelCase import identifier.
18419
+ *
18420
+ * @example
18421
+ * ```ts
18422
+ * toImportName('system', 'core/agent') // => 'coreAgentSystem'
18423
+ * toImportName('greeting', undefined) // => 'greeting'
18424
+ * ```
18425
+ */
18426
+ function toImportName(name, group) {
18427
+ return toCamelCase(toFileSlug(name, group));
18428
+ }
18356
18429
  /**
18357
18430
  * Generate a per-prompt TypeScript module with a default export.
18358
18431
  *
18359
- * The module contains the Zod schema, inlined template, and
18360
- * `render` / `validate` functions.
18432
+ * The module uses `createPrompt` from `@funkai/prompts` to
18433
+ * encapsulate the Zod schema, inlined template, and render logic.
18434
+ *
18435
+ * @param prompt - The parsed prompt configuration.
18436
+ * @returns The generated TypeScript module source code.
18361
18437
  */
18362
18438
  function generatePromptModule(prompt) {
18363
18439
  const escaped = escapeTemplateLiteral(prompt.template);
18364
18440
  const schemaExpr = generateSchemaExpression(prompt.schema);
18365
18441
  const groupValue = formatGroupValue(prompt.group);
18366
18442
  return [
18367
- HEADER,
18368
- `// Source: ${prompt.sourcePath}`,
18443
+ formatHeader(prompt.sourcePath),
18369
18444
  "",
18370
18445
  "import { z } from 'zod'",
18371
- "import { liquidEngine } from '@funkai/prompts/runtime'",
18446
+ "import { createPrompt } from '@funkai/prompts'",
18372
18447
  "",
18373
18448
  `const schema = ${schemaExpr}`,
18374
18449
  "",
@@ -18376,32 +18451,25 @@ function generatePromptModule(prompt) {
18376
18451
  "",
18377
18452
  `const template = \`${escaped}\``,
18378
18453
  "",
18379
- "export default {",
18380
- ` name: '${prompt.name}' as const,`,
18454
+ "/**",
18455
+ ` * **${prompt.name}** prompt module.`,
18456
+ ...formatGroupJsdoc(prompt.group),
18457
+ " */",
18458
+ "export default createPrompt<Variables>({",
18459
+ ` name: '${prompt.name}',`,
18381
18460
  ` group: ${groupValue},`,
18461
+ " template,",
18382
18462
  " schema,",
18383
- ...match(prompt.schema.length).with(0, () => [
18384
- " render(variables?: undefined): string {",
18385
- " return liquidEngine.parseAndRenderSync(template, {})",
18386
- " },",
18387
- " validate(variables?: undefined): Variables {",
18388
- " return schema.parse(variables ?? {})",
18389
- " },"
18390
- ]).otherwise(() => [
18391
- " render(variables: Variables): string {",
18392
- " return liquidEngine.parseAndRenderSync(template, schema.parse(variables))",
18393
- " },",
18394
- " validate(variables: unknown): Variables {",
18395
- " return schema.parse(variables)",
18396
- " },"
18397
- ]),
18398
- "}",
18463
+ "})",
18399
18464
  ""
18400
18465
  ].join("\n");
18401
18466
  }
18402
18467
  /**
18403
18468
  * Build a nested tree from sorted prompts, grouped by their `group` field.
18404
18469
  *
18470
+ * Leaf values are the unique import name derived from group+name,
18471
+ * so prompts with the same name in different groups do not collide.
18472
+ *
18405
18473
  * @param prompts - Sorted parsed prompts.
18406
18474
  * @returns A tree where leaves are import names and branches are group namespaces.
18407
18475
  * @throws If a prompt name collides with a group namespace at the same level.
@@ -18410,15 +18478,16 @@ function generatePromptModule(prompt) {
18410
18478
  */
18411
18479
  function buildTree(prompts) {
18412
18480
  return prompts.reduce((root, prompt) => {
18413
- const importName = toCamelCase(prompt.name);
18481
+ const leafKey = toCamelCase(prompt.name);
18482
+ const importName = toImportName(prompt.name, prompt.group);
18414
18483
  const target = parseGroupSegments(prompt.group).reduce((current, segment) => {
18415
18484
  const existing = current[segment];
18416
18485
  if (typeof existing === "string") throw new TypeError(`Collision: prompt "${existing}" and group namespace "${segment}" share the same key at the same level.`);
18417
18486
  if (existing === null || existing === void 0) current[segment] = {};
18418
18487
  return current[segment];
18419
18488
  }, root);
18420
- if (typeof target[importName] === "object" && target[importName] !== null) throw new Error(`Collision: prompt "${importName}" conflicts with existing group namespace "${importName}" at the same level.`);
18421
- target[importName] = importName;
18489
+ if (typeof target[leafKey] === "object" && target[leafKey] !== null) throw new Error(`Collision: prompt "${leafKey}" conflicts with existing group namespace "${leafKey}" at the same level.`);
18490
+ target[leafKey] = importName;
18422
18491
  return root;
18423
18492
  }, {});
18424
18493
  }
@@ -18433,7 +18502,10 @@ function buildTree(prompts) {
18433
18502
  */
18434
18503
  function serializeTree(node, indent) {
18435
18504
  const pad = " ".repeat(indent);
18436
- return Object.entries(node).flatMap(([key, value]) => match(typeof value).with("string", () => [`${pad}${key},`]).otherwise(() => [
18505
+ return Object.entries(node).flatMap(([key, value]) => match(typeof value).with("string", () => {
18506
+ if (key === value) return [`${pad}${key},`];
18507
+ return [`${pad}${key}: ${value},`];
18508
+ }).otherwise(() => [
18437
18509
  `${pad}${key}: {`,
18438
18510
  ...serializeTree(value, indent + 1),
18439
18511
  `${pad}},`
@@ -18445,34 +18517,41 @@ function serializeTree(node, indent) {
18445
18517
  *
18446
18518
  * Prompts are organized into a nested object structure based on their
18447
18519
  * `group` field, with each `/`-separated segment becoming a nesting level.
18520
+ *
18521
+ * @param prompts - Sorted parsed prompts to include in the registry.
18522
+ * @returns The generated TypeScript source for the registry index module.
18523
+ *
18524
+ * @example
18525
+ * ```ts
18526
+ * const source = generateRegistry([
18527
+ * { name: 'system', group: 'core/agent', schema: [], template: '...', sourcePath: 'prompts/system.prompt' },
18528
+ * ])
18529
+ * writeFileSync('index.ts', source)
18530
+ * ```
18448
18531
  */
18449
18532
  function generateRegistry(prompts) {
18450
- const sorted = [...prompts].toSorted((a, b) => a.name.localeCompare(b.name));
18533
+ const sorted = [...prompts].toSorted((a, b) => {
18534
+ const slugA = toFileSlug(a.name, a.group);
18535
+ const slugB = toFileSlug(b.name, b.group);
18536
+ return slugA.localeCompare(slugB);
18537
+ });
18538
+ const imports = sorted.map((p) => {
18539
+ return `import ${toImportName(p.name, p.group)} from './${toFileSlug(p.name, p.group)}.js'`;
18540
+ }).join("\n");
18541
+ const treeLines = serializeTree(buildTree(sorted), 1);
18451
18542
  return [
18452
- HEADER,
18543
+ formatHeader(),
18453
18544
  "",
18454
18545
  "import { createPromptRegistry } from '@funkai/prompts'",
18455
- sorted.map((p) => `import ${toCamelCase(p.name)} from './${p.name}.js'`).join("\n"),
18546
+ imports,
18456
18547
  "",
18457
18548
  "export const prompts = createPromptRegistry({",
18458
- ...serializeTree(buildTree(sorted), 1),
18549
+ ...treeLines,
18459
18550
  "})",
18460
18551
  ""
18461
18552
  ].join("\n");
18462
18553
  }
18463
- var HEADER;
18464
- var init_codegen = __esmMin((() => {
18465
- HEADER = [
18466
- "/*",
18467
- "|==========================================================================",
18468
- "| AUTO-GENERATED — DO NOT EDIT",
18469
- "|==========================================================================",
18470
- "|",
18471
- "| Run `funkai prompts generate` to regenerate.",
18472
- "|",
18473
- "*/"
18474
- ].join("\n");
18475
- }));
18554
+ var init_codegen = __esmMin((() => {}));
18476
18555
 
18477
18556
  //#endregion
18478
18557
  //#region src/lib/prompts/lint.ts
@@ -18483,13 +18562,20 @@ var init_codegen = __esmMin((() => {
18483
18562
  * - **Error**: template uses a variable NOT declared in the schema (undefined var).
18484
18563
  * - **Warn**: schema declares a variable NOT used in the template (unused var).
18485
18564
  *
18486
- * @param name - Prompt name (for error messages).
18487
- * @param filePath - Source file path (for error messages).
18488
- * @param schemaVars - Variables declared in frontmatter schema.
18489
- * @param templateVars - Variables extracted from the template body.
18565
+ * @param params - Lint prompt parameters.
18490
18566
  * @returns Lint result with diagnostics.
18567
+ *
18568
+ * @example
18569
+ * ```ts
18570
+ * const result = lintPrompt({
18571
+ * name: 'greeting',
18572
+ * filePath: 'prompts/greeting.prompt',
18573
+ * schemaVars: [{ name: 'name', type: 'string', required: true }],
18574
+ * templateVars: ['name'],
18575
+ * });
18576
+ * ```
18491
18577
  */
18492
- function lintPrompt(name, filePath, schemaVars, templateVars) {
18578
+ function lintPrompt({ name, filePath, schemaVars, templateVars }) {
18493
18579
  const declared = new Set(schemaVars.map((v) => v.name));
18494
18580
  const used = new Set(templateVars);
18495
18581
  const undeclaredErrors = [...used].filter((varName) => !declared.has(varName)).map((varName) => ({
@@ -18573,11 +18659,12 @@ function parseParamsOrEmpty(raw, partialName) {
18573
18659
  */
18574
18660
  function parseParams(raw, partialName) {
18575
18661
  const literalMatches = [...raw.matchAll(LITERAL_PARAM_RE)];
18576
- const allParamNames = [...raw.matchAll(/(\w+)\s*:/g)].map((m) => m[1]);
18662
+ const allParamNames = [...raw.matchAll(/(\w+)\s*:/g)].map(([, m1]) => m1);
18577
18663
  return Object.fromEntries(allParamNames.map((name) => {
18578
- const literal = literalMatches.find((m) => m[1] === name);
18664
+ const literal = literalMatches.find(([, m1]) => m1 === name);
18579
18665
  if (!literal) throw new Error(`Cannot flatten {% render '${partialName}' %}: parameter "${name}" uses a variable reference. Only literal string values are supported at codegen time.`);
18580
- return [name, literal[2]];
18666
+ const { 2: literalValue } = literal;
18667
+ return [name, literalValue];
18581
18668
  }));
18582
18669
  }
18583
18670
  /** @private */
@@ -18605,10 +18692,12 @@ function renderPartial(engine, tag) {
18605
18692
  */
18606
18693
  function parseRenderTags(template) {
18607
18694
  return [...template.matchAll(RENDER_TAG_RE)].map((m) => {
18608
- const params = parseParamsOrEmpty((m[2] ?? "").trim(), m[1]);
18695
+ const [, partialName] = m;
18696
+ if (partialName === void 0) throw new Error("Malformed render tag: missing partial name");
18697
+ const params = parseParamsOrEmpty((m[2] ?? "").trim(), partialName);
18609
18698
  return {
18610
18699
  fullMatch: m[0],
18611
- partialName: m[1],
18700
+ partialName,
18612
18701
  params
18613
18702
  };
18614
18703
  });
@@ -18687,17 +18776,22 @@ function parseYamlContent(yaml, filePath) {
18687
18776
  function parseFrontmatter({ content, filePath }) {
18688
18777
  const fmMatch = content.match(FRONTMATTER_RE);
18689
18778
  if (!fmMatch) throw new Error(`No frontmatter found in ${filePath}`);
18690
- const parsed = parseYamlContent(fmMatch[1], filePath);
18779
+ const [, fmContent] = fmMatch;
18780
+ if (fmContent === void 0) throw new Error(`No frontmatter content found in ${filePath}`);
18781
+ const parsed = parseYamlContent(fmContent, filePath);
18691
18782
  if (!parsed || typeof parsed !== "object") throw new Error(`Frontmatter is not a valid object in ${filePath}`);
18692
18783
  const { name } = parsed;
18693
18784
  if (typeof name !== "string" || name.length === 0) throw new Error(`Missing or empty "name" in frontmatter: ${filePath}`);
18694
18785
  if (!NAME_RE.test(name)) throw new Error(`Invalid prompt name "${name}" in ${filePath}. Names must be lowercase alphanumeric with hyphens only.`);
18695
- return {
18786
+ const group = parseGroup(parsed["group"], filePath);
18787
+ const version = parseVersion(parsed["version"]);
18788
+ const result = {
18696
18789
  name,
18697
- group: parseGroup(parsed.group, filePath),
18698
- version: parseVersion(parsed.version),
18699
- schema: parseSchemaBlock(parsed.schema, filePath)
18790
+ schema: parseSchemaBlock(parsed["schema"], filePath)
18700
18791
  };
18792
+ if (group !== void 0) result.group = group;
18793
+ if (version !== void 0) result.version = version;
18794
+ return result;
18701
18795
  }
18702
18796
  /** @private */
18703
18797
  function stringOrDefault(value, fallback) {
@@ -18743,9 +18837,9 @@ function parseSchemaBlock(raw, filePath) {
18743
18837
  })).with(P.when((v) => typeof v === "object" && v !== null && !Array.isArray(v)), (def) => {
18744
18838
  return {
18745
18839
  name: varName,
18746
- type: stringOrDefault(def.type, "string"),
18747
- required: def.required !== false,
18748
- description: stringOrUndefined(def.description)
18840
+ type: stringOrDefault(def["type"], "string"),
18841
+ required: def["required"] !== false,
18842
+ description: stringOrUndefined(def["description"])
18749
18843
  };
18750
18844
  }).otherwise(() => {
18751
18845
  throw new Error(`Invalid schema definition for "${varName}" in ${filePath}. Expected a type string or an object with { type, required?, description? }.`);
@@ -18770,8 +18864,10 @@ function extractName(content) {
18770
18864
  const fmMatch = content.match(FRONTMATTER_RE);
18771
18865
  if (!fmMatch) return;
18772
18866
  try {
18773
- const parsed = parse(fmMatch[1]);
18774
- if (parsed !== null && parsed !== void 0 && typeof parsed.name === "string") return parsed.name;
18867
+ const [, fmContent] = fmMatch;
18868
+ if (fmContent === void 0) return;
18869
+ const parsed = parse(fmContent);
18870
+ if (parsed !== null && parsed !== void 0 && typeof parsed["name"] === "string") return parsed["name"];
18775
18871
  return;
18776
18872
  } catch {
18777
18873
  return;
@@ -18791,6 +18887,28 @@ function deriveNameFromPath(filePath) {
18791
18887
  return stem;
18792
18888
  }
18793
18889
  /**
18890
+ * Extract the static base directory from a glob pattern.
18891
+ *
18892
+ * Returns the longest directory prefix before any glob characters
18893
+ * (`*`, `?`, `{`, `[`). Falls back to `'.'` if the pattern starts
18894
+ * with a glob character.
18895
+ *
18896
+ * @private
18897
+ */
18898
+ function extractBaseDir(pattern) {
18899
+ const globChars = new Set([
18900
+ "*",
18901
+ "?",
18902
+ "{",
18903
+ "["
18904
+ ]);
18905
+ const parts = pattern.split("/");
18906
+ const firstGlobIndex = parts.findIndex((part) => [...part].some((ch) => globChars.has(ch)));
18907
+ const staticParts = match(firstGlobIndex).with(-1, () => parts).otherwise(() => parts.slice(0, firstGlobIndex));
18908
+ if (staticParts.length === 0) return ".";
18909
+ return staticParts.join("/");
18910
+ }
18911
+ /**
18794
18912
  * Recursively scan a directory for `.prompt` files.
18795
18913
  *
18796
18914
  * @private
@@ -18815,21 +18933,29 @@ function scanDirectory(dir, depth) {
18815
18933
  });
18816
18934
  }
18817
18935
  /**
18818
- * Discover all `.prompt` files from the given root directories.
18936
+ * Discover all `.prompt` files matching the given include/exclude patterns.
18937
+ *
18938
+ * Extracts base directories from the include patterns, scans them
18939
+ * recursively, then filters results through picomatch.
18819
18940
  *
18820
- * @param roots - Directories to scan recursively.
18821
- * @returns Sorted, deduplicated list of discovered prompts.
18822
- * @throws If duplicate prompt names are found across roots.
18941
+ * Name uniqueness is **not** enforced here — prompts with the same name
18942
+ * are allowed as long as they belong to different groups. Uniqueness
18943
+ * is validated downstream in the pipeline after frontmatter parsing,
18944
+ * where group information is available.
18945
+ *
18946
+ * @param options - Include and exclude glob patterns.
18947
+ * @returns Sorted list of discovered prompts.
18823
18948
  */
18824
- function discoverPrompts(roots) {
18825
- const all = roots.flatMap((root) => scanDirectory(resolve(root), 0));
18826
- const duplicate = [...Map.groupBy(all, (prompt) => prompt.name).entries()].find(([, prompts]) => prompts.length > 1);
18827
- if (duplicate) {
18828
- const [name, prompts] = duplicate;
18829
- const paths = prompts.map((p) => p.filePath).join("\n ");
18830
- throw new Error(`Duplicate prompt name "${name}" found in:\n ${paths}`);
18831
- }
18832
- return all.toSorted((a, b) => a.name.localeCompare(b.name));
18949
+ function discoverPrompts(options) {
18950
+ const { includes, excludes = [] } = options;
18951
+ const all = [...new Set(includes.map((pattern) => resolve(extractBaseDir(pattern))))].flatMap((dir) => scanDirectory(dir, 0));
18952
+ const isIncluded = picomatch(includes);
18953
+ const isExcluded = picomatch(excludes);
18954
+ const filtered = all.filter((prompt) => {
18955
+ const matchPath = relative(process.cwd(), prompt.filePath).replaceAll("\\", "/");
18956
+ return isIncluded(matchPath) && !isExcluded(matchPath);
18957
+ });
18958
+ return [...new Map(filtered.map((prompt) => [prompt.filePath, prompt])).values()].toSorted((a, b) => a.name.localeCompare(b.name));
18833
18959
  }
18834
18960
  var MAX_DEPTH, PROMPT_EXT;
18835
18961
  var init_paths = __esmMin((() => {
@@ -18841,6 +18967,44 @@ var init_paths = __esmMin((() => {
18841
18967
  //#endregion
18842
18968
  //#region src/lib/prompts/pipeline.ts
18843
18969
  /**
18970
+ * Validate that no two prompts share the same group+name combination.
18971
+ *
18972
+ * @param prompts - Parsed prompts with group and name fields.
18973
+ * @throws If duplicate group+name combinations are found.
18974
+ *
18975
+ * @private
18976
+ */
18977
+ function validateUniqueness(prompts) {
18978
+ const duplicate = [...Map.groupBy(prompts, (p) => toFileSlug(p.name, p.group)).entries()].find(([, entries]) => entries.length > 1);
18979
+ if (duplicate) {
18980
+ const [slug, entries] = duplicate;
18981
+ const paths = entries.map((p) => p.sourcePath).join("\n ");
18982
+ throw new Error(`Duplicate prompt "${slug}" (group+name) found in:\n ${paths}`);
18983
+ }
18984
+ }
18985
+ /**
18986
+ * Resolve a prompt's group from config-defined group patterns.
18987
+ *
18988
+ * Matches the prompt's file path against each group's `includes`/`excludes`
18989
+ * patterns. First matching group wins.
18990
+ *
18991
+ * @param filePath - Absolute path to the prompt file.
18992
+ * @param groups - Config-defined group definitions.
18993
+ * @returns The matching group name, or undefined if no match.
18994
+ *
18995
+ * @private
18996
+ */
18997
+ function resolveGroupFromConfig(filePath, groups) {
18998
+ const matchPath = relative(process.cwd(), filePath).replaceAll("\\", "/");
18999
+ const matched = groups.find((group) => {
19000
+ const isIncluded = picomatch(group.includes);
19001
+ const isExcluded = picomatch(group.excludes ?? []);
19002
+ return isIncluded(matchPath) && !isExcluded(matchPath);
19003
+ });
19004
+ if (isNil(matched)) return;
19005
+ return matched.name;
19006
+ }
19007
+ /**
18844
19008
  * Resolve the list of partial directories to search.
18845
19009
  *
18846
19010
  * @private
@@ -18858,7 +19022,9 @@ function resolvePartialsDirs(customDir) {
18858
19022
  * @returns Lint results for all discovered prompts.
18859
19023
  */
18860
19024
  function runLintPipeline(options) {
18861
- const discovered = discoverPrompts([...options.roots]);
19025
+ const discoverLintOptions = { includes: [...options.includes] };
19026
+ if (options.excludes !== void 0) discoverLintOptions.excludes = [...options.excludes];
19027
+ const discovered = discoverPrompts(discoverLintOptions);
18862
19028
  const partialsDirs = resolvePartialsDirs(resolve(options.partials ?? ".prompts/partials"));
18863
19029
  const results = discovered.map((d) => {
18864
19030
  const raw = readFileSync(d.filePath, "utf8");
@@ -18870,7 +19036,12 @@ function runLintPipeline(options) {
18870
19036
  template: clean(raw),
18871
19037
  partialsDirs
18872
19038
  }));
18873
- return lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars);
19039
+ return lintPrompt({
19040
+ name: frontmatter.name,
19041
+ filePath: d.filePath,
19042
+ schemaVars: frontmatter.schema,
19043
+ templateVars
19044
+ });
18874
19045
  });
18875
19046
  return {
18876
19047
  discovered: discovered.length,
@@ -18886,8 +19057,11 @@ function runLintPipeline(options) {
18886
19057
  * @returns Parsed prompts ready for code generation, along with lint results.
18887
19058
  */
18888
19059
  function runGeneratePipeline(options) {
18889
- const discovered = discoverPrompts([...options.roots]);
19060
+ const discoverGenerateOptions = { includes: [...options.includes] };
19061
+ if (options.excludes !== void 0) discoverGenerateOptions.excludes = [...options.excludes];
19062
+ const discovered = discoverPrompts(discoverGenerateOptions);
18890
19063
  const partialsDirs = resolvePartialsDirs(resolve(options.partials ?? resolve(options.out, "../partials")));
19064
+ const configGroups = options.groups ?? [];
18891
19065
  const processed = discovered.map((d) => {
18892
19066
  const raw = readFileSync(d.filePath, "utf8");
18893
19067
  const frontmatter = parseFrontmatter({
@@ -18899,24 +19073,34 @@ function runGeneratePipeline(options) {
18899
19073
  partialsDirs
18900
19074
  });
18901
19075
  const templateVars = extractVariables(template);
19076
+ const group = frontmatter.group ?? resolveGroupFromConfig(d.filePath, configGroups);
19077
+ const promptObj = {
19078
+ name: frontmatter.name,
19079
+ schema: frontmatter.schema,
19080
+ template,
19081
+ sourcePath: d.filePath
19082
+ };
19083
+ if (group !== void 0) promptObj.group = group;
18902
19084
  return {
18903
- lintResult: lintPrompt(frontmatter.name, d.filePath, frontmatter.schema, templateVars),
18904
- prompt: {
19085
+ lintResult: lintPrompt({
18905
19086
  name: frontmatter.name,
18906
- group: frontmatter.group,
18907
- schema: frontmatter.schema,
18908
- template,
18909
- sourcePath: d.filePath
18910
- }
19087
+ filePath: d.filePath,
19088
+ schemaVars: frontmatter.schema,
19089
+ templateVars
19090
+ }),
19091
+ prompt: promptObj
18911
19092
  };
18912
19093
  });
19094
+ const prompts = processed.map((p) => p.prompt);
19095
+ validateUniqueness(prompts);
18913
19096
  return {
18914
19097
  discovered: discovered.length,
18915
19098
  lintResults: processed.map((p) => p.lintResult),
18916
- prompts: processed.map((p) => p.prompt)
19099
+ prompts
18917
19100
  };
18918
19101
  }
18919
19102
  var init_pipeline = __esmMin((() => {
19103
+ init_codegen();
18920
19104
  init_extract_variables();
18921
19105
  init_flatten();
18922
19106
  init_frontmatter();
@@ -18927,17 +19111,44 @@ var init_pipeline = __esmMin((() => {
18927
19111
  //#endregion
18928
19112
  //#region src/commands/prompts/generate.ts
18929
19113
  /**
18930
- * Shared handler for prompts code generation.
19114
+ * Resolve generate args by merging CLI flags with config defaults.
18931
19115
  *
18932
- * @param params - Handler context with args, logger, and fail callback.
19116
+ * @param args - CLI arguments (take precedence).
19117
+ * @param config - Prompts config from funkai.config.ts (fallback).
19118
+ * @param fail - Error handler for missing required values.
19119
+ * @returns Resolved args with required fields guaranteed.
18933
19120
  */
18934
- function handleGenerate({ args, logger, fail }) {
18935
- const { out, roots, partials, silent } = args;
18936
- const { discovered, lintResults, prompts } = runGeneratePipeline({
18937
- roots,
19121
+ function resolveGenerateArgs(args, config, fail) {
19122
+ const out = args.out ?? (config && config.out);
19123
+ const includes = args.includes ?? (config && config.includes) ?? ["./**"];
19124
+ const excludes = (config && config.excludes) ?? [];
19125
+ const partials = args.partials ?? (config && config.partials);
19126
+ if (!out) fail("Missing --out flag. Provide it via CLI or set prompts.out in funkai.config.ts.");
19127
+ const resolved = {
18938
19128
  out,
18939
- partials
18940
- });
19129
+ includes,
19130
+ excludes,
19131
+ silent: args.silent
19132
+ };
19133
+ if (partials !== void 0) resolved.partials = partials;
19134
+ return resolved;
19135
+ }
19136
+ /**
19137
+ * Shared handler for prompts code generation.
19138
+ *
19139
+ * @param params - Handler context with args, config, logger, and fail callback.
19140
+ */
19141
+ function handleGenerate({ args, config, logger, fail }) {
19142
+ const { out, includes, excludes, partials, silent } = resolveGenerateArgs(args, config, fail);
19143
+ const configGroups = config && config.groups;
19144
+ const pipelineOptions = {
19145
+ includes,
19146
+ excludes,
19147
+ out
19148
+ };
19149
+ if (partials !== void 0) pipelineOptions.partials = partials;
19150
+ if (configGroups !== void 0) pipelineOptions.groups = configGroups;
19151
+ const { discovered, lintResults, prompts } = runGeneratePipeline(pipelineOptions);
18941
19152
  if (!silent) logger.info(`Found ${discovered} prompt(s)`);
18942
19153
  if (!silent) for (const prompt of prompts) {
18943
19154
  const varList = formatVarList(prompt.schema);
@@ -18951,7 +19162,7 @@ function handleGenerate({ args, logger, fail }) {
18951
19162
  mkdirSync(outDir, { recursive: true });
18952
19163
  for (const prompt of prompts) {
18953
19164
  const content = generatePromptModule(prompt);
18954
- writeFileSync(resolve(outDir, `${prompt.name}.ts`), content, "utf8");
19165
+ writeFileSync(resolve(outDir, `${toFileSlug(prompt.name, prompt.group)}.ts`), content, "utf8");
18955
19166
  }
18956
19167
  const registryContent = generateRegistry(prompts);
18957
19168
  writeFileSync(resolve(outDir, "index.ts"), registryContent, "utf8");
@@ -18965,12 +19176,13 @@ function formatVarList(schema) {
18965
19176
  var generateArgs, generate_default$1;
18966
19177
  var init_generate$1 = __esmMin((() => {
18967
19178
  init_dist();
19179
+ init_config();
18968
19180
  init_codegen();
18969
19181
  init_lint$1();
18970
19182
  init_pipeline();
18971
19183
  generateArgs = z.object({
18972
- out: z.string().describe("Output directory for generated files"),
18973
- roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
19184
+ out: z.string().optional().describe("Output directory for generated files"),
19185
+ includes: z.array(z.string()).optional().describe("Glob patterns to scan for .prompt files"),
18974
19186
  partials: z.string().optional().describe("Custom partials directory"),
18975
19187
  silent: z.boolean().default(false).describe("Suppress output except errors")
18976
19188
  });
@@ -18978,8 +19190,14 @@ var init_generate$1 = __esmMin((() => {
18978
19190
  description: "Generate TypeScript modules from .prompt files",
18979
19191
  options: generateArgs,
18980
19192
  handler(ctx) {
19193
+ const config = getConfig(ctx);
19194
+ const generateArgs2 = { silent: ctx.args.silent };
19195
+ if (ctx.args.out !== void 0) generateArgs2.out = ctx.args.out;
19196
+ if (ctx.args.includes !== void 0) generateArgs2.includes = ctx.args.includes;
19197
+ if (ctx.args.partials !== void 0) generateArgs2.partials = ctx.args.partials;
18981
19198
  handleGenerate({
18982
- args: ctx.args,
19199
+ args: generateArgs2,
19200
+ config: config.prompts,
18983
19201
  logger: ctx.logger,
18984
19202
  fail: ctx.fail
18985
19203
  });
@@ -18993,14 +19211,21 @@ var generate_default;
18993
19211
  var init_generate = __esmMin((() => {
18994
19212
  init_dist();
18995
19213
  init_generate$1();
19214
+ init_config();
18996
19215
  generate_default = command({
18997
19216
  description: "Run all code generation across the funkai SDK",
18998
19217
  options: generateArgs,
18999
19218
  handler(ctx) {
19000
19219
  const { silent } = ctx.args;
19220
+ const config = getConfig(ctx);
19001
19221
  if (!silent) ctx.logger.info("Running prompts code generation...");
19222
+ const generateHandleArgs = { silent: ctx.args.silent };
19223
+ if (ctx.args.out !== void 0) generateHandleArgs.out = ctx.args.out;
19224
+ if (ctx.args.includes !== void 0) generateHandleArgs.includes = ctx.args.includes;
19225
+ if (ctx.args.partials !== void 0) generateHandleArgs.partials = ctx.args.partials;
19002
19226
  handleGenerate({
19003
- args: ctx.args,
19227
+ args: generateHandleArgs,
19228
+ config: config.prompts,
19004
19229
  logger: ctx.logger,
19005
19230
  fail: ctx.fail
19006
19231
  });
@@ -19009,18 +19234,245 @@ var init_generate = __esmMin((() => {
19009
19234
  }));
19010
19235
 
19011
19236
  //#endregion
19012
- //#region src/commands/setup.ts
19013
- var setup_default$1;
19237
+ //#region src/commands/prompts/setup.ts
19238
+ /**
19239
+ * Shared prompts setup logic used by both `funkai prompts setup` and `funkai setup`.
19240
+ *
19241
+ * @param ctx - The CLI context with prompts and logger.
19242
+ */
19243
+ async function setupPrompts(ctx) {
19244
+ if (await ctx.prompts.confirm({
19245
+ message: "Configure VSCode to treat .prompt files as Markdown with Liquid syntax?",
19246
+ initialValue: true
19247
+ })) {
19248
+ const vscodeDir = resolve(VSCODE_DIR);
19249
+ mkdirSync(vscodeDir, { recursive: true });
19250
+ const settingsPath = resolve(vscodeDir, SETTINGS_FILE);
19251
+ const settings = readJsonFile(settingsPath);
19252
+ const updatedSettings = {
19253
+ ...settings,
19254
+ "files.associations": {
19255
+ ...settings["files.associations"] ?? {},
19256
+ "*.prompt": "markdown"
19257
+ },
19258
+ "liquid.engine": "standard"
19259
+ };
19260
+ writeFileSync(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
19261
+ ctx.logger.success(`Updated ${settingsPath}`);
19262
+ }
19263
+ if (await ctx.prompts.confirm({
19264
+ message: "Add Shopify Liquid extension to VSCode recommendations?",
19265
+ initialValue: true
19266
+ })) {
19267
+ const vscodeDir = resolve(VSCODE_DIR);
19268
+ mkdirSync(vscodeDir, { recursive: true });
19269
+ const extensionsPath = resolve(vscodeDir, EXTENSIONS_FILE);
19270
+ const extensions = readJsonFile(extensionsPath);
19271
+ const recommendations = ensureRecommendation(extensions["recommendations"] ?? [], "sissel.shopify-liquid");
19272
+ const updatedExtensions = {
19273
+ ...extensions,
19274
+ recommendations
19275
+ };
19276
+ writeFileSync(extensionsPath, `${JSON.stringify(updatedExtensions, null, 2)}\n`, "utf8");
19277
+ ctx.logger.success(`Updated ${extensionsPath}`);
19278
+ }
19279
+ if (await ctx.prompts.confirm({
19280
+ message: "Add .prompts/client/ to .gitignore? (generated client should not be committed)",
19281
+ initialValue: true
19282
+ })) {
19283
+ const gitignorePath = resolve(GITIGNORE_FILE);
19284
+ const existing = readFileOrEmpty(gitignorePath);
19285
+ if (existing.includes(GITIGNORE_ENTRY)) ctx.logger.info(`${GITIGNORE_ENTRY} already in ${gitignorePath}`);
19286
+ else {
19287
+ writeFileSync(gitignorePath, `${existing}${`${trailingSeparator(existing)}\n# Generated prompt client (created by \`funkai prompts generate\`)\n${GITIGNORE_ENTRY}\n`}`, "utf8");
19288
+ ctx.logger.success(`Added ${GITIGNORE_ENTRY} to ${gitignorePath}`);
19289
+ }
19290
+ }
19291
+ if (await ctx.prompts.confirm({
19292
+ message: "Add ~prompts path alias to tsconfig.json?",
19293
+ initialValue: true
19294
+ })) {
19295
+ const tsconfigPath = resolve(TSCONFIG_FILE);
19296
+ const tsconfig = readJsonFile(tsconfigPath);
19297
+ const compilerOptions = tsconfig["compilerOptions"] ?? {};
19298
+ const existingPaths = compilerOptions["paths"] ?? {};
19299
+ if (existingPaths[PROMPTS_ALIAS]) ctx.logger.info(`${PROMPTS_ALIAS} alias already in ${tsconfigPath}`);
19300
+ else {
19301
+ const updatedTsconfig = {
19302
+ ...tsconfig,
19303
+ compilerOptions: {
19304
+ ...compilerOptions,
19305
+ paths: {
19306
+ ...existingPaths,
19307
+ [PROMPTS_ALIAS]: [PROMPTS_ALIAS_PATH]
19308
+ }
19309
+ }
19310
+ };
19311
+ writeFileSync(tsconfigPath, `${JSON.stringify(updatedTsconfig, null, 2)}\n`, "utf8");
19312
+ ctx.logger.success(`Added ${PROMPTS_ALIAS} alias to ${tsconfigPath}`);
19313
+ }
19314
+ }
19315
+ }
19316
+ /** @private */
19317
+ function errorMessage(error) {
19318
+ if (error instanceof Error) return error.message;
19319
+ return String(error);
19320
+ }
19321
+ /** @private */
19322
+ function ensureRecommendation(current, id) {
19323
+ if (current.includes(id)) return [...current];
19324
+ return [...current, id];
19325
+ }
19326
+ /** @private */
19327
+ function readFileOrEmpty(filePath) {
19328
+ if (existsSync(filePath)) return readFileSync(filePath, "utf8");
19329
+ return "";
19330
+ }
19331
+ /** @private */
19332
+ function trailingSeparator(content) {
19333
+ if (content.length > 0 && !content.endsWith("\n")) return "\n";
19334
+ return "";
19335
+ }
19336
+ /**
19337
+ * Read a JSON file, returning an empty object if it doesn't exist.
19338
+ * Throws if the file exists but contains invalid JSON, preventing
19339
+ * silent data loss from overwriting malformed config files.
19340
+ *
19341
+ * @private
19342
+ */
19343
+ function readJsonFile(filePath) {
19344
+ if (!existsSync(filePath)) return {};
19345
+ const content = readFileSync(filePath, "utf8");
19346
+ try {
19347
+ return JSON.parse(content);
19348
+ } catch (error) {
19349
+ throw new Error(`Failed to parse ${filePath}: ${errorMessage(error)}. Fix the JSON syntax or remove the file before running setup.`, { cause: error });
19350
+ }
19351
+ }
19352
+ var VSCODE_DIR, SETTINGS_FILE, EXTENSIONS_FILE, GITIGNORE_FILE, TSCONFIG_FILE, GITIGNORE_ENTRY, PROMPTS_ALIAS, PROMPTS_ALIAS_PATH, setup_default$1;
19014
19353
  var init_setup$1 = __esmMin((() => {
19015
19354
  init_dist();
19355
+ VSCODE_DIR = ".vscode";
19356
+ SETTINGS_FILE = "settings.json";
19357
+ EXTENSIONS_FILE = "extensions.json";
19358
+ GITIGNORE_FILE = ".gitignore";
19359
+ TSCONFIG_FILE = "tsconfig.json";
19360
+ GITIGNORE_ENTRY = ".prompts/client/";
19361
+ PROMPTS_ALIAS = "~prompts";
19362
+ PROMPTS_ALIAS_PATH = "./.prompts/client/index.ts";
19016
19363
  setup_default$1 = command({
19364
+ description: "Configure VSCode IDE settings for .prompt files",
19365
+ async handler(ctx) {
19366
+ ctx.logger.intro("Prompt SDK — Project Setup");
19367
+ await setupPrompts(ctx);
19368
+ ctx.logger.outro("Prompts setup complete.");
19369
+ }
19370
+ });
19371
+ }));
19372
+
19373
+ //#endregion
19374
+ //#region src/commands/setup.ts
19375
+ /**
19376
+ * Gather prompt include patterns and output directory from the user.
19377
+ *
19378
+ * @private
19379
+ * @param ctx - CLI context for prompts.
19380
+ * @param hasPrompts - Whether the prompts domain is selected.
19381
+ * @returns The resolved prompt settings.
19382
+ */
19383
+ async function resolvePromptSettings(ctx, hasPrompts) {
19384
+ if (!hasPrompts) return {
19385
+ includes: ["src/prompts/**"],
19386
+ out: ".prompts/client"
19387
+ };
19388
+ const includesInput = await ctx.prompts.text({
19389
+ message: "Prompt include patterns (comma-separated)",
19390
+ defaultValue: "src/prompts/**",
19391
+ placeholder: "src/prompts/**"
19392
+ });
19393
+ const out = await ctx.prompts.text({
19394
+ message: "Output directory for generated prompt modules",
19395
+ defaultValue: ".prompts/client",
19396
+ placeholder: ".prompts/client"
19397
+ });
19398
+ return {
19399
+ includes: includesInput.split(",").map((r) => r.trim()),
19400
+ out
19401
+ };
19402
+ }
19403
+ /** @private */
19404
+ function buildConfigTemplate({ hasPrompts, hasAgents, includes, out }) {
19405
+ if (hasPrompts && hasAgents) return buildCustomTemplate(includes, out, true);
19406
+ if (hasPrompts) return buildCustomTemplate(includes, out, false);
19407
+ return CONFIG_TEMPLATE_AGENTS_ONLY;
19408
+ }
19409
+ /** @private */
19410
+ function buildCustomTemplate(includes, out, includeAgents) {
19411
+ return `import { defineConfig } from "@funkai/config";
19412
+
19413
+ export default defineConfig({
19414
+ prompts: {
19415
+ includes: [${includes.map((r) => `"${r}"`).join(", ")}],
19416
+ out: "${out}",
19417
+ },${match(includeAgents).with(true, () => "\n agents: {},\n").with(false, () => "\n").exhaustive()}});
19418
+ `;
19419
+ }
19420
+ var CONFIG_TEMPLATE_AGENTS_ONLY, setup_default;
19421
+ var init_setup = __esmMin((() => {
19422
+ init_dist();
19423
+ init_setup$1();
19424
+ CONFIG_TEMPLATE_AGENTS_ONLY = `import { defineConfig } from "@funkai/config";
19425
+
19426
+ export default defineConfig({
19427
+ agents: {},
19428
+ });
19429
+ `;
19430
+ setup_default = command({
19017
19431
  description: "Set up your project for the funkai SDK",
19018
19432
  async handler(ctx) {
19019
19433
  ctx.logger.intro("funkai — Project Setup");
19020
- ctx.logger.info("Run domain-specific setup commands:");
19021
- ctx.logger.step("funkai prompts setup — Configure IDE and project for .prompt files");
19022
- ctx.logger.step("funkai agents setup — (coming soon)");
19023
- ctx.logger.outro("Choose the setup command for your domain.");
19434
+ const domains = await ctx.prompts.multiselect({
19435
+ message: "Which domains do you want to set up?",
19436
+ options: [{
19437
+ value: "prompts",
19438
+ label: "Prompts",
19439
+ hint: "LiquidJS templating, codegen, IDE integration"
19440
+ }, {
19441
+ value: "agents",
19442
+ label: "Agents",
19443
+ hint: "Agent scaffolding and configuration"
19444
+ }],
19445
+ initialValues: ["prompts"],
19446
+ required: true
19447
+ });
19448
+ const hasPrompts = domains.includes("prompts");
19449
+ const hasAgents = domains.includes("agents");
19450
+ if (await ctx.prompts.confirm({
19451
+ message: "Create funkai.config.ts?",
19452
+ initialValue: true
19453
+ })) {
19454
+ const { includes, out } = await resolvePromptSettings(ctx, hasPrompts);
19455
+ const template = buildConfigTemplate({
19456
+ hasPrompts,
19457
+ hasAgents,
19458
+ includes,
19459
+ out
19460
+ });
19461
+ const configPath = resolve("funkai.config.ts");
19462
+ writeFileSync(configPath, template, "utf8");
19463
+ ctx.logger.success(`Created ${configPath}`);
19464
+ }
19465
+ if (hasPrompts) {
19466
+ ctx.logger.info("");
19467
+ ctx.logger.info("Configuring Prompts...");
19468
+ await setupPrompts(ctx);
19469
+ }
19470
+ if (hasAgents) {
19471
+ ctx.logger.info("");
19472
+ ctx.logger.info("Agents configuration is not yet available.");
19473
+ ctx.logger.info("The agents section has been added to your config for future use.");
19474
+ }
19475
+ ctx.logger.outro("Project setup complete.");
19024
19476
  }
19025
19477
  });
19026
19478
  }));
@@ -19028,16 +19480,38 @@ var init_setup$1 = __esmMin((() => {
19028
19480
  //#endregion
19029
19481
  //#region src/commands/prompts/lint.ts
19030
19482
  /**
19483
+ * Resolve lint args by merging CLI flags with config defaults.
19484
+ *
19485
+ * @param args - CLI arguments (take precedence).
19486
+ * @param config - Prompts config from funkai.config.ts (fallback).
19487
+ * @param fail - Error handler for missing required values.
19488
+ * @returns Resolved args with required fields guaranteed.
19489
+ */
19490
+ function resolveLintArgs(args, config, _fail) {
19491
+ const includes = args.includes ?? (config && config.includes) ?? ["./**"];
19492
+ const excludes = (config && config.excludes) ?? [];
19493
+ const partials = args.partials ?? (config && config.partials);
19494
+ const resolved = {
19495
+ includes,
19496
+ excludes,
19497
+ silent: args.silent
19498
+ };
19499
+ if (partials !== void 0) resolved.partials = partials;
19500
+ return resolved;
19501
+ }
19502
+ /**
19031
19503
  * Shared handler for prompts lint/validation.
19032
19504
  *
19033
- * @param params - Handler context with args, logger, and fail callback.
19505
+ * @param params - Handler context with args, config, logger, and fail callback.
19034
19506
  */
19035
- function handleLint({ args, logger, fail }) {
19036
- const { roots, partials, silent } = args;
19037
- const { discovered, results } = runLintPipeline({
19038
- roots,
19039
- partials
19040
- });
19507
+ function handleLint({ args, config, logger, fail }) {
19508
+ const { includes, excludes, partials, silent } = resolveLintArgs(args, config, fail);
19509
+ const lintPipelineOptions = {
19510
+ includes,
19511
+ excludes
19512
+ };
19513
+ if (partials !== void 0) lintPipelineOptions.partials = partials;
19514
+ const { discovered, results } = runLintPipeline(lintPipelineOptions);
19041
19515
  if (!silent) logger.info(`Linting ${discovered} prompt(s)...`);
19042
19516
  const diagnostics = results.flatMap((result) => result.diagnostics);
19043
19517
  for (const diag of diagnostics) match(diag.level).with("error", () => logger.error(diag.message)).with("warn", () => logger.warn(diag.message)).exhaustive();
@@ -19055,10 +19529,11 @@ function handleLint({ args, logger, fail }) {
19055
19529
  var lintArgs, lint_default;
19056
19530
  var init_lint = __esmMin((() => {
19057
19531
  init_dist();
19532
+ init_config();
19058
19533
  init_lint$1();
19059
19534
  init_pipeline();
19060
19535
  lintArgs = z.object({
19061
- roots: z.array(z.string()).describe("Root directories to scan for .prompt files"),
19536
+ includes: z.array(z.string()).optional().describe("Glob patterns to scan for .prompt files"),
19062
19537
  partials: z.string().optional().describe("Custom partials directory"),
19063
19538
  silent: z.boolean().default(false).describe("Suppress output except errors")
19064
19539
  });
@@ -19066,8 +19541,13 @@ var init_lint = __esmMin((() => {
19066
19541
  description: "Validate .prompt files for schema/template mismatches",
19067
19542
  options: lintArgs,
19068
19543
  handler(ctx) {
19544
+ const config = getConfig(ctx);
19545
+ const lintHandleArgs = { silent: ctx.args.silent };
19546
+ if (ctx.args.includes !== void 0) lintHandleArgs.includes = ctx.args.includes;
19547
+ if (ctx.args.partials !== void 0) lintHandleArgs.partials = ctx.args.partials;
19069
19548
  handleLint({
19070
- args: ctx.args,
19549
+ args: lintHandleArgs,
19550
+ config: config.prompts,
19071
19551
  logger: ctx.logger,
19072
19552
  fail: ctx.fail
19073
19553
  });
@@ -19081,18 +19561,24 @@ var validate_default$1;
19081
19561
  var init_validate$1 = __esmMin((() => {
19082
19562
  init_dist();
19083
19563
  init_lint();
19564
+ init_config();
19084
19565
  validate_default$1 = command({
19085
19566
  description: "Run all validations across the funkai SDK",
19086
19567
  options: lintArgs,
19087
19568
  handler(ctx) {
19088
19569
  const { silent } = ctx.args;
19570
+ const config = getConfig(ctx);
19089
19571
  if (!silent) ctx.logger.info("Running prompts validation...");
19572
+ const lintHandleArgs = { silent: ctx.args.silent };
19573
+ if (ctx.args.includes !== void 0) lintHandleArgs.includes = ctx.args.includes;
19574
+ if (ctx.args.partials !== void 0) lintHandleArgs.partials = ctx.args.partials;
19090
19575
  handleLint({
19091
- args: ctx.args,
19576
+ args: lintHandleArgs,
19577
+ config: config.prompts,
19092
19578
  logger: ctx.logger,
19093
19579
  fail: ctx.fail
19094
19580
  });
19095
- if (!silent) ctx.logger.success("All validations passed.");
19581
+ if (!silent) ctx.logger.success("No errors found.");
19096
19582
  }
19097
19583
  });
19098
19584
  }));
@@ -19116,6 +19602,7 @@ var init_validate = __esmMin((() => {
19116
19602
  var createTemplate, create_default;
19117
19603
  var init_create = __esmMin((() => {
19118
19604
  init_dist();
19605
+ init_config();
19119
19606
  createTemplate = (name) => `---
19120
19607
  name: ${name}
19121
19608
  ---
@@ -19125,15 +19612,28 @@ name: ${name}
19125
19612
  description: "Create a new .prompt file",
19126
19613
  options: z.object({
19127
19614
  name: z.string().describe("Prompt name (kebab-case)"),
19128
- out: z.string().optional().describe("Output directory (defaults to cwd)"),
19615
+ out: z.string().optional().describe("Output directory (defaults to first root in config or cwd)"),
19129
19616
  partial: z.boolean().default(false).describe("Create as a partial in .prompts/partials/")
19130
19617
  }),
19131
19618
  handler(ctx) {
19132
19619
  const { name, out, partial } = ctx.args;
19620
+ const promptsConfig = getConfig(ctx).prompts;
19621
+ const firstInclude = match(promptsConfig).with({ includes: P.array(P.string).select() }, (includes) => {
19622
+ if (includes.length > 0) {
19623
+ const [pattern] = includes;
19624
+ if (pattern === void 0) return;
19625
+ const staticParts = pattern.split("/").filter((p) => !p.includes("*") && !p.includes("?"));
19626
+ if (staticParts.length > 0) return staticParts.join("/");
19627
+ return;
19628
+ }
19629
+ }).otherwise(() => void 0);
19133
19630
  const dir = match({
19134
19631
  partial,
19135
19632
  out
19136
- }).with({ partial: true }, () => resolve(".prompts/partials")).with({ out: P.string }, ({ out: outDir }) => resolve(outDir)).otherwise(() => process.cwd());
19633
+ }).with({ partial: true }, () => resolve(".prompts/partials")).with({ out: P.string }, ({ out: outDir }) => resolve(outDir)).otherwise(() => {
19634
+ if (firstInclude) return resolve(firstInclude);
19635
+ return process.cwd();
19636
+ });
19137
19637
  const filePath = resolve(dir, `${name}.prompt`);
19138
19638
  if (existsSync(filePath)) ctx.fail(`File already exists: ${filePath}`);
19139
19639
  mkdirSync(dir, { recursive: true });
@@ -19143,135 +19643,6 @@ name: ${name}
19143
19643
  });
19144
19644
  }));
19145
19645
 
19146
- //#endregion
19147
- //#region src/commands/prompts/setup.ts
19148
- /** @private */
19149
- function errorMessage(error) {
19150
- if (error instanceof Error) return error.message;
19151
- return String(error);
19152
- }
19153
- /** @private */
19154
- function ensureRecommendation(current, id) {
19155
- if (current.includes(id)) return [...current];
19156
- return [...current, id];
19157
- }
19158
- /** @private */
19159
- function readFileOrEmpty(filePath) {
19160
- if (existsSync(filePath)) return readFileSync(filePath, "utf8");
19161
- return "";
19162
- }
19163
- /** @private */
19164
- function trailingSeparator(content) {
19165
- if (content.length > 0 && !content.endsWith("\n")) return "\n";
19166
- return "";
19167
- }
19168
- /**
19169
- * Read a JSON file, returning an empty object if it doesn't exist.
19170
- * Throws if the file exists but contains invalid JSON, preventing
19171
- * silent data loss from overwriting malformed config files.
19172
- *
19173
- * @private
19174
- */
19175
- function readJsonFile(filePath) {
19176
- if (!existsSync(filePath)) return {};
19177
- const content = readFileSync(filePath, "utf8");
19178
- try {
19179
- return JSON.parse(content);
19180
- } catch (error) {
19181
- throw new Error(`Failed to parse ${filePath}: ${errorMessage(error)}. Fix the JSON syntax or remove the file before running setup.`, { cause: error });
19182
- }
19183
- }
19184
- var VSCODE_DIR, SETTINGS_FILE, EXTENSIONS_FILE, GITIGNORE_FILE, TSCONFIG_FILE, GITIGNORE_ENTRY, PROMPTS_ALIAS, PROMPTS_ALIAS_PATH, setup_default;
19185
- var init_setup = __esmMin((() => {
19186
- init_dist();
19187
- VSCODE_DIR = ".vscode";
19188
- SETTINGS_FILE = "settings.json";
19189
- EXTENSIONS_FILE = "extensions.json";
19190
- GITIGNORE_FILE = ".gitignore";
19191
- TSCONFIG_FILE = "tsconfig.json";
19192
- GITIGNORE_ENTRY = ".prompts/client/";
19193
- PROMPTS_ALIAS = "~prompts";
19194
- PROMPTS_ALIAS_PATH = "./.prompts/client/index.ts";
19195
- setup_default = command({
19196
- description: "Configure VSCode IDE settings for .prompt files",
19197
- async handler(ctx) {
19198
- ctx.logger.intro("Prompt SDK — Project Setup");
19199
- if (await ctx.prompts.confirm({
19200
- message: "Configure VSCode to treat .prompt files as Markdown with Liquid syntax?",
19201
- initialValue: true
19202
- })) {
19203
- const vscodeDir = resolve(VSCODE_DIR);
19204
- mkdirSync(vscodeDir, { recursive: true });
19205
- const settingsPath = resolve(vscodeDir, SETTINGS_FILE);
19206
- const settings = readJsonFile(settingsPath);
19207
- const updatedSettings = {
19208
- ...settings,
19209
- "files.associations": {
19210
- ...settings["files.associations"] ?? {},
19211
- "*.prompt": "markdown"
19212
- },
19213
- "liquid.engine": "standard"
19214
- };
19215
- writeFileSync(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
19216
- ctx.logger.success(`Updated ${settingsPath}`);
19217
- }
19218
- if (await ctx.prompts.confirm({
19219
- message: "Add Shopify Liquid extension to VSCode recommendations?",
19220
- initialValue: true
19221
- })) {
19222
- const vscodeDir = resolve(VSCODE_DIR);
19223
- mkdirSync(vscodeDir, { recursive: true });
19224
- const extensionsPath = resolve(vscodeDir, EXTENSIONS_FILE);
19225
- const extensions = readJsonFile(extensionsPath);
19226
- const recommendations = ensureRecommendation(extensions.recommendations ?? [], "sissel.shopify-liquid");
19227
- const updatedExtensions = {
19228
- ...extensions,
19229
- recommendations
19230
- };
19231
- writeFileSync(extensionsPath, `${JSON.stringify(updatedExtensions, null, 2)}\n`, "utf8");
19232
- ctx.logger.success(`Updated ${extensionsPath}`);
19233
- }
19234
- if (await ctx.prompts.confirm({
19235
- message: "Add .prompts/client/ to .gitignore? (generated client should not be committed)",
19236
- initialValue: true
19237
- })) {
19238
- const gitignorePath = resolve(GITIGNORE_FILE);
19239
- const existing = readFileOrEmpty(gitignorePath);
19240
- if (existing.includes(GITIGNORE_ENTRY)) ctx.logger.info(`${GITIGNORE_ENTRY} already in ${gitignorePath}`);
19241
- else {
19242
- writeFileSync(gitignorePath, `${existing}${`${trailingSeparator(existing)}\n# Generated prompt client (created by \`funkai prompts generate\`)\n${GITIGNORE_ENTRY}\n`}`, "utf8");
19243
- ctx.logger.success(`Added ${GITIGNORE_ENTRY} to ${gitignorePath}`);
19244
- }
19245
- }
19246
- if (await ctx.prompts.confirm({
19247
- message: "Add ~prompts path alias to tsconfig.json?",
19248
- initialValue: true
19249
- })) {
19250
- const tsconfigPath = resolve(TSCONFIG_FILE);
19251
- const tsconfig = readJsonFile(tsconfigPath);
19252
- const compilerOptions = tsconfig.compilerOptions ?? {};
19253
- const existingPaths = compilerOptions.paths ?? {};
19254
- if (existingPaths[PROMPTS_ALIAS]) ctx.logger.info(`${PROMPTS_ALIAS} alias already in ${tsconfigPath}`);
19255
- else {
19256
- const updatedTsconfig = {
19257
- ...tsconfig,
19258
- compilerOptions: {
19259
- ...compilerOptions,
19260
- paths: {
19261
- ...existingPaths,
19262
- [PROMPTS_ALIAS]: [PROMPTS_ALIAS_PATH]
19263
- }
19264
- }
19265
- };
19266
- writeFileSync(tsconfigPath, `${JSON.stringify(updatedTsconfig, null, 2)}\n`, "utf8");
19267
- ctx.logger.success(`Added ${PROMPTS_ALIAS} alias to ${tsconfigPath}`);
19268
- }
19269
- }
19270
- ctx.logger.outro("Project setup complete.");
19271
- }
19272
- });
19273
- }));
19274
-
19275
19646
  //#endregion
19276
19647
  //#region \0virtual:kidd-static-commands
19277
19648
  var _virtual_kidd_static_commands_exports = /* @__PURE__ */ __exportAll$1({ autoload: () => autoload$1 });
@@ -19282,29 +19653,29 @@ var commands;
19282
19653
  var init__virtual_kidd_static_commands = __esmMin((() => {
19283
19654
  init_tag();
19284
19655
  init_generate();
19285
- init_setup$1();
19656
+ init_setup();
19286
19657
  init_validate$1();
19287
19658
  init_validate();
19288
19659
  init_create();
19289
19660
  init_generate$1();
19290
19661
  init_lint();
19291
- init_setup();
19662
+ init_setup$1();
19292
19663
  commands = {
19293
19664
  "generate": generate_default,
19294
- "setup": setup_default$1,
19665
+ "setup": setup_default,
19295
19666
  "validate": validate_default$1,
19296
19667
  "agents": withTag({ commands: { "validate": validate_default } }, "Command"),
19297
19668
  "prompts": withTag({ commands: {
19298
19669
  "create": create_default,
19299
19670
  "generate": generate_default$1,
19300
19671
  "lint": lint_default,
19301
- "setup": setup_default
19672
+ "setup": setup_default$1
19302
19673
  } }, "Command")
19303
19674
  };
19304
19675
  }));
19305
19676
 
19306
19677
  //#endregion
19307
- //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_h5jt7i6rdfwaknd5di57zdj5zy/node_modules/@kidd-cli/core/dist/cli-DhHGZzjZ.js
19678
+ //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_ac2wgu34upx3xtuhxg7cvmcnvm/node_modules/@kidd-cli/core/dist/cli-DhHGZzjZ.js
19308
19679
  async function autoload() {
19309
19680
  return (await Promise.resolve().then(() => (init__virtual_kidd_static_commands(), _virtual_kidd_static_commands_exports))).autoload();
19310
19681
  }
@@ -20139,7 +20510,7 @@ function resolveVersion(explicit) {
20139
20510
  return err(VERSION_ERROR);
20140
20511
  }
20141
20512
  {
20142
- const parsed = VersionSchema.safeParse("0.2.0");
20513
+ const parsed = VersionSchema.safeParse("0.3.1");
20143
20514
  if (parsed.success) return ok(parsed.data);
20144
20515
  }
20145
20516
  return err(VERSION_ERROR);
@@ -20314,7 +20685,7 @@ var init_cli_DhHGZzjZ = __esmMin((() => {
20314
20685
  }));
20315
20686
 
20316
20687
  //#endregion
20317
- //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_h5jt7i6rdfwaknd5di57zdj5zy/node_modules/@kidd-cli/core/dist/index.js
20688
+ //#region ../../node_modules/.pnpm/@kidd-cli+core@0.10.0_chokidar@5.0.0_jiti@2.6.1_magicast@0.5.2_vitest@4.1.0_@opentelemetry+ap_ac2wgu34upx3xtuhxg7cvmcnvm/node_modules/@kidd-cli/core/dist/index.js
20318
20689
  var init_dist = __esmMin((() => {
20319
20690
  init_cli_DhHGZzjZ();
20320
20691
  }));
@@ -20327,16 +20698,18 @@ init_generate();
20327
20698
  init_create();
20328
20699
  init_generate$1();
20329
20700
  init_lint();
20330
- init_setup();
20331
20701
  init_setup$1();
20702
+ init_setup();
20332
20703
  init_validate$1();
20704
+ init_config();
20333
20705
  await cli({
20334
20706
  description: "CLI for the funkai AI SDK framework",
20335
20707
  name: "funkai",
20336
20708
  version: createRequire(import.meta.url)("../package.json").version,
20709
+ config: { schema: configSchema },
20337
20710
  commands: {
20338
20711
  generate: generate_default,
20339
- setup: setup_default$1,
20712
+ setup: setup_default,
20340
20713
  validate: validate_default$1,
20341
20714
  agents: command({
20342
20715
  description: "Agent-related commands",
@@ -20348,7 +20721,7 @@ await cli({
20348
20721
  create: create_default,
20349
20722
  generate: generate_default$1,
20350
20723
  lint: lint_default,
20351
- setup: setup_default
20724
+ setup: setup_default$1
20352
20725
  }
20353
20726
  })
20354
20727
  }