@profoundlogic/coderflow-cli 0.13.11-dev.20260807233136.g074f26b1 → 0.13.11

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 CHANGED
@@ -106,7 +106,7 @@ available. Runtime-only server credential configuration is not included.
106
106
  Import an export package or a plain local workspace directory:
107
107
 
108
108
  ```bash
109
- # Restore a ready-to-use staged container (default)
109
+ # Restore the task in the state it was exported in (default)
110
110
  coder import ./task-backup.zip
111
111
 
112
112
  # Package a directory and restore it into an environment
@@ -117,10 +117,24 @@ coder import ./task-backup.zip --mode=objective
117
117
  coder import ./task-backup.zip --mode=running
118
118
  ```
119
119
 
120
- Import modes are `objective`, `staged` (the default), and `running`. Use
121
- `--environment`, `--name`, `--agent`, or `--instructions` to override package
122
- metadata. In the Web UI, **Import** creates an objective from the Objectives
123
- view and a staged task from the Tasks or Board view.
120
+ Import modes are `preserve` (the default), `objective`, `staged`, `running`,
121
+ and `completed`. `preserve` reproduces the exported state: a finished task
122
+ imports as a finished task, an objective as an objective, and anything else —
123
+ including a plain workspace directory, which carries no state — as a staged
124
+ task. Use `--environment`, `--name`, `--agent`, or `--instructions` to override
125
+ package metadata. In the Web UI, **Import** creates an objective from the
126
+ Objectives view and preserves the exported state everywhere else.
127
+
128
+ A restored task keeps its transcript, its follow-up history, and its changed
129
+ files: the changed-files list is recomputed against the destination's own
130
+ repositories once the workspace is restored, so the Changed Files tab and the
131
+ approve dialog are populated as soon as the import finishes.
132
+
133
+ Continuing an imported conversation replays it rather than resuming it. An
134
+ agent session belongs to the container that created it, and the destination
135
+ container has never run the agent, so the first message after an import hands
136
+ the agent the imported transcript as context — the same mechanism a forked task
137
+ uses.
124
138
 
125
139
  ## Configuration and Profiles
126
140
 
@@ -10,9 +10,14 @@ import { pipeline } from 'node:stream/promises';
10
10
  import { getApiKey, getServerUrl } from '../config.js';
11
11
  import { buildHttpError } from '../http-client.js';
12
12
 
