@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.
@@ -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
- const res = await ctx.client.call("documents.answerQuestion", body, {
391
- maxAttempts: toInteger(args.maxAttempts, 2),
392
- });
393
- const payload = maybeDropPolicies(res.body, !!args.includePolicies);
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
- return {
396
- tool: "documents.answer",
397
- profile: ctx.profile.id,
398
- result:
399
- payload && typeof payload === "object"
400
- ? { question, ...payload }
401
- : { question, data: payload },
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.length > contextChars ? `${context.slice(0, contextChars)}...` : 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.length > excerptChars ? `${row.text.slice(0, excerptChars)}...` : 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
- "Set parentDocumentId=null to list only collection root pages.",
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 def = TOOLS[name];
1030
- if (!def) {
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 tool = TOOLS[name];
1045
- if (!tool) {
1046
- throw new CliError(`Unknown tool: ${name}`);
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
- validateToolArgs(name, args);
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
- const result = await tool.handler(ctx, args);
1052
- if (args.compact ?? true) {
1053
- return compactValue(result) || {};
1308
+ if (normalizedArgs.compact ?? true) {
1309
+ return compactValue(enriched) || {};
1054
1310
  }
1055
- return result;
1311
+ return enriched;
1056
1312
  }
@@ -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.length > excerptChars ? `${doc.text.slice(0, excerptChars)}...` : doc.text;
32
+ summary.excerpt = summarizeSafeText(doc.text, excerptChars);
32
33
  }
33
34
 
34
35
  return summary;
@@ -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.length > excerptChars ? `${row.text.slice(0, excerptChars)}...` : 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.length > excerptChars ? `${row.text.slice(0, excerptChars)}...` : 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 === "npm i -g @khanglvm/outline-cli"));
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(