@botconnector/bccli 0.1.0-beta.4 → 0.3.0-beta.1

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,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/session.ts
4
+ import { randomBytes } from "crypto";
5
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
6
+ import { join } from "path";
7
+ function projectDir(home, cwd) {
8
+ const slug = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
9
+ return join(home, "sessions", slug);
10
+ }
11
+ function repairTail(messages) {
12
+ const out = [...messages];
13
+ for (let i = out.length - 1; i >= 0; i--) {
14
+ const m = out[i];
15
+ if (m.role !== "assistant" || !m.tool_calls?.length) continue;
16
+ const answered = new Set(out.slice(i + 1).flatMap((x) => x.role === "tool" ? [x.tool_call_id] : []));
17
+ if (!m.tool_calls.every((c) => answered.has(c.id))) out.length = i;
18
+ break;
19
+ }
20
+ return out;
21
+ }
22
+ var Session = class _Session {
23
+ constructor(file) {
24
+ this.file = file;
25
+ }
26
+ file;
27
+ static create(home, cwd, now = /* @__PURE__ */ new Date()) {
28
+ const dir = projectDir(home, cwd);
29
+ mkdirSync(dir, { recursive: true });
30
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
31
+ return new _Session(join(dir, `${stamp}-${randomBytes(3).toString("hex")}.jsonl`));
32
+ }
33
+ static list(home, cwd) {
34
+ const dir = projectDir(home, cwd);
35
+ if (!existsSync(dir)) return [];
36
+ return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
37
+ const session = new _Session(join(dir, f));
38
+ const first = session.load().find((m) => m.role === "user");
39
+ return { session, mtime: statSync(session.file).mtime, preview: first ? String(first.content).slice(0, 80) : "" };
40
+ }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
41
+ }
42
+ static latest(home, cwd) {
43
+ return _Session.list(home, cwd)[0]?.session;
44
+ }
45
+ write(record) {
46
+ appendFileSync(this.file, `${JSON.stringify(record)}
47
+ `);
48
+ }
49
+ append(message) {
50
+ this.write({ t: "msg", m: message });
51
+ }
52
+ reset() {
53
+ this.write({ t: "reset" });
54
+ }
55
+ load() {
56
+ if (!existsSync(this.file)) return [];
57
+ let messages = [];
58
+ for (const line of readFileSync(this.file, "utf8").split("\n")) {
59
+ if (!line.trim()) continue;
60
+ let record;
61
+ try {
62
+ record = JSON.parse(line);
63
+ } catch {
64
+ continue;
65
+ }
66
+ if (record.t === "reset") messages = [];
67
+ else if (record.t === "msg") messages.push(record.m);
68
+ }
69
+ return repairTail(messages);
70
+ }
71
+ };
72
+ function pruneSessions(home, now = Date.now(), maxAgeDays = 30) {
73
+ const root = join(home, "sessions");
74
+ if (!existsSync(root)) return 0;
75
+ let removed = 0;
76
+ for (const project of readdirSync(root)) {
77
+ const dir = join(root, project);
78
+ for (const file of readdirSync(dir)) {
79
+ const path = join(dir, file);
80
+ if (now - statSync(path).mtimeMs > maxAgeDays * 864e5) {
81
+ rmSync(path);
82
+ removed++;
83
+ }
84
+ }
85
+ }
86
+ return removed;
87
+ }
88
+
89
+ export {
90
+ Session,
91
+ pruneSessions
92
+ };
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/print.ts
4
+ function toolLabel(tool) {
5
+ return tool.charAt(0).toUpperCase() + tool.slice(1);
6
+ }
7
+ async function runPrint(rt, prompt, io = { out: process.stdout, err: process.stderr }, opts = {}) {
8
+ rt.interaction.approvePlan = async () => {
9
+ if (opts.allowAll) return "allowAll";
10
+ io.err.write("Rencana butuh persetujuan; jalankan dengan --allow-all.\n");
11
+ return "no";
12
+ };
13
+ let failed = false;
14
+ let wroteText = false;
15
+ rt.agent.onEvent = (event) => {
16
+ switch (event.type) {
17
+ case "text":
18
+ io.out.write(event.delta);
19
+ wroteText = true;
20
+ break;
21
+ case "toolStart":
22
+ io.err.write(`\u23BF ${toolLabel(event.tool)} ${event.target}
23
+ `);
24
+ break;
25
+ case "toolEnd":
26
+ if (event.isError) io.err.write(` \u2717 ${event.output.split("\n")[0]}
27
+ `);
28
+ break;
29
+ case "stepLimit":
30
+ io.err.write("Batas langkah tercapai.\n");
31
+ failed = true;
32
+ break;
33
+ case "aborted":
34
+ io.err.write("Dibatalkan.\n");
35
+ failed = true;
36
+ break;
37
+ case "error":
38
+ io.err.write(`Error: ${event.message}
39
+ `);
40
+ failed = true;
41
+ break;
42
+ }
43
+ };
44
+ rt.agent.askPermission = async (req) => {
45
+ io.err.write(` Butuh izin untuk ${req.tool}. Jalankan dengan --allow-all atau --allowed-tools ${req.kind}.
46
+ `);
47
+ return "no";
48
+ };
49
+ const controller = new AbortController();
50
+ const onSigint = () => controller.abort();
51
+ process.once("SIGINT", onSigint);
52
+ try {
53
+ await rt.agent.run(prompt, controller.signal);
54
+ } finally {
55
+ process.off("SIGINT", onSigint);
56
+ }
57
+ if (wroteText) io.out.write("\n");
58
+ return failed ? 1 : 0;
59
+ }
60
+
61
+ // src/extensions.ts
62
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
63
+ import { basename, join } from "path";
64
+ function parseFrontmatter(text) {
65
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
66
+ if (!match) return { data: {}, body: text };
67
+ const data = {};
68
+ for (const line of match[1].split(/\r?\n/)) {
69
+ const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
70
+ if (!kv) continue;
71
+ let value = kv[2].trim();
72
+ if (value.startsWith("[") && value.endsWith("]")) {
73
+ data[kv[1]] = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
74
+ continue;
75
+ }
76
+ if (/^(["']).*\1$/.test(value)) value = value.slice(1, -1);
77
+ data[kv[1]] = value;
78
+ }
79
+ return { data, body: text.slice(match[0].length) };
80
+ }
81
+ var str = (v) => Array.isArray(v) ? v.join(", ") : v;
82
+ function dirs(r, kind) {
83
+ return [join(r.userHome, ".claude", kind), join(r.home, kind), join(r.cwd, ".claude", kind), join(r.cwd, ".bccli", kind)];
84
+ }
85
+ function list(dir) {
86
+ try {
87
+ return readdirSync(dir).sort();
88
+ } catch {
89
+ return [];
90
+ }
91
+ }
92
+ function loadSkills(r) {
93
+ const byName = /* @__PURE__ */ new Map();
94
+ for (const dir of dirs(r, "skills")) {
95
+ for (const entry of list(dir)) {
96
+ const file = join(dir, entry, "SKILL.md");
97
+ if (!existsSync(file)) continue;
98
+ const { data } = parseFrontmatter(readFileSync(file, "utf8"));
99
+ const name = str(data.name) || entry;
100
+ byName.set(name, { name, description: str(data.description) ?? "", dir: join(dir, entry), file });
101
+ }
102
+ }
103
+ return [...byName.values()];
104
+ }
105
+ function loadCommands(r) {
106
+ const byName = /* @__PURE__ */ new Map();
107
+ for (const dir of dirs(r, "commands")) {
108
+ for (const entry of list(dir)) {
109
+ const file = join(dir, entry);
110
+ if (!entry.endsWith(".md") || !statSync(file).isFile()) continue;
111
+ const { data, body } = parseFrontmatter(readFileSync(file, "utf8"));
112
+ const def = { name: basename(entry, ".md"), body: body.trim() };
113
+ if (data.description) def.description = str(data.description);
114
+ if (data["argument-hint"]) def.argumentHint = str(data["argument-hint"]);
115
+ byName.set(def.name, def);
116
+ }
117
+ }
118
+ return [...byName.values()];
119
+ }
120
+ function loadAgentDefs(r) {
121
+ const byName = /* @__PURE__ */ new Map();
122
+ for (const dir of dirs(r, "agents")) {
123
+ for (const entry of list(dir)) {
124
+ if (!entry.endsWith(".md")) continue;
125
+ const { data, body } = parseFrontmatter(readFileSync(join(dir, entry), "utf8"));
126
+ const name = str(data.name) || basename(entry, ".md");
127
+ const def = { name, description: str(data.description) ?? "", prompt: body.trim() };
128
+ if (data.tools)
129
+ def.tools = Array.isArray(data.tools) ? data.tools : String(data.tools).split(",").map((s) => s.trim()).filter(Boolean);
130
+ if (data.model) def.model = str(data.model);
131
+ byName.set(name, def);
132
+ }
133
+ }
134
+ return [...byName.values()];
135
+ }
136
+
137
+ // src/tools/ignore.ts
138
+ import { readFileSync as readFileSync2 } from "fs";
139
+ import { join as join2 } from "path";
140
+ function ignorePatterns(root) {
141
+ const patterns = ["**/node_modules/**", "**/.git/**"];
142
+ let lines = [];
143
+ try {
144
+ lines = readFileSync2(join2(root, ".gitignore"), "utf8").split(/\r?\n/);
145
+ } catch {
146
+ return patterns;
147
+ }
148
+ for (const raw of lines) {
149
+ const line = raw.trim();
150
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
151
+ const p = line.replace(/^\//, "").replace(/\/$/, "");
152
+ patterns.push(`**/${p}`, `**/${p}/**`);
153
+ }
154
+ return patterns;
155
+ }
156
+
157
+ export {
158
+ toolLabel,
159
+ runPrint,
160
+ ignorePatterns,
161
+ parseFrontmatter,
162
+ loadSkills,
163
+ loadCommands,
164
+ loadAgentDefs
165
+ };
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConfigError
4
+ } from "./chunk-Z5JM76EL.js";
5
+
6
+ // src/mcp/catalog.ts
7
+ var CATALOG = [
8
+ { name: "playwright", description: "kontrol browser (buka halaman, klik, isi form, screenshot)", config: { command: "npx", args: ["-y", "@playwright/mcp@latest"] } },
9
+ { name: "context7", description: "dokumentasi library terbaru", config: { type: "http", url: "https://mcp.context7.com/mcp" } },
10
+ { name: "fetch", description: "ambil halaman web jadi markdown (butuh uv)", config: { command: "uvx", args: ["mcp-server-fetch"] } },
11
+ {
12
+ name: "filesystem",
13
+ description: "akses folder di luar project",
14
+ config: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "${dir}"] },
15
+ inputs: [{ key: "dir", label: "Folder yang boleh diakses" }]
16
+ },
17
+ { name: "git", description: "operasi git terstruktur (butuh uv)", config: { command: "uvx", args: ["mcp-server-git"] } },
18
+ {
19
+ name: "github",
20
+ description: "issue, PR, dan repo GitHub",
21
+ config: { type: "http", url: "https://api.githubcopilot.com/mcp/", headers: { Authorization: "Bearer ${token}" } },
22
+ inputs: [{ key: "token", label: "GitHub Personal Access Token", secret: true }]
23
+ }
24
+ ];
25
+ function fillTemplate(config, values) {
26
+ const missing = /* @__PURE__ */ new Set();
27
+ const fill = (s) => s.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_m, key) => {
28
+ if (!values[key]) missing.add(key);
29
+ return values[key] ?? "";
30
+ });
31
+ const mapValues = (o) => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, fill(v)]));
32
+ const out = config.type === "http" ? { type: "http", url: fill(config.url), ...config.headers ? { headers: mapValues(config.headers) } : {} } : {
33
+ ...config.type ? { type: config.type } : {},
34
+ command: fill(config.command),
35
+ ...config.args ? { args: config.args.map(fill) } : {},
36
+ ...config.env ? { env: mapValues(config.env) } : {}
37
+ };
38
+ if (missing.size) throw new ConfigError(`Isian MCP belum lengkap: ${[...missing].join(", ")}`);
39
+ return out;
40
+ }
41
+
42
+ export {
43
+ CATALOG,
44
+ fillTemplate
45
+ };
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConfigError
4
+ } from "./chunk-Z5JM76EL.js";
5
+
6
+ // src/mcp/config.ts
7
+ import { createHash } from "crypto";
8
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
+ import { dirname, join, resolve } from "path";
10
+ var globalMcpPath = (home) => join(home, "mcp.json");
11
+ var projectMcpPath = (cwd) => join(cwd, ".bccli", "mcp.json");
12
+ var trustPath = (home) => join(home, "trusted-mcp.json");
13
+ function readJson(path, fallback) {
14
+ if (!existsSync(path)) return fallback;
15
+ try {
16
+ return JSON.parse(readFileSync(path, "utf8"));
17
+ } catch (error) {
18
+ throw new ConfigError(`File MCP tidak valid: ${path} (${error.message})`);
19
+ }
20
+ }
21
+ function writePrivate(path, data) {
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}
24
+ `, { mode: 384 });
25
+ chmodSync(path, 384);
26
+ }
27
+ function readMcpFile(path) {
28
+ return readJson(path, {}).mcpServers ?? {};
29
+ }
30
+ function addGlobalServer(home, name, config) {
31
+ writePrivate(globalMcpPath(home), { mcpServers: { ...readMcpFile(globalMcpPath(home)), [name]: config } });
32
+ }
33
+ function removeGlobalServer(home, name) {
34
+ const { [name]: _removed, ...rest } = readMcpFile(globalMcpPath(home));
35
+ writePrivate(globalMcpPath(home), { mcpServers: rest });
36
+ }
37
+ var configHash = (config) => createHash("sha256").update(JSON.stringify(config)).digest("hex");
38
+ function projectTrust(home, cwd, name, config) {
39
+ const decision = readJson(trustPath(home), {})[resolve(cwd)]?.[name];
40
+ return decision?.hash === configHash(config) ? decision.allowed : void 0;
41
+ }
42
+ function setProjectTrust(home, cwd, name, allowed, config) {
43
+ const all = readJson(trustPath(home), {});
44
+ const key = resolve(cwd);
45
+ writePrivate(trustPath(home), { ...all, [key]: { ...all[key], [name]: { allowed, hash: configHash(config) } } });
46
+ }
47
+ function serversToStart(home, cwd) {
48
+ const global = readMcpFile(globalMcpPath(home));
49
+ const start = Object.entries(global).map(([name, config]) => ({ name, config, source: "global" }));
50
+ const needTrust = [];
51
+ for (const [name, config] of Object.entries(readMcpFile(projectMcpPath(cwd)))) {
52
+ if (global[name]) continue;
53
+ const trust = projectTrust(home, cwd, name, config);
54
+ if (trust === true) start.push({ name, config, source: "project" });
55
+ else if (trust === void 0) needTrust.push({ name, config, source: "project" });
56
+ }
57
+ return { start, needTrust };
58
+ }
59
+
60
+ export {
61
+ globalMcpPath,
62
+ projectMcpPath,
63
+ readMcpFile,
64
+ addGlobalServer,
65
+ removeGlobalServer,
66
+ projectTrust,
67
+ setProjectTrust,
68
+ serversToStart
69
+ };
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/presets.ts
4
+ var PRESETS = [
5
+ { id: "bc-cloud", name: "BotConnector Cloud", baseURL: "https://api.botconnector.id/v1", apiKeyEnv: "BOTCONNECTOR_API_KEY" },
6
+ { id: "openrouter", name: "OpenRouter", baseURL: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY" },
7
+ { id: "openai", name: "OpenAI", baseURL: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY" },
8
+ { id: "gemini", name: "Google Gemini", baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", apiKeyEnv: "GEMINI_API_KEY" },
9
+ { id: "deepseek", name: "DeepSeek", baseURL: "https://api.deepseek.com/v1", apiKeyEnv: "DEEPSEEK_API_KEY" },
10
+ { id: "groq", name: "Groq", baseURL: "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_API_KEY" },
11
+ { id: "ollama-cloud", name: "Ollama Cloud", baseURL: "https://ollama.com/v1", apiKeyEnv: "OLLAMA_API_KEY" },
12
+ { id: "ollama", name: "Ollama lokal", baseURL: "http://127.0.0.1:11434/v1", local: true },
13
+ { id: "lmstudio", name: "LM Studio lokal", baseURL: "http://127.0.0.1:1234/v1", local: true }
14
+ ];
15
+
16
+ // src/config.ts
17
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
18
+ import { homedir } from "os";
19
+ import { join } from "path";
20
+ var ConfigError = class extends Error {
21
+ };
22
+ var DEFAULT_CONFIG = {
23
+ model: "bc-cloud/glm-5.3-flash",
24
+ permissionMode: "default",
25
+ providers: Object.fromEntries(PRESETS.map((p) => [p.id, { baseURL: p.baseURL, apiKeyEnv: p.apiKeyEnv }])),
26
+ allow: []
27
+ };
28
+ function bccliHome(env = process.env) {
29
+ return env.BCCLI_HOME ?? join(homedir(), ".bccli");
30
+ }
31
+ function readJsonConfig(path) {
32
+ if (!existsSync(path)) return {};
33
+ try {
34
+ return JSON.parse(readFileSync(path, "utf8"));
35
+ } catch (error) {
36
+ throw new ConfigError(`Config tidak valid: ${path} (${error.message})`);
37
+ }
38
+ }
39
+ function untrustedProviders(project, known) {
40
+ const out = {};
41
+ for (const [id, provider] of Object.entries(project.providers ?? {})) {
42
+ if (known[id] || typeof provider?.baseURL !== "string") continue;
43
+ out[id] = { baseURL: provider.baseURL };
44
+ }
45
+ return out;
46
+ }
47
+ function loadConfig(cwd, env = process.env) {
48
+ const global = readJsonConfig(join(bccliHome(env), "config.json"));
49
+ const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
50
+ const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
51
+ const fromProject = untrustedProviders(project, known);
52
+ return {
53
+ model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
54
+ permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
55
+ providers: { ...known, ...fromProject },
56
+ projectProviders: Object.keys(fromProject),
57
+ allow: [...global.allow ?? []]
58
+ };
59
+ }
60
+ function readCredentials(env = process.env) {
61
+ const path = join(bccliHome(env), "credentials");
62
+ if (!existsSync(path)) return {};
63
+ try {
64
+ return JSON.parse(readFileSync(path, "utf8"));
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+ function saveCredential(providerId, key, env = process.env) {
70
+ const home = bccliHome(env);
71
+ mkdirSync(home, { recursive: true });
72
+ const path = join(home, "credentials");
73
+ writeFileSync(path, JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), { mode: 384 });
74
+ chmodSync(path, 384);
75
+ }
76
+ function resolveModel(config, modelRef, env = process.env) {
77
+ const slash = modelRef.indexOf("/");
78
+ if (slash <= 0 || slash === modelRef.length - 1) {
79
+ throw new ConfigError(`Model harus berformat provider/model, contoh bc-cloud/glm-5.3-flash (dapat: ${modelRef})`);
80
+ }
81
+ const providerId = modelRef.slice(0, slash);
82
+ const provider = config.providers[providerId];
83
+ if (!provider) {
84
+ throw new ConfigError(`Provider "${providerId}" tidak ada di config. Tersedia: ${Object.keys(config.providers).join(", ")}`);
85
+ }
86
+ const apiKey = (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[providerId] || void 0;
87
+ if (provider.apiKeyEnv && !apiKey) {
88
+ throw new ConfigError(`API key untuk ${providerId} belum ada. Jalankan \`bccli login\` atau set env ${provider.apiKeyEnv}.`);
89
+ }
90
+ return { providerId, model: modelRef.slice(slash + 1), baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey };
91
+ }
92
+ function removeCredential(providerId, env = process.env) {
93
+ const creds = readCredentials(env);
94
+ if (!(providerId in creds)) return;
95
+ delete creds[providerId];
96
+ const path = join(bccliHome(env), "credentials");
97
+ writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 384 });
98
+ chmodSync(path, 384);
99
+ }
100
+
101
+ export {
102
+ PRESETS,
103
+ ConfigError,
104
+ bccliHome,
105
+ readJsonConfig,
106
+ loadConfig,
107
+ readCredentials,
108
+ saveCredential,
109
+ resolveModel,
110
+ removeCredential
111
+ };