bluekiwi 0.1.0
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/README.md +4 -0
- package/dist/api-client.js +24 -0
- package/dist/assets/index.js +15 -0
- package/dist/assets/mcp/api-client.d.ts +6 -0
- package/dist/assets/mcp/api-client.js +61 -0
- package/dist/assets/mcp/errors.d.ts +12 -0
- package/dist/assets/mcp/errors.js +24 -0
- package/dist/assets/mcp/server.d.ts +1 -0
- package/dist/assets/mcp/server.js +805 -0
- package/dist/assets/skills/bk-next/SKILL.md +207 -0
- package/dist/assets/skills/bk-rewind/SKILL.md +77 -0
- package/dist/assets/skills/bk-start/SKILL.md +142 -0
- package/dist/assets/skills/bk-status/SKILL.md +34 -0
- package/dist/commands/accept.js +79 -0
- package/dist/commands/dev-link.js +37 -0
- package/dist/commands/init.js +137 -0
- package/dist/commands/logout.js +18 -0
- package/dist/commands/runtimes.js +51 -0
- package/dist/commands/status.js +22 -0
- package/dist/commands/upgrade.js +28 -0
- package/dist/config.js +30 -0
- package/dist/index.js +52 -0
- package/dist/runtimes/base.js +1 -0
- package/dist/runtimes/claude-code.js +56 -0
- package/dist/runtimes/codex.js +54 -0
- package/dist/runtimes/detect.js +17 -0
- package/dist/runtimes/gemini-cli.js +56 -0
- package/dist/runtimes/openclaw.js +56 -0
- package/dist/runtimes/opencode.js +56 -0
- package/package.json +31 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { BlueKiwiClient } from "../api-client.js";
|
|
4
|
+
import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "../assets/index.js";
|
|
5
|
+
import { saveConfig } from "../config.js";
|
|
6
|
+
import { detectInstalledAdapters, getAllAdapters } from "../runtimes/detect.js";
|
|
7
|
+
function normalizeEnvValue(value) {
|
|
8
|
+
const trimmed = value?.trim();
|
|
9
|
+
return trimmed ? trimmed : undefined;
|
|
10
|
+
}
|
|
11
|
+
function parseCommaSeparatedList(values) {
|
|
12
|
+
const parsed = [];
|
|
13
|
+
for (const value of values ?? []) {
|
|
14
|
+
for (const item of value.split(",")) {
|
|
15
|
+
const trimmed = item.trim();
|
|
16
|
+
if (trimmed)
|
|
17
|
+
parsed.push(trimmed);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
function uniquePreserveOrder(values) {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const unique = [];
|
|
25
|
+
for (const value of values) {
|
|
26
|
+
if (seen.has(value))
|
|
27
|
+
continue;
|
|
28
|
+
seen.add(value);
|
|
29
|
+
unique.push(value);
|
|
30
|
+
}
|
|
31
|
+
return unique;
|
|
32
|
+
}
|
|
33
|
+
export async function initCommand(options = {}) {
|
|
34
|
+
const isNonInteractive = options.yes === true || process.stdin.isTTY !== true;
|
|
35
|
+
let server = normalizeEnvValue(options.server) ??
|
|
36
|
+
normalizeEnvValue(process.env.BLUEKIWI_SERVER);
|
|
37
|
+
let apiKey = normalizeEnvValue(options.apiKey) ??
|
|
38
|
+
normalizeEnvValue(process.env.BLUEKIWI_API_KEY);
|
|
39
|
+
if (!isNonInteractive && (server === undefined || apiKey === undefined)) {
|
|
40
|
+
const questions = [];
|
|
41
|
+
if (server === undefined) {
|
|
42
|
+
questions.push({
|
|
43
|
+
type: "text",
|
|
44
|
+
name: "server",
|
|
45
|
+
message: "BlueKiwi server URL",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (apiKey === undefined) {
|
|
49
|
+
questions.push({
|
|
50
|
+
type: "password",
|
|
51
|
+
name: "apiKey",
|
|
52
|
+
message: "API key (bk_...)",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const answers = await prompts(questions);
|
|
56
|
+
server ??= answers.server;
|
|
57
|
+
apiKey ??= answers.apiKey;
|
|
58
|
+
}
|
|
59
|
+
if (server === undefined || apiKey === undefined) {
|
|
60
|
+
if (isNonInteractive) {
|
|
61
|
+
throw new Error("Non-interactive mode: --server and --api-key (or BLUEKIWI_SERVER/BLUEKIWI_API_KEY) are required");
|
|
62
|
+
}
|
|
63
|
+
throw new Error("BlueKiwi server URL and API key are required");
|
|
64
|
+
}
|
|
65
|
+
const client = new BlueKiwiClient(server, apiKey);
|
|
66
|
+
await client.request("GET", "/api/workflows");
|
|
67
|
+
const me = {
|
|
68
|
+
id: 0,
|
|
69
|
+
username: "unknown",
|
|
70
|
+
email: "",
|
|
71
|
+
role: "viewer",
|
|
72
|
+
};
|
|
73
|
+
const detected = detectInstalledAdapters();
|
|
74
|
+
const all = getAllAdapters();
|
|
75
|
+
const adaptersByName = new Map(all.map((adapter) => [adapter.name, adapter]));
|
|
76
|
+
const validRuntimeNames = all.map((adapter) => adapter.name);
|
|
77
|
+
const requestedFromFlags = uniquePreserveOrder(parseCommaSeparatedList(options.runtimes));
|
|
78
|
+
const requestedFromEnv = uniquePreserveOrder(parseCommaSeparatedList([process.env.BLUEKIWI_RUNTIMES ?? ""]));
|
|
79
|
+
const requestedRuntimeNames = requestedFromFlags.length > 0 ? requestedFromFlags : requestedFromEnv;
|
|
80
|
+
let selectedRuntimeNames = [];
|
|
81
|
+
if (requestedRuntimeNames.length > 0) {
|
|
82
|
+
const unknown = requestedRuntimeNames.filter((name) => !adaptersByName.has(name));
|
|
83
|
+
if (unknown.length > 0) {
|
|
84
|
+
throw new Error(`Unknown runtime(s): ${unknown.join(", ")}. Valid runtimes: ${validRuntimeNames.join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
for (const name of requestedRuntimeNames) {
|
|
87
|
+
const adapter = adaptersByName.get(name);
|
|
88
|
+
if (!adapter)
|
|
89
|
+
continue;
|
|
90
|
+
if (!adapter.isInstalled()) {
|
|
91
|
+
throw new Error(`Runtime '${name}' is not installed on this system`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
selectedRuntimeNames = requestedRuntimeNames;
|
|
95
|
+
}
|
|
96
|
+
else if (isNonInteractive) {
|
|
97
|
+
if (detected.length === 0) {
|
|
98
|
+
throw new Error("Non-interactive mode: at least one runtime is required (--runtime <name>) or install a supported runtime");
|
|
99
|
+
}
|
|
100
|
+
selectedRuntimeNames = [detected[0].name];
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const { selected } = (await prompts({
|
|
104
|
+
type: "multiselect",
|
|
105
|
+
name: "selected",
|
|
106
|
+
message: "Install into which runtimes?",
|
|
107
|
+
choices: all.map((adapter) => ({
|
|
108
|
+
title: adapter.displayName,
|
|
109
|
+
value: adapter.name,
|
|
110
|
+
selected: detected.some((item) => item.name === adapter.name),
|
|
111
|
+
disabled: !adapter.isInstalled(),
|
|
112
|
+
})),
|
|
113
|
+
}));
|
|
114
|
+
selectedRuntimeNames = selected ?? [];
|
|
115
|
+
}
|
|
116
|
+
for (const name of selectedRuntimeNames) {
|
|
117
|
+
const adapter = all.find((item) => item.name === name);
|
|
118
|
+
if (!adapter)
|
|
119
|
+
continue;
|
|
120
|
+
adapter.installSkills(BUNDLED_SKILLS);
|
|
121
|
+
adapter.installMcp({
|
|
122
|
+
command: "node",
|
|
123
|
+
args: [BUNDLED_MCP_PATH],
|
|
124
|
+
env: { BLUEKIWI_API_URL: server, BLUEKIWI_API_KEY: apiKey },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
saveConfig({
|
|
128
|
+
version: "1.0.0",
|
|
129
|
+
server_url: server,
|
|
130
|
+
api_key: apiKey,
|
|
131
|
+
user: me,
|
|
132
|
+
runtimes: selectedRuntimeNames,
|
|
133
|
+
installed_at: new Date().toISOString(),
|
|
134
|
+
last_used: new Date().toISOString(),
|
|
135
|
+
});
|
|
136
|
+
console.log(pc.green("✓ BlueKiwi connected"));
|
|
137
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { clearConfig, loadConfig } from "../config.js";
|
|
3
|
+
import { getAllAdapters } from "../runtimes/detect.js";
|
|
4
|
+
export async function logoutCommand() {
|
|
5
|
+
const cfg = loadConfig();
|
|
6
|
+
if (!cfg) {
|
|
7
|
+
console.log(pc.yellow("Already logged out."));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
for (const adapter of getAllAdapters()) {
|
|
11
|
+
if (cfg.runtimes.includes(adapter.name)) {
|
|
12
|
+
adapter.uninstall();
|
|
13
|
+
console.log(pc.dim(` removed ${adapter.displayName}`));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
clearConfig();
|
|
17
|
+
console.log(pc.green("✓ Logged out and uninstalled."));
|
|
18
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getAllAdapters } from "../runtimes/detect.js";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "../assets/index.js";
|
|
4
|
+
import { loadConfig, requireConfig, saveConfig } from "../config.js";
|
|
5
|
+
async function list() {
|
|
6
|
+
const cfg = loadConfig();
|
|
7
|
+
const installed = new Set(cfg?.runtimes ?? []);
|
|
8
|
+
for (const adapter of getAllAdapters()) {
|
|
9
|
+
const detected = adapter.isInstalled()
|
|
10
|
+
? pc.green("detected")
|
|
11
|
+
: pc.dim("not installed");
|
|
12
|
+
const active = installed.has(adapter.name)
|
|
13
|
+
? pc.green("● active")
|
|
14
|
+
: pc.dim("○ inactive");
|
|
15
|
+
console.log(`${adapter.displayName.padEnd(14)} ${detected} ${active}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function add(name) {
|
|
19
|
+
const cfg = requireConfig();
|
|
20
|
+
const adapter = getAllAdapters().find((item) => item.name === name);
|
|
21
|
+
if (!adapter) {
|
|
22
|
+
console.error(`Unknown runtime: ${name}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
adapter.installSkills(BUNDLED_SKILLS);
|
|
26
|
+
adapter.installMcp({
|
|
27
|
+
command: "node",
|
|
28
|
+
args: [BUNDLED_MCP_PATH],
|
|
29
|
+
env: { BLUEKIWI_API_URL: cfg.server_url, BLUEKIWI_API_KEY: cfg.api_key },
|
|
30
|
+
});
|
|
31
|
+
saveConfig({
|
|
32
|
+
...cfg,
|
|
33
|
+
runtimes: Array.from(new Set([...cfg.runtimes, name])),
|
|
34
|
+
});
|
|
35
|
+
console.log(pc.green(`✓ Installed to ${adapter.displayName}`));
|
|
36
|
+
}
|
|
37
|
+
async function remove(name) {
|
|
38
|
+
const cfg = requireConfig();
|
|
39
|
+
const adapter = getAllAdapters().find((item) => item.name === name);
|
|
40
|
+
if (!adapter) {
|
|
41
|
+
console.error(`Unknown runtime: ${name}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
adapter.uninstall();
|
|
45
|
+
saveConfig({
|
|
46
|
+
...cfg,
|
|
47
|
+
runtimes: cfg.runtimes.filter((runtime) => runtime !== name),
|
|
48
|
+
});
|
|
49
|
+
console.log(pc.green(`✓ Removed ${adapter.displayName}`));
|
|
50
|
+
}
|
|
51
|
+
export const runtimesCommand = { list, add, remove };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { BlueKiwiClient } from "../api-client.js";
|
|
3
|
+
import { CONFIG_PATH, loadConfig } from "../config.js";
|
|
4
|
+
export async function statusCommand() {
|
|
5
|
+
const cfg = loadConfig();
|
|
6
|
+
if (!cfg) {
|
|
7
|
+
console.log(pc.yellow(`Not authenticated. No config at ${CONFIG_PATH}.`));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
console.log(`${pc.bold("Server:")} ${cfg.server_url}`);
|
|
11
|
+
console.log(`${pc.bold("User:")} ${cfg.user.username} (${cfg.user.role})`);
|
|
12
|
+
console.log(`${pc.bold("Runtimes:")} ${cfg.runtimes.join(", ") || "(none)"}`);
|
|
13
|
+
try {
|
|
14
|
+
const client = new BlueKiwiClient(cfg.server_url, cfg.api_key);
|
|
15
|
+
await client.request("GET", "/api/workflows");
|
|
16
|
+
console.log(pc.green("✓ Connection OK"));
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.log(pc.red(`✗ Connection failed: ${err.message}`));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "../assets/index.js";
|
|
4
|
+
import { loadConfig, saveConfig } from "../config.js";
|
|
5
|
+
import { getAllAdapters } from "../runtimes/detect.js";
|
|
6
|
+
export async function upgradeCommand() {
|
|
7
|
+
console.log(pc.cyan("→ Upgrading bluekiwi..."));
|
|
8
|
+
execFileSync("npm", ["install", "-g", "bluekiwi@latest"], {
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
});
|
|
11
|
+
const cfg = loadConfig();
|
|
12
|
+
if (!cfg) {
|
|
13
|
+
console.log(pc.yellow("No config found. Run `bluekiwi accept` or `bluekiwi init` next."));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const adapter of getAllAdapters()) {
|
|
17
|
+
if (!cfg.runtimes.includes(adapter.name))
|
|
18
|
+
continue;
|
|
19
|
+
adapter.installSkills(BUNDLED_SKILLS);
|
|
20
|
+
adapter.installMcp({
|
|
21
|
+
command: "node",
|
|
22
|
+
args: [BUNDLED_MCP_PATH],
|
|
23
|
+
env: { BLUEKIWI_API_URL: cfg.server_url, BLUEKIWI_API_KEY: cfg.api_key },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
saveConfig({ ...cfg, last_used: new Date().toISOString() });
|
|
27
|
+
console.log(pc.green("✓ Upgraded and reinstalled assets."));
|
|
28
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync, } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
export const CONFIG_PATH = join(homedir(), ".bluekiwi", "config.json");
|
|
5
|
+
export function loadConfig() {
|
|
6
|
+
if (!existsSync(CONFIG_PATH))
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function saveConfig(config) {
|
|
16
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
|
|
17
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
18
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
19
|
+
}
|
|
20
|
+
export function clearConfig() {
|
|
21
|
+
if (existsSync(CONFIG_PATH))
|
|
22
|
+
unlinkSync(CONFIG_PATH);
|
|
23
|
+
}
|
|
24
|
+
export function requireConfig() {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
if (!config) {
|
|
27
|
+
throw new Error("Not authenticated. Run `npx bluekiwi accept <token> --server <url>` first.");
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { acceptCommand } from "./commands/accept.js";
|
|
4
|
+
import { initCommand } from "./commands/init.js";
|
|
5
|
+
import { statusCommand } from "./commands/status.js";
|
|
6
|
+
import { upgradeCommand } from "./commands/upgrade.js";
|
|
7
|
+
import { logoutCommand } from "./commands/logout.js";
|
|
8
|
+
import { runtimesCommand } from "./commands/runtimes.js";
|
|
9
|
+
import { devLinkCommand } from "./commands/dev-link.js";
|
|
10
|
+
function splitCommaSeparatedList(value) {
|
|
11
|
+
return value
|
|
12
|
+
.split(",")
|
|
13
|
+
.map((item) => item.trim())
|
|
14
|
+
.filter((item) => item.length > 0);
|
|
15
|
+
}
|
|
16
|
+
function collectRuntimes(value, previous) {
|
|
17
|
+
return previous.concat(splitCommaSeparatedList(value));
|
|
18
|
+
}
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name("bluekiwi")
|
|
22
|
+
.description("BlueKiwi CLI — connect your agent runtime to a BlueKiwi server")
|
|
23
|
+
.version("0.0.0");
|
|
24
|
+
program
|
|
25
|
+
.command("accept <token>")
|
|
26
|
+
.requiredOption("--server <url>", "BlueKiwi server URL")
|
|
27
|
+
.option("--username <name>", "Username (non-interactive)")
|
|
28
|
+
.option("--password <pass>", "Password (non-interactive)")
|
|
29
|
+
.action(acceptCommand);
|
|
30
|
+
program
|
|
31
|
+
.command("init")
|
|
32
|
+
.option("--server <url>", "BlueKiwi server URL")
|
|
33
|
+
.option("--api-key <key>", "API key (bk_...)")
|
|
34
|
+
.option("--runtime <name>", "Runtime to install into (repeatable, or comma-separated)", collectRuntimes, [])
|
|
35
|
+
.option("--yes", "Suppress all prompts (non-interactive)")
|
|
36
|
+
.action((opts) => initCommand({
|
|
37
|
+
server: opts.server,
|
|
38
|
+
apiKey: opts.apiKey,
|
|
39
|
+
runtimes: opts.runtime?.length ? opts.runtime : undefined,
|
|
40
|
+
yes: opts.yes,
|
|
41
|
+
}));
|
|
42
|
+
program.command("status").action(statusCommand);
|
|
43
|
+
program.command("upgrade").action(upgradeCommand);
|
|
44
|
+
program.command("logout").action(logoutCommand);
|
|
45
|
+
program.command("runtimes").action(runtimesCommand.list);
|
|
46
|
+
program.command("runtimes:add <name>").action(runtimesCommand.add);
|
|
47
|
+
program.command("runtimes:remove <name>").action(runtimesCommand.remove);
|
|
48
|
+
program.command("dev-link").action(devLinkCommand);
|
|
49
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
50
|
+
console.error(err.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
const BASE = join(homedir(), ".claude");
|
|
5
|
+
const SKILLS_DIR = join(BASE, "skills");
|
|
6
|
+
const MCP_CONFIG = join(BASE, "mcp.json");
|
|
7
|
+
export class ClaudeCodeAdapter {
|
|
8
|
+
name = "claude-code";
|
|
9
|
+
displayName = "Claude Code";
|
|
10
|
+
isInstalled() {
|
|
11
|
+
return existsSync(BASE);
|
|
12
|
+
}
|
|
13
|
+
getSkillsDir() {
|
|
14
|
+
return SKILLS_DIR;
|
|
15
|
+
}
|
|
16
|
+
getMcpConfigPath() {
|
|
17
|
+
return MCP_CONFIG;
|
|
18
|
+
}
|
|
19
|
+
installSkills(skills) {
|
|
20
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
const dir = join(SKILLS_DIR, skill.name);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(join(dir, "SKILL.md"), skill.content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
installMcp(config) {
|
|
28
|
+
let existing = {};
|
|
29
|
+
if (existsSync(MCP_CONFIG)) {
|
|
30
|
+
try {
|
|
31
|
+
existing = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
existing.mcpServers = existing.mcpServers ?? {};
|
|
36
|
+
existing.mcpServers.bluekiwi = config;
|
|
37
|
+
mkdirSync(BASE, { recursive: true });
|
|
38
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(existing, null, 2));
|
|
39
|
+
}
|
|
40
|
+
uninstall() {
|
|
41
|
+
for (const name of ["bk-start", "bk-next", "bk-status", "bk-rewind"]) {
|
|
42
|
+
const dir = join(SKILLS_DIR, name);
|
|
43
|
+
if (existsSync(dir)) {
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(MCP_CONFIG)) {
|
|
48
|
+
try {
|
|
49
|
+
const config = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
50
|
+
delete config.mcpServers?.bluekiwi;
|
|
51
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(config, null, 2));
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
const BASE = join(homedir(), ".codex");
|
|
5
|
+
const SKILLS_DIR = join(BASE, "skills");
|
|
6
|
+
const MCP_CONFIG = join(BASE, "config.toml");
|
|
7
|
+
export class CodexAdapter {
|
|
8
|
+
name = "codex";
|
|
9
|
+
displayName = "Codex CLI";
|
|
10
|
+
isInstalled() {
|
|
11
|
+
return existsSync(BASE);
|
|
12
|
+
}
|
|
13
|
+
getSkillsDir() {
|
|
14
|
+
return SKILLS_DIR;
|
|
15
|
+
}
|
|
16
|
+
getMcpConfigPath() {
|
|
17
|
+
return MCP_CONFIG;
|
|
18
|
+
}
|
|
19
|
+
installSkills(skills) {
|
|
20
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
const dir = join(SKILLS_DIR, skill.name);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(join(dir, "SKILL.md"), skill.content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
installMcp(config) {
|
|
28
|
+
mkdirSync(BASE, { recursive: true });
|
|
29
|
+
const envLines = Object.entries(config.env)
|
|
30
|
+
.map(([key, value]) => `${key} = "${value}"`)
|
|
31
|
+
.join("\n");
|
|
32
|
+
const snippet = `\n[mcp_servers.bluekiwi]\n` +
|
|
33
|
+
`command = "${config.command}"\n` +
|
|
34
|
+
`args = ${JSON.stringify(config.args)}\n\n` +
|
|
35
|
+
`[mcp_servers.bluekiwi.env]\n${envLines}\n`;
|
|
36
|
+
const existing = existsSync(MCP_CONFIG)
|
|
37
|
+
? readFileSync(MCP_CONFIG, "utf8")
|
|
38
|
+
: "";
|
|
39
|
+
const stripped = existing.replace(/\n?\[mcp_servers\.bluekiwi\][\s\S]*?(?=\n\[|$)/g, "");
|
|
40
|
+
writeFileSync(MCP_CONFIG, stripped + snippet);
|
|
41
|
+
}
|
|
42
|
+
uninstall() {
|
|
43
|
+
if (existsSync(MCP_CONFIG)) {
|
|
44
|
+
const existing = readFileSync(MCP_CONFIG, "utf8");
|
|
45
|
+
writeFileSync(MCP_CONFIG, existing.replace(/\n?\[mcp_servers\.bluekiwi\][\s\S]*?(?=\n\[|$)/g, ""));
|
|
46
|
+
}
|
|
47
|
+
for (const name of ["bk-start", "bk-next", "bk-status", "bk-rewind"]) {
|
|
48
|
+
const dir = join(SKILLS_DIR, name);
|
|
49
|
+
if (existsSync(dir)) {
|
|
50
|
+
rmSync(dir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ClaudeCodeAdapter } from "./claude-code.js";
|
|
2
|
+
import { CodexAdapter } from "./codex.js";
|
|
3
|
+
import { GeminiCliAdapter } from "./gemini-cli.js";
|
|
4
|
+
import { OpenCodeAdapter } from "./opencode.js";
|
|
5
|
+
import { OpenClawAdapter } from "./openclaw.js";
|
|
6
|
+
export function getAllAdapters() {
|
|
7
|
+
return [
|
|
8
|
+
new ClaudeCodeAdapter(),
|
|
9
|
+
new CodexAdapter(),
|
|
10
|
+
new GeminiCliAdapter(),
|
|
11
|
+
new OpenCodeAdapter(),
|
|
12
|
+
new OpenClawAdapter(),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
export function detectInstalledAdapters() {
|
|
16
|
+
return getAllAdapters().filter((adapter) => adapter.isInstalled());
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
const BASE = join(homedir(), ".gemini");
|
|
5
|
+
const SKILLS_DIR = join(BASE, "skills");
|
|
6
|
+
const MCP_CONFIG = join(BASE, "settings.json");
|
|
7
|
+
export class GeminiCliAdapter {
|
|
8
|
+
name = "gemini-cli";
|
|
9
|
+
displayName = "Gemini CLI";
|
|
10
|
+
isInstalled() {
|
|
11
|
+
return existsSync(BASE);
|
|
12
|
+
}
|
|
13
|
+
getSkillsDir() {
|
|
14
|
+
return SKILLS_DIR;
|
|
15
|
+
}
|
|
16
|
+
getMcpConfigPath() {
|
|
17
|
+
return MCP_CONFIG;
|
|
18
|
+
}
|
|
19
|
+
installSkills(skills) {
|
|
20
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
const dir = join(SKILLS_DIR, skill.name);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(join(dir, "SKILL.md"), skill.content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
installMcp(config) {
|
|
28
|
+
mkdirSync(BASE, { recursive: true });
|
|
29
|
+
let existing = {};
|
|
30
|
+
if (existsSync(MCP_CONFIG)) {
|
|
31
|
+
try {
|
|
32
|
+
existing = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
}
|
|
36
|
+
existing.mcpServers = existing.mcpServers ?? {};
|
|
37
|
+
existing.mcpServers.bluekiwi = config;
|
|
38
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(existing, null, 2));
|
|
39
|
+
}
|
|
40
|
+
uninstall() {
|
|
41
|
+
for (const name of ["bk-start", "bk-next", "bk-status", "bk-rewind"]) {
|
|
42
|
+
const dir = join(SKILLS_DIR, name);
|
|
43
|
+
if (existsSync(dir)) {
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(MCP_CONFIG)) {
|
|
48
|
+
try {
|
|
49
|
+
const config = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
50
|
+
delete config.mcpServers?.bluekiwi;
|
|
51
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(config, null, 2));
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
const BASE = join(homedir(), ".openclaw");
|
|
5
|
+
const SKILLS_DIR = join(BASE, "skills");
|
|
6
|
+
const MCP_CONFIG = join(BASE, "mcp.json");
|
|
7
|
+
export class OpenClawAdapter {
|
|
8
|
+
name = "openclaw";
|
|
9
|
+
displayName = "OpenClaw";
|
|
10
|
+
isInstalled() {
|
|
11
|
+
return existsSync(BASE);
|
|
12
|
+
}
|
|
13
|
+
getSkillsDir() {
|
|
14
|
+
return SKILLS_DIR;
|
|
15
|
+
}
|
|
16
|
+
getMcpConfigPath() {
|
|
17
|
+
return MCP_CONFIG;
|
|
18
|
+
}
|
|
19
|
+
installSkills(skills) {
|
|
20
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
const dir = join(SKILLS_DIR, skill.name);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(join(dir, "SKILL.md"), skill.content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
installMcp(config) {
|
|
28
|
+
mkdirSync(BASE, { recursive: true });
|
|
29
|
+
let existing = {};
|
|
30
|
+
if (existsSync(MCP_CONFIG)) {
|
|
31
|
+
try {
|
|
32
|
+
existing = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
}
|
|
36
|
+
existing.mcpServers = existing.mcpServers ?? {};
|
|
37
|
+
existing.mcpServers.bluekiwi = config;
|
|
38
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(existing, null, 2));
|
|
39
|
+
}
|
|
40
|
+
uninstall() {
|
|
41
|
+
for (const name of ["bk-start", "bk-next", "bk-status", "bk-rewind"]) {
|
|
42
|
+
const dir = join(SKILLS_DIR, name);
|
|
43
|
+
if (existsSync(dir)) {
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(MCP_CONFIG)) {
|
|
48
|
+
try {
|
|
49
|
+
const config = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
50
|
+
delete config.mcpServers?.bluekiwi;
|
|
51
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(config, null, 2));
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
const BASE = join(homedir(), ".opencode");
|
|
5
|
+
const SKILLS_DIR = join(BASE, "skills");
|
|
6
|
+
const MCP_CONFIG = join(BASE, "mcp.json");
|
|
7
|
+
export class OpenCodeAdapter {
|
|
8
|
+
name = "opencode";
|
|
9
|
+
displayName = "OpenCode";
|
|
10
|
+
isInstalled() {
|
|
11
|
+
return existsSync(BASE);
|
|
12
|
+
}
|
|
13
|
+
getSkillsDir() {
|
|
14
|
+
return SKILLS_DIR;
|
|
15
|
+
}
|
|
16
|
+
getMcpConfigPath() {
|
|
17
|
+
return MCP_CONFIG;
|
|
18
|
+
}
|
|
19
|
+
installSkills(skills) {
|
|
20
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
const dir = join(SKILLS_DIR, skill.name);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(join(dir, "SKILL.md"), skill.content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
installMcp(config) {
|
|
28
|
+
mkdirSync(BASE, { recursive: true });
|
|
29
|
+
let existing = {};
|
|
30
|
+
if (existsSync(MCP_CONFIG)) {
|
|
31
|
+
try {
|
|
32
|
+
existing = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
}
|
|
36
|
+
existing.mcpServers = existing.mcpServers ?? {};
|
|
37
|
+
existing.mcpServers.bluekiwi = config;
|
|
38
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(existing, null, 2));
|
|
39
|
+
}
|
|
40
|
+
uninstall() {
|
|
41
|
+
for (const name of ["bk-start", "bk-next", "bk-status", "bk-rewind"]) {
|
|
42
|
+
const dir = join(SKILLS_DIR, name);
|
|
43
|
+
if (existsSync(dir)) {
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(MCP_CONFIG)) {
|
|
48
|
+
try {
|
|
49
|
+
const config = JSON.parse(readFileSync(MCP_CONFIG, "utf8"));
|
|
50
|
+
delete config.mcpServers?.bluekiwi;
|
|
51
|
+
writeFileSync(MCP_CONFIG, JSON.stringify(config, null, 2));
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bluekiwi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BlueKiwi CLI — install MCP client and skills into your agent runtime",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"bluekiwi": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json && node scripts/bundle-assets.mjs",
|
|
19
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"commander": "^12.0.0",
|
|
23
|
+
"prompts": "^2.4.2",
|
|
24
|
+
"picocolors": "^1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"@types/prompts": "^2.4.9",
|
|
29
|
+
"typescript": "^5.5.0"
|
|
30
|
+
}
|
|
31
|
+
}
|