conductor-remote 1.116.0 → 1.118.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/README.md +10 -8
- package/dist/assets/{PierrePatch-BPp5Cf-p.js → PierrePatch-D4YbKD8Q.js} +1 -1
- package/dist/assets/index-BRzsSW8g.css +1 -0
- package/dist/assets/index-BaS6qXUW.js +65 -0
- package/dist/index.html +2 -2
- package/dist/push-sw.js +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/nosleep-setup.js +2 -2
- package/dist-node/scripts/nosleep.js +3 -3
- package/dist-node/scripts/service/commands.js +32 -0
- package/dist-node/scripts/service/configuration.js +205 -0
- package/dist-node/scripts/service/environment.js +59 -0
- package/dist-node/scripts/service/flags.js +45 -0
- package/dist-node/scripts/service/installation.js +91 -0
- package/dist-node/scripts/service/launch-agent.js +134 -0
- package/dist-node/scripts/service/network.js +244 -0
- package/dist-node/scripts/service/presentation.js +101 -0
- package/dist-node/scripts/service/voice.js +113 -0
- package/dist-node/scripts/service.js +5 -980
- package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
- package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
- package/dist-node/src/{roles.js → agents/roles.js} +3 -3
- package/dist-node/src/db.js +1 -1
- package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
- package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
- package/dist-node/src/dev-server/ports.js +63 -0
- package/dist-node/src/dev-server/processes.js +73 -0
- package/dist-node/src/dev-server/proxy.js +158 -0
- package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
- package/dist-node/src/dev-server/types.js +1 -0
- package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
- package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
- package/dist-node/src/{git.js → git/diff.js} +1 -1
- package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
- package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
- package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
- package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
- package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
- package/dist-node/src/http/router-types.js +2 -0
- package/dist-node/src/http/router.js +78 -0
- package/dist-node/src/http/routes/create-workspace.js +90 -0
- package/dist-node/src/http/routes/files.js +53 -0
- package/dist-node/src/http/routes/prompts.js +363 -0
- package/dist-node/src/http/routes/sessions.js +279 -0
- package/dist-node/src/http/routes/state.js +186 -0
- package/dist-node/src/http/routes/system.js +220 -0
- package/dist-node/src/http/routes/voice.js +117 -0
- package/dist-node/src/http/routes/workflows.js +248 -0
- package/dist-node/src/http/routes/workspaces.js +267 -0
- package/dist-node/src/http/services/base.js +252 -0
- package/dist-node/src/http/services/delegations.js +301 -0
- package/dist-node/src/http/services/delivery.js +433 -0
- package/dist-node/src/http/services/files.js +199 -0
- package/dist-node/src/http/services/mcp.js +94 -0
- package/dist-node/src/http/services/responses.js +120 -0
- package/dist-node/src/http/services/voice.js +147 -0
- package/dist-node/src/http/services/workflow-probes.js +215 -0
- package/dist-node/src/http/services/workflow-state.js +195 -0
- package/dist-node/src/http/services/workflow.js +366 -0
- package/dist-node/src/http/services.js +35 -0
- package/dist-node/src/mcp/arguments.js +14 -0
- package/dist-node/src/mcp/dispatcher.js +56 -0
- package/dist-node/src/mcp/formatters.js +110 -0
- package/dist-node/src/mcp/protocol.js +7 -0
- package/dist-node/src/mcp/registry.js +66 -0
- package/dist-node/src/mcp/tools/agent-options.js +122 -0
- package/dist-node/src/mcp/tools/chat-actions.js +194 -0
- package/dist-node/src/mcp/tools/chats.js +194 -0
- package/dist-node/src/mcp/tools/delegation.js +135 -0
- package/dist-node/src/mcp/tools/system.js +135 -0
- package/dist-node/src/mcp/tools/voice.js +136 -0
- package/dist-node/src/mcp/tools/workspaces.js +259 -0
- package/dist-node/src/mcp/types.js +1 -0
- package/dist-node/src/mcp.js +6 -4
- package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
- package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
- package/dist-node/src/orchestration/delegation/codec.js +201 -0
- package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
- package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
- package/dist-node/src/orchestration/delegation/queue.js +255 -0
- package/dist-node/src/orchestration/delegation/store.js +111 -0
- package/dist-node/src/orchestration/delegation/types.js +2 -0
- package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
- package/dist-node/src/orchestration/persistence/codecs.js +148 -0
- package/dist-node/src/orchestration/persistence/connection.js +205 -0
- package/dist-node/src/orchestration/persistence/db.js +101 -0
- package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
- package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
- package/dist-node/src/orchestration/persistence/effects.js +109 -0
- package/dist-node/src/orchestration/persistence/errors.js +39 -0
- package/dist-node/src/orchestration/persistence/events.js +29 -0
- package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
- package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
- package/dist-node/src/orchestration/persistence/jobs.js +159 -0
- package/dist-node/src/orchestration/persistence/projections.js +190 -0
- package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
- package/dist-node/src/orchestration/persistence/records.js +115 -0
- package/dist-node/src/orchestration/persistence/relays.js +78 -0
- package/dist-node/src/orchestration/persistence/runs.js +156 -0
- package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
- package/dist-node/src/orchestration/persistence/types.js +1 -0
- package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
- package/dist-node/src/orchestration/persistence/values.js +114 -0
- package/dist-node/src/orchestration/workflow/commands.js +309 -0
- package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
- package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
- package/dist-node/src/orchestration/workflow/effects.js +307 -0
- package/dist-node/src/orchestration/workflow/errors.js +13 -0
- package/dist-node/src/orchestration/workflow/helpers.js +154 -0
- package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
- package/dist-node/src/orchestration/workflow/job-results.js +280 -0
- package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
- package/dist-node/src/orchestration/workflow/root.js +293 -0
- package/dist-node/src/orchestration/workflow/start.js +99 -0
- package/dist-node/src/orchestration/workflow/state.js +105 -0
- package/dist-node/src/orchestration/workflow/types.js +1 -0
- package/dist-node/src/orchestration/workflow/wake.js +164 -0
- package/dist-node/src/prefs.js +1 -1
- package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
- package/dist-node/src/reads/messages.js +362 -0
- package/dist-node/src/reads/repository.js +87 -0
- package/dist-node/src/reads/sessions.js +165 -0
- package/dist-node/src/reads/types.js +1 -0
- package/dist-node/src/reads/workspaces.js +230 -0
- package/dist-node/src/reads/worktrees.js +45 -0
- package/dist-node/src/routes.js +5 -5
- package/dist-node/src/{search.js → search/coordinator.js} +4 -4
- package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
- package/dist-node/src/server.js +14 -3930
- package/dist-node/src/settings.js +1 -1
- package/dist-node/src/shared.js +130 -9
- package/dist-node/src/{transcript.js → transcript/parser.js} +4 -124
- package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
- package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
- package/dist-node/src/voice/brief.js +22 -50
- package/dist-node/src/voice/context.js +1 -1
- package/dist-node/src/voice/dates.js +38 -0
- package/dist-node/src/voice/history.js +14 -5
- package/dist-node/src/voice/prompt.js +10 -9
- package/dist-node/src/voice/recall.js +115 -0
- package/dist-node/src/{speech.js → voice/speech.js} +1 -1
- package/dist-node/src/voice/tools.js +92 -3
- package/dist-node/src/voice/webrtc.js +1 -1
- package/dist-node/src/wire.js +2 -2
- package/dist-node/src/writes/actuator.js +124 -0
- package/dist-node/src/writes/agent-options.js +151 -0
- package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
- package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
- package/dist-node/src/writes/applescript/composer.applescript +195 -0
- package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
- package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
- package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
- package/dist-node/src/writes/applescript/source.js +52 -0
- package/dist-node/src/writes/applescript/window.applescript +436 -0
- package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
- package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
- package/dist-node/src/writes/chats.js +160 -0
- package/dist-node/src/writes/guards.js +77 -0
- package/dist-node/src/writes/runner.js +67 -0
- package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
- package/dist-node/src/writes/system.js +81 -0
- package/dist-node/src/writes/targeting.js +121 -0
- package/dist-node/src/writes/types.js +1 -0
- package/dist-node/src/writes/ui-lock.js +229 -0
- package/dist-node/src/writes/workspaces.js +233 -0
- package/docs/voice-setup.md +34 -12
- package/package.json +2 -2
- package/dist/assets/index-DNLdIfZ2.css +0 -1
- package/dist/assets/index-QJR7tn2I.js +0 -61
- package/dist-node/src/conductor.applescript +0 -2279
- package/dist-node/src/delegations.js +0 -557
- package/dist-node/src/mcp-tools.js +0 -1319
- package/dist-node/src/orchestration-db.js +0 -2186
- package/dist-node/src/reads.js +0 -787
- package/dist-node/src/workflow-coordinator.js +0 -2119
- package/dist-node/src/writes.js +0 -1223
- /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
- /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
- /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
- /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
- /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
- /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
- /package/dist-node/src/{icons.js → files/icons.js} +0 -0
- /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
- /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
- /package/dist-node/src/{merge.js → git/merge.js} +0 -0
- /package/dist-node/src/{pr.js → git/pr.js} +0 -0
- /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
- /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
- /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
- /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
- /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
- /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
- /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
- /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
- /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
- /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
- /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
- /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
- /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* must be `run-run:<n>.sh` specifically, so the setup wrapper (`run-setup:<n>.sh`) that
|
|
21
21
|
* runs beside it is not read as an active Run.
|
|
22
22
|
*
|
|
23
|
-
* Like src/background-tasks.ts, only `ps` **args** are read, never the environment. The
|
|
23
|
+
* Like src/reads/background-tasks.ts, only `ps` **args** are read, never the environment. The
|
|
24
24
|
* args snapshot can itself hold sensitive command-line data, so it is never logged.
|
|
25
25
|
*/
|
|
26
26
|
import { execFile } from 'node:child_process';
|
|
@@ -70,7 +70,7 @@ let refreshing = null;
|
|
|
70
70
|
/**
|
|
71
71
|
* The project keys with a live Run wrapper, stale-while-revalidate.
|
|
72
72
|
*
|
|
73
|
-
* Synchronous on purpose, exactly like src/background-tasks.ts ▸ `agentProcessStarts`:
|
|
73
|
+
* Synchronous on purpose, exactly like src/reads/background-tasks.ts ▸ `agentProcessStarts`:
|
|
74
74
|
* `/api/state` is assembled from plain reads and must not fork a `ps` on every 2.5s
|
|
75
75
|
* poll. The first call after start answers empty — no badge for one poll — and every
|
|
76
76
|
* call after that answers from the last listing while a fresh one lands.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { isPreviewableImage, isPreviewableSource } from "
|
|
3
|
+
import { isPreviewableImage, isPreviewableSource } from "../shared.js";
|
|
4
4
|
/** True only for a descendant. A prefix test would let `/workspaces-old` escape `/workspaces`. */
|
|
5
5
|
function insideDirectory(filePath, root) {
|
|
6
6
|
const relative = path.relative(root, filePath);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { workspaceDiffStats } from "./
|
|
1
|
+
import { workspaceDiffStats } from "./diff.js";
|
|
2
2
|
/**
|
|
3
3
|
* Git stats are local and cheap, but not cheap enough to run synchronously for every
|
|
4
4
|
* workspace on the 2.5s state poll. Keep the last answer on the wire while a bounded
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
-
import { isPreviewableSource } from "
|
|
4
|
+
import { isPreviewableSource } from "../shared.js";
|
|
5
5
|
const exec = promisify(execFile);
|
|
6
6
|
const MAX_PATCH_BYTES = 400_000;
|
|
7
7
|
const MAX_LISTED_FILES = 20_000;
|
|
@@ -30,7 +30,7 @@ import { execFile } from 'node:child_process';
|
|
|
30
30
|
import fs from 'node:fs';
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
import { promisify } from 'node:util';
|
|
33
|
-
import { packageRoot } from "
|
|
33
|
+
import { packageRoot } from "../pkg-root.js";
|
|
34
34
|
const execFileP = promisify(execFile);
|
|
35
35
|
const NAME = 'conductor-remote';
|
|
36
36
|
const projectDir = packageRoot(import.meta.dirname);
|
|
@@ -28,10 +28,10 @@ import { Resolver } from 'node:dns/promises';
|
|
|
28
28
|
import https from 'node:https';
|
|
29
29
|
import net from 'node:net';
|
|
30
30
|
import { promisify } from 'node:util';
|
|
31
|
-
import { readSettings } from "
|
|
31
|
+
import { readSettings } from "../settings.js";
|
|
32
|
+
import { joinInstantHotspot } from "../writes/system.js";
|
|
32
33
|
import { magicDnsName, parseServeStatus, readExposeMode, relayPort, relayServeState, tailscaleBin } from "./tailscale.js";
|
|
33
34
|
import { hasDefaultRoute, joinNetwork, preferredNetworks } from "./wifi.js";
|
|
34
|
-
import { joinInstantHotspot } from "./writes.js";
|
|
35
35
|
const execFileP = promisify(execFile);
|
|
36
36
|
const PROBE_PATH = '/health'; // unauthenticated 200 on the relay — no token needed to prove reachability
|
|
37
37
|
const PROBE_TIMEOUT_MS = 8000;
|
|
@@ -205,7 +205,7 @@ async function routeAppeared(waitMs) {
|
|
|
205
205
|
*
|
|
206
206
|
* The guards matter more than the action. Switching Wi-Fi networks can take a working
|
|
207
207
|
* Mac off a working network, so this borrows the shape of `serverWindowCount()`'s veto in
|
|
208
|
-
* writes.
|
|
208
|
+
* src/writes/applescript/window.applescript: **a probe that can't answer must prevent the action, never cause it.**
|
|
209
209
|
* - Opt-in only (`autoRejoin`), and only with somewhere to go.
|
|
210
210
|
* - Only when `hasDefaultRoute()` is definitively false. That probe needs no permission
|
|
211
211
|
* grant, unlike reading the SSID, which macOS refuses without Location Services and
|
|
@@ -216,7 +216,7 @@ async function routeAppeared(waitMs) {
|
|
|
216
216
|
*
|
|
217
217
|
* Two ways in, tried in order. `networksetup` first — cheap, no UI — and when it answers
|
|
218
218
|
* "Could not find network" (a hotspot doesn't broadcast until asked), the Accessibility
|
|
219
|
-
* press on the Wi-Fi menu's own row (`joinInstantHotspot` in writes.ts), which wakes the
|
|
219
|
+
* press on the Wi-Fi menu's own row (`joinInstantHotspot` in src/writes/system.ts), which wakes the
|
|
220
220
|
* phone's hotspot over Continuity exactly like clicking it. The press needs an unlocked
|
|
221
221
|
* screen — the lock hides the session from Accessibility, and the failure names that —
|
|
222
222
|
* so a lid-closed Mac still wants macOS's own Auto-Join Hotspot set to Automatic as the
|
|
@@ -21,7 +21,7 @@ import fs from 'node:fs';
|
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import util from 'node:util';
|
|
24
|
-
import { scrubWorkflowSecrets } from "
|
|
24
|
+
import { scrubWorkflowSecrets } from "../shared.js";
|
|
25
25
|
/** Kept small enough that a phone on a slow link can pull the whole thing; ~a day of a quiet relay. */
|
|
26
26
|
const CAPACITY = 600;
|
|
27
27
|
/** One AppleScript failure can carry a long stack; keep a single entry from crowding out the rest. */
|
|
@@ -30,7 +30,7 @@ export const HELPER_PATH = '/usr/local/libexec/conductor-remote-nosleep';
|
|
|
30
30
|
* with SIGKILL leaves this file behind, macOS recycles pids freely, and the helper runs
|
|
31
31
|
* as root — so `kill -0` succeeds against whatever inherited the number, and `--stop`
|
|
32
32
|
* would then SIGTERM an unrelated root process. Readers that only want the pid and the
|
|
33
|
-
* expiry (`src/nosleep.ts` ▸ readPidfile) can ignore it; a two-field file from an older
|
|
33
|
+
* expiry (`src/host/nosleep.ts` ▸ readPidfile) can ignore it; a two-field file from an older
|
|
34
34
|
* version still parses, and skips the check. The fourth field records whether this
|
|
35
35
|
* window also holds the display assertion that blocks the automatic screen lock.
|
|
36
36
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared Tailscale helpers used by BOTH the deploy script (scripts/service.ts) and the runtime funnel
|
|
3
|
-
* watchdog (src/funnel-watchdog.ts). They must agree: the watchdog re-establishes Funnel with the same
|
|
3
|
+
* watchdog (src/host/funnel-watchdog.ts). They must agree: the watchdog re-establishes Funnel with the same
|
|
4
4
|
* port and posture the deploy configured, so "how do we find tailscale / this node's public name / the
|
|
5
5
|
* expose mode" lives here once rather than drifting between deploy-time and runtime copies.
|
|
6
6
|
*
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { UiBusyError, uiQueueDepth, withUiPriority } from "../writes/ui-lock.js";
|
|
3
|
+
import { NOT_HANDLED } from "./router-types.js";
|
|
4
|
+
import { createCreateWorkspaceRoutes } from "./routes/create-workspace.js";
|
|
5
|
+
import { createFilesRoutes } from "./routes/files.js";
|
|
6
|
+
import { createPromptsRoutes } from "./routes/prompts.js";
|
|
7
|
+
import { createSessionsRoutes } from "./routes/sessions.js";
|
|
8
|
+
import { createStateRoutes } from "./routes/state.js";
|
|
9
|
+
import { createSystemRoutes } from "./routes/system.js";
|
|
10
|
+
import { createVoiceRoutes } from "./routes/voice.js";
|
|
11
|
+
import { createWorkflowsRoutes } from "./routes/workflows.js";
|
|
12
|
+
import { createWorkspacesRoutes } from "./routes/workspaces.js";
|
|
13
|
+
export function createRelayServer(services) {
|
|
14
|
+
const { handleMcpHttp, serveStatic, authed, json, PayloadTooLargeError, workflowHttpError } = services;
|
|
15
|
+
const handlers = [
|
|
16
|
+
createStateRoutes(services),
|
|
17
|
+
createWorkflowsRoutes(services),
|
|
18
|
+
createVoiceRoutes(services),
|
|
19
|
+
createSystemRoutes(services),
|
|
20
|
+
createCreateWorkspaceRoutes(services),
|
|
21
|
+
createFilesRoutes(services),
|
|
22
|
+
createWorkspacesRoutes(services),
|
|
23
|
+
createSessionsRoutes(services),
|
|
24
|
+
createPromptsRoutes(services)
|
|
25
|
+
];
|
|
26
|
+
const server = http.createServer(async (req, res) => {
|
|
27
|
+
const url = new URL(req.url ?? '/', 'http://x');
|
|
28
|
+
const { pathname } = url;
|
|
29
|
+
// POST /mcp — the MCP Streamable HTTP transport, for a client that can only reach a
|
|
30
|
+
// URL (an agent on another machine, or a hosted one). Same tools as
|
|
31
|
+
// `conductor-remote mcp`'s stdio, same token gate as /api/*, and — because this runs
|
|
32
|
+
// *inside* the relay — the same UI lock, with no second process to sit outside it.
|
|
33
|
+
//
|
|
34
|
+
// Deliberately minimal: this server never initiates a message, so there is no SSE
|
|
35
|
+
// stream to open and GET is answered 405, which the spec allows. It keeps no session
|
|
36
|
+
// either, so no `Mcp-Session-Id` is issued and every request stands alone.
|
|
37
|
+
if (pathname === '/mcp')
|
|
38
|
+
return handleMcpHttp(req, res);
|
|
39
|
+
if (!pathname.startsWith('/api/'))
|
|
40
|
+
return serveStatic(req, res, pathname);
|
|
41
|
+
// Everything under /api requires the shared secret.
|
|
42
|
+
if (!authed(req))
|
|
43
|
+
return json(req, res, 401, { error: 'unauthorized' });
|
|
44
|
+
// Who is asking decides who waits for Conductor's window. An agent (src/mcp.ts sets
|
|
45
|
+
// this header) yields the UI lock to the person holding the phone — see src/writes/ui-lock.ts ▸
|
|
46
|
+
// uiTurn. Anything unlabelled is treated as the person, because the phone is the
|
|
47
|
+
// only caller that predates the header and mislabelling it would be the bad way round.
|
|
48
|
+
const priority = req.headers['x-relay-client'] === 'mcp' ? 'background' : 'interactive';
|
|
49
|
+
return withUiPriority(priority, async () => {
|
|
50
|
+
try {
|
|
51
|
+
for (const handler of handlers) {
|
|
52
|
+
if ((await handler(req, res, url)) !== NOT_HANDLED)
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
return json(req, res, 404, { error: 'no route', pathname });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err instanceof PayloadTooLargeError)
|
|
59
|
+
return json(req, res, 413, { error: err.message });
|
|
60
|
+
const workflowError = workflowHttpError(err);
|
|
61
|
+
if (workflowError) {
|
|
62
|
+
console.warn(`[workflow] ${req.method} ${pathname}: ${workflowError.error.code}: ${workflowError.error.message}`);
|
|
63
|
+
return json(req, res, workflowError.status, { ok: false, error: workflowError.error });
|
|
64
|
+
}
|
|
65
|
+
// A refused UI turn is not a server fault and a retry is the right move, so it gets
|
|
66
|
+
// 503 + Retry-After rather than a 500 that reads as "the relay is broken".
|
|
67
|
+
if (err instanceof UiBusyError) {
|
|
68
|
+
res.setHeader('retry-after', '15');
|
|
69
|
+
return json(req, res, 503, { error: err.message, busy: true, queue: uiQueueDepth() });
|
|
70
|
+
}
|
|
71
|
+
// Log the detail locally; don't reflect internals (paths, stack strings) back over the wire.
|
|
72
|
+
console.error(`[relay] ${req.method} ${pathname} failed:`, err);
|
|
73
|
+
return json(req, res, 500, { error: 'internal error' });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
return server;
|
|
78
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { stagedAttachments } from "../../files/staged-attachments.js";
|
|
2
|
+
import { isRoute, routes } from "../../routes.js";
|
|
3
|
+
import { EFFORT_LABELS } from "../../writes/agent-options.js";
|
|
4
|
+
import { NOT_HANDLED } from "../router-types.js";
|
|
5
|
+
export function createCreateWorkspaceRoutes(services) {
|
|
6
|
+
const { readBody, json, STAGED_ATTACHMENTS_DIR, reads, createWorkspaceAndRead, firstPrompts } = services;
|
|
7
|
+
return async (req, res, url) => {
|
|
8
|
+
const { pathname } = url;
|
|
9
|
+
// POST /api/workspaces { repo, prompt, model?/effort?/plan?/fast?, send? }
|
|
10
|
+
// — create a workspace via Conductor's deep link, then configure its first chat.
|
|
11
|
+
if (isRoute(routes.createWorkspace, req.method, pathname)) {
|
|
12
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
13
|
+
const attachmentIds = body.attachmentIds ?? [];
|
|
14
|
+
if ('workflow' in body)
|
|
15
|
+
return json(req, res, 400, { error: 'Workflow starts through POST /api/workflows.' });
|
|
16
|
+
if (body.model !== undefined && typeof body.model !== 'string')
|
|
17
|
+
return json(req, res, 400, { error: 'model must be a picker label' });
|
|
18
|
+
if (body.effort !== undefined && typeof body.effort !== 'string')
|
|
19
|
+
return json(req, res, 400, { error: 'effort must be a string' });
|
|
20
|
+
const effort = body.effort?.trim() || undefined;
|
|
21
|
+
if (effort && !EFFORT_LABELS[effort])
|
|
22
|
+
return json(req, res, 400, { error: `effort must be one of ${Object.keys(EFFORT_LABELS).join(', ')}` });
|
|
23
|
+
if (body.plan !== undefined && typeof body.plan !== 'boolean')
|
|
24
|
+
return json(req, res, 400, { error: 'plan must be a boolean' });
|
|
25
|
+
if (body.fast !== undefined && typeof body.fast !== 'boolean')
|
|
26
|
+
return json(req, res, 400, { error: 'fast must be a boolean' });
|
|
27
|
+
const requestedAgent = {
|
|
28
|
+
model: body.model?.trim() || undefined,
|
|
29
|
+
effort,
|
|
30
|
+
plan: body.plan,
|
|
31
|
+
fast: body.fast
|
|
32
|
+
};
|
|
33
|
+
if (!Array.isArray(attachmentIds) || attachmentIds.some(id => typeof id !== 'string'))
|
|
34
|
+
return json(req, res, 400, { error: 'attachment ids must be a list of strings' });
|
|
35
|
+
const attachments = stagedAttachments(STAGED_ATTACHMENTS_DIR, attachmentIds);
|
|
36
|
+
if (!attachments)
|
|
37
|
+
return json(req, res, 409, { error: 'an attached file is no longer available; add it again' });
|
|
38
|
+
// The prompt is optional — a bare `path=` opens an empty workspace, like
|
|
39
|
+
// Conductor's own New workspace — but *something* has to say where it goes.
|
|
40
|
+
const objective = [...attachments.map(attachment => attachment.token), (body.prompt ?? '').trim()]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join('\n');
|
|
43
|
+
if (!objective && !body.repo)
|
|
44
|
+
return json(req, res, 400, { error: 'need a repo or a prompt' });
|
|
45
|
+
const prompt = objective;
|
|
46
|
+
const agent = requestedAgent;
|
|
47
|
+
const configureAgent = Object.values(agent).some(value => value !== undefined);
|
|
48
|
+
// Resolve the repo to a real path: an unmatched `path` would silently land
|
|
49
|
+
// the workspace in whichever repo Conductor happens to list first.
|
|
50
|
+
const repo = body.repo ? reads.listRepos().find(r => r.name === body.repo) : undefined;
|
|
51
|
+
if (body.repo && !repo)
|
|
52
|
+
return json(req, res, 404, { error: `unknown repo ${body.repo}` });
|
|
53
|
+
if (repo && !repo.root_path)
|
|
54
|
+
return json(req, res, 409, { error: `${repo.name} has no checkout path` });
|
|
55
|
+
const { result, created } = await createWorkspaceAndRead(prompt, repo?.root_path ?? null, repo?.name);
|
|
56
|
+
if (!result.ok)
|
|
57
|
+
return json(req, res, 502, result);
|
|
58
|
+
if (!created) {
|
|
59
|
+
return json(req, res, 502, {
|
|
60
|
+
ok: false,
|
|
61
|
+
strategy: result.strategy,
|
|
62
|
+
error: 'Conductor didn’t create a workspace — check it’s running and not showing a dialog.'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Return as soon as the row exists (~2s) — waiting for delivery would block the
|
|
66
|
+
// request through Conductor's whole setup, measured at 30s+ on a real repo and
|
|
67
|
+
// past any budget a phone should hold a request open for. The queue delivers on
|
|
68
|
+
// its own schedule and the phone watches it in /api/state; `send:true` opts API
|
|
69
|
+
// callers into waiting.
|
|
70
|
+
// Whatever happens, the prompt is already pre-filled in Conductor's composer.
|
|
71
|
+
const settled = prompt || configureAgent
|
|
72
|
+
? firstPrompts.enqueue(created.id, prompt, body.sendImmediately !== false, attachmentIds, configureAgent ? agent : undefined)
|
|
73
|
+
: null;
|
|
74
|
+
const failed = settled && body.send === true ? await settled : null;
|
|
75
|
+
settled?.catch(() => undefined); // fire-and-forget: it reports failure, it never rejects
|
|
76
|
+
return json(req, res, 200, {
|
|
77
|
+
ok: true,
|
|
78
|
+
workspaceId: created.id,
|
|
79
|
+
workspace: reads.getWorkspace(created.id) ?? created,
|
|
80
|
+
pendingPrompt: prompt || undefined,
|
|
81
|
+
model: agent.model,
|
|
82
|
+
sent: body.send === true && !!prompt && !failed,
|
|
83
|
+
configured: body.send === true && configureAgent && !failed,
|
|
84
|
+
warning: failed?.error &&
|
|
85
|
+
`Workspace created; the initial ${configureAgent ? 'agent settings and prompt' : 'prompt'} didn’t finish (${failed.error}).`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return NOT_HANDLED;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { routeParam, routes } from "../../routes.js";
|
|
3
|
+
import { NOT_HANDLED } from "../router-types.js";
|
|
4
|
+
export function createFilesRoutes(services) {
|
|
5
|
+
const { reads, json, serveLocalImage, serveFilePreview } = services;
|
|
6
|
+
return async (req, res, url) => {
|
|
7
|
+
const { pathname } = url;
|
|
8
|
+
// GET /api/repos/:name/icon — the repo's resolved sidebar icon (see src/files/icons.ts)
|
|
9
|
+
const repo = routeParam(routes.repoIcon, req.method, pathname);
|
|
10
|
+
if (repo) {
|
|
11
|
+
const icon = reads.resolveRepoIcon(repo);
|
|
12
|
+
if (!icon)
|
|
13
|
+
return json(req, res, 404, { error: 'no icon' });
|
|
14
|
+
return void fs.readFile(icon.path, (err, data) => {
|
|
15
|
+
if (err)
|
|
16
|
+
return void json(req, res, 404, { error: 'no icon' });
|
|
17
|
+
// Cache briefly on the phone; the resolver itself refreshes within ~30s of an icon change.
|
|
18
|
+
res.writeHead(200, { 'content-type': icon.contentType, 'cache-control': 'public, max-age=300' });
|
|
19
|
+
res.end(data);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// GET /api/local-images/:path — local images linked from agent Markdown. The browser fetches this
|
|
23
|
+
// with its Authorization header and turns the reply into an object URL (web/src/components/transcript/Markdown.tsx), so the secret
|
|
24
|
+
// stays out of the image URL. `serveLocalImage` limits reads to temp files and permitted workspace paths.
|
|
25
|
+
const localImage = routeParam(routes.localImage, req.method, pathname);
|
|
26
|
+
if (localImage)
|
|
27
|
+
return serveLocalImage(req, res, localImage);
|
|
28
|
+
// GET /api/tool-images/:reference — a screenshot or other image a tool returned. Held
|
|
29
|
+
// back from the transcript itself (~100 kB of base64 each) and fetched only for a step
|
|
30
|
+
// the reader opened, with the phone's auth header, like every other image route here.
|
|
31
|
+
const toolImageRef = routeParam(routes.toolImage, req.method, pathname);
|
|
32
|
+
if (toolImageRef) {
|
|
33
|
+
const image = reads.toolImage(toolImageRef);
|
|
34
|
+
if (!image)
|
|
35
|
+
return json(req, res, 404, { error: 'image not found' });
|
|
36
|
+
const bytes = Buffer.from(image.data, 'base64');
|
|
37
|
+
// Immutable: a transcript row is written once, so the reference names one picture
|
|
38
|
+
// forever and re-opening the step costs nothing.
|
|
39
|
+
res.writeHead(200, {
|
|
40
|
+
'content-type': image.mediaType,
|
|
41
|
+
'content-length': String(bytes.length),
|
|
42
|
+
'cache-control': 'private, max-age=86400, immutable'
|
|
43
|
+
});
|
|
44
|
+
return void res.end(bytes);
|
|
45
|
+
}
|
|
46
|
+
// GET /api/files/:reference — source linked from an agent reply. The Markdown component
|
|
47
|
+
// intercepts the browser navigation and fetches this endpoint with its auth header.
|
|
48
|
+
const fileReference = routeParam(routes.filePreview, req.method, pathname);
|
|
49
|
+
if (fileReference)
|
|
50
|
+
return serveFilePreview(req, res, fileReference);
|
|
51
|
+
return NOT_HANDLED;
|
|
52
|
+
};
|
|
53
|
+
}
|