@ricsam/r5dctl 0.0.66 → 0.0.68

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
@@ -75,6 +75,9 @@ r5dctl conversation inspect-node <session-id> <node-id> # raw persisted node pl
75
75
  r5dctl conversation inspect-work <session-id> <work-id> # compact call list for one omitted turn
76
76
  r5dctl conversation inspect-work <session-id> <work-id> --summary
77
77
  r5dctl conversation inspect-work <session-id> <work-id> --full
78
+ r5dctl conversation move-head <session-id> <node-id> # rewind or replay the transcript; files stay put
79
+ r5dctl conversation fork <session-id> <node-id> # copy the branch up to a node into a new session
80
+ r5dctl conversation fork <session-id> <node-id> --name "Retry with tests"
78
81
  r5dctl session prompt --worker <label> --mode plan --model max <session-id> "..."
79
82
  r5dctl -s <session-id> answer-questions --worker <label> --mode plan --model max -a1 "Recipe Collection" -a2 "Both Manual & AI"
80
83
  r5dctl -s <session-id> answer-env-request --worker <label> --mode agent --model max -e KEY=value --context "Generate the remaining salts"
@@ -133,6 +136,8 @@ When available, conversation rendering uses the newest complete persisted model-
133
136
 
134
137
  `conversation inspect-node` reads one exact persisted conversation node as formatted JSON. Its header includes the previous node, every direct next node, the next node on the active branch, whether the selected node is active, and the containing `work_...` ID when one is available. This allows callers that start with only a copied chat node ID to traverse nearby nodes or zoom out with `conversation inspect-work`.
135
138
 
139
+ `conversation move-head` and `conversation fork` change the transcript and nothing else. Neither one checks out, resets, or publishes anything: worker workspaces and the canonical repository keep the state the agent left them in. `move-head` points the session at any recorded node, so it rewinds past work, replays forward again, or switches to a sibling branch; every node stays stored, which makes the move reversible. The next prompt continues from the new head, and when that head already has children the new message becomes a sibling branch. `fork` instead copies the branch ending at the chosen node into a new session on the same project branch, named after its source, leaving the original session running untouched. Both refuse hidden maintenance sessions, and `move-head` refuses while a turn is running — stop the session first.
140
+
136
141
  Project mode is derived from the persisted `backwardsCompatible` project setting:
137
142
 
138
143
  - `greenfield`: agent can make breaking/reset-style changes
