codeep 1.1.36 → 1.2.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 (36) hide show
  1. package/README.md +90 -4
  2. package/dist/api/index.js +64 -2
  3. package/dist/renderer/App.d.ts +5 -0
  4. package/dist/renderer/App.js +164 -227
  5. package/dist/renderer/components/Export.d.ts +22 -0
  6. package/dist/renderer/components/Export.js +64 -0
  7. package/dist/renderer/components/Help.js +5 -1
  8. package/dist/renderer/components/Logout.d.ts +29 -0
  9. package/dist/renderer/components/Logout.js +91 -0
  10. package/dist/renderer/components/Search.d.ts +30 -0
  11. package/dist/renderer/components/Search.js +83 -0
  12. package/dist/renderer/components/Settings.js +20 -0
  13. package/dist/renderer/components/Status.d.ts +6 -0
  14. package/dist/renderer/components/Status.js +20 -1
  15. package/dist/renderer/main.js +296 -142
  16. package/dist/utils/agent.d.ts +5 -0
  17. package/dist/utils/agent.js +238 -3
  18. package/dist/utils/agent.test.d.ts +1 -0
  19. package/dist/utils/agent.test.js +250 -0
  20. package/dist/utils/diffPreview.js +104 -35
  21. package/dist/utils/gitignore.d.ts +24 -0
  22. package/dist/utils/gitignore.js +161 -0
  23. package/dist/utils/gitignore.test.d.ts +1 -0
  24. package/dist/utils/gitignore.test.js +167 -0
  25. package/dist/utils/skills.d.ts +21 -0
  26. package/dist/utils/skills.js +51 -0
  27. package/dist/utils/smartContext.js +8 -0
  28. package/dist/utils/smartContext.test.d.ts +1 -0
  29. package/dist/utils/smartContext.test.js +382 -0
  30. package/dist/utils/tokenTracker.d.ts +52 -0
  31. package/dist/utils/tokenTracker.js +86 -0
  32. package/dist/utils/tools.d.ts +16 -0
  33. package/dist/utils/tools.js +146 -19
  34. package/dist/utils/tools.test.d.ts +1 -0
  35. package/dist/utils/tools.test.js +664 -0
  36. package/package.json +1 -1
@@ -131,50 +131,119 @@ function finalizeHunk(hunk) {
131
131
  hunk.newLines = hunk.lines.filter(l => l.type !== 'remove').length;
132
132
  }
133
133
  /**
134
- * Simple LCS algorithm for diff
134
+ * Myers diff algorithm finds the shortest edit script (SES).
135
+ * Returns matched line pairs as [oldIndex, newIndex][].
136
+ * The final entry is the sentinel [old.length, new.length].
135
137
  */
