@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.
- package/dist/ConnectionManager.d.ts +1 -0
- package/dist/ConnectionManager.js +18 -4
- package/dist/http/AgreementClient.js +33 -13
- package/dist/http/ChatClient.d.ts +20 -0
- package/dist/http/ChatClient.js +39 -4
- package/dist/http/ContextDiscoveryClient.d.ts +23 -0
- package/dist/http/ContextDiscoveryClient.js +35 -0
- package/dist/http/ContextReadClient.d.ts +33 -0
- package/dist/http/ContextReadClient.js +54 -0
- package/dist/http/MessagesClient.d.ts +6 -2
- package/dist/http/MessagesClient.js +14 -6
- package/dist/http/TaskClient.js +12 -3
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +2 -0
- package/dist/types.d.ts +17 -7
- package/dist/types.js +1 -1
- package/dist/websocket/ControlSocket.js +2 -1
- package/dist/websocket/WebSocketClient.js +23 -7
- package/package.json +15 -9
- package/src/ConnectionManager.ts +172 -0
- package/src/http/AgentSearchClient.ts +115 -0
- package/src/http/AgreementClient.ts +721 -0
- package/src/http/ArtifactsClient.ts +133 -0
- package/src/http/ChatClient.ts +147 -0
- package/src/http/ContextDiscoveryClient.ts +52 -0
- package/src/http/ContextReadClient.ts +83 -0
- package/src/http/MarketplaceClient.ts +94 -0
- package/src/http/MessagesClient.ts +71 -0
- package/src/http/ScopeClient.ts +64 -0
- package/src/http/TaskClient.ts +450 -0
- package/src/http/TelemetryClient.ts +57 -0
- package/src/http/index.ts +26 -0
- package/src/index.ts +27 -0
- package/src/shared/runtimeLog.ts +68 -0
- package/src/types.ts +158 -0
- package/src/utils/urlUtils.ts +9 -0
- package/src/websocket/ControlSocket.ts +51 -0
- package/src/websocket/WebSocketClient.ts +315 -0
- package/src/websocket/index.ts +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
|
+
|
|
5
|
+
export type ArtifactVisibility = 'chat' | 'agent-private';
|
|
6
|
+
|
|
7
|
+
export interface ListArtifactsOptions {
|
|
8
|
+
after?: string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ListArtifactsQuery {
|
|
13
|
+
chatId?: string;
|
|
14
|
+
agreementId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ListArtifactsResult {
|
|
18
|
+
artifacts: unknown[];
|
|
19
|
+
latestSequence: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WriteArtifactInput {
|
|
23
|
+
text: string;
|
|
24
|
+
content_type?: string;
|
|
25
|
+
visibility?: ArtifactVisibility;
|
|
26
|
+
/** When the artifact is associated with a chat. */
|
|
27
|
+
chatId?: string;
|
|
28
|
+
/** When the artifact is associated with an agreement (and optionally a task). */
|
|
29
|
+
agreementId?: string;
|
|
30
|
+
taskId?: string;
|
|
31
|
+
service?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Replaces `ContextReader`'s artifact reads + `ContextWriter`'s breadcrumb
|
|
36
|
+
* writes. `visibility: 'agent-private'` is the new home for agent thoughts —
|
|
37
|
+
* they persist, are searchable, but are not visible to other chat parties.
|
|
38
|
+
*/
|
|
39
|
+
export class ArtifactsClient {
|
|
40
|
+
private readonly operatorKey: string;
|
|
41
|
+
private readonly agentId: string;
|
|
42
|
+
|
|
43
|
+
constructor(operatorKey: string, agentId: string) {
|
|
44
|
+
if (!operatorKey) throw new Error('ArtifactsClient: operatorKey is required');
|
|
45
|
+
if (!agentId)
|
|
46
|
+
throw new Error(
|
|
47
|
+
'ArtifactsClient: agentId is required (operator-token impersonation)',
|
|
48
|
+
);
|
|
49
|
+
this.operatorKey = operatorKey;
|
|
50
|
+
this.agentId = agentId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async list(
|
|
54
|
+
q: ListArtifactsQuery,
|
|
55
|
+
opts: ListArtifactsOptions = {},
|
|
56
|
+
): Promise<ListArtifactsResult> {
|
|
57
|
+
if ((q.chatId && q.agreementId) || (!q.chatId && !q.agreementId)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'ArtifactsClient.list: pass exactly one of chatId or agreementId',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const url = new URL(`${getBackendUrl()}/artifacts`);
|
|
63
|
+
if (q.chatId) url.searchParams.set('chatId', q.chatId);
|
|
64
|
+
if (q.agreementId) url.searchParams.set('agreementId', q.agreementId);
|
|
65
|
+
if (opts.after) url.searchParams.set('after', opts.after);
|
|
66
|
+
if (opts.limit != null) url.searchParams.set('limit', String(opts.limit));
|
|
67
|
+
const res = await fetch(url.toString(), { headers: this._headers() });
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const body = await res.text().catch(() => '');
|
|
70
|
+
throw new Error(
|
|
71
|
+
`ArtifactsClient.list ${res.status} ${res.statusText} ${body.slice(0, 200)}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return (await res.json()) as ListArtifactsResult;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Record an agent thought as an `agent-private` artifact. Replaces the
|
|
79
|
+
* `ContextWriter.recordEvent({ kind: 'thought' })` hack.
|
|
80
|
+
*
|
|
81
|
+
* Note: the artifact write endpoint lives in the existing chat/agreement
|
|
82
|
+
* write paths; this helper is the SDK contract. Until the backend write
|
|
83
|
+
* endpoint is updated to accept `visibility`, this falls back to logging
|
|
84
|
+
* locally (operators can still wire their own sink).
|
|
85
|
+
*/
|
|
86
|
+
async recordThought(chatId: string, text: string): Promise<void> {
|
|
87
|
+
return this.write({
|
|
88
|
+
chatId,
|
|
89
|
+
text,
|
|
90
|
+
content_type: 'thought',
|
|
91
|
+
visibility: 'agent-private',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async write(input: WriteArtifactInput): Promise<void> {
|
|
96
|
+
if (!input.text || !input.text.trim()) return;
|
|
97
|
+
if ((input.chatId && input.agreementId) || (!input.chatId && !input.agreementId)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'ArtifactsClient.write: pass exactly one of chatId or agreementId',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const url = `${getBackendUrl()}/artifacts`;
|
|
103
|
+
const res = await fetch(url, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: this._headers(),
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
text: input.text.trim(),
|
|
108
|
+
content_type: input.content_type ?? 'text',
|
|
109
|
+
visibility: input.visibility ?? 'chat',
|
|
110
|
+
chatId: input.chatId,
|
|
111
|
+
agreementId: input.agreementId,
|
|
112
|
+
taskId: input.taskId,
|
|
113
|
+
service: input.service,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
const body = await res.text().catch(() => '');
|
|
118
|
+
// Soft-fail: artifact writes are breadcrumbs, not load-bearing.
|
|
119
|
+
runtimeLog.warn(
|
|
120
|
+
'ArtifactsClient',
|
|
121
|
+
`⚠️ write failed ${res.status} ${res.statusText} ${body.slice(0, 200)}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private _headers(): Record<string, string> {
|
|
127
|
+
return {
|
|
128
|
+
'content-type': 'application/json',
|
|
129
|
+
Authorization: `Bearer ${this.operatorKey}`,
|
|
130
|
+
'X-Agent-Id': this.agentId,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
4
|
+
import { type Creds, ApiError } from '../types.js';
|
|
5
|
+
|
|
6
|
+
function buildHeaders(creds: Creds): Record<string, string> {
|
|
7
|
+
return {
|
|
8
|
+
'content-type': 'application/json',
|
|
9
|
+
Authorization: `Bearer ${creds.operatorKey}`,
|
|
10
|
+
'X-Agent-Id': creds.agentId,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function assertCreds(creds: Creds | undefined | null, op: string): asserts creds is Creds {
|
|
15
|
+
if (!creds?.operatorKey) throw new Error(`operatorKey is required for ${op}`);
|
|
16
|
+
if (!creds?.agentId) throw new Error(`agentId is required for ${op}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function throwApiError(response: Response, responseBody: string, defaultMessage: string): never {
|
|
20
|
+
let message = defaultMessage;
|
|
21
|
+
if (responseBody) {
|
|
22
|
+
try {
|
|
23
|
+
const d = JSON.parse(responseBody) as Record<string, unknown>;
|
|
24
|
+
message = (d['details'] as string) || (d['error'] as string) || (d['message'] as string) || defaultMessage;
|
|
25
|
+
} catch { message = responseBody || defaultMessage; }
|
|
26
|
+
}
|
|
27
|
+
throw new ApiError(message, response.status, responseBody);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ChatSummary {
|
|
31
|
+
chatId: string;
|
|
32
|
+
userIds?: string[];
|
|
33
|
+
agentIds?: string[];
|
|
34
|
+
createdAt?: string;
|
|
35
|
+
updatedAt?: string;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function openConversation(
|
|
40
|
+
participantId: string,
|
|
41
|
+
creds: Creds,
|
|
42
|
+
): Promise<{ chatId: string }> {
|
|
43
|
+
if (!participantId) throw new Error('participantId is required for openConversation');
|
|
44
|
+
assertCreds(creds, 'open conversation');
|
|
45
|
+
|
|
46
|
+
// Canonical REST: POST /chats with { participantId }, returns { chatId }.
|
|
47
|
+
const res = await fetch(`${getBackendUrl()}/chats`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: buildHeaders(creds),
|
|
50
|
+
body: JSON.stringify({ participantId }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const body = await res.text().catch(() => '');
|
|
55
|
+
throwApiError(res, body, `openConversation failed: ${res.status} ${res.statusText}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
59
|
+
if (!data?.['chatId'] || typeof data['chatId'] !== 'string') {
|
|
60
|
+
throw new Error('Invalid response: expected { chatId } from POST /chats');
|
|
61
|
+
}
|
|
62
|
+
return { chatId: data['chatId'] as string };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SendChatMessageInput {
|
|
66
|
+
chatId: string;
|
|
67
|
+
receiverId: string;
|
|
68
|
+
text: string;
|
|
69
|
+
messageId: string;
|
|
70
|
+
entryType?: string;
|
|
71
|
+
contentType?: string;
|
|
72
|
+
underAgreementId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SendChatMessageResult {
|
|
76
|
+
success: boolean;
|
|
77
|
+
message: string;
|
|
78
|
+
messageId: string;
|
|
79
|
+
chatId: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* POST /chats/:chatId/messages as an impersonated delegate agent.
|
|
84
|
+
* Requires operator token + X-Agent-Id (ZIG-222).
|
|
85
|
+
*/
|
|
86
|
+
export async function sendChatMessage(
|
|
87
|
+
input: SendChatMessageInput,
|
|
88
|
+
creds: Creds,
|
|
89
|
+
): Promise<SendChatMessageResult> {
|
|
90
|
+
assertCreds(creds, 'send chat message');
|
|
91
|
+
const entryType = input.entryType ?? 'message';
|
|
92
|
+
const contentType = input.contentType ?? 'text';
|
|
93
|
+
|
|
94
|
+
const res = await fetch(
|
|
95
|
+
`${getBackendUrl()}/chats/${encodeURIComponent(input.chatId)}/messages`,
|
|
96
|
+
{
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: buildHeaders(creds),
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
chatId: input.chatId,
|
|
101
|
+
messageId: input.messageId,
|
|
102
|
+
text: input.text,
|
|
103
|
+
entryType,
|
|
104
|
+
contentType,
|
|
105
|
+
receiver: input.receiverId,
|
|
106
|
+
underAgreementId: input.underAgreementId,
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const body = await res.text().catch(() => '');
|
|
113
|
+
throwApiError(res, body, `sendChatMessage failed: ${res.status} ${res.statusText}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data = (await res.json().catch(() => null)) as Record<string, unknown> | null;
|
|
117
|
+
return {
|
|
118
|
+
success: Boolean(data?.['success']),
|
|
119
|
+
message: String(data?.['message'] ?? 'ok'),
|
|
120
|
+
messageId: String(data?.['messageId'] ?? input.messageId),
|
|
121
|
+
chatId: String(data?.['chatId'] ?? input.chatId),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function listMyChats(creds: Creds): Promise<ChatSummary[]> {
|
|
126
|
+
assertCreds(creds, 'list my chats');
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Canonical REST: GET /chats/mine.
|
|
130
|
+
const res = await fetch(`${getBackendUrl()}/chats/mine`, {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
headers: buildHeaders(creds),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const body = await res.text().catch(() => '');
|
|
137
|
+
runtimeLog.warn('ChatClient', `⚠️ listMyChats failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
142
|
+
return Array.isArray(data?.['chats']) ? (data['chats'] as ChatSummary[]) : [];
|
|
143
|
+
} catch (e) {
|
|
144
|
+
runtimeLog.warn('ChatClient', `⚠️ listMyChats failed: ${(e as Error).message}`);
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
|
|
4
|
+
export interface ContextReachDescriptor {
|
|
5
|
+
grantId: string;
|
|
6
|
+
scope: { kind: 'chat' | 'agreement' | 'org'; id: string };
|
|
7
|
+
temporal: 'from-now' | 'from-start';
|
|
8
|
+
watermarkAt: string;
|
|
9
|
+
expiresAt: string | null;
|
|
10
|
+
parentGrantId: string | null;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ZIG-412 discovery — scope descriptors only, no content.
|
|
16
|
+
*/
|
|
17
|
+
export class ContextDiscoveryClient {
|
|
18
|
+
private readonly operatorKey: string;
|
|
19
|
+
private readonly agentId: string;
|
|
20
|
+
private readonly baseUrl: string;
|
|
21
|
+
|
|
22
|
+
constructor(operatorKey: string, agentId: string, baseUrl?: string) {
|
|
23
|
+
if (!operatorKey) {
|
|
24
|
+
throw new Error('ContextDiscoveryClient: operatorKey is required');
|
|
25
|
+
}
|
|
26
|
+
if (!agentId) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'ContextDiscoveryClient: agentId is required (operator-token impersonation)',
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
this.operatorKey = operatorKey;
|
|
32
|
+
this.agentId = agentId;
|
|
33
|
+
this.baseUrl = baseUrl || getBackendUrl();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async discover(): Promise<ContextReachDescriptor[]> {
|
|
37
|
+
const res = await fetch(`${this.baseUrl}/context/discovery`, {
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${this.operatorKey}`,
|
|
40
|
+
'X-Agent-Id': this.agentId,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const body = await res.text().catch(() => '');
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`ContextDiscoveryClient.discover ${res.status} ${body.slice(0, 200)}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const parsed = JSON.parse(body) as { reach?: ContextReachDescriptor[] };
|
|
50
|
+
return parsed.reach ?? [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
|
|
4
|
+
export type ContextReadType = 'messages' | 'artifacts' | 'agreements' | 'tasks';
|
|
5
|
+
|
|
6
|
+
export interface ContextReadQuery {
|
|
7
|
+
via: string;
|
|
8
|
+
cursor?: string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
after?: string;
|
|
11
|
+
direction?: 'forward';
|
|
12
|
+
state?: string;
|
|
13
|
+
contextGrantId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ContextReadEnvelope<T = unknown> {
|
|
17
|
+
type: ContextReadType;
|
|
18
|
+
via: { kind: string; id: string };
|
|
19
|
+
items: T[];
|
|
20
|
+
hasMore: boolean;
|
|
21
|
+
nextCursor: string | null;
|
|
22
|
+
latestSequence?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Protocol-first uniform context reads (ZIG-427).
|
|
27
|
+
* Wraps `GET /context/read/:type` — one client, one envelope, four types.
|
|
28
|
+
*/
|
|
29
|
+
export class ContextReadClient {
|
|
30
|
+
private readonly operatorKey: string;
|
|
31
|
+
private readonly agentId: string;
|
|
32
|
+
private readonly baseUrl: string;
|
|
33
|
+
|
|
34
|
+
constructor(operatorKey: string, agentId: string, baseUrl?: string) {
|
|
35
|
+
if (!operatorKey) throw new Error('ContextReadClient: operatorKey is required');
|
|
36
|
+
if (!agentId) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'ContextReadClient: agentId is required (operator-token impersonation)',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
this.operatorKey = operatorKey;
|
|
42
|
+
this.agentId = agentId;
|
|
43
|
+
this.baseUrl = baseUrl || getBackendUrl();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async read<T = unknown>(
|
|
47
|
+
type: ContextReadType,
|
|
48
|
+
query: ContextReadQuery,
|
|
49
|
+
): Promise<ContextReadEnvelope<T>> {
|
|
50
|
+
if (!query.via?.trim()) {
|
|
51
|
+
throw new Error('ContextReadClient.read: via is required');
|
|
52
|
+
}
|
|
53
|
+
const url = new URL(
|
|
54
|
+
`${this.baseUrl}/context/read/${encodeURIComponent(type)}`,
|
|
55
|
+
);
|
|
56
|
+
url.searchParams.set('via', query.via.trim());
|
|
57
|
+
if (query.cursor) url.searchParams.set('cursor', query.cursor);
|
|
58
|
+
if (query.limit != null) url.searchParams.set('limit', String(query.limit));
|
|
59
|
+
if (query.after) url.searchParams.set('after', query.after);
|
|
60
|
+
if (query.direction) url.searchParams.set('direction', query.direction);
|
|
61
|
+
if (query.state) url.searchParams.set('state', query.state);
|
|
62
|
+
if (query.contextGrantId) {
|
|
63
|
+
url.searchParams.set('contextGrantId', query.contextGrantId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const headers: Record<string, string> = {
|
|
67
|
+
Authorization: `Bearer ${this.operatorKey}`,
|
|
68
|
+
'X-Agent-Id': this.agentId,
|
|
69
|
+
};
|
|
70
|
+
if (query.contextGrantId) {
|
|
71
|
+
headers['X-Context-Grant-Id'] = query.contextGrantId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const res = await fetch(url.toString(), { headers });
|
|
75
|
+
const body = await res.text().catch(() => '');
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`ContextReadClient.read ${type} ${res.status} ${body.slice(0, 200)}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return JSON.parse(body) as ContextReadEnvelope<T>;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
import { type Creds, type Agreement, ApiError } from '../types.js';
|
|
4
|
+
|
|
5
|
+
function getMarketplaceBaseUrl(): string { return `${getBackendUrl()}/marketplace`; }
|
|
6
|
+
|
|
7
|
+
function buildHeaders(creds: Creds): Record<string, string> {
|
|
8
|
+
return {
|
|
9
|
+
'content-type': 'application/json',
|
|
10
|
+
Authorization: `Bearer ${creds.operatorKey}`,
|
|
11
|
+
'X-Agent-Id': creds.agentId,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertCreds(creds: Creds | undefined | null, op: string): asserts creds is Creds {
|
|
16
|
+
if (!creds?.operatorKey) throw new Error(`operatorKey is required for ${op}`);
|
|
17
|
+
if (!creds?.agentId) throw new Error(`agentId is required for ${op}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function throwApiError(response: Response, body: string, defaultMsg: string): never {
|
|
21
|
+
let msg = defaultMsg;
|
|
22
|
+
try {
|
|
23
|
+
const d = JSON.parse(body) as Record<string, unknown>;
|
|
24
|
+
msg = (d['details'] as string) || (d['error'] as string) || (d['message'] as string) || defaultMsg;
|
|
25
|
+
} catch { /**/ }
|
|
26
|
+
throw new ApiError(msg, response.status);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Seller-broadcast offers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export interface PublishOfferPayload {
|
|
34
|
+
description: string;
|
|
35
|
+
price?: number;
|
|
36
|
+
lifecycle?: string;
|
|
37
|
+
expiresAt?: string;
|
|
38
|
+
maxExecutions?: number;
|
|
39
|
+
/** `hire` = claimer becomes the provider's principal on claim. Defaults to `service` server-side. */
|
|
40
|
+
engagementKind?: 'hire' | 'service';
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function publishOffer(payload: PublishOfferPayload, creds: Creds): Promise<Agreement> {
|
|
45
|
+
assertCreds(creds, 'marketplace offer publish');
|
|
46
|
+
const res = await fetch(`${getMarketplaceBaseUrl()}/offers/publish`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: buildHeaders(creds),
|
|
49
|
+
body: JSON.stringify(payload || {}),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const body = await res.text().catch(() => '');
|
|
53
|
+
throwApiError(res, body, `Marketplace offer publish failed: ${res.status}`);
|
|
54
|
+
}
|
|
55
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
56
|
+
return (data?.['offer'] ?? data) as Agreement;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PullOffersOptions {
|
|
60
|
+
limit?: number;
|
|
61
|
+
since?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function pullOffers(options: PullOffersOptions | undefined, creds: Creds): Promise<Agreement[]> {
|
|
65
|
+
assertCreds(creds, 'marketplace offers pull');
|
|
66
|
+
const res = await fetch(`${getMarketplaceBaseUrl()}/offers/pull`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: buildHeaders(creds),
|
|
69
|
+
body: JSON.stringify(options || {}),
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const body = await res.text().catch(() => '');
|
|
73
|
+
throwApiError(res, body, `Marketplace offers pull failed: ${res.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
76
|
+
return (data?.['offers'] as Agreement[]) ?? [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function claimOffer(agreementId: string, creds: Creds): Promise<Agreement> {
|
|
80
|
+
if (!agreementId) throw new Error('agreementId is required');
|
|
81
|
+
assertCreds(creds, 'marketplace offer claim');
|
|
82
|
+
const res = await fetch(`${getMarketplaceBaseUrl()}/offers/claim`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: buildHeaders(creds),
|
|
85
|
+
body: JSON.stringify({ agreementId }),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const body = await res.text().catch(() => '');
|
|
89
|
+
throwApiError(res, body, `Marketplace offer claim failed: ${res.status}`);
|
|
90
|
+
}
|
|
91
|
+
const data = await res.json().catch(() => null) as Record<string, unknown> | null;
|
|
92
|
+
if (!data?.['ok']) throw new Error((data?.['error'] as string) || 'Claim failed');
|
|
93
|
+
return data['offer'] as Agreement;
|
|
94
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
|
|
4
|
+
export interface ListMessagesOptions {
|
|
5
|
+
/** ISO timestamp; only entries with `timestamp > after` are returned. */
|
|
6
|
+
after?: string;
|
|
7
|
+
/** Page size, default 100, hard cap 1000. */
|
|
8
|
+
limit?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ListMessagesResult {
|
|
12
|
+
messages: unknown[];
|
|
13
|
+
latestSequence: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read-side client for forward-delta message reads.
|
|
18
|
+
*
|
|
19
|
+
* Hits the canonical `GET /chats/:chatId/messages?after=&direction=forward&limit=`
|
|
20
|
+
* endpoint and returns a `{ messages, latestSequence }` envelope
|
|
21
|
+
* (`chat-messaging.controller.listMessagesForward` →
|
|
22
|
+
* `MessagesService.listForChat`).
|
|
23
|
+
*/
|
|
24
|
+
export class MessagesClient {
|
|
25
|
+
private readonly operatorKey: string;
|
|
26
|
+
private readonly agentId: string;
|
|
27
|
+
|
|
28
|
+
constructor(operatorKey: string, agentId: string) {
|
|
29
|
+
if (!operatorKey) throw new Error('MessagesClient: operatorKey is required');
|
|
30
|
+
if (!agentId)
|
|
31
|
+
throw new Error(
|
|
32
|
+
'MessagesClient: agentId is required (operator-token impersonation)',
|
|
33
|
+
);
|
|
34
|
+
this.operatorKey = operatorKey;
|
|
35
|
+
this.agentId = agentId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async list(
|
|
39
|
+
chatId: string,
|
|
40
|
+
opts: ListMessagesOptions = {},
|
|
41
|
+
): Promise<ListMessagesResult> {
|
|
42
|
+
if (!chatId) return { messages: [], latestSequence: null };
|
|
43
|
+
const url = new URL(
|
|
44
|
+
`${getBackendUrl()}/chats/${encodeURIComponent(chatId)}/messages`,
|
|
45
|
+
);
|
|
46
|
+
// Forward-delta read mode. Backend routes to `listMessagesForward`
|
|
47
|
+
// only when BOTH `after` and `direction=forward` are present; without
|
|
48
|
+
// `after` the same handler falls into backward pagination and returns
|
|
49
|
+
// a different envelope. Default `after` to the unix epoch so the
|
|
50
|
+
// forward branch always wins when callers want "from the beginning".
|
|
51
|
+
url.searchParams.set('direction', 'forward');
|
|
52
|
+
url.searchParams.set('after', opts.after || '1970-01-01T00:00:00.000Z');
|
|
53
|
+
if (opts.limit != null) url.searchParams.set('limit', String(opts.limit));
|
|
54
|
+
const res = await fetch(url.toString(), { headers: this._headers() });
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const body = await res.text().catch(() => '');
|
|
57
|
+
throw new Error(
|
|
58
|
+
`MessagesClient.list ${res.status} ${res.statusText} ${body.slice(0, 200)}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return (await res.json()) as ListMessagesResult;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private _headers(): Record<string, string> {
|
|
65
|
+
return {
|
|
66
|
+
'content-type': 'application/json',
|
|
67
|
+
Authorization: `Bearer ${this.operatorKey}`,
|
|
68
|
+
'X-Agent-Id': this.agentId,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
|
|
4
|
+
export type ScopeKind = 'chat' | 'agreement' | 'task' | 'counterparty';
|
|
5
|
+
|
|
6
|
+
export interface PartyRef {
|
|
7
|
+
id: string;
|
|
8
|
+
role: 'payer' | 'provider' | 'creator' | 'proposedTo' | 'providerPrincipal';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ScopeResult {
|
|
12
|
+
via: { kind: ScopeKind; id: string };
|
|
13
|
+
agent: string;
|
|
14
|
+
accessible: {
|
|
15
|
+
chats: string[];
|
|
16
|
+
agreements: string[];
|
|
17
|
+
tasks: string[];
|
|
18
|
+
counterparties: PartyRef[];
|
|
19
|
+
};
|
|
20
|
+
permissions: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Client for `GET /scope?via=<kind>:<id>`. Returns the access graph reachable
|
|
25
|
+
* from an entry point for the impersonated agent. Used by `runTurn` to
|
|
26
|
+
* bootstrap "what does my current session-key resolve to" before fanning out
|
|
27
|
+
* to primitive reads.
|
|
28
|
+
*/
|
|
29
|
+
export class ScopeClient {
|
|
30
|
+
private readonly operatorKey: string;
|
|
31
|
+
private readonly agentId: string;
|
|
32
|
+
|
|
33
|
+
constructor(operatorKey: string, agentId: string) {
|
|
34
|
+
if (!operatorKey) throw new Error('ScopeClient: operatorKey is required');
|
|
35
|
+
if (!agentId)
|
|
36
|
+
throw new Error(
|
|
37
|
+
'ScopeClient: agentId is required (operator-token impersonation)',
|
|
38
|
+
);
|
|
39
|
+
this.operatorKey = operatorKey;
|
|
40
|
+
this.agentId = agentId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async get(kind: ScopeKind, id: string): Promise<ScopeResult> {
|
|
44
|
+
if (!id) throw new Error('ScopeClient.get: id is required');
|
|
45
|
+
const url = new URL(`${getBackendUrl()}/scope`);
|
|
46
|
+
url.searchParams.set('via', `${kind}:${id}`);
|
|
47
|
+
const res = await fetch(url.toString(), { headers: this._headers() });
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const body = await res.text().catch(() => '');
|
|
50
|
+
throw new Error(
|
|
51
|
+
`ScopeClient.get ${res.status} ${res.statusText} ${body.slice(0, 200)}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return (await res.json()) as ScopeResult;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private _headers(): Record<string, string> {
|
|
58
|
+
return {
|
|
59
|
+
'content-type': 'application/json',
|
|
60
|
+
Authorization: `Bearer ${this.operatorKey}`,
|
|
61
|
+
'X-Agent-Id': this.agentId,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|