calabasas 0.22.0 → 0.23.1

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.
Files changed (2) hide show
  1. package/dist/index.js +339 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7834,6 +7834,340 @@ Run ${pc3.cyan("`calabasas init`")} first.`);
7834
7834
  await interactiveConfig(currentConfig, configPath);
7835
7835
  }
7836
7836
 
7837
+ // src/commands/webhook.ts
7838
+ import pc4 from "picocolors";
7839
+ function resolveAuthOrExit() {
7840
+ const resolved = resolvePlatformApiKey({});
7841
+ if (!resolved) {
7842
+ console.error(pc4.red("Error:") + ` no Calabasas credentials found.
7843
+ ` + " Run `calabasas login` (user key) or set CALABASAS_PLATFORM_API_KEY in .env.local.");
7844
+ process.exit(1);
7845
+ }
7846
+ return resolved.key;
7847
+ }
7848
+ function parseDuration(input) {
7849
+ const match = /^(\d+)(s|m|h|d)$/.exec(input.trim());
7850
+ if (!match)
7851
+ return null;
7852
+ const value = Number(match[1]);
7853
+ const unit = match[2];
7854
+ const multipliers = {
7855
+ s: 1000,
7856
+ m: 60000,
7857
+ h: 3600000,
7858
+ d: 86400000
7859
+ };
7860
+ return value * multipliers[unit];
7861
+ }
7862
+ function normalizeCreateInput(options, now = Date.now()) {
7863
+ if (!options.bot) {
7864
+ return { ok: false, error: "--bot <botId> is required" };
7865
+ }
7866
+ if (options.action !== "sendDM" && options.action !== "sendChannelMessage") {
7867
+ return {
7868
+ ok: false,
7869
+ error: `invalid --action: ${options.action ?? "(missing)"}. Must be one of: sendDM, sendChannelMessage`
7870
+ };
7871
+ }
7872
+ let params;
7873
+ if (options.action === "sendDM") {
7874
+ if (!options.userId) {
7875
+ return { ok: false, error: "--user-id <discordUserId> is required for sendDM" };
7876
+ }
7877
+ params = { userId: options.userId };
7878
+ } else {
7879
+ if (!options.channelId) {
7880
+ return {
7881
+ ok: false,
7882
+ error: "--channel-id <discordChannelId> is required for sendChannelMessage"
7883
+ };
7884
+ }
7885
+ params = { channelId: options.channelId };
7886
+ }
7887
+ let maxUses;
7888
+ if (options.maxUses !== undefined) {
7889
+ const parsed = Number(options.maxUses);
7890
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
7891
+ return {
7892
+ ok: false,
7893
+ error: `invalid --max-uses: ${options.maxUses}. Must be a positive integer.`
7894
+ };
7895
+ }
7896
+ maxUses = parsed;
7897
+ }
7898
+ let expiresAt;
7899
+ if (options.expires !== undefined) {
7900
+ const ms = parseDuration(options.expires);
7901
+ if (ms === null) {
7902
+ return {
7903
+ ok: false,
7904
+ error: `invalid --expires: ${options.expires}. Format: <number><unit> where unit is s|m|h|d (e.g. 30m, 24h, 7d).`
7905
+ };
7906
+ }
7907
+ expiresAt = now + ms;
7908
+ }
7909
+ return {
7910
+ ok: true,
7911
+ payload: {
7912
+ botId: options.bot,
7913
+ action: options.action,
7914
+ params,
7915
+ label: options.label,
7916
+ maxUses,
7917
+ expiresAt,
7918
+ platformId: options.platformId
7919
+ }
7920
+ };
7921
+ }
7922
+ async function webhookCreate(options) {
7923
+ const apiKey = resolveAuthOrExit();
7924
+ const result = normalizeCreateInput(options);
7925
+ if (!result.ok) {
7926
+ console.error(pc4.red("Error:") + " " + result.error);
7927
+ process.exit(1);
7928
+ }
7929
+ const { payload } = result;
7930
+ const { ok, data, status } = await platformApiRequest("POST", "/api/cli/webhooks", apiKey, payload);
7931
+ if (!ok) {
7932
+ const errMsg = data?.error ?? `HTTP ${status}`;
7933
+ console.error(pc4.red("Error:") + " " + errMsg);
7934
+ process.exit(1);
7935
+ }
7936
+ const created = data;
7937
+ console.log(pc4.green("Webhook created."));
7938
+ console.log("");
7939
+ console.log(`URL: ${pc4.cyan(created.url)}`);
7940
+ console.log(`token: ${created.token}`);
7941
+ console.log(`webhookId: ${created.webhookId}`);
7942
+ console.log(`action: ${created.action}`);
7943
+ console.log(`params: ${JSON.stringify(created.params)}`);
7944
+ if (payload.label)
7945
+ console.log(`label: ${payload.label}`);
7946
+ if (payload.maxUses !== undefined)
7947
+ console.log(`maxUses: ${payload.maxUses}`);
7948
+ if (payload.expiresAt !== undefined) {
7949
+ console.log(`expiresAt: ${new Date(payload.expiresAt).toISOString()}`);
7950
+ }
7951
+ console.log("");
7952
+ console.log(pc4.dim(`Fire it with: curl -X POST -H 'Content-Type: application/json' -d '{"content":"hi"}' ` + created.url));
7953
+ }
7954
+ async function webhookList(options) {
7955
+ const apiKey = resolveAuthOrExit();
7956
+ const { ok, data, status } = await platformApiRequest("GET", "/api/cli/webhooks", apiKey);
7957
+ if (!ok) {
7958
+ const errMsg = data?.error ?? `HTTP ${status}`;
7959
+ console.error(pc4.red("Error:") + " " + errMsg);
7960
+ process.exit(1);
7961
+ }
7962
+ const webhooks = data.webhooks;
7963
+ if (options.json) {
7964
+ console.log(JSON.stringify(webhooks, null, 2));
7965
+ return;
7966
+ }
7967
+ if (webhooks.length === 0) {
7968
+ console.log("No webhooks found.");
7969
+ console.log(pc4.dim("Create one with: calabasas webhook create --bot <id> --action sendDM --user-id <id>"));
7970
+ return;
7971
+ }
7972
+ for (const w of webhooks) {
7973
+ const target = w.params.userId ? `user ${w.params.userId}` : `channel ${w.params.channelId}`;
7974
+ const uses = w.maxUses !== undefined ? `${w.usageCount}/${w.maxUses}` : `${w.usageCount}/—`;
7975
+ const statusColor = w.status === "active" ? pc4.green : w.status === "expired" ? pc4.yellow : pc4.red;
7976
+ console.log(`${pc4.cyan(w.token)} ${w.action} ${target} uses=${uses} ${statusColor(w.status)}` + (w.label ? ` label=${w.label}` : ""));
7977
+ console.log(` ${pc4.dim(w.url)}`);
7978
+ }
7979
+ }
7980
+ async function webhookRevoke(token, _options) {
7981
+ if (!token) {
7982
+ console.error(pc4.red("Error:") + " token argument is required.");
7983
+ process.exit(1);
7984
+ }
7985
+ const apiKey = resolveAuthOrExit();
7986
+ const { ok, data, status } = await platformApiRequest("DELETE", `/api/cli/webhooks?token=${encodeURIComponent(token)}`, apiKey);
7987
+ if (!ok) {
7988
+ const errMsg = data?.error ?? `HTTP ${status}`;
7989
+ console.error(pc4.red("Error:") + " " + errMsg);
7990
+ process.exit(1);
7991
+ }
7992
+ console.log(pc4.green("Webhook revoked."));
7993
+ }
7994
+
7995
+ // src/commands/webhook-skill.ts
7996
+ import * as fs9 from "fs";
7997
+ import * as path9 from "path";
7998
+ import pc5 from "picocolors";
7999
+ var DEFAULT_SKILL_PATH = ".claude/skills/calabasas-webhook/SKILL.md";
8000
+ var SKILL_CONTENT = `---
8001
+ name: calabasas-webhook
8002
+ description: Use when the user wants to fire a single Discord action (DM a user, post to a channel) from a non-Discord-aware system — Zapier, n8n, cron jobs, GitHub Actions, an LLM tool, a no-code form, a webhook from another SaaS. The Calabasas one-off webhook turns a pre-bound action into a plain POST URL with no Discord SDK or auth on the caller side. Trigger when the user says things like "send a DM from a cron", "fire a Discord message from Zapier", "give me a URL that DMs user X", "I need a webhook to post in #channel", or asks how to bridge an external tool to Discord without writing a bot integration.
8003
+ ---
8004
+
8005
+ # Calabasas one-off webhooks
8006
+
8007
+ A Calabasas webhook is a URL that fires **one** pre-bound Discord action when POSTed to. The action, the bot, and the target (user or channel) are all baked into the URL at create time. The body only carries the message payload (\`content\` / \`embeds\`).
8008
+
8009
+ This is the right tool when:
8010
+ - A non-Discord system needs to send a Discord message (Zapier, n8n, cron, external SaaS webhooks, GitHub Actions, monitoring alerts).
8011
+ - The caller cannot or should not hold a bot token.
8012
+ - The action is narrow and known up front — "DM this exact user", "post to this exact channel".
8013
+
8014
+ **Not** the right tool when:
8015
+ - The caller needs to choose the target at fire time (use the Calabasas SDK or generated Discord actions instead).
8016
+ - The action is more complex than \`sendDM\` / \`sendChannelMessage\` (kick, ban, role assignment — use the SDK).
8017
+ - The caller is the user's own Convex backend (use generated Discord actions; no HTTP roundtrip needed).
8018
+
8019
+ ## Supported actions (v1)
8020
+
8021
+ | Action | Bound param | Body fields |
8022
+ |--------|-------------|-------------|
8023
+ | \`sendDM\` | \`--user-id <discordUserId>\` | \`content\` and/or \`embeds\` |
8024
+ | \`sendChannelMessage\` | \`--channel-id <discordChannelId>\` | \`content\` and/or \`embeds\` |
8025
+
8026
+ ## CLI commands (non-interactive, scriptable)
8027
+
8028
+ All flags are required-or-fail; no prompts. Safe to drive from an LLM or shell script.
8029
+
8030
+ ### Create
8031
+
8032
+ \`\`\`bash
8033
+ calabasas webhook create \\
8034
+ --bot <botId> \\
8035
+ --action <sendDM|sendChannelMessage> \\
8036
+ [--user-id <discordUserId> | --channel-id <discordChannelId>] \\
8037
+ [--label <label>] \\
8038
+ [--max-uses <number>] \\
8039
+ [--expires <duration>]
8040
+ \`\`\`
8041
+
8042
+ | Flag | Notes |
8043
+ |------|-------|
8044
+ | \`--bot\` | Calabasas bot ID. Use \`calabasas bot list --once\` to find one. |
8045
+ | \`--action\` | \`sendDM\` or \`sendChannelMessage\`. |
8046
+ | \`--user-id\` | Required for \`sendDM\`. Discord user snowflake. |
8047
+ | \`--channel-id\` | Required for \`sendChannelMessage\`. Discord channel snowflake. |
8048
+ | \`--label\` | Free-form label shown in \`webhook list\`. |
8049
+ | \`--max-uses\` | Hard cap. Both successful and failed fires count toward the cap. |
8050
+ | \`--expires\` | Auto-expire after duration: \`30m\`, \`24h\`, \`7d\`, etc. (\`s\\|m\\|h\\|d\`). |
8051
+
8052
+ Output (key=value lines, easy to grep):
8053
+
8054
+ \`\`\`
8055
+ Webhook created.
8056
+
8057
+ URL: https://calabasas-production.up.railway.app/api/hooks/whk_xxxxx
8058
+ token: whk_xxxxx
8059
+ webhookId: <id>
8060
+ action: sendDM
8061
+ params: {"userId":"123456789012345678"}
8062
+ \`\`\`
8063
+
8064
+ ### Fire
8065
+
8066
+ \`\`\`bash
8067
+ curl -X POST \\
8068
+ -H 'Content-Type: application/json' \\
8069
+ -d '{"content":"Hello!"}' \\
8070
+ https://calabasas-production.up.railway.app/api/hooks/whk_xxxxx
8071
+ \`\`\`
8072
+
8073
+ Body fields:
8074
+
8075
+ | Field | Type | Notes |
8076
+ |-------|------|-------|
8077
+ | \`content\` | string | Message text. At least one of \`content\` or \`embeds\` is required. |
8078
+ | \`embeds\` | array | Discord embed objects. |
8079
+ | \`tts\` | boolean | Text-to-speech. |
8080
+ | \`allowedMentions\` | object | Discord allowed-mentions object (camelCase). |
8081
+
8082
+ Response shape (always JSON):
8083
+
8084
+ - Success → \`{ "ok": true, "messageId": "..." }\` (HTTP 200)
8085
+ - Failure → \`{ "ok": false, "error": "..." }\` (HTTP 4xx/5xx)
8086
+
8087
+ Status codes:
8088
+
8089
+ | Code | Meaning |
8090
+ |------|---------|
8091
+ | 200 | Action fired |
8092
+ | 400 | Invalid body (missing \`content\`/\`embeds\`, malformed JSON) |
8093
+ | 404 | Token not found or revoked |
8094
+ | 410 | Token expired or \`maxUses\` reached |
8095
+ | 502 | Discord rejected the request |
8096
+ | 503 | Bot is currently offline — retry shortly |
8097
+
8098
+ ### List
8099
+
8100
+ \`\`\`bash
8101
+ calabasas webhook list # human-friendly table
8102
+ calabasas webhook list --json # raw JSON for tooling/LLM use
8103
+ \`\`\`
8104
+
8105
+ Status column shows \`active\`, \`expired\`, or \`exhausted\`. Revoked rows are filtered out.
8106
+
8107
+ ### Revoke
8108
+
8109
+ \`\`\`bash
8110
+ calabasas webhook revoke <token>
8111
+ \`\`\`
8112
+
8113
+ Soft delete — URL stops working immediately, row stays for audit. To rotate a leaked token, revoke and create a fresh one (the URL changes; consumers must update).
8114
+
8115
+ ## Choosing limits
8116
+
8117
+ Webhooks are public — anyone with the URL can fire. Treat the URL like a one-shot capability token. Defaults to keep things safe:
8118
+
8119
+ - **Set \`--max-uses\`** unless the integration genuinely needs unlimited fires. Even \`--max-uses 1000\` is far better than uncapped.
8120
+ - **Set \`--expires\`** for any webhook tied to a campaign / deploy / time-bound flow.
8121
+ - **Use \`--label\`** so you can identify it in \`webhook list\` later.
8122
+
8123
+ ## Common patterns
8124
+
8125
+ **Cron alert to a channel:**
8126
+ \`\`\`bash
8127
+ calabasas webhook create --bot <id> --action sendChannelMessage \\
8128
+ --channel-id 123 --label "nightly-report" --max-uses 365 --expires 365d
8129
+ # Then in your cron: curl -X POST -d '{"content":"Nightly report ready"}' <url>
8130
+ \`\`\`
8131
+
8132
+ **One-shot welcome DM from a sign-up form:**
8133
+ \`\`\`bash
8134
+ calabasas webhook create --bot <id> --action sendDM \\
8135
+ --user-id <id> --max-uses 1 --expires 1h
8136
+ \`\`\`
8137
+
8138
+ **Zapier "post to Discord" step:** create a \`sendChannelMessage\` webhook, paste URL into Zapier's webhooks-by-Zapier action with body \`{"content": "{{trigger field}}"}\`.
8139
+
8140
+ ## Authentication
8141
+
8142
+ CLI commands resolve credentials in this order:
8143
+ 1. \`CALABASAS_PLATFORM_API_KEY\` in \`.env.local\` (preferred — \`cpk_\` prefix).
8144
+ 2. User API key from \`~/.calabasas/config.json\` (fallback — \`clb_\` prefix).
8145
+
8146
+ Run \`calabasas login\` (user key) or \`calabasas platform create\` / \`calabasas platform connect\` (platform key) if neither is set.
8147
+
8148
+ ## Anti-patterns
8149
+
8150
+ - **Don't bake secrets into the URL beyond the token** — the token *is* the credential.
8151
+ - **Don't share one webhook across many services** — one URL per integration so revoke kills exactly one consumer.
8152
+ - **Don't substitute webhooks for the SDK** when the caller is your own Convex backend; the generated \`discord.actions.ts\` is faster and richer.
8153
+ `;
8154
+ async function webhookSkill(options) {
8155
+ const target = options.output ?? DEFAULT_SKILL_PATH;
8156
+ const fullPath = path9.resolve(process.cwd(), target);
8157
+ const dir = path9.dirname(fullPath);
8158
+ if (fs9.existsSync(fullPath) && !options.force) {
8159
+ console.error(pc5.red("Error:") + ` ${target} already exists.
8160
+ ` + " Re-run with --force to overwrite, or pass --output <path> to write elsewhere.");
8161
+ process.exit(1);
8162
+ }
8163
+ if (!fs9.existsSync(dir)) {
8164
+ fs9.mkdirSync(dir, { recursive: true });
8165
+ }
8166
+ fs9.writeFileSync(fullPath, SKILL_CONTENT);
8167
+ console.log(pc5.green("Installed calabasas-webhook skill at ") + pc5.cyan(target));
8168
+ console.log(pc5.dim("Claude Code will pick it up on next start. The skill triggers when the user wants to fire a single Discord action from an external service."));
8169
+ }
8170
+
7837
8171
  // src/index.ts
7838
8172
  var dashboard = async () => {
7839
8173
  const mod = await import("./dashboard-a862sabz.js");
@@ -7870,4 +8204,9 @@ platformCmd.command("info").description("Show platform info (secret, URL, API ke
7870
8204
  platformCmd.command("set-url").description("Set Convex URL for your platform").action(platformSetUrl);
7871
8205
  platformCmd.command("rotate-key").description("Rotate your platform API key (for SDK access)").action(platformRotateKey);
7872
8206
  platformCmd.command("rotate-secret").description("Rotate the platform secret (invalidates CALABASAS_SECRET)").action(platformRotateSecret);
8207
+ var webhookCmd = program2.command("webhook").description("Manage one-off webhook URLs that fire pre-bound Discord actions");
8208
+ webhookCmd.command("create").description("Create a webhook URL bound to a single bot, action, and target").requiredOption("-b, --bot <botId>", "Bot ID that will perform the action").requiredOption("-a, --action <action>", "Action to fire: sendDM | sendChannelMessage").option("--user-id <discordUserId>", "Target user ID (required for sendDM)").option("--channel-id <discordChannelId>", "Target channel ID (required for sendChannelMessage)").option("--label <label>", "Human-readable label for listing").option("--max-uses <number>", "Max times this URL can be fired before it stops working").option("--expires <duration>", "Auto-expire after duration: e.g. 30m, 24h, 7d").option("--platform-id <id>", "Platform ID (only needed with user key + multiple platforms)").action(webhookCreate);
8209
+ webhookCmd.command("list").alias("ls").description("List all non-revoked webhooks for the current platform").option("--json", "Output raw JSON instead of a table").action(webhookList);
8210
+ webhookCmd.command("revoke <token>").description("Revoke a webhook by token (soft delete)").option("-y, --yes", "Skip confirmation prompt (currently a no-op)").action(webhookRevoke);
8211
+ webhookCmd.command("skill").description("Install the calabasas-webhook Claude Code skill in this project").option("-o, --output <path>", "Output path", ".claude/skills/calabasas-webhook/SKILL.md").option("-f, --force", "Overwrite if the file already exists").action(webhookSkill);
7873
8212
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calabasas",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "CLI for Calabasas - Discord Gateway as a Service for Convex",
5
5
  "type": "module",
6
6
  "bin": {