@khanglvm/outline-cli 0.1.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/.env.test.example +2 -0
- package/AGENTS.md +107 -0
- package/CHANGELOG.md +102 -0
- package/README.md +244 -0
- package/bin/outline-agent.js +5 -0
- package/bin/outline-cli.js +13 -0
- package/package.json +25 -0
- package/scripts/generate-entry-integrity.mjs +123 -0
- package/scripts/release.mjs +353 -0
- package/src/action-gate.js +257 -0
- package/src/agent-skills.js +759 -0
- package/src/cli.js +956 -0
- package/src/config-store.js +720 -0
- package/src/entry-integrity-binding.generated.js +6 -0
- package/src/entry-integrity-manifest.generated.js +74 -0
- package/src/entry-integrity.js +112 -0
- package/src/errors.js +15 -0
- package/src/outline-client.js +237 -0
- package/src/result-store.js +183 -0
- package/src/secure-keyring.js +290 -0
- package/src/tool-arg-schemas.js +2346 -0
- package/src/tools.extended.js +3252 -0
- package/src/tools.js +1056 -0
- package/src/tools.mutation.js +1807 -0
- package/src/tools.navigation.js +2273 -0
- package/src/tools.platform.js +554 -0
- package/src/utils.js +176 -0
- package/test/action-gate.unit.test.js +157 -0
- package/test/agent-skills.unit.test.js +52 -0
- package/test/config-store.unit.test.js +89 -0
- package/test/hardening.unit.test.js +3778 -0
- package/test/live.integration.test.js +5140 -0
- package/test/profile-selection.unit.test.js +279 -0
- package/test/security.unit.test.js +113 -0
package/src/tools.js
ADDED
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
import { ApiError, CliError } from "./errors.js";
|
|
2
|
+
import {
|
|
3
|
+
assertPerformAction,
|
|
4
|
+
consumeDocumentDeleteReadReceipt,
|
|
5
|
+
getDocumentDeleteReadReceipt,
|
|
6
|
+
isLikelyDeleteMethod,
|
|
7
|
+
isLikelyMutatingMethod,
|
|
8
|
+
issueDocumentDeleteReadReceipt,
|
|
9
|
+
} from "./action-gate.js";
|
|
10
|
+
import { NAVIGATION_TOOLS } from "./tools.navigation.js";
|
|
11
|
+
import { MUTATION_TOOLS } from "./tools.mutation.js";
|
|
12
|
+
import { PLATFORM_TOOLS } from "./tools.platform.js";
|
|
13
|
+
import { EXTENDED_TOOLS } from "./tools.extended.js";
|
|
14
|
+
import { validateToolArgs } from "./tool-arg-schemas.js";
|
|
15
|
+
import {
|
|
16
|
+
compactValue,
|
|
17
|
+
ensureStringArray,
|
|
18
|
+
mapLimit,
|
|
19
|
+
parseCsv,
|
|
20
|
+
projectObject,
|
|
21
|
+
toInteger,
|
|
22
|
+
} from "./utils.js";
|
|
23
|
+
|
|
24
|
+
function normalizeStatusFilter(statusFilter) {
|
|
25
|
+
if (statusFilter === undefined || statusFilter === null) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(statusFilter)) {
|
|
29
|
+
return statusFilter;
|
|
30
|
+
}
|
|
31
|
+
if (typeof statusFilter === "string") {
|
|
32
|
+
return parseCsv(statusFilter);
|
|
33
|
+
}
|
|
34
|
+
throw new CliError("statusFilter must be string or string[]");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeIds(args) {
|
|
38
|
+
const ids = [];
|
|
39
|
+
if (args.id) {
|
|
40
|
+
ids.push(String(args.id));
|
|
41
|
+
}
|
|
42
|
+
if (args.ids) {
|
|
43
|
+
ids.push(...ensureStringArray(args.ids, "ids"));
|
|
44
|
+
}
|
|
45
|
+
return [...new Set(ids)];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeSearchRow(row, view = "summary", contextChars = 320) {
|
|
49
|
+
if (view === "full") {
|
|
50
|
+
return row;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const doc = row.document || row;
|
|
54
|
+
const context = row.context || "";
|
|
55
|
+
const summary = {
|
|
56
|
+
id: doc.id,
|
|
57
|
+
title: doc.title,
|
|
58
|
+
collectionId: doc.collectionId,
|
|
59
|
+
parentDocumentId: doc.parentDocumentId,
|
|
60
|
+
updatedAt: doc.updatedAt,
|
|
61
|
+
publishedAt: doc.publishedAt,
|
|
62
|
+
urlId: doc.urlId,
|
|
63
|
+
ranking: row.ranking,
|
|
64
|
+
context: context.length > contextChars ? `${context.slice(0, contextChars)}...` : context,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (view === "ids") {
|
|
68
|
+
return {
|
|
69
|
+
id: summary.id,
|
|
70
|
+
title: summary.title,
|
|
71
|
+
ranking: summary.ranking,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return summary;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeDocumentRow(row, view = "summary", excerptChars = 280) {
|
|
79
|
+
if (view === "full") {
|
|
80
|
+
return row;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const summary = {
|
|
84
|
+
id: row.id,
|
|
85
|
+
title: row.title,
|
|
86
|
+
collectionId: row.collectionId,
|
|
87
|
+
parentDocumentId: row.parentDocumentId,
|
|
88
|
+
revision: row.revision,
|
|
89
|
+
updatedAt: row.updatedAt,
|
|
90
|
+
publishedAt: row.publishedAt,
|
|
91
|
+
urlId: row.urlId,
|
|
92
|
+
emoji: row.emoji,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (view === "ids") {
|
|
96
|
+
return {
|
|
97
|
+
id: summary.id,
|
|
98
|
+
title: summary.title,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (row.text) {
|
|
103
|
+
summary.excerpt = row.text.length > excerptChars ? `${row.text.slice(0, excerptChars)}...` : row.text;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return summary;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeCollectionRow(row, view = "summary") {
|
|
110
|
+
if (view === "full") {
|
|
111
|
+
return row;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
id: row.id,
|
|
115
|
+
name: row.name,
|
|
116
|
+
description: row.description,
|
|
117
|
+
permission: row.permission,
|
|
118
|
+
sharing: row.sharing,
|
|
119
|
+
updatedAt: row.updatedAt,
|
|
120
|
+
color: row.color,
|
|
121
|
+
icon: row.icon,
|
|
122
|
+
urlId: row.urlId,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function applyViewToList(data, mapper) {
|
|
127
|
+
if (!Array.isArray(data)) {
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
130
|
+
return data.map((item) => mapper(item));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function maybeDropPolicies(payload, includePolicies) {
|
|
134
|
+
if (includePolicies) {
|
|
135
|
+
return payload;
|
|
136
|
+
}
|
|
137
|
+
if (payload && typeof payload === "object" && "policies" in payload) {
|
|
138
|
+
const clone = { ...payload };
|
|
139
|
+
delete clone.policies;
|
|
140
|
+
return clone;
|
|
141
|
+
}
|
|
142
|
+
return payload;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function applySelectToData(payload, select) {
|
|
146
|
+
if (!select || select.length === 0) {
|
|
147
|
+
return payload;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.data)) {
|
|
151
|
+
return {
|
|
152
|
+
...payload,
|
|
153
|
+
data: payload.data.map((item) => projectObject(item, select)),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (payload && typeof payload === "object" && payload.data && typeof payload.data === "object") {
|
|
158
|
+
return {
|
|
159
|
+
...payload,
|
|
160
|
+
data: projectObject(payload.data, select),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return projectObject(payload, select);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function apiCallTool(ctx, args) {
|
|
168
|
+
const method = args.method || args.endpoint;
|
|
169
|
+
if (!method) {
|
|
170
|
+
throw new CliError("api.call requires args.method");
|
|
171
|
+
}
|
|
172
|
+
const body = args.body && typeof args.body === "object" ? args.body : {};
|
|
173
|
+
const maxAttempts = toInteger(args.maxAttempts, 1);
|
|
174
|
+
|
|
175
|
+
if (isLikelyMutatingMethod(method)) {
|
|
176
|
+
assertPerformAction(args, {
|
|
177
|
+
tool: "api.call",
|
|
178
|
+
action: `invoke mutating method '${method}'`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let deleteGate = null;
|
|
183
|
+
if (isLikelyDeleteMethod(method)) {
|
|
184
|
+
const targetId = body?.id;
|
|
185
|
+
if (!targetId) {
|
|
186
|
+
throw new CliError("Delete via api.call requires body.id", {
|
|
187
|
+
code: "DELETE_TARGET_REQUIRED",
|
|
188
|
+
method,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const receipt = await getDocumentDeleteReadReceipt({
|
|
193
|
+
token: args.readToken,
|
|
194
|
+
profileId: ctx.profile.id,
|
|
195
|
+
documentId: String(targetId),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const latest = await ctx.client.call("documents.info", { id: String(targetId) }, {
|
|
199
|
+
maxAttempts: Math.max(1, maxAttempts),
|
|
200
|
+
});
|
|
201
|
+
const actualRevision = Number(latest.body?.data?.revision);
|
|
202
|
+
const expectedRevision = Number(receipt.revision);
|
|
203
|
+
if (
|
|
204
|
+
Number.isFinite(expectedRevision) &&
|
|
205
|
+
Number.isFinite(actualRevision) &&
|
|
206
|
+
actualRevision !== expectedRevision
|
|
207
|
+
) {
|
|
208
|
+
throw new CliError("Delete read confirmation is stale; re-read the document with armDelete=true", {
|
|
209
|
+
code: "DELETE_READ_TOKEN_STALE",
|
|
210
|
+
method,
|
|
211
|
+
id: String(targetId),
|
|
212
|
+
expectedRevision,
|
|
213
|
+
actualRevision,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
deleteGate = {
|
|
218
|
+
token: args.readToken,
|
|
219
|
+
id: String(targetId),
|
|
220
|
+
expectedRevision,
|
|
221
|
+
actualRevision,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const res = await ctx.client.call(method, body, { maxAttempts });
|
|
226
|
+
if (deleteGate && res.body?.success !== false) {
|
|
227
|
+
await consumeDocumentDeleteReadReceipt(deleteGate.token);
|
|
228
|
+
}
|
|
229
|
+
let payload = res.body;
|
|
230
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
231
|
+
|
|
232
|
+
if (args.select) {
|
|
233
|
+
payload = applySelectToData(payload, ensureStringArray(args.select, "select"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
tool: "api.call",
|
|
238
|
+
profile: ctx.profile.id,
|
|
239
|
+
method,
|
|
240
|
+
result: payload,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function authInfoTool(ctx, args) {
|
|
245
|
+
const res = await ctx.client.call("auth.info", {});
|
|
246
|
+
let payload = res.body;
|
|
247
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
248
|
+
|
|
249
|
+
if (args.view === "summary") {
|
|
250
|
+
payload = {
|
|
251
|
+
data: {
|
|
252
|
+
user: payload?.data?.user,
|
|
253
|
+
team: payload?.data?.team,
|
|
254
|
+
},
|
|
255
|
+
policies: payload?.policies,
|
|
256
|
+
};
|
|
257
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
tool: "auth.info",
|
|
262
|
+
profile: ctx.profile.id,
|
|
263
|
+
result: payload,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function documentsSearchTool(ctx, args) {
|
|
268
|
+
const mode = args.mode === "titles" ? "titles" : "semantic";
|
|
269
|
+
const endpoint = mode === "titles" ? "documents.search_titles" : "documents.search";
|
|
270
|
+
const queries = ensureStringArray(args.queries, "queries") || (args.query ? [String(args.query)] : []);
|
|
271
|
+
|
|
272
|
+
if (queries.length === 0) {
|
|
273
|
+
throw new CliError("documents.search requires args.query or args.queries[]");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const baseBody = compactValue({
|
|
277
|
+
collectionId: args.collectionId,
|
|
278
|
+
documentId: args.documentId,
|
|
279
|
+
userId: args.userId,
|
|
280
|
+
statusFilter: normalizeStatusFilter(args.statusFilter),
|
|
281
|
+
dateFilter: args.dateFilter,
|
|
282
|
+
shareId: args.shareId,
|
|
283
|
+
limit: toInteger(args.limit, 25),
|
|
284
|
+
offset: toInteger(args.offset, 0),
|
|
285
|
+
snippetMinWords: mode === "semantic" ? toInteger(args.snippetMinWords, 20) : undefined,
|
|
286
|
+
snippetMaxWords: mode === "semantic" ? toInteger(args.snippetMaxWords, 30) : undefined,
|
|
287
|
+
sort: args.sort,
|
|
288
|
+
direction: args.direction,
|
|
289
|
+
}) || {};
|
|
290
|
+
|
|
291
|
+
const concurrency = toInteger(args.concurrency, 4);
|
|
292
|
+
const view = args.view || "summary";
|
|
293
|
+
const contextChars = toInteger(args.contextChars, 320);
|
|
294
|
+
|
|
295
|
+
const perQuery = await mapLimit(queries, concurrency, async (query) => {
|
|
296
|
+
const body = {
|
|
297
|
+
...baseBody,
|
|
298
|
+
query,
|
|
299
|
+
};
|
|
300
|
+
const res = await ctx.client.call(endpoint, body, {
|
|
301
|
+
maxAttempts: toInteger(args.maxAttempts, 2),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
let payload = res.body;
|
|
305
|
+
if (view !== "full") {
|
|
306
|
+
payload = {
|
|
307
|
+
...payload,
|
|
308
|
+
data: applyViewToList(payload.data, (item) => normalizeSearchRow(item, view, contextChars)),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
313
|
+
if (args.select) {
|
|
314
|
+
payload = applySelectToData(payload, ensureStringArray(args.select, "select"));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
query,
|
|
319
|
+
...payload,
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const mergeResults = args.merge !== false;
|
|
324
|
+
let merged = undefined;
|
|
325
|
+
|
|
326
|
+
if (mergeResults) {
|
|
327
|
+
const byId = new Map();
|
|
328
|
+
for (const q of perQuery) {
|
|
329
|
+
for (const item of q.data || []) {
|
|
330
|
+
const id = item.id || item?.document?.id;
|
|
331
|
+
if (!id) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const ranking = Number(item.ranking ?? 0);
|
|
335
|
+
const previous = byId.get(id);
|
|
336
|
+
if (!previous || ranking > previous.ranking) {
|
|
337
|
+
byId.set(id, {
|
|
338
|
+
...item,
|
|
339
|
+
queries: [q.query],
|
|
340
|
+
ranking,
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
previous.queries = [...new Set([...(previous.queries || []), q.query])];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
merged = Array.from(byId.values()).sort((a, b) => (b.ranking || 0) - (a.ranking || 0));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (queries.length === 1 && !args.forceGroupedResult) {
|
|
351
|
+
return {
|
|
352
|
+
tool: "documents.search",
|
|
353
|
+
mode,
|
|
354
|
+
profile: ctx.profile.id,
|
|
355
|
+
query: queries[0],
|
|
356
|
+
result: {
|
|
357
|
+
...perQuery[0],
|
|
358
|
+
merged,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
tool: "documents.search",
|
|
365
|
+
mode,
|
|
366
|
+
profile: ctx.profile.id,
|
|
367
|
+
queryCount: queries.length,
|
|
368
|
+
result: {
|
|
369
|
+
perQuery,
|
|
370
|
+
merged,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function documentsListTool(ctx, args) {
|
|
376
|
+
const body = compactValue({
|
|
377
|
+
limit: toInteger(args.limit, 25),
|
|
378
|
+
offset: toInteger(args.offset, 0),
|
|
379
|
+
sort: args.sort,
|
|
380
|
+
direction: args.direction,
|
|
381
|
+
collectionId: args.collectionId,
|
|
382
|
+
userId: args.userId,
|
|
383
|
+
backlinkDocumentId: args.backlinkDocumentId,
|
|
384
|
+
parentDocumentId: Object.prototype.hasOwnProperty.call(args, "parentDocumentId")
|
|
385
|
+
? args.parentDocumentId
|
|
386
|
+
: undefined,
|
|
387
|
+
statusFilter: normalizeStatusFilter(args.statusFilter),
|
|
388
|
+
}) || {};
|
|
389
|
+
|
|
390
|
+
const res = await ctx.client.call("documents.list", body, {
|
|
391
|
+
maxAttempts: toInteger(args.maxAttempts, 2),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const view = args.view || "summary";
|
|
395
|
+
const excerptChars = toInteger(args.excerptChars, 220);
|
|
396
|
+
let payload = res.body;
|
|
397
|
+
if (view !== "full") {
|
|
398
|
+
payload = {
|
|
399
|
+
...payload,
|
|
400
|
+
data: applyViewToList(payload.data, (item) => normalizeDocumentRow(item, view, excerptChars)),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
405
|
+
if (args.select) {
|
|
406
|
+
payload = applySelectToData(payload, ensureStringArray(args.select, "select"));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
tool: "documents.list",
|
|
411
|
+
profile: ctx.profile.id,
|
|
412
|
+
result: payload,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function documentsInfoTool(ctx, args) {
|
|
417
|
+
const ids = normalizeIds(args);
|
|
418
|
+
const view = args.view || "summary";
|
|
419
|
+
const excerptChars = toInteger(args.excerptChars, 280);
|
|
420
|
+
const armDelete = args.armDelete === true;
|
|
421
|
+
const readTokenTtlSeconds = toInteger(args.readTokenTtlSeconds, 900);
|
|
422
|
+
|
|
423
|
+
if (ids.length === 0 && !args.shareId) {
|
|
424
|
+
throw new CliError("documents.info requires args.id, args.ids, or args.shareId");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const maxAttempts = toInteger(args.maxAttempts, 2);
|
|
428
|
+
|
|
429
|
+
if (ids.length <= 1 && !args.ids) {
|
|
430
|
+
const body = compactValue({
|
|
431
|
+
id: ids[0],
|
|
432
|
+
shareId: args.shareId,
|
|
433
|
+
}) || {};
|
|
434
|
+
|
|
435
|
+
const res = await ctx.client.call("documents.info", body, { maxAttempts });
|
|
436
|
+
const rawDoc = res.body?.data;
|
|
437
|
+
let payload = res.body;
|
|
438
|
+
if (view !== "full" && payload.data) {
|
|
439
|
+
payload = {
|
|
440
|
+
...payload,
|
|
441
|
+
data: normalizeDocumentRow(payload.data, view, excerptChars),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
445
|
+
|
|
446
|
+
if (args.select) {
|
|
447
|
+
payload = applySelectToData(payload, ensureStringArray(args.select, "select"));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let deleteReadReceipt = undefined;
|
|
451
|
+
if (armDelete && rawDoc?.id) {
|
|
452
|
+
deleteReadReceipt = await issueDocumentDeleteReadReceipt({
|
|
453
|
+
profileId: ctx.profile.id,
|
|
454
|
+
documentId: rawDoc.id,
|
|
455
|
+
revision: rawDoc.revision,
|
|
456
|
+
title: rawDoc.title,
|
|
457
|
+
ttlSeconds: readTokenTtlSeconds,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const result = compactValue({
|
|
462
|
+
...payload,
|
|
463
|
+
deleteReadReceipt,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
tool: "documents.info",
|
|
468
|
+
profile: ctx.profile.id,
|
|
469
|
+
result,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const concurrency = toInteger(args.concurrency, 4);
|
|
474
|
+
const results = await mapLimit(ids, concurrency, async (id) => {
|
|
475
|
+
try {
|
|
476
|
+
const res = await ctx.client.call("documents.info", { id }, { maxAttempts });
|
|
477
|
+
let payload = res.body;
|
|
478
|
+
if (view !== "full" && payload.data) {
|
|
479
|
+
payload = {
|
|
480
|
+
...payload,
|
|
481
|
+
data: normalizeDocumentRow(payload.data, view, excerptChars),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
485
|
+
|
|
486
|
+
let deleteReadReceipt = undefined;
|
|
487
|
+
if (armDelete && res.body?.data?.id) {
|
|
488
|
+
deleteReadReceipt = await issueDocumentDeleteReadReceipt({
|
|
489
|
+
profileId: ctx.profile.id,
|
|
490
|
+
documentId: res.body.data.id,
|
|
491
|
+
revision: res.body.data.revision,
|
|
492
|
+
title: res.body.data.title,
|
|
493
|
+
ttlSeconds: readTokenTtlSeconds,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
id,
|
|
498
|
+
ok: true,
|
|
499
|
+
...payload,
|
|
500
|
+
...(deleteReadReceipt ? { deleteReadReceipt } : {}),
|
|
501
|
+
};
|
|
502
|
+
} catch (err) {
|
|
503
|
+
if (err instanceof ApiError) {
|
|
504
|
+
return {
|
|
505
|
+
id,
|
|
506
|
+
ok: false,
|
|
507
|
+
error: err.message,
|
|
508
|
+
status: err.details.status,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
throw err;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
tool: "documents.info",
|
|
517
|
+
profile: ctx.profile.id,
|
|
518
|
+
batched: true,
|
|
519
|
+
result: {
|
|
520
|
+
total: ids.length,
|
|
521
|
+
ok: results.filter((r) => r.ok).length,
|
|
522
|
+
failed: results.filter((r) => !r.ok).length,
|
|
523
|
+
items: results,
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function documentsCreateTool(ctx, args) {
|
|
529
|
+
const body = compactValue({
|
|
530
|
+
id: args.id,
|
|
531
|
+
title: args.title,
|
|
532
|
+
text: args.text,
|
|
533
|
+
icon: args.icon,
|
|
534
|
+
color: args.color,
|
|
535
|
+
collectionId: args.collectionId,
|
|
536
|
+
parentDocumentId: args.parentDocumentId,
|
|
537
|
+
templateId: args.templateId,
|
|
538
|
+
publish: args.publish,
|
|
539
|
+
fullWidth: args.fullWidth,
|
|
540
|
+
createdAt: args.createdAt,
|
|
541
|
+
dataAttributes: args.dataAttributes,
|
|
542
|
+
}) || {};
|
|
543
|
+
|
|
544
|
+
const res = await ctx.client.call("documents.create", body, {
|
|
545
|
+
maxAttempts: toInteger(args.maxAttempts, 1),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
let payload = res.body;
|
|
549
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
550
|
+
|
|
551
|
+
const view = args.view || "summary";
|
|
552
|
+
if (view !== "full" && payload.data) {
|
|
553
|
+
payload = {
|
|
554
|
+
...payload,
|
|
555
|
+
data: normalizeDocumentRow(payload.data, view, toInteger(args.excerptChars, 220)),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
tool: "documents.create",
|
|
561
|
+
profile: ctx.profile.id,
|
|
562
|
+
result: payload,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function documentsUpdateTool(ctx, args) {
|
|
567
|
+
if (!args.id) {
|
|
568
|
+
throw new CliError("documents.update requires args.id");
|
|
569
|
+
}
|
|
570
|
+
assertPerformAction(args, {
|
|
571
|
+
tool: "documents.update",
|
|
572
|
+
action: "update a document",
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const body = compactValue({
|
|
576
|
+
id: args.id,
|
|
577
|
+
title: args.title,
|
|
578
|
+
text: args.text,
|
|
579
|
+
icon: args.icon,
|
|
580
|
+
color: args.color,
|
|
581
|
+
fullWidth: args.fullWidth,
|
|
582
|
+
templateId: args.templateId,
|
|
583
|
+
collectionId: args.collectionId,
|
|
584
|
+
insightsEnabled: args.insightsEnabled,
|
|
585
|
+
editMode: args.editMode,
|
|
586
|
+
publish: args.publish,
|
|
587
|
+
dataAttributes: args.dataAttributes,
|
|
588
|
+
}) || {};
|
|
589
|
+
|
|
590
|
+
const res = await ctx.client.call("documents.update", body, {
|
|
591
|
+
maxAttempts: toInteger(args.maxAttempts, 1),
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
let payload = res.body;
|
|
595
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
596
|
+
|
|
597
|
+
const view = args.view || "summary";
|
|
598
|
+
if (view !== "full" && payload.data) {
|
|
599
|
+
payload = {
|
|
600
|
+
...payload,
|
|
601
|
+
data: normalizeDocumentRow(payload.data, view, toInteger(args.excerptChars, 220)),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
tool: "documents.update",
|
|
607
|
+
profile: ctx.profile.id,
|
|
608
|
+
result: payload,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function collectionsListTool(ctx, args) {
|
|
613
|
+
const body = compactValue({
|
|
614
|
+
limit: toInteger(args.limit, 25),
|
|
615
|
+
offset: toInteger(args.offset, 0),
|
|
616
|
+
sort: args.sort,
|
|
617
|
+
direction: args.direction,
|
|
618
|
+
query: args.query,
|
|
619
|
+
statusFilter: normalizeStatusFilter(args.statusFilter),
|
|
620
|
+
}) || {};
|
|
621
|
+
|
|
622
|
+
const res = await ctx.client.call("collections.list", body, {
|
|
623
|
+
maxAttempts: toInteger(args.maxAttempts, 2),
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const view = args.view || "summary";
|
|
627
|
+
let payload = res.body;
|
|
628
|
+
if (view !== "full") {
|
|
629
|
+
payload = {
|
|
630
|
+
...payload,
|
|
631
|
+
data: applyViewToList(payload.data, (item) => normalizeCollectionRow(item, view)),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
635
|
+
|
|
636
|
+
if (args.select) {
|
|
637
|
+
payload = applySelectToData(payload, ensureStringArray(args.select, "select"));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
tool: "collections.list",
|
|
642
|
+
profile: ctx.profile.id,
|
|
643
|
+
result: payload,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function collectionsInfoTool(ctx, args) {
|
|
648
|
+
const ids = normalizeIds(args);
|
|
649
|
+
if (ids.length === 0) {
|
|
650
|
+
throw new CliError("collections.info requires args.id or args.ids");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const view = args.view || "summary";
|
|
654
|
+
const maxAttempts = toInteger(args.maxAttempts, 2);
|
|
655
|
+
|
|
656
|
+
if (ids.length === 1 && !args.ids) {
|
|
657
|
+
const res = await ctx.client.call("collections.info", { id: ids[0] }, { maxAttempts });
|
|
658
|
+
let payload = res.body;
|
|
659
|
+
if (view !== "full" && payload.data) {
|
|
660
|
+
payload = {
|
|
661
|
+
...payload,
|
|
662
|
+
data: normalizeCollectionRow(payload.data, view),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
666
|
+
return {
|
|
667
|
+
tool: "collections.info",
|
|
668
|
+
profile: ctx.profile.id,
|
|
669
|
+
result: payload,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const concurrency = toInteger(args.concurrency, 4);
|
|
674
|
+
const results = await mapLimit(ids, concurrency, async (id) => {
|
|
675
|
+
try {
|
|
676
|
+
const res = await ctx.client.call("collections.info", { id }, { maxAttempts });
|
|
677
|
+
let payload = res.body;
|
|
678
|
+
if (view !== "full" && payload.data) {
|
|
679
|
+
payload = {
|
|
680
|
+
...payload,
|
|
681
|
+
data: normalizeCollectionRow(payload.data, view),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
payload = maybeDropPolicies(payload, !!args.includePolicies);
|
|
685
|
+
return {
|
|
686
|
+
id,
|
|
687
|
+
ok: true,
|
|
688
|
+
...payload,
|
|
689
|
+
};
|
|
690
|
+
} catch (err) {
|
|
691
|
+
if (err instanceof ApiError) {
|
|
692
|
+
return {
|
|
693
|
+
id,
|
|
694
|
+
ok: false,
|
|
695
|
+
error: err.message,
|
|
696
|
+
status: err.details.status,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
tool: "collections.info",
|
|
705
|
+
profile: ctx.profile.id,
|
|
706
|
+
batched: true,
|
|
707
|
+
result: {
|
|
708
|
+
total: ids.length,
|
|
709
|
+
ok: results.filter((r) => r.ok).length,
|
|
710
|
+
failed: results.filter((r) => !r.ok).length,
|
|
711
|
+
items: results,
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function collectionsCreateTool(ctx, args) {
|
|
717
|
+
if (!args.name) {
|
|
718
|
+
throw new CliError("collections.create requires args.name");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const body = compactValue({
|
|
722
|
+
name: args.name,
|
|
723
|
+
description: args.description,
|
|
724
|
+
permission: args.permission,
|
|
725
|
+
icon: args.icon,
|
|
726
|
+
color: args.color,
|
|
727
|
+
sharing: args.sharing,
|
|
728
|
+
}) || {};
|
|
729
|
+
|
|
730
|
+
const res = await ctx.client.call("collections.create", body, {
|
|
731
|
+
maxAttempts: toInteger(args.maxAttempts, 1),
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
let payload = maybeDropPolicies(res.body, !!args.includePolicies);
|
|
735
|
+
if ((args.view || "summary") !== "full" && payload.data) {
|
|
736
|
+
payload = {
|
|
737
|
+
...payload,
|
|
738
|
+
data: normalizeCollectionRow(payload.data, args.view || "summary"),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
tool: "collections.create",
|
|
744
|
+
profile: ctx.profile.id,
|
|
745
|
+
result: payload,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function collectionsUpdateTool(ctx, args) {
|
|
750
|
+
if (!args.id) {
|
|
751
|
+
throw new CliError("collections.update requires args.id");
|
|
752
|
+
}
|
|
753
|
+
assertPerformAction(args, {
|
|
754
|
+
tool: "collections.update",
|
|
755
|
+
action: "update a collection",
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const body = compactValue({
|
|
759
|
+
id: args.id,
|
|
760
|
+
name: args.name,
|
|
761
|
+
description: args.description,
|
|
762
|
+
permission: args.permission,
|
|
763
|
+
icon: args.icon,
|
|
764
|
+
color: args.color,
|
|
765
|
+
sharing: args.sharing,
|
|
766
|
+
}) || {};
|
|
767
|
+
|
|
768
|
+
const res = await ctx.client.call("collections.update", body, {
|
|
769
|
+
maxAttempts: toInteger(args.maxAttempts, 1),
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
let payload = maybeDropPolicies(res.body, !!args.includePolicies);
|
|
773
|
+
if ((args.view || "summary") !== "full" && payload.data) {
|
|
774
|
+
payload = {
|
|
775
|
+
...payload,
|
|
776
|
+
data: normalizeCollectionRow(payload.data, args.view || "summary"),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return {
|
|
781
|
+
tool: "collections.update",
|
|
782
|
+
profile: ctx.profile.id,
|
|
783
|
+
result: payload,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export const TOOLS = {
|
|
788
|
+
"api.call": {
|
|
789
|
+
signature:
|
|
790
|
+
"api.call(args: { method?: string; endpoint?: string; body?: object; includePolicies?: boolean; maxAttempts?: number; select?: string[]; performAction?: boolean; readToken?: string })",
|
|
791
|
+
description: "Call any Outline API RPC endpoint directly.",
|
|
792
|
+
usageExample: {
|
|
793
|
+
tool: "api.call",
|
|
794
|
+
args: {
|
|
795
|
+
method: "documents.info",
|
|
796
|
+
body: {
|
|
797
|
+
id: "outline-api-NTpezNwhUP",
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
bestPractices: [
|
|
802
|
+
"Use this for endpoints not yet wrapped as dedicated tools.",
|
|
803
|
+
"Send only the fields you need in `body` and use `select` to reduce tokens.",
|
|
804
|
+
"Pass either `method` or `endpoint`; both are accepted aliases.",
|
|
805
|
+
"Set maxAttempts=2 for read endpoints to absorb transient 429/5xx.",
|
|
806
|
+
"Mutating methods are action-gated; set performAction=true intentionally.",
|
|
807
|
+
],
|
|
808
|
+
handler: apiCallTool,
|
|
809
|
+
},
|
|
810
|
+
"auth.info": {
|
|
811
|
+
signature: "auth.info(args?: { includePolicies?: boolean; view?: 'summary' | 'full' })",
|
|
812
|
+
description: "Return authenticated user and team info to confirm profile permissions.",
|
|
813
|
+
usageExample: {
|
|
814
|
+
tool: "auth.info",
|
|
815
|
+
args: {
|
|
816
|
+
view: "summary",
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
bestPractices: [
|
|
820
|
+
"Call this once per session before mutating data.",
|
|
821
|
+
"Use summary view first, then full only when needed.",
|
|
822
|
+
"Inspect policies only when making capability decisions.",
|
|
823
|
+
],
|
|
824
|
+
handler: authInfoTool,
|
|
825
|
+
},
|
|
826
|
+
"documents.search": {
|
|
827
|
+
signature:
|
|
828
|
+
"documents.search(args: { query?: string; queries?: string[]; mode?: 'semantic' | 'titles'; limit?: number; offset?: number; collectionId?: string; documentId?: string; userId?: string; statusFilter?: string[]; dateFilter?: 'day'|'week'|'month'|'year'; snippetMinWords?: number; snippetMaxWords?: number; sort?: string; direction?: 'ASC'|'DESC'; view?: 'summary'|'ids'|'full'; includePolicies?: boolean; merge?: boolean; concurrency?: number; })",
|
|
829
|
+
description: "Search documents with single or multi-query batch in one invocation.",
|
|
830
|
+
usageExample: {
|
|
831
|
+
tool: "documents.search",
|
|
832
|
+
args: {
|
|
833
|
+
queries: ["deployment runbook", "oncall escalation"],
|
|
834
|
+
mode: "semantic",
|
|
835
|
+
limit: 8,
|
|
836
|
+
view: "summary",
|
|
837
|
+
merge: true,
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
bestPractices: [
|
|
841
|
+
"Prefer `queries[]` batch mode to reduce round trips.",
|
|
842
|
+
"Use `view=ids` for planning and follow with documents.info only on selected IDs.",
|
|
843
|
+
"Tune snippetMinWords/snippetMaxWords to control context window size.",
|
|
844
|
+
],
|
|
845
|
+
handler: documentsSearchTool,
|
|
846
|
+
},
|
|
847
|
+
"documents.list": {
|
|
848
|
+
signature:
|
|
849
|
+
"documents.list(args?: { limit?: number; offset?: number; sort?: string; direction?: 'ASC'|'DESC'; collectionId?: string; parentDocumentId?: string | null; userId?: string; statusFilter?: string[]; view?: 'ids'|'summary'|'full'; includePolicies?: boolean })",
|
|
850
|
+
description: "List documents with filtering and pagination.",
|
|
851
|
+
usageExample: {
|
|
852
|
+
tool: "documents.list",
|
|
853
|
+
args: {
|
|
854
|
+
collectionId: "6f35e6db-5930-4db8-9c31-66fe12f9f4aa",
|
|
855
|
+
limit: 20,
|
|
856
|
+
statusFilter: ["published"],
|
|
857
|
+
view: "summary",
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
bestPractices: [
|
|
861
|
+
"Use small page sizes (10-25) and iterate with offset.",
|
|
862
|
+
"Set parentDocumentId=null to list only collection root pages.",
|
|
863
|
+
"Use summary view unless the full document body is required.",
|
|
864
|
+
],
|
|
865
|
+
handler: documentsListTool,
|
|
866
|
+
},
|
|
867
|
+
"documents.info": {
|
|
868
|
+
signature:
|
|
869
|
+
"documents.info(args: { id?: string; ids?: string[]; shareId?: string; view?: 'summary'|'full'; includePolicies?: boolean; concurrency?: number; armDelete?: boolean; readTokenTtlSeconds?: number })",
|
|
870
|
+
description: "Read one or many documents by ID.",
|
|
871
|
+
usageExample: {
|
|
872
|
+
tool: "documents.info",
|
|
873
|
+
args: {
|
|
874
|
+
ids: ["doc-1", "doc-2", "doc-3"],
|
|
875
|
+
view: "summary",
|
|
876
|
+
concurrency: 3,
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
bestPractices: [
|
|
880
|
+
"Use `ids[]` batch mode to fetch multiple docs in one CLI call.",
|
|
881
|
+
"Use summary view first; only request full for final chosen docs.",
|
|
882
|
+
"Handle partial failures by checking each item.ok in batched result.",
|
|
883
|
+
"Set armDelete=true when you need a short-lived delete read token for a safe delete flow.",
|
|
884
|
+
],
|
|
885
|
+
handler: documentsInfoTool,
|
|
886
|
+
},
|
|
887
|
+
"documents.create": {
|
|
888
|
+
signature:
|
|
889
|
+
"documents.create(args: { title?: string; text?: string; collectionId?: string; parentDocumentId?: string; publish?: boolean; icon?: string; color?: string; templateId?: string; fullWidth?: boolean; view?: 'summary'|'full' })",
|
|
890
|
+
description: "Create a new document in Outline.",
|
|
891
|
+
usageExample: {
|
|
892
|
+
tool: "documents.create",
|
|
893
|
+
args: {
|
|
894
|
+
title: "Incident 2026-03-04",
|
|
895
|
+
text: "# Incident\n\nSummary...",
|
|
896
|
+
collectionId: "collection-id",
|
|
897
|
+
publish: true,
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
bestPractices: [
|
|
901
|
+
"Set publish=true only when collection/parent is known.",
|
|
902
|
+
"Use templates when available to standardize structure.",
|
|
903
|
+
"Store long markdown in a file and pass via args-file to avoid shell escaping issues.",
|
|
904
|
+
],
|
|
905
|
+
handler: documentsCreateTool,
|
|
906
|
+
},
|
|
907
|
+
"documents.update": {
|
|
908
|
+
signature:
|
|
909
|
+
"documents.update(args: { id: string; title?: string; text?: string; editMode?: 'replace'|'append'|'prepend'; publish?: boolean; collectionId?: string; templateId?: string; fullWidth?: boolean; insightsEnabled?: boolean; view?: 'summary'|'full'; performAction?: boolean })",
|
|
910
|
+
description: "Update an existing document.",
|
|
911
|
+
usageExample: {
|
|
912
|
+
tool: "documents.update",
|
|
913
|
+
args: {
|
|
914
|
+
id: "doc-id",
|
|
915
|
+
text: "\n\n## Follow-up\n- Added RCA",
|
|
916
|
+
editMode: "append",
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
bestPractices: [
|
|
920
|
+
"For append/prepend, include only incremental text instead of full document body.",
|
|
921
|
+
"Read the document first when multiple agents may edit concurrently.",
|
|
922
|
+
"Use publish=true only when transitioning drafts to published state intentionally.",
|
|
923
|
+
"This tool is action-gated; set performAction=true only after explicit confirmation.",
|
|
924
|
+
],
|
|
925
|
+
handler: documentsUpdateTool,
|
|
926
|
+
},
|
|
927
|
+
"collections.list": {
|
|
928
|
+
signature:
|
|
929
|
+
"collections.list(args?: { query?: string; limit?: number; offset?: number; sort?: string; direction?: 'ASC'|'DESC'; statusFilter?: string[]; view?: 'summary'|'full'; includePolicies?: boolean })",
|
|
930
|
+
description: "List collections visible to the current profile.",
|
|
931
|
+
usageExample: {
|
|
932
|
+
tool: "collections.list",
|
|
933
|
+
args: {
|
|
934
|
+
query: "engineering",
|
|
935
|
+
limit: 10,
|
|
936
|
+
view: "summary",
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
bestPractices: [
|
|
940
|
+
"Resolve collection IDs once and reuse them in later calls.",
|
|
941
|
+
"Use summary view for navigation and planning.",
|
|
942
|
+
"Include policies only when checking write privileges.",
|
|
943
|
+
],
|
|
944
|
+
handler: collectionsListTool,
|
|
945
|
+
},
|
|
946
|
+
"collections.info": {
|
|
947
|
+
signature:
|
|
948
|
+
"collections.info(args: { id?: string; ids?: string[]; view?: 'summary'|'full'; includePolicies?: boolean; concurrency?: number })",
|
|
949
|
+
description: "Read one or many collections by ID.",
|
|
950
|
+
usageExample: {
|
|
951
|
+
tool: "collections.info",
|
|
952
|
+
args: {
|
|
953
|
+
ids: ["col-1", "col-2"],
|
|
954
|
+
view: "summary",
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
bestPractices: [
|
|
958
|
+
"Use ids[] to batch collection hydration in one request.",
|
|
959
|
+
"Treat missing collections as permission or existence issues.",
|
|
960
|
+
"Use full view only for collection metadata you actually need.",
|
|
961
|
+
],
|
|
962
|
+
handler: collectionsInfoTool,
|
|
963
|
+
},
|
|
964
|
+
"collections.create": {
|
|
965
|
+
signature:
|
|
966
|
+
"collections.create(args: { name: string; description?: string; permission?: string; icon?: string; color?: string; sharing?: boolean; view?: 'summary'|'full' })",
|
|
967
|
+
description: "Create a collection.",
|
|
968
|
+
usageExample: {
|
|
969
|
+
tool: "collections.create",
|
|
970
|
+
args: {
|
|
971
|
+
name: "Agent Notes",
|
|
972
|
+
description: "Working area for AI-assisted drafts",
|
|
973
|
+
permission: "read_write",
|
|
974
|
+
sharing: false,
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
bestPractices: [
|
|
978
|
+
"Prefer explicit permission values to avoid ambiguous defaults.",
|
|
979
|
+
"Create collection first, then create documents under it.",
|
|
980
|
+
"Use summary output in autonomous loops.",
|
|
981
|
+
],
|
|
982
|
+
handler: collectionsCreateTool,
|
|
983
|
+
},
|
|
984
|
+
"collections.update": {
|
|
985
|
+
signature:
|
|
986
|
+
"collections.update(args: { id: string; name?: string; description?: string; permission?: string; icon?: string; color?: string; sharing?: boolean; view?: 'summary'|'full'; performAction?: boolean })",
|
|
987
|
+
description: "Update collection metadata.",
|
|
988
|
+
usageExample: {
|
|
989
|
+
tool: "collections.update",
|
|
990
|
+
args: {
|
|
991
|
+
id: "col-id",
|
|
992
|
+
description: "Updated description",
|
|
993
|
+
sharing: true,
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
bestPractices: [
|
|
997
|
+
"Read collection first when coordinating changes across agents.",
|
|
998
|
+
"Apply minimal field diffs rather than resending all properties.",
|
|
999
|
+
"Keep sharing changes explicit and auditable.",
|
|
1000
|
+
"This tool is action-gated; set performAction=true only after explicit confirmation.",
|
|
1001
|
+
],
|
|
1002
|
+
handler: collectionsUpdateTool,
|
|
1003
|
+
},
|
|
1004
|
+
...NAVIGATION_TOOLS,
|
|
1005
|
+
...MUTATION_TOOLS,
|
|
1006
|
+
...EXTENDED_TOOLS,
|
|
1007
|
+
...PLATFORM_TOOLS,
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
export function listTools() {
|
|
1011
|
+
return Object.entries(TOOLS).map(([name, def]) => ({
|
|
1012
|
+
name,
|
|
1013
|
+
signature: def.signature,
|
|
1014
|
+
description: def.description,
|
|
1015
|
+
}));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function getToolContract(name) {
|
|
1019
|
+
if (name === "all") {
|
|
1020
|
+
return Object.entries(TOOLS).map(([toolName, def]) => ({
|
|
1021
|
+
name: toolName,
|
|
1022
|
+
signature: def.signature,
|
|
1023
|
+
description: def.description,
|
|
1024
|
+
usageExample: def.usageExample,
|
|
1025
|
+
bestPractices: def.bestPractices,
|
|
1026
|
+
}));
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const def = TOOLS[name];
|
|
1030
|
+
if (!def) {
|
|
1031
|
+
throw new CliError(`Unknown tool: ${name}`);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return {
|
|
1035
|
+
name,
|
|
1036
|
+
signature: def.signature,
|
|
1037
|
+
description: def.description,
|
|
1038
|
+
usageExample: def.usageExample,
|
|
1039
|
+
bestPractices: def.bestPractices,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export async function invokeTool(ctx, name, args = {}) {
|
|
1044
|
+
const tool = TOOLS[name];
|
|
1045
|
+
if (!tool) {
|
|
1046
|
+
throw new CliError(`Unknown tool: ${name}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
validateToolArgs(name, args);
|
|
1050
|
+
|
|
1051
|
+
const result = await tool.handler(ctx, args);
|
|
1052
|
+
if (args.compact ?? true) {
|
|
1053
|
+
return compactValue(result) || {};
|
|
1054
|
+
}
|
|
1055
|
+
return result;
|
|
1056
|
+
}
|