@orchagent/cli 0.3.64 → 0.3.66

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.
@@ -34,6 +34,7 @@ const service_1 = require("./service");
34
34
  const transfer_1 = require("./transfer");
35
35
  const pull_1 = require("./pull");
36
36
  const logs_1 = require("./logs");
37
+ const secrets_1 = require("./secrets");
37
38
  function registerCommands(program) {
38
39
  (0, login_1.registerLoginCommand)(program);
39
40
  (0, whoami_1.registerWhoamiCommand)(program);
@@ -68,4 +69,5 @@ function registerCommands(program) {
68
69
  (0, transfer_1.registerTransferCommand)(program);
69
70
  (0, pull_1.registerPullCommand)(program);
70
71
  (0, logs_1.registerLogsCommand)(program);
72
+ (0, secrets_1.registerSecretsCommand)(program);
71
73
  }
@@ -7,6 +7,7 @@ exports.registerInitCommand = registerInitCommand;
7
7
  const promises_1 = __importDefault(require("fs/promises"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const errors_1 = require("../lib/errors");
10
+ const github_weekly_summary_1 = require("./templates/github-weekly-summary");
10
11
  const MANIFEST_TEMPLATE = `{
11
12
  "name": "my-agent",
12
13
  "description": "A simple AI agent",
@@ -86,6 +87,64 @@ if __name__ == "__main__":
86
87
  main()
87
88
  `;
88
89
  function readmeTemplate(agentName, flavor) {
90
+ if (flavor === 'discord') {
91
+ return `# ${agentName}
92
+
93
+ An always-on Discord bot powered by Claude.
94
+
95
+ ## Setup
96
+
97
+ ### 1. Create a Discord bot
98
+
99
+ 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
100
+ 2. Create a new application, then go to **Bot** and copy the bot token
101
+ 3. Under **Privileged Gateway Intents**, enable **Message Content Intent**
102
+ 4. Go to **OAuth2 > URL Generator**, select \`bot\` scope, then invite to your server
103
+
104
+ ### 2. Get channel IDs
105
+
106
+ Enable Developer Mode in Discord (Settings > Advanced), then right-click a channel and copy its ID.
107
+
108
+ ### 3. Local development
109
+
110
+ \`\`\`sh
111
+ cp .env.example .env
112
+ # Fill in DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY, DISCORD_CHANNEL_IDS
113
+
114
+ pip install -r requirements.txt
115
+ python main.py
116
+ \`\`\`
117
+
118
+ ### 4. Deploy
119
+
120
+ \`\`\`sh
121
+ orch publish
122
+
123
+ # Add secrets in your workspace (web dashboard > Settings > Secrets):
124
+ # DISCORD_BOT_TOKEN — your bot token
125
+ # DISCORD_CHANNEL_IDS — comma-separated channel IDs
126
+
127
+ orch service deploy
128
+ \`\`\`
129
+
130
+ ## Customization
131
+
132
+ Edit \`main.py\` to customize:
133
+
134
+ - **SYSTEM_PROMPT** — controls how the bot responds
135
+ - **MODEL** / **MAX_TOKENS** — override via env vars
136
+
137
+ ## Environment Variables
138
+
139
+ | Variable | Required | Description |
140
+ |----------|----------|-------------|
141
+ | \`DISCORD_BOT_TOKEN\` | Yes | Discord bot token (workspace secret) |
142
+ | \`ANTHROPIC_API_KEY\` | Auto | Injected by orchagent via \`supported_providers\` |
143
+ | \`DISCORD_CHANNEL_IDS\` | Yes | Comma-separated channel IDs (workspace secret) |
144
+ | \`MODEL\` | No | Claude model (default: claude-sonnet-4-5-20250929) |
145
+ | \`MAX_TOKENS\` | No | Max response tokens (default: 1024) |
146
+ `;
147
+ }
89
148
  const inputField = flavor === 'managed_loop' || flavor === 'orchestrator' ? 'task' : 'input';
90
149
  const inputDescription = flavor === 'managed_loop' || flavor === 'orchestrator' ? 'The task to perform' : 'The input to process';
91
150
  const cloudExample = flavor === 'code_runtime'
@@ -230,6 +289,162 @@ if __name__ == "__main__":
230
289
  `;
231
290
  const ORCHESTRATOR_REQUIREMENTS = `orchagent-sdk>=0.1.0
232
291
  `;
292
+ const DISCORD_MAIN_PY = `"""
293
+ Discord bot agent — powered by Claude.
294
+
295
+ Listens for messages in configured channels and responds using the Anthropic API.
296
+
297
+ Local development:
298
+ 1. Copy .env.example to .env and fill in your tokens
299
+ 2. pip install -r requirements.txt
300
+ 3. python main.py
301
+ """
302
+
303
+ import asyncio
304
+ import logging
305
+ import os
306
+ import sys
307
+
308
+ import anthropic
309
+ import discord
310
+
311
+ # ---------------------------------------------------------------------------
312
+ # Configuration
313
+ # ---------------------------------------------------------------------------
314
+
315
+ DISCORD_BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN", "")
316
+ ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
317
+ DISCORD_CHANNEL_IDS = os.environ.get("DISCORD_CHANNEL_IDS", "")
318
+
319
+ MODEL = os.environ.get("MODEL", "claude-sonnet-4-5-20250929")
320
+ MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "1024"))
321
+
322
+ logging.basicConfig(
323
+ level=logging.INFO,
324
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
325
+ stream=sys.stdout,
326
+ )
327
+ logger = logging.getLogger("discord-bot")
328
+
329
+ SYSTEM_PROMPT = """\\
330
+ You are a helpful assistant in a Discord server.
331
+
332
+ Be concise and friendly. Use code blocks for code examples.
333
+ Keep responses under 1800 characters (Discord limit is 2000)."""
334
+
335
+
336
+ # ---------------------------------------------------------------------------
337
+ # Anthropic API
338
+ # ---------------------------------------------------------------------------
339
+
340
+
341
+ def ask_claude(client: anthropic.Anthropic, user_message: str) -> str:
342
+ """Send a message to Claude and return the response."""
343
+ response = client.messages.create(
344
+ model=MODEL,
345
+ max_tokens=MAX_TOKENS,
346
+ system=SYSTEM_PROMPT,
347
+ messages=[{"role": "user", "content": user_message}],
348
+ )
349
+ return response.content[0].text
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # Discord bot
354
+ # ---------------------------------------------------------------------------
355
+
356
+
357
+ def parse_channel_ids(raw: str) -> set[int]:
358
+ """Parse comma-separated channel IDs from env var."""
359
+ return {int(x.strip()) for x in raw.split(",") if x.strip().isdigit()}
360
+
361
+
362
+ class Bot(discord.Client):
363
+ def __init__(self, anthropic_client: anthropic.Anthropic, allowed_channels: set[int]):
364
+ intents = discord.Intents.default()
365
+ intents.message_content = True
366
+ super().__init__(intents=intents)
367
+ self.anthropic_client = anthropic_client
368
+ self.allowed_channels = allowed_channels
369
+
370
+ async def on_ready(self):
371
+ logger.info("Bot connected as %s", self.user)
372
+
373
+ async def on_message(self, message: discord.Message):
374
+ if message.author.bot or not message.content.strip():
375
+ return
376
+
377
+ # Only respond in allowed channels (or threads within them)
378
+ channel_id = message.channel.id
379
+ parent_id = getattr(message.channel, "parent_id", None)
380
+ if channel_id not in self.allowed_channels and parent_id not in self.allowed_channels:
381
+ return
382
+
383
+ logger.info("Message from %s: %.100s", message.author, message.content)
384
+
385
+ async with message.channel.typing():
386
+ try:
387
+ answer = await asyncio.to_thread(
388
+ ask_claude, self.anthropic_client, message.content
389
+ )
390
+ except anthropic.APIError as exc:
391
+ logger.error("Anthropic API error: %s", exc)
392
+ await message.reply("Sorry, I ran into an issue. Please try again.")
393
+ return
394
+
395
+ if len(answer) > 1900:
396
+ answer = answer[:1897] + "..."
397
+
398
+ await message.reply(answer)
399
+
400
+
401
+ # ---------------------------------------------------------------------------
402
+ # Entry point
403
+ # ---------------------------------------------------------------------------
404
+
405
+
406
+ def main():
407
+ if not DISCORD_BOT_TOKEN:
408
+ logger.error("DISCORD_BOT_TOKEN not set")
409
+ sys.exit(1)
410
+ if not ANTHROPIC_API_KEY:
411
+ logger.error("ANTHROPIC_API_KEY not set")
412
+ sys.exit(1)
413
+ if not DISCORD_CHANNEL_IDS:
414
+ logger.error("DISCORD_CHANNEL_IDS not set — add comma-separated channel IDs")
415
+ sys.exit(1)
416
+
417
+ allowed = parse_channel_ids(DISCORD_CHANNEL_IDS)
418
+ if not allowed:
419
+ logger.error("No valid channel IDs in DISCORD_CHANNEL_IDS=%r", DISCORD_CHANNEL_IDS)
420
+ sys.exit(1)
421
+
422
+ logger.info("Starting bot — model: %s, channels: %s", MODEL, allowed)
423
+
424
+ client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
425
+ bot = Bot(client, allowed)
426
+ bot.run(DISCORD_BOT_TOKEN, log_handler=None)
427
+
428
+
429
+ if __name__ == "__main__":
430
+ main()
431
+ `;
432
+ const DISCORD_REQUIREMENTS = `discord.py>=2.3.0,<3.0.0
433
+ anthropic>=0.40.0,<1.0.0
434
+ `;
435
+ const DISCORD_ENV_EXAMPLE = `# Required — get your bot token from https://discord.com/developers/applications
436
+ DISCORD_BOT_TOKEN=
437
+
438
+ # Required for local dev — auto-injected in production via supported_providers
439
+ ANTHROPIC_API_KEY=
440
+
441
+ # Required — comma-separated Discord channel IDs where the bot should respond
442
+ DISCORD_CHANNEL_IDS=
443
+
444
+ # Optional — customize the model and response length
445
+ # MODEL=claude-sonnet-4-5-20250929
446
+ # MAX_TOKENS=1024
447
+ `;
233
448
  const SKILL_TEMPLATE = `---
234
449
  name: my-skill
235
450
  description: When to use this skill
@@ -264,9 +479,10 @@ function registerInitCommand(program) {
264
479
  .option('--type <type>', 'Type: prompt, tool, agent, or skill (legacy aliases: agentic, code)', 'prompt')
265
480
  .option('--orchestrator', 'Create an orchestrator agent with dependency scaffolding and SDK boilerplate')
266
481
  .option('--run-mode <mode>', 'Run mode for agents: on_demand or always_on', 'on_demand')
482
+ .option('--template <name>', 'Start from a template (available: github-weekly-summary, discord)')
267
483
  .action(async (name, options) => {
268
484
  const cwd = process.cwd();
269
- const runMode = (options.runMode || 'on_demand').trim().toLowerCase();
485
+ let runMode = (options.runMode || 'on_demand').trim().toLowerCase();
270
486
  if (!['on_demand', 'always_on'].includes(runMode)) {
271
487
  throw new errors_1.CliError("Invalid --run-mode. Use 'on_demand' or 'always_on'.");
272
488
  }
@@ -277,6 +493,26 @@ function registerInitCommand(program) {
277
493
  }
278
494
  initMode = { type: 'agent', flavor: 'orchestrator' };
279
495
  }
496
+ if (options.template) {
497
+ const template = options.template.trim().toLowerCase();
498
+ const validTemplates = ['discord', 'github-weekly-summary'];
499
+ if (!validTemplates.includes(template)) {
500
+ throw new errors_1.CliError(`Unknown --template '${template}'. Available templates: ${validTemplates.join(', ')}`);
501
+ }
502
+ if (options.orchestrator) {
503
+ throw new errors_1.CliError('Cannot use --template with --orchestrator.');
504
+ }
505
+ if (initMode.type === 'skill') {
506
+ throw new errors_1.CliError('Cannot use --template with --type skill.');
507
+ }
508
+ if (template === 'discord') {
509
+ initMode = { type: 'agent', flavor: 'discord' };
510
+ runMode = 'always_on';
511
+ }
512
+ else if (template === 'github-weekly-summary') {
513
+ initMode = { type: 'agent', flavor: 'github_weekly_summary' };
514
+ }
515
+ }
280
516
  // When a name is provided, create a subdirectory for the project
281
517
  const targetDir = name ? path_1.default.join(cwd, name) : cwd;
282
518
  const agentName = name || path_1.default.basename(cwd);
@@ -314,6 +550,61 @@ function registerInitCommand(program) {
314
550
  }
315
551
  return;
316
552
  }
553
+ // Handle github-weekly-summary template separately (own file set + output)
554
+ if (initMode.flavor === 'github_weekly_summary') {
555
+ const manifestPath = path_1.default.join(targetDir, 'orchagent.json');
556
+ // Check if already initialized
557
+ try {
558
+ await promises_1.default.access(manifestPath);
559
+ throw new errors_1.CliError(`Already initialized (orchagent.json exists in ${name ? name + '/' : 'current directory'})`);
560
+ }
561
+ catch (err) {
562
+ if (err.code !== 'ENOENT') {
563
+ throw err;
564
+ }
565
+ }
566
+ const sub = (s) => s.replace(/\{\{name\}\}/g, agentName);
567
+ // Create prompts/ subdirectory
568
+ await promises_1.default.mkdir(path_1.default.join(targetDir, 'prompts'), { recursive: true });
569
+ // Write all files
570
+ await promises_1.default.writeFile(manifestPath, sub(github_weekly_summary_1.TEMPLATE_MANIFEST));
571
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.py'), github_weekly_summary_1.TEMPLATE_MAIN_PY);
572
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'config.py'), github_weekly_summary_1.TEMPLATE_CONFIG_PY);
573
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'github_fetcher.py'), github_weekly_summary_1.TEMPLATE_GITHUB_FETCHER_PY);
574
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'activity_store.py'), github_weekly_summary_1.TEMPLATE_ACTIVITY_STORE_PY);
575
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'analyst.py'), github_weekly_summary_1.TEMPLATE_ANALYST_PY);
576
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'models.py'), github_weekly_summary_1.TEMPLATE_MODELS_PY);
577
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'requirements.txt'), github_weekly_summary_1.TEMPLATE_REQUIREMENTS_TXT);
578
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'prompts', 'weekly_summary.md'), github_weekly_summary_1.TEMPLATE_WEEKLY_SUMMARY_PROMPT);
579
+ await promises_1.default.writeFile(path_1.default.join(targetDir, '.env.example'), sub(github_weekly_summary_1.TEMPLATE_ENV_EXAMPLE));
580
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'README.md'), sub(github_weekly_summary_1.TEMPLATE_README));
581
+ const prefix = name ? name + '/' : '';
582
+ process.stdout.write(`\nInitialized github-weekly-summary agent "${agentName}" in ${targetDir}\n`);
583
+ process.stdout.write(`\nFiles created:\n`);
584
+ process.stdout.write(` ${prefix}orchagent.json Agent manifest\n`);
585
+ process.stdout.write(` ${prefix}main.py Entrypoint\n`);
586
+ process.stdout.write(` ${prefix}config.py Config loader\n`);
587
+ process.stdout.write(` ${prefix}github_fetcher.py GitHub API client\n`);
588
+ process.stdout.write(` ${prefix}activity_store.py Stats computation\n`);
589
+ process.stdout.write(` ${prefix}analyst.py LLM summary generator\n`);
590
+ process.stdout.write(` ${prefix}models.py Data models\n`);
591
+ process.stdout.write(` ${prefix}requirements.txt Python dependencies\n`);
592
+ process.stdout.write(` ${prefix}prompts/weekly_summary.md LLM prompt template\n`);
593
+ process.stdout.write(` ${prefix}.env.example Secret reference\n`);
594
+ process.stdout.write(` ${prefix}README.md Setup guide\n`);
595
+ process.stdout.write(`\nNext steps:\n`);
596
+ const s = name ? 2 : 1;
597
+ if (name) {
598
+ process.stdout.write(` 1. cd ${name}\n`);
599
+ }
600
+ process.stdout.write(` ${s}. orch github connect Connect your GitHub account\n`);
601
+ process.stdout.write(` ${s + 1}. orch publish Publish the agent\n`);
602
+ process.stdout.write(` ${s + 2}. Add secrets in web dashboard ORCHAGENT_API_KEY, DISCORD_WEBHOOK_URL, ANTHROPIC_API_KEY, GITHUB_REPOS\n`);
603
+ process.stdout.write(` ${s + 3}. orch run <org>/${agentName} Test it\n`);
604
+ process.stdout.write(` ${s + 4}. orch schedule create <org>/${agentName} --cron "0 9 * * 1" Schedule weekly\n`);
605
+ process.stdout.write(`\n See README.md for full setup guide.\n`);
606
+ return;
607
+ }
317
608
  const manifestPath = path_1.default.join(targetDir, 'orchagent.json');
