@j0hanz/thinkseq-mcp 1.2.2 → 1.2.4

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,5 @@
1
1
  import { buildShutdownDependencies, resolvePackageIdentity, resolveRunDependencies, } from './appConfig.js';
2
- import { installInitializationGuards } from './lib/protocolGuards.js';
2
+ import { installMcpLogging } from './lib/mcpLogging.js';
3
3
  const toError = (value) => value instanceof Error ? value : new Error(String(value));
4
4
  const createExit = (proc, exit) => exit ?? ((code) => proc.exit(code));
5
5
  const createHandlerFor = (logError, exit) => (label) => (value) => {
@@ -19,14 +19,14 @@ export async function run(deps = {}) {
19
19
  const resolved = resolveRunDependencies(deps);
20
20
  const pkg = await resolved.readPackageJson(AbortSignal.timeout(resolved.packageReadTimeoutMs));
21
21
  const { name, version } = resolvePackageIdentity(pkg);
22
+ const server = resolved.createServer(name, version);
23
+ installMcpLogging(server);
22
24
  resolved.publishLifecycleEvent({
23
25
  type: 'lifecycle.started',
24
26
  ts: resolved.now(),
25
27
  });
26
- const server = resolved.createServer(name, version);
27
28
  const engine = resolved.engineFactory();
28
29
  resolved.registerTool(server, engine);
29
- installInitializationGuards(server);
30
30
  const transport = await resolved.connectServer(server);
31
31
  resolved.installShutdownHandlers(buildShutdownDependencies(resolved, { server, engine, transport }));
32
32
  }
@@ -1,73 +1,71 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
1
  import { readFileSync } from 'node:fs';
2
+ import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
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
- function loadServerInstructions() {
10
+ const INSTRUCTIONS_URL = new URL('../instructions.md', import.meta.url);
11
+ const INSTRUCTIONS_FALLBACK = 'ThinkSeq is a tool for structured, sequential thinking with revision support.';
12
+ function readInstructionsText() {
12
13
  try {
13
- const raw = readFileSync(new URL('../instructions.md', import.meta.url), {
14
- encoding: 'utf8',
15
- });
16
- const trimmed = raw.trim();
17
- return trimmed.length > 0 ? trimmed : DEFAULT_SERVER_INSTRUCTIONS;
14
+ return readFileSync(INSTRUCTIONS_URL, { encoding: 'utf8' });
18
15
  }
19
16
  catch {
20
- return DEFAULT_SERVER_INSTRUCTIONS;
17
+ return INSTRUCTIONS_FALLBACK;
21
18
  }
22
19
  }
20
+ function loadServerInstructions() {
21
+ const raw = readInstructionsText();
22
+ const trimmed = raw.trim();
23
+ return trimmed.length > 0 ? trimmed : INSTRUCTIONS_FALLBACK;
24
+ }
25
+ function registerInstructionsResource(server) {
26
+ server.registerResource('instructions', new ResourceTemplate('internal://instructions', { list: undefined }), { title: 'Instructions', mimeType: 'text/markdown' }, (uri) => ({
27
+ contents: [
28
+ {
29
+ uri: uri.href,
30
+ text: readInstructionsText(),
31
+ mimeType: 'text/markdown',
32
+ },
33
+ ],
34
+ }));
35
+ }
23
36
  const SERVER_INSTRUCTIONS = loadServerInstructions();
24
37
  const DEFAULT_PACKAGE_READ_TIMEOUT_MS = 2000;
25
38
  const defaultCreateServer = (name, version) => {
26
- return new McpServer({ name, version }, {
39
+ const server = new McpServer({ name, version }, {
27
40
  instructions: SERVER_INSTRUCTIONS,
28
- capabilities: { logging: {} },
41
+ capabilities: { logging: {}, tools: { listChanged: true } },
29
42
  });
43
+ registerInstructionsResource(server);
44
+ return server;
30
45
  };
31
46
  const defaultConnectServer = async (server, createTransport = () => new StdioServerTransport()) => {
32
47
  const transport = createTransport();
33
48
  await server.connect(transport);
49
+ installStdioInitializationGuards(transport);
34
50
  installStdioInvalidMessageGuards(transport);
35
51
  installStdioParseErrorResponder(transport);
36
52
  return transport;
37
53
  };
38
- function resolveCoreDependencies(deps) {
54
+ export function resolveRunDependencies(deps) {
39
55
  return {
40
56
  processLike: deps.processLike ?? process,
41
57
  packageReadTimeoutMs: deps.packageReadTimeoutMs ?? DEFAULT_PACKAGE_READ_TIMEOUT_MS,
42
58
  readPackageJson: deps.readPackageJson ?? readSelfPackageJson,
43
59
  publishLifecycleEvent: deps.publishLifecycleEvent ?? publishLifecycleEvent,
44
- now: deps.now ?? Date.now,
45
- };
46
- }
47
- function resolveServerDependencies(deps) {
48
- return {
49
60
  createServer: deps.createServer ?? defaultCreateServer,
50
61
  connectServer: deps.connectServer ?? defaultConnectServer,
51
- };
52
- }
53
- function resolveEngineDependencies(deps) {
54
- return {
55
62
  registerTool: deps.registerTool ?? registerThinkSeq,
56
63
  engineFactory: deps.engineFactory ?? (() => new ThinkingEngine()),
57
64
  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),
65
+ now: deps.now ?? Date.now,
66
+ ...(deps.shutdownTimeoutMs !== undefined
67
+ ? { shutdownTimeoutMs: deps.shutdownTimeoutMs }
68
+ : {}),
71
69
  };
72
70
  }
73
71
  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
- export type ServerLike = Pick<McpServer, 'connect' | 'registerTool'>;
7
- export type EngineLike = Pick<ThinkingEngine, 'processThought'> & {
8
- close?: CloseFn;
9
- };
5
+ export type ServerLike = Pick<McpServer, 'connect' | 'registerTool' | 'registerResource' | 'sendLoggingMessage'>;
@@ -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,63 +1,40 @@
1
- # ThinkSeq MCP Server — AI Usage Instructions
1
+ # ThinkSeq MCP Server Instructions
2
2
 
3
- Use this server to record sequential thinking steps to plan, reason, and debug. Prefer these tools over "remembering" state in chat.
3
+ > **Guidance for the Agent:** These instructions are available as a resource (`internal://instructions`). Load them when you are confused about tool usage.
4
4
 
5
- ## Operating Rules
5
+ ## 1. Core Capability
6
6
 
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.
7
+ - **Domain:** In-memory, sequential thinking with revision (destructive rewind).
8
+ - **Primary Resources:** `Thoughts`, `RevisionChain`, `ProgressContext`.
10
9
 
11
- ### Strategies
10
+ ## 2. The "Golden Path" Workflows (Critical)
12
11
 
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.
12
+ _Follow this order; do not guess indices._
15
13
 
16
- ## Data Model
14
+ ### Workflow A: Capture a reasoning chain
17
15
 
18
- - **Thinking Step:** `thought` (text), `thoughtNumber` (int), `progress` (0-1), `isComplete` (bool)
16
+ 1. Call `thinkseq` with `thought` (and optionally `totalThoughts`).
17
+ 2. Continue calling `thinkseq` for each step.
18
+ 3. Read `progress` and `isComplete` from the response to know when to stop.
19
+ > **Constraint:** Keep each step atomic; one decision per call.
19
20
 
20
- ## Workflows
21
+ ### Workflow B: Revise a prior step
21
22
 
22
- ### 1) Structured Reasoning
23
+ 1. Call `thinkseq` to get the latest `revisableThoughts` list.
24
+ 2. Call `thinkseq` again with `revisesThought` set to a valid entry.
25
+ 3. Continue from the revised step.
26
+ > **Constraint:** Never guess `revisesThought`; always pick from `revisableThoughts`.
23
27
 
24
- ```text
25
- thinkseq(thought="Plan: 1. check, 2. fix", totalThoughts=5) → Start chain
26
- thinkseq(thought="Check passed, starting fix") → Progress chain
27
- thinkseq(thought="Revised plan: use new API", revisesThought=1) → Correction
28
- ```
28
+ ## 3. Tool Nuances & "Gotchas"
29
29
 
30
- ## Tools
30
+ - **`thinkseq`**:
31
+ - **Side Effects:** Mutates in-memory thought history (write operation).
32
+ - **Limits:** `thought` max 5000 chars; `totalThoughts` max 25.
33
+ - **Revisions:** Revising a thought supersedes that step and all later active steps.
34
+ - **Compatibility:** Set `THINKSEQ_INCLUDE_TEXT_CONTENT=0|false|no|off` to omit the JSON string in `content`.
31
35
 
32
- ### thinkseq
36
+ ## 4. Error Handling Strategy
33
37
 
34
- Record a concise thinking step (max 5000 chars). Be brief: capture only the essential insight, calculation, or decision.
35
-
36
- - **Use when:** You need to structured reasoning, planning, or a decision log.
37
- - **Args:**
38
- - `thought` (string, required): Your current thinking step.
39
- - `totalThoughts` (number, optional): Estimated total thoughts (1-25, default: 3).
40
- - `revisesThought` (number, optional): Revise a previous thought by number.
41
- - **Returns:** `thoughtNumber`, `progress`, `isComplete`, `revisableThoughts`, `context` (history previews).
42
-
43
- ## Response Shape
44
-
45
- Success: `{ "ok": true, "result": { ... } }`
46
- Error: `{ "ok": false, "error": { "code": "...", "message": "..." } }`
47
-
48
- ### Common Errors
49
-
50
- | Code | Meaning | Resolution |
51
- | ------------------------------ | -------------------------------------- | ----------------------------------------- |
52
- | `E_REVISION_TARGET_NOT_FOUND` | Revision target ID does not exist | Check `revisableThoughts` for valid IDs |
53
- | `E_REVISION_TARGET_SUPERSEDED` | Target thought was already overwritten | Revise the current active thought instead |
54
- | `E_THINK` | Generic engine error | Check arguments and retry |
55
-
56
- ## Limits
57
-
58
- - **Max Thoughts:** 25 (default estimate)
59
- - **Max Length:** 5000 chars per thought
60
-
61
- ## Security
62
-
63
- - Do not store credentials, secrets, or PII in thoughts. State is in-memory only but may be logged.
38
+ - If `E_REVISION_TARGET_NOT_FOUND`, fetch a fresh `revisableThoughts` list and retry.
39
+ - If `E_REVISION_TARGET_SUPERSEDED`, revise the latest active thought instead.
40
+ - If `E_THINK`, verify inputs and retry once.
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
  }
@@ -0,0 +1,5 @@
1
+ import type { LoggingMessageNotificationParams } from '@modelcontextprotocol/sdk/types.js';
2
+ export interface LoggingTarget {
3
+ sendLoggingMessage: (params: LoggingMessageNotificationParams, sessionId?: string) => Promise<void>;
4
+ }
5
+ export declare function installMcpLogging(target: LoggingTarget): () => void;
@@ -0,0 +1,60 @@
1
+ import diagnosticsChannel from 'node:diagnostics_channel';
2
+ const TOOL_LOGGER = 'thinkseq.tool';
3
+ const LIFECYCLE_LOGGER = 'thinkseq.lifecycle';
4
+ const toolChannel = diagnosticsChannel.channel('thinkseq:tool');
5
+ const lifecycleChannel = diagnosticsChannel.channel('thinkseq:lifecycle');
6
+ function isRecord(value) {
7
+ return typeof value === 'object' && value !== null;
8
+ }
9
+ function isToolEvent(value) {
10
+ if (!isRecord(value))
11
+ return false;
12
+ const { type } = value;
13
+ if (type !== 'tool.start' && type !== 'tool.end')
14
+ return false;
15
+ return value.tool === 'thinkseq';
16
+ }
17
+ function isLifecycleEvent(value) {
18
+ if (!isRecord(value))
19
+ return false;
20
+ return (value.type === 'lifecycle.started' || value.type === 'lifecycle.shutdown');
21
+ }
22
+ function sendLog(target, params) {
23
+ void target.sendLoggingMessage(params).catch(() => {
24
+ return;
25
+ });
26
+ }
27
+ function toolEventToLog(event) {
28
+ const level = event.type === 'tool.end' && !event.ok ? 'error' : 'info';
29
+ return {
30
+ level,
31
+ logger: TOOL_LOGGER,
32
+ data: event,
33
+ };
34
+ }
35
+ function lifecycleEventToLog(event) {
36
+ const level = event.type === 'lifecycle.shutdown' ? 'notice' : 'info';
37
+ return {
38
+ level,
39
+ logger: LIFECYCLE_LOGGER,
40
+ data: event,
41
+ };
42
+ }
43
+ export function installMcpLogging(target) {
44
+ const onTool = (event) => {
45
+ if (!isToolEvent(event))
46
+ return;
47
+ sendLog(target, toolEventToLog(event));
48
+ };
49
+ const onLifecycle = (event) => {
50
+ if (!isLifecycleEvent(event))
51
+ return;
52
+ sendLog(target, lifecycleEventToLog(event));
53
+ };
54
+ toolChannel.subscribe(onTool);
55
+ lifecycleChannel.subscribe(onLifecycle);
56
+ return () => {
57
+ toolChannel.unsubscribe(onTool);
58
+ lifecycleChannel.unsubscribe(onLifecycle);
59
+ };
60
+ }
@@ -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 {