@qvac/harness 0.8.9 → 0.8.10

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 (114) hide show
  1. package/CHANGELOG.md +62 -1
  2. package/dist-lib/index.d.ts +1 -1
  3. package/dist-lib/index.js +51 -50
  4. package/dist-lib/lib/audio-transcode-bare.js +2 -2
  5. package/dist-lib/lib/capabilities/completion.d.ts +1 -2
  6. package/dist-lib/lib/capabilities/completion.js +29 -25
  7. package/dist-lib/lib/capabilities/tool-prompt-log.js +3 -3
  8. package/dist-lib/lib/capabilities/voice.js +7 -6
  9. package/dist-lib/lib/connect.js +18 -17
  10. package/dist-lib/lib/errors.d.ts +266 -0
  11. package/dist-lib/lib/errors.js +189 -0
  12. package/dist-lib/lib/helpers/load-stall.d.ts +1 -1
  13. package/dist-lib/lib/helpers/load-stall.js +2 -2
  14. package/dist-lib/lib/helpers/race-abort.d.ts +2 -1
  15. package/dist-lib/lib/helpers/race-abort.js +3 -4
  16. package/dist-lib/lib/hooks/built-in/arg-repair.js +2 -2
  17. package/dist-lib/lib/hooks/pipeline.js +4 -4
  18. package/dist-lib/lib/hooks/run.js +2 -2
  19. package/dist-lib/lib/log.d.ts +26 -2
  20. package/dist-lib/lib/log.js +66 -16
  21. package/dist-lib/lib/models/model-pool.js +11 -6
  22. package/dist-lib/lib/models/model-source.js +3 -2
  23. package/dist-lib/lib/no-redirect-fetch-bare.js +2 -1
  24. package/dist-lib/lib/no-redirect-fetch-impl.js +2 -1
  25. package/dist-lib/lib/office/extract.js +4 -3
  26. package/dist-lib/lib/office/index.js +2 -1
  27. package/dist-lib/lib/resolution-detector.js +3 -3
  28. package/dist-lib/lib/rpc-session.js +2 -1
  29. package/dist-lib/lib/sdk-loader.js +2 -2
  30. package/dist-lib/lib/serve.js +33 -29
  31. package/dist-lib/lib/spawn.d.ts +2 -4
  32. package/dist-lib/lib/spawn.js +14 -7
  33. package/dist-lib/lib/tools/edit-image.js +2 -1
  34. package/dist-lib/lib/tools/exec/adapters/python/attachments.js +8 -8
  35. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +7 -6
  36. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +2 -2
  37. package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
  38. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -8
  39. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +11 -7
  40. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +5 -5
  41. package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +2 -2
  42. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +9 -8
  43. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +2 -0
  44. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +4 -4
  45. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.d.ts +2 -1
  46. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.js +2 -1
  47. package/dist-lib/lib/tools/exec/obsidian/note-name.js +2 -1
  48. package/dist-lib/lib/tools/exec/obsidian/safe-path.js +5 -4
  49. package/dist-lib/lib/tools/exec/obsidian/vault-create.js +3 -2
  50. package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +2 -2
  51. package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +3 -2
  52. package/dist-lib/lib/tools/exec/obsidian/vault-read.js +6 -5
  53. package/dist-lib/lib/tools/exec/obsidian/vault-write.js +13 -12
  54. package/dist-lib/lib/tools/exec/runner.js +2 -1
  55. package/dist-lib/lib/tools/files/edit-file.d.ts +3 -0
  56. package/dist-lib/lib/tools/files/edit-file.js +73 -0
  57. package/dist-lib/lib/tools/files/glob.d.ts +3 -0
  58. package/dist-lib/lib/tools/files/glob.js +63 -0
  59. package/dist-lib/lib/tools/files/grep.d.ts +3 -0
  60. package/dist-lib/lib/tools/files/grep.js +114 -0
  61. package/dist-lib/lib/tools/files/index.d.ts +3 -0
  62. package/dist-lib/lib/tools/files/index.js +23 -0
  63. package/dist-lib/lib/tools/files/list-dir.d.ts +5 -0
  64. package/dist-lib/lib/tools/files/list-dir.js +130 -0
  65. package/dist-lib/lib/tools/files/make-dir.d.ts +3 -0
  66. package/dist-lib/lib/tools/files/make-dir.js +47 -0
  67. package/dist-lib/lib/tools/files/read-file.d.ts +3 -0
  68. package/dist-lib/lib/tools/files/read-file.js +103 -0
  69. package/dist-lib/lib/tools/files/transfer.d.ts +4 -0
  70. package/dist-lib/lib/tools/files/transfer.js +82 -0
  71. package/dist-lib/lib/tools/files/trash-file.d.ts +4 -0
  72. package/dist-lib/lib/tools/files/trash-file.js +61 -0
  73. package/dist-lib/lib/tools/files/write-file.d.ts +3 -0
  74. package/dist-lib/lib/tools/files/write-file.js +55 -0
  75. package/dist-lib/lib/tools/fs/document-text.d.ts +2 -0
  76. package/dist-lib/lib/tools/fs/document-text.js +38 -0
  77. package/dist-lib/lib/tools/fs/edit-match.d.ts +11 -0
  78. package/dist-lib/lib/tools/fs/edit-match.js +85 -0
  79. package/dist-lib/lib/tools/fs/remote.d.ts +2 -1
  80. package/dist-lib/lib/tools/fs/remote.js +4 -4
  81. package/dist-lib/lib/tools/fs/scope.d.ts +1 -1
  82. package/dist-lib/lib/tools/fs/scope.js +3 -2
  83. package/dist-lib/lib/tools/fs/targets.d.ts +10 -0
  84. package/dist-lib/lib/tools/fs/targets.js +32 -0
  85. package/dist-lib/lib/tools/fs/tool-path.d.ts +24 -0
  86. package/dist-lib/lib/tools/fs/tool-path.js +58 -0
  87. package/dist-lib/lib/tools/fs/walk.js +3 -3
  88. package/dist-lib/lib/tools/generate-image.d.ts +2 -2
  89. package/dist-lib/lib/tools/generate-image.js +5 -6
  90. package/dist-lib/lib/tools/office.js +2 -1
  91. package/dist-lib/lib/tools/skill.js +2 -1
  92. package/dist-lib/lib/tools/skills/bundled-dir.js +4 -4
  93. package/dist-lib/lib/tools/skills/catalog.js +2 -2
  94. package/dist-lib/lib/tools/skills/credential-health.js +3 -3
  95. package/dist-lib/lib/tools/skills/overlay-crud.js +3 -2
  96. package/dist-lib/lib/tools/translate.d.ts +2 -2
  97. package/dist-lib/lib/tools/translate.js +5 -6
  98. package/dist-lib/lib/tools/weather-lookup/geocode.js +5 -4
  99. package/dist-lib/lib/web-search/engines/bing.js +2 -1
  100. package/dist-lib/lib/web-search/engines/google.js +2 -1
  101. package/dist-lib/lib/web-search/fetch-html.d.ts +5 -5
  102. package/dist-lib/lib/web-search/fetch-html.js +12 -15
  103. package/package.json +66 -24
  104. package/worker/harness-sidecar.mjs +12 -13
  105. package/dist-lib/lib/tools/create-documents.d.ts +0 -3
  106. package/dist-lib/lib/tools/create-documents.js +0 -95
  107. package/dist-lib/lib/tools/edit-documents.d.ts +0 -3
  108. package/dist-lib/lib/tools/edit-documents.js +0 -132
  109. package/dist-lib/lib/tools/file-glob.d.ts +0 -14
  110. package/dist-lib/lib/tools/file-glob.js +0 -118
  111. package/dist-lib/lib/tools/grep.d.ts +0 -5
  112. package/dist-lib/lib/tools/grep.js +0 -184
  113. package/dist-lib/lib/tools/read-files.d.ts +0 -5
  114. package/dist-lib/lib/tools/read-files.js +0 -157
