@meistrari/remy-cli 1.16.0 → 1.17.1

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 (3) hide show
  1. package/README.md +13 -2
  2. package/dist/remy.js +110 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,7 +38,7 @@ The dashboard is the starting point for all interactive work. It refreshes the v
38
38
 
39
39
  Press `n` in the dashboard. The new-session flow asks what you want done, selects a GitHub installation when needed, suggests repositories based on your request, and lets you review those suggestions. Remy analyzes the request for existing work: a Remy session ID or number, a PR URL or number (`PR 267`, `#267`, or `owner/repo#267`), or an exact or approximate branch name. It verifies matches against the selected repositories and offers up to five branches per repository. In the branch step, use arrows to move, Space to select, and Enter to select the current option and review your choices. Each repository can continue one branch or start a new session branch. A lookup failure stays on a retry screen; press `r` to retry, `n` to explicitly choose new branches, or Esc to edit the request. Before creating the session, choose the agent model and reasoning effort. The wizard starts with `gpt-6-astra` and `medium` reasoning by default. Remy then opens the session and streams its activity.
40
40
 
41
- Use `Tab` while writing the request to complete a local file or directory path. Remy attaches selected paths when you confirm the request. Each file may be at most 20 MiB. A directory may contain at most 20 MiB across its regular files before compression; Remy uploads it as a temporary `<directory>.tar.gz` archive with the selected directory preserved as its root, so the remote agent can extract and use its files. Remy rejects oversized attachments with guidance to choose a smaller file or directory, and rejects directory attachments that contain symbolic links or other non-regular entries. On macOS, `Ctrl+V` adds a PNG from the system clipboard to the request; Remy attaches it on confirmation. Remy uploads attachments before creating the session and shows their filenames beneath your message.
41
+ Use `Tab` while writing the request to complete a local file or directory path. Remy attaches selected paths when you confirm the request. You can also paste an absolute path directly; quote paths containing spaces (for example, `"/Users/me/Desktop/Screen Shot.png"`) or use shell-style backslash escapes. Autocomplete selection is not required. Each file may be at most 20 MiB. A directory may contain at most 20 MiB across its regular files before compression; Remy uploads it as a temporary `<directory>.tar.gz` archive with the selected directory preserved as its root, so the remote agent can extract and use its files. Remy rejects oversized attachments with guidance to choose a smaller file or directory, and rejects directory attachments that contain symbolic links or other non-regular entries. On macOS, `Ctrl+V` saves a clipboard image as a temporary PNG and adds its path to the request; Remy attaches it on confirmation. Remy uploads attachments before creating the session and shows their filenames beneath your message.
42
42
 
43
43
  ### Resume or follow up
44
44
 
