infernoflow 0.37.1 → 0.37.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/CHANGELOG.md +71 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -520
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,298 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* lib/learning/patternDetector.mjs
|
|
3
|
-
*
|
|
4
|
-
* Analyzes recentSessions from developer-profile.json and surfaces two kinds
|
|
5
|
-
* of candidates:
|
|
6
|
-
*
|
|
7
|
-
* 1. AGENT candidates — command sequences repeated 3+ times in the same order
|
|
8
|
-
* e.g. ["suggest","implement","check","changelog"] → "release-feature" agent
|
|
9
|
-
*
|
|
10
|
-
* 2. SKILL candidates — task descriptions that share a template pattern
|
|
11
|
-
* e.g. "add X filter", "add Y filter", "add Z filter"
|
|
12
|
-
* → skill: "When adding filters, follow the FilterBy* naming convention"
|
|
13
|
-
*
|
|
14
|
-
* Returns: { agentCandidates: Candidate[], skillCandidates: Candidate[] }
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
// ── Sequence analysis ─────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Extract all command sequences from sessions.
|
|
21
|
-
* Returns an array of string[] — one per session.
|
|
22
|
-
*/
|
|
23
|
-
function extractSequences(sessions) {
|
|
24
|
-
return (sessions || [])
|
|
25
|
-
.map(s => (s.commands || []).map(c => c.cmd))
|
|
26
|
-
.filter(seq => seq.length >= 2);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Find subsequences that repeat across sessions.
|
|
31
|
-
* A subsequence is a contiguous run of 2+ commands.
|
|
32
|
-
* Returns Map<sequenceKey, { seq, count, sessions[] }>
|
|
33
|
-
*/
|
|
34
|
-
function findRepeatedSubsequences(sequences, minLen = 2, minFreq = 2) {
|
|
35
|
-
const counts = new Map();
|
|
36
|
-
|
|
37
|
-
for (let si = 0; si < sequences.length; si++) {
|
|
38
|
-
const seq = sequences[si];
|
|
39
|
-
const seen = new Set(); // deduplicate within one session
|
|
40
|
-
for (let start = 0; start < seq.length; start++) {
|
|
41
|
-
for (let end = start + minLen; end <= seq.length; end++) {
|
|
42
|
-
const sub = seq.slice(start, end);
|
|
43
|
-
const key = sub.join("→");
|
|
44
|
-
if (seen.has(key)) continue;
|
|
45
|
-
seen.add(key);
|
|
46
|
-
if (!counts.has(key)) counts.set(key, { seq: sub, count: 0, sessionIndexes: [] });
|
|
47
|
-
const entry = counts.get(key);
|
|
48
|
-
entry.count++;
|
|
49
|
-
entry.sessionIndexes.push(si);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Filter by minimum frequency and return sorted by count desc
|
|
55
|
-
return Array.from(counts.values())
|
|
56
|
-
.filter(e => e.count >= minFreq)
|
|
57
|
-
.sort((a, b) => b.count - a.count || b.seq.length - a.seq.length);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Remove subsequences that are strict subsets of a longer one with equal count.
|
|
62
|
-
* e.g. if "suggest→implement→check" appears 4x and "suggest→implement" appears 4x,
|
|
63
|
-
* drop the shorter one.
|
|
64
|
-
*/
|
|
65
|
-
function deduplicateSubsequences(entries) {
|
|
66
|
-
return entries.filter(e => {
|
|
67
|
-
const eKey = e.seq.join("→");
|
|
68
|
-
// Keep if no longer sequence contains this one with same or higher count
|
|
69
|
-
return !entries.some(other => {
|
|
70
|
-
if (other === e) return false;
|
|
71
|
-
const otherKey = other.seq.join("→");
|
|
72
|
-
return other.seq.length > e.seq.length &&
|
|
73
|
-
other.count >= e.count &&
|
|
74
|
-
otherKey.includes(eKey);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── Task template analysis ────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Extract all task strings from sessions.
|
|
83
|
-
*/
|
|
84
|
-
function extractTasks(sessions) {
|
|
85
|
-
const tasks = [];
|
|
86
|
-
for (const session of sessions || []) {
|
|
87
|
-
for (const cmd of session.commands || []) {
|
|
88
|
-
if (cmd.task && typeof cmd.task === "string") {
|
|
89
|
-
tasks.push({ task: cmd.task, cmd: cmd.cmd, sessionId: session.id });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return tasks;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Tokenize a task string into [verb, ...rest].
|
|
98
|
-
* "add due date filter" → ["add", "due date filter"]
|
|
99
|
-
*/
|
|
100
|
-
function tokenize(task) {
|
|
101
|
-
const words = task.toLowerCase().trim().split(/\s+/);
|
|
102
|
-
return words;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Find the longest common prefix between two word arrays.
|
|
107
|
-
*/
|
|
108
|
-
function commonPrefixLen(a, b) {
|
|
109
|
-
let i = 0;
|
|
110
|
-
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
111
|
-
return i;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Group tasks by their verb (first word) and detect template patterns.
|
|
116
|
-
* Returns Map<verb, { verb, tasks, template, frequency }>
|
|
117
|
-
*/
|
|
118
|
-
function detectTaskTemplates(taskEntries, minFreq = 2) {
|
|
119
|
-
// Group by first word (verb)
|
|
120
|
-
const byVerb = new Map();
|
|
121
|
-
for (const { task, cmd } of taskEntries) {
|
|
122
|
-
const words = tokenize(task);
|
|
123
|
-
if (words.length < 2) continue;
|
|
124
|
-
const verb = words[0];
|
|
125
|
-
if (!byVerb.has(verb)) byVerb.set(verb, []);
|
|
126
|
-
byVerb.get(verb).push({ words, original: task, cmd });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const templates = [];
|
|
130
|
-
for (const [verb, entries] of byVerb) {
|
|
131
|
-
if (entries.length < minFreq) continue;
|
|
132
|
-
|
|
133
|
-
// Find common prefix across all tasks with this verb
|
|
134
|
-
let prefix = entries[0].words;
|
|
135
|
-
for (const e of entries.slice(1)) {
|
|
136
|
-
const len = commonPrefixLen(prefix, e.words);
|
|
137
|
-
prefix = prefix.slice(0, Math.max(1, len));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const prefixStr = prefix.join(" ");
|
|
141
|
-
const templateStr = prefixStr + (prefix.length < entries[0].words.length ? " {feature}" : "");
|
|
142
|
-
const commands = [...new Set(entries.map(e => e.cmd))];
|
|
143
|
-
|
|
144
|
-
templates.push({
|
|
145
|
-
verb,
|
|
146
|
-
template: templateStr,
|
|
147
|
-
examples: entries.slice(0, 3).map(e => e.original),
|
|
148
|
-
frequency: entries.length,
|
|
149
|
-
commands,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return templates.sort((a, b) => b.frequency - a.frequency);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── Candidate builders ────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
function agentName(seq) {
|
|
159
|
-
// Generate a readable name from the sequence
|
|
160
|
-
const known = {
|
|
161
|
-
"suggest→implement→check": "feature-workflow",
|
|
162
|
-
"suggest→check": "quick-suggest",
|
|
163
|
-
"check→changelog→publish": "release-workflow",
|
|
164
|
-
"suggest→implement→check→changelog": "full-feature-cycle",
|
|
165
|
-
"diff→changelog": "changelog-from-diff",
|
|
166
|
-
"context→implement": "context-first-implement",
|
|
167
|
-
"check→diff": "health-check",
|
|
168
|
-
};
|
|
169
|
-
const key = seq.join("→");
|
|
170
|
-
if (known[key]) return known[key];
|
|
171
|
-
return seq.join("-");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function agentDescription(seq) {
|
|
175
|
-
const labels = {
|
|
176
|
-
suggest: "generate contract update",
|
|
177
|
-
implement: "generate implementation prompt",
|
|
178
|
-
check: "validate contract health",
|
|
179
|
-
changelog: "draft changelog entry",
|
|
180
|
-
diff: "show capability diff",
|
|
181
|
-
context: "refresh AI context",
|
|
182
|
-
run: "run full task flow",
|
|
183
|
-
publish: "publish new version",
|
|
184
|
-
setup: "set up project",
|
|
185
|
-
status: "show status",
|
|
186
|
-
};
|
|
187
|
-
return seq.map(cmd => labels[cmd] || cmd).join(" → ");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function skillName(template) {
|
|
191
|
-
return template.replace(/[^a-zA-Z0-9]+/g, "-").replace(/-+/g, "-").toLowerCase().slice(0, 40);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function confidence(frequency, totalSessions) {
|
|
195
|
-
// Sigmoid-like: saturates at 1.0 around 8+ occurrences
|
|
196
|
-
const raw = Math.min(frequency / Math.max(totalSessions * 0.4, 4), 1);
|
|
197
|
-
return Math.round(raw * 100) / 100;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── Main export ───────────────────────────────────────────────────────────────
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Analyze sessions and return skill + agent candidates.
|
|
204
|
-
*
|
|
205
|
-
* @param {object} profile — the full developer profile
|
|
206
|
-
* @param {object} options
|
|
207
|
-
* @param {number} [options.minFreq=3] — minimum repetitions to surface
|
|
208
|
-
* @param {number} [options.minSeqLen=2] — minimum sequence length for agents
|
|
209
|
-
* @returns {{ agentCandidates: Candidate[], skillCandidates: Candidate[] }}
|
|
210
|
-
*/
|
|
211
|
-
export function detectPatterns(profile, { minFreq = 3, minSeqLen = 2 } = {}) {
|
|
212
|
-
const sessions = profile.recentSessions || [];
|
|
213
|
-
const totalSessions = Math.max(sessions.length, 1);
|
|
214
|
-
|
|
215
|
-
// ── Agent candidates (command sequences) ─────────────────────────────────
|
|
216
|
-
const sequences = extractSequences(sessions);
|
|
217
|
-
const repeated = findRepeatedSubsequences(sequences, minSeqLen, minFreq);
|
|
218
|
-
const deduped = deduplicateSubsequences(repeated);
|
|
219
|
-
|
|
220
|
-
const existingAgentIds = new Set([
|
|
221
|
-
...(profile.agentCandidates || []).map(c => c.id),
|
|
222
|
-
...(profile.approvedAgents || []).map(c => c.id),
|
|
223
|
-
]);
|
|
224
|
-
|
|
225
|
-
const agentCandidates = deduped.map(entry => {
|
|
226
|
-
const id = `agent_${entry.seq.join("_")}`;
|
|
227
|
-
if (existingAgentIds.has(id)) return null;
|
|
228
|
-
return {
|
|
229
|
-
id,
|
|
230
|
-
type: "agent",
|
|
231
|
-
name: agentName(entry.seq),
|
|
232
|
-
description: agentDescription(entry.seq),
|
|
233
|
-
trigger: `infernoflow agent run ${agentName(entry.seq)}`,
|
|
234
|
-
steps: entry.seq,
|
|
235
|
-
frequency: entry.count,
|
|
236
|
-
confidence: confidence(entry.count, totalSessions),
|
|
237
|
-
status: "pending",
|
|
238
|
-
detectedAt: new Date().toISOString(),
|
|
239
|
-
};
|
|
240
|
-
}).filter(Boolean);
|
|
241
|
-
|
|
242
|
-
// ── Skill candidates (task templates) ────────────────────────────────────
|
|
243
|
-
const taskEntries = extractTasks(sessions);
|
|
244
|
-
const templates = detectTaskTemplates(taskEntries, minFreq);
|
|
245
|
-
|
|
246
|
-
const existingSkillIds = new Set([
|
|
247
|
-
...(profile.skillCandidates || []).map(c => c.id),
|
|
248
|
-
...(profile.approvedSkills || []).map(c => c.id),
|
|
249
|
-
]);
|
|
250
|
-
|
|
251
|
-
const skillCandidates = templates.map(t => {
|
|
252
|
-
const id = `skill_${skillName(t.template)}`;
|
|
253
|
-
if (existingSkillIds.has(id)) return null;
|
|
254
|
-
return {
|
|
255
|
-
id,
|
|
256
|
-
type: "skill",
|
|
257
|
-
name: skillName(t.template),
|
|
258
|
-
description: `Auto-skill: "${t.template}" pattern`,
|
|
259
|
-
trigger: t.template,
|
|
260
|
-
steps: t.commands,
|
|
261
|
-
examples: t.examples,
|
|
262
|
-
frequency: t.frequency,
|
|
263
|
-
confidence: confidence(t.frequency, totalSessions),
|
|
264
|
-
status: "pending",
|
|
265
|
-
detectedAt: new Date().toISOString(),
|
|
266
|
-
};
|
|
267
|
-
}).filter(Boolean);
|
|
268
|
-
|
|
269
|
-
return { agentCandidates, skillCandidates };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Merge newly detected candidates into the profile without duplicating
|
|
274
|
-
* already-known (pending/approved/rejected) entries.
|
|
275
|
-
*/
|
|
276
|
-
export function mergeCandidates(profile, { agentCandidates, skillCandidates }) {
|
|
277
|
-
const existingAgentIds = new Set((profile.agentCandidates || []).map(c => c.id));
|
|
278
|
-
const existingSkillIds = new Set((profile.skillCandidates || []).map(c => c.id));
|
|
279
|
-
|
|
280
|
-
for (const c of agentCandidates) {
|
|
281
|
-
if (!existingAgentIds.has(c.id)) {
|
|
282
|
-
profile.agentCandidates = [...(profile.agentCandidates || []), c];
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
for (const c of skillCandidates) {
|
|
286
|
-
if (!existingSkillIds.has(c.id)) {
|
|
287
|
-
profile.skillCandidates = [...(profile.skillCandidates || []), c];
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return profile;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Return all pending candidates (not yet approved or rejected). */
|
|
294
|
-
export function pendingCandidates(profile) {
|
|
295
|
-
const agents = (profile.agentCandidates || []).filter(c => c.status === "pending");
|
|
296
|
-
const skills = (profile.skillCandidates || []).filter(c => c.status === "pending");
|
|
297
|
-
return [...agents, ...skills].sort((a, b) => b.confidence - a.confidence);
|
|
298
|
-
}
|
|
1
|
+
function x(e){return(e||[]).map(n=>(n.commands||[]).map(t=>t.cmd)).filter(n=>n.length>=2)}function q(e,n=2,t=2){const o=new Map;for(let c=0;c<e.length;c++){const s=e[c],i=new Set;for(let l=0;l<s.length;l++)for(let d=l+n;d<=s.length;d++){const u=s.slice(l,d),r=u.join("\u2192");if(i.has(r))continue;i.add(r),o.has(r)||o.set(r,{seq:u,count:0,sessionIndexes:[]});const f=o.get(r);f.count++,f.sessionIndexes.push(c)}}return Array.from(o.values()).filter(c=>c.count>=t).sort((c,s)=>s.count-c.count||s.seq.length-c.seq.length)}function S(e){return e.filter(n=>{const t=n.seq.join("\u2192");return!e.some(o=>{if(o===n)return!1;const c=o.seq.join("\u2192");return o.seq.length>n.seq.length&&o.count>=n.count&&c.includes(t)})})}function C(e){const n=[];for(const t of e||[])for(const o of t.commands||[])o.task&&typeof o.task=="string"&&n.push({task:o.task,cmd:o.cmd,sessionId:t.id});return n}function y(e){return e.toLowerCase().trim().split(/\s+/)}function b(e,n){let t=0;for(;t<e.length&&t<n.length&&e[t]===n[t];)t++;return t}function I(e,n=2){const t=new Map;for(const{task:c,cmd:s}of e){const i=y(c);if(i.length<2)continue;const l=i[0];t.has(l)||t.set(l,[]),t.get(l).push({words:i,original:c,cmd:s})}const o=[];for(const[c,s]of t){if(s.length<n)continue;let i=s[0].words;for(const r of s.slice(1)){const f=b(i,r.words);i=i.slice(0,Math.max(1,f))}const d=i.join(" ")+(i.length<s[0].words.length?" {feature}":""),u=[...new Set(s.map(r=>r.cmd))];o.push({verb:c,template:d,examples:s.slice(0,3).map(r=>r.original),frequency:s.length,commands:u})}return o.sort((c,s)=>s.frequency-c.frequency)}function p(e){const n={"suggest\u2192implement\u2192check":"feature-workflow","suggest\u2192check":"quick-suggest","check\u2192changelog\u2192publish":"release-workflow","suggest\u2192implement\u2192check\u2192changelog":"full-feature-cycle","diff\u2192changelog":"changelog-from-diff","context\u2192implement":"context-first-implement","check\u2192diff":"health-check"},t=e.join("\u2192");return n[t]?n[t]:e.join("-")}function j(e){const n={suggest:"generate contract update",implement:"generate implementation prompt",check:"validate contract health",changelog:"draft changelog entry",diff:"show capability diff",context:"refresh AI context",run:"run full task flow",publish:"publish new version",setup:"set up project",status:"show status"};return e.map(t=>n[t]||t).join(" \u2192 ")}function m(e){return e.replace(/[^a-zA-Z0-9]+/g,"-").replace(/-+/g,"-").toLowerCase().slice(0,40)}function h(e,n){const t=Math.min(e/Math.max(n*.4,4),1);return Math.round(t*100)/100}function A(e,{minFreq:n=3,minSeqLen:t=2}={}){const o=e.recentSessions||[],c=Math.max(o.length,1),s=x(o),i=q(s,t,n),l=S(i),d=new Set([...(e.agentCandidates||[]).map(a=>a.id),...(e.approvedAgents||[]).map(a=>a.id)]),u=l.map(a=>{const g=`agent_${a.seq.join("_")}`;return d.has(g)?null:{id:g,type:"agent",name:p(a.seq),description:j(a.seq),trigger:`infernoflow agent run ${p(a.seq)}`,steps:a.seq,frequency:a.count,confidence:h(a.count,c),status:"pending",detectedAt:new Date().toISOString()}}).filter(Boolean),r=C(o),f=I(r,n),k=new Set([...(e.skillCandidates||[]).map(a=>a.id),...(e.approvedSkills||[]).map(a=>a.id)]),w=f.map(a=>{const g=`skill_${m(a.template)}`;return k.has(g)?null:{id:g,type:"skill",name:m(a.template),description:`Auto-skill: "${a.template}" pattern`,trigger:a.template,steps:a.commands,examples:a.examples,frequency:a.frequency,confidence:h(a.frequency,c),status:"pending",detectedAt:new Date().toISOString()}}).filter(Boolean);return{agentCandidates:u,skillCandidates:w}}function v(e,{agentCandidates:n,skillCandidates:t}){const o=new Set((e.agentCandidates||[]).map(s=>s.id)),c=new Set((e.skillCandidates||[]).map(s=>s.id));for(const s of n)o.has(s.id)||(e.agentCandidates=[...e.agentCandidates||[],s]);for(const s of t)c.has(s.id)||(e.skillCandidates=[...e.skillCandidates||[],s]);return e}function M(e){const n=(e.agentCandidates||[]).filter(o=>o.status==="pending"),t=(e.skillCandidates||[]).filter(o=>o.status==="pending");return[...n,...t].sort((o,c)=>c.confidence-o.confidence)}export{A as detectPatterns,v as mergeCandidates,M as pendingCandidates};
|
|
@@ -1,279 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Reads and writes inferno/developer-profile.json.
|
|
4
|
-
*
|
|
5
|
-
* The developer profile is built up over time by observing how a
|
|
6
|
-
* developer uses infernoflow — naming conventions, session patterns,
|
|
7
|
-
* feature clusters, etc. It is the foundation for personalized
|
|
8
|
-
* suggestions and auto-generated skills.
|
|
9
|
-
*
|
|
10
|
-
* Schema:
|
|
11
|
-
* {
|
|
12
|
-
* schemaVersion: 2,
|
|
13
|
-
* createdAt: ISO string,
|
|
14
|
-
* updatedAt: ISO string,
|
|
15
|
-
* sessionCount: number,
|
|
16
|
-
*
|
|
17
|
-
* // Naming style detected from capability IDs used
|
|
18
|
-
* namingStyle: "PascalCase" | "camelCase" | "kebab-case" | "unknown",
|
|
19
|
-
* preferredVerbs: string[], // e.g. ["Add", "Update", "Remove"]
|
|
20
|
-
*
|
|
21
|
-
* // Commands the developer uses most
|
|
22
|
-
* commandUsage: { [command]: number },
|
|
23
|
-
*
|
|
24
|
-
* // Capability clusters — groups of capabilities added together
|
|
25
|
-
* featureClusters: string[][],
|
|
26
|
-
*
|
|
27
|
-
* // Session behavior
|
|
28
|
-
* avgSessionLength: number, // minutes (estimated from command gaps)
|
|
29
|
-
* commitFrequency: "high"|"medium"|"low"|"unknown",
|
|
30
|
-
* changelogVerbosity: "detailed"|"brief"|"unknown",
|
|
31
|
-
*
|
|
32
|
-
* // Project signals (from --adopt)
|
|
33
|
-
* stack: {
|
|
34
|
-
* language: string,
|
|
35
|
-
* framework: string,
|
|
36
|
-
* projectType: string,
|
|
37
|
-
* },
|
|
38
|
-
*
|
|
39
|
-
* // Sprint 5: Session sequence tracking for auto-skill synthesis
|
|
40
|
-
* recentSessions: SessionRecord[], // last 100 sessions
|
|
41
|
-
* skillCandidates: Candidate[], // pending skill proposals
|
|
42
|
-
* agentCandidates: Candidate[], // pending agent proposals
|
|
43
|
-
* approvedSkills: ApprovedItem[], // approved + written to disk
|
|
44
|
-
* approvedAgents: ApprovedItem[], // approved + written to disk
|
|
45
|
-
* }
|
|
46
|
-
*
|
|
47
|
-
* SessionRecord = {
|
|
48
|
-
* id: string,
|
|
49
|
-
* startedAt: ISO string,
|
|
50
|
-
* commands: Array<{ cmd, task?, caps?, ts }>,
|
|
51
|
-
* }
|
|
52
|
-
*
|
|
53
|
-
* Candidate = {
|
|
54
|
-
* id: string,
|
|
55
|
-
* type: "skill" | "agent",
|
|
56
|
-
* name: string,
|
|
57
|
-
* trigger: string, // e.g. "add {feature} filter"
|
|
58
|
-
* steps: string[], // e.g. ["suggest", "implement", "check"]
|
|
59
|
-
* frequency: number,
|
|
60
|
-
* confidence: number, // 0–1
|
|
61
|
-
* status: "pending" | "approved" | "rejected",
|
|
62
|
-
* detectedAt: ISO string,
|
|
63
|
-
* }
|
|
64
|
-
*
|
|
65
|
-
* ApprovedItem = { id, name, filePath, approvedAt }
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
import * as fs from "node:fs";
|
|
69
|
-
import * as path from "node:path";
|
|
70
|
-
|
|
71
|
-
export const PROFILE_SCHEMA_VERSION = 2;
|
|
72
|
-
export const MAX_RECENT_SESSIONS = 100;
|
|
73
|
-
|
|
74
|
-
export function profilePath(infernoDir) {
|
|
75
|
-
return path.join(infernoDir, "developer-profile.json");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Return a blank profile with all defaults. */
|
|
79
|
-
export function blankProfile() {
|
|
80
|
-
const now = new Date().toISOString();
|
|
81
|
-
return {
|
|
82
|
-
schemaVersion: PROFILE_SCHEMA_VERSION,
|
|
83
|
-
createdAt: now,
|
|
84
|
-
updatedAt: now,
|
|
85
|
-
sessionCount: 0,
|
|
86
|
-
namingStyle: "unknown",
|
|
87
|
-
preferredVerbs: [],
|
|
88
|
-
commandUsage: {},
|
|
89
|
-
featureClusters: [],
|
|
90
|
-
avgSessionLength: 0,
|
|
91
|
-
commitFrequency: "unknown",
|
|
92
|
-
changelogVerbosity: "unknown",
|
|
93
|
-
stack: {
|
|
94
|
-
language: "unknown",
|
|
95
|
-
framework: "unknown",
|
|
96
|
-
projectType: "unknown",
|
|
97
|
-
},
|
|
98
|
-
// Sprint 5 fields
|
|
99
|
-
recentSessions: [],
|
|
100
|
-
skillCandidates: [],
|
|
101
|
-
agentCandidates: [],
|
|
102
|
-
approvedSkills: [],
|
|
103
|
-
approvedAgents: [],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── Session helpers ───────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
/** Open or continue the current session in the profile. */
|
|
110
|
-
export function openSession(profile) {
|
|
111
|
-
const SESSION_GAP_MS = 30 * 60 * 1000;
|
|
112
|
-
const now = Date.now();
|
|
113
|
-
const last = profile.recentSessions?.[profile.recentSessions.length - 1];
|
|
114
|
-
|
|
115
|
-
if (last && now - new Date(last.startedAt).getTime() < SESSION_GAP_MS) {
|
|
116
|
-
return last; // continue existing session
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Start new session
|
|
120
|
-
const session = {
|
|
121
|
-
id: `s${profile.sessionCount + 1}_${now}`,
|
|
122
|
-
startedAt: new Date(now).toISOString(),
|
|
123
|
-
commands: [],
|
|
124
|
-
};
|
|
125
|
-
if (!profile.recentSessions) profile.recentSessions = [];
|
|
126
|
-
profile.recentSessions.push(session);
|
|
127
|
-
// Keep only last MAX_RECENT_SESSIONS
|
|
128
|
-
if (profile.recentSessions.length > MAX_RECENT_SESSIONS) {
|
|
129
|
-
profile.recentSessions = profile.recentSessions.slice(-MAX_RECENT_SESSIONS);
|
|
130
|
-
}
|
|
131
|
-
return session;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Append a command event to the current session. */
|
|
135
|
-
export function recordSessionCommand(profile, cmd, extras = {}) {
|
|
136
|
-
const session = openSession(profile);
|
|
137
|
-
session.commands.push({ cmd, ts: Date.now(), ...extras });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Read the profile, returning a blank one if it doesn't exist yet. */
|
|
141
|
-
export function readProfile(infernoDir) {
|
|
142
|
-
const filePath = profilePath(infernoDir);
|
|
143
|
-
if (!fs.existsSync(filePath)) return blankProfile();
|
|
144
|
-
try {
|
|
145
|
-
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
146
|
-
// Migrate older schemas by merging with blank defaults
|
|
147
|
-
return { ...blankProfile(), ...raw };
|
|
148
|
-
} catch {
|
|
149
|
-
return blankProfile();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Write the profile back to disk. */
|
|
154
|
-
export function writeProfile(infernoDir, profile) {
|
|
155
|
-
profile.updatedAt = new Date().toISOString();
|
|
156
|
-
profile.schemaVersion = PROFILE_SCHEMA_VERSION;
|
|
157
|
-
fs.mkdirSync(infernoDir, { recursive: true });
|
|
158
|
-
fs.writeFileSync(profilePath(infernoDir), JSON.stringify(profile, null, 2) + "\n", "utf8");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Record a command use. Call this every time a CLI command runs.
|
|
163
|
-
* Returns the updated profile (does NOT write — call writeProfile to persist).
|
|
164
|
-
*/
|
|
165
|
-
export function recordCommandUse(profile, command) {
|
|
166
|
-
if (!profile.commandUsage) profile.commandUsage = {};
|
|
167
|
-
profile.commandUsage[command] = (profile.commandUsage[command] || 0) + 1;
|
|
168
|
-
return profile;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Detect naming style from a list of capability IDs.
|
|
173
|
-
* "PascalCase" → CreateItem, SearchResults
|
|
174
|
-
* "camelCase" → createItem, searchResults
|
|
175
|
-
* "kebab-case" → create-item, search-results
|
|
176
|
-
*/
|
|
177
|
-
export function detectNamingStyle(capabilityIds) {
|
|
178
|
-
if (!capabilityIds || capabilityIds.length === 0) return "unknown";
|
|
179
|
-
let pascal = 0, camel = 0, kebab = 0;
|
|
180
|
-
for (const id of capabilityIds) {
|
|
181
|
-
if (/^[A-Z][a-z]/.test(id)) pascal++;
|
|
182
|
-
else if (/^[a-z].*[A-Z]/.test(id)) camel++;
|
|
183
|
-
else if (id.includes("-")) kebab++;
|
|
184
|
-
}
|
|
185
|
-
const max = Math.max(pascal, camel, kebab);
|
|
186
|
-
if (max === 0) return "unknown";
|
|
187
|
-
if (pascal === max) return "PascalCase";
|
|
188
|
-
if (camel === max) return "camelCase";
|
|
189
|
-
return "kebab-case";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Extract the most common verb prefixes from capability IDs.
|
|
194
|
-
* e.g. ["CreateItem", "CreateTask", "UpdateItem"] → ["Create", "Update"]
|
|
195
|
-
*/
|
|
196
|
-
export function detectPreferredVerbs(capabilityIds) {
|
|
197
|
-
const verbCounts = {};
|
|
198
|
-
const verbPattern = /^(Create|Add|Update|Edit|Delete|Remove|Get|Read|List|Fetch|Search|Filter|Toggle|Set|Clear|Send|Upload|Download|Export|Import|Generate|Sync|Validate|Check|Run|Start|Stop|Enable|Disable|Show|Hide)/;
|
|
199
|
-
for (const id of capabilityIds || []) {
|
|
200
|
-
const m = id.match(verbPattern);
|
|
201
|
-
if (m) verbCounts[m[1]] = (verbCounts[m[1]] || 0) + 1;
|
|
202
|
-
}
|
|
203
|
-
return Object.entries(verbCounts)
|
|
204
|
-
.sort((a, b) => b[1] - a[1])
|
|
205
|
-
.slice(0, 5)
|
|
206
|
-
.map(([verb]) => verb);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Seed a profile from adoption signals (run once during `infernoflow init --adopt`).
|
|
211
|
-
* This gives the profile an immediate starting point without needing any sessions.
|
|
212
|
-
*/
|
|
213
|
-
export function seedProfileFromAdoption(infernoDir, signals, capabilities) {
|
|
214
|
-
const profile = readProfile(infernoDir);
|
|
215
|
-
|
|
216
|
-
// Stack
|
|
217
|
-
if (signals?.developmentProfile) {
|
|
218
|
-
profile.stack = {
|
|
219
|
-
language: signals.developmentProfile.language || "unknown",
|
|
220
|
-
framework: signals.developmentProfile.framework || "unknown",
|
|
221
|
-
projectType: signals.developmentProfile.projectType || "unknown",
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Naming style and verbs from detected capability IDs
|
|
226
|
-
const capIds = (capabilities || []).map(c => c.id);
|
|
227
|
-
if (capIds.length > 0) {
|
|
228
|
-
profile.namingStyle = detectNamingStyle(capIds);
|
|
229
|
-
profile.preferredVerbs = detectPreferredVerbs(capIds);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Initial feature cluster from capabilities found together
|
|
233
|
-
if (capIds.length > 1) {
|
|
234
|
-
profile.featureClusters = [capIds];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
writeProfile(infernoDir, profile);
|
|
238
|
-
return profile;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Add a new capability cluster observation.
|
|
243
|
-
* Merges with existing clusters if there's significant overlap (>50%).
|
|
244
|
-
*/
|
|
245
|
-
export function recordCapabilityCluster(profile, capabilityIds) {
|
|
246
|
-
if (!capabilityIds || capabilityIds.length < 2) return profile;
|
|
247
|
-
if (!profile.featureClusters) profile.featureClusters = [];
|
|
248
|
-
|
|
249
|
-
// Check if this cluster significantly overlaps an existing one
|
|
250
|
-
for (let i = 0; i < profile.featureClusters.length; i++) {
|
|
251
|
-
const existing = new Set(profile.featureClusters[i]);
|
|
252
|
-
const newIds = new Set(capabilityIds);
|
|
253
|
-
const intersection = [...newIds].filter(id => existing.has(id));
|
|
254
|
-
const overlapRatio = intersection.length / Math.min(existing.size, newIds.size);
|
|
255
|
-
if (overlapRatio > 0.5) {
|
|
256
|
-
// Merge: add any new IDs to the existing cluster
|
|
257
|
-
const merged = Array.from(new Set([...existing, ...newIds]));
|
|
258
|
-
profile.featureClusters[i] = merged;
|
|
259
|
-
return profile;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// New cluster
|
|
264
|
-
profile.featureClusters.push([...capabilityIds]);
|
|
265
|
-
return profile;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Human-readable summary of the profile for display in status/context. */
|
|
269
|
-
export function summarizeProfile(profile) {
|
|
270
|
-
if (!profile || profile.sessionCount === 0 && profile.namingStyle === "unknown") {
|
|
271
|
-
return null; // Not enough data yet
|
|
272
|
-
}
|
|
273
|
-
const lines = [];
|
|
274
|
-
if (profile.namingStyle !== "unknown") lines.push(`naming: ${profile.namingStyle}`);
|
|
275
|
-
if (profile.preferredVerbs.length > 0) lines.push(`verbs: ${profile.preferredVerbs.slice(0, 3).join(", ")}`);
|
|
276
|
-
if (profile.stack.framework !== "unknown") lines.push(`stack: ${profile.stack.framework} (${profile.stack.language})`);
|
|
277
|
-
if (profile.sessionCount > 0) lines.push(`sessions: ${profile.sessionCount}`);
|
|
278
|
-
return lines.join(" · ");
|
|
279
|
-
}
|
|
1
|
+
import*as a from"node:fs";import*as d from"node:path";const i=2,m=100;function S(e){return d.join(e,"developer-profile.json")}function c(){const e=new Date().toISOString();return{schemaVersion:i,createdAt:e,updatedAt:e,sessionCount:0,namingStyle:"unknown",preferredVerbs:[],commandUsage:{},featureClusters:[],avgSessionLength:0,commitFrequency:"unknown",changelogVerbosity:"unknown",stack:{language:"unknown",framework:"unknown",projectType:"unknown"},recentSessions:[],skillCandidates:[],agentCandidates:[],approvedSkills:[],approvedAgents:[]}}function f(e){const r=Date.now(),t=e.recentSessions?.[e.recentSessions.length-1];if(t&&r-new Date(t.startedAt).getTime()<18e5)return t;const s={id:`s${e.sessionCount+1}_${r}`,startedAt:new Date(r).toISOString(),commands:[]};return e.recentSessions||(e.recentSessions=[]),e.recentSessions.push(s),e.recentSessions.length>m&&(e.recentSessions=e.recentSessions.slice(-m)),s}function C(e,n,r={}){f(e).commands.push({cmd:n,ts:Date.now(),...r})}function l(e){const n=S(e);if(!a.existsSync(n))return c();try{const r=JSON.parse(a.readFileSync(n,"utf8"));return{...c(),...r}}catch{return c()}}function g(e,n){n.updatedAt=new Date().toISOString(),n.schemaVersion=i,a.mkdirSync(e,{recursive:!0}),a.writeFileSync(S(e),JSON.stringify(n,null,2)+`
|
|
2
|
+
`,"utf8")}function p(e,n){return e.commandUsage||(e.commandUsage={}),e.commandUsage[n]=(e.commandUsage[n]||0)+1,e}function w(e){if(!e||e.length===0)return"unknown";let n=0,r=0,t=0;for(const o of e)/^[A-Z][a-z]/.test(o)?n++:/^[a-z].*[A-Z]/.test(o)?r++:o.includes("-")&&t++;const s=Math.max(n,r,t);return s===0?"unknown":n===s?"PascalCase":r===s?"camelCase":"kebab-case"}function h(e){const n={},r=/^(Create|Add|Update|Edit|Delete|Remove|Get|Read|List|Fetch|Search|Filter|Toggle|Set|Clear|Send|Upload|Download|Export|Import|Generate|Sync|Validate|Check|Run|Start|Stop|Enable|Disable|Show|Hide)/;for(const t of e||[]){const s=t.match(r);s&&(n[s[1]]=(n[s[1]]||0)+1)}return Object.entries(n).sort((t,s)=>s[1]-t[1]).slice(0,5).map(([t])=>t)}function x(e,n,r){const t=l(e);n?.developmentProfile&&(t.stack={language:n.developmentProfile.language||"unknown",framework:n.developmentProfile.framework||"unknown",projectType:n.developmentProfile.projectType||"unknown"});const s=(r||[]).map(o=>o.id);return s.length>0&&(t.namingStyle=w(s),t.preferredVerbs=h(s)),s.length>1&&(t.featureClusters=[s]),g(e,t),t}function P(e,n){if(!n||n.length<2)return e;e.featureClusters||(e.featureClusters=[]);for(let r=0;r<e.featureClusters.length;r++){const t=new Set(e.featureClusters[r]),s=new Set(n);if([...s].filter(u=>t.has(u)).length/Math.min(t.size,s.size)>.5){const u=Array.from(new Set([...t,...s]));return e.featureClusters[r]=u,e}}return e.featureClusters.push([...n]),e}function v(e){if(!e||e.sessionCount===0&&e.namingStyle==="unknown")return null;const n=[];return e.namingStyle!=="unknown"&&n.push(`naming: ${e.namingStyle}`),e.preferredVerbs.length>0&&n.push(`verbs: ${e.preferredVerbs.slice(0,3).join(", ")}`),e.stack.framework!=="unknown"&&n.push(`stack: ${e.stack.framework} (${e.stack.language})`),e.sessionCount>0&&n.push(`sessions: ${e.sessionCount}`),n.join(" \xB7 ")}export{m as MAX_RECENT_SESSIONS,i as PROFILE_SCHEMA_VERSION,c as blankProfile,w as detectNamingStyle,h as detectPreferredVerbs,f as openSession,S as profilePath,l as readProfile,P as recordCapabilityCluster,p as recordCommandUse,C as recordSessionCommand,x as seedProfileFromAdoption,v as summarizeProfile,g as writeProfile};
|