@ekaone/json-cli 0.1.2 → 0.1.3
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/cli.cjs +71 -15
- package/dist/index.cjs +21 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -117,7 +117,20 @@ var CATALOG = {
|
|
|
117
117
|
pnpm: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
118
118
|
yarn: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
119
119
|
bun: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
120
|
-
git: [
|
|
120
|
+
git: [
|
|
121
|
+
"init",
|
|
122
|
+
"add",
|
|
123
|
+
"commit",
|
|
124
|
+
"push",
|
|
125
|
+
"pull",
|
|
126
|
+
"clone",
|
|
127
|
+
"status",
|
|
128
|
+
"log",
|
|
129
|
+
"branch",
|
|
130
|
+
"checkout",
|
|
131
|
+
"merge",
|
|
132
|
+
"stash"
|
|
133
|
+
],
|
|
121
134
|
shell: ["any"]
|
|
122
135
|
// escape hatch — always requires extra confirmation
|
|
123
136
|
};
|
|
@@ -231,8 +244,13 @@ async function runStep(step) {
|
|
|
231
244
|
});
|
|
232
245
|
return { success: true };
|
|
233
246
|
} catch (err) {
|
|
234
|
-
const
|
|
235
|
-
|
|
247
|
+
const parts = [
|
|
248
|
+
`Command: ${bin} ${args.join(" ")}`,
|
|
249
|
+
err?.exitCode ? `Exit code: ${err.exitCode}` : null,
|
|
250
|
+
err?.stderr ? `Reason: ${err.stderr.trim()}` : null,
|
|
251
|
+
!err?.stderr ? err?.message ?? String(err) : null
|
|
252
|
+
].filter(Boolean).join("\n ");
|
|
253
|
+
return { success: false, error: parts };
|
|
236
254
|
}
|
|
237
255
|
}
|
|
238
256
|
async function runPlan(plan, onStep) {
|
|
@@ -249,28 +267,58 @@ async function runPlan(plan, onStep) {
|
|
|
249
267
|
}
|
|
250
268
|
|
|
251
269
|
// src/cli.ts
|
|
270
|
+
function showHelp() {
|
|
271
|
+
p.intro("json-cli \u2014 AI-powered CLI task runner");
|
|
272
|
+
p.log.message(`Usage
|
|
273
|
+
json-cli "<your goal>" [options]
|
|
274
|
+
`);
|
|
275
|
+
p.log.message(
|
|
276
|
+
`Options
|
|
277
|
+
--provider <name> AI provider: claude | openai | ollama (default: claude)
|
|
278
|
+
--yes Skip confirmation prompt
|
|
279
|
+
--dry-run Show plan without executing
|
|
280
|
+
--help Show this help message`
|
|
281
|
+
);
|
|
282
|
+
p.log.message(
|
|
283
|
+
`Examples
|
|
284
|
+
json-cli "please run tests"
|
|
285
|
+
json-cli "run tests and build"
|
|
286
|
+
json-cli "run tests and build" --yes
|
|
287
|
+
json-cli "git add, commit with message 'fix: bug', push"
|
|
288
|
+
json-cli "clone https://github.com/user/repo, install deps, run dev"
|
|
289
|
+
json-cli "run tests and publish" --provider openai
|
|
290
|
+
json-cli "run tests" --dry-run`
|
|
291
|
+
);
|
|
292
|
+
p.outro("Docs: https://github.com/ekaone/json-cli");
|
|
293
|
+
}
|
|
252
294
|
function parseArgs() {
|
|
253
295
|
const args = process.argv.slice(2);
|
|
296
|
+
if (args.length === 0 || args.includes("--help")) {
|
|
297
|
+
showHelp();
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
254
300
|
const providerFlag = args.indexOf("--provider");
|
|
255
301
|
const provider = providerFlag !== -1 ? args[providerFlag + 1] : "claude";
|
|
302
|
+
const yes = args.includes("--yes");
|
|
303
|
+
const dryRun = args.includes("--dry-run");
|
|
256
304
|
const prompt = args.filter(
|
|
257
305
|
(a, i) => !a.startsWith("--") && (providerFlag === -1 || i !== providerFlag + 1)
|
|
258
306
|
).join(" ");
|
|
259
307
|
if (!prompt) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
);
|
|
263
|
-
process.exit(1);
|
|
308
|
+
showHelp();
|
|
309
|
+
process.exit(0);
|
|
264
310
|
}
|
|
265
|
-
return { prompt, provider };
|
|
311
|
+
return { prompt, provider, yes, dryRun };
|
|
266
312
|
}
|
|
267
313
|
function formatStep(step) {
|
|
268
314
|
const cmd = step.type === "shell" ? `${step.command} ${step.args.join(" ")}` : `${step.type} ${step.command} ${step.args.join(" ")}`.trim();
|
|
269
315
|
return `${cmd.padEnd(35)} \u2192 ${step.description}`;
|
|
270
316
|
}
|
|
271
317
|
async function main() {
|
|
272
|
-
const { prompt, provider: providerName } = parseArgs();
|
|
273
|
-
p.intro(
|
|
318
|
+
const { prompt, provider: providerName, yes, dryRun } = parseArgs();
|
|
319
|
+
p.intro(
|
|
320
|
+
`json-cli \u2014 powered by ${providerName}${dryRun ? " (dry run)" : ""}`
|
|
321
|
+
);
|
|
274
322
|
const spinner2 = p.spinner();
|
|
275
323
|
spinner2.start("Thinking...");
|
|
276
324
|
let plan;
|
|
@@ -297,10 +345,18 @@ async function main() {
|
|
|
297
345
|
"Plan contains shell commands \u2014 review carefully before proceeding."
|
|
298
346
|
);
|
|
299
347
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
348
|
+
if (dryRun) {
|
|
349
|
+
p.outro("Dry run complete \u2014 no commands were executed.");
|
|
350
|
+
setTimeout(() => process.exit(0), 50);
|
|
351
|
+
}
|
|
352
|
+
if (!yes) {
|
|
353
|
+
const confirmed = await p.confirm({ message: "Proceed?" });
|
|
354
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
355
|
+
p.cancel("Aborted.");
|
|
356
|
+
setTimeout(() => process.exit(0), 50);
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
p.log.info("Skipping confirmation (--yes)");
|
|
304
360
|
}
|
|
305
361
|
console.log("");
|
|
306
362
|
const result = await runPlan(plan, (step, i, total) => {
|
|
@@ -313,7 +369,7 @@ async function main() {
|
|
|
313
369
|
`\u274C Failed at step ${result.failedStep?.id}: ${result.failedStep?.description}
|
|
314
370
|
${result.error ?? ""}`
|
|
315
371
|
);
|
|
316
|
-
process.exit(1);
|
|
372
|
+
setTimeout(() => process.exit(1), 50);
|
|
317
373
|
}
|
|
318
374
|
}
|
|
319
375
|
main();
|
package/dist/index.cjs
CHANGED
|
@@ -32,7 +32,20 @@ var CATALOG = {
|
|
|
32
32
|
pnpm: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
33
33
|
yarn: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
34
34
|
bun: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
35
|
-
git: [
|
|
35
|
+
git: [
|
|
36
|
+
"init",
|
|
37
|
+
"add",
|
|
38
|
+
"commit",
|
|
39
|
+
"push",
|
|
40
|
+
"pull",
|
|
41
|
+
"clone",
|
|
42
|
+
"status",
|
|
43
|
+
"log",
|
|
44
|
+
"branch",
|
|
45
|
+
"checkout",
|
|
46
|
+
"merge",
|
|
47
|
+
"stash"
|
|
48
|
+
],
|
|
36
49
|
shell: ["any"]
|
|
37
50
|
// escape hatch — always requires extra confirmation
|
|
38
51
|
};
|
|
@@ -146,8 +159,13 @@ async function runStep(step) {
|
|
|
146
159
|
});
|
|
147
160
|
return { success: true };
|
|
148
161
|
} catch (err) {
|
|
149
|
-
const
|
|
150
|
-
|
|
162
|
+
const parts = [
|
|
163
|
+
`Command: ${bin} ${args.join(" ")}`,
|
|
164
|
+
err?.exitCode ? `Exit code: ${err.exitCode}` : null,
|
|
165
|
+
err?.stderr ? `Reason: ${err.stderr.trim()}` : null,
|
|
166
|
+
!err?.stderr ? err?.message ?? String(err) : null
|
|
167
|
+
].filter(Boolean).join("\n ");
|
|
168
|
+
return { success: false, error: parts };
|
|
151
169
|
}
|
|
152
170
|
}
|
|
153
171
|
async function runPlan(plan, onStep) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/catalog.ts","../src/planner.ts","../src/runner.ts"],"sourcesContent":["/**\n * @file index.ts\n * @description Core entry point for @ekaone/json-cli.\n * @author Eka Prasetia\n * @website https://prasetia.me\n * @license MIT\n */\n\nexport { generatePlan } from \"./planner.js\";\nexport { runPlan } from \"./runner.js\";\nexport type { Plan, Step } from \"./catalog.js\";\nexport type { AIProvider } from \"./providers/types.js\";\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Allowed commands per type — the whitelist that prevents hallucination\n// ---------------------------------------------------------------------------\nexport const CATALOG = {\n npm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"ci\"],\n pnpm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n yarn: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n bun: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n git: [\"init\", \"add\", \"commit\", \"push\", \"pull\", \"clone\", \"status\", \"log\"],\n shell: [\"any\"], // escape hatch — always requires extra confirmation\n} as const;\n\nexport type CommandType = keyof typeof CATALOG;\n\n// ---------------------------------------------------------------------------\n// Zod schemas — Layer 2 defense against hallucinated output\n// ---------------------------------------------------------------------------\nexport const StepSchema = z.object({\n id: z.number(),\n type: z.enum([\"npm\", \"pnpm\", \"yarn\", \"bun\", \"git\", \"shell\"]),\n command: z.string(),\n args: z.array(z.string()).default([]),\n description: z.string(),\n cwd: z.string().optional(), // optional working directory override\n});\n\nexport const PlanSchema = z.object({\n goal: z.string(),\n steps: z.array(StepSchema).min(1).max(10),\n});\n\nexport type Step = z.infer<typeof StepSchema>;\nexport type Plan = z.infer<typeof PlanSchema>;\n\n// ---------------------------------------------------------------------------\n// Catalog validation — Layer 3: check command is in whitelist\n// ---------------------------------------------------------------------------\nexport function validateStep(step: Step): { valid: boolean; reason?: string } {\n const allowed = CATALOG[step.type];\n\n // shell type is always allowed but flagged for extra confirmation\n if (step.type === \"shell\") {\n return { valid: true };\n }\n\n if (!allowed.includes(step.command as never)) {\n return {\n valid: false,\n reason: `\"${step.command}\" is not an allowed command for type \"${step.type}\". Allowed: ${allowed.join(\", \")}`,\n };\n }\n\n return { valid: true };\n}\n\n// ---------------------------------------------------------------------------\n// Build the catalog string injected into AI system prompt\n// ---------------------------------------------------------------------------\nexport function buildCatalogPrompt(): string {\n const lines = Object.entries(CATALOG).map(([type, commands]) => {\n const list = commands[0] === \"any\" ? \"any shell command (use sparingly)\" : commands.join(\", \");\n return ` - ${type}: [${list}]`;\n });\n\n return `Allowed command types and commands:\\n${lines.join(\"\\n\")}`;\n}\n","import { buildCatalogPrompt, PlanSchema, validateStep } from \"./catalog.js\";\nimport type { AIProvider } from \"./providers/types.js\";\nimport type { Plan } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// System prompt — constrains AI to only produce catalog-valid JSON\n// ---------------------------------------------------------------------------\nfunction buildSystemPrompt(): string {\n return `You are a CLI task planner. Given a user's goal, generate a JSON execution plan.\n\n${buildCatalogPrompt()}\n\nRules:\n- ONLY use command types and commands listed above\n- Prefer pnpm over npm unless the user specifies otherwise\n- Use \"shell\" type only when no other type fits\n- Keep steps minimal — don't add unnecessary steps\n- Each step must have a clear, short description\n- NEVER generate a \"cd\" step — each step runs in a separate process so \"cd\" has no effect\n- If subsequent steps need to run inside a cloned directory, set the \"cwd\" field instead\n\nRespond ONLY with valid JSON matching this exact shape, no markdown, no explanation:\n{\n \"goal\": \"string describing the overall goal\",\n \"steps\": [\n {\n \"id\": 1,\n \"type\": \"pnpm\",\n \"command\": \"run\",\n \"args\": [\"dev\"],\n \"description\": \"Start dev server\"\n }\n ]\n}`;\n}\n\n// ---------------------------------------------------------------------------\n// Main planner function\n// ---------------------------------------------------------------------------\nexport async function generatePlan(\n userPrompt: string,\n provider: AIProvider,\n): Promise<Plan> {\n const raw = await provider.generate(userPrompt, buildSystemPrompt());\n\n // Strip markdown fences if any provider wraps output\n const cleaned = raw.replace(/```json|```/g, \"\").trim();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(cleaned);\n } catch {\n throw new Error(`AI returned invalid JSON:\\n${cleaned}`);\n }\n\n // Layer 2: Zod shape validation\n const result = PlanSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((i) => ` - ${i.path.join(\".\")}: ${i.message}`)\n .join(\"\\n\");\n throw new Error(`Plan failed schema validation:\\n${issues}`);\n }\n\n // Layer 3: Catalog whitelist validation\n for (const step of result.data.steps) {\n const check = validateStep(step);\n if (!check.valid) {\n throw new Error(\n `Step ${step.id} failed catalog validation: ${check.reason}`,\n );\n }\n }\n\n return result.data;\n}\n","import { execa } from \"execa\";\nimport type { Plan, Step } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// Build the actual shell command from a Step\n// ---------------------------------------------------------------------------\nfunction resolveCommand(step: Step): { bin: string; args: string[] } {\n if (step.type === \"shell\") {\n // shell: command is the binary, args are the args\n return { bin: step.command, args: step.args };\n }\n\n // For npm/pnpm/yarn/bun/git: binary is the type, command + args follow\n return { bin: step.type, args: [step.command, ...step.args] };\n}\n\n// ---------------------------------------------------------------------------\n// Run a single step, streaming stdout/stderr live\n// ---------------------------------------------------------------------------\nexport async function runStep(step: Step): Promise<{ success: boolean; error?: string }> {\n const { bin, args } = resolveCommand(step);\n\n try {\n await execa(bin, args, {\n cwd: step.cwd ?? process.cwd(),\n stdout: \"inherit\", // stream directly to terminal\n stderr: \"inherit\",\n });\n return { success: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return { success: false, error: message };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run the full plan, stopping on first failure\n// ---------------------------------------------------------------------------\nexport async function runPlan(\n plan: Plan,\n onStep: (step: Step, index: number, total: number) => void\n): Promise<{ success: boolean; failedStep?: Step; error?: string }> {\n const total = plan.steps.length;\n\n for (let i = 0; i < total; i++) {\n const step = plan.steps[i];\n onStep(step, i, total);\n\n const result = await runStep(step);\n\n if (!result.success) {\n return { success: false, failedStep: step, error: result.error };\n }\n }\n\n return { success: true };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/catalog.ts","../src/planner.ts","../src/runner.ts"],"sourcesContent":["/**\n * @file index.ts\n * @description Core entry point for @ekaone/json-cli.\n * @author Eka Prasetia\n * @website https://prasetia.me\n * @license MIT\n */\n\nexport { generatePlan } from \"./planner.js\";\nexport { runPlan } from \"./runner.js\";\nexport type { Plan, Step } from \"./catalog.js\";\nexport type { AIProvider } from \"./providers/types.js\";\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Allowed commands per type — the whitelist that prevents hallucination\n// ---------------------------------------------------------------------------\nexport const CATALOG = {\n npm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"ci\"],\n pnpm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n yarn: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n bun: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n git: [\n \"init\",\n \"add\",\n \"commit\",\n \"push\",\n \"pull\",\n \"clone\",\n \"status\",\n \"log\",\n \"branch\",\n \"checkout\",\n \"merge\",\n \"stash\",\n ],\n shell: [\"any\"], // escape hatch — always requires extra confirmation\n} as const;\n\nexport type CommandType = keyof typeof CATALOG;\n\n// ---------------------------------------------------------------------------\n// Zod schemas — Layer 2 defense against hallucinated output\n// ---------------------------------------------------------------------------\nexport const StepSchema = z.object({\n id: z.number(),\n type: z.enum([\"npm\", \"pnpm\", \"yarn\", \"bun\", \"git\", \"shell\"]),\n command: z.string(),\n args: z.array(z.string()).default([]),\n description: z.string(),\n cwd: z.string().optional(), // optional working directory override\n});\n\nexport const PlanSchema = z.object({\n goal: z.string(),\n steps: z.array(StepSchema).min(1).max(10),\n});\n\nexport type Step = z.infer<typeof StepSchema>;\nexport type Plan = z.infer<typeof PlanSchema>;\n\n// ---------------------------------------------------------------------------\n// Catalog validation — Layer 3: check command is in whitelist\n// ---------------------------------------------------------------------------\nexport function validateStep(step: Step): { valid: boolean; reason?: string } {\n const allowed = CATALOG[step.type];\n\n // shell type is always allowed but flagged for extra confirmation\n if (step.type === \"shell\") {\n return { valid: true };\n }\n\n if (!allowed.includes(step.command as never)) {\n return {\n valid: false,\n reason: `\"${step.command}\" is not an allowed command for type \"${step.type}\". Allowed: ${allowed.join(\", \")}`,\n };\n }\n\n return { valid: true };\n}\n\n// ---------------------------------------------------------------------------\n// Build the catalog string injected into AI system prompt\n// ---------------------------------------------------------------------------\nexport function buildCatalogPrompt(): string {\n const lines = Object.entries(CATALOG).map(([type, commands]) => {\n const list =\n commands[0] === \"any\"\n ? \"any shell command (use sparingly)\"\n : commands.join(\", \");\n return ` - ${type}: [${list}]`;\n });\n\n return `Allowed command types and commands:\\n${lines.join(\"\\n\")}`;\n}\n","import { buildCatalogPrompt, PlanSchema, validateStep } from \"./catalog.js\";\nimport type { AIProvider } from \"./providers/types.js\";\nimport type { Plan } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// System prompt — constrains AI to only produce catalog-valid JSON\n// ---------------------------------------------------------------------------\nfunction buildSystemPrompt(): string {\n return `You are a CLI task planner. Given a user's goal, generate a JSON execution plan.\n\n${buildCatalogPrompt()}\n\nRules:\n- ONLY use command types and commands listed above\n- Prefer pnpm over npm unless the user specifies otherwise\n- Use \"shell\" type only when no other type fits\n- Keep steps minimal — don't add unnecessary steps\n- Each step must have a clear, short description\n- NEVER generate a \"cd\" step — each step runs in a separate process so \"cd\" has no effect\n- If subsequent steps need to run inside a cloned directory, set the \"cwd\" field instead\n\nRespond ONLY with valid JSON matching this exact shape, no markdown, no explanation:\n{\n \"goal\": \"string describing the overall goal\",\n \"steps\": [\n {\n \"id\": 1,\n \"type\": \"pnpm\",\n \"command\": \"run\",\n \"args\": [\"dev\"],\n \"description\": \"Start dev server\"\n }\n ]\n}`;\n}\n\n// ---------------------------------------------------------------------------\n// Main planner function\n// ---------------------------------------------------------------------------\nexport async function generatePlan(\n userPrompt: string,\n provider: AIProvider,\n): Promise<Plan> {\n const raw = await provider.generate(userPrompt, buildSystemPrompt());\n\n // Strip markdown fences if any provider wraps output\n const cleaned = raw.replace(/```json|```/g, \"\").trim();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(cleaned);\n } catch {\n throw new Error(`AI returned invalid JSON:\\n${cleaned}`);\n }\n\n // Layer 2: Zod shape validation\n const result = PlanSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((i) => ` - ${i.path.join(\".\")}: ${i.message}`)\n .join(\"\\n\");\n throw new Error(`Plan failed schema validation:\\n${issues}`);\n }\n\n // Layer 3: Catalog whitelist validation\n for (const step of result.data.steps) {\n const check = validateStep(step);\n if (!check.valid) {\n throw new Error(\n `Step ${step.id} failed catalog validation: ${check.reason}`,\n );\n }\n }\n\n return result.data;\n}\n","import { execa } from \"execa\";\nimport type { Plan, Step } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// Build the actual shell command from a Step\n// ---------------------------------------------------------------------------\nfunction resolveCommand(step: Step): { bin: string; args: string[] } {\n if (step.type === \"shell\") {\n // shell: command is the binary, args are the args\n return { bin: step.command, args: step.args };\n }\n\n // For npm/pnpm/yarn/bun/git: binary is the type, command + args follow\n return { bin: step.type, args: [step.command, ...step.args] };\n}\n\n// ---------------------------------------------------------------------------\n// Run a single step, streaming stdout/stderr live\n// ---------------------------------------------------------------------------\nexport async function runStep(\n step: Step,\n): Promise<{ success: boolean; error?: string }> {\n const { bin, args } = resolveCommand(step);\n\n try {\n await execa(bin, args, {\n cwd: step.cwd ?? process.cwd(),\n stdout: \"inherit\", // stream directly to terminal\n stderr: \"inherit\",\n });\n return { success: true };\n } catch (err: any) {\n const parts = [\n `Command: ${bin} ${args.join(\" \")}`,\n err?.exitCode ? `Exit code: ${err.exitCode}` : null,\n err?.stderr ? `Reason: ${err.stderr.trim()}` : null,\n !err?.stderr ? (err?.message ?? String(err)) : null,\n ]\n .filter(Boolean)\n .join(\"\\n \");\n\n return { success: false, error: parts };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run the full plan, stopping on first failure\n// ---------------------------------------------------------------------------\nexport async function runPlan(\n plan: Plan,\n onStep: (step: Step, index: number, total: number) => void,\n): Promise<{ success: boolean; failedStep?: Step; error?: string }> {\n const total = plan.steps.length;\n\n for (let i = 0; i < total; i++) {\n const step = plan.steps[i];\n onStep(step, i, total);\n\n const result = await runStep(step);\n\n if (!result.success) {\n return { success: false, failedStep: step, error: result.error };\n }\n }\n\n return { success: true };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,20 @@ var CATALOG = {
|
|
|
5
5
|
pnpm: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
6
6
|
yarn: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
7
7
|
bun: ["install", "run", "build", "test", "publish", "add", "remove"],
|
|
8
|
-
git: [
|
|
8
|
+
git: [
|
|
9
|
+
"init",
|
|
10
|
+
"add",
|
|
11
|
+
"commit",
|
|
12
|
+
"push",
|
|
13
|
+
"pull",
|
|
14
|
+
"clone",
|
|
15
|
+
"status",
|
|
16
|
+
"log",
|
|
17
|
+
"branch",
|
|
18
|
+
"checkout",
|
|
19
|
+
"merge",
|
|
20
|
+
"stash"
|
|
21
|
+
],
|
|
9
22
|
shell: ["any"]
|
|
10
23
|
// escape hatch — always requires extra confirmation
|
|
11
24
|
};
|
|
@@ -119,8 +132,13 @@ async function runStep(step) {
|
|
|
119
132
|
});
|
|
120
133
|
return { success: true };
|
|
121
134
|
} catch (err) {
|
|
122
|
-
const
|
|
123
|
-
|
|
135
|
+
const parts = [
|
|
136
|
+
`Command: ${bin} ${args.join(" ")}`,
|
|
137
|
+
err?.exitCode ? `Exit code: ${err.exitCode}` : null,
|
|
138
|
+
err?.stderr ? `Reason: ${err.stderr.trim()}` : null,
|
|
139
|
+
!err?.stderr ? err?.message ?? String(err) : null
|
|
140
|
+
].filter(Boolean).join("\n ");
|
|
141
|
+
return { success: false, error: parts };
|
|
124
142
|
}
|
|
125
143
|
}
|
|
126
144
|
async function runPlan(plan, onStep) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/catalog.ts","../src/planner.ts","../src/runner.ts"],"sourcesContent":["import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Allowed commands per type — the whitelist that prevents hallucination\n// ---------------------------------------------------------------------------\nexport const CATALOG = {\n npm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"ci\"],\n pnpm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n yarn: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n bun: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n git: [\"init\", \"add\", \"commit\", \"push\", \"pull\", \"clone\", \"status\", \"log\"],\n shell: [\"any\"], // escape hatch — always requires extra confirmation\n} as const;\n\nexport type CommandType = keyof typeof CATALOG;\n\n// ---------------------------------------------------------------------------\n// Zod schemas — Layer 2 defense against hallucinated output\n// ---------------------------------------------------------------------------\nexport const StepSchema = z.object({\n id: z.number(),\n type: z.enum([\"npm\", \"pnpm\", \"yarn\", \"bun\", \"git\", \"shell\"]),\n command: z.string(),\n args: z.array(z.string()).default([]),\n description: z.string(),\n cwd: z.string().optional(), // optional working directory override\n});\n\nexport const PlanSchema = z.object({\n goal: z.string(),\n steps: z.array(StepSchema).min(1).max(10),\n});\n\nexport type Step = z.infer<typeof StepSchema>;\nexport type Plan = z.infer<typeof PlanSchema>;\n\n// ---------------------------------------------------------------------------\n// Catalog validation — Layer 3: check command is in whitelist\n// ---------------------------------------------------------------------------\nexport function validateStep(step: Step): { valid: boolean; reason?: string } {\n const allowed = CATALOG[step.type];\n\n // shell type is always allowed but flagged for extra confirmation\n if (step.type === \"shell\") {\n return { valid: true };\n }\n\n if (!allowed.includes(step.command as never)) {\n return {\n valid: false,\n reason: `\"${step.command}\" is not an allowed command for type \"${step.type}\". Allowed: ${allowed.join(\", \")}`,\n };\n }\n\n return { valid: true };\n}\n\n// ---------------------------------------------------------------------------\n// Build the catalog string injected into AI system prompt\n// ---------------------------------------------------------------------------\nexport function buildCatalogPrompt(): string {\n const lines = Object.entries(CATALOG).map(([type, commands]) => {\n const list = commands[0] === \"any\" ? \"any shell command (use sparingly)\" : commands.join(\", \");\n return ` - ${type}: [${list}]`;\n });\n\n return `Allowed command types and commands:\\n${lines.join(\"\\n\")}`;\n}\n","import { buildCatalogPrompt, PlanSchema, validateStep } from \"./catalog.js\";\nimport type { AIProvider } from \"./providers/types.js\";\nimport type { Plan } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// System prompt — constrains AI to only produce catalog-valid JSON\n// ---------------------------------------------------------------------------\nfunction buildSystemPrompt(): string {\n return `You are a CLI task planner. Given a user's goal, generate a JSON execution plan.\n\n${buildCatalogPrompt()}\n\nRules:\n- ONLY use command types and commands listed above\n- Prefer pnpm over npm unless the user specifies otherwise\n- Use \"shell\" type only when no other type fits\n- Keep steps minimal — don't add unnecessary steps\n- Each step must have a clear, short description\n- NEVER generate a \"cd\" step — each step runs in a separate process so \"cd\" has no effect\n- If subsequent steps need to run inside a cloned directory, set the \"cwd\" field instead\n\nRespond ONLY with valid JSON matching this exact shape, no markdown, no explanation:\n{\n \"goal\": \"string describing the overall goal\",\n \"steps\": [\n {\n \"id\": 1,\n \"type\": \"pnpm\",\n \"command\": \"run\",\n \"args\": [\"dev\"],\n \"description\": \"Start dev server\"\n }\n ]\n}`;\n}\n\n// ---------------------------------------------------------------------------\n// Main planner function\n// ---------------------------------------------------------------------------\nexport async function generatePlan(\n userPrompt: string,\n provider: AIProvider,\n): Promise<Plan> {\n const raw = await provider.generate(userPrompt, buildSystemPrompt());\n\n // Strip markdown fences if any provider wraps output\n const cleaned = raw.replace(/```json|```/g, \"\").trim();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(cleaned);\n } catch {\n throw new Error(`AI returned invalid JSON:\\n${cleaned}`);\n }\n\n // Layer 2: Zod shape validation\n const result = PlanSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((i) => ` - ${i.path.join(\".\")}: ${i.message}`)\n .join(\"\\n\");\n throw new Error(`Plan failed schema validation:\\n${issues}`);\n }\n\n // Layer 3: Catalog whitelist validation\n for (const step of result.data.steps) {\n const check = validateStep(step);\n if (!check.valid) {\n throw new Error(\n `Step ${step.id} failed catalog validation: ${check.reason}`,\n );\n }\n }\n\n return result.data;\n}\n","import { execa } from \"execa\";\nimport type { Plan, Step } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// Build the actual shell command from a Step\n// ---------------------------------------------------------------------------\nfunction resolveCommand(step: Step): { bin: string; args: string[] } {\n if (step.type === \"shell\") {\n // shell: command is the binary, args are the args\n return { bin: step.command, args: step.args };\n }\n\n // For npm/pnpm/yarn/bun/git: binary is the type, command + args follow\n return { bin: step.type, args: [step.command, ...step.args] };\n}\n\n// ---------------------------------------------------------------------------\n// Run a single step, streaming stdout/stderr live\n// ---------------------------------------------------------------------------\nexport async function runStep(step: Step): Promise<{ success: boolean; error?: string }> {\n const { bin, args } = resolveCommand(step);\n\n try {\n await execa(bin, args, {\n cwd: step.cwd ?? process.cwd(),\n stdout: \"inherit\", // stream directly to terminal\n stderr: \"inherit\",\n });\n return { success: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return { success: false, error: message };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run the full plan, stopping on first failure\n// ---------------------------------------------------------------------------\nexport async function runPlan(\n plan: Plan,\n onStep: (step: Step, index: number, total: number) => void\n): Promise<{ success: boolean; failedStep?: Step; error?: string }> {\n const total = plan.steps.length;\n\n for (let i = 0; i < total; i++) {\n const step = plan.steps[i];\n onStep(step, i, total);\n\n const result = await runStep(step);\n\n if (!result.success) {\n return { success: false, failedStep: step, error: result.error };\n }\n }\n\n return { success: true };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/catalog.ts","../src/planner.ts","../src/runner.ts"],"sourcesContent":["import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Allowed commands per type — the whitelist that prevents hallucination\n// ---------------------------------------------------------------------------\nexport const CATALOG = {\n npm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"ci\"],\n pnpm: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n yarn: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n bun: [\"install\", \"run\", \"build\", \"test\", \"publish\", \"add\", \"remove\"],\n git: [\n \"init\",\n \"add\",\n \"commit\",\n \"push\",\n \"pull\",\n \"clone\",\n \"status\",\n \"log\",\n \"branch\",\n \"checkout\",\n \"merge\",\n \"stash\",\n ],\n shell: [\"any\"], // escape hatch — always requires extra confirmation\n} as const;\n\nexport type CommandType = keyof typeof CATALOG;\n\n// ---------------------------------------------------------------------------\n// Zod schemas — Layer 2 defense against hallucinated output\n// ---------------------------------------------------------------------------\nexport const StepSchema = z.object({\n id: z.number(),\n type: z.enum([\"npm\", \"pnpm\", \"yarn\", \"bun\", \"git\", \"shell\"]),\n command: z.string(),\n args: z.array(z.string()).default([]),\n description: z.string(),\n cwd: z.string().optional(), // optional working directory override\n});\n\nexport const PlanSchema = z.object({\n goal: z.string(),\n steps: z.array(StepSchema).min(1).max(10),\n});\n\nexport type Step = z.infer<typeof StepSchema>;\nexport type Plan = z.infer<typeof PlanSchema>;\n\n// ---------------------------------------------------------------------------\n// Catalog validation — Layer 3: check command is in whitelist\n// ---------------------------------------------------------------------------\nexport function validateStep(step: Step): { valid: boolean; reason?: string } {\n const allowed = CATALOG[step.type];\n\n // shell type is always allowed but flagged for extra confirmation\n if (step.type === \"shell\") {\n return { valid: true };\n }\n\n if (!allowed.includes(step.command as never)) {\n return {\n valid: false,\n reason: `\"${step.command}\" is not an allowed command for type \"${step.type}\". Allowed: ${allowed.join(\", \")}`,\n };\n }\n\n return { valid: true };\n}\n\n// ---------------------------------------------------------------------------\n// Build the catalog string injected into AI system prompt\n// ---------------------------------------------------------------------------\nexport function buildCatalogPrompt(): string {\n const lines = Object.entries(CATALOG).map(([type, commands]) => {\n const list =\n commands[0] === \"any\"\n ? \"any shell command (use sparingly)\"\n : commands.join(\", \");\n return ` - ${type}: [${list}]`;\n });\n\n return `Allowed command types and commands:\\n${lines.join(\"\\n\")}`;\n}\n","import { buildCatalogPrompt, PlanSchema, validateStep } from \"./catalog.js\";\nimport type { AIProvider } from \"./providers/types.js\";\nimport type { Plan } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// System prompt — constrains AI to only produce catalog-valid JSON\n// ---------------------------------------------------------------------------\nfunction buildSystemPrompt(): string {\n return `You are a CLI task planner. Given a user's goal, generate a JSON execution plan.\n\n${buildCatalogPrompt()}\n\nRules:\n- ONLY use command types and commands listed above\n- Prefer pnpm over npm unless the user specifies otherwise\n- Use \"shell\" type only when no other type fits\n- Keep steps minimal — don't add unnecessary steps\n- Each step must have a clear, short description\n- NEVER generate a \"cd\" step — each step runs in a separate process so \"cd\" has no effect\n- If subsequent steps need to run inside a cloned directory, set the \"cwd\" field instead\n\nRespond ONLY with valid JSON matching this exact shape, no markdown, no explanation:\n{\n \"goal\": \"string describing the overall goal\",\n \"steps\": [\n {\n \"id\": 1,\n \"type\": \"pnpm\",\n \"command\": \"run\",\n \"args\": [\"dev\"],\n \"description\": \"Start dev server\"\n }\n ]\n}`;\n}\n\n// ---------------------------------------------------------------------------\n// Main planner function\n// ---------------------------------------------------------------------------\nexport async function generatePlan(\n userPrompt: string,\n provider: AIProvider,\n): Promise<Plan> {\n const raw = await provider.generate(userPrompt, buildSystemPrompt());\n\n // Strip markdown fences if any provider wraps output\n const cleaned = raw.replace(/```json|```/g, \"\").trim();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(cleaned);\n } catch {\n throw new Error(`AI returned invalid JSON:\\n${cleaned}`);\n }\n\n // Layer 2: Zod shape validation\n const result = PlanSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((i) => ` - ${i.path.join(\".\")}: ${i.message}`)\n .join(\"\\n\");\n throw new Error(`Plan failed schema validation:\\n${issues}`);\n }\n\n // Layer 3: Catalog whitelist validation\n for (const step of result.data.steps) {\n const check = validateStep(step);\n if (!check.valid) {\n throw new Error(\n `Step ${step.id} failed catalog validation: ${check.reason}`,\n );\n }\n }\n\n return result.data;\n}\n","import { execa } from \"execa\";\nimport type { Plan, Step } from \"./catalog.js\";\n\n// ---------------------------------------------------------------------------\n// Build the actual shell command from a Step\n// ---------------------------------------------------------------------------\nfunction resolveCommand(step: Step): { bin: string; args: string[] } {\n if (step.type === \"shell\") {\n // shell: command is the binary, args are the args\n return { bin: step.command, args: step.args };\n }\n\n // For npm/pnpm/yarn/bun/git: binary is the type, command + args follow\n return { bin: step.type, args: [step.command, ...step.args] };\n}\n\n// ---------------------------------------------------------------------------\n// Run a single step, streaming stdout/stderr live\n// ---------------------------------------------------------------------------\nexport async function runStep(\n step: Step,\n): Promise<{ success: boolean; error?: string }> {\n const { bin, args } = resolveCommand(step);\n\n try {\n await execa(bin, args, {\n cwd: step.cwd ?? process.cwd(),\n stdout: \"inherit\", // stream directly to terminal\n stderr: \"inherit\",\n });\n return { success: true };\n } catch (err: any) {\n const parts = [\n `Command: ${bin} ${args.join(\" \")}`,\n err?.exitCode ? `Exit code: ${err.exitCode}` : null,\n err?.stderr ? `Reason: ${err.stderr.trim()}` : null,\n !err?.stderr ? (err?.message ?? String(err)) : null,\n ]\n .filter(Boolean)\n .join(\"\\n \");\n\n return { success: false, error: parts };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run the full plan, stopping on first failure\n// ---------------------------------------------------------------------------\nexport async function runPlan(\n plan: Plan,\n onStep: (step: Step, index: number, total: number) => void,\n): Promise<{ success: boolean; failedStep?: Step; error?: string }> {\n const total = plan.steps.length;\n\n for (let i = 0; i < total; i++) {\n const step = plan.steps[i];\n onStep(step, i, total);\n\n const result = await runStep(step);\n\n if (!result.success) {\n return { success: false, failedStep: step, error: result.error };\n }\n }\n\n return { success: true };\n}\n"],"mappings":"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","names":[]}
|