@khanglvm/outline-cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +44 -22
- package/bin/outline-cli.js +14 -0
- package/docs/TOOL_CONTRACTS.md +8 -0
- package/package.json +1 -1
- package/src/agent-skills.js +160 -23
- package/src/cli.js +207 -63
- package/src/config-store.js +86 -6
- package/src/entry-integrity-manifest.generated.js +15 -11
- package/src/entry-integrity.js +3 -0
- package/src/summary-redaction.js +37 -0
- package/src/tool-arg-schemas.js +266 -10
- package/src/tools.extended.js +123 -16
- package/src/tools.js +277 -21
- package/src/tools.mutation.js +2 -1
- package/src/tools.navigation.js +3 -2
- package/test/agent-skills.unit.test.js +64 -1
- package/test/config-store.unit.test.js +32 -0
- package/test/hardening.unit.test.js +26 -1
- package/test/live.integration.test.js +20 -24
- package/test/profile-selection.unit.test.js +14 -4
- package/test/tool-resolution.unit.test.js +333 -0
- package/test/version.unit.test.js +21 -0
package/src/agent-skills.js
CHANGED
|
@@ -1,71 +1,173 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
2
|
|
|
3
|
-
const AI_SKILL_DATA_VERSION = "2026-03-
|
|
3
|
+
const AI_SKILL_DATA_VERSION = "2026-03-07.1";
|
|
4
4
|
const AI_HELP_SECTION_ID = "ai-skills";
|
|
5
5
|
const QUICK_START_HELP_SECTION_ID = "quick-start-agent";
|
|
6
|
-
const QUICK_START_HELP_VERSION = "2026-03-
|
|
6
|
+
const QUICK_START_HELP_VERSION = "2026-03-07.1";
|
|
7
7
|
|
|
8
8
|
const QUICK_START_AGENT_PLAYBOOK = {
|
|
9
9
|
title: "outline-cli onboarding for non-expert users",
|
|
10
10
|
audience: "AI agents assisting non-expert users in terminal setup and first usage.",
|
|
11
11
|
objective:
|
|
12
|
-
"
|
|
12
|
+
"Use this guide only for first-time setup or when profile/auth access is missing. For normal work, prefer direct task execution with search.research, documents.search, documents.info, and batch before reading onboarding docs.",
|
|
13
13
|
steps: [
|
|
14
14
|
{
|
|
15
15
|
step: 1,
|
|
16
|
-
title: "
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
title: "Confirm whether onboarding help is actually needed",
|
|
17
|
+
commands: [
|
|
18
|
+
"outline-cli --version",
|
|
19
|
+
"outline-cli profile list",
|
|
20
|
+
],
|
|
21
|
+
fallbackCommands: [
|
|
22
|
+
"npm i -g @khanglvm/outline-cli@latest",
|
|
23
|
+
"outline-cli --version",
|
|
24
|
+
"outline-cli profile list",
|
|
25
|
+
],
|
|
26
|
+
successCheck: "If version and profile list both work, skip onboarding help and proceed directly to the user's task.",
|
|
19
27
|
},
|
|
20
28
|
{
|
|
21
29
|
step: 2,
|
|
30
|
+
title: "Load full onboarding guide only when setup is missing or broken",
|
|
31
|
+
command: "outline-cli tools help quick-start-agent --view full",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
step: 3,
|
|
35
|
+
title: "Suggest optional outline-cli skill installation via npx skills",
|
|
36
|
+
question:
|
|
37
|
+
"Would you like me to install the outline-cli skill now via npx skills for all currently supported local AI tools? This helps future AI sessions use the right workflow by default.",
|
|
38
|
+
commandTemplates: [
|
|
39
|
+
"npx skills add https://github.com/khanglvm/skills --skill outline-cli -y",
|
|
40
|
+
],
|
|
41
|
+
decisionRules: [
|
|
42
|
+
"If user explicitly approves, run the command in headless mode exactly once so installation applies to currently supported local tools.",
|
|
43
|
+
"If user declines, skip installation and continue onboarding without blocking profile setup.",
|
|
44
|
+
],
|
|
45
|
+
expectedSignals: [
|
|
46
|
+
"Successful install prints completion output from npx skills.",
|
|
47
|
+
"Failure should be reported with the exact command and next recovery step.",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
step: 4,
|
|
22
52
|
title: "Inspect command surfaces before asking the user for inputs",
|
|
23
53
|
commands: [
|
|
24
|
-
"outline-cli --help",
|
|
25
54
|
"outline-cli profile --help",
|
|
26
|
-
"outline-cli tools --help",
|
|
27
55
|
"outline-cli invoke --help",
|
|
28
56
|
],
|
|
29
57
|
},
|
|
30
58
|
{
|
|
31
|
-
step:
|
|
32
|
-
title: "
|
|
59
|
+
step: 5,
|
|
60
|
+
title: "List existing profiles first and branch setup flow",
|
|
61
|
+
command: "outline-cli profile list",
|
|
62
|
+
decisionRules: [
|
|
63
|
+
"If one or more profiles exist, show them and ask whether to use an existing profile or create a new one.",
|
|
64
|
+
"If no profiles exist, continue with new profile setup questions.",
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
step: 6,
|
|
69
|
+
title: "Ask for Outline base URL with beginner-friendly explanation",
|
|
70
|
+
question:
|
|
71
|
+
"What is your Outline base URL? This is the web address you open in your browser to use Outline.",
|
|
72
|
+
examples: [
|
|
73
|
+
"Official cloud: https://app.getoutline.com",
|
|
74
|
+
"Custom/self-hosted: https://docs.yourcompany.com",
|
|
75
|
+
],
|
|
76
|
+
guidance: [
|
|
77
|
+
"Use https:// and do not include a trailing path.",
|
|
78
|
+
"If unsure, copy the URL from your browser while viewing your Outline workspace home.",
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
step: 7,
|
|
83
|
+
title: "Validate the provided base URL before asking for API key",
|
|
84
|
+
commandTemplates: [
|
|
85
|
+
'curl -sS -o /dev/null -w "HTTP %{http_code}\\n" "<base-url>"',
|
|
86
|
+
'curl -sS -o /dev/null -w "API %{http_code}\\n" "<base-url>/api/auth.info"',
|
|
87
|
+
],
|
|
88
|
+
expectedSignals: [
|
|
89
|
+
"Base URL check should return a reachable HTTP status (often 200/301/302).",
|
|
90
|
+
"API auth endpoint without token often returns 401/403, which still confirms domain + API route are reachable.",
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
step: 8,
|
|
95
|
+
title: "Guide user to create API key in Outline UI",
|
|
96
|
+
apiKeySettingsUrlTemplate: "<base-url>/settings/api-and-apps",
|
|
97
|
+
fallbackNavigation: "In Outline UI: Settings → API & Apps",
|
|
98
|
+
apiKeyConfigTemplate: [
|
|
99
|
+
"Name: outline-cli-<profile-id-or-your-name>",
|
|
100
|
+
"Expiration date: choose your policy (for example 90 days or no expiry if policy allows)",
|
|
101
|
+
"Scopes: leave empty for same permissions as your user, or restrict to needed endpoints",
|
|
102
|
+
],
|
|
103
|
+
scopeExamples: [
|
|
104
|
+
"Read-focused starter scope: *.info documents.search collections.list",
|
|
105
|
+
"Document automation scope: documents.* collections.info",
|
|
106
|
+
],
|
|
107
|
+
requiredUserAction:
|
|
108
|
+
"Create the key, then copy the generated token value (typically starts with ol_api_) and share it with the agent for profile setup.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
step: 9,
|
|
112
|
+
title: "Ask follow-up questions for remaining setup values (one at a time)",
|
|
33
113
|
questions: [
|
|
34
|
-
"What is your Outline base URL?",
|
|
35
114
|
"What is your Outline API key?",
|
|
36
115
|
"What profile id do you want? (default to prod if user has no preference)",
|
|
37
116
|
],
|
|
38
117
|
},
|
|
39
118
|
{
|
|
40
|
-
step:
|
|
41
|
-
title: "Guide profile setup and set it as default",
|
|
119
|
+
step: 10,
|
|
120
|
+
title: "Guide profile setup (API key mode) and set it as default",
|
|
42
121
|
command:
|
|
43
|
-
'outline-cli profile add <profile-id> --base-url <base-url> --api-key "<api-key>" --set-default',
|
|
122
|
+
'outline-cli profile add <profile-id> --base-url <base-url> --auth-type apiKey --api-key "<api-key>" --set-default',
|
|
44
123
|
},
|
|
45
124
|
{
|
|
46
|
-
step:
|
|
125
|
+
step: 11,
|
|
47
126
|
title: "Verify profile authentication",
|
|
48
127
|
command: "outline-cli profile test <profile-id> --pretty",
|
|
49
128
|
},
|
|
50
129
|
{
|
|
51
|
-
step:
|
|
52
|
-
title: "Show
|
|
130
|
+
step: 12,
|
|
131
|
+
title: "Show natural-language prompts for what the AI agent can do with outline-cli",
|
|
132
|
+
minimumPromptCount: 10,
|
|
133
|
+
naturalLanguagePrompts: [
|
|
134
|
+
"Find our onboarding checklist in Outline.",
|
|
135
|
+
"List recent documents in the Engineering collection.",
|
|
136
|
+
"Show me a summary of document <document-id>.",
|
|
137
|
+
"Create a draft release notes document with today's date.",
|
|
138
|
+
"Append a short status update to document <document-id>.",
|
|
139
|
+
"Find documents mentioning incident response in the last month.",
|
|
140
|
+
"Show who can access document <document-id>.",
|
|
141
|
+
"Compare two documents and list key differences.",
|
|
142
|
+
"Generate a TODO list from document <document-id>.",
|
|
143
|
+
"Find stale docs that have not been updated recently.",
|
|
144
|
+
],
|
|
53
145
|
commandTemplates: [
|
|
54
146
|
'outline-cli invoke documents.search --args \'{"query":"onboarding checklist","mode":"semantic","limit":5,"view":"summary"}\' --pretty',
|
|
147
|
+
'outline-cli invoke documents.list --args \'{"limit":5,"view":"summary"}\' --pretty',
|
|
55
148
|
'outline-cli invoke documents.info --args \'{"id":"<document-id>","view":"summary"}\' --pretty',
|
|
56
149
|
],
|
|
57
150
|
},
|
|
58
151
|
],
|
|
59
152
|
interactionRules: [
|
|
60
153
|
"Use short and clear explanations for beginners.",
|
|
154
|
+
"Do not open onboarding help for routine tasks when a working profile already exists.",
|
|
155
|
+
"For routine read tasks, start with search.research, documents.search, documents.info, or batch before reading contracts/help.",
|
|
156
|
+
"Always suggest optional outline-cli skill installation via npx skills and ask for explicit approval before running install.",
|
|
157
|
+
"If approved, run npx skills installation in headless mode (`-y`, no tool-specific agent filter) and continue onboarding.",
|
|
158
|
+
"Always run profile list before profile add.",
|
|
159
|
+
"Prefer API key authentication for beginner onboarding unless the user explicitly requests another auth mode.",
|
|
160
|
+
"Explain base URL and API key steps in plain language, then validate the domain before requesting the API key.",
|
|
61
161
|
"Ask one question at a time when required information is missing.",
|
|
62
162
|
"If a command fails, explain the cause and provide the exact next command.",
|
|
63
163
|
"Confirm each step completion before moving to the next step.",
|
|
164
|
+
"After setup, provide at least 10 natural-language prompts and run one command example.",
|
|
64
165
|
],
|
|
65
166
|
};
|
|
66
167
|
|
|
67
168
|
const AI_GLOBAL_GUIDANCE = {
|
|
68
169
|
principles: [
|
|
170
|
+
"Start with the best-fit task tool first; open help/contracts only after validation fails or capability is unclear.",
|
|
69
171
|
"Use ids/summary views first, then hydrate only selected records.",
|
|
70
172
|
"Prefer batch operations (queries, ids, or batch command) before multi-call loops.",
|
|
71
173
|
"For heavy retrieval, use search.research with precisionMode + perQueryView/perQueryHitLimit to control token cost.",
|
|
@@ -707,11 +809,46 @@ function summarizeQuickStartPlaybook() {
|
|
|
707
809
|
title: QUICK_START_AGENT_PLAYBOOK.title,
|
|
708
810
|
audience: QUICK_START_AGENT_PLAYBOOK.audience,
|
|
709
811
|
objective: QUICK_START_AGENT_PLAYBOOK.objective,
|
|
710
|
-
steps: QUICK_START_AGENT_PLAYBOOK.steps.map((row) =>
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
812
|
+
steps: QUICK_START_AGENT_PLAYBOOK.steps.map((row) => {
|
|
813
|
+
const summaryRow = {
|
|
814
|
+
step: row.step,
|
|
815
|
+
title: row.title,
|
|
816
|
+
};
|
|
817
|
+
if (row.command) {
|
|
818
|
+
summaryRow.command = row.command;
|
|
819
|
+
}
|
|
820
|
+
if (Array.isArray(row.commands)) {
|
|
821
|
+
summaryRow.commands = row.commands;
|
|
822
|
+
}
|
|
823
|
+
if (Array.isArray(row.questions)) {
|
|
824
|
+
summaryRow.questions = row.questions;
|
|
825
|
+
}
|
|
826
|
+
if (row.question) {
|
|
827
|
+
summaryRow.question = row.question;
|
|
828
|
+
}
|
|
829
|
+
if (row.minimumPromptCount) {
|
|
830
|
+
summaryRow.minimumPromptCount = row.minimumPromptCount;
|
|
831
|
+
}
|
|
832
|
+
if (Array.isArray(row.commandTemplates)) {
|
|
833
|
+
summaryRow.commandTemplates = row.commandTemplates;
|
|
834
|
+
}
|
|
835
|
+
if (Array.isArray(row.decisionRules)) {
|
|
836
|
+
summaryRow.decisionRules = row.decisionRules;
|
|
837
|
+
}
|
|
838
|
+
if (Array.isArray(row.expectedSignals)) {
|
|
839
|
+
summaryRow.expectedSignals = row.expectedSignals;
|
|
840
|
+
}
|
|
841
|
+
if (row.apiKeySettingsUrlTemplate) {
|
|
842
|
+
summaryRow.apiKeySettingsUrlTemplate = row.apiKeySettingsUrlTemplate;
|
|
843
|
+
}
|
|
844
|
+
if (Array.isArray(row.apiKeyConfigTemplate)) {
|
|
845
|
+
summaryRow.apiKeyConfigTemplate = row.apiKeyConfigTemplate;
|
|
846
|
+
}
|
|
847
|
+
if (Array.isArray(row.scopeExamples)) {
|
|
848
|
+
summaryRow.scopeExamples = row.scopeExamples;
|
|
849
|
+
}
|
|
850
|
+
return summaryRow;
|
|
851
|
+
}),
|
|
715
852
|
interactionRules: QUICK_START_AGENT_PLAYBOOK.interactionRules,
|
|
716
853
|
nextCommand: "outline-cli tools help quick-start-agent --view full",
|
|
717
854
|
};
|
|
@@ -850,7 +987,7 @@ export function listHelpSections() {
|
|
|
850
987
|
id: QUICK_START_HELP_SECTION_ID,
|
|
851
988
|
title: "AI setup onboarding",
|
|
852
989
|
description:
|
|
853
|
-
"
|
|
990
|
+
"First-time AI setup only: verify installation, guide API-key profile setup, and provide starter prompts when no working profile exists.",
|
|
854
991
|
commandExample: "outline-cli tools help quick-start-agent --view full",
|
|
855
992
|
},
|
|
856
993
|
{
|
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);
|
|
@@ -789,8 +908,11 @@ export async function run(argv = process.argv) {
|
|
|
789
908
|
if (
|
|
790
909
|
sectionName === "quick-start-agent" ||
|
|
791
910
|
sectionName === "quickstart-agent" ||
|
|
911
|
+
sectionName === "quick-start" ||
|
|
792
912
|
sectionName === "quickstart" ||
|
|
793
|
-
sectionName === "setup"
|
|
913
|
+
sectionName === "setup" ||
|
|
914
|
+
sectionName === "agent" ||
|
|
915
|
+
sectionName === "onboarding"
|
|
794
916
|
) {
|
|
795
917
|
await emitOutput(
|
|
796
918
|
store,
|
|
@@ -823,11 +945,21 @@ export async function run(argv = process.argv) {
|
|
|
823
945
|
);
|
|
824
946
|
|
|
825
947
|
invoke.action(async (tool, opts) => {
|
|
826
|
-
const runtime = await getRuntime(opts);
|
|
827
|
-
const store = buildStoreFromOptions(opts);
|
|
828
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);
|
|
829
955
|
const result = await invokeTool(runtime, tool, args);
|
|
830
|
-
|
|
956
|
+
const output = runtime.profileSelection?.autoSelected
|
|
957
|
+
? {
|
|
958
|
+
...result,
|
|
959
|
+
profileRouting: runtime.profileSelection,
|
|
960
|
+
}
|
|
961
|
+
: result;
|
|
962
|
+
await emitOutput(store, output, opts, {
|
|
831
963
|
label: `tool-${tool.replace(/\./g, "-")}`,
|
|
832
964
|
mode: opts.resultMode,
|
|
833
965
|
});
|
|
@@ -855,19 +987,25 @@ export async function run(argv = process.argv) {
|
|
|
855
987
|
const store = buildStoreFromOptions(opts);
|
|
856
988
|
const clientCache = new Map();
|
|
857
989
|
|
|
858
|
-
async function runtimeForProfile(profileId) {
|
|
859
|
-
const selected = getProfile(config, profileId || opts.profile
|
|
860
|
-
|
|
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)) {
|
|
861
998
|
const hydrated = hydrateProfileFromKeychain({
|
|
862
999
|
configPath,
|
|
863
|
-
profile:
|
|
1000
|
+
profile: storedProfile,
|
|
864
1001
|
});
|
|
865
|
-
clientCache.set(
|
|
1002
|
+
clientCache.set(storedProfile.id, {
|
|
866
1003
|
profile: hydrated,
|
|
867
1004
|
client: new OutlineClient(hydrated),
|
|
1005
|
+
profileSelection,
|
|
868
1006
|
});
|
|
869
1007
|
}
|
|
870
|
-
return clientCache.get(
|
|
1008
|
+
return clientCache.get(storedProfile.id);
|
|
871
1009
|
}
|
|
872
1010
|
|
|
873
1011
|
const parallel = toInteger(opts.parallel, 4);
|
|
@@ -879,7 +1017,11 @@ export async function run(argv = process.argv) {
|
|
|
879
1017
|
if (!operation.tool) {
|
|
880
1018
|
throw new CliError(`Operation at index ${index} is missing tool`);
|
|
881
1019
|
}
|
|
882
|
-
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
|
+
});
|
|
883
1025
|
const payload = await invokeTool(runtime, operation.tool, operation.args || {});
|
|
884
1026
|
const mode = (opts.itemEnvelope || "compact").toLowerCase();
|
|
885
1027
|
const compactResult =
|
|
@@ -895,9 +1037,11 @@ export async function run(argv = process.argv) {
|
|
|
895
1037
|
return {
|
|
896
1038
|
index,
|
|
897
1039
|
tool: operation.tool,
|
|
1040
|
+
...(payload?.tool && payload.tool !== operation.tool ? { resolvedTool: payload.tool } : {}),
|
|
898
1041
|
profile: runtime.profile.id,
|
|
899
1042
|
ok: true,
|
|
900
1043
|
result: mode === "full" ? payload : compactResult,
|
|
1044
|
+
...(runtime.profileSelection?.autoSelected ? { profileRouting: runtime.profileSelection } : {}),
|
|
901
1045
|
...(mode === "full" || Object.keys(compactMeta).length === 0 ? {} : { meta: compactMeta }),
|
|
902
1046
|
};
|
|
903
1047
|
} catch (err) {
|
|
@@ -945,7 +1089,7 @@ export async function run(argv = process.argv) {
|
|
|
945
1089
|
const merged = { ...cmd.parent.opts(), ...opts };
|
|
946
1090
|
const store = buildStoreFromOptions(merged);
|
|
947
1091
|
const content = await store.read(file);
|
|
948
|
-
process.stdout
|
|
1092
|
+
safeWrite(process.stdout, content.content);
|
|
949
1093
|
});
|
|
950
1094
|
|
|
951
1095
|
tmp
|
|
@@ -973,7 +1117,7 @@ export async function run(argv = process.argv) {
|
|
|
973
1117
|
await program.parseAsync(argv);
|
|
974
1118
|
} catch (err) {
|
|
975
1119
|
const output = formatError(err);
|
|
976
|
-
process.stderr
|
|
1120
|
+
safeWrite(process.stderr, `${JSON.stringify(output, null, 2)}\n`, process.exitCode || 1);
|
|
977
1121
|
process.exitCode = process.exitCode || 1;
|
|
978
1122
|
}
|
|
979
1123
|
}
|