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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cueloop",
3
- "version": "0.1.0-alpha.97",
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.97",
34
- "@cueloop/client": "0.1.0-alpha.97",
35
- "@cueloop/daemon": "0.1.0-alpha.97",
36
- "@cueloop/extension-api": "0.1.0-alpha.97",
37
- "@cueloop/schema": "0.1.0-alpha.97",
38
- "@modelcontextprotocol/server": "2.0.0",
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
- while (!existsSync(join(home, "delivered.txt")) && Date.now() < deadline) await Bun.sleep(20);
65
- expect(readFileSync(join(home, "delivered.txt"), "utf8")).toContain("Continue after MCP exit.");
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
  }
@@ -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
+ }
@@ -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 ??= join(homedir(), ".cueloop-dev"));
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
  }
@@ -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),
@@ -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;