@sendbird/cli 0.0.0-stage → 0.4.0

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 (182) hide show
  1. package/.claude-plugin/plugin.json +11 -0
  2. package/.codex-plugin/plugin.json +39 -0
  3. package/LICENSE +13 -0
  4. package/NOTICE.md +1105 -0
  5. package/PROVENANCE.md +15 -0
  6. package/README.md +44 -2
  7. package/dist/acl.d.ts +2 -0
  8. package/dist/acl.js +12 -0
  9. package/dist/acl.js.map +1 -0
  10. package/dist/billing-plan.d.ts +68 -0
  11. package/dist/billing-plan.js +235 -0
  12. package/dist/billing-plan.js.map +1 -0
  13. package/dist/browser.d.ts +1 -0
  14. package/dist/browser.js +20 -0
  15. package/dist/browser.js.map +1 -0
  16. package/dist/calls-catalog.d.ts +79 -0
  17. package/dist/calls-catalog.js +322 -0
  18. package/dist/calls-catalog.js.map +1 -0
  19. package/dist/calls-credits.d.ts +31 -0
  20. package/dist/calls-credits.js +224 -0
  21. package/dist/calls-credits.js.map +1 -0
  22. package/dist/calls-trial.d.ts +45 -0
  23. package/dist/calls-trial.js +77 -0
  24. package/dist/calls-trial.js.map +1 -0
  25. package/dist/calls.d.ts +10 -0
  26. package/dist/calls.js +470 -0
  27. package/dist/calls.js.map +1 -0
  28. package/dist/catalog.d.ts +67 -0
  29. package/dist/catalog.js +784 -0
  30. package/dist/catalog.js.map +1 -0
  31. package/dist/cli.d.ts +6 -0
  32. package/dist/cli.js +502 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/core.d.ts +86 -0
  35. package/dist/core.js +531 -0
  36. package/dist/core.js.map +1 -0
  37. package/dist/diagnostics.d.ts +206 -0
  38. package/dist/diagnostics.js +664 -0
  39. package/dist/diagnostics.js.map +1 -0
  40. package/dist/discovery.d.ts +703 -0
  41. package/dist/discovery.js +846 -0
  42. package/dist/discovery.js.map +1 -0
  43. package/dist/docs.d.ts +31 -0
  44. package/dist/docs.js +139 -0
  45. package/dist/docs.js.map +1 -0
  46. package/dist/features.d.ts +131 -0
  47. package/dist/features.js +227 -0
  48. package/dist/features.js.map +1 -0
  49. package/dist/generated-api.d.ts +116 -0
  50. package/dist/generated-api.js +118 -0
  51. package/dist/generated-api.js.map +1 -0
  52. package/dist/human-format.d.ts +40 -0
  53. package/dist/human-format.js +205 -0
  54. package/dist/human-format.js.map +1 -0
  55. package/dist/human-list.d.ts +2 -0
  56. package/dist/human-list.js +368 -0
  57. package/dist/human-list.js.map +1 -0
  58. package/dist/human-output.d.ts +10 -0
  59. package/dist/human-output.js +1351 -0
  60. package/dist/human-output.js.map +1 -0
  61. package/dist/human-pro.d.ts +3 -0
  62. package/dist/human-pro.js +201 -0
  63. package/dist/human-pro.js.map +1 -0
  64. package/dist/human-progress.d.ts +5 -0
  65. package/dist/human-progress.js +49 -0
  66. package/dist/human-progress.js.map +1 -0
  67. package/dist/local.d.ts +42 -0
  68. package/dist/local.js +707 -0
  69. package/dist/local.js.map +1 -0
  70. package/dist/oauth-callback.d.ts +3 -0
  71. package/dist/oauth-callback.js +66 -0
  72. package/dist/oauth-callback.js.map +1 -0
  73. package/dist/oauth-lock.d.ts +6 -0
  74. package/dist/oauth-lock.js +212 -0
  75. package/dist/oauth-lock.js.map +1 -0
  76. package/dist/oauth.d.ts +32 -0
  77. package/dist/oauth.js +493 -0
  78. package/dist/oauth.js.map +1 -0
  79. package/dist/presentation.d.ts +208 -0
  80. package/dist/presentation.js +156 -0
  81. package/dist/presentation.js.map +1 -0
  82. package/dist/pro-api-catalog.d.ts +24 -0
  83. package/dist/pro-api-catalog.js +619 -0
  84. package/dist/pro-api-catalog.js.map +1 -0
  85. package/dist/pro-api.d.ts +10 -0
  86. package/dist/pro-api.js +338 -0
  87. package/dist/pro-api.js.map +1 -0
  88. package/dist/project.d.ts +34 -0
  89. package/dist/project.js +62 -0
  90. package/dist/project.js.map +1 -0
  91. package/dist/recovery.d.ts +41 -0
  92. package/dist/recovery.js +417 -0
  93. package/dist/recovery.js.map +1 -0
  94. package/dist/runner.d.ts +47 -0
  95. package/dist/runner.js +603 -0
  96. package/dist/runner.js.map +1 -0
  97. package/dist/sdk-catalog.d.ts +69 -0
  98. package/dist/sdk-catalog.js +527 -0
  99. package/dist/sdk-catalog.js.map +1 -0
  100. package/dist/sdk-config.d.ts +19 -0
  101. package/dist/sdk-config.js +85 -0
  102. package/dist/sdk-config.js.map +1 -0
  103. package/dist/sdk-dto.d.ts +244 -0
  104. package/dist/sdk-dto.js +98 -0
  105. package/dist/sdk-dto.js.map +1 -0
  106. package/dist/sdk-limits.d.ts +2 -0
  107. package/dist/sdk-limits.js +3 -0
  108. package/dist/sdk-limits.js.map +1 -0
  109. package/dist/sdk-node-file-reader.d.ts +6 -0
  110. package/dist/sdk-node-file-reader.js +38 -0
  111. package/dist/sdk-node-file-reader.js.map +1 -0
  112. package/dist/sdk-pro-catalog.d.ts +22 -0
  113. package/dist/sdk-pro-catalog.js +234 -0
  114. package/dist/sdk-pro-catalog.js.map +1 -0
  115. package/dist/sdk-pro-runtime.d.ts +31 -0
  116. package/dist/sdk-pro-runtime.js +293 -0
  117. package/dist/sdk-pro-runtime.js.map +1 -0
  118. package/dist/sdk-query.d.ts +10 -0
  119. package/dist/sdk-query.js +29 -0
  120. package/dist/sdk-query.js.map +1 -0
  121. package/dist/sdk-runtime.d.ts +15 -0
  122. package/dist/sdk-runtime.js +685 -0
  123. package/dist/sdk-runtime.js.map +1 -0
  124. package/dist/sdk-worker.d.ts +1 -0
  125. package/dist/sdk-worker.js +26 -0
  126. package/dist/sdk-worker.js.map +1 -0
  127. package/dist/sdk.d.ts +29 -0
  128. package/dist/sdk.js +346 -0
  129. package/dist/sdk.js.map +1 -0
  130. package/dist/search-engine.d.ts +17 -0
  131. package/dist/search-engine.js +54 -0
  132. package/dist/search-engine.js.map +1 -0
  133. package/dist/search-intents.d.ts +2 -0
  134. package/dist/search-intents.js +106 -0
  135. package/dist/search-intents.js.map +1 -0
  136. package/dist/search.d.ts +7 -0
  137. package/dist/search.js +58 -0
  138. package/dist/search.js.map +1 -0
  139. package/dist/secret-output.d.ts +2 -0
  140. package/dist/secret-output.js +108 -0
  141. package/dist/secret-output.js.map +1 -0
  142. package/dist/shell.d.ts +3 -0
  143. package/dist/shell.js +14 -0
  144. package/dist/shell.js.map +1 -0
  145. package/dist/skills.d.ts +210 -0
  146. package/dist/skills.js +374 -0
  147. package/dist/skills.js.map +1 -0
  148. package/dist/value-semantics.d.ts +70 -0
  149. package/dist/value-semantics.js +173 -0
  150. package/dist/value-semantics.js.map +1 -0
  151. package/dist/version.d.ts +1 -0
  152. package/dist/version.js +3 -0
  153. package/dist/version.js.map +1 -0
  154. package/dist/windows.d.ts +4 -0
  155. package/dist/windows.js +136 -0
  156. package/dist/windows.js.map +1 -0
  157. package/dist/workflows.d.ts +228 -0
  158. package/dist/workflows.js +1207 -0
  159. package/dist/workflows.js.map +1 -0
  160. package/package.json +90 -4
  161. package/plugin.json +25 -0
  162. package/references/agent-guide.json +199 -0
  163. package/references/commands.md +247 -0
  164. package/references/dashboard-inventory.json +14796 -0
  165. package/references/docs-index.json +5329 -0
  166. package/references/sdk-commands.json +15065 -0
  167. package/references/workflows.json +1246 -0
  168. package/skills/sendbird/SKILL.md +96 -0
  169. package/skills/sendbird/agents/openai.yaml +4 -0
  170. package/skills/sendbird/references/build-chat-app.md +50 -0
  171. package/skills/sendbird/references/client-guide.md +34 -0
  172. package/skills/sendbird/references/client-plugin-bridge.md +77 -0
  173. package/skills/sendbird/references/client-plugin-install.md +39 -0
  174. package/skills/sendbird/references/operational-checks.md +35 -0
  175. package/skills/sendbird/references/pro-capabilities.md +52 -0
  176. package/skills/sendbird/scripts/client-guide.mjs +283 -0
  177. package/skills/sendbird/scripts/client-plugin-bridge.mjs +703 -0
  178. package/skills/sendbird-setup/SKILL.md +18 -0
  179. package/skills/sendbird-setup/agents/openai.yaml +4 -0
  180. package/skills/sendbird-setup/references/onboarding.md +73 -0
  181. package/skills/sendbird-setup/scripts/install-cli.mjs +447 -0
  182. package/skills/sendbird-setup/scripts/npm-run.mjs +34 -0
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: sendbird-setup
3
+ description: Install or update Sendbird CLI for Claude Code or Codex, connect an account in the selected environment, select an existing organization and app, ask about saving defaults, and offer an optional authorized chat practice or client-plugin handoff. Use when Sendbird is missing or the user asks to connect or get started.
4
+ ---
5
+
6
+ Prepare a working Sendbird connection and verify account/app read access. Chat practice is optional; setup alone does not authorize resource creation. Installing a plugin, authenticating, sending an SDK message and completing a customer app are separate outcomes. Reuse the user's working installation and login when possible. Read [installation and first task](references/onboarding.md) for the complete procedure and per-host commands.
7
+
8
+ 1. **Check the installation.** Resolve the actual executable, Node version and `sendbird --version`, then run `sendbird --json agent`. Inspect the commands needed for the task. An equal version does not prove an old npm link is current. If missing/incompatible, run this skill's `scripts/install-cli.mjs` with Node, resolving its absolute path from the installed skill location. It prints JSON with a verified executable; use that path for later commands or add its bin directory to this session's PATH. Never invent an npm release, overwrite an unmanaged executable, or edit shell rc files without a separate request. If project Skill registration is denied because the host protects `.agents` or `.claude`, give the exact `sendbird skills install --agent HOST --scope project` command for the person’s normal terminal. Do not retry through a different path, create custom permission rules, or disable the host sandbox. Restart or reload the host after registration and check actual Skill consumption.
9
+ 2. **Connect the intended environment.** CLI browser OAuth supports the selected prod/staging profile where Gate advertises and enables its CLI client. Use the requested profile/environment explicitly; ask if the intended target is unclear. Run `--profile PROFILE --json auth status --summary`. If missing/expired, start `auth login` and wait for the person to approve in their browser, then recheck. Do not request passwords, cookies, callback codes or tokens in chat. Explain OS credential store failures; there is no credential-file fallback. Do not rerun login after a role denial.
10
+ 3. **Select existing resources.** List the accessible organizations and apps, confirm the intended organization and exact app, then use explicit IDs on commands. Ask whether to save this exact organization/app as defaults in the selected profile or use them only for this session. Reuse a choice already supplied; do not silently choose either. Inspect `--profile PROFILE profile` before saving. `org use` clears the saved app and region, so skip it when the same org is already saved; use `app use` to save the app. Ask before making the profile globally active with `profile use`; storing context and activating a profile are separate choices. Read the profile back and report what was saved. If declined, keep explicit IDs on commands and report that defaults were not changed. No results or ambiguous results need a user choice. Do not create an organization/app as a setup shortcut. If the user wants an account or new resources, route to the CLI's `new-account-chat` workflow and preserve human signup/terms approval.
11
+ 4. **Offer optional Chat practice.** After read-only connection checks, offer to finish setup or run a two-user Chat practice. Default to finishing without writes until the person authorizes practice. Reuse explicit authorization for this target/task; do not ask again. A generic request to install, connect or set up is not permission to create users/channels, issue tokens, send messages or delete resources. If practice is authorized, read `describe` and the matching workflow, then show the planned target/resources and supported offline previews. A safe Chat practice uses two dedicated test users and a private Group channel in the user-approved app, issues user access tokens explicitly, sends a unique message as each user and reads it as the other. Inventory collisions first and never modify existing customer users/channels to make the demo pass. Write acknowledgement alone is not delivery or read state. Record returned user/channel/message IDs without tokens. Clean up only resources this run created when authorized. With no permission for writes, stop after read-only checks and previews and label practice unexecuted.
12
+ 5. **Choose assistance only when customer code is requested.** Read the sibling Skill's [three app-assistance modes](../sendbird/references/build-chat-app.md). Offer official plugin information/installation, pinned guide-only assistance in this conversation, or no client assistance; reuse an explicit selection for the same task. Do not guess or auto-switch. Show the notice summary and publisher README link before installation/execution guidance. Native installation/loading can remain pending; the separate advanced ZIP route starts a separate edit-only Claude process only after an explicit ZIP request and emits the notice before launch. Preserve prior guide-only selections; never treat them as permission to launch a model. CLI setup does not require a client plugin.
13
+
14
+ Report the executable/version, plugin loaded, authentication status, verified org/app, saved defaults (or session-only context), optional practice completed/skipped/pending, first-task checks and customer-app handoff separately. Keep pending human approvals and server/account restrictions visible. Do not label a dry-run, mock, plugin install or build as first-chat success. Billing changes stay in Dashboard; never use onboarding as permission to buy credits, change a plan, start a trial, enroll 2FA or enable guest access.
15
+
16
+ ## Windows execution
17
+
18
+ Use Windows 11 x64 with Node 22.23.4+ (22.x) or 24.22.1+ (24.x) and PowerShell 7 for native Windows work. Prefer npm's generated PowerShell shim; keep command arguments as an array. Generated `describe` examples on Windows use PowerShell quoting; Bash backslash continuations and environment assignments are not portable. If organization policy blocks a `.ps1` shim, use `node` with the installed `dist/cli.js` and separate arguments. Do not disable execution policy or elevate privileges automatically. Login uses Windows Credential Manager; if unavailable, stop and report the storage error. Never fall back to credential files or request tokens in chat. Git Bash still uses Windows Node; WSL is a separate Linux environment requiring Secret Service. Actual browser approval and complete customer app validation are separate from CI.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Set up Sendbird"
3
+ short_description: "Connect the CLI, choose defaults, and offer optional chat practice"
4
+ default_prompt: "Use $sendbird-cli:sendbird-setup to connect my coding agent to Sendbird, ask whether to save defaults, and offer optional Chat practice without creating resources until authorized."
@@ -0,0 +1,73 @@
1
+ # Install and connect Sendbird
2
+
3
+ The npm package is `@sendbird/cli`; the executable is `sendbird`. Node.js 22.23.4+ (22.x) or 24.22.1+ (24.x) is required. Install the published package with npm; use an approved bundled artifact when testing an unpublished candidate. The installer checks the exact plugin version and required command capabilities, prefers a checksum-verified bundled CLI archive, and otherwise tries npm before building a temporary copy of a trusted Git-installed plugin checkout. It never stores Sendbird credentials or changes shell rc files. Duplicate flags or missing option values are rejected before installation. An incomplete cached install is rechecked using the returned executable, including its npm bin link.
4
+
5
+ Resolve `../scripts/install-cli.mjs` relative to this reference's skill folder:
6
+
7
+ ```sh
8
+ node /ABSOLUTE/INSTALLED/SKILL/scripts/install-cli.mjs --bin-dir "$HOME/.local/bin"
9
+ ```
10
+
11
+ Use the returned `executable` or prepend the returned bin directory to PATH for the current session. If that bin directory contains an unmanaged command, use another directory or the prefix executable; do not remove the existing file. `--package-file /path/to/sendbird-cli.tgz` supports an explicitly supplied trusted CLI archive. `--source-root /path/to/sendbird-cli` supports an approved source checkout when npm is unavailable. `--prefix /path/to/cache` isolates installation without touching an existing CLI. A matching version with missing required commands (including skills install/status/show/remove) fails verification. CLI version strings alone do not establish that the current Skill-registration interface exists.
12
+
13
+ ## Native plugin installation
14
+
15
+ The shared `sendbird/sendbird-plugins` repository owns the `sendbird` marketplace for both CLI and client plugins. Once its catalogs are available, hosts with repository access can install the CLI plugin:
16
+
17
+ ```sh
18
+ # Claude Code
19
+ claude plugin marketplace add sendbird/sendbird-plugins
20
+ claude plugin install sendbird-cli@sendbird
21
+ # Start a new session, then invoke /sendbird-cli:sendbird-setup.
22
+
23
+ # Codex
24
+ codex plugin marketplace add sendbird/sendbird-plugins
25
+ codex plugin add sendbird-cli@sendbird
26
+ # Start a new session, then use $sendbird-cli:sendbird-setup.
27
+ ```
28
+
29
+ For an unpublished branch or offline alpha artifact, add the absolute generated marketplace directory and install `sendbird-cli@sendbird-cli-local` instead. The local catalog uses a separate name so it can coexist with the shared `sendbird` marketplace. `npm run plugins:build` creates a self-contained marketplace with both host catalogs, a CLI tarball and its checksum. This is private/local distribution, not publication in either host's public directory. GitHub access and OAuth approval remain separate prerequisites. Installation does not run authentication or mutate Sendbird resources.
30
+
31
+ ## Registering Skills from a sandboxed Agent
32
+
33
+ Codex can protect `.agents` and `.codex` even when the project is otherwise writable. A denied `skills install` is a local host-permission issue, not a Sendbird account or server error. Run the CLI registration from a normal terminal outside that sandbox, then start a fresh Agent session. Do not repeatedly create the directory, write to another hidden folder, relax sandbox rules, or bypass a denied command through the underlying Node entrypoint. File registration and actual host loading are separate checks.
34
+
35
+ ## Agent-led account and app selection
36
+
37
+ Use the installed CLI path in place of `sendbird` when PATH still points to an old link:
38
+
39
+ ```sh
40
+ sendbird --profile staging --json auth status --summary
41
+ # Only if missing/expired; the person approves the browser screen.
42
+ sendbird --profile staging auth login
43
+ sendbird --profile staging --json auth status --summary
44
+ sendbird --profile staging --json org list
45
+ sendbird --profile staging --org-id ORG_UID --json app list
46
+ sendbird --profile staging --org-id ORG_UID --app-id APP_ID --json app get
47
+ ```
48
+
49
+ List pages must be followed far enough to find the user's intended target. Never guess an app ID from its name or reuse another profile's context. Ask whether to persist the exact selection in this profile or keep session-only IDs; reuse an existing answer. Inspect `sendbird --profile PROFILE profile` first. If approved, use `org use --id ORG_UID` only when changing the saved org (it clears app/region), then `app use --id APP_ID`. These are real local changes and do not support dry-run. Verify the result with `profile`. Ask separately before `profile use --name PROFILE` makes the profile globally active. If declined, keep explicit profile/org/app options and report that defaults were not changed. Normal Agent setup uses explicit noninteractive commands; the human `setup` wizard requires a TTY.
50
+
51
+ When a person uses `setup`, it reviews the profile/environment, org, app and
52
+ region before asking to save defaults. Saving also activates the profile.
53
+ Read `defaults_saved` and use the returned `next_argv` when defaults were not
54
+ saved; never infer persistence from setup success alone. Skipping/cancelling
55
+ does not save selections, but sign-in and already-created resources remain.
56
+ Reauthentication may reset the old account context. The default region is for
57
+ future app creation, not migration of the selected app.
58
+
59
+ After read-only setup, offer to finish or run optional Chat practice. Installing/connecting alone does not authorize users, channels, token issuance, messages or cleanup. Until practice is authorized for the selected app, finish without writes and report practice as skipped/pending. Reuse a prior explicit authorization rather than asking again. Once authorized, read `sendbird --json workflows first-message` and `sendbird --json describe chat message send` before preparing the first Chat practice. Use task search if that workflow ID is unavailable in the installed version. User-token issuance is explicit; the default SDK path retrieves an existing token into memory. Do not export it for ordinary CLI practice. Keep the exact app and profile on every operation. Customer UI integration must implement runtime authentication under the selected assistance mode; a Dashboard login is not an end user's Chat identity.
60
+
61
+ ## Completion and recovery
62
+
63
+ - CLI/plugin installed: package and host loader checked; no Sendbird access implied.
64
+ - Connected: auth read succeeded for the intended environment/account.
65
+ - Context verified: exact org/app read succeeded.
66
+ - First Chat verified: the acknowledged messages were independently read as the other test user.
67
+ - Customer app: selected assistance mode, native-plugin/guide source when used, build and live UI checks are separate, reported individually.
68
+
69
+ 401/expired grant requires approval again. 403 requires the stated permission, not account switching. Region/service errors require a compatible approved app. Unknown mutation outcomes require readback before retry. Only delete IDs in this run's created-resource record. Do not publish tokens, private files or raw agent logs in the completion report.
70
+
71
+ This flow borrows Bird's inspect/reuse → browser sign-in → workspace confirmation → first operation → verification sequence. It does not copy Bird's MCP dependency or assume server enablement/approval for production OAuth or quoted paid operations.
72
+
73
+ When reusing the same installer prefix and bin directory, a link from a completed installation is upgraded atomically after the new CLI is verified. The installer checks the previous receipt and cache boundary; merely pointing inside versions/ is not enough. Regular files, foreign links, unreceipted paths and links escaping the cache are preserved. Previous version caches are retained.
@@ -0,0 +1,447 @@
1
+ #!/usr/bin/env node
2
+ import { npmInvocation } from "./npm-run.mjs";
3
+ // Explicit onboarding action, never an install hook. No credentials or shell
4
+ // configuration are read/copied by this helper. npm lifecycle scripts are off.
5
+ import { execFileSync } from "node:child_process";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import {
8
+ existsSync,
9
+ readFileSync,
10
+ writeFileSync,
11
+ mkdirSync,
12
+ mkdtempSync,
13
+ cpSync,
14
+ rmSync,
15
+ symlinkSync,
16
+ lstatSync,
17
+ readlinkSync,
18
+ realpathSync,
19
+ renameSync,
20
+ unlinkSync,
21
+ } from "node:fs";
22
+ import { dirname, join, resolve, relative, isAbsolute } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { homedir, tmpdir } from "node:os";
25
+
26
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
27
+ const run = (exe, args, cwd) => {
28
+ if (exe === "npm") {
29
+ const invocation = npmInvocation(args);
30
+ exe = invocation.command;
31
+ args = invocation.args;
32
+ } else if (process.platform === "win32" && exe.endsWith("sendbird.cmd")) {
33
+ args = [
34
+ join(dirname(exe), "..", "@sendbird", "cli", "dist", "cli.js"),
35
+ ...args,
36
+ ];
37
+ exe = process.execPath;
38
+ }
39
+ return execFileSync(exe, args, {
40
+ cwd,
41
+ encoding: "utf8",
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ timeout: 180_000,
44
+ });
45
+ };
46
+ const json = (file) => JSON.parse(readFileSync(file, "utf8"));
47
+ const digest = (file) =>
48
+ createHash("sha256").update(readFileSync(file)).digest("hex");
49
+ function fail(code, message) {
50
+ const e = Error(message);
51
+ e.code = code;
52
+ throw e;
53
+ }
54
+ function inside(base, path) {
55
+ const r = relative(base, path);
56
+ return r && !r.startsWith("..") && !isAbsolute(r);
57
+ }
58
+ // A path inside versions/ is not ownership evidence by itself. Require the
59
+ // receipt from a completed install and reject cache symlinks escaping the root.
60
+ export function isManagedExecutable(target, dataRoot) {
61
+ try {
62
+ const prefix = resolve(dirname(target), "../..");
63
+ if (
64
+ !inside(join(dataRoot, "versions"), prefix) ||
65
+ target !==
66
+ join(
67
+ prefix,
68
+ "node_modules",
69
+ ".bin",
70
+ process.platform === "win32" ? "sendbird.cmd" : "sendbird",
71
+ )
72
+ )
73
+ return false;
74
+ const receipt = json(join(prefix, "installation.json"));
75
+ return (
76
+ receipt.ok === true &&
77
+ receipt.installed === true &&
78
+ typeof receipt.version === "string" &&
79
+ /^[a-f0-9]{64}$/.test(receipt.sha256) &&
80
+ prefix ===
81
+ join(
82
+ dataRoot,
83
+ "versions",
84
+ `${receipt.version}-${receipt.sha256.slice(0, 16)}`,
85
+ ) &&
86
+ receipt.executable === target &&
87
+ inside(realpathSync(dataRoot), realpathSync(target))
88
+ );
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ export function publishExecutable(link, executable, dataRoot) {
94
+ let existing;
95
+ try {
96
+ existing = lstatSync(link);
97
+ } catch (e) {
98
+ if (e.code !== "ENOENT") throw e;
99
+ }
100
+ if (!existing) {
101
+ symlinkSync(executable, link);
102
+ return;
103
+ } // No-replace creation.
104
+ const bin = dirname(link);
105
+ const oldTarget = existing.isSymbolicLink()
106
+ ? resolve(bin, readlinkSync(link))
107
+ : undefined;
108
+ if (oldTarget === executable) return;
109
+ if (!oldTarget || !isManagedExecutable(oldTarget, dataRoot))
110
+ fail(
111
+ "E_INSTALL_CONFLICT",
112
+ `A different sendbird already exists in ${bin}; use the verified executable or choose another bin directory`,
113
+ );
114
+ const pending = join(bin, `.sendbird-${randomUUID()}.tmp`);
115
+ let created = false;
116
+ try {
117
+ symlinkSync(executable, pending);
118
+ created = true;
119
+ // Detect ordinary concurrent replacement before atomic publication. The
120
+ // caller's own bin/cache directories remain a local filesystem trust boundary.
121
+ const current = lstatSync(link);
122
+ if (
123
+ !current.isSymbolicLink() ||
124
+ current.dev !== existing.dev ||
125
+ current.ino !== existing.ino ||
126
+ resolve(bin, readlinkSync(link)) !== oldTarget
127
+ )
128
+ fail(
129
+ "E_INSTALL_CONFLICT",
130
+ "sendbird changed during installation; inspect the existing link before retrying",
131
+ );
132
+ renameSync(pending, link);
133
+ } finally {
134
+ if (created)
135
+ try {
136
+ unlinkSync(pending);
137
+ } catch (e) {
138
+ if (e.code !== "ENOENT") throw e;
139
+ }
140
+ }
141
+ }
142
+
143
+ export function installCli(argv = process.argv) {
144
+ const opts = {};
145
+ let stage = "input",
146
+ temporary,
147
+ verifiedExecutable;
148
+ try {
149
+ if (argv.length === 3 && ["--help", "-h"].includes(argv[2])) {
150
+ process.stdout.write(
151
+ "Install a verified Sendbird CLI for agent onboarding.\nOptions: --package-file FILE --source-root DIR --prefix DIR --bin-dir DIR\nNo shell rc changes; an existing unmanaged executable is never overwritten.\n",
152
+ );
153
+ } else {
154
+ for (let i = 2; i < argv.length; i += 2) {
155
+ if (
156
+ ![
157
+ "--package-file",
158
+ "--source-root",
159
+ "--prefix",
160
+ "--bin-dir",
161
+ ].includes(argv[i]) ||
162
+ !argv[i + 1] ||
163
+ argv[i + 1].startsWith("--") ||
164
+ Object.hasOwn(opts, argv[i])
165
+ )
166
+ fail("E_INPUT", "Use --help for supported installation options");
167
+ opts[argv[i]] = resolve(argv[i + 1]);
168
+ }
169
+ const version = process.versions.node;
170
+ const [major, minor, patch] = version.split(".").map(Number);
171
+ const supported =
172
+ /^\d+\.\d+\.\d+$/.test(version) &&
173
+ ((major === 22 && (minor > 23 || (minor === 23 && patch >= 4))) ||
174
+ (major === 24 && (minor > 22 || (minor === 22 && patch >= 1))));
175
+ if (!supported)
176
+ fail(
177
+ "E_NODE_VERSION",
178
+ "Sendbird CLI requires Node.js 22.23.4+ (22.x) or 24.22.1+ (24.x); update Node.js before installing",
179
+ );
180
+ const source = opts["--source-root"] ?? root;
181
+ const expected = json(join(root, "plugin.json")).version;
182
+ const dataRoot =
183
+ opts["--prefix"] ??
184
+ (process.platform === "win32"
185
+ ? join(process.env.LOCALAPPDATA ?? homedir(), "Sendbird", "CLI")
186
+ : join(homedir(), ".local", "share", "sendbird-cli"));
187
+ temporary = mkdtempSync(join(tmpdir(), "sendbird-cli-install-"));
188
+ let archive = opts["--package-file"],
189
+ origin = "explicit_archive";
190
+ if (!archive && existsSync(join(root, "release-manifest.json"))) {
191
+ stage = "integrity";
192
+ const manifest = json(join(root, "release-manifest.json"));
193
+ archive = resolve(root, manifest.cli.file);
194
+ if (
195
+ !inside(root, archive) ||
196
+ !/^[a-f0-9]{64}$/.test(manifest.cli.sha256) ||
197
+ digest(archive) !== manifest.cli.sha256
198
+ )
199
+ fail(
200
+ "E_INTEGRITY",
201
+ "Bundled CLI checksum is invalid; obtain the original plugin archive",
202
+ );
203
+ origin = "bundled_archive";
204
+ }
205
+ if (!archive) {
206
+ stage = "registry";
207
+ try {
208
+ const packed = JSON.parse(
209
+ run("npm", [
210
+ "pack",
211
+ `@sendbird/cli@${expected}`,
212
+ "--ignore-scripts",
213
+ "--json",
214
+ "--pack-destination",
215
+ temporary,
216
+ ]),
217
+ );
218
+ archive = join(temporary, packed[0].filename);
219
+ origin = "npm";
220
+ } catch {
221
+ // Private alpha: a Git-installed plugin can build its own trusted
222
+ // checkout in a temporary copy, without mutating the host's cache.
223
+ if (!existsSync(join(source, "src", "cli.ts")))
224
+ fail(
225
+ "E_INSTALL_UNAVAILABLE",
226
+ "This CLI version is not available from npm. Use a CLI/plugin release archive or a trusted source checkout with --source-root",
227
+ );
228
+ stage = "source_build";
229
+ const build = join(temporary, "source");
230
+ mkdirSync(build);
231
+ for (const path of [
232
+ "src",
233
+ "skills",
234
+ "references",
235
+ "package.json",
236
+ "package-lock.json",
237
+ "tsconfig.json",
238
+ "README.md",
239
+ "PROVENANCE.md",
240
+ "LICENSE",
241
+ "NOTICE.md",
242
+ "plugin.json",
243
+ ".claude-plugin",
244
+ ".codex-plugin",
245
+ ".agents",
246
+ ])
247
+ if (existsSync(join(source, path)))
248
+ cpSync(join(source, path), join(build, path), {
249
+ recursive: true,
250
+ });
251
+ mkdirSync(join(build, "scripts"));
252
+ cpSync(
253
+ join(source, "scripts/build.mjs"),
254
+ join(build, "scripts/build.mjs"),
255
+ );
256
+ const metadata = json(join(build, "package.json"));
257
+ if (
258
+ metadata.name !== "@sendbird/cli" ||
259
+ metadata.version !== expected
260
+ )
261
+ fail(
262
+ "E_VERSION",
263
+ "Source CLI identity/version differs from this plugin",
264
+ );
265
+ run(
266
+ "npm",
267
+ ["ci", "--ignore-scripts", "--no-audit", "--no-fund"],
268
+ build,
269
+ );
270
+ run("npm", ["run", "build"], build);
271
+ const packed = JSON.parse(
272
+ run(
273
+ "npm",
274
+ [
275
+ "pack",
276
+ "--ignore-scripts",
277
+ "--json",
278
+ "--pack-destination",
279
+ temporary,
280
+ ],
281
+ build,
282
+ ),
283
+ );
284
+ archive = join(temporary, packed[0].filename);
285
+ origin = "plugin_source";
286
+ }
287
+ }
288
+ stage = "package";
289
+ const metadata = JSON.parse(
290
+ run("tar", ["-xOf", archive, "package/package.json"]),
291
+ );
292
+ if (metadata.name !== "@sendbird/cli" || metadata.version !== expected)
293
+ fail(
294
+ "E_VERSION",
295
+ "CLI archive identity/version differs from this plugin",
296
+ );
297
+ const sha256 = digest(archive);
298
+ const prefix = join(
299
+ dataRoot,
300
+ "versions",
301
+ `${expected}-${sha256.slice(0, 16)}`,
302
+ );
303
+ const cli = join(
304
+ prefix,
305
+ "node_modules",
306
+ "@sendbird",
307
+ "cli",
308
+ "dist",
309
+ "cli.js",
310
+ );
311
+ stage = "install";
312
+ mkdirSync(prefix, { recursive: true, mode: 0o700 });
313
+ const executable = join(
314
+ prefix,
315
+ "node_modules",
316
+ ".bin",
317
+ process.platform === "win32" ? "sendbird.cmd" : "sendbird",
318
+ );
319
+ if (!existsSync(cli) || !existsSync(executable))
320
+ run("npm", [
321
+ "install",
322
+ "--prefix",
323
+ prefix,
324
+ "--omit=dev",
325
+ "--ignore-scripts",
326
+ "--no-audit",
327
+ "--no-fund",
328
+ archive,
329
+ ]);
330
+ stage = "verify";
331
+ const guide = JSON.parse(run(executable, ["--json", "agent"]));
332
+ if (guide.version !== expected)
333
+ fail("E_VERSION", "Installed CLI version does not match");
334
+ const names = new Set();
335
+ for (let offset = 0; offset < 500; offset += 100) {
336
+ const page = JSON.parse(
337
+ run(executable, [
338
+ "--json",
339
+ "commands",
340
+ "--compact",
341
+ "--limit",
342
+ "100",
343
+ "--offset",
344
+ String(offset),
345
+ ]),
346
+ );
347
+ for (const d of page.commands) names.add(d.command);
348
+ if (page.next_offset == null) break;
349
+ }
350
+ for (const name of [
351
+ "skills install",
352
+ "skills status",
353
+ "skills show",
354
+ "skills remove",
355
+ "auth login",
356
+ "org list",
357
+ "app list",
358
+ "chat channel create",
359
+ "chat message send",
360
+ "billing plan check",
361
+ "calls settings general get",
362
+ ])
363
+ if (!names.has(`sendbird ${name}`))
364
+ fail("E_VERSION", "Installed CLI lacks required onboarding commands");
365
+ verifiedExecutable = executable;
366
+ let binDir = opts["--bin-dir"];
367
+ if (binDir) {
368
+ stage = "link";
369
+ mkdirSync(binDir, { recursive: true });
370
+ if (process.platform === "win32") {
371
+ // Shims require no symlink privilege. Never replace an unrelated file.
372
+ const launchers = {
373
+ "sendbird.cmd": `@echo off\r\nsetlocal DisableDelayedExpansion\r\nchcp 65001 >nul\r\nrem Sendbird CLI managed launcher\r\n"${process.execPath}" "${cli}" %*\r\n`,
374
+ "sendbird.ps1": `# Sendbird CLI managed launcher\r\n& '${process.execPath.replaceAll("'", "''")}' '${cli.replaceAll("'", "''")}' @args\r\nexit $LASTEXITCODE\r\n`,
375
+ };
376
+ // cmd expands % and interprets control characters even inside quoted paths.
377
+ if (/[%%\r\n"]/u.test(process.execPath + cli))
378
+ fail(
379
+ "E_INPUT",
380
+ "Windows launcher paths cannot contain percent, quotes or newlines",
381
+ );
382
+ for (const name of Object.keys(launchers)) {
383
+ const path = join(binDir, name);
384
+ if (existsSync(path))
385
+ fail(
386
+ "E_LINK_EXISTS",
387
+ "A Windows launcher already exists; choose an empty --bin-dir. No launcher was replaced",
388
+ );
389
+ }
390
+ const created = [];
391
+ try {
392
+ for (const [name, body] of Object.entries(launchers)) {
393
+ const path = join(binDir, name);
394
+ writeFileSync(path, body, { flag: "wx" });
395
+ created.push(path);
396
+ }
397
+ } catch (error) {
398
+ for (const path of created) unlinkSync(path);
399
+ throw error;
400
+ }
401
+ } else {
402
+ const link = join(binDir, "sendbird");
403
+ publishExecutable(link, executable, dataRoot);
404
+ }
405
+ }
406
+ const output = {
407
+ ok: true,
408
+ installed: true,
409
+ version: expected,
410
+ source: origin,
411
+ sha256,
412
+ executable,
413
+ bin_dir: binDir ?? null,
414
+ commands_verified: names.size,
415
+ authentication: "not_checked",
416
+ };
417
+ writeFileSync(
418
+ join(prefix, "installation.json"),
419
+ JSON.stringify(output, null, 2) + "\n",
420
+ { mode: 0o600 },
421
+ );
422
+ process.stdout.write(JSON.stringify(output) + "\n");
423
+ }
424
+ } catch (e) {
425
+ process.stderr.write(
426
+ JSON.stringify({
427
+ ok: false,
428
+ ...(verifiedExecutable
429
+ ? { executable: verifiedExecutable, cli_verified: true }
430
+ : {}),
431
+ code: /^E_[A-Z_]+$/.test(e.code ?? "") ? e.code : "E_INSTALL",
432
+ stage,
433
+ message: /^E_[A-Z_]+$/.test(e.code ?? "")
434
+ ? e.message
435
+ : `CLI ${stage} failed; check Node/npm access and retry the explicit installation. No authentication was performed.`,
436
+ }) + "\n",
437
+ );
438
+ process.exitCode = 1;
439
+ } finally {
440
+ if (temporary) rmSync(temporary, { recursive: true, force: true });
441
+ }
442
+ }
443
+ if (
444
+ process.argv[1] &&
445
+ realpathSync(resolve(process.argv[1])) === fileURLToPath(import.meta.url)
446
+ )
447
+ installCli();
@@ -0,0 +1,34 @@
1
+ /** Execute npm without a shell, including Windows' non-executable npm.cmd shim. */
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join, delimiter } from "node:path";
5
+ export function npmInvocation(args) {
6
+ if (process.platform !== "win32") return { command: "npm", args };
7
+ const candidates = [
8
+ process.env.npm_execpath,
9
+ join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
10
+ ...(process.env.PATH ?? "")
11
+ .split(delimiter)
12
+ .map((p) =>
13
+ join(
14
+ p.replace(/^"|"$/g, ""),
15
+ "node_modules",
16
+ "npm",
17
+ "bin",
18
+ "npm-cli.js",
19
+ ),
20
+ ),
21
+ ];
22
+ const cli = candidates.find(
23
+ (p) => p && /[\\/]npm-cli\.js$/i.test(p) && existsSync(p),
24
+ );
25
+ if (!cli)
26
+ throw Error(
27
+ "Cannot locate npm-cli.js; install Node.js with npm and reopen the terminal",
28
+ );
29
+ return { command: process.execPath, args: [cli, ...args] };
30
+ }
31
+ export function runNpm(args, options) {
32
+ const invocation = npmInvocation(args);
33
+ return execFileSync(invocation.command, invocation.args, options);
34
+ }