@answerloops/agent-sdk 0.6.0 → 0.6.2

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
@@ -48,7 +48,9 @@ const answer = await client.generateAnswer({ question: "How do I reset my API ke
48
48
 
49
49
  const ticket = await client.createTicket({ content: "My webhook stopped firing." });
50
50
 
51
- const { tickets } = await client.getTickets({ status: "open", limit: 10 });
51
+ const { tickets, next_cursor } = await client.getTickets({ status: "open", limit: 10 });
52
+ // next_cursor is non-null while more tickets remain — pass it back as `cursor` to keep paging:
53
+ // await client.getTickets({ status: "open", limit: 10, cursor: next_cursor });
52
54
  ```
53
55
 
54
56
  Every key carries least-privilege scopes (`kb:read`, `faq:read`, `tickets:read`, `tickets:write`, `answers:write`) set in **Settings → API Keys**. A call against a scope the key doesn't have throws `AgentApiError` with `status === 403`.
@@ -64,7 +66,14 @@ try {
64
66
  await client.generateAnswer({ question: "..." });
65
67
  } catch (err) {
66
68
  if (err instanceof AgentApiError) {
67
- console.error(err.status, err.body);
69
+ console.error(err.status, err.message);
70
+ // On a 429, err.code tells apart the per-minute rate limit from either
71
+ // monthly quota — "rate_limited" (retry shortly) vs.
72
+ // "deflection_limit_reached" / "call_limit_reached" (upgrade, or wait
73
+ // for the next billing cycle; retrying immediately won't help).
74
+ if (err.code === "rate_limited") {
75
+ // back off and retry
76
+ }
68
77
  }
69
78
  }
70
79
  ```
package/dist/index.cjs CHANGED
@@ -29,11 +29,14 @@ module.exports = __toCommonJS(index_exports);
29
29
  var AgentApiError = class extends Error {
30
30
  status;
31
31
  body;
32
+ /** Only ever set on a 429 — see AgentApiErrorCode. */
33
+ code;
32
34
  constructor(status, body, message) {
33
35
  super(message);
34
36
  this.name = "AgentApiError";
35
37
  this.status = status;
36
38
  this.body = body;
39
+ this.code = body?.error?.code;
37
40
  }
38
41
  };
39
42
  var DEFAULT_BASE_URL = "https://answerloops.com";
@@ -49,54 +52,64 @@ var AgentClient = class {
49
52
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
50
53
  this.fetchImpl = options.fetch ?? fetch;
51
54
  }
52
- /** GET /api/agent/kb/search — requires the `kb:read` scope. */
55
+ /** GET /api/v1/agent/kb/search — requires the `kb:read` scope. */
53
56
  searchKb(params) {
54
57
  const query = new URLSearchParams({ query: params.query });
55
58
  if (params.limit !== void 0) query.set("limit", String(params.limit));
56
- return this.request("GET", `/api/agent/kb/search?${query}`);
59
+ return this.request("GET", `/api/v1/agent/kb/search?${query}`);
57
60
  }
58
- /** GET /api/agent/faq — requires the `faq:read` scope. */
61
+ /** GET /api/v1/agent/faq — requires the `faq:read` scope. */
59
62
  getFaq() {
60
- return this.request("GET", "/api/agent/faq");
63
+ return this.request("GET", "/api/v1/agent/faq");
61
64
  }
62
- /** GET /api/agent/tickets — requires the `tickets:read` scope. */
65
+ /** GET /api/v1/agent/tickets — requires the `tickets:read` scope. */
63
66
  getTickets(params = {}) {
64
67
  const query = new URLSearchParams();
65
68
  if (params.status) query.set("status", params.status);
66
69
  if (params.priority) query.set("priority", params.priority);
67
70
  if (params.category) query.set("category", params.category);
68
71
  if (params.limit !== void 0) query.set("limit", String(params.limit));
72
+ if (params.cursor !== void 0) query.set("cursor", params.cursor);
69
73
  const qs = query.toString();
70
- return this.request("GET", `/api/agent/tickets${qs ? `?${qs}` : ""}`);
74
+ return this.request("GET", `/api/v1/agent/tickets${qs ? `?${qs}` : ""}`);
71
75
  }
72
- /** POST /api/agent/tickets — requires the `tickets:write` scope. */
76
+ /**
77
+ * POST /api/v1/agent/tickets — requires the `tickets:write` scope.
78
+ * `idempotencyKey`, when given, is sent as the standard `Idempotency-Key`
79
+ * header (what most HTTP clients/SDKs attach automatically) rather than a
80
+ * body field — the server accepts either, but the header is what it
81
+ * prefers when both are present.
82
+ */
73
83
  createTicket(params) {
74
- return this.request("POST", "/api/agent/tickets", {
75
- content: params.content,
76
- authorName: params.authorName,
77
- idempotencyKey: params.idempotencyKey
78
- });
84
+ return this.request(
85
+ "POST",
86
+ "/api/v1/agent/tickets",
87
+ { content: params.content, authorName: params.authorName },
88
+ params.idempotencyKey ? { "Idempotency-Key": params.idempotencyKey } : void 0
89
+ );
79
90
  }
80
- /** POST /api/agent/answers — requires the `answers:write` scope. */
91
+ /** POST /api/v1/agent/answers — requires the `answers:write` scope. */
81
92
  generateAnswer(params) {
82
- return this.request("POST", "/api/agent/answers", { question: params.question });
93
+ return this.request("POST", "/api/v1/agent/answers", { question: params.question });
83
94
  }
84
- async request(method, path, body) {
95
+ async request(method, path, body, extraHeaders) {
85
96
  const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
86
97
  method,
87
98
  headers: {
88
99
  Authorization: `Bearer ${this.apiKey}`,
89
- ...body ? { "Content-Type": "application/json" } : {}
100
+ ...body ? { "Content-Type": "application/json" } : {},
101
+ ...extraHeaders
90
102
  },
91
103
  body: body ? JSON.stringify(body) : void 0
92
104
  });
93
105
  const text = await res.text();
94
106
  const data = text ? JSON.parse(text) : void 0;
95
107
  if (!res.ok) {
108
+ const errorBody = data;
96
109
  throw new AgentApiError(
97
110
  res.status,
98
- data,
99
- data?.error ?? `Agent API request failed: ${res.status}`
111
+ errorBody,
112
+ errorBody?.error?.message ?? `Agent API request failed: ${res.status}`
100
113
  );
101
114
  }
102
115
  return data;
package/dist/index.d.cts CHANGED
@@ -22,11 +22,15 @@ interface GetTicketsParams {
22
22
  status?: TicketStatus;
23
23
  priority?: TicketPriority;
24
24
  category?: TicketCategory;
25
- /** 1-20, defaults to 10 */
25
+ /** Max results per page, 1-20, defaults to 10 */
26
26
  limit?: number;
27
+ /** Opaque value from a previous response's `next_cursor`. Omit for the first page. */
28
+ cursor?: string;
27
29
  }
28
30
  interface GetTicketsResponse {
29
31
  tickets: Ticket[];
32
+ /** Pass as `cursor` on the next call to keep paging; null once there's nothing left. */
33
+ next_cursor: string | null;
30
34
  }
31
35
  interface CreateTicketParams {
32
36
  /** The user's question or issue, verbatim. Max 4000 characters. */
@@ -36,6 +40,19 @@ interface CreateTicketParams {
36
40
  /** Retrying with the same key returns the original ticket instead of opening a duplicate. */
37
41
  idempotencyKey?: string;
38
42
  }
43
+ interface CreateTicketResponse {
44
+ ticket_id: number;
45
+ /** True if this call matched an existing idempotency key instead of opening a new ticket. */
46
+ duplicate: boolean;
47
+ }
48
+ interface FaqResult {
49
+ /** Present only when no FAQ has been generated for the org yet. */
50
+ message?: string;
51
+ week_start?: string;
52
+ week_end?: string;
53
+ ticket_count?: number;
54
+ content?: string;
55
+ }
39
56
  interface GenerateAnswerParams {
40
57
  /** Max 2000 characters. */
41
58
  question: string;
@@ -52,10 +69,19 @@ interface SearchKbParams {
52
69
  /** 1-20, defaults to 5 */
53
70
  limit?: number;
54
71
  }
72
+ /**
73
+ * `code` is only ever present on a 429 — it's how a caller tells apart the
74
+ * three independent ceilings this API can hit (rate limit vs. either
75
+ * monthly quota) without pattern-matching `message`. See the `Error` schema
76
+ * in the OpenAPI spec (lib/agent/openapi-spec.ts) for the source of truth.
77
+ */
78
+ type AgentApiErrorCode = "rate_limited" | "deflection_limit_reached" | "call_limit_reached";
55
79
  /** Shape of the `Error` schema returned on 400/401/403/429 responses. */
56
80
  interface AgentApiErrorBody {
57
- error: string;
58
- [key: string]: unknown;
81
+ error: {
82
+ message: string;
83
+ code?: AgentApiErrorCode;
84
+ };
59
85
  }
60
86
 
61
87
  interface AgentClientOptions {
@@ -72,6 +98,8 @@ interface AgentClientOptions {
72
98
  declare class AgentApiError extends Error {
73
99
  readonly status: number;
74
100
  readonly body: AgentApiErrorBody | undefined;
101
+ /** Only ever set on a 429 — see AgentApiErrorCode. */
102
+ readonly code: AgentApiErrorCode | undefined;
75
103
  constructor(status: number, body: AgentApiErrorBody | undefined, message: string);
76
104
  }
77
105
  declare class AgentClient {
@@ -79,17 +107,23 @@ declare class AgentClient {
79
107
  private readonly baseUrl;
80
108
  private readonly fetchImpl;
81
109
  constructor(options: AgentClientOptions);
82
- /** GET /api/agent/kb/search — requires the `kb:read` scope. */
110
+ /** GET /api/v1/agent/kb/search — requires the `kb:read` scope. */
83
111
  searchKb(params: SearchKbParams): Promise<KbSearchResponse>;
84
- /** GET /api/agent/faq — requires the `faq:read` scope. */
85
- getFaq(): Promise<unknown>;
86
- /** GET /api/agent/tickets — requires the `tickets:read` scope. */
112
+ /** GET /api/v1/agent/faq — requires the `faq:read` scope. */
113
+ getFaq(): Promise<FaqResult>;
114
+ /** GET /api/v1/agent/tickets — requires the `tickets:read` scope. */
87
115
  getTickets(params?: GetTicketsParams): Promise<GetTicketsResponse>;
88
- /** POST /api/agent/tickets — requires the `tickets:write` scope. */
89
- createTicket(params: CreateTicketParams): Promise<Ticket>;
90
- /** POST /api/agent/answers — requires the `answers:write` scope. */
116
+ /**
117
+ * POST /api/v1/agent/tickets — requires the `tickets:write` scope.
118
+ * `idempotencyKey`, when given, is sent as the standard `Idempotency-Key`
119
+ * header (what most HTTP clients/SDKs attach automatically) rather than a
120
+ * body field — the server accepts either, but the header is what it
121
+ * prefers when both are present.
122
+ */
123
+ createTicket(params: CreateTicketParams): Promise<CreateTicketResponse>;
124
+ /** POST /api/v1/agent/answers — requires the `answers:write` scope. */
91
125
  generateAnswer(params: GenerateAnswerParams): Promise<GenerateAnswerResponse>;
92
126
  private request;
93
127
  }
94
128
 
95
- export { AgentApiError, type AgentApiErrorBody, AgentClient, type AgentClientOptions, type CreateTicketParams, type GenerateAnswerParams, type GenerateAnswerResponse, type GetTicketsParams, type GetTicketsResponse, type KbSearchResponse, type KbSearchResult, type SearchKbParams, type Ticket, type TicketCategory, type TicketPriority, type TicketStatus };
129
+ export { AgentApiError, type AgentApiErrorBody, type AgentApiErrorCode, AgentClient, type AgentClientOptions, type CreateTicketParams, type CreateTicketResponse, type FaqResult, type GenerateAnswerParams, type GenerateAnswerResponse, type GetTicketsParams, type GetTicketsResponse, type KbSearchResponse, type KbSearchResult, type SearchKbParams, type Ticket, type TicketCategory, type TicketPriority, type TicketStatus };
package/dist/index.d.ts CHANGED
@@ -22,11 +22,15 @@ interface GetTicketsParams {
22
22
  status?: TicketStatus;
23
23
  priority?: TicketPriority;
24
24
  category?: TicketCategory;
25
- /** 1-20, defaults to 10 */
25
+ /** Max results per page, 1-20, defaults to 10 */
26
26
  limit?: number;
27
+ /** Opaque value from a previous response's `next_cursor`. Omit for the first page. */
28
+ cursor?: string;
27
29
  }
28
30
  interface GetTicketsResponse {
29
31
  tickets: Ticket[];
32
+ /** Pass as `cursor` on the next call to keep paging; null once there's nothing left. */
33
+ next_cursor: string | null;
30
34
  }
31
35
  interface CreateTicketParams {
32
36
  /** The user's question or issue, verbatim. Max 4000 characters. */
@@ -36,6 +40,19 @@ interface CreateTicketParams {
36
40
  /** Retrying with the same key returns the original ticket instead of opening a duplicate. */
37
41
  idempotencyKey?: string;
38
42
  }
43
+ interface CreateTicketResponse {
44
+ ticket_id: number;
45
+ /** True if this call matched an existing idempotency key instead of opening a new ticket. */
46
+ duplicate: boolean;
47
+ }
48
+ interface FaqResult {
49
+ /** Present only when no FAQ has been generated for the org yet. */
50
+ message?: string;
51
+ week_start?: string;
52
+ week_end?: string;
53
+ ticket_count?: number;
54
+ content?: string;
55
+ }
39
56
  interface GenerateAnswerParams {
40
57
  /** Max 2000 characters. */
41
58
  question: string;
@@ -52,10 +69,19 @@ interface SearchKbParams {
52
69
  /** 1-20, defaults to 5 */
53
70
  limit?: number;
54
71
  }
72
+ /**
73
+ * `code` is only ever present on a 429 — it's how a caller tells apart the
74
+ * three independent ceilings this API can hit (rate limit vs. either
75
+ * monthly quota) without pattern-matching `message`. See the `Error` schema
76
+ * in the OpenAPI spec (lib/agent/openapi-spec.ts) for the source of truth.
77
+ */
78
+ type AgentApiErrorCode = "rate_limited" | "deflection_limit_reached" | "call_limit_reached";
55
79
  /** Shape of the `Error` schema returned on 400/401/403/429 responses. */
56
80
  interface AgentApiErrorBody {
57
- error: string;
58
- [key: string]: unknown;
81
+ error: {
82
+ message: string;
83
+ code?: AgentApiErrorCode;
84
+ };
59
85
  }
60
86
 
61
87
  interface AgentClientOptions {
@@ -72,6 +98,8 @@ interface AgentClientOptions {
72
98
  declare class AgentApiError extends Error {
73
99
  readonly status: number;
74
100
  readonly body: AgentApiErrorBody | undefined;
101
+ /** Only ever set on a 429 — see AgentApiErrorCode. */
102
+ readonly code: AgentApiErrorCode | undefined;
75
103
  constructor(status: number, body: AgentApiErrorBody | undefined, message: string);
76
104
  }
77
105
  declare class AgentClient {
@@ -79,17 +107,23 @@ declare class AgentClient {
79
107
  private readonly baseUrl;
80
108
  private readonly fetchImpl;
81
109
  constructor(options: AgentClientOptions);
82
- /** GET /api/agent/kb/search — requires the `kb:read` scope. */
110
+ /** GET /api/v1/agent/kb/search — requires the `kb:read` scope. */
83
111
  searchKb(params: SearchKbParams): Promise<KbSearchResponse>;
84
- /** GET /api/agent/faq — requires the `faq:read` scope. */
85
- getFaq(): Promise<unknown>;
86
- /** GET /api/agent/tickets — requires the `tickets:read` scope. */
112
+ /** GET /api/v1/agent/faq — requires the `faq:read` scope. */
113
+ getFaq(): Promise<FaqResult>;
114
+ /** GET /api/v1/agent/tickets — requires the `tickets:read` scope. */
87
115
  getTickets(params?: GetTicketsParams): Promise<GetTicketsResponse>;
88
- /** POST /api/agent/tickets — requires the `tickets:write` scope. */
89
- createTicket(params: CreateTicketParams): Promise<Ticket>;
90
- /** POST /api/agent/answers — requires the `answers:write` scope. */
116
+ /**
117
+ * POST /api/v1/agent/tickets — requires the `tickets:write` scope.
118
+ * `idempotencyKey`, when given, is sent as the standard `Idempotency-Key`
119
+ * header (what most HTTP clients/SDKs attach automatically) rather than a
120
+ * body field — the server accepts either, but the header is what it
121
+ * prefers when both are present.
122
+ */
123
+ createTicket(params: CreateTicketParams): Promise<CreateTicketResponse>;
124
+ /** POST /api/v1/agent/answers — requires the `answers:write` scope. */
91
125
  generateAnswer(params: GenerateAnswerParams): Promise<GenerateAnswerResponse>;
92
126
  private request;
93
127
  }
94
128
 
95
- export { AgentApiError, type AgentApiErrorBody, AgentClient, type AgentClientOptions, type CreateTicketParams, type GenerateAnswerParams, type GenerateAnswerResponse, type GetTicketsParams, type GetTicketsResponse, type KbSearchResponse, type KbSearchResult, type SearchKbParams, type Ticket, type TicketCategory, type TicketPriority, type TicketStatus };
129
+ export { AgentApiError, type AgentApiErrorBody, type AgentApiErrorCode, AgentClient, type AgentClientOptions, type CreateTicketParams, type CreateTicketResponse, type FaqResult, type GenerateAnswerParams, type GenerateAnswerResponse, type GetTicketsParams, type GetTicketsResponse, type KbSearchResponse, type KbSearchResult, type SearchKbParams, type Ticket, type TicketCategory, type TicketPriority, type TicketStatus };
package/dist/index.js CHANGED
@@ -2,11 +2,14 @@
2
2
  var AgentApiError = class extends Error {
3
3
  status;
4
4
  body;
5
+ /** Only ever set on a 429 — see AgentApiErrorCode. */
6
+ code;
5
7
  constructor(status, body, message) {
6
8
  super(message);
7
9
  this.name = "AgentApiError";
8
10
  this.status = status;
9
11
  this.body = body;
12
+ this.code = body?.error?.code;
10
13
  }
11
14
  };
12
15
  var DEFAULT_BASE_URL = "https://answerloops.com";
@@ -22,54 +25,64 @@ var AgentClient = class {
22
25
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
23
26
  this.fetchImpl = options.fetch ?? fetch;
24
27
  }
25
- /** GET /api/agent/kb/search — requires the `kb:read` scope. */
28
+ /** GET /api/v1/agent/kb/search — requires the `kb:read` scope. */
26
29
  searchKb(params) {
27
30
  const query = new URLSearchParams({ query: params.query });
28
31
  if (params.limit !== void 0) query.set("limit", String(params.limit));
29
- return this.request("GET", `/api/agent/kb/search?${query}`);
32
+ return this.request("GET", `/api/v1/agent/kb/search?${query}`);
30
33
  }
31
- /** GET /api/agent/faq — requires the `faq:read` scope. */
34
+ /** GET /api/v1/agent/faq — requires the `faq:read` scope. */
32
35
  getFaq() {
33
- return this.request("GET", "/api/agent/faq");
36
+ return this.request("GET", "/api/v1/agent/faq");
34
37
  }
35
- /** GET /api/agent/tickets — requires the `tickets:read` scope. */
38
+ /** GET /api/v1/agent/tickets — requires the `tickets:read` scope. */
36
39
  getTickets(params = {}) {
37
40
  const query = new URLSearchParams();
38
41
  if (params.status) query.set("status", params.status);
39
42
  if (params.priority) query.set("priority", params.priority);
40
43
  if (params.category) query.set("category", params.category);
41
44
  if (params.limit !== void 0) query.set("limit", String(params.limit));
45
+ if (params.cursor !== void 0) query.set("cursor", params.cursor);
42
46
  const qs = query.toString();
43
- return this.request("GET", `/api/agent/tickets${qs ? `?${qs}` : ""}`);
47
+ return this.request("GET", `/api/v1/agent/tickets${qs ? `?${qs}` : ""}`);
44
48
  }
45
- /** POST /api/agent/tickets — requires the `tickets:write` scope. */
49
+ /**
50
+ * POST /api/v1/agent/tickets — requires the `tickets:write` scope.
51
+ * `idempotencyKey`, when given, is sent as the standard `Idempotency-Key`
52
+ * header (what most HTTP clients/SDKs attach automatically) rather than a
53
+ * body field — the server accepts either, but the header is what it
54
+ * prefers when both are present.
55
+ */
46
56
  createTicket(params) {
47
- return this.request("POST", "/api/agent/tickets", {
48
- content: params.content,
49
- authorName: params.authorName,
50
- idempotencyKey: params.idempotencyKey
51
- });
57
+ return this.request(
58
+ "POST",
59
+ "/api/v1/agent/tickets",
60
+ { content: params.content, authorName: params.authorName },
61
+ params.idempotencyKey ? { "Idempotency-Key": params.idempotencyKey } : void 0
62
+ );
52
63
  }
53
- /** POST /api/agent/answers — requires the `answers:write` scope. */
64
+ /** POST /api/v1/agent/answers — requires the `answers:write` scope. */
54
65
  generateAnswer(params) {
55
- return this.request("POST", "/api/agent/answers", { question: params.question });
66
+ return this.request("POST", "/api/v1/agent/answers", { question: params.question });
56
67
  }
57
- async request(method, path, body) {
68
+ async request(method, path, body, extraHeaders) {
58
69
  const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
59
70
  method,
60
71
  headers: {
61
72
  Authorization: `Bearer ${this.apiKey}`,
62
- ...body ? { "Content-Type": "application/json" } : {}
73
+ ...body ? { "Content-Type": "application/json" } : {},
74
+ ...extraHeaders
63
75
  },
64
76
  body: body ? JSON.stringify(body) : void 0
65
77
  });
66
78
  const text = await res.text();
67
79
  const data = text ? JSON.parse(text) : void 0;
68
80
  if (!res.ok) {
81
+ const errorBody = data;
69
82
  throw new AgentApiError(
70
83
  res.status,
71
- data,
72
- data?.error ?? `Agent API request failed: ${res.status}`
84
+ errorBody,
85
+ errorBody?.error?.message ?? `Agent API request failed: ${res.status}`
73
86
  );
74
87
  }
75
88
  return data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@answerloops/agent-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Typed client for the answerLoops Agent API (knowledge base search, FAQ, tickets, grounded answers) and a CLI to self-host answerLoops or install its Claude Code skills.",
5
5
  "license": "AGPL-3.0",
6
6
  "type": "module",