nuabase 2.3.5 → 2.3.7

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 CHANGED
@@ -172,7 +172,7 @@ Every call returns a discriminated union. When `success` is `false`, you get an
172
172
  | `engine` | `'http'` \| `'local-agent'` | Whether an LLM provider's API or a coding agent on this machine answered. |
173
173
  | `schemaEnforcement` | `'in-prompt'` \| `'native'` | Whether the schema was described in the prompt or enforced by the engine. Output is validated either way. |
174
174
  | `provider` | `string` | `engine: 'http'` only. The LLM provider. |
175
- | `agent` | `'claude-code'` \| `'codex'` | `engine: 'local-agent'` only. Which coding agent answered. |
175
+ | `agent` | `'claude-code'` \| `'codex'` \| `'pi'` | `engine: 'local-agent'` only. Which coding agent answered. |
176
176
  | `costUsdEstimate` | `number \| undefined` | `engine: 'local-agent'` only. The agent's own estimate. On a subscription this is not what you pay. |
177
177
 
178
178
  **Error**
@@ -233,13 +233,13 @@ const agents = await detectLocalAgents();
233
233
  // [{ agent: 'claude-code', installed: true, loggedIn: true, version: '...', binaryPath: '...' }, ...]
234
234
  ```
235
235
 
236
- This runs a coding-agent CLI already installed and logged in on this machine, in its headless mode (`claude -p` or `codex exec`). Calls use the user's own Claude or ChatGPT subscription, so no API key is needed. `get()` and `list()` behave as with providers: the output is validated against your schema and typed the same way. Queue operations are not supported. `nuabase/local-agent` is a separate entry point so that browser bundles of `nuabase` never include it.
236
+ This runs a coding-agent CLI already installed and logged in on this machine, in its headless mode (`claude -p`, `codex exec`, or `pi --mode json`). Calls use the user's own logged-in subscription or provider login, so no API key is needed. `get()` and `list()` behave as with providers: the output is validated against your schema and typed the same way. Queue operations are not supported. `nuabase/local-agent` is a separate entry point so that browser bundles of `nuabase` never include it.
237
237
 
238
238
  The agent is set up once, when you create it. If you install an agent or log in after that, create a new one.
239
239
 
240
240
  **`localAgent(config)`** sets up a named agent. It throws right away if the agent's executable cannot be found; if the agent is not logged in, the first call fails with that error.
241
241
 
242
- - `agent: 'claude-code' | 'codex'`
242
+ - `agent: 'claude-code' | 'codex' | 'pi'`
243
243
  - `binaryPath?: string` – Path to the agent executable. Defaults to looking it up on `PATH`. On Windows, point this at a native executable; `.cmd` shims are not supported.
244
244
  - and the options below.
245
245
 
@@ -247,13 +247,13 @@ The agent is set up once, when you create it. If you install an agent or log in
247
247
 
248
248
  Options for both:
249
249
 
250
- - `model?: string` – The agent's own model name, e.g. `'haiku'` for Claude Code, for calls that name no model. Defaults to the agent's default. Per-call `model: { alias }` works for aliases the agent understands (`haiku`, `sonnet`, `opus` for Claude Code).
250
+ - `model?: string` – The agent's own model name, e.g. `'haiku'` for Claude Code, for calls that name no model. Defaults to the agent's default. Per-call `model: { alias }` works for aliases the agent understands (`haiku`, `sonnet`, `opus` for Claude Code). Codex and Pi have no aliases: pass one of their own model names, and for Pi a `provider/id` name such as `'openrouter/z-ai/glm-5.2'`.
251
251
  - `timeoutMs?: number` – Per-call limit. Defaults to `180000`.
252
252
  - `concurrency?: number` – Maximum agent processes at once. Defaults to `2`.
253
253
  - `auth?: 'subscription' | 'inherit'` – `subscription` (default) hides `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` and similar variables from the agent so it bills the logged-in plan rather than an API key.
254
254
  - `logger?` – An object with `debug`, `info`, `warn` and `error` methods, where calls and retries are logged. Defaults to the console.
255
255
 
256
- How it works: each call runs the agent in a fresh, empty temporary directory with its tools, project settings and MCP servers turned off. The schema is wrapped as `{ value: ... }` (the agents require an object at the top level) and passed to the agent's own schema option. Codex only accepts OpenAI strict-mode schemas, so a schema with optional properties or open-ended objects is instead described in the prompt, and the output is validated as usual. If an agent rejects a schema, the call is retried with the schema in the prompt. `maxTokens` has no CLI equivalent and is ignored. Calls count against the user's plan limits; check each vendor's terms before offering this to other users.
256
+ How it works: each call runs the agent in a fresh, empty temporary directory with its tools, project settings and MCP servers turned off. The schema is wrapped as `{ value: ... }` (the agents require an object at the top level) and passed to the agent's own schema option. Codex only accepts OpenAI strict-mode schemas, so a schema with optional properties or open-ended objects is instead described in the prompt, and the output is validated as usual. Pi has no schema flag at all, so its schemas are always described in the prompt. If an agent rejects a schema, the call is retried with the schema in the prompt. `maxTokens` has no CLI equivalent and is ignored. Calls count against the user's plan limits; check each vendor's terms before offering this to other users.
257
257
 
258
258
  ### Methods
259
259
 
@@ -367,6 +367,161 @@ ${result.stderr}`.trim();
367
367
  return codex;
