@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.
package/dist/commands/index.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/commands/publish.js
CHANGED
|
@@ -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) {
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
}
|