cueloop 0.1.0-alpha.90 → 0.1.0-alpha.92

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cueloop",
3
- "version": "0.1.0-alpha.90",
3
+ "version": "0.1.0-alpha.92",
4
4
  "description": "Terminal review surface for coding agents: annotate plans and diffs, return a message, send structured feedback back",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -30,11 +30,11 @@
30
30
  "build:binary": "bun build --compile --minify --external cpu-features ./src/main.ts --outfile dist/cueloop"
31
31
  },
32
32
  "dependencies": {
33
- "@cueloop/adapters": "0.1.0-alpha.90",
34
- "@cueloop/client": "0.1.0-alpha.90",
35
- "@cueloop/daemon": "0.1.0-alpha.90",
36
- "@cueloop/extension-api": "0.1.0-alpha.90",
37
- "@cueloop/schema": "0.1.0-alpha.90",
33
+ "@cueloop/adapters": "0.1.0-alpha.92",
34
+ "@cueloop/client": "0.1.0-alpha.92",
35
+ "@cueloop/daemon": "0.1.0-alpha.92",
36
+ "@cueloop/extension-api": "0.1.0-alpha.92",
37
+ "@cueloop/schema": "0.1.0-alpha.92",
38
38
  "@modelcontextprotocol/server": "2.0.0",
39
39
  "@valibot/to-json-schema": "1.4.0",
40
40
  "valibot": "^1.5.0"
package/src/args.ts CHANGED
@@ -7,7 +7,7 @@ export interface ParsedArgs {
7
7
  flags: Record<string, string | boolean>;
8
8
  }
9
9
 
10
- export function parseArgs(argv: string[]): ParsedArgs {
10
+ export function parseArgs(argv: string[], booleanFlags: readonly string[] = []): ParsedArgs {
11
11
  const positional: string[] = [];
12
12
  const flags: Record<string, string | boolean> = {};
13
13
 
@@ -26,7 +26,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
26
26
  const key = argument.slice(2);
27
27
  const next = argv[i + 1];
28
28
 
29
- if (next !== undefined && !next.startsWith("--")) {
29
+ if (!booleanFlags.includes(key) && next !== undefined && !next.startsWith("--")) {
30
30
  flags[key] = next;
31
31
  i++;
32
32
  } else {
package/src/main.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  import { sessionCommand } from "./thread-commands";
24
24
  import { CLI_VERSION } from "./version";
25
25
  import { DaemonClient } from "@cueloop/daemon/client";
26
- import type { Thread } from "@cueloop/schema";
26
+ import type { Thread, ThreadSurfaceOpenStatus } from "@cueloop/schema";
27
27
  import { openReview } from "@cueloop/daemon/thread-review";
28
28
 
29
29
  const argv = process.argv.slice(2);
@@ -107,6 +107,7 @@ const commandHandlers: CommandHandlers = {
107
107
  stop: async () => (await import("./daemon-control")).stopCommand(),
108
108
  restart: async () => (await import("./daemon-control")).restartCommand(),
109
109
  plan: (rest) => planCommand(rest),
110
+ pair: (rest) => pairCommand(rest),
110
111
  reply: (rest) => replyCommand(rest),
111
112
  diff: (rest) => diffCommand(rest),
112
113
  prototype: (rest) => prototypeCommand(rest),
@@ -278,6 +279,43 @@ async function diffCommand(argv: string[]): Promise<number> {
278
279
  return runTui(workbench.id, "review");
279
280
  }
280
281
 
282
+ async function pairCommand(argv: string[]): Promise<number> {
283
+ const startedAt = new Date().toISOString();
284
+ const parsed = parseArgs(argv, ["open-tab", "no-tui"]);
285
+ const client = await DaemonClient.connect({ autostart: true });
286
+ let thread: Thread;
287
+ let openStatus: ThreadSurfaceOpenStatus | undefined;
288
+
289
+ try {
290
+ const sessionId = parsed.positional[0];
291
+
292
+ if (sessionId !== undefined) {
293
+ thread = await client.sessionGet(sessionId);
294
+ if (thread.artifact.meta.workbench !== true || thread.status !== "pending") {
295
+ throw new Error(`Thread ${sessionId} is not an open workbench`);
296
+ }
297
+ } else {
298
+ thread = await client.sessionWorkbench(process.cwd());
299
+ }
300
+ if (parsed.flags["open-tab"] === true) {
301
+ const { openHerdrThreadSurface } = await import("@cueloop/daemon/herdr-thread-surface");
302
+
303
+ openStatus = await openHerdrThreadSurface(thread, client, process.env, "tab", "pair");
304
+ }
305
+ } finally {
306
+ client.close();
307
+ }
308
+ if (parsed.flags["no-tui"] === true || parsed.flags["open-tab"] === true) {
309
+ console.log(
310
+ JSON.stringify({ threadId: thread.id, startedAt, ...(openStatus && { openStatus }) }),
311
+ );
312
+
313
+ return 0;
314
+ }
315
+
316
+ return runTui(thread.id, "pair");
317
+ }
318
+
281
319
  /**
282
320
  * `cueloop review` disambiguates create from open by intent:
283
321
  * - bare `cueloop review`, `--open`/`--latest`, or a `ses_*` selector opens a
@@ -310,10 +348,16 @@ async function reviewEntry(argv: string[]): Promise<number> {
310
348
  * omitted) restores the remembered one. Resolve the factory here so the heavy client
311
349
  * module stays lazily imported for non-TUI commands.
312
350
  */
313
- async function runTui(sessionId?: string, layout?: "review" | "plan"): Promise<number> {
314
- const { runClient, reviewLayout, planLayout } = await import("@cueloop/client");
351
+ async function runTui(sessionId?: string, layout?: "review" | "plan" | "pair"): Promise<number> {
352
+ const { runClient, pairLayout, reviewLayout, planLayout } = await import("@cueloop/client");
315
353
  const resolved =
316
- layout === "review" ? reviewLayout() : layout === "plan" ? planLayout() : undefined;
354
+ layout === "review"
355
+ ? reviewLayout()
356
+ : layout === "plan"
357
+ ? planLayout()
358
+ : layout === "pair"
359
+ ? pairLayout()
360
+ : undefined;
317
361
 
318
362
  return runClient({ sessionId, layout: resolved });
319
363
  }
@@ -331,6 +375,7 @@ function printHelp(): void {
331
375
  " cueloop reply [id|title] open the latest pending reply review (the agent's previous message)",
332
376
  " cueloop diff [id|title] review your working tree (untracked files included);",
333
377
  " with a clean tree, open the latest pending diff review",
378
+ " cueloop pair [thread-id] open a live workbench; --open-tab opens Herdr; --no-tui prints its Thread ID",
334
379
  " cueloop review <pr> review a pull request (--no-tui prints the session)",
335
380
  " cueloop prototype <file.md> review a component design doc (or open the latest by id/title)",
336
381
  "",
@@ -12,6 +12,7 @@ import {
12
12
  newAnnotationId,
13
13
  type Anchor,
14
14
  type Annotation,
15
+ type AnnotationTarget,
15
16
  } from "@cueloop/schema";
16
17
  import * as v from "valibot";
17
18
  import { DaemonClient } from "@cueloop/daemon/client";
@@ -127,7 +128,7 @@ async function annotateAnchor(
127
128
  flags: Record<string, string | boolean>,
128
129
  client: DaemonClient,
129
130
  id: string,
130
- ): Promise<{ anchor: Anchor; replyTo?: string }> {
131
+ ): Promise<{ anchor: Anchor; replyTo?: string; target?: AnnotationTarget }> {
131
132
  const replyTo = stringFlag(flags, "reply-to");
132
133
 
133
134
  if (replyTo !== undefined) {
@@ -137,7 +138,9 @@ async function annotateAnchor(
137
138
  if (!root) throw new Error(`no comment ${replyTo} to reply to`);
138
139
 
139
140
  // a reply to a reply still hangs off the discussion's root comment
140
- return { anchor: root.anchor, replyTo: root.replyTo ?? root.id };
141
+ return root.target === undefined
142
+ ? { anchor: root.anchor, replyTo: root.replyTo ?? root.id }
143
+ : { anchor: root.anchor, replyTo: root.replyTo ?? root.id, target: root.target };
141
144
  }
142
145
  const selector = stringFlag(flags, "selector");
143
146
  const quote =
@@ -163,8 +166,8 @@ async function sessionAnnotateCommand({
163
166
  const id = required(positional[1], "session id");
164
167
  const author = stringFlag(flags, "author");
165
168
  const body = await annotateBody(flags, client, id);
166
- const { anchor, replyTo } = await annotateAnchor(flags, client, id);
167
- const annotation: Omit<Annotation, "createdAt"> = {
169
+ const { anchor, replyTo, target } = await annotateAnchor(flags, client, id);
170
+ const base: Omit<Annotation, "createdAt"> = {
168
171
  id: stringFlag(flags, "annotation-id") ?? newAnnotationId(),
169
172
  kind: stringFlag(flags, "kind") ?? "comment",
170
173
  anchor,
@@ -172,7 +175,9 @@ async function sessionAnnotateCommand({
172
175
  author,
173
176
  };
174
177
 
175
- if (replyTo !== undefined) annotation.replyTo = replyTo;
178
+ if (replyTo !== undefined) base.replyTo = replyTo;
179
+ const annotation: Omit<Annotation, "createdAt"> =
180
+ target === undefined ? base : { ...base, target };
176
181
  out(await client.sessionAnnotate(id, annotation, stringFlag(flags, "author-name")));
177
182
 
178
183
  return 0;
@@ -340,6 +345,7 @@ async function sessionEventsCommand({
340
345
  }: SessionContext): Promise<number> {
341
346
  const id = required(positional[1], "session id");
342
347
  const once = flags.once === true;
348
+ const ready = flags.ready === true;
343
349
  const stream = new Promise<number>((resolve) => {
344
350
  client.onEvent((event) => {
345
351
  if (event.sessionId !== id) return;
@@ -347,11 +353,24 @@ async function sessionEventsCommand({
347
353
  if (once) resolve(0);
348
354
  });
349
355
  });
356
+ let finished = false;
357
+ const disconnect = new Promise<number>((resolve) => {
358
+ client.onDisconnect(() => {
359
+ if (finished) return;
360
+ console.error("daemon connection closed - restart cueloop session events to resume");
361
+ resolve(1);
362
+ });
363
+ });
350
364
 
351
365
  await client.subscribe();
352
366
  await client.sessionGet(id);
367
+ if (ready) console.log(JSON.stringify({ event: "events.ready", sessionId: id }));
368
+
369
+ const code = await Promise.race([stream, disconnect]);
370
+
371
+ finished = true;
353
372
 
354
- return stream;
373
+ return code;
355
374
  }
356
375
 
357
376
  async function sessionSendMessageCommand({