@j0hanz/thinkseq-mcp 1.2.1 → 1.2.3

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
@@ -268,7 +268,7 @@ Output:
268
268
 
269
269
  ## Behavior and validation
270
270
 
271
- - Inputs are validated with Zod and unknown keys are stripped (ignored).
271
+ - Inputs are validated with Zod and unknown keys are rejected.
272
272
  - `thoughtNumber` is auto-incremented (1, 2, 3...).
273
273
  - `totalThoughts` defaults to 3, must be in 1-25, and is adjusted up to at least `thoughtNumber`.
274
274
  - The engine stores thoughts in memory and prunes when limits are exceeded:
@@ -282,7 +282,6 @@ This server publishes events via `node:diagnostics_channel`:
282
282
 
283
283
  - `thinkseq:tool` for `tool.start` and `tool.end` (includes duration, errors, and request context).
284
284
  - `thinkseq:lifecycle` for `lifecycle.started` and `lifecycle.shutdown`.
285
- - `thinkseq:engine` for internal engine events such as `engine.sequence_gap`.
286
285
 
287
286
  ## Configuration
288
287
 
package/dist/app.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { buildShutdownDependencies, resolvePackageIdentity, resolveRunDependencies, } from './appConfig.js';
2
- import { installInitializationGuards } from './lib/protocolGuards.js';
3
2
  const toError = (value) => value instanceof Error ? value : new Error(String(value));
4
3
  const createExit = (proc, exit) => exit ?? ((code) => proc.exit(code));
