@itz4blitz/agentful 1.2.0 → 1.3.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.
- package/README.md +28 -1
- package/bin/cli.js +11 -1055
- package/bin/hooks/block-file-creation.js +271 -0
- package/bin/hooks/product-spec-watcher.js +151 -0
- package/lib/index.js +0 -11
- package/lib/init.js +2 -21
- package/lib/parallel-execution.js +235 -0
- package/lib/presets.js +26 -4
- package/package.json +4 -7
- package/template/.claude/agents/architect.md +2 -2
- package/template/.claude/agents/backend.md +17 -30
- package/template/.claude/agents/frontend.md +17 -39
- package/template/.claude/agents/orchestrator.md +63 -4
- package/template/.claude/agents/product-analyzer.md +1 -1
- package/template/.claude/agents/tester.md +16 -29
- package/template/.claude/commands/agentful-generate.md +221 -14
- package/template/.claude/commands/agentful-init.md +621 -0
- package/template/.claude/commands/agentful-product.md +1 -1
- package/template/.claude/commands/agentful-start.md +99 -1
- package/template/.claude/product/EXAMPLES.md +2 -2
- package/template/.claude/product/index.md +1 -1
- package/template/.claude/settings.json +22 -0
- package/template/.claude/skills/research/SKILL.md +432 -0
- package/template/CLAUDE.md +5 -6
- package/template/bin/hooks/architect-drift-detector.js +242 -0
- package/template/bin/hooks/product-spec-watcher.js +151 -0
- package/version.json +1 -1
- package/bin/hooks/post-agent.js +0 -101
- package/bin/hooks/post-feature.js +0 -227
- package/bin/hooks/pre-agent.js +0 -118
- package/bin/hooks/pre-feature.js +0 -138
- package/lib/VALIDATION_README.md +0 -455
- package/lib/ci/claude-action-integration.js +0 -641
- package/lib/ci/index.js +0 -10
- package/lib/core/analyzer.js +0 -497
- package/lib/core/cli.js +0 -141
- package/lib/core/detectors/conventions.js +0 -342
- package/lib/core/detectors/framework.js +0 -276
- package/lib/core/detectors/index.js +0 -15
- package/lib/core/detectors/language.js +0 -199
- package/lib/core/detectors/patterns.js +0 -356
- package/lib/core/generator.js +0 -626
- package/lib/core/index.js +0 -9
- package/lib/core/output-parser.js +0 -458
- package/lib/core/storage.js +0 -515
- package/lib/core/templates.js +0 -556
- package/lib/pipeline/cli.js +0 -423
- package/lib/pipeline/engine.js +0 -928
- package/lib/pipeline/executor.js +0 -440
- package/lib/pipeline/index.js +0 -33
- package/lib/pipeline/integrations.js +0 -559
- package/lib/pipeline/schemas.js +0 -288
- package/lib/remote/client.js +0 -361
- package/lib/server/auth.js +0 -270
- package/lib/server/client-example.js +0 -190
- package/lib/server/executor.js +0 -477
- package/lib/server/index.js +0 -494
- package/lib/update-helpers.js +0 -505
- package/lib/validation.js +0 -460
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output Parser - Structured data extraction from agent responses
|
|
3
|
-
*
|
|
4
|
-
* Parses raw agent output to extract:
|
|
5
|
-
* - JSON blocks (structured data)
|
|
6
|
-
* - Decision points (requiring user input)
|
|
7
|
-
* - Progress markers (completion tracking)
|
|
8
|
-
* - Questions (blocking queries)
|
|
9
|
-
* - Plain text (fallback)
|
|
10
|
-
*
|
|
11
|
-
* @module lib/core/output-parser
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Main output parser class
|
|
16
|
-
*/
|
|
17
|
-
export class OutputParser {
|
|
18
|
-
/**
|
|
19
|
-
* Parse agent output and return structured result
|
|
20
|
-
*
|
|
21
|
-
* @param {string} rawOutput - Raw agent response text
|
|
22
|
-
* @returns {ParsedOutput} Parsed output with type and extracted data
|
|
23
|
-
*/
|
|
24
|
-
parse(rawOutput) {
|
|
25
|
-
if (!rawOutput || typeof rawOutput !== 'string') {
|
|
26
|
-
return {
|
|
27
|
-
type: 'text',
|
|
28
|
-
data: null,
|
|
29
|
-
raw: rawOutput || ''
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Try parsing in order of specificity
|
|
34
|
-
const jsonResult = this.extractJSON(rawOutput);
|
|
35
|
-
if (jsonResult.found) {
|
|
36
|
-
return {
|
|
37
|
-
type: 'structured',
|
|
38
|
-
data: jsonResult.data,
|
|
39
|
-
raw: rawOutput,
|
|
40
|
-
jsonBlocks: jsonResult.all
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const decisions = this.extractDecisions(rawOutput);
|
|
45
|
-
if (decisions.length > 0) {
|
|
46
|
-
return {
|
|
47
|
-
type: 'decisions',
|
|
48
|
-
decisions,
|
|
49
|
-
raw: rawOutput
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const progress = this.extractProgress(rawOutput);
|
|
54
|
-
if (progress) {
|
|
55
|
-
return {
|
|
56
|
-
type: 'progress',
|
|
57
|
-
progress,
|
|
58
|
-
raw: rawOutput
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const questions = this.extractQuestions(rawOutput);
|
|
63
|
-
if (questions.length > 0) {
|
|
64
|
-
return {
|
|
65
|
-
type: 'question',
|
|
66
|
-
questions,
|
|
67
|
-
raw: rawOutput
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
type: 'text',
|
|
73
|
-
data: rawOutput.trim(),
|
|
74
|
-
raw: rawOutput
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Extract JSON from markdown code blocks or inline JSON
|
|
80
|
-
*
|
|
81
|
-
* @param {string} text - Text containing potential JSON
|
|
82
|
-
* @returns {Object} { found: boolean, data: object|null, all: array }
|
|
83
|
-
*/
|
|
84
|
-
extractJSON(text) {
|
|
85
|
-
const result = {
|
|
86
|
-
found: false,
|
|
87
|
-
data: null,
|
|
88
|
-
all: []
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Pattern 1: Markdown JSON code blocks
|
|
92
|
-
const codeBlockPattern = /```(?:json)?\s*\n([\s\S]*?)\n```/g;
|
|
93
|
-
let match;
|
|
94
|
-
|
|
95
|
-
while ((match = codeBlockPattern.exec(text)) !== null) {
|
|
96
|
-
try {
|
|
97
|
-
const parsed = JSON.parse(match[1].trim());
|
|
98
|
-
result.all.push(parsed);
|
|
99
|
-
if (!result.found) {
|
|
100
|
-
result.data = parsed;
|
|
101
|
-
result.found = true;
|
|
102
|
-
}
|
|
103
|
-
} catch (e) {
|
|
104
|
-
// Not valid JSON, skip
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Pattern 2: Inline JSON objects (if no code blocks found)
|
|
109
|
-
if (!result.found) {
|
|
110
|
-
const inlinePattern = /(\{[\s\S]*?\})/g;
|
|
111
|
-
|
|
112
|
-
while ((match = inlinePattern.exec(text)) !== null) {
|
|
113
|
-
try {
|
|
114
|
-
const parsed = JSON.parse(match[1]);
|
|
115
|
-
// Verify it's a real object (not just {})
|
|
116
|
-
if (Object.keys(parsed).length > 0) {
|
|
117
|
-
result.all.push(parsed);
|
|
118
|
-
if (!result.found) {
|
|
119
|
-
result.data = parsed;
|
|
120
|
-
result.found = true;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
} catch (e) {
|
|
124
|
-
// Not valid JSON, skip
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Extract decision points from agent output
|
|
134
|
-
*
|
|
135
|
-
* Format:
|
|
136
|
-
* DECISION: Question text?
|
|
137
|
-
* OPTIONS:
|
|
138
|
-
* - Option A (description)
|
|
139
|
-
* - Option B (description)
|
|
140
|
-
*
|
|
141
|
-
* @param {string} text - Text containing decisions
|
|
142
|
-
* @returns {Array<Decision>} Array of decision objects
|
|
143
|
-
*/
|
|
144
|
-
extractDecisions(text) {
|
|
145
|
-
const decisions = [];
|
|
146
|
-
|
|
147
|
-
// Pattern: DECISION: ... OPTIONS: ...
|
|
148
|
-
const decisionPattern = /DECISION:\s*([^\n]+)\s*(?:OPTIONS?:)?\s*((?:[-*]\s*[^\n]+\n?)*)/gi;
|
|
149
|
-
let match;
|
|
150
|
-
|
|
151
|
-
while ((match = decisionPattern.exec(text)) !== null) {
|
|
152
|
-
const question = match[1].trim();
|
|
153
|
-
const optionsText = match[2].trim();
|
|
154
|
-
|
|
155
|
-
// Parse options
|
|
156
|
-
const options = [];
|
|
157
|
-
const optionPattern = /[-*]\s*([^\n(]+)(?:\(([^)]+)\))?/g;
|
|
158
|
-
let optionMatch;
|
|
159
|
-
|
|
160
|
-
while ((optionMatch = optionPattern.exec(optionsText)) !== null) {
|
|
161
|
-
options.push({
|
|
162
|
-
value: optionMatch[1].trim(),
|
|
163
|
-
description: optionMatch[2]?.trim() || null
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
decisions.push({
|
|
168
|
-
question,
|
|
169
|
-
options: options.length > 0 ? options : null,
|
|
170
|
-
context: this._extractContext(text, match.index)
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Alternative pattern: Simple "DECIDE: " marker
|
|
175
|
-
const simplePattern = /DECIDE:\s*([^\n]+)/gi;
|
|
176
|
-
|
|
177
|
-
while ((match = simplePattern.exec(text)) !== null) {
|
|
178
|
-
const question = match[1].trim();
|
|
179
|
-
|
|
180
|
-
// Don't duplicate if already found
|
|
181
|
-
if (!decisions.some(d => d.question === question)) {
|
|
182
|
-
decisions.push({
|
|
183
|
-
question,
|
|
184
|
-
options: null,
|
|
185
|
-
context: this._extractContext(text, match.index)
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return decisions;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Extract progress indicators from text
|
|
195
|
-
*
|
|
196
|
-
* Patterns:
|
|
197
|
-
* - [PROGRESS: 50%]
|
|
198
|
-
* - Progress: 7/10 tests passing
|
|
199
|
-
* - 3 of 5 features complete
|
|
200
|
-
*
|
|
201
|
-
* @param {string} text - Text containing progress markers
|
|
202
|
-
* @returns {Progress|null} Progress object or null
|
|
203
|
-
*/
|
|
204
|
-
extractProgress(text) {
|
|
205
|
-
// Pattern 1: [PROGRESS: 50%]
|
|
206
|
-
const bracketPattern = /\[PROGRESS:\s*(\d+)%\]/i;
|
|
207
|
-
let match = bracketPattern.exec(text);
|
|
208
|
-
|
|
209
|
-
if (match) {
|
|
210
|
-
return {
|
|
211
|
-
percentage: parseInt(match[1], 10),
|
|
212
|
-
format: 'percentage',
|
|
213
|
-
raw: match[0]
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Pattern 2: Progress: 7/10 tests passing
|
|
218
|
-
const ratioPattern = /Progress:\s*(\d+)\s*\/\s*(\d+)\s*([^\n]*)/i;
|
|
219
|
-
match = ratioPattern.exec(text);
|
|
220
|
-
|
|
221
|
-
if (match) {
|
|
222
|
-
const current = parseInt(match[1], 10);
|
|
223
|
-
const total = parseInt(match[2], 10);
|
|
224
|
-
return {
|
|
225
|
-
current,
|
|
226
|
-
total,
|
|
227
|
-
percentage: total > 0 ? Math.round((current / total) * 100) : 0,
|
|
228
|
-
format: 'ratio',
|
|
229
|
-
description: match[3].trim() || null,
|
|
230
|
-
raw: match[0]
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Pattern 3: 3 of 5 features complete
|
|
235
|
-
const ofPattern = /(\d+)\s+of\s+(\d+)\s+([^\n]*?)\s+(?:complete|done|finished|passing)/i;
|
|
236
|
-
match = ofPattern.exec(text);
|
|
237
|
-
|
|
238
|
-
if (match) {
|
|
239
|
-
const current = parseInt(match[1], 10);
|
|
240
|
-
const total = parseInt(match[2], 10);
|
|
241
|
-
return {
|
|
242
|
-
current,
|
|
243
|
-
total,
|
|
244
|
-
percentage: total > 0 ? Math.round((current / total) * 100) : 0,
|
|
245
|
-
format: 'of',
|
|
246
|
-
description: match[3].trim() || null,
|
|
247
|
-
raw: match[0]
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Pattern 4: Simple percentage
|
|
252
|
-
const percentPattern = /(\d+)%\s+(?:complete|done|finished)/i;
|
|
253
|
-
match = percentPattern.exec(text);
|
|
254
|
-
|
|
255
|
-
if (match) {
|
|
256
|
-
return {
|
|
257
|
-
percentage: parseInt(match[1], 10),
|
|
258
|
-
format: 'percentage',
|
|
259
|
-
raw: match[0]
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Extract questions requiring user input
|
|
268
|
-
*
|
|
269
|
-
* Patterns:
|
|
270
|
-
* - Lines ending with "?"
|
|
271
|
-
* - "QUESTION: " prefix
|
|
272
|
-
* - "Need input: " prefix
|
|
273
|
-
*
|
|
274
|
-
* @param {string} text - Text containing questions
|
|
275
|
-
* @returns {Array<Question>} Array of question objects
|
|
276
|
-
*/
|
|
277
|
-
extractQuestions(text) {
|
|
278
|
-
const questions = [];
|
|
279
|
-
const lines = text.split('\n');
|
|
280
|
-
|
|
281
|
-
for (let i = 0; i < lines.length; i++) {
|
|
282
|
-
const line = lines[i].trim();
|
|
283
|
-
|
|
284
|
-
// Skip empty lines and code blocks
|
|
285
|
-
if (!line || line.startsWith('```')) continue;
|
|
286
|
-
|
|
287
|
-
// Pattern 1: Explicit QUESTION: marker
|
|
288
|
-
let match = /^QUESTION:\s*(.+)$/i.exec(line);
|
|
289
|
-
if (match) {
|
|
290
|
-
questions.push({
|
|
291
|
-
question: match[1].trim(),
|
|
292
|
-
type: 'explicit',
|
|
293
|
-
line: i + 1,
|
|
294
|
-
context: this._getLineContext(lines, i)
|
|
295
|
-
});
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Pattern 2: "Need input:" prefix
|
|
300
|
-
match = /^(?:Need input|Needs? input|Input needed):\s*(.+)$/i.exec(line);
|
|
301
|
-
if (match) {
|
|
302
|
-
questions.push({
|
|
303
|
-
question: match[1].trim(),
|
|
304
|
-
type: 'input_request',
|
|
305
|
-
line: i + 1,
|
|
306
|
-
context: this._getLineContext(lines, i)
|
|
307
|
-
});
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Pattern 3: Questions ending with "?"
|
|
312
|
-
// Only consider substantial questions (>10 chars, starts with question word or context)
|
|
313
|
-
if (line.endsWith('?') && line.length > 10) {
|
|
314
|
-
const questionWords = /^(what|where|when|why|how|which|who|should|can|could|would|will|is|are|do|does)/i;
|
|
315
|
-
if (questionWords.test(line)) {
|
|
316
|
-
questions.push({
|
|
317
|
-
question: line,
|
|
318
|
-
type: 'inferred',
|
|
319
|
-
line: i + 1,
|
|
320
|
-
context: this._getLineContext(lines, i)
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return questions;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Extract surrounding context for a match
|
|
331
|
-
*
|
|
332
|
-
* @private
|
|
333
|
-
* @param {string} text - Full text
|
|
334
|
-
* @param {number} index - Match index
|
|
335
|
-
* @param {number} radius - Characters before/after to include
|
|
336
|
-
* @returns {string} Context snippet
|
|
337
|
-
*/
|
|
338
|
-
_extractContext(text, index, radius = 100) {
|
|
339
|
-
const start = Math.max(0, index - radius);
|
|
340
|
-
const end = Math.min(text.length, index + radius);
|
|
341
|
-
|
|
342
|
-
let context = text.substring(start, end).trim();
|
|
343
|
-
|
|
344
|
-
// Add ellipsis if truncated
|
|
345
|
-
if (start > 0) context = '...' + context;
|
|
346
|
-
if (end < text.length) context = context + '...';
|
|
347
|
-
|
|
348
|
-
return context;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Get context lines around a specific line
|
|
353
|
-
*
|
|
354
|
-
* @private
|
|
355
|
-
* @param {Array<string>} lines - All lines
|
|
356
|
-
* @param {number} lineIndex - Target line index
|
|
357
|
-
* @param {number} radius - Lines before/after to include
|
|
358
|
-
* @returns {Array<string>} Context lines
|
|
359
|
-
*/
|
|
360
|
-
_getLineContext(lines, lineIndex, radius = 2) {
|
|
361
|
-
const start = Math.max(0, lineIndex - radius);
|
|
362
|
-
const end = Math.min(lines.length, lineIndex + radius + 1);
|
|
363
|
-
return lines.slice(start, end);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Convenience function for parsing agent output
|
|
369
|
-
*
|
|
370
|
-
* @param {string} rawOutput - Raw agent response
|
|
371
|
-
* @returns {ParsedOutput} Parsed output
|
|
372
|
-
*/
|
|
373
|
-
export function parseAgentOutput(rawOutput) {
|
|
374
|
-
const parser = new OutputParser();
|
|
375
|
-
return parser.parse(rawOutput);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Extract only JSON from agent output
|
|
380
|
-
*
|
|
381
|
-
* @param {string} text - Text containing JSON
|
|
382
|
-
* @returns {Object} { found: boolean, data: object|null, all: array }
|
|
383
|
-
*/
|
|
384
|
-
export function extractJSON(text) {
|
|
385
|
-
const parser = new OutputParser();
|
|
386
|
-
return parser.extractJSON(text);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Extract only decisions from agent output
|
|
391
|
-
*
|
|
392
|
-
* @param {string} text - Text containing decisions
|
|
393
|
-
* @returns {Array<Decision>} Array of decisions
|
|
394
|
-
*/
|
|
395
|
-
export function extractDecisions(text) {
|
|
396
|
-
const parser = new OutputParser();
|
|
397
|
-
return parser.extractDecisions(text);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Extract only progress from agent output
|
|
402
|
-
*
|
|
403
|
-
* @param {string} text - Text containing progress markers
|
|
404
|
-
* @returns {Progress|null} Progress object or null
|
|
405
|
-
*/
|
|
406
|
-
export function extractProgress(text) {
|
|
407
|
-
const parser = new OutputParser();
|
|
408
|
-
return parser.extractProgress(text);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Extract only questions from agent output
|
|
413
|
-
*
|
|
414
|
-
* @param {string} text - Text containing questions
|
|
415
|
-
* @returns {Array<Question>} Array of questions
|
|
416
|
-
*/
|
|
417
|
-
export function extractQuestions(text) {
|
|
418
|
-
const parser = new OutputParser();
|
|
419
|
-
return parser.extractQuestions(text);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* @typedef {Object} ParsedOutput
|
|
424
|
-
* @property {'structured'|'decisions'|'progress'|'question'|'text'} type - Output type
|
|
425
|
-
* @property {*} data - Extracted data (type-specific)
|
|
426
|
-
* @property {string} raw - Original raw output
|
|
427
|
-
*/
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* @typedef {Object} Decision
|
|
431
|
-
* @property {string} question - Decision question
|
|
432
|
-
* @property {Array<Option>|null} options - Available options (if provided)
|
|
433
|
-
* @property {string} context - Surrounding text context
|
|
434
|
-
*/
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* @typedef {Object} Option
|
|
438
|
-
* @property {string} value - Option value
|
|
439
|
-
* @property {string|null} description - Option description
|
|
440
|
-
*/
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* @typedef {Object} Progress
|
|
444
|
-
* @property {number} percentage - Progress as percentage (0-100)
|
|
445
|
-
* @property {'percentage'|'ratio'|'of'} format - Format type
|
|
446
|
-
* @property {number} [current] - Current count (for ratio/of formats)
|
|
447
|
-
* @property {number} [total] - Total count (for ratio/of formats)
|
|
448
|
-
* @property {string|null} [description] - Progress description
|
|
449
|
-
* @property {string} raw - Raw progress marker
|
|
450
|
-
*/
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* @typedef {Object} Question
|
|
454
|
-
* @property {string} question - Question text
|
|
455
|
-
* @property {'explicit'|'input_request'|'inferred'} type - Question detection type
|
|
456
|
-
* @property {number} line - Line number in output
|
|
457
|
-
* @property {Array<string>} context - Surrounding lines for context
|
|
458
|
-
*/
|