tana-mcp-codemode 0.1.0 → 0.2.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/LICENSE.MD ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fábio galiano (fabiogaliano.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -5,7 +5,7 @@ A codemode MCP server for [Tana](https://tana.inc) knowledge management. AI writ
5
5
  ## Features
6
6
 
7
7
  - **Codemode Pattern** — AI writes executable TypeScript, not structured API calls
8
- - **Full Tana API** — Workspaces, nodes, tags, fields, calendar, import
8
+ - **Tana Local API** — Workspaces, nodes, tags, fields, calendar, import
9
9
  - **Top-level Await** — `await tana.workspaces.list()` works directly
10
10
  - **Timeout Protection** — 10s max execution prevents infinite loops
11
11
  - **Script History** — SQLite persistence for debugging and replay
@@ -37,18 +37,18 @@ bun install
37
37
 
38
38
  ## Configuration
39
39
 
40
- | Variable | Default | Description |
41
- | ------------------- | ----------------------- | --------------------------------------- |
42
- | `TANA_API_TOKEN` | (required) | Bearer token from Tana Desktop |
43
- | `TANA_API_URL` | `http://127.0.0.1:8262` | Tana Local API URL |
44
- | `TANA_TIMEOUT` | `10000` | Request timeout in ms |
45
- | `TANA_HISTORY_PATH` | (platform default) | Custom path for SQLite history database |
40
+ | Variable | Default | Description |
41
+ | ----------------------- | ----------------------- | -------------------------------------------------------------- |
42
+ | `TANA_API_TOKEN` | (required) | Bearer token from Tana Desktop |
43
+ | `TANA_API_URL` | `http://127.0.0.1:8262` | Tana Local API URL |
44
+ | `TANA_TIMEOUT` | `10000` | Request timeout in ms |
45
+ | `TANA_HISTORY_PATH` | (platform default) | Custom path for SQLite history database |
46
+ | `MAIN_TANA_WORKSPACE` | (none) | Default workspace name or ID, resolved at startup |
47
+ | `TANA_SEARCH_WORKSPACES`| (none) | Comma-separated workspace names or IDs for default search scoping |
46
48
 
47
49
  ## MCP Integration
48
50
 
49
- Add to your Claude Desktop config (`claude_desktop_config.json`):
50
-
51
- ### If installed globally via bun:
51
+ Add to your MCP client's configuration:
52
52
 
53
53
  ```json
54
54
  {
@@ -56,14 +56,25 @@ Add to your Claude Desktop config (`claude_desktop_config.json`):
56
56
  "tana": {
57
57
  "command": "tana-mcp-codemode",
58
58
  "env": {
59
- "TANA_API_TOKEN": "your_token_here"
59
+ "TANA_API_TOKEN": "your_token_here",
60
+ // Optional
61
+ "MAIN_TANA_WORKSPACE": "My Workspace",
62
+ "TANA_SEARCH_WORKSPACES": "My Workspace",
63
+ // Optional: customize where the SQLite history database is stored
64
+ "TANA_HISTORY_PATH": "/path/to/history.db"
60
65
  }
61
66
  }
62
67
  }
63
68
  }
64
69
  ```
65
70
 
66
- ### If installed from source:
71
+ | Client | Config Location |
72
+ | ----------------- | ----------------------------------------------------------- |
73
+ | Claude Desktop | `claude_desktop_config.json` |
74
+ | Claude Code | `.mcp.json` (project) or `~/.claude/settings.json` (global) |
75
+ | Cursor / Windsurf | IDE MCP settings |
76
+
77
+ **If installed from source**, use `bun` as the command:
67
78
 
68
79
  ```json
69
80
  {
@@ -72,108 +83,18 @@ Add to your Claude Desktop config (`claude_desktop_config.json`):
72
83
  "command": "bun",
73
84
  "args": ["run", "/path/to/tana-mcp-codemode/src/index.ts"],
74
85
  "env": {
75
- "TANA_API_TOKEN": "your_token_here"
86
+ "TANA_API_TOKEN": "your_token_here",
87
+ // Optional
88
+ "MAIN_TANA_WORKSPACE": "My Workspace",
89
+ "TANA_SEARCH_WORKSPACES": "My Workspace",
90
+ // Optional: customize where the SQLite history database is stored
91
+ "TANA_HISTORY_PATH": "/path/to/history.db"
76
92
  }
77
93
  }
78
94
  }
79
95
  }
80
96
  ```
81
97
 
82
- ## API Reference
83
-
84
- The server exposes a single `execute` tool. AI writes TypeScript code with access to the `tana` object:
85
-
86
- ### Workspaces
87
-
88
- ```typescript
89
- const workspaces = await tana.workspaces.list();
90
- // → Workspace[] (id, name, homeNodeId)
91
- ```
92
-
93
- ### Nodes
94
-
95
- ```typescript
96
- await tana.nodes.search(query, options?) // → SearchResult[]
97
- await tana.nodes.read(nodeId, maxDepth?) // → string (markdown)
98
- await tana.nodes.getChildren(nodeId, opts?) // → { children, total, hasMore }
99
- await tana.nodes.edit({ nodeId, name?, description? })
100
- await tana.nodes.trash(nodeId)
101
- await tana.nodes.check(nodeId) // mark done
102
- await tana.nodes.uncheck(nodeId) // mark undone
103
- ```
104
-
105
- ### Tags (Supertags)
106
-
107
- ```typescript
108
- await tana.tags.list(workspaceId, limit?)
109
- await tana.tags.getSchema(tagId, includeEditInstructions?)
110
- await tana.tags.modify(nodeId, action, tagIds) // action: "add" | "remove"
111
- await tana.tags.create({ workspaceId, name, description?, extendsTagIds?, showCheckbox? })
112
- await tana.tags.addField({ tagId, name, dataType, ... })
113
- await tana.tags.setCheckbox({ tagId, showCheckbox, doneStateMapping? })
114
- ```
115
-
116
- ### Fields
117
-
118
- ```typescript
119
- await tana.fields.setOption(nodeId, attributeId, optionId) // dropdown fields
120
- await tana.fields.setContent(nodeId, attributeId, content) // text/date/url fields
121
- ```
122
-
123
- ### Calendar
124
-
125
- ```typescript
126
- await tana.calendar.getOrCreate(workspaceId, granularity, date?)
127
- // granularity: "day" | "week" | "month" | "year"
128
- ```
129
-
130
- ### Import (Tana Paste)
131
-
132
- ```typescript
133
- await tana.import(parentNodeId, tanaPasteContent)
134
- // → { success, nodeIds?, error? }
135
- ```
136
-
137
- ### Utility
138
-
139
- ```typescript
140
- await tana.health() // → { status: "ok" }
141
- ```
142
-
143
- ## Script Helpers
144
-
145
- ### stdin() — Input Data
146
-
147
- Pass data to scripts via the `input` parameter:
148
-
149
- ```typescript
150
- // Parse JSON input
151
- const data = stdin().json<{ ids: string[] }>();
152
-
153
- // Split into lines
154
- const lines = stdin().lines();
155
-
156
- // Raw text
157
- const text = stdin().text();
158
-
159
- // Check if input exists
160
- if (stdin().hasInput()) {
161
- // process input
162
- }
163
- ```
164
-
165
- ### workflow — Progress Tracking
166
-
167
- Track multi-step operations:
168
-
169
- ```typescript
170
- workflow.start("Processing nodes");
171
- workflow.step("Fetching workspaces");
172
- workflow.progress(5, 100, "Processing");
173
- workflow.complete("Done!");
174
- // Or: workflow.abort("Error message");
175
- ```
176
-
177
98
  ## Examples
178
99
 
179
100
  ### Search for nodes
@@ -208,16 +129,6 @@ await tana.import(parentNodeId, `
208
129
  `);
209
130
  ```
210
131
 
211
- ### Process input data
212
-
213
- ```typescript
214
- const { nodeIds } = stdin().json<{ nodeIds: string[] }>();
215
- for (const id of nodeIds) {
216
- const content = await tana.nodes.read(id);
217
- console.log(content);
218
- }
219
- ```
220
-
221
132
  ## Debug UI
222
133
 
223
134
  A WebSocket-based dashboard for testing scripts:
@@ -229,11 +140,29 @@ bun run src/debug-server.ts
229
140
  # Open http://localhost:3333
230
141
  ```
231
142
 
232
- Features:
143
+ **Routes:**
144
+ - `http://localhost:3333/#debug` — Script execution console
145
+ - `http://localhost:3333/#benchmark` — Codemode vs tana-local performance comparison
146
+
147
+ **Features:**
233
148
  - Real-time script execution
234
149
  - Workflow event timeline
235
- - Input data testing
236
150
  - Error display with suggestions
151
+ - Benchmark results visualization (cost, speed, token usage)
152
+
153
+ ### workflow Helper (Debug UI only)
154
+
155
+ Track multi-step operations with a timeline view:
156
+
157
+ ```typescript
158
+ workflow.start("Processing nodes");
159
+ workflow.step("Fetching workspaces");
160
+ workflow.progress(5, 100, "Processing");
161
+ workflow.complete("Done!");
162
+ // Or: workflow.abort("Error message");
163
+ ```
164
+
165
+ > **Note**: `workflow` is only available in the Debug UI. It's not exposed in the production MCP prompt.
237
166
 
238
167
  ### Building the React UI (optional)
239
168
 
@@ -259,7 +188,6 @@ src/
259
188
  │ └── types.ts # API type definitions
260
189
  ├── sandbox/
261
190
  │ ├── executor.ts # Code execution engine
262
- │ ├── stdin.ts # Input data helper
263
191
  │ └── workflow.ts # Progress tracking
264
192
  ├── storage/
265
193
  │ └── history.ts # SQLite script history
@@ -272,8 +200,8 @@ ui/ # React debug dashboard
272
200
  ### How It Works
273
201
 
274
202
  1. AI sends TypeScript code to the `execute` tool
275
- 2. `sandbox.ts` creates an AsyncFunction for top-level await
276
- 3. Code runs with injected `tana`, `console`, `stdin`, `workflow` objects
203
+ 2. `executor.ts` creates an AsyncFunction for top-level await
204
+ 3. Code runs with injected `tana` and `console` objects
277
205
  4. 10s timeout via Promise.race prevents hangs
278
206
  5. `console.log()` output is captured and returned
279
207
  6. Script run is saved to SQLite history
@@ -290,38 +218,19 @@ Runs are persisted to SQLite:
290
218
 
291
219
  Old runs (>30 days) are automatically cleaned up on startup.
292
220
 
293
- ### Custom History Location
294
-
295
- Set `TANA_HISTORY_PATH` to use a custom database path:
296
-
297
- ```json
298
- {
299
- "mcpServers": {
300
- "tana": {
301
- "command": "tana-mcp-codemode",
302
- "env": {
303
- "TANA_API_TOKEN": "your_token_here",
304
- "TANA_HISTORY_PATH": "/path/to/custom/tana-history.db"
305
- }
306
- }
307
- }
308
- }
309
- ```
310
-
311
221
  ### What Gets Saved
312
222
 
313
223
  Each script run records:
314
224
 
315
- | Field | Description |
316
- |-------|-------------|
317
- | `script` | The TypeScript code that was executed |
318
- | `input` | Data passed via `stdin()` helper |
319
- | `output` | Captured `console.log()` output |
320
- | `error` | Error message if execution failed |
321
- | `api_calls` | Which Tana API methods were called |
322
- | `node_ids_affected` | Node IDs that were read/modified |
323
- | `workspace_id` | Workspace used (if detected) |
324
- | `duration_ms` | Execution time |
225
+ | Field | Description |
226
+ | ------------------- | ------------------------------------- |
227
+ | `script` | The TypeScript code that was executed |
228
+ | `output` | Captured `console.log()` output |
229
+ | `error` | Error message if execution failed |
230
+ | `api_calls` | Which Tana API methods were called |
231
+ | `node_ids_affected` | Node IDs that were read/modified |
232
+ | `workspace_id` | Workspace used (if detected) |
233
+ | `duration_ms` | Execution time |
325
234
 
326
235
  ## Development
327
236
 
@@ -339,6 +248,69 @@ bun run generate
339
248
  bun run debug
340
249
  ```
341
250
 
251
+ ## API Reference
252
+
253
+ The server exposes a single `execute` tool. AI writes TypeScript code with access to the `tana` object:
254
+
255
+ ### Workspaces
256
+
257
+ ```typescript
258
+ const workspaces = await tana.workspaces.list();
259
+ // → Workspace[] (id, name, homeNodeId)
260
+ ```
261
+
262
+ ### Nodes
263
+
264
+ ```typescript
265
+ await tana.nodes.search(query, options?) // → SearchResult[]
266
+ await tana.nodes.read(nodeId, maxDepth?) // → string (markdown)
267
+ await tana.nodes.getChildren(nodeId, opts?) // → { children, total, hasMore }
268
+ await tana.nodes.edit({ nodeId, name?, description? })
269
+ await tana.nodes.trash(nodeId)
270
+ await tana.nodes.check(nodeId) // mark done
271
+ await tana.nodes.uncheck(nodeId) // mark undone
272
+ ```
273
+
274
+ ### Tags (Supertags)
275
+
276
+ ```typescript
277
+ await tana.tags.listAll(workspaceId) // All workspace supertags (paginated)
278
+ await tana.tags.getSchema(tagId, opts?) // opts: { includeInheritedFields? }
279
+ await tana.tags.modify(nodeId, action, tagIds) // action: "add" | "remove"
280
+ await tana.tags.create({ workspaceId, name, description?, extendsTagIds?, showCheckbox? })
281
+ await tana.tags.addField({ tagId, name, dataType, ... })
282
+ await tana.tags.setCheckbox({ tagId, showCheckbox, doneStateMapping? })
283
+ ```
284
+
285
+ ### Fields
286
+
287
+ ```typescript
288
+ await tana.fields.setOption(nodeId, attributeId, optionId) // dropdown fields
289
+ await tana.fields.setContent(nodeId, attributeId, content) // text/date/url fields
290
+ ```
291
+
292
+ ### Calendar
293
+
294
+ ```typescript
295
+ await tana.calendar.getOrCreate(workspaceId, granularity, date?)
296
+ // granularity: "day" | "week" | "month" | "year"
297
+ ```
298
+
299
+ ### Import (Tana Paste)
300
+
301
+ ```typescript
302
+ await tana.import(parentNodeId, tanaPasteContent)
303
+ // → { success, nodeIds?, error? }
304
+ ```
305
+
306
+ ### Utility
307
+
308
+ ```typescript
309
+ tana.workspace // Pre-resolved default workspace (from MAIN_TANA_WORKSPACE) or null
310
+ tana.format(data) // Compact display of any API response
311
+ await tana.health() // → { status: "ok" }
312
+ ```
313
+
342
314
  ## License
343
315
 
344
316
  MIT
package/package.json CHANGED
@@ -1,21 +1,12 @@
1
1
  {
2
2
  "name": "tana-mcp-codemode",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Codemode MCP server for Tana — AI writes TypeScript that executes against Tana Local API",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "bin": {
8
8
  "tana-mcp-codemode": "src/index.ts"
9
9
  },
10
- "scripts": {
11
- "generate": "bun run scripts/generate.ts",
12
- "list-endpoints": "bun run scripts/list-endpoints.ts",
13
- "start": "bun run --env-file=.env src/index.ts",
14
- "dev": "bun run --env-file=.env --watch src/index.ts",
15
- "debug": "bun run --env-file=.env src/debug-server.ts",
16
- "typecheck": "bunx tsc --noEmit",
17
- "prepublishOnly": "bun run typecheck"
18
- },
19
10
  "files": [
20
11
  "src/index.ts",
21
12
  "src/prompts.ts",
@@ -36,7 +27,7 @@
36
27
  ],
37
28
  "repository": {
38
29
  "type": "git",
39
- "url": "https://github.com/fabiogaliano/tana-mcp-codemode"
30
+ "url": "git+https://github.com/fabiogaliano/tana-mcp-codemode.git"
40
31
  },
41
32
  "engines": {
42
33
  "bun": ">=1.0.0"
@@ -50,5 +41,14 @@
50
41
  "openapi-typescript": "^7.10.1",
51
42
  "typescript": "^5.9.3"
52
43
  },
53
- "license": "MIT"
54
- }
44
+ "license": "MIT",
45
+ "scripts": {
46
+ "generate": "bun run scripts/generate.ts",
47
+ "list-endpoints": "bun run scripts/list-endpoints.ts",
48
+ "start": "bun run --env-file=.env src/index.ts",
49
+ "dev": "bun run --env-file=.env --watch src/index.ts",
50
+ "debug": "bun run --env-file=.env src/debug-server.ts",
51
+ "test": "bun test",
52
+ "typecheck": "bunx tsc --noEmit"
53
+ }
54
+ }
package/src/api/client.ts CHANGED
@@ -18,8 +18,8 @@ export class TanaAPIError extends Error {
18
18
  }
19
19
 
20
20
  const RETRY_CONFIG = {
21
- maxRetries: 3,
22
- baseDelayMs: 1000,
21
+ maxRetries: 2,
22
+ baseDelayMs: 500,
23
23
  retryableCodes: new Set([408, 429, 500, 502, 503, 504]),
24
24
  };
25
25
 
@@ -230,7 +230,7 @@ export class TanaClient {
230
230
  export function createClient(): TanaClient {
231
231
  const baseUrl = process.env.TANA_API_URL || "http://127.0.0.1:8262";
232
232
  const token = process.env.TANA_API_TOKEN;
233
- const timeout = parseInt(process.env.TANA_TIMEOUT || "10000", 10);
233
+ const timeout = parseInt(process.env.TANA_TIMEOUT || "3000", 10);
234
234
 
235
235
  if (!token) {
236
236
  throw new Error(
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Compact Formatter for Tana API Responses
3
+ *
4
+ * Pure function — detects response shape and returns a compact string representation.
5
+ * Designed to reduce token waste when AI logs API results via console.log(tana.format(data)).
6
+ *
7
+ * Shape detection uses discriminating fields from the OpenAPI spec:
8
+ * - SearchResult[]: has `breadcrumb`
9
+ * - Children: has `children` + `total` + `hasMore`
10
+ * - ChildNode[]: has `childCount`
11
+ * - Tag[]: has `id` + `name`, no `breadcrumb`/`childCount`/`homeNodeId`
12
+ * - Workspace[]: has `homeNodeId`
13
+ * - ImportResult: has `success` + (`nodeIds` or `error`)
14
+ * - string: passthrough (read/getSchema already return markdown)
15
+ */
16
+
17
+ function hasKey(obj: unknown, key: string): boolean {
18
+ return typeof obj === "object" && obj !== null && key in obj;
19
+ }
20
+
21
+ function formatTags(tags: { id: string; name: string }[]): string {
22
+ if (!tags || tags.length === 0) return "";
23
+ return " " + tags.map((t) => `#${t.name}`).join(" ");
24
+ }
25
+
26
+ function formatSearchResults(data: unknown[]): string {
27
+ const lines = data.map((r: any) => {
28
+ const tags = formatTags(r.tags);
29
+ const breadcrumb = r.breadcrumb?.length ? ` — ${r.breadcrumb.join(" > ")}` : "";
30
+ return ` [${r.id}] ${r.name}${tags}${breadcrumb}`;
31
+ });
32
+ return `${data.length} results:\n${lines.join("\n")}`;
33
+ }
34
+
35
+ function formatChildren(data: any): string {
36
+ const showing = data.children.length;
37
+ const header = `${data.total} children (showing ${showing}${data.hasMore ? ", has more" : ""}):`;
38
+ const lines = data.children.map((c: any) => {
39
+ const tags = formatTags(c.tags);
40
+ const kids = c.childCount > 0 ? ` (${c.childCount} children)` : "";
41
+ return ` [${c.id}] ${c.name}${tags}${kids}`;
42
+ });
43
+ return `${header}\n${lines.join("\n")}`;
44
+ }
45
+
46
+ function formatChildNodes(data: unknown[]): string {
47
+ const lines = data.map((c: any) => {
48
+ const tags = formatTags(c.tags);
49
+ const kids = c.childCount > 0 ? ` (${c.childCount} children)` : "";
50
+ return ` [${c.id}] ${c.name}${tags}${kids}`;
51
+ });
52
+ return `${data.length} nodes:\n${lines.join("\n")}`;
53
+ }
54
+
55
+ function formatTags_(data: unknown[]): string {
56
+ const lines = data.map((t: any) => ` [${t.id}] ${t.name}`);
57
+ return `${data.length} tags:\n${lines.join("\n")}`;
58
+ }
59
+
60
+ function formatWorkspaces(data: unknown[]): string {
61
+ const lines = data.map((w: any) => ` [${w.id}] ${w.name} (home: ${w.homeNodeId})`);
62
+ return `${data.length} workspaces:\n${lines.join("\n")}`;
63
+ }
64
+
65
+ function formatImportResult(data: any): string {
66
+ if (data.success) {
67
+ const ids = data.nodeIds?.length ? ` (${data.nodeIds.join(", ")})` : "";
68
+ return `Import success: ${data.nodeIds?.length ?? 0} nodes${ids}`;
69
+ }
70
+ return `Import failed: ${data.error ?? "unknown error"}`;
71
+ }
72
+
73
+ /**
74
+ * Compact formatter for any Tana API response.
75
+ *
76
+ * Detects the shape of the data and returns a human-readable, token-efficient string.
77
+ * Falls back to JSON.stringify for unrecognized shapes.
78
+ */
79
+ export function format(data: unknown): string {
80
+ if (typeof data === "string") return data;
81
+ if (data === null || data === undefined) return String(data);
82
+
83
+ // ImportResult: { success, nodeIds?, error? }
84
+ if (hasKey(data, "success") && (hasKey(data, "nodeIds") || hasKey(data, "error"))) {
85
+ return formatImportResult(data);
86
+ }
87
+
88
+ // Children: { children, total, hasMore }
89
+ if (hasKey(data, "children") && hasKey(data, "total") && hasKey(data, "hasMore")) {
90
+ return formatChildren(data);
91
+ }
92
+
93
+ if (Array.isArray(data)) {
94
+ if (data.length === 0) return "0 results";
95
+
96
+ const first = data[0];
97
+
98
+ // SearchResult[]: has `breadcrumb`
99
+ if (hasKey(first, "breadcrumb")) {
100
+ return formatSearchResults(data);
101
+ }
102
+
103
+ // ChildNode[]: has `childCount`
104
+ if (hasKey(first, "childCount")) {
105
+ return formatChildNodes(data);
106
+ }
107
+
108
+ // Workspace[]: has `homeNodeId`
109
+ if (hasKey(first, "homeNodeId")) {
110
+ return formatWorkspaces(data);
111
+ }
112
+
113
+ // Tag[]: has `id` + `name`, no discriminators for other types
114
+ if (hasKey(first, "id") && hasKey(first, "name") && !hasKey(first, "breadcrumb") && !hasKey(first, "childCount") && !hasKey(first, "homeNodeId")) {
115
+ return formatTags_(data);
116
+ }
117
+ }
118
+
119
+ // Fallback: pretty JSON
120
+ try {
121
+ return JSON.stringify(data, null, 2);
122
+ } catch {
123
+ return String(data);
124
+ }
125
+ }
package/src/api/tana.ts CHANGED
@@ -22,6 +22,7 @@ import type {
22
22
  SetCheckboxOptions,
23
23
  ImportResult,
24
24
  } from "./types";
25
+ import { format } from "./format";
25
26
 
26
27
  /**
27
28
  * TanaAPI Interface
@@ -30,6 +31,9 @@ import type {
30
31
  * Provides high-level methods organized by domain (workspaces, nodes, tags, etc.)
31
32
  */
32
33
  export interface TanaAPI {
34
+ /** Pre-resolved default workspace (from MAIN_TANA_WORKSPACE env var), or null */
35
+ workspace: Workspace | null;
36
+
33
37
  /** Check API health */
34
38
  health(): Promise<{ status: string; timestamp: string; nodeSpaceReady: boolean }>;
35
39
 
@@ -59,10 +63,10 @@ export interface TanaAPI {
59
63
  };
60
64
 
61
65
  tags: {
62
- /** List tags in a workspace */
63
- list(workspaceId: string, limit?: number): Promise<Tag[]>;
66
+ /** List all tags in a workspace */
67
+ listAll(workspaceId: string): Promise<Tag[]>;
64
68
  /** Get tag schema */
65
- getSchema(tagId: string, includeEditInstructions?: boolean): Promise<string>;
69
+ getSchema(tagId: string, includeEditInstructions?: boolean, includeInheritedFields?: boolean): Promise<string>;
66
70
  /** Add/remove tags from a node */
67
71
  modify(
68
72
  nodeId: string,
@@ -103,10 +107,19 @@ export interface TanaAPI {
103
107
 
104
108
  /** Import Tana Paste formatted content */
105
109
  import(parentNodeId: string, content: string): Promise<ImportResult>;
110
+
111
+ /** Compact formatter for any API response */
112
+ format(data: unknown): string;
106
113
  }
107
114
 
108
- export function createTanaAPI(client: TanaClient): TanaAPI {
115
+ export function createTanaAPI(
116
+ client: TanaClient,
117
+ workspace?: Workspace | null,
118
+ defaultSearchWorkspaceIds?: string[]
119
+ ): TanaAPI {
109
120
  return {
121
+ workspace: workspace ?? null,
122
+
110
123
  async health() {
111
124
  return client.get<{ status: string; timestamp: string; nodeSpaceReady: boolean }>("/health");
112
125
  },
@@ -147,8 +160,10 @@ export function createTanaAPI(client: TanaClient): TanaAPI {
147
160
 
148
161
  addQueryParams(query as unknown as Record<string, unknown>, "query");
149
162
  if (options?.limit) params.push(`limit=${options.limit}`);
150
- if (options?.workspaceIds) {
151
- options.workspaceIds.forEach((id, i) => {
163
+ const effectiveWorkspaceIds = options?.workspaceIds
164
+ ?? (defaultSearchWorkspaceIds?.length ? defaultSearchWorkspaceIds : undefined);
165
+ if (effectiveWorkspaceIds) {
166
+ effectiveWorkspaceIds.forEach((id, i) => {
152
167
  params.push(`workspaceIds[${i}]=${encodeURIComponent(id)}`);
153
168
  });
154
169
  }
@@ -213,20 +228,36 @@ export function createTanaAPI(client: TanaClient): TanaAPI {
213
228
  },
214
229
 
215
230
  tags: {
216
- async list(workspaceId: string, limit = 50): Promise<Tag[]> {
217
- return client.get<Tag[]>(
218
- `/workspaces/${workspaceId}/tags?limit=${limit}`
219
- );
231
+ async listAll(workspaceId: string): Promise<Tag[]> {
232
+ const schemaNodeId = `${workspaceId}_SCHEMA`;
233
+ const tags: Tag[] = [];
234
+ let offset = 0;
235
+
236
+ while (true) {
237
+ const page = await client.get<Children>(
238
+ `/nodes/${schemaNodeId}/children?limit=200&offset=${offset}`
239
+ );
240
+ for (const child of page.children) {
241
+ if (child.docType === "tagDef") {
242
+ tags.push({ id: child.id, name: child.name });
243
+ }
244
+ }
245
+ if (!page.hasMore) break;
246
+ offset += 200;
247
+ }
248
+
249
+ return tags;
220
250
  },
221
251
 
222
252
  async getSchema(
223
253
  tagId: string,
224
- includeEditInstructions = false
254
+ includeEditInstructions = false,
255
+ includeInheritedFields = true
225
256
  ): Promise<string> {
226
257
  const result = await client.get<{ markdown: string }>(
227
- `/tags/${tagId}/schema?includeEditInstructions=${includeEditInstructions}`
258
+ `/tags/${tagId}/schema?includeEditInstructions=${includeEditInstructions}&includeInheritedFields=${includeInheritedFields}`
228
259
  );
229
- return result.markdown;
260
+ return truncateOptionLists(result.markdown);
230
261
  },
231
262
 
232
263
  async modify(
@@ -328,5 +359,45 @@ export function createTanaAPI(client: TanaClient): TanaAPI {
328
359
  content,
329
360
  });
330
361
  },
362
+
363
+ format(data: unknown): string {
364
+ return format(data);
365
+ },
331
366
  };
332
367
  }
368
+
369
+ const MAX_OPTIONS_SHOWN = 5;
370
+ const OPTION_LINE_RE = /^ - .+ \(id:/;
371
+
372
+ function truncateOptionLists(markdown: string): string {
373
+ const lines = markdown.split("\n");
374
+ const result: string[] = [];
375
+ let optionCount = 0;
376
+ let inOptions = false;
377
+
378
+ for (const line of lines) {
379
+ if (OPTION_LINE_RE.test(line)) {
380
+ if (!inOptions) {
381
+ inOptions = true;
382
+ optionCount = 0;
383
+ }
384
+ optionCount++;
385
+ if (optionCount <= MAX_OPTIONS_SHOWN) {
386
+ result.push(line);
387
+ }
388
+ } else {
389
+ if (inOptions && optionCount > MAX_OPTIONS_SHOWN) {
390
+ result.push(` - ... (${optionCount - MAX_OPTIONS_SHOWN} more, ${optionCount} total)`);
391
+ }
392
+ inOptions = false;
393
+ optionCount = 0;
394
+ result.push(line);
395
+ }
396
+ }
397
+
398
+ if (inOptions && optionCount > MAX_OPTIONS_SHOWN) {
399
+ result.push(` - ... (${optionCount - MAX_OPTIONS_SHOWN} more, ${optionCount} total)`);
400
+ }
401
+
402
+ return result.join("\n");
403
+ }
package/src/api/types.ts CHANGED
@@ -8,10 +8,6 @@
8
8
  */
9
9
  import type { operations } from "../generated/api";
10
10
 
11
- // ============================================================================
12
- // Response Types (extracted from operation responses)
13
- // ============================================================================
14
-
15
11
  /** Health check response */
16
12
  export type HealthResponse =
17
13
  operations["health.ping"]["responses"]["200"]["content"]["application/json"];
@@ -87,10 +83,6 @@ export type SetFieldOptionResponse =
87
83
  export type SetFieldContentResponse =
88
84
  operations["nodes.setFieldContent"]["responses"]["200"]["content"]["application/json"];
89
85
 
90
- // ============================================================================
91
- // Request/Query Types (extracted from operation parameters)
92
- // ============================================================================
93
-
94
86
  /** Search query structure */
95
87
  export type SearchQuery = NonNullable<
96
88
  operations["nodes.search"]["parameters"]["query"]
@@ -106,10 +98,6 @@ export type FieldDataType = NonNullable<
106
98
  operations["tags.addField"]["requestBody"]
107
99
  >["content"]["application/json"]["dataType"];
108
100
 
109
- // ============================================================================
110
- // Request Body Types
111
- // ============================================================================
112
-
113
101
  /** Create tag request body */
114
102
  export type CreateTagBody = NonNullable<
115
103
  operations["tags.create"]["requestBody"]
@@ -135,10 +123,6 @@ export type NodeUpdateBody = NonNullable<
135
123
  operations["nodes.update"]["requestBody"]
136
124
  >["content"]["application/json"];
137
125
 
138
- // ============================================================================
139
- // Derived Helper Types (for ergonomic API usage)
140
- // ============================================================================
141
-
142
126
  /** Options for search */
143
127
  export interface SearchOptions {
144
128
  limit?: number;
@@ -199,9 +183,5 @@ export interface ImportResult {
199
183
  error?: string;
200
184
  }
201
185
 
202
- // ============================================================================
203
- // Re-export full generated types for escape hatch
204
- // ============================================================================
205
-
206
186
  export type { operations } from "../generated/api";
207
187
  export type { paths } from "../generated/api";
package/src/index.ts CHANGED
@@ -12,29 +12,114 @@ import { z } from "zod";
12
12
 
13
13
  import { createClient } from "./api/client";
14
14
  import { createTanaAPI } from "./api/tana";
15
+ import type { TanaClient } from "./api/client";
16
+ import type { Workspace } from "./api/types";
15
17
  import { executeSandbox } from "./sandbox/executor";
16
18
  import { cleanupOldRuns, initDb } from "./storage/history";
17
19
  import { TOOL_DESCRIPTION } from "./prompts";
18
20
 
21
+ /**
22
+ * Resolves the default workspace from MAIN_TANA_WORKSPACE env var.
23
+ * Matches by ID first, then case-insensitive name.
24
+ * Returns null if unset, unmatched, or API unreachable.
25
+ */
26
+ async function resolveWorkspace(client: TanaClient): Promise<Workspace | null> {
27
+ const envValue = process.env.MAIN_TANA_WORKSPACE?.trim();
28
+ if (!envValue) return null;
29
+
30
+ let workspaces: Workspace[];
31
+ try {
32
+ workspaces = await client.get<Workspace[]>("/workspaces");
33
+ } catch (err) {
34
+ console.error(
35
+ `[workspace] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
36
+ );
37
+ return null;
38
+ }
39
+
40
+ const byId = workspaces.find((w) => w.id === envValue);
41
+ if (byId) {
42
+ console.error(`[workspace] Resolved "${envValue}" → ${byId.name} (${byId.id})`);
43
+ return byId;
44
+ }
45
+
46
+ const lowerEnv = envValue.toLowerCase();
47
+ const byName = workspaces.find((w) => w.name.toLowerCase() === lowerEnv);
48
+ if (byName) {
49
+ console.error(`[workspace] Resolved "${envValue}" → ${byName.name} (${byName.id})`);
50
+ return byName;
51
+ }
52
+
53
+ const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
54
+ console.error(
55
+ `[workspace] No match for "${envValue}". Available: ${available || "none"}`
56
+ );
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Resolves TANA_SEARCH_WORKSPACES env var to an array of workspace IDs.
62
+ * Each value can be an ID or case-insensitive name.
63
+ * Unresolved values are logged and skipped.
64
+ */
65
+ async function resolveSearchWorkspaces(client: TanaClient): Promise<string[]> {
66
+ const envValue = process.env.TANA_SEARCH_WORKSPACES?.trim();
67
+ if (!envValue) return [];
68
+
69
+ const values = envValue.split(",").map((v) => v.trim()).filter(Boolean);
70
+ if (values.length === 0) return [];
71
+
72
+ let workspaces: Workspace[];
73
+ try {
74
+ workspaces = await client.get<Workspace[]>("/workspaces");
75
+ } catch (err) {
76
+ console.error(
77
+ `[search-workspaces] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
78
+ );
79
+ return [];
80
+ }
81
+
82
+ const resolvedIds: string[] = [];
83
+ for (const value of values) {
84
+ const byId = workspaces.find((w) => w.id === value);
85
+ if (byId) {
86
+ resolvedIds.push(byId.id);
87
+ console.error(`[search-workspaces] Resolved "${value}" → ${byId.name} (${byId.id})`);
88
+ continue;
89
+ }
90
+ const lowerValue = value.toLowerCase();
91
+ const byName = workspaces.find((w) => w.name.toLowerCase() === lowerValue);
92
+ if (byName) {
93
+ resolvedIds.push(byName.id);
94
+ console.error(`[search-workspaces] Resolved "${value}" → ${byName.name} (${byName.id})`);
95
+ continue;
96
+ }
97
+ const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
98
+ console.error(
99
+ `[search-workspaces] No match for "${value}". Available: ${available || "none"}`
100
+ );
101
+ }
102
+
103
+ return resolvedIds;
104
+ }
105
+
19
106
  async function main() {
20
- // Initialize database and cleanup old runs
21
107
  initDb();
22
108
  const cleaned = cleanupOldRuns(30);
23
109
  if (cleaned > 0) {
24
110
  console.error(`Cleaned up ${cleaned} old script runs`);
25
111
  }
26
112
 
27
- // Create Tana client and API
28
113
  const client = createClient();
29
- const tana = createTanaAPI(client);
114
+ const workspace = await resolveWorkspace(client);
115
+ const searchWorkspaceIds = await resolveSearchWorkspaces(client);
116
+ const tana = createTanaAPI(client, workspace, searchWorkspaceIds);
30
117
 
31
- // Create MCP server using the modern McpServer API
32
118
  const server = new McpServer({
33
119
  name: "tana-mcp-codemode",
34
120
  version: "0.1.0",
35
121
  });
36
122
 
37
- // Register the execute tool using registerTool (non-deprecated API)
38
123
  server.registerTool(
39
124
  "execute",
40
125
  {
@@ -45,16 +130,11 @@ async function main() {
45
130
  .string()
46
131
  .optional()
47
132
  .describe("Optional session ID for grouping script runs"),
48
- input: z
49
- .string()
50
- .optional()
51
- .describe("Data to pass to script via stdin() helper"),
52
133
  },
53
134
  },
54
- async ({ code, sessionId, input }) => {
55
- const result = await executeSandbox(code, tana, sessionId, input);
135
+ async ({ code, sessionId }) => {
136
+ const result = await executeSandbox(code, tana, sessionId);
56
137
 
57
- // Format response
58
138
  let responseText = "";
59
139
  if (result.output) {
60
140
  responseText += result.output;
@@ -77,7 +157,6 @@ async function main() {
77
157
  }
78
158
  );
79
159
 
80
- // Start server with stdio transport
81
160
  const transport = new StdioServerTransport();
82
161
  await server.connect(transport);
83
162
 
package/src/prompts.ts CHANGED
@@ -6,7 +6,7 @@ export const TOOL_DESCRIPTION = `Execute TypeScript code to interact with Tana.
6
6
 
7
7
  ## APIs
8
8
 
9
- tana.health(){ status }
9
+ tana.workspaceWorkspace | null (pre-resolved default workspace)
10
10
  tana.workspaces.list() → Workspace[] { id, name, homeNodeId }
11
11
 
12
12
  ### Nodes
@@ -18,8 +18,8 @@ tana.nodes.trash(nodeId) → { success }
18
18
  tana.nodes.check(nodeId) / uncheck(nodeId) → { success }
19
19
 
20
20
  ### Tags
21
- tana.tags.list(workspaceId, limit?) → Tag[]
22
- tana.tags.getSchema(tagId, includeEditInstructions?) → string
21
+ tana.tags.listAll(workspaceId) → Tag[] (workspace supertags only — for schema analysis)
22
+ tana.tags.getSchema(tagId) → string
23
23
  tana.tags.modify(nodeId, "add"|"remove", tagIds[])
24
24
  tana.tags.create({ workspaceId, name, description?, extendsTagIds?, showCheckbox? })
25
25
  tana.tags.addField({ tagId, name, dataType: "plain"|"number"|"date"|"url"|"email"|"checkbox"|"user"|"instance"|"options", ... })
@@ -35,13 +35,10 @@ tana.calendar.getOrCreate(workspaceId, "day"|"week"|"month"|"year", date?)
35
35
  ### Import
36
36
  tana.import(parentNodeId, tanaPasteContent) → { success, nodeIds? }
37
37
 
38
- ### Entry Points
39
- inbox: \`\${workspaceId}_CAPTURE_INBOX\`
40
- library: \`\${workspaceId}_STASH\`
38
+ ### Formatting
39
+ tana.format(data) → string (compact display of any API response)
41
40
 
42
- ### Helpers
43
- stdin().json<T>() | .lines() | .text() | .hasInput()
44
- workflow.start(msg) | .step(msg) | .progress(n, total, msg) | .complete(msg) | .abort(err)
41
+ Entry points: \`\${workspaceId}_CAPTURE_INBOX\` (inbox), \`\${workspaceId}_STASH\` (library)
45
42
 
46
43
  ## Edit (search-and-replace)
47
44
 
@@ -53,15 +50,35 @@ Empty old_string matches absent field.
53
50
  { textContains: string } | { textMatches: "/regex/i" }
54
51
  { hasType: tagId } | { field: { fieldId, stringValue?, numberValue?, nodeId?, state? } }
55
52
  { compare: { fieldId, operator: "gt"|"lt"|"eq", value, type } }
56
- { childOf: { nodeIds[], recursive?, includeRefs? } } | { ownedBy: { nodeId, recursive?, includeSelf? } }
57
53
  { linksTo: nodeIds[] }
58
- { is: "done"|"todo"|"template"|"entity"|"calendarNode"|"search"|"inLibrary" }
54
+ { is: "done"|"todo"|"template"|"field"|"published"|"entity"|"calendarNode"|"onDayNode"|"chat"|"search"|"command"|"inLibrary" }
59
55
  { has: "tag"|"field"|"media"|"audio"|"video"|"image" }
60
56
  { created: { last: N } } | { edited: { last?, by?, since? } } | { done: { last: N } }
61
57
  { onDate: "YYYY-MM-DD" | { date, fieldId?, overlaps? } }
62
58
  { overdue: true } | { inLibrary: true }
63
59
  { and: [...] } | { or: [...] } | { not: {...} }
64
60
 
61
+ ## Default Workspace
62
+
63
+ tana.workspace is pre-resolved if MAIN_TANA_WORKSPACE is set. Prefer over workspaces.list(): \`const ws = tana.workspace ?? (await tana.workspaces.list())[0];\`
64
+
65
+ ## Output
66
+
67
+ console.log() output becomes LLM context. Keep it compact:
68
+ - Use tana.format(data) for any API response, or .map() for task-specific fields
69
+ - search returns metadata only (name, id, tags); use .read() for content and field values
70
+ - Never JSON.stringify API responses
71
+
72
+ ## API Notes
73
+
74
+ - search: no offset/pagination — use narrower queries, not repeated calls
75
+ - getChildren: only endpoint with pagination (limit + offset)
76
+ - Timeout is 10s. On timeout, try a different approach, not the same call.
77
+ - childOf/ownedBy/inWorkspace operators broken. Scope by workspace: search(query, { workspaceIds: ["id"] })
78
+ - Tag names are not unique. Find a tag by name: search({ and: [{ hasType: "SYS_T01" }, { textContains: "name" }] })
79
+ - search results: { id, name, breadcrumb[], tags[{id,name}], tagIds[], workspaceId, docType, description, created, inTrash }
80
+ - getSchema output: line 1 is \`# Tag definition: name (id:xxx)\`. Line 2 is \`Extends #parent (id:xxx)\` when tag has inheritance, or \`Extends #parent (base type) (id:xxx)\` for Tana built-in types. Parse "Extends" to find relationships.
81
+
65
82
  ## Examples
66
83
 
67
84
  Search: await tana.nodes.search({ and: [{ hasType: "tagId" }, { is: "todo" }] })
@@ -75,7 +92,4 @@ await tana.import(parentNodeId, \`
75
92
  \`)
76
93
  \`\`\`
77
94
 
78
- ## Output
79
-
80
- console.log() to return data.
81
95
  `;
@@ -12,7 +12,6 @@
12
12
  import type { SandboxResult } from "../types";
13
13
  import type { TanaAPI } from "../api/tana";
14
14
  import { saveScriptRun } from "../storage/history";
15
- import { createStdinHelper } from "./stdin";
16
15
  import { createWorkflowHelper } from "./workflow";
17
16
 
18
17
  const EXECUTION_TIMEOUT = 10_000; // 10 seconds
@@ -32,6 +31,8 @@ function createTrackedTanaAPI(
32
31
  const trackNodeId = (id: string) => tracker.nodeIds.add(id);
33
32
 
34
33
  return {
34
+ workspace: tana.workspace,
35
+
35
36
  health: () => track("health", () => tana.health()),
36
37
 
37
38
  workspaces: {
@@ -72,9 +73,9 @@ function createTrackedTanaAPI(
72
73
  },
73
74
 
74
75
  tags: {
75
- list: (workspaceId, limit) => {
76
+ listAll: (workspaceId: string) => {
76
77
  tracker.workspaceId = workspaceId;
77
- return track("tags.list", () => tana.tags.list(workspaceId, limit));
78
+ return track("tags.listAll", () => tana.tags.listAll(workspaceId));
78
79
  },
79
80
  getSchema: (tagId, includeEditInstructions) =>
80
81
  track("tags.getSchema", () => tana.tags.getSchema(tagId, includeEditInstructions)),
@@ -116,14 +117,15 @@ function createTrackedTanaAPI(
116
117
  trackNodeId(parentNodeId);
117
118
  return track("import", () => tana.import(parentNodeId, content));
118
119
  },
120
+
121
+ format: (data) => tana.format(data),
119
122
  };
120
123
  }
121
124
 
122
125
  export async function executeSandbox(
123
126
  code: string,
124
127
  tana: TanaAPI,
125
- sessionId?: string,
126
- input?: string
128
+ sessionId?: string
127
129
  ): Promise<SandboxResult> {
128
130
  const startTime = performance.now();
129
131
  const logs: string[] = [];
@@ -132,7 +134,7 @@ export async function executeSandbox(
132
134
  const tracker = {
133
135
  calls: [] as string[],
134
136
  nodeIds: new Set<string>(),
135
- workspaceId: null as string | null,
137
+ workspaceId: tana.workspace?.id ?? null,
136
138
  };
137
139
 
138
140
  // Custom console that captures output
@@ -151,8 +153,6 @@ export async function executeSandbox(
151
153
  },
152
154
  };
153
155
 
154
- // Create helpers for script execution
155
- const stdin = createStdinHelper(input);
156
156
  const effectiveSessionId = sessionId ?? crypto.randomUUID();
157
157
  const workflow = createWorkflowHelper(effectiveSessionId);
158
158
 
@@ -160,7 +160,6 @@ export async function executeSandbox(
160
160
  const trackedTana = createTrackedTanaAPI(tana, tracker);
161
161
 
162
162
  try {
163
- // Create async function to support top-level await
164
163
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
165
164
  const AsyncFunction = Object.getPrototypeOf(
166
165
  async function () {}
@@ -183,26 +182,26 @@ export async function executeSandbox(
183
182
  "exports",
184
183
  ];
185
184
 
185
+ const transpiler = new Bun.Transpiler({ loader: "ts" });
186
+ const jsCode = transpiler.transformSync(code);
187
+
186
188
  const fn = new AsyncFunction(
187
189
  "tana",
188
190
  "console",
189
- "stdin",
190
191
  "workflow",
191
192
  ...shadowedGlobals,
192
- code
193
+ jsCode
193
194
  );
194
195
 
195
- // Race between execution and timeout
196
196
  const timeoutPromise = new Promise<never>((_, reject) => {
197
197
  setTimeout(() => {
198
198
  reject(new Error(`Execution timed out after ${EXECUTION_TIMEOUT}ms`));
199
199
  }, EXECUTION_TIMEOUT);
200
200
  });
201
201
 
202
- // Pass undefined for all shadowed globals
203
202
  const undefinedArgs = shadowedGlobals.map(() => undefined);
204
203
  await Promise.race([
205
- fn(trackedTana, sandboxConsole, stdin, workflow, ...undefinedArgs),
204
+ fn(trackedTana, sandboxConsole, workflow, ...undefinedArgs),
206
205
  timeoutPromise,
207
206
  ]);
208
207
 
@@ -217,7 +216,6 @@ export async function executeSandbox(
217
216
  error: null,
218
217
  durationMs,
219
218
  sessionId: effectiveSessionId,
220
- input: input ?? null,
221
219
  apiCalls: tracker.calls.length > 0 ? tracker.calls : null,
222
220
  nodeIdsAffected: tracker.nodeIds.size > 0 ? Array.from(tracker.nodeIds) : null,
223
221
  workspaceId: tracker.workspaceId,
@@ -242,7 +240,6 @@ export async function executeSandbox(
242
240
  error: errorMessage,
243
241
  durationMs,
244
242
  sessionId: effectiveSessionId,
245
- input: input ?? null,
246
243
  apiCalls: tracker.calls.length > 0 ? tracker.calls : null,
247
244
  nodeIdsAffected: tracker.nodeIds.size > 0 ? Array.from(tracker.nodeIds) : null,
248
245
  workspaceId: tracker.workspaceId,
@@ -1,69 +0,0 @@
1
- /**
2
- * stdin() Helper
3
- *
4
- * Provides convenient methods for handling input data passed to scripts.
5
- * In MCP context, this handles data passed via the `input` parameter.
6
- *
7
- * Usage in scripts:
8
- * const data = stdin().json(); // Parse as JSON
9
- * const lines = stdin().lines(); // Split into lines
10
- * const text = stdin().text(); // Raw trimmed text
11
- */
12
-
13
- export interface StdinHelper {
14
- /** Get raw input as trimmed string */
15
- text(): string;
16
- /** Split input into array of lines (empty lines filtered) */
17
- lines(): string[];
18
- /** Parse input as JSON (throws if invalid) */
19
- json<T = unknown>(): T;
20
- /** Parse input as JSON, return null if invalid */
21
- jsonSafe<T = unknown>(): T | null;
22
- /** Check if any input was provided */
23
- hasInput(): boolean;
24
- }
25
-
26
- export function createStdinHelper(input: string | undefined): () => StdinHelper {
27
- const rawInput = input ?? "";
28
-
29
- return () => ({
30
- text(): string {
31
- return rawInput.trim();
32
- },
33
-
34
- lines(): string[] {
35
- return rawInput
36
- .split("\n")
37
- .map((line) => line.trim())
38
- .filter((line) => line.length > 0);
39
- },
40
-
41
- json<T = unknown>(): T {
42
- const text = rawInput.trim();
43
- if (!text) {
44
- throw new Error("stdin is empty - no JSON to parse");
45
- }
46
- try {
47
- return JSON.parse(text) as T;
48
- } catch (e) {
49
- throw new Error(
50
- `Failed to parse stdin as JSON: ${e instanceof Error ? e.message : String(e)}`
51
- );
52
- }
53
- },
54
-
55
- jsonSafe<T = unknown>(): T | null {
56
- const text = rawInput.trim();
57
- if (!text) return null;
58
- try {
59
- return JSON.parse(text) as T;
60
- } catch {
61
- return null;
62
- }
63
- },
64
-
65
- hasInput(): boolean {
66
- return rawInput.trim().length > 0;
67
- },
68
- });
69
- }