@minionry/minion 0.7.36 → 0.7.39
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/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -11,6 +11,7 @@ export const APP_TOOL_MAX_RESULT_BYTES = 1024 * 1024;
|
|
|
11
11
|
const registrations = new Map();
|
|
12
12
|
const senders = new Map();
|
|
13
13
|
const pending = new Map();
|
|
14
|
+
const detachedViewers = new Set();
|
|
14
15
|
export class AppToolCallError extends Error {
|
|
15
16
|
code;
|
|
16
17
|
constructor(code, message) {
|
|
@@ -222,6 +223,13 @@ export function callAppTool(args) {
|
|
|
222
223
|
const spec = registration.tools.get(args.tool);
|
|
223
224
|
const timeoutMs = Math.min(args.timeoutMs ?? spec.timeoutMs ?? APP_TOOL_CALL_TIMEOUT_MS, APP_TOOL_CALL_TIMEOUT_MAX_MS);
|
|
224
225
|
const callId = `tc-${randomUUID()}`;
|
|
226
|
+
const frame = {
|
|
227
|
+
op: 'call',
|
|
228
|
+
callId,
|
|
229
|
+
requestId: registration.requestId,
|
|
230
|
+
tool: args.tool,
|
|
231
|
+
...(args.input !== undefined ? { input: args.input } : {}),
|
|
232
|
+
};
|
|
225
233
|
return new Promise((resolve, reject) => {
|
|
226
234
|
const timer = setTimeout(() => {
|
|
227
235
|
const call = pending.get(callId);
|
|
@@ -238,19 +246,44 @@ export function callAppTool(args) {
|
|
|
238
246
|
appId: args.appId,
|
|
239
247
|
spaceKey: args.spaceKey,
|
|
240
248
|
tool: args.tool,
|
|
249
|
+
frame,
|
|
241
250
|
timer,
|
|
242
251
|
settle: resolve,
|
|
243
252
|
fail: reject,
|
|
244
253
|
});
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
callId,
|
|
248
|
-
requestId: registration.requestId,
|
|
249
|
-
tool: args.tool,
|
|
250
|
-
...(args.input !== undefined ? { input: args.input } : {}),
|
|
251
|
-
});
|
|
254
|
+
if (!isDetached(registration))
|
|
255
|
+
sendToolCall(registration, frame);
|
|
252
256
|
});
|
|
253
257
|
}
|
|
258
|
+
function isDetached(registration) {
|
|
259
|
+
return registration.viewerId !== undefined && detachedViewers.has(registration.viewerId);
|
|
260
|
+
}
|
|
261
|
+
export function holdAppToolCallsFor(viewerId) {
|
|
262
|
+
if (servesAnything(viewerId))
|
|
263
|
+
detachedViewers.add(viewerId);
|
|
264
|
+
}
|
|
265
|
+
function servesAnything(viewerId) {
|
|
266
|
+
for (const registration of registrations.values()) {
|
|
267
|
+
if (registration.viewerId === viewerId)
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
export function resumeAppToolCallsFor(viewerId) {
|
|
273
|
+
if (!detachedViewers.delete(viewerId))
|
|
274
|
+
return 0;
|
|
275
|
+
let sent = 0;
|
|
276
|
+
for (const call of [...pending.values()]) {
|
|
277
|
+
if (!pending.has(call.callId))
|
|
278
|
+
continue;
|
|
279
|
+
const registration = registrations.get(call.registrationId);
|
|
280
|
+
if (registration?.viewerId !== viewerId)
|
|
281
|
+
continue;
|
|
282
|
+
sendToolCall(registration, call.frame);
|
|
283
|
+
sent += 1;
|
|
284
|
+
}
|
|
285
|
+
return sent;
|
|
286
|
+
}
|
|
254
287
|
export function cancelAppToolCall(callId, cause = 'cancelled by the host') {
|
|
255
288
|
const call = pending.get(callId);
|
|
256
289
|
if (!call)
|
|
@@ -267,6 +300,9 @@ export function retireAppToolRegistration(requestId, cause) {
|
|
|
267
300
|
return false;
|
|
268
301
|
registrations.delete(requestId);
|
|
269
302
|
senders.delete(requestId);
|
|
303
|
+
if (registration.viewerId !== undefined && !servesAnything(registration.viewerId)) {
|
|
304
|
+
detachedViewers.delete(registration.viewerId);
|
|
305
|
+
}
|
|
270
306
|
for (const call of [...pending.values()]) {
|
|
271
307
|
if (call.registrationId !== requestId)
|
|
272
308
|
continue;
|
|
@@ -300,4 +336,5 @@ export function resetAppTools() {
|
|
|
300
336
|
pending.clear();
|
|
301
337
|
registrations.clear();
|
|
302
338
|
senders.clear();
|
|
339
|
+
detachedViewers.clear();
|
|
303
340
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { closeSync, fchmodSync, fsyncSync, mkdirSync, openSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { SdkMethodError } from './registry.js';
|
|
6
|
+
export const MTIME_TOLERANCE_MS = 1;
|
|
7
|
+
const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
8
|
+
export function parseExpectedModifiedAt(params, method) {
|
|
9
|
+
const value = params.expectedModifiedAt;
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (typeof value !== 'string' || !ISO_DATE_TIME.test(value) || Number.isNaN(Date.parse(value))) {
|
|
13
|
+
throw new SdkMethodError('BAD_REQUEST', `${method}: expectedModifiedAt must be an ISO-8601 timestamp with a zone, such as the modifiedAt files.readEntry returns`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
let tempCounter = 0;
|
|
18
|
+
function statOrUndefined(path) {
|
|
19
|
+
try {
|
|
20
|
+
return statSync(path);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function refuseUnlessUnmodified(existing, expected, displayPath) {
|
|
27
|
+
if (!existing) {
|
|
28
|
+
throw new SdkMethodError('CONFLICT', `'${displayPath}' no longer exists (expected it unmodified since ${expected}) — nothing was written`);
|
|
29
|
+
}
|
|
30
|
+
if (Math.abs(existing.mtimeMs - Date.parse(expected)) > MTIME_TOLERANCE_MS) {
|
|
31
|
+
throw new SdkMethodError('CONFLICT', `'${displayPath}' was modified at ${existing.mtime.toISOString()}, not ${expected} — nothing was written; files.readEntry returns its current content and modifiedAt`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function writeFileAtomic({ target, content, expectedModifiedAt, displayPath }) {
|
|
35
|
+
const existing = statOrUndefined(target);
|
|
36
|
+
if (expectedModifiedAt !== undefined)
|
|
37
|
+
refuseUnlessUnmodified(existing, expectedModifiedAt, displayPath);
|
|
38
|
+
const dir = dirname(target);
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
const temp = join(dir, `.${basename(target).slice(0, 64)}.${process.pid}.${++tempCounter}.tmp`);
|
|
41
|
+
let fd;
|
|
42
|
+
let created = false;
|
|
43
|
+
try {
|
|
44
|
+
fd = openSync(temp, 'wx');
|
|
45
|
+
created = true;
|
|
46
|
+
if (existing)
|
|
47
|
+
fchmodSync(fd, existing.mode & 0o7777);
|
|
48
|
+
writeFileSync(fd, content, 'utf-8');
|
|
49
|
+
fsyncSync(fd);
|
|
50
|
+
closeSync(fd);
|
|
51
|
+
fd = undefined;
|
|
52
|
+
renameSync(temp, target);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (fd !== undefined)
|
|
56
|
+
closeQuietly(fd);
|
|
57
|
+
if (created)
|
|
58
|
+
unlinkQuietly(temp);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function closeQuietly(fd) {
|
|
63
|
+
try {
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function unlinkQuietly(path) {
|
|
70
|
+
try {
|
|
71
|
+
unlinkSync(path);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -130,7 +130,11 @@ export async function browserOpenHandler(args) {
|
|
|
130
130
|
const raw = paramsObject(args.params);
|
|
131
131
|
const url = requireWebUrlParam(raw, 'url', args.method);
|
|
132
132
|
const pinnedApp = raw.pinnedApp === true;
|
|
133
|
-
return requireHost(args.method).open({
|
|
133
|
+
return requireHost(args.method).open({
|
|
134
|
+
url,
|
|
135
|
+
...(pinnedApp ? { reuseOrigin: args.origin } : {}),
|
|
136
|
+
opener: { appId: args.appId, origin: args.origin },
|
|
137
|
+
});
|
|
134
138
|
}
|
|
135
139
|
export async function browserCloseHandler(args) {
|
|
136
140
|
const raw = paramsObject(args.params);
|
|
@@ -6,6 +6,7 @@ import { peekModelCatalog } from '../model-catalog.js';
|
|
|
6
6
|
import { getEffortLevel, getEngine, getFastMode, getModel } from '../settings.js';
|
|
7
7
|
import { collectAllSkills } from '../websocket/skill-handlers.js';
|
|
8
8
|
import { rankAllSnippets, rankQuerySnippets } from '../websocket/snippet-handlers.js';
|
|
9
|
+
import { appRunEngines } from './agents.js';
|
|
9
10
|
import { isAppOwnedProfile } from './app-endpoints.js';
|
|
10
11
|
import { inferenceEngines } from './inference.js';
|
|
11
12
|
import { SdkMethodError } from './registry.js';
|
|
@@ -125,5 +126,6 @@ export function composerSettingsHandler(_args) {
|
|
|
125
126
|
modelsStatus: catalog.status,
|
|
126
127
|
effortLevels: Object.fromEntries(ENGINE_IDS.map((engine) => [engine, [...ENGINE_EFFORT_LEVELS[engine]]])),
|
|
127
128
|
inferenceEngines: inferenceEngines(),
|
|
129
|
+
agentRunEngines: appRunEngines(),
|
|
128
130
|
};
|
|
129
131
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
|
-
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
5
5
|
const listeners = new Set();
|
|
6
6
|
export function registerFileChangeListener(spaceKey, root, cb) {
|
|
7
7
|
const listener = { spaceKey, root, cb };
|
|
@@ -10,26 +10,73 @@ export function registerFileChangeListener(spaceKey, root, cb) {
|
|
|
10
10
|
listeners.delete(listener);
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
function within(root, absolutePath) {
|
|
14
|
+
if (absolutePath === root)
|
|
15
|
+
return '';
|
|
16
|
+
return absolutePath.startsWith(root + sep) ? relative(root, absolutePath) : undefined;
|
|
17
|
+
}
|
|
18
|
+
function deliveryFor(root, source, kind, target, dir) {
|
|
19
|
+
const facts = dir ? { dir: true } : {};
|
|
20
|
+
const path = within(root, source);
|
|
21
|
+
if (kind === 'renamed') {
|
|
22
|
+
const newPath = target === undefined ? undefined : within(root, target);
|
|
23
|
+
if (path !== undefined && newPath !== undefined)
|
|
24
|
+
return { path, kind, newPath, ...facts };
|
|
25
|
+
if (path !== undefined)
|
|
26
|
+
return { path, kind: 'deleted', ...facts };
|
|
27
|
+
if (newPath !== undefined)
|
|
28
|
+
return { path: newPath, kind: 'created', ...facts };
|
|
29
|
+
}
|
|
30
|
+
else if (path !== undefined) {
|
|
31
|
+
return { path, kind, ...facts };
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
let observer;
|
|
36
|
+
export function setFileChangeObserver(next) {
|
|
37
|
+
observer = next;
|
|
38
|
+
}
|
|
39
|
+
function deliver(spaceKey, source, kind, target, dir) {
|
|
40
|
+
if (kind !== 'opened')
|
|
41
|
+
observer?.(spaceKey, source, target);
|
|
14
42
|
for (const listener of listeners) {
|
|
15
43
|
if (listener.spaceKey !== spaceKey)
|
|
16
44
|
continue;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
const change = deliveryFor(listener.root, source, kind, target, dir);
|
|
46
|
+
if (change)
|
|
47
|
+
listener.cb(change);
|
|
20
48
|
}
|
|
21
49
|
}
|
|
22
|
-
export function
|
|
50
|
+
export function notifyFileChangedAbsolute(spaceKey, absolutePath, kind, facts = {}) {
|
|
51
|
+
deliver(spaceKey, absolutePath, kind, facts.newPath, facts.dir === true);
|
|
52
|
+
}
|
|
53
|
+
function realpathLenient(absolutePath) {
|
|
54
|
+
const rest = [];
|
|
55
|
+
let current = absolutePath;
|
|
56
|
+
for (;;) {
|
|
57
|
+
try {
|
|
58
|
+
return join(realpathSync(current), ...rest);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
const parent = dirname(current);
|
|
62
|
+
if (parent === current)
|
|
63
|
+
return undefined;
|
|
64
|
+
rest.unshift(basename(current));
|
|
65
|
+
current = parent;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function notifyFileChanged(spaceKey, workingDir, path, kind, facts = {}) {
|
|
23
70
|
if (listeners.size === 0)
|
|
24
71
|
return;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
72
|
+
const toReal = (p) => realpathLenient(p.startsWith('/') ? p : join(workingDir, p));
|
|
73
|
+
const source = toReal(path);
|
|
74
|
+
if (source === undefined)
|
|
30
75
|
return;
|
|
31
|
-
|
|
32
|
-
|
|
76
|
+
const target = facts.newPath === undefined ? undefined : toReal(facts.newPath);
|
|
77
|
+
if (kind === 'renamed' && target === undefined)
|
|
78
|
+
return;
|
|
79
|
+
deliver(spaceKey, source, kind, target, facts.dir === true);
|
|
33
80
|
}
|
|
34
81
|
export function resetFileChangeListeners() {
|
|
35
82
|
listeners.clear();
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute } from 'node:path';
|
|
5
5
|
import { createDirectory, renameFile } from '../files.js';
|
|
6
6
|
import { readFileContent } from '../websocket/file-utils.js';
|
|
7
|
+
import { parseExpectedModifiedAt, writeFileAtomic } from './atomic-write.js';
|
|
7
8
|
import { SdkMethodError } from './registry.js';
|
|
8
9
|
import { provisionAppSandbox, resolveWithinRoot } from './sandbox.js';
|
|
9
10
|
export const FILES_APP_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
11
|
+
export function readEntryResult(result, stat) {
|
|
12
|
+
const meta = { modifiedAt: stat.mtime.toISOString(), size: stat.size };
|
|
13
|
+
return result.isImage
|
|
14
|
+
? { kind: 'binary', content: result.content, mimeType: result.mimeType ?? 'application/octet-stream', ...meta }
|
|
15
|
+
: { kind: 'text', content: result.content, ...meta };
|
|
16
|
+
}
|
|
10
17
|
function paramsObject(params) {
|
|
11
18
|
return typeof params === 'object' && params !== null && !Array.isArray(params)
|
|
12
19
|
? params
|
|
@@ -85,9 +92,7 @@ export function filesReadEntryHandler(args) {
|
|
|
85
92
|
const result = readFileContent(resolved, dataDir);
|
|
86
93
|
if (result.error)
|
|
87
94
|
throw serviceError(result.error);
|
|
88
|
-
return result
|
|
89
|
-
? { kind: 'binary', content: result.content, mimeType: result.mimeType ?? 'application/octet-stream' }
|
|
90
|
-
: { kind: 'text', content: result.content };
|
|
95
|
+
return readEntryResult(result, stat);
|
|
91
96
|
}
|
|
92
97
|
export function filesWriteHandler(args) {
|
|
93
98
|
const path = parsePathParam(args.params, args.method);
|
|
@@ -95,6 +100,7 @@ export function filesWriteHandler(args) {
|
|
|
95
100
|
if (typeof raw.content !== 'string') {
|
|
96
101
|
throw new SdkMethodError('BAD_REQUEST', `${args.method} requires params { path, content } with string content`);
|
|
97
102
|
}
|
|
103
|
+
const expectedModifiedAt = parseExpectedModifiedAt(raw, args.method);
|
|
98
104
|
const bytes = Buffer.byteLength(raw.content, 'utf-8');
|
|
99
105
|
if (bytes > FILES_APP_MAX_WRITE_BYTES) {
|
|
100
106
|
throw new SdkMethodError('BAD_REQUEST', `content is ${bytes} bytes — exceeds the ${FILES_APP_MAX_WRITE_BYTES}-byte per-file files.write cap`);
|
|
@@ -103,8 +109,7 @@ export function filesWriteHandler(args) {
|
|
|
103
109
|
if (statOrUndefined(resolved)?.isDirectory()) {
|
|
104
110
|
throw new SdkMethodError('BAD_REQUEST', `'${path}' is a directory — cannot write over it`);
|
|
105
111
|
}
|
|
106
|
-
|
|
107
|
-
writeFileSync(resolved, raw.content, 'utf-8');
|
|
112
|
+
writeFileAtomic({ target: resolved, content: raw.content, expectedModifiedAt, displayPath: path });
|
|
108
113
|
return null;
|
|
109
114
|
}
|
|
110
115
|
export function filesListHandler(args) {
|
|
@@ -5,6 +5,7 @@ import { findDefinitions } from '../websocket/file-definition-handlers.js';
|
|
|
5
5
|
import { streamRgSearch } from '../websocket/file-search-handlers.js';
|
|
6
6
|
import { registerFileChangeListener } from './file-change-notify.js';
|
|
7
7
|
import { appDataDir, guardAppDataPath } from './files-app.js';
|
|
8
|
+
import { acquireRootWatch } from './files-watch.js';
|
|
8
9
|
import { guardWorkspacePath, parseWorkspacePath, resolveFilesReadScopes } from './files-workspace.js';
|
|
9
10
|
import { getGrant } from './grant-store.js';
|
|
10
11
|
import { listLiveSubscriptions, registerLiveSubscription } from './live-registry.js';
|
|
@@ -115,6 +116,18 @@ export function filesSubscribeChangesHandler(args) {
|
|
|
115
116
|
return;
|
|
116
117
|
args.emit('changed', change);
|
|
117
118
|
});
|
|
119
|
+
const releaseWatch = parseWorkspacePath({ path }) === null
|
|
120
|
+
? () => { }
|
|
121
|
+
: acquireRootWatch({
|
|
122
|
+
spaceKey: args.spaceKey,
|
|
123
|
+
appId: args.appId,
|
|
124
|
+
workingDir: args.workingDir,
|
|
125
|
+
realRoot: root,
|
|
126
|
+
onGap: (gap) => {
|
|
127
|
+
if (active)
|
|
128
|
+
args.emit('gap', gap);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
118
131
|
registerLiveSubscription({
|
|
119
132
|
requestId: args.requestId,
|
|
120
133
|
appId: args.appId,
|
|
@@ -125,6 +138,7 @@ export function filesSubscribeChangesHandler(args) {
|
|
|
125
138
|
teardown: () => {
|
|
126
139
|
active = false;
|
|
127
140
|
unsubscribe();
|
|
141
|
+
releaseWatch();
|
|
128
142
|
},
|
|
129
143
|
});
|
|
130
144
|
return { subscribed: true };
|
|
@@ -137,7 +137,7 @@ function finishUpload(args, uploadId, session) {
|
|
|
137
137
|
writeFileSync(resolvedPath, Buffer.concat(session.chunks));
|
|
138
138
|
uploads.delete(uploadId);
|
|
139
139
|
if (session.guard.rooting === 'workspace') {
|
|
140
|
-
notifyFileChangedAbsolute(session.spaceKey, resolvedPath, 'changed');
|
|
140
|
+
notifyFileChangedAbsolute(session.spaceKey, resolvedPath, stat ? 'changed' : 'created');
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
export function filesUploadChunkHandler(args) {
|