minutes-mcp 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autoInstall.d.ts +22 -0
- package/dist/autoInstall.js +32 -0
- package/dist/corpus-lease-worker.d.ts +1 -0
- package/dist/corpus-lease-worker.js +94 -0
- package/dist/corpus-lease.d.ts +176 -0
- package/dist/corpus-lease.js +1911 -0
- package/dist/httpTransport.d.ts +118 -0
- package/dist/httpTransport.js +446 -0
- package/dist/index.d.ts +733 -18
- package/dist/index.js +4467 -1252
- package/dist/paths.d.ts +14 -0
- package/dist/paths.js +33 -6
- package/dist/process-audio-helper.d.ts +12 -0
- package/dist/process-audio-helper.js +333 -0
- package/dist/secure-read.d.ts +85 -0
- package/dist/secure-read.js +783 -0
- package/dist-ui/index.html +100 -90
- package/package.json +9 -4
package/dist/autoInstall.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type ExecFileAsync = (file: string, args: readonly string[], options?: {
|
|
2
2
|
timeout?: number;
|
|
3
|
+
env?: NodeJS.ProcessEnv;
|
|
3
4
|
}) => Promise<unknown>;
|
|
4
5
|
export type Sha256Entry = {
|
|
5
6
|
filename: string;
|
|
@@ -9,6 +10,27 @@ export declare function parseSha256Sums(raw: string): Sha256Entry[];
|
|
|
9
10
|
export declare function findSha256ForAsset(raw: string, assetName: string): string | null;
|
|
10
11
|
export declare function computeFileSha256(path: string): Promise<string>;
|
|
11
12
|
export declare function verifyDownloadedAsset(path: string, expectedSha256: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Extract a zip on Windows via PowerShell's Expand-Archive.
|
|
15
|
+
*
|
|
16
|
+
* The paths travel in the environment, never in the command text. Two ways to
|
|
17
|
+
* get this wrong, both of which shipped:
|
|
18
|
+
*
|
|
19
|
+
* - Interpolating into a single-quoted PowerShell string breaks on a home
|
|
20
|
+
* directory containing an apostrophe (`C:\Users\O'Brien`).
|
|
21
|
+
* - Passing them as trailing arguments and reading `$args[0]` does not work at
|
|
22
|
+
* all: `powershell -Command` appends trailing arguments to the command text
|
|
23
|
+
* and leaves `$args` empty (only `-File` binds them), so `$args[0]` is
|
|
24
|
+
* `$null` and `Expand-Archive` rejects the parameter on every machine.
|
|
25
|
+
*
|
|
26
|
+
* `$env:` lookups are read at runtime and are never parsed as script, so no
|
|
27
|
+
* value of either path can alter the command.
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractZipWithPowerShell(options: {
|
|
30
|
+
archivePath: string;
|
|
31
|
+
destDir: string;
|
|
32
|
+
execFileAsync: ExecFileAsync;
|
|
33
|
+
}): Promise<void>;
|
|
12
34
|
export declare function downloadReleaseBinaryWithChecksum(options: {
|
|
13
35
|
binaryName: string;
|
|
14
36
|
targetPath: string;
|
package/dist/autoInstall.js
CHANGED
|
@@ -36,6 +36,38 @@ export async function verifyDownloadedAsset(path, expectedSha256) {
|
|
|
36
36
|
throw new Error(`checksum mismatch: expected ${expectedSha256.toLowerCase()}, got ${actual}`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract a zip on Windows via PowerShell's Expand-Archive.
|
|
41
|
+
*
|
|
42
|
+
* The paths travel in the environment, never in the command text. Two ways to
|
|
43
|
+
* get this wrong, both of which shipped:
|
|
44
|
+
*
|
|
45
|
+
* - Interpolating into a single-quoted PowerShell string breaks on a home
|
|
46
|
+
* directory containing an apostrophe (`C:\Users\O'Brien`).
|
|
47
|
+
* - Passing them as trailing arguments and reading `$args[0]` does not work at
|
|
48
|
+
* all: `powershell -Command` appends trailing arguments to the command text
|
|
49
|
+
* and leaves `$args` empty (only `-File` binds them), so `$args[0]` is
|
|
50
|
+
* `$null` and `Expand-Archive` rejects the parameter on every machine.
|
|
51
|
+
*
|
|
52
|
+
* `$env:` lookups are read at runtime and are never parsed as script, so no
|
|
53
|
+
* value of either path can alter the command.
|
|
54
|
+
*/
|
|
55
|
+
export async function extractZipWithPowerShell(options) {
|
|
56
|
+
const { archivePath, destDir, execFileAsync } = options;
|
|
57
|
+
await execFileAsync("powershell", [
|
|
58
|
+
"-NoProfile",
|
|
59
|
+
"-NonInteractive",
|
|
60
|
+
"-Command",
|
|
61
|
+
"Expand-Archive -Path $env:MINUTES_ZIP_PATH -DestinationPath $env:MINUTES_ZIP_DEST -Force",
|
|
62
|
+
], {
|
|
63
|
+
timeout: 120000,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
MINUTES_ZIP_PATH: archivePath,
|
|
67
|
+
MINUTES_ZIP_DEST: destDir,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
39
71
|
export async function downloadReleaseBinaryWithChecksum(options) {
|
|
40
72
|
const { binaryName, targetPath, execFileAsync } = options;
|
|
41
73
|
const baseUrl = options.baseUrl ?? "https://github.com/silverstein/minutes/releases/latest/download";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { markCorpusLeaseWorkerProcess, runCorpusLeaseWorkerRequest, } from "./corpus-lease.js";
|
|
3
|
+
import { retireBoundReadersForProcessShutdown } from "./secure-read.js";
|
|
4
|
+
// Content is sent in 64 KiB raw chunks, so every control line stays far below
|
|
5
|
+
// this fixed ceiling and never scales with the full corpus size.
|
|
6
|
+
const MAX_CONTROL_LINE_BYTES = 512 * 1024;
|
|
7
|
+
markCorpusLeaseWorkerProcess();
|
|
8
|
+
function fail() {
|
|
9
|
+
process.exit(70);
|
|
10
|
+
}
|
|
11
|
+
function send(message) {
|
|
12
|
+
const serialized = JSON.stringify(message);
|
|
13
|
+
if (Buffer.byteLength(serialized) > MAX_CONTROL_LINE_BYTES)
|
|
14
|
+
fail();
|
|
15
|
+
process.stdout.write(serialized + "\n");
|
|
16
|
+
}
|
|
17
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
18
|
+
const responses = [];
|
|
19
|
+
const responseWaiters = [];
|
|
20
|
+
const pauseWaiters = new Map();
|
|
21
|
+
lines.on("line", (line) => {
|
|
22
|
+
if (Buffer.byteLength(line) > MAX_CONTROL_LINE_BYTES)
|
|
23
|
+
fail();
|
|
24
|
+
let message;
|
|
25
|
+
try {
|
|
26
|
+
message = JSON.parse(line);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
fail();
|
|
30
|
+
}
|
|
31
|
+
if (message?.type === "resume" && Number.isSafeInteger(message.id)) {
|
|
32
|
+
const resume = pauseWaiters.get(message.id);
|
|
33
|
+
if (!resume)
|
|
34
|
+
fail();
|
|
35
|
+
pauseWaiters.delete(message.id);
|
|
36
|
+
resume();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const waiter = responseWaiters.shift();
|
|
40
|
+
if (waiter)
|
|
41
|
+
waiter(message);
|
|
42
|
+
else
|
|
43
|
+
responses.push(message);
|
|
44
|
+
});
|
|
45
|
+
lines.on("close", () => fail());
|
|
46
|
+
function nextResponse() {
|
|
47
|
+
const ready = responses.shift();
|
|
48
|
+
if (ready !== undefined)
|
|
49
|
+
return Promise.resolve(ready);
|
|
50
|
+
return new Promise((resolve) => responseWaiters.push(resolve));
|
|
51
|
+
}
|
|
52
|
+
const bridge = {
|
|
53
|
+
exchange: async (message) => {
|
|
54
|
+
send(message);
|
|
55
|
+
return nextResponse();
|
|
56
|
+
},
|
|
57
|
+
pause: (id, reservedEvent) => {
|
|
58
|
+
if (!Number.isSafeInteger(id) || pauseWaiters.has(id))
|
|
59
|
+
fail();
|
|
60
|
+
let resume;
|
|
61
|
+
const promise = new Promise((resolve) => {
|
|
62
|
+
resume = resolve;
|
|
63
|
+
});
|
|
64
|
+
pauseWaiters.set(id, resume);
|
|
65
|
+
return {
|
|
66
|
+
promise,
|
|
67
|
+
onReserved: () => send({ type: reservedEvent, id }),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
let exitCode = 0;
|
|
72
|
+
try {
|
|
73
|
+
const first = await nextResponse();
|
|
74
|
+
if (!first ||
|
|
75
|
+
first.type !== "begin" ||
|
|
76
|
+
!first.request ||
|
|
77
|
+
typeof first.request.root !== "string" ||
|
|
78
|
+
!first.request.budgets ||
|
|
79
|
+
!Number.isSafeInteger(first.request.timeoutMs) ||
|
|
80
|
+
!Array.isArray(first.request.hookNames)) {
|
|
81
|
+
fail();
|
|
82
|
+
}
|
|
83
|
+
await runCorpusLeaseWorkerRequest(first.request, bridge);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
exitCode = 70;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await retireBoundReadersForProcessShutdown();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
exitCode = 70;
|
|
93
|
+
}
|
|
94
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
export type CorpusReadBudgets = {
|
|
2
|
+
maxFileBytes: number;
|
|
3
|
+
maxCorpusBytes: number;
|
|
4
|
+
maxRetainedPathBytes: number;
|
|
5
|
+
maxFileCount: number;
|
|
6
|
+
maxDirectoryCount: number;
|
|
7
|
+
maxDirectoryEntries: number;
|
|
8
|
+
maxWatcherCount: number;
|
|
9
|
+
maxReaderCount: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const DEFAULT_CORPUS_READ_BUDGETS: Readonly<CorpusReadBudgets>;
|
|
12
|
+
export type CorpusVerificationStats = Readonly<{
|
|
13
|
+
fileCount: number;
|
|
14
|
+
retainedContentBytes: number;
|
|
15
|
+
totalBytes: number;
|
|
16
|
+
}>;
|
|
17
|
+
export type StableCorpusFile = Readonly<{
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly relativePath: string;
|
|
20
|
+
readonly content: string;
|
|
21
|
+
}>;
|
|
22
|
+
export type StableCorpusSnapshot = Readonly<{
|
|
23
|
+
readonly canonicalRoot: string;
|
|
24
|
+
files: readonly StableCorpusFile[];
|
|
25
|
+
}>;
|
|
26
|
+
export type CorpusLeaseControls = {
|
|
27
|
+
failWatcher: (reason?: string) => void;
|
|
28
|
+
suppressNextFence: () => void;
|
|
29
|
+
requireRepulseForNextFence: () => void;
|
|
30
|
+
failNextFencePulse: () => void;
|
|
31
|
+
failNextSentinelOpen: () => void;
|
|
32
|
+
pauseNextSentinelOpen: (until: Promise<void>, onReserved?: () => void) => void;
|
|
33
|
+
pauseNextFenceAfterPending: (until: Promise<void>, onPending?: () => void) => void;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Test/diagnostic hooks for deterministic authorization-race coverage.
|
|
37
|
+
* Every awaited hook runs before the final sentinel fence; the successful
|
|
38
|
+
* final fence remains the operation's linearization point.
|
|
39
|
+
*/
|
|
40
|
+
export type CorpusLeaseHooks = {
|
|
41
|
+
/** Explicit corpus resource limits; omitted fields use the safe defaults. */
|
|
42
|
+
budgets?: Partial<CorpusReadBudgets>;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
beforeSentinelCreate?: (context: {
|
|
45
|
+
attempt: number;
|
|
46
|
+
slot: number;
|
|
47
|
+
capacity: Readonly<{
|
|
48
|
+
globalReserved: number;
|
|
49
|
+
globalRetained: number;
|
|
50
|
+
rootReserved: number;
|
|
51
|
+
}>;
|
|
52
|
+
}) => void | Promise<void>;
|
|
53
|
+
onWatcherReady?: (context: {
|
|
54
|
+
attempt: number;
|
|
55
|
+
controls: CorpusLeaseControls;
|
|
56
|
+
}) => void | Promise<void>;
|
|
57
|
+
afterBaseline?: (context: {
|
|
58
|
+
attempt: number;
|
|
59
|
+
controls: CorpusLeaseControls;
|
|
60
|
+
}) => void | Promise<void>;
|
|
61
|
+
beforeFinalManifest?: (context: {
|
|
62
|
+
attempt: number;
|
|
63
|
+
controls: CorpusLeaseControls;
|
|
64
|
+
}) => void | Promise<void>;
|
|
65
|
+
afterFinalManifest?: (context: {
|
|
66
|
+
attempt: number;
|
|
67
|
+
controls: CorpusLeaseControls;
|
|
68
|
+
verification: CorpusVerificationStats;
|
|
69
|
+
}) => void | Promise<void>;
|
|
70
|
+
beforeFinalFence?: (context: {
|
|
71
|
+
attempt: number;
|
|
72
|
+
controls: CorpusLeaseControls;
|
|
73
|
+
}) => void | Promise<void>;
|
|
74
|
+
/** Test-only worker entry override for hostile protocol fixtures. */
|
|
75
|
+
workerScriptForTest?: string;
|
|
76
|
+
/** Test-only deterministic worker stall injection. */
|
|
77
|
+
workerStallPhaseForTest?: "before-baseline" | "after-baseline" | "before-finalize" | "before-authorized";
|
|
78
|
+
/** Test-only deterministic parent deadline after the projection has started. */
|
|
79
|
+
operationDeadlineForTest?: Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Test-only: treat worker termination as unconfirmed, whatever really happened.
|
|
82
|
+
*
|
|
83
|
+
* Whether a kill is confirmed inside the grace window is a race a test cannot
|
|
84
|
+
* force: SIGKILL is untrappable, so no fixture can refuse to die on cue, and
|
|
85
|
+
* a shortened window is not enough either. A never-fed child dies almost
|
|
86
|
+
* immediately, so the termination promise usually wins even a zero-length
|
|
87
|
+
* race, and a test written that way passes without ever entering the branch
|
|
88
|
+
* it claims to cover.
|
|
89
|
+
*
|
|
90
|
+
* A boolean forces the branch instead. It is also the least powerful shape
|
|
91
|
+
* available: no delay to coerce, nothing to hold cleanup open, and it can
|
|
92
|
+
* only make the parent more conservative, since an unconfirmed kill of a
|
|
93
|
+
* worker that knew the corpus still poisons.
|
|
94
|
+
*/
|
|
95
|
+
forceUnconfirmedTerminationForTest?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Held *in addition to* the real reap before a stranded child counts as
|
|
98
|
+
* confirmed dead. Without it both halves of the contract race, because the
|
|
99
|
+
* child is SIGKILLed and so reaped within milliseconds: a test cannot
|
|
100
|
+
* observe "still refused" before recovery overtakes it.
|
|
101
|
+
*
|
|
102
|
+
* A promise the test never settles pins the refusing half; one it settles
|
|
103
|
+
* pins the recovering half. Because recovery waits for this promise *and*
|
|
104
|
+
* the real termination, it can only ever delay recovery relative to
|
|
105
|
+
* production. It cannot unlock the process before the child is genuinely
|
|
106
|
+
* reaped, so it cannot manufacture a pass out of a parent that fails closed
|
|
107
|
+
* too weakly. An earlier draft used `??` here, which substituted the hook
|
|
108
|
+
* for real termination and did exactly that.
|
|
109
|
+
*/
|
|
110
|
+
confirmTerminationForTest?: Promise<void>;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Test-only: give this process's deferred releases a chance to settle.
|
|
114
|
+
*
|
|
115
|
+
* Best-effort and bounded, deliberately. The flake this exists for is a release
|
|
116
|
+
* that has not settled YET: a killed child whose termination lands as soon as
|
|
117
|
+
* the event loop breathes. Awaiting gives it that chance, so the next case
|
|
118
|
+
* starts from a clean charge instead of inheriting one.
|
|
119
|
+
*
|
|
120
|
+
* It does NOT throw on an unsettled deferral, because "never settles" is a
|
|
121
|
+
* supported end state, not a defect: a projection that ignores cancellation
|
|
122
|
+
* poisons admission permanently and by design, and the test covering that would
|
|
123
|
+
* fail a stricter hook for doing its job. Returns whether everything settled,
|
|
124
|
+
* for a caller that wants to know.
|
|
125
|
+
*
|
|
126
|
+
* Measured, so the budget is not a guess: one stranded child holds 15,335,424
|
|
127
|
+
* bytes against MAX_RETAINED_CORPUS_MEMORY_BYTES, awaiting while the child is
|
|
128
|
+
* still unreaped clears nothing, and awaiting after the reap lands clears it to
|
|
129
|
+
* zero. The bound only has to cover the gap between a case ending and its
|
|
130
|
+
* child's reap arriving, which is milliseconds unless the machine is saturated,
|
|
131
|
+
* so it is set well above that and still far below vitest's hook timeout.
|
|
132
|
+
*/
|
|
133
|
+
export declare function awaitDeferredCorpusReleasesForTests(timeoutMs?: number): Promise<boolean>;
|
|
134
|
+
/** Test-only: the live retained-corpus charge, for leak assertions. */
|
|
135
|
+
export declare function reservedCorpusMemoryBytesForTests(): number;
|
|
136
|
+
export type CorpusLeaseWorkerRequest = Readonly<{
|
|
137
|
+
root: string;
|
|
138
|
+
budgets: Readonly<CorpusReadBudgets>;
|
|
139
|
+
timeoutMs: number;
|
|
140
|
+
hookNames: readonly string[];
|
|
141
|
+
stallPhase?: CorpusLeaseHooks["workerStallPhaseForTest"];
|
|
142
|
+
}>;
|
|
143
|
+
export type CorpusLeaseWorkerBridge = {
|
|
144
|
+
exchange: (message: unknown) => Promise<any>;
|
|
145
|
+
pause: (id: number, reservedEvent: string) => {
|
|
146
|
+
promise: Promise<void>;
|
|
147
|
+
onReserved: () => void;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
/** Mark this short-lived helper before it touches the caller's corpus. */
|
|
151
|
+
export declare function markCorpusLeaseWorkerProcess(): void;
|
|
152
|
+
/** Worker-only entry point used by corpus-lease-worker.ts. */
|
|
153
|
+
export declare function runCorpusLeaseWorkerRequest(request: CorpusLeaseWorkerRequest, bridge: CorpusLeaseWorkerBridge): Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* Run a multi-source projection while a killable worker owns every corpus
|
|
156
|
+
* traversal, exact read, watcher, fence, and cleanup filesystem operation.
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* Authorize a stable corpus read, presenting one error for one condition.
|
|
160
|
+
*
|
|
161
|
+
* Every way the lease can refuse is a refusal, and callers get the same
|
|
162
|
+
* sentence for all of them. Without this, which sentence you saw depended on
|
|
163
|
+
* where the failure landed: `fail()` wraps refusals raised once the worker is
|
|
164
|
+
* running, but a `CorpusLeaseChangedError` thrown before that, such as the
|
|
165
|
+
* deadline guard at the top of the dispatch below, escaped raw.
|
|
166
|
+
*
|
|
167
|
+
* That is observable, not cosmetic. On a contended Windows runner a short
|
|
168
|
+
* budget can elapse between computing the deadline and the very next
|
|
169
|
+
* statement checking it, so the same timeout surfaced as
|
|
170
|
+
* "meeting corpus authorization deadline elapsed" instead of the documented
|
|
171
|
+
* refusal, and CI failed with a message mismatch rather than a real defect.
|
|
172
|
+
*
|
|
173
|
+
* `CorpusLeaseBudgetError` and the plain `Error` refusals keep their own text:
|
|
174
|
+
* those name a caller-actionable limit rather than an authorization outcome.
|
|
175
|
+
*/
|
|
176
|
+
export declare function withStableCorpusLease<T>(root: string, operation: (snapshot: StableCorpusSnapshot, attempt: number, signal: AbortSignal) => T | Promise<T>, hooks?: CorpusLeaseHooks): Promise<T>;
|