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.
Files changed (72) hide show
  1. package/README.md +16 -18
  2. package/package.json +7 -6
  3. package/src/agents/jules/config/definition.js +13 -62
  4. package/src/agents/jules/config/system-prompt.js +8 -1
  5. package/src/agents/jules/fix-cycle.js +12 -372
  6. package/src/agents/jules/loop.js +116 -26
  7. package/src/agents/jules/pulse.js +10 -327
  8. package/src/agents/jules/stream.js +13 -12
  9. package/src/agents/jules/swarm/orchestrator.js +3 -3
  10. package/src/agents/jules/swarm/sub-agent.js +6 -3
  11. package/src/agents/jules/tools/aidenid-email.js +189 -0
  12. package/src/agents/jules/tools/auth-audit.js +1187 -45
  13. package/src/agents/jules/tools/dispatch.js +25 -12
  14. package/src/agents/jules/tools/file-edit.js +2 -180
  15. package/src/agents/jules/tools/file-read.js +2 -100
  16. package/src/agents/jules/tools/glob.js +2 -168
  17. package/src/agents/jules/tools/grep.js +2 -228
  18. package/src/agents/jules/tools/path-guards.js +2 -161
  19. package/src/agents/jules/tools/runtime-audit.js +6 -2
  20. package/src/agents/jules/tools/shell.js +2 -383
  21. package/src/agents/persona-visuals.js +64 -0
  22. package/src/agents/shared-tools/dispatch-core.js +320 -0
  23. package/src/agents/shared-tools/file-edit.js +180 -0
  24. package/src/agents/shared-tools/file-read.js +100 -0
  25. package/src/agents/shared-tools/glob.js +168 -0
  26. package/src/agents/shared-tools/grep.js +228 -0
  27. package/src/agents/shared-tools/index.js +46 -0
  28. package/src/agents/shared-tools/path-guards.js +161 -0
  29. package/src/agents/shared-tools/shell.js +383 -0
  30. package/src/ai/aidenid.js +56 -7
  31. package/src/ai/client.js +45 -0
  32. package/src/ai/proxy.js +137 -0
  33. package/src/auth/gate.js +290 -16
  34. package/src/auth/http.js +450 -39
  35. package/src/auth/service.js +262 -47
  36. package/src/auth/session-store.js +475 -21
  37. package/src/cli.js +5 -0
  38. package/src/commands/audit.js +13 -8
  39. package/src/commands/auth.js +53 -9
  40. package/src/commands/omargate.js +10 -2
  41. package/src/commands/scan.js +10 -4
  42. package/src/commands/session.js +590 -0
  43. package/src/commands/spec.js +62 -0
  44. package/src/commands/watch.js +3 -2
  45. package/src/daemon/assignment-ledger.js +196 -0
  46. package/src/daemon/error-worker.js +599 -16
  47. package/src/daemon/fix-cycle.js +384 -0
  48. package/src/daemon/ingest-refresh.js +10 -9
  49. package/src/daemon/jira-lifecycle.js +135 -0
  50. package/src/daemon/pulse.js +327 -0
  51. package/src/daemon/scope-engine.js +1068 -0
  52. package/src/events/schema.js +190 -0
  53. package/src/interactive/index.js +18 -16
  54. package/src/legacy-cli.js +606 -37
  55. package/src/prompt/generator.js +19 -1
  56. package/src/review/ai-review.js +11 -1
  57. package/src/review/local-review.js +75 -19
  58. package/src/review/omargate-interactive.js +68 -0
  59. package/src/review/omargate-orchestrator.js +404 -0
  60. package/src/review/persona-prompts.js +296 -0
  61. package/src/review/scan-modes.js +48 -0
  62. package/src/scan/generator.js +1 -1
  63. package/src/session/agent-registry.js +352 -0
  64. package/src/session/daemon.js +801 -0
  65. package/src/session/paths.js +33 -0
  66. package/src/session/runtime-bridge.js +739 -0
  67. package/src/session/store.js +388 -0
  68. package/src/session/stream.js +325 -0
  69. package/src/spec/generator.js +100 -0
  70. package/src/telemetry/session-tracker.js +148 -32
  71. package/src/telemetry/sync.js +6 -2
  72. 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
- import fs from "node:fs";
2
- import path from "node:path";
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
- import fs from "node:fs";
2
- import path from "node:path";
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
- import fs from "node:fs";
2
- import path from "node:path";
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";