368
368
  }
369
369
 
370
+ var pi = {};
371
+
372
+ var hasRequiredPi;
373
+ function requirePi() {
374
+ if (hasRequiredPi) return pi;
375
+ hasRequiredPi = 1;
376
+ Object.defineProperty(pi, "__esModule", { value: true });
377
+ pi.piAdapter = void 0;
378
+ const failure_classification_1 = requireFailureClassification();
379
+ const SYSTEM_PROMPT = [
380
+ "You are a data transformation function called by a program.",
381
+ "You have no tools. Follow the instructions in the user message exactly",
382
+ "and reply only with the requested JSON."
383
+ ].join(" ");
384
+ pi.piAdapter = {
385
+ id: "pi",
386
+ binaryName: "pi",
387
+ // Every environment credential Pi's providers recognize. Under `auth:
388
+ // "subscription"` these are removed so Pi bills its own `auth.json` login.
389
+ apiKeyEnvVars: [
390
+ "AI_GATEWAY_API_KEY",
391
+ "ANT_LING_API_KEY",
392
+ "ANTHROPIC_API_KEY",
393
+ "ANTHROPIC_AUTH_TOKEN",
394
+ "ANTHROPIC_OAUTH_TOKEN",
395
+ "AWS_ACCESS_KEY_ID",
396
+ "AWS_SECRET_ACCESS_KEY",
397
+ "AWS_SESSION_TOKEN",
398
+ "AZURE_OPENAI_API_KEY",
399
+ "BASETEN_API_KEY",
400
+ "CEREBRAS_API_KEY",
401
+ "CLOUDFLARE_API_KEY",
402
+ "COPILOT_GITHUB_TOKEN",
403
+ "DEEPSEEK_API_KEY",
404
+ "FIREWORKS_API_KEY",
405
+ "GEMINI_API_KEY",
406
+ "GOOGLE_API_KEY",
407
+ "GOOGLE_CLOUD_API_KEY",
408
+ "GROQ_API_KEY",
409
+ "HF_TOKEN",
410
+ "KIMI_API_KEY",
411
+ "META_API_KEY",
412
+ "MINIMAX_API_KEY",
413
+ "MINIMAX_CN_API_KEY",
414
+ "MISTRAL_API_KEY",
415
+ "MOONSHOT_API_KEY",
416
+ "NVIDIA_API_KEY",
417
+ "OPENAI_API_KEY",
418
+ "OPENCODE_API_KEY",
419
+ "OPENROUTER_API_KEY",
420
+ "QWEN_TOKEN_PLAN_API_KEY",
421
+ "QWEN_TOKEN_PLAN_CN_API_KEY",
422
+ "RADIUS_API_KEY",
423
+ "TOGETHER_API_KEY",
424
+ "XAI_API_KEY",
425
+ "XIAOMI_API_KEY",
426
+ "XIAOMI_TOKEN_PLAN_AMS_API_KEY",
427
+ "XIAOMI_TOKEN_PLAN_CN_API_KEY",
428
+ "XIAOMI_TOKEN_PLAN_SGP_API_KEY",
429
+ "ZAI_API_KEY",
430
+ "ZAI_CODING_CN_API_KEY"
431
+ ],
432
+ // Pi uses its own `provider/id` model names; set one with localAgent({ model }).
433
+ modelAliases: {},
434
+ nativeSchema() {
435
+ return null;
436
+ },
437
+ buildInvocation(request, model) {
438
+ const args = [
439
+ "--mode",
440
+ "json",
441
+ "--no-session",
442
+ "--no-tools",
443
+ "--no-builtin-tools",
444
+ "--no-extensions",
445
+ "--no-skills",
446
+ "--no-prompt-templates",
447
+ "--no-themes",
448
+ "--no-context-files",
449
+ "--system-prompt",
450
+ SYSTEM_PROMPT,
451
+ "--offline"
452
+ ];
453
+ if (model !== void 0) {
454
+ args.push("--model", model);
455
+ }
456
+ return { args, stdin: request.prompt };
457
+ },
458
+ readResult(result, request) {
459
+ const events = parseEvents(result.stdout);
460
+ const assistant = events.filter((event) => event.type === "message_end" && event.message?.role === "assistant").map((event) => event.message).pop();
461
+ if (assistant && (assistant.stopReason === "error" || assistant.errorMessage)) {
462
+ return failed((0, failure_classification_1.classifyAgentFailure)("pi", assistant.errorMessage ?? "pi stopped with an error"));
463
+ }
464
+ const retryFailed = events.find((event) => event.type === "auto_retry_end" && event.success === false);
465
+ if (retryFailed) {
466
+ return failed((0, failure_classification_1.classifyAgentFailure)("pi", retryFailed.finalError ?? "auto retry failed"));
467
+ }
468
+ if (!assistant) {
469
+ return failed((0, failure_classification_1.classifyAgentFailure)("pi", `exited with code ${result.exitCode} without a reply: ${(0, failure_classification_1.stderrTail)(result)}`));
470
+ }
471
+ const text = (assistant.content ?? []).filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
472
+ const usage = assistant.usage ?? {};
473
+ const promptTokens = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
474
+ const completionTokens = usage.output ?? 0;
475
+ const reported = {
476
+ usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens },
477
+ model: assistant.model,
478
+ costUsdEstimate: usage.cost?.total
479
+ };
480
+ if (request.enforcement.kind === "in-prompt") {
481
+ return { kind: "answered", answer: { kind: "text", text }, ...reported };
482
+ }
483
+ let envelope;
484
+ try {
485
+ envelope = JSON.parse(text);
486
+ } catch {
487
+ return failed({ kind: "transient", message: `pi reply is not JSON: ${text}` });
488
+ }
489
+ return { kind: "answered", answer: { kind: "structured", envelope }, ...reported };
490
+ },
491
+ // Pi has no provider-agnostic login check: `pi auth check` requires --provider
492
+ // or --model, and the provider differs per user (it is not known until Pi's
493
+ // settings are read). Report installed Pi as logged in; the first real call
494
+ // surfaces any auth error, which classifyAgentFailure marks fatal.
495
+ loginStatusArgs: ["--version"],
496
+ readLoginStatus(result) {
497
+ return {
498
+ loggedIn: result.exitCode === 0,
499
+ detail: "installed; auth is verified on the first call"
500
+ };
501
+ }
502
+ };
503
+ function failed(failure) {
504
+ return { kind: "failed", failure };
505
+ }
506
+ function parseEvents(stdout) {
507
+ const events = [];
508
+ for (const line of stdout.split("\n")) {
509
+ const trimmed = line.replace(/\r$/, "").trim();
510
+ if (!trimmed.startsWith("{"))
511
+ continue;
512
+ try {
513
+ const parsed = JSON.parse(trimmed);
514
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
515
+ events.push(parsed);
516
+ }
517
+ } catch {
518
+ }
519
+ }
520
+ return events;
521
+ }
522
+ return pi;
523
+ }
524
+
370
525
  var hasRequiredAdapters;
