@khanglvm/outline-cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +44 -22
- package/bin/outline-cli.js +14 -0
- package/docs/TOOL_CONTRACTS.md +8 -0
- package/package.json +1 -1
- package/src/agent-skills.js +160 -23
- package/src/cli.js +207 -63
- package/src/config-store.js +86 -6
- package/src/entry-integrity-manifest.generated.js +15 -11
- package/src/entry-integrity.js +3 -0
- package/src/summary-redaction.js +37 -0
- package/src/tool-arg-schemas.js +266 -10
- package/src/tools.extended.js +123 -16
- package/src/tools.js +277 -21
- package/src/tools.mutation.js +2 -1
- package/src/tools.navigation.js +3 -2
- package/test/agent-skills.unit.test.js +64 -1
- package/test/config-store.unit.test.js +32 -0
- package/test/hardening.unit.test.js +26 -1
- package/test/live.integration.test.js +20 -24
- package/test/profile-selection.unit.test.js +14 -4
- package/test/tool-resolution.unit.test.js +333 -0
- package/test/version.unit.test.js +21 -0
package/src/tools.extended.js
CHANGED
|
@@ -376,6 +376,87 @@ function parseQuestionItem(raw, index) {
|
|
|
376
376
|
throw new CliError(`questions[${index}] must be string or object`);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function isAnswerEndpointUnsupported(err) {
|
|
380
|
+
if (!(err instanceof ApiError)) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (err.details?.status !== 404) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
const url = String(err.details?.url || "").toLowerCase();
|
|
387
|
+
const message = String(err.message || "").toLowerCase();
|
|
388
|
+
return url.includes("documents.answerquestion") || message.includes("answerquestion");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normalizeFallbackAnswerHit(row, contextChars = 220) {
|
|
392
|
+
const doc = row?.document || row || {};
|
|
393
|
+
const context = typeof row?.context === "string" ? row.context : "";
|
|
394
|
+
return compactValue({
|
|
395
|
+
id: doc.id,
|
|
396
|
+
title: doc.title,
|
|
397
|
+
collectionId: doc.collectionId,
|
|
398
|
+
parentDocumentId: doc.parentDocumentId,
|
|
399
|
+
updatedAt: doc.updatedAt,
|
|
400
|
+
publishedAt: doc.publishedAt,
|
|
401
|
+
urlId: doc.urlId,
|
|
402
|
+
ranking: row?.ranking,
|
|
403
|
+
context: context.length > contextChars ? `${context.slice(0, contextChars)}...` : context,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function buildAnswerFallbackResult(ctx, question, args = {}) {
|
|
408
|
+
const maxAttempts = Math.max(1, toInteger(args.maxAttempts, 2));
|
|
409
|
+
const contextChars = Math.max(80, toInteger(args.contextChars, 220));
|
|
410
|
+
const limit = Math.max(1, Math.min(8, toInteger(args.limit, 5)));
|
|
411
|
+
const body = compactValue({
|
|
412
|
+
query: question,
|
|
413
|
+
collectionId: args.collectionId,
|
|
414
|
+
documentId: args.documentId || args.id,
|
|
415
|
+
userId: args.userId,
|
|
416
|
+
shareId: args.shareId,
|
|
417
|
+
statusFilter: args.statusFilter,
|
|
418
|
+
limit,
|
|
419
|
+
offset: 0,
|
|
420
|
+
snippetMinWords: toInteger(args.snippetMinWords, 20),
|
|
421
|
+
snippetMaxWords: toInteger(args.snippetMaxWords, 30),
|
|
422
|
+
}) || {};
|
|
423
|
+
|
|
424
|
+
const res = await ctx.client.call("documents.search", body, { maxAttempts });
|
|
425
|
+
const payload = maybeDropPolicies(res.body, !!args.includePolicies);
|
|
426
|
+
const hits = Array.isArray(payload?.data) ? payload.data : [];
|
|
427
|
+
const documents = hits.slice(0, Math.min(5, limit)).map((row) => normalizeFallbackAnswerHit(row, contextChars));
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
question,
|
|
431
|
+
answer: "",
|
|
432
|
+
noAnswerReason: "documents.answerQuestion is unsupported by this Outline deployment; returning ranked retrieval results instead.",
|
|
433
|
+
unsupported: true,
|
|
434
|
+
fallbackUsed: true,
|
|
435
|
+
fallbackTool: "documents.search",
|
|
436
|
+
fallbackSuggestion: {
|
|
437
|
+
tool: "documents.search",
|
|
438
|
+
args: compactValue({
|
|
439
|
+
query: question,
|
|
440
|
+
collectionId: args.collectionId,
|
|
441
|
+
documentId: args.documentId || args.id,
|
|
442
|
+
userId: args.userId,
|
|
443
|
+
shareId: args.shareId,
|
|
444
|
+
statusFilter: args.statusFilter,
|
|
445
|
+
limit,
|
|
446
|
+
view: "summary",
|
|
447
|
+
}),
|
|
448
|
+
},
|
|
449
|
+
documents,
|
|
450
|
+
retrieval: {
|
|
451
|
+
query: question,
|
|
452
|
+
result: compactValue({
|
|
453
|
+
...payload,
|
|
454
|
+
data: hits.map((row) => normalizeFallbackAnswerHit(row, contextChars)),
|
|
455
|
+
}),
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
379
460
|
async function documentsAnswerTool(ctx, args = {}) {
|
|
380
461
|
const question = String(args.question ?? args.query ?? "").trim();
|
|
381
462
|
if (!question) {
|
|
@@ -383,23 +464,35 @@ async function documentsAnswerTool(ctx, args = {}) {
|
|
|
383
464
|
}
|
|
384
465
|
|
|
385
466
|
const body = {
|
|
386
|
-
...buildBody(args, ["question", "query"]),
|
|
467
|
+
...buildBody(args, ["question", "query", "limit"]),
|
|
387
468
|
query: question,
|
|
388
469
|
};
|
|
389
470
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
471
|
+
try {
|
|
472
|
+
const res = await ctx.client.call("documents.answerQuestion", body, {
|
|
473
|
+
maxAttempts: toInteger(args.maxAttempts, 2),
|
|
474
|
+
});
|
|
475
|
+
const payload = maybeDropPolicies(res.body, !!args.includePolicies);
|
|
394
476
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
477
|
+
return {
|
|
478
|
+
tool: "documents.answer",
|
|
479
|
+
profile: ctx.profile.id,
|
|
480
|
+
result:
|
|
481
|
+
payload && typeof payload === "object"
|
|
482
|
+
? { question, ...payload }
|
|
483
|
+
: { question, data: payload },
|
|
484
|
+
};
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (!isAnswerEndpointUnsupported(err)) {
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
tool: "documents.answer",
|
|
492
|
+
profile: ctx.profile.id,
|
|
493
|
+
result: await buildAnswerFallbackResult(ctx, question, args),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
403
496
|
}
|
|
404
497
|
|
|
405
498
|
async function documentsAnswerBatchTool(ctx, args = {}) {
|
|
@@ -415,7 +508,7 @@ async function documentsAnswerBatchTool(ctx, args = {}) {
|
|
|
415
508
|
throw new CliError("documents.answer_batch requires args.question or args.questions[]");
|
|
416
509
|
}
|
|
417
510
|
|
|
418
|
-
const baseBody = buildBody(args, ["question", "questions", "query", "concurrency"]);
|
|
511
|
+
const baseBody = buildBody(args, ["question", "questions", "query", "concurrency", "limit"]);
|
|
419
512
|
const includePolicies = !!args.includePolicies;
|
|
420
513
|
const maxAttempts = toInteger(args.maxAttempts, 2);
|
|
421
514
|
const concurrency = Math.max(1, Math.min(10, toInteger(args.concurrency, 3)));
|
|
@@ -441,6 +534,18 @@ async function documentsAnswerBatchTool(ctx, args = {}) {
|
|
|
441
534
|
result: payload,
|
|
442
535
|
};
|
|
443
536
|
} catch (err) {
|
|
537
|
+
if (parsed && isAnswerEndpointUnsupported(err)) {
|
|
538
|
+
return {
|
|
539
|
+
index,
|
|
540
|
+
ok: true,
|
|
541
|
+
question: parsed.question,
|
|
542
|
+
documentId: parsed.documentId,
|
|
543
|
+
result: await buildAnswerFallbackResult(ctx, parsed.question, {
|
|
544
|
+
...args,
|
|
545
|
+
...parsed.body,
|
|
546
|
+
}),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
444
549
|
if (err instanceof ApiError || err instanceof CliError) {
|
|
445
550
|
return {
|
|
446
551
|
index,
|
|
@@ -2972,7 +3077,7 @@ export const EXTENDED_TOOLS = {
|
|
|
2972
3077
|
...RPC_TOOLS,
|
|
2973
3078
|
"documents.answer": {
|
|
2974
3079
|
signature:
|
|
2975
|
-
"documents.answer(args: { question?: string; query?: string; ...endpointArgs; includePolicies?: boolean; maxAttempts?: number })",
|
|
3080
|
+
"documents.answer(args: { question?: string; query?: string; limit?: number; ...endpointArgs; includePolicies?: boolean; maxAttempts?: number })",
|
|
2976
3081
|
description: "Answer a question using Outline AI over the selected document scope.",
|
|
2977
3082
|
usageExample: {
|
|
2978
3083
|
tool: "documents.answer",
|
|
@@ -2984,12 +3089,13 @@ export const EXTENDED_TOOLS = {
|
|
|
2984
3089
|
bestPractices: [
|
|
2985
3090
|
"Use question text that is specific enough to resolve citations quickly.",
|
|
2986
3091
|
"Scope by collectionId or documentId to reduce latency and hallucination risk.",
|
|
3092
|
+
"If the deployment lacks documents.answerQuestion, this wrapper returns fallback retrieval evidence and a concrete search suggestion instead of a raw 404.",
|
|
2987
3093
|
],
|
|
2988
3094
|
handler: documentsAnswerTool,
|
|
2989
3095
|
},
|
|
2990
3096
|
"documents.answer_batch": {
|
|
2991
3097
|
signature:
|
|
2992
|
-
"documents.answer_batch(args: { question?: string; questions?: Array<string | { question?: string; query?: string; ...endpointArgs }>; ...endpointArgs; concurrency?: number; includePolicies?: boolean; maxAttempts?: number })",
|
|
3098
|
+
"documents.answer_batch(args: { question?: string; questions?: Array<string | { question?: string; query?: string; ...endpointArgs }>; limit?: number; ...endpointArgs; concurrency?: number; includePolicies?: boolean; maxAttempts?: number })",
|
|
2993
3099
|
description: "Run multiple documents.answerQuestion calls with per-item isolation.",
|
|
2994
3100
|
usageExample: {
|
|
2995
3101
|
tool: "documents.answer_batch",
|
|
@@ -3005,6 +3111,7 @@ export const EXTENDED_TOOLS = {
|
|
|
3005
3111
|
bestPractices: [
|
|
3006
3112
|
"Prefer small batches and low concurrency for predictable token and latency budgets.",
|
|
3007
3113
|
"Use per-item statuses to retry only failures.",
|
|
3114
|
+
"Unsupported answer endpoints degrade to per-item retrieval evidence rather than failing the whole batch.",
|
|
3008
3115
|
],
|
|
3009
3116
|
handler: documentsAnswerBatchTool,
|
|
3010
3117
|
},
|
package/src/tools.js
CHANGED
|
@@ -12,6 +12,7 @@ import { MUTATION_TOOLS } from "./tools.mutation.js";
|
|
|
12
12
|
import { PLATFORM_TOOLS } from "./tools.platform.js";
|
|
13
13
|
import { EXTENDED_TOOLS } from "./tools.extended.js";
|
|
14
14
|
import { validateToolArgs } from "./tool-arg-schemas.js";
|
|
15
|
+
import { summarizeSafeText } from "./summary-redaction.js";
|
|
15
16
|
import {
|
|
16
17
|
compactValue,
|
|
17
18
|
ensureStringArray,
|
|
@@ -51,7 +52,7 @@ function normalizeSearchRow(row, view = "summary", contextChars = 320) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const doc = row.document || row;
|
|
54
|
-
const context = row.context
|
|
55
|
+
const context = typeof row.context === "string" ? row.context : "";
|
|
55
56
|
const summary = {
|
|
56
57
|
id: doc.id,
|
|
57
58
|
title: doc.title,
|
|
@@ -61,7 +62,7 @@ function normalizeSearchRow(row, view = "summary", contextChars = 320) {
|
|
|
61
62
|
publishedAt: doc.publishedAt,
|
|
62
63
|
urlId: doc.urlId,
|
|
63
64
|
ranking: row.ranking,
|
|
64
|
-
context: context
|
|
65
|
+
context: summarizeSafeText(context, contextChars),
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
if (view === "ids") {
|
|
@@ -100,7 +101,7 @@ function normalizeDocumentRow(row, view = "summary", excerptChars = 280) {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
if (row.text) {
|
|
103
|
-
summary.excerpt = row.text
|
|
104
|
+
summary.excerpt = summarizeSafeText(row.text, excerptChars);
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
return summary;
|
|
@@ -381,12 +382,15 @@ async function documentsListTool(ctx, args) {
|
|
|
381
382
|
collectionId: args.collectionId,
|
|
382
383
|
userId: args.userId,
|
|
383
384
|
backlinkDocumentId: args.backlinkDocumentId,
|
|
384
|
-
parentDocumentId: Object.prototype.hasOwnProperty.call(args, "parentDocumentId")
|
|
385
|
-
? args.parentDocumentId
|
|
386
|
-
: undefined,
|
|
387
385
|
statusFilter: normalizeStatusFilter(args.statusFilter),
|
|
388
386
|
}) || {};
|
|
389
387
|
|
|
388
|
+
if (Object.prototype.hasOwnProperty.call(args, "parentDocumentId")) {
|
|
389
|
+
body.parentDocumentId = args.parentDocumentId;
|
|
390
|
+
} else if (args.rootOnly === true) {
|
|
391
|
+
body.parentDocumentId = null;
|
|
392
|
+
}
|
|
393
|
+
|
|
390
394
|
const res = await ctx.client.call("documents.list", body, {
|
|
391
395
|
maxAttempts: toInteger(args.maxAttempts, 2),
|
|
392
396
|
});
|
|
@@ -846,7 +850,7 @@ export const TOOLS = {
|
|
|
846
850
|
},
|
|
847
851
|
"documents.list": {
|
|
848
852
|
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 })",
|
|
853
|
+
"documents.list(args?: { limit?: number; offset?: number; sort?: string; direction?: 'ASC'|'DESC'; collectionId?: string; parentDocumentId?: string | null; rootOnly?: boolean; userId?: string; statusFilter?: string[]; view?: 'ids'|'summary'|'full'; includePolicies?: boolean })",
|
|
850
854
|
description: "List documents with filtering and pagination.",
|
|
851
855
|
usageExample: {
|
|
852
856
|
tool: "documents.list",
|
|
@@ -859,7 +863,7 @@ export const TOOLS = {
|
|
|
859
863
|
},
|
|
860
864
|
bestPractices: [
|
|
861
865
|
"Use small page sizes (10-25) and iterate with offset.",
|
|
862
|
-
"
|
|
866
|
+
"Use rootOnly=true (or parentDocumentId=null) to list only collection root pages.",
|
|
863
867
|
"Use summary view unless the full document body is required.",
|
|
864
868
|
],
|
|
865
869
|
handler: documentsListTool,
|
|
@@ -1007,6 +1011,216 @@ export const TOOLS = {
|
|
|
1007
1011
|
...PLATFORM_TOOLS,
|
|
1008
1012
|
};
|
|
1009
1013
|
|
|
1014
|
+
const TOOL_ALIAS_DEFS = [
|
|
1015
|
+
{
|
|
1016
|
+
aliases: [
|
|
1017
|
+
"documents.search_titles",
|
|
1018
|
+
"documents.searchtitles",
|
|
1019
|
+
"documents.search-titles",
|
|
1020
|
+
"documents.search titles",
|
|
1021
|
+
],
|
|
1022
|
+
name: "documents.search",
|
|
1023
|
+
argPatch: { mode: "titles" },
|
|
1024
|
+
reason: "mapped Outline title-search endpoint to wrapped documents.search with mode=titles",
|
|
1025
|
+
},
|
|
1026
|
+
{ aliases: ["docs.search"], name: "documents.search", reason: "mapped shorthand docs.* alias to documents.*" },
|
|
1027
|
+
{ aliases: ["docs.list"], name: "documents.list", reason: "mapped shorthand docs.* alias to documents.*" },
|
|
1028
|
+
{ aliases: ["docs.info"], name: "documents.info", reason: "mapped shorthand docs.* alias to documents.*" },
|
|
1029
|
+
{ aliases: ["docs.answer"], name: "documents.answer", reason: "mapped shorthand docs.* alias to documents.*" },
|
|
1030
|
+
{ aliases: ["docs.answer_batch"], name: "documents.answer_batch", reason: "mapped shorthand docs.* alias to documents.*" },
|
|
1031
|
+
];
|
|
1032
|
+
|
|
1033
|
+
function toSnakeCase(value) {
|
|
1034
|
+
return String(value || "")
|
|
1035
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
1036
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2");
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function canonicalizeToolName(value, options = {}) {
|
|
1040
|
+
const hyphenMode = options.hyphenMode || "underscore";
|
|
1041
|
+
let normalized = toSnakeCase(value).trim().toLowerCase();
|
|
1042
|
+
normalized = normalized.replace(/[/:\s]+/g, ".");
|
|
1043
|
+
normalized = hyphenMode === "dot"
|
|
1044
|
+
? normalized.replace(/-+/g, ".")
|
|
1045
|
+
: normalized.replace(/-+/g, "_");
|
|
1046
|
+
normalized = normalized.replace(/\.+/g, ".").replace(/^\.+|\.+$/g, "");
|
|
1047
|
+
normalized = normalized.replace(/_+/g, "_");
|
|
1048
|
+
return normalized;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function toolNameVariants(name) {
|
|
1052
|
+
const raw = String(name || "").trim();
|
|
1053
|
+
return [...new Set([
|
|
1054
|
+
raw,
|
|
1055
|
+
raw.toLowerCase(),
|
|
1056
|
+
canonicalizeToolName(raw, { hyphenMode: "underscore" }),
|
|
1057
|
+
canonicalizeToolName(raw, { hyphenMode: "dot" }),
|
|
1058
|
+
].filter(Boolean))];
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function buildCanonicalToolMap() {
|
|
1062
|
+
const byCanonical = new Map();
|
|
1063
|
+
for (const toolName of Object.keys(TOOLS)) {
|
|
1064
|
+
for (const variant of toolNameVariants(toolName)) {
|
|
1065
|
+
const existing = byCanonical.get(variant) || [];
|
|
1066
|
+
existing.push(toolName);
|
|
1067
|
+
byCanonical.set(variant, existing);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return byCanonical;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function buildAliasMap() {
|
|
1074
|
+
const aliasMap = new Map();
|
|
1075
|
+
for (const def of TOOL_ALIAS_DEFS) {
|
|
1076
|
+
for (const alias of def.aliases) {
|
|
1077
|
+
for (const variant of toolNameVariants(alias)) {
|
|
1078
|
+
aliasMap.set(variant, def);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return aliasMap;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const CANONICAL_TOOL_MAP = buildCanonicalToolMap();
|
|
1086
|
+
const TOOL_ALIAS_MAP = buildAliasMap();
|
|
1087
|
+
|
|
1088
|
+
function levenshteinDistance(a, b) {
|
|
1089
|
+
const left = String(a || "");
|
|
1090
|
+
const right = String(b || "");
|
|
1091
|
+
const rows = left.length + 1;
|
|
1092
|
+
const cols = right.length + 1;
|
|
1093
|
+
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
1094
|
+
|
|
1095
|
+
for (let i = 0; i < rows; i += 1) {
|
|
1096
|
+
matrix[i][0] = i;
|
|
1097
|
+
}
|
|
1098
|
+
for (let j = 0; j < cols; j += 1) {
|
|
1099
|
+
matrix[0][j] = j;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
for (let i = 1; i < rows; i += 1) {
|
|
1103
|
+
for (let j = 1; j < cols; j += 1) {
|
|
1104
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
1105
|
+
matrix[i][j] = Math.min(
|
|
1106
|
+
matrix[i - 1][j] + 1,
|
|
1107
|
+
matrix[i][j - 1] + 1,
|
|
1108
|
+
matrix[i - 1][j - 1] + cost
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return matrix[rows - 1][cols - 1];
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function suggestionScore(input, candidate) {
|
|
1117
|
+
const requested = canonicalizeToolName(input, { hyphenMode: "underscore" });
|
|
1118
|
+
const option = canonicalizeToolName(candidate, { hyphenMode: "underscore" });
|
|
1119
|
+
const requestedParts = requested.split(".").filter(Boolean);
|
|
1120
|
+
const optionParts = option.split(".").filter(Boolean);
|
|
1121
|
+
const distance = levenshteinDistance(requested, option);
|
|
1122
|
+
let score = 1 / (1 + distance);
|
|
1123
|
+
|
|
1124
|
+
if (requested === option) {
|
|
1125
|
+
score += 10;
|
|
1126
|
+
}
|
|
1127
|
+
if (requested.startsWith(option) || option.startsWith(requested)) {
|
|
1128
|
+
score += 1.5;
|
|
1129
|
+
}
|
|
1130
|
+
if (requested.includes(option) || option.includes(requested)) {
|
|
1131
|
+
score += 0.8;
|
|
1132
|
+
}
|
|
1133
|
+
if (requestedParts[0] && requestedParts[0] === optionParts[0]) {
|
|
1134
|
+
score += 0.5;
|
|
1135
|
+
}
|
|
1136
|
+
if (requestedParts.at(-1) && requestedParts.at(-1) === optionParts.at(-1)) {
|
|
1137
|
+
score += 0.9;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return Number(score.toFixed(4));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export function listToolSuggestions(name, limit = 5) {
|
|
1144
|
+
return Object.keys(TOOLS)
|
|
1145
|
+
.map((toolName) => ({
|
|
1146
|
+
name: toolName,
|
|
1147
|
+
score: suggestionScore(name, toolName),
|
|
1148
|
+
}))
|
|
1149
|
+
.sort((a, b) => {
|
|
1150
|
+
if (b.score !== a.score) {
|
|
1151
|
+
return b.score - a.score;
|
|
1152
|
+
}
|
|
1153
|
+
return a.name.localeCompare(b.name);
|
|
1154
|
+
})
|
|
1155
|
+
.filter((row, index) => row.score >= 0.22 || index < Math.min(limit, 3))
|
|
1156
|
+
.slice(0, limit);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export function resolveToolInvocation(name, args = {}) {
|
|
1160
|
+
const requestedName = String(name || "").trim();
|
|
1161
|
+
if (TOOLS[requestedName]) {
|
|
1162
|
+
return {
|
|
1163
|
+
requestedName,
|
|
1164
|
+
resolvedName: requestedName,
|
|
1165
|
+
args,
|
|
1166
|
+
autoCorrected: false,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
for (const variant of toolNameVariants(requestedName)) {
|
|
1171
|
+
const alias = TOOL_ALIAS_MAP.get(variant);
|
|
1172
|
+
if (alias) {
|
|
1173
|
+
const nextArgs = { ...(args || {}) };
|
|
1174
|
+
const injectedArgs = [];
|
|
1175
|
+
for (const [key, value] of Object.entries(alias.argPatch || {})) {
|
|
1176
|
+
if (nextArgs[key] === undefined) {
|
|
1177
|
+
nextArgs[key] = value;
|
|
1178
|
+
injectedArgs.push(key);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
requestedName,
|
|
1184
|
+
resolvedName: alias.name,
|
|
1185
|
+
args: nextArgs,
|
|
1186
|
+
autoCorrected: true,
|
|
1187
|
+
reason: alias.reason,
|
|
1188
|
+
injectedArgs,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const candidates = new Set();
|
|
1194
|
+
for (const variant of toolNameVariants(requestedName)) {
|
|
1195
|
+
const matches = CANONICAL_TOOL_MAP.get(variant) || [];
|
|
1196
|
+
for (const match of matches) {
|
|
1197
|
+
candidates.add(match);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (candidates.size === 1) {
|
|
1202
|
+
const [resolvedName] = [...candidates];
|
|
1203
|
+
return {
|
|
1204
|
+
requestedName,
|
|
1205
|
+
resolvedName,
|
|
1206
|
+
args,
|
|
1207
|
+
autoCorrected: true,
|
|
1208
|
+
reason: "normalized separators/casing to a known wrapped tool",
|
|
1209
|
+
injectedArgs: [],
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const suggestions = listToolSuggestions(requestedName);
|
|
1214
|
+
throw new CliError(`Unknown tool: ${requestedName}`, {
|
|
1215
|
+
code: "UNKNOWN_TOOL",
|
|
1216
|
+
requestedTool: requestedName,
|
|
1217
|
+
suggestions,
|
|
1218
|
+
hint: suggestions.length > 0
|
|
1219
|
+
? `Try ${suggestions.map((row) => row.name).join(", ")}`
|
|
1220
|
+
: "Run `outline-cli tools list` to inspect available tools.",
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1010
1224
|
export function listTools() {
|
|
1011
1225
|
return Object.entries(TOOLS).map(([name, def]) => ({
|
|
1012
1226
|
name,
|
|
@@ -1026,31 +1240,73 @@ export function getToolContract(name) {
|
|
|
1026
1240
|
}));
|
|
1027
1241
|
}
|
|
1028
1242
|
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
throw new CliError(`Unknown tool: ${name}`);
|
|
1032
|
-
}
|
|
1243
|
+
const resolution = resolveToolInvocation(name, {});
|
|
1244
|
+
const def = TOOLS[resolution.resolvedName];
|
|
1033
1245
|
|
|
1034
1246
|
return {
|
|
1035
|
-
name,
|
|
1247
|
+
name: resolution.resolvedName,
|
|
1036
1248
|
signature: def.signature,
|
|
1037
1249
|
description: def.description,
|
|
1038
1250
|
usageExample: def.usageExample,
|
|
1039
1251
|
bestPractices: def.bestPractices,
|
|
1252
|
+
...(resolution.autoCorrected
|
|
1253
|
+
? {
|
|
1254
|
+
requestedName: resolution.requestedName,
|
|
1255
|
+
autoCorrected: true,
|
|
1256
|
+
reason: resolution.reason,
|
|
1257
|
+
injectedArgs: resolution.injectedArgs,
|
|
1258
|
+
}
|
|
1259
|
+
: {}),
|
|
1040
1260
|
};
|
|
1041
1261
|
}
|
|
1042
1262
|
|
|
1043
1263
|
export async function invokeTool(ctx, name, args = {}) {
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1264
|
+
const resolution = resolveToolInvocation(name, args);
|
|
1265
|
+
const tool = TOOLS[resolution.resolvedName];
|
|
1266
|
+
let normalizedArgs;
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
normalizedArgs = validateToolArgs(resolution.resolvedName, resolution.args);
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
if (err instanceof CliError && err.details?.code === "ARG_VALIDATION_FAILED") {
|
|
1272
|
+
err.details = {
|
|
1273
|
+
...err.details,
|
|
1274
|
+
toolSignature: tool.signature,
|
|
1275
|
+
usageExample: tool.usageExample,
|
|
1276
|
+
contractHint:
|
|
1277
|
+
"Run `outline-cli tools contract " + resolution.resolvedName + " --result-mode inline` for the full contract.",
|
|
1278
|
+
...(resolution.autoCorrected
|
|
1279
|
+
? {
|
|
1280
|
+
requestedTool: resolution.requestedName,
|
|
1281
|
+
toolResolution: {
|
|
1282
|
+
autoCorrected: true,
|
|
1283
|
+
resolvedTool: resolution.resolvedName,
|
|
1284
|
+
reason: resolution.reason,
|
|
1285
|
+
injectedArgs: resolution.injectedArgs,
|
|
1286
|
+
},
|
|
1287
|
+
}
|
|
1288
|
+
: {}),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
throw err;
|
|
1047
1292
|
}
|
|
1048
1293
|
|
|
1049
|
-
|
|
1294
|
+
const result = await tool.handler(ctx, normalizedArgs);
|
|
1295
|
+
const enriched = resolution.autoCorrected
|
|
1296
|
+
? {
|
|
1297
|
+
...result,
|
|
1298
|
+
requestedTool: resolution.requestedName,
|
|
1299
|
+
toolResolution: {
|
|
1300
|
+
autoCorrected: true,
|
|
1301
|
+
resolvedTool: resolution.resolvedName,
|
|
1302
|
+
reason: resolution.reason,
|
|
1303
|
+
injectedArgs: resolution.injectedArgs,
|
|
1304
|
+
},
|
|
1305
|
+
}
|
|
1306
|
+
: result;
|
|
1050
1307
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return compactValue(result) || {};
|
|
1308
|
+
if (normalizedArgs.compact ?? true) {
|
|
1309
|
+
return compactValue(enriched) || {};
|
|
1054
1310
|
}
|
|
1055
|
-
return
|
|
1311
|
+
return enriched;
|
|
1056
1312
|
}
|
package/src/tools.mutation.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getDocumentDeleteReadReceipt,
|
|
7
7
|
} from "./action-gate.js";
|
|
8
8
|
import { compactValue, mapLimit, toInteger } from "./utils.js";
|
|
9
|
+
import { summarizeSafeText } from "./summary-redaction.js";
|
|
9
10
|
|
|
10
11
|
function normalizeDocumentSummary(doc, view = "summary", excerptChars = 220) {
|
|
11
12
|
if (!doc) {
|
|
@@ -28,7 +29,7 @@ function normalizeDocumentSummary(doc, view = "summary", excerptChars = 220) {
|
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
if (doc.text) {
|
|
31
|
-
summary.excerpt = doc.text
|
|
32
|
+
summary.excerpt = summarizeSafeText(doc.text, excerptChars);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
return summary;
|
package/src/tools.navigation.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
2
|
import { compactValue, ensureStringArray, mapLimit, toInteger } from "./utils.js";
|
|
3
|
+
import { summarizeSafeText } from "./summary-redaction.js";
|
|
3
4
|
|
|
4
5
|
function normalizeDocumentRow(row, view = "summary", excerptChars = 220) {
|
|
5
6
|
if (!row) {
|
|
@@ -29,7 +30,7 @@ function normalizeDocumentRow(row, view = "summary", excerptChars = 220) {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
if (row.text) {
|
|
32
|
-
summary.excerpt = row.text
|
|
33
|
+
summary.excerpt = summarizeSafeText(row.text, excerptChars);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
return summary;
|
|
@@ -407,7 +408,7 @@ function shapeResearchMergedRow(row, view, excerptChars, evidencePerDocument = 5
|
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
if (row.text) {
|
|
410
|
-
summary.excerpt = row.text
|
|
411
|
+
summary.excerpt = summarizeSafeText(row.text, excerptChars);
|
|
411
412
|
}
|
|
412
413
|
|
|
413
414
|
if (Array.isArray(row.evidence)) {
|
|
@@ -18,6 +18,34 @@ test("getQuickStartAgentHelp returns summary by default", () => {
|
|
|
18
18
|
assert.equal(payload.view, "summary");
|
|
19
19
|
assert.ok(Array.isArray(payload.steps));
|
|
20
20
|
assert.ok(payload.steps.length >= 4);
|
|
21
|
+
assert.ok(
|
|
22
|
+
payload.steps.some(
|
|
23
|
+
(row) => Array.isArray(row.commands) && row.commands.includes("outline-cli --version")
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
assert.ok(
|
|
27
|
+
payload.steps.some(
|
|
28
|
+
(row) =>
|
|
29
|
+
typeof row.question === "string" &&
|
|
30
|
+
row.question.toLowerCase().includes("install the outline-cli skill")
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
assert.ok(
|
|
34
|
+
payload.steps.some(
|
|
35
|
+
(row) =>
|
|
36
|
+
Array.isArray(row.commandTemplates) &&
|
|
37
|
+
row.commandTemplates.some(
|
|
38
|
+
(cmd) => cmd.includes("npx skills add") && cmd.includes("--skill outline-cli -y") && !cmd.includes("--agent")
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
assert.ok(
|
|
43
|
+
payload.steps.some(
|
|
44
|
+
(row) =>
|
|
45
|
+
typeof row.question === "string" &&
|
|
46
|
+
row.question.toLowerCase().includes("base url")
|
|
47
|
+
)
|
|
48
|
+
);
|
|
21
49
|
assert.equal(payload.nextCommand, "outline-cli tools help quick-start-agent --view full");
|
|
22
50
|
});
|
|
23
51
|
|
|
@@ -26,7 +54,42 @@ test("getQuickStartAgentHelp returns full payload and validates view", () => {
|
|
|
26
54
|
assert.equal(payload.section, "quick-start-agent");
|
|
27
55
|
assert.equal(payload.view, "full");
|
|
28
56
|
assert.ok(Array.isArray(payload.steps));
|
|
29
|
-
assert.ok(payload.steps.some((row) => row.command === "
|
|
57
|
+
assert.ok(payload.steps.some((row) => row.command === "outline-cli profile list"));
|
|
58
|
+
assert.ok(
|
|
59
|
+
payload.steps.some(
|
|
60
|
+
(row) =>
|
|
61
|
+
row.command &&
|
|
62
|
+
row.command.includes("--auth-type apiKey") &&
|
|
63
|
+
row.command.includes("--api-key")
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
assert.ok(
|
|
67
|
+
payload.steps.some(
|
|
68
|
+
(row) =>
|
|
69
|
+
Array.isArray(row.commandTemplates) &&
|
|
70
|
+
row.commandTemplates.includes(
|
|
71
|
+
"npx skills add https://github.com/khanglvm/skills --skill outline-cli -y"
|
|
72
|
+
) &&
|
|
73
|
+
Array.isArray(row.decisionRules) &&
|
|
74
|
+
row.decisionRules.some((rule) => rule.toLowerCase().includes("explicitly approves"))
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
assert.ok(
|
|
78
|
+
payload.steps.some(
|
|
79
|
+
(row) =>
|
|
80
|
+
row.apiKeySettingsUrlTemplate === "<base-url>/settings/api-and-apps" &&
|
|
81
|
+
Array.isArray(row.apiKeyConfigTemplate) &&
|
|
82
|
+
row.apiKeyConfigTemplate.length >= 3
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
assert.ok(
|
|
86
|
+
payload.steps.some(
|
|
87
|
+
(row) =>
|
|
88
|
+
row.minimumPromptCount >= 10 &&
|
|
89
|
+
Array.isArray(row.naturalLanguagePrompts) &&
|
|
90
|
+
row.naturalLanguagePrompts.length >= 10
|
|
91
|
+
)
|
|
92
|
+
);
|
|
30
93
|
assert.ok(Array.isArray(payload.interactionRules));
|
|
31
94
|
|
|
32
95
|
assert.throws(
|