@botconnector/bccli 0.4.0-beta.9 → 0.4.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.
Files changed (183) hide show
  1. package/README.en.md +253 -0
  2. package/README.md +93 -6
  3. package/dist/abort.d.ts +5 -0
  4. package/dist/acp-3PHBAI64.js +182 -0
  5. package/dist/acp.d.ts +27 -0
  6. package/dist/agent.d.ts +143 -0
  7. package/dist/agents.d.ts +4 -0
  8. package/dist/args-W6BNKY7K.js +13 -0
  9. package/dist/args.d.ts +51 -0
  10. package/dist/budget.d.ts +23 -0
  11. package/dist/checkpoints.d.ts +34 -0
  12. package/dist/{chunk-BFZXRKW5.js → chunk-24JN75FZ.js} +29 -7
  13. package/dist/chunk-45SDLMTV.js +341 -0
  14. package/dist/chunk-75BECX4F.js +56 -0
  15. package/dist/{chunk-SJH3YLW4.js → chunk-BC7UXFE4.js} +69 -6
  16. package/dist/chunk-BGXNR74O.js +733 -0
  17. package/dist/chunk-BKGFA2PT.js +8 -0
  18. package/dist/{chunk-CLN4Z5E3.js → chunk-BLOYFIAD.js} +6 -3
  19. package/dist/chunk-BXQQCUAC.js +145 -0
  20. package/dist/chunk-FLQPE4S5.js +373 -0
  21. package/dist/chunk-GN4W3T6E.js +76 -0
  22. package/dist/{chunk-7S6JXQCH.js → chunk-JHPGOAXU.js} +2 -1
  23. package/dist/chunk-MFHCQQEN.js +201 -0
  24. package/dist/chunk-MHIYLXJC.js +143 -0
  25. package/dist/chunk-PSO7R5AD.js +10 -0
  26. package/dist/chunk-PZRKU4UM.js +2143 -0
  27. package/dist/chunk-QZKLZELZ.js +64 -0
  28. package/dist/chunk-R5RE46CW.js +231 -0
  29. package/dist/chunk-UZWJQF67.js +500 -0
  30. package/dist/{chunk-DX7CZTBB.js → chunk-WI22M2ID.js} +14 -9
  31. package/dist/chunk-WNACAUDK.js +946 -0
  32. package/dist/chunk-XV7ZVL4J.js +235 -0
  33. package/dist/cli.d.ts +1 -0
  34. package/dist/cli.js +150 -1382
  35. package/dist/clipboardImage.d.ts +17 -0
  36. package/dist/commands.d.ts +16 -0
  37. package/dist/completion-HPPFDAPB.js +125 -0
  38. package/dist/completion.d.ts +14 -0
  39. package/dist/{config-YUNKPCNB.js → config-DIDLDONU.js} +5 -2
  40. package/dist/config-IU76CPK2.js +34 -0
  41. package/dist/config.d.ts +97 -0
  42. package/dist/context.d.ts +13 -0
  43. package/dist/contextWindow.d.ts +3 -0
  44. package/dist/diff.d.ts +7 -0
  45. package/dist/exit.d.ts +9 -0
  46. package/dist/extensions.d.ts +34 -0
  47. package/dist/ghBotCli-E3VPHNZY.js +677 -0
  48. package/dist/ghBotCli.d.ts +9 -0
  49. package/dist/ghBotDispatch.d.ts +68 -0
  50. package/dist/ghBotRunner.d.ts +82 -0
  51. package/dist/ghBotServe.d.ts +21 -0
  52. package/dist/ghBotWebhook.d.ts +45 -0
  53. package/dist/hooks.d.ts +27 -0
  54. package/dist/i18n/id.d.ts +1 -0
  55. package/dist/i18n.d.ts +16 -0
  56. package/dist/integrationCli-Q2YPDTPG.js +685 -0
  57. package/dist/integrationCli.d.ts +9 -0
  58. package/dist/keybinds.d.ts +22 -0
  59. package/dist/login-FP3BB44A.js +16 -0
  60. package/dist/login.d.ts +19 -0
  61. package/dist/lsp.d.ts +47 -0
  62. package/dist/mcp/catalog.d.ts +14 -0
  63. package/dist/mcp/config.d.ts +36 -0
  64. package/dist/mcp/manager.d.ts +34 -0
  65. package/dist/mcp/oauth.d.ts +82 -0
  66. package/dist/{mcpCli-DZWQPTOS.js → mcpCli-SNBDUWJN.js} +42 -9
  67. package/dist/mcpCli.d.ts +3 -0
  68. package/dist/mcpServer/serve.d.ts +8 -0
  69. package/dist/mcpServer/server.d.ts +19 -0
  70. package/dist/models.d.ts +13 -0
  71. package/dist/modelsCli-CZ2HNGKR.js +36 -0
  72. package/dist/modelsCli.d.ts +15 -0
  73. package/dist/npmRunner-LU43GM3W.js +46 -0
  74. package/dist/npmRunner.d.ts +21 -0
  75. package/dist/oauthLogin-6FWQABJ7.js +210 -0
  76. package/dist/oauthLogin.d.ts +47 -0
  77. package/dist/permissions.d.ts +30 -0
  78. package/dist/pluginSpec.d.ts +25 -0
  79. package/dist/plugins.d.ts +45 -0
  80. package/dist/pluginsCli-LPIUUDWB.js +284 -0
  81. package/dist/pluginsCli.d.ts +16 -0
  82. package/dist/prCli-5IMV3VNQ.js +78 -0
  83. package/dist/prCli.d.ts +33 -0
  84. package/dist/presets.d.ts +8 -0
  85. package/dist/print.d.ts +14 -0
  86. package/dist/provider.d.ts +140 -0
  87. package/dist/providerCli-NMUFXAAS.js +15 -0
  88. package/dist/providerCli.d.ts +17 -0
  89. package/dist/providers.d.ts +6 -0
  90. package/dist/reasoning.d.ts +12 -0
  91. package/dist/repomap.d.ts +10 -0
  92. package/dist/sdk.d.ts +57 -0
  93. package/dist/sdk.js +5571 -0
  94. package/dist/serve-KWOECK5W.js +178 -0
  95. package/dist/serve-X2ZV4S5U.js +521 -0
  96. package/dist/serve.d.ts +37 -0
  97. package/dist/serveHeartbeat.d.ts +14 -0
  98. package/dist/serveProtocol.d.ts +47 -0
  99. package/dist/session-GKEXDJKZ.js +12 -0
  100. package/dist/session.d.ts +30 -0
  101. package/dist/setup-OV3NWQN5.js +24 -0
  102. package/dist/setup.d.ts +74 -0
  103. package/dist/slash/copy.d.ts +1 -0
  104. package/dist/slash/diff.d.ts +1 -0
  105. package/dist/slash/doctor.d.ts +18 -0
  106. package/dist/slash/export.d.ts +2 -0
  107. package/dist/slash/info.d.ts +45 -0
  108. package/dist/slash/memory.d.ts +10 -0
  109. package/dist/statsCli-CF3R7ATL.js +353 -0
  110. package/dist/statsCli.d.ts +62 -0
  111. package/dist/textToolCalls.d.ts +6 -0
  112. package/dist/thinking.d.ts +17 -0
  113. package/dist/tokenBudget.d.ts +22 -0
  114. package/dist/tools/bash.d.ts +52 -0
  115. package/dist/tools/diagnostics.d.ts +5 -0
  116. package/dist/tools/edit.d.ts +7 -0
  117. package/dist/tools/fetch.d.ts +41 -0
  118. package/dist/tools/glob.d.ts +5 -0
  119. package/dist/tools/globSafety.d.ts +7 -0
  120. package/dist/tools/grep.d.ts +13 -0
  121. package/dist/tools/ignore.d.ts +2 -0
  122. package/dist/tools/index.d.ts +12 -0
  123. package/dist/tools/memory.d.ts +13 -0
  124. package/dist/tools/paths.d.ts +2 -0
  125. package/dist/tools/plan.d.ts +10 -0
  126. package/dist/tools/read.d.ts +16 -0
  127. package/dist/tools/repomap.d.ts +2 -0
  128. package/dist/tools/sandbox.d.ts +12 -0
  129. package/dist/tools/shell.d.ts +18 -0
  130. package/dist/tools/skill.d.ts +3 -0
  131. package/dist/tools/task.d.ts +28 -0
  132. package/dist/tools/todo.d.ts +13 -0
  133. package/dist/tools/types.d.ts +52 -0
  134. package/dist/tools/websearch.d.ts +9 -0
  135. package/dist/tools/write.d.ts +5 -0
  136. package/dist/transferCli-YUFPI6AH.js +71 -0
  137. package/dist/transferCli.d.ts +23 -0
  138. package/dist/ui/App.d.ts +7 -0
  139. package/dist/ui/DiffView.d.ts +3 -0
  140. package/dist/ui/EditableText.d.ts +7 -0
  141. package/dist/ui/LinePrompt.d.ts +6 -0
  142. package/dist/ui/ListPicker.d.ts +11 -0
  143. package/dist/ui/Markdown.d.ts +8 -0
  144. package/dist/ui/McpMenu.d.ts +11 -0
  145. package/dist/ui/ModelPicker.d.ts +6 -0
  146. package/dist/ui/PermissionPrompt.d.ts +5 -0
  147. package/dist/ui/PlanApproval.d.ts +5 -0
  148. package/dist/ui/PromptInput.d.ts +18 -0
  149. package/dist/ui/ProviderMenu.d.ts +10 -0
  150. package/dist/ui/Spinner.d.ts +4 -0
  151. package/dist/ui/StatusBar.d.ts +9 -0
  152. package/dist/ui/TodoList.d.ts +4 -0
  153. package/dist/ui/ToolBlock.d.ts +14 -0
  154. package/dist/ui/complete.d.ts +1 -0
  155. package/dist/ui/focusReport.d.ts +1 -0
  156. package/dist/ui/index.d.ts +7 -0
  157. package/dist/ui/lineEdit.d.ts +42 -0
  158. package/dist/ui/replay.d.ts +4 -0
  159. package/dist/ui/table.d.ts +20 -0
  160. package/dist/ui/theme.d.ts +5 -0
  161. package/dist/ui/thinkingMouse.d.ts +33 -0
  162. package/dist/ui/transcript.d.ts +55 -0
  163. package/dist/ui/vim.d.ts +26 -0
  164. package/dist/ui-IVIFNUCC.js +2367 -0
  165. package/dist/update-TZ2G6KQ3.js +145 -0
  166. package/dist/update.d.ts +30 -0
  167. package/dist/verify.d.ts +16 -0
  168. package/dist/version.d.ts +1 -0
  169. package/dist/webBudget.d.ts +25 -0
  170. package/dist/worktree.d.ts +12 -0
  171. package/package.json +42 -8
  172. package/dist/args-JPVSGCGY.js +0 -12
  173. package/dist/chunk-2MGINW4O.js +0 -114
  174. package/dist/chunk-ELML4KVQ.js +0 -76
  175. package/dist/chunk-ISGBGFSU.js +0 -52
  176. package/dist/chunk-KV27KTOX.js +0 -254
  177. package/dist/chunk-U7GK73CK.js +0 -341
  178. package/dist/chunk-ZYCYTEKD.js +0 -96
  179. package/dist/integrationCli-BGT5UZU4.js +0 -223
  180. package/dist/login-6K32PIXV.js +0 -9
  181. package/dist/providerCli-Q7LRM2NP.js +0 -12
  182. package/dist/session-7UB77CEG.js +0 -9
  183. package/dist/ui-6GTQUJOE.js +0 -1415
