@meistrari/remy-cli 1.17.0 → 1.17.2

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 +4 -2
  2. package/dist/remy.js +99 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,9 +36,9 @@ The dashboard is the starting point for all interactive work. It refreshes the v
36
36
 
37
37
  ### Start work
38
38
 
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.
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. Remy loads the session number when opening the session and refreshes details when its generated-title event arrives or the stream reconnects. The header and composer update without reopening the session or losing your draft. If a title refresh fails, Remy keeps the last details and continues loading or streaming the conversation.
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
 
@@ -46,6 +46,8 @@ Every terminal follow-up is appended as an explicit **Steer**, so it targets the
46
46
 
47
47
  Use Up/Down to select a session in the dashboard, Left/Right to load the adjacent page, and `Enter` to open it. Remy loads the complete retained conversation before showing the session view, then streams new activity as it arrives. Press `s` to search the loaded page by session ID, title, or number; `Enter` keeps the search active and `Esc` clears it. Press `f` to filter sessions by lifecycle status or `a` to filter by author. Both pickers use checkboxes: Up/Down or `j`/`k` moves, `Space` toggles, Right/`l` checks, Left/`h` clears, `G` and `gg` jump to the last and first option, and `Enter` applies every checked value. An empty picker means all values. Press `s` inside either picker to fuzzy-search its options; `Enter` keeps the matching list and `Esc` cancels the search. Press `Esc` outside search to discard staged checkbox changes. Filters apply to the complete remote result set and reset pagination to its first page. After the dashboard list or repository-review list has focus, Vim keys work too: `j`/`k` move, `h`/`l` go to the previous/next dashboard page (or clear/mark a repository), `G` goes to the last row, and `gg` goes to the first row. Press `Shift+L` in the dashboard to start a logout confirmation. In any message composer, plain `Enter` and `Shift+Enter` submit; `Option+Enter` on macOS (`Alt+Enter` elsewhere) inserts a newline. `Ctrl+J` also inserts a newline for terminals that do not report the Option/Alt modifier. The conversation header keeps the numbered session title on its first line and repository, pull request, work state, elapsed time, and connection state on its second line. The header also keeps the last provider-reported context observation across reconnects, such as `Context 50% left · 62,000 used`; when the provider does not report a context window, it shows only used tokens. This is an observation, not a live occupancy guarantee or billing total.
48
48
 
49
+ If a connected session loses its activity stream, Remy retries for up to two minutes and resumes from the last received event. After the live stream stays connected for at least 30 seconds, the next disconnect starts a fresh retry window. Connections that drop sooner share the existing window and backoff. While a submitted message is still awaiting its outcome, retries continue beyond that window.
50
+
49
51
  The **Live reasoning preview** shows the newest main-agent summary as it arrives. It is a short, best-effort preview—not private chain-of-thought. Long previews show **Preview shortened.** Opening or reconnecting partway through a summary may show no preview; the final summary still appears in activity history. Disconnecting or finishing the item clears the live preview.
50
52
 
51
53
  Subagent work appears as named **Subagent · <name>** activity rather than messages from Remy. Resolved descendants stay grouped under their actual parent as a compact `Parent › Child` path; missing or contradictory ancestry remains explicitly **Unattributed subagent** instead of being assigned to the active main turn. Press `Ctrl+O` to read retained child messages, complete tool results, reasoning summaries, and child IDs. Child input does not enter your prompt history, child output does not replace Remy's main live preview, and parent or main completion does not end a still-running descendant. Child-specific Stop controls are not available.
package/dist/remy.js CHANGED
@@ -33072,8 +33072,8 @@ async function appendSessionMessage({
33072
33072
  });
33073
33073
  return parseAppendSessionMessageResponse(await parseJson2(response, "Session message append response was not valid JSON."));
33074
33074
  }
