@almyty/client 0.1.0 → 1.2.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 ADDED
@@ -0,0 +1,36 @@
1
+ # @almyty/client
2
+
3
+ Shared HTTP client and credential resolver used by all almyty CLI packages.
4
+
5
+ ## Usage
6
+
7
+ ```typescript
8
+ import { AlmytyClient, resolveCredentialsOrExit } from '@almyty/client';
9
+
10
+ const creds = resolveCredentialsOrExit();
11
+ const client = new AlmytyClient(creds.url, creds.token);
12
+
13
+ const agents = await client.listAgents();
14
+ ```
15
+
16
+ ## Exports
17
+
18
+ - `AlmytyClient` -- API client (agents, runs, gateways)
19
+ - `GatewayClient` -- gateway-scoped client (invoke, stream, conversations)
20
+ - `resolveCredentials()` -- read `~/.almyty/credentials.json` (returns null if missing)
21
+ - `resolveCredentialsOrExit()` -- same, but exits with an error message if missing
22
+ - `getOrgSlugFromToken(token)` -- extract org slug from JWT
23
+ - `loadCredentials()` -- raw file read
24
+ - `CREDENTIALS_FILE` -- path to `~/.almyty/credentials.json`
25
+
26
+ ## About almyty
27
+
28
+ almyty is the full-stack platform for AI agents, agnostic by design: any LLM, any
29
+ API turned into tools, served over MCP, A2A, UTCP, and Agent Skills. Open source,
30
+ no lock-in.
31
+
32
+ - Website — https://almyty.com
33
+ - Docs — https://docs.almyty.com
34
+ - Source — https://github.com/almyty-inc/almyty
35
+
36
+ Apache-2.0 © Almyty Inc.
package/dist/client.d.ts CHANGED
@@ -5,6 +5,11 @@
5
5
  * and @almyty/mcp-server. Covers agent discovery, invocation,
6
6
  * autonomous run management, and polling.
7
7
  */
8
+ export interface AgentTool {
9
+ id: string;
10
+ name: string;
11
+ description?: string;
12
+ }
8
13
  export interface AgentInfo {
9
14
  id: string;
10
15
  name: string;
@@ -16,6 +21,7 @@ export interface AgentInfo {
16
21
  nodes?: PipelineNode[];
17
22
  };
18
23
  modelConfig?: Record<string, unknown>;
24
+ tools?: AgentTool[];
19
25
  }