371
526
  function requireAdapters() {
372
527
  if (hasRequiredAdapters) return adapters;
@@ -375,9 +530,11 @@ function requireAdapters() {
375
530
  adapters.AGENT_ADAPTERS = void 0;
376
531
  const claude_code_1 = requireClaudeCode();
377
532
  const codex_1 = requireCodex();
533
+ const pi_1 = requirePi();
378
534
  adapters.AGENT_ADAPTERS = {
379
535
  "claude-code": claude_code_1.claudeCodeAdapter,
380
- codex: codex_1.codexAdapter
536
+ codex: codex_1.codexAdapter,
537
+ pi: pi_1.piAdapter
381
538
  };
382
539
  return adapters;
383
540
  }
@@ -392,7 +549,7 @@ function requireAgentId() {
392
549
  Object.defineProperty(exports$1, "__esModule", { value: true });
393
550
  exports$1.LOCAL_AGENT_IDS = void 0;
394
551
  exports$1.isLocalAgentId = isLocalAgentId;
395
- exports$1.LOCAL_AGENT_IDS = ["claude-code", "codex"];
552
+ exports$1.LOCAL_AGENT_IDS = ["claude-code", "codex", "pi"];
396
553
  const LOCAL_AGENT_ID_SET = new Set(exports$1.LOCAL_AGENT_IDS);
397
554
  function isLocalAgentId(value) {
398
555
  return typeof value === "string" && LOCAL_AGENT_ID_SET.has(value);
@@ -364,6 +364,161 @@ ${result.stderr}`.trim();
364
364
  return codex;
365
365
  }
366
366
 
367
+ var pi = {};
368
+
369
+ var hasRequiredPi;
370
+ function requirePi() {
371
+ if (hasRequiredPi) return pi;
372
+ hasRequiredPi = 1;
373
+ Object.defineProperty(pi, "__esModule", { value: true });
374
+ pi.piAdapter = void 0;
375
+ const failure_classification_1 = requireFailureClassification();
376
+ const SYSTEM_PROMPT = [
377
+ "You are a data transformation function called by a program.",
378
+ "You have no tools. Follow the instructions in the user message exactly",
379
+ "and reply only with the requested JSON."
380
+ ].join(" ");
381
+ pi.piAdapter = {
382
+ id: "pi",
383
+ binaryName: "pi",
384
+ // Every environment credential Pi's providers recognize. Under `auth:
385
+ // "subscription"` these are removed so Pi bills its own `auth.json` login.
386
+ apiKeyEnvVars: [
387
+ "AI_GATEWAY_API_KEY",
388
+ "ANT_LING_API_KEY",
389
+ "ANTHROPIC_API_KEY",
390
+ "ANTHROPIC_AUTH_TOKEN",
391
+ "ANTHROPIC_OAUTH_TOKEN",
392
+ "AWS_ACCESS_KEY_ID",
393
+ "AWS_SECRET_ACCESS_KEY",
394
+ "AWS_SESSION_TOKEN",
395
+ "AZURE_OPENAI_API_KEY",
396
+ "BASETEN_API_KEY",
397
+ "CEREBRAS_API_KEY",
398
+ "CLOUDFLARE_API_KEY",
399
+ "COPILOT_GITHUB_TOKEN",
400
+ "DEEPSEEK_API_KEY",
401
+ "FIREWORKS_API_KEY",
402
+ "GEMINI_API_KEY",
403
+ "GOOGLE_API_KEY",
404
+ "GOOGLE_CLOUD_API_KEY",
405
+ "GROQ_API_KEY",
406
+ "HF_TOKEN",
407
+ "KIMI_API_KEY",
408
+ "META_API_KEY",
409
+ "MINIMAX_API_KEY",
410
+ "MINIMAX_CN_API_KEY",
411
+ "MISTRAL_API_KEY",
412
+ "MOONSHOT_API_KEY",
413
+ "NVIDIA_API_KEY",
414
+ "OPENAI_API_KEY",
415
+ "OPENCODE_API_KEY",
416
+ "OPENROUTER_API_KEY",
417
+ "QWEN_TOKEN_PLAN_API_KEY",
418
+ "QWEN_TOKEN_PLAN_CN_API_KEY",
419
+ "RADIUS_API_KEY",
420
+ "TOGETHER_API_KEY",
421
+ "XAI_API_KEY",
422
+ "XIAOMI_API_KEY",
423
+ "XIAOMI_TOKEN_PLAN_AMS_API_KEY",
424
+ "XIAOMI_TOKEN_PLAN_CN_API_KEY",
425
+ "XIAOMI_TOKEN_PLAN_SGP_API_KEY",
426
+ "ZAI_API_KEY",
427
+ "ZAI_CODING_CN_API_KEY"
428
+ ],
429
+ // Pi uses its own `provider/id` model names; set one with localAgent({ model }).
430
+ modelAliases: {},
431
+ nativeSchema() {
432
+ return null;
433
+ },
434
+ buildInvocation(request, model) {
435
+ const args = [
436
+ "--mode",
437
+ "json",
438
+ "--no-session",
439
+ "--no-tools",
440
+ "--no-builtin-tools",
441
+ "--no-extensions",
442
+ "--no-skills",
443
+ "--no-prompt-templates",
444
+ "--no-themes",
445
+ "--no-context-files",
446
+ "--system-prompt",
447
+ SYSTEM_PROMPT,
448
+ "--offline"
449
+ ];
450
+ if (model !== void 0) {
451
+ args.push("--model", model);
452
+ }
453
+ return { args, stdin: request.prompt };
454
+ },
455
+ readResult(result, request) {
456
+ const events = parseEvents(result.stdout);
457
+ const assistant = events.filter((event) => event.type === "message_end" && event.message?.role === "assistant").map((event) => event.message).pop();
458
+ if (assistant && (assistant.stopReason === "error" || assistant.errorMessage)) {
459
+ return failed((0, failure_classification_1.classifyAgentFailure)("pi", assistant.errorMessage ?? "pi stopped with an error"));
460
+ }
461
+ const retryFailed = events.find((event) => event.type === "auto_retry_end" && event.success === false);
462
+ if (retryFailed) {
463
+ return failed((0, failure_classification_1.classifyAgentFailure)("pi", retryFailed.finalError ?? "auto retry failed"));
464
+ }
465
+ if (!assistant) {
466
+ return failed((0, failure_classification_1.classifyAgentFailure)("pi", `exited with code ${result.exitCode} without a reply: ${(0, failure_classification_1.stderrTail)(result)}`));
467
+ }
468
+ const text = (assistant.content ?? []).filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
469
+ const usage = assistant.usage ?? {};
470
+ const promptTokens = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
471
+ const completionTokens = usage.output ?? 0;
472
+ const reported = {
473
+ usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens },
474
+ model: assistant.model,
475
+ costUsdEstimate: usage.cost?.total
476
+ };
477
+ if (request.enforcement.kind === "in-prompt") {
478
+ return { kind: "answered", answer: { kind: "text", text }, ...reported };
479
+ }
480
+ let envelope;
481
+ try {
482
+ envelope = JSON.parse(text);
483
+ } catch {
484
+ return failed({ kind: "transient", message: `pi reply is not JSON: ${text}` });
485
+ }
486
+ return { kind: "answered", answer: { kind: "structured", envelope }, ...reported };
487
+ },
488
+ // Pi has no provider-agnostic login check: `pi auth check` requires --provider
489
+ // or --model, and the provider differs per user (it is not known until Pi's
490
+ // settings are read). Report installed Pi as logged in; the first real call
491
+ // surfaces any auth error, which classifyAgentFailure marks fatal.
492
+ loginStatusArgs: ["--version"],
493
+ readLoginStatus(result) {
494
+ return {
495
+ loggedIn: result.exitCode === 0,
496
+ detail: "installed; auth is verified on the first call"
497
+ };
498
+ }
499
+ };
500
+ function failed(failure) {
501
+ return { kind: "failed", failure };
502
+ }
503
+ function parseEvents(stdout) {
504
+ const events = [];
505
+ for (const line of stdout.split("\n")) {
506
+ const trimmed = line.replace(/\r$/, "").trim();
507
+ if (!trimmed.startsWith("{"))
508
+ continue;
509
+ try {
510
+ const parsed = JSON.parse(trimmed);
511
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
512
+ events.push(parsed);
513
+ }
514
+ } catch {
515
+ }
516
+ }
517
+ return events;
518
+ }
519
+ return pi;
520
+ }
521
+
367
522
  var hasRequiredAdapters;
