infernoflow 0.33.0 → 0.34.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.
Files changed (84) hide show
  1. package/README.md +208 -120
  2. package/dist/bin/infernoflow.mjs +271 -85
  3. package/dist/lib/adopters/angular.mjs +128 -1
  4. package/dist/lib/adopters/css.mjs +111 -1
  5. package/dist/lib/adopters/react.mjs +104 -1
  6. package/dist/lib/ai/ideDetection.mjs +31 -1
  7. package/dist/lib/ai/localProvider.mjs +88 -1
  8. package/dist/lib/ai/providerRouter.mjs +295 -2
  9. package/dist/lib/commands/adopt.mjs +869 -20
  10. package/dist/lib/commands/adoptWizard.mjs +320 -9
  11. package/dist/lib/commands/agent.mjs +191 -5
  12. package/dist/lib/commands/ai.mjs +407 -2
  13. package/dist/lib/commands/ask.mjs +299 -0
  14. package/dist/lib/commands/audit.mjs +300 -13
  15. package/dist/lib/commands/changelog.mjs +594 -26
  16. package/dist/lib/commands/check.mjs +184 -3
  17. package/dist/lib/commands/ci.mjs +208 -3
  18. package/dist/lib/commands/claudeMd.mjs +139 -28
  19. package/dist/lib/commands/cloud.mjs +521 -5
  20. package/dist/lib/commands/context.mjs +346 -34
  21. package/dist/lib/commands/coverage.mjs +282 -2
  22. package/dist/lib/commands/dashboard.mjs +635 -123
  23. package/dist/lib/commands/demo.mjs +465 -8
  24. package/dist/lib/commands/diff.mjs +274 -5
  25. package/dist/lib/commands/docGate.mjs +81 -2
  26. package/dist/lib/commands/doctor.mjs +321 -3
  27. package/dist/lib/commands/explain.mjs +438 -8
  28. package/dist/lib/commands/export.mjs +239 -10
  29. package/dist/lib/commands/generateSkills.mjs +163 -38
  30. package/dist/lib/commands/graph.mjs +378 -11
  31. package/dist/lib/commands/health.mjs +309 -2
  32. package/dist/lib/commands/impact.mjs +325 -2
  33. package/dist/lib/commands/implement.mjs +103 -7
  34. package/dist/lib/commands/init.mjs +545 -23
  35. package/dist/lib/commands/installCursorHooks.mjs +36 -1
  36. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
  37. package/dist/lib/commands/link.mjs +342 -2
  38. package/dist/lib/commands/log.mjs +164 -16
  39. package/dist/lib/commands/monorepo.mjs +428 -4
  40. package/dist/lib/commands/notify.mjs +258 -4
  41. package/dist/lib/commands/onboard.mjs +296 -4
  42. package/dist/lib/commands/prComment.mjs +361 -2
  43. package/dist/lib/commands/prImpact.mjs +157 -2
  44. package/dist/lib/commands/publish.mjs +316 -15
  45. package/dist/lib/commands/recap.mjs +359 -0
  46. package/dist/lib/commands/report.mjs +272 -28
  47. package/dist/lib/commands/review.mjs +223 -9
  48. package/dist/lib/commands/run.mjs +336 -8
  49. package/dist/lib/commands/scaffold.mjs +419 -54
  50. package/dist/lib/commands/scan.mjs +1118 -5
  51. package/dist/lib/commands/scout.mjs +291 -2
  52. package/dist/lib/commands/setup.mjs +310 -5
  53. package/dist/lib/commands/share.mjs +196 -13
  54. package/dist/lib/commands/snapshot.mjs +383 -3
  55. package/dist/lib/commands/stability.mjs +293 -2
  56. package/dist/lib/commands/stats.mjs +402 -0
  57. package/dist/lib/commands/status.mjs +172 -4
  58. package/dist/lib/commands/suggest.mjs +563 -21
  59. package/dist/lib/commands/switch.mjs +310 -0
  60. package/dist/lib/commands/syncAuto.mjs +96 -1
  61. package/dist/lib/commands/synthesize.mjs +228 -10
  62. package/dist/lib/commands/teamSync.mjs +388 -2
  63. package/dist/lib/commands/test.mjs +363 -6
  64. package/dist/lib/commands/theme.mjs +195 -18
  65. package/dist/lib/commands/upgrade.mjs +153 -0
  66. package/dist/lib/commands/version.mjs +282 -2
  67. package/dist/lib/commands/vibe.mjs +357 -7
  68. package/dist/lib/commands/watch.mjs +203 -4
  69. package/dist/lib/commands/why.mjs +358 -4
  70. package/dist/lib/cursorHooksInstall.mjs +60 -1
  71. package/dist/lib/draftToolingInstall.mjs +68 -7
  72. package/dist/lib/git/detect-drift.mjs +208 -4
  73. package/dist/lib/learning/adapt.mjs +101 -6
  74. package/dist/lib/learning/observe.mjs +119 -1
  75. package/dist/lib/learning/patternDetector.mjs +298 -1
  76. package/dist/lib/learning/profile.mjs +279 -2
  77. package/dist/lib/learning/skillSynthesizer.mjs +145 -24
  78. package/dist/lib/templates/index.mjs +131 -1
  79. package/dist/lib/theme/scanner.mjs +343 -4
  80. package/dist/lib/ui/errors.mjs +142 -1
  81. package/dist/lib/ui/output.mjs +72 -6
  82. package/dist/lib/ui/prompts.mjs +147 -6
  83. package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
  84. package/package.json +1 -1
