@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.
@@ -1,71 +1,173 @@
1
1
  import { CliError } from "./errors.js";
2
2
 
3
- const AI_SKILL_DATA_VERSION = "2026-03-05.1";
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-05.1";
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
- "Install outline-cli globally, discover required setup commands, guide profile creation with follow-up questions, then demonstrate example use cases in natural language.",
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: "Install outline-cli globally",
17
- command: "npm i -g @khanglvm/outline-cli",
18
- successCheck: "outline-cli --help",
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: 3,
32
- title: "Ask follow-up questions for missing setup values",
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: 4,
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: 5,
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: 6,
52
- title: "Show example use cases in plain language and run one command per example",
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
- step: row.step,
712
- title: row.title,
713
- command: row.command || null,
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
- "Copy-ready AI onboarding instructions to install outline-cli, guide profile setup, and demonstrate first use cases.",
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.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);
@@ -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
- 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, {
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
- 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)) {
861
998
  const hydrated = hydrateProfileFromKeychain({
862
999
  configPath,
863
- profile: selected,
1000
+ profile: storedProfile,
864
1001
  });
865
- clientCache.set(selected.id, {
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(selected.id);
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 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
+ });
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.write(content.content);
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.write(`${JSON.stringify(output, null, 2)}\n`);
1120
+ safeWrite(process.stderr, `${JSON.stringify(output, null, 2)}\n`, process.exitCode || 1);
977
1121
  process.exitCode = process.exitCode || 1;
978
1122
  }
979
1123
  }