@v1nvn/agentic-core 0.44.0 → 0.45.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 CHANGED
@@ -13,7 +13,7 @@ User-facing docs: [root README](../../README.md).
13
13
  | ------------------- | ----------------------------------------------------------------------- |
14
14
  | `src/last-reply.ts` | finds the previous assistant reply across local Claude Code transcripts |
15
15
  | `src/input.ts` | stdin and markdown-file input with the CLI error contracts |
16
- | `src/cli.ts` | quiet commander parsing, usage/exit, and the runMain failure tail |
16
+ | `src/cli.ts` | the runCli/parseCli doors over declarative command specs |
17
17
  | `src/logger.ts` | level-filtered stderr logger for the MCP servers |
18
18
  | `src/shutdown.ts` | close-then-exit signal handling for the server entrypoints |
19
19
  | `src/dev.ts` | the hot-reload watcher wiring the servers' `yarn dev` harnesses share |
package/package.json CHANGED
@@ -1,21 +1,15 @@
1
1
  {
2
2
  "name": "@v1nvn/agentic-core",
3
- "version": "0.44.0",
4
- "description": "Shared runtime for the @v1nvn agent tools — last-reply transcript reader.",
3
+ "version": "0.45.0",
4
+ "description": "Shared source home for the @v1nvn agent tools — runtime helpers and the drawing kit every process-side surface imports.",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
6
+ "main": "./src/index.ts",
7
7
  "exports": {
8
- ".": {
9
- "types": "./src/index.ts",
10
- "default": "./dist/index.js"
11
- },
12
- "./vite": {
13
- "types": "./src/vite.ts",
14
- "default": "./dist/vite.js"
15
- }
8
+ ".": "./src/index.ts",
9
+ "./draw": "./src/draw.ts",
10
+ "./vite": "./src/vite.ts"
16
11
  },
17
12
  "scripts": {
18
- "build": "vite build",
19
13
  "test": "vitest run",
20
14
  "test:watch": "vitest"
21
15
  },
@@ -35,11 +29,12 @@
35
29
  "directory": "packages/core"
36
30
  },
37
31
  "files": [
38
- "dist",
39
32
  "src"
40
33
  ],
34
+ "dependencies": {
35
+ "commander": "^15.0.0"
36
+ },
41
37
  "devDependencies": {
42
- "commander": "^15.0.0",
43
38
  "vite": "^8.1.4",
44
39
  "vitest": "^4.1.10"
45
40
  },
@@ -47,6 +42,6 @@
47
42
  "access": "public"
48
43
  },
49
44
  "engines": {
50
- "node": ">=22"
45
+ "node": ">=23.6"
51
46
  }
52
47
  }
package/src/cli.ts CHANGED
@@ -1,60 +1,274 @@
1
- import type { Command } from 'commander';
1
+ import { Command, InvalidArgumentError, Option } from 'commander';
2
2
 
