@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.
- package/README.en.md +253 -0
- package/README.md +93 -6
- package/dist/abort.d.ts +5 -0
- package/dist/acp-3PHBAI64.js +182 -0
- package/dist/acp.d.ts +27 -0
- package/dist/agent.d.ts +143 -0
- package/dist/agents.d.ts +4 -0
- package/dist/args-W6BNKY7K.js +13 -0
- package/dist/args.d.ts +51 -0
- package/dist/budget.d.ts +23 -0
- package/dist/checkpoints.d.ts +34 -0
- package/dist/{chunk-BFZXRKW5.js → chunk-24JN75FZ.js} +29 -7
- package/dist/chunk-45SDLMTV.js +341 -0
- package/dist/chunk-75BECX4F.js +56 -0
- package/dist/{chunk-SJH3YLW4.js → chunk-BC7UXFE4.js} +69 -6
- package/dist/chunk-BGXNR74O.js +733 -0
- package/dist/chunk-BKGFA2PT.js +8 -0
- package/dist/{chunk-CLN4Z5E3.js → chunk-BLOYFIAD.js} +6 -3
- package/dist/chunk-BXQQCUAC.js +145 -0
- package/dist/chunk-FLQPE4S5.js +373 -0
- package/dist/chunk-GN4W3T6E.js +76 -0
- package/dist/{chunk-7S6JXQCH.js → chunk-JHPGOAXU.js} +2 -1
- package/dist/chunk-MFHCQQEN.js +201 -0
- package/dist/chunk-MHIYLXJC.js +143 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-PZRKU4UM.js +2143 -0
- package/dist/chunk-QZKLZELZ.js +64 -0
- package/dist/chunk-R5RE46CW.js +231 -0
- package/dist/chunk-UZWJQF67.js +500 -0
- package/dist/{chunk-DX7CZTBB.js → chunk-WI22M2ID.js} +14 -9
- package/dist/chunk-WNACAUDK.js +946 -0
- package/dist/chunk-XV7ZVL4J.js +235 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +150 -1382
- package/dist/clipboardImage.d.ts +17 -0
- package/dist/commands.d.ts +16 -0
- package/dist/completion-HPPFDAPB.js +125 -0
- package/dist/completion.d.ts +14 -0
- package/dist/{config-YUNKPCNB.js → config-DIDLDONU.js} +5 -2
- package/dist/config-IU76CPK2.js +34 -0
- package/dist/config.d.ts +97 -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-E3VPHNZY.js +677 -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-Q2YPDTPG.js +685 -0
- package/dist/integrationCli.d.ts +9 -0
- package/dist/keybinds.d.ts +22 -0
- package/dist/login-FP3BB44A.js +16 -0
- package/dist/login.d.ts +19 -0
- package/dist/lsp.d.ts +47 -0
- package/dist/mcp/catalog.d.ts +14 -0
- package/dist/mcp/config.d.ts +36 -0
- package/dist/mcp/manager.d.ts +34 -0
- package/dist/mcp/oauth.d.ts +82 -0
- package/dist/{mcpCli-DZWQPTOS.js → mcpCli-SNBDUWJN.js} +42 -9
- 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-CZ2HNGKR.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-6FWQABJ7.js +210 -0
- package/dist/oauthLogin.d.ts +47 -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-LPIUUDWB.js +284 -0
- package/dist/pluginsCli.d.ts +16 -0
- package/dist/prCli-5IMV3VNQ.js +78 -0
- package/dist/prCli.d.ts +33 -0
- package/dist/presets.d.ts +8 -0
- package/dist/print.d.ts +14 -0
- package/dist/provider.d.ts +140 -0
- package/dist/providerCli-NMUFXAAS.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 +57 -0
- package/dist/sdk.js +5571 -0
- package/dist/serve-KWOECK5W.js +178 -0
- package/dist/serve-X2ZV4S5U.js +521 -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-GKEXDJKZ.js +12 -0
- package/dist/session.d.ts +30 -0
- package/dist/setup-OV3NWQN5.js +24 -0
- package/dist/setup.d.ts +74 -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 +45 -0
- package/dist/slash/memory.d.ts +10 -0
- package/dist/statsCli-CF3R7ATL.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 +52 -0
- package/dist/tools/diagnostics.d.ts +5 -0
- package/dist/tools/edit.d.ts +7 -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 +13 -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 +2 -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 +12 -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-YUFPI6AH.js +71 -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 +18 -0
- package/dist/ui/ProviderMenu.d.ts +10 -0
- package/dist/ui/Spinner.d.ts +4 -0
- package/dist/ui/StatusBar.d.ts +9 -0
- package/dist/ui/TodoList.d.ts +4 -0
- package/dist/ui/ToolBlock.d.ts +14 -0
- package/dist/ui/complete.d.ts +1 -0
- package/dist/ui/focusReport.d.ts +1 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/lineEdit.d.ts +42 -0
- package/dist/ui/replay.d.ts +4 -0
- package/dist/ui/table.d.ts +20 -0
- package/dist/ui/theme.d.ts +5 -0
- package/dist/ui/thinkingMouse.d.ts +33 -0
- package/dist/ui/transcript.d.ts +55 -0
- package/dist/ui/vim.d.ts +26 -0
- package/dist/ui-IVIFNUCC.js +2367 -0
- package/dist/update-TZ2G6KQ3.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 +42 -8
- package/dist/args-JPVSGCGY.js +0 -12
- package/dist/chunk-2MGINW4O.js +0 -114
- package/dist/chunk-ELML4KVQ.js +0 -76
- package/dist/chunk-ISGBGFSU.js +0 -52
- package/dist/chunk-KV27KTOX.js +0 -254
- package/dist/chunk-U7GK73CK.js +0 -341
- package/dist/chunk-ZYCYTEKD.js +0 -96
- package/dist/integrationCli-BGT5UZU4.js +0 -223
- package/dist/login-6K32PIXV.js +0 -9
- package/dist/providerCli-Q7LRM2NP.js +0 -12
- package/dist/session-7UB77CEG.js +0 -9
- package/dist/ui-6GTQUJOE.js +0 -1415
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Provider } from './provider';
|
|
2
|
+
export interface ServeOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
host?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
provider?: Provider;
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
heartbeatMs?: number;
|
|
11
|
+
err?: (s: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ServeHandle {
|
|
14
|
+
port: number;
|
|
15
|
+
token: string;
|
|
16
|
+
url: string;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export type PermissionMode = 'ask' | 'bypassPermissions';
|
|
20
|
+
/**
|
|
21
|
+
* One-line terminal banner for `bccli serve`. A token the user supplied (`--token` or
|
|
22
|
+
* `BCCLI_SERVE_TOKEN`) is left out so it cannot leak into logs; a token BCCLI generated
|
|
23
|
+
* itself must be printed or nobody could connect.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serveBanner(handle: {
|
|
26
|
+
url: string;
|
|
27
|
+
token: string;
|
|
28
|
+
}, opts?: {
|
|
29
|
+
token?: string;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
}): string;
|
|
32
|
+
/**
|
|
33
|
+
* Starts the loopback session server (HTTP unary + WS mux at /v1/mux). Refuses
|
|
34
|
+
* non-loopback bind addresses; every request passes the DSH-style trust fence
|
|
35
|
+
* (403) and, except /health, the bearer-token check (401).
|
|
36
|
+
*/
|
|
37
|
+
export declare function startServe(opts?: ServeOptions): Promise<ServeHandle>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Heartbeat {
|
|
2
|
+
/** Sends the first ping and schedules the check loop. Idempotent until stop(). */
|
|
3
|
+
start(): void;
|
|
4
|
+
/** Marks the last ping as answered (ws 'pong' event). */
|
|
5
|
+
pong(): void;
|
|
6
|
+
/** Clears the timer; no further pings or terminations. */
|
|
7
|
+
stop(): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* ws heartbeat with deadline: every interval a ping goes out; a ping left
|
|
11
|
+
* unanswered by the next tick terminates the socket (connection, not stream).
|
|
12
|
+
* Modeled on the DSH gateway's `websocketHeartbeatIntervalMs` behavior.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createHeartbeat(intervalMs: number, ping: () => void, terminate: () => void): Heartbeat;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Wire frames of the /v1/mux WebSocket (subset of the DSH stream protocol). */
|
|
2
|
+
export type ClientFrame = {
|
|
3
|
+
type: 'open';
|
|
4
|
+
streamId: string;
|
|
5
|
+
target: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'cancel';
|
|
8
|
+
streamId: string;
|
|
9
|
+
};
|
|
10
|
+
export type ServerFrame = {
|
|
11
|
+
type: 'item';
|
|
12
|
+
streamId: string;
|
|
13
|
+
value: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'end';
|
|
16
|
+
streamId: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'error';
|
|
19
|
+
streamId: string;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
/** Uplink frame size limit; oversized frames error their stream without closing the socket. */
|
|
23
|
+
export declare const MAX_FRAME_BYTES = 262144;
|
|
24
|
+
/**
|
|
25
|
+
* Parses one uplink mux frame. Malformed JSON, unknown types, an empty streamId,
|
|
26
|
+
* or an `open` without a target produce `{ error }` instead of throwing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseClientFrame(raw: string): ClientFrame | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
/** Addresses `bccli serve` is allowed to bind (loopback only; 0.0.0.0 is refused). */
|
|
32
|
+
export declare const LOOPBACK_BIND: Set<string>;
|
|
33
|
+
/**
|
|
34
|
+
* DSH-style trust fence: loopback Host with a port matching the listener, a
|
|
35
|
+
* same-machine Origin when attached, and no cross-site sec-fetch-site. Returns
|
|
36
|
+
* null when trusted, else the 403 reason. Never establishes identity — the
|
|
37
|
+
* bearer token does.
|
|
38
|
+
*/
|
|
39
|
+
export declare function trustFence(headers: {
|
|
40
|
+
host?: string;
|
|
41
|
+
origin?: string;
|
|
42
|
+
'sec-fetch-site'?: string;
|
|
43
|
+
}, port: number): string | null;
|
|
44
|
+
/** Timing-safe `Authorization: Bearer <token>` check. */
|
|
45
|
+
export declare function bearerOk(header: string | undefined, token: string): boolean;
|
|
46
|
+
/** Fresh 32-byte hex serve token. */
|
|
47
|
+
export declare function randomToken(): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ChatMessage, type Usage } from './provider';
|
|
2
|
+
export declare class Session {
|
|
3
|
+
readonly file: string;
|
|
4
|
+
constructor(file: string);
|
|
5
|
+
static create(home: string, cwd: string, now?: Date): Session;
|
|
6
|
+
static list(home: string, cwd: string): {
|
|
7
|
+
session: Session;
|
|
8
|
+
mtime: Date;
|
|
9
|
+
preview: string;
|
|
10
|
+
}[];
|
|
11
|
+
static latest(home: string, cwd: string): Session | undefined;
|
|
12
|
+
private write;
|
|
13
|
+
append(message: ChatMessage): void;
|
|
14
|
+
appendUsage(usage: Usage & {
|
|
15
|
+
model?: string;
|
|
16
|
+
}): void;
|
|
17
|
+
reset(): void;
|
|
18
|
+
private records;
|
|
19
|
+
/** What the model continues from: everything after the last reset (after a compaction that is the summary and the recent turns). */
|
|
20
|
+
load(): ChatMessage[];
|
|
21
|
+
/** Model of the most recent turn that recorded usage — serve attach continues a session with its own model. */
|
|
22
|
+
lastModel(): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
|
|
25
|
+
* its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
|
|
26
|
+
* synthetic summary pair is never shown, and the recent turns a compaction re-writes are not shown twice.
|
|
27
|
+
*/
|
|
28
|
+
loadHistory(): ChatMessage[];
|
|
29
|
+
}
|
|
30
|
+
export declare function pruneSessions(home: string, now?: number, maxAgeDays?: number): number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createRuntime,
|
|
4
|
+
lunaPtcAutoEnabled
|
|
5
|
+
} from "./chunk-PZRKU4UM.js";
|
|
6
|
+
import "./chunk-QZKLZELZ.js";
|
|
7
|
+
import "./chunk-BGXNR74O.js";
|
|
8
|
+
import "./chunk-45SDLMTV.js";
|
|
9
|
+
import "./chunk-PSO7R5AD.js";
|
|
10
|
+
import "./chunk-MHIYLXJC.js";
|
|
11
|
+
import "./chunk-GN4W3T6E.js";
|
|
12
|
+
import "./chunk-75BECX4F.js";
|
|
13
|
+
import "./chunk-JHPGOAXU.js";
|
|
14
|
+
import "./chunk-MFHCQQEN.js";
|
|
15
|
+
import "./chunk-BC7UXFE4.js";
|
|
16
|
+
import "./chunk-WNACAUDK.js";
|
|
17
|
+
import "./chunk-BLOYFIAD.js";
|
|
18
|
+
import "./chunk-24JN75FZ.js";
|
|
19
|
+
import "./chunk-FLQPE4S5.js";
|
|
20
|
+
import "./chunk-UZWJQF67.js";
|
|
21
|
+
export {
|
|
22
|
+
createRuntime,
|
|
23
|
+
lunaPtcAutoEnabled
|
|
24
|
+
};
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Agent } from './agent';
|
|
2
|
+
import { CheckpointStore } from './checkpoints';
|
|
3
|
+
import type { McpServerSpec } from './mcp/config';
|
|
4
|
+
import { McpManager } from './mcp/manager';
|
|
5
|
+
import type { CliArgs } from './args';
|
|
6
|
+
import { type Config } from './config';
|
|
7
|
+
import { type AgentDef, type CommandDef, type SkillDef } from './extensions';
|
|
8
|
+
import { type ModelGroup } from './models';
|
|
9
|
+
import { type ChatMessage, type Provider } from './provider';
|
|
10
|
+
import type { ReasoningLevel } from './reasoning';
|
|
11
|
+
import { addProviderKey } from './providerCli';
|
|
12
|
+
import { Session } from './session';
|
|
13
|
+
import { type Interaction } from './tools/plan';
|
|
14
|
+
import { TodoStore } from './tools/todo';
|
|
15
|
+
import { type PluginPayload } from './plugins';
|
|
16
|
+
import type { HookEvent } from './hooks';
|
|
17
|
+
export interface Runtime {
|
|
18
|
+
cwd: string;
|
|
19
|
+
home: string;
|
|
20
|
+
config: Config;
|
|
21
|
+
modelRef: string;
|
|
22
|
+
reasoning: ReasoningLevel;
|
|
23
|
+
agent: Agent;
|
|
24
|
+
session: Session;
|
|
25
|
+
setModel(ref: string): void;
|
|
26
|
+
setReasoning(level: ReasoningLevel): void;
|
|
27
|
+
env: NodeJS.ProcessEnv;
|
|
28
|
+
skills: SkillDef[];
|
|
29
|
+
commands: CommandDef[];
|
|
30
|
+
agentDefs: AgentDef[];
|
|
31
|
+
todos: TodoStore;
|
|
32
|
+
interaction: Interaction;
|
|
33
|
+
mcp: McpManager;
|
|
34
|
+
startMcp(specs: McpServerSpec[]): Promise<void>;
|
|
35
|
+
reloadConfig(): void;
|
|
36
|
+
providerLabel(): string;
|
|
37
|
+
listModels: (only?: string) => Promise<ModelGroup[]>;
|
|
38
|
+
addProviderKey(id: string, key: string): ReturnType<typeof addProviderKey>;
|
|
39
|
+
/** Continue an earlier session: load its history and append new messages to its file. */
|
|
40
|
+
resume(session: Session): void;
|
|
41
|
+
/** Starts a fresh session file; the previous one stays on disk for /resume. */
|
|
42
|
+
newSession(): void;
|
|
43
|
+
/** Re-reads AGENTS.md/BCCLI.md into the system prompt (after /memory). */
|
|
44
|
+
rebuildSystemPrompt(): void;
|
|
45
|
+
/** Fires a plugin observer event (SessionStart/Stop are emitted here; tool events come from the agent). */
|
|
46
|
+
emitPlugins(event: HookEvent, payload?: PluginPayload): Promise<void>;
|
|
47
|
+
checkpoints: CheckpointStore;
|
|
48
|
+
/**
|
|
49
|
+
* Steps back n conversation turns: truncates the history (and the session log) to before
|
|
50
|
+
* them and restores the file snapshots they took. Returns undefined when nothing is left.
|
|
51
|
+
*/
|
|
52
|
+
rewindTurns(n: number): Promise<RewindResult | undefined>;
|
|
53
|
+
startedAt: Date;
|
|
54
|
+
}
|
|
55
|
+
export interface RewindResult {
|
|
56
|
+
turns: number;
|
|
57
|
+
restored: string[];
|
|
58
|
+
deleted: string[];
|
|
59
|
+
skipped: string[];
|
|
60
|
+
failed: string[];
|
|
61
|
+
}
|
|
62
|
+
/** PTC is automatic for Luna. This env var is only an emergency/internal kill switch. */
|
|
63
|
+
export declare function lunaPtcAutoEnabled(env: NodeJS.ProcessEnv): boolean;
|
|
64
|
+
export declare function createRuntime(opts: {
|
|
65
|
+
cwd: string;
|
|
66
|
+
args: CliArgs;
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
provider?: Provider;
|
|
69
|
+
history?: ChatMessage[];
|
|
70
|
+
fetch?: typeof fetch;
|
|
71
|
+
userHome?: string;
|
|
72
|
+
/** Override langkah agent (mis. githubBot.stepLimit) — menang atas config.maxSteps. */
|
|
73
|
+
maxSteps?: number | null;
|
|
74
|
+
}): Runtime;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function osc52(text: string, inTmux: boolean): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function gitDiff(cwd: string, maxLines?: number, bin?: string): Promise<string>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface DoctorDeps {
|
|
2
|
+
which: (cmd: string) => boolean | Promise<boolean>;
|
|
3
|
+
nodeVersion: string;
|
|
4
|
+
listModels: () => Promise<unknown[]>;
|
|
5
|
+
}
|
|
6
|
+
export declare function doctorText(input: {
|
|
7
|
+
providers: {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
ready: boolean;
|
|
11
|
+
}[];
|
|
12
|
+
mcp: {
|
|
13
|
+
name: string;
|
|
14
|
+
status: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
}[];
|
|
17
|
+
activeProvider: string;
|
|
18
|
+
}, deps: DoctorDeps): Promise<string>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface StatusInput {
|
|
2
|
+
version: string;
|
|
3
|
+
modelRef: string;
|
|
4
|
+
providerLabel: string;
|
|
5
|
+
mode: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
mcp: {
|
|
8
|
+
name: string;
|
|
9
|
+
status: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
tools: number;
|
|
12
|
+
}[];
|
|
13
|
+
usage: {
|
|
14
|
+
inputTokens: number;
|
|
15
|
+
outputTokens: number;
|
|
16
|
+
cachedInputTokens?: number;
|
|
17
|
+
cacheWriteTokens?: number;
|
|
18
|
+
};
|
|
19
|
+
lastInputTokens: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function statusText(s: StatusInput): string;
|
|
22
|
+
export declare function agentsText(defs: {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
tools?: string[];
|
|
26
|
+
model?: string;
|
|
27
|
+
}[]): string;
|
|
28
|
+
export declare function skillsText(skills: {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
dir: string;
|
|
32
|
+
}[], commands: {
|
|
33
|
+
name: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
}[]): string;
|
|
36
|
+
export declare function sessionText(s: {
|
|
37
|
+
file: string;
|
|
38
|
+
started: Date;
|
|
39
|
+
messages: number;
|
|
40
|
+
usage: {
|
|
41
|
+
inputTokens: number;
|
|
42
|
+
outputTokens: number;
|
|
43
|
+
};
|
|
44
|
+
modelRef: string;
|
|
45
|
+
}): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function instructionFiles(cwd: string, home: string): {
|
|
2
|
+
path: string;
|
|
3
|
+
lines: number;
|
|
4
|
+
}[];
|
|
5
|
+
export declare function appendMemory(file: string, text: string): void;
|
|
6
|
+
/** `/memory [global] <teks>` → target file and text; text is empty when nothing follows. */
|
|
7
|
+
export declare function parseMemoryArgs(args: string): {
|
|
8
|
+
global: boolean;
|
|
9
|
+
text: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
estimateTokens
|
|
4
|
+
} from "./chunk-QZKLZELZ.js";
|
|
5
|
+
import {
|
|
6
|
+
t
|
|
7
|
+
} from "./chunk-UZWJQF67.js";
|
|
8
|
+
|
|
9
|
+
// src/statsCli.ts
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
function isUsageRecord(value) {
|
|
13
|
+
if (!value || typeof value !== "object") return false;
|
|
14
|
+
const v = value;
|
|
15
|
+
return v.t === "usage" && typeof v.inputTokens === "number" && typeof v.outputTokens === "number";
|
|
16
|
+
}
|
|
17
|
+
function isMsgRecord(value) {
|
|
18
|
+
if (!value || typeof value !== "object") return false;
|
|
19
|
+
const v = value;
|
|
20
|
+
return v.t === "msg" && !!v.m;
|
|
21
|
+
}
|
|
22
|
+
function emptyRow(project) {
|
|
23
|
+
return {
|
|
24
|
+
project,
|
|
25
|
+
sessions: 0,
|
|
26
|
+
messages: 0,
|
|
27
|
+
inputTokens: 0,
|
|
28
|
+
outputTokens: 0,
|
|
29
|
+
usageInputTokens: 0,
|
|
30
|
+
usageOutputTokens: 0,
|
|
31
|
+
cachedInputTokens: 0,
|
|
32
|
+
cacheWriteTokens: 0,
|
|
33
|
+
toolCalls: {},
|
|
34
|
+
sessionTotals: [],
|
|
35
|
+
modelUsage: {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function aggregateSessions(home, opts) {
|
|
39
|
+
const now = opts.now ?? Date.now();
|
|
40
|
+
const cutoff = now - opts.days * 864e5;
|
|
41
|
+
const root = join(home, "sessions");
|
|
42
|
+
if (!existsSync(root)) return [];
|
|
43
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const project of readdirSync(root)) {
|
|
45
|
+
const dir = join(root, project);
|
|
46
|
+
let st;
|
|
47
|
+
try {
|
|
48
|
+
st = statSync(dir);
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!st.isDirectory()) continue;
|
|
53
|
+
for (const file of readdirSync(dir)) {
|
|
54
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
55
|
+
const path = join(dir, file);
|
|
56
|
+
let fst;
|
|
57
|
+
try {
|
|
58
|
+
fst = statSync(path);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (fst.mtimeMs < cutoff) continue;
|
|
63
|
+
let row = byProject.get(project);
|
|
64
|
+
if (!row) {
|
|
65
|
+
row = emptyRow(project);
|
|
66
|
+
byProject.set(project, row);
|
|
67
|
+
}
|
|
68
|
+
row.sessions++;
|
|
69
|
+
let usageIn = 0;
|
|
70
|
+
let usageOut = 0;
|
|
71
|
+
let cachedIn = 0;
|
|
72
|
+
let cacheWrite = 0;
|
|
73
|
+
let usageCount = 0;
|
|
74
|
+
let estIn = 0;
|
|
75
|
+
let estOut = 0;
|
|
76
|
+
let messages = 0;
|
|
77
|
+
const tools = {};
|
|
78
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
79
|
+
if (!line.trim()) continue;
|
|
80
|
+
let record;
|
|
81
|
+
try {
|
|
82
|
+
record = JSON.parse(line);
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (isUsageRecord(record)) {
|
|
87
|
+
usageIn += record.inputTokens;
|
|
88
|
+
usageOut += record.outputTokens;
|
|
89
|
+
cachedIn += record.cachedInputTokens ?? 0;
|
|
90
|
+
cacheWrite += record.cacheWriteTokens ?? 0;
|
|
91
|
+
usageCount++;
|
|
92
|
+
if (record.model) {
|
|
93
|
+
let mt = row.modelUsage[record.model];
|
|
94
|
+
if (!mt) {
|
|
95
|
+
mt = { turns: 0, inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, cacheWriteTokens: 0 };
|
|
96
|
+
row.modelUsage[record.model] = mt;
|
|
97
|
+
}
|
|
98
|
+
mt.turns++;
|
|
99
|
+
mt.inputTokens += record.inputTokens;
|
|
100
|
+
mt.outputTokens += record.outputTokens;
|
|
101
|
+
mt.cachedInputTokens += record.cachedInputTokens ?? 0;
|
|
102
|
+
mt.cacheWriteTokens += record.cacheWriteTokens ?? 0;
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!isMsgRecord(record)) continue;
|
|
107
|
+
messages++;
|
|
108
|
+
if (record.m.role === "user" || record.m.role === "assistant") {
|
|
109
|
+
const text = typeof record.m.content === "string" ? record.m.content : JSON.stringify(record.m.content ?? "");
|
|
110
|
+
const tokens = estimateTokens(text);
|
|
111
|
+
if (record.m.role === "user") estIn += tokens;
|
|
112
|
+
else estOut += tokens;
|
|
113
|
+
}
|
|
114
|
+
if (record.m.role === "assistant") {
|
|
115
|
+
for (const call of record.m.tool_calls ?? []) {
|
|
116
|
+
const name = call?.function?.name;
|
|
117
|
+
if (typeof name === "string" && name) tools[name] = (tools[name] ?? 0) + 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
row.messages += messages;
|
|
122
|
+
if (usageCount > 0) {
|
|
123
|
+
row.usageInputTokens += usageIn;
|
|
124
|
+
row.usageOutputTokens += usageOut;
|
|
125
|
+
row.cachedInputTokens += cachedIn;
|
|
126
|
+
row.cacheWriteTokens += cacheWrite;
|
|
127
|
+
row.sessionTotals.push(usageIn + usageOut + cachedIn + cacheWrite);
|
|
128
|
+
} else {
|
|
129
|
+
row.inputTokens += estIn;
|
|
130
|
+
row.outputTokens += estOut;
|
|
131
|
+
row.sessionTotals.push(estIn + estOut);
|
|
132
|
+
}
|
|
133
|
+
for (const [name, count] of Object.entries(tools)) row.toolCalls[name] = (row.toolCalls[name] ?? 0) + count;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return [...byProject.values()].sort((a, b) => b.messages - a.messages || a.project.localeCompare(b.project));
|
|
137
|
+
}
|
|
138
|
+
function fmtCompact(n) {
|
|
139
|
+
if (n < 1e3) return String(Math.round(n));
|
|
140
|
+
const units = [
|
|
141
|
+
[1e9, "B"],
|
|
142
|
+
[1e6, "M"],
|
|
143
|
+
[1e3, "K"]
|
|
144
|
+
];
|
|
145
|
+
for (const [size, suffix] of units) {
|
|
146
|
+
if (n >= size) return `${(n / size).toFixed(1)}${suffix}`;
|
|
147
|
+
}
|
|
148
|
+
return String(Math.round(n));
|
|
149
|
+
}
|
|
150
|
+
function fmtCount(n) {
|
|
151
|
+
return n.toLocaleString("en-US");
|
|
152
|
+
}
|
|
153
|
+
function median(values) {
|
|
154
|
+
if (!values.length) return 0;
|
|
155
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
156
|
+
const mid = sorted.length >> 1;
|
|
157
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
158
|
+
}
|
|
159
|
+
var BOX_INNER = 46;
|
|
160
|
+
function border(left, right) {
|
|
161
|
+
return `${left}${"\u2500".repeat(BOX_INNER + 2)}${right}`;
|
|
162
|
+
}
|
|
163
|
+
function center(text) {
|
|
164
|
+
const pad = Math.max(0, BOX_INNER - text.length);
|
|
165
|
+
const left = Math.floor(pad / 2);
|
|
166
|
+
return `\u2502${" ".repeat(left)}${text}${" ".repeat(pad - left)}\u2502`;
|
|
167
|
+
}
|
|
168
|
+
function contentLine(content) {
|
|
169
|
+
return `\u2502${content}${" ".repeat(Math.max(1, BOX_INNER + 1 - content.length))}\u2502`;
|
|
170
|
+
}
|
|
171
|
+
function valueLine(label, value) {
|
|
172
|
+
return contentLine(` ${label}${" ".repeat(Math.max(1, BOX_INNER - 1 - label.length - value.length))}${value}`);
|
|
173
|
+
}
|
|
174
|
+
function box(title, rows) {
|
|
175
|
+
return [border("\u250C", "\u2510"), center(title), border("\u251C", "\u2524"), ...rows.map(([l, v]) => valueLine(l, v)), border("\u2514", "\u2518")];
|
|
176
|
+
}
|
|
177
|
+
function truncateName(name) {
|
|
178
|
+
return name.length > 18 ? `${name.slice(0, 16)}..` : name;
|
|
179
|
+
}
|
|
180
|
+
function toolBox(tools) {
|
|
181
|
+
const total = tools.reduce((sum, [, n]) => sum + n, 0);
|
|
182
|
+
const max = tools[0]?.[1] ?? 1;
|
|
183
|
+
const lines = tools.map(([name, count]) => {
|
|
184
|
+
const blocks = Math.max(1, Math.round(count / max * 20));
|
|
185
|
+
const pct = (count / total * 100).toFixed(1).padStart(5);
|
|
186
|
+
return contentLine(` ${truncateName(name).padEnd(16)} ${"\u2588".repeat(blocks).padEnd(20)} ${count} (${pct}%)`);
|
|
187
|
+
});
|
|
188
|
+
return [border("\u250C", "\u2510"), center("TOOL USAGE"), border("\u251C", "\u2524"), ...lines, border("\u2514", "\u2518")];
|
|
189
|
+
}
|
|
190
|
+
function modelBox(models) {
|
|
191
|
+
const lines = [];
|
|
192
|
+
for (let i = 0; i < models.length; i++) {
|
|
193
|
+
const [name, m] = models[i];
|
|
194
|
+
if (i > 0) lines.push(border("\u251C", "\u2524"));
|
|
195
|
+
lines.push(contentLine(` ${truncateName(name)}`));
|
|
196
|
+
lines.push(valueLine(" Turns", fmtCount(m.turns)));
|
|
197
|
+
lines.push(valueLine(" Input Tokens", fmtCompact(m.inputTokens)));
|
|
198
|
+
lines.push(valueLine(" Output Tokens", fmtCompact(m.outputTokens)));
|
|
199
|
+
lines.push(valueLine(" Cache Read", fmtCompact(m.cachedInputTokens)));
|
|
200
|
+
lines.push(valueLine(" Cache Write", fmtCompact(m.cacheWriteTokens)));
|
|
201
|
+
}
|
|
202
|
+
return [border("\u250C", "\u2510"), center("MODEL USAGE"), border("\u251C", "\u2524"), ...lines, border("\u2514", "\u2518")];
|
|
203
|
+
}
|
|
204
|
+
function projectSlug(cwd) {
|
|
205
|
+
return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-100) || "root";
|
|
206
|
+
}
|
|
207
|
+
function runStatsCommand(args, deps) {
|
|
208
|
+
let days = args.days ?? 7;
|
|
209
|
+
let toolsCap = args.tools;
|
|
210
|
+
let modelsFlag = args.models;
|
|
211
|
+
let projectFilter = args.project;
|
|
212
|
+
const rest = args.subArgs;
|
|
213
|
+
for (let i = 0; i < rest.length; i++) {
|
|
214
|
+
const a = rest[i];
|
|
215
|
+
if (a === "--days" || a === "--tools" || a === "--models" || a === "--project") {
|
|
216
|
+
const raw = rest[++i] ?? "";
|
|
217
|
+
if (a === "--days") {
|
|
218
|
+
const n = Number(raw);
|
|
219
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
220
|
+
deps.err?.(t("--days must be a positive integer"));
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
days = n;
|
|
224
|
+
} else if (a === "--tools") {
|
|
225
|
+
const n = Number(raw);
|
|
226
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
227
|
+
deps.err?.(t("--tools must be a positive integer"));
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
toolsCap = n;
|
|
231
|
+
} else if (a === "--models") {
|
|
232
|
+
if (raw === "" || raw === "all" || raw.startsWith("-")) {
|
|
233
|
+
modelsFlag = "all";
|
|
234
|
+
if (raw.startsWith("-")) i--;
|
|
235
|
+
} else {
|
|
236
|
+
const n = Number(raw);
|
|
237
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
238
|
+
deps.err?.(t('--models must be a positive integer or "all"'));
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
modelsFlag = raw;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
projectFilter = raw;
|
|
245
|
+
}
|
|
246
|
+
} else if (a.startsWith("--days=")) {
|
|
247
|
+
const n = Number(a.slice("--days=".length));
|
|
248
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
249
|
+
deps.err?.(t("--days must be a positive integer"));
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
days = n;
|
|
253
|
+
} else if (a.startsWith("--tools=")) {
|
|
254
|
+
const n = Number(a.slice("--tools=".length));
|
|
255
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
256
|
+
deps.err?.(t("--tools must be a positive integer"));
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
toolsCap = n;
|
|
260
|
+
} else if (a.startsWith("--models=")) {
|
|
261
|
+
const raw = a.slice("--models=".length);
|
|
262
|
+
if (raw === "" || raw === "all") {
|
|
263
|
+
modelsFlag = "all";
|
|
264
|
+
} else {
|
|
265
|
+
const n = Number(raw);
|
|
266
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
267
|
+
deps.err?.(t('--models must be a positive integer or "all"'));
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
modelsFlag = raw;
|
|
271
|
+
}
|
|
272
|
+
} else if (a.startsWith("--project=")) {
|
|
273
|
+
projectFilter = a.slice("--project=".length);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
let rows = aggregateSessions(args.home, { days, now: args.now });
|
|
277
|
+
if (projectFilter !== void 0) {
|
|
278
|
+
const needle = projectFilter === "" ? projectSlug(args.cwd ?? "") : projectFilter;
|
|
279
|
+
rows = rows.filter((r) => r.project.includes(needle));
|
|
280
|
+
}
|
|
281
|
+
if (!rows.length) {
|
|
282
|
+
deps.out(t("no sessions in the last {days} day(s)", { days: String(days) }));
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
const sessions = rows.reduce((s, r) => s + r.sessions, 0);
|
|
286
|
+
const messages = rows.reduce((s, r) => s + r.messages, 0);
|
|
287
|
+
const input = rows.reduce((s, r) => s + r.inputTokens + r.usageInputTokens, 0);
|
|
288
|
+
const output = rows.reduce((s, r) => s + r.outputTokens + r.usageOutputTokens, 0);
|
|
289
|
+
const cachedIn = rows.reduce((s, r) => s + r.cachedInputTokens, 0);
|
|
290
|
+
const cacheWrite = rows.reduce((s, r) => s + r.cacheWriteTokens, 0);
|
|
291
|
+
const totals = rows.flatMap((r) => r.sessionTotals);
|
|
292
|
+
const avg = totals.length ? totals.reduce((s, n) => s + n, 0) / totals.length : 0;
|
|
293
|
+
const tools = {};
|
|
294
|
+
for (const r of rows) for (const [name, count] of Object.entries(r.toolCalls)) tools[name] = (tools[name] ?? 0) + count;
|
|
295
|
+
const modelUsage = {};
|
|
296
|
+
for (const r of rows) {
|
|
297
|
+
for (const [name, m] of Object.entries(r.modelUsage)) {
|
|
298
|
+
let mt = modelUsage[name];
|
|
299
|
+
if (!mt) {
|
|
300
|
+
mt = { turns: 0, inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, cacheWriteTokens: 0 };
|
|
301
|
+
modelUsage[name] = mt;
|
|
302
|
+
}
|
|
303
|
+
mt.turns += m.turns;
|
|
304
|
+
mt.inputTokens += m.inputTokens;
|
|
305
|
+
mt.outputTokens += m.outputTokens;
|
|
306
|
+
mt.cachedInputTokens += m.cachedInputTokens;
|
|
307
|
+
mt.cacheWriteTokens += m.cacheWriteTokens;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const line of box("OVERVIEW", [
|
|
311
|
+
["Sessions", fmtCount(sessions)],
|
|
312
|
+
["Messages", fmtCount(messages)],
|
|
313
|
+
["Days", String(days)]
|
|
314
|
+
])) deps.out(line);
|
|
315
|
+
deps.out("");
|
|
316
|
+
for (const line of box("TOKENS", [
|
|
317
|
+
["Input", fmtCompact(input)],
|
|
318
|
+
["Output", fmtCompact(output)],
|
|
319
|
+
["Cache Read", fmtCompact(cachedIn)],
|
|
320
|
+
["Cache Write", fmtCompact(cacheWrite)],
|
|
321
|
+
["Avg Tokens/Session", fmtCompact(avg)],
|
|
322
|
+
["Median Tokens/Session", fmtCompact(median(totals))]
|
|
323
|
+
])) deps.out(line);
|
|
324
|
+
if (modelsFlag !== void 0) {
|
|
325
|
+
let modelEntries = Object.entries(modelUsage).sort(
|
|
326
|
+
(a, b) => b[1].inputTokens + b[1].outputTokens - (a[1].inputTokens + a[1].outputTokens) || a[0].localeCompare(b[0])
|
|
327
|
+
);
|
|
328
|
+
if (modelsFlag !== "all") modelEntries = modelEntries.slice(0, Number(modelsFlag));
|
|
329
|
+
if (modelEntries.length) {
|
|
330
|
+
deps.out("");
|
|
331
|
+
for (const line of modelBox(modelEntries)) deps.out(line);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
let toolEntries = Object.entries(tools).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
335
|
+
if (toolsCap !== void 0) toolEntries = toolEntries.slice(0, toolsCap);
|
|
336
|
+
if (toolEntries.length) {
|
|
337
|
+
deps.out("");
|
|
338
|
+
for (const line of toolBox(toolEntries)) deps.out(line);
|
|
339
|
+
}
|
|
340
|
+
deps.out("");
|
|
341
|
+
deps.out(t("sessions (last {days} day(s)):", { days: String(days) }));
|
|
342
|
+
for (const r of rows) {
|
|
343
|
+
const inputTotal = r.inputTokens + r.usageInputTokens;
|
|
344
|
+
const outputTotal = r.outputTokens + r.usageOutputTokens;
|
|
345
|
+
deps.out(`${r.project} ${r.sessions} ${t("session(s)")} ${r.messages} ${t("message(s)")} ${fmtCompact(inputTotal)}in/${fmtCompact(outputTotal)}out`);
|
|
346
|
+
}
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
export {
|
|
350
|
+
aggregateSessions,
|
|
351
|
+
fmtCompact,
|
|
352
|
+
runStatsCommand
|
|
353
|
+
};
|