@junoflow/cli 0.15.0 → 0.17.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/src/index.js CHANGED
@@ -1,15 +1,12 @@
1
1
  import { parseArgs } from 'node:util';
2
2
 
3
- import {
4
- AGENT_SKILL_INSTALL_USAGE,
5
- agentSkillInstall,
6
- } from './commands/agent-skill-install.js';
7
3
  import { APP_CHECK_USAGE, appCheck } from './commands/app-check.js';
8
4
  import { APP_DEPLOY_USAGE, appDeploy } from './commands/app-deploy.js';
9
5
  import { APP_DEV_USAGE, appDev } from './commands/app-dev.js';
10
6
  import { APP_GEN_TYPES_USAGE, appGenTypes } from './commands/app-gen-types.js';
11
7
  import { APP_INIT_USAGE, appInit } from './commands/app-init.js';
12
8
  import { APP_PULL_USAGE, appPull } from './commands/app-pull.js';
9
+ import { APPROVAL_WAIT_USAGE, approvalWait } from './commands/approval-wait.js';
13
10
  import { ASK_USAGE, ask } from './commands/ask.js';
14
11
  import { CHECK_USAGE, check } from './commands/check.js';
15
12
  import { CONFIG_SET_USAGE, configSet } from './commands/config.js';
@@ -36,13 +33,33 @@ import {
36
33
  taskRun,
37
34
  } from './commands/task-run.js';
38
35
  import { PULL_USAGE, pull } from './commands/pull.js';
36
+ import { PERMISSIONS_USAGE, permissions } from './commands/permissions.js';
39
37
  import { SKILL_USAGE, skill } from './commands/skill.js';
40
38
  import {
41
39
  SESSION_APPROVE_USAGE,
42
40
  sessionApprove,
43
41
  } from './commands/session-approve.js';
42
+ import { runFollow } from './commands/run-follow.js';
43
+ import {
44
+ SESSION_CANCEL_USAGE,
45
+ SESSION_EXPORT_USAGE,
46
+ SESSION_GET_USAGE,
47
+ SESSION_LIST_USAGE,
48
+ SESSION_RATE_USAGE,
49
+ sessionCancel,
50
+ sessionExport,
51
+ sessionGet,
52
+ sessionList,
53
+ sessionRate,
54
+ } from './commands/session.js';
44
55
  import { runTool } from './commands/tool.js';
45
56
  import { VERIFY_TYPES_USAGE, verifyTypes } from './commands/verify-types.js';
