@controllerai/cli 0.2.20 → 0.2.22
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 +6 -3
- package/dist/api/client.js +5 -38
- package/dist/api/client.js.map +1 -1
- package/dist/commands/agent.js +169 -146
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/expr.js +10 -3
- package/dist/commands/expr.js.map +1 -1
- package/dist/commands/integration.js +30 -10
- package/dist/commands/integration.js.map +1 -1
- package/dist/commands/skills.js +1 -0
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/templateSetup.js +42 -0
- package/dist/commands/templateSetup.js.map +1 -0
- package/dist/commands/workflow.js +6 -7
- package/dist/commands/workflow.js.map +1 -1
- package/dist/errors.js +2 -5
- package/dist/errors.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/permissionTool.js +1 -1
- package/dist/permissionTool.js.map +1 -1
- package/package.json +6 -4
- package/skills/controllerai-agents/SKILL.md +109 -26
- package/skills/controllerai-agents/references/instructions-and-versioning.md +32 -34
- package/skills/controllerai-ai-nodes/SKILL.md +83 -10
- package/skills/controllerai-ai-nodes/references/selection-and-testing.md +44 -26
- package/skills/controllerai-data/SKILL.md +106 -34
- package/skills/controllerai-data/references/data-rails.md +24 -26
- package/skills/controllerai-expressions/SKILL.md +82 -29
- package/skills/controllerai-expressions/references/dsl-and-recipes.md +42 -40
- package/skills/controllerai-flow-control/SKILL.md +104 -21
- package/skills/controllerai-flow-control/references/patterns.md +23 -60
- package/skills/controllerai-integration-actions/SKILL.md +89 -11
- package/skills/controllerai-node-config/SKILL.md +108 -18
- package/skills/controllerai-node-config/references/prop-and-readiness.md +28 -78
- package/skills/controllerai-runs/SKILL.md +82 -25
- package/skills/controllerai-runs/references/debugging.md +24 -22
- package/skills/controllerai-start/SKILL.md +63 -35
- package/skills/controllerai-start/references/mcp-tool-map.md +22 -21
- package/skills/controllerai-template-setup/SKILL.md +132 -0
- package/skills/controllerai-template-setup/agents/openai.yaml +4 -0
- package/skills/controllerai-triggers/SKILL.md +113 -16
- package/skills/controllerai-triggers/references/lifecycle.md +36 -48
- package/skills/controllerai-workflows/SKILL.md +105 -25
- package/skills/controllerai-workflows/references/command-map.md +20 -44
- package/skills/manifest.json +96 -136
- package/skills-mcp/controllerai-agents/SKILL.md +121 -0
- package/skills-mcp/controllerai-agents/references/instructions-and-versioning.md +76 -0
- package/skills-mcp/controllerai-ai-nodes/SKILL.md +91 -0
- package/skills-mcp/controllerai-ai-nodes/references/selection-and-testing.md +66 -0
- package/skills-mcp/controllerai-data/SKILL.md +116 -0
- package/skills-mcp/controllerai-data/references/data-rails.md +43 -0
- package/skills-mcp/controllerai-expressions/SKILL.md +97 -0
- package/skills-mcp/controllerai-expressions/references/dsl-and-recipes.md +136 -0
- package/skills-mcp/controllerai-flow-control/SKILL.md +112 -0
- package/skills-mcp/controllerai-flow-control/references/patterns.md +82 -0
- package/skills-mcp/controllerai-integration-actions/SKILL.md +90 -0
- package/skills-mcp/controllerai-node-config/SKILL.md +106 -0
- package/skills-mcp/controllerai-node-config/references/prop-and-readiness.md +49 -0
- package/skills-mcp/controllerai-runs/SKILL.md +87 -0
- package/skills-mcp/controllerai-runs/references/debugging.md +53 -0
- package/skills-mcp/controllerai-start/SKILL.md +82 -0
- package/skills-mcp/controllerai-start/references/mcp-tool-map.md +177 -0
- package/skills-mcp/controllerai-template-setup/SKILL.md +10 -0
- package/skills-mcp/controllerai-triggers/SKILL.md +109 -0
- package/skills-mcp/controllerai-triggers/references/lifecycle.md +59 -0
- package/skills-mcp/controllerai-workflows/SKILL.md +108 -0
- package/skills-mcp/controllerai-workflows/references/command-map.md +1 -0
- package/skills-mcp/manifest.json +200 -0
- package/skills/controllerai-agents/references/command-reference.md +0 -205
- package/skills/controllerai-ai-nodes/references/command-reference.md +0 -98
- package/skills/controllerai-data/references/command-reference.md +0 -177
- package/skills/controllerai-expressions/references/command-reference.md +0 -166
- package/skills/controllerai-flow-control/references/command-reference.md +0 -171
- package/skills/controllerai-integration-actions/references/command-reference.md +0 -118
- package/skills/controllerai-node-config/references/command-reference.md +0 -186
- package/skills/controllerai-runs/references/command-reference.md +0 -113
- package/skills/controllerai-triggers/references/command-reference.md +0 -215
- package/skills/controllerai-workflows/references/command-reference.md +0 -118
package/dist/commands/agent.js
CHANGED
|
@@ -1,38 +1,10 @@
|
|
|
1
|
-
import { readFileSync
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { agentUrl, conversationUrl } from '../appLinks.js';
|
|
3
3
|
import { buildClient } from '../context.js';
|
|
4
4
|
import { CaiError, EXIT, userError } from '../errors.js';
|
|
5
5
|
import { emit, printKeyValues, printTable, truncate, warn } from '../output.js';
|
|
6
6
|
import { isCanonicalPermissionToolName, recordedPermissionToolName, resolvePermissionTool, } from '../permissionTool.js';
|
|
7
7
|
import { registerAgentSetup } from './agentSetup.js';
|
|
8
|
-
/** Public status deliberately excludes SDK session data and frozen tool snapshots. */
|
|
9
|
-
export const compactConversation = (row) => {
|
|
10
|
-
if (!row)
|
|
11
|
-
return null;
|
|
12
|
-
const { id, agentId, title, status, model, isDraft, agentVersionId, agentVersionNumber, createdAt, updatedAt } = row;
|
|
13
|
-
return { id, agentId, title, status, model, isDraft, agentVersionId, agentVersionNumber, createdAt, updatedAt };
|
|
14
|
-
};
|
|
15
|
-
export const compactConversationHistory = (history, limit = 20, beforeId) => {
|
|
16
|
-
const eligible = (history.messages ?? []).filter(message => beforeId === undefined || message.id < beforeId);
|
|
17
|
-
const selected = eligible.slice(-limit);
|
|
18
|
-
const messages = selected.map(message => {
|
|
19
|
-
const text = contentText(message.content);
|
|
20
|
-
return { id: message.id, role: message.role, content: text.slice(0, 8000),
|
|
21
|
-
...(text.length > 8000 ? { contentTruncated: true } : {}),
|
|
22
|
-
...(message.createdAt ? { createdAt: message.createdAt } : {}),
|
|
23
|
-
...(message.totalCostUsd !== undefined ? { totalCostUsd: message.totalCostUsd } : {}) };
|
|
24
|
-
});
|
|
25
|
-
const latest = lastReplySummary(history);
|
|
26
|
-
return {
|
|
27
|
-
conversation: compactConversation(history.conversation), messages,
|
|
28
|
-
pendingHitlRequests: (history.pendingHitlRequests ?? []).map(request => ({ id: request.id, requestId: request.requestId, agentToolId: request.agentToolId })),
|
|
29
|
-
status: latest.status, turnComplete: latest.turnComplete,
|
|
30
|
-
hasMore: eligible.length > selected.length,
|
|
31
|
-
nextBeforeId: eligible.length > selected.length ? selected[0]?.id ?? null : null,
|
|
32
|
-
totalMessages: history.messages?.length ?? 0,
|
|
33
|
-
eventCount: history.events?.length ?? 0,
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
8
|
export const resolveConfirmationChoice = (opts) => {
|
|
37
9
|
if (opts.requireConfirmation === true && opts.confirmation === false) {
|
|
38
10
|
throw userError('--require-confirmation and --no-confirmation are mutually exclusive.', 'CONFLICTING_INPUT');
|
|
@@ -46,17 +18,86 @@ export const resolveConfirmationChoice = (opts) => {
|
|
|
46
18
|
const confirmationReview = (required) => required
|
|
47
19
|
? 'approval required before every run'
|
|
48
20
|
: 'runs without approval — review writes, messages, deletion, and spend before attaching';
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
21
|
+
const slackRouteChoice = (opts) => {
|
|
22
|
+
const hasRouteFlags = opts.slackConnection !== undefined ||
|
|
23
|
+
opts.slackChannel !== undefined ||
|
|
24
|
+
opts.slackChannelName !== undefined;
|
|
25
|
+
if (opts.slack === false) {
|
|
26
|
+
if (hasRouteFlags) {
|
|
27
|
+
throw userError('--no-slack cannot be combined with --slack-* flags.', 'CONFLICTING_INPUT');
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!hasRouteFlags)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (opts.slackConnection === undefined || opts.slackChannel === undefined) {
|
|
34
|
+
throw userError('Pass --slack-connection and --slack-channel together; --slack-channel-name is optional.', 'SLACK_ROUTE_PAIR_REQUIRED');
|
|
35
|
+
}
|
|
36
|
+
if (!opts.slackChannel.trim() || opts.slackChannelName?.trim() === '') {
|
|
37
|
+
throw userError('Slack channel id and name must not be empty.', 'BAD_SLACK_CHANNEL');
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
connectionId: parsePositiveId(opts.slackConnection, 'slack connectionId'),
|
|
41
|
+
targetType: 'channel',
|
|
42
|
+
channelId: opts.slackChannel.trim(),
|
|
43
|
+
...(opts.slackChannelName !== undefined ? { channelName: opts.slackChannelName.trim() } : {}),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const requireSlackApproval = (requiresConfirmation) => {
|
|
47
|
+
if (requiresConfirmation !== true) {
|
|
48
|
+
throw userError('Slack routes only matter for tools that require approval. Pass --require-confirmation ' +
|
|
49
|
+
'to configure a Slack approval route.', 'SLACK_ROUTE_REQUIRES_CONFIRMATION');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const slackApprovals = (route) => !route || route.enabled === false ? 'no' : route.channelName || route.channelId || 'no';
|
|
53
|
+
const slackApprovalReview = (route) => {
|
|
54
|
+
if (route === undefined)
|
|
55
|
+
return 'not returned — verify with `cai agent tools`';
|
|
56
|
+
if (!route || route.enabled === false)
|
|
57
|
+
return 'not posted to Slack';
|
|
58
|
+
const channel = route.channelName
|
|
59
|
+
? `#${route.channelName.replace(/^#/, '')} (${route.channelId})`
|
|
60
|
+
: route.channelId;
|
|
61
|
+
return `${channel ?? route.people?.join(', ') ?? '-'} via connection ${route.connectionId}`;
|
|
62
|
+
};
|
|
63
|
+
const isDirectActionToolType = (toolType) => toolType === 'pipedream' || toolType === 'registry';
|
|
64
|
+
export const directActionToolOp = async (api, input) => {
|
|
65
|
+
const [node, details] = await Promise.all([
|
|
66
|
+
api.rest(`/nodes/${encodeURIComponent(input.integration)}`),
|
|
67
|
+
api.rest('/nodes/component/get', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: { nodeSlug: input.integration, key: input.action },
|
|
70
|
+
}),
|
|
71
|
+
]);
|
|
72
|
+
if (details?.component?.agentExposable !== true) {
|
|
73
|
+
throw userError(`Action "${input.action}" on integration "${input.integration}" is not attachable to ` +
|
|
74
|
+
'agents. Control-flow, data, file, agent, AI, and HTTP nodes are never attachable.', 'ACTION_NOT_AGENT_ATTACHABLE');
|
|
75
|
+
}
|
|
76
|
+
if (!isDirectActionToolType(node?.source)) {
|
|
77
|
+
throw userError(`Integration "${input.integration}" returned an unsupported action source; expected ` +
|
|
78
|
+
'registry or pipedream.', 'BAD_ACTION_SOURCE');
|
|
79
|
+
}
|
|
80
|
+
if (node.authRequired !== true && input.connectionId !== undefined) {
|
|
81
|
+
throw userError(`Action "${input.action}" on integration "${input.integration}" needs no connection. ` +
|
|
82
|
+
'Omit --connection and retry.', 'ACTION_CONNECTION_NOT_USED');
|
|
83
|
+
}
|
|
84
|
+
if (node.authRequired === true && input.connectionId === undefined) {
|
|
85
|
+
throw userError(`Action "${input.action}" on integration "${input.integration}" requires a connection. ` +
|
|
86
|
+
`Keep this exact action, run \`cai connection connect ${input.integration}\` to authorize ` +
|
|
87
|
+
'it, then retry with --connection <id>.', 'ACTION_CONNECTION_REQUIRED');
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
op: 'add',
|
|
91
|
+
toolType: node.source,
|
|
92
|
+
nodeSlug: input.integration,
|
|
93
|
+
nodeComponentKey: input.action,
|
|
94
|
+
...(input.connectionId !== undefined ? { connectionId: input.connectionId } : {}),
|
|
95
|
+
...(input.description !== undefined ? { toolDescription: input.description } : {}),
|
|
96
|
+
// Never omit this field: the product default is no approval while older
|
|
97
|
+
// backend entry points historically defaulted an omitted value to approval.
|
|
98
|
+
requiresConfirmation: input.requiresConfirmation,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
60
101
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
61
102
|
const parsePositiveId = (raw, label) => {
|
|
62
103
|
const value = Number(raw);
|
|
@@ -74,7 +115,7 @@ const parseAgentFileSpace = (raw) => {
|
|
|
74
115
|
return 'shared';
|
|
75
116
|
if (raw === 'conversation') {
|
|
76
117
|
throw userError('--space conversation is not an agent file space. Conversation files use ' +
|
|
77
|
-
'
|
|
118
|
+
'REST /agent-conversations/:conversationId/files routes; CLI file support is future work.', 'BAD_AGENT_FILE_SPACE');
|
|
78
119
|
}
|
|
79
120
|
throw userError('--space must be knowledge, shared, or runtime (an alias for shared).', 'BAD_AGENT_FILE_SPACE');
|
|
80
121
|
};
|
|
@@ -177,7 +218,10 @@ const updateDraft = async (api, agentId, patch) => {
|
|
|
177
218
|
try {
|
|
178
219
|
return await api.rest(`/agents/${agentId}/draft`, {
|
|
179
220
|
method: 'PATCH',
|
|
180
|
-
body: {
|
|
221
|
+
body: {
|
|
222
|
+
baseUpdatedAt: current.updatedAt,
|
|
223
|
+
...(typeof patch === 'function' ? await patch() : patch),
|
|
224
|
+
},
|
|
181
225
|
});
|
|
182
226
|
}
|
|
183
227
|
catch (error) {
|
|
@@ -604,10 +648,16 @@ export const registerAgent = (program) => {
|
|
|
604
648
|
.requiredOption('--flow <flowId>', 'flow exposed to the agent')
|
|
605
649
|
.option('--description <text>', 'description the model uses when choosing the tool')
|
|
606
650
|
.option('--require-confirmation', 'pause every invocation for an explicit user approval before the workflow runs', false)
|
|
651
|
+
.option('--slack-connection <connectionId>', 'your active Slack connection; requires --slack-channel and --require-confirmation')
|
|
652
|
+
.option('--slack-channel <channelId>', 'approval channel; requires --slack-connection')
|
|
653
|
+
.option('--slack-channel-name <name>', 'optional approval channel display name')
|
|
607
654
|
.description('Add a follow-live workflow tool with a backend-generated name to the draft')
|
|
608
655
|
.action(async (rawId, opts, command) => {
|
|
609
656
|
const { api } = buildClient(command);
|
|
610
657
|
const agentId = parsePositiveId(rawId, 'agentId');
|
|
658
|
+
const slackRoute = slackRouteChoice(opts);
|
|
659
|
+
if (slackRoute)
|
|
660
|
+
requireSlackApproval(opts.requireConfirmation);
|
|
611
661
|
const result = await updateDraft(api, agentId, {
|
|
612
662
|
toolOps: [
|
|
613
663
|
{
|
|
@@ -619,11 +669,13 @@ export const registerAgent = (program) => {
|
|
|
619
669
|
// Workflow tools always follow the latest published workflow release.
|
|
620
670
|
versionPolicy: 'latest',
|
|
621
671
|
requiresConfirmation: Boolean(opts.requireConfirmation),
|
|
672
|
+
...(slackRoute !== undefined ? { slackRoute } : {}),
|
|
622
673
|
},
|
|
623
674
|
],
|
|
624
675
|
});
|
|
625
676
|
const added = result.tools.find(tool => tool.workflowId === Number(opts.workflowId) && tool.flowId === opts.flow);
|
|
626
|
-
|
|
677
|
+
const slackReview = slackApprovalReview(added?.slackRoute);
|
|
678
|
+
emit({ agentId, tool: added ?? null, liveChanged: false, slackApprovals: slackReview }, () => printKeyValues([
|
|
627
679
|
['agentId', agentId],
|
|
628
680
|
['toolId', added?.id ?? '-'],
|
|
629
681
|
['tool', added?.toolName ?? '(generated by backend)'],
|
|
@@ -635,6 +687,7 @@ export const registerAgent = (program) => {
|
|
|
635
687
|
? 'not required'
|
|
636
688
|
: 'not returned — verify with `cai agent tools`',
|
|
637
689
|
],
|
|
690
|
+
['Slack approvals', slackReview],
|
|
638
691
|
['state', 'draft updated — live version unchanged'],
|
|
639
692
|
]));
|
|
640
693
|
});
|
|
@@ -643,41 +696,49 @@ export const registerAgent = (program) => {
|
|
|
643
696
|
.argument('<agentId>')
|
|
644
697
|
.requiredOption('--integration <slug>', 'exact slug from `cai integration search`')
|
|
645
698
|
.requiredOption('--action <key>', 'exact action key from `cai integration actions`')
|
|
646
|
-
.
|
|
699
|
+
.option('--connection <id>', 'active connection when this integration requires auth')
|
|
647
700
|
.option('--description <text>', 'when the agent should choose this action')
|
|
648
701
|
.option('--require-confirmation', 'require approval before each run; default is no approval, so decide for writes or spend', false)
|
|
702
|
+
.option('--slack-connection <connectionId>', 'your active Slack connection; requires --slack-channel and --require-confirmation')
|
|
703
|
+
.option('--slack-channel <channelId>', 'approval channel; requires --slack-connection')
|
|
704
|
+
.option('--slack-channel-name <name>', 'optional approval channel display name')
|
|
649
705
|
.description('Attach one ready integration action to the draft; explicitly review its confirmation policy')
|
|
650
706
|
.action(async (rawId, opts, command) => {
|
|
651
707
|
const { api } = buildClient(command);
|
|
652
708
|
const agentId = parsePositiveId(rawId, 'agentId');
|
|
653
|
-
const connectionId = parsePositiveId(opts.connection, 'connectionId');
|
|
709
|
+
const connectionId = opts.connection === undefined ? undefined : parsePositiveId(opts.connection, 'connectionId');
|
|
654
710
|
const requiresConfirmation = Boolean(opts.requireConfirmation);
|
|
711
|
+
const slackRoute = slackRouteChoice(opts);
|
|
712
|
+
if (slackRoute)
|
|
713
|
+
requireSlackApproval(requiresConfirmation);
|
|
714
|
+
const toolOp = await directActionToolOp(api, {
|
|
715
|
+
integration: opts.integration,
|
|
716
|
+
action: opts.action,
|
|
717
|
+
connectionId,
|
|
718
|
+
description: opts.description,
|
|
719
|
+
requiresConfirmation,
|
|
720
|
+
});
|
|
655
721
|
const result = await updateDraft(api, agentId, {
|
|
656
|
-
toolOps: [
|
|
657
|
-
directActionToolOp({
|
|
658
|
-
integration: opts.integration,
|
|
659
|
-
action: opts.action,
|
|
660
|
-
connectionId,
|
|
661
|
-
description: opts.description,
|
|
662
|
-
requiresConfirmation,
|
|
663
|
-
}),
|
|
664
|
-
],
|
|
722
|
+
toolOps: [{ ...toolOp, ...(slackRoute !== undefined ? { slackRoute } : {}) }],
|
|
665
723
|
});
|
|
666
|
-
const added = result.tools.find(tool => tool.toolType ===
|
|
724
|
+
const added = result.tools.find(tool => tool.toolType === toolOp.toolType &&
|
|
667
725
|
tool.nodeSlug === opts.integration &&
|
|
668
726
|
tool.nodeComponentKey === opts.action);
|
|
727
|
+
const slackReview = slackApprovalReview(added?.slackRoute);
|
|
669
728
|
emit({
|
|
670
729
|
agentId,
|
|
671
730
|
tool: added ?? null,
|
|
672
731
|
liveChanged: false,
|
|
673
732
|
confirmationReview: confirmationReview(requiresConfirmation),
|
|
733
|
+
slackApprovals: slackReview,
|
|
674
734
|
}, () => printKeyValues([
|
|
675
735
|
['agentId', agentId],
|
|
676
736
|
['toolId', added?.id ?? '-'],
|
|
677
737
|
['integration', opts.integration],
|
|
678
738
|
['action', opts.action],
|
|
679
|
-
['connection', connectionId],
|
|
739
|
+
['connection', connectionId ?? 'not required'],
|
|
680
740
|
['confirmation', confirmationReview(requiresConfirmation)],
|
|
741
|
+
['Slack approvals', slackReview],
|
|
681
742
|
['state', 'draft updated — live version unchanged'],
|
|
682
743
|
]));
|
|
683
744
|
});
|
|
@@ -689,37 +750,60 @@ export const registerAgent = (program) => {
|
|
|
689
750
|
.option('--connection <id>', 'rebind a ready integration action to another connection')
|
|
690
751
|
.option('--require-confirmation', 'require approval before every invocation')
|
|
691
752
|
.option('--no-confirmation', 'run without approval; use only after reviewing writes, messages, deletion, and spend')
|
|
692
|
-
.
|
|
753
|
+
.option('--slack-connection <connectionId>', 'your active Slack connection; requires --slack-channel and an approval-requiring tool')
|
|
754
|
+
.option('--slack-channel <channelId>', 'approval channel; requires --slack-connection')
|
|
755
|
+
.option('--slack-channel-name <name>', 'optional approval channel display name')
|
|
756
|
+
.option('--no-slack', 'remove Slack approval routing from the draft tool')
|
|
757
|
+
.description('Update a draft tool; action connection grants apply immediately without publishing')
|
|
693
758
|
.action(async (rawId, opts, command) => {
|
|
694
759
|
const { api } = buildClient(command);
|
|
695
760
|
const agentId = parsePositiveId(rawId, 'agentId');
|
|
696
761
|
const toolId = parsePositiveId(opts.tool, 'toolId');
|
|
697
762
|
const requiresConfirmation = resolveConfirmationChoice(opts);
|
|
763
|
+
const slackRoute = slackRouteChoice(opts);
|
|
764
|
+
if (slackRoute && requiresConfirmation !== undefined)
|
|
765
|
+
requireSlackApproval(requiresConfirmation);
|
|
698
766
|
if (opts.description === undefined &&
|
|
699
767
|
opts.connection === undefined &&
|
|
700
|
-
requiresConfirmation === undefined
|
|
701
|
-
|
|
768
|
+
requiresConfirmation === undefined &&
|
|
769
|
+
slackRoute === undefined) {
|
|
770
|
+
throw userError('Pass --description, --connection, --require-confirmation, --no-confirmation, ' +
|
|
771
|
+
'--slack-connection with --slack-channel, or --no-slack.', 'NO_CHANGES');
|
|
702
772
|
}
|
|
703
|
-
const result = await updateDraft(api, agentId, {
|
|
704
|
-
|
|
705
|
-
{
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
773
|
+
const result = await updateDraft(api, agentId, async () => {
|
|
774
|
+
if (slackRoute && requiresConfirmation === undefined) {
|
|
775
|
+
const currentTools = await api.rest(`/agents/${agentId}/tools`);
|
|
776
|
+
const currentTool = (currentTools ?? []).find(tool => tool.id === toolId);
|
|
777
|
+
if (!currentTool)
|
|
778
|
+
throw userError(`Agent tool ${toolId} was not found.`, 'AGENT_TOOL_NOT_FOUND');
|
|
779
|
+
requireSlackApproval(currentTool.requiresConfirmation);
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
toolOps: [
|
|
783
|
+
{
|
|
784
|
+
op: 'update',
|
|
785
|
+
toolId,
|
|
786
|
+
...(opts.description !== undefined ? { toolDescription: opts.description } : {}),
|
|
787
|
+
...(opts.connection !== undefined
|
|
788
|
+
? { connectionId: parsePositiveId(opts.connection, 'connectionId') }
|
|
789
|
+
: {}),
|
|
790
|
+
...(requiresConfirmation !== undefined ? { requiresConfirmation } : {}),
|
|
791
|
+
...(slackRoute !== undefined ? { slackRoute } : {}),
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
};
|
|
715
795
|
});
|
|
716
796
|
const updated = result.tools.find(tool => tool.id === toolId);
|
|
717
|
-
const liveAuthorizationChanged = opts.connection !== undefined && updated?.toolType
|
|
797
|
+
const liveAuthorizationChanged = opts.connection !== undefined && isDirectActionToolType(updated?.toolType);
|
|
798
|
+
const liveSlackRoutingChanged = slackRoute !== undefined;
|
|
799
|
+
const slackReview = slackApprovalReview(updated?.slackRoute);
|
|
718
800
|
emit({
|
|
719
801
|
agentId,
|
|
720
802
|
tool: updated ?? null,
|
|
721
|
-
liveChanged: liveAuthorizationChanged,
|
|
803
|
+
liveChanged: liveAuthorizationChanged || liveSlackRoutingChanged,
|
|
722
804
|
liveAuthorizationChanged,
|
|
805
|
+
...(liveSlackRoutingChanged ? { liveSlackRoutingChanged } : {}),
|
|
806
|
+
slackApprovals: slackReview,
|
|
723
807
|
...(requiresConfirmation !== undefined
|
|
724
808
|
? { confirmationReview: confirmationReview(requiresConfirmation) }
|
|
725
809
|
: {}),
|
|
@@ -733,6 +817,7 @@ export const registerAgent = (program) => {
|
|
|
733
817
|
? 'unchanged'
|
|
734
818
|
: confirmationReview(requiresConfirmation),
|
|
735
819
|
],
|
|
820
|
+
['Slack approvals', slackReview],
|
|
736
821
|
[
|
|
737
822
|
'state',
|
|
738
823
|
liveAuthorizationChanged
|
|
@@ -753,7 +838,7 @@ export const registerAgent = (program) => {
|
|
|
753
838
|
const currentTools = await api.rest(`/agents/${agentId}/tools`);
|
|
754
839
|
const removed = (currentTools ?? []).find(tool => tool.id === toolId);
|
|
755
840
|
const result = await updateDraft(api, agentId, { toolOps: [{ op: 'remove', toolId }] });
|
|
756
|
-
const liveAuthorizationRevoked = removed?.toolType
|
|
841
|
+
const liveAuthorizationRevoked = isDirectActionToolType(removed?.toolType);
|
|
757
842
|
emit({
|
|
758
843
|
agentId,
|
|
759
844
|
removedToolId: toolId,
|
|
@@ -910,7 +995,7 @@ export const registerAgent = (program) => {
|
|
|
910
995
|
agent
|
|
911
996
|
.command('tools')
|
|
912
997
|
.argument('<agentId>')
|
|
913
|
-
.description('List owner draft tools or the published projection visible to a shared user')
|
|
998
|
+
.description('List owner draft tools with Slack approvals, or the published projection visible to a shared user')
|
|
914
999
|
.action(async (rawId, _opts, command) => {
|
|
915
1000
|
const { api } = buildClient(command);
|
|
916
1001
|
const agentId = parsePositiveId(rawId, 'agentId');
|
|
@@ -922,6 +1007,7 @@ export const registerAgent = (program) => {
|
|
|
922
1007
|
action: tool.nodeComponentKey ?? tool.flowName ?? tool.flowId ?? null,
|
|
923
1008
|
confirm: Boolean(tool.requiresConfirmation),
|
|
924
1009
|
connection: tool.connectionId ?? null,
|
|
1010
|
+
slackApprovals: slackApprovals(tool.slackRoute),
|
|
925
1011
|
}));
|
|
926
1012
|
emit({ agentId, tools: annotated }, () => printTable([
|
|
927
1013
|
'id',
|
|
@@ -933,6 +1019,7 @@ export const registerAgent = (program) => {
|
|
|
933
1019
|
'tier',
|
|
934
1020
|
'policy',
|
|
935
1021
|
'confirm',
|
|
1022
|
+
'slackApprovals',
|
|
936
1023
|
], (tools ?? []).map(tool => [
|
|
937
1024
|
tool.id,
|
|
938
1025
|
tool.toolType,
|
|
@@ -943,6 +1030,7 @@ export const registerAgent = (program) => {
|
|
|
943
1030
|
tool.schemaTier ?? '-',
|
|
944
1031
|
tool.versionPolicy ?? '-',
|
|
945
1032
|
tool.requiresConfirmation ? 'required' : 'no',
|
|
1033
|
+
slackApprovals(tool.slackRoute),
|
|
946
1034
|
])));
|
|
947
1035
|
});
|
|
948
1036
|
agent
|
|
@@ -1097,16 +1185,13 @@ export const registerAgent = (program) => {
|
|
|
1097
1185
|
const { api } = buildClient(command);
|
|
1098
1186
|
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
1099
1187
|
const row = await api.rest(`/agent-conversations/${conversationId}`);
|
|
1100
|
-
emit(
|
|
1188
|
+
emit(row, () => printConversation(row));
|
|
1101
1189
|
});
|
|
1102
1190
|
conversation
|
|
1103
1191
|
.command('history')
|
|
1104
1192
|
.argument('<conversationId>')
|
|
1105
1193
|
.option('--last', 'aggregates the assistant turn: status, text, tool-call summary, cost')
|
|
1106
|
-
.
|
|
1107
|
-
.option('--limit <n>', 'recent messages per page (1-100)', '20')
|
|
1108
|
-
.option('--before <id>', 'messages before nextBeforeId from the previous page')
|
|
1109
|
-
.description('Read a compact paginated transcript; use --last for the latest reply or --raw for diagnostics')
|
|
1194
|
+
.description('Read persisted messages, events, and pending permission requests')
|
|
1110
1195
|
.action(async (rawId, opts, command) => {
|
|
1111
1196
|
const { api } = buildClient(command);
|
|
1112
1197
|
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
@@ -1132,69 +1217,7 @@ export const registerAgent = (program) => {
|
|
|
1132
1217
|
]));
|
|
1133
1218
|
return;
|
|
1134
1219
|
}
|
|
1135
|
-
|
|
1136
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
1137
|
-
throw userError('--limit must be an integer from 1 through 100.', 'BAD_LIMIT');
|
|
1138
|
-
const beforeId = opts.before ? parsePositiveId(opts.before, 'before') : undefined;
|
|
1139
|
-
emit(opts.raw ? history : compactConversationHistory(history, limit, beforeId), () => printHistory(history));
|
|
1140
|
-
});
|
|
1141
|
-
conversation
|
|
1142
|
-
.command('files')
|
|
1143
|
-
.argument('<conversationId>')
|
|
1144
|
-
.option('--cursor <cursor>', 'nextCursor from the previous page')
|
|
1145
|
-
.option('--limit <n>', 'files per page (1-200)', '50')
|
|
1146
|
-
.option('--search <query>', 'file name search')
|
|
1147
|
-
.description('List conversation attachments and generated files with IDs, paths and sizes')
|
|
1148
|
-
.action(async (rawId, opts, command) => {
|
|
1149
|
-
const { api } = buildClient(command);
|
|
1150
|
-
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
1151
|
-
const query = new URLSearchParams({ purpose: 'all', limit: String(parseAgentFilesLimit(opts.limit)) });
|
|
1152
|
-
if (opts.cursor)
|
|
1153
|
-
query.set('cursor', opts.cursor);
|
|
1154
|
-
if (opts.search)
|
|
1155
|
-
query.set('search', opts.search);
|
|
1156
|
-
const result = await api.rest(`/agent-conversations/${conversationId}/files?${query}`);
|
|
1157
|
-
emit({ conversationId, ...result }, () => printTable(['id', 'name', 'path', 'bytes'], result.files.map(file => [file.id, file.originalName, file.sandboxPath, file.sizeBytes])));
|
|
1158
|
-
});
|
|
1159
|
-
conversation
|
|
1160
|
-
.command('read-file')
|
|
1161
|
-
.argument('<conversationId>')
|
|
1162
|
-
.requiredOption('--file-id <id>', 'file ID returned by conversation files')
|
|
1163
|
-
.description('Read the text content of an attached or generated conversation file')
|
|
1164
|
-
.action(async (rawId, opts, command) => {
|
|
1165
|
-
const { api } = buildClient(command);
|
|
1166
|
-
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
1167
|
-
const result = await api.rest(`/agent-conversations/${conversationId}/files/${encodeURIComponent(opts.fileId)}/content`);
|
|
1168
|
-
emit({ conversationId, ...result }, () => process.stdout.write(result.content + '\n'));
|
|
1169
|
-
});
|
|
1170
|
-
conversation
|
|
1171
|
-
.command('download-file')
|
|
1172
|
-
.argument('<conversationId>')
|
|
1173
|
-
.requiredOption('--file-id <id>', 'file ID returned by conversation files')
|
|
1174
|
-
.requiredOption('--out <path>', 'new local file path; existing files are not overwritten')
|
|
1175
|
-
.description('Download the original bytes of an attached or generated conversation file')
|
|
1176
|
-
.action(async (rawId, opts, command) => {
|
|
1177
|
-
const { api } = buildClient(command);
|
|
1178
|
-
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
1179
|
-
const bytes = await api.download(`/agent-conversations/${conversationId}/files/${encodeURIComponent(opts.fileId)}/download`);
|
|
1180
|
-
writeFileSync(opts.out, bytes, { flag: 'wx' });
|
|
1181
|
-
emit({ conversationId, fileId: opts.fileId, path: opts.out, sizeBytes: bytes.length }, () => printKeyValues([['file', opts.out], ['bytes', bytes.length]]));
|
|
1182
|
-
});
|
|
1183
|
-
conversation
|
|
1184
|
-
.command('add-file')
|
|
1185
|
-
.argument('<conversationId>')
|
|
1186
|
-
.requiredOption('--file <path>', 'local UTF-8 file to attach')
|
|
1187
|
-
.requiredOption('--name <name>', 'stored file name')
|
|
1188
|
-
.requiredOption('--type <type>', 'txt, md, json, csv, html or xml')
|
|
1189
|
-
.description('Attach a text file to a conversation so the agent can read and process it')
|
|
1190
|
-
.action(async (rawId, opts, command) => {
|
|
1191
|
-
const { api } = buildClient(command);
|
|
1192
|
-
const conversationId = parsePositiveId(rawId, 'conversationId');
|
|
1193
|
-
const content = readFileSync(opts.file, 'utf8');
|
|
1194
|
-
const result = await api.rest(`/agent-conversations/${conversationId}/files`, {
|
|
1195
|
-
method: 'POST', body: { fileName: opts.name, fileType: opts.type, content },
|
|
1196
|
-
});
|
|
1197
|
-
emit({ ...result, conversationId }, () => printKeyValues([['file', result.originalName], ['id', result.id], ['path', result.sandboxPath]]));
|
|
1220
|
+
emit(history, () => printHistory(history));
|
|
1198
1221
|
});
|
|
1199
1222
|
conversation
|
|
1200
1223
|
.command('approve')
|
|
@@ -1263,7 +1286,7 @@ export const registerAgent = (program) => {
|
|
|
1263
1286
|
}
|
|
1264
1287
|
const result = await pollAgentReply(api, conversationId, afterMessageId, afterEventId, (timeoutSeconds ?? 120) * 1_000);
|
|
1265
1288
|
const reply = requireReply(result, conversationId);
|
|
1266
|
-
emit({ conversationId, accepted: true, reply, history: result.history
|
|
1289
|
+
emit({ conversationId, accepted: true, reply, history: result.history }, () => printKeyValues([
|
|
1267
1290
|
['conversationId', conversationId],
|
|
1268
1291
|
['verified reply', contentText(reply.content)],
|
|
1269
1292
|
['cost USD', reply.totalCostUsd ?? '-'],
|
|
@@ -1305,9 +1328,9 @@ export const registerAgent = (program) => {
|
|
|
1305
1328
|
rail: draft ? 'draft' : 'live',
|
|
1306
1329
|
agentId,
|
|
1307
1330
|
...(appUrl ? { appUrl } : {}),
|
|
1308
|
-
conversation:
|
|
1331
|
+
conversation: row,
|
|
1309
1332
|
reply,
|
|
1310
|
-
history: result.history
|
|
1333
|
+
history: result.history,
|
|
1311
1334
|
}, () => {
|
|
1312
1335
|
printConversation(row, appUrl);
|
|
1313
1336
|
process.stdout.write('\nverification\n');
|