@qvac/harness 0.8.9 → 0.8.11

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 (115) hide show
  1. package/CHANGELOG.md +73 -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/cli-argv.js +25 -5
  95. package/dist-lib/lib/tools/skills/credential-health.js +3 -3
  96. package/dist-lib/lib/tools/skills/overlay-crud.js +3 -2
  97. package/dist-lib/lib/tools/translate.d.ts +2 -2
  98. package/dist-lib/lib/tools/translate.js +5 -6
  99. package/dist-lib/lib/tools/weather-lookup/geocode.js +5 -4
  100. package/dist-lib/lib/web-search/engines/bing.js +2 -1
  101. package/dist-lib/lib/web-search/engines/google.js +2 -1
  102. package/dist-lib/lib/web-search/fetch-html.d.ts +5 -5
  103. package/dist-lib/lib/web-search/fetch-html.js +12 -15
  104. package/package.json +67 -25
  105. package/worker/harness-sidecar.mjs +12 -13
  106. package/dist-lib/lib/tools/create-documents.d.ts +0 -3
  107. package/dist-lib/lib/tools/create-documents.js +0 -95
  108. package/dist-lib/lib/tools/edit-documents.d.ts +0 -3
  109. package/dist-lib/lib/tools/edit-documents.js +0 -132
  110. package/dist-lib/lib/tools/file-glob.d.ts +0 -14
  111. package/dist-lib/lib/tools/file-glob.js +0 -118
  112. package/dist-lib/lib/tools/grep.d.ts +0 -5
  113. package/dist-lib/lib/tools/grep.js +0 -184
  114. package/dist-lib/lib/tools/read-files.d.ts +0 -5
  115. package/dist-lib/lib/tools/read-files.js +0 -157
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/harness",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bare": ">=1.34.0"
@@ -163,6 +163,10 @@
163
163
  "types": "./dist-lib/lib/helpers/race-abort.d.ts",
164
164
  "default": "./dist-lib/lib/helpers/race-abort.js"
165
165
  },
