conductor-remote 1.56.1 → 1.57.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/index.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<title>Conductor Remote</title>
|
|
14
14
|
<!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
|
|
15
15
|
<script src="/self-heal.js"></script>
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-DzCE_FWE.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/index-C2BviSOi.css">
|
|
18
18
|
<link rel="manifest" href="/manifest.webmanifest"></head>
|
|
19
19
|
<body>
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"
|
|
1
|
+
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"000fa930603fb0277be9ebdef5a79be3"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DzCE_FWE.js",revision:null},{url:"assets/index-C2BviSOi.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An opaque, copyable pointer to one source message in a Conductor chat.
|
|
3
|
+
*
|
|
4
|
+
* Search stores `session_messages.rowid` already. Keep that implementation detail
|
|
5
|
+
* out of the MCP contract so the cursor can change shape later without teaching
|
|
6
|
+
* agents to do arithmetic on it.
|
|
7
|
+
*/
|
|
8
|
+
export function chatCursor(rowid) {
|
|
9
|
+
if (!Number.isSafeInteger(rowid) || rowid < 1)
|
|
10
|
+
throw new Error('chat cursor rowid must be a positive integer');
|
|
11
|
+
return `m${rowid.toString(36)}`;
|
|
12
|
+
}
|
|
13
|
+
/** Decode a cursor supplied back to `read_chat`; null means it was not one of ours. */
|
|
14
|
+
export function parseChatCursor(cursor) {
|
|
15
|
+
if (!/^m[0-9a-z]+$/.test(cursor))
|
|
16
|
+
return null;
|
|
17
|
+
const rowid = Number.parseInt(cursor.slice(1), 36);
|
|
18
|
+
return Number.isSafeInteger(rowid) && rowid > 0 ? rowid : null;
|
|
19
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* router it would otherwise have to be carved out of, and it keeps *one* code path:
|
|
26
26
|
* a tool cannot behave differently over HTTP than it does over stdio.
|
|
27
27
|
*/
|
|
28
|
+
import { chatCursor, parseChatCursor } from "./chat-cursor.js";
|
|
28
29
|
import { routes } from "./routes.js";
|
|
29
30
|
import { HIT_CLOSE, HIT_OPEN, workspaceTitle } from "./shared.js";
|
|
30
31
|
/** Versions we know how to speak. The client's choice wins when we know it. */
|
|
@@ -44,6 +45,42 @@ function unmark(text) {
|
|
|
44
45
|
function clip(text, max) {
|
|
45
46
|
return text.length > max ? `${text.slice(0, max)}… [${text.length - max} more chars]` : text;
|
|
46
47
|
}
|
|
48
|
+
/** A true output bound: unlike `clip`, the truncation marker is inside `max`. */
|
|
49
|
+
function clipExact(text, max) {
|
|
50
|
+
if (text.length <= max)
|
|
51
|
+
return text;
|
|
52
|
+
const suffix = '… [truncated]';
|
|
53
|
+
if (max <= suffix.length)
|
|
54
|
+
return text.slice(0, max);
|
|
55
|
+
return `${text.slice(0, max - suffix.length)}${suffix}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fit a transcript inside one MCP result without dropping either side of a nearby
|
|
59
|
+
* read. Short entries keep their full text; the longest entries share what remains.
|
|
60
|
+
*/
|
|
61
|
+
function boundedTranscript(header, entries, maxChars) {
|
|
62
|
+
const prefix = header.length ? `${header.join('\n')}\n\n` : '';
|
|
63
|
+
if (!entries.length)
|
|
64
|
+
return clipExact(prefix.trim(), maxChars);
|
|
65
|
+
const separators = Math.max(0, entries.length - 1) * 2;
|
|
66
|
+
const available = Math.max(1, maxChars - prefix.length - separators);
|
|
67
|
+
const fullLength = entries.reduce((sum, entry) => sum + entry.length, 0);
|
|
68
|
+
if (fullLength <= available)
|
|
69
|
+
return `${prefix}${entries.join('\n\n')}`;
|
|
70
|
+
// Find the largest per-entry ceiling whose clipped rows fit. Entries shorter than
|
|
71
|
+
// it return their unused share to the longer ones, unlike a fixed equal split.
|
|
72
|
+
let low = 1;
|
|
73
|
+
let high = Math.max(...entries.map(entry => entry.length));
|
|
74
|
+
while (low < high) {
|
|
75
|
+
const mid = Math.ceil((low + high) / 2);
|
|
76
|
+
const used = entries.reduce((sum, entry) => sum + Math.min(entry.length, mid), 0);
|
|
77
|
+
if (used <= available)
|
|
78
|
+
low = mid;
|
|
79
|
+
else
|
|
80
|
+
high = mid - 1;
|
|
81
|
+
}
|
|
82
|
+
return clipExact(`${prefix}${entries.map(entry => clipExact(entry, low)).join('\n\n')}`, maxChars);
|
|
83
|
+
}
|
|
47
84
|
function plural(n, one, many = `${one}s`) {
|
|
48
85
|
return `${n} ${n === 1 ? one : many}`;
|
|
49
86
|
}
|
|
@@ -75,7 +112,7 @@ export function createTools(call) {
|
|
|
75
112
|
return [
|
|
76
113
|
{
|
|
77
114
|
name: 'search_chats',
|
|
78
|
-
description: 'Full-text search every Conductor chat on this Mac, archived workspaces included, and get back the workspaces that discussed it with the matching excerpts. Use this to answer "which workspace did I do X in" or "what did we decide about X". Searches the prompts the user typed, the agent replies and the agent\'s reasoning, not tool output. Each excerpt is tagged [user], [assistant] or [thinking] — a [thinking] hit is reasoning the agent never said out loud, so do not quote it back as its answer.
|
|
115
|
+
description: 'Full-text search every Conductor chat on this Mac, archived workspaces included, and get back the workspaces that discussed it with the matching excerpts. Use this to answer "which workspace did I do X in" or "what did we decide about X". Searches the prompts the user typed, the agent replies and the agent\'s reasoning, not tool output. Each excerpt is tagged [user], [assistant] or [thinking] — a [thinking] hit is reasoning the agent never said out loud, so do not quote it back as its answer. Every excerpt carries its own session_id and cursor for a bounded nearby read_chat.',
|
|
79
116
|
inputSchema: {
|
|
80
117
|
type: 'object',
|
|
81
118
|
properties: {
|
|
@@ -104,20 +141,32 @@ export function createTools(call) {
|
|
|
104
141
|
lines.push(`workspace_id: ${w.id}${r.sessionId ? ` session_id: ${r.sessionId}` : ''}`);
|
|
105
142
|
if (r.hits)
|
|
106
143
|
lines.push(`${r.hits} matching message${r.hits === 1 ? '' : 's'}:`);
|
|
107
|
-
for (const s of r.snippets)
|
|
144
|
+
for (const s of r.snippets) {
|
|
108
145
|
lines.push(` [${s.role}] ${clip(unmark(s.text), 400)}`);
|
|
146
|
+
lines.push(` session_id: ${s.sessionId} cursor: ${s.cursor}`);
|
|
147
|
+
}
|
|
109
148
|
}
|
|
110
149
|
return lines.join('\n').trim();
|
|
111
150
|
}
|
|
112
151
|
},
|
|
113
152
|
{
|
|
114
153
|
name: 'read_chat',
|
|
115
|
-
description: 'Read a Conductor chat transcript by session_id, newest messages last.
|
|
154
|
+
description: 'Read a bounded Conductor chat transcript by session_id, newest messages last. Pass a cursor from search_chats or a prior read_chat as near; before and after expand either direction or both together. For a [thinking] search hit, set include_thinking true to include the matching block. Without near, returns the trailing entries. Works for archived workspaces. Tool calls and failed tool output are summarised to one line each; prose is verbatim unless the output budget truncates it.',
|
|
116
155
|
inputSchema: {
|
|
117
156
|
type: 'object',
|
|
118
157
|
properties: {
|
|
119
158
|
session_id: { type: 'string', description: 'From search_chats or list_chats.' },
|
|
120
|
-
|
|
159
|
+
near: { type: 'string', description: 'A cursor from search_chats or a prior read_chat.' },
|
|
160
|
+
before: { type: 'number', description: 'Entries before near to return (default 6, max 100).' },
|
|
161
|
+
after: { type: 'number', description: 'Entries after near to return (default 6, max 100).' },
|
|
162
|
+
limit: {
|
|
163
|
+
type: 'number',
|
|
164
|
+
description: 'Trailing entries when near is omitted (default 12, max 400).'
|
|
165
|
+
},
|
|
166
|
+
max_chars: {
|
|
167
|
+
type: 'number',
|
|
168
|
+
description: 'Hard result-size budget (default 12000, min 1000, max 40000).'
|
|
169
|
+
},
|
|
121
170
|
include_thinking: { type: 'boolean', description: 'Include the agent’s reasoning (default false).' },
|
|
122
171
|
include_tools: {
|
|
123
172
|
type: 'boolean',
|
|
@@ -128,7 +177,15 @@ export function createTools(call) {
|
|
|
128
177
|
},
|
|
129
178
|
run: async (args) => {
|
|
130
179
|
const sessionId = need(args, 'session_id');
|
|
131
|
-
const
|
|
180
|
+
const near = str(args.near);
|
|
181
|
+
const anchor = near ? parseChatCursor(near) : null;
|
|
182
|
+
if (near && anchor === null)
|
|
183
|
+
throw new Error('near must be a cursor returned by search_chats');
|
|
184
|
+
const count = (value, fallback) => Math.min(100, Math.max(0, Math.floor(num(value) ?? fallback)));
|
|
185
|
+
const before = count(args.before, 6);
|
|
186
|
+
const after = count(args.after, 6);
|
|
187
|
+
const limit = Math.min(400, Math.max(1, Math.floor(num(args.limit) ?? 12)));
|
|
188
|
+
const maxChars = Math.min(40_000, Math.max(1_000, Math.floor(num(args.max_chars) ?? 12_000)));
|
|
132
189
|
// One flag each, because they answer different questions: tools are what the agent
|
|
133
190
|
// *did*, thinking is why it did it. Reading both off `include_tools` meant the only
|
|
134
191
|
// way to see the reasoning was to take the tool churn along with it.
|
|
@@ -137,19 +194,47 @@ export function createTools(call) {
|
|
|
137
194
|
const data = await call(`${routes.messages.path(sessionId)}?after=0`, {
|
|
138
195
|
timeoutMs: 30_000
|
|
139
196
|
});
|
|
197
|
+
if (anchor !== null && !data.entries.some(entry => entry.rowid === anchor)) {
|
|
198
|
+
throw new Error('near cursor is not in that session');
|
|
199
|
+
}
|
|
140
200
|
const wanted = data.entries.filter(e => e.role === 'thinking' ? includeThinking : e.role === 'tool' ? includeTools : true);
|
|
141
201
|
if (!wanted.length)
|
|
142
202
|
return `no messages in session ${sessionId}`;
|
|
143
|
-
|
|
144
|
-
const head =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
203
|
+
let selected;
|
|
204
|
+
const head = [];
|
|
205
|
+
if (anchor === null) {
|
|
206
|
+
selected = wanted.slice(-limit);
|
|
207
|
+
if (selected.length < wanted.length) {
|
|
208
|
+
head.push(`(last ${selected.length} of ${wanted.length} entries · older_cursor: ${chatCursor(selected[0].rowid)})`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const older = wanted.filter(entry => entry.rowid < anchor);
|
|
213
|
+
const at = wanted.filter(entry => entry.rowid === anchor);
|
|
214
|
+
const newer = wanted.filter(entry => entry.rowid > anchor);
|
|
215
|
+
selected = [...(before ? older.slice(-before) : []), ...at, ...newer.slice(0, after)];
|
|
216
|
+
const parts = [
|
|
217
|
+
`near ${near}`,
|
|
218
|
+
`${Math.min(before, older.length)} before`,
|
|
219
|
+
`${at.length} at`,
|
|
220
|
+
`${Math.min(after, newer.length)} after`
|
|
221
|
+
];
|
|
222
|
+
if (older.length > before && selected[0])
|
|
223
|
+
parts.push(`older_cursor: ${chatCursor(selected[0].rowid)}`);
|
|
224
|
+
if (newer.length > after && selected.at(-1)) {
|
|
225
|
+
parts.push(`newer_cursor: ${chatCursor(selected.at(-1).rowid)}`);
|
|
226
|
+
}
|
|
227
|
+
head.push(`(${parts.join(' · ')})`);
|
|
228
|
+
}
|
|
229
|
+
if (!selected.length) {
|
|
230
|
+
return boundedTranscript(head, ['(the matching entry is excluded by the current role filters)'], maxChars);
|
|
231
|
+
}
|
|
232
|
+
const rendered = selected.map(entry => {
|
|
233
|
+
if (entry.role === 'tool')
|
|
234
|
+
return `[tool ${entry.tool ?? ''}] ${clip(entry.text, 200)}${entry.detail ? ` — ${entry.detail}` : ''}`;
|
|
235
|
+
return `[${entry.role}] ${entry.text}`;
|
|
236
|
+
});
|
|
237
|
+
return boundedTranscript(head, rendered, maxChars);
|
|
153
238
|
}
|
|
154
239
|
},
|
|
155
240
|
{
|
package/dist-node/src/search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { chatCursor } from "./chat-cursor.js";
|
|
4
5
|
import { HIT_CLOSE, HIT_OPEN } from "./shared.js";
|
|
5
6
|
import { parseMessage } from "./transcript.js";
|
|
6
7
|
/**
|
|
@@ -335,7 +336,13 @@ export function foldHits(hits, resolve) {
|
|
|
335
336
|
// scoring and snippeting the same slice needs no second sort.
|
|
336
337
|
if (entry.snippets.length < SNIPPETS_PER_RESULT) {
|
|
337
338
|
entry.score += hit.score;
|
|
338
|
-
entry.snippets.push({
|
|
339
|
+
entry.snippets.push({
|
|
340
|
+
sessionId: hit.sessionId,
|
|
341
|
+
cursor: chatCursor(hit.srcRowid),
|
|
342
|
+
role: hit.role,
|
|
343
|
+
at: hit.at,
|
|
344
|
+
text: hit.snippet
|
|
345
|
+
});
|
|
339
346
|
}
|
|
340
347
|
}
|
|
341
348
|
const results = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.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.",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"typecheck": "tsc -p tsconfig.json",
|
|
51
51
|
"lint": "biome check .",
|
|
52
52
|
"fix": "biome check . --fix",
|
|
53
|
-
"verify": "yarn typecheck && yarn lint && yarn check:pr && yarn check:imports && yarn check:routes && yarn check:attachments && yarn check:applescript && yarn check:config && yarn check:utf8-target && yarn check:file-links && yarn check:model-cache && yarn check:model-labels && yarn check:transcript-actions && yarn check:nosleep && yarn check:uilock && yarn check:firstprompt && yarn check:sendonce && yarn check:notify && yarn check:online",
|
|
53
|
+
"verify": "yarn typecheck && yarn lint && yarn check:pr && yarn check:imports && yarn check:routes && yarn check:attachments && yarn check:applescript && yarn check:config && yarn check:utf8-target && yarn check:file-links && yarn check:model-cache && yarn check:mcp-chat-read && yarn check:model-labels && yarn check:transcript-actions && yarn check:nosleep && yarn check:uilock && yarn check:firstprompt && yarn check:sendonce && yarn check:notify && yarn check:online",
|
|
54
54
|
"release": "semantic-release",
|
|
55
55
|
"prepack": "yarn build && yarn build:node",
|
|
56
56
|
"postinstall": "husky || true",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"check:file-links": "node scripts/check-file-links.ts",
|
|
62
62
|
"check:firstprompt": "node scripts/check-firstprompt.ts",
|
|
63
63
|
"check:model-cache": "node scripts/check-model-cache.ts",
|
|
64
|
+
"check:mcp-chat-read": "node scripts/check-mcp-chat-read.ts",
|
|
64
65
|
"check:model-labels": "node scripts/check-model-labels.ts",
|
|
65
66
|
"check:transcript-actions": "node scripts/check-transcript-actions.ts",
|
|
66
67
|
"check:sendonce": "node scripts/check-sendonce.ts",
|