vessels-sdk 0.5.1 → 0.8.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
@@ -38,14 +38,20 @@ const { messageId, vesselId } = await vessels.push({
38
38
  });
39
39
  ```
40
40
 
41
- Get your API key from Settings API Keys in the [Vessels app](https://vessels.app), or via the CLI:
41
+ Get your API key via the CLI (Claude Code / AI assistants fully non-interactive):
42
42
 
43
43
  ```bash
44
44
  npm install -g vessels
45
- vessels login
46
- vessels keys create
45
+
46
+ # Step 1: send OTP to your email
47
+ vessels init --email me@example.com
48
+
49
+ # Step 2: verify with the code from your inbox — prints VESSELS_API_KEY=vsl_xxx
50
+ vessels init --email me@example.com --otp 847293
47
51
  ```
48
52
 
53
+ Or sign in at [vessels.app](https://vessels.app) → Settings → API Keys.
54
+
49
55
  ---
50
56
 
51
57
  ## API reference
package/dist/index.cjs CHANGED
@@ -3,6 +3,14 @@
3
3
  var zod = require('zod');
4
4
 
5
5
  // ../types/src/index.ts
6
+ var SourceSchema = zod.z.enum(["agent", "user", "system"]);
7
+ var InteractionTypeSchema = zod.z.enum([
8
+ "approval",
9
+ "choice",
10
+ "checklist",
11
+ "text_input",
12
+ "confirm_preview"
13
+ ]);
6
14
  var ApprovalInteractionSchema = zod.z.object({
7
15
  type: zod.z.literal("approval"),
8
16
  prompt: zod.z.string().min(1),
@@ -152,11 +160,20 @@ var WebhookVesselSchema = zod.z.object({
152
160
  id: zod.z.string(),
153
161
  external_id: zod.z.string().nullable(),
154
162
  title: zod.z.string().nullable(),
163
+ /** The vessel's type (e.g. "Ticket"). Nullable — agent-created vessels have no type. */
164
+ type: zod.z.string().nullable().optional(),
155
165
  metadata: zod.z.record(zod.z.unknown())
156
166
  });
157
167
  var WebhookContextMessageSchema = zod.z.object({
158
- source: zod.z.string(),
168
+ source: SourceSchema,
169
+ content: zod.z.string().nullable(),
170
+ created_at: zod.z.string()
171
+ });
172
+ var WebhookOriginMessageSchema = zod.z.object({
173
+ id: zod.z.string(),
159
174
  content: zod.z.string().nullable(),
175
+ interaction: zod.z.record(zod.z.unknown()).nullable(),
176
+ metadata: zod.z.record(zod.z.unknown()),
160
177
  created_at: zod.z.string()
161
178
  });
162
179
  var WebhookInteractionResponsePayloadSchema = zod.z.object({
@@ -166,10 +183,12 @@ var WebhookInteractionResponsePayloadSchema = zod.z.object({
166
183
  timestamp: zod.z.string(),
167
184
  data: zod.z.object({
168
185
  message_id: zod.z.string(),
169
- interaction_type: zod.z.string(),
186
+ interaction_type: InteractionTypeSchema,
170
187
  response: zod.z.record(zod.z.unknown()),
171
188
  response_id: zod.z.string(),
172
189
  metadata: zod.z.record(zod.z.unknown()).optional(),
190
+ /** The agent message that carried the interaction (prompt, content, metadata). */
191
+ message: WebhookOriginMessageSchema,
173
192
  vessel: WebhookVesselSchema
174
193
  })
175
194
  });
@@ -185,9 +204,24 @@ var WebhookUserMessagePayloadSchema = zod.z.object({
185
204
  context: zod.z.array(WebhookContextMessageSchema)
186
205
  })
187
206
  });
207
+ var WebhookVesselCreatedPayloadSchema = zod.z.object({
208
+ event: zod.z.literal("vessel.created"),
209
+ vessel_id: zod.z.string(),
210
+ workspace_id: zod.z.string(),
211
+ timestamp: zod.z.string(),
212
+ data: zod.z.object({
213
+ vessel: WebhookVesselSchema,
214
+ message: zod.z.object({
215
+ message_id: zod.z.string(),
216
+ content: zod.z.string().nullable(),
217
+ created_at: zod.z.string()
218
+ })
219
+ })
220
+ });
188
221
  var WebhookPayloadSchema = zod.z.discriminatedUnion("event", [
189
222
  WebhookInteractionResponsePayloadSchema,
190
- WebhookUserMessagePayloadSchema
223
+ WebhookUserMessagePayloadSchema,
224
+ WebhookVesselCreatedPayloadSchema
191
225
  ]);
192
226
 
193
227
  // src/index.ts
@@ -220,6 +254,12 @@ var VesselsRateLimitError = class extends Error {
220
254
  this.retryAfter = retryAfter;
221
255
  }
222
256
  };
257
+ var VesselsConflictError = class extends Error {
258
+ constructor(message) {
259
+ super(message);
260
+ this.name = "VesselsConflictError";
261
+ }
262
+ };
223
263
  var Vessels = class {
224
264
  apiKey;
225
265
  baseUrl;
@@ -253,38 +293,45 @@ var Vessels = class {
253
293
  return res;
254
294
  }
255
295
  async push(payload) {
296
+ const { idempotencyKey, ...body } = payload;
256
297
  const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
257
298
  method: "POST",
258
299
  headers: {
259
300
  "Content-Type": "application/json",
260
- "Authorization": `Bearer ${this.apiKey}`
301
+ "Authorization": `Bearer ${this.apiKey}`,
302
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
261
303
  },
262
- body: JSON.stringify(payload)
304
+ body: JSON.stringify(body)
263
305
  });
264
306
  const data = await res.json();
265
307
  if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
266
308
  if (res.status === 429) throw new VesselsRateLimitError(data.error ?? "Rate limited", Number(res.headers.get("retry-after")));
309
+ if (res.status === 409) throw new VesselsConflictError(data.error ?? "Idempotent request in progress");
267
310
  if (res.status === 400) throw new VesselsValidationError(data.error ?? "Validation failed", data.details);
268
311
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
269
312
  return {
270
313
  ok: true,
271
314
  messageId: data.message_id,
272
315
  vesselId: data.vessel_id,
273
- createdAt: data.created_at
316
+ createdAt: data.created_at,
317
+ replayed: res.headers.get("Idempotent-Replayed") === "true"
274
318
  };
275
319
  }
276
320
  async pushMany(payload) {
321
+ const { idempotencyKey, ...body } = payload;
277
322
  const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
278
323
  method: "POST",
279
324
  headers: {
280
325
  "Content-Type": "application/json",
281
- "Authorization": `Bearer ${this.apiKey}`
326
+ "Authorization": `Bearer ${this.apiKey}`,
327
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
282
328
  },
283
- body: JSON.stringify(payload)
329
+ body: JSON.stringify(body)
284
330
  });
285
331
  const data = await res.json();
286
332
  if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
287
333
  if (res.status === 429) throw new VesselsRateLimitError(data.error ?? "Rate limited", Number(res.headers.get("retry-after")));
334
+ if (res.status === 409) throw new VesselsConflictError(data.error ?? "Idempotent request in progress");
288
335
  if (res.status === 400) throw new VesselsValidationError(data.error ?? "Validation failed", data.details);
289
336
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
290
337
  return {
@@ -294,7 +341,8 @@ var Vessels = class {
294
341
  messageId: r.message_id,
295
342
  vesselId: r.vessel_id,
296
343
  ...r.error ? { error: r.error } : {}
297
- }))
344
+ })),
345
+ replayed: res.headers.get("Idempotent-Replayed") === "true"
298
346
  };
299
347
  }
300
348
  async editMessage(messageId, patch) {
@@ -314,6 +362,38 @@ var Vessels = class {
314
362
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
315
363
  return { ok: true };
316
364
  }
365
+ /**
366
+ * Read a vessel's message history — the human-facing record, for re-reading
367
+ * the channel (e.g. a stateless or just-restarted worker reconciling state).
368
+ * This is NOT your agent's memory; keep your canonical history in your own
369
+ * system. `vessel` is your own external_id string by default; pass
370
+ * { byUuid: true } if you hold a Vessels UUID. Newest-last, paginate with `before`.
371
+ */
372
+ async getMessages(opts) {
373
+ const params = new URLSearchParams();
374
+ if (opts.limit != null) params.set("limit", String(opts.limit));
375
+ if (opts.before) params.set("before", opts.before);
376
+ const path = opts.byUuid ? `/api/v1/vessels/${encodeURIComponent(opts.vessel)}/messages` : `/api/v1/vessels/by-external/${encodeURIComponent(opts.vessel)}/messages`;
377
+ const res = await this._fetch(`${this.baseUrl}${path}?${params}`, {
378
+ headers: { "Authorization": `Bearer ${this.apiKey}` }
379
+ });
380
+ const data = await res.json();
381
+ if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
382
+ if (res.status === 404) throw new Error(data.error ?? "Vessel not found");
383
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
384
+ const messages = (data.messages ?? []).map((m) => ({
385
+ id: m.id,
386
+ source: m.source,
387
+ content: m.content ?? null,
388
+ card: m.card ?? null,
389
+ interaction: m.interaction ?? null,
390
+ attachments: m.attachments ?? [],
391
+ suggestions: m.suggestions ?? [],
392
+ metadata: m.metadata ?? {},
393
+ createdAt: m.created_at
394
+ }));
395
+ return { messages, hasMore: data.has_more ?? false };
396
+ }
317
397
  // Interaction helpers
318
398
  approval(opts) {
319
399
  return { type: "approval", ...opts };
@@ -347,9 +427,19 @@ var Vessels = class {
347
427
  id: e.vessel?.id,
348
428
  externalId: e.vessel?.external_id ?? null,
349
429
  title: e.vessel?.title ?? null,
430
+ type: e.vessel?.type ?? null,
350
431
  metadata: e.vessel?.metadata ?? {},
351
432
  labels: e.vessel?.labels ?? []
352
433
  };
434
+ if (e.type === "vessel.created") {
435
+ return {
436
+ id: e.id,
437
+ type: "vessel.created",
438
+ timestamp: e.timestamp,
439
+ vessel,
440
+ message: { id: e.message?.id, content: e.message?.content ?? null }
441
+ };
442
+ }
353
443
  if (e.type === "interaction.response") {
354
444
  return {
355
445
  id: e.id,
@@ -360,6 +450,13 @@ var Vessels = class {
360
450
  interactionType: e.interaction_type,
361
451
  response: e.response,
362
452
  interactionMetadata: e.interaction_metadata ?? null,
453
+ originMessage: e.message ? {
454
+ id: e.message.id,
455
+ content: e.message.content ?? null,
456
+ interaction: e.message.interaction ?? null,
457
+ metadata: e.message.metadata ?? {},
458
+ createdAt: e.message.created_at
459
+ } : null,
363
460
  user: e.user ?? null
364
461
  };
365
462
  }
@@ -414,9 +511,22 @@ var Vessels = class {
414
511
  id: v.id,
415
512
  externalId: v.external_id ?? null,
416
513
  title: v.title ?? null,
514
+ type: v.type ?? null,
417
515
  metadata: v.metadata ?? {},
418
516
  labels: v.labels ?? []
419
517
  };
518
+ if (raw.event === "vessel.created") {
519
+ return {
520
+ id: raw.vessel_id,
521
+ type: "vessel.created",
522
+ timestamp: raw.timestamp,
523
+ vessel,
524
+ message: {
525
+ id: raw.data.message?.message_id,
526
+ content: raw.data.message?.content ?? null
527
+ }
528
+ };
529
+ }
420
530
  if (raw.event === "interaction.response") {
421
531
  return {
422
532
  id: raw.data.response_id,
@@ -427,6 +537,13 @@ var Vessels = class {
427
537
  interactionType: raw.data.interaction_type,
428
538
  response: raw.data.response,
429
539
  interactionMetadata: raw.data.metadata ?? null,
540
+ originMessage: raw.data.message ? {
541
+ id: raw.data.message.id,
542
+ content: raw.data.message.content ?? null,
543
+ interaction: raw.data.message.interaction ?? null,
544
+ metadata: raw.data.message.metadata ?? {},
545
+ createdAt: raw.data.message.created_at
546
+ } : null,
430
547
  user: raw.data.user ?? null
431
548
  };
432
549
  }
@@ -454,8 +571,10 @@ var Vessels = class {
454
571
  exports.AgentActivityTypes = AgentActivityTypes;
455
572
  exports.Vessels = Vessels;
456
573
  exports.VesselsAuthError = VesselsAuthError;
574
+ exports.VesselsConflictError = VesselsConflictError;
457
575
  exports.VesselsRateLimitError = VesselsRateLimitError;
458
576
  exports.VesselsValidationError = VesselsValidationError;
459
577
  exports.WebhookInteractionResponsePayloadSchema = WebhookInteractionResponsePayloadSchema;
460
578
  exports.WebhookPayloadSchema = WebhookPayloadSchema;
461
579
  exports.WebhookUserMessagePayloadSchema = WebhookUserMessagePayloadSchema;
580
+ exports.WebhookVesselCreatedPayloadSchema = WebhookVesselCreatedPayloadSchema;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _vessels_types from '@vessels/types';
2
- export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyPayload, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema } from '@vessels/types';
2
+ export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyOptions, PushManyPayload, PushOptions, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema, WebhookVesselCreatedPayload, WebhookVesselCreatedPayloadSchema } from '@vessels/types';
3
3
 
4
4
  declare const AgentActivityTypes: {
5
5
  readonly thinking: "thinking";
@@ -19,6 +19,10 @@ declare class VesselsRateLimitError extends Error {
19
19
  retryAfter?: number;
20
20
  constructor(message: string, retryAfter?: number);
21
21
  }
22
+ /** Thrown on HTTP 409 — an identical idempotent request is still in flight. Back off and retry. */
23
+ declare class VesselsConflictError extends Error {
24
+ constructor(message: string);
25
+ }
22
26
  interface VesselsConfig {
23
27
  apiKey: string;
24
28
  baseUrl?: string;
@@ -29,6 +33,8 @@ interface PushResponse {
29
33
  messageId: string;
30
34
  vesselId: string;
31
35
  createdAt: string;
36
+ /** True when this response is a replay of an earlier request with the same idempotency key. */
37
+ replayed: boolean;
32
38
  }
33
39
  interface PushManyResult {
34
40
  ok: true;
@@ -38,6 +44,8 @@ interface PushManyResult {
38
44
  vesselId: string;
39
45
  error?: string;
40
46
  }>;
47
+ /** True when this response is a replay of an earlier request with the same idempotency key. */
48
+ replayed: boolean;
41
49
  }
42
50
  interface PollOptions {
43
51
  since?: string;
@@ -48,18 +56,31 @@ interface VesselContext {
48
56
  id: string;
49
57
  externalId: string | null;
50
58
  title: string | null;
59
+ /** The vessel's type (e.g. "Ticket"). Null for agent-created vessels. */
60
+ type: string | null;
51
61
  metadata: Record<string, unknown>;
52
62
  labels: string[];
53
63
  }
64
+ /** The agent message an interaction was attached to. */
65
+ interface OriginMessage {
66
+ id: string;
67
+ content: string | null;
68
+ /** The interaction object you originally pushed (type, prompt, options, labels…). */
69
+ interaction: Record<string, unknown> | null;
70
+ metadata: Record<string, unknown>;
71
+ createdAt: string;
72
+ }
54
73
  interface InteractionResponseEvent {
55
74
  id: string;
56
75
  type: 'interaction.response';
57
76
  timestamp: string;
58
77
  vessel: VesselContext;
59
78
  messageId: string;
60
- interactionType: string;
79
+ interactionType: _vessels_types.InteractionType;
61
80
  response: Record<string, unknown>;
62
81
  interactionMetadata: Record<string, unknown> | null;
82
+ /** The message that carried the interaction — so you know WHAT was responded to. */
83
+ originMessage: OriginMessage | null;
63
84
  user: {
64
85
  id: string;
65
86
  email: string;
@@ -75,12 +96,39 @@ interface UserMessageEvent {
75
96
  content: string | null;
76
97
  };
77
98
  context: Array<{
78
- source: string;
99
+ source: _vessels_types.Source;
79
100
  content: string | null;
80
101
  createdAt: string;
81
102
  }>;
82
103
  }
83
- type PollEvent = InteractionResponseEvent | UserMessageEvent;
104
+ /**
105
+ * Fired when a human opens a new typed vessel from the app (user-initiated
106
+ * vessels). `message` is the first thing the human typed; `vessel.type` is the
107
+ * vessel's type (e.g. "Ticket").
108
+ */
109
+ interface VesselCreatedEvent {
110
+ id: string;
111
+ type: 'vessel.created';
112
+ timestamp: string;
113
+ vessel: VesselContext;
114
+ message: {
115
+ id: string;
116
+ content: string | null;
117
+ };
118
+ }
119
+ /** A message in a vessel, as returned by getMessages — the human-facing record. */
120
+ interface Message {
121
+ id: string;
122
+ source: _vessels_types.Source;
123
+ content: string | null;
124
+ card: Record<string, unknown> | null;
125
+ interaction: Record<string, unknown> | null;
126
+ attachments: Array<Record<string, unknown>>;
127
+ suggestions: string[];
128
+ metadata: Record<string, unknown>;
129
+ createdAt: string;
130
+ }
131
+ type PollEvent = InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent;
84
132
  interface PollResponse {
85
133
  ok: true;
86
134
  events: PollEvent[];
@@ -92,11 +140,27 @@ declare class Vessels {
92
140
  private _debug;
93
141
  constructor(config: VesselsConfig);
94
142
  private _fetch;
95
- push(payload: _vessels_types.PushPayload): Promise<PushResponse>;
96
- pushMany(payload: _vessels_types.PushManyPayload): Promise<PushManyResult>;
143
+ push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
144
+ pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
97
145
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
98
146
  ok: true;
99
147
  }>;
148
+ /**
149
+ * Read a vessel's message history — the human-facing record, for re-reading
150
+ * the channel (e.g. a stateless or just-restarted worker reconciling state).
151
+ * This is NOT your agent's memory; keep your canonical history in your own
152
+ * system. `vessel` is your own external_id string by default; pass
153
+ * { byUuid: true } if you hold a Vessels UUID. Newest-last, paginate with `before`.
154
+ */
155
+ getMessages(opts: {
156
+ vessel: string;
157
+ byUuid?: boolean;
158
+ limit?: number;
159
+ before?: string;
160
+ }): Promise<{
161
+ messages: Message[];
162
+ hasMore: boolean;
163
+ }>;
100
164
  approval(opts: {
101
165
  prompt: string;
102
166
  approveLabel?: string;
@@ -143,7 +207,7 @@ declare class Vessels {
143
207
  }): _vessels_types.ConfirmPreviewInteraction;
144
208
  poll(options?: PollOptions): Promise<PollResponse>;
145
209
  verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
146
- parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | null>;
210
+ parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | null>;
147
211
  }
148
212
 
149
- export { AgentActivityTypes, type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
213
+ export { AgentActivityTypes, type InteractionResponseEvent, type Message, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, type VesselCreatedEvent, Vessels, VesselsAuthError, type VesselsConfig, VesselsConflictError, VesselsRateLimitError, VesselsValidationError };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _vessels_types from '@vessels/types';
2
- export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyPayload, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema } from '@vessels/types';
2
+ export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyOptions, PushManyPayload, PushOptions, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema, WebhookVesselCreatedPayload, WebhookVesselCreatedPayloadSchema } from '@vessels/types';
3
3
 
4
4
  declare const AgentActivityTypes: {
5
5
  readonly thinking: "thinking";
@@ -19,6 +19,10 @@ declare class VesselsRateLimitError extends Error {
19
19
  retryAfter?: number;
20
20
  constructor(message: string, retryAfter?: number);
21
21
  }
22
+ /** Thrown on HTTP 409 — an identical idempotent request is still in flight. Back off and retry. */
23
+ declare class VesselsConflictError extends Error {
24
+ constructor(message: string);
25
+ }
22
26
  interface VesselsConfig {
23
27
  apiKey: string;
24
28
  baseUrl?: string;
@@ -29,6 +33,8 @@ interface PushResponse {
29
33
  messageId: string;
30
34
  vesselId: string;
31
35
  createdAt: string;
36
+ /** True when this response is a replay of an earlier request with the same idempotency key. */
37
+ replayed: boolean;
32
38
  }
33
39
  interface PushManyResult {
34
40
  ok: true;
@@ -38,6 +44,8 @@ interface PushManyResult {
38
44
  vesselId: string;
39
45
  error?: string;
40
46
  }>;
47
+ /** True when this response is a replay of an earlier request with the same idempotency key. */
48
+ replayed: boolean;
41
49
  }
42
50
  interface PollOptions {
43
51
  since?: string;
@@ -48,18 +56,31 @@ interface VesselContext {
48
56
  id: string;
49
57
  externalId: string | null;
50
58
  title: string | null;
59
+ /** The vessel's type (e.g. "Ticket"). Null for agent-created vessels. */
60
+ type: string | null;
51
61
  metadata: Record<string, unknown>;
52
62
  labels: string[];
53
63
  }
64
+ /** The agent message an interaction was attached to. */
65
+ interface OriginMessage {
66
+ id: string;
67
+ content: string | null;
68
+ /** The interaction object you originally pushed (type, prompt, options, labels…). */
69
+ interaction: Record<string, unknown> | null;
70
+ metadata: Record<string, unknown>;
71
+ createdAt: string;
72
+ }
54
73
  interface InteractionResponseEvent {
55
74
  id: string;
56
75
  type: 'interaction.response';
57
76
  timestamp: string;
58
77
  vessel: VesselContext;
59
78
  messageId: string;
60
- interactionType: string;
79
+ interactionType: _vessels_types.InteractionType;
61
80
  response: Record<string, unknown>;
62
81
  interactionMetadata: Record<string, unknown> | null;
82
+ /** The message that carried the interaction — so you know WHAT was responded to. */
83
+ originMessage: OriginMessage | null;
63
84
  user: {
64
85
  id: string;
65
86
  email: string;
@@ -75,12 +96,39 @@ interface UserMessageEvent {
75
96
  content: string | null;
76
97
  };
77
98
  context: Array<{
78
- source: string;
99
+ source: _vessels_types.Source;
79
100
  content: string | null;
80
101
  createdAt: string;
81
102
  }>;
82
103
  }
83
- type PollEvent = InteractionResponseEvent | UserMessageEvent;
104
+ /**
105
+ * Fired when a human opens a new typed vessel from the app (user-initiated
106
+ * vessels). `message` is the first thing the human typed; `vessel.type` is the
107
+ * vessel's type (e.g. "Ticket").
108
+ */
109
+ interface VesselCreatedEvent {
110
+ id: string;
111
+ type: 'vessel.created';
112
+ timestamp: string;
113
+ vessel: VesselContext;
114
+ message: {
115
+ id: string;
116
+ content: string | null;
117
+ };
118
+ }
119
+ /** A message in a vessel, as returned by getMessages — the human-facing record. */
120
+ interface Message {
121
+ id: string;
122
+ source: _vessels_types.Source;
123
+ content: string | null;
124
+ card: Record<string, unknown> | null;
125
+ interaction: Record<string, unknown> | null;
126
+ attachments: Array<Record<string, unknown>>;
127
+ suggestions: string[];
128
+ metadata: Record<string, unknown>;
129
+ createdAt: string;
130
+ }
131
+ type PollEvent = InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent;
84
132
  interface PollResponse {
85
133
  ok: true;
86
134
  events: PollEvent[];
@@ -92,11 +140,27 @@ declare class Vessels {
92
140
  private _debug;
93
141
  constructor(config: VesselsConfig);
94
142
  private _fetch;
95
- push(payload: _vessels_types.PushPayload): Promise<PushResponse>;
96
- pushMany(payload: _vessels_types.PushManyPayload): Promise<PushManyResult>;
143
+ push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
144
+ pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
97
145
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
98
146
  ok: true;
99
147
  }>;
148
+ /**
149
+ * Read a vessel's message history — the human-facing record, for re-reading
150
+ * the channel (e.g. a stateless or just-restarted worker reconciling state).
151
+ * This is NOT your agent's memory; keep your canonical history in your own
152
+ * system. `vessel` is your own external_id string by default; pass
153
+ * { byUuid: true } if you hold a Vessels UUID. Newest-last, paginate with `before`.
154
+ */
155
+ getMessages(opts: {
156
+ vessel: string;
157
+ byUuid?: boolean;
158
+ limit?: number;
159
+ before?: string;
160
+ }): Promise<{
161
+ messages: Message[];
162
+ hasMore: boolean;
163
+ }>;
100
164
  approval(opts: {
101
165
  prompt: string;
102
166
  approveLabel?: string;
@@ -143,7 +207,7 @@ declare class Vessels {
143
207
  }): _vessels_types.ConfirmPreviewInteraction;
144
208
  poll(options?: PollOptions): Promise<PollResponse>;
145
209
  verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
146
- parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | null>;
210
+ parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | null>;
147
211
  }
148
212
 
149
- export { AgentActivityTypes, type InteractionResponseEvent, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, Vessels, VesselsAuthError, type VesselsConfig, VesselsRateLimitError, VesselsValidationError };
213
+ export { AgentActivityTypes, type InteractionResponseEvent, type Message, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, type VesselCreatedEvent, Vessels, VesselsAuthError, type VesselsConfig, VesselsConflictError, VesselsRateLimitError, VesselsValidationError };
package/dist/index.js CHANGED
@@ -1,6 +1,14 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  // ../types/src/index.ts
4
+ var SourceSchema = z.enum(["agent", "user", "system"]);
5
+ var InteractionTypeSchema = z.enum([
6
+ "approval",
7
+ "choice",
8
+ "checklist",
9
+ "text_input",
10
+ "confirm_preview"
11
+ ]);
4
12
  var ApprovalInteractionSchema = z.object({
5
13
  type: z.literal("approval"),
6
14
  prompt: z.string().min(1),
@@ -150,11 +158,20 @@ var WebhookVesselSchema = z.object({
150
158
  id: z.string(),
151
159
  external_id: z.string().nullable(),
152
160
  title: z.string().nullable(),
161
+ /** The vessel's type (e.g. "Ticket"). Nullable — agent-created vessels have no type. */
162
+ type: z.string().nullable().optional(),
153
163
  metadata: z.record(z.unknown())
154
164
  });
155
165
  var WebhookContextMessageSchema = z.object({
156
- source: z.string(),
166
+ source: SourceSchema,
167
+ content: z.string().nullable(),
168
+ created_at: z.string()
169
+ });
170
+ var WebhookOriginMessageSchema = z.object({
171
+ id: z.string(),
157
172
  content: z.string().nullable(),
173
+ interaction: z.record(z.unknown()).nullable(),
174
+ metadata: z.record(z.unknown()),
158
175
  created_at: z.string()
159
176
  });
160
177
  var WebhookInteractionResponsePayloadSchema = z.object({
@@ -164,10 +181,12 @@ var WebhookInteractionResponsePayloadSchema = z.object({
164
181
  timestamp: z.string(),
165
182
  data: z.object({
166
183
  message_id: z.string(),
167
- interaction_type: z.string(),
184
+ interaction_type: InteractionTypeSchema,
168
185
  response: z.record(z.unknown()),
169
186
  response_id: z.string(),
170
187
  metadata: z.record(z.unknown()).optional(),
188
+ /** The agent message that carried the interaction (prompt, content, metadata). */
189
+ message: WebhookOriginMessageSchema,
171
190
  vessel: WebhookVesselSchema
172
191
  })
173
192
  });
@@ -183,9 +202,24 @@ var WebhookUserMessagePayloadSchema = z.object({
183
202
  context: z.array(WebhookContextMessageSchema)
184
203
  })
185
204
  });
205
+ var WebhookVesselCreatedPayloadSchema = z.object({
206
+ event: z.literal("vessel.created"),
207
+ vessel_id: z.string(),
208
+ workspace_id: z.string(),
209
+ timestamp: z.string(),
210
+ data: z.object({
211
+ vessel: WebhookVesselSchema,
212
+ message: z.object({
213
+ message_id: z.string(),
214
+ content: z.string().nullable(),
215
+ created_at: z.string()
216
+ })
217
+ })
218
+ });
186
219
  var WebhookPayloadSchema = z.discriminatedUnion("event", [
187
220
  WebhookInteractionResponsePayloadSchema,
188
- WebhookUserMessagePayloadSchema
221
+ WebhookUserMessagePayloadSchema,
222
+ WebhookVesselCreatedPayloadSchema
189
223
  ]);
190
224
 
191
225
  // src/index.ts
@@ -218,6 +252,12 @@ var VesselsRateLimitError = class extends Error {
218
252
  this.retryAfter = retryAfter;
219
253
  }
220
254
  };
255
+ var VesselsConflictError = class extends Error {
256
+ constructor(message) {
257
+ super(message);
258
+ this.name = "VesselsConflictError";
259
+ }
260
+ };
221
261
  var Vessels = class {
222
262
  apiKey;
223
263
  baseUrl;
@@ -251,38 +291,45 @@ var Vessels = class {
251
291
  return res;
252
292
  }
253
293
  async push(payload) {
294
+ const { idempotencyKey, ...body } = payload;
254
295
  const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
255
296
  method: "POST",
256
297
  headers: {
257
298
  "Content-Type": "application/json",
258
- "Authorization": `Bearer ${this.apiKey}`
299
+ "Authorization": `Bearer ${this.apiKey}`,
300
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
259
301
  },
260
- body: JSON.stringify(payload)
302
+ body: JSON.stringify(body)
261
303
  });
262
304
  const data = await res.json();
263
305
  if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
264
306
  if (res.status === 429) throw new VesselsRateLimitError(data.error ?? "Rate limited", Number(res.headers.get("retry-after")));
307
+ if (res.status === 409) throw new VesselsConflictError(data.error ?? "Idempotent request in progress");
265
308
  if (res.status === 400) throw new VesselsValidationError(data.error ?? "Validation failed", data.details);
266
309
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
267
310
  return {
268
311
  ok: true,
269
312
  messageId: data.message_id,
270
313
  vesselId: data.vessel_id,
271
- createdAt: data.created_at
314
+ createdAt: data.created_at,
315
+ replayed: res.headers.get("Idempotent-Replayed") === "true"
272
316
  };
273
317
  }
274
318
  async pushMany(payload) {
319
+ const { idempotencyKey, ...body } = payload;
275
320
  const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
276
321
  method: "POST",
277
322
  headers: {
278
323
  "Content-Type": "application/json",
279
- "Authorization": `Bearer ${this.apiKey}`
324
+ "Authorization": `Bearer ${this.apiKey}`,
325
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
280
326
  },
281
- body: JSON.stringify(payload)
327
+ body: JSON.stringify(body)
282
328
  });
283
329
  const data = await res.json();
284
330
  if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
285
331
  if (res.status === 429) throw new VesselsRateLimitError(data.error ?? "Rate limited", Number(res.headers.get("retry-after")));
332
+ if (res.status === 409) throw new VesselsConflictError(data.error ?? "Idempotent request in progress");
286
333
  if (res.status === 400) throw new VesselsValidationError(data.error ?? "Validation failed", data.details);
287
334
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
288
335
  return {
@@ -292,7 +339,8 @@ var Vessels = class {
292
339
  messageId: r.message_id,
293
340
  vesselId: r.vessel_id,
294
341
  ...r.error ? { error: r.error } : {}
295
- }))
342
+ })),
343
+ replayed: res.headers.get("Idempotent-Replayed") === "true"
296
344
  };
297
345
  }
298
346
  async editMessage(messageId, patch) {
@@ -312,6 +360,38 @@ var Vessels = class {
312
360
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
313
361
  return { ok: true };
314
362
  }
363
+ /**
364
+ * Read a vessel's message history — the human-facing record, for re-reading
365
+ * the channel (e.g. a stateless or just-restarted worker reconciling state).
366
+ * This is NOT your agent's memory; keep your canonical history in your own
367
+ * system. `vessel` is your own external_id string by default; pass
368
+ * { byUuid: true } if you hold a Vessels UUID. Newest-last, paginate with `before`.
369
+ */
370
+ async getMessages(opts) {
371
+ const params = new URLSearchParams();
372
+ if (opts.limit != null) params.set("limit", String(opts.limit));
373
+ if (opts.before) params.set("before", opts.before);
374
+ const path = opts.byUuid ? `/api/v1/vessels/${encodeURIComponent(opts.vessel)}/messages` : `/api/v1/vessels/by-external/${encodeURIComponent(opts.vessel)}/messages`;
375
+ const res = await this._fetch(`${this.baseUrl}${path}?${params}`, {
376
+ headers: { "Authorization": `Bearer ${this.apiKey}` }
377
+ });
378
+ const data = await res.json();
379
+ if (res.status === 401) throw new VesselsAuthError(data.error ?? "Unauthorized");
380
+ if (res.status === 404) throw new Error(data.error ?? "Vessel not found");
381
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
382
+ const messages = (data.messages ?? []).map((m) => ({
383
+ id: m.id,
384
+ source: m.source,
385
+ content: m.content ?? null,
386
+ card: m.card ?? null,
387
+ interaction: m.interaction ?? null,
388
+ attachments: m.attachments ?? [],
389
+ suggestions: m.suggestions ?? [],
390
+ metadata: m.metadata ?? {},
391
+ createdAt: m.created_at
392
+ }));
393
+ return { messages, hasMore: data.has_more ?? false };
394
+ }
315
395
  // Interaction helpers
316
396
  approval(opts) {
317
397
  return { type: "approval", ...opts };
@@ -345,9 +425,19 @@ var Vessels = class {
345
425
  id: e.vessel?.id,
346
426
  externalId: e.vessel?.external_id ?? null,
347
427
  title: e.vessel?.title ?? null,
428
+ type: e.vessel?.type ?? null,
348
429
  metadata: e.vessel?.metadata ?? {},
349
430
  labels: e.vessel?.labels ?? []
350
431
  };
432
+ if (e.type === "vessel.created") {
433
+ return {
434
+ id: e.id,
435
+ type: "vessel.created",
436
+ timestamp: e.timestamp,
437
+ vessel,
438
+ message: { id: e.message?.id, content: e.message?.content ?? null }
439
+ };
440
+ }
351
441
  if (e.type === "interaction.response") {
352
442
  return {
353
443
  id: e.id,
@@ -358,6 +448,13 @@ var Vessels = class {
358
448
  interactionType: e.interaction_type,
359
449
  response: e.response,
360
450
  interactionMetadata: e.interaction_metadata ?? null,
451
+ originMessage: e.message ? {
452
+ id: e.message.id,
453
+ content: e.message.content ?? null,
454
+ interaction: e.message.interaction ?? null,
455
+ metadata: e.message.metadata ?? {},
456
+ createdAt: e.message.created_at
457
+ } : null,
361
458
  user: e.user ?? null
362
459
  };
363
460
  }
@@ -412,9 +509,22 @@ var Vessels = class {
412
509
  id: v.id,
413
510
  externalId: v.external_id ?? null,
414
511
  title: v.title ?? null,
512
+ type: v.type ?? null,
415
513
  metadata: v.metadata ?? {},
416
514
  labels: v.labels ?? []
417
515
  };
516
+ if (raw.event === "vessel.created") {
517
+ return {
518
+ id: raw.vessel_id,
519
+ type: "vessel.created",
520
+ timestamp: raw.timestamp,
521
+ vessel,
522
+ message: {
523
+ id: raw.data.message?.message_id,
524
+ content: raw.data.message?.content ?? null
525
+ }
526
+ };
527
+ }
418
528
  if (raw.event === "interaction.response") {
419
529
  return {
420
530
  id: raw.data.response_id,
@@ -425,6 +535,13 @@ var Vessels = class {
425
535
  interactionType: raw.data.interaction_type,
426
536
  response: raw.data.response,
427
537
  interactionMetadata: raw.data.metadata ?? null,
538
+ originMessage: raw.data.message ? {
539
+ id: raw.data.message.id,
540
+ content: raw.data.message.content ?? null,
541
+ interaction: raw.data.message.interaction ?? null,
542
+ metadata: raw.data.message.metadata ?? {},
543
+ createdAt: raw.data.message.created_at
544
+ } : null,
428
545
  user: raw.data.user ?? null
429
546
  };
430
547
  }
@@ -449,4 +566,4 @@ var Vessels = class {
449
566
  }
450
567
  };
451
568
 
452
- export { AgentActivityTypes, Vessels, VesselsAuthError, VesselsRateLimitError, VesselsValidationError, WebhookInteractionResponsePayloadSchema, WebhookPayloadSchema, WebhookUserMessagePayloadSchema };
569
+ export { AgentActivityTypes, Vessels, VesselsAuthError, VesselsConflictError, VesselsRateLimitError, VesselsValidationError, WebhookInteractionResponsePayloadSchema, WebhookPayloadSchema, WebhookUserMessagePayloadSchema, WebhookVesselCreatedPayloadSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vessels-sdk",
3
- "version": "0.5.1",
3
+ "version": "0.8.0",
4
4
  "description": "Let your agent reach you. Official Vessels SDK.",
5
5
  "type": "module",
6
6
  "exports": {