166
+ "./errors": {
167
+ "types": "./dist-lib/lib/errors.d.ts",
168
+ "default": "./dist-lib/lib/errors.js"
169
+ },
166
170
  "./sdk-config-loader": {
167
171
  "bare": {
168
172
  "types": "./dist-lib/lib/sdk-config-loader.bare.d.ts",
@@ -190,14 +194,6 @@
190
194
  "types": "./dist-lib/lib/tools/catalog.d.ts",
191
195
  "default": "./dist-lib/lib/tools/catalog.js"
192
196
  },
193
- "./lib/tools/create-documents.ts": {
194
- "types": "./dist-lib/lib/tools/create-documents.d.ts",
195
- "default": "./dist-lib/lib/tools/create-documents.js"
196
- },
197
- "./lib/tools/edit-documents.ts": {
198
- "types": "./dist-lib/lib/tools/edit-documents.d.ts",
199
- "default": "./dist-lib/lib/tools/edit-documents.js"
200
- },
201
197
  "./lib/tools/edit-image.ts": {
202
198
  "types": "./dist-lib/lib/tools/edit-image.d.ts",
203
199
  "default": "./dist-lib/lib/tools/edit-image.js"
@@ -374,14 +370,58 @@
374
370
  "types": "./dist-lib/lib/tools/exec/video-tools.d.ts",
375
371
  "default": "./dist-lib/lib/tools/exec/video-tools.js"
376
372
  },
377
- "./lib/tools/file-glob.ts": {
378
- "types": "./dist-lib/lib/tools/file-glob.d.ts",
379
- "default": "./dist-lib/lib/tools/file-glob.js"
373
+ "./lib/tools/files/edit-file.ts": {
374
+ "types": "./dist-lib/lib/tools/files/edit-file.d.ts",
375
+ "default": "./dist-lib/lib/tools/files/edit-file.js"
376
+ },
377
+ "./lib/tools/files/glob.ts": {
378
+ "types": "./dist-lib/lib/tools/files/glob.d.ts",
379
+ "default": "./dist-lib/lib/tools/files/glob.js"
380
+ },
381
+ "./lib/tools/files/grep.ts": {
382
+ "types": "./dist-lib/lib/tools/files/grep.d.ts",
383
+ "default": "./dist-lib/lib/tools/files/grep.js"
384
+ },
385
+ "./lib/tools/files/index.ts": {
386
+ "types": "./dist-lib/lib/tools/files/index.d.ts",
387
+ "default": "./dist-lib/lib/tools/files/index.js"
388
+ },
389
+ "./lib/tools/files/list-dir.ts": {
390
+ "types": "./dist-lib/lib/tools/files/list-dir.d.ts",
391
+ "default": "./dist-lib/lib/tools/files/list-dir.js"
392
+ },
393
+ "./lib/tools/files/make-dir.ts": {
394
+ "types": "./dist-lib/lib/tools/files/make-dir.d.ts",
395
+ "default": "./dist-lib/lib/tools/files/make-dir.js"
396
+ },
397
+ "./lib/tools/files/read-file.ts": {
398
+ "types": "./dist-lib/lib/tools/files/read-file.d.ts",
399
+ "default": "./dist-lib/lib/tools/files/read-file.js"
400
+ },
401
+ "./lib/tools/files/transfer.ts": {
402
+ "types": "./dist-lib/lib/tools/files/transfer.d.ts",
403
+ "default": "./dist-lib/lib/tools/files/transfer.js"
404
+ },
405
+ "./lib/tools/files/trash-file.ts": {
406
+ "types": "./dist-lib/lib/tools/files/trash-file.d.ts",
407
+ "default": "./dist-lib/lib/tools/files/trash-file.js"
408
+ },
409
+ "./lib/tools/files/write-file.ts": {
410
+ "types": "./dist-lib/lib/tools/files/write-file.d.ts",
411
+ "default": "./dist-lib/lib/tools/files/write-file.js"
412
+ },
413
+ "./lib/tools/fs/document-text.ts": {
414
+ "types": "./dist-lib/lib/tools/fs/document-text.d.ts",
415
+ "default": "./dist-lib/lib/tools/fs/document-text.js"
380
416
  },
381
417
  "./lib/tools/fs/drive.ts": {
382
418
  "types": "./dist-lib/lib/tools/fs/drive.d.ts",
383
419
  "default": "./dist-lib/lib/tools/fs/drive.js"
384
420
  },
421
+ "./lib/tools/fs/edit-match.ts": {
422
+ "types": "./dist-lib/lib/tools/fs/edit-match.d.ts",
423
+ "default": "./dist-lib/lib/tools/fs/edit-match.js"
424
+ },
385
425
  "./lib/tools/fs/glob.ts": {
386
426
  "types": "./dist-lib/lib/tools/fs/glob.d.ts",
387
427
  "default": "./dist-lib/lib/tools/fs/glob.js"
@@ -406,6 +446,14 @@
406
446
  "types": "./dist-lib/lib/tools/fs/scope.d.ts",
407
447
  "default": "./dist-lib/lib/tools/fs/scope.js"
408
448
  },
449
+ "./lib/tools/fs/targets.ts": {
450
+ "types": "./dist-lib/lib/tools/fs/targets.d.ts",
451
+ "default": "./dist-lib/lib/tools/fs/targets.js"
452
+ },
453
+ "./lib/tools/fs/tool-path.ts": {
454
+ "types": "./dist-lib/lib/tools/fs/tool-path.d.ts",
455
+ "default": "./dist-lib/lib/tools/fs/tool-path.js"
456
+ },
409
457
  "./lib/tools/fs/walk.ts": {
410
458
  "types": "./dist-lib/lib/tools/fs/walk.d.ts",
411
459
  "default": "./dist-lib/lib/tools/fs/walk.js"
@@ -422,10 +470,6 @@
422
470
  "types": "./dist-lib/lib/tools/generate-music.d.ts",
423
471
  "default": "./dist-lib/lib/tools/generate-music.js"
424
472
  },
425
- "./lib/tools/grep.ts": {
426
- "types": "./dist-lib/lib/tools/grep.d.ts",
427
- "default": "./dist-lib/lib/tools/grep.js"
428
- },
429
473
  "./lib/tools/http-methods.ts": {
430
474
  "types": "./dist-lib/lib/tools/http-methods.d.ts",
431
475
  "default": "./dist-lib/lib/tools/http-methods.js"
@@ -502,10 +546,6 @@
502
546
  "types": "./dist-lib/lib/tools/private-ip.d.ts",
503
547
  "default": "./dist-lib/lib/tools/private-ip.js"
504
548
  },
505
- "./lib/tools/read-files.ts": {
506
- "types": "./dist-lib/lib/tools/read-files.d.ts",
507
- "default": "./dist-lib/lib/tools/read-files.js"
508
- },
509
549
  "./lib/tools/skills/auth-resolver.ts": {
510
550
  "types": "./dist-lib/lib/tools/skills/auth-resolver.d.ts",
511
551
  "default": "./dist-lib/lib/tools/skills/auth-resolver.js"
@@ -642,7 +682,7 @@
642
682
  "scripts": {
643
683
  "build:schema": "bare schema/build.ts",
644
684
  "test": "npm run vendor:wheels && npm run test:bare && npm run test:addons",
645
- "test:bare": "QVAC_NO_SKILL_BIN_PROBE=1 brittle-jobs 'test/{,!(helpers)/}*.ts'",
685
+ "test:bare": "QVAC_NO_SKILL_BIN_PROBE=1 brittle-jobs 'test/{,!(helpers)/,!(helpers|fixtures)/*/}*.ts'",
646
686
  "format": "prettier . --write",
647
687
  "lint": "prettier . --check && lunte && tsc --noEmit",
648
688
  "build:bundle": "node scripts/build-bundle.mjs",
@@ -744,10 +784,12 @@
744
784
  }
745
785
  },
746
786
  "dependencies": {
747
- "@qvac/inference": "0.21.0",
748
- "@qvac/langdetect-text": "0.1.2",
749
- "@qvac/sdk": "0.21.0",
750
- "@qvac/skills": "0.1.15",
787
+ "@qvac/error": "^0.1.1",
788
+ "@qvac/inference": "^0.21.0",
789
+ "@qvac/langdetect-text": "^0.1.2",
790
+ "@qvac/logging": "0.1.1",
791
+ "@qvac/sdk": "^0.21.0",
792
+ "@qvac/skills": "^0.1.16",
751
793
  "@xmldom/xmldom": "^0.9.12",
752
794
  "asenion": "^0.1.0",
753
795
  "b4a": "^1.9.0",
@@ -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;