@botconnector/bccli 0.4.0-beta.36 → 0.4.0-beta.38

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.
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ t
4
+ } from "./chunk-4RMP4CNT.js";
5
+
6
+ // src/presets.ts
7
+ var PRESETS = [
8
+ { id: "bc-cloud", name: "BotConnector Cloud", baseURL: "https://api.botconnector.id/v1", apiKeyEnv: "BOTCONNECTOR_API_KEY" },
9
+ { id: "openrouter", name: "OpenRouter", baseURL: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY" },
10
+ { id: "openai", name: "OpenAI", baseURL: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY" },
11
+ { id: "gemini", name: "Google Gemini", baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", apiKeyEnv: "GEMINI_API_KEY" },
12
+ { id: "deepseek", name: "DeepSeek", baseURL: "https://api.deepseek.com/v1", apiKeyEnv: "DEEPSEEK_API_KEY" },
13
+ { id: "groq", name: "Groq", baseURL: "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_API_KEY" },
14
+ { id: "ollama-cloud", name: "Ollama Cloud", baseURL: "https://ollama.com/v1", apiKeyEnv: "OLLAMA_API_KEY" },
15
+ { id: "ollama", name: "Ollama lokal", baseURL: "http://127.0.0.1:11434/v1", local: true },
16
+ { id: "lmstudio", name: "LM Studio lokal", baseURL: "http://127.0.0.1:1234/v1", local: true }
17
+ ];
18
+
19
+ // src/config.ts
20
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
21
+ import { homedir } from "os";
22
+ import { join } from "path";
23
+ var ConfigError = class extends Error {
24
+ };
25
+ var DEFAULT_MAX_STEPS = 100;
26
+ var DEFAULT_CONFIG = {
27
+ model: "bc-cloud/glm-5.3-flash",
28
+ permissionMode: "default",
29
+ reasoning: "auto",
30
+ vision: true,
31
+ networkPolicy: "allow",
32
+ providers: Object.fromEntries(PRESETS.map((p) => [p.id, { baseURL: p.baseURL, apiKeyEnv: p.apiKeyEnv }])),
33
+ allow: []
34
+ };
35
+ function bccliHome(env = process.env) {
36
+ return env.BCCLI_HOME ?? join(homedir(), ".bccli");
37
+ }
38
+ function readJsonConfig(path) {
39
+ if (!existsSync(path)) return {};
40
+ try {
41
+ return JSON.parse(readFileSync(path, "utf8"));
42
+ } catch (error) {
43
+ throw new ConfigError(t("Invalid config: {path} ({error})", { path, error: error.message }));
44
+ }
45
+ }
46
+ function resolvedMaxSteps(global) {
47
+ const value = global.maxSteps;
48
+ if (value === void 0) return DEFAULT_MAX_STEPS;
49
+ if (value === null || value === 0) return null;
50
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
51
+ throw new ConfigError(t("maxSteps must be a positive number, or 0/null to disable the limit."));
52
+ }
53
+ return Math.max(1, Math.floor(value));
54
+ }
55
+ function resolvedNetworkPolicy(global) {
56
+ const value = global.networkPolicy ?? "allow";
57
+ if (value !== "allow" && value !== "offline") {
58
+ throw new ConfigError(t('networkPolicy must be "allow" or "offline".'));
59
+ }
60
+ return value;
61
+ }
62
+ function resolvedPlugins(global) {
63
+ const value = global.plugins;
64
+ if (value === void 0) return [];
65
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
66
+ throw new ConfigError(t("plugins must be an array of strings."));
67
+ }
68
+ return [...value];
69
+ }
70
+ function untrustedProviders(project, known) {
71
+ const out = {};
72
+ for (const [id, provider] of Object.entries(project.providers ?? {})) {
73
+ if (known[id] || typeof provider?.baseURL !== "string") continue;
74
+ out[id] = { baseURL: provider.baseURL };
75
+ }
76
+ return out;
77
+ }
78
+ function loadConfig(cwd, env = process.env) {
79
+ const global = readJsonConfig(join(bccliHome(env), "config.json"));
80
+ const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
81
+ const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
82
+ const fromProject = untrustedProviders(project, known);
83
+ return {
84
+ model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
85
+ permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
86
+ reasoning: global.reasoning ?? DEFAULT_CONFIG.reasoning,
87
+ maxSteps: resolvedMaxSteps(global),
88
+ hooks: global.hooks,
89
+ vision: global.vision ?? DEFAULT_CONFIG.vision,
90
+ networkPolicy: resolvedNetworkPolicy(global),
91
+ plugins: resolvedPlugins(global),
92
+ providers: { ...known, ...fromProject },
93
+ projectProviders: Object.keys(fromProject),
94
+ allow: [...global.allow ?? []]
95
+ };
96
+ }
97
+ function readCredentials(env = process.env) {
98
+ const path = join(bccliHome(env), "credentials");
99
+ if (!existsSync(path)) return {};
100
+ try {
101
+ return JSON.parse(readFileSync(path, "utf8"));
102
+ } catch {
103
+ return {};
104
+ }
105
+ }
106
+ function saveCredential(providerId, key, env = process.env) {
107
+ const home = bccliHome(env);
108
+ mkdirSync(home, { recursive: true });
109
+ const path = join(home, "credentials");
110
+ writeFileSync(path, JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), { mode: 384 });
111
+ chmodSync(path, 384);
112
+ }
113
+ function resolveModel(config, modelRef, env = process.env) {
114
+ const slash = modelRef.indexOf("/");
115
+ if (slash <= 0 || slash === modelRef.length - 1) {
116
+ throw new ConfigError(t("Model must be in provider/model format, e.g. bc-cloud/glm-5.3-flash (got: {ref})", { ref: modelRef }));
117
+ }
118
+ const providerId = modelRef.slice(0, slash);
119
+ const provider = config.providers[providerId];
120
+ if (!provider) {
121
+ throw new ConfigError(t('Provider "{id}" is not in the config. Available: {list}', { id: providerId, list: Object.keys(config.providers).join(", ") }));
122
+ }
123
+ const apiKey = (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[providerId] || void 0;
124
+ if (provider.apiKeyEnv && !apiKey) {
125
+ throw new ConfigError(t("No API key for {id} yet. Run `bccli login` or set the env var {env}.", { id: providerId, env: provider.apiKeyEnv }));
126
+ }
127
+ return { providerId, model: modelRef.slice(slash + 1), baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey };
128
+ }
129
+ function removeCredential(providerId, env = process.env) {
130
+ const creds = readCredentials(env);
131
+ if (!(providerId in creds)) return false;
132
+ delete creds[providerId];
133
+ const path = join(bccliHome(env), "credentials");
134
+ writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 384 });
135
+ chmodSync(path, 384);
136
+ return true;
137
+ }
138
+
139
+ export {
140
+ PRESETS,
141
+ ConfigError,
142
+ bccliHome,
143
+ readJsonConfig,
144
+ loadConfig,
145
+ readCredentials,
146
+ saveCredential,
147
+ resolveModel,
148
+ removeCredential
149
+ };
@@ -1,20 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- hasKey,
4
- providerName,
5
- removeGlobalProvider,
6
- writeGlobalConfig
7
- } from "./chunk-3DHJO7NC.js";
8
2
  import {
9
3
  reasoningPayload
10
- } from "./chunk-3NTTXK4Y.js";
4
+ } from "./chunk-YURAIW7A.js";
11
5
  import {
12
- PRESETS,
13
- loadConfig,
14
- readCredentials,
15
- saveCredential,
16
6
  t
17
- } from "./chunk-CZMTWXCE.js";
7
+ } from "./chunk-4RMP4CNT.js";
18
8
 
