@relayflows/sdk 2.0.15 → 2.0.17

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 (155) hide show
  1. package/dist/agent-artifacts.d.ts +20 -0
  2. package/dist/agent-artifacts.d.ts.map +1 -0
  3. package/dist/agent-artifacts.js +81 -0
  4. package/dist/agent-artifacts.js.map +1 -0
  5. package/dist/authored-flow-error.d.ts +1 -1
  6. package/dist/authored-flow-error.d.ts.map +1 -1
  7. package/dist/authored-flow-error.js.map +1 -1
  8. package/dist/authored-flow-executor.d.ts +3 -3
  9. package/dist/authored-flow-executor.d.ts.map +1 -1
  10. package/dist/authored-flow-executor.js +93 -7
  11. package/dist/authored-flow-executor.js.map +1 -1
  12. package/dist/authored-flow-lifecycle.d.ts +10 -0
  13. package/dist/authored-flow-lifecycle.d.ts.map +1 -1
  14. package/dist/authored-flow-lifecycle.js +7 -0
  15. package/dist/authored-flow-lifecycle.js.map +1 -1
  16. package/dist/authored-flow-operation.d.ts +11 -0
  17. package/dist/authored-flow-operation.d.ts.map +1 -1
  18. package/dist/authored-flow-operation.js +32 -7
  19. package/dist/authored-flow-operation.js.map +1 -1
  20. package/dist/authored-node-runner.d.ts.map +1 -1
  21. package/dist/authored-node-runner.js +37 -14
  22. package/dist/authored-node-runner.js.map +1 -1
  23. package/dist/authored-step-output.d.ts +2 -0
  24. package/dist/authored-step-output.d.ts.map +1 -1
  25. package/dist/authored-step-output.js +5 -1
  26. package/dist/authored-step-output.js.map +1 -1
  27. package/dist/authored-worker-step.d.ts.map +1 -1
  28. package/dist/authored-worker-step.js +15 -8
  29. package/dist/authored-worker-step.js.map +1 -1
  30. package/dist/cli/check-triggers.d.ts.map +1 -1
  31. package/dist/cli/check-triggers.js +10 -0
  32. package/dist/cli/check-triggers.js.map +1 -1
  33. package/dist/cli/check.d.ts +14 -0
  34. package/dist/cli/check.d.ts.map +1 -1
  35. package/dist/cli/check.js +6 -1
  36. package/dist/cli/check.js.map +1 -1
  37. package/dist/cli/cloud-deploy.d.ts +30 -0
  38. package/dist/cli/cloud-deploy.d.ts.map +1 -0
  39. package/dist/cli/cloud-deploy.js +162 -0
  40. package/dist/cli/cloud-deploy.js.map +1 -0
  41. package/dist/cli/cloud-run.d.ts +3 -1
  42. package/dist/cli/cloud-run.d.ts.map +1 -1
  43. package/dist/cli/cloud-run.js +37 -5
  44. package/dist/cli/cloud-run.js.map +1 -1
  45. package/dist/cli/cloud-schedule.d.ts +29 -0
  46. package/dist/cli/cloud-schedule.d.ts.map +1 -0
  47. package/dist/cli/cloud-schedule.js +131 -0
  48. package/dist/cli/cloud-schedule.js.map +1 -0
  49. package/dist/cli/cloud-sync.d.ts +13 -0
  50. package/dist/cli/cloud-sync.d.ts.map +1 -0
  51. package/dist/cli/cloud-sync.js +39 -0
  52. package/dist/cli/cloud-sync.js.map +1 -0
  53. package/dist/cli/direct-run.js +1 -1
  54. package/dist/cli/direct-run.js.map +1 -1
  55. package/dist/cli/run.d.ts +1 -1
  56. package/dist/cli/run.d.ts.map +1 -1
  57. package/dist/cli/run.js +17 -4
  58. package/dist/cli/run.js.map +1 -1
  59. package/dist/cli.d.ts.map +1 -1
  60. package/dist/cli.js +117 -12
  61. package/dist/cli.js.map +1 -1
  62. package/dist/cloud-deploy.d.ts +70 -0
  63. package/dist/cloud-deploy.d.ts.map +1 -0
  64. package/dist/cloud-deploy.js +192 -0
  65. package/dist/cloud-deploy.js.map +1 -0
  66. package/dist/cloud-http.d.ts +52 -2
  67. package/dist/cloud-http.d.ts.map +1 -1
  68. package/dist/cloud-http.js +135 -9
  69. package/dist/cloud-http.js.map +1 -1
  70. package/dist/cloud-run.d.ts +53 -0
  71. package/dist/cloud-run.d.ts.map +1 -1
  72. package/dist/cloud-run.js +97 -25
  73. package/dist/cloud-run.js.map +1 -1
  74. package/dist/cloud-schedule.d.ts +48 -0
  75. package/dist/cloud-schedule.d.ts.map +1 -0
  76. package/dist/cloud-schedule.js +125 -0
  77. package/dist/cloud-schedule.js.map +1 -0
  78. package/dist/cloud-sync.d.ts +66 -0
  79. package/dist/cloud-sync.d.ts.map +1 -0
  80. package/dist/cloud-sync.js +287 -0
  81. package/dist/cloud-sync.js.map +1 -0
  82. package/dist/failure-kinds.d.ts +10 -3
  83. package/dist/failure-kinds.d.ts.map +1 -1
  84. package/dist/failure-kinds.js +10 -0
  85. package/dist/failure-kinds.js.map +1 -1
  86. package/dist/index.d.ts +5 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +5 -0
  89. package/dist/index.js.map +1 -1
  90. package/dist/named-gate-lowering.js +9 -0
  91. package/dist/named-gate-lowering.js.map +1 -1
  92. package/dist/named-gates.d.ts +1 -1
  93. package/dist/named-gates.d.ts.map +1 -1
  94. package/dist/named-gates.js +11 -1
  95. package/dist/named-gates.js.map +1 -1
  96. package/dist/preflight.d.ts.map +1 -1
  97. package/dist/preflight.js +155 -9
  98. package/dist/preflight.js.map +1 -1
  99. package/dist/schedule-trigger.d.ts +35 -0
  100. package/dist/schedule-trigger.d.ts.map +1 -0
  101. package/dist/schedule-trigger.js +53 -0
  102. package/dist/schedule-trigger.js.map +1 -0
  103. package/dist/spec.d.ts +11 -1
  104. package/dist/spec.d.ts.map +1 -1
  105. package/dist/spec.js.map +1 -1
  106. package/dist/trigger-executor.js +1 -1
  107. package/dist/trigger-executor.js.map +1 -1
  108. package/dist/validate.js +1 -1
  109. package/dist/validate.js.map +1 -1
  110. package/dist/worker-cli.d.ts +10 -0
  111. package/dist/worker-cli.d.ts.map +1 -1
  112. package/dist/worker-cli.js +51 -22
  113. package/dist/worker-cli.js.map +1 -1
  114. package/dist/worker.d.ts.map +1 -1
  115. package/dist/worker.js +9 -0
  116. package/dist/worker.js.map +1 -1
  117. package/dist/wrapper-session.d.ts +3 -1
  118. package/dist/wrapper-session.d.ts.map +1 -1
  119. package/dist/wrapper-session.js +6 -3
  120. package/dist/wrapper-session.js.map +1 -1
  121. package/package.json +3 -3
  122. package/src/agent-artifacts.ts +76 -0
  123. package/src/authored-flow-error.ts +1 -0
  124. package/src/authored-flow-executor.ts +95 -8
  125. package/src/authored-flow-lifecycle.ts +7 -0
  126. package/src/authored-flow-operation.ts +35 -10
  127. package/src/authored-node-runner.ts +35 -13
  128. package/src/authored-step-output.ts +6 -1
  129. package/src/authored-worker-step.ts +16 -9
  130. package/src/cli/check-triggers.ts +9 -0
  131. package/src/cli/check.ts +21 -1
  132. package/src/cli/cloud-deploy.ts +157 -0
  133. package/src/cli/cloud-run.ts +37 -6
  134. package/src/cli/cloud-schedule.ts +135 -0
  135. package/src/cli/cloud-sync.ts +37 -0
  136. package/src/cli/direct-run.ts +1 -1
  137. package/src/cli/run.ts +18 -5
  138. package/src/cli.ts +107 -12
  139. package/src/cloud-deploy.ts +247 -0
  140. package/src/cloud-http.ts +173 -9
  141. package/src/cloud-run.ts +143 -24
  142. package/src/cloud-schedule.ts +158 -0
  143. package/src/cloud-sync.ts +315 -0
  144. package/src/failure-kinds.ts +10 -0
  145. package/src/index.ts +14 -0
  146. package/src/named-gate-lowering.ts +9 -0
  147. package/src/named-gates.ts +11 -1
  148. package/src/preflight.ts +132 -9
  149. package/src/schedule-trigger.ts +73 -0
  150. package/src/spec.ts +12 -1
  151. package/src/trigger-executor.ts +1 -1
  152. package/src/validate.ts +1 -1
  153. package/src/worker-cli.ts +41 -0
  154. package/src/worker.ts +9 -0
  155. package/src/wrapper-session.ts +5 -1