@@ -0,0 +1,143 @@
1
+ import type { PermissionRequest, Permissions } from './permissions';
2
+ import { type ChatMessage, type Provider, type Usage } from './provider';
3
+ import type { ReasoningLevel } from './reasoning';
4
+ import { type HookEvent, type HooksConfig } from './hooks';
5
+ import type { Tool } from './tools/types';
6
+ import { type UsageCap } from './budget';
7
+ export type AgentEvent = {
8
+ type: 'text';
9
+ delta: string;
10
+ } | {
11
+ type: 'thinking';
12
+ delta: string;
13
+ } | {
14
+ type: 'textReplace';
15
+ text: string;
16
+ } | {
17
+ type: 'toolStart';
18
+ id: string;
19
+ tool: string;
20
+ target: string;
21
+ } | {
22
+ type: 'toolEnd';
23
+ id: string;
24
+ tool: string;
25
+ output: string;
26
+ display?: string;
27
+ isError: boolean;
28
+ } | {
29
+ type: 'usage';
30
+ inputTokens: number;
31
+ outputTokens: number;
32
+ cachedInputTokens?: number;
33
+ cacheWriteTokens?: number;
34
+ } | {
35
+ type: 'compacted';
36
+ } | {
37
+ type: 'stepLimit';
38
+ maxSteps: number;
39
+ } | {
40
+ type: 'budgetExceeded';
41
+ kind: 'tokens' | 'usd';
42
+ used: number;
43
+ limit: number;
44
+ } | {
45
+ type: 'aborted';
46
+ } | {
47
+ type: 'error';
48
+ message: string;
49
+ } | {
50
+ type: 'done';
51
+ } | {
52
+ type: 'subagent';
53
+ parentId: string;
54
+ agent: string;
55
+ event: AgentEvent;
56
+ };
57
+ export type PermissionAnswer = 'yes' | 'session' | 'all' | 'no';
58
+ export type PermissionAsk = PermissionRequest & {
59
+ preview?: string;
60
+ sessionRules?: string[];
61
+ agent?: string;
62
+ };
63
+ export type AskPermission = (req: PermissionAsk) => Promise<PermissionAnswer>;
64
+ export interface AgentOptions {
65
+ provider: Provider;
66
+ tools: Tool[];
67
+ permissions: Permissions;
68
+ systemPrompt: string;
69
+ cwd: string;
70
+ history?: ChatMessage[];
71
+ maxSteps?: number | null;
72
+ contextWindow?: number;
73
+ onMessage?: (message: ChatMessage) => void;
74
+ onReset?: () => void;
75
+ /** Per-turn token usage delta (provider-reported or estimated); the session persists it for stats. */
76
+ onUsage?: (usage: Usage) => void;
77
+ /** Marks the start of a user turn (undo boundary). */
78
+ onTurnStart?: () => void;
79
+ checkpoint?: (absPath: string) => Promise<void>;
80
+ /** Subagent name, shown on its permission prompts. */
81
+ label?: string;
82
+ reasoning?: ReasoningLevel;
83
+ /** Tool/session hooks (global config only). */
84
+ hooks?: HooksConfig;
85
+ /** In-process plugin observers fired alongside the shell hooks (global config plugins). */
86
+ pluginEmit?: (event: HookEvent, payload?: {
87
+ tool?: string;
88
+ input?: unknown;
89
+ output?: string;
90
+ }) => Promise<void>;
91
+ /** Base environment handed to hook processes (defaults to the process env at spawn). */
92
+ env?: NodeJS.ProcessEnv;
93
+ /** Per-session token/USD budget (global config usageCap); blocks model calls once exceeded. */
94
+ usageCap?: UsageCap;
95
+ /** Model ref used to match usageCap.prices keys. */
96
+ budgetModel?: string;
97
+ }
98
+ export declare class Agent {
99
+ private readonly opts;
100
+ provider: Provider;
101
+ permissions: Permissions;
102
+ messages: ChatMessage[];
103
+ totalUsage: Usage;
104
+ lastInputTokens: number;
105
+ onEvent: (event: AgentEvent) => void;
106
+ reasoning: ReasoningLevel;
107
+ askPermission: AskPermission;
108
+ tools: Tool[];
109
+ private definitions;
110
+ private turnTools;
111
+ private turnDefinitions;
112
+ private readonly readFiles;
113
+ private readonly webBudget;
114
+ private readonly maxSteps?;
115
+ private contextWindow;
116
+ private systemPrompt;
117
+ constructor(opts: AgentOptions);
118
+ setSystemPrompt(text: string): void;
119
+ setContextWindow(window: number): void;
120
+ setTools(tools: Tool[]): void;
121
+ private isParallelSafe;
122
+ private push;
123
+ /** Swaps in another conversation (resume, new session) without writing to the session log. */
124
+ load(messages: ChatMessage[]): void;
125
+ clear(): void;
126
+ compact(signal: AbortSignal): Promise<void>;
127
+ /**
128
+ * Events of one tool execution. A cancelled turn gives up on a tool that does not stop, and whatever that tool
129
+ * reports later must not reach the screen or a newer turn, so this is per execution and not per call id (ids repeat).
130
+ */
131
+ private report;
132
+ run(text: string, signal: AbortSignal): Promise<void>;
133
+ /** Current cap status; undefined means the budget is not exhausted. */
134
+ private overBudget;
135
+ /** Every tool call needs an answer in the history, or the next request to the model is rejected. */
136
+ private cancelCalls;
137
+ /**
138
+ * Every tool call must resolve with text: a rejected one would leave the assistant tool_calls
139
+ * without answers, and the next request to the model would be rejected until a restart.
140
+ */
141
+ private runTool;
142
+ private executeTool;
143
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentDef } from './extensions';
2
+ export declare const EXPLORE_PROMPT = "You are a read-only research agent working for another agent. Search and read the codebase to answer the request. You cannot edit files or run commands. Reply with a concise report: the answer, the relevant file paths with line numbers, and nothing else.";
3
+ export declare const GENERAL_PROMPT = "You are an agent handling a delegated task for another agent. Complete the task end to end with the tools you have, verify the result, and reply with a short report of what you did and what you found.";
4
+ export declare const BUILTIN_AGENTS: AgentDef[];
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ helpText,
4
+ parseCliArgs
5
+ } from "./chunk-XV7ZVL4J.js";
6
+ import "./chunk-MHIYLXJC.js";
7
+ import "./chunk-BLOYFIAD.js";
8
+ import "./chunk-FLQPE4S5.js";
9
+ import "./chunk-UZWJQF67.js";
10
+ export {
11
+ helpText,
12
+ parseCliArgs
13
+ };
package/dist/args.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { type PermissionMode } from './config';
2
+ import { type ReasoningLevel } from './reasoning';
3
+ import { type Lang } from './i18n';
4
+ export interface CliArgs {
5
+ command: 'run' | 'login' | 'models' | 'update' | 'acp' | 'serve' | 'plugins' | 'gh-bot' | 'provider' | 'mcp' | 'connect' | 'disconnect' | 'integrations' | 'completion' | 'stats' | 'export' | 'import' | 'pr';
6
+ subArgs: string[];
7
+ url?: string;
8
+ name?: string;
9
+ keyEnv?: string;
10
+ values: string[];
11
+ prompt?: string;
12
+ print: boolean;
13
+ model?: string;
14
+ reasoning?: ReasoningLevel;
15
+ lang?: Lang;
16
+ continue: boolean;
17
+ resume: boolean;
18
+ allowAll: boolean;
19
+ allowedTools: string[];
20
+ worktree?: string;
21
+ outputFormat?: 'text' | 'json' | 'stream-json';
22
+ permissionMode?: PermissionMode;
23
+ help: boolean;
24
+ version: boolean;
25
+ loginProvider?: string;
26
+ /** `bccli login` — pakai prompt API key lama, bukan OAuth. */
27
+ key: boolean;
28
+ /** `bccli login` — hapus credential tersimpan (hanya openrouter via OAuth path). */
29
+ logout: boolean;
30
+ /** `bccli serve` listener port (0 = ephemeral). */
31
+ port?: number;
32
+ /** `bccli serve` bind address — loopback only. */
33
+ host?: string;
34
+ /** `bccli serve` bearer token override. */
35
+ token?: string;
36
+ /** Base URL override for `bccli mcp serve`. */
37
+ baseUrl?: string;
38
+ /** `bccli stats` — only count sessions from the last N days (default 7). */
39
+ days?: number;
40
+ /** `bccli stats` - cap the tool-usage list to the top N tools. */
41
+ tools?: number;
42
+ /** `bccli stats [--models [N|all]]` - show per-model token usage (top N or all). */
43
+ models?: string;
44
+ /** `bccli stats --project <slug>` - filter rows; empty string = current project. */
45
+ project?: string;
46
+ /** `bccli pr` — explicit owner/name repo when cwd is not a git checkout of the PR. */
47
+ repo?: string;
48
+ }
49
+ /** Help text in the current language (call it after the language is set). */
50
+ export declare const helpText: () => string;
51
+ export declare function parseCliArgs(argv: string[]): CliArgs;
@@ -0,0 +1,23 @@
1
+ export interface UsageCap {
2
+ /** Total tokens (input + output) per session. */
3
+ tokens?: number;
4
+ /** Total USD per session; requires `prices`. */
5
+ usd?: number;
6
+ /** USD per 1M tokens, keyed by a substring of the model ref (first match wins). */
7
+ prices?: Record<string, {
8
+ input: number;
9
+ output: number;
10
+ }>;
11
+ }
12
+ export declare function priceFor(cap: UsageCap, model: string): {
13
+ input: number;
14
+ output: number;
15
+ } | undefined;
16
+ export declare function budgetStatus(usage: {
17
+ inputTokens: number;
18
+ outputTokens: number;
19
+ }, cap: UsageCap | undefined, model: string): {
20
+ kind: 'tokens' | 'usd';
21
+ used: number;
22
+ limit: number;
23
+ } | undefined;
@@ -0,0 +1,34 @@
1
+ interface UndoResult {
2
+ restored: string[];
3
+ deleted: string[];
4
+ skipped: string[];
5
+ failed: string[];
6
+ }
7
+ export declare class CheckpointStore {
8
+ private readonly history;
9
+ private current;
10
+ private pendingRedo;
11
+ beginTurn(): void;
12
+ /** Call before writing a file; only the first snapshot per file per turn is kept. */
13
+ snapshot(path: string): Promise<void>;
14
+ turns(): number;
15
+ /** Total entries including empty ones; used to align file snapshots with conversation turns. */
16
+ entries(): number;
17
+ undo(): Promise<UndoResult | undefined>;
18
+ /**
19
+ * Revert every snapshot taken at or after the `keep`-th entry, newest first, so the files
20
+ * end in the state from before the turn that created entry `keep`. Used by /rewind, which
21
+ * pairs conversation turns with the entry count recorded when each turn started.
22
+ */
23
+ undoTo(keep: number): Promise<UndoResult | undefined>;
24
+ /**
25
+ * Re-apply the file state captured right before the last undo/undoTo.
26
+ * Only file edits are redone, never conversation history. Invalidated by
27
+ * any new snapshot turn or clear(), so redo can't resurrect stale content.
28
+ */
29
+ redo(): Promise<UndoResult | undefined>;
30
+ /** Records the on-disk state of a turn's paths so redo() can put it back. */
31
+ private captureFor;
32
+ clear(): void;
33
+ }
34
+ export {};
@@ -1,12 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ConfigError
4
- } from "./chunk-2MGINW4O.js";
3
+ ConfigError,
4
+ writeFileAtomic
5
+ } from "./chunk-FLQPE4S5.js";
6
+ import {
7
+ t
8
+ } from "./chunk-UZWJQF67.js";
5
9
 
