@controllerai/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -0
- package/dist/api/client.js +330 -0
- package/dist/api/client.js.map +1 -0
- package/dist/appLinks.js +99 -0
- package/dist/appLinks.js.map +1 -0
- package/dist/argv.js +124 -0
- package/dist/argv.js.map +1 -0
- package/dist/commands/agent.js +813 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/auth.js +327 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/branch.js +441 -0
- package/dist/commands/branch.js.map +1 -0
- package/dist/commands/connect.js +148 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/connection.js +148 -0
- package/dist/commands/connection.js.map +1 -0
- package/dist/commands/data.js +441 -0
- package/dist/commands/data.js.map +1 -0
- package/dist/commands/deps.js +63 -0
- package/dist/commands/deps.js.map +1 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/events.js +72 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/exec.js +306 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/expr.js +346 -0
- package/dist/commands/expr.js.map +1 -0
- package/dist/commands/flow.js +447 -0
- package/dist/commands/flow.js.map +1 -0
- package/dist/commands/integration.js +102 -0
- package/dist/commands/integration.js.map +1 -0
- package/dist/commands/node.js +885 -0
- package/dist/commands/node.js.map +1 -0
- package/dist/commands/organize.js +234 -0
- package/dist/commands/organize.js.map +1 -0
- package/dist/commands/run.js +437 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/schema.js +269 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/skills.js +1156 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/trigger.js +992 -0
- package/dist/commands/trigger.js.map +1 -0
- package/dist/commands/workflow.js +503 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/config.js +74 -0
- package/dist/config.js.map +1 -0
- package/dist/context.js +39 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/jsSource.js +32 -0
- package/dist/jsSource.js.map +1 -0
- package/dist/nodeProps.js +177 -0
- package/dist/nodeProps.js.map +1 -0
- package/dist/output.js +85 -0
- package/dist/output.js.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +41 -0
- package/skills/controllerai-agents/SKILL.md +169 -0
- package/skills/controllerai-agents/agents/openai.yaml +4 -0
- package/skills/controllerai-agents/references/instructions-and-versioning.md +76 -0
- package/skills/controllerai-ai-nodes/SKILL.md +83 -0
- package/skills/controllerai-ai-nodes/agents/openai.yaml +4 -0
- package/skills/controllerai-ai-nodes/references/selection-and-testing.md +46 -0
- package/skills/controllerai-data/SKILL.md +121 -0
- package/skills/controllerai-data/agents/openai.yaml +4 -0
- package/skills/controllerai-data/references/data-rails.md +46 -0
- package/skills/controllerai-expressions/SKILL.md +115 -0
- package/skills/controllerai-expressions/agents/openai.yaml +4 -0
- package/skills/controllerai-expressions/references/dsl-and-recipes.md +120 -0
- package/skills/controllerai-flow-control/SKILL.md +132 -0
- package/skills/controllerai-flow-control/agents/openai.yaml +4 -0
- package/skills/controllerai-flow-control/references/patterns.md +61 -0
- package/skills/controllerai-integration-actions/SKILL.md +95 -0
- package/skills/controllerai-integration-actions/agents/openai.yaml +4 -0
- package/skills/controllerai-node-config/SKILL.md +138 -0
- package/skills/controllerai-node-config/agents/openai.yaml +4 -0
- package/skills/controllerai-node-config/references/prop-and-readiness.md +72 -0
- package/skills/controllerai-runs/SKILL.md +109 -0
- package/skills/controllerai-runs/agents/openai.yaml +4 -0
- package/skills/controllerai-runs/references/debugging.md +53 -0
- package/skills/controllerai-start/SKILL.md +44 -0
- package/skills/controllerai-start/agents/openai.yaml +4 -0
- package/skills/controllerai-triggers/SKILL.md +174 -0
- package/skills/controllerai-triggers/agents/openai.yaml +4 -0
- package/skills/controllerai-triggers/references/lifecycle.md +54 -0
- package/skills/controllerai-workflows/SKILL.md +86 -0
- package/skills/controllerai-workflows/agents/openai.yaml +4 -0
- package/skills/controllerai-workflows/references/command-map.md +64 -0
- package/skills/manifest.json +248 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { agentUrl, conversationUrl } from '../appLinks.js';
|
|
3
|
+
import { buildClient } from '../context.js';
|
|
4
|
+
import { CaiError, EXIT, userError } from '../errors.js';
|
|
5
|
+
import { emit, printKeyValues, printTable, truncate, warn } from '../output.js';
|
|
6
|
+
export const resolveConfirmationChoice = (opts) => {
|
|
7
|
+
if (opts.requireConfirmation === true && opts.confirmation === false) {
|
|
8
|
+
throw userError('--require-confirmation and --no-confirmation are mutually exclusive.', 'CONFLICTING_INPUT');
|
|
9
|
+
}
|
|
10
|
+
if (opts.requireConfirmation === true)
|
|
11
|
+
return true;
|
|
12
|
+
if (opts.confirmation === false)
|
|
13
|
+
return false;
|
|
14
|
+
return undefined;
|
|
15
|
+
};
|
|
16
|
+
const confirmationReview = (required) => required
|
|
17
|
+
? 'approval required before every run'
|
|
18
|
+
: 'runs without approval — review writes, messages, deletion, and spend before attaching';
|
|
19
|
+
export const directActionToolOp = (input) => ({
|
|
20
|
+
op: 'add',
|
|
21
|
+
toolType: 'pipedream',
|
|
22
|
+
nodeSlug: input.integration,
|
|
23
|
+
nodeComponentKey: input.action,
|
|
24
|
+
connectionId: input.connectionId,
|
|
25
|
+
...(input.description !== undefined ? { toolDescription: input.description } : {}),
|
|
26
|
+
// Never omit this field: the product default is no approval while older
|
|
27
|
+
// backend entry points historically defaulted an omitted value to approval.
|
|
28
|
+
requiresConfirmation: input.requiresConfirmation,
|
|
29
|
+
});
|
|
30
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
const parsePositiveId = (raw, label) => {
|
|
32
|
+
const value = Number(raw);
|
|
33
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
34
|
+
throw userError(`${label} must be a positive integer.`, 'BAD_ID');
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
export const assertAgentVisibility = (value) => {
|
|
39
|
+
if (value !== 'private' && value !== 'organization') {
|
|
40
|
+
throw userError('--value must be private or organization. Public sharing is not supported.', 'BAD_AGENT_VISIBILITY');
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
export const setAgentVisibility = (api, rawAgentId, rawVisibility) => {
|
|
45
|
+
const agentId = parsePositiveId(rawAgentId, 'agentId');
|
|
46
|
+
const visibility = assertAgentVisibility(rawVisibility);
|
|
47
|
+
return api.rest(`/agents/${agentId}/share`, {
|
|
48
|
+
method: 'PATCH',
|
|
49
|
+
body: { visibility },
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
export const respondAgentPermission = (api, rawConversationId, requestId, behavior, opts = {}) => {
|
|
53
|
+
const conversationId = parsePositiveId(rawConversationId, 'conversationId');
|
|
54
|
+
const normalizedRequestId = requestId.trim();
|
|
55
|
+
if (!normalizedRequestId) {
|
|
56
|
+
throw userError('--request must be a non-empty permission request id.', 'BAD_PERMISSION_ID');
|
|
57
|
+
}
|
|
58
|
+
const result = behavior === 'allow'
|
|
59
|
+
? { behavior: 'allow' }
|
|
60
|
+
: { behavior: 'deny', message: opts.message?.trim() || 'Denied from the Controller AI CLI.' };
|
|
61
|
+
return api.rest(`/agent-conversations/${conversationId}/respond-permission`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body: {
|
|
64
|
+
requestId: normalizedRequestId,
|
|
65
|
+
...(opts.toolName ? { toolName: opts.toolName } : {}),
|
|
66
|
+
result,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const instructionText = (opts) => {
|
|
71
|
+
if (opts.instructions !== undefined && opts.instructionsFile) {
|
|
72
|
+
throw userError('--instructions and --instructions-file are mutually exclusive.', 'CONFLICTING_INPUT');
|
|
73
|
+
}
|
|
74
|
+
if (opts.instructionsFile)
|
|
75
|
+
return readFileSync(opts.instructionsFile, 'utf8');
|
|
76
|
+
return opts.instructions;
|
|
77
|
+
};
|
|
78
|
+
const getAgent = (api, id) => api.rest(`/agents/${id}`);
|
|
79
|
+
const updateDraft = async (api, agentId, patch) => {
|
|
80
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
81
|
+
const current = await getAgent(api, agentId);
|
|
82
|
+
if (!current.updatedAt) {
|
|
83
|
+
throw userError('Agent response did not include updatedAt; refusing an unsafe draft write.', 'NO_AGENT_HEAD');
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return await api.rest(`/agents/${agentId}/draft`, {
|
|
87
|
+
method: 'PATCH',
|
|
88
|
+
body: { baseUpdatedAt: current.updatedAt, ...patch },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (attempt === 0 && error instanceof CaiError && error.exitCode === EXIT.conflict)
|
|
93
|
+
continue;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw userError('Agent draft update did not complete.', 'AGENT_DRAFT_UPDATE_FAILED');
|
|
98
|
+
};
|
|
99
|
+
const parseSeconds = (raw, label, max = 600) => {
|
|
100
|
+
const value = Number(raw);
|
|
101
|
+
if (!Number.isFinite(value) || value <= 0 || value > max) {
|
|
102
|
+
throw userError(`${label} must be greater than 0 and at most ${max} seconds.`, 'BAD_TIMEOUT');
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
};
|
|
106
|
+
const selectedDraftMode = (opts, defaultDraft) => {
|
|
107
|
+
if (opts.draft && opts.live) {
|
|
108
|
+
throw userError('--draft and --live are mutually exclusive.', 'CONFLICTING_AGENT_RAIL');
|
|
109
|
+
}
|
|
110
|
+
if (opts.draft)
|
|
111
|
+
return true;
|
|
112
|
+
if (opts.live)
|
|
113
|
+
return false;
|
|
114
|
+
return defaultDraft;
|
|
115
|
+
};
|
|
116
|
+
const assertConversationRail = (conversation, draft) => {
|
|
117
|
+
if (conversation.isDraft !== draft) {
|
|
118
|
+
throw userError(`Conversation ${conversation.id} was created on the ${conversation.isDraft ? 'draft' : 'live'} rail, but the CLI requested ${draft ? 'draft' : 'live'}.`, 'CONVERSATION_RAIL_MISMATCH');
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const contentText = (content) => typeof content === 'string' ? content : JSON.stringify(content ?? null);
|
|
122
|
+
export const assistantReplyAfter = (history, afterMessageId) => [...(history.messages ?? [])]
|
|
123
|
+
.reverse()
|
|
124
|
+
.find(message => message.id > afterMessageId && message.role === 'assistant') ?? null;
|
|
125
|
+
export const completedAgentTurnAfter = (history, afterEventId) => (history.events ?? []).some(event => event.id > afterEventId && event.eventType === 'sdk_message' && event.sdkType === 'result');
|
|
126
|
+
export const pollAgentReply = async (api, conversationId, afterMessageId, afterEventId, timeoutMs, intervalMs = 1_000, wait = sleep) => {
|
|
127
|
+
const deadline = Date.now() + timeoutMs;
|
|
128
|
+
let last = null;
|
|
129
|
+
while (Date.now() < deadline) {
|
|
130
|
+
last = await api.rest(`/agent-conversations/${conversationId}/history`);
|
|
131
|
+
if ((last.pendingHitlRequests ?? []).length > 0) {
|
|
132
|
+
return { kind: 'permission', history: last };
|
|
133
|
+
}
|
|
134
|
+
if (['INTERRUPTED', 'CLOSED'].includes(last.conversation?.status ?? '')) {
|
|
135
|
+
return { kind: 'stopped', history: last };
|
|
136
|
+
}
|
|
137
|
+
if (completedAgentTurnAfter(last, afterEventId)) {
|
|
138
|
+
const reply = assistantReplyAfter(last, afterMessageId);
|
|
139
|
+
if (reply)
|
|
140
|
+
return { kind: 'reply', history: last, reply };
|
|
141
|
+
}
|
|
142
|
+
await wait(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
143
|
+
}
|
|
144
|
+
return { kind: 'timeout', history: last };
|
|
145
|
+
};
|
|
146
|
+
const requireReply = (result, conversationId) => {
|
|
147
|
+
if (result.kind === 'reply')
|
|
148
|
+
return result.reply;
|
|
149
|
+
if (result.kind === 'permission') {
|
|
150
|
+
throw userError(`Agent conversation ${conversationId} requested Controller AI permission instead of ` +
|
|
151
|
+
`completing autonomously. Inspect \`cai agent conversation history ` +
|
|
152
|
+
`${conversationId} --json\`.`, 'AGENT_PERMISSION_REQUIRED');
|
|
153
|
+
}
|
|
154
|
+
if (result.kind === 'stopped') {
|
|
155
|
+
throw userError(`Agent conversation ${conversationId} stopped with status ${result.history.conversation?.status ?? 'unknown'} before replying.`, 'AGENT_CONVERSATION_STOPPED');
|
|
156
|
+
}
|
|
157
|
+
throw userError(`Agent conversation ${conversationId} did not produce an assistant reply before timeout. ` +
|
|
158
|
+
`Re-read it with \`cai agent conversation history ${conversationId}\`.`, 'AGENT_TEST_TIMEOUT');
|
|
159
|
+
};
|
|
160
|
+
const printConversation = (row, appUrl = null) => {
|
|
161
|
+
const pairs = [
|
|
162
|
+
['conversationId', row.id],
|
|
163
|
+
['agentId', row.agentId],
|
|
164
|
+
['rail', row.isDraft ? 'draft' : 'live'],
|
|
165
|
+
['agent version', row.agentVersionNumber ?? row.agentVersionId ?? '-'],
|
|
166
|
+
['status', row.status],
|
|
167
|
+
['model', row.model ?? '-'],
|
|
168
|
+
['title', row.title ?? '-'],
|
|
169
|
+
];
|
|
170
|
+
if (appUrl)
|
|
171
|
+
pairs.push(['open in app', appUrl]);
|
|
172
|
+
printKeyValues(pairs);
|
|
173
|
+
};
|
|
174
|
+
const printHistory = (history) => {
|
|
175
|
+
if (history.conversation)
|
|
176
|
+
printConversation(history.conversation);
|
|
177
|
+
process.stdout.write('\nmessages\n');
|
|
178
|
+
printTable(['id', 'role', 'content', 'cost', 'created'], (history.messages ?? []).map(message => [
|
|
179
|
+
message.id,
|
|
180
|
+
message.role,
|
|
181
|
+
truncate(contentText(message.content), 100),
|
|
182
|
+
message.totalCostUsd ?? '-',
|
|
183
|
+
message.createdAt ?? '-',
|
|
184
|
+
]));
|
|
185
|
+
if ((history.pendingHitlRequests ?? []).length) {
|
|
186
|
+
process.stdout.write('\npending permissions\n');
|
|
187
|
+
printTable(['requestId', 'tool'], (history.pendingHitlRequests ?? []).map(request => [
|
|
188
|
+
request.requestId ?? request.id ?? '-',
|
|
189
|
+
request.toolName ?? '-',
|
|
190
|
+
]));
|
|
191
|
+
const request = history.pendingHitlRequests?.[0];
|
|
192
|
+
const requestId = request?.requestId ?? request?.id;
|
|
193
|
+
if (requestId !== undefined) {
|
|
194
|
+
const tool = request?.toolName ? ` --tool ${request.toolName}` : '';
|
|
195
|
+
process.stdout.write(`\nnext: cai agent conversation approve ${history.conversation?.id ?? '<conversationId>'} ` +
|
|
196
|
+
`--request ${requestId}${tool}\n` +
|
|
197
|
+
`or: cai agent conversation deny ${history.conversation?.id ?? '<conversationId>'} ` +
|
|
198
|
+
`--request ${requestId}${tool} --message "<reason>"\n`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
export const registerAgent = (program) => {
|
|
203
|
+
const agent = program
|
|
204
|
+
.command('agent')
|
|
205
|
+
.description('Build and test mutable agent drafts, then publish immutable live versions');
|
|
206
|
+
agent
|
|
207
|
+
.command('list')
|
|
208
|
+
.option('--search <text>', 'filter by name')
|
|
209
|
+
.option('--ownership <mine|shared>', 'ownership filter', 'mine')
|
|
210
|
+
.description('List agents visible to the current account')
|
|
211
|
+
.action(async (opts, command) => {
|
|
212
|
+
const { api } = buildClient(command);
|
|
213
|
+
const query = new URLSearchParams();
|
|
214
|
+
if (opts.search)
|
|
215
|
+
query.set('search', opts.search);
|
|
216
|
+
if (opts.ownership)
|
|
217
|
+
query.set('ownership', opts.ownership);
|
|
218
|
+
const rows = await api.rest(`/agents?${query.toString()}`);
|
|
219
|
+
emit({ agents: rows ?? [] }, () => printTable(['id', 'name', 'enabled', 'visibility', 'live version id'], (rows ?? []).map(row => [
|
|
220
|
+
row.id,
|
|
221
|
+
row.displayName,
|
|
222
|
+
row.enabled ? 'yes' : 'no',
|
|
223
|
+
row.visibility,
|
|
224
|
+
row.liveAgentVersionId ?? '-',
|
|
225
|
+
])));
|
|
226
|
+
});
|
|
227
|
+
agent
|
|
228
|
+
.command('get')
|
|
229
|
+
.argument('<agentId>')
|
|
230
|
+
.description('Inspect the agent configuration visible to this account and its version status')
|
|
231
|
+
.action(async (rawId, _opts, command) => {
|
|
232
|
+
const { api } = buildClient(command);
|
|
233
|
+
const row = await getAgent(api, parsePositiveId(rawId, 'agentId'));
|
|
234
|
+
emit({
|
|
235
|
+
...row,
|
|
236
|
+
next: {
|
|
237
|
+
integrationAction: 'cai integration search <capability>',
|
|
238
|
+
workflowTool: `cai agent add-workflow ${row.id} --workflow-id <id> --flow <flowId>`,
|
|
239
|
+
},
|
|
240
|
+
}, () => printKeyValues([
|
|
241
|
+
['id', row.id],
|
|
242
|
+
['name', row.displayName],
|
|
243
|
+
['enabled', row.enabled ? 'yes' : 'no'],
|
|
244
|
+
['visibility', row.visibility],
|
|
245
|
+
['live version', row.liveVersion?.versionNumber ?? '(never published)'],
|
|
246
|
+
['unpublished changes', row.hasUnpublishedChanges ? 'yes' : 'no'],
|
|
247
|
+
['instructions', truncate(row.systemPrompt, 120)],
|
|
248
|
+
]));
|
|
249
|
+
});
|
|
250
|
+
agent
|
|
251
|
+
.command('create')
|
|
252
|
+
.requiredOption('--name <name>', 'agent display name')
|
|
253
|
+
.option('--instructions <text>', 'agent instructions')
|
|
254
|
+
.option('--instructions-file <path>', 'read agent instructions from a UTF-8 file')
|
|
255
|
+
.option('--disabled', 'create the draft disabled', false)
|
|
256
|
+
.description('Create a private draft agent; nothing becomes live until publish')
|
|
257
|
+
.action(async (opts, command) => {
|
|
258
|
+
const { api, config } = buildClient(command);
|
|
259
|
+
const instructions = instructionText(opts);
|
|
260
|
+
const row = await api.rest('/agents', {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
body: {
|
|
263
|
+
displayName: opts.name,
|
|
264
|
+
...(instructions !== undefined ? { systemPrompt: instructions } : {}),
|
|
265
|
+
enabled: !opts.disabled,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
const appUrl = agentUrl(config, row.id);
|
|
269
|
+
emit({ ...row, ...(appUrl ? { appUrl } : {}) }, () => {
|
|
270
|
+
const pairs = [
|
|
271
|
+
['agentId', row.id],
|
|
272
|
+
['name', row.displayName],
|
|
273
|
+
['state', 'draft — not live'],
|
|
274
|
+
['next', 'choose a ready integration action or a workflow tool'],
|
|
275
|
+
['ready action', 'cai integration search <capability>'],
|
|
276
|
+
['workflow tool', `cai agent add-workflow ${row.id} --workflow-id <id> --flow <flowId>`],
|
|
277
|
+
];
|
|
278
|
+
if (appUrl)
|
|
279
|
+
pairs.push(['open in app', appUrl]);
|
|
280
|
+
printKeyValues(pairs);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
agent
|
|
284
|
+
.command('update')
|
|
285
|
+
.argument('<agentId>')
|
|
286
|
+
.option('--name <name>', 'new display name')
|
|
287
|
+
.option('--instructions <text>', 'new agent instructions')
|
|
288
|
+
.option('--instructions-file <path>', 'read instructions from a UTF-8 file')
|
|
289
|
+
.description('Update only the draft; the live agent remains unchanged')
|
|
290
|
+
.action(async (rawId, opts, command) => {
|
|
291
|
+
const { api } = buildClient(command);
|
|
292
|
+
const systemPrompt = instructionText(opts);
|
|
293
|
+
if (opts.name === undefined && systemPrompt === undefined) {
|
|
294
|
+
throw userError('Pass --name, --instructions, or --instructions-file.', 'NO_CHANGES');
|
|
295
|
+
}
|
|
296
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
297
|
+
const result = await updateDraft(api, agentId, {
|
|
298
|
+
...(opts.name !== undefined ? { displayName: opts.name } : {}),
|
|
299
|
+
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
300
|
+
});
|
|
301
|
+
emit(result, () => printKeyValues([
|
|
302
|
+
['agentId', result.agent.id],
|
|
303
|
+
['name', result.agent.displayName],
|
|
304
|
+
['state', 'draft updated — live version unchanged'],
|
|
305
|
+
['next', `verify Test, then cai agent publish ${agentId}`],
|
|
306
|
+
]));
|
|
307
|
+
});
|
|
308
|
+
agent
|
|
309
|
+
.command('visibility')
|
|
310
|
+
.argument('<agentId>')
|
|
311
|
+
.requiredOption('--value <private|organization>', 'private revokes organization access; organization shares the published version')
|
|
312
|
+
.description('Change live agent access (organization requires publishing the agent first; no public mode)')
|
|
313
|
+
.action(async (rawId, opts, command) => {
|
|
314
|
+
const visibility = assertAgentVisibility(opts.value);
|
|
315
|
+
warn(visibility === 'organization'
|
|
316
|
+
? 'LIVE ACCESS CHANGE: organization visibility lets active organization members use ' +
|
|
317
|
+
'the agent’s published version. Publish the agent first; the backend rejects ' +
|
|
318
|
+
'sharing an unpublished draft. Direct integration actions use the agent owner’s ' +
|
|
319
|
+
'connected accounts; Controller AI usage comes from the organization’s shared pool.'
|
|
320
|
+
: 'LIVE ACCESS CHANGE: private visibility revokes organization access and stops ' +
|
|
321
|
+
'teammates’ active sessions for this agent.');
|
|
322
|
+
const { api } = buildClient(command);
|
|
323
|
+
const result = await setAgentVisibility(api, rawId, visibility);
|
|
324
|
+
emit({ agentId: result.id, visibility: result.visibility, liveAccessChanged: true }, () => printKeyValues([
|
|
325
|
+
['agentId', result.id],
|
|
326
|
+
['visibility', result.visibility],
|
|
327
|
+
[
|
|
328
|
+
'effect',
|
|
329
|
+
result.visibility === 'organization'
|
|
330
|
+
? 'published version available to organization'
|
|
331
|
+
: 'organization access revoked',
|
|
332
|
+
],
|
|
333
|
+
]));
|
|
334
|
+
});
|
|
335
|
+
agent
|
|
336
|
+
.command('add-workflow')
|
|
337
|
+
.argument('<agentId>')
|
|
338
|
+
.requiredOption('--workflow-id <id>', 'workflow containing the tool flow')
|
|
339
|
+
.requiredOption('--flow <flowId>', 'flow exposed to the agent')
|
|
340
|
+
.option('--description <text>', 'description the model uses when choosing the tool')
|
|
341
|
+
.option('--require-confirmation', 'pause every invocation for an explicit user approval before the workflow runs', false)
|
|
342
|
+
.description('Add a follow-live workflow tool with a backend-generated name to the draft')
|
|
343
|
+
.action(async (rawId, opts, command) => {
|
|
344
|
+
const { api } = buildClient(command);
|
|
345
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
346
|
+
const result = await updateDraft(api, agentId, {
|
|
347
|
+
toolOps: [
|
|
348
|
+
{
|
|
349
|
+
op: 'add',
|
|
350
|
+
toolType: 'workflow',
|
|
351
|
+
workflowId: parsePositiveId(opts.workflowId, 'workflowId'),
|
|
352
|
+
flowId: opts.flow,
|
|
353
|
+
...(opts.description !== undefined ? { toolDescription: opts.description } : {}),
|
|
354
|
+
// Workflow tools always follow the latest published workflow release.
|
|
355
|
+
versionPolicy: 'latest',
|
|
356
|
+
requiresConfirmation: Boolean(opts.requireConfirmation),
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
});
|
|
360
|
+
const added = result.tools.find(tool => tool.workflowId === Number(opts.workflowId) && tool.flowId === opts.flow);
|
|
361
|
+
emit({ agentId, tool: added ?? null, liveChanged: false }, () => printKeyValues([
|
|
362
|
+
['agentId', agentId],
|
|
363
|
+
['toolId', added?.id ?? '-'],
|
|
364
|
+
['tool', added?.toolName ?? '(generated by backend)'],
|
|
365
|
+
[
|
|
366
|
+
'confirmation',
|
|
367
|
+
added?.requiresConfirmation === true
|
|
368
|
+
? 'required'
|
|
369
|
+
: added?.requiresConfirmation === false
|
|
370
|
+
? 'not required'
|
|
371
|
+
: 'not returned — verify with `cai agent tools`',
|
|
372
|
+
],
|
|
373
|
+
['state', 'draft updated — live version unchanged'],
|
|
374
|
+
]));
|
|
375
|
+
});
|
|
376
|
+
agent
|
|
377
|
+
.command('add-action')
|
|
378
|
+
.argument('<agentId>')
|
|
379
|
+
.requiredOption('--integration <slug>', 'exact slug from `cai integration search`')
|
|
380
|
+
.requiredOption('--action <key>', 'exact action key from `cai integration actions`')
|
|
381
|
+
.requiredOption('--connection <id>', 'active connection for this integration')
|
|
382
|
+
.option('--description <text>', 'when the agent should choose this action')
|
|
383
|
+
.option('--require-confirmation', 'require approval before each run; default is no approval, so decide for writes or spend', false)
|
|
384
|
+
.description('Attach one ready integration action to the draft; explicitly review its confirmation policy')
|
|
385
|
+
.action(async (rawId, opts, command) => {
|
|
386
|
+
const { api } = buildClient(command);
|
|
387
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
388
|
+
const connectionId = parsePositiveId(opts.connection, 'connectionId');
|
|
389
|
+
const requiresConfirmation = Boolean(opts.requireConfirmation);
|
|
390
|
+
const result = await updateDraft(api, agentId, {
|
|
391
|
+
toolOps: [
|
|
392
|
+
directActionToolOp({
|
|
393
|
+
integration: opts.integration,
|
|
394
|
+
action: opts.action,
|
|
395
|
+
connectionId,
|
|
396
|
+
description: opts.description,
|
|
397
|
+
requiresConfirmation,
|
|
398
|
+
}),
|
|
399
|
+
],
|
|
400
|
+
});
|
|
401
|
+
const added = result.tools.find(tool => tool.toolType === 'pipedream' &&
|
|
402
|
+
tool.nodeSlug === opts.integration &&
|
|
403
|
+
tool.nodeComponentKey === opts.action);
|
|
404
|
+
emit({
|
|
405
|
+
agentId,
|
|
406
|
+
tool: added ?? null,
|
|
407
|
+
liveChanged: false,
|
|
408
|
+
confirmationReview: confirmationReview(requiresConfirmation),
|
|
409
|
+
}, () => printKeyValues([
|
|
410
|
+
['agentId', agentId],
|
|
411
|
+
['toolId', added?.id ?? '-'],
|
|
412
|
+
['integration', opts.integration],
|
|
413
|
+
['action', opts.action],
|
|
414
|
+
['connection', connectionId],
|
|
415
|
+
['confirmation', confirmationReview(requiresConfirmation)],
|
|
416
|
+
['state', 'draft updated — live version unchanged'],
|
|
417
|
+
]));
|
|
418
|
+
});
|
|
419
|
+
agent
|
|
420
|
+
.command('update-tool')
|
|
421
|
+
.argument('<agentId>')
|
|
422
|
+
.requiredOption('--tool <toolId>', 'draft tool id')
|
|
423
|
+
.option('--description <text>', 'new routing description')
|
|
424
|
+
.option('--connection <id>', 'rebind a ready integration action to another connection')
|
|
425
|
+
.option('--require-confirmation', 'require approval before every invocation')
|
|
426
|
+
.option('--no-confirmation', 'run without approval; use only after reviewing writes, messages, deletion, and spend')
|
|
427
|
+
.description('Update a draft tool; direct-action connection grants are live-bound before republish')
|
|
428
|
+
.action(async (rawId, opts, command) => {
|
|
429
|
+
const { api } = buildClient(command);
|
|
430
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
431
|
+
const toolId = parsePositiveId(opts.tool, 'toolId');
|
|
432
|
+
const requiresConfirmation = resolveConfirmationChoice(opts);
|
|
433
|
+
if (opts.description === undefined &&
|
|
434
|
+
opts.connection === undefined &&
|
|
435
|
+
requiresConfirmation === undefined) {
|
|
436
|
+
throw userError('Pass --description, --connection, --require-confirmation, or --no-confirmation.', 'NO_CHANGES');
|
|
437
|
+
}
|
|
438
|
+
const result = await updateDraft(api, agentId, {
|
|
439
|
+
toolOps: [
|
|
440
|
+
{
|
|
441
|
+
op: 'update',
|
|
442
|
+
toolId,
|
|
443
|
+
...(opts.description !== undefined ? { toolDescription: opts.description } : {}),
|
|
444
|
+
...(opts.connection !== undefined
|
|
445
|
+
? { connectionId: parsePositiveId(opts.connection, 'connectionId') }
|
|
446
|
+
: {}),
|
|
447
|
+
...(requiresConfirmation !== undefined ? { requiresConfirmation } : {}),
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
});
|
|
451
|
+
const updated = result.tools.find(tool => tool.id === toolId);
|
|
452
|
+
const liveAuthorizationChanged = opts.connection !== undefined && updated?.toolType === 'pipedream';
|
|
453
|
+
emit({
|
|
454
|
+
agentId,
|
|
455
|
+
tool: updated ?? null,
|
|
456
|
+
liveChanged: liveAuthorizationChanged,
|
|
457
|
+
liveAuthorizationChanged,
|
|
458
|
+
...(requiresConfirmation !== undefined
|
|
459
|
+
? { confirmationReview: confirmationReview(requiresConfirmation) }
|
|
460
|
+
: {}),
|
|
461
|
+
}, () => printKeyValues([
|
|
462
|
+
['agentId', agentId],
|
|
463
|
+
['toolId', toolId],
|
|
464
|
+
['connection', updated?.connectionId ?? opts.connection ?? '-'],
|
|
465
|
+
[
|
|
466
|
+
'confirmation',
|
|
467
|
+
requiresConfirmation === undefined
|
|
468
|
+
? 'unchanged'
|
|
469
|
+
: confirmationReview(requiresConfirmation),
|
|
470
|
+
],
|
|
471
|
+
[
|
|
472
|
+
'state',
|
|
473
|
+
liveAuthorizationChanged
|
|
474
|
+
? 'draft updated; live action authorization may redirect immediately — publish to version the draft'
|
|
475
|
+
: 'draft updated — live version unchanged',
|
|
476
|
+
],
|
|
477
|
+
]));
|
|
478
|
+
});
|
|
479
|
+
agent
|
|
480
|
+
.command('remove-tool')
|
|
481
|
+
.argument('<agentId>')
|
|
482
|
+
.requiredOption('--tool <toolId>', 'draft tool id')
|
|
483
|
+
.description('Remove a draft tool; direct-action detachment revokes live authorization immediately')
|
|
484
|
+
.action(async (rawId, opts, command) => {
|
|
485
|
+
const { api } = buildClient(command);
|
|
486
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
487
|
+
const toolId = parsePositiveId(opts.tool, 'toolId');
|
|
488
|
+
const currentTools = await api.rest(`/agents/${agentId}/tools`);
|
|
489
|
+
const removed = (currentTools ?? []).find(tool => tool.id === toolId);
|
|
490
|
+
const result = await updateDraft(api, agentId, { toolOps: [{ op: 'remove', toolId }] });
|
|
491
|
+
const liveAuthorizationRevoked = removed?.toolType === 'pipedream';
|
|
492
|
+
emit({
|
|
493
|
+
agentId,
|
|
494
|
+
removedToolId: toolId,
|
|
495
|
+
removedToolType: removed?.toolType ?? null,
|
|
496
|
+
tools: result.tools,
|
|
497
|
+
liveChanged: liveAuthorizationRevoked,
|
|
498
|
+
liveAuthorizationRevoked,
|
|
499
|
+
}, () => printKeyValues([
|
|
500
|
+
['agentId', agentId],
|
|
501
|
+
['removed tool', toolId],
|
|
502
|
+
[
|
|
503
|
+
'state',
|
|
504
|
+
liveAuthorizationRevoked
|
|
505
|
+
? 'live action authorization revoked; publish to remove the tool from the live list'
|
|
506
|
+
: 'draft updated — live version unchanged',
|
|
507
|
+
],
|
|
508
|
+
]));
|
|
509
|
+
});
|
|
510
|
+
agent
|
|
511
|
+
.command('add-file')
|
|
512
|
+
.argument('<agentId>')
|
|
513
|
+
.requiredOption('--file <path>', 'knowledge file to add to the draft')
|
|
514
|
+
.option('--name <fileName>', 'name visible to the agent')
|
|
515
|
+
.option('--type <fileType>', 'txt, md, json, csv, html, or xml')
|
|
516
|
+
.description('Add a knowledge file to the draft; publish later to freeze it into live')
|
|
517
|
+
.action(async (rawId, opts, command) => {
|
|
518
|
+
const { basename, extname } = await import('node:path');
|
|
519
|
+
const { api } = buildClient(command);
|
|
520
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
521
|
+
const fileName = opts.name ?? basename(opts.file);
|
|
522
|
+
const inferred = extname(fileName).replace(/^\./, '') || 'txt';
|
|
523
|
+
const result = await updateDraft(api, agentId, {
|
|
524
|
+
fileOps: [
|
|
525
|
+
{
|
|
526
|
+
op: 'add',
|
|
527
|
+
fileName,
|
|
528
|
+
fileType: opts.type ?? inferred,
|
|
529
|
+
content: readFileSync(opts.file, 'utf8'),
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
});
|
|
533
|
+
emit({ agentId, files: result.files, liveChanged: false }, () => printKeyValues([
|
|
534
|
+
['agentId', agentId],
|
|
535
|
+
['file', fileName],
|
|
536
|
+
['state', 'draft updated — live version unchanged'],
|
|
537
|
+
]));
|
|
538
|
+
});
|
|
539
|
+
agent
|
|
540
|
+
.command('tools')
|
|
541
|
+
.argument('<agentId>')
|
|
542
|
+
.description('List owner draft tools or the published projection visible to a shared user')
|
|
543
|
+
.action(async (rawId, _opts, command) => {
|
|
544
|
+
const { api } = buildClient(command);
|
|
545
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
546
|
+
const tools = await api.rest(`/agents/${agentId}/tools`);
|
|
547
|
+
emit({ agentId, tools: tools ?? [] }, () => printTable([
|
|
548
|
+
'id',
|
|
549
|
+
'type',
|
|
550
|
+
'name',
|
|
551
|
+
'integration/workflow',
|
|
552
|
+
'action/flow',
|
|
553
|
+
'connection',
|
|
554
|
+
'tier',
|
|
555
|
+
'policy',
|
|
556
|
+
'confirm',
|
|
557
|
+
], (tools ?? []).map(tool => [
|
|
558
|
+
tool.id,
|
|
559
|
+
tool.toolType,
|
|
560
|
+
tool.displayName ?? tool.toolName,
|
|
561
|
+
tool.nodeSlug ?? tool.workflowName ?? tool.workflowId ?? '-',
|
|
562
|
+
tool.nodeComponentKey ?? tool.flowName ?? tool.flowId ?? '-',
|
|
563
|
+
tool.connectionId ?? '-',
|
|
564
|
+
tool.schemaTier ?? '-',
|
|
565
|
+
tool.versionPolicy ?? '-',
|
|
566
|
+
tool.requiresConfirmation ? 'required' : 'no',
|
|
567
|
+
])));
|
|
568
|
+
});
|
|
569
|
+
agent
|
|
570
|
+
.command('publish-preflight')
|
|
571
|
+
.argument('<agentId>')
|
|
572
|
+
.description('Check the agent draft and every attached tool before publishing')
|
|
573
|
+
.action(async (rawId, _opts, command) => {
|
|
574
|
+
const { api } = buildClient(command);
|
|
575
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
576
|
+
const result = await api.rest(`/agents/${agentId}/publish-preflight`);
|
|
577
|
+
emit(result, () => printKeyValues([
|
|
578
|
+
['agentId', agentId],
|
|
579
|
+
['preflight', JSON.stringify(result)],
|
|
580
|
+
]));
|
|
581
|
+
});
|
|
582
|
+
agent
|
|
583
|
+
.command('publish')
|
|
584
|
+
.argument('<agentId>')
|
|
585
|
+
.option('--name <releaseName>', 'optional release name')
|
|
586
|
+
.description('Snapshot the draft and move every existing live conversation to the new version')
|
|
587
|
+
.action(async (rawId, opts, command) => {
|
|
588
|
+
const { api, config } = buildClient(command);
|
|
589
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
590
|
+
const preflight = await api.rest(`/agents/${agentId}/publish-preflight`);
|
|
591
|
+
if (preflight?.hasBlockers) {
|
|
592
|
+
throw userError(`Agent publish preflight has blockers:\n${JSON.stringify(preflight, null, 2)}`, 'AGENT_PUBLISH_BLOCKED');
|
|
593
|
+
}
|
|
594
|
+
const result = await api.rest(`/agents/${agentId}/publish`, { method: 'POST', body: opts.name ? { name: opts.name } : {} });
|
|
595
|
+
const appUrl = agentUrl(config, agentId);
|
|
596
|
+
emit({ agentId, ...result, ...(appUrl ? { appUrl } : {}) }, () => {
|
|
597
|
+
const pairs = [
|
|
598
|
+
['agentId', agentId],
|
|
599
|
+
['live version', result.versionNumber],
|
|
600
|
+
['version id', result.agentVersionId],
|
|
601
|
+
['effect', 'existing live conversations moved to this version'],
|
|
602
|
+
];
|
|
603
|
+
if (appUrl)
|
|
604
|
+
pairs.push(['open in app', appUrl]);
|
|
605
|
+
printKeyValues(pairs);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
agent
|
|
609
|
+
.command('versions')
|
|
610
|
+
.argument('<agentId>')
|
|
611
|
+
.description('List immutable published versions')
|
|
612
|
+
.action(async (rawId, _opts, command) => {
|
|
613
|
+
const { api } = buildClient(command);
|
|
614
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
615
|
+
const versions = await api.rest(`/agents/${agentId}/versions`);
|
|
616
|
+
emit({ agentId, versions: versions ?? [] }, () => printTable(['version', 'name', 'live', 'created'], (versions ?? []).map(version => [
|
|
617
|
+
version.versionNumber,
|
|
618
|
+
version.name ?? '-',
|
|
619
|
+
version.isLive ? 'yes' : '',
|
|
620
|
+
version.createdAt,
|
|
621
|
+
])));
|
|
622
|
+
});
|
|
623
|
+
const conversation = agent
|
|
624
|
+
.command('conversation')
|
|
625
|
+
.description('Verify owner-only Test/draft or latest-published Live agent conversations');
|
|
626
|
+
conversation
|
|
627
|
+
.command('list')
|
|
628
|
+
.argument('<agentId>')
|
|
629
|
+
.option('--mode <dev|live>', 'filter to the draft or live rail')
|
|
630
|
+
.description('List verification conversations for an agent')
|
|
631
|
+
.action(async (rawId, opts, command) => {
|
|
632
|
+
if (opts.mode && !['dev', 'live'].includes(opts.mode)) {
|
|
633
|
+
throw userError('--mode must be dev or live.', 'BAD_CONVERSATION_MODE');
|
|
634
|
+
}
|
|
635
|
+
const { api } = buildClient(command);
|
|
636
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
637
|
+
const query = opts.mode ? `?mode=${encodeURIComponent(opts.mode)}` : '';
|
|
638
|
+
const result = await api.rest(`/agents/${agentId}/conversations${query}`);
|
|
639
|
+
emit(result, () => printTable(['id', 'rail', 'version', 'status', 'title', 'updated'], (result.conversations ?? []).map(row => [
|
|
640
|
+
row.id,
|
|
641
|
+
row.isDraft ? 'draft' : 'live',
|
|
642
|
+
row.agentVersionNumber ?? row.agentVersionId ?? '-',
|
|
643
|
+
row.status,
|
|
644
|
+
row.title ?? '-',
|
|
645
|
+
row.updatedAt ?? '-',
|
|
646
|
+
])));
|
|
647
|
+
});
|
|
648
|
+
conversation
|
|
649
|
+
.command('create')
|
|
650
|
+
.argument('<agentId>')
|
|
651
|
+
.option('--draft', 'owner-only Test: current agent draft and workflow development heads')
|
|
652
|
+
.option('--live', 'Live: latest published agent and workflow releases (default)')
|
|
653
|
+
.option('--title <title>', 'conversation title')
|
|
654
|
+
.option('--model <model>', 'runtime model override')
|
|
655
|
+
.description('Create a conversation on an explicit agent rail')
|
|
656
|
+
.action(async (rawId, opts, command) => {
|
|
657
|
+
const { api, config } = buildClient(command);
|
|
658
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
659
|
+
const draft = selectedDraftMode(opts, false);
|
|
660
|
+
const row = await api.rest(`/agents/${agentId}/conversations`, {
|
|
661
|
+
method: 'POST',
|
|
662
|
+
body: {
|
|
663
|
+
draft,
|
|
664
|
+
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
665
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
assertConversationRail(row, draft);
|
|
669
|
+
const appUrl = conversationUrl(config, agentId, row.id);
|
|
670
|
+
emit({ ...row, ...(appUrl ? { appUrl } : {}) }, () => printConversation(row, appUrl));
|
|
671
|
+
});
|
|
672
|
+
conversation
|
|
673
|
+
.command('status')
|
|
674
|
+
.argument('<conversationId>')
|
|
675
|
+
.description('Read the conversation rail, adopted version, model, and status')
|
|
676
|
+
.action(async (rawId, _opts, command) => {
|
|
677
|
+
const { api } = buildClient(command);
|
|
678
|
+
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
679
|
+
const row = await api.rest(`/agent-conversations/${conversationId}`);
|
|
680
|
+
emit(row, () => printConversation(row));
|
|
681
|
+
});
|
|
682
|
+
conversation
|
|
683
|
+
.command('history')
|
|
684
|
+
.argument('<conversationId>')
|
|
685
|
+
.description('Read persisted messages, events, and pending permission requests')
|
|
686
|
+
.action(async (rawId, _opts, command) => {
|
|
687
|
+
const { api } = buildClient(command);
|
|
688
|
+
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
689
|
+
const history = await api.rest(`/agent-conversations/${conversationId}/history`);
|
|
690
|
+
emit(history, () => printHistory(history));
|
|
691
|
+
});
|
|
692
|
+
conversation
|
|
693
|
+
.command('approve')
|
|
694
|
+
.argument('<conversationId>')
|
|
695
|
+
.requiredOption('--request <requestId>', 'exact requestId from conversation history')
|
|
696
|
+
.option('--tool <toolName>', 'exact toolName from the pending request, when present')
|
|
697
|
+
.description('Approve one pending tool request and resume the active conversation')
|
|
698
|
+
.action(async (rawId, opts, command) => {
|
|
699
|
+
const { api } = buildClient(command);
|
|
700
|
+
const result = await respondAgentPermission(api, rawId, opts.request, 'allow', {
|
|
701
|
+
toolName: opts.tool,
|
|
702
|
+
});
|
|
703
|
+
emit({ conversationId: Number(rawId), requestId: opts.request, behavior: 'allow', ...result }, () => printKeyValues([
|
|
704
|
+
['conversationId', rawId],
|
|
705
|
+
['requestId', opts.request],
|
|
706
|
+
['decision', 'approved — conversation resumed'],
|
|
707
|
+
['next', `cai agent conversation history ${rawId}`],
|
|
708
|
+
]));
|
|
709
|
+
});
|
|
710
|
+
conversation
|
|
711
|
+
.command('deny')
|
|
712
|
+
.argument('<conversationId>')
|
|
713
|
+
.requiredOption('--request <requestId>', 'exact requestId from conversation history')
|
|
714
|
+
.option('--tool <toolName>', 'exact toolName from the pending request, when present')
|
|
715
|
+
.option('--message <reason>', 'reason returned to the agent')
|
|
716
|
+
.description('Deny one pending tool request and resume the active conversation')
|
|
717
|
+
.action(async (rawId, opts, command) => {
|
|
718
|
+
const { api } = buildClient(command);
|
|
719
|
+
const result = await respondAgentPermission(api, rawId, opts.request, 'deny', {
|
|
720
|
+
toolName: opts.tool,
|
|
721
|
+
message: opts.message,
|
|
722
|
+
});
|
|
723
|
+
emit({ conversationId: Number(rawId), requestId: opts.request, behavior: 'deny', ...result }, () => printKeyValues([
|
|
724
|
+
['conversationId', rawId],
|
|
725
|
+
['requestId', opts.request],
|
|
726
|
+
['decision', 'denied — conversation resumed'],
|
|
727
|
+
['next', `cai agent conversation history ${rawId}`],
|
|
728
|
+
]));
|
|
729
|
+
});
|
|
730
|
+
conversation
|
|
731
|
+
.command('send')
|
|
732
|
+
.argument('<conversationId>')
|
|
733
|
+
.requiredOption('--message <text>', 'message to send')
|
|
734
|
+
.option('--wait', 'wait for the next completed persisted assistant reply', false)
|
|
735
|
+
.option('--timeout <seconds>', 'reply wait budget', '120')
|
|
736
|
+
.description('Send a message, optionally waiting for a verifiable persisted reply')
|
|
737
|
+
.action(async (rawId, opts, command) => {
|
|
738
|
+
const { api } = buildClient(command);
|
|
739
|
+
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
740
|
+
// Validate the wait budget BEFORE the message is posted: failing after
|
|
741
|
+
// the POST makes a retry send the message twice.
|
|
742
|
+
const timeoutSeconds = opts.wait ? parseSeconds(opts.timeout, '--timeout') : null;
|
|
743
|
+
const before = await api.rest(`/agent-conversations/${conversationId}/history`);
|
|
744
|
+
const afterMessageId = Math.max(0, ...(before.messages ?? []).map(message => message.id));
|
|
745
|
+
const afterEventId = Math.max(0, ...(before.events ?? []).map(event => event.id));
|
|
746
|
+
const accepted = await api.rest(`/agent-conversations/${conversationId}/send-message`, { method: 'POST', body: { message: opts.message } });
|
|
747
|
+
if (!opts.wait) {
|
|
748
|
+
emit({ conversationId, accepted: accepted.success }, () => printKeyValues([
|
|
749
|
+
['conversationId', conversationId],
|
|
750
|
+
['accepted', accepted.success ? 'yes' : 'no'],
|
|
751
|
+
['next', `cai agent conversation history ${conversationId}`],
|
|
752
|
+
]));
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const result = await pollAgentReply(api, conversationId, afterMessageId, afterEventId, (timeoutSeconds ?? 120) * 1_000);
|
|
756
|
+
const reply = requireReply(result, conversationId);
|
|
757
|
+
emit({ conversationId, accepted: true, reply, history: result.history }, () => printKeyValues([
|
|
758
|
+
['conversationId', conversationId],
|
|
759
|
+
['verified reply', contentText(reply.content)],
|
|
760
|
+
['cost USD', reply.totalCostUsd ?? '-'],
|
|
761
|
+
]));
|
|
762
|
+
});
|
|
763
|
+
agent
|
|
764
|
+
.command('test')
|
|
765
|
+
.argument('<agentId>')
|
|
766
|
+
.requiredOption('--message <text>', 'verification prompt')
|
|
767
|
+
.option('--draft', 'owner-only Test: current agent draft and workflow development heads (default)')
|
|
768
|
+
.option('--live', 'Live: latest published agent and workflow releases')
|
|
769
|
+
.option('--title <title>', 'conversation title', 'CLI verification')
|
|
770
|
+
.option('--model <model>', 'runtime model override')
|
|
771
|
+
.option('--timeout <seconds>', 'reply wait budget', '120')
|
|
772
|
+
.description('Create, send, and verify one persisted reply on an explicit agent rail')
|
|
773
|
+
.action(async (rawId, opts, command) => {
|
|
774
|
+
const { api, config } = buildClient(command);
|
|
775
|
+
const agentId = parsePositiveId(rawId, 'agentId');
|
|
776
|
+
const draft = selectedDraftMode(opts, true);
|
|
777
|
+
const timeoutSeconds = parseSeconds(opts.timeout, '--timeout');
|
|
778
|
+
const row = await api.rest(`/agents/${agentId}/conversations`, {
|
|
779
|
+
method: 'POST',
|
|
780
|
+
body: {
|
|
781
|
+
draft,
|
|
782
|
+
title: opts.title,
|
|
783
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
assertConversationRail(row, draft);
|
|
787
|
+
await api.rest(`/agent-conversations/${row.id}/send-message`, {
|
|
788
|
+
method: 'POST',
|
|
789
|
+
body: { message: opts.message },
|
|
790
|
+
});
|
|
791
|
+
const result = await pollAgentReply(api, row.id, 0, 0, timeoutSeconds * 1_000);
|
|
792
|
+
const reply = requireReply(result, row.id);
|
|
793
|
+
const appUrl = conversationUrl(config, agentId, row.id);
|
|
794
|
+
emit({
|
|
795
|
+
verified: true,
|
|
796
|
+
rail: draft ? 'draft' : 'live',
|
|
797
|
+
agentId,
|
|
798
|
+
...(appUrl ? { appUrl } : {}),
|
|
799
|
+
conversation: row,
|
|
800
|
+
reply,
|
|
801
|
+
history: result.history,
|
|
802
|
+
}, () => {
|
|
803
|
+
printConversation(row, appUrl);
|
|
804
|
+
process.stdout.write('\nverification\n');
|
|
805
|
+
printKeyValues([
|
|
806
|
+
['verified', 'yes'],
|
|
807
|
+
['reply', contentText(reply.content)],
|
|
808
|
+
['cost USD', reply.totalCostUsd ?? '-'],
|
|
809
|
+
]);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
};
|
|
813
|
+
//# sourceMappingURL=agent.js.map
|