@@ -0,0 +1,135 @@
1
+ import { CloudFlowError } from '../cloud-http.js';
2
+ import { listCloudSchedules, scheduleInCloud, unscheduleInCloud, type CloudSchedule } from '../cloud-schedule.js';
3
+ import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
4
+ import { snapshotJsonValue, type JsonValue } from '../json-value.js';
5
+ import type { CliIo } from '../cli.js';
6
+
7
+ export interface CloudScheduleArgs {
8
+ command: 'schedule';
9
+ value: string;
10
+ cron: string | undefined;
11
+ every: string | undefined;
12
+ tz: string | undefined;
13
+ input: string | undefined;
14
+ name: string | undefined;
15
+ json: boolean;
16
+ }
17
+
18
+ /**
19
+ * `flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>]
20
+ * [--tz <IANA>] [--input <json|file>] [--name <n>] [--json]`
21
+ *
22
+ * With neither `--cron` nor `--every`, the flow's own `schedule.*` handler
23
+ * supplies the cron, so a declared schedule and its hosted registration
24
+ * cannot drift.
25
+ */
26
+ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleArgs | undefined {
27
+ let value: string | undefined;
28
+ const flags: Record<'cron' | 'every' | 'tz' | 'input' | 'name', string | undefined> = {
29
+ cron: undefined, every: undefined, tz: undefined, input: undefined, name: undefined,
30
+ };
31
+ let json = false;
32
+ for (let i = 0; i < args.length; i++) {
33
+ const arg = args[i]!;
34
+ if (arg === '--json') {
35
+ if (json) return undefined;
36
+ json = true;
37
+ continue;
38
+ }
39
+ if (arg === '--cron' || arg === '--every' || arg === '--tz' || arg === '--input' || arg === '--name') {
40
+ const key = arg.slice(2) as keyof typeof flags;
41
+ const next = args[i + 1];
42
+ // A cron expression legitimately starts with `*`; only a flag-shaped value is refused.
43
+ if (flags[key] !== undefined || next === undefined || next.startsWith('--')) return undefined;
44
+ flags[key] = next;
45
+ i += 1;
46
+ continue;
47
+ }
48
+ if (arg.startsWith('-') || value !== undefined) return undefined;
49
+ value = arg;
50
+ }
51
+ if (value === undefined) return undefined;
52
+ if (flags.cron !== undefined && flags.every !== undefined) return undefined;
53
+ if (flags.input !== undefined && !isAuthoredFlowPath(value)) return undefined;
54
+ return { command: 'schedule', value, ...flags, json };
55
+ }
56
+
57
+ function describe(schedule: CloudSchedule): string {
58
+ const last = schedule.lastTriggeredAt === undefined ? ''
59
+ : ` last ${schedule.lastTriggeredAt}${schedule.lastTriggerStatus === undefined ? '' : ` (${schedule.lastTriggerStatus})`}`;
60
+ return `${schedule.id} ${schedule.status} ${JSON.stringify(schedule.name)} cron "${schedule.cronExpression}" tz ${schedule.timezone}${last}`;
61
+ }
62
+
63
+ export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): Promise<0 | 1 | 2> {
64
+ try {
65
+ let input: JsonValue | undefined;
66
+ let inputPresent = false;
67
+ if (isAuthoredFlowPath(args.value)) {
68
+ try {
69
+ input = snapshotJsonValue(parseDirectInput(args.input), 'Cloud authored input');
70
+ inputPresent = true;
71
+ } catch (error) {
72
+ if (error instanceof DirectInputError) throw new CloudFlowError('invalid_input', error.message);
73
+ throw error;
74
+ }
75
+ }
76
+ const schedule = await scheduleInCloud({
77
+ flow: { path: args.value },
78
+ ...(args.cron === undefined ? {} : { cron: args.cron }),
79
+ ...(args.every === undefined ? {} : { every: args.every }),
80
+ ...(args.tz === undefined ? {} : { tz: args.tz }),
81
+ ...(args.name === undefined ? {} : { name: args.name }),
82
+ ...(inputPresent ? { input } : {}),
83
+ });
84
+ if (args.json) {
85
+ io.stdout(JSON.stringify({ ok: true, ...schedule }));
86
+ return 0;
87
+ }
88
+ io.stdout(`SCHEDULED ${describe(schedule)}`);
89
+ io.stdout('Each fire submits this exact source through the same admission as flows run --cloud; list with: flows schedules');
90
+ return 0;
91
+ } catch (error) {
92
+ return reportFailure(error, args.json, io);
93
+ }
94
+ }
95
+
96
+ export async function runCloudSchedulesCli({ json }: { json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
97
+ try {
98
+ const schedules = await listCloudSchedules();
99
+ if (json) {
100
+ io.stdout(JSON.stringify({ ok: true, schedules }));
101
+ return 0;
102
+ }
103
+ if (schedules.length === 0) io.stdout('No workflow schedules in this workspace.');
104
+ for (const schedule of schedules) io.stdout(describe(schedule));
105
+ return 0;
106
+ } catch (error) {
107
+ return reportFailure(error, json, io);
108
+ }
109
+ }
110
+
111
+ export async function runCloudUnscheduleCli(
112
+ { scheduleId, json }: { scheduleId: string; json: boolean }, io: CliIo,
113
+ ): Promise<0 | 1 | 2> {
114
+ try {
115
+ await unscheduleInCloud(scheduleId);
116
+ io.stdout(json ? JSON.stringify({ ok: true, scheduleId, deleted: true }) : `UNSCHEDULED ${scheduleId}`);
117
+ return 0;
118
+ } catch (error) {
119
+ return reportFailure(error, json, io);
120
+ }
121
+ }
122
+
123
+ function reportFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
124
+ const code = error instanceof CloudFlowError ? error.code : 'cloud_schedule_failed';
125
+ let message = error instanceof Error ? error.message : 'Cloud schedule failed.';
126
+ // The schedule routes take a browser session or a `cli:auth` token.
127
+ if (error instanceof CloudFlowError && error.status === 403) {
128
+ message += ' Scheduling needs an interactive `cli:auth` credential: run `agent-relay cloud login`.';
129
+ }
130
+ if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
131
+ else io.stderr(`${code}: ${message}`);
132
+ return error instanceof CloudFlowError
133
+ && (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code) || error.status === 403 || error.status === 401)
134
+ ? 2 : 1;
135
+ }
@@ -0,0 +1,37 @@
1
+ import { CloudFlowError } from '../cloud-http.js';
2
+ import { applyCloudPatch, downloadCloudPatch, patchedPaths } from '../cloud-sync.js';
3
+ import type { CliIo } from '../cli.js';
4
+
5
+ /**
6
+ * `flows sync <run-id>`: fetch the diff a hosted run left in its synced tree
7
+ * and apply it here. The patch is the sandbox's own `git diff` against the
8
+ * uploaded baseline, so applying it reproduces exactly what the run's steps
9
+ * wrote — no re-execution, no re-upload.
10
+ */
11
+ export async function runCloudSyncCli(
12
+ { runId, json, root }: { runId: string; json: boolean; root: string },
13
+ io: CliIo,
14
+ ): Promise<0 | 1 | 2> {
15
+ try {
16
+ const { patch, hasChanges } = await downloadCloudPatch(runId, {});
17
+ if (!hasChanges || !patch.trim()) {
18
+ if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: false, applied: false }));
19
+ else io.stdout(`NO CHANGES ${runId}`);
20
+ return 0;
21
+ }
22
+ applyCloudPatch(root, patch);
23
+ const files = patchedPaths(patch);
24
+ if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: true, applied: true, files }));
25
+ else {
26
+ io.stdout(`APPLIED ${runId}: ${files.length} file${files.length === 1 ? '' : 's'}${files.length ? `\n ${files.join('\n ')}` : ''}`);
27
+ io.stdout('Applied to the working tree, uncommitted: review with git diff before keeping it.');
28
+ }
29
+ return 0;
30
+ } catch (error) {
31
+ const code = error instanceof CloudFlowError ? error.code : 'cloud_sync_failed';
32
+ const message = error instanceof Error ? error.message : 'Cloud sync failed.';
33
+ if (json) io.stdout(JSON.stringify({ ok: false, code, message, runId }));
34
+ else io.stderr(`${code}: ${message} (run ${runId})`);
35
+ return error instanceof CloudFlowError && ['configuration', 'sync_unsupported', 'patch_conflict'].includes(error.code) ? 2 : 1;
36
+ }
37
+ }
@@ -172,7 +172,7 @@ export async function runDirectFlow(
172
172
  // diagnostic carried up from `classifyOutcome` already names the step, its
173
173
  // exit code and its output tail; this branch is what lets it reach the
174
174
  // terminal. `resumeFlow` takes the same branch, through the same helper.
175
- if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
175
+ if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
176
176
  return authoredStepFailure('run', base, socketPath, error);
177
177
  }