20
26
  export interface PipelineNode {
21
27
  id: string;
@@ -39,16 +45,61 @@ export interface RunLimits {
39
45
  maxCostCents?: number;
40
46
  maxDurationMs?: number;
41
47
  }
48
+ /** SSE event from the agent run stream. */
49
+ export interface StreamEvent {
50
+ type: string;
51
+ data: Record<string, unknown>;
52
+ }
53
+ /** A coding CLI detected on a runner machine. */
54
+ export interface RunnerCodingAgent {
55
+ id: string;
56
+ displayName: string;
57
+ binary: string;
58
+ version?: string;
59
+ providerFamily?: string;
60
+ }
61
+ /** A registered runner (one of the user's machines). */
62
+ export interface RunnerSummary {
63
+ id: string;
64
+ name: string;
65
+ state?: string;
66
+ labels?: Record<string, string>;
67
+ /** Coding CLIs the runner reported at registration. */
68
+ codingAgents: RunnerCodingAgent[];
69
+ }
70
+ /** A coding session running on a runner. */
71
+ export interface CodingSession {
72
+ sessionId: string;
73
+ agent: string;
74
+ binary?: string;
75
+ processId?: string;
76
+ cwd?: string;
77
+ task?: string;
78
+ status?: string;
79
+ exitCode?: number | null;
80
+ }
81
+ /** Callback for stream events. */
82
+ export type StreamEventHandler = (event: StreamEvent) => void;
42
83
  export declare class AlmytyClient {
43
84
  private readonly baseUrl;
44
85
  private readonly token;
45
86
  constructor(baseUrl: string, token: string);
46
87
  private headers;
47
88
  request(path: string, init?: RequestInit): Promise<any>;
89
+ /**
90
+ * Connect to an SSE endpoint and call handler for each event.
91
+ * Returns when the stream ends or a terminal event is received.
92
+ */
93
+ streamSSE(path: string, handler: StreamEventHandler, signal?: AbortSignal): Promise<void>;
48
94
  private unwrap;
49
95
  listAgents(): Promise<AgentInfo[]>;
50
96
  getAgent(id: string): Promise<AgentInfo>;
51
97
  findAgentByNameOrId(nameOrId: string): Promise<AgentInfo | null>;
98
+ /**
99
+ * Return a gateway-scoped client that routes all calls through
100
+ * /:orgSlug/:agentSlug instead of /agents/:id.
101
+ */
102
+ gateway(orgSlug: string, agentSlug: string): GatewayClient;
52
103
  invokeAgent(agentId: string, input: Record<string, any>): Promise<any>;
53
104
  startRun(agentId: string, input: any, options?: RunLimits & {
54
105
  conversationId?: string;
@@ -68,4 +119,65 @@ export declare class AlmytyClient {
68
119
  timeoutMs?: number;
69
120
  onStep?: (run: AgentRun) => void;
70
121
  }): Promise<AgentRun>;
122
+ /** The caller's registered runners, with their detected coding CLIs. */
123
+ listRunners(): Promise<RunnerSummary[]>;
124
+ /** Fresh probe of coding CLIs installed on the runner machine. */
125
+ listRunnerCodingAgents(runnerId: string): Promise<RunnerCodingAgent[]>;
126
+ /** Start a coding session (spawns the CLI with the task prompt). */
127
+ startCodingSession(runnerId: string, options: {
128
+ agent: string;
129
+ task: string;
130
+ cwd?: string;
131
+ model?: string;
132
+ }): Promise<CodingSession>;
133
+ getCodingSession(runnerId: string, sessionId: string): Promise<CodingSession>;
134
+ /** Route a line of user input to the session's stdin. */
135
+ sendCodingInput(runnerId: string, sessionId: string, data: string): Promise<void>;
136
+ stopCodingSession(runnerId: string, sessionId: string, force?: boolean): Promise<void>;
137
+ /**
138
+ * Stream a coding session's output via SSE. Calls handler for each
139
+ * coding.output / coding.exit event; returns when the session exits or
140
+ * the stream ends.
141
+ */
142
+ streamCodingEvents(runnerId: string, sessionId: string, handler: StreamEventHandler, signal?: AbortSignal): Promise<void>;
143
+ }
144
+ /**
145
+ * Routes all agent calls through the gateway unified endpoint
146
+ * (/:orgSlug/:agentSlug/...) instead of /agents/:id/...
147
+ *
148
+ * Authenticates via API key (same Bearer token).
149
+ */
150
+ export declare class GatewayClient {
151
+ private readonly client;
152
+ private readonly prefix;
153
+ readonly orgSlug: string;
154
+ readonly agentSlug: string;
155
+ constructor(client: AlmytyClient, orgSlug: string, agentSlug: string);
156
+ getInfo(): Promise<AgentInfo>;
157
+ invoke(input: Record<string, any>): Promise<any>;
158
+ startRun(input: any, options?: RunLimits & {
159
+ conversationId?: string;
160
+ }): Promise<AgentRun>;
161
+ getRun(runId: string): Promise<AgentRun>;
162
+ /**
163
+ * Stream run events via SSE. Calls handler for each event
164
+ * (llm.started, llm.chunk, llm.response, tool.started, tool.result,
165
+ * step.completed, run.completed, run.failed).
166
+ * Returns when the run completes or fails.
167
+ * Falls back to polling if SSE fails.
168
+ */
169
+ streamRun(runId: string, handler: StreamEventHandler, signal?: AbortSignal): Promise<AgentRun>;
170
+ getConversationMessages(conversationId: string): Promise<Array<{
171
+ id: string;
172
+ role: string;
173
+ content: string;
174
+ createdAt: string;
175
+ }>>;
176
+ sendRunInput(runId: string, input: string): Promise<void>;
177
+ cancelRun(runId: string): Promise<void>;
178
+ pollRun(runId: string, options?: {
179
+ intervalMs?: number;
180
+ timeoutMs?: number;
181
+ onStep?: (run: AgentRun) => void;
182
+ }): Promise<AgentRun>;
71
183
  }
package/dist/client.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * autonomous run management, and polling.
7
7
  */
8
8
  const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'timeout']);
