@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.
Files changed (79) hide show
  1. package/README.md +6 -3
  2. package/dist/api/client.js +5 -38
  3. package/dist/api/client.js.map +1 -1
  4. package/dist/commands/agent.js +169 -146
  5. package/dist/commands/agent.js.map +1 -1
  6. package/dist/commands/expr.js +10 -3
  7. package/dist/commands/expr.js.map +1 -1
  8. package/dist/commands/integration.js +30 -10
  9. package/dist/commands/integration.js.map +1 -1
  10. package/dist/commands/skills.js +1 -0
  11. package/dist/commands/skills.js.map +1 -1
  12. package/dist/commands/templateSetup.js +42 -0
  13. package/dist/commands/templateSetup.js.map +1 -0
  14. package/dist/commands/workflow.js +6 -7
  15. package/dist/commands/workflow.js.map +1 -1
  16. package/dist/errors.js +2 -5
  17. package/dist/errors.js.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/permissionTool.js +1 -1
  21. package/dist/permissionTool.js.map +1 -1
  22. package/package.json +6 -4
  23. package/skills/controllerai-agents/SKILL.md +109 -26
  24. package/skills/controllerai-agents/references/instructions-and-versioning.md +32 -34
  25. package/skills/controllerai-ai-nodes/SKILL.md +83 -10
  26. package/skills/controllerai-ai-nodes/references/selection-and-testing.md +44 -26
  27. package/skills/controllerai-data/SKILL.md +106 -34
  28. package/skills/controllerai-data/references/data-rails.md +24 -26
  29. package/skills/controllerai-expressions/SKILL.md +82 -29
  30. package/skills/controllerai-expressions/references/dsl-and-recipes.md +42 -40
  31. package/skills/controllerai-flow-control/SKILL.md +104 -21
  32. package/skills/controllerai-flow-control/references/patterns.md +23 -60
  33. package/skills/controllerai-integration-actions/SKILL.md +89 -11
  34. package/skills/controllerai-node-config/SKILL.md +108 -18
  35. package/skills/controllerai-node-config/references/prop-and-readiness.md +28 -78
  36. package/skills/controllerai-runs/SKILL.md +82 -25
  37. package/skills/controllerai-runs/references/debugging.md +24 -22
  38. package/skills/controllerai-start/SKILL.md +63 -35
  39. package/skills/controllerai-start/references/mcp-tool-map.md +22 -21
  40. package/skills/controllerai-template-setup/SKILL.md +132 -0
  41. package/skills/controllerai-template-setup/agents/openai.yaml +4 -0
  42. package/skills/controllerai-triggers/SKILL.md +113 -16
  43. package/skills/controllerai-triggers/references/lifecycle.md +36 -48
  44. package/skills/controllerai-workflows/SKILL.md +105 -25
  45. package/skills/controllerai-workflows/references/command-map.md +20 -44
  46. package/skills/manifest.json +96 -136
  47. package/skills-mcp/controllerai-agents/SKILL.md +121 -0
  48. package/skills-mcp/controllerai-agents/references/instructions-and-versioning.md +76 -0
  49. package/skills-mcp/controllerai-ai-nodes/SKILL.md +91 -0
  50. package/skills-mcp/controllerai-ai-nodes/references/selection-and-testing.md +66 -0
  51. package/skills-mcp/controllerai-data/SKILL.md +116 -0
  52. package/skills-mcp/controllerai-data/references/data-rails.md +43 -0
  53. package/skills-mcp/controllerai-expressions/SKILL.md +97 -0
  54. package/skills-mcp/controllerai-expressions/references/dsl-and-recipes.md +136 -0
  55. package/skills-mcp/controllerai-flow-control/SKILL.md +112 -0
  56. package/skills-mcp/controllerai-flow-control/references/patterns.md +82 -0
  57. package/skills-mcp/controllerai-integration-actions/SKILL.md +90 -0
  58. package/skills-mcp/controllerai-node-config/SKILL.md +106 -0
  59. package/skills-mcp/controllerai-node-config/references/prop-and-readiness.md +49 -0
  60. package/skills-mcp/controllerai-runs/SKILL.md +87 -0
  61. package/skills-mcp/controllerai-runs/references/debugging.md +53 -0
  62. package/skills-mcp/controllerai-start/SKILL.md +82 -0
  63. package/skills-mcp/controllerai-start/references/mcp-tool-map.md +177 -0
  64. package/skills-mcp/controllerai-template-setup/SKILL.md +10 -0
  65. package/skills-mcp/controllerai-triggers/SKILL.md +109 -0
  66. package/skills-mcp/controllerai-triggers/references/lifecycle.md +59 -0
  67. package/skills-mcp/controllerai-workflows/SKILL.md +108 -0
  68. package/skills-mcp/controllerai-workflows/references/command-map.md +1 -0
  69. package/skills-mcp/manifest.json +200 -0
  70. package/skills/controllerai-agents/references/command-reference.md +0 -205
  71. package/skills/controllerai-ai-nodes/references/command-reference.md +0 -98
  72. package/skills/controllerai-data/references/command-reference.md +0 -177
  73. package/skills/controllerai-expressions/references/command-reference.md +0 -166
  74. package/skills/controllerai-flow-control/references/command-reference.md +0 -171
  75. package/skills/controllerai-integration-actions/references/command-reference.md +0 -118
  76. package/skills/controllerai-node-config/references/command-reference.md +0 -186
  77. package/skills/controllerai-runs/references/command-reference.md +0 -113
  78. package/skills/controllerai-triggers/references/command-reference.md +0 -215
  79. package/skills/controllerai-workflows/references/command-reference.md +0 -118