178
178
  const runId = error instanceof AuthoredFlowExecutionError ? error.runId : undefined;
package/src/cli/run.ts CHANGED
@@ -37,7 +37,7 @@ export interface ParkedStep {
37
37
  }
38
38
 
39
39
  export interface RunDiagnostic extends StepFailedDetails {
40
- severity: 'refusal' | 'failure' | 'parked' | 'warning';
40
+ severity: 'refusal' | 'failure' | 'parked' | 'warning' | 'declined';
41
41
  kind: RunFailureKind | RunWarningKind | RunCompletionReason;
42
42
  message: string;
43
43
  }
@@ -223,7 +223,7 @@ export async function resumeFlow(
223
223
  // leaving it on `protocolFailure` meant `flows run` printed the evidence
224
224
  // while `flows resume` still printed `protocol_error` and
225
225
  // `RUN <id> unknown` for the identical failure.
226
- if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
226
+ if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
227
227
  return authoredStepFailure('resume', base, socketPath, error, runId);
228
228
  }
229
229
  if (!(error instanceof JournalProtocolError) || error.code !== 'run_not_found') {
@@ -287,10 +287,12 @@ export function authoredStepFailure(
287
287
  completionReason: 'step_failed',
288
288
  diagnostics: [...base.diagnostics, {
289
289
  severity: 'failure',
290
- kind: 'step_failed',
290
+ // A predicate gate that judged false is a run failure with its own
291
+ // name, so the report says which kind of check the body did not pass.
292
+ kind: error.code === 'gate_failed' ? 'gate_failed' : 'step_failed',
291
293
  // The `step_failed: ` prefix `AuthoredFlowExecutionError` adds is
292
294
  // redundant once the diagnostic is labelled `[step_failed]`.
293
- message: error.message.replace(/^step_failed: /, ''),
295
+ message: error.message.replace(/^(?:step_failed|gate_failed): /, ''),
294
296
  }],
295
297
  },
296
298
  };