57
+ import {
58
+ VERSION_USAGE,
59
+ WHOAMI_USAGE,
60
+ version,
61
+ whoami,
62
+ } from './commands/whoami.js';
46
63
  import {
47
64
  ConflictError,
48
65
  DiagnosticsError,
@@ -58,15 +75,15 @@ import {
58
75
  EXIT_UNREACHABLE,
59
76
  EXIT_USAGE,
60
77
  } from './exit.js';
61
- import {
62
- groupHelp,
63
- nonToolHelp,
64
- verbHelp,
65
- WORKING_COPY_HEADING,
66
- workingCopyListing,
67
- } from './help.js';
78
+ import { groupHelp, nonToolHelp, rootHelp, verbHelp } from './help.js';
68
79
  import { closest, closestByVerb, levenshtein } from './suggest.js';
69
- import { groupOf, normalizeVerb, rowFor, rows } from './verbs.js';
80
+ import {
81
+ groupOf,
82
+ groupSummaries,
83
+ normalizeVerb,
84
+ rowFor,
85
+ rows,
86
+ } from './verbs.js';
70
87
 
71
88
  /**
72
89
  * The two flags every command that reaches an instance takes, and the two a
@@ -94,11 +111,58 @@ const NON_TOOL = {
94
111
  },
95
112
  usage: ASK_USAGE,
96
113
  },
114
+ whoami: { run: whoami, options: URL_OPTION, usage: WHOAMI_USAGE },
115
+ version: { run: version, options: URL_OPTION, usage: VERSION_USAGE },
116
+ permissions: {
117
+ run: permissions,
118
+ options: {
119
+ ...URL_OPTION,
120
+ 'allow-change': { type: 'boolean', default: false },
121
+ 'working-copy': { type: 'boolean', default: false },
122
+ json: { type: 'boolean', default: false },
123
+ },
124
+ usage: PERMISSIONS_USAGE,
125
+ },
126
+ 'session list': {
127
+ run: sessionList,
128
+ options: {
129
+ ...URL_OPTION,
130
+ rating: { type: 'string' },
131
+ profile: { type: 'string' },
132
+ limit: { type: 'string' },
133
+ },
134
+ usage: SESSION_LIST_USAGE,
135
+ },
136
+ 'session get': {
137
+ run: sessionGet,
138
+ options: { ...URL_OPTION, transcript: { type: 'boolean', default: false } },
139
+ usage: SESSION_GET_USAGE,
140
+ },
141
+ 'session export': {
142
+ run: sessionExport,
143
+ options: URL_OPTION,
144
+ usage: SESSION_EXPORT_USAGE,
145
+ },
146
+ 'session rate': {
147
+ run: sessionRate,
148
+ options: { ...URL_OPTION, rating: { type: 'string' } },
149
+ usage: SESSION_RATE_USAGE,
150
+ },
151
+ 'session cancel': {
152
+ run: sessionCancel,
153
+ options: URL_OPTION,
154
+ usage: SESSION_CANCEL_USAGE,
155
+ },
97
156
  'session approve': {
98
157
  run: sessionApprove,
99
158
  options: { ...URL_OPTION, wait: { type: 'string' } },
100
159
  usage: SESSION_APPROVE_USAGE,
101
160
  },
161
+ 'approval wait': {
162
+ run: approvalWait,
163
+ options: { ...URL_OPTION, wait: { type: 'string' } },
164
+ usage: APPROVAL_WAIT_USAGE,
165
+ },
102
166
  'credential set': {
103
167
  run: credentialSet,
104
168
  options: {
@@ -135,18 +199,6 @@ const NON_TOOL = {
135
199
  usage: TASK_PING_USAGE,
136
200
  },
137
201
  'config set': { run: configSet, options: {}, usage: CONFIG_SET_USAGE },
138
- 'agent-skill install': {
139
- run: agentSkillInstall,
140
- options: {
141
- ...URL_OPTION,
142
- dir: { type: 'string' },
143
- 'allow-change': { type: 'boolean', default: false },
144
- 'working-copy': { type: 'boolean', default: false },
145
- 'agents-md': { type: 'boolean', default: false },
146
- json: { type: 'boolean', default: false },
147
- },
148
- usage: AGENT_SKILL_INSTALL_USAGE,
149
- },
150
202
  };
151
203
 
152
204
  /**
@@ -301,6 +353,21 @@ const WORKING_COPY = {
301
353
  },
302
354
  };
303
355
 
356
+ /**
357
+ * Flags a tool row takes beyond its verbs.json ones, which name no field of
358
+ * the tool's input and so cannot be in the table: what the CLI does around
359
+ * the call. `run get --follow` streams the run's steps to stderr until it is
360
+ * done, then prints get_run as without it.
361
+ */
362
+ const TOOL_EXTRAS = {
363
+ 'run get': {
364
+ options: { follow: { type: 'boolean', default: false } },
365
+ help: "flag: --follow → no input field: stream the run's steps to stderr until it ends, then print the run",
366
+ run: (row, args, io) =>
367
+ args.follow ? runFollow(row, args, io) : runTool(row, args, io),
368
+ },
369
+ };
370
+
304
371
  /** The handler of a non-tool row, working-copy or not, or undefined. */
305
372
  function handlerFor(row) {
306
373
  if (row.tool) return undefined;
@@ -320,26 +387,6 @@ function dispatchable(command) {
320
387
  return row && DISPATCHABLE.includes(row) ? row : undefined;
321
388
  }
322
389
 
323
- const USAGE = `juno — operate a Juno instance, and develop flows, integrations and apps for it
324
-
325
- Usage: juno <command> [options]
326
- juno <group> <verb> [id] [--input <json>|-|@file] [--jq <expr>]
327
-
328
- Groups: ${[...GROUPS].join(', ')}
329
- Commands: ${DISPATCHABLE.filter((r) => !groupOf(r))
330
- .map((r) => r.command)
331
- .join(', ')}
332
- Run \`juno <group> --help\` for a group's commands.
333
-
334
- ${WORKING_COPY_HEADING}:
335
- ${workingCopyListing(DISPATCHABLE, handlerFor)}
336
-
337
- --url <url> picks the instance; else $JUNO_URL, package.json juno.target,
338
- then the user config (juno config set url <url>).
339
- --token <key> is the API key to send; else $JUNO_TOKEN, then the user config
340
- (juno config set token <key>). Create one under Settings → API keys.
341
- `;
342
-
343
390
  /**
344
391
  * run is the whole CLI, injectable so the tests drive it without spawning a
345
392
  * process: io carries cwd, env, the TTY bit, stdin and the three output
@@ -387,9 +434,11 @@ async function dispatch(argv, io) {
387
434
  const { leading, rest } = leadingFlags(argv);
388
435
  const [first, ...tail] = rest;
389
436
 
437
+ // Bare `juno` is `juno --help`, exit 0: it is where an agent with nothing
438
+ // installed starts, and the primer is the answer it came for.
390
439
  if (!first || first === '--help' || first === '-h' || first === 'help') {
391
- io.log(USAGE);
392
- return first ? EXIT_OK : EXIT_USAGE;
440
+ const show = (args) => showRootHelp(args, io);
441
+ return runCommand({ options: URL_OPTION }, tail, leading, io, show, show);
393
442
  }
394
443
 
395
444
  const top = dispatchable(first);
@@ -428,6 +477,11 @@ async function dispatch(argv, io) {
428
477
  throw unknownTopLevel(first, first, [first]);
429
478
  }
430
479
 
480
+ async function showRootHelp(args, io) {
481
+ io.log(await rootHelp(DISPATCHABLE, handlerFor, groupSummaries, args, io));
482
+ return EXIT_OK;
483
+ }
484
+
431
485
  async function showGroupHelp(group, args, io, code) {
432
486
  io.log(await groupHelp(group, DISPATCHABLE, handlerFor, args, io));
433
487
  return code;
@@ -468,21 +522,28 @@ function runRow(row, argv, leading, io) {
468
522
  );
469
523
  }
470
524
 
525
+ // --wait is how long a destructive tool's approval is waited for; every
526
+ // tool row takes it, since which tools are destructive is the server's.
471
527
  const options = {
472
528
  ...URL_OPTION,
473
529
  input: { type: 'string' },
474
530
  jq: { type: 'string' },
531
+ wait: { type: 'string' },
475
532
  };
476
533
  for (const [flag, spec] of Object.entries(row.flags ?? {})) {
477
534
  options[flag] = { type: 'string', multiple: spec.multiple === true };
478
535
  }
536
+ const extras = TOOL_EXTRAS[row.command];
537
+ Object.assign(options, extras?.options);
538
+ const run = extras?.run ?? runTool;
479
539
  return runCommand(
480
- { run: (args) => runTool(row, args, io), options },
540
+ { run: (args) => run(row, args, io), options },
481
541
  argv,
482
542
  leading,
483
543
  io,
484
544
  async (args) => {
485
- io.log(await verbHelp(row, args, io));
545
+ const help = await verbHelp(row, args, io);
546
+ io.log(extras?.help ? `${help}\n${extras.help}` : help);
486
547
  return EXIT_OK;
487
548
  }
488
549
  );
@@ -611,6 +672,12 @@ export function defaultIO() {
611
672
  write: (text) => process.stdout.write(text),
612
673
  warn: (msg) => process.stderr.write(`${msg}\n`),
613
674
  error: (msg) => process.stderr.write(`${msg}\n`),
675
+ // Ctrl-C while waiting for an approval: the handler says how to resume,
676
+ // and the wait returns rather than the process dying mid-line.
677
+ onInterrupt: (fn) => {
678
+ process.once('SIGINT', fn);
679
+ return () => process.off('SIGINT', fn);
680
+ },
614
681
  };
615
682
  }
616
683
 
package/src/prompt.js CHANGED
@@ -182,6 +182,15 @@ function nextStep(event, id) {
182
182
  return `still running; keep waiting with: juno ask --session ${id}`;
183
183
  }
184
184
  if (event.type !== 'awaiting_input') return null;
185
+ const approvalID = event.pending_input?.approval_id;
186
+ if (approvalID) {
187
+ // A destructive call's approval record: only a person in Juno answers it.
188
+ const url = event.pending_input.approval_url ?? `/approvals/${approvalID}`;
189
+ return (
190
+ `waiting for a person to approve it in Juno: ${url}\n` +
191
+ `wait for the outcome with: juno ask --session ${id}`
192
+ );
193
+ }
185
194
  if (event.pending_input?.kind === 'approval') {
186
195
  return (
187
196
  `awaiting approval; approve with: juno session approve ${id}\n` +
package/src/verbs.js CHANGED
@@ -2,7 +2,9 @@ import { createRequire } from 'node:module';
2
2
 
3
3
  // createRequire rather than an import attribute: `with { type: 'json' }` needs
4
4
  // Node 20.10+, and engines says >=20.
5
- const { commands: ROWS } = createRequire(import.meta.url)('../verbs.json');
5
+ const { commands: ROWS, groups: GROUPS } = createRequire(import.meta.url)(
6
+ '../verbs.json'
7
+ );
6
8
 
7
9
  /**
8
10
  * verbs.json is the command table: every `juno <group> <verb>` the CLI
@@ -12,6 +14,9 @@ const { commands: ROWS } = createRequire(import.meta.url)('../verbs.json');
12
14
  */
13
15
  export const rows = ROWS;
14
16
 
17
+ /** The one line `juno --help` lists each group by, keyed by group. */
18
+ export const groupSummaries = GROUPS;
19
+
15
20
  const byCommand = new Map(ROWS.map((row) => [row.command, row]));
16
21
 
17
22
  /** The row for a command string ("state get"), or undefined. */