sentinelayer-cli 0.39.2 → 0.41.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 +4 -4
- package/package.json +2 -3
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +703 -263
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +64 -0
- package/src/engram/namespace.js +89 -0
- package/src/engram/observations.js +34 -0
- package/src/engram/sla.js +104 -0
- package/src/engram/store.js +115 -0
- package/src/engram/tools.js +146 -0
- package/src/engram/trust.js +105 -0
- package/src/legacy-cli.js +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +194 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon.js +19 -124
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/listener.js +243 -20
- package/src/session/listeners.js +71 -80
- package/src/session/paths.js +2 -0
- package/src/session/recall/embedder.js +181 -0
- package/src/session/recall/entities.js +0 -0
- package/src/session/recall/index-build.js +38 -0
- package/src/session/recall/index-core.js +108 -0
- package/src/session/recall/index.js +131 -0
- package/src/session/recall/needle-eval.js +279 -0
- package/src/session/recall/observation-core.js +183 -0
- package/src/session/recall/observations.js +21 -0
- package/src/session/recall/provenance.js +153 -0
- package/src/session/recall/retrieve.js +374 -0
- package/src/session/recall/text.js +0 -0
- package/src/session/recall/token-stats.js +81 -0
- package/src/session/recap.js +29 -23
- package/src/session/remote-hydrate.js +5 -1
- package/src/session/sync.js +661 -11
- package/src/session/wake/pump.js +8 -3
package/README.md
CHANGED
|
@@ -142,14 +142,14 @@ Then point your agents at it:
|
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
144
|
sl session join <session-id> --agent <agent-name>
|
|
145
|
-
sl session
|
|
145
|
+
sl session action <session-id> working_on --target-sequence <n>
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
Sentinelayer includes a deterministic session coordination surface for multi-agent coding loops:
|
|
149
149
|
|
|
150
150
|
- session event stream and replay (`start`, `join`, `say`, `read`, `status`, `leave`, `list`, `kill`)
|
|
151
|
-
- low-noise message actions (`react ack|like|dislike`, `action working_on|disregard`, `reply`/`comment`, `
|
|
152
|
-
- agent lifecycle controls (join/
|
|
151
|
+
- low-noise message actions (`react ack|like|dislike`, `action working_on|disregard`, `reply`/`comment`, `actions`) plus one monotonic per-agent read cursor (`view`)
|
|
152
|
+
- agent lifecycle controls (join/ephemeral TTL presence/leave/kill); listener liveness never enters the durable transcript
|
|
153
153
|
- recap and context briefing for late-joining agents
|
|
154
154
|
- analytics + lineage artifacts at session closeout
|
|
155
155
|
|
|
@@ -694,7 +694,7 @@ Use `init-aidenid` to scaffold an Anthropic-compatible tool schema wrapper for A
|
|
|
694
694
|
Use `init-aidenid-adapter` to scaffold a deterministic AIdenID provisioning API contract (tool binding -> HTTP path/method -> response field mapping) and cross-check it against the registry with `validate-aidenid-adapter`.
|
|
695
695
|
Use `token mint` to request a short-lived hosted MCP bearer token from the Sentinelayer API. Text output hides the bearer value; `--json` is the explicit operator path that includes `accessToken`. The command is blocked from the generated CLI MCP bridge.
|
|
696
696
|
Use `smoke` to verify the hosted MCP resource without printing the bearer. The canonical remote MCP paste URL is `https://api.sentinelayer.com/mcp`; the token audience/resource value is `https://mcp.sentinelayer.com` and is handled by the OAuth/MCP flow. `sl mcp smoke` works today through the CLI direct-mint path; third-party browser connectors such as Claude.ai still require the OAuth browser-flow and connector-registration work to complete before users can authenticate from that product.
|
|
697
|
-
Use `init-session` plus `server run` for the local stdio Senti MCP server. It exposes session inbox, durable posts, actions/replies/reactions, file
|
|
697
|
+
Use `init-session` plus `server run` for the local stdio Senti MCP server. It exposes session inbox, durable posts, actions/replies/reactions, API-authoritative file leases, and attention requests to local MCP clients. File-lease lifecycle operations never write session events; install edit preflights with `sl session guard-install` and remove them with `sl session guard-uninstall` before any CLI rollback. Hosted Claude-web/ChatGPT connectors need a separate HTTPS/OAuth service and are not shipped by the local stdio server. See [docs/mcp.md](docs/mcp.md) and [docs/file-leases.md](docs/file-leases.md).
|
|
698
698
|
Use `init-cli` to generate a bridge registry for every `sl` leaf command from Commander metadata. This exposes the full CLI surface as `sl.<command.path>` tool schemas, but execution still requires a bridge-capable MCP host with OAuth/session-seat policy and human approval enforcement. The hosted connector target contract is documented in [docs/mcp-hosted-connector.md](docs/mcp-hosted-connector.md).
|
|
699
699
|
|
|
700
700
|
MCP operator verification:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sentinelayer-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -66,8 +66,7 @@
|
|
|
66
66
|
"zod": "4.1.12"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"c8": "
|
|
70
|
-
"license-checker-rseidelsohn": "4.4.2"
|
|
69
|
+
"c8": "11.0.0"
|
|
71
70
|
},
|
|
72
71
|
"overrides": {
|
|
73
72
|
"lighthouse": {
|
|
@@ -1,171 +1,24 @@
|
|
|
1
1
|
// Shared helpers for Amina's (ai-governance) domain tools (#A24).
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
".idea",
|
|
21
|
-
".vscode",
|
|
22
|
-
"__pycache__",
|
|
23
|
-
".cache",
|
|
24
|
-
]);
|
|
25
|
-
const MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
26
|
-
const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
27
|
-
|
|
28
|
-
export function toPosix(value) {
|
|
29
|
-
return String(value || "").replace(/\\/g, "/");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function createFinding({
|
|
33
|
-
severity,
|
|
34
|
-
kind,
|
|
35
|
-
file,
|
|
36
|
-
line = 0,
|
|
37
|
-
evidence = "",
|
|
38
|
-
rootCause = "",
|
|
39
|
-
recommendedFix = "",
|
|
40
|
-
confidence = null,
|
|
41
|
-
tool = "",
|
|
42
|
-
persona = "ai-governance",
|
|
43
|
-
} = {}) {
|
|
44
|
-
const normalizedSeverity = SEVERITIES.includes(String(severity || "").toUpperCase())
|
|
45
|
-
? String(severity).toUpperCase()
|
|
46
|
-
: "P2";
|
|
47
|
-
return {
|
|
48
|
-
persona,
|
|
49
|
-
tool: String(tool || "").trim(),
|
|
50
|
-
kind: String(kind || "").trim() || "ai-governance",
|
|
51
|
-
severity: normalizedSeverity,
|
|
52
|
-
file: toPosix(file || ""),
|
|
53
|
-
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
54
|
-
evidence: String(evidence || "").trim().slice(0, 400),
|
|
55
|
-
rootCause: String(rootCause || "").trim(),
|
|
56
|
-
recommendedFix: String(recommendedFix || "").trim(),
|
|
57
|
-
confidence:
|
|
58
|
-
confidence === null || confidence === undefined
|
|
59
|
-
? null
|
|
60
|
-
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function readIgnorePatterns(filePath) {
|
|
65
|
-
try {
|
|
66
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
67
|
-
return String(raw || "")
|
|
68
|
-
.split(/\r?\n/)
|
|
69
|
-
.map((line) => line.trim())
|
|
70
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
71
|
-
} catch (err) {
|
|
72
|
-
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
throw err;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function createIgnoreMatcher(rootPath) {
|
|
80
|
-
const matcher = ignore();
|
|
81
|
-
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
82
|
-
const sentinel = await readIgnorePatterns(
|
|
83
|
-
path.join(rootPath, ".sentinelayerignore")
|
|
84
|
-
);
|
|
85
|
-
matcher.add([...gitignore, ...sentinel]);
|
|
86
|
-
return (relativePath, isDirectory) => {
|
|
87
|
-
const normalized = toPosix(relativePath);
|
|
88
|
-
if (!normalized) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
92
|
-
return matcher.ignores(candidate);
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function findLineMatches(content, pattern) {
|
|
97
|
-
const text = String(content || "");
|
|
98
|
-
const global = new RegExp(
|
|
99
|
-
pattern.source,
|
|
100
|
-
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
101
|
-
);
|
|
102
|
-
const matches = [];
|
|
103
|
-
let match;
|
|
104
|
-
while ((match = global.exec(text)) !== null) {
|
|
105
|
-
const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
|
|
106
|
-
matches.push({ index: match.index, line: lineIndex, match: match[0] });
|
|
107
|
-
}
|
|
108
|
-
return matches;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function getLineContent(content, line) {
|
|
112
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
113
|
-
return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
|
|
3
|
+
import {
|
|
4
|
+
createFinding as createDomainFinding,
|
|
5
|
+
findLineMatches,
|
|
6
|
+
getLineContent,
|
|
7
|
+
toPosix,
|
|
8
|
+
walkRepoFiles as walkDomainRepoFiles,
|
|
9
|
+
} from "../../shared-tools/domain-base.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PERSONA = "ai-governance";
|
|
12
|
+
|
|
13
|
+
export { findLineMatches, getLineContent, toPosix };
|
|
14
|
+
|
|
15
|
+
export function createFinding(options = {}) {
|
|
16
|
+
return createDomainFinding(options, {
|
|
17
|
+
defaultPersona: DEFAULT_PERSONA,
|
|
18
|
+
defaultKind: DEFAULT_PERSONA,
|
|
19
|
+
});
|
|
114
20
|
}
|
|
115
21
|
|
|
116
|
-
export async function* walkRepoFiles({
|
|
117
|
-
|
|
118
|
-
extensions = new Set(),
|
|
119
|
-
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
120
|
-
} = {}) {
|
|
121
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
122
|
-
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
123
|
-
const wantedExtensions =
|
|
124
|
-
extensions instanceof Set
|
|
125
|
-
? extensions
|
|
126
|
-
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
127
|
-
const stack = [resolvedRoot];
|
|
128
|
-
while (stack.length > 0) {
|
|
129
|
-
const current = stack.pop();
|
|
130
|
-
let entries = [];
|
|
131
|
-
try {
|
|
132
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
133
|
-
} catch {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
for (const entry of entries) {
|
|
137
|
-
const fullPath = path.join(current, entry.name);
|
|
138
|
-
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
139
|
-
if (entry.isDirectory()) {
|
|
140
|
-
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (ignoreMatcher(relativePath, true)) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
stack.push(fullPath);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (!entry.isFile()) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (ignoreMatcher(relativePath, false)) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
156
|
-
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
let stat = null;
|
|
160
|
-
try {
|
|
161
|
-
stat = await fsp.stat(fullPath);
|
|
162
|
-
} catch {
|
|
163
|
-
stat = null;
|
|
164
|
-
}
|
|
165
|
-
if (!stat || stat.size > maxFileSize) {
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
yield { fullPath, relativePath, stat };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
22
|
+
export async function* walkRepoFiles(options = {}) {
|
|
23
|
+
yield* walkDomainRepoFiles(options, { entryMetadata: "stat" });
|
|
171
24
|
}
|
|
@@ -1,189 +1,36 @@
|
|
|
1
1
|
// Shared helpers for Maya's (backend) domain tools (#A14).
|
|
2
|
-
// Mirrors src/agents/security/tools/base.js but with persona defaulting to
|
|
3
|
-
// "backend". Intentional per-persona copy — a later refactor (#A25+) can
|
|
4
|
-
// hoist this to src/agents/shared-tools/ once the 12 persona modules are
|
|
5
|
-
// shipped and the common surface is stable.
|
|
6
2
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return "P2";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function createFinding({
|
|
45
|
-
severity,
|
|
46
|
-
kind,
|
|
47
|
-
file,
|
|
48
|
-
line = 0,
|
|
49
|
-
evidence = "",
|
|
50
|
-
rootCause = "",
|
|
51
|
-
recommendedFix = "",
|
|
52
|
-
confidence = null,
|
|
53
|
-
tool = "",
|
|
54
|
-
persona = "backend",
|
|
55
|
-
} = {}) {
|
|
56
|
-
return {
|
|
57
|
-
persona,
|
|
58
|
-
tool: String(tool || "").trim(),
|
|
59
|
-
kind: String(kind || "").trim() || "backend",
|
|
60
|
-
severity: normalizeSeverity(severity),
|
|
61
|
-
file: toPosix(file || ""),
|
|
62
|
-
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
63
|
-
evidence: String(evidence || "").trim().slice(0, 400),
|
|
64
|
-
rootCause: String(rootCause || "").trim(),
|
|
65
|
-
recommendedFix: String(recommendedFix || "").trim(),
|
|
66
|
-
confidence:
|
|
67
|
-
confidence === null || confidence === undefined
|
|
68
|
-
? null
|
|
69
|
-
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function readIgnorePatterns(filePath) {
|
|
74
|
-
try {
|
|
75
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
76
|
-
return String(raw || "")
|
|
77
|
-
.split(/\r?\n/)
|
|
78
|
-
.map((line) => line.trim())
|
|
79
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
80
|
-
} catch (err) {
|
|
81
|
-
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_IGNORED_DIRS,
|
|
5
|
+
MAX_FILE_SIZE_BYTES,
|
|
6
|
+
SEVERITIES,
|
|
7
|
+
createFinding as createDomainFinding,
|
|
8
|
+
findLineMatches,
|
|
9
|
+
getLineContent,
|
|
10
|
+
normalizeSeverity,
|
|
11
|
+
toPosix,
|
|
12
|
+
walkRepoFiles as walkDomainRepoFiles,
|
|
13
|
+
} from "../../shared-tools/domain-base.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PERSONA = "backend";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_IGNORED_DIRS,
|
|
19
|
+
MAX_FILE_SIZE_BYTES,
|
|
20
|
+
SEVERITIES,
|
|
21
|
+
findLineMatches,
|
|
22
|
+
getLineContent,
|
|
23
|
+
normalizeSeverity,
|
|
24
|
+
toPosix,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function createFinding(options = {}) {
|
|
28
|
+
return createDomainFinding(options, {
|
|
29
|
+
defaultPersona: DEFAULT_PERSONA,
|
|
30
|
+
defaultKind: DEFAULT_PERSONA,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function* walkRepoFiles(options = {}) {
|
|
35
|
+
yield* walkDomainRepoFiles(options);
|
|
86
36
|
}
|
|
87
|
-
|
|
88
|
-
async function createIgnoreMatcher(rootPath) {
|
|
89
|
-
const matcher = ignore();
|
|
90
|
-
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
91
|
-
const sentinel = await readIgnorePatterns(
|
|
92
|
-
path.join(rootPath, ".sentinelayerignore")
|
|
93
|
-
);
|
|
94
|
-
matcher.add([...gitignore, ...sentinel]);
|
|
95
|
-
return (relativePath, isDirectory) => {
|
|
96
|
-
const normalized = toPosix(relativePath);
|
|
97
|
-
if (!normalized) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
101
|
-
return matcher.ignores(candidate);
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function* walkRepoFiles({
|
|
106
|
-
rootPath = process.cwd(),
|
|
107
|
-
extensions = new Set(),
|
|
108
|
-
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
109
|
-
} = {}) {
|
|
110
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
111
|
-
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
112
|
-
const wantedExtensions =
|
|
113
|
-
extensions instanceof Set
|
|
114
|
-
? extensions
|
|
115
|
-
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
116
|
-
const stack = [resolvedRoot];
|
|
117
|
-
while (stack.length > 0) {
|
|
118
|
-
const current = stack.pop();
|
|
119
|
-
let entries = [];
|
|
120
|
-
try {
|
|
121
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
122
|
-
} catch {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
for (const entry of entries) {
|
|
126
|
-
const fullPath = path.join(current, entry.name);
|
|
127
|
-
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
128
|
-
if (entry.isDirectory()) {
|
|
129
|
-
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (ignoreMatcher(relativePath, true)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
stack.push(fullPath);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (!entry.isFile()) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (ignoreMatcher(relativePath, false)) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
145
|
-
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
let stat = null;
|
|
149
|
-
try {
|
|
150
|
-
stat = await fsp.stat(fullPath);
|
|
151
|
-
} catch {
|
|
152
|
-
stat = null;
|
|
153
|
-
}
|
|
154
|
-
if (!stat || stat.size > maxFileSize) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
yield { fullPath, relativePath };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function findLineMatches(content, pattern) {
|
|
163
|
-
const text = String(content || "");
|
|
164
|
-
if (!pattern) {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
const global = new RegExp(
|
|
168
|
-
pattern.source,
|
|
169
|
-
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
170
|
-
);
|
|
171
|
-
const matches = [];
|
|
172
|
-
let match;
|
|
173
|
-
while ((match = global.exec(text)) !== null) {
|
|
174
|
-
const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
|
|
175
|
-
matches.push({
|
|
176
|
-
index: match.index,
|
|
177
|
-
line: lineIndex,
|
|
178
|
-
match: match[0],
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return matches;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function getLineContent(content, line) {
|
|
185
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
186
|
-
return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|
|
@@ -1,159 +1,32 @@
|
|
|
1
1
|
// Shared helpers for Ethan's (code-quality) domain tools (#A16).
|
|
2
|
-
// Same per-persona base pattern as security / backend / testing.
|
|
3
2
|
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export function toPosix(value) {
|
|
30
|
-
return String(value || "").replace(/\\/g, "/");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function normalizeSeverity(value) {
|
|
34
|
-
const normalized = String(value || "").trim().toUpperCase();
|
|
35
|
-
if (SEVERITIES.includes(normalized)) {
|
|
36
|
-
return normalized;
|
|
37
|
-
}
|
|
38
|
-
return "P2";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function createFinding({
|
|
42
|
-
severity,
|
|
43
|
-
kind,
|
|
44
|
-
file,
|
|
45
|
-
line = 0,
|
|
46
|
-
evidence = "",
|
|
47
|
-
rootCause = "",
|
|
48
|
-
recommendedFix = "",
|
|
49
|
-
confidence = null,
|
|
50
|
-
tool = "",
|
|
51
|
-
persona = "code-quality",
|
|
52
|
-
} = {}) {
|
|
53
|
-
return {
|
|
54
|
-
persona,
|
|
55
|
-
tool: String(tool || "").trim(),
|
|
56
|
-
kind: String(kind || "").trim() || "code-quality",
|
|
57
|
-
severity: normalizeSeverity(severity),
|
|
58
|
-
file: toPosix(file || ""),
|
|
59
|
-
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
60
|
-
evidence: String(evidence || "").trim().slice(0, 400),
|
|
61
|
-
rootCause: String(rootCause || "").trim(),
|
|
62
|
-
recommendedFix: String(recommendedFix || "").trim(),
|
|
63
|
-
confidence:
|
|
64
|
-
confidence === null || confidence === undefined
|
|
65
|
-
? null
|
|
66
|
-
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function readIgnorePatterns(filePath) {
|
|
71
|
-
try {
|
|
72
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
73
|
-
return String(raw || "")
|
|
74
|
-
.split(/\r?\n/)
|
|
75
|
-
.map((line) => line.trim())
|
|
76
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
77
|
-
} catch (err) {
|
|
78
|
-
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
throw err;
|
|
82
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_IGNORED_DIRS,
|
|
5
|
+
MAX_FILE_SIZE_BYTES,
|
|
6
|
+
SEVERITIES,
|
|
7
|
+
createFinding as createDomainFinding,
|
|
8
|
+
normalizeSeverity,
|
|
9
|
+
toPosix,
|
|
10
|
+
walkRepoFiles as walkDomainRepoFiles,
|
|
11
|
+
} from "../../shared-tools/domain-base.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PERSONA = "code-quality";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_IGNORED_DIRS,
|
|
17
|
+
MAX_FILE_SIZE_BYTES,
|
|
18
|
+
SEVERITIES,
|
|
19
|
+
normalizeSeverity,
|
|
20
|
+
toPosix,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createFinding(options = {}) {
|
|
24
|
+
return createDomainFinding(options, {
|
|
25
|
+
defaultPersona: DEFAULT_PERSONA,
|
|
26
|
+
defaultKind: DEFAULT_PERSONA,
|
|
27
|
+
});
|
|
83
28
|
}
|
|
84
29
|
|
|
85
|
-
async function
|
|
86
|
-
|
|
87
|
-
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
88
|
-
const sentinel = await readIgnorePatterns(
|
|
89
|
-
path.join(rootPath, ".sentinelayerignore")
|
|
90
|
-
);
|
|
91
|
-
matcher.add([...gitignore, ...sentinel]);
|
|
92
|
-
return (relativePath, isDirectory) => {
|
|
93
|
-
const normalized = toPosix(relativePath);
|
|
94
|
-
if (!normalized) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
98
|
-
return matcher.ignores(candidate);
|
|
99
|
-
};
|
|
30
|
+
export async function* walkRepoFiles(options = {}) {
|
|
31
|
+
yield* walkDomainRepoFiles(options);
|
|
100
32
|
}
|
|
101
|
-
|
|
102
|
-
export async function* walkRepoFiles({
|
|
103
|
-
rootPath = process.cwd(),
|
|
104
|
-
extensions = new Set(),
|
|
105
|
-
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
106
|
-
} = {}) {
|
|
107
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
108
|
-
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
109
|
-
const wantedExtensions =
|
|
110
|
-
extensions instanceof Set
|
|
111
|
-
? extensions
|
|
112
|
-
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
113
|
-
const stack = [resolvedRoot];
|
|
114
|
-
while (stack.length > 0) {
|
|
115
|
-
const current = stack.pop();
|
|
116
|
-
let entries = [];
|
|
117
|
-
try {
|
|
118
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
119
|
-
} catch {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
const fullPath = path.join(current, entry.name);
|
|
124
|
-
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
125
|
-
if (entry.isDirectory()) {
|
|
126
|
-
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (ignoreMatcher(relativePath, true)) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
stack.push(fullPath);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (!entry.isFile()) {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (ignoreMatcher(relativePath, false)) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
142
|
-
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
let stat = null;
|
|
146
|
-
try {
|
|
147
|
-
stat = await fsp.stat(fullPath);
|
|
148
|
-
} catch {
|
|
149
|
-
stat = null;
|
|
150
|
-
}
|
|
151
|
-
if (!stat || stat.size > maxFileSize) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
yield { fullPath, relativePath };
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|