twelveai 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,6 +91,43 @@ if (res.pendingConfirmation) {
91
91
  }
92
92
  ```
93
93
 
94
+ ## Manage your workspace from code or the terminal
95
+
96
+ Everything the console does is also available programmatically (same API key):
97
+
98
+ ```ts
99
+ await twelve.manage.plugins.install('bills')
100
+ await twelve.manage.agents.create({
101
+ intent: 'loan_request',
102
+ keywords: ['loan', 'borrow'],
103
+ systemPrompt: 'You help customers with loan questions. Call list_loans and answer only from what it returns.',
104
+ tools: ['list_loans'],
105
+ })
106
+ await twelve.manage.tools.create({
107
+ name: 'list_loans',
108
+ description: "List this customer's active loans.",
109
+ sideEffect: 'read',
110
+ method: 'GET',
111
+ url: 'https://api.yourbank.com/v1/customers/{customer_id}/loans',
112
+ sampleResponse: { status: 'success', data: { loans: [] } },
113
+ })
114
+ await twelve.manage.settings.update({ webhookUrl: 'https://api.yourapp.com/hooks' })
115
+ ```
116
+
117
+ Namespaces: `agents`, `tools`, `plugins`, `customers`, `settings`, `billing`, `usage`, plus `twelve.api(method, path, body?)` for anything else.
118
+
119
+ Or use the CLI (ships with this package):
120
+
121
+ ```bash
122
+ export TWELVE_API_KEY=sk-...
123
+ npx -p twelveai twelveai plugins install bills
124
+ npx -p twelveai twelveai agents list
125
+ npx -p twelveai twelveai chat "what's my balance?" --customer cus_123 --sandbox
126
+ npx -p twelveai twelveai usage
127
+ ```
128
+
129
+ Run `twelveai help` for the full command list.
130
+
94
131
  ## Sandbox
95
132
 
96
133
  Add `sandbox: true` and every tool returns realistic sample data - nothing real is called or moved. Perfect for CI and demos.
@@ -188,7 +225,7 @@ and test sandbox chats while it codes:
188
225
  "mcpServers": {
189
226
  "twelveai": {
190
227
  "command": "npx",
191
- "args": ["-y", "twelveai-mcp"],
228
+ "args": ["-y", "-p", "twelveai", "twelveai-mcp"],
192
229
  "env": { "TWELVE_API_KEY": "sk_live_..." }
193
230
  }
194
231
  }
@@ -0,0 +1,248 @@
1
+ // src/manage.ts
2
+ function buildManage(request) {
3
+ return {
4
+ agents: {
5
+ list: () => request("GET", "/v1/agents"),
6
+ create: (input) => request("POST", "/v1/agents", { ...input }),
7
+ update: (intent, patch) => request("PUT", "/v1/agents", { intent, ...patch }),
8
+ enable: (intent) => request("PUT", "/v1/agents", { intent, enabled: true }),
9
+ disable: (intent) => request("PUT", "/v1/agents", { intent, enabled: false }),
10
+ delete: (intent) => request("DELETE", `/v1/agents/${encodeURIComponent(intent)}`)
11
+ },
12
+ tools: {
13
+ list: () => request("GET", "/v1/tools"),
14
+ create: (input) => request("POST", "/v1/tools", { ...input }),
15
+ bind: (name, binding) => request("PUT", `/v1/tools/${encodeURIComponent(name)}`, { ...binding }),
16
+ test: (name) => request("POST", `/v1/tools/${encodeURIComponent(name)}/test`),
17
+ importOpenApi: (input) => request("POST", "/v1/tools/import", { ...input })
18
+ },
19
+ plugins: {
20
+ list: () => request("GET", "/v1/plugins"),
21
+ install: (id) => request("POST", `/v1/plugins/${encodeURIComponent(id)}/install`),
22
+ uninstall: (id) => request("POST", `/v1/plugins/${encodeURIComponent(id)}/uninstall`)
23
+ },
24
+ customers: {
25
+ list: (opts = {}) => request("GET", `/v1/customers?limit=${opts.limit ?? 100}${opts.range ? `&range=${opts.range}` : ""}`),
26
+ upsert: (input) => request("POST", "/v1/customers", { ...input }),
27
+ get: (externalId) => request("GET", `/v1/customers/${encodeURIComponent(externalId)}`)
28
+ },
29
+ settings: {
30
+ get: () => request("GET", "/v1/settings"),
31
+ update: (patch) => request("PUT", "/v1/settings", patch)
32
+ },
33
+ billing: {
34
+ get: () => request("GET", "/v1/billing"),
35
+ topup: (amountNgn, email) => request("POST", "/v1/billing/topup", { amount: amountNgn, ...email ? { email } : {} })
36
+ },
37
+ usage: {
38
+ summary: () => request("GET", "/v1/usage"),
39
+ events: (limit = 20) => request("GET", `/v1/usage/events?limit=${limit}`),
40
+ event: (id) => request("GET", `/v1/usage/events/${id}`),
41
+ analytics: (days = 30) => request("GET", `/v1/usage/analytics?days=${days}`)
42
+ },
43
+ manifest: () => request("GET", "/v1/manifest")
44
+ };
45
+ }
46
+
47
+ // src/client.ts
48
+ var DEFAULT_BASE_URL = "https://ai.twelveai.app";
49
+ var TwelveAI = class {
50
+ apiKey;
51
+ baseUrl;
52
+ fetchImpl;
53
+ clientAuth;
54
+ maxHandoffRounds;
55
+ /**
56
+ * Everything the console can do, from code: agents, tools, plugins,
57
+ * customers, settings, billing, usage. `twelve.manage.plugins.install('bills')`.
58
+ */
59
+ manage;
60
+ constructor(options) {
61
+ if (!options?.apiKey) throw new Error("TwelveAI: apiKey is required.");
62
+ this.apiKey = options.apiKey;
63
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
64
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
65
+ this.clientAuth = options.clientAuth;
66
+ this.maxHandoffRounds = options.maxHandoffRounds ?? 3;
67
+ if (typeof this.fetchImpl !== "function") {
68
+ throw new Error("TwelveAI: global fetch is unavailable - pass options.fetch (Node 18+ has fetch built in).");
69
+ }
70
+ this.manage = buildManage((method, path, body) => this.api(method, path, body));
71
+ }
72
+ /**
73
+ * Low-level authenticated call to any engine endpoint. Returns
74
+ * `{ ok, status, ...payload }`; never throws on HTTP errors.
75
+ */
76
+ async api(method, path, body) {
77
+ try {
78
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
79
+ method,
80
+ headers: { "content-type": "application/json", "x-api-key": this.apiKey },
81
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
82
+ });
83
+ const data = await res.json().catch(() => ({}));
84
+ return {
85
+ ...data,
86
+ ok: res.ok && data.ok !== false,
87
+ status: res.status,
88
+ ...res.ok ? {} : { error: data.error ?? `Engine returned ${res.status}` }
89
+ };
90
+ } catch (error) {
91
+ return { ok: false, status: 0, error: error?.message || "Could not reach the engine." };
92
+ }
93
+ }
94
+ /**
95
+ * Send one chat turn and return the completed result. Read hand-offs are
96
+ * auto-executed (see class docs); pass `autoExecute: false` to get the raw
97
+ * paused response instead, or `onHandoff` to execute them yourself.
98
+ */
99
+ async chat(input, opts = {}) {
100
+ let res = await this.post("/v1/chat", this.chatBody(input));
101
+ if (opts.autoExecute === false) return res;
102
+ return this.completeHandoffs(res, input, opts.onHandoff);
103
+ }
104
+ /** Resume a paused turn with tool results your system produced. */
105
+ async resume(continuation, toolResults, input = {}) {
106
+ return this.post("/v1/chat", { ...this.chatBody(input), continuation, toolResults });
107
+ }
108
+ /**
109
+ * Approve a pending action (after your PIN/OTP step) - resends the turn with
110
+ * `confirmed: true` so the engine proceeds.
111
+ */
112
+ async confirm(continuation, input = {}) {
113
+ return this.chat({ message: "yes", ...input, continuation, confirmed: true });
114
+ }
115
+ /**
116
+ * Level-0 integration: classify a message WITHOUT running the conversation.
117
+ * Returns the intent, a deterministic confidence score, and cheap extracted
118
+ * entities (amount / account number / phone) - you keep your existing flows
119
+ * and make the call yourself. Free: no tools run, nothing is stored.
120
+ */
121
+ async classify(input) {
122
+ try {
123
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/classify`, {
124
+ method: "POST",
125
+ headers: { "content-type": "application/json", "x-api-key": this.apiKey },
126
+ body: JSON.stringify({ message: input.message, ...input.reasoning ? { reasoning: input.reasoning } : {} })
127
+ });
128
+ const data = await res.json().catch(() => ({}));
129
+ return {
130
+ intent: null,
131
+ label: null,
132
+ confidence: 0,
133
+ alternatives: [],
134
+ entities: {},
135
+ ...data,
136
+ // pass every field through (matchedKeywords, score, margin, classifyOnly, ...)
137
+ ok: res.ok && data.ok !== false,
138
+ status: res.status,
139
+ ...res.ok ? {} : { error: data.error ?? `Engine returned ${res.status}` }
140
+ };
141
+ } catch (error) {
142
+ return { ok: false, intent: null, label: null, confidence: 0, alternatives: [], entities: {}, status: 0, error: error?.message || "Could not reach the engine." };
143
+ }
144
+ }
145
+ /* ------------------------------ internals ------------------------------ */
146
+ chatBody(input) {
147
+ const body = {};
148
+ if (input.message !== void 0) body.message = input.message;
149
+ if (input.customerId !== void 0) body.customerId = input.customerId;
150
+ if (input.continuation) body.continuation = input.continuation;
151
+ if (input.confirmed !== void 0) body.confirmed = input.confirmed;
152
+ if (input.sandbox !== void 0) body.sandbox = input.sandbox;
153
+ if (input.channel) body.channel = input.channel;
154
+ if (input.intent) body.intent = input.intent;
155
+ if (input.tier) body.tier = input.tier;
156
+ if (input.reasoning) body.reasoning = input.reasoning;
157
+ if (input.customerToken) body.customerToken = input.customerToken;
158
+ if (input.attachments?.length) body.attachments = input.attachments;
159
+ if (input.metadata) body.metadata = input.metadata;
160
+ return body;
161
+ }
162
+ async post(path, body) {
163
+ try {
164
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
165
+ method: "POST",
166
+ headers: { "content-type": "application/json", "x-api-key": this.apiKey },
167
+ body: JSON.stringify(body)
168
+ });
169
+ const data = await res.json().catch(() => ({}));
170
+ return {
171
+ ok: res.ok && data.ok !== false,
172
+ message: data.message ?? null,
173
+ intent: data.intent ?? null,
174
+ toolCalls: data.toolCalls ?? [],
175
+ pendingConfirmation: data.pendingConfirmation ?? null,
176
+ pendingToolCalls: data.pendingToolCalls ?? null,
177
+ continuation: data.continuation ?? null,
178
+ ...data,
179
+ status: res.status,
180
+ ...res.ok ? {} : { error: data.error ?? `Engine returned ${res.status}` }
181
+ };
182
+ } catch (error) {
183
+ return {
184
+ ok: false,
185
+ status: 0,
186
+ message: null,
187
+ intent: null,
188
+ toolCalls: [],
189
+ pendingConfirmation: null,
190
+ pendingToolCalls: null,
191
+ continuation: null,
192
+ error: error?.message || "Could not reach the engine."
193
+ };
194
+ }
195
+ }
196
+ /**
197
+ * Execute client-fetch hand-offs and resume until the turn completes. Only
198
+ * calls that carry a resolved `request` are auto-executed - a hand-off
199
+ * without one (e.g. a money move awaiting your PIN flow) stops the loop and
200
+ * is returned to you untouched.
201
+ */
202
+ async completeHandoffs(res, input, onHandoff) {
203
+ const executed = [];
204
+ for (let round = 0; round < this.maxHandoffRounds; round++) {
205
+ const calls = res.pendingToolCalls ?? [];
206
+ if (!calls.length || !res.continuation) break;
207
+ if (!calls.every((c) => c.request?.url)) break;
208
+ const results = [];
209
+ for (const call of calls) {
210
+ const result = onHandoff ? await onHandoff(call) : await this.performHandoff(call);
211
+ executed.push({
212
+ name: call.name,
213
+ method: call.request.method,
214
+ url: call.request.url,
215
+ ok: result?.ok !== false
216
+ });
217
+ results.push({ id: call.id, result });
218
+ }
219
+ const next = await this.resume(res.continuation, results, input);
220
+ if (next.error) break;
221
+ res = next;
222
+ }
223
+ if (executed.length) res.executedHandoffs = executed;
224
+ return res;
225
+ }
226
+ /** Default hand-off executor: perform the request with your clientAuth headers. */
227
+ async performHandoff(call) {
228
+ const req = call.request;
229
+ try {
230
+ const extra = typeof this.clientAuth === "function" ? await this.clientAuth() : this.clientAuth ?? {};
231
+ const res = await this.fetchImpl(req.url, {
232
+ method: req.method,
233
+ headers: { "content-type": "application/json", ...req.headers ?? {}, ...extra },
234
+ ...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
235
+ });
236
+ const body = await res.json().catch(() => ({}));
237
+ if (!res.ok) return { ok: false, error: `Endpoint returned ${res.status}` };
238
+ return { ok: true, data: body.data ?? body };
239
+ } catch (error) {
240
+ return { ok: false, error: error?.message || "Request failed." };
241
+ }
242
+ }
243
+ };
244
+
245
+ export {
246
+ buildManage,
247
+ TwelveAI
248
+ };