reasonix 0.4.23 → 0.4.24
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.md +23 -0
- package/dist/cli/index.js +299 -53
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +123 -1
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2595,4 +2595,126 @@ declare function compareVersions(a: string, b: string): number;
|
|
|
2595
2595
|
*/
|
|
2596
2596
|
declare function isNpxInstall(): boolean;
|
|
2597
2597
|
|
|
2598
|
-
|
|
2598
|
+
/**
|
|
2599
|
+
* Persistent per-turn usage log at `~/.reasonix/usage.jsonl`.
|
|
2600
|
+
*
|
|
2601
|
+
* Each line is a single `UsageRecord` — one turn's tokens + cost
|
|
2602
|
+
* snapshot — appended after every `assistant_final` event. This is
|
|
2603
|
+
* what drives `reasonix stats` (the dashboard, no-arg form), so the
|
|
2604
|
+
* user can see how much they've spent vs what the equivalent Claude
|
|
2605
|
+
* spend would have been. The Pillar 1 pitch (94–97% cost reduction
|
|
2606
|
+
* vs Claude, from the v0.3 hard-number table) becomes a fact users
|
|
2607
|
+
* can verify on their own machine.
|
|
2608
|
+
*
|
|
2609
|
+
* Format choices:
|
|
2610
|
+
* - **append-only JSONL** — one line per turn, durable, survives
|
|
2611
|
+
* abrupt exits. A corrupted tail line loses at most one record.
|
|
2612
|
+
* - **flat keys, no nesting** — readable with `jq` / `cut` / `awk`;
|
|
2613
|
+
* the model doesn't need to parse this, humans do.
|
|
2614
|
+
* - **best-effort writes** — disk errors never propagate into the
|
|
2615
|
+
* turn. We log nothing (no `console.error`) because the TUI is
|
|
2616
|
+
* rendering Ink; a silent skip is the least-worst failure mode.
|
|
2617
|
+
* - **no PII, no prompts, no completions** — the log contains
|
|
2618
|
+
* tokens and costs, that's it. Sessions are identified by the
|
|
2619
|
+
* user-chosen name (never a prompt).
|
|
2620
|
+
*
|
|
2621
|
+
* This file is deliberately NOT wired through project memory or
|
|
2622
|
+
* skills — those are content pins. Usage is pure telemetry.
|
|
2623
|
+
*/
|
|
2624
|
+
|
|
2625
|
+
/** One turn's snapshot — serialized verbatim as a JSONL line. */
|
|
2626
|
+
interface UsageRecord {
|
|
2627
|
+
/** Epoch millis when the record was written. */
|
|
2628
|
+
ts: number;
|
|
2629
|
+
/** Session name if the turn ran inside a persisted session, `null` for ephemeral. */
|
|
2630
|
+
session: string | null;
|
|
2631
|
+
/** Model id the turn ran against (drives the pricing lookup). */
|
|
2632
|
+
model: string;
|
|
2633
|
+
promptTokens: number;
|
|
2634
|
+
completionTokens: number;
|
|
2635
|
+
cacheHitTokens: number;
|
|
2636
|
+
cacheMissTokens: number;
|
|
2637
|
+
/** Total cost of the turn in USD. */
|
|
2638
|
+
costUsd: number;
|
|
2639
|
+
/** What the same turn would have cost at Claude Sonnet 4.6 rates. */
|
|
2640
|
+
claudeEquivUsd: number;
|
|
2641
|
+
}
|
|
2642
|
+
/** Where the log lives. Tests override via `opts.path`. */
|
|
2643
|
+
declare function defaultUsageLogPath(homeDirOverride?: string): string;
|
|
2644
|
+
interface AppendUsageInput {
|
|
2645
|
+
session: string | null;
|
|
2646
|
+
model: string;
|
|
2647
|
+
usage: Usage;
|
|
2648
|
+
/** Override the timestamp (tests). */
|
|
2649
|
+
now?: number;
|
|
2650
|
+
/** Override the log path (tests). */
|
|
2651
|
+
path?: string;
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Append one record and return it. Swallows disk errors — the TUI
|
|
2655
|
+
* should keep working even if `~/.reasonix/` is read-only.
|
|
2656
|
+
*
|
|
2657
|
+
* Returns the record that was written (or would have been written
|
|
2658
|
+
* if the disk had cooperated) so tests / callers can assert on the
|
|
2659
|
+
* computed cost fields without a round trip through the log file.
|
|
2660
|
+
*/
|
|
2661
|
+
declare function appendUsage(input: AppendUsageInput): UsageRecord;
|
|
2662
|
+
/**
|
|
2663
|
+
* Read + parse the log. Malformed lines are silently skipped so a
|
|
2664
|
+
* single corrupted write (half-flushed on power loss, user hand-edit)
|
|
2665
|
+
* doesn't throw away the rest of the history.
|
|
2666
|
+
*/
|
|
2667
|
+
declare function readUsageLog(path?: string): UsageRecord[];
|
|
2668
|
+
/** One row of the `reasonix stats` dashboard — a rolled-up window. */
|
|
2669
|
+
interface UsageBucket {
|
|
2670
|
+
label: string;
|
|
2671
|
+
/** Start of the window as epoch millis. `0` = unbounded (all-time). */
|
|
2672
|
+
since: number;
|
|
2673
|
+
turns: number;
|
|
2674
|
+
promptTokens: number;
|
|
2675
|
+
completionTokens: number;
|
|
2676
|
+
cacheHitTokens: number;
|
|
2677
|
+
cacheMissTokens: number;
|
|
2678
|
+
costUsd: number;
|
|
2679
|
+
claudeEquivUsd: number;
|
|
2680
|
+
}
|
|
2681
|
+
/** Cache hit ratio for a bucket — zero denominator returns 0. */
|
|
2682
|
+
declare function bucketCacheHitRatio(b: UsageBucket): number;
|
|
2683
|
+
/** Savings vs Claude as a fraction (0.94 = 94% savings). 0 if Claude cost is 0. */
|
|
2684
|
+
declare function bucketSavingsFraction(b: UsageBucket): number;
|
|
2685
|
+
interface AggregateOptions {
|
|
2686
|
+
/** Override `Date.now()` for deterministic tests. */
|
|
2687
|
+
now?: number;
|
|
2688
|
+
}
|
|
2689
|
+
interface UsageAggregate {
|
|
2690
|
+
/** Fixed-order rolling windows: today, week, month, all-time. */
|
|
2691
|
+
buckets: UsageBucket[];
|
|
2692
|
+
/** Model id → turn count. Sorted descending; top entry is the "most used." */
|
|
2693
|
+
byModel: Array<{
|
|
2694
|
+
model: string;
|
|
2695
|
+
turns: number;
|
|
2696
|
+
}>;
|
|
2697
|
+
/** Session name → turn count. Sorted descending. Null sessions are grouped under `"(ephemeral)"`. */
|
|
2698
|
+
bySession: Array<{
|
|
2699
|
+
session: string;
|
|
2700
|
+
turns: number;
|
|
2701
|
+
}>;
|
|
2702
|
+
/** Earliest record's ts, or `null` when the log is empty. Drives "saved $X since <date>". */
|
|
2703
|
+
firstSeen: number | null;
|
|
2704
|
+
/** Latest record's ts, or `null` when the log is empty. */
|
|
2705
|
+
lastSeen: number | null;
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Fold a flat record list into the dashboard shape — rolling windows
|
|
2709
|
+
* plus model / session histograms. Windows are INCLUSIVE of boundary:
|
|
2710
|
+
* - today = last 24h (rolling, not calendar-day)
|
|
2711
|
+
* - week = last 7d
|
|
2712
|
+
* - month = last 30d
|
|
2713
|
+
* - all = every record
|
|
2714
|
+
* Rolling windows avoid "it's 00:03, 'today' is empty" surprises.
|
|
2715
|
+
*/
|
|
2716
|
+
declare function aggregateUsage(records: UsageRecord[], opts?: AggregateOptions): UsageAggregate;
|
|
2717
|
+
/** File-size helper for the stats header — "1.2 MB" etc. Returns "" if missing. */
|
|
2718
|
+
declare function formatLogSize(path?: string): string;
|
|
2719
|
+
|
|
2720
|
+
export { type AggregateOptions, AppendOnlyLog, type AppendUsageInput, type ApplyResult, type ApplyStatus, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FilesystemToolsOptions, type FlattenDecision, type FlattenOptions, type GetLatestVersionOptions, type GetPromptResult, HOOK_EVENTS, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME, type HarvestOptions, type HookConfig, type HookEvent, type HookOutcome, type HookPayload, type HookReport, type HookScope, type HookSettings, type HookSpawnInput, type HookSpawnResult, type HookSpawner, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type InspectionReport, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, LATEST_CACHE_TTL_MS, LATEST_FETCH_TIMEOUT_MS, type ListPromptsResult, type ListResourcesResult, type ListToolsResult, type LoadHookSettingsOptions, type LoopEvent, MCP_PROTOCOL_VERSION, MEMORY_INDEX_FILE, MEMORY_INDEX_MAX_CHARS, McpClient, type McpClientOptions, type McpContentBlock, type McpProgressHandler, type McpProgressInfo, type McpPrompt, type McpPromptArgument, type McpPromptMessage, type McpPromptResourceBlock, type McpResource, type McpResourceContents, type McpResourceContentsBlob, type McpResourceContentsText, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type MemoryEntry, type MemoryScope, MemoryStore, type MemoryStoreOptions, type MemoryToolsOptions, type MemoryType, type WriteInput as MemoryWriteInput, NeedsConfirmationError, PROJECT_MEMORY_FILE, PROJECT_MEMORY_MAX_CHARS, type PageContent, PlanProposedError, type PlanToolOptions, type ProgressNotificationParams, type ProjectMemory, type ReadResourceResult, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type ResolvedHook, type RetryInfo, type RetryOptions, type Role, type RunCommandResult, type RunHooksOptions, type ScavengeOptions, type ScavengeResult, type SearchResult, type SectionResult, type SessionInfo, SessionStats, type SessionSummary, type ShellToolsOptions, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, type ToolCallContext, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, USER_MEMORY_DIR, Usage, type UsageAggregate, type UsageBucket, type UsageRecord, VERSION, VolatileScratch, type WebFetchOptions, type WebSearchOptions, type WebToolsOptions, aggregateBranchUsage, aggregateUsage, analyzeSchema, appendSessionMessage, appendUsage, applyEditBlock, applyEditBlocks, applyMemoryStack, applyProjectMemory, applyUserMemory, bridgeMcpTools, bucketCacheHitRatio, bucketSavingsFraction, claudeEquivalentCost, codeSystemPrompt, compareVersions, computeReplayStats, costUsd, decideOutcome, defaultConfigPath, defaultSelector, defaultUsageLogPath, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatCommandResult, formatHookOutcomeMessage, formatLogSize, formatLoopError, formatSearchResults, getLatestVersion, globalSettingsPath, harvest, healLoadedMessages, htmlToText, inputCostUsd, inspectMcpServer, isAllowed, isJsonRpcError, isNpxInstall, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadHooks, loadSessionMessages, matchesTool, memoryEnabled, nestArguments, openTranscriptFile, outputCostUsd, parseEditBlocks, parseMcpSpec, parseMojeekResults, parseTranscript, prepareSpawn, projectHash, projectSettingsPath, quoteForCmdExe, readConfig, readProjectMemory, readTranscript, readUsageLog, recordFromLoopEvent, redactKey, registerFilesystemTools, registerMemoryTools, registerPlanTool, registerShellTools, registerWebTools, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, resolveExecutable, restoreSnapshots, runBranches, runCommand, runHooks, sanitizeMemoryName, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, stripHallucinatedToolMarkup, tokenizeCommand, truncateForModel, webFetch, webSearch, writeConfig, writeMeta, writeRecord };
|
package/dist/index.js
CHANGED
|
@@ -5099,6 +5099,132 @@ function isNpxInstall() {
|
|
|
5099
5099
|
if (ua.includes("npx/")) return true;
|
|
5100
5100
|
return false;
|
|
5101
5101
|
}
|
|
5102
|
+
|
|
5103
|
+
// src/usage.ts
|
|
5104
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
5105
|
+
import { homedir as homedir7 } from "os";
|
|
5106
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
5107
|
+
function defaultUsageLogPath(homeDirOverride) {
|
|
5108
|
+
return join10(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
|
|
5109
|
+
}
|
|
5110
|
+
function appendUsage(input) {
|
|
5111
|
+
const record = {
|
|
5112
|
+
ts: input.now ?? Date.now(),
|
|
5113
|
+
session: input.session,
|
|
5114
|
+
model: input.model,
|
|
5115
|
+
promptTokens: input.usage.promptTokens,
|
|
5116
|
+
completionTokens: input.usage.completionTokens,
|
|
5117
|
+
cacheHitTokens: input.usage.promptCacheHitTokens,
|
|
5118
|
+
cacheMissTokens: input.usage.promptCacheMissTokens,
|
|
5119
|
+
costUsd: costUsd(input.model, input.usage),
|
|
5120
|
+
claudeEquivUsd: claudeEquivalentCost(input.usage)
|
|
5121
|
+
};
|
|
5122
|
+
const path = input.path ?? defaultUsageLogPath();
|
|
5123
|
+
try {
|
|
5124
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
5125
|
+
appendFileSync2(path, `${JSON.stringify(record)}
|
|
5126
|
+
`, "utf8");
|
|
5127
|
+
} catch {
|
|
5128
|
+
}
|
|
5129
|
+
return record;
|
|
5130
|
+
}
|
|
5131
|
+
function readUsageLog(path = defaultUsageLogPath()) {
|
|
5132
|
+
if (!existsSync10(path)) return [];
|
|
5133
|
+
let raw;
|
|
5134
|
+
try {
|
|
5135
|
+
raw = readFileSync12(path, "utf8");
|
|
5136
|
+
} catch {
|
|
5137
|
+
return [];
|
|
5138
|
+
}
|
|
5139
|
+
const out = [];
|
|
5140
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
5141
|
+
if (!line.trim()) continue;
|
|
5142
|
+
try {
|
|
5143
|
+
const rec = JSON.parse(line);
|
|
5144
|
+
if (isValidRecord(rec)) out.push(rec);
|
|
5145
|
+
} catch {
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
return out;
|
|
5149
|
+
}
|
|
5150
|
+
function isValidRecord(rec) {
|
|
5151
|
+
if (!rec || typeof rec !== "object") return false;
|
|
5152
|
+
const r = rec;
|
|
5153
|
+
return typeof r.ts === "number" && typeof r.model === "string" && typeof r.promptTokens === "number" && typeof r.completionTokens === "number" && typeof r.cacheHitTokens === "number" && typeof r.cacheMissTokens === "number" && typeof r.costUsd === "number" && typeof r.claudeEquivUsd === "number";
|
|
5154
|
+
}
|
|
5155
|
+
function bucketCacheHitRatio(b) {
|
|
5156
|
+
const denom = b.cacheHitTokens + b.cacheMissTokens;
|
|
5157
|
+
return denom > 0 ? b.cacheHitTokens / denom : 0;
|
|
5158
|
+
}
|
|
5159
|
+
function bucketSavingsFraction(b) {
|
|
5160
|
+
return b.claudeEquivUsd > 0 ? 1 - b.costUsd / b.claudeEquivUsd : 0;
|
|
5161
|
+
}
|
|
5162
|
+
function emptyBucket(label, since) {
|
|
5163
|
+
return {
|
|
5164
|
+
label,
|
|
5165
|
+
since,
|
|
5166
|
+
turns: 0,
|
|
5167
|
+
promptTokens: 0,
|
|
5168
|
+
completionTokens: 0,
|
|
5169
|
+
cacheHitTokens: 0,
|
|
5170
|
+
cacheMissTokens: 0,
|
|
5171
|
+
costUsd: 0,
|
|
5172
|
+
claudeEquivUsd: 0
|
|
5173
|
+
};
|
|
5174
|
+
}
|
|
5175
|
+
function addToBucket(b, r) {
|
|
5176
|
+
b.turns += 1;
|
|
5177
|
+
b.promptTokens += r.promptTokens;
|
|
5178
|
+
b.completionTokens += r.completionTokens;
|
|
5179
|
+
b.cacheHitTokens += r.cacheHitTokens;
|
|
5180
|
+
b.cacheMissTokens += r.cacheMissTokens;
|
|
5181
|
+
b.costUsd += r.costUsd;
|
|
5182
|
+
b.claudeEquivUsd += r.claudeEquivUsd;
|
|
5183
|
+
}
|
|
5184
|
+
function aggregateUsage(records, opts = {}) {
|
|
5185
|
+
const now = opts.now ?? Date.now();
|
|
5186
|
+
const day = 24 * 60 * 60 * 1e3;
|
|
5187
|
+
const today = emptyBucket("today", now - day);
|
|
5188
|
+
const week = emptyBucket("week", now - 7 * day);
|
|
5189
|
+
const month = emptyBucket("month", now - 30 * day);
|
|
5190
|
+
const all = emptyBucket("all-time", 0);
|
|
5191
|
+
const modelCounts = /* @__PURE__ */ new Map();
|
|
5192
|
+
const sessionCounts = /* @__PURE__ */ new Map();
|
|
5193
|
+
let firstSeen = null;
|
|
5194
|
+
let lastSeen = null;
|
|
5195
|
+
for (const r of records) {
|
|
5196
|
+
addToBucket(all, r);
|
|
5197
|
+
if (r.ts >= today.since) addToBucket(today, r);
|
|
5198
|
+
if (r.ts >= week.since) addToBucket(week, r);
|
|
5199
|
+
if (r.ts >= month.since) addToBucket(month, r);
|
|
5200
|
+
modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);
|
|
5201
|
+
const sessKey = r.session ?? "(ephemeral)";
|
|
5202
|
+
sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);
|
|
5203
|
+
if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;
|
|
5204
|
+
if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;
|
|
5205
|
+
}
|
|
5206
|
+
const byModel = Array.from(modelCounts.entries()).map(([model, turns]) => ({ model, turns })).sort((a, b) => b.turns - a.turns);
|
|
5207
|
+
const bySession = Array.from(sessionCounts.entries()).map(([session, turns]) => ({ session, turns })).sort((a, b) => b.turns - a.turns);
|
|
5208
|
+
return {
|
|
5209
|
+
buckets: [today, week, month, all],
|
|
5210
|
+
byModel,
|
|
5211
|
+
bySession,
|
|
5212
|
+
firstSeen,
|
|
5213
|
+
lastSeen
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
function formatLogSize(path = defaultUsageLogPath()) {
|
|
5217
|
+
if (!existsSync10(path)) return "";
|
|
5218
|
+
try {
|
|
5219
|
+
const s = statSync4(path);
|
|
5220
|
+
const bytes = s.size;
|
|
5221
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
5222
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
5223
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
5224
|
+
} catch {
|
|
5225
|
+
return "";
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5102
5228
|
export {
|
|
5103
5229
|
AppendOnlyLog,
|
|
5104
5230
|
CODE_SYSTEM_PROMPT,
|
|
@@ -5131,14 +5257,18 @@ export {
|
|
|
5131
5257
|
VERSION,
|
|
5132
5258
|
VolatileScratch,
|
|
5133
5259
|
aggregateBranchUsage,
|
|
5260
|
+
aggregateUsage,
|
|
5134
5261
|
analyzeSchema,
|
|
5135
5262
|
appendSessionMessage,
|
|
5263
|
+
appendUsage,
|
|
5136
5264
|
applyEditBlock,
|
|
5137
5265
|
applyEditBlocks,
|
|
5138
5266
|
applyMemoryStack,
|
|
5139
5267
|
applyProjectMemory,
|
|
5140
5268
|
applyUserMemory,
|
|
5141
5269
|
bridgeMcpTools,
|
|
5270
|
+
bucketCacheHitRatio,
|
|
5271
|
+
bucketSavingsFraction,
|
|
5142
5272
|
claudeEquivalentCost,
|
|
5143
5273
|
codeSystemPrompt,
|
|
5144
5274
|
compareVersions,
|
|
@@ -5147,6 +5277,7 @@ export {
|
|
|
5147
5277
|
decideOutcome,
|
|
5148
5278
|
defaultConfigPath,
|
|
5149
5279
|
defaultSelector,
|
|
5280
|
+
defaultUsageLogPath,
|
|
5150
5281
|
deleteSession,
|
|
5151
5282
|
diffTranscripts,
|
|
5152
5283
|
emptyPlanState,
|
|
@@ -5155,6 +5286,7 @@ export {
|
|
|
5155
5286
|
flattenSchema,
|
|
5156
5287
|
formatCommandResult,
|
|
5157
5288
|
formatHookOutcomeMessage,
|
|
5289
|
+
formatLogSize,
|
|
5158
5290
|
formatLoopError,
|
|
5159
5291
|
formatSearchResults,
|
|
5160
5292
|
getLatestVersion,
|
|
@@ -5190,6 +5322,7 @@ export {
|
|
|
5190
5322
|
readConfig,
|
|
5191
5323
|
readProjectMemory,
|
|
5192
5324
|
readTranscript,
|
|
5325
|
+
readUsageLog,
|
|
5193
5326
|
recordFromLoopEvent,
|
|
5194
5327
|
redactKey,
|
|
5195
5328
|
registerFilesystemTools,
|