@ziggs-ai/api-client 0.1.4 → 0.1.5

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.
Files changed (39) hide show
  1. package/dist/ConnectionManager.d.ts +1 -0
  2. package/dist/ConnectionManager.js +18 -4
  3. package/dist/http/AgreementClient.js +33 -13
  4. package/dist/http/ChatClient.d.ts +20 -0
  5. package/dist/http/ChatClient.js +39 -4
  6. package/dist/http/ContextDiscoveryClient.d.ts +23 -0
  7. package/dist/http/ContextDiscoveryClient.js +35 -0
  8. package/dist/http/ContextReadClient.d.ts +33 -0
  9. package/dist/http/ContextReadClient.js +54 -0
  10. package/dist/http/MessagesClient.d.ts +6 -2
  11. package/dist/http/MessagesClient.js +14 -6
  12. package/dist/http/TaskClient.js +12 -3
  13. package/dist/http/index.d.ts +4 -0
  14. package/dist/http/index.js +2 -0
  15. package/dist/types.d.ts +17 -7
  16. package/dist/types.js +1 -1
  17. package/dist/websocket/ControlSocket.js +2 -1
  18. package/dist/websocket/WebSocketClient.js +23 -7
  19. package/package.json +15 -9
  20. package/src/ConnectionManager.ts +172 -0
  21. package/src/http/AgentSearchClient.ts +115 -0
  22. package/src/http/AgreementClient.ts +721 -0
  23. package/src/http/ArtifactsClient.ts +133 -0
  24. package/src/http/ChatClient.ts +147 -0
  25. package/src/http/ContextDiscoveryClient.ts +52 -0
  26. package/src/http/ContextReadClient.ts +83 -0
  27. package/src/http/MarketplaceClient.ts +94 -0
  28. package/src/http/MessagesClient.ts +71 -0
  29. package/src/http/ScopeClient.ts +64 -0
  30. package/src/http/TaskClient.ts +450 -0
  31. package/src/http/TelemetryClient.ts +57 -0
  32. package/src/http/index.ts +26 -0
  33. package/src/index.ts +27 -0
  34. package/src/shared/runtimeLog.ts +68 -0
  35. package/src/types.ts +158 -0
  36. package/src/utils/urlUtils.ts +9 -0
  37. package/src/websocket/ControlSocket.ts +51 -0
  38. package/src/websocket/WebSocketClient.ts +315 -0
  39. package/src/websocket/index.ts +1 -0
@@ -59,7 +59,13 @@ export class WebSocketClient {
59
59
  const description = reasonDescriptions[reason] || 'Unknown disconnect reason';
60
60
  runtimeLog.debug(this.label, `Disconnected: ${reason} — ${description}`);
61
61
  });
