open-agents-ai 0.185.74 → 0.185.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +251 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -65550,6 +65550,9 @@ body {
65550
65550
  <div id="agent-events" style="font-size:0.78rem;line-height:1.5"></div>
65551
65551
  </div>
65552
65552
  <div id="jobs-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
65553
+ <div id="dashboard-health" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap"></div>
65554
+ <div id="dashboard-usage" style="margin-bottom:16px"></div>
65555
+ <h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Job History</h3>
65553
65556
  <div id="jobs-list" style="font-size:0.78rem"></div>
65554
65557
  </div>
65555
65558
 
@@ -65873,7 +65876,40 @@ async function abortAgentTask() {
65873
65876
  try { await fetch('/v1/runs/' + currentRunId, { method: 'DELETE', headers: headers() }); } catch {}
65874
65877
  }
65875
65878
 
65879
+ async function loadDashboard() {
65880
+ // Health card
65881
+ try {
65882
+ const r = await fetch('/health', { headers: headers() });
65883
+ const d = await r.json();
65884
+ document.getElementById('dashboard-health').innerHTML =
65885
+ '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
65886
+ '<div style="color:#555;font-size:0.6rem">STATUS</div>' +
65887
+ '<div style="color:#4ec94e;font-size:0.8rem">' + d.status + '</div></div>' +
65888
+ '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
65889
+ '<div style="color:#555;font-size:0.6rem">UPTIME</div>' +
65890
+ '<div style="color:#b2920a;font-size:0.8rem">' + Math.floor(d.uptime_s/60) + 'm</div></div>' +
65891
+ '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
65892
+ '<div style="color:#555;font-size:0.6rem">VERSION</div>' +
65893
+ '<div style="color:#b0b0b0;font-size:0.8rem">' + d.version + '</div></div>';
65894
+ } catch {}
65895
+ // Usage
65896
+ try {
65897
+ const r = await fetch('/v1/usage', { headers: headers() });
65898
+ const d = await r.json();
65899
+ document.getElementById('dashboard-usage').innerHTML =
65900
+ '<div style="display:flex;gap:12px;flex-wrap:wrap">' +
65901
+ '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1">' +
65902
+ '<div style="color:#555;font-size:0.6rem">TOKENS IN</div>' +
65903
+ '<div style="color:#b2920a;font-size:0.8rem">' + (d.totalTokensIn || 0).toLocaleString() + '</div></div>' +
65904
+ '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1">' +
65905
+ '<div style="color:#555;font-size:0.6rem">TOKENS OUT</div>' +
65906
+ '<div style="color:#b2920a;font-size:0.8rem">' + (d.totalTokensOut || 0).toLocaleString() + '</div></div>' +
65907
+ '</div>';
65908
+ } catch {}
65909
+ }
65910
+
65876
65911
  async function loadJobs() {
65912
+ loadDashboard();
65877
65913
  const list = document.getElementById('jobs-list');
65878
65914
  try {
65879
65915
  const r = await fetch('/v1/runs', { headers: headers() });
@@ -65941,6 +65977,98 @@ var init_logger = __esm({
65941
65977
  }
65942
65978
  });
65943
65979
 
65980
+ // packages/cli/dist/api/openapi.js
65981
+ function getOpenApiSpec() {
65982
+ return {
65983
+ openapi: "3.0.3",
65984
+ info: {
65985
+ title: "Open Agents REST API",
65986
+ description: "AI coding agent powered by open-weight models. Enterprise REST API for agentic task execution, OpenAI-compatible inference, and system management.",
65987
+ version: "0.185.75",
65988
+ license: { name: "CC-BY-NC-4.0", url: "https://creativecommons.org/licenses/by-nc/4.0/" }
65989
+ },
65990
+ servers: [{ url: "http://localhost:11435", description: "Local development" }],
65991
+ security: [{ BearerAuth: [] }],
65992
+ components: {
65993
+ securitySchemes: {
65994
+ BearerAuth: { type: "http", scheme: "bearer", description: "API key with scope (read/run/admin)" }
65995
+ }
65996
+ },
65997
+ paths: {
65998
+ "/health": { get: { summary: "Liveness probe", tags: ["Health"], security: [], responses: { 200: { description: "Server is alive" } } } },
65999
+ "/health/ready": { get: { summary: "Readiness probe (checks backend)", tags: ["Health"], security: [], responses: { 200: { description: "Backend reachable" }, 503: { description: "Backend unreachable" } } } },
66000
+ "/health/startup": { get: { summary: "Startup complete", tags: ["Health"], security: [], responses: { 200: { description: "Started" } } } },
66001
+ "/version": { get: { summary: "Version info", tags: ["Health"], security: [], responses: { 200: { description: "Version, node, platform" } } } },
66002
+ "/metrics": { get: { summary: "Prometheus metrics", tags: ["Health"], security: [], responses: { 200: { description: "Prometheus text format" } } } },
66003
+ "/v1/models": { get: { summary: "List available models", tags: ["Inference"], security: [{ BearerAuth: [] }], responses: { 200: { description: "OpenAI-format model list" } } } },
66004
+ "/v1/chat/completions": { post: { summary: "Chat completion (OpenAI-compatible)", tags: ["Inference"], security: [{ BearerAuth: [] }], requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["model", "messages"], properties: { model: { type: "string" }, messages: { type: "array" }, stream: { type: "boolean" }, max_tokens: { type: "integer" }, temperature: { type: "number" } } } } } }, responses: { 200: { description: "Chat response" } } } },
66005
+ "/v1/embeddings": { post: { summary: "Generate embeddings", tags: ["Inference"], responses: { 200: { description: "Embedding vectors" } } } },
66006
+ "/v1/run": { post: { summary: "Submit agentic task", tags: ["Agentic"], requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["task"], properties: { task: { type: "string" }, model: { type: "string" }, stream: { type: "boolean" }, profile: { type: "string" }, working_directory: { type: "string" }, isolate: { type: "boolean" } } } } } }, responses: { 202: { description: "Task accepted" } } } },
66007
+ "/v1/runs": { get: { summary: "List all runs", tags: ["Agentic"], parameters: [{ name: "limit", in: "query", schema: { type: "integer" } }, { name: "offset", in: "query", schema: { type: "integer" } }, { name: "status", in: "query", schema: { type: "string" } }], responses: { 200: { description: "Run list with pagination" } } } },
66008
+ "/v1/runs/{id}": { get: { summary: "Get run status", tags: ["Agentic"], parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { 200: { description: "Run details" }, 404: { description: "Not found" } } }, delete: { summary: "Abort run", tags: ["Agentic"], responses: { 200: { description: "Aborted" } } } },
66009
+ "/v1/config": { get: { summary: "Get configuration", tags: ["Config"], responses: { 200: { description: "Current config" } } }, patch: { summary: "Update configuration", tags: ["Config"], responses: { 200: { description: "Updated" } } } },
66010
+ "/v1/config/model": { get: { summary: "Current model", tags: ["Config"], responses: { 200: { description: "Model name" } } }, put: { summary: "Switch model", tags: ["Config"], responses: { 200: { description: "Switched" } } } },
66011
+ "/v1/config/endpoint": { get: { summary: "Current endpoint", tags: ["Config"], responses: { 200: { description: "Endpoint URL" } } }, put: { summary: "Switch endpoint", tags: ["Config"], responses: { 200: { description: "Switched" } } } },
66012
+ "/v1/usage": { get: { summary: "Token usage and rate limits", tags: ["Metering"], responses: { 200: { description: "Usage stats" } } } },
66013
+ "/v1/audit": { get: { summary: "Query audit log", tags: ["Audit"], parameters: [{ name: "since", in: "query", schema: { type: "string" } }, { name: "user", in: "query", schema: { type: "string" } }, { name: "limit", in: "query", schema: { type: "integer" } }], responses: { 200: { description: "Audit records" } } } },
66014
+ "/v1/commands": { get: { summary: "List slash commands", tags: ["Commands"], responses: { 200: { description: "Command list" } } } },
66015
+ "/v1/commands/{cmd}": { post: { summary: "Execute slash command", tags: ["Commands"], responses: { 200: { description: "Command output" } } } },
66016
+ "/v1/profiles": { get: { summary: "List tool profiles", tags: ["Profiles"], responses: { 200: { description: "Profile list" } } }, post: { summary: "Create profile", tags: ["Profiles"], responses: { 201: { description: "Created" } } } },
66017
+ "/v1/profiles/{name}": { get: { summary: "Get profile", tags: ["Profiles"], responses: { 200: { description: "Profile details" } } }, delete: { summary: "Delete profile", tags: ["Profiles"], responses: { 200: { description: "Deleted" } } } }
66018
+ }
66019
+ };
66020
+ }
66021
+ function getSwaggerUI() {
66022
+ return `<!DOCTYPE html><html><head><title>OA API Docs</title>
66023
+ <style>body{font-family:'SF Mono',monospace;background:#1a1a1e;color:#b0b0b0;margin:0;padding:20px}
66024
+ h1{color:#b2920a;font-size:1.2rem}h2{color:#b2920a;font-size:0.9rem;margin-top:20px}
66025
+ .endpoint{background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:10px;margin:8px 0}
66026
+ .method{font-weight:bold;padding:2px 8px;border-radius:2px;font-size:0.75rem}
66027
+ .get{color:#4ec94e}.post{color:#b2920a}.put{color:#4e94c9}.patch{color:#c9944e}.delete{color:#c94e4e}
66028
+ .path{color:#b0b0b0;margin-left:8px}.summary{color:#888;font-size:0.75rem;margin-left:12px}
66029
+ .tag{margin-top:16px;border-bottom:1px solid #2a2a30;padding-bottom:4px}
66030
+ a{color:#b2920a}
66031
+ </style></head><body>
66032
+ <h1>Open Agents API</h1>
66033
+ <p>Interactive docs. For full spec: <a href="/openapi.json">/openapi.json</a></p>
66034
+ <div id="docs"></div>
66035
+ <script>
66036
+ fetch('/openapi.json').then(r=>r.json()).then(spec=>{
66037
+ const docs=document.getElementById('docs');
66038
+ const tags={};
66039
+ for(const[path,methods]of Object.entries(spec.paths)){
66040
+ for(const[method,op]of Object.entries(methods)){
66041
+ const tag=(op.tags||['Other'])[0];
66042
+ if(!tags[tag])tags[tag]=[];
66043
+ tags[tag].push({method,path,summary:op.summary||''});
66044
+ }
66045
+ }
66046
+ for(const[tag,endpoints]of Object.entries(tags)){
66047
+ docs.innerHTML+='<div class="tag"><h2>'+tag+'</h2></div>';
66048
+ for(const ep of endpoints){
66049
+ docs.innerHTML+='<div class="endpoint"><span class="method '+ep.method+'">'+ep.method.toUpperCase()+'</span><span class="path">'+ep.path+'</span><span class="summary">'+ep.summary+'</span></div>';
66050
+ }
66051
+ }
66052
+ });
66053
+ </script></body></html>`;
66054
+ }
66055
+ var init_openapi = __esm({
66056
+ "packages/cli/dist/api/openapi.js"() {
66057
+ "use strict";
66058
+ }
66059
+ });
66060
+
66061
+ // packages/cli/dist/api/auth-oidc.js
66062
+ var OIDC_ISSUER, OIDC_AUDIENCE, OIDC_SCOPE_CLAIM;
66063
+ var init_auth_oidc = __esm({
66064
+ "packages/cli/dist/api/auth-oidc.js"() {
66065
+ "use strict";
66066
+ OIDC_ISSUER = process.env["OA_OIDC_ISSUER"] || "";
66067
+ OIDC_AUDIENCE = process.env["OA_OIDC_AUDIENCE"] || "";
66068
+ OIDC_SCOPE_CLAIM = process.env["OA_OIDC_SCOPE_CLAIM"] || "scope";
66069
+ }
66070
+ });
66071
+
65944
66072
  // packages/cli/dist/api/profiles.js
