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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describeRepoIcon, resolveRepoIcon } from "../files/icons.js";
|
|
2
|
+
import { resolveWorktree } from "./worktrees.js";
|
|
3
|
+
/**
|
|
4
|
+
* Neutralize LIKE's own wildcards in a user's search word. `queryTokens` already
|
|
5
|
+
* strips everything but letters, digits and `_` — and `_` is LIKE's single-character
|
|
6
|
+
* wildcard, so "add_set" would quietly also match "addaset" without this.
|
|
7
|
+
*/
|
|
8
|
+
function escapeLike(s) {
|
|
9
|
+
return s.replace(/[\\%_]/g, m => `\\${m}`);
|
|
10
|
+
}
|
|
11
|
+
/** Shared row → `SearchWorkspace` shape for the two search reads below. */
|
|
12
|
+
function toSearchWorkspace(r) {
|
|
13
|
+
return {
|
|
14
|
+
id: r.id,
|
|
15
|
+
workspace_name: r.workspace_name,
|
|
16
|
+
pr_title: r.pr_title,
|
|
17
|
+
branch: r.branch,
|
|
18
|
+
directory_name: r.directory_name,
|
|
19
|
+
state: r.state,
|
|
20
|
+
updated_at: r.updated_at,
|
|
21
|
+
repo_name: r.repo_name,
|
|
22
|
+
icon: describeRepoIcon({ icon: r.repo_icon, repoRoot: r.repo_root, remoteUrl: r.remote_url }),
|
|
23
|
+
archived: r.state === 'archived'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export class WorkspacesReads {
|
|
27
|
+
db;
|
|
28
|
+
workspacesRoot;
|
|
29
|
+
constructor(db, workspacesRoot) {
|
|
30
|
+
this.db = db;
|
|
31
|
+
this.workspacesRoot = workspacesRoot;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Unread chats, grouped by workspace. `workspaces.unread` is a **dead column** —
|
|
35
|
+
* Conductor still declares it (and its migration still says "when 1, the workspace
|
|
36
|
+
* should be marked visually as unread") but writes 0 on every row: 0 of 1691 are
|
|
37
|
+
* non-zero on this Mac, which is why the sidebar's bold/badge never once fired.
|
|
38
|
+
* The live flag is per session, and it *is* a flag — `unread_count` is only ever
|
|
39
|
+
* 0 or 1 — so what's worth counting is how many chats have news, not the column.
|
|
40
|
+
*/
|
|
41
|
+
unreadSessions() {
|
|
42
|
+
const rows = this.db.query(`SELECT workspace_id, id, updated_at
|
|
43
|
+
FROM sessions
|
|
44
|
+
WHERE COALESCE(unread_count, 0) > 0 AND COALESCE(is_hidden, 0) = 0`);
|
|
45
|
+
const byWorkspace = new Map();
|
|
46
|
+
for (const r of rows) {
|
|
47
|
+
if (!r.workspace_id)
|
|
48
|
+
continue;
|
|
49
|
+
const list = byWorkspace.get(r.workspace_id);
|
|
50
|
+
if (list)
|
|
51
|
+
list.push({ id: r.id, at: r.updated_at });
|
|
52
|
+
else
|
|
53
|
+
byWorkspace.set(r.workspace_id, [{ id: r.id, at: r.updated_at }]);
|
|
54
|
+
}
|
|
55
|
+
return byWorkspace;
|
|
56
|
+
}
|
|
57
|
+
listWorkspaces() {
|
|
58
|
+
const rows = this.db.query(`SELECT w.id, w.directory_name, w.workspace_name, w.branch, w.pr_title, w.derived_status, w.manual_status,
|
|
59
|
+
w.state, w.created_at, w.updated_at, w.pinned_at, w.active_session_id, w.intended_target_branch,
|
|
60
|
+
r.name AS repo_name, r.root_path AS repo_root, r.icon AS repo_icon,
|
|
61
|
+
r.remote_url AS remote_url, r.default_branch AS default_branch,
|
|
62
|
+
s.status AS session_status, s.title AS session_title, s.model AS model,
|
|
63
|
+
s.agent_type AS agent_type
|
|
64
|
+
FROM workspaces w
|
|
65
|
+
LEFT JOIN repos r ON r.id = w.repository_id
|
|
66
|
+
LEFT JOIN sessions s ON s.id = w.active_session_id
|
|
67
|
+
WHERE w.state IN ('ready', 'setting_up')
|
|
68
|
+
ORDER BY (w.pinned_at IS NULL), w.updated_at DESC`);
|
|
69
|
+
const unread = this.unreadSessions();
|
|
70
|
+
return rows.map(r => ({
|
|
71
|
+
...r,
|
|
72
|
+
unread_sessions: unread.get(r.id) ?? [],
|
|
73
|
+
worktree: resolveWorktree(this.workspacesRoot, r.repo_name, r.directory_name, r.branch, r.repo_root),
|
|
74
|
+
baseBranch: r.intended_target_branch || r.default_branch || 'main',
|
|
75
|
+
icon: describeRepoIcon({ icon: r.repo_icon, repoRoot: r.repo_root, remoteUrl: r.remote_url })
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
getWorkspace(id) {
|
|
79
|
+
return this.listWorkspaces().find(w => w.id === id) ?? null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The repos Conductor knows about, most recently used first. `root_path` is
|
|
83
|
+
* what a `conductor://…&path=` deep link needs to pick the target repo — with
|
|
84
|
+
* no path Conductor silently falls back to the *first* repo.
|
|
85
|
+
*
|
|
86
|
+
* "Used" is the newest workspace in the repo, never `repos.updated_at`: that
|
|
87
|
+
* column moves when the repo's *settings* change, so the repo worked in every
|
|
88
|
+
* day here reads as last touched six weeks ago. Repos with no workspace at all
|
|
89
|
+
* keep Conductor's own sidebar order, behind every repo that has one.
|
|
90
|
+
*
|
|
91
|
+
* The `replace()`s are load-bearing: `created_at` is always `YYYY-MM-DD HH:MM:SS`
|
|
92
|
+
* while `updated_at` is written both that way (306 rows) and as ISO with a `Z`
|
|
93
|
+
* (1,819), and 'T' sorts after ' ', so an untouched string compare can rank an
|
|
94
|
+
* earlier ISO row above a later plain one on the same day.
|
|
95
|
+
*/
|
|
96
|
+
listRepos() {
|
|
97
|
+
const rows = this.db.query(`SELECT r.name, r.root_path, r.default_branch, r.icon, r.remote_url
|
|
98
|
+
FROM repos r
|
|
99
|
+
LEFT JOIN workspaces w ON w.repository_id = r.id
|
|
100
|
+
WHERE COALESCE(r.hidden, 0) = 0
|
|
101
|
+
GROUP BY r.id
|
|
102
|
+
ORDER BY (MAX(MAX(REPLACE(REPLACE(w.updated_at, 'T', ' '), 'Z', ''), w.created_at)) IS NULL),
|
|
103
|
+
MAX(MAX(REPLACE(REPLACE(w.updated_at, 'T', ' '), 'Z', ''), w.created_at)) DESC,
|
|
104
|
+
(r.display_order IS NULL), r.display_order, r.name`);
|
|
105
|
+
return rows.map(r => ({
|
|
106
|
+
name: r.name,
|
|
107
|
+
root_path: r.root_path,
|
|
108
|
+
default_branch: r.default_branch,
|
|
109
|
+
icon: describeRepoIcon({ icon: r.icon, repoRoot: r.root_path, remoteUrl: r.remote_url })
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Map chat ids to the workspace that owns them, archived included.
|
|
114
|
+
*
|
|
115
|
+
* `listWorkspaces` filters to `state IN ('ready','setting_up')`, which is right for
|
|
116
|
+
* the sidebar and wrong for search: the whole point of searching the transcript is
|
|
117
|
+
* to reach work you finished and put away. One query for the whole hit set, because
|
|
118
|
+
* a search fans out over up to 300 chunks and a per-chat lookup would be 300 of them.
|
|
119
|
+
*/
|
|
120
|
+
searchTargets(sessionIds) {
|
|
121
|
+
const out = new Map();
|
|
122
|
+
if (!sessionIds.length)
|
|
123
|
+
return out;
|
|
124
|
+
const holes = sessionIds.map(() => '?').join(',');
|
|
125
|
+
const rows = this.db.query(`SELECT s.id AS session_id, s.title AS session_title,
|
|
126
|
+
w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
|
|
127
|
+
r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
|
|
128
|
+
FROM sessions s
|
|
129
|
+
JOIN workspaces w ON w.id = s.workspace_id
|
|
130
|
+
LEFT JOIN repos r ON r.id = w.repository_id
|
|
131
|
+
WHERE s.id IN (${holes})`, sessionIds);
|
|
132
|
+
for (const r of rows)
|
|
133
|
+
out.set(r.session_id, { sessionTitle: r.session_title, workspace: toSearchWorkspace(r) });
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Every chat in the requested search scope. The index only carries chat ids, so
|
|
138
|
+
* repo and archive filters have to be resolved here before ranking. Names, not repo
|
|
139
|
+
* ids, because names are what the phone's picker and `list_repos` hold.
|
|
140
|
+
*/
|
|
141
|
+
searchSessionIds(repos, includeArchived) {
|
|
142
|
+
if (repos && !repos.length)
|
|
143
|
+
return [];
|
|
144
|
+
const scope = [
|
|
145
|
+
...(repos ? [`r.name IN (${repos.map(() => '?').join(',')})`] : []),
|
|
146
|
+
...(!includeArchived ? ["w.state IS NOT 'archived'"] : [])
|
|
147
|
+
];
|
|
148
|
+
return this.db
|
|
149
|
+
.query(`SELECT s.id FROM sessions s
|
|
150
|
+
JOIN workspaces w ON w.id = s.workspace_id
|
|
151
|
+
LEFT JOIN repos r ON r.id = w.repository_id
|
|
152
|
+
${scope.length ? ` WHERE ${scope.join(' AND ')}` : ''}`, repos ?? [])
|
|
153
|
+
.map(r => r.id);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Workspaces whose own identity matches every token — name, PR title, branch,
|
|
157
|
+
* worktree codename or repo. Archived are included by default, for the same reason
|
|
158
|
+
* as `searchTargets`; `includeArchived` lets the phone narrow the search to current
|
|
159
|
+
* work. `repos` narrows to those repos by name; an empty list matches nothing.
|
|
160
|
+
*
|
|
161
|
+
* This is the half of search the transcript index cannot do. A workspace named for
|
|
162
|
+
* the thing you are looking for may never have said those words in its chat, and
|
|
163
|
+
* one whose chat is empty has no chunks at all.
|
|
164
|
+
*/
|
|
165
|
+
findWorkspacesByName(tokens, limit = 20, repos, includeArchived = true) {
|
|
166
|
+
if (!tokens.length)
|
|
167
|
+
return [];
|
|
168
|
+
if (repos && !repos.length)
|
|
169
|
+
return [];
|
|
170
|
+
const fields = ['w.workspace_name', 'w.pr_title', 'w.branch', 'w.directory_name', 'r.name'];
|
|
171
|
+
// AND across tokens, OR across fields: "auk lamp" should find the lamp workspace in
|
|
172
|
+
// the auk repo, where no single column holds both words.
|
|
173
|
+
const byToken = tokens.map(() => `(${fields.map(f => `${f} LIKE ? ESCAPE '\\'`).join(' OR ')})`);
|
|
174
|
+
const scope = [
|
|
175
|
+
...(repos ? [`r.name IN (${repos.map(() => '?').join(',')})`] : []),
|
|
176
|
+
...(!includeArchived ? ["w.state IS NOT 'archived'"] : [])
|
|
177
|
+
];
|
|
178
|
+
const where = [...byToken, ...scope].join(' AND ');
|
|
179
|
+
const params = [...tokens.flatMap(t => fields.map(() => `%${escapeLike(t)}%`)), ...(repos ?? [])];
|
|
180
|
+
const rows = this.db.query(`SELECT w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
|
|
181
|
+
r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
|
|
182
|
+
FROM workspaces w
|
|
183
|
+
LEFT JOIN repos r ON r.id = w.repository_id
|
|
184
|
+
WHERE ${where}
|
|
185
|
+
ORDER BY (w.state = 'archived'), w.updated_at DESC
|
|
186
|
+
LIMIT ?`, [...params, limit]);
|
|
187
|
+
return rows.map(toSearchWorkspace);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* One workspace by id, whatever state it is in — the read behind opening an archived
|
|
191
|
+
* chat (`GET /api/workspaces/:id`).
|
|
192
|
+
*
|
|
193
|
+
* `getWorkspace` above is the *live* one: it resolves a worktree and a base branch,
|
|
194
|
+
* and returns null for the 1,846 archived workspaces here, which is right for every
|
|
195
|
+
* write (there is nothing to focus and nothing to diff) and wrong for reading. The
|
|
196
|
+
* transcript survives archiving — Conductor deletes the worktree, not the chat — so
|
|
197
|
+
* this returns the same `SearchWorkspace` a search result carries, with no worktree
|
|
198
|
+
* and no git, and `listSessions`/`getMessages` do the rest by id.
|
|
199
|
+
*/
|
|
200
|
+
getAnyWorkspace(id) {
|
|
201
|
+
const rows = this.db.query(`SELECT w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
|
|
202
|
+
r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
|
|
203
|
+
FROM workspaces w
|
|
204
|
+
LEFT JOIN repos r ON r.id = w.repository_id
|
|
205
|
+
WHERE w.id = ?
|
|
206
|
+
LIMIT 1`, [id]);
|
|
207
|
+
return rows[0] ? toSearchWorkspace(rows[0]) : null;
|
|
208
|
+
}
|
|
209
|
+
/** Resolve a repo's icon by its name (the sidebar avatar) — null if the repo or icon is unknown. */
|
|
210
|
+
resolveRepoIcon(repoName) {
|
|
211
|
+
const rows = this.db.query('SELECT root_path FROM repos WHERE name = ? LIMIT 1', [
|
|
212
|
+
repoName
|
|
213
|
+
]);
|
|
214
|
+
const root = rows[0]?.root_path;
|
|
215
|
+
return root ? resolveRepoIcon(root) : null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Which workspace a chat belongs to.
|
|
219
|
+
*
|
|
220
|
+
* Every other route resolves this by matching `active_session_id`, which only ever
|
|
221
|
+
* finds the tab that is currently on screen. That is fine for a phone, where the
|
|
222
|
+
* chat you are looking at is the chat you are sending to. It is wrong for anything
|
|
223
|
+
* addressing a chat by id — a background tab has a workspace too, and `sessions`
|
|
224
|
+
* has carried `workspace_id` all along.
|
|
225
|
+
*/
|
|
226
|
+
sessionWorkspaceId(sessionId) {
|
|
227
|
+
const rows = this.db.query(`SELECT workspace_id FROM sessions WHERE id = ? LIMIT 1`, [sessionId]);
|
|
228
|
+
return rows[0]?.workspace_id ?? null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const worktreeCache = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a workspace's worktree path. Conductor lays worktrees out as
|
|
7
|
+
* `<workspacesRoot>/<repoName>/<directoryName>`, but we verify against
|
|
8
|
+
* `git worktree list` (matched by branch) so a layout change can't silently
|
|
9
|
+
* point us at the wrong tree.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveWorktree(workspacesRoot, repoName, directoryName, branch, repoRoot) {
|
|
12
|
+
if (repoName && directoryName) {
|
|
13
|
+
const guess = path.join(workspacesRoot, repoName, directoryName);
|
|
14
|
+
if (fs.existsSync(path.join(guess, '.git')))
|
|
15
|
+
return guess;
|
|
16
|
+
}
|
|
17
|
+
if (!(repoRoot && branch))
|
|
18
|
+
return null;
|
|
19
|
+
const cacheKey = repoRoot;
|
|
20
|
+
let listing = worktreeCache.get(cacheKey);
|
|
21
|
+
if (listing === undefined) {
|
|
22
|
+
try {
|
|
23
|
+
listing = execFileSync('git', ['-C', repoRoot, 'worktree', 'list', '--porcelain'], {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
timeout: 5000
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
listing = null;
|
|
30
|
+
}
|
|
31
|
+
worktreeCache.set(cacheKey, listing);
|
|
32
|
+
}
|
|
33
|
+
if (!listing)
|
|
34
|
+
return null;
|
|
35
|
+
// Porcelain: blocks of "worktree <path>" / "branch refs/heads/<name>"
|
|
36
|
+
const blocks = listing.split('\n\n');
|
|
37
|
+
for (const block of blocks) {
|
|
38
|
+
if (block.includes(`refs/heads/${branch}`)) {
|
|
39
|
+
const m = block.match(/^worktree (.+)$/m);
|
|
40
|
+
if (m)
|
|
41
|
+
return m[1];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
package/dist-node/src/routes.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Every `/api` path, declared once, for the three callers that must agree on it.
|
|
3
3
|
*
|
|
4
4
|
* The relay matches these, the phone builds them (`web/src/lib/api.ts`) and the MCP
|
|
5
|
-
* tools build them too (`src/mcp
|
|
5
|
+
* tools build them too (`src/mcp/tools/`). Before this each path was spelled three
|
|
6
6
|
* times: a regex here, a template literal there, another template literal in the third
|
|
7
7
|
* place. `src/wire.ts` had already made the *shapes* impossible to disagree about, and
|
|
8
8
|
* this is the other half — a renamed path used to typecheck cleanly in all three files
|
|
@@ -85,7 +85,7 @@ export const routes = {
|
|
|
85
85
|
/**
|
|
86
86
|
* Quit Conductor and start it again. Not a workspace route: it is about the app,
|
|
87
87
|
* and its whole reason to exist is a Conductor that looks healthy from every other
|
|
88
|
-
* route while nothing behind it runs (src/writes.ts ▸ restartConductorApp).
|
|
88
|
+
* route while nothing behind it runs (src/writes/system.ts ▸ restartConductorApp).
|
|
89
89
|
*/
|
|
90
90
|
restartConductor: flat('POST', '/api/conductor/restart'),
|
|
91
91
|
settings: flat('GET', '/api/settings'),
|
|
@@ -142,7 +142,7 @@ export const routes = {
|
|
|
142
142
|
devServer: param('GET', '/api/workspaces/:workspaceId/dev-server'),
|
|
143
143
|
startDevServer: param('POST', '/api/workspaces/:workspaceId/dev-server'),
|
|
144
144
|
stopDevServer: param('DELETE', '/api/workspaces/:workspaceId/dev-server'),
|
|
145
|
-
/** Dismiss a first prompt the relay never managed to deliver (src/firstprompt.ts). */
|
|
145
|
+
/** Dismiss a first prompt the relay never managed to deliver (src/delivery/firstprompt.ts). */
|
|
146
146
|
dismissFirstPrompt: param('DELETE', '/api/workspaces/:workspaceId/prompt'),
|
|
147
147
|
// ── chats ──
|
|
148
148
|
messages: param('GET', '/api/sessions/:sessionId/messages'),
|
|
@@ -160,11 +160,11 @@ export const routes = {
|
|
|
160
160
|
sendPrompt: param('POST', '/api/sessions/:sessionId/prompt'),
|
|
161
161
|
/** Write a phone-selected file into Conductor's attachment layout for this chat's workspace. */
|
|
162
162
|
uploadAttachment: param('POST', '/api/sessions/:sessionId/attachments'),
|
|
163
|
-
/** Copy a chat into a fresh tab beside it, as a Conductor attachment (src/attachments.ts). */
|
|
163
|
+
/** Copy a chat into a fresh tab beside it, as a Conductor attachment (src/files/attachments.ts). */
|
|
164
164
|
splitChat: param('POST', '/api/sessions/:sessionId/split'),
|
|
165
165
|
/** Lightweight tracked delegation from a chat outside an active Workflow. */
|
|
166
166
|
delegateTask: param('POST', '/api/sessions/:sessionId/delegate'),
|
|
167
|
-
/** Dismiss a prompt parked behind the lock screen (src/parked.ts). */
|
|
167
|
+
/** Dismiss a prompt parked behind the lock screen (src/delivery/parked.ts). */
|
|
168
168
|
dismissParkedPrompt: param('DELETE', '/api/sessions/:sessionId/prompt')
|
|
169
169
|
};
|
|
170
170
|
// ── matching, for the relay ─────────────────────────────────────────────────────
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
|
-
import { chatCursor } from "
|
|
2
|
+
import { chatCursor } from "../transcript/cursor.js";
|
|
3
3
|
/**
|
|
4
4
|
* Full-text search over the chat history, in a sidecar DB the relay owns.
|
|
5
5
|
*
|
|
@@ -48,7 +48,7 @@ const CHUNK_LIMIT = 300;
|
|
|
48
48
|
* phone, and no transcript contains them, so the client needs no parser. The client
|
|
49
49
|
* must not render them literally.
|
|
50
50
|
*/
|
|
51
|
-
export { HIT_CLOSE, HIT_OPEN } from "
|
|
51
|
+
export { HIT_CLOSE, HIT_OPEN } from "../shared.js";
|
|
52
52
|
/**
|
|
53
53
|
* Turn a phone query into an FTS5 MATCH expression.
|
|
54
54
|
*
|
|
@@ -114,7 +114,7 @@ export function matchQuery(raw) {
|
|
|
114
114
|
return required.length ? `${required.join(' AND ')} AND ${looseExpr}` : looseExpr;
|
|
115
115
|
}
|
|
116
116
|
/** The tokens `matchQuery` will search for — what a caller matches names against. */
|
|
117
|
-
export { queryTokens } from "
|
|
117
|
+
export { queryTokens } from "../shared.js";
|
|
118
118
|
/**
|
|
119
119
|
* Main-thread facade for the disposable full-text index.
|
|
120
120
|
*
|
|
@@ -142,7 +142,7 @@ export class SearchIndex {
|
|
|
142
142
|
return;
|
|
143
143
|
this.stopping = false;
|
|
144
144
|
this.indexStatus = { chunks: 0, ready: false, progress: 0 };
|
|
145
|
-
const module = import.meta.url.endsWith('.ts') ? './
|
|
145
|
+
const module = import.meta.url.endsWith('.ts') ? './worker.ts' : './worker.js';
|
|
146
146
|
try {
|
|
147
147
|
const worker = new Worker(new URL(module, import.meta.url), {
|
|
148
148
|
workerData: { sourceDbPath: this.sourceDbPath, file: this.file },
|
|
@@ -2,9 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
4
|
import { parentPort, workerData } from 'node:worker_threads';
|
|
5
|
-
import { ConductorDb } from "
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { ConductorDb } from "../db.js";
|
|
6
|
+
import { parseMessage } from "../transcript/parser.js";
|
|
7
|
+
import { HIT_CLOSE, HIT_OPEN, matchQuery } from "./coordinator.js";
|
|
8
8
|
/** Bump when extraction or an invariant such as one-chunk-per-source changes stored rows. */
|
|
9
9
|
// v3 rebuilds every chunk through WorkflowSecretScrubber so a capability indexed by
|
|
10
10
|
// an older relay cannot survive after the private-envelope boundary is introduced.
|