@benzotti/jedi 0.1.2 → 0.1.4

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.js CHANGED
@@ -1,6 +1,35 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
3
5
  var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
4
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
5
34
  var __returnValue = (v) => v;
6
35
  function __exportSetter(name, newValue) {
@@ -7676,6 +7705,118 @@ var require_public_api = __commonJS((exports) => {
7676
7705
  exports.stringify = stringify;
7677
7706
  });
7678
7707
 
7708
+ // node_modules/yaml/dist/index.js
7709
+ var require_dist = __commonJS((exports) => {
7710
+ var composer = require_composer();
7711
+ var Document = require_Document();
7712
+ var Schema = require_Schema();
7713
+ var errors = require_errors();
7714
+ var Alias = require_Alias();
7715
+ var identity = require_identity();
7716
+ var Pair = require_Pair();
7717
+ var Scalar = require_Scalar();
7718
+ var YAMLMap = require_YAMLMap();
7719
+ var YAMLSeq = require_YAMLSeq();
7720
+ var cst = require_cst();
7721
+ var lexer = require_lexer();
7722
+ var lineCounter = require_line_counter();
7723
+ var parser = require_parser();
7724
+ var publicApi = require_public_api();
7725
+ var visit = require_visit();
7726
+ exports.Composer = composer.Composer;
7727
+ exports.Document = Document.Document;
7728
+ exports.Schema = Schema.Schema;
7729
+ exports.YAMLError = errors.YAMLError;
7730
+ exports.YAMLParseError = errors.YAMLParseError;
7731
+ exports.YAMLWarning = errors.YAMLWarning;
7732
+ exports.Alias = Alias.Alias;
7733
+ exports.isAlias = identity.isAlias;
7734
+ exports.isCollection = identity.isCollection;
7735
+ exports.isDocument = identity.isDocument;
7736
+ exports.isMap = identity.isMap;
7737
+ exports.isNode = identity.isNode;
7738
+ exports.isPair = identity.isPair;
7739
+ exports.isScalar = identity.isScalar;
7740
+ exports.isSeq = identity.isSeq;
7741
+ exports.Pair = Pair.Pair;
7742
+ exports.Scalar = Scalar.Scalar;
7743
+ exports.YAMLMap = YAMLMap.YAMLMap;
7744
+ exports.YAMLSeq = YAMLSeq.YAMLSeq;
7745
+ exports.CST = cst;
7746
+ exports.Lexer = lexer.Lexer;
7747
+ exports.LineCounter = lineCounter.LineCounter;
7748
+ exports.Parser = parser.Parser;
7749
+ exports.parse = publicApi.parse;
7750
+ exports.parseAllDocuments = publicApi.parseAllDocuments;
7751
+ exports.parseDocument = publicApi.parseDocument;
7752
+ exports.stringify = publicApi.stringify;
7753
+ exports.visit = visit.visit;
7754
+ exports.visitAsync = visit.visitAsync;
7755
+ });
7756
+
7757
+ // src/utils/git.ts
7758
+ var exports_git = {};
7759
+ __export(exports_git, {
7760
+ gitStatus: () => gitStatus,
7761
+ gitRoot: () => gitRoot,
7762
+ gitMergeBase: () => gitMergeBase,
7763
+ gitLog: () => gitLog,
7764
+ gitDiffNames: () => gitDiffNames,
7765
+ gitDiff: () => gitDiff,
7766
+ gitBranch: () => gitBranch,
7767
+ exec: () => exec
7768
+ });
7769
+ async function exec(cmd, cwd) {
7770
+ const proc = Bun.spawn(cmd, {
7771
+ cwd: cwd ?? process.cwd(),
7772
+ stdout: "pipe",
7773
+ stderr: "pipe"
7774
+ });
7775
+ const stdout2 = await new Response(proc.stdout).text();
7776
+ const exitCode = await proc.exited;
7777
+ return { stdout: stdout2.trim(), exitCode };
7778
+ }
7779
+ async function gitDiff(staged) {
7780
+ const args = ["git", "diff"];
7781
+ if (staged)
7782
+ args.push("--cached");
7783
+ const { stdout: stdout2 } = await exec(args);
7784
+ return stdout2;
7785
+ }
7786
+ async function gitDiffNames(staged) {
7787
+ const args = ["git", "diff", "--name-only"];
7788
+ if (staged)
7789
+ args.push("--cached");
7790
+ const { stdout: stdout2 } = await exec(args);
7791
+ return stdout2 ? stdout2.split(`
7792
+ `) : [];
7793
+ }
7794
+ async function gitLog(range) {
7795
+ const args = ["git", "log", "--oneline"];
7796
+ if (range)
7797
+ args.push(range);
7798
+ else
7799
+ args.push("-20");
7800
+ const { stdout: stdout2 } = await exec(args);
7801
+ return stdout2;
7802
+ }
7803
+ async function gitBranch() {
7804
+ const { stdout: stdout2 } = await exec(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
7805
+ return stdout2;
7806
+ }
7807
+ async function gitRoot() {
7808
+ const { stdout: stdout2 } = await exec(["git", "rev-parse", "--show-toplevel"]);
7809
+ return stdout2;
7810
+ }
7811
+ async function gitStatus() {
7812
+ const { stdout: stdout2 } = await exec(["git", "status", "--porcelain"]);
7813
+ return stdout2;
7814
+ }
7815
+ async function gitMergeBase(branch) {
7816
+ const { stdout: stdout2 } = await exec(["git", "merge-base", "HEAD", branch]);
7817
+ return stdout2;
7818
+ }
7819
+
7679
7820
  // node_modules/consola/dist/core.mjs
7680
7821
  var LogLevels = {
7681
7822
  silent: Number.NEGATIVE_INFINITY,
@@ -9203,30 +9344,73 @@ var initCommand = defineCommand({
9203
9344
  type: "boolean",
9204
9345
  description: "Overwrite existing files",
9205
9346
  default: false
9347
+ },
9348
+ ci: {
9349
+ type: "boolean",
9350
+ description: "Headless CI mode (no prompts, JSON output)",
9351
+ default: false
9352
+ },
9353
+ storage: {
9354
+ type: "string",
9355
+ description: "Storage adapter to configure (default: fs)"
9356
+ },
9357
+ "storage-path": {
9358
+ type: "string",
9359
+ description: "Storage base path (default: .jdi/persistence/)"
9206
9360
  }
9207
9361
  },
9208
9362
  async run({ args }) {
9209
9363
  const cwd = process.cwd();
9210
9364
  const projectType = await detectProjectType(cwd);
9211
- consola.info(`Detected project type: ${projectType}`);
9212
- consola.start("Initialising JDI...");
9365
+ if (!args.ci) {
9366
+ consola.info(`Detected project type: ${projectType}`);
9367
+ consola.start("Initialising JDI...");
9368
+ }
9213
9369
  const dirs = [
9214
9370
  ".claude/commands/jdi",
9215
9371
  ".jdi/plans",
9216
9372
  ".jdi/research",
9217
9373
  ".jdi/codebase",
9218
9374
  ".jdi/reviews",
9219
- ".jdi/config"
9375
+ ".jdi/config",
9376
+ ".jdi/persistence"
9220
9377
  ];
9221
9378
  for (const dir of dirs) {
9222
9379
  await Bun.write(join3(cwd, dir, ".gitkeep"), "");
9223
9380
  }
9224
9381
  await copyFrameworkFiles(cwd, projectType, args.force);
9225
- consola.success("JDI initialised successfully!");
9226
- consola.info("");
9227
- consola.info("Get started:");
9228
- consola.info(' /jdi:create-plan "your feature"');
9229
- consola.info(' /jdi:quick "small fix"');
9382
+ if (args.storage || args["storage-path"]) {
9383
+ const { parse, stringify } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
9384
+ const configPath = join3(cwd, ".jdi", "config", "jdi-config.yaml");
9385
+ let config = {};
9386
+ try {
9387
+ const existing = await Bun.file(configPath).text();
9388
+ config = parse(existing) ?? {};
9389
+ } catch {}
9390
+ config.storage = {
9391
+ adapter: args.storage ?? "fs",
9392
+ base_path: args["storage-path"] ?? ".jdi/persistence/"
9393
+ };
9394
+ await Bun.write(configPath, stringify(config));
9395
+ }
9396
+ if (args.ci) {
9397
+ const result = {
9398
+ status: "initialised",
9399
+ project_type: projectType,
9400
+ cwd,
9401
+ storage: {
9402
+ adapter: args.storage ?? "fs",
9403
+ base_path: args["storage-path"] ?? ".jdi/persistence/"
9404
+ }
9405
+ };
9406
+ console.log(JSON.stringify(result));
9407
+ } else {
9408
+ consola.success("JDI initialised successfully!");
9409
+ consola.info("");
9410
+ consola.info("Get started:");
9411
+ consola.info(' /jdi:create-plan "your feature"');
9412
+ consola.info(' /jdi:quick "small fix"');
9413
+ }
9230
9414
  }
9231
9415
  });
9232
9416
 
@@ -9234,69 +9418,292 @@ var initCommand = defineCommand({
9234
9418
  import { resolve as resolve2 } from "path";
9235
9419
 
9236
9420
  // src/utils/adapter.ts
9421
+ var import_yaml = __toESM(require_dist(), 1);
9237
9422
  import { join as join4 } from "path";
9238
9423
  import { existsSync as existsSync3 } from "fs";
9239
-
9240
- // node_modules/yaml/dist/index.js
9241
- var composer = require_composer();
9242
- var Document = require_Document();
9243
- var Schema = require_Schema();
9244
- var errors = require_errors();
9245
- var Alias = require_Alias();
9246
- var identity = require_identity();
9247
- var Pair = require_Pair();
9248
- var Scalar = require_Scalar();
9249
- var YAMLMap = require_YAMLMap();
9250
- var YAMLSeq = require_YAMLSeq();
9251
- var cst = require_cst();
9252
- var lexer = require_lexer();
9253
- var lineCounter = require_line_counter();
9254
- var parser = require_parser();
9255
- var publicApi = require_public_api();
9256
- var visit = require_visit();
9257
- var $Composer = composer.Composer;
9258
- var $Document = Document.Document;
9259
- var $Schema = Schema.Schema;
9260
- var $YAMLError = errors.YAMLError;
9261
- var $YAMLParseError = errors.YAMLParseError;
9262
- var $YAMLWarning = errors.YAMLWarning;
9263
- var $Alias = Alias.Alias;
9264
- var $isAlias = identity.isAlias;
9265
- var $isCollection = identity.isCollection;
9266
- var $isDocument = identity.isDocument;
9267
- var $isMap = identity.isMap;
9268
- var $isNode = identity.isNode;
9269
- var $isPair = identity.isPair;
9270
- var $isScalar = identity.isScalar;
9271
- var $isSeq = identity.isSeq;
9272
- var $Pair = Pair.Pair;
9273
- var $Scalar = Scalar.Scalar;
9274
- var $YAMLMap = YAMLMap.YAMLMap;
9275
- var $YAMLSeq = YAMLSeq.YAMLSeq;
9276
- var $Lexer = lexer.Lexer;
9277
- var $LineCounter = lineCounter.LineCounter;
9278
- var $Parser = parser.Parser;
9279
- var $parse = publicApi.parse;
9280
- var $parseAllDocuments = publicApi.parseAllDocuments;
9281
- var $parseDocument = publicApi.parseDocument;
9282
- var $stringify = publicApi.stringify;
9283
- var $visit = visit.visit;
9284
- var $visitAsync = visit.visitAsync;
9285
-
9286
- // src/utils/adapter.ts
9287
9424
  async function readAdapter(cwd) {
9288
9425
  const adapterPath = join4(cwd, ".jdi", "config", "adapter.yaml");
9289
9426
  if (!existsSync3(adapterPath))
9290
9427
  return null;
9291
9428
  const content = await Bun.file(adapterPath).text();
9292
- return $parse(content);
9429
+ return import_yaml.parse(content);
9430
+ }
9431
+
9432
+ // src/utils/claude.ts
9433
+ var PROMPT_LENGTH_THRESHOLD = 1e5;
9434
+ async function findClaude() {
9435
+ const path = Bun.which("claude");
9436
+ if (path)
9437
+ return path;
9438
+ const { exec: exec2 } = await Promise.resolve().then(() => exports_git);
9439
+ const { stdout: stdout2, exitCode } = await exec2(["which", "claude"]);
9440
+ if (exitCode === 0 && stdout2)
9441
+ return stdout2;
9442
+ throw new Error("Claude CLI not found. Install it from https://docs.anthropic.com/en/docs/claude-code");
9443
+ }
9444
+ var lastEventType = "";
9445
+ function formatStreamEvent(event) {
9446
+ if (event.type === "assistant" && event.message?.content) {
9447
+ const parts = [];
9448
+ for (const block of event.message.content) {
9449
+ if (block.type === "text" && block.text) {
9450
+ const prefix = lastEventType === "tool" ? `
9451
+ ` : "";
9452
+ parts.push(prefix + block.text.trim());
9453
+ lastEventType = "text";
9454
+ } else if (block.type === "tool_use") {
9455
+ const name = block.name ?? "tool";
9456
+ const input = block.input;
9457
+ let detail = "";
9458
+ if (input?.file_path) {
9459
+ const segments = input.file_path.split("/");
9460
+ detail = ` \u2192 ${segments.slice(-3).join("/")}`;
9461
+ } else if (name === "Bash" && input?.command) {
9462
+ detail = ` \u2192 ${input.command.slice(0, 60)}`;
9463
+ } else if (input?.pattern) {
9464
+ detail = ` \u2192 ${input.pattern}`;
9465
+ }
9466
+ parts.push(` \u26A1 ${name}${detail}`);
9467
+ lastEventType = "tool";
9468
+ }
9469
+ }
9470
+ if (parts.length > 0)
9471
+ return parts.join(`
9472
+ `) + `
9473
+ `;
9474
+ }
9475
+ if (event.type === "result" && event.subtype === "error_tool_result") {
9476
+ lastEventType = "error";
9477
+ return ` \u274C Tool error
9478
+ `;
9479
+ }
9480
+ return null;
9481
+ }
9482
+ async function spawnClaude(prompt2, opts) {
9483
+ const claudePath = await findClaude();
9484
+ const args = [
9485
+ claudePath,
9486
+ "-p",
9487
+ "--verbose",
9488
+ "--output-format",
9489
+ "stream-json",
9490
+ "--permission-mode",
9491
+ opts?.permissionMode ?? "acceptEdits"
9492
+ ];
9493
+ if (opts?.allowedTools) {
9494
+ for (const tool of opts.allowedTools) {
9495
+ args.push("--allowedTools", tool);
9496
+ }
9497
+ }
9498
+ if (opts?.model) {
9499
+ args.push("--model", opts.model);
9500
+ }
9501
+ const useStdin = prompt2.length >= PROMPT_LENGTH_THRESHOLD;
9502
+ if (!useStdin) {
9503
+ args.push(prompt2);
9504
+ }
9505
+ consola.start(`Launching Claude Code...
9506
+ `);
9507
+ const proc = Bun.spawn(args, {
9508
+ cwd: opts?.cwd ?? process.cwd(),
9509
+ stdout: "pipe",
9510
+ stderr: "inherit",
9511
+ stdin: useStdin ? "pipe" : "ignore"
9512
+ });
9513
+ if (useStdin) {
9514
+ proc.stdin.write(prompt2);
9515
+ proc.stdin.end();
9516
+ }
9517
+ const reader = proc.stdout.getReader();
9518
+ const decoder = new TextDecoder;
9519
+ let buffer = "";
9520
+ try {
9521
+ while (true) {
9522
+ const { done, value } = await reader.read();
9523
+ if (done)
9524
+ break;
9525
+ buffer += decoder.decode(value, { stream: true });
9526
+ const lines = buffer.split(`
9527
+ `);
9528
+ buffer = lines.pop() ?? "";
9529
+ for (const line of lines) {
9530
+ const trimmed = line.trim();
9531
+ if (!trimmed)
9532
+ continue;
9533
+ try {
9534
+ const event = JSON.parse(trimmed);
9535
+ const output = formatStreamEvent(event);
9536
+ if (output)
9537
+ process.stdout.write(output);
9538
+ } catch {}
9539
+ }
9540
+ }
9541
+ if (buffer.trim()) {
9542
+ try {
9543
+ const event = JSON.parse(buffer.trim());
9544
+ const output = formatStreamEvent(event);
9545
+ if (output)
9546
+ process.stdout.write(output);
9547
+ } catch {}
9548
+ }
9549
+ } catch {}
9550
+ const exitCode = await proc.exited;
9551
+ process.stdout.write(`
9552
+ `);
9553
+ return { exitCode };
9554
+ }
9555
+
9556
+ // src/storage/index.ts
9557
+ var import_yaml2 = __toESM(require_dist(), 1);
9558
+ import { join as join6 } from "path";
9559
+ import { existsSync as existsSync5 } from "fs";
9560
+
9561
+ // src/storage/fs-storage.ts
9562
+ import { join as join5 } from "path";
9563
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
9564
+
9565
+ class FsStorage {
9566
+ basePath;
9567
+ constructor(basePath = ".jdi/persistence") {
9568
+ this.basePath = basePath;
9569
+ }
9570
+ async load(key) {
9571
+ const filePath = join5(this.basePath, `${key}.md`);
9572
+ if (!existsSync4(filePath))
9573
+ return null;
9574
+ return Bun.file(filePath).text();
9575
+ }
9576
+ async save(key, content) {
9577
+ if (!existsSync4(this.basePath)) {
9578
+ mkdirSync2(this.basePath, { recursive: true });
9579
+ }
9580
+ const filePath = join5(this.basePath, `${key}.md`);
9581
+ await Bun.write(filePath, content);
9582
+ }
9583
+ }
9584
+
9585
+ // src/storage/index.ts
9586
+ async function createStorage(cwd, config) {
9587
+ let adapter = config?.adapter ?? "fs";
9588
+ let basePath = config?.basePath;
9589
+ if (!config?.adapter && !config?.basePath) {
9590
+ const configPath = join6(cwd, ".jdi", "config", "jdi-config.yaml");
9591
+ if (existsSync5(configPath)) {
9592
+ const content = await Bun.file(configPath).text();
9593
+ const parsed = import_yaml2.parse(content);
9594
+ if (parsed?.storage?.adapter)
9595
+ adapter = parsed.storage.adapter;
9596
+ if (parsed?.storage?.base_path)
9597
+ basePath = parsed.storage.base_path;
9598
+ }
9599
+ }
9600
+ if (adapter === "fs") {
9601
+ const resolvedPath = basePath ? join6(cwd, basePath) : join6(cwd, ".jdi", "persistence");
9602
+ return new FsStorage(resolvedPath);
9603
+ }
9604
+ const adapterPath = join6(cwd, adapter);
9605
+ if (!existsSync5(adapterPath)) {
9606
+ throw new Error(`Storage adapter not found: ${adapterPath}
9607
+ ` + `Set storage.adapter in .jdi/config/jdi-config.yaml to "fs" or a path to a custom adapter module.`);
9608
+ }
9609
+ try {
9610
+ const mod = await import(adapterPath);
9611
+ const AdapterClass = mod.default ?? mod.Storage ?? mod[Object.keys(mod)[0]];
9612
+ if (!AdapterClass || typeof AdapterClass !== "function") {
9613
+ throw new Error(`Storage adapter at ${adapterPath} must export a class as default export.`);
9614
+ }
9615
+ const instance = new AdapterClass({ basePath, cwd });
9616
+ if (typeof instance.load !== "function" || typeof instance.save !== "function") {
9617
+ throw new Error(`Storage adapter at ${adapterPath} must implement JediStorage (load and save methods).`);
9618
+ }
9619
+ return instance;
9620
+ } catch (err) {
9621
+ if (err.message?.includes("Storage adapter"))
9622
+ throw err;
9623
+ throw new Error(`Failed to load storage adapter from ${adapterPath}: ${err.message}`);
9624
+ }
9625
+ }
9626
+
9627
+ // src/utils/storage-lifecycle.ts
9628
+ import { join as join7 } from "path";
9629
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
9630
+ async function loadPersistedState(cwd, storage) {
9631
+ let learningsPath = null;
9632
+ let codebaseIndexPath = null;
9633
+ const learnings = await storage.load("learnings");
9634
+ if (learnings) {
9635
+ const dir = join7(cwd, ".jdi", "framework", "learnings");
9636
+ if (!existsSync6(dir))
9637
+ mkdirSync3(dir, { recursive: true });
9638
+ learningsPath = join7(dir, "_consolidated.md");
9639
+ await Bun.write(learningsPath, learnings);
9640
+ }
9641
+ const codebaseIndex = await storage.load("codebase-index");
9642
+ if (codebaseIndex) {
9643
+ const dir = join7(cwd, ".jdi", "codebase");
9644
+ if (!existsSync6(dir))
9645
+ mkdirSync3(dir, { recursive: true });
9646
+ codebaseIndexPath = join7(dir, "INDEX.md");
9647
+ await Bun.write(codebaseIndexPath, codebaseIndex);
9648
+ }
9649
+ return { learningsPath, codebaseIndexPath };
9650
+ }
9651
+ async function savePersistedState(cwd, storage) {
9652
+ let learningsSaved = false;
9653
+ let codebaseIndexSaved = false;
9654
+ const learningsDir = join7(cwd, ".jdi", "framework", "learnings");
9655
+ if (existsSync6(learningsDir)) {
9656
+ const merged = await mergeLearningFiles(learningsDir);
9657
+ if (merged) {
9658
+ await storage.save("learnings", merged);
9659
+ learningsSaved = true;
9660
+ }
9661
+ }
9662
+ const indexPath = join7(cwd, ".jdi", "codebase", "INDEX.md");
9663
+ if (existsSync6(indexPath)) {
9664
+ const content = await Bun.file(indexPath).text();
9665
+ if (content.trim()) {
9666
+ await storage.save("codebase-index", content);
9667
+ codebaseIndexSaved = true;
9668
+ }
9669
+ }
9670
+ return { learningsSaved, codebaseIndexSaved };
9671
+ }
9672
+ async function mergeLearningFiles(dir) {
9673
+ const categories = ["general", "backend", "frontend", "testing", "devops"];
9674
+ const sections = [];
9675
+ for (const category of categories) {
9676
+ const filePath = join7(dir, `${category}.md`);
9677
+ if (!existsSync6(filePath))
9678
+ continue;
9679
+ const content = await Bun.file(filePath).text();
9680
+ const trimmed = content.trim();
9681
+ const lines = trimmed.split(`
9682
+ `).filter((l2) => l2.trim() && !l2.startsWith("#") && !l2.startsWith("<!--"));
9683
+ if (lines.length === 0)
9684
+ continue;
9685
+ sections.push(trimmed);
9686
+ }
9687
+ const consolidatedPath = join7(dir, "_consolidated.md");
9688
+ if (existsSync6(consolidatedPath)) {
9689
+ const content = await Bun.file(consolidatedPath).text();
9690
+ const trimmed = content.trim();
9691
+ if (trimmed && !sections.some((s2) => s2.includes(trimmed))) {
9692
+ sections.push(trimmed);
9693
+ }
9694
+ }
9695
+ return sections.length > 0 ? sections.join(`
9696
+
9697
+ ---
9698
+
9699
+ `) : null;
9293
9700
  }
9294
9701
 
9295
9702
  // src/commands/plan.ts
9296
9703
  var planCommand = defineCommand({
9297
9704
  meta: {
9298
9705
  name: "plan",
9299
- description: "Generate a planning prompt for Claude Code"
9706
+ description: "Plan a feature using Claude Code"
9300
9707
  },
9301
9708
  args: {
9302
9709
  description: {
@@ -9307,6 +9714,11 @@ var planCommand = defineCommand({
9307
9714
  output: {
9308
9715
  type: "string",
9309
9716
  description: "Write prompt to file instead of stdout"
9717
+ },
9718
+ print: {
9719
+ type: "boolean",
9720
+ description: "Print the prompt to stdout instead of executing",
9721
+ default: false
9310
9722
  }
9311
9723
  },
9312
9724
  async run({ args }) {
@@ -9336,29 +9748,61 @@ var planCommand = defineCommand({
9336
9748
  if (args.output) {
9337
9749
  await Bun.write(resolve2(cwd, args.output), prompt2);
9338
9750
  consola.success(`Prompt written to ${args.output}`);
9339
- } else {
9751
+ } else if (args.print) {
9340
9752
  console.log(prompt2);
9753
+ } else {
9754
+ const storage = await createStorage(cwd);
9755
+ await loadPersistedState(cwd, storage);
9756
+ const { exitCode } = await spawnClaude(prompt2, { cwd });
9757
+ await savePersistedState(cwd, storage);
9758
+ if (exitCode !== 0) {
9759
+ consola.error(`Claude exited with code ${exitCode}`);
9760
+ process.exit(exitCode);
9761
+ }
9341
9762
  }
9342
9763
  }
9343
9764
  });
9344
9765
 
9345
9766
  // src/commands/implement.ts
9346
9767
  import { resolve as resolve3 } from "path";
9768
+
9769
+ // src/utils/state.ts
9770
+ var import_yaml3 = __toESM(require_dist(), 1);
9771
+ import { join as join8 } from "path";
9772
+ import { existsSync as existsSync7 } from "fs";
9773
+ async function readState(cwd) {
9774
+ const statePath = join8(cwd, ".jdi", "config", "state.yaml");
9775
+ if (!existsSync7(statePath))
9776
+ return null;
9777
+ const content = await Bun.file(statePath).text();
9778
+ return import_yaml3.parse(content);
9779
+ }
9780
+ async function writeState(cwd, state) {
9781
+ const statePath = join8(cwd, ".jdi", "config", "state.yaml");
9782
+ await Bun.write(statePath, import_yaml3.stringify(state));
9783
+ }
9784
+
9785
+ // src/commands/implement.ts
9347
9786
  var implementCommand = defineCommand({
9348
9787
  meta: {
9349
9788
  name: "implement",
9350
- description: "Generate an implementation prompt for Claude Code"
9789
+ description: "Execute an implementation plan using Claude Code"
9351
9790
  },
9352
9791
  args: {
9353
9792
  plan: {
9354
9793
  type: "positional",
9355
- description: "Path to the PLAN.md file to execute",
9356
- required: true
9794
+ description: "Path to the PLAN.md file (auto-detected from state if omitted)",
9795
+ required: false
9357
9796
  },
9358
9797
  output: {
9359
9798
  type: "string",
9360
9799
  description: "Write prompt to file instead of stdout"
9361
9800
  },
9801
+ print: {
9802
+ type: "boolean",
9803
+ description: "Print the prompt to stdout instead of executing",
9804
+ default: false
9805
+ },
9362
9806
  team: {
9363
9807
  type: "boolean",
9364
9808
  description: "Force Agent Teams mode",
@@ -9372,6 +9816,16 @@ var implementCommand = defineCommand({
9372
9816
  },
9373
9817
  async run({ args }) {
9374
9818
  const cwd = process.cwd();
9819
+ let planPath = args.plan;
9820
+ if (!planPath) {
9821
+ const state = await readState(cwd);
9822
+ planPath = state?.current_plan?.path ?? null;
9823
+ if (!planPath) {
9824
+ consola.error("No plan specified and no current plan found in state. Run `jdi plan` first or provide a plan path.");
9825
+ process.exit(1);
9826
+ }
9827
+ consola.info(`Using current plan: ${planPath}`);
9828
+ }
9375
9829
  const baseProtocol = resolve3(cwd, ".jdi/framework/components/meta/AgentBase.md");
9376
9830
  const complexityRouter = resolve3(cwd, ".jdi/framework/components/meta/ComplexityRouter.md");
9377
9831
  const orchestration = resolve3(cwd, ".jdi/framework/components/meta/AgentTeamsOrchestration.md");
@@ -9394,7 +9848,7 @@ Override: --single (force single-agent mode)` : "";
9394
9848
  `- Working directory: ${cwd}`,
9395
9849
  ``,
9396
9850
  `## Task`,
9397
- `Execute implementation plan: ${resolve3(cwd, args.plan)}${overrideFlag}`,
9851
+ `Execute implementation plan: ${resolve3(cwd, planPath)}${overrideFlag}`,
9398
9852
  ``,
9399
9853
  `Follow the implement-plan orchestration:`,
9400
9854
  `1. Read codebase context (.jdi/codebase/SUMMARY.md if exists)`,
@@ -9410,27 +9864,21 @@ Override: --single (force single-agent mode)` : "";
9410
9864
  if (args.output) {
9411
9865
  await Bun.write(resolve3(cwd, args.output), prompt2);
9412
9866
  consola.success(`Prompt written to ${args.output}`);
9413
- } else {
9867
+ } else if (args.print) {
9414
9868
  console.log(prompt2);
9869
+ } else {
9870
+ const storage = await createStorage(cwd);
9871
+ await loadPersistedState(cwd, storage);
9872
+ const { exitCode } = await spawnClaude(prompt2, { cwd });
9873
+ await savePersistedState(cwd, storage);
9874
+ if (exitCode !== 0) {
9875
+ consola.error(`Claude exited with code ${exitCode}`);
9876
+ process.exit(exitCode);
9877
+ }
9415
9878
  }
9416
9879
  }
9417
9880
  });
9418
9881
 
9419
- // src/utils/state.ts
9420
- import { join as join5 } from "path";
9421
- import { existsSync as existsSync4 } from "fs";
9422
- async function readState(cwd) {
9423
- const statePath = join5(cwd, ".jdi", "config", "state.yaml");
9424
- if (!existsSync4(statePath))
9425
- return null;
9426
- const content = await Bun.file(statePath).text();
9427
- return $parse(content);
9428
- }
9429
- async function writeState(cwd, state) {
9430
- const statePath = join5(cwd, ".jdi", "config", "state.yaml");
9431
- await Bun.write(statePath, $stringify(state));
9432
- }
9433
-
9434
9882
  // src/commands/status.ts
9435
9883
  var statusCommand = defineCommand({
9436
9884
  meta: {
@@ -9478,15 +9926,15 @@ var statusCommand = defineCommand({
9478
9926
  import { relative } from "path";
9479
9927
 
9480
9928
  // src/utils/resolve-components.ts
9481
- import { join as join6, basename } from "path";
9929
+ import { join as join9, basename } from "path";
9482
9930
  import { homedir } from "os";
9483
9931
  async function resolveComponents(cwd) {
9484
9932
  const components = [];
9485
9933
  const seen = new Set;
9486
9934
  const sources = [
9487
- { dir: join6(cwd, ".jdi", "framework", "components"), source: "project" },
9488
- { dir: join6(homedir(), ".jdi", "components"), source: "user" },
9489
- { dir: join6(import.meta.dir, "../framework/components"), source: "builtin" }
9935
+ { dir: join9(cwd, ".jdi", "framework", "components"), source: "project" },
9936
+ { dir: join9(homedir(), ".jdi", "components"), source: "user" },
9937
+ { dir: join9(import.meta.dir, "../framework/components"), source: "builtin" }
9490
9938
  ];
9491
9939
  for (const { dir, source } of sources) {
9492
9940
  try {
@@ -9495,7 +9943,7 @@ async function resolveComponents(cwd) {
9495
9943
  const name = basename(file, ".md");
9496
9944
  if (!seen.has(name)) {
9497
9945
  seen.add(name);
9498
- components.push({ name, path: join6(dir, file), source });
9946
+ components.push({ name, path: join9(dir, file), source });
9499
9947
  }
9500
9948
  }
9501
9949
  } catch {}
@@ -9523,51 +9971,6 @@ var componentsCommand = defineCommand({
9523
9971
  }
9524
9972
  });
9525
9973
 
9526
- // src/utils/git.ts
9527
- async function exec(cmd, cwd) {
9528
- const proc = Bun.spawn(cmd, {
9529
- cwd: cwd ?? process.cwd(),
9530
- stdout: "pipe",
9531
- stderr: "pipe"
9532
- });
9533
- const stdout2 = await new Response(proc.stdout).text();
9534
- const exitCode = await proc.exited;
9535
- return { stdout: stdout2.trim(), exitCode };
9536
- }
9537
- async function gitDiffNames(staged) {
9538
- const args = ["git", "diff", "--name-only"];
9539
- if (staged)
9540
- args.push("--cached");
9541
- const { stdout: stdout2 } = await exec(args);
9542
- return stdout2 ? stdout2.split(`
9543
- `) : [];
9544
- }
9545
- async function gitLog(range) {
9546
- const args = ["git", "log", "--oneline"];
9547
- if (range)
9548
- args.push(range);
9549
- else
9550
- args.push("-20");
9551
- const { stdout: stdout2 } = await exec(args);
9552
- return stdout2;
9553
- }
9554
- async function gitBranch() {
9555
- const { stdout: stdout2 } = await exec(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
9556
- return stdout2;
9557
- }
9558
- async function gitRoot() {
9559
- const { stdout: stdout2 } = await exec(["git", "rev-parse", "--show-toplevel"]);
9560
- return stdout2;
9561
- }
9562
- async function gitStatus() {
9563
- const { stdout: stdout2 } = await exec(["git", "status", "--porcelain"]);
9564
- return stdout2;
9565
- }
9566
- async function gitMergeBase(branch) {
9567
- const { stdout: stdout2 } = await exec(["git", "merge-base", "HEAD", branch]);
9568
- return stdout2;
9569
- }
9570
-
9571
9974
  // src/commands/commit.ts
9572
9975
  import { dirname as dirname2 } from "path";
9573
9976
  function detectScope(files) {
@@ -9686,8 +10089,8 @@ Use --all to stage and commit all, or stage files manually.`);
9686
10089
  });
9687
10090
 
9688
10091
  // src/commands/pr.ts
9689
- import { existsSync as existsSync5 } from "fs";
9690
- import { join as join7 } from "path";
10092
+ import { existsSync as existsSync8 } from "fs";
10093
+ import { join as join10 } from "path";
9691
10094
  async function hasGhCli() {
9692
10095
  const { exitCode } = await exec(["which", "gh"]);
9693
10096
  return exitCode === 0;
@@ -9737,8 +10140,8 @@ var prCommand = defineCommand({
9737
10140
 
9738
10141
  **Plan:** ${state.position.plan_name}` : "";
9739
10142
  let template = "";
9740
- const templatePath = join7(cwd, ".github", "pull_request_template.md");
9741
- if (existsSync5(templatePath)) {
10143
+ const templatePath = join10(cwd, ".github", "pull_request_template.md");
10144
+ if (existsSync8(templatePath)) {
9742
10145
  template = await Bun.file(templatePath).text();
9743
10146
  }
9744
10147
  const title = branch.replace(/^(feat|fix|chore|docs|refactor|test|ci)\//, "").replace(/[-_]/g, " ").replace(/^\w/, (c3) => c3.toUpperCase());
@@ -9791,7 +10194,7 @@ import { resolve as resolve5 } from "path";
9791
10194
  var reviewCommand = defineCommand({
9792
10195
  meta: {
9793
10196
  name: "review",
9794
- description: "Fetch PR diff and generate a structured review prompt"
10197
+ description: "Review a PR using Claude Code"
9795
10198
  },
9796
10199
  args: {
9797
10200
  pr: {
@@ -9802,6 +10205,11 @@ var reviewCommand = defineCommand({
9802
10205
  output: {
9803
10206
  type: "string",
9804
10207
  description: "Write prompt to file instead of stdout"
10208
+ },
10209
+ print: {
10210
+ type: "boolean",
10211
+ description: "Print the prompt to stdout instead of executing",
10212
+ default: false
9805
10213
  }
9806
10214
  },
9807
10215
  async run({ args }) {
@@ -9878,8 +10286,14 @@ ${data.body}` : ""
9878
10286
  if (args.output) {
9879
10287
  await Bun.write(resolve5(process.cwd(), args.output), prompt2);
9880
10288
  consola.success(`Review prompt written to ${args.output}`);
9881
- } else {
10289
+ } else if (args.print) {
9882
10290
  console.log(prompt2);
10291
+ } else {
10292
+ const { exitCode } = await spawnClaude(prompt2, { cwd: process.cwd() });
10293
+ if (exitCode !== 0) {
10294
+ consola.error(`Claude exited with code ${exitCode}`);
10295
+ process.exit(exitCode);
10296
+ }
9883
10297
  }
9884
10298
  }
9885
10299
  });
@@ -10013,7 +10427,7 @@ import { resolve as resolve6 } from "path";
10013
10427
  var quickCommand = defineCommand({
10014
10428
  meta: {
10015
10429
  name: "quick",
10016
- description: "Generate a focused change prompt for Claude Code"
10430
+ description: "Make a focused change using Claude Code"
10017
10431
  },
10018
10432
  args: {
10019
10433
  description: {
@@ -10024,6 +10438,11 @@ var quickCommand = defineCommand({
10024
10438
  output: {
10025
10439
  type: "string",
10026
10440
  description: "Write prompt to file instead of stdout"
10441
+ },
10442
+ print: {
10443
+ type: "boolean",
10444
+ description: "Print the prompt to stdout instead of executing",
10445
+ default: false
10027
10446
  }
10028
10447
  },
10029
10448
  async run({ args }) {
@@ -10057,14 +10476,20 @@ var quickCommand = defineCommand({
10057
10476
  if (args.output) {
10058
10477
  await Bun.write(resolve6(cwd, args.output), prompt2);
10059
10478
  consola.success(`Prompt written to ${args.output}`);
10060
- } else {
10479
+ } else if (args.print) {
10061
10480
  console.log(prompt2);
10481
+ } else {
10482
+ const { exitCode } = await spawnClaude(prompt2, { cwd });
10483
+ if (exitCode !== 0) {
10484
+ consola.error(`Claude exited with code ${exitCode}`);
10485
+ process.exit(exitCode);
10486
+ }
10062
10487
  }
10063
10488
  }
10064
10489
  });
10065
10490
 
10066
10491
  // src/commands/worktree.ts
10067
- import { existsSync as existsSync6 } from "fs";
10492
+ import { existsSync as existsSync9 } from "fs";
10068
10493
  function slugify(name) {
10069
10494
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
10070
10495
  }
@@ -10098,7 +10523,7 @@ var worktreeCommand = defineCommand({
10098
10523
  }
10099
10524
  const slug = slugify(args.name);
10100
10525
  const worktreePath = `${root}/.worktrees/${slug}`;
10101
- if (existsSync6(worktreePath)) {
10526
+ if (existsSync9(worktreePath)) {
10102
10527
  consola.error(`Worktree already exists at ${worktreePath}`);
10103
10528
  return;
10104
10529
  }
@@ -10171,7 +10596,6 @@ var worktreeCommand = defineCommand({
10171
10596
  consola.info(` cd ${worktreePath}`);
10172
10597
  }
10173
10598
  });
10174
-
10175
10599
  // src/commands/worktree-remove.ts
10176
10600
  var worktreeRemoveCommand = defineCommand({
10177
10601
  meta: {
@@ -10265,7 +10689,7 @@ Specify a worktree name: jdi worktree-remove <name>`);
10265
10689
 
10266
10690
  // src/commands/plan-review.ts
10267
10691
  import { resolve as resolve7 } from "path";
10268
- import { existsSync as existsSync7 } from "fs";
10692
+ import { existsSync as existsSync10 } from "fs";
10269
10693
  function parsePlanSummary(content) {
10270
10694
  const nameMatch = content.match(/^# .+?: (.+)$/m);
10271
10695
  const name = nameMatch?.[1] ?? "Unknown";
@@ -10304,7 +10728,7 @@ var planReviewCommand = defineCommand({
10304
10728
  consola.error("No plan found. Run `jdi plan` first.");
10305
10729
  return;
10306
10730
  }
10307
- if (!existsSync7(planPath)) {
10731
+ if (!existsSync10(planPath)) {
10308
10732
  consola.error(`Plan not found: ${planPath}`);
10309
10733
  return;
10310
10734
  }
@@ -10394,7 +10818,7 @@ Tasks (${tasks.length}):`);
10394
10818
 
10395
10819
  // src/commands/plan-approve.ts
10396
10820
  import { resolve as resolve8 } from "path";
10397
- import { existsSync as existsSync8 } from "fs";
10821
+ import { existsSync as existsSync11 } from "fs";
10398
10822
  var planApproveCommand = defineCommand({
10399
10823
  meta: {
10400
10824
  name: "plan-approve",
@@ -10423,7 +10847,7 @@ var planApproveCommand = defineCommand({
10423
10847
  consola.error("No plan to approve. Run `jdi plan` first.");
10424
10848
  return;
10425
10849
  }
10426
- if (!existsSync8(planPath)) {
10850
+ if (!existsSync11(planPath)) {
10427
10851
  consola.error(`Plan not found: ${planPath}`);
10428
10852
  return;
10429
10853
  }
@@ -10447,15 +10871,576 @@ var planApproveCommand = defineCommand({
10447
10871
  consola.info("Say 'implement this' in Claude Code or run `/jdi:implement-plan` to execute.");
10448
10872
  }
10449
10873
  });
10874
+
10875
+ // src/commands/action.ts
10876
+ import { resolve as resolve9 } from "path";
10877
+
10878
+ // src/utils/clickup.ts
10879
+ var CLICKUP_URL_PATTERNS = [
10880
+ /app\.clickup\.com\/t\/([a-z0-9]+)/i,
10881
+ /sharing\.clickup\.com\/t\/([a-z0-9]+)/i,
10882
+ /clickup\.com\/t\/([a-z0-9]+)/i
10883
+ ];
10884
+ function extractClickUpId(text) {
10885
+ for (const pattern of CLICKUP_URL_PATTERNS) {
10886
+ const match = text.match(pattern);
10887
+ if (match)
10888
+ return match[1];
10889
+ }
10890
+ return null;
10891
+ }
10892
+ async function fetchClickUpTicket(taskId) {
10893
+ const token = process.env.CLICKUP_API_TOKEN;
10894
+ if (!token)
10895
+ return null;
10896
+ try {
10897
+ const res = await fetch(`https://api.clickup.com/api/v2/task/${taskId}`, { headers: { Authorization: token } });
10898
+ if (!res.ok)
10899
+ return null;
10900
+ const data = await res.json();
10901
+ const acceptanceCriteria = [];
10902
+ if (data.checklists) {
10903
+ for (const checklist of data.checklists) {
10904
+ for (const item of checklist.items ?? []) {
10905
+ acceptanceCriteria.push(item.name);
10906
+ }
10907
+ }
10908
+ }
10909
+ const subtasks = (data.subtasks ?? []).map((st) => ({
10910
+ name: st.name,
10911
+ status: st.status?.status ?? "unknown"
10912
+ }));
10913
+ const priorityMap = {
10914
+ 1: "urgent",
10915
+ 2: "high",
10916
+ 3: "normal",
10917
+ 4: "low"
10918
+ };
10919
+ return {
10920
+ id: data.id,
10921
+ name: data.name,
10922
+ description: data.description ?? "",
10923
+ status: data.status?.status ?? "unknown",
10924
+ priority: priorityMap[data.priority?.id] ?? "normal",
10925
+ acceptanceCriteria,
10926
+ subtasks
10927
+ };
10928
+ } catch {
10929
+ return null;
10930
+ }
10931
+ }
10932
+ function formatTicketAsContext(ticket) {
10933
+ const lines = [
10934
+ `## ClickUp Ticket: ${ticket.name}`,
10935
+ `- **ID:** ${ticket.id}`,
10936
+ `- **Status:** ${ticket.status}`,
10937
+ `- **Priority:** ${ticket.priority}`,
10938
+ ``,
10939
+ `### Description`,
10940
+ ticket.description || "_No description_"
10941
+ ];
10942
+ if (ticket.acceptanceCriteria.length > 0) {
10943
+ lines.push(``, `### Acceptance Criteria`);
10944
+ for (const ac of ticket.acceptanceCriteria) {
10945
+ lines.push(`- [ ] ${ac}`);
10946
+ }
10947
+ }
10948
+ if (ticket.subtasks.length > 0) {
10949
+ lines.push(``, `### Subtasks`);
10950
+ for (const st of ticket.subtasks) {
10951
+ const check = st.status === "complete" ? "x" : " ";
10952
+ lines.push(`- [${check}] ${st.name}`);
10953
+ }
10954
+ }
10955
+ return lines.join(`
10956
+ `);
10957
+ }
10958
+
10959
+ // src/utils/github.ts
10960
+ async function postGitHubComment(repo, issueNumber, body) {
10961
+ await exec([
10962
+ "gh",
10963
+ "api",
10964
+ `repos/${repo}/issues/${issueNumber}/comments`,
10965
+ "-f",
10966
+ `body=${body}`
10967
+ ]);
10968
+ }
10969
+ async function reactToComment(repo, commentId, reaction) {
10970
+ await exec([
10971
+ "gh",
10972
+ "api",
10973
+ `repos/${repo}/issues/comments/${commentId}/reactions`,
10974
+ "-f",
10975
+ `content=${reaction}`
10976
+ ]);
10977
+ }
10978
+ async function fetchCommentThread(repo, issueNumber) {
10979
+ const { stdout: stdout2, exitCode } = await exec([
10980
+ "gh",
10981
+ "api",
10982
+ `repos/${repo}/issues/${issueNumber}/comments`,
10983
+ "--paginate",
10984
+ "--jq",
10985
+ `.[] | {id: .id, author: .user.login, body: .body, createdAt: .created_at}`
10986
+ ]);
10987
+ if (exitCode !== 0 || !stdout2.trim())
10988
+ return [];
10989
+ const comments = [];
10990
+ for (const line of stdout2.trim().split(`
10991
+ `)) {
10992
+ if (!line.trim())
10993
+ continue;
10994
+ try {
10995
+ const parsed = JSON.parse(line);
10996
+ comments.push({
10997
+ id: parsed.id,
10998
+ author: parsed.author,
10999
+ body: parsed.body,
11000
+ createdAt: parsed.createdAt,
11001
+ isJedi: parsed.body.includes("Powered by [@benzotti/jedi]") || parsed.body.includes("Jedi plan") || parsed.body.includes("Jedi quick") || parsed.body.includes("Jedi implement")
11002
+ });
11003
+ } catch {}
11004
+ }
11005
+ return comments;
11006
+ }
11007
+ function buildConversationContext(thread, currentCommentId) {
11008
+ const jediSegments = [];
11009
+ let inJediConversation = false;
11010
+ for (const comment of thread) {
11011
+ if (comment.id === currentCommentId)
11012
+ break;
11013
+ if (comment.body.includes("@jedi")) {
11014
+ inJediConversation = true;
11015
+ jediSegments.push(comment);
11016
+ } else if (inJediConversation) {
11017
+ jediSegments.push(comment);
11018
+ if (comment.isJedi) {}
11019
+ }
11020
+ }
11021
+ const previousJediRuns = jediSegments.filter((c3) => c3.isJedi).length;
11022
+ const isFollowUp = previousJediRuns > 0;
11023
+ if (jediSegments.length === 0) {
11024
+ return { history: "", previousJediRuns: 0, isFollowUp: false };
11025
+ }
11026
+ const lines = ["## Previous Conversation", ""];
11027
+ for (const comment of jediSegments) {
11028
+ const role = comment.isJedi ? "Jedi" : `@${comment.author}`;
11029
+ let body = comment.body;
11030
+ if (comment.isJedi && body.length > 2000) {
11031
+ body = body.slice(0, 2000) + `
11032
+
11033
+ ... (truncated)`;
11034
+ }
11035
+ lines.push(`**${role}** (${comment.createdAt}):`);
11036
+ lines.push(body);
11037
+ lines.push("");
11038
+ }
11039
+ return { history: lines.join(`
11040
+ `), previousJediRuns, isFollowUp };
11041
+ }
11042
+ function formatResultComment(command, success, summary) {
11043
+ const status = success ? "Completed" : "Failed";
11044
+ const icon = success ? "white_check_mark" : "x";
11045
+ return [
11046
+ `### :${icon}: Jedi ${command} \u2014 ${status}`,
11047
+ ``,
11048
+ `<details>`,
11049
+ `<summary>Details</summary>`,
11050
+ ``,
11051
+ summary,
11052
+ ``,
11053
+ `</details>`,
11054
+ ``,
11055
+ `---`,
11056
+ `_Powered by [@benzotti/jedi](https://github.com/zottiben/jedi)_`
11057
+ ].join(`
11058
+ `);
11059
+ }
11060
+
11061
+ // src/commands/action.ts
11062
+ function parseComment(comment, isFollowUp) {
11063
+ const match = comment.match(/@jedi\s+(.+)/is);
11064
+ if (!match) {
11065
+ if (isFollowUp) {
11066
+ return {
11067
+ command: "plan",
11068
+ description: comment.trim(),
11069
+ clickUpUrl: null,
11070
+ fullFlow: false,
11071
+ isFeedback: true
11072
+ };
11073
+ }
11074
+ return null;
11075
+ }
11076
+ const body = match[1].trim();
11077
+ const clickUpMatch = body.match(/(https?:\/\/[^\s]*clickup\.com\/t\/[a-z0-9]+)/i);
11078
+ const clickUpUrl = clickUpMatch ? clickUpMatch[1] : null;
11079
+ const description = body.replace(/(https?:\/\/[^\s]*clickup\.com\/t\/[a-z0-9]+)/i, "").replace(/\s+/g, " ").trim();
11080
+ const lower = body.toLowerCase();
11081
+ if (lower.startsWith("plan ")) {
11082
+ return { command: "plan", description, clickUpUrl, fullFlow: false, isFeedback: false };
11083
+ }
11084
+ if (lower.startsWith("implement")) {
11085
+ return { command: "implement", description, clickUpUrl, fullFlow: false, isFeedback: false };
11086
+ }
11087
+ if (lower.startsWith("quick ")) {
11088
+ return { command: "quick", description, clickUpUrl, fullFlow: false, isFeedback: false };
11089
+ }
11090
+ if (lower.startsWith("review")) {
11091
+ return { command: "review", description, clickUpUrl, fullFlow: false, isFeedback: false };
11092
+ }
11093
+ if (lower.startsWith("feedback")) {
11094
+ return { command: "feedback", description, clickUpUrl, fullFlow: false, isFeedback: false };
11095
+ }
11096
+ if (lower.startsWith("do ")) {
11097
+ if (clickUpUrl) {
11098
+ return { command: "plan", description, clickUpUrl, fullFlow: true, isFeedback: false };
11099
+ }
11100
+ return { command: "quick", description, clickUpUrl, fullFlow: false, isFeedback: false };
11101
+ }
11102
+ if (/^(approved?|lgtm|looks?\s*good|ship\s*it)/i.test(lower)) {
11103
+ return {
11104
+ command: "plan",
11105
+ description: body,
11106
+ clickUpUrl: null,
11107
+ fullFlow: false,
11108
+ isFeedback: true
11109
+ };
11110
+ }
11111
+ if (isFollowUp) {
11112
+ return {
11113
+ command: "plan",
11114
+ description: body,
11115
+ clickUpUrl: null,
11116
+ fullFlow: false,
11117
+ isFeedback: true
11118
+ };
11119
+ }
11120
+ return { command: "plan", description, clickUpUrl, fullFlow: false, isFeedback: false };
11121
+ }
11122
+ var actionCommand = defineCommand({
11123
+ meta: {
11124
+ name: "action",
11125
+ description: "GitHub Action entry point \u2014 parse @jedi comment and run workflow"
11126
+ },
11127
+ args: {
11128
+ comment: {
11129
+ type: "positional",
11130
+ description: "The raw comment body containing @jedi mention",
11131
+ required: true
11132
+ },
11133
+ "comment-id": {
11134
+ type: "string",
11135
+ description: "GitHub comment ID for reactions"
11136
+ },
11137
+ "pr-number": {
11138
+ type: "string",
11139
+ description: "PR number (if triggered from a PR)"
11140
+ },
11141
+ "issue-number": {
11142
+ type: "string",
11143
+ description: "Issue number (if triggered from an issue)"
11144
+ },
11145
+ repo: {
11146
+ type: "string",
11147
+ description: "Repository in owner/repo format"
11148
+ }
11149
+ },
11150
+ async run({ args }) {
11151
+ const cwd = process.cwd();
11152
+ const repo = args.repo ?? process.env.GITHUB_REPOSITORY;
11153
+ const commentId = args["comment-id"] ? Number(args["comment-id"]) : null;
11154
+ const issueNumber = Number(args["pr-number"] ?? args["issue-number"] ?? 0);
11155
+ let conversationHistory = "";
11156
+ let isFollowUp = false;
11157
+ if (repo && issueNumber) {
11158
+ const thread = await fetchCommentThread(repo, issueNumber);
11159
+ const context = buildConversationContext(thread, commentId ?? 0);
11160
+ conversationHistory = context.history;
11161
+ isFollowUp = context.isFollowUp;
11162
+ if (isFollowUp) {
11163
+ consola.info(`Continuing conversation (${context.previousJediRuns} previous Jedi run(s))`);
11164
+ }
11165
+ }
11166
+ const intent = parseComment(args.comment, isFollowUp);
11167
+ if (!intent) {
11168
+ consola.error("Could not parse @jedi intent from comment");
11169
+ process.exit(1);
11170
+ }
11171
+ consola.info(`Parsed intent: ${intent.isFeedback ? "feedback on previous" : intent.command}${intent.fullFlow ? " (full flow)" : ""}`);
11172
+ if (repo && commentId) {
11173
+ await reactToComment(repo, commentId, "eyes").catch(() => {});
11174
+ }
11175
+ const storage = await createStorage(cwd);
11176
+ const { learningsPath, codebaseIndexPath } = await loadPersistedState(cwd, storage);
11177
+ let ticketContext = "";
11178
+ if (intent.clickUpUrl) {
11179
+ const taskId = extractClickUpId(intent.clickUpUrl);
11180
+ if (taskId) {
11181
+ consola.info(`Fetching ClickUp ticket: ${taskId}`);
11182
+ const ticket = await fetchClickUpTicket(taskId);
11183
+ if (ticket) {
11184
+ ticketContext = formatTicketAsContext(ticket);
11185
+ consola.success(`Loaded ticket: ${ticket.name}`);
11186
+ }
11187
+ }
11188
+ }
11189
+ const projectType = await detectProjectType(cwd);
11190
+ const adapter = await readAdapter(cwd);
11191
+ const techStack = adapter?.tech_stack ? Object.entries(adapter.tech_stack).map(([k2, v2]) => `${k2}: ${v2}`).join(", ") : projectType;
11192
+ const contextLines = [
11193
+ `## Project Context`,
11194
+ `- Type: ${projectType}`,
11195
+ `- Tech stack: ${techStack}`,
11196
+ `- Working directory: ${cwd}`
11197
+ ];
11198
+ if (learningsPath) {
11199
+ contextLines.push(`- Learnings: ${learningsPath}`);
11200
+ }
11201
+ if (codebaseIndexPath) {
11202
+ contextLines.push(`- Codebase index: ${codebaseIndexPath}`);
11203
+ }
11204
+ if (ticketContext) {
11205
+ contextLines.push(``, ticketContext);
11206
+ }
11207
+ const baseProtocol = resolve9(cwd, ".jdi/framework/components/meta/AgentBase.md");
11208
+ let prompt2;
11209
+ if (intent.isFeedback) {
11210
+ prompt2 = [
11211
+ `Read ${baseProtocol} for the base agent protocol.`,
11212
+ ``,
11213
+ ...contextLines,
11214
+ ``,
11215
+ conversationHistory,
11216
+ ``,
11217
+ `## Current Feedback`,
11218
+ `The user has provided feedback on previous Jedi work:`,
11219
+ ``,
11220
+ `> ${intent.description}`,
11221
+ ``,
11222
+ `## Instructions`,
11223
+ `Review the previous conversation above. The user is iterating on work Jedi already started.`,
11224
+ ``,
11225
+ `- If the feedback is an **approval** ("approved", "lgtm", "looks good", "ship it"), finalise the current work \u2014 create commits and/or a PR as appropriate.`,
11226
+ `- If the feedback is a **refinement** ("change task 2", "use a different approach", "add error handling"), apply the requested changes to the existing plan or implementation. Present an updated summary when done.`,
11227
+ `- If the feedback is a **question**, answer it with full context from the conversation.`,
11228
+ ``,
11229
+ `Read state.yaml and any existing plan files to understand what was previously done. Apply changes incrementally \u2014 do not restart from scratch.`
11230
+ ].join(`
11231
+ `);
11232
+ } else {
11233
+ const agentSpec = resolve9(cwd, `.jdi/framework/agents/jdi-planner.md`);
11234
+ const historyBlock = conversationHistory ? `
11235
+ ${conversationHistory}
11236
+
11237
+ The above is prior conversation on this issue for context.
11238
+ ` : "";
11239
+ switch (intent.command) {
11240
+ case "plan":
11241
+ prompt2 = [
11242
+ `Read ${baseProtocol} for the base agent protocol.`,
11243
+ `You are jdi-planner. Read ${agentSpec} for your full specification.`,
11244
+ ``,
11245
+ ...contextLines,
11246
+ historyBlock,
11247
+ `## Task`,
11248
+ `Create an implementation plan for: ${intent.description}`,
11249
+ ticketContext ? `
11250
+ Use the ClickUp ticket above as the primary requirements source.` : ``,
11251
+ ``,
11252
+ `Follow the planning workflow in your spec. Present the plan summary when complete and ask for feedback. The user will respond via another GitHub comment.`
11253
+ ].join(`
11254
+ `);
11255
+ break;
11256
+ case "implement":
11257
+ prompt2 = [
11258
+ `Read ${baseProtocol} for the base agent protocol.`,
11259
+ `Read ${resolve9(cwd, ".jdi/framework/components/meta/ComplexityRouter.md")} for complexity routing rules.`,
11260
+ `Read ${resolve9(cwd, ".jdi/framework/components/meta/AgentTeamsOrchestration.md")} for Agent Teams orchestration (if needed).`,
11261
+ ``,
11262
+ ...contextLines,
11263
+ historyBlock,
11264
+ `## Task`,
11265
+ `Execute the current implementation plan. Read state.yaml for the active plan path.`,
11266
+ `Follow the implement-plan orchestration. Present a summary when complete and ask for feedback.`
11267
+ ].join(`
11268
+ `);
11269
+ break;
11270
+ case "quick":
11271
+ prompt2 = [
11272
+ `Read ${baseProtocol} for the base agent protocol.`,
11273
+ ``,
11274
+ ...contextLines,
11275
+ historyBlock,
11276
+ `## Task`,
11277
+ `Make this quick change: ${intent.description}`,
11278
+ `Keep changes minimal and focused. Commit when done. Present what you changed.`
11279
+ ].join(`
11280
+ `);
11281
+ break;
11282
+ case "review":
11283
+ prompt2 = [
11284
+ `Read ${baseProtocol} for the base agent protocol.`,
11285
+ ``,
11286
+ ...contextLines,
11287
+ historyBlock,
11288
+ `## Task`,
11289
+ `Review the current PR. Post line-level review comments via gh api.`
11290
+ ].join(`
11291
+ `);
11292
+ break;
11293
+ case "feedback":
11294
+ prompt2 = [
11295
+ `Read ${baseProtocol} for the base agent protocol.`,
11296
+ ``,
11297
+ ...contextLines,
11298
+ historyBlock,
11299
+ `## Task`,
11300
+ `Address PR feedback comments. Read comments via gh api, make changes, reply to each.`
11301
+ ].join(`
11302
+ `);
11303
+ break;
11304
+ default:
11305
+ prompt2 = [
11306
+ `Read ${baseProtocol} for the base agent protocol.`,
11307
+ ``,
11308
+ ...contextLines,
11309
+ historyBlock,
11310
+ `## Task`,
11311
+ intent.description
11312
+ ].join(`
11313
+ `);
11314
+ }
11315
+ }
11316
+ let success = true;
11317
+ try {
11318
+ const { exitCode } = await spawnClaude(prompt2, {
11319
+ cwd,
11320
+ permissionMode: "bypassPermissions"
11321
+ });
11322
+ if (exitCode !== 0) {
11323
+ success = false;
11324
+ consola.error(`Claude exited with code ${exitCode}`);
11325
+ }
11326
+ if (intent.fullFlow && success) {
11327
+ consola.info("Full flow: now running implement...");
11328
+ const implementPrompt = [
11329
+ `Read ${baseProtocol} for the base agent protocol.`,
11330
+ `Read ${resolve9(cwd, ".jdi/framework/components/meta/ComplexityRouter.md")} for complexity routing rules.`,
11331
+ `Read ${resolve9(cwd, ".jdi/framework/components/meta/AgentTeamsOrchestration.md")} for Agent Teams orchestration (if needed).`,
11332
+ ``,
11333
+ ...contextLines,
11334
+ ``,
11335
+ `## Task`,
11336
+ `Execute the most recently created implementation plan in .jdi/plans/.`,
11337
+ `Follow the implement-plan orchestration. Present a summary when complete.`
11338
+ ].join(`
11339
+ `);
11340
+ const implResult = await spawnClaude(implementPrompt, {
11341
+ cwd,
11342
+ permissionMode: "bypassPermissions"
11343
+ });
11344
+ if (implResult.exitCode !== 0) {
11345
+ success = false;
11346
+ }
11347
+ }
11348
+ } catch (err) {
11349
+ success = false;
11350
+ consola.error("Execution failed:", err);
11351
+ }
11352
+ const saved = await savePersistedState(cwd, storage);
11353
+ if (saved.learningsSaved)
11354
+ consola.info("Learnings persisted to storage");
11355
+ if (saved.codebaseIndexSaved)
11356
+ consola.info("Codebase index persisted to storage");
11357
+ if (repo && issueNumber) {
11358
+ const actionLabel = intent.isFeedback ? "feedback" : intent.command;
11359
+ const summary = success ? `Executed \`${actionLabel}\` successfully.${saved.learningsSaved ? " Learnings updated." : ""}` : `Execution of \`${actionLabel}\` failed. Check workflow logs for details.`;
11360
+ const commentBody = formatResultComment(actionLabel, success, summary);
11361
+ await postGitHubComment(repo, issueNumber, commentBody).catch((err) => {
11362
+ consola.error("Failed to post result comment:", err);
11363
+ });
11364
+ }
11365
+ if (repo && commentId) {
11366
+ const reaction = success ? "+1" : "-1";
11367
+ await reactToComment(repo, commentId, reaction).catch(() => {});
11368
+ }
11369
+ if (!success)
11370
+ process.exit(1);
11371
+ }
11372
+ });
11373
+
11374
+ // src/commands/setup-action.ts
11375
+ import { join as join12, dirname as dirname3 } from "path";
11376
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4 } from "fs";
11377
+ var setupActionCommand = defineCommand({
11378
+ meta: {
11379
+ name: "setup-action",
11380
+ description: "Set up the Jedi GitHub Action in your repository"
11381
+ },
11382
+ args: {},
11383
+ async run() {
11384
+ const cwd = process.cwd();
11385
+ const workflowDest = join12(cwd, ".github", "workflows", "jedi.yml");
11386
+ if (existsSync12(workflowDest)) {
11387
+ consola.warn(`Workflow already exists at ${workflowDest}`);
11388
+ consola.info("Skipping workflow copy. Delete it manually to regenerate.");
11389
+ } else {
11390
+ const templatePath = join12(import.meta.dir, "../../action/workflow-template.yml");
11391
+ if (!existsSync12(templatePath)) {
11392
+ consola.error("Workflow template not found. Ensure @benzotti/jedi is properly installed.");
11393
+ process.exit(1);
11394
+ }
11395
+ const dir = dirname3(workflowDest);
11396
+ if (!existsSync12(dir))
11397
+ mkdirSync4(dir, { recursive: true });
11398
+ const template = await Bun.file(templatePath).text();
11399
+ await Bun.write(workflowDest, template);
11400
+ consola.success(`Created ${workflowDest}`);
11401
+ }
11402
+ consola.info("");
11403
+ consola.box([
11404
+ "Jedi GitHub Action Setup",
11405
+ "",
11406
+ "Uses: anthropics/claude-code-action@v1",
11407
+ "Trigger: @jedi in issue/PR comments",
11408
+ "",
11409
+ "Required secrets (set via GitHub UI or CLI):",
11410
+ "",
11411
+ " gh secret set ANTHROPIC_API_KEY --body '<your-key>'",
11412
+ "",
11413
+ "Optional secrets:",
11414
+ "",
11415
+ " gh secret set CLICKUP_API_TOKEN --body '<your-token>'",
11416
+ "",
11417
+ "Usage: Comment on any issue or PR with:",
11418
+ "",
11419
+ " @jedi plan <description>",
11420
+ " @jedi quick <small fix>",
11421
+ " @jedi do <clickup-ticket-url>",
11422
+ " @jedi review",
11423
+ " @jedi feedback",
11424
+ "",
11425
+ "Conversation: Reply to Jedi with feedback to iterate,",
11426
+ "or say 'approved' to finalise."
11427
+ ].join(`
11428
+ `));
11429
+ }
11430
+ });
10450
11431
  // package.json
10451
11432
  var package_default = {
10452
11433
  name: "@benzotti/jedi",
10453
- version: "0.1.2",
11434
+ version: "0.1.4",
10454
11435
  description: "JDI - Context-efficient AI development framework for Claude Code",
10455
11436
  type: "module",
10456
11437
  bin: {
10457
11438
  jdi: "./dist/index.js"
10458
11439
  },
11440
+ exports: {
11441
+ ".": "./dist/index.js",
11442
+ "./storage": "./src/storage/index.ts"
11443
+ },
10459
11444
  scripts: {
10460
11445
  build: "bun build src/index.ts --outdir dist --target bun",
10461
11446
  dev: "bun run src/index.ts",
@@ -10472,7 +11457,8 @@ var package_default = {
10472
11457
  },
10473
11458
  files: [
10474
11459
  "dist",
10475
- "framework"
11460
+ "framework",
11461
+ "action"
10476
11462
  ],
10477
11463
  repository: {
10478
11464
  type: "git",
@@ -10505,7 +11491,9 @@ var main = defineCommand({
10505
11491
  worktree: worktreeCommand,
10506
11492
  "worktree-remove": worktreeRemoveCommand,
10507
11493
  "plan-review": planReviewCommand,
10508
- "plan-approve": planApproveCommand
11494
+ "plan-approve": planApproveCommand,
11495
+ action: actionCommand,
11496
+ "setup-action": setupActionCommand
10509
11497
  }
10510
11498
  });
10511
11499
  runMain(main);