cueloop 0.1.0-alpha.69 → 0.1.0-alpha.70
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 +5 -5
- package/src/main.ts +3 -3
- package/src/open-target.test.ts +2 -2
- package/src/open-target.ts +9 -9
- package/src/pr.ts +3 -3
- package/src/refine-command.test.ts +6 -6
- package/src/refine-command.ts +10 -10
- package/src/share-command.test.ts +7 -6
- package/src/share-command.ts +5 -8
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.70",
|
|
4
4
|
"description": "Terminal review surface for coding agents: annotate plans and diffs, return a verdict, send structured feedback back",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"build:binary": "bun build --compile --external cpu-features ./src/main.ts --outfile dist/cueloop"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@cueloop/adapters": "0.1.0-alpha.
|
|
34
|
-
"@cueloop/client": "0.1.0-alpha.
|
|
35
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
36
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
33
|
+
"@cueloop/adapters": "0.1.0-alpha.70",
|
|
34
|
+
"@cueloop/client": "0.1.0-alpha.70",
|
|
35
|
+
"@cueloop/daemon": "0.1.0-alpha.70",
|
|
36
|
+
"@cueloop/schema": "0.1.0-alpha.70",
|
|
37
37
|
"valibot": "^1.4.2"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
package/src/main.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { sessionCommand } from "./session-commands";
|
|
|
24
24
|
import { CLI_VERSION } from "./version";
|
|
25
25
|
import { DaemonClient } from "@cueloop/daemon/client";
|
|
26
26
|
import { workingTreeDiff } from "@cueloop/daemon/working-tree";
|
|
27
|
-
import type {
|
|
27
|
+
import type { Thread } from "@cueloop/schema";
|
|
28
28
|
import { openReview, resolveWorkspace } from "@cueloop/daemon/review";
|
|
29
29
|
|
|
30
30
|
const argv = process.argv.slice(2);
|
|
@@ -162,13 +162,13 @@ function openSelector(parsed: ParsedArgs): string | undefined {
|
|
|
162
162
|
* one caller that needs a scope-specific hint (a clean working tree).
|
|
163
163
|
*/
|
|
164
164
|
async function openReviewOfKind(
|
|
165
|
-
match: (session:
|
|
165
|
+
match: (session: Thread) => boolean,
|
|
166
166
|
label: string,
|
|
167
167
|
selector: string | undefined,
|
|
168
168
|
emptyMessage?: string,
|
|
169
169
|
): Promise<number> {
|
|
170
170
|
const client = await DaemonClient.connect({ autostart: true });
|
|
171
|
-
let sessions:
|
|
171
|
+
let sessions: Thread[];
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
174
|
sessions = await client.sessionList();
|
package/src/open-target.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Primitive-first open resolution over a plain session list: latest-pending default, by-id, and fuzzy-by-title (exact wins, unique substring wins, else ambiguous). Pure over the array - no daemon, no TUI. */
|
|
2
2
|
|
|
3
3
|
import { describe, expect, test } from "bun:test";
|
|
4
|
-
import type { ArtifactType,
|
|
4
|
+
import type { ArtifactType, Thread } from "@cueloop/schema";
|
|
5
5
|
import {
|
|
6
6
|
isDiffReview,
|
|
7
7
|
isPlanReview,
|
|
@@ -20,7 +20,7 @@ function session(overrides: {
|
|
|
20
20
|
status?: "pending" | "resolved";
|
|
21
21
|
createdAt?: string;
|
|
22
22
|
pr?: string;
|
|
23
|
-
}):
|
|
23
|
+
}): Thread {
|
|
24
24
|
return {
|
|
25
25
|
schemaVersion: "1",
|
|
26
26
|
id: overrides.id,
|
package/src/open-target.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ever resolves to its own artifact kind.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type {
|
|
19
|
+
import type { Thread } from "@cueloop/schema";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* The primitive scopes, kept disjoint on purpose so a single session never resolves
|
|
@@ -25,19 +25,19 @@ import type { ReviewSession } from "@cueloop/schema";
|
|
|
25
25
|
* diff scope must exclude those - otherwise `cueloop diff` and `cueloop
|
|
26
26
|
* review` would both open the same pending PR review.
|
|
27
27
|
*/
|
|
28
|
-
export function isPlanReview(session:
|
|
28
|
+
export function isPlanReview(session: Thread): boolean {
|
|
29
29
|
return session.artifact.type === "plan";
|
|
30
30
|
}
|
|
31
|
-
export function isReplyReview(session:
|
|
31
|
+
export function isReplyReview(session: Thread): boolean {
|
|
32
32
|
return session.artifact.type === "reply";
|
|
33
33
|
}
|
|
34
|
-
export function isPrototypeReview(session:
|
|
34
|
+
export function isPrototypeReview(session: Thread): boolean {
|
|
35
35
|
return session.artifact.type === "prototype";
|
|
36
36
|
}
|
|
37
|
-
export function isDiffReview(session:
|
|
37
|
+
export function isDiffReview(session: Thread): boolean {
|
|
38
38
|
return session.artifact.type === "diff" && session.artifact.meta.pr === undefined;
|
|
39
39
|
}
|
|
40
|
-
export function isPrReview(session:
|
|
40
|
+
export function isPrReview(session: Thread): boolean {
|
|
41
41
|
return session.artifact.type === "diff" && session.artifact.meta.pr !== undefined;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -48,7 +48,7 @@ export function isSessionId(value: string): boolean {
|
|
|
48
48
|
|
|
49
49
|
export interface OpenTargetQuery {
|
|
50
50
|
/** Only sessions this predicate accepts are eligible - the primitive's artifact scope. */
|
|
51
|
-
match: (session:
|
|
51
|
+
match: (session: Thread) => boolean;
|
|
52
52
|
/** Positional id-or-title selector; absent means "latest pending". */
|
|
53
53
|
selector?: string;
|
|
54
54
|
}
|
|
@@ -60,11 +60,11 @@ export type OpenTarget =
|
|
|
60
60
|
| { kind: "ambiguous"; selector: string; titles: string[] };
|
|
61
61
|
|
|
62
62
|
/** Most-recent-first, so the head of a sorted list is always the latest. */
|
|
63
|
-
function newestFirst(left:
|
|
63
|
+
function newestFirst(left: Thread, right: Thread): number {
|
|
64
64
|
return right.createdAt.localeCompare(left.createdAt);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export function resolveOpenTarget(sessions:
|
|
67
|
+
export function resolveOpenTarget(sessions: Thread[], query: OpenTargetQuery): OpenTarget {
|
|
68
68
|
const scoped = sessions.filter(query.match);
|
|
69
69
|
|
|
70
70
|
if (query.selector === undefined) {
|
package/src/pr.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* half for agents and scripts.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { Thread, VerdictKind } from "@cueloop/schema";
|
|
10
10
|
import { DaemonClient } from "@cueloop/daemon/client";
|
|
11
11
|
import { openReview } from "@cueloop/daemon/review";
|
|
12
12
|
import { parseArgs } from "./args";
|
|
@@ -116,7 +116,7 @@ export async function reviewPostCommand(argv: string[]): Promise<number> {
|
|
|
116
116
|
return postVerdict(session, pr);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
async function getSession(id: string): Promise<
|
|
119
|
+
async function getSession(id: string): Promise<Thread> {
|
|
120
120
|
const client = await DaemonClient.connect({ autostart: true });
|
|
121
121
|
|
|
122
122
|
try {
|
|
@@ -127,7 +127,7 @@ async function getSession(id: string): Promise<ReviewSession> {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/** Post the resolved session's verdict to the PR: feedback.md is the review body. */
|
|
130
|
-
async function postVerdict(session:
|
|
130
|
+
async function postVerdict(session: Thread, pr: string): Promise<number> {
|
|
131
131
|
const verdict = session.verdict!;
|
|
132
132
|
const result = await gh([
|
|
133
133
|
"pr",
|
|
@@ -4,8 +4,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
4
4
|
import { mkdtempSync, readFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import type { Annotation,
|
|
8
|
-
import {
|
|
7
|
+
import type { Annotation, Thread, Verdict, VerdictKind } from "@cueloop/schema";
|
|
8
|
+
import { ThreadStore } from "@cueloop/daemon/store";
|
|
9
9
|
import { reportsDir } from "@cueloop/daemon/paths";
|
|
10
10
|
import { buildRefineReport, refineCommand } from "./refine-command";
|
|
11
11
|
|
|
@@ -29,8 +29,8 @@ function verdict(kind: VerdictKind): Verdict {
|
|
|
29
29
|
|
|
30
30
|
function session(
|
|
31
31
|
id: string,
|
|
32
|
-
overrides: Partial<
|
|
33
|
-
):
|
|
32
|
+
overrides: Partial<Thread> & { type?: Thread["artifact"]["type"] } = {},
|
|
33
|
+
): Thread {
|
|
34
34
|
const { type, ...rest } = overrides;
|
|
35
35
|
|
|
36
36
|
return {
|
|
@@ -87,7 +87,7 @@ describe("refineCommand", () => {
|
|
|
87
87
|
test("writes a report, skips signal-free sessions, and consumes only resolved sessions", async () => {
|
|
88
88
|
// Arrange
|
|
89
89
|
const home = tempHome();
|
|
90
|
-
const store = new
|
|
90
|
+
const store = new ThreadStore(home);
|
|
91
91
|
|
|
92
92
|
store.upsert(session("ses_a", { verdict: verdict("approve"), status: "resolved" }));
|
|
93
93
|
store.upsert(
|
|
@@ -120,7 +120,7 @@ describe("refineCommand", () => {
|
|
|
120
120
|
test("re-analyzes a resolved session that is reopened and resolved again with new feedback", async () => {
|
|
121
121
|
// Arrange
|
|
122
122
|
const home = tempHome();
|
|
123
|
-
const store = new
|
|
123
|
+
const store = new ThreadStore(home);
|
|
124
124
|
|
|
125
125
|
store.upsert(
|
|
126
126
|
session("ses_x", {
|
package/src/refine-command.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { cueloopHome, reportsDir } from "@cueloop/daemon/paths";
|
|
4
|
-
import {
|
|
4
|
+
import { ThreadStore } from "@cueloop/daemon/store";
|
|
5
5
|
import {
|
|
6
6
|
LATEST_REPORT_FILENAME,
|
|
7
7
|
parseRefineState,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveCleanupPeriodDays,
|
|
10
10
|
timestampedReportFilename,
|
|
11
11
|
} from "@cueloop/daemon/retention";
|
|
12
|
-
import { isAgentNote, type Annotation, type
|
|
12
|
+
import { isAgentNote, type Annotation, type Thread } from "@cueloop/schema";
|
|
13
13
|
import { parseArgs, stringFlag } from "./args";
|
|
14
14
|
|
|
15
15
|
const DEFAULT_SESSION_LIMIT = 200;
|
|
@@ -18,7 +18,7 @@ const BODY_LIMIT = 100;
|
|
|
18
18
|
|
|
19
19
|
interface AnnotatedEntry {
|
|
20
20
|
annotation: Annotation;
|
|
21
|
-
session:
|
|
21
|
+
session: Thread;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function refineCommand(argv: string[]): Promise<number> {
|
|
@@ -27,7 +27,7 @@ export async function refineCommand(argv: string[]): Promise<number> {
|
|
|
27
27
|
const limit = parseLimit(stringFlag(flags, "limit"));
|
|
28
28
|
const nowMs = Date.now();
|
|
29
29
|
|
|
30
|
-
const store = new
|
|
30
|
+
const store = new ThreadStore(home);
|
|
31
31
|
|
|
32
32
|
store.recover();
|
|
33
33
|
const all = store.list();
|
|
@@ -72,7 +72,7 @@ export async function refineCommand(argv: string[]): Promise<number> {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export function buildRefineReport(
|
|
75
|
-
analyzed:
|
|
75
|
+
analyzed: Thread[],
|
|
76
76
|
totalCount: number,
|
|
77
77
|
generatedAt: string,
|
|
78
78
|
): string {
|
|
@@ -137,13 +137,13 @@ export function buildRefineReport(
|
|
|
137
137
|
return lines.join("\n");
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
function hasReviewSignal(session:
|
|
140
|
+
function hasReviewSignal(session: Thread): boolean {
|
|
141
141
|
return (
|
|
142
142
|
session.verdict !== null || session.annotations.some((annotation) => !isAgentNote(annotation))
|
|
143
143
|
);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function flattenReviewAnnotations(sessions:
|
|
146
|
+
function flattenReviewAnnotations(sessions: Thread[]): AnnotatedEntry[] {
|
|
147
147
|
const entries: AnnotatedEntry[] = [];
|
|
148
148
|
|
|
149
149
|
for (const session of sessions) {
|
|
@@ -194,13 +194,13 @@ function tally<T>(items: T[], keyOf: (item: T) => string): Map<string, number> {
|
|
|
194
194
|
return counts;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
function primitiveLabel(session:
|
|
197
|
+
function primitiveLabel(session: Thread): string {
|
|
198
198
|
if (session.artifact.type === "diff" && session.artifact.meta.pr) return "pull request";
|
|
199
199
|
|
|
200
200
|
return session.artifact.type;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
function verdictLabel(session:
|
|
203
|
+
function verdictLabel(session: Thread): string {
|
|
204
204
|
switch (session.verdict?.kind) {
|
|
205
205
|
case "approve":
|
|
206
206
|
return "approve";
|
|
@@ -239,7 +239,7 @@ function parseLimit(raw: string | undefined): number {
|
|
|
239
239
|
return Number.isInteger(value) && value > 0 ? value : DEFAULT_SESSION_LIMIT;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
function analysisFingerprint(session:
|
|
242
|
+
function analysisFingerprint(session: Thread): string {
|
|
243
243
|
return `${session.revisions.length}:${session.annotations.length}:${session.verdict?.resolvedAt ?? "pending"}`;
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
historyFromLinear,
|
|
5
5
|
SCHEMA_VERSION,
|
|
6
6
|
type Annotation,
|
|
7
|
-
type
|
|
7
|
+
type Thread,
|
|
8
8
|
} from "@cueloop/schema";
|
|
9
9
|
import type { SessionClient } from "@cueloop/daemon/client";
|
|
10
10
|
import { pullSession, shareSession, type PullDeps, type ShareDeps } from "./share-command";
|
|
11
11
|
|
|
12
|
-
function sessionFixture(id: string, overrides: Partial<
|
|
12
|
+
function sessionFixture(id: string, overrides: Partial<Thread> = {}): Thread {
|
|
13
13
|
return {
|
|
14
14
|
schemaVersion: SCHEMA_VERSION,
|
|
15
15
|
id,
|
|
@@ -40,12 +40,13 @@ const unimplemented = (member: string) => () =>
|
|
|
40
40
|
Promise.reject(new Error(`fakeClient does not implement ${member}`));
|
|
41
41
|
|
|
42
42
|
/** A SessionClient that answers get/list from a fixed list and records the share/merge primitives. */
|
|
43
|
-
function fakeClient(sessions:
|
|
43
|
+
function fakeClient(sessions: Thread[]): SessionClient {
|
|
44
44
|
return {
|
|
45
45
|
onEvent: () => () => {},
|
|
46
46
|
subscribe: async () => {},
|
|
47
47
|
sessionGet: async (id: string) => sessions.find((session) => session.id === id)!,
|
|
48
48
|
sessionList: async () => sessions,
|
|
49
|
+
sessionComment: unimplemented("sessionComment"),
|
|
49
50
|
sessionAnnotate: unimplemented("sessionAnnotate"),
|
|
50
51
|
sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
|
|
51
52
|
sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
|
|
@@ -113,7 +114,7 @@ describe(shareSession, () => {
|
|
|
113
114
|
test("publishes the named session and reports the copied ssh line", async () => {
|
|
114
115
|
// Arrange
|
|
115
116
|
const publish = mock(
|
|
116
|
-
async (session:
|
|
117
|
+
async (session: Thread) => (
|
|
117
118
|
expect(session.id).toBe("ses_2"),
|
|
118
119
|
{ line: "ssh p_abc123xy@cueloop.dev", copied: true }
|
|
119
120
|
),
|
|
@@ -135,7 +136,7 @@ describe(shareSession, () => {
|
|
|
135
136
|
test("without an id, shares the most recent session", async () => {
|
|
136
137
|
// Arrange
|
|
137
138
|
const publish = mock(
|
|
138
|
-
async (session:
|
|
139
|
+
async (session: Thread) => (
|
|
139
140
|
expect(session.id).toBe("ses_2"),
|
|
140
141
|
{ line: "ssh p_zzzzzzzz@cueloop.dev", copied: true }
|
|
141
142
|
),
|
|
@@ -211,7 +212,7 @@ describe(shareSession, () => {
|
|
|
211
212
|
});
|
|
212
213
|
});
|
|
213
214
|
|
|
214
|
-
function pullDepsSpy(remote:
|
|
215
|
+
function pullDepsSpy(remote: Thread): PullDeps & { lines: string[] } {
|
|
215
216
|
const lines: string[] = [];
|
|
216
217
|
|
|
217
218
|
return {
|
package/src/share-command.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type ShareResult,
|
|
16
16
|
type ShareTarget,
|
|
17
17
|
} from "@cueloop/client";
|
|
18
|
-
import type {
|
|
18
|
+
import type { Thread } from "@cueloop/schema";
|
|
19
19
|
|
|
20
20
|
export interface ShareParams {
|
|
21
21
|
sessionId?: string;
|
|
@@ -27,12 +27,12 @@ export interface ShareParams {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface ShareDeps {
|
|
30
|
-
publish: (session:
|
|
30
|
+
publish: (session: Thread, target: ShareTarget) => Promise<ShareResult>;
|
|
31
31
|
out: (message: string) => void;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface PullDeps {
|
|
35
|
-
pull: (shareId: string, target: ShareTarget) => Promise<
|
|
35
|
+
pull: (shareId: string, target: ShareTarget) => Promise<Thread>;
|
|
36
36
|
out: (message: string) => void;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -122,10 +122,7 @@ export async function pullSession(
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** The named session, or the most recent one when no id is given. */
|
|
125
|
-
async function pickSession(
|
|
126
|
-
client: SessionClient,
|
|
127
|
-
sessionId?: string,
|
|
128
|
-
): Promise<ReviewSession | null> {
|
|
125
|
+
async function pickSession(client: SessionClient, sessionId?: string): Promise<Thread | null> {
|
|
129
126
|
if (sessionId) return client.sessionGet(sessionId);
|
|
130
127
|
const sessions = await client.sessionList();
|
|
131
128
|
|
|
@@ -136,7 +133,7 @@ async function pickSession(
|
|
|
136
133
|
async function pickSharedSession(
|
|
137
134
|
client: SessionClient,
|
|
138
135
|
sessionId?: string,
|
|
139
|
-
): Promise<
|
|
136
|
+
): Promise<Thread | null> {
|
|
140
137
|
if (sessionId) {
|
|
141
138
|
const session = await client.sessionGet(sessionId);
|
|
142
139
|
|