@murmurly/sdk 0.3.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.
package/AI_GUIDE.md ADDED
@@ -0,0 +1,49 @@
1
+ # AI Coding Agent Integration Guide for @murmurly/sdk
2
+
3
+ For AI coding assistants (Cursor, Claude Code, GitHub Copilot, ChatGPT, Lovable, Bolt.new) adding Murmurly engagement requests to a project.
4
+
5
+ ---
6
+
7
+ ## 1. Keys: what you may see, and what you must never ask for
8
+
9
+ - `murmur_id_…` is a **client id**. For a public app it is the whole key and may appear in page source. You may read it and write it into code.
10
+ - `murmur_secret_…` is a **secret key**. **Never ask the user to paste it into the conversation**, and never write it into code, a prompt, a commit, a log or a URL. If the user pastes one anyway, do not repeat it back; tell them to replace it in Murmurly (App settings → Keys & permissions → Replace secret).
11
+
12
+ To get a secret key into the project, ask the user to run this in their own terminal. It prompts for the key without echoing it, verifies it, writes it to `.env.local` and adds that file to `.gitignore`:
13
+
14
+ ```bash
15
+ npx @murmurly/sdk init
16
+ ```
17
+
18
+ Then read it from the environment: `process.env.MURMURLY_SECRET_KEY`. On a host or in CI, the user sets `MURMURLY_SECRET_KEY` and `MURMURLY_BASE_URL` in the platform's secret store.
19
+
20
+ ## 2. Where the code runs
21
+
22
+ - **Secret key → server only.** API routes, server actions, workers. Never in a client component, and never behind a `VITE_`, `NEXT_PUBLIC_`, `PUBLIC_` or `REACT_APP_` prefix. The SDK throws if a secret key is used in a browser.
23
+ - **Client id → browser is fine.** The page's origin must be on the app's allowed origins in Murmurly. If the browser gets a `403` about the origin, tell the user to add it there.
24
+
25
+ ## 3. The client
26
+
27
+ ```typescript
28
+ import { createMurmurlyClient } from '@murmurly/sdk'
29
+
30
+ export const murmurly = createMurmurlyClient({
31
+ apiKey: process.env.MURMURLY_SECRET_KEY!,
32
+ baseUrl: process.env.MURMURLY_BASE_URL!
33
+ })
34
+ ```
35
+
36
+ ## 4. The flow
37
+
38
+ 1. `await murmurly.eventTypes.list()` returns the types this app was granted: `{ id, name, slug, shortcode, description, color, icon }[]`. An empty list means an admin has not granted any yet.
39
+ 2. `await murmurly.eventTypes.getSchema(slug)` returns `{ eventType, sections, jsonSchema }`. `sections[].attributes[]` are the fields (`key`, `label`, `type`, `required`, `config`); send their values under `attributes` using `key`. Send nothing that is not in `jsonSchema`.
40
+ 3. `await murmurly.eventTypes.availability({ eventType, from, to })` returns `closedDays`, `earliestOpenDate`, `openHours` and the workspace `timeZone`. Advisory: offer open days first, but a closed day is still accepted.
41
+ 4. `await murmurly.engagements.request(payload, { idempotencyKey })` submits. Generate one idempotency key per submission (`crypto.randomUUID()`) and reuse it for that submission's retries.
42
+ 5. `await murmurly.engagements.get(idOrShortcode)` reads its status back (confidential apps with `engagements:read_own`).
43
+
44
+ ## 5. Common mistakes
45
+
46
+ 1. **Hand-writing HTTP calls.** Use the SDK: it sends the key, parses errors into `MurmurlyApiError` (with `code` and `retryAfter`) and types the responses.
47
+ 2. **Guessing attribute names.** Read them from `getSchema`; unknown fields are refused with `422`.
48
+ 3. **Retrying without an idempotency key.** It can create the same request twice.
49
+ 4. **Live schema.** `murmurly.agentGuide()` returns a guide written for this app: its permissions, granted types and a ready tool definition. It never contains a secret. `npx @murmurly/sdk codegen` writes `murmurly.d.ts` with exact payload types.
package/CHANGELOG.md ADDED
@@ -0,0 +1,107 @@
1
+ # @murmurly/sdk
2
+
3
+ Written for whoever installs this package. Each entry says what you have to do
4
+ differently, not what we did.
5
+
6
+ `0.x` means the minor slot carries breaking changes: pin `~0.2.0` if you want
7
+ only fixes, `^0.2.0` if you are happy to follow them.
8
+
9
+ ## 0.3.0
10
+
11
+ **On the public npm registry.** `npm install @murmurly/sdk` works as-is: no
12
+ `.npmrc` entry, no token.
13
+
14
+ **New key format.** New keys say what they are: `murmur_id_…` is a client id,
15
+ `murmur_secret_…` is a secret key. Keys you already have (`pk_live_…`, `app_…`,
16
+ `sk_live_…`) keep working; nothing to change unless you issue a new one.
17
+
18
+ **Breaking — `getSchema()` returns what the API actually sends.** The old type
19
+ promised `eventTypeId`, `eventTypeName`, `eventTypeSlug` and `fields`, which
20
+ were always `undefined`. Read them from `eventType` and `sections[].attributes`:
21
+
22
+ ```diff
23
+ const schema = await client.eventTypes.getSchema('roundtable')
24
+ - schema.eventTypeSlug
25
+ + schema.eventType.slug
26
+ - schema.fields.map((f) => f.name)
27
+ + schema.sections.flatMap((s) => s.attributes.map((a) => a.key))
28
+ ```
29
+
30
+ **Breaking — `baseUrl` is required** in `createMurmurlyClient()` and
31
+ `getAiIntegrationPrompt()`. The old default pointed at a Murmurly development
32
+ server, not yours. Copy the URL from the app's settings page.
33
+
34
+ **Breaking — a secret key refuses to run in a browser.** `createMurmurlyClient()`
35
+ throws when given `murmur_secret_…` (or `sk_live_…`) where `window` exists. Call
36
+ the API from your server, or use a public app's client id in the browser.
37
+
38
+ **Breaking — the CLI is `murmurly`, with commands.** `npx @murmurly/sdk codegen`
39
+ replaces `npx @murmurly/sdk murmurly-codegen` (still accepted). If you installed
40
+ the package and ran `murmurly-codegen` directly, run `murmurly codegen`.
41
+
42
+ - `--key` no longer takes a secret key: it would land in your shell history.
43
+ Run `npx @murmurly/sdk init`, which prompts for it unseen and saves it to a
44
+ git-ignored `.env.local`, or set `MURMURLY_SECRET_KEY`.
45
+ - Environment variables are spelled `MURMURLY_…` now. The misspelt `MCRMURLY_…`
46
+ names still work, with a warning.
47
+
48
+ **`MurmurlyEventType` fields are always present.** `description`, `color`,
49
+ `icon` and `shortcode` were optional in the type but always sent, as `null`
50
+ when empty. They are typed `string | null` now.
51
+
52
+ **New.**
53
+
54
+ - `engagements.request(payload, { idempotencyKey })` makes a retry safe: the
55
+ same key within 24 hours returns the first result, with `replayed: true`.
56
+ - `engagements.list()` and `engagements.get(idOrShortcode)` read back requests
57
+ your app submitted (confidential apps with `engagements:read_own`).
58
+ - `MurmurlyApiError.retryAfter` holds the seconds to wait after a `429`.
59
+ - `getAiIntegrationPrompt()` never writes a secret key into the prompt. It
60
+ points the assistant at `MURMURLY_SECRET_KEY` and `init` instead.
61
+
62
+ ## 0.2.1
63
+
64
+ No API change. This file now ships inside the package, so `node_modules/@murmurly/sdk/CHANGELOG.md`
65
+ tells you what moved without a trip to the repository.
66
+
67
+ ## 0.2.0
68
+
69
+ **Breaking — `engagements.request()` takes one event type field instead of two.**
70
+
71
+ `eventTypeId` and `eventTypeSlug` are gone. Pass `eventType`, which accepts
72
+ either form:
73
+
74
+ ```diff
75
+ await client.engagements.request({
76
+ name: 'Quarterly advisory call',
77
+ email: 'piet@example.com',
78
+ firstName: 'Piet',
79
+ lastName: 'de Vries',
80
+ - eventTypeSlug: 'advisory-call'
81
+ + eventType: 'advisory-call'
82
+ })
83
+ ```
84
+
85
+ It is required now, where the old pair was optional on paper — a request with
86
+ neither was never accepted, so the type says what the API always did.
87
+
88
+ **New — ask whether a type can actually be booked.**
89
+
90
+ `client.eventTypes.availability({ eventType, ... })` returns the type's open
91
+ hours and the days it will refuse, so a form can stop offering a slot the API
92
+ would reject. Advisory, not authoritative: the request endpoint remains the
93
+ thing that decides.
94
+
95
+ ```ts
96
+ const { openHours, closedDays } = await client.eventTypes.availability({
97
+ eventType: 'advisory-call'
98
+ })
99
+ ```
100
+
101
+ New exported types: `EventTypeAvailability`, `OpenHours`, `ClosedDay`,
102
+ `ClosedDayReason`.
103
+
104
+ ## 0.1.0
105
+
106
+ First published release: `eventTypes.list()`, `eventTypes.getSchema()`,
107
+ `engagements.request()`, `agentGuide()`, and the `murmurly-codegen` CLI.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Murmurly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @murmurly/sdk
2
+
3
+ Official TypeScript & JavaScript SDK for the Murmurly Public Ingestion API.
4
+
5
+ Works in the browser for public apps (protected by an origin check) and on servers for confidential apps (protected by a secret key).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @murmurly/sdk
11
+ ```
12
+
13
+ ## Keys
14
+
15
+ Each app in Murmurly has keys that say what they are:
16
+
17
+ | Key | What it is | Where it may live |
18
+ | :---------------- | :---------------------------------- | :----------------------------------------------------- |
19
+ | `murmur_id_…` | Client id. A public app's whole key | Page source, browser bundles. Not a secret. |
20
+ | `murmur_secret_…` | Secret key of a confidential app | Server environment only. Never in code, chat or a URL. |
21
+
22
+ A public app's client id is only accepted from the origins set on the app. A confidential app's id names it but proves nothing: it authenticates with its secret key.
23
+
24
+ ## Store the key
25
+
26
+ Run this in your own terminal. It asks for the base URL and the key, shows nothing as you type the key, checks it against the API, and saves it to `.env.local` (which it adds to `.gitignore`):
27
+
28
+ ```bash
29
+ npx @murmurly/sdk init
30
+ ```
31
+
32
+ In CI or on a host, set `MURMURLY_SECRET_KEY` (or `MURMURLY_CLIENT_ID`) and `MURMURLY_BASE_URL` in the platform's secret store instead.
33
+
34
+ > **Using an AI coding assistant?** Have it ask you to run `init` rather than paste a secret key into the chat. The live agent guide (`client.agentGuide()`) tells assistants the same thing, and never contains a secret.
35
+
36
+ ## Quick start (server, confidential app)
37
+
38
+ ```typescript
39
+ import { createMurmurlyClient } from '@murmurly/sdk'
40
+
41
+ const murmurly = createMurmurlyClient({
42
+ apiKey: process.env.MURMURLY_SECRET_KEY!,
43
+ baseUrl: process.env.MURMURLY_BASE_URL!
44
+ })
45
+
46
+ // 1. The engagement types this app was granted
47
+ const types = await murmurly.eventTypes.list()
48
+
49
+ // 2. The fields a type's request takes
50
+ const { sections, jsonSchema } = await murmurly.eventTypes.getSchema('roundtable')
51
+
52
+ // 3. Which days the workspace can take (advisory)
53
+ const { closedDays, earliestOpenDate } = await murmurly.eventTypes.availability({
54
+ eventType: 'roundtable',
55
+ from: '2026-10-01',
56
+ to: '2026-10-31'
57
+ })
58
+
59
+ // 4. Submit. The idempotency key makes a retry safe: same key, same result, no duplicate.
60
+ const result = await murmurly.engagements.request(
61
+ {
62
+ name: 'Quarterly Strategic Offsite',
63
+ email: 'organizer@company.com',
64
+ firstName: 'Jane',
65
+ lastName: 'Doe',
66
+ eventType: 'roundtable',
67
+ startDate: '2026-10-20',
68
+ startTime: '10:00',
69
+ timeZone: 'America/New_York',
70
+ attributes: { budget: 15000, topic: 'Engineering Leadership' }
71
+ },
72
+ { idempotencyKey: crypto.randomUUID() }
73
+ )
74
+
75
+ console.log('Engagement requested:', result.eventId, result.shortcode)
76
+ if (!result.availability.open) console.log('That date is unlikely to be approved.')
77
+
78
+ // 5. Later: where it stands (needs the engagements:read_own permission)
79
+ const request = await murmurly.engagements.get(result.shortcode)
80
+ ```
81
+
82
+ ## Quick start (browser, public app)
83
+
84
+ ```typescript
85
+ const murmurly = createMurmurlyClient({
86
+ apiKey: import.meta.env.VITE_MURMURLY_CLIENT_ID, // murmur_id_…: fine in a browser bundle
87
+ baseUrl: import.meta.env.VITE_MURMURLY_BASE_URL
88
+ })
89
+ ```
90
+
91
+ The page's origin must be one of the app's allowed origins. From a server or script, pass `origin` explicitly: `createMurmurlyClient({ apiKey, baseUrl, origin: 'https://example.com' })`.
92
+
93
+ The SDK refuses to start with a secret key in a browser.
94
+
95
+ ## Errors
96
+
97
+ Failed calls throw `MurmurlyApiError` with the API's message and its HTTP `code`: `401` bad key, `403` wrong origin, missing permission, type not granted or app paused, `422` a field the type does not take, `429` rate limited (`retryAfter` holds the seconds to wait; retry with the same idempotency key).
98
+
99
+ ## CLI
100
+
101
+ ```bash
102
+ npx @murmurly/sdk init # store the key, hidden prompt
103
+ npx @murmurly/sdk codegen # write murmurly.d.ts for your workspace's types
104
+ npx @murmurly/sdk guide # print the live agent guide for this app
105
+ npx @murmurly/sdk prompt # print a prompt for an AI coding assistant (never contains a secret)
106
+ ```
107
+
108
+ Each command reads the key from the environment or `.env.local`. `--key` accepts a public client id only: a secret key on the command line would end up in your shell history, so it is refused.
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Murmurly CLI.
5
+ *
6
+ * npx @murmurly/sdk init Store this app's key in .env.local, prompting for it unseen
7
+ * npx @murmurly/sdk codegen Write murmurly.d.ts for the workspace's engagement types
8
+ * npx @murmurly/sdk guide Print the live agent guide for this app
9
+ * npx @murmurly/sdk prompt Print a prompt for an AI coding assistant
10
+ *
11
+ * A secret key (`murmur_secret_…`) is only ever read from the environment,
12
+ * from .env.local, or typed at a hidden prompt. It is never accepted as a
13
+ * command-line argument (shell history, process lists, CI logs) and never
14
+ * printed: output shows its first few characters at most.
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, chmodSync } from 'node:fs'
18
+ import { resolve } from 'node:path'
19
+ import process from 'node:process'
20
+ import { createInterface } from 'node:readline/promises'
21
+
22
+ import { getAiIntegrationPrompt, isSecretKey } from '../dist/index.js'
23
+
24
+ const DEFAULT_ENV_FILE = '.env.local'
25
+ const ENV = {
26
+ secret: 'MURMURLY_SECRET_KEY',
27
+ clientId: 'MURMURLY_CLIENT_ID',
28
+ baseUrl: 'MURMURLY_BASE_URL',
29
+ origin: 'MURMURLY_ORIGIN'
30
+ }
31
+ /** Misspelt names 0.2.x read. Still honoured, with a warning, so nothing breaks on upgrade. */
32
+ const LEGACY_ENV = {
33
+ key: 'MCRMURLY_API_KEY',
34
+ baseUrl: 'MCRMURLY_BASE_URL',
35
+ origin: 'MCRMURLY_ORIGIN'
36
+ }
37
+
38
+ const USAGE = `Usage: npx @murmurly/sdk <command> [options]
39
+
40
+ Commands:
41
+ init Store this app's key and base URL in ${DEFAULT_ENV_FILE}. Prompts for the key
42
+ without showing it, checks it against the API, and keeps the file out of git.
43
+ codegen Write TypeScript declarations for your workspace (default: murmurly.d.ts).
44
+ guide Print the live agent guide for this app.
45
+ prompt Print a prompt for an AI coding assistant. Never contains a secret key.
46
+
47
+ Options:
48
+ --url <url> API base URL (else ${ENV.baseUrl}, else ${DEFAULT_ENV_FILE})
49
+ --origin <origin> Origin to send, for a public app's client id
50
+ --key <client-id> A public client id. Secret keys are refused here: use init.
51
+ --env-file <path> Where init stores the key (default: ${DEFAULT_ENV_FILE})
52
+ --out <file> Where codegen writes (default: murmurly.d.ts)`
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Arguments and configuration
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const COMMAND_ALIASES = {
59
+ 'murmurly-codegen': 'codegen',
60
+ '--ai-prompt': 'prompt',
61
+ '--guide': 'guide'
62
+ }
63
+ const COMMANDS = new Set(['init', 'codegen', 'guide', 'prompt', 'help'])
64
+
65
+ function flag(args, name) {
66
+ const index = args.indexOf(name)
67
+ return index !== -1 && index + 1 < args.length ? args[index + 1] : undefined
68
+ }
69
+
70
+ function commandFrom(args) {
71
+ const words = args.filter((a) => !a.startsWith('--') || COMMAND_ALIASES[a])
72
+ const named = words.map((w) => COMMAND_ALIASES[w] ?? w).find((w) => COMMANDS.has(w))
73
+ if (named) return named
74
+ return args.includes('--help') || args.includes('-h') ? 'help' : 'codegen'
75
+ }
76
+
77
+ /** KEY=value lines; quotes stripped. Enough for the file init writes. */
78
+ function readEnvFile(path) {
79
+ if (!existsSync(path)) return {}
80
+ const entries = readFileSync(path, 'utf8')
81
+ .split('\n')
82
+ .map((line) => /^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/.exec(line))
83
+ .filter(Boolean)
84
+ .map(([, key, value]) => [key, value.replace(/^(['"])(.*)\1$/, '$2')])
85
+ return Object.fromEntries(entries)
86
+ }
87
+
88
+ function legacy(name) {
89
+ const value = process.env[name]
90
+ if (value) console.warn(`Warning: ${name} is deprecated; rename it to the MURMURLY_ equivalent.`)
91
+ return value
92
+ }
93
+
94
+ function refuseSecretOnCommandLine(key) {
95
+ if (!key || !isSecretKey(key)) return
96
+ console.error(
97
+ 'Refusing a secret key on the command line: it would end up in your shell history.\n' +
98
+ `Run \`npx @murmurly/sdk init\` to store it, or set ${ENV.secret} in the environment.`
99
+ )
100
+ process.exit(1)
101
+ }
102
+
103
+ function resolveConfig(args) {
104
+ const fileEnv = readEnvFile(resolve(process.cwd(), flag(args, '--env-file') ?? DEFAULT_ENV_FILE))
105
+ const pick = (name) => process.env[name] || fileEnv[name]
106
+ const flagKey = flag(args, '--key')
107
+ refuseSecretOnCommandLine(flagKey)
108
+ return {
109
+ key: flagKey || pick(ENV.secret) || pick(ENV.clientId) || legacy(LEGACY_ENV.key),
110
+ baseUrl: flag(args, '--url') || pick(ENV.baseUrl) || legacy(LEGACY_ENV.baseUrl),
111
+ origin: flag(args, '--origin') || pick(ENV.origin) || legacy(LEGACY_ENV.origin)
112
+ }
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Prompts
117
+ // ---------------------------------------------------------------------------
118
+
119
+ async function ask(question) {
120
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
121
+ try {
122
+ return (await rl.question(question)).trim()
123
+ } finally {
124
+ rl.close()
125
+ }
126
+ }
127
+
128
+ /** Reads a line from the terminal without echoing it. Pasting works. */
129
+ function askHidden(question) {
130
+ const { stdin, stdout } = process
131
+ stdout.write(question)
132
+ stdin.setRawMode(true)
133
+ stdin.setEncoding('utf8')
134
+ return new Promise((resolvePrompt) => {
135
+ let value = ''
136
+ const finish = () => {
137
+ stdin.setRawMode(false)
138
+ stdin.pause()
139
+ stdin.off('data', onData)
140
+ stdout.write('\n')
141
+ resolvePrompt(value.trim())
142
+ }
143
+ const onData = (chunk) => {
144
+ for (const char of chunk) {
145
+ if (char === '') process.exit(130)
146
+ if (char === '\r' || char === '\n' || char === '') return finish()
147
+ value = char === '' || char === '\b' ? value.slice(0, -1) : value + char
148
+ }
149
+ }
150
+ // Listen before resuming, or input already buffered is lost.
151
+ stdin.on('data', onData)
152
+ stdin.resume()
153
+ })
154
+ }
155
+
156
+ function requireTerminal() {
157
+ if (process.stdin.isTTY) return
158
+ console.error(
159
+ 'This needs an interactive terminal, so the key can be typed without being shown.\n' +
160
+ `In CI or on a host, set ${ENV.secret} (or ${ENV.clientId}) and ${ENV.baseUrl} in its secret store instead.`
161
+ )
162
+ process.exit(1)
163
+ }
164
+
165
+ const preview = (key) => `${key.slice(0, isSecretKey(key) ? 22 : 18)}…`
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // API
169
+ // ---------------------------------------------------------------------------
170
+
171
+ function headersFor(key, origin) {
172
+ const headers = { 'x-api-key': key }
173
+ if (origin) headers.Origin = origin
174
+ return headers
175
+ }
176
+
177
+ async function fetchOrExit(url, key, origin, what) {
178
+ const res = await fetch(url, { headers: headersFor(key, origin) })
179
+ if (res.ok) return res
180
+ let message = `${res.status} ${res.statusText}`
181
+ try {
182
+ message = (await res.json())?.error?.message ?? message
183
+ } catch {
184
+ // Not JSON: keep the status line.
185
+ }
186
+ console.error(`Could not ${what}: ${message}`)
187
+ process.exit(1)
188
+ }
189
+
190
+ function requireKeyAndUrl({ key, baseUrl }) {
191
+ if (key && baseUrl) return
192
+ console.error(
193
+ `Missing ${key ? 'base URL' : 'key'}. Run \`npx @murmurly/sdk init\` first, ` +
194
+ `or set ${key ? ENV.baseUrl : `${ENV.secret} / ${ENV.clientId}`}.`
195
+ )
196
+ process.exit(1)
197
+ }
198
+
199
+ const trimSlash = (url) => url.replace(/\/+$/, '')
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Commands
203
+ // ---------------------------------------------------------------------------
204
+
205
+ /** Sets each KEY in the file, replacing an existing line or appending one. */
206
+ function writeEnvValues(path, values) {
207
+ let text = existsSync(path) ? readFileSync(path, 'utf8') : ''
208
+ for (const [name, value] of Object.entries(values)) {
209
+ const line = `${name}=${value}`
210
+ const pattern = new RegExp(`^${name}=.*$`, 'm')
211
+ text = pattern.test(text)
212
+ ? text.replace(pattern, line)
213
+ : `${text}${text && !text.endsWith('\n') ? '\n' : ''}${line}\n`
214
+ }
215
+ writeFileSync(path, text, { mode: 0o600 })
216
+ chmodSync(path, 0o600)
217
+ }
218
+
219
+ /** Adds the env file to .gitignore when this is a git checkout and it is not listed yet. */
220
+ function keepOutOfGit(envFile) {
221
+ const gitignore = resolve(process.cwd(), '.gitignore')
222
+ if (!existsSync(resolve(process.cwd(), '.git')) && !existsSync(gitignore)) return
223
+ const listed =
224
+ existsSync(gitignore) &&
225
+ readFileSync(gitignore, 'utf8')
226
+ .split('\n')
227
+ .some((l) => l.trim() === envFile)
228
+ if (listed) return
229
+ appendFileSync(gitignore, `\n# Murmurly API key\n${envFile}\n`)
230
+ console.log(`Added ${envFile} to .gitignore.`)
231
+ }
232
+
233
+ /** Base URL, key and (for a client id) origin, asked for where no flag gave them. */
234
+ async function askInitInputs(args) {
235
+ const baseUrl = trimSlash(
236
+ flag(args, '--url') || (await ask('API base URL (from the app settings page): '))
237
+ )
238
+ const key = await askHidden('App key (input hidden): ')
239
+ if (!baseUrl || !key) {
240
+ console.error('Nothing saved: the base URL and key are both needed.')
241
+ process.exit(1)
242
+ }
243
+ if (isSecretKey(key)) return { baseUrl, key, secret: true }
244
+ const origin =
245
+ flag(args, '--origin') ||
246
+ (await ask('An allowed origin for this app, e.g. https://example.com: '))
247
+ return { baseUrl, key, secret: false, origin }
248
+ }
249
+
250
+ async function runInit(args) {
251
+ requireTerminal()
252
+ const envFile = flag(args, '--env-file') ?? DEFAULT_ENV_FILE
253
+ const { baseUrl, key, secret, origin } = await askInitInputs(args)
254
+ const res = await fetchOrExit(`${baseUrl}/api/v1/event-types`, key, origin, 'verify the key')
255
+ const { eventTypes = [] } = await res.json()
256
+
257
+ const keyVariable = secret ? ENV.secret : ENV.clientId
258
+ const values = { [ENV.baseUrl]: baseUrl, [keyVariable]: key }
259
+ if (origin) values[ENV.origin] = origin
260
+ writeEnvValues(resolve(process.cwd(), envFile), values)
261
+ keepOutOfGit(envFile)
262
+
263
+ console.log(`Verified ${preview(key)}: ${eventTypes.length} engagement type(s) granted.`)
264
+ console.log(`Saved to ${envFile} as ${keyVariable}.`)
265
+ if (secret) console.log('Read it on the server only: process.env.MURMURLY_SECRET_KEY')
266
+ }
267
+
268
+ async function runCodegen(args) {
269
+ const config = resolveConfig(args)
270
+ requireKeyAndUrl(config)
271
+ const out = resolve(process.cwd(), flag(args, '--out') ?? 'murmurly.d.ts')
272
+ const url = `${trimSlash(config.baseUrl)}/api/v1/types.d.ts`
273
+ const res = await fetchOrExit(url, config.key, config.origin, 'download types')
274
+ writeFileSync(out, await res.text(), 'utf-8')
275
+ console.log(`Wrote TypeScript declarations to ${out}`)
276
+ }
277
+
278
+ async function runGuide(args) {
279
+ const config = resolveConfig(args)
280
+ requireKeyAndUrl(config)
281
+ const url = `${trimSlash(config.baseUrl)}/api/v1/agent-guide`
282
+ const res = await fetchOrExit(url, config.key, config.origin, 'fetch the agent guide')
283
+ console.log(await res.text())
284
+ }
285
+
286
+ function runPrompt(args) {
287
+ const config = resolveConfig(args)
288
+ requireKeyAndUrl(config)
289
+ console.log(
290
+ getAiIntegrationPrompt({ apiKey: config.key, baseUrl: config.baseUrl, origin: config.origin })
291
+ )
292
+ }
293
+
294
+ const RUNNERS = {
295
+ init: runInit,
296
+ codegen: runCodegen,
297
+ guide: runGuide,
298
+ prompt: runPrompt,
299
+ help: () => console.log(USAGE)
300
+ }
301
+
302
+ const args = process.argv.slice(2)
303
+ Promise.resolve(RUNNERS[commandFrom(args)](args)).catch((err) => {
304
+ console.error('Unexpected error:', err instanceof Error ? err.message : err)
305
+ process.exit(1)
306
+ })
@@ -0,0 +1,281 @@
1
+ /** Prefix of a client id: names an app. For a public app it is the whole key. Not secret. */
2
+ export declare const CLIENT_ID_PREFIX = "murmur_id_";
3
+ /** Prefix of a secret key: proves a confidential app is who it says. Server-side only. */
4
+ export declare const SECRET_KEY_PREFIX = "murmur_secret_";
5
+ /** Whether a key is a secret key (`murmur_secret_…`, or a legacy `sk_live_…`). */
6
+ export declare function isSecretKey(key: string): boolean;
7
+ export interface MurmurlyClientOptions {
8
+ /**
9
+ * The app's key: its client id (`murmur_id_…`) for a public app, its secret
10
+ * key (`murmur_secret_…`) for a confidential one. Read a secret from the
11
+ * environment (`process.env.MURMURLY_SECRET_KEY`); never hard-code it.
12
+ */
13
+ apiKey: string;
14
+ /**
15
+ * The API's base URL, shown on the app's settings page in Murmurly
16
+ * (e.g. `https://<deployment>.convex.site`).
17
+ */
18
+ baseUrl: string;
19
+ /**
20
+ * The `Origin` to send. Public apps only, and only outside a browser: a
21
+ * browser sends its own and ignores this.
22
+ */
23
+ origin?: string;
24
+ /**
25
+ * Custom fetch implementation (defaults to global fetch).
26
+ */
27
+ fetch?: typeof fetch;
28
+ }
29
+ export interface MurmurlyEventType {
30
+ /** Unique ID of the event/engagement type */
31
+ id: string;
32
+ /** Human-readable title (e.g. "Product Demo", "Advisory Call") */
33
+ name: string;
34
+ /** URL-friendly slug */
35
+ slug: string;
36
+ /** Description or briefing */
37
+ description: string | null;
38
+ /** Brand color token or hex */
39
+ color: string | null;
40
+ /** Icon identifier */
41
+ icon: string | null;
42
+ /** Unique shortcode identifier */
43
+ shortcode: string | null;
44
+ }
45
+ export interface FormSchemaField {
46
+ /** The key to send under `attributes`. */
47
+ key: string;
48
+ label: string;
49
+ description: string | null;
50
+ /** Attribute type, e.g. `text`, `number`, `select`. */
51
+ type: string;
52
+ required: boolean;
53
+ /** Type-specific settings, e.g. the options of a `select`. */
54
+ config: unknown;
55
+ }
56
+ export interface FormSchemaSection {
57
+ id: string;
58
+ name: string;
59
+ slug: string;
60
+ description: string | null;
61
+ attributes: FormSchemaField[];
62
+ }
63
+ export interface EventTypeFormSchema {
64
+ eventType: MurmurlyEventType;
65
+ /** The request form's sections, as this app may see them. */
66
+ sections: FormSchemaSection[];
67
+ /** JSON Schema (draft 2020-12) for the whole request body, `attributes` included. */
68
+ jsonSchema: Record<string, unknown>;
69
+ }
70
+ export interface EngagementRequestPayload<TAttributes extends Record<string, unknown> = Record<string, unknown>> {
71
+ /** Title or subject of the requested engagement */
72
+ name: string;
73
+ /** Requester email address */
74
+ email: string;
75
+ /** Requester first name */
76
+ firstName: string;
77
+ /** Requester last name */
78
+ lastName: string;
79
+ /** Context, background, or goals */
80
+ description?: string;
81
+ /** Event type slug (e.g. "roundtable") or id */
82
+ eventType: string;
83
+ /** Preferred first day (YYYY-MM-DD) in the workspace's calendar */
84
+ startDate: string;
85
+ /** Preferred start time (HH:mm) on the requester's clock, within the type's `openHours` */
86
+ startTime?: string;
87
+ /** Last day (YYYY-MM-DD), for types that allow multi-day requests */
88
+ endDate?: string;
89
+ /** The requester's IANA time zone, e.g. "America/New_York" */
90
+ timeZone?: string;
91
+ /** Custom attribute values matching the event type's schema */
92
+ attributes?: TAttributes;
93
+ }
94
+ export interface EngagementRequestOptions {
95
+ /**
96
+ * Makes the submission safe to retry: the same key within 24 hours returns
97
+ * the first result (`replayed: true`) instead of creating a duplicate.
98
+ * Generate one per submission and reuse it for that submission's retries.
99
+ */
100
+ idempotencyKey?: string;
101
+ }
102
+ /**
103
+ * Why a day is closed. `unavailable` covers both the type's schedule and
104
+ * workspace closures; the workspace decides whether a closure's name is shown.
105
+ */
106
+ export type ClosedDayReason = 'unavailable' | 'tooSoon' | 'tooFar';
107
+ export interface ClosedDay {
108
+ date: string;
109
+ reason: ClosedDayReason;
110
+ labels: string[];
111
+ }
112
+ export interface OpenHours {
113
+ /** `09:00`, on the workspace clock. */
114
+ start: string;
115
+ /** `17:00`, on the workspace clock. */
116
+ end: string;
117
+ }
118
+ export interface EventTypeAvailability {
119
+ /** The workspace's time zone; every date and time of day is read in it. */
120
+ timeZone: string;
121
+ today: string;
122
+ earliestOpenDate: string | null;
123
+ /** Closed days in the range; every other day is open. */
124
+ closedDays: ClosedDay[];
125
+ /** Times of day this type can start. Empty means any time works. */
126
+ openHours: OpenHours[];
127
+ /** How long one of these runs, when the type says so. */
128
+ durationMinutes: number | null;
129
+ }
130
+ export interface EngagementRequestResult {
131
+ success: boolean;
132
+ /** True when an earlier submission with the same idempotency key was returned instead. */
133
+ replayed?: boolean;
134
+ eventId: string;
135
+ shortcode: string;
136
+ /** Advisory: a request on a closed day, or outside the hours, is still created. */
137
+ availability: {
138
+ open: boolean;
139
+ closedDays: ClosedDay[];
140
+ timeOpen: boolean;
141
+ };
142
+ allowedOrigins: string[];
143
+ }
144
+ /** A request this app submitted: where it stands and what it asked for. */
145
+ export interface OwnEngagementRequest {
146
+ id: string;
147
+ shortcode: string;
148
+ /** e.g. `requested`, `approved`, `rejected`, `confirmed`. */
149
+ status: string;
150
+ name: string;
151
+ eventType?: string;
152
+ startDate?: string;
153
+ endDate?: string;
154
+ startTime?: string;
155
+ submittedAt?: string;
156
+ }
157
+ export declare class MurmurlyApiError extends Error {
158
+ code: number;
159
+ /** Seconds to wait before retrying, when the API said so (`429`). */
160
+ retryAfter?: number;
161
+ constructor(message: string, code?: number, retryAfter?: number);
162
+ }
163
+ /**
164
+ * Main client interface for Murmurly Public Ingestion API.
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * import { createMurmurlyClient } from '@murmurly/sdk'
169
+ *
170
+ * // Server-side, confidential app. The secret comes from the environment.
171
+ * const murmurly = createMurmurlyClient({
172
+ * apiKey: process.env.MURMURLY_SECRET_KEY!,
173
+ * baseUrl: process.env.MURMURLY_BASE_URL!
174
+ * })
175
+ *
176
+ * const types = await murmurly.eventTypes.list()
177
+ * const booking = await murmurly.engagements.request(
178
+ * {
179
+ * name: 'Discovery Consultation',
180
+ * email: 'client@example.com',
181
+ * firstName: 'Jane',
182
+ * lastName: 'Doe',
183
+ * eventType: types[0].slug,
184
+ * startDate: '2026-10-20'
185
+ * },
186
+ * { idempotencyKey: crypto.randomUUID() }
187
+ * )
188
+ * ```
189
+ */
190
+ export interface MurmurlyClient {
191
+ /**
192
+ * Discovery methods for retrieving public engagement types and attribute schemas.
193
+ */
194
+ eventTypes: {
195
+ /**
196
+ * Lists the engagement types this app has been granted.
197
+ */
198
+ list: () => Promise<MurmurlyEventType[]>;
199
+ /**
200
+ * The request form of one type: its fields and a JSON Schema for the request body.
201
+ */
202
+ getSchema: (slugOrId: string | {
203
+ slug?: string;
204
+ id?: string;
205
+ }) => Promise<EventTypeFormSchema>;
206
+ /**
207
+ * Which days a type can take. Advisory: a request on a closed day is still accepted.
208
+ */
209
+ availability: (query: {
210
+ eventType: string;
211
+ from: string;
212
+ to: string;
213
+ }) => Promise<EventTypeAvailability>;
214
+ };
215
+ /**
216
+ * Methods for booking and requesting engagements.
217
+ */
218
+ engagements: {
219
+ /**
220
+ * Submits an engagement request.
221
+ *
222
+ * @throws {MurmurlyApiError} 400 (malformed), 401 (invalid key), 403 (origin, scope or
223
+ * type not granted), 422 (field the type does not take), 429 (rate limited; see `retryAfter`).
224
+ */
225
+ request: <TAttributes extends Record<string, unknown> = Record<string, unknown>>(payload: EngagementRequestPayload<TAttributes>, options?: EngagementRequestOptions) => Promise<EngagementRequestResult>;
226
+ /**
227
+ * The latest 50 requests this app submitted. Confidential apps with `engagements:read_own`.
228
+ */
229
+ list: () => Promise<OwnEngagementRequest[]>;
230
+ /**
231
+ * One request this app submitted, by id or shortcode. Confidential apps with `engagements:read_own`.
232
+ */
233
+ get: (idOrShortcode: string) => Promise<OwnEngagementRequest>;
234
+ };
235
+ /**
236
+ * Type definitions synchronization.
237
+ */
238
+ types: {
239
+ /**
240
+ * Downloads TypeScript declaration file for the connected workspace.
241
+ */
242
+ download: () => Promise<string>;
243
+ };
244
+ /**
245
+ * Fetches real-time dynamic Markdown documentation tailored to this application key.
246
+ * AI agents can query this endpoint to inspect active schemas at runtime. It never contains a secret.
247
+ */
248
+ agentGuide: () => Promise<string>;
249
+ }
250
+ /**
251
+ * Creates a strongly typed Murmurly client for interacting with the Public API.
252
+ */
253
+ export declare function createMurmurlyClient(options: MurmurlyClientOptions): MurmurlyClient;
254
+ export interface AiPromptOptions {
255
+ /**
256
+ * A public app's client id (`murmur_id_…`) is written into the prompt. A
257
+ * secret key never is: the prompt tells the assistant to read it from
258
+ * `MURMURLY_SECRET_KEY` and have the user store it with `murmurly init`.
259
+ */
260
+ apiKey: string;
261
+ baseUrl: string;
262
+ origin?: string;
263
+ appName?: string;
264
+ }
265
+ /**
266
+ * Generates an instruction prompt for AI assistants (Cursor, Claude, Copilot, Lovable, Bolt.new).
267
+ * The prompt never contains a secret key, whatever `apiKey` holds.
268
+ */
269
+ export declare function getAiIntegrationPrompt(options: AiPromptOptions): string;
270
+ /**
271
+ * Contextual hints and architecture notes for AI agents inspecting this library.
272
+ */
273
+ export declare const AI_INTEGRATION_HINTS: {
274
+ readonly description: "Official TypeScript client for Murmurly Public Ingestion API";
275
+ readonly coreMethods: readonly ["createMurmurlyClient({ apiKey, baseUrl, origin })", "client.eventTypes.list() - engagement types this app was granted", "client.eventTypes.getSchema(slugOrId) - { eventType, sections, jsonSchema } for one type", "client.eventTypes.availability({ eventType, from, to }) - closed days, advisory", "client.engagements.request(payload, { idempotencyKey }) - submit a request", "client.engagements.list() / get(idOrShortcode) - read back own requests (confidential apps)", "client.agentGuide() - real-time Markdown guide for this app, never contains a secret"];
276
+ readonly keyFormats: "murmur_id_… is a client id (public, safe in page source). murmur_secret_… is a secret key (server-only).";
277
+ readonly securityNote: "Never ask a user to paste a secret key into chat or code. Have them run `npx @murmurly/sdk init`, then read process.env.MURMURLY_SECRET_KEY on the server.";
278
+ readonly cliSetup: "npx @murmurly/sdk init";
279
+ readonly cliCodegen: "npx @murmurly/sdk codegen";
280
+ };
281
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,185 @@
1
+ /** Prefix of a client id: names an app. For a public app it is the whole key. Not secret. */
2
+ export const CLIENT_ID_PREFIX = 'murmur_id_';
3
+ /** Prefix of a secret key: proves a confidential app is who it says. Server-side only. */
4
+ export const SECRET_KEY_PREFIX = 'murmur_secret_';
5
+ /** Whether a key is a secret key (`murmur_secret_…`, or a legacy `sk_live_…`). */
6
+ export function isSecretKey(key) {
7
+ return key.startsWith(SECRET_KEY_PREFIX) || key.startsWith('sk_live_');
8
+ }
9
+ export class MurmurlyApiError extends Error {
10
+ code;
11
+ /** Seconds to wait before retrying, when the API said so (`429`). */
12
+ retryAfter;
13
+ constructor(message, code = 500, retryAfter) {
14
+ super(message);
15
+ this.name = 'MurmurlyApiError';
16
+ this.code = code;
17
+ this.retryAfter = retryAfter;
18
+ }
19
+ }
20
+ const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
21
+ function readOptions(options) {
22
+ const key = typeof options.apiKey === 'string' ? options.apiKey.trim() : '';
23
+ if (!key)
24
+ throw new Error('MurmurlyClient requires a valid apiKey.');
25
+ if (isSecretKey(key) && isBrowser()) {
26
+ throw new Error('A Murmurly secret key must never reach a browser. Call the API from your server, ' +
27
+ 'or use a public app and its client id (murmur_id_…) here.');
28
+ }
29
+ const baseUrl = typeof options.baseUrl === 'string' ? options.baseUrl.trim() : '';
30
+ if (!baseUrl) {
31
+ throw new Error('MurmurlyClient requires a baseUrl: copy it from the app settings page in Murmurly.');
32
+ }
33
+ return { key, baseUrl: baseUrl.replace(/\/+$/, '') };
34
+ }
35
+ async function toApiError(res, fallback) {
36
+ const retryAfterHeader = res.headers?.get?.('Retry-After');
37
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : undefined;
38
+ let message = `${fallback} (HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ''})`;
39
+ try {
40
+ const body = (await res.json());
41
+ if (body?.error?.message)
42
+ message = body.error.message;
43
+ }
44
+ catch {
45
+ // Not JSON: keep the status line.
46
+ }
47
+ return new MurmurlyApiError(message, res.status, retryAfter);
48
+ }
49
+ /**
50
+ * Creates a strongly typed Murmurly client for interacting with the Public API.
51
+ */
52
+ export function createMurmurlyClient(options) {
53
+ const { key, baseUrl } = readOptions(options);
54
+ const { origin, fetch: customFetch = globalThis.fetch } = options;
55
+ function buildHeaders(extraHeaders = {}) {
56
+ const headers = { 'x-api-key': key, ...extraHeaders };
57
+ if (origin)
58
+ headers.Origin = origin;
59
+ return headers;
60
+ }
61
+ async function call(path, init = {}) {
62
+ const headers = buildHeaders(init.headers || {});
63
+ const res = await customFetch(`${baseUrl}${path}`, { ...init, headers });
64
+ if (!res.ok)
65
+ throw await toApiError(res, 'Murmurly API request failed');
66
+ return res;
67
+ }
68
+ const json = async (path, init) => (await (await call(path, init)).json());
69
+ const text = async (path) => (await call(path)).text();
70
+ return {
71
+ eventTypes: {
72
+ async list() {
73
+ const data = await json('/api/v1/event-types');
74
+ return data.eventTypes ?? [];
75
+ },
76
+ async getSchema(slugOrId) {
77
+ const ref = typeof slugOrId === 'string' ? { slug: slugOrId } : slugOrId;
78
+ const params = new URLSearchParams();
79
+ if (ref.slug)
80
+ params.set('slug', ref.slug);
81
+ if (!ref.slug && ref.id)
82
+ params.set('id', ref.id);
83
+ return json(`/api/v1/event-types/schema?${params}`);
84
+ },
85
+ async availability(query) {
86
+ const params = new URLSearchParams(query);
87
+ return json(`/api/v1/event-types/availability?${params}`);
88
+ }
89
+ },
90
+ engagements: {
91
+ async request(payload, requestOptions = {}) {
92
+ const headers = { 'Content-Type': 'application/json' };
93
+ if (requestOptions.idempotencyKey)
94
+ headers['Idempotency-Key'] = requestOptions.idempotencyKey;
95
+ return json('/api/v1/engagements/request', {
96
+ method: 'POST',
97
+ headers,
98
+ body: JSON.stringify(payload)
99
+ });
100
+ },
101
+ async list() {
102
+ const data = await json('/api/v1/engagements/requests');
103
+ return data.requests ?? [];
104
+ },
105
+ async get(idOrShortcode) {
106
+ const params = new URLSearchParams({ id: idOrShortcode });
107
+ return json(`/api/v1/engagements/requests?${params}`);
108
+ }
109
+ },
110
+ types: {
111
+ download: () => text('/api/v1/types.d.ts')
112
+ },
113
+ agentGuide: () => text('/api/v1/agent-guide')
114
+ };
115
+ }
116
+ /**
117
+ * Generates an instruction prompt for AI assistants (Cursor, Claude, Copilot, Lovable, Bolt.new).
118
+ * The prompt never contains a secret key, whatever `apiKey` holds.
119
+ */
120
+ export function getAiIntegrationPrompt(options) {
121
+ const { apiKey, baseUrl, origin, appName = 'My App' } = options;
122
+ const cleanBase = baseUrl.replace(/\/+$/, '');
123
+ const secret = isSecretKey(apiKey.trim());
124
+ const keyExpression = secret ? 'process.env.MURMURLY_SECRET_KEY' : `'${apiKey.trim()}'`;
125
+ const keyLine = secret
126
+ ? '- Secret key: stored by the user in MURMURLY_SECRET_KEY (server-side only). Never ask for it in chat or write it into code.'
127
+ : `- Client id (public, safe in page source): ${apiKey.trim()}`;
128
+ const setupStep = secret
129
+ ? `\n0. Ask the user to run this in their own terminal. It prompts for the secret key without echoing it
130
+ and saves it to a git-ignored .env.local:
131
+ npx @murmurly/sdk init --url ${cleanBase}\n`
132
+ : '';
133
+ const originLine = origin ? `\n- Allowed Origin: ${origin}` : '';
134
+ const originNote = secret
135
+ ? 'Call Murmurly from server code only: the SDK refuses to run a secret key in a browser.'
136
+ : `Ensure '${origin || 'your website origin'}' is configured in Allowed CORS Domains in Murmurly Settings.`;
137
+ return `Integrate Murmurly public engagement booking into "${appName}".
138
+
139
+ API Configuration:
140
+ - Base URL: ${cleanBase}
141
+ ${keyLine}${originLine}
142
+
143
+ Integration Steps:${setupStep}
144
+ 1. Install official SDK:
145
+ npm install @murmurly/sdk
146
+
147
+ 2. Query live engagement types:
148
+ import { createMurmurlyClient } from '@murmurly/sdk'
149
+ const client = createMurmurlyClient({ apiKey: ${keyExpression}, baseUrl: '${cleanBase}' })
150
+ const eventTypes = await client.eventTypes.list()
151
+
152
+ 3. Read the chosen type's fields: await client.eventTypes.getSchema(slug)
153
+
154
+ 4. Submit the request, with a fresh idempotency key per submission:
155
+ await client.engagements.request({
156
+ name: 'Meeting Title',
157
+ email: 'guest@example.com',
158
+ firstName: 'First',
159
+ lastName: 'Last',
160
+ eventType: eventTypes[0].slug,
161
+ startDate: '2026-10-20'
162
+ }, { idempotencyKey: crypto.randomUUID() })
163
+
164
+ Note: ${originNote}`;
165
+ }
166
+ /**
167
+ * Contextual hints and architecture notes for AI agents inspecting this library.
168
+ */
169
+ export const AI_INTEGRATION_HINTS = {
170
+ description: 'Official TypeScript client for Murmurly Public Ingestion API',
171
+ coreMethods: [
172
+ 'createMurmurlyClient({ apiKey, baseUrl, origin })',
173
+ 'client.eventTypes.list() - engagement types this app was granted',
174
+ 'client.eventTypes.getSchema(slugOrId) - { eventType, sections, jsonSchema } for one type',
175
+ 'client.eventTypes.availability({ eventType, from, to }) - closed days, advisory',
176
+ 'client.engagements.request(payload, { idempotencyKey }) - submit a request',
177
+ 'client.engagements.list() / get(idOrShortcode) - read back own requests (confidential apps)',
178
+ 'client.agentGuide() - real-time Markdown guide for this app, never contains a secret'
179
+ ],
180
+ keyFormats: 'murmur_id_… is a client id (public, safe in page source). murmur_secret_… is a secret key (server-only).',
181
+ securityNote: 'Never ask a user to paste a secret key into chat or code. Have them run `npx @murmurly/sdk init`, then read process.env.MURMURLY_SECRET_KEY on the server.',
182
+ cliSetup: 'npx @murmurly/sdk init',
183
+ cliCodegen: 'npx @murmurly/sdk codegen'
184
+ };
185
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './client.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './client.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA"}
package/llms.txt ADDED
@@ -0,0 +1,47 @@
1
+ # @murmurly/sdk
2
+
3
+ > Official client SDK and CLI for the Murmurly Public Ingestion API.
4
+
5
+ ## Overview
6
+ A typed client for submitting engagement requests to a Murmurly workspace. Runs in browsers (public apps, protected by an origin check) and on servers (confidential apps, protected by a secret key).
7
+
8
+ ## Installation
9
+ ```bash
10
+ npm install @murmurly/sdk
11
+ ```
12
+
13
+ ## Keys
14
+ - `murmur_id_…`: client id. A public app's whole key; safe in page source.
15
+ - `murmur_secret_…`: secret key. Server-side only. Never ask a user to paste it into chat; never write it into code, prompts, logs or URLs.
16
+
17
+ To store a key, the user runs `npx @murmurly/sdk init` in their own terminal. It prompts without echoing, verifies the key, and saves `MURMURLY_SECRET_KEY` (or `MURMURLY_CLIENT_ID`) and `MURMURLY_BASE_URL` to a git-ignored `.env.local`.
18
+
19
+ ## Quick Start
20
+ ```typescript
21
+ import { createMurmurlyClient } from '@murmurly/sdk'
22
+
23
+ const murmurly = createMurmurlyClient({
24
+ apiKey: process.env.MURMURLY_SECRET_KEY!,
25
+ baseUrl: process.env.MURMURLY_BASE_URL!
26
+ })
27
+
28
+ const eventTypes = await murmurly.eventTypes.list()
29
+ const { jsonSchema } = await murmurly.eventTypes.getSchema(eventTypes[0].slug)
30
+
31
+ const result = await murmurly.engagements.request(
32
+ {
33
+ name: 'Introductory Discovery Call',
34
+ email: 'client@example.com',
35
+ firstName: 'Jane',
36
+ lastName: 'Doe',
37
+ eventType: eventTypes[0].slug,
38
+ startDate: '2026-10-20'
39
+ },
40
+ { idempotencyKey: crypto.randomUUID() }
41
+ )
42
+ ```
43
+
44
+ ## AI Agent Integration Guidance
45
+ 1. **Live guide**: `await murmurly.agentGuide()` (or `npx @murmurly/sdk guide`) returns Markdown written for this app: permissions, granted types, schemas and a tool definition. It never contains a secret.
46
+ 2. **Types**: `npx @murmurly/sdk codegen` writes `murmurly.d.ts` with exact payload interfaces.
47
+ 3. **Browser origin**: a public app only answers the origins set on it in Murmurly (e.g. `https://my-site.lovable.app`, `http://localhost:5173`). A `403` naming the origin means it must be added there.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@murmurly/sdk",
3
+ "version": "0.3.0",
4
+ "description": "Official TypeScript & JavaScript SDK for the Murmurly Public Ingestion API",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/MurmurlyIO/platform.git",
9
+ "directory": "packages/sdk"
10
+ },
11
+ "bin": {
12
+ "murmurly": "./bin/murmurly.mjs"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "llms.txt",
20
+ "AI_GUIDE.md"
21
+ ],
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "type-check": "tsc --noEmit"
38
+ },
39
+ "devDependencies": {
40
+ "typescript": "^5.7.3",
41
+ "vitest": "catalog:"
42
+ }
43
+ }