@@ -4,6 +4,7 @@ import FileLog from 'bare-file-logger'
4
4
  import fs from 'fs'
5
5
  import { dirname, join } from 'path'
6
6
  import process from 'bare-process'
7
+ import { followHostLevel, harnessLogLevel } from '../dist-lib/lib/log.js'
7
8
  import { configureSdkStorage } from '../dist-lib/lib/sdk-storage.js'
8
9
  import {
9
10
  Harness,
@@ -12,18 +13,14 @@ import {
12
13
  setHarnessLog,
13
14
  setHarnessLogSink
14
15
  } from '../dist-lib/index.js'
15
- import { createCreateDocumentsTool } from '../dist-lib/lib/tools/create-documents.js'
16
- import { createEditDocumentsTool } from '../dist-lib/lib/tools/edit-documents.js'
17
16
  import { createExecTool } from '../dist-lib/lib/tools/exec.js'
18
- import { createFileGlobTool } from '../dist-lib/lib/tools/file-glob.js'
19
17
  import { deniedRootsFor } from '../dist-lib/lib/tools/fs/scope.js'
20
- import { createGrepTool } from '../dist-lib/lib/tools/grep.js'
21
18
  import { createHttpRequestTool } from '../dist-lib/lib/tools/http-request.js'
22
19
  import { createMcpCallTool } from '../dist-lib/lib/tools/mcp-call.js'
23
20
  import { createOfficeTools } from '../dist-lib/lib/tools/office.js'
24
21
  import { compileOperationTools } from '../dist-lib/lib/tools/operations/compile.js'
25
22
  import { createPdfTool } from '../dist-lib/lib/tools/pdf.js'
26
- import { createReadFilesTool } from '../dist-lib/lib/tools/read-files.js'
23
+ import { createFileTools } from '../dist-lib/lib/tools/files/index.js'
27
24
  import { createSkillTool } from '../dist-lib/lib/tools/skill.js'
28
25
  import { ensureBundledSkillsDir } from '../dist-lib/lib/tools/skills/bundled-dir.js'
29
26
  import { credentialAllowListFor, mcpReadMethods } from '../dist-lib/lib/tools/skills/index.js'
@@ -81,11 +78,17 @@ export default async function start(ipc, ready) {
81
78
 
82
79
  const sdkStorage = JSON.parse(Bare.argv[3] || '{}')
83
80
  const deniedRoots = deniedRootsFor(sdkStorage.home)
84
- const hostWantsLogs = Bare.argv[7] === '1'
85
- setHarnessLog(hostWantsLogs)
81
+ // spawn.ts's log slot: the host's level, '1' for on at QVAC_LOG_LEVEL, empty for off
82
+ if (Bare.argv[7]) {
83
+ followHostLevel(Bare.argv[7])
84
+ } else {
85
+ setHarnessLog(false)
86
+ }
87
+
88
+ const hostWantsLogs = harnessLogLevel() !== 'off'
86
89
  // before the first await so nothing logs ahead of the sink; the host writes assistant.log beside it
87
90
  const log = openHarnessLog(join(dirname(sdkStorage.home), 'harness.log')) ?? DISCARD
88
- setHarnessLogSink((line) => log.info(SCOPE, line))
91
+ setHarnessLogSink((line, level) => log[level](SCOPE, line))
89
92
  captureConsole(log)
90
93
  await alignSdkConsoleWithHost(hostWantsLogs)
91
94
 
@@ -131,11 +134,7 @@ export default async function start(ipc, ready) {
131
134
  createWebSearchTool(),
132
135
  createWebFetchTool(),
133
136
  createWeatherLookupTool(),
134
- createReadFilesTool({ deniedRoots }),
135
- createFileGlobTool({ deniedRoots }),
136
- createGrepTool({ deniedRoots }),
137
- createCreateDocumentsTool({ deniedRoots }),
138
- createEditDocumentsTool({ deniedRoots }),
137
+ ...createFileTools({ deniedRoots }),
139
138
  createPdfTool(),
140
139
  ...createOfficeTools()
141
140
  ]
@@ -1,3 +0,0 @@
1
- import type { ToolDefinition } from '../capabilities/completion.ts';
2
- import { type FsToolOptions } from './fs/scope.ts';
3
- export declare function createCreateDocumentsTool({ realpath, deniedRoots }?: FsToolOptions): ToolDefinition;
@@ -1,95 +0,0 @@
1
- import { canonicalDenyRoots, defaultRealpath, resolveKey } from './fs/scope.js';
2
- import { writeText } from './fs/write.js';
3
- import { errorMessage } from '../guards.js';
4
- function documentRequests(raw) {
5
- if (!Array.isArray(raw) || raw.length === 0)
6
- return null;
7
- const documents = [];
8
- for (const entry of raw) {
9
- if (!isDocumentRequest(entry))
10
- return null;
11
- documents.push({ path: entry.path, content: entry.content });
12
- }
13
- return documents;
14
- }
15
- function isDocumentRequest(value) {
16
- if (typeof value !== 'object' || value === null)
17
- return false;
18
- if (!('path' in value) || typeof value.path !== 'string' || !value.path.trim()) {
19
- return false;
20
- }
21
- return 'content' in value && typeof value.content === 'string';
22
- }
23
- export function createCreateDocumentsTool({ realpath = defaultRealpath, deniedRoots } = {}) {
24
- return {
25
- name: 'create_documents',
26
- toolset: 'files',
27
- description: 'Create new files inside the granted directories. Parent directories are ' +
28
- 'created as needed. Fails per file if the file already exists — use ' +
29
- 'edit_documents to change existing files.',
30
- parameters: {
31
- type: 'object',
32
- properties: {
33
- documents: {
34
- type: 'array',
35
- description: 'Files to create',
36
- items: {
37
- type: 'object',
38
- properties: {
39
- path: { type: 'string', description: 'File path' },
40
- content: { type: 'string', description: 'Full file content' }
41
- },
42
- required: ['path', 'content']
43
- }
44
- }
45
- },
46
- required: ['documents']
47
- },
48
- effect: () => 'read',
49
- writeRootsRequired: true,
50
- validate(rawArgs) {
51
- return documentRequests(rawArgs.documents)
52
- ? null
53
- : 'documents must be a non-empty array of { path, content }';
54
- },
55
- resource(args) {
56
- const documents = documentRequests(args.documents);
57
- if (!documents) {
58
- return null;
59
- }
60
- const bytes = documents.reduce((sum, doc) => sum + doc.content.length, 0);
61
- const first = documents[0];
62
- const suffix = documents.length > 1 ? ` (+${documents.length - 1} more)` : '';
63
- return { kind: 'file', label: `create ${first.path}${suffix}, ${bytes} chars` };
64
- },
65
- async execute(rawArgs, ctx) {
66
- const roots = ctx?.fsWriteRoots ?? [];
67
- const documents = documentRequests(rawArgs.documents);
68
- if (!documents) {
69
- return { error: 'documents must be a non-empty array of { path, content }' };
70
- }
71
- const created = [];
72
- const failed = [];
73
- const deny = await canonicalDenyRoots(deniedRoots, realpath);
74
- for (const doc of documents) {
75
- const resolved = await resolveKey(doc.path, roots, { realpath, deny });
76
- if ('error' in resolved) {
77
- failed.push({ path: doc.path, error: resolved.error });
78
- continue;
79
- }
80
- try {
81
- if ((await resolved.root.drive.entry(resolved.key)) !== null) {
82
- failed.push({ path: doc.path, error: 'file already exists' });
83
- continue;
84
- }
85
- await writeText(resolved.root.drive, resolved.key, doc.content);
86
- created.push(doc.path);
87
- }
88
- catch (err) {
89
- failed.push({ path: doc.path, error: errorMessage(err) });
90
- }
91
- }
92
- return { created, failed: failed.length ? failed : undefined };
93
- }
94
- };
95
- }
@@ -1,3 +0,0 @@
1
- import type { ToolDefinition } from '../capabilities/completion.ts';
2
- import { type FsToolOptions } from './fs/scope.ts';
3
- export declare function createEditDocumentsTool({ realpath, deniedRoots }?: FsToolOptions): ToolDefinition;
@@ -1,132 +0,0 @@
1
- import b4a from 'b4a';
2
- import { canonicalDenyRoots, defaultRealpath, resolveKey } from './fs/scope.js';
3
- import { writeText } from './fs/write.js';
4
- import { errorMessage } from '../guards.js';
5
- function editRequests(raw) {
6
- if (!Array.isArray(raw) || raw.length === 0)
7
- return null;
8
- const edits = [];
9
- for (const entry of raw) {
10
- if (!isEditRequest(entry))
11
- return null;
12
- edits.push({ path: entry.path, search: entry.search, replace: entry.replace });
13
- }
14
- return edits;
15
- }
16
- function isEditRequest(value) {
17
- if (typeof value !== 'object' || value === null)
18
- return false;
19
- if (!('path' in value) || typeof value.path !== 'string' || !value.path.trim()) {
20
- return false;
21
- }
22
- if (!('search' in value) || typeof value.search !== 'string' || !value.search) {
23
- return false;
24
- }
25
- return 'replace' in value && typeof value.replace === 'string';
26
- }
27
- function applyEdits(content, edits) {
28
- let next = content;
29
- for (const edit of edits) {
30
- const first = next.indexOf(edit.search);
31
- if (first === -1) {
32
- return { error: 'search text not found' };
33
- }
34
- if (next.indexOf(edit.search, first + edit.search.length) !== -1) {
35
- return { error: 'search text is ambiguous (matches more than once)' };
36
- }
37
- next = next.slice(0, first) + edit.replace + next.slice(first + edit.search.length);
38
- }
39
- return next;
40
- }
41
- export function createEditDocumentsTool({ realpath = defaultRealpath, deniedRoots } = {}) {
42
- return {
43
- name: 'edit_documents',
44
- toolset: 'files',
45
- description: 'Edit existing files inside the granted directories via search/replace. ' +
46
- 'Each search string must match exactly once in its file; include enough ' +
47
- 'surrounding context to make it unique. Edits to one file apply together.',
48
- parameters: {
49
- type: 'object',
50
- properties: {
51
- edits: {
52
- type: 'array',
53
- description: 'Search/replace edits',
54
- items: {
55
- type: 'object',
56
- properties: {
57
- path: { type: 'string', description: 'File path' },
58
- search: { type: 'string', description: 'Exact text to find (must match once)' },
59
- replace: { type: 'string', description: 'Replacement text' }
60
- },
61
- required: ['path', 'search', 'replace']
62
- }
63
- }
64
- },
65
- required: ['edits']
66
- },
67
- effect: () => 'write',
68
- autoApprove: false,
69
- writeRootsRequired: true,
70
- validate(rawArgs) {
71
- return editRequests(rawArgs.edits)
72
- ? null
73
- : 'edits must be a non-empty array of { path, search, replace }';
74
- },
75
- resource(args) {
76
- const edits = editRequests(args.edits);
77
- if (!edits) {
78
- return null;
79
- }
80
- const paths = [...new Set(edits.map((edit) => edit.path))];
81
- const label = paths.slice(0, 3).join(', ');
82
- return {
83
- kind: 'file',
84
- label: `edit ${paths.length > 3 ? `${label} (+${paths.length - 3} more)` : label} (${edits.length} edit${edits.length > 1 ? 's' : ''})`
85
- };
86
- },
87
- async execute(rawArgs, ctx) {
88
- const roots = ctx?.fsWriteRoots ?? [];
89
- const edits = editRequests(rawArgs.edits);
90
- if (!edits) {
91
- return { error: 'edits must be a non-empty array of { path, search, replace }' };
92
- }
93
- const byPath = new Map();
94
- for (const edit of edits) {
95
- const list = byPath.get(edit.path) ?? [];
96
- list.push(edit);
97
- byPath.set(edit.path, list);
98
- }
99
- const edited = [];
100
- const failed = [];
101
- const deny = await canonicalDenyRoots(deniedRoots, realpath);
102
- for (const [file, fileEdits] of byPath) {
103
- const resolved = await resolveKey(file, roots, { realpath, deny });
104
- if ('error' in resolved) {
105
- failed.push({ path: file, error: resolved.error });
106
- continue;
107
- }
108
- try {
109
- const entry = await resolved.root.drive.entry(resolved.key);
110
- const blob = entry === null ? null : await resolved.root.drive.get(resolved.key);
111
- if (entry === null || blob === null) {
112
- failed.push({ path: file, error: `no such file: ${file}` });
113
- continue;
114
- }
115
- const next = applyEdits(b4a.toString(blob, 'utf8'), fileEdits);
116
- if (typeof next === 'object') {
117
- failed.push({ path: file, error: next.error });
118
- continue;
119
- }
120
- await writeText(resolved.root.drive, resolved.key, next, {
121
- executable: entry.value.executable
122
- });
123
- edited.push(file);
124
- }
125
- catch (err) {
126
- failed.push({ path: file, error: errorMessage(err) });
127
- }
128
- }
129
- return { edited, failed: failed.length ? failed : undefined };
130
- }
131
- };
132
- }
@@ -1,14 +0,0 @@
1
- import type { ToolDefinition } from '../capabilities/completion.ts';
2
- import { type DriveRoot, type FsToolError, type FsToolOptions, type ResolveKeyOptions } from './fs/scope.ts';
3
- export declare const DEFAULT_GLOB_MATCHES = 100;
4
- export declare const MAX_GLOB_MATCHES = 1000;
5
- export interface SearchTarget {
6
- root: DriveRoot;
7
- folder: string;
8
- }
9
- export interface ResolvedTargets {
10
- targets: SearchTarget[];
11
- }
12
- export declare function searchTargets(dir: unknown, roots: readonly DriveRoot[], { realpath, deny }: ResolveKeyOptions): Promise<ResolvedTargets | FsToolError>;
13
- export declare function relativeKey(key: string, folder: string): string;
14
- export declare function createFileGlobTool({ realpath, deniedRoots }?: FsToolOptions): ToolDefinition;
@@ -1,118 +0,0 @@
1
- import { compileGlob } from './fs/glob.js';
2
- import { NO_DIRS_GRANTED, canonicalDenyRoots, defaultRealpath, isDeniedPath, resolveKey } from './fs/scope.js';
3
- import { walkFiles } from './fs/walk.js';
4
- export const DEFAULT_GLOB_MATCHES = 100;
5
- export const MAX_GLOB_MATCHES = 1000;
6
- export async function searchTargets(dir, roots, { realpath, deny }) {
7
- if (typeof dir === 'string' && dir.trim()) {
8
- const resolved = await resolveKey(dir, roots, { realpath, deny });
9
- if ('error' in resolved) {
10
- return resolved;
11
- }
12
- if (resolved.root.kind === 'local') {
13
- if (!resolved.exists) {
14
- return { error: `no such file: ${dir}` };
15
- }
16
- }
17
- else {
18
- let hasEntry = false;
19
- for await (const _entry of resolved.root.drive.list(resolved.key)) {
20
- hasEntry = true;
21
- break;
22
- }
23
- if (!hasEntry && (await resolved.root.drive.entry(resolved.key)) === null) {
24
- return { error: `no such file: ${dir}` };
25
- }
26
- }
27
- return { targets: [{ root: resolved.root, folder: resolved.key }] };
28
- }
29
- if (roots.length === 0) {
30
- return { error: NO_DIRS_GRANTED };
31
- }
32
- return { targets: roots.map((root) => ({ root, folder: '/' })) };
33
- }
34
- export function relativeKey(key, folder) {
35
- return folder === '/' ? key.slice(1) : key.slice(folder.length + 1);
36
- }
37
- export function createFileGlobTool({ realpath = defaultRealpath, deniedRoots } = {}) {
38
- return {
39
- name: 'file_glob',
40
- toolset: 'files',
41
- description: 'List files matching a glob pattern inside the granted directories. ' +
42
- 'Use ** to cross directories (e.g. "**/*.ts"). Returns absolute paths. ' +
43
- 'Symlinks are never followed; skippedLinks counts the ones left out.',
44
- parameters: {
45
- type: 'object',
46
- properties: {
47
- pattern: { type: 'string', description: 'Glob pattern relative to the searched directory' },
48
- dir: {
49
- type: 'string',
50
- description: 'Optional directory to search (default: every granted directory)'
51
- },
52
- limit: {
53
- type: 'number',
54
- description: `Optional match cap (default ${DEFAULT_GLOB_MATCHES}, max ${MAX_GLOB_MATCHES})`
55
- }
56
- },
57
- required: ['pattern']
58
- },
59
- effect: () => 'read',
60
- stableRead: true,
61
- autoApprove: true,
62
- rootsRequired: true,
63
- validate(rawArgs) {
64
- if (typeof rawArgs.pattern !== 'string' || !rawArgs.pattern.trim()) {
65
- return 'pattern is required';
66
- }
67
- const limit = rawArgs.limit;
68
- if (limit !== undefined && limit !== null) {
69
- const n = Number(limit);
70
- if (!Number.isInteger(n) || n <= 0 || n > MAX_GLOB_MATCHES) {
71
- return `limit must be a positive integer ≤ ${MAX_GLOB_MATCHES}`;
72
- }
73
- }
74
- return null;
75
- },
76
- resource(args) {
77
- const pattern = String(args.pattern ?? '').trim();
78
- return pattern ? { kind: 'file', label: `glob ${pattern}` } : null;
79
- },
80
- async execute(rawArgs, ctx) {
81
- const roots = ctx?.fsRoots ?? [];
82
- const pattern = String(rawArgs.pattern ?? '').trim();
83
- if (!pattern) {
84
- return { error: 'pattern is required' };
85
- }
86
- const limit = rawArgs.limit ? Number(rawArgs.limit) : DEFAULT_GLOB_MATCHES;
87
- const deny = await canonicalDenyRoots(deniedRoots, realpath);
88
- const scoped = await searchTargets(rawArgs.dir, roots, { realpath, deny });
89
- if ('error' in scoped) {
90
- return scoped;
91
- }
92
- const match = compileGlob(pattern);
93
- const matches = [];
94
- let truncated = false;
95
- let skippedLinks = 0;
96
- for (const target of scoped.targets) {
97
- const walk = await walkFiles(target.root, target.folder);
98
- truncated ||= walk.truncated;
99
- skippedLinks += walk.skippedLinks;
100
- for (const file of walk.files) {
101
- if (isDeniedPath(file.path, deny))
102
- continue;
103
- if (!match(relativeKey(file.key, target.folder)))
104
- continue;
105
- matches.push(file.path);
106
- if (matches.length >= limit) {
107
- return { matches, truncated: true, skippedLinks: skippedLinks || undefined };
108
- }
109
- }
110
- }
111
- return {
112
- matches,
113
- truncated: truncated || undefined,
114
- skippedLinks: skippedLinks || undefined
115
- };
116
- }
117
- };
118
- }
@@ -1,5 +0,0 @@
1
- import type { ToolDefinition } from '../capabilities/completion.ts';
2
- import { type FsToolOptions } from './fs/scope.ts';
3
- export declare const DEFAULT_GREP_MATCHES = 200;
4
- export declare const MAX_GREP_MATCHES = 1000;
5
- export declare function createGrepTool({ realpath, deniedRoots }?: FsToolOptions): ToolDefinition;
@@ -1,184 +0,0 @@
1
- import { errorMessage } from '../guards.js';
2
- import { hlog } from '../log.js';
3
- import { searchTargets } from './file-glob.js';
4
- import { readLines } from './fs/read.js';
5
- import { isDriveReadTruncated } from './fs/remote.js';
6
- import { canonicalDenyRoots, defaultRealpath, isDeniedPath } from './fs/scope.js';
7
- import { walkFiles } from './fs/walk.js';
8
- export const DEFAULT_GREP_MATCHES = 200;
9
- export const MAX_GREP_MATCHES = 1000;
10
- const MAX_LINE_CHARS = 500;
11
- const BINARY_PROBE_CHARS = 1024;
12
- async function scanFile(drive, key, { path, tests, room }) {
13
- const hits = [];
14
- let probed = 0;
15
- let binary = false;
16
- let index = 0;
17
- try {
18
- for await (const line of readLines(drive, key)) {
19
- index++;
20
- if (probed < BINARY_PROBE_CHARS && line.includes('\0')) {
21
- binary = true;
22
- break;
23
- }
24
- if (probed < BINARY_PROBE_CHARS) {
25
- probed += line.length + 1;
26
- }
27
- if (!tests.some((test) => test(line))) {
28
- continue;
29
- }
30
- hits.push({ file: path, line: index, text: line.slice(0, MAX_LINE_CHARS) });
31
- if (hits.length >= room) {
32
- return { hits, binary, end: 'capped' };
33
- }
34
- }
35
- }
36
- catch (error) {
37
- if (isDriveReadTruncated(error)) {
38
- return { hits, binary, end: 'cut' };
39
- }
40
- return { hits, binary, end: 'unreadable', error };
41
- }
42
- return { hits, binary, end: 'complete' };
43
- }
44
- function stringArray(raw) {
45
- if (!Array.isArray(raw) || raw.length === 0)
46
- return null;
47
- const values = raw.filter((entry) => typeof entry === 'string' && entry.length > 0);
48
- return values.length === raw.length ? values : null;
49
- }
50
- function compileQueries(queries, regex, caseSensitive) {
51
- if (regex) {
52
- try {
53
- return queries.map((q) => {
54
- const rx = new RegExp(q, caseSensitive ? '' : 'i');
55
- return (line) => rx.test(line);
56
- });
57
- }
58
- catch (err) {
59
- return { error: `invalid regex: ${errorMessage(err)}` };
60
- }
61
- }
62
- return queries.map((q) => {
63
- const needle = caseSensitive ? q : q.toLowerCase();
64
- return caseSensitive
65
- ? (line) => line.includes(needle)
66
- : (line) => line.toLowerCase().includes(needle);
67
- });
68
- }
69
- export function createGrepTool({ realpath = defaultRealpath, deniedRoots } = {}) {
70
- return {
71
- name: 'grep',
72
- toolset: 'files',
73
- description: 'Search file contents inside the granted directories. Queries are literal ' +
74
- 'substrings by default (case-insensitive); set regex=true for patterns. ' +
75
- 'Returns matching lines with file and line number. Symlinks are never ' +
76
- 'followed; skippedLinks counts the ones left out.',
77
- parameters: {
78
- type: 'object',
79
- properties: {
80
- queries: {
81
- type: 'array',
82
- description: 'Text or patterns to search for',
83
- items: { type: 'string' }
84
- },
85
- dir: {
86
- type: 'string',
87
- description: 'Optional directory to search (default: every granted directory)'
88
- },
89
- regex: {
90
- type: 'boolean',
91
- description: 'Treat queries as regular expressions (default false)'
92
- },
93
- caseSensitive: { type: 'boolean', description: 'Match case exactly (default false)' },
94
- maxMatches: {
95
- type: 'number',
96
- description: `Optional match cap (default ${DEFAULT_GREP_MATCHES}, max ${MAX_GREP_MATCHES})`
97
- }
98
- },
99
- required: ['queries']
100
- },
101
- effect: () => 'read',
102
- stableRead: true,
103
- autoApprove: true,
104
- rootsRequired: true,
105
- validate(rawArgs) {
106
- const queries = stringArray(rawArgs.queries);
107
- if (!queries) {
108
- return 'queries must be a non-empty array of strings';
109
- }
110
- const compiled = compileQueries(queries, rawArgs.regex === true, rawArgs.caseSensitive === true);
111
- if ('error' in compiled) {
112
- return compiled.error;
113
- }
114
- const maxMatches = rawArgs.maxMatches;
115
- if (maxMatches !== undefined && maxMatches !== null) {
116
- const n = Number(maxMatches);
117
- if (!Number.isInteger(n) || n <= 0 || n > MAX_GREP_MATCHES) {
118
- return `maxMatches must be a positive integer ≤ ${MAX_GREP_MATCHES}`;
119
- }
120
- }
121
- return null;
122
- },
123
- resource(args) {
124
- const queries = stringArray(args.queries);
125
- return queries ? { kind: 'file', label: `grep ${queries.join(', ')}` } : null;
126
- },
127
- async execute(rawArgs, ctx) {
128
- const roots = ctx?.fsRoots ?? [];
129
- const queries = stringArray(rawArgs.queries);
130
- if (!queries) {
131
- return { error: 'queries must be a non-empty array of strings' };
132
- }
133
- const compiled = compileQueries(queries, rawArgs.regex === true, rawArgs.caseSensitive === true);
134
- if ('error' in compiled) {
135
- return compiled;
136
- }
137
- const maxMatches = rawArgs.maxMatches ? Number(rawArgs.maxMatches) : DEFAULT_GREP_MATCHES;
138
- const deny = await canonicalDenyRoots(deniedRoots, realpath);
139
- const scoped = await searchTargets(rawArgs.dir, roots, { realpath, deny });
140
- if ('error' in scoped) {
141
- return scoped;
142
- }
143
- const matches = [];
144
- let truncated = false;
145
- let skippedLinks = 0;
146
- for (const target of scoped.targets) {
147
- const walk = await walkFiles(target.root, target.folder);
148
- truncated ||= walk.truncated;
149
- skippedLinks += walk.skippedLinks;
150
- for (const file of walk.files) {
151
- if (isDeniedPath(file.path, deny))
152
- continue;
153
- const scan = await scanFile(target.root.drive, file.key, {
154
- path: file.path,
155
- tests: compiled,
156
- room: maxMatches - matches.length
157
- });
158
- if (scan.end === 'capped') {
159
- matches.push(...scan.hits);
160
- return { matches, truncated: true, skippedLinks: skippedLinks || undefined };
161
- }
162
- if (scan.end === 'unreadable') {
163
- hlog('warn: grep skipped a file', {
164
- file: file.path,
165
- err: errorMessage(scan.error)
166
- });
167
- continue;
168
- }
169
- if (scan.end === 'cut') {
170
- truncated = true;
171
- }
172
- if (!scan.binary) {
173
- matches.push(...scan.hits);
174
- }
175
- }
176
- }
177
- return {
178
- matches,
179
- truncated: truncated || undefined,
180
- skippedLinks: skippedLinks || undefined
181
- };
182
- }
183
- };
184
- }
@@ -1,5 +0,0 @@
1
- import type { ToolDefinition } from '../capabilities/completion.ts';
2
- import { type FsToolOptions } from './fs/scope.ts';
3
- export declare const DEFAULT_READ_CHARS = 8192;
4
- export declare const MAX_READ_CHARS = 65536;
5
- export declare function createReadFilesTool({ realpath, deniedRoots }?: FsToolOptions): ToolDefinition;