@v1nvn/agentic-core 0.31.0 → 0.33.0
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 +12 -13
- package/dist/index.js +1 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -1
- package/src/text-format.ts +0 -98
package/README.md
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
# @v1nvn/agentic-core
|
|
2
2
|
|
|
3
3
|
The shared runtime of the `@v1nvn` agent tools: transcript reading, CLI and
|
|
4
|
-
hook plumbing,
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
hook plumbing, logging, and the MCP server lifecycle helpers. A library — no
|
|
5
|
+
bin, not directly runnable; every other package consumes it as a workspace
|
|
6
|
+
dependency.
|
|
7
7
|
|
|
8
8
|
User-facing docs: [root README](../../README.md).
|
|
9
9
|
|
|
10
10
|
## Modules
|
|
11
11
|
|
|
12
|
-
| File
|
|
13
|
-
|
|
12
|
+
| File | Role |
|
|
13
|
+
| ------------------- | ----------------------------------------------------------------------- |
|
|
14
14
|
| `src/last-reply.ts` | finds the previous assistant reply across local Claude Code transcripts |
|
|
15
|
-
| `src/input.ts`
|
|
16
|
-
| `src/
|
|
17
|
-
| `src/
|
|
18
|
-
| `src/
|
|
19
|
-
| `src/
|
|
20
|
-
| `src/
|
|
21
|
-
| `src/
|
|
22
|
-
| `src/index.ts` | the public entry — re-exports the modules above |
|
|
15
|
+
| `src/input.ts` | stdin and markdown-file input with the CLI error contracts |
|
|
16
|
+
| `src/cli.ts` | quiet commander parsing and usage/exit helpers shared by the tool bins |
|
|
17
|
+
| `src/hook.ts` | the `UserPromptExpansion` hook plumbing the zero-token plugins ride |
|
|
18
|
+
| `src/logger.ts` | level-filtered stderr logger for the MCP servers |
|
|
19
|
+
| `src/shutdown.ts` | close-then-exit signal handling for the server entrypoints |
|
|
20
|
+
| `src/dev.ts` | the hot-reload watcher wiring the servers' `yarn dev` harnesses share |
|
|
21
|
+
| `src/index.ts` | the public entry — re-exports the modules above |
|
|
23
22
|
|
|
24
23
|
## Develop
|
|
25
24
|
|
package/dist/index.js
CHANGED
|
@@ -232,87 +232,6 @@ function shutdownOnSignals(closables, signals = ["SIGINT", "SIGTERM"]) {
|
|
|
232
232
|
for (const signal of signals) process.on(signal, shutdown);
|
|
233
233
|
}
|
|
234
234
|
//#endregion
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Plain-text rendering primitives shared by the zai and tokens report
|
|
238
|
-
* renderers. Output targets a monospace terminal / hook-block `reason`, so
|
|
239
|
-
* everything here is fixed-width: padding, block-glyph bars, and compact
|
|
240
|
-
* number formatting.
|
|
241
|
-
*/
|
|
242
|
-
var MONTHS = [
|
|
243
|
-
"Jan",
|
|
244
|
-
"Feb",
|
|
245
|
-
"Mar",
|
|
246
|
-
"Apr",
|
|
247
|
-
"May",
|
|
248
|
-
"Jun",
|
|
249
|
-
"Jul",
|
|
250
|
-
"Aug",
|
|
251
|
-
"Sep",
|
|
252
|
-
"Oct",
|
|
253
|
-
"Nov",
|
|
254
|
-
"Dec"
|
|
255
|
-
];
|
|
256
|
-
var EIGHTHS = [
|
|
257
|
-
"",
|
|
258
|
-
"▏",
|
|
259
|
-
"▎",
|
|
260
|
-
"▍",
|
|
261
|
-
"▌",
|
|
262
|
-
"▋",
|
|
263
|
-
"▊",
|
|
264
|
-
"▉"
|
|
265
|
-
];
|
|
266
|
-
var RULE_WIDTH = 68;
|
|
267
|
-
function rule() {
|
|
268
|
-
return "─".repeat(68);
|
|
269
|
-
}
|
|
270
|
-
function pad2(n) {
|
|
271
|
-
return String(n).padStart(2, "0");
|
|
272
|
-
}
|
|
273
|
-
/** Local-time `YYYY-MM-DD` key. */
|
|
274
|
-
function ymd(date) {
|
|
275
|
-
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
276
|
-
}
|
|
277
|
-
function fmtTokens(n) {
|
|
278
|
-
if (n == null || Number.isNaN(n)) return "—";
|
|
279
|
-
if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
|
|
280
|
-
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
281
|
-
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
282
|
-
return String(n);
|
|
283
|
-
}
|
|
284
|
-
function fmtNum(n) {
|
|
285
|
-
return (n || 0).toLocaleString("en-US");
|
|
286
|
-
}
|
|
287
|
-
function padR(s, n) {
|
|
288
|
-
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
289
|
-
}
|
|
290
|
-
function padL(s, n) {
|
|
291
|
-
return s.length >= n ? s : " ".repeat(n - s.length) + s;
|
|
292
|
-
}
|
|
293
|
-
/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */
|
|
294
|
-
function barField(v, max, width) {
|
|
295
|
-
if (!v || v <= 0 || max <= 0) return " ".repeat(width);
|
|
296
|
-
const scaled = v / max * width;
|
|
297
|
-
let full = Math.floor(scaled);
|
|
298
|
-
let fi = Math.round((scaled - full) * 8);
|
|
299
|
-
if (fi === 8) {
|
|
300
|
-
full += 1;
|
|
301
|
-
fi = 0;
|
|
302
|
-
}
|
|
303
|
-
if (full === 0 && fi === 0) fi = 1;
|
|
304
|
-
let s = "█".repeat(Math.min(full, width));
|
|
305
|
-
if (full < width && fi > 0) s += EIGHTHS[fi];
|
|
306
|
-
if (s.length < width) s += " ".repeat(width - s.length);
|
|
307
|
-
return s.slice(0, width);
|
|
308
|
-
}
|
|
309
|
-
/** Filled/empty meter: █ for used, ░ for remaining. */
|
|
310
|
-
function meter(pct, width) {
|
|
311
|
-
let filled = Math.round((pct || 0) / 100 * width);
|
|
312
|
-
filled = Math.max(0, Math.min(width, filled));
|
|
313
|
-
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
314
|
-
}
|
|
315
|
-
//#endregion
|
|
316
|
-
export { Logger, MONTHS, RULE_WIDTH, barField, claudeProjectsDir, emitHookBlock, fmtNum, fmtTokens, hookOrPrint, isFile, lastReply, levelEnabled, meter, pad2, padL, padR, parseQuietly, printUsageAndExit, readAll, readHookEvent, readMarkdownFile, readStdin, replyTarget, rule, shutdownOnSignals, watchWithReload, ymd };
|
|
235
|
+
export { Logger, claudeProjectsDir, emitHookBlock, hookOrPrint, isFile, lastReply, levelEnabled, parseQuietly, printUsageAndExit, readAll, readHookEvent, readMarkdownFile, readStdin, replyTarget, shutdownOnSignals, watchWithReload };
|
|
317
236
|
|
|
318
237
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/input.ts","../src/hook.ts","../src/cli.ts","../src/dev.ts","../src/last-reply.ts","../src/logger.ts","../src/shutdown.ts","../src/text-format.ts"],"sourcesContent":["import { readFileSync, statSync } from 'node:fs';\n\nexport function readAll(stream: NodeJS.ReadableStream): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n stream.setEncoding('utf8');\n stream.on('data', (chunk: string) => {\n data += chunk;\n });\n stream.on('end', () => {\n resolve(data);\n });\n stream.on('error', reject);\n });\n}\n\nexport function readStdin(\n stream: NodeJS.ReadableStream = process.stdin,\n): Promise<string> {\n return readAll(stream);\n}\n\n/** A markdown file's contents, with the CLI's no-such-file error. */\nexport function readMarkdownFile(path: string): string {\n const stats = statSync(path, { throwIfNoEntry: false });\n if (!stats?.isFile()) {\n throw new Error(`no such file: ${path}`);\n }\n return readFileSync(path, 'utf8');\n}\n","import { statSync } from 'node:fs';\n\nimport { readAll } from './input.js';\n\nexport interface HookEvent {\n cwd?: string;\n session_id?: string;\n transcript_path?: string;\n}\n\n/**\n * Read the Claude Code hook event JSON from stdin; empty or unparseable input\n * yields an empty event, never a thrown error — a hook must always answer.\n */\nexport function readHookEvent(\n stream: NodeJS.ReadableStream = process.stdin,\n): Promise<HookEvent> {\n return readAll(stream).then(\n data => {\n try {\n return JSON.parse(data) as HookEvent;\n } catch {\n return {};\n }\n },\n () => ({}),\n );\n}\n\n/**\n * Which transcript lastReply should read, per the hook event: transcript_path\n * when it names a real file, else session_id, else nothing (lastReply then\n * falls back to the newest session for the current project).\n */\nexport function replyTarget(event: HookEvent): string | undefined {\n if (event.transcript_path !== undefined && isFile(event.transcript_path)) {\n return event.transcript_path;\n }\n return event.session_id || undefined;\n}\n\nexport function isFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Print the UserPromptExpansion decision for a zero-token command: `block`\n * keeps the command from reaching the model, with the output as the `reason`.\n */\nexport function emitHookBlock(reason: string): void {\n process.stdout.write(`${JSON.stringify({ decision: 'block', reason })}\\n`);\n}\n","import type { HookEvent } from './hook.js';\n\nimport { emitHookBlock, readHookEvent } from './hook.js';\n\nimport type { Command } from 'commander';\n\nexport function parseQuietly<T = never>(\n program: Command,\n args: readonly string[],\n recover?: (err: unknown) => T | undefined,\n): Command | T | undefined {\n try {\n program\n .allowExcessArguments(false)\n .exitOverride()\n .configureOutput({ writeOut: () => undefined, writeErr: () => undefined })\n .parse([...args], { from: 'user' });\n return program;\n } catch (err) {\n return recover?.(err);\n }\n}\n\nexport function printUsageAndExit(program: Command): never {\n console.error(program.helpInformation());\n // CLIs report failure through the exit code; the rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(1);\n}\n\nexport async function hookOrPrint(\n hook: boolean,\n failure: string,\n run: (event: HookEvent | undefined) => Promise<string> | string,\n stdin: NodeJS.ReadableStream = process.stdin,\n): Promise<void> {\n if (hook) {\n let reason: string;\n try {\n reason = await run(await readHookEvent(stdin));\n } catch (e) {\n reason = `${failure}: ${(e as Error).message}`;\n }\n emitHookBlock(reason);\n return;\n }\n try {\n console.log(await run(undefined));\n } catch (e) {\n console.error((e as Error).message);\n // CLIs report failure through the exit code; the rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(1);\n }\n}\n","// Structural stand-ins for vite's dev server and module runner — the shared\n// hot-reload wiring below must not pull vite into core's runtime surface; the\n// MCP packages pass their real instances through.\nexport interface ReloadableRunner {\n readonly evaluatedModules: { clear(): void };\n import<T>(entry: string): Promise<T>;\n}\n\nexport interface WatchableDevServer {\n close(): Promise<unknown>;\n readonly watcher: {\n on(event: 'change', cb: (file: string) => void): unknown;\n };\n}\n\nconst RELOAD_DEBOUNCE_MS = 150;\n\nconst IGNORED_SEGMENTS = ['/node_modules/', '/dist/', '/.git/', '/coverage/'];\n\nfunction shouldReload(file: string): boolean {\n return IGNORED_SEGMENTS.every(seg => !file.includes(seg));\n}\n\n/**\n * Debounced file-change reloads, serialized via a promise chain: concurrent\n * saves never overlap, and a mid-reload save still produces a follow-up.\n */\nexport function watchWithReload(\n vite: WatchableDevServer,\n reload: () => Promise<void>,\n): void {\n let chain: Promise<void> = Promise.resolve();\n let timer: NodeJS.Timeout | undefined;\n vite.watcher.on('change', file => {\n if (!shouldReload(file)) {\n return;\n }\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = undefined;\n chain = chain.then(reload);\n }, RELOAD_DEBOUNCE_MS);\n });\n}\n","import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { isFile } from './hook.js';\n\ninterface ContentBlock {\n text?: string;\n type: string;\n}\n\ninterface TranscriptEntry {\n message?: { content?: unknown };\n type?: string;\n}\n\nfunction textBlocks(entry: TranscriptEntry): ContentBlock[] | undefined {\n const content = entry.message?.content;\n if (entry.type !== 'assistant' || !Array.isArray(content)) {\n return undefined;\n }\n const blocks = content.filter(\n (b): b is ContentBlock =>\n typeof b === 'object' &&\n b !== null &&\n (b as ContentBlock).type === 'text',\n );\n return blocks.length > 0 ? blocks : undefined;\n}\n\nexport function claudeProjectsDir(): string {\n const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), '.claude');\n return join(claudeDir, 'projects');\n}\n\n/**\n * The last assistant text reply from a Claude Code session, reproducing Claude\n * Code's `/copy` byte-for-byte: the last assistant entry that contains a\n * `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n * joined with a blank line, and no trailing newline.\n *\n * @param arg a transcript file, a session UUID under the project dir, or\n * nothing to use the newest session for the current project.\n */\nexport function lastReply(arg?: string): string {\n // Claude Code keys transcripts under the project root with \"/\" → \"-\".\n // Prefer $CLAUDE_PROJECT_DIR (exported to hook processes) over $PWD.\n const proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\n '/',\n '-',\n );\n const projDir = join(claudeProjectsDir(), proj);\n\n let file: string | undefined;\n if (arg !== undefined) {\n file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n } else if (existsSync(projDir)) {\n const newest = readdirSync(projDir)\n .filter(f => f.endsWith('.jsonl'))\n .map(f => ({ f, mtimeMs: statSync(join(projDir, f)).mtimeMs }))\n .sort((a, b) => b.mtimeMs - a.mtimeMs)\n .at(0);\n if (newest) {\n file = join(projDir, newest.f);\n }\n }\n\n if (file === undefined || !isFile(file)) {\n throw new Error(`no session transcript found in ${projDir}`);\n }\n\n let last: ContentBlock[] | undefined;\n for (const line of readFileSync(file, 'utf8').split('\\n')) {\n if (!line.includes('\"assistant\"')) {\n continue;\n }\n let entry: TranscriptEntry;\n try {\n entry = JSON.parse(line) as TranscriptEntry;\n } catch {\n continue;\n }\n const blocks = textBlocks(entry);\n if (blocks) {\n last = blocks;\n }\n }\n return (last ?? []).map(b => b.text ?? '').join('\\n\\n');\n}\n","export type LogLevel = 'debug' | 'error' | 'info' | 'silent' | 'warn';\n\nconst LEVEL_RANK: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: Number.MAX_SAFE_INTEGER,\n};\n\nexport function levelEnabled(\n active: LogLevel,\n wanted: Exclude<LogLevel, 'silent'>,\n): boolean {\n return LEVEL_RANK[wanted] >= LEVEL_RANK[active];\n}\n\nconst LEVEL_LABEL: Record<Exclude<LogLevel, 'silent'>, string> = {\n debug: 'DEBUG',\n error: 'ERROR',\n info: 'INFO',\n warn: 'WARN',\n};\n\ntype Writer = (message: string) => void;\n\n// stdout carries the MCP transport; everything here goes to stderr.\nexport class Logger {\n private readonly stderr: Writer;\n\n constructor(\n readonly activeLevel: LogLevel,\n stderr: Writer = line => process.stderr.write(`${line}\\n`),\n ) {\n this.stderr = stderr;\n }\n\n debug(message: string): void {\n this.log('debug', message);\n }\n\n error(message: string): void {\n this.log('error', message);\n }\n\n info(message: string): void {\n this.log('info', message);\n }\n\n warn(message: string): void {\n this.log('warn', message);\n }\n\n private log(level: Exclude<LogLevel, 'silent'>, message: string): void {\n if (levelEnabled(this.activeLevel, level)) {\n this.stderr(`${LEVEL_LABEL[level]} ${message}`);\n }\n }\n}\n","/** Close each closable in order (best-effort), then force-exit. */\nexport function shutdownOnSignals(\n closables: readonly { close(): Promise<unknown> }[],\n signals: readonly NodeJS.Signals[] = ['SIGINT', 'SIGTERM'],\n): void {\n function shutdown(): void {\n void closables\n .reduce(\n (acc: Promise<void>, closable) =>\n acc.then(async () => {\n try {\n await closable.close();\n } catch {\n // best-effort; exiting regardless\n }\n return;\n }),\n Promise.resolve(),\n )\n .finally(() => {\n // Force-exit on signal; the n/no-process-exit rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(0);\n });\n }\n for (const signal of signals) {\n process.on(signal, shutdown);\n }\n}\n","/**\n * Plain-text rendering primitives shared by the zai and tokens report\n * renderers. Output targets a monospace terminal / hook-block `reason`, so\n * everything here is fixed-width: padding, block-glyph bars, and compact\n * number formatting.\n */\n\nexport const MONTHS = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\nconst EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];\n\nexport const RULE_WIDTH = 68;\n\nexport function rule(): string {\n return '─'.repeat(RULE_WIDTH);\n}\n\nexport function pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\n/** Local-time `YYYY-MM-DD` key. */\nexport function ymd(date: Date): string {\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;\n}\n\nexport function fmtTokens(n: null | number | undefined): string {\n if (n == null || Number.isNaN(n)) {\n return '—';\n }\n if (n >= 1e9) {\n return (n / 1e9).toFixed(1) + 'B';\n }\n if (n >= 1e6) {\n return (n / 1e6).toFixed(1) + 'M';\n }\n if (n >= 1e3) {\n return (n / 1e3).toFixed(1) + 'K';\n }\n return String(n);\n}\n\nexport function fmtNum(n: null | number | undefined): string {\n return (n || 0).toLocaleString('en-US');\n}\n\nexport function padR(s: string, n: number): string {\n return s.length >= n ? s : s + ' '.repeat(n - s.length);\n}\n\nexport function padL(s: string, n: number): string {\n return s.length >= n ? s : ' '.repeat(n - s.length) + s;\n}\n\n/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */\nexport function barField(v: number, max: number, width: number): string {\n if (!v || v <= 0 || max <= 0) {\n return ' '.repeat(width);\n }\n const scaled = (v / max) * width;\n let full = Math.floor(scaled);\n let fi = Math.round((scaled - full) * 8);\n if (fi === 8) {\n full += 1;\n fi = 0;\n }\n if (full === 0 && fi === 0) {\n fi = 1;\n } // keep a sliver for any nonzero value\n let s = '█'.repeat(Math.min(full, width));\n if (full < width && fi > 0) {\n s += EIGHTHS[fi];\n }\n if (s.length < width) {\n s += ' '.repeat(width - s.length);\n }\n return s.slice(0, width);\n}\n\n/** Filled/empty meter: █ for used, ░ for remaining. */\nexport function meter(pct: number | undefined, width: number): string {\n let filled = Math.round(((pct || 0) / 100) * width);\n filled = Math.max(0, Math.min(width, filled));\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/input.ts","../src/hook.ts","../src/cli.ts","../src/dev.ts","../src/last-reply.ts","../src/logger.ts","../src/shutdown.ts"],"sourcesContent":["import { readFileSync, statSync } from 'node:fs';\n\nexport function readAll(stream: NodeJS.ReadableStream): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n stream.setEncoding('utf8');\n stream.on('data', (chunk: string) => {\n data += chunk;\n });\n stream.on('end', () => {\n resolve(data);\n });\n stream.on('error', reject);\n });\n}\n\nexport function readStdin(\n stream: NodeJS.ReadableStream = process.stdin,\n): Promise<string> {\n return readAll(stream);\n}\n\n/** A markdown file's contents, with the CLI's no-such-file error. */\nexport function readMarkdownFile(path: string): string {\n const stats = statSync(path, { throwIfNoEntry: false });\n if (!stats?.isFile()) {\n throw new Error(`no such file: ${path}`);\n }\n return readFileSync(path, 'utf8');\n}\n","import { statSync } from 'node:fs';\n\nimport { readAll } from './input.js';\n\nexport interface HookEvent {\n cwd?: string;\n session_id?: string;\n transcript_path?: string;\n}\n\n/**\n * Read the Claude Code hook event JSON from stdin; empty or unparseable input\n * yields an empty event, never a thrown error — a hook must always answer.\n */\nexport function readHookEvent(\n stream: NodeJS.ReadableStream = process.stdin,\n): Promise<HookEvent> {\n return readAll(stream).then(\n data => {\n try {\n return JSON.parse(data) as HookEvent;\n } catch {\n return {};\n }\n },\n () => ({}),\n );\n}\n\n/**\n * Which transcript lastReply should read, per the hook event: transcript_path\n * when it names a real file, else session_id, else nothing (lastReply then\n * falls back to the newest session for the current project).\n */\nexport function replyTarget(event: HookEvent): string | undefined {\n if (event.transcript_path !== undefined && isFile(event.transcript_path)) {\n return event.transcript_path;\n }\n return event.session_id || undefined;\n}\n\nexport function isFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Print the UserPromptExpansion decision for a zero-token command: `block`\n * keeps the command from reaching the model, with the output as the `reason`.\n */\nexport function emitHookBlock(reason: string): void {\n process.stdout.write(`${JSON.stringify({ decision: 'block', reason })}\\n`);\n}\n","import type { HookEvent } from './hook.js';\n\nimport { emitHookBlock, readHookEvent } from './hook.js';\n\nimport type { Command } from 'commander';\n\nexport function parseQuietly<T = never>(\n program: Command,\n args: readonly string[],\n recover?: (err: unknown) => T | undefined,\n): Command | T | undefined {\n try {\n program\n .allowExcessArguments(false)\n .exitOverride()\n .configureOutput({ writeOut: () => undefined, writeErr: () => undefined })\n .parse([...args], { from: 'user' });\n return program;\n } catch (err) {\n return recover?.(err);\n }\n}\n\nexport function printUsageAndExit(program: Command): never {\n console.error(program.helpInformation());\n // CLIs report failure through the exit code; the rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(1);\n}\n\nexport async function hookOrPrint(\n hook: boolean,\n failure: string,\n run: (event: HookEvent | undefined) => Promise<string> | string,\n stdin: NodeJS.ReadableStream = process.stdin,\n): Promise<void> {\n if (hook) {\n let reason: string;\n try {\n reason = await run(await readHookEvent(stdin));\n } catch (e) {\n reason = `${failure}: ${(e as Error).message}`;\n }\n emitHookBlock(reason);\n return;\n }\n try {\n console.log(await run(undefined));\n } catch (e) {\n console.error((e as Error).message);\n // CLIs report failure through the exit code; the rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(1);\n }\n}\n","// Structural stand-ins for vite's dev server and module runner — the shared\n// hot-reload wiring below must not pull vite into core's runtime surface; the\n// MCP packages pass their real instances through.\nexport interface ReloadableRunner {\n readonly evaluatedModules: { clear(): void };\n import<T>(entry: string): Promise<T>;\n}\n\nexport interface WatchableDevServer {\n close(): Promise<unknown>;\n readonly watcher: {\n on(event: 'change', cb: (file: string) => void): unknown;\n };\n}\n\nconst RELOAD_DEBOUNCE_MS = 150;\n\nconst IGNORED_SEGMENTS = ['/node_modules/', '/dist/', '/.git/', '/coverage/'];\n\nfunction shouldReload(file: string): boolean {\n return IGNORED_SEGMENTS.every(seg => !file.includes(seg));\n}\n\n/**\n * Debounced file-change reloads, serialized via a promise chain: concurrent\n * saves never overlap, and a mid-reload save still produces a follow-up.\n */\nexport function watchWithReload(\n vite: WatchableDevServer,\n reload: () => Promise<void>,\n): void {\n let chain: Promise<void> = Promise.resolve();\n let timer: NodeJS.Timeout | undefined;\n vite.watcher.on('change', file => {\n if (!shouldReload(file)) {\n return;\n }\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = undefined;\n chain = chain.then(reload);\n }, RELOAD_DEBOUNCE_MS);\n });\n}\n","import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { isFile } from './hook.js';\n\ninterface ContentBlock {\n text?: string;\n type: string;\n}\n\ninterface TranscriptEntry {\n message?: { content?: unknown };\n type?: string;\n}\n\nfunction textBlocks(entry: TranscriptEntry): ContentBlock[] | undefined {\n const content = entry.message?.content;\n if (entry.type !== 'assistant' || !Array.isArray(content)) {\n return undefined;\n }\n const blocks = content.filter(\n (b): b is ContentBlock =>\n typeof b === 'object' &&\n b !== null &&\n (b as ContentBlock).type === 'text',\n );\n return blocks.length > 0 ? blocks : undefined;\n}\n\nexport function claudeProjectsDir(): string {\n const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), '.claude');\n return join(claudeDir, 'projects');\n}\n\n/**\n * The last assistant text reply from a Claude Code session, reproducing Claude\n * Code's `/copy` byte-for-byte: the last assistant entry that contains a\n * `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n * joined with a blank line, and no trailing newline.\n *\n * @param arg a transcript file, a session UUID under the project dir, or\n * nothing to use the newest session for the current project.\n */\nexport function lastReply(arg?: string): string {\n // Claude Code keys transcripts under the project root with \"/\" → \"-\".\n // Prefer $CLAUDE_PROJECT_DIR (exported to hook processes) over $PWD.\n const proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\n '/',\n '-',\n );\n const projDir = join(claudeProjectsDir(), proj);\n\n let file: string | undefined;\n if (arg !== undefined) {\n file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n } else if (existsSync(projDir)) {\n const newest = readdirSync(projDir)\n .filter(f => f.endsWith('.jsonl'))\n .map(f => ({ f, mtimeMs: statSync(join(projDir, f)).mtimeMs }))\n .sort((a, b) => b.mtimeMs - a.mtimeMs)\n .at(0);\n if (newest) {\n file = join(projDir, newest.f);\n }\n }\n\n if (file === undefined || !isFile(file)) {\n throw new Error(`no session transcript found in ${projDir}`);\n }\n\n let last: ContentBlock[] | undefined;\n for (const line of readFileSync(file, 'utf8').split('\\n')) {\n if (!line.includes('\"assistant\"')) {\n continue;\n }\n let entry: TranscriptEntry;\n try {\n entry = JSON.parse(line) as TranscriptEntry;\n } catch {\n continue;\n }\n const blocks = textBlocks(entry);\n if (blocks) {\n last = blocks;\n }\n }\n return (last ?? []).map(b => b.text ?? '').join('\\n\\n');\n}\n","export type LogLevel = 'debug' | 'error' | 'info' | 'silent' | 'warn';\n\nconst LEVEL_RANK: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: Number.MAX_SAFE_INTEGER,\n};\n\nexport function levelEnabled(\n active: LogLevel,\n wanted: Exclude<LogLevel, 'silent'>,\n): boolean {\n return LEVEL_RANK[wanted] >= LEVEL_RANK[active];\n}\n\nconst LEVEL_LABEL: Record<Exclude<LogLevel, 'silent'>, string> = {\n debug: 'DEBUG',\n error: 'ERROR',\n info: 'INFO',\n warn: 'WARN',\n};\n\ntype Writer = (message: string) => void;\n\n// stdout carries the MCP transport; everything here goes to stderr.\nexport class Logger {\n private readonly stderr: Writer;\n\n constructor(\n readonly activeLevel: LogLevel,\n stderr: Writer = line => process.stderr.write(`${line}\\n`),\n ) {\n this.stderr = stderr;\n }\n\n debug(message: string): void {\n this.log('debug', message);\n }\n\n error(message: string): void {\n this.log('error', message);\n }\n\n info(message: string): void {\n this.log('info', message);\n }\n\n warn(message: string): void {\n this.log('warn', message);\n }\n\n private log(level: Exclude<LogLevel, 'silent'>, message: string): void {\n if (levelEnabled(this.activeLevel, level)) {\n this.stderr(`${LEVEL_LABEL[level]} ${message}`);\n }\n }\n}\n","/** Close each closable in order (best-effort), then force-exit. */\nexport function shutdownOnSignals(\n closables: readonly { close(): Promise<unknown> }[],\n signals: readonly NodeJS.Signals[] = ['SIGINT', 'SIGTERM'],\n): void {\n function shutdown(): void {\n void closables\n .reduce(\n (acc: Promise<void>, closable) =>\n acc.then(async () => {\n try {\n await closable.close();\n } catch {\n // best-effort; exiting regardless\n }\n return;\n }),\n Promise.resolve(),\n )\n .finally(() => {\n // Force-exit on signal; the n/no-process-exit rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(0);\n });\n }\n for (const signal of signals) {\n process.on(signal, shutdown);\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/text-format.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plain-text rendering primitives shared by the zai and tokens report
|
|
3
|
-
* renderers. Output targets a monospace terminal / hook-block `reason`, so
|
|
4
|
-
* everything here is fixed-width: padding, block-glyph bars, and compact
|
|
5
|
-
* number formatting.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export const MONTHS = [
|
|
9
|
-
'Jan',
|
|
10
|
-
'Feb',
|
|
11
|
-
'Mar',
|
|
12
|
-
'Apr',
|
|
13
|
-
'May',
|
|
14
|
-
'Jun',
|
|
15
|
-
'Jul',
|
|
16
|
-
'Aug',
|
|
17
|
-
'Sep',
|
|
18
|
-
'Oct',
|
|
19
|
-
'Nov',
|
|
20
|
-
'Dec',
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
24
|
-
|
|
25
|
-
export const RULE_WIDTH = 68;
|
|
26
|
-
|
|
27
|
-
export function rule(): string {
|
|
28
|
-
return '─'.repeat(RULE_WIDTH);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function pad2(n: number): string {
|
|
32
|
-
return String(n).padStart(2, '0');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Local-time `YYYY-MM-DD` key. */
|
|
36
|
-
export function ymd(date: Date): string {
|
|
37
|
-
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function fmtTokens(n: null | number | undefined): string {
|
|
41
|
-
if (n == null || Number.isNaN(n)) {
|
|
42
|
-
return '—';
|
|
43
|
-
}
|
|
44
|
-
if (n >= 1e9) {
|
|
45
|
-
return (n / 1e9).toFixed(1) + 'B';
|
|
46
|
-
}
|
|
47
|
-
if (n >= 1e6) {
|
|
48
|
-
return (n / 1e6).toFixed(1) + 'M';
|
|
49
|
-
}
|
|
50
|
-
if (n >= 1e3) {
|
|
51
|
-
return (n / 1e3).toFixed(1) + 'K';
|
|
52
|
-
}
|
|
53
|
-
return String(n);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function fmtNum(n: null | number | undefined): string {
|
|
57
|
-
return (n || 0).toLocaleString('en-US');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function padR(s: string, n: number): string {
|
|
61
|
-
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function padL(s: string, n: number): string {
|
|
65
|
-
return s.length >= n ? s : ' '.repeat(n - s.length) + s;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */
|
|
69
|
-
export function barField(v: number, max: number, width: number): string {
|
|
70
|
-
if (!v || v <= 0 || max <= 0) {
|
|
71
|
-
return ' '.repeat(width);
|
|
72
|
-
}
|
|
73
|
-
const scaled = (v / max) * width;
|
|
74
|
-
let full = Math.floor(scaled);
|
|
75
|
-
let fi = Math.round((scaled - full) * 8);
|
|
76
|
-
if (fi === 8) {
|
|
77
|
-
full += 1;
|
|
78
|
-
fi = 0;
|
|
79
|
-
}
|
|
80
|
-
if (full === 0 && fi === 0) {
|
|
81
|
-
fi = 1;
|
|
82
|
-
} // keep a sliver for any nonzero value
|
|
83
|
-
let s = '█'.repeat(Math.min(full, width));
|
|
84
|
-
if (full < width && fi > 0) {
|
|
85
|
-
s += EIGHTHS[fi];
|
|
86
|
-
}
|
|
87
|
-
if (s.length < width) {
|
|
88
|
-
s += ' '.repeat(width - s.length);
|
|
89
|
-
}
|
|
90
|
-
return s.slice(0, width);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Filled/empty meter: █ for used, ░ for remaining. */
|
|
94
|
-
export function meter(pct: number | undefined, width: number): string {
|
|
95
|
-
let filled = Math.round(((pct || 0) / 100) * width);
|
|
96
|
-
filled = Math.max(0, Math.min(width, filled));
|
|
97
|
-
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
98
|
-
}
|