19
9
  // src/provider.ts
20
10
  import OpenAI from "openai";
@@ -98,15 +88,15 @@ function detectRepetition(text) {
98
88
  }
99
89
  function sleep(ms, signal) {
100
90
  return new Promise((resolve, reject) => {
101
- const timer = setTimeout(resolve, ms);
102
- signal?.addEventListener(
103
- "abort",
104
- () => {
105
- clearTimeout(timer);
106
- reject(signal.reason);
107
- },
108
- { once: true }
109
- );
91
+ const onAbort = () => {
92
+ clearTimeout(timer);
93
+ reject(signal?.reason);
94
+ };
95
+ const timer = setTimeout(() => {
96
+ signal?.removeEventListener("abort", onAbort);
97
+ resolve();
98
+ }, ms);
99
+ signal?.addEventListener("abort", onAbort, { once: true });
110
100
  });
111
101
  }
112
102
  function errorMessage(body) {
@@ -205,11 +195,38 @@ async function readJson(res) {
205
195
  const thinking = (choice?.message?.reasoning_content || choice?.message?.reasoning || "") + split.thinking;
206
196
  return { text: split.text, toolCalls, thinking: thinking || void 0, usage: toUsage(body.usage), finishReason: choice?.finish_reason };
207
197
  }
198
+ function contentText(content) {
199
+ if (typeof content === "string") return content;
200
+ const text = content.filter((part) => part.type === "text").map((part) => part.text).join(" ").trim();
201
+ const hasImage = content.some((part) => part.type === "image_url");
202
+ if (!hasImage) return text;
203
+ return text ? `${text} [image]` : "[image]";
204
+ }
205
+ function imageUserMessage(images) {
206
+ const paths = images.map((image) => image.path);
207
+ const caption = paths.length === 1 ? `Image read from ${paths[0]}.` : `Images read from ${paths.join(", ")}.`;
208
+ return {
209
+ role: "user",
210
+ content: [
211
+ { type: "text", text: caption },
212
+ ...images.map((image) => ({ type: "image_url", image_url: { url: `data:${image.mediaType};base64,${image.data}` } }))
213
+ ]
214
+ };
215
+ }
208
216
  function responseInputFromMessages(messages) {
209
217
  const input = [];
210
218
  for (const message of messages) {
211
219
  if (message.role === "system" || message.role === "user") {
212
- input.push({ role: message.role, content: message.content });
220
+ if (Array.isArray(message.content)) {
221
+ input.push({
222
+ role: message.role,
223
+ content: message.content.map(
224
+ (part) => part.type === "image_url" ? { type: "input_image", image_url: part.image_url.url } : { type: "input_text", text: part.text }
225
+ )
226
+ });
227
+ } else {
228
+ input.push({ role: message.role, content: message.content });
229
+ }
213
230
  continue;
214
231
  }
215
232
  if (message.role === "assistant") {
@@ -249,7 +266,7 @@ function shouldForceLunaPtc(messages, enabled) {
249
266
  for (let i = messages.length - 1; i >= 0; i--) {
250
267
  const message = messages[i];
251
268
  if (message.role === "assistant" && message.responses_output_items?.some((item) => item.type === "program")) return true;
252
- if (message.role === "user") return LUNA_PTC_FORCE_CUE.test(message.content);
269
+ if (message.role === "user") return LUNA_PTC_FORCE_CUE.test(contentText(message.content));
253
270
  }
254
271
  return false;
255
272
  }
@@ -264,7 +281,7 @@ function shouldEnableLunaPtc(messages, enabled, programmaticToolNames = []) {
264
281
  }
265
282
  const turn = messages.slice(Math.max(0, lastUser));
266
283
  const latestUser = lastUser >= 0 ? messages[lastUser] : void 0;
267
- const prompt = latestUser?.role === "user" ? latestUser.content : "";
284
+ const prompt = latestUser?.role === "user" ? contentText(latestUser.content) : "";
268
285
  if (LUNA_PTC_MULTI_WORK_CUE.test(prompt)) return true;
269
286
  const safeToolNames = /* @__PURE__ */ new Set([...LUNA_PTC_SAFE_FUNCTIONS, ...programmaticToolNames]);
270
287
  const safeCalls = /* @__PURE__ */ new Set();
@@ -852,106 +869,16 @@ function createProvider(options) {
852
869
  const res = await doFetch(`${baseURL}/models`, { headers });
853
870
  if (!res.ok) throw new ProviderError(t("{status} while fetching the model list", { status: res.status }), res.status);
854
871
  const body = await res.json();
855
- return (body.data ?? []).map((m) => m.id).sort();
872
+ const ids = (body.data ?? []).map((m) => m?.id).filter((id) => typeof id === "string" && id.length > 0);
873
+ return [...new Set(ids)].sort();
856
874
  }
857
875
  };
858
876
  }
859
877
 
860
- // src/models.ts
861
- function order(ids) {
862
- const presetIds = PRESETS.map((p) => p.id);
863
- const presets = presetIds.filter((id) => ids.includes(id));
864
- const custom = ids.filter((id) => !presetIds.includes(id)).sort();
865
- return [...presets, ...custom];
866
- }
867
- async function listAllModels(config, env = process.env, opts = {}) {
868
- const timeoutMs = opts.timeoutMs ?? 5e3;
869
- const baseFetch = opts.fetch ?? fetch;
870
- const untrusted = new Set(config.projectProviders ?? []);
871
- const ids = order(Object.keys(config.providers)).filter((id) => opts.only ? id === opts.only : !untrusted.has(id) && hasKey(config, id, env));
872
- const results = await Promise.all(
873
- ids.map(async (id) => {
874
- const provider = config.providers[id];
875
- const apiKey = (opts.keyOverride && id === opts.only ? opts.keyOverride : void 0) || (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[id] || void 0;
876
- const local = !provider.apiKeyEnv;
877
- const timedFetch = ((input, init) => baseFetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
878
- const list = createProvider({ baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey, model: "", fetch: timedFetch }).listModels();
879
- let timer;
880
- const timeout = new Promise((_, reject) => {
881
- timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
882
- });
883
- try {
884
- const models = await Promise.race([list, timeout]);
885
- return { providerId: id, providerName: providerName(config, id), models };
886
- } catch {
887
- if (local && !opts.only) return void 0;
888
- return { providerId: id, providerName: providerName(config, id), models: [], error: t("could not be reached") };
889
- } finally {
890
- clearTimeout(timer);
891
- }
892
- })
893
- );
894
- return results.filter((g) => !!g);
895
- }
896
-
897
- // src/providerCli.ts
898
- async function addProviderKey(providerId, key, deps) {
899
- const config = loadConfig(deps.cwd, deps.env);
900
- const [group] = await listAllModels(config, deps.env, { only: providerId, fetch: deps.fetch, keyOverride: key || void 0 });
901
- if (!group || group.error) return { ok: false, error: t("The API key was rejected or {provider} could not be reached.", { provider: providerName(config, providerId) }) };
902
- if (key) saveCredential(providerId, key, deps.env);
903
- return { ok: true, models: group.models.length };
904
- }
905
- async function runProviderCommand(args, deps) {
906
- const [action = "list", id] = args.subArgs;
907
- const config = loadConfig(deps.cwd, deps.env);
908
- if (action === "list") {
909
- for (const pid of Object.keys(config.providers)) {
910
- const mark = hasKey(config, pid, deps.env) ? "\u2713" : "\u25CB";
911
- deps.out(`${mark} ${pid.padEnd(14)} ${providerName(config, pid).padEnd(20)} ${config.providers[pid].baseURL}`);
912
- }
913
- return 0;
914
- }
915
- if (!id) {
916
- deps.err(t("Usage: bccli provider {action} <id>", { action }));
917
- return 1;
918
- }
919
- if (action === "remove") {
920
- removeGlobalProvider(id, deps.env);
921
- deps.out(t("{id} removed.", { id }));
922
- return 0;
923
- }
924
- if (action !== "add") {
925
- deps.err(t("Unknown action: {action}. Use list, add, or remove.", { action }));
926
- return 1;
927
- }
928
- const preset = PRESETS.find((p) => p.id === id);
929
- if (!preset && !config.providers[id]) {
930
- if (!args.url) {
931
- deps.err(t("{id} is not a preset. For a custom provider add --url <OpenAI-compatible base URL>.", { id }));
932
- return 1;
933
- }
934
- writeGlobalConfig(
935
- { providers: { [id]: { baseURL: args.url, ...args.name ? { name: args.name } : {}, ...args.keyEnv ? { apiKeyEnv: args.keyEnv } : {} } } },
936
- deps.env
937
- );
938
- }
939
- const needsKey = !!loadConfig(deps.cwd, deps.env).providers[id].apiKeyEnv || !preset;
940
- const key = needsKey ? await deps.readSecret(t("API key for {id} (leave empty if not needed): ", { id })) : "";
941
- const result = await addProviderKey(id, key, deps);
942
- if (!result.ok) {
943
- deps.err(result.error);
944
- return 1;
945
- }
946
- deps.out(t("{id} is ready. {n} models available.", { id, n: result.models }));
947
- return 0;
948
- }
949
-
950
878
  export {
879
+ contentText,
880
+ imageUserMessage,
951
881
  hasPendingProgrammaticReplay,
952
882
  completionAssistantMessage,
953
- createProvider,
954
- listAllModels,
955
- addProviderKey,
956
- runProviderCommand
883
+ createProvider
957
884
  };
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ConfigError,
3
+ ConfigError
4
+ } from "./chunk-H5N4ST6B.js";
5
+ import {
4
6
  t
5
- } from "./chunk-CZMTWXCE.js";
7
+ } from "./chunk-4RMP4CNT.js";
6
8
 
7
9
  // src/mcp/catalog.ts
8
10
  var CATALOG = [
@@ -12,6 +14,7 @@ var CATALOG = [
12
14
  {
13
15
  name: "filesystem",
14
16
  description: "access folders outside the project",
17
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: ${dir} is filled by fillTemplate, not a template literal
15
18
  config: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "${dir}"] },
16
19
  inputs: [{ key: "dir", label: "Folder the server may access" }]
17
20
  },
@@ -19,6 +22,7 @@ var CATALOG = [
19
22
  {
20
23
  name: "github",
21
24
  description: "GitHub issues, PRs, and repos",
25
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: ${token} is filled by fillTemplate, not a template literal
22
26
  config: { type: "http", url: "https://api.githubcopilot.com/mcp/", headers: { Authorization: "Bearer ${token}" } },
23
27
  inputs: [{ key: "token", label: "GitHub Personal Access Token", secret: true }]
24
28
  }