5
4
  const createHandlerFor = (logError, exit) => (label) => (value) => {
@@ -26,7 +25,6 @@ export async function run(deps = {}) {
26
25
  const server = resolved.createServer(name, version);
27
26
  const engine = resolved.engineFactory();
28
27
  resolved.registerTool(server, engine);
29
- installInitializationGuards(server);
30
28
  const transport = await resolved.connectServer(server);
31
29
  resolved.installShutdownHandlers(buildShutdownDependencies(resolved, { server, engine, transport }));
32
30
  }
@@ -1,23 +1,23 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { readFileSync } from 'node:fs';
4
4
  import { ThinkingEngine } from '../engine.js';
5
5
  import { publishLifecycleEvent } from '../lib/diagnostics.js';
6
6
  import { readSelfPackageJson } from '../lib/package.js';
7
- import { installStdioInvalidMessageGuards, installStdioParseErrorResponder, } from '../lib/stdioGuards.js';
7
+ import { installStdioInitializationGuards, installStdioInvalidMessageGuards, installStdioParseErrorResponder, } from '../lib/stdioGuards.js';
8
8
  import { registerThinkSeq } from '../tools/thinkseq.js';
9
9
  import { installShutdownHandlers } from './shutdown.js';
10
- const DEFAULT_SERVER_INSTRUCTIONS = 'ThinkSeq is a tool for structured, sequential thinking with revision support.';
11
10
  function loadServerInstructions() {
11
+ const fallback = 'ThinkSeq is a tool for structured, sequential thinking with revision support.';
12
12
  try {
13
13
  const raw = readFileSync(new URL('../instructions.md', import.meta.url), {
14
14
  encoding: 'utf8',
15
15
  });
16
16
  const trimmed = raw.trim();
17
- return trimmed.length > 0 ? trimmed : DEFAULT_SERVER_INSTRUCTIONS;
17
+ return trimmed.length > 0 ? trimmed : fallback;
18
18
  }
19
19
  catch {
20
- return DEFAULT_SERVER_INSTRUCTIONS;
20
+ return fallback;
21
21
  }
22
22
  }
23
23
  const SERVER_INSTRUCTIONS = loadServerInstructions();
@@ -31,43 +31,26 @@ const defaultCreateServer = (name, version) => {
31
31
  const defaultConnectServer = async (server, createTransport = () => new StdioServerTransport()) => {
32
32
  const transport = createTransport();
33
33
  await server.connect(transport);
34
+ installStdioInitializationGuards(transport);
34
35
  installStdioInvalidMessageGuards(transport);
35
36
  installStdioParseErrorResponder(transport);
36
37
  return transport;
37
38
  };
38
- function resolveCoreDependencies(deps) {
39
+ export function resolveRunDependencies(deps) {
39
40
  return {
40
41
  processLike: deps.processLike ?? process,
41
42
  packageReadTimeoutMs: deps.packageReadTimeoutMs ?? DEFAULT_PACKAGE_READ_TIMEOUT_MS,
42
43
  readPackageJson: deps.readPackageJson ?? readSelfPackageJson,
43
44
  publishLifecycleEvent: deps.publishLifecycleEvent ?? publishLifecycleEvent,
44
- now: deps.now ?? Date.now,
45
- };
46
- }
47
- function resolveServerDependencies(deps) {
48
- return {
49
45
  createServer: deps.createServer ?? defaultCreateServer,
50
46
  connectServer: deps.connectServer ?? defaultConnectServer,
51
- };
52
- }
53
- function resolveEngineDependencies(deps) {
54
- return {
55
47
  registerTool: deps.registerTool ?? registerThinkSeq,
56
48
  engineFactory: deps.engineFactory ?? (() => new ThinkingEngine()),
57
49
  installShutdownHandlers: deps.installShutdownHandlers ?? installShutdownHandlers,
58
- };
59
- }
60
- function resolveShutdownTimeout(deps) {
61
- if (deps.shutdownTimeoutMs === undefined)
62
- return {};
63
- return { shutdownTimeoutMs: deps.shutdownTimeoutMs };
64
- }
65
- export function resolveRunDependencies(deps) {
66
- return {
67
- ...resolveCoreDependencies(deps),
68
- ...resolveServerDependencies(deps),
69
- ...resolveEngineDependencies(deps),
70
- ...resolveShutdownTimeout(deps),
50
+ now: deps.now ?? Date.now,
51
+ ...(deps.shutdownTimeoutMs !== undefined
52
+ ? { shutdownTimeoutMs: deps.shutdownTimeoutMs }
53
+ : {}),
71
54
  };
72
55
  }
73
56
  export function resolvePackageIdentity(pkg) {
@@ -6,17 +6,24 @@ function isRecord(value) {
6
6
  function hasClose(value) {
7
7
  return isRecord(value) && typeof value.close === 'function';
8
8
  }
9
- async function closeWithTimeout(value, timeoutMs) {
9
+ async function closeSafely(value) {
10
+ try {
11
+ if (hasClose(value))
12
+ await value.close();
13
+ }
14
+ catch {
15
+ return;
16
+ }
17
+ }
18
+ async function closeAllWithinTimeout(values, timeoutMs) {
10
19
  const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
11
- const attempt = (async () => {
12
- try {
13
- if (hasClose(value))
14
- await value.close();
15
- }
16
- catch {
17
- return;
18
- }
19
- })();
20
+ const attempt = Promise.allSettled(values.map((value) => closeSafely(value)))
21
+ .then(() => {
22
+ return;
23
+ })
24
+ .catch(() => {
25
+ return;
26
+ });
20
27
  await Promise.race([attempt, timeout]);
21
28
  }
22
29
  function buildShutdownRunner(deps, proc) {
@@ -33,9 +40,7 @@ function buildShutdownRunner(deps, proc) {
33
40
  ts: timestamp(),
34
41
  signal,
35
42
  });
36
- await closeWithTimeout(deps.server, timeoutMs);
37
- await closeWithTimeout(deps.engine, timeoutMs);
38
- await closeWithTimeout(deps.transport, timeoutMs);
43
+ await closeAllWithinTimeout([deps.server, deps.engine, deps.transport], timeoutMs);
39
44
  proc.exit(0);
40
45
  };
41
46
  }
@@ -1,9 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { ThinkingEngine } from '../engine.js';
3
- export type CloseFn = () => Promise<void> | void;
2
+ export type { CloseFn, EngineLike } from '../lib/types.js';
4
3
  export type ProcessLike = Pick<typeof process, 'on' | 'exit'>;
5
4
  export type TransportLike = Parameters<McpServer['connect']>[0];
6
5
  export type ServerLike = Pick<McpServer, 'connect' | 'registerTool'>;
7
- export type EngineLike = Pick<ThinkingEngine, 'processThought'> & {
8
- close?: CloseFn;
9
- };
@@ -1,23 +1,16 @@
1
1
  export function resolveRevisionTarget(input, getThoughtByNumber) {
2
- const targetNumberResult = getRevisionTargetNumber(input);
3
- if (!targetNumberResult.ok) {
4
- return targetNumberResult;
2
+ const targetNumber = input.revisesThought;
3
+ if (targetNumber === undefined) {
4
+ return {
5
+ ok: false,
6
+ error: buildRevisionError('E_REVISION_MISSING', 'revisesThought is required for revision'),
7
+ };
5
8
  }
6
- const validationError = validateRevisionTarget(getThoughtByNumber, targetNumberResult.targetNumber);
9
+ const validationError = validateRevisionTarget(getThoughtByNumber, targetNumber);
7
10
  if (validationError) {
8
11
  return { ok: false, error: validationError };
9
12
  }
10
- return { ok: true, targetNumber: targetNumberResult.targetNumber };
11
- }
12
- function getRevisionTargetNumber(input) {
13
- const targetNumber = input.revisesThought;
14
- if (targetNumber !== undefined) {
15
- return { ok: true, targetNumber };
16
- }
17
- return {
18
- ok: false,
19
- error: buildRevisionError('E_REVISION_MISSING', 'revisesThought is required for revision'),
20
- };
13
+ return { ok: true, targetNumber };
21
14
  }
22
15
  function validateRevisionTarget(getThoughtByNumber, targetNumber) {
23
16
  const target = getThoughtByNumber(targetNumber);
@@ -1,10 +1,5 @@
1
- function getRecentActiveThoughts(activeThoughts, limit) {
2
- if (limit <= 0)
3
- return [];
4
- return activeThoughts.slice(-limit);
5
- }
6
1
  export function buildContextSummary(activeThoughts, revisionInfo) {
7
- const recent = getRecentActiveThoughts(activeThoughts, 5);
2
+ const recent = activeThoughts.slice(-5);
8
3
  const startIndex = activeThoughts.length - recent.length;
9
4
  const recentThoughts = recent.map((thought, index) => ({
10
5
  stepIndex: startIndex + index + 1,
@@ -4,6 +4,7 @@ export class ThoughtStore {
4
4
  #thoughtIndex = new Map();
5
5
  #activeThoughts = [];
6
6
  #activeThoughtNumbers = [];
7
+ #activeMaxTotalThoughts = 0;
7
8
  #headIndex = 0;
8
9
  #nextThoughtNumber = 1;
9
10
  #estimatedBytes = 0;
@@ -18,9 +19,10 @@ export class ThoughtStore {
18
19
  nextThoughtNumbers(totalThoughts) {
19
20
  const thoughtNumber = this.#nextThoughtNumber;
20
21
  this.#nextThoughtNumber += 1;
22
+ const effectiveTotalThoughts = Math.max(totalThoughts, thoughtNumber, this.#activeMaxTotalThoughts);
21
23
  return {
22
24
  thoughtNumber,
23
- totalThoughts: Math.max(totalThoughts, thoughtNumber),
25
+ totalThoughts: effectiveTotalThoughts,
24
26
  };
25
27
  }
26
28
  storeThought(stored) {
@@ -29,9 +31,17 @@ export class ThoughtStore {
29
31
  if (stored.isActive) {
30
32
  this.#activeThoughts.push(stored);
31
33
  this.#activeThoughtNumbers.push(stored.thoughtNumber);
34
+ this.#activeMaxTotalThoughts = Math.max(this.#activeMaxTotalThoughts, stored.totalThoughts);
32
35
  }
33
36
  this.#estimatedBytes += this.#estimateThoughtBytes(stored);
34
37
  }
38
+ #recomputeActiveMaxTotalThoughts() {
39
+ let maxTotal = 0;
40
+ for (const thought of this.#activeThoughts) {
41
+ maxTotal = Math.max(maxTotal, thought.totalThoughts);
42
+ }
43
+ this.#activeMaxTotalThoughts = maxTotal;
44
+ }
35
45
  #findActiveThoughtIndex(thoughtNumber) {
36
46
  const activeThoughtNumbers = this.#activeThoughtNumbers;
37
47
  let low = 0;
@@ -71,6 +81,7 @@ export class ThoughtStore {
71
81
  }
72
82
  this.#activeThoughts.length = startIndex;
73
83
  this.#activeThoughtNumbers.length = startIndex;
84
+ this.#recomputeActiveMaxTotalThoughts();
74
85
  return supersedes;
75
86
  }
76
87
  getActiveThoughts() {
@@ -114,6 +125,7 @@ export class ThoughtStore {
114
125
  return;
115
126
  this.#activeThoughts = this.#activeThoughts.slice(startIndex);
116
127
  this.#activeThoughtNumbers = this.#activeThoughtNumbers.slice(startIndex);
128
+ this.#recomputeActiveMaxTotalThoughts();
117
129
  }
118
130
  #removeOldest(count, options = {}) {
119
131
  const totalLength = this.getTotalLength();
@@ -147,27 +159,24 @@ export class ThoughtStore {
147
159
  #compactIfNeeded(force = false) {
148
160
  if (this.#headIndex === 0)
149
161
  return;
150
- const totalLength = this.getTotalLength();
151
- if (totalLength === 0) {
162
+ if (this.getTotalLength() === 0) {
152
163
  this.#resetThoughts();
153
164
  return;
154
165
  }
155
- if (!this.#shouldCompact(force))
166
+ if (!force &&
167
+ this.#headIndex < COMPACT_THRESHOLD &&
168
+ this.#headIndex < this.#thoughts.length * COMPACT_RATIO) {
156
169
  return;
170
+ }
157
171
  this.#thoughts = this.#thoughts.slice(this.#headIndex);
158
172
  this.#headIndex = 0;
159
173
  }
160
- #shouldCompact(force) {
161
- if (force)
162
- return true;
163
- return (this.#headIndex >= COMPACT_THRESHOLD ||
164
- this.#headIndex >= this.#thoughts.length * COMPACT_RATIO);
165
- }
166
174
  #resetThoughts() {
167
175
  this.#thoughts = [];
168
176
  this.#thoughtIndex.clear();
169
177
  this.#activeThoughts = [];
170
178
  this.#activeThoughtNumbers = [];
179
+ this.#activeMaxTotalThoughts = 0;
171
180
  this.#headIndex = 0;
172
181
  this.#nextThoughtNumber = 1;
173
182
  this.#estimatedBytes = 0;
package/dist/engine.js CHANGED
@@ -24,11 +24,8 @@ export class ThinkingEngine {
24
24
  return this.#processNewThought(input);
25
25
  }
26
26
  #processNewThought(input) {
27
- const effectiveTotalThoughts = this.#resolveEffectiveTotalThoughts(input);
28
- const numbers = this.#store.nextThoughtNumbers(effectiveTotalThoughts);
29
- const stored = this.#buildStoredThought(input, numbers);
30
- this.#store.storeThought(stored);
31
- this.#store.pruneHistoryIfNeeded();
27
+ const { stored } = this.#createStoredThought(input);
28
+ this.#commitThought(stored);
32
29
  return this.#buildProcessResult(stored);
33
30
  }
34
31
  #processRevision(input) {
@@ -36,18 +33,14 @@ export class ThinkingEngine {
36
33
  if (!resolved.ok)
37
34
  return resolved.error;
38
35
  const { targetNumber } = resolved;
39
- const effectiveTotalThoughts = this.#resolveEffectiveTotalThoughts(input);
40
- const numbers = this.#store.nextThoughtNumbers(effectiveTotalThoughts);
36
+ const { numbers, stored } = this.#createStoredThought(input, {
37
+ revisionOf: targetNumber,
38
+ });
41
39
  const supersedesAll = this.#store.supersedeFrom(targetNumber, numbers.thoughtNumber);
42
40
  const supersedesTotal = supersedesAll.length;
43
41
  const supersedes = capArrayStart(supersedesAll, MAX_SUPERSEDES);
44
- const stored = this.#buildStoredThought(input, {
45
- ...numbers,
46
- revisionOf: targetNumber,
47
- });
48
- this.#store.storeThought(stored);
49
42
  this.#hasRevisions = true;
50
- this.#store.pruneHistoryIfNeeded();
43
+ this.#commitThought(stored);
51
44
  return this.#buildProcessResult(stored, {
52
45
  revises: targetNumber,
53
46
  supersedes,
@@ -69,14 +62,26 @@ export class ThinkingEngine {
69
62
  }),
70
63
  };
71
64
  }
65
+ #createStoredThought(input, extras = {}) {
66
+ const effectiveTotalThoughts = this.#resolveEffectiveTotalThoughts(input);
67
+ const numbers = this.#store.nextThoughtNumbers(effectiveTotalThoughts);
68
+ const stored = this.#buildStoredThought(input, {
69
+ ...numbers,
70
+ ...extras,
71
+ });
72
+ return { stored, numbers };
73
+ }
74
+ #commitThought(stored) {
75
+ this.#store.storeThought(stored);
76
+ this.#store.pruneHistoryIfNeeded();
77
+ }
72
78
  #resolveEffectiveTotalThoughts(input) {
73
79
  if (input.totalThoughts !== undefined) {
74
80
  return input.totalThoughts;
75
81
  }
76
82
  const activeThoughts = this.#store.getActiveThoughts();
77
83
  const lastActive = activeThoughts[activeThoughts.length - 1];
78
- if (lastActive !== undefined &&
79
- lastActive.totalThoughts > _a.DEFAULT_TOTAL_THOUGHTS) {
84
+ if (lastActive !== undefined) {
80
85
  return lastActive.totalThoughts;
81
86
  }
82
87
  return _a.DEFAULT_TOTAL_THOUGHTS;
@@ -1,66 +1,76 @@
1
- # ThinkSeq MCP Server — Instructions
1
+ # ThinkSeq MCP Server — AI Usage Instructions
2
2
 
3
- ## What this server does
3
+ Use this server to record sequential thinking steps to plan, reason, and debug. Prefer these tools over "remembering" state in chat.
4
4
 
5
- ThinkSeq exposes one MCP tool, `thinkseq`, to help you keep a short, numbered chain of reasoning steps with progress tracking and revision support.
5
+ ## Operating Rules
6
6
 
7
- - State is in-memory only and resets when the server restarts.
8
- - Keep each step concise; this is a structure/coordination tool, not a scratchpad for long text.
7
+ - Keep thoughts atomic: one decision, calculation, or action per step.
8
+ - Use revisions to fix mistakes in the active chain instead of apologizing in chat.
9
+ - If request is vague, ask clarifying questions.
9
10
 
10
- ## Available tools
11
+ ### Strategies
11
12
 
12
- ### `thinkseq`
13
+ - **Discovery:** Read the tool output's `context` to see recent thoughts and available revision targets.
14
+ - **Action:** Use `thinkseq` to advance the reasoning chain or `revisesThought` to rewind and correct.
13
15
 
14
- Record a single sequential thinking step.
16
+ ## Data Model
15
17
 
16
- **Use it when:**
18
+ - **Thinking Step:** `thought` (text), `thoughtNumber` (int), `progress` (0-1), `isComplete` (bool)
17
19
 
18
- - You want a small, explicit plan (step-by-step) and a progress signal.
19
- - You want a lightweight decision log (assumptions → choice → next action).
20
- - You are debugging and want a clear hypothesis → check → result chain.
20
+ ## Runtime Controls
21
21
 
22
- **Do not use it for:**
22
+ - **Retention (server CLI):** the server keeps a rolling in-memory history.
23
+ - `--max-thoughts <number>` controls how many total stored thoughts are retained (default is 500).
24
+ - `--max-memory-mb <number>` caps estimated memory use for stored thoughts (default is 100MB).
25
+ - **Text content compatibility:** by default the tool returns both `structuredContent` and a JSON string in `content`.
26
+ - Set `THINKSEQ_INCLUDE_TEXT_CONTENT=0|false|no|off` to omit the JSON string and return only `structuredContent`.
23
27
 
24
- - Long-form writing or dumping large transcripts.
25
- - Storing secrets, credentials, or personal data.
28
+ ## Workflows
26
29
 
27
- **Inputs:**
30
+ ### 1) Structured Reasoning
28
31
 
29
- - `thought` (string, 1–5000 chars, required): one concise step.
30
- - `totalThoughts` (int, 1–25, optional): estimated total steps (default: 3).
31
- - `revisesThought` (int ≥ 1, optional): revise a previous step by its `thoughtNumber`.
32
+ ```text
33
+ thinkseq(thought="Plan: 1. check, 2. fix", totalThoughts=5) Start chain
34
+ thinkseq(thought="Check passed, starting fix") Progress chain
35
+ thinkseq(thought="Revised plan: use new API", revisesThought=1) → Correction
36
+ ```
32
37
 
33
- **Revision semantics (important):**
38
+ Notes:
34
39
 
35
- - A revision is a destructive rewind of the _active chain_.
36
- - When you set `revisesThought`, the tool supersedes the targeted thought and every later _active_ thought, then continues from your corrected step.
37
- - Superseded thoughts remain in history for audit, but they are no longer in the active chain.
40
+ - `totalThoughts` is only an estimate for progress/completion (max 25); it does **not** change retention.
41
+ - Revisions can only target active (non-superseded) thoughts.
38
42
 
39
- **Outputs (prefer `structuredContent`):**
43
+ ## Tools
40
44
 
41
- - `result.progress` in [0, 1] and `result.isComplete`
42
- - `result.revisableThoughts`: thought numbers you can revise
43
- - `result.context.recentThoughts`: previews of the current active chain
44
- - `result.context.revisionInfo`: present only when a revision occurred
45
+ ### thinkseq
45
46
 
46
- ## Recommended workflow
47
+ Record a concise thinking step (max 5000 chars). Be brief: capture only the essential insight, calculation, or decision.
47
48
 
48
- 1. Set `totalThoughts` once at the start (adjust later if needed).
49
- 2. Call `thinkseq` once per step; keep `thought` focused (one decision, one calculation, or one next action).
50
- 3. If you discover an earlier mistake, call `thinkseq` again with `revisesThought` to correct it.
51
- 4. Stop using the tool when `isComplete` becomes `true` (or once you have enough structure to proceed).
49
+ - **Use when:** You need to structured reasoning, planning, or a decision log.
50
+ - **Args:**
51
+ - `thought` (string, required): Your current thinking step.
52
+ - `totalThoughts` (number, optional): Estimated total thoughts (1-25, default: 3).
53
+ - `revisesThought` (number, optional): Revise a previous thought by number.
54
+ - **Returns:** `thoughtNumber`, `progress`, `isComplete`, `revisableThoughts`, `context` (history previews).
52
55
 
53
- ## Response handling guidance
56
+ ## Response Shape
54
57
 
55
- - Don’t paste raw tool JSON to the user unless they ask.
56
- - Use the tool output to write a clean, user-facing summary and next steps.
58
+ Success: `{ "ok": true, "result": { ... } }`
59
+ Error: `{ "ok": false, "error": { "code": "...", "message": "..." } }`
57
60
 
58
- ## Common errors
61
+ ### Common Errors
59
62
 
60
- - `E_REVISION_TARGET_NOT_FOUND`: the target thought number doesn’t exist.
61
- - `E_REVISION_TARGET_SUPERSEDED`: the target thought is no longer active.
62
- - `E_THINK`: unexpected server-side failure.
63
+ | Code | Meaning | Resolution |
64
+ | ------------------------------ | -------------------------------------- | ----------------------------------------- |
65
+ | `E_REVISION_TARGET_NOT_FOUND` | Revision target ID does not exist | Check `revisableThoughts` for valid IDs |
66
+ | `E_REVISION_TARGET_SUPERSEDED` | Target thought was already overwritten | Revise the current active thought instead |
67
+ | `E_THINK` | Generic engine error | Check arguments and retry |
63
68
 
64
- ## Optional configuration
69
+ ## Limits
65
70
 
66
- - `THINKSEQ_INCLUDE_TEXT_CONTENT`: when set to `0`/`false`/`no`/`off`, the tool returns no `content` text and you should rely on `structuredContent`.
71
+ - **Max Thoughts:** 25 (default estimate)
72
+ - **Max Length:** 5000 chars per thought
73
+
74
+ ## Security
75
+
76
+ - Do not store credentials, secrets, or PII in thoughts. State is in-memory only but may be logged.
package/dist/lib/cli.js CHANGED
@@ -20,11 +20,24 @@ Options:
20
20
  --package-read-timeout-ms <number> Package.json read timeout
21
21
  -h, --help Show this help`;
22
22
  const BYTES_PER_MB = 1024 * 1024;
23
+ const CLI_OPTION_SPECS = [
24
+ { key: 'max-thoughts', configKey: 'maxThoughts' },
25
+ {
26
+ key: 'max-memory-mb',
27
+ configKey: 'maxMemoryBytes',
28
+ map: (value) => value * BYTES_PER_MB,
29
+ },
30
+ { key: 'shutdown-timeout-ms', configKey: 'shutdownTimeoutMs' },
31
+ { key: 'package-read-timeout-ms', configKey: 'packageReadTimeoutMs' },
32
+ ];
23
33
  function parsePositiveInt(value, label) {
24
34
  if (value === undefined)
25
35
  return undefined;
26
- const parsed = Number.parseInt(value, 10);
27
- if (!Number.isFinite(parsed) || parsed <= 0) {
36
+ if (!/^\d+$/.test(value)) {
37
+ throw new Error(`Invalid ${label}: ${value}`);
38
+ }
39
+ const parsed = Number(value);
40
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
28
41
  throw new Error(`Invalid ${label}: ${value}`);
29
42
  }
30
43
  return parsed;
@@ -40,21 +53,14 @@ function getStringOption(values, key) {
40
53
  return typeof value === 'string' ? value : undefined;
41
54
  }
42
55
  function buildCliConfig(values) {
43
- const maxThoughts = parsePositiveInt(getStringOption(values, 'max-thoughts'), 'max-thoughts');
44
- const maxMemoryMb = parsePositiveInt(getStringOption(values, 'max-memory-mb'), 'max-memory-mb');
45
- const shutdownTimeoutMs = parsePositiveInt(getStringOption(values, 'shutdown-timeout-ms'), 'shutdown-timeout-ms');
46
- const packageReadTimeoutMs = parsePositiveInt(getStringOption(values, 'package-read-timeout-ms'), 'package-read-timeout-ms');
47
56
  const config = {};
48
- if (maxThoughts !== undefined)
49
- config.maxThoughts = maxThoughts;
50
- if (maxMemoryMb !== undefined) {
51
- config.maxMemoryBytes = maxMemoryMb * BYTES_PER_MB;
52
- }
53
- if (shutdownTimeoutMs !== undefined) {
54
- config.shutdownTimeoutMs = shutdownTimeoutMs;
55
- }
56
- if (packageReadTimeoutMs !== undefined) {
57
- config.packageReadTimeoutMs = packageReadTimeoutMs;
57
+ for (const spec of CLI_OPTION_SPECS) {
58
+ const raw = getStringOption(values, spec.key);
59
+ const parsed = parsePositiveInt(raw, spec.key);
60
+ if (parsed === undefined)
61
+ continue;
62
+ const mapped = spec.map ? spec.map(parsed) : parsed;
63
+ config[spec.configKey] = mapped;
58
64
  }
59
65
  return config;
60
66
  }
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  const defaultReadFile = (path, options) => readFile(path, options);
5
- const defaultCwd = () => process.cwd();
6
5
  const defaultPackageJsonPath = fileURLToPath(new URL('../../package.json', import.meta.url));
7
6
  function isRecord(value) {
8
7
  return typeof value === 'object' && value !== null;
@@ -30,25 +29,16 @@ function parsePackageJson(raw) {
30
29
  return {};
31
30
  }
32
31
  }
33
- function resolveReadFile(deps) {
34
- return deps?.readFile ?? defaultReadFile;
35
- }
36
- function resolveCwd(deps) {
37
- return deps?.cwd ?? defaultCwd;
38
- }
39
32
  function resolvePackageJsonPath(deps) {
40
33
  // Only honor cwd injection when readFile is also injected (test seam).
41
34
  if (deps?.readFile && deps.cwd)
42
- return join(resolveCwd(deps)(), 'package.json');
35
+ return join(deps.cwd(), 'package.json');
43
36
  return defaultPackageJsonPath;
44
37
  }
45
- function buildReadOptions(signal) {
46
- return signal ? { encoding: 'utf8', signal } : { encoding: 'utf8' };
47
- }
48
38
  export async function readSelfPackageJson(signal, deps) {
49
39
  try {
50
- const readFileImpl = resolveReadFile(deps);
51
- const raw = await readFileImpl(resolvePackageJsonPath(deps), buildReadOptions(signal));
40
+ const readFileImpl = deps?.readFile ?? defaultReadFile;
41
+ const raw = await readFileImpl(resolvePackageJsonPath(deps), signal ? { encoding: 'utf8', signal } : { encoding: 'utf8' });
52
42
  return parsePackageJson(raw);
53
43
  }
54
44
  catch {
@@ -42,15 +42,16 @@ function wrapWithInitializationGuard(method, handler, state) {
42
42
  return await handler(request, extra);
43
43
  };
44
44
  }
45
- function getProtocolObject(server) {
45
+ function getProtocolHandlers(server) {
46
46
  if (!isRecord(server))
47
47
  return undefined;
48
48
  const protocol = Reflect.get(server, 'server');
49
- return isRecord(protocol) ? protocol : undefined;
50
- }
51
- function getRequestHandlers(protocol) {
49
+ if (!isRecord(protocol))
50
+ return undefined;
52
51
  const handlers = Reflect.get(protocol, '_requestHandlers');
53
- return handlers instanceof Map ? handlers : undefined;
52
+ if (!(handlers instanceof Map))
53
+ return undefined;
54
+ return { protocol, handlers };
54
55
  }
55
56
  function installFallbackRequestHandler(protocol, state) {
56
57
  // Guard unknown methods as well (so pre-init calls to unknown methods don't
@@ -76,13 +77,10 @@ function wrapRequestHandlers(handlers, state) {
76
77
  }
77
78
  }
78
79
  export function installInitializationGuards(server) {
79
- const protocol = getProtocolObject(server);
80
- if (!protocol)
81
- return;
82
- const handlers = getRequestHandlers(protocol);
83
- if (!handlers)
80
+ const resolved = getProtocolHandlers(server);
81
+ if (!resolved)
84
82
  return;
85
83
  const state = { sawInitialize: false };
86
- wrapRequestHandlers(handlers, state);
87
- installFallbackRequestHandler(protocol, state);
84
+ wrapRequestHandlers(resolved.handlers, state);
85
+ installFallbackRequestHandler(resolved.protocol, state);
88
86
  }
@@ -1,2 +1,3 @@
1
+ export declare function installStdioInitializationGuards(transport: unknown): void;
1
2
  export declare function installStdioInvalidMessageGuards(transport: unknown): void;
2
3
  export declare function installStdioParseErrorResponder(transport: unknown): void;