@topolo/cli 0.13.8 → 0.13.9
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.
|
@@ -24,7 +24,8 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
|
|
|
24
24
|
if (plan.noEffects && (plan.mode !== "recover" || previous?.effectAttempted)) throw new Error("No-effect recovery is not qualified.");
|
|
25
25
|
if (apply) manager[fenceKey] = { operationId, running: true, effectAttempted: false };
|
|
26
26
|
try {
|
|
27
|
-
|
|
27
|
+
const incremental = (plan.mode ?? "configure") === "configure";
|
|
28
|
+
verifyContext(incremental ? [] : void 0);
|
|
28
29
|
const checkDeadline = () => {
|
|
29
30
|
if (Date.now() >= plan.deadline) throw new Error("Native operation deadline expired; reconcile journal before retry.");
|
|
30
31
|
};
|
|
@@ -64,18 +65,28 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
|
|
|
64
65
|
const result = [];
|
|
65
66
|
for (const member of members) {
|
|
66
67
|
checkDeadline();
|
|
67
|
-
|
|
68
|
+
let session = await manager.getSession(member.sessionId);
|
|
68
69
|
if (!session || session.sessionId !== member.sessionId || session.cwd !== member.cwd || session.title !== member.title || session.isArchived && !["switch", "recover"].includes(plan.mode ?? "configure"))
|
|
69
70
|
throw new Error("Owned session identity or active membership changed: " + member.key);
|
|
70
|
-
|
|
71
|
+
let descriptor;
|
|
72
|
+
if (!incremental && (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length))
|
|
71
73
|
throw new Error("Session has live or pending work; no changes permitted: " + member.key);
|
|
72
74
|
if (!plan.noEffects) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
descriptor = await nativeDescriptor(member);
|
|
76
|
+
session = await manager.getSession(member.sessionId);
|
|
77
|
+
checkDeadline();
|
|
78
|
+
if (!session || session.sessionId !== member.sessionId || session.cwd !== member.cwd || session.title !== member.title || (session.isArchived || descriptor.isArchived) && incremental) throw new Error("Owned session identity or active membership changed: " + member.key);
|
|
79
|
+
if (!incremental && (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length || descriptor.isRunning || descriptor.starting || descriptor.pendingToolPermissions?.length))
|
|
75
80
|
throw new Error("Session has live or pending work; no changes permitted: " + member.key);
|
|
76
81
|
if (descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || descriptor.spawnedFrom)
|
|
77
82
|
throw new Error("Incoming session is not a qualified local role.");
|
|
78
83
|
}
|
|
84
|
+
const busyReasons = [
|
|
85
|
+
...manager.hasLosableWork(member.sessionId) ? ["losable"] : [],
|
|
86
|
+
...session.isRunning || descriptor?.isRunning ? ["running"] : [],
|
|
87
|
+
...session.starting || descriptor?.starting ? ["starting"] : [],
|
|
88
|
+
...session.pendingToolPermissions?.length || descriptor?.pendingToolPermissions?.length ? ["pending-permissions"] : []
|
|
89
|
+
];
|
|
79
90
|
result.push({
|
|
80
91
|
key: member.key,
|
|
81
92
|
sessionId: session.sessionId,
|
|
@@ -86,7 +97,8 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
|
|
|
86
97
|
model: session.model ?? null,
|
|
87
98
|
effort: session.effort ?? null,
|
|
88
99
|
permissionMode: session.permissionMode ?? null,
|
|
89
|
-
archived: session.isArchived === true
|
|
100
|
+
archived: session.isArchived === true,
|
|
101
|
+
...incremental ? { busy: busyReasons.length > 0, busyReasons } : {}
|
|
90
102
|
});
|
|
91
103
|
}
|
|
92
104
|
return result;
|
|
@@ -118,6 +130,124 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
|
|
|
118
130
|
const outgoingBefore = await outgoingSnapshot();
|
|
119
131
|
const sections = await command({ kind: "list_sections" });
|
|
120
132
|
checkpoint({ kind: "baseline", before, outgoingBefore, sections: sections.sections, at: Date.now() });
|
|
133
|
+
if (incremental) {
|
|
134
|
+
const settings = ["group", "pinned", "model", "effort", "permissionMode"];
|
|
135
|
+
const idleBefore = plan.members.filter((_, i) => !before[i].busy);
|
|
136
|
+
verifyContext([]);
|
|
137
|
+
if (!apply) return {
|
|
138
|
+
state: "preflight",
|
|
139
|
+
before,
|
|
140
|
+
outgoingBefore,
|
|
141
|
+
sections: sections.sections,
|
|
142
|
+
availableModels: models,
|
|
143
|
+
applied: false,
|
|
144
|
+
contextVerifiedSessionIds: idleBefore.map((m) => m.sessionId)
|
|
145
|
+
};
|
|
146
|
+
const appliedFields = [];
|
|
147
|
+
const deferredReasons = /* @__PURE__ */ new Map();
|
|
148
|
+
const differences = (member, row) => settings.filter((field) => member[field] !== row[field]);
|
|
149
|
+
for (const member of plan.members) {
|
|
150
|
+
const initial = (await snapshot([member]))[0];
|
|
151
|
+
if (initial.busy) {
|
|
152
|
+
deferredReasons.set(member.sessionId, initial.busyReasons);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
for (const setting of differences(member, initial)) {
|
|
156
|
+
let row = (await snapshot([member]))[0];
|
|
157
|
+
if (row.busy) {
|
|
158
|
+
deferredReasons.set(member.sessionId, row.busyReasons);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (row[setting] === member[setting]) continue;
|
|
162
|
+
verifyContext([member]);
|
|
163
|
+
if (setting === "group" && member.group !== null) {
|
|
164
|
+
let matching = manager.listCustomGroups()?.filter((g) => g.name === member.group);
|
|
165
|
+
if (!matching || matching.length > 1) throw new Error("Ambiguous native sidebar group: " + member.group);
|
|
166
|
+
if (!matching.length) {
|
|
167
|
+
row = (await snapshot([member]))[0];
|
|
168
|
+
if (row.busy) {
|
|
169
|
+
deferredReasons.set(member.sessionId, row.busyReasons);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
verifyContext([member]);
|
|
173
|
+
checkDeadline();
|
|
174
|
+
checkpoint({ kind: "attempt", operation: "create_group", key: member.key, name: member.group, at: Date.now() });
|
|
175
|
+
manager[fenceKey].effectAttempted = true;
|
|
176
|
+
const created = await command({ kind: "create_group", name: member.group });
|
|
177
|
+
checkpoint({ kind: "acknowledged", operation: "create_group", key: member.key, name: member.group, at: Date.now() });
|
|
178
|
+
matching = manager.listCustomGroups()?.filter((g) => g.name === member.group);
|
|
179
|
+
if (!created.groupId) throw new Error("Native create_group did not return an identity.");
|
|
180
|
+
if (matching?.length !== 1 || matching[0].id !== created.groupId)
|
|
181
|
+
throw new Error("Native group read-back mismatch.");
|
|
182
|
+
verifyContext([member]);
|
|
183
|
+
appliedFields.push({ key: member.key, operation: "create_group", name: member.group });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
row = (await snapshot([member]))[0];
|
|
187
|
+
if (row.busy) {
|
|
188
|
+
deferredReasons.set(member.sessionId, row.busyReasons);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
if (row[setting] === member[setting]) continue;
|
|
192
|
+
verifyContext([member]);
|
|
193
|
+
checkDeadline();
|
|
194
|
+
if (setting === "model" && !manager.getAvailableCodeModels().includes(member.model))
|
|
195
|
+
throw new Error("Requested model is absent from the native account catalog: " + member.model);
|
|
196
|
+
const operation = setting === "group" ? "move_sessions" : setting === "pinned" ? "set_pinned" : setting === "model" ? "set_model" : setting === "effort" ? "set_effort" : "set_permission_mode";
|
|
197
|
+
checkpoint({ kind: "attempt", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
|
|
198
|
+
manager[fenceKey].effectAttempted = true;
|
|
199
|
+
if (setting === "group") {
|
|
200
|
+
const matching = member.group === null ? [] : manager.listCustomGroups()?.filter((g) => g.name === member.group);
|
|
201
|
+
if (!matching || member.group !== null && matching.length !== 1) throw new Error("Native group identity changed.");
|
|
202
|
+
await command({
|
|
203
|
+
kind: "move_sessions",
|
|
204
|
+
sessionIds: [member.sessionId],
|
|
205
|
+
...member.group === null ? {} : { groupId: matching[0].id }
|
|
206
|
+
});
|
|
207
|
+
} else if (setting === "pinned") await command({ kind: "set_pinned", sessionId: member.sessionId, pinned: member.pinned });
|
|
208
|
+
else if (setting === "model") await manager.setModel(member.sessionId, member.model);
|
|
209
|
+
else if (setting === "effort") await manager.setEffort(member.sessionId, member.effort);
|
|
210
|
+
else if (!await manager.setPermissionMode(member.sessionId, member.permissionMode, "picker"))
|
|
211
|
+
throw new Error("Native permission mode was refused: " + member.key);
|
|
212
|
+
checkpoint({ kind: "acknowledged", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
|
|
213
|
+
const actual = (await snapshot([member]))[0];
|
|
214
|
+
if (actual[setting] !== member[setting]) throw new Error("Native read-back mismatch: " + member.key + " / " + setting);
|
|
215
|
+
verifyContext([member]);
|
|
216
|
+
appliedFields.push({ key: member.key, sessionId: member.sessionId, setting, value: member[setting] });
|
|
217
|
+
checkpoint({ kind: "verified-field", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const after2 = await snapshot();
|
|
221
|
+
const idleAfter = plan.members.filter((member, i) => !after2[i].busy && !deferredReasons.has(member.sessionId));
|
|
222
|
+
verifyContext(idleAfter);
|
|
223
|
+
const deferred = plan.members.flatMap((member, i) => {
|
|
224
|
+
const row = after2[i], fields = differences(member, row);
|
|
225
|
+
if (!fields.length) return [];
|
|
226
|
+
const reasons = deferredReasons.get(member.sessionId) ?? row.busyReasons;
|
|
227
|
+
if (!reasons?.length) throw new Error("Native read-back mismatch: " + member.key + " / " + fields.join(","));
|
|
228
|
+
return [{ key: member.key, sessionId: member.sessionId, fields, reasons }];
|
|
229
|
+
});
|
|
230
|
+
const attestedBusyKeys = after2.filter((row) => row.busy && !deferred.some((d) => d.sessionId === row.sessionId)).map((row) => row.key);
|
|
231
|
+
checkpoint({ kind: deferred.length ? "partial-verified" : "verified", after: after2, appliedFields, deferred, attestedBusyKeys, at: Date.now() });
|
|
232
|
+
delete manager[fenceKey];
|
|
233
|
+
return {
|
|
234
|
+
state: deferred.length ? "native-settings-partial" : "native-settings-verified",
|
|
235
|
+
before,
|
|
236
|
+
after: after2,
|
|
237
|
+
outgoingAfter: [],
|
|
238
|
+
verifiedAgents: after2.length - deferred.length,
|
|
239
|
+
complete: deferred.length === 0,
|
|
240
|
+
applied: appliedFields.length > 0,
|
|
241
|
+
appliedFields,
|
|
242
|
+
deferred,
|
|
243
|
+
attestedBusyKeys,
|
|
244
|
+
contextVerifiedSessionIds: idleAfter.map((m) => m.sessionId),
|
|
245
|
+
busyObservations: [...deferredReasons].map(([sessionId, reasons]) => ({ key: plan.members.find((m) => m.sessionId === sessionId).key, sessionId, reasons })),
|
|
246
|
+
modelInferenceVerified: false,
|
|
247
|
+
readOnlyConfinementVerified: false,
|
|
248
|
+
fullPackSwitchSupported: false
|
|
249
|
+
};
|
|
250
|
+
}
|
|
121
251
|
if (plan.mode === "recover") {
|
|
122
252
|
if (plan.noEffects) {
|
|
123
253
|
verifyContext();
|
|
@@ -672,6 +802,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
672
802
|
if (!records.length || records.length !== policy.agents.length) throw new Error("Reviewed policy must cover the complete installed roster.");
|
|
673
803
|
const members = [];
|
|
674
804
|
const context = {};
|
|
805
|
+
const roleContextFiles = {};
|
|
675
806
|
const contextInventories = {};
|
|
676
807
|
const captureMarkdown = (cwd) => {
|
|
677
808
|
contextInventories[cwd] = snapshotClaudeMarkdown(cwd, filesystem, cryptography, paths);
|
|
@@ -690,6 +821,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
690
821
|
const transcript = realpathSync(record.transcript);
|
|
691
822
|
context[charter] = createHash("sha256").update(readFileSync(charter)).digest("hex");
|
|
692
823
|
context[transcript] = createHash("sha256").update(readFileSync(transcript)).digest("hex");
|
|
824
|
+
roleContextFiles[record.desktopSessionId] = [charter, transcript];
|
|
693
825
|
members.push({
|
|
694
826
|
key: record.key,
|
|
695
827
|
sessionId: record.desktopSessionId,
|
|
@@ -744,6 +876,8 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
744
876
|
outgoing,
|
|
745
877
|
context,
|
|
746
878
|
contextInventories,
|
|
879
|
+
roleContextFiles,
|
|
880
|
+
globalContextFiles: [resolve(root, "claude/state.json"), realpathSync(policyPath)],
|
|
747
881
|
approvalRoutes,
|
|
748
882
|
installationConsent: policy.installationConsent,
|
|
749
883
|
libraryRoot: root,
|
|
@@ -752,6 +886,27 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
752
886
|
activeBefore: active
|
|
753
887
|
};
|
|
754
888
|
}
|
|
889
|
+
function verifyClaudePlanContext(plan, members, fs, crypto, path, snapshotMarkdown) {
|
|
890
|
+
if (!plan.contextInventories || !Object.keys(plan.contextInventories).length) throw new Error("Complete context inventory proof is unavailable.");
|
|
891
|
+
if (members && (!plan.roleContextFiles || !plan.globalContextFiles?.length)) throw new Error("Scoped role context proof is unavailable.");
|
|
892
|
+
const selected = members ?? plan.members;
|
|
893
|
+
if (new Set(selected.map((m) => m.sessionId)).size !== selected.length || selected.some((m) => !plan.members.some((p) => p.sessionId === m.sessionId && p.key === m.key && p.cwd === m.cwd && p.title === m.title)))
|
|
894
|
+
throw new Error("Scoped context identity differs from the complete roster.");
|
|
895
|
+
const roots = members ? selected.map((m) => m.cwd) : Object.keys(plan.contextInventories);
|
|
896
|
+
for (const root of roots) {
|
|
897
|
+
const inventory = plan.contextInventories[root];
|
|
898
|
+
if (!inventory || JSON.stringify(snapshotMarkdown(root, fs, crypto, path)) !== JSON.stringify(inventory))
|
|
899
|
+
throw new Error("Context or transcript changed during configuration: Markdown inventory " + root);
|
|
900
|
+
}
|
|
901
|
+
const files = members ? [...plan.globalContextFiles, ...selected.flatMap((m) => {
|
|
902
|
+
const files2 = plan.roleContextFiles[m.sessionId];
|
|
903
|
+
if (!files2?.length) throw new Error("Scoped role context proof is unavailable.");
|
|
904
|
+
return files2;
|
|
905
|
+
})] : Object.keys(plan.context);
|
|
906
|
+
for (const file of files)
|
|
907
|
+
if (!plan.context[file] || crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== plan.context[file])
|
|
908
|
+
throw new Error("Context or transcript changed during configuration: " + file);
|
|
909
|
+
}
|
|
755
910
|
function qualifyPermissionConsent(policy, key, records, full) {
|
|
756
911
|
const consent = policy.installationConsent;
|
|
757
912
|
const intakeKey = consent?.intakeRoutes?.[key];
|
|
@@ -784,15 +939,9 @@ function nativePackExpression(pid, plan, journal, apply) {
|
|
|
784
939
|
const context = ${JSON.stringify(plan.context)};
|
|
785
940
|
const inventories = ${JSON.stringify(plan.contextInventories)};
|
|
786
941
|
const snapshotMarkdown = (${snapshotClaudeMarkdown.toString()});
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (JSON.stringify(snapshotMarkdown(root, fs, crypto, path)) !== JSON.stringify(inventory))
|
|
791
|
-
throw new Error('Context or transcript changed during configuration: Markdown inventory ' + root);
|
|
792
|
-
for (const [file, hash] of Object.entries(context))
|
|
793
|
-
if (crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') !== hash)
|
|
794
|
-
throw new Error('Context or transcript changed during configuration: ' + file);
|
|
795
|
-
};
|
|
942
|
+
const contextPlan = {context, contextInventories: inventories, members: ${JSON.stringify(plan.members)},
|
|
943
|
+
roleContextFiles: ${JSON.stringify(plan.roleContextFiles)}, globalContextFiles: ${JSON.stringify(plan.globalContextFiles)}};
|
|
944
|
+
const verifyContext = members => (${verifyClaudePlanContext.toString()})(contextPlan, members, fs, crypto, path, snapshotMarkdown);
|
|
796
945
|
const result = await (${nativeClaudePackOperation.toString()})(manager,
|
|
797
946
|
value => transport.P(value, transport.M),
|
|
798
947
|
{ members: ${JSON.stringify(plan.members)}, outgoing: ${JSON.stringify(plan.outgoing)},
|
|
@@ -815,15 +964,61 @@ function nativePackExpression(pid, plan, journal, apply) {
|
|
|
815
964
|
row => checkpoint({kind: 'approval', ...row}),
|
|
816
965
|
fs.readFileSync(expected.journal, 'utf8').split('\\n').filter(Boolean).map(line => JSON.parse(line)));
|
|
817
966
|
if (expected.apply) {
|
|
818
|
-
result.approvalRouting = await invoke({kind: 'install'});
|
|
819
967
|
const control = ${JSON.stringify(plan.control)};
|
|
820
968
|
if (!control) throw new Error('Owned native control setup is unavailable.');
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
969
|
+
const identity = fingerprint(routes);
|
|
970
|
+
const current = manager[Symbol.for('topolo.agent-pack.approval-control.v1')];
|
|
971
|
+
const listener = manager[Symbol.for('topolo.agent-pack.approvals.v1')];
|
|
972
|
+
const incremental = ${JSON.stringify(plan.mode === "configure" && !plan.recovery)};
|
|
973
|
+
const unchanged = listener?.identity === identity && listener.listener && !listener.auditFailed &&
|
|
974
|
+
current?.descriptor.pid === expected.pid && current.descriptor.identity === identity &&
|
|
975
|
+
current.descriptor.socket === control.socket && current.server?.listening === true && !current.failure;
|
|
976
|
+
if (!incremental) {
|
|
977
|
+
result.approvalRouting = await invoke({kind: 'install'});
|
|
978
|
+
const stat = fs.lstatSync(control.secretPath);
|
|
979
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
|
|
980
|
+
const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
|
|
981
|
+
result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
|
|
982
|
+
{socket: control.socket, pid: expected.pid, identity}, secret.token,
|
|
983
|
+
load('node:net'), fs, invoke, qualifyNative, checkpoint);
|
|
984
|
+
} else if (unchanged) {
|
|
985
|
+
// Re-attest the owned listener/control; do not replace matching hooks
|
|
986
|
+
// or interrupt an in-flight request merely to configure idle settings.
|
|
987
|
+
result.approvalRouting = {state: 'native-approval-listener-installed', roles: routes.length,
|
|
988
|
+
identity, control: current.descriptor, unchanged: true};
|
|
989
|
+
} else {
|
|
990
|
+
qualifyNative();
|
|
991
|
+
const deferred = [];
|
|
992
|
+
for (const route of routes) {
|
|
993
|
+
const descriptor = await manager.getSessionWhenLoaded(route.sessionId, 'topolo-agent-pack-approval', {warnOnMiss: false});
|
|
994
|
+
const session = await manager.getSession(route.sessionId);
|
|
995
|
+
if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title ||
|
|
996
|
+
descriptor.isArchived || descriptor.backend?.kind !== 'local' || descriptor.backend.remoteTarget || descriptor.worktreePath ||
|
|
997
|
+
descriptor.worktreeLazy || descriptor.spawnedFrom || !session || session.sessionId !== route.sessionId ||
|
|
998
|
+
session.cwd !== route.cwd || session.title !== route.title || session.isArchived)
|
|
999
|
+
throw new Error('Native approval route ownership changed.');
|
|
1000
|
+
if (session.pendingToolPermissions?.length || descriptor.pendingToolPermissions?.length)
|
|
1001
|
+
deferred.push({key: route.key, fields: ['intakeHooks'], reason: 'pending-permissions'});
|
|
1002
|
+
}
|
|
1003
|
+
if (current?.active || current?.failure || listener?.auditFailed ||
|
|
1004
|
+
(listener && listener.identity !== identity) ||
|
|
1005
|
+
(current && (current.descriptor.pid !== expected.pid || current.descriptor.identity !== identity)))
|
|
1006
|
+
for (const route of routes) deferred.push({key: route.key, fields: ['intakeHooks'], reason: 'existing-control-custody'});
|
|
1007
|
+
if (deferred.length) {
|
|
1008
|
+
result.routingDeferred = deferred;
|
|
1009
|
+
result.state = 'native-settings-partial';result.complete = false;
|
|
1010
|
+
checkpoint({kind: 'routing-deferred', deferred, at: Date.now()});
|
|
1011
|
+
} else {
|
|
1012
|
+
verifyContext(result.after.filter(row => !row.busy).map(row => contextPlan.members.find(m => m.sessionId === row.sessionId)));
|
|
1013
|
+
result.approvalRouting = await invoke({kind: 'install'});
|
|
1014
|
+
const stat = fs.lstatSync(control.secretPath);
|
|
1015
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
|
|
1016
|
+
const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
|
|
1017
|
+
result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
|
|
1018
|
+
{socket: control.socket, pid: expected.pid, identity}, secret.token,
|
|
1019
|
+
load('node:net'), fs, invoke, qualifyNative, checkpoint);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
827
1022
|
} else if (${JSON.stringify(plan.recovery === true && !plan.noEffects)}) {
|
|
828
1023
|
const current = manager[Symbol.for('topolo.agent-pack.approval-control.v1')];
|
|
829
1024
|
if (!current || current.descriptor.pid !== expected.pid || current.descriptor.identity !== fingerprint(routes))
|
|
@@ -920,13 +1115,8 @@ function syncDirectory(path) {
|
|
|
920
1115
|
closeSync(fd);
|
|
921
1116
|
}
|
|
922
1117
|
}
|
|
923
|
-
function verifyPlanContext(plan) {
|
|
924
|
-
|
|
925
|
-
for (const [root, inventory] of Object.entries(plan.contextInventories))
|
|
926
|
-
if (JSON.stringify(snapshotClaudeMarkdown(root, filesystem, cryptography, paths)) !== JSON.stringify(inventory))
|
|
927
|
-
throw new Error("Context or transcript changed during configuration: Markdown inventory " + root);
|
|
928
|
-
for (const [file, hash] of Object.entries(plan.context))
|
|
929
|
-
if (createHash("sha256").update(readFileSync(file)).digest("hex") !== hash) throw new Error("Context or transcript changed during configuration: " + file);
|
|
1118
|
+
function verifyPlanContext(plan, members) {
|
|
1119
|
+
verifyClaudePlanContext(plan, members, filesystem, cryptography, paths, snapshotClaudeMarkdown);
|
|
930
1120
|
}
|
|
931
1121
|
function verifyClaudeOrganizationPolicy(root, pack, consent, run = spawnSync, locate = existsSync) {
|
|
932
1122
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -976,18 +1166,35 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
976
1166
|
} else {
|
|
977
1167
|
plan = { ...prepareClaudeNativePlan(root, options.pack, options.policy, options.mode === "switch" ? "switch" : "configure"), operationId };
|
|
978
1168
|
if (options.apply && plan.approvalRoutes.length) {
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const
|
|
985
|
-
if (
|
|
986
|
-
|
|
1169
|
+
const connectionPath = resolve(root, "claude/approval-connection.json");
|
|
1170
|
+
if (plan.mode === "configure" && existsSync(connectionPath)) {
|
|
1171
|
+
const entry = lstatSync(connectionPath);
|
|
1172
|
+
if (!entry.isFile() || entry.isSymbolicLink() || typeof process.getuid !== "function" || entry.uid !== process.getuid() || entry.mode & 63)
|
|
1173
|
+
throw new Error("Unowned native approval connection.");
|
|
1174
|
+
const connection = JSON.parse(readFileSync(connectionPath, "utf8"));
|
|
1175
|
+
if (connection.pid === options.pid && connection.pack === options.pack && JSON.stringify(connection.routes) === JSON.stringify(plan.approvalRoutes) && JSON.stringify(connection.installationConsent) === JSON.stringify(plan.installationConsent)) {
|
|
1176
|
+
if (typeof connection.secretPath !== "string" || typeof connection.socket !== "string" || !resolve(connection.secretPath).startsWith(realpathSync(resolve(root, "claude/private")) + sep) || realpathSync(connection.secretPath) !== resolve(connection.secretPath) || dirname(resolve(connection.socket)) !== resolve("/tmp", "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16)))
|
|
1177
|
+
throw new Error("Native approval connection is outside owned control paths.");
|
|
1178
|
+
const secret = lstatSync(connection.secretPath);
|
|
1179
|
+
if (!secret.isFile() || secret.isSymbolicLink() || typeof process.getuid !== "function" || secret.uid !== process.getuid() || secret.mode & 63)
|
|
1180
|
+
throw new Error("Unowned native control secret.");
|
|
1181
|
+
plan.control = { socket: connection.socket, secretPath: connection.secretPath };
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (!plan.control) {
|
|
1185
|
+
const directory2 = resolve(root, "claude/private");
|
|
1186
|
+
mkdirSync(directory2, { recursive: true, mode: 448 });
|
|
1187
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16));
|
|
1188
|
+
mkdirSync(socketDirectory, { recursive: true, mode: 448 });
|
|
1189
|
+
for (const path of [directory2, socketDirectory]) {
|
|
1190
|
+
const stat = lstatSync(path);
|
|
1191
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63)
|
|
1192
|
+
throw new Error("Native control directory must be owned and private.");
|
|
1193
|
+
}
|
|
1194
|
+
plan.control = { socket: resolve(socketDirectory, operationId + ".sock"), secretPath: resolve(directory2, operationId + ".json") };
|
|
1195
|
+
durableRecord(plan.control.secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
1196
|
+
syncDirectory(directory2);
|
|
987
1197
|
}
|
|
988
|
-
plan.control = { socket: resolve(socketDirectory, operationId + ".sock"), secretPath: resolve(directory2, operationId + ".json") };
|
|
989
|
-
durableRecord(plan.control.secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
990
|
-
syncDirectory(directory2);
|
|
991
1198
|
}
|
|
992
1199
|
journal = resolve(directory, operationId + ".jsonl");
|
|
993
1200
|
const record = { kind: "plan", pack: options.pack, ...plan, apply: options.apply };
|
|
@@ -998,7 +1205,8 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
998
1205
|
syncDirectory(resolve(root, "claude"));
|
|
999
1206
|
}
|
|
1000
1207
|
try {
|
|
1001
|
-
|
|
1208
|
+
const incremental = !recovering && plan.mode === "configure";
|
|
1209
|
+
verifyPlanContext(plan, incremental ? [] : void 0);
|
|
1002
1210
|
const organizationPolicy = options.apply && plan.approvalRoutes.length ? qualify(root, options.pack, plan.installationConsent) : null;
|
|
1003
1211
|
if (options.apply) {
|
|
1004
1212
|
const dispatched = { ...marker, dispatchState: "may-have-effects" };
|
|
@@ -1009,7 +1217,18 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1009
1217
|
marker = dispatched;
|
|
1010
1218
|
}
|
|
1011
1219
|
const result = await evaluate(options.endpoint, nativePackExpression(options.pid, plan, journal, options.apply));
|
|
1012
|
-
|
|
1220
|
+
if (incremental && result.contextVerifiedSessionIds !== void 0) {
|
|
1221
|
+
if (!Array.isArray(result.contextVerifiedSessionIds) || new Set(result.contextVerifiedSessionIds).size !== result.contextVerifiedSessionIds.length || result.contextVerifiedSessionIds.some((id) => !plan.members.some((m) => m.sessionId === id)))
|
|
1222
|
+
throw new Error("Native scoped context identities differ.");
|
|
1223
|
+
const observations = result.busyObservations ?? [];
|
|
1224
|
+
if (!Array.isArray(observations) || observations.some((row) => !plan.members.some((m) => m.key === row.key && m.sessionId === row.sessionId) || !Array.isArray(row.reasons) || !row.reasons.length || row.reasons.some((reason) => !["running", "starting", "losable", "pending-permissions"].includes(reason))))
|
|
1225
|
+
throw new Error("Native busy context attestation differs.");
|
|
1226
|
+
for (let i = 0; i < plan.members.length; i++)
|
|
1227
|
+
if (!result.contextVerifiedSessionIds.includes(plan.members[i].sessionId) && result.after?.[i]?.busy !== true && !observations.some((row) => row.sessionId === plan.members[i].sessionId))
|
|
1228
|
+
throw new Error("Native result omits idle context qualification.");
|
|
1229
|
+
}
|
|
1230
|
+
const contextMembers = incremental && Array.isArray(result.after) ? plan.members.filter((m, i) => result.after[i]?.busy !== true && (result.contextVerifiedSessionIds === void 0 || result.contextVerifiedSessionIds.includes(m.sessionId))) : void 0;
|
|
1231
|
+
verifyPlanContext(plan, contextMembers);
|
|
1013
1232
|
if (options.apply || recovering) {
|
|
1014
1233
|
if (plan.noEffects) {
|
|
1015
1234
|
if (result.state !== "native-reconciled-no-effects" || result.nativeEffectsReplayed !== false || result.after?.length !== plan.members.length || result.after.some((actual, i) => ["key", "sessionId", "cwd", "title"].some((key) => actual[key] !== plan.members[i][key])))
|
|
@@ -1023,6 +1242,46 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1023
1242
|
syncDirectory(resolve(root, "claude"));
|
|
1024
1243
|
return { ...result, journal, contextPreserved: true };
|
|
1025
1244
|
}
|
|
1245
|
+
if (incremental && result.state === "native-settings-partial") {
|
|
1246
|
+
const settings = ["group", "pinned", "model", "effort", "permissionMode"];
|
|
1247
|
+
if (result.after?.length !== plan.members.length || !Array.isArray(result.deferred) || !result.deferred.length && !result.routingDeferred?.length) throw new Error("Native partial result has no qualified complete roster.");
|
|
1248
|
+
if (new Set(result.deferred.map((d) => d.sessionId)).size !== result.deferred.length || result.deferred.some((d) => !plan.members.some((m) => m.key === d.key && m.sessionId === d.sessionId) || !Array.isArray(d.fields) || !d.fields.length || new Set(d.fields).size !== d.fields.length || d.fields.some((field) => !settings.includes(field)) || !Array.isArray(d.reasons) || !d.reasons.length || d.reasons.some((reason) => !["running", "starting", "losable", "pending-permissions"].includes(reason))))
|
|
1249
|
+
throw new Error("Native deferred identity or fields differ.");
|
|
1250
|
+
if (result.routingDeferred !== void 0 && (!Array.isArray(result.routingDeferred) || result.routingDeferred.some((d) => !plan.approvalRoutes.some((r) => r.key === d.key) || JSON.stringify(d.fields) !== JSON.stringify(["intakeHooks"]) || !["pending-permissions", "existing-control-custody"].includes(d.reason))))
|
|
1251
|
+
throw new Error("Native deferred routing ownership differs.");
|
|
1252
|
+
for (let i = 0; i < plan.members.length; i++) {
|
|
1253
|
+
const wanted = plan.members[i], actual = result.after[i];
|
|
1254
|
+
if (actual.archived !== false || ["key", "sessionId", "cwd", "title"].some((key) => actual[key] !== wanted[key]))
|
|
1255
|
+
throw new Error("Native partial roster identity differs.");
|
|
1256
|
+
const mismatch = settings.filter((key) => actual[key] !== wanted[key]);
|
|
1257
|
+
const deferred = result.deferred.find((d) => d.sessionId === wanted.sessionId);
|
|
1258
|
+
if (JSON.stringify(mismatch.slice().sort()) !== JSON.stringify((deferred?.fields ?? []).slice().sort()))
|
|
1259
|
+
throw new Error("Native partial result hides an undeferred mismatch.");
|
|
1260
|
+
}
|
|
1261
|
+
const activePath2 = resolve(root, "claude/active.json");
|
|
1262
|
+
const active2 = existsSync(activePath2) ? JSON.parse(readFileSync(activePath2, "utf8")) : null;
|
|
1263
|
+
if (JSON.stringify(active2) !== JSON.stringify(plan.activeBefore)) throw new Error("Active roster changed during partial configuration.");
|
|
1264
|
+
durableRecord(journal, {
|
|
1265
|
+
kind: "partial-verified",
|
|
1266
|
+
after: result.after,
|
|
1267
|
+
deferred: result.deferred,
|
|
1268
|
+
routingDeferred: result.routingDeferred ?? [],
|
|
1269
|
+
appliedFields: result.appliedFields ?? [],
|
|
1270
|
+
activeUnchanged: true
|
|
1271
|
+
});
|
|
1272
|
+
if (JSON.stringify(JSON.parse(readFileSync(markerPath, "utf8"))) !== JSON.stringify(marker)) throw new Error("Recovery marker identity changed.");
|
|
1273
|
+
unlinkSync(markerPath);
|
|
1274
|
+
syncDirectory(resolve(root, "claude"));
|
|
1275
|
+
return {
|
|
1276
|
+
...result,
|
|
1277
|
+
complete: false,
|
|
1278
|
+
journal,
|
|
1279
|
+
contextPreserved: contextMembers?.length === plan.members.length,
|
|
1280
|
+
idleContextsVerified: contextMembers?.map((m) => m.key),
|
|
1281
|
+
activeRosterUnchanged: true,
|
|
1282
|
+
resume: "explicit-configure-only"
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1026
1285
|
if (result.state !== (recovering ? "native-reconciled" : "native-settings-verified") || result.after?.length !== plan.members.length) throw new Error("Native result has no complete verified roster.");
|
|
1027
1286
|
for (let i = 0; i < plan.members.length; i++) {
|
|
1028
1287
|
const wanted = plan.members[i];
|
|
@@ -1066,7 +1325,12 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1066
1325
|
unlinkSync(markerPath);
|
|
1067
1326
|
syncDirectory(resolve(root, "claude"));
|
|
1068
1327
|
} else durableRecord(journal, { kind: "context-verified", context: plan.context });
|
|
1069
|
-
return {
|
|
1328
|
+
return {
|
|
1329
|
+
...result,
|
|
1330
|
+
journal,
|
|
1331
|
+
contextPreserved: !incremental || contextMembers?.length === plan.members.length,
|
|
1332
|
+
...incremental ? { idleContextsVerified: contextMembers?.map((m) => m.key) } : {}
|
|
1333
|
+
};
|
|
1070
1334
|
} catch (error) {
|
|
1071
1335
|
durableRecord(journal, { kind: "recovery-required" });
|
|
1072
1336
|
throw new Error(`Claude configuration is incomplete. Reconcile ${journal}. ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1203,5 +1467,6 @@ export {
|
|
|
1203
1467
|
runClaudeSmokeHelper,
|
|
1204
1468
|
snapshotClaudeMarkdown,
|
|
1205
1469
|
validateInspectorEndpoint,
|
|
1206
|
-
verifyClaudeOrganizationPolicy
|
|
1470
|
+
verifyClaudeOrganizationPolicy,
|
|
1471
|
+
verifyClaudePlanContext
|
|
1207
1472
|
};
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
14
14
|
throw new Error("Exact closed smoke operation, role/case and UUID required.");
|
|
15
15
|
const store = packs.opts().store ?? resolve(homedir(), "AgentPacks");
|
|
16
16
|
if (options.harness === "claude") {
|
|
17
|
-
const { operateClaudeSmoke } = await import("./claude-pack-backend-
|
|
17
|
+
const { operateClaudeSmoke } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
18
18
|
console.log(JSON.stringify(await operateClaudeSmoke({
|
|
19
19
|
store,
|
|
20
20
|
kind: "smoke-" + operation,
|
|
@@ -44,7 +44,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
44
44
|
});
|
|
45
45
|
packs.command("configure").description("Qualify and configure an adopted Claude roster through its live native backend").argument("<pack>").option("--harness <name>", "Host adapter", "claude").requiredOption("--policy <path>", "Reviewed execution policy JSON").requiredOption("--endpoint <url>", "Explicit loopback main-process inspector endpoint, enabled separately with authorization").requiredOption("--pid <pid>", "Expected Claude main-process PID").option("--apply", "Apply native groups, pins, models, effort and permission modes; otherwise preflight only").action(async (pack, options) => {
|
|
46
46
|
if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
|
|
47
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
47
|
+
const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
48
48
|
const result = await configureClaudePack({
|
|
49
49
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
50
50
|
pack,
|
|
@@ -96,7 +96,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
96
96
|
throw new Error("Only a one-request once/deny response is supported.");
|
|
97
97
|
decision = response.decision;
|
|
98
98
|
}
|
|
99
|
-
const { operateClaudeApprovals } = await import("./claude-pack-backend-
|
|
99
|
+
const { operateClaudeApprovals } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
100
100
|
console.log(JSON.stringify(await operateClaudeApprovals({
|
|
101
101
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
102
102
|
kind: args[0],
|
|
@@ -111,7 +111,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
111
111
|
if (options.policy || options.endpoint || options.pid || options.apply) {
|
|
112
112
|
if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
|
|
113
113
|
throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
|
|
114
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
114
|
+
const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
115
115
|
const result2 = await configureClaudePack({
|
|
116
116
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
117
117
|
pack: args[0],
|
|
@@ -1741,7 +1741,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1741
1741
|
} catch (error) {
|
|
1742
1742
|
const transportFailure = !(error instanceof TopoloSdkError) && !opts.signal?.aborted;
|
|
1743
1743
|
if (!transportFailure || attempt === maxAttempts) throw error;
|
|
1744
|
-
await new Promise((
|
|
1744
|
+
await new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
1745
1745
|
}
|
|
1746
1746
|
}
|
|
1747
1747
|
throw new TopoloSdkError("network_error", "Topolo read retry loop exhausted.");
|
|
@@ -3740,7 +3740,7 @@ function createTopolo(options) {
|
|
|
3740
3740
|
async function pollUntilApproved(params) {
|
|
3741
3741
|
let interval = Math.max(params.intervalSeconds ?? 5, 1);
|
|
3742
3742
|
while (Date.now() < params.expiresAtMs) {
|
|
3743
|
-
await new Promise((
|
|
3743
|
+
await new Promise((resolve5) => setTimeout(resolve5, Math.min(interval * 1e3, params.expiresAtMs - Date.now())));
|
|
3744
3744
|
if (Date.now() >= params.expiresAtMs) break;
|
|
3745
3745
|
try {
|
|
3746
3746
|
return await params.oauth.pollDeviceToken({
|
|
@@ -6521,7 +6521,7 @@ import { spawnSync as spawnSync4 } from "child_process";
|
|
|
6521
6521
|
import { existsSync as existsSync6 } from "fs";
|
|
6522
6522
|
import { readFile as readFile6, rm as rm2 } from "fs/promises";
|
|
6523
6523
|
import { homedir as homedir5 } from "os";
|
|
6524
|
-
import { dirname as dirname6, join as join7, resolve as
|
|
6524
|
+
import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
|
|
6525
6525
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6526
6526
|
import { createRequire } from "module";
|
|
6527
6527
|
|
|
@@ -6530,7 +6530,7 @@ import { spawnSync as spawnSync3 } from "child_process";
|
|
|
6530
6530
|
import { existsSync as existsSync4 } from "fs";
|
|
6531
6531
|
import { readFile as readFile4 } from "fs/promises";
|
|
6532
6532
|
import { homedir as homedir4 } from "os";
|
|
6533
|
-
import { isAbsolute, join as join6 } from "path";
|
|
6533
|
+
import { isAbsolute, join as join6, resolve as resolve2 } from "path";
|
|
6534
6534
|
var SUPPORTED_AGENT_IDS = [
|
|
6535
6535
|
"claude",
|
|
6536
6536
|
"claude-desktop",
|
|
@@ -6570,6 +6570,18 @@ var AGENT_NAME_FOR = {
|
|
|
6570
6570
|
goose: "goose",
|
|
6571
6571
|
"amazon-q": "amazon-q"
|
|
6572
6572
|
};
|
|
6573
|
+
function resolveAgentHostTarget(options) {
|
|
6574
|
+
const homeDir = resolve2(options.homeDir ?? homedir4());
|
|
6575
|
+
const codexHome = options.codexHome ?? (options.homeDir === void 0 ? process.env.CODEX_HOME || void 0 : void 0);
|
|
6576
|
+
if (codexHome !== void 0 && (!codexHome.trim() || !isAbsolute(codexHome))) {
|
|
6577
|
+
throw new Error("Codex home must be an absolute nonempty directory.");
|
|
6578
|
+
}
|
|
6579
|
+
return Object.freeze({
|
|
6580
|
+
homeDir,
|
|
6581
|
+
...options.projectDir ? { projectDir: resolve2(options.projectDir) } : {},
|
|
6582
|
+
codexHome: codexHome ?? join6(homeDir, ".codex")
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6573
6585
|
var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
|
|
6574
6586
|
function isPublicRegistrationName(name) {
|
|
6575
6587
|
return name === "topolo" || name.startsWith("topolo-") && name !== RESERVED_OPERATOR_REGISTRATION;
|
|
@@ -6585,14 +6597,14 @@ function stableNodeCommand() {
|
|
|
6585
6597
|
});
|
|
6586
6598
|
return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
|
|
6587
6599
|
}
|
|
6588
|
-
function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
|
|
6600
|
+
function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6589
6601
|
switch (agent) {
|
|
6590
6602
|
case "claude":
|
|
6591
6603
|
return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
6592
6604
|
case "claude-desktop":
|
|
6593
6605
|
return join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
6594
6606
|
case "codex":
|
|
6595
|
-
return join6(homeDir, ".codex", "skills", "topolo", "SKILL.md");
|
|
6607
|
+
return join6(codexHome ?? join6(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
|
|
6596
6608
|
case "cursor":
|
|
6597
6609
|
return join6(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
|
|
6598
6610
|
case "grok":
|
|
@@ -6610,10 +6622,10 @@ function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
|
|
|
6610
6622
|
return void 0;
|
|
6611
6623
|
}
|
|
6612
6624
|
}
|
|
6613
|
-
function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
|
|
6625
|
+
function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6614
6626
|
switch (agent) {
|
|
6615
6627
|
case "claude":
|
|
6616
|
-
return [{ path: join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
|
|
6628
|
+
return [{ path: projectDir ? join6(projectDir, ".mcp.json") : join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
|
|
6617
6629
|
case "claude-desktop":
|
|
6618
6630
|
return [{
|
|
6619
6631
|
path: claudeDesktopConfigPath(homeDir),
|
|
@@ -6621,7 +6633,7 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
|
|
|
6621
6633
|
rootKey: "mcpServers"
|
|
6622
6634
|
}];
|
|
6623
6635
|
case "codex":
|
|
6624
|
-
return [{ path: join6(homeDir, ".codex", "config.toml"), format: "toml" }];
|
|
6636
|
+
return [{ path: join6(codexHome ?? join6(homeDir, ".codex"), "config.toml"), format: "toml" }];
|
|
6625
6637
|
case "cursor":
|
|
6626
6638
|
return [{
|
|
6627
6639
|
path: projectDir ? join6(projectDir, ".cursor", "mcp.json") : join6(homeDir, ".cursor", "mcp.json"),
|
|
@@ -6702,8 +6714,36 @@ function agentCliNames(agent) {
|
|
|
6702
6714
|
return ["qchat", "q"];
|
|
6703
6715
|
}
|
|
6704
6716
|
}
|
|
6705
|
-
function agentConfigExists(agent, homeDir = homedir4(), projectDir) {
|
|
6706
|
-
return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync4(config.path));
|
|
6717
|
+
function agentConfigExists(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6718
|
+
return agentConfigPaths(agent, homeDir, projectDir, codexHome).some((config) => existsSync4(config.path));
|
|
6719
|
+
}
|
|
6720
|
+
async function assertClaudeProjectScope(homeDir, projectDir, name) {
|
|
6721
|
+
const path = join6(homeDir, ".claude.json");
|
|
6722
|
+
let contents;
|
|
6723
|
+
try {
|
|
6724
|
+
contents = await readFile4(path, "utf8");
|
|
6725
|
+
} catch (error) {
|
|
6726
|
+
if (error.code === "ENOENT") return;
|
|
6727
|
+
throw error;
|
|
6728
|
+
}
|
|
6729
|
+
let config;
|
|
6730
|
+
try {
|
|
6731
|
+
config = JSON.parse(contents);
|
|
6732
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error();
|
|
6733
|
+
} catch {
|
|
6734
|
+
throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
|
|
6735
|
+
}
|
|
6736
|
+
const object = (value) => {
|
|
6737
|
+
if (value === void 0) return void 0;
|
|
6738
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6739
|
+
throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
|
|
6740
|
+
}
|
|
6741
|
+
return value;
|
|
6742
|
+
};
|
|
6743
|
+
const local = object(object(config.projects)?.[resolve2(projectDir)]);
|
|
6744
|
+
if (Object.hasOwn(object(config.mcpServers) ?? {}, name) || Object.hasOwn(object(local?.mcpServers) ?? {}, name)) {
|
|
6745
|
+
throw new Error("Claude MCP scope conflict for " + name + ": a user or local definition can override the project registration. Resolve that scope before setup.");
|
|
6746
|
+
}
|
|
6707
6747
|
}
|
|
6708
6748
|
function claudeDesktopConfigPath(homeDir) {
|
|
6709
6749
|
if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
@@ -6715,9 +6755,9 @@ function vscodeUserConfigPath(homeDir) {
|
|
|
6715
6755
|
if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
|
|
6716
6756
|
return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
|
|
6717
6757
|
}
|
|
6718
|
-
async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir) {
|
|
6758
|
+
async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6719
6759
|
const registrations = [];
|
|
6720
|
-
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
6760
|
+
for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
|
|
6721
6761
|
if (!existsSync4(config.path)) continue;
|
|
6722
6762
|
if (config.format === "json") {
|
|
6723
6763
|
registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
|
|
@@ -6982,17 +7022,19 @@ function message(error) {
|
|
|
6982
7022
|
|
|
6983
7023
|
// src/commands/agent-config.ts
|
|
6984
7024
|
import { existsSync as existsSync5 } from "fs";
|
|
6985
|
-
import { mkdir as mkdir3, readFile as readFile5, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
7025
|
+
import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
7026
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6986
7027
|
import { dirname as dirname5 } from "path";
|
|
6987
|
-
async function installAgentSkill(agent, source, homeDir, projectDir) {
|
|
6988
|
-
const path = agentSkillPath(agent, homeDir, projectDir);
|
|
7028
|
+
async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
|
|
7029
|
+
const path = agentSkillPath(agent, homeDir, projectDir, codexHome);
|
|
6989
7030
|
if (!path) return { installed: false };
|
|
6990
7031
|
const contents = formatSkillForAgent(agent, source);
|
|
6991
7032
|
await atomicWrite(path, contents);
|
|
6992
7033
|
return { path, installed: true };
|
|
6993
7034
|
}
|
|
6994
|
-
async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir) {
|
|
6995
|
-
|
|
7035
|
+
async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome) {
|
|
7036
|
+
if (agent === "claude" && projectDir) await assertClaudeProjectScope(homeDir, projectDir, registration.name);
|
|
7037
|
+
for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
|
|
6996
7038
|
if (config.format === "json") {
|
|
6997
7039
|
await registerJson(config, agent, mcp, registration);
|
|
6998
7040
|
} else if (config.format === "toml") {
|
|
@@ -7002,8 +7044,8 @@ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir
|
|
|
7002
7044
|
}
|
|
7003
7045
|
}
|
|
7004
7046
|
}
|
|
7005
|
-
async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
|
|
7006
|
-
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
7047
|
+
async function unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome) {
|
|
7048
|
+
for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
|
|
7007
7049
|
if (!existsSync5(config.path)) continue;
|
|
7008
7050
|
if (config.format === "json") {
|
|
7009
7051
|
await unregisterJson(config, agent, name);
|
|
@@ -7016,18 +7058,39 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
|
|
|
7016
7058
|
}
|
|
7017
7059
|
}
|
|
7018
7060
|
function registrationEnvironment(agent, registration) {
|
|
7061
|
+
let privateAliases = {};
|
|
7062
|
+
if (registration.configPath) {
|
|
7063
|
+
const prefixes = {
|
|
7064
|
+
production: "TOPOLO_PRODUCTION",
|
|
7065
|
+
staging: "TOPOLO_STAGING",
|
|
7066
|
+
development: "TOPOLO_DEVELOPMENT",
|
|
7067
|
+
local: "TOPOLO_LOCAL"
|
|
7068
|
+
};
|
|
7069
|
+
const prefix = prefixes[registration.environment];
|
|
7070
|
+
if (!prefix) throw new Error("Cannot isolate a private profile for an unknown environment.");
|
|
7071
|
+
privateAliases = {
|
|
7072
|
+
TOPOLO_API_KEY: "",
|
|
7073
|
+
TOPOLO_ACCESS_TOKEN: "",
|
|
7074
|
+
[prefix + "_API_KEY"]: "",
|
|
7075
|
+
[prefix + "_ACCESS_TOKEN"]: "",
|
|
7076
|
+
TOPOLO_ENV: registration.environment
|
|
7077
|
+
};
|
|
7078
|
+
}
|
|
7019
7079
|
return {
|
|
7020
7080
|
TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
|
|
7021
7081
|
...registration.configPath ? { TOPOLO_CONFIG_PATH: registration.configPath } : {},
|
|
7022
7082
|
...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
|
|
7023
|
-
...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
|
|
7083
|
+
...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {},
|
|
7084
|
+
...privateAliases
|
|
7024
7085
|
};
|
|
7025
7086
|
}
|
|
7026
7087
|
async function registerJson(config, agent, mcp, registration) {
|
|
7088
|
+
const before = await fileSnapshot(config.path);
|
|
7027
7089
|
let parsed = {};
|
|
7028
|
-
if (
|
|
7090
|
+
if (before.contents !== void 0) {
|
|
7029
7091
|
try {
|
|
7030
|
-
parsed = JSON.parse(
|
|
7092
|
+
parsed = JSON.parse(before.contents);
|
|
7093
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected an object.");
|
|
7031
7094
|
} catch (error) {
|
|
7032
7095
|
throw new Error(
|
|
7033
7096
|
"Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
|
|
@@ -7048,7 +7111,7 @@ async function registerJson(config, agent, mcp, registration) {
|
|
|
7048
7111
|
if (agent === "claude" || agent === "claude-desktop" || rootKey === "servers") entry.type = "stdio";
|
|
7049
7112
|
servers[registration.name] = entry;
|
|
7050
7113
|
parsed[rootKey] = servers;
|
|
7051
|
-
await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
|
|
7114
|
+
await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n", before);
|
|
7052
7115
|
}
|
|
7053
7116
|
async function unregisterJson(config, agent, name) {
|
|
7054
7117
|
let parsed;
|
|
@@ -7070,13 +7133,14 @@ async function unregisterJson(config, agent, name) {
|
|
|
7070
7133
|
await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
|
|
7071
7134
|
}
|
|
7072
7135
|
async function registerToml(path, agent, mcp, registration) {
|
|
7073
|
-
const
|
|
7136
|
+
const before = await fileSnapshot(path);
|
|
7137
|
+
const existing = before.contents ?? "";
|
|
7074
7138
|
const text2 = withoutMcpServerSections(existing, registration.name);
|
|
7075
7139
|
const args = mcp.args.length ? "\nargs = [" + mcp.args.map((item) => JSON.stringify(item)).join(", ") + "]" : "";
|
|
7076
7140
|
const environment = registrationEnvironment(agent, registration);
|
|
7077
7141
|
const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
|
|
7078
7142
|
const block = agent === "grok" ? "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\nenv = { " + environmentText + " }\n" : "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\n\n[mcp_servers." + registration.name + ".env]\n" + Object.entries(environment).map(([key, value]) => key + " = " + JSON.stringify(value)).join("\n") + "\n";
|
|
7079
|
-
await atomicWrite(path, text2 + block);
|
|
7143
|
+
await atomicWrite(path, text2 + block, before);
|
|
7080
7144
|
}
|
|
7081
7145
|
async function registerGoose(path, mcp, registration) {
|
|
7082
7146
|
const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
|
|
@@ -7146,11 +7210,82 @@ function formatSkillForAgent(agent, source) {
|
|
|
7146
7210
|
}
|
|
7147
7211
|
return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
|
|
7148
7212
|
}
|
|
7149
|
-
|
|
7213
|
+
function fileIdentity(stat2) {
|
|
7214
|
+
return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeMs, stat2.ctimeMs, stat2.mode, stat2.nlink].join(":");
|
|
7215
|
+
}
|
|
7216
|
+
async function fileSnapshot(path) {
|
|
7217
|
+
const parents = [];
|
|
7218
|
+
for (let parent = dirname5(path); ; parent = dirname5(parent)) {
|
|
7219
|
+
try {
|
|
7220
|
+
const stat2 = await lstat2(parent);
|
|
7221
|
+
if (!stat2.isDirectory() || stat2.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
|
|
7222
|
+
parents.push({ path: parent, identity: [stat2.dev, stat2.ino].join(":") });
|
|
7223
|
+
} catch (error) {
|
|
7224
|
+
if (error.code !== "ENOENT") throw error;
|
|
7225
|
+
}
|
|
7226
|
+
if (dirname5(parent) === parent) break;
|
|
7227
|
+
}
|
|
7228
|
+
try {
|
|
7229
|
+
const stat2 = await lstat2(path);
|
|
7230
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
|
|
7231
|
+
const contents = await readFile5(path, "utf8");
|
|
7232
|
+
if (fileIdentity(await lstat2(path)) !== fileIdentity(stat2)) throw new Error("Host configuration changed while reading at " + path + ".");
|
|
7233
|
+
return { contents, identity: fileIdentity(stat2), parents };
|
|
7234
|
+
} catch (error) {
|
|
7235
|
+
if (error.code === "ENOENT") return { parents };
|
|
7236
|
+
throw error;
|
|
7237
|
+
}
|
|
7238
|
+
}
|
|
7239
|
+
async function assertSnapshot(path, before) {
|
|
7240
|
+
const current = await fileSnapshot(path);
|
|
7241
|
+
if (current.contents !== before.contents || current.identity !== before.identity || before.parents.some((parent) => !current.parents.some((candidate) => candidate.path === parent.path && candidate.identity === parent.identity))) {
|
|
7242
|
+
throw new Error("Host destination changed before write at " + path + ".");
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
async function atomicWrite(path, contents, before) {
|
|
7246
|
+
before ??= await fileSnapshot(path);
|
|
7247
|
+
await assertSnapshot(path, before);
|
|
7248
|
+
if (before.contents === contents) return;
|
|
7150
7249
|
await mkdir3(dirname5(path), { recursive: true });
|
|
7151
|
-
const
|
|
7152
|
-
await
|
|
7153
|
-
|
|
7250
|
+
const destination = await fileSnapshot(path);
|
|
7251
|
+
await assertSnapshot(path, before);
|
|
7252
|
+
const temporaryPath = path + ".topolo-" + randomUUID3() + ".tmp";
|
|
7253
|
+
const handle = await open(temporaryPath, "wx", 384);
|
|
7254
|
+
let temporaryIdentity;
|
|
7255
|
+
let handleClosed = false;
|
|
7256
|
+
let temporaryPending = true;
|
|
7257
|
+
try {
|
|
7258
|
+
const created = await handle.stat();
|
|
7259
|
+
temporaryIdentity = [created.dev, created.ino].join(":");
|
|
7260
|
+
await assertSnapshot(path, destination);
|
|
7261
|
+
await writeFile3(handle, contents, { encoding: "utf8" });
|
|
7262
|
+
await handle.close();
|
|
7263
|
+
handleClosed = true;
|
|
7264
|
+
await assertSnapshot(path, destination);
|
|
7265
|
+
const temporary = await lstat2(temporaryPath);
|
|
7266
|
+
if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.nlink !== 1 || [temporary.dev, temporary.ino].join(":") !== temporaryIdentity) {
|
|
7267
|
+
throw new Error("Host temporary file custody changed before rename at " + path + ".");
|
|
7268
|
+
}
|
|
7269
|
+
await rename2(temporaryPath, path);
|
|
7270
|
+
temporaryPending = false;
|
|
7271
|
+
const installed = await fileSnapshot(path);
|
|
7272
|
+
if (installed.contents !== contents) throw new Error("Host configuration read-back failed at " + path + ".");
|
|
7273
|
+
} finally {
|
|
7274
|
+
if (!handleClosed) await handle.close();
|
|
7275
|
+
if (temporaryPending && temporaryIdentity) {
|
|
7276
|
+
let cleanupOwned = false;
|
|
7277
|
+
try {
|
|
7278
|
+
const parentsOwned = await Promise.all(destination.parents.map(async (parent) => {
|
|
7279
|
+
const current2 = await lstat2(parent.path);
|
|
7280
|
+
return current2.isDirectory() && !current2.isSymbolicLink() && [current2.dev, current2.ino].join(":") === parent.identity;
|
|
7281
|
+
}));
|
|
7282
|
+
const current = await lstat2(temporaryPath);
|
|
7283
|
+
cleanupOwned = parentsOwned.every(Boolean) && current.isFile() && !current.isSymbolicLink() && current.nlink === 1 && [current.dev, current.ino].join(":") === temporaryIdentity;
|
|
7284
|
+
} catch {
|
|
7285
|
+
}
|
|
7286
|
+
if (cleanupOwned) await unlink(temporaryPath);
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7154
7289
|
}
|
|
7155
7290
|
function withoutMcpServerSections(text2, name) {
|
|
7156
7291
|
const section = "[mcp_servers." + name + "]";
|
|
@@ -7171,8 +7306,8 @@ function withoutMcpServerSections(text2, name) {
|
|
|
7171
7306
|
var AGENT_LABELS2 = AGENT_LABELS;
|
|
7172
7307
|
function skillsSourceDir() {
|
|
7173
7308
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
7174
|
-
const distCandidate =
|
|
7175
|
-
const srcCandidate =
|
|
7309
|
+
const distCandidate = resolve3(here, "../skills");
|
|
7310
|
+
const srcCandidate = resolve3(here, "../../skills");
|
|
7176
7311
|
for (const candidate of [distCandidate, srcCandidate]) {
|
|
7177
7312
|
if (existsSync6(join7(candidate, "topolo", "SKILL.md"))) return candidate;
|
|
7178
7313
|
}
|
|
@@ -7224,14 +7359,22 @@ function resolveMcpRegistration(options) {
|
|
|
7224
7359
|
};
|
|
7225
7360
|
}
|
|
7226
7361
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
7362
|
+
const { homeDir, projectDir, codexHome } = opts.target;
|
|
7363
|
+
if (opts.withMcp && agent === "claude" && projectDir) {
|
|
7364
|
+
try {
|
|
7365
|
+
await assertClaudeProjectScope(homeDir, projectDir, opts.registration.name);
|
|
7366
|
+
} catch (err) {
|
|
7367
|
+
return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7227
7370
|
const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
|
|
7228
|
-
const skill = await installAgentSkill(agent, source,
|
|
7371
|
+
const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
|
|
7229
7372
|
if (!opts.withMcp) {
|
|
7230
7373
|
return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
|
|
7231
7374
|
}
|
|
7232
7375
|
try {
|
|
7233
|
-
await registerAgentConfig(agent, mcp,
|
|
7234
|
-
const installed = (await readAgentRegistrations(agent,
|
|
7376
|
+
await registerAgentConfig(agent, mcp, homeDir, opts.registration, projectDir, codexHome);
|
|
7377
|
+
const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((registration) => registration.name === opts.registration.name);
|
|
7235
7378
|
const expectedEnvironment = registrationEnvironment(agent, opts.registration);
|
|
7236
7379
|
if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
|
|
7237
7380
|
throw new Error(`Host configuration did not retain the ${opts.registration.name} MCP registration.`);
|
|
@@ -7265,12 +7408,12 @@ function resolveTargets(opts) {
|
|
|
7265
7408
|
if (opts.all) return [...SUPPORTED_AGENT_IDS];
|
|
7266
7409
|
const homeDir = opts.homeDir ?? homedir5();
|
|
7267
7410
|
const detectBinaries = homeDir === homedir5();
|
|
7268
|
-
const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
|
|
7411
|
+
const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project, opts.codexHome) || detectBinaries && agentBinaryExists(agent));
|
|
7269
7412
|
return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
|
|
7270
7413
|
}
|
|
7271
7414
|
function registerSkillsCommand(program2) {
|
|
7272
7415
|
const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
|
|
7273
|
-
skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
|
|
7416
|
+
skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
|
|
7274
7417
|
async (opts) => {
|
|
7275
7418
|
const mode = resolveOutputMode(opts.json);
|
|
7276
7419
|
try {
|
|
@@ -7286,11 +7429,12 @@ function registerSkillsCommand(program2) {
|
|
|
7286
7429
|
}
|
|
7287
7430
|
}
|
|
7288
7431
|
);
|
|
7289
|
-
skills.command("status").description("List public Topolo skill and MCP registrations for each agent").option("--agent <host...>", "Inspect one or more hosts by id").option("--all", "Inspect every supported host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--name <name>", "Inspect one registration name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7432
|
+
skills.command("status").description("List public Topolo skill and MCP registrations for each agent").option("--agent <host...>", "Inspect one or more hosts by id").option("--all", "Inspect every supported host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Inspect an alternate agent home").option("--name <name>", "Inspect one registration name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7290
7433
|
const mode = resolveOutputMode(opts.json);
|
|
7291
7434
|
try {
|
|
7292
7435
|
const rows = await agentSurfaceStatus({
|
|
7293
|
-
homeDir: opts.agentHome ?
|
|
7436
|
+
homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0,
|
|
7437
|
+
codexHome: opts.codexHome,
|
|
7294
7438
|
name: opts.name,
|
|
7295
7439
|
agent: opts.agent,
|
|
7296
7440
|
all: opts.all,
|
|
@@ -7301,12 +7445,12 @@ function registerSkillsCommand(program2) {
|
|
|
7301
7445
|
fail2(err, mode);
|
|
7302
7446
|
}
|
|
7303
7447
|
});
|
|
7304
|
-
skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7448
|
+
skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7305
7449
|
const mode = resolveOutputMode(opts.json);
|
|
7306
7450
|
try {
|
|
7307
7451
|
const removed = await uninstallAgentSurfaces({
|
|
7308
7452
|
...opts,
|
|
7309
|
-
homeDir: opts.agentHome ?
|
|
7453
|
+
homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0
|
|
7310
7454
|
});
|
|
7311
7455
|
emit(
|
|
7312
7456
|
{ removed },
|
|
@@ -7325,20 +7469,20 @@ async function installAgentSurfaces(opts) {
|
|
|
7325
7469
|
}
|
|
7326
7470
|
const mcp = resolveMcpCommand();
|
|
7327
7471
|
const registration = resolveMcpRegistration(opts);
|
|
7328
|
-
const
|
|
7472
|
+
const target = resolveAgentHostTarget({ homeDir: opts.homeDir, projectDir: opts.project, codexHome: opts.codexHome });
|
|
7473
|
+
const { homeDir, projectDir, codexHome } = target;
|
|
7329
7474
|
const results = [];
|
|
7330
|
-
for (const agent of resolveTargets({ ...opts, homeDir, project:
|
|
7475
|
+
for (const agent of resolveTargets({ ...opts, homeDir, project: projectDir, codexHome })) {
|
|
7331
7476
|
results.push(await installForAgent(agent, sourceDir, mcp, {
|
|
7332
7477
|
withMcp: opts.mcp !== false,
|
|
7333
|
-
|
|
7334
|
-
homeDir,
|
|
7478
|
+
target,
|
|
7335
7479
|
registration
|
|
7336
7480
|
}));
|
|
7337
7481
|
}
|
|
7338
7482
|
return { results, mcp };
|
|
7339
7483
|
}
|
|
7340
7484
|
function registerSetupCommand(program2) {
|
|
7341
|
-
program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7485
|
+
program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7342
7486
|
const mode = resolveOutputMode(opts.json);
|
|
7343
7487
|
try {
|
|
7344
7488
|
const { results, mcp } = await installAgentSurfaces({
|
|
@@ -7360,18 +7504,20 @@ function assertAgentSetupSucceeded(results) {
|
|
|
7360
7504
|
}
|
|
7361
7505
|
}
|
|
7362
7506
|
async function agentSurfaceStatus(options = {}) {
|
|
7363
|
-
const
|
|
7507
|
+
const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
|
|
7508
|
+
const { homeDir, projectDir, codexHome } = target;
|
|
7364
7509
|
const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
|
|
7365
7510
|
const agents = resolveTargets({
|
|
7366
7511
|
agent: options.agent,
|
|
7367
7512
|
all: options.all,
|
|
7368
7513
|
homeDir,
|
|
7369
|
-
project:
|
|
7514
|
+
project: projectDir,
|
|
7515
|
+
codexHome
|
|
7370
7516
|
});
|
|
7371
7517
|
return Promise.all(agents.map(async (agent) => {
|
|
7372
|
-
const skillPath = agentSkillPath(agent, homeDir,
|
|
7518
|
+
const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
|
|
7373
7519
|
const installedSkill = skillPath && existsSync6(skillPath) ? await readFile6(skillPath, "utf8") : void 0;
|
|
7374
|
-
const registrations = (await readAgentRegistrations(agent, homeDir,
|
|
7520
|
+
const registrations = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).filter((registration) => !options.name || registration.name === options.name);
|
|
7375
7521
|
return {
|
|
7376
7522
|
agent,
|
|
7377
7523
|
skillInstalled: installedSkill !== void 0,
|
|
@@ -7384,16 +7530,17 @@ async function agentSurfaceStatus(options = {}) {
|
|
|
7384
7530
|
}));
|
|
7385
7531
|
}
|
|
7386
7532
|
async function uninstallAgentSurfaces(options) {
|
|
7387
|
-
const
|
|
7533
|
+
const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
|
|
7534
|
+
const { homeDir, projectDir, codexHome } = target;
|
|
7388
7535
|
const name = options.name?.trim() || "topolo";
|
|
7389
7536
|
resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
|
|
7390
7537
|
const removed = [];
|
|
7391
|
-
for (const agent of resolveTargets({ ...options, homeDir, project:
|
|
7392
|
-
const before = await readAgentRegistrations(agent, homeDir,
|
|
7538
|
+
for (const agent of resolveTargets({ ...options, homeDir, project: projectDir, codexHome })) {
|
|
7539
|
+
const before = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
|
|
7393
7540
|
const registrationRemoved = before.some((entry) => entry.name === name);
|
|
7394
|
-
await unregisterAgentConfig(agent, homeDir, name,
|
|
7395
|
-
const remaining = await readAgentRegistrations(agent, homeDir,
|
|
7396
|
-
const skillPath = agentSkillPath(agent, homeDir,
|
|
7541
|
+
await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
|
|
7542
|
+
const remaining = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
|
|
7543
|
+
const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
|
|
7397
7544
|
const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
|
|
7398
7545
|
if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
|
|
7399
7546
|
removed.push({ agent, name, registrationRemoved, skillRemoved });
|
|
@@ -7426,7 +7573,7 @@ function formatStatus(rows) {
|
|
|
7426
7573
|
// src/commands/doctor.ts
|
|
7427
7574
|
import { access } from "fs/promises";
|
|
7428
7575
|
import { homedir as homedir6 } from "os";
|
|
7429
|
-
import { resolve as
|
|
7576
|
+
import { resolve as resolve4 } from "path";
|
|
7430
7577
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7431
7578
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7432
7579
|
var defaultDependencies = {
|
|
@@ -7488,7 +7635,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
|
|
|
7488
7635
|
}
|
|
7489
7636
|
async function resolveInstalledDoctorTarget(options) {
|
|
7490
7637
|
const name = options.name?.trim() || "topolo";
|
|
7491
|
-
const homeDir = options.agentHome ?
|
|
7638
|
+
const homeDir = options.agentHome ? resolve4(options.agentHome) : homedir6();
|
|
7492
7639
|
const expected = resolveMcpCommand();
|
|
7493
7640
|
const requestedAgents = resolveTargets({
|
|
7494
7641
|
...options,
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.13.
|
|
5
|
-
"topoloPublishedVersion": "0.13.
|
|
4
|
+
"version": "0.13.9",
|
|
5
|
+
"topoloPublishedVersion": "0.13.9",
|
|
6
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|