@majeanson/lac 3.0.0 → 3.0.2

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/mcp.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import process from "node:process";
3
+ import process$1 from "node:process";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
- import { fillFeature, genFromFeature } from "@life-as-code/lac-claude";
7
+ import readline from "node:readline";
8
+ import Anthropic from "@anthropic-ai/sdk";
9
+ import { execSync } from "node:child_process";
8
10
 
9
11
  //#region ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
10
12
  /** A special constant with type `never` */
@@ -2606,7 +2608,7 @@ function initializeContext(params) {
2606
2608
  external: params?.external ?? void 0
2607
2609
  };
2608
2610
  }
2609
- function process$1(schema, ctx, _params = {
2611
+ function process$2(schema, ctx, _params = {
2610
2612
  path: [],
2611
2613
  schemaPath: []
2612
2614
  }) {
@@ -2643,7 +2645,7 @@ function process$1(schema, ctx, _params = {
2643
2645
  const parent = schema._zod.parent;
2644
2646
  if (parent) {
2645
2647
  if (!result.ref) result.ref = parent;
2646
- process$1(parent, ctx, params);
2648
+ process$2(parent, ctx, params);
2647
2649
  ctx.seen.get(parent).isParent = true;
2648
2650
  }
2649
2651
  }
@@ -2855,7 +2857,7 @@ const createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
2855
2857
  ...params,
2856
2858
  processors
2857
2859
  });
2858
- process$1(schema, ctx);
2860
+ process$2(schema, ctx);
2859
2861
  extractDefs(ctx, schema);
2860
2862
  return finalize(ctx, schema);
2861
2863
  };
@@ -2867,7 +2869,7 @@ const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params)
2867
2869
  io,
2868
2870
  processors
2869
2871
  });
2870
- process$1(schema, ctx);
2872
+ process$2(schema, ctx);
2871
2873
  extractDefs(ctx, schema);
2872
2874
  return finalize(ctx, schema);
2873
2875
  };
