@unwarkz/n8n-nodes-outline-wiki 1.0.1
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 +223 -0
- package/dist/credentials/OutlineApi.credentials.js +43 -0
- package/dist/nodes/outline/Outline.node.js +1641 -0
- package/dist/nodes/outline/OutlineAiTools.node.js +1506 -0
- package/dist/nodes/outline/outline.svg +7 -0
- package/index.js +3 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OutlineAiTools = void 0;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load zod for schema definitions (available in n8n's runtime environment).
|
|
7
|
+
*/
|
|
8
|
+
let z = null;
|
|
9
|
+
try { z = require('zod'); } catch (_) { /* no zod */ }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load DynamicStructuredTool from LangChain (available in n8n's runtime environment).
|
|
13
|
+
* DynamicStructuredTool accepts a structured zod-validated object as input, which
|
|
14
|
+
* avoids "Expected string, received object" errors when an LLM passes structured arguments.
|
|
15
|
+
* Falls back to a minimal StructuredTool-compatible shim when not resolvable.
|
|
16
|
+
*/
|
|
17
|
+
let DynamicStructuredTool;
|
|
18
|
+
(function () {
|
|
19
|
+
const candidates = ['@langchain/core/tools', 'langchain/tools'];
|
|
20
|
+
for (const mod of candidates) {
|
|
21
|
+
try {
|
|
22
|
+
const exported = require(mod);
|
|
23
|
+
if (exported && exported.DynamicStructuredTool) {
|
|
24
|
+
DynamicStructuredTool = exported.DynamicStructuredTool;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
} catch (_) { /* continue */ }
|
|
28
|
+
}
|
|
29
|
+
// Minimal shim that satisfies the structured-tool contract used by n8n AI Agent
|
|
30
|
+
DynamicStructuredTool = class DynamicStructuredToolShim {
|
|
31
|
+
constructor({ name, description, schema, func }) {
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.description = description;
|
|
34
|
+
this.schema = schema || (z ? z.object({}).passthrough() : null);
|
|
35
|
+
this.func = func;
|
|
36
|
+
this.returnDirect = false;
|
|
37
|
+
this.verbose = false;
|
|
38
|
+
this.lc_namespace = ['langchain_core', 'tools'];
|
|
39
|
+
this.lc_serializable = true;
|
|
40
|
+
}
|
|
41
|
+
async invoke(input) {
|
|
42
|
+
const inputObj = typeof input === 'string'
|
|
43
|
+
? (() => { try { return JSON.parse(input); } catch (_) { return { input }; } })()
|
|
44
|
+
: (input || {});
|
|
45
|
+
return this.func(inputObj);
|
|
46
|
+
}
|
|
47
|
+
async call(arg, _configArg) {
|
|
48
|
+
return this.invoke(arg);
|
|
49
|
+
}
|
|
50
|
+
_type() { return 'structured'; }
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* OutlineAiTools – n8n AI sub-node
|
|
56
|
+
*
|
|
57
|
+
* Exposes Outline Wiki API operations as DynamicStructuredTool instances consumable
|
|
58
|
+
* by the n8n AI Agent node. Supports the full Outline API surface including:
|
|
59
|
+
* - Documents (CRUD, search, import/export, archive, restore, move, AI answers)
|
|
60
|
+
* - Collections (CRUD, export, document tree)
|
|
61
|
+
* - Comments (CRUD)
|
|
62
|
+
* - Attachments (upload with binary data support, delete)
|
|
63
|
+
* - Users (list, get)
|
|
64
|
+
* - Shares (create, list, revoke)
|
|
65
|
+
*
|
|
66
|
+
* Binary data protocol (from PR#14 pattern):
|
|
67
|
+
* Tools that PRODUCE files store them in n8n's binary data system and return
|
|
68
|
+
* a JSON object with a "binaryPropertyName" field.
|
|
69
|
+
* Tools that CONSUME files accept a "binary_property_name" parameter.
|
|
70
|
+
* Binary data is shared across AI tool modules via global._n8nBinaryRegistry.
|
|
71
|
+
*
|
|
72
|
+
* Connect the "ai_tool" output to an AI Agent node's "Tools" input.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async function buildAuthHeaders(ctx) {
|
|
78
|
+
const credentials = await ctx.getCredentials('outlineApi');
|
|
79
|
+
const baseUrl = (credentials.baseUrl || 'https://app.getoutline.com').replace(/\/$/, '');
|
|
80
|
+
const apiKey = credentials.apiKey;
|
|
81
|
+
const headers = {
|
|
82
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
'Accept': 'application/json',
|
|
85
|
+
};
|
|
86
|
+
return { baseUrl, apiKey, headers };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function outlinePost(ctx, endpoint, body) {
|
|
90
|
+
const { baseUrl, headers } = await buildAuthHeaders(ctx);
|
|
91
|
+
const options = {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
url: `${baseUrl}/api${endpoint}`,
|
|
94
|
+
headers,
|
|
95
|
+
body: JSON.stringify(body || {}),
|
|
96
|
+
json: true,
|
|
97
|
+
};
|
|
98
|
+
return ctx.helpers.request(options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function outlinePostMultipart(ctx, endpoint, formData) {
|
|
102
|
+
const { baseUrl, apiKey } = await buildAuthHeaders(ctx);
|
|
103
|
+
const options = {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
url: `${baseUrl}/api${endpoint}`,
|
|
106
|
+
headers: {
|
|
107
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
108
|
+
'Accept': 'application/json',
|
|
109
|
+
},
|
|
110
|
+
formData,
|
|
111
|
+
json: true,
|
|
112
|
+
};
|
|
113
|
+
return ctx.helpers.request(options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Process-global registry shared across all AI tool modules.
|
|
117
|
+
if (!global._n8nBinaryRegistry) {
|
|
118
|
+
global._n8nBinaryRegistry = new Map();
|
|
119
|
+
}
|
|
120
|
+
let _binaryCounter = 0;
|
|
121
|
+
|
|
122
|
+
async function storeBinaryOutput(ctx, buf, filename, mimeType) {
|
|
123
|
+
const buffer = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
124
|
+
const sizeKb = Math.round(buffer.length / 1024);
|
|
125
|
+
const binaryPropertyName = `outline_file_${_binaryCounter++}`;
|
|
126
|
+
const binaryData = await ctx.helpers.prepareBinaryData(buffer, filename, mimeType);
|
|
127
|
+
// Keep registry bounded to avoid unbounded memory growth across many runs.
|
|
128
|
+
if (global._n8nBinaryRegistry.size >= 100) {
|
|
129
|
+
const firstKey = global._n8nBinaryRegistry.keys().next().value;
|
|
130
|
+
global._n8nBinaryRegistry.delete(firstKey);
|
|
131
|
+
}
|
|
132
|
+
global._n8nBinaryRegistry.set(binaryPropertyName, { buffer, binaryData, filename, mimeType });
|
|
133
|
+
// Also mutate the input item so in-module getBinaryDataBuffer still works.
|
|
134
|
+
const inputItems = ctx.getInputData();
|
|
135
|
+
const item = inputItems[0] || { json: {}, binary: {} };
|
|
136
|
+
if (!item.binary) item.binary = {};
|
|
137
|
+
item.binary[binaryPropertyName] = binaryData;
|
|
138
|
+
return JSON.stringify({
|
|
139
|
+
success: true,
|
|
140
|
+
binaryPropertyName,
|
|
141
|
+
filename,
|
|
142
|
+
mimeType,
|
|
143
|
+
sizeKb,
|
|
144
|
+
message: `File "${filename}" (${sizeKb} KB) stored in binary property "${binaryPropertyName}". Pass this binaryPropertyName to other tools that need this file.`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function getBinaryInputBuffer(ctx, binaryPropertyName) {
|
|
149
|
+
const reg = global._n8nBinaryRegistry;
|
|
150
|
+
if (reg && reg.has(binaryPropertyName)) {
|
|
151
|
+
return reg.get(binaryPropertyName).buffer;
|
|
152
|
+
}
|
|
153
|
+
return ctx.helpers.getBinaryDataBuffer(0, binaryPropertyName);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getBinaryMeta(ctx, binaryPropertyName) {
|
|
157
|
+
const reg = global._n8nBinaryRegistry;
|
|
158
|
+
if (reg && reg.has(binaryPropertyName)) {
|
|
159
|
+
return reg.get(binaryPropertyName).binaryData;
|
|
160
|
+
}
|
|
161
|
+
const items = ctx.getInputData();
|
|
162
|
+
const item = items && items[0];
|
|
163
|
+
if (item && item.binary && item.binary[binaryPropertyName]) {
|
|
164
|
+
return item.binary[binaryPropertyName];
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function guessMimeType(filePath) {
|
|
170
|
+
const ext = (filePath || '').split('.').pop().toLowerCase();
|
|
171
|
+
const map = {
|
|
172
|
+
md: 'text/markdown', markdown: 'text/markdown',
|
|
173
|
+
txt: 'text/plain', html: 'text/html', htm: 'text/html',
|
|
174
|
+
csv: 'text/csv', tsv: 'text/tab-separated-values',
|
|
175
|
+
pdf: 'application/pdf',
|
|
176
|
+
doc: 'application/msword',
|
|
177
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
178
|
+
xls: 'application/vnd.ms-excel',
|
|
179
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
180
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
181
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
182
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
|
|
183
|
+
webp: 'image/webp', svg: 'image/svg+xml',
|
|
184
|
+
zip: 'application/zip', json: 'application/json',
|
|
185
|
+
};
|
|
186
|
+
return map[ext] || 'application/octet-stream';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Node Class ──────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
class OutlineAiTools {
|
|
192
|
+
constructor() {
|
|
193
|
+
this.description = {
|
|
194
|
+
displayName: 'Outline Wiki AI Tools',
|
|
195
|
+
name: 'outlineAiTools',
|
|
196
|
+
icon: 'file:outline.svg',
|
|
197
|
+
group: ['transform'],
|
|
198
|
+
version: 1,
|
|
199
|
+
description: 'Provides Outline Wiki tools (documents, collections, comments, attachments, users, shares) to an AI Agent node',
|
|
200
|
+
defaults: { name: 'Outline Wiki AI Tools' },
|
|
201
|
+
inputs: [],
|
|
202
|
+
outputs: ['ai_tool'],
|
|
203
|
+
outputNames: ['Tool'],
|
|
204
|
+
credentials: [{ name: 'outlineApi', required: true }],
|
|
205
|
+
codex: {
|
|
206
|
+
categories: ['AI'],
|
|
207
|
+
subcategories: {
|
|
208
|
+
AI: ['Tools', 'Agents & LLMs'],
|
|
209
|
+
},
|
|
210
|
+
resources: {
|
|
211
|
+
primaryDocumentation: [{ url: 'https://www.getoutline.com/developers' }],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
properties: [
|
|
215
|
+
{
|
|
216
|
+
displayName: 'Tools to Enable',
|
|
217
|
+
name: 'enabledTools',
|
|
218
|
+
type: 'multiOptions',
|
|
219
|
+
options: [
|
|
220
|
+
// Documents
|
|
221
|
+
{ name: 'Search Documents', value: 'searchDocuments', description: 'Full-text search across all documents' },
|
|
222
|
+
{ name: 'Search Document Titles', value: 'searchTitles', description: 'Fast title-only search' },
|
|
223
|
+
{ name: 'Create Document', value: 'createDocument', description: 'Create a new document in a collection' },
|
|
224
|
+
{ name: 'Get Document', value: 'getDocument', description: 'Retrieve a document by ID or URL slug' },
|
|
225
|
+
{ name: 'Update Document', value: 'updateDocument', description: 'Update a document title or body' },
|
|
226
|
+
{ name: 'Delete Document', value: 'deleteDocument', description: 'Move a document to trash (or permanently delete)' },
|
|
227
|
+
{ name: 'List Documents', value: 'listDocuments', description: 'List documents (with optional collection/status filters)' },
|
|
228
|
+
{ name: 'Import Document', value: 'importDocument', description: 'Import a file (markdown, docx, html, csv…) as a new document (accepts binary property reference)' },
|
|
229
|
+
{ name: 'Export Document', value: 'exportDocument', description: 'Export a document as Markdown and store in a binary property' },
|
|
230
|
+
{ name: 'Archive Document', value: 'archiveDocument', description: 'Archive a document (move out of sight, restorable)' },
|
|
231
|
+
{ name: 'Restore Document', value: 'restoreDocument', description: 'Restore an archived or deleted document' },
|
|
232
|
+
{ name: 'Move Document', value: 'moveDocument', description: 'Move a document to a different collection or parent' },
|
|
233
|
+
{ name: 'Answer Question', value: 'answerQuestion', description: 'Query documents with natural language (requires AI answers enabled)' },
|
|
234
|
+
// Collections
|
|
235
|
+
{ name: 'List Collections', value: 'listCollections', description: 'List all accessible collections' },
|
|
236
|
+
{ name: 'Create Collection', value: 'createCollection', description: 'Create a new collection' },
|
|
237
|
+
{ name: 'Get Collection', value: 'getCollection', description: 'Retrieve collection details by ID' },
|
|
238
|
+
{ name: 'Update Collection', value: 'updateCollection', description: 'Update a collection name, description, or settings' },
|
|
239
|
+
{ name: 'Delete Collection', value: 'deleteCollection', description: 'Delete a collection and all its documents' },
|
|
240
|
+
{ name: 'Get Collection Documents', value: 'getCollectionDocuments', description: 'Get the document hierarchy/tree for a collection' },
|
|
241
|
+
{ name: 'Export Collection', value: 'exportCollection', description: 'Trigger a bulk export of a collection (returns FileOperation)' },
|
|
242
|
+
// Comments
|
|
243
|
+
{ name: 'List Comments', value: 'listComments', description: 'List comments on a document' },
|
|
244
|
+
{ name: 'Create Comment', value: 'createComment', description: 'Add a comment to a document' },
|
|
245
|
+
{ name: 'Update Comment', value: 'updateComment', description: 'Update an existing comment' },
|
|
246
|
+
{ name: 'Delete Comment', value: 'deleteComment', description: 'Delete a comment' },
|
|
247
|
+
// Attachments
|
|
248
|
+
{ name: 'Upload Attachment', value: 'uploadAttachment', description: 'Upload a file as an Outline attachment (accepts binary property reference), returns attachment URL for embedding in documents' },
|
|
249
|
+
{ name: 'Delete Attachment', value: 'deleteAttachment', description: 'Permanently delete an attachment' },
|
|
250
|
+
// Users
|
|
251
|
+
{ name: 'List Users', value: 'listUsers', description: 'List workspace users' },
|
|
252
|
+
{ name: 'Get User', value: 'getUser', description: 'Get details for a specific user' },
|
|
253
|
+
// Shares
|
|
254
|
+
{ name: 'Create Share', value: 'createShare', description: 'Create a public share link for a document' },
|
|
255
|
+
{ name: 'List Shares', value: 'listShares', description: 'List existing share links' },
|
|
256
|
+
{ name: 'Revoke Share', value: 'revokeShare', description: 'Revoke a share link' },
|
|
257
|
+
],
|
|
258
|
+
default: ['searchDocuments', 'createDocument', 'getDocument', 'updateDocument', 'listDocuments', 'importDocument', 'uploadAttachment', 'listCollections', 'createCollection'],
|
|
259
|
+
description: 'Which Outline Wiki tools to expose to the AI Agent',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
displayName: 'Tool Description Override',
|
|
263
|
+
name: 'toolDescription',
|
|
264
|
+
type: 'string',
|
|
265
|
+
default: '',
|
|
266
|
+
description: 'Optional: override the description shown to the AI Agent for all tools',
|
|
267
|
+
typeOptions: { rows: 2 },
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async supplyData(itemIndex) {
|
|
274
|
+
const self = this;
|
|
275
|
+
const enabledTools = this.getNodeParameter('enabledTools', itemIndex, [
|
|
276
|
+
'searchDocuments', 'createDocument', 'getDocument', 'updateDocument',
|
|
277
|
+
'listDocuments', 'importDocument', 'uploadAttachment', 'listCollections', 'createCollection',
|
|
278
|
+
]);
|
|
279
|
+
const toolDescriptionOverride = this.getNodeParameter('toolDescription', itemIndex, '');
|
|
280
|
+
|
|
281
|
+
// ── Logging helpers ────────────────────────────────────────────────────
|
|
282
|
+
function log(level, message, meta) {
|
|
283
|
+
try {
|
|
284
|
+
if (self.logger && typeof self.logger[level] === 'function') {
|
|
285
|
+
self.logger[level](message, meta);
|
|
286
|
+
}
|
|
287
|
+
} catch (_) { /* ignore */ }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Execution logging helpers ──────────────────────────────────────────
|
|
291
|
+
function startToolRun(payload) {
|
|
292
|
+
try {
|
|
293
|
+
const { index } = self.addInputData('ai_tool', [[{ json: payload }]]);
|
|
294
|
+
return index;
|
|
295
|
+
} catch (_) { return 0; }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function endToolRun(runIndex, data) {
|
|
299
|
+
try {
|
|
300
|
+
const json = (data !== null && typeof data === 'object' && !Array.isArray(data))
|
|
301
|
+
? data
|
|
302
|
+
: { result: data };
|
|
303
|
+
self.addOutputData('ai_tool', runIndex, [[{ json }]]);
|
|
304
|
+
} catch (_) { /* addOutputData may not be available in all n8n versions */ }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Zod schema helpers ─────────────────────────────────────────────────
|
|
308
|
+
function strOpt(desc) { return z ? z.string().optional().describe(desc) : undefined; }
|
|
309
|
+
function boolOpt(desc) { return z ? z.boolean().optional().describe(desc) : undefined; }
|
|
310
|
+
function numOpt(desc) { return z ? z.number().optional().describe(desc) : undefined; }
|
|
311
|
+
|
|
312
|
+
// ── Shared API helpers bound to this execution context ─────────────────
|
|
313
|
+
async function apiPost(endpoint, body) {
|
|
314
|
+
return outlinePost(self, endpoint, body);
|
|
315
|
+
}
|
|
316
|
+
async function apiPostMultipart(endpoint, formData) {
|
|
317
|
+
return outlinePostMultipart(self, endpoint, formData);
|
|
318
|
+
}
|
|
319
|
+
async function getBuffer(binaryPropertyName) {
|
|
320
|
+
return getBinaryInputBuffer(self, binaryPropertyName);
|
|
321
|
+
}
|
|
322
|
+
function getMeta(binaryPropertyName) {
|
|
323
|
+
return getBinaryMeta(self, binaryPropertyName);
|
|
324
|
+
}
|
|
325
|
+
async function storeFile(buf, filename, mimeType) {
|
|
326
|
+
return storeBinaryOutput(self, buf, filename, mimeType);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const tools = [];
|
|
330
|
+
|
|
331
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
332
|
+
// DOCUMENTS
|
|
333
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
334
|
+
|
|
335
|
+
// ── Tool: outline_search_documents ─────────────────────────────────────
|
|
336
|
+
if (enabledTools.includes('searchDocuments')) {
|
|
337
|
+
tools.push(new DynamicStructuredTool({
|
|
338
|
+
name: 'outline_search_documents',
|
|
339
|
+
description: toolDescriptionOverride ||
|
|
340
|
+
'Full-text search across all documents in Outline Wiki. Returns matching documents with context snippets. ' +
|
|
341
|
+
'Use this to find knowledge base articles, documentation, or any content stored in Outline. ' +
|
|
342
|
+
'Can filter by collection, status (draft/published/archived), date range, and user.',
|
|
343
|
+
schema: z ? z.object({
|
|
344
|
+
query: z.string().describe('The search query to find relevant documents'),
|
|
345
|
+
collection_id: strOpt('UUID of the collection to limit search within'),
|
|
346
|
+
status_filter: strOpt('Filter by status: "draft" | "published" | "archived" (default: published)'),
|
|
347
|
+
date_filter: strOpt('Only return documents updated within this period: "day" | "week" | "month" | "year"'),
|
|
348
|
+
limit: numOpt('Maximum results to return (default: 25)'),
|
|
349
|
+
}) : null,
|
|
350
|
+
func: async ({ query, collection_id, status_filter, date_filter, limit } = {}) => {
|
|
351
|
+
const runIndex = startToolRun({ tool: 'outline_search_documents', query });
|
|
352
|
+
log('debug', '[Outline] outline_search_documents called', { query });
|
|
353
|
+
try {
|
|
354
|
+
if (!query) return JSON.stringify({ error: 'query is required' });
|
|
355
|
+
const body = { query };
|
|
356
|
+
if (collection_id) body.collectionId = collection_id;
|
|
357
|
+
if (status_filter) body.statusFilter = [status_filter];
|
|
358
|
+
if (date_filter) body.dateFilter = date_filter;
|
|
359
|
+
if (limit) body.limit = limit;
|
|
360
|
+
const res = await apiPost('/documents.search', body);
|
|
361
|
+
const result = { success: true, data: res.data, pagination: res.pagination };
|
|
362
|
+
log('info', '[Outline] outline_search_documents succeeded', { count: res.data && res.data.length });
|
|
363
|
+
endToolRun(runIndex, result);
|
|
364
|
+
return JSON.stringify(result);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
const errObj = { error: err.message || String(err) };
|
|
367
|
+
log('error', '[Outline] outline_search_documents failed', errObj);
|
|
368
|
+
endToolRun(runIndex, errObj);
|
|
369
|
+
return JSON.stringify(errObj);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Tool: outline_search_titles ────────────────────────────────────────
|
|
376
|
+
if (enabledTools.includes('searchTitles')) {
|
|
377
|
+
tools.push(new DynamicStructuredTool({
|
|
378
|
+
name: 'outline_search_titles',
|
|
379
|
+
description: toolDescriptionOverride ||
|
|
380
|
+
'Fast title-only search for documents in Outline Wiki. Much faster than full-text search. ' +
|
|
381
|
+
'Use this when you need to quickly find a document by its title or heading.',
|
|
382
|
+
schema: z ? z.object({
|
|
383
|
+
query: z.string().describe('Title search query'),
|
|
384
|
+
collection_id: strOpt('UUID of the collection to limit search within'),
|
|
385
|
+
limit: numOpt('Maximum results to return (default: 25)'),
|
|
386
|
+
}) : null,
|
|
387
|
+
func: async ({ query, collection_id, limit } = {}) => {
|
|
388
|
+
const runIndex = startToolRun({ tool: 'outline_search_titles', query });
|
|
389
|
+
log('debug', '[Outline] outline_search_titles called', { query });
|
|
390
|
+
try {
|
|
391
|
+
if (!query) return JSON.stringify({ error: 'query is required' });
|
|
392
|
+
const body = { query };
|
|
393
|
+
if (collection_id) body.collectionId = collection_id;
|
|
394
|
+
if (limit) body.limit = limit;
|
|
395
|
+
const res = await apiPost('/documents.search_titles', body);
|
|
396
|
+
const result = { success: true, data: res.data, pagination: res.pagination };
|
|
397
|
+
log('info', '[Outline] outline_search_titles succeeded', { count: res.data && res.data.length });
|
|
398
|
+
endToolRun(runIndex, result);
|
|
399
|
+
return JSON.stringify(result);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const errObj = { error: err.message || String(err) };
|
|
402
|
+
log('error', '[Outline] outline_search_titles failed', errObj);
|
|
403
|
+
endToolRun(runIndex, errObj);
|
|
404
|
+
return JSON.stringify(errObj);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── Tool: outline_create_document ──────────────────────────────────────
|
|
411
|
+
if (enabledTools.includes('createDocument')) {
|
|
412
|
+
tools.push(new DynamicStructuredTool({
|
|
413
|
+
name: 'outline_create_document',
|
|
414
|
+
description: toolDescriptionOverride ||
|
|
415
|
+
'Create a new document in Outline Wiki. The document body is in Markdown format. ' +
|
|
416
|
+
'Documents can be created as drafts (publish=false) or immediately published (publish=true). ' +
|
|
417
|
+
'Requires either a collection_id (for a top-level document) or a parent_document_id (for a nested child document).',
|
|
418
|
+
schema: z ? z.object({
|
|
419
|
+
title: z.string().describe('The document title'),
|
|
420
|
+
text: strOpt('The document body in Markdown format'),
|
|
421
|
+
collection_id: strOpt('UUID of the collection to place the document in (required unless parent_document_id is given)'),
|
|
422
|
+
parent_document_id: strOpt('UUID of the parent document to create a nested child document under'),
|
|
423
|
+
publish: boolOpt('Whether to immediately publish the document (default: false = draft)'),
|
|
424
|
+
template_id: strOpt('UUID of a template to use as the basis for this document'),
|
|
425
|
+
}) : null,
|
|
426
|
+
func: async ({ title, text, collection_id, parent_document_id, publish, template_id } = {}) => {
|
|
427
|
+
const runIndex = startToolRun({ tool: 'outline_create_document', title });
|
|
428
|
+
log('debug', '[Outline] outline_create_document called', { title });
|
|
429
|
+
try {
|
|
430
|
+
if (!title) return JSON.stringify({ error: 'title is required' });
|
|
431
|
+
if (!collection_id && !parent_document_id) return JSON.stringify({ error: 'Either collection_id or parent_document_id is required' });
|
|
432
|
+
const body = { title };
|
|
433
|
+
if (text) body.text = text;
|
|
434
|
+
if (collection_id) body.collectionId = collection_id;
|
|
435
|
+
if (parent_document_id) body.parentDocumentId = parent_document_id;
|
|
436
|
+
if (publish !== undefined) body.publish = publish;
|
|
437
|
+
if (template_id) body.templateId = template_id;
|
|
438
|
+
const res = await apiPost('/documents.create', body);
|
|
439
|
+
const result = { success: true, data: res.data };
|
|
440
|
+
log('info', '[Outline] outline_create_document succeeded', { id: res.data && res.data.id });
|
|
441
|
+
endToolRun(runIndex, result);
|
|
442
|
+
return JSON.stringify(result);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
const errObj = { error: err.message || String(err) };
|
|
445
|
+
log('error', '[Outline] outline_create_document failed', errObj);
|
|
446
|
+
endToolRun(runIndex, errObj);
|
|
447
|
+
return JSON.stringify(errObj);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Tool: outline_get_document ─────────────────────────────────────────
|
|
454
|
+
if (enabledTools.includes('getDocument')) {
|
|
455
|
+
tools.push(new DynamicStructuredTool({
|
|
456
|
+
name: 'outline_get_document',
|
|
457
|
+
description: toolDescriptionOverride ||
|
|
458
|
+
'Retrieve the full content of a document from Outline Wiki by its ID or URL slug. ' +
|
|
459
|
+
'Returns the document title, body (in Markdown), metadata (created/updated dates, author, collection), and policies. ' +
|
|
460
|
+
'Use this to read the full content of a specific document.',
|
|
461
|
+
schema: z ? z.object({
|
|
462
|
+
id: z.string().describe('Document UUID or URL slug (urlId)'),
|
|
463
|
+
}) : null,
|
|
464
|
+
func: async ({ id } = {}) => {
|
|
465
|
+
const runIndex = startToolRun({ tool: 'outline_get_document', id });
|
|
466
|
+
log('debug', '[Outline] outline_get_document called', { id });
|
|
467
|
+
try {
|
|
468
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
469
|
+
const res = await apiPost('/documents.info', { id });
|
|
470
|
+
const result = { success: true, data: res.data };
|
|
471
|
+
log('info', '[Outline] outline_get_document succeeded', { id });
|
|
472
|
+
endToolRun(runIndex, result);
|
|
473
|
+
return JSON.stringify(result);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
const errObj = { error: err.message || String(err) };
|
|
476
|
+
log('error', '[Outline] outline_get_document failed', errObj);
|
|
477
|
+
endToolRun(runIndex, errObj);
|
|
478
|
+
return JSON.stringify(errObj);
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Tool: outline_update_document ──────────────────────────────────────
|
|
485
|
+
if (enabledTools.includes('updateDocument')) {
|
|
486
|
+
tools.push(new DynamicStructuredTool({
|
|
487
|
+
name: 'outline_update_document',
|
|
488
|
+
description: toolDescriptionOverride ||
|
|
489
|
+
'Update an existing document in Outline Wiki. Can update the title, body (Markdown), and publish status. ' +
|
|
490
|
+
'To update content, provide the full new body in Markdown format. ' +
|
|
491
|
+
'To publish a draft, set publish=true.',
|
|
492
|
+
schema: z ? z.object({
|
|
493
|
+
id: z.string().describe('Document UUID or URL slug (urlId)'),
|
|
494
|
+
title: strOpt('New document title'),
|
|
495
|
+
text: strOpt('New document body in Markdown format (replaces existing content)'),
|
|
496
|
+
publish: boolOpt('Set to true to publish a draft, or false to unpublish'),
|
|
497
|
+
full_width: boolOpt('Whether the document should be displayed in full width'),
|
|
498
|
+
}) : null,
|
|
499
|
+
func: async ({ id, title, text, publish, full_width } = {}) => {
|
|
500
|
+
const runIndex = startToolRun({ tool: 'outline_update_document', id });
|
|
501
|
+
log('debug', '[Outline] outline_update_document called', { id });
|
|
502
|
+
try {
|
|
503
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
504
|
+
const body = { id };
|
|
505
|
+
if (title !== undefined) body.title = title;
|
|
506
|
+
if (text !== undefined) body.text = text;
|
|
507
|
+
if (publish !== undefined) body.publish = publish;
|
|
508
|
+
if (full_width !== undefined) body.fullWidth = full_width;
|
|
509
|
+
const res = await apiPost('/documents.update', body);
|
|
510
|
+
const result = { success: true, data: res.data };
|
|
511
|
+
log('info', '[Outline] outline_update_document succeeded', { id });
|
|
512
|
+
endToolRun(runIndex, result);
|
|
513
|
+
return JSON.stringify(result);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
const errObj = { error: err.message || String(err) };
|
|
516
|
+
log('error', '[Outline] outline_update_document failed', errObj);
|
|
517
|
+
endToolRun(runIndex, errObj);
|
|
518
|
+
return JSON.stringify(errObj);
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ── Tool: outline_delete_document ──────────────────────────────────────
|
|
525
|
+
if (enabledTools.includes('deleteDocument')) {
|
|
526
|
+
tools.push(new DynamicStructuredTool({
|
|
527
|
+
name: 'outline_delete_document',
|
|
528
|
+
description: toolDescriptionOverride ||
|
|
529
|
+
'Delete a document in Outline Wiki. By default moves it to trash (recoverable for 30 days). ' +
|
|
530
|
+
'Set permanent=true to permanently and immediately destroy the document with no recovery option. ' +
|
|
531
|
+
'Use with caution.',
|
|
532
|
+
schema: z ? z.object({
|
|
533
|
+
id: z.string().describe('Document UUID or URL slug (urlId)'),
|
|
534
|
+
permanent: boolOpt('Set to true to permanently destroy (no recovery). Default: false (moves to trash)'),
|
|
535
|
+
}) : null,
|
|
536
|
+
func: async ({ id, permanent } = {}) => {
|
|
537
|
+
const runIndex = startToolRun({ tool: 'outline_delete_document', id });
|
|
538
|
+
log('debug', '[Outline] outline_delete_document called', { id, permanent });
|
|
539
|
+
try {
|
|
540
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
541
|
+
const body = { id };
|
|
542
|
+
if (permanent !== undefined) body.permanent = permanent;
|
|
543
|
+
const res = await apiPost('/documents.delete', body);
|
|
544
|
+
const result = { success: true, ok: res.ok !== false };
|
|
545
|
+
log('info', '[Outline] outline_delete_document succeeded', { id });
|
|
546
|
+
endToolRun(runIndex, result);
|
|
547
|
+
return JSON.stringify(result);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
const errObj = { error: err.message || String(err) };
|
|
550
|
+
log('error', '[Outline] outline_delete_document failed', errObj);
|
|
551
|
+
endToolRun(runIndex, errObj);
|
|
552
|
+
return JSON.stringify(errObj);
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ── Tool: outline_list_documents ───────────────────────────────────────
|
|
559
|
+
if (enabledTools.includes('listDocuments')) {
|
|
560
|
+
tools.push(new DynamicStructuredTool({
|
|
561
|
+
name: 'outline_list_documents',
|
|
562
|
+
description: toolDescriptionOverride ||
|
|
563
|
+
'List documents in Outline Wiki. Can filter by collection, parent document, status (draft/published/archived), and user. ' +
|
|
564
|
+
'Returns a paginated list of documents with their metadata.',
|
|
565
|
+
schema: z ? z.object({
|
|
566
|
+
collection_id: strOpt('UUID of collection to list documents from'),
|
|
567
|
+
parent_document_id: strOpt('UUID of parent document to list children of'),
|
|
568
|
+
status_filter: strOpt('Filter by status: "draft" | "published" | "archived"'),
|
|
569
|
+
limit: numOpt('Maximum results to return (default: 25)'),
|
|
570
|
+
offset: numOpt('Offset for pagination (default: 0)'),
|
|
571
|
+
}) : null,
|
|
572
|
+
func: async ({ collection_id, parent_document_id, status_filter, limit, offset } = {}) => {
|
|
573
|
+
const runIndex = startToolRun({ tool: 'outline_list_documents' });
|
|
574
|
+
log('debug', '[Outline] outline_list_documents called');
|
|
575
|
+
try {
|
|
576
|
+
const body = {};
|
|
577
|
+
if (collection_id) body.collectionId = collection_id;
|
|
578
|
+
if (parent_document_id) body.parentDocumentId = parent_document_id;
|
|
579
|
+
if (status_filter) body.statusFilter = [status_filter];
|
|
580
|
+
if (limit) body.limit = limit;
|
|
581
|
+
if (offset) body.offset = offset;
|
|
582
|
+
const res = await apiPost('/documents.list', body);
|
|
583
|
+
const result = { success: true, data: res.data, pagination: res.pagination };
|
|
584
|
+
log('info', '[Outline] outline_list_documents succeeded', { count: res.data && res.data.length });
|
|
585
|
+
endToolRun(runIndex, result);
|
|
586
|
+
return JSON.stringify(result);
|
|
587
|
+
} catch (err) {
|
|
588
|
+
const errObj = { error: err.message || String(err) };
|
|
589
|
+
log('error', '[Outline] outline_list_documents failed', errObj);
|
|
590
|
+
endToolRun(runIndex, errObj);
|
|
591
|
+
return JSON.stringify(errObj);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── Tool: outline_import_document ──────────────────────────────────────
|
|
598
|
+
if (enabledTools.includes('importDocument')) {
|
|
599
|
+
tools.push(new DynamicStructuredTool({
|
|
600
|
+
name: 'outline_import_document',
|
|
601
|
+
description: toolDescriptionOverride ||
|
|
602
|
+
'Import a file as a new document in Outline Wiki. Supported formats: Markdown (.md), plain text (.txt), ' +
|
|
603
|
+
'HTML (.html), Word (.docx), CSV (.csv), TSV (.tsv). ' +
|
|
604
|
+
'The file must be provided via binary_property_name — the name of the binary property where the file was stored by a previous tool ' +
|
|
605
|
+
'(e.g., telegram_get_file, gotenberg_url_to_pdf, or any tool that stores binary files). ' +
|
|
606
|
+
'Requires either collection_id or parent_document_id.',
|
|
607
|
+
schema: z ? z.object({
|
|
608
|
+
binary_property_name: z.string().describe('Name of the binary property containing the file to import (binaryPropertyName from a previous tool)'),
|
|
609
|
+
collection_id: strOpt('UUID of the collection to import into. Required unless parent_document_id is given.'),
|
|
610
|
+
parent_document_id: strOpt('UUID of the parent document to import under (creates nested document)'),
|
|
611
|
+
publish: boolOpt('Whether to immediately publish the imported document (default: false)'),
|
|
612
|
+
filename: strOpt('Override filename (including extension, e.g., "notes.md"). If omitted, uses filename from binary metadata.'),
|
|
613
|
+
}) : null,
|
|
614
|
+
func: async ({ binary_property_name, collection_id, parent_document_id, publish, filename } = {}) => {
|
|
615
|
+
const runIndex = startToolRun({ tool: 'outline_import_document', binary_property_name });
|
|
616
|
+
log('debug', '[Outline] outline_import_document called', { binary_property_name });
|
|
617
|
+
try {
|
|
618
|
+
if (!binary_property_name) return JSON.stringify({ error: 'binary_property_name is required' });
|
|
619
|
+
if (!collection_id && !parent_document_id) return JSON.stringify({ error: 'Either collection_id or parent_document_id is required' });
|
|
620
|
+
|
|
621
|
+
const buffer = await getBuffer(binary_property_name);
|
|
622
|
+
const meta = getMeta(binary_property_name);
|
|
623
|
+
const resolvedFilename = filename || (meta && meta.fileName) || 'document.md';
|
|
624
|
+
const contentType = guessMimeType(resolvedFilename);
|
|
625
|
+
|
|
626
|
+
const formData = {
|
|
627
|
+
file: {
|
|
628
|
+
value: buffer,
|
|
629
|
+
options: { filename: resolvedFilename, contentType },
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
if (collection_id) formData.collectionId = collection_id;
|
|
633
|
+
if (parent_document_id) formData.parentDocumentId = parent_document_id;
|
|
634
|
+
if (publish !== undefined) formData.publish = String(publish);
|
|
635
|
+
|
|
636
|
+
const res = await apiPostMultipart('/documents.import', formData);
|
|
637
|
+
const result = { success: true, data: res.data };
|
|
638
|
+
log('info', '[Outline] outline_import_document succeeded', { id: res.data && res.data.id });
|
|
639
|
+
endToolRun(runIndex, result);
|
|
640
|
+
return JSON.stringify(result);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
const errObj = { error: err.message || String(err) };
|
|
643
|
+
log('error', '[Outline] outline_import_document failed', errObj);
|
|
644
|
+
endToolRun(runIndex, errObj);
|
|
645
|
+
return JSON.stringify(errObj);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ── Tool: outline_export_document ──────────────────────────────────────
|
|
652
|
+
if (enabledTools.includes('exportDocument')) {
|
|
653
|
+
tools.push(new DynamicStructuredTool({
|
|
654
|
+
name: 'outline_export_document',
|
|
655
|
+
description: toolDescriptionOverride ||
|
|
656
|
+
'Export a document from Outline Wiki as Markdown. ' +
|
|
657
|
+
'Stores the exported document in a binary property and returns its name. ' +
|
|
658
|
+
'The binary property can then be passed to other tools (e.g., telegram_send_document, gotenberg_libreoffice_convert).',
|
|
659
|
+
schema: z ? z.object({
|
|
660
|
+
id: z.string().describe('Document UUID or URL slug (urlId)'),
|
|
661
|
+
output_filename: strOpt('Desired filename for the exported document (default: document.md)'),
|
|
662
|
+
}) : null,
|
|
663
|
+
func: async ({ id, output_filename } = {}) => {
|
|
664
|
+
const runIndex = startToolRun({ tool: 'outline_export_document', id });
|
|
665
|
+
log('debug', '[Outline] outline_export_document called', { id });
|
|
666
|
+
try {
|
|
667
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
668
|
+
const res = await apiPost('/documents.export', { id });
|
|
669
|
+
// The response contains the markdown text in data
|
|
670
|
+
const markdown = (res && res.data) ? res.data : JSON.stringify(res);
|
|
671
|
+
const buffer = Buffer.from(typeof markdown === 'string' ? markdown : JSON.stringify(markdown), 'utf-8');
|
|
672
|
+
const filename = output_filename || 'document.md';
|
|
673
|
+
const resultStr = await storeFile(buffer, filename, 'text/markdown');
|
|
674
|
+
const result = JSON.parse(resultStr);
|
|
675
|
+
log('info', '[Outline] outline_export_document succeeded', { id, sizeKb: result.sizeKb });
|
|
676
|
+
endToolRun(runIndex, result);
|
|
677
|
+
return resultStr;
|
|
678
|
+
} catch (err) {
|
|
679
|
+
const errObj = { error: err.message || String(err) };
|
|
680
|
+
log('error', '[Outline] outline_export_document failed', errObj);
|
|
681
|
+
endToolRun(runIndex, errObj);
|
|
682
|
+
return JSON.stringify(errObj);
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
}));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Tool: outline_archive_document ─────────────────────────────────────
|
|
689
|
+
if (enabledTools.includes('archiveDocument')) {
|
|
690
|
+
tools.push(new DynamicStructuredTool({
|
|
691
|
+
name: 'outline_archive_document',
|
|
692
|
+
description: toolDescriptionOverride ||
|
|
693
|
+
'Archive a document in Outline Wiki. Archiving moves the document out of sight while retaining the ability to search and restore it later. ' +
|
|
694
|
+
'Use this for outdated content that should not appear in normal views but must be preserved.',
|
|
695
|
+
schema: z ? z.object({
|
|
696
|
+
id: z.string().describe('Document UUID or URL slug (urlId)'),
|
|
697
|
+
}) : null,
|
|
698
|
+
func: async ({ id } = {}) => {
|
|
699
|
+
const runIndex = startToolRun({ tool: 'outline_archive_document', id });
|
|
700
|
+
log('debug', '[Outline] outline_archive_document called', { id });
|
|
701
|
+
try {
|
|
702
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
703
|
+
const res = await apiPost('/documents.archive', { id });
|
|
704
|
+
const result = { success: true, data: res.data };
|
|
705
|
+
log('info', '[Outline] outline_archive_document succeeded', { id });
|
|
706
|
+
endToolRun(runIndex, result);
|
|
707
|
+
return JSON.stringify(result);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
const errObj = { error: err.message || String(err) };
|
|
710
|
+
log('error', '[Outline] outline_archive_document failed', errObj);
|
|
711
|
+
endToolRun(runIndex, errObj);
|
|
712
|
+
return JSON.stringify(errObj);
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ── Tool: outline_restore_document ─────────────────────────────────────
|
|
719
|
+
if (enabledTools.includes('restoreDocument')) {
|
|
720
|
+
tools.push(new DynamicStructuredTool({
|
|
721
|
+
name: 'outline_restore_document',
|
|
722
|
+
description: toolDescriptionOverride ||
|
|
723
|
+
'Restore an archived or deleted document in Outline Wiki. Can optionally restore to a specific revision.',
|
|
724
|
+
schema: z ? z.object({
|
|
725
|
+
id: z.string().describe('Document UUID or URL slug (urlId)'),
|
|
726
|
+
collection_id: strOpt('UUID of the collection to restore the document into'),
|
|
727
|
+
revision_id: strOpt('UUID of a specific revision to restore the document to (optional)'),
|
|
728
|
+
}) : null,
|
|
729
|
+
func: async ({ id, collection_id, revision_id } = {}) => {
|
|
730
|
+
const runIndex = startToolRun({ tool: 'outline_restore_document', id });
|
|
731
|
+
log('debug', '[Outline] outline_restore_document called', { id });
|
|
732
|
+
try {
|
|
733
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
734
|
+
const body = { id };
|
|
735
|
+
if (collection_id) body.collectionId = collection_id;
|
|
736
|
+
if (revision_id) body.revisionId = revision_id;
|
|
737
|
+
const res = await apiPost('/documents.restore', body);
|
|
738
|
+
const result = { success: true, data: res.data };
|
|
739
|
+
log('info', '[Outline] outline_restore_document succeeded', { id });
|
|
740
|
+
endToolRun(runIndex, result);
|
|
741
|
+
return JSON.stringify(result);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
const errObj = { error: err.message || String(err) };
|
|
744
|
+
log('error', '[Outline] outline_restore_document failed', errObj);
|
|
745
|
+
endToolRun(runIndex, errObj);
|
|
746
|
+
return JSON.stringify(errObj);
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ── Tool: outline_move_document ────────────────────────────────────────
|
|
753
|
+
if (enabledTools.includes('moveDocument')) {
|
|
754
|
+
tools.push(new DynamicStructuredTool({
|
|
755
|
+
name: 'outline_move_document',
|
|
756
|
+
description: toolDescriptionOverride ||
|
|
757
|
+
'Move a document to a different collection or parent document in Outline Wiki. ' +
|
|
758
|
+
'If no parent_document_id is given the document moves to the collection root.',
|
|
759
|
+
schema: z ? z.object({
|
|
760
|
+
id: z.string().describe('Document UUID or URL slug (urlId)'),
|
|
761
|
+
collection_id: strOpt('UUID of the target collection'),
|
|
762
|
+
parent_document_id: strOpt('UUID of the new parent document (omit to move to collection root)'),
|
|
763
|
+
}) : null,
|
|
764
|
+
func: async ({ id, collection_id, parent_document_id } = {}) => {
|
|
765
|
+
const runIndex = startToolRun({ tool: 'outline_move_document', id });
|
|
766
|
+
log('debug', '[Outline] outline_move_document called', { id });
|
|
767
|
+
try {
|
|
768
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
769
|
+
const body = { id };
|
|
770
|
+
if (collection_id) body.collectionId = collection_id;
|
|
771
|
+
if (parent_document_id) body.parentDocumentId = parent_document_id;
|
|
772
|
+
const res = await apiPost('/documents.move', body);
|
|
773
|
+
const result = { success: true, data: res.data };
|
|
774
|
+
log('info', '[Outline] outline_move_document succeeded', { id });
|
|
775
|
+
endToolRun(runIndex, result);
|
|
776
|
+
return JSON.stringify(result);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
const errObj = { error: err.message || String(err) };
|
|
779
|
+
log('error', '[Outline] outline_move_document failed', errObj);
|
|
780
|
+
endToolRun(runIndex, errObj);
|
|
781
|
+
return JSON.stringify(errObj);
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
}));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// ── Tool: outline_answer_question ──────────────────────────────────────
|
|
788
|
+
if (enabledTools.includes('answerQuestion')) {
|
|
789
|
+
tools.push(new DynamicStructuredTool({
|
|
790
|
+
name: 'outline_answer_question',
|
|
791
|
+
description: toolDescriptionOverride ||
|
|
792
|
+
'Query Outline Wiki documents with a natural language question. ' +
|
|
793
|
+
'Outline\'s AI will attempt to find and synthesize an answer from relevant documents. ' +
|
|
794
|
+
'Note: This requires the "AI answers" feature to be enabled for the workspace (Business/Enterprise/Cloud plans only).',
|
|
795
|
+
schema: z ? z.object({
|
|
796
|
+
query: z.string().describe('A natural language question to ask against the knowledge base (e.g., "What is our vacation policy?")'),
|
|
797
|
+
collection_id: strOpt('UUID of the collection to search within'),
|
|
798
|
+
document_id: strOpt('UUID of a specific document to search within'),
|
|
799
|
+
}) : null,
|
|
800
|
+
func: async ({ query, collection_id, document_id } = {}) => {
|
|
801
|
+
const runIndex = startToolRun({ tool: 'outline_answer_question', query });
|
|
802
|
+
log('debug', '[Outline] outline_answer_question called', { query });
|
|
803
|
+
try {
|
|
804
|
+
if (!query) return JSON.stringify({ error: 'query is required' });
|
|
805
|
+
const body = { query };
|
|
806
|
+
if (collection_id) body.collectionId = collection_id;
|
|
807
|
+
if (document_id) body.documentId = document_id;
|
|
808
|
+
const res = await apiPost('/documents.answerQuestion', body);
|
|
809
|
+
const result = { success: true, search: res.search, documents: res.documents };
|
|
810
|
+
log('info', '[Outline] outline_answer_question succeeded');
|
|
811
|
+
endToolRun(runIndex, result);
|
|
812
|
+
return JSON.stringify(result);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
const errObj = { error: err.message || String(err) };
|
|
815
|
+
log('error', '[Outline] outline_answer_question failed', errObj);
|
|
816
|
+
endToolRun(runIndex, errObj);
|
|
817
|
+
return JSON.stringify(errObj);
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
}));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
824
|
+
// COLLECTIONS
|
|
825
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
826
|
+
|
|
827
|
+
// ── Tool: outline_list_collections ─────────────────────────────────────
|
|
828
|
+
if (enabledTools.includes('listCollections')) {
|
|
829
|
+
tools.push(new DynamicStructuredTool({
|
|
830
|
+
name: 'outline_list_collections',
|
|
831
|
+
description: toolDescriptionOverride ||
|
|
832
|
+
'List all collections in Outline Wiki that the current API key has access to. ' +
|
|
833
|
+
'Collections are the top-level groupings of documents in Outline. ' +
|
|
834
|
+
'Returns collection IDs, names, descriptions, and document counts.',
|
|
835
|
+
schema: z ? z.object({
|
|
836
|
+
limit: numOpt('Maximum results to return (default: 25)'),
|
|
837
|
+
offset: numOpt('Offset for pagination (default: 0)'),
|
|
838
|
+
}) : null,
|
|
839
|
+
func: async ({ limit, offset } = {}) => {
|
|
840
|
+
const runIndex = startToolRun({ tool: 'outline_list_collections' });
|
|
841
|
+
log('debug', '[Outline] outline_list_collections called');
|
|
842
|
+
try {
|
|
843
|
+
const body = {};
|
|
844
|
+
if (limit) body.limit = limit;
|
|
845
|
+
if (offset) body.offset = offset;
|
|
846
|
+
const res = await apiPost('/collections.list', body);
|
|
847
|
+
const result = { success: true, data: res.data, pagination: res.pagination };
|
|
848
|
+
log('info', '[Outline] outline_list_collections succeeded', { count: res.data && res.data.length });
|
|
849
|
+
endToolRun(runIndex, result);
|
|
850
|
+
return JSON.stringify(result);
|
|
851
|
+
} catch (err) {
|
|
852
|
+
const errObj = { error: err.message || String(err) };
|
|
853
|
+
log('error', '[Outline] outline_list_collections failed', errObj);
|
|
854
|
+
endToolRun(runIndex, errObj);
|
|
855
|
+
return JSON.stringify(errObj);
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ── Tool: outline_create_collection ───────────────────────────────────
|
|
862
|
+
if (enabledTools.includes('createCollection')) {
|
|
863
|
+
tools.push(new DynamicStructuredTool({
|
|
864
|
+
name: 'outline_create_collection',
|
|
865
|
+
description: toolDescriptionOverride ||
|
|
866
|
+
'Create a new collection in Outline Wiki. Collections are the top-level organizational units that group related documents. ' +
|
|
867
|
+
'You can set name, description, icon, color, sharing settings, and default permission.',
|
|
868
|
+
schema: z ? z.object({
|
|
869
|
+
name: z.string().describe('Collection name'),
|
|
870
|
+
description: strOpt('A brief description of the collection (markdown supported)'),
|
|
871
|
+
permission: strOpt('Default permission: "read" | "read_write" (default: read_write)'),
|
|
872
|
+
sharing: boolOpt('Whether public sharing of documents is allowed (default: false)'),
|
|
873
|
+
color: strOpt('Hex color code for the collection icon (e.g., "#FF5733")'),
|
|
874
|
+
icon: strOpt('Icon name from outline-icons or an emoji character'),
|
|
875
|
+
}) : null,
|
|
876
|
+
func: async ({ name, description, permission, sharing, color, icon } = {}) => {
|
|
877
|
+
const runIndex = startToolRun({ tool: 'outline_create_collection', name });
|
|
878
|
+
log('debug', '[Outline] outline_create_collection called', { name });
|
|
879
|
+
try {
|
|
880
|
+
if (!name) return JSON.stringify({ error: 'name is required' });
|
|
881
|
+
const body = { name };
|
|
882
|
+
if (description) body.description = description;
|
|
883
|
+
if (permission) body.permission = permission;
|
|
884
|
+
if (sharing !== undefined) body.sharing = sharing;
|
|
885
|
+
if (color) body.color = color;
|
|
886
|
+
if (icon) body.icon = icon;
|
|
887
|
+
const res = await apiPost('/collections.create', body);
|
|
888
|
+
const result = { success: true, data: res.data };
|
|
889
|
+
log('info', '[Outline] outline_create_collection succeeded', { id: res.data && res.data.id });
|
|
890
|
+
endToolRun(runIndex, result);
|
|
891
|
+
return JSON.stringify(result);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
const errObj = { error: err.message || String(err) };
|
|
894
|
+
log('error', '[Outline] outline_create_collection failed', errObj);
|
|
895
|
+
endToolRun(runIndex, errObj);
|
|
896
|
+
return JSON.stringify(errObj);
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
}));
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ── Tool: outline_get_collection ───────────────────────────────────────
|
|
903
|
+
if (enabledTools.includes('getCollection')) {
|
|
904
|
+
tools.push(new DynamicStructuredTool({
|
|
905
|
+
name: 'outline_get_collection',
|
|
906
|
+
description: toolDescriptionOverride ||
|
|
907
|
+
'Retrieve details of a specific collection in Outline Wiki by its UUID.',
|
|
908
|
+
schema: z ? z.object({
|
|
909
|
+
id: z.string().describe('Collection UUID'),
|
|
910
|
+
}) : null,
|
|
911
|
+
func: async ({ id } = {}) => {
|
|
912
|
+
const runIndex = startToolRun({ tool: 'outline_get_collection', id });
|
|
913
|
+
log('debug', '[Outline] outline_get_collection called', { id });
|
|
914
|
+
try {
|
|
915
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
916
|
+
const res = await apiPost('/collections.info', { id });
|
|
917
|
+
const result = { success: true, data: res.data };
|
|
918
|
+
log('info', '[Outline] outline_get_collection succeeded', { id });
|
|
919
|
+
endToolRun(runIndex, result);
|
|
920
|
+
return JSON.stringify(result);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
const errObj = { error: err.message || String(err) };
|
|
923
|
+
log('error', '[Outline] outline_get_collection failed', errObj);
|
|
924
|
+
endToolRun(runIndex, errObj);
|
|
925
|
+
return JSON.stringify(errObj);
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── Tool: outline_update_collection ───────────────────────────────────
|
|
932
|
+
if (enabledTools.includes('updateCollection')) {
|
|
933
|
+
tools.push(new DynamicStructuredTool({
|
|
934
|
+
name: 'outline_update_collection',
|
|
935
|
+
description: toolDescriptionOverride ||
|
|
936
|
+
'Update properties of an existing collection in Outline Wiki.',
|
|
937
|
+
schema: z ? z.object({
|
|
938
|
+
id: z.string().describe('Collection UUID'),
|
|
939
|
+
name: strOpt('New collection name'),
|
|
940
|
+
description: strOpt('New description (markdown supported)'),
|
|
941
|
+
permission: strOpt('Default permission: "read" | "read_write"'),
|
|
942
|
+
sharing: boolOpt('Whether public sharing of documents is allowed'),
|
|
943
|
+
color: strOpt('Hex color code for the collection icon'),
|
|
944
|
+
icon: strOpt('Icon name or emoji'),
|
|
945
|
+
}) : null,
|
|
946
|
+
func: async ({ id, name, description, permission, sharing, color, icon } = {}) => {
|
|
947
|
+
const runIndex = startToolRun({ tool: 'outline_update_collection', id });
|
|
948
|
+
log('debug', '[Outline] outline_update_collection called', { id });
|
|
949
|
+
try {
|
|
950
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
951
|
+
const body = { id };
|
|
952
|
+
if (name) body.name = name;
|
|
953
|
+
if (description) body.description = description;
|
|
954
|
+
if (permission) body.permission = permission;
|
|
955
|
+
if (sharing !== undefined) body.sharing = sharing;
|
|
956
|
+
if (color) body.color = color;
|
|
957
|
+
if (icon) body.icon = icon;
|
|
958
|
+
const res = await apiPost('/collections.update', body);
|
|
959
|
+
const result = { success: true, data: res.data };
|
|
960
|
+
log('info', '[Outline] outline_update_collection succeeded', { id });
|
|
961
|
+
endToolRun(runIndex, result);
|
|
962
|
+
return JSON.stringify(result);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
const errObj = { error: err.message || String(err) };
|
|
965
|
+
log('error', '[Outline] outline_update_collection failed', errObj);
|
|
966
|
+
endToolRun(runIndex, errObj);
|
|
967
|
+
return JSON.stringify(errObj);
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
}));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// ── Tool: outline_delete_collection ───────────────────────────────────
|
|
974
|
+
if (enabledTools.includes('deleteCollection')) {
|
|
975
|
+
tools.push(new DynamicStructuredTool({
|
|
976
|
+
name: 'outline_delete_collection',
|
|
977
|
+
description: toolDescriptionOverride ||
|
|
978
|
+
'Delete a collection and ALL of its documents in Outline Wiki. THIS ACTION CANNOT BE UNDONE. ' +
|
|
979
|
+
'Use with extreme caution — this permanently removes the collection and all documents within it.',
|
|
980
|
+
schema: z ? z.object({
|
|
981
|
+
id: z.string().describe('Collection UUID to delete'),
|
|
982
|
+
}) : null,
|
|
983
|
+
func: async ({ id } = {}) => {
|
|
984
|
+
const runIndex = startToolRun({ tool: 'outline_delete_collection', id });
|
|
985
|
+
log('debug', '[Outline] outline_delete_collection called', { id });
|
|
986
|
+
try {
|
|
987
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
988
|
+
const res = await apiPost('/collections.delete', { id });
|
|
989
|
+
const result = { success: true, ok: res.ok !== false };
|
|
990
|
+
log('info', '[Outline] outline_delete_collection succeeded', { id });
|
|
991
|
+
endToolRun(runIndex, result);
|
|
992
|
+
return JSON.stringify(result);
|
|
993
|
+
} catch (err) {
|
|
994
|
+
const errObj = { error: err.message || String(err) };
|
|
995
|
+
log('error', '[Outline] outline_delete_collection failed', errObj);
|
|
996
|
+
endToolRun(runIndex, errObj);
|
|
997
|
+
return JSON.stringify(errObj);
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
}));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// ── Tool: outline_get_collection_documents ─────────────────────────────
|
|
1004
|
+
if (enabledTools.includes('getCollectionDocuments')) {
|
|
1005
|
+
tools.push(new DynamicStructuredTool({
|
|
1006
|
+
name: 'outline_get_collection_documents',
|
|
1007
|
+
description: toolDescriptionOverride ||
|
|
1008
|
+
'Get the full document hierarchy/tree for a collection in Outline Wiki. ' +
|
|
1009
|
+
'Returns a nested tree of NavigationNode objects showing the collection structure. ' +
|
|
1010
|
+
'Use this to understand how documents are organized within a collection.',
|
|
1011
|
+
schema: z ? z.object({
|
|
1012
|
+
id: z.string().describe('Collection UUID'),
|
|
1013
|
+
}) : null,
|
|
1014
|
+
func: async ({ id } = {}) => {
|
|
1015
|
+
const runIndex = startToolRun({ tool: 'outline_get_collection_documents', id });
|
|
1016
|
+
log('debug', '[Outline] outline_get_collection_documents called', { id });
|
|
1017
|
+
try {
|
|
1018
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
1019
|
+
const res = await apiPost('/collections.documents', { id });
|
|
1020
|
+
const result = { success: true, data: res.data };
|
|
1021
|
+
log('info', '[Outline] outline_get_collection_documents succeeded', { id });
|
|
1022
|
+
endToolRun(runIndex, result);
|
|
1023
|
+
return JSON.stringify(result);
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
const errObj = { error: err.message || String(err) };
|
|
1026
|
+
log('error', '[Outline] outline_get_collection_documents failed', errObj);
|
|
1027
|
+
endToolRun(runIndex, errObj);
|
|
1028
|
+
return JSON.stringify(errObj);
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
}));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ── Tool: outline_export_collection ───────────────────────────────────
|
|
1035
|
+
if (enabledTools.includes('exportCollection')) {
|
|
1036
|
+
tools.push(new DynamicStructuredTool({
|
|
1037
|
+
name: 'outline_export_collection',
|
|
1038
|
+
description: toolDescriptionOverride ||
|
|
1039
|
+
'Trigger a bulk export of an Outline Wiki collection in the specified format. ' +
|
|
1040
|
+
'Returns a FileOperation object with a status and download URL once the background job completes. ' +
|
|
1041
|
+
'You can poll the FileOperation status to check when the export is ready.',
|
|
1042
|
+
schema: z ? z.object({
|
|
1043
|
+
id: z.string().describe('Collection UUID to export'),
|
|
1044
|
+
format: strOpt('Export format: "outline-markdown" | "json" | "html" (default: outline-markdown)'),
|
|
1045
|
+
}) : null,
|
|
1046
|
+
func: async ({ id, format } = {}) => {
|
|
1047
|
+
const runIndex = startToolRun({ tool: 'outline_export_collection', id });
|
|
1048
|
+
log('debug', '[Outline] outline_export_collection called', { id, format });
|
|
1049
|
+
try {
|
|
1050
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
1051
|
+
const body = { id, format: format || 'outline-markdown' };
|
|
1052
|
+
const res = await apiPost('/collections.export', body);
|
|
1053
|
+
const result = { success: true, data: res.data };
|
|
1054
|
+
log('info', '[Outline] outline_export_collection succeeded', { id });
|
|
1055
|
+
endToolRun(runIndex, result);
|
|
1056
|
+
return JSON.stringify(result);
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
const errObj = { error: err.message || String(err) };
|
|
1059
|
+
log('error', '[Outline] outline_export_collection failed', errObj);
|
|
1060
|
+
endToolRun(runIndex, errObj);
|
|
1061
|
+
return JSON.stringify(errObj);
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
}));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1068
|
+
// COMMENTS
|
|
1069
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1070
|
+
|
|
1071
|
+
// ── Tool: outline_list_comments ────────────────────────────────────────
|
|
1072
|
+
if (enabledTools.includes('listComments')) {
|
|
1073
|
+
tools.push(new DynamicStructuredTool({
|
|
1074
|
+
name: 'outline_list_comments',
|
|
1075
|
+
description: toolDescriptionOverride ||
|
|
1076
|
+
'List comments on a document or within a collection in Outline Wiki.',
|
|
1077
|
+
schema: z ? z.object({
|
|
1078
|
+
document_id: strOpt('UUID of the document to list comments for'),
|
|
1079
|
+
collection_id: strOpt('UUID of the collection to list all comments within'),
|
|
1080
|
+
limit: numOpt('Maximum results to return (default: 25)'),
|
|
1081
|
+
offset: numOpt('Offset for pagination (default: 0)'),
|
|
1082
|
+
}) : null,
|
|
1083
|
+
func: async ({ document_id, collection_id, limit, offset } = {}) => {
|
|
1084
|
+
const runIndex = startToolRun({ tool: 'outline_list_comments', document_id });
|
|
1085
|
+
log('debug', '[Outline] outline_list_comments called', { document_id });
|
|
1086
|
+
try {
|
|
1087
|
+
const body = {};
|
|
1088
|
+
if (document_id) body.documentId = document_id;
|
|
1089
|
+
if (collection_id) body.collectionId = collection_id;
|
|
1090
|
+
if (limit) body.limit = limit;
|
|
1091
|
+
if (offset) body.offset = offset;
|
|
1092
|
+
const res = await apiPost('/comments.list', body);
|
|
1093
|
+
const result = { success: true, data: res.data, pagination: res.pagination };
|
|
1094
|
+
log('info', '[Outline] outline_list_comments succeeded', { count: res.data && res.data.length });
|
|
1095
|
+
endToolRun(runIndex, result);
|
|
1096
|
+
return JSON.stringify(result);
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
const errObj = { error: err.message || String(err) };
|
|
1099
|
+
log('error', '[Outline] outline_list_comments failed', errObj);
|
|
1100
|
+
endToolRun(runIndex, errObj);
|
|
1101
|
+
return JSON.stringify(errObj);
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
}));
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ── Tool: outline_create_comment ───────────────────────────────────────
|
|
1108
|
+
if (enabledTools.includes('createComment')) {
|
|
1109
|
+
tools.push(new DynamicStructuredTool({
|
|
1110
|
+
name: 'outline_create_comment',
|
|
1111
|
+
description: toolDescriptionOverride ||
|
|
1112
|
+
'Add a comment to a document in Outline Wiki. ' +
|
|
1113
|
+
'Comments can be top-level or replies to existing comments (use parent_comment_id for replies).',
|
|
1114
|
+
schema: z ? z.object({
|
|
1115
|
+
document_id: z.string().describe('UUID of the document to comment on'),
|
|
1116
|
+
text: z.string().describe('Comment body in Markdown format'),
|
|
1117
|
+
parent_comment_id: strOpt('UUID of the parent comment to reply to (for threaded replies)'),
|
|
1118
|
+
}) : null,
|
|
1119
|
+
func: async ({ document_id, text, parent_comment_id } = {}) => {
|
|
1120
|
+
const runIndex = startToolRun({ tool: 'outline_create_comment', document_id });
|
|
1121
|
+
log('debug', '[Outline] outline_create_comment called', { document_id });
|
|
1122
|
+
try {
|
|
1123
|
+
if (!document_id) return JSON.stringify({ error: 'document_id is required' });
|
|
1124
|
+
if (!text) return JSON.stringify({ error: 'text is required' });
|
|
1125
|
+
const body = { documentId: document_id, text };
|
|
1126
|
+
if (parent_comment_id) body.parentCommentId = parent_comment_id;
|
|
1127
|
+
const res = await apiPost('/comments.create', body);
|
|
1128
|
+
const result = { success: true, data: res.data };
|
|
1129
|
+
log('info', '[Outline] outline_create_comment succeeded', { id: res.data && res.data.id });
|
|
1130
|
+
endToolRun(runIndex, result);
|
|
1131
|
+
return JSON.stringify(result);
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
const errObj = { error: err.message || String(err) };
|
|
1134
|
+
log('error', '[Outline] outline_create_comment failed', errObj);
|
|
1135
|
+
endToolRun(runIndex, errObj);
|
|
1136
|
+
return JSON.stringify(errObj);
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
}));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// ── Tool: outline_update_comment ───────────────────────────────────────
|
|
1143
|
+
if (enabledTools.includes('updateComment')) {
|
|
1144
|
+
tools.push(new DynamicStructuredTool({
|
|
1145
|
+
name: 'outline_update_comment',
|
|
1146
|
+
description: toolDescriptionOverride ||
|
|
1147
|
+
'Update the text of an existing comment in Outline Wiki.',
|
|
1148
|
+
schema: z ? z.object({
|
|
1149
|
+
id: z.string().describe('Comment UUID'),
|
|
1150
|
+
text: z.string().describe('New comment body in Markdown format'),
|
|
1151
|
+
}) : null,
|
|
1152
|
+
func: async ({ id, text } = {}) => {
|
|
1153
|
+
const runIndex = startToolRun({ tool: 'outline_update_comment', id });
|
|
1154
|
+
log('debug', '[Outline] outline_update_comment called', { id });
|
|
1155
|
+
try {
|
|
1156
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
1157
|
+
if (!text) return JSON.stringify({ error: 'text is required' });
|
|
1158
|
+
// comments.update expects data object per API spec
|
|
1159
|
+
const res = await apiPost('/comments.update', { id, data: { text } });
|
|
1160
|
+
const result = { success: true, data: res.data };
|
|
1161
|
+
log('info', '[Outline] outline_update_comment succeeded', { id });
|
|
1162
|
+
endToolRun(runIndex, result);
|
|
1163
|
+
return JSON.stringify(result);
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
const errObj = { error: err.message || String(err) };
|
|
1166
|
+
log('error', '[Outline] outline_update_comment failed', errObj);
|
|
1167
|
+
endToolRun(runIndex, errObj);
|
|
1168
|
+
return JSON.stringify(errObj);
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
}));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// ── Tool: outline_delete_comment ───────────────────────────────────────
|
|
1175
|
+
if (enabledTools.includes('deleteComment')) {
|
|
1176
|
+
tools.push(new DynamicStructuredTool({
|
|
1177
|
+
name: 'outline_delete_comment',
|
|
1178
|
+
description: toolDescriptionOverride ||
|
|
1179
|
+
'Delete a comment in Outline Wiki. If the comment has replies, all child comments are also deleted.',
|
|
1180
|
+
schema: z ? z.object({
|
|
1181
|
+
id: z.string().describe('Comment UUID'),
|
|
1182
|
+
}) : null,
|
|
1183
|
+
func: async ({ id } = {}) => {
|
|
1184
|
+
const runIndex = startToolRun({ tool: 'outline_delete_comment', id });
|
|
1185
|
+
log('debug', '[Outline] outline_delete_comment called', { id });
|
|
1186
|
+
try {
|
|
1187
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
1188
|
+
const res = await apiPost('/comments.delete', { id });
|
|
1189
|
+
const result = { success: true, ok: res.ok !== false };
|
|
1190
|
+
log('info', '[Outline] outline_delete_comment succeeded', { id });
|
|
1191
|
+
endToolRun(runIndex, result);
|
|
1192
|
+
return JSON.stringify(result);
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
const errObj = { error: err.message || String(err) };
|
|
1195
|
+
log('error', '[Outline] outline_delete_comment failed', errObj);
|
|
1196
|
+
endToolRun(runIndex, errObj);
|
|
1197
|
+
return JSON.stringify(errObj);
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
}));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1204
|
+
// ATTACHMENTS
|
|
1205
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1206
|
+
|
|
1207
|
+
// ── Tool: outline_upload_attachment ────────────────────────────────────
|
|
1208
|
+
if (enabledTools.includes('uploadAttachment')) {
|
|
1209
|
+
tools.push(new DynamicStructuredTool({
|
|
1210
|
+
name: 'outline_upload_attachment',
|
|
1211
|
+
description: toolDescriptionOverride ||
|
|
1212
|
+
'Upload a file as an attachment in Outline Wiki. ' +
|
|
1213
|
+
'The file must be provided via binary_property_name — the binary property name returned by a previous tool ' +
|
|
1214
|
+
'(e.g., telegram_get_file, gotenberg_url_screenshot, gotenberg_url_to_pdf, or any other tool that stores binary files). ' +
|
|
1215
|
+
'Returns the attachment URL that can be embedded in document Markdown using  or [text](url) syntax. ' +
|
|
1216
|
+
'Supports all common file types: images (PNG, JPEG, GIF, WebP), PDFs, documents (DOCX, XLSX), archives (ZIP), and more.',
|
|
1217
|
+
schema: z ? z.object({
|
|
1218
|
+
binary_property_name: z.string().describe('Name of the binary property containing the file to upload (binaryPropertyName returned by a previous tool)'),
|
|
1219
|
+
document_id: strOpt('UUID of the document to associate this attachment with (optional)'),
|
|
1220
|
+
filename: strOpt('Override filename (including extension, e.g., "photo.png"). If omitted, uses filename from binary metadata.'),
|
|
1221
|
+
}) : null,
|
|
1222
|
+
func: async ({ binary_property_name, document_id, filename } = {}) => {
|
|
1223
|
+
const runIndex = startToolRun({ tool: 'outline_upload_attachment', binary_property_name });
|
|
1224
|
+
log('debug', '[Outline] outline_upload_attachment called', { binary_property_name });
|
|
1225
|
+
try {
|
|
1226
|
+
if (!binary_property_name) return JSON.stringify({ error: 'binary_property_name is required' });
|
|
1227
|
+
|
|
1228
|
+
const buffer = await getBuffer(binary_property_name);
|
|
1229
|
+
const meta = getMeta(binary_property_name);
|
|
1230
|
+
const resolvedFilename = filename || (meta && meta.fileName) || 'file';
|
|
1231
|
+
const contentType = (meta && meta.mimeType) || guessMimeType(resolvedFilename);
|
|
1232
|
+
const sizeBytes = buffer.length;
|
|
1233
|
+
|
|
1234
|
+
// Step 1: Create attachment record and get signed upload credentials
|
|
1235
|
+
const createBody = {
|
|
1236
|
+
name: resolvedFilename,
|
|
1237
|
+
contentType,
|
|
1238
|
+
size: sizeBytes,
|
|
1239
|
+
};
|
|
1240
|
+
if (document_id) createBody.documentId = document_id;
|
|
1241
|
+
|
|
1242
|
+
const createRes = await apiPost('/attachments.create', createBody);
|
|
1243
|
+
if (!createRes || !createRes.data) {
|
|
1244
|
+
return JSON.stringify({ error: 'Failed to create attachment record', details: createRes });
|
|
1245
|
+
}
|
|
1246
|
+
const { uploadUrl, form, attachment } = createRes.data;
|
|
1247
|
+
|
|
1248
|
+
// Step 2: Upload the file to the signed URL (S3/GCS signed POST)
|
|
1249
|
+
// Build multipart form data with the signed form fields + the file
|
|
1250
|
+
const uploadFormData = {};
|
|
1251
|
+
if (form && typeof form === 'object') {
|
|
1252
|
+
Object.assign(uploadFormData, form);
|
|
1253
|
+
}
|
|
1254
|
+
uploadFormData.file = {
|
|
1255
|
+
value: buffer,
|
|
1256
|
+
options: { filename: resolvedFilename, contentType },
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
await self.helpers.request({
|
|
1260
|
+
method: 'POST',
|
|
1261
|
+
url: uploadUrl,
|
|
1262
|
+
formData: uploadFormData,
|
|
1263
|
+
json: false,
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
const result = {
|
|
1267
|
+
success: true,
|
|
1268
|
+
attachmentId: attachment && attachment.id,
|
|
1269
|
+
attachmentUrl: attachment && attachment.url,
|
|
1270
|
+
filename: resolvedFilename,
|
|
1271
|
+
contentType,
|
|
1272
|
+
sizeBytes,
|
|
1273
|
+
message: `File "${resolvedFilename}" uploaded as Outline attachment. Embed in documents using: `,
|
|
1274
|
+
};
|
|
1275
|
+
log('info', '[Outline] outline_upload_attachment succeeded', { filename: resolvedFilename, attachmentId: result.attachmentId });
|
|
1276
|
+
endToolRun(runIndex, result);
|
|
1277
|
+
return JSON.stringify(result);
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
const errObj = { error: err.message || String(err) };
|
|
1280
|
+
log('error', '[Outline] outline_upload_attachment failed', errObj);
|
|
1281
|
+
endToolRun(runIndex, errObj);
|
|
1282
|
+
return JSON.stringify(errObj);
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
}));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// ── Tool: outline_delete_attachment ────────────────────────────────────
|
|
1289
|
+
if (enabledTools.includes('deleteAttachment')) {
|
|
1290
|
+
tools.push(new DynamicStructuredTool({
|
|
1291
|
+
name: 'outline_delete_attachment',
|
|
1292
|
+
description: toolDescriptionOverride ||
|
|
1293
|
+
'Permanently delete an attachment from Outline Wiki. ' +
|
|
1294
|
+
'Note: this does not remove references or links to the attachment in documents.',
|
|
1295
|
+
schema: z ? z.object({
|
|
1296
|
+
id: z.string().describe('Attachment UUID to delete'),
|
|
1297
|
+
}) : null,
|
|
1298
|
+
func: async ({ id } = {}) => {
|
|
1299
|
+
const runIndex = startToolRun({ tool: 'outline_delete_attachment', id });
|
|
1300
|
+
log('debug', '[Outline] outline_delete_attachment called', { id });
|
|
1301
|
+
try {
|
|
1302
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
1303
|
+
const res = await apiPost('/attachments.delete', { id });
|
|
1304
|
+
const result = { success: true, ok: res.ok !== false };
|
|
1305
|
+
log('info', '[Outline] outline_delete_attachment succeeded', { id });
|
|
1306
|
+
endToolRun(runIndex, result);
|
|
1307
|
+
return JSON.stringify(result);
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
const errObj = { error: err.message || String(err) };
|
|
1310
|
+
log('error', '[Outline] outline_delete_attachment failed', errObj);
|
|
1311
|
+
endToolRun(runIndex, errObj);
|
|
1312
|
+
return JSON.stringify(errObj);
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
}));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1319
|
+
// USERS
|
|
1320
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1321
|
+
|
|
1322
|
+
// ── Tool: outline_list_users ───────────────────────────────────────────
|
|
1323
|
+
if (enabledTools.includes('listUsers')) {
|
|
1324
|
+
tools.push(new DynamicStructuredTool({
|
|
1325
|
+
name: 'outline_list_users',
|
|
1326
|
+
description: toolDescriptionOverride ||
|
|
1327
|
+
'List users in the Outline Wiki workspace. Returns user names, emails, roles, and activity information.',
|
|
1328
|
+
schema: z ? z.object({
|
|
1329
|
+
query: strOpt('Filter users by name or email'),
|
|
1330
|
+
limit: numOpt('Maximum results to return (default: 25)'),
|
|
1331
|
+
offset: numOpt('Offset for pagination (default: 0)'),
|
|
1332
|
+
}) : null,
|
|
1333
|
+
func: async ({ query, limit, offset } = {}) => {
|
|
1334
|
+
const runIndex = startToolRun({ tool: 'outline_list_users' });
|
|
1335
|
+
log('debug', '[Outline] outline_list_users called');
|
|
1336
|
+
try {
|
|
1337
|
+
const body = {};
|
|
1338
|
+
if (query) body.query = query;
|
|
1339
|
+
if (limit) body.limit = limit;
|
|
1340
|
+
if (offset) body.offset = offset;
|
|
1341
|
+
const res = await apiPost('/users.list', body);
|
|
1342
|
+
const result = { success: true, data: res.data, pagination: res.pagination };
|
|
1343
|
+
log('info', '[Outline] outline_list_users succeeded', { count: res.data && res.data.length });
|
|
1344
|
+
endToolRun(runIndex, result);
|
|
1345
|
+
return JSON.stringify(result);
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
const errObj = { error: err.message || String(err) };
|
|
1348
|
+
log('error', '[Outline] outline_list_users failed', errObj);
|
|
1349
|
+
endToolRun(runIndex, errObj);
|
|
1350
|
+
return JSON.stringify(errObj);
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
}));
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// ── Tool: outline_get_user ─────────────────────────────────────────────
|
|
1357
|
+
if (enabledTools.includes('getUser')) {
|
|
1358
|
+
tools.push(new DynamicStructuredTool({
|
|
1359
|
+
name: 'outline_get_user',
|
|
1360
|
+
description: toolDescriptionOverride ||
|
|
1361
|
+
'Get details of a specific user in Outline Wiki by their UUID.',
|
|
1362
|
+
schema: z ? z.object({
|
|
1363
|
+
id: z.string().describe('User UUID'),
|
|
1364
|
+
}) : null,
|
|
1365
|
+
func: async ({ id } = {}) => {
|
|
1366
|
+
const runIndex = startToolRun({ tool: 'outline_get_user', id });
|
|
1367
|
+
log('debug', '[Outline] outline_get_user called', { id });
|
|
1368
|
+
try {
|
|
1369
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
1370
|
+
const res = await apiPost('/users.info', { id });
|
|
1371
|
+
const result = { success: true, data: res.data };
|
|
1372
|
+
log('info', '[Outline] outline_get_user succeeded', { id });
|
|
1373
|
+
endToolRun(runIndex, result);
|
|
1374
|
+
return JSON.stringify(result);
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
const errObj = { error: err.message || String(err) };
|
|
1377
|
+
log('error', '[Outline] outline_get_user failed', errObj);
|
|
1378
|
+
endToolRun(runIndex, errObj);
|
|
1379
|
+
return JSON.stringify(errObj);
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
}));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1386
|
+
// SHARES
|
|
1387
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1388
|
+
|
|
1389
|
+
// ── Tool: outline_create_share ─────────────────────────────────────────
|
|
1390
|
+
if (enabledTools.includes('createShare')) {
|
|
1391
|
+
tools.push(new DynamicStructuredTool({
|
|
1392
|
+
name: 'outline_create_share',
|
|
1393
|
+
description: toolDescriptionOverride ||
|
|
1394
|
+
'Create a public share link for a document in Outline Wiki. ' +
|
|
1395
|
+
'Returns the share URL that can be sent to anyone to view the document without requiring authentication. ' +
|
|
1396
|
+
'The sharing setting on the collection must be enabled.',
|
|
1397
|
+
schema: z ? z.object({
|
|
1398
|
+
document_id: z.string().describe('UUID of the document to share'),
|
|
1399
|
+
include_child_documents: boolOpt('Whether to include child documents in the share (default: false)'),
|
|
1400
|
+
}) : null,
|
|
1401
|
+
func: async ({ document_id, include_child_documents } = {}) => {
|
|
1402
|
+
const runIndex = startToolRun({ tool: 'outline_create_share', document_id });
|
|
1403
|
+
log('debug', '[Outline] outline_create_share called', { document_id });
|
|
1404
|
+
try {
|
|
1405
|
+
if (!document_id) return JSON.stringify({ error: 'document_id is required' });
|
|
1406
|
+
const body = { documentId: document_id };
|
|
1407
|
+
if (include_child_documents !== undefined) body.includeChildDocuments = include_child_documents;
|
|
1408
|
+
const res = await apiPost('/shares.create', body);
|
|
1409
|
+
const result = { success: true, data: res.data };
|
|
1410
|
+
log('info', '[Outline] outline_create_share succeeded', { shareId: res.data && res.data.id });
|
|
1411
|
+
endToolRun(runIndex, result);
|
|
1412
|
+
return JSON.stringify(result);
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
const errObj = { error: err.message || String(err) };
|
|
1415
|
+
log('error', '[Outline] outline_create_share failed', errObj);
|
|
1416
|
+
endToolRun(runIndex, errObj);
|
|
1417
|
+
return JSON.stringify(errObj);
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
}));
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// ── Tool: outline_list_shares ──────────────────────────────────────────
|
|
1424
|
+
if (enabledTools.includes('listShares')) {
|
|
1425
|
+
tools.push(new DynamicStructuredTool({
|
|
1426
|
+
name: 'outline_list_shares',
|
|
1427
|
+
description: toolDescriptionOverride ||
|
|
1428
|
+
'List existing share links in Outline Wiki.',
|
|
1429
|
+
schema: z ? z.object({
|
|
1430
|
+
limit: numOpt('Maximum results to return (default: 25)'),
|
|
1431
|
+
offset: numOpt('Offset for pagination (default: 0)'),
|
|
1432
|
+
}) : null,
|
|
1433
|
+
func: async ({ limit, offset } = {}) => {
|
|
1434
|
+
const runIndex = startToolRun({ tool: 'outline_list_shares' });
|
|
1435
|
+
log('debug', '[Outline] outline_list_shares called');
|
|
1436
|
+
try {
|
|
1437
|
+
const body = {};
|
|
1438
|
+
if (limit) body.limit = limit;
|
|
1439
|
+
if (offset) body.offset = offset;
|
|
1440
|
+
const res = await apiPost('/shares.list', body);
|
|
1441
|
+
const result = { success: true, data: res.data, pagination: res.pagination };
|
|
1442
|
+
log('info', '[Outline] outline_list_shares succeeded', { count: res.data && res.data.length });
|
|
1443
|
+
endToolRun(runIndex, result);
|
|
1444
|
+
return JSON.stringify(result);
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
const errObj = { error: err.message || String(err) };
|
|
1447
|
+
log('error', '[Outline] outline_list_shares failed', errObj);
|
|
1448
|
+
endToolRun(runIndex, errObj);
|
|
1449
|
+
return JSON.stringify(errObj);
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
}));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// ── Tool: outline_revoke_share ─────────────────────────────────────────
|
|
1456
|
+
if (enabledTools.includes('revokeShare')) {
|
|
1457
|
+
tools.push(new DynamicStructuredTool({
|
|
1458
|
+
name: 'outline_revoke_share',
|
|
1459
|
+
description: toolDescriptionOverride ||
|
|
1460
|
+
'Revoke a share link in Outline Wiki, making the document no longer publicly accessible via that link.',
|
|
1461
|
+
schema: z ? z.object({
|
|
1462
|
+
id: z.string().describe('Share UUID to revoke'),
|
|
1463
|
+
}) : null,
|
|
1464
|
+
func: async ({ id } = {}) => {
|
|
1465
|
+
const runIndex = startToolRun({ tool: 'outline_revoke_share', id });
|
|
1466
|
+
log('debug', '[Outline] outline_revoke_share called', { id });
|
|
1467
|
+
try {
|
|
1468
|
+
if (!id) return JSON.stringify({ error: 'id is required' });
|
|
1469
|
+
const res = await apiPost('/shares.revoke', { id });
|
|
1470
|
+
const result = { success: true, ok: res.ok !== false };
|
|
1471
|
+
log('info', '[Outline] outline_revoke_share succeeded', { id });
|
|
1472
|
+
endToolRun(runIndex, result);
|
|
1473
|
+
return JSON.stringify(result);
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
const errObj = { error: err.message || String(err) };
|
|
1476
|
+
log('error', '[Outline] outline_revoke_share failed', errObj);
|
|
1477
|
+
endToolRun(runIndex, errObj);
|
|
1478
|
+
return JSON.stringify(errObj);
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
}));
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return { response: tools };
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* execute is called when the node is triggered as a regular node (not via AI Agent).
|
|
1489
|
+
* Returns a helpful message explaining how to use this node.
|
|
1490
|
+
*/
|
|
1491
|
+
async execute() {
|
|
1492
|
+
const items = this.getInputData();
|
|
1493
|
+
const returnData = [];
|
|
1494
|
+
for (let i = 0; i < items.length; i++) {
|
|
1495
|
+
returnData.push({
|
|
1496
|
+
json: {
|
|
1497
|
+
message: 'Outline Wiki AI Tools node is designed to be connected to an AI Agent node. Connect the "Tool" output to an AI Agent node\'s "Tools" input.',
|
|
1498
|
+
enabledTools: this.getNodeParameter('enabledTools', i, []),
|
|
1499
|
+
documentation: 'https://www.getoutline.com/developers',
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
return [returnData];
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
exports.OutlineAiTools = OutlineAiTools;
|