@@ -1,38 +1,10 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
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
- export const directActionToolOp = (input) => ({
50
- op: 'add',
51
- toolType: 'pipedream',
52
- nodeSlug: input.integration,
53
- nodeComponentKey: input.action,
54
- connectionId: input.connectionId,
55
- ...(input.description !== undefined ? { toolDescription: input.description } : {}),
56
- // Never omit this field: the product default is no approval while older
57
- // backend entry points historically defaulted an omitted value to approval.
58
- requiresConfirmation: input.requiresConfirmation,
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
- 'cai agent conversation files, read-file and add-file commands.', 'BAD_AGENT_FILE_SPACE');
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: { baseUpdatedAt: current.updatedAt, ...patch },
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
- emit({ agentId, tool: added ?? null, liveChanged: false }, () => printKeyValues([
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
- .requiredOption('--connection <id>', 'active connection for this integration')
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 === 'pipedream' &&
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
- .description('Update a draft tool; direct-action connection grants are live-bound before republish')
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
- throw userError('Pass --description, --connection, --require-confirmation, or --no-confirmation.', 'NO_CHANGES');
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
- toolOps: [
705
- {
706
- op: 'update',
707
- toolId,
708
- ...(opts.description !== undefined ? { toolDescription: opts.description } : {}),
709
- ...(opts.connection !== undefined
710
- ? { connectionId: parsePositiveId(opts.connection, 'connectionId') }
711
- : {}),
712
- ...(requiresConfirmation !== undefined ? { requiresConfirmation } : {}),
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 === 'pipedream';
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 === 'pipedream';
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(compactConversation(row), () => printConversation(row));
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
- .option('--raw', 'include full diagnostic events and SDK snapshots explicitly')
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
- const limit = Number(opts.limit);
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 ? compactConversationHistory(result.history) : null }, () => printKeyValues([
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: compactConversation(row),
1331
+ conversation: row,
1309
1332
  reply,
1310
- history: result.history ? compactConversationHistory(result.history) : null,
1333
+ history: result.history,
1311
1334
  }, () => {
1312
1335
  printConversation(row, appUrl);
1313
1336
  process.stdout.write('\nverification\n');