@khanglvm/outline-cli 0.1.3 → 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 +5 -0
- package/README.md +34 -13
- package/bin/outline-cli.js +14 -0
- package/docs/TOOL_CONTRACTS.md +8 -0
- package/package.json +1 -1
- package/src/agent-skills.js +15 -15
- package/src/cli.js +203 -62
- 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 +2 -2
- 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.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)) {
|
|
@@ -54,7 +54,7 @@ test("getQuickStartAgentHelp returns full payload and validates view", () => {
|
|
|
54
54
|
assert.equal(payload.section, "quick-start-agent");
|
|
55
55
|
assert.equal(payload.view, "full");
|
|
56
56
|
assert.ok(Array.isArray(payload.steps));
|
|
57
|
-
assert.ok(payload.steps.some((row) => row.command === "outline-cli profile list
|
|
57
|
+
assert.ok(payload.steps.some((row) => row.command === "outline-cli profile list"));
|
|
58
58
|
assert.ok(
|
|
59
59
|
payload.steps.some(
|
|
60
60
|
(row) =>
|
|
@@ -77,7 +77,7 @@ test("getQuickStartAgentHelp returns full payload and validates view", () => {
|
|
|
77
77
|
assert.ok(
|
|
78
78
|
payload.steps.some(
|
|
79
79
|
(row) =>
|
|
80
|
-
row.apiKeySettingsUrlTemplate === "<base-url>/settings/api" &&
|
|
80
|
+
row.apiKeySettingsUrlTemplate === "<base-url>/settings/api-and-apps" &&
|
|
81
81
|
Array.isArray(row.apiKeyConfigTemplate) &&
|
|
82
82
|
row.apiKeyConfigTemplate.length >= 3
|
|
83
83
|
)
|
|
@@ -2,6 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import {
|
|
4
4
|
buildProfile,
|
|
5
|
+
getProfile,
|
|
5
6
|
normalizeBaseUrlWithHints,
|
|
6
7
|
suggestProfileMetadata,
|
|
7
8
|
suggestProfiles,
|
|
@@ -70,6 +71,37 @@ test("suggestProfiles ranks profiles by keywords/description/host signals", () =
|
|
|
70
71
|
assert.ok(Array.isArray(result.matches[0].matchedOn));
|
|
71
72
|
});
|
|
72
73
|
|
|
74
|
+
test("getProfile can auto-select a strong read-only match when query context is provided", () => {
|
|
75
|
+
const config = {
|
|
76
|
+
version: 1,
|
|
77
|
+
profiles: {
|
|
78
|
+
engineering: {
|
|
79
|
+
name: "Engineering",
|
|
80
|
+
baseUrl: "https://wiki.example.com",
|
|
81
|
+
description: "Runbooks and incident policy",
|
|
82
|
+
keywords: ["incident", "runbook", "sre"],
|
|
83
|
+
auth: { type: "apiKey", apiKey: "ol_api_eng" },
|
|
84
|
+
},
|
|
85
|
+
marketing: {
|
|
86
|
+
name: "Acme Handbook",
|
|
87
|
+
baseUrl: "https://handbook.acme.example",
|
|
88
|
+
description: "Marketing campaign and event tracking handbook",
|
|
89
|
+
keywords: ["tracking", "campaign", "analytics"],
|
|
90
|
+
auth: { type: "apiKey", apiKey: "ol_api_marketing" },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const selected = getProfile(config, undefined, {
|
|
96
|
+
query: "documents search incident runbook sre",
|
|
97
|
+
allowAutoSelect: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(selected.id, "engineering");
|
|
101
|
+
assert.equal(selected.selection?.autoSelected, true);
|
|
102
|
+
assert.equal(selected.selection?.query, "documents search incident runbook sre");
|
|
103
|
+
});
|
|
104
|
+
|
|
73
105
|
test("suggestProfileMetadata can generate and enrich metadata from hints", () => {
|
|
74
106
|
const next = suggestProfileMetadata({
|
|
75
107
|
id: "acme-handbook",
|
|
@@ -24,6 +24,26 @@ test("validateToolArgs rejects unknown args by default", () => {
|
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
test("validateToolArgs exposes accepted args and closest suggestions", () => {
|
|
28
|
+
assert.throws(
|
|
29
|
+
() => validateToolArgs("auth.info", { vew: "summary", unexpected: true }),
|
|
30
|
+
(err) => {
|
|
31
|
+
assert.ok(err instanceof CliError);
|
|
32
|
+
assert.equal(err.details?.code, "ARG_VALIDATION_FAILED");
|
|
33
|
+
assert.deepEqual(err.details?.requiredArgs, []);
|
|
34
|
+
assert.ok(Array.isArray(err.details?.acceptedArgs));
|
|
35
|
+
assert.ok(err.details.acceptedArgs.includes("view"));
|
|
36
|
+
assert.ok(err.details.acceptedArgs.includes("compact"));
|
|
37
|
+
assert.deepEqual(err.details?.unknownArgs, ["vew", "unexpected"]);
|
|
38
|
+
const typoIssue = err.details?.issues?.find((issue) => issue.path === "args.vew");
|
|
39
|
+
assert.deepEqual(typoIssue?.suggestions, ["view"]);
|
|
40
|
+
assert.equal(err.details?.suggestedArgs, undefined);
|
|
41
|
+
assert.match(err.details?.validationHint || "", /Accepted args:/);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
27
47
|
test("validateToolArgs supports allowUnknown opt-out", () => {
|
|
28
48
|
const toolName = "__test.allow_unknown";
|
|
29
49
|
TOOL_ARG_SCHEMAS[toolName] = {
|
|
@@ -1651,8 +1671,13 @@ test("shares lifecycle schemas enforce deterministic selectors and update requir
|
|
|
1651
1671
|
}
|
|
1652
1672
|
);
|
|
1653
1673
|
|
|
1674
|
+
assert.equal(
|
|
1675
|
+
validateToolArgs("shares.update", { id: "share-1", published: "true" }).published,
|
|
1676
|
+
true
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1654
1679
|
assert.throws(
|
|
1655
|
-
() => validateToolArgs("shares.update", { id: "share-1", published: "
|
|
1680
|
+
() => validateToolArgs("shares.update", { id: "share-1", published: "yes" }),
|
|
1656
1681
|
(err) => {
|
|
1657
1682
|
assert.ok(err instanceof CliError);
|
|
1658
1683
|
assert.ok(err.details?.issues?.some((issue) => issue.path === "args.published"));
|
|
@@ -1789,11 +1789,6 @@ test("live integration suite (real Outline API, no mocks)", { timeout: 300_000 }
|
|
|
1789
1789
|
});
|
|
1790
1790
|
|
|
1791
1791
|
if (run.status !== 0) {
|
|
1792
|
-
if (isApiNotFoundErrorEnvelope(run.stderrJson)) {
|
|
1793
|
-
t.diagnostic(`documents.answer unsupported payload: ${run.stderr || "<empty stderr>"}`);
|
|
1794
|
-
t.skip("documents.answerQuestion endpoint unsupported by this Outline deployment");
|
|
1795
|
-
return;
|
|
1796
|
-
}
|
|
1797
1792
|
assert.fail(`documents.answer expected success, got status=${run.status}, stderr=${run.stderr || "<empty>"}`);
|
|
1798
1793
|
}
|
|
1799
1794
|
|
|
@@ -1805,11 +1800,17 @@ test("live integration suite (real Outline API, no mocks)", { timeout: 300_000 }
|
|
|
1805
1800
|
assert.equal(payload.result?.question, happyQuestion);
|
|
1806
1801
|
|
|
1807
1802
|
const signals = extractAnswerSignals(payload.result);
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1803
|
+
if (payload.result?.fallbackUsed) {
|
|
1804
|
+
assert.equal(payload.result?.unsupported, true);
|
|
1805
|
+
assert.ok(Array.isArray(signals.citations), "documents.answer fallback should expose citations/documents array");
|
|
1806
|
+
assert.ok(signals.citations.length > 0, `documents.answer fallback should include retrieved docs: ${JSON.stringify(payload.result)}`);
|
|
1807
|
+
} else {
|
|
1808
|
+
assert.ok(
|
|
1809
|
+
signals.answerText.length > 0,
|
|
1810
|
+
`documents.answer happy path should include answer text: ${JSON.stringify(payload.result)}`
|
|
1811
|
+
);
|
|
1812
|
+
assert.ok(Array.isArray(signals.citations), "documents.answer envelope should expose citations array");
|
|
1813
|
+
}
|
|
1813
1814
|
});
|
|
1814
1815
|
|
|
1815
1816
|
await t.test("documents.answer no-hit path assertions", async (t) => {
|
|
@@ -1826,11 +1827,6 @@ test("live integration suite (real Outline API, no mocks)", { timeout: 300_000 }
|
|
|
1826
1827
|
});
|
|
1827
1828
|
|
|
1828
1829
|
if (run.status !== 0) {
|
|
1829
|
-
if (isApiNotFoundErrorEnvelope(run.stderrJson)) {
|
|
1830
|
-
t.diagnostic(`documents.answer unsupported payload: ${run.stderr || "<empty stderr>"}`);
|
|
1831
|
-
t.skip("documents.answerQuestion endpoint unsupported by this Outline deployment");
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
1830
|
assert.fail(`documents.answer no-hit expected success, got status=${run.status}, stderr=${run.stderr || "<empty>"}`);
|
|
1835
1831
|
}
|
|
1836
1832
|
|
|
@@ -1840,10 +1836,15 @@ test("live integration suite (real Outline API, no mocks)", { timeout: 300_000 }
|
|
|
1840
1836
|
assert.equal(payload.result?.question, noHitQuestion);
|
|
1841
1837
|
|
|
1842
1838
|
const signals = extractAnswerSignals(payload.result);
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
`documents.answer no-hit should include
|
|
1846
|
-
|
|
1839
|
+
if (payload.result?.fallbackUsed) {
|
|
1840
|
+
assert.equal(payload.result?.unsupported, true);
|
|
1841
|
+
assert.ok(signals.noAnswerReason.length > 0, `documents.answer fallback no-hit should include reason: ${JSON.stringify(payload.result)}`);
|
|
1842
|
+
} else {
|
|
1843
|
+
assert.ok(
|
|
1844
|
+
signals.noAnswerReason.length > 0 || signals.citations.length === 0 || noHitPattern.test(signals.answerText),
|
|
1845
|
+
`documents.answer no-hit should include explicit no-hit signal: ${JSON.stringify(payload.result)}`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1847
1848
|
});
|
|
1848
1849
|
|
|
1849
1850
|
await t.test("documents.answer_batch mixed questions keep per-item isolation", async (t) => {
|
|
@@ -1865,11 +1866,6 @@ test("live integration suite (real Outline API, no mocks)", { timeout: 300_000 }
|
|
|
1865
1866
|
});
|
|
1866
1867
|
|
|
1867
1868
|
if (run.status !== 0) {
|
|
1868
|
-
if (isApiNotFoundErrorEnvelope(run.stderrJson)) {
|
|
1869
|
-
t.diagnostic(`documents.answer_batch unsupported payload: ${run.stderr || "<empty stderr>"}`);
|
|
1870
|
-
t.skip("documents.answerQuestion endpoint unsupported by this Outline deployment");
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
1869
|
assert.fail(`documents.answer_batch expected success, got status=${run.status}, stderr=${run.stderr || "<empty>"}`);
|
|
1874
1870
|
}
|
|
1875
1871
|
|
|
@@ -74,6 +74,17 @@ test("profile selection supports explicit, default, and single-profile fallback
|
|
|
74
74
|
]);
|
|
75
75
|
assert.equal(addAlpha.stdoutJson?.defaultProfile, null, "first add should not auto-set default");
|
|
76
76
|
|
|
77
|
+
const profileListJson = runCli([
|
|
78
|
+
"profile",
|
|
79
|
+
"list",
|
|
80
|
+
"--config",
|
|
81
|
+
configPath,
|
|
82
|
+
"--output",
|
|
83
|
+
"json",
|
|
84
|
+
]);
|
|
85
|
+
assert.equal(profileListJson.stdoutJson?.ok, true);
|
|
86
|
+
assert.equal(profileListJson.stdoutJson?.profiles?.length, 1);
|
|
87
|
+
|
|
77
88
|
const singleFallback = runCli([
|
|
78
89
|
"invoke",
|
|
79
90
|
"no.such.tool",
|
|
@@ -105,10 +116,9 @@ test("profile selection supports explicit, default, and single-profile fallback
|
|
|
105
116
|
"--args",
|
|
106
117
|
"{}",
|
|
107
118
|
], { expectCode: 1 });
|
|
108
|
-
assert.match(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
119
|
+
assert.match(ambiguous.stderrJson?.error?.message || "", /Unknown tool: no\.such\.tool/);
|
|
120
|
+
assert.equal(ambiguous.stderrJson?.error?.code, "UNKNOWN_TOOL");
|
|
121
|
+
assert.ok(Array.isArray(ambiguous.stderrJson?.error?.suggestions));
|
|
112
122
|
|
|
113
123
|
const explicitProfile = runCli([
|
|
114
124
|
"invoke",
|