@@ -2951,7 +2953,7 @@ const arrayProcessor = (schema, ctx, _json, params) => {
2951
2953
  if (typeof minimum === "number") json.minItems = minimum;
2952
2954
  if (typeof maximum === "number") json.maxItems = maximum;
2953
2955
  json.type = "array";
2954
- json.items = process$1(def.element, ctx, {
2956
+ json.items = process$2(def.element, ctx, {
2955
2957
  ...params,
2956
2958
  path: [...params.path, "items"]
2957
2959
  });
@@ -2962,7 +2964,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
2962
2964
  json.type = "object";
2963
2965
  json.properties = {};
2964
2966
  const shape = def.shape;
2965
- for (const key in shape) json.properties[key] = process$1(shape[key], ctx, {
2967
+ for (const key in shape) json.properties[key] = process$2(shape[key], ctx, {
2966
2968
  ...params,
2967
2969
  path: [
2968
2970
  ...params.path,
@@ -2980,7 +2982,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
2980
2982
  if (def.catchall?._zod.def.type === "never") json.additionalProperties = false;
2981
2983
  else if (!def.catchall) {
2982
2984
  if (ctx.io === "output") json.additionalProperties = false;
2983
- } else if (def.catchall) json.additionalProperties = process$1(def.catchall, ctx, {
2985
+ } else if (def.catchall) json.additionalProperties = process$2(def.catchall, ctx, {
2984
2986
  ...params,
2985
2987
  path: [...params.path, "additionalProperties"]
2986
2988
  });
@@ -2988,7 +2990,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
2988
2990
  const unionProcessor = (schema, ctx, json, params) => {
2989
2991
  const def = schema._zod.def;
2990
2992
  const isExclusive = def.inclusive === false;
2991
- const options = def.options.map((x, i) => process$1(x, ctx, {
2993
+ const options = def.options.map((x, i) => process$2(x, ctx, {
2992
2994
  ...params,
2993
2995
  path: [
2994
2996
  ...params.path,
@@ -3001,7 +3003,7 @@ const unionProcessor = (schema, ctx, json, params) => {
3001
3003
  };
3002
3004
  const intersectionProcessor = (schema, ctx, json, params) => {
3003
3005
  const def = schema._zod.def;
3004
- const a = process$1(def.left, ctx, {
3006
+ const a = process$2(def.left, ctx, {
3005
3007
  ...params,
3006
3008
  path: [
3007
3009
  ...params.path,
@@ -3009,7 +3011,7 @@ const intersectionProcessor = (schema, ctx, json, params) => {
3009
3011
  0
3010
3012
  ]
3011
3013
  });
3012
- const b = process$1(def.right, ctx, {
3014
+ const b = process$2(def.right, ctx, {
3013
3015
  ...params,
3014
3016
  path: [
3015
3017
  ...params.path,
@@ -3022,7 +3024,7 @@ const intersectionProcessor = (schema, ctx, json, params) => {
3022
3024
  };
3023
3025
  const nullableProcessor = (schema, ctx, json, params) => {
3024
3026
  const def = schema._zod.def;
3025
- const inner = process$1(def.innerType, ctx, params);
3027
+ const inner = process$2(def.innerType, ctx, params);
3026
3028
  const seen = ctx.seen.get(schema);
3027
3029
  if (ctx.target === "openapi-3.0") {
3028
3030
  seen.ref = def.innerType;
@@ -3031,27 +3033,27 @@ const nullableProcessor = (schema, ctx, json, params) => {
3031
3033
  };
3032
3034
  const nonoptionalProcessor = (schema, ctx, _json, params) => {
3033
3035
  const def = schema._zod.def;
3034
- process$1(def.innerType, ctx, params);
3036
+ process$2(def.innerType, ctx, params);
3035
3037
  const seen = ctx.seen.get(schema);
3036
3038
  seen.ref = def.innerType;
3037
3039
  };
3038
3040
  const defaultProcessor = (schema, ctx, json, params) => {
3039
3041
  const def = schema._zod.def;
3040
- process$1(def.innerType, ctx, params);
3042
+ process$2(def.innerType, ctx, params);
3041
3043
  const seen = ctx.seen.get(schema);
3042
3044
  seen.ref = def.innerType;
3043
3045
  json.default = JSON.parse(JSON.stringify(def.defaultValue));
3044
3046
  };
3045
3047
  const prefaultProcessor = (schema, ctx, json, params) => {
3046
3048
  const def = schema._zod.def;
3047
- process$1(def.innerType, ctx, params);
3049
+ process$2(def.innerType, ctx, params);
3048
3050
  const seen = ctx.seen.get(schema);
3049
3051
  seen.ref = def.innerType;
3050
3052
  if (ctx.io === "input") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
3051
3053
  };
3052
3054
  const catchProcessor = (schema, ctx, json, params) => {
3053
3055
  const def = schema._zod.def;
3054
- process$1(def.innerType, ctx, params);
3056
+ process$2(def.innerType, ctx, params);
3055
3057
  const seen = ctx.seen.get(schema);
3056
3058
  seen.ref = def.innerType;
3057
3059
  let catchValue;
@@ -3065,20 +3067,20 @@ const catchProcessor = (schema, ctx, json, params) => {
3065
3067
  const pipeProcessor = (schema, ctx, _json, params) => {
3066
3068
  const def = schema._zod.def;
3067
3069
  const innerType = ctx.io === "input" ? def.in._zod.def.type === "transform" ? def.out : def.in : def.out;
3068
- process$1(innerType, ctx, params);
3070
+ process$2(innerType, ctx, params);
3069
3071
  const seen = ctx.seen.get(schema);
3070
3072
  seen.ref = innerType;
3071
3073
  };
3072
3074
  const readonlyProcessor = (schema, ctx, json, params) => {
3073
3075
  const def = schema._zod.def;
3074
- process$1(def.innerType, ctx, params);
3076
+ process$2(def.innerType, ctx, params);
3075
3077
  const seen = ctx.seen.get(schema);
3076
3078
  seen.ref = def.innerType;
3077
3079
  json.readOnly = true;
3078
3080
  };
3079
3081
  const optionalProcessor = (schema, ctx, _json, params) => {
3080
3082
  const def = schema._zod.def;
3081
- process$1(def.innerType, ctx, params);
3083
+ process$2(def.innerType, ctx, params);
3082
3084
  const seen = ctx.seen.get(schema);
3083
3085
  seen.ref = def.innerType;
3084
3086
  };
@@ -3773,9 +3775,437 @@ function validateFeature(data) {
3773
3775
  };
3774
3776
  }
3775
3777
 
3778
+ //#endregion
3779
+ //#region ../lac-claude/dist/index.mjs
3780
+ function createClient() {
3781
+ let apiKey = process$1.env.ANTHROPIC_API_KEY;
3782
+ if (!apiKey) {
3783
+ const configPath = findLacConfig();
3784
+ if (configPath) try {
3785
+ apiKey = JSON.parse(fs.readFileSync(configPath, "utf-8"))?.ai?.apiKey;
3786
+ } catch {}
3787
+ }
3788
+ if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set.\nSet it via:\n export ANTHROPIC_API_KEY=sk-ant-...\nOr add it to .lac/config.json:\n { \"ai\": { \"apiKey\": \"sk-ant-...\" } }\nGet a key at https://console.anthropic.com/settings/keys");
3789
+ return new Anthropic({ apiKey });
3790
+ }
3791
+ function findLacConfig() {
3792
+ let current = process$1.cwd();
3793
+ while (true) {
3794
+ const candidate = path.join(current, ".lac", "config.json");
3795
+ if (fs.existsSync(candidate)) return candidate;
3796
+ const parent = path.dirname(current);
3797
+ if (parent === current) return null;
3798
+ current = parent;
3799
+ }
3800
+ }
3801
+ async function generateText(client, systemPrompt, userMessage, model = "claude-sonnet-4-6") {
3802
+ const content = (await client.messages.create({
3803
+ model,
3804
+ max_tokens: 4096,
3805
+ system: systemPrompt,
3806
+ messages: [{
3807
+ role: "user",
3808
+ content: userMessage
3809
+ }]
3810
+ })).content[0];
3811
+ if (!content || content.type !== "text") throw new Error("Unexpected response type from Claude API");
3812
+ return content.text;
3813
+ }
3814
+ const SOURCE_EXTENSIONS = new Set([
3815
+ ".ts",
3816
+ ".tsx",
3817
+ ".js",
3818
+ ".jsx",
3819
+ ".py",
3820
+ ".go",
3821
+ ".rs",
3822
+ ".java",
3823
+ ".cs",
3824
+ ".rb",
3825
+ ".php",
3826
+ ".vue",
3827
+ ".svelte",
3828
+ ".sql"
3829
+ ]);
3830
+ const MAX_FILE_CHARS = 8e3;
3831
+ const MAX_TOTAL_CHARS = 32e4;
3832
+ function buildContext(featureDir, feature) {
3833
+ return {
3834
+ feature,
3835
+ featurePath: path.join(featureDir, "feature.json"),
3836
+ sourceFiles: gatherSourceFiles(featureDir),
3837
+ gitLog: getGitLog(featureDir)
3838
+ };
3839
+ }
3840
+ function gatherSourceFiles(dir) {
3841
+ const files = [];
3842
+ let totalChars = 0;
3843
+ const priorityNames = [
3844
+ "package.json",
3845
+ "README.md",
3846
+ "tsconfig.json",
3847
+ ".env.example"
3848
+ ];
3849
+ for (const name of priorityNames) {
3850
+ const p = path.join(dir, name);
3851
+ if (fs.existsSync(p)) try {
3852
+ const content = truncate(fs.readFileSync(p, "utf-8"), 4e3);
3853
+ files.push({
3854
+ relativePath: name,
3855
+ content
3856
+ });
3857
+ totalChars += content.length;
3858
+ } catch {}
3859
+ }
3860
+ const allSource = walkDir(dir).filter((f) => SOURCE_EXTENSIONS.has(path.extname(f)) && !f.includes("node_modules") && !f.includes(".turbo") && !f.includes("dist/"));
3861
+ allSource.sort((a, b) => {
3862
+ const aTest = /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(a);
3863
+ return aTest === /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(b) ? 0 : aTest ? 1 : -1;
3864
+ });
3865
+ for (const filePath of allSource) {
3866
+ if (totalChars >= MAX_TOTAL_CHARS) break;
3867
+ if (priorityNames.includes(path.basename(filePath))) continue;
3868
+ try {
3869
+ const content = truncate(fs.readFileSync(filePath, "utf-8"), MAX_FILE_CHARS);
3870
+ const relativePath = path.relative(dir, filePath);
3871
+ files.push({
3872
+ relativePath,
3873
+ content
3874
+ });
3875
+ totalChars += content.length;
3876
+ } catch {}
3877
+ }
3878
+ return files;
3879
+ }
3880
+ function walkDir(dir) {
3881
+ const results = [];
3882
+ try {
3883
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
3884
+ for (const entry of entries) {
3885
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
3886
+ const full = path.join(dir, entry.name);
3887
+ if (entry.isDirectory()) results.push(...walkDir(full));
3888
+ else results.push(full);
3889
+ }
3890
+ } catch {}
3891
+ return results;
3892
+ }
3893
+ function truncate(content, maxChars) {
3894
+ if (content.length <= maxChars) return content;
3895
+ return content.slice(0, maxChars) + "\n... [truncated]";
3896
+ }
3897
+ function getGitLog(dir) {
3898
+ try {
3899
+ return execSync("git log --oneline --follow -20 -- .", {
3900
+ cwd: dir,
3901
+ encoding: "utf-8",
3902
+ stdio: [
3903
+ "pipe",
3904
+ "pipe",
3905
+ "pipe"
3906
+ ]
3907
+ }).trim();
3908
+ } catch {
3909
+ return "";
3910
+ }
3911
+ }
3912
+ function contextToString(ctx) {
3913
+ const parts = [];
3914
+ parts.push("=== feature.json ===");
3915
+ parts.push(JSON.stringify(ctx.feature, null, 2));
3916
+ if (ctx.gitLog) {
3917
+ parts.push("\n=== git log (last 20 commits) ===");
3918
+ parts.push(ctx.gitLog);
3919
+ }
3920
+ for (const file of ctx.sourceFiles) {
3921
+ parts.push(`\n=== ${file.relativePath} ===`);
3922
+ parts.push(file.content);
3923
+ }
3924
+ return parts.join("\n");
3925
+ }
3926
+ const RESET = "\x1B[0m";
3927
+ const BOLD = "\x1B[1m";
3928
+ const GREEN = "\x1B[32m";
3929
+ const CYAN = "\x1B[36m";
3930
+ const DIM = "\x1B[2m";
3931
+ function formatValue(value) {
3932
+ if (typeof value === "string") return value.length > 300 ? value.slice(0, 300) + "…" : value;
3933
+ return JSON.stringify(value, null, 2);
3934
+ }
3935
+ function printDiff(diffs) {
3936
+ const separator = "━".repeat(52);
3937
+ for (const diff of diffs) {
3938
+ const label = diff.wasEmpty ? "empty → generated" : "updated";
3939
+ process.stdout.write(`\n${BOLD}${CYAN}${separator}${RESET}\n`);
3940
+ process.stdout.write(`${BOLD} ${diff.field}${RESET} ${DIM}(${label})${RESET}\n`);
3941
+ process.stdout.write(`${CYAN}${separator}${RESET}\n`);
3942
+ const lines = formatValue(diff.proposed).split("\n");
3943
+ for (const line of lines) process.stdout.write(`${GREEN} ${line}${RESET}\n`);
3944
+ }
3945
+ process.stdout.write("\n");
3946
+ }
3947
+ const FILL_PROMPTS = {
3948
+ analysis: {
3949
+ system: `You are a software engineering analyst. Given a feature.json and the feature's source code, write a clear analysis section. Cover: what the code does architecturally, key patterns used, and why they were likely chosen. Be specific — name actual functions, modules, and techniques visible in the code. Write in first-person technical prose, 150-300 words. Return only the analysis text, no JSON wrapper, no markdown heading.`,
3950
+ userSuffix: "Write the analysis field for this feature."
3951
+ },
3952
+ decisions: {
3953
+ system: `You are a software engineering analyst. Given a feature.json and source code, extract 2-4 key technical decisions evident from the code. For each: what was decided (concrete), why (rationale from code evidence), what alternatives were likely considered.
3954
+
3955
+ Return ONLY a valid JSON array — no other text, no markdown fences:
3956
+ [
3957
+ {
3958
+ "decision": "string",
3959
+ "rationale": "string",
3960
+ "alternativesConsidered": ["string"],
3961
+ "date": null
3962
+ }
3963
+ ]`,
3964
+ userSuffix: "Extract the key technical decisions from this feature."
3965
+ },
3966
+ implementation: {
3967
+ system: `You are a software engineering analyst. Given a feature.json and source code, write concise implementation notes. Cover: the main components and their roles, how data flows through the feature, and any non-obvious patterns or constraints. 100-200 words. Return only the text, no JSON wrapper, no heading.`,
3968
+ userSuffix: "Write the implementation field for this feature."
3969
+ },
3970
+ knownLimitations: {
3971
+ system: `You are a software engineering analyst. Identify 2-4 known limitations, trade-offs, or tech-debt items visible in this code. Look for TODOs, FIXMEs, missing error handling, overly complex patterns, or performance gaps.
3972
+
3973
+ Return ONLY a valid JSON array of strings — no other text:
3974
+ ["limitation 1", "limitation 2"]`,
3975
+ userSuffix: "List the known limitations visible in this feature."
3976
+ },
3977
+ tags: {
3978
+ system: `You are a software engineering analyst. Generate 3-6 tags from the domain language in this code. Lowercase, single words or hyphenated. Reflect the actual domain, not generic terms like "code" or "feature".
3979
+
3980
+ Return ONLY a valid JSON array of strings — no other text:
3981
+ ["tag1", "tag2", "tag3"]`,
3982
+ userSuffix: "Generate tags for this feature."
3983
+ },
3984
+ annotations: {
3985
+ system: `You are a software engineering analyst. Identify 1-3 significant annotations worth capturing — warnings, lessons, tech debt, or breaking-change risks visible in the code.
3986
+
3987
+ Return ONLY a valid JSON array — no other text:
3988
+ [
3989
+ {
3990
+ "id": "auto-1",
3991
+ "author": "lac fill",
3992
+ "date": "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}",
3993
+ "type": "tech-debt",
3994
+ "body": "string"
3995
+ }
3996
+ ]`,
3997
+ userSuffix: "Generate annotations for this feature."
3998
+ },
3999
+ successCriteria: {
4000
+ system: `You are a software engineering analyst. Write a plain-language success criteria statement for this feature — "how do we know it's done and working?" Be specific and testable. 1-3 sentences. Return only the text, no JSON wrapper, no heading.`,
4001
+ userSuffix: "Write the success criteria for this feature."
4002
+ },
4003
+ domain: {
4004
+ system: `You are a software engineering analyst. Identify the primary technical domain for this feature from its code and problem statement. Return a single lowercase word or short hyphenated phrase (e.g. "auth", "payments", "notifications", "data-pipeline"). Return only the domain value — nothing else.`,
4005
+ userSuffix: "Identify the domain for this feature."
4006
+ }
4007
+ };
4008
+ const JSON_FIELDS = new Set([
4009
+ "decisions",
4010
+ "knownLimitations",
4011
+ "tags",
4012
+ "annotations"
4013
+ ]);
4014
+ const ALL_FILLABLE_FIELDS = [
4015
+ "analysis",
4016
+ "decisions",
4017
+ "implementation",
4018
+ "knownLimitations",
4019
+ "tags",
4020
+ "successCriteria",
4021
+ "domain"
4022
+ ];
4023
+ function getMissingFields(feature) {
4024
+ return ALL_FILLABLE_FIELDS.filter((field) => {
4025
+ const val = feature[field];
4026
+ if (val === void 0 || val === null) return true;
4027
+ if (typeof val === "string") return val.trim().length === 0;
4028
+ if (Array.isArray(val)) return val.length === 0;
4029
+ return false;
4030
+ });
4031
+ }
4032
+ const GEN_PROMPTS = {
4033
+ component: {
4034
+ system: `You are an expert React/TypeScript developer. You will be given a feature.json describing a feature. Generate a production-quality React component implementing the core UI for this feature. Include TypeScript types, sensible props, and clear comments. Make it maintainable — any developer unfamiliar with this feature should understand it. Return only the component code, no explanation.`,
4035
+ userSuffix: "Generate a React TypeScript component for this feature."
4036
+ },
4037
+ test: {
4038
+ system: `You are an expert software testing engineer. You will be given a feature.json. Generate a comprehensive test suite using Vitest. Use the successCriteria to derive happy-path tests and the knownLimitations to derive edge-case tests. Return only the test code, no explanation.`,
4039
+ userSuffix: "Generate a Vitest test suite for this feature."
4040
+ },
4041
+ migration: {
4042
+ system: `You are an expert database engineer. You will be given a feature.json. Generate a database migration scaffold for the data model this feature implies. Use SQL with clear comments. Include both up (CREATE) and down (DROP) sections. Return only the SQL, no explanation.`,
4043
+ userSuffix: "Generate a database migration for this feature."
4044
+ },
4045
+ docs: {
4046
+ system: `You are a technical writer. You will be given a feature.json. Generate user-facing documentation for this feature. Write it clearly enough that any end user can understand it (not developer-focused). Cover: what it does, how to use it, and known limitations. Use Markdown. Return only the documentation, no explanation.`,
4047
+ userSuffix: "Generate user-facing documentation for this feature."
4048
+ }
4049
+ };
4050
+ async function fillFeature(options) {
4051
+ const { featureDir, dryRun = false, skipConfirm = false, model = "claude-sonnet-4-6" } = options;
4052
+ const featurePath = path.join(featureDir, "feature.json");
4053
+ let raw;
4054
+ try {
4055
+ raw = fs.readFileSync(featurePath, "utf-8");
4056
+ } catch {
4057
+ throw new Error(`No feature.json found at "${featurePath}"`);
4058
+ }
4059
+ let parsed;
4060
+ try {
4061
+ parsed = JSON.parse(raw);
4062
+ } catch {
4063
+ throw new Error(`Invalid JSON in "${featurePath}"`);
4064
+ }
4065
+ const result = validateFeature(parsed);
4066
+ if (!result.success) throw new Error(`Invalid feature.json: ${result.errors.join(", ")}`);
4067
+ const feature = result.data;
4068
+ const client = createClient();
4069
+ const fieldsToFill = options.fields ? options.fields : getMissingFields(feature);
4070
+ if (fieldsToFill.length === 0) {
4071
+ process$1.stdout.write(` All fields already filled for ${feature.featureKey}.\n`);
4072
+ return {
4073
+ applied: false,
4074
+ fields: [],
4075
+ patch: {}
4076
+ };
4077
+ }
4078
+ process$1.stdout.write(`\nAnalyzing ${feature.featureKey} (${feature.title})...\n`);
4079
+ const ctx = buildContext(featureDir, feature);
4080
+ const contextStr = contextToString(ctx);
4081
+ process$1.stdout.write(`Reading ${ctx.sourceFiles.length} source file(s)...\n`);
4082
+ process$1.stdout.write(`Generating with ${model}...\n`);
4083
+ const patch = {};
4084
+ const diffs = [];
4085
+ for (const field of fieldsToFill) {
4086
+ const prompt = FILL_PROMPTS[field];
4087
+ if (!prompt) continue;
4088
+ process$1.stdout.write(` → ${field}...`);
4089
+ try {
4090
+ const rawValue = await generateText(client, prompt.system, `${contextStr}\n\n${prompt.userSuffix}`, model);
4091
+ let value = rawValue.trim();
4092
+ if (JSON_FIELDS.has(field)) try {
4093
+ const jsonStr = rawValue.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1] ?? rawValue;
4094
+ value = JSON.parse(jsonStr.trim());
4095
+ } catch {
4096
+ process$1.stderr.write(`\n Warning: could not parse JSON for "${field}", storing as string\n`);
4097
+ }
4098
+ patch[field] = value;
4099
+ const existing = feature[field];
4100
+ const wasEmpty = existing === void 0 || existing === null || typeof existing === "string" && existing.trim().length === 0 || Array.isArray(existing) && existing.length === 0;
4101
+ diffs.push({
4102
+ field,
4103
+ wasEmpty,
4104
+ proposed: value
4105
+ });
4106
+ process$1.stdout.write(" done\n");
4107
+ } catch (err) {
4108
+ process$1.stdout.write(" failed\n");
4109
+ process$1.stderr.write(` Error generating "${field}": ${err instanceof Error ? err.message : String(err)}\n`);
4110
+ }
4111
+ }
4112
+ if (diffs.length === 0) return {
4113
+ applied: false,
4114
+ fields: [],
4115
+ patch: {}
4116
+ };
4117
+ printDiff(diffs);
4118
+ if (dryRun) {
4119
+ process$1.stdout.write(" [dry-run] No changes written.\n\n");
4120
+ return {
4121
+ applied: false,
4122
+ fields: Object.keys(patch),
4123
+ patch
4124
+ };
4125
+ }
4126
+ if (!skipConfirm) {
4127
+ const answer = await askUser("Apply? [Y]es / [n]o / [f]ield-by-field: ");
4128
+ if (answer.toLowerCase() === "n") {
4129
+ process$1.stdout.write(" Cancelled.\n");
4130
+ return {
4131
+ applied: false,
4132
+ fields: Object.keys(patch),
4133
+ patch
4134
+ };
4135
+ }
4136
+ if (answer.toLowerCase() === "f") {
4137
+ const approved = {};
4138
+ for (const [field, value] of Object.entries(patch)) if ((await askUser(` Apply "${field}"? [Y/n]: `)).toLowerCase() !== "n") approved[field] = value;
4139
+ for (const key of Object.keys(patch)) if (!(key in approved)) delete patch[key];
4140
+ Object.assign(patch, approved);
4141
+ }
4142
+ }
4143
+ const updated = {
4144
+ ...parsed,
4145
+ ...patch
4146
+ };
4147
+ fs.writeFileSync(featurePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
4148
+ const count = Object.keys(patch).length;
4149
+ process$1.stdout.write(`\n ✓ Updated ${feature.featureKey} — ${count} field${count === 1 ? "" : "s"} written.\n\n`);
4150
+ return {
4151
+ applied: true,
4152
+ fields: Object.keys(patch),
4153
+ patch
4154
+ };
4155
+ }
4156
+ async function genFromFeature(options) {
4157
+ const { featureDir, type, dryRun = false, model = "claude-sonnet-4-6" } = options;
4158
+ const featurePath = path.join(featureDir, "feature.json");
4159
+ let raw;
4160
+ try {
4161
+ raw = fs.readFileSync(featurePath, "utf-8");
4162
+ } catch {
4163
+ throw new Error(`No feature.json found at "${featurePath}"`);
4164
+ }
4165
+ const result = validateFeature(JSON.parse(raw));
4166
+ if (!result.success) throw new Error(`Invalid feature.json: ${result.errors.join(", ")}`);
4167
+ const feature = result.data;
4168
+ const promptConfig = GEN_PROMPTS[type];
4169
+ if (!promptConfig) throw new Error(`Unknown generation type: "${type}". Available: component, test, migration, docs`);
4170
+ const client = createClient();
4171
+ process$1.stdout.write(`\nGenerating ${type} for ${feature.featureKey} (${feature.title})...\n`);
4172
+ process$1.stdout.write(`Model: ${model}\n\n`);
4173
+ const contextStr = contextToString(buildContext(featureDir, feature));
4174
+ const generated = await generateText(client, promptConfig.system, `${contextStr}\n\n${promptConfig.userSuffix}`, model);
4175
+ if (dryRun) {
4176
+ process$1.stdout.write(generated);
4177
+ process$1.stdout.write("\n\n [dry-run] No file written.\n");
4178
+ return generated;
4179
+ }
4180
+ const outFile = options.outFile ?? path.join(featureDir, `${feature.featureKey}${typeToExt(type)}`);
4181
+ fs.writeFileSync(outFile, generated, "utf-8");
4182
+ process$1.stdout.write(` ✓ Written to ${outFile}\n\n`);
4183
+ return generated;
4184
+ }
4185
+ function typeToExt(type) {
4186
+ return {
4187
+ component: ".tsx",
4188
+ test: ".test.ts",
4189
+ migration: ".sql",
4190
+ docs: ".md"
4191
+ }[type] ?? ".txt";
4192
+ }
4193
+ function askUser(question) {
4194
+ return new Promise((resolve) => {
4195
+ const rl = readline.createInterface({
4196
+ input: process$1.stdin,
4197
+ output: process$1.stdout
4198
+ });
4199
+ rl.question(question, (answer) => {
4200
+ rl.close();
4201
+ resolve(answer.trim() || "y");
4202
+ });
4203
+ });
4204
+ }
4205
+
3776
4206
  //#endregion
3777
4207
  //#region ../lac-mcp/src/index.ts
3778
- const workspaceRoot = process.argv[2] ?? process.env.LAC_WORKSPACE ?? process.cwd();
4208
+ const workspaceRoot = process$1.argv[2] ?? process$1.env.LAC_WORKSPACE ?? process$1.cwd();
3779
4209
  const server = new Server({
3780
4210
  name: "lac",
3781
4211
  version: "1.0.0"
@@ -3939,6 +4369,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [
3939
4369
  description: "Directory to scan (default: workspace root)"
3940
4370
  } }
3941
4371
  }
4372
+ },
4373
+ {
4374
+ name: "read_feature_context",
4375
+ description: "Read a feature.json and all surrounding source files. Returns the full context needed to fill missing fields — use this when the user asks you to fill or analyse a feature WITHOUT calling an external AI API (you ARE the AI). After reading, generate the missing fields yourself and call write_feature_fields to save.",
4376
+ inputSchema: {
4377
+ type: "object",
4378
+ properties: { path: {
4379
+ type: "string",
4380
+ description: "Absolute or relative path to the feature folder (contains feature.json)"
4381
+ } },
4382
+ required: ["path"]
4383
+ }
4384
+ },
4385
+ {
4386
+ name: "write_feature_fields",
4387
+ description: "Patch a feature.json with new field values. Use this after read_feature_context — write the fields you generated back to disk.",
4388
+ inputSchema: {
4389
+ type: "object",
4390
+ properties: {
4391
+ path: {
4392
+ type: "string",
4393
+ description: "Absolute or relative path to the feature folder (contains feature.json)"
4394
+ },
4395
+ fields: {
4396
+ type: "object",
4397
+ description: "Key-value pairs to merge into feature.json. Values may be strings, arrays, or objects depending on the field."
4398
+ }
4399
+ },
4400
+ required: ["path", "fields"]
4401
+ }
3942
4402
  }
3943
4403
  ] }));
3944
4404
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -4061,6 +4521,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4061
4521
  text: `${passes.length} passed, ${failures.length} failed — ${results.length} features checked\n\n${lines.join("\n")}`
4062
4522
  }] };
4063
4523
  }
4524
+ case "read_feature_context": {
4525
+ const featureDir = resolvePath(String(a.path));
4526
+ const featurePath = path.join(featureDir, "feature.json");
4527
+ let raw;
4528
+ try {
4529
+ raw = fs.readFileSync(featurePath, "utf-8");
4530
+ } catch {
4531
+ return {
4532
+ content: [{
4533
+ type: "text",
4534
+ text: `No feature.json found at "${featurePath}"`
4535
+ }],
4536
+ isError: true
4537
+ };
4538
+ }
4539
+ const result = validateFeature(JSON.parse(raw));
4540
+ if (!result.success) return {
4541
+ content: [{
4542
+ type: "text",
4543
+ text: `Invalid feature.json: ${result.errors.join(", ")}`
4544
+ }],
4545
+ isError: true
4546
+ };
4547
+ const feature = result.data;
4548
+ const contextStr = contextToString(buildContext(featureDir, feature));
4549
+ const missingFields = getMissingFields(feature);
4550
+ const fieldInstructions = missingFields.map((field) => {
4551
+ const prompt = FILL_PROMPTS[field];
4552
+ const isJson = JSON_FIELDS.has(field);
4553
+ return `### ${field}\n${prompt.system}\n${prompt.userSuffix}\n${isJson ? "(Return valid JSON for this field)" : "(Return plain text for this field)"}`;
4554
+ }).join("\n\n");
4555
+ return { content: [{
4556
+ type: "text",
4557
+ text: `${missingFields.length === 0 ? "All fillable fields are already populated. No generation needed." : `## Missing fields to fill (${missingFields.join(", ")})\n\nFor each field below, generate the value described, then call write_feature_fields with all generated values.\n\n${fieldInstructions}`}\n\n## Context\n\n${contextStr}`
4558
+ }] };
4559
+ }
4560
+ case "write_feature_fields": {
4561
+ const featureDir = resolvePath(String(a.path));
4562
+ const featurePath = path.join(featureDir, "feature.json");
4563
+ let raw;
4564
+ try {
4565
+ raw = fs.readFileSync(featurePath, "utf-8");
4566
+ } catch {
4567
+ return {
4568
+ content: [{
4569
+ type: "text",
4570
+ text: `No feature.json found at "${featurePath}"`
4571
+ }],
4572
+ isError: true
4573
+ };
4574
+ }
4575
+ const existing = JSON.parse(raw);
4576
+ const fields = a.fields;
4577
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) return {
4578
+ content: [{
4579
+ type: "text",
4580
+ text: "fields must be a JSON object"
4581
+ }],
4582
+ isError: true
4583
+ };
4584
+ const updated = {
4585
+ ...existing,
4586
+ ...fields
4587
+ };
4588
+ fs.writeFileSync(featurePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
4589
+ const writtenKeys = Object.keys(fields);
4590
+ return { content: [{
4591
+ type: "text",
4592
+ text: `✓ Wrote ${writtenKeys.length} field(s) to ${featurePath}: ${writtenKeys.join(", ")}`
4593
+ }] };
4594
+ }
4064
4595
  default: return { content: [{
4065
4596
  type: "text",
4066
4597
  text: `Unknown tool: ${name}`
@@ -4146,11 +4677,11 @@ function statusIcon(status) {
4146
4677
  async function main() {
4147
4678
  const transport = new StdioServerTransport();
4148
4679
  await server.connect(transport);
4149
- process.stderr.write(`lac MCP server running (workspace: ${workspaceRoot})\n`);
4680
+ process$1.stderr.write(`lac MCP server running (workspace: ${workspaceRoot})\n`);
4150
4681
  }
4151
4682
  main().catch((err) => {
4152
- process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
4153
- process.exit(1);
4683
+ process$1.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
4684
+ process$1.exit(1);
4154
4685
  });
4155
4686
 
4156
4687
  //#endregion