@botconnector/bccli 0.5.0-rc.4 → 0.5.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.
- package/LICENSE +21 -203
- package/README.en.md +253 -0
- package/README.md +250 -0
- package/dist/abort.d.ts +5 -0
- package/dist/acp-HWHF25KU.js +221 -0
- package/dist/acp.d.ts +44 -0
- package/dist/agent.d.ts +158 -0
- package/dist/agents.d.ts +4 -0
- package/dist/args-XA77XUMD.js +13 -0
- package/dist/args.d.ts +59 -0
- package/dist/budget.d.ts +26 -0
- package/dist/checkpoints.d.ts +35 -0
- package/dist/chunk-2IP5F3EK.js +56 -0
- package/dist/chunk-5CVHTYBQ.js +270 -0
- package/dist/chunk-6LO6YRDY.js +216 -0
- package/dist/chunk-6PJRWKVI.js +50 -0
- package/dist/chunk-6YRTPXGY.js +8 -0
- package/dist/chunk-7OTFZK4B.js +159 -0
- package/dist/chunk-7U7CMKKI.js +861 -0
- package/dist/chunk-A3NBWIEQ.js +2706 -0
- package/dist/chunk-FY543EQW.js +50 -0
- package/dist/chunk-IDZLHEVL.js +524 -0
- package/dist/chunk-IF5RNXBI.js +341 -0
- package/dist/chunk-M5T4AM4I.js +256 -0
- package/dist/chunk-NGFWBVHD.js +71 -0
- package/dist/chunk-OCEQ2TRJ.js +1075 -0
- package/dist/chunk-OK2USAXX.js +88 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-RLSQAZBT.js +78 -0
- package/dist/chunk-VPFCIUC6.js +91 -0
- package/dist/chunk-YGBTNDPK.js +202 -0
- package/dist/chunk-YYTFJUO2.js +148 -0
- package/dist/chunk-ZXWRLN7E.js +378 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +251 -0
- package/dist/clipboardImage.d.ts +17 -0
- package/dist/commands.d.ts +16 -0
- package/dist/completion-IRVEVW6H.js +125 -0
- package/dist/completion.d.ts +14 -0
- package/dist/config-OVWCUVY7.js +25 -0
- package/dist/config-S3CZUIBB.js +34 -0
- package/dist/config.d.ts +99 -0
- package/dist/context.d.ts +13 -0
- package/dist/contextWindow.d.ts +3 -0
- package/dist/diff.d.ts +7 -0
- package/dist/exit.d.ts +9 -0
- package/dist/extensions.d.ts +34 -0
- package/dist/ghBotCli-FYKJDJOD.js +680 -0
- package/dist/ghBotCli.d.ts +9 -0
- package/dist/ghBotDispatch.d.ts +68 -0
- package/dist/ghBotRunner.d.ts +82 -0
- package/dist/ghBotServe.d.ts +21 -0
- package/dist/ghBotWebhook.d.ts +45 -0
- package/dist/hooks.d.ts +27 -0
- package/dist/i18n/id.d.ts +1 -0
- package/dist/i18n.d.ts +16 -0
- package/dist/integrationCli-GY73S2K7.js +876 -0
- package/dist/integrationCli.d.ts +9 -0
- package/dist/keybinds.d.ts +22 -0
- package/dist/login-KSN63EM4.js +18 -0
- package/dist/login.d.ts +21 -0
- package/dist/lsp.d.ts +73 -0
- package/dist/mcp/catalog.d.ts +14 -0
- package/dist/mcp/config.d.ts +36 -0
- package/dist/mcp/manager.d.ts +52 -0
- package/dist/mcp/oauth.d.ts +82 -0
- package/dist/mcpCli-VWSCCDWT.js +113 -0
- package/dist/mcpCli.d.ts +3 -0
- package/dist/mcpServer/serve.d.ts +8 -0
- package/dist/mcpServer/server.d.ts +19 -0
- package/dist/models.d.ts +13 -0
- package/dist/modelsCli-4Q6KFWV2.js +36 -0
- package/dist/modelsCli.d.ts +15 -0
- package/dist/npmRunner-LU43GM3W.js +46 -0
- package/dist/npmRunner.d.ts +21 -0
- package/dist/oauthLogin-JWXQZTMZ.js +210 -0
- package/dist/oauthLogin.d.ts +47 -0
- package/dist/onboarding.d.ts +6 -0
- package/dist/permissions.d.ts +30 -0
- package/dist/pluginSpec.d.ts +25 -0
- package/dist/plugins.d.ts +45 -0
- package/dist/pluginsCli-7XWDLJEL.js +288 -0
- package/dist/pluginsCli.d.ts +16 -0
- package/dist/prCli-FH7N26D4.js +94 -0
- package/dist/prCli.d.ts +46 -0
- package/dist/presets.d.ts +8 -0
- package/dist/print.d.ts +17 -0
- package/dist/provider.d.ts +145 -0
- package/dist/providerCli-YMG4G56R.js +15 -0
- package/dist/providerCli.d.ts +17 -0
- package/dist/providers.d.ts +6 -0
- package/dist/reasoning.d.ts +12 -0
- package/dist/repomap.d.ts +10 -0
- package/dist/sdk.d.ts +61 -0
- package/dist/sdk.js +6532 -0
- package/dist/serve-HSFF6MGB.js +178 -0
- package/dist/serve-R2IHP26A.js +532 -0
- package/dist/serve.d.ts +37 -0
- package/dist/serveHeartbeat.d.ts +14 -0
- package/dist/serveProtocol.d.ts +47 -0
- package/dist/session-YM36TGU7.js +12 -0
- package/dist/session.d.ts +33 -0
- package/dist/setup-3YB6EKYX.js +24 -0
- package/dist/setup.d.ts +79 -0
- package/dist/slash/copy.d.ts +1 -0
- package/dist/slash/diff.d.ts +1 -0
- package/dist/slash/doctor.d.ts +18 -0
- package/dist/slash/export.d.ts +2 -0
- package/dist/slash/info.d.ts +46 -0
- package/dist/slash/memory.d.ts +10 -0
- package/dist/statsCli-I4TSDU3X.js +353 -0
- package/dist/statsCli.d.ts +62 -0
- package/dist/textToolCalls.d.ts +6 -0
- package/dist/thinking.d.ts +17 -0
- package/dist/tokenBudget.d.ts +22 -0
- package/dist/tools/bash.d.ts +54 -0
- package/dist/tools/diagnostics.d.ts +13 -0
- package/dist/tools/edit.d.ts +12 -0
- package/dist/tools/fetch.d.ts +41 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/globSafety.d.ts +7 -0
- package/dist/tools/grep.d.ts +25 -0
- package/dist/tools/ignore.d.ts +2 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/paths.d.ts +4 -0
- package/dist/tools/plan.d.ts +10 -0
- package/dist/tools/read.d.ts +16 -0
- package/dist/tools/repomap.d.ts +2 -0
- package/dist/tools/sandbox.d.ts +15 -0
- package/dist/tools/shell.d.ts +18 -0
- package/dist/tools/skill.d.ts +3 -0
- package/dist/tools/task.d.ts +28 -0
- package/dist/tools/todo.d.ts +13 -0
- package/dist/tools/types.d.ts +52 -0
- package/dist/tools/websearch.d.ts +9 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/transferCli-3B4LMOYI.js +73 -0
- package/dist/transferCli.d.ts +23 -0
- package/dist/ui/App.d.ts +7 -0
- package/dist/ui/DiffView.d.ts +3 -0
- package/dist/ui/EditableText.d.ts +7 -0
- package/dist/ui/LinePrompt.d.ts +6 -0
- package/dist/ui/ListPicker.d.ts +11 -0
- package/dist/ui/Markdown.d.ts +8 -0
- package/dist/ui/McpMenu.d.ts +11 -0
- package/dist/ui/ModelPicker.d.ts +6 -0
- package/dist/ui/PermissionPrompt.d.ts +5 -0
- package/dist/ui/PlanApproval.d.ts +5 -0
- package/dist/ui/PromptInput.d.ts +20 -0
- package/dist/ui/ProviderMenu.d.ts +10 -0
- package/dist/ui/Spinner.d.ts +4 -0
- package/dist/ui/StatusBar.d.ts +14 -0
- package/dist/ui/TodoList.d.ts +4 -0
- package/dist/ui/ToolBlock.d.ts +14 -0
- package/dist/ui/complete.d.ts +8 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/lineEdit.d.ts +42 -0
- package/dist/ui/liveCap.d.ts +29 -0
- package/dist/ui/markdownLayout.d.ts +23 -0
- package/dist/ui/replay.d.ts +4 -0
- package/dist/ui/stdinFilter.d.ts +18 -0
- package/dist/ui/table.d.ts +20 -0
- package/dist/ui/theme.d.ts +5 -0
- package/dist/ui/thinkingMouse.d.ts +38 -0
- package/dist/ui/transcript.d.ts +55 -0
- package/dist/ui/tty.d.ts +11 -0
- package/dist/ui/vim.d.ts +26 -0
- package/dist/ui-TAZMRXRI.js +3044 -0
- package/dist/update-XC3B6SJC.js +145 -0
- package/dist/update.d.ts +30 -0
- package/dist/verify.d.ts +16 -0
- package/dist/version.d.ts +1 -0
- package/dist/webBudget.d.ts +25 -0
- package/dist/worktree.d.ts +12 -0
- package/package.json +59 -35
- package/THIRD_PARTY_NOTICES.md +0 -9
- package/branding.mjs +0 -347
- package/cli.mjs +0 -120
- package/core.mjs +0 -260
- package/credentials.mjs +0 -52
- package/gateway.mjs +0 -2
- package/model-command.mjs +0 -61
- package/provider-cli.mjs +0 -77
- package/providers.mjs +0 -166
- package/proxy.mjs +0 -242
- package/startup-menu.mjs +0 -120
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
helpText,
|
|
4
|
+
parseCliArgs
|
|
5
|
+
} from "./chunk-M5T4AM4I.js";
|
|
6
|
+
import "./chunk-YYTFJUO2.js";
|
|
7
|
+
import "./chunk-NGFWBVHD.js";
|
|
8
|
+
import "./chunk-ZXWRLN7E.js";
|
|
9
|
+
import "./chunk-IDZLHEVL.js";
|
|
10
|
+
export {
|
|
11
|
+
helpText,
|
|
12
|
+
parseCliArgs
|
|
13
|
+
};
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
/** `bccli mcp add <name> --command <cmd>`: server stdio, bukan HTTP. */
|
|
9
|
+
stdioCommand?: string;
|
|
10
|
+
/** Argumen untuk server stdio (dipisah spasi). */
|
|
11
|
+
stdioArgs?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
keyEnv?: string;
|
|
14
|
+
values: string[];
|
|
15
|
+
prompt?: string;
|
|
16
|
+
print: boolean;
|
|
17
|
+
model?: string;
|
|
18
|
+
reasoning?: ReasoningLevel;
|
|
19
|
+
lang?: Lang;
|
|
20
|
+
continue: boolean;
|
|
21
|
+
resume: boolean;
|
|
22
|
+
allowAll: boolean;
|
|
23
|
+
/** `-p` untuk CI: kegagalan tool apa pun membuat exit code 1. */
|
|
24
|
+
failOnToolError: boolean;
|
|
25
|
+
allowedTools: string[];
|
|
26
|
+
worktree?: string;
|
|
27
|
+
outputFormat?: 'text' | 'json' | 'stream-json';
|
|
28
|
+
permissionMode?: PermissionMode;
|
|
29
|
+
help: boolean;
|
|
30
|
+
version: boolean;
|
|
31
|
+
loginProvider?: string;
|
|
32
|
+
/** `bccli login` — pakai prompt API key lama, bukan OAuth. */
|
|
33
|
+
key: boolean;
|
|
34
|
+
/** `bccli login` — hapus credential tersimpan (hanya openrouter via OAuth path). */
|
|
35
|
+
logout: boolean;
|
|
36
|
+
/** `bccli serve` listener port (0 = ephemeral). */
|
|
37
|
+
port?: number;
|
|
38
|
+
/** `bccli serve` bind address — loopback only. */
|
|
39
|
+
host?: string;
|
|
40
|
+
/** `bccli serve` bearer token override. */
|
|
41
|
+
token?: string;
|
|
42
|
+
/** Base URL override for `bccli mcp serve`. */
|
|
43
|
+
baseUrl?: string;
|
|
44
|
+
/** `bccli stats` — only count sessions from the last N days (default 7). */
|
|
45
|
+
days?: number;
|
|
46
|
+
/** `bccli stats` - cap the tool-usage list to the top N tools. */
|
|
47
|
+
tools?: number;
|
|
48
|
+
/** `bccli stats [--models [N|all]]` - show per-model token usage (top N or all). */
|
|
49
|
+
models?: string;
|
|
50
|
+
/** `bccli stats --project <slug>` - filter rows; empty string = current project. */
|
|
51
|
+
project?: string;
|
|
52
|
+
/** `bccli pr` — explicit owner/name repo when cwd is not a git checkout of the PR. */
|
|
53
|
+
repo?: string;
|
|
54
|
+
/** Automatic fallback model ref when the primary model fails with 429 or 5xx. */
|
|
55
|
+
fallbackModel?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Help text in the current language (call it after the language is set). */
|
|
58
|
+
export declare const helpText: () => string;
|
|
59
|
+
export declare function parseCliArgs(argv: string[]): CliArgs;
|
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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. `cachedInput` is optional: without it cache reads cost the input price. */
|
|
7
|
+
prices?: Record<string, {
|
|
8
|
+
input: number;
|
|
9
|
+
output: number;
|
|
10
|
+
cachedInput?: number;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export declare function priceFor(cap: UsageCap, model: string): {
|
|
14
|
+
input: number;
|
|
15
|
+
output: number;
|
|
16
|
+
cachedInput?: number;
|
|
17
|
+
} | undefined;
|
|
18
|
+
export declare function budgetStatus(usage: {
|
|
19
|
+
inputTokens: number;
|
|
20
|
+
outputTokens: number;
|
|
21
|
+
cachedInputTokens?: number;
|
|
22
|
+
}, cap: UsageCap | undefined, model: string): {
|
|
23
|
+
kind: 'tokens' | 'usd';
|
|
24
|
+
used: number;
|
|
25
|
+
limit: number;
|
|
26
|
+
} | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
private offset;
|
|
12
|
+
beginTurn(): void;
|
|
13
|
+
/** Call before writing a file; only the first snapshot per file per turn is kept. */
|
|
14
|
+
snapshot(path: string): Promise<void>;
|
|
15
|
+
turns(): number;
|
|
16
|
+
/** Total entries including empty ones; used to align file snapshots with conversation turns. */
|
|
17
|
+
entries(): number;
|
|
18
|
+
undo(): Promise<UndoResult | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Revert every snapshot taken at or after the `keep`-th entry, newest first, so the files
|
|
21
|
+
* end in the state from before the turn that created entry `keep`. Used by /rewind, which
|
|
22
|
+
* pairs conversation turns with the entry count recorded when each turn started.
|
|
23
|
+
*/
|
|
24
|
+
undoTo(keep: number): Promise<UndoResult | undefined>;
|
|
25
|
+
/**
|
|
26
|
+
* Re-apply the file state captured right before the last undo/undoTo.
|
|
27
|
+
* Only file edits are redone, never conversation history. Invalidated by
|
|
28
|
+
* any new snapshot turn or clear(), so redo can't resurrect stale content.
|
|
29
|
+
*/
|
|
30
|
+
redo(): Promise<UndoResult | undefined>;
|
|
31
|
+
/** Records the on-disk state of a turn's paths so redo() can put it back. */
|
|
32
|
+
private captureFor;
|
|
33
|
+
clear(): void;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
hasKey,
|
|
4
|
+
providerName
|
|
5
|
+
} from "./chunk-FY543EQW.js";
|
|
6
|
+
import {
|
|
7
|
+
createProvider
|
|
8
|
+
} from "./chunk-OCEQ2TRJ.js";
|
|
9
|
+
import {
|
|
10
|
+
PRESETS,
|
|
11
|
+
readCredentials
|
|
12
|
+
} from "./chunk-ZXWRLN7E.js";
|
|
13
|
+
import {
|
|
14
|
+
t
|
|
15
|
+
} from "./chunk-IDZLHEVL.js";
|
|
16
|
+
|
|
17
|
+
// src/models.ts
|
|
18
|
+
function order(ids) {
|
|
19
|
+
const presetIds = PRESETS.map((p) => p.id);
|
|
20
|
+
const presets = presetIds.filter((id) => ids.includes(id));
|
|
21
|
+
const custom = ids.filter((id) => !presetIds.includes(id)).sort();
|
|
22
|
+
return [...presets, ...custom];
|
|
23
|
+
}
|
|
24
|
+
async function listAllModels(config, env = process.env, opts = {}) {
|
|
25
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
26
|
+
const baseFetch = opts.fetch ?? fetch;
|
|
27
|
+
const untrusted = new Set(config.projectProviders ?? []);
|
|
28
|
+
const ids = order(Object.keys(config.providers)).filter((id) => opts.only ? id === opts.only : !untrusted.has(id) && hasKey(config, id, env));
|
|
29
|
+
const results = await Promise.all(
|
|
30
|
+
ids.map(async (id) => {
|
|
31
|
+
const provider = config.providers[id];
|
|
32
|
+
const apiKey = (opts.keyOverride && id === opts.only ? opts.keyOverride : void 0) || readCredentials(env)[id] || (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || void 0;
|
|
33
|
+
const local = !provider.apiKeyEnv;
|
|
34
|
+
const timedFetch = ((input, init) => baseFetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
35
|
+
const list = createProvider({ baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey, model: "", fetch: timedFetch }).listModels();
|
|
36
|
+
let timer;
|
|
37
|
+
const timeout = new Promise((_, reject) => {
|
|
38
|
+
timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
const models = await Promise.race([list, timeout]);
|
|
42
|
+
return { providerId: id, providerName: providerName(config, id), models };
|
|
43
|
+
} catch {
|
|
44
|
+
if (local && !opts.only) return void 0;
|
|
45
|
+
return { providerId: id, providerName: providerName(config, id), models: [], error: t("could not be reached") };
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
return results.filter((g) => !!g);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
listAllModels
|
|
56
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runCommand,
|
|
4
|
+
runHooks
|
|
5
|
+
} from "./chunk-7U7CMKKI.js";
|
|
6
|
+
import {
|
|
7
|
+
ConfigError
|
|
8
|
+
} from "./chunk-ZXWRLN7E.js";
|
|
9
|
+
import {
|
|
10
|
+
t
|
|
11
|
+
} from "./chunk-IDZLHEVL.js";
|
|
12
|
+
|
|
13
|
+
// src/verify.ts
|
|
14
|
+
var MAX_VERIFY_ROUNDS = 2;
|
|
15
|
+
async function runVerify(commands, opts) {
|
|
16
|
+
const run = opts.run ?? runCommand;
|
|
17
|
+
const failures = [];
|
|
18
|
+
for (const cmd of commands) {
|
|
19
|
+
const r = await run(cmd, { cwd: opts.cwd, timeoutMs: opts.timeoutMs ?? 3e5, signal: opts.signal, env: opts.env });
|
|
20
|
+
const exitCode = r.exitCode ?? -1;
|
|
21
|
+
if (exitCode !== 0) failures.push({ cmd, exitCode, output: r.output });
|
|
22
|
+
}
|
|
23
|
+
return failures;
|
|
24
|
+
}
|
|
25
|
+
function verifyFollowup(failures, maxBytes = 8e3) {
|
|
26
|
+
const blocks = failures.map((f) => {
|
|
27
|
+
const output = f.output.length > maxBytes ? `${f.output.slice(0, maxBytes)}
|
|
28
|
+
[truncated]` : f.output;
|
|
29
|
+
return `$ ${f.cmd}
|
|
30
|
+
[exit code ${f.exitCode}]
|
|
31
|
+
${output}`;
|
|
32
|
+
});
|
|
33
|
+
return t("Automatic verification after your edits failed. Fix the problems so these commands pass, then stop.\n\n{blocks}", {
|
|
34
|
+
blocks: blocks.join("\n\n")
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/print.ts
|
|
39
|
+
function toolLabel(tool) {
|
|
40
|
+
if (tool === "web_search") return "Search";
|
|
41
|
+
return tool.charAt(0).toUpperCase() + tool.slice(1);
|
|
42
|
+
}
|
|
43
|
+
function resolvePrintPrompt(prompt, stdinText) {
|
|
44
|
+
const cleanStdin = stdinText?.trim();
|
|
45
|
+
const cleanPrompt = prompt?.trim();
|
|
46
|
+
if (cleanStdin && cleanPrompt) return `${cleanStdin}
|
|
47
|
+
|
|
48
|
+
${cleanPrompt}`;
|
|
49
|
+
return cleanPrompt || cleanStdin || void 0;
|
|
50
|
+
}
|
|
51
|
+
async function runPrint(rt, prompt, io = { out: process.stdout, err: process.stderr }, opts = {}) {
|
|
52
|
+
const format = opts.outputFormat ?? "text";
|
|
53
|
+
const emit = (line) => io.out.write(`${JSON.stringify(line)}
|
|
54
|
+
`);
|
|
55
|
+
const startedMs = Date.now();
|
|
56
|
+
let result = "";
|
|
57
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
58
|
+
rt.interaction.approvePlan = async () => {
|
|
59
|
+
if (opts.allowAll) return "allowAll";
|
|
60
|
+
io.err.write(`${t("Plan needs approval; run with --allow-all.")}
|
|
61
|
+
`);
|
|
62
|
+
return "no";
|
|
63
|
+
};
|
|
64
|
+
let failed = false;
|
|
65
|
+
let wroteText = false;
|
|
66
|
+
let edited = false;
|
|
67
|
+
rt.agent.onEvent = (event) => {
|
|
68
|
+
switch (event.type) {
|
|
69
|
+
case "text":
|
|
70
|
+
if (format === "text") {
|
|
71
|
+
io.out.write(event.delta);
|
|
72
|
+
wroteText = true;
|
|
73
|
+
} else {
|
|
74
|
+
result += event.delta;
|
|
75
|
+
if (format === "stream-json") emit({ type: "text_delta", delta: event.delta });
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "thinking":
|
|
79
|
+
if (format === "stream-json") emit({ type: "thinking_delta", delta: event.delta });
|
|
80
|
+
break;
|
|
81
|
+
case "textReplace":
|
|
82
|
+
result = event.text;
|
|
83
|
+
break;
|
|
84
|
+
case "toolStart":
|
|
85
|
+
toolNames.set(event.id, event.tool);
|
|
86
|
+
io.err.write(`\u23BF ${toolLabel(event.tool)} ${event.target}
|
|
87
|
+
`);
|
|
88
|
+
if (format === "stream-json") emit({ type: "tool_use", tool: event.tool, target: event.target });
|
|
89
|
+
break;
|
|
90
|
+
case "toolEnd":
|
|
91
|
+
if (event.isError) io.err.write(` \u2717 ${event.output.split("\n")[0]}
|
|
92
|
+
`);
|
|
93
|
+
if (event.isError && opts.failOnToolError) failed = true;
|
|
94
|
+
if ((event.tool === "edit" || event.tool === "write") && !event.isError) edited = true;
|
|
95
|
+
if (format === "stream-json") emit({ type: "tool_result", tool: toolNames.get(event.id), isError: !!event.isError, output: event.output });
|
|
96
|
+
break;
|
|
97
|
+
case "stepLimit":
|
|
98
|
+
io.err.write(`${t("Step limit reached.")}
|
|
99
|
+
`);
|
|
100
|
+
failed = true;
|
|
101
|
+
break;
|
|
102
|
+
case "budgetExceeded":
|
|
103
|
+
io.err.write(`${t("Budget exceeded ({kind}): {used} of {limit}. Further model calls are blocked; raise usageCap in ~/.bccli/config.json.", { kind: event.kind, used: Math.round(event.used * 100) / 100, limit: event.limit })}
|
|
104
|
+
`);
|
|
105
|
+
if (format === "stream-json") emit({ type: "error", message: "budgetExceeded" });
|
|
106
|
+
failed = true;
|
|
107
|
+
break;
|
|
108
|
+
case "aborted":
|
|
109
|
+
io.err.write(`${t("Cancelled.")}
|
|
110
|
+
`);
|
|
111
|
+
failed = true;
|
|
112
|
+
break;
|
|
113
|
+
case "error":
|
|
114
|
+
io.err.write(`Error: ${event.message}
|
|
115
|
+
`);
|
|
116
|
+
if (format === "stream-json") emit({ type: "error", message: event.message });
|
|
117
|
+
failed = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
rt.agent.askPermission = async (req) => {
|
|
122
|
+
io.err.write(` ${t("Needs permission for {tool}. Run with --allow-all or --allowed-tools {kind}.", { tool: req.tool, kind: req.kind })}
|
|
123
|
+
`);
|
|
124
|
+
return "no";
|
|
125
|
+
};
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const onSigint = () => controller.abort();
|
|
128
|
+
process.once("SIGINT", onSigint);
|
|
129
|
+
const onSigbreak = () => controller.abort();
|
|
130
|
+
const useSigbreak = process.platform === "win32";
|
|
131
|
+
if (useSigbreak) process.once("SIGBREAK", onSigbreak);
|
|
132
|
+
const start = await runHooks(rt.config.hooks, "SessionStart", {}, { env: rt.env, cwd: rt.cwd });
|
|
133
|
+
for (const warning of start.warnings) io.err.write(`${warning}
|
|
134
|
+
`);
|
|
135
|
+
await rt.emitPlugins("SessionStart");
|
|
136
|
+
try {
|
|
137
|
+
await rt.agent.run(prompt, controller.signal);
|
|
138
|
+
const commands = rt.config.verifyCommands;
|
|
139
|
+
if (edited && commands.length && !controller.signal.aborted) {
|
|
140
|
+
for (let round = 0; round < MAX_VERIFY_ROUNDS; round++) {
|
|
141
|
+
io.err.write(`${t("Verifying edits: {cmds}", { cmds: commands.join(", ") })}
|
|
142
|
+
`);
|
|
143
|
+
const failures = await runVerify(commands, { cwd: rt.cwd, env: rt.env, signal: controller.signal });
|
|
144
|
+
if (!failures.length) {
|
|
145
|
+
io.err.write(`${t("Verification passed.")}
|
|
146
|
+
`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (round === MAX_VERIFY_ROUNDS - 1 || controller.signal.aborted) {
|
|
150
|
+
io.err.write(`${t("Verification still failing after {n} fix round(s). Run the commands manually to see why.", { n: MAX_VERIFY_ROUNDS })}
|
|
151
|
+
`);
|
|
152
|
+
failed = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
await rt.agent.run(verifyFollowup(failures), controller.signal);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} finally {
|
|
159
|
+
process.off("SIGINT", onSigint);
|
|
160
|
+
if (useSigbreak) process.off("SIGBREAK", onSigbreak);
|
|
161
|
+
}
|
|
162
|
+
const stop = await runHooks(rt.config.hooks, "Stop", {}, { env: rt.env, cwd: rt.cwd });
|
|
163
|
+
for (const warning of stop.warnings) io.err.write(`${warning}
|
|
164
|
+
`);
|
|
165
|
+
await rt.emitPlugins("Stop");
|
|
166
|
+
const durationMs = Math.max(0, Date.now() - startedMs);
|
|
167
|
+
if (format === "json") {
|
|
168
|
+
io.out.write(
|
|
169
|
+
`${JSON.stringify({
|
|
170
|
+
status: failed ? "failed" : "ok",
|
|
171
|
+
model: rt.modelRef,
|
|
172
|
+
session_id: rt.session.id,
|
|
173
|
+
usage: rt.agent.totalUsage,
|
|
174
|
+
duration_ms: durationMs,
|
|
175
|
+
result
|
|
176
|
+
})}
|
|
177
|
+
`
|
|
178
|
+
);
|
|
179
|
+
} else if (format === "stream-json") {
|
|
180
|
+
if (result) emit({ type: "message", text: result });
|
|
181
|
+
emit({
|
|
182
|
+
type: "result",
|
|
183
|
+
status: failed ? "failed" : "ok",
|
|
184
|
+
model: rt.modelRef,
|
|
185
|
+
session_id: rt.session.id,
|
|
186
|
+
usage: rt.agent.totalUsage,
|
|
187
|
+
duration_ms: durationMs
|
|
188
|
+
});
|
|
189
|
+
} else if (wroteText) {
|
|
190
|
+
io.out.write("\n");
|
|
191
|
+
}
|
|
192
|
+
return failed ? 1 : 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/worktree.ts
|
|
196
|
+
import { execFile } from "child_process";
|
|
197
|
+
import { existsSync } from "fs";
|
|
198
|
+
import { basename, dirname, join } from "path";
|
|
199
|
+
function git(cwd, args, bin = "git") {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
execFile(
|
|
202
|
+
bin,
|
|
203
|
+
args,
|
|
204
|
+
{ cwd, maxBuffer: 20 * 1024 * 1024 },
|
|
205
|
+
(error, stdout, stderr) => resolve({ ok: !error, out: error ? stderr : stdout, missing: error?.code === "ENOENT" })
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
var NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
210
|
+
function samePath(a, b) {
|
|
211
|
+
const norm = (p) => process.platform === "win32" ? p.replace(/\//g, "\\").toLowerCase() : p.replace(/\\/g, "/");
|
|
212
|
+
return norm(a) === norm(b);
|
|
213
|
+
}
|
|
214
|
+
function worktreeEntries(porcelain) {
|
|
215
|
+
return porcelain.split(/\r?\n\r?\n/).map((block) => {
|
|
216
|
+
const path = /^worktree (.+)$/m.exec(block)?.[1]?.trim();
|
|
217
|
+
if (!path) return void 0;
|
|
218
|
+
const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
|
|
219
|
+
return branch ? { path, branch } : { path };
|
|
220
|
+
}).filter((entry) => !!entry);
|
|
221
|
+
}
|
|
222
|
+
async function resolveWorktree(cwd, name, bin = "git") {
|
|
223
|
+
if (!NAME_RE.test(name)) {
|
|
224
|
+
throw new ConfigError(t('Invalid worktree name "{name}": use letters, digits, dot, dash, or underscore (no slashes).', { name }));
|
|
225
|
+
}
|
|
226
|
+
const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
|
|
227
|
+
if (inside.missing) throw new ConfigError(t("git is not installed or not on PATH, so -w cannot be used."));
|
|
228
|
+
if (!inside.ok || inside.out.trim() !== "true") throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
|
|
229
|
+
const list = await git(cwd, ["worktree", "list", "--porcelain"], bin);
|
|
230
|
+
if (!list.ok) throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
|
|
231
|
+
const entries = worktreeEntries(list.out);
|
|
232
|
+
const main = entries[0]?.path;
|
|
233
|
+
if (!main) throw new ConfigError(t("This folder is not a git repository, so -w cannot create a worktree."));
|
|
234
|
+
const path = join(dirname(main), `${basename(main)}.worktrees`, name);
|
|
235
|
+
if (entries.some((entry) => samePath(entry.path, path))) return { path, created: false };
|
|
236
|
+
if (existsSync(path)) {
|
|
237
|
+
throw new ConfigError(t("Worktree path {path} already exists but is not a registered worktree; remove or rename it first.", { path }));
|
|
238
|
+
}
|
|
239
|
+
const branch = `bccli/${name}`;
|
|
240
|
+
const hasBranch = (await git(main, ["rev-parse", "--verify", "-q", `refs/heads/${branch}`], bin)).ok;
|
|
241
|
+
const add = hasBranch ? await git(main, ["worktree", "add", path, branch], bin) : await git(main, ["worktree", "add", "-b", branch, path], bin);
|
|
242
|
+
if (!add.ok) throw new ConfigError(add.out.trim() || t("This folder is not a git repository, so -w cannot create a worktree."));
|
|
243
|
+
return { path, created: true };
|
|
244
|
+
}
|
|
245
|
+
async function worktreeListText(cwd, bin = "git") {
|
|
246
|
+
const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
|
|
247
|
+
if (inside.missing) return t("git is not installed or not on PATH, so /worktree cannot be used.");
|
|
248
|
+
if (!inside.ok || inside.out.trim() !== "true") return t("This folder is not a git repository, so there is no diff.");
|
|
249
|
+
const list = await git(cwd, ["worktree", "list", "--porcelain"], bin);
|
|
250
|
+
if (!list.ok) return t("This folder is not a git repository, so there is no diff.");
|
|
251
|
+
const rows = worktreeEntries(list.out).map((entry) => ` ${entry.path}${entry.branch ? ` (${entry.branch})` : ""}`);
|
|
252
|
+
return [
|
|
253
|
+
t("Git worktrees:"),
|
|
254
|
+
...rows,
|
|
255
|
+
"",
|
|
256
|
+
t("Create one with: bccli -w <name>"),
|
|
257
|
+
t("Remove with: git worktree remove <path>")
|
|
258
|
+
].join("\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export {
|
|
262
|
+
MAX_VERIFY_ROUNDS,
|
|
263
|
+
runVerify,
|
|
264
|
+
verifyFollowup,
|
|
265
|
+
toolLabel,
|
|
266
|
+
resolvePrintPrompt,
|
|
267
|
+
runPrint,
|
|
268
|
+
resolveWorktree,
|
|
269
|
+
worktreeListText
|
|
270
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
contentText
|
|
4
|
+
} from "./chunk-OCEQ2TRJ.js";
|
|
5
|
+
|
|
6
|
+
// src/session.ts
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
9
|
+
import { basename, dirname, join } from "path";
|
|
10
|
+
function projectDir(home, cwd) {
|
|
11
|
+
const slug = cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
|
|
12
|
+
return join(home, "sessions", slug);
|
|
13
|
+
}
|
|
14
|
+
function repairTail(messages) {
|
|
15
|
+
const out = [...messages];
|
|
16
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
17
|
+
const m = out[i];
|
|
18
|
+
if (m.role !== "assistant" || !m.tool_calls?.length) continue;
|
|
19
|
+
const answered = new Set(out.slice(i + 1).flatMap((x) => x.role === "tool" ? [x.tool_call_id] : []));
|
|
20
|
+
if (!m.tool_calls.every((c) => answered.has(c.id))) out.length = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
var Session = class _Session {
|
|
26
|
+
constructor(file) {
|
|
27
|
+
this.file = file;
|
|
28
|
+
}
|
|
29
|
+
file;
|
|
30
|
+
get id() {
|
|
31
|
+
return basename(this.file, ".jsonl");
|
|
32
|
+
}
|
|
33
|
+
static create(home, cwd, now = /* @__PURE__ */ new Date()) {
|
|
34
|
+
const dir = projectDir(home, cwd);
|
|
35
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
36
|
+
return new _Session(join(dir, `${stamp}-${randomBytes(3).toString("hex")}.jsonl`));
|
|
37
|
+
}
|
|
38
|
+
static list(home, cwd) {
|
|
39
|
+
const dir = projectDir(home, cwd);
|
|
40
|
+
if (!existsSync(dir)) return [];
|
|
41
|
+
return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
|
|
42
|
+
const session = new _Session(join(dir, f));
|
|
43
|
+
const first = session.loadHistory().find((m) => m.role === "user");
|
|
44
|
+
return { session, mtime: statSync(session.file).mtime, preview: first ? contentText(first.content).slice(0, 80) : "" };
|
|
45
|
+
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
|
|
46
|
+
}
|
|
47
|
+
static latest(home, cwd) {
|
|
48
|
+
return _Session.list(home, cwd)[0]?.session;
|
|
49
|
+
}
|
|
50
|
+
write(record) {
|
|
51
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
52
|
+
appendFileSync(this.file, `${JSON.stringify(record)}
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
append(message) {
|
|
56
|
+
this.write({ t: "msg", m: message });
|
|
57
|
+
}
|
|
58
|
+
appendUsage(usage) {
|
|
59
|
+
this.write({ t: "usage", ...usage });
|
|
60
|
+
}
|
|
61
|
+
reset() {
|
|
62
|
+
this.write({ t: "reset" });
|
|
63
|
+
}
|
|
64
|
+
records() {
|
|
65
|
+
if (!existsSync(this.file)) return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const line of readFileSync(this.file, "utf8").split("\n")) {
|
|
68
|
+
if (!line.trim()) continue;
|
|
69
|
+
try {
|
|
70
|
+
out.push(JSON.parse(line));
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** What the model continues from: everything after the last reset (after a compaction that is the summary and the recent turns). */
|
|
77
|
+
load() {
|
|
78
|
+
let messages = [];
|
|
79
|
+
for (const record of this.records()) {
|
|
80
|
+
if (record.t === "reset") messages = [];
|
|
81
|
+
else if (record.t === "msg") messages.push(record.m);
|
|
82
|
+
}
|
|
83
|
+
return repairTail(messages);
|
|
84
|
+
}
|
|
85
|
+
/** Model of the most recent turn that recorded usage — serve attach continues a session with its own model. */
|
|
86
|
+
lastModel() {
|
|
87
|
+
const records = this.records();
|
|
88
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
89
|
+
const record = records[i];
|
|
90
|
+
if (record.t === "usage" && record.model) return record.model;
|
|
91
|
+
}
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
/** Accumulated token usage since the last non-compaction reset. */
|
|
95
|
+
totalUsage() {
|
|
96
|
+
const segments = [{ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } }];
|
|
97
|
+
for (const record of this.records()) {
|
|
98
|
+
const cur = segments[segments.length - 1];
|
|
99
|
+
if (record.t === "reset") {
|
|
100
|
+
segments.push({ msgs: [], usage: { inputTokens: 0, outputTokens: 0 } });
|
|
101
|
+
} else if (record.t === "msg") {
|
|
102
|
+
cur.msgs.push(record.m);
|
|
103
|
+
} else if (record.t === "usage") {
|
|
104
|
+
cur.usage.inputTokens += record.inputTokens;
|
|
105
|
+
cur.usage.outputTokens += record.outputTokens;
|
|
106
|
+
if (record.cachedInputTokens) cur.usage.cachedInputTokens = (cur.usage.cachedInputTokens ?? 0) + record.cachedInputTokens;
|
|
107
|
+
if (record.cacheWriteTokens) cur.usage.cacheWriteTokens = (cur.usage.cacheWriteTokens ?? 0) + record.cacheWriteTokens;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let total = { ...segments[0].usage };
|
|
111
|
+
for (const seg of segments.slice(1)) {
|
|
112
|
+
if (!isCompactionSummary(seg.msgs[0])) {
|
|
113
|
+
total = { ...seg.usage };
|
|
114
|
+
} else {
|
|
115
|
+
total.inputTokens += seg.usage.inputTokens;
|
|
116
|
+
total.outputTokens += seg.usage.outputTokens;
|
|
117
|
+
if (seg.usage.cachedInputTokens) total.cachedInputTokens = (total.cachedInputTokens ?? 0) + seg.usage.cachedInputTokens;
|
|
118
|
+
if (seg.usage.cacheWriteTokens) total.cacheWriteTokens = (total.cacheWriteTokens ?? 0) + seg.usage.cacheWriteTokens;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return total;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
|
|
125
|
+
* its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
|
|
126
|
+
* synthetic summary pair is never shown, and the recent turns a compaction re-writes are not shown twice.
|
|
127
|
+
*/
|
|
128
|
+
loadHistory() {
|
|
129
|
+
const segments = [[]];
|
|
130
|
+
for (const record of this.records()) {
|
|
131
|
+
if (record.t === "reset") segments.push([]);
|
|
132
|
+
else if (record.t === "msg") segments[segments.length - 1].push(record.m);
|
|
133
|
+
}
|
|
134
|
+
let shown = segments[0];
|
|
135
|
+
let lastSummary;
|
|
136
|
+
for (const segment of segments.slice(1)) {
|
|
137
|
+
if (!isCompactionSummary(segment[0])) {
|
|
138
|
+
shown = segment;
|
|
139
|
+
lastSummary = void 0;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const kept = segment.slice(2);
|
|
143
|
+
if (lastSummary && sameMessage(segment[0], lastSummary)) {
|
|
144
|
+
const subIdx = findContiguousSlice(shown, kept);
|
|
145
|
+
if (subIdx !== -1) {
|
|
146
|
+
shown = shown.slice(0, subIdx + kept.length);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
lastSummary = segment[0];
|
|
151
|
+
let at = 0;
|
|
152
|
+
while (at <= shown.length && !endsWithPrefixOf(shown, at, kept)) at++;
|
|
153
|
+
shown = [...shown, ...kept.slice(shown.length - at)];
|
|
154
|
+
}
|
|
155
|
+
return repairTail(shown.filter((m) => !isCompactionSummary(m) && !isCompactionAck(m)));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var SUMMARY_PREFIX = "Summary of the previous conversation:";
|
|
159
|
+
var SUMMARY_ACK = "OK, I will continue from this summary.";
|
|
160
|
+
function isCompactionSummary(m) {
|
|
161
|
+
return m?.role === "user" && typeof m.content === "string" && m.content.startsWith(SUMMARY_PREFIX);
|
|
162
|
+
}
|
|
163
|
+
function isCompactionAck(m) {
|
|
164
|
+
return m.role === "assistant" && m.content === SUMMARY_ACK;
|
|
165
|
+
}
|
|
166
|
+
function sameMessage(a, b) {
|
|
167
|
+
if (a.role !== b.role) return false;
|
|
168
|
+
if (a.role === "tool" && b.role === "tool") return a.tool_call_id === b.tool_call_id;
|
|
169
|
+
return JSON.stringify(a.content) === JSON.stringify(b.content) && JSON.stringify("tool_calls" in a && a.tool_calls) === JSON.stringify("tool_calls" in b && b.tool_calls);
|
|
170
|
+
}
|
|
171
|
+
function endsWithPrefixOf(shown, at, kept) {
|
|
172
|
+
const n = shown.length - at;
|
|
173
|
+
if (n > kept.length) return false;
|
|
174
|
+
for (let j = 0; j < n; j++) if (!sameMessage(shown[at + j], kept[j])) return false;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
function findContiguousSlice(shown, slice) {
|
|
178
|
+
if (slice.length === 0) return shown.length;
|
|
179
|
+
for (let i = 0; i + slice.length <= shown.length; i++) {
|
|
180
|
+
let ok = true;
|
|
181
|
+
for (let j = 0; j < slice.length; j++) {
|
|
182
|
+
if (!sameMessage(shown[i + j], slice[j])) {
|
|
183
|
+
ok = false;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (ok) return i;
|
|
188
|
+
}
|
|
189
|
+
return -1;
|
|
190
|
+
}
|
|
191
|
+
function pruneSessions(home, now = Date.now(), maxAgeDays = 30) {
|
|
192
|
+
const root = join(home, "sessions");
|
|
193
|
+
if (!existsSync(root)) return 0;
|
|
194
|
+
let removed = 0;
|
|
195
|
+
for (const project of readdirSync(root)) {
|
|
196
|
+
const dir = join(root, project);
|
|
197
|
+
try {
|
|
198
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
199
|
+
} catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
for (const file of readdirSync(dir)) {
|
|
203
|
+
const path = join(dir, file);
|
|
204
|
+
if (now - statSync(path).mtimeMs > maxAgeDays * 864e5) {
|
|
205
|
+
rmSync(path);
|
|
206
|
+
removed++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return removed;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export {
|
|
214
|
+
Session,
|
|
215
|
+
pruneSessions
|
|
216
|
+
};
|