external-subagent 0.6.0 → 0.6.2
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/cli/commands/plugin.mjs +19 -2
- package/cli/constants.mjs +1 -1
- package/cli/install/dsh-registry.mjs +143 -0
- package/cli/install/dsh.mjs +118 -36
- package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
- package/npm/native/darwin-arm64/external-subagentd +0 -0
- package/npm/native/darwin-arm64/payload.json +4 -4
- package/npm/native/linux-x64/external-subagent-mcp +0 -0
- package/npm/native/linux-x64/external-subagentd +0 -0
- package/npm/native/linux-x64/payload.json +5 -5
- package/package.json +1 -1
package/cli/commands/plugin.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { CliError } from '../errors.mjs';
|
|
2
2
|
import { installMcp, installPlugin, uninstallPlugin } from '../install/codex.mjs';
|
|
3
3
|
import { installZcodePlugin, uninstallZcodePlugin } from '../install/zcode.mjs';
|
|
4
|
-
import { installDshPlugin, uninstallDshPlugin } from '../install/dsh.mjs';
|
|
4
|
+
import { installDshPlugin, uninstallDshPlugin, dshHomeFor } from '../install/dsh.mjs';
|
|
5
5
|
import { registerCodexHome, unregisterCodexHome } from '../install/reconcile.mjs';
|
|
6
|
+
import { registerDshBinding, unregisterDshBinding } from '../install/dsh-registry.mjs';
|
|
6
7
|
import { verifyPayload } from '../install/payload.mjs';
|
|
7
8
|
|
|
8
9
|
// Command surface for the managed host bindings. `install-plugin` takes an
|
|
@@ -68,7 +69,23 @@ export function pluginCommand(paths, args) {
|
|
|
68
69
|
// error at the CLI parsing boundary with the other flag-combination
|
|
69
70
|
// errors instead of deep inside the installer.
|
|
70
71
|
if (!options.profile) throw new CliError('INVALID_ARGUMENT', 'the dsh host requires --profile <name>', 2);
|
|
71
|
-
|
|
72
|
+
const result = options.uninstall ? uninstallDshPlugin(paths, installerOptions) : installDshPlugin(paths, installerOptions);
|
|
73
|
+
// Registry bookkeeping survives a dsh-desktop rewrite that drops the
|
|
74
|
+
// managed block: a registered profile is RESTORED by reconcile, an
|
|
75
|
+
// unregistered one is forgotten. Dry-runs never touch the registry.
|
|
76
|
+
if (!options.dry_run) {
|
|
77
|
+
const dshHome = dshHomeFor(installerOptions, paths);
|
|
78
|
+
if (options.uninstall) {
|
|
79
|
+
if (result.uninstalled) {
|
|
80
|
+
const claim = unregisterDshBinding(paths, dshHome, options.profile);
|
|
81
|
+
result.claim_released = claim.unregistered;
|
|
82
|
+
}
|
|
83
|
+
} else if (result.installed) {
|
|
84
|
+
const claim = registerDshBinding(paths, dshHome, options.profile, { status: 'claimed' });
|
|
85
|
+
result.claim = { registered: claim.registered, deduplicated: claim.deduplicated, bindings: claim.bindings };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
72
89
|
}
|
|
73
90
|
if (options.profile) throw new CliError('INVALID_ARGUMENT', '--profile applies to the dsh host only', 2);
|
|
74
91
|
if (options.dshHome) throw new CliError('INVALID_ARGUMENT', '--dsh-home applies to the dsh host only', 2);
|
package/cli/constants.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const IS_DEBUG_VARIANT = process.env.EXTERNAL_SUBAGENT_VARIANT === 'debug';
|
|
|
12
12
|
|
|
13
13
|
export const PRODUCT_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug' : 'external-subagent';
|
|
14
14
|
export const PRODUCT_ID = IS_DEBUG_VARIANT ? 'external_subagent_debug' : 'external_subagent';
|
|
15
|
-
export const VERSION = '0.6.
|
|
15
|
+
export const VERSION = '0.6.2';
|
|
16
16
|
export const LAUNCH_AGENT_LABEL = IS_DEBUG_VARIANT ? 'com.external-subagent-debug.daemon' : 'com.external-subagent.daemon';
|
|
17
17
|
export const DAEMON_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debugd' : 'external-subagentd';
|
|
18
18
|
export const MCP_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug-mcp' : 'external-subagent-mcp';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CliError } from '../errors.mjs';
|
|
4
|
+
import { atomicWrite, jsonBytes, readOptional } from '../fs-atomic.mjs';
|
|
5
|
+
|
|
6
|
+
// D28: the persistent registry of bound DSH profiles. The dsh binding is
|
|
7
|
+
// derivable from the patch sentinel in daily operation, but the sentinel
|
|
8
|
+
// itself lives inside a file the dsh desktop rewrites wholesale on upgrade
|
|
9
|
+
// (OBSERVED 2026-10-28, dsh 0.2.1-rc.3: both bound profiles lost the
|
|
10
|
+
// managed block AND the staged skill, while the product's sentinel-only
|
|
11
|
+
// reconcile reported bound:false and silently forgot the machines ever
|
|
12
|
+
// bound). This registry remembers what the user explicitly bound so
|
|
13
|
+
// reconcile can RESTORE a dropped binding instead of merely refreshing
|
|
14
|
+
// surviving ones. It follows the codex-homes D08 model: atomic writes,
|
|
15
|
+
// per-entry status bookkeeping, and corruption recovery that preserves the
|
|
16
|
+
// bad bytes as evidence and continues with an empty table rather than
|
|
17
|
+
// aborting the caller.
|
|
18
|
+
|
|
19
|
+
const REGISTRY_SCHEMA_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
export function dshBindingsRegistryPath(paths) {
|
|
22
|
+
return path.join(paths.data, 'dsh-bindings.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function emptyRegistry() {
|
|
26
|
+
return { schema_version: REGISTRY_SCHEMA_VERSION, product: 'external-subagent', bindings: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Resolve a home to its canonical registry key. A plain realpath fails on
|
|
30
|
+
// a not-yet-created directory (a dsh home registered before its first
|
|
31
|
+
// boot), and on macOS the /var -> /private/var symlink then yields the
|
|
32
|
+
// UNRESOLVED prefix — so the same home would register and reconcile under
|
|
33
|
+
// two different strings depending on whether it existed at the time.
|
|
34
|
+
// Resolve the deepest EXISTING ancestor and append the remainder instead:
|
|
35
|
+
// the key is stable across creation, removal, and symlinked tmpdirs.
|
|
36
|
+
export function canonicalHome(home) {
|
|
37
|
+
if (typeof home !== 'string' || home.length === 0) throw new CliError('INVALID_ARGUMENT', 'dsh home must be a non-empty string', 2);
|
|
38
|
+
const absolute = path.resolve(home);
|
|
39
|
+
try { return fs.realpathSync(absolute); } catch { /* fall through to ancestor resolution */ }
|
|
40
|
+
const parent = path.dirname(absolute);
|
|
41
|
+
if (parent === absolute) return absolute;
|
|
42
|
+
return path.join(canonicalHome(parent), path.basename(absolute));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveHome(home) {
|
|
46
|
+
return canonicalHome(home);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeEntry(entry) {
|
|
50
|
+
if (!entry || typeof entry.home !== 'string' || typeof entry.profile !== 'string') {
|
|
51
|
+
throw new CliError('DSH_BINDINGS_REGISTRY_INVALID', 'registry entry is missing its home or profile path');
|
|
52
|
+
}
|
|
53
|
+
if (/[\\/]/u.test(entry.profile)) {
|
|
54
|
+
throw new CliError('DSH_BINDINGS_REGISTRY_INVALID', 'registry entry profile must not contain path separators');
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
home: entry.home,
|
|
58
|
+
profile: entry.profile,
|
|
59
|
+
claimed_at_ms: Number.isInteger(entry.claimed_at_ms) ? entry.claimed_at_ms : null,
|
|
60
|
+
last_sync_ms: Number.isInteger(entry.last_sync_ms) ? entry.last_sync_ms : null,
|
|
61
|
+
last_status: typeof entry.last_status === 'string' ? entry.last_status : null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseRegistry(bytes, file) {
|
|
66
|
+
const doc = JSON.parse(bytes.toString('utf8'));
|
|
67
|
+
if (doc?.schema_version !== REGISTRY_SCHEMA_VERSION || doc?.product !== 'external-subagent' || !Array.isArray(doc.bindings)) {
|
|
68
|
+
throw new CliError('DSH_BINDINGS_REGISTRY_INVALID', 'registry identity or bindings table is invalid');
|
|
69
|
+
}
|
|
70
|
+
const registry = emptyRegistry();
|
|
71
|
+
registry.bindings = doc.bindings.map(normalizeEntry);
|
|
72
|
+
return registry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Corrupted registry: preserve the bytes for inspection, atomically stage a
|
|
76
|
+
// fresh registry, and report a recoverable error — never abort the caller.
|
|
77
|
+
export function loadDshBindings(paths) {
|
|
78
|
+
const file = dshBindingsRegistryPath(paths);
|
|
79
|
+
const bytes = readOptional(file);
|
|
80
|
+
if (bytes === null) return { registry: emptyRegistry(), recovery: null };
|
|
81
|
+
try {
|
|
82
|
+
return { registry: parseRegistry(bytes, file), recovery: null };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const backup = `${file}.corrupt-${Date.now()}`;
|
|
85
|
+
try { fs.renameSync(file, backup); } catch (renameError) {
|
|
86
|
+
throw new CliError('DSH_BINDINGS_REGISTRY_CORRUPT', `registry is corrupted and could not be preserved: ${renameError.message}`);
|
|
87
|
+
}
|
|
88
|
+
atomicWrite(file, jsonBytes(emptyRegistry()));
|
|
89
|
+
return {
|
|
90
|
+
registry: emptyRegistry(),
|
|
91
|
+
recovery: { status: 'recovered', code: 'DSH_BINDINGS_REGISTRY_CORRUPT', backup, error: error.message },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function persistRegistry(paths, registry) {
|
|
97
|
+
atomicWrite(dshBindingsRegistryPath(paths), jsonBytes(registry));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function registerDshBinding(paths, dshHome, profile, meta = {}) {
|
|
101
|
+
const { registry, recovery } = loadDshBindings(paths);
|
|
102
|
+
const home = resolveHome(dshHome);
|
|
103
|
+
const existing = registry.bindings.find((entry) => entry.home === home && entry.profile === profile);
|
|
104
|
+
let entry;
|
|
105
|
+
if (existing) {
|
|
106
|
+
entry = existing;
|
|
107
|
+
if (typeof meta.status === 'string') entry.last_status = meta.status;
|
|
108
|
+
} else {
|
|
109
|
+
entry = {
|
|
110
|
+
home,
|
|
111
|
+
profile,
|
|
112
|
+
claimed_at_ms: Date.now(),
|
|
113
|
+
last_sync_ms: null,
|
|
114
|
+
last_status: typeof meta.status === 'string' ? meta.status : 'claimed',
|
|
115
|
+
};
|
|
116
|
+
registry.bindings.push(entry);
|
|
117
|
+
}
|
|
118
|
+
persistRegistry(paths, registry);
|
|
119
|
+
return { registered: true, home, profile, deduplicated: Boolean(existing), bindings: registry.bindings.length, recovery };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function unregisterDshBinding(paths, dshHome, profile) {
|
|
123
|
+
const { registry } = loadDshBindings(paths);
|
|
124
|
+
const home = resolveHome(dshHome);
|
|
125
|
+
const remaining = registry.bindings.filter((entry) => !(entry.home === home && entry.profile === profile));
|
|
126
|
+
const unregistered = remaining.length !== registry.bindings.length;
|
|
127
|
+
registry.bindings = remaining;
|
|
128
|
+
if (unregistered) persistRegistry(paths, registry);
|
|
129
|
+
return { unregistered, home, profile, bindings: registry.bindings.length };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Product uninstall: no profile remains claimed. The registry file itself
|
|
133
|
+
// stays inside retained product data, matching the codex-homes posture.
|
|
134
|
+
export function unregisterAllDshBindings(paths) {
|
|
135
|
+
const { registry } = loadDshBindings(paths);
|
|
136
|
+
const released = registry.bindings.map((entry) => ({ home: entry.home, profile: entry.profile }));
|
|
137
|
+
const file = dshBindingsRegistryPath(paths);
|
|
138
|
+
if (released.length > 0 || fs.existsSync(file)) {
|
|
139
|
+
registry.bindings = [];
|
|
140
|
+
persistRegistry(paths, registry);
|
|
141
|
+
}
|
|
142
|
+
return { unregistered: released.length, bindings: released };
|
|
143
|
+
}
|
package/cli/install/dsh.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { atomicWrite, readOptional, restoreOptional } from '../fs-atomic.mjs';
|
|
|
7
7
|
import { MCP_BIN_NAME } from '../constants.mjs';
|
|
8
8
|
import { nativeBinary, pluginSourceRoot, PLUGIN_NAME } from './layout.mjs';
|
|
9
9
|
import { guardedRestores } from './plugin-stage.mjs';
|
|
10
|
+
import { loadDshBindings, persistRegistry as persistDshRegistry, dshBindingsRegistryPath, canonicalHome } from './dsh-registry.mjs';
|
|
10
11
|
|
|
11
12
|
// Managed DSH host binding. DSH boots profiles as an ordered stack of
|
|
12
13
|
// plugin-bundle patch layers under the user's own overrides; the user layer
|
|
@@ -17,9 +18,11 @@ import { guardedRestores } from './plugin-stage.mjs';
|
|
|
17
18
|
// into the user skill root (<dshHome>/skills), which dsh-skill-filesystem
|
|
18
19
|
// scans by default (rank 400; OBSERVED against dsh 0.2.0-rc.2). It never
|
|
19
20
|
// touches the profile's package.json, bundles, node_modules, or any dsh
|
|
20
|
-
// market/plugin-install state.
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
// market/plugin-install state. Claims also live in the persistent
|
|
22
|
+
// dsh-bindings.json registry (dsh-registry.mjs): the dsh desktop rewrites
|
|
23
|
+
// cordis.patch.yml wholesale on upgrade and DROPS the managed block
|
|
24
|
+
// (OBSERVED 2026-10-28), so reconcile pairs the sentinel scan with the
|
|
25
|
+
// registry to RESTORE a dropped binding instead of forgetting it.
|
|
23
26
|
|
|
24
27
|
// The MCP client plugin is resolved by the dsh loader from the CLI's own
|
|
25
28
|
// install tree when the bare package name is used (OBSERVED: a fresh
|
|
@@ -546,61 +549,135 @@ export function uninstallDshPlugin(paths, options = {}) {
|
|
|
546
549
|
};
|
|
547
550
|
}
|
|
548
551
|
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
// the
|
|
552
|
+
// Reconcile for update flows: the persistent registry (dsh-bindings.json)
|
|
553
|
+
// UNION the sentinel scan. The scan alone follows the zcode model — detect
|
|
554
|
+
// rather than remember — but the dsh desktop rewrites cordis.patch.yml
|
|
555
|
+
// wholesale on upgrade and DROPS the managed block (OBSERVED 2026-10-28),
|
|
556
|
+
// after which a sentinel-only reconcile silently forgets the profile was
|
|
557
|
+
// ever bound. Registry entries therefore drive a RESTORE: a registered
|
|
558
|
+
// profile whose patch lost the sentinel is re-bound (status `rebound`),
|
|
559
|
+
// and one whose profile directory itself vanished is reported
|
|
560
|
+
// (`missing_profile_dir`) without failing the reconcile. Profiles bearing
|
|
561
|
+
// the sentinel but absent from the registry (bound by an older build, or
|
|
562
|
+
// by hand) are adopted into the registry so future drops restore too.
|
|
553
563
|
export function reconcileDshBindings(paths, options = {}) {
|
|
554
564
|
const dshHome = dshHomeFor(options, paths);
|
|
565
|
+
// Registry entries store realpath-resolved homes (registerDshBinding
|
|
566
|
+
// resolves); compare and dedupe against the SAME resolved form, or a
|
|
567
|
+
// symlinked dshHome (/var -> /private/var on macOS tmpdirs) would never
|
|
568
|
+
// match its own registry claims.
|
|
569
|
+
const resolvedHome = canonicalHome(dshHome);
|
|
555
570
|
const profilesRoot = path.join(dshHome, 'profiles');
|
|
556
|
-
|
|
571
|
+
const { registry, recovery } = loadDshBindings(paths);
|
|
572
|
+
const results = [];
|
|
573
|
+
const visited = new Set();
|
|
574
|
+
let directories = [];
|
|
575
|
+
let profilesRootAbsent = false;
|
|
557
576
|
try {
|
|
558
|
-
|
|
577
|
+
directories = fs.readdirSync(profilesRoot, { withFileTypes: true })
|
|
578
|
+
.filter((entry) => entry.isDirectory())
|
|
579
|
+
.map((entry) => entry.name);
|
|
559
580
|
} catch (error) {
|
|
560
|
-
if (error?.code === 'ENOENT')
|
|
561
|
-
|
|
581
|
+
if (error?.code === 'ENOENT') profilesRootAbsent = true;
|
|
582
|
+
else {
|
|
583
|
+
return { bound: false, profiles: [], error: { code: 'DSH_PROFILES_UNREADABLE', message: error.message }, ...(recovery ? { recovery } : {}) };
|
|
584
|
+
}
|
|
562
585
|
}
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
586
|
+
const directorySet = new Set(directories);
|
|
587
|
+
// Sentinel scan: refresh every surviving binding and adopt unregistered ones.
|
|
588
|
+
for (const name of directories) {
|
|
589
|
+
if (DSH_FORBIDDEN_PROFILES.includes(name)) continue;
|
|
590
|
+
const patchFile = path.join(profilesRoot, name, 'cordis.patch.yml');
|
|
567
591
|
const bytes = readOptional(patchFile);
|
|
568
592
|
if (bytes === null || !bytes.toString('utf8').includes(SENTINEL)) continue;
|
|
593
|
+
visited.add(`${resolvedHome}\u0000${name}`);
|
|
594
|
+
try {
|
|
595
|
+
installDshPlugin(paths, { ...options, profile: name });
|
|
596
|
+
upsertRegistryEntry(registry, resolvedHome, name, 'updated');
|
|
597
|
+
results.push({ profile: name, status: 'updated', idempotent: true });
|
|
598
|
+
} catch (error) {
|
|
599
|
+
results.push({ profile: name, status: 'failed', error: { code: error.code || 'DSH_BINDING_FAILED', message: error.message } });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Registry pass: restore registered profiles whose sentinel was dropped.
|
|
603
|
+
for (const entry of registry.bindings) {
|
|
604
|
+
if (entry.home !== resolvedHome) continue;
|
|
605
|
+
if (visited.has(`${entry.home}\u0000${entry.profile}`)) continue;
|
|
606
|
+
if (DSH_FORBIDDEN_PROFILES.includes(entry.profile)) continue;
|
|
607
|
+
if (!directorySet.has(entry.profile)) {
|
|
608
|
+
entry.last_status = 'missing_profile_dir';
|
|
609
|
+
results.push({ profile: entry.profile, status: 'missing_profile_dir' });
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
569
612
|
try {
|
|
570
|
-
|
|
571
|
-
|
|
613
|
+
installDshPlugin(paths, { ...options, dshHome: entry.home, profile: entry.profile });
|
|
614
|
+
entry.last_status = 'rebound';
|
|
615
|
+
entry.last_sync_ms = Date.now();
|
|
616
|
+
results.push({ profile: entry.profile, status: 'rebound' });
|
|
572
617
|
} catch (error) {
|
|
573
|
-
|
|
618
|
+
entry.last_status = 'failed';
|
|
619
|
+
results.push({ profile: entry.profile, status: 'failed', error: { code: error.code || 'DSH_BINDING_FAILED', message: error.message } });
|
|
574
620
|
}
|
|
575
621
|
}
|
|
576
|
-
|
|
622
|
+
if (results.length > 0 || fs.existsSync(dshBindingsRegistryPath(paths))) {
|
|
623
|
+
persistDshRegistry(paths, registry);
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
bound: results.length > 0,
|
|
627
|
+
profiles: results,
|
|
628
|
+
...(profilesRootAbsent ? { note: 'no dsh profiles directory' } : {}),
|
|
629
|
+
...(recovery ? { recovery } : {}),
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Insert or refresh a registry entry in place (the loaded registry object is
|
|
634
|
+
// the caller's working copy; persisting happens once at the end).
|
|
635
|
+
function upsertRegistryEntry(registry, dshHome, profile, status) {
|
|
636
|
+
const home = canonicalHome(dshHome);
|
|
637
|
+
const existing = registry.bindings.find((entry) => entry.home === home && entry.profile === profile);
|
|
638
|
+
if (existing) {
|
|
639
|
+
existing.last_status = status;
|
|
640
|
+
existing.last_sync_ms = Date.now();
|
|
641
|
+
} else {
|
|
642
|
+
registry.bindings.push({
|
|
643
|
+
home,
|
|
644
|
+
profile,
|
|
645
|
+
claimed_at_ms: Date.now(),
|
|
646
|
+
last_sync_ms: Date.now(),
|
|
647
|
+
last_status: status,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
577
650
|
}
|
|
578
651
|
|
|
579
652
|
// Product uninstall: release every detected binding at once. Detection is
|
|
580
|
-
// the same sentinel scan
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
653
|
+
// the same sentinel scan plus the registry, so a desktop-rewritten patch
|
|
654
|
+
// (sentinel dropped) is still unregistered even though its block is gone.
|
|
655
|
+
// Each profile is uninstalled independently (a failure on one never skips
|
|
656
|
+
// the rest, matching the per-home codex release posture), and the shared
|
|
657
|
+
// staged skill is removed when no bound profile remains.
|
|
584
658
|
export function uninstallAllDshBindings(paths, options = {}) {
|
|
585
659
|
const dshHome = dshHomeFor(options, paths);
|
|
586
660
|
const profilesRoot = path.join(dshHome, 'profiles');
|
|
587
|
-
let entries;
|
|
661
|
+
let entries = [];
|
|
588
662
|
try {
|
|
589
|
-
entries = fs.readdirSync(profilesRoot, { withFileTypes: true })
|
|
590
|
-
|
|
591
|
-
|
|
663
|
+
entries = fs.readdirSync(profilesRoot, { withFileTypes: true })
|
|
664
|
+
.filter((entry) => entry.isDirectory())
|
|
665
|
+
.map((entry) => entry.name);
|
|
666
|
+
} catch { /* registry pass below still releases claims */ }
|
|
667
|
+
const names = new Set(entries);
|
|
668
|
+
const resolvedHome = canonicalHome(dshHome);
|
|
669
|
+
const { registry } = loadDshBindings(paths);
|
|
670
|
+
for (const entry of registry.bindings) {
|
|
671
|
+
if (entry.home === resolvedHome) names.add(entry.profile);
|
|
592
672
|
}
|
|
593
673
|
const results = [];
|
|
594
|
-
for (const
|
|
595
|
-
if (DSH_FORBIDDEN_PROFILES.includes(
|
|
596
|
-
const patchFile = path.join(profilesRoot, entry.name, 'cordis.patch.yml');
|
|
597
|
-
const bytes = readOptional(patchFile);
|
|
598
|
-
if (bytes === null || !bytes.toString('utf8').includes(SENTINEL)) continue;
|
|
674
|
+
for (const name of names) {
|
|
675
|
+
if (DSH_FORBIDDEN_PROFILES.includes(name)) continue;
|
|
599
676
|
try {
|
|
600
|
-
const removed = uninstallDshPlugin(paths, { ...options, profile:
|
|
601
|
-
results.push({ profile:
|
|
677
|
+
const removed = uninstallDshPlugin(paths, { ...options, profile: name });
|
|
678
|
+
results.push({ profile: name, status: removed.uninstalled ? 'released' : 'absent' });
|
|
602
679
|
} catch (error) {
|
|
603
|
-
results.push({ profile:
|
|
680
|
+
results.push({ profile: name, status: 'failed', error: { code: error.code || 'DSH_BINDING_RELEASE_FAILED', message: error.message } });
|
|
604
681
|
}
|
|
605
682
|
}
|
|
606
683
|
// The skill is shared across profiles; remove it once no binding remains.
|
|
@@ -609,5 +686,10 @@ export function uninstallAllDshBindings(paths, options = {}) {
|
|
|
609
686
|
unstageSkill(dshHome, options.source || pluginSourceRoot());
|
|
610
687
|
} catch { /* reported per-profile above; a stuck skill never fails the release */ }
|
|
611
688
|
}
|
|
612
|
-
|
|
689
|
+
// Release every registry claim for THIS dsh home (other homes keep theirs).
|
|
690
|
+
const before = registry.bindings.length;
|
|
691
|
+
registry.bindings = registry.bindings.filter((entry) => entry.home !== resolvedHome);
|
|
692
|
+
const registry_cleared = before !== registry.bindings.length || fs.existsSync(dshBindingsRegistryPath(paths));
|
|
693
|
+
if (registry_cleared) persistDshRegistry(paths, registry);
|
|
694
|
+
return { released: results.filter((result) => result.status === 'released').length, profiles: results, registry_cleared };
|
|
613
695
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"product": "external-subagent",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"platform": "darwin-arm64",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"name": "external-subagentd",
|
|
9
|
-
"bytes":
|
|
9
|
+
"bytes": 10436400,
|
|
10
10
|
"mode": "755",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "1a954915bde8dc5bafd1d73f9a767a55068a54c7707cafe1508a8ddebd35128d"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "external-subagent-mcp",
|
|
15
15
|
"bytes": 935776,
|
|
16
16
|
"mode": "755",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "e0f51f5b901bdb5c01bff16fa8dc136df9583b73aea07c687c24f6949a8dd066"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"product": "external-subagent",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"platform": "linux-x64",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"name": "external-subagentd",
|
|
9
|
-
"bytes":
|
|
9
|
+
"bytes": 12031728,
|
|
10
10
|
"mode": "755",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "ef39528c0c033016f06a6af0bbf727d15ecbaca3f697ee3d5ca321abb4ca75cd"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "external-subagent-mcp",
|
|
15
|
-
"bytes":
|
|
15
|
+
"bytes": 1025480,
|
|
16
16
|
"mode": "755",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "fac84526602dca1cd82574f82afe28e5b6700532297670e33b8fb092c1fad7cb"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "external-subagent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Manage bounded ZCode, DeepSeek Harness (DSH), Codex, and Google Antigravity (agy) subagent tasks through a local MCP service for any local host (Codex, ZCode, or a custom MCP client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|