6
10
  // src/mcp/config.ts
7
11
  import { createHash } from "crypto";
8
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { existsSync, mkdirSync, readFileSync } from "fs";
9
13
  import { dirname, join, resolve } from "path";
14
+ var ENV_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
15
+ function expandEnvVars(config, env) {
16
+ const expand = (value) => value.replace(ENV_REF, (match, name, fallback) => {
17
+ const resolved = env[name];
18
+ if (resolved !== void 0) return resolved;
19
+ return fallback !== void 0 ? fallback : match;
20
+ });
21
+ const expandMap = (map) => Object.fromEntries(Object.entries(map).map(([k, v]) => [k, expand(v)]));
22
+ if (config.type === "http") {
23
+ return { ...config, url: expand(config.url), ...config.headers ? { headers: expandMap(config.headers) } : {} };
24
+ }
25
+ return {
26
+ ...config,
27
+ command: expand(config.command),
28
+ ...config.args ? { args: config.args.map(expand) } : {},
29
+ ...config.env ? { env: expandMap(config.env) } : {}
30
+ };
31
+ }
10
32
  var globalMcpPath = (home) => join(home, "mcp.json");
11
33
  var projectMcpPath = (cwd) => join(cwd, ".bccli", "mcp.json");
12
34
  var trustPath = (home) => join(home, "trusted-mcp.json");
