bluekiwi 0.3.18 → 0.3.19

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.
@@ -1,10 +1,12 @@
1
1
  import prompts from "prompts";
2
2
  import pc from "picocolors";
3
3
  import { BlueKiwiClient } from "../api-client.js";
4
- import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "../assets/index.js";
5
- import { saveConfig } from "../config.js";
4
+ import { createEmptyConfig, loadConfig, normalizeProfileName, saveConfig, upsertProfile, } from "../config.js";
5
+ import { applyProfileToRuntimes } from "../runtime-sync.js";
6
6
  import { detectInstalledAdapters, getAllAdapters } from "../runtimes/detect.js";
7
7
  export async function acceptCommand(token, opts) {
8
+ const profileName = normalizeProfileName(opts.profile);
9
+ const currentConfig = loadConfig() ?? createEmptyConfig();
8
10
  console.log(pc.cyan("→ Validating invite..."));
9
11
  const validateRes = await fetch(`${opts.server}/api/invites/accept/${token}`);
10
12
  if (!validateRes.ok) {
@@ -60,7 +62,8 @@ export async function acceptCommand(token, opts) {
60
62
  const choices = all.map((adapter) => ({
61
63
  title: adapter.displayName,
62
64
  value: adapter.name,
63
- selected: detected.some((detectedAdapter) => detectedAdapter.name === adapter.name),
65
+ selected: currentConfig.runtimes.includes(adapter.name) ||
66
+ detected.some((detectedAdapter) => detectedAdapter.name === adapter.name),
64
67
  disabled: !adapter.isInstalled(),
65
68
  }));
66
69
  const { selected } = (await prompts({
@@ -72,31 +75,26 @@ export async function acceptCommand(token, opts) {
72
75
  }));
73
76
  chosen = selected ?? [];
74
77
  }
75
- const all = getAllAdapters();
76
- for (const name of chosen) {
77
- const adapter = all.find((item) => item.name === name);
78
- if (!adapter)
79
- continue;
80
- console.log(pc.cyan(`→ Installing to ${adapter.displayName}...`));
81
- adapter.installSkills(BUNDLED_SKILLS);
82
- adapter.installMcp({
83
- command: "node",
84
- args: [BUNDLED_MCP_PATH],
85
- env: {
86
- BLUEKIWI_API_URL: opts.server,
87
- BLUEKIWI_API_KEY: result.api_key,
88
- },
89
- });
90
- }
91
- saveConfig({
92
- version: "1.0.0",
78
+ const now = new Date().toISOString();
79
+ const targetProfile = {
80
+ name: profileName,
93
81
  server_url: opts.server,
94
82
  api_key: result.api_key,
95
83
  user: result.user,
96
- runtimes: chosen,
97
- installed_at: new Date().toISOString(),
98
- last_used: new Date().toISOString(),
84
+ installed_at: now,
85
+ last_used: now,
86
+ };
87
+ const nextConfig = upsertProfile({
88
+ ...currentConfig,
89
+ runtimes: Array.from(new Set([...currentConfig.runtimes, ...chosen])),
90
+ }, targetProfile, {
91
+ activate: chosen.length > 0 || Object.keys(currentConfig.profiles).length === 0,
99
92
  });
93
+ if (chosen.length > 0) {
94
+ applyProfileToRuntimes(nextConfig, profileName, chosen);
95
+ }
96
+ saveConfig(nextConfig);
100
97
  console.log(pc.green("\n✓ BlueKiwi installed successfully!"));
98
+ console.log(pc.dim(`Profile: ${profileName}`));
101
99
  console.log(pc.dim("Try /bk-start in your agent runtime to begin."));
102
100
  }
@@ -1,8 +1,8 @@
1
1
  import prompts from "prompts";
2
2
  import pc from "picocolors";
3
3
  import { BlueKiwiClient } from "../api-client.js";
4
- import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "../assets/index.js";
5
- import { loadConfig, saveConfig } from "../config.js";
4
+ import { createEmptyConfig, getProfile, loadConfig, normalizeProfileName, saveConfig, upsertProfile, } from "../config.js";
5
+ import { applyProfileToRuntimes } from "../runtime-sync.js";
6
6
  import { detectInstalledAdapters, getAllAdapters } from "../runtimes/detect.js";
7
7
  function normalizeEnvValue(value) {
8
8
  const trimmed = value?.trim();
@@ -37,8 +37,10 @@ function maskApiKey(key) {
37
37
  }
38
38
  export async function initCommand(options = {}) {
39
39
  const isNonInteractive = options.yes === true || process.stdin.isTTY !== true;
40
+ const profileName = normalizeProfileName(options.profile);
40
41
  // Load existing config for pre-filling prompts
41
- const existingCfg = loadConfig();
42
+ const existingCfg = loadConfig() ?? createEmptyConfig();
43
+ const existingProfile = getProfile(existingCfg, profileName);
42
44
  let server = normalizeEnvValue(options.server) ??
43
45
  normalizeEnvValue(process.env.BLUEKIWI_API_URL) ??
44
46
  normalizeEnvValue(process.env.BLUEKIWI_SERVER);
@@ -51,12 +53,12 @@ export async function initCommand(options = {}) {
51
53
  type: "text",
52
54
  name: "server",
53
55
  message: "BlueKiwi server URL",
54
- initial: existingCfg?.server_url,
56
+ initial: existingProfile?.profile.server_url,
55
57
  });
56
58
  }
57
59
  if (apiKey === undefined) {
58
- const maskedKey = existingCfg?.api_key
59
- ? maskApiKey(existingCfg.api_key)
60
+ const maskedKey = existingProfile?.profile.api_key
61
+ ? maskApiKey(existingProfile.profile.api_key)
60
62
  : undefined;
61
63
  questions.push({
62
64
  type: "text",
@@ -72,11 +74,11 @@ export async function initCommand(options = {}) {
72
74
  // If user kept the masked key (just pressed Enter), restore original
73
75
  if (apiKey === undefined) {
74
76
  const entered = answers.apiKey;
75
- const maskedKey = existingCfg?.api_key
76
- ? maskApiKey(existingCfg.api_key)
77
+ const maskedKey = existingProfile?.profile.api_key
78
+ ? maskApiKey(existingProfile.profile.api_key)
77
79
  : undefined;
78
80
  if (entered && maskedKey && entered === maskedKey) {
79
- apiKey = existingCfg.api_key;
81
+ apiKey = existingProfile.profile.api_key;
80
82
  }
81
83
  else {
82
84
  apiKey = entered;
@@ -91,7 +93,7 @@ export async function initCommand(options = {}) {
91
93
  }
92
94
  const client = new BlueKiwiClient(server, apiKey);
93
95
  await client.request("GET", "/api/workflows");
94
- const me = {
96
+ const me = existingProfile?.profile.user ?? {
95
97
  id: 0,
96
98
  username: "unknown",
97
99
  email: "",
@@ -104,8 +106,9 @@ export async function initCommand(options = {}) {
104
106
  const requestedFromFlags = uniquePreserveOrder(parseCommaSeparatedList(options.runtimes));
105
107
  const requestedFromEnv = uniquePreserveOrder(parseCommaSeparatedList([process.env.BLUEKIWI_RUNTIMES ?? ""]));
106
108
  const requestedRuntimeNames = requestedFromFlags.length > 0 ? requestedFromFlags : requestedFromEnv;
109
+ const hasExplicitRuntimeSelection = requestedRuntimeNames.length > 0;
107
110
  let selectedRuntimeNames = [];
108
- if (requestedRuntimeNames.length > 0) {
111
+ if (hasExplicitRuntimeSelection) {
109
112
  const unknown = requestedRuntimeNames.filter((name) => !adaptersByName.has(name));
110
113
  if (unknown.length > 0) {
111
114
  throw new Error(`Unknown runtime(s): ${unknown.join(", ")}. Valid runtimes: ${validRuntimeNames.join(", ")}`);
@@ -120,6 +123,9 @@ export async function initCommand(options = {}) {
120
123
  }
121
124
  selectedRuntimeNames = requestedRuntimeNames;
122
125
  }
126
+ else if (existingCfg.runtimes.length > 0) {
127
+ selectedRuntimeNames = existingCfg.runtimes;
128
+ }
123
129
  else if (isNonInteractive) {
124
130
  if (detected.length === 0) {
125
131
  throw new Error("Non-interactive mode: at least one runtime is required (--runtime <name>) or install a supported runtime");
@@ -140,25 +146,26 @@ export async function initCommand(options = {}) {
140
146
  }));
141
147
  selectedRuntimeNames = selected ?? [];
142
148
  }
143
- for (const name of selectedRuntimeNames) {
144
- const adapter = all.find((item) => item.name === name);
145
- if (!adapter)
146
- continue;
147
- adapter.installSkills(BUNDLED_SKILLS);
148
- adapter.installMcp({
149
- command: "node",
150
- args: [BUNDLED_MCP_PATH],
151
- env: { BLUEKIWI_API_URL: server, BLUEKIWI_API_KEY: apiKey },
152
- });
153
- }
154
- saveConfig({
155
- version: "1.0.0",
149
+ const now = new Date().toISOString();
150
+ const targetProfile = {
151
+ name: profileName,
156
152
  server_url: server,
157
153
  api_key: apiKey,
158
154
  user: me,
159
- runtimes: selectedRuntimeNames,
160
- installed_at: new Date().toISOString(),
161
- last_used: new Date().toISOString(),
162
- });
155
+ installed_at: existingProfile?.profile.installed_at ?? now,
156
+ last_used: now,
157
+ };
158
+ const shouldActivate = hasExplicitRuntimeSelection ||
159
+ existingCfg.active_profile === profileName ||
160
+ Object.keys(existingCfg.profiles).length === 0;
161
+ const nextConfig = upsertProfile({
162
+ ...existingCfg,
163
+ runtimes: Array.from(new Set([...existingCfg.runtimes, ...selectedRuntimeNames])),
164
+ }, targetProfile, { activate: shouldActivate });
165
+ if (shouldActivate && nextConfig.runtimes.length > 0) {
166
+ applyProfileToRuntimes(nextConfig, profileName, nextConfig.runtimes);
167
+ }
168
+ saveConfig(nextConfig);
163
169
  console.log(pc.green("✓ BlueKiwi connected"));
170
+ console.log(pc.dim(`Profile: ${profileName}`));
164
171
  }
@@ -1,12 +1,33 @@
1
1
  import pc from "picocolors";
2
- import { clearConfig, loadConfig } from "../config.js";
2
+ import { clearConfig, loadConfig, removeProfile, saveConfig } from "../config.js";
3
3
  import { getAllAdapters } from "../runtimes/detect.js";
4
- export async function logoutCommand() {
4
+ import { applyProfileToRuntimes } from "../runtime-sync.js";
5
+ export async function logoutCommand(profileName) {
5
6
  const cfg = loadConfig();
6
7
  if (!cfg) {
7
8
  console.log(pc.yellow("Already logged out."));
8
9
  return;
9
10
  }
11
+ if (profileName) {
12
+ const next = removeProfile(cfg, profileName);
13
+ if (!next) {
14
+ for (const adapter of getAllAdapters()) {
15
+ if (cfg.runtimes.includes(adapter.name)) {
16
+ adapter.uninstall();
17
+ console.log(pc.dim(` removed ${adapter.displayName}`));
18
+ }
19
+ }
20
+ clearConfig();
21
+ console.log(pc.green(`✓ Removed profile '${profileName}' and logged out.`));
22
+ return;
23
+ }
24
+ saveConfig(next);
25
+ if (profileName === cfg.active_profile && next.runtimes.length > 0) {
26
+ applyProfileToRuntimes(next, next.active_profile);
27
+ }
28
+ console.log(pc.green(`✓ Removed profile '${profileName}'. Active profile: ${next.active_profile}`));
29
+ return;
30
+ }
10
31
  for (const adapter of getAllAdapters()) {
11
32
  if (cfg.runtimes.includes(adapter.name)) {
12
33
  adapter.uninstall();
@@ -0,0 +1,50 @@
1
+ import pc from "picocolors";
2
+ import { clearConfig, loadConfig, removeProfile, requireConfig, requireProfile, saveConfig, } from "../config.js";
3
+ import { getAllAdapters } from "../runtimes/detect.js";
4
+ import { applyProfileToRuntimes } from "../runtime-sync.js";
5
+ async function list() {
6
+ const cfg = loadConfig();
7
+ if (!cfg) {
8
+ console.log(pc.yellow("No profiles configured."));
9
+ return;
10
+ }
11
+ for (const [name, profile] of Object.entries(cfg.profiles)) {
12
+ const marker = name === cfg.active_profile ? pc.green("●") : pc.dim("○");
13
+ console.log(`${marker} ${name} ${pc.dim(profile.server_url)} ${profile.user.username} (${profile.user.role})`);
14
+ }
15
+ }
16
+ async function use(name) {
17
+ const cfg = requireConfig();
18
+ requireProfile(cfg, name);
19
+ const next = {
20
+ ...cfg,
21
+ active_profile: name,
22
+ };
23
+ if (next.runtimes.length > 0) {
24
+ applyProfileToRuntimes(next, name);
25
+ }
26
+ saveConfig(next);
27
+ console.log(pc.green(`✓ Active profile switched to '${name}'`));
28
+ }
29
+ async function remove(name) {
30
+ const cfg = requireConfig();
31
+ requireProfile(cfg, name);
32
+ const next = removeProfile(cfg, name);
33
+ if (!next) {
34
+ for (const adapter of getAllAdapters()) {
35
+ if (cfg.runtimes.includes(adapter.name)) {
36
+ adapter.uninstall();
37
+ console.log(pc.dim(` removed ${adapter.displayName}`));
38
+ }
39
+ }
40
+ clearConfig();
41
+ console.log(pc.green(`✓ Removed profile '${name}' and cleared config.`));
42
+ return;
43
+ }
44
+ saveConfig(next);
45
+ if (cfg.active_profile === name && next.runtimes.length > 0) {
46
+ applyProfileToRuntimes(next, next.active_profile);
47
+ }
48
+ console.log(pc.green(`✓ Removed profile '${name}'`));
49
+ }
50
+ export const profileCommand = { list, use, remove };
@@ -1,7 +1,7 @@
1
1
  import { getAllAdapters } from "../runtimes/detect.js";
2
2
  import pc from "picocolors";
3
- import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "../assets/index.js";
4
- import { loadConfig, requireConfig, saveConfig } from "../config.js";
3
+ import { loadConfig, requireConfig, requireProfile, saveConfig, } from "../config.js";
4
+ import { applyProfileToRuntimes } from "../runtime-sync.js";
5
5
  async function list() {
6
6
  const cfg = loadConfig();
7
7
  const installed = new Set(cfg?.runtimes ?? []);
@@ -15,23 +15,21 @@ async function list() {
15
15
  console.log(`${adapter.displayName.padEnd(14)} ${detected} ${active}`);
16
16
  }
17
17
  }
18
- async function add(name) {
18
+ async function add(name, profileName) {
19
19
  const cfg = requireConfig();
20
+ const { name: resolvedProfile } = requireProfile(cfg, profileName);
20
21
  const adapter = getAllAdapters().find((item) => item.name === name);
21
22
  if (!adapter) {
22
23
  console.error(`Unknown runtime: ${name}`);
23
24
  process.exit(1);
24
25
  }
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({
26
+ const next = {
32
27
  ...cfg,
28
+ active_profile: resolvedProfile,
33
29
  runtimes: Array.from(new Set([...cfg.runtimes, name])),
34
- });
30
+ };
31
+ applyProfileToRuntimes(next, resolvedProfile, [name]);
32
+ saveConfig(next);
35
33
  console.log(pc.green(`✓ Installed to ${adapter.displayName}`));
36
34
  }
37
35
  async function remove(name) {
@@ -1,17 +1,19 @@
1
1
  import pc from "picocolors";
2
2
  import { BlueKiwiClient } from "../api-client.js";
3
- import { CONFIG_PATH, loadConfig } from "../config.js";
4
- export async function statusCommand() {
3
+ import { CONFIG_PATH, loadConfig, requireProfile } from "../config.js";
4
+ export async function statusCommand(profileName) {
5
5
  const cfg = loadConfig();
6
6
  if (!cfg) {
7
7
  console.log(pc.yellow(`Not authenticated. No config at ${CONFIG_PATH}.`));
8
8
  process.exit(1);
9
9
  }
10
- console.log(`${pc.bold("Server:")} ${cfg.server_url}`);
11
- console.log(`${pc.bold("User:")} ${cfg.user.username} (${cfg.user.role})`);
10
+ const { name, profile } = requireProfile(cfg, profileName);
11
+ console.log(`${pc.bold("Profile:")} ${name}${name === cfg.active_profile ? " (active)" : ""}`);
12
+ console.log(`${pc.bold("Server:")} ${profile.server_url}`);
13
+ console.log(`${pc.bold("User:")} ${profile.user.username} (${profile.user.role})`);
12
14
  console.log(`${pc.bold("Runtimes:")} ${cfg.runtimes.join(", ") || "(none)"}`);
13
15
  try {
14
- const client = new BlueKiwiClient(cfg.server_url, cfg.api_key);
16
+ const client = new BlueKiwiClient(profile.server_url, profile.api_key);
15
17
  await client.request("GET", "/api/workflows");
16
18
  console.log(pc.green("✓ Connection OK"));
17
19
  }
@@ -1,8 +1,7 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import pc from "picocolors";
3
- import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "../assets/index.js";
4
3
  import { loadConfig, saveConfig } from "../config.js";
5
- import { getAllAdapters } from "../runtimes/detect.js";
4
+ import { applyProfileToRuntimes, pruneBundledSkills } from "../runtime-sync.js";
6
5
  export async function upgradeCommand() {
7
6
  console.log(pc.cyan("→ Upgrading bluekiwi..."));
8
7
  execFileSync("npm", ["install", "-g", "bluekiwi@latest"], {
@@ -13,18 +12,20 @@ export async function upgradeCommand() {
13
12
  console.log(pc.yellow("No config found. Run `bluekiwi accept` or `bluekiwi init` next."));
14
13
  return;
15
14
  }
16
- const bundledNames = new Set(BUNDLED_SKILLS.map((s) => s.name));
17
- for (const adapter of getAllAdapters()) {
18
- if (!cfg.runtimes.includes(adapter.name))
19
- continue;
20
- adapter.installSkills(BUNDLED_SKILLS);
21
- adapter.pruneSkills(bundledNames);
22
- adapter.installMcp({
23
- command: "node",
24
- args: [BUNDLED_MCP_PATH],
25
- env: { BLUEKIWI_API_URL: cfg.server_url, BLUEKIWI_API_KEY: cfg.api_key },
26
- });
27
- }
28
- saveConfig({ ...cfg, last_used: new Date().toISOString() });
15
+ pruneBundledSkills(cfg);
16
+ applyProfileToRuntimes(cfg, cfg.active_profile);
17
+ const active = cfg.profiles[cfg.active_profile];
18
+ saveConfig({
19
+ ...cfg,
20
+ profiles: active
21
+ ? {
22
+ ...cfg.profiles,
23
+ [cfg.active_profile]: {
24
+ ...active,
25
+ last_used: new Date().toISOString(),
26
+ },
27
+ }
28
+ : cfg.profiles,
29
+ });
29
30
  console.log(pc.green("✓ Upgraded and reinstalled assets."));
30
31
  }
package/dist/config.js CHANGED
@@ -1,20 +1,126 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync, } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join, dirname } from "path";
4
+ const CONFIG_VERSION = "2.0.0";
5
+ export const DEFAULT_PROFILE = "default";
4
6
  export const CONFIG_PATH = join(homedir(), ".bluekiwi", "config.json");
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+ function coerceUser(value) {
11
+ const raw = (value ?? {});
12
+ return {
13
+ id: typeof raw.id === "number" ? raw.id : 0,
14
+ username: typeof raw.username === "string" ? raw.username : "unknown",
15
+ email: typeof raw.email === "string" ? raw.email : "",
16
+ role: typeof raw.role === "string" ? raw.role : "viewer",
17
+ };
18
+ }
19
+ function coerceRuntimes(value) {
20
+ if (!Array.isArray(value))
21
+ return [];
22
+ return value.filter((item) => typeof item === "string");
23
+ }
24
+ function normalizeProfileName(name) {
25
+ const trimmed = name?.trim();
26
+ return trimmed ? trimmed : DEFAULT_PROFILE;
27
+ }
28
+ function isLegacyConfig(value) {
29
+ const raw = value;
30
+ return !!raw && typeof raw.server_url === "string" && typeof raw.api_key === "string";
31
+ }
32
+ function normalizeProfile(name, raw) {
33
+ if (!raw ||
34
+ typeof raw.server_url !== "string" ||
35
+ typeof raw.api_key !== "string") {
36
+ return null;
37
+ }
38
+ return {
39
+ name,
40
+ server_url: raw.server_url,
41
+ api_key: raw.api_key,
42
+ user: coerceUser(raw.user),
43
+ installed_at: typeof raw.installed_at === "string" ? raw.installed_at : nowIso(),
44
+ last_used: typeof raw.last_used === "string" ? raw.last_used : nowIso(),
45
+ };
46
+ }
47
+ function migrateLegacyConfig(raw) {
48
+ const profile = normalizeProfile(DEFAULT_PROFILE, {
49
+ name: DEFAULT_PROFILE,
50
+ server_url: raw.server_url,
51
+ api_key: raw.api_key,
52
+ user: raw.user,
53
+ installed_at: raw.installed_at,
54
+ last_used: raw.last_used,
55
+ });
56
+ return {
57
+ version: CONFIG_VERSION,
58
+ active_profile: DEFAULT_PROFILE,
59
+ profiles: profile ? { [DEFAULT_PROFILE]: profile } : {},
60
+ runtimes: coerceRuntimes(raw.runtimes),
61
+ };
62
+ }
63
+ function normalizeConfig(raw) {
64
+ if (isLegacyConfig(raw)) {
65
+ return migrateLegacyConfig(raw);
66
+ }
67
+ const value = raw;
68
+ if (!value || typeof value !== "object" || value.profiles == null) {
69
+ return null;
70
+ }
71
+ const profiles = {};
72
+ for (const [name, profileValue] of Object.entries(value.profiles)) {
73
+ const normalized = normalizeProfile(name, profileValue);
74
+ if (normalized)
75
+ profiles[name] = normalized;
76
+ }
77
+ const profileNames = Object.keys(profiles);
78
+ if (profileNames.length === 0)
79
+ return null;
80
+ const requestedActive = typeof value.active_profile === "string" ? value.active_profile : undefined;
81
+ const activeProfile = profiles[requestedActive ?? ""]
82
+ ? requestedActive
83
+ : profiles[DEFAULT_PROFILE]
84
+ ? DEFAULT_PROFILE
85
+ : profileNames[0];
86
+ return {
87
+ version: typeof value.version === "string" ? value.version : CONFIG_VERSION,
88
+ active_profile: activeProfile,
89
+ profiles,
90
+ runtimes: coerceRuntimes(value.runtimes),
91
+ };
92
+ }
5
93
  export function loadConfig() {
6
94
  if (!existsSync(CONFIG_PATH))
7
95
  return null;
8
96
  try {
9
- return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
97
+ const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
98
+ const normalized = normalizeConfig(raw);
99
+ if (!normalized)
100
+ return null;
101
+ if (JSON.stringify(raw, null, 2) !== JSON.stringify(normalized, null, 2)) {
102
+ saveConfig(normalized);
103
+ }
104
+ return normalized;
10
105
  }
11
106
  catch {
12
107
  return null;
13
108
  }
14
109
  }
110
+ export function createEmptyConfig() {
111
+ return {
112
+ version: CONFIG_VERSION,
113
+ active_profile: DEFAULT_PROFILE,
114
+ profiles: {},
115
+ runtimes: [],
116
+ };
117
+ }
15
118
  export function saveConfig(config) {
16
119
  mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
17
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
120
+ writeFileSync(CONFIG_PATH, JSON.stringify({
121
+ ...config,
122
+ version: CONFIG_VERSION,
123
+ }, null, 2), { mode: 0o600 });
18
124
  chmodSync(CONFIG_PATH, 0o600);
19
125
  }
20
126
  export function clearConfig() {
@@ -28,3 +134,51 @@ export function requireConfig() {
28
134
  }
29
135
  return config;
30
136
  }
137
+ export function getProfile(config, profileName) {
138
+ const resolved = normalizeProfileName(profileName ?? config.active_profile);
139
+ const profile = config.profiles[resolved];
140
+ if (!profile)
141
+ return null;
142
+ return { name: resolved, profile };
143
+ }
144
+ export function requireProfile(config, profileName) {
145
+ const resolved = getProfile(config, profileName);
146
+ if (!resolved) {
147
+ throw new Error(`Unknown profile '${normalizeProfileName(profileName)}'. Use \`bluekiwi profile list\` to see configured profiles.`);
148
+ }
149
+ return resolved;
150
+ }
151
+ export function upsertProfile(config, profile, options) {
152
+ const next = {
153
+ ...config,
154
+ profiles: {
155
+ ...config.profiles,
156
+ [profile.name]: profile,
157
+ },
158
+ };
159
+ if (options?.activate) {
160
+ next.active_profile = profile.name;
161
+ }
162
+ else if (!next.active_profile) {
163
+ next.active_profile = profile.name;
164
+ }
165
+ return next;
166
+ }
167
+ export function removeProfile(config, profileName) {
168
+ const nextProfiles = { ...config.profiles };
169
+ delete nextProfiles[profileName];
170
+ const remaining = Object.keys(nextProfiles);
171
+ if (remaining.length === 0)
172
+ return null;
173
+ const nextActive = config.active_profile === profileName
174
+ ? nextProfiles[DEFAULT_PROFILE]
175
+ ? DEFAULT_PROFILE
176
+ : remaining[0]
177
+ : config.active_profile;
178
+ return {
179
+ ...config,
180
+ active_profile: nextActive,
181
+ profiles: nextProfiles,
182
+ };
183
+ }
184
+ export { normalizeProfileName };
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { upgradeCommand } from "./commands/upgrade.js";
8
8
  import { logoutCommand } from "./commands/logout.js";
9
9
  import { runtimesCommand } from "./commands/runtimes.js";
10
10
  import { devLinkCommand } from "./commands/dev-link.js";
11
+ import { profileCommand } from "./commands/profile.js";
11
12
  const require = createRequire(import.meta.url);
12
13
  const pkg = require("../package.json");
13
14
  function splitCommaSeparatedList(value) {
@@ -27,6 +28,7 @@ program
27
28
  program
28
29
  .command("accept <token>")
29
30
  .requiredOption("--server <url>", "BlueKiwi server URL")
31
+ .option("--profile <name>", "Profile name (default: default)")
30
32
  .option("--username <name>", "Username (non-interactive)")
31
33
  .option("--password <pass>", "Password (non-interactive)")
32
34
  .action(acceptCommand);
@@ -34,20 +36,35 @@ program
34
36
  .command("init")
35
37
  .option("--server <url>", "BlueKiwi server URL")
36
38
  .option("--api-key <key>", "API key (bk_...)")
39
+ .option("--profile <name>", "Profile name (default: default)")
37
40
  .option("--runtime <name>", "Runtime to install into (repeatable, or comma-separated)", collectRuntimes, [])
38
41
  .option("--yes", "Suppress all prompts (non-interactive)")
39
42
  .action((opts) => initCommand({
40
43
  server: opts.server,
41
44
  apiKey: opts.apiKey,
42
45
  runtimes: opts.runtime?.length ? opts.runtime : undefined,
46
+ profile: opts.profile,
43
47
  yes: opts.yes,
44
48
  }));
45
- program.command("status").action(statusCommand);
49
+ program
50
+ .command("status")
51
+ .option("--profile <name>", "Profile name (default: active profile)")
52
+ .action((opts) => statusCommand(opts.profile));
46
53
  program.command("upgrade").action(upgradeCommand);
47
- program.command("logout").action(logoutCommand);
54
+ program
55
+ .command("logout")
56
+ .option("--profile <name>", "Remove only one profile")
57
+ .action((opts) => logoutCommand(opts.profile));
48
58
  program.command("runtimes").action(runtimesCommand.list);
49
- program.command("runtimes:add <name>").action(runtimesCommand.add);
59
+ program
60
+ .command("runtimes:add <name>")
61
+ .option("--profile <name>", "Profile to install into runtimes and set active")
62
+ .action((name, opts) => runtimesCommand.add(name, opts.profile));
50
63
  program.command("runtimes:remove <name>").action(runtimesCommand.remove);
64
+ program.command("profile").action(profileCommand.list);
65
+ program.command("profile:list").action(profileCommand.list);
66
+ program.command("profile:use <name>").action(profileCommand.use);
67
+ program.command("profile:remove <name>").action(profileCommand.remove);
51
68
  program.command("dev-link").action(devLinkCommand);
52
69
  program.parseAsync(process.argv).catch((err) => {
53
70
  console.error(err.message);
@@ -0,0 +1,34 @@
1
+ import { BUNDLED_MCP_PATH, BUNDLED_SKILLS } from "./assets/index.js";
2
+ import { requireProfile, } from "./config.js";
3
+ import { getAllAdapters } from "./runtimes/detect.js";
4
+ export function applyProfileToRuntimes(config, profileName, runtimeNames) {
5
+ const { profile } = requireProfile(config, profileName);
6
+ const targetNames = runtimeNames ?? config.runtimes;
7
+ const allAdapters = getAllAdapters();
8
+ const installed = [];
9
+ for (const name of targetNames) {
10
+ const adapter = allAdapters.find((item) => item.name === name);
11
+ if (!adapter)
12
+ continue;
13
+ adapter.installSkills(BUNDLED_SKILLS);
14
+ adapter.installMcp({
15
+ command: "node",
16
+ args: [BUNDLED_MCP_PATH],
17
+ env: {
18
+ BLUEKIWI_API_URL: profile.server_url,
19
+ BLUEKIWI_API_KEY: profile.api_key,
20
+ },
21
+ });
22
+ installed.push(name);
23
+ }
24
+ return installed;
25
+ }
26
+ export function pruneBundledSkills(config) {
27
+ const bundledNames = new Set(BUNDLED_SKILLS.map((skill) => skill.name));
28
+ for (const adapter of getAllAdapters()) {
29
+ if (!config.runtimes.includes(adapter.name))
30
+ continue;
31
+ adapter.installSkills(BUNDLED_SKILLS);
32
+ adapter.pruneSkills(bundledNames);
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluekiwi",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "BlueKiwi CLI — install MCP client and skills into your agent runtime",
5
5
  "license": "MIT",
6
6
  "repository": {