@khanglvm/outline-cli 0.1.3 → 0.1.5

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/src/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import path from "node:path";
3
+ import { createRequire } from "node:module";
3
4
  import { getAgentSkillHelp, getQuickStartAgentHelp, listHelpSections } from "./agent-skills.js";
4
5
  import {
5
6
  buildProfile,
@@ -21,9 +22,12 @@ import {
21
22
  removeProfileFromKeychain,
22
23
  secureProfileForStorage,
23
24
  } from "./secure-keyring.js";
24
- import { getToolContract, invokeTool, listTools } from "./tools.js";
25
+ import { getToolContract, invokeTool, listTools, resolveToolInvocation } from "./tools.js";
25
26
  import { mapLimit, parseJsonArg, parseCsv, toInteger } from "./utils.js";
26
27
 
28
+ const require = createRequire(import.meta.url);
29
+ const { version: packageVersion } = require("../package.json");
30
+
27
31
  function configureSharedOutputOptions(command) {
28
32
  return command
29
33
  .option("--config <path>", "Config file path", defaultConfigPath())
@@ -44,23 +48,6 @@ function buildStoreFromOptions(opts) {
44
48
  });
45
49
  }
46
50
 
47
- async function getRuntime(opts, overrideProfileId) {
48
- const configPath = path.resolve(opts.config || defaultConfigPath());
49
- const config = await loadConfig(configPath);
50
- const selectedProfile = getProfile(config, overrideProfileId || opts.profile);
51
- const profile = hydrateProfileFromKeychain({
52
- configPath,
53
- profile: selectedProfile,
54
- });
55
- const client = new OutlineClient(profile);
56
- return {
57
- configPath,
58
- config,
59
- profile,
60
- client,
61
- };
62
- }
63
-
64
51
  function parseHeaders(input) {
65
52
  if (!input) {
66
53
  return {};
@@ -79,6 +66,130 @@ function parseHeaders(input) {
79
66
  return headers;
80
67
  }
81
68
 
69
+ function isBrokenPipeError(err) {
70
+ return err?.code === "EPIPE" || err?.errno === -32;
71
+ }
72
+
73
+ function safeWrite(stream, content, exitCode = 0) {
74
+ try {
75
+ return stream.write(content);
76
+ } catch (err) {
77
+ if (isBrokenPipeError(err)) {
78
+ process.exit(exitCode);
79
+ }
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ function installBrokenPipeGuards() {
85
+ const handlePipeError = (exitCode) => (err) => {
86
+ if (isBrokenPipeError(err)) {
87
+ process.exit(exitCode);
88
+ return;
89
+ }
90
+ throw err;
91
+ };
92
+
93
+ process.stdout.on("error", handlePipeError(0));
94
+ process.stderr.on("error", handlePipeError(process.exitCode || 1));
95
+ }
96
+
97
+ const PROFILE_ROUTING_ARG_KEYS = [
98
+ "query",
99
+ "queries",
100
+ "question",
101
+ "questions",
102
+ "title",
103
+ "titles",
104
+ "description",
105
+ "name",
106
+ "keywords",
107
+ "collectionId",
108
+ "documentId",
109
+ "parentDocumentId",
110
+ "id",
111
+ "ids",
112
+ "url",
113
+ "shareId",
114
+ "email",
115
+ ];
116
+
117
+ function collectRoutingHints(value, bucket, depth = 0) {
118
+ if (value === undefined || value === null || depth > 2) {
119
+ return;
120
+ }
121
+ if (typeof value === "string") {
122
+ const trimmed = value.trim();
123
+ if (trimmed) {
124
+ bucket.push(normalizeUrlHint(trimmed) || trimmed);
125
+ }
126
+ return;
127
+ }
128
+ if (typeof value === "number" || typeof value === "boolean") {
129
+ bucket.push(String(value));
130
+ return;
131
+ }
132
+ if (Array.isArray(value)) {
133
+ for (const item of value.slice(0, 8)) {
134
+ collectRoutingHints(item, bucket, depth + 1);
135
+ }
136
+ return;
137
+ }
138
+ if (typeof value === "object") {
139
+ for (const [key, nested] of Object.entries(value).slice(0, 8)) {
140
+ if (PROFILE_ROUTING_ARG_KEYS.includes(key)) {
141
+ collectRoutingHints(nested, bucket, depth + 1);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ function buildProfileRoutingQuery(context = {}) {
148
+ const bucket = [];
149
+ const tool = String(context.tool || "").trim();
150
+ if (tool) {
151
+ bucket.push(tool.replace(/[._]+/g, " "));
152
+ }
153
+ const args = context.args && typeof context.args === "object" ? context.args : {};
154
+ for (const key of PROFILE_ROUTING_ARG_KEYS) {
155
+ if (key in args) {
156
+ collectRoutingHints(args[key], bucket);
157
+ }
158
+ }
159
+ return [...new Set(bucket.map((item) => String(item || "").trim()).filter(Boolean))].join(" ");
160
+ }
161
+
162
+ function isLikelyReadOnlyToolName(tool, args = {}) {
163
+ if (args?.performAction === true) {
164
+ return false;
165
+ }
166
+ const name = String(tool || "").toLowerCase();
167
+ return !/(^|\.)(create|update|delete|remove|restore|duplicate|revoke|invite|suspend|activate|import|apply|rotate|cleanup|templatize|add_|remove_|permanent_delete|empty_trash|batch_update|safe_update)/.test(name);
168
+ }
169
+
170
+ async function getRuntime(opts, overrideProfileId, context = {}) {
171
+ const configPath = path.resolve(opts.config || defaultConfigPath());
172
+ const config = await loadConfig(configPath);
173
+ const selectedProfile = getProfile(config, overrideProfileId || opts.profile, {
174
+ query: buildProfileRoutingQuery(context),
175
+ allowAutoSelect: isLikelyReadOnlyToolName(context.tool, context.args),
176
+ suggestionLimit: 3,
177
+ });
178
+ const { selection: profileSelection, ...storedProfile } = selectedProfile;
179
+ const profile = hydrateProfileFromKeychain({
180
+ configPath,
181
+ profile: storedProfile,
182
+ });
183
+ const client = new OutlineClient(profile);
184
+ return {
185
+ configPath,
186
+ config,
187
+ profile,
188
+ client,
189
+ profileSelection,
190
+ };
191
+ }
192
+
82
193
  const URL_HINT_PATH_MARKERS = new Set(["doc", "d", "share", "s"]);
83
194
 
84
195
  function normalizeUrlHint(value) {
@@ -144,7 +255,7 @@ function formatError(err) {
144
255
  }
145
256
 
146
257
  function writeNdjsonLine(value) {
147
- process.stdout.write(`${JSON.stringify(value)}\n`);
258
+ safeWrite(process.stdout, `${JSON.stringify(value)}\n`);
148
259
  }
149
260
 
150
261
  function emitNdjson(payload) {
@@ -233,11 +344,12 @@ async function emitOutput(store, payload, opts, emitOptions = {}) {
233
344
  }
234
345
 
235
346
  export async function run(argv = process.argv) {
347
+ installBrokenPipeGuards();
236
348
  const program = new Command();
237
349
  program
238
350
  .name("outline-cli")
239
351
  .description("Agent-optimized CLI for Outline API")
240
- .version("0.1.0")
352
+ .version(packageVersion)
241
353
  .showHelpAfterError(true);
242
354
 
243
355
  const profile = program.command("profile").description("Manage Outline profiles");
@@ -353,6 +465,11 @@ export async function run(argv = process.argv) {
353
465
  .command("list")
354
466
  .description("List configured profiles")
355
467
  .option("--config <path>", "Config file path", defaultConfigPath())
468
+ .option("--output <format>", "Output format: json|ndjson", "json")
469
+ .option("--result-mode <mode>", "Result mode: auto|inline|file", "auto")
470
+ .option("--inline-max-bytes <n>", "Max inline JSON payload size", "12000")
471
+ .option("--tmp-dir <path>", "Directory for large result files")
472
+ .option("--pretty", "Pretty-print JSON output", false)
356
473
  .action(async (opts) => {
357
474
  const configPath = path.resolve(opts.config || defaultConfigPath());
358
475
  const config = await loadConfig(configPath);
@@ -360,16 +477,13 @@ export async function run(argv = process.argv) {
360
477
  ...redactProfile(item),
361
478
  isDefault: config.defaultProfile === item.id,
362
479
  }));
363
- const store = new ResultStore({ pretty: true });
364
- await store.emit(
365
- {
366
- ok: true,
367
- configPath,
368
- defaultProfile: config.defaultProfile,
369
- profiles,
370
- },
371
- { mode: "inline", pretty: true, label: "profile-list" }
372
- );
480
+ const store = buildStoreFromOptions(opts);
481
+ await emitOutput(store, {
482
+ ok: true,
483
+ configPath,
484
+ defaultProfile: config.defaultProfile,
485
+ profiles,
486
+ }, opts, { mode: opts.resultMode, label: "profile-list", pretty: !!opts.pretty });
373
487
  });
374
488
 
375
489
  profile
@@ -377,40 +491,44 @@ export async function run(argv = process.argv) {
377
491
  .description("Suggest best-matching profile(s) by id/name/base-url/description/keywords")
378
492
  .option("--config <path>", "Config file path", defaultConfigPath())
379
493
  .option("--limit <n>", "Max number of profile matches to return", "5")
494
+ .option("--output <format>", "Output format: json|ndjson", "json")
495
+ .option("--result-mode <mode>", "Result mode: auto|inline|file", "auto")
496
+ .option("--inline-max-bytes <n>", "Max inline JSON payload size", "12000")
497
+ .option("--tmp-dir <path>", "Directory for large result files")
498
+ .option("--pretty", "Pretty-print JSON output", false)
380
499
  .action(async (query, opts) => {
381
500
  const configPath = path.resolve(opts.config || defaultConfigPath());
382
501
  const config = await loadConfig(configPath);
383
502
  const result = suggestProfiles(config, query, { limit: toInteger(opts.limit, 5) });
384
- const store = new ResultStore({ pretty: true });
385
- await store.emit(
386
- {
387
- ok: true,
388
- configPath,
389
- defaultProfile: config.defaultProfile,
390
- ...result,
391
- bestMatch: result.matches[0] || null,
392
- },
393
- { mode: "inline", pretty: true, label: "profile-suggest" }
394
- );
503
+ const store = buildStoreFromOptions(opts);
504
+ await emitOutput(store, {
505
+ ok: true,
506
+ configPath,
507
+ defaultProfile: config.defaultProfile,
508
+ ...result,
509
+ bestMatch: result.matches[0] || null,
510
+ }, opts, { mode: opts.resultMode, label: "profile-suggest", pretty: !!opts.pretty });
395
511
  });
396
512
 
397
513
  profile
398
514
  .command("show [id]")
399
515
  .description("Show one profile (redacted)")
400
516
  .option("--config <path>", "Config file path", defaultConfigPath())
517
+ .option("--output <format>", "Output format: json|ndjson", "json")
518
+ .option("--result-mode <mode>", "Result mode: auto|inline|file", "auto")
519
+ .option("--inline-max-bytes <n>", "Max inline JSON payload size", "12000")
520
+ .option("--tmp-dir <path>", "Directory for large result files")
521
+ .option("--pretty", "Pretty-print JSON output", false)
401
522
  .action(async (id, opts) => {
402
523
  const configPath = path.resolve(opts.config || defaultConfigPath());
403
524
  const config = await loadConfig(configPath);
404
525
  const profileData = getProfile(config, id);
405
- const store = new ResultStore({ pretty: true });
406
- await store.emit(
407
- {
408
- ok: true,
409
- configPath,
410
- profile: redactProfile(profileData),
411
- },
412
- { mode: "inline", pretty: true, label: "profile-show" }
413
- );
526
+ const store = buildStoreFromOptions(opts);
527
+ await emitOutput(store, {
528
+ ok: true,
529
+ configPath,
530
+ profile: redactProfile(profileData),
531
+ }, opts, { mode: opts.resultMode, label: "profile-show", pretty: !!opts.pretty });
414
532
  });
415
533
 
416
534
  profile
@@ -726,6 +844,7 @@ export async function run(argv = process.argv) {
726
844
  tools
727
845
  .command("contract [name]")
728
846
  .description("Show tool contract (signature, usage, best practices)")
847
+ .option("--pretty", "Pretty-print JSON output", false)
729
848
  .action(async (name, opts, cmd) => {
730
849
  const merged = { ...cmd.parent.opts(), ...opts };
731
850
  const store = buildStoreFromOptions(merged);
@@ -826,11 +945,21 @@ export async function run(argv = process.argv) {
826
945
  );
827
946
 
828
947
  invoke.action(async (tool, opts) => {
829
- const runtime = await getRuntime(opts);
830
- const store = buildStoreFromOptions(opts);
831
948
  const args = (await parseJsonArg({ json: opts.args, file: opts.argsFile, name: "args" })) || {};
949
+ const resolution = resolveToolInvocation(tool, args);
950
+ const runtime = await getRuntime(opts, undefined, {
951
+ tool: resolution.resolvedName,
952
+ args: resolution.args,
953
+ });
954
+ const store = buildStoreFromOptions(opts);
832
955
  const result = await invokeTool(runtime, tool, args);
833
- await emitOutput(store, result, opts, {
956
+ const output = runtime.profileSelection?.autoSelected
957
+ ? {
958
+ ...result,
959
+ profileRouting: runtime.profileSelection,
960
+ }
961
+ : result;
962
+ await emitOutput(store, output, opts, {
834
963
  label: `tool-${tool.replace(/\./g, "-")}`,
835
964
  mode: opts.resultMode,
836
965
  });
@@ -858,19 +987,25 @@ export async function run(argv = process.argv) {
858
987
  const store = buildStoreFromOptions(opts);
859
988
  const clientCache = new Map();
860
989
 
861
- async function runtimeForProfile(profileId) {
862
- const selected = getProfile(config, profileId || opts.profile);
863
- if (!clientCache.has(selected.id)) {
990
+ async function runtimeForProfile(profileId, context = {}) {
991
+ const selected = getProfile(config, profileId || opts.profile, {
992
+ query: buildProfileRoutingQuery(context),
993
+ allowAutoSelect: isLikelyReadOnlyToolName(context.tool, context.args),
994
+ suggestionLimit: 3,
995
+ });
996
+ const { selection: profileSelection, ...storedProfile } = selected;
997
+ if (!clientCache.has(storedProfile.id)) {
864
998
  const hydrated = hydrateProfileFromKeychain({
865
999
  configPath,
866
- profile: selected,
1000
+ profile: storedProfile,
867
1001
  });
868
- clientCache.set(selected.id, {
1002
+ clientCache.set(storedProfile.id, {
869
1003
  profile: hydrated,
870
1004
  client: new OutlineClient(hydrated),
1005
+ profileSelection,
871
1006
  });
872
1007
  }
873
- return clientCache.get(selected.id);
1008
+ return clientCache.get(storedProfile.id);
874
1009
  }
875
1010
 
876
1011
  const parallel = toInteger(opts.parallel, 4);
@@ -882,7 +1017,11 @@ export async function run(argv = process.argv) {
882
1017
  if (!operation.tool) {
883
1018
  throw new CliError(`Operation at index ${index} is missing tool`);
884
1019
  }
885
- const runtime = await runtimeForProfile(operation.profile);
1020
+ const resolution = resolveToolInvocation(operation.tool, operation.args || {});
1021
+ const runtime = await runtimeForProfile(operation.profile, {
1022
+ tool: resolution.resolvedName,
1023
+ args: resolution.args,
1024
+ });
886
1025
  const payload = await invokeTool(runtime, operation.tool, operation.args || {});
887
1026
  const mode = (opts.itemEnvelope || "compact").toLowerCase();
888
1027
  const compactResult =
@@ -898,9 +1037,11 @@ export async function run(argv = process.argv) {
898
1037
  return {
899
1038
  index,
900
1039
  tool: operation.tool,
1040
+ ...(payload?.tool && payload.tool !== operation.tool ? { resolvedTool: payload.tool } : {}),
901
1041
  profile: runtime.profile.id,
902
1042
  ok: true,
903
1043
  result: mode === "full" ? payload : compactResult,
1044
+ ...(runtime.profileSelection?.autoSelected ? { profileRouting: runtime.profileSelection } : {}),
904
1045
  ...(mode === "full" || Object.keys(compactMeta).length === 0 ? {} : { meta: compactMeta }),
905
1046
  };
906
1047
  } catch (err) {
@@ -948,7 +1089,7 @@ export async function run(argv = process.argv) {
948
1089
  const merged = { ...cmd.parent.opts(), ...opts };
949
1090
  const store = buildStoreFromOptions(merged);
950
1091
  const content = await store.read(file);
951
- process.stdout.write(content.content);
1092
+ safeWrite(process.stdout, content.content);
952
1093
  });
953
1094
 
954
1095
  tmp
@@ -976,7 +1117,7 @@ export async function run(argv = process.argv) {
976
1117
  await program.parseAsync(argv);
977
1118
  } catch (err) {
978
1119
  const output = formatError(err);
979
- process.stderr.write(`${JSON.stringify(output, null, 2)}\n`);
1120
+ safeWrite(process.stderr, `${JSON.stringify(output, null, 2)}\n`, process.exitCode || 1);
980
1121
  process.exitCode = process.exitCode || 1;
981
1122
  }
982
1123
  }
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import fsSync from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { CliError } from "./errors.js";
5
6
 
6
7
  export const CONFIG_VERSION = 1;
7
8
 
@@ -479,6 +480,45 @@ export function suggestProfiles(config, query, options = {}) {
479
480
  };
480
481
  }
481
482
 
483
+ function isHighConfidenceProfileMatch(top, second, options = {}) {
484
+ if (!top) {
485
+ return false;
486
+ }
487
+
488
+ const minScore = Number.isFinite(Number(options.minScore)) ? Number(options.minScore) : 1.7;
489
+ const minGap = Number.isFinite(Number(options.minGap)) ? Number(options.minGap) : 0.55;
490
+ const exactSignals = new Set(["id_exact", "name_exact", "keyword_exact", "host"]);
491
+ const hasExactSignal = Array.isArray(top.matchedOn) && top.matchedOn.some((item) => exactSignals.has(item));
492
+ const gap = top.score - Number(second?.score || 0);
493
+
494
+ if (top.score >= 3.2) {
495
+ return true;
496
+ }
497
+ if (top.score >= minScore && gap >= minGap) {
498
+ return true;
499
+ }
500
+ if (hasExactSignal && top.score >= 1.2 && gap >= 0.35) {
501
+ return true;
502
+ }
503
+
504
+ return false;
505
+ }
506
+
507
+ function formatProfileSelectionError(config, message, options = {}) {
508
+ const availableProfiles = Object.keys(config?.profiles || {});
509
+ const query = String(options.query || "").trim();
510
+ const suggestionLimit = Number.isFinite(Number(options.suggestionLimit))
511
+ ? Math.max(1, Number(options.suggestionLimit))
512
+ : 3;
513
+ const suggestions = query ? suggestProfiles(config, query, { limit: suggestionLimit }).matches : [];
514
+
515
+ return new CliError(message, {
516
+ code: "PROFILE_SELECTION_REQUIRED",
517
+ availableProfiles,
518
+ ...(query ? { query, suggestions } : {}),
519
+ });
520
+ }
521
+
482
522
  export function suggestProfileMetadata(input = {}, options = {}) {
483
523
  const id = String(input.id || "").trim();
484
524
  const name = String(input.name || id || "").trim();
@@ -566,13 +606,17 @@ export function listProfiles(config) {
566
606
  }));
567
607
  }
568
608
 
569
- export function getProfile(config, explicitId) {
609
+ export function getProfile(config, explicitId, options = {}) {
570
610
  const profiles = config?.profiles || {};
571
611
 
572
612
  if (explicitId) {
573
613
  const profile = profiles[explicitId];
574
614
  if (!profile) {
575
- throw new Error(`Profile not found: ${explicitId}`);
615
+ throw new CliError(`Profile not found: ${explicitId}`, {
616
+ code: "PROFILE_NOT_FOUND",
617
+ profileId: explicitId,
618
+ availableProfiles: Object.keys(profiles),
619
+ });
576
620
  }
577
621
  return {
578
622
  id: explicitId,
@@ -583,7 +627,11 @@ export function getProfile(config, explicitId) {
583
627
  if (config.defaultProfile) {
584
628
  const profile = profiles[config.defaultProfile];
585
629
  if (!profile) {
586
- throw new Error(`Profile not found: ${config.defaultProfile}`);
630
+ throw new CliError(`Profile not found: ${config.defaultProfile}`, {
631
+ code: "PROFILE_NOT_FOUND",
632
+ profileId: config.defaultProfile,
633
+ availableProfiles: Object.keys(profiles),
634
+ });
587
635
  }
588
636
  return {
589
637
  id: config.defaultProfile,
@@ -601,12 +649,44 @@ export function getProfile(config, explicitId) {
601
649
  }
602
650
 
603
651
  if (profileIds.length > 1) {
604
- throw new Error(
605
- "Profile selection required: multiple profiles are saved and no default profile is set. Use --profile <id> or `outline-cli profile use <id>`."
652
+ const query = String(options.query || "").trim();
653
+ const allowAutoSelect = options.allowAutoSelect !== false;
654
+ const suggestionLimit = Number.isFinite(Number(options.suggestionLimit))
655
+ ? Math.max(1, Number(options.suggestionLimit))
656
+ : 3;
657
+
658
+ if (allowAutoSelect && query) {
659
+ const suggestions = suggestProfiles(config, query, { limit: suggestionLimit }).matches;
660
+ const top = suggestions[0];
661
+ const second = suggestions[1];
662
+
663
+ if (isHighConfidenceProfileMatch(top, second, options)) {
664
+ return {
665
+ id: top.id,
666
+ ...profiles[top.id],
667
+ selection: {
668
+ autoSelected: true,
669
+ query,
670
+ score: top.score,
671
+ matchedOn: top.matchedOn,
672
+ },
673
+ };
674
+ }
675
+ }
676
+
677
+ throw formatProfileSelectionError(
678
+ config,
679
+ "Profile selection required: multiple profiles are saved and no default profile is set. Use --profile <id> or `outline-cli profile use <id>`.",
680
+ {
681
+ query,
682
+ suggestionLimit,
683
+ }
606
684
  );
607
685
  }
608
686
 
609
- throw new Error("No profiles configured. Use `outline-cli profile add <id> ...` first.");
687
+ throw new CliError("No profiles configured. Use `outline-cli profile add <id> ...` first.", {
688
+ code: "PROFILE_NOT_CONFIGURED",
689
+ });
610
690
  }
611
691
 
612
692
  export function redactProfile(profile) {
@@ -3,8 +3,8 @@ export const ENTRY_INTEGRITY_MANIFEST = Object.freeze({
3
3
  version: 1,
4
4
  algorithm: "sha256",
5
5
  signatureAlgorithm: "sha256-salted-manifest-v1",
6
- signature: "03f876687f3972f11cce73da3da5f5f5266536bdce151a03326a813082240016",
7
- generatedAt: "2026-03-06T04:21:12.443Z",
6
+ signature: "c69d41948b78c8ae35c0223377149ece8df5588863a010adf92a0b6ad11876d3",
7
+ generatedAt: "2026-03-08T01:17:47.858Z",
8
8
  files: [
9
9
  {
10
10
  "path": "src/action-gate.js",
@@ -12,19 +12,19 @@ export const ENTRY_INTEGRITY_MANIFEST = Object.freeze({
12
12
  },
13
13
  {
14
14
  "path": "src/agent-skills.js",
15
- "sha256": "12d54830ec84d9e8815cd28915070453175520416c83983de2496c1734acd7a2"
15
+ "sha256": "d40e94a4b61922d3ee8a43ed89d5e9ce1562a2cdffd37e2f396c2a18b9a481fc"
16
16
  },
17
17
  {
18
18
  "path": "src/cli.js",
19
- "sha256": "7959970b8e2d0d81de0c2e35201274f0d16744aaa2a73f001decfdad45c85522"
19
+ "sha256": "2b6b17a71c15954c2663397d57afde2b8825ed368bb7f71833f6191662eff079"
20
20
  },
21
21
  {
22
22
  "path": "src/config-store.js",
23
- "sha256": "e0d62083c694f84895a93752738accbbd98830157227f06380ccdf8e421508fd"
23
+ "sha256": "3773d59d74502c75b7375c194b4f0e2c39fb43e95f739e6e4878aa1b0957a511"
24
24
  },
25
25
  {
26
26
  "path": "src/entry-integrity.js",
27
- "sha256": "3463fc3a3c6d0aead8e889ecce7c323b64b09ef54c772c52ff8b7266eb8c0d69"
27
+ "sha256": "5f66c29e57f53f11fc1751c9a8bd41c318001e3fb6562ee6f3a593f31849e45e"
28
28
  },
29
29
  {
30
30
  "path": "src/errors.js",
@@ -42,25 +42,29 @@ export const ENTRY_INTEGRITY_MANIFEST = Object.freeze({
42
42
  "path": "src/secure-keyring.js",
43
43
  "sha256": "370d904265733f7d6b0f47b3213c7f026c95e59bc18d364dff96be90dbd7f479"
44
44
  },
45
+ {
46
+ "path": "src/summary-redaction.js",
47
+ "sha256": "ddd3562334e704de5d9f6068726e311a98564e7e46bcdb56d9aade7e63b65bd3"
48
+ },
45
49
  {
46
50
  "path": "src/tool-arg-schemas.js",
47
- "sha256": "de6f389349d6688296a8e66d2d612937f31b4fc2a0efdca085d36ec5b09cab26"
51
+ "sha256": "51f8727255e5b2b0a4cce78495cfaed9bb1869a7ef104b22dbc170604f878feb"
48
52
  },
49
53
  {
50
54
  "path": "src/tools.extended.js",
51
- "sha256": "8f29601301c7942cce8884c689916a78a22231e5f6691ecebfe09d9d4f87d6c3"
55
+ "sha256": "0c5317ca0b28a0b7110d9e6c81d8ca6ccd705e9b303de2f29e03f847b31df310"
52
56
  },
53
57
  {
54
58
  "path": "src/tools.js",
55
- "sha256": "bfcb36045abecc758237ce9441d7e5994a4467893b09d48557749a90de424401"
59
+ "sha256": "96cfbb14cac2baa5221dc4e9294a0520f81c3e22a0c69ee648cd6c2354e1512e"
56
60
  },
57
61
  {
58
62
  "path": "src/tools.mutation.js",
59
- "sha256": "013cee7d9815f868cca8990af39abe385239fa0f324054477d681e479605e903"
63
+ "sha256": "15fa3d146d99f55bc16d814ffd9360127b99eca2f9271cf751c6e5c7e4234317"
60
64
  },
61
65
  {
62
66
  "path": "src/tools.navigation.js",
63
- "sha256": "8cd6ad37db2ef4d8a2ddd2eea408c8ab50b5649b620680cef07bd8f3ecea7e89"
67
+ "sha256": "f7118e91c2edd0e6b57591d41fe7707c68eef51cde77918495bb4ee166139339"
64
68
  },
65
69
  {
66
70
  "path": "src/tools.platform.js",
@@ -52,6 +52,7 @@ export async function assertEntryIntegrity(rootDir = REPO_ROOT) {
52
52
  if (files.length === 0) {
53
53
  throw new CliError("Entry integrity manifest is empty", {
54
54
  code: "ENTRY_INTEGRITY_MANIFEST_EMPTY",
55
+ hint: "Run `npm run integrity:refresh` to regenerate local integrity metadata.",
55
56
  });
56
57
  }
57
58
 
@@ -62,6 +63,7 @@ export async function assertEntryIntegrity(rootDir = REPO_ROOT) {
62
63
  expected: manifest.signature,
63
64
  actual: computedSignature,
64
65
  keyId: ENTRY_INTEGRITY_BINDING.keyId,
66
+ hint: "Run `npm run integrity:refresh` after local source edits, or set `OUTLINE_CLI_SKIP_INTEGRITY_CHECK=1` for local smoke runs.",
65
67
  });
66
68
  }
67
69
 
@@ -93,6 +95,7 @@ export async function assertEntryIntegrity(rootDir = REPO_ROOT) {
93
95
  code: "ENTRY_SUBMODULE_INTEGRITY_FAILED",
94
96
  mismatchCount: mismatches.length,
95
97
  mismatches,
98
+ hint: "Run `npm run integrity:refresh` after local source edits, or set `OUTLINE_CLI_SKIP_INTEGRITY_CHECK=1` for local smoke runs.",
96
99
  });
97
100
  }
98
101
 
@@ -0,0 +1,37 @@
1
+ const SUMMARY_SECRET_LINE_PATTERNS = [
2
+ /(^|\n)([ \t>*+\-]*(?:[*_`~]+\s*)*(?:user(?:name)?\s*\/\s*pass(?:word)?|username\s*\/\s*password|credentials?|api[ _-]*key|access[ _-]*token|refresh[ _-]*token|client[ _-]*secret|secret|password|pass|pwd|authorization|bearer)(?:\s*[*_`~]+)*\s*:\s*)([^\n]+)/gi,
3
+ /(^|\n)([ \t>*+\-]*(?:[*_`~]+\s*)*(?:user(?:name)?|email|login)(?:\s*[*_`~]+)*\s*:\s*)([^\n]+)(\n[ \t>*+\-]*(?:[*_`~]+\s*)*(?:pass(?:word)?|pwd)(?:\s*[*_`~]+)*\s*:\s*)([^\n]+)/gi,
4
+ ];
5
+
6
+ export function redactSensitiveSummaryText(text) {
7
+ if (typeof text !== "string" || text.length === 0) {
8
+ return text;
9
+ }
10
+
11
+ let redacted = text;
12
+ for (const pattern of SUMMARY_SECRET_LINE_PATTERNS) {
13
+ redacted = redacted.replace(pattern, (...parts) => {
14
+ const leading = parts[1] || "";
15
+ const prefix = parts[2] || "";
16
+ const secondPrefix = parts[4];
17
+ if (typeof secondPrefix === "string") {
18
+ return `${leading}${prefix}[REDACTED]${secondPrefix}[REDACTED]`;
19
+ }
20
+ return `${leading}${prefix}[REDACTED]`;
21
+ });
22
+ }
23
+
24
+ redacted = redacted.replace(/\bol_api_[A-Za-z0-9]+\b/g, "ol_api_[REDACTED]");
25
+ redacted = redacted.replace(/\b(?:ghp|gho|ghu|ghs|github_pat|sk|pk)_[A-Za-z0-9_]+\b/g, "[REDACTED_TOKEN]");
26
+ redacted = redacted.replace(/(https?:\/\/)([^\s:@/]+):([^\s@/]+)@/gi, "$1[REDACTED]@");
27
+
28
+ return redacted;
29
+ }
30
+
31
+ export function summarizeSafeText(text, maxChars) {
32
+ const redacted = redactSensitiveSummaryText(text);
33
+ if (typeof redacted !== "string") {
34
+ return redacted;
35
+ }
36
+ return redacted.length > maxChars ? `${redacted.slice(0, maxChars)}...` : redacted;
37
+ }