@botconnector/bccli 0.4.0-beta.1 → 0.4.0-beta.10

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,347 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConfigError,
4
+ bccliHome,
5
+ loadConfig,
6
+ readCredentials,
7
+ resolveModel
8
+ } from "./chunk-2MGINW4O.js";
9
+
10
+ // src/integrationCli.ts
11
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
12
+ import { dirname, join } from "path";
13
+ import { parseDocument } from "yaml";
14
+ var BASE_URL = "https://api.botconnector.id/v1";
15
+ var PROVIDER_ID = "botconnector";
16
+ function integrationHome(env) {
17
+ return join(bccliHome(env), "integrations");
18
+ }
19
+ function statePath(target, env) {
20
+ return join(integrationHome(env), `${target}.state.json`);
21
+ }
22
+ function keyPath(env) {
23
+ return join(integrationHome(env), "bc-cloud.key");
24
+ }
25
+ function parseTarget(raw) {
26
+ if (raw === "opencode" || raw === "aider" || raw === "cline" || raw === "deepseek-harness") return raw;
27
+ if (raw === "dsh") return "deepseek-harness";
28
+ throw new ConfigError("Agent harus salah satu dari: opencode, aider, cline, deepseek-harness (alias: dsh)");
29
+ }
30
+ function homeDir(env) {
31
+ const value = env.HOME || env.USERPROFILE;
32
+ if (!value) throw new ConfigError("Home directory tidak dapat ditentukan.");
33
+ return value;
34
+ }
35
+ function ensureSecret(env) {
36
+ const key = readCredentials(env)["bc-cloud"] || env.BOTCONNECTOR_API_KEY;
37
+ if (!key) throw new ConfigError("API key BotConnector belum tersedia. Jalankan bccli login bc-cloud terlebih dahulu.");
38
+ const dir = integrationHome(env);
39
+ mkdirSync(dir, { recursive: true });
40
+ const path = keyPath(env);
41
+ writeFileSync(path, `${key}
42
+ `, { mode: 384 });
43
+ try {
44
+ chmodSync(path, 384);
45
+ } catch {
46
+ }
47
+ return path;
48
+ }
49
+ function safeBackup(path, env) {
50
+ if (!existsSync(path)) return { created: true };
51
+ const dir = integrationHome(env);
52
+ mkdirSync(dir, { recursive: true });
53
+ const name = path.split(/[\\/]/).pop() || "config";
54
+ const backup = join(dir, `${name}.${Date.now()}.bak`);
55
+ copyFileSync(path, backup);
56
+ return { backup, created: false };
57
+ }
58
+ function writeState(target, state, env) {
59
+ const path = statePath(target, env);
60
+ mkdirSync(dirname(path), { recursive: true });
61
+ writeFileSync(path, `${JSON.stringify(state, null, 2)}
62
+ `, { mode: 384 });
63
+ }
64
+ function readState(target, env) {
65
+ const path = statePath(target, env);
66
+ if (!existsSync(path)) return null;
67
+ try {
68
+ return JSON.parse(readFileSync(path, "utf8"));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ async function botConnectorModels(deps) {
74
+ const config = loadConfig(deps.cwd);
75
+ const resolved = resolveModel(config, config.model, deps.env);
76
+ const key = readCredentials(deps.env)["bc-cloud"] || resolved.apiKey;
77
+ if (!key) throw new ConfigError("API key BotConnector belum tersedia. Jalankan bccli login bc-cloud terlebih dahulu.");
78
+ const response = await deps.fetch(`${BASE_URL}/models`, {
79
+ headers: { authorization: `Bearer ${key}` }
80
+ });
81
+ if (!response.ok) throw new ConfigError(`Katalog model BotConnector gagal dimuat (HTTP ${response.status}).`);
82
+ const payload = await response.json();
83
+ return (payload.data ?? []).map((item) => item.id).filter((id) => typeof id === "string" && !!id);
84
+ }
85
+ async function connectOpenCode(deps) {
86
+ const home = homeDir(deps.env);
87
+ const path = join(home, ".config", "opencode", "opencode.json");
88
+ const backup = safeBackup(path, deps.env);
89
+ const secret = ensureSecret(deps.env);
90
+ const models = await botConnectorModels(deps);
91
+ let doc = {};
92
+ if (existsSync(path)) {
93
+ try {
94
+ doc = JSON.parse(readFileSync(path, "utf8"));
95
+ } catch {
96
+ throw new ConfigError(`Config OpenCode tidak valid JSON: ${path}`);
97
+ }
98
+ }
99
+ const providers = doc.provider && typeof doc.provider === "object" && !Array.isArray(doc.provider) ? { ...doc.provider } : {};
100
+ providers[PROVIDER_ID] = {
101
+ npm: "@ai-sdk/openai-compatible",
102
+ name: "BotConnector",
103
+ options: {
104
+ baseURL: BASE_URL,
105
+ apiKey: `{file:${secret.replace(/\\/g, "/")}}`
106
+ },
107
+ models: Object.fromEntries(models.map((id) => [id, { name: id }]))
108
+ };
109
+ if (!doc.$schema) doc.$schema = "https://opencode.ai/config.json";
110
+ doc.provider = providers;
111
+ mkdirSync(dirname(path), { recursive: true });
112
+ writeFileSync(path, `${JSON.stringify(doc, null, 2)}
113
+ `);
114
+ writeState("opencode", {
115
+ target: "opencode",
116
+ files: [{ path, ...backup }],
117
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
118
+ }, deps.env);
119
+ deps.out(`OpenCode terhubung ke BotConnector (${models.length} model).`);
120
+ deps.out(`Config: ${path}`);
121
+ }
122
+ var AIDER_BEGIN = "# BEGIN BCCLI BOTCONNECTOR";
123
+ var AIDER_END = "# END BCCLI BOTCONNECTOR";
124
+ function stripManagedBlock(input) {
125
+ const start = input.indexOf(AIDER_BEGIN);
126
+ if (start < 0) return input;
127
+ const end = input.indexOf(AIDER_END, start);
128
+ if (end < 0) return input;
129
+ const after = end + AIDER_END.length;
130
+ const left = input.slice(0, start).trimEnd();
131
+ const right = input.slice(after).trimStart();
132
+ return right ? `${left}
133
+ ${right}` : left;
134
+ }
135
+ function yamlQuote(value) {
136
+ return JSON.stringify(value.replace(/\\/g, "/"));
137
+ }
138
+ async function connectAider(deps) {
139
+ const home = homeDir(deps.env);
140
+ const configPath = join(home, ".aider.conf.yml");
141
+ const envPath = join(integrationHome(deps.env), "aider.env");
142
+ const configBackup = safeBackup(configPath, deps.env);
143
+ const envBackup = safeBackup(envPath, deps.env);
144
+ const secret = ensureSecret(deps.env);
145
+ const key = readFileSync(secret, "utf8").trim();
146
+ const config = loadConfig(deps.cwd);
147
+ const defaultModel = config.model.startsWith("bc-cloud/") ? config.model.slice("bc-cloud/".length) : "glm-5.3-flash";
148
+ let existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
149
+ existing = stripManagedBlock(existing);
150
+ if (/^\s*(env-file|openai-api-base|openai-api-key)\s*:/m.test(existing)) {
151
+ throw new ConfigError(`Config Aider sudah memiliki env-file/openai-api setting sendiri: ${configPath}. BCCLI tidak akan menimpanya.`);
152
+ }
153
+ mkdirSync(dirname(envPath), { recursive: true });
154
+ writeFileSync(envPath, [
155
+ `OPENAI_API_BASE=${BASE_URL}`,
156
+ `OPENAI_API_KEY=${key}`,
157
+ `AIDER_MODEL=openai/${defaultModel}`,
158
+ ""
159
+ ].join("\n"), { mode: 384 });
160
+ try {
161
+ chmodSync(envPath, 384);
162
+ } catch {
163
+ }
164
+ const managed = [
165
+ AIDER_BEGIN,
166
+ `env-file: ${yamlQuote(envPath)}`,
167
+ AIDER_END
168
+ ].join("\n");
169
+ const merged = existing.trim() ? `${existing.trimEnd()}
170
+
171
+ ${managed}
172
+ ` : `${managed}
173
+ `;
174
+ writeFileSync(configPath, merged);
175
+ writeState("aider", {
176
+ target: "aider",
177
+ files: [{ path: configPath, ...configBackup }, { path: envPath, ...envBackup }],
178
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
179
+ }, deps.env);
180
+ deps.out("Aider terhubung ke BotConnector.");
181
+ deps.out(`Config: ${configPath}`);
182
+ }
183
+ function externalDefaultModel(models, deps) {
184
+ const config = loadConfig(deps.cwd);
185
+ const preferred = config.model.startsWith("bc-cloud/") ? config.model.slice("bc-cloud/".length) : "";
186
+ if (preferred && models.includes(preferred)) return preferred;
187
+ const first = models[0];
188
+ if (!first) throw new ConfigError("Katalog BotConnector tidak memiliki model yang dapat dipakai agent eksternal.");
189
+ return first;
190
+ }
191
+ async function connectCline(deps) {
192
+ const home = homeDir(deps.env);
193
+ const dataDir = deps.env.CLINE_DATA_DIR || join(home, ".cline", "data");
194
+ const path = join(dataDir, "settings", "providers.json");
195
+ const backup = safeBackup(path, deps.env);
196
+ const secret = ensureSecret(deps.env);
197
+ const key = readFileSync(secret, "utf8").trim();
198
+ const models = await botConnectorModels(deps);
199
+ const model = externalDefaultModel(models, deps);
200
+ let doc = { version: 1, modes: {}, providers: {} };
201
+ if (existsSync(path)) {
202
+ try {
203
+ doc = JSON.parse(readFileSync(path, "utf8"));
204
+ } catch {
205
+ throw new ConfigError(`Config Cline tidak valid JSON: ${path}`);
206
+ }
207
+ }
208
+ const providers = doc.providers && typeof doc.providers === "object" && !Array.isArray(doc.providers) ? { ...doc.providers } : {};
209
+ const current = providers["openai-compatible"];
210
+ const currentBase = current?.settings?.baseUrl;
211
+ if (currentBase && currentBase.replace(/\/+$/, "") !== BASE_URL) {
212
+ throw new ConfigError(
213
+ "Cline sudah memakai provider openai-compatible lain (" + currentBase + "). BCCLI tidak akan menimpanya; disconnect/ubah provider tersebut lebih dulu."
214
+ );
215
+ }
216
+ providers["openai-compatible"] = {
217
+ settings: { provider: "openai-compatible", apiKey: key, model, baseUrl: BASE_URL },
218
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
219
+ tokenSource: "manual"
220
+ };
221
+ doc.version = typeof doc.version === "number" ? doc.version : 1;
222
+ doc.lastUsedProvider = "openai-compatible";
223
+ doc.modes = doc.modes && typeof doc.modes === "object" ? doc.modes : {};
224
+ doc.providers = providers;
225
+ mkdirSync(dirname(path), { recursive: true });
226
+ writeFileSync(path, `${JSON.stringify(doc, null, 2)}
227
+ `, { mode: 384 });
228
+ try {
229
+ chmodSync(path, 384);
230
+ } catch {
231
+ }
232
+ writeState("cline", {
233
+ target: "cline",
234
+ files: [{ path, ...backup }],
235
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
236
+ }, deps.env);
237
+ deps.out(`Cline terhubung ke BotConnector (${models.length} model tersedia; default ${model}).`);
238
+ deps.out(`Config: ${path}`);
239
+ deps.out("Catatan: Cline menyimpan API key provider di providers.json miliknya.");
240
+ }
241
+ var DSH_ENV_BEGIN = "# BEGIN BCCLI BOTCONNECTOR";
242
+ var DSH_ENV_END = "# END BCCLI BOTCONNECTOR";
243
+ function stripDshEnvBlock(input) {
244
+ const start = input.indexOf(DSH_ENV_BEGIN);
245
+ if (start < 0) return input;
246
+ const end = input.indexOf(DSH_ENV_END, start);
247
+ if (end < 0) return input;
248
+ const after = end + DSH_ENV_END.length;
249
+ const left = input.slice(0, start).trimEnd();
250
+ const right = input.slice(after).trimStart();
251
+ return right ? `${left}
252
+ ${right}` : left;
253
+ }
254
+ async function connectDeepSeekHarness(deps) {
255
+ const home = homeDir(deps.env);
256
+ const dshHome = deps.env.DSH_HOME || join(home, ".dsh");
257
+ const settingsPath = join(dshHome, "settings.yaml");
258
+ const envPath = join(dshHome, ".env");
259
+ const settingsBackup = safeBackup(settingsPath, deps.env);
260
+ const envBackup = safeBackup(envPath, deps.env);
261
+ const secret = ensureSecret(deps.env);
262
+ const key = readFileSync(secret, "utf8").trim();
263
+ const models = await botConnectorModels(deps);
264
+ const source = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : "{}\n";
265
+ const doc = parseDocument(source);
266
+ if (doc.errors.length) throw new ConfigError(`Config DeepSeek Harness tidak valid YAML: ${settingsPath}`);
267
+ const existingProvider = doc.getIn(["llm-pi-ai", "providers", PROVIDER_ID]);
268
+ if (existingProvider?.baseURL && existingProvider.baseURL.replace(/\/+$/, "") !== BASE_URL) {
269
+ throw new ConfigError(`DeepSeek Harness sudah memiliki provider "${PROVIDER_ID}" dengan endpoint lain.`);
270
+ }
271
+ doc.setIn(["llm-pi-ai", "providers", PROVIDER_ID], {
272
+ displayName: "BotConnector",
273
+ apiKeyEnv: "BOTCONNECTOR_API_KEY",
274
+ api: "openai-completions",
275
+ baseURL: BASE_URL,
276
+ defaultInput: ["text"],
277
+ compat: { supportsDeveloperRole: false, maxTokensField: "max_tokens" },
278
+ models: models.map((id) => ({ id, name: id }))
279
+ });
280
+ let envText = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
281
+ envText = stripDshEnvBlock(envText);
282
+ if (/^\s*BOTCONNECTOR_API_KEY\s*=/m.test(envText)) {
283
+ throw new ConfigError(`$DSH_HOME/.env sudah memiliki BOTCONNECTOR_API_KEY di luar blok BCCLI: ${envPath}`);
284
+ }
285
+ const managed = [DSH_ENV_BEGIN, `BOTCONNECTOR_API_KEY=${key}`, DSH_ENV_END].join("\n");
286
+ const mergedEnv = envText.trim() ? `${envText.trimEnd()}
287
+
288
+ ${managed}
289
+ ` : `${managed}
290
+ `;
291
+ mkdirSync(dshHome, { recursive: true });
292
+ writeFileSync(settingsPath, doc.toString());
293
+ writeFileSync(envPath, mergedEnv, { mode: 384 });
294
+ try {
295
+ chmodSync(envPath, 384);
296
+ } catch {
297
+ }
298
+ writeState("deepseek-harness", {
299
+ target: "deepseek-harness",
300
+ files: [{ path: settingsPath, ...settingsBackup }, { path: envPath, ...envBackup }],
301
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
302
+ }, deps.env);
303
+ deps.out(`DeepSeek Harness terhubung ke BotConnector (${models.length} model).`);
304
+ deps.out(`Config: ${settingsPath}`);
305
+ deps.out("Provider: botconnector (OpenAI Chat Completions)");
306
+ }
307
+ function disconnect(target, deps) {
308
+ const state = readState(target, deps.env);
309
+ if (!state) {
310
+ deps.out(`${target}: belum dikelola oleh BCCLI.`);
311
+ return;
312
+ }
313
+ for (const file of [...state.files].reverse()) {
314
+ if (file.backup && existsSync(file.backup)) {
315
+ mkdirSync(dirname(file.path), { recursive: true });
316
+ copyFileSync(file.backup, file.path);
317
+ } else if (file.created) {
318
+ rmSync(file.path, { force: true });
319
+ }
320
+ }
321
+ rmSync(statePath(target, deps.env), { force: true });
322
+ deps.out(`${target}: integrasi BotConnector dilepas dan config dipulihkan.`);
323
+ }
324
+ function listIntegrations(deps) {
325
+ for (const target of ["opencode", "aider", "cline", "deepseek-harness"]) {
326
+ deps.out(`${target} ${readState(target, deps.env) ? "connected" : "not connected"}`);
327
+ }
328
+ }
329
+ async function runIntegrationCommand(args, deps) {
330
+ if (args.command === "integrations") {
331
+ listIntegrations(deps);
332
+ return 0;
333
+ }
334
+ const target = parseTarget(args.subArgs[0]);
335
+ if (args.command === "disconnect") {
336
+ disconnect(target, deps);
337
+ return 0;
338
+ }
339
+ if (target === "opencode") await connectOpenCode(deps);
340
+ else if (target === "aider") await connectAider(deps);
341
+ else if (target === "cline") await connectCline(deps);
342
+ else await connectDeepSeekHarness(deps);
343
+ return 0;
344
+ }
345
+ export {
346
+ runIntegrationCommand
347
+ };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  readSecret,
4
4
  runLogin
5
- } from "./chunk-QH7XXNHU.js";
5
+ } from "./chunk-IOWDWOXS.js";
6
6
  export {
7
7
  readSecret,
8
8
  runLogin
@@ -2,16 +2,16 @@
2
2
  import {
3
3
  CATALOG,
4
4
  fillTemplate
5
- } from "./chunk-TV6LEHOH.js";
5
+ } from "./chunk-DX7CZTBB.js";
6
6
  import {
7
7
  addGlobalServer,
8
8
  globalMcpPath,
9
9
  readMcpFile,
10
10
  removeGlobalServer
11
- } from "./chunk-XQLXE666.js";
11
+ } from "./chunk-BFZXRKW5.js";
12
12
  import {
13
13
  bccliHome
14
- } from "./chunk-Z5JM76EL.js";
14
+ } from "./chunk-2MGINW4O.js";
15
15
 
16
16
  // src/mcpCli.ts
17
17
  async function runMcpCommand(args, deps) {
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addProviderKey,
4
+ runProviderCommand
5
+ } from "./chunk-U7GK73CK.js";
6
+ import "./chunk-7S6JXQCH.js";
7
+ import "./chunk-CLN4Z5E3.js";
8
+ import "./chunk-2MGINW4O.js";
9
+ export {
10
+ addProviderKey,
11
+ runProviderCommand
12
+ };