@@ -102,6 +102,14 @@ The branch-suggestion flow belongs to the interactive new-session wizard (`remy`
102
102
  remy --session <session-id>
103
103
  ```
104
104
 
105
+ To submit a follow-up immediately, add one quoted positional `prompt`:
106
+
107
+ ```bash
108
+ remy --session <session-id> "Add regression coverage for that fix"
109
+ ```
110
+
111
+ Remy sends the prompt as **Steer**, then opens the conversation. Omitting the prompt only attaches. An explicitly empty prompt is rejected. Use `--` before a prompt beginning with `--` to treat it as text.
112
+
105
113
  ### Automate a session
106
114
 
107
115
  `remy new` and `remy --session` use JSON Lines output automatically when standard input or output is not a terminal. Pass `--no-tui` to choose that mode explicitly; `--json` also selects it.
@@ -111,8 +119,11 @@ Opaque session and message metadata returned by the API is accepted for compatib
111
119
  ```bash
112
120
  remy new --no-tui --repository owner/repository "Add a health check endpoint"
113
121
  remy --session <session-id> --json
122
+ remy --session <session-id> --no-tui "Add regression coverage for that fix"
114
123
  ```
115
124
 
125
+ With a follow-up prompt, Remy submits it once and streams JSON Lines until that message reaches a terminal outcome, even if a previous message is cached locally. It exits with status `0` for a completed turn or `1` for another terminal outcome or an API failure. A follow-up does not emit a `created` record because the session already exists. If the local session cache becomes unavailable after admission, Remy warns once and continues observing the accepted follow-up in memory. Do not resubmit the prompt because of that warning; local resume metadata may remain stale. These commands require prior sign-in and can be used by scripts or other agents.
126
+
116
127
  For a newly created session, Remy writes a `created` record, event-name records as they arrive, then a `terminal` record when the submitted turn settles. A completed turn exits with status `0`; another terminal outcome exits with status `1`.
117
128
 
118
129
  ```json
@@ -154,7 +165,7 @@ remy logout [--api-url <url>]
154
165
  remy whoami
155
166
  remy dashboard
156
167
  remy new [--repository <owner/name> ... --installation <id> --model <name> --reasoning-effort <low|medium|high|xhigh> --attach <path> ... --no-tui --json] <prompt>
157
- remy --session <session-id> [--no-tui] [--json]
168
+ remy --session <session-id> [--no-tui] [--json] [prompt]
158
169
  ```
159
170
 
160
171
  Run `remy <command> --help` for flags and command-specific usage. `remy whoami` prints the saved signed-in identity and organization.
package/dist/remy.js CHANGED
@@ -35315,6 +35315,7 @@ function createRemoteSessionController(dependencies) {
35315
35315
  let stopped = false;
35316
35316
  let state;
35317
35317
  let cachedLastRetainedEventId;
35318
+ let cacheUnavailable = false;
35318
35319
  let reasoningPreviewState = initialReasoningPreviewState();
35319
35320
  let reasoningConnectionGeneration = 0;
35320
35321
  let reasoningHistorySequenceFloor = -1;
@@ -35323,7 +35324,10 @@ function createRemoteSessionController(dependencies) {
35323
35324
  ready = new Promise((resolve) => {
35324
35325
  resolveReady = resolve;
35325
35326
  });
35326
- const cache = await readSessionCache(cachePath);
35327
+ const cache = await readSessionCache(cachePath).catch((error93) => {
35328
+ handleCacheError(error93);
35329
+ return null;
35330
+ });
35327
35331
  cachedLastRetainedEventId = input.mode === "live" ? input.lastRetainedEventId ?? cache?.lastRetainedEventId : undefined;
35328
35332
  state = createSessionViewState({
35329
35333
  detail: input.detail,
@@ -35550,6 +35554,8 @@ function createRemoteSessionController(dependencies) {
35550
35554
  };
35551
35555
  }
35552
35556
  async function writeCache() {
35557
+ if (cacheUnavailable)
35558
+ return;
35553
35559
  const currentState = getState();
35554
35560
  const cache = {
35555
35561
  version: 1,
@@ -35559,7 +35565,13 @@ function createRemoteSessionController(dependencies) {
35559
35565
  ...currentState.activeMessageId ? { activeMessageId: currentState.activeMessageId } : {},
35560
35566
  updatedAt: new Date().toISOString()
35561
35567
  };
35562
- await writeSessionCache({ path: cachePath, cache });
35568
+ await writeSessionCache({ path: cachePath, cache }).catch(handleCacheError);
35569
+ }
35570
+ function handleCacheError(error93) {
35571
+ if (!dependencies.onCacheError)
35572
+ throw error93;
35573
+ cacheUnavailable = true;
35574
+ dependencies.onCacheError(error93);
35563
35575
  }
35564
35576
  function publishState(frame) {
35565
35577
  const update = { state: getState(), ...frame ? { frame } : {} };
@@ -39350,13 +39362,35 @@ var directoryArchiveMediaType = "application/gzip";
39350
39362
  var directoryArchiveSuffix = ".tar.gz";
39351
39363
  var maxPortableFilenameBytes = 255;
39352
39364
  var attachmentSizeLimitLabel = `${fileUploadMaxBytes / (1024 * 1024)} MiB`;
39365
+ var clipboardImageScript = `
39366
+ ObjC.import('AppKit');
39367
+ const pasteboard = $.NSPasteboard.generalPasteboard;
39368
+ let png = pasteboard.dataForType($.NSPasteboardTypePNG);
39369
+ if (!png || png.isNil()) {
39370
+ const tiff = pasteboard.dataForType($.NSPasteboardTypeTIFF);
39371
+ if (!tiff || tiff.isNil()) throw new Error('Clipboard does not contain an image.');
39372
+ const bitmap = $.NSBitmapImageRep.imageRepWithData(tiff);
39373
+ if (!bitmap || bitmap.isNil()) throw new Error('Clipboard image could not be decoded.');
39374
+ png = bitmap.representationUsingTypeProperties($.NSPNGFileType, $.NSDictionary.dictionary);
39375
+ }
39376
+ if (!png || png.isNil()) throw new Error('Clipboard image could not be converted to PNG.');
39377
+ if (png.length > ${fileUploadMaxBytes}) throw new Error('Clipboard image is larger than ${attachmentSizeLimitLabel}.');
39378
+ ObjC.unwrap(png.base64EncodedStringWithOptions(0));
39379
+ `;
39353
39380
  async function readClipboardImage() {
39354
39381
  if (process.platform !== "darwin")
39355
39382
  throw new Error("Clipboard image paste is supported on macOS only.");
39356
- const clipboard = Bun.spawn(["pbpaste", "-Prefer", "png"], { stdout: "pipe", stderr: "ignore" });
39357
- const bytes = new Uint8Array(await new Response(clipboard.stdout).arrayBuffer());
39358
- if (await clipboard.exited !== 0 || !isPng(bytes))
39359
- throw new Error("Clipboard does not contain an image.");
39383
+ const clipboard = Bun.spawn(["/usr/bin/osascript", "-l", "JavaScript", "-e", clipboardImageScript], { stdout: "pipe", stderr: "pipe" });
39384
+ const [encoded, error93, exitCode] = await Promise.all([
39385
+ new Response(clipboard.stdout).text(),
39386
+ new Response(clipboard.stderr).text(),
39387
+ clipboard.exited
39388
+ ]);
39389
+ if (exitCode !== 0)
39390
+ throw new Error(`Could not read clipboard image: ${error93.trim() || "macOS clipboard reader failed."}`);
39391
+ const bytes = new Uint8Array(Buffer2.from(encoded.trim(), "base64"));
39392
+ if (!isPng(bytes))
39393
+ throw new Error("Clipboard image could not be decoded as PNG.");
39360
39394
  return { bytes, mediaType: "image/png" };
39361
39395
  }
39362
39396
  async function writePastedImage({
@@ -39546,8 +39580,11 @@ function promptFilePaths({ text, cwd, selectedPaths }) {
39546
39580
  const paths = new Map;
39547
39581
  for (const selectedPath of selectedPaths)
39548
39582
  paths.set(isAbsolute2(selectedPath) ? selectedPath : resolve2(cwd, selectedPath), true);
39549
- for (const rawToken of text.split(/\s+/)) {
39550
- const token = rawToken.replace(/^[([{'"`]+/, "").replace(/[),.;:!?\]}"`]+$/, "");
39583
+ const tokens = text.match(/[([{]*@?(?:"[^"]*"|'[^']*'|`[^`]*`)|(?:\\.|[^\s\\])+/g) ?? [];
39584
+ for (const quotedToken of tokens) {
39585
+ const wrappedToken = quotedToken.replace(/^[([{]+/, "").replace(/[),.;:!?\]}]+$/, "");
39586
+ const quotedPath = /^(@?)(["'`])([\s\S]*)\2$/.exec(wrappedToken);
39587
+ const token = quotedPath ? `${quotedPath[1]}${quotedPath[3]}` : wrappedToken.replace(/\\(.)/g, "$1");
39551
39588
  const pathText = token.startsWith("@") ? token.slice(1) : token;
39552
39589
  if (!pathText)
39553
39590
  continue;
@@ -39596,7 +39633,7 @@ var compactMarkRows = 9;
39596
39633
  var compactMinWidth = 48;
39597
39634
  var compactMinHeight = 20;
39598
39635
  var markBrightnessGain = 4.2;
39599
- var remyCliVersion = "1.16.0";
39636
+ var remyCliVersion = "1.17.1";
39600
39637
  async function showRemySplash({
39601
39638
  createRenderer = createRemyRenderer,
39602
39639
  durationMs = splashDurationMs,
@@ -39969,7 +40006,7 @@ async function dispatchCliCommandWithShutdown({
39969
40006
  });
39970
40007
  }
39971
40008
  if (command.name === "session")
39972
- return await attachSession({ dependencies, sessionId: command.sessionId, noTui: command.noTui, json: command.json });
40009
+ return await attachSession({ dependencies, sessionId: command.sessionId, noTui: command.noTui, json: command.json, prompt: command.prompt });
39973
40010
  return await createNewSession({ dependencies, command });
39974
40011
  }
39975
40012
  function createCliShutdown({ abortSignal }) {
@@ -40876,12 +40913,30 @@ async function attachSession({
40876
40913
  dependencies,
40877
40914
  sessionId,
40878
40915
  noTui,
40879
- json: json3
40916
+ json: json3,
40917
+ prompt
40880
40918
  }) {
40881
40919
  const operations = await createSessionOperations(dependencies);
40882
40920
  const detail = await operations.getSession({ client: operations.client, sessionId });
40883
40921
  const repositories = detail.repositories.map((repository) => ({ id: repository.id, fullName: repository.full_name }));
40884
40922
  const cache = await (dependencies.readSessionCache ?? readSessionCache)(resolveSessionCachePathForCommand({ dependencies, sessionId }));
40923
+ let activeMessageId = cache?.activeMessageId;
40924
+ let cacheWarningReported = false;
40925
+ const onCacheError = prompt === undefined ? undefined : () => {
40926
+ if (cacheWarningReported)
40927
+ return;
40928
+ cacheWarningReported = true;
40929
+ dependencies.output.writeStderr(`Local session cache is unavailable; continuing to observe the accepted follow-up. Do not resubmit the prompt.
40930
+ `);
40931
+ };
40932
+ if (prompt !== undefined) {
40933
+ throwIfAborted2(dependencies.abortSignal);
40934
+ const appended = await operations.appendSessionMessage({
40935
+ client: operations.client,
40936
+ input: { sessionId, text: prompt, fileIds: [], mode: "steer", idempotencyKey: randomUUID5() }
40937
+ });
40938
+ activeMessageId = appended.message.id;
40939
+ }
40885
40940
  await (dependencies.writeSessionCache ?? writeSessionCache)({
40886
40941
  path: resolveSessionCachePathForCommand({ dependencies, sessionId }),
40887
40942
  cache: {
@@ -40889,20 +40944,25 @@ async function attachSession({
40889
40944
  sessionId,
40890
40945
  repositories,
40891
40946
  ...cache?.lastRetainedEventId ? { lastRetainedEventId: cache.lastRetainedEventId } : {},
40892
- ...cache?.activeMessageId ? { activeMessageId: cache.activeMessageId } : {},
40947
+ ...activeMessageId ? { activeMessageId } : {},
40893
40948
  updatedAt: new Date().toISOString()
40894
40949
  }
40950
+ }).catch((error93) => {
40951
+ if (!onCacheError)
40952
+ throw error93;
40953
+ onCacheError();
40895
40954
  });
40896
40955
  return await runAttachedSession({
40897
40956
  dependencies,
40898
40957
  operations,
40899
40958
  sessionId,
40900
40959
  repositories,
40901
- activeMessageId: cache?.activeMessageId,
40960
+ activeMessageId,
40902
40961
  start: { mode: "cold-resume", detail },
40903
40962
  noTui,
40904
40963
  json: json3,
40905
- emitCreated: false
40964
+ emitCreated: false,
40965
+ onCacheError
40906
40966
  });
40907
40967
  }
40908
40968
  async function runAttachedSession({
@@ -40914,7 +40974,8 @@ async function runAttachedSession({
40914
40974
  start,
40915
40975
  noTui,
40916
40976
  json: json3,
40917
- emitCreated
40977
+ emitCreated,
40978
+ onCacheError
40918
40979
  }) {
40919
40980
  if (dependencies.abortSignal?.aborted)
40920
40981
  throw dependencies.abortSignal.reason ?? new Error("interrupted");
@@ -40924,6 +40985,7 @@ async function runAttachedSession({
40924
40985
  repositories,
40925
40986
  ...activeMessageId ? { activeMessageId } : {},
40926
40987
  environment: dependencies.environment,
40988
+ onCacheError,
40927
40989
  getSession: async ({ sessionId: id }) => await operations.getSession({ client: operations.client, sessionId: id }),
40928
40990
  listSessionEvents: async ({ sessionId: id, limit, after, signal }) => await operations.listSessionEvents({ client: operations.client, sessionId: id, limit, after, signal }),
40929
40991
  openEventStream: ({ sessionId: id, lastRetainedEventId, signal, onSynchronized }) => operations.streamSessionEvents({ client: operations.client, sessionId: id, lastRetainedEventId, signal, onSynchronized })
@@ -41306,10 +41368,15 @@ Options:
41306
41368
  `;
41307
41369
  }
41308
41370
  if (topic === "session") {
41309
- return `Usage: remy --session <session-id> [options]
41371
+ return `Usage: remy --session <session-id> [options] [prompt]
41310
41372
 
41311
41373
  Attach to an existing remote session. An interactive terminal opens the session view; --no-tui streams output instead.
41312
41374
 
41375
+ Arguments:
41376
+ prompt Submit a follow-up as Steer before attaching
41377
+
41378
+ With a prompt, JSON output waits for that message's turn result. Use -- before a prompt beginning with --.
41379
+
41313
41380
  Options:
41314
41381
  --no-tui Do not open the interactive terminal view
41315
41382
  --json Write session updates as JSON
@@ -41341,8 +41408,8 @@ Usage:
41341
41408
  Commands:
41342
41409
  remy Open interactive dashboard
41343
41410
  remy dashboard Open interactive dashboard
41344
- remy --session <session-id> [--no-tui] [--json]
41345
- Attach to a session
41411
+ remy --session <session-id> [--no-tui] [--json] [prompt]
41412
+ Attach to a session, optionally submitting a follow-up
41346
41413
  remy new [--repository <owner/name> ... --installation <id> --model <name> --reasoning-effort <low|medium|high|xhigh> --attach <path> ... --no-tui --json] <prompt>
41347
41414
  Create a session
41348
41415
  remy login [--env production|staging] [--api-url <url> --auth-api-url <url> --requester-application-id <uuid> --target-application-id <uuid>]
@@ -41374,14 +41441,32 @@ function parseSessionAttach(argv) {
41374
41441
  const sessionId = argv[0];
41375
41442
  if (!sessionId || sessionId.startsWith("--"))
41376
41443
  throw new Error("--session requires a session ID.");
41377
- const flags = parseFlags(argv.slice(1));
41378
- if (typeof flags["no-tui"] === "string" || typeof flags.json === "string")
41379
- throw new Error("--no-tui and --json do not accept values.");
41380
- for (const flag of Object.keys(flags)) {
41381
- if (flag !== "no-tui" && flag !== "json")
41382
- throw new Error(`Unknown --session flag --${flag}.`);
41444
+ let noTui = false;
41445
+ let json3 = false;
41446
+ let prompt;
41447
+ let positionalOnly = false;
41448
+ for (const token of argv.slice(1)) {
41449
+ if (!positionalOnly && token === "--") {
41450
+ positionalOnly = true;
41451
+ continue;
41452
+ }
41453
+ if (!positionalOnly && token === "--no-tui") {
41454
+ noTui = true;
41455
+ continue;
41456
+ }
41457
+ if (!positionalOnly && token === "--json") {
41458
+ json3 = true;
41459
+ continue;
41460
+ }
41461
+ if (!positionalOnly && token.startsWith("--"))
41462
+ throw new Error(`Unknown --session flag ${token}.`);
41463
+ if (prompt !== undefined)
41464
+ throw new Error("--session accepts one prompt. Quote the full prompt as a single argument.");
41465
+ if (!token.trim())
41466
+ throw new Error("A follow-up prompt must not be empty.");
41467
+ prompt = token;
41383
41468
  }
41384
- return { name: "session", sessionId, noTui: flags["no-tui"] === true, json: flags.json === true };
41469
+ return { name: "session", sessionId, noTui, json: json3, ...prompt === undefined ? {} : { prompt } };
41385
41470
  }
41386
41471
  function parseNewCommand(argv) {
41387
41472
  const repositories = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/remy-cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "description": "Remy, the Coding Agent terminal client.",
5
5
  "type": "module",
6
6
  "bin": {