9
+ const TERMINAL_EVENT_TYPES = new Set(['run.completed', 'run.failed', 'run.cancelled', 'coding.exit']);
9
10
  export class AlmytyClient {
10
11
  baseUrl;
11
12
  token;
@@ -38,6 +39,67 @@ export class AlmytyClient {
38
39
  return null;
39
40
  return res.json();
40
41
  }
42
+ /**
43
+ * Connect to an SSE endpoint and call handler for each event.
44
+ * Returns when the stream ends or a terminal event is received.
45
+ */
46
+ async streamSSE(path, handler, signal) {
47
+ const url = `${this.baseUrl}${path}`;
48
+ const res = await fetch(url, {
49
+ headers: { ...this.headers(), Accept: 'text/event-stream' },
50
+ signal,
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => '');
54
+ throw new Error(`SSE ${res.status}: ${text}`);
55
+ }
56
+ const body = res.body;
57
+ if (!body)
58
+ return;
59
+ const reader = body.getReader();
60
+ const decoder = new TextDecoder();
61
+ let buffer = '';
62
+ try {
63
+ while (true) {
64
+ const { value, done } = await reader.read();
65
+ if (done)
66
+ break;
67
+ buffer += decoder.decode(value, { stream: true });
68
+ // Parse SSE frames
69
+ const lines = buffer.split('\n');
70
+ buffer = lines.pop(); // keep incomplete line
71
+ let eventType = 'message';
72
+ let dataLines = [];
73
+ for (const line of lines) {
74
+ if (line.startsWith('event: ')) {
75
+ eventType = line.slice(7).trim();
76
+ }
77
+ else if (line.startsWith('data: ')) {
78
+ dataLines.push(line.slice(6));
79
+ }
80
+ else if (line === '') {
81
+ // End of frame
82
+ if (dataLines.length) {
83
+ const raw = dataLines.join('\n');
84
+ try {
85
+ const data = JSON.parse(raw);
86
+ const event = { type: data.type || eventType, data };
87
+ handler(event);
88
+ if (TERMINAL_EVENT_TYPES.has(event.type))
89
+ return;
90
+ }
91
+ catch { /* skip malformed */ }
92
+ dataLines = [];
93
+ eventType = 'message';
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ finally {
100
+ reader.releaseLock();
101
+ }
102
+ }
41
103
  unwrap(data) {
42
104
  return data?.data ?? data;
43
105
  }
@@ -81,7 +143,15 @@ export class AlmytyClient {
81
143
  all.find((a) => a.slug?.toLowerCase() === lower) ||
82
144
  null);
83
145
  }
84
- // ── Workflow invocation ───────────────────────���─────────────────
146
+ // ── Gateway-scoped client ───────────────────────────────────────
147
+ /**
148
+ * Return a gateway-scoped client that routes all calls through
149
+ * /:orgSlug/:agentSlug instead of /agents/:id.
150
+ */
151
+ gateway(orgSlug, agentSlug) {
152
+ return new GatewayClient(this, orgSlug, agentSlug);
153
+ }
154
+ // ── Workflow invocation ────────────────────────────────────────
85
155
  async invokeAgent(agentId, input) {
86
156
  const data = await this.request(`/agents/${encodeURIComponent(agentId)}/invoke`, {
87
157
  method: 'POST',
@@ -155,4 +225,151 @@ export class AlmytyClient {
155
225
  }
156
226
  throw new Error(`Run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s`);
157
227
  }
228
+ // ── Runners & coding sessions ───────────────────────────────────
229
+ /** The caller's registered runners, with their detected coding CLIs. */
230
+ async listRunners() {
231
+ const data = await this.request('/runners');
232
+ const list = data?.data ?? data ?? [];
233
+ return list.map((r) => ({
234
+ id: r.id,
235
+ name: r.name,
236
+ state: r.state,
237
+ labels: r.labels,
238
+ codingAgents: r.runtimeInfo?.codingAgents ?? [],
239
+ }));
240
+ }
241
+ /** Fresh probe of coding CLIs installed on the runner machine. */
242
+ async listRunnerCodingAgents(runnerId) {
243
+ const data = await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/agents`);
244
+ return this.unwrap(data)?.agents ?? [];
245
+ }
246
+ /** Start a coding session (spawns the CLI with the task prompt). */
247
+ async startCodingSession(runnerId, options) {
248
+ const data = await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions`, { method: 'POST', body: JSON.stringify(options) });
249
+ return this.unwrap(data);
250
+ }
251
+ async getCodingSession(runnerId, sessionId) {
252
+ const data = await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}`);
253
+ return this.unwrap(data);
254
+ }
255
+ /** Route a line of user input to the session's stdin. */
256
+ async sendCodingInput(runnerId, sessionId, data) {
257
+ await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}/input`, { method: 'POST', body: JSON.stringify({ data }) });
258
+ }
259
+ async stopCodingSession(runnerId, sessionId, force = false) {
260
+ await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}/stop`, { method: 'POST', body: JSON.stringify(force ? { force } : {}) });
261
+ }
262
+ /**
263
+ * Stream a coding session's output via SSE. Calls handler for each
264
+ * coding.output / coding.exit event; returns when the session exits or
265
+ * the stream ends.
266
+ */
267
+ async streamCodingEvents(runnerId, sessionId, handler, signal) {
268
+ await this.streamSSE(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}/events`, handler, signal);
269
+ }
270
+ }
271
+ // ── Gateway-scoped client ───────────────────────────────────────
272
+ /**
273
+ * Routes all agent calls through the gateway unified endpoint
274
+ * (/:orgSlug/:agentSlug/...) instead of /agents/:id/...
275
+ *
276
+ * Authenticates via API key (same Bearer token).
277
+ */
278
+ export class GatewayClient {
279
+ client;
280
+ prefix;
281
+ orgSlug;
282
+ agentSlug;
283
+ constructor(client, orgSlug, agentSlug) {
284
+ this.client = client;
285
+ this.orgSlug = orgSlug;
286
+ this.agentSlug = agentSlug;
287
+ this.prefix = `/${encodeURIComponent(orgSlug)}/${encodeURIComponent(agentSlug)}`;
288
+ }
289
+ async getInfo() {
290
+ const data = await this.client.request(this.prefix);
291
+ return data?.data ?? data;
292
+ }
293
+ async invoke(input) {
294
+ const data = await this.client.request(`${this.prefix}/invoke`, {
295
+ method: 'POST',
296
+ body: JSON.stringify({ input }),
297
+ });
298
+ return data?.data ?? data;
299
+ }
300
+ async startRun(input, options) {
301
+ const body = { input };
302
+ if (options?.maxSteps)
303
+ body.maxSteps = options.maxSteps;
304
+ if (options?.maxCostCents)
305
+ body.maxCostCents = options.maxCostCents;
306
+ if (options?.maxDurationMs)
307
+ body.maxDurationMs = options.maxDurationMs;
308
+ if (options?.conversationId)
309
+ body.conversationId = options.conversationId;
310
+ const data = await this.client.request(`${this.prefix}/runs`, {
311
+ method: 'POST',
312
+ body: JSON.stringify(body),
313
+ });
314
+ const run = data?.data ?? data;
315
+ return {
316
+ id: run.id,
317
+ agentId: run.agentId,
318
+ status: run.status,
319
+ conversationId: run.conversationId,
320
+ output: run.output,
321
+ error: run.error,
322
+ steps: run.steps,
323
+ };
324
+ }
325
+ async getRun(runId) {
326
+ const data = await this.client.request(`${this.prefix}/runs/${encodeURIComponent(runId)}`);
327
+ return (data?.data ?? data);
328
+ }
329
+ /**
330
+ * Stream run events via SSE. Calls handler for each event
331
+ * (llm.started, llm.chunk, llm.response, tool.started, tool.result,
332
+ * step.completed, run.completed, run.failed).
333
+ * Returns when the run completes or fails.
334
+ * Falls back to polling if SSE fails.
335
+ */
336
+ async streamRun(runId, handler, signal) {
337
+ try {
338
+ await this.client.streamSSE(`${this.prefix}/runs/${encodeURIComponent(runId)}/stream`, handler, signal);
339
+ // Stream ended — get final state
340
+ return this.getRun(runId);
341
+ }
342
+ catch {
343
+ // SSE failed — fall back to polling until completion
344
+ return this.pollRun(runId);
345
+ }
346
+ }
347
+ async getConversationMessages(conversationId) {
348
+ const data = await this.client.request(`${this.prefix}/conversations/${encodeURIComponent(conversationId)}/messages`);
349
+ return data?.data ?? [];
350
+ }
351
+ async sendRunInput(runId, input) {
352
+ await this.client.request(`${this.prefix}/runs/${encodeURIComponent(runId)}/input`, { method: 'POST', body: JSON.stringify({ input }) });
353
+ }
354
+ async cancelRun(runId) {
355
+ await this.client.request(`${this.prefix}/runs/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
356
+ }
357
+ async pollRun(runId, options = {}) {
358
+ const intervalMs = options.intervalMs ?? 1500;
359
+ const timeoutMs = options.timeoutMs ?? 5 * 60_000;
360
+ const deadline = Date.now() + timeoutMs;
361
+ let lastStepCount = -1;
362
+ while (Date.now() < deadline) {
363
+ const run = await this.getRun(runId);
364
+ if (Array.isArray(run.steps) && run.steps.length !== lastStepCount) {
365
+ lastStepCount = run.steps.length;
366
+ options.onStep?.(run);
367
+ }
368
+ if (run.status && (TERMINAL_STATUSES.has(run.status) || run.status === 'waiting_input')) {
369
+ return run;
370
+ }
371
+ await new Promise((r) => setTimeout(r, intervalMs));
372
+ }
373
+ throw new Error(`Run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s`);
374
+ }
158
375
  }
@@ -20,3 +20,8 @@ export declare function resolveCredentials(): StoredCredentials | null;
20
20
  * Resolve credentials or exit with an error message.
21
21
  */
22
22
  export declare function resolveCredentialsOrExit(): StoredCredentials;
23
+ /**
24
+ * Extract the default org slug from a JWT token.
25
+ * Returns null if the token isn't a JWT or has no orgs.
26
+ */
27
+ export declare function getOrgSlugFromToken(token: string): string | null;
@@ -43,3 +43,30 @@ export function resolveCredentialsOrExit() {
43
43
  console.error(' export ALMYTY_TOKEN=<your-token>');
44
44
  process.exit(1);
45
45
  }
46
+ /**
47
+ * Extract the default org slug from a JWT token.
48
+ * Returns null if the token isn't a JWT or has no orgs.
49
+ */
50
+ export function getOrgSlugFromToken(token) {
51
+ try {
52
+ const parts = token.split('.');
53
+ if (parts.length !== 3)
54
+ return null;
55
+ let payload = parts[1];
56
+ payload += '='.repeat((4 - payload.length % 4) % 4);
57
+ const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());
58
+ const orgs = decoded.organizations;
59
+ if (!Array.isArray(orgs) || !orgs.length)
60
+ return null;
61
+ // Use slug if available, otherwise derive from name
62
+ const org = orgs[0];
63
+ if (org.slug)
64
+ return org.slug;
65
+ if (org.name)
66
+ return org.name.toLowerCase().replace(/\s+/g, '-');
67
+ return null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { AlmytyClient } from './client.js';
2
- export type { AgentInfo, AgentRun, PipelineNode, RunLimits } from './client.js';
3
- export { loadCredentials, resolveCredentials, resolveCredentialsOrExit, CREDENTIALS_FILE, } from './credentials.js';
1
+ export { AlmytyClient, GatewayClient } from './client.js';
2
+ export type { AgentInfo, AgentTool, AgentRun, PipelineNode, RunLimits, StreamEvent, StreamEventHandler, RunnerSummary, RunnerCodingAgent, CodingSession, } from './client.js';
3
+ export { loadCredentials, resolveCredentials, resolveCredentialsOrExit, getOrgSlugFromToken, CREDENTIALS_FILE, } from './credentials.js';
4
4
  export type { StoredCredentials } from './credentials.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { AlmytyClient } from './client.js';
2
- export { loadCredentials, resolveCredentials, resolveCredentialsOrExit, CREDENTIALS_FILE, } from './credentials.js';
1
+ export { AlmytyClient, GatewayClient } from './client.js';
2
+ export { loadCredentials, resolveCredentials, resolveCredentialsOrExit, getOrgSlugFromToken, CREDENTIALS_FILE, } from './credentials.js';
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@almyty/client",
3
- "version": "0.1.0",
4
- "description": "Shared HTTP client and credential resolver for almyty CLI packages",
3
+ "version": "1.2.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Shared HTTP client and credential resolver used by the almyty CLIs. Reads ~/.almyty/credentials.json; not usually installed on its own.",
5
8
  "type": "module",
6
9
  "main": "dist/index.js",
7
10
  "types": "dist/index.d.ts",
@@ -20,10 +23,19 @@
20
23
  "sdk"
21
24
  ],
22
25
  "author": "almyty",
23
- "license": "BSL-1.1",
26
+ "license": "Apache-2.0",
24
27
  "devDependencies": {
25
28
  "@types/node": "^25.4.0",
26
29
  "typescript": "^5.3.0",
27
30
  "vitest": "^4.1.0"
31
+ },
32
+ "homepage": "https://almyty.com",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/almyty-inc/almyty.git",
36
+ "directory": "packages/client"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/almyty-inc/almyty/issues"
28
40
  }
29
41
  }