13
- function parseExportArgs(args) {
13
+ const WORKSPACE_MODES = ['delta', 'full', 'none'];
14
+
15
+ const USAGE = 'Usage: coder export <task-id> [--output <file.zip>] [--workspace delta|full|none]';
16
+
17
+ export function parseExportArgs(args) {
14
18
  let taskId = null;
15
19
  let output = null;
20
+ let workspace = null;
16
21
 
17
22
  for (let index = 0; index < args.length; index += 1) {
18
23
  const arg = args[index];
@@ -20,6 +25,10 @@ function parseExportArgs(args) {
20
25
  output = args[++index];
21
26
  } else if (arg.startsWith('--output=')) {
22
27
  output = arg.slice('--output='.length);
28
+ } else if (arg === '--workspace') {
29
+ workspace = args[++index];
30
+ } else if (arg.startsWith('--workspace=')) {
31
+ workspace = arg.slice('--workspace='.length);
23
32
  } else if (!arg.startsWith('-') && !taskId) {
24
33
  taskId = arg;
25
34
  } else {
@@ -27,8 +36,45 @@ function parseExportArgs(args) {
27
36
  }
28
37
  }
29
38
 
30
- if (!taskId) throw new Error('Task ID required. Usage: coder export <task-id> [--output <file.zip>]');
31
- return { taskId, output };
39
+ if (!taskId) throw new Error(`Task ID required. ${USAGE}`);
40
+ if (workspace != null && !WORKSPACE_MODES.includes(workspace)) {
41
+ throw new Error(`--workspace must be ${WORKSPACE_MODES.join(', ')}`);
42
+ }
43
+ return { taskId, output, workspace };
44
+ }
45
+
46
+ /**
47
+ * Describe what the server's capture actually carried.
48
+ *
49
+ * The archive body is a zip, so the capture summary travels in headers. An
50
+ * export that came back without a workspace says why, because "the delta was
51
+ * over the size cap" and "the task changed nothing" are not the same news.
52
+ */
53
+ export function describeExportWorkspace(headers) {
54
+ const mode = headers.get('x-coderflow-workspace-mode') || 'none';
55
+ const reasonHeader = headers.get('x-coderflow-workspace-reason');
56
+ let reason = null;
57
+ if (reasonHeader) {
58
+ try {
59
+ reason = decodeURIComponent(reasonHeader);
60
+ } catch {
61
+ reason = reasonHeader;
62
+ }
63
+ }
64
+
65
+ if (mode === 'none') {
66
+ return reason ? `Workspace: not included (${reason})` : 'Workspace: not included';
67
+ }
68
+ if (mode !== 'delta') {
69
+ return 'Workspace: full capture';
70
+ }
71
+
72
+ const files = Number(headers.get('x-coderflow-workspace-files')) || 0;
73
+ const bytes = Number(headers.get('x-coderflow-workspace-bytes')) || 0;
74
+ const deleted = Number(headers.get('x-coderflow-workspace-deleted')) || 0;
75
+ const parts = [`${files} changed file${files === 1 ? '' : 's'} (${bytes} bytes)`];
76
+ if (deleted > 0) parts.push(`${deleted} deleted path${deleted === 1 ? '' : 's'}`);
77
+ return `Workspace: delta - ${parts.join(', ')}`;
32
78
  }
33
79
 
34
80
  function getResponseFilename(response, taskId) {
@@ -55,10 +101,11 @@ export async function writeExportStream(source, destination) {
55
101
  }
56
102
 
57
103
  export async function exportTask(args = []) {
58
- const { taskId, output } = parseExportArgs(args);
104
+ const { taskId, output, workspace } = parseExportArgs(args);
59
105
  const serverUrl = await getServerUrl();
60
106
  const apiKey = await getApiKey();
61
- const response = await fetch(`${serverUrl}/tasks/${encodeURIComponent(taskId)}/export`, {
107
+ const query = workspace ? `?workspace=${encodeURIComponent(workspace)}` : '';
108
+ const response = await fetch(`${serverUrl}/tasks/${encodeURIComponent(taskId)}/export${query}`, {
62
109
  headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
63
110
  });
64
111
 
@@ -74,5 +121,6 @@ export async function exportTask(args = []) {
74
121
  await fs.mkdir(path.dirname(destination), { recursive: true });
75
122
  await writeExportStream(Readable.fromWeb(response.body), destination);
76
123
  console.log(`Task ${taskId} exported to ${destination}`);
124
+ console.log(describeExportWorkspace(response.headers));
77
125
  return destination;
78
126
  }
@@ -21,13 +21,26 @@ function takeOption(args, index, name) {
21
21
  return null;
22
22
  }
23
23
 
24
+ // `preserve` is not a state of its own: it asks the server to land the task in
25
+ // whatever state it was exported from, which is what makes a round trip a round
26
+ // trip. The explicit modes force a state instead.
27
+ export const IMPORT_MODES = ['preserve', 'objective', 'staged', 'running', 'completed'];
28
+
24
29
  export function parseImportArgs(args) {
25
30
  let source = null;
26
- const options = { mode: 'staged' };
31
+ const options = { mode: 'preserve' };
27
32
  const names = ['mode', 'environment', 'name', 'agent', 'instructions'];
28
33
 
29
34
  for (let index = 0; index < args.length;) {
30
35
  const arg = args[index];
36
+ // A delta records the base each repository came from. By default a
37
+ // differing base is reported and the import proceeds; this asks the server
38
+ // to refuse instead.
39
+ if (arg === '--require-matching-base') {
40
+ options.requireMatchingBase = 'true';
41
+ index += 1;
42
+ continue;
43
+ }
31
44
  let matched = false;
32
45
  for (const name of names) {
33
46
  const parsed = takeOption(args, index, name);
@@ -47,12 +60,78 @@ export function parseImportArgs(args) {
47
60
  }
48
61
 
49
62
  if (!source) throw new Error('Path required. Usage: coder import <directory-or-zip> [options]');
50
- if (!['objective', 'staged', 'running'].includes(options.mode)) {
51
- throw new Error('--mode must be objective, staged, or running');
63
+ if (!IMPORT_MODES.includes(options.mode)) {
64
+ throw new Error(`--mode must be one of ${IMPORT_MODES.join(', ')}`);
52
65
  }
53
66
  return { source, options };
54
67
  }
55
68
 
69
+ /**
70
+ * Say which state the import landed in, and — for a preserved import — that it
71
+ * was the package that decided.
72
+ */
73
+ export function describeImportState(data = {}) {
74
+ const state = data.mode === 'objective' ? 'objective' : `${data.status} task`;
75
+ const preserved = data.requestedMode === 'preserve' && data.importedStatus
76
+ ? ` (preserved from the exported ${data.importedStatus} task)`
77
+ : '';
78
+ return `Imported as ${state}${preserved}`;
79
+ }
80
+
81
+ /**
82
+ * An imported conversation is replayed, not resumed: the destination container
83
+ * has never run this agent, so there is no session to attach to. Say so, or a
84
+ * user reading "completed" will expect `--continue` semantics they will not get.
85
+ */
86
+ export function describeImportConversation(data = {}) {
87
+ if (data.conversationMode === 'history_replay') {
88
+ return 'Conversation: the imported transcript replays into a fresh agent session on your next message.';
89
+ }
90
+ if (data.conversationMode === 'instructions_replay') {
91
+ return 'Conversation: no transcript travelled with this package, so the next message replays the '
92
+ + 'imported instructions and follow-up history into a fresh agent session.';
93
+ }
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Say what the import will do to the destination workspace: merge a delta into
99
+ * it, or replace it outright.
100
+ */
101
+ export function describeImportWorkspace(data = {}) {
102
+ if (!data.workspaceIncluded) {
103
+ // "not included" alone reads as a task that changed nothing. If the export
104
+ // said why it carried no workspace, say it here too.
105
+ return data.workspaceUnavailableReason
106
+ ? `Workspace: not included (${data.workspaceUnavailableReason})`
107
+ : 'Workspace: not included';
108
+ }
109
+ if (data.workspaceMode !== 'delta' || !data.workspaceDelta) {
110
+ return 'Workspace: full archive (replaces the initialized workspace)';
111
+ }
112
+
113
+ const { fileCount = 0, byteCount = 0, deletedPathCount = 0, repositories = [] } = data.workspaceDelta;
114
+ const parts = [`${fileCount} changed file${fileCount === 1 ? '' : 's'} (${byteCount} bytes)`];
115
+ if (deletedPathCount > 0) parts.push(`${deletedPathCount} deleted path${deletedPathCount === 1 ? '' : 's'}`);
116
+ if (repositories.length > 0) parts.push(`repos: ${repositories.join(', ')}`);
117
+ return `Workspace: delta merged into the initialized workspace - ${parts.join(', ')}`;
118
+ }
119
+
120
+ /**
121
+ * The base check cannot run before the server answers: it compares against
122
+ * repositories that only exist once the destination container has finished
123
+ * cloning them, which happens after the import is accepted. Say so, rather than
124
+ * letting a printed success imply the check already passed.
125
+ */
126
+ export function describeBaseCheck(data = {}) {
127
+ if (!data.requireMatchingBase) return null;
128
+ if (!data.workspaceIncluded || data.workspaceMode !== 'delta') {
129
+ return 'Base match: requested, but this package carries no delta to check.';
130
+ }
131
+ return 'Base match: required. The check runs once the destination repositories are cloned; '
132
+ + `if the base differs the import stops and task ${data.taskId} is marked failed.`;
133
+ }
134
+
56
135
  async function zipDirectory(sourceDir) {
57
136
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coderflow-cli-import-'));
58
137
  const zipPath = path.join(tempDir, 'workspace.zip');
@@ -108,6 +187,12 @@ export async function importTask(args = []) {
108
187
  if (data.containerId) console.log(`Container: ${data.containerId}`);
109
188
  if (data.queuePosition) console.log(`Queue position: ${data.queuePosition}`);
110
189
  console.log(`Environment: ${data.environment}`);
190
+ console.log(describeImportState(data));
191
+ console.log(describeImportWorkspace(data));
192
+ const conversation = describeImportConversation(data);
193
+ if (conversation) console.log(conversation);
194
+ const baseCheck = describeBaseCheck(data);
195
+ if (baseCheck) console.log(baseCheck);
111
196
  return data;
112
197
  } finally {
113
198
  if (tempDir) await fs.rm(tempDir, { recursive: true, force: true });
package/lib/help.js CHANGED
@@ -186,39 +186,66 @@ Examples:
186
186
  Usage: coder export <task-id> [options]
187
187
 
188
188
  Download a portable task package containing task output, logs, metadata, and
189
- the retained container workspace when it is available.
189
+ the workspace changes the task made.
190
+
191
+ By default the package carries a workspace *delta*: only the files the task
192
+ added or modified, plus the list of paths it deleted. Importing it merges that
193
+ into the destination environment's own initialized workspace, so dependencies
194
+ and untouched sources are neither shipped nor overwritten.
190
195
 
191
196
  Arguments:
192
197
  task-id Task ID to export (required)
193
198
 
194
199
  Options:
195
200
  --output, -o <file> Destination zip path (defaults to the server filename)
201
+ --workspace=<mode> delta (default), full, or none.
202
+ full archives the entire /workspace and replaces the
203
+ destination workspace on import; none omits it.
196
204
 
197
205
  Examples:
198
206
  coder export 1759542727986-3uoyf48lr
199
207
  coder export 1759542727986-3uoyf48lr --output ./task-backup.zip
208
+ coder export 1759542727986-3uoyf48lr --workspace=full
200
209
  `,
201
210
 
202
211
  import: `
203
212
  Usage: coder import <directory-or-zip> [options]
204
213
 
205
- Create a new objective or task from a CoderFlow export package or a plain
214
+ Recreate a task from a CoderFlow export package, or create one from a plain
206
215
  workspace directory. Plain directories are packaged before upload.
207
216
 
217
+ By default the import reproduces the state the task was exported from: a
218
+ finished task comes back finished, with its transcript and its changed files,
219
+ ready to be continued. Its container has never run the agent, so the first
220
+ message replays the imported conversation into a fresh session rather than
221
+ resuming one.
222
+
208
223
  Arguments:
209
224
  directory-or-zip Export zip or local workspace directory (required)
210
225
 
211
226
  Options:
212
- --mode=<mode> objective, staged, or running (default: staged)
227
+ --mode=<mode> preserve (default), objective, staged, running, or
228
+ completed. preserve reproduces the exported state;
229
+ the others force one. A plain workspace directory has
230
+ no exported state, so preserve stages it.
213
231
  --environment=<name> Override/select the destination environment
214
232
  --name=<name> Override the imported task name
215
233
  --agent=<agent> Override the imported/default agent
216
234
  --instructions=<text> Override instructions used when the task runs
235
+ --require-matching-base
236
+ Refuse a workspace delta whose repositories were
237
+ exported from a different base commit, remote, or
238
+ branch. Without it the mismatch is reported and the
239
+ delta is applied anyway. The check needs the
240
+ destination's repositories, so it runs after the
241
+ import is accepted: a refusal shows up as a failed
242
+ task, not as a failed command.
217
243
 
218
244
  Examples:
219
245
  coder import ./task-backup.zip
220
246
  coder import ./workspace --environment=hello --mode=staged
221
247
  coder import ./task-backup.zip --mode=objective
248
+ coder import ./task-backup.zip --mode=completed
222
249
  coder import ./task-backup.zip --mode=running --agent=codex
223
250
  `,
224
251
 
@@ -367,7 +394,7 @@ Commands:
367
394
  coder apply [task-id] Apply patches from completed task to local repos
368
395
  coder discard [--env=<environment>] [--yes] Discard applied changes from repos
369
396
  coder attach [container-or-task-id] [options] Connect to a running container
370
- coder export <task-id> [--output=<file.zip>] Export task contents and logs
397
+ coder export <task-id> [--workspace=delta|full] Export task contents and logs
371
398
  coder import <directory-or-zip> [options] Import an objective or task container
372
399
 
373
400
  Setup:
@@ -431,7 +458,7 @@ Task Management:
431
458
  coder logs <task-id> [--tail=N] Show task logs
432
459
  coder list [--status=...] [--environment=...] List tasks with optional filters
433
460
  coder reject <task-id> [--cleanup] Reject task results
434
- coder export <task-id> [--output=file.zip] Export task contents and logs
461
+ coder export <task-id> [--workspace=delta|full] Export task contents and logs
435
462
  coder import <directory-or-zip> [options] Restore an objective or task
436
463
 
437
464
  Interactive Sessions:
@@ -490,7 +517,7 @@ Task Management (Web UI alternative):
490
517
  coder results 1759542727986-3uoyf48lr # Get task results
491
518
  coder list --status=completed # List completed tasks
492
519
  coder export 1759542727986-3uoyf48lr # Download a portable task package
493
- coder import ./task-export.zip --mode=staged # Restore into a staged container
520
+ coder import ./task-export.zip # Restore it in the state it was exported in
494
521
 
495
522
  Interactive Sessions (Web UI alternative):
496
523
  coder start hello # Start AI agent in environment
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profoundlogic/coderflow-cli",
3
- "version": "0.13.11-dev.20260807233136.g074f26b1",
3
+ "version": "0.13.11",
4
4
  "description": "AI Coder CLI - Command-line interface for managing AI coding tasks",
5
5
  "main": "coder.js",
6
6
  "type": "module",