cowork-cli 1.1.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "work with cowork",
5
5
  "bin": {
6
6
  "cwk": "bin/cli.js"
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
3
+ import { getIgnorePatterns, isSafeEntry, loadNestedIgnores, safePath } from '../../utils/fsUtils.js';
4
4
 
5
5
  /**
6
6
  * findDir tool: Finds directories by name using regex.
@@ -13,10 +13,10 @@ import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
13
13
  export default async function findDir({ pattern, dirPath = '.', recursive = true, limit = 15 }) {
14
14
  try {
15
15
  if (!pattern) return "Error: Search pattern cannot be empty.";
16
-
16
+
17
17
  // Enforce max limit of 15
18
18
  const finalLimit = Math.min(limit, 15);
19
-
19
+
20
20
  let regex;
21
21
  try {
22
22
  regex = new RegExp(pattern, 'i');
@@ -24,10 +24,17 @@ export default async function findDir({ pattern, dirPath = '.', recursive = true
24
24
  return `Error: Invalid regex pattern '${pattern}': ${e.message}`;
25
25
  }
26
26
 
27
- const ignoreList = await getIgnorePatterns();
27
+ let resolvedPath;
28
+ try {
29
+ resolvedPath = safePath(dirPath);
30
+ } catch (err) {
31
+ return `Error: ${err.message}`;
32
+ }
33
+
34
+ const rootIgnoreList = await getIgnorePatterns();
28
35
  const results = [];
29
36
 
30
- async function walk(currentPath) {
37
+ async function walk(currentPath, ignoreList) {
31
38
  if (results.length >= finalLimit) return;
32
39
 
33
40
  let items;
@@ -39,23 +46,24 @@ export default async function findDir({ pattern, dirPath = '.', recursive = true
39
46
 
40
47
  for (const item of items) {
41
48
  if (results.length >= finalLimit) break;
42
- if (shouldIgnore(item.name, ignoreList)) continue;
49
+ if (!isSafeEntry(item, currentPath, ignoreList)) continue;
43
50
 
44
51
  const fullPath = path.join(currentPath, item.name);
45
-
52
+
46
53
  if (item.isDirectory()) {
47
54
  if (regex.test(item.name)) {
48
55
  results.push(path.relative(process.cwd(), fullPath));
49
56
  }
50
-
57
+
51
58
  if (recursive) {
52
- await walk(fullPath);
59
+ const childIgnores = await loadNestedIgnores(fullPath, ignoreList);
60
+ await walk(fullPath, childIgnores);
53
61
  }
54
62
  }
55
63
  }
56
64
  }
57
65
 
58
- await walk(dirPath);
66
+ await walk(resolvedPath, rootIgnoreList);
59
67
 
60
68
  if (results.length === 0) {
61
69
  return `No directories found matching "${pattern}" in "${dirPath}".`;
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
3
+ import { getIgnorePatterns, isSafeEntry, loadNestedIgnores, safePath } from '../../utils/fsUtils.js';
4
4
 
5
5
  /**
6
6
  * findFile tool: Finds files by name using regex.
@@ -13,10 +13,10 @@ import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
13
13
  export default async function findFile({ pattern, dirPath = '.', recursive = true, limit = 15 }) {
14
14
  try {
15
15
  if (!pattern) return "Error: Search pattern cannot be empty.";
16
-
16
+
17
17
  // Enforce max limit of 15
18
18
  const finalLimit = Math.min(limit, 15);
19
-
19
+
20
20
  let regex;
21
21
  try {
22
22
  regex = new RegExp(pattern, 'i');
@@ -24,11 +24,17 @@ export default async function findFile({ pattern, dirPath = '.', recursive = tru
24
24
  return `Error: Invalid regex pattern '${pattern}': ${e.message}`;
25
25
  }
26
26
 
27
- const ignoreList = await getIgnorePatterns();
27
+ let resolvedPath;
28
+ try {
29
+ resolvedPath = safePath(dirPath);
30
+ } catch (err) {
31
+ return `Error: ${err.message}`;
32
+ }
33
+
34
+ const rootIgnoreList = await getIgnorePatterns();
28
35
  const results = [];
29
- let totalFound = 0;
30
36
 
31
- async function walk(currentPath) {
37
+ async function walk(currentPath, ignoreList) {
32
38
  if (results.length >= finalLimit) return;
33
39
 
34
40
  let items;
@@ -40,13 +46,14 @@ export default async function findFile({ pattern, dirPath = '.', recursive = tru
40
46
 
41
47
  for (const item of items) {
42
48
  if (results.length >= finalLimit) break;
43
- if (shouldIgnore(item.name, ignoreList)) continue;
49
+ if (!isSafeEntry(item, currentPath, ignoreList)) continue;
44
50
 
45
51
  const fullPath = path.join(currentPath, item.name);
46
-
52
+
47
53
  if (item.isDirectory()) {
48
54
  if (recursive) {
49
- await walk(fullPath);
55
+ const childIgnores = await loadNestedIgnores(fullPath, ignoreList);
56
+ await walk(fullPath, childIgnores);
50
57
  }
51
58
  } else if (item.isFile()) {
52
59
  if (regex.test(item.name)) {
@@ -56,7 +63,7 @@ export default async function findFile({ pattern, dirPath = '.', recursive = tru
56
63
  }
57
64
  }
58
65
 
59
- await walk(dirPath);
66
+ await walk(resolvedPath, rootIgnoreList);
60
67
 
61
68
  if (results.length === 0) {
62
69
  return `No files found matching "${pattern}" in "${dirPath}".`;
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
3
+ import { getIgnorePatterns, isSafeEntry, loadNestedIgnores, safePath } from '../../utils/fsUtils.js';
4
4
 
5
5
  const MAX_DEPTH = 10;
6
6
  const MAX_ITEMS = 500;
@@ -15,17 +15,23 @@ export default async function projectTree({ dirPath }) {
15
15
  let itemCount = 0;
16
16
  let isTruncated = false;
17
17
 
18
+ let absolutePath;
19
+ try {
20
+ absolutePath = safePath(dirPath);
21
+ } catch (err) {
22
+ return `Error: ${err.message}`;
23
+ }
24
+
18
25
  try {
19
- const absolutePath = path.resolve(dirPath);
20
26
  const stats = await fs.stat(absolutePath);
21
-
27
+
22
28
  if (!stats.isDirectory()) {
23
29
  return `Error: '${dirPath}' is not a directory.`;
24
30
  }
25
31
 
26
- const ignoreList = await getIgnorePatterns();
32
+ const rootIgnoreList = await getIgnorePatterns();
27
33
 
28
- async function buildTree(currentDir, depth = 0, currentPrefix = '') {
34
+ async function buildTree(currentDir, depth = 0, currentPrefix = '', ignoreList = rootIgnoreList) {
29
35
  if (depth > MAX_DEPTH || itemCount >= MAX_ITEMS) {
30
36
  if (itemCount >= MAX_ITEMS) isTruncated = true;
31
37
  return '';
@@ -40,7 +46,7 @@ export default async function projectTree({ dirPath }) {
40
46
  }
41
47
 
42
48
  const filteredItems = items
43
- .filter(item => !shouldIgnore(item.name, ignoreList))
49
+ .filter(item => isSafeEntry(item, currentDir, ignoreList))
44
50
  .sort((a, b) => {
45
51
  if (a.isDirectory() && !b.isDirectory()) return -1;
46
52
  if (!a.isDirectory() && b.isDirectory()) return 1;
@@ -64,7 +70,8 @@ export default async function projectTree({ dirPath }) {
64
70
 
65
71
  if (item.isDirectory()) {
66
72
  const fullPath = path.join(currentDir, item.name);
67
- result += await buildTree(fullPath, depth + 1, currentPrefix + childPrefix);
73
+ const childIgnores = await loadNestedIgnores(fullPath, ignoreList);
74
+ result += await buildTree(fullPath, depth + 1, currentPrefix + childPrefix, childIgnores);
68
75
  }
69
76
  }
70
77
  return result;
@@ -72,7 +79,7 @@ export default async function projectTree({ dirPath }) {
72
79
 
73
80
  const rootName = path.basename(absolutePath) || absolutePath;
74
81
  const tree = await buildTree(absolutePath);
75
-
82
+
76
83
  let finalOutput = `${rootName}/\n${tree || '└(empty)'}`;
77
84
  finalOutput = finalOutput.trimEnd();
78
85
 
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs/promises';
2
- import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
2
+ import { getIgnorePatterns, isSafeEntry, safePath } from '../../utils/fsUtils.js';
3
3
 
4
4
  /**
5
5
  * Implementation of the readDir tool.
@@ -8,12 +8,19 @@ import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
8
8
  * @returns {Promise<string>} List of files and folders or error message.
9
9
  */
10
10
  export default async function readDir({ dirPath }) {
11
+ let resolvedPath;
12
+ try {
13
+ resolvedPath = safePath(dirPath);
14
+ } catch (err) {
15
+ return `Error: ${err.message}`;
16
+ }
17
+
11
18
  try {
12
19
  const ignoreList = await getIgnorePatterns();
13
- const items = await fs.readdir(dirPath, { withFileTypes: true });
14
-
20
+ const items = await fs.readdir(resolvedPath, { withFileTypes: true });
21
+
15
22
  const formattedItems = items
16
- .filter(item => !shouldIgnore(item.name, ignoreList))
23
+ .filter(item => isSafeEntry(item, resolvedPath, ignoreList))
17
24
  .map(item => {
18
25
  const type = item.isDirectory() ? '[D]' : '[F]';
19
26
  return `${type}${item.name}`;
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import { Buffer } from 'buffer';
3
+ import { safePath } from '../../utils/fsUtils.js';
3
4
 
4
5
  const MAX_FILE_SIZE = 1024 * 1024; // 1MB limit
5
6
 
@@ -10,9 +11,16 @@ const MAX_FILE_SIZE = 1024 * 1024; // 1MB limit
10
11
  * @returns {Promise<string>} File content or error message.
11
12
  */
12
13
  export default async function readFile({ filePath }) {
14
+ let resolvedPath;
13
15
  try {
14
- const stats = await fs.stat(filePath);
15
-
16
+ resolvedPath = safePath(filePath);
17
+ } catch (err) {
18
+ return `Error: ${err.message}`;
19
+ }
20
+
21
+ try {
22
+ const stats = await fs.stat(resolvedPath);
23
+
16
24
  if (!stats.isFile()) {
17
25
  return `Error: '${filePath}' is not a file.`;
18
26
  }
@@ -22,17 +30,17 @@ export default async function readFile({ filePath }) {
22
30
  }
23
31
 
24
32
  // Binary check: read first 1KB and look for null bytes
25
- const handle = await fs.open(filePath, 'r');
33
+ const handle = await fs.open(resolvedPath, 'r');
26
34
  const { bytesRead, buffer } = await handle.read(Buffer.alloc(1024), 0, 1024, 0);
27
35
  await handle.close();
28
-
36
+
29
37
  for (let i = 0; i < bytesRead; i++) {
30
38
  if (buffer[i] === 0) {
31
39
  return `Error: '${filePath}' appears to be a binary file. Reading binary files is not supported.`;
32
40
  }
33
41
  }
34
42
 
35
- const content = await fs.readFile(filePath, 'utf8');
43
+ const content = await fs.readFile(resolvedPath, 'utf8');
36
44
  return content;
37
45
  } catch (err) {
38
46
  if (err.code === 'ENOENT') return `Error: File not found at '${filePath}'.`;
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import { Buffer } from 'buffer';
3
+ import { safePath } from '../../utils/fsUtils.js';
3
4
 
4
5
  /**
5
6
  * Implementation of the readFileChunk tool.
@@ -10,22 +11,29 @@ import { Buffer } from 'buffer';
10
11
  * @returns {Promise<string>} File chunk or error message.
11
12
  */
12
13
  export default async function readFileChunk({ filePath, startLine, endLine }) {
14
+ let resolvedPath;
13
15
  try {
14
- const stats = await fs.stat(filePath);
16
+ resolvedPath = safePath(filePath);
17
+ } catch (err) {
18
+ return `Error: ${err.message}`;
19
+ }
20
+
21
+ try {
22
+ const stats = await fs.stat(resolvedPath);
15
23
  if (!stats.isFile()) return `Error: '${filePath}' is not a file.`;
16
24
 
17
25
  // Binary check: read first 1KB and look for null bytes
18
- const handle = await fs.open(filePath, 'r');
26
+ const handle = await fs.open(resolvedPath, 'r');
19
27
  const { bytesRead, buffer } = await handle.read(Buffer.alloc(1024), 0, 1024, 0);
20
28
  await handle.close();
21
-
29
+
22
30
  for (let i = 0; i < bytesRead; i++) {
23
31
  if (buffer[i] === 0) return `Error: '${filePath}' appears to be a binary file. Reading binary files is not supported.`;
24
32
  }
25
33
 
26
- const data = await fs.readFile(filePath, 'utf8');
34
+ const data = await fs.readFile(resolvedPath, 'utf8');
27
35
  const lines = data.split('\n');
28
-
36
+
29
37
  // Boundary validation
30
38
  const totalLines = lines.length;
31
39
  const actualStart = Math.max(1, startLine);
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { Buffer } from 'buffer';
4
- import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
4
+ import { getIgnorePatterns, isSafeEntry, loadNestedIgnores, safePath } from '../../utils/fsUtils.js';
5
5
 
6
6
  const MAX_MATCHES_PER_FILE = 20;
7
7
  const MAX_TOTAL_MATCHES = 100;
@@ -14,8 +14,15 @@ export default async function searchText({ pattern, path: searchPath, recursive
14
14
  let totalMatches = 0;
15
15
  let isTruncated = false;
16
16
 
17
+ let resolvedPath;
17
18
  try {
18
- const stats = await fs.stat(searchPath);
19
+ resolvedPath = safePath(searchPath);
20
+ } catch (err) {
21
+ return `Error: ${err.message}`;
22
+ }
23
+
24
+ try {
25
+ const stats = await fs.stat(resolvedPath);
19
26
  if (!pattern) return "Error: Search pattern cannot be empty.";
20
27
 
21
28
  let regex;
@@ -25,15 +32,15 @@ export default async function searchText({ pattern, path: searchPath, recursive
25
32
  return `Error: Invalid regex pattern '${pattern}': ${e.message}`;
26
33
  }
27
34
 
28
- const ignoreList = await getIgnorePatterns();
35
+ const rootIgnoreList = await getIgnorePatterns();
29
36
  const results = [];
30
37
 
31
- const walk = async (currentPath, depth = 0) => {
38
+ const walk = async (currentPath, depth = 0, ignoreList = rootIgnoreList) => {
32
39
  if (totalMatches >= MAX_TOTAL_MATCHES) {
33
40
  isTruncated = true;
34
41
  return;
35
42
  }
36
-
43
+
37
44
  if (depth > MAX_DEPTH) return;
38
45
 
39
46
  let items;
@@ -49,13 +56,14 @@ export default async function searchText({ pattern, path: searchPath, recursive
49
56
  break;
50
57
  }
51
58
 
52
- if (shouldIgnore(item.name, ignoreList)) continue;
59
+ if (!isSafeEntry(item, currentPath, ignoreList)) continue;
53
60
 
54
61
  const fullPath = path.join(currentPath, item.name);
55
62
 
56
63
  if (item.isDirectory()) {
57
64
  if (recursive || depth === 0) {
58
- await walk(fullPath, depth + 1);
65
+ const childIgnores = await loadNestedIgnores(fullPath, ignoreList);
66
+ await walk(fullPath, depth + 1, childIgnores);
59
67
  }
60
68
  } else if (item.isFile()) {
61
69
  const fileMatches = await searchInFile(fullPath, regex);
@@ -73,18 +81,18 @@ export default async function searchText({ pattern, path: searchPath, recursive
73
81
  };
74
82
 
75
83
  if (stats.isFile()) {
76
- const fileMatches = await searchInFile(searchPath, regex);
84
+ const fileMatches = await searchInFile(resolvedPath, regex);
77
85
  if (fileMatches.length > 0) {
78
86
  const allowed = Math.min(fileMatches.length, MAX_TOTAL_MATCHES);
79
87
  results.push({
80
- file: path.relative(process.cwd(), searchPath),
88
+ file: path.relative(process.cwd(), resolvedPath),
81
89
  matches: fileMatches.slice(0, allowed)
82
90
  });
83
91
  totalMatches = allowed;
84
92
  if (fileMatches.length > allowed) isTruncated = true;
85
93
  }
86
94
  } else {
87
- await walk(searchPath);
95
+ await walk(resolvedPath);
88
96
  }
89
97
 
90
98
  if (results.length === 0) return "No matches found.";
@@ -99,7 +107,6 @@ export default async function searchText({ pattern, path: searchPath, recursive
99
107
 
100
108
  return output;
101
109
 
102
-
103
110
  } catch (err) {
104
111
  if (err.code === 'ENOENT') return `Error: Path not found at '${searchPath}'.`;
105
112
  return `Error searching text: ${err.message}`;
@@ -111,7 +118,7 @@ async function searchInFile(filePath, regex) {
111
118
  const handle = await fs.open(filePath, 'r');
112
119
  const { bytesRead, buffer } = await handle.read(Buffer.alloc(1024), 0, 1024, 0);
113
120
  await handle.close();
114
-
121
+
115
122
  for (let i = 0; i < bytesRead; i++) {
116
123
  if (buffer[i] === 0) return []; // Skip binary
117
124
  }
@@ -119,7 +126,7 @@ async function searchInFile(filePath, regex) {
119
126
  const content = await fs.readFile(filePath, 'utf8');
120
127
  const lines = content.split('\n');
121
128
  const matches = [];
122
-
129
+
123
130
  for (let i = 0; i < lines.length; i++) {
124
131
  if (regex.test(lines[i])) {
125
132
  matches.push(`${i + 1}:${lines[i].trim()}`);