infernoflow 0.13.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,11 @@ const COMMAND_DESCRIPTIONS = {
32
32
  synthesize: "Auto-detect workflow patterns and synthesize reusable skills + agents",
33
33
  agent: "Manage and run auto-synthesized agents (list | run | show | delete)",
34
34
  version: "Smart semver bump recommendation based on capability changes (--apply to write)",
35
+ "pr-comment": "Post capability drift analysis as a GitHub PR comment (works in CI automatically)",
36
+ dashboard: "Launch local web dashboard on localhost:7337 — live contract health, capabilities, agents",
37
+ "team-sync": "Sync capability contract across a team via a shared git branch (push | pull | status | init)",
38
+ onboard: "Interactive onboarding wizard for new developers — explains infernoflow in 5 minutes",
39
+ cloud: "Sync capability contracts via infernoflow cloud (init | push | pull | status | dashboard)",
35
40
  };
36
41
 
37
42
  const COMMAND_HANDLERS = {
@@ -57,6 +62,11 @@ const COMMAND_HANDLERS = {
57
62
  synthesize: async (args) => (await import("../lib/commands/synthesize.mjs")).synthesizeCommand(args),
58
63
  agent: async (args) => (await import("../lib/commands/agent.mjs")).agentCommand(args),
59
64
  version: async (args) => (await import("../lib/commands/version.mjs")).versionCommand(args),
65
+ "pr-comment": async (args) => (await import("../lib/commands/prComment.mjs")).prCommentCommand(args),
66
+ dashboard: async (args) => (await import("../lib/commands/dashboard.mjs")).dashboardCommand(args),
67
+ "team-sync": async (args) => (await import("../lib/commands/teamSync.mjs")).teamSyncCommand(args),
68
+ onboard: async (args) => (await import("../lib/commands/onboard.mjs")).onboardCommand(args),
69
+ cloud: async (args) => (await import("../lib/commands/cloud.mjs")).cloudCommand(args),
60
70
  };
61
71
 
62
72
  function formatCommandsHelp() {
@@ -86,7 +96,9 @@ ${formatCommandsHelp()}
86
96
  update Draft ## Unreleased from commits (default sub-command)
87
97
  show Print the current ## Unreleased block
88
98
  list List commits since last tag
99
+ ai Generate human-readable changelog with AI (Anthropic or Ollama)
89
100
  --ref <tag|commit> Use a specific ref instead of last tag
101
+ --version <x.y.z> Version label for the AI-generated entry
90
102
  --dry-run Print what would be written without modifying file
91
103
  --append Append to existing ## Unreleased instead of replacing
92
104
  --json Machine-readable output
@@ -167,6 +179,28 @@ ${formatCommandsHelp()}
167
179
  --apply Write recommended version bump to package.json
168
180
  --json Machine-readable output
169
181
 
182
+ ${bold("pr-comment options:")}
183
+ --pr <number> PR number to comment on (auto-detected in GitHub Actions)
184
+ --repo <owner/repo> GitHub repository (auto-detected in GitHub Actions)
185
+ --token <ghp_...> GitHub token (auto-detected from GITHUB_TOKEN env var)
186
+ --ref <ref> Base ref to diff against (auto-detected from GITHUB_BASE_REF)
187
+ --dry-run Print the comment without posting it
188
+ --json Machine-readable output
189
+
190
+ ${bold("cloud sub-commands:")}
191
+ init Generate a project token and configure cloud sync
192
+ push Upload local capability contract to cloud
193
+ pull Download latest contract from cloud (conflict detection)
194
+ status Compare local vs cloud (hashes, capability counts)
195
+ dashboard Print hosted dashboard URL and open in browser
196
+
197
+ ${bold("cloud options:")}
198
+ --token <tok> Override token (or set INFERNOFLOW_TOKEN env var)
199
+ --endpoint <url> Override default endpoint (https://cloud.infernoflow.dev)
200
+ --force, -f Overwrite on init; overwrite local on conflicted pull
201
+ --dry-run Print what would happen without sending
202
+ --json Machine-readable output
203
+
170
204
  ${bold("Machine output:")}
171
205
  ${gray("status --json")}
172
206
  ${gray("check --json")}
@@ -303,6 +303,248 @@ async function subcmdUpdate(cwd, changelogPath, opts) {
303
303
  console.log(` Run ${cyan("infernoflow publish")} when ready to cut the release\n`);
304
304
  }
305
305
 
306
+ // ── AI changelog writer ───────────────────────────────────────────────────────
307
+
308
+ function parseCaps(jsonText) {
309
+ if (!jsonText) return [];
310
+ try {
311
+ const obj = JSON.parse(jsonText);
312
+ const raw = obj.capabilities || [];
313
+ return raw.map(c => typeof c === "string" ? { id: c, title: c } : c);
314
+ } catch { return []; }
315
+ }
316
+
317
+ function loadCapsFromDisk(infernoDir) {
318
+ for (const name of ["capabilities.json", "contract.json"]) {
319
+ const p = path.join(infernoDir, name);
320
+ if (fs.existsSync(p)) return parseCaps(fs.readFileSync(p, "utf8"));
321
+ }
322
+ return [];
323
+ }
324
+
325
+ function loadCapsAtRef(ref, cwd) {
326
+ const { execSync: _exec } = await import ? null : null; // handled below
327
+ try {
328
+ const { execSync: ex } = await import("node:child_process");
329
+ for (const name of ["capabilities.json", "contract.json"]) {
330
+ try {
331
+ const content = ex(`git show "${ref}:inferno/${name}"`, {
332
+ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
333
+ }).trim();
334
+ if (content) return parseCaps(content);
335
+ } catch {}
336
+ }
337
+ } catch {}
338
+ return [];
339
+ }
340
+
341
+ function diffCapsSimple(before, after) {
342
+ const beforeMap = new Map(before.map(c => [c.id, c]));
343
+ const afterMap = new Map(after.map(c => [c.id, c]));
344
+ return {
345
+ added: after.filter(c => !beforeMap.has(c.id)),
346
+ removed: before.filter(c => !afterMap.has(c.id)),
347
+ changed: after.filter(c => {
348
+ const old = beforeMap.get(c.id);
349
+ return old && old.title !== c.title;
350
+ }),
351
+ };
352
+ }
353
+
354
+ function buildAiPrompt(commits, diff, ref, version) {
355
+ const lines = [];
356
+ lines.push(`You are writing a changelog entry for a software release.`);
357
+ lines.push(`Write it in a friendly, clear, developer-facing tone.`);
358
+ lines.push(`Use Markdown. Start with ## ${version || "Unreleased"}`);
359
+ lines.push(`Include sections: ### Added, ### Fixed, ### Changed, ### Removed (only if non-empty).`);
360
+ lines.push(`Be concise — one line per item. Do not include commit hashes.`);
361
+ lines.push(`Do not invent features that are not listed below.`);
362
+ lines.push(``);
363
+ lines.push(`## Capability changes since ${ref || "last release"}:`);
364
+ if (diff.added.length) lines.push(`Added: ${diff.added.map(c => c.title || c.id).join(", ")}`);
365
+ if (diff.removed.length) lines.push(`Removed: ${diff.removed.map(c => c.title || c.id).join(", ")}`);
366
+ if (diff.changed.length) lines.push(`Changed: ${diff.changed.map(c => c.title || c.id).join(", ")}`);
367
+ if (!diff.added.length && !diff.removed.length && !diff.changed.length) lines.push(`(no capability changes)`);
368
+ lines.push(``);
369
+ lines.push(`## Git commits since ${ref || "last release"}:`);
370
+ for (const c of commits.slice(0, 40)) lines.push(`- ${c.subject}`);
371
+ lines.push(``);
372
+ lines.push(`Write the changelog entry now:`);
373
+ return lines.join("\n");
374
+ }
375
+
376
+ function buildStructuredFallback(commits, diff, version) {
377
+ // High-quality template fallback when no AI provider is available
378
+ const sections = groupCommits(commits);
379
+
380
+ // Merge capability changes into sections
381
+ for (const c of diff.added) sections.Added.unshift(`${c.title || c.id} capability`);
382
+ for (const c of diff.removed) sections.Removed.unshift(`${c.title || c.id} capability`);
383
+ for (const c of diff.changed) sections.Changed.unshift(`Updated ${c.title || c.id} capability`);
384
+
385
+ // Deduplicate
386
+ for (const key of Object.keys(sections)) sections[key] = [...new Set(sections[key])];
387
+
388
+ const tag = version || "Unreleased";
389
+ const date = new Date().toISOString().slice(0, 10);
390
+ const lines = [`## ${tag} — ${date}`, ""];
391
+
392
+ const ORDER = ["Breaking", "Added", "Fixed", "Changed", "Removed"];
393
+ for (const heading of ORDER) {
394
+ const items = sections[heading];
395
+ if (!items?.length) continue;
396
+ lines.push(`### ${heading}`);
397
+ for (const item of items) lines.push(`- ${item}`);
398
+ lines.push("");
399
+ }
400
+
401
+ return lines.join("\n");
402
+ }
403
+
404
+ async function callLocalAi(prompt) {
405
+ // Try Ollama (localhost:11434) — most common local AI setup
406
+ try {
407
+ const { default: http } = await import("node:http");
408
+ return new Promise((resolve, reject) => {
409
+ const body = JSON.stringify({ model: "llama3", prompt, stream: false });
410
+ const req = http.request({
411
+ hostname: "localhost", port: 11434,
412
+ path: "/api/generate", method: "POST",
413
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
414
+ }, (res) => {
415
+ let raw = "";
416
+ res.on("data", d => raw += d);
417
+ res.on("end", () => {
418
+ try { resolve(JSON.parse(raw).response || null); } catch { resolve(null); }
419
+ });
420
+ });
421
+ req.setTimeout(30_000, () => { req.destroy(); reject(new Error("timeout")); });
422
+ req.on("error", reject);
423
+ req.write(body);
424
+ req.end();
425
+ });
426
+ } catch { return null; }
427
+ }
428
+
429
+ async function callAnthropicAi(prompt, apiKey) {
430
+ const { default: https } = await import("node:https");
431
+ return new Promise((resolve, reject) => {
432
+ const body = JSON.stringify({
433
+ model: "claude-haiku-4-5-20251001",
434
+ max_tokens: 1024,
435
+ messages: [{ role: "user", content: prompt }],
436
+ });
437
+ const req = https.request({
438
+ hostname: "api.anthropic.com",
439
+ path: "/v1/messages", method: "POST",
440
+ headers: {
441
+ "x-api-key": apiKey,
442
+ "anthropic-version": "2023-06-01",
443
+ "Content-Type": "application/json",
444
+ "Content-Length": Buffer.byteLength(body),
445
+ },
446
+ }, (res) => {
447
+ let raw = "";
448
+ res.on("data", d => raw += d);
449
+ res.on("end", () => {
450
+ try {
451
+ const data = JSON.parse(raw);
452
+ resolve(data.content?.[0]?.text || null);
453
+ } catch { resolve(null); }
454
+ });
455
+ });
456
+ req.setTimeout(30_000, () => { req.destroy(); reject(new Error("timeout")); });
457
+ req.on("error", reject);
458
+ req.write(body);
459
+ req.end();
460
+ });
461
+ }
462
+
463
+ async function subcmdAi(cwd, changelogPath, opts) {
464
+ const { ref, dryRun, asJson, version } = opts;
465
+ const infernoDir = path.join(cwd, "inferno");
466
+
467
+ const tag = ref || lastTag(cwd);
468
+ const commits = commitsSince(tag, cwd);
469
+
470
+ // Load capability diff
471
+ const currentCaps = fs.existsSync(infernoDir) ? loadCapsFromDisk(infernoDir) : [];
472
+ const prevCaps = tag ? (() => {
473
+ try {
474
+ const ex = require("child_process").execSync;
475
+ for (const name of ["capabilities.json", "contract.json"]) {
476
+ try {
477
+ const c = ex(`git show "${tag}:inferno/${name}"`, { cwd, encoding: "utf8", stdio: ["ignore","pipe","pipe"] }).trim();
478
+ if (c) return parseCaps(c);
479
+ } catch {}
480
+ }
481
+ return [];
482
+ } catch { return []; }
483
+ })() : [];
484
+
485
+ const diff = diffCapsSimple(prevCaps, currentCaps);
486
+
487
+ if (!asJson) {
488
+ info(`Generating AI changelog since ${bold(tag || "beginning")}...`);
489
+ info(`${commits.length} commits · ${diff.added.length} added · ${diff.removed.length} removed · ${diff.changed.length} changed`);
490
+ console.log();
491
+ }
492
+
493
+ const prompt = buildAiPrompt(commits, diff, tag, version);
494
+ let aiText = null;
495
+ let provider = "template";
496
+
497
+ // Try AI providers in order: Anthropic API → Ollama → structured template
498
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
499
+ if (anthropicKey) {
500
+ try {
501
+ aiText = await callAnthropicAi(prompt, anthropicKey);
502
+ if (aiText) provider = "anthropic";
503
+ } catch {}
504
+ }
505
+
506
+ if (!aiText) {
507
+ try {
508
+ aiText = await callLocalAi(prompt);
509
+ if (aiText) provider = "ollama";
510
+ } catch {}
511
+ }
512
+
513
+ // Fallback: structured template
514
+ if (!aiText) {
515
+ aiText = buildStructuredFallback(commits, diff, version);
516
+ provider = "template";
517
+ }
518
+
519
+ if (asJson) {
520
+ console.log(JSON.stringify({ ok: true, provider, ref: tag, commits: commits.length, markdown: aiText }));
521
+ return;
522
+ }
523
+
524
+ console.log(gray(" ─── Generated changelog ──────────────────────────────"));
525
+ aiText.split("\n").forEach(l => console.log(" " + l));
526
+ console.log(gray(" ──────────────────────────────────────────────────────"));
527
+ console.log();
528
+ info(`Generated via: ${bold(provider)}`);
529
+
530
+ if (dryRun) {
531
+ warn("Dry run — CHANGELOG.md not modified");
532
+ console.log();
533
+ return;
534
+ }
535
+
536
+ // Write to CHANGELOG.md
537
+ let text = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, "utf8") : "# Changelog\n\n";
538
+ const updated = injectUnreleased(text, aiText);
539
+ fs.writeFileSync(changelogPath, updated);
540
+
541
+ ok(`CHANGELOG.md updated with AI-generated entry`);
542
+ if (provider === "template") {
543
+ info(`Tip: set ANTHROPIC_API_KEY for richer AI-written changelogs`);
544
+ }
545
+ console.log();
546
+ }
547
+
306
548
  // ── main ─────────────────────────────────────────────────────────────────────
307
549
 
308
550
  export async function changelogCommand(rawArgs) {
@@ -311,12 +553,14 @@ export async function changelogCommand(rawArgs) {
311
553
  // Sub-command: first non-flag arg
312
554
  const sub = args.find(a => !a.startsWith("-")) || "update";
313
555
 
314
- const dryRun = args.includes("--dry-run");
315
- const append = args.includes("--append");
316
- const asJson = args.includes("--json");
556
+ const dryRun = args.includes("--dry-run");
557
+ const append = args.includes("--append");
558
+ const asJson = args.includes("--json");
317
559
 
318
- const refIdx = args.indexOf("--ref");
319
- const ref = refIdx !== -1 ? args[refIdx + 1] : null;
560
+ const refIdx = args.indexOf("--ref");
561
+ const versionIdx = args.indexOf("--version");
562
+ const ref = refIdx !== -1 ? args[refIdx + 1] : null;
563
+ const version = versionIdx !== -1 ? args[versionIdx + 1] : null;
320
564
 
321
565
  const cwd = process.cwd();
322
566
  const changelogPath = path.join(cwd, "CHANGELOG.md");
@@ -338,6 +582,11 @@ export async function changelogCommand(rawArgs) {
338
582
  return;
339
583
  }
340
584
 
341
- fail(`Unknown sub-command: ${sub}`, "Use: update | show | list");
585
+ if (sub === "ai") {
586
+ await subcmdAi(cwd, changelogPath, { ref, dryRun, asJson, version });
587
+ return;
588
+ }
589
+
590
+ fail(`Unknown sub-command: ${sub}`, "Use: update | show | list | ai");
342
591
  process.exit(1);
343
592
  }