conductor-remote 1.66.0 → 1.68.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/assets/index-R0LB5-IQ.js +47 -0
- package/dist/assets/{index-87r9u-0C.css → index-YPRuGHP4.css} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/conductor.applescript +263 -38
- package/dist-node/src/file-preview.js +11 -3
- package/dist-node/src/git.js +26 -0
- package/dist-node/src/mcp-tools.js +1 -1
- package/dist-node/src/routes.js +2 -0
- package/dist-node/src/server.js +43 -3
- package/dist-node/src/writes.js +10 -4
- package/package.json +1 -1
- package/dist/assets/index-DrKfyo8j.js +0 -47
package/dist-node/src/git.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { isPreviewableSource } from "./shared.js";
|
|
3
4
|
const exec = promisify(execFile);
|
|
4
5
|
const MAX_PATCH_BYTES = 400_000;
|
|
6
|
+
const MAX_LISTED_FILES = 20_000;
|
|
5
7
|
async function git(cwd, args) {
|
|
6
8
|
const { stdout } = await exec('git', ['-C', cwd, ...args], {
|
|
7
9
|
encoding: 'utf8',
|
|
@@ -116,3 +118,27 @@ async function localState(worktree) {
|
|
|
116
118
|
}
|
|
117
119
|
return { dirty, unpushed };
|
|
118
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Every file in the worktree the phone may turn a chat mention into a link for.
|
|
123
|
+
*
|
|
124
|
+
* Agents name files in prose all day — "updated `tests/foo.ts`" — and the phone
|
|
125
|
+
* links a mention only when it matches a real file, so this is the list it matches
|
|
126
|
+
* against. Tracked plus untracked-not-ignored: an agent that just wrote a file
|
|
127
|
+
* names it in the same message, long before anything commits it.
|
|
128
|
+
*
|
|
129
|
+
* Two things keep the payload small. Only previewable extensions ship, because
|
|
130
|
+
* `/api/files` refuses everything else anyway, and 20,000 paths is the ceiling — a
|
|
131
|
+
* repo whose build output isn't ignored would otherwise send its whole `node_modules`
|
|
132
|
+
* to a phone. `-z`, because a path may legally contain a newline.
|
|
133
|
+
*/
|
|
134
|
+
export async function listSourceFiles(worktree) {
|
|
135
|
+
let listing = '';
|
|
136
|
+
try {
|
|
137
|
+
listing = await git(worktree, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { files: [], truncated: false };
|
|
141
|
+
}
|
|
142
|
+
const files = listing.split('\0').filter(p => p !== '' && isPreviewableSource(p));
|
|
143
|
+
return { files: files.slice(0, MAX_LISTED_FILES), truncated: files.length > MAX_LISTED_FILES };
|
|
144
|
+
}
|
|
@@ -613,7 +613,7 @@ export function createTools(call) {
|
|
|
613
613
|
},
|
|
614
614
|
{
|
|
615
615
|
name: 'set_workspace_status',
|
|
616
|
-
description: 'Set a workspace’s status in Conductor’s sidebar (backlog, in-progress, in-review, done, canceled). Drives the real UI through the sidebar row menu, but changes nothing on screen.
|
|
616
|
+
description: 'Set a workspace’s status in Conductor’s sidebar (backlog, in-progress, in-review, done, canceled). Drives the real UI through the sidebar row menu, but changes nothing on screen. A collapsed section hides its rows from Accessibility, so folded sections are opened, used, and folded back — which makes this run a few seconds longer.',
|
|
617
617
|
inputSchema: {
|
|
618
618
|
type: 'object',
|
|
619
619
|
properties: {
|
package/dist-node/src/routes.js
CHANGED
|
@@ -81,6 +81,8 @@ export const routes = {
|
|
|
81
81
|
sessions: param('GET', '/api/workspaces/:workspaceId/sessions'),
|
|
82
82
|
newChat: param('POST', '/api/workspaces/:workspaceId/sessions'),
|
|
83
83
|
diff: param('GET', '/api/workspaces/:workspaceId/diff'),
|
|
84
|
+
/** The worktree's own file list, which is what makes a chat's `src/foo.ts` a link. */
|
|
85
|
+
workspaceFiles: param('GET', '/api/workspaces/:workspaceId/files'),
|
|
84
86
|
merge: param('POST', '/api/workspaces/:workspaceId/merge'),
|
|
85
87
|
workspaceStatus: param('POST', '/api/workspaces/:workspaceId/status'),
|
|
86
88
|
/** Read, start/forward, or stop a local workspace's selected Conductor Run task. */
|
package/dist-node/src/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import { DevServerController } from "./dev-server.js";
|
|
|
12
12
|
import { isAllowedPreviewPath, parseFileReference } from "./file-preview.js";
|
|
13
13
|
import { FirstPromptQueue } from "./firstprompt.js";
|
|
14
14
|
import { startFunnelWatchdog } from "./funnel-watchdog.js";
|
|
15
|
-
import { workspaceDiff } from "./git.js";
|
|
15
|
+
import { listSourceFiles, workspaceDiff } from "./git.js";
|
|
16
16
|
import { installLogCapture, isManaged, LOG_FILE_NAMES, logFiles, processStartedAt, recentLogs, redactSecrets, tailLogFile } from "./logbuf.js";
|
|
17
17
|
import { createTools, handleRpc, READ_TIMEOUT_MS } from "./mcp-tools.js";
|
|
18
18
|
import { mergePr } from "./merge.js";
|
|
@@ -489,6 +489,10 @@ async function serveFilePreview(req, res, reference) {
|
|
|
489
489
|
const target = parseFileReference(reference);
|
|
490
490
|
if (!target)
|
|
491
491
|
return json(req, res, 404, { error: 'source file not found' });
|
|
492
|
+
const refused = (filePath) => {
|
|
493
|
+
const answer = previewRefusal(filePath);
|
|
494
|
+
return json(req, res, answer.status, { error: answer.error });
|
|
495
|
+
};
|
|
492
496
|
let filePath;
|
|
493
497
|
let workspaceRoot;
|
|
494
498
|
let homeRoot;
|
|
@@ -503,7 +507,7 @@ async function serveFilePreview(req, res, reference) {
|
|
|
503
507
|
fs.promises.realpath(BUNDLED_SKILLS_ROOT).catch(() => null)
|
|
504
508
|
]);
|
|
505
509
|
if (!isAllowedPreviewPath(filePath, workspaceRoot, homeRoot, readExposeMode(), bundledSkillsRoot)) {
|
|
506
|
-
return
|
|
510
|
+
return refused(filePath);
|
|
507
511
|
}
|
|
508
512
|
const info = await fs.promises.stat(filePath);
|
|
509
513
|
if (!info.isFile())
|
|
@@ -511,7 +515,9 @@ async function serveFilePreview(req, res, reference) {
|
|
|
511
515
|
size = info.size;
|
|
512
516
|
}
|
|
513
517
|
catch {
|
|
514
|
-
|
|
518
|
+
// A path this relay would refuse must answer the same whether or not it is there, or
|
|
519
|
+
// a public client learns which home files exist by watching 404 turn into 403.
|
|
520
|
+
return refused(target.path);
|
|
515
521
|
}
|
|
516
522
|
if (size > FILE_PREVIEW_MAX_BYTES)
|
|
517
523
|
return json(req, res, 413, { error: 'source file is too large to preview' });
|
|
@@ -541,6 +547,28 @@ async function serveFilePreview(req, res, reference) {
|
|
|
541
547
|
truncated: start > 0 || end < lines.length
|
|
542
548
|
});
|
|
543
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* How to answer for a file the preview will not serve, from the path alone.
|
|
552
|
+
*
|
|
553
|
+
* Chat mentions made the home-directory case ordinary — agents write "plan written to
|
|
554
|
+
* `~/.gstack/plan.md`" constantly — and on a public funnel every one of those is refused
|
|
555
|
+
* by policy. Answering "source file not found" then sends someone hunting for a file that
|
|
556
|
+
* is sitting right there, so a refusal says it is a refusal. It discloses nothing: the
|
|
557
|
+
* verdict comes from the path and the funnel's posture, never from the disk, and it is
|
|
558
|
+
* the answer for an out-of-bounds path whether or not that path exists.
|
|
559
|
+
*/
|
|
560
|
+
function previewRefusal(filePath) {
|
|
561
|
+
const mode = readExposeMode();
|
|
562
|
+
if (isAllowedPreviewPath(path.resolve(filePath), cfg.workspacesRoot, os.homedir(), mode, BUNDLED_SKILLS_ROOT)) {
|
|
563
|
+
return { status: 404, error: 'source file not found' };
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
status: 403,
|
|
567
|
+
error: mode === 'public'
|
|
568
|
+
? 'this relay is reachable from the internet, so it previews files inside Conductor workspaces only'
|
|
569
|
+
: 'outside the files this relay may read'
|
|
570
|
+
};
|
|
571
|
+
}
|
|
544
572
|
/** Per-file ceiling for a relay exposed through Tailscale Funnel. Large media belongs in a link. */
|
|
545
573
|
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
546
574
|
class PayloadTooLargeError extends Error {
|
|
@@ -1140,6 +1168,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
1140
1168
|
const diff = await workspaceDiff(ws.worktree, ws.baseBranch);
|
|
1141
1169
|
return json(req, res, 200, diff);
|
|
1142
1170
|
}
|
|
1171
|
+
// GET /api/workspaces/:id/files — the worktree's own file list, which is what lets the
|
|
1172
|
+
// phone link `tests/foo.ts` in a message: a mention becomes a link only when it names
|
|
1173
|
+
// a file that is really there. A workspace with no worktree simply links nothing.
|
|
1174
|
+
const filesOf = routeParam(routes.workspaceFiles, req.method, pathname);
|
|
1175
|
+
if (filesOf) {
|
|
1176
|
+
const ws = reads.getWorkspace(filesOf);
|
|
1177
|
+
if (!ws)
|
|
1178
|
+
return json(req, res, 404, { error: 'workspace not found' });
|
|
1179
|
+
if (!ws.worktree)
|
|
1180
|
+
return json(req, res, 200, { files: [], truncated: false });
|
|
1181
|
+
return json(req, res, 200, await listSourceFiles(ws.worktree));
|
|
1182
|
+
}
|
|
1143
1183
|
// POST /api/workspaces/:id/merge — merge the workspace's open PR (mirrors Conductor's merge button)
|
|
1144
1184
|
const mergeOf = routeParam(routes.merge, req.method, pathname);
|
|
1145
1185
|
if (mergeOf) {
|
package/dist-node/src/writes.js
CHANGED
|
@@ -596,9 +596,15 @@ export const WORKSPACE_STATUS_LABELS = {
|
|
|
596
596
|
*
|
|
597
597
|
* Unlike every other write here this one never changes what's on screen: it
|
|
598
598
|
* right-clicks the workspace's *row* (AXShowMenu) and works the menu, so the
|
|
599
|
-
* workspace you were reading stays open. It does need the row to be rendered,
|
|
600
|
-
*
|
|
601
|
-
*
|
|
599
|
+
* workspace you were reading stays open. It does need the row to be rendered, and
|
|
600
|
+
* a collapsed sidebar section renders none — so the script opens the folded
|
|
601
|
+
* sections itself, looks again, and folds back exactly the ones it opened. That
|
|
602
|
+
* costs a second sidebar scan, which is affordable only because that scan reads
|
|
603
|
+
* every row's name in two Apple events rather than one per row (15s → ~1s on a
|
|
604
|
+
* 50-workspace sidebar; see findSidebarRow). Measured end to end on 2026-09-01:
|
|
605
|
+
* 8.2s through a folded section, 5.4s through an open one. The budget is 35s
|
|
606
|
+
* anyway, because sidebarRowsAndNames falls back to the per-row reads when the
|
|
607
|
+
* list no longer matches its shape, and that path pays the old 15s twice.
|
|
602
608
|
*/
|
|
603
609
|
export async function setWorkspaceStatus(workspace, status) {
|
|
604
610
|
const label = WORKSPACE_STATUS_LABELS[status];
|
|
@@ -617,7 +623,7 @@ return "ok"`.trim();
|
|
|
617
623
|
...targetEnvironment,
|
|
618
624
|
RELAY_SET_STATUS: label
|
|
619
625
|
},
|
|
620
|
-
timeout:
|
|
626
|
+
timeout: 35000
|
|
621
627
|
})));
|
|
622
628
|
return { ok: true, strategy: 'applescript' };
|
|
623
629
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
6
|
"description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
|