@dingdawg/sdk 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/dist/client.d.ts +126 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +302 -0
- package/dist/client.js.map +1 -0
- package/dist/durable.d.ts +162 -0
- package/dist/durable.d.ts.map +1 -0
- package/dist/durable.js +223 -0
- package/dist/durable.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +69 -0
- package/src/__tests__/client.test.ts +576 -0
- package/src/__tests__/durable.test.ts +212 -0
- package/src/client.ts +461 -0
- package/src/durable.ts +347 -0
- package/src/index.ts +43 -0
- package/src/types.ts +178 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DurableDingDawgClient — unit tests for DDAG v1 TypeScript SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
|
6
|
+
import { DurableDingDawgClient, AgentFSMState } from "../durable.js";
|
|
7
|
+
import { DingDawgApiError } from "../client.js";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Mock helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function makeFetch(body: unknown, ok = true, status = 200) {
|
|
14
|
+
return jest.fn(async () => ({
|
|
15
|
+
ok,
|
|
16
|
+
status,
|
|
17
|
+
headers: { get: () => "application/json" },
|
|
18
|
+
json: async () => body,
|
|
19
|
+
})) as unknown as typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Fixtures
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const DURABLE_RESPONSE = {
|
|
27
|
+
reply: "Compliance report generated.",
|
|
28
|
+
session_id: "sess-ddag-001",
|
|
29
|
+
timestamp: "2026-04-05T12:00:00Z",
|
|
30
|
+
checkpoint_cid: "ipfs:QmTestCID123",
|
|
31
|
+
step_index: 3,
|
|
32
|
+
verified: true,
|
|
33
|
+
fsm_state: "done",
|
|
34
|
+
proof_cid: "ipfs:QmProof456",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SOUL_RESPONSE = {
|
|
38
|
+
soul_id: "soul-001",
|
|
39
|
+
agent_id: "acme-support",
|
|
40
|
+
soul_cid: "ipfs:QmSoul789",
|
|
41
|
+
mission: "Govern AI reliably.",
|
|
42
|
+
learned_prefs: { preferred_model: "gpt-5.4" },
|
|
43
|
+
created_at: "2026-04-01T00:00:00Z",
|
|
44
|
+
updated_at: "2026-04-05T12:00:00Z",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CHECKPOINT_RESPONSE = {
|
|
48
|
+
session_id: "sess-ddag-001",
|
|
49
|
+
step_index: 3,
|
|
50
|
+
state_cid: "ipfs:QmCheckpoint321",
|
|
51
|
+
fsm_state: "checkpointed",
|
|
52
|
+
created_at: "2026-04-05T12:00:00Z",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Tests
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
describe("DurableDingDawgClient — construction", () => {
|
|
60
|
+
it("extends DingDawgClient — all namespaces present", () => {
|
|
61
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test_key" });
|
|
62
|
+
expect(typeof client.agent.sendMessage).toBe("function");
|
|
63
|
+
expect(typeof client.billing.currentMonth).toBe("function");
|
|
64
|
+
expect(typeof client.invokeWithCheckpoint).toBe("function");
|
|
65
|
+
expect(typeof client.resume).toBe("function");
|
|
66
|
+
expect(typeof client.getSoul).toBe("function");
|
|
67
|
+
expect(typeof client.getCheckpoint).toBe("function");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("throws TypeError on empty apiKey", () => {
|
|
71
|
+
expect(() => new DurableDingDawgClient({ apiKey: "" })).toThrow(TypeError);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("DurableDingDawgClient — invokeWithCheckpoint", () => {
|
|
76
|
+
it("returns DurableResponse with checkpoint_cid and fsm_state", async () => {
|
|
77
|
+
global.fetch = makeFetch(DURABLE_RESPONSE);
|
|
78
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
79
|
+
const result = await client.invokeWithCheckpoint("acme-support", {
|
|
80
|
+
message: "Run compliance report",
|
|
81
|
+
userId: "user_123",
|
|
82
|
+
});
|
|
83
|
+
expect(result.reply).toBe("Compliance report generated.");
|
|
84
|
+
expect(result.checkpoint_cid).toBe("ipfs:QmTestCID123");
|
|
85
|
+
expect(result.step_index).toBe(3);
|
|
86
|
+
expect(result.verified).toBe(true);
|
|
87
|
+
expect(result.fsm_state).toBe(AgentFSMState.Done);
|
|
88
|
+
expect(result.proof_cid).toBe("ipfs:QmProof456");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("sends to correct durable invoke endpoint", async () => {
|
|
92
|
+
const mock = makeFetch(DURABLE_RESPONSE);
|
|
93
|
+
global.fetch = mock;
|
|
94
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
95
|
+
await client.invokeWithCheckpoint("acme-support", { message: "Hello" });
|
|
96
|
+
const [url] = (mock as jest.Mock).mock.calls[0] as [string, RequestInit];
|
|
97
|
+
expect(url).toContain("/api/v2/agents/acme-support/durable/invoke");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("sends Idempotency-Key header when idempotency_key is set", async () => {
|
|
101
|
+
const mock = makeFetch(DURABLE_RESPONSE);
|
|
102
|
+
global.fetch = mock;
|
|
103
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
104
|
+
await client.invokeWithCheckpoint("acme-support", {
|
|
105
|
+
message: "Hello",
|
|
106
|
+
idempotency_key: "my-idem-key-001",
|
|
107
|
+
});
|
|
108
|
+
const [, init] = (mock as jest.Mock).mock.calls[0] as [string, RequestInit];
|
|
109
|
+
const headers = init.headers as Record<string, string>;
|
|
110
|
+
expect(headers["Idempotency-Key"]).toBe("my-idem-key-001");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("includes resume_cid in payload when provided", async () => {
|
|
114
|
+
const mock = makeFetch(DURABLE_RESPONSE);
|
|
115
|
+
global.fetch = mock;
|
|
116
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
117
|
+
await client.invokeWithCheckpoint("acme-support", {
|
|
118
|
+
message: "Continue task",
|
|
119
|
+
resume_cid: "ipfs:QmPriorCID",
|
|
120
|
+
});
|
|
121
|
+
const [, init] = (mock as jest.Mock).mock.calls[0] as [string, RequestInit];
|
|
122
|
+
const body = JSON.parse(init.body as string);
|
|
123
|
+
expect(body.resume_cid).toBe("ipfs:QmPriorCID");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("throws DingDawgApiError on 500 response", async () => {
|
|
127
|
+
global.fetch = makeFetch({ detail: "server error" }, false, 500);
|
|
128
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
129
|
+
await expect(
|
|
130
|
+
client.invokeWithCheckpoint("acme-support", { message: "fail" })
|
|
131
|
+
).rejects.toBeInstanceOf(DingDawgApiError);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("DurableDingDawgClient — resume", () => {
|
|
136
|
+
it("calls resume endpoint with checkpoint_cid in body", async () => {
|
|
137
|
+
const mock = makeFetch({ ...DURABLE_RESPONSE, fsm_state: "running" });
|
|
138
|
+
global.fetch = mock;
|
|
139
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
140
|
+
const result = await client.resume("acme-support", "ipfs:QmPriorCID");
|
|
141
|
+
const [url, init] = (mock as jest.Mock).mock.calls[0] as [string, RequestInit];
|
|
142
|
+
expect(url).toContain("/api/v2/agents/acme-support/durable/resume");
|
|
143
|
+
const body = JSON.parse(init.body as string);
|
|
144
|
+
expect(body.checkpoint_cid).toBe("ipfs:QmPriorCID");
|
|
145
|
+
expect(result.fsm_state).toBe(AgentFSMState.Running);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("DurableDingDawgClient — getSoul", () => {
|
|
150
|
+
it("returns AgentSoul with all fields", async () => {
|
|
151
|
+
global.fetch = makeFetch(SOUL_RESPONSE);
|
|
152
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
153
|
+
const soul = await client.getSoul("acme-support");
|
|
154
|
+
expect(soul.soul_id).toBe("soul-001");
|
|
155
|
+
expect(soul.soul_cid).toBe("ipfs:QmSoul789");
|
|
156
|
+
expect(soul.mission).toBe("Govern AI reliably.");
|
|
157
|
+
expect(soul.learned_prefs["preferred_model"]).toBe("gpt-5.4");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("calls correct soul endpoint", async () => {
|
|
161
|
+
const mock = makeFetch(SOUL_RESPONSE);
|
|
162
|
+
global.fetch = mock;
|
|
163
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
164
|
+
await client.getSoul("acme-support");
|
|
165
|
+
const [url] = (mock as jest.Mock).mock.calls[0] as [string, RequestInit];
|
|
166
|
+
expect(url).toContain("/api/v2/agents/acme-support/soul");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("DurableDingDawgClient — getCheckpoint", () => {
|
|
171
|
+
it("returns CheckpointState when found", async () => {
|
|
172
|
+
global.fetch = makeFetch(CHECKPOINT_RESPONSE);
|
|
173
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
174
|
+
const ckpt = await client.getCheckpoint("acme-support", "sess-ddag-001");
|
|
175
|
+
expect(ckpt).not.toBeNull();
|
|
176
|
+
expect(ckpt!.state_cid).toBe("ipfs:QmCheckpoint321");
|
|
177
|
+
expect(ckpt!.fsm_state).toBe(AgentFSMState.Checkpointed);
|
|
178
|
+
expect(ckpt!.step_index).toBe(3);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("returns null on 404", async () => {
|
|
182
|
+
global.fetch = makeFetch({ detail: "Not found" }, false, 404);
|
|
183
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
184
|
+
const ckpt = await client.getCheckpoint("acme-support", "unknown-session");
|
|
185
|
+
expect(ckpt).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("rethrows non-404 errors", async () => {
|
|
189
|
+
global.fetch = makeFetch({ detail: "server error" }, false, 500);
|
|
190
|
+
const client = new DurableDingDawgClient({ apiKey: "dd_test" });
|
|
191
|
+
await expect(
|
|
192
|
+
client.getCheckpoint("acme-support", "sess-001")
|
|
193
|
+
).rejects.toBeInstanceOf(DingDawgApiError);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("AgentFSMState enum", () => {
|
|
198
|
+
it("has all 10 states", () => {
|
|
199
|
+
const states = Object.values(AgentFSMState);
|
|
200
|
+
expect(states).toContain("idle");
|
|
201
|
+
expect(states).toContain("running");
|
|
202
|
+
expect(states).toContain("tool_pending");
|
|
203
|
+
expect(states).toContain("verifying");
|
|
204
|
+
expect(states).toContain("committing");
|
|
205
|
+
expect(states).toContain("remediating");
|
|
206
|
+
expect(states).toContain("checkpointed");
|
|
207
|
+
expect(states).toContain("resuming");
|
|
208
|
+
expect(states).toContain("done");
|
|
209
|
+
expect(states).toContain("failed");
|
|
210
|
+
expect(states).toHaveLength(10);
|
|
211
|
+
});
|
|
212
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dingdawg/sdk — DingDawgClient
|
|
3
|
+
*
|
|
4
|
+
* The primary entry point for B2B2C partners embedding DingDawg agents.
|
|
5
|
+
*
|
|
6
|
+
* Design goals:
|
|
7
|
+
* - Zero runtime dependencies (Node 18+ built-in fetch only)
|
|
8
|
+
* - Full TypeScript types on all inputs and outputs
|
|
9
|
+
* - Typed error class so callers can catch + inspect failures
|
|
10
|
+
* - Grouped API namespaces: client.agent.* and client.billing.*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
AgentRecord,
|
|
15
|
+
BillingSummary,
|
|
16
|
+
CreateAgentOptions,
|
|
17
|
+
DingDawgClientOptions,
|
|
18
|
+
MonthlyBillingSummary,
|
|
19
|
+
PaginatedList,
|
|
20
|
+
SendMessageOptions,
|
|
21
|
+
TriggerResponse,
|
|
22
|
+
ApiErrorBody,
|
|
23
|
+
DingDawgApiErrorDetails,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Default configuration
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const DEFAULT_BASE_URL = "https://api.dingdawg.com";
|
|
31
|
+
|
|
32
|
+
const SDK_USER_AGENT = "@dingdawg/sdk/0.1.0";
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Typed error class
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Thrown by all DingDawgClient methods when the API returns a non-2xx status.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* try {
|
|
44
|
+
* await client.agent.get("non-existent-id");
|
|
45
|
+
* } catch (err) {
|
|
46
|
+
* if (err instanceof DingDawgApiError) {
|
|
47
|
+
* console.error(err.status, err.body?.detail);
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class DingDawgApiError extends Error {
|
|
53
|
+
/** HTTP status code. */
|
|
54
|
+
readonly status: number;
|
|
55
|
+
/** Parsed error body from the API, or null if the body was not JSON. */
|
|
56
|
+
readonly body: ApiErrorBody | null;
|
|
57
|
+
|
|
58
|
+
constructor(details: DingDawgApiErrorDetails, message?: string) {
|
|
59
|
+
const derived =
|
|
60
|
+
message ??
|
|
61
|
+
details.body?.detail ??
|
|
62
|
+
details.body?.message ??
|
|
63
|
+
`DingDawg API error: HTTP ${details.status}`;
|
|
64
|
+
super(String(derived));
|
|
65
|
+
this.name = "DingDawgApiError";
|
|
66
|
+
this.status = details.status;
|
|
67
|
+
this.body = details.body;
|
|
68
|
+
// Maintains proper prototype chain in transpiled environments
|
|
69
|
+
Object.setPrototypeOf(this, DingDawgApiError.prototype);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Internal HTTP helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse a Response, throwing DingDawgApiError on non-2xx status.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
async function _parseResponse<T>(resp: Response): Promise<T> {
|
|
83
|
+
let body: unknown = null;
|
|
84
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
85
|
+
|
|
86
|
+
if (contentType.includes("application/json")) {
|
|
87
|
+
try {
|
|
88
|
+
body = await resp.json();
|
|
89
|
+
} catch {
|
|
90
|
+
body = null;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
try {
|
|
94
|
+
body = await resp.text();
|
|
95
|
+
} catch {
|
|
96
|
+
body = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!resp.ok) {
|
|
101
|
+
const errorBody: ApiErrorBody | null =
|
|
102
|
+
body !== null && typeof body === "object"
|
|
103
|
+
? (body as ApiErrorBody)
|
|
104
|
+
: typeof body === "string"
|
|
105
|
+
? { detail: body }
|
|
106
|
+
: null;
|
|
107
|
+
|
|
108
|
+
throw new DingDawgApiError({ status: resp.status, body: errorBody });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return body as T;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Agent API namespace
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/** Methods for managing agents. Available as `client.agent`. */
|
|
119
|
+
export interface AgentApi {
|
|
120
|
+
/**
|
|
121
|
+
* Create a new agent.
|
|
122
|
+
*
|
|
123
|
+
* @param opts - Agent creation options.
|
|
124
|
+
* @returns The newly created AgentRecord.
|
|
125
|
+
*/
|
|
126
|
+
create(opts: CreateAgentOptions): Promise<AgentRecord>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* List all agents belonging to the authenticated partner account.
|
|
130
|
+
*
|
|
131
|
+
* @param params - Optional pagination parameters.
|
|
132
|
+
* @returns Paginated list of AgentRecords.
|
|
133
|
+
*/
|
|
134
|
+
list(params?: { limit?: number; offset?: number }): Promise<PaginatedList<AgentRecord>>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get a single agent by ID.
|
|
138
|
+
*
|
|
139
|
+
* @param id - Agent UUID.
|
|
140
|
+
* @returns The AgentRecord.
|
|
141
|
+
*/
|
|
142
|
+
get(id: string): Promise<AgentRecord>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Send a message to an agent via the trigger endpoint.
|
|
146
|
+
*
|
|
147
|
+
* @param agentId - Agent UUID.
|
|
148
|
+
* @param message - Message text or full options object.
|
|
149
|
+
* @returns The agent's reply and session information.
|
|
150
|
+
*/
|
|
151
|
+
sendMessage(
|
|
152
|
+
agentId: string,
|
|
153
|
+
message: string | SendMessageOptions
|
|
154
|
+
): Promise<TriggerResponse>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Billing API namespace
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/** Methods for querying billing and usage. Available as `client.billing`. */
|
|
162
|
+
export interface BillingApi {
|
|
163
|
+
/**
|
|
164
|
+
* Get billing details for the current calendar month.
|
|
165
|
+
*
|
|
166
|
+
* @returns Monthly billing summary including line items and free-tier usage.
|
|
167
|
+
*/
|
|
168
|
+
currentMonth(): Promise<MonthlyBillingSummary>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get aggregate billing summary (lifetime + current month).
|
|
172
|
+
*
|
|
173
|
+
* @returns Full billing summary.
|
|
174
|
+
*/
|
|
175
|
+
summary(): Promise<BillingSummary>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Main client class
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* DingDawgClient — the single entry point for the DingDawg Agent SDK.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* import { DingDawgClient } from "@dingdawg/sdk";
|
|
188
|
+
*
|
|
189
|
+
* const client = new DingDawgClient({ apiKey: "dd_live_..." });
|
|
190
|
+
*
|
|
191
|
+
* const agent = await client.agent.create({
|
|
192
|
+
* name: "Support Bot",
|
|
193
|
+
* handle: "acme-support",
|
|
194
|
+
* agentType: "business",
|
|
195
|
+
* });
|
|
196
|
+
*
|
|
197
|
+
* const reply = await client.agent.sendMessage(agent.id, "Hello!");
|
|
198
|
+
* console.log(reply.reply);
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export class DingDawgClient {
|
|
202
|
+
private readonly _apiKey: string;
|
|
203
|
+
private readonly _baseUrl: string;
|
|
204
|
+
|
|
205
|
+
/** Methods for managing agents. */
|
|
206
|
+
readonly agent: AgentApi;
|
|
207
|
+
|
|
208
|
+
/** Methods for querying billing and usage. */
|
|
209
|
+
readonly billing: BillingApi;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create a new DingDawgClient instance.
|
|
213
|
+
*
|
|
214
|
+
* @param opts - Configuration options.
|
|
215
|
+
* @throws {TypeError} When apiKey is missing or empty.
|
|
216
|
+
*/
|
|
217
|
+
constructor(opts: DingDawgClientOptions) {
|
|
218
|
+
if (!opts.apiKey || typeof opts.apiKey !== "string" || opts.apiKey.trim() === "") {
|
|
219
|
+
throw new TypeError(
|
|
220
|
+
"DingDawgClient: apiKey is required and must be a non-empty string"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this._apiKey = opts.apiKey.trim();
|
|
225
|
+
this._baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
226
|
+
|
|
227
|
+
// Bind namespace objects so methods can be destructured safely
|
|
228
|
+
this.agent = this._buildAgentApi();
|
|
229
|
+
this.billing = this._buildBillingApi();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
// Internal HTTP request
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Perform an authenticated JSON request.
|
|
238
|
+
*
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
private async _request<T>(
|
|
242
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
243
|
+
path: string,
|
|
244
|
+
body?: unknown
|
|
245
|
+
): Promise<T> {
|
|
246
|
+
const url = `${this._baseUrl}${path}`;
|
|
247
|
+
|
|
248
|
+
const headers: Record<string, string> = {
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
"User-Agent": SDK_USER_AGENT,
|
|
251
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
252
|
+
"X-DingDawg-SDK": "1",
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const init: RequestInit = { method, headers };
|
|
256
|
+
|
|
257
|
+
if (body !== undefined && method !== "GET") {
|
|
258
|
+
init.body = JSON.stringify(body);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let resp: Response;
|
|
262
|
+
try {
|
|
263
|
+
resp = await fetch(url, init);
|
|
264
|
+
} catch (err: unknown) {
|
|
265
|
+
// Network-level failure (DNS, connection refused, etc.)
|
|
266
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
267
|
+
throw new DingDawgApiError(
|
|
268
|
+
{ status: 0, body: { detail: `Network error: ${message}` } },
|
|
269
|
+
`Network error reaching DingDawg API: ${message}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return _parseResponse<T>(resp);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// -------------------------------------------------------------------------
|
|
277
|
+
// Agent API builder
|
|
278
|
+
// -------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
private _buildAgentApi(): AgentApi {
|
|
281
|
+
return {
|
|
282
|
+
create: async (opts: CreateAgentOptions): Promise<AgentRecord> => {
|
|
283
|
+
const payload = {
|
|
284
|
+
name: opts.name,
|
|
285
|
+
handle: opts.handle,
|
|
286
|
+
agent_type: opts.agentType ?? "business",
|
|
287
|
+
industry_type: opts.industry,
|
|
288
|
+
system_prompt: opts.systemPrompt,
|
|
289
|
+
template_id: opts.templateId,
|
|
290
|
+
branding: opts.branding
|
|
291
|
+
? {
|
|
292
|
+
primary_color: opts.branding.primaryColor,
|
|
293
|
+
avatar_url: opts.branding.avatarUrl,
|
|
294
|
+
}
|
|
295
|
+
: undefined,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const raw = await this._request<Record<string, unknown>>(
|
|
299
|
+
"POST",
|
|
300
|
+
"/api/v2/partner/agents",
|
|
301
|
+
payload
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
return _normalizeAgent(raw);
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
list: async (
|
|
308
|
+
params: { limit?: number; offset?: number } = {}
|
|
309
|
+
): Promise<PaginatedList<AgentRecord>> => {
|
|
310
|
+
const qs = new URLSearchParams();
|
|
311
|
+
if (params.limit !== undefined) qs.set("limit", String(params.limit));
|
|
312
|
+
if (params.offset !== undefined) qs.set("offset", String(params.offset));
|
|
313
|
+
const queryString = qs.toString();
|
|
314
|
+
const path = queryString
|
|
315
|
+
? `/api/v2/partner/agents?${queryString}`
|
|
316
|
+
: "/api/v2/partner/agents";
|
|
317
|
+
|
|
318
|
+
const raw = await this._request<Record<string, unknown>>("GET", path);
|
|
319
|
+
const items = (raw["items"] ?? raw["agents"] ?? []) as Record<
|
|
320
|
+
string,
|
|
321
|
+
unknown
|
|
322
|
+
>[];
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
items: items.map(_normalizeAgent),
|
|
326
|
+
total: (raw["total"] as number) ?? items.length,
|
|
327
|
+
limit: (raw["limit"] as number) ?? (params.limit ?? 20),
|
|
328
|
+
offset: (raw["offset"] as number) ?? (params.offset ?? 0),
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
get: async (id: string): Promise<AgentRecord> => {
|
|
333
|
+
const raw = await this._request<Record<string, unknown>>(
|
|
334
|
+
"GET",
|
|
335
|
+
`/api/v2/partner/agents/${encodeURIComponent(id)}`
|
|
336
|
+
);
|
|
337
|
+
return _normalizeAgent(raw);
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
sendMessage: async (
|
|
341
|
+
agentId: string,
|
|
342
|
+
message: string | SendMessageOptions
|
|
343
|
+
): Promise<TriggerResponse> => {
|
|
344
|
+
const opts: SendMessageOptions =
|
|
345
|
+
typeof message === "string" ? { message } : message;
|
|
346
|
+
|
|
347
|
+
const payload = {
|
|
348
|
+
message: opts.message,
|
|
349
|
+
user_id: opts.userId,
|
|
350
|
+
session_id: opts.sessionId,
|
|
351
|
+
metadata: opts.metadata,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const raw = await this._request<Record<string, unknown>>(
|
|
355
|
+
"POST",
|
|
356
|
+
`/api/v1/agents/${encodeURIComponent(agentId)}/trigger`,
|
|
357
|
+
payload
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const triggerResult: TriggerResponse = {
|
|
361
|
+
reply: String(raw["reply"] ?? raw["response"] ?? ""),
|
|
362
|
+
sessionId: String(raw["session_id"] ?? raw["sessionId"] ?? ""),
|
|
363
|
+
timestamp: String(raw["timestamp"] ?? new Date().toISOString()),
|
|
364
|
+
queued: raw["queued"] === true,
|
|
365
|
+
...(raw["model"] !== undefined ? { model: String(raw["model"]) } : {}),
|
|
366
|
+
};
|
|
367
|
+
return triggerResult;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
// Billing API builder
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
private _buildBillingApi(): BillingApi {
|
|
377
|
+
return {
|
|
378
|
+
currentMonth: async (): Promise<MonthlyBillingSummary> => {
|
|
379
|
+
const raw = await this._request<Record<string, unknown>>(
|
|
380
|
+
"GET",
|
|
381
|
+
"/api/v2/partner/billing/current-month"
|
|
382
|
+
);
|
|
383
|
+
return _normalizeMonthlySummary(raw);
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
summary: async (): Promise<BillingSummary> => {
|
|
387
|
+
const raw = await this._request<Record<string, unknown>>(
|
|
388
|
+
"GET",
|
|
389
|
+
"/api/v2/partner/billing"
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const currentMonth = _normalizeMonthlySummary(
|
|
393
|
+
(raw["current_month"] as Record<string, unknown> | undefined) ?? {}
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const billingSummaryResult: BillingSummary = {
|
|
397
|
+
totalActions: (raw["total_actions"] as number) ?? 0,
|
|
398
|
+
totalCents: (raw["total_cents"] as number) ?? 0,
|
|
399
|
+
currentMonth,
|
|
400
|
+
...(raw["stripe_customer_id"] !== undefined
|
|
401
|
+
? { stripeCustomerId: String(raw["stripe_customer_id"]) }
|
|
402
|
+
: {}),
|
|
403
|
+
};
|
|
404
|
+
return billingSummaryResult;
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Normalizer helpers (snake_case → camelCase)
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
function _normalizeAgent(raw: Record<string, unknown>): AgentRecord {
|
|
415
|
+
const branding = raw["branding"] as Record<string, unknown> | undefined;
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
id: String(raw["id"] ?? raw["agent_id"] ?? ""),
|
|
419
|
+
handle: String(raw["handle"] ?? ""),
|
|
420
|
+
name: String(raw["name"] ?? ""),
|
|
421
|
+
agentType: (raw["agent_type"] ?? raw["agentType"] ?? "business") as AgentRecord["agentType"],
|
|
422
|
+
industry: raw["industry_type"] !== undefined
|
|
423
|
+
? String(raw["industry_type"])
|
|
424
|
+
: raw["industry"] !== undefined
|
|
425
|
+
? String(raw["industry"])
|
|
426
|
+
: null,
|
|
427
|
+
status: (raw["status"] ?? "active") as AgentRecord["status"],
|
|
428
|
+
createdAt: String(raw["created_at"] ?? raw["createdAt"] ?? ""),
|
|
429
|
+
updatedAt: String(raw["updated_at"] ?? raw["updatedAt"] ?? ""),
|
|
430
|
+
...(branding
|
|
431
|
+
? {
|
|
432
|
+
branding: {
|
|
433
|
+
...(branding["primary_color"] !== undefined
|
|
434
|
+
? { primaryColor: String(branding["primary_color"]) }
|
|
435
|
+
: {}),
|
|
436
|
+
...(branding["avatar_url"] !== undefined
|
|
437
|
+
? { avatarUrl: String(branding["avatar_url"]) }
|
|
438
|
+
: {}),
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
: {}),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function _normalizeMonthlySummary(
|
|
446
|
+
raw: Record<string, unknown>
|
|
447
|
+
): MonthlyBillingSummary {
|
|
448
|
+
const lineItemsRaw = Array.isArray(raw["line_items"]) ? raw["line_items"] : [];
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
month: String(raw["month"] ?? ""),
|
|
452
|
+
totalActions: (raw["total_actions"] as number) ?? 0,
|
|
453
|
+
totalCents: (raw["total_cents"] as number) ?? 0,
|
|
454
|
+
freeActionsRemaining: (raw["free_actions_remaining"] as number) ?? 0,
|
|
455
|
+
lineItems: (lineItemsRaw as Record<string, unknown>[]).map((item) => ({
|
|
456
|
+
action: String(item["action"] ?? ""),
|
|
457
|
+
count: (item["count"] as number) ?? 0,
|
|
458
|
+
costCents: (item["cost_cents"] as number) ?? 0,
|
|
459
|
+
})),
|
|
460
|
+
};
|
|
461
|
+
}
|