@@ -15,14 +37,13 @@ function readJson(path, fallback) {
15
37
  try {
16
38
  return JSON.parse(readFileSync(path, "utf8"));
17
39
  } catch (error) {
18
- throw new ConfigError(`File MCP tidak valid: ${path} (${error.message})`);
40
+ throw new ConfigError(t("Invalid MCP file: {path} ({error})", { path, error: error.message }));
19
41
  }
20
42
  }
21
43
  function writePrivate(path, data) {
22
44
  mkdirSync(dirname(path), { recursive: true });
23
- writeFileSync(path, `${JSON.stringify(data, null, 2)}
24
- `, { mode: 384 });
25
- chmodSync(path, 384);
45
+ writeFileAtomic(path, `${JSON.stringify(data, null, 2)}
46
+ `, 384);
26
47
  }
27
48
  function readMcpFile(path) {
28
49
  return readJson(path, {}).mcpServers ?? {};
@@ -58,6 +79,7 @@ function serversToStart(home, cwd) {
58
79
  }
59
80
 
60
81
  export {
82
+ expandEnvVars,
61
83
  globalMcpPath,
62
84
  projectMcpPath,
63
85
  readMcpFile,
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ defineTool
4
+ } from "./chunk-PSO7R5AD.js";
5
+ import {
6
+ t
7
+ } from "./chunk-UZWJQF67.js";
8
+
9
+ // src/tools/websearch.ts
10
+ import { z } from "zod";
11
+ var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
12
+ title: String(r.title || r.url).slice(0, 200),
13
+ url: r.url,
14
+ snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
15
+ }));
16
+ async function postJson(url, body, headers, signal) {
17
+ const res = await fetch(url, {
18
+ method: "POST",
19
+ headers: { "content-type": "application/json", ...headers },
20
+ body: JSON.stringify(body),
21
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
22
+ });
23
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
24
+ return res.json();
25
+ }
26
+ function createWebSearchTool(opts) {
27
+ const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
28
+ return defineTool({
29
+ name: "web_search",
30
+ description: "Search the web. Returns titles, URLs and short snippets. Use it to find the right pages instead of guessing URLs; then fetch a result URL when you need the full page.",
31
+ schema: z.object({
32
+ query: z.string().min(1).max(500).describe("Search query"),
33
+ max_results: z.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
34
+ }),
35
+ kind: "read",
36
+ target: (input) => input.query,
37
+ parallelSafe: () => true,
38
+ async run(input, ctx) {
39
+ const max = input.max_results ?? 6;
40
+ const errors = [];
41
+ let results = [];
42
+ const bc = opts.botconnector();
43
+ if (bc) {
44
+ try {
45
+ const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
46
+ results = clean(data.results, max);
47
+ } catch (error) {
48
+ errors.push(`BotConnector: ${error.message}`);
49
+ }
50
+ }
51
+ if (!results.length) {
52
+ try {
53
+ const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
54
+ results = clean(data.results, max);
55
+ } catch (error) {
56
+ errors.push(`Keenable: ${error.message}`);
57
+ }
58
+ }
59
+ if (!results.length) {
60
+ return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
61
+ }
62
+ const output = results.map((r, i) => `${i + 1}. ${r.title}
63
+ ${r.url}${r.snippet ? `
64
+ ${r.snippet}` : ""}`).join("\n");
65
+ return { output, display: t("{n} results", { n: results.length }) };
66
+ }
67
+ });
68
+ }
69
+
70
+ // src/tools/fetch.ts
71
+ import { lookup as dnsCbLookup } from "dns";
72
+ import { lookup as dnsLookup } from "dns/promises";
73
+ import { isIP } from "net";
74
+ import { Agent, fetch as undiciFetch } from "undici";
75
+ import { z as z2 } from "zod";
76
+ var MAX_CHARS = 12e3;
77
+ var FOCUS_CHUNKS = 3;
78
+ var FOCUS_SIZE = 800;
79
+ var CACHE_MAX = 16;
80
+ var CACHE_TTL_MS = 15 * 6e4;
81
+ var CACHE_MAX_CHARS = 2e6;
82
+ var MAX_BYTES = 5 * 1024 * 1024;
83
+ async function readCapped(res) {
84
+ const reader = res.body?.getReader();
85
+ if (!reader) return { text: "", truncated: false };
86
+ const chunks = [];
87
+ let size = 0;
88
+ for (; ; ) {
89
+ const { done, value } = await reader.read();
90
+ if (done) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
91
+ size += value.length;
92
+ if (size > MAX_BYTES) {
93
+ chunks.push(value.subarray(0, value.length - (size - MAX_BYTES)));
94
+ await reader.cancel();
95
+ return { text: Buffer.concat(chunks).toString("utf8"), truncated: true };
96
+ }
97
+ chunks.push(value);
98
+ }
99
+ }
100
+ function htmlToText(html) {
101
+ return html.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<(br|\/p|\/div|\/h[1-6]|\/li|\/tr)\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
102
+ }
103
+ function describeFetchError(error) {
104
+ const cause = error.cause;
105
+ const code = cause?.code ?? "";
106
+ const detail = cause?.message ?? error.message;
107
+ if (error.name === "TimeoutError") return "no response within 30 seconds";
108
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS could not find the host (typo, offline, or blocked by DNS/ISP)`;
109
+ if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
110
+ return `${detail} \u2014 TLS certificate rejected; often caused by an antivirus or proxy that inspects HTTPS. Try running with NODE_OPTIONS=--use-system-ca`;
111
+ }
112
+ if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
113
+ return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
114
+ }
115
+ return code ? `${detail} (${code})` : detail;
116
+ }
117
+ var tokensOf = (text) => Math.ceil(text.length / 4);
118
+ function isPrivateHost(host) {
119
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
120
+ if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
121
+ if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
122
+ const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
123
+ if (v4) {
124
+ const [a, b] = [Number(v4[1]), Number(v4[2])];
125
+ return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
126
+ }
127
+ return !h.includes(".");
128
+ }
129
+ var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
130
+ function pinLookup(pinned) {
131
+ return (hostname, options, callback) => {
132
+ const opts = typeof options === "object" && options !== null ? options : {};
133
+ const family = typeof options === "number" ? options : opts.family;
134
+ const realDns = () => dnsCbLookup(hostname, typeof options === "number" ? { family: options } : opts, callback);
135
+ if (!pinned.length) return realDns();
136
+ const list = family ? pinned.filter((a) => a.family === family) : pinned;
137
+ if (!list.length) {
138
+ callback(Object.assign(new Error(`no pinned address for family ${family}`), { code: "ENOTFOUND" }), "");
139
+ return;
140
+ }
141
+ if (opts.all) callback(null, list);
142
+ else callback(null, list[0].address, list[0].family);
143
+ };
144
+ }
145
+ function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
146
+ const chunks = [];
147
+ let cur = "";
148
+ for (const line of text.split("\n")) {
149
+ for (let i = 0; i < Math.max(line.length, 1); i += size) {
150
+ cur += `${line.slice(i, i + size)}
151
+ `;
152
+ if (cur.length >= size) {
153
+ chunks.push(cur.trimEnd());
154
+ cur = "";
155
+ }
156
+ }
157
+ }
158
+ if (cur.trim()) chunks.push(cur.trimEnd());
159
+ const q = [...new Set(words(query))];
160
+ const docs = chunks.map(words);
161
+ const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
162
+ const idf = q.map((t2) => {
163
+ const df = docs.filter((d) => d.includes(t2)).length;
164
+ return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
165
+ });
166
+ const scored = docs.map((d, i) => ({
167
+ i,
168
+ score: q.reduce((sum, t2, k) => {
169
+ const tf = d.filter((w) => w === t2).length;
170
+ return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
171
+ }, 0)
172
+ })).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
173
+ return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
174
+ }
175
+ var cache = /* @__PURE__ */ new Map();
176
+ function remember(key, value, size) {
177
+ if (size > CACHE_MAX_CHARS) return;
178
+ cache.delete(key);
179
+ cache.set(key, value);
180
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
181
+ }
182
+ function recall(key) {
183
+ const hit = cache.get(key);
184
+ if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
185
+ cache.delete(key);
186
+ return void 0;
187
+ }
188
+ return hit;
189
+ }
190
+ var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
191
+ var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
192
+ function createFetchTool(opts = {}) {
193
+ const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
194
+ const isPrivate = opts.isPrivate ?? isPrivateHost;
195
+ const lookup = opts.lookup ?? ((hostname) => dnsLookup(hostname, { all: true }));
196
+ return defineTool({
197
+ name: "fetch",
198
+ description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
199
+ - Pass \`prompt\` (what you want to know from the page) to get only the relevant excerpt instead of the whole page. Prefer this whenever you are looking for specific facts.
200
+ - Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
201
+ - Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
202
+ schema: z2.object({
203
+ url: z2.string().describe("Absolute http(s) URL"),
204
+ prompt: z2.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
205
+ offset: z2.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
206
+ }),
207
+ kind: "fetch",
208
+ target: (input) => input.url,
209
+ async run(input, ctx) {
210
+ if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
211
+ const target = new URL(input.url);
212
+ let pinned;
213
+ if (!isPrivate(target.hostname) && isIP(target.hostname) === 0) {
214
+ try {
215
+ const addresses = await lookup(target.hostname);
216
+ const internal = addresses.find((a) => isPrivate(a.address));
217
+ if (internal) {
218
+ return {
219
+ output: t("Blocked: {host} resolves to a private address ({addr}). Fetching an internal host through a public DNS name is not allowed.", { host: target.hostname, addr: internal.address }),
220
+ isError: true
221
+ };
222
+ }
223
+ pinned = addresses;
224
+ } catch {
225
+ }
226
+ }
227
+ const prompt = input.prompt?.trim();
228
+ const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
229
+ if (askKeenable) {
230
+ const key = `x
231
+ ${input.url}
232
+ ${prompt}`;
233
+ const hit = recall(key);
234
+ if (hit) {
235
+ ctx.refundFetch?.();
236
+ return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
237
+ }
238
+ const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
239
+ if (out) {
240
+ const output2 = `[Excerpt from ${input.url} for "${prompt}" (extracted by Keenable). If it says the answer is not there, try fetch without a prompt.]
241
+ ${out}`;
242
+ remember(key, output2, output2.length);
243
+ return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
244
+ }
245
+ }
246
+ let page = recall(`p
247
+ ${input.url}`);
248
+ const cached = !!page;
249
+ if (cached) ctx.refundFetch?.();
250
+ if (!page) {
251
+ const loaded = await loadPage(input.url, ctx.signal, pinned, opts.fetchPage);
252
+ if ("error" in loaded) return { output: loaded.error, isError: true };
253
+ page = loaded;
254
+ remember(`p
255
+ ${input.url}`, page, page.text.length);
256
+ }
257
+ const { text, truncated } = page;
258
+ const via = cached ? " \xB7 cache" : "";
259
+ if (prompt && !input.offset) {
260
+ const picked = pickRelevant(text, prompt);
261
+ if (picked) {
262
+ const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
263
+ ${picked}`;
264
+ return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
265
+ }
266
+ }
267
+ const offset = input.offset ?? 0;
268
+ if (offset >= text.length && text.length > 0) {
269
+ return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
270
+ }
271
+ const end = Math.min(offset + MAX_CHARS, text.length);
272
+ let output = text.slice(offset, end);
273
+ if (end < text.length || truncated) {
274
+ const total = truncated ? `${text.length}+` : text.length;
275
+ output += `
276
+ \u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
277
+ }
278
+ const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
279
+ return { output, display: withCost(output, `${size}${via}`) };
280
+ }
281
+ });
282
+ }
283
+ async function extractWithKeenable(endpoint, url, prompt, signal) {
284
+ try {
285
+ const u = new URL(endpoint);
286
+ u.searchParams.set("url", url);
287
+ u.searchParams.set("prompt", prompt);
288
+ u.searchParams.set("live", "true");
289
+ u.searchParams.set("max_chars", String(MAX_CHARS));
290
+ const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
291
+ if (!res.ok) return void 0;
292
+ const content = (await res.json()).content;
293
+ return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
294
+ } catch {
295
+ return void 0;
296
+ }
297
+ }
298
+ async function loadPage(inputUrl, signal, pinned, fetchPage) {
299
+ let url = new URL(inputUrl);
300
+ const dispatcher = pinned?.length ? new Agent({ connect: { lookup: pinLookup(pinned) } }) : void 0;
301
+ const doFetch = fetchPage ?? (dispatcher ? undiciFetch : fetch);
302
+ try {
303
+ let res;
304
+ for (let hop = 0; hop < 5; hop++) {
305
+ res = await doFetch(url, {
306
+ headers: { accept: ACCEPT },
307
+ signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]),
308
+ redirect: "manual",
309
+ ...dispatcher ? { dispatcher } : {}
310
+ });
311
+ const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
312
+ if (!location) break;
313
+ const next = new URL(location, url);
314
+ if (next.host !== url.host) {
315
+ await res.body?.cancel();
316
+ return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
317
+ }
318
+ await res.body?.cancel();
319
+ url = next;
320
+ }
321
+ if (!res) return { error: `Failed to fetch ${inputUrl}` };
322
+ const { text: body, truncated } = await readCapped(res);
323
+ const type = res.headers.get("content-type") ?? "";
324
+ const text = type.includes("html") ? htmlToText(body) : body;
325
+ if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
326
+ return { text, truncated, at: Date.now() };
327
+ } catch (error) {
328
+ return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
329
+ } finally {
330
+ await dispatcher?.close().catch(() => {
331
+ });
332
+ }
333
+ }
334
+ var fetchTool = createFetchTool();
335
+
336
+ export {
337
+ isPrivateHost,
338
+ createFetchTool,
339
+ fetchTool,
340
+ createWebSearchTool
341
+ };