cueloop 0.1.0-alpha.97 → 0.1.0-alpha.99
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 +7 -7
- package/src/agent-harness.ts +17 -0
- package/src/codex-delivery-worker-command.test.ts +8 -2
- package/src/codex-mcp-command.ts +1 -0
- package/src/daemon-command.ts +38 -0
- package/src/dev-command.ts +12 -2
- package/src/install-command.ts +1 -0
- package/src/main.ts +4 -23
- package/src/thread-commands.ts +2 -0
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.99",
|
|
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,12 +30,12 @@
|
|
|
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.
|
|
34
|
-
"@cueloop/client": "0.1.0-alpha.
|
|
35
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
36
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
37
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
38
|
-
"@modelcontextprotocol/server": "2.
|
|
33
|
+
"@cueloop/adapters": "0.1.0-alpha.99",
|
|
34
|
+
"@cueloop/client": "0.1.0-alpha.99",
|
|
35
|
+
"@cueloop/daemon": "0.1.0-alpha.99",
|
|
36
|
+
"@cueloop/extension-api": "0.1.0-alpha.99",
|
|
37
|
+
"@cueloop/schema": "0.1.0-alpha.99",
|
|
38
|
+
"@modelcontextprotocol/server": "2.1.0",
|
|
39
39
|
"@valibot/to-json-schema": "1.8.0",
|
|
40
40
|
"valibot": "^1.5.0"
|
|
41
41
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createFxHarness } from "@cueloop/adapters/fx/harness";
|
|
2
|
+
import { loadConfig } from "@cueloop/client/config";
|
|
3
|
+
import type { AgentHarnessAdapter } from "@cueloop/schema";
|
|
4
|
+
|
|
5
|
+
/** Select a locally installed harness; unsupported providers fail before a daemon starts. */
|
|
6
|
+
export function configuredAgentHarness(): AgentHarnessAdapter | undefined {
|
|
7
|
+
const id = process.env.CUELOOP_AGENT_HARNESS ?? "fx";
|
|
8
|
+
|
|
9
|
+
if (id !== "fx") throw new Error(`Thread agent harness is not installed: ${id}`);
|
|
10
|
+
|
|
11
|
+
return createFxHarness({ command: [process.env.CUELOOP_FX_EXECUTABLE ?? "fx", "acp"] });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resolve the experimental Thread agent flag from user and workspace TOML. */
|
|
15
|
+
export function threadAgentEnabled(repoRoot: string): boolean {
|
|
16
|
+
return loadConfig({ repoRoot }).experimental.threadAgent;
|
|
17
|
+
}
|
|
@@ -61,8 +61,14 @@ test("a Codex reply is delivered without an MCP transport", async () => {
|
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
63
|
await client.sessionSendMessage(opened.threadId, "approved", "Continue after MCP exit.");
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const deliveryPath = join(home, "delivered.txt");
|
|
65
|
+
const delivered = () =>
|
|
66
|
+
existsSync(deliveryPath) &&
|
|
67
|
+
readFileSync(deliveryPath, "utf8").includes("Continue after MCP exit.");
|
|
68
|
+
|
|
69
|
+
// Shell redirection creates the file before printf writes the delivered message.
|
|
70
|
+
while (!delivered() && Date.now() < deadline) await Bun.sleep(20);
|
|
71
|
+
expect(delivered()).toBe(true);
|
|
66
72
|
} finally {
|
|
67
73
|
client.close();
|
|
68
74
|
}
|
package/src/codex-mcp-command.ts
CHANGED
|
@@ -85,6 +85,7 @@ export async function codexMcpCommand(): Promise<number> {
|
|
|
85
85
|
const server = serveStdio(() => createCodexMcpServer(), {
|
|
86
86
|
onerror: (error) => console.error(`cueloop Codex MCP: ${String(error)}`),
|
|
87
87
|
});
|
|
88
|
+
|
|
88
89
|
await new Promise<void>((resolve) => process.stdin.once("end", resolve));
|
|
89
90
|
delivery.stop();
|
|
90
91
|
await server.close();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Compose the local daemon with its installed harness adapters at the CLI boundary. */
|
|
2
|
+
export async function daemonCommand(argv: string[]): Promise<number> {
|
|
3
|
+
const { DaemonServer } = await import("@cueloop/daemon");
|
|
4
|
+
// Explicit foreground never idle-exits; --autostart (a re-exec from the client)
|
|
5
|
+
// takes the normal idle-exit, matching the source main.ts entry.
|
|
6
|
+
const envIdle = process.env.CUELOOP_IDLE_EXIT_MS;
|
|
7
|
+
let idleExitMs: number | undefined = 0;
|
|
8
|
+
|
|
9
|
+
if (argv.includes("--autostart")) idleExitMs = envIdle ? Number(envIdle) : undefined;
|
|
10
|
+
const { configuredAgentHarness, threadAgentEnabled } = await import("./agent-harness");
|
|
11
|
+
const server = new DaemonServer({
|
|
12
|
+
idleExitMs,
|
|
13
|
+
threadAgent: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
enabledForThread: (thread) => threadAgentEnabled(thread.workspace.repoRoot),
|
|
16
|
+
adapter: configuredAgentHarness(),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const path = server.start();
|
|
20
|
+
|
|
21
|
+
if (path === null) {
|
|
22
|
+
console.error("a cueloop daemon already owns this home - nothing to do");
|
|
23
|
+
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
console.log(`cueloop daemon (foreground) on ${path}`);
|
|
27
|
+
const stop = () => {
|
|
28
|
+
server.stop();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
process.once("SIGINT", stop);
|
|
33
|
+
process.once("SIGTERM", stop);
|
|
34
|
+
process.once("exit", () => server.stop());
|
|
35
|
+
await new Promise(() => {}); // run until signalled
|
|
36
|
+
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
package/src/dev-command.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { threadAgentEnabled } from "./agent-harness";
|
|
1
2
|
/**
|
|
2
3
|
* `cueloop dev`: open the TUI against an isolated dev home seeded once with a
|
|
3
4
|
* plan and a diff under this repo's project plus a standalone reply thread, so
|
|
@@ -83,6 +84,7 @@ async function refreshDevSeed(client: DaemonClient, home: string): Promise<void>
|
|
|
83
84
|
const versionFile = join(home, ".seed-version");
|
|
84
85
|
const stored = existsSync(versionFile) ? Number(readFileSync(versionFile, "utf8").trim()) : 0;
|
|
85
86
|
const existing = await client.sessionList();
|
|
87
|
+
|
|
86
88
|
if (existing.length > 0 && stored === SEED_VERSION) return;
|
|
87
89
|
|
|
88
90
|
await Promise.all(
|
|
@@ -97,16 +99,24 @@ async function refreshDevSeed(client: DaemonClient, home: string): Promise<void>
|
|
|
97
99
|
|
|
98
100
|
/** `cueloop dev`: seed an isolated dev home (refreshing a stale seed), then open the TUI on it. */
|
|
99
101
|
export async function devCommand(): Promise<number> {
|
|
100
|
-
const home = (process.env.CUELOOP_HOME ??=
|
|
102
|
+
const home = (process.env.CUELOOP_HOME ??= threadAgentEnabled(process.cwd())
|
|
103
|
+
? join(process.cwd(), ".cueloop-dev")
|
|
104
|
+
: join(homedir(), ".cueloop-dev"));
|
|
101
105
|
|
|
106
|
+
let sessionId: string | undefined;
|
|
102
107
|
const client = await DaemonClient.connect({ autostart: true });
|
|
108
|
+
|
|
103
109
|
try {
|
|
104
110
|
await refreshDevSeed(client, home);
|
|
111
|
+
if (threadAgentEnabled(process.cwd()))
|
|
112
|
+
sessionId = (await client.sessionList()).find(
|
|
113
|
+
(thread) => thread.artifact.meta.title === "Read the repository",
|
|
114
|
+
)?.id;
|
|
105
115
|
} finally {
|
|
106
116
|
client.close();
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
const { runClient } = await import("@cueloop/client");
|
|
110
120
|
|
|
111
|
-
return runClient({});
|
|
121
|
+
return runClient({ sessionId });
|
|
112
122
|
}
|
package/src/install-command.ts
CHANGED
|
@@ -92,6 +92,7 @@ export async function installExtensionCommand(
|
|
|
92
92
|
|
|
93
93
|
if (!installed) {
|
|
94
94
|
const error = discovered.errors.find((message) => message.includes(` ${name}:`));
|
|
95
|
+
|
|
95
96
|
console.error(`cueloop install: ${error ?? `extension ${name} has no valid cueloop manifest`}`);
|
|
96
97
|
const restored = await runCommand(
|
|
97
98
|
previousSpec
|
package/src/main.ts
CHANGED
|
@@ -26,31 +26,12 @@ import { DaemonClient, DaemonClientError } from "@cueloop/daemon/client";
|
|
|
26
26
|
import type { DiffFileContents, Thread, ThreadSurfaceOpenStatus } from "@cueloop/schema";
|
|
27
27
|
import { openReview } from "@cueloop/daemon/thread-review";
|
|
28
28
|
|
|
29
|
+
// The CLI owns adapter composition; the daemon package still starts independently.
|
|
30
|
+
process.env.CUELOOP_DAEMON_ENTRY = import.meta.path;
|
|
31
|
+
|
|
29
32
|
const argv = process.argv.slice(2);
|
|
30
33
|
const cmd = argv[0];
|
|
31
34
|
|
|
32
|
-
async function daemonCommand(argv: string[]): Promise<number> {
|
|
33
|
-
const { DaemonServer } = await import("@cueloop/daemon");
|
|
34
|
-
// Explicit foreground never idle-exits; --autostart (a re-exec from the client)
|
|
35
|
-
// takes the normal idle-exit, matching the source main.ts entry.
|
|
36
|
-
const envIdle = process.env.CUELOOP_IDLE_EXIT_MS;
|
|
37
|
-
let idleExitMs: number | undefined = 0;
|
|
38
|
-
|
|
39
|
-
if (argv.includes("--autostart")) idleExitMs = envIdle ? Number(envIdle) : undefined;
|
|
40
|
-
const server = new DaemonServer({ idleExitMs });
|
|
41
|
-
const path = server.start();
|
|
42
|
-
|
|
43
|
-
if (path === null) {
|
|
44
|
-
console.error("a cueloop daemon already owns this home - nothing to do");
|
|
45
|
-
|
|
46
|
-
return 1;
|
|
47
|
-
}
|
|
48
|
-
console.log(`cueloop daemon (foreground) on ${path}`);
|
|
49
|
-
await new Promise(() => {}); // run until signalled
|
|
50
|
-
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
35
|
async function serveEntry(rest: string[]): Promise<number> {
|
|
55
36
|
const { positional, flags } = parseArgs(rest);
|
|
56
37
|
const port = stringFlag(flags, "port");
|
|
@@ -103,7 +84,7 @@ interface CommandHandlers {
|
|
|
103
84
|
|
|
104
85
|
const commandHandlers: CommandHandlers = {
|
|
105
86
|
session: (rest) => sessionCommand(rest),
|
|
106
|
-
daemon: (rest) => daemonCommand(rest),
|
|
87
|
+
daemon: async (rest) => (await import("./daemon-command")).daemonCommand(rest),
|
|
107
88
|
stop: async () => (await import("./daemon-control")).stopCommand(),
|
|
108
89
|
restart: async () => (await import("./daemon-control")).restartCommand(),
|
|
109
90
|
plan: (rest) => planCommand(rest),
|
package/src/thread-commands.ts
CHANGED
|
@@ -43,6 +43,7 @@ async function sessionCreate({ client, flags }: SessionContext): Promise<number>
|
|
|
43
43
|
|
|
44
44
|
if (!isArtifactType(type)) {
|
|
45
45
|
console.error(`unknown artifact type "${type}" - one of: ${ARTIFACT_TYPES.join(", ")}`);
|
|
46
|
+
|
|
46
47
|
return 2;
|
|
47
48
|
}
|
|
48
49
|
const contentFile = stringFlag(flags, "content-file");
|
|
@@ -178,6 +179,7 @@ async function sessionAnnotateCommand({
|
|
|
178
179
|
if (replyTo !== undefined) base.replyTo = replyTo;
|
|
179
180
|
const annotation: Omit<Annotation, "createdAt"> =
|
|
180
181
|
target === undefined ? base : { ...base, target };
|
|
182
|
+
|
|
181
183
|
out(await client.sessionAnnotate(id, annotation, stringFlag(flags, "author-name")));
|
|
182
184
|
|
|
183
185
|
return 0;
|