@almyty/client 0.1.0 → 1.3.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 +36 -0
- package/dist/client.d.ts +149 -0
- package/dist/client.js +354 -6
- package/dist/credentials.d.ts +29 -2
- package/dist/credentials.js +55 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/package.json +18 -3
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,79 @@ 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;
|
|
83
|
+
/** Whether a rejection is a caller-requested abort rather than a failure. */
|
|
84
|
+
export declare function isAbortError(err: unknown): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Turn one SSE frame's lines into a StreamEvent, or null when the frame
|
|
87
|
+
* carries nothing usable (a keep-alive comment, or malformed JSON).
|
|
88
|
+
*
|
|
89
|
+
* Server frames are not uniform. Run events arrive wrapped as
|
|
90
|
+
* `{type, data, timestamp}`; coding and pipeline events put their
|
|
91
|
+
* fields at the top level and may also carry a `data` object. So the
|
|
92
|
+
* envelope's own `data` object is flattened onto the result and the
|
|
93
|
+
* top-level fields are kept. Reading `event.data.content` off an
|
|
94
|
+
* `llm.chunk` returned undefined before this, which is why a streaming
|
|
95
|
+
* reply used to arrive as one block once the run had already finished.
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseSseFrame(lines: string[]): StreamEvent | null;
|
|
42
98
|
export declare class AlmytyClient {
|
|
43
99
|
private readonly baseUrl;
|
|
44
100
|
private readonly token;
|
|
45
101
|
constructor(baseUrl: string, token: string);
|
|
46
102
|
private headers;
|
|
47
103
|
request(path: string, init?: RequestInit): Promise<any>;
|
|
104
|
+
/**
|
|
105
|
+
* Connect to an SSE endpoint and call handler for each event.
|
|
106
|
+
* Returns when the stream ends or a terminal event is received.
|
|
107
|
+
*
|
|
108
|
+
* `init` lets a caller POST (the workflow pipeline stream does);
|
|
109
|
+
* omitted, this is a GET.
|
|
110
|
+
*/
|
|
111
|
+
streamSSE(path: string, handler: StreamEventHandler, signal?: AbortSignal, init?: RequestInit): Promise<void>;
|
|
48
112
|
private unwrap;
|
|
49
113
|
listAgents(): Promise<AgentInfo[]>;
|
|
50
114
|
getAgent(id: string): Promise<AgentInfo>;
|
|
51
115
|
findAgentByNameOrId(nameOrId: string): Promise<AgentInfo | null>;
|
|
116
|
+
/**
|
|
117
|
+
* Return a gateway-scoped client that routes all calls through
|
|
118
|
+
* /:orgSlug/:agentSlug instead of /agents/:id.
|
|
119
|
+
*/
|
|
120
|
+
gateway(orgSlug: string, agentSlug: string): GatewayClient;
|
|
52
121
|
invokeAgent(agentId: string, input: Record<string, any>): Promise<any>;
|
|
53
122
|
startRun(agentId: string, input: any, options?: RunLimits & {
|
|
54
123
|
conversationId?: string;
|
|
@@ -68,4 +137,84 @@ export declare class AlmytyClient {
|
|
|
68
137
|
timeoutMs?: number;
|
|
69
138
|
onStep?: (run: AgentRun) => void;
|
|
70
139
|
}): Promise<AgentRun>;
|
|
140
|
+
/** The caller's registered runners, with their detected coding CLIs. */
|
|
141
|
+
listRunners(): Promise<RunnerSummary[]>;
|
|
142
|
+
/** Fresh probe of coding CLIs installed on the runner machine. */
|
|
143
|
+
listRunnerCodingAgents(runnerId: string): Promise<RunnerCodingAgent[]>;
|
|
144
|
+
/** Start a coding session (spawns the CLI with the task prompt). */
|
|
145
|
+
startCodingSession(runnerId: string, options: {
|
|
146
|
+
agent: string;
|
|
147
|
+
task: string;
|
|
148
|
+
cwd?: string;
|
|
149
|
+
model?: string;
|
|
150
|
+
}): Promise<CodingSession>;
|
|
151
|
+
getCodingSession(runnerId: string, sessionId: string): Promise<CodingSession>;
|
|
152
|
+
/** Route a line of user input to the session's stdin. */
|
|
153
|
+
sendCodingInput(runnerId: string, sessionId: string, data: string): Promise<void>;
|
|
154
|
+
stopCodingSession(runnerId: string, sessionId: string, force?: boolean): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Stream a coding session's output via SSE. Calls handler for each
|
|
157
|
+
* coding.output / coding.exit event; returns when the session exits or
|
|
158
|
+
* the stream ends.
|
|
159
|
+
*/
|
|
160
|
+
streamCodingEvents(runnerId: string, sessionId: string, handler: StreamEventHandler, signal?: AbortSignal): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Routes all agent calls through the gateway unified endpoint
|
|
164
|
+
* (/:orgSlug/:agentSlug/...) instead of /agents/:id/...
|
|
165
|
+
*
|
|
166
|
+
* Authenticates via API key (same Bearer token).
|
|
167
|
+
*/
|
|
168
|
+
export declare class GatewayClient {
|
|
169
|
+
private readonly client;
|
|
170
|
+
private readonly prefix;
|
|
171
|
+
readonly orgSlug: string;
|
|
172
|
+
readonly agentSlug: string;
|
|
173
|
+
constructor(client: AlmytyClient, orgSlug: string, agentSlug: string);
|
|
174
|
+
getInfo(): Promise<AgentInfo>;
|
|
175
|
+
invoke(input: Record<string, any>): Promise<any>;
|
|
176
|
+
startRun(input: any, options?: RunLimits & {
|
|
177
|
+
conversationId?: string;
|
|
178
|
+
}): Promise<AgentRun>;
|
|
179
|
+
getRun(runId: string): Promise<AgentRun>;
|
|
180
|
+
/**
|
|
181
|
+
* Stream a workflow agent's pipeline as it executes.
|
|
182
|
+
*
|
|
183
|
+
* The unified endpoint answers POST /:org/:agent/stream with SSE:
|
|
184
|
+
* execution.started, node.started, node.output, node.completed,
|
|
185
|
+
* node.skipped, then execution.completed or execution.failed. Without
|
|
186
|
+
* this a multi-node pipeline is a blocking POST with nothing to show
|
|
187
|
+
* while it runs.
|
|
188
|
+
*/
|
|
189
|
+
streamInvoke(input: Record<string, any>, handler: StreamEventHandler, signal?: AbortSignal): Promise<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Stream run events via SSE. Calls handler for each event
|
|
192
|
+
* (llm.started, llm.chunk, llm.response, tool.started, tool.result,
|
|
193
|
+
* step.completed, run.completed, run.failed).
|
|
194
|
+
* Returns when the run completes or fails.
|
|
195
|
+
* Falls back to polling if SSE fails.
|
|
196
|
+
*/
|
|
197
|
+
streamRun(runId: string, handler: StreamEventHandler, signal?: AbortSignal): Promise<AgentRun>;
|
|
198
|
+
getConversationMessages(conversationId: string): Promise<Array<{
|
|
199
|
+
id: string;
|
|
200
|
+
role: string;
|
|
201
|
+
content: string;
|
|
202
|
+
createdAt: string;
|
|
203
|
+
}>>;
|
|
204
|
+
sendRunInput(runId: string, input: string): Promise<void>;
|
|
205
|
+
cancelRun(runId: string): Promise<void>;
|
|
206
|
+
/**
|
|
207
|
+
* Cancel a workflow execution.
|
|
208
|
+
*
|
|
209
|
+
* The workflow counterpart of cancelRun. A workflow run is an execution,
|
|
210
|
+
* not a run, so cancelRun could never stop one -- a Ctrl-C that did not
|
|
211
|
+
* also drop the SSE connection left the pipeline running and billing.
|
|
212
|
+
*/
|
|
213
|
+
cancelExecution(executionId: string): Promise<void>;
|
|
214
|
+
pollRun(runId: string, options?: {
|
|
215
|
+
intervalMs?: number;
|
|
216
|
+
timeoutMs?: number;
|
|
217
|
+
onStep?: (run: AgentRun) => void;
|
|
218
|
+
signal?: AbortSignal;
|
|
219
|
+
}): Promise<AgentRun>;
|
|
71
220
|
}
|
package/dist/client.js
CHANGED
|
@@ -6,6 +6,70 @@
|
|
|
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([
|
|
10
|
+
'run.completed',
|
|
11
|
+
'run.failed',
|
|
12
|
+
'run.cancelled',
|
|
13
|
+
'coding.exit',
|
|
14
|
+
// The workflow pipeline stream's own terminators.
|
|
15
|
+
'execution.completed',
|
|
16
|
+
'execution.failed',
|
|
17
|
+
'done',
|
|
18
|
+
]);
|
|
19
|
+
/** Whether a rejection is a caller-requested abort rather than a failure. */
|
|
20
|
+
export function isAbortError(err) {
|
|
21
|
+
const e = err;
|
|
22
|
+
return !!e && (e.name === 'AbortError' || e.code === 'ABORT_ERR');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Turn one SSE frame's lines into a StreamEvent, or null when the frame
|
|
26
|
+
* carries nothing usable (a keep-alive comment, or malformed JSON).
|
|
27
|
+
*
|
|
28
|
+
* Server frames are not uniform. Run events arrive wrapped as
|
|
29
|
+
* `{type, data, timestamp}`; coding and pipeline events put their
|
|
30
|
+
* fields at the top level and may also carry a `data` object. So the
|
|
31
|
+
* envelope's own `data` object is flattened onto the result and the
|
|
32
|
+
* top-level fields are kept. Reading `event.data.content` off an
|
|
33
|
+
* `llm.chunk` returned undefined before this, which is why a streaming
|
|
34
|
+
* reply used to arrive as one block once the run had already finished.
|
|
35
|
+
*/
|
|
36
|
+
export function parseSseFrame(lines) {
|
|
37
|
+
let eventType = 'message';
|
|
38
|
+
const dataLines = [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
// A line starting with ':' is a comment. The server sends
|
|
41
|
+
// ': keep-alive' every 15s on the coding stream.
|
|
42
|
+
if (line.startsWith(':'))
|
|
43
|
+
continue;
|
|
44
|
+
if (line.startsWith('event:')) {
|
|
45
|
+
eventType = line.slice(6).trim();
|
|
46
|
+
}
|
|
47
|
+
else if (line.startsWith('data:')) {
|
|
48
|
+
const value = line.slice(5);
|
|
49
|
+
dataLines.push(value.startsWith(' ') ? value.slice(1) : value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!dataLines.length)
|
|
53
|
+
return null;
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(dataLines.join('\n'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
62
|
+
return null;
|
|
63
|
+
const envelope = parsed;
|
|
64
|
+
const inner = envelope.data;
|
|
65
|
+
const flat = inner && typeof inner === 'object' && !Array.isArray(inner)
|
|
66
|
+
? { ...envelope, ...inner }
|
|
67
|
+
: envelope;
|
|
68
|
+
return {
|
|
69
|
+
type: typeof envelope.type === 'string' ? envelope.type : eventType,
|
|
70
|
+
data: flat,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
9
73
|
export class AlmytyClient {
|
|
10
74
|
baseUrl;
|
|
11
75
|
token;
|
|
@@ -26,18 +90,112 @@ export class AlmytyClient {
|
|
|
26
90
|
...this.headers(),
|
|
27
91
|
...(init.headers || {}),
|
|
28
92
|
};
|
|
29
|
-
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await fetch(url, { ...init, headers });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
// A transport failure carries no status, so callers that want to
|
|
99
|
+
// say something useful about it need the cause and the host.
|
|
100
|
+
throw Object.assign(new Error(err?.message || 'Network request failed'), {
|
|
101
|
+
url,
|
|
102
|
+
cause: err,
|
|
103
|
+
networkError: true,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
30
106
|
if (!res.ok) {
|
|
31
|
-
if (res.status === 401) {
|
|
32
|
-
throw new Error('Authentication failed. Run: npx @almyty/auth login');
|
|
33
|
-
}
|
|
34
107
|
const text = await res.text().catch(() => '');
|
|
35
|
-
|
|
108
|
+
// Status and body ride on the error. A CLI cannot turn
|
|
109
|
+
// "API error 400: {...}" into a sentence a user can act on without
|
|
110
|
+
// them, and parsing the message string back apart is worse.
|
|
111
|
+
throw Object.assign(new Error(res.status === 401
|
|
112
|
+
? 'Authentication failed. Run: npx @almyty/auth login'
|
|
113
|
+
: `API error ${res.status}: ${text}`), { status: res.status, body: text, url });
|
|
36
114
|
}
|
|
37
115
|
if (res.status === 204)
|
|
38
116
|
return null;
|
|
39
117
|
return res.json();
|
|
40
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Connect to an SSE endpoint and call handler for each event.
|
|
121
|
+
* Returns when the stream ends or a terminal event is received.
|
|
122
|
+
*
|
|
123
|
+
* `init` lets a caller POST (the workflow pipeline stream does);
|
|
124
|
+
* omitted, this is a GET.
|
|
125
|
+
*/
|
|
126
|
+
async streamSSE(path, handler, signal, init = {}) {
|
|
127
|
+
const url = `${this.baseUrl}${path}`;
|
|
128
|
+
const res = await fetch(url, {
|
|
129
|
+
...init,
|
|
130
|
+
headers: { ...this.headers(), Accept: 'text/event-stream', ...(init.headers || {}) },
|
|
131
|
+
signal,
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
const text = await res.text().catch(() => '');
|
|
135
|
+
throw Object.assign(new Error(`SSE ${res.status}: ${text}`), {
|
|
136
|
+
status: res.status,
|
|
137
|
+
body: text,
|
|
138
|
+
url,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const body = res.body;
|
|
142
|
+
if (!body)
|
|
143
|
+
return;
|
|
144
|
+
const reader = body.getReader();
|
|
145
|
+
const decoder = new TextDecoder();
|
|
146
|
+
let buffer = '';
|
|
147
|
+
// Frame state lives outside the read loop. It used to be declared
|
|
148
|
+
// per chunk, so any frame whose terminating blank line arrived in
|
|
149
|
+
// the next chunk was dropped -- which is most of them on a busy
|
|
150
|
+
// stream, and is why streamed tokens never reached the CLI.
|
|
151
|
+
let frame = [];
|
|
152
|
+
try {
|
|
153
|
+
for (;;) {
|
|
154
|
+
const { value, done } = await reader.read();
|
|
155
|
+
if (done)
|
|
156
|
+
break;
|
|
157
|
+
buffer += decoder.decode(value, { stream: true });
|
|
158
|
+
let nl = buffer.indexOf('\n');
|
|
159
|
+
while (nl !== -1) {
|
|
160
|
+
// CRLF is legal in SSE and a proxy may rewrite to it. Without
|
|
161
|
+
// stripping the carriage return, no line ever compares equal
|
|
162
|
+
// to '' and not one frame is ever dispatched.
|
|
163
|
+
const line = buffer.slice(0, nl).replace(/\r$/, '');
|
|
164
|
+
buffer = buffer.slice(nl + 1);
|
|
165
|
+
if (line === '') {
|
|
166
|
+
const event = parseSseFrame(frame);
|
|
167
|
+
frame = [];
|
|
168
|
+
if (event) {
|
|
169
|
+
handler(event);
|
|
170
|
+
if (TERMINAL_EVENT_TYPES.has(event.type))
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
frame.push(line);
|
|
176
|
+
}
|
|
177
|
+
nl = buffer.indexOf('\n');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// A server that closes without a trailing blank line still sent a
|
|
181
|
+
// frame worth reading.
|
|
182
|
+
if (buffer)
|
|
183
|
+
frame.push(buffer.replace(/\r$/, ''));
|
|
184
|
+
const tail = parseSseFrame(frame);
|
|
185
|
+
if (tail)
|
|
186
|
+
handler(tail);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
// Releasing the lock does not close the connection. A terminal
|
|
190
|
+
// event returns from the loop above with the body unread and the
|
|
191
|
+
// socket still open, and an SSE endpoint holds its end open too,
|
|
192
|
+
// so the handle keeps Node's event loop alive: `almyty chat` would
|
|
193
|
+
// not exit after a streamed turn, and a REPL leaked one connection
|
|
194
|
+
// per answer. Cancelling the body is what actually closes it.
|
|
195
|
+
await reader.cancel().catch(() => undefined);
|
|
196
|
+
reader.releaseLock();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
41
199
|
unwrap(data) {
|
|
42
200
|
return data?.data ?? data;
|
|
43
201
|
}
|
|
@@ -81,7 +239,15 @@ export class AlmytyClient {
|
|
|
81
239
|
all.find((a) => a.slug?.toLowerCase() === lower) ||
|
|
82
240
|
null);
|
|
83
241
|
}
|
|
84
|
-
// ──
|
|
242
|
+
// ── Gateway-scoped client ───────────────────────────────────────
|
|
243
|
+
/**
|
|
244
|
+
* Return a gateway-scoped client that routes all calls through
|
|
245
|
+
* /:orgSlug/:agentSlug instead of /agents/:id.
|
|
246
|
+
*/
|
|
247
|
+
gateway(orgSlug, agentSlug) {
|
|
248
|
+
return new GatewayClient(this, orgSlug, agentSlug);
|
|
249
|
+
}
|
|
250
|
+
// ── Workflow invocation ────────────────────────────────────────
|
|
85
251
|
async invokeAgent(agentId, input) {
|
|
86
252
|
const data = await this.request(`/agents/${encodeURIComponent(agentId)}/invoke`, {
|
|
87
253
|
method: 'POST',
|
|
@@ -155,4 +321,186 @@ export class AlmytyClient {
|
|
|
155
321
|
}
|
|
156
322
|
throw new Error(`Run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s`);
|
|
157
323
|
}
|
|
324
|
+
// ── Runners & coding sessions ───────────────────────────────────
|
|
325
|
+
/** The caller's registered runners, with their detected coding CLIs. */
|
|
326
|
+
async listRunners() {
|
|
327
|
+
const data = await this.request('/runners');
|
|
328
|
+
const list = data?.data ?? data ?? [];
|
|
329
|
+
return list.map((r) => ({
|
|
330
|
+
id: r.id,
|
|
331
|
+
name: r.name,
|
|
332
|
+
state: r.state,
|
|
333
|
+
labels: r.labels,
|
|
334
|
+
codingAgents: r.runtimeInfo?.codingAgents ?? [],
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
/** Fresh probe of coding CLIs installed on the runner machine. */
|
|
338
|
+
async listRunnerCodingAgents(runnerId) {
|
|
339
|
+
const data = await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/agents`);
|
|
340
|
+
return this.unwrap(data)?.agents ?? [];
|
|
341
|
+
}
|
|
342
|
+
/** Start a coding session (spawns the CLI with the task prompt). */
|
|
343
|
+
async startCodingSession(runnerId, options) {
|
|
344
|
+
const data = await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions`, { method: 'POST', body: JSON.stringify(options) });
|
|
345
|
+
return this.unwrap(data);
|
|
346
|
+
}
|
|
347
|
+
async getCodingSession(runnerId, sessionId) {
|
|
348
|
+
const data = await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}`);
|
|
349
|
+
return this.unwrap(data);
|
|
350
|
+
}
|
|
351
|
+
/** Route a line of user input to the session's stdin. */
|
|
352
|
+
async sendCodingInput(runnerId, sessionId, data) {
|
|
353
|
+
await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}/input`, { method: 'POST', body: JSON.stringify({ data }) });
|
|
354
|
+
}
|
|
355
|
+
async stopCodingSession(runnerId, sessionId, force = false) {
|
|
356
|
+
await this.request(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}/stop`, { method: 'POST', body: JSON.stringify(force ? { force } : {}) });
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Stream a coding session's output via SSE. Calls handler for each
|
|
360
|
+
* coding.output / coding.exit event; returns when the session exits or
|
|
361
|
+
* the stream ends.
|
|
362
|
+
*/
|
|
363
|
+
async streamCodingEvents(runnerId, sessionId, handler, signal) {
|
|
364
|
+
await this.streamSSE(`/runners/${encodeURIComponent(runnerId)}/coding/sessions/${encodeURIComponent(sessionId)}/events`, handler, signal);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// ── Gateway-scoped client ───────────────────────────────────────
|
|
368
|
+
/**
|
|
369
|
+
* Routes all agent calls through the gateway unified endpoint
|
|
370
|
+
* (/:orgSlug/:agentSlug/...) instead of /agents/:id/...
|
|
371
|
+
*
|
|
372
|
+
* Authenticates via API key (same Bearer token).
|
|
373
|
+
*/
|
|
374
|
+
export class GatewayClient {
|
|
375
|
+
client;
|
|
376
|
+
prefix;
|
|
377
|
+
orgSlug;
|
|
378
|
+
agentSlug;
|
|
379
|
+
constructor(client, orgSlug, agentSlug) {
|
|
380
|
+
this.client = client;
|
|
381
|
+
this.orgSlug = orgSlug;
|
|
382
|
+
this.agentSlug = agentSlug;
|
|
383
|
+
this.prefix = `/${encodeURIComponent(orgSlug)}/${encodeURIComponent(agentSlug)}`;
|
|
384
|
+
}
|
|
385
|
+
async getInfo() {
|
|
386
|
+
const data = await this.client.request(this.prefix);
|
|
387
|
+
return data?.data ?? data;
|
|
388
|
+
}
|
|
389
|
+
async invoke(input) {
|
|
390
|
+
const data = await this.client.request(`${this.prefix}/invoke`, {
|
|
391
|
+
method: 'POST',
|
|
392
|
+
body: JSON.stringify({ input }),
|
|
393
|
+
});
|
|
394
|
+
return data?.data ?? data;
|
|
395
|
+
}
|
|
396
|
+
async startRun(input, options) {
|
|
397
|
+
const body = { input };
|
|
398
|
+
if (options?.maxSteps)
|
|
399
|
+
body.maxSteps = options.maxSteps;
|
|
400
|
+
if (options?.maxCostCents)
|
|
401
|
+
body.maxCostCents = options.maxCostCents;
|
|
402
|
+
if (options?.maxDurationMs)
|
|
403
|
+
body.maxDurationMs = options.maxDurationMs;
|
|
404
|
+
if (options?.conversationId)
|
|
405
|
+
body.conversationId = options.conversationId;
|
|
406
|
+
const data = await this.client.request(`${this.prefix}/runs`, {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
body: JSON.stringify(body),
|
|
409
|
+
});
|
|
410
|
+
const run = data?.data ?? data;
|
|
411
|
+
return {
|
|
412
|
+
id: run.id,
|
|
413
|
+
agentId: run.agentId,
|
|
414
|
+
status: run.status,
|
|
415
|
+
conversationId: run.conversationId,
|
|
416
|
+
output: run.output,
|
|
417
|
+
error: run.error,
|
|
418
|
+
steps: run.steps,
|
|
419
|
+
totalCost: run.totalCost,
|
|
420
|
+
totalTokens: run.totalTokens,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async getRun(runId) {
|
|
424
|
+
const data = await this.client.request(`${this.prefix}/runs/${encodeURIComponent(runId)}`);
|
|
425
|
+
return (data?.data ?? data);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Stream a workflow agent's pipeline as it executes.
|
|
429
|
+
*
|
|
430
|
+
* The unified endpoint answers POST /:org/:agent/stream with SSE:
|
|
431
|
+
* execution.started, node.started, node.output, node.completed,
|
|
432
|
+
* node.skipped, then execution.completed or execution.failed. Without
|
|
433
|
+
* this a multi-node pipeline is a blocking POST with nothing to show
|
|
434
|
+
* while it runs.
|
|
435
|
+
*/
|
|
436
|
+
async streamInvoke(input, handler, signal) {
|
|
437
|
+
await this.client.streamSSE(`${this.prefix}/stream`, handler, signal, {
|
|
438
|
+
method: 'POST',
|
|
439
|
+
body: JSON.stringify({ input }),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Stream run events via SSE. Calls handler for each event
|
|
444
|
+
* (llm.started, llm.chunk, llm.response, tool.started, tool.result,
|
|
445
|
+
* step.completed, run.completed, run.failed).
|
|
446
|
+
* Returns when the run completes or fails.
|
|
447
|
+
* Falls back to polling if SSE fails.
|
|
448
|
+
*/
|
|
449
|
+
async streamRun(runId, handler, signal) {
|
|
450
|
+
try {
|
|
451
|
+
await this.client.streamSSE(`${this.prefix}/runs/${encodeURIComponent(runId)}/stream`, handler, signal);
|
|
452
|
+
// Stream ended — get final state
|
|
453
|
+
return this.getRun(runId);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
// An abort is what the caller asked for, not a transport failure.
|
|
457
|
+
// Falling back to polling here kept a cancelled run under watch
|
|
458
|
+
// for the full five-minute poll window.
|
|
459
|
+
if (signal?.aborted || isAbortError(err))
|
|
460
|
+
throw err;
|
|
461
|
+
// SSE failed — fall back to polling until completion
|
|
462
|
+
return this.pollRun(runId, { signal });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async getConversationMessages(conversationId) {
|
|
466
|
+
const data = await this.client.request(`${this.prefix}/conversations/${encodeURIComponent(conversationId)}/messages`);
|
|
467
|
+
return data?.data ?? [];
|
|
468
|
+
}
|
|
469
|
+
async sendRunInput(runId, input) {
|
|
470
|
+
await this.client.request(`${this.prefix}/runs/${encodeURIComponent(runId)}/input`, { method: 'POST', body: JSON.stringify({ input }) });
|
|
471
|
+
}
|
|
472
|
+
async cancelRun(runId) {
|
|
473
|
+
await this.client.request(`${this.prefix}/runs/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Cancel a workflow execution.
|
|
477
|
+
*
|
|
478
|
+
* The workflow counterpart of cancelRun. A workflow run is an execution,
|
|
479
|
+
* not a run, so cancelRun could never stop one -- a Ctrl-C that did not
|
|
480
|
+
* also drop the SSE connection left the pipeline running and billing.
|
|
481
|
+
*/
|
|
482
|
+
async cancelExecution(executionId) {
|
|
483
|
+
await this.client.request(`${this.prefix}/executions/${encodeURIComponent(executionId)}/cancel`, { method: 'POST' });
|
|
484
|
+
}
|
|
485
|
+
async pollRun(runId, options = {}) {
|
|
486
|
+
const intervalMs = options.intervalMs ?? 1500;
|
|
487
|
+
const timeoutMs = options.timeoutMs ?? 5 * 60_000;
|
|
488
|
+
const deadline = Date.now() + timeoutMs;
|
|
489
|
+
let lastStepCount = -1;
|
|
490
|
+
while (Date.now() < deadline) {
|
|
491
|
+
if (options.signal?.aborted) {
|
|
492
|
+
throw Object.assign(new Error('Aborted'), { name: 'AbortError' });
|
|
493
|
+
}
|
|
494
|
+
const run = await this.getRun(runId);
|
|
495
|
+
if (Array.isArray(run.steps) && run.steps.length !== lastStepCount) {
|
|
496
|
+
lastStepCount = run.steps.length;
|
|
497
|
+
options.onStep?.(run);
|
|
498
|
+
}
|
|
499
|
+
if (run.status && (TERMINAL_STATUSES.has(run.status) || run.status === 'waiting_input')) {
|
|
500
|
+
return run;
|
|
501
|
+
}
|
|
502
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
503
|
+
}
|
|
504
|
+
throw Object.assign(new Error(`Run ${runId} did not finish within ${Math.round(timeoutMs / 1000)}s`), { runId, pollTimeout: true });
|
|
505
|
+
}
|
|
158
506
|
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -10,13 +10,40 @@ export interface StoredCredentials {
|
|
|
10
10
|
token: string;
|
|
11
11
|
email?: string;
|
|
12
12
|
frontendUrl?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When the token stops working, from the JWT's own `exp` claim.
|
|
15
|
+
*
|
|
16
|
+
* `@almyty/auth` writes it; this reader did not carry the field, so
|
|
17
|
+
* every CLI other than `auth` could not tell an expired credential
|
|
18
|
+
* from a live one and discovered the difference on its first API call
|
|
19
|
+
* — as a 401 from whatever the user was actually trying to do.
|
|
20
|
+
*/
|
|
21
|
+
expiresAt?: string;
|
|
13
22
|
}
|
|
23
|
+
/** Past its `exp`, treating a malformed or absent value as "no idea, assume live". */
|
|
24
|
+
export declare function credentialsExpired(creds: Pick<StoredCredentials, 'expiresAt'>): boolean;
|
|
14
25
|
export declare function loadCredentials(): StoredCredentials | null;
|
|
15
26
|
/**
|
|
16
|
-
* Resolve credentials from env or file. Returns null if nothing
|
|
27
|
+
* Resolve credentials from env or file. Returns null if nothing usable
|
|
28
|
+
* was found — an expired stored credential counts as nothing, because
|
|
29
|
+
* using it produces a 401 on the user's actual request rather than a
|
|
30
|
+
* sentence telling them to log in again.
|
|
31
|
+
*
|
|
32
|
+
* `ALMYTY_TOKEN` is never expiry-checked: it did not come from `auth
|
|
33
|
+
* login`, so there is no claim to check and no file to correct.
|
|
17
34
|
*/
|
|
18
35
|
export declare function resolveCredentials(): StoredCredentials | null;
|
|
19
36
|
/**
|
|
20
|
-
* Resolve credentials or exit
|
|
37
|
+
* Resolve credentials or exit.
|
|
38
|
+
*
|
|
39
|
+
* Exits 3, which is "not authenticated" in the exit-code table every
|
|
40
|
+
* almyty CLI shares (0 ok, 1 unexpected, 2 usage, 3 not authenticated,
|
|
41
|
+
* 4 not found, 5 the operation ran and failed). It exited 1 before, so
|
|
42
|
+
* a script could not tell a stale login from a crash.
|
|
21
43
|
*/
|
|
22
44
|
export declare function resolveCredentialsOrExit(): StoredCredentials;
|
|
45
|
+
/**
|
|
46
|
+
* Extract the default org slug from a JWT token.
|
|
47
|
+
* Returns null if the token isn't a JWT or has no orgs.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getOrgSlugFromToken(token: string): string | null;
|
package/dist/credentials.js
CHANGED
|
@@ -8,6 +8,13 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
export const CREDENTIALS_FILE = join(homedir(), '.almyty', 'credentials.json');
|
|
11
|
+
/** Past its `exp`, treating a malformed or absent value as "no idea, assume live". */
|
|
12
|
+
export function credentialsExpired(creds) {
|
|
13
|
+
if (!creds.expiresAt)
|
|
14
|
+
return false;
|
|
15
|
+
const at = Date.parse(creds.expiresAt);
|
|
16
|
+
return Number.isFinite(at) && at <= Date.now();
|
|
17
|
+
}
|
|
11
18
|
export function loadCredentials() {
|
|
12
19
|
try {
|
|
13
20
|
if (!existsSync(CREDENTIALS_FILE))
|
|
@@ -19,7 +26,13 @@ export function loadCredentials() {
|
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
|
-
* Resolve credentials from env or file. Returns null if nothing
|
|
29
|
+
* Resolve credentials from env or file. Returns null if nothing usable
|
|
30
|
+
* was found — an expired stored credential counts as nothing, because
|
|
31
|
+
* using it produces a 401 on the user's actual request rather than a
|
|
32
|
+
* sentence telling them to log in again.
|
|
33
|
+
*
|
|
34
|
+
* `ALMYTY_TOKEN` is never expiry-checked: it did not come from `auth
|
|
35
|
+
* login`, so there is no claim to check and no file to correct.
|
|
23
36
|
*/
|
|
24
37
|
export function resolveCredentials() {
|
|
25
38
|
const envToken = process.env.ALMYTY_TOKEN;
|
|
@@ -27,19 +40,57 @@ export function resolveCredentials() {
|
|
|
27
40
|
if (envToken)
|
|
28
41
|
return { url: envUrl, token: envToken };
|
|
29
42
|
const stored = loadCredentials();
|
|
30
|
-
if (stored?.token)
|
|
43
|
+
if (stored?.token && !credentialsExpired(stored))
|
|
31
44
|
return stored;
|
|
32
45
|
return null;
|
|
33
46
|
}
|
|
34
47
|
/**
|
|
35
|
-
* Resolve credentials or exit
|
|
48
|
+
* Resolve credentials or exit.
|
|
49
|
+
*
|
|
50
|
+
* Exits 3, which is "not authenticated" in the exit-code table every
|
|
51
|
+
* almyty CLI shares (0 ok, 1 unexpected, 2 usage, 3 not authenticated,
|
|
52
|
+
* 4 not found, 5 the operation ran and failed). It exited 1 before, so
|
|
53
|
+
* a script could not tell a stale login from a crash.
|
|
36
54
|
*/
|
|
37
55
|
export function resolveCredentialsOrExit() {
|
|
38
56
|
const creds = resolveCredentials();
|
|
39
57
|
if (creds)
|
|
40
58
|
return creds;
|
|
59
|
+
const stored = loadCredentials();
|
|
60
|
+
if (stored?.token && credentialsExpired(stored)) {
|
|
61
|
+
console.error(`Your login expired on ${stored.expiresAt}. Run:`);
|
|
62
|
+
console.error(' npx @almyty/auth login');
|
|
63
|
+
process.exit(3);
|
|
64
|
+
}
|
|
41
65
|
console.error('Not authenticated. Run one of:');
|
|
42
66
|
console.error(' npx @almyty/auth login');
|
|
43
67
|
console.error(' export ALMYTY_TOKEN=<your-token>');
|
|
44
|
-
process.exit(
|
|
68
|
+
process.exit(3);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract the default org slug from a JWT token.
|
|
72
|
+
* Returns null if the token isn't a JWT or has no orgs.
|
|
73
|
+
*/
|
|
74
|
+
export function getOrgSlugFromToken(token) {
|
|
75
|
+
try {
|
|
76
|
+
const parts = token.split('.');
|
|
77
|
+
if (parts.length !== 3)
|
|
78
|
+
return null;
|
|
79
|
+
let payload = parts[1];
|
|
80
|
+
payload += '='.repeat((4 - payload.length % 4) % 4);
|
|
81
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());
|
|
82
|
+
const orgs = decoded.organizations;
|
|
83
|
+
if (!Array.isArray(orgs) || !orgs.length)
|
|
84
|
+
return null;
|
|
85
|
+
// Use slug if available, otherwise derive from name
|
|
86
|
+
const org = orgs[0];
|
|
87
|
+
if (org.slug)
|
|
88
|
+
return org.slug;
|
|
89
|
+
if (org.name)
|
|
90
|
+
return org.name.toLowerCase().replace(/\s+/g, '-');
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
45
96
|
}
|
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, parseSseFrame, isAbortError } 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, credentialsExpired, 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, parseSseFrame, isAbortError } from './client.js';
|
|
2
|
+
export { loadCredentials, resolveCredentials, resolveCredentialsOrExit, getOrgSlugFromToken, credentialsExpired, CREDENTIALS_FILE, } from './credentials.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almyty/client",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
3
|
+
"version": "1.3.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,22 @@
|
|
|
20
23
|
"sdk"
|
|
21
24
|
],
|
|
22
25
|
"author": "almyty",
|
|
23
|
-
"license": "
|
|
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"
|
|
40
|
+
},
|
|
41
|
+
"overrides": {
|
|
42
|
+
"postcss": "^8.5.23"
|
|
28
43
|
}
|
|
29
44
|
}
|