tana-mcp-codemode 0.2.0 → 0.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/package.json +16 -12
- package/src/api/tana.ts +266 -36
- package/src/api/types.ts +10 -0
- package/src/index.ts +0 -0
- package/src/prompts.ts +5 -2
- package/src/sandbox/executor.ts +10 -0
package/package.json
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tana-mcp-codemode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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
|
+
"test": "bun test",
|
|
17
|
+
"typecheck": "bunx tsc --noEmit",
|
|
18
|
+
"prepublishOnly": "bun run typecheck",
|
|
19
|
+
"release:patch": "npm version patch && npm publish",
|
|
20
|
+
"release:minor": "npm version minor && npm publish",
|
|
21
|
+
"release:major": "npm version major && npm publish"
|
|
22
|
+
},
|
|
10
23
|
"files": [
|
|
11
24
|
"src/index.ts",
|
|
12
25
|
"src/prompts.ts",
|
|
@@ -41,14 +54,5 @@
|
|
|
41
54
|
"openapi-typescript": "^7.10.1",
|
|
42
55
|
"typescript": "^5.9.3"
|
|
43
56
|
},
|
|
44
|
-
"license": "MIT"
|
|
45
|
-
|
|
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
|
-
}
|
|
57
|
+
"license": "MIT"
|
|
58
|
+
}
|
package/src/api/tana.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
SearchResult,
|
|
17
17
|
Children,
|
|
18
18
|
EditNodeOptions,
|
|
19
|
+
MoveNodeOptions,
|
|
19
20
|
Tag,
|
|
20
21
|
CreateTagOptions,
|
|
21
22
|
AddFieldOptions,
|
|
@@ -24,6 +25,115 @@ import type {
|
|
|
24
25
|
} from "./types";
|
|
25
26
|
import { format } from "./format";
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Build deep-object query params (style: deepObject, explode: true).
|
|
30
|
+
* Used by search and getFieldOptions.
|
|
31
|
+
*/
|
|
32
|
+
function addQueryParams(params: string[], obj: Record<string, unknown>, prefix: string) {
|
|
33
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
34
|
+
const paramKey = prefix ? `${prefix}[${key}]` : key;
|
|
35
|
+
if (value === null || value === undefined) continue;
|
|
36
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
37
|
+
addQueryParams(params, value as Record<string, unknown>, paramKey);
|
|
38
|
+
} else if (Array.isArray(value)) {
|
|
39
|
+
value.forEach((item, i) => {
|
|
40
|
+
if (typeof item === "object") {
|
|
41
|
+
addQueryParams(params, item as Record<string, unknown>, `${paramKey}[${i}]`);
|
|
42
|
+
} else {
|
|
43
|
+
params.push(`${encodeURIComponent(`${paramKey}[${i}]`)}=${encodeURIComponent(String(item))}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
params.push(`${encodeURIComponent(paramKey)}=${encodeURIComponent(String(value))}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parse predetermined options: ` - Name <!-- node-id: ABC -->` */
|
|
53
|
+
function parseOptionsFromMarkdown(md: string): { id: string; name: string }[] {
|
|
54
|
+
const results: { id: string; name: string }[] = [];
|
|
55
|
+
const lines = md.split("\n");
|
|
56
|
+
let inOptions = false;
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.includes("**Options**:")) { inOptions = true; continue; }
|
|
59
|
+
if (inOptions) {
|
|
60
|
+
const match = line.match(/^\s+- (.+?)\s*<!--\s*node-id:\s*(\S+)\s*-->/);
|
|
61
|
+
if (match) {
|
|
62
|
+
results.push({ id: match[2], name: match[1] });
|
|
63
|
+
} else if (line.trim() && !line.startsWith(" ")) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Parse `[name](tana:id)` references from markdown */
|
|
72
|
+
function parseRefsFromMarkdown(md: string): { id: string; name: string }[] {
|
|
73
|
+
const results: { id: string; name: string }[] = [];
|
|
74
|
+
const re = /\[([^\]]+)\]\(tana:([^)]+)\)/g;
|
|
75
|
+
let match;
|
|
76
|
+
while ((match = re.exec(md)) !== null) {
|
|
77
|
+
results.push({ id: match[2], name: match[1] });
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract option values for a specific field from node markdown.
|
|
84
|
+
* Scopes parsing to only the target field's section to avoid noise
|
|
85
|
+
* from other fields, tags, and node names.
|
|
86
|
+
*/
|
|
87
|
+
function parseFieldValues(md: string, fieldName: string): { id: string; name: string }[] {
|
|
88
|
+
const results: { id: string; name: string }[] = [];
|
|
89
|
+
const lines = md.split("\n");
|
|
90
|
+
let inField = false;
|
|
91
|
+
let fieldIndent = -1;
|
|
92
|
+
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
// Detect the field section: " - **FieldName**:" or " - **FieldName**: value <!-- node-id: xxx -->"
|
|
95
|
+
if (!inField && line.includes(`**${fieldName}**`)) {
|
|
96
|
+
inField = true;
|
|
97
|
+
fieldIndent = line.search(/\S/);
|
|
98
|
+
|
|
99
|
+
// Inline single-value: " - **Type**: negotiation <!-- node-id: xxx -->"
|
|
100
|
+
const inlineMatch = line.match(
|
|
101
|
+
new RegExp(`\\*\\*${fieldName}\\*\\*:\\s*(.+?)\\s*<!--\\s*node-id:\\s*(\\S+)\\s*-->`)
|
|
102
|
+
);
|
|
103
|
+
if (inlineMatch) {
|
|
104
|
+
results.push({ id: inlineMatch[2], name: inlineMatch[1].trim() });
|
|
105
|
+
}
|
|
106
|
+
// Also check for tana-link format: " - **Type**: [value](tana:id)"
|
|
107
|
+
const linkMatch = line.match(
|
|
108
|
+
new RegExp(`\\*\\*${fieldName}\\*\\*:\\s*\\[([^\\]]+)\\]\\(tana:([^)]+)\\)`)
|
|
109
|
+
);
|
|
110
|
+
if (linkMatch) {
|
|
111
|
+
results.push({ id: linkMatch[2], name: linkMatch[1] });
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (inField) {
|
|
117
|
+
const currentIndent = line.search(/\S/);
|
|
118
|
+
// Left the field's section (same or less indentation)
|
|
119
|
+
if (line.trim() && currentIndent <= fieldIndent) break;
|
|
120
|
+
|
|
121
|
+
// Child value: " - value <!-- node-id: xxx -->"
|
|
122
|
+
const childMatch = line.match(/^\s+- (.+?)\s*<!--\s*node-id:\s*(\S+)\s*-->/);
|
|
123
|
+
if (childMatch) {
|
|
124
|
+
results.push({ id: childMatch[2], name: childMatch[1].trim() });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Child value (tana-link): " - [value](tana:id)"
|
|
128
|
+
const childLinkMatch = line.match(/^\s+- \[([^\]]+)\]\(tana:([^)]+)\)/);
|
|
129
|
+
if (childLinkMatch) {
|
|
130
|
+
results.push({ id: childLinkMatch[2], name: childLinkMatch[1] });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
|
|
27
137
|
/**
|
|
28
138
|
* TanaAPI Interface
|
|
29
139
|
*
|
|
@@ -54,6 +164,10 @@ export interface TanaAPI {
|
|
|
54
164
|
): Promise<Children>;
|
|
55
165
|
/** Edit a node's name/description */
|
|
56
166
|
edit(options: EditNodeOptions): Promise<{ success: boolean }>;
|
|
167
|
+
/** Move node to a new parent */
|
|
168
|
+
move(options: MoveNodeOptions): Promise<{ success: boolean }>;
|
|
169
|
+
/** Open a node in the Tana UI */
|
|
170
|
+
open(nodeId: string, openType?: "current" | "panel" | "tab"): Promise<{ success: boolean }>;
|
|
57
171
|
/** Move node to trash */
|
|
58
172
|
trash(nodeId: string): Promise<{ success: boolean }>;
|
|
59
173
|
/** Check a node's checkbox */
|
|
@@ -82,18 +196,24 @@ export interface TanaAPI {
|
|
|
82
196
|
};
|
|
83
197
|
|
|
84
198
|
fields: {
|
|
85
|
-
/** Set a field to
|
|
199
|
+
/** Set a field to option value(s). Pass string[] for multi-value fields. */
|
|
86
200
|
setOption(
|
|
87
201
|
nodeId: string,
|
|
88
202
|
attributeId: string,
|
|
89
|
-
optionId: string
|
|
203
|
+
optionId: string | string[]
|
|
90
204
|
): Promise<{ success: boolean }>;
|
|
91
|
-
/** Set a field to a string value */
|
|
205
|
+
/** Set a field to a string value, or null to clear it */
|
|
92
206
|
setContent(
|
|
93
207
|
nodeId: string,
|
|
94
208
|
attributeId: string,
|
|
95
|
-
content: string
|
|
209
|
+
content: string | null,
|
|
210
|
+
mode?: "replace" | "append"
|
|
96
211
|
): Promise<{ success: boolean }>;
|
|
212
|
+
/** Discover available options for an Options-type field */
|
|
213
|
+
getFieldOptions(
|
|
214
|
+
fieldId: string,
|
|
215
|
+
options?: { tagId?: string; workspaceId?: string; limit?: number }
|
|
216
|
+
): Promise<{ id: string; name: string }[]>;
|
|
97
217
|
};
|
|
98
218
|
|
|
99
219
|
calendar: {
|
|
@@ -135,30 +255,8 @@ export function createTanaAPI(
|
|
|
135
255
|
query: SearchQuery,
|
|
136
256
|
options?: SearchOptions
|
|
137
257
|
): Promise<SearchResult[]> {
|
|
138
|
-
// Build deep object query params (style: deepObject, explode: true)
|
|
139
258
|
const params: string[] = [];
|
|
140
|
-
|
|
141
|
-
function addQueryParams(obj: Record<string, unknown>, prefix: string) {
|
|
142
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
143
|
-
const paramKey = prefix ? `${prefix}[${key}]` : key;
|
|
144
|
-
if (value === null || value === undefined) continue;
|
|
145
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
146
|
-
addQueryParams(value as Record<string, unknown>, paramKey);
|
|
147
|
-
} else if (Array.isArray(value)) {
|
|
148
|
-
value.forEach((item, i) => {
|
|
149
|
-
if (typeof item === "object") {
|
|
150
|
-
addQueryParams(item as Record<string, unknown>, `${paramKey}[${i}]`);
|
|
151
|
-
} else {
|
|
152
|
-
params.push(`${encodeURIComponent(`${paramKey}[${i}]`)}=${encodeURIComponent(String(item))}`);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
} else {
|
|
156
|
-
params.push(`${encodeURIComponent(paramKey)}=${encodeURIComponent(String(value))}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
addQueryParams(query as unknown as Record<string, unknown>, "query");
|
|
259
|
+
addQueryParams(params, query as unknown as Record<string, unknown>, "query");
|
|
162
260
|
if (options?.limit) params.push(`limit=${options.limit}`);
|
|
163
261
|
const effectiveWorkspaceIds = options?.workspaceIds
|
|
164
262
|
?? (defaultSearchWorkspaceIds?.length ? defaultSearchWorkspaceIds : undefined);
|
|
@@ -202,6 +300,31 @@ export function createTanaAPI(
|
|
|
202
300
|
return { success: !!result.nodeId };
|
|
203
301
|
},
|
|
204
302
|
|
|
303
|
+
async move(options: MoveNodeOptions): Promise<{ success: boolean }> {
|
|
304
|
+
const result = await client.post<{ nodeId: string; message: string }>(
|
|
305
|
+
`/nodes/${options.nodeId}/move`,
|
|
306
|
+
{
|
|
307
|
+
targetNodeId: options.targetNodeId,
|
|
308
|
+
keepSourceReference: options.keepSourceReference,
|
|
309
|
+
position: options.position,
|
|
310
|
+
referenceNodeId: options.referenceNodeId,
|
|
311
|
+
sourceParentId: options.sourceParentId,
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
return { success: !!result.nodeId };
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
async open(
|
|
318
|
+
nodeId: string,
|
|
319
|
+
openType: "current" | "panel" | "tab" = "current"
|
|
320
|
+
): Promise<{ success: boolean }> {
|
|
321
|
+
const result = await client.post<{ nodeId: string; message: string }>(
|
|
322
|
+
`/nodes/${nodeId}/open`,
|
|
323
|
+
{ openType }
|
|
324
|
+
);
|
|
325
|
+
return { success: !!result.nodeId };
|
|
326
|
+
},
|
|
327
|
+
|
|
205
328
|
async trash(nodeId: string): Promise<{ success: boolean }> {
|
|
206
329
|
const result = await client.post<{ nodeId: string; message: string }>(
|
|
207
330
|
`/nodes/${nodeId}/trash`,
|
|
@@ -314,29 +437,130 @@ export function createTanaAPI(
|
|
|
314
437
|
},
|
|
315
438
|
|
|
316
439
|
fields: {
|
|
440
|
+
// Workaround: append mode is broken in Tana API (clears field instead of appending).
|
|
441
|
+
// For arrays, we set first value via API, then import remaining into the field tuple.
|
|
442
|
+
// See claudedocs/api-bugs.md
|
|
317
443
|
async setOption(
|
|
318
444
|
nodeId: string,
|
|
319
445
|
attributeId: string,
|
|
320
|
-
optionId: string
|
|
446
|
+
optionId: string | string[],
|
|
447
|
+
_mode?: "replace" | "append"
|
|
321
448
|
): Promise<{ success: boolean }> {
|
|
322
|
-
const
|
|
449
|
+
const ids = Array.isArray(optionId) ? optionId : [optionId];
|
|
450
|
+
if (ids.length === 0) return { success: true };
|
|
451
|
+
|
|
452
|
+
// Step 1: set first value via API (creates the field tuple)
|
|
453
|
+
await client.post<{ nodeId: string; message: string }>(
|
|
323
454
|
`/nodes/${nodeId}/fields/${attributeId}/option`,
|
|
324
|
-
{ optionId }
|
|
455
|
+
{ optionId: ids[0], mode: "replace" }
|
|
325
456
|
);
|
|
326
|
-
|
|
457
|
+
|
|
458
|
+
if (ids.length === 1) return { success: true };
|
|
459
|
+
|
|
460
|
+
// Step 2: find the field tuple via getChildren
|
|
461
|
+
const { children } = await client.get<Children>(
|
|
462
|
+
`/nodes/${nodeId}/children?limit=100`
|
|
463
|
+
);
|
|
464
|
+
let tupleId: string | null = null;
|
|
465
|
+
for (const child of children) {
|
|
466
|
+
if (child.docType === "tuple") {
|
|
467
|
+
const sub = await client.get<Children>(
|
|
468
|
+
`/nodes/${child.id}/children?limit=20`
|
|
469
|
+
);
|
|
470
|
+
if (sub.children.some(s => s.id === attributeId)) {
|
|
471
|
+
tupleId = child.id;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!tupleId) return { success: false };
|
|
477
|
+
|
|
478
|
+
// Step 3: import remaining values into the tuple
|
|
479
|
+
const refs = ids.slice(1).map(id => `- [[^${id}]]`).join("\n");
|
|
480
|
+
await client.post<ImportResult>(`/nodes/${tupleId}/import`, { content: refs });
|
|
481
|
+
return { success: true };
|
|
327
482
|
},
|
|
328
483
|
|
|
329
484
|
async setContent(
|
|
330
485
|
nodeId: string,
|
|
331
486
|
attributeId: string,
|
|
332
|
-
content: string
|
|
487
|
+
content: string | null,
|
|
488
|
+
mode?: "replace" | "append"
|
|
333
489
|
): Promise<{ success: boolean }> {
|
|
334
490
|
const result = await client.post<{ nodeId: string; message: string }>(
|
|
335
491
|
`/nodes/${nodeId}/fields/${attributeId}/content`,
|
|
336
|
-
{ content }
|
|
492
|
+
{ content, mode }
|
|
337
493
|
);
|
|
338
494
|
return { success: !!result.nodeId };
|
|
339
495
|
},
|
|
496
|
+
|
|
497
|
+
async getFieldOptions(
|
|
498
|
+
fieldId: string,
|
|
499
|
+
options?: { tagId?: string; workspaceId?: string; limit?: number }
|
|
500
|
+
): Promise<{ id: string; name: string }[]> {
|
|
501
|
+
const result = await client.get<{ markdown: string }>(
|
|
502
|
+
`/nodes/${fieldId}?maxDepth=2`
|
|
503
|
+
);
|
|
504
|
+
const md = result.markdown;
|
|
505
|
+
|
|
506
|
+
// Pattern A: Predetermined options (field definition lists them)
|
|
507
|
+
if (md.includes("**Options**:")) {
|
|
508
|
+
return parseOptionsFromMarkdown(md);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Pattern B: Instance of supertag
|
|
512
|
+
if (md.includes("Options from supertag")) {
|
|
513
|
+
const sourceTagMatch = md.match(/\(tana:([^)]+)\)/);
|
|
514
|
+
if (sourceTagMatch) {
|
|
515
|
+
const params: string[] = [];
|
|
516
|
+
addQueryParams(params, { hasType: sourceTagMatch[1] } as Record<string, unknown>, "query");
|
|
517
|
+
if (options?.workspaceId) {
|
|
518
|
+
params.push(`workspaceIds[0]=${encodeURIComponent(options.workspaceId)}`);
|
|
519
|
+
}
|
|
520
|
+
if (options?.limit) params.push(`limit=${options.limit}`);
|
|
521
|
+
const results = await client.get<SearchResult[]>(`/nodes/search?${params.join("&")}`);
|
|
522
|
+
return results.map(r => ({ id: r.id, name: r.name }));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Pattern C: Source search node
|
|
527
|
+
const searchNodeMatch = md.match(/\*\*Options List\*\*:.*?\(tana:([^)]+)\)/);
|
|
528
|
+
if (searchNodeMatch) {
|
|
529
|
+
const searchResult = await client.get<{ markdown: string }>(
|
|
530
|
+
`/nodes/${searchNodeMatch[1]}?maxDepth=1`
|
|
531
|
+
);
|
|
532
|
+
return parseRefsFromMarkdown(searchResult.markdown);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Pattern D: Ad-hoc — sample from existing nodes that have this field set
|
|
536
|
+
// Extract field name from definition for scoped parsing
|
|
537
|
+
const fieldNameMatch = md.match(/^- (.+?) #field-definition/m);
|
|
538
|
+
if (!fieldNameMatch) return [];
|
|
539
|
+
|
|
540
|
+
const fieldName = fieldNameMatch[1];
|
|
541
|
+
const limit = options?.limit ?? 10;
|
|
542
|
+
const params: string[] = [];
|
|
543
|
+
const query: Record<string, unknown> = options?.tagId
|
|
544
|
+
? { and: [{ hasType: options.tagId }, { field: { fieldId, state: "set" } }] }
|
|
545
|
+
: { field: { fieldId, state: "set" } };
|
|
546
|
+
addQueryParams(params, query, "query");
|
|
547
|
+
if (options?.workspaceId) {
|
|
548
|
+
params.push(`workspaceIds[0]=${encodeURIComponent(options.workspaceId)}`);
|
|
549
|
+
}
|
|
550
|
+
params.push(`limit=${limit}`);
|
|
551
|
+
|
|
552
|
+
const searchResults = await client.get<SearchResult[]>(`/nodes/search?${params.join("&")}`);
|
|
553
|
+
const seen = new Map<string, string>();
|
|
554
|
+
for (const node of searchResults) {
|
|
555
|
+
const nodeMd = await client.get<{ markdown: string }>(
|
|
556
|
+
`/nodes/${node.id}?maxDepth=1`
|
|
557
|
+
);
|
|
558
|
+
for (const { id, name } of parseFieldValues(nodeMd.markdown, fieldName)) {
|
|
559
|
+
if (!seen.has(id)) seen.set(id, name);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return Array.from(seen, ([id, name]) => ({ id, name }));
|
|
563
|
+
},
|
|
340
564
|
},
|
|
341
565
|
|
|
342
566
|
calendar: {
|
|
@@ -355,9 +579,15 @@ export function createTanaAPI(
|
|
|
355
579
|
},
|
|
356
580
|
|
|
357
581
|
async import(parentNodeId: string, content: string): Promise<ImportResult> {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
582
|
+
// API returns { createdNodes: [{id,name}], ... } — normalize to ImportResult
|
|
583
|
+
const raw = await client.post<{
|
|
584
|
+
createdNodes: { id: string; name: string }[];
|
|
585
|
+
message: string;
|
|
586
|
+
}>(`/nodes/${parentNodeId}/import`, { content });
|
|
587
|
+
return {
|
|
588
|
+
success: true,
|
|
589
|
+
nodeIds: raw.createdNodes.map(n => n.id),
|
|
590
|
+
};
|
|
361
591
|
},
|
|
362
592
|
|
|
363
593
|
format(data: unknown): string {
|
package/src/api/types.ts
CHANGED
|
@@ -161,6 +161,16 @@ export interface SetCheckboxOptions {
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/** Options for moving a node to a new parent */
|
|
165
|
+
export interface MoveNodeOptions {
|
|
166
|
+
nodeId: string;
|
|
167
|
+
targetNodeId: string;
|
|
168
|
+
keepSourceReference?: boolean;
|
|
169
|
+
position?: "start" | "end" | "after" | "before";
|
|
170
|
+
referenceNodeId?: string;
|
|
171
|
+
sourceParentId?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
164
174
|
/** Options for editing a node */
|
|
165
175
|
export interface EditNodeOptions {
|
|
166
176
|
nodeId: string;
|
package/src/index.ts
CHANGED
|
File without changes
|
package/src/prompts.ts
CHANGED
|
@@ -14,6 +14,8 @@ tana.nodes.search(query, options?) → SearchResult[]
|
|
|
14
14
|
tana.nodes.read(nodeId, maxDepth?) → string (markdown)
|
|
15
15
|
tana.nodes.getChildren(nodeId, { limit?, offset? }) → { children, total, hasMore }
|
|
16
16
|
tana.nodes.edit({ nodeId, name?, description? }) → { success }
|
|
17
|
+
tana.nodes.move({ nodeId, targetNodeId, keepSourceReference?, position?, referenceNodeId?, sourceParentId? }) → { success }
|
|
18
|
+
tana.nodes.open(nodeId, openType?) → { success } // openType: "current" | "panel" | "tab" (default: "current")
|
|
17
19
|
tana.nodes.trash(nodeId) → { success }
|
|
18
20
|
tana.nodes.check(nodeId) / uncheck(nodeId) → { success }
|
|
19
21
|
|
|
@@ -26,8 +28,9 @@ tana.tags.addField({ tagId, name, dataType: "plain"|"number"|"date"|"url"|"email
|
|
|
26
28
|
tana.tags.setCheckbox({ tagId, showCheckbox, doneStateMapping? })
|
|
27
29
|
|
|
28
30
|
### Fields
|
|
29
|
-
tana.fields.setOption(nodeId, attributeId, optionId)
|
|
30
|
-
tana.fields.setContent(nodeId, attributeId, content)
|
|
31
|
+
tana.fields.setOption(nodeId, attributeId, optionId) // optionId: string | string[] (array for multi-value)
|
|
32
|
+
tana.fields.setContent(nodeId, attributeId, content, mode?) // content: string | null (null clears field), mode: "replace" | "append"
|
|
33
|
+
tana.fields.getFieldOptions(fieldId, options?) → { id, name }[] // discover available options for a field
|
|
31
34
|
|
|
32
35
|
### Calendar
|
|
33
36
|
tana.calendar.getOrCreate(workspaceId, "day"|"week"|"month"|"year", date?)
|
package/src/sandbox/executor.ts
CHANGED
|
@@ -58,6 +58,14 @@ function createTrackedTanaAPI(
|
|
|
58
58
|
trackNodeId(options.nodeId);
|
|
59
59
|
return track("nodes.edit", () => tana.nodes.edit(options));
|
|
60
60
|
},
|
|
61
|
+
move: (options) => {
|
|
62
|
+
trackNodeId(options.nodeId);
|
|
63
|
+
return track("nodes.move", () => tana.nodes.move(options));
|
|
64
|
+
},
|
|
65
|
+
open: (nodeId, openType) => {
|
|
66
|
+
trackNodeId(nodeId);
|
|
67
|
+
return track("nodes.open", () => tana.nodes.open(nodeId, openType));
|
|
68
|
+
},
|
|
61
69
|
trash: (nodeId) => {
|
|
62
70
|
trackNodeId(nodeId);
|
|
63
71
|
return track("nodes.trash", () => tana.nodes.trash(nodeId));
|
|
@@ -102,6 +110,8 @@ function createTrackedTanaAPI(
|
|
|
102
110
|
trackNodeId(nodeId);
|
|
103
111
|
return track("fields.setContent", () => tana.fields.setContent(nodeId, attributeId, content));
|
|
104
112
|
},
|
|
113
|
+
getFieldOptions: (...args) =>
|
|
114
|
+
track("fields.getFieldOptions", () => tana.fields.getFieldOptions(...args)),
|
|
105
115
|
},
|
|
106
116
|
|
|
107
117
|
calendar: {
|