cueloop 0.1.0-alpha.9 → 0.1.0-alpha.91
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 +31 -23
- package/src/actions-command.ts +49 -0
- package/src/args.ts +21 -10
- package/src/codex-delivery-worker-command.test.ts +69 -0
- package/src/codex-delivery-worker-command.ts +99 -0
- package/src/codex-hook-command.ts +21 -0
- package/src/codex-mcp-command.ts +107 -0
- package/src/daemon-control.test.ts +68 -0
- package/src/daemon-control.ts +104 -0
- package/src/dev-command.ts +112 -0
- package/src/harness-command.ts +16 -0
- package/src/install-command.test.ts +142 -0
- package/src/install-command.ts +114 -0
- package/src/main.ts +372 -93
- package/src/open-target.test.ts +241 -0
- package/src/open-target.ts +128 -0
- package/src/pr.ts +464 -64
- package/src/refine-command.test.ts +187 -0
- package/src/refine-command.ts +45 -0
- package/src/share-command.test.ts +361 -0
- package/src/share-command.ts +147 -0
- package/src/thread-commands.ts +557 -0
- package/src/update-command.test.ts +203 -0
- package/src/update-command.ts +198 -0
- package/src/version.ts +5 -0
- package/src/session-cmds.ts +0 -107
- package/src/working-tree.ts +0 -27
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cueloop dev`: open the TUI against an isolated dev home seeded once with a
|
|
3
|
+
* plan and a diff under this repo's project plus a standalone reply thread, so
|
|
4
|
+
* the Projects and Threads sidebar and the thread view are populated without a
|
|
5
|
+
* live agent. It never touches the real ~/.cueloop home.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { DaemonClient } from "@cueloop/daemon/client";
|
|
12
|
+
import { resolveWorkspace } from "@cueloop/daemon/thread-review";
|
|
13
|
+
|
|
14
|
+
/** Bump whenever the seed content below changes, so a long-lived dev home refreshes instead of keeping
|
|
15
|
+
* stale threads (an old seed's diff had no files, which showed as "No changes"). */
|
|
16
|
+
const SEED_VERSION = 2;
|
|
17
|
+
const SEED_TITLES = new Set([
|
|
18
|
+
"Read the repository",
|
|
19
|
+
"Review the accent change",
|
|
20
|
+
"A standalone thought",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const SEED_PLAN = `# Read the repository
|
|
24
|
+
|
|
25
|
+
A seeded plan so \`cueloop dev\` always has a thread to open.
|
|
26
|
+
|
|
27
|
+
## Steps
|
|
28
|
+
- open the thread view
|
|
29
|
+
- move the cursor into the agent's text
|
|
30
|
+
- type to leave a comment
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const SEED_REPLY = `A standalone thought, not tied to any repository.
|
|
34
|
+
|
|
35
|
+
It lands under Threads in the sidebar rather than a project.
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const SEED_DIFF_PATH = "packages/client/src/theme.ts";
|
|
39
|
+
const SEED_DIFF_OLD = ' accent: "#f5a3a3",\n';
|
|
40
|
+
const SEED_DIFF_NEW = ' accent: "#cba6f7",\n';
|
|
41
|
+
const SEED_DIFF = `diff --git a/${SEED_DIFF_PATH} b/${SEED_DIFF_PATH}
|
|
42
|
+
--- a/${SEED_DIFF_PATH}
|
|
43
|
+
+++ b/${SEED_DIFF_PATH}
|
|
44
|
+
@@ -1 +1 @@
|
|
45
|
+
-${SEED_DIFF_OLD.trimEnd()}
|
|
46
|
+
+${SEED_DIFF_NEW.trimEnd()}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
async function seedDevSessions(client: DaemonClient): Promise<void> {
|
|
50
|
+
// the current repo carries a root commit, so its threads gather under a project
|
|
51
|
+
const project = await resolveWorkspace(process.cwd());
|
|
52
|
+
// a non-repo directory has no root commit, so its thread stays standalone
|
|
53
|
+
const standalone = await resolveWorkspace(tmpdir());
|
|
54
|
+
|
|
55
|
+
await client.sessionCreate(project, {
|
|
56
|
+
type: "plan",
|
|
57
|
+
content: SEED_PLAN,
|
|
58
|
+
meta: { title: "Read the repository" },
|
|
59
|
+
});
|
|
60
|
+
await client.sessionCreate(project, {
|
|
61
|
+
type: "diff",
|
|
62
|
+
content: SEED_DIFF,
|
|
63
|
+
meta: { title: "Review the accent change" },
|
|
64
|
+
files: [
|
|
65
|
+
{
|
|
66
|
+
path: SEED_DIFF_PATH,
|
|
67
|
+
oldContents: SEED_DIFF_OLD,
|
|
68
|
+
newContents: SEED_DIFF_NEW,
|
|
69
|
+
status: "modified",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
await client.sessionCreate(standalone, {
|
|
74
|
+
type: "reply",
|
|
75
|
+
content: SEED_REPLY,
|
|
76
|
+
meta: { title: "A standalone thought" },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Seed the dev home, refreshing the seed threads when SEED_VERSION moved on so a stale home never
|
|
81
|
+
* keeps an old seed (which is why an aged home showed "No changes" - its diff thread had no files). */
|
|
82
|
+
async function refreshDevSeed(client: DaemonClient, home: string): Promise<void> {
|
|
83
|
+
const versionFile = join(home, ".seed-version");
|
|
84
|
+
const stored = existsSync(versionFile) ? Number(readFileSync(versionFile, "utf8").trim()) : 0;
|
|
85
|
+
const existing = await client.sessionList();
|
|
86
|
+
if (existing.length > 0 && stored === SEED_VERSION) return;
|
|
87
|
+
|
|
88
|
+
await Promise.all(
|
|
89
|
+
existing
|
|
90
|
+
.filter((thread) => SEED_TITLES.has(thread.artifact.meta.title ?? ""))
|
|
91
|
+
.map((thread) => client.sessionDelete(thread.id)),
|
|
92
|
+
);
|
|
93
|
+
await seedDevSessions(client);
|
|
94
|
+
mkdirSync(dirname(versionFile), { recursive: true });
|
|
95
|
+
writeFileSync(versionFile, String(SEED_VERSION));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `cueloop dev`: seed an isolated dev home (refreshing a stale seed), then open the TUI on it. */
|
|
99
|
+
export async function devCommand(): Promise<number> {
|
|
100
|
+
const home = (process.env.CUELOOP_HOME ??= join(homedir(), ".cueloop-dev"));
|
|
101
|
+
|
|
102
|
+
const client = await DaemonClient.connect({ autostart: true });
|
|
103
|
+
try {
|
|
104
|
+
await refreshDevSeed(client, home);
|
|
105
|
+
} finally {
|
|
106
|
+
client.close();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { runClient } = await import("@cueloop/client");
|
|
110
|
+
|
|
111
|
+
return runClient({});
|
|
112
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { runHarnessBridge } from "@cueloop/adapters/harness-bridge";
|
|
2
|
+
|
|
3
|
+
export async function harnessCommand(): Promise<number> {
|
|
4
|
+
try {
|
|
5
|
+
const request = JSON.parse(await new Response(Bun.stdin.stream()).text());
|
|
6
|
+
const response = await runHarnessBridge(request);
|
|
7
|
+
|
|
8
|
+
console.log(JSON.stringify(response));
|
|
9
|
+
|
|
10
|
+
return 0;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(`cueloop harness bridge failed: ${String(error)}`);
|
|
13
|
+
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { installExtensionCommand } from "./install-command";
|
|
6
|
+
|
|
7
|
+
const roots: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function testRoot(): string {
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), "cueloop-install-test-"));
|
|
15
|
+
|
|
16
|
+
roots.push(root);
|
|
17
|
+
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("cueloop install", () => {
|
|
22
|
+
test("rejects sources outside npm without running a package command", async () => {
|
|
23
|
+
const root = testRoot();
|
|
24
|
+
let called = false;
|
|
25
|
+
|
|
26
|
+
expect(
|
|
27
|
+
await installExtensionCommand(["file:./extension"], {
|
|
28
|
+
installRoot: root,
|
|
29
|
+
runPackageCommand: async () => {
|
|
30
|
+
called = true;
|
|
31
|
+
|
|
32
|
+
return { exitCode: 0, stderr: "" };
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
).toBe(2);
|
|
36
|
+
expect(called).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("installs a package whose manifest declares a client entry", async () => {
|
|
40
|
+
const root = testRoot();
|
|
41
|
+
const commands: string[][] = [];
|
|
42
|
+
const result = await installExtensionCommand(["npm:@example/tools@1.0.0"], {
|
|
43
|
+
installRoot: root,
|
|
44
|
+
runPackageCommand: async (args) => {
|
|
45
|
+
commands.push(args);
|
|
46
|
+
const packageRoot = join(root, "node_modules", "@example/tools");
|
|
47
|
+
|
|
48
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(root, "package.json"),
|
|
51
|
+
JSON.stringify({ dependencies: { "@example/tools": "1.0.0" } }),
|
|
52
|
+
);
|
|
53
|
+
writeFileSync(
|
|
54
|
+
join(packageRoot, "package.json"),
|
|
55
|
+
JSON.stringify({ cueloop: { client: "./client.js" } }),
|
|
56
|
+
);
|
|
57
|
+
writeFileSync(join(packageRoot, "client.js"), "export default () => {};");
|
|
58
|
+
|
|
59
|
+
return { exitCode: 0, stderr: "" };
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result).toBe(0);
|
|
64
|
+
expect(commands).toEqual([["add", "--ignore-scripts", "--cwd", root, "@example/tools@1.0.0"]]);
|
|
65
|
+
expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dependencies).toEqual({
|
|
66
|
+
"@example/tools": "1.0.0",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("removes a package without a cueloop manifest", async () => {
|
|
71
|
+
const root = testRoot();
|
|
72
|
+
const commands: string[][] = [];
|
|
73
|
+
const result = await installExtensionCommand(["npm:example"], {
|
|
74
|
+
installRoot: root,
|
|
75
|
+
runPackageCommand: async (args) => {
|
|
76
|
+
commands.push(args);
|
|
77
|
+
if (args[0] === "add") {
|
|
78
|
+
const packageRoot = join(root, "node_modules", "example");
|
|
79
|
+
|
|
80
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
81
|
+
writeFileSync(
|
|
82
|
+
join(root, "package.json"),
|
|
83
|
+
JSON.stringify({ dependencies: { example: "1.0.0" } }),
|
|
84
|
+
);
|
|
85
|
+
writeFileSync(join(packageRoot, "package.json"), "{}");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { exitCode: 0, stderr: "" };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result).toBe(1);
|
|
93
|
+
expect(commands[1]).toEqual(["remove", "--ignore-scripts", "--cwd", root, "example"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("restores the prior version after an invalid upgrade", async () => {
|
|
97
|
+
const root = testRoot();
|
|
98
|
+
const packageRoot = join(root, "node_modules", "example");
|
|
99
|
+
const commands: string[][] = [];
|
|
100
|
+
|
|
101
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(root, "package.json"),
|
|
104
|
+
JSON.stringify({ dependencies: { example: "1.0.0" } }),
|
|
105
|
+
);
|
|
106
|
+
writeFileSync(
|
|
107
|
+
join(packageRoot, "package.json"),
|
|
108
|
+
JSON.stringify({ version: "1.0.0", cueloop: { client: "./client.js" } }),
|
|
109
|
+
);
|
|
110
|
+
writeFileSync(join(packageRoot, "client.js"), "export default () => {};");
|
|
111
|
+
const result = await installExtensionCommand(["npm:example@2.0.0"], {
|
|
112
|
+
installRoot: root,
|
|
113
|
+
runPackageCommand: async (args) => {
|
|
114
|
+
commands.push(args);
|
|
115
|
+
if (args.at(-1) === "example@2.0.0") {
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(root, "package.json"),
|
|
118
|
+
JSON.stringify({ dependencies: { example: "2.0.0" } }),
|
|
119
|
+
);
|
|
120
|
+
writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ version: "2.0.0" }));
|
|
121
|
+
} else {
|
|
122
|
+
writeFileSync(
|
|
123
|
+
join(root, "package.json"),
|
|
124
|
+
JSON.stringify({ dependencies: { example: "1.0.0" } }),
|
|
125
|
+
);
|
|
126
|
+
writeFileSync(
|
|
127
|
+
join(packageRoot, "package.json"),
|
|
128
|
+
JSON.stringify({ version: "1.0.0", cueloop: { client: "./client.js" } }),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { exitCode: 0, stderr: "" };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result).toBe(1);
|
|
137
|
+
expect(commands[1]).toEqual(["add", "--ignore-scripts", "--cwd", root, "example@1.0.0"]);
|
|
138
|
+
expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dependencies.example).toBe(
|
|
139
|
+
"1.0.0",
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import {
|
|
5
|
+
discoverInstalledExtensionPackages,
|
|
6
|
+
extensionInstallRoot,
|
|
7
|
+
} from "@cueloop/extension-api/installed-packages";
|
|
8
|
+
|
|
9
|
+
const NPM_PACKAGE_SOURCE = /^npm:((?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+)(?:@([^\s/]+))?$/i;
|
|
10
|
+
|
|
11
|
+
interface PackageCommandResult {
|
|
12
|
+
exitCode: number;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface InstallOptions {
|
|
17
|
+
installRoot?: string;
|
|
18
|
+
runPackageCommand?: (args: string[]) => Promise<PackageCommandResult>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runPackageCommand(args: string[]): Promise<PackageCommandResult> {
|
|
22
|
+
// A compiled executable can invoke its bundled Bun CLI without `bun` on PATH.
|
|
23
|
+
const child = Bun.spawn([process.execPath, ...args], {
|
|
24
|
+
env: { ...process.env, BUN_BE_BUN: "1" },
|
|
25
|
+
stdout: "ignore",
|
|
26
|
+
stderr: "pipe",
|
|
27
|
+
});
|
|
28
|
+
const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
|
|
29
|
+
|
|
30
|
+
return { exitCode, stderr };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function previousPackageSpec(installRoot: string, name: string): string | null {
|
|
34
|
+
const path = join(installRoot, "node_modules", name, "package.json");
|
|
35
|
+
|
|
36
|
+
if (!existsSync(path)) return null;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = v.safeParse(
|
|
39
|
+
v.object({ version: v.pipe(v.string(), v.minLength(1)) }),
|
|
40
|
+
JSON.parse(readFileSync(path, "utf8")),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return parsed.success ? `${name}@${parsed.output.version}` : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Install a user-level npm extension after checking its declared entry points. */
|
|
50
|
+
export async function installExtensionCommand(
|
|
51
|
+
argv: string[],
|
|
52
|
+
options: InstallOptions = {},
|
|
53
|
+
): Promise<number> {
|
|
54
|
+
const source = argv[0];
|
|
55
|
+
const match = source?.match(NPM_PACKAGE_SOURCE);
|
|
56
|
+
|
|
57
|
+
if (argv.length !== 1 || !match) {
|
|
58
|
+
console.error("cueloop install: expected npm:<package>[@version]");
|
|
59
|
+
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
const name = match[1]!;
|
|
63
|
+
const spec = source!.slice("npm:".length);
|
|
64
|
+
const installRoot = options.installRoot ?? extensionInstallRoot();
|
|
65
|
+
const runCommand = options.runPackageCommand ?? runPackageCommand;
|
|
66
|
+
const previousSpec = previousPackageSpec(installRoot, name);
|
|
67
|
+
|
|
68
|
+
mkdirSync(installRoot, { recursive: true });
|
|
69
|
+
const rootManifest = join(installRoot, "package.json");
|
|
70
|
+
|
|
71
|
+
if (!existsSync(rootManifest)) {
|
|
72
|
+
writeFileSync(
|
|
73
|
+
rootManifest,
|
|
74
|
+
JSON.stringify({ name: "cueloop-extensions", private: true }, null, 2),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const { exitCode, stderr } = await runCommand([
|
|
78
|
+
"add",
|
|
79
|
+
"--ignore-scripts",
|
|
80
|
+
"--cwd",
|
|
81
|
+
installRoot,
|
|
82
|
+
spec,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
if (exitCode !== 0) {
|
|
86
|
+
console.error(`cueloop install: ${stderr.trim() || "bun add failed"}`);
|
|
87
|
+
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
const discovered = discoverInstalledExtensionPackages(installRoot);
|
|
91
|
+
const installed = discovered.packages.find((entry) => entry.name === name);
|
|
92
|
+
|
|
93
|
+
if (!installed) {
|
|
94
|
+
const error = discovered.errors.find((message) => message.includes(` ${name}:`));
|
|
95
|
+
console.error(`cueloop install: ${error ?? `extension ${name} has no valid cueloop manifest`}`);
|
|
96
|
+
const restored = await runCommand(
|
|
97
|
+
previousSpec
|
|
98
|
+
? ["add", "--ignore-scripts", "--cwd", installRoot, previousSpec]
|
|
99
|
+
: ["remove", "--ignore-scripts", "--cwd", installRoot, name],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (restored.exitCode !== 0)
|
|
103
|
+
console.error(
|
|
104
|
+
`cueloop install: could not restore ${previousSpec ?? name}: ${restored.stderr.trim()}`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
console.log(
|
|
110
|
+
`Installed ${name}. Restart cueloop and run 'cueloop restart' to load daemon capabilities.`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return 0;
|
|
114
|
+
}
|