impel-cli 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/package.json +1 -1
- package/src/agents.js +142 -91
- package/src/cli.js +1 -1
- package/src/commands/agents.js +5 -4
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ impel use account [claude|codex|all] Revert to your own login (defaul
|
|
|
148
148
|
impel off [claude|codex|all] Alias for `impel use account`
|
|
149
149
|
|
|
150
150
|
impel skills sync [claude|codex|all] Sync Bifrost shared skills into managed clients (default: all)
|
|
151
|
-
impel agents sync [claude|codex|all] Sync
|
|
151
|
+
impel agents sync [claude|codex|all] Sync explicit tenant agents into native clients
|
|
152
152
|
|
|
153
153
|
impel status Launcher readiness + native mode + gateway reachability
|
|
154
154
|
impel app install [target] [--tenant <org>] Install isolated apps for one tenant
|
|
@@ -591,15 +591,17 @@ Set `IMPEL_SKIP_SKILL_SYNC=1` to disable it entirely (offline/CI).
|
|
|
591
591
|
|
|
592
592
|
### `impel agents sync [claude|codex|all]`
|
|
593
593
|
|
|
594
|
-
Fetches the selected organization's
|
|
594
|
+
Fetches the selected organization's explicit native-agent catalog through the
|
|
595
595
|
authenticated Impel MCP endpoint, then generates native custom-agent files for
|
|
596
|
-
every managed profile:
|
|
596
|
+
every managed profile. This catalog is separate from automatic specialists: it
|
|
597
|
+
may include truthfully declared write-capable agents, but those agents require
|
|
598
|
+
explicit user selection before the server will start a run.
|
|
597
599
|
|
|
598
600
|
- Claude Code: `<CLAUDE_CONFIG_DIR>/agents/impel-managed/*.md`
|
|
599
601
|
- Codex / ChatGPT Codex mode: `<CODEX_HOME>/agents/impel-managed/*.toml`
|
|
600
602
|
|
|
601
|
-
Each generated agent is bound to one exact
|
|
602
|
-
`scopeParam`. It verifies that
|
|
603
|
+
Each generated agent is bound to one exact `agentId` and
|
|
604
|
+
`scopeParam`. It verifies that the exact agent is still available, starts one
|
|
603
605
|
idempotent run, polls the durable run to a terminal state, and returns
|
|
604
606
|
`result.finalText` without repeating the work. The files include a tenant-bound
|
|
605
607
|
MCP subprocess definition so they also work in isolated desktop profiles. They
|
|
@@ -627,11 +629,12 @@ Invocation uses the clients' native agent behavior:
|
|
|
627
629
|
claude --agent impel-acme-research-agent -p "investigate the dependency change"
|
|
628
630
|
|
|
629
631
|
# Codex CLI / ChatGPT desktop Codex task
|
|
630
|
-
|
|
632
|
+
Explicitly use the configured impel-acme-research-agent for this request and wait for it.
|
|
631
633
|
```
|
|
632
634
|
|
|
633
635
|
Claude exposes native custom agents in its `@` typeahead. Current Codex releases
|
|
634
|
-
load
|
|
636
|
+
load the generated standalone definitions directly from `$CODEX_HOME/agents/`
|
|
637
|
+
but do not provide an `@agent`
|
|
635
638
|
mention target; named spawning remains model-mediated and may also be limited by
|
|
636
639
|
the active MultiAgentV2 tool schema. Use `/agent` in Codex to inspect spawned
|
|
637
640
|
agent threads.
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Synchronizes tenant-scoped Impel
|
|
2
|
-
// registries used by Claude Code and Codex. Unlike the public
|
|
3
|
-
// marketplace, the
|
|
4
|
-
// specific, so it is fetched through the selected tenant's MCP
|
|
1
|
+
// Synchronizes explicitly invocable tenant-scoped Impel agents into the native
|
|
2
|
+
// custom-agent registries used by Claude Code and Codex. Unlike the public
|
|
3
|
+
// shared-skill marketplace, the native-agent catalog is authenticated and
|
|
4
|
+
// organization specific, so it is fetched through the selected tenant's MCP
|
|
5
|
+
// session. Automatic read-only specialists remain a separate catalog.
|
|
5
6
|
|
|
6
7
|
import crypto from "node:crypto";
|
|
7
8
|
import fs from "node:fs";
|
|
@@ -15,17 +16,17 @@ import { normalizeTenantId } from "./tenants.js";
|
|
|
15
16
|
export const AGENT_SYNC_TTL_MS = 6 * 60 * 60 * 1000;
|
|
16
17
|
export const MANAGED_AGENT_DIRECTORY = "impel-managed";
|
|
17
18
|
export const MANAGED_AGENT_MANIFEST = ".manifest.json";
|
|
18
|
-
export const
|
|
19
|
-
export const
|
|
20
|
-
export const
|
|
19
|
+
export const NATIVE_AGENT_LIST_TOOL = "impel_specialists-list_native_agents";
|
|
20
|
+
export const NATIVE_AGENT_START_TOOL = "impel_specialists-start_native_agent_run";
|
|
21
|
+
export const NATIVE_AGENT_READ_TOOL = "impel_specialists-read_native_agent_run";
|
|
21
22
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const NATIVE_AGENT_TOOL_NAMES = [
|
|
25
|
+
NATIVE_AGENT_LIST_TOOL,
|
|
26
|
+
NATIVE_AGENT_START_TOOL,
|
|
27
|
+
NATIVE_AGENT_READ_TOOL,
|
|
27
28
|
];
|
|
28
|
-
const
|
|
29
|
+
const SAFE_AGENT_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
29
30
|
const SAFE_SCOPE_PARAM_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
30
31
|
const MAX_CATALOG_ITEMS = 500;
|
|
31
32
|
|
|
@@ -63,61 +64,73 @@ function atomicPrivateWrite(filePath, contents) {
|
|
|
63
64
|
|
|
64
65
|
function boundedString(value, field, { max = 1024, pattern = null } = {}) {
|
|
65
66
|
if (typeof value !== "string" || !value.trim()) {
|
|
66
|
-
throw new Error(`
|
|
67
|
+
throw new Error(`native-agent catalog returned an invalid ${field}`);
|
|
67
68
|
}
|
|
68
69
|
const normalized = value.trim();
|
|
69
70
|
if (normalized.length > max || (pattern && !pattern.test(normalized))) {
|
|
70
|
-
throw new Error(`
|
|
71
|
+
throw new Error(`native-agent catalog returned an invalid ${field}`);
|
|
71
72
|
}
|
|
72
73
|
return normalized;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function stringList(value, field) {
|
|
76
77
|
if (!Array.isArray(value) || value.length > 100) {
|
|
77
|
-
throw new Error(`
|
|
78
|
+
throw new Error(`native-agent catalog returned an invalid ${field}`);
|
|
78
79
|
}
|
|
79
80
|
return value.map((item) => boundedString(item, field, { max: 512 }));
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
function enumString(value, field, allowed) {
|
|
84
|
+
const normalized = boundedString(value, field, { max: 80 });
|
|
85
|
+
if (!allowed.includes(normalized)) {
|
|
86
|
+
throw new Error(`native-agent catalog returned an invalid ${field}`);
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizeNativeAgentCatalog(payload, expectedTenantId) {
|
|
83
92
|
const tenantId = normalizeTenantId(expectedTenantId);
|
|
84
93
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
85
|
-
throw new Error("
|
|
94
|
+
throw new Error("native-agent catalog returned an invalid response");
|
|
86
95
|
}
|
|
87
96
|
if (normalizeTenantId(payload.orgId) !== tenantId) {
|
|
88
|
-
throw new Error("
|
|
97
|
+
throw new Error("native-agent catalog returned the wrong organization");
|
|
89
98
|
}
|
|
90
|
-
if (!Array.isArray(payload.
|
|
91
|
-
throw new Error("
|
|
99
|
+
if (!Array.isArray(payload.agents) || payload.agents.length > MAX_CATALOG_ITEMS) {
|
|
100
|
+
throw new Error("native-agent catalog returned an invalid agents list");
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
if (!
|
|
97
|
-
throw new Error("
|
|
103
|
+
const seenBindings = new Set();
|
|
104
|
+
const agents = payload.agents.map((agent) => {
|
|
105
|
+
if (!agent || typeof agent !== "object" || Array.isArray(agent)) {
|
|
106
|
+
throw new Error("native-agent catalog returned an invalid agent");
|
|
98
107
|
}
|
|
99
|
-
const agentId = boundedString(
|
|
108
|
+
const agentId = boundedString(agent.agentId, "agentId", {
|
|
100
109
|
max: 160,
|
|
101
|
-
pattern:
|
|
110
|
+
pattern: SAFE_AGENT_ID_RE,
|
|
102
111
|
});
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
const scopeParam = boundedString(agent.scopeParam, "scopeParam", {
|
|
113
|
+
max: 160,
|
|
114
|
+
pattern: SAFE_SCOPE_PARAM_RE,
|
|
115
|
+
});
|
|
116
|
+
const binding = `${scopeParam}:${agentId}`;
|
|
117
|
+
if (seenBindings.has(binding)) {
|
|
118
|
+
throw new Error(`native-agent catalog returned duplicate binding ${binding}`);
|
|
119
|
+
}
|
|
120
|
+
seenBindings.add(binding);
|
|
105
121
|
return {
|
|
106
122
|
agentId,
|
|
107
|
-
title: boundedString(
|
|
108
|
-
description: boundedString(
|
|
109
|
-
scopeParam
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
exclusions: stringList(specialist.exclusions, "exclusions"),
|
|
116
|
-
requiredContext: stringList(specialist.requiredContext, "requiredContext"),
|
|
117
|
-
sideEffects: boundedString(specialist.sideEffects, "sideEffects", { max: 256 }),
|
|
123
|
+
title: boundedString(agent.title, "title", { max: 160 }),
|
|
124
|
+
description: boundedString(agent.description, "description", { max: 1024 }),
|
|
125
|
+
scopeParam,
|
|
126
|
+
provider: enumString(agent.provider, "provider", ["claude_code", "codex"]),
|
|
127
|
+
capabilities: stringList(agent.capabilities, "capabilities"),
|
|
128
|
+
exclusions: stringList(agent.exclusions, "exclusions"),
|
|
129
|
+
requiredContext: stringList(agent.requiredContext, "requiredContext"),
|
|
130
|
+
sideEffects: enumString(agent.sideEffects, "sideEffects", ["read-only", "writes"]),
|
|
118
131
|
};
|
|
119
132
|
});
|
|
120
|
-
return { orgId: tenantId,
|
|
133
|
+
return { orgId: tenantId, agents };
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
function decodeMcpResponse(contentType, body) {
|
|
@@ -151,13 +164,13 @@ async function postMcp({ endpoint, credential, sessionId, message, fetchImpl, ti
|
|
|
151
164
|
});
|
|
152
165
|
} catch (error) {
|
|
153
166
|
const detail = error?.name === "AbortError" ? "request timed out" : error?.message || error;
|
|
154
|
-
throw new Error(`
|
|
167
|
+
throw new Error(`native-agent MCP request failed: ${redactSecretText(detail)}`);
|
|
155
168
|
} finally {
|
|
156
169
|
clearTimeout(timeout);
|
|
157
170
|
}
|
|
158
171
|
const body = await response.text();
|
|
159
172
|
if (!response.ok) {
|
|
160
|
-
throw new Error(`
|
|
173
|
+
throw new Error(`native-agent MCP returned HTTP ${response.status}`);
|
|
161
174
|
}
|
|
162
175
|
return {
|
|
163
176
|
sessionId: response.headers.get("mcp-session-id") || sessionId || null,
|
|
@@ -169,26 +182,26 @@ function responseForId(messages, id) {
|
|
|
169
182
|
return messages.find((message) => message?.id === id) || null;
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
function
|
|
173
|
-
if (!message) throw new Error("
|
|
185
|
+
function nativeAgentPayloadFromToolResult(message) {
|
|
186
|
+
if (!message) throw new Error("native-agent MCP returned no tool result");
|
|
174
187
|
if (message.error) {
|
|
175
|
-
throw new Error(redactSecretText(message.error.message || "
|
|
188
|
+
throw new Error(redactSecretText(message.error.message || "native-agent MCP tool failed"));
|
|
176
189
|
}
|
|
177
190
|
const result = message.result;
|
|
178
|
-
if (result?.isError) throw new Error("
|
|
191
|
+
if (result?.isError) throw new Error("native-agent MCP list tool returned an error");
|
|
179
192
|
if (result?.structuredContent && typeof result.structuredContent === "object") {
|
|
180
193
|
return result.structuredContent;
|
|
181
194
|
}
|
|
182
195
|
const text = result?.content?.find((item) => item?.type === "text" && typeof item.text === "string")?.text;
|
|
183
|
-
if (!text) throw new Error("
|
|
196
|
+
if (!text) throw new Error("native-agent MCP list tool returned no catalog");
|
|
184
197
|
try {
|
|
185
198
|
return JSON.parse(text);
|
|
186
199
|
} catch {
|
|
187
|
-
throw new Error("
|
|
200
|
+
throw new Error("native-agent MCP list tool returned invalid JSON");
|
|
188
201
|
}
|
|
189
202
|
}
|
|
190
203
|
|
|
191
|
-
export async function
|
|
204
|
+
export async function fetchNativeAgentCatalog({
|
|
192
205
|
gatewayUrl,
|
|
193
206
|
credential,
|
|
194
207
|
tenantId,
|
|
@@ -218,7 +231,7 @@ export async function fetchSpecialistCatalog({
|
|
|
218
231
|
sessionId = initialized.sessionId;
|
|
219
232
|
const initializeMessage = responseForId(initialized.messages, 1);
|
|
220
233
|
if (initializeMessage?.error || !initializeMessage?.result) {
|
|
221
|
-
throw new Error("
|
|
234
|
+
throw new Error("native-agent MCP initialization failed");
|
|
222
235
|
}
|
|
223
236
|
|
|
224
237
|
const ready = await postMcp({
|
|
@@ -240,11 +253,11 @@ export async function fetchSpecialistCatalog({
|
|
|
240
253
|
jsonrpc: "2.0",
|
|
241
254
|
id: 2,
|
|
242
255
|
method: "tools/call",
|
|
243
|
-
params: { name:
|
|
256
|
+
params: { name: NATIVE_AGENT_LIST_TOOL, arguments: {} },
|
|
244
257
|
},
|
|
245
258
|
});
|
|
246
|
-
return
|
|
247
|
-
|
|
259
|
+
return normalizeNativeAgentCatalog(
|
|
260
|
+
nativeAgentPayloadFromToolResult(responseForId(listed.messages, 2)),
|
|
248
261
|
tenantId,
|
|
249
262
|
);
|
|
250
263
|
}
|
|
@@ -257,29 +270,36 @@ function slug(value) {
|
|
|
257
270
|
.slice(0, 48) || "agent";
|
|
258
271
|
}
|
|
259
272
|
|
|
260
|
-
function generatedAgentNames(tenantId,
|
|
273
|
+
function generatedAgentNames(tenantId, agents) {
|
|
261
274
|
const used = new Map();
|
|
262
|
-
return
|
|
263
|
-
const base = `impel-${slug(tenantId)}-${slug(
|
|
275
|
+
return agents.map((agent) => {
|
|
276
|
+
const base = `impel-${slug(tenantId)}-${slug(agent.agentId)}`.slice(0, 63).replace(/-+$/u, "");
|
|
264
277
|
const previous = used.get(base);
|
|
265
278
|
used.set(base, (previous || 0) + 1);
|
|
266
279
|
if (!previous) return base;
|
|
267
|
-
const suffix = crypto.createHash("sha256")
|
|
280
|
+
const suffix = crypto.createHash("sha256")
|
|
281
|
+
.update(`${agent.scopeParam}:${agent.agentId}`)
|
|
282
|
+
.digest("hex")
|
|
283
|
+
.slice(0, 8);
|
|
268
284
|
return `${base.slice(0, 54)}-${suffix}`;
|
|
269
285
|
});
|
|
270
286
|
}
|
|
271
287
|
|
|
272
|
-
function adapterInstructions(tenantId,
|
|
288
|
+
function adapterInstructions(tenantId, agent) {
|
|
273
289
|
const toolName = nativeToolName;
|
|
274
|
-
const contextRequirement =
|
|
275
|
-
? ` Required context keys are ${JSON.stringify(
|
|
290
|
+
const contextRequirement = agent.requiredContext.length
|
|
291
|
+
? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
|
|
276
292
|
: "";
|
|
293
|
+
const sideEffectInstruction = agent.sideEffects === "writes"
|
|
294
|
+
? `The catalog declares that this agent writes user or workspace data. Use this adapter only when the user explicitly selected this exact agent for the current request; that explicit selection is the required side-effect confirmation, so pass confirmedSideEffects true. If the agent was chosen automatically or the selection is ambiguous, do not start it and ask the user to select it explicitly.`
|
|
295
|
+
: `The catalog declares that this agent is read-only; omit confirmedSideEffects.`;
|
|
277
296
|
return [
|
|
278
|
-
`You are a thin transport adapter for the exact Impel
|
|
279
|
-
`Do not perform the assigned task yourself and do not delegate to any other
|
|
280
|
-
`First call ${toolName(
|
|
281
|
-
|
|
282
|
-
`
|
|
297
|
+
`You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
|
|
298
|
+
`Do not perform the assigned task yourself and do not delegate to any other agent.`,
|
|
299
|
+
`First call ${toolName(NATIVE_AGENT_LIST_TOOL)} and verify that the exact agentId is still available with sideEffects ${JSON.stringify(agent.sideEffects)}. If it is unavailable or its policy excludes the request, stop with that explicit error.`,
|
|
300
|
+
sideEffectInstruction,
|
|
301
|
+
`Call ${toolName(NATIVE_AGENT_START_TOOL)} exactly once with agentId ${JSON.stringify(agent.agentId)}, scopeParam ${JSON.stringify(agent.scopeParam)}, task set to the complete assigned task, context and contextKeys containing all supplied context, confirmedSideEffects as directed above, and one stable idempotencyKey that you reuse for this logical task.${contextRequirement}`,
|
|
302
|
+
`Then call ${toolName(NATIVE_AGENT_READ_TOOL)} with the returned runId and waitSeconds 20 until the run reaches a terminal state.`,
|
|
283
303
|
`When it succeeds, return result.finalText faithfully as the answer. When it fails, return the durable runId, preserved output, and error. Never invent or independently synthesize a replacement result.`,
|
|
284
304
|
].join(" ");
|
|
285
305
|
}
|
|
@@ -288,15 +308,15 @@ function nativeToolName(toolName) {
|
|
|
288
308
|
return `mcp__${MANAGED_AGENT_MCP_SERVER}__${toolName}`;
|
|
289
309
|
}
|
|
290
310
|
|
|
291
|
-
function renderClaudeAgent({ tenantId,
|
|
292
|
-
const description = `
|
|
311
|
+
function renderClaudeAgent({ tenantId, agent, name, invocation }) {
|
|
312
|
+
const description = `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)"}: ${agent.description}`.slice(0, 900);
|
|
293
313
|
const lines = [
|
|
294
314
|
"---",
|
|
295
315
|
`name: ${JSON.stringify(name)}`,
|
|
296
316
|
`description: ${JSON.stringify(description)}`,
|
|
297
317
|
"model: inherit",
|
|
298
318
|
"tools:",
|
|
299
|
-
...
|
|
319
|
+
...NATIVE_AGENT_TOOL_NAMES.map((tool) => ` - ${JSON.stringify(nativeToolName(tool))}`),
|
|
300
320
|
"mcpServers:",
|
|
301
321
|
` - ${MANAGED_AGENT_MCP_SERVER}:`,
|
|
302
322
|
" type: stdio",
|
|
@@ -307,14 +327,14 @@ function renderClaudeAgent({ tenantId, specialist, name, invocation }) {
|
|
|
307
327
|
...Object.entries(invocation.env || {}).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`),
|
|
308
328
|
"---",
|
|
309
329
|
"",
|
|
310
|
-
adapterInstructions(tenantId,
|
|
330
|
+
adapterInstructions(tenantId, agent),
|
|
311
331
|
"",
|
|
312
332
|
];
|
|
313
333
|
return lines.join("\n");
|
|
314
334
|
}
|
|
315
335
|
|
|
316
|
-
function renderCodexAgent({ tenantId,
|
|
317
|
-
const description = `
|
|
336
|
+
function renderCodexAgent({ tenantId, agent, name, invocation }) {
|
|
337
|
+
const description = `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)"}: ${agent.description}`.slice(0, 900);
|
|
318
338
|
const envEntries = Object.entries(invocation.env || {})
|
|
319
339
|
.map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
|
|
320
340
|
.join(", ");
|
|
@@ -322,32 +342,32 @@ function renderCodexAgent({ tenantId, specialist, name, invocation }) {
|
|
|
322
342
|
`name = ${JSON.stringify(name)}`,
|
|
323
343
|
`description = ${JSON.stringify(description)}`,
|
|
324
344
|
'sandbox_mode = "read-only"',
|
|
325
|
-
`developer_instructions = ${JSON.stringify(adapterInstructions(tenantId,
|
|
345
|
+
`developer_instructions = ${JSON.stringify(adapterInstructions(tenantId, agent))}`,
|
|
326
346
|
"",
|
|
327
347
|
`[mcp_servers.${MANAGED_AGENT_MCP_SERVER}]`,
|
|
328
348
|
`command = ${JSON.stringify(invocation.command)}`,
|
|
329
349
|
`args = [${invocation.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
|
|
330
|
-
`enabled_tools = [${
|
|
350
|
+
`enabled_tools = [${NATIVE_AGENT_TOOL_NAMES.map((tool) => JSON.stringify(tool)).join(", ")}]`,
|
|
331
351
|
...(envEntries ? [`env = { ${envEntries} }`] : []),
|
|
332
352
|
"",
|
|
333
353
|
];
|
|
334
|
-
for (const tool of
|
|
354
|
+
for (const tool of NATIVE_AGENT_TOOL_NAMES) {
|
|
335
355
|
lines.push(`[mcp_servers.${MANAGED_AGENT_MCP_SERVER}.tools.${JSON.stringify(tool)}]`, 'approval_mode = "approve"', "");
|
|
336
356
|
}
|
|
337
357
|
return lines.join("\n");
|
|
338
358
|
}
|
|
339
359
|
|
|
340
|
-
export function renderManagedAgents(client, tenantId,
|
|
360
|
+
export function renderManagedAgents(client, tenantId, agents, invocation = impelMcpInvocation(["--tenant", tenantId])) {
|
|
341
361
|
if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
|
|
342
362
|
const normalizedTenant = normalizeTenantId(tenantId);
|
|
343
|
-
const names = generatedAgentNames(normalizedTenant,
|
|
344
|
-
return
|
|
363
|
+
const names = generatedAgentNames(normalizedTenant, agents);
|
|
364
|
+
return agents.map((agent, index) => {
|
|
345
365
|
const name = names[index];
|
|
346
366
|
const extension = client === "claude" ? ".md" : ".toml";
|
|
347
367
|
const contents = client === "claude"
|
|
348
|
-
? renderClaudeAgent({ tenantId: normalizedTenant,
|
|
349
|
-
: renderCodexAgent({ tenantId: normalizedTenant,
|
|
350
|
-
return { agentId:
|
|
368
|
+
? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation })
|
|
369
|
+
: renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation });
|
|
370
|
+
return { agentId: agent.agentId, name, fileName: `${name}${extension}`, contents };
|
|
351
371
|
});
|
|
352
372
|
}
|
|
353
373
|
|
|
@@ -378,17 +398,17 @@ function readManifest(manifestPath) {
|
|
|
378
398
|
|
|
379
399
|
function profileIsFresh(profile, tenantId, now, ttlMs) {
|
|
380
400
|
const manifest = readManifest(path.join(profile.root, "agents", MANAGED_AGENT_DIRECTORY, MANAGED_AGENT_MANIFEST));
|
|
381
|
-
if (!manifest || manifest.tenantId !== tenantId) return false;
|
|
401
|
+
if (!manifest || manifest.version !== 2 || manifest.tenantId !== tenantId) return false;
|
|
382
402
|
const syncedAt = Date.parse(manifest.syncedAt || "");
|
|
383
403
|
if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
|
|
384
404
|
return manifest.files.every((fileName) =>
|
|
385
405
|
typeof fileName === "string"
|
|
386
406
|
&& path.basename(fileName) === fileName
|
|
387
|
-
&& fs.existsSync(path.join(profile.root, "agents",
|
|
407
|
+
&& fs.existsSync(path.join(profile.root, "agents", fileName))
|
|
388
408
|
);
|
|
389
409
|
}
|
|
390
410
|
|
|
391
|
-
export function syncAgentProfile({ client, root, label, tenantId,
|
|
411
|
+
export function syncAgentProfile({ client, root, label, tenantId, agents, now = Date.now() }) {
|
|
392
412
|
const agentsDir = path.join(root, "agents");
|
|
393
413
|
for (const candidate of [root, agentsDir]) {
|
|
394
414
|
if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink()) {
|
|
@@ -399,10 +419,26 @@ export function syncAgentProfile({ client, root, label, tenantId, specialists, n
|
|
|
399
419
|
privateDirectory(managedDir);
|
|
400
420
|
const manifestPath = path.join(managedDir, MANAGED_AGENT_MANIFEST);
|
|
401
421
|
const prior = readManifest(manifestPath);
|
|
402
|
-
const rendered = renderManagedAgents(client, tenantId,
|
|
422
|
+
const rendered = renderManagedAgents(client, tenantId, agents);
|
|
423
|
+
const priorFiles = new Set(prior?.files || []);
|
|
424
|
+
const priorUsesDiscoveryRoot = prior?.version === 2;
|
|
403
425
|
|
|
426
|
+
// Native clients discover standalone definitions directly under `agents/`.
|
|
427
|
+
// Preflight every destination before writing so an unmanaged file with the
|
|
428
|
+
// same generated name is never overwritten.
|
|
404
429
|
for (const agent of rendered) {
|
|
405
|
-
|
|
430
|
+
const destination = path.join(agentsDir, agent.fileName);
|
|
431
|
+
if (fs.existsSync(destination)) {
|
|
432
|
+
if (!priorUsesDiscoveryRoot || !priorFiles.has(agent.fileName)) {
|
|
433
|
+
throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
|
|
434
|
+
}
|
|
435
|
+
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
436
|
+
throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const agent of rendered) {
|
|
441
|
+
atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
|
|
406
442
|
}
|
|
407
443
|
const currentFiles = new Set(rendered.map((agent) => agent.fileName));
|
|
408
444
|
for (const stale of prior?.files || []) {
|
|
@@ -412,16 +448,31 @@ export function syncAgentProfile({ client, root, label, tenantId, specialists, n
|
|
|
412
448
|
&& !currentFiles.has(stale)
|
|
413
449
|
&& (stale.endsWith(".md") || stale.endsWith(".toml"))
|
|
414
450
|
) {
|
|
415
|
-
fs.rmSync(path.join(managedDir, stale), { force: true });
|
|
451
|
+
fs.rmSync(path.join(priorUsesDiscoveryRoot ? agentsDir : managedDir, stale), { force: true });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (!priorUsesDiscoveryRoot) {
|
|
455
|
+
for (const legacy of prior?.files || []) {
|
|
456
|
+
if (
|
|
457
|
+
typeof legacy === "string"
|
|
458
|
+
&& path.basename(legacy) === legacy
|
|
459
|
+
&& (legacy.endsWith(".md") || legacy.endsWith(".toml"))
|
|
460
|
+
) {
|
|
461
|
+
fs.rmSync(path.join(managedDir, legacy), { force: true });
|
|
462
|
+
}
|
|
416
463
|
}
|
|
417
464
|
}
|
|
418
465
|
atomicPrivateWrite(manifestPath, `${JSON.stringify({
|
|
419
|
-
version:
|
|
466
|
+
version: 2,
|
|
420
467
|
tenantId,
|
|
421
468
|
client,
|
|
422
469
|
syncedAt: new Date(now).toISOString(),
|
|
423
470
|
files: [...currentFiles].sort(),
|
|
424
|
-
agents: rendered.map(({ agentId, name }) => ({
|
|
471
|
+
agents: rendered.map(({ agentId, name }, index) => ({
|
|
472
|
+
agentId,
|
|
473
|
+
scopeParam: agents[index].scopeParam,
|
|
474
|
+
name,
|
|
475
|
+
})),
|
|
425
476
|
}, null, 2)}\n`);
|
|
426
477
|
return { client, root, label, synced: true, count: rendered.length, files: [...currentFiles] };
|
|
427
478
|
}
|
|
@@ -434,7 +485,7 @@ export async function syncAgentProfiles({
|
|
|
434
485
|
staleOnly = false,
|
|
435
486
|
ttlMs = AGENT_SYNC_TTL_MS,
|
|
436
487
|
now = Date.now(),
|
|
437
|
-
fetchCatalog =
|
|
488
|
+
fetchCatalog = fetchNativeAgentCatalog,
|
|
438
489
|
logger = console,
|
|
439
490
|
}) {
|
|
440
491
|
if (process.env.IMPEL_SKIP_AGENT_SYNC === "1" || process.env.IMPEL_SKIP_AGENT_SYNC === "true") {
|
|
@@ -455,10 +506,10 @@ export async function syncAgentProfiles({
|
|
|
455
506
|
const result = syncAgentProfile({
|
|
456
507
|
...profile,
|
|
457
508
|
tenantId: normalizedTenant,
|
|
458
|
-
|
|
509
|
+
agents: catalog.agents,
|
|
459
510
|
now,
|
|
460
511
|
});
|
|
461
|
-
logger.log(`Agents: ${profile.label || profile.client} up to date (${result.count} tenant
|
|
512
|
+
logger.log(`Agents: ${profile.label || profile.client} up to date (${result.count} explicit tenant agent${result.count === 1 ? "" : "s"}).`);
|
|
462
513
|
results.push(result);
|
|
463
514
|
}
|
|
464
515
|
return [...results, ...skipped];
|
package/src/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ Manage:
|
|
|
44
44
|
impel token [--tenant <org>] Print the selected-tenant bearer
|
|
45
45
|
impel mcp Run the local Impel MCP stdio bridge
|
|
46
46
|
impel skills sync [claude|codex|all] Sync shared skills into managed clients
|
|
47
|
-
impel agents sync [claude|codex|all] Sync tenant
|
|
47
|
+
impel agents sync [claude|codex|all] Sync explicit tenant agents into native clients
|
|
48
48
|
|
|
49
49
|
Desktop apps (macOS and Windows):
|
|
50
50
|
target is claude, chatgpt/codex, or all (default: all)
|
package/src/commands/agents.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `impel agents sync [claude|codex|all]` — synchronize the selected tenant's
|
|
2
|
-
//
|
|
2
|
+
// explicitly invocable Impel agents into every managed native registry.
|
|
3
3
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
|
|
@@ -8,13 +8,14 @@ import { loadConfig, normalizeGatewayUrl, redactSecretText, resolveDefaultGatewa
|
|
|
8
8
|
import { ensureTenantSelection, tenantCredential } from "../tenants.js";
|
|
9
9
|
import { managedSkillProfiles } from "./skills.js";
|
|
10
10
|
|
|
11
|
-
const HELP = `impel agents - sync tenant
|
|
11
|
+
const HELP = `impel agents - sync explicit tenant agents into native clients
|
|
12
12
|
|
|
13
13
|
Usage:
|
|
14
14
|
impel agents sync [claude|codex|all]
|
|
15
15
|
|
|
16
|
-
The selected tenant's authenticated
|
|
17
|
-
managed native profile.
|
|
16
|
+
The selected tenant's authenticated explicit-agent catalog is written into
|
|
17
|
+
every managed native profile. Write-capable agents require explicit user
|
|
18
|
+
selection before a run can start. The default target is all.`;
|
|
18
19
|
|
|
19
20
|
function normalizeAgentTarget(token) {
|
|
20
21
|
if (token === undefined || token === "all") return "all";
|