cueloop 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +11 -6
- package/src/args.ts +10 -10
- package/src/main.ts +11 -9
- package/src/pr.ts +6 -5
- package/src/{session-cmds.ts → session-commands.ts} +33 -36
- package/src/{workspace.ts → working-tree.ts} +2 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cueloop",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
".": "./src/main.ts"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
14
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
15
|
-
"@cueloop/client": "0.1.0-alpha.
|
|
13
|
+
"@cueloop/schema": "0.1.0-alpha.20",
|
|
14
|
+
"@cueloop/daemon": "0.1.0-alpha.20",
|
|
15
|
+
"@cueloop/client": "0.1.0-alpha.20"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
@@ -28,5 +28,10 @@
|
|
|
28
28
|
"type": "git",
|
|
29
29
|
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
30
30
|
"directory": "packages/cli"
|
|
31
|
-
}
|
|
32
|
-
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/mmurakaru/cueloop/issues"
|
|
35
|
+
},
|
|
36
|
+
"description": "Terminal review surface for coding agents: annotate plans and diffs, return a verdict, send structured feedback back"
|
|
37
|
+
}
|
package/src/args.ts
CHANGED
|
@@ -9,16 +9,16 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
9
9
|
const positional: string[] = [];
|
|
10
10
|
const flags: Record<string, string | boolean> = {};
|
|
11
11
|
for (let i = 0; i < argv.length; i++) {
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
14
|
-
positional.push(
|
|
12
|
+
const argument = argv[i]!;
|
|
13
|
+
if (!argument.startsWith("--")) {
|
|
14
|
+
positional.push(argument);
|
|
15
15
|
continue;
|
|
16
16
|
}
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
flags[
|
|
17
|
+
const equalsIndex = argument.indexOf("=");
|
|
18
|
+
if (equalsIndex !== -1) {
|
|
19
|
+
flags[argument.slice(2, equalsIndex)] = argument.slice(equalsIndex + 1);
|
|
20
20
|
} else {
|
|
21
|
-
const key =
|
|
21
|
+
const key = argument.slice(2);
|
|
22
22
|
const next = argv[i + 1];
|
|
23
23
|
if (next !== undefined && !next.startsWith("--")) {
|
|
24
24
|
flags[key] = next;
|
|
@@ -31,7 +31,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
31
31
|
return { positional, flags };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function
|
|
35
|
-
const
|
|
36
|
-
return typeof
|
|
34
|
+
export function stringFlag(flags: Record<string, string | boolean>, key: string): string | undefined {
|
|
35
|
+
const value = flags[key];
|
|
36
|
+
return typeof value === "string" ? value : undefined;
|
|
37
37
|
}
|
package/src/main.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* cueloop entry points
|
|
3
|
+
* cueloop entry points: `cueloop` opens the TUI on the inbox,
|
|
4
4
|
* `cueloop diff` reviews the working tree, `cueloop review <pr>` a PR,
|
|
5
5
|
* `cueloop session *` mirrors the daemon API for agents and scripts,
|
|
6
6
|
* `cueloop serve` shares a session over ssh (read-only observers),
|
|
7
7
|
* `cueloop daemon` runs the daemon in the foreground.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { parseArgs,
|
|
11
|
-
import { sessionCommand } from "./session-
|
|
12
|
-
import {
|
|
10
|
+
import { parseArgs, stringFlag } from "./args";
|
|
11
|
+
import { sessionCommand } from "./session-commands";
|
|
12
|
+
import { workingTreeDiff } from "./working-tree";
|
|
13
13
|
import { DaemonClient } from "@cueloop/daemon/client";
|
|
14
|
+
import { openReview, resolveWorkspace } from "@cueloop/daemon/review";
|
|
14
15
|
|
|
15
16
|
const argv = process.argv.slice(2);
|
|
16
17
|
const cmd = argv[0];
|
|
@@ -39,21 +40,22 @@ async function main(): Promise<number> {
|
|
|
39
40
|
return 1;
|
|
40
41
|
}
|
|
41
42
|
const client = await DaemonClient.connect({ autostart: true });
|
|
42
|
-
const
|
|
43
|
+
const review = await openReview(client, {
|
|
43
44
|
type: "diff",
|
|
44
45
|
content: diff,
|
|
45
|
-
|
|
46
|
+
workspace,
|
|
47
|
+
title: `working tree @ ${workspace.branch}`,
|
|
46
48
|
});
|
|
47
49
|
client.close();
|
|
48
|
-
return runTui(
|
|
50
|
+
return runTui(review.id);
|
|
49
51
|
}
|
|
50
52
|
case "serve": {
|
|
51
53
|
const { positional, flags } = parseArgs(argv.slice(1));
|
|
52
|
-
const port =
|
|
54
|
+
const port = stringFlag(flags, "port");
|
|
53
55
|
const { serveClient } = await import("@cueloop/client");
|
|
54
56
|
const handle = await serveClient({
|
|
55
57
|
port: port !== undefined ? Number(port) : undefined,
|
|
56
|
-
host:
|
|
58
|
+
host: stringFlag(flags, "host"),
|
|
57
59
|
sessionId: positional[0],
|
|
58
60
|
});
|
|
59
61
|
console.log(
|
package/src/pr.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PR review entry
|
|
2
|
+
* PR review entry: `cueloop review <pr>` fetches the PR diff through the
|
|
3
3
|
* user's `gh` CLI (auth fully delegated), opens a diff session, and posts the
|
|
4
4
|
* verdict back to the PR as a real review when the session resolves.
|
|
5
5
|
* `cueloop review-post <session-id> <pr>` is the non-interactive post-back
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import type { ReviewSession, VerdictKind } from "@cueloop/schema";
|
|
10
10
|
import { DaemonClient } from "@cueloop/daemon/client";
|
|
11
|
+
import { openReview } from "@cueloop/daemon/review";
|
|
11
12
|
import { parseArgs } from "./args";
|
|
12
|
-
import { resolveWorkspace } from "./workspace";
|
|
13
13
|
|
|
14
14
|
/** The gh binary is injectable so tests can stub it. */
|
|
15
15
|
function ghBin(): string {
|
|
@@ -55,13 +55,14 @@ export async function reviewCommand(argv: string[]): Promise<number> {
|
|
|
55
55
|
console.error(`PR ${pr} has an empty diff - nothing to review`);
|
|
56
56
|
return 1;
|
|
57
57
|
}
|
|
58
|
-
const workspace = await resolveWorkspace();
|
|
59
58
|
const client = await DaemonClient.connect({ autostart: true });
|
|
60
|
-
const
|
|
59
|
+
const review = await openReview(client, {
|
|
61
60
|
type: "diff",
|
|
62
61
|
content: diff.stdout,
|
|
63
|
-
|
|
62
|
+
title: `PR ${pr}`,
|
|
63
|
+
pr,
|
|
64
64
|
});
|
|
65
|
+
const session = review.session;
|
|
65
66
|
client.close();
|
|
66
67
|
|
|
67
68
|
if (flags["no-tui"]) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `cueloop session *` - mirrors the daemon socket API 1:1
|
|
2
|
+
* `cueloop session *` - mirrors the daemon socket API 1:1. One surface,
|
|
3
3
|
* three consumers: agent adapters, the dev loop, and integrations. Output is
|
|
4
4
|
* JSON on stdout; exit code 0 unless the daemon returned an error.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { newAnnotationId, type ArtifactType, type VerdictKind } from "@cueloop/schema";
|
|
8
8
|
import { DaemonClient } from "@cueloop/daemon/client";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { openReview, verdictResponse, type ReviewNote } from "@cueloop/daemon/review";
|
|
10
|
+
import { parseArgs, stringFlag } from "./args";
|
|
11
11
|
|
|
12
12
|
async function readStdin(): Promise<string> {
|
|
13
13
|
return await new Response(Bun.stdin.stream()).text();
|
|
@@ -24,21 +24,22 @@ export async function sessionCommand(argv: string[]): Promise<number> {
|
|
|
24
24
|
try {
|
|
25
25
|
switch (verb) {
|
|
26
26
|
case "create": {
|
|
27
|
-
const
|
|
28
|
-
const contentFile = flagStr(flags, "content-file");
|
|
27
|
+
const contentFile = stringFlag(flags, "content-file");
|
|
29
28
|
const content = contentFile ? await Bun.file(contentFile).text() : await readStdin();
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// per-file agent notes for diff sessions: a JSON array of { path, body }
|
|
30
|
+
const notesFile = stringFlag(flags, "notes-file");
|
|
31
|
+
const notes = notesFile ? (JSON.parse(await Bun.file(notesFile).text()) as ReviewNote[]) : undefined;
|
|
32
|
+
const review = await openReview(client, {
|
|
33
|
+
type: (stringFlag(flags, "type") ?? "plan") as ArtifactType,
|
|
32
34
|
content,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
35
|
+
cwd: stringFlag(flags, "cwd"),
|
|
36
|
+
agent: stringFlag(flags, "agent"),
|
|
37
|
+
agentSessionId: stringFlag(flags, "agent-session-id"),
|
|
38
|
+
planPath: stringFlag(flags, "plan-path"),
|
|
39
|
+
title: stringFlag(flags, "title"),
|
|
40
|
+
notes,
|
|
40
41
|
});
|
|
41
|
-
out(session);
|
|
42
|
+
out(review.session);
|
|
42
43
|
return 0;
|
|
43
44
|
}
|
|
44
45
|
case "get": {
|
|
@@ -46,38 +47,34 @@ export async function sessionCommand(argv: string[]): Promise<number> {
|
|
|
46
47
|
return 0;
|
|
47
48
|
}
|
|
48
49
|
case "list": {
|
|
49
|
-
const status =
|
|
50
|
+
const status = stringFlag(flags, "status") as "pending" | "resolved" | undefined;
|
|
50
51
|
out(await client.sessionList(status ? { status } : undefined));
|
|
51
52
|
return 0;
|
|
52
53
|
}
|
|
53
54
|
case "wait": {
|
|
54
55
|
const id = required(positional[1], "session id");
|
|
55
|
-
const timeoutMs = Number(
|
|
56
|
+
const timeoutMs = Number(stringFlag(flags, "timeout-ms") ?? "60000");
|
|
56
57
|
const session = await client.sessionWait(id, timeoutMs);
|
|
57
58
|
if (session === null) {
|
|
58
59
|
out({ status: "pending" });
|
|
59
60
|
return 0;
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
allow: session.verdict!.kind === "approve",
|
|
64
|
-
verdict: session.verdict!.kind,
|
|
65
|
-
feedback: session.verdict!.feedback,
|
|
66
|
-
});
|
|
62
|
+
const { allow, feedback } = verdictResponse(session);
|
|
63
|
+
out({ status: "resolved", allow, verdict: session.verdict!.kind, feedback });
|
|
67
64
|
return 0;
|
|
68
65
|
}
|
|
69
66
|
case "annotate": {
|
|
70
67
|
const id = required(positional[1], "session id");
|
|
71
|
-
const body =
|
|
72
|
-
const quote = required(
|
|
68
|
+
const body = stringFlag(flags, "body") ?? "";
|
|
69
|
+
const quote = required(stringFlag(flags, "quote"), "--quote");
|
|
73
70
|
out(
|
|
74
71
|
await client.sessionAnnotate(id, {
|
|
75
|
-
id:
|
|
76
|
-
kind:
|
|
72
|
+
id: stringFlag(flags, "annotation-id") ?? newAnnotationId(),
|
|
73
|
+
kind: stringFlag(flags, "kind") ?? "comment",
|
|
77
74
|
anchor: {
|
|
78
75
|
quote,
|
|
79
|
-
prefix:
|
|
80
|
-
suffix:
|
|
76
|
+
prefix: stringFlag(flags, "prefix") ?? "",
|
|
77
|
+
suffix: stringFlag(flags, "suffix") ?? "",
|
|
81
78
|
},
|
|
82
79
|
body,
|
|
83
80
|
}),
|
|
@@ -86,13 +83,13 @@ export async function sessionCommand(argv: string[]): Promise<number> {
|
|
|
86
83
|
}
|
|
87
84
|
case "resolve": {
|
|
88
85
|
const id = required(positional[1], "session id");
|
|
89
|
-
const kind = required(
|
|
90
|
-
out(await client.sessionResolve(id, kind,
|
|
86
|
+
const kind = required(stringFlag(flags, "verdict"), "--verdict") as VerdictKind;
|
|
87
|
+
out(await client.sessionResolve(id, kind, stringFlag(flags, "summary") ?? ""));
|
|
91
88
|
return 0;
|
|
92
89
|
}
|
|
93
90
|
case "submit-revision": {
|
|
94
91
|
const id = required(positional[1], "session id");
|
|
95
|
-
const contentFile =
|
|
92
|
+
const contentFile = stringFlag(flags, "content-file");
|
|
96
93
|
const content = contentFile ? await Bun.file(contentFile).text() : await readStdin();
|
|
97
94
|
out(await client.sessionSubmitRevision(id, content));
|
|
98
95
|
return 0;
|
|
@@ -108,7 +105,7 @@ export async function sessionCommand(argv: string[]): Promise<number> {
|
|
|
108
105
|
}
|
|
109
106
|
}
|
|
110
107
|
|
|
111
|
-
function required<T>(
|
|
112
|
-
if (
|
|
113
|
-
return
|
|
108
|
+
function required<T>(value: T | undefined, description: string): T {
|
|
109
|
+
if (value === undefined) throw new Error(`missing ${description}`);
|
|
110
|
+
return value;
|
|
114
111
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
import type { WorkspaceKey } from "@cueloop/schema";
|
|
1
|
+
/** Working-tree diff capture for `cueloop diff`, untracked files included. */
|
|
4
2
|
|
|
5
3
|
async function git(args: string[], cwd: string): Promise<string | null> {
|
|
6
4
|
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "ignore" });
|
|
@@ -9,15 +7,9 @@ async function git(args: string[], cwd: string): Promise<string | null> {
|
|
|
9
7
|
return out.trim();
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
export async function resolveWorkspace(cwd = process.cwd()): Promise<WorkspaceKey> {
|
|
13
|
-
const repoRoot = (await git(["rev-parse", "--show-toplevel"], cwd)) ?? cwd;
|
|
14
|
-
const branch = (await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd)) ?? "detached";
|
|
15
|
-
return { repoRoot, branch };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
10
|
export async function workingTreeDiff(cwd = process.cwd()): Promise<string> {
|
|
19
11
|
const tracked = (await git(["diff", "HEAD"], cwd)) ?? "";
|
|
20
|
-
// untracked files included by default
|
|
12
|
+
// untracked files included by default
|
|
21
13
|
const untrackedList = (await git(["ls-files", "--others", "--exclude-standard"], cwd)) ?? "";
|
|
22
14
|
let untracked = "";
|
|
23
15
|
for (const file of untrackedList.split("\n").filter(Boolean)) {
|