65945
66073
  import { existsSync as existsSync53, readFileSync as readFileSync42, writeFileSync as writeFileSync25, mkdirSync as mkdirSync27, readdirSync as readdirSync20, unlinkSync as unlinkSync12 } from "node:fs";
65946
66074
  import { join as join70 } from "node:path";
@@ -66462,6 +66590,39 @@ function listJobs() {
66462
66590
  }
66463
66591
  return jobs;
66464
66592
  }
66593
+ function getKeyUsage(user) {
66594
+ if (!perKeyUsage.has(user)) {
66595
+ perKeyUsage.set(user, { requestTimestamps: [], tokensToday: 0, tokenDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), activeJobs: 0 });
66596
+ }
66597
+ const u = perKeyUsage.get(user);
66598
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
66599
+ if (u.tokenDate !== today) {
66600
+ u.tokensToday = 0;
66601
+ u.tokenDate = today;
66602
+ }
66603
+ return u;
66604
+ }
66605
+ function checkKeyRateLimit(auth) {
66606
+ if (!auth.user || !auth.rpm)
66607
+ return null;
66608
+ const usage = getKeyUsage(auth.user);
66609
+ const now = Date.now();
66610
+ usage.requestTimestamps = usage.requestTimestamps.filter((t) => now - t < 6e4);
66611
+ if (auth.rpm && usage.requestTimestamps.length >= auth.rpm) {
66612
+ return `Rate limit exceeded for ${auth.user}: ${auth.rpm} RPM`;
66613
+ }
66614
+ if (auth.tpd && usage.tokensToday >= auth.tpd) {
66615
+ return `Daily token limit exceeded for ${auth.user}: ${usage.tokensToday}/${auth.tpd}`;
66616
+ }
66617
+ return null;
66618
+ }
66619
+ function recordKeyUsage(user, tokens) {
66620
+ if (!user)
66621
+ return;
66622
+ const usage = getKeyUsage(user);
66623
+ usage.requestTimestamps.push(Date.now());
66624
+ usage.tokensToday += tokens;
66625
+ }
66465
66626
  function resolveAuth(req) {
66466
66627
  const authHeader = req.headers["authorization"];
66467
66628
  const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
@@ -66470,9 +66631,17 @@ function resolveAuth(req) {
66470
66631
  if (!token)
66471
66632
  return { authenticated: false, scope: "read" };
66472
66633
  for (const entry of multiKeys.split(",")) {
66473
- const [key, scope, user] = entry.trim().split(":");
66634
+ const parts = entry.trim().split(":");
66635
+ const [key, scope, user, rpmStr, tpdStr, maxJobsStr] = parts;
66474
66636
  if (key === token) {
66475
- return { authenticated: true, scope: scope || "admin", user: user || void 0 };
66637
+ return {
66638
+ authenticated: true,
66639
+ scope: scope || "admin",
66640
+ user: user || void 0,
66641
+ rpm: rpmStr ? parseInt(rpmStr, 10) : void 0,
66642
+ tpd: tpdStr ? parseInt(tpdStr, 10) : void 0,
66643
+ maxJobs: maxJobsStr ? parseInt(maxJobsStr, 10) : void 0
66644
+ };
66476
66645
  }
66477
66646
  }
66478
66647
  return { authenticated: false, scope: "read" };
@@ -66835,6 +67004,20 @@ async function handleV1Run(req, res) {
66835
67004
  jsonResponse(res, 400, { error: "Missing required field: task" });
66836
67005
  return;
66837
67006
  }
67007
+ if (task.length > 5e4) {
67008
+ jsonResponse(res, 400, { error: "Task too long", message: "Max 50,000 characters", length: task.length });
67009
+ return;
67010
+ }
67011
+ const sandbox = requestBody["sandbox"] || process.env["OA_DEFAULT_SANDBOX"] || "none";
67012
+ if (sandbox === "container") {
67013
+ try {
67014
+ const { execSync: es } = __require("node:child_process");
67015
+ es("docker --version", { stdio: "pipe", timeout: 5e3 });
67016
+ } catch {
67017
+ jsonResponse(res, 400, { error: "Container sandbox unavailable", message: "Docker not found. Install Docker or use sandbox:'none'." });
67018
+ return;
67019
+ }
67020
+ }
66838
67021
  const id = `job-${randomBytes16(3).toString("hex")}`;
66839
67022
  const dir = jobsDir();
66840
67023
  const workingDir = requestBody["working_directory"] || req.headers["x-working-directory"];
@@ -66982,9 +67165,17 @@ async function handleV1Run(req, res) {
66982
67165
  jsonResponse(res, 202, { run_id: id, status: "running", pid: job.pid });
66983
67166
  }
66984
67167
  }
66985
- function handleV1Runs(res) {
66986
- const jobs = listJobs();
66987
- jsonResponse(res, 200, { runs: jobs });
67168
+ function handleV1Runs(res, url) {
67169
+ let jobs = listJobs();
67170
+ const statusFilter = url?.searchParams.get("status");
67171
+ if (statusFilter)
67172
+ jobs = jobs.filter((j) => j.status === statusFilter);
67173
+ jobs.sort((a, b) => new Date(b.startedAt ?? 0).getTime() - new Date(a.startedAt ?? 0).getTime());
67174
+ const limit = parseInt(url?.searchParams.get("limit") ?? "50", 10);
67175
+ const offset = parseInt(url?.searchParams.get("offset") ?? "0", 10);
67176
+ const total = jobs.length;
67177
+ jobs = jobs.slice(offset, offset + limit);
67178
+ jsonResponse(res, 200, { runs: jobs, total, limit, offset });
66988
67179
  }
66989
67180
  function handleV1RunsById(res, id) {
66990
67181
  const job = loadJob(id);
@@ -67193,6 +67384,15 @@ async function handleRequest(req, res, ollamaUrl, verbose) {
67193
67384
  handleMetrics(res);
67194
67385
  return;
67195
67386
  }
67387
+ if (pathname === "/openapi.json" && method === "GET") {
67388
+ jsonResponse(res, 200, getOpenApiSpec());
67389
+ return;
67390
+ }
67391
+ if (pathname === "/docs" && method === "GET") {
67392
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
67393
+ res.end(getSwaggerUI());
67394
+ return;
67395
+ }
67196
67396
  if (pathname === "/" && method === "GET" && req.headers.accept?.includes("text/html")) {
67197
67397
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
67198
67398
  res.end(getWebUI());
@@ -67208,6 +67408,23 @@ async function handleRequest(req, res, ollamaUrl, verbose) {
67208
67408
  status = 401;
67209
67409
  return;
67210
67410
  }
67411
+ const auth = resolveAuth(req);
67412
+ const rateLimitMsg = checkKeyRateLimit(auth);
67413
+ if (rateLimitMsg) {
67414
+ status = 429;
67415
+ jsonResponse(res, 429, { error: "Too Many Requests", message: rateLimitMsg });
67416
+ return;
67417
+ }
67418
+ if (auth.user)
67419
+ recordKeyUsage(auth.user, 0);
67420
+ }
67421
+ if ((method === "POST" || method === "PUT" || method === "PATCH") && req.headers["content-length"]) {
67422
+ const bodySize = parseInt(req.headers["content-length"], 10);
67423
+ if (bodySize > 1048576) {
67424
+ status = 413;
67425
+ jsonResponse(res, 413, { error: "Payload Too Large", message: "Request body exceeds 1MB limit" });
67426
+ return;
67427
+ }
67211
67428
  }
67212
67429
  if (pathname === "/v1/models" && method === "GET") {
67213
67430
  await handleV1Models(res, ollamaUrl);
@@ -67226,7 +67443,7 @@ async function handleRequest(req, res, ollamaUrl, verbose) {
67226
67443
  return;
67227
67444
  }
67228
67445
  if (pathname === "/v1/runs" && method === "GET") {
67229
- handleV1Runs(res);
67446
+ handleV1Runs(res, urlObj);
67230
67447
  return;
67231
67448
  }
67232
67449
  const runsMatch = pathname.match(/^\/v1\/runs\/([a-zA-Z0-9_-]+)$/);
@@ -67425,6 +67642,30 @@ function startApiServer(options = {}) {
67425
67642
  const ollamaUrl = options.ollamaUrl ?? config.backendUrl;
67426
67643
  const cwd4 = process.cwd();
67427
67644
  initAuditLog(join71(cwd4, ".oa"));
67645
+ const retentionDays = parseInt(process.env["OA_JOB_RETENTION_DAYS"] ?? "30", 10);
67646
+ if (retentionDays > 0) {
67647
+ try {
67648
+ const jobsDir3 = join71(cwd4, ".oa", "jobs");
67649
+ if (existsSync54(jobsDir3)) {
67650
+ const cutoff = Date.now() - retentionDays * 864e5;
67651
+ for (const f of readdirSync21(jobsDir3)) {
67652
+ if (!f.endsWith(".json"))
67653
+ continue;
67654
+ try {
67655
+ const jobPath = join71(jobsDir3, f);
67656
+ const job = JSON.parse(readFileSync43(jobPath, "utf-8"));
67657
+ const jobTime = new Date(job.startedAt ?? job.completedAt ?? 0).getTime();
67658
+ if (jobTime > 0 && jobTime < cutoff && job.status !== "running") {
67659
+ const { unlinkSync: unlinkSync13 } = __require("node:fs");
67660
+ unlinkSync13(jobPath);
67661
+ }
67662
+ } catch {
67663
+ }
67664
+ }
67665
+ }
67666
+ } catch {
67667
+ }
67668
+ }
67428
67669
  const tlsCert = process.env["OA_TLS_CERT"];
67429
67670
  const tlsKey = process.env["OA_TLS_KEY"];
67430
67671
  const useTls = !!(tlsCert && tlsKey);
@@ -67573,7 +67814,7 @@ async function apiServeCommand(opts, config) {
67573
67814
  server.on("close", resolve36);
67574
67815
  });
67575
67816
  }
67576
- var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses;
67817
+ var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses, perKeyUsage;
67577
67818
  var init_serve = __esm({
67578
67819
  "packages/cli/dist/api/serve.js"() {
67579
67820
  "use strict";
@@ -67581,6 +67822,8 @@ var init_serve = __esm({
67581
67822
  init_audit_log();
67582
67823
  init_web_ui();
67583
67824
  init_logger();
67825
+ init_openapi();
67826
+ init_auth_oidc();
67584
67827
  init_oa_directory();
67585
67828
  init_render();
67586
67829
  init_profiles();
@@ -67597,6 +67840,7 @@ var init_serve = __esm({
67597
67840
  _corsOrigins = (process.env["OA_CORS_ORIGINS"] || "").split(",").filter(Boolean);
67598
67841
  _corsLocalOnly = _corsOrigins.length === 0;
67599
67842
  runningProcesses = /* @__PURE__ */ new Map();
67843
+ perKeyUsage = /* @__PURE__ */ new Map();
67600
67844
  }
67601
67845
  });
67602
67846
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.185.74",
3
+ "version": "0.185.76",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",