sentinelayer-cli 0.4.5 → 0.8.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 +16 -18
- package/package.json +7 -6
- package/src/agents/jules/config/definition.js +13 -62
- package/src/agents/jules/config/system-prompt.js +8 -1
- package/src/agents/jules/fix-cycle.js +12 -372
- package/src/agents/jules/loop.js +116 -26
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +13 -12
- package/src/agents/jules/swarm/orchestrator.js +3 -3
- package/src/agents/jules/swarm/sub-agent.js +6 -3
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1187 -45
- package/src/agents/jules/tools/dispatch.js +25 -12
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +6 -2
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/persona-visuals.js +64 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +56 -7
- package/src/ai/client.js +45 -0
- package/src/ai/proxy.js +137 -0
- package/src/auth/gate.js +290 -16
- package/src/auth/http.js +450 -39
- package/src/auth/service.js +262 -47
- package/src/auth/session-store.js +475 -21
- package/src/cli.js +5 -0
- package/src/commands/audit.js +13 -8
- package/src/commands/auth.js +53 -9
- package/src/commands/omargate.js +10 -2
- package/src/commands/scan.js +10 -4
- package/src/commands/session.js +590 -0
- package/src/commands/spec.js +62 -0
- package/src/commands/watch.js +3 -2
- package/src/daemon/assignment-ledger.js +196 -0
- package/src/daemon/error-worker.js +599 -16
- package/src/daemon/fix-cycle.js +384 -0
- package/src/daemon/ingest-refresh.js +10 -9
- package/src/daemon/jira-lifecycle.js +135 -0
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/scope-engine.js +1068 -0
- package/src/events/schema.js +190 -0
- package/src/interactive/index.js +18 -16
- package/src/legacy-cli.js +606 -37
- package/src/prompt/generator.js +19 -1
- package/src/review/ai-review.js +11 -1
- package/src/review/local-review.js +75 -19
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +404 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/scan-modes.js +48 -0
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +352 -0
- package/src/session/daemon.js +801 -0
- package/src/session/paths.js +33 -0
- package/src/session/runtime-bridge.js +739 -0
- package/src/session/store.js +388 -0
- package/src/session/stream.js +325 -0
- package/src/spec/generator.js +100 -0
- package/src/telemetry/session-tracker.js +148 -32
- package/src/telemetry/sync.js +6 -2
- package/src/ui/command-hints.js +13 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
normalizeRunEvent,
|
|
5
5
|
appendRunEvent,
|
|
6
6
|
} from "../../../telemetry/ledger.js";
|
|
7
|
+
import { createAgentEvent } from "../../../events/schema.js";
|
|
7
8
|
import { fileRead } from "./file-read.js";
|
|
8
9
|
import { grep } from "./grep.js";
|
|
9
10
|
import { glob } from "./glob.js";
|
|
@@ -78,8 +79,7 @@ export async function dispatchTool(toolName, input, ctx) {
|
|
|
78
79
|
await safeAppendEvent(ctx, stopEvent);
|
|
79
80
|
|
|
80
81
|
if (ctx.onEvent) {
|
|
81
|
-
ctx.onEvent({
|
|
82
|
-
stream: "sl_event",
|
|
82
|
+
ctx.onEvent(createAgentEvent({
|
|
83
83
|
event: "budget_stop",
|
|
84
84
|
agent: ctx.agentIdentity,
|
|
85
85
|
payload: {
|
|
@@ -87,7 +87,9 @@ export async function dispatchTool(toolName, input, ctx) {
|
|
|
87
87
|
reasons: budgetCheck.reasons,
|
|
88
88
|
},
|
|
89
89
|
usage: snapshotUsage(ctx),
|
|
90
|
-
|
|
90
|
+
sessionId: ctx.sessionId,
|
|
91
|
+
runId: ctx.runId,
|
|
92
|
+
}));
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
throw new BudgetExhaustedError(budgetCheck);
|
|
@@ -95,13 +97,14 @@ export async function dispatchTool(toolName, input, ctx) {
|
|
|
95
97
|
|
|
96
98
|
// Emit budget warnings
|
|
97
99
|
if (budgetCheck.warnings.length > 0 && ctx.onEvent) {
|
|
98
|
-
ctx.onEvent({
|
|
99
|
-
stream: "sl_event",
|
|
100
|
+
ctx.onEvent(createAgentEvent({
|
|
100
101
|
event: "budget_warning",
|
|
101
102
|
agent: ctx.agentIdentity,
|
|
102
103
|
payload: { warnings: budgetCheck.warnings },
|
|
103
104
|
usage: snapshotUsage(ctx),
|
|
104
|
-
|
|
105
|
+
sessionId: ctx.sessionId,
|
|
106
|
+
runId: ctx.runId,
|
|
107
|
+
}));
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
// 2. Emit tool_call event
|
|
@@ -121,13 +124,14 @@ export async function dispatchTool(toolName, input, ctx) {
|
|
|
121
124
|
await safeAppendEvent(ctx, callEvent);
|
|
122
125
|
|
|
123
126
|
if (ctx.onEvent) {
|
|
124
|
-
ctx.onEvent({
|
|
125
|
-
stream: "sl_event",
|
|
127
|
+
ctx.onEvent(createAgentEvent({
|
|
126
128
|
event: "tool_call",
|
|
127
129
|
agent: ctx.agentIdentity,
|
|
128
130
|
payload: { tool: toolName, input: sanitizeInput(toolName, input) },
|
|
129
131
|
usage: snapshotUsage(ctx),
|
|
130
|
-
|
|
132
|
+
sessionId: ctx.sessionId,
|
|
133
|
+
runId: ctx.runId,
|
|
134
|
+
}));
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
// 3. Execute
|
|
@@ -147,6 +151,12 @@ export async function dispatchTool(toolName, input, ctx) {
|
|
|
147
151
|
ctx.lastToolCallAt = Date.now();
|
|
148
152
|
ctx.lastToolName = toolName;
|
|
149
153
|
|
|
154
|
+
// Track confirmed file reads for coverage accounting
|
|
155
|
+
if (!error && toolName === "FileRead") {
|
|
156
|
+
const readPath = input?.file_path || input?.filePath || input?.path || "";
|
|
157
|
+
if (readPath) ctx.usage.filesRead.add(readPath);
|
|
158
|
+
}
|
|
159
|
+
|
|
150
160
|
// 5. Emit tool_result event
|
|
151
161
|
const resultEvent = {
|
|
152
162
|
eventType: "tool_call",
|
|
@@ -168,8 +178,7 @@ export async function dispatchTool(toolName, input, ctx) {
|
|
|
168
178
|
await safeAppendEvent(ctx, resultEvent);
|
|
169
179
|
|
|
170
180
|
if (ctx.onEvent) {
|
|
171
|
-
ctx.onEvent({
|
|
172
|
-
stream: "sl_event",
|
|
181
|
+
ctx.onEvent(createAgentEvent({
|
|
173
182
|
event: "tool_result",
|
|
174
183
|
agent: ctx.agentIdentity,
|
|
175
184
|
payload: {
|
|
@@ -179,7 +188,9 @@ export async function dispatchTool(toolName, input, ctx) {
|
|
|
179
188
|
error: error?.message,
|
|
180
189
|
},
|
|
181
190
|
usage: snapshotUsage(ctx),
|
|
182
|
-
|
|
191
|
+
sessionId: ctx.sessionId,
|
|
192
|
+
runId: ctx.runId,
|
|
193
|
+
}));
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
if (error) throw error;
|
|
@@ -248,6 +259,7 @@ export function createAgentContext({
|
|
|
248
259
|
outputTokens: 0,
|
|
249
260
|
toolCalls: 0,
|
|
250
261
|
runtimeMs: 0,
|
|
262
|
+
filesRead: new Set(),
|
|
251
263
|
},
|
|
252
264
|
sessionId: sessionId || randomUUID(),
|
|
253
265
|
runId: runId || `jules-${Date.now()}-${randomUUID().slice(0, 8)}`,
|
|
@@ -265,6 +277,7 @@ function snapshotUsage(ctx) {
|
|
|
265
277
|
outputTokens: ctx.usage.outputTokens,
|
|
266
278
|
toolCalls: ctx.usage.toolCalls,
|
|
267
279
|
durationMs: Date.now() - ctx.startedAt,
|
|
280
|
+
filesRead: [...(ctx.usage.filesRead || [])],
|
|
268
281
|
};
|
|
269
282
|
}
|
|
270
283
|
|
|
@@ -1,180 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { PathGuardError, resolveGuardedPath } from "./path-guards.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* String replacement in files with uniqueness enforcement and diff generation.
|
|
8
|
-
* Designed for use inside a worktree — validates path is within allowed directory.
|
|
9
|
-
*
|
|
10
|
-
* @param {object} input
|
|
11
|
-
* @param {string} input.file_path - Absolute path to the file to modify.
|
|
12
|
-
* @param {string} input.old_string - Exact text to replace.
|
|
13
|
-
* @param {string} input.new_string - Replacement text (must differ from old_string).
|
|
14
|
-
* @param {boolean} [input.replace_all] - Replace all occurrences (default: false).
|
|
15
|
-
* @param {string} [input.allowed_root] - Root directory edits are permitted in (worktree guard).
|
|
16
|
-
* @returns {{ filePath, diff, occurrencesFound, occurrencesReplaced, linesChanged }}
|
|
17
|
-
*/
|
|
18
|
-
export function fileEdit(input) {
|
|
19
|
-
if (!input.old_string && input.old_string !== "") {
|
|
20
|
-
throw new FileEditError("old_string is required.");
|
|
21
|
-
}
|
|
22
|
-
if (input.new_string === undefined || input.new_string === null) {
|
|
23
|
-
throw new FileEditError("new_string is required.");
|
|
24
|
-
}
|
|
25
|
-
if (input.old_string === input.new_string) {
|
|
26
|
-
throw new FileEditError("old_string and new_string must be different.");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let filePath;
|
|
30
|
-
try {
|
|
31
|
-
const guarded = resolveGuardedPath({
|
|
32
|
-
filePath: input.file_path,
|
|
33
|
-
allowedRoot: input.allowed_root || undefined,
|
|
34
|
-
});
|
|
35
|
-
filePath = guarded.resolvedPath;
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (error instanceof PathGuardError) {
|
|
38
|
-
throw new FileEditError(error.message);
|
|
39
|
-
}
|
|
40
|
-
if (error instanceof FileEditError) {
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
throw new FileEditError(`Cannot access path: ${error.message}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Read current content
|
|
47
|
-
let content;
|
|
48
|
-
try {
|
|
49
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
50
|
-
} catch (err) {
|
|
51
|
-
if (err.code === "ENOENT") {
|
|
52
|
-
throw new FileEditError(`File not found: ${filePath}`);
|
|
53
|
-
}
|
|
54
|
-
throw new FileEditError(`Cannot read file: ${err.message}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Count occurrences
|
|
58
|
-
const occurrences = countOccurrences(content, input.old_string);
|
|
59
|
-
if (occurrences === 0) {
|
|
60
|
-
throw new FileEditError(
|
|
61
|
-
`old_string not found in ${filePath}. Verify the exact text including whitespace and indentation.`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
if (occurrences > 1 && !input.replace_all) {
|
|
65
|
-
throw new FileEditError(
|
|
66
|
-
`old_string found ${occurrences} times in ${filePath}. Use replace_all: true to replace all, or provide more surrounding context to make it unique.`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Perform replacement
|
|
71
|
-
const replaceCount = input.replace_all ? occurrences : 1;
|
|
72
|
-
let newContent;
|
|
73
|
-
if (input.replace_all) {
|
|
74
|
-
newContent = content.split(input.old_string).join(input.new_string);
|
|
75
|
-
} else {
|
|
76
|
-
const idx = content.indexOf(input.old_string);
|
|
77
|
-
newContent =
|
|
78
|
-
content.slice(0, idx) +
|
|
79
|
-
input.new_string +
|
|
80
|
-
content.slice(idx + input.old_string.length);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Generate unified diff for display
|
|
84
|
-
const diff = generateUnifiedDiff(filePath, content, newContent);
|
|
85
|
-
|
|
86
|
-
// Count changed lines
|
|
87
|
-
const oldLines = content.split("\n").length;
|
|
88
|
-
const newLines = newContent.split("\n").length;
|
|
89
|
-
const linesChanged = Math.abs(newLines - oldLines) +
|
|
90
|
-
countDiffLines(content, newContent);
|
|
91
|
-
|
|
92
|
-
// Write atomically: temp file + rename
|
|
93
|
-
const tmpPath = filePath + `.sl-edit-${Date.now()}`;
|
|
94
|
-
fs.writeFileSync(tmpPath, newContent, "utf-8");
|
|
95
|
-
fs.renameSync(tmpPath, filePath);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
filePath,
|
|
99
|
-
diff,
|
|
100
|
-
occurrencesFound: occurrences,
|
|
101
|
-
occurrencesReplaced: replaceCount,
|
|
102
|
-
linesChanged,
|
|
103
|
-
beforeHash: hashContent(content),
|
|
104
|
-
afterHash: hashContent(newContent),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function countOccurrences(haystack, needle) {
|
|
109
|
-
if (!needle) return 0;
|
|
110
|
-
let count = 0;
|
|
111
|
-
let idx = 0;
|
|
112
|
-
while ((idx = haystack.indexOf(needle, idx)) !== -1) {
|
|
113
|
-
count++;
|
|
114
|
-
idx += needle.length;
|
|
115
|
-
}
|
|
116
|
-
return count;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function generateUnifiedDiff(filePath, oldContent, newContent) {
|
|
120
|
-
const oldLines = oldContent.split("\n");
|
|
121
|
-
const newLines = newContent.split("\n");
|
|
122
|
-
const diffLines = [];
|
|
123
|
-
|
|
124
|
-
diffLines.push(`--- a/${path.basename(filePath)}`);
|
|
125
|
-
diffLines.push(`+++ b/${path.basename(filePath)}`);
|
|
126
|
-
|
|
127
|
-
// Simple line-by-line diff (not full Myers — sufficient for review display)
|
|
128
|
-
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
129
|
-
let chunkStart = -1;
|
|
130
|
-
let chunkOld = [];
|
|
131
|
-
let chunkNew = [];
|
|
132
|
-
|
|
133
|
-
for (let i = 0; i < maxLines; i++) {
|
|
134
|
-
const oldLine = i < oldLines.length ? oldLines[i] : undefined;
|
|
135
|
-
const newLine = i < newLines.length ? newLines[i] : undefined;
|
|
136
|
-
|
|
137
|
-
if (oldLine !== newLine) {
|
|
138
|
-
if (chunkStart === -1) chunkStart = i;
|
|
139
|
-
if (oldLine !== undefined) chunkOld.push(`-${oldLine}`);
|
|
140
|
-
if (newLine !== undefined) chunkNew.push(`+${newLine}`);
|
|
141
|
-
} else if (chunkStart !== -1) {
|
|
142
|
-
// Flush chunk
|
|
143
|
-
diffLines.push(`@@ -${chunkStart + 1},${chunkOld.length} +${chunkStart + 1},${chunkNew.length} @@`);
|
|
144
|
-
diffLines.push(...chunkOld, ...chunkNew);
|
|
145
|
-
chunkStart = -1;
|
|
146
|
-
chunkOld = [];
|
|
147
|
-
chunkNew = [];
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Flush final chunk
|
|
152
|
-
if (chunkStart !== -1) {
|
|
153
|
-
diffLines.push(`@@ -${chunkStart + 1},${chunkOld.length} +${chunkStart + 1},${chunkNew.length} @@`);
|
|
154
|
-
diffLines.push(...chunkOld, ...chunkNew);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return diffLines.join("\n");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function countDiffLines(oldContent, newContent) {
|
|
161
|
-
const oldLines = oldContent.split("\n");
|
|
162
|
-
const newLines = newContent.split("\n");
|
|
163
|
-
let changed = 0;
|
|
164
|
-
const max = Math.min(oldLines.length, newLines.length);
|
|
165
|
-
for (let i = 0; i < max; i++) {
|
|
166
|
-
if (oldLines[i] !== newLines[i]) changed++;
|
|
167
|
-
}
|
|
168
|
-
return changed;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function hashContent(content) {
|
|
172
|
-
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export class FileEditError extends Error {
|
|
176
|
-
constructor(message) {
|
|
177
|
-
super(message);
|
|
178
|
-
this.name = "FileEditError";
|
|
179
|
-
}
|
|
180
|
-
}
|
|
1
|
+
// Re-export from shared tools. FileEdit is not Jules-specific.
|
|
2
|
+
export { fileEdit, FileEditError } from "../../shared-tools/file-edit.js";
|
|
@@ -1,100 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { PathGuardError, resolveGuardedPath } from "./path-guards.js";
|
|
4
|
-
|
|
5
|
-
const MAX_RESULT_CHARS = 5000;
|
|
6
|
-
const BINARY_EXTENSIONS = new Set([
|
|
7
|
-
".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico", ".svg",
|
|
8
|
-
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
|
9
|
-
".mp3", ".mp4", ".ogg", ".webm", ".wav",
|
|
10
|
-
".zip", ".tar", ".gz", ".br", ".zst",
|
|
11
|
-
".pdf", ".wasm", ".node", ".exe", ".dll", ".so", ".dylib",
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Read a file with line numbers, offset/limit pagination, and binary detection.
|
|
16
|
-
* Returns { filePath, content, numLines, startLine, totalLines, truncated }.
|
|
17
|
-
*/
|
|
18
|
-
export function fileRead(input) {
|
|
19
|
-
const filePath = resolveAndValidatePath(input.file_path, input.allowed_root);
|
|
20
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
21
|
-
|
|
22
|
-
if (BINARY_EXTENSIONS.has(ext)) {
|
|
23
|
-
const stat = fs.statSync(filePath);
|
|
24
|
-
return {
|
|
25
|
-
filePath,
|
|
26
|
-
content: `[Binary file: ${ext}, ${stat.size} bytes. Use a specialized viewer.]`,
|
|
27
|
-
numLines: 0,
|
|
28
|
-
startLine: 0,
|
|
29
|
-
totalLines: 0,
|
|
30
|
-
truncated: false,
|
|
31
|
-
binary: true,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let raw;
|
|
36
|
-
try {
|
|
37
|
-
raw = fs.readFileSync(filePath, "utf-8");
|
|
38
|
-
} catch (err) {
|
|
39
|
-
if (err.code === "ENOENT") {
|
|
40
|
-
throw new FileReadError(`File not found: ${filePath}`);
|
|
41
|
-
}
|
|
42
|
-
if (err.code === "EISDIR") {
|
|
43
|
-
throw new FileReadError(`Path is a directory, not a file: ${filePath}`);
|
|
44
|
-
}
|
|
45
|
-
throw new FileReadError(`Cannot read file: ${err.message}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const allLines = raw.split("\n");
|
|
49
|
-
const totalLines = allLines.length;
|
|
50
|
-
const offset = Math.max(0, input.offset ?? 0);
|
|
51
|
-
const limit = input.limit ?? 2000;
|
|
52
|
-
const sliced = allLines.slice(offset, offset + limit);
|
|
53
|
-
const startLine = offset + 1;
|
|
54
|
-
|
|
55
|
-
const numbered = sliced.map(
|
|
56
|
-
(line, i) => `${String(startLine + i).padStart(6)}\t${line}`,
|
|
57
|
-
);
|
|
58
|
-
let content = numbered.join("\n");
|
|
59
|
-
let truncated = false;
|
|
60
|
-
|
|
61
|
-
if (content.length > MAX_RESULT_CHARS) {
|
|
62
|
-
content = content.slice(0, MAX_RESULT_CHARS) + "\n[... truncated]";
|
|
63
|
-
truncated = true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
filePath,
|
|
68
|
-
content,
|
|
69
|
-
numLines: sliced.length,
|
|
70
|
-
startLine,
|
|
71
|
-
totalLines,
|
|
72
|
-
truncated,
|
|
73
|
-
binary: false,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export class FileReadError extends Error {
|
|
78
|
-
constructor(message) {
|
|
79
|
-
super(message);
|
|
80
|
-
this.name = "FileReadError";
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function resolveAndValidatePath(filePath, allowedRoot) {
|
|
85
|
-
try {
|
|
86
|
-
const guarded = resolveGuardedPath({
|
|
87
|
-
filePath,
|
|
88
|
-
allowedRoot: allowedRoot || undefined,
|
|
89
|
-
});
|
|
90
|
-
return guarded.resolvedPath;
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (error instanceof PathGuardError) {
|
|
93
|
-
throw new FileReadError(error.message);
|
|
94
|
-
}
|
|
95
|
-
if (error instanceof FileReadError) {
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
throw new FileReadError(`Cannot access path: ${error.message}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
1
|
+
// Re-export from shared tools. FileRead is not Jules-specific.
|
|
2
|
+
export { fileRead, FileReadError } from "../../shared-tools/file-read.js";
|
|
@@ -1,168 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const MAX_RESULTS = 200;
|
|
5
|
-
const IGNORE_DIRS = new Set([
|
|
6
|
-
".git", "node_modules", ".next", "dist", "build", "coverage",
|
|
7
|
-
".turbo", ".idea", ".vscode", "__pycache__", ".venv", ".cache",
|
|
8
|
-
".parcel-cache", ".svelte-kit", ".nuxt", ".output", ".vercel",
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Fast file pattern matching sorted by modification time (newest first).
|
|
13
|
-
*
|
|
14
|
-
* @param {object} input
|
|
15
|
-
* @param {string} input.pattern - Glob pattern (e.g., "**\/*.tsx", "src/**\/*.js").
|
|
16
|
-
* @param {string} [input.path] - Directory to search (default: cwd).
|
|
17
|
-
* @param {number} [input.limit] - Max results (default: 200).
|
|
18
|
-
* @returns {{ filenames, numFiles, truncated, durationMs }}
|
|
19
|
-
*/
|
|
20
|
-
export function glob(input) {
|
|
21
|
-
if (!input.pattern || typeof input.pattern !== "string") {
|
|
22
|
-
throw new GlobError("pattern is required and must be a non-empty string.");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const searchPath = input.path ? path.resolve(input.path) : process.cwd();
|
|
26
|
-
const limit = input.limit ?? MAX_RESULTS;
|
|
27
|
-
const startMs = Date.now();
|
|
28
|
-
|
|
29
|
-
if (!fs.existsSync(searchPath)) {
|
|
30
|
-
throw new GlobError(`Directory not found: ${searchPath}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const stat = fs.statSync(searchPath);
|
|
34
|
-
if (!stat.isDirectory()) {
|
|
35
|
-
throw new GlobError(`Path is not a directory: ${searchPath}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const matcher = buildMatcher(input.pattern);
|
|
39
|
-
const ignorePatterns = loadIgnorePatterns(searchPath);
|
|
40
|
-
const results = [];
|
|
41
|
-
|
|
42
|
-
walk(searchPath, searchPath, matcher, ignorePatterns, results, limit);
|
|
43
|
-
|
|
44
|
-
// Sort by mtime descending (newest first)
|
|
45
|
-
results.sort((a, b) => b.mtime - a.mtime);
|
|
46
|
-
|
|
47
|
-
const truncated = results.length >= limit;
|
|
48
|
-
const filenames = results.map((r) => r.relativePath);
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
filenames,
|
|
52
|
-
numFiles: filenames.length,
|
|
53
|
-
truncated,
|
|
54
|
-
durationMs: Date.now() - startMs,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function walk(rootPath, currentPath, matcher, ignorePatterns, results, limit) {
|
|
59
|
-
let entries;
|
|
60
|
-
try {
|
|
61
|
-
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
62
|
-
} catch {
|
|
63
|
-
return; // skip unreadable directories
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
for (const entry of entries) {
|
|
67
|
-
if (results.length >= limit) return;
|
|
68
|
-
|
|
69
|
-
const name = entry.name;
|
|
70
|
-
if (IGNORE_DIRS.has(name)) continue;
|
|
71
|
-
|
|
72
|
-
const fullPath = path.join(currentPath, name);
|
|
73
|
-
const relativePath = path.relative(rootPath, fullPath);
|
|
74
|
-
|
|
75
|
-
if (ignorePatterns.some((p) => p(relativePath))) continue;
|
|
76
|
-
|
|
77
|
-
if (entry.isDirectory()) {
|
|
78
|
-
walk(rootPath, fullPath, matcher, ignorePatterns, results, limit);
|
|
79
|
-
} else if (entry.isFile() && matcher(relativePath)) {
|
|
80
|
-
let mtime = 0;
|
|
81
|
-
try {
|
|
82
|
-
mtime = fs.statSync(fullPath).mtimeMs;
|
|
83
|
-
} catch { /* use 0 if stat fails */ }
|
|
84
|
-
results.push({ relativePath, mtime });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Build a filename matcher from a glob pattern.
|
|
91
|
-
* Supports: *.ext, **\/*.ext, *.{ext1,ext2}, prefix*, *suffix
|
|
92
|
-
*/
|
|
93
|
-
function buildMatcher(pattern) {
|
|
94
|
-
// Handle brace expansion: *.{ts,tsx} → ["*.ts", "*.tsx"]
|
|
95
|
-
const expanded = expandBraces(pattern);
|
|
96
|
-
|
|
97
|
-
const matchers = expanded.map((p) => {
|
|
98
|
-
// ** recursive match
|
|
99
|
-
if (p.includes("**/")) {
|
|
100
|
-
const suffix = p.split("**/").pop();
|
|
101
|
-
const suffixMatcher = buildSimpleMatcher(suffix);
|
|
102
|
-
return (filepath) => {
|
|
103
|
-
const basename = path.basename(filepath);
|
|
104
|
-
const segments = filepath.split(path.sep);
|
|
105
|
-
return segments.some((_, i) =>
|
|
106
|
-
suffixMatcher(segments.slice(i).join(path.sep)),
|
|
107
|
-
) || suffixMatcher(basename);
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
return buildSimpleMatcher(p);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return (filepath) => matchers.some((m) => m(filepath));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function buildSimpleMatcher(pattern) {
|
|
117
|
-
if (pattern.startsWith("*.")) {
|
|
118
|
-
const ext = pattern.slice(1);
|
|
119
|
-
return (filepath) => filepath.endsWith(ext) || path.basename(filepath).endsWith(ext);
|
|
120
|
-
}
|
|
121
|
-
if (pattern.endsWith("*")) {
|
|
122
|
-
const prefix = pattern.slice(0, -1);
|
|
123
|
-
return (filepath) => filepath.startsWith(prefix) || path.basename(filepath).startsWith(prefix);
|
|
124
|
-
}
|
|
125
|
-
if (pattern.includes("*")) {
|
|
126
|
-
const regex = new RegExp(
|
|
127
|
-
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
|
|
128
|
-
);
|
|
129
|
-
return (filepath) => regex.test(filepath) || regex.test(path.basename(filepath));
|
|
130
|
-
}
|
|
131
|
-
return (filepath) => filepath === pattern || path.basename(filepath) === pattern;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function expandBraces(pattern) {
|
|
135
|
-
const braceMatch = pattern.match(/\{([^}]+)\}/);
|
|
136
|
-
if (!braceMatch) return [pattern];
|
|
137
|
-
const alternatives = braceMatch[1].split(",");
|
|
138
|
-
return alternatives.map((alt) =>
|
|
139
|
-
pattern.replace(braceMatch[0], alt.trim()),
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function loadIgnorePatterns(rootPath) {
|
|
144
|
-
const patterns = [];
|
|
145
|
-
const gitignorePath = path.join(rootPath, ".gitignore");
|
|
146
|
-
const slignorePath = path.join(rootPath, ".sentinelayerignore");
|
|
147
|
-
|
|
148
|
-
for (const ignorePath of [gitignorePath, slignorePath]) {
|
|
149
|
-
try {
|
|
150
|
-
const content = fs.readFileSync(ignorePath, "utf-8");
|
|
151
|
-
for (const line of content.split("\n")) {
|
|
152
|
-
const trimmed = line.trim();
|
|
153
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
154
|
-
const matcher = buildSimpleMatcher(trimmed);
|
|
155
|
-
patterns.push(matcher);
|
|
156
|
-
}
|
|
157
|
-
} catch { /* ignore missing files */ }
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return patterns;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export class GlobError extends Error {
|
|
164
|
-
constructor(message) {
|
|
165
|
-
super(message);
|
|
166
|
-
this.name = "GlobError";
|
|
167
|
-
}
|
|
168
|
-
}
|
|
1
|
+
// Re-export from shared tools. Glob is not Jules-specific.
|
|
2
|
+
export { glob, GlobError } from "../../shared-tools/glob.js";
|