@love-moon/conductor-cli 0.15.2 → 0.17.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/CHANGELOG.md +148 -0
- package/bin/conductor-auth.js +162 -0
- package/bin/conductor-config.js +6 -9
- package/bin/conductor-daemon-query.js +13 -17
- package/bin/conductor-daemon.js +6 -1
- package/bin/conductor-fire.js +91 -2
- package/bin/conductor-issue.js +215 -13
- package/bin/conductor-project.js +840 -27
- package/bin/conductor-search.js +150 -0
- package/bin/conductor-settings.js +608 -0
- package/bin/conductor-task.js +344 -9
- package/bin/conductor-update.js +17 -0
- package/bin/conductor.js +29 -8
- package/package.json +5 -5
- package/src/backend-http.js +246 -0
- package/src/daemon-share-commands.js +528 -0
- package/src/daemon-update.js +3 -0
- package/src/daemon.js +51 -11
- package/src/remote/mcp-launch.js +3 -3
- package/src/remote-file-handlers.js +65 -6
- package/src/task-commands.js +813 -0
- package/src/version-check.js +10 -0
|
@@ -38,6 +38,13 @@ const MAX_CONCURRENT_TRANSFERS = 4;
|
|
|
38
38
|
const NUL = String.fromCharCode(0);
|
|
39
39
|
|
|
40
40
|
export const REMOTE_FILE_CAPABILITY = "remote_file";
|
|
41
|
+
/**
|
|
42
|
+
* This daemon enforces `rootPath` on `push` and resolves a relative path against
|
|
43
|
+
* a task's working directory. The backend must see it before opening a file
|
|
44
|
+
* preview: an older daemon would silently ignore `rootPath` and serve the whole
|
|
45
|
+
* disk to a preview link.
|
|
46
|
+
*/
|
|
47
|
+
export const REMOTE_FILE_PREVIEW_CAPABILITY = "remote_file_preview";
|
|
41
48
|
|
|
42
49
|
/**
|
|
43
50
|
* Move single files between this daemon's disk and the account that owns it,
|
|
@@ -68,6 +75,7 @@ export const REMOTE_FILE_CAPABILITY = "remote_file";
|
|
|
68
75
|
* @param {number} [opts.chunkBytes]
|
|
69
76
|
* @param {(ms:number)=>Promise<unknown>} [opts.sleep] backoff hook, injectable for tests
|
|
70
77
|
* @param {string|null} [opts.guestRoot] when set, paths outside it are refused
|
|
78
|
+
* @param {(taskId:string)=>string|null} [opts.resolveTaskCwd] working directory of a live task
|
|
71
79
|
*/
|
|
72
80
|
export function createRemoteFileHandlers(opts = {}) {
|
|
73
81
|
const config = opts.config || null;
|
|
@@ -77,6 +85,7 @@ export function createRemoteFileHandlers(opts = {}) {
|
|
|
77
85
|
const chunkBytes = resolveRemoteChunkBytes(opts.chunkBytes);
|
|
78
86
|
const sleep = typeof opts.sleep === "function" ? opts.sleep : delay;
|
|
79
87
|
const guestRoot = opts.guestRoot || null;
|
|
88
|
+
const resolveTaskCwd = typeof opts.resolveTaskCwd === "function" ? opts.resolveTaskCwd : () => null;
|
|
80
89
|
// One controller for the whole daemon: `abortAll()` on shutdown tears down
|
|
81
90
|
// every in-flight stream instead of letting them hold the process open.
|
|
82
91
|
const shutdownController = new AbortController();
|
|
@@ -114,14 +123,57 @@ export function createRemoteFileHandlers(opts = {}) {
|
|
|
114
123
|
* mistakes rather than a determined escape — same caveat as everywhere else
|
|
115
124
|
* in this repo.
|
|
116
125
|
*/
|
|
117
|
-
function resolveRemotePath(value) {
|
|
118
|
-
const resolved = normalizeRemotePath(value);
|
|
126
|
+
function resolveRemotePath(value, taskId) {
|
|
127
|
+
const resolved = normalizeRemotePath(taskId ? resolveAgainstTask(value, taskId) : value);
|
|
119
128
|
if (guestRoot && !isPathInsideGuestRoot(resolved, guestRoot)) {
|
|
120
129
|
throw new Error(`path is outside this daemon's shared root: ${resolved}`);
|
|
121
130
|
}
|
|
122
131
|
return resolved;
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
/** A link the AI wrote as `docs/report.md` means "relative to where I ran". */
|
|
135
|
+
function resolveAgainstTask(value, taskId) {
|
|
136
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
137
|
+
if (!raw || path.isAbsolute(raw) || raw === "~" || raw.startsWith("~/")) return value;
|
|
138
|
+
const cwd = resolveTaskCwd(String(taskId));
|
|
139
|
+
if (!cwd) {
|
|
140
|
+
throw new Error(`cannot resolve relative path ${raw}: task ${taskId} is not running on this daemon`);
|
|
141
|
+
}
|
|
142
|
+
return path.join(cwd, raw);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Confine a preview read to `rootPath`. Unlike the guest-root check this one
|
|
147
|
+
* follows symlinks: the paths under a preview link are chosen by the page
|
|
148
|
+
* being previewed, not typed by the machine's owner, so a symlink into
|
|
149
|
+
* `~/.ssh` must not be a way out. Dotfiles below the root are refused for the
|
|
150
|
+
* same reason (`.env`, `.git/config`).
|
|
151
|
+
*/
|
|
152
|
+
async function resolveInsideRoot(source, rootPath) {
|
|
153
|
+
let real;
|
|
154
|
+
let root;
|
|
155
|
+
try {
|
|
156
|
+
root = await fsp.realpath(normalizeRemotePath(rootPath));
|
|
157
|
+
real = await fsp.realpath(source);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
160
|
+
throw new Error(`no such file: ${source}`);
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
const relative = path.relative(root, real);
|
|
165
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
166
|
+
throw new Error(`path is outside the preview root: ${source}`);
|
|
167
|
+
}
|
|
168
|
+
if (relative.split(path.sep).some((segment) => segment.startsWith("."))) {
|
|
169
|
+
throw new Error(`hidden files are not available to preview: ${source}`);
|
|
170
|
+
}
|
|
171
|
+
if (guestRoot && !isPathInsideGuestRoot(real, await fsp.realpath(guestRoot))) {
|
|
172
|
+
throw new Error(`path is outside this daemon's shared root: ${source}`);
|
|
173
|
+
}
|
|
174
|
+
return real;
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
async function acquireSlot() {
|
|
126
178
|
if (activeTransfers >= MAX_CONCURRENT_TRANSFERS) {
|
|
127
179
|
throw new Error(
|
|
@@ -306,7 +358,10 @@ export function createRemoteFileHandlers(opts = {}) {
|
|
|
306
358
|
async function push(args = {}) {
|
|
307
359
|
const transferId = requireId(args.transferId ?? args.transfer_id, "transferId");
|
|
308
360
|
const transferToken = requireId(args.transferToken ?? args.transfer_token, "transferToken");
|
|
309
|
-
|
|
361
|
+
let source = resolveRemotePath(args.remotePath ?? args.remote_path);
|
|
362
|
+
const rootPath = args.rootPath ?? args.root_path;
|
|
363
|
+
if (rootPath) source = await resolveInsideRoot(source, rootPath);
|
|
364
|
+
const limit = Math.min(maxBytes, toPositiveInt(args.maxBytes ?? args.max_bytes) || maxBytes);
|
|
310
365
|
|
|
311
366
|
const stat = await statOrNull(source);
|
|
312
367
|
if (!stat) {
|
|
@@ -318,8 +373,8 @@ export function createRemoteFileHandlers(opts = {}) {
|
|
|
318
373
|
if (!stat.isFile()) {
|
|
319
374
|
throw new Error(`${source} is not a regular file`);
|
|
320
375
|
}
|
|
321
|
-
if (stat.size >
|
|
322
|
-
throw new Error(`${source} is ${stat.size} bytes, over the ${
|
|
376
|
+
if (stat.size > limit) {
|
|
377
|
+
throw new Error(`${source} is ${stat.size} bytes, over the ${limit} byte limit`);
|
|
323
378
|
}
|
|
324
379
|
|
|
325
380
|
await acquireSlot();
|
|
@@ -414,11 +469,12 @@ export function createRemoteFileHandlers(opts = {}) {
|
|
|
414
469
|
|
|
415
470
|
/** Probe a path. A missing path is an answer, not an error. */
|
|
416
471
|
async function stat(args = {}) {
|
|
417
|
-
const target = resolveRemotePath(args.remotePath ?? args.remote_path);
|
|
472
|
+
const target = resolveRemotePath(args.remotePath ?? args.remote_path, args.taskId ?? args.task_id);
|
|
418
473
|
const info = await statOrNull(target);
|
|
419
474
|
if (!info) {
|
|
420
475
|
return {
|
|
421
476
|
path: target,
|
|
477
|
+
realPath: null,
|
|
422
478
|
exists: false,
|
|
423
479
|
isFile: false,
|
|
424
480
|
isDirectory: false,
|
|
@@ -429,6 +485,9 @@ export function createRemoteFileHandlers(opts = {}) {
|
|
|
429
485
|
}
|
|
430
486
|
return {
|
|
431
487
|
path: target,
|
|
488
|
+
// Symlinks resolved: a preview roots itself here, so that the root it
|
|
489
|
+
// later enforces is the directory the file really lives in.
|
|
490
|
+
realPath: await fsp.realpath(target),
|
|
432
491
|
exists: true,
|
|
433
492
|
isFile: info.isFile(),
|
|
434
493
|
isDirectory: info.isDirectory(),
|