136
- function longestCommonSubsequence(old, newArr) {
137
- const result = [];
138
- let oldIdx = 0;
139
- let newIdx = 0;
140
- while (oldIdx < old.length && newIdx < newArr.length) {
141
- if (old[oldIdx] === newArr[newIdx]) {
142
- result.push([oldIdx, newIdx]);
143
- oldIdx++;
144
- newIdx++;
145
- }
146
- else {
147
- // Try to find match
148
- let foundOld = -1;
149
- let foundNew = -1;
150
- // Look ahead in new array
151
- for (let i = newIdx + 1; i < Math.min(newIdx + 10, newArr.length); i++) {
152
- if (old[oldIdx] === newArr[i]) {
153
- foundNew = i;
154
- break;
155
- }
138
+ function longestCommonSubsequence(oldArr, newArr) {
139
+ const N = oldArr.length;
140
+ const M = newArr.length;
141
+ // Trivial cases
142
+ if (N === 0 && M === 0)
143
+ return [[0, 0]];
144
+ if (N === 0)
145
+ return [[0, M]];
146
+ if (M === 0)
147
+ return [[N, M]];
148
+ const MAX = N + M;
149
+ // V[k] stores the furthest-reaching x on diagonal k.
150
+ // Diagonals range from -MAX to +MAX, index with offset MAX.
151
+ const size = 2 * MAX + 1;
152
+ const V = new Int32Array(size);
153
+ V.fill(-1);
154
+ V[MAX + 1] = 0; // V[1] = 0
155
+ // Store each step's V snapshot for backtracking
156
+ const trace = [];
157
+ let found = false;
158
+ for (let d = 0; d <= MAX; d++) {
159
+ // Save current V before mutation
160
+ trace.push(V.slice());
161
+ for (let k = -d; k <= d; k += 2) {
162
+ const kIdx = k + MAX;
163
+ let x;
164
+ if (k === -d || (k !== d && V[kIdx - 1] < V[kIdx + 1])) {
165
+ x = V[kIdx + 1]; // move down
156
166
  }
157
- // Look ahead in old array
158
- for (let i = oldIdx + 1; i < Math.min(oldIdx + 10, old.length); i++) {
159
- if (old[i] === newArr[newIdx]) {
160
- foundOld = i;
161
- break;
162
- }
167
+ else {
168
+ x = V[kIdx - 1] + 1; // move right
163
169
  }
164
- if (foundNew !== -1 && (foundOld === -1 || foundNew - newIdx < foundOld - oldIdx)) {
165
- newIdx = foundNew;
170
+ let y = x - k;
171
+ // Follow diagonal (matching lines)
172
+ while (x < N && y < M && oldArr[x] === newArr[y]) {
173
+ x++;
174
+ y++;
166
175
  }
167
- else if (foundOld !== -1) {
168
- oldIdx = foundOld;
176
+ V[kIdx] = x;
177
+ if (x >= N && y >= M) {
178
+ found = true;
179
+ break;
180
+ }
181
+ }
182
+ if (found)
183
+ break;
184
+ }
185
+ // Backtrack through trace to recover the edit path
186
+ let x = N;
187
+ let y = M;
188
+ const edits = [];
189
+ for (let d = trace.length - 1; d >= 0; d--) {
190
+ const Vd = trace[d];
191
+ const k = x - y;
192
+ const kIdx = k + MAX;
193
+ let prevK;
194
+ if (k === -d || (k !== d && Vd[kIdx - 1] < Vd[kIdx + 1])) {
195
+ prevK = k + 1; // came from above (insertion)
196
+ }
197
+ else {
198
+ prevK = k - 1; // came from left (deletion)
199
+ }
200
+ const prevX = Vd[prevK + MAX];
201
+ const prevY = prevX - prevK;
202
+ // Record diagonal moves (matches) between prevX,prevY and x,y
203
+ edits.push({ prevX, prevY, x, y });
204
+ x = prevX;
205
+ y = prevY;
206
+ }
207
+ edits.reverse();
208
+ // Extract matched pairs from the diagonal segments
209
+ const result = [];
210
+ for (const edit of edits) {
211
+ // The diagonal portion: from (edit.prevX, edit.prevY) moving diagonally to where the non-diagonal step leads to (edit.x, edit.y)
212
+ // The non-diagonal step comes first, then diagonal. So diagonal is from (startX, startY) to (edit.x, edit.y)
213
+ // where startX/startY is one step from prevX/prevY.
214
+ let sx = edit.prevX;
215
+ let sy = edit.prevY;
216
+ // The non-diagonal step
217
+ if (sx < edit.x && sy < edit.y) {
218
+ // Both moved — this is diagonal only if lines match
219
+ // Actually in Myers, the non-diagonal step is exactly 1 move (right or down)
220
+ // followed by diagonal matches. Let's just skip non-diagonal and collect diagonals.
221
+ }
222
+ // Determine the start of diagonal: prevX,prevY + one step
223
+ const k = edit.x - edit.y;
224
+ const prevK = edit.prevX - edit.prevY;
225
+ if (prevK !== k) {
226
+ // There was a non-diagonal step
227
+ if (k === prevK + 1) {
228
+ // Moved right (deletion in old)
229
+ sx = edit.prevX + 1;
230
+ sy = edit.prevY;
169
231
  }
170
232
  else {
171
- oldIdx++;
172
- newIdx++;
233
+ // Moved down (insertion in new)
234
+ sx = edit.prevX;
235
+ sy = edit.prevY + 1;
173
236
  }
174
237
  }
238
+ // Collect diagonal matches
239
+ while (sx < edit.x && sy < edit.y) {
240
+ result.push([sx, sy]);
241
+ sx++;
242
+ sy++;
243
+ }
175
244
  }
176
- // Add end markers
177
- result.push([old.length, newArr.length]);
245
+ // Sentinel
246
+ result.push([N, M]);
178
247
  return result;
179
248
  }
180
249
  /**
@@ -0,0 +1,24 @@
1
+ /**
2
+ * .gitignore parser — loads ignore patterns and tests file paths against them.
3
+ */
4
+ export interface IgnoreRules {
5
+ patterns: IgnorePattern[];
6
+ projectRoot: string;
7
+ }
8
+ interface IgnorePattern {
9
+ regex: RegExp;
10
+ negated: boolean;
11
+ }
12
+ /**
13
+ * Load .gitignore rules from a project root.
14
+ * Falls back to built-in ignores if no .gitignore exists.
15
+ */
16
+ export declare function loadIgnoreRules(projectRoot: string): IgnoreRules;
17
+ /**
18
+ * Test whether a file path should be ignored.
19
+ * @param filePath Absolute or relative path
20
+ * @param rules Loaded ignore rules
21
+ * @returns true if the path should be ignored
22
+ */
23
+ export declare function isIgnored(filePath: string, rules: IgnoreRules): boolean;
24
+ export {};
@@ -0,0 +1,161 @@
1
+ /**
2
+ * .gitignore parser — loads ignore patterns and tests file paths against them.
3
+ */
4
+ import { existsSync, readFileSync } from 'fs';
5
+ import { join, relative, sep } from 'path';
6
+ /**
7
+ * Always-ignored directories (even without a .gitignore)
8
+ */
9
+ const BUILTIN_IGNORES = [
10
+ 'node_modules',
11
+ '.git',
12
+ '.codeep',
13
+ 'dist',
14
+ 'build',
15
+ '.next',
16
+ '__pycache__',
17
+ '.venv',
18
+ 'venv',
19
+ '.tox',
20
+ '.mypy_cache',
21
+ 'coverage',
22
+ '.cache',
23
+ '.turbo',
24
+ '.parcel-cache',
25
+ 'target', // Rust, Java/Maven
26
+ 'out',
27
+ '.output',
28
+ ];
29
+ /**
30
+ * Load .gitignore rules from a project root.
31
+ * Falls back to built-in ignores if no .gitignore exists.
32
+ */
33
+ export function loadIgnoreRules(projectRoot) {
34
+ const patterns = [];
35
+ // Built-in ignores always apply
36
+ for (const dir of BUILTIN_IGNORES) {
37
+ patterns.push({ regex: new RegExp(`(^|/)${escapeRegex(dir)}(/|$)`), negated: false });
38
+ }
39
+ // Parse .gitignore if it exists
40
+ const gitignorePath = join(projectRoot, '.gitignore');
41
+ if (existsSync(gitignorePath)) {
42
+ try {
43
+ const content = readFileSync(gitignorePath, 'utf-8');
44
+ const parsed = parseGitignore(content);
45
+ patterns.push(...parsed);
46
+ }
47
+ catch {
48
+ // Ignore read errors
49
+ }
50
+ }
51
+ return { patterns, projectRoot };
52
+ }
53
+ /**
54
+ * Test whether a file path should be ignored.
55
+ * @param filePath Absolute or relative path
56
+ * @param rules Loaded ignore rules
57
+ * @returns true if the path should be ignored
58
+ */
59
+ export function isIgnored(filePath, rules) {
60
+ // Normalize to forward-slash relative path
61
+ let rel = filePath;
62
+ if (filePath.startsWith(rules.projectRoot)) {
63
+ rel = relative(rules.projectRoot, filePath);
64
+ }
65
+ rel = rel.split(sep).join('/');
66
+ // Empty path is never ignored
67
+ if (!rel)
68
+ return false;
69
+ let ignored = false;
70
+ for (const pattern of rules.patterns) {
71
+ if (pattern.regex.test(rel)) {
72
+ ignored = !pattern.negated;
73
+ }
74
+ }
75
+ return ignored;
76
+ }
77
+ /**
78
+ * Parse .gitignore content into patterns.
79
+ */
80
+ function parseGitignore(content) {
81
+ const patterns = [];
82
+ for (let line of content.split('\n')) {
83
+ // Strip trailing whitespace (but not leading — significant in gitignore)
84
+ line = line.replace(/\s+$/, '');
85
+ // Skip empty lines and comments
86
+ if (!line || line.startsWith('#'))
87
+ continue;
88
+ let negated = false;
89
+ if (line.startsWith('!')) {
90
+ negated = true;
91
+ line = line.slice(1);
92
+ }
93
+ // Remove leading slash (anchored to root)
94
+ const anchored = line.startsWith('/');
95
+ if (anchored) {
96
+ line = line.slice(1);
97
+ }
98
+ // Remove trailing slash (directory-only marker — we don't distinguish)
99
+ if (line.endsWith('/')) {
100
+ line = line.slice(0, -1);
101
+ }
102
+ // Convert glob pattern to regex
103
+ const regex = globToRegex(line, anchored);
104
+ patterns.push({ regex, negated });
105
+ }
106
+ return patterns;
107
+ }
108
+ /**
109
+ * Convert a gitignore glob pattern to a RegExp.
110
+ */
111
+ function globToRegex(pattern, anchored) {
112
+ let re = '';
113
+ for (let i = 0; i < pattern.length; i++) {
114
+ const c = pattern[i];
115
+ if (c === '*') {
116
+ if (pattern[i + 1] === '*') {
117
+ // ** matches everything including /
118
+ if (pattern[i + 2] === '/') {
119
+ re += '(?:.*/)?';
120
+ i += 2;
121
+ }
122
+ else {
123
+ re += '.*';
124
+ i += 1;
125
+ }
126
+ }
127
+ else {
128
+ // * matches everything except /
129
+ re += '[^/]*';
130
+ }
131
+ }
132
+ else if (c === '?') {
133
+ re += '[^/]';
134
+ }
135
+ else if (c === '[') {
136
+ // Character class — pass through until ]
137
+ const end = pattern.indexOf(']', i + 1);
138
+ if (end !== -1) {
139
+ re += pattern.slice(i, end + 1);
140
+ i = end;
141
+ }
142
+ else {
143
+ re += escapeRegex(c);
144
+ }
145
+ }
146
+ else {
147
+ re += escapeRegex(c);
148
+ }
149
+ }
150
+ if (anchored) {
151
+ return new RegExp(`^${re}(/|$)`);
152
+ }
153
+ // Unanchored patterns match anywhere in the path
154
+ return new RegExp(`(^|/)${re}(/|$)`);
155
+ }
156
+ /**
157
+ * Escape special regex characters.
158
+ */
159
+ function escapeRegex(str) {
160
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
161
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ // Mock fs
3
+ vi.mock('fs', () => ({
4
+ existsSync: vi.fn(),
5
+ readFileSync: vi.fn(),
6
+ }));
7
+ import { existsSync, readFileSync } from 'fs';
8
+ import { loadIgnoreRules, isIgnored } from './gitignore.js';
9
+ const mockExistsSync = existsSync;
10
+ const mockReadFileSync = readFileSync;
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+ describe('loadIgnoreRules', () => {
15
+ it('returns built-in ignores when no .gitignore exists', () => {
16
+ mockExistsSync.mockReturnValue(false);
17
+ const rules = loadIgnoreRules('/project');
18
+ expect(rules.projectRoot).toBe('/project');
19
+ expect(rules.patterns.length).toBeGreaterThan(0);
20
+ // node_modules should be a built-in
21
+ expect(isIgnored('node_modules/foo.js', rules)).toBe(true);
22
+ });
23
+ it('loads .gitignore when it exists', () => {
24
+ mockExistsSync.mockReturnValue(true);
25
+ mockReadFileSync.mockReturnValue('*.log\ntmp/\n');
26
+ const rules = loadIgnoreRules('/project');
27
+ expect(isIgnored('error.log', rules)).toBe(true);
28
+ expect(isIgnored('tmp/cache', rules)).toBe(true);
29
+ });
30
+ it('handles read errors gracefully', () => {
31
+ mockExistsSync.mockReturnValue(true);
32
+ mockReadFileSync.mockImplementation(() => { throw new Error('EACCES'); });
33
+ const rules = loadIgnoreRules('/project');
34
+ // Should still have built-in ignores
35
+ expect(rules.patterns.length).toBeGreaterThan(0);
36
+ });
37
+ });
38
+ describe('isIgnored — built-in ignores', () => {
39
+ let rules;
40
+ beforeEach(() => {
41
+ mockExistsSync.mockReturnValue(false);
42
+ rules = loadIgnoreRules('/project');
43
+ });
44
+ it('ignores node_modules', () => {
45
+ expect(isIgnored('node_modules/package/index.js', rules)).toBe(true);
46
+ expect(isIgnored('/project/node_modules', rules)).toBe(true);
47
+ });
48
+ it('ignores .git', () => {
49
+ expect(isIgnored('.git/config', rules)).toBe(true);
50
+ });
51
+ it('ignores .codeep', () => {
52
+ expect(isIgnored('.codeep/state.json', rules)).toBe(true);
53
+ });
54
+ it('ignores dist and build', () => {
55
+ expect(isIgnored('dist/bundle.js', rules)).toBe(true);
56
+ expect(isIgnored('build/output.js', rules)).toBe(true);
57
+ });
58
+ it('ignores __pycache__', () => {
59
+ expect(isIgnored('src/__pycache__/module.pyc', rules)).toBe(true);
60
+ });
61
+ it('ignores .next', () => {
62
+ expect(isIgnored('.next/static/chunks/main.js', rules)).toBe(true);
63
+ });
64
+ it('ignores coverage', () => {
65
+ expect(isIgnored('coverage/lcov.info', rules)).toBe(true);
66
+ });
67
+ it('does not ignore regular source files', () => {
68
+ expect(isIgnored('src/index.ts', rules)).toBe(false);
69
+ expect(isIgnored('package.json', rules)).toBe(false);
70
+ expect(isIgnored('README.md', rules)).toBe(false);
71
+ });
72
+ it('does not ignore files that contain ignore names as substrings', () => {
73
+ expect(isIgnored('src/builder.ts', rules)).toBe(false);
74
+ expect(isIgnored('src/distribute.ts', rules)).toBe(false);
75
+ });
76
+ });
77
+ describe('isIgnored — .gitignore patterns', () => {
78
+ function rulesFrom(gitignore) {
79
+ mockExistsSync.mockReturnValue(true);
80
+ mockReadFileSync.mockReturnValue(gitignore);
81
+ return loadIgnoreRules('/project');
82
+ }
83
+ it('matches simple file patterns', () => {
84
+ const rules = rulesFrom('*.log');
85
+ expect(isIgnored('error.log', rules)).toBe(true);
86
+ expect(isIgnored('logs/debug.log', rules)).toBe(true);
87
+ expect(isIgnored('error.txt', rules)).toBe(false);
88
+ });
89
+ it('matches directory patterns', () => {
90
+ const rules = rulesFrom('tmp/');
91
+ expect(isIgnored('tmp/file.txt', rules)).toBe(true);
92
+ expect(isIgnored('src/tmp/file.txt', rules)).toBe(true);
93
+ });
94
+ it('matches anchored patterns (leading /)', () => {
95
+ const rules = rulesFrom('/config.local');
96
+ expect(isIgnored('config.local', rules)).toBe(true);
97
+ expect(isIgnored('sub/config.local', rules)).toBe(false);
98
+ });
99
+ it('matches ** glob patterns', () => {
100
+ const rules = rulesFrom('docs/**/*.md');
101
+ expect(isIgnored('docs/readme.md', rules)).toBe(true);
102
+ expect(isIgnored('docs/guides/setup.md', rules)).toBe(true);
103
+ expect(isIgnored('src/readme.md', rules)).toBe(false);
104
+ });
105
+ it('matches ? single char wildcard', () => {
106
+ const rules = rulesFrom('file?.txt');
107
+ expect(isIgnored('file1.txt', rules)).toBe(true);
108
+ expect(isIgnored('fileA.txt', rules)).toBe(true);
109
+ expect(isIgnored('file10.txt', rules)).toBe(false);
110
+ });
111
+ it('matches character classes [...]', () => {
112
+ const rules = rulesFrom('file[0-9].txt');
113
+ expect(isIgnored('file5.txt', rules)).toBe(true);
114
+ expect(isIgnored('fileA.txt', rules)).toBe(false);
115
+ });
116
+ it('handles negation patterns', () => {
117
+ const rules = rulesFrom('*.log\n!important.log');
118
+ expect(isIgnored('error.log', rules)).toBe(true);
119
+ expect(isIgnored('important.log', rules)).toBe(false);
120
+ });
121
+ it('skips comments and empty lines', () => {
122
+ const rules = rulesFrom('# this is a comment\n\n*.tmp');
123
+ expect(isIgnored('file.tmp', rules)).toBe(true);
124
+ expect(isIgnored('# this is a comment', rules)).toBe(false);
125
+ });
126
+ it('handles .env files', () => {
127
+ const rules = rulesFrom('.env\n.env.local\n.env.*.local');
128
+ expect(isIgnored('.env', rules)).toBe(true);
129
+ expect(isIgnored('.env.local', rules)).toBe(true);
130
+ expect(isIgnored('.env.production.local', rules)).toBe(true);
131
+ expect(isIgnored('.env.example', rules)).toBe(false);
132
+ });
133
+ it('handles complex real-world .gitignore', () => {
134
+ const rules = rulesFrom([
135
+ 'node_modules/',
136
+ '*.log',
137
+ '.env',
138
+ '/dist',
139
+ '!dist/keep.txt',
140
+ '**/*.map',
141
+ ].join('\n'));
142
+ expect(isIgnored('app.log', rules)).toBe(true);
143
+ expect(isIgnored('.env', rules)).toBe(true);
144
+ expect(isIgnored('dist/bundle.js', rules)).toBe(true);
145
+ expect(isIgnored('src/utils/helper.js.map', rules)).toBe(true);
146
+ expect(isIgnored('src/index.ts', rules)).toBe(false);
147
+ });
148
+ });
149
+ describe('isIgnored — path normalization', () => {
150
+ let rules;
151
+ beforeEach(() => {
152
+ mockExistsSync.mockReturnValue(true);
153
+ mockReadFileSync.mockReturnValue('*.log');
154
+ rules = loadIgnoreRules('/project');
155
+ });
156
+ it('handles absolute paths under project root', () => {
157
+ expect(isIgnored('/project/error.log', rules)).toBe(true);
158
+ expect(isIgnored('/project/src/debug.log', rules)).toBe(true);
159
+ });
160
+ it('handles relative paths', () => {
161
+ expect(isIgnored('error.log', rules)).toBe(true);
162
+ expect(isIgnored('src/debug.log', rules)).toBe(true);
163
+ });
164
+ it('returns false for empty path', () => {
165
+ expect(isIgnored('', rules)).toBe(false);
166
+ });
167
+ });
@@ -82,6 +82,27 @@ export declare function generateSkillPrompt(skill: Skill, context: ProjectContex
82
82
  * Get skill steps that need execution
83
83
  */
84
84
  export declare function getExecutableSteps(skill: Skill): SkillStep[];
85
+ /**
86
+ * Skill execution callbacks
87
+ */
88
+ export interface SkillExecutionCallbacks {
89
+ /** Run a shell command, return stdout */
90
+ onCommand: (cmd: string) => Promise<string>;
91
+ /** Send a prompt to AI chat, return AI response */
92
+ onPrompt: (prompt: string) => Promise<string>;
93
+ /** Run an agent task autonomously */
94
+ onAgent: (task: string) => Promise<string>;
95
+ /** Show confirmation dialog, return true if user confirms */
96
+ onConfirm: (message: string) => Promise<boolean>;
97
+ /** Show a notification to the user */
98
+ onNotify: (message: string) => void;
99
+ }
100
+ /**
101
+ * Execute a skill's steps sequentially.
102
+ * Each step's output is available as ${_prev} in the next step's content.
103
+ * Returns the collected results from all steps.
104
+ */
105
+ export declare function executeSkill(skill: Skill, params: Record<string, string>, callbacks: SkillExecutionCallbacks): Promise<SkillExecutionResult>;
85
106
  /**
86
107
  * Format skills list for display
87
108
  */
@@ -768,6 +768,57 @@ export function generateSkillPrompt(skill, context, additionalContext, params) {
768
768
  export function getExecutableSteps(skill) {
769
769
  return skill.steps.filter(s => s.type === 'command' || s.type === 'agent');
770
770
  }
771
+ /**
772
+ * Execute a skill's steps sequentially.
773
+ * Each step's output is available as ${_prev} in the next step's content.
774
+ * Returns the collected results from all steps.
775
+ */
776
+ export async function executeSkill(skill, params, callbacks) {
777
+ const stepResults = [];
778
+ let lastOutput = '';
779
+ for (const step of skill.steps) {
780
+ // Interpolate params and ${_prev} into step content
781
+ const allParams = { ...params, _prev: lastOutput };
782
+ const content = interpolateParams(step.content, allParams);
783
+ try {
784
+ let result = '';
785
+ switch (step.type) {
786
+ case 'command':
787
+ result = await callbacks.onCommand(content);
788
+ break;
789
+ case 'prompt':
790
+ result = await callbacks.onPrompt(content);
791
+ break;
792
+ case 'agent':
793
+ result = await callbacks.onAgent(content);
794
+ break;
795
+ case 'confirm': {
796
+ const confirmed = await callbacks.onConfirm(content);
797
+ if (!confirmed) {
798
+ stepResults.push({ step, result: 'cancelled', success: false });
799
+ return { success: false, output: 'Cancelled by user', steps: stepResults };
800
+ }
801
+ result = 'confirmed';
802
+ break;
803
+ }
804
+ case 'notify':
805
+ callbacks.onNotify(content);
806
+ result = 'notified';
807
+ break;
808
+ }
809
+ lastOutput = result;
810
+ stepResults.push({ step, result, success: true });
811
+ }
812
+ catch (err) {
813
+ const errMsg = err.message || String(err);
814
+ stepResults.push({ step, result: errMsg, success: false });
815
+ if (!step.optional) {
816
+ return { success: false, output: errMsg, steps: stepResults };
817
+ }
818
+ }
819
+ }
820
+ return { success: true, output: lastOutput, steps: stepResults };
821
+ }
771
822
  /**
772
823
  * Format skills list for display
773
824
  */
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { existsSync, readFileSync, statSync } from 'fs';
5
5
  import { join, dirname, basename, extname, relative } from 'path';
6
+ import { loadIgnoreRules, isIgnored } from './gitignore.js';
6
7
  // Max context size (characters)
7
8
  const MAX_CONTEXT_SIZE = 50000;
8
9
  const MAX_FILES = 15;
@@ -259,6 +260,7 @@ function findConfigFiles(projectRoot) {
259
260
  */
260
261
  export function gatherSmartContext(targetFile, projectContext, taskDescription) {
261
262
  const projectRoot = projectContext.root || process.cwd();
263
+ const ignoreRules = loadIgnoreRules(projectRoot);
262
264
  const allRelated = new Map();
263
265
  // If we have a target file, analyze it
264
266
  if (targetFile) {
@@ -329,6 +331,12 @@ export function gatherSmartContext(targetFile, projectContext, taskDescription)
329
331
  }
330
332
  }
331
333
  }
334
+ // Filter out ignored files (except the target file itself)
335
+ for (const [key, file] of allRelated) {
336
+ if (file.reason !== 'target file' && isIgnored(file.path, ignoreRules)) {
337
+ allRelated.delete(key);
338
+ }
339
+ }
332
340
  // Sort by priority and limit
333
341
  let files = Array.from(allRelated.values())
334
342
  .sort((a, b) => b.priority - a.priority)
@@ -0,0 +1 @@
1
+ export {};