62
- this.socket.on('messages', async (payload) => {
62
+ // ZIG-207: subscribe to the canonical event name. Backend dual-emits
63
+ // `messages` + `chat:message:new` with byte-identical payloads, so
64
+ // listening to ONE of the two is exactly correct — listening to both
65
+ // would double-invoke handleIncomingMessage(). We pick the canonical
66
+ // namespaced name so once the sunset PR drops the legacy `messages`
67
+ // emit, no client change is needed.
68
+ this.socket.on('chat:message:new', async (payload) => {
63
69
  try {
64
70
  await this.handleIncomingMessage(payload);
65
71
  }
@@ -89,9 +95,11 @@ export class WebSocketClient {
89
95
  return Promise.resolve(this);
90
96
  return new Promise((resolve, reject) => {
91
97
  this._connect();
98
+ // Only reject on timeout — not on the first connect_error. socket.io
99
+ // retries automatically (reconnectionAttempts: Infinity), so a single
100
+ // network hiccup or a briefly-down backend shouldn't crash the caller.
92
101
  const timer = setTimeout(() => reject(new Error(`[${this.label}] Connection timeout after ${timeout}ms`)), timeout);
93
102
  this.socket.once('connect', () => { clearTimeout(timer); resolve(this); });
94
- this.socket.once('connect_error', (err) => { clearTimeout(timer); reject(err); });
95
103
  });
96
104
  }
97
105
  disconnect() {
@@ -123,7 +131,11 @@ export class WebSocketClient {
123
131
  content_type: options.content_type || options.contentType || 'text',
124
132
  };
125
133
  const messagePreview = content.length > 100 ? content.substring(0, 100) + '...' : content;
126
- const event = options.partial ? 'chat:chunk' : 'chat';
134
+ // ZIG-207: emit the canonical namespaced inbound event. Backend
135
+ // registers `chat:message:send` as a one-line alias on `handleChat`,
136
+ // so this is byte-identical to the legacy `chat` emit. Streaming
137
+ // chunks keep their existing `chat:chunk` name (already namespaced).
138
+ const event = options.partial ? 'chat:chunk' : 'chat:message:send';
127
139
  runtimeLog.debug(this.label, `Sending message (${event}) - chatId: ${chatId}, receiverId: ${receiverId}\n Message: ${messagePreview}`);
128
140
  this.socket.emit(event, message);
129
141
  }
@@ -157,7 +169,9 @@ export class WebSocketClient {
157
169
  const receiver = p['receiver'];
158
170
  const task = p['task'] ?? null;
159
171
  const agreement = p['agreement'] ?? null;
160
- runtimeLog.info(this.label, `[wire-debug] incoming chatId=${chatId} sender=${senderId} content_type=${p['content_type'] ?? p['contentType'] ?? '<none>'} entryType=${p['entryType'] ?? '<none>'} task?=${task ? `taskId=${task['taskId']} state=${task['state']}` : 'no'} agreement?=${agreement ? `agreementId=${agreement['agreementId']} status=${agreement['status']}` : 'no'}`);
172
+ const operation = p['operation'] ?? null;
173
+ const agreementId = p['agreementId'] ?? null;
174
+ runtimeLog.info(this.label, `[wire-debug] incoming chatId=${chatId} sender=${senderId} content_type=${p['content_type'] ?? p['contentType'] ?? '<none>'} entryType=${p['entryType'] ?? '<none>'} task?=${task ? `taskId=${task['taskId']} state=${task['state']}` : 'no'} agreement?=${agreement ? `agreementId=${agreement['agreementId']} status=${agreement['status']}` : 'no'} operation?=${operation ?? 'no'}`);
161
175
  const metadata = {
162
176
  chatId,
163
177
  userId: senderId,
@@ -171,6 +185,8 @@ export class WebSocketClient {
171
185
  taskId: task?.['taskId'] ?? null,
172
186
  task,
173
187
  agreement,
188
+ operation,
189
+ agreementId,
174
190
  };
175
191
  await this.messageHandler(p['text'], metadata);
176
192
  }
@@ -188,7 +204,8 @@ export class WebSocketClient {
188
204
  reconnection: true,
189
205
  reconnectionAttempts: Infinity,
190
206
  reconnectionDelay: 1000,
191
- reconnectionDelayMax: 5000,
207
+ reconnectionDelayMax: 30000,
208
+ randomizationFactor: 0.5,
192
209
  timeout: 20000,
193
210
  autoConnect: true,
194
211
  forceNew: true,
@@ -204,8 +221,7 @@ export class WebSocketClient {
204
221
  const bearer = `Bearer ${this.operatorKey}`;
205
222
  options.auth = { token: bearer };
206
223
  options.extraHeaders = { Authorization: bearer };
207
- // agentId goes in query so the gateway can route; token stays out of the URL
208
- // to avoid leaking the long-lived operator key into proxy access logs.
224
+ // agentId goes in query for fleet keys; agent-scoped keys omit it (ZIG-279).
209
225
  if (this.agentId)
210
226
  options.query = { agentId: this.agentId };
211
227
  }
package/package.json CHANGED
@@ -1,21 +1,20 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
6
+ "main": "src/index.ts",
8
7
  "exports": {
9
- ".": "./dist/index.js",
10
- "./http": "./dist/http/index.js",
11
- "./websocket": "./dist/websocket/index.js"
8
+ ".": "./src/index.ts",
9
+ "./http": "./src/http/index.ts",
10
+ "./websocket": "./src/websocket/index.ts"
12
11
  },
13
12
  "engines": {
14
13
  "node": ">=18"
15
14
  },
16
15
  "scripts": {
17
16
  "build": "tsc -p tsconfig.json",
18
- "prepare": "npm run build",
17
+ "prepack": "npm run build",
19
18
  "test": "node --import tsx/esm --test --test-reporter=spec test/*.test.ts",
20
19
  "test:watch": "node --import tsx/esm --test --watch test/*.test.ts"
21
20
  },
@@ -25,9 +24,16 @@
25
24
  },
26
25
  "keywords": ["ziggs", "api", "client", "websocket"],
27
26
  "license": "MIT",
28
- "files": ["dist", "README.md"],
27
+ "files": ["dist", "src", "README.md"],
29
28
  "publishConfig": {
30
29
  "access": "public",
31
- "registry": "https://registry.npmjs.org/"
30
+ "registry": "https://registry.npmjs.org/",
31
+ "main": "dist/index.js",
32
+ "types": "dist/index.d.ts",
33
+ "exports": {
34
+ ".": "./dist/index.js",
35
+ "./http": "./dist/http/index.js",
36
+ "./websocket": "./dist/websocket/index.js"
37
+ }
32
38
  }
33
39
  }
@@ -0,0 +1,172 @@
1
+ import { createControlSocket, type ControlSocketOptions, type ControlSocketHandle } from './websocket/ControlSocket.js';
2
+ import { runtimeLog } from './shared/runtimeLog.js';
3
+
4
+ type OpenFn = () => Promise<unknown>;
5
+ type CloseFn = (handle: unknown) => Promise<void>;
6
+
7
+ interface Entry {
8
+ openFn: OpenFn;
9
+ closeFn: CloseFn;
10
+ }
11
+
12
+ interface ActiveEntry {
13
+ handle: unknown;
14
+ timer: ReturnType<typeof setTimeout> | null;
15
+ lastActive: number;
16
+ }
17
+
18
+ export interface ConnectionManagerMeta {
19
+ domain?: string;
20
+ expertise?: string[];
21
+ tags?: string[];
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface ConnectionManagerOptions {
26
+ maxActive?: number;
27
+ idleTimeoutMs?: number;
28
+ control?: Pick<ControlSocketOptions, 'wsUrl' | 'operatorKey'>;
29
+ }
30
+
31
+ export interface QueryFilter {
32
+ domain?: string;
33
+ expertise?: string[];
34
+ tags?: string[];
35
+ }
36
+
37
+ export class ConnectionManager {
38
+ private maxActive: number;
39
+ private idleTimeoutMs: number;
40
+ private _controlOpts: Pick<ControlSocketOptions, 'wsUrl' | 'operatorKey'> | null;
41
+ private _controlHandle: ControlSocketHandle | null;
42
+ private _entries: Map<string, Entry>;
43
+ private _active: Map<string, ActiveEntry>;
44
+ private _meta: Map<string, ConnectionManagerMeta>;
45
+ private _waking: Map<string, Promise<unknown>>;
46
+
47
+ constructor({ maxActive = 50, idleTimeoutMs = 60_000, control }: ConnectionManagerOptions = {}) {
48
+ this.maxActive = maxActive;
49
+ this.idleTimeoutMs = idleTimeoutMs;
50
+ this._controlOpts = control ?? null;
51
+ this._controlHandle = null;
52
+ this._entries = new Map();
53
+ this._active = new Map();
54
+ this._meta = new Map();
55
+ this._waking = new Map();
56
+ }
57
+
58
+ register(id: string, openFn: OpenFn, closeFn: CloseFn, meta?: ConnectionManagerMeta): void {
59
+ if (!id) throw new Error('[ConnectionManager] id is required');
60
+ if (typeof openFn !== 'function') throw new Error('[ConnectionManager] openFn must be a function');
61
+ if (typeof closeFn !== 'function') throw new Error('[ConnectionManager] closeFn must be a function');
62
+ this._entries.set(id, { openFn, closeFn });
63
+ if (meta) this._meta.set(id, meta);
64
+ }
65
+
66
+ start(): void {
67
+ if (this._controlHandle || !this._controlOpts) return;
68
+ this._controlHandle = createControlSocket({
69
+ ...this._controlOpts,
70
+ agentIds: () => this.list(),
71
+ onWake: (id) =>
72
+ this.wake(id).catch(err =>
73
+ runtimeLog.warn('ConnectionManager', `wake("${id}") failed: ${(err as Error).message}`),
74
+ ),
75
+ });
76
+ }
77
+
78
+ async stop(): Promise<void> {
79
+ this._controlHandle?.close();
80
+ this._controlHandle = null;
81
+ await this.sleepAll();
82
+ }
83
+
84
+ async wake(id: string): Promise<unknown> {
85
+ const existing = this._active.get(id);
86
+ if (existing) {
87
+ this._resetTimer(id);
88
+ return existing.handle;
89
+ }
90
+
91
+ const pending = this._waking.get(id);
92
+ if (pending) return pending;
93
+
94
+ const entry = this._entries.get(id);
95
+ if (!entry) throw new Error(`[ConnectionManager] unknown id: "${id}"`);
96
+
97
+ if (this._active.size >= this.maxActive) await this._evictLRU();
98
+
99
+ const wakePromise = (async () => {
100
+ try {
101
+ const handle = await entry.openFn();
102
+ this._active.set(id, { handle, timer: null, lastActive: Date.now() });
103
+ this._scheduleIdle(id);
104
+ return handle;
105
+ } finally {
106
+ this._waking.delete(id);
107
+ }
108
+ })();
109
+
110
+ this._waking.set(id, wakePromise);
111
+ return wakePromise;
112
+ }
113
+
114
+ async sleep(id: string): Promise<void> {
115
+ const entry = this._active.get(id);
116
+ if (!entry) return;
117
+ clearTimeout(entry.timer!);
118
+ this._active.delete(id);
119
+ const reg = this._entries.get(id);
120
+ if (reg) await reg.closeFn(entry.handle);
121
+ }
122
+
123
+ async sleepAll(): Promise<void> {
124
+ await Promise.all([...this._active.keys()].map(id => this.sleep(id)));
125
+ }
126
+
127
+ touch(id: string): void { this._resetTimer(id); }
128
+
129
+ list(): string[] { return [...this._entries.keys()]; }
130
+ listActive(): string[] { return [...this._active.keys()]; }
131
+ get size(): number { return this._entries.size; }
132
+
133
+ query({ domain, expertise, tags }: QueryFilter = {}): string[] {
134
+ const results: string[] = [];
135
+ for (const [id, meta] of this._meta) {
136
+ if (domain && meta.domain !== domain) continue;
137
+ if (expertise?.length && !expertise.some(e => meta.expertise?.includes(e))) continue;
138
+ if (tags?.length && !tags.some(t => meta.tags?.includes(t))) continue;
139
+ results.push(id);
140
+ }
141
+ return results;
142
+ }
143
+
144
+ getMeta(id: string): ConnectionManagerMeta | undefined { return this._meta.get(id); }
145
+
146
+ private _scheduleIdle(id: string): void {
147
+ const entry = this._active.get(id);
148
+ if (!entry) return;
149
+ clearTimeout(entry.timer!);
150
+ entry.timer = setTimeout(() => {
151
+ this.sleep(id).catch(err =>
152
+ runtimeLog.warn('ConnectionManager', `idle sleep("${id}") failed: ${(err as Error).message}`),
153
+ );
154
+ }, this.idleTimeoutMs);
155
+ }
156
+
157
+ private _resetTimer(id: string): void {
158
+ const entry = this._active.get(id);
159
+ if (!entry) return;
160
+ entry.lastActive = Date.now();
161
+ this._scheduleIdle(id);
162
+ }
163
+
164
+ private async _evictLRU(): Promise<void> {
165
+ let oldest: string | null = null;
166
+ for (const [id, entry] of this._active) {
167
+ const oldestEntry = oldest ? this._active.get(oldest) : null;
168
+ if (!oldest || !oldestEntry || entry.lastActive < oldestEntry.lastActive) oldest = id;
169
+ }
170
+ if (oldest) await this.sleep(oldest);
171
+ }
172
+ }
@@ -0,0 +1,115 @@
1
+ import 'dotenv/config';
2
+ import { runtimeLog } from '../shared/runtimeLog.js';
3
+ import { getBackendUrl } from '../utils/urlUtils.js';
4
+
5
+ export interface AgentSearchOptions {
6
+ limit?: number;
7
+ minScore?: number;
8
+ }
9
+
10
+ export interface AgentSearchResult {
11
+ success: boolean;
12
+ agents?: AgentProfile[];
13
+ error?: string;
14
+ message?: string;
15
+ }
16
+
17
+ export interface AgentProfile {
18
+ agentId: string;
19
+ name: string;
20
+ description?: string;
21
+ tags?: string[];
22
+ matchScore?: number;
23
+ reliability?: {
24
+ sampleSize: number;
25
+ band: string;
26
+ medianResponseMs: number;
27
+ lastActivityDaysAgo: number;
28
+ };
29
+ category?: string;
30
+ version?: string;
31
+ recentTaskSamples?: unknown[];
32
+ }
33
+
34
+ export class AgentSearchClient {
35
+ private readonly operatorKey: string;
36
+ private readonly agentId: string;
37
+ private readonly baseUrl: string;
38
+
39
+ constructor(operatorKey: string, agentId: string) {
40
+ if (!operatorKey) throw new Error('AgentSearchClient: operatorKey is required');
41
+ if (!agentId) throw new Error('AgentSearchClient: agentId is required (operator-token impersonation)');
42
+ this.operatorKey = operatorKey;
43
+ this.agentId = agentId;
44
+ this.baseUrl = getBackendUrl();
45
+ }
46
+
47
+ async searchAgents(query: string, options: AgentSearchOptions = {}): Promise<AgentSearchResult> {
48
+ if (!query || typeof query !== 'string') {
49
+ return { success: false, error: 'query is required' };
50
+ }
51
+ const params = new URLSearchParams({ q: query });
52
+ if (typeof options.limit === 'number') params.set('limit', String(options.limit));
53
+ if (typeof options.minScore === 'number') params.set('minScore', String(options.minScore));
54
+
55
+ const url = `${this.baseUrl}/agent-api/v1/agents/search?${params}`;
56
+ try {
57
+ const response = await fetch(url, { method: 'GET', headers: this._buildHeaders() });
58
+ if (!response.ok) {
59
+ const errorText = await response.text().catch(() => '');
60
+ runtimeLog.warn(
61
+ 'AgentSearchClient',
62
+ `⚠️ searchAgents failed agent=${this.agentId} ${response.status} ${response.statusText} url=${url} body=${errorText.slice(0, 200)}`,
63
+ );
64
+ return { success: false, error: `Search failed: ${response.status}`, message: errorText };
65
+ }
66
+ const { results = [] } = await response.json() as { results: AgentProfile[] };
67
+ return { success: true, agents: results };
68
+ } catch (error) {
69
+ runtimeLog.warn(
70
+ 'AgentSearchClient',
71
+ `⚠️ searchAgents error agent=${this.agentId} url=${url} message=${(error as Error).message}`,
72
+ );
73
+ return { success: false, error: (error as Error).message || 'Failed to search agents' };
74
+ }
75
+ }
76
+
77
+ async getAgentById(agentId: string): Promise<AgentSearchResult & Partial<AgentProfile>> {
78
+ if (!agentId) return { success: false, error: 'agentId is required' };
79
+ const url = `${this.baseUrl}/agent-api/v1/agents/${encodeURIComponent(agentId)}`;
80
+ try {
81
+ const response = await fetch(url, { method: 'GET', headers: this._buildHeaders() });
82
+ if (response.status === 404) {
83
+ runtimeLog.warn(
84
+ 'AgentSearchClient',
85
+ `⚠️ getAgentById 404 agent=${this.agentId} target=${agentId} url=${url}`,
86
+ );
87
+ return { success: false, error: 'agent not found' };
88
+ }
89
+ if (!response.ok) {
90
+ const body = await response.text().catch(() => '');
91
+ runtimeLog.warn(
92
+ 'AgentSearchClient',
93
+ `⚠️ getAgentById failed agent=${this.agentId} target=${agentId} ${response.status} ${response.statusText} body=${body.slice(0, 200)}`,
94
+ );
95
+ return { success: false, error: `Failed to get agent details: ${response.status}` };
96
+ }
97
+ const agent = await response.json() as AgentProfile;
98
+ return { success: true, ...agent };
99
+ } catch (error) {
100
+ runtimeLog.warn(
101
+ 'AgentSearchClient',
102
+ `⚠️ getAgentById error agent=${this.agentId} target=${agentId} url=${url} message=${(error as Error).message}`,
103
+ );
104
+ return { success: false, error: (error as Error).message || 'Failed to get agent details' };
105
+ }
106
+ }
107
+
108
+ private _buildHeaders(): Record<string, string> {
109
+ return {
110
+ 'Content-Type': 'application/json',
111
+ Authorization: `Bearer ${this.operatorKey}`,
112
+ 'X-Agent-Id': this.agentId,
113
+ };
114
+ }
115
+ }