@@ -328,6 +330,17 @@ export function authoredCompletion(
328
330
  exitCode: 0,
329
331
  report: { ...common, ok: true, status: 'completed', completionReason: 'success' },
330
332
  };
333
+ case 'declined':
334
+ return {
335
+ exitCode: 0,
336
+ report: {
337
+ ...common, ok: true, status: 'completed', completionReason: 'success',
338
+ diagnostics: [...base.diagnostics, {
339
+ severity: 'declined', kind: 'run_declined',
340
+ message: 'Flow deliberately chose not to act on this input.',
341
+ }],
342
+ },
343
+ };
331
344
  case 'needs_human':
332
345
  return {
333
346
  exitCode: 3,
@@ -358,7 +371,7 @@ export function authoredCompletion(
358
371
  },
359
372
  };
360
373
  }
361
- // Exhaustive by construction. A fourth lowered completion has to choose its
374
+ // Exhaustive by construction. A new lowered completion has to choose its
362
375
  // own exit code and wording here; it must not inherit "its own checks did not
363
376
  // pass", which would state something the body never declared. Letting an
364
377
  // unlisted reason fall through to the failure branch is how a reporting-side
package/src/cli.ts CHANGED
@@ -24,6 +24,9 @@ import { runDirectFlow } from './cli/direct-run.js';
24
24
  import { parseReplayArgs, replayJournal, type ReplayArgs } from './cli/replay.js';
