@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.
- package/CHANGELOG.md +73 -1
- package/dist-lib/index.d.ts +1 -1
- package/dist-lib/index.js +51 -50
- package/dist-lib/lib/audio-transcode-bare.js +2 -2
- package/dist-lib/lib/capabilities/completion.d.ts +1 -2
- package/dist-lib/lib/capabilities/completion.js +29 -25
- package/dist-lib/lib/capabilities/tool-prompt-log.js +3 -3
- package/dist-lib/lib/capabilities/voice.js +7 -6
- package/dist-lib/lib/connect.js +18 -17
- package/dist-lib/lib/errors.d.ts +266 -0
- package/dist-lib/lib/errors.js +189 -0
- package/dist-lib/lib/helpers/load-stall.d.ts +1 -1
- package/dist-lib/lib/helpers/load-stall.js +2 -2
- package/dist-lib/lib/helpers/race-abort.d.ts +2 -1
- package/dist-lib/lib/helpers/race-abort.js +3 -4
- package/dist-lib/lib/hooks/built-in/arg-repair.js +2 -2
- package/dist-lib/lib/hooks/pipeline.js +4 -4
- package/dist-lib/lib/hooks/run.js +2 -2
- package/dist-lib/lib/log.d.ts +26 -2
- package/dist-lib/lib/log.js +66 -16
- package/dist-lib/lib/models/model-pool.js +11 -6
- package/dist-lib/lib/models/model-source.js +3 -2
- package/dist-lib/lib/no-redirect-fetch-bare.js +2 -1
- package/dist-lib/lib/no-redirect-fetch-impl.js +2 -1
- package/dist-lib/lib/office/extract.js +4 -3
- package/dist-lib/lib/office/index.js +2 -1
- package/dist-lib/lib/resolution-detector.js +3 -3
- package/dist-lib/lib/rpc-session.js +2 -1
- package/dist-lib/lib/sdk-loader.js +2 -2
- package/dist-lib/lib/serve.js +33 -29
- package/dist-lib/lib/spawn.d.ts +2 -4
- package/dist-lib/lib/spawn.js +14 -7
- package/dist-lib/lib/tools/edit-image.js +2 -1
- package/dist-lib/lib/tools/exec/adapters/python/attachments.js +8 -8
- package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +7 -6
- package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -8
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +11 -7
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +5 -5
- package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +9 -8
- package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +2 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +4 -4
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.d.ts +2 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.js +2 -1
- package/dist-lib/lib/tools/exec/obsidian/note-name.js +2 -1
- package/dist-lib/lib/tools/exec/obsidian/safe-path.js +5 -4
- package/dist-lib/lib/tools/exec/obsidian/vault-create.js +3 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +3 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-read.js +6 -5
- package/dist-lib/lib/tools/exec/obsidian/vault-write.js +13 -12
- package/dist-lib/lib/tools/exec/runner.js +2 -1
- package/dist-lib/lib/tools/files/edit-file.d.ts +3 -0
- package/dist-lib/lib/tools/files/edit-file.js +73 -0
- package/dist-lib/lib/tools/files/glob.d.ts +3 -0
- package/dist-lib/lib/tools/files/glob.js +63 -0
- package/dist-lib/lib/tools/files/grep.d.ts +3 -0
- package/dist-lib/lib/tools/files/grep.js +114 -0
- package/dist-lib/lib/tools/files/index.d.ts +3 -0
- package/dist-lib/lib/tools/files/index.js +23 -0
- package/dist-lib/lib/tools/files/list-dir.d.ts +5 -0
- package/dist-lib/lib/tools/files/list-dir.js +130 -0
- package/dist-lib/lib/tools/files/make-dir.d.ts +3 -0
- package/dist-lib/lib/tools/files/make-dir.js +47 -0
- package/dist-lib/lib/tools/files/read-file.d.ts +3 -0
- package/dist-lib/lib/tools/files/read-file.js +103 -0
- package/dist-lib/lib/tools/files/transfer.d.ts +4 -0
- package/dist-lib/lib/tools/files/transfer.js +82 -0
- package/dist-lib/lib/tools/files/trash-file.d.ts +4 -0
- package/dist-lib/lib/tools/files/trash-file.js +61 -0
- package/dist-lib/lib/tools/files/write-file.d.ts +3 -0
- package/dist-lib/lib/tools/files/write-file.js +55 -0
- package/dist-lib/lib/tools/fs/document-text.d.ts +2 -0
- package/dist-lib/lib/tools/fs/document-text.js +38 -0
- package/dist-lib/lib/tools/fs/edit-match.d.ts +11 -0
- package/dist-lib/lib/tools/fs/edit-match.js +85 -0
- package/dist-lib/lib/tools/fs/remote.d.ts +2 -1
- package/dist-lib/lib/tools/fs/remote.js +4 -4
- package/dist-lib/lib/tools/fs/scope.d.ts +1 -1
- package/dist-lib/lib/tools/fs/scope.js +3 -2
- package/dist-lib/lib/tools/fs/targets.d.ts +10 -0
- package/dist-lib/lib/tools/fs/targets.js +32 -0
- package/dist-lib/lib/tools/fs/tool-path.d.ts +24 -0
- package/dist-lib/lib/tools/fs/tool-path.js +58 -0
- package/dist-lib/lib/tools/fs/walk.js +3 -3
- package/dist-lib/lib/tools/generate-image.d.ts +2 -2
- package/dist-lib/lib/tools/generate-image.js +5 -6
- package/dist-lib/lib/tools/office.js +2 -1
- package/dist-lib/lib/tools/skill.js +2 -1
- package/dist-lib/lib/tools/skills/bundled-dir.js +4 -4
- package/dist-lib/lib/tools/skills/catalog.js +2 -2
- package/dist-lib/lib/tools/skills/cli-argv.js +25 -5
- package/dist-lib/lib/tools/skills/credential-health.js +3 -3
- package/dist-lib/lib/tools/skills/overlay-crud.js +3 -2
- package/dist-lib/lib/tools/translate.d.ts +2 -2
- package/dist-lib/lib/tools/translate.js +5 -6
- package/dist-lib/lib/tools/weather-lookup/geocode.js +5 -4
- package/dist-lib/lib/web-search/engines/bing.js +2 -1
- package/dist-lib/lib/web-search/engines/google.js +2 -1
- package/dist-lib/lib/web-search/fetch-html.d.ts +5 -5
- package/dist-lib/lib/web-search/fetch-html.js +12 -15
- package/package.json +67 -25
- package/worker/harness-sidecar.mjs +12 -13
- package/dist-lib/lib/tools/create-documents.d.ts +0 -3
- package/dist-lib/lib/tools/create-documents.js +0 -95
- package/dist-lib/lib/tools/edit-documents.d.ts +0 -3
- package/dist-lib/lib/tools/edit-documents.js +0 -132
- package/dist-lib/lib/tools/file-glob.d.ts +0 -14
- package/dist-lib/lib/tools/file-glob.js +0 -118
- package/dist-lib/lib/tools/grep.d.ts +0 -5
- package/dist-lib/lib/tools/grep.js +0 -184
- package/dist-lib/lib/tools/read-files.d.ts +0 -5
- package/dist-lib/lib/tools/read-files.js +0 -157
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { HarnessError } from '../../../../errors.js';
|
|
3
4
|
import { errorMessage } from '../../../../guards.js';
|
|
4
|
-
import {
|
|
5
|
+
import { log } from '../../../../log.js';
|
|
5
6
|
import { checkOutputBytes, MAX_INPUT_TOTAL_BYTES, RUN_OUTPUT_BUDGET_BYTES } from './output-policy.js';
|
|
6
7
|
const STAGE_TIMEOUT_MS = 60_000;
|
|
7
|
-
const STAGE_TIMEOUT_MESSAGE = `staging attachments timed out after ${STAGE_TIMEOUT_MS}ms`;
|
|
8
8
|
const OVER_BUDGET_ERROR = 'inputs exceed maximum total size';
|
|
9
9
|
const NO_UPLOAD_ERROR = 'no uploaded image in this chat — attach an image or pass an attachmentId';
|
|
10
10
|
const DOCUMENT_INPUT = /\.(pdf|docx?|xlsx?|xlsm|pptx?|csv|tsv|txt|md|markdown|rtf|odt|ods|odp|epub|jsonl?|xhtml|html?|xml|ya?ml|log|zip)$/i;
|
|
11
11
|
const NO_DOCUMENT_ERROR = 'an id-less input stages an uploaded image, and this path names a document — pass its ' +
|
|
12
12
|
'attachmentId, which the [Attached file "…"] line on the user\'s message carries';
|
|
13
|
-
function bounded(work, ms
|
|
13
|
+
function bounded(work, ms) {
|
|
14
14
|
let timer;
|
|
15
15
|
const bound = new Promise((_, reject) => {
|
|
16
|
-
timer = setTimeout(() => reject(new
|
|
16
|
+
timer = setTimeout(() => reject(new HarnessError('ATTACHMENT_STAGING_TIMED_OUT', [STAGE_TIMEOUT_MS])), ms);
|
|
17
17
|
});
|
|
18
18
|
return Promise.race([work, bound]).finally(() => {
|
|
19
19
|
if (timer) {
|
|
@@ -54,14 +54,14 @@ async function probeUpload(fetchAttachment, input, { index, turnImages, signal,
|
|
|
54
54
|
if (DOCUMENT_INPUT.test(input.path)) {
|
|
55
55
|
return { error: NO_DOCUMENT_ERROR };
|
|
56
56
|
}
|
|
57
|
-
const bytes = await bounded(uploadedBytes(fetchAttachment, index, { turnImages, signal }), deadline - Date.now()
|
|
57
|
+
const bytes = await bounded(uploadedBytes(fetchAttachment, index, { turnImages, signal }), deadline - Date.now());
|
|
58
58
|
if ('error' in bytes) {
|
|
59
59
|
return bytes;
|
|
60
60
|
}
|
|
61
61
|
return { size: bytes.byteLength, bytes };
|
|
62
62
|
}
|
|
63
63
|
async function probeAttachment(fetchAttachment, id, { signal, deadline }) {
|
|
64
|
-
const head = await bounded(fetchAttachment({ attachmentId: id, meta: true, signal }), deadline - Date.now()
|
|
64
|
+
const head = await bounded(fetchAttachment({ attachmentId: id, meta: true, signal }), deadline - Date.now());
|
|
65
65
|
if (!head) {
|
|
66
66
|
return { error: `attachment ${id} not found in this chat` };
|
|
67
67
|
}
|
|
@@ -90,7 +90,7 @@ export async function stageInputs(fetchAttachment, inputs, opts = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
total += probe.size;
|
|
92
92
|
if (total > MAX_INPUT_TOTAL_BYTES) {
|
|
93
|
-
|
|
93
|
+
log.warn('python: input attachments over budget; skipping blob download', {
|
|
94
94
|
total,
|
|
95
95
|
cap: MAX_INPUT_TOTAL_BYTES,
|
|
96
96
|
count: inputs.length
|
|
@@ -111,7 +111,7 @@ export async function stageInputs(fetchAttachment, inputs, opts = {}) {
|
|
|
111
111
|
for (const { index, input } of pending) {
|
|
112
112
|
const id = input.attachmentId;
|
|
113
113
|
try {
|
|
114
|
-
const fetched = await bounded(fetchAttachment({ attachmentId: id, signal }), deadline - Date.now()
|
|
114
|
+
const fetched = await bounded(fetchAttachment({ attachmentId: id, signal }), deadline - Date.now());
|
|
115
115
|
if (!fetched || !hasBytes(fetched)) {
|
|
116
116
|
return { error: `attachment ${id} not found in this chat` };
|
|
117
117
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AbortController } from 'bare-abort-controller';
|
|
2
2
|
import b4a from 'b4a';
|
|
3
3
|
import { noRedirectFetch } from '#no-redirect-fetch';
|
|
4
|
+
import { HarnessError } from '../../../../errors.js';
|
|
4
5
|
import { lookupHost } from '../../../../dns-lookup.js';
|
|
5
6
|
import { hostIsNamed } from './named-hosts.js';
|
|
6
7
|
import { checkOutboundHost, resolveOutboundUrl } from '../../../url-guard.js';
|
|
@@ -182,18 +183,18 @@ export function createGuardedFetch(options = {}) {
|
|
|
182
183
|
try {
|
|
183
184
|
for (let hops = 0;; hops++) {
|
|
184
185
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
185
|
-
throw new
|
|
186
|
+
throw new HarnessError('FETCH_REFUSED', [`unsupported protocol ${url.protocol}`]);
|
|
186
187
|
}
|
|
187
188
|
const literal = checkOutboundHost(url);
|
|
188
189
|
if (literal) {
|
|
189
|
-
throw new
|
|
190
|
+
throw new HarnessError('FETCH_REFUSED', [`${literal}`]);
|
|
190
191
|
}
|
|
191
192
|
if (!(await mayConnect(url.hostname))) {
|
|
192
|
-
throw new
|
|
193
|
+
throw new HarnessError('FETCH_REFUSED', [`${url.hostname} was not approved for this run`]);
|
|
193
194
|
}
|
|
194
195
|
const check = await resolveOutboundUrl(url, resolveHost, controller.signal);
|
|
195
196
|
if (check.blocked) {
|
|
196
|
-
throw new
|
|
197
|
+
throw new HarnessError('FETCH_REFUSED', [`${check.blocked}`]);
|
|
197
198
|
}
|
|
198
199
|
const response = await send(url, {
|
|
199
200
|
method,
|
|
@@ -213,10 +214,10 @@ export function createGuardedFetch(options = {}) {
|
|
|
213
214
|
});
|
|
214
215
|
}
|
|
215
216
|
if (init.redirect === 'error') {
|
|
216
|
-
throw new
|
|
217
|
+
throw new HarnessError('FETCH_REFUSED', ['redirect not allowed']);
|
|
217
218
|
}
|
|
218
219
|
if (hops >= MAX_REDIRECTS) {
|
|
219
|
-
throw new
|
|
220
|
+
throw new HarnessError('FETCH_REFUSED', ['too many redirects']);
|
|
220
221
|
}
|
|
221
222
|
const next = new URL(location, url);
|
|
222
223
|
if (next.protocol !== url.protocol || next.host !== url.host) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from 'url';
|
|
2
2
|
import { Worker } from 'worker_threads';
|
|
3
3
|
import { errorMessage } from '../../../../guards.js';
|
|
4
|
-
import {
|
|
4
|
+
import { log } from '../../../../log.js';
|
|
5
5
|
import { DISARMED, FIRED } from './deadline-state.js';
|
|
6
6
|
export function startInterruptTimer(ms) {
|
|
7
7
|
const state = new Int32Array(new SharedArrayBuffer(4));
|
|
@@ -9,7 +9,7 @@ export function startInterruptTimer(ms) {
|
|
|
9
9
|
const workerData = { state: state.buffer, interrupt: interrupt.buffer, ms };
|
|
10
10
|
const entry = fileURLToPath(import.meta.resolve('#pyodide-interrupt-deadline'));
|
|
11
11
|
new Worker(entry, { workerData })
|
|
12
|
-
.on('error', (error) =>
|
|
12
|
+
.on('error', (error) => log.warn('python: interrupt deadline failed', { err: errorMessage(error) }))
|
|
13
13
|
.unref();
|
|
14
14
|
return {
|
|
15
15
|
interrupt,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errorMessage } from '../../../../guards.js';
|
|
2
|
-
import {
|
|
2
|
+
import { log } from '../../../../log.js';
|
|
3
3
|
import { MAX_OUTPUT_FILE_BYTES, MAX_OUTPUT_TOTAL_BYTES } from './output-policy.js';
|
|
4
4
|
let runCounter = 0;
|
|
5
5
|
function filePath(dir, name) {
|
|
@@ -69,7 +69,7 @@ export function removeScratch(fs, dir, previousCwd) {
|
|
|
69
69
|
removeTree(fs, dir);
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
|
|
72
|
+
log.warn('python: scratch cleanup failed', {
|
|
73
73
|
dir,
|
|
74
74
|
err: errorMessage(error)
|
|
75
75
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AbortController } from 'bare-abort-controller';
|
|
2
2
|
import ScopeLock from 'scope-lock';
|
|
3
|
+
import { HarnessError } from '../../../../errors.js';
|
|
3
4
|
import { errorMessage } from '../../../../guards.js';
|
|
4
5
|
import { AbortedError, raceAbort } from '../../../../helpers/race-abort.js';
|
|
5
|
-
import {
|
|
6
|
+
import { log } from '../../../../log.js';
|
|
6
7
|
import { MAX_OUTPUT_CHARS } from '../../runner.js';
|
|
7
8
|
import { startInterruptTimer } from './interrupt-timer.js';
|
|
8
9
|
import { pyodideLoadOptions } from './loader.js';
|
|
@@ -15,15 +16,15 @@ const cache = new Map();
|
|
|
15
16
|
function getPyodide(load) {
|
|
16
17
|
let loaded = cache.get(load);
|
|
17
18
|
if (!loaded) {
|
|
18
|
-
|
|
19
|
+
log.info('python: booting interpreter');
|
|
19
20
|
loaded = load()
|
|
20
21
|
.then((py) => {
|
|
21
|
-
|
|
22
|
+
log.info('python: interpreter ready');
|
|
22
23
|
return py;
|
|
23
24
|
})
|
|
24
25
|
.catch((error) => {
|
|
25
26
|
cache.delete(load);
|
|
26
|
-
|
|
27
|
+
log.error('python: interpreter boot failed', {
|
|
27
28
|
err: errorMessage(error)
|
|
28
29
|
});
|
|
29
30
|
throw error;
|
|
@@ -35,7 +36,6 @@ function getPyodide(load) {
|
|
|
35
36
|
const runLock = new ScopeLock();
|
|
36
37
|
async function runInProcess(load, spec, options) {
|
|
37
38
|
const deadline = Date.now() + options.timeoutMs;
|
|
38
|
-
const timeoutMessage = `Command timed out after ${options.timeoutMs}ms`;
|
|
39
39
|
let timedOut = false;
|
|
40
40
|
const stop = new AbortController();
|
|
41
41
|
const ask = options.askConnect;
|
|
@@ -63,10 +63,10 @@ async function runInProcess(load, spec, options) {
|
|
|
63
63
|
}
|
|
64
64
|
if (outcome.evict) {
|
|
65
65
|
cache.delete(load);
|
|
66
|
-
|
|
66
|
+
log.warn('python: interpreter evicted; next run boots fresh');
|
|
67
67
|
}
|
|
68
68
|
if (timedOut) {
|
|
69
|
-
throw new
|
|
69
|
+
throw new HarnessError('COMMAND_TIMED_OUT', [options.timeoutMs]);
|
|
70
70
|
}
|
|
71
71
|
return outcome.result;
|
|
72
72
|
}
|
|
@@ -84,7 +84,7 @@ async function runInProcess(load, spec, options) {
|
|
|
84
84
|
const timeout = new Promise((_, reject) => {
|
|
85
85
|
timer = setTimeout(() => {
|
|
86
86
|
timedOut = true;
|
|
87
|
-
reject(new
|
|
87
|
+
reject(new HarnessError('COMMAND_TIMED_OUT', [options.timeoutMs]));
|
|
88
88
|
}, options.timeoutMs);
|
|
89
89
|
});
|
|
90
90
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { HarnessError } from '../../../../errors.js';
|
|
1
2
|
import { errorMessage } from '../../../../guards.js';
|
|
2
3
|
import { AbortedError } from '../../../../helpers/race-abort.js';
|
|
3
|
-
import {
|
|
4
|
+
import { log } from '../../../../log.js';
|
|
4
5
|
import { TRUNCATED_OUTPUT_SUFFIX } from '../../runner.js';
|
|
5
6
|
import { collectOutputs, openScratch, stageInputFiles } from './memfs.js';
|
|
6
7
|
import { isGuardedFetch, setConnectPolicy } from './guarded-fetch.js';
|
|
@@ -105,7 +106,10 @@ async function runOnce(py, code, opts) {
|
|
|
105
106
|
}
|
|
106
107
|
return retry;
|
|
107
108
|
}
|
|
108
|
-
class InstallTimeoutError extends
|
|
109
|
+
class InstallTimeoutError extends HarnessError {
|
|
110
|
+
constructor(ms) {
|
|
111
|
+
super('PACKAGE_INSTALL_TIMED_OUT', [ms]);
|
|
112
|
+
}
|
|
109
113
|
}
|
|
110
114
|
function installSource(py, packages) {
|
|
111
115
|
const plan = planVendoredInstall(packages);
|
|
@@ -113,7 +117,7 @@ function installSource(py, packages) {
|
|
|
113
117
|
return `import micropip\nawait micropip.install(${JSON.stringify(packages)})`;
|
|
114
118
|
}
|
|
115
119
|
const specs = [...stageVendoredWheels(py.FS, plan), ...plan.packages];
|
|
116
|
-
|
|
120
|
+
log.info('python: installing from vendored wheels', { specs });
|
|
117
121
|
return `import micropip\nawait micropip.install(${JSON.stringify(specs)}, deps=False)`;
|
|
118
122
|
}
|
|
119
123
|
async function installPackages(py, packages, { timeoutMs, isCancelled }) {
|
|
@@ -124,7 +128,7 @@ async function installPackages(py, packages, { timeoutMs, isCancelled }) {
|
|
|
124
128
|
const install = py.runPythonAsync(installSource(py, packages));
|
|
125
129
|
let timer;
|
|
126
130
|
const bound = new Promise((_, reject) => {
|
|
127
|
-
timer = setTimeout(() => reject(new InstallTimeoutError(
|
|
131
|
+
timer = setTimeout(() => reject(new InstallTimeoutError(timeoutMs)), timeoutMs);
|
|
128
132
|
});
|
|
129
133
|
try {
|
|
130
134
|
await Promise.race([install, bound]);
|
|
@@ -158,7 +162,7 @@ export async function performRun(py, spec, opts = {}) {
|
|
|
158
162
|
if (isCancelled()) {
|
|
159
163
|
throw new AbortedError();
|
|
160
164
|
}
|
|
161
|
-
|
|
165
|
+
log.info('python: installing packages', {
|
|
162
166
|
packages: [...spec.packages],
|
|
163
167
|
timeoutMs: spec.installTimeoutMs
|
|
164
168
|
});
|
|
@@ -167,7 +171,7 @@ export async function performRun(py, spec, opts = {}) {
|
|
|
167
171
|
timeoutMs: spec.installTimeoutMs,
|
|
168
172
|
isCancelled
|
|
169
173
|
});
|
|
170
|
-
|
|
174
|
+
log.info('python: packages ready', { packages: [...spec.packages] });
|
|
171
175
|
}
|
|
172
176
|
catch (error) {
|
|
173
177
|
if (error instanceof AbortedError) {
|
|
@@ -176,7 +180,7 @@ export async function performRun(py, spec, opts = {}) {
|
|
|
176
180
|
const message = errorMessage(error);
|
|
177
181
|
const timedOut = error instanceof InstallTimeoutError;
|
|
178
182
|
const evict = timedOut || isPyodideFatal(error);
|
|
179
|
-
|
|
183
|
+
log.warn(timedOut ? 'python: package install timed out' : 'python: package install failed', {
|
|
180
184
|
packages: [...spec.packages],
|
|
181
185
|
err: message,
|
|
182
186
|
evict
|
|
@@ -3,7 +3,7 @@ import process from 'process';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { errorMessage } from '../../../../guards.js';
|
|
6
|
-
import {
|
|
6
|
+
import { log } from '../../../../log.js';
|
|
7
7
|
const ManifestSchema = z.object({
|
|
8
8
|
pyodide: z.string(),
|
|
9
9
|
packages: z.record(z.string(), z.object({
|
|
@@ -21,14 +21,14 @@ export function wheelsDir() {
|
|
|
21
21
|
export function vendoredManifest(dir = wheelsDir()) {
|
|
22
22
|
const file = `${dir}/manifest.json`;
|
|
23
23
|
if (!fs.existsSync(file)) {
|
|
24
|
-
|
|
24
|
+
log.info('python: no vendored wheels', { dir });
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
28
|
return ManifestSchema.parse(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
|
-
|
|
31
|
+
log.warn('python: vendored wheel manifest unusable', {
|
|
32
32
|
file,
|
|
33
33
|
err: errorMessage(error)
|
|
34
34
|
});
|
|
@@ -72,7 +72,7 @@ export function planVendoredInstall(specs, opts = {}) {
|
|
|
72
72
|
}
|
|
73
73
|
for (const wheel of entry.pypi) {
|
|
74
74
|
if (!fs.existsSync(`${dir}/${wheel}`)) {
|
|
75
|
-
|
|
75
|
+
log.warn('python: vendored wheel missing, installing from PyPI instead', { wheel });
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
78
|
wheels.add(`${dir}/${wheel}`);
|
|
@@ -112,6 +112,6 @@ export function seedPackageCache(cacheDir, dir = wheelsDir()) {
|
|
|
112
112
|
seeded++;
|
|
113
113
|
}
|
|
114
114
|
if (seeded > 0) {
|
|
115
|
-
|
|
115
|
+
log.debug('python: seeded package cache', { cacheDir, wheels: seeded });
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { log } from '../../../../../log.js';
|
|
2
2
|
import { asWorkerMessage } from './protocol.js';
|
|
3
3
|
const SHUTDOWN_GRACE_MS = 3_000;
|
|
4
4
|
let active = null;
|
|
@@ -20,7 +20,7 @@ export async function readMessage(link) {
|
|
|
20
20
|
if (message?.type !== 'log') {
|
|
21
21
|
return message;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
log[message.level](`python worker: ${message.message}`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
export async function drainAndReap(worker) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AbortController } from 'bare-abort-controller';
|
|
2
2
|
import process from 'process';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import { HarnessError } from '../../../../../errors.js';
|
|
4
5
|
import { AbortedError, raceAbort } from '../../../../../helpers/race-abort.js';
|
|
5
|
-
import {
|
|
6
|
+
import { harnessLogLevel, log } from '../../../../../log.js';
|
|
6
7
|
import { RESEND_MS, SIGINT } from '../deadline-state.js';
|
|
7
8
|
import { fromWireResult } from './protocol.js';
|
|
8
9
|
import { activeWorker, destroy, drainAndReap, forget, grace, readMessage } from './active.js';
|
|
@@ -21,18 +22,18 @@ async function awaitReady(link) {
|
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
24
|
if (message?.type === 'boot-error') {
|
|
24
|
-
throw new
|
|
25
|
+
throw new HarnessError('PYTHON_WORKER_FAILED', [message.message]);
|
|
25
26
|
}
|
|
26
27
|
throw new Error('python worker sent an unexpected first message');
|
|
27
28
|
}
|
|
28
29
|
function boot() {
|
|
29
|
-
|
|
30
|
+
log.info('python: booting worker thread');
|
|
30
31
|
const sab = new SharedArrayBuffer(1);
|
|
31
|
-
const link = spawnPythonWorker(workerEntry(), { sab, log:
|
|
32
|
+
const link = spawnPythonWorker(workerEntry(), { sab, log: harnessLogLevel() });
|
|
32
33
|
const worker = {
|
|
33
34
|
link,
|
|
34
35
|
view: new Uint8Array(sab),
|
|
35
|
-
ready: awaitReady(link).then(() =>
|
|
36
|
+
ready: awaitReady(link).then(() => log.info('python: worker ready')),
|
|
36
37
|
nextId: 1
|
|
37
38
|
};
|
|
38
39
|
worker.ready.catch(() => {
|
|
@@ -55,7 +56,7 @@ async function resultFor(worker, id, askConnect) {
|
|
|
55
56
|
return { result: fromWireResult(message.result), evict: message.evict };
|
|
56
57
|
}
|
|
57
58
|
if (message?.type === 'boot-error') {
|
|
58
|
-
throw new
|
|
59
|
+
throw new HarnessError('PYTHON_WORKER_FAILED', [message.message]);
|
|
59
60
|
}
|
|
60
61
|
throw new Error('python worker sent an unexpected message');
|
|
61
62
|
}
|
|
@@ -70,7 +71,7 @@ async function interrupt(worker, outcome) {
|
|
|
70
71
|
timer.expired
|
|
71
72
|
]);
|
|
72
73
|
if (settled === 'grace') {
|
|
73
|
-
|
|
74
|
+
log.warn('python: worker ignored interrupt; killing', { graceMs });
|
|
74
75
|
destroy(worker);
|
|
75
76
|
}
|
|
76
77
|
else {
|
|
@@ -134,7 +135,7 @@ export async function runPythonInWorker(spec, opts) {
|
|
|
134
135
|
}
|
|
135
136
|
if (first === 'timeout') {
|
|
136
137
|
await cancel();
|
|
137
|
-
throw new
|
|
138
|
+
throw new HarnessError('COMMAND_TIMED_OUT', [opts.timeoutMs]);
|
|
138
139
|
}
|
|
139
140
|
if (first.evict) {
|
|
140
141
|
await drainAndReap(worker);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LineLevel } from '../../../../../log.ts';
|
|
1
2
|
import type { PythonRunResult, RunSpec } from '../run-session.ts';
|
|
2
3
|
export type HostMessage = {
|
|
3
4
|
readonly type: 'run';
|
|
@@ -23,6 +24,7 @@ export interface WireResult {
|
|
|
23
24
|
}
|
|
24
25
|
export type WorkerMessage = {
|
|
25
26
|
readonly type: 'log';
|
|
27
|
+
readonly level: LineLevel;
|
|
26
28
|
readonly message: string;
|
|
27
29
|
} | {
|
|
28
30
|
readonly type: 'ready';
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { parentPort, workerData } from 'worker_threads';
|
|
2
2
|
import { errorMessage, isRecord } from '../../../../../guards.js';
|
|
3
|
-
import {
|
|
3
|
+
import { isLogLevel, setHarnessLogLevel, setHarnessLogSink } from '../../../../../log.js';
|
|
4
4
|
import { performRun } from '../run-session.js';
|
|
5
5
|
import { asHostMessage, toWireResult } from './protocol.js';
|
|
6
6
|
function narrowThreadData(data) {
|
|
7
7
|
if (isRecord(data) && 'sab' in data) {
|
|
8
8
|
const sab = data.sab;
|
|
9
|
-
const log = 'log' in data && data.log
|
|
9
|
+
const log = 'log' in data && isLogLevel(data.log) ? data.log : 'off';
|
|
10
10
|
if (sab instanceof SharedArrayBuffer) {
|
|
11
11
|
return { sab, log };
|
|
12
12
|
}
|
|
@@ -73,8 +73,8 @@ async function serveRuns(port, py) {
|
|
|
73
73
|
}
|
|
74
74
|
export async function serve(load) {
|
|
75
75
|
const { port, sab, log } = openPort();
|
|
76
|
-
|
|
77
|
-
setHarnessLogSink((message) => port.write({ type: 'log', message }));
|
|
76
|
+
setHarnessLogLevel(log);
|
|
77
|
+
setHarnessLogSink((message, level) => port.write({ type: 'log', level, message }));
|
|
78
78
|
try {
|
|
79
79
|
const py = await load();
|
|
80
80
|
py.setInterruptBuffer(new Uint8Array(sab));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LogLevel } from '../../../../../log.ts';
|
|
1
2
|
export interface WorkerLink {
|
|
2
3
|
read(): Promise<unknown>;
|
|
3
4
|
write(message: unknown): Promise<void>;
|
|
@@ -8,6 +9,6 @@ export interface WorkerLink {
|
|
|
8
9
|
}
|
|
9
10
|
export interface WorkerData {
|
|
10
11
|
readonly sab: SharedArrayBuffer;
|
|
11
|
-
readonly log:
|
|
12
|
+
readonly log: LogLevel;
|
|
12
13
|
}
|
|
13
14
|
export declare function spawnPythonWorker(entry: string, data: WorkerData): WorkerLink;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Worker } from 'worker_threads';
|
|
2
|
+
import { HarnessError } from '../../../../../errors.js';
|
|
2
3
|
export function spawnPythonWorker(entry, data) {
|
|
3
4
|
const worker = new Worker(entry, {
|
|
4
5
|
workerData: data,
|
|
@@ -26,7 +27,7 @@ export function spawnPythonWorker(entry, data) {
|
|
|
26
27
|
};
|
|
27
28
|
worker.on('message', push);
|
|
28
29
|
worker.on('error', (error) => fail(error instanceof Error ? error : new Error(String(error))));
|
|
29
|
-
worker.on('exit', (code) => fail(new
|
|
30
|
+
worker.on('exit', (code) => fail(new HarnessError('PYTHON_WORKER_EXITED', [code])));
|
|
30
31
|
worker.unref();
|
|
31
32
|
return {
|
|
32
33
|
read() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { HarnessError } from '../../../errors.js';
|
|
1
2
|
export function markdownFileName(name) {
|
|
2
3
|
if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
|
|
3
|
-
throw new
|
|
4
|
+
throw new HarnessError('OBSIDIAN_NOTE_NAME_IS_PATH');
|
|
4
5
|
}
|
|
5
6
|
return name.endsWith('.md') ? name : `${name}.md`;
|
|
6
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { HarnessError } from '../../../errors.js';
|
|
3
4
|
function lstatOrNull(p) {
|
|
4
5
|
try {
|
|
5
6
|
return fs.lstatSync(p);
|
|
@@ -18,7 +19,7 @@ function rejectSymlinkComponents(root, target) {
|
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
if (stat.isSymbolicLink()) {
|
|
21
|
-
throw new
|
|
22
|
+
throw new HarnessError('OBSIDIAN_PATH_CROSSES_SYMLINK', [target]);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -26,7 +27,7 @@ export function ensureWithinRoot(root, rel) {
|
|
|
26
27
|
const resolvedRoot = path.resolve(root);
|
|
27
28
|
const target = path.resolve(resolvedRoot, rel);
|
|
28
29
|
if (target !== resolvedRoot && !target.startsWith(resolvedRoot + path.sep)) {
|
|
29
|
-
throw new
|
|
30
|
+
throw new HarnessError('OBSIDIAN_PATH_ESCAPES_VAULT', [rel]);
|
|
30
31
|
}
|
|
31
32
|
rejectSymlinkComponents(resolvedRoot, target);
|
|
32
33
|
return target;
|
|
@@ -36,7 +37,7 @@ export function isSymlink(p) {
|
|
|
36
37
|
}
|
|
37
38
|
export function writeNewFileText(filePath, content) {
|
|
38
39
|
if (fs.existsSync(filePath)) {
|
|
39
|
-
throw new
|
|
40
|
+
throw new HarnessError('OBSIDIAN_DESTINATION_EXISTS', [filePath]);
|
|
40
41
|
}
|
|
41
42
|
let fd;
|
|
42
43
|
try {
|
|
@@ -44,7 +45,7 @@ export function writeNewFileText(filePath, content) {
|
|
|
44
45
|
}
|
|
45
46
|
catch (error) {
|
|
46
47
|
if (fs.existsSync(filePath)) {
|
|
47
|
-
throw new
|
|
48
|
+
throw new HarnessError('OBSIDIAN_DESTINATION_EXISTS', [filePath]);
|
|
48
49
|
}
|
|
49
50
|
throw error;
|
|
50
51
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { HarnessError } from '../../../errors.js';
|
|
3
4
|
import { markdownFileName } from './note-name.js';
|
|
4
5
|
import { ensureWithinRoot, getArg, getContentArg, writeNewFileText } from './vault-paths.js';
|
|
5
6
|
function markdownPath(vaultPath, target) {
|
|
@@ -20,7 +21,7 @@ function createNamed(vaultPath, { name, folder, content }) {
|
|
|
20
21
|
const fileName = markdownFileName(name);
|
|
21
22
|
const filePath = ensureWithinRoot(vaultPath, path.join(folder, fileName));
|
|
22
23
|
if (fs.existsSync(filePath)) {
|
|
23
|
-
throw new
|
|
24
|
+
throw new HarnessError('OBSIDIAN_NOTE_EXISTS', [name, folder || '/']);
|
|
24
25
|
}
|
|
25
26
|
writeNewFileText(filePath, content);
|
|
26
27
|
return `Created: ${folder ? `${folder}/` : ''}${fileName}`;
|
|
@@ -42,7 +43,7 @@ export function handleCreate(vaultPath, args) {
|
|
|
42
43
|
}
|
|
43
44
|
const target = filePath ?? file;
|
|
44
45
|
if (!target) {
|
|
45
|
-
throw new
|
|
46
|
+
throw new HarnessError('OBSIDIAN_MISSING_ARGUMENT', ['name=, file=, or path=']);
|
|
46
47
|
}
|
|
47
48
|
return createAtPath(vaultPath, target, content || '');
|
|
48
49
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HarnessError } from '../../../errors.js';
|
|
1
2
|
import { handleCreate } from './vault-create.js';
|
|
2
3
|
import { todayStamp } from './vault-paths.js';
|
|
3
4
|
import { handleDailyRead, handleFiles, handleFolders, handleOutline, handleProperties, handleRead, handleSearch, handleTags, handleVault, handleVaults, handleWordcount } from './vault-read.js';
|
|
@@ -51,7 +52,6 @@ export function executeVaultFsAction(vaultPath, action, args) {
|
|
|
51
52
|
case 'daily:path':
|
|
52
53
|
return `Daily Notes/${todayStamp()}.md`;
|
|
53
54
|
default:
|
|
54
|
-
throw new
|
|
55
|
-
`This action requires the Obsidian CLI connected to the running app.`);
|
|
55
|
+
throw new HarnessError('OBSIDIAN_ACTION_UNSUPPORTED', [action]);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { HarnessError } from '../../../errors.js';
|
|
3
4
|
import { ensureWithinRoot, isSymlink, writeNewFileText } from './safe-path.js';
|
|
4
5
|
export { ensureWithinRoot, writeNewFileText } from './safe-path.js';
|
|
5
6
|
export const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
@@ -90,13 +91,13 @@ export function writeIfUnchanged(filePath, content, expectedMtimeMs) {
|
|
|
90
91
|
}
|
|
91
92
|
if (expectedMtimeMs === null) {
|
|
92
93
|
if (current) {
|
|
93
|
-
throw new
|
|
94
|
+
throw new HarnessError('OBSIDIAN_NOTE_CHANGED', ['file appeared', path.basename(filePath)]);
|
|
94
95
|
}
|
|
95
96
|
fs.writeFileSync(filePath, content);
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
if (!current || current.mtimeMs !== expectedMtimeMs) {
|
|
99
|
-
throw new
|
|
100
|
+
throw new HarnessError('OBSIDIAN_NOTE_CHANGED', ['re-read and retry', path.basename(filePath)]);
|
|
100
101
|
}
|
|
101
102
|
fs.writeFileSync(filePath, content);
|
|
102
103
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { HarnessError } from '../../../errors.js';
|
|
3
4
|
import { dailyCandidates, ensureWithinRoot, FRONTMATTER_RE, getArg, listFolders, listMarkdownFiles, parseFrontmatter, readFileText, resolveNotePath, todayStamp } from './vault-paths.js';
|
|
4
5
|
export function handleVault(vaultPath) {
|
|
5
6
|
return JSON.stringify({ name: path.basename(vaultPath), path: vaultPath });
|
|
@@ -17,7 +18,7 @@ export function handleVaults(vaultPath) {
|
|
|
17
18
|
export function handleRead(vaultPath, args) {
|
|
18
19
|
const notePath = resolveNotePath(vaultPath, args);
|
|
19
20
|
if (!notePath || !fs.existsSync(notePath)) {
|
|
20
|
-
throw new
|
|
21
|
+
throw new HarnessError('OBSIDIAN_NOTE_NOT_FOUND', [args]);
|
|
21
22
|
}
|
|
22
23
|
return readFileText(notePath);
|
|
23
24
|
}
|
|
@@ -43,7 +44,7 @@ function parseLimit(args) {
|
|
|
43
44
|
export function handleSearch(vaultPath, args) {
|
|
44
45
|
const query = getArg(args, 'query');
|
|
45
46
|
if (!query) {
|
|
46
|
-
throw new
|
|
47
|
+
throw new HarnessError('OBSIDIAN_MISSING_ARGUMENT', ['query=']);
|
|
47
48
|
}
|
|
48
49
|
const limit = parseLimit(args);
|
|
49
50
|
const lowerQuery = query.toLowerCase();
|
|
@@ -70,7 +71,7 @@ export function handleSearch(vaultPath, args) {
|
|
|
70
71
|
export function handleProperties(vaultPath, args) {
|
|
71
72
|
const notePath = resolveNotePath(vaultPath, args);
|
|
72
73
|
if (!notePath || !fs.existsSync(notePath)) {
|
|
73
|
-
throw new
|
|
74
|
+
throw new HarnessError('OBSIDIAN_NOTE_NOT_FOUND', [args]);
|
|
74
75
|
}
|
|
75
76
|
return JSON.stringify(parseFrontmatter(readFileText(notePath)), null, 2);
|
|
76
77
|
}
|
|
@@ -99,7 +100,7 @@ export function handleTags(vaultPath) {
|
|
|
99
100
|
export function handleWordcount(vaultPath, args) {
|
|
100
101
|
const notePath = resolveNotePath(vaultPath, args);
|
|
101
102
|
if (!notePath || !fs.existsSync(notePath)) {
|
|
102
|
-
throw new
|
|
103
|
+
throw new HarnessError('OBSIDIAN_NOTE_NOT_FOUND', [args]);
|
|
103
104
|
}
|
|
104
105
|
const body = readFileText(notePath).replace(FRONTMATTER_RE, '').trim();
|
|
105
106
|
const words = body.split(/\s+/).filter(Boolean).length;
|
|
@@ -108,7 +109,7 @@ export function handleWordcount(vaultPath, args) {
|
|
|
108
109
|
export function handleOutline(vaultPath, args) {
|
|
109
110
|
const notePath = resolveNotePath(vaultPath, args);
|
|
110
111
|
if (!notePath || !fs.existsSync(notePath)) {
|
|
111
|
-
throw new
|
|
112
|
+
throw new HarnessError('OBSIDIAN_NOTE_NOT_FOUND', [args]);
|
|
112
113
|
}
|
|
113
114
|
const headings = readFileText(notePath)
|
|
114
115
|
.split('\n')
|