368
523
  function requireAdapters() {
369
524
  if (hasRequiredAdapters) return adapters;
@@ -372,9 +527,11 @@ function requireAdapters() {
372
527
  adapters.AGENT_ADAPTERS = void 0;
373
528
  const claude_code_1 = requireClaudeCode();
374
529
  const codex_1 = requireCodex();
530
+ const pi_1 = requirePi();
375
531
  adapters.AGENT_ADAPTERS = {
376
532
  "claude-code": claude_code_1.claudeCodeAdapter,
377
- codex: codex_1.codexAdapter
533
+ codex: codex_1.codexAdapter,
534
+ pi: pi_1.piAdapter
378
535
  };
379
536
  return adapters;
380
537
  }
@@ -389,7 +546,7 @@ function requireAgentId() {
389
546
  Object.defineProperty(exports$1, "__esModule", { value: true });
390
547
  exports$1.LOCAL_AGENT_IDS = void 0;
391
548
  exports$1.isLocalAgentId = isLocalAgentId;
392
- exports$1.LOCAL_AGENT_IDS = ["claude-code", "codex"];
549
+ exports$1.LOCAL_AGENT_IDS = ["claude-code", "codex", "pi"];
393
550
  const LOCAL_AGENT_ID_SET = new Set(exports$1.LOCAL_AGENT_IDS);
394
551
  function isLocalAgentId(value) {
395
552
  return typeof value === "string" && LOCAL_AGENT_ID_SET.has(value);
@@ -0,0 +1,2 @@
1
+ import { AgentAdapter } from "./types";
2
+ export declare const piAdapter: AgentAdapter;
@@ -1,4 +1,4 @@
1
1
  /** Supported coding-agent CLIs, in the order findLocalAgent() tries them. */
2
- export declare const LOCAL_AGENT_IDS: readonly ["claude-code", "codex"];
2
+ export declare const LOCAL_AGENT_IDS: readonly ["claude-code", "codex", "pi"];
3
3
  export type LocalAgentId = (typeof LOCAL_AGENT_IDS)[number];
4
4
  export declare function isLocalAgentId(value: unknown): value is LocalAgentId;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuabase",
3
3
  "description": "Nuabase TypeScript client to create typed functions from LLM prompts",
4
- "version": "2.3.5",
4
+ "version": "2.3.7",
5
5
  "type": "module",
6
6
  "author": "Jasim A Basheer <jasim@nuabase.com>",
7
7
  "engines": {