@harness-engineering/core 0.14.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.
- package/README.md +24 -0
- package/dist/index.d.mts +521 -4
- package/dist/index.d.ts +521 -4
- package/dist/index.js +2449 -151
- package/dist/index.mjs +2392 -147
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ __export(index_exports, {
|
|
|
45
45
|
BlueprintGenerator: () => BlueprintGenerator,
|
|
46
46
|
BundleConstraintsSchema: () => BundleConstraintsSchema,
|
|
47
47
|
BundleSchema: () => BundleSchema,
|
|
48
|
+
CACHE_TTL_MS: () => CACHE_TTL_MS,
|
|
48
49
|
COMPLIANCE_DESCRIPTOR: () => COMPLIANCE_DESCRIPTOR,
|
|
49
50
|
CategoryBaselineSchema: () => CategoryBaselineSchema,
|
|
50
51
|
CategoryRegressionSchema: () => CategoryRegressionSchema,
|
|
@@ -62,7 +63,9 @@ __export(index_exports, {
|
|
|
62
63
|
DEFAULT_SECURITY_CONFIG: () => DEFAULT_SECURITY_CONFIG,
|
|
63
64
|
DEFAULT_STATE: () => DEFAULT_STATE,
|
|
64
65
|
DEFAULT_STREAM_INDEX: () => DEFAULT_STREAM_INDEX,
|
|
66
|
+
DESTRUCTIVE_BASH: () => DESTRUCTIVE_BASH,
|
|
65
67
|
DepDepthCollector: () => DepDepthCollector,
|
|
68
|
+
EXTENSION_MAP: () => EXTENSION_MAP,
|
|
66
69
|
EmitInteractionInputSchema: () => EmitInteractionInputSchema,
|
|
67
70
|
EntropyAnalyzer: () => EntropyAnalyzer,
|
|
68
71
|
EntropyConfigSchema: () => EntropyConfigSchema,
|
|
@@ -75,6 +78,7 @@ __export(index_exports, {
|
|
|
75
78
|
HandoffSchema: () => HandoffSchema,
|
|
76
79
|
HarnessStateSchema: () => HarnessStateSchema,
|
|
77
80
|
InteractionTypeSchema: () => InteractionTypeSchema,
|
|
81
|
+
LITELLM_PRICING_URL: () => LITELLM_PRICING_URL,
|
|
78
82
|
LayerViolationCollector: () => LayerViolationCollector,
|
|
79
83
|
LockfilePackageSchema: () => LockfilePackageSchema,
|
|
80
84
|
LockfileSchema: () => LockfileSchema,
|
|
@@ -91,12 +95,14 @@ __export(index_exports, {
|
|
|
91
95
|
RegressionDetector: () => RegressionDetector,
|
|
92
96
|
RuleRegistry: () => RuleRegistry,
|
|
93
97
|
SECURITY_DESCRIPTOR: () => SECURITY_DESCRIPTOR,
|
|
98
|
+
STALENESS_WARNING_DAYS: () => STALENESS_WARNING_DAYS,
|
|
94
99
|
SecurityConfigSchema: () => SecurityConfigSchema,
|
|
95
100
|
SecurityScanner: () => SecurityScanner,
|
|
96
101
|
SharableBoundaryConfigSchema: () => SharableBoundaryConfigSchema,
|
|
97
102
|
SharableForbiddenImportSchema: () => SharableForbiddenImportSchema,
|
|
98
103
|
SharableLayerSchema: () => SharableLayerSchema,
|
|
99
104
|
SharableSecurityRulesSchema: () => SharableSecurityRulesSchema,
|
|
105
|
+
SkillEventSchema: () => SkillEventSchema,
|
|
100
106
|
StreamIndexSchema: () => StreamIndexSchema,
|
|
101
107
|
StreamInfoSchema: () => StreamInfoSchema,
|
|
102
108
|
ThresholdConfigSchema: () => ThresholdConfigSchema,
|
|
@@ -105,6 +111,9 @@ __export(index_exports, {
|
|
|
105
111
|
VERSION: () => VERSION,
|
|
106
112
|
ViolationSchema: () => ViolationSchema,
|
|
107
113
|
addProvenance: () => addProvenance,
|
|
114
|
+
agentConfigRules: () => agentConfigRules,
|
|
115
|
+
aggregateByDay: () => aggregateByDay,
|
|
116
|
+
aggregateBySession: () => aggregateBySession,
|
|
108
117
|
analyzeDiff: () => analyzeDiff,
|
|
109
118
|
analyzeLearningPatterns: () => analyzeLearningPatterns,
|
|
110
119
|
appendFailure: () => appendFailure,
|
|
@@ -112,6 +121,7 @@ __export(index_exports, {
|
|
|
112
121
|
appendSessionEntry: () => appendSessionEntry,
|
|
113
122
|
applyFixes: () => applyFixes,
|
|
114
123
|
applyHotspotDowngrade: () => applyHotspotDowngrade,
|
|
124
|
+
applySyncChanges: () => applySyncChanges,
|
|
115
125
|
archMatchers: () => archMatchers,
|
|
116
126
|
archModule: () => archModule,
|
|
117
127
|
architecture: () => architecture,
|
|
@@ -122,16 +132,23 @@ __export(index_exports, {
|
|
|
122
132
|
buildDependencyGraph: () => buildDependencyGraph,
|
|
123
133
|
buildExclusionSet: () => buildExclusionSet,
|
|
124
134
|
buildSnapshot: () => buildSnapshot,
|
|
135
|
+
calculateCost: () => calculateCost,
|
|
125
136
|
checkDocCoverage: () => checkDocCoverage,
|
|
126
137
|
checkEligibility: () => checkEligibility,
|
|
127
138
|
checkEvidenceCoverage: () => checkEvidenceCoverage,
|
|
139
|
+
checkTaint: () => checkTaint,
|
|
128
140
|
classifyFinding: () => classifyFinding,
|
|
141
|
+
clearEventHashCache: () => clearEventHashCache,
|
|
129
142
|
clearFailuresCache: () => clearFailuresCache,
|
|
130
143
|
clearLearningsCache: () => clearLearningsCache,
|
|
144
|
+
clearTaint: () => clearTaint,
|
|
145
|
+
computeOverallSeverity: () => computeOverallSeverity,
|
|
146
|
+
computeScanExitCode: () => computeScanExitCode,
|
|
131
147
|
configureFeedback: () => configureFeedback,
|
|
132
148
|
constraintRuleId: () => constraintRuleId,
|
|
133
149
|
contextBudget: () => contextBudget,
|
|
134
150
|
contextFilter: () => contextFilter,
|
|
151
|
+
countLearningEntries: () => countLearningEntries,
|
|
135
152
|
createBoundaryValidator: () => createBoundaryValidator,
|
|
136
153
|
createCommentedCodeFixes: () => createCommentedCodeFixes,
|
|
137
154
|
createError: () => createError,
|
|
@@ -155,66 +172,95 @@ __export(index_exports, {
|
|
|
155
172
|
detectCouplingViolations: () => detectCouplingViolations,
|
|
156
173
|
detectDeadCode: () => detectDeadCode,
|
|
157
174
|
detectDocDrift: () => detectDocDrift,
|
|
175
|
+
detectLanguage: () => detectLanguage,
|
|
158
176
|
detectPatternViolations: () => detectPatternViolations,
|
|
159
177
|
detectSizeBudgetViolations: () => detectSizeBudgetViolations,
|
|
160
178
|
detectStack: () => detectStack,
|
|
161
179
|
detectStaleConstraints: () => detectStaleConstraints,
|
|
162
180
|
determineAssessment: () => determineAssessment,
|
|
163
181
|
diff: () => diff,
|
|
182
|
+
emitEvent: () => emitEvent,
|
|
164
183
|
executeWorkflow: () => executeWorkflow,
|
|
165
184
|
expressRules: () => expressRules,
|
|
166
185
|
extractBundle: () => extractBundle,
|
|
186
|
+
extractIndexEntry: () => extractIndexEntry,
|
|
167
187
|
extractMarkdownLinks: () => extractMarkdownLinks,
|
|
168
188
|
extractSections: () => extractSections,
|
|
169
189
|
fanOutReview: () => fanOutReview,
|
|
190
|
+
formatEventTimeline: () => formatEventTimeline,
|
|
170
191
|
formatFindingBlock: () => formatFindingBlock,
|
|
171
192
|
formatGitHubComment: () => formatGitHubComment,
|
|
172
193
|
formatGitHubSummary: () => formatGitHubSummary,
|
|
194
|
+
formatOutline: () => formatOutline,
|
|
173
195
|
formatTerminalOutput: () => formatTerminalOutput,
|
|
174
196
|
generateAgentsMap: () => generateAgentsMap,
|
|
175
197
|
generateSuggestions: () => generateSuggestions,
|
|
176
198
|
getActionEmitter: () => getActionEmitter,
|
|
177
199
|
getExitCode: () => getExitCode,
|
|
178
200
|
getFeedbackConfig: () => getFeedbackConfig,
|
|
201
|
+
getInjectionPatterns: () => getInjectionPatterns,
|
|
202
|
+
getModelPrice: () => getModelPrice,
|
|
203
|
+
getOutline: () => getOutline,
|
|
204
|
+
getParser: () => getParser,
|
|
179
205
|
getPhaseCategories: () => getPhaseCategories,
|
|
180
206
|
getStreamForBranch: () => getStreamForBranch,
|
|
207
|
+
getTaintFilePath: () => getTaintFilePath,
|
|
181
208
|
getUpdateNotification: () => getUpdateNotification,
|
|
182
209
|
goRules: () => goRules,
|
|
183
210
|
injectionRules: () => injectionRules,
|
|
211
|
+
insecureDefaultsRules: () => insecureDefaultsRules,
|
|
212
|
+
isDuplicateFinding: () => isDuplicateFinding,
|
|
184
213
|
isSmallSuggestion: () => isSmallSuggestion,
|
|
185
214
|
isUpdateCheckEnabled: () => isUpdateCheckEnabled,
|
|
186
215
|
listActiveSessions: () => listActiveSessions,
|
|
187
216
|
listStreams: () => listStreams,
|
|
217
|
+
listTaintedSessions: () => listTaintedSessions,
|
|
188
218
|
loadBudgetedLearnings: () => loadBudgetedLearnings,
|
|
219
|
+
loadEvents: () => loadEvents,
|
|
189
220
|
loadFailures: () => loadFailures,
|
|
190
221
|
loadHandoff: () => loadHandoff,
|
|
222
|
+
loadIndexEntries: () => loadIndexEntries,
|
|
223
|
+
loadPricingData: () => loadPricingData,
|
|
191
224
|
loadRelevantLearnings: () => loadRelevantLearnings,
|
|
192
225
|
loadSessionSummary: () => loadSessionSummary,
|
|
193
226
|
loadState: () => loadState,
|
|
194
227
|
loadStreamIndex: () => loadStreamIndex,
|
|
195
228
|
logAgentAction: () => logAgentAction,
|
|
229
|
+
mapInjectionFindings: () => mapInjectionFindings,
|
|
230
|
+
mapSecurityFindings: () => mapSecurityFindings,
|
|
231
|
+
mapSecuritySeverity: () => mapSecuritySeverity,
|
|
232
|
+
mcpRules: () => mcpRules,
|
|
196
233
|
migrateToStreams: () => migrateToStreams,
|
|
197
234
|
networkRules: () => networkRules,
|
|
198
235
|
nodeRules: () => nodeRules,
|
|
236
|
+
parseCCRecords: () => parseCCRecords,
|
|
199
237
|
parseDateFromEntry: () => parseDateFromEntry,
|
|
200
238
|
parseDiff: () => parseDiff,
|
|
239
|
+
parseFile: () => parseFile,
|
|
240
|
+
parseFrontmatter: () => parseFrontmatter,
|
|
241
|
+
parseHarnessIgnore: () => parseHarnessIgnore,
|
|
242
|
+
parseLiteLLMData: () => parseLiteLLMData,
|
|
201
243
|
parseManifest: () => parseManifest,
|
|
202
244
|
parseRoadmap: () => parseRoadmap,
|
|
203
245
|
parseSecurityConfig: () => parseSecurityConfig,
|
|
204
246
|
parseSize: () => parseSize,
|
|
205
247
|
pathTraversalRules: () => pathTraversalRules,
|
|
206
248
|
previewFix: () => previewFix,
|
|
249
|
+
promoteSessionLearnings: () => promoteSessionLearnings,
|
|
207
250
|
pruneLearnings: () => pruneLearnings,
|
|
208
251
|
reactRules: () => reactRules,
|
|
209
252
|
readCheckState: () => readCheckState,
|
|
253
|
+
readCostRecords: () => readCostRecords,
|
|
210
254
|
readLockfile: () => readLockfile,
|
|
211
255
|
readSessionSection: () => readSessionSection,
|
|
212
256
|
readSessionSections: () => readSessionSections,
|
|
257
|
+
readTaint: () => readTaint,
|
|
213
258
|
removeContributions: () => removeContributions,
|
|
214
259
|
removeProvenance: () => removeProvenance,
|
|
215
260
|
requestMultiplePeerReviews: () => requestMultiplePeerReviews,
|
|
216
261
|
requestPeerReview: () => requestPeerReview,
|
|
217
262
|
resetFeedbackConfig: () => resetFeedbackConfig,
|
|
263
|
+
resetParserCache: () => resetParserCache,
|
|
218
264
|
resolveFileToLayer: () => resolveFileToLayer,
|
|
219
265
|
resolveModelTier: () => resolveModelTier,
|
|
220
266
|
resolveRuleSeverity: () => resolveRuleSeverity,
|
|
@@ -235,10 +281,13 @@ __export(index_exports, {
|
|
|
235
281
|
saveHandoff: () => saveHandoff,
|
|
236
282
|
saveState: () => saveState,
|
|
237
283
|
saveStreamIndex: () => saveStreamIndex,
|
|
284
|
+
scanForInjection: () => scanForInjection,
|
|
238
285
|
scopeContext: () => scopeContext,
|
|
286
|
+
searchSymbols: () => searchSymbols,
|
|
239
287
|
secretRules: () => secretRules,
|
|
240
288
|
serializeRoadmap: () => serializeRoadmap,
|
|
241
289
|
setActiveStream: () => setActiveStream,
|
|
290
|
+
sharpEdgesRules: () => sharpEdgesRules,
|
|
242
291
|
shouldRunCheck: () => shouldRunCheck,
|
|
243
292
|
spawnBackgroundCheck: () => spawnBackgroundCheck,
|
|
244
293
|
syncConstraintNodes: () => syncConstraintNodes,
|
|
@@ -246,6 +295,8 @@ __export(index_exports, {
|
|
|
246
295
|
tagUncitedFindings: () => tagUncitedFindings,
|
|
247
296
|
touchStream: () => touchStream,
|
|
248
297
|
trackAction: () => trackAction,
|
|
298
|
+
unfoldRange: () => unfoldRange,
|
|
299
|
+
unfoldSymbol: () => unfoldSymbol,
|
|
249
300
|
updateSessionEntryStatus: () => updateSessionEntryStatus,
|
|
250
301
|
updateSessionIndex: () => updateSessionIndex,
|
|
251
302
|
validateAgentsMap: () => validateAgentsMap,
|
|
@@ -261,6 +312,7 @@ __export(index_exports, {
|
|
|
261
312
|
writeConfig: () => writeConfig,
|
|
262
313
|
writeLockfile: () => writeLockfile,
|
|
263
314
|
writeSessionSummary: () => writeSessionSummary,
|
|
315
|
+
writeTaint: () => writeTaint,
|
|
264
316
|
xssRules: () => xssRules
|
|
265
317
|
});
|
|
266
318
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -284,17 +336,17 @@ var import_node_path = require("path");
|
|
|
284
336
|
var import_glob = require("glob");
|
|
285
337
|
var accessAsync = (0, import_util.promisify)(import_fs.access);
|
|
286
338
|
var readFileAsync = (0, import_util.promisify)(import_fs.readFile);
|
|
287
|
-
async function fileExists(
|
|
339
|
+
async function fileExists(path26) {
|
|
288
340
|
try {
|
|
289
|
-
await accessAsync(
|
|
341
|
+
await accessAsync(path26, import_fs.constants.F_OK);
|
|
290
342
|
return true;
|
|
291
343
|
} catch {
|
|
292
344
|
return false;
|
|
293
345
|
}
|
|
294
346
|
}
|
|
295
|
-
async function readFileContent(
|
|
347
|
+
async function readFileContent(path26) {
|
|
296
348
|
try {
|
|
297
|
-
const content = await readFileAsync(
|
|
349
|
+
const content = await readFileAsync(path26, "utf-8");
|
|
298
350
|
return (0, import_types.Ok)(content);
|
|
299
351
|
} catch (error) {
|
|
300
352
|
return (0, import_types.Err)(error);
|
|
@@ -345,15 +397,15 @@ function validateConfig(data, schema) {
|
|
|
345
397
|
let message = "Configuration validation failed";
|
|
346
398
|
const suggestions = [];
|
|
347
399
|
if (firstError) {
|
|
348
|
-
const
|
|
349
|
-
const pathDisplay =
|
|
400
|
+
const path26 = firstError.path.join(".");
|
|
401
|
+
const pathDisplay = path26 ? ` at "${path26}"` : "";
|
|
350
402
|
if (firstError.code === "invalid_type") {
|
|
351
403
|
const received = firstError.received;
|
|
352
404
|
const expected = firstError.expected;
|
|
353
405
|
if (received === "undefined") {
|
|
354
406
|
code = "MISSING_FIELD";
|
|
355
407
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
356
|
-
suggestions.push(`Field "${
|
|
408
|
+
suggestions.push(`Field "${path26}" is required and must be of type "${expected}"`);
|
|
357
409
|
} else {
|
|
358
410
|
code = "INVALID_TYPE";
|
|
359
411
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -569,27 +621,27 @@ function extractSections(content) {
|
|
|
569
621
|
}
|
|
570
622
|
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
571
623
|
}
|
|
572
|
-
function isExternalLink(
|
|
573
|
-
return
|
|
624
|
+
function isExternalLink(path26) {
|
|
625
|
+
return path26.startsWith("http://") || path26.startsWith("https://") || path26.startsWith("#") || path26.startsWith("mailto:");
|
|
574
626
|
}
|
|
575
627
|
function resolveLinkPath(linkPath, baseDir) {
|
|
576
628
|
return linkPath.startsWith(".") ? (0, import_path.join)(baseDir, linkPath) : linkPath;
|
|
577
629
|
}
|
|
578
|
-
async function validateAgentsMap(
|
|
579
|
-
const contentResult = await readFileContent(
|
|
630
|
+
async function validateAgentsMap(path26 = "./AGENTS.md") {
|
|
631
|
+
const contentResult = await readFileContent(path26);
|
|
580
632
|
if (!contentResult.ok) {
|
|
581
633
|
return (0, import_types.Err)(
|
|
582
634
|
createError(
|
|
583
635
|
"PARSE_ERROR",
|
|
584
636
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
585
|
-
{ path:
|
|
637
|
+
{ path: path26 },
|
|
586
638
|
["Ensure the file exists", "Check file permissions"]
|
|
587
639
|
)
|
|
588
640
|
);
|
|
589
641
|
}
|
|
590
642
|
const content = contentResult.value;
|
|
591
643
|
const sections = extractSections(content);
|
|
592
|
-
const baseDir = (0, import_path.dirname)(
|
|
644
|
+
const baseDir = (0, import_path.dirname)(path26);
|
|
593
645
|
const sectionTitles = sections.map((s) => s.title);
|
|
594
646
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
595
647
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -730,8 +782,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
730
782
|
|
|
731
783
|
// src/context/knowledge-map.ts
|
|
732
784
|
var import_path3 = require("path");
|
|
733
|
-
function suggestFix(
|
|
734
|
-
const targetName = (0, import_path3.basename)(
|
|
785
|
+
function suggestFix(path26, existingFiles) {
|
|
786
|
+
const targetName = (0, import_path3.basename)(path26).toLowerCase();
|
|
735
787
|
const similar = existingFiles.find((file) => {
|
|
736
788
|
const fileName = (0, import_path3.basename)(file).toLowerCase();
|
|
737
789
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -739,7 +791,7 @@ function suggestFix(path22, existingFiles) {
|
|
|
739
791
|
if (similar) {
|
|
740
792
|
return `Did you mean "${similar}"?`;
|
|
741
793
|
}
|
|
742
|
-
return `Create the file "${
|
|
794
|
+
return `Create the file "${path26}" or remove the link`;
|
|
743
795
|
}
|
|
744
796
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
745
797
|
const agentsPath = (0, import_path3.join)(rootDir, "AGENTS.md");
|
|
@@ -1345,8 +1397,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
1345
1397
|
return (0, import_types.Ok)(result.data);
|
|
1346
1398
|
}
|
|
1347
1399
|
const suggestions = result.error.issues.map((issue) => {
|
|
1348
|
-
const
|
|
1349
|
-
return
|
|
1400
|
+
const path26 = issue.path.join(".");
|
|
1401
|
+
return path26 ? `${path26}: ${issue.message}` : issue.message;
|
|
1350
1402
|
});
|
|
1351
1403
|
return (0, import_types.Err)(
|
|
1352
1404
|
createError(
|
|
@@ -1978,11 +2030,11 @@ function processExportListSpecifiers(exportDecl, exports2) {
|
|
|
1978
2030
|
var TypeScriptParser = class {
|
|
1979
2031
|
name = "typescript";
|
|
1980
2032
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1981
|
-
async parseFile(
|
|
1982
|
-
const contentResult = await readFileContent(
|
|
2033
|
+
async parseFile(path26) {
|
|
2034
|
+
const contentResult = await readFileContent(path26);
|
|
1983
2035
|
if (!contentResult.ok) {
|
|
1984
2036
|
return (0, import_types.Err)(
|
|
1985
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
2037
|
+
createParseError("NOT_FOUND", `File not found: ${path26}`, { path: path26 }, [
|
|
1986
2038
|
"Check that the file exists",
|
|
1987
2039
|
"Verify the path is correct"
|
|
1988
2040
|
])
|
|
@@ -1992,7 +2044,7 @@ var TypeScriptParser = class {
|
|
|
1992
2044
|
const ast = (0, import_typescript_estree.parse)(contentResult.value, {
|
|
1993
2045
|
loc: true,
|
|
1994
2046
|
range: true,
|
|
1995
|
-
jsx:
|
|
2047
|
+
jsx: path26.endsWith(".tsx"),
|
|
1996
2048
|
errorOnUnknownASTType: false
|
|
1997
2049
|
});
|
|
1998
2050
|
return (0, import_types.Ok)({
|
|
@@ -2003,7 +2055,7 @@ var TypeScriptParser = class {
|
|
|
2003
2055
|
} catch (e) {
|
|
2004
2056
|
const error = e;
|
|
2005
2057
|
return (0, import_types.Err)(
|
|
2006
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
2058
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path26}: ${error.message}`, { path: path26 }, [
|
|
2007
2059
|
"Check for syntax errors in the file",
|
|
2008
2060
|
"Ensure valid TypeScript syntax"
|
|
2009
2061
|
])
|
|
@@ -2188,22 +2240,22 @@ function extractInlineRefs(content) {
|
|
|
2188
2240
|
}
|
|
2189
2241
|
return refs;
|
|
2190
2242
|
}
|
|
2191
|
-
async function parseDocumentationFile(
|
|
2192
|
-
const contentResult = await readFileContent(
|
|
2243
|
+
async function parseDocumentationFile(path26) {
|
|
2244
|
+
const contentResult = await readFileContent(path26);
|
|
2193
2245
|
if (!contentResult.ok) {
|
|
2194
2246
|
return (0, import_types.Err)(
|
|
2195
2247
|
createEntropyError(
|
|
2196
2248
|
"PARSE_ERROR",
|
|
2197
|
-
`Failed to read documentation file: ${
|
|
2198
|
-
{ file:
|
|
2249
|
+
`Failed to read documentation file: ${path26}`,
|
|
2250
|
+
{ file: path26 },
|
|
2199
2251
|
["Check that the file exists"]
|
|
2200
2252
|
)
|
|
2201
2253
|
);
|
|
2202
2254
|
}
|
|
2203
2255
|
const content = contentResult.value;
|
|
2204
|
-
const type =
|
|
2256
|
+
const type = path26.endsWith(".md") ? "markdown" : "text";
|
|
2205
2257
|
return (0, import_types.Ok)({
|
|
2206
|
-
path:
|
|
2258
|
+
path: path26,
|
|
2207
2259
|
type,
|
|
2208
2260
|
content,
|
|
2209
2261
|
codeBlocks: extractCodeBlocks(content),
|
|
@@ -6868,6 +6920,8 @@ var SESSION_INDEX_FILE = "index.md";
|
|
|
6868
6920
|
var SUMMARY_FILE = "summary.md";
|
|
6869
6921
|
var SESSION_STATE_FILE = "session-state.json";
|
|
6870
6922
|
var ARCHIVE_DIR = "archive";
|
|
6923
|
+
var CONTENT_HASHES_FILE = "content-hashes.json";
|
|
6924
|
+
var EVENTS_FILE = "events.jsonl";
|
|
6871
6925
|
|
|
6872
6926
|
// src/state/stream-resolver.ts
|
|
6873
6927
|
var STREAMS_DIR = "streams";
|
|
@@ -7210,6 +7264,85 @@ async function saveState(projectPath, state, stream, session) {
|
|
|
7210
7264
|
// src/state/learnings.ts
|
|
7211
7265
|
var fs9 = __toESM(require("fs"));
|
|
7212
7266
|
var path6 = __toESM(require("path"));
|
|
7267
|
+
var crypto = __toESM(require("crypto"));
|
|
7268
|
+
function parseFrontmatter(line) {
|
|
7269
|
+
const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
|
|
7270
|
+
if (!match) return null;
|
|
7271
|
+
const hash = match[1];
|
|
7272
|
+
const tags = match[2] ? match[2].split(",").filter(Boolean) : [];
|
|
7273
|
+
return { hash, tags };
|
|
7274
|
+
}
|
|
7275
|
+
function computeEntryHash(text) {
|
|
7276
|
+
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 8);
|
|
7277
|
+
}
|
|
7278
|
+
function normalizeLearningContent(text) {
|
|
7279
|
+
let normalized = text;
|
|
7280
|
+
normalized = normalized.replace(/\d{4}-\d{2}-\d{2}/g, "");
|
|
7281
|
+
normalized = normalized.replace(/\[skill:[^\]]*\]/g, "");
|
|
7282
|
+
normalized = normalized.replace(/\[outcome:[^\]]*\]/g, "");
|
|
7283
|
+
normalized = normalized.replace(/^[\s]*[-*]\s+/gm, "");
|
|
7284
|
+
normalized = normalized.replace(/\*\*/g, "");
|
|
7285
|
+
normalized = normalized.replace(/:\s*/g, " ");
|
|
7286
|
+
normalized = normalized.toLowerCase();
|
|
7287
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
7288
|
+
return normalized;
|
|
7289
|
+
}
|
|
7290
|
+
function computeContentHash(text) {
|
|
7291
|
+
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
7292
|
+
}
|
|
7293
|
+
function loadContentHashes(stateDir) {
|
|
7294
|
+
const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
|
|
7295
|
+
if (!fs9.existsSync(hashesPath)) return {};
|
|
7296
|
+
try {
|
|
7297
|
+
const raw = fs9.readFileSync(hashesPath, "utf-8");
|
|
7298
|
+
const parsed = JSON.parse(raw);
|
|
7299
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
7300
|
+
return parsed;
|
|
7301
|
+
} catch {
|
|
7302
|
+
return {};
|
|
7303
|
+
}
|
|
7304
|
+
}
|
|
7305
|
+
function saveContentHashes(stateDir, index) {
|
|
7306
|
+
const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
|
|
7307
|
+
fs9.writeFileSync(hashesPath, JSON.stringify(index, null, 2) + "\n");
|
|
7308
|
+
}
|
|
7309
|
+
function rebuildContentHashes(stateDir) {
|
|
7310
|
+
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
7311
|
+
if (!fs9.existsSync(learningsPath)) return {};
|
|
7312
|
+
const content = fs9.readFileSync(learningsPath, "utf-8");
|
|
7313
|
+
const lines = content.split("\n");
|
|
7314
|
+
const index = {};
|
|
7315
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7316
|
+
const line = lines[i];
|
|
7317
|
+
const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
|
|
7318
|
+
if (isDatedBullet) {
|
|
7319
|
+
const learningMatch = line.match(/:\*\*\s*(.+)$/);
|
|
7320
|
+
if (learningMatch?.[1]) {
|
|
7321
|
+
const normalized = normalizeLearningContent(learningMatch[1]);
|
|
7322
|
+
const hash = computeContentHash(normalized);
|
|
7323
|
+
const dateMatch = line.match(/(\d{4}-\d{2}-\d{2})/);
|
|
7324
|
+
index[hash] = { date: dateMatch?.[1] ?? "", line: i + 1 };
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
saveContentHashes(stateDir, index);
|
|
7329
|
+
return index;
|
|
7330
|
+
}
|
|
7331
|
+
function extractIndexEntry(entry) {
|
|
7332
|
+
const lines = entry.split("\n");
|
|
7333
|
+
const summary = lines[0] ?? entry;
|
|
7334
|
+
const tags = [];
|
|
7335
|
+
const skillMatch = entry.match(/\[skill:([^\]]+)\]/);
|
|
7336
|
+
if (skillMatch?.[1]) tags.push(skillMatch[1]);
|
|
7337
|
+
const outcomeMatch = entry.match(/\[outcome:([^\]]+)\]/);
|
|
7338
|
+
if (outcomeMatch?.[1]) tags.push(outcomeMatch[1]);
|
|
7339
|
+
return {
|
|
7340
|
+
hash: computeEntryHash(entry),
|
|
7341
|
+
tags,
|
|
7342
|
+
summary,
|
|
7343
|
+
fullText: entry
|
|
7344
|
+
};
|
|
7345
|
+
}
|
|
7213
7346
|
var learningsCacheMap = /* @__PURE__ */ new Map();
|
|
7214
7347
|
function clearLearningsCache() {
|
|
7215
7348
|
learningsCacheMap.clear();
|
|
@@ -7221,27 +7354,55 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
|
|
|
7221
7354
|
const stateDir = dirResult.value;
|
|
7222
7355
|
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
7223
7356
|
fs9.mkdirSync(stateDir, { recursive: true });
|
|
7357
|
+
const normalizedContent = normalizeLearningContent(learning);
|
|
7358
|
+
const contentHash = computeContentHash(normalizedContent);
|
|
7359
|
+
const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
|
|
7360
|
+
let contentHashes;
|
|
7361
|
+
if (fs9.existsSync(hashesPath)) {
|
|
7362
|
+
contentHashes = loadContentHashes(stateDir);
|
|
7363
|
+
if (Object.keys(contentHashes).length === 0 && fs9.existsSync(learningsPath)) {
|
|
7364
|
+
contentHashes = rebuildContentHashes(stateDir);
|
|
7365
|
+
}
|
|
7366
|
+
} else if (fs9.existsSync(learningsPath)) {
|
|
7367
|
+
contentHashes = rebuildContentHashes(stateDir);
|
|
7368
|
+
} else {
|
|
7369
|
+
contentHashes = {};
|
|
7370
|
+
}
|
|
7371
|
+
if (contentHashes[contentHash]) {
|
|
7372
|
+
return (0, import_types.Ok)(void 0);
|
|
7373
|
+
}
|
|
7224
7374
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
7225
|
-
|
|
7375
|
+
const fmTags = [];
|
|
7376
|
+
if (skillName) fmTags.push(skillName);
|
|
7377
|
+
if (outcome) fmTags.push(outcome);
|
|
7378
|
+
let bulletLine;
|
|
7226
7379
|
if (skillName && outcome) {
|
|
7227
|
-
|
|
7228
|
-
- **${timestamp} [skill:${skillName}] [outcome:${outcome}]:** ${learning}
|
|
7229
|
-
`;
|
|
7380
|
+
bulletLine = `- **${timestamp} [skill:${skillName}] [outcome:${outcome}]:** ${learning}`;
|
|
7230
7381
|
} else if (skillName) {
|
|
7231
|
-
|
|
7232
|
-
- **${timestamp} [skill:${skillName}]:** ${learning}
|
|
7233
|
-
`;
|
|
7382
|
+
bulletLine = `- **${timestamp} [skill:${skillName}]:** ${learning}`;
|
|
7234
7383
|
} else {
|
|
7235
|
-
|
|
7236
|
-
- **${timestamp}:** ${learning}
|
|
7237
|
-
`;
|
|
7384
|
+
bulletLine = `- **${timestamp}:** ${learning}`;
|
|
7238
7385
|
}
|
|
7386
|
+
const hash = crypto.createHash("sha256").update(bulletLine).digest("hex").slice(0, 8);
|
|
7387
|
+
const tagsStr = fmTags.length > 0 ? ` tags:${fmTags.join(",")}` : "";
|
|
7388
|
+
const frontmatter = `<!-- hash:${hash}${tagsStr} -->`;
|
|
7389
|
+
const entry = `
|
|
7390
|
+
${frontmatter}
|
|
7391
|
+
${bulletLine}
|
|
7392
|
+
`;
|
|
7393
|
+
let existingLineCount;
|
|
7239
7394
|
if (!fs9.existsSync(learningsPath)) {
|
|
7240
7395
|
fs9.writeFileSync(learningsPath, `# Learnings
|
|
7241
7396
|
${entry}`);
|
|
7397
|
+
existingLineCount = 1;
|
|
7242
7398
|
} else {
|
|
7399
|
+
const existingContent = fs9.readFileSync(learningsPath, "utf-8");
|
|
7400
|
+
existingLineCount = existingContent.split("\n").length;
|
|
7243
7401
|
fs9.appendFileSync(learningsPath, entry);
|
|
7244
7402
|
}
|
|
7403
|
+
const bulletLine_lineNum = existingLineCount + 2;
|
|
7404
|
+
contentHashes[contentHash] = { date: timestamp ?? "", line: bulletLine_lineNum };
|
|
7405
|
+
saveContentHashes(stateDir, contentHashes);
|
|
7245
7406
|
learningsCacheMap.delete(learningsPath);
|
|
7246
7407
|
return (0, import_types.Ok)(void 0);
|
|
7247
7408
|
} catch (error) {
|
|
@@ -7289,7 +7450,30 @@ function analyzeLearningPatterns(entries) {
|
|
|
7289
7450
|
return patterns.sort((a, b) => b.count - a.count);
|
|
7290
7451
|
}
|
|
7291
7452
|
async function loadBudgetedLearnings(projectPath, options) {
|
|
7292
|
-
const { intent, tokenBudget = 1e3, skill, session, stream } = options;
|
|
7453
|
+
const { intent, tokenBudget = 1e3, skill, session, stream, depth = "summary" } = options;
|
|
7454
|
+
if (depth === "index") {
|
|
7455
|
+
const indexEntries = [];
|
|
7456
|
+
if (session) {
|
|
7457
|
+
const sessionResult = await loadIndexEntries(projectPath, skill, stream, session);
|
|
7458
|
+
if (sessionResult.ok) indexEntries.push(...sessionResult.value);
|
|
7459
|
+
}
|
|
7460
|
+
const globalResult2 = await loadIndexEntries(projectPath, skill, stream);
|
|
7461
|
+
if (globalResult2.ok) {
|
|
7462
|
+
const sessionHashes = new Set(indexEntries.map((e) => e.hash));
|
|
7463
|
+
const uniqueGlobal = globalResult2.value.filter((e) => !sessionHashes.has(e.hash));
|
|
7464
|
+
indexEntries.push(...uniqueGlobal);
|
|
7465
|
+
}
|
|
7466
|
+
const budgeted2 = [];
|
|
7467
|
+
let totalTokens2 = 0;
|
|
7468
|
+
for (const entry of indexEntries) {
|
|
7469
|
+
const separator = budgeted2.length > 0 ? "\n" : "";
|
|
7470
|
+
const entryCost = estimateTokens(entry.summary + separator);
|
|
7471
|
+
if (totalTokens2 + entryCost > tokenBudget) break;
|
|
7472
|
+
budgeted2.push(entry.summary);
|
|
7473
|
+
totalTokens2 += entryCost;
|
|
7474
|
+
}
|
|
7475
|
+
return (0, import_types.Ok)(budgeted2);
|
|
7476
|
+
}
|
|
7293
7477
|
const sortByRecencyAndRelevance = (entries) => {
|
|
7294
7478
|
return [...entries].sort((a, b) => {
|
|
7295
7479
|
const dateA = parseDateFromEntry(a) ?? "0000-00-00";
|
|
@@ -7308,7 +7492,9 @@ async function loadBudgetedLearnings(projectPath, options) {
|
|
|
7308
7492
|
}
|
|
7309
7493
|
const globalResult = await loadRelevantLearnings(projectPath, skill, stream);
|
|
7310
7494
|
if (globalResult.ok) {
|
|
7311
|
-
allEntries.
|
|
7495
|
+
const sessionSet = new Set(allEntries.map((e) => e.trim()));
|
|
7496
|
+
const uniqueGlobal = globalResult.value.filter((e) => !sessionSet.has(e.trim()));
|
|
7497
|
+
allEntries.push(...sortByRecencyAndRelevance(uniqueGlobal));
|
|
7312
7498
|
}
|
|
7313
7499
|
const budgeted = [];
|
|
7314
7500
|
let totalTokens = 0;
|
|
@@ -7321,6 +7507,68 @@ async function loadBudgetedLearnings(projectPath, options) {
|
|
|
7321
7507
|
}
|
|
7322
7508
|
return (0, import_types.Ok)(budgeted);
|
|
7323
7509
|
}
|
|
7510
|
+
async function loadIndexEntries(projectPath, skillName, stream, session) {
|
|
7511
|
+
try {
|
|
7512
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
7513
|
+
if (!dirResult.ok) return dirResult;
|
|
7514
|
+
const stateDir = dirResult.value;
|
|
7515
|
+
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
7516
|
+
if (!fs9.existsSync(learningsPath)) {
|
|
7517
|
+
return (0, import_types.Ok)([]);
|
|
7518
|
+
}
|
|
7519
|
+
const content = fs9.readFileSync(learningsPath, "utf-8");
|
|
7520
|
+
const lines = content.split("\n");
|
|
7521
|
+
const indexEntries = [];
|
|
7522
|
+
let pendingFrontmatter = null;
|
|
7523
|
+
let currentBlock = [];
|
|
7524
|
+
for (const line of lines) {
|
|
7525
|
+
if (line.startsWith("# ")) continue;
|
|
7526
|
+
const fm = parseFrontmatter(line);
|
|
7527
|
+
if (fm) {
|
|
7528
|
+
pendingFrontmatter = fm;
|
|
7529
|
+
continue;
|
|
7530
|
+
}
|
|
7531
|
+
const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
|
|
7532
|
+
const isHeading = /^## \d{4}-\d{2}-\d{2}/.test(line);
|
|
7533
|
+
if (isDatedBullet || isHeading) {
|
|
7534
|
+
if (pendingFrontmatter) {
|
|
7535
|
+
indexEntries.push({
|
|
7536
|
+
hash: pendingFrontmatter.hash,
|
|
7537
|
+
tags: pendingFrontmatter.tags,
|
|
7538
|
+
summary: line,
|
|
7539
|
+
fullText: ""
|
|
7540
|
+
// Placeholder — full text not loaded in index mode
|
|
7541
|
+
});
|
|
7542
|
+
pendingFrontmatter = null;
|
|
7543
|
+
} else {
|
|
7544
|
+
const idx = extractIndexEntry(line);
|
|
7545
|
+
indexEntries.push({
|
|
7546
|
+
hash: idx.hash,
|
|
7547
|
+
tags: idx.tags,
|
|
7548
|
+
summary: line,
|
|
7549
|
+
fullText: ""
|
|
7550
|
+
});
|
|
7551
|
+
}
|
|
7552
|
+
currentBlock = [line];
|
|
7553
|
+
} else if (line.trim() !== "" && currentBlock.length > 0) {
|
|
7554
|
+
currentBlock.push(line);
|
|
7555
|
+
}
|
|
7556
|
+
}
|
|
7557
|
+
if (skillName) {
|
|
7558
|
+
const filtered = indexEntries.filter(
|
|
7559
|
+
(e) => e.tags.includes(skillName) || e.summary.includes(`[skill:${skillName}]`)
|
|
7560
|
+
);
|
|
7561
|
+
return (0, import_types.Ok)(filtered);
|
|
7562
|
+
}
|
|
7563
|
+
return (0, import_types.Ok)(indexEntries);
|
|
7564
|
+
} catch (error) {
|
|
7565
|
+
return (0, import_types.Err)(
|
|
7566
|
+
new Error(
|
|
7567
|
+
`Failed to load index entries: ${error instanceof Error ? error.message : String(error)}`
|
|
7568
|
+
)
|
|
7569
|
+
);
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7324
7572
|
async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
7325
7573
|
try {
|
|
7326
7574
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
@@ -7343,6 +7591,7 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
|
7343
7591
|
let currentBlock = [];
|
|
7344
7592
|
for (const line of lines) {
|
|
7345
7593
|
if (line.startsWith("# ")) continue;
|
|
7594
|
+
if (/^<!--\s+hash:[a-f0-9]+/.test(line)) continue;
|
|
7346
7595
|
const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
|
|
7347
7596
|
const isHeading = /^## \d{4}-\d{2}-\d{2}/.test(line);
|
|
7348
7597
|
if (isDatedBullet || isHeading) {
|
|
@@ -7452,6 +7701,68 @@ async function pruneLearnings(projectPath, stream) {
|
|
|
7452
7701
|
);
|
|
7453
7702
|
}
|
|
7454
7703
|
}
|
|
7704
|
+
var PROMOTABLE_OUTCOMES = ["gotcha", "decision", "observation"];
|
|
7705
|
+
function isGeneralizable(entry) {
|
|
7706
|
+
for (const outcome of PROMOTABLE_OUTCOMES) {
|
|
7707
|
+
if (entry.includes(`[outcome:${outcome}]`)) return true;
|
|
7708
|
+
}
|
|
7709
|
+
return false;
|
|
7710
|
+
}
|
|
7711
|
+
async function promoteSessionLearnings(projectPath, sessionSlug, stream) {
|
|
7712
|
+
try {
|
|
7713
|
+
const sessionResult = await loadRelevantLearnings(projectPath, void 0, stream, sessionSlug);
|
|
7714
|
+
if (!sessionResult.ok) return sessionResult;
|
|
7715
|
+
const sessionEntries = sessionResult.value;
|
|
7716
|
+
if (sessionEntries.length === 0) {
|
|
7717
|
+
return (0, import_types.Ok)({ promoted: 0, skipped: 0 });
|
|
7718
|
+
}
|
|
7719
|
+
const toPromote = [];
|
|
7720
|
+
let skipped = 0;
|
|
7721
|
+
for (const entry of sessionEntries) {
|
|
7722
|
+
if (isGeneralizable(entry)) {
|
|
7723
|
+
toPromote.push(entry);
|
|
7724
|
+
} else {
|
|
7725
|
+
skipped++;
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7728
|
+
if (toPromote.length === 0) {
|
|
7729
|
+
return (0, import_types.Ok)({ promoted: 0, skipped });
|
|
7730
|
+
}
|
|
7731
|
+
const dirResult = await getStateDir(projectPath, stream);
|
|
7732
|
+
if (!dirResult.ok) return dirResult;
|
|
7733
|
+
const stateDir = dirResult.value;
|
|
7734
|
+
const globalPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
7735
|
+
const existingGlobal = fs9.existsSync(globalPath) ? fs9.readFileSync(globalPath, "utf-8") : "";
|
|
7736
|
+
const newEntries = toPromote.filter((entry) => !existingGlobal.includes(entry.trim()));
|
|
7737
|
+
if (newEntries.length === 0) {
|
|
7738
|
+
return (0, import_types.Ok)({ promoted: 0, skipped: skipped + toPromote.length });
|
|
7739
|
+
}
|
|
7740
|
+
const promotedContent = newEntries.join("\n\n") + "\n";
|
|
7741
|
+
if (!existingGlobal) {
|
|
7742
|
+
fs9.writeFileSync(globalPath, `# Learnings
|
|
7743
|
+
|
|
7744
|
+
${promotedContent}`);
|
|
7745
|
+
} else {
|
|
7746
|
+
fs9.appendFileSync(globalPath, "\n\n" + promotedContent);
|
|
7747
|
+
}
|
|
7748
|
+
learningsCacheMap.delete(globalPath);
|
|
7749
|
+
return (0, import_types.Ok)({
|
|
7750
|
+
promoted: newEntries.length,
|
|
7751
|
+
skipped: skipped + (toPromote.length - newEntries.length)
|
|
7752
|
+
});
|
|
7753
|
+
} catch (error) {
|
|
7754
|
+
return (0, import_types.Err)(
|
|
7755
|
+
new Error(
|
|
7756
|
+
`Failed to promote session learnings: ${error instanceof Error ? error.message : String(error)}`
|
|
7757
|
+
)
|
|
7758
|
+
);
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7761
|
+
async function countLearningEntries(projectPath, stream) {
|
|
7762
|
+
const loadResult = await loadRelevantLearnings(projectPath, void 0, stream);
|
|
7763
|
+
if (!loadResult.ok) return 0;
|
|
7764
|
+
return loadResult.value.length;
|
|
7765
|
+
}
|
|
7455
7766
|
|
|
7456
7767
|
// src/state/failures.ts
|
|
7457
7768
|
var fs10 = __toESM(require("fs"));
|
|
@@ -7913,6 +8224,151 @@ async function archiveSession(projectPath, sessionSlug) {
|
|
|
7913
8224
|
}
|
|
7914
8225
|
}
|
|
7915
8226
|
|
|
8227
|
+
// src/state/events.ts
|
|
8228
|
+
var fs16 = __toESM(require("fs"));
|
|
8229
|
+
var path13 = __toESM(require("path"));
|
|
8230
|
+
var import_zod6 = require("zod");
|
|
8231
|
+
var SkillEventSchema = import_zod6.z.object({
|
|
8232
|
+
timestamp: import_zod6.z.string(),
|
|
8233
|
+
skill: import_zod6.z.string(),
|
|
8234
|
+
session: import_zod6.z.string().optional(),
|
|
8235
|
+
type: import_zod6.z.enum(["phase_transition", "decision", "gate_result", "handoff", "error", "checkpoint"]),
|
|
8236
|
+
summary: import_zod6.z.string(),
|
|
8237
|
+
data: import_zod6.z.record(import_zod6.z.unknown()).optional(),
|
|
8238
|
+
refs: import_zod6.z.array(import_zod6.z.string()).optional(),
|
|
8239
|
+
contentHash: import_zod6.z.string().optional()
|
|
8240
|
+
});
|
|
8241
|
+
function computeEventHash(event, session) {
|
|
8242
|
+
const identity = `${event.skill}|${event.type}|${event.summary}|${session ?? ""}`;
|
|
8243
|
+
return computeContentHash(identity);
|
|
8244
|
+
}
|
|
8245
|
+
var knownHashesCache = /* @__PURE__ */ new Map();
|
|
8246
|
+
function loadKnownHashes(eventsPath) {
|
|
8247
|
+
const cached = knownHashesCache.get(eventsPath);
|
|
8248
|
+
if (cached) return cached;
|
|
8249
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
8250
|
+
if (fs16.existsSync(eventsPath)) {
|
|
8251
|
+
const content = fs16.readFileSync(eventsPath, "utf-8");
|
|
8252
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
8253
|
+
for (const line of lines) {
|
|
8254
|
+
try {
|
|
8255
|
+
const existing = JSON.parse(line);
|
|
8256
|
+
if (existing.contentHash) {
|
|
8257
|
+
hashes.add(existing.contentHash);
|
|
8258
|
+
}
|
|
8259
|
+
} catch {
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
8262
|
+
}
|
|
8263
|
+
knownHashesCache.set(eventsPath, hashes);
|
|
8264
|
+
return hashes;
|
|
8265
|
+
}
|
|
8266
|
+
function clearEventHashCache() {
|
|
8267
|
+
knownHashesCache.clear();
|
|
8268
|
+
}
|
|
8269
|
+
async function emitEvent(projectPath, event, options) {
|
|
8270
|
+
try {
|
|
8271
|
+
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
8272
|
+
if (!dirResult.ok) return dirResult;
|
|
8273
|
+
const stateDir = dirResult.value;
|
|
8274
|
+
const eventsPath = path13.join(stateDir, EVENTS_FILE);
|
|
8275
|
+
fs16.mkdirSync(stateDir, { recursive: true });
|
|
8276
|
+
const contentHash = computeEventHash(event, options?.session);
|
|
8277
|
+
const knownHashes = loadKnownHashes(eventsPath);
|
|
8278
|
+
if (knownHashes.has(contentHash)) {
|
|
8279
|
+
return (0, import_types.Ok)({ written: false, reason: "duplicate" });
|
|
8280
|
+
}
|
|
8281
|
+
const fullEvent = {
|
|
8282
|
+
...event,
|
|
8283
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8284
|
+
contentHash
|
|
8285
|
+
};
|
|
8286
|
+
if (options?.session) {
|
|
8287
|
+
fullEvent.session = options.session;
|
|
8288
|
+
}
|
|
8289
|
+
fs16.appendFileSync(eventsPath, JSON.stringify(fullEvent) + "\n");
|
|
8290
|
+
knownHashes.add(contentHash);
|
|
8291
|
+
return (0, import_types.Ok)({ written: true });
|
|
8292
|
+
} catch (error) {
|
|
8293
|
+
return (0, import_types.Err)(
|
|
8294
|
+
new Error(`Failed to emit event: ${error instanceof Error ? error.message : String(error)}`)
|
|
8295
|
+
);
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
8298
|
+
async function loadEvents(projectPath, options) {
|
|
8299
|
+
try {
|
|
8300
|
+
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
8301
|
+
if (!dirResult.ok) return dirResult;
|
|
8302
|
+
const stateDir = dirResult.value;
|
|
8303
|
+
const eventsPath = path13.join(stateDir, EVENTS_FILE);
|
|
8304
|
+
if (!fs16.existsSync(eventsPath)) {
|
|
8305
|
+
return (0, import_types.Ok)([]);
|
|
8306
|
+
}
|
|
8307
|
+
const content = fs16.readFileSync(eventsPath, "utf-8");
|
|
8308
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
8309
|
+
const events = [];
|
|
8310
|
+
for (const line of lines) {
|
|
8311
|
+
try {
|
|
8312
|
+
const parsed = JSON.parse(line);
|
|
8313
|
+
const result = SkillEventSchema.safeParse(parsed);
|
|
8314
|
+
if (result.success) {
|
|
8315
|
+
events.push(result.data);
|
|
8316
|
+
}
|
|
8317
|
+
} catch {
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
return (0, import_types.Ok)(events);
|
|
8321
|
+
} catch (error) {
|
|
8322
|
+
return (0, import_types.Err)(
|
|
8323
|
+
new Error(`Failed to load events: ${error instanceof Error ? error.message : String(error)}`)
|
|
8324
|
+
);
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
function formatPhaseTransition(event) {
|
|
8328
|
+
const data = event.data;
|
|
8329
|
+
const suffix = data?.taskCount ? ` (${data.taskCount} tasks)` : "";
|
|
8330
|
+
return `phase: ${data?.from ?? "?"} -> ${data?.to ?? "?"}${suffix}`;
|
|
8331
|
+
}
|
|
8332
|
+
function formatGateResult(event) {
|
|
8333
|
+
const data = event.data;
|
|
8334
|
+
const status = data?.passed ? "passed" : "failed";
|
|
8335
|
+
const checks = data?.checks?.map((c) => `${c.name} ${c.passed ? "Y" : "N"}`).join(", ");
|
|
8336
|
+
return checks ? `gate: ${status} (${checks})` : `gate: ${status}`;
|
|
8337
|
+
}
|
|
8338
|
+
function formatHandoffDetail(event) {
|
|
8339
|
+
const data = event.data;
|
|
8340
|
+
const direction = data?.toSkill ? ` -> ${data.toSkill}` : "";
|
|
8341
|
+
return `handoff: ${event.summary}${direction}`;
|
|
8342
|
+
}
|
|
8343
|
+
var EVENT_FORMATTERS = {
|
|
8344
|
+
phase_transition: formatPhaseTransition,
|
|
8345
|
+
gate_result: formatGateResult,
|
|
8346
|
+
decision: (event) => `decision: ${event.summary}`,
|
|
8347
|
+
handoff: formatHandoffDetail,
|
|
8348
|
+
error: (event) => `error: ${event.summary}`,
|
|
8349
|
+
checkpoint: (event) => `checkpoint: ${event.summary}`
|
|
8350
|
+
};
|
|
8351
|
+
function formatEventTimeline(events, limit = 20) {
|
|
8352
|
+
if (events.length === 0) return "";
|
|
8353
|
+
const recent = events.slice(-limit);
|
|
8354
|
+
return recent.map((event) => {
|
|
8355
|
+
const time = formatTime(event.timestamp);
|
|
8356
|
+
const formatter = EVENT_FORMATTERS[event.type];
|
|
8357
|
+
const detail = formatter ? formatter(event) : event.summary;
|
|
8358
|
+
return `- ${time} [${event.skill}] ${detail}`;
|
|
8359
|
+
}).join("\n");
|
|
8360
|
+
}
|
|
8361
|
+
function formatTime(timestamp) {
|
|
8362
|
+
try {
|
|
8363
|
+
const date = new Date(timestamp);
|
|
8364
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
8365
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
8366
|
+
return `${hours}:${minutes}`;
|
|
8367
|
+
} catch {
|
|
8368
|
+
return "??:??";
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
|
|
7916
8372
|
// src/workflow/runner.ts
|
|
7917
8373
|
async function executeWorkflow(workflow, executor) {
|
|
7918
8374
|
const stepResults = [];
|
|
@@ -8062,7 +8518,8 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
|
|
|
8062
8518
|
}
|
|
8063
8519
|
|
|
8064
8520
|
// src/security/scanner.ts
|
|
8065
|
-
var
|
|
8521
|
+
var fs18 = __toESM(require("fs/promises"));
|
|
8522
|
+
var import_minimatch5 = require("minimatch");
|
|
8066
8523
|
|
|
8067
8524
|
// src/security/rules/registry.ts
|
|
8068
8525
|
var RuleRegistry = class {
|
|
@@ -8093,7 +8550,7 @@ var RuleRegistry = class {
|
|
|
8093
8550
|
};
|
|
8094
8551
|
|
|
8095
8552
|
// src/security/config.ts
|
|
8096
|
-
var
|
|
8553
|
+
var import_zod7 = require("zod");
|
|
8097
8554
|
|
|
8098
8555
|
// src/security/types.ts
|
|
8099
8556
|
var DEFAULT_SECURITY_CONFIG = {
|
|
@@ -8104,19 +8561,19 @@ var DEFAULT_SECURITY_CONFIG = {
|
|
|
8104
8561
|
};
|
|
8105
8562
|
|
|
8106
8563
|
// src/security/config.ts
|
|
8107
|
-
var RuleOverrideSchema =
|
|
8108
|
-
var SecurityConfigSchema =
|
|
8109
|
-
enabled:
|
|
8110
|
-
strict:
|
|
8111
|
-
rules:
|
|
8112
|
-
exclude:
|
|
8113
|
-
external:
|
|
8114
|
-
semgrep:
|
|
8115
|
-
enabled:
|
|
8116
|
-
rulesets:
|
|
8564
|
+
var RuleOverrideSchema = import_zod7.z.enum(["off", "error", "warning", "info"]);
|
|
8565
|
+
var SecurityConfigSchema = import_zod7.z.object({
|
|
8566
|
+
enabled: import_zod7.z.boolean().default(true),
|
|
8567
|
+
strict: import_zod7.z.boolean().default(false),
|
|
8568
|
+
rules: import_zod7.z.record(import_zod7.z.string(), RuleOverrideSchema).optional().default({}),
|
|
8569
|
+
exclude: import_zod7.z.array(import_zod7.z.string()).optional().default(["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/fixtures/**"]),
|
|
8570
|
+
external: import_zod7.z.object({
|
|
8571
|
+
semgrep: import_zod7.z.object({
|
|
8572
|
+
enabled: import_zod7.z.union([import_zod7.z.literal("auto"), import_zod7.z.boolean()]).default("auto"),
|
|
8573
|
+
rulesets: import_zod7.z.array(import_zod7.z.string()).optional()
|
|
8117
8574
|
}).optional(),
|
|
8118
|
-
gitleaks:
|
|
8119
|
-
enabled:
|
|
8575
|
+
gitleaks: import_zod7.z.object({
|
|
8576
|
+
enabled: import_zod7.z.union([import_zod7.z.literal("auto"), import_zod7.z.boolean()]).default("auto")
|
|
8120
8577
|
}).optional()
|
|
8121
8578
|
}).optional()
|
|
8122
8579
|
});
|
|
@@ -8149,15 +8606,15 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
8149
8606
|
}
|
|
8150
8607
|
|
|
8151
8608
|
// src/security/stack-detector.ts
|
|
8152
|
-
var
|
|
8153
|
-
var
|
|
8609
|
+
var fs17 = __toESM(require("fs"));
|
|
8610
|
+
var path14 = __toESM(require("path"));
|
|
8154
8611
|
function detectStack(projectRoot) {
|
|
8155
8612
|
const stacks = [];
|
|
8156
|
-
const pkgJsonPath =
|
|
8157
|
-
if (
|
|
8613
|
+
const pkgJsonPath = path14.join(projectRoot, "package.json");
|
|
8614
|
+
if (fs17.existsSync(pkgJsonPath)) {
|
|
8158
8615
|
stacks.push("node");
|
|
8159
8616
|
try {
|
|
8160
|
-
const pkgJson = JSON.parse(
|
|
8617
|
+
const pkgJson = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
|
|
8161
8618
|
const allDeps = {
|
|
8162
8619
|
...pkgJson.dependencies,
|
|
8163
8620
|
...pkgJson.devDependencies
|
|
@@ -8172,13 +8629,13 @@ function detectStack(projectRoot) {
|
|
|
8172
8629
|
} catch {
|
|
8173
8630
|
}
|
|
8174
8631
|
}
|
|
8175
|
-
const goModPath =
|
|
8176
|
-
if (
|
|
8632
|
+
const goModPath = path14.join(projectRoot, "go.mod");
|
|
8633
|
+
if (fs17.existsSync(goModPath)) {
|
|
8177
8634
|
stacks.push("go");
|
|
8178
8635
|
}
|
|
8179
|
-
const requirementsPath =
|
|
8180
|
-
const pyprojectPath =
|
|
8181
|
-
if (
|
|
8636
|
+
const requirementsPath = path14.join(projectRoot, "requirements.txt");
|
|
8637
|
+
const pyprojectPath = path14.join(projectRoot, "pyproject.toml");
|
|
8638
|
+
if (fs17.existsSync(requirementsPath) || fs17.existsSync(pyprojectPath)) {
|
|
8182
8639
|
stacks.push("python");
|
|
8183
8640
|
}
|
|
8184
8641
|
return stacks;
|
|
@@ -8242,6 +8699,72 @@ var secretRules = [
|
|
|
8242
8699
|
message: "Hardcoded JWT token detected",
|
|
8243
8700
|
remediation: "Tokens should be fetched at runtime, not embedded in source",
|
|
8244
8701
|
references: ["CWE-798"]
|
|
8702
|
+
},
|
|
8703
|
+
{
|
|
8704
|
+
id: "SEC-SEC-006",
|
|
8705
|
+
name: "Anthropic API Key",
|
|
8706
|
+
category: "secrets",
|
|
8707
|
+
severity: "error",
|
|
8708
|
+
confidence: "high",
|
|
8709
|
+
patterns: [/sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}/],
|
|
8710
|
+
message: "Hardcoded Anthropic API key detected",
|
|
8711
|
+
remediation: "Use environment variables: process.env.ANTHROPIC_API_KEY",
|
|
8712
|
+
references: ["CWE-798"]
|
|
8713
|
+
},
|
|
8714
|
+
{
|
|
8715
|
+
id: "SEC-SEC-007",
|
|
8716
|
+
name: "OpenAI API Key",
|
|
8717
|
+
category: "secrets",
|
|
8718
|
+
severity: "error",
|
|
8719
|
+
confidence: "high",
|
|
8720
|
+
patterns: [/sk-proj-[A-Za-z0-9_-]{20,}/],
|
|
8721
|
+
message: "Hardcoded OpenAI API key detected",
|
|
8722
|
+
remediation: "Use environment variables: process.env.OPENAI_API_KEY",
|
|
8723
|
+
references: ["CWE-798"]
|
|
8724
|
+
},
|
|
8725
|
+
{
|
|
8726
|
+
id: "SEC-SEC-008",
|
|
8727
|
+
name: "Google API Key",
|
|
8728
|
+
category: "secrets",
|
|
8729
|
+
severity: "error",
|
|
8730
|
+
confidence: "high",
|
|
8731
|
+
patterns: [/AIza[A-Za-z0-9_-]{35}/],
|
|
8732
|
+
message: "Hardcoded Google API key detected",
|
|
8733
|
+
remediation: "Use environment variables or a secrets manager for Google API keys",
|
|
8734
|
+
references: ["CWE-798"]
|
|
8735
|
+
},
|
|
8736
|
+
{
|
|
8737
|
+
id: "SEC-SEC-009",
|
|
8738
|
+
name: "GitHub Personal Access Token",
|
|
8739
|
+
category: "secrets",
|
|
8740
|
+
severity: "error",
|
|
8741
|
+
confidence: "high",
|
|
8742
|
+
patterns: [/gh[pous]_[A-Za-z0-9_]{36,}/],
|
|
8743
|
+
message: "Hardcoded GitHub personal access token detected",
|
|
8744
|
+
remediation: "Use environment variables: process.env.GITHUB_TOKEN",
|
|
8745
|
+
references: ["CWE-798"]
|
|
8746
|
+
},
|
|
8747
|
+
{
|
|
8748
|
+
id: "SEC-SEC-010",
|
|
8749
|
+
name: "Stripe Live Key",
|
|
8750
|
+
category: "secrets",
|
|
8751
|
+
severity: "error",
|
|
8752
|
+
confidence: "high",
|
|
8753
|
+
patterns: [/\b[spr]k_live_[A-Za-z0-9]{24,}/],
|
|
8754
|
+
message: "Hardcoded Stripe live key detected",
|
|
8755
|
+
remediation: "Use environment variables for Stripe keys; never commit live keys",
|
|
8756
|
+
references: ["CWE-798"]
|
|
8757
|
+
},
|
|
8758
|
+
{
|
|
8759
|
+
id: "SEC-SEC-011",
|
|
8760
|
+
name: "Database Connection String with Credentials",
|
|
8761
|
+
category: "secrets",
|
|
8762
|
+
severity: "error",
|
|
8763
|
+
confidence: "high",
|
|
8764
|
+
patterns: [/(?:postgres|mysql|mongodb|redis|amqp|mssql)(?:\+\w+)?:\/\/[^/\s:]+:[^@/\s]+@/i],
|
|
8765
|
+
message: "Database connection string with embedded credentials detected",
|
|
8766
|
+
remediation: "Use environment variables for connection strings; separate credentials from URIs",
|
|
8767
|
+
references: ["CWE-798"]
|
|
8245
8768
|
}
|
|
8246
8769
|
];
|
|
8247
8770
|
|
|
@@ -8428,6 +8951,360 @@ var deserializationRules = [
|
|
|
8428
8951
|
}
|
|
8429
8952
|
];
|
|
8430
8953
|
|
|
8954
|
+
// src/security/rules/agent-config.ts
|
|
8955
|
+
var agentConfigRules = [
|
|
8956
|
+
{
|
|
8957
|
+
id: "SEC-AGT-001",
|
|
8958
|
+
name: "Hidden Unicode Characters",
|
|
8959
|
+
category: "agent-config",
|
|
8960
|
+
severity: "error",
|
|
8961
|
+
confidence: "high",
|
|
8962
|
+
patterns: [/\u200B|\u200C|\u200D|\uFEFF|\u2060/],
|
|
8963
|
+
fileGlob: "**/CLAUDE.md,**/AGENTS.md,**/*.yaml",
|
|
8964
|
+
message: "Hidden zero-width Unicode characters detected in agent configuration",
|
|
8965
|
+
remediation: "Remove invisible Unicode characters; they may hide malicious instructions",
|
|
8966
|
+
references: ["CWE-116"]
|
|
8967
|
+
},
|
|
8968
|
+
{
|
|
8969
|
+
id: "SEC-AGT-002",
|
|
8970
|
+
name: "URL Execution Directives",
|
|
8971
|
+
category: "agent-config",
|
|
8972
|
+
severity: "warning",
|
|
8973
|
+
confidence: "medium",
|
|
8974
|
+
patterns: [/\b(?:curl|wget)\s+\S+/i, /\bfetch\s*\(/i],
|
|
8975
|
+
fileGlob: "**/CLAUDE.md,**/AGENTS.md",
|
|
8976
|
+
message: "URL execution directive found in agent configuration",
|
|
8977
|
+
remediation: "Avoid instructing agents to download and execute remote content",
|
|
8978
|
+
references: ["CWE-94"]
|
|
8979
|
+
},
|
|
8980
|
+
{
|
|
8981
|
+
id: "SEC-AGT-003",
|
|
8982
|
+
name: "Wildcard Tool Permissions",
|
|
8983
|
+
category: "agent-config",
|
|
8984
|
+
severity: "warning",
|
|
8985
|
+
confidence: "high",
|
|
8986
|
+
patterns: [/(?:Bash|Write|Edit)\s*\(\s*\*\s*\)/],
|
|
8987
|
+
fileGlob: "**/.claude/**,**/settings*.json",
|
|
8988
|
+
message: "Wildcard tool permissions grant unrestricted access",
|
|
8989
|
+
remediation: "Scope tool permissions to specific patterns instead of wildcards",
|
|
8990
|
+
references: ["CWE-250"]
|
|
8991
|
+
},
|
|
8992
|
+
{
|
|
8993
|
+
id: "SEC-AGT-004",
|
|
8994
|
+
name: "Auto-approve Patterns",
|
|
8995
|
+
category: "agent-config",
|
|
8996
|
+
severity: "warning",
|
|
8997
|
+
confidence: "high",
|
|
8998
|
+
patterns: [/\bautoApprove\b/i, /\bauto_approve\b/i],
|
|
8999
|
+
fileGlob: "**/.claude/**,**/.mcp.json",
|
|
9000
|
+
message: "Auto-approve configuration bypasses human review of tool calls",
|
|
9001
|
+
remediation: "Review auto-approved tools carefully; prefer explicit approval for destructive operations",
|
|
9002
|
+
references: ["CWE-862"]
|
|
9003
|
+
},
|
|
9004
|
+
{
|
|
9005
|
+
id: "SEC-AGT-005",
|
|
9006
|
+
name: "Prompt Injection Surface",
|
|
9007
|
+
category: "agent-config",
|
|
9008
|
+
severity: "warning",
|
|
9009
|
+
confidence: "medium",
|
|
9010
|
+
patterns: [/\$\{[^}]*\}/, /\{\{[^}]*\}\}/],
|
|
9011
|
+
fileGlob: "**/skill.yaml",
|
|
9012
|
+
message: "Template interpolation syntax in skill YAML may enable prompt injection",
|
|
9013
|
+
remediation: "Avoid dynamic interpolation in skill descriptions; use static text",
|
|
9014
|
+
references: ["CWE-94"]
|
|
9015
|
+
},
|
|
9016
|
+
{
|
|
9017
|
+
id: "SEC-AGT-006",
|
|
9018
|
+
name: "Permission Bypass Flags",
|
|
9019
|
+
category: "agent-config",
|
|
9020
|
+
severity: "error",
|
|
9021
|
+
confidence: "high",
|
|
9022
|
+
patterns: [/--dangerously-skip-permissions/, /--no-verify/],
|
|
9023
|
+
fileGlob: "**/CLAUDE.md,**/AGENTS.md,**/.claude/**",
|
|
9024
|
+
message: "Permission bypass flag detected in agent configuration",
|
|
9025
|
+
remediation: "Remove flags that bypass safety checks; they undermine enforcement",
|
|
9026
|
+
references: ["CWE-863"]
|
|
9027
|
+
},
|
|
9028
|
+
{
|
|
9029
|
+
id: "SEC-AGT-007",
|
|
9030
|
+
name: "Hook Injection Surface",
|
|
9031
|
+
category: "agent-config",
|
|
9032
|
+
severity: "error",
|
|
9033
|
+
confidence: "low",
|
|
9034
|
+
patterns: [/\$\(/, /`[^`]+`/, /\s&&\s/, /\s\|\|\s/],
|
|
9035
|
+
fileGlob: "**/settings*.json,**/hooks.json",
|
|
9036
|
+
message: "Shell metacharacters in hook commands may enable command injection",
|
|
9037
|
+
remediation: "Use simple, single-command hooks without shell operators; chain logic inside the script",
|
|
9038
|
+
references: ["CWE-78"]
|
|
9039
|
+
}
|
|
9040
|
+
];
|
|
9041
|
+
|
|
9042
|
+
// src/security/rules/mcp.ts
|
|
9043
|
+
var mcpRules = [
|
|
9044
|
+
{
|
|
9045
|
+
id: "SEC-MCP-001",
|
|
9046
|
+
name: "Hardcoded MCP Secrets",
|
|
9047
|
+
category: "mcp",
|
|
9048
|
+
severity: "error",
|
|
9049
|
+
confidence: "medium",
|
|
9050
|
+
patterns: [/(?:API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)\s*["']?\s*:\s*["'][^"']{8,}["']/i],
|
|
9051
|
+
fileGlob: "**/.mcp.json",
|
|
9052
|
+
message: "Hardcoded secret detected in MCP server configuration",
|
|
9053
|
+
remediation: "Use environment variable references instead of inline secrets in .mcp.json",
|
|
9054
|
+
references: ["CWE-798"]
|
|
9055
|
+
},
|
|
9056
|
+
{
|
|
9057
|
+
id: "SEC-MCP-002",
|
|
9058
|
+
name: "Shell Injection in MCP Args",
|
|
9059
|
+
category: "mcp",
|
|
9060
|
+
severity: "error",
|
|
9061
|
+
confidence: "medium",
|
|
9062
|
+
patterns: [/\$\(/, /`[^`]+`/],
|
|
9063
|
+
fileGlob: "**/.mcp.json",
|
|
9064
|
+
message: "Shell metacharacters detected in MCP server arguments",
|
|
9065
|
+
remediation: "Use literal argument values; avoid shell interpolation in MCP args",
|
|
9066
|
+
references: ["CWE-78"]
|
|
9067
|
+
},
|
|
9068
|
+
{
|
|
9069
|
+
id: "SEC-MCP-003",
|
|
9070
|
+
name: "Network Exposure",
|
|
9071
|
+
category: "mcp",
|
|
9072
|
+
severity: "warning",
|
|
9073
|
+
confidence: "high",
|
|
9074
|
+
patterns: [/0\.0\.0\.0/, /["']\*["']\s*:\s*\d/, /host["']?\s*:\s*["']\*["']/i],
|
|
9075
|
+
fileGlob: "**/.mcp.json",
|
|
9076
|
+
message: "MCP server binding to all network interfaces (0.0.0.0 or wildcard *)",
|
|
9077
|
+
remediation: "Bind to 127.0.0.1 or localhost to restrict access to local machine",
|
|
9078
|
+
references: ["CWE-668"]
|
|
9079
|
+
},
|
|
9080
|
+
{
|
|
9081
|
+
id: "SEC-MCP-004",
|
|
9082
|
+
name: "Typosquatting Vector",
|
|
9083
|
+
category: "mcp",
|
|
9084
|
+
severity: "warning",
|
|
9085
|
+
confidence: "medium",
|
|
9086
|
+
patterns: [/\bnpx\s+(?:-y|--yes)\b/],
|
|
9087
|
+
fileGlob: "**/.mcp.json",
|
|
9088
|
+
message: "npx -y auto-installs packages without confirmation, enabling typosquatting",
|
|
9089
|
+
remediation: "Pin exact package versions or install packages explicitly before use",
|
|
9090
|
+
references: ["CWE-427"]
|
|
9091
|
+
},
|
|
9092
|
+
{
|
|
9093
|
+
id: "SEC-MCP-005",
|
|
9094
|
+
name: "Unencrypted Transport",
|
|
9095
|
+
category: "mcp",
|
|
9096
|
+
severity: "warning",
|
|
9097
|
+
confidence: "medium",
|
|
9098
|
+
patterns: [/http:\/\/(?!localhost\b|127\.0\.0\.1\b)/],
|
|
9099
|
+
fileGlob: "**/.mcp.json",
|
|
9100
|
+
message: "Unencrypted HTTP transport detected for MCP server connection",
|
|
9101
|
+
remediation: "Use https:// for all non-localhost MCP server connections",
|
|
9102
|
+
references: ["CWE-319"]
|
|
9103
|
+
}
|
|
9104
|
+
];
|
|
9105
|
+
|
|
9106
|
+
// src/security/rules/insecure-defaults.ts
|
|
9107
|
+
var insecureDefaultsRules = [
|
|
9108
|
+
{
|
|
9109
|
+
id: "SEC-DEF-001",
|
|
9110
|
+
name: "Security-Sensitive Fallback to Hardcoded Default",
|
|
9111
|
+
category: "insecure-defaults",
|
|
9112
|
+
severity: "warning",
|
|
9113
|
+
confidence: "medium",
|
|
9114
|
+
patterns: [
|
|
9115
|
+
/(?:SECRET|KEY|TOKEN|PASSWORD|SALT|PEPPER|SIGNING|ENCRYPTION|AUTH|JWT|SESSION).*(?:\|\||\?\?)\s*['"][^'"]+['"]/i
|
|
9116
|
+
],
|
|
9117
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9118
|
+
message: "Security-sensitive variable falls back to a hardcoded default when env var is missing",
|
|
9119
|
+
remediation: "Throw an error if the env var is missing instead of falling back to a default. Use a startup validation check.",
|
|
9120
|
+
references: ["CWE-1188"]
|
|
9121
|
+
},
|
|
9122
|
+
{
|
|
9123
|
+
id: "SEC-DEF-002",
|
|
9124
|
+
name: "TLS/SSL Disabled by Default",
|
|
9125
|
+
category: "insecure-defaults",
|
|
9126
|
+
severity: "warning",
|
|
9127
|
+
confidence: "medium",
|
|
9128
|
+
patterns: [
|
|
9129
|
+
/(?:tls|ssl|https|secure)\s*(?:=|:)\s*(?:false|config\??\.\w+\s*(?:\?\?|&&|\|\|)\s*false)/i
|
|
9130
|
+
],
|
|
9131
|
+
fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
|
|
9132
|
+
message: "Security feature defaults to disabled; missing configuration degrades to insecure mode",
|
|
9133
|
+
remediation: "Default security features to enabled (true). Require explicit opt-out, not opt-in.",
|
|
9134
|
+
references: ["CWE-1188"]
|
|
9135
|
+
},
|
|
9136
|
+
{
|
|
9137
|
+
id: "SEC-DEF-003",
|
|
9138
|
+
name: "Swallowed Authentication/Authorization Error",
|
|
9139
|
+
category: "insecure-defaults",
|
|
9140
|
+
severity: "warning",
|
|
9141
|
+
confidence: "low",
|
|
9142
|
+
patterns: [
|
|
9143
|
+
// Matches single-line empty catch: catch(e) { } or catch(e) { // ignore }
|
|
9144
|
+
// Note: multi-line catch blocks are handled by AI review, not this rule
|
|
9145
|
+
/catch\s*\([^)]*\)\s*\{\s*(?:\/\/\s*(?:ignore|skip|noop|todo)\b.*)?\s*\}/
|
|
9146
|
+
],
|
|
9147
|
+
fileGlob: "**/*auth*.{ts,js,mjs,cjs},**/*session*.{ts,js,mjs,cjs},**/*token*.{ts,js,mjs,cjs}",
|
|
9148
|
+
message: "Single-line empty catch block in authentication/authorization code may silently allow unauthorized access. Note: multi-line empty catch blocks are detected by AI review, not this mechanical rule.",
|
|
9149
|
+
remediation: "Re-throw the error or return an explicit denial. Never silently swallow auth errors.",
|
|
9150
|
+
references: ["CWE-754", "CWE-390"]
|
|
9151
|
+
},
|
|
9152
|
+
{
|
|
9153
|
+
id: "SEC-DEF-004",
|
|
9154
|
+
name: "Permissive CORS Fallback",
|
|
9155
|
+
category: "insecure-defaults",
|
|
9156
|
+
severity: "warning",
|
|
9157
|
+
confidence: "medium",
|
|
9158
|
+
patterns: [
|
|
9159
|
+
/(?:origin|cors)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*['"]\*/
|
|
9160
|
+
],
|
|
9161
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9162
|
+
message: "CORS origin falls back to wildcard (*) when configuration is missing",
|
|
9163
|
+
remediation: "Default to a restrictive origin list. Require explicit configuration for permissive CORS.",
|
|
9164
|
+
references: ["CWE-942"]
|
|
9165
|
+
},
|
|
9166
|
+
{
|
|
9167
|
+
id: "SEC-DEF-005",
|
|
9168
|
+
name: "Rate Limiting Disabled by Default",
|
|
9169
|
+
category: "insecure-defaults",
|
|
9170
|
+
severity: "info",
|
|
9171
|
+
confidence: "low",
|
|
9172
|
+
patterns: [
|
|
9173
|
+
/(?:rateLimit|rateLimiting|throttle)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*(?:false|0|null|undefined)/i
|
|
9174
|
+
],
|
|
9175
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9176
|
+
message: "Rate limiting defaults to disabled when configuration is missing",
|
|
9177
|
+
remediation: "Default to a sensible rate limit. Require explicit opt-out for disabling.",
|
|
9178
|
+
references: ["CWE-770"]
|
|
9179
|
+
}
|
|
9180
|
+
];
|
|
9181
|
+
|
|
9182
|
+
// src/security/rules/sharp-edges.ts
|
|
9183
|
+
var sharpEdgesRules = [
|
|
9184
|
+
// --- Deprecated Crypto APIs ---
|
|
9185
|
+
{
|
|
9186
|
+
id: "SEC-EDGE-001",
|
|
9187
|
+
name: "Deprecated createCipher API",
|
|
9188
|
+
category: "sharp-edges",
|
|
9189
|
+
severity: "error",
|
|
9190
|
+
confidence: "high",
|
|
9191
|
+
patterns: [/crypto\.createCipher\s*\(/],
|
|
9192
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9193
|
+
message: "crypto.createCipher is deprecated \u2014 uses weak key derivation (no IV)",
|
|
9194
|
+
remediation: "Use crypto.createCipheriv with a random IV and proper key derivation (scrypt/pbkdf2)",
|
|
9195
|
+
references: ["CWE-327"]
|
|
9196
|
+
},
|
|
9197
|
+
{
|
|
9198
|
+
id: "SEC-EDGE-002",
|
|
9199
|
+
name: "Deprecated createDecipher API",
|
|
9200
|
+
category: "sharp-edges",
|
|
9201
|
+
severity: "error",
|
|
9202
|
+
confidence: "high",
|
|
9203
|
+
patterns: [/crypto\.createDecipher\s*\(/],
|
|
9204
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9205
|
+
message: "crypto.createDecipher is deprecated \u2014 uses weak key derivation (no IV)",
|
|
9206
|
+
remediation: "Use crypto.createDecipheriv with the same IV used for encryption",
|
|
9207
|
+
references: ["CWE-327"]
|
|
9208
|
+
},
|
|
9209
|
+
{
|
|
9210
|
+
id: "SEC-EDGE-003",
|
|
9211
|
+
name: "ECB Mode Selection",
|
|
9212
|
+
category: "sharp-edges",
|
|
9213
|
+
severity: "warning",
|
|
9214
|
+
confidence: "high",
|
|
9215
|
+
patterns: [/-ecb['"]/],
|
|
9216
|
+
fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
|
|
9217
|
+
message: "ECB mode does not provide semantic security \u2014 identical plaintext blocks produce identical ciphertext",
|
|
9218
|
+
remediation: "Use CBC, CTR, or GCM mode instead of ECB",
|
|
9219
|
+
references: ["CWE-327"]
|
|
9220
|
+
},
|
|
9221
|
+
// --- Unsafe Deserialization ---
|
|
9222
|
+
{
|
|
9223
|
+
id: "SEC-EDGE-004",
|
|
9224
|
+
name: "yaml.load Without Safe Loader",
|
|
9225
|
+
category: "sharp-edges",
|
|
9226
|
+
severity: "error",
|
|
9227
|
+
confidence: "high",
|
|
9228
|
+
patterns: [
|
|
9229
|
+
/yaml\.load\s*\(/
|
|
9230
|
+
// Python: yaml.load() without SafeLoader
|
|
9231
|
+
],
|
|
9232
|
+
fileGlob: "**/*.py",
|
|
9233
|
+
message: "yaml.load() executes arbitrary Python objects \u2014 use yaml.safe_load() instead",
|
|
9234
|
+
remediation: "Replace yaml.load() with yaml.safe_load() or yaml.load(data, Loader=SafeLoader). Note: this rule will flag yaml.load(data, Loader=SafeLoader) \u2014 suppress with # harness-ignore SEC-EDGE-004: safe usage with SafeLoader",
|
|
9235
|
+
references: ["CWE-502"]
|
|
9236
|
+
},
|
|
9237
|
+
{
|
|
9238
|
+
id: "SEC-EDGE-005",
|
|
9239
|
+
name: "Pickle/Marshal Deserialization",
|
|
9240
|
+
category: "sharp-edges",
|
|
9241
|
+
severity: "error",
|
|
9242
|
+
confidence: "high",
|
|
9243
|
+
patterns: [/pickle\.loads?\s*\(/, /marshal\.loads?\s*\(/],
|
|
9244
|
+
fileGlob: "**/*.py",
|
|
9245
|
+
message: "pickle/marshal deserialization executes arbitrary code \u2014 never use on untrusted data",
|
|
9246
|
+
remediation: "Use JSON, MessagePack, or Protocol Buffers for untrusted data serialization",
|
|
9247
|
+
references: ["CWE-502"]
|
|
9248
|
+
},
|
|
9249
|
+
// --- TOCTOU (Time-of-Check to Time-of-Use) ---
|
|
9250
|
+
{
|
|
9251
|
+
id: "SEC-EDGE-006",
|
|
9252
|
+
name: "Check-Then-Act File Operation",
|
|
9253
|
+
category: "sharp-edges",
|
|
9254
|
+
severity: "warning",
|
|
9255
|
+
confidence: "medium",
|
|
9256
|
+
// Patterns use .{0,N} since scanner matches single lines only (no multiline mode)
|
|
9257
|
+
patterns: [
|
|
9258
|
+
/(?:existsSync|accessSync|statSync)\s*\([^)]+\).{0,50}(?:readFileSync|writeFileSync|unlinkSync|mkdirSync)\s*\(/
|
|
9259
|
+
],
|
|
9260
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9261
|
+
message: "Check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
|
|
9262
|
+
remediation: "Use the operation directly and handle ENOENT/EEXIST errors instead of checking first",
|
|
9263
|
+
references: ["CWE-367"]
|
|
9264
|
+
},
|
|
9265
|
+
{
|
|
9266
|
+
id: "SEC-EDGE-007",
|
|
9267
|
+
name: "Check-Then-Act File Operation (Async)",
|
|
9268
|
+
category: "sharp-edges",
|
|
9269
|
+
severity: "warning",
|
|
9270
|
+
confidence: "medium",
|
|
9271
|
+
// Uses .{0,N} since scanner matches single lines only (no multiline mode)
|
|
9272
|
+
patterns: [/(?:access|stat)\s*\([^)]+\).{0,80}(?:readFile|writeFile|unlink|mkdir)\s*\(/],
|
|
9273
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9274
|
+
message: "Async check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
|
|
9275
|
+
remediation: "Use the operation directly with try/catch instead of checking existence first",
|
|
9276
|
+
references: ["CWE-367"]
|
|
9277
|
+
},
|
|
9278
|
+
// --- Stringly-Typed Security ---
|
|
9279
|
+
{
|
|
9280
|
+
id: "SEC-EDGE-008",
|
|
9281
|
+
name: 'JWT Algorithm "none"',
|
|
9282
|
+
category: "sharp-edges",
|
|
9283
|
+
severity: "error",
|
|
9284
|
+
confidence: "high",
|
|
9285
|
+
patterns: [
|
|
9286
|
+
/algorithm[s]?\s*[:=]\s*\[?\s*['"]none['"]/i,
|
|
9287
|
+
/alg(?:orithm)?\s*[:=]\s*['"]none['"]/i
|
|
9288
|
+
],
|
|
9289
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
9290
|
+
message: 'JWT "none" algorithm disables signature verification entirely',
|
|
9291
|
+
remediation: 'Specify an explicit algorithm (e.g., "HS256", "RS256") and set algorithms allowlist in verify options',
|
|
9292
|
+
references: ["CWE-345"]
|
|
9293
|
+
},
|
|
9294
|
+
{
|
|
9295
|
+
id: "SEC-EDGE-009",
|
|
9296
|
+
name: "DES/RC4 Algorithm Selection",
|
|
9297
|
+
category: "sharp-edges",
|
|
9298
|
+
severity: "error",
|
|
9299
|
+
confidence: "high",
|
|
9300
|
+
patterns: [/['"]\s*(?:des|des-ede|des-ede3|des3|rc4|rc2|blowfish)\s*['"]/i],
|
|
9301
|
+
fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
|
|
9302
|
+
message: "Weak/deprecated cipher algorithm selected \u2014 DES, RC4, RC2, and Blowfish are broken or deprecated",
|
|
9303
|
+
remediation: "Use AES-256-GCM or ChaCha20-Poly1305",
|
|
9304
|
+
references: ["CWE-327"]
|
|
9305
|
+
}
|
|
9306
|
+
];
|
|
9307
|
+
|
|
8431
9308
|
// src/security/rules/stack/node.ts
|
|
8432
9309
|
var nodeRules = [
|
|
8433
9310
|
{
|
|
@@ -8541,6 +9418,14 @@ var goRules = [
|
|
|
8541
9418
|
];
|
|
8542
9419
|
|
|
8543
9420
|
// src/security/scanner.ts
|
|
9421
|
+
function parseHarnessIgnore(line, ruleId) {
|
|
9422
|
+
if (!line.includes("harness-ignore")) return null;
|
|
9423
|
+
if (!line.includes(ruleId)) return null;
|
|
9424
|
+
const match = line.match(/(?:\/\/|#)\s*harness-ignore\s+(SEC-[A-Z]+-\d+)(?::\s*(.+))?/);
|
|
9425
|
+
if (match?.[1] !== ruleId) return null;
|
|
9426
|
+
const text = match[2]?.trim();
|
|
9427
|
+
return { ruleId, justification: text || null };
|
|
9428
|
+
}
|
|
8544
9429
|
var SecurityScanner = class {
|
|
8545
9430
|
registry;
|
|
8546
9431
|
config;
|
|
@@ -8555,7 +9440,11 @@ var SecurityScanner = class {
|
|
|
8555
9440
|
...cryptoRules,
|
|
8556
9441
|
...pathTraversalRules,
|
|
8557
9442
|
...networkRules,
|
|
8558
|
-
...deserializationRules
|
|
9443
|
+
...deserializationRules,
|
|
9444
|
+
...agentConfigRules,
|
|
9445
|
+
...mcpRules,
|
|
9446
|
+
...insecureDefaultsRules,
|
|
9447
|
+
...sharpEdgesRules
|
|
8559
9448
|
]);
|
|
8560
9449
|
this.registry.registerAll([...nodeRules, ...expressRules, ...reactRules, ...goRules]);
|
|
8561
9450
|
this.activeRules = this.registry.getAll();
|
|
@@ -8564,11 +9453,40 @@ var SecurityScanner = class {
|
|
|
8564
9453
|
const stacks = detectStack(projectRoot);
|
|
8565
9454
|
this.activeRules = this.registry.getForStacks(stacks.length > 0 ? stacks : []);
|
|
8566
9455
|
}
|
|
9456
|
+
/**
|
|
9457
|
+
* Scan raw content against all active rules. Note: this method does NOT apply
|
|
9458
|
+
* fileGlob filtering — every active rule is evaluated regardless of filePath.
|
|
9459
|
+
* If you are scanning a specific file and want fileGlob-based rule filtering,
|
|
9460
|
+
* use {@link scanFile} instead.
|
|
9461
|
+
*/
|
|
8567
9462
|
scanContent(content, filePath, startLine = 1) {
|
|
8568
9463
|
if (!this.config.enabled) return [];
|
|
8569
|
-
const findings = [];
|
|
8570
9464
|
const lines = content.split("\n");
|
|
8571
|
-
|
|
9465
|
+
return this.scanLinesWithRules(lines, this.activeRules, filePath, startLine);
|
|
9466
|
+
}
|
|
9467
|
+
async scanFile(filePath) {
|
|
9468
|
+
if (!this.config.enabled) return [];
|
|
9469
|
+
const content = await fs18.readFile(filePath, "utf-8");
|
|
9470
|
+
return this.scanContentForFile(content, filePath, 1);
|
|
9471
|
+
}
|
|
9472
|
+
scanContentForFile(content, filePath, startLine = 1) {
|
|
9473
|
+
if (!this.config.enabled) return [];
|
|
9474
|
+
const lines = content.split("\n");
|
|
9475
|
+
const applicableRules = this.activeRules.filter((rule) => {
|
|
9476
|
+
if (!rule.fileGlob) return true;
|
|
9477
|
+
const globs = rule.fileGlob.split(",").map((g) => g.trim());
|
|
9478
|
+
return globs.some((glob2) => (0, import_minimatch5.minimatch)(filePath, glob2, { dot: true }));
|
|
9479
|
+
});
|
|
9480
|
+
return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
|
|
9481
|
+
}
|
|
9482
|
+
/**
|
|
9483
|
+
* Core scanning loop shared by scanContent and scanContentForFile.
|
|
9484
|
+
* Evaluates each rule against each line, handling suppression (FP gate)
|
|
9485
|
+
* and pattern matching uniformly.
|
|
9486
|
+
*/
|
|
9487
|
+
scanLinesWithRules(lines, rules, filePath, startLine) {
|
|
9488
|
+
const findings = [];
|
|
9489
|
+
for (const rule of rules) {
|
|
8572
9490
|
const resolved = resolveRuleSeverity(
|
|
8573
9491
|
rule.id,
|
|
8574
9492
|
rule.severity,
|
|
@@ -8578,7 +9496,25 @@ var SecurityScanner = class {
|
|
|
8578
9496
|
if (resolved === "off") continue;
|
|
8579
9497
|
for (let i = 0; i < lines.length; i++) {
|
|
8580
9498
|
const line = lines[i] ?? "";
|
|
8581
|
-
|
|
9499
|
+
const suppressionMatch = parseHarnessIgnore(line, rule.id);
|
|
9500
|
+
if (suppressionMatch) {
|
|
9501
|
+
if (!suppressionMatch.justification) {
|
|
9502
|
+
findings.push({
|
|
9503
|
+
ruleId: rule.id,
|
|
9504
|
+
ruleName: rule.name,
|
|
9505
|
+
category: rule.category,
|
|
9506
|
+
severity: this.config.strict ? "error" : "warning",
|
|
9507
|
+
confidence: "high",
|
|
9508
|
+
file: filePath,
|
|
9509
|
+
line: startLine + i,
|
|
9510
|
+
match: line.trim(),
|
|
9511
|
+
context: line,
|
|
9512
|
+
message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
|
|
9513
|
+
remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
|
|
9514
|
+
});
|
|
9515
|
+
}
|
|
9516
|
+
continue;
|
|
9517
|
+
}
|
|
8582
9518
|
for (const pattern of rule.patterns) {
|
|
8583
9519
|
pattern.lastIndex = 0;
|
|
8584
9520
|
if (pattern.test(line)) {
|
|
@@ -8603,11 +9539,6 @@ var SecurityScanner = class {
|
|
|
8603
9539
|
}
|
|
8604
9540
|
return findings;
|
|
8605
9541
|
}
|
|
8606
|
-
async scanFile(filePath) {
|
|
8607
|
-
if (!this.config.enabled) return [];
|
|
8608
|
-
const content = await fs17.readFile(filePath, "utf-8");
|
|
8609
|
-
return this.scanContent(content, filePath, 1);
|
|
8610
|
-
}
|
|
8611
9542
|
async scanFiles(filePaths) {
|
|
8612
9543
|
const allFindings = [];
|
|
8613
9544
|
let scannedCount = 0;
|
|
@@ -8627,10 +9558,418 @@ var SecurityScanner = class {
|
|
|
8627
9558
|
coverage: "baseline"
|
|
8628
9559
|
};
|
|
8629
9560
|
}
|
|
8630
|
-
};
|
|
9561
|
+
};
|
|
9562
|
+
|
|
9563
|
+
// src/security/injection-patterns.ts
|
|
9564
|
+
var hiddenUnicodePatterns = [
|
|
9565
|
+
{
|
|
9566
|
+
ruleId: "INJ-UNI-001",
|
|
9567
|
+
severity: "high",
|
|
9568
|
+
category: "hidden-unicode",
|
|
9569
|
+
description: "Zero-width characters that can hide malicious instructions",
|
|
9570
|
+
// eslint-disable-next-line no-misleading-character-class -- intentional: regex detects zero-width chars for security scanning
|
|
9571
|
+
pattern: /[\u200B\u200C\u200D\uFEFF\u2060]/
|
|
9572
|
+
},
|
|
9573
|
+
{
|
|
9574
|
+
ruleId: "INJ-UNI-002",
|
|
9575
|
+
severity: "high",
|
|
9576
|
+
category: "hidden-unicode",
|
|
9577
|
+
description: "RTL/LTR override characters that can disguise text direction",
|
|
9578
|
+
pattern: /[\u202A-\u202E\u2066-\u2069]/
|
|
9579
|
+
}
|
|
9580
|
+
];
|
|
9581
|
+
var reRolingPatterns = [
|
|
9582
|
+
{
|
|
9583
|
+
ruleId: "INJ-REROL-001",
|
|
9584
|
+
severity: "high",
|
|
9585
|
+
category: "explicit-re-roling",
|
|
9586
|
+
description: "Instruction to ignore/disregard/forget previous instructions",
|
|
9587
|
+
pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context|rules?|guidelines?)/i
|
|
9588
|
+
},
|
|
9589
|
+
{
|
|
9590
|
+
ruleId: "INJ-REROL-002",
|
|
9591
|
+
severity: "high",
|
|
9592
|
+
category: "explicit-re-roling",
|
|
9593
|
+
description: "Attempt to reassign the AI role",
|
|
9594
|
+
pattern: /you\s+are\s+now\s+(?:a\s+|an\s+)?(?:new\s+)?(?:helpful\s+)?(?:my\s+)?(?:\w+\s+)?(?:assistant|agent|AI|bot|chatbot|system|persona)\b/i
|
|
9595
|
+
},
|
|
9596
|
+
{
|
|
9597
|
+
ruleId: "INJ-REROL-003",
|
|
9598
|
+
severity: "high",
|
|
9599
|
+
category: "explicit-re-roling",
|
|
9600
|
+
description: "Direct instruction override attempt",
|
|
9601
|
+
pattern: /(?:new\s+)?(?:system\s+)?(?:instruction|directive|role|persona)\s*[:=]\s*/i
|
|
9602
|
+
}
|
|
9603
|
+
];
|
|
9604
|
+
var permissionEscalationPatterns = [
|
|
9605
|
+
{
|
|
9606
|
+
ruleId: "INJ-PERM-001",
|
|
9607
|
+
severity: "high",
|
|
9608
|
+
category: "permission-escalation",
|
|
9609
|
+
description: "Attempt to enable all tools or grant unrestricted access",
|
|
9610
|
+
pattern: /(?:allow|enable|grant)\s+all\s+(?:tools?|permissions?|access)/i
|
|
9611
|
+
},
|
|
9612
|
+
{
|
|
9613
|
+
ruleId: "INJ-PERM-002",
|
|
9614
|
+
severity: "high",
|
|
9615
|
+
category: "permission-escalation",
|
|
9616
|
+
description: "Attempt to disable safety or security features",
|
|
9617
|
+
pattern: /(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|security|restrictions?|guardrails?|protections?|checks?)/i
|
|
9618
|
+
},
|
|
9619
|
+
{
|
|
9620
|
+
ruleId: "INJ-PERM-003",
|
|
9621
|
+
severity: "high",
|
|
9622
|
+
category: "permission-escalation",
|
|
9623
|
+
description: "Auto-approve directive that bypasses human review",
|
|
9624
|
+
pattern: /(?:auto[- ]?approve|--no-verify|--dangerously-skip-permissions)/i
|
|
9625
|
+
}
|
|
9626
|
+
];
|
|
9627
|
+
var encodedPayloadPatterns = [
|
|
9628
|
+
{
|
|
9629
|
+
ruleId: "INJ-ENC-001",
|
|
9630
|
+
severity: "high",
|
|
9631
|
+
category: "encoded-payloads",
|
|
9632
|
+
description: "Base64-encoded string long enough to contain instructions (>=28 chars)",
|
|
9633
|
+
// Match base64 strings of 28+ chars (7+ groups of 4).
|
|
9634
|
+
// Excludes JWT tokens (eyJ prefix) and Bearer-prefixed tokens.
|
|
9635
|
+
pattern: /(?<!Bearer\s)(?<![:])(?<![A-Za-z0-9/])(?!eyJ)(?:[A-Za-z0-9+/]{4}){7,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?(?![A-Za-z0-9/])/
|
|
9636
|
+
},
|
|
9637
|
+
{
|
|
9638
|
+
ruleId: "INJ-ENC-002",
|
|
9639
|
+
severity: "high",
|
|
9640
|
+
category: "encoded-payloads",
|
|
9641
|
+
description: "Hex-encoded string long enough to contain directives (>=20 hex chars)",
|
|
9642
|
+
// Excludes hash-prefixed hex (sha256:, sha512:, md5:, etc.) and hex preceded by 0x.
|
|
9643
|
+
// Note: 40-char git SHA hashes (e.g. in `git log` output) may match — downstream
|
|
9644
|
+
// callers should filter matches of exactly 40 hex chars if scanning git output.
|
|
9645
|
+
pattern: /(?<![:x])(?<![A-Fa-f0-9])(?:[0-9a-fA-F]{2}){10,}(?![A-Fa-f0-9])/
|
|
9646
|
+
}
|
|
9647
|
+
];
|
|
9648
|
+
var indirectInjectionPatterns = [
|
|
9649
|
+
{
|
|
9650
|
+
ruleId: "INJ-IND-001",
|
|
9651
|
+
severity: "medium",
|
|
9652
|
+
category: "indirect-injection",
|
|
9653
|
+
description: "Instruction to influence future responses",
|
|
9654
|
+
pattern: /(?:when\s+the\s+user\s+asks|if\s+(?:the\s+user|someone|anyone)\s+asks)\s*,?\s*(?:say|respond|reply|answer|tell)/i
|
|
9655
|
+
},
|
|
9656
|
+
{
|
|
9657
|
+
ruleId: "INJ-IND-002",
|
|
9658
|
+
severity: "medium",
|
|
9659
|
+
category: "indirect-injection",
|
|
9660
|
+
description: "Directive to include content in responses",
|
|
9661
|
+
pattern: /(?:include|insert|add|embed|put)\s+(?:this|the\s+following)\s+(?:in|into|to)\s+(?:your|the)\s+(?:response|output|reply|answer)/i
|
|
9662
|
+
},
|
|
9663
|
+
{
|
|
9664
|
+
ruleId: "INJ-IND-003",
|
|
9665
|
+
severity: "medium",
|
|
9666
|
+
category: "indirect-injection",
|
|
9667
|
+
description: "Standing instruction to always respond a certain way",
|
|
9668
|
+
pattern: /always\s+(?:respond|reply|answer|say|output)\s+(?:with|that|by)/i
|
|
9669
|
+
}
|
|
9670
|
+
];
|
|
9671
|
+
var contextManipulationPatterns = [
|
|
9672
|
+
{
|
|
9673
|
+
ruleId: "INJ-CTX-001",
|
|
9674
|
+
severity: "medium",
|
|
9675
|
+
category: "context-manipulation",
|
|
9676
|
+
description: "Claim about system prompt content",
|
|
9677
|
+
pattern: /(?:the\s+)?(?:system\s+prompt|system\s+message|hidden\s+instructions?)\s+(?:says?|tells?|instructs?|contains?|is)/i
|
|
9678
|
+
},
|
|
9679
|
+
{
|
|
9680
|
+
ruleId: "INJ-CTX-002",
|
|
9681
|
+
severity: "medium",
|
|
9682
|
+
category: "context-manipulation",
|
|
9683
|
+
description: "Claim about AI instructions",
|
|
9684
|
+
pattern: /your\s+(?:instructions?|directives?|guidelines?|rules?)\s+(?:are|say|tell|state)/i
|
|
9685
|
+
},
|
|
9686
|
+
{
|
|
9687
|
+
ruleId: "INJ-CTX-003",
|
|
9688
|
+
severity: "medium",
|
|
9689
|
+
category: "context-manipulation",
|
|
9690
|
+
description: "Fake XML/HTML system or instruction tags",
|
|
9691
|
+
// Case-sensitive: only match lowercase tags to avoid false positives on
|
|
9692
|
+
// React components like <User>, <Context>, <Role> etc.
|
|
9693
|
+
pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/
|
|
9694
|
+
},
|
|
9695
|
+
{
|
|
9696
|
+
ruleId: "INJ-CTX-004",
|
|
9697
|
+
severity: "medium",
|
|
9698
|
+
category: "context-manipulation",
|
|
9699
|
+
description: "Fake JSON role assignment mimicking chat format",
|
|
9700
|
+
pattern: /[{,]\s*"role"\s*:\s*"(?:system|assistant|function)"/i
|
|
9701
|
+
}
|
|
9702
|
+
];
|
|
9703
|
+
var socialEngineeringPatterns = [
|
|
9704
|
+
{
|
|
9705
|
+
ruleId: "INJ-SOC-001",
|
|
9706
|
+
severity: "medium",
|
|
9707
|
+
category: "social-engineering",
|
|
9708
|
+
description: "Urgency pressure to bypass checks",
|
|
9709
|
+
pattern: /(?:this\s+is\s+(?:very\s+)?urgent|this\s+is\s+(?:an?\s+)?emergency|do\s+(?:this|it)\s+(?:now|immediately))\b/i
|
|
9710
|
+
},
|
|
9711
|
+
{
|
|
9712
|
+
ruleId: "INJ-SOC-002",
|
|
9713
|
+
severity: "medium",
|
|
9714
|
+
category: "social-engineering",
|
|
9715
|
+
description: "False authority claim",
|
|
9716
|
+
pattern: /(?:the\s+)?(?:admin|administrator|manager|CEO|CTO|owner|supervisor)\s+(?:authorized|approved|said|told|confirmed|requested)/i
|
|
9717
|
+
},
|
|
9718
|
+
{
|
|
9719
|
+
ruleId: "INJ-SOC-003",
|
|
9720
|
+
severity: "medium",
|
|
9721
|
+
category: "social-engineering",
|
|
9722
|
+
description: "Testing pretext to bypass safety",
|
|
9723
|
+
pattern: /(?:for\s+testing\s+purposes?|this\s+is\s+(?:just\s+)?a\s+test|in\s+test\s+mode)\b/i
|
|
9724
|
+
}
|
|
9725
|
+
];
|
|
9726
|
+
var suspiciousPatterns = [
|
|
9727
|
+
{
|
|
9728
|
+
ruleId: "INJ-SUS-001",
|
|
9729
|
+
severity: "low",
|
|
9730
|
+
category: "suspicious-patterns",
|
|
9731
|
+
description: "Excessive consecutive whitespace (>10 chars) mid-line that may hide content",
|
|
9732
|
+
// Only match whitespace runs not at the start of a line (indentation is normal)
|
|
9733
|
+
pattern: /\S\s{11,}/
|
|
9734
|
+
},
|
|
9735
|
+
{
|
|
9736
|
+
ruleId: "INJ-SUS-002",
|
|
9737
|
+
severity: "low",
|
|
9738
|
+
category: "suspicious-patterns",
|
|
9739
|
+
description: "Repeated delimiters (>5) that may indicate obfuscation",
|
|
9740
|
+
pattern: /([|;,=\-_~`])\1{5,}/
|
|
9741
|
+
},
|
|
9742
|
+
{
|
|
9743
|
+
ruleId: "INJ-SUS-003",
|
|
9744
|
+
severity: "low",
|
|
9745
|
+
category: "suspicious-patterns",
|
|
9746
|
+
description: "Mathematical alphanumeric symbols used as Latin character substitutes",
|
|
9747
|
+
// Mathematical bold/italic/script Unicode ranges (U+1D400-U+1D7FF)
|
|
9748
|
+
pattern: /[\uD835][\uDC00-\uDFFF]/
|
|
9749
|
+
}
|
|
9750
|
+
];
|
|
9751
|
+
var ALL_PATTERNS = [
|
|
9752
|
+
...hiddenUnicodePatterns,
|
|
9753
|
+
...reRolingPatterns,
|
|
9754
|
+
...permissionEscalationPatterns,
|
|
9755
|
+
...encodedPayloadPatterns,
|
|
9756
|
+
...indirectInjectionPatterns,
|
|
9757
|
+
...contextManipulationPatterns,
|
|
9758
|
+
...socialEngineeringPatterns,
|
|
9759
|
+
...suspiciousPatterns
|
|
9760
|
+
];
|
|
9761
|
+
function scanForInjection(text) {
|
|
9762
|
+
const findings = [];
|
|
9763
|
+
const lines = text.split("\n");
|
|
9764
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
9765
|
+
const line = lines[lineIdx];
|
|
9766
|
+
for (const rule of ALL_PATTERNS) {
|
|
9767
|
+
if (rule.pattern.test(line)) {
|
|
9768
|
+
findings.push({
|
|
9769
|
+
severity: rule.severity,
|
|
9770
|
+
ruleId: rule.ruleId,
|
|
9771
|
+
match: line.trim(),
|
|
9772
|
+
line: lineIdx + 1
|
|
9773
|
+
});
|
|
9774
|
+
}
|
|
9775
|
+
}
|
|
9776
|
+
}
|
|
9777
|
+
const severityOrder = {
|
|
9778
|
+
high: 0,
|
|
9779
|
+
medium: 1,
|
|
9780
|
+
low: 2
|
|
9781
|
+
};
|
|
9782
|
+
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
9783
|
+
return findings;
|
|
9784
|
+
}
|
|
9785
|
+
function getInjectionPatterns() {
|
|
9786
|
+
return ALL_PATTERNS;
|
|
9787
|
+
}
|
|
9788
|
+
var DESTRUCTIVE_BASH = [
|
|
9789
|
+
/\bgit\s+push\b/,
|
|
9790
|
+
/\bgit\s+commit\b/,
|
|
9791
|
+
/\brm\s+-rf?\b/,
|
|
9792
|
+
/\brm\s+-r\b/
|
|
9793
|
+
];
|
|
9794
|
+
|
|
9795
|
+
// src/security/taint.ts
|
|
9796
|
+
var import_node_fs4 = require("fs");
|
|
9797
|
+
var import_node_path7 = require("path");
|
|
9798
|
+
var TAINT_DURATION_MS = 30 * 60 * 1e3;
|
|
9799
|
+
var DEFAULT_SESSION_ID = "default";
|
|
9800
|
+
function getTaintFilePath(projectRoot, sessionId) {
|
|
9801
|
+
const id = sessionId || DEFAULT_SESSION_ID;
|
|
9802
|
+
return (0, import_node_path7.join)(projectRoot, ".harness", `session-taint-${id}.json`);
|
|
9803
|
+
}
|
|
9804
|
+
function readTaint(projectRoot, sessionId) {
|
|
9805
|
+
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
9806
|
+
let content;
|
|
9807
|
+
try {
|
|
9808
|
+
content = (0, import_node_fs4.readFileSync)(filePath, "utf8");
|
|
9809
|
+
} catch {
|
|
9810
|
+
return null;
|
|
9811
|
+
}
|
|
9812
|
+
let state;
|
|
9813
|
+
try {
|
|
9814
|
+
state = JSON.parse(content);
|
|
9815
|
+
} catch {
|
|
9816
|
+
try {
|
|
9817
|
+
(0, import_node_fs4.unlinkSync)(filePath);
|
|
9818
|
+
} catch {
|
|
9819
|
+
}
|
|
9820
|
+
return null;
|
|
9821
|
+
}
|
|
9822
|
+
if (!state.sessionId || !state.taintedAt || !state.expiresAt || !state.findings) {
|
|
9823
|
+
try {
|
|
9824
|
+
(0, import_node_fs4.unlinkSync)(filePath);
|
|
9825
|
+
} catch {
|
|
9826
|
+
}
|
|
9827
|
+
return null;
|
|
9828
|
+
}
|
|
9829
|
+
return state;
|
|
9830
|
+
}
|
|
9831
|
+
function checkTaint(projectRoot, sessionId) {
|
|
9832
|
+
const state = readTaint(projectRoot, sessionId);
|
|
9833
|
+
if (!state) {
|
|
9834
|
+
return { tainted: false, expired: false, state: null };
|
|
9835
|
+
}
|
|
9836
|
+
const now = /* @__PURE__ */ new Date();
|
|
9837
|
+
const expiresAt = new Date(state.expiresAt);
|
|
9838
|
+
if (now >= expiresAt) {
|
|
9839
|
+
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
9840
|
+
try {
|
|
9841
|
+
(0, import_node_fs4.unlinkSync)(filePath);
|
|
9842
|
+
} catch {
|
|
9843
|
+
}
|
|
9844
|
+
return { tainted: false, expired: true, state };
|
|
9845
|
+
}
|
|
9846
|
+
return { tainted: true, expired: false, state };
|
|
9847
|
+
}
|
|
9848
|
+
function writeTaint(projectRoot, sessionId, reason, findings, source) {
|
|
9849
|
+
const id = sessionId || DEFAULT_SESSION_ID;
|
|
9850
|
+
const filePath = getTaintFilePath(projectRoot, id);
|
|
9851
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9852
|
+
const dir = (0, import_node_path7.dirname)(filePath);
|
|
9853
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
9854
|
+
const existing = readTaint(projectRoot, id);
|
|
9855
|
+
const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
|
|
9856
|
+
const taintFindings = findings.map((f) => ({
|
|
9857
|
+
ruleId: f.ruleId,
|
|
9858
|
+
severity: f.severity,
|
|
9859
|
+
match: f.match,
|
|
9860
|
+
source,
|
|
9861
|
+
detectedAt: now
|
|
9862
|
+
}));
|
|
9863
|
+
const state = {
|
|
9864
|
+
sessionId: id,
|
|
9865
|
+
taintedAt: existing?.taintedAt || now,
|
|
9866
|
+
expiresAt: new Date(Date.now() + TAINT_DURATION_MS).toISOString(),
|
|
9867
|
+
reason,
|
|
9868
|
+
severity: existing?.severity === "high" || maxSeverity === "high" ? "high" : "medium",
|
|
9869
|
+
findings: [...existing?.findings || [], ...taintFindings]
|
|
9870
|
+
};
|
|
9871
|
+
(0, import_node_fs4.writeFileSync)(filePath, JSON.stringify(state, null, 2) + "\n");
|
|
9872
|
+
return state;
|
|
9873
|
+
}
|
|
9874
|
+
function clearTaint(projectRoot, sessionId) {
|
|
9875
|
+
if (sessionId) {
|
|
9876
|
+
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
9877
|
+
try {
|
|
9878
|
+
(0, import_node_fs4.unlinkSync)(filePath);
|
|
9879
|
+
return 1;
|
|
9880
|
+
} catch {
|
|
9881
|
+
return 0;
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9884
|
+
const harnessDir = (0, import_node_path7.join)(projectRoot, ".harness");
|
|
9885
|
+
let count = 0;
|
|
9886
|
+
try {
|
|
9887
|
+
const files = (0, import_node_fs4.readdirSync)(harnessDir);
|
|
9888
|
+
for (const file of files) {
|
|
9889
|
+
if (file.startsWith("session-taint-") && file.endsWith(".json")) {
|
|
9890
|
+
try {
|
|
9891
|
+
(0, import_node_fs4.unlinkSync)((0, import_node_path7.join)(harnessDir, file));
|
|
9892
|
+
count++;
|
|
9893
|
+
} catch {
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9896
|
+
}
|
|
9897
|
+
} catch {
|
|
9898
|
+
}
|
|
9899
|
+
return count;
|
|
9900
|
+
}
|
|
9901
|
+
function listTaintedSessions(projectRoot) {
|
|
9902
|
+
const harnessDir = (0, import_node_path7.join)(projectRoot, ".harness");
|
|
9903
|
+
const sessions = [];
|
|
9904
|
+
try {
|
|
9905
|
+
const files = (0, import_node_fs4.readdirSync)(harnessDir);
|
|
9906
|
+
for (const file of files) {
|
|
9907
|
+
if (file.startsWith("session-taint-") && file.endsWith(".json")) {
|
|
9908
|
+
const sessionId = file.replace("session-taint-", "").replace(".json", "");
|
|
9909
|
+
const result = checkTaint(projectRoot, sessionId);
|
|
9910
|
+
if (result.tainted) {
|
|
9911
|
+
sessions.push(sessionId);
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
} catch {
|
|
9916
|
+
}
|
|
9917
|
+
return sessions;
|
|
9918
|
+
}
|
|
9919
|
+
|
|
9920
|
+
// src/security/scan-config-shared.ts
|
|
9921
|
+
function mapSecuritySeverity(severity) {
|
|
9922
|
+
if (severity === "error") return "high";
|
|
9923
|
+
if (severity === "warning") return "medium";
|
|
9924
|
+
return "low";
|
|
9925
|
+
}
|
|
9926
|
+
function computeOverallSeverity(findings) {
|
|
9927
|
+
if (findings.length === 0) return "clean";
|
|
9928
|
+
if (findings.some((f) => f.severity === "high")) return "high";
|
|
9929
|
+
if (findings.some((f) => f.severity === "medium")) return "medium";
|
|
9930
|
+
return "low";
|
|
9931
|
+
}
|
|
9932
|
+
function computeScanExitCode(results) {
|
|
9933
|
+
for (const r of results) {
|
|
9934
|
+
if (r.overallSeverity === "high") return 2;
|
|
9935
|
+
}
|
|
9936
|
+
for (const r of results) {
|
|
9937
|
+
if (r.overallSeverity === "medium") return 1;
|
|
9938
|
+
}
|
|
9939
|
+
return 0;
|
|
9940
|
+
}
|
|
9941
|
+
function mapInjectionFindings(injectionFindings) {
|
|
9942
|
+
return injectionFindings.map((f) => ({
|
|
9943
|
+
ruleId: f.ruleId,
|
|
9944
|
+
severity: f.severity,
|
|
9945
|
+
message: `Injection pattern detected: ${f.ruleId}`,
|
|
9946
|
+
match: f.match,
|
|
9947
|
+
...f.line !== void 0 ? { line: f.line } : {}
|
|
9948
|
+
}));
|
|
9949
|
+
}
|
|
9950
|
+
function isDuplicateFinding(existing, secFinding) {
|
|
9951
|
+
return existing.some(
|
|
9952
|
+
(e) => e.line === secFinding.line && e.match === secFinding.match.trim() && e.ruleId.split("-")[0] === secFinding.ruleId.split("-")[0]
|
|
9953
|
+
);
|
|
9954
|
+
}
|
|
9955
|
+
function mapSecurityFindings(secFindings, existing) {
|
|
9956
|
+
const result = [];
|
|
9957
|
+
for (const f of secFindings) {
|
|
9958
|
+
if (!isDuplicateFinding(existing, f)) {
|
|
9959
|
+
result.push({
|
|
9960
|
+
ruleId: f.ruleId,
|
|
9961
|
+
severity: mapSecuritySeverity(f.severity),
|
|
9962
|
+
message: f.message,
|
|
9963
|
+
match: f.match,
|
|
9964
|
+
...f.line !== void 0 ? { line: f.line } : {}
|
|
9965
|
+
});
|
|
9966
|
+
}
|
|
9967
|
+
}
|
|
9968
|
+
return result;
|
|
9969
|
+
}
|
|
8631
9970
|
|
|
8632
9971
|
// src/ci/check-orchestrator.ts
|
|
8633
|
-
var
|
|
9972
|
+
var path15 = __toESM(require("path"));
|
|
8634
9973
|
var ALL_CHECKS = [
|
|
8635
9974
|
"validate",
|
|
8636
9975
|
"deps",
|
|
@@ -8643,7 +9982,7 @@ var ALL_CHECKS = [
|
|
|
8643
9982
|
];
|
|
8644
9983
|
async function runValidateCheck(projectRoot, config) {
|
|
8645
9984
|
const issues = [];
|
|
8646
|
-
const agentsPath =
|
|
9985
|
+
const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
8647
9986
|
const result = await validateAgentsMap(agentsPath);
|
|
8648
9987
|
if (!result.ok) {
|
|
8649
9988
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -8700,7 +10039,7 @@ async function runDepsCheck(projectRoot, config) {
|
|
|
8700
10039
|
}
|
|
8701
10040
|
async function runDocsCheck(projectRoot, config) {
|
|
8702
10041
|
const issues = [];
|
|
8703
|
-
const docsDir =
|
|
10042
|
+
const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
|
|
8704
10043
|
const entropyConfig = config.entropy || {};
|
|
8705
10044
|
const result = await checkDocCoverage("project", {
|
|
8706
10045
|
docsDir,
|
|
@@ -8812,7 +10151,7 @@ async function runPerfCheck(projectRoot, config) {
|
|
|
8812
10151
|
if (perfReport.complexity) {
|
|
8813
10152
|
for (const v of perfReport.complexity.violations) {
|
|
8814
10153
|
issues.push({
|
|
8815
|
-
severity:
|
|
10154
|
+
severity: "warning",
|
|
8816
10155
|
message: `[Tier ${v.tier}] ${v.metric}: ${v.function} in ${v.file} (${v.value} > ${v.threshold})`,
|
|
8817
10156
|
file: v.file,
|
|
8818
10157
|
line: v.line
|
|
@@ -8978,7 +10317,7 @@ async function runCIChecks(input) {
|
|
|
8978
10317
|
}
|
|
8979
10318
|
|
|
8980
10319
|
// src/review/mechanical-checks.ts
|
|
8981
|
-
var
|
|
10320
|
+
var path16 = __toESM(require("path"));
|
|
8982
10321
|
async function runMechanicalChecks(options) {
|
|
8983
10322
|
const { projectRoot, config, skip = [], changedFiles } = options;
|
|
8984
10323
|
const findings = [];
|
|
@@ -8990,7 +10329,7 @@ async function runMechanicalChecks(options) {
|
|
|
8990
10329
|
};
|
|
8991
10330
|
if (!skip.includes("validate")) {
|
|
8992
10331
|
try {
|
|
8993
|
-
const agentsPath =
|
|
10332
|
+
const agentsPath = path16.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
8994
10333
|
const result = await validateAgentsMap(agentsPath);
|
|
8995
10334
|
if (!result.ok) {
|
|
8996
10335
|
statuses.validate = "fail";
|
|
@@ -9027,7 +10366,7 @@ async function runMechanicalChecks(options) {
|
|
|
9027
10366
|
statuses.validate = "fail";
|
|
9028
10367
|
findings.push({
|
|
9029
10368
|
tool: "validate",
|
|
9030
|
-
file:
|
|
10369
|
+
file: path16.join(projectRoot, "AGENTS.md"),
|
|
9031
10370
|
message: err instanceof Error ? err.message : String(err),
|
|
9032
10371
|
severity: "error"
|
|
9033
10372
|
});
|
|
@@ -9091,7 +10430,7 @@ async function runMechanicalChecks(options) {
|
|
|
9091
10430
|
(async () => {
|
|
9092
10431
|
const localFindings = [];
|
|
9093
10432
|
try {
|
|
9094
|
-
const docsDir =
|
|
10433
|
+
const docsDir = path16.join(projectRoot, config.docsDir ?? "docs");
|
|
9095
10434
|
const result = await checkDocCoverage("project", { docsDir });
|
|
9096
10435
|
if (!result.ok) {
|
|
9097
10436
|
statuses["check-docs"] = "warn";
|
|
@@ -9118,7 +10457,7 @@ async function runMechanicalChecks(options) {
|
|
|
9118
10457
|
statuses["check-docs"] = "warn";
|
|
9119
10458
|
localFindings.push({
|
|
9120
10459
|
tool: "check-docs",
|
|
9121
|
-
file:
|
|
10460
|
+
file: path16.join(projectRoot, "docs"),
|
|
9122
10461
|
message: err instanceof Error ? err.message : String(err),
|
|
9123
10462
|
severity: "warning"
|
|
9124
10463
|
});
|
|
@@ -9266,7 +10605,7 @@ function detectChangeType(commitMessage, diff2) {
|
|
|
9266
10605
|
}
|
|
9267
10606
|
|
|
9268
10607
|
// src/review/context-scoper.ts
|
|
9269
|
-
var
|
|
10608
|
+
var path17 = __toESM(require("path"));
|
|
9270
10609
|
var ALL_DOMAINS = ["compliance", "bug", "security", "architecture"];
|
|
9271
10610
|
var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
|
|
9272
10611
|
function computeContextBudget(diffLines) {
|
|
@@ -9274,18 +10613,18 @@ function computeContextBudget(diffLines) {
|
|
|
9274
10613
|
return diffLines;
|
|
9275
10614
|
}
|
|
9276
10615
|
function isWithinProject(absPath, projectRoot) {
|
|
9277
|
-
const resolvedRoot =
|
|
9278
|
-
const resolvedPath =
|
|
9279
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
10616
|
+
const resolvedRoot = path17.resolve(projectRoot) + path17.sep;
|
|
10617
|
+
const resolvedPath = path17.resolve(absPath);
|
|
10618
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path17.resolve(projectRoot);
|
|
9280
10619
|
}
|
|
9281
10620
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
9282
|
-
const absPath =
|
|
10621
|
+
const absPath = path17.isAbsolute(filePath) ? filePath : path17.join(projectRoot, filePath);
|
|
9283
10622
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
9284
10623
|
const result = await readFileContent(absPath);
|
|
9285
10624
|
if (!result.ok) return null;
|
|
9286
10625
|
const content = result.value;
|
|
9287
10626
|
const lines = content.split("\n").length;
|
|
9288
|
-
const relPath =
|
|
10627
|
+
const relPath = path17.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
|
|
9289
10628
|
return { path: relPath, content, reason, lines };
|
|
9290
10629
|
}
|
|
9291
10630
|
function extractImportSources2(content) {
|
|
@@ -9300,18 +10639,18 @@ function extractImportSources2(content) {
|
|
|
9300
10639
|
}
|
|
9301
10640
|
async function resolveImportPath2(projectRoot, fromFile, importSource) {
|
|
9302
10641
|
if (!importSource.startsWith(".")) return null;
|
|
9303
|
-
const fromDir =
|
|
9304
|
-
const basePath =
|
|
10642
|
+
const fromDir = path17.dirname(path17.join(projectRoot, fromFile));
|
|
10643
|
+
const basePath = path17.resolve(fromDir, importSource);
|
|
9305
10644
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
9306
10645
|
const relBase = relativePosix(projectRoot, basePath);
|
|
9307
10646
|
const candidates = [
|
|
9308
10647
|
relBase + ".ts",
|
|
9309
10648
|
relBase + ".tsx",
|
|
9310
10649
|
relBase + ".mts",
|
|
9311
|
-
|
|
10650
|
+
path17.join(relBase, "index.ts")
|
|
9312
10651
|
];
|
|
9313
10652
|
for (const candidate of candidates) {
|
|
9314
|
-
const absCandidate =
|
|
10653
|
+
const absCandidate = path17.join(projectRoot, candidate);
|
|
9315
10654
|
if (await fileExists(absCandidate)) {
|
|
9316
10655
|
return candidate;
|
|
9317
10656
|
}
|
|
@@ -9319,7 +10658,7 @@ async function resolveImportPath2(projectRoot, fromFile, importSource) {
|
|
|
9319
10658
|
return null;
|
|
9320
10659
|
}
|
|
9321
10660
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
9322
|
-
const baseName =
|
|
10661
|
+
const baseName = path17.basename(sourceFile, path17.extname(sourceFile));
|
|
9323
10662
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
9324
10663
|
const results = await findFiles(pattern, projectRoot);
|
|
9325
10664
|
return results.map((f) => relativePosix(projectRoot, f));
|
|
@@ -10128,7 +11467,7 @@ async function fanOutReview(options) {
|
|
|
10128
11467
|
}
|
|
10129
11468
|
|
|
10130
11469
|
// src/review/validate-findings.ts
|
|
10131
|
-
var
|
|
11470
|
+
var path18 = __toESM(require("path"));
|
|
10132
11471
|
var DOWNGRADE_MAP = {
|
|
10133
11472
|
critical: "important",
|
|
10134
11473
|
important: "suggestion",
|
|
@@ -10149,7 +11488,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
10149
11488
|
let normalized = filePath;
|
|
10150
11489
|
normalized = normalized.replace(/\\/g, "/");
|
|
10151
11490
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
10152
|
-
if (
|
|
11491
|
+
if (path18.isAbsolute(normalized)) {
|
|
10153
11492
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
10154
11493
|
if (normalized.startsWith(root)) {
|
|
10155
11494
|
normalized = normalized.slice(root.length);
|
|
@@ -10174,12 +11513,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
10174
11513
|
while ((match = importRegex.exec(content)) !== null) {
|
|
10175
11514
|
const importPath = match[1];
|
|
10176
11515
|
if (!importPath.startsWith(".")) continue;
|
|
10177
|
-
const dir =
|
|
10178
|
-
let resolved =
|
|
11516
|
+
const dir = path18.dirname(current.file);
|
|
11517
|
+
let resolved = path18.join(dir, importPath).replace(/\\/g, "/");
|
|
10179
11518
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
10180
11519
|
resolved += ".ts";
|
|
10181
11520
|
}
|
|
10182
|
-
resolved =
|
|
11521
|
+
resolved = path18.normalize(resolved).replace(/\\/g, "/");
|
|
10183
11522
|
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
10184
11523
|
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
10185
11524
|
}
|
|
@@ -10196,7 +11535,7 @@ async function validateFindings(options) {
|
|
|
10196
11535
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
|
|
10197
11536
|
continue;
|
|
10198
11537
|
}
|
|
10199
|
-
const absoluteFile =
|
|
11538
|
+
const absoluteFile = path18.isAbsolute(finding.file) ? finding.file : path18.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
10200
11539
|
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
10201
11540
|
continue;
|
|
10202
11541
|
}
|
|
@@ -10824,7 +12163,7 @@ function parseRoadmap(markdown) {
|
|
|
10824
12163
|
if (!fmMatch) {
|
|
10825
12164
|
return (0, import_types19.Err)(new Error("Missing or malformed YAML frontmatter"));
|
|
10826
12165
|
}
|
|
10827
|
-
const fmResult =
|
|
12166
|
+
const fmResult = parseFrontmatter2(fmMatch[1]);
|
|
10828
12167
|
if (!fmResult.ok) return fmResult;
|
|
10829
12168
|
const body = markdown.slice(fmMatch[0].length);
|
|
10830
12169
|
const milestonesResult = parseMilestones(body);
|
|
@@ -10834,7 +12173,7 @@ function parseRoadmap(markdown) {
|
|
|
10834
12173
|
milestones: milestonesResult.value
|
|
10835
12174
|
});
|
|
10836
12175
|
}
|
|
10837
|
-
function
|
|
12176
|
+
function parseFrontmatter2(raw) {
|
|
10838
12177
|
const lines = raw.split("\n");
|
|
10839
12178
|
const map = /* @__PURE__ */ new Map();
|
|
10840
12179
|
for (const line of lines) {
|
|
@@ -11000,8 +12339,8 @@ function serializeFeature(feature) {
|
|
|
11000
12339
|
}
|
|
11001
12340
|
|
|
11002
12341
|
// src/roadmap/sync.ts
|
|
11003
|
-
var
|
|
11004
|
-
var
|
|
12342
|
+
var fs19 = __toESM(require("fs"));
|
|
12343
|
+
var path19 = __toESM(require("path"));
|
|
11005
12344
|
var import_types20 = require("@harness-engineering/types");
|
|
11006
12345
|
function inferStatus(feature, projectPath, allFeatures) {
|
|
11007
12346
|
if (feature.blockedBy.length > 0) {
|
|
@@ -11016,10 +12355,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
11016
12355
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
11017
12356
|
const useRootState = featuresWithPlans.length <= 1;
|
|
11018
12357
|
if (useRootState) {
|
|
11019
|
-
const rootStatePath =
|
|
11020
|
-
if (
|
|
12358
|
+
const rootStatePath = path19.join(projectPath, ".harness", "state.json");
|
|
12359
|
+
if (fs19.existsSync(rootStatePath)) {
|
|
11021
12360
|
try {
|
|
11022
|
-
const raw =
|
|
12361
|
+
const raw = fs19.readFileSync(rootStatePath, "utf-8");
|
|
11023
12362
|
const state = JSON.parse(raw);
|
|
11024
12363
|
if (state.progress) {
|
|
11025
12364
|
for (const status of Object.values(state.progress)) {
|
|
@@ -11030,16 +12369,16 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
11030
12369
|
}
|
|
11031
12370
|
}
|
|
11032
12371
|
}
|
|
11033
|
-
const sessionsDir =
|
|
11034
|
-
if (
|
|
12372
|
+
const sessionsDir = path19.join(projectPath, ".harness", "sessions");
|
|
12373
|
+
if (fs19.existsSync(sessionsDir)) {
|
|
11035
12374
|
try {
|
|
11036
|
-
const sessionDirs =
|
|
12375
|
+
const sessionDirs = fs19.readdirSync(sessionsDir, { withFileTypes: true });
|
|
11037
12376
|
for (const entry of sessionDirs) {
|
|
11038
12377
|
if (!entry.isDirectory()) continue;
|
|
11039
|
-
const autopilotPath =
|
|
11040
|
-
if (!
|
|
12378
|
+
const autopilotPath = path19.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
12379
|
+
if (!fs19.existsSync(autopilotPath)) continue;
|
|
11041
12380
|
try {
|
|
11042
|
-
const raw =
|
|
12381
|
+
const raw = fs19.readFileSync(autopilotPath, "utf-8");
|
|
11043
12382
|
const autopilot = JSON.parse(raw);
|
|
11044
12383
|
if (!autopilot.phases) continue;
|
|
11045
12384
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -11069,17 +12408,26 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
11069
12408
|
if (anyStarted) return "in-progress";
|
|
11070
12409
|
return null;
|
|
11071
12410
|
}
|
|
12411
|
+
var STATUS_RANK = {
|
|
12412
|
+
backlog: 0,
|
|
12413
|
+
planned: 1,
|
|
12414
|
+
blocked: 1,
|
|
12415
|
+
// lateral to planned — sync can move to/from blocked freely
|
|
12416
|
+
"in-progress": 2,
|
|
12417
|
+
done: 3
|
|
12418
|
+
};
|
|
12419
|
+
function isRegression(from, to) {
|
|
12420
|
+
return STATUS_RANK[to] < STATUS_RANK[from];
|
|
12421
|
+
}
|
|
11072
12422
|
function syncRoadmap(options) {
|
|
11073
12423
|
const { projectPath, roadmap, forceSync } = options;
|
|
11074
|
-
const isManuallyEdited = new Date(roadmap.frontmatter.lastManualEdit) > new Date(roadmap.frontmatter.lastSynced);
|
|
11075
|
-
const skipOverride = isManuallyEdited && !forceSync;
|
|
11076
12424
|
const allFeatures = roadmap.milestones.flatMap((m) => m.features);
|
|
11077
12425
|
const changes = [];
|
|
11078
12426
|
for (const feature of allFeatures) {
|
|
11079
|
-
if (skipOverride) continue;
|
|
11080
12427
|
const inferred = inferStatus(feature, projectPath, allFeatures);
|
|
11081
12428
|
if (inferred === null) continue;
|
|
11082
12429
|
if (inferred === feature.status) continue;
|
|
12430
|
+
if (!forceSync && isRegression(feature.status, inferred)) continue;
|
|
11083
12431
|
changes.push({
|
|
11084
12432
|
feature: feature.name,
|
|
11085
12433
|
from: feature.status,
|
|
@@ -11088,48 +12436,60 @@ function syncRoadmap(options) {
|
|
|
11088
12436
|
}
|
|
11089
12437
|
return (0, import_types20.Ok)(changes);
|
|
11090
12438
|
}
|
|
12439
|
+
function applySyncChanges(roadmap, changes) {
|
|
12440
|
+
for (const change of changes) {
|
|
12441
|
+
for (const m of roadmap.milestones) {
|
|
12442
|
+
const feature = m.features.find((f) => f.name.toLowerCase() === change.feature.toLowerCase());
|
|
12443
|
+
if (feature) {
|
|
12444
|
+
feature.status = change.to;
|
|
12445
|
+
break;
|
|
12446
|
+
}
|
|
12447
|
+
}
|
|
12448
|
+
}
|
|
12449
|
+
roadmap.frontmatter.lastSynced = (/* @__PURE__ */ new Date()).toISOString();
|
|
12450
|
+
}
|
|
11091
12451
|
|
|
11092
12452
|
// src/interaction/types.ts
|
|
11093
|
-
var
|
|
11094
|
-
var InteractionTypeSchema =
|
|
11095
|
-
var QuestionSchema =
|
|
11096
|
-
text:
|
|
11097
|
-
options:
|
|
11098
|
-
default:
|
|
12453
|
+
var import_zod8 = require("zod");
|
|
12454
|
+
var InteractionTypeSchema = import_zod8.z.enum(["question", "confirmation", "transition"]);
|
|
12455
|
+
var QuestionSchema = import_zod8.z.object({
|
|
12456
|
+
text: import_zod8.z.string(),
|
|
12457
|
+
options: import_zod8.z.array(import_zod8.z.string()).optional(),
|
|
12458
|
+
default: import_zod8.z.string().optional()
|
|
11099
12459
|
});
|
|
11100
|
-
var ConfirmationSchema =
|
|
11101
|
-
text:
|
|
11102
|
-
context:
|
|
12460
|
+
var ConfirmationSchema = import_zod8.z.object({
|
|
12461
|
+
text: import_zod8.z.string(),
|
|
12462
|
+
context: import_zod8.z.string()
|
|
11103
12463
|
});
|
|
11104
|
-
var TransitionSchema =
|
|
11105
|
-
completedPhase:
|
|
11106
|
-
suggestedNext:
|
|
11107
|
-
reason:
|
|
11108
|
-
artifacts:
|
|
11109
|
-
requiresConfirmation:
|
|
11110
|
-
summary:
|
|
12464
|
+
var TransitionSchema = import_zod8.z.object({
|
|
12465
|
+
completedPhase: import_zod8.z.string(),
|
|
12466
|
+
suggestedNext: import_zod8.z.string(),
|
|
12467
|
+
reason: import_zod8.z.string(),
|
|
12468
|
+
artifacts: import_zod8.z.array(import_zod8.z.string()),
|
|
12469
|
+
requiresConfirmation: import_zod8.z.boolean(),
|
|
12470
|
+
summary: import_zod8.z.string()
|
|
11111
12471
|
});
|
|
11112
|
-
var EmitInteractionInputSchema =
|
|
11113
|
-
path:
|
|
12472
|
+
var EmitInteractionInputSchema = import_zod8.z.object({
|
|
12473
|
+
path: import_zod8.z.string(),
|
|
11114
12474
|
type: InteractionTypeSchema,
|
|
11115
|
-
stream:
|
|
12475
|
+
stream: import_zod8.z.string().optional(),
|
|
11116
12476
|
question: QuestionSchema.optional(),
|
|
11117
12477
|
confirmation: ConfirmationSchema.optional(),
|
|
11118
12478
|
transition: TransitionSchema.optional()
|
|
11119
12479
|
});
|
|
11120
12480
|
|
|
11121
12481
|
// src/blueprint/scanner.ts
|
|
11122
|
-
var
|
|
11123
|
-
var
|
|
12482
|
+
var fs20 = __toESM(require("fs/promises"));
|
|
12483
|
+
var path20 = __toESM(require("path"));
|
|
11124
12484
|
var ProjectScanner = class {
|
|
11125
12485
|
constructor(rootDir) {
|
|
11126
12486
|
this.rootDir = rootDir;
|
|
11127
12487
|
}
|
|
11128
12488
|
async scan() {
|
|
11129
|
-
let projectName =
|
|
12489
|
+
let projectName = path20.basename(this.rootDir);
|
|
11130
12490
|
try {
|
|
11131
|
-
const pkgPath =
|
|
11132
|
-
const pkgRaw = await
|
|
12491
|
+
const pkgPath = path20.join(this.rootDir, "package.json");
|
|
12492
|
+
const pkgRaw = await fs20.readFile(pkgPath, "utf-8");
|
|
11133
12493
|
const pkg = JSON.parse(pkgRaw);
|
|
11134
12494
|
if (pkg.name) projectName = pkg.name;
|
|
11135
12495
|
} catch {
|
|
@@ -11170,8 +12530,8 @@ var ProjectScanner = class {
|
|
|
11170
12530
|
};
|
|
11171
12531
|
|
|
11172
12532
|
// src/blueprint/generator.ts
|
|
11173
|
-
var
|
|
11174
|
-
var
|
|
12533
|
+
var fs21 = __toESM(require("fs/promises"));
|
|
12534
|
+
var path21 = __toESM(require("path"));
|
|
11175
12535
|
var ejs = __toESM(require("ejs"));
|
|
11176
12536
|
|
|
11177
12537
|
// src/blueprint/templates.ts
|
|
@@ -11255,19 +12615,19 @@ var BlueprintGenerator = class {
|
|
|
11255
12615
|
styles: STYLES,
|
|
11256
12616
|
scripts: SCRIPTS
|
|
11257
12617
|
});
|
|
11258
|
-
await
|
|
11259
|
-
await
|
|
12618
|
+
await fs21.mkdir(options.outputDir, { recursive: true });
|
|
12619
|
+
await fs21.writeFile(path21.join(options.outputDir, "index.html"), html);
|
|
11260
12620
|
}
|
|
11261
12621
|
};
|
|
11262
12622
|
|
|
11263
12623
|
// src/update-checker.ts
|
|
11264
|
-
var
|
|
11265
|
-
var
|
|
12624
|
+
var fs22 = __toESM(require("fs"));
|
|
12625
|
+
var path22 = __toESM(require("path"));
|
|
11266
12626
|
var os = __toESM(require("os"));
|
|
11267
12627
|
var import_child_process3 = require("child_process");
|
|
11268
12628
|
function getStatePath() {
|
|
11269
12629
|
const home = process.env["HOME"] || os.homedir();
|
|
11270
|
-
return
|
|
12630
|
+
return path22.join(home, ".harness", "update-check.json");
|
|
11271
12631
|
}
|
|
11272
12632
|
function isUpdateCheckEnabled(configInterval) {
|
|
11273
12633
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -11280,7 +12640,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
11280
12640
|
}
|
|
11281
12641
|
function readCheckState() {
|
|
11282
12642
|
try {
|
|
11283
|
-
const raw =
|
|
12643
|
+
const raw = fs22.readFileSync(getStatePath(), "utf-8");
|
|
11284
12644
|
const parsed = JSON.parse(raw);
|
|
11285
12645
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
11286
12646
|
const state = parsed;
|
|
@@ -11297,7 +12657,7 @@ function readCheckState() {
|
|
|
11297
12657
|
}
|
|
11298
12658
|
function spawnBackgroundCheck(currentVersion) {
|
|
11299
12659
|
const statePath = getStatePath();
|
|
11300
|
-
const stateDir =
|
|
12660
|
+
const stateDir = path22.dirname(statePath);
|
|
11301
12661
|
const script = `
|
|
11302
12662
|
const { execSync } = require('child_process');
|
|
11303
12663
|
const fs = require('fs');
|
|
@@ -11350,8 +12710,894 @@ function getUpdateNotification(currentVersion) {
|
|
|
11350
12710
|
Run "harness update" to upgrade.`;
|
|
11351
12711
|
}
|
|
11352
12712
|
|
|
12713
|
+
// src/code-nav/types.ts
|
|
12714
|
+
var EXTENSION_MAP = {
|
|
12715
|
+
".ts": "typescript",
|
|
12716
|
+
".tsx": "typescript",
|
|
12717
|
+
".mts": "typescript",
|
|
12718
|
+
".cts": "typescript",
|
|
12719
|
+
".js": "javascript",
|
|
12720
|
+
".jsx": "javascript",
|
|
12721
|
+
".mjs": "javascript",
|
|
12722
|
+
".cjs": "javascript",
|
|
12723
|
+
".py": "python"
|
|
12724
|
+
};
|
|
12725
|
+
function detectLanguage(filePath) {
|
|
12726
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
12727
|
+
return EXTENSION_MAP[ext] ?? null;
|
|
12728
|
+
}
|
|
12729
|
+
|
|
12730
|
+
// src/code-nav/parser.ts
|
|
12731
|
+
var import_web_tree_sitter = __toESM(require("web-tree-sitter"));
|
|
12732
|
+
var import_meta = {};
|
|
12733
|
+
var parserCache = /* @__PURE__ */ new Map();
|
|
12734
|
+
var initialized = false;
|
|
12735
|
+
var GRAMMAR_MAP = {
|
|
12736
|
+
typescript: "tree-sitter-typescript",
|
|
12737
|
+
javascript: "tree-sitter-javascript",
|
|
12738
|
+
python: "tree-sitter-python"
|
|
12739
|
+
};
|
|
12740
|
+
async function ensureInit() {
|
|
12741
|
+
if (!initialized) {
|
|
12742
|
+
await import_web_tree_sitter.default.init();
|
|
12743
|
+
initialized = true;
|
|
12744
|
+
}
|
|
12745
|
+
}
|
|
12746
|
+
async function resolveWasmPath(grammarName) {
|
|
12747
|
+
const { createRequire } = await import("module");
|
|
12748
|
+
const require2 = createRequire(import_meta.url ?? __filename);
|
|
12749
|
+
const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
|
|
12750
|
+
const path26 = await import("path");
|
|
12751
|
+
const pkgDir = path26.dirname(pkgPath);
|
|
12752
|
+
return path26.join(pkgDir, "out", `${grammarName}.wasm`);
|
|
12753
|
+
}
|
|
12754
|
+
async function loadLanguage(lang) {
|
|
12755
|
+
const grammarName = GRAMMAR_MAP[lang];
|
|
12756
|
+
const wasmPath = await resolveWasmPath(grammarName);
|
|
12757
|
+
return import_web_tree_sitter.default.Language.load(wasmPath);
|
|
12758
|
+
}
|
|
12759
|
+
async function getParser(lang) {
|
|
12760
|
+
const cached = parserCache.get(lang);
|
|
12761
|
+
if (cached) return cached;
|
|
12762
|
+
await ensureInit();
|
|
12763
|
+
const parser = new import_web_tree_sitter.default();
|
|
12764
|
+
const language = await loadLanguage(lang);
|
|
12765
|
+
parser.setLanguage(language);
|
|
12766
|
+
parserCache.set(lang, parser);
|
|
12767
|
+
return parser;
|
|
12768
|
+
}
|
|
12769
|
+
async function parseFile(filePath) {
|
|
12770
|
+
const lang = detectLanguage(filePath);
|
|
12771
|
+
if (!lang) {
|
|
12772
|
+
return (0, import_types.Err)({
|
|
12773
|
+
code: "UNSUPPORTED_LANGUAGE",
|
|
12774
|
+
message: `Unsupported file extension: ${filePath}`
|
|
12775
|
+
});
|
|
12776
|
+
}
|
|
12777
|
+
const contentResult = await readFileContent(filePath);
|
|
12778
|
+
if (!contentResult.ok) {
|
|
12779
|
+
return (0, import_types.Err)({
|
|
12780
|
+
code: "FILE_NOT_FOUND",
|
|
12781
|
+
message: `Cannot read file: ${filePath}`
|
|
12782
|
+
});
|
|
12783
|
+
}
|
|
12784
|
+
try {
|
|
12785
|
+
const parser = await getParser(lang);
|
|
12786
|
+
const tree = parser.parse(contentResult.value);
|
|
12787
|
+
return (0, import_types.Ok)({ tree, language: lang, source: contentResult.value, filePath });
|
|
12788
|
+
} catch (e) {
|
|
12789
|
+
return (0, import_types.Err)({
|
|
12790
|
+
code: "PARSE_FAILED",
|
|
12791
|
+
message: `Tree-sitter parse failed for ${filePath}: ${e.message}`
|
|
12792
|
+
});
|
|
12793
|
+
}
|
|
12794
|
+
}
|
|
12795
|
+
function resetParserCache() {
|
|
12796
|
+
parserCache.clear();
|
|
12797
|
+
initialized = false;
|
|
12798
|
+
}
|
|
12799
|
+
|
|
12800
|
+
// src/code-nav/outline.ts
|
|
12801
|
+
var TOP_LEVEL_TYPES = {
|
|
12802
|
+
typescript: {
|
|
12803
|
+
function_declaration: "function",
|
|
12804
|
+
class_declaration: "class",
|
|
12805
|
+
interface_declaration: "interface",
|
|
12806
|
+
type_alias_declaration: "type",
|
|
12807
|
+
lexical_declaration: "variable",
|
|
12808
|
+
variable_declaration: "variable",
|
|
12809
|
+
export_statement: "export",
|
|
12810
|
+
import_statement: "import",
|
|
12811
|
+
enum_declaration: "type"
|
|
12812
|
+
},
|
|
12813
|
+
javascript: {
|
|
12814
|
+
function_declaration: "function",
|
|
12815
|
+
class_declaration: "class",
|
|
12816
|
+
lexical_declaration: "variable",
|
|
12817
|
+
variable_declaration: "variable",
|
|
12818
|
+
export_statement: "export",
|
|
12819
|
+
import_statement: "import"
|
|
12820
|
+
},
|
|
12821
|
+
python: {
|
|
12822
|
+
function_definition: "function",
|
|
12823
|
+
class_definition: "class",
|
|
12824
|
+
assignment: "variable",
|
|
12825
|
+
import_statement: "import",
|
|
12826
|
+
import_from_statement: "import"
|
|
12827
|
+
}
|
|
12828
|
+
};
|
|
12829
|
+
var METHOD_TYPES = {
|
|
12830
|
+
typescript: ["method_definition", "public_field_definition"],
|
|
12831
|
+
javascript: ["method_definition"],
|
|
12832
|
+
python: ["function_definition"]
|
|
12833
|
+
};
|
|
12834
|
+
var IDENTIFIER_TYPES = /* @__PURE__ */ new Set(["identifier", "property_identifier", "type_identifier"]);
|
|
12835
|
+
function findIdentifier(node) {
|
|
12836
|
+
return node.childForFieldName("name") ?? node.children.find((c) => IDENTIFIER_TYPES.has(c.type)) ?? null;
|
|
12837
|
+
}
|
|
12838
|
+
function getVariableDeclarationName(node) {
|
|
12839
|
+
const declarator = node.children.find((c) => c.type === "variable_declarator");
|
|
12840
|
+
if (!declarator) return null;
|
|
12841
|
+
const id = findIdentifier(declarator);
|
|
12842
|
+
return id?.text ?? null;
|
|
12843
|
+
}
|
|
12844
|
+
function getExportName(node, source) {
|
|
12845
|
+
const decl = node.children.find(
|
|
12846
|
+
(c) => c.type !== "export" && c.type !== "default" && c.type !== "comment"
|
|
12847
|
+
);
|
|
12848
|
+
return decl ? getNodeName(decl, source) : "<anonymous>";
|
|
12849
|
+
}
|
|
12850
|
+
function getAssignmentName(node) {
|
|
12851
|
+
const left = node.childForFieldName("left") ?? node.children[0];
|
|
12852
|
+
return left?.text ?? "<anonymous>";
|
|
12853
|
+
}
|
|
12854
|
+
function getNodeName(node, source) {
|
|
12855
|
+
const id = findIdentifier(node);
|
|
12856
|
+
if (id) return id.text;
|
|
12857
|
+
const isVarDecl = node.type === "lexical_declaration" || node.type === "variable_declaration";
|
|
12858
|
+
if (isVarDecl) return getVariableDeclarationName(node) ?? "<anonymous>";
|
|
12859
|
+
if (node.type === "export_statement") return getExportName(node, source);
|
|
12860
|
+
if (node.type === "assignment") return getAssignmentName(node);
|
|
12861
|
+
return "<anonymous>";
|
|
12862
|
+
}
|
|
12863
|
+
function getSignature(node, source) {
|
|
12864
|
+
const startLine = node.startPosition.row;
|
|
12865
|
+
const lines = source.split("\n");
|
|
12866
|
+
return (lines[startLine] ?? "").trim();
|
|
12867
|
+
}
|
|
12868
|
+
function extractMethods(classNode, language, source, filePath) {
|
|
12869
|
+
const methodTypes = METHOD_TYPES[language] ?? [];
|
|
12870
|
+
const body = classNode.childForFieldName("body") ?? classNode.children.find((c) => c.type === "class_body" || c.type === "block");
|
|
12871
|
+
if (!body) return [];
|
|
12872
|
+
return body.children.filter((child) => methodTypes.includes(child.type)).map((child) => ({
|
|
12873
|
+
name: getNodeName(child, source),
|
|
12874
|
+
kind: "method",
|
|
12875
|
+
file: filePath,
|
|
12876
|
+
line: child.startPosition.row + 1,
|
|
12877
|
+
endLine: child.endPosition.row + 1,
|
|
12878
|
+
signature: getSignature(child, source)
|
|
12879
|
+
}));
|
|
12880
|
+
}
|
|
12881
|
+
function nodeToSymbol(node, kind, source, filePath) {
|
|
12882
|
+
return {
|
|
12883
|
+
name: getNodeName(node, source),
|
|
12884
|
+
kind,
|
|
12885
|
+
file: filePath,
|
|
12886
|
+
line: node.startPosition.row + 1,
|
|
12887
|
+
endLine: node.endPosition.row + 1,
|
|
12888
|
+
signature: getSignature(node, source)
|
|
12889
|
+
};
|
|
12890
|
+
}
|
|
12891
|
+
function processExportStatement(child, topLevelTypes, lang, source, filePath) {
|
|
12892
|
+
const declaration = child.children.find(
|
|
12893
|
+
(c) => c.type !== "export" && c.type !== "default" && c.type !== ";" && c.type !== "comment"
|
|
12894
|
+
);
|
|
12895
|
+
const kind = declaration ? topLevelTypes[declaration.type] : void 0;
|
|
12896
|
+
if (declaration && kind) {
|
|
12897
|
+
const sym = nodeToSymbol(child, kind, source, filePath);
|
|
12898
|
+
sym.name = getNodeName(declaration, source);
|
|
12899
|
+
if (kind === "class") {
|
|
12900
|
+
sym.children = extractMethods(declaration, lang, source, filePath);
|
|
12901
|
+
}
|
|
12902
|
+
return sym;
|
|
12903
|
+
}
|
|
12904
|
+
return nodeToSymbol(child, "export", source, filePath);
|
|
12905
|
+
}
|
|
12906
|
+
function extractSymbols(rootNode, lang, source, filePath) {
|
|
12907
|
+
const symbols = [];
|
|
12908
|
+
const topLevelTypes = TOP_LEVEL_TYPES[lang] ?? {};
|
|
12909
|
+
for (const child of rootNode.children) {
|
|
12910
|
+
if (child.type === "export_statement") {
|
|
12911
|
+
symbols.push(processExportStatement(child, topLevelTypes, lang, source, filePath));
|
|
12912
|
+
continue;
|
|
12913
|
+
}
|
|
12914
|
+
const kind = topLevelTypes[child.type];
|
|
12915
|
+
if (!kind || kind === "import") continue;
|
|
12916
|
+
const sym = nodeToSymbol(child, kind, source, filePath);
|
|
12917
|
+
if (kind === "class") {
|
|
12918
|
+
sym.children = extractMethods(child, lang, source, filePath);
|
|
12919
|
+
}
|
|
12920
|
+
symbols.push(sym);
|
|
12921
|
+
}
|
|
12922
|
+
return symbols;
|
|
12923
|
+
}
|
|
12924
|
+
function buildFailedResult(filePath, lang) {
|
|
12925
|
+
return { file: filePath, language: lang, totalLines: 0, symbols: [], error: "[parse-failed]" };
|
|
12926
|
+
}
|
|
12927
|
+
async function getOutline(filePath) {
|
|
12928
|
+
const lang = detectLanguage(filePath);
|
|
12929
|
+
if (!lang) return buildFailedResult(filePath, "unknown");
|
|
12930
|
+
const result = await parseFile(filePath);
|
|
12931
|
+
if (!result.ok) return buildFailedResult(filePath, lang);
|
|
12932
|
+
const { tree, source } = result.value;
|
|
12933
|
+
const totalLines = source.split("\n").length;
|
|
12934
|
+
const symbols = extractSymbols(tree.rootNode, lang, source, filePath);
|
|
12935
|
+
return { file: filePath, language: lang, totalLines, symbols };
|
|
12936
|
+
}
|
|
12937
|
+
function formatOutline(outline) {
|
|
12938
|
+
if (outline.error) {
|
|
12939
|
+
return `${outline.file} ${outline.error}`;
|
|
12940
|
+
}
|
|
12941
|
+
const lines = [`${outline.file} (${outline.totalLines} lines)`];
|
|
12942
|
+
const last = outline.symbols.length - 1;
|
|
12943
|
+
outline.symbols.forEach((sym, i) => {
|
|
12944
|
+
const prefix = i === last ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
12945
|
+
lines.push(`${prefix} ${sym.signature} :${sym.line}`);
|
|
12946
|
+
if (sym.children) {
|
|
12947
|
+
const childLast = sym.children.length - 1;
|
|
12948
|
+
sym.children.forEach((child, j) => {
|
|
12949
|
+
const childConnector = i === last ? " " : "\u2502 ";
|
|
12950
|
+
const childPrefix = j === childLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
12951
|
+
lines.push(`${childConnector}${childPrefix} ${child.signature} :${child.line}`);
|
|
12952
|
+
});
|
|
12953
|
+
}
|
|
12954
|
+
});
|
|
12955
|
+
return lines.join("\n");
|
|
12956
|
+
}
|
|
12957
|
+
|
|
12958
|
+
// src/code-nav/search.ts
|
|
12959
|
+
function buildGlob(directory, fileGlob) {
|
|
12960
|
+
const dir = directory.replaceAll("\\", "/");
|
|
12961
|
+
if (fileGlob) {
|
|
12962
|
+
return `${dir}/**/${fileGlob}`;
|
|
12963
|
+
}
|
|
12964
|
+
const exts = Object.keys(EXTENSION_MAP).map((e) => e.slice(1));
|
|
12965
|
+
return `${dir}/**/*.{${exts.join(",")}}`;
|
|
12966
|
+
}
|
|
12967
|
+
function matchesQuery(name, query) {
|
|
12968
|
+
return name.toLowerCase().includes(query.toLowerCase());
|
|
12969
|
+
}
|
|
12970
|
+
function flattenSymbols(symbols) {
|
|
12971
|
+
const flat = [];
|
|
12972
|
+
for (const sym of symbols) {
|
|
12973
|
+
flat.push(sym);
|
|
12974
|
+
if (sym.children) {
|
|
12975
|
+
flat.push(...sym.children);
|
|
12976
|
+
}
|
|
12977
|
+
}
|
|
12978
|
+
return flat;
|
|
12979
|
+
}
|
|
12980
|
+
async function searchSymbols(query, directory, fileGlob) {
|
|
12981
|
+
const pattern = buildGlob(directory, fileGlob);
|
|
12982
|
+
let files;
|
|
12983
|
+
try {
|
|
12984
|
+
files = await findFiles(pattern, directory);
|
|
12985
|
+
} catch {
|
|
12986
|
+
files = [];
|
|
12987
|
+
}
|
|
12988
|
+
const matches = [];
|
|
12989
|
+
const skipped = [];
|
|
12990
|
+
for (const file of files) {
|
|
12991
|
+
const lang = detectLanguage(file);
|
|
12992
|
+
if (!lang) {
|
|
12993
|
+
skipped.push(file);
|
|
12994
|
+
continue;
|
|
12995
|
+
}
|
|
12996
|
+
const outline = await getOutline(file);
|
|
12997
|
+
if (outline.error) {
|
|
12998
|
+
skipped.push(file);
|
|
12999
|
+
continue;
|
|
13000
|
+
}
|
|
13001
|
+
const allSymbols = flattenSymbols(outline.symbols);
|
|
13002
|
+
for (const sym of allSymbols) {
|
|
13003
|
+
if (matchesQuery(sym.name, query)) {
|
|
13004
|
+
matches.push({
|
|
13005
|
+
symbol: sym,
|
|
13006
|
+
context: sym.signature
|
|
13007
|
+
});
|
|
13008
|
+
}
|
|
13009
|
+
}
|
|
13010
|
+
}
|
|
13011
|
+
return { query, matches, skipped };
|
|
13012
|
+
}
|
|
13013
|
+
|
|
13014
|
+
// src/code-nav/unfold.ts
|
|
13015
|
+
function findSymbolInList(symbols, name) {
|
|
13016
|
+
for (const sym of symbols) {
|
|
13017
|
+
if (sym.name === name) return sym;
|
|
13018
|
+
if (sym.children) {
|
|
13019
|
+
const found = findSymbolInList(sym.children, name);
|
|
13020
|
+
if (found) return found;
|
|
13021
|
+
}
|
|
13022
|
+
}
|
|
13023
|
+
return null;
|
|
13024
|
+
}
|
|
13025
|
+
function extractLines(source, startLine, endLine) {
|
|
13026
|
+
const lines = source.split("\n");
|
|
13027
|
+
const start = Math.max(0, startLine - 1);
|
|
13028
|
+
const end = Math.min(lines.length, endLine);
|
|
13029
|
+
return lines.slice(start, end).join("\n");
|
|
13030
|
+
}
|
|
13031
|
+
function buildFallbackResult(filePath, symbolName, content, language) {
|
|
13032
|
+
const totalLines = content ? content.split("\n").length : 0;
|
|
13033
|
+
return {
|
|
13034
|
+
file: filePath,
|
|
13035
|
+
symbolName,
|
|
13036
|
+
startLine: content ? 1 : 0,
|
|
13037
|
+
endLine: totalLines,
|
|
13038
|
+
content,
|
|
13039
|
+
language,
|
|
13040
|
+
fallback: true,
|
|
13041
|
+
warning: "[fallback: raw content]"
|
|
13042
|
+
};
|
|
13043
|
+
}
|
|
13044
|
+
async function readContentSafe(filePath) {
|
|
13045
|
+
const result = await readFileContent(filePath);
|
|
13046
|
+
return result.ok ? result.value : "";
|
|
13047
|
+
}
|
|
13048
|
+
async function unfoldSymbol(filePath, symbolName) {
|
|
13049
|
+
const lang = detectLanguage(filePath);
|
|
13050
|
+
if (!lang) {
|
|
13051
|
+
const content2 = await readContentSafe(filePath);
|
|
13052
|
+
return buildFallbackResult(filePath, symbolName, content2, "unknown");
|
|
13053
|
+
}
|
|
13054
|
+
const outline = await getOutline(filePath);
|
|
13055
|
+
if (outline.error) {
|
|
13056
|
+
const content2 = await readContentSafe(filePath);
|
|
13057
|
+
return buildFallbackResult(filePath, symbolName, content2, lang);
|
|
13058
|
+
}
|
|
13059
|
+
const symbol = findSymbolInList(outline.symbols, symbolName);
|
|
13060
|
+
if (!symbol) {
|
|
13061
|
+
const content2 = await readContentSafe(filePath);
|
|
13062
|
+
return buildFallbackResult(filePath, symbolName, content2, lang);
|
|
13063
|
+
}
|
|
13064
|
+
const parseResult = await parseFile(filePath);
|
|
13065
|
+
if (!parseResult.ok) {
|
|
13066
|
+
const content2 = await readContentSafe(filePath);
|
|
13067
|
+
return {
|
|
13068
|
+
...buildFallbackResult(
|
|
13069
|
+
filePath,
|
|
13070
|
+
symbolName,
|
|
13071
|
+
extractLines(content2, symbol.line, symbol.endLine),
|
|
13072
|
+
lang
|
|
13073
|
+
),
|
|
13074
|
+
startLine: symbol.line,
|
|
13075
|
+
endLine: symbol.endLine
|
|
13076
|
+
};
|
|
13077
|
+
}
|
|
13078
|
+
const content = extractLines(parseResult.value.source, symbol.line, symbol.endLine);
|
|
13079
|
+
return {
|
|
13080
|
+
file: filePath,
|
|
13081
|
+
symbolName,
|
|
13082
|
+
startLine: symbol.line,
|
|
13083
|
+
endLine: symbol.endLine,
|
|
13084
|
+
content,
|
|
13085
|
+
language: lang,
|
|
13086
|
+
fallback: false
|
|
13087
|
+
};
|
|
13088
|
+
}
|
|
13089
|
+
async function unfoldRange(filePath, startLine, endLine) {
|
|
13090
|
+
const lang = detectLanguage(filePath) ?? "unknown";
|
|
13091
|
+
const contentResult = await readFileContent(filePath);
|
|
13092
|
+
if (!contentResult.ok) {
|
|
13093
|
+
return {
|
|
13094
|
+
file: filePath,
|
|
13095
|
+
startLine: 0,
|
|
13096
|
+
endLine: 0,
|
|
13097
|
+
content: "",
|
|
13098
|
+
language: lang,
|
|
13099
|
+
fallback: true,
|
|
13100
|
+
warning: "[fallback: raw content]"
|
|
13101
|
+
};
|
|
13102
|
+
}
|
|
13103
|
+
const totalLines = contentResult.value.split("\n").length;
|
|
13104
|
+
const clampedEnd = Math.min(endLine, totalLines);
|
|
13105
|
+
const content = extractLines(contentResult.value, startLine, clampedEnd);
|
|
13106
|
+
return {
|
|
13107
|
+
file: filePath,
|
|
13108
|
+
startLine,
|
|
13109
|
+
endLine: clampedEnd,
|
|
13110
|
+
content,
|
|
13111
|
+
language: lang,
|
|
13112
|
+
fallback: false
|
|
13113
|
+
};
|
|
13114
|
+
}
|
|
13115
|
+
|
|
13116
|
+
// src/pricing/pricing.ts
|
|
13117
|
+
var TOKENS_PER_MILLION = 1e6;
|
|
13118
|
+
function parseLiteLLMData(raw) {
|
|
13119
|
+
const dataset = /* @__PURE__ */ new Map();
|
|
13120
|
+
for (const [modelName, entry] of Object.entries(raw)) {
|
|
13121
|
+
if (modelName === "sample_spec") continue;
|
|
13122
|
+
if (entry.mode && entry.mode !== "chat") continue;
|
|
13123
|
+
const inputCost = entry.input_cost_per_token;
|
|
13124
|
+
const outputCost = entry.output_cost_per_token;
|
|
13125
|
+
if (inputCost == null || outputCost == null) continue;
|
|
13126
|
+
const pricing = {
|
|
13127
|
+
inputPer1M: inputCost * TOKENS_PER_MILLION,
|
|
13128
|
+
outputPer1M: outputCost * TOKENS_PER_MILLION
|
|
13129
|
+
};
|
|
13130
|
+
if (entry.cache_read_input_token_cost != null) {
|
|
13131
|
+
pricing.cacheReadPer1M = entry.cache_read_input_token_cost * TOKENS_PER_MILLION;
|
|
13132
|
+
}
|
|
13133
|
+
if (entry.cache_creation_input_token_cost != null) {
|
|
13134
|
+
pricing.cacheWritePer1M = entry.cache_creation_input_token_cost * TOKENS_PER_MILLION;
|
|
13135
|
+
}
|
|
13136
|
+
dataset.set(modelName, pricing);
|
|
13137
|
+
}
|
|
13138
|
+
return dataset;
|
|
13139
|
+
}
|
|
13140
|
+
function getModelPrice(model, dataset) {
|
|
13141
|
+
if (!model) {
|
|
13142
|
+
console.warn("[harness pricing] No model specified \u2014 cannot look up pricing.");
|
|
13143
|
+
return null;
|
|
13144
|
+
}
|
|
13145
|
+
const pricing = dataset.get(model);
|
|
13146
|
+
if (!pricing) {
|
|
13147
|
+
console.warn(
|
|
13148
|
+
`[harness pricing] No pricing data for model "${model}". Consider updating pricing data.`
|
|
13149
|
+
);
|
|
13150
|
+
return null;
|
|
13151
|
+
}
|
|
13152
|
+
return pricing;
|
|
13153
|
+
}
|
|
13154
|
+
|
|
13155
|
+
// src/pricing/cache.ts
|
|
13156
|
+
var fs23 = __toESM(require("fs/promises"));
|
|
13157
|
+
var path23 = __toESM(require("path"));
|
|
13158
|
+
|
|
13159
|
+
// src/pricing/fallback.json
|
|
13160
|
+
var fallback_default = {
|
|
13161
|
+
_generatedAt: "2026-03-31",
|
|
13162
|
+
_source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
13163
|
+
models: {
|
|
13164
|
+
"claude-opus-4-20250514": {
|
|
13165
|
+
inputPer1M: 15,
|
|
13166
|
+
outputPer1M: 75,
|
|
13167
|
+
cacheReadPer1M: 1.5,
|
|
13168
|
+
cacheWritePer1M: 18.75
|
|
13169
|
+
},
|
|
13170
|
+
"claude-sonnet-4-20250514": {
|
|
13171
|
+
inputPer1M: 3,
|
|
13172
|
+
outputPer1M: 15,
|
|
13173
|
+
cacheReadPer1M: 0.3,
|
|
13174
|
+
cacheWritePer1M: 3.75
|
|
13175
|
+
},
|
|
13176
|
+
"claude-3-5-haiku-20241022": {
|
|
13177
|
+
inputPer1M: 0.8,
|
|
13178
|
+
outputPer1M: 4,
|
|
13179
|
+
cacheReadPer1M: 0.08,
|
|
13180
|
+
cacheWritePer1M: 1
|
|
13181
|
+
},
|
|
13182
|
+
"gpt-4o": {
|
|
13183
|
+
inputPer1M: 2.5,
|
|
13184
|
+
outputPer1M: 10,
|
|
13185
|
+
cacheReadPer1M: 1.25
|
|
13186
|
+
},
|
|
13187
|
+
"gpt-4o-mini": {
|
|
13188
|
+
inputPer1M: 0.15,
|
|
13189
|
+
outputPer1M: 0.6,
|
|
13190
|
+
cacheReadPer1M: 0.075
|
|
13191
|
+
},
|
|
13192
|
+
"gemini-2.0-flash": {
|
|
13193
|
+
inputPer1M: 0.1,
|
|
13194
|
+
outputPer1M: 0.4,
|
|
13195
|
+
cacheReadPer1M: 0.025
|
|
13196
|
+
},
|
|
13197
|
+
"gemini-2.5-pro": {
|
|
13198
|
+
inputPer1M: 1.25,
|
|
13199
|
+
outputPer1M: 10,
|
|
13200
|
+
cacheReadPer1M: 0.3125
|
|
13201
|
+
}
|
|
13202
|
+
}
|
|
13203
|
+
};
|
|
13204
|
+
|
|
13205
|
+
// src/pricing/cache.ts
|
|
13206
|
+
var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
13207
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13208
|
+
var STALENESS_WARNING_DAYS = 7;
|
|
13209
|
+
function getCachePath(projectRoot) {
|
|
13210
|
+
return path23.join(projectRoot, ".harness", "cache", "pricing.json");
|
|
13211
|
+
}
|
|
13212
|
+
function getStalenessMarkerPath(projectRoot) {
|
|
13213
|
+
return path23.join(projectRoot, ".harness", "cache", "staleness-marker.json");
|
|
13214
|
+
}
|
|
13215
|
+
async function readDiskCache(projectRoot) {
|
|
13216
|
+
try {
|
|
13217
|
+
const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
|
|
13218
|
+
return JSON.parse(raw);
|
|
13219
|
+
} catch {
|
|
13220
|
+
return null;
|
|
13221
|
+
}
|
|
13222
|
+
}
|
|
13223
|
+
async function writeDiskCache(projectRoot, data) {
|
|
13224
|
+
const cachePath = getCachePath(projectRoot);
|
|
13225
|
+
await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
|
|
13226
|
+
await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
|
|
13227
|
+
}
|
|
13228
|
+
async function fetchFromNetwork() {
|
|
13229
|
+
try {
|
|
13230
|
+
const response = await fetch(LITELLM_PRICING_URL);
|
|
13231
|
+
if (!response.ok) return null;
|
|
13232
|
+
const data = await response.json();
|
|
13233
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
|
|
13234
|
+
return {
|
|
13235
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13236
|
+
data
|
|
13237
|
+
};
|
|
13238
|
+
} catch {
|
|
13239
|
+
return null;
|
|
13240
|
+
}
|
|
13241
|
+
}
|
|
13242
|
+
function loadFallbackDataset() {
|
|
13243
|
+
const fb = fallback_default;
|
|
13244
|
+
const dataset = /* @__PURE__ */ new Map();
|
|
13245
|
+
for (const [model, pricing] of Object.entries(fb.models)) {
|
|
13246
|
+
dataset.set(model, pricing);
|
|
13247
|
+
}
|
|
13248
|
+
return dataset;
|
|
13249
|
+
}
|
|
13250
|
+
async function checkAndWarnStaleness(projectRoot) {
|
|
13251
|
+
const markerPath = getStalenessMarkerPath(projectRoot);
|
|
13252
|
+
try {
|
|
13253
|
+
const raw = await fs23.readFile(markerPath, "utf-8");
|
|
13254
|
+
const marker = JSON.parse(raw);
|
|
13255
|
+
const firstUse = new Date(marker.firstFallbackUse).getTime();
|
|
13256
|
+
const now = Date.now();
|
|
13257
|
+
const daysSinceFirstUse = (now - firstUse) / (24 * 60 * 60 * 1e3);
|
|
13258
|
+
if (daysSinceFirstUse > STALENESS_WARNING_DAYS) {
|
|
13259
|
+
console.warn(
|
|
13260
|
+
`[harness pricing] Pricing data is stale \u2014 using bundled fallback for ${Math.floor(daysSinceFirstUse)} days. Connect to the internet to refresh pricing data.`
|
|
13261
|
+
);
|
|
13262
|
+
}
|
|
13263
|
+
} catch {
|
|
13264
|
+
try {
|
|
13265
|
+
await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
|
|
13266
|
+
await fs23.writeFile(
|
|
13267
|
+
markerPath,
|
|
13268
|
+
JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
|
|
13269
|
+
);
|
|
13270
|
+
} catch {
|
|
13271
|
+
}
|
|
13272
|
+
}
|
|
13273
|
+
}
|
|
13274
|
+
async function clearStalenessMarker(projectRoot) {
|
|
13275
|
+
try {
|
|
13276
|
+
await fs23.unlink(getStalenessMarkerPath(projectRoot));
|
|
13277
|
+
} catch {
|
|
13278
|
+
}
|
|
13279
|
+
}
|
|
13280
|
+
async function loadPricingData(projectRoot) {
|
|
13281
|
+
const cache = await readDiskCache(projectRoot);
|
|
13282
|
+
if (cache) {
|
|
13283
|
+
const cacheAge = Date.now() - new Date(cache.fetchedAt).getTime();
|
|
13284
|
+
if (cacheAge < CACHE_TTL_MS) {
|
|
13285
|
+
await clearStalenessMarker(projectRoot);
|
|
13286
|
+
return parseLiteLLMData(cache.data);
|
|
13287
|
+
}
|
|
13288
|
+
}
|
|
13289
|
+
const fetched = await fetchFromNetwork();
|
|
13290
|
+
if (fetched) {
|
|
13291
|
+
await writeDiskCache(projectRoot, fetched);
|
|
13292
|
+
await clearStalenessMarker(projectRoot);
|
|
13293
|
+
return parseLiteLLMData(fetched.data);
|
|
13294
|
+
}
|
|
13295
|
+
if (cache) {
|
|
13296
|
+
return parseLiteLLMData(cache.data);
|
|
13297
|
+
}
|
|
13298
|
+
await checkAndWarnStaleness(projectRoot);
|
|
13299
|
+
return loadFallbackDataset();
|
|
13300
|
+
}
|
|
13301
|
+
|
|
13302
|
+
// src/pricing/calculator.ts
|
|
13303
|
+
var MICRODOLLARS_PER_DOLLAR = 1e6;
|
|
13304
|
+
var TOKENS_PER_MILLION2 = 1e6;
|
|
13305
|
+
function calculateCost(record, dataset) {
|
|
13306
|
+
if (!record.model) return null;
|
|
13307
|
+
const pricing = getModelPrice(record.model, dataset);
|
|
13308
|
+
if (!pricing) return null;
|
|
13309
|
+
let costUSD = 0;
|
|
13310
|
+
costUSD += record.tokens.inputTokens / TOKENS_PER_MILLION2 * pricing.inputPer1M;
|
|
13311
|
+
costUSD += record.tokens.outputTokens / TOKENS_PER_MILLION2 * pricing.outputPer1M;
|
|
13312
|
+
if (record.cacheReadTokens != null && pricing.cacheReadPer1M != null) {
|
|
13313
|
+
costUSD += record.cacheReadTokens / TOKENS_PER_MILLION2 * pricing.cacheReadPer1M;
|
|
13314
|
+
}
|
|
13315
|
+
if (record.cacheCreationTokens != null && pricing.cacheWritePer1M != null) {
|
|
13316
|
+
costUSD += record.cacheCreationTokens / TOKENS_PER_MILLION2 * pricing.cacheWritePer1M;
|
|
13317
|
+
}
|
|
13318
|
+
return Math.round(costUSD * MICRODOLLARS_PER_DOLLAR);
|
|
13319
|
+
}
|
|
13320
|
+
|
|
13321
|
+
// src/usage/aggregator.ts
|
|
13322
|
+
function aggregateBySession(records) {
|
|
13323
|
+
if (records.length === 0) return [];
|
|
13324
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
13325
|
+
for (const record of records) {
|
|
13326
|
+
const tagged = record;
|
|
13327
|
+
const id = record.sessionId;
|
|
13328
|
+
if (!sessionMap.has(id)) {
|
|
13329
|
+
sessionMap.set(id, { harnessRecords: [], ccRecords: [], allRecords: [] });
|
|
13330
|
+
}
|
|
13331
|
+
const bucket = sessionMap.get(id);
|
|
13332
|
+
if (tagged._source === "claude-code") {
|
|
13333
|
+
bucket.ccRecords.push(tagged);
|
|
13334
|
+
} else {
|
|
13335
|
+
bucket.harnessRecords.push(tagged);
|
|
13336
|
+
}
|
|
13337
|
+
bucket.allRecords.push(tagged);
|
|
13338
|
+
}
|
|
13339
|
+
const results = [];
|
|
13340
|
+
for (const [sessionId, bucket] of sessionMap) {
|
|
13341
|
+
const hasHarness = bucket.harnessRecords.length > 0;
|
|
13342
|
+
const hasCC = bucket.ccRecords.length > 0;
|
|
13343
|
+
const isMerged = hasHarness && hasCC;
|
|
13344
|
+
const tokenSource = hasHarness ? bucket.harnessRecords : bucket.ccRecords;
|
|
13345
|
+
const tokens = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
13346
|
+
let cacheCreation;
|
|
13347
|
+
let cacheRead;
|
|
13348
|
+
let costMicroUSD = 0;
|
|
13349
|
+
let model;
|
|
13350
|
+
for (const r of tokenSource) {
|
|
13351
|
+
tokens.inputTokens += r.tokens.inputTokens;
|
|
13352
|
+
tokens.outputTokens += r.tokens.outputTokens;
|
|
13353
|
+
tokens.totalTokens += r.tokens.totalTokens;
|
|
13354
|
+
if (r.cacheCreationTokens != null) {
|
|
13355
|
+
cacheCreation = (cacheCreation ?? 0) + r.cacheCreationTokens;
|
|
13356
|
+
}
|
|
13357
|
+
if (r.cacheReadTokens != null) {
|
|
13358
|
+
cacheRead = (cacheRead ?? 0) + r.cacheReadTokens;
|
|
13359
|
+
}
|
|
13360
|
+
if (r.costMicroUSD != null && costMicroUSD != null) {
|
|
13361
|
+
costMicroUSD += r.costMicroUSD;
|
|
13362
|
+
} else if (r.costMicroUSD == null) {
|
|
13363
|
+
costMicroUSD = null;
|
|
13364
|
+
}
|
|
13365
|
+
if (!model && r.model) {
|
|
13366
|
+
model = r.model;
|
|
13367
|
+
}
|
|
13368
|
+
}
|
|
13369
|
+
if (!model && hasCC) {
|
|
13370
|
+
for (const r of bucket.ccRecords) {
|
|
13371
|
+
if (r.model) {
|
|
13372
|
+
model = r.model;
|
|
13373
|
+
break;
|
|
13374
|
+
}
|
|
13375
|
+
}
|
|
13376
|
+
}
|
|
13377
|
+
const timestamps = bucket.allRecords.map((r) => r.timestamp).sort();
|
|
13378
|
+
const source = isMerged ? "merged" : hasCC ? "claude-code" : "harness";
|
|
13379
|
+
const session = {
|
|
13380
|
+
sessionId,
|
|
13381
|
+
firstTimestamp: timestamps[0] ?? "",
|
|
13382
|
+
lastTimestamp: timestamps[timestamps.length - 1] ?? "",
|
|
13383
|
+
tokens,
|
|
13384
|
+
costMicroUSD,
|
|
13385
|
+
source
|
|
13386
|
+
};
|
|
13387
|
+
if (model) session.model = model;
|
|
13388
|
+
if (cacheCreation != null) session.cacheCreationTokens = cacheCreation;
|
|
13389
|
+
if (cacheRead != null) session.cacheReadTokens = cacheRead;
|
|
13390
|
+
results.push(session);
|
|
13391
|
+
}
|
|
13392
|
+
results.sort((a, b) => b.firstTimestamp.localeCompare(a.firstTimestamp));
|
|
13393
|
+
return results;
|
|
13394
|
+
}
|
|
13395
|
+
function aggregateByDay(records) {
|
|
13396
|
+
if (records.length === 0) return [];
|
|
13397
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
13398
|
+
for (const record of records) {
|
|
13399
|
+
const date = record.timestamp.slice(0, 10);
|
|
13400
|
+
if (!dayMap.has(date)) {
|
|
13401
|
+
dayMap.set(date, {
|
|
13402
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
13403
|
+
tokens: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
13404
|
+
costMicroUSD: 0,
|
|
13405
|
+
models: /* @__PURE__ */ new Set()
|
|
13406
|
+
});
|
|
13407
|
+
}
|
|
13408
|
+
const day = dayMap.get(date);
|
|
13409
|
+
day.sessions.add(record.sessionId);
|
|
13410
|
+
day.tokens.inputTokens += record.tokens.inputTokens;
|
|
13411
|
+
day.tokens.outputTokens += record.tokens.outputTokens;
|
|
13412
|
+
day.tokens.totalTokens += record.tokens.totalTokens;
|
|
13413
|
+
if (record.cacheCreationTokens != null) {
|
|
13414
|
+
day.cacheCreation = (day.cacheCreation ?? 0) + record.cacheCreationTokens;
|
|
13415
|
+
}
|
|
13416
|
+
if (record.cacheReadTokens != null) {
|
|
13417
|
+
day.cacheRead = (day.cacheRead ?? 0) + record.cacheReadTokens;
|
|
13418
|
+
}
|
|
13419
|
+
if (record.costMicroUSD != null && day.costMicroUSD != null) {
|
|
13420
|
+
day.costMicroUSD += record.costMicroUSD;
|
|
13421
|
+
} else if (record.costMicroUSD == null) {
|
|
13422
|
+
day.costMicroUSD = null;
|
|
13423
|
+
}
|
|
13424
|
+
if (record.model) {
|
|
13425
|
+
day.models.add(record.model);
|
|
13426
|
+
}
|
|
13427
|
+
}
|
|
13428
|
+
const results = [];
|
|
13429
|
+
for (const [date, day] of dayMap) {
|
|
13430
|
+
const entry = {
|
|
13431
|
+
date,
|
|
13432
|
+
sessionCount: day.sessions.size,
|
|
13433
|
+
tokens: day.tokens,
|
|
13434
|
+
costMicroUSD: day.costMicroUSD,
|
|
13435
|
+
models: Array.from(day.models).sort()
|
|
13436
|
+
};
|
|
13437
|
+
if (day.cacheCreation != null) entry.cacheCreationTokens = day.cacheCreation;
|
|
13438
|
+
if (day.cacheRead != null) entry.cacheReadTokens = day.cacheRead;
|
|
13439
|
+
results.push(entry);
|
|
13440
|
+
}
|
|
13441
|
+
results.sort((a, b) => b.date.localeCompare(a.date));
|
|
13442
|
+
return results;
|
|
13443
|
+
}
|
|
13444
|
+
|
|
13445
|
+
// src/usage/jsonl-reader.ts
|
|
13446
|
+
var fs24 = __toESM(require("fs"));
|
|
13447
|
+
var path24 = __toESM(require("path"));
|
|
13448
|
+
function parseLine(line, lineNumber) {
|
|
13449
|
+
let entry;
|
|
13450
|
+
try {
|
|
13451
|
+
entry = JSON.parse(line);
|
|
13452
|
+
} catch {
|
|
13453
|
+
console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
|
|
13454
|
+
return null;
|
|
13455
|
+
}
|
|
13456
|
+
const tokenUsage = entry.token_usage;
|
|
13457
|
+
if (!tokenUsage || typeof tokenUsage !== "object") {
|
|
13458
|
+
console.warn(
|
|
13459
|
+
`[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
|
|
13460
|
+
);
|
|
13461
|
+
return null;
|
|
13462
|
+
}
|
|
13463
|
+
const inputTokens = tokenUsage.input_tokens ?? 0;
|
|
13464
|
+
const outputTokens = tokenUsage.output_tokens ?? 0;
|
|
13465
|
+
const record = {
|
|
13466
|
+
sessionId: entry.session_id ?? "unknown",
|
|
13467
|
+
timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
13468
|
+
tokens: {
|
|
13469
|
+
inputTokens,
|
|
13470
|
+
outputTokens,
|
|
13471
|
+
totalTokens: inputTokens + outputTokens
|
|
13472
|
+
}
|
|
13473
|
+
};
|
|
13474
|
+
if (entry.cache_creation_tokens != null) {
|
|
13475
|
+
record.cacheCreationTokens = entry.cache_creation_tokens;
|
|
13476
|
+
}
|
|
13477
|
+
if (entry.cache_read_tokens != null) {
|
|
13478
|
+
record.cacheReadTokens = entry.cache_read_tokens;
|
|
13479
|
+
}
|
|
13480
|
+
if (entry.model != null) {
|
|
13481
|
+
record.model = entry.model;
|
|
13482
|
+
}
|
|
13483
|
+
return record;
|
|
13484
|
+
}
|
|
13485
|
+
function readCostRecords(projectRoot) {
|
|
13486
|
+
const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
|
|
13487
|
+
let raw;
|
|
13488
|
+
try {
|
|
13489
|
+
raw = fs24.readFileSync(costsFile, "utf-8");
|
|
13490
|
+
} catch {
|
|
13491
|
+
return [];
|
|
13492
|
+
}
|
|
13493
|
+
const records = [];
|
|
13494
|
+
const lines = raw.split("\n");
|
|
13495
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13496
|
+
const line = lines[i]?.trim();
|
|
13497
|
+
if (!line) continue;
|
|
13498
|
+
const record = parseLine(line, i + 1);
|
|
13499
|
+
if (record) {
|
|
13500
|
+
records.push(record);
|
|
13501
|
+
}
|
|
13502
|
+
}
|
|
13503
|
+
return records;
|
|
13504
|
+
}
|
|
13505
|
+
|
|
13506
|
+
// src/usage/cc-parser.ts
|
|
13507
|
+
var fs25 = __toESM(require("fs"));
|
|
13508
|
+
var path25 = __toESM(require("path"));
|
|
13509
|
+
var os2 = __toESM(require("os"));
|
|
13510
|
+
function extractUsage(entry) {
|
|
13511
|
+
if (entry.type !== "assistant") return null;
|
|
13512
|
+
const message = entry.message;
|
|
13513
|
+
if (!message || typeof message !== "object") return null;
|
|
13514
|
+
const usage = message.usage;
|
|
13515
|
+
return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
|
|
13516
|
+
}
|
|
13517
|
+
function buildRecord(entry, usage) {
|
|
13518
|
+
const inputTokens = Number(usage.input_tokens) || 0;
|
|
13519
|
+
const outputTokens = Number(usage.output_tokens) || 0;
|
|
13520
|
+
const message = entry.message;
|
|
13521
|
+
const record = {
|
|
13522
|
+
sessionId: entry.sessionId ?? "unknown",
|
|
13523
|
+
timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
13524
|
+
tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
13525
|
+
_source: "claude-code"
|
|
13526
|
+
};
|
|
13527
|
+
const model = message.model;
|
|
13528
|
+
if (model) record.model = model;
|
|
13529
|
+
const cacheCreate = usage.cache_creation_input_tokens;
|
|
13530
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
13531
|
+
if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
|
|
13532
|
+
if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
|
|
13533
|
+
return record;
|
|
13534
|
+
}
|
|
13535
|
+
function parseCCLine(line, filePath, lineNumber) {
|
|
13536
|
+
let entry;
|
|
13537
|
+
try {
|
|
13538
|
+
entry = JSON.parse(line);
|
|
13539
|
+
} catch {
|
|
13540
|
+
console.warn(
|
|
13541
|
+
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path25.basename(filePath)}`
|
|
13542
|
+
);
|
|
13543
|
+
return null;
|
|
13544
|
+
}
|
|
13545
|
+
const usage = extractUsage(entry);
|
|
13546
|
+
if (!usage) return null;
|
|
13547
|
+
return {
|
|
13548
|
+
record: buildRecord(entry, usage),
|
|
13549
|
+
requestId: entry.requestId ?? null
|
|
13550
|
+
};
|
|
13551
|
+
}
|
|
13552
|
+
function readCCFile(filePath) {
|
|
13553
|
+
let raw;
|
|
13554
|
+
try {
|
|
13555
|
+
raw = fs25.readFileSync(filePath, "utf-8");
|
|
13556
|
+
} catch {
|
|
13557
|
+
return [];
|
|
13558
|
+
}
|
|
13559
|
+
const byRequestId = /* @__PURE__ */ new Map();
|
|
13560
|
+
const noRequestId = [];
|
|
13561
|
+
const lines = raw.split("\n");
|
|
13562
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13563
|
+
const line = lines[i]?.trim();
|
|
13564
|
+
if (!line) continue;
|
|
13565
|
+
const parsed = parseCCLine(line, filePath, i + 1);
|
|
13566
|
+
if (!parsed) continue;
|
|
13567
|
+
if (parsed.requestId) {
|
|
13568
|
+
byRequestId.set(parsed.requestId, parsed.record);
|
|
13569
|
+
} else {
|
|
13570
|
+
noRequestId.push(parsed.record);
|
|
13571
|
+
}
|
|
13572
|
+
}
|
|
13573
|
+
return [...byRequestId.values(), ...noRequestId];
|
|
13574
|
+
}
|
|
13575
|
+
function parseCCRecords() {
|
|
13576
|
+
const homeDir = process.env.HOME ?? os2.homedir();
|
|
13577
|
+
const projectsDir = path25.join(homeDir, ".claude", "projects");
|
|
13578
|
+
let projectDirs;
|
|
13579
|
+
try {
|
|
13580
|
+
projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
|
|
13581
|
+
} catch {
|
|
13582
|
+
return [];
|
|
13583
|
+
}
|
|
13584
|
+
const records = [];
|
|
13585
|
+
for (const dir of projectDirs) {
|
|
13586
|
+
let files;
|
|
13587
|
+
try {
|
|
13588
|
+
files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
|
|
13589
|
+
} catch {
|
|
13590
|
+
continue;
|
|
13591
|
+
}
|
|
13592
|
+
for (const file of files) {
|
|
13593
|
+
records.push(...readCCFile(file));
|
|
13594
|
+
}
|
|
13595
|
+
}
|
|
13596
|
+
return records;
|
|
13597
|
+
}
|
|
13598
|
+
|
|
11353
13599
|
// src/index.ts
|
|
11354
|
-
var VERSION = "0.
|
|
13600
|
+
var VERSION = "0.15.0";
|
|
11355
13601
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11356
13602
|
0 && (module.exports = {
|
|
11357
13603
|
AGENT_DESCRIPTORS,
|
|
@@ -11368,6 +13614,7 @@ var VERSION = "0.14.0";
|
|
|
11368
13614
|
BlueprintGenerator,
|
|
11369
13615
|
BundleConstraintsSchema,
|
|
11370
13616
|
BundleSchema,
|
|
13617
|
+
CACHE_TTL_MS,
|
|
11371
13618
|
COMPLIANCE_DESCRIPTOR,
|
|
11372
13619
|
CategoryBaselineSchema,
|
|
11373
13620
|
CategoryRegressionSchema,
|
|
@@ -11385,7 +13632,9 @@ var VERSION = "0.14.0";
|
|
|
11385
13632
|
DEFAULT_SECURITY_CONFIG,
|
|
11386
13633
|
DEFAULT_STATE,
|
|
11387
13634
|
DEFAULT_STREAM_INDEX,
|
|
13635
|
+
DESTRUCTIVE_BASH,
|
|
11388
13636
|
DepDepthCollector,
|
|
13637
|
+
EXTENSION_MAP,
|
|
11389
13638
|
EmitInteractionInputSchema,
|
|
11390
13639
|
EntropyAnalyzer,
|
|
11391
13640
|
EntropyConfigSchema,
|
|
@@ -11398,6 +13647,7 @@ var VERSION = "0.14.0";
|
|
|
11398
13647
|
HandoffSchema,
|
|
11399
13648
|
HarnessStateSchema,
|
|
11400
13649
|
InteractionTypeSchema,
|
|
13650
|
+
LITELLM_PRICING_URL,
|
|
11401
13651
|
LayerViolationCollector,
|
|
11402
13652
|
LockfilePackageSchema,
|
|
11403
13653
|
LockfileSchema,
|
|
@@ -11414,12 +13664,14 @@ var VERSION = "0.14.0";
|
|
|
11414
13664
|
RegressionDetector,
|
|
11415
13665
|
RuleRegistry,
|
|
11416
13666
|
SECURITY_DESCRIPTOR,
|
|
13667
|
+
STALENESS_WARNING_DAYS,
|
|
11417
13668
|
SecurityConfigSchema,
|
|
11418
13669
|
SecurityScanner,
|
|
11419
13670
|
SharableBoundaryConfigSchema,
|
|
11420
13671
|
SharableForbiddenImportSchema,
|
|
11421
13672
|
SharableLayerSchema,
|
|
11422
13673
|
SharableSecurityRulesSchema,
|
|
13674
|
+
SkillEventSchema,
|
|
11423
13675
|
StreamIndexSchema,
|
|
11424
13676
|
StreamInfoSchema,
|
|
11425
13677
|
ThresholdConfigSchema,
|
|
@@ -11428,6 +13680,9 @@ var VERSION = "0.14.0";
|
|
|
11428
13680
|
VERSION,
|
|
11429
13681
|
ViolationSchema,
|
|
11430
13682
|
addProvenance,
|
|
13683
|
+
agentConfigRules,
|
|
13684
|
+
aggregateByDay,
|
|
13685
|
+
aggregateBySession,
|
|
11431
13686
|
analyzeDiff,
|
|
11432
13687
|
analyzeLearningPatterns,
|
|
11433
13688
|
appendFailure,
|
|
@@ -11435,6 +13690,7 @@ var VERSION = "0.14.0";
|
|
|
11435
13690
|
appendSessionEntry,
|
|
11436
13691
|
applyFixes,
|
|
11437
13692
|
applyHotspotDowngrade,
|
|
13693
|
+
applySyncChanges,
|
|
11438
13694
|
archMatchers,
|
|
11439
13695
|
archModule,
|
|
11440
13696
|
architecture,
|
|
@@ -11445,16 +13701,23 @@ var VERSION = "0.14.0";
|
|
|
11445
13701
|
buildDependencyGraph,
|
|
11446
13702
|
buildExclusionSet,
|
|
11447
13703
|
buildSnapshot,
|
|
13704
|
+
calculateCost,
|
|
11448
13705
|
checkDocCoverage,
|
|
11449
13706
|
checkEligibility,
|
|
11450
13707
|
checkEvidenceCoverage,
|
|
13708
|
+
checkTaint,
|
|
11451
13709
|
classifyFinding,
|
|
13710
|
+
clearEventHashCache,
|
|
11452
13711
|
clearFailuresCache,
|
|
11453
13712
|
clearLearningsCache,
|
|
13713
|
+
clearTaint,
|
|
13714
|
+
computeOverallSeverity,
|
|
13715
|
+
computeScanExitCode,
|
|
11454
13716
|
configureFeedback,
|
|
11455
13717
|
constraintRuleId,
|
|
11456
13718
|
contextBudget,
|
|
11457
13719
|
contextFilter,
|
|
13720
|
+
countLearningEntries,
|
|
11458
13721
|
createBoundaryValidator,
|
|
11459
13722
|
createCommentedCodeFixes,
|
|
11460
13723
|
createError,
|
|
@@ -11478,66 +13741,95 @@ var VERSION = "0.14.0";
|
|
|
11478
13741
|
detectCouplingViolations,
|
|
11479
13742
|
detectDeadCode,
|
|
11480
13743
|
detectDocDrift,
|
|
13744
|
+
detectLanguage,
|
|
11481
13745
|
detectPatternViolations,
|
|
11482
13746
|
detectSizeBudgetViolations,
|
|
11483
13747
|
detectStack,
|
|
11484
13748
|
detectStaleConstraints,
|
|
11485
13749
|
determineAssessment,
|
|
11486
13750
|
diff,
|
|
13751
|
+
emitEvent,
|
|
11487
13752
|
executeWorkflow,
|
|
11488
13753
|
expressRules,
|
|
11489
13754
|
extractBundle,
|
|
13755
|
+
extractIndexEntry,
|
|
11490
13756
|
extractMarkdownLinks,
|
|
11491
13757
|
extractSections,
|
|
11492
13758
|
fanOutReview,
|
|
13759
|
+
formatEventTimeline,
|
|
11493
13760
|
formatFindingBlock,
|
|
11494
13761
|
formatGitHubComment,
|
|
11495
13762
|
formatGitHubSummary,
|
|
13763
|
+
formatOutline,
|
|
11496
13764
|
formatTerminalOutput,
|
|
11497
13765
|
generateAgentsMap,
|
|
11498
13766
|
generateSuggestions,
|
|
11499
13767
|
getActionEmitter,
|
|
11500
13768
|
getExitCode,
|
|
11501
13769
|
getFeedbackConfig,
|
|
13770
|
+
getInjectionPatterns,
|
|
13771
|
+
getModelPrice,
|
|
13772
|
+
getOutline,
|
|
13773
|
+
getParser,
|
|
11502
13774
|
getPhaseCategories,
|
|
11503
13775
|
getStreamForBranch,
|
|
13776
|
+
getTaintFilePath,
|
|
11504
13777
|
getUpdateNotification,
|
|
11505
13778
|
goRules,
|
|
11506
13779
|
injectionRules,
|
|
13780
|
+
insecureDefaultsRules,
|
|
13781
|
+
isDuplicateFinding,
|
|
11507
13782
|
isSmallSuggestion,
|
|
11508
13783
|
isUpdateCheckEnabled,
|
|
11509
13784
|
listActiveSessions,
|
|
11510
13785
|
listStreams,
|
|
13786
|
+
listTaintedSessions,
|
|
11511
13787
|
loadBudgetedLearnings,
|
|
13788
|
+
loadEvents,
|
|
11512
13789
|
loadFailures,
|
|
11513
13790
|
loadHandoff,
|
|
13791
|
+
loadIndexEntries,
|
|
13792
|
+
loadPricingData,
|
|
11514
13793
|
loadRelevantLearnings,
|
|
11515
13794
|
loadSessionSummary,
|
|
11516
13795
|
loadState,
|
|
11517
13796
|
loadStreamIndex,
|
|
11518
13797
|
logAgentAction,
|
|
13798
|
+
mapInjectionFindings,
|
|
13799
|
+
mapSecurityFindings,
|
|
13800
|
+
mapSecuritySeverity,
|
|
13801
|
+
mcpRules,
|
|
11519
13802
|
migrateToStreams,
|
|
11520
13803
|
networkRules,
|
|
11521
13804
|
nodeRules,
|
|
13805
|
+
parseCCRecords,
|
|
11522
13806
|
parseDateFromEntry,
|
|
11523
13807
|
parseDiff,
|
|
13808
|
+
parseFile,
|
|
13809
|
+
parseFrontmatter,
|
|
13810
|
+
parseHarnessIgnore,
|
|
13811
|
+
parseLiteLLMData,
|
|
11524
13812
|
parseManifest,
|
|
11525
13813
|
parseRoadmap,
|
|
11526
13814
|
parseSecurityConfig,
|
|
11527
13815
|
parseSize,
|
|
11528
13816
|
pathTraversalRules,
|
|
11529
13817
|
previewFix,
|
|
13818
|
+
promoteSessionLearnings,
|
|
11530
13819
|
pruneLearnings,
|
|
11531
13820
|
reactRules,
|
|
11532
13821
|
readCheckState,
|
|
13822
|
+
readCostRecords,
|
|
11533
13823
|
readLockfile,
|
|
11534
13824
|
readSessionSection,
|
|
11535
13825
|
readSessionSections,
|
|
13826
|
+
readTaint,
|
|
11536
13827
|
removeContributions,
|
|
11537
13828
|
removeProvenance,
|
|
11538
13829
|
requestMultiplePeerReviews,
|
|
11539
13830
|
requestPeerReview,
|
|
11540
13831
|
resetFeedbackConfig,
|
|
13832
|
+
resetParserCache,
|
|
11541
13833
|
resolveFileToLayer,
|
|
11542
13834
|
resolveModelTier,
|
|
11543
13835
|
resolveRuleSeverity,
|
|
@@ -11558,10 +13850,13 @@ var VERSION = "0.14.0";
|
|
|
11558
13850
|
saveHandoff,
|
|
11559
13851
|
saveState,
|
|
11560
13852
|
saveStreamIndex,
|
|
13853
|
+
scanForInjection,
|
|
11561
13854
|
scopeContext,
|
|
13855
|
+
searchSymbols,
|
|
11562
13856
|
secretRules,
|
|
11563
13857
|
serializeRoadmap,
|
|
11564
13858
|
setActiveStream,
|
|
13859
|
+
sharpEdgesRules,
|
|
11565
13860
|
shouldRunCheck,
|
|
11566
13861
|
spawnBackgroundCheck,
|
|
11567
13862
|
syncConstraintNodes,
|
|
@@ -11569,6 +13864,8 @@ var VERSION = "0.14.0";
|
|
|
11569
13864
|
tagUncitedFindings,
|
|
11570
13865
|
touchStream,
|
|
11571
13866
|
trackAction,
|
|
13867
|
+
unfoldRange,
|
|
13868
|
+
unfoldSymbol,
|
|
11572
13869
|
updateSessionEntryStatus,
|
|
11573
13870
|
updateSessionIndex,
|
|
11574
13871
|
validateAgentsMap,
|
|
@@ -11584,6 +13881,7 @@ var VERSION = "0.14.0";
|
|
|
11584
13881
|
writeConfig,
|
|
11585
13882
|
writeLockfile,
|
|
11586
13883
|
writeSessionSummary,
|
|
13884
|
+
writeTaint,
|
|
11587
13885
|
xssRules,
|
|
11588
13886
|
...require("@harness-engineering/types")
|
|
11589
13887
|
});
|