@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/CHANGELOG.md +10 -0
- package/README.md +36 -16
- 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/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
|
|
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(
|
|
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 =
|
|
364
|
-
await store
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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 =
|
|
385
|
-
await store
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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 =
|
|
406
|
-
await store
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1000
|
+
profile: storedProfile,
|
|
867
1001
|
});
|
|
868
|
-
clientCache.set(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
1120
|
+
safeWrite(process.stderr, `${JSON.stringify(output, null, 2)}\n`, process.exitCode || 1);
|
|
980
1121
|
process.exitCode = process.exitCode || 1;
|
|
981
1122
|
}
|
|
982
1123
|
}
|
package/src/config-store.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
605
|
-
|
|
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
|
|
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: "
|
|
7
|
-
generatedAt: "2026-03-
|
|
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": "
|
|
15
|
+
"sha256": "d40e94a4b61922d3ee8a43ed89d5e9ce1562a2cdffd37e2f396c2a18b9a481fc"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": "src/cli.js",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "2b6b17a71c15954c2663397d57afde2b8825ed368bb7f71833f6191662eff079"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "src/config-store.js",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "3773d59d74502c75b7375c194b4f0e2c39fb43e95f739e6e4878aa1b0957a511"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "src/entry-integrity.js",
|
|
27
|
-
"sha256": "
|
|
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": "
|
|
51
|
+
"sha256": "51f8727255e5b2b0a4cce78495cfaed9bb1869a7ef104b22dbc170604f878feb"
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
54
|
"path": "src/tools.extended.js",
|
|
51
|
-
"sha256": "
|
|
55
|
+
"sha256": "0c5317ca0b28a0b7110d9e6c81d8ca6ccd705e9b303de2f29e03f847b31df310"
|
|
52
56
|
},
|
|
53
57
|
{
|
|
54
58
|
"path": "src/tools.js",
|
|
55
|
-
"sha256": "
|
|
59
|
+
"sha256": "96cfbb14cac2baa5221dc4e9294a0520f81c3e22a0c69ee648cd6c2354e1512e"
|
|
56
60
|
},
|
|
57
61
|
{
|
|
58
62
|
"path": "src/tools.mutation.js",
|
|
59
|
-
"sha256": "
|
|
63
|
+
"sha256": "15fa3d146d99f55bc16d814ffd9360127b99eca2f9271cf751c6e5c7e4234317"
|
|
60
64
|
},
|
|
61
65
|
{
|
|
62
66
|
"path": "src/tools.navigation.js",
|
|
63
|
-
"sha256": "
|
|
67
|
+
"sha256": "f7118e91c2edd0e6b57591d41fe7707c68eef51cde77918495bb4ee166139339"
|
|
64
68
|
},
|
|
65
69
|
{
|
|
66
70
|
"path": "src/tools.platform.js",
|
package/src/entry-integrity.js
CHANGED
|
@@ -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
|
+
}
|