poe-code 3.0.427 → 3.0.428

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.427",
3
+ "version": "3.0.428",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -196,6 +196,7 @@
196
196
  "ignore": "^5.3.2",
197
197
  "jose": "^6.1.3",
198
198
  "jsonc-parser": "^3.3.1",
199
+ "lru-cache": "^10.4.3",
199
200
  "mustache": "^4.2.0",
200
201
  "openai": "^6.34.0",
201
202
  "parse-duration": "^2.1.5",
@@ -1,3 +1,7 @@
1
1
  import type { NormalizedTrace } from "../../agent-traces/dist/index.js";
2
2
  import type { ContextBreakdown } from "./types.js";
3
- export declare function computeContextBreakdown(trace: NormalizedTrace): ContextBreakdown;
3
+ export interface ComputeContextBreakdownOptions {
4
+ signal?: AbortSignal;
5
+ mode?: "exact" | "estimated";
6
+ }
7
+ export declare function computeContextBreakdown(trace: NormalizedTrace, options?: ComputeContextBreakdownOptions): Promise<ContextBreakdown>;
@@ -1,4 +1,4 @@
1
- import { estimateTokens } from "tokenfill";
1
+ import { countTokens } from "tokenfill";
2
2
  const MATCHERS = [
3
3
  {
4
4
  id: "system-prompt",
@@ -44,15 +44,28 @@ const MATCHERS = [
44
44
  matches: () => true
45
45
  }
46
46
  ];
47
- export function computeContextBreakdown(trace) {
47
+ const TURNS_PER_YIELD = 256;
48
+ const ESTIMATOR_SAMPLE_TARGET_CHARS = 16_384;
49
+ const ESTIMATOR_SAMPLE_CHARS_PER_TURN = 1_024;
50
+ const ESTIMATOR_DEFAULT_CHARS_PER_TOKEN = 4;
51
+ export async function computeContextBreakdown(trace, options = {}) {
52
+ const mode = options.mode ?? "exact";
53
+ const estimate = mode === "exact" ? undefined : createTraceTokenEstimator(trace.turns);
48
54
  const categories = MATCHERS.map((matcher) => ({
49
55
  id: matcher.id,
50
56
  label: matcher.label,
51
57
  tokens: 0,
52
58
  items: new Map()
53
59
  }));
54
- for (const turn of trace.turns) {
55
- const tokens = estimateTokens(turn.text);
60
+ for (let index = 0; index < trace.turns.length; index += 1) {
61
+ if (mode === "exact" && index > 0 && index % TURNS_PER_YIELD === 0) {
62
+ await yieldToEventLoop();
63
+ if (options.signal?.aborted) {
64
+ break;
65
+ }
66
+ }
67
+ const turn = trace.turns[index];
68
+ const tokens = estimate === undefined ? await countTokensYielding(turn.text) : estimate(turn.text);
56
69
  const matcherIndex = MATCHERS.findIndex((matcher) => matcher.matches(turn));
57
70
  const matcher = MATCHERS[matcherIndex];
58
71
  const category = categories[matcherIndex];
@@ -85,6 +98,42 @@ export function computeContextBreakdown(trace) {
85
98
  }));
86
99
  return {
87
100
  measuredTokens,
88
- categories: outputCategories
101
+ categories: outputCategories,
102
+ source: mode
89
103
  };
90
104
  }
105
+ function createTraceTokenEstimator(turns) {
106
+ const sampleParts = [];
107
+ let sampleLength = 0;
108
+ for (const turn of turns) {
109
+ if (sampleLength >= ESTIMATOR_SAMPLE_TARGET_CHARS) {
110
+ break;
111
+ }
112
+ const part = turn.text.slice(0, Math.min(ESTIMATOR_SAMPLE_CHARS_PER_TURN, ESTIMATOR_SAMPLE_TARGET_CHARS - sampleLength));
113
+ sampleParts.push(part);
114
+ sampleLength += part.length;
115
+ }
116
+ const sample = sampleParts.join("");
117
+ const sampleTokens = sample.length === 0 ? 0 : countTokens(sample);
118
+ const charsPerToken = sampleTokens === 0 ? ESTIMATOR_DEFAULT_CHARS_PER_TOKEN : sample.length / sampleTokens;
119
+ return (text) => Math.round(text.length / charsPerToken);
120
+ }
121
+ const COUNT_CHUNK_CHARS = 512 * 1024;
122
+ async function countTokensYielding(text) {
123
+ if (text.length <= COUNT_CHUNK_CHARS) {
124
+ return countTokens(text);
125
+ }
126
+ let total = 0;
127
+ for (let index = 0; index < text.length; index += COUNT_CHUNK_CHARS) {
128
+ if (index > 0) {
129
+ await yieldToEventLoop();
130
+ }
131
+ total += countTokens(text.slice(index, index + COUNT_CHUNK_CHARS));
132
+ }
133
+ return total;
134
+ }
135
+ function yieldToEventLoop() {
136
+ return new Promise((resolve) => {
137
+ setImmediate(resolve);
138
+ });
139
+ }
@@ -5,4 +5,4 @@ export declare const CONTEXT_WINDOWS: {
5
5
  match: string;
6
6
  window: number;
7
7
  }[];
8
- export declare function computeContextUsage(trace: NormalizedTrace): ContextUsage;
8
+ export declare function computeContextUsage(trace: NormalizedTrace, measuredTokens: number): ContextUsage;
@@ -1,11 +1,8 @@
1
- import { estimateTokens } from "tokenfill";
2
1
  export const DEFAULT_CONTEXT_WINDOW = 200000;
3
2
  export const CONTEXT_WINDOWS = [{ match: "claude", window: 200000 }];
4
- export function computeContextUsage(trace) {
3
+ export function computeContextUsage(trace, measuredTokens) {
5
4
  const window = resolveContextWindow(trace);
6
- const tokens = trace.usage !== undefined
7
- ? trace.usage.contextTokens
8
- : trace.turns.reduce((sum, turn) => sum + estimateTokens(turn.text), 0);
5
+ const tokens = trace.usage !== undefined ? trace.usage.contextTokens : measuredTokens;
9
6
  return {
10
7
  tokens,
11
8
  window,
@@ -1,6 +1,7 @@
1
1
  import { traceReaders } from "@poe-code/agent-traces";
2
2
  import { computeContextBreakdown } from "./breakdown.js";
3
3
  import { computeContextUsage } from "./context.js";
4
+ import { defaultTraceTokenCacheDir, readCachedBreakdown, traceFileIdentity, writeCachedBreakdown } from "./token-cache.js";
4
5
  export async function listTraces(options) {
5
6
  const readers = options.sources === undefined
6
7
  ? traceReaders
@@ -29,13 +30,55 @@ export async function loadTrace(reference, options) {
29
30
  if (reader === undefined) {
30
31
  throw new Error(`No trace reader registered for source: ${reference.source}`);
31
32
  }
33
+ const filePath = reference.path;
34
+ const identity = filePath === undefined ? undefined : await traceFileIdentity(options.fs, filePath);
32
35
  const trace = await reader.read(reference, { fs: options.fs });
36
+ const breakdown = await loadBreakdown(trace, filePath, identity, options);
33
37
  return {
34
38
  ...trace,
35
- context: computeContextUsage(trace),
36
- breakdown: computeContextBreakdown(trace)
39
+ context: computeContextUsage(trace, breakdown.measuredTokens),
40
+ breakdown
37
41
  };
38
42
  }
43
+ const exactBreakdownsInFlight = new Map();
44
+ async function loadBreakdown(trace, filePath, identity, options) {
45
+ const cacheDir = options.cacheDir ?? defaultTraceTokenCacheDir();
46
+ if (filePath !== undefined && identity !== undefined) {
47
+ const cached = await readCachedBreakdown(options.fs, cacheDir, filePath, identity);
48
+ if (cached !== undefined) {
49
+ return cached;
50
+ }
51
+ }
52
+ if (options.deferExactTokens === true) {
53
+ const estimated = await computeContextBreakdown(trace, { mode: "estimated" });
54
+ if (filePath !== undefined && identity !== undefined && options.signal?.aborted !== true) {
55
+ scheduleExactBreakdown(trace, filePath, identity, cacheDir, options);
56
+ }
57
+ return estimated;
58
+ }
59
+ const breakdown = await computeContextBreakdown(trace, { signal: options.signal });
60
+ if (filePath !== undefined && identity !== undefined && options.signal?.aborted !== true) {
61
+ await writeCachedBreakdown(options.fs, cacheDir, filePath, identity, breakdown);
62
+ }
63
+ return breakdown;
64
+ }
65
+ function scheduleExactBreakdown(trace, filePath, identity, cacheDir, options) {
66
+ if (exactBreakdownsInFlight.has(filePath)) {
67
+ return;
68
+ }
69
+ const task = (async () => {
70
+ const breakdown = await computeContextBreakdown(trace);
71
+ await writeCachedBreakdown(options.fs, cacheDir, filePath, identity, breakdown);
72
+ options.onExactBreakdown?.(breakdown);
73
+ })()
74
+ .catch(() => {
75
+ // Exact counting is best-effort in the background; the estimate stays visible.
76
+ })
77
+ .finally(() => {
78
+ exactBreakdownsInFlight.delete(filePath);
79
+ });
80
+ exactBreakdownsInFlight.set(filePath, task);
81
+ }
39
82
  export async function loadSubagentSummaries(view, options) {
40
83
  if (view.children === undefined || view.children.length === 0) {
41
84
  return [];
@@ -7,4 +7,7 @@ export declare function renderTraceLine(item: TraceReference): {
7
7
  meta: string;
8
8
  };
9
9
  export declare function renderSubagents(summaries: SubagentSummary[]): string;
10
- export declare function renderTraceDetail(view: TraceView, subagents?: SubagentSummary[]): string;
10
+ export interface RenderTraceDetailOptions {
11
+ signal?: AbortSignal;
12
+ }
13
+ export declare function renderTraceDetail(view: TraceView, subagents?: SubagentSummary[], options?: RenderTraceDetailOptions): Promise<string>;
@@ -8,7 +8,7 @@ const MAX_TRACE_META_CWD_WIDTH = 18;
8
8
  const MAX_SUBAGENT_AGENT_WIDTH = 13;
9
9
  const MAX_SUBAGENT_DESCRIPTION_WIDTH = 26;
10
10
  const COLLAPSED_TURN_LINES = 3;
11
- const MAX_CONVERSATION_LINES = 48;
11
+ const TURNS_PER_RENDER_YIELD = 256;
12
12
  const CATEGORY_ORDER = [
13
13
  "system-prompt",
14
14
  "skills",
@@ -58,11 +58,14 @@ export function renderBreakdown(breakdown, width = 32) {
58
58
  return "Context breakdown\n No context tokens measured";
59
59
  }
60
60
  const theme = getTheme();
61
+ const title = breakdown.source === "estimated"
62
+ ? `Context breakdown ${theme.muted("(counting exact tokens…)")}`
63
+ : "Context breakdown";
61
64
  const bar = renderSegmentedBar(categories, normalizeGaugeWidth(width), theme);
62
65
  const labelWidth = Math.max(10, ...categories.map((category) => category.label.length));
63
66
  const tokenWidth = Math.max(1, ...categories.map((category) => formatCount(category.tokens).length));
64
67
  const percentWidth = Math.max(3, ...categories.map((category) => `${category.percent}%`.length));
65
- const lines = ["Context breakdown", ` ${bar}`];
68
+ const lines = [title, ` ${bar}`];
66
69
  for (const category of categories) {
67
70
  const styler = categoryColor(category.id, theme);
68
71
  lines.push([
@@ -130,7 +133,7 @@ export function renderSubagents(summaries) {
130
133
  }
131
134
  return lines.join("\n");
132
135
  }
133
- export function renderTraceDetail(view, subagents = []) {
136
+ export async function renderTraceDetail(view, subagents = [], options = {}) {
134
137
  const theme = getTheme();
135
138
  const lines = [
136
139
  theme.header(truncate(sanitizeInline(view.title?.trim() || view.id), MAX_RENDER_WIDTH)),
@@ -152,26 +155,24 @@ export function renderTraceDetail(view, subagents = []) {
152
155
  lines.push("", theme.header("Conversation"));
153
156
  if (view.turns.length === 0) {
154
157
  lines.push(` ${theme.muted("No turns")}`);
158
+ return lines.join("\n");
155
159
  }
156
- else {
157
- const conversationLines = [];
158
- let renderedTurns = 0;
159
- for (const turn of view.turns) {
160
- if (conversationLines.length >= MAX_CONVERSATION_LINES) {
160
+ for (let index = 0; index < view.turns.length; index += 1) {
161
+ if (index > 0 && index % TURNS_PER_RENDER_YIELD === 0) {
162
+ await yieldToEventLoop();
163
+ if (options.signal?.aborted) {
161
164
  break;
162
165
  }
163
- conversationLines.push(...renderTurn(turn, theme));
164
- renderedTurns += 1;
165
- }
166
- lines.push(...conversationLines.slice(0, MAX_CONVERSATION_LINES));
167
- const truncatedLastTurn = conversationLines.length > MAX_CONVERSATION_LINES;
168
- const remaining = view.turns.length - renderedTurns + (truncatedLastTurn ? 1 : 0);
169
- if (remaining > 0) {
170
- lines.push(` ${theme.muted(`… ${remaining} more turns`)}`);
171
166
  }
167
+ lines.push(...renderTurn(view.turns[index], theme));
172
168
  }
173
169
  return lines.join("\n");
174
170
  }
171
+ function yieldToEventLoop() {
172
+ return new Promise((resolve) => {
173
+ setImmediate(resolve);
174
+ });
175
+ }
175
176
  function renderBreakdownItems(items, minimumTokenWidth, theme) {
176
177
  const shown = items.slice(0, 5);
177
178
  const labels = shown.map((item) => truncate(item.name, 32));
@@ -247,14 +248,23 @@ function renderCompactContextGauge(context) {
247
248
  function renderTurn(turn, theme) {
248
249
  const renderer = ROLE_RENDERING[turn.role];
249
250
  const role = renderer.color(theme)(`${turn.role} ${renderer.glyph}`);
250
- const sanitizedText = sanitizeTraceText(turn.text);
251
+ const shouldCollapse = turn.role === "tool" || turn.role === "system";
252
+ let sourceText = turn.text;
253
+ let collapsedLines = 0;
254
+ if (shouldCollapse) {
255
+ const cut = lineEndIndex(sourceText, COLLAPSED_TURN_LINES);
256
+ if (cut !== -1) {
257
+ collapsedLines = countNewlines(sourceText, cut);
258
+ sourceText = sourceText.slice(0, cut);
259
+ }
260
+ }
261
+ const sanitizedText = sanitizeTraceText(sourceText);
251
262
  const text = turn.role === "assistant"
252
263
  ? stripAnsi(renderMarkdown(sanitizedText)).trimEnd()
253
264
  : sanitizedText.trimEnd();
254
265
  const rawLines = text.length === 0 ? [""] : text.split("\n");
255
- const shouldCollapse = turn.role === "tool" || turn.role === "system";
256
266
  const visibleLines = shouldCollapse ? rawLines.slice(0, COLLAPSED_TURN_LINES) : rawLines;
257
- const remaining = rawLines.length - visibleLines.length;
267
+ const remaining = collapsedLines + rawLines.length - visibleLines.length;
258
268
  const prefix = ` ${role} `;
259
269
  const continuationPrefix = " ".repeat(plainLength(` ${turn.role} ${renderer.glyph} `));
260
270
  const lines = [];
@@ -348,7 +358,41 @@ function padVisible(value, width) {
348
358
  function sanitizeInline(value) {
349
359
  return sanitizeTraceText(value).split("\n").join(" ");
350
360
  }
361
+ function lineEndIndex(value, lineCount) {
362
+ let index = -1;
363
+ for (let line = 0; line < lineCount; line += 1) {
364
+ index = value.indexOf("\n", index + 1);
365
+ if (index === -1) {
366
+ return -1;
367
+ }
368
+ }
369
+ return index;
370
+ }
371
+ function countNewlines(value, from) {
372
+ let count = 0;
373
+ for (let index = from; index < value.length; index += 1) {
374
+ if (value.charCodeAt(index) === 10) {
375
+ count += 1;
376
+ }
377
+ }
378
+ return count;
379
+ }
380
+ function needsSanitize(value) {
381
+ for (let index = 0; index < value.length; index += 1) {
382
+ const code = value.charCodeAt(index);
383
+ if ((code < 32 && code !== 10) || code === 127) {
384
+ return true;
385
+ }
386
+ }
387
+ return false;
388
+ }
351
389
  function sanitizeTraceText(value) {
390
+ if (!needsSanitize(value)) {
391
+ return value;
392
+ }
393
+ return sanitizeTraceTextSlow(value);
394
+ }
395
+ function sanitizeTraceTextSlow(value) {
352
396
  let result = "";
353
397
  let index = 0;
354
398
  while (index < value.length) {
@@ -12,7 +12,7 @@ export async function runTraceViewer(options) {
12
12
  writeLine(output, JSON.stringify({ ...view, subagents }, null, 2));
13
13
  return;
14
14
  }
15
- writeLine(output, renderTraceDetail(view, subagents));
15
+ writeLine(output, await renderTraceDetail(view, subagents));
16
16
  return;
17
17
  }
18
18
  const discover = () => listTraces({
@@ -115,12 +115,17 @@ function buildTraceExplorerConfig(options) {
115
115
  ctx.openModal({ title: ctx.row.title, content: cached });
116
116
  return;
117
117
  }
118
- const view = await loadTrace(reference, { fs: options.fs });
118
+ const rowId = ctx.row.id;
119
+ const view = await loadTrace(reference, {
120
+ fs: options.fs,
121
+ deferExactTokens: true,
122
+ onExactBreakdown: () => state.detailByRowId.delete(rowId)
123
+ });
119
124
  const subagents = await loadSubagentSummariesIfPresent(view, options.fs);
120
125
  const loaded = { view, subagents };
121
- const content = renderTraceDetail(loaded.view, loaded.subagents);
122
- state.detailByRowId.set(ctx.row.id, content);
123
- state.childrenByRowId.set(ctx.row.id, loaded.view.children ?? []);
126
+ const content = await renderTraceDetail(loaded.view, loaded.subagents);
127
+ state.detailByRowId.set(rowId, content);
128
+ state.childrenByRowId.set(rowId, loaded.view.children ?? []);
124
129
  ctx.openModal({ title: ctx.row.title, content });
125
130
  }
126
131
  },
@@ -177,8 +182,22 @@ function buildTraceExplorerConfig(options) {
177
182
  detail: {
178
183
  items: async (row, ctx) => {
179
184
  const reference = getReference(state.referenceByRowId, row.id);
185
+ const cached = state.detailByRowId.get(row.id);
186
+ if (cached !== undefined) {
187
+ return [
188
+ {
189
+ id: row.id,
190
+ render: () => cached
191
+ }
192
+ ];
193
+ }
180
194
  const loaded = await loadUnlessAborted(ctx.signal, async () => {
181
- const view = await loadTrace(reference, { fs: options.fs });
195
+ const view = await loadTrace(reference, {
196
+ fs: options.fs,
197
+ signal: ctx.signal,
198
+ deferExactTokens: true,
199
+ onExactBreakdown: () => state.detailByRowId.delete(row.id)
200
+ });
182
201
  const subagents = await loadSubagentSummariesIfPresent(view, options.fs);
183
202
  return { view, subagents };
184
203
  });
@@ -186,7 +205,12 @@ function buildTraceExplorerConfig(options) {
186
205
  return [];
187
206
  }
188
207
  state.childrenByRowId.set(row.id, loaded.view.children ?? []);
189
- const content = renderTraceDetail(loaded.view, loaded.subagents);
208
+ const content = await renderTraceDetail(loaded.view, loaded.subagents, {
209
+ signal: ctx.signal
210
+ });
211
+ if (ctx.signal.aborted) {
212
+ return [];
213
+ }
190
214
  state.detailByRowId.set(row.id, content);
191
215
  return [
192
216
  {
@@ -0,0 +1,10 @@
1
+ import type { AgentTraceFileSystem } from "../../agent-traces/dist/index.js";
2
+ import type { ContextBreakdown } from "./types.js";
3
+ export interface TraceFileIdentity {
4
+ mtimeMs: number;
5
+ size: number;
6
+ }
7
+ export declare function defaultTraceTokenCacheDir(): string;
8
+ export declare function traceFileIdentity(fs: AgentTraceFileSystem, filePath: string): Promise<TraceFileIdentity | undefined>;
9
+ export declare function readCachedBreakdown(fs: AgentTraceFileSystem, cacheDir: string, filePath: string, identity: TraceFileIdentity): Promise<ContextBreakdown | undefined>;
10
+ export declare function writeCachedBreakdown(fs: AgentTraceFileSystem, cacheDir: string, filePath: string, identity: TraceFileIdentity, breakdown: ContextBreakdown): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { resolveCacheDir } from "@poe-code/cached-resource";
4
+ const CACHE_VERSION = 2;
5
+ export function defaultTraceTokenCacheDir() {
6
+ return path.join(resolveCacheDir("poe-code"), "trace-tokens");
7
+ }
8
+ export async function traceFileIdentity(fs, filePath) {
9
+ try {
10
+ const stats = await fs.stat(filePath);
11
+ const mtimeMs = stats.mtime instanceof Date ? stats.mtime.getTime() : undefined;
12
+ if (mtimeMs === undefined || Number.isNaN(mtimeMs) || typeof stats.size !== "number") {
13
+ return undefined;
14
+ }
15
+ return { mtimeMs, size: stats.size };
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ export async function readCachedBreakdown(fs, cacheDir, filePath, identity) {
22
+ let entry;
23
+ try {
24
+ entry = JSON.parse(await fs.readFile(cacheFilePath(cacheDir, filePath), "utf8"));
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ if (entry.version !== CACHE_VERSION ||
30
+ entry.mtimeMs !== identity.mtimeMs ||
31
+ entry.size !== identity.size ||
32
+ typeof entry.breakdown?.measuredTokens !== "number" ||
33
+ entry.breakdown.source !== "exact") {
34
+ return undefined;
35
+ }
36
+ return entry.breakdown;
37
+ }
38
+ export async function writeCachedBreakdown(fs, cacheDir, filePath, identity, breakdown) {
39
+ const entry = {
40
+ version: CACHE_VERSION,
41
+ mtimeMs: identity.mtimeMs,
42
+ size: identity.size,
43
+ breakdown
44
+ };
45
+ try {
46
+ await fs.mkdir(cacheDir, { recursive: true });
47
+ await fs.writeFile(cacheFilePath(cacheDir, filePath), JSON.stringify(entry));
48
+ }
49
+ catch {
50
+ // Token caching is best-effort; recomputing on the next view is always safe.
51
+ }
52
+ }
53
+ function cacheFilePath(cacheDir, filePath) {
54
+ const hash = createHash("sha256").update(filePath).digest("hex").slice(0, 32);
55
+ return path.join(cacheDir, `${hash}.json`);
56
+ }
@@ -11,6 +11,10 @@ export interface ListTracesOptions {
11
11
  }
12
12
  export interface LoadTraceOptions {
13
13
  fs: AgentTraceFileSystem;
14
+ signal?: AbortSignal;
15
+ cacheDir?: string;
16
+ deferExactTokens?: boolean;
17
+ onExactBreakdown?: (breakdown: ContextBreakdown) => void;
14
18
  }
15
19
  export interface ContextUsage {
16
20
  tokens: number;
@@ -33,6 +37,7 @@ export interface ContextBreakdownCategory {
33
37
  export interface ContextBreakdown {
34
38
  measuredTokens: number;
35
39
  categories: ContextBreakdownCategory[];
40
+ source: "exact" | "estimated";
36
41
  }
37
42
  export type TraceView = NormalizedTrace & {
38
43
  context: ContextUsage;
@@ -12,6 +12,7 @@ export interface AgentTraceFileSystem {
12
12
  isFile(): boolean;
13
13
  isDirectory(): boolean;
14
14
  mtime?: Date;
15
+ size?: number;
15
16
  }>;
16
17
  }
17
18
  export interface SqliteTraceDatabase {
@@ -7,11 +7,10 @@ import { loadOAuthVerifier } from "./load-oauth-verifier.js";
7
7
  function readPackageInfo() {
8
8
  try {
9
9
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
10
- if (typeof packageJson.name === "string" &&
11
- typeof packageJson.version === "string") {
10
+ if (typeof packageJson.name === "string" && typeof packageJson.version === "string") {
12
11
  return {
13
12
  name: packageJson.name,
14
- version: packageJson.version,
13
+ version: packageJson.version
15
14
  };
16
15
  }
17
16
  }
@@ -20,7 +19,7 @@ function readPackageInfo() {
20
19
  }
21
20
  return {
22
21
  name: "tiny-http-mcp-server",
23
- version: "0.0.0",
22
+ version: "0.0.0"
24
23
  };
25
24
  }
26
25
  const packageInfo = readPackageInfo();
@@ -45,6 +44,8 @@ const HELP_TEXT = [
45
44
  " Maximum concurrent GET SSE streams per session",
46
45
  " --max-sse-event-history <count>",
47
46
  " Number of SSE events retained for Last-Event-ID replay",
47
+ " --sse-keep-alive-ms <ms>",
48
+ " GET SSE keepalive interval (default: 30000; 0 disables)",
48
49
  " --max-concurrent-tool-calls <count>",
49
50
  " Maximum concurrent tool calls across sessions",
50
51
  " --trusted-proxy Trust X-Forwarded-Proto and X-Forwarded-Host",
@@ -67,7 +68,7 @@ const HELP_TEXT = [
67
68
  " Module that exports the TokenVerifier implementation",
68
69
  " --oauth-verifier-export <name>",
69
70
  " Export name to load from the verifier module (default: default)",
70
- " -h, --help Show this help message",
71
+ " -h, --help Show this help message"
71
72
  ].join("\n");
72
73
  function parsePort(value) {
73
74
  if (value === undefined) {
@@ -126,7 +127,7 @@ function hasConfiguredOAuthFlag(values) {
126
127
  values["oauth-required-scope"],
127
128
  values["oauth-bearer-method"],
128
129
  values["oauth-verifier-module"],
129
- values["oauth-verifier-export"],
130
+ values["oauth-verifier-export"]
130
131
  ].some((value) => value !== undefined);
131
132
  }
132
133
  function parseRepeatableStrings(value, flagName) {
@@ -174,9 +175,7 @@ function parseCliOAuthOptions(values) {
174
175
  ...(supportedScopes === undefined ? {} : { scopesSupported: supportedScopes }),
175
176
  ...(bearerMethods === undefined ? {} : { bearerMethodsSupported: bearerMethods }),
176
177
  verifierModule,
177
- verifierExport: typeof verifierExport === "string" && verifierExport.length > 0
178
- ? verifierExport
179
- : "default",
178
+ verifierExport: typeof verifierExport === "string" && verifierExport.length > 0 ? verifierExport : "default"
180
179
  };
181
180
  }
182
181
  function parseCliOptions(args) {
@@ -198,6 +197,7 @@ function parseCliOptions(args) {
198
197
  "session-ttl-ms": { type: "string" },
199
198
  "max-streams-per-session": { type: "string" },
200
199
  "max-sse-event-history": { type: "string" },
200
+ "sse-keep-alive-ms": { type: "string" },
201
201
  "max-concurrent-tool-calls": { type: "string" },
202
202
  "trusted-proxy": { type: "boolean" },
203
203
  "request-timeout-ms": { type: "string" },
@@ -210,8 +210,8 @@ function parseCliOptions(args) {
210
210
  "oauth-bearer-method": { type: "string", multiple: true },
211
211
  "oauth-verifier-module": { type: "string" },
212
212
  "oauth-verifier-export": { type: "string" },
213
- help: { type: "boolean", short: "h" },
214
- },
213
+ help: { type: "boolean", short: "h" }
214
+ }
215
215
  });
216
216
  const maxRequestBytes = parseOptionalInteger(values["max-request-bytes"], "--max-request-bytes", 1);
217
217
  const maxBatchSize = parseOptionalInteger(values["max-batch-size"], "--max-batch-size", 1);
@@ -219,6 +219,7 @@ function parseCliOptions(args) {
219
219
  const sessionTtlMs = parseOptionalInteger(values["session-ttl-ms"], "--session-ttl-ms", 1);
220
220
  const maxStreamsPerSession = parseOptionalInteger(values["max-streams-per-session"], "--max-streams-per-session", 1);
221
221
  const maxSseEventHistory = parseOptionalInteger(values["max-sse-event-history"], "--max-sse-event-history", 0);
222
+ const sseKeepAliveMs = parseOptionalInteger(values["sse-keep-alive-ms"], "--sse-keep-alive-ms", 0);
222
223
  const maxConcurrentToolCalls = parseOptionalInteger(values["max-concurrent-tool-calls"], "--max-concurrent-tool-calls", 1);
223
224
  const requestTimeoutMs = parseOptionalInteger(values["request-timeout-ms"], "--request-timeout-ms", 0);
224
225
  const headersTimeoutMs = parseOptionalInteger(values["headers-timeout-ms"], "--headers-timeout-ms", 0);
@@ -235,7 +236,7 @@ function parseCliOptions(args) {
235
236
  : {}),
236
237
  ...(Array.isArray(values["allowed-origin"]) && values["allowed-origin"].length > 0
237
238
  ? {
238
- allowedOrigins: values["allowed-origin"].map((value) => parseOrigin(value, "--allowed-origin")),
239
+ allowedOrigins: values["allowed-origin"].map((value) => parseOrigin(value, "--allowed-origin"))
239
240
  }
240
241
  : {}),
241
242
  ...(maxRequestBytes === undefined ? {} : { maxRequestBytes }),
@@ -244,12 +245,13 @@ function parseCliOptions(args) {
244
245
  ...(sessionTtlMs === undefined ? {} : { sessionTtlMs }),
245
246
  ...(maxStreamsPerSession === undefined ? {} : { maxStreamsPerSession }),
246
247
  ...(maxSseEventHistory === undefined ? {} : { maxSseEventHistory }),
248
+ ...(sseKeepAliveMs === undefined ? {} : { sseKeepAliveMs }),
247
249
  ...(maxConcurrentToolCalls === undefined ? {} : { maxConcurrentToolCalls }),
248
250
  trustedProxy: values["trusted-proxy"] ?? false,
249
251
  ...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
250
252
  ...(headersTimeoutMs === undefined ? {} : { headersTimeoutMs }),
251
253
  ...(keepAliveTimeoutMs === undefined ? {} : { keepAliveTimeoutMs }),
252
- oauth: parseCliOAuthOptions(values),
254
+ oauth: parseCliOAuthOptions(values)
253
255
  };
254
256
  }
255
257
  function waitForShutdown(shutdown) {
@@ -306,8 +308,8 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
306
308
  : {}),
307
309
  verifier: await loadVerifier({
308
310
  modulePath: options.oauth.verifierModule,
309
- exportName: options.oauth.verifierExport,
310
- }),
311
+ exportName: options.oauth.verifierExport
312
+ })
311
313
  };
312
314
  const server = createServer({
313
315
  name: packageInfo.name,
@@ -316,7 +318,9 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
316
318
  ...(options.jsonResponse ? { enableJsonResponse: true } : {}),
317
319
  ...(options.allowedHosts === undefined ? {} : { allowedHosts: options.allowedHosts }),
318
320
  ...(options.allowedOrigins === undefined ? {} : { allowedOrigins: options.allowedOrigins }),
319
- ...(options.maxRequestBytes === undefined ? {} : { maxRequestBytes: options.maxRequestBytes }),
321
+ ...(options.maxRequestBytes === undefined
322
+ ? {}
323
+ : { maxRequestBytes: options.maxRequestBytes }),
320
324
  ...(options.maxBatchSize === undefined ? {} : { maxBatchSize: options.maxBatchSize }),
321
325
  ...(options.maxSessions === undefined ? {} : { maxSessions: options.maxSessions }),
322
326
  ...(options.sessionTtlMs === undefined ? {} : { sessionTtlMs: options.sessionTtlMs }),
@@ -326,11 +330,12 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
326
330
  ...(options.maxSseEventHistory === undefined
327
331
  ? {}
328
332
  : { maxSseEventHistory: options.maxSseEventHistory }),
333
+ ...(options.sseKeepAliveMs === undefined ? {} : { sseKeepAliveMs: options.sseKeepAliveMs }),
329
334
  ...(options.maxConcurrentToolCalls === undefined
330
335
  ? {}
331
336
  : { maxConcurrentToolCalls: options.maxConcurrentToolCalls }),
332
337
  ...(options.trustedProxy ? { trustedProxy: true } : {}),
333
- ...(oauth === undefined ? {} : { oauth }),
338
+ ...(oauth === undefined ? {} : { oauth })
334
339
  });
335
340
  handle = await server.listenHttp({
336
341
  port: options.port,
@@ -344,7 +349,7 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
344
349
  : { headersTimeoutMs: options.headersTimeoutMs }),
345
350
  ...(options.keepAliveTimeoutMs === undefined
346
351
  ? {}
347
- : { keepAliveTimeoutMs: options.keepAliveTimeoutMs }),
352
+ : { keepAliveTimeoutMs: options.keepAliveTimeoutMs })
348
353
  });
349
354
  const shutdown = async () => {
350
355
  await handle?.close();