package/dist/cjs/cli.cjs CHANGED
@@ -46,6 +46,7 @@ __export(cli_exports, {
46
46
  parseAnswerEnvRequestCommandArgs: () => parseAnswerEnvRequestCommandArgs,
47
47
  parseAnswerFlags: () => parseAnswerFlags,
48
48
  parseAnswerQuestionsCommandArgs: () => parseAnswerQuestionsCommandArgs,
49
+ parseConversationForkArgs: () => parseConversationForkArgs,
49
50
  parseConversationRenderArgs: () => parseConversationRenderArgs,
50
51
  parseConversationWorkDetailArgs: () => parseConversationWorkDetailArgs,
51
52
  parseEnvFlags: () => parseEnvFlags,
@@ -61,6 +62,8 @@ __export(cli_exports, {
61
62
  parseSetEnvArgs: () => parseSetEnvArgs,
62
63
  parseShellArgs: () => parseShellArgs,
63
64
  readDotenvFile: () => readDotenvFile,
65
+ renderConversationForkResponse: () => renderConversationForkResponse,
66
+ renderConversationHeadResponse: () => renderConversationHeadResponse,
64
67
  renderConversationNodeResponse: () => renderConversationNodeResponse,
65
68
  renderConversationOverviewResponse: () => renderConversationOverviewResponse,
66
69
  renderConversationResponse: () => renderConversationResponse,
@@ -261,6 +264,16 @@ const SHARED_HELP_ENTRIES = [
261
264
  usage: "conversation inspect-work <session-id> <work-id> [--compact|--summary|--full]",
262
265
  description: "Inspect one turn's omitted tool work at the selected detail."
263
266
  },
267
+ {
268
+ section: "sessions",
269
+ usage: "conversation move-head <session-id> <node-id>",
270
+ description: "Rewind or replay the conversation to a node. Only the transcript moves; no files change."
271
+ },
272
+ {
273
+ section: "sessions",
274
+ usage: "conversation fork <session-id> <node-id> [--name <name>]",
275
+ description: "Copy the branch ending at a node into a new session, leaving the source untouched."
276
+ },
264
277
  {
265
278
  section: "sessions",
266
279
  usage: '-p <project> session start --worker <label> (--worktree <branch>|--new-worktree <branch> --source worktree:<branch>) --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
@@ -2425,6 +2438,21 @@ function parseConversationWorkDetailArgs(args) {
2425
2438
  }
2426
2439
  return detail;
2427
2440
  }
2441
+ function parseConversationForkArgs(args) {
2442
+ let name;
2443
+ for (let index = 0; index < args.length; index += 1) {
2444
+ const arg = args[index];
2445
+ if (arg !== "--name") {
2446
+ throw new Error(`Unknown conversation fork argument: ${arg}`);
2447
+ }
2448
+ if (name !== void 0) {
2449
+ throw new Error("Use --name only once");
2450
+ }
2451
+ index += 1;
2452
+ name = requireValue(args[index], "Missing value for --name");
2453
+ }
2454
+ return name;
2455
+ }
2428
2456
  function renderConversationOverviewResponse(read) {
2429
2457
  return read.overviewText.endsWith("\n") ? read.overviewText : `${read.overviewText}
2430
2458
  `;
@@ -2451,6 +2479,15 @@ function renderConversationWorkResponse(read) {
2451
2479
  return read.workText.endsWith("\n") ? read.workText : `${read.workText}
2452
2480
  `;
2453
2481
  }
2482
+ function renderConversationHeadResponse(read) {
2483
+ return `Conversation head moved to ${read.head} (revision ${read.revision}). Files on disk are unchanged.
2484
+ `;
2485
+ }
2486
+ function renderConversationForkResponse(read) {
2487
+ const name = read.session.name ? ` (${read.session.name})` : "";
2488
+ return `Forked ${read.copiedNodeCount} node(s) up to ${read.forkedFromNodeId} into session ${read.session.id}${name}.
2489
+ `;
2490
+ }
2454
2491
  async function executeR5dctlCommand(client, json, args) {
2455
2492
  const write = (data, human) => writeDataOutput(json, data, human);
2456
2493
  const [first, second, third] = args;
@@ -2645,6 +2682,24 @@ Sessions: ${result.sessions.length}
2645
2682
  write(read, renderConversationNodeResponse(read));
2646
2683
  return;
2647
2684
  }
2685
+ if (first === "conversation" && second === "move-head") {
2686
+ if (args.length > 4) {
2687
+ throw new Error(`Unexpected conversation move-head argument: ${args[4]}`);
2688
+ }
2689
+ const sessionId = requireValue(args[2], "Missing session id");
2690
+ const nodeId2 = requireValue(args[3], "Missing node id");
2691
+ const read = await client.sessions.moveConversationHead(sessionId, nodeId2);
2692
+ write(read, renderConversationHeadResponse(read));
2693
+ return;
2694
+ }
2695
+ if (first === "conversation" && second === "fork") {
2696
+ const sessionId = requireValue(args[2], "Missing session id");
2697
+ const nodeId2 = requireValue(args[3], "Missing node id");
2698
+ const name = parseConversationForkArgs(args.slice(4));
2699
+ const read = await client.sessions.forkConversation(sessionId, nodeId2, name === void 0 ? {} : { name });
2700
+ write(read, renderConversationForkResponse(read));
2701
+ return;
2702
+ }
2648
2703
  if (first === "conversation" && second === "inspect-work") {
2649
2704
  const sessionId = requireValue(args[2], "Missing session id");
2650
2705
  const workId = requireValue(args[3], "Missing work id");
@@ -3055,34 +3110,26 @@ function resolveCommandExecution(options, rest) {
3055
3110
  pluginArgs: ["conversation", "overview", sessionId2, ...remainingArgs]
3056
3111
  };
3057
3112
  }
3058
- if (subcommand === "inspect-work") {
3059
- const sessionId2 = options.session ?? commandArgs[1];
3060
- if (!sessionId2) {
3061
- throw new Error("Session id is required for `conversation inspect-work`");
3062
- }
3063
- const workId = options.session ? commandArgs[1] : commandArgs[2];
3064
- if (!workId) {
3065
- throw new Error("Work id is required for `conversation inspect-work`");
3066
- }
3067
- const remainingArgs = options.session ? commandArgs.slice(2) : commandArgs.slice(3);
3068
- return {
3069
- kind: "plugin",
3070
- pluginArgs: ["conversation", "inspect-work", sessionId2, workId, ...remainingArgs]
3071
- };
3072
- }
3073
- if (subcommand === "inspect-node") {
3113
+ const targetedSubcommands = [
3114
+ { subcommand: "inspect-work", secondIdLabel: "Work id" },
3115
+ { subcommand: "inspect-node", secondIdLabel: "Node id" },
3116
+ { subcommand: "move-head", secondIdLabel: "Node id" },
3117
+ { subcommand: "fork", secondIdLabel: "Node id" }
3118
+ ];
3119
+ const targeted = targetedSubcommands.find((candidate) => candidate.subcommand === subcommand);
3120
+ if (targeted) {
3074
3121
  const sessionId2 = options.session ?? commandArgs[1];
3075
3122
  if (!sessionId2) {
3076
- throw new Error("Session id is required for `conversation inspect-node`");
3123
+ throw new Error(`Session id is required for \`conversation ${targeted.subcommand}\``);
3077
3124
  }
3078
- const nodeId2 = options.session ? commandArgs[1] : commandArgs[2];
3079
- if (!nodeId2) {
3080
- throw new Error("Node id is required for `conversation inspect-node`");
3125
+ const secondId = options.session ? commandArgs[1] : commandArgs[2];
3126
+ if (!secondId) {
3127
+ throw new Error(`${targeted.secondIdLabel} is required for \`conversation ${targeted.subcommand}\``);
3081
3128
  }
3082
3129
  const remainingArgs = options.session ? commandArgs.slice(2) : commandArgs.slice(3);
3083
3130
  return {
3084
3131
  kind: "plugin",
3085
- pluginArgs: ["conversation", "inspect-node", sessionId2, nodeId2, ...remainingArgs]
3132
+ pluginArgs: ["conversation", targeted.subcommand, sessionId2, secondId, ...remainingArgs]
3086
3133
  };
3087
3134
  }
3088
3135
  const sessionId = options.session;
@@ -3269,6 +3316,7 @@ async function main(argv = process.argv.slice(2)) {
3269
3316
  parseAnswerEnvRequestCommandArgs,
3270
3317
  parseAnswerFlags,
3271
3318
  parseAnswerQuestionsCommandArgs,
3319
+ parseConversationForkArgs,
3272
3320
  parseConversationRenderArgs,
3273
3321
  parseConversationWorkDetailArgs,
3274
3322
  parseEnvFlags,
@@ -3284,6 +3332,8 @@ async function main(argv = process.argv.slice(2)) {
3284
3332
  parseSetEnvArgs,
3285
3333
  parseShellArgs,
3286
3334
  readDotenvFile,
3335
+ renderConversationForkResponse,
3336
+ renderConversationHeadResponse,
3287
3337
  renderConversationNodeResponse,
3288
3338
  renderConversationOverviewResponse,
3289
3339
  renderConversationResponse,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -180,6 +180,16 @@ const SHARED_HELP_ENTRIES = [
180
180
  usage: "conversation inspect-work <session-id> <work-id> [--compact|--summary|--full]",
181
181
  description: "Inspect one turn's omitted tool work at the selected detail."
182
182
  },
183
+ {
184
+ section: "sessions",
185
+ usage: "conversation move-head <session-id> <node-id>",
186
+ description: "Rewind or replay the conversation to a node. Only the transcript moves; no files change."
187
+ },
188
+ {
189
+ section: "sessions",
190
+ usage: "conversation fork <session-id> <node-id> [--name <name>]",
191
+ description: "Copy the branch ending at a node into a new session, leaving the source untouched."
192
+ },
183
193
  {
184
194
  section: "sessions",
185
195
  usage: '-p <project> session start --worker <label> (--worktree <branch>|--new-worktree <branch> --source worktree:<branch>) --model <tier> [-f|--follow|--watch] [--tools] "<prompt>"',
@@ -2344,6 +2354,21 @@ function parseConversationWorkDetailArgs(args) {
2344
2354
  }
2345
2355
  return detail;
2346
2356
  }
2357
+ function parseConversationForkArgs(args) {
2358
+ let name;
2359
+ for (let index = 0; index < args.length; index += 1) {
2360
+ const arg = args[index];
2361
+ if (arg !== "--name") {
2362
+ throw new Error(`Unknown conversation fork argument: ${arg}`);
2363
+ }
2364
+ if (name !== void 0) {
2365
+ throw new Error("Use --name only once");
2366
+ }
2367
+ index += 1;
2368
+ name = requireValue(args[index], "Missing value for --name");
2369
+ }
2370
+ return name;
2371
+ }
2347
2372
  function renderConversationOverviewResponse(read) {
2348
2373
  return read.overviewText.endsWith("\n") ? read.overviewText : `${read.overviewText}
2349
2374
  `;
@@ -2370,6 +2395,15 @@ function renderConversationWorkResponse(read) {
2370
2395
  return read.workText.endsWith("\n") ? read.workText : `${read.workText}
2371
2396
  `;
2372
2397
  }
2398
+ function renderConversationHeadResponse(read) {
2399
+ return `Conversation head moved to ${read.head} (revision ${read.revision}). Files on disk are unchanged.
2400
+ `;
2401
+ }
2402
+ function renderConversationForkResponse(read) {
2403
+ const name = read.session.name ? ` (${read.session.name})` : "";
2404
+ return `Forked ${read.copiedNodeCount} node(s) up to ${read.forkedFromNodeId} into session ${read.session.id}${name}.
2405
+ `;
2406
+ }
2373
2407
  async function executeR5dctlCommand(client, json, args) {
2374
2408
  const write = (data, human) => writeDataOutput(json, data, human);
2375
2409
  const [first, second, third] = args;
@@ -2564,6 +2598,24 @@ Sessions: ${result.sessions.length}
2564
2598
  write(read, renderConversationNodeResponse(read));
2565
2599
  return;
2566
2600
  }
2601
+ if (first === "conversation" && second === "move-head") {
2602
+ if (args.length > 4) {
2603
+ throw new Error(`Unexpected conversation move-head argument: ${args[4]}`);
2604
+ }
2605
+ const sessionId = requireValue(args[2], "Missing session id");
2606
+ const nodeId2 = requireValue(args[3], "Missing node id");
2607
+ const read = await client.sessions.moveConversationHead(sessionId, nodeId2);
2608
+ write(read, renderConversationHeadResponse(read));
2609
+ return;
2610
+ }
2611
+ if (first === "conversation" && second === "fork") {
2612
+ const sessionId = requireValue(args[2], "Missing session id");
2613
+ const nodeId2 = requireValue(args[3], "Missing node id");
2614
+ const name = parseConversationForkArgs(args.slice(4));
2615
+ const read = await client.sessions.forkConversation(sessionId, nodeId2, name === void 0 ? {} : { name });
2616
+ write(read, renderConversationForkResponse(read));
2617
+ return;
2618
+ }
2567
2619
  if (first === "conversation" && second === "inspect-work") {
2568
2620
  const sessionId = requireValue(args[2], "Missing session id");
2569
2621
  const workId = requireValue(args[3], "Missing work id");
@@ -2974,34 +3026,26 @@ function resolveCommandExecution(options, rest) {
2974
3026
  pluginArgs: ["conversation", "overview", sessionId2, ...remainingArgs]
2975
3027
  };
2976
3028
  }
2977
- if (subcommand === "inspect-work") {
2978
- const sessionId2 = options.session ?? commandArgs[1];
2979
- if (!sessionId2) {
2980
- throw new Error("Session id is required for `conversation inspect-work`");
2981
- }
2982
- const workId = options.session ? commandArgs[1] : commandArgs[2];
2983
- if (!workId) {
2984
- throw new Error("Work id is required for `conversation inspect-work`");
2985
- }
2986
- const remainingArgs = options.session ? commandArgs.slice(2) : commandArgs.slice(3);
2987
- return {
2988
- kind: "plugin",
2989
- pluginArgs: ["conversation", "inspect-work", sessionId2, workId, ...remainingArgs]
2990
- };
2991
- }
2992
- if (subcommand === "inspect-node") {
3029
+ const targetedSubcommands = [
3030
+ { subcommand: "inspect-work", secondIdLabel: "Work id" },
3031
+ { subcommand: "inspect-node", secondIdLabel: "Node id" },
3032
+ { subcommand: "move-head", secondIdLabel: "Node id" },
3033
+ { subcommand: "fork", secondIdLabel: "Node id" }
3034
+ ];
3035
+ const targeted = targetedSubcommands.find((candidate) => candidate.subcommand === subcommand);
3036
+ if (targeted) {
2993
3037
  const sessionId2 = options.session ?? commandArgs[1];
2994
3038
  if (!sessionId2) {
2995
- throw new Error("Session id is required for `conversation inspect-node`");
3039
+ throw new Error(`Session id is required for \`conversation ${targeted.subcommand}\``);
2996
3040
  }
2997
- const nodeId2 = options.session ? commandArgs[1] : commandArgs[2];
2998
- if (!nodeId2) {
2999
- throw new Error("Node id is required for `conversation inspect-node`");
3041
+ const secondId = options.session ? commandArgs[1] : commandArgs[2];
3042
+ if (!secondId) {
3043
+ throw new Error(`${targeted.secondIdLabel} is required for \`conversation ${targeted.subcommand}\``);
3000
3044
  }
3001
3045
  const remainingArgs = options.session ? commandArgs.slice(2) : commandArgs.slice(3);
3002
3046
  return {
3003
3047
  kind: "plugin",
3004
- pluginArgs: ["conversation", "inspect-node", sessionId2, nodeId2, ...remainingArgs]
3048
+ pluginArgs: ["conversation", targeted.subcommand, sessionId2, secondId, ...remainingArgs]
3005
3049
  };
3006
3050
  }
3007
3051
  const sessionId = options.session;
@@ -3187,6 +3231,7 @@ export {
3187
3231
  parseAnswerEnvRequestCommandArgs,
3188
3232
  parseAnswerFlags,
3189
3233
  parseAnswerQuestionsCommandArgs,
3234
+ parseConversationForkArgs,
3190
3235
  parseConversationRenderArgs,
3191
3236
  parseConversationWorkDetailArgs,
3192
3237
  parseEnvFlags,
@@ -3202,6 +3247,8 @@ export {
3202
3247
  parseSetEnvArgs,
3203
3248
  parseShellArgs,
3204
3249
  readDotenvFile,
3250
+ renderConversationForkResponse,
3251
+ renderConversationHeadResponse,
3205
3252
  renderConversationNodeResponse,
3206
3253
  renderConversationOverviewResponse,
3207
3254
  renderConversationResponse,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
4
4
  "type": "module"
5
5
  }
@@ -1,4 +1,4 @@
1
- import { R5dctlClient, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessRun, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorktreeSource, type R5dctlMergeConflictMode, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
1
+ import { R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessRun, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorktreeSource, type R5dctlMergeConflictMode, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
2
2
  export type GlobalOptions = {
3
3
  baseUrl?: string;
4
4
  json: boolean;
@@ -299,9 +299,12 @@ export type R5dctlFollowOptions = {
299
299
  };
300
300
  export declare function followR5dctlSession(client: R5dctlFollowClient, sessionId: string, options: R5dctlFollowOptions): Promise<number>;
301
301
  export declare function parseConversationWorkDetailArgs(args: string[]): R5dctlConversationWorkDetail;
302
+ export declare function parseConversationForkArgs(args: string[]): string | undefined;
302
303
  export declare function renderConversationOverviewResponse(read: R5dctlConversationOverviewResponse): string;
303
304
  export declare function renderConversationNodeResponse(read: R5dctlConversationNodeResponse): string;
304
305
  export declare function renderConversationWorkResponse(read: R5dctlConversationWorkResponse): string;
306
+ export declare function renderConversationHeadResponse(read: R5dctlConversationHeadResponse): string;
307
+ export declare function renderConversationForkResponse(read: R5dctlConversationForkResponse): string;
305
308
  export declare function resolveCommandExecution(options: GlobalOptions, rest: string[]): CommandExecutionPlan;
306
309
  export declare function runR5dctlCli(argv: string[]): Promise<number>;
307
310
  export declare function main(argv?: string[]): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/cli.cjs",
6
6
  "module": "./dist/mjs/cli.mjs",
@@ -26,7 +26,7 @@
26
26
  "r5dctl": "dist/cjs/main.cjs"
27
27
  },
28
28
  "dependencies": {
29
- "@ricsam/r5d-api": "^0.0.66",
29
+ "@ricsam/r5d-api": "^0.0.68",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"