@@ -1,2 +1,342 @@
1
- import*as d from"node:fs";import*as b from"node:path";import*as N from"node:https";import{done as J,warn as $,info as E,bold as k,cyan as I,gray as f,green as S,yellow as L}from"../ui/output.mjs";const x="links.json",T="integrations.json";function j(n){const o=b.join(n,x);if(!d.existsSync(o))return[];try{return JSON.parse(d.readFileSync(o,"utf8"))}catch{return[]}}function C(n,o){d.writeFileSync(b.join(n,x),JSON.stringify(o,null,2)+`
2
- `)}function U(n){const o=b.join(n,T);if(!d.existsSync(o))return{};try{return JSON.parse(d.readFileSync(o,"utf8"))}catch{return{}}}function A(n,o={}){return new Promise((i,t)=>{const l=new URL(n);N.get({hostname:l.hostname,path:l.pathname+(l.search||""),headers:{"User-Agent":"infernoflow-cli",Accept:"application/json",...o}},e=>{let r="";e.on("data",a=>r+=a),e.on("end",()=>{try{i({status:e.statusCode,body:JSON.parse(r)})}catch{i({status:e.statusCode,body:r})}})}).on("error",t)})}async function v(n,o){const i=process.env.JIRA_BASE_URL||o.jira?.baseUrl,t=process.env.JIRA_TOKEN||o.jira?.token,l=process.env.JIRA_EMAIL||o.jira?.email;if(!i||!t)return null;try{const e=Buffer.from(`${l}:${t}`).toString("base64"),r=await A(`${i}/rest/api/3/issue/${n}`,{Authorization:`Basic ${e}`});if(r.status===200)return{id:n,title:r.body.fields?.summary||n,status:r.body.fields?.status?.name||"unknown",url:`${i}/browse/${n}`}}catch{}return{id:n,title:n,status:"unknown",url:null}}async function R(n,o){const i=process.env.LINEAR_API_KEY||o.linear?.apiKey;if(!i)return null;try{const t=JSON.stringify({query:`{ issue(id: "${n}") { title state { name } url } }`}),e=(await new Promise((r,a)=>{const s=t,c=N.request({hostname:"api.linear.app",path:"/graphql",method:"POST",headers:{"Content-Type":"application/json",Authorization:i,"Content-Length":Buffer.byteLength(s)}},p=>{let g="";p.on("data",u=>g+=u),p.on("end",()=>r(JSON.parse(g)))});c.on("error",a),c.write(s),c.end()})).data?.issue;if(e)return{id:n,title:e.title,status:e.state?.name||"unknown",url:e.url}}catch{}return{id:n,title:n,status:"unknown",url:null}}async function _(n,o){const i=process.env.GITHUB_TOKEN||o.github?.token,t=process.env.GITHUB_REPOSITORY||o.github?.repo;if(!t)return null;try{const l={Authorization:i?`Bearer ${i}`:void 0},e=await A(`https://api.github.com/repos/${t}/issues/${n}`,l);if(e.status===200)return{id:`#${n}`,title:e.body.title||`Issue #${n}`,status:e.body.state||"unknown",url:e.body.html_url}}catch{}return{id:`#${n}`,title:`Issue #${n}`,status:"unknown",url:null}}async function F(n,o,i){const t=n.includes("--json"),l=n.indexOf("--capability"),e=n.indexOf("--jira"),r=n.indexOf("--linear"),a=n.indexOf("--github"),s=l!==-1?n[l+1]:null,c=e!==-1?n[e+1]:null,p=r!==-1?n[r+1]:null,g=a!==-1?n[a+1]:null;if(!s){const h="Usage: infernoflow link --capability <id> --jira <TICKET> | --linear <ID> | --github <NUM>";t?console.log(JSON.stringify({ok:!1,error:h})):$(h);return}let u=null,m=null;if(c)t||process.stdout.write(` Fetching Jira ${c}\u2026 `),u=await v(c,i),m="jira";else if(p)t||process.stdout.write(` Fetching Linear ${p}\u2026 `),u=await R(p,i),m="linear";else if(g)t||process.stdout.write(` Fetching GitHub #${g}\u2026 `),u=await _(g,i),m="github";else{const h="Specify --jira, --linear, or --github";t?console.log(JSON.stringify({ok:!1,error:h})):$(h);return}const w=j(o),O=w.findIndex(h=>h.capability===s),y={capability:s,platform:m,ticketId:u?.id||c||p||`#${g}`,title:u?.title||"",status:u?.status||"unknown",url:u?.url||null,linkedAt:new Date().toISOString()};O!==-1?w[O]=y:w.push(y),C(o,w),t||console.log(S("done")),t?console.log(JSON.stringify({ok:!0,link:y})):(J(`Linked: ${k(s)} \u2192 ${I(y.ticketId)} (${y.status})`),y.url&&console.log(` ${f(y.url)}`),console.log())}async function K(n,o){const i=n.includes("--json"),t=j(o);if(i){console.log(JSON.stringify({ok:!0,links:t}));return}if(!t.length){E("No links yet. Use: infernoflow link --capability <id> --jira <TICKET>");return}console.log(),console.log(` ${k(`${t.length} capability link${t.length!==1?"s":""}`)}`),console.log();const l=Math.max(...t.map(e=>e.capability.length),10)+2;for(const e of t){const r=e.status?.toLowerCase()==="done"||e.status?.toLowerCase()==="closed"?S:e.status?.toLowerCase()==="in progress"?L:f;console.log(` ${k(e.capability.padEnd(l))} ${I(e.ticketId.padEnd(14))} ${r(e.status||"unknown")}`),e.title&&e.title!==e.ticketId&&console.log(` ${" ".repeat(l+2)}${f(e.title)}`)}console.log()}async function B(n,o){const i=n.includes("--json"),t=j(o);let l=null;for(const s of["contract.json","capabilities.json"]){const c=b.join(o,s);if(d.existsSync(c))try{l=JSON.parse(d.readFileSync(c,"utf8"));break}catch{}}const e=(l?.capabilities||[]).map(s=>typeof s=="string"?s:s.id),r=new Set(t.map(s=>s.capability)),a=e.filter(s=>!r.has(s));if(i){console.log(JSON.stringify({ok:!0,linked:t.length,unlinked:a.length,links:t,unlinkedCapabilities:a}));return}if(console.log(),console.log(` ${k("Capability link status")}`),console.log(),t.length){console.log(` ${f("Linked:")}`);for(const s of t){const c=s.status?.toLowerCase()==="done"?S("\u2714"):s.status?.toLowerCase()==="in progress"?L("\u27F3"):f("\u25CB");console.log(` ${c} ${k(s.capability)} ${I(s.ticketId)} ${f(s.status||"")}`)}console.log()}a.length&&(console.log(` ${f("Unlinked capabilities:")}`),a.forEach(s=>console.log(` ${f("\xB7")} ${s}`)),console.log()),console.log(` ${S(String(t.length))} linked \xB7 ${f(String(a.length))} unlinked`),console.log()}async function M(n,o){const i=n.includes("--json"),t=n.indexOf("--capability"),l=t!==-1?n[t+1]:null;if(!l){i?console.log(JSON.stringify({ok:!1,error:"Usage: infernoflow link remove --capability <id>"})):$("Usage: infernoflow link remove --capability <id>");return}const e=j(o),r=e.length,a=e.filter(s=>s.capability!==l);if(a.length===r){i?console.log(JSON.stringify({ok:!1,error:`No link found for: ${l}`})):$(`No link found for: ${l}`);return}C(o,a),i?console.log(JSON.stringify({ok:!0,removed:l})):(J(`Removed link for ${k(l)}`),console.log())}async function P(n){const o=n.slice(1),i=process.cwd(),t=b.join(i,"inferno");if(!d.existsSync(t)){const r="inferno/ not found. Run: infernoflow init";o.includes("--json")?console.log(JSON.stringify({ok:!1,error:r})):$(r),process.exit(1)}const l=U(t),e=o[0];return e==="list"?K(o.slice(1),t):e==="status"?B(o.slice(1),t):e==="remove"?M(o.slice(1),t):F(o,t,l)}export{P as linkCommand};
1
+ /**
2
+ * infernoflow link
3
+ *
4
+ * Link capabilities to tickets in Jira, Linear, or GitHub Issues.
5
+ * Stored in inferno/links.json — travels with the repo.
6
+ *
7
+ * Usage:
8
+ * infernoflow link --jira PROJ-123 --capability CreateTask
9
+ * infernoflow link --linear LIN-456 --capability FilterByTag
10
+ * infernoflow link --github 78 --capability ExportToCsv
11
+ * infernoflow link list Show all links
12
+ * infernoflow link status Show which caps have open tickets
13
+ * infernoflow link remove --capability CreateTask
14
+ * infernoflow link --json Machine-readable
15
+ *
16
+ * Config (inferno/integrations.json):
17
+ * {
18
+ * "jira": { "baseUrl": "https://myorg.atlassian.net", "token": "...", "email": "..." },
19
+ * "linear": { "apiKey": "lin_api_..." },
20
+ * "github": { "repo": "owner/repo", "token": "ghp_..." }
21
+ * }
22
+ *
23
+ * Env vars (override config):
24
+ * JIRA_BASE_URL, JIRA_TOKEN, JIRA_EMAIL
25
+ * LINEAR_API_KEY
26
+ * GITHUB_TOKEN, GITHUB_REPOSITORY
27
+ */
28
+
29
+ import * as fs from "node:fs";
30
+ import * as path from "node:path";
31
+ import * as https from "node:https";
32
+ import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
33
+
34
+ const LINKS_FILE = "links.json";
35
+ const CONFIG_FILE = "integrations.json";
36
+
37
+ // ── Storage ───────────────────────────────────────────────────────────────────
38
+
39
+ function readLinks(infernoDir) {
40
+ const p = path.join(infernoDir, LINKS_FILE);
41
+ if (!fs.existsSync(p)) return [];
42
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return []; }
43
+ }
44
+
45
+ function writeLinks(infernoDir, links) {
46
+ fs.writeFileSync(path.join(infernoDir, LINKS_FILE), JSON.stringify(links, null, 2) + "\n");
47
+ }
48
+
49
+ function readIntegrationConfig(infernoDir) {
50
+ const p = path.join(infernoDir, CONFIG_FILE);
51
+ if (!fs.existsSync(p)) return {};
52
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
53
+ }
54
+
55
+ // ── HTTP helper ───────────────────────────────────────────────────────────────
56
+
57
+ function httpsGet(url, headers = {}) {
58
+ return new Promise((resolve, reject) => {
59
+ const parsed = new URL(url);
60
+ https.get({
61
+ hostname: parsed.hostname,
62
+ path: parsed.pathname + (parsed.search || ""),
63
+ headers: { "User-Agent": "infernoflow-cli", "Accept": "application/json", ...headers },
64
+ }, (res) => {
65
+ let data = "";
66
+ res.on("data", d => (data += d));
67
+ res.on("end", () => {
68
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
69
+ catch { resolve({ status: res.statusCode, body: data }); }
70
+ });
71
+ }).on("error", reject);
72
+ });
73
+ }
74
+
75
+ // ── Ticket fetchers ───────────────────────────────────────────────────────────
76
+
77
+ async function fetchJiraTicket(ticketId, config) {
78
+ const base = process.env.JIRA_BASE_URL || config.jira?.baseUrl;
79
+ const token = process.env.JIRA_TOKEN || config.jira?.token;
80
+ const email = process.env.JIRA_EMAIL || config.jira?.email;
81
+ if (!base || !token) return null;
82
+
83
+ try {
84
+ const creds = Buffer.from(`${email}:${token}`).toString("base64");
85
+ const resp = await httpsGet(`${base}/rest/api/3/issue/${ticketId}`, {
86
+ "Authorization": `Basic ${creds}`,
87
+ });
88
+ if (resp.status === 200) {
89
+ return {
90
+ id: ticketId,
91
+ title: resp.body.fields?.summary || ticketId,
92
+ status: resp.body.fields?.status?.name || "unknown",
93
+ url: `${base}/browse/${ticketId}`,
94
+ };
95
+ }
96
+ } catch {}
97
+ return { id: ticketId, title: ticketId, status: "unknown", url: null };
98
+ }
99
+
100
+ async function fetchLinearTicket(ticketId, config) {
101
+ const apiKey = process.env.LINEAR_API_KEY || config.linear?.apiKey;
102
+ if (!apiKey) return null;
103
+
104
+ try {
105
+ const query = JSON.stringify({
106
+ query: `{ issue(id: "${ticketId}") { title state { name } url } }`
107
+ });
108
+ const resp = await new Promise((resolve, reject) => {
109
+ const body = query;
110
+ const req = https.request({
111
+ hostname: "api.linear.app",
112
+ path: "/graphql",
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json", "Authorization": apiKey, "Content-Length": Buffer.byteLength(body) },
115
+ }, (res) => {
116
+ let data = "";
117
+ res.on("data", d => (data += d));
118
+ res.on("end", () => resolve(JSON.parse(data)));
119
+ });
120
+ req.on("error", reject);
121
+ req.write(body);
122
+ req.end();
123
+ });
124
+ const issue = resp.data?.issue;
125
+ if (issue) return { id: ticketId, title: issue.title, status: issue.state?.name || "unknown", url: issue.url };
126
+ } catch {}
127
+ return { id: ticketId, title: ticketId, status: "unknown", url: null };
128
+ }
129
+
130
+ async function fetchGithubIssue(issueNum, config) {
131
+ const token = process.env.GITHUB_TOKEN || config.github?.token;
132
+ const repo = process.env.GITHUB_REPOSITORY || config.github?.repo;
133
+ if (!repo) return null;
134
+
135
+ try {
136
+ const headers = { "Authorization": token ? `Bearer ${token}` : undefined };
137
+ const resp = await httpsGet(`https://api.github.com/repos/${repo}/issues/${issueNum}`, headers);
138
+ if (resp.status === 200) {
139
+ return {
140
+ id: `#${issueNum}`,
141
+ title: resp.body.title || `Issue #${issueNum}`,
142
+ status: resp.body.state || "unknown",
143
+ url: resp.body.html_url,
144
+ };
145
+ }
146
+ } catch {}
147
+ return { id: `#${issueNum}`, title: `Issue #${issueNum}`, status: "unknown", url: null };
148
+ }
149
+
150
+ // ── Sub-commands ──────────────────────────────────────────────────────────────
151
+
152
+ async function subcmdAdd(args, infernoDir, config) {
153
+ const jsonMode = args.includes("--json");
154
+ const capIdx = args.indexOf("--capability");
155
+ const jiraIdx = args.indexOf("--jira");
156
+ const linearIdx = args.indexOf("--linear");
157
+ const githubIdx = args.indexOf("--github");
158
+
159
+ const capability = capIdx !== -1 ? args[capIdx + 1] : null;
160
+ const jiraId = jiraIdx !== -1 ? args[jiraIdx + 1] : null;
161
+ const linearId = linearIdx !== -1 ? args[linearIdx + 1] : null;
162
+ const githubNum = githubIdx !== -1 ? args[githubIdx + 1] : null;
163
+
164
+ if (!capability) {
165
+ const msg = "Usage: infernoflow link --capability <id> --jira <TICKET> | --linear <ID> | --github <NUM>";
166
+ if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
167
+ return;
168
+ }
169
+
170
+ let ticket = null;
171
+ let platform = null;
172
+
173
+ if (jiraId) {
174
+ if (!jsonMode) process.stdout.write(` Fetching Jira ${jiraId}… `);
175
+ ticket = await fetchJiraTicket(jiraId, config);
176
+ platform = "jira";
177
+ } else if (linearId) {
178
+ if (!jsonMode) process.stdout.write(` Fetching Linear ${linearId}… `);
179
+ ticket = await fetchLinearTicket(linearId, config);
180
+ platform = "linear";
181
+ } else if (githubNum) {
182
+ if (!jsonMode) process.stdout.write(` Fetching GitHub #${githubNum}… `);
183
+ ticket = await fetchGithubIssue(githubNum, config);
184
+ platform = "github";
185
+ } else {
186
+ const msg = "Specify --jira, --linear, or --github";
187
+ if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
188
+ return;
189
+ }
190
+
191
+ const links = readLinks(infernoDir);
192
+ const existing = links.findIndex(l => l.capability === capability);
193
+
194
+ const link = {
195
+ capability,
196
+ platform,
197
+ ticketId: ticket?.id || jiraId || linearId || `#${githubNum}`,
198
+ title: ticket?.title || "",
199
+ status: ticket?.status || "unknown",
200
+ url: ticket?.url || null,
201
+ linkedAt: new Date().toISOString(),
202
+ };
203
+
204
+ if (existing !== -1) {
205
+ links[existing] = link;
206
+ } else {
207
+ links.push(link);
208
+ }
209
+
210
+ writeLinks(infernoDir, links);
211
+
212
+ if (!jsonMode) console.log(green("done"));
213
+
214
+ if (jsonMode) {
215
+ console.log(JSON.stringify({ ok: true, link }));
216
+ } else {
217
+ done(`Linked: ${bold(capability)} → ${cyan(link.ticketId)} (${link.status})`);
218
+ if (link.url) console.log(` ${gray(link.url)}`);
219
+ console.log();
220
+ }
221
+ }
222
+
223
+ async function subcmdList(args, infernoDir) {
224
+ const jsonMode = args.includes("--json");
225
+ const links = readLinks(infernoDir);
226
+
227
+ if (jsonMode) {
228
+ console.log(JSON.stringify({ ok: true, links }));
229
+ return;
230
+ }
231
+
232
+ if (!links.length) {
233
+ info("No links yet. Use: infernoflow link --capability <id> --jira <TICKET>");
234
+ return;
235
+ }
236
+
237
+ console.log();
238
+ console.log(` ${bold(`${links.length} capability link${links.length !== 1 ? "s" : ""}`)}`);
239
+ console.log();
240
+
241
+ const w = Math.max(...links.map(l => l.capability.length), 10) + 2;
242
+ for (const l of links) {
243
+ const statusColor = l.status?.toLowerCase() === "done" || l.status?.toLowerCase() === "closed"
244
+ ? green : l.status?.toLowerCase() === "in progress" ? yellow : gray;
245
+ console.log(` ${bold(l.capability.padEnd(w))} ${cyan(l.ticketId.padEnd(14))} ${statusColor(l.status || "unknown")}`);
246
+ if (l.title && l.title !== l.ticketId) console.log(` ${" ".repeat(w + 2)}${gray(l.title)}`);
247
+ }
248
+ console.log();
249
+ }
250
+
251
+ async function subcmdStatus(args, infernoDir) {
252
+ const jsonMode = args.includes("--json");
253
+ const links = readLinks(infernoDir);
254
+
255
+ // Load contract to find unlinked capabilities
256
+ let contract = null;
257
+ for (const f of ["contract.json", "capabilities.json"]) {
258
+ const p = path.join(infernoDir, f);
259
+ if (fs.existsSync(p)) { try { contract = JSON.parse(fs.readFileSync(p, "utf8")); break; } catch {} }
260
+ }
261
+ const allCaps = (contract?.capabilities || []).map(c => typeof c === "string" ? c : c.id);
262
+ const linkedIds = new Set(links.map(l => l.capability));
263
+ const unlinked = allCaps.filter(id => !linkedIds.has(id));
264
+
265
+ if (jsonMode) {
266
+ console.log(JSON.stringify({ ok: true, linked: links.length, unlinked: unlinked.length, links, unlinkedCapabilities: unlinked }));
267
+ return;
268
+ }
269
+
270
+ console.log();
271
+ console.log(` ${bold("Capability link status")}`);
272
+ console.log();
273
+
274
+ if (links.length) {
275
+ console.log(` ${gray("Linked:")}`);
276
+ for (const l of links) {
277
+ const icon = l.status?.toLowerCase() === "done" ? green("✔") : l.status?.toLowerCase() === "in progress" ? yellow("⟳") : gray("○");
278
+ console.log(` ${icon} ${bold(l.capability)} ${cyan(l.ticketId)} ${gray(l.status || "")}`);
279
+ }
280
+ console.log();
281
+ }
282
+
283
+ if (unlinked.length) {
284
+ console.log(` ${gray("Unlinked capabilities:")}`);
285
+ unlinked.forEach(id => console.log(` ${gray("·")} ${id}`));
286
+ console.log();
287
+ }
288
+
289
+ console.log(` ${green(String(links.length))} linked · ${gray(String(unlinked.length))} unlinked`);
290
+ console.log();
291
+ }
292
+
293
+ async function subcmdRemove(args, infernoDir) {
294
+ const jsonMode = args.includes("--json");
295
+ const capIdx = args.indexOf("--capability");
296
+ const capId = capIdx !== -1 ? args[capIdx + 1] : null;
297
+
298
+ if (!capId) {
299
+ if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "Usage: infernoflow link remove --capability <id>" })); }
300
+ else { warn("Usage: infernoflow link remove --capability <id>"); }
301
+ return;
302
+ }
303
+
304
+ const links = readLinks(infernoDir);
305
+ const before = links.length;
306
+ const updated = links.filter(l => l.capability !== capId);
307
+
308
+ if (updated.length === before) {
309
+ if (jsonMode) { console.log(JSON.stringify({ ok: false, error: `No link found for: ${capId}` })); }
310
+ else { warn(`No link found for: ${capId}`); }
311
+ return;
312
+ }
313
+
314
+ writeLinks(infernoDir, updated);
315
+ if (jsonMode) { console.log(JSON.stringify({ ok: true, removed: capId })); }
316
+ else { done(`Removed link for ${bold(capId)}`); console.log(); }
317
+ }
318
+
319
+ // ── Entry ─────────────────────────────────────────────────────────────────────
320
+
321
+ export async function linkCommand(rawArgs) {
322
+ const args = rawArgs.slice(1);
323
+ const cwd = process.cwd();
324
+ const infernoDir = path.join(cwd, "inferno");
325
+
326
+ if (!fs.existsSync(infernoDir)) {
327
+ const msg = "inferno/ not found. Run: infernoflow init";
328
+ if (args.includes("--json")) { console.log(JSON.stringify({ ok: false, error: msg })); }
329
+ else { warn(msg); }
330
+ process.exit(1);
331
+ }
332
+
333
+ const config = readIntegrationConfig(infernoDir);
334
+ const subcmd = args[0];
335
+
336
+ if (subcmd === "list") return subcmdList(args.slice(1), infernoDir);
337
+ if (subcmd === "status") return subcmdStatus(args.slice(1), infernoDir);
338
+ if (subcmd === "remove") return subcmdRemove(args.slice(1), infernoDir);
339
+
340
+ // Default: add a link
341
+ return subcmdAdd(args, infernoDir, config);
342
+ }
@@ -1,16 +1,164 @@
1
- import*as l from"node:fs";import*as y from"node:path";import"node:os";import{bold as w,cyan as _,gray as n,green as $,red as h}from"../ui/output.mjs";const O="inferno",i=y.join(O,"sessions.jsonl"),N=["note","attempt","decision","gotcha","preference","theme","handoff","error"],E=["worked","failed","partial","unknown"];function j(){return l.existsSync(i)?l.readFileSync(i,"utf8").split(`
2
- `).filter(Boolean).map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(Boolean):[]}function D(e){l.existsSync(O)||(console.error(h(` \u2718 inferno/ not found \u2014 run: infernoflow init
3
- `)),process.exit(1)),l.appendFileSync(i,JSON.stringify(e)+`
4
- `,"utf8")}function R(){return process.env.CURSOR_SESSION?"cursor":process.env.COPILOT_SESSION?"copilot":process.env.CLAUDE_CODE_SESSION?"claude":process.env.WINDSURF_SESSION?"windsurf":process.env.INFERNOFLOW_AGENT?process.env.INFERNOFLOW_AGENT:"human"}function T(e,c){const r=new Date(e.ts).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),t=e.type||"note",d=t==="gotcha"?"\x1B[33m":t==="decision"?"\x1B[36m":t==="theme"?"\x1B[35m":t==="preference"?"\x1B[34m":t==="attempt"?"\x1B[90m":t==="error"?"\x1B[31m":"\x1B[0m",m="\x1B[0m",S=e.result?` [${e.result}]`:"",a=e.agent&&e.agent!=="human"?n(` (${e.agent})`):"";return` ${n(String(c+1).padStart(3))} ${n(r)} ${d}${t}${m}${S} ${e.summary}${a}`}async function U(e){const c=o=>e.includes(o),r=(o,g)=>{const u=e.indexOf(o);return u!==-1&&e[u+1]?e[u+1]:g},t=c("--show"),d=c("--clear"),m=c("--json");if(t||m){const o=j(),g=e[e.indexOf("--show")+1],u=g&&/^\d+$/.test(g)?parseInt(g):20,p=o.slice(-u);if(m){console.log(JSON.stringify(p,null,2));return}if(console.log(`
5
- `+w("\u{1F525} infernoflow \u2014 session memory")),console.log(" "+"\u2500".repeat(50)),!p.length){console.log(n(`
6
- No entries yet. Start logging with: infernoflow log "<what happened>"
7
- `));return}console.log(n(` Showing last ${p.length} of ${o.length} entries
8
- `)),p.forEach((L,v)=>console.log(T(L,o.length-p.length+v))),console.log();return}if(d){if(!l.existsSync(i)){console.log(n(` Nothing to clear.
9
- `));return}const o=i.replace(".jsonl",`-archive-${Date.now()}.jsonl`);l.renameSync(i,o),console.log($(` \u2714 Session log archived \u2192 ${y.basename(o)}
10
- `));return}const a=e.filter(o=>!o.startsWith("--")&&o!==r("--type","")&&o!==r("--result","")&&o!==r("--agent","")).join(" ").trim();if(!a){console.log(`
11
- `+w("\u{1F525} infernoflow log")+` \u2014 append to session memory
12
- `),console.log(n(" Usage:")),console.log(n(' infernoflow log "what happened"')),console.log(n(' infernoflow log "tried X, failed because Y" --type attempt --result failed')),console.log(n(' infernoflow log "always use multipart/form-data" --type gotcha')),console.log(n(' infernoflow log "switched to dark mode" --type theme')),console.log(n(" infernoflow log --show Print last 20 entries")),console.log(n(" infernoflow log --json Print as JSON")),console.log(),console.log(n(" Types: note \xB7 attempt \xB7 decision \xB7 gotcha \xB7 preference \xB7 theme \xB7 handoff \xB7 error")),console.log(n(` Results: worked \xB7 failed \xB7 partial \xB7 unknown
13
- `));return}const f=r("--type","note"),s=r("--result",null),I=r("--agent",R());N.includes(f)||(console.error(h(` \u2718 Invalid type: ${f}. Valid: ${N.join(", ")}
14
- `)),process.exit(1)),s&&!E.includes(s)&&(console.error(h(` \u2718 Invalid result: ${s}. Valid: ${E.join(", ")}
15
- `)),process.exit(1));const x={ts:new Date().toISOString(),agent:I,type:f,summary:a,...s?{result:s}:{}};D(x);const b=f!=="note"?_(` [${f}]`):"",F=s?n(` \u2192 ${s}`):"";console.log($(` \u2714 Logged${b}${F}: `)+a+`
16
- `)}export{U as logCommand};
1
+ /**
2
+ * infernoflow log
3
+ *
4
+ * Appends a human or agent entry to inferno/sessions.jsonl
5
+ * the append-only memory file that captures what AI can't infer from code:
6
+ * failed attempts, decisions, gotchas, preferences, theme changes.
7
+ *
8
+ * Usage:
9
+ * infernoflow log "tried streaming upload, server rejected chunked transfer"
10
+ * infernoflow log "API expects multipart/form-data" --type gotcha
11
+ * infernoflow log "switched primary color to #f97316" --type theme
12
+ * infernoflow log "user prefers inline styles" --type preference
13
+ * infernoflow log "axios over fetch because of progress events" --type decision --result worked
14
+ * infernoflow log --show Print last 20 entries
15
+ * infernoflow log --show 5 Print last 5 entries
16
+ * infernoflow log --clear Archive and clear the log
17
+ * infernoflow log --json Print entries as JSON array
18
+ */
19
+
20
+ import * as fs from "node:fs";
21
+ import * as path from "node:path";
22
+ import * as os from "node:os";
23
+ import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
24
+
25
+ const INFERNO_DIR = "inferno";
26
+ const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
27
+
28
+ const VALID_TYPES = ["note","attempt","decision","gotcha","preference","theme","handoff","error"];
29
+ const VALID_RESULTS = ["worked","failed","partial","unknown"];
30
+
31
+ function readEntries() {
32
+ if (!fs.existsSync(SESSIONS_FILE)) return [];
33
+ return fs.readFileSync(SESSIONS_FILE, "utf8")
34
+ .split("\n")
35
+ .filter(Boolean)
36
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
37
+ .filter(Boolean);
38
+ }
39
+
40
+ function appendEntry(entry) {
41
+ if (!fs.existsSync(INFERNO_DIR)) {
42
+ console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
43
+ process.exit(1);
44
+ }
45
+ fs.appendFileSync(SESSIONS_FILE, JSON.stringify(entry) + "\n", "utf8");
46
+ }
47
+
48
+ function detectAgent() {
49
+ // Try to detect which AI agent is running this
50
+ if (process.env.CURSOR_SESSION) return "cursor";
51
+ if (process.env.COPILOT_SESSION) return "copilot";
52
+ if (process.env.CLAUDE_CODE_SESSION) return "claude";
53
+ if (process.env.WINDSURF_SESSION) return "windsurf";
54
+ if (process.env.INFERNOFLOW_AGENT) return process.env.INFERNOFLOW_AGENT;
55
+ return "human";
56
+ }
57
+
58
+ function formatEntry(e, i) {
59
+ const ts = new Date(e.ts).toLocaleString("en-GB", { day:"2-digit", month:"short", hour:"2-digit", minute:"2-digit" });
60
+ const type = e.type || "note";
61
+ const color = type === "gotcha" ? "\x1b[33m" // yellow
62
+ : type === "decision" ? "\x1b[36m" // cyan
63
+ : type === "theme" ? "\x1b[35m" // magenta
64
+ : type === "preference" ? "\x1b[34m" // blue
65
+ : type === "attempt" ? "\x1b[90m" // dark gray
66
+ : type === "error" ? "\x1b[31m" // red
67
+ : "\x1b[0m";
68
+ const reset = "\x1b[0m";
69
+ const result = e.result ? ` [${e.result}]` : "";
70
+ const agent = e.agent && e.agent !== "human" ? gray(` (${e.agent})`) : "";
71
+ return ` ${gray(String(i+1).padStart(3))} ${gray(ts)} ${color}${type}${reset}${result} ${e.summary}${agent}`;
72
+ }
73
+
74
+ export async function logCommand(args) {
75
+ const has = (f) => args.includes(f);
76
+ const flag = (f, def) => { const i = args.indexOf(f); return i !== -1 && args[i+1] ? args[i+1] : def; };
77
+
78
+ const showFlag = has("--show");
79
+ const clearFlag = has("--clear");
80
+ const jsonFlag = has("--json");
81
+
82
+ // ── Show mode ───────────────────────────────────────────────────────────────
83
+ if (showFlag || jsonFlag) {
84
+ const entries = readEntries();
85
+ const countArg = args[args.indexOf("--show") + 1];
86
+ const count = countArg && /^\d+$/.test(countArg) ? parseInt(countArg) : 20;
87
+ const recent = entries.slice(-count);
88
+
89
+ if (jsonFlag) {
90
+ console.log(JSON.stringify(recent, null, 2));
91
+ return;
92
+ }
93
+
94
+ console.log("\n " + bold("🔥 infernoflow — session memory"));
95
+ console.log(" " + "─".repeat(50));
96
+ if (!recent.length) {
97
+ console.log(gray("\n No entries yet. Start logging with: infernoflow log \"<what happened>\"\n"));
98
+ return;
99
+ }
100
+ console.log(gray(` Showing last ${recent.length} of ${entries.length} entries\n`));
101
+ recent.forEach((e, i) => console.log(formatEntry(e, entries.length - recent.length + i)));
102
+ console.log();
103
+ return;
104
+ }
105
+
106
+ // ── Clear mode ──────────────────────────────────────────────────────────────
107
+ if (clearFlag) {
108
+ if (!fs.existsSync(SESSIONS_FILE)) {
109
+ console.log(gray(" Nothing to clear.\n"));
110
+ return;
111
+ }
112
+ const archive = SESSIONS_FILE.replace(".jsonl", `-archive-${Date.now()}.jsonl`);
113
+ fs.renameSync(SESSIONS_FILE, archive);
114
+ console.log(green(` ✔ Session log archived → ${path.basename(archive)}\n`));
115
+ return;
116
+ }
117
+
118
+ // ── Append mode ─────────────────────────────────────────────────────────────
119
+ // Collect the message — everything that's not a flag
120
+ const messageTokens = args.filter(a => !a.startsWith("--") && a !== flag("--type","") && a !== flag("--result","") && a !== flag("--agent",""));
121
+ const summary = messageTokens.join(" ").trim();
122
+
123
+ if (!summary) {
124
+ console.log("\n " + bold("🔥 infernoflow log") + " — append to session memory\n");
125
+ console.log(gray(" Usage:"));
126
+ console.log(gray(' infernoflow log "what happened"'));
127
+ console.log(gray(' infernoflow log "tried X, failed because Y" --type attempt --result failed'));
128
+ console.log(gray(' infernoflow log "always use multipart/form-data" --type gotcha'));
129
+ console.log(gray(' infernoflow log "switched to dark mode" --type theme'));
130
+ console.log(gray(' infernoflow log --show Print last 20 entries'));
131
+ console.log(gray(' infernoflow log --json Print as JSON'));
132
+ console.log();
133
+ console.log(gray(" Types: note · attempt · decision · gotcha · preference · theme · handoff · error"));
134
+ console.log(gray(" Results: worked · failed · partial · unknown\n"));
135
+ return;
136
+ }
137
+
138
+ const type = flag("--type", "note");
139
+ const result = flag("--result", null);
140
+ const agent = flag("--agent", detectAgent());
141
+
142
+ if (!VALID_TYPES.includes(type)) {
143
+ console.error(red(` ✘ Invalid type: ${type}. Valid: ${VALID_TYPES.join(", ")}\n`));
144
+ process.exit(1);
145
+ }
146
+ if (result && !VALID_RESULTS.includes(result)) {
147
+ console.error(red(` ✘ Invalid result: ${result}. Valid: ${VALID_RESULTS.join(", ")}\n`));
148
+ process.exit(1);
149
+ }
150
+
151
+ const entry = {
152
+ ts: new Date().toISOString(),
153
+ agent,
154
+ type,
155
+ summary,
156
+ ...(result ? { result } : {}),
157
+ };
158
+
159
+ appendEntry(entry);
160
+
161
+ const typeLabel = type !== "note" ? cyan(` [${type}]`) : "";
162
+ const resultLabel = result ? gray(` → ${result}`) : "";
163
+ console.log(green(` ✔ Logged${typeLabel}${resultLabel}: `) + summary + "\n");
164
+ }