@azmxailabs/agent-sdk 0.1.0 → 0.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 +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +157 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/openai.d.ts +36 -0
- package/dist/providers/openai.js +149 -0
- package/dist/security/deny-list.js +0 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -215,7 +215,7 @@ MIT — see [LICENSE](./LICENSE).
|
|
|
215
215
|
|
|
216
216
|
## About AZMX AI
|
|
217
217
|
|
|
218
|
-
AZMX AI is a native
|
|
218
|
+
AZMX AI is a native AI coding agent that runs on your machine, with your keys (BYOK across 11+ providers, or fully offline via Ollama / LM Studio). Every write is gated by per-call approval. No account, no telemetry. Free forever for individuals.
|
|
219
219
|
|
|
220
220
|
- Homepage: https://azmx.ai
|
|
221
221
|
- Docs: https://azmx.ai/docs
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* azmx — a headless AZMX agent CLI. Run a turn from the terminal / a pipe / CI,
|
|
4
|
+
* streaming the answer to stdout. The parity + scripting entry point the desktop
|
|
5
|
+
* app didn't have.
|
|
6
|
+
*
|
|
7
|
+
* azmx "explain this error" < build.log
|
|
8
|
+
* azmx code "write a python function to reverse a string"
|
|
9
|
+
* cat main.rs | azmx code "review this for bugs"
|
|
10
|
+
* azmx -m gpt-5.5 "summarize the news" # cloud BYOK
|
|
11
|
+
* AZMX_BASE_URL=http://localhost:8757/v1 azmx code "…" # sovereign REVA / local
|
|
12
|
+
*
|
|
13
|
+
* BYOK direct, no proxy, no telemetry — same primitives as the app (the SDK's
|
|
14
|
+
* ProviderRouter + provider adapters). Keys come from the environment.
|
|
15
|
+
*/
|
|
16
|
+
import { ProviderRouter } from "./providers/router.js";
|
|
17
|
+
import { AnthropicProvider } from "./providers/anthropic.js";
|
|
18
|
+
import { OpenAIProvider } from "./providers/openai.js";
|
|
19
|
+
const HELP = `azmx — headless AZMX agent CLI
|
|
20
|
+
|
|
21
|
+
USAGE
|
|
22
|
+
azmx [code] <prompt> run a turn; stream the answer to stdout
|
|
23
|
+
<cmd> | azmx [code] [prompt] read piped stdin as context (prompt optional)
|
|
24
|
+
|
|
25
|
+
OPTIONS
|
|
26
|
+
-m, --model <id> model id / local slot (default: provider default, or "sutra" for code)
|
|
27
|
+
-s, --system <text> override the system prompt
|
|
28
|
+
--base-url <url> OpenAI-compatible endpoint (REVA/Ollama/LM Studio/proxy)
|
|
29
|
+
--temperature <n> sampling temperature
|
|
30
|
+
--max-tokens <n> max output tokens
|
|
31
|
+
--no-stream buffer the whole reply, then print
|
|
32
|
+
-h, --help this help
|
|
33
|
+
|
|
34
|
+
PROVIDER (auto-detected from the environment, in order)
|
|
35
|
+
--base-url / AZMX_BASE_URL → OpenAI-compatible (key: AZMX_API_KEY, else "local")
|
|
36
|
+
ANTHROPIC_API_KEY → Anthropic
|
|
37
|
+
OPENAI_API_KEY → OpenAI
|
|
38
|
+
(else) http://127.0.0.1:8757/v1 → the sovereign REVA runtime, if it's running
|
|
39
|
+
|
|
40
|
+
'azmx code' adds a concise coding system prompt and defaults to a code model.`;
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const a = { code: false, stream: true, help: false, prompt: "" };
|
|
43
|
+
const rest = [];
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const t = argv[i];
|
|
46
|
+
const next = () => argv[++i];
|
|
47
|
+
if (t === "-h" || t === "--help")
|
|
48
|
+
a.help = true;
|
|
49
|
+
else if (t === "--no-stream")
|
|
50
|
+
a.stream = false;
|
|
51
|
+
else if (t === "-m" || t === "--model")
|
|
52
|
+
a.model = next();
|
|
53
|
+
else if (t === "-s" || t === "--system")
|
|
54
|
+
a.system = next();
|
|
55
|
+
else if (t === "--base-url")
|
|
56
|
+
a.baseUrl = next();
|
|
57
|
+
else if (t === "--temperature")
|
|
58
|
+
a.temperature = Number(next());
|
|
59
|
+
else if (t === "--max-tokens")
|
|
60
|
+
a.maxTokens = Number(next());
|
|
61
|
+
else if (t === "code" && rest.length === 0 && !a.prompt)
|
|
62
|
+
a.code = true;
|
|
63
|
+
else
|
|
64
|
+
rest.push(t);
|
|
65
|
+
}
|
|
66
|
+
a.prompt = rest.join(" ").trim();
|
|
67
|
+
return a;
|
|
68
|
+
}
|
|
69
|
+
async function readStdin() {
|
|
70
|
+
if (process.stdin.isTTY)
|
|
71
|
+
return "";
|
|
72
|
+
const chunks = [];
|
|
73
|
+
for await (const c of process.stdin)
|
|
74
|
+
chunks.push(c);
|
|
75
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
76
|
+
}
|
|
77
|
+
/** Pick a provider + model from flags/env. Returns the router alias to call with. */
|
|
78
|
+
function buildRouter(a) {
|
|
79
|
+
const router = new ProviderRouter();
|
|
80
|
+
const codeModelFallback = a.code ? "sutra" : undefined;
|
|
81
|
+
const baseUrl = a.baseUrl ?? process.env.AZMX_BASE_URL;
|
|
82
|
+
let provider;
|
|
83
|
+
let model;
|
|
84
|
+
if (baseUrl) {
|
|
85
|
+
model = a.model ?? process.env.AZMX_MODEL ?? codeModelFallback ?? "bija";
|
|
86
|
+
provider = new OpenAIProvider({ apiKey: process.env.AZMX_API_KEY || "local", model, baseUrl });
|
|
87
|
+
}
|
|
88
|
+
else if (process.env.ANTHROPIC_API_KEY) {
|
|
89
|
+
model = a.model ?? "claude-sonnet-4-6";
|
|
90
|
+
provider = new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY, model });
|
|
91
|
+
}
|
|
92
|
+
else if (process.env.OPENAI_API_KEY) {
|
|
93
|
+
model = a.model ?? (a.code ? "gpt-5.3-codex" : "gpt-5.5");
|
|
94
|
+
provider = new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY, model });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Sovereign default: the local REVA runtime (no key, offline). Works when the
|
|
98
|
+
// AZMX app / `reva serve` is up; otherwise the request fails with a clear error.
|
|
99
|
+
model = a.model ?? codeModelFallback ?? "bija";
|
|
100
|
+
provider = new OpenAIProvider({ apiKey: "local", model, baseUrl: "http://127.0.0.1:8757/v1" });
|
|
101
|
+
}
|
|
102
|
+
router.register(model, provider, { default: true });
|
|
103
|
+
return { router, model };
|
|
104
|
+
}
|
|
105
|
+
const CODE_SYSTEM = "You are AZMX, an expert coding agent. Be direct and correct. Prefer working code over prose; " +
|
|
106
|
+
"when you show code, make it complete and runnable. Match the style of any code the user shows.";
|
|
107
|
+
async function main() {
|
|
108
|
+
const a = parseArgs(process.argv.slice(2));
|
|
109
|
+
if (a.help) {
|
|
110
|
+
process.stdout.write(HELP + "\n");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const piped = await readStdin();
|
|
114
|
+
let userText = a.prompt;
|
|
115
|
+
if (piped)
|
|
116
|
+
userText = userText ? `${userText}\n\n---\n${piped}` : piped;
|
|
117
|
+
if (!userText) {
|
|
118
|
+
process.stderr.write("azmx: no prompt (pass one as an argument or pipe stdin). Try `azmx --help`.\n");
|
|
119
|
+
process.exitCode = 2;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const { router, model } = buildRouter(a);
|
|
123
|
+
const messages = [];
|
|
124
|
+
const system = a.system ?? (a.code ? CODE_SYSTEM : undefined);
|
|
125
|
+
if (system)
|
|
126
|
+
messages.push({ role: "system", content: system });
|
|
127
|
+
messages.push({ role: "user", content: userText });
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const onSig = () => controller.abort();
|
|
130
|
+
process.on("SIGINT", onSig);
|
|
131
|
+
process.on("SIGTERM", onSig);
|
|
132
|
+
const req = {
|
|
133
|
+
model,
|
|
134
|
+
messages,
|
|
135
|
+
temperature: a.temperature,
|
|
136
|
+
maxTokens: a.maxTokens,
|
|
137
|
+
signal: controller.signal,
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
if (a.stream) {
|
|
141
|
+
for await (const chunk of router.stream(req)) {
|
|
142
|
+
if (chunk.delta)
|
|
143
|
+
process.stdout.write(chunk.delta);
|
|
144
|
+
}
|
|
145
|
+
process.stdout.write("\n");
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const r = await router.complete(req);
|
|
149
|
+
process.stdout.write(r.text + "\n");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
process.stderr.write(`\nazmx: ${e.message}\n`);
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
void main();
|
package/dist/providers/index.js
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ChatRequest, ChatResponse, Provider, StreamChunk } from "./types.js";
|
|
2
|
+
export interface OpenAIProviderOptions {
|
|
3
|
+
/** Bearer key. For a no-auth local endpoint (REVA / Ollama / LM Studio) pass any placeholder. */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** Provider model id, e.g. "gpt-5.5", or a local slot like "sutra"/"bija" for REVA. */
|
|
6
|
+
model: string;
|
|
7
|
+
/**
|
|
8
|
+
* API base, including the version segment. Default OpenAI. Point this at any
|
|
9
|
+
* OpenAI-compatible server — REVA (`http://localhost:8757/v1`), Ollama
|
|
10
|
+
* (`http://localhost:11434/v1`), LM Studio, vLLM, OpenRouter, Groq, …
|
|
11
|
+
*/
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
defaultMaxTokens?: number;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* OpenAI Chat Completions adapter — POST {baseUrl}/chat/completions.
|
|
18
|
+
*
|
|
19
|
+
* BYOK direct (no AZMX proxy). Because the wire shape is the OpenAI standard,
|
|
20
|
+
* this single adapter also drives every OpenAI-compatible backend — including
|
|
21
|
+
* the sovereign REVA runtime and local Ollama/LM Studio — just by changing
|
|
22
|
+
* `baseUrl`. That's what lets the CLI run fully offline on your own models.
|
|
23
|
+
*/
|
|
24
|
+
export declare class OpenAIProvider implements Provider {
|
|
25
|
+
readonly name = "openai";
|
|
26
|
+
private apiKey;
|
|
27
|
+
private model;
|
|
28
|
+
private baseUrl;
|
|
29
|
+
private defaultMaxTokens?;
|
|
30
|
+
private fetch;
|
|
31
|
+
constructor(opts: OpenAIProviderOptions);
|
|
32
|
+
complete(req: ChatRequest): Promise<ChatResponse>;
|
|
33
|
+
stream(req: ChatRequest): AsyncIterable<StreamChunk>;
|
|
34
|
+
private buildBody;
|
|
35
|
+
private headers;
|
|
36
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Chat Completions adapter — POST {baseUrl}/chat/completions.
|
|
3
|
+
*
|
|
4
|
+
* BYOK direct (no AZMX proxy). Because the wire shape is the OpenAI standard,
|
|
5
|
+
* this single adapter also drives every OpenAI-compatible backend — including
|
|
6
|
+
* the sovereign REVA runtime and local Ollama/LM Studio — just by changing
|
|
7
|
+
* `baseUrl`. That's what lets the CLI run fully offline on your own models.
|
|
8
|
+
*/
|
|
9
|
+
export class OpenAIProvider {
|
|
10
|
+
name = "openai";
|
|
11
|
+
apiKey;
|
|
12
|
+
model;
|
|
13
|
+
baseUrl;
|
|
14
|
+
defaultMaxTokens;
|
|
15
|
+
fetch;
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
if (!opts.apiKey)
|
|
18
|
+
throw new Error("OpenAIProvider: apiKey is required (use a placeholder for a no-auth local endpoint)");
|
|
19
|
+
if (!opts.model)
|
|
20
|
+
throw new Error("OpenAIProvider: model is required");
|
|
21
|
+
this.apiKey = opts.apiKey;
|
|
22
|
+
this.model = opts.model;
|
|
23
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
24
|
+
this.defaultMaxTokens = opts.defaultMaxTokens;
|
|
25
|
+
this.fetch = opts.fetch ?? fetch;
|
|
26
|
+
}
|
|
27
|
+
async complete(req) {
|
|
28
|
+
const res = await this.fetch(`${this.baseUrl}/chat/completions`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: this.headers(),
|
|
31
|
+
body: JSON.stringify(this.buildBody(req, false)),
|
|
32
|
+
signal: req.signal,
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw await httpError(res);
|
|
36
|
+
const data = (await res.json());
|
|
37
|
+
const choice = data.choices?.[0];
|
|
38
|
+
return {
|
|
39
|
+
text: choice?.message?.content ?? "",
|
|
40
|
+
finishReason: choice?.finish_reason ?? "stop",
|
|
41
|
+
usage: data.usage
|
|
42
|
+
? { inputTokens: data.usage.prompt_tokens ?? 0, outputTokens: data.usage.completion_tokens ?? 0 }
|
|
43
|
+
: undefined,
|
|
44
|
+
raw: data,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async *stream(req) {
|
|
48
|
+
const res = await this.fetch(`${this.baseUrl}/chat/completions`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: this.headers(),
|
|
51
|
+
body: JSON.stringify(this.buildBody(req, true)),
|
|
52
|
+
signal: req.signal,
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw await httpError(res);
|
|
56
|
+
// Some OpenAI-compatible servers (e.g. the sovereign REVA runtime) ignore `stream:true`
|
|
57
|
+
// and return one buffered JSON completion. Detect that by content-type and yield it whole,
|
|
58
|
+
// so streaming callers still get the text instead of nothing.
|
|
59
|
+
const ctype = res.headers.get("content-type") ?? "";
|
|
60
|
+
if (!ctype.includes("event-stream")) {
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
const choice = data.choices?.[0];
|
|
63
|
+
const text = choice?.message?.content ?? "";
|
|
64
|
+
if (text)
|
|
65
|
+
yield { delta: text };
|
|
66
|
+
yield {
|
|
67
|
+
delta: "",
|
|
68
|
+
done: true,
|
|
69
|
+
finishReason: choice?.finish_reason ?? "stop",
|
|
70
|
+
usage: data.usage
|
|
71
|
+
? { inputTokens: data.usage.prompt_tokens ?? 0, outputTokens: data.usage.completion_tokens ?? 0 }
|
|
72
|
+
: undefined,
|
|
73
|
+
};
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!res.body)
|
|
77
|
+
throw new Error("OpenAIProvider: streaming response had no body");
|
|
78
|
+
let usage;
|
|
79
|
+
let finishReason;
|
|
80
|
+
for await (const data of sseData(res.body)) {
|
|
81
|
+
if (data === "[DONE]")
|
|
82
|
+
break;
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(data);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const choice = parsed.choices?.[0];
|
|
91
|
+
if (choice?.delta?.content)
|
|
92
|
+
yield { delta: choice.delta.content };
|
|
93
|
+
if (choice?.finish_reason)
|
|
94
|
+
finishReason = choice.finish_reason;
|
|
95
|
+
if (parsed.usage) {
|
|
96
|
+
usage = { inputTokens: parsed.usage.prompt_tokens ?? 0, outputTokens: parsed.usage.completion_tokens ?? 0 };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
yield { delta: "", done: true, finishReason: finishReason ?? "stop", usage };
|
|
100
|
+
}
|
|
101
|
+
buildBody(req, stream) {
|
|
102
|
+
return {
|
|
103
|
+
model: this.model,
|
|
104
|
+
messages: req.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
105
|
+
temperature: req.temperature,
|
|
106
|
+
max_tokens: req.maxTokens ?? this.defaultMaxTokens,
|
|
107
|
+
stop: req.stop,
|
|
108
|
+
stream,
|
|
109
|
+
...(req.providerOptions ?? {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
headers() {
|
|
113
|
+
return { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function httpError(res) {
|
|
117
|
+
let body = "";
|
|
118
|
+
try {
|
|
119
|
+
body = await res.text();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
}
|
|
124
|
+
return new Error(`OpenAI-compatible HTTP ${res.status}: ${body.slice(0, 500)}`);
|
|
125
|
+
}
|
|
126
|
+
/** Yield the `data:` payload of each SSE event from a streaming body. */
|
|
127
|
+
async function* sseData(body) {
|
|
128
|
+
const reader = body.getReader();
|
|
129
|
+
const decoder = new TextDecoder();
|
|
130
|
+
let buf = "";
|
|
131
|
+
while (true) {
|
|
132
|
+
const { value, done } = await reader.read();
|
|
133
|
+
if (done)
|
|
134
|
+
break;
|
|
135
|
+
buf += decoder.decode(value, { stream: true });
|
|
136
|
+
let nl;
|
|
137
|
+
while ((nl = buf.indexOf("\n\n")) !== -1) {
|
|
138
|
+
const chunk = buf.slice(0, nl);
|
|
139
|
+
buf = buf.slice(nl + 2);
|
|
140
|
+
let data = "";
|
|
141
|
+
for (const line of chunk.split("\n")) {
|
|
142
|
+
if (line.startsWith("data:"))
|
|
143
|
+
data += line.slice(5).trim();
|
|
144
|
+
}
|
|
145
|
+
if (data)
|
|
146
|
+
yield data;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azmxailabs/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Build approval-gated AI agents with the same primitives that power AZMX AI — BYOK provider router, approval gate, deny-list, hash-chained audit log. Secure by default, BYOK direct (no proxy), no telemetry.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Build approval-gated AI agents with the same primitives that power AZMX AI — BYOK provider router (Anthropic / OpenAI / any OpenAI-compatible incl. local REVA & Ollama), approval gate, deny-list, hash-chained audit log. Ships a headless `azmx` CLI. Secure by default, BYOK direct (no proxy), no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"azmx": "./dist/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"homepage": "https://azmx.ai",
|
|
8
11
|
"repository": {
|
|
9
12
|
"type": "git",
|