3
- // A --help ask caught mid-parse: parseQuietly returns it instead of exiting,
4
- // so a spec can drive the parse and the bin decides to print and exit.
3
+ import { describeError } from './mcp.ts';
4
+
5
+ export interface CliOption {
6
+ readonly choices?: readonly string[];
7
+ readonly default?: string;
8
+ readonly description: string;
9
+ readonly env?: string;
10
+ readonly flags: string;
11
+ readonly parse?: (value: string) => unknown;
12
+ }
13
+
14
+ export type CliResult = number | undefined;
15
+
16
+ export type CliRun = (
17
+ options: Record<string, unknown>,
18
+ args: readonly string[],
19
+ ) => CliResult | Promise<CliResult>;
20
+
21
+ export interface CliSpec {
22
+ readonly argument?: { readonly description: string; readonly flags: string };
23
+ readonly commands?: readonly CliSpec[];
24
+ readonly description?: string;
25
+ readonly name: string;
26
+ readonly options?: readonly CliOption[];
27
+ readonly run?: CliRun;
28
+ readonly usageExit?: number;
29
+ readonly version?: string;
30
+ }
31
+
32
+ export interface CliInvocation {
33
+ readonly args: readonly string[];
34
+ readonly command?: string;
35
+ readonly options: Record<string, unknown>;
36
+ readonly root: Record<string, unknown>;
37
+ }
38
+
39
+ // A --help ask caught mid-parse: the parse doors return it instead of exiting,
40
+ // and runCli answers it (the help payload) on stdout with 0.
5
41
  export class HelpRequested extends Error {
6
- constructor(readonly command: Command) {
42
+ readonly command: Command;
43
+
44
+ constructor(command: Command) {
7
45
  super(`${command.name()} --help`);
46
+ this.command = command;
8
47
  }
9
48
  }
10
49
 
11
- export function parseQuietly(
12
- program: Command,
13
- args: readonly string[],
14
- ): Command | HelpRequested | undefined {
15
- function quiet(command: Command): void {
16
- command.configureOutput({
17
- writeOut: () => undefined,
18
- writeErr: () => undefined,
19
- });
20
- command.exitOverride(err => {
21
- const code = (err as { code?: string }).code;
22
- if (code === 'commander.help' || code === 'commander.helpDisplayed') {
23
- throw new HelpRequested(command);
50
+ export type CliHelpHook<T> = (topic: string, text: string) => T;
51
+
52
+ function addOption(cmd: Command, option: CliOption): void {
53
+ const bare =
54
+ option.env === undefined &&
55
+ option.choices === undefined &&
56
+ option.default === undefined &&
57
+ option.parse === undefined;
58
+ if (bare) {
59
+ cmd.option(option.flags, option.description);
60
+ return;
61
+ }
62
+ const built = new Option(option.flags, option.description);
63
+ if (option.env !== undefined) {
64
+ built.env(option.env);
65
+ }
66
+ if (option.choices !== undefined) {
67
+ built.choices(option.choices);
68
+ }
69
+ if (option.default !== undefined) {
70
+ built.default(option.default);
71
+ }
72
+ const { parse } = option;
73
+ if (parse !== undefined) {
74
+ // A plain Error from a parse fn skips commander's exitOverride, which is
75
+ // what records the subcommand for the usage print — wrap it so it rides.
76
+ built.argParser((value: string) => {
77
+ try {
78
+ return parse(value);
79
+ } catch (err) {
80
+ if ((err as { code?: string }).code === 'commander.invalidArgument') {
81
+ throw err;
82
+ }
83
+ throw new InvalidArgumentError(describeError(err));
24
84
  }
25
- throw err;
26
85
  });
27
- command.commands.forEach(quiet);
28
86
  }
87
+ cmd.addOption(built);
88
+ }
89
+
90
+ function wire(
91
+ cmd: Command,
92
+ spec: CliSpec,
93
+ program: Command,
94
+ isRoot: boolean,
95
+ sink?: (invocation: CliInvocation) => void,
96
+ ): void {
97
+ if (spec.description !== undefined) {
98
+ cmd.description(spec.description);
99
+ }
100
+ if (spec.argument !== undefined) {
101
+ cmd.argument(spec.argument.flags, spec.argument.description);
102
+ }
103
+ for (const option of spec.options ?? []) {
104
+ addOption(cmd, option);
105
+ }
106
+ if (isRoot && spec.version !== undefined) {
107
+ cmd.option('-V, --version', 'print the version and exit');
108
+ }
109
+ cmd.action((...all: unknown[]) => {
110
+ const options = all.at(-2) as Record<string, unknown>;
111
+ const command = all.at(-1) as Command;
112
+ sink?.({
113
+ command: isRoot ? undefined : spec.name,
114
+ options,
115
+ args: command.args,
116
+ root: program.opts(),
117
+ });
118
+ });
119
+ for (const sub of spec.commands ?? []) {
120
+ const child = new Command(sub.name);
121
+ wire(child, sub, program, false, sink);
122
+ cmd.addCommand(child);
123
+ }
124
+ }
125
+
126
+ function materialize(
127
+ spec: CliSpec,
128
+ sink?: (invocation: CliInvocation) => void,
129
+ ): Command {
130
+ const program = new Command(spec.name);
131
+ wire(program, spec, program, true, sink);
132
+ return program;
133
+ }
134
+
135
+ export function buildCli(spec: CliSpec): Command {
136
+ return materialize(spec);
137
+ }
138
+
139
+ function quiet(
140
+ command: Command,
141
+ onUsageError: (command: Command) => void,
142
+ ): void {
143
+ command.configureOutput({
144
+ writeOut: () => undefined,
145
+ writeErr: () => undefined,
146
+ });
147
+ command.exitOverride(err => {
148
+ const code = (err as { code?: string }).code;
149
+ if (code === 'commander.help' || code === 'commander.helpDisplayed') {
150
+ throw new HelpRequested(command);
151
+ }
152
+ onUsageError(command);
153
+ throw err;
154
+ });
155
+ command.commands.forEach(c => {
156
+ quiet(c, onUsageError);
157
+ });
158
+ }
159
+
160
+ export function parseCli<T = HelpRequested>(
161
+ spec: CliSpec,
162
+ argv: readonly string[],
163
+ onHelp?: CliHelpHook<T>,
164
+ onUsageError?: (command: Command, err: Error) => void,
165
+ ): CliInvocation | HelpRequested | T | undefined {
166
+ let invocation: CliInvocation | undefined;
167
+ const program = materialize(spec, found => {
168
+ invocation = found;
169
+ });
170
+ let errored: Command | undefined;
171
+ quiet(program, command => {
172
+ errored = command;
173
+ });
29
174
  try {
30
- quiet(program);
31
- program.parse([...args], { from: 'user' });
32
- return program;
175
+ program.parse([...argv], { from: 'user' });
33
176
  } catch (err) {
34
- return err instanceof HelpRequested ? err : undefined;
177
+ if (!(err instanceof HelpRequested)) {
178
+ if (errored !== undefined) {
179
+ const cause = err instanceof Error ? err : new Error(String(err));
180
+ onUsageError?.(errored, cause);
181
+ }
182
+ return undefined;
183
+ }
184
+ return onHelp !== undefined
185
+ ? onHelp(err.command.name(), err.command.helpInformation())
186
+ : err;
35
187
  }
188
+ return invocation;
36
189
  }
37
190
 
38
- function exitWithCode(code: number): never {
39
- // eslint-disable-next-line n/no-process-exit
40
- process.exit(code);
191
+ export function helpTextFor(spec: CliSpec, topic: string): string {
192
+ const program = materialize(spec);
193
+ const command =
194
+ topic === spec.name
195
+ ? program
196
+ : program.commands.find(c => c.name() === topic);
197
+ if (command === undefined) {
198
+ throw new Error(`no '${topic}' command to describe`);
199
+ }
200
+ return command.helpInformation();
41
201
  }
42
202
 
43
- export function printHelpAndExit(command: Command): never {
44
- console.log(command.helpInformation());
45
- exitWithCode(0);
203
+ function usageError(spec: CliSpec, topic?: string, message?: string): number {
204
+ if (message !== undefined) {
205
+ // Commander 15 builds its messages with the 'error: ' prefix already in.
206
+ process.stderr.write(`${message}\n`);
207
+ }
208
+ process.stderr.write(
209
+ `${helpTextFor(spec, topic === undefined ? spec.name : topic)}\n`,
210
+ );
211
+ return spec.usageExit ?? 1;
46
212
  }
47
213
 
48
- export function printUsageAndExit(program: Command): never {
49
- console.error(program.helpInformation());
50
- exitWithCode(1);
214
+ async function call(
215
+ run: CliRun,
216
+ options: Record<string, unknown>,
217
+ args: readonly string[],
218
+ ): Promise<number> {
219
+ try {
220
+ return (await run(options, args)) ?? 0;
221
+ } catch (err) {
222
+ console.error((err as Error).message);
223
+ return 1;
224
+ }
51
225
  }
52
226
 
53
- export async function runMain(main: () => Promise<void> | void): Promise<void> {
54
- try {
55
- await main();
56
- } catch (e) {
57
- console.error((e as Error).message);
58
- exitWithCode(1);
227
+ export async function runCli(
228
+ spec: CliSpec,
229
+ argv: readonly string[],
230
+ onHelp?: CliHelpHook<CliResult>,
231
+ ): Promise<number> {
232
+ let asked: undefined | { readonly text: string; readonly topic: string };
233
+ let usageTopic: string | undefined;
234
+ let usageMessage: string | undefined;
235
+ const parsed = parseCli(
236
+ spec,
237
+ argv,
238
+ (topic, text) => {
239
+ asked = { topic, text };
240
+ return 0;
241
+ },
242
+ (command, err) => {
243
+ usageTopic = command.name();
244
+ usageMessage = err.message;
245
+ },
246
+ );
247
+ if (parsed === undefined) {
248
+ return usageError(spec, usageTopic, usageMessage);
249
+ }
250
+ if (asked !== undefined) {
251
+ if (onHelp !== undefined) {
252
+ return onHelp(asked.topic, asked.text) ?? 0;
253
+ }
254
+ process.stdout.write(`${asked.text}\n`);
255
+ return 0;
256
+ }
257
+ const invocation = parsed as CliInvocation;
258
+ if (spec.version !== undefined && invocation.root.version === true) {
259
+ process.stdout.write(`${spec.version}\n`);
260
+ return 0;
261
+ }
262
+ if (invocation.command === undefined) {
263
+ if ((spec.commands?.length ?? 0) > 0) {
264
+ return usageError(spec);
265
+ }
266
+ return spec.run === undefined
267
+ ? 0
268
+ : await call(spec.run, invocation.options, invocation.args);
59
269
  }
270
+ const sub = spec.commands?.find(c => c.name === invocation.command);
271
+ return sub?.run === undefined
272
+ ? 0
273
+ : await call(sub.run, invocation.options, invocation.args);
60
274
  }
package/src/dev.ts CHANGED
@@ -1,11 +1,6 @@
1
- // Structural stand-ins for vite's dev server and module runner — the shared
2
- // hot-reload wiring below must not pull vite into core's runtime surface; the
3
- // MCP packages pass their real instances through.
4
- export interface ReloadableRunner {
5
- readonly evaluatedModules: { clear(): void };
6
- import<T>(entry: string): Promise<T>;
7
- }
8
-
1
+ // A structural stand-in for vite's dev server — the shared hot-reload wiring
2
+ // below must not pull vite into core's runtime surface; vite.ts passes the
3
+ // real instance through.
9
4
  export interface WatchableDevServer {
10
5
  close(): Promise<unknown>;
11
6
  readonly watcher: {
package/src/draw.ts ADDED
@@ -0,0 +1,332 @@
1
+ /**
2
+ * The drawing kit — the one home of the repo's terminal drawing model: ink
3
+ * kinds are a closed union, color values are data, surfaces render
4
+ * documents, and `toSgr` is the one skin, resetting each inked segment
5
+ * unless it carries its ink into the next. Loaded only by processes — the
6
+ * engine's island loader imports its own files by relative path alone.
7
+ */
8
+
9
+ export type InkKind =
10
+ | 'blue'
11
+ | 'bold'
12
+ | 'brightBlack'
13
+ | 'cyan'
14
+ | 'dim'
15
+ | 'green'
16
+ | 'magenta'
17
+ | 'mark'
18
+ | 'plain'
19
+ | 'red'
20
+ | 'white'
21
+ | 'yellow';
22
+
23
+ export type Ink =
24
+ | InkKind
25
+ | { readonly bg: Ink }
26
+ | { readonly indexed: number }
27
+ | { readonly rgb: readonly [number, number, number] };
28
+
29
+ export function rgb(r: number, g: number, b: number): Ink {
30
+ return { rgb: [r, g, b] };
31
+ }
32
+
33
+ export function indexed(n: number): Ink {
34
+ return { indexed: n };
35
+ }
36
+
37
+ export function bg(ink: Ink): Ink {
38
+ return { bg: ink };
39
+ }
40
+
41
+ export interface Segment {
42
+ readonly carry?: boolean;
43
+ readonly ink?: Ink;
44
+ readonly text: string;
45
+ }
46
+
47
+ declare const lineBrand: unique symbol;
48
+ declare const docBrand: unique symbol;
49
+
50
+ export type Line = readonly Segment[] & { readonly [lineBrand]: true };
51
+ export type Doc = readonly Line[] & { readonly [docBrand]: true };
52
+
53
+ export function line(...segments: Segment[]): Line {
54
+ return segments as unknown as Line;
55
+ }
56
+
57
+ export function doc(...rows: Line[]): Doc {
58
+ return rows as unknown as Doc;
59
+ }
60
+
61
+ export function plain(text: string): Segment {
62
+ return { text };
63
+ }
64
+
65
+ export function dim(text: string): Segment {
66
+ return { text, ink: 'dim' };
67
+ }
68
+
69
+ export function bold(text: string): Segment {
70
+ return { text, ink: 'bold' };
71
+ }
72
+
73
+ export function green(text: string): Segment {
74
+ return { text, ink: 'green' };
75
+ }
76
+
77
+ export function magenta(text: string): Segment {
78
+ return { text, ink: 'magenta' };
79
+ }
80
+
81
+ export function red(text: string): Segment {
82
+ return { text, ink: 'red' };
83
+ }
84
+
85
+ export function yellow(text: string): Segment {
86
+ return { text, ink: 'yellow' };
87
+ }
88
+
89
+ function assertNever(kind: never): never {
90
+ throw new Error(`unhandled Ink kind: ${String(kind)}`);
91
+ }
92
+
93
+ function paramsOf(ink: Ink): string {
94
+ if (typeof ink !== 'string') {
95
+ if ('bg' in ink) {
96
+ return paramsOf(ink.bg)
97
+ .replace(/^3(\d)/, '4$1')
98
+ .replace(/^9(\d)/, '10$1');
99
+ }
100
+ return 'indexed' in ink
101
+ ? `38;5;${ink.indexed}`
102
+ : `38;2;${ink.rgb[0]};${ink.rgb[1]};${ink.rgb[2]}`;
103
+ }
104
+ switch (ink) {
105
+ case 'blue':
106
+ return '34';
107
+ case 'bold':
108
+ return '1';
109
+ case 'brightBlack':
110
+ return '90';
111
+ case 'cyan':
112
+ return '36';
113
+ case 'dim':
114
+ return '2';
115
+ case 'green':
116
+ return '32';
117
+ case 'magenta':
118
+ return '35';
119
+ case 'mark':
120
+ return '1;97';
121
+ case 'plain':
122
+ return '39';
123
+ case 'red':
124
+ return '31';
125
+ case 'white':
126
+ return '37';
127
+ case 'yellow':
128
+ return '33';
129
+ default:
130
+ return assertNever(ink);
131
+ }
132
+ }
133
+
134
+ function rowsOf(rows: Doc | Line): readonly (readonly Segment[])[] {
135
+ return (rows as readonly unknown[]).every(r => Array.isArray(r))
136
+ ? (rows as Doc)
137
+ : [rows as Line];
138
+ }
139
+
140
+ export function toSgr(rows: Doc | Line): string {
141
+ return rowsOf(rows)
142
+ .map(row => {
143
+ let out = '';
144
+ let inkOpen = false;
145
+ for (const seg of row) {
146
+ if (seg.ink === undefined) {
147
+ out += (inkOpen ? '\x1b[0m' : '') + seg.text;
148
+ inkOpen = false;
149
+ continue;
150
+ }
151
+ out += `\x1b[${paramsOf(seg.ink)}m${seg.text}`;
152
+ if (seg.carry === true) {
153
+ inkOpen = true;
154
+ } else {
155
+ out += '\x1b[0m';
156
+ inkOpen = false;
157
+ }
158
+ }
159
+ return inkOpen ? `${out}\x1b[0m` : out;
160
+ })
161
+ .join('\n');
162
+ }
163
+
164
+ export function stripSgr(text: string): string {
165
+ // eslint-disable-next-line no-control-regex
166
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
167
+ }
168
+
169
+ export function textOf(rows: Doc | Line): string {
170
+ return rowsOf(rows)
171
+ .map(row => row.map(seg => seg.text).join(''))
172
+ .join('\n');
173
+ }
174
+
175
+ export interface ToolLineSource {
176
+ readonly deny?: string;
177
+ readonly isError?: boolean;
178
+ readonly result?: unknown;
179
+ readonly text?: string;
180
+ }
181
+
182
+ interface Stdio {
183
+ readonly stderr?: unknown;
184
+ readonly stdout?: unknown;
185
+ }
186
+
187
+ function stdioOf(result: unknown): Stdio {
188
+ return typeof result === 'object' && result !== null ? result : {};
189
+ }
190
+
191
+ export function stdoutOf(result: unknown): string {
192
+ const { stdout } = stdioOf(result);
193
+ return typeof stdout === 'string' ? stdout : '';
194
+ }
195
+
196
+ /** The one line an exec shows: the refusal's reason, stdout, else stderr or `fallback`. */
197
+ export function lineOf(call: ToolLineSource, fallback: string): string {
198
+ if (call.deny !== undefined) {
199
+ return call.deny;
200
+ }
201
+ if (call.isError === true) {
202
+ return call.text ?? fallback;
203
+ }
204
+ const out = stdoutOf(call.result).trim();
205
+ if (out !== '') {
206
+ return out;
207
+ }
208
+ const { stderr } = stdioOf(call.result);
209
+ const err = typeof stderr === 'string' ? stderr.trim() : '';
210
+ return err !== '' ? err : fallback;
211
+ }
212
+
213
+ export const RULE_WIDTH = 68;
214
+
215
+ export const BLOCK = '█';
216
+ export const TRACK = '·';
217
+ export const EIGHTHS: readonly (string | undefined)[] = [
218
+ '',
219
+ '▏',
220
+ '▎',
221
+ '▍',
222
+ '▌',
223
+ '▋',
224
+ '▊',
225
+ '▉',
226
+ ];
227
+
228
+ export function rule(width: number = RULE_WIDTH): string {
229
+ return '─'.repeat(Math.max(0, width));
230
+ }
231
+
232
+ export function pad(
233
+ text: string,
234
+ width: number,
235
+ side: 'left' | 'right',
236
+ ): string {
237
+ if (text.length >= width) {
238
+ return text;
239
+ }
240
+ const gap = ' '.repeat(width - text.length);
241
+ return side === 'left' ? gap + text : text + gap;
242
+ }
243
+
244
+ export function meter(
245
+ pct: null | number | undefined,
246
+ width: number,
247
+ filled: Ink = 'green',
248
+ ): Line {
249
+ const safePct = typeof pct === 'number' && Number.isFinite(pct) ? pct : 0;
250
+ const n = Math.max(0, Math.min(width, Math.round((safePct / 100) * width)));
251
+ return line(
252
+ { text: BLOCK.repeat(n), ink: filled },
253
+ { text: TRACK.repeat(width - n), ink: 'dim' },
254
+ );
255
+ }
256
+
257
+ /** Fixed-width bar field: full blocks, an eighth-fraction edge, spaces to width. */
258
+ export function bar(value: number, max: number, width: number): string {
259
+ if (!(value > 0) || !(max > 0)) {
260
+ return ' '.repeat(width);
261
+ }
262
+ const scaled = (value / max) * width;
263
+ let full = Math.floor(scaled);
264
+ let fi = Math.round((scaled - full) * 8);
265
+ if (fi === 8) {
266
+ full += 1;
267
+ fi = 0;
268
+ }
269
+ if (full === 0 && fi === 0) {
270
+ fi = 1;
271
+ }
272
+ let out = BLOCK.repeat(Math.min(full, width));
273
+ if (full < width && fi > 0) {
274
+ out += EIGHTHS[fi] ?? '';
275
+ }
276
+ return pad(out, width, 'right');
277
+ }
278
+
279
+ export function pad2(n: number): string {
280
+ return String(n).padStart(2, '0');
281
+ }
282
+
283
+ export const MONTHS = [
284
+ 'Jan',
285
+ 'Feb',
286
+ 'Mar',
287
+ 'Apr',
288
+ 'May',
289
+ 'Jun',
290
+ 'Jul',
291
+ 'Aug',
292
+ 'Sep',
293
+ 'Oct',
294
+ 'Nov',
295
+ 'Dec',
296
+ ] as const;
297
+
298
+ /** Compact count: 1.2K / 3.4M / 5.6B; '—' for absent or NaN. */
299
+ export function fmtTokens(n: null | number | undefined): string {
300
+ if (n == null || Number.isNaN(n)) {
301
+ return '—';
302
+ }
303
+ if (n >= 1e9) {
304
+ return (n / 1e9).toFixed(1) + 'B';
305
+ }
306
+ if (n >= 1e6) {
307
+ return (n / 1e6).toFixed(1) + 'M';
308
+ }
309
+ if (n >= 1e3) {
310
+ return (n / 1e3).toFixed(1) + 'K';
311
+ }
312
+ return String(n);
313
+ }
314
+
315
+ /** Grouped integer, en-US separators; 0 for absent. */
316
+ export function fmtNum(n: null | number | undefined): string {
317
+ return (n || 0).toLocaleString('en-US');
318
+ }
319
+
320
+ export type Clock = 'minute' | 'none' | 'second';
321
+
322
+ /** Local date-time stamp: `YYYY-MM-DD`, plus `HH:mm` and optionally seconds. */
323
+ export function localStamp(now: Date, clock: Clock): string {
324
+ const day = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
325
+ if (clock === 'none') {
326
+ return day;
327
+ }
328
+ const hhmm = `${pad2(now.getHours())}:${pad2(now.getMinutes())}`;
329
+ return clock === 'second'
330
+ ? `${day} ${hhmm}:${pad2(now.getSeconds())}`
331
+ : `${day} ${hhmm}`;
332
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
- export * from './cli.js';
2
- export * from './dev.js';
3
- export * from './input.js';
4
- export * from './last-reply.js';
5
- export * from './logger.js';
6
- export * from './shutdown.js';
1
+ export * from './cli.ts';
2
+ export * from './dev.ts';
3
+ export * from './draw.ts';
4
+ export * from './input.ts';
5
+ export * from './last-reply.ts';
6
+ export * from './logger.ts';
7
+ export * from './mcp.ts';
8
+ export * from './shutdown.ts';
package/src/logger.ts CHANGED
@@ -15,16 +15,36 @@ const LEVEL_LABEL: Record<Exclude<LogLevel, 'silent'>, string> = {
15
15
  warn: 'WARN',
16
16
  };
17
17
 
18
+ const VALID_LEVELS: readonly LogLevel[] = [
19
+ 'debug',
20
+ 'info',
21
+ 'warn',
22
+ 'error',
23
+ 'silent',
24
+ ];
25
+
26
+ export function loggerFromEnv(
27
+ env: NodeJS.ProcessEnv,
28
+ variable: string,
29
+ fallback: LogLevel = 'info',
30
+ ): Logger {
31
+ const raw = env[variable];
32
+ return new Logger(VALID_LEVELS.find(level => level === raw) ?? fallback);
33
+ }
34
+
18
35
  type Writer = (message: string) => void;
19
36
 
20
37
  // stdout carries the MCP transport; everything here goes to stderr.
21
38
  export class Logger {
39
+ readonly activeLevel: LogLevel;
40
+
22
41
  private readonly stderr: Writer;
23
42
 
24
43
  constructor(
25
- readonly activeLevel: LogLevel,
44
+ activeLevel: LogLevel,
26
45
  stderr: Writer = line => process.stderr.write(`${line}\n`),
27
46
  ) {
47
+ this.activeLevel = activeLevel;
28
48
  this.stderr = stderr;
29
49
  }
30
50
 
package/src/mcp.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { Logger } from './logger.ts';
2
+
3
+ export interface TextContent {
4
+ readonly text: string;
5
+ readonly type: 'text';
6
+ }
7
+
8
+ // The index signature mirrors the SDK's CallToolResult (zod-inferred, so it
9
+ // carries one); without it an interface would not assign to that type.
10
+ export interface ErrorResult {
11
+ [key: string]: unknown;
12
+ readonly content: TextContent[];
13
+ readonly isError: true;
14
+ }
15
+
16
+ export function describeError(err: unknown): string {
17
+ return err instanceof Error ? err.message : String(err);
18
+ }
19
+
20
+ export type ToolRun<Args, Result> = (args: Args) => Promise<Result> | Result;
21
+
22
+ /** A zod schema, named structurally so core stays zod-free. */
23
+ export interface ToolInputSchema<Args> {
24
+ parse(raw: unknown): Args;
25
+ }
26
+
27
+ /**
28
+ * The wrapper owns the error-drift class: a thrown or rejected error (zod
29
+ * parse included) logs and answers an `isError` result instead of escaping
30
+ * the tool call.
31
+ */
32
+ export function toolHandler<Result>(
33
+ log: Logger,
34
+ toErrorResult: (err: unknown) => Result,
35
+ ) {
36
+ return tool;
37
+
38
+ function tool<Args>(
39
+ name: string,
40
+ run: (args: Args) => Promise<Result>,
41
+ schema?: ToolInputSchema<Args>,
42
+ ): (raw?: unknown) => Promise<Result>;
43
+ function tool<Args>(
44
+ name: string,
45
+ run: (args: Args) => Result,
46
+ schema?: ToolInputSchema<Args>,
47
+ ): (raw?: unknown) => Result;
48
+ function tool<Args>(
49
+ name: string,
50
+ run: ToolRun<Args, Result>,
51
+ schema?: ToolInputSchema<Args>,
52
+ ): ToolRun<unknown, Result> {
53
+ function fail(err: unknown): Result {
54
+ log.error(`${name} failed: ${describeError(err)}`);
55
+ return toErrorResult(err);
56
+ }
57
+ return raw => {
58
+ try {
59
+ const args = schema === undefined ? (raw as Args) : schema.parse(raw);
60
+ const result = run(args);
61
+ return result instanceof Promise
62
+ ? result.then(undefined, fail)
63
+ : result;
64
+ } catch (err) {
65
+ return fail(err);
66
+ }
67
+ };
68
+ }
69
+ }
package/src/vite.ts CHANGED
@@ -1,71 +1,89 @@
1
- /**
2
- * The one two-mode build every mod home runs — a home's `vite.config.ts` is a
3
- * thin wrapper handing `modVite` its standalone mode name and extra entries.
4
- */
1
+ import { dirname, resolve } from 'node:path';
2
+ import {
3
+ createServerModuleRunner,
4
+ createServer as createViteServer,
5
+ } from 'vite';
5
6
  import { configDefaults } from 'vitest/config';
6
7
 
7
- export interface ModViteOptions {
8
- /** Extra CLI-pass entries beside the CLI itself, e.g. tokens' `text` library chunk. */
9
- entries?: Record<string, string>;
10
- /** The `--mode` that builds the standalone artifact, and the `bin/<name>.mjs` it emits. */
11
- standalone: { mode: string; name: string };
8
+ import type { Logger } from './logger.ts';
9
+
10
+ import { watchWithReload } from './dev.ts';
11
+ import { describeError } from './mcp.ts';
12
+ import { shutdownOnSignals } from './shutdown.ts';
13
+
14
+ export interface ServerViteOptions {
15
+ testTimeout?: number;
12
16
  }
13
17
 
14
- export function modVite(
15
- mode: string,
16
- { standalone, entries = {} }: ModViteOptions,
17
- ) {
18
- // The standalone pass emits the artifact the plugin folder carries — the
19
- // mod execs it through the session's Bash tool, where no node_modules
20
- // exists, so it inlines every dependency. Only ever run under `node`, where
21
- // the banner is inert.
22
- if (mode === standalone.mode) {
23
- return {
24
- ssr: { noExternal: true },
25
- build: {
26
- outDir: 'dist',
27
- emptyOutDir: false,
28
- sourcemap: true,
29
- minify: false,
30
- ssr: true,
31
- rollupOptions: {
32
- input: { [standalone.name]: 'src/index.ts' },
33
- output: {
34
- banner: '#!/usr/bin/env node',
35
- entryFileNames: `${standalone.name}.mjs`,
36
- },
37
- },
38
- },
39
- };
40
- }
41
- // The CLI pass builds the default way: external deps, installed beside it
42
- // by npx. SSR/Node build: Vite externalizes node built-ins AND package.json
43
- // `dependencies` (`@v1nvn/agentic-core` stays a real dep), so only source
44
- // is bundled; ESM out matches "type":"module".
18
+ export function serverVite({ testTimeout }: ServerViteOptions = {}) {
45
19
  return {
46
- build: {
47
- outDir: 'dist',
48
- emptyOutDir: true,
49
- sourcemap: true,
50
- minify: false,
51
- ssr: true,
52
- rollupOptions: {
53
- input: { index: 'src/index.ts', ...entries },
54
- // `bin` (dist/index.js) is exec'd by the kernel; without a shebang
55
- // the OS runs it under /bin/sh and `npx <bin>` dies parsing `import`.
56
- // Library chunks carry none.
57
- output: {
58
- banner: (chunk: { facadeModuleId?: string; isEntry: boolean }) =>
59
- chunk.isEntry && chunk.facadeModuleId?.endsWith('src/index.ts')
60
- ? '#!/usr/bin/env node'
61
- : '',
62
- },
63
- },
64
- },
20
+ // Vite 8 spins the HMR websocket from `server.ws`, not `server.hmr`
21
+ // (`hmr: false` stops messaging, not the socket): without this a second
22
+ // dev boot dies fighting over the default ws port 24678.
23
+ server: { ws: false },
65
24
  test: {
66
25
  globals: true,
67
26
  environment: 'node',
68
- exclude: [...configDefaults.exclude, 'dist/**', 'tests/**'],
27
+ ...(testTimeout === undefined ? {} : { testTimeout }),
28
+ exclude: [...configDefaults.exclude, 'dist/**'],
69
29
  },
70
30
  };
71
31
  }
32
+
33
+ export interface DevSession<T> {
34
+ closers: readonly { close(): Promise<unknown> }[];
35
+ reload(module: T): void;
36
+ }
37
+
38
+ /**
39
+ * The hot-reload harness every MCP-server dev entry runs. The transport and
40
+ * McpServer are single-use under the SDK (`StdioServerTransport.start()`
41
+ * throws on a second call), so `start` creates and connects them once and
42
+ * only the registrations swap on each file change. `start` must register
43
+ * before connecting — the SDK's `registerCapabilities` throws post-connect.
44
+ * `entry` is the module that exports the registration families, at
45
+ * `<package root>/src/<name>.ts`.
46
+ */
47
+ export async function runDevServer<T>(options: {
48
+ entry: string;
49
+ log: Logger;
50
+ start: (module: T) => DevSession<T> | Promise<DevSession<T>>;
51
+ }): Promise<void> {
52
+ const vite = await createViteServer({
53
+ appType: 'custom',
54
+ configFile: false,
55
+ // Vite logs `info` (e.g. "(ssr) page reload") via console.log to stdout,
56
+ // which corrupts the MCP stream; `warn` keeps errors on stderr.
57
+ logLevel: 'warn',
58
+ root: resolve(dirname(options.entry), '..'),
59
+ server: { middlewareMode: true, ws: false },
60
+ });
61
+
62
+ // hmr: false in a hot-reload harness looks wrong but is right: the
63
+ // watcher re-imports the whole entry rather than patching modules.
64
+ const runner = createServerModuleRunner(vite.environments.ssr, {
65
+ hmr: false,
66
+ });
67
+
68
+ const first = await runner.import<T>(options.entry);
69
+ const session = await options.start(first);
70
+
71
+ async function runOneReload(): Promise<void> {
72
+ try {
73
+ runner.evaluatedModules.clear();
74
+ // Import the next module before reload removes the live
75
+ // registrations: a failed import leaves what is serving intact.
76
+ const next = await runner.import<T>(options.entry);
77
+ session.reload(next);
78
+ options.log.info('[reload] success');
79
+ } catch (err) {
80
+ options.log.error(`[reload] failed: ${describeError(err)}`);
81
+ }
82
+ }
83
+
84
+ watchWithReload(vite, runOneReload);
85
+
86
+ options.log.info('[reload] ready');
87
+
88
+ shutdownOnSignals([...session.closers, vite]);
89
+ }
package/dist/index.js DELETED
@@ -1,214 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- //#region src/cli.ts
5
- var HelpRequested = class extends Error {
6
- command;
7
- constructor(command) {
8
- super(`${command.name()} --help`);
9
- this.command = command;
10
- }
11
- };
12
- function parseQuietly(program, args) {
13
- function quiet(command) {
14
- command.configureOutput({
15
- writeOut: () => void 0,
16
- writeErr: () => void 0
17
- });
18
- command.exitOverride((err) => {
19
- const code = err.code;
20
- if (code === "commander.help" || code === "commander.helpDisplayed") throw new HelpRequested(command);
21
- throw err;
22
- });
23
- command.commands.forEach(quiet);
24
- }
25
- try {
26
- quiet(program);
27
- program.parse([...args], { from: "user" });
28
- return program;
29
- } catch (err) {
30
- return err instanceof HelpRequested ? err : void 0;
31
- }
32
- }
33
- function exitWithCode(code) {
34
- process.exit(code);
35
- }
36
- function printHelpAndExit(command) {
37
- console.log(command.helpInformation());
38
- exitWithCode(0);
39
- }
40
- function printUsageAndExit(program) {
41
- console.error(program.helpInformation());
42
- exitWithCode(1);
43
- }
44
- async function runMain(main) {
45
- try {
46
- await main();
47
- } catch (e) {
48
- console.error(e.message);
49
- exitWithCode(1);
50
- }
51
- }
52
- //#endregion
53
- //#region src/dev.ts
54
- var RELOAD_DEBOUNCE_MS = 150;
55
- var IGNORED_SEGMENTS = [
56
- "/node_modules/",
57
- "/dist/",
58
- "/.git/",
59
- "/coverage/"
60
- ];
61
- function shouldReload(file) {
62
- return IGNORED_SEGMENTS.every((seg) => !file.includes(seg));
63
- }
64
- /**
65
- * Debounced file-change reloads, serialized via a promise chain: concurrent
66
- * saves never overlap, and a mid-reload save still produces a follow-up.
67
- */
68
- function watchWithReload(vite, reload) {
69
- let chain = Promise.resolve();
70
- let timer;
71
- vite.watcher.on("change", (file) => {
72
- if (!shouldReload(file)) return;
73
- if (timer) clearTimeout(timer);
74
- timer = setTimeout(() => {
75
- timer = void 0;
76
- chain = chain.then(reload);
77
- }, RELOAD_DEBOUNCE_MS);
78
- });
79
- }
80
- //#endregion
81
- //#region src/input.ts
82
- function readAll(stream) {
83
- return new Promise((resolve, reject) => {
84
- let data = "";
85
- stream.setEncoding("utf8");
86
- stream.on("data", (chunk) => {
87
- data += chunk;
88
- });
89
- stream.on("end", () => {
90
- resolve(data);
91
- });
92
- stream.on("error", reject);
93
- });
94
- }
95
- function readStdin(stream = process.stdin) {
96
- return readAll(stream);
97
- }
98
- /** A markdown file's contents, with the CLI's no-such-file error. */
99
- function readMarkdownFile(path) {
100
- if (!statSync(path, { throwIfNoEntry: false })?.isFile()) throw new Error(`no such file: ${path}`);
101
- return readFileSync(path, "utf8");
102
- }
103
- //#endregion
104
- //#region src/last-reply.ts
105
- function isFile(path) {
106
- try {
107
- return statSync(path).isFile();
108
- } catch {
109
- return false;
110
- }
111
- }
112
- function textBlocks(entry) {
113
- const content = entry.message?.content;
114
- if (entry.type !== "assistant" || !Array.isArray(content)) return;
115
- const blocks = content.filter((b) => typeof b === "object" && b !== null && b.type === "text");
116
- return blocks.length > 0 ? blocks : void 0;
117
- }
118
- function claudeProjectsDir() {
119
- const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), ".claude");
120
- return join(claudeDir, "projects");
121
- }
122
- /**
123
- * The last assistant text reply from a Claude Code session, reproducing Claude
124
- * Code's `/copy` byte-for-byte: the last assistant entry that contains a
125
- * `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks
126
- * joined with a blank line, and no trailing newline.
127
- *
128
- * @param arg a transcript file, a session UUID under the project dir, or
129
- * nothing to use the newest session for the current project.
130
- */
131
- function lastReply(arg) {
132
- const proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll("/", "-");
133
- const projDir = join(claudeProjectsDir(), proj);
134
- let file;
135
- if (arg !== void 0) file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);
136
- else if (existsSync(projDir)) {
137
- const newest = readdirSync(projDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
138
- f,
139
- mtimeMs: statSync(join(projDir, f)).mtimeMs
140
- })).sort((a, b) => b.mtimeMs - a.mtimeMs).at(0);
141
- if (newest) file = join(projDir, newest.f);
142
- }
143
- if (file === void 0 || !isFile(file)) throw new Error(`no session transcript found in ${projDir}`);
144
- let last;
145
- for (const line of readFileSync(file, "utf8").split("\n")) {
146
- if (!line.includes("\"assistant\"")) continue;
147
- let entry;
148
- try {
149
- entry = JSON.parse(line);
150
- } catch {
151
- continue;
152
- }
153
- const blocks = textBlocks(entry);
154
- if (blocks) last = blocks;
155
- }
156
- return (last ?? []).map((b) => b.text ?? "").join("\n\n");
157
- }
158
- //#endregion
159
- //#region src/logger.ts
160
- var LEVEL_RANK = {
161
- debug: 10,
162
- info: 20,
163
- warn: 30,
164
- error: 40,
165
- silent: Number.MAX_SAFE_INTEGER
166
- };
167
- var LEVEL_LABEL = {
168
- debug: "DEBUG",
169
- error: "ERROR",
170
- info: "INFO",
171
- warn: "WARN"
172
- };
173
- var Logger = class {
174
- activeLevel;
175
- stderr;
176
- constructor(activeLevel, stderr = (line) => process.stderr.write(`${line}\n`)) {
177
- this.activeLevel = activeLevel;
178
- this.stderr = stderr;
179
- }
180
- debug(message) {
181
- this.log("debug", message);
182
- }
183
- error(message) {
184
- this.log("error", message);
185
- }
186
- info(message) {
187
- this.log("info", message);
188
- }
189
- warn(message) {
190
- this.log("warn", message);
191
- }
192
- log(level, message) {
193
- if (LEVEL_RANK[level] >= LEVEL_RANK[this.activeLevel]) this.stderr(`${LEVEL_LABEL[level]} ${message}`);
194
- }
195
- };
196
- //#endregion
197
- //#region src/shutdown.ts
198
- /** Close each closable in order (best-effort), then force-exit. */
199
- function shutdownOnSignals(closables, signals = ["SIGINT", "SIGTERM"]) {
200
- function shutdown() {
201
- closables.reduce((acc, closable) => acc.then(async () => {
202
- try {
203
- await closable.close();
204
- } catch {}
205
- }), Promise.resolve()).finally(() => {
206
- process.exit(0);
207
- });
208
- }
209
- for (const signal of signals) process.on(signal, shutdown);
210
- }
211
- //#endregion
212
- export { HelpRequested, Logger, claudeProjectsDir, lastReply, parseQuietly, printHelpAndExit, printUsageAndExit, readMarkdownFile, readStdin, runMain, shutdownOnSignals, watchWithReload };
213
-
214
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/cli.ts","../src/dev.ts","../src/input.ts","../src/last-reply.ts","../src/logger.ts","../src/shutdown.ts"],"sourcesContent":["import type { Command } from 'commander';\n\n// A --help ask caught mid-parse: parseQuietly returns it instead of exiting,\n// so a spec can drive the parse and the bin decides to print and exit.\nexport class HelpRequested extends Error {\n constructor(readonly command: Command) {\n super(`${command.name()} --help`);\n }\n}\n\nexport function parseQuietly(\n program: Command,\n args: readonly string[],\n): Command | HelpRequested | undefined {\n function quiet(command: Command): void {\n command.configureOutput({\n writeOut: () => undefined,\n writeErr: () => undefined,\n });\n command.exitOverride(err => {\n const code = (err as { code?: string }).code;\n if (code === 'commander.help' || code === 'commander.helpDisplayed') {\n throw new HelpRequested(command);\n }\n throw err;\n });\n command.commands.forEach(quiet);\n }\n try {\n quiet(program);\n program.parse([...args], { from: 'user' });\n return program;\n } catch (err) {\n return err instanceof HelpRequested ? err : undefined;\n }\n}\n\nfunction exitWithCode(code: number): never {\n // eslint-disable-next-line n/no-process-exit\n process.exit(code);\n}\n\nexport function printHelpAndExit(command: Command): never {\n console.log(command.helpInformation());\n exitWithCode(0);\n}\n\nexport function printUsageAndExit(program: Command): never {\n console.error(program.helpInformation());\n exitWithCode(1);\n}\n\nexport async function runMain(main: () => Promise<void> | void): Promise<void> {\n try {\n await main();\n } catch (e) {\n console.error((e as Error).message);\n exitWithCode(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 { readFileSync, statSync } from 'node:fs';\n\nfunction 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 { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nfunction isFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\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\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 (LEVEL_RANK[level] >= LEVEL_RANK[this.activeLevel]) {\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/dist/vite.js DELETED
@@ -1,54 +0,0 @@
1
- import { configDefaults } from "vitest/config";
2
- //#region src/vite.ts
3
- /**
4
- * The one two-mode build every mod home runs — a home's `vite.config.ts` is a
5
- * thin wrapper handing `modVite` its standalone mode name and extra entries.
6
- */
7
- function modVite(mode, { standalone, entries = {} }) {
8
- if (mode === standalone.mode) return {
9
- ssr: { noExternal: true },
10
- build: {
11
- outDir: "dist",
12
- emptyOutDir: false,
13
- sourcemap: true,
14
- minify: false,
15
- ssr: true,
16
- rollupOptions: {
17
- input: { [standalone.name]: "src/index.ts" },
18
- output: {
19
- banner: "#!/usr/bin/env node",
20
- entryFileNames: `${standalone.name}.mjs`
21
- }
22
- }
23
- }
24
- };
25
- return {
26
- build: {
27
- outDir: "dist",
28
- emptyOutDir: true,
29
- sourcemap: true,
30
- minify: false,
31
- ssr: true,
32
- rollupOptions: {
33
- input: {
34
- index: "src/index.ts",
35
- ...entries
36
- },
37
- output: { banner: (chunk) => chunk.isEntry && chunk.facadeModuleId?.endsWith("src/index.ts") ? "#!/usr/bin/env node" : "" }
38
- }
39
- },
40
- test: {
41
- globals: true,
42
- environment: "node",
43
- exclude: [
44
- ...configDefaults.exclude,
45
- "dist/**",
46
- "tests/**"
47
- ]
48
- }
49
- };
50
- }
51
- //#endregion
52
- export { modVite };
53
-
54
- //# sourceMappingURL=vite.js.map
package/dist/vite.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"vite.js","names":[],"sources":["../src/vite.ts"],"sourcesContent":["/**\n * The one two-mode build every mod home runs — a home's `vite.config.ts` is a\n * thin wrapper handing `modVite` its standalone mode name and extra entries.\n */\nimport { configDefaults } from 'vitest/config';\n\nexport interface ModViteOptions {\n /** Extra CLI-pass entries beside the CLI itself, e.g. tokens' `text` library chunk. */\n entries?: Record<string, string>;\n /** The `--mode` that builds the standalone artifact, and the `bin/<name>.mjs` it emits. */\n standalone: { mode: string; name: string };\n}\n\nexport function modVite(\n mode: string,\n { standalone, entries = {} }: ModViteOptions,\n) {\n // The standalone pass emits the artifact the plugin folder carries — the\n // mod execs it through the session's Bash tool, where no node_modules\n // exists, so it inlines every dependency. Only ever run under `node`, where\n // the banner is inert.\n if (mode === standalone.mode) {\n return {\n ssr: { noExternal: true },\n build: {\n outDir: 'dist',\n emptyOutDir: false,\n sourcemap: true,\n minify: false,\n ssr: true,\n rollupOptions: {\n input: { [standalone.name]: 'src/index.ts' },\n output: {\n banner: '#!/usr/bin/env node',\n entryFileNames: `${standalone.name}.mjs`,\n },\n },\n },\n };\n }\n // The CLI pass builds the default way: external deps, installed beside it\n // by npx. SSR/Node build: Vite externalizes node built-ins AND package.json\n // `dependencies` (`@v1nvn/agentic-core` stays a real dep), so only source\n // is bundled; ESM out matches \"type\":\"module\".\n return {\n build: {\n outDir: 'dist',\n emptyOutDir: true,\n sourcemap: true,\n minify: false,\n ssr: true,\n rollupOptions: {\n input: { index: 'src/index.ts', ...entries },\n // `bin` (dist/index.js) is exec'd by the kernel; without a shebang\n // the OS runs it under /bin/sh and `npx <bin>` dies parsing `import`.\n // Library chunks carry none.\n output: {\n banner: (chunk: { facadeModuleId?: string; isEntry: boolean }) =>\n chunk.isEntry && chunk.facadeModuleId?.endsWith('src/index.ts')\n ? '#!/usr/bin/env node'\n : '',\n },\n },\n },\n test: {\n globals: true,\n environment: 'node',\n exclude: [...configDefaults.exclude, 'dist/**', 'tests/**'],\n },\n };\n}\n"],"mappings":";;;;;;AAaA,SAAgB,QACd,MACA,EAAE,YAAY,UAAU,CAAC,KACzB;CAKA,IAAI,SAAS,WAAW,MACtB,OAAO;EACL,KAAK,EAAE,YAAY,KAAK;EACxB,OAAO;GACL,QAAQ;GACR,aAAa;GACb,WAAW;GACX,QAAQ;GACR,KAAK;GACL,eAAe;IACb,OAAO,GAAG,WAAW,OAAO,eAAe;IAC3C,QAAQ;KACN,QAAQ;KACR,gBAAgB,GAAG,WAAW,KAAK;IACrC;GACF;EACF;CACF;CAMF,OAAO;EACL,OAAO;GACL,QAAQ;GACR,aAAa;GACb,WAAW;GACX,QAAQ;GACR,KAAK;GACL,eAAe;IACb,OAAO;KAAE,OAAO;KAAgB,GAAG;IAAQ;IAI3C,QAAQ,EACN,SAAS,UACP,MAAM,WAAW,MAAM,gBAAgB,SAAS,cAAc,IAC1D,wBACA,GACR;GACF;EACF;EACA,MAAM;GACJ,SAAS;GACT,aAAa;GACb,SAAS;IAAC,GAAG,eAAe;IAAS;IAAW;GAAU;EAC5D;CACF;AACF"}