25
25
  import { checkTypeScriptFlow } from './cli/check-typescript.js';
26
26
  import { runCloudCli } from './cli/cloud-run.js';
27
+ import { runCloudSyncCli } from './cli/cloud-sync.js';
28
+ import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
29
+ import { parseCloudScheduleArgs, runCloudScheduleCli, runCloudSchedulesCli, runCloudUnscheduleCli, type CloudScheduleArgs } from './cli/cloud-schedule.js';
27
30
  import { isAuthoredFlowPath } from './direct-input.js';
28
31
  import { parseDeployArgs, runDeploy, type DeployArgs } from './cli/deploy.js';
29
32
  import { parseDigestReference } from './bundle-transport.js';
@@ -51,7 +54,14 @@ type ParsedArgs =
51
54
  | BuildArgs
52
55
  | DeployArgs
53
56
  | { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
54
- | { command: 'cloud-run'; value: string; json: boolean; wait: boolean }
57
+ | { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean }
58
+ | { command: 'sync'; runId: string; json: boolean; root: string }
59
+ | CloudDeployArgs
60
+ | { command: 'deployments'; json: boolean }
61
+ | { command: 'undeploy'; agentId: string; json: boolean }
62
+ | CloudScheduleArgs
63
+ | { command: 'schedules'; json: boolean }
64
+ | { command: 'unschedule'; scheduleId: string; json: boolean }
55
65
  | { command: 'check'; json: boolean; watch: boolean; value: string }
56
66
  | { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
57
67
  | { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
@@ -66,12 +76,20 @@ const USAGE = [
66
76
  'flows add <helper-name|@flows/helper-name>',
67
77
  'flows build [--out <dir>] <flow.yaml|flow.ts>',
68
78
  'flows build --verify <bundle-dir>',
79
+ 'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--json]',
80
+ 'flows deployments [--json]',
81
+ 'flows undeploy [--json] <deployment-id>',
82
+ 'flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>] [--tz <IANA>] [--input <inline-json-or-file>] [--name <name>] [--json]',
83
+ 'flows schedules [--json]',
84
+ 'flows unschedule [--json] <schedule-id>',
69
85
  'flows deploy <flow>@sha256:<digest> --to <file-bucket-uri>',
70
86
  'flows run <flow>@sha256:<digest> [--bucket <file-bucket-uri>] [--data-dir <dir>] [--json]',
71
87
  'flows check [--watch] [--json] <flow.ts|flow.yaml|spec.json>',
72
88
  'flows serve-webhook --data-dir <dir> --port <p> [--allow <name>[,<name>]]',
73
89
  'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] [--reuse-from <run-id>] <flow.yaml|spec.json>',
74
- 'flows run --cloud [--json] [--wait] <flow.yaml|spec.json>',
90
+ 'flows run --cloud [--json] [--wait] [--sync-code] <flow.yaml|spec.json>',
91
+ 'flows run --cloud [--json] [--wait] [--sync-code] <flow.ts> --input <inline-json-or-file>',
92
+ 'flows sync [--json] [--dir <path>] <run-id>',
75
93
  'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
76
94
  'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
77
95
  'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
@@ -116,6 +134,13 @@ export async function runCli(
116
134
  if (parsed.command === 'serve-webhook') return runServeWebhook(parsed, io);
117
135
 
118
136
  if (parsed.command === 'cloud-run') return runCloudCli(parsed, io);
137
+ if (parsed.command === 'sync') return runCloudSyncCli(parsed, io);
138
+ if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
139
+ if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
140
+ if (parsed.command === 'undeploy') return runCloudUndeployCli(parsed, io);
141
+ if (parsed.command === 'schedule') return runCloudScheduleCli(parsed, io);
142
+ if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
143
+ if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
119
144
  if (parsed.command === 'replay') return replayJournal(parsed, io);
120
145
  if (parsed.command === 'build') return runBuild(parsed, io);
121
146
  if (parsed.command === 'deploy') return runDeploy(parsed, io);
@@ -251,6 +276,7 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
251
276
  return {
252
277
  report: {
253
278
  ...mcp.report,
279
+ ...(triggers?.report.schedules === undefined ? {} : { schedules: triggers.report.schedules }),
254
280
  diagnostics: [...helper.report.diagnostics, ...mcp.report.diagnostics, ...triggerDiagnostics],
255
281
  ok: helper.report.ok && mcp.report.ok && triggerOk,
256
282
  },
@@ -419,17 +445,47 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
419
445
  if (command === 'add') return args.length === 2 ? { command: 'add', value: args[1]! } : undefined;
420
446
  if (command === 'replay') return parseReplayArgs(args.slice(1));
421
447
  if (command === 'build') return parseBuildArgs(args.slice(1));
422
- if (command === 'deploy') return parseDeployArgs(args.slice(1));
448
+ if (command === 'deploy') {
449
+ // The positional decides the form: an authored source deploys a hosted
450
+ // listener; a digest reference copies a sealed bundle into a file bucket.
451
+ const source = args.slice(1).find(a => !a.startsWith('-') && isAuthoredFlowPath(a));
452
+ return source !== undefined ? parseCloudDeployArgs(args.slice(1)) : parseDeployArgs(args.slice(1));
453
+ }
454
+ if (command === 'schedule') return parseCloudScheduleArgs(args.slice(1));
455
+ if (command === 'schedules') {
456
+ const rest = args.slice(1);
457
+ if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
458
+ return { command: 'schedules', json: rest.length === 1 };
459
+ }
460
+ if (command === 'unschedule') {
461
+ const rest = args.slice(1).filter(a => a !== '--json');
462
+ const json = args.length - 1 - rest.length;
463
+ if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
464
+ return { command: 'unschedule', scheduleId: rest[0]!, json: json === 1 };
465
+ }
466
+ if (command === 'undeploy') {
467
+ const rest = args.slice(1).filter(a => a !== '--json');
468
+ const json = args.length - 1 - rest.length;
469
+ if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
470
+ return { command: 'undeploy', agentId: rest[0]!, json: json === 1 };
471
+ }
472
+ if (command === 'deployments') {
473
+ const rest = args.slice(1);
474
+ if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
475
+ return { command: 'deployments', json: rest.length === 1 };
476
+ }
423
477
  if (command === 'serve-webhook') return parseWebhookArgs(args.slice(1));
424
478
  if (command === 'hn-monitor') return parseHnMonitorArgs(args.slice(1));
425
479
  if (command === 'tick') return parseTickArgs(args.slice(1));
426
480
  if (command === 'observer') return parseObserverArgs(args.slice(1));
481
+ if (command === 'sync') return parseSyncArgs(args.slice(1));
427
482
  if (command !== 'check' && command !== 'run' && command !== 'resume') return undefined;
428
483
 
429
484
  let json = false;
430
485
  let watch = false;
431
486
  let cloud = false;
432
487
  let wait = false;
488
+ let syncCode = false;
433
489
  let localAgent = false;
434
490
  let allowHumanInfluenced = false;
435
491
  let dataDir = DEFAULT_DATA_DIR;
@@ -443,10 +499,11 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
443
499
  const positionals: string[] = [];
444
500
  for (let index = 1; index < args.length; index += 1) {
445
501
  const argument = args[index]!;
446
- if (argument === '--cloud' || argument === '--wait') {
447
- if (command !== 'run' || (argument === '--cloud' ? cloud : wait)) return undefined;
502
+ if (argument === '--cloud' || argument === '--wait' || argument === '--sync-code') {
503
+ if (command !== 'run' || (argument === '--cloud' ? cloud : argument === '--wait' ? wait : syncCode)) return undefined;
448
504
  if (argument === '--cloud') cloud = true;
449
- else wait = true;
505
+ else if (argument === '--wait') wait = true;
506
+ else syncCode = true;
450
507
  continue;
451
508
  }
452
509
  if (argument === '--allow-human-influenced') {
@@ -520,13 +577,15 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
520
577
  if (bucket !== undefined && (cloud || !parseDigestReference(positionals[0]!))) return undefined;
521
578
  if (cloud) {
522
579
  // `--cloud` submits the spec to Cloud, so every flag that only describes a
523
- // local run -- an inline input, a data dir, a suppressed daemon, a local
524
- // agent, a local observer-link opt-out -- describes nothing there and is
525
- // refused rather than ignored.
526
- if (allowHumanInfluenced || sawInput || sawDataDir || !spawn || localAgent || noObserverLink || reuseFromRunId !== undefined) return undefined;
527
- return { command: 'cloud-run', value: positionals[0]!, json, wait };
580
+ // local run -- a data dir, a suppressed daemon, a local agent, a local
581
+ // observer-link opt-out -- describes nothing there and is refused rather
582
+ // than ignored. `--input` is the authored body's argument and travels with
583
+ // the source, so it is accepted exactly where a local run accepts it.
584
+ if (allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink || reuseFromRunId !== undefined) return undefined;
585
+ if (sawInput && !isAuthoredFlowPath(positionals[0]!)) return undefined;
586
+ return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode };
528
587
  }
529
- if (wait) return undefined;
588
+ if (wait || syncCode) return undefined;
530
589
  if (reuseFromRunId !== undefined && isAuthoredFlowPath(positionals[0]!)) return undefined;
531
590
 
532
591
  if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!)) return undefined;
@@ -579,6 +638,32 @@ function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
579
638
  * directory at all -- the mint is a pure Relaycast API round-trip. No
580
639
  * positional argument, no other flags.
581
640
  */
641
+ /** `flows sync [--json] [--dir <path>] <run-id>`: apply a hosted run's patch to a local tree. */
642
+ function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
643
+ let json = false;
644
+ let root: string | undefined;
645
+ const positionals: string[] = [];
646
+ for (let index = 0; index < args.length; index += 1) {
647
+ const argument = args[index]!;
648
+ if (argument === '--json') {
649
+ if (json) return undefined;
650
+ json = true;
651
+ continue;
652
+ }
653
+ if (argument === '--dir') {
654
+ const value = args[index + 1];
655
+ if (root !== undefined || value === undefined || value.startsWith('-')) return undefined;
656
+ root = value;
657
+ index += 1;
658
+ continue;
659
+ }
660
+ if (argument.startsWith('-')) return undefined;
661
+ positionals.push(argument);
662
+ }
663
+ if (positionals.length !== 1) return undefined;
664
+ return { command: 'sync', runId: positionals[0]!, json, root: root ?? '.' };
665
+ }
666
+
582
667
  function parseObserverArgs(rest: readonly string[]): ParsedArgs | undefined {
583
668
  let dataDir = DEFAULT_DATA_DIR;
584
669
  let sawDataDir = false;
@@ -689,6 +774,15 @@ function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
689
774
  const vacuous = gate.acceptsAnyOutput === true ? ' [json_schema accepts any output]' : '';
690
775
  io.stdout(`GATE step "${gate.stepId}" ${gate.checks.join('+')} from data (kernel, journal-replayable)${vacuous}`);
691
776
  }
777
+ for (const schedule of report.schedules ?? []) {
778
+ const declared = schedule.cron !== undefined
779
+ ? `cron "${schedule.cron}"${schedule.tz === undefined ? '' : ` tz ${schedule.tz}`}`
780
+ : `every ${schedule.intervalMs}ms`;
781
+ const local = schedule.localUnsupported !== undefined
782
+ ? `Cloud only: ${schedule.localUnsupported}`
783
+ : `local: flows tick start --schedule-id ${schedule.scheduleId} --interval-ms ${schedule.intervalMs} --epoch-ms ${schedule.epochMs}`;
784
+ io.stdout(`SCHEDULE handler ${schedule.handler} ${declared} -> flows.tick schedule_id ${schedule.scheduleId} [${local}]`);
785
+ }
692
786
  for (const resolution of report.resolutions) {
693
787
  const config = resolution.source === 'project' && report.projectConfigPath !== undefined
694
788
  ? ` (${report.projectConfigPath})`
@@ -750,6 +844,7 @@ function diagnosticLabel(severity: string): string {
750
844
  case 'warning': return 'WARNING';
751
845
  case 'failure': return 'FAILED';
752
846
  case 'parked': return 'PARKED';
847
+ case 'declined': return 'DECLINED';
753
848
  default: return 'REFUSED';
754
849
  }
755
850
  }