@detergent-software/conductor 3.5.0-dev.16 → 3.5.0-dev.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/build/daemon.js +1 -0
- package/build/daemon.js.map +1 -1
- package/build/inbox.js +52 -28
- package/build/inbox.js.map +1 -1
- package/build/manager.js +64 -4
- package/build/manager.js.map +1 -1
- package/build/plugin.js +9 -3
- package/build/plugin.js.map +1 -1
- package/build/server.js +1 -1
- package/build/server.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +4 -2
- package/plugin/hooks/model.ts +45 -11
- package/plugin/hooks/register.tsx +54 -22
- package/plugin/types/index.d.ts +5 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.5.0-dev.18",
|
|
4
4
|
"description": "The conductor's plugin, loaded only into the sessions it starts: the mod (the console's band, pane and commands, and each session's report to the daemon), the console skill, the reference work skill and the verification-skill pair.",
|
|
5
|
-
"author": {
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Detergent Software"
|
|
7
|
+
},
|
|
6
8
|
"types": "./types/index.d.ts"
|
|
7
9
|
}
|
package/plugin/hooks/model.ts
CHANGED
|
@@ -655,15 +655,30 @@ export function approvedPlanItem(inbox: readonly ConductorInboxItem[], ref: stri
|
|
|
655
655
|
// of a daemon kept in CONDUCTOR_HOME would otherwise report to ~/.conductor (ATK-525). The package's
|
|
656
656
|
// own plugin folder (a `claude plugin test`, a `--plugin-dir` on a checkout) has none: its CLI sits
|
|
657
657
|
// beside it, and its home is the environment's (homeOf).
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
658
|
+
// The version is the build's whose CLI the mod runs, which the pane shows (ATK-882): conductor.json
|
|
659
|
+
// names it in the daemon's copy, and elsewhere the folder's .claude-plugin/plugin.json does, which
|
|
660
|
+
// the release keeps equal to the package's. Null when neither says.
|
|
661
|
+
export function daemonOf(
|
|
662
|
+
root: string,
|
|
663
|
+
conductorJson: null | string,
|
|
664
|
+
manifestJson: null | string = null,
|
|
665
|
+
): { cli: string; home: null | string; version: null | string } {
|
|
666
|
+
const parsed = (json: null | string): Record<string, unknown> => {
|
|
667
|
+
try {
|
|
668
|
+
const value: unknown = json === null ? null : JSON.parse(json);
|
|
669
|
+
return isRecord(value) ? value : {};
|
|
670
|
+
} catch {
|
|
671
|
+
// Not JSON: as if there were none.
|
|
672
|
+
return {};
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
const named = parsed(conductorJson);
|
|
665
676
|
const text = (value: unknown): null | string => (typeof value === 'string' && value !== '' ? value : null);
|
|
666
|
-
return {
|
|
677
|
+
return {
|
|
678
|
+
cli: text(named.cli) ?? `${root}/../build/cli.js`,
|
|
679
|
+
home: text(named.home),
|
|
680
|
+
version: text(named.version) ?? text(parsed(manifestJson).version),
|
|
681
|
+
};
|
|
667
682
|
}
|
|
668
683
|
|
|
669
684
|
// The port and token files' text as an endpoint; null unless both are whole.
|
|
@@ -784,15 +799,20 @@ export function runText(
|
|
|
784
799
|
}
|
|
785
800
|
|
|
786
801
|
// GET /fleet, GET /inbox and GET /notify as one snapshot; `error` set when the daemon didn't answer
|
|
787
|
-
// the first two. A daemon older than the notifier answers /notify 404, which leaves `notify` null
|
|
802
|
+
// the first two. A daemon older than the notifier answers /notify 404, which leaves `notify` null,
|
|
803
|
+
// and one older than ATK-882 sends no version (or an empty one), which leaves `daemonVersion` null.
|
|
804
|
+
// `modVersion` is the mod's to fill in (register.tsx, refresh).
|
|
788
805
|
export function snapshotOf(fleet: ApiAnswer, inbox: ApiAnswer, readAt: number, notify?: ApiAnswer): ConductorSnapshot {
|
|
789
806
|
const workers = fleet.status === 200 ? parseFleet(fleet.body) : null;
|
|
790
|
-
if (workers === null) return
|
|
807
|
+
if (workers === null) return unanswered(errorOf(fleet), readAt);
|
|
791
808
|
const items = inbox.status === 200 ? parseInbox(inbox.body) : null;
|
|
792
|
-
if (items === null) return
|
|
809
|
+
if (items === null) return unanswered(errorOf(inbox), readAt);
|
|
810
|
+
const version = isRecord(fleet.body) ? fleet.body.version : null;
|
|
793
811
|
return {
|
|
812
|
+
daemonVersion: typeof version === 'string' && version !== '' ? version : null,
|
|
794
813
|
error: null,
|
|
795
814
|
inbox: items,
|
|
815
|
+
modVersion: null,
|
|
796
816
|
notify: notify?.status === 200 ? parseNotify(notify.body) : null,
|
|
797
817
|
readAt,
|
|
798
818
|
workers,
|
|
@@ -813,6 +833,20 @@ export function triageText(item: string, action: 'hold' | 'queue', answer: ApiAn
|
|
|
813
833
|
return `${action === 'queue' ? 'queued' : 'held'} ${key}: its inbox item resolves on the next tick, and ${then}`;
|
|
814
834
|
}
|
|
815
835
|
|
|
836
|
+
// The snapshot when no daemon answered: the error, and nothing else.
|
|
837
|
+
export function unanswered(error: string, readAt: number): ConductorSnapshot {
|
|
838
|
+
return { daemonVersion: null, error, inbox: [], modVersion: null, notify: null, readAt, workers: [] };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// The pane header's versions (ATK-882): the mod's alone when the daemon runs the same build; else
|
|
842
|
+
// `mod <a>` and `daemon <b>` (`daemon ?` from a daemon too old to say), the daemon's to highlight.
|
|
843
|
+
// Null when the mod doesn't know its own version.
|
|
844
|
+
export function versionParts(mod: null | string, daemon: null | string): null | { daemon: null | string; mod: string } {
|
|
845
|
+
if (mod === null) return null;
|
|
846
|
+
if (daemon === mod) return { daemon: null, mod };
|
|
847
|
+
return { daemon: `daemon ${daemon ?? '?'}`, mod: `mod ${mod}` };
|
|
848
|
+
}
|
|
849
|
+
|
|
816
850
|
const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
817
851
|
|
|
818
852
|
// `/notify` with no profile: the profile, quiet hours, Remote Control.
|
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
takesAnswer,
|
|
54
54
|
tokens,
|
|
55
55
|
triageText,
|
|
56
|
+
unanswered,
|
|
57
|
+
versionParts,
|
|
56
58
|
WARM_AFTER_MS,
|
|
57
59
|
WARM_PROMPT,
|
|
58
60
|
whyText,
|
|
@@ -238,18 +240,10 @@ async function copy(
|
|
|
238
240
|
$.ui.toast(copied.isCopied ? `copied: ${text}` : `not copied: ${text}`);
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
// The conductor this plugin folder belongs to (model.ts, daemonOf).
|
|
242
|
-
// changes only with the folder, and that reloads this module.
|
|
243
|
+
// The conductor this plugin folder belongs to (model.ts, daemonOf). Its CLI and home are read once
|
|
244
|
+
// per load: the file changes only with the folder, and that reloads this module.
|
|
243
245
|
async function daemon($: EngineInterface): Promise<ReturnType<typeof daemonOf>> {
|
|
244
|
-
named ??= (
|
|
245
|
-
let text: null | string = null;
|
|
246
|
-
try {
|
|
247
|
-
text = await $.fs.read(`${$.plugin.root}/conductor.json`);
|
|
248
|
-
} catch {
|
|
249
|
-
// The package's own plugin folder has none.
|
|
250
|
-
}
|
|
251
|
-
return daemonOf($.plugin.root, text);
|
|
252
|
-
})();
|
|
246
|
+
named ??= pluginFolder($);
|
|
253
247
|
return named;
|
|
254
248
|
}
|
|
255
249
|
|
|
@@ -359,6 +353,13 @@ async function markSkillLoaded($: EngineInterface): Promise<void> {
|
|
|
359
353
|
await update($, cleared, () => null);
|
|
360
354
|
}
|
|
361
355
|
|
|
356
|
+
// The mod's version, read again on each refresh (ATK-882): a refresh of the daemon's copy that
|
|
357
|
+
// changes only conductor.json and plugin.json may not reload this module, and a version the load
|
|
358
|
+
// kept would show as a mismatch with a daemon on the new build.
|
|
359
|
+
async function modVersion($: EngineInterface): Promise<null | string> {
|
|
360
|
+
return (await pluginFolder($)).version;
|
|
361
|
+
}
|
|
362
|
+
|
|
362
363
|
// The console has been idle WARM_AFTER_MS: keep its cache warm with a ping, or clear it (ATK-386).
|
|
363
364
|
// Either is told to the daemon, which logs it as a `console.cache` event: a fork is no turn of the
|
|
364
365
|
// transcript, so the event is the only place its cost shows.
|
|
@@ -420,7 +421,8 @@ async function onLine($: EngineInterface, text: string): Promise<void> {
|
|
|
420
421
|
switch (act.kind) {
|
|
421
422
|
case 'disconnected': {
|
|
422
423
|
const readAt = await $.clock.now();
|
|
423
|
-
|
|
424
|
+
const mod = await modVersion($);
|
|
425
|
+
await update($, snapshot, () => ({ ...unanswered(NOT_RUNNING, readAt), modVersion: mod }));
|
|
424
426
|
return;
|
|
425
427
|
}
|
|
426
428
|
case 'escalate':
|
|
@@ -478,6 +480,23 @@ async function override($: EngineInterface, command: 'focus' | 'pause' | 'unpaus
|
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
483
|
+
// The plugin folder's conductor.json and, for the package's own folder, which has none, its
|
|
484
|
+
// manifest's version.
|
|
485
|
+
async function pluginFolder($: EngineInterface): Promise<ReturnType<typeof daemonOf>> {
|
|
486
|
+
const readOrNull = async (path: string): Promise<null | string> => {
|
|
487
|
+
try {
|
|
488
|
+
return await $.fs.read(path);
|
|
489
|
+
} catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
const [conductorJson, manifestJson] = await Promise.all([
|
|
494
|
+
readOrNull(`${$.plugin.root}/conductor.json`),
|
|
495
|
+
readOrNull(`${$.plugin.root}/.claude-plugin/plugin.json`),
|
|
496
|
+
]);
|
|
497
|
+
return daemonOf($.plugin.root, conductorJson, manifestJson);
|
|
498
|
+
}
|
|
499
|
+
|
|
481
500
|
// Pushes to the phone through PushNotification, tells the daemon what came of it whatever it was
|
|
482
501
|
// (`conduct notify` shows it), and says once in the console when pushes stop reaching the phone
|
|
483
502
|
// (ATK-255): Remote Control off, pushes off in the config, or the call failing.
|
|
@@ -507,9 +526,10 @@ async function push($: EngineInterface, message: string): Promise<void> {
|
|
|
507
526
|
}
|
|
508
527
|
}
|
|
509
528
|
|
|
510
|
-
// Reads /fleet, /inbox and /notify into the state the drawings read.
|
|
529
|
+
// Reads /fleet, /inbox and /notify, and the mod's own version, into the state the drawings read.
|
|
511
530
|
async function refresh($: EngineInterface): Promise<ConductorSnapshot> {
|
|
512
531
|
const readAt = await $.clock.now();
|
|
532
|
+
const mod = modVersion($);
|
|
513
533
|
let next: ConductorSnapshot;
|
|
514
534
|
try {
|
|
515
535
|
const at = await endpoint($);
|
|
@@ -520,14 +540,9 @@ async function refresh($: EngineInterface): Promise<ConductorSnapshot> {
|
|
|
520
540
|
]);
|
|
521
541
|
next = snapshotOf(fleet, inbox, readAt, notify);
|
|
522
542
|
} catch (error) {
|
|
523
|
-
next =
|
|
524
|
-
error: error instanceof Error ? error.message : String(error),
|
|
525
|
-
inbox: [],
|
|
526
|
-
notify: null,
|
|
527
|
-
readAt,
|
|
528
|
-
workers: [],
|
|
529
|
-
};
|
|
543
|
+
next = unanswered(error instanceof Error ? error.message : String(error), readAt);
|
|
530
544
|
}
|
|
545
|
+
next = { ...next, modVersion: await mod };
|
|
531
546
|
await update($, snapshot, () => next);
|
|
532
547
|
return next;
|
|
533
548
|
}
|
|
@@ -971,22 +986,39 @@ export const register: Register = (on) => {
|
|
|
971
986
|
<Link href={href} key={key} label={`open ${label}`} />
|
|
972
987
|
);
|
|
973
988
|
if (snap === null) return <Text dimColor>Reading the conductor…</Text>;
|
|
989
|
+
// The mod's version after the counts, so a narrow pane cuts it before them; a daemon on another
|
|
990
|
+
// build gets a line of its own, highlighted, so nothing cuts it (ATK-882). With no daemon
|
|
991
|
+
// answering, the mod's alone.
|
|
974
992
|
if (snap.error !== null) {
|
|
975
993
|
return (
|
|
976
994
|
<Box flexDirection='column'>
|
|
977
|
-
<Text bold>
|
|
995
|
+
<Text bold wrap='truncate'>
|
|
996
|
+
{TITLE}
|
|
997
|
+
{snap.modVersion !== null && <Text dimColor>{` ${snap.modVersion}`}</Text>}
|
|
998
|
+
</Text>
|
|
978
999
|
<Text>{snap.error}</Text>
|
|
979
1000
|
</Box>
|
|
980
1001
|
);
|
|
981
1002
|
}
|
|
1003
|
+
const versions = versionParts(snap.modVersion, snap.daemonVersion);
|
|
1004
|
+
const stale = versions?.daemon ?? null;
|
|
982
1005
|
const columns = fleetColumns(snap.workers);
|
|
983
1006
|
return (
|
|
984
1007
|
<Box flexDirection='column'>
|
|
985
1008
|
<Text bold wrap='truncate'>
|
|
986
1009
|
{TITLE}
|
|
987
1010
|
{' '}
|
|
988
|
-
<Text dimColor>
|
|
1011
|
+
<Text dimColor>
|
|
1012
|
+
{statusText(snap)}
|
|
1013
|
+
{versions !== null && stale === null ? ` · ${versions.mod}` : ''}
|
|
1014
|
+
</Text>
|
|
989
1015
|
</Text>
|
|
1016
|
+
{versions !== null && stale !== null && (
|
|
1017
|
+
<Text wrap='truncate'>
|
|
1018
|
+
<Text dimColor>{`${versions.mod} · `}</Text>
|
|
1019
|
+
<Text color='warning'>{stale}</Text>
|
|
1020
|
+
</Text>
|
|
1021
|
+
)}
|
|
990
1022
|
{snap.notify !== null && (
|
|
991
1023
|
<Box columnGap={2} flexWrap='wrap'>
|
|
992
1024
|
{e.surface !== 'mobile' && 'Select' in ui ? (
|
package/plugin/types/index.d.ts
CHANGED
|
@@ -28,10 +28,14 @@ export type ConductorNotify = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
// What the mod last read from the daemon; `error` set (and the lists empty) when none answered.
|
|
31
|
-
// `notify` is null from a daemon older than the notifier
|
|
31
|
+
// `notify` is null from a daemon older than the notifier, `daemonVersion` from one older than ATK-882
|
|
32
|
+
// (GET /fleet's `version`). `modVersion` is the mod's own, read from its plugin folder with each
|
|
33
|
+
// refresh (ATK-882); null when the folder doesn't say.
|
|
32
34
|
export type ConductorSnapshot = {
|
|
35
|
+
daemonVersion: null | string;
|
|
33
36
|
error: null | string;
|
|
34
37
|
inbox: ConductorInboxItem[];
|
|
38
|
+
modVersion: null | string;
|
|
35
39
|
notify: ConductorNotify | null;
|
|
36
40
|
readAt: number;
|
|
37
41
|
workers: ConductorWorker[];
|