33075
- async function getSession({ client, sessionId }) {
33076
- const response = await client.request(`/v1/sessions/${encodeURIComponent(sessionId)}`);
33075
+ async function getSession({ client, sessionId, signal }) {
33076
+ const response = await client.request(`/v1/sessions/${encodeURIComponent(sessionId)}`, { signal });
33077
33077
  const parsed = sessionDetailResponseSchema.safeParse(await parseJson2(response, "Session detail response was not valid JSON."));
33078
33078
  if (!parsed.success)
33079
33079
  throw new CodingAgentProtocolError("Session detail response did not match the public API contract.", { cause: parsed.error });
@@ -34203,6 +34203,10 @@ var publicMessageEndedAgentEventSchema = wrappedMessageEndedAgentEventSchema.ext
34203
34203
  content: exports_external2.array(publicMessageContentSegmentSchema)
34204
34204
  }).strict()
34205
34205
  }).strict();
34206
+ var sessionTitleGeneratedEventSchema = exports_external2.strictObject({
34207
+ type: exports_external2.literal("session.title.generated"),
34208
+ payload: exports_external2.strictObject({})
34209
+ });
34206
34210
 
34207
34211
  class SessionProjectionProtocolError extends Error {
34208
34212
  constructor(message) {
@@ -34311,6 +34315,12 @@ function projectRetainedEvent({
34311
34315
  retainedEventId,
34312
34316
  artifact
34313
34317
  }) {
34318
+ if (event.type === "session.title.generated") {
34319
+ const parsed = sessionTitleGeneratedEventSchema.safeParse(event);
34320
+ if (!parsed.success)
34321
+ throw new SessionProjectionProtocolError("Invalid session.title.generated event.");
34322
+ return state;
34323
+ }
34314
34324
  const messageCreated = sessionMessageCreatedEventSchema.safeParse(event);
34315
34325
  const turnAssociated = sessionMessageTurnAssociatedEventSchema.safeParse(event);
34316
34326
  const messageDispatch = sessionMessageDispatchEventSchema.safeParse(event);
@@ -35284,6 +35294,7 @@ var remoteSessionReconnectPolicy = {
35284
35294
  retryBudgetMs: 120000
35285
35295
  };
35286
35296
  var sessionHistoryPageSize = 100;
35297
+ var sessionStreamRecoveryMs = 30000;
35287
35298
  var sessionCacheSchema = exports_external2.strictObject({
35288
35299
  version: exports_external2.literal(1),
35289
35300
  sessionId: exports_external2.string().min(1),
@@ -35312,6 +35323,7 @@ function createRemoteSessionController(dependencies) {
35312
35323
  let ready = new Promise((resolve) => {
35313
35324
  resolveReady = resolve;
35314
35325
  });
35326
+ let detailRevision = 0;
35315
35327
  let stopped = false;
35316
35328
  let state;
35317
35329
  let cachedLastRetainedEventId;
@@ -35339,7 +35351,16 @@ function createRemoteSessionController(dependencies) {
35339
35351
  }
35340
35352
  resolveReady();
35341
35353
  publishState();
35342
- await streamWithControllerReconnects();
35354
+ try {
35355
+ await streamWithControllerReconnects();
35356
+ } catch (error93) {
35357
+ if (stopped)
35358
+ return;
35359
+ stop();
35360
+ throw error93;
35361
+ }
35362
+ if (getState().aggregateStatus !== "open")
35363
+ stop();
35343
35364
  }
35344
35365
  function stop() {
35345
35366
  if (stopped)
@@ -35380,7 +35401,23 @@ function createRemoteSessionController(dependencies) {
35380
35401
  await writeCache();
35381
35402
  publishState();
35382
35403
  }
35404
+ async function refreshDetail() {
35405
+ const revision = detailRevision;
35406
+ const timeout = new AbortController;
35407
+ const timeoutTimer = setTimeout(() => timeout.abort(new Error("Session detail refresh timed out.")), 1e4);
35408
+ try {
35409
+ const detail = await dependencies.getSession({
35410
+ sessionId: dependencies.sessionId,
35411
+ signal: AbortSignal.any([abortController.signal, timeout.signal])
35412
+ });
35413
+ if (!stopped && revision === detailRevision)
35414
+ updateDetail(detail);
35415
+ } finally {
35416
+ clearTimeout(timeoutTimer);
35417
+ }
35418
+ }
35383
35419
  function updateDetail(detail) {
35420
+ detailRevision += 1;
35384
35421
  if (detail.status !== "open")
35385
35422
  invalidateReasoningAttempt({ publish: false });
35386
35423
  state = updateSessionDetail({ state: getState(), detail });
@@ -35417,11 +35454,12 @@ function createRemoteSessionController(dependencies) {
35417
35454
  }
35418
35455
  async function streamWithControllerReconnects() {
35419
35456
  let backoffMs = reconnectPolicy.initialBackoffMs;
35420
- const deadlineMs = monotonicNowMs() + reconnectPolicy.retryBudgetMs;
35457
+ let deadlineMs = monotonicNowMs() + reconnectPolicy.retryBudgetMs;
35421
35458
  while (true) {
35422
35459
  if (stopped)
35423
35460
  return;
35424
35461
  const generation = beginReasoningAttempt();
35462
+ let synchronizedAtMs;
35425
35463
  state = markSessionConnected(getState());
35426
35464
  publishState();
35427
35465
  try {
@@ -35429,7 +35467,12 @@ function createRemoteSessionController(dependencies) {
35429
35467
  sessionId: dependencies.sessionId,
35430
35468
  lastRetainedEventId: cachedLastRetainedEventId,
35431
35469
  signal: abortController.signal,
35432
- onSynchronized: () => synchronizeReasoningAttempt(generation)
35470
+ onSynchronized: () => {
35471
+ if (generation !== reasoningConnectionGeneration || stopped)
35472
+ return;
35473
+ synchronizedAtMs ??= monotonicNowMs();
35474
+ synchronizeReasoningAttempt(generation);
35475
+ }
35433
35476
  });
35434
35477
  for await (const frame of stream) {
35435
35478
  if (stopped)
@@ -35443,12 +35486,13 @@ function createRemoteSessionController(dependencies) {
35443
35486
  return;
35444
35487
  if (error93 instanceof Error && error93.name === "SessionProjectionProtocolError")
35445
35488
  throw error93;
35446
- const refreshGeneration = reasoningConnectionGeneration;
35447
- const detail = await dependencies.getSession({ sessionId: dependencies.sessionId });
35448
- if (stopped || refreshGeneration !== reasoningConnectionGeneration)
35449
- return;
35450
- updateDetail(detail);
35451
35489
  }
35490
+ if (stopped)
35491
+ return;
35492
+ const streamEndedAtMs = monotonicNowMs();
35493
+ try {
35494
+ await refreshDetail();
35495
+ } catch {}
35452
35496
  if (stopped)
35453
35497
  return;
35454
35498
  state = markSessionDisconnected(getState());
@@ -35456,6 +35500,10 @@ function createRemoteSessionController(dependencies) {
35456
35500
  if (getState().aggregateStatus !== "open")
35457
35501
  return;
35458
35502
  const nowMs = monotonicNowMs();
35503
+ if (synchronizedAtMs !== undefined && streamEndedAtMs - synchronizedAtMs >= sessionStreamRecoveryMs) {
35504
+ deadlineMs = streamEndedAtMs + reconnectPolicy.retryBudgetMs;
35505
+ backoffMs = reconnectPolicy.initialBackoffMs;
35506
+ }
35459
35507
  const activeMessageId = getState().activeMessageId;
35460
35508
  const activeTurnNeedsObservation = activeMessageId ? sessionTurnOutcome({ state: getState(), sessionMessageId: activeMessageId }).status !== "ended" : false;
35461
35509
  if (nowMs >= deadlineMs && !activeTurnNeedsObservation)
@@ -35481,6 +35529,11 @@ function createRemoteSessionController(dependencies) {
35481
35529
  await writeCache();
35482
35530
  cachedLastRetainedEventId = projectedState.lastRetainedEventId;
35483
35531
  }
35532
+ if (frame.kind === "retained" && frame.data.event.type === "session.title.generated" && !previousState.seenRetainedEventIds[frame.id]) {
35533
+ try {
35534
+ await refreshDetail();
35535
+ } catch {}
35536
+ }
35484
35537
  resolveCompletedWaiters();
35485
35538
  if (stopped)
35486
35539
  return;
@@ -35902,7 +35955,7 @@ function createComposer({
35902
35955
  };
35903
35956
  function render() {
35904
35957
  const dividerWidth = renderer.width - screenPaddingX * 2;
35905
- dividerTop.content = renderComposerDivider({ width: dividerWidth, tag, leadLabel: topDividerLabel?.() });
35958
+ dividerTop.content = renderComposerDivider({ width: dividerWidth, tag: typeof tag === "function" ? tag() : tag, leadLabel: topDividerLabel?.() });
35906
35959
  dividerBottom.content = renderComposerDivider({ width: dividerWidth });
35907
35960
  }
35908
35961
  return {
@@ -38345,7 +38398,7 @@ async function createSessionTui({
38345
38398
  resolveAction = resolve2;
38346
38399
  rejectAction = reject;
38347
38400
  });
38348
- const composerTag = latestState.sessionNumber !== undefined ? `Session #${latestState.sessionNumber}` : latestState.sessionId;
38401
+ const composerTag = () => latestState.sessionNumber !== undefined ? `Session #${latestState.sessionNumber}` : latestState.sessionId;
38349
38402
  let composer;
38350
38403
  const composerLayout = createComposer({
38351
38404
  renderer,
@@ -39362,13 +39415,35 @@ var directoryArchiveMediaType = "application/gzip";
39362
39415
  var directoryArchiveSuffix = ".tar.gz";
39363
39416
  var maxPortableFilenameBytes = 255;
39364
39417
  var attachmentSizeLimitLabel = `${fileUploadMaxBytes / (1024 * 1024)} MiB`;
39418
+ var clipboardImageScript = `
39419
+ ObjC.import('AppKit');
39420
+ const pasteboard = $.NSPasteboard.generalPasteboard;
39421
+ let png = pasteboard.dataForType($.NSPasteboardTypePNG);
39422
+ if (!png || png.isNil()) {
39423
+ const tiff = pasteboard.dataForType($.NSPasteboardTypeTIFF);
39424
+ if (!tiff || tiff.isNil()) throw new Error('Clipboard does not contain an image.');
39425
+ const bitmap = $.NSBitmapImageRep.imageRepWithData(tiff);
39426
+ if (!bitmap || bitmap.isNil()) throw new Error('Clipboard image could not be decoded.');
39427
+ png = bitmap.representationUsingTypeProperties($.NSPNGFileType, $.NSDictionary.dictionary);
39428
+ }
39429
+ if (!png || png.isNil()) throw new Error('Clipboard image could not be converted to PNG.');
39430
+ if (png.length > ${fileUploadMaxBytes}) throw new Error('Clipboard image is larger than ${attachmentSizeLimitLabel}.');
39431
+ ObjC.unwrap(png.base64EncodedStringWithOptions(0));
39432
+ `;
39365
39433
  async function readClipboardImage() {
39366
39434
  if (process.platform !== "darwin")
39367
39435
  throw new Error("Clipboard image paste is supported on macOS only.");
39368
- const clipboard = Bun.spawn(["pbpaste", "-Prefer", "png"], { stdout: "pipe", stderr: "ignore" });
39369
- const bytes = new Uint8Array(await new Response(clipboard.stdout).arrayBuffer());
39370
- if (await clipboard.exited !== 0 || !isPng(bytes))
39371
- throw new Error("Clipboard does not contain an image.");
39436
+ const clipboard = Bun.spawn(["/usr/bin/osascript", "-l", "JavaScript", "-e", clipboardImageScript], { stdout: "pipe", stderr: "pipe" });
39437
+ const [encoded, error93, exitCode] = await Promise.all([
39438
+ new Response(clipboard.stdout).text(),
39439
+ new Response(clipboard.stderr).text(),
39440
+ clipboard.exited
39441
+ ]);
39442
+ if (exitCode !== 0)
39443
+ throw new Error(`Could not read clipboard image: ${error93.trim() || "macOS clipboard reader failed."}`);
39444
+ const bytes = new Uint8Array(Buffer2.from(encoded.trim(), "base64"));
39445
+ if (!isPng(bytes))
39446
+ throw new Error("Clipboard image could not be decoded as PNG.");
39372
39447
  return { bytes, mediaType: "image/png" };
39373
39448
  }
39374
39449
  async function writePastedImage({
@@ -39558,8 +39633,11 @@ function promptFilePaths({ text, cwd, selectedPaths }) {
39558
39633
  const paths = new Map;
39559
39634
  for (const selectedPath of selectedPaths)
39560
39635
  paths.set(isAbsolute2(selectedPath) ? selectedPath : resolve2(cwd, selectedPath), true);
39561
- for (const rawToken of text.split(/\s+/)) {
39562
- const token = rawToken.replace(/^[([{'"`]+/, "").replace(/[),.;:!?\]}"`]+$/, "");
39636
+ const tokens = text.match(/[([{]*@?(?:"[^"]*"|'[^']*'|`[^`]*`)|(?:\\.|[^\s\\])+/g) ?? [];
39637
+ for (const quotedToken of tokens) {
39638
+ const wrappedToken = quotedToken.replace(/^[([{]+/, "").replace(/[),.;:!?\]}]+$/, "");
39639
+ const quotedPath = /^(@?)(["'`])([\s\S]*)\2$/.exec(wrappedToken);
39640
+ const token = quotedPath ? `${quotedPath[1]}${quotedPath[3]}` : wrappedToken.replace(/\\(.)/g, "$1");
39563
39641
  const pathText = token.startsWith("@") ? token.slice(1) : token;
39564
39642
  if (!pathText)
39565
39643
  continue;
@@ -39608,7 +39686,7 @@ var compactMarkRows = 9;
39608
39686
  var compactMinWidth = 48;
39609
39687
  var compactMinHeight = 20;
39610
39688
  var markBrightnessGain = 4.2;
39611
- var remyCliVersion = "1.17.0";
39689
+ var remyCliVersion = "1.17.2";
39612
39690
  async function showRemySplash({
39613
39691
  createRenderer = createRemyRenderer,
39614
39692
  durationMs = splashDurationMs,
@@ -40802,14 +40880,7 @@ async function attachCreatedSession({
40802
40880
  activeMessageId: initialMessage.id,
40803
40881
  start: {
40804
40882
  mode: "live",
40805
- detail: {
40806
- id: created.session.id,
40807
- status: created.session.status,
40808
- repositories: sessionRepositories.map((repository) => ({ id: repository.id, full_name: repository.fullName })),
40809
- pull_requests: [],
40810
- connections: created.connections,
40811
- artifacts: []
40812
- }
40883
+ detail: await operations.getSession({ client: operations.client, sessionId: created.session.id, signal: dependencies.abortSignal })
40813
40884
  },
40814
40885
  noTui,
40815
40886
  json: json3,
@@ -40961,7 +41032,7 @@ async function runAttachedSession({
40961
41032
  ...activeMessageId ? { activeMessageId } : {},
40962
41033
  environment: dependencies.environment,
40963
41034
  onCacheError,
40964
- getSession: async ({ sessionId: id }) => await operations.getSession({ client: operations.client, sessionId: id }),
41035
+ getSession: async ({ sessionId: id, signal }) => await operations.getSession({ client: operations.client, sessionId: id, signal }),
40965
41036
  listSessionEvents: async ({ sessionId: id, limit, after, signal }) => await operations.listSessionEvents({ client: operations.client, sessionId: id, limit, after, signal }),
40966
41037
  openEventStream: ({ sessionId: id, lastRetainedEventId, signal, onSynchronized }) => operations.streamSessionEvents({ client: operations.client, sessionId: id, lastRetainedEventId, signal, onSynchronized })
40967
41038
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/remy-cli",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "Remy, the Coding Agent terminal client.",
5
5
  "type": "module",
6
6
  "bin": {