318
609
  const promptPath = path_1.default.join(targetDir, 'prompt.md');
319
610
  const schemaPath = path_1.default.join(targetDir, 'schema.json');
@@ -327,7 +618,7 @@ function registerInitCommand(program) {
327
618
  throw err;
328
619
  }
329
620
  }
330
- if (initMode.flavor !== 'code_runtime' && initMode.flavor !== 'orchestrator' && runMode === 'always_on') {
621
+ if (initMode.flavor !== 'code_runtime' && initMode.flavor !== 'orchestrator' && initMode.flavor !== 'discord' && runMode === 'always_on') {
331
622
  throw new errors_1.CliError("run_mode=always_on requires runtime.command in orchagent.json (e.g. \"runtime\": { \"command\": \"python main.py\" }). Use --type tool for code-runtime agents.");
332
623
  }
333
624
  // Create manifest and type-specific files
@@ -353,6 +644,13 @@ function registerInitCommand(program) {
353
644
  manifest.loop = { max_turns: 25 };
354
645
  manifest.required_secrets = [];
355
646
  }
647
+ else if (initMode.flavor === 'discord') {
648
+ manifest.description = 'An always-on Discord bot powered by Claude';
649
+ manifest.runtime = { command: 'python main.py' };
650
+ manifest.supported_providers = ['anthropic'];
651
+ manifest.required_secrets = ['DISCORD_BOT_TOKEN', 'DISCORD_CHANNEL_IDS'];
652
+ manifest.tags = ['discord', 'always-on'];
653
+ }
356
654
  else if (initMode.flavor === 'code_runtime') {
357
655
  manifest.description = 'A code-runtime agent';
358
656
  manifest.runtime = { command: 'python main.py' };
@@ -366,6 +664,14 @@ function registerInitCommand(program) {
366
664
  await promises_1.default.writeFile(requirementsPath, ORCHESTRATOR_REQUIREMENTS);
367
665
  await promises_1.default.writeFile(schemaPath, AGENT_SCHEMA_TEMPLATE);
368
666
  }
667
+ else if (initMode.flavor === 'discord') {
668
+ const entrypointPath = path_1.default.join(targetDir, 'main.py');
669
+ const requirementsPath = path_1.default.join(targetDir, 'requirements.txt');
670
+ const envExamplePath = path_1.default.join(targetDir, '.env.example');
671
+ await promises_1.default.writeFile(entrypointPath, DISCORD_MAIN_PY);
672
+ await promises_1.default.writeFile(requirementsPath, DISCORD_REQUIREMENTS);
673
+ await promises_1.default.writeFile(envExamplePath, DISCORD_ENV_EXAMPLE);
674
+ }
369
675
  else if (initMode.flavor === 'code_runtime') {
370
676
  const entrypointPath = path_1.default.join(targetDir, 'main.py');
371
677
  await promises_1.default.writeFile(entrypointPath, CODE_TEMPLATE_PY);
@@ -390,16 +696,23 @@ function registerInitCommand(program) {
390
696
  process.stdout.write(` ${prefix}main.py - Orchestrator entrypoint (SDK calls)\n`);
391
697
  process.stdout.write(` ${prefix}requirements.txt - Python dependencies (orchagent-sdk)\n`);
392
698
  }
699
+ else if (initMode.flavor === 'discord') {
700
+ process.stdout.write(` ${prefix}main.py - Discord bot (discord.py + Anthropic)\n`);
701
+ process.stdout.write(` ${prefix}requirements.txt - Python dependencies\n`);
702
+ process.stdout.write(` ${prefix}.env.example - Environment variables template\n`);
703
+ }
393
704
  else if (initMode.flavor === 'code_runtime') {
394
705
  process.stdout.write(` ${prefix}main.py - Agent entrypoint (stdin/stdout JSON)\n`);
395
706
  }
396
707
  else {
397
708
  process.stdout.write(` ${prefix}prompt.md - Prompt template\n`);
398
709
  }
399
- process.stdout.write(` ${prefix}schema.json - Input/output schemas\n`);
710
+ if (initMode.flavor !== 'discord') {
711
+ process.stdout.write(` ${prefix}schema.json - Input/output schemas\n`);
712
+ }
400
713
  process.stdout.write(` ${prefix}README.md - Agent documentation\n`);
401
714
  process.stdout.write(` Run mode: ${runMode}\n`);
402
- process.stdout.write(` Execution: ${initMode.flavor === 'orchestrator' ? 'code_runtime (orchestrator)' : initMode.flavor}\n`);
715
+ process.stdout.write(` Execution: ${initMode.flavor === 'orchestrator' ? 'code_runtime (orchestrator)' : initMode.flavor === 'discord' ? 'code_runtime (discord)' : initMode.flavor}\n`);
403
716
  process.stdout.write(`\nNext steps:\n`);
404
717
  if (initMode.flavor === 'orchestrator') {
405
718
  const stepNum = name ? 2 : 1;
@@ -410,6 +723,17 @@ function registerInitCommand(program) {
410
723
  process.stdout.write(` ${stepNum + 1}. Edit main.py with your orchestration logic\n`);
411
724
  process.stdout.write(` ${stepNum + 2}. Publish dependency agents first, then: orchagent publish\n`);
412
725
  }
726
+ else if (initMode.flavor === 'discord') {
727
+ const stepNum = name ? 2 : 1;
728
+ if (name) {
729
+ process.stdout.write(` 1. cd ${name}\n`);
730
+ }
731
+ process.stdout.write(` ${stepNum}. Create a Discord bot at https://discord.com/developers/applications\n`);
732
+ process.stdout.write(` ${stepNum + 1}. Enable Message Content Intent in bot settings\n`);
733
+ process.stdout.write(` ${stepNum + 2}. Copy .env.example to .env and fill in your tokens\n`);
734
+ process.stdout.write(` ${stepNum + 3}. Test locally: pip install -r requirements.txt && python main.py\n`);
735
+ process.stdout.write(` ${stepNum + 4}. Deploy: orch publish\n`);
736
+ }
413
737
  else if (initMode.flavor === 'code_runtime') {
414
738
  const stepNum = name ? 2 : 1;
415
739
  if (name) {
@@ -753,6 +753,9 @@ function registerPublishCommand(program) {
753
753
  else if (effectiveSkills?.length) {
754
754
  process.stderr.write(` Skills: ${effectiveSkills.join(', ')}\n`);
755
755
  }
756
+ if (manifest.required_secrets?.length) {
757
+ process.stderr.write(` Secrets: ${manifest.required_secrets.join(', ')}\n`);
758
+ }
756
759
  process.stderr.write(`\nWould publish: ${preview.org_slug}/${manifest.name}@${preview.next_version}\n`);
757
760
  if (shouldUploadBundle) {
758
761
  const bundlePreview = await (0, bundle_1.previewBundle)(cwd, {
@@ -936,6 +939,18 @@ function registerPublishCommand(program) {
936
939
  process.stdout.write(`Callable: ${callable ? 'enabled' : 'disabled'}\n`);
937
940
  process.stdout.write(`Providers: ${supportedProviders.join(', ')}\n`);
938
941
  process.stdout.write(`Visibility: private\n`);
942
+ // Show required secrets with setup instructions (F-18)
943
+ if (manifest.required_secrets?.length) {
944
+ process.stdout.write(`\nRequired secrets:\n`);
945
+ for (const secret of manifest.required_secrets) {
946
+ process.stdout.write(` ${secret}\n`);
947
+ }
948
+ process.stdout.write(`\nSet secrets before running:\n`);
949
+ for (const secret of manifest.required_secrets) {
950
+ process.stdout.write(` orch secrets set ${secret} <value>\n`);
951
+ }
952
+ process.stdout.write(`\nView existing secrets: ${chalk_1.default.cyan('orch secrets list')}\n`);
953
+ }
939
954
  // Show security review result if available
940
955
  const secReview = result.security_review;
941
956
  if (secReview?.verdict) {
@@ -1624,6 +1624,38 @@ async function executeCloud(agentRef, file, options) {
1624
1624
  runtime: agentMeta.runtime ?? null,
1625
1625
  loop: agentMeta.loop ?? null,
1626
1626
  });
1627
+ // Pre-flight: check required secrets before running (F-18)
1628
+ // Only for sandbox-backed engines where secrets are injected as env vars
1629
+ if (cloudEngine !== 'direct_llm') {
1630
+ const agentRequiredSecrets = agentMeta.required_secrets;
1631
+ if (agentRequiredSecrets?.length) {
1632
+ try {
1633
+ const wsSlug = configFile.workspace;
1634
+ if (wsSlug) {
1635
+ const { workspaces } = await (0, api_1.request)(resolved, 'GET', '/workspaces');
1636
+ const ws = workspaces.find((w) => w.slug === wsSlug);
1637
+ if (ws) {
1638
+ const secretsResult = await (0, api_1.request)(resolved, 'GET', `/workspaces/${ws.id}/secrets`);
1639
+ const existingNames = new Set(secretsResult.secrets.map((s) => s.name));
1640
+ const missing = agentRequiredSecrets.filter((s) => !existingNames.has(s));
1641
+ if (missing.length > 0) {
1642
+ throw new errors_1.CliError(`Agent requires secrets not found in workspace '${wsSlug}':\n` +
1643
+ missing.map((s) => ` - ${s}`).join('\n') + '\n\n' +
1644
+ `Set them before running:\n` +
1645
+ missing.map((s) => ` orch secrets set ${s} <value>`).join('\n') + '\n\n' +
1646
+ `Secrets are injected as environment variables into the agent sandbox.\n` +
1647
+ `View existing secrets: orch secrets list`);
1648
+ }
1649
+ }
1650
+ }
1651
+ }
1652
+ catch (err) {
1653
+ if (err instanceof errors_1.CliError)
1654
+ throw err;
1655
+ // Non-fatal: gateway will catch missing secrets at execution time
1656
+ }
1657
+ }
1658
+ }
1627
1659
  // Pre-call balance check for paid agents
1628
1660
  let pricingInfo;
1629
1661
  if ((0, pricing_1.isPaidAgent)(agentMeta)) {
@@ -1680,6 +1712,7 @@ async function executeCloud(agentRef, file, options) {
1680
1712
  const headers = {
1681
1713
  Authorization: `Bearer ${resolved.apiKey}`,
1682
1714
  'X-CLI-Version': package_json_1.default.version,
1715
+ 'X-OrchAgent-Client': 'cli',
1683
1716
  };
1684
1717
  if (options.tenant) {
1685
1718
  headers['X-OrchAgent-Tenant'] = options.tenant;
@@ -2022,6 +2055,37 @@ async function executeCloud(agentRef, file, options) {
2022
2055
  ` - Contacting the agent author to increase the timeout` +
2023
2056
  refSuffix);
2024
2057
  }
2058
+ if (errorCode === 'MISSING_SECRETS') {
2059
+ spinner?.fail('Missing workspace secrets');
2060
+ // Extract secret names from gateway message:
2061
+ // "Agent requires secret(s) not found in workspace: NAME1, NAME2. Add them in Settings > Secrets."
2062
+ const secretNames = [];
2063
+ if (message) {
2064
+ const match = message.match(/not found in workspace:\s*(.+?)\./);
2065
+ if (match) {
2066
+ secretNames.push(...match[1].split(',').map((s) => s.trim()).filter(Boolean));
2067
+ }
2068
+ }
2069
+ let hint = '';
2070
+ if (secretNames.length > 0) {
2071
+ hint += `Missing secrets:\n`;
2072
+ for (const name of secretNames) {
2073
+ hint += ` - ${name}\n`;
2074
+ }
2075
+ hint += `\nSet them with:\n`;
2076
+ for (const name of secretNames) {
2077
+ hint += ` orch secrets set ${name} <value>\n`;
2078
+ }
2079
+ }
2080
+ else {
2081
+ hint += `${message}\n\n`;
2082
+ hint += `Set missing secrets:\n`;
2083
+ hint += ` orch secrets set <NAME> <value>\n`;
2084
+ }
2085
+ hint += `\nView existing secrets:\n`;
2086
+ hint += ` orch secrets list`;
2087
+ throw new errors_1.CliError(hint + refSuffix);
2088
+ }
2025
2089
  if (response.status >= 500) {
2026
2090
  spinner?.fail(`Server error (${response.status})`);
2027
2091
  throw new errors_1.CliError(`${message}\n\n` +
@@ -2103,6 +2167,10 @@ async function executeCloud(agentRef, file, options) {
2103
2167
  if (parts.length > 0) {
2104
2168
  process.stderr.write(chalk_1.default.gray(`${parts.join(' · ')}\n`));
2105
2169
  }
2170
+ const runId = response.headers?.get?.('x-run-id');
2171
+ if (runId) {
2172
+ process.stderr.write(chalk_1.default.gray(`View logs: orch logs ${runId}\n`));
2173
+ }
2106
2174
  }
2107
2175
  }
2108
2176
  }
@@ -2191,6 +2259,10 @@ async function executeCloud(agentRef, file, options) {
2191
2259
  if (parts.length > 0) {
2192
2260
  process.stderr.write(chalk_1.default.gray(`\n${parts.join(' · ')}\n`));
2193
2261
  }
2262
+ const runId = response.headers?.get?.('x-run-id');
2263
+ if (runId) {
2264
+ process.stderr.write(chalk_1.default.gray(`View logs: orch logs ${runId}\n`));
2265
+ }
2194
2266
  }
2195
2267
  }
2196
2268
  }