narai-primitives 2.0.0-rc.1 → 2.0.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/package.json +1 -1
- package/plugins/create-connector/.claude-plugin/plugin.json +8 -0
- package/plugins/create-connector/README.md +49 -0
- package/plugins/create-connector/skills/create-connector/SKILL.md +252 -0
- package/plugins/create-connector/skills/create-connector/assets/templates/bin.tmpl +6 -0
- package/plugins/create-connector/skills/create-connector/assets/templates/connector-SKILL.md.tmpl +49 -0
- package/plugins/create-connector/skills/create-connector/assets/templates/index.mjs.tmpl +80 -0
- package/plugins/create-connector/skills/create-connector/assets/templates/tests-example.mjs.tmpl +45 -0
- package/plugins/create-connector/skills/create-connector/references/action-design.md +133 -0
- package/plugins/create-connector/skills/create-connector/references/auth-patterns.md +122 -0
- package/plugins/create-connector/skills/create-connector/references/connector-anatomy.md +116 -0
- package/plugins/create-connector/skills/create-connector/references/db-agent-pointer.md +84 -0
- package/plugins/create-connector/skills/create-connector/references/plugin-layer.md +115 -0
- package/plugins/create-connector/skills/create-connector/references/template-sync.md +71 -0
- package/plugins/create-connector/skills/create-connector/references/verification.md +103 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "narai-primitives",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Read-only connectors + planning hub + connector framework, in one package. Bundles what was @narai/connector-toolkit, @narai/connector-config, @narai/connector-hub, and the seven @narai/<svc>-agent-connector packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/hub/index.js",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-connector",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a custom connector for narai-primitives' gather() — wraps a SaaS API, REST/GraphQL endpoint, SDK, or CLI tool. Project- or user-scoped. No git init, no npm publish, no marketplace.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "NarAI Labs"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# create-connector
|
|
2
|
+
|
|
3
|
+
A Claude Code skill that scaffolds custom connectors for `narai-primitives`'s `gather()`. Wraps any SaaS API, REST endpoint, GraphQL endpoint, SDK, or CLI tool into a minimal local connector.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Via the `narai` marketplace:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
/plugin marketplace add narailabs/narai-claude-plugins
|
|
11
|
+
/plugin install create-connector@narai
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Use
|
|
15
|
+
|
|
16
|
+
In Claude Code, just describe what you want to wrap:
|
|
17
|
+
|
|
18
|
+
> "I want to query Stripe from Claude"
|
|
19
|
+
> "Wrap our internal orders API"
|
|
20
|
+
> "Add Linear to our agents"
|
|
21
|
+
> "Make me a Slack connector"
|
|
22
|
+
|
|
23
|
+
The skill walks you through scope (project vs user), identity, auth, and action surface, then stamps out a minimal local connector at `<scope>/.connectors/connectors/<slug>/`. The `gather()` from `narai-primitives` picks it up immediately — no install, no publish, no restart.
|
|
24
|
+
|
|
25
|
+
## What you get
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
<scope>/.connectors/connectors/<slug>/
|
|
29
|
+
├── SKILL.md describes actions; read by gather()'s planner
|
|
30
|
+
├── index.mjs uses createConnector from narai-primitives
|
|
31
|
+
├── bin/<slug> shell shim → exec node ../index.mjs
|
|
32
|
+
└── (optional) tests/example.test.mjs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Plus one entry in `<scope>/.connectors/config.yaml`:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
connectors:
|
|
39
|
+
<slug>:
|
|
40
|
+
skill: <abs-path>
|
|
41
|
+
bin: <abs-path>
|
|
42
|
+
enabled: true
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Differences from the legacy version
|
|
46
|
+
|
|
47
|
+
The legacy create-connector flow scaffolded a full `@narai/<svc>-agent-connector` repo with `git init`, `npm install`, plugin manifest, tests/, README, LICENSE, marketplace entry. That flow is now used only for **builtin** connectors that get contributed to `narai-primitives` via PR — see [CONTRIBUTING.md](https://github.com/narailabs/narai-primitives/blob/main/CONTRIBUTING.md).
|
|
48
|
+
|
|
49
|
+
This skill targets the much more common case: an **end user** who wants a quick local connector for their own work. No git, no publish, no plugin scaffold.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-connector
|
|
3
|
+
description: |
|
|
4
|
+
Use this skill when the user wants to add a custom connector to their project —
|
|
5
|
+
wrapping a SaaS API, REST endpoint, GraphQL endpoint, SDK, or CLI tool so
|
|
6
|
+
Claude can call it via `gather()` from `narai-primitives`. Trigger even when
|
|
7
|
+
the user doesn't say "connector" explicitly: phrases like "I want to query
|
|
8
|
+
Stripe from Claude", "add Slack to our agents", "wrap our internal orders
|
|
9
|
+
API", "connect Salesforce", "make a Linear agent" all warrant this skill.
|
|
10
|
+
Scaffolds a minimal local connector at `.connectors/connectors/<name>/`
|
|
11
|
+
(project scope, default) or `~/.connectors/connectors/<name>/` (user scope) —
|
|
12
|
+
no `git init`, no `npm publish`, no plugin manifest, no marketplace entry.
|
|
13
|
+
Do NOT use for: modifying an existing connector (just edit the file),
|
|
14
|
+
wrapping an MCP server (different abstraction), querying databases (the `db`
|
|
15
|
+
connector inside narai-primitives already covers postgres/mysql/sqlite/mssql/
|
|
16
|
+
mongodb/dynamodb/oracle), or contributing a new builtin connector (that's a
|
|
17
|
+
PR to https://github.com/narailabs/narai-primitives — see its CONTRIBUTING.md).
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# create-connector
|
|
21
|
+
|
|
22
|
+
Scaffold a custom connector that the user's local installation loads via `narai-primitives`'s `gather()`. The connector is **local-only**: it does not get published to npm, does not become a Claude Code plugin, and does not go in any marketplace. The user can later send a PR to `narailabs/narai-primitives` if their connector turns out to be broadly useful — that's a separate flow.
|
|
23
|
+
|
|
24
|
+
## What gets created
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
<scope>/.connectors/connectors/<slug>/
|
|
28
|
+
├── SKILL.md # Describes actions; read by gather()'s planner
|
|
29
|
+
├── index.mjs # Uses createConnector from narai-primitives
|
|
30
|
+
├── bin/<slug> # Shell shim → exec node ../index.mjs
|
|
31
|
+
└── (optional) tests/example.test.mjs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Plus one entry appended to `<scope>/.connectors/config.yaml`:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
connectors:
|
|
38
|
+
<slug>:
|
|
39
|
+
skill: <abs-path-to-connector-dir> # path-style (config-loader supports it)
|
|
40
|
+
bin: <abs-path-to-bin>
|
|
41
|
+
enabled: true
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That's it — the connector is reachable via `gather()` immediately. No install, no publish, no restart.
|
|
45
|
+
|
|
46
|
+
## When to invoke
|
|
47
|
+
|
|
48
|
+
Use this skill when the user wants to **create a new custom connector** for local use.
|
|
49
|
+
|
|
50
|
+
Classic phrasings:
|
|
51
|
+
- "I want to wrap the Stripe API"
|
|
52
|
+
- "Make me a Slack connector"
|
|
53
|
+
- "We need a connector for our internal orders API"
|
|
54
|
+
- "Add Linear to our agents"
|
|
55
|
+
|
|
56
|
+
Near-miss phrasings (still trigger):
|
|
57
|
+
- "I want to query Stripe from Claude"
|
|
58
|
+
- "Connect Salesforce to Claude Code"
|
|
59
|
+
- "Make a thing that lets me search Jira from Claude"
|
|
60
|
+
|
|
61
|
+
**Do NOT use this skill for:**
|
|
62
|
+
|
|
63
|
+
- **Modifying an existing connector.** Just edit the file directly.
|
|
64
|
+
- **Wrapping an MCP server.** MCP servers and connectors are different abstractions in Claude Code. Point the user at the Claude Code MCP docs.
|
|
65
|
+
- **Querying a database.** `narai-primitives/db` covers postgres, mysql, sqlite, mssql, mongodb, dynamodb, oracle. See `references/db-agent-pointer.md`. Only suggest a custom DB connector if it's a backend the bundled `db` connector doesn't support.
|
|
66
|
+
- **Contributing a builtin connector to `narai-primitives`.** That's a different flow (PR to the bundle's repo, separate test suite, plugin marketplace entry). This skill is for end-user local connectors only.
|
|
67
|
+
|
|
68
|
+
## Policy gate is automatic
|
|
69
|
+
|
|
70
|
+
Every connector built on `createConnector` from `narai-primitives/toolkit` gets the policy gate **automatically**. Classification (`read` / `write` / `delete` / `admin` / `privilege`), approval-mode resolution (`auto` / `confirm_once` / `confirm_each` / `grant_required`), escalation, audit logging, and hardship recording all flow from the toolkit — you don't import any extra modules and don't write any approval logic yourself.
|
|
71
|
+
|
|
72
|
+
What you **do** choose:
|
|
73
|
+
|
|
74
|
+
- The **classification** of each action (defaults to `read`).
|
|
75
|
+
- The **approval mode** for the connector (defaults to `auto` for read-only connectors).
|
|
76
|
+
|
|
77
|
+
Both surface in the interview only when relevant.
|
|
78
|
+
|
|
79
|
+
## Interview
|
|
80
|
+
|
|
81
|
+
Conversational, not a fixed form. Capture the essentials in **5–7 quick exchanges**, then generate a draft and let the user react. People react faster than they author from scratch.
|
|
82
|
+
|
|
83
|
+
### 1. Scope
|
|
84
|
+
|
|
85
|
+
Ask: *"Should this connector be available only in this project, or for all your projects?"*
|
|
86
|
+
|
|
87
|
+
| Choice | Where it lives | Default for |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| **Project (default)** | `./.connectors/connectors/<slug>/` | repo-specific stuff ("our internal orders API") |
|
|
90
|
+
| **User** | `~/.connectors/connectors/<slug>/` | personal tools ("my company's Linear") |
|
|
91
|
+
|
|
92
|
+
Pick a sensible default based on the user's phrasing. Confirm with them. The scope determines:
|
|
93
|
+
- Where files get written
|
|
94
|
+
- Which `config.yaml` gets the entry (`./.connectors/config.yaml` for project, `~/.connectors/config.yaml` for user)
|
|
95
|
+
|
|
96
|
+
### 2. Identity
|
|
97
|
+
|
|
98
|
+
Ask: *"What's the service slug?"*
|
|
99
|
+
|
|
100
|
+
- **Slug**: lowercase, alphanumeric + hyphens (e.g., `stripe`, `slack`, `linear`, `acme`, `acme-orders`). Used everywhere — directory name, bin name, config key, envelope `name`.
|
|
101
|
+
- **Description**: one sentence (e.g., "Read-only Stripe connector: customers, charges, invoices.")
|
|
102
|
+
|
|
103
|
+
Record. Move on.
|
|
104
|
+
|
|
105
|
+
### 3. Auth
|
|
106
|
+
|
|
107
|
+
Ask: *"How does authentication work for this API?"*
|
|
108
|
+
|
|
109
|
+
Map the answer to one of:
|
|
110
|
+
|
|
111
|
+
- **`bearer-token-env-var`** (default) — single env var like `STRIPE_API_KEY`, used as `Authorization: Bearer …`.
|
|
112
|
+
- **`api-key-header-env-var`** — single env var, used as a custom header like `X-API-Key`. Capture the header name.
|
|
113
|
+
- **`multi-secret`** — multiple env vars (e.g., `GITHUB_TOKEN` + `GITHUB_OWNER`). Capture each pairing of `config-key → env-var`.
|
|
114
|
+
- **`basic-auth`** — username + password env vars.
|
|
115
|
+
- **`oauth-with-refresh`** — leave a `// TODO` placeholder in `loadCredentials`. Tell the user explicitly: *"You'll need to implement the OAuth flow before the connector will work."*
|
|
116
|
+
- **`custom`** — anything else (mTLS, signed URLs, etc.). Same TODO treatment.
|
|
117
|
+
|
|
118
|
+
See `references/auth-patterns.md` for per-scheme `loadCredentials` snippets.
|
|
119
|
+
|
|
120
|
+
### 4. API basics
|
|
121
|
+
|
|
122
|
+
Ask: *"What's the API base URL? Any rate limit or versioning header you know about?"*
|
|
123
|
+
|
|
124
|
+
Defaults:
|
|
125
|
+
- **Rate limit**: 60/min. Adjust if the user knows.
|
|
126
|
+
- **Read timeout**: 30s.
|
|
127
|
+
- **User-Agent**: `narai-custom-<slug>` (helps the upstream service identify the caller).
|
|
128
|
+
|
|
129
|
+
### 5. Action surface
|
|
130
|
+
|
|
131
|
+
Ask: *"What actions should this connector expose? Just describe them — name, what it does, what params, what it returns."*
|
|
132
|
+
|
|
133
|
+
The user will say something like *"`get_customer` takes an id, returns the customer. `list_charges` takes optional `customer_id` and `limit` (default 25), returns a list."*
|
|
134
|
+
|
|
135
|
+
You write the Zod schemas, pick HTTP methods/endpoints (ask if not obvious), and assign default classifications:
|
|
136
|
+
|
|
137
|
+
- Names starting with `get_*`, `list_*`, `search_*`, `query_*`, `fetch_*` → `read`
|
|
138
|
+
- Names starting with `create_*`, `post_*`, `send_*`, `update_*`, `patch_*` → `write`
|
|
139
|
+
- Names starting with `delete_*`, `remove_*`, `archive_*` → `delete`
|
|
140
|
+
- Names starting with `grant_*`, `revoke_*` → `privilege`
|
|
141
|
+
|
|
142
|
+
Override on user signal — if they say *"this one mutates state"*, classify as `write` even if the name says otherwise.
|
|
143
|
+
|
|
144
|
+
See `references/action-design.md` for Zod schema patterns and the full classification → approval-mode table.
|
|
145
|
+
|
|
146
|
+
### 6. Approval mode (only if non-read actions exist)
|
|
147
|
+
|
|
148
|
+
If any action is non-`read`, ask: *"For the write/delete actions, how should the user approve them — `auto` (no prompt), `confirm_once` (per session), `confirm_each` (every call), or `grant_required` (out-of-band)?"*
|
|
149
|
+
|
|
150
|
+
Defaults:
|
|
151
|
+
- `read` → `auto`
|
|
152
|
+
- `write` → `confirm_once`
|
|
153
|
+
- `delete` → `confirm_each`
|
|
154
|
+
- `admin` / `privilege` → `grant_required`
|
|
155
|
+
|
|
156
|
+
Skip entirely if all actions are read.
|
|
157
|
+
|
|
158
|
+
### 7. Confirmation
|
|
159
|
+
|
|
160
|
+
Show the user a summary:
|
|
161
|
+
|
|
162
|
+
- File tree that will be created
|
|
163
|
+
- Actions table with classifications
|
|
164
|
+
- Auth scheme + env vars
|
|
165
|
+
- Scope path (project or user)
|
|
166
|
+
|
|
167
|
+
Ask: *"Anything to change before I scaffold?"* Wait for explicit OK.
|
|
168
|
+
|
|
169
|
+
## Scaffold
|
|
170
|
+
|
|
171
|
+
Templates live at `assets/templates/`. Three files plus an optional test:
|
|
172
|
+
|
|
173
|
+
| Stamp | From template | Substitutions |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `<scope>/.connectors/connectors/<slug>/index.mjs` | `assets/templates/index.mjs.tmpl` | slug, description, auth, action specs |
|
|
176
|
+
| `<scope>/.connectors/connectors/<slug>/bin/<slug>` | `assets/templates/bin.tmpl` | slug |
|
|
177
|
+
| `<scope>/.connectors/connectors/<slug>/SKILL.md` | `assets/templates/connector-SKILL.md.tmpl` | slug, description, action surface |
|
|
178
|
+
| `<scope>/.connectors/connectors/<slug>/tests/example.test.mjs` (optional) | `assets/templates/tests-example.mjs.tmpl` | slug, first action |
|
|
179
|
+
|
|
180
|
+
After stamping:
|
|
181
|
+
|
|
182
|
+
1. `chmod +x <bin>` so the shim is executable.
|
|
183
|
+
2. Open `<scope>/.connectors/config.yaml` (create it if missing — minimal valid file is `connectors: {}`) and append the entry under `connectors:`:
|
|
184
|
+
```yaml
|
|
185
|
+
connectors:
|
|
186
|
+
<slug>:
|
|
187
|
+
skill: <abs-path-to-connector-dir>
|
|
188
|
+
bin: <abs-path-to-bin>
|
|
189
|
+
enabled: true
|
|
190
|
+
```
|
|
191
|
+
3. Report what was created with absolute paths.
|
|
192
|
+
|
|
193
|
+
### Placeholders
|
|
194
|
+
|
|
195
|
+
| Placeholder | Example value |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `{{SLUG}}` | `stripe` |
|
|
198
|
+
| `{{ServicePascal}}` | `Stripe` (PascalCase, hyphens stripped) |
|
|
199
|
+
| `{{DESCRIPTION}}` | `Read-only Stripe connector: customers, charges, invoices.` |
|
|
200
|
+
| `{{API_BASE}}` | `https://api.stripe.com` |
|
|
201
|
+
| `{{RATE_LIMIT_PER_MIN}}` | `60` |
|
|
202
|
+
| `{{CREDENTIAL_ENV_VAR}}` | `STRIPE_API_KEY` |
|
|
203
|
+
| `{{AUTH_HEADER_ENTRY}}` | `Authorization: \`Bearer ${creds.token}\`` (one line; for `X-API-Key` it's `"X-API-Key": creds.token,`) |
|
|
204
|
+
| `{{ACTIONS_DICTIONARY}}` | the JS object literal of action handlers (filled in from interview) |
|
|
205
|
+
| `{{ACTIONS_TABLE_MD}}` | markdown table of actions for the connector's SKILL.md |
|
|
206
|
+
| `{{FIRST_ACTION}}` | first action name, used in the smoke-test invocation |
|
|
207
|
+
|
|
208
|
+
## Verify
|
|
209
|
+
|
|
210
|
+
After scaffolding:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
# 1. Smoke-test the bin in isolation (env vars set, real API call optional).
|
|
214
|
+
<scope>/.connectors/connectors/<slug>/bin/<slug> --action <first-action> --params '{}'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Expectation: a JSON envelope on stdout. Without credentials, expect `{"status":"error","error_code":"CONFIG_ERROR",…}` — that's the **right** shape; we're testing the dispatch plumbing, not connectivity.
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
# 2. End-to-end via the hub.
|
|
221
|
+
node -e 'import("narai-primitives").then(({gather}) => gather({prompt:"call <slug> <first-action>"}).then(r => console.log(JSON.stringify(r, null, 2))))'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Expectation: `gather()` plans `{ connector: "<slug>", action: "<first-action>", … }`, dispatches, returns either a success envelope (with credentials) or the same `CONFIG_ERROR` (without). The dispatch path is what's being verified.
|
|
225
|
+
|
|
226
|
+
If either fails, the most common causes are:
|
|
227
|
+
- `bin` not executable (`chmod +x`)
|
|
228
|
+
- `config.yaml` `skill:` and `bin:` paths not absolute (must be absolute, not `~/...`)
|
|
229
|
+
- `index.mjs` import path wrong (must be `narai-primitives/toolkit`, not the legacy `@narai/connector-toolkit`)
|
|
230
|
+
|
|
231
|
+
## Next steps (tell the user)
|
|
232
|
+
|
|
233
|
+
After verification:
|
|
234
|
+
|
|
235
|
+
1. **Set the credential**: `export <ENV_VAR>="…"` (or persist in their shell rc).
|
|
236
|
+
2. **Run a real action**: `node <bin> --action <action> --params '<real-params>'`. Should return `{"status":"success","data":…}`.
|
|
237
|
+
3. **(Optional) Add tests**: drop happy-path tests in `tests/` using vitest if installed locally, or skip — these are local connectors; tests are nice-to-have, not required.
|
|
238
|
+
4. **(If broadly useful)** Send a PR to `narailabs/narai-primitives` to promote the connector to a builtin — see that repo's CONTRIBUTING.md for the contributor flow (different scaffolding, with a plugin layer, marketplace entry, etc.).
|
|
239
|
+
|
|
240
|
+
If the auth scheme was `oauth-with-refresh` or `custom`, also flag: *"You'll need to implement the OAuth/custom flow in the `loadCredentials` block of `index.mjs` before the connector will work against the live API."*
|
|
241
|
+
|
|
242
|
+
## Pointers
|
|
243
|
+
|
|
244
|
+
- **Reference**: `references/connector-anatomy.md` — the createConnector contract, envelope shape, error codes.
|
|
245
|
+
- **Auth patterns**: `references/auth-patterns.md` — auth scheme → `loadCredentials` template per scheme.
|
|
246
|
+
- **Action design**: `references/action-design.md` — Zod schema patterns, classifications, handler shape.
|
|
247
|
+
- **DB redirect**: `references/db-agent-pointer.md` — when to point the user at the bundled `db` connector instead.
|
|
248
|
+
- **Builtin connectors** in `narai-primitives` (canonical examples to read for inspiration):
|
|
249
|
+
- GitHub: https://github.com/narailabs/narai-primitives/tree/main/src/connectors/github
|
|
250
|
+
- Notion: https://github.com/narailabs/narai-primitives/tree/main/src/connectors/notion
|
|
251
|
+
- DB (policy-gated, more complex): https://github.com/narailabs/narai-primitives/tree/main/src/connectors/db
|
|
252
|
+
- **Legacy version of this skill** (when connectors used to scaffold as full `@narai/<svc>-agent-connector` repos with their own npm package + plugin layer): see `SKILL.legacy.md` in this directory.
|
package/plugins/create-connector/skills/create-connector/assets/templates/connector-SKILL.md.tmpl
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: {{SLUG}}-connector
|
|
3
|
+
description: {{DESCRIPTION}}
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# {{ServicePascal}} connector
|
|
7
|
+
|
|
8
|
+
{{DESCRIPTION}}
|
|
9
|
+
|
|
10
|
+
Loaded by `gather()` from `narai-primitives` via the `connectors.{{SLUG}}` entry in your `.connectors/config.yaml`. Spawn-isolated: each invocation runs in its own Node subprocess.
|
|
11
|
+
|
|
12
|
+
## Invocation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
./bin/{{SLUG}} --action <action> --params '<json>'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The CLI prints a single JSON envelope on stdout:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{ "status": "success", "action": "<name>", "data": { ... } }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Status can be `success`, `error`, `denied`, or `escalate`. Errors carry an `error_code` (`CONFIG_ERROR`, `AUTH_ERROR`, `UPSTREAM_ERROR`, …).
|
|
25
|
+
|
|
26
|
+
## Actions
|
|
27
|
+
|
|
28
|
+
{{ACTIONS_TABLE_MD}}
|
|
29
|
+
|
|
30
|
+
## Credentials
|
|
31
|
+
|
|
32
|
+
This connector reads `{{CREDENTIAL_ENV_VAR}}` from the environment.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
export {{CREDENTIAL_ENV_VAR}}="…"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If unset, every action returns `{ "status": "error", "error_code": "CONFIG_ERROR", … }` without touching the API.
|
|
39
|
+
|
|
40
|
+
## Hub usage
|
|
41
|
+
|
|
42
|
+
The hub plans actions automatically based on the user's natural-language prompt:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { gather } from "narai-primitives";
|
|
46
|
+
const out = await gather({ prompt: "{{FIRST_ACTION_PROMPT_EXAMPLE}}" });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The planner reads this SKILL.md to decide when to call which action.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* {{ServicePascal}} connector — {{DESCRIPTION}}
|
|
4
|
+
*
|
|
5
|
+
* Local custom connector loaded by `gather()` from `narai-primitives` via
|
|
6
|
+
* the `connectors.{{SLUG}}` entry in your `.connectors/config.yaml`.
|
|
7
|
+
*
|
|
8
|
+
* Run via the bin shim: ./bin/{{SLUG}} --action <name> --params '<json>'
|
|
9
|
+
*/
|
|
10
|
+
import { createConnector } from "narai-primitives/toolkit";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
const API_BASE = "{{API_BASE}}";
|
|
14
|
+
const RATE_LIMIT_PER_MIN = {{RATE_LIMIT_PER_MIN}};
|
|
15
|
+
|
|
16
|
+
// ── Credentials ──────────────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// `loadCredentials` runs once per spawn before any handler. Throwing here
|
|
19
|
+
// produces a {status: "error", error_code: "CONFIG_ERROR", …} envelope
|
|
20
|
+
// without touching the API.
|
|
21
|
+
|
|
22
|
+
async function loadCredentials() {
|
|
23
|
+
const token = process.env["{{CREDENTIAL_ENV_VAR}}"];
|
|
24
|
+
if (!token) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Missing {{CREDENTIAL_ENV_VAR}}. Export it before running this connector.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return { token };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── HTTP client ──────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
async function request(creds, method, path, body) {
|
|
35
|
+
const url = new URL(path, API_BASE);
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
method,
|
|
38
|
+
headers: {
|
|
39
|
+
{{AUTH_HEADER_ENTRY}}
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"User-Agent": "narai-custom-{{SLUG}}",
|
|
42
|
+
},
|
|
43
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const text = await res.text().catch(() => "");
|
|
47
|
+
throw Object.assign(new Error(`{{ServicePascal}} ${method} ${path} → ${res.status}: ${text.slice(0, 200)}`), {
|
|
48
|
+
code: res.status === 401 || res.status === 403 ? "AUTH_ERROR" : "UPSTREAM_ERROR",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return await res.json();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Action surface ───────────────────────────────────────────────────
|
|
55
|
+
//
|
|
56
|
+
// Each entry is { description, params (Zod), classify, handler }.
|
|
57
|
+
// `handler` receives validated params and the loaded credentials; the
|
|
58
|
+
// envelope (success / error / denied / escalate) is wrapped by createConnector.
|
|
59
|
+
|
|
60
|
+
const connector = createConnector({
|
|
61
|
+
name: "{{SLUG}}",
|
|
62
|
+
version: "0.1.0",
|
|
63
|
+
credentials: loadCredentials,
|
|
64
|
+
|
|
65
|
+
actions: {
|
|
66
|
+
{{ACTIONS_DICTIONARY}}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Optional: if you need rate-limit wrapping, plug in a token-bucket here.
|
|
70
|
+
// The toolkit doesn't enforce limits by itself.
|
|
71
|
+
// rateLimit: { perMinute: RATE_LIMIT_PER_MIN },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
connector.main(process.argv.slice(2)).then(
|
|
75
|
+
(code) => process.exit(code),
|
|
76
|
+
(err) => {
|
|
77
|
+
process.stderr.write(`Unhandled: ${err?.stack ?? err}\n`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
},
|
|
80
|
+
);
|
package/plugins/create-connector/skills/create-connector/assets/templates/tests-example.mjs.tmpl
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for the {{SLUG}} connector.
|
|
3
|
+
*
|
|
4
|
+
* Runs the bin in a subprocess with no credentials set — should produce a
|
|
5
|
+
* CONFIG_ERROR envelope (not a thrown exception). That confirms the
|
|
6
|
+
* dispatch plumbing is sound; live API behavior is out of scope here.
|
|
7
|
+
*
|
|
8
|
+
* Run with: npx vitest run (vitest must be installed somewhere)
|
|
9
|
+
* -- or -- node --test tests/example.test.mjs (Node's built-in runner)
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const BIN = join(HERE, "..", "bin", "{{SLUG}}");
|
|
18
|
+
|
|
19
|
+
function run(args, env = {}) {
|
|
20
|
+
// Strip the credential env var so we get the deterministic CONFIG_ERROR.
|
|
21
|
+
const cleanEnv = { ...process.env, ...env };
|
|
22
|
+
delete cleanEnv["{{CREDENTIAL_ENV_VAR}}"];
|
|
23
|
+
return spawnSync(BIN, args, { encoding: "utf-8", env: cleanEnv });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("{{SLUG}} connector", () => {
|
|
27
|
+
it("emits a JSON envelope on stdout for {{FIRST_ACTION}}", () => {
|
|
28
|
+
const r = run(["--action", "{{FIRST_ACTION}}", "--params", "{}"]);
|
|
29
|
+
expect(r.status).toBe(0);
|
|
30
|
+
const env = JSON.parse(r.stdout.trim());
|
|
31
|
+
expect(env.action).toBe("{{FIRST_ACTION}}");
|
|
32
|
+
// No creds → CONFIG_ERROR. Status is 'error' but the envelope is well-formed.
|
|
33
|
+
expect(env.status).toBe("error");
|
|
34
|
+
expect(env.error_code).toBe("CONFIG_ERROR");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects an unknown action with a structured error", () => {
|
|
38
|
+
const r = run(["--action", "nope_does_not_exist", "--params", "{}"]);
|
|
39
|
+
const env = JSON.parse(r.stdout.trim());
|
|
40
|
+
expect(env.status).toBe("error");
|
|
41
|
+
// The toolkit emits VALIDATION_ERROR or ACTION_NOT_FOUND depending on
|
|
42
|
+
// version — we only assert it's NOT a thrown exception.
|
|
43
|
+
expect(typeof env.error_code).toBe("string");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Action design
|
|
2
|
+
|
|
3
|
+
How to design the action surface of a new connector — what to expose, how to validate, and what classifications to assign.
|
|
4
|
+
|
|
5
|
+
## What an action is
|
|
6
|
+
|
|
7
|
+
An action is a named operation the connector exposes. From the user's perspective, it's `--action <name> --params '<json>'`. From the toolkit's perspective, it's an entry in the `actions` dictionary with four fields:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
get_customer: {
|
|
11
|
+
description: "Fetch a customer by ID",
|
|
12
|
+
params: z.object({ id: z.string().min(1) }),
|
|
13
|
+
classify: { kind: "read" },
|
|
14
|
+
handler: async (p, ctx) => { /* ... */ },
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Naming conventions
|
|
19
|
+
|
|
20
|
+
- **Snake case**, lowercase: `get_customer`, `list_charges`, `query_database`.
|
|
21
|
+
- **Verb-first**: `get_*`, `list_*`, `search_*`, `query_*` for reads; `create_*`, `update_*`, `delete_*`, `post_*` for writes.
|
|
22
|
+
- **Don't pluralize**: `list_customers` (plural noun) is fine; `gets_customer` is not.
|
|
23
|
+
|
|
24
|
+
The naming itself is what the skill uses as a *signal* for default classification — anything starting with `create_`, `update_`, `delete_`, `post_`, `send_`, `revoke_` defaults to non-read.
|
|
25
|
+
|
|
26
|
+
## Param schemas with Zod
|
|
27
|
+
|
|
28
|
+
The params field is a Zod schema. The toolkit calls `params.parse(rawParams)` before your handler runs; failures become `VALIDATION_ERROR` envelopes automatically.
|
|
29
|
+
|
|
30
|
+
Common patterns:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// String IDs
|
|
34
|
+
const getCustomerParams = z.object({
|
|
35
|
+
id: z.string().min(1, "id is required"),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Optional with default
|
|
39
|
+
const listChargesParams = z.object({
|
|
40
|
+
customer_id: z.string().optional(),
|
|
41
|
+
limit: z.coerce.number().int().positive().max(100).default(25),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// UUID validation
|
|
45
|
+
const UUID_RE = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32})$/;
|
|
46
|
+
const getPageParams = z.object({
|
|
47
|
+
page_id: z.string().regex(UUID_RE, "Invalid page_id — expected UUID format"),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Enum
|
|
51
|
+
const searchParams = z.object({
|
|
52
|
+
query: z.string().min(1),
|
|
53
|
+
filter_type: z.enum(["page", "database"]).optional(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Free-form JSON
|
|
57
|
+
const queryParams = z.object({
|
|
58
|
+
filter: z.record(z.unknown()).nullable().default(null),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`z.coerce.number()` is useful because CLI params come in as strings; it converts them to numbers without the user having to quote them differently.
|
|
63
|
+
|
|
64
|
+
## Classification
|
|
65
|
+
|
|
66
|
+
Every action carries a `classify: { kind: ... }`. The toolkit's policy gate uses this to decide whether to run the handler, ask for approval, escalate, or deny.
|
|
67
|
+
|
|
68
|
+
| `kind` | Examples | Default approval mode |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `read` | `get_*`, `list_*`, `search_*`, `query_*` | `auto` (no prompt) |
|
|
71
|
+
| `write` | `create_*`, `update_*`, `post_*`, `send_*` | `confirm_once` (per session) |
|
|
72
|
+
| `delete` | `delete_*`, `remove_*`, `archive_*` | `confirm_each` (per call) |
|
|
73
|
+
| `admin` | role/permission changes, user provisioning | `grant_required` (out-of-band) |
|
|
74
|
+
| `privilege` | `grant_*`, `revoke_*` on access controls | `grant_required` |
|
|
75
|
+
|
|
76
|
+
The defaults are sensible but not hard rules; the user can override per action during interview.
|
|
77
|
+
|
|
78
|
+
## The handler signature
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
handler: async (params, ctx) => {
|
|
82
|
+
// params is fully validated (Zod has run)
|
|
83
|
+
// ctx has: sdk (your client), creds, name, action
|
|
84
|
+
const result = await ctx.sdk.getCustomer(params.id);
|
|
85
|
+
throwIfError(result); // converts !ok results into a thrown ServiceError
|
|
86
|
+
return {
|
|
87
|
+
id: result.data.id,
|
|
88
|
+
email: result.data.email,
|
|
89
|
+
// shape what you return — this becomes envelope.data
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Don't return the raw API response — pick out the fields the user actually wants. The shape you return is the contract callers see.
|
|
95
|
+
|
|
96
|
+
If you throw, the toolkit catches and runs `mapError(err)` to translate. If `mapError` returns `undefined`, the toolkit falls back to `CONNECTION_ERROR` with the error message.
|
|
97
|
+
|
|
98
|
+
## Approval modes (when classification is non-read)
|
|
99
|
+
|
|
100
|
+
The toolkit reads the connector's policy config (`~/.connectors/<slug>/config.yaml` or `.connectors/<slug>/config.yaml` in cwd) and resolves an approval mode per action:
|
|
101
|
+
|
|
102
|
+
- `auto` — handler runs, no prompt.
|
|
103
|
+
- `confirm_once` — first call in a session prompts the user; subsequent calls auto-allow.
|
|
104
|
+
- `confirm_each` — every call prompts.
|
|
105
|
+
- `grant_required` — refuses to run unless the user has issued a `--grant` token via the toolkit's grant CLI.
|
|
106
|
+
|
|
107
|
+
When the user defines a write/delete action during interview, the skill asks which approval mode to default to. Most users want `confirm_once` for writes, `grant_required` for delete/admin/privilege.
|
|
108
|
+
|
|
109
|
+
**Write-action happy-path tests need a permissive config in scope.** The toolkit's default policy escalates writes when no operator config is present. So for a connector that exposes any non-`read` action, the unit tests that assert `success` for the write action must set up a temp `HOME`/`cwd` with a permissive `.{name}-agent/config.yaml` (e.g., `policy: { write: success }, approval_mode: auto`) in `beforeAll`, and tear it down in `afterAll`. Read-only happy-path tests don't need this — `read` defaults to `allow`. The skill's `tests/integration/framework.test.ts.tmpl` already does this pattern for the escalate test; mirror it for write-action happy paths.
|
|
110
|
+
|
|
111
|
+
## Non-HTTP shapes (CLI-wrap, RPC, anything not REST/GraphQL)
|
|
112
|
+
|
|
113
|
+
The default `client.ts.tmpl` is HTTP-shaped: it bakes in `validateUrl`, `_throttle`, `_authHeaders`, `request<T>(method, relPath, ...)`, `Retry-After` parsing, and `fetchImpl` injection. For connectors that don't speak HTTP — e.g., wrapping a CLI binary via `child_process.execFile`, an RPC client, or anything else — write the client from scratch instead of trying to retrofit the HTTP frame. Preserve these load-bearing contracts so the rest of the toolkit fits:
|
|
114
|
+
|
|
115
|
+
- The same `Result<T>` discriminated union: `{ ok: true; data: T; status: number } | { ok: false; code: string; message: string; retriable: boolean; status?: number }`. Many tests + the `index.ts` handlers depend on this shape.
|
|
116
|
+
- An exported `load<Service>Credentials()` function (returns `{}` if the wrapped tool authenticates itself locally, or whatever object the client constructor expects).
|
|
117
|
+
- The matching `<Service>Error` bridge class in `error.ts`.
|
|
118
|
+
- The same `CODE_MAP` shape in `index.ts`.
|
|
119
|
+
|
|
120
|
+
For test-time injection, mirror the `fetchImpl` pattern with a shape-appropriate analog (e.g., `execFileImpl` for CLI wraps, `clientImpl` for RPC). Tests then stub the analog instead of `fetchImpl`. The `cli.test.ts` and `client_extras.test.ts` template helpers will need adapting — that's expected for non-HTTP shapes.
|
|
121
|
+
|
|
122
|
+
## What NOT to expose as actions
|
|
123
|
+
|
|
124
|
+
- **Internal pagination cursors** — handle pagination inside the handler; expose a single `list_*` action that does the full pass (capped at a reasonable max).
|
|
125
|
+
- **Auth flows** — credentials come from env/config; never let the user pass them as action params.
|
|
126
|
+
- **Raw HTTP plumbing** — if the user could call your underlying client directly, they'd just use `curl`. The connector's value is the *shaped, validated, gated* surface.
|
|
127
|
+
- **Secrets in responses** — the toolkit scrubs known secret patterns, but don't rely on it; redact in the handler.
|
|
128
|
+
|
|
129
|
+
## Examples from existing connectors
|
|
130
|
+
|
|
131
|
+
- `notion-agent-connector` — 7 actions: `search`, `get_page`, `get_database`, `query_database`, `list_attachments`, `get_attachment`, `get_comments`. All `read`.
|
|
132
|
+
- `github-agent-connector` — multiple read actions over Octokit. All `read`.
|
|
133
|
+
- `db-agent-connector` — single `query` action; classification is computed at runtime by parsing the SQL. (This is the one connector where the classification is dynamic.)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Auth patterns
|
|
2
|
+
|
|
3
|
+
The skill supports the common auth schemes natively. For each, this doc shows what the scaffold produces and what the user has to provide.
|
|
4
|
+
|
|
5
|
+
## Pattern 1: Bearer token via env var (default)
|
|
6
|
+
|
|
7
|
+
The dominant case. Notion, Linear, Stripe, most public REST APIs.
|
|
8
|
+
|
|
9
|
+
**Interview answer**: `bearer-token-env-var` + env var name (e.g., `STRIPE_API_KEY`).
|
|
10
|
+
|
|
11
|
+
**Scaffold output**:
|
|
12
|
+
|
|
13
|
+
- `src/cli.ts`:
|
|
14
|
+
```ts
|
|
15
|
+
const STRIPE_ENV_MAPPING: Record<string, string> = {
|
|
16
|
+
token: "STRIPE_API_KEY",
|
|
17
|
+
};
|
|
18
|
+
```
|
|
19
|
+
- `src/lib/stripe_client.ts` `loadStripeCredentials()`:
|
|
20
|
+
```ts
|
|
21
|
+
const token = (await resolveSecret("STRIPE_API_KEY")) ?? process.env["STRIPE_API_KEY"] ?? null;
|
|
22
|
+
if (!token) return null;
|
|
23
|
+
return { token };
|
|
24
|
+
```
|
|
25
|
+
- `_authHeaders()`:
|
|
26
|
+
```ts
|
|
27
|
+
return {
|
|
28
|
+
Authorization: `Bearer ${this._token}`,
|
|
29
|
+
"User-Agent": USER_AGENT,
|
|
30
|
+
Accept: "application/json",
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Pattern 2: API-key header (X-API-Key etc.)
|
|
35
|
+
|
|
36
|
+
For services that reject `Authorization: Bearer` and want a custom header. Think internal APIs, some partner integrations.
|
|
37
|
+
|
|
38
|
+
**Interview answer**: `api-key-header-env-var` + env var name + header name (e.g., `X-API-Key`).
|
|
39
|
+
|
|
40
|
+
**Scaffold differences from pattern 1**: `_authHeaders()` becomes:
|
|
41
|
+
```ts
|
|
42
|
+
return {
|
|
43
|
+
"X-API-Key": this._token,
|
|
44
|
+
"User-Agent": USER_AGENT,
|
|
45
|
+
Accept: "application/json",
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The `loadCredentials` and `cli.ts` env mapping are otherwise the same — only the header construction differs.
|
|
50
|
+
|
|
51
|
+
## Pattern 3: Multi-secret (token + extra context)
|
|
52
|
+
|
|
53
|
+
Like GitHub, where the token alone isn't sufficient — you also need an owner / org / workspace ID. The cli.ts env mapping has multiple entries:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const GITHUB_ENV_MAPPING: Record<string, string> = {
|
|
57
|
+
token: "GITHUB_TOKEN",
|
|
58
|
+
owner: "GITHUB_OWNER",
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Scaffold differences**:
|
|
63
|
+
- `ServiceClientOptions` adds extra fields (`owner: string`, etc.).
|
|
64
|
+
- `loadCredentials()` resolves all of them; missing required fields return `null`.
|
|
65
|
+
- The client class stores the extras as private fields and uses them when constructing URLs (e.g., `${this._apiBase}/repos/${this._owner}/...`).
|
|
66
|
+
|
|
67
|
+
## Pattern 4: Basic auth (username + password)
|
|
68
|
+
|
|
69
|
+
Rare in modern APIs but still seen with internal tooling.
|
|
70
|
+
|
|
71
|
+
**Interview answer**: `basic-auth` + env var names for user + pass.
|
|
72
|
+
|
|
73
|
+
**Scaffold differences**: `_authHeaders()` becomes:
|
|
74
|
+
```ts
|
|
75
|
+
const credentials = Buffer.from(`${this._user}:${this._pass}`).toString("base64");
|
|
76
|
+
return {
|
|
77
|
+
Authorization: `Basic ${credentials}`,
|
|
78
|
+
"User-Agent": USER_AGENT,
|
|
79
|
+
Accept: "application/json",
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Pattern 5: OAuth with refresh tokens (NOT TEMPLATED)
|
|
84
|
+
|
|
85
|
+
The skill does **not** scaffold OAuth flows. None of the existing connectors implement this; it's genuinely novel territory per service.
|
|
86
|
+
|
|
87
|
+
**What the scaffold produces**: a placeholder `loadCredentials()` that throws `CONFIG_ERROR` with a TODO message:
|
|
88
|
+
```ts
|
|
89
|
+
export async function loadServiceCredentials(): Promise<{ access_token: string } | null> {
|
|
90
|
+
// TODO: implement OAuth flow.
|
|
91
|
+
// - Read a refresh token from `resolveSecret("SERVICE_REFRESH_TOKEN")`
|
|
92
|
+
// - Exchange it at the provider's token endpoint for an access token
|
|
93
|
+
// - Cache the access token (in-memory or via credential-providers) until expiry
|
|
94
|
+
// See https://oauth.net/2/grant-types/refresh-token/ for the canonical flow.
|
|
95
|
+
throw new Error("OAuth not implemented — see TODO in loadServiceCredentials");
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The user fills this in. The skill flags this clearly during interview and the post-scaffold next-steps.
|
|
100
|
+
|
|
101
|
+
## Pattern 6: Custom (mTLS, signed URLs, anything else)
|
|
102
|
+
|
|
103
|
+
For schemes that don't fit any of the above (mutual TLS, request signing, ephemeral session tokens). The skill scaffolds with a simplified `loadCredentials()` that returns an empty object and points the user at:
|
|
104
|
+
|
|
105
|
+
- `notion-agent-connector/src/lib/notion_client.ts` for a Bearer reference
|
|
106
|
+
- `aws-agent-connector` (in the workspace) for SDK-mediated credentials
|
|
107
|
+
|
|
108
|
+
## Where credentials get resolved at runtime
|
|
109
|
+
|
|
110
|
+
The `cli.ts` runs `loadConnectorEnvironment("<slug>", { envMapping })` from `@narai/connector-config`. This:
|
|
111
|
+
|
|
112
|
+
1. Reads `~/.connectors/config.yaml` (or `NARAI_CONFIG_BLOB` if injected by `connector-hub`).
|
|
113
|
+
2. Maps configured fields (`token`, `owner`, etc.) to env var names per `envMapping`.
|
|
114
|
+
3. Sets `process.env.<NAME>` only if not already set — existing env wins.
|
|
115
|
+
|
|
116
|
+
Inside the client, `resolveSecret(envName)` from `@narai/credential-providers` checks cloud-backed stores first, then falls back to `process.env`. This means a user can either:
|
|
117
|
+
|
|
118
|
+
- Set `STRIPE_API_KEY` directly in their shell, OR
|
|
119
|
+
- Configure it in `~/.connectors/config.yaml`, OR
|
|
120
|
+
- Register a custom credential provider via `@narai/credential-providers`
|
|
121
|
+
|
|
122
|
+
All three paths work without code changes.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Connector anatomy
|
|
2
|
+
|
|
3
|
+
The canonical shape of an action-dispatch connector built on `@narai/connector-toolkit@^3.x`. Read this when you need a fuller picture than what `SKILL.md` covers — typically when scaffolding something unusual or debugging why a generated file is shaped the way it is.
|
|
4
|
+
|
|
5
|
+
## The two surfaces
|
|
6
|
+
|
|
7
|
+
Every connector exposes two consumption modes from the same code:
|
|
8
|
+
|
|
9
|
+
- **CLI** — `npx <pkg> --action <name> --params '<json>'` emits a JSON envelope on stdout. Implemented by `connector.main(argv)` from the toolkit. Stdout is JSON-only; diagnostics go to stderr.
|
|
10
|
+
- **Library** — `import { fetch } from "@narai/<pkg>"; await fetch(action, params)` returns the same envelope as a JS object. Implemented by `connector.fetch(action, params)`.
|
|
11
|
+
|
|
12
|
+
Both are returned by a single `createConnector<TSdk>` call. You don't write the dispatch logic — the toolkit does.
|
|
13
|
+
|
|
14
|
+
## The factory call
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
return createConnector<MyClient>({
|
|
18
|
+
name: "myservice", // connector slug; used for audit, hardship, policy
|
|
19
|
+
version: "0.1.0",
|
|
20
|
+
scope: (ctx) => ctx.sdk.workspaceId, // optional: returns a tenant id; null = global tier
|
|
21
|
+
credentials: async () => ({...}), // resolved creds; passed to sdk if you call it manually
|
|
22
|
+
sdk: async () => new MyClient({...}), // build the client; called once per main()/fetch()
|
|
23
|
+
actions: { /* action dictionary */ },
|
|
24
|
+
mapError: (err) => {/* ... */}, // optional: translate exceptions to canonical envelopes
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The toolkit owns:
|
|
29
|
+
|
|
30
|
+
- Argument parsing (`parseAgentArgs` strict whitelist)
|
|
31
|
+
- Zod validation of `params` per action
|
|
32
|
+
- Policy-gate evaluation (classification + approval mode + rules)
|
|
33
|
+
- Audit JSONL emission
|
|
34
|
+
- Hardship JSONL recording
|
|
35
|
+
- Envelope construction (success / denied / escalate / error)
|
|
36
|
+
- The `--curate` flag
|
|
37
|
+
|
|
38
|
+
You own:
|
|
39
|
+
|
|
40
|
+
- Action definitions (Zod schema + classification + handler)
|
|
41
|
+
- The HTTP/SDK client itself
|
|
42
|
+
- Error mapping from your client's internal codes to toolkit's canonical taxonomy
|
|
43
|
+
|
|
44
|
+
## The action dictionary
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
actions: {
|
|
48
|
+
get_customer: {
|
|
49
|
+
description: "Fetch a customer by ID",
|
|
50
|
+
params: z.object({ id: z.string().min(1) }),
|
|
51
|
+
classify: { kind: "read" },
|
|
52
|
+
handler: async (p, ctx) => {
|
|
53
|
+
const result = await ctx.sdk.getCustomer(p.id);
|
|
54
|
+
throwIfError(result);
|
|
55
|
+
return result.data; // becomes envelope.data
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
// ...
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `description` — surfaced by `--help` and the toolkit's introspection. Keep it tight.
|
|
63
|
+
- `params` — a Zod schema. The toolkit validates inputs before calling the handler; validation failures become a `VALIDATION_ERROR` envelope with no handler invocation.
|
|
64
|
+
- `classify` — `{ kind: "read" | "write" | "delete" | "admin" | "privilege" }`. Drives the policy gate and approval-mode resolution. Default to `read` unless the action genuinely mutates state.
|
|
65
|
+
- `handler` — receives validated params + a context with `sdk` (your client), `creds`, `name`, `action`. Whatever you return becomes the envelope's `data` field. If you throw, `mapError` decides how to translate.
|
|
66
|
+
|
|
67
|
+
## The Result-envelope client pattern
|
|
68
|
+
|
|
69
|
+
Inside `src/lib/<svc>_client.ts`, methods return a discriminated union rather than throwing:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export type ServiceResult<T> =
|
|
73
|
+
| { ok: true; data: T; status: number }
|
|
74
|
+
| { ok: false; code: string; message: string; retriable: boolean; status?: number };
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Why: it lets the client express *expected failures* (rate limit, auth error, network) declaratively, while the toolkit's handler-throws-an-Error contract takes over for the *unexpected* path. The bridge is `throwIfError(result)` in the handler:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
function throwIfError<T>(r: ServiceResult<T>): asserts r is Extract<ServiceResult<T>, { ok: true }> {
|
|
81
|
+
if (!r.ok) throw new ServiceError(r.code, r.message, r.retriable, r.status);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`ServiceError` carries the service-internal code; `mapError` translates it to a toolkit canonical code (`AUTH_ERROR`, `NOT_FOUND`, `RATE_LIMITED`, `TIMEOUT`, `VALIDATION_ERROR`, `CONFIG_ERROR`, `CONNECTION_ERROR`).
|
|
86
|
+
|
|
87
|
+
## Required client features
|
|
88
|
+
|
|
89
|
+
The frame in `client.ts.tmpl` covers all of these — don't reinvent:
|
|
90
|
+
|
|
91
|
+
- **Sliding-window rate limit** (per-minute) with a configurable cap.
|
|
92
|
+
- **Retry loop** (max 4 attempts) on retriable errors: 5xx, 429, network/timeout. Honors `Retry-After` header when present; falls back to exponential backoff (capped at 30s).
|
|
93
|
+
- **HTTP method allowlist**: only `GET`, `POST`, `PUT`, `PATCH`, `DELETE` reach the wire. The default template covers all five so you don't need to extend.
|
|
94
|
+
- **URL validation** via `validateUrl` from the toolkit (rejects non-http/https).
|
|
95
|
+
- **Dependency injection**: `fetchImpl` and `sleepImpl` are constructor options so tests can mock without touching the global fetch.
|
|
96
|
+
- **Configurable timeouts**: `connectTimeoutMs` (default 10s) + `readTimeoutMs` (default 30s); enforced by an `AbortController`.
|
|
97
|
+
|
|
98
|
+
## Error code taxonomy
|
|
99
|
+
|
|
100
|
+
| Toolkit code | When |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `AUTH_ERROR` | 401, 403, missing/invalid credentials |
|
|
103
|
+
| `NOT_FOUND` | 404 |
|
|
104
|
+
| `RATE_LIMITED` | 429 |
|
|
105
|
+
| `TIMEOUT` | Request aborted on timer |
|
|
106
|
+
| `VALIDATION_ERROR` | 400, 422, Zod validation failure, invalid URL/method |
|
|
107
|
+
| `CONFIG_ERROR` | Credentials not configured, connector misconfigured |
|
|
108
|
+
| `CONNECTION_ERROR` | 5xx, network errors, anything else |
|
|
109
|
+
|
|
110
|
+
Map your service's internal codes (e.g., Stripe's `card_declined`) to these via `CODE_MAP`.
|
|
111
|
+
|
|
112
|
+
## What to read in the codebase for examples
|
|
113
|
+
|
|
114
|
+
- `/Users/narayan/src/connectors/notion-agent-connector/src/index.ts` — full factory call with 7 actions
|
|
115
|
+
- `/Users/narayan/src/connectors/notion-agent-connector/src/lib/notion_client.ts` — full Result-envelope client
|
|
116
|
+
- `/Users/narayan/src/connectors/connector-toolkit/src/index.ts` — toolkit's public API surface (the contract you're consuming)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Pointer: when to use db-agent-connector instead
|
|
2
|
+
|
|
3
|
+
If the user wants to query a database from Claude, the answer is almost always **install and configure the existing `db-agent-connector`**, not scaffold a new connector. This doc explains why and how to redirect.
|
|
4
|
+
|
|
5
|
+
## What db-agent-connector already does
|
|
6
|
+
|
|
7
|
+
Lives at `/Users/narayan/src/connectors/db-agent-connector/`. Single CLI surface, single library API, single Claude Code plugin — but speaks to **seven** database systems through a pluggable driver registry:
|
|
8
|
+
|
|
9
|
+
- PostgreSQL (`pg`)
|
|
10
|
+
- MySQL / MariaDB (`mysql2`)
|
|
11
|
+
- SQLite (`better-sqlite3`)
|
|
12
|
+
- Microsoft SQL Server (`mssql`)
|
|
13
|
+
- MongoDB (`mongodb`)
|
|
14
|
+
- Amazon DynamoDB (`@aws-sdk/client-dynamodb`)
|
|
15
|
+
- Oracle (`oracledb`)
|
|
16
|
+
|
|
17
|
+
Drivers are `optionalDependencies`, lazy-loaded on first use. A user querying only SQLite doesn't carry the weight of `pg` or `mssql`.
|
|
18
|
+
|
|
19
|
+
## Why a fresh action-dispatch connector is the wrong shape
|
|
20
|
+
|
|
21
|
+
- **Multi-state envelope**: db-agent emits `ok` / `present_only` / `denied` / `escalate` / `error`. The `present_only` state is unique to db-agent — it returns formatted SQL for the user to review without executing. Action-dispatch connectors don't have this state, and adding it ad-hoc would mean reimplementing db-agent's policy engine.
|
|
22
|
+
- **Operator-configurable safety**: db-agent reads policy rules from `~/.connectors/config.yaml` (V2.0 layout) under the `connectors.db.policy` slice: classifications `read` / `write` / `delete` / `admin` / `privilege` map to actions `allow` / `present` / `escalate` / `deny`. Plus `unbounded_select` (escalate SELECTs without WHERE/LIMIT) and a safety floor that rejects `admin: allow` and `privilege: allow`. Operator-tunable per server via the optional per-server `policy` block.
|
|
23
|
+
- **SQL keyword classification**: db-agent classifies queries by parsing the SQL keyword (`SELECT` → read, `INSERT/UPDATE/DELETE` → write, `CREATE/ALTER/DROP` → delete, `GRANT/REVOKE` → privilege). The toolkit's static `classify` field can't do this because the action shape is just "run this query".
|
|
24
|
+
- **Stateful connection pooling** with per-server config (driver, host, db, credentials, approval mode).
|
|
25
|
+
|
|
26
|
+
If a new connector tried to replicate this, it would be a ~2000-line lift duplicating work db-agent already shipped.
|
|
27
|
+
|
|
28
|
+
## How to redirect the user
|
|
29
|
+
|
|
30
|
+
When the interview triages to `database`:
|
|
31
|
+
|
|
32
|
+
1. **Identify the backend**: ask which DB engine. If it's one of the seven listed above, use db-agent. If it's something else (Snowflake, BigQuery, ClickHouse, Redis), see "Genuinely novel backends" below.
|
|
33
|
+
|
|
34
|
+
2. **Confirm db-agent is installed**: check whether `/Users/narayan/src/connectors/db-agent-connector/` has `node_modules/`. If not, `cd` there and run `npm install`.
|
|
35
|
+
|
|
36
|
+
3. **Walk through `~/.connectors/config.yaml`** (V2.0 layout — single shared config file with a `connectors.db.*` slice). Tell the user to read `db-agent-connector/CLAUDE.md` for the authoritative format. A minimal example:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
connectors:
|
|
40
|
+
db:
|
|
41
|
+
policy:
|
|
42
|
+
read: allow
|
|
43
|
+
write: present
|
|
44
|
+
delete: present
|
|
45
|
+
admin: deny # safety floor — `allow` is rejected by validation
|
|
46
|
+
privilege: deny # safety floor — `allow` is rejected by validation
|
|
47
|
+
unbounded_select: escalate # SELECTs lacking WHERE/LIMIT/OFFSET get escalated
|
|
48
|
+
|
|
49
|
+
servers:
|
|
50
|
+
prod:
|
|
51
|
+
driver: postgres
|
|
52
|
+
host: db.acme.com
|
|
53
|
+
database: app
|
|
54
|
+
user: readonly
|
|
55
|
+
password: env:PROD_PG_PASSWORD # provider:key form — see below
|
|
56
|
+
approval_mode: confirm_each
|
|
57
|
+
|
|
58
|
+
audit:
|
|
59
|
+
enabled: true
|
|
60
|
+
path: ~/.connectors/db-agent/audit.jsonl
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Repo-level overlay: `./.connectors/config.yaml` wins on per-key conflicts with the user-level file.
|
|
64
|
+
|
|
65
|
+
Credential references inside the slice use a `provider:key` form: `env:VAR_NAME`, `keychain:NAME`, `file:path.json:dotted.key`, `cloud:alias`. Plain strings without a recognized prefix pass through as literals. Set the env var in your shell as usual.
|
|
66
|
+
|
|
67
|
+
4. **Smoke test**: `npx db-agent-connector --action query --params '{"server":"prod","sql":"SELECT 1"}'`.
|
|
68
|
+
|
|
69
|
+
The skill should produce this redirect without scaffolding any new files.
|
|
70
|
+
|
|
71
|
+
## Genuinely novel backends
|
|
72
|
+
|
|
73
|
+
If the user wants to query a backend db-agent doesn't yet support (Snowflake, BigQuery, ClickHouse, Redis, etc.):
|
|
74
|
+
|
|
75
|
+
- **Suggest extending db-agent's driver registry**, not building a fresh connector. The driver interface lives at `db-agent-connector/src/lib/drivers/`. Adding a new driver gets the user the policy gate, audit, escalation, and approval modes for free.
|
|
76
|
+
- If the user *insists* on a fresh action-dispatch connector for a non-SQL store (e.g., a key-value lookup that doesn't need policy gating), the skill can scaffold it — but warn that they're duplicating infrastructure db-agent already provides.
|
|
77
|
+
|
|
78
|
+
## Critical files for the user to read
|
|
79
|
+
|
|
80
|
+
- `/Users/narayan/src/connectors/db-agent-connector/CLAUDE.md` — authoritative documentation for db-agent.
|
|
81
|
+
- `/Users/narayan/src/connectors/db-agent-connector/src/connector.ts` — the policy-gate orchestration.
|
|
82
|
+
- `/Users/narayan/src/connectors/db-agent-connector/src/lib/policy.ts` — the SQL keyword classifier.
|
|
83
|
+
- `/Users/narayan/src/connectors/db-agent-connector/src/lib/plugin_config.ts` — config schema.
|
|
84
|
+
- `/Users/narayan/src/connectors/db-agent-connector/src/lib/drivers/` — driver registry pattern (for extension).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# The Claude Code plugin layer
|
|
2
|
+
|
|
3
|
+
Every connector ships a `plugin/` subdirectory that lets Claude Code install and invoke it without any manual setup. Read this when scaffolding the plugin layer or debugging why the SessionStart hook isn't picking up changes.
|
|
4
|
+
|
|
5
|
+
## What lives in plugin/
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
plugin/
|
|
9
|
+
├── .claude-plugin/
|
|
10
|
+
│ └── plugin.json # Manifest (name, version, description, author)
|
|
11
|
+
├── hooks/
|
|
12
|
+
│ ├── hooks.json # SessionStart, PostToolUse, SessionEnd hooks
|
|
13
|
+
│ └── reminder.mjs # Best-effort SessionStart curation banner
|
|
14
|
+
├── bin/
|
|
15
|
+
│ └── <svc>-agent # Bash shim that execs the published CLI
|
|
16
|
+
├── skills/
|
|
17
|
+
│ └── <svc>-agent/
|
|
18
|
+
│ └── SKILL.md # Tells Claude how/when to invoke the bin
|
|
19
|
+
└── commands/
|
|
20
|
+
└── <svc>-agent.md # Slash command (/<svc>-agent <action> <params>)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The whole `plugin/` tree is excluded from the npm tarball via `.npmignore`. Claude Code installs from the source repo, not from npm.
|
|
24
|
+
|
|
25
|
+
## The SessionStart hook (hooks.json)
|
|
26
|
+
|
|
27
|
+
Three commands run at session start, in order:
|
|
28
|
+
|
|
29
|
+
1. **Idempotent npm install**
|
|
30
|
+
```
|
|
31
|
+
diff -q "${CLAUDE_PLUGIN_ROOT}/package.json" "${CLAUDE_PLUGIN_DATA}/package.json" \
|
|
32
|
+
|| (mkdir -p "${CLAUDE_PLUGIN_DATA}" && cp "${CLAUDE_PLUGIN_ROOT}/package.json" "${CLAUDE_PLUGIN_DATA}/" \
|
|
33
|
+
&& cd "${CLAUDE_PLUGIN_DATA}" && npm install --no-audit --no-fund) \
|
|
34
|
+
|| rm -f "${CLAUDE_PLUGIN_DATA}/package.json"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Diffs the plugin's `package.json` against the cached copy in `${CLAUDE_PLUGIN_DATA}`. If different (or missing), copies + runs `npm install`. On failure, removes the partial copy so the next session retries cleanly. This is what makes `node_modules/@narai/<pkg>/dist/cli.js` available to the bash shim.
|
|
38
|
+
|
|
39
|
+
2. **Service-specific reminder** (`reminder.mjs`) — best-effort import of the toolkit's nudge module to print a banner if the user has enabled curation. Never blocks startup.
|
|
40
|
+
|
|
41
|
+
3. **Toolkit stale-summarize hook** — invokes `connector-toolkit/plugin-hooks/stale-summarize.mjs` to surface session activity. Tagged with `USAGE_CONNECTOR_NAME` for cross-connector aggregation.
|
|
42
|
+
|
|
43
|
+
## PostToolUse + SessionEnd hooks
|
|
44
|
+
|
|
45
|
+
- **PostToolUse** with matcher `Bash` — invokes `usage-record.mjs` after every Bash call to capture connector usage events. Tagged with `USAGE_CONNECTOR_NAME` and `USAGE_BIN_HINT` for telemetry.
|
|
46
|
+
- **SessionEnd** — invokes `session-summary.mjs` to write an end-of-session summary. Same tagging.
|
|
47
|
+
|
|
48
|
+
All four hook commands swallow errors (`|| true`) — no plugin should ever wedge a session.
|
|
49
|
+
|
|
50
|
+
## The bash shim (bin/<svc>-agent)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
#!/usr/bin/env bash
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
|
|
56
|
+
if [ -z "${CLAUDE_PLUGIN_DATA:-}" ]; then
|
|
57
|
+
echo "<svc>-agent: CLAUDE_PLUGIN_DATA is not set (run from inside Claude Code)" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
CLI="${CLAUDE_PLUGIN_DATA}/node_modules/@narai/<svc>-agent-connector/dist/cli.js"
|
|
62
|
+
|
|
63
|
+
if [ ! -f "$CLI" ]; then
|
|
64
|
+
echo "<svc>-agent: connector CLI not found at $CLI" >&2
|
|
65
|
+
echo "Restart your Claude Code session to re-run the SessionStart install hook." >&2
|
|
66
|
+
exit 2
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
exec node "$CLI" "$@"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`CLAUDE_PLUGIN_DATA` is set by the runtime to a session-specific cache directory. The shim hands off to `node` with `exec` so the Node process *replaces* the bash process — no fork overhead, signals propagate correctly.
|
|
73
|
+
|
|
74
|
+
If the CLI is missing, the shim tells the user to restart the session (which re-runs the SessionStart install hook). It does NOT try to install from inside the shim, because that would block on first invocation.
|
|
75
|
+
|
|
76
|
+
## The plugin SKILL.md (skills/<svc>-agent/SKILL.md)
|
|
77
|
+
|
|
78
|
+
This is what Claude actually reads to decide when to invoke the connector. Keep it tight:
|
|
79
|
+
|
|
80
|
+
- **Frontmatter**: `name`, `description`, `context: fork`. The description triggers the skill — make it specific to the service ("read-only Notion content"), not generic.
|
|
81
|
+
- **Body**: invocation syntax (`<svc>-agent --action <name> --params '<json>'`), supported actions table, credentials note, safety statement.
|
|
82
|
+
|
|
83
|
+
The `context: fork` directive matters: it tells Claude Code to load this skill into a subagent context rather than the main one, keeping conversation context clean.
|
|
84
|
+
|
|
85
|
+
## The slash command (commands/<svc>-agent.md)
|
|
86
|
+
|
|
87
|
+
A 7-line markdown file with frontmatter + a one-line body:
|
|
88
|
+
|
|
89
|
+
```markdown
|
|
90
|
+
---
|
|
91
|
+
description: Run a <svc> action via the <svc>-agent connector
|
|
92
|
+
argument-hint: "<action> <params-json>"
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
Invoke the `<svc>-agent` skill with the user's $ARGUMENTS as the action name and params JSON. Return the connector's JSON envelope verbatim.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This adds `/<svc>-agent <action> <params-json>` as a typed shortcut for the skill.
|
|
99
|
+
|
|
100
|
+
## The .npmignore at repo root
|
|
101
|
+
|
|
102
|
+
Excludes everything that's not the published library:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
src/
|
|
106
|
+
tests/
|
|
107
|
+
plugin/
|
|
108
|
+
evals/
|
|
109
|
+
vitest.config.ts
|
|
110
|
+
tsconfig.json
|
|
111
|
+
.gitignore
|
|
112
|
+
.git/
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Result: the published npm tarball contains only `dist/`, `README.md`, and `LICENSE`. The plugin lives in the source repo.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Template sync strategy
|
|
2
|
+
|
|
3
|
+
Templates in `assets/templates/connector/` are snapshots of `notion-agent-connector` files (the canonical reference) at a specific point in time. They will drift as the canonical pattern evolves — toolkit upgrades, new hook types, version pin bumps, etc. This doc tells you how to detect and apply that drift.
|
|
4
|
+
|
|
5
|
+
## Last synced
|
|
6
|
+
|
|
7
|
+
- **Date**: 2026-04-26
|
|
8
|
+
- **Source connector**: `/Users/narayan/src/connectors/notion-agent-connector/`
|
|
9
|
+
- **Toolkit version pinned**: `@narai/connector-toolkit ^3.1.0` (workspace published `3.4.0`)
|
|
10
|
+
- **Sibling pins**: `@narai/credential-providers ^0.2.1`, `@narai/connector-config ^1.1.0`, `zod ^3.23.0`
|
|
11
|
+
|
|
12
|
+
## Source-of-truth file mapping
|
|
13
|
+
|
|
14
|
+
| Template | Canonical source | Substitutions applied |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `package.json.tmpl` | `notion-agent-connector/package.json` | `notion` → `{{SERVICE_SLUG}}`, version reset to `0.1.0` |
|
|
17
|
+
| `tsconfig.json` | `notion-agent-connector/tsconfig.json` | none (literal copy) |
|
|
18
|
+
| `vitest.config.ts.tmpl` | `notion-agent-connector/vitest.config.ts` | `TEST_LIVE_NOTION` → `TEST_LIVE_{{SERVICE_SLUG_UPPER}}` |
|
|
19
|
+
| `LICENSE` | `notion-agent-connector/LICENSE` | none |
|
|
20
|
+
| `.npmignore` | `notion-agent-connector/.npmignore` | comment line slug |
|
|
21
|
+
| `README.md.tmpl` | `notion-agent-connector/README.md` | name, action table, env var name |
|
|
22
|
+
| `src/cli.ts.tmpl` | `notion-agent-connector/src/cli.ts` | `NOTION` → slug, env mapping body |
|
|
23
|
+
| `src/lib/error.ts.tmpl` | `notion-agent-connector/src/lib/notion_error.ts` | `Notion` → `{{ServicePascal}}` |
|
|
24
|
+
| `src/lib/client.ts.tmpl` | `notion-agent-connector/src/lib/notion_client.ts` (frame only) | full reshape (auth header, methods, types removed) |
|
|
25
|
+
| `src/index.ts.tmpl` | `notion-agent-connector/src/index.ts` (skeleton only) | full reshape (param schemas, actions removed) |
|
|
26
|
+
| `tests/unit/cli.test.ts.tmpl` | `notion-agent-connector/tests/unit/cli.test.ts` (helpers only) | service-name swap |
|
|
27
|
+
| `tests/unit/client_extras.test.ts.tmpl` | per-method test pattern from notion's tests | starter slot |
|
|
28
|
+
| `tests/integration/framework.test.ts.tmpl` | `notion-agent-connector/tests/integration/framework.test.ts` (one happy-path) | service-name swap |
|
|
29
|
+
| `plugin/.claude-plugin/plugin.json.tmpl` | `notion-agent-connector/plugin/.claude-plugin/plugin.json` | name + description |
|
|
30
|
+
| `plugin/hooks/hooks.json.tmpl` | `notion-agent-connector/plugin/hooks/hooks.json` | `notion` → slug |
|
|
31
|
+
| `plugin/hooks/reminder.mjs.tmpl` | `notion-agent-connector/plugin/hooks/reminder.mjs` | `notion` → slug |
|
|
32
|
+
| `plugin/bin/service-agent.tmpl` | `notion-agent-connector/plugin/bin/notion-agent` | `notion` → slug, package name |
|
|
33
|
+
| `plugin/skills/service-agent/SKILL.md.tmpl` | `notion-agent-connector/plugin/skills/notion-agent/SKILL.md` | full reshape |
|
|
34
|
+
| `plugin/commands/service-agent.md.tmpl` | `notion-agent-connector/plugin/commands/notion-agent.md` | `notion` → slug |
|
|
35
|
+
|
|
36
|
+
## Re-sync procedure
|
|
37
|
+
|
|
38
|
+
When notion-agent-connector changes in a way that should propagate to new scaffolds:
|
|
39
|
+
|
|
40
|
+
1. **Identify the changed file(s)** in notion-agent. If it's a content change to a templated file, the template needs updating. If it's notion-specific behavior (e.g., a new Notion API endpoint), don't propagate.
|
|
41
|
+
|
|
42
|
+
2. **Diff the canonical against the template** (treating placeholders as free variables):
|
|
43
|
+
```bash
|
|
44
|
+
diff -u \
|
|
45
|
+
<(sed 's/notion/{{SERVICE_SLUG}}/g; s/Notion/{{ServicePascal}}/g; s/NOTION/{{SERVICE_SLUG_UPPER}}/g' /Users/narayan/src/connectors/notion-agent-connector/<file>) \
|
|
46
|
+
/Users/narayan/.claude/skills/create-connector/assets/templates/connector/<file>.tmpl
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. **Apply non-substitution diffs** to the template. Test by scaffolding a fresh connector and running `npm install && npm run build && npm run typecheck && npm test`.
|
|
50
|
+
|
|
51
|
+
4. **Update this doc**: bump the "Last synced" date and any version pins that changed.
|
|
52
|
+
|
|
53
|
+
## Major bump checklist
|
|
54
|
+
|
|
55
|
+
When `@narai/connector-toolkit` ships a major version (e.g., `^3.x` → `^4.0.0`):
|
|
56
|
+
|
|
57
|
+
1. Read the toolkit's CHANGELOG for breaking changes.
|
|
58
|
+
2. Update the version pin in `package.json.tmpl`.
|
|
59
|
+
3. Re-run all eval test cases in `evals/evals.json` against the new toolkit. Confirm scaffolds still build clean.
|
|
60
|
+
4. Update template internals if APIs shifted (e.g., `createConnector` signature, error code taxonomy, `mapError` semantics).
|
|
61
|
+
5. Update `references/connector-anatomy.md` with any contract changes.
|
|
62
|
+
|
|
63
|
+
## Detecting drift in the wild
|
|
64
|
+
|
|
65
|
+
The skill's verification step (`npm install && npm run build && npm run typecheck && npm test` on a fresh scaffold) catches drift the moment a new connector fails to build. If a user reports "scaffold doesn't typecheck against latest toolkit", that's the trigger to re-sync.
|
|
66
|
+
|
|
67
|
+
Soft signals to watch for:
|
|
68
|
+
|
|
69
|
+
- A new connector PR in the workspace adds a file the templates don't have.
|
|
70
|
+
- The toolkit publishes a new helper that materially simplifies client code.
|
|
71
|
+
- A new plugin hook type appears in another connector's `hooks.json`.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
Step-by-step smoke test for a freshly scaffolded connector. Run this before declaring the scaffold "done".
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- The new connector lives at `<workspace>/<svc>-agent-connector/` (default: `/Users/narayan/src/connectors/<svc>-agent-connector/`).
|
|
8
|
+
- Node 20+ is available (`node --version`).
|
|
9
|
+
- The npm scope `@narai` is reachable (the connector depends on `@narai/connector-toolkit`, `@narai/credential-providers`, `@narai/connector-config`).
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
### 1. Install dependencies
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd <new-connector-dir>
|
|
17
|
+
npm install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Expect: clean install, no warnings about missing peers (carets resolve).
|
|
21
|
+
|
|
22
|
+
If `npm install` fails on `@narai/*` packages, the user may not have access to the private npm scope. The skill should flag this.
|
|
23
|
+
|
|
24
|
+
### 2. Build
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Expect: `dist/` directory populated with compiled JS + `.d.ts`. No TS errors.
|
|
31
|
+
|
|
32
|
+
If errors mention "Cannot find module '@narai/connector-toolkit'", install failed silently — re-run step 1 with `--verbose`.
|
|
33
|
+
|
|
34
|
+
If errors mention strict TS settings (`exactOptionalPropertyTypes`, `noUnusedParameters`), the generated code has issues. Read the error, fix, re-run.
|
|
35
|
+
|
|
36
|
+
### 3. Typecheck
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run typecheck
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Expect: silent success.
|
|
43
|
+
|
|
44
|
+
This is mostly redundant with `build`, but it's faster (no emit) and catches anything `tsc` would warn about. Useful in CI.
|
|
45
|
+
|
|
46
|
+
### 4. Test
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm test
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Expect: vitest reports all tests passing.
|
|
53
|
+
|
|
54
|
+
Common failures:
|
|
55
|
+
- **Mock fetch returns wrong shape**: the test's `jsonResponse({...})` body doesn't match what the client method expects. Check the action's response shape contract.
|
|
56
|
+
- **Rate limiter sleeps in test**: `sleepImpl` should be `async () => {}` (no-op) in tests.
|
|
57
|
+
- **Hardship logger writes to real `~/.claude/`**: integration tests should set `process.env.HOME` to a tmpdir; check `beforeEach`.
|
|
58
|
+
|
|
59
|
+
### 5. Smoke envelope
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node dist/cli.js --action <first-action> --params '{}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Expect: a JSON object on stdout. Without credentials configured, you'll see something like:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"status": "error",
|
|
70
|
+
"action": "<first-action>",
|
|
71
|
+
"error_code": "CONFIG_ERROR",
|
|
72
|
+
"message": "<service> credentials not configured. Set <SVC>_TOKEN or register a credential provider via @narai/credential-providers.",
|
|
73
|
+
"retriable": false
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's the **right** shape — the CLI plumbing works end-to-end. We're testing the envelope structure, not connectivity.
|
|
78
|
+
|
|
79
|
+
If the output is **not** valid JSON (e.g., a stack trace, "command not found", or shell noise), something is broken upstream. Common causes:
|
|
80
|
+
|
|
81
|
+
- `dist/cli.js` doesn't exist → step 2 (build) failed.
|
|
82
|
+
- The shebang `#!/usr/bin/env node` is missing or the file isn't executable → re-check the cli.ts template.
|
|
83
|
+
- An import error → check `package.json` "type": "module" and `tsconfig.json` "module": "NodeNext".
|
|
84
|
+
|
|
85
|
+
### 6. (Optional) Live connectivity test
|
|
86
|
+
|
|
87
|
+
Only if the user has real credentials and asks for it:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export <SVC>_TOKEN="..."
|
|
91
|
+
node dist/cli.js --action <first-action> --params '<real-params>'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Expect: `status: "success"` with shaped data. If you get `AUTH_ERROR`, the token is wrong; if `NOT_FOUND`, the resource id doesn't exist; if `CONNECTION_ERROR`, the API is unreachable.
|
|
95
|
+
|
|
96
|
+
## Verification of the skill itself (not a scaffold)
|
|
97
|
+
|
|
98
|
+
Separate from per-scaffold verification, the skill itself should be sanity-checked periodically:
|
|
99
|
+
|
|
100
|
+
1. Open `SKILL.md`, confirm < 500 lines and frontmatter is well-formed.
|
|
101
|
+
2. Spot-check that all referenced files in `references/*.md` exist.
|
|
102
|
+
3. Run the eval set in `evals/evals.json` through skill-creator's eval loop (see `~/.claude/plugins/cache/claude-plugins-official/skill-creator/`).
|
|
103
|
+
4. After a major scaffold change, manually run test case 1 (Stripe) and `diff` the resulting tree against `notion-agent-connector/` (ignoring service-name substitutions).
|