@nodus-ai/equile 0.0.1-preview
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 +544 -0
- package/dist/cli.js +13 -0
- package/dist/desktop-agent-J5A2KK2P.js +59 -0
- package/dist/nodus.cjs +1 -0
- package/dist/nodus.d.cts +709 -0
- package/dist/nodus.d.ts +709 -0
- package/dist/nodus.js +1 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
# `@nodus-ai/sdk`
|
|
2
|
+
|
|
3
|
+
Run authenticated Codex, Claude, Grok, Gemini, Cursor, or Factory agents through Nodus.
|
|
4
|
+
|
|
5
|
+
Use this package from trusted server-side code only. Never expose `NODUS_API_KEY` in a browser, Git repository, prompt, screenshot, or log.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @nodus-ai/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node.js 18 or newer is required.
|
|
14
|
+
|
|
15
|
+
## Required values
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export NODUS_BASE_URL="https://nodus-api-production-d291.up.railway.app"
|
|
19
|
+
export NODUS_API_KEY="ndk_..."
|
|
20
|
+
export NODUS_AGENT_ID="..."
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `NODUS_API_KEY` is either scoped to fixed Agent IDs or to one Agent Workspace. A Workspace key dynamically includes Agents added to that Workspace later.
|
|
24
|
+
- `NODUS_AGENT_ID` identifies an Agent already created and authenticated in Nodus.
|
|
25
|
+
- The SDK cannot create Agents or change provider credentials.
|
|
26
|
+
|
|
27
|
+
## Run one task
|
|
28
|
+
|
|
29
|
+
For a Workspace SDK key, omit `agentId`. Nodus distributes runs across healthy matching Agents in that Workspace:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const run = await nodus.agentRuns.createAndWait({
|
|
33
|
+
provider: "codex",
|
|
34
|
+
model: "gpt-5.5",
|
|
35
|
+
mode: "goal",
|
|
36
|
+
prompt: "Begin the review and write the final report.",
|
|
37
|
+
goal: `# Review Goal
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
- Inspect the complete project.
|
|
41
|
+
- Identify the three highest-priority issues.
|
|
42
|
+
- Support every finding with concrete evidence.
|
|
43
|
+
|
|
44
|
+
## Deliverable
|
|
45
|
+
Write the completed report to output/report.md.`
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The bundled CLI supports interactive login and persistent local profiles:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install -g @nodus-ai/sdk@preview
|
|
53
|
+
nodus login
|
|
54
|
+
nodus auth status
|
|
55
|
+
nodus agents list
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`nodus login` opens a browser, displays a one-time code, and waits for approval.
|
|
59
|
+
The resulting revocable Workspace SDK key is stored in
|
|
60
|
+
`~/.config/nodus/config.json` with file mode `0600`. Use `--profile NAME` or
|
|
61
|
+
`NODUS_PROFILE` for multiple accounts and environments. `NODUS_API_KEY` and
|
|
62
|
+
`NODUS_BASE_URL` always take precedence for CI and headless use. `nodus logout`
|
|
63
|
+
revokes the key server-side and removes the local profile.
|
|
64
|
+
|
|
65
|
+
The CLI also accepts direct environment credentials:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export NODUS_API_KEY="ndk_..."
|
|
69
|
+
nodus sdk
|
|
70
|
+
nodus capabilities
|
|
71
|
+
nodus agents list
|
|
72
|
+
nodus agents quota --agent "$NODUS_AGENT_ID" --refresh
|
|
73
|
+
nodus agents configure --agent "$NODUS_AGENT_ID" --model gpt-5.5 --reasoning high
|
|
74
|
+
nodus agents concurrency --agent "$NODUS_AGENT_ID" --max 12
|
|
75
|
+
nodus run --provider codex --model gpt-5.5 --instructions "Return exactly: workspace smoke" --wait
|
|
76
|
+
nodus runs list --status running
|
|
77
|
+
nodus runs get --run <run-id>
|
|
78
|
+
nodus runs events --run <run-id>
|
|
79
|
+
nodus runs cancel --run <run-id>
|
|
80
|
+
nodus runs artifact --run <run-id> --artifact <artifact-id>
|
|
81
|
+
nodus session list # Active Sessions only; use --status destroyed --limit 100 for history
|
|
82
|
+
nodus session create --agent "$NODUS_AGENT_ID" --persistent
|
|
83
|
+
nodus session run --session <session-id> --instructions "Continue the task" --wait
|
|
84
|
+
nodus session exec --session <session-id> --command "git status --short"
|
|
85
|
+
nodus session events --session <session-id> --after 0
|
|
86
|
+
nodus session telemetry --session <session-id>
|
|
87
|
+
nodus session telemetry-history --session <session-id> --step-sec 60
|
|
88
|
+
nodus session pause --session <session-id>
|
|
89
|
+
nodus session resume --session <session-id>
|
|
90
|
+
nodus session destroy --session <session-id>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Workspace keys can still target a specific Agent when deterministic routing is required:
|
|
94
|
+
|
|
95
|
+
Create `run.mjs`:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { createNodus } from "@nodus-ai/sdk";
|
|
99
|
+
|
|
100
|
+
const nodus = createNodus({
|
|
101
|
+
baseUrl: process.env.NODUS_BASE_URL,
|
|
102
|
+
apiKey: process.env.NODUS_API_KEY,
|
|
103
|
+
requestTimeoutMs: 15 * 60_000
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const run = await nodus.agentRuns.createAndWait({
|
|
107
|
+
agentId: process.env.NODUS_AGENT_ID,
|
|
108
|
+
instructions: "Review this project and return the three highest-priority issues.",
|
|
109
|
+
timeoutSeconds: 1800
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log(run.status); // "completed"
|
|
113
|
+
console.log(run.finalOutput); // final assistant response
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Run it:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
node run.mjs
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`createAndWait()` returns an `AgentRun` directly. `create()` returns `{ run }`:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const { run } = await nodus.agentRuns.create({
|
|
126
|
+
agentId: process.env.NODUS_AGENT_ID,
|
|
127
|
+
instructions: "Perform the task."
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.log(run.id);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Do not read `result.id` from `create()`. The ID is `result.run.id`.
|
|
134
|
+
|
|
135
|
+
## Reliable create-then-wait pattern
|
|
136
|
+
|
|
137
|
+
Use this pattern for batch jobs and unreliable networks:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const { run } = await nodus.agentRuns.create({
|
|
141
|
+
agentId,
|
|
142
|
+
instructions,
|
|
143
|
+
idempotencyKey: `teacher:${teacherId}:review-v1`,
|
|
144
|
+
timeoutSeconds: 7200
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const completed = await nodus.agentRuns.wait(run.id, {
|
|
148
|
+
timeoutMs: 10 * 60 * 60 * 1000,
|
|
149
|
+
pollIntervalMs: 5000
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`nodus sdk` (also available as `nodus capabilities`) prints the installed SDK
|
|
154
|
+
version, every callable client method, feature flags such as Workspace
|
|
155
|
+
credential failover and persistent session snapshots, plus the supported CLI
|
|
156
|
+
commands. It does not make an authenticated API request, so it also works
|
|
157
|
+
before `NODUS_API_KEY` is configured.
|
|
158
|
+
|
|
159
|
+
The Session namespace also exposes `diff`, `ports`, `git`, and
|
|
160
|
+
`terminalTicket` for workspace inspection and terminal clients. The Desktop
|
|
161
|
+
Runtime namespace exposes `browse`, `read`, `ports`, `attach`, `reveal`, and
|
|
162
|
+
`terminalTicket` in addition to lifecycle methods. These owner-wide methods
|
|
163
|
+
require a Workspace-scoped SDK key.
|
|
164
|
+
|
|
165
|
+
`nodus workspace build` excludes `.git` from the uploaded Docker context.
|
|
166
|
+
Keep generated files, local dependencies, and secrets out of the selected
|
|
167
|
+
context directory; the CLI does not reinterpret Docker ignore rules.
|
|
168
|
+
|
|
169
|
+
Rules:
|
|
170
|
+
|
|
171
|
+
1. Generate a stable `idempotencyKey` for each logical task.
|
|
172
|
+
2. After `create()` returns a run ID, keep waiting on that same ID.
|
|
173
|
+
3. If polling fails, retry `wait(run.id)`; do not create a new run.
|
|
174
|
+
4. If the create response is lost, call `create()` again with the same `idempotencyKey`.
|
|
175
|
+
5. Do not log complete prompts or API keys.
|
|
176
|
+
|
|
177
|
+
## Concurrency and queues
|
|
178
|
+
|
|
179
|
+
Submitting many promises only submits many runs. Actual execution is limited by the Agent's server-side `maxConcurrency`.
|
|
180
|
+
|
|
181
|
+
This is Run concurrency, not a sandbox count. Nodus may use one sandbox per Run or multiple isolated runtime sessions inside one sandbox. SDK behavior stays the same.
|
|
182
|
+
|
|
183
|
+
Railway replica count and `NODUS_AGENT_RUN_WORKER_CAPACITY` only control internal worker throughput. Postgres enforces `maxConcurrency` globally across all replicas.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
await nodus.agents.update(agentId, { modelId: "gpt-5.5", reasoningEffort: "high" });
|
|
187
|
+
await nodus.agents.update(agentId, { modelId: null, reasoningEffort: null }); // Restore provider defaults
|
|
188
|
+
await nodus.agents.setRunConcurrency(agentId, 160); // Valid range: 1-160
|
|
189
|
+
|
|
190
|
+
const created = await Promise.all(
|
|
191
|
+
tasks.map((task) => nodus.agentRuns.create({
|
|
192
|
+
agentId,
|
|
193
|
+
instructions: task.instructions,
|
|
194
|
+
idempotencyKey: task.id
|
|
195
|
+
}))
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Runs above the available Agent concurrency remain `queued` and start automatically when a slot becomes available. Queue time does not consume the run's `timeoutSeconds`; the execution timer starts when Nodus claims the run.
|
|
200
|
+
|
|
201
|
+
Before starting a replacement batch, inspect existing work instead of blindly creating duplicates:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
const queued = await nodus.agentRuns.list({ agentId, status: "queued" });
|
|
205
|
+
const running = await nodus.agentRuns.list({ agentId, status: "running" });
|
|
206
|
+
|
|
207
|
+
console.log({ queued: queued.runs.length, running: running.runs.length });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## List and cancel old runs
|
|
211
|
+
|
|
212
|
+
`list()` returns at most 100 lightweight run summaries. Use `nextCursor` to continue.
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
let cursor;
|
|
216
|
+
const queuedRunIds = [];
|
|
217
|
+
|
|
218
|
+
do {
|
|
219
|
+
const page = await nodus.agentRuns.list({
|
|
220
|
+
agentId,
|
|
221
|
+
status: "queued",
|
|
222
|
+
cursor,
|
|
223
|
+
limit: 100
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
queuedRunIds.push(...page.runs.map((run) => run.id));
|
|
227
|
+
cursor = page.nextCursor ?? undefined;
|
|
228
|
+
} while (cursor);
|
|
229
|
+
|
|
230
|
+
await nodus.agentRuns.cancelMany(queuedRunIds);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Only runs belonging to Agents allowed by the SDK key are visible or cancellable.
|
|
234
|
+
|
|
235
|
+
## Batch runner
|
|
236
|
+
|
|
237
|
+
The repository includes [`examples/batch-agent-runs.mjs`](https://github.com/Nodus-my/Nodus-backend/blob/main/examples/batch-agent-runs.mjs). It:
|
|
238
|
+
|
|
239
|
+
- runs up to 80 tasks concurrently;
|
|
240
|
+
- uses a stable SHA-256 `idempotencyKey` per task;
|
|
241
|
+
- waits on the original run ID after creation;
|
|
242
|
+
- writes completed results to JSONL;
|
|
243
|
+
- can cancel old queued runs before starting.
|
|
244
|
+
|
|
245
|
+
Input file:
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
[
|
|
249
|
+
{
|
|
250
|
+
"instructions": "Review teacher 123 and return JSON.",
|
|
251
|
+
"timeoutSeconds": 7200,
|
|
252
|
+
"metadata": { "teacherId": "123" }
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Run:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
node examples/batch-agent-runs.mjs tasks.json results.jsonl
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Cancel old queued runs first:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
NODUS_CANCEL_QUEUED=1 node examples/batch-agent-runs.mjs tasks.json results.jsonl
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## File inputs
|
|
270
|
+
|
|
271
|
+
Pass short-lived HTTPS URLs as task resources:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
const run = await nodus.agentRuns.createAndWait({
|
|
275
|
+
agentId,
|
|
276
|
+
instructions: "Inspect the attached project.",
|
|
277
|
+
resources: [{
|
|
278
|
+
url: signedUrl,
|
|
279
|
+
path: "input/project.zip",
|
|
280
|
+
contentType: "application/zip"
|
|
281
|
+
}]
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Never place credentials in resource URLs, filenames, metadata, or instructions.
|
|
286
|
+
|
|
287
|
+
## Send images and other inline files
|
|
288
|
+
|
|
289
|
+
Small binary payloads (screenshots, photos, PDFs) can be passed directly — no hosting required. Nodus stages the bytes privately and the agent reads the file from its workspace:
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
import { readFile } from "node:fs/promises";
|
|
293
|
+
|
|
294
|
+
const run = await nodus.agentRuns.createAndWait({
|
|
295
|
+
agentId,
|
|
296
|
+
instructions: "Describe what is wrong in the attached screenshot.",
|
|
297
|
+
resources: [{
|
|
298
|
+
contentBase64: (await readFile("screenshot.png")).toString("base64"),
|
|
299
|
+
path: "input/screenshot.png",
|
|
300
|
+
contentType: "image/png"
|
|
301
|
+
}]
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Sessions accept per-message attachments; the delivered turn references the staged file paths so the agent can open them with its file-reading tools:
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
await nodus.sessions.runAndWait(session.id, "Compare this design against the implementation.", {
|
|
309
|
+
attachments: [{ name: "design.png", contentBase64: (await readFile("design.png")).toString("base64") }]
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The CLI reads local files for you:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
nodus run --agent "$NODUS_AGENT_ID" --instructions "Describe the screenshot." --attach ./screenshot.png --wait
|
|
317
|
+
nodus session run --session <session-id> --instructions "Look at this photo." --attach ./photo.jpg --wait
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Limits: about 7 MB per file (10,000,000 base64 characters), 20 MB of inline content per run, and 4 attachments per session message. Attachment names must look like `name.ext` (letters, digits, `_`, `-`). Inline blobs are deleted when the run is cleaned up; session staging blobs are deleted immediately after the file lands in the workspace.
|
|
321
|
+
|
|
322
|
+
## Goal mode
|
|
323
|
+
|
|
324
|
+
Goal mode is supported only by Codex Agents:
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
const run = await nodus.agentRuns.createAndWait({
|
|
328
|
+
agentId,
|
|
329
|
+
mode: "goal",
|
|
330
|
+
instructions: "Implement and verify the requested change."
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
For all other providers, omit `mode` or use `"standard"`.
|
|
335
|
+
|
|
336
|
+
## Artifacts
|
|
337
|
+
|
|
338
|
+
Completed runs can return output artifacts:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
for (const artifact of run.artifacts) {
|
|
342
|
+
const { url, expiresAt } = await nodus.agentRuns.getArtifactDownloadUrl(run.id, artifact.id);
|
|
343
|
+
console.log(artifact.path, url, expiresAt);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Artifact download URLs are short-lived. Artifact metadata reports `available` or `expired`.
|
|
348
|
+
|
|
349
|
+
## Events and streaming
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
for await (const event of nodus.agentRuns.stream(runId)) {
|
|
353
|
+
console.log(event.type, event.payload);
|
|
354
|
+
console.log(event.raw?.provider, event.raw?.type, event.raw?.payload);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Use `wait()` when only the final result matters. Use `stream()` when progress events are needed. Both wait for runtime cleanup before finishing.
|
|
359
|
+
|
|
360
|
+
SDK event methods request full event detail. `event.payload` is the stable,
|
|
361
|
+
normalized client contract; `event.raw` contains the complete persisted provider
|
|
362
|
+
event for rich desktop rendering. Raw payloads are not truncated, but they are
|
|
363
|
+
still recursively sanitized before persistence: credentials and internal
|
|
364
|
+
infrastructure identifiers are removed or redacted.
|
|
365
|
+
|
|
366
|
+
## Persistent sessions
|
|
367
|
+
|
|
368
|
+
Agent Runs are temporary and automatically cleaned up. Use Sessions only when the conversation must continue across multiple messages.
|
|
369
|
+
|
|
370
|
+
A Session is intentionally bound to one Agent identity. Automatic credential failover applies to Workspace-scheduled Agent Runs created with `provider` and no `agentId`; it does not silently switch an existing Session to a different Agent. If a fixed Session loses its credential, start a new Workspace-scheduled Run when cross-Agent continuation is required.
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
const { session } = await nodus.sessions.create({ agentId });
|
|
374
|
+
|
|
375
|
+
const first = await nodus.sessions.runAndWait(session.id, "Inspect the repository.");
|
|
376
|
+
const second = await nodus.sessions.runAndWait(session.id, "Now implement the fix.");
|
|
377
|
+
|
|
378
|
+
const live = await nodus.sessions.telemetry(session.id);
|
|
379
|
+
const history = await nodus.sessions.telemetryHistory(session.id, { stepSec: 60 });
|
|
380
|
+
|
|
381
|
+
await nodus.sessions.pause(session.id);
|
|
382
|
+
await nodus.sessions.resume(session.id);
|
|
383
|
+
await nodus.sessions.destroy(session.id);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Persistent sandboxes (Claude only)
|
|
387
|
+
|
|
388
|
+
By default a sandbox is a single-shot workflow: it is reaped when idle and an
|
|
389
|
+
agent run destroys it on completion. Pass `persistent: true` to keep the
|
|
390
|
+
sandbox alive as an ongoing workspace instead. Claude only — its tmux harness
|
|
391
|
+
accepts appended messages; other providers reject the flag.
|
|
392
|
+
|
|
393
|
+
```js
|
|
394
|
+
// A session whose sandbox is never idle-reaped (max sandbox lifetime, 24h;
|
|
395
|
+
// pause/resume snapshots carry it beyond that):
|
|
396
|
+
const { session } = await nodus.sessions.create({ agentId, persistent: true });
|
|
397
|
+
|
|
398
|
+
// An agent run that leaves its workspace running. The prompt drops the
|
|
399
|
+
// "deliver files to an output folder" single-shot instructions and treats
|
|
400
|
+
// /workspace as an ongoing project. The completed run exposes `sessionId`,
|
|
401
|
+
// and follow-up persistent runs on the same agent continue the same live
|
|
402
|
+
// workspace:
|
|
403
|
+
const run = await nodus.agentRuns.createAndWait({
|
|
404
|
+
agentId,
|
|
405
|
+
instructions: "Scaffold the service.",
|
|
406
|
+
persistent: true
|
|
407
|
+
});
|
|
408
|
+
await nodus.sessions.runAndWait(run.sessionId, "Now add tests.");
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Persistent sessions are billed while live; `pause()`, `resume()` and
|
|
412
|
+
`destroy()` work as usual.
|
|
413
|
+
|
|
414
|
+
## Errors
|
|
415
|
+
|
|
416
|
+
SDK failures throw `NodusError`:
|
|
417
|
+
|
|
418
|
+
```js
|
|
419
|
+
import { NodusError } from "@nodus-ai/sdk";
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
await nodus.agentRuns.wait(runId);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (error instanceof NodusError) {
|
|
425
|
+
console.error(error.status, error.code, error.retryAt, error.message, error.data);
|
|
426
|
+
}
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Common statuses:
|
|
432
|
+
|
|
433
|
+
- `400`: invalid request;
|
|
434
|
+
- `401`: invalid SDK key;
|
|
435
|
+
- `403`: SDK key is not allowed to use the Agent;
|
|
436
|
+
- `404`: run or session not found;
|
|
437
|
+
- `409`: runtime/auth conflict;
|
|
438
|
+
- `504`: the local SDK wait deadline elapsed. The remote run may still be active; call `get()` or `wait()` again before creating another run.
|
|
439
|
+
|
|
440
|
+
Agent Run creation also exposes stable business codes through `NodusError.code`:
|
|
441
|
+
|
|
442
|
+
- `workspace_auth_unavailable`: every matching Workspace credential is invalid or unavailable;
|
|
443
|
+
- `workspace_capacity_unavailable`: usable credentials exist, but every matching Agent is at capacity;
|
|
444
|
+
- `quota_exhausted`: a specifically selected Agent is rate-limited; `NodusError.retryAt` contains the reset time when known.
|
|
445
|
+
|
|
446
|
+
Session APIs expose stable codes for caller-actionable failures:
|
|
447
|
+
|
|
448
|
+
- `invalid_session_request`, `invalid_session_list_request`, `invalid_session_list_cursor`, `invalid_session_message`, `invalid_session_wait_request`;
|
|
449
|
+
- `session_not_found`, `session_turn_conflict`, `session_turn_not_found`;
|
|
450
|
+
- `session_not_live`, `session_not_paused`, `session_checkpoint_unavailable`;
|
|
451
|
+
- `desktop_runtime_offline`, `agent_auth_unavailable`, `workspace_revision_not_ready`.
|
|
452
|
+
|
|
453
|
+
## API reference
|
|
454
|
+
|
|
455
|
+
### `agents`
|
|
456
|
+
|
|
457
|
+
- `list()`
|
|
458
|
+
- `create({ provider, harness, modelId, displayName?, defaultResourceProfile?, maxConcurrency?, metadata? })`
|
|
459
|
+
- `update(agentId, { modelId?, reasoningEffort? })`
|
|
460
|
+
- `setRunConcurrency(agentId, maxConcurrency)`
|
|
461
|
+
- `setConcurrency(agentId, maxConcurrency)` — compatibility alias
|
|
462
|
+
- `quota(agentId, { refresh? })`
|
|
463
|
+
|
|
464
|
+
### `agentRuns`
|
|
465
|
+
|
|
466
|
+
- `create(input)` → `{ run }`
|
|
467
|
+
- `createAndWait(input, waitOptions?)` → `AgentRun`
|
|
468
|
+
- `get(runId)`
|
|
469
|
+
- `list({ agentId?, status?, cursor?, limit? })`
|
|
470
|
+
- `wait(runId, options?)`
|
|
471
|
+
- `events(runId, { after? })` → normalized events plus sanitized `raw` provider payloads
|
|
472
|
+
- `stream(runId, options?)`
|
|
473
|
+
- `cancel(runId)`
|
|
474
|
+
- `cancelMany(runIds)`
|
|
475
|
+
- `getArtifactDownloadUrl(runId, artifactId)`
|
|
476
|
+
|
|
477
|
+
### `sessions`
|
|
478
|
+
|
|
479
|
+
- `list({ status?, limit?, cursor? })` → `{ sessions, nextCursor }`; defaults to active Sessions only
|
|
480
|
+
- `create({ agentId })`
|
|
481
|
+
- `get(sessionId)`
|
|
482
|
+
- `telemetry(sessionId)` / `telemetryHistory(sessionId, { startMs?, endMs?, stepSec? })`
|
|
483
|
+
- `diff(sessionId, knownHash?)` / `ports(sessionId, knownHash?)`
|
|
484
|
+
- `git(sessionId, { action, message?, path? })`
|
|
485
|
+
- `terminalTicket(sessionId)`
|
|
486
|
+
- `run(sessionId, text, { attachments? })`
|
|
487
|
+
- `execWorkspace(sessionId, command, timeoutSeconds?)`
|
|
488
|
+
- `runAndWait(sessionId, text, options?)` — options may include `attachments`
|
|
489
|
+
- `waitForIdle(sessionId, options)`
|
|
490
|
+
- `events(sessionId, { after? })` → normalized events plus sanitized `raw` provider payloads
|
|
491
|
+
- `stream(sessionId, options?)`
|
|
492
|
+
- `pause(sessionId)`
|
|
493
|
+
- `resume(sessionId)`
|
|
494
|
+
- `destroy(sessionId)`
|
|
495
|
+
|
|
496
|
+
### `workspaces` (runtime images and revisions)
|
|
497
|
+
|
|
498
|
+
This namespace manages immutable execution environments and interactive builders. Agent Workspace membership and routing are exposed through the Workspace-scoped key plus `agents`, `agentRuns`, and `sessions`.
|
|
499
|
+
These methods use the same Workspace SDK key as Agents, Runs, and Sessions.
|
|
500
|
+
|
|
501
|
+
- `create({ name, defaultResourceProfile? })`
|
|
502
|
+
- `list()` / `get(workspaceId)` / `archive(workspaceId)` / `revisions(workspaceId)`
|
|
503
|
+
- `startBuilder(workspaceId, { baseRevisionId? })` / `getBuild(buildId)`
|
|
504
|
+
- `exec(buildId, command, timeoutSeconds?)`
|
|
505
|
+
- `listFiles(buildId, path?)` / `readFile(buildId, path)` / `writeFile(buildId, path, content)`
|
|
506
|
+
- `upload(buildId, path, content)` / `download(buildId, path)`
|
|
507
|
+
- `publish(buildId)` / `destroyBuilder(buildId)`
|
|
508
|
+
- `createContextUpload(workspaceId)`
|
|
509
|
+
- `buildFromDockerfile(workspaceId, input)` / `buildFromContext(workspaceId, input)`
|
|
510
|
+
|
|
511
|
+
### `desktopRuntimes`
|
|
512
|
+
|
|
513
|
+
These methods require a Workspace-scoped SDK key because Desktop Runtimes are owner-wide execution resources rather than properties of one Agent.
|
|
514
|
+
|
|
515
|
+
- `list()`
|
|
516
|
+
- `get(desktopRuntimeId)`
|
|
517
|
+
- `rename(desktopRuntimeId, name)`
|
|
518
|
+
- `grants(desktopRuntimeId)`
|
|
519
|
+
- `grant(desktopRuntimeId, agentId, "full" | "files_only")`
|
|
520
|
+
- `revokeGrant(desktopRuntimeId, agentId)`
|
|
521
|
+
- `browse(desktopRuntimeId, { path?, dirsOnly? })` / `read(desktopRuntimeId, path)`
|
|
522
|
+
- `ports(desktopRuntimeId, knownHash?)`
|
|
523
|
+
- `attach(desktopRuntimeId, { name, contentBase64 })`
|
|
524
|
+
- `reveal(desktopRuntimeId, { path, app? })`
|
|
525
|
+
- `terminalTicket(desktopRuntimeId)`
|
|
526
|
+
- `disconnect(desktopRuntimeId)`; disconnected runtimes are hidden from `list()` and `get()`
|
|
527
|
+
|
|
528
|
+
Terminal tickets are short-lived bearer credentials. Pass them directly to
|
|
529
|
+
the WebSocket client and do not persist or log them.
|
|
530
|
+
|
|
531
|
+
The same paired computer can serve multiple Agents with independent permissions. `files_only` grants expose only list, read, and write operations; `full` grants additionally allow commands, PTYs, local skills/plugins, and local MCP. Desktop daemons use bounded concurrency through `NODUS_DESKTOP_CONCURRENCY` (default `4`, maximum `16`). Runs remain queued with `desktopWaiting: true` while the computer is offline, so execution timeout and Goal turns do not start until it reconnects.
|
|
532
|
+
|
|
533
|
+
## Checklist for coding agents
|
|
534
|
+
|
|
535
|
+
Before writing a Nodus integration, verify all of these:
|
|
536
|
+
|
|
537
|
+
- Import from `@nodus-ai/sdk`, not a repository-relative `sdk/dist` path.
|
|
538
|
+
- Read `create()` results from `{ run }`.
|
|
539
|
+
- Give every logical task a stable `idempotencyKey`.
|
|
540
|
+
- Keep application metadata under your own keys; Nodus rejects reserved runtime and failover metadata keys.
|
|
541
|
+
- Retry polling against the same run ID.
|
|
542
|
+
- Inspect/cancel old queued runs before replacing a failed batch.
|
|
543
|
+
- Treat `timeoutSeconds` as execution time, not queue time.
|
|
544
|
+
- Keep API keys and full prompts out of logs.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {execFile}from'child_process';import {readFile,writeFile,mkdtemp,rm,mkdir,chmod,rename}from'fs/promises';import {platform,hostname,tmpdir,homedir}from'os';import y from'path';import {promisify}from'util';var h=class extends Error{constructor(u,f,i){super(u);this.status=f;this.data=i;this.name="NodusError";let b=i&&typeof i=="object"?i:null;this.code=typeof b?.code=="string"?b.code:null,this.retryAt=typeof b?.retryAt=="string"?b.retryAt:null;}status;data;code;retryAt},E=new Set(["completed","failed","cancelled","timed_out"]);function G(l){let s=l.baseUrl.replace(/\/+$/,""),u=l.fetch??globalThis.fetch,f=l.requestTimeoutMs??9e5,i=async(e,t,r,o={})=>{let p=new AbortController,n=o.timeoutMs??f,a=()=>p.abort(o.signal?.reason);o.signal?.addEventListener("abort",a,{once:true});let d=setTimeout(()=>p.abort(new Error("Nodus request timed out.")),n);try{let c=await u(`${s}${t}`,{method:e,headers:{authorization:`Bearer ${l.apiKey}`,...r===void 0?{}:{"content-type":"application/json"}},...r===void 0?{}:{body:JSON.stringify(r)},signal:p.signal}),g=await c.json().catch(()=>({}));if(!c.ok)throw new h(String(g.error??`Nodus request failed: ${c.status}`),c.status,g);return g}catch(c){throw o.signal?.aborted?new h("Nodus request was aborted.",499,null):p.signal.aborted?new h("Nodus request timed out.",504,{method:e,path:t,timeoutMs:n}):c}finally{clearTimeout(d),o.signal?.removeEventListener("abort",a);}},b=async(e,t={})=>{let r=Date.now()+(t.timeoutMs??18e5),o=t.pollIntervalMs??1e3;for(;Date.now()<r;){if(t.signal?.aborted)throw new h("Nodus agent run was aborted.",499,null);let{run:p}=await i("GET",`/agent-runs/${encodeURIComponent(e)}?view=status`,void 0,{signal:t.signal});if(E.has(p.status)&&!p.cleanupCompletedAt){await new Promise(n=>setTimeout(n,o));continue}if(p.status==="completed")return (await i("GET",`/agent-runs/${encodeURIComponent(e)}`)).run;if(E.has(p.status))throw new h(p.error??`Nodus agent run ${p.status}.`,502,{run:p});await new Promise(n=>setTimeout(n,o));}throw new h("Timed out waiting for Nodus agent run.",504,{runId:e})},A=(e,t={})=>i("GET",`/agent-runs/${encodeURIComponent(e)}/events?after=${t.after??0}&detail=full`),v=(e,t={})=>i("GET",`/runtime-sessions/${encodeURIComponent(e)}/events?after=${t.after??0}&detail=full`),C=async(e,t)=>{let r=Date.now()+(t.timeoutMs??144e5),o=t.afterSequence??0,p=[];for(;Date.now()<r;){if(t.signal?.aborted)throw new h("Nodus session turn was aborted.",499,null);let n=Math.max(10,Math.ceil((r-Date.now())/1e3)),a=Math.min(840,t.waitSeconds??840,n),d=await i("POST",`/runtime-sessions/${encodeURIComponent(e)}/wait?detail=full`,{turnId:t.turnId,afterSequence:o,timeoutSeconds:a},{signal:t.signal,timeoutMs:Math.max(f,(a+30)*1e3)});p.push(...d.events),o=Math.max(o,d.lastSequence);let c={...d,events:p};if(d.status==="completed")return c;if(d.status==="failed")throw new h(d.error??"Nodus session turn failed.",502,c)}throw new h("Timed out waiting for Nodus session turn.",504,{sessionId:e,turnId:t.turnId,afterSequence:o})},k=async function*(e,t={},r){let o=t.after??0,p=t.pollIntervalMs??1e3;for(;!t.signal?.aborted;){let{events:n}=await e(o,t.signal);for(let a of n)o=Math.max(o,a.sequence),yield a;if(r&&await r())return;await new Promise(a=>setTimeout(a,p));}};return {agents:{list:async()=>(await i("GET","/agent-runs/agents")).agents,create:e=>i("POST","/agent-runs/agents",e),update:(e,t)=>i("PATCH",`/agent-runs/agents/${encodeURIComponent(e)}/configuration`,t),setRunConcurrency:(e,t)=>i("POST",`/agent-runs/agents/${encodeURIComponent(e)}/concurrency`,{maxConcurrency:t}),quota:(e,t)=>i("GET",`/agent-runs/agents/${encodeURIComponent(e)}/quota${t?.refresh?"?refresh=1":""}`),setConcurrency:(e,t)=>i("POST",`/agent-runs/agents/${encodeURIComponent(e)}/concurrency`,{maxConcurrency:t})},agentRuns:{list:(e={})=>{let t=new URLSearchParams;e.agentId&&t.set("agentId",e.agentId),e.status&&t.set("status",e.status),e.cursor&&t.set("cursor",e.cursor),e.limit&&t.set("limit",String(e.limit));for(let[r,o]of Object.entries(e.metadata??{}))t.append("metadata",`${r}=${o}`);return i("GET",`/agent-runs?${t}`)},queueStats:()=>i("GET","/agent-runs/queue-stats"),create:e=>i("POST","/agent-runs",e),get:e=>i("GET",`/agent-runs/${encodeURIComponent(e)}`),events:A,stream:(e,t)=>k((r,o)=>i("GET",`/agent-runs/${encodeURIComponent(e)}/events?after=${r}&detail=full`,void 0,{signal:o}),t,async()=>{let{run:r}=await i("GET",`/agent-runs/${encodeURIComponent(e)}?view=status`);return E.has(r.status)&&!!r.cleanupCompletedAt}),cancel:e=>i("POST",`/agent-runs/${encodeURIComponent(e)}/cancel`),cancelMany:e=>Promise.all(e.map(async t=>(await i("POST",`/agent-runs/${encodeURIComponent(t)}/cancel`)).run)),getArtifactDownloadUrl:(e,t)=>i("GET",`/agent-runs/${encodeURIComponent(e)}/artifacts/${encodeURIComponent(t)}/download`),wait:b,createAndWait:async(e,t)=>{let{run:r}=await i("POST","/agent-runs",e);return r.status==="completed"&&r.cleanupCompletedAt?r:b(r.id,t)}},sessions:{create:e=>i("POST","/runtime-sessions",e),list:(e={})=>{let t=new URLSearchParams(Object.entries(e).filter(r=>r[1]!==void 0).map(([r,o])=>[r,String(o)]));return i("GET",`/runtime-sessions${t.size?`?${t}`:""}`)},get:e=>i("GET",`/runtime-sessions/${encodeURIComponent(e)}`),telemetry:e=>i("GET",`/runtime-sessions/${encodeURIComponent(e)}/telemetry`),telemetryHistory:(e,t={})=>{let r=new URLSearchParams(Object.entries(t).filter(o=>o[1]!==void 0).map(([o,p])=>[o,String(p)]));return i("GET",`/runtime-sessions/${encodeURIComponent(e)}/telemetry-history${r.size?`?${r}`:""}`)},diff:(e,t)=>i("GET",`/runtime-sessions/${encodeURIComponent(e)}/diff${t?`?known=${encodeURIComponent(t)}`:""}`),terminalTicket:e=>i("POST",`/runtime-sessions/${encodeURIComponent(e)}/terminal-ticket`),ports:(e,t)=>i("GET",`/runtime-sessions/${encodeURIComponent(e)}/ports${t?`?known=${encodeURIComponent(t)}`:""}`),git:(e,t)=>i("POST",`/runtime-sessions/${encodeURIComponent(e)}/git`,t),run:(e,t,r={})=>i("POST",`/runtime-sessions/${encodeURIComponent(e)}/messages`,{text:t,...r.attachments?.length?{attachments:r.attachments}:{}}),execWorkspace:(e,t,r)=>i("POST",`/runtime-sessions/${encodeURIComponent(e)}/workspace/exec`,{cmd:t,...r?{timeoutSeconds:r}:{}},{timeoutMs:r?Math.max(f,(r+30)*1e3):f}),waitForIdle:C,runAndWait:async(e,t,r={})=>{let{attachments:o,...p}=r,{turn:n}=await i("POST",`/runtime-sessions/${encodeURIComponent(e)}/messages`,{text:t,...o?.length?{attachments:o}:{}});return C(e,{...p,turnId:n.id,afterSequence:n.acceptedSequence})},events:v,stream:(e,t)=>k((r,o)=>i("GET",`/runtime-sessions/${encodeURIComponent(e)}/events?after=${r}&detail=full`,void 0,{signal:o}),t),pause:e=>i("POST",`/runtime-sessions/${encodeURIComponent(e)}/pause`),resume:e=>i("POST",`/runtime-sessions/${encodeURIComponent(e)}/resume`),activate:e=>i("POST",`/runtime-sessions/${encodeURIComponent(e)}/activate`),destroy:e=>i("DELETE",`/runtime-sessions/${encodeURIComponent(e)}`)},workspaces:{create:e=>i("POST","/workspace-runtimes",e),list:async()=>(await i("GET","/workspace-runtimes")).workspaces,get:e=>i("GET",`/workspace-runtimes/${encodeURIComponent(e)}`),archive:e=>i("DELETE",`/workspace-runtimes/${encodeURIComponent(e)}`),revisions:async e=>{let t=await i("GET",`/workspace-runtimes/${encodeURIComponent(e)}/revisions`);if(!Array.isArray(t.revisions))throw new Error("Invalid Workspace revisions response.");return t.revisions},startBuilder:(e,t={})=>i("POST",`/workspace-runtimes/${encodeURIComponent(e)}/builds`,{mode:"interactive",...t}),getBuild:e=>i("GET",`/workspace-builds/${encodeURIComponent(e)}`),exec:(e,t,r)=>i("POST",`/workspace-builds/${encodeURIComponent(e)}/exec`,{cmd:t,...r?{timeoutSeconds:r}:{}},{timeoutMs:r?Math.max(f,(r+30)*1e3):f}),listFiles:(e,t="/")=>i("GET",`/workspace-builds/${encodeURIComponent(e)}/files?path=${encodeURIComponent(t)}`),readFile:(e,t)=>i("GET",`/workspace-builds/${encodeURIComponent(e)}/file?path=${encodeURIComponent(t)}`),writeFile:(e,t,r)=>i("PUT",`/workspace-builds/${encodeURIComponent(e)}/file`,{path:t,contentBase64:Buffer.from(r).toString("base64")}),upload:(e,t,r)=>i("PUT",`/workspace-builds/${encodeURIComponent(e)}/file`,{path:t,contentBase64:Buffer.from(r).toString("base64")}),download:async(e,t)=>Buffer.from((await i("GET",`/workspace-builds/${encodeURIComponent(e)}/file?path=${encodeURIComponent(t)}`)).contentBase64,"base64"),publish:e=>i("POST",`/workspace-builds/${encodeURIComponent(e)}/publish`),destroyBuilder:e=>i("DELETE",`/workspace-builds/${encodeURIComponent(e)}`),createContextUpload:e=>i("POST",`/workspace-runtimes/${encodeURIComponent(e)}/context-upload`),buildFromDockerfile:(e,t)=>i("POST",`/workspace-runtimes/${encodeURIComponent(e)}/builds`,{mode:"dockerfile",...t}),buildFromContext:(e,t)=>i("POST",`/workspace-runtimes/${encodeURIComponent(e)}/builds`,{mode:"dockerfile",...t})},desktopRuntimes:{list:async(e={})=>(await i("GET",`/desktop-runtimes${e.agentId?`?agentId=${encodeURIComponent(e.agentId)}`:""}`)).desktopRuntimes,get:e=>i("GET",`/desktop-runtimes/${encodeURIComponent(e)}`),rename:(e,t)=>i("PATCH",`/desktop-runtimes/${encodeURIComponent(e)}`,{name:t}),bind:(e,t)=>i("PATCH",`/desktop-runtimes/${encodeURIComponent(e)}/bind`,{agentId:t}),grants:e=>i("GET",`/desktop-runtimes/${encodeURIComponent(e)}/grants`),grant:(e,t,r)=>i("PUT",`/desktop-runtimes/${encodeURIComponent(e)}/grants/${encodeURIComponent(t)}`,{permission:r}),revokeGrant:(e,t)=>i("DELETE",`/desktop-runtimes/${encodeURIComponent(e)}/grants/${encodeURIComponent(t)}`),browse:(e,t={})=>i("POST",`/desktop-runtimes/${encodeURIComponent(e)}/browse`,t),read:(e,t)=>i("POST",`/desktop-runtimes/${encodeURIComponent(e)}/read`,{path:t}),ports:(e,t)=>i("POST",`/desktop-runtimes/${encodeURIComponent(e)}/ports`,t?{known:t}:{}),attach:(e,t)=>i("POST",`/desktop-runtimes/${encodeURIComponent(e)}/attach`,t),terminalTicket:e=>i("POST",`/desktop-runtimes/${encodeURIComponent(e)}/terminal-ticket`),reveal:(e,t)=>i("POST",`/desktop-runtimes/${encodeURIComponent(e)}/reveal`,t),disconnect:e=>i("DELETE",`/desktop-runtimes/${encodeURIComponent(e)}`)}}}var q="0.4.0-preview.47";function L(){return {workspaceCredentialFailover:true,fixedSessionIdentity:true,persistentSessionSnapshots:true,runtimeResourceTelemetry:true,cliIntrospection:true,inlineRunResources:true,sessionAttachments:true,providers:["claude","codex","grok","gemini","cursor","factory"]}}function W(){return [{command:"nodus login|auth status|logout",description:"Authorize the CLI in a browser and manage local credential profiles."},{command:"nodus sdk",description:"List installed SDK methods, features, and CLI commands."},{command:"nodus capabilities",description:"Alias for `nodus sdk`."},{command:"nodus agents list|quota|configure|concurrency",description:"Inspect and configure every Agent available to the SDK key."},{command:"nodus agents deploy --provider <provider> --api-key <key> [--name <displayName>] [--owner-tag <tag>]",description:"Create an Agent in the key's Workspace and authenticate it with a provider API key."},{command:"nodus run --agent <id> --prompt <text> [--goal-file <markdown>] [--attach <file>]... [--wait]",description:"Run one task on a specific Agent. Goal Markdown is stored separately from the initial prompt."},{command:"nodus run --provider <provider> [--model <model>] --prompt <text> [--goal-file <markdown>] [--attach <file>]... [--wait]",description:"Let a Workspace key route a task; --goal-file enables Codex Goal mode."},{command:"nodus runs list|get|events|cancel|artifact",description:"Inspect and control existing Agent Runs, including artifact download URLs."},{command:"nodus session list|create|get|run|exec|events|telemetry|telemetry-history|diff|ports|git|terminal-ticket|pause|resume|destroy",description:"List, create, observe, and control persistent or temporary runtime Sessions."},{command:"nodus workspace create|list|get|revisions|archive|build",description:"Create, inspect, archive, and build persistent Workspace runtimes."},{command:"nodus workspace builder start|exec|put|get|files|status|publish|destroy",description:"Build and publish Workspace revisions interactively."},{command:"nodus desktop connect|list|get|rename|browse|read|ports|attach|reveal|terminal-ticket|disconnect",description:"Attach, inspect, control, or disconnect a local Desktop runtime."},{command:"nodus desktop bind --desktop <id> [--agent <id>|--none]",description:"Bind a Desktop runtime to one Agent (only that Agent's runs/sessions may use it) or unbind it."}]}function j(l){let s=[],u=(f,i="")=>{if(!(!f||typeof f!="object"))for(let[b,A]of Object.entries(f)){let v=i?`${i}.${b}`:b;typeof A=="function"?s.push(v):u(A,v);}};return u(l),s.sort()}function T(l){let s=l?.trim()||"default";if(!/^[A-Za-z0-9._-]{1,64}$/.test(s))throw new Error("Profile name must use 1-64 letters, numbers, dots, underscores, or hyphens.");return s}function P(l){let s=new URL(l);if(s.protocol!=="http:"&&s.protocol!=="https:")throw new Error("Nodus base URL must use HTTP or HTTPS.");return s.toString().replace(/\/$/,"")}var I=()=>process.env.NODUS_CONFIG_FILE||y.join(homedir(),".config","nodus","config.json");async function U(){try{let l=JSON.parse(await readFile(I(),"utf8"));return {activeProfile:l.activeProfile||"default",profiles:l.profiles||{}}}catch{return {activeProfile:"default",profiles:{}}}}async function O(l){let s=I();await mkdir(y.dirname(s),{recursive:true,mode:448});let u=`${s}.${process.pid}.tmp`;await writeFile(u,`${JSON.stringify(l,null,2)}
|
|
3
|
+
`,{mode:384}),await chmod(u,384),await rename(u,s),await chmod(s,384);}async function z(l){let s=await U(),u=T(l||process.env.NODUS_PROFILE||s.activeProfile),f=s.profiles[u];return {profileName:u,apiKey:process.env.NODUS_API_KEY||f?.apiKey||"",baseUrl:P(process.env.NODUS_BASE_URL||f?.baseUrl||"https://nodus-api-preview-production.up.railway.app"),source:process.env.NODUS_API_KEY?"environment":f?"profile":"none"}}var $=promisify(execFile);async function ae(){let l=process.argv.slice(2),s=n=>{let a=l.indexOf(n);if(a<0)return;let d=l[a+1];if(!d||d.startsWith("--"))throw new Error(`Missing value for ${n}.`);return d},u=n=>{let a=s(n);if(!a)throw new Error(`Missing required option ${n}.`);return a},f=n=>{let a=[];for(let d=0;d<l.length;d+=1){if(l[d]!==n)continue;let c=l[d+1];if(!c||c.startsWith("--"))throw new Error(`Missing value for ${n}.`);a.push(c);}return a},i={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",pdf:"application/pdf"},b=n=>{let a=(y.extname(n).slice(1).replace(/[^A-Za-z0-9]/g,"")||"bin").slice(0,8),d=y.basename(n,y.extname(n)).replace(/[^A-Za-z0-9_-]/g,"-").slice(0,60);return /^[A-Za-z0-9]/.test(d)||(d=`f${d}`.slice(0,60)),`${d}.${a}`},A=async n=>Promise.all(n.map(async a=>({name:b(a),contentBase64:(await readFile(a)).toString("base64")}))),v=n=>{if(!n.length)return;let a={};for(let d of n){let c=d.indexOf("=");if(c<1)throw new Error(`--metadata expects key=value, got: ${d}`);a[d.slice(0,c)]=d.slice(c+1);}return a},C=async(n,a,d)=>{let c=new Array(n.length),g=0;return await Promise.all(Array.from({length:Math.min(a,n.length)},async()=>{for(;g<n.length;){let m=g;g+=1,c[m]=await d(n[m],m);}})),c},k=await z(s("--profile")),e=G({baseUrl:k.baseUrl,apiKey:k.apiKey}),t=n=>process.stdout.write(JSON.stringify(n,null,2)+`
|
|
4
|
+
`),[r,o,p]=l;if(!r||r==="help"||r==="--help"||r==="-h")return process.stdout.write(["Usage:"," nodus login [--profile <name>] [--base-url <url>] [--no-open]"," nodus auth status [--profile <name>]"," nodus logout [--profile <name>]"," nodus sdk"," nodus capabilities"," nodus agents list"," nodus agents deploy --provider <provider> --api-key <key> [--name <displayName>] [--model <id>] [--owner-tag <tag>]"," nodus agents quota --agent <id> [--refresh]"," nodus agents configure --agent <id> [--model <id>|--default-model] [--reasoning <level>|--default-reasoning]"," nodus agents concurrency --agent <id> --max <1-160>"," nodus run --agent <id> (--prompt <text>|--instructions <text>) [--goal <markdown>|--goal-file <path>] [--output-schema <file>] [--attach <file>]... [--metadata key=value]... [--wait]"," nodus run --provider <provider> [--model <model>] (--prompt <text>|--instructions <text>) [--goal <markdown>|--goal-file <path>] [--output-schema <file>] [--attach <file>]... [--metadata key=value]... [--wait]"," nodus runs list [--agent <id>] [--status <status>] [--metadata key=value]... [--limit <n>] [--cursor <cursor>]"," nodus runs queue"," nodus runs create --from-jsonl <file> [--concurrency <n>]"," nodus runs export [--agent <id>] [--status <status>] [--concurrency <n>]"," nodus runs get|events|cancel --run <id>"," nodus runs artifact --run <id> --artifact <id> [--output <file>]"," nodus session list"," nodus session create --agent <id> [--persistent]"," nodus session get|events|telemetry|telemetry-history|diff|ports|terminal-ticket|pause|resume|activate|destroy --session <id>"," nodus session git --session <id> --action commit|revert [--message <text>] [--path <path>]"," nodus session run --session <id> --instructions <text> [--attach <file>]... [--wait]"," nodus session exec --session <id> --command <shell command>"," nodus desktop connect --folder <dir>"," nodus desktop list"," nodus desktop get --desktop <id>"," nodus desktop rename --desktop <id> --name <name>"," nodus desktop browse --desktop <id> [--path <path>] [--files]"," nodus desktop read --desktop <id> --path <path>"," nodus desktop ports|terminal-ticket --desktop <id>"," nodus desktop attach --desktop <id> --file <path> [--name <name>]"," nodus desktop reveal --desktop <id> --path <path> [--app finder|editor|terminal]"," nodus desktop bind --desktop <id> [--agent <id>|--none]"," nodus desktop disconnect --desktop <id>"," nodus workspace create [--name <name>]"," nodus workspace list"," nodus workspace get|revisions|archive --workspace <id>"," nodus workspace build --workspace <id> [--context <dir>] [--dockerfile <path>]"," nodus workspace builder start --workspace <id> [--base-revision <id>]"," nodus workspace builder exec --build <id> -- <command>"," nodus workspace builder put --build <id> --file <local> [--path <remote>]"," nodus workspace builder get --build <id> --path <remote> [--output <local>]"," nodus workspace builder files|status|publish|destroy --build <id>","","Environment: NODUS_API_KEY, optional NODUS_BASE_URL and NODUS_PROFILE",""].join(`
|
|
5
|
+
`));if(r==="login"){let n=T(s("--profile")||process.env.NODUS_PROFILE),a=P(s("--base-url")||process.env.NODUS_BASE_URL||"https://nodus-api-production-d291.up.railway.app"),d=await fetch(`${a}/cli-auth/start`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:hostname(),platform:platform()})}),c=await d.json();if(!d.ok||!c.deviceCode||!c.userCode||!c.verificationUri)throw new Error(c.error||`Login start failed: ${d.status}`);console.log(`
|
|
6
|
+
Authorize Nodus CLI in your browser:
|
|
7
|
+
code: ${c.userCode}
|
|
8
|
+
open: ${c.verificationUri}
|
|
9
|
+
`),l.includes("--no-open")||await ue(c.verificationUri).catch(()=>{});let g=Date.now()+(c.expiresIn??600)*1e3;for(;Date.now()<g;){await new Promise(x=>setTimeout(x,(c.interval??3)*1e3));let m=await fetch(`${a}/cli-auth/poll`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({deviceCode:c.deviceCode})}),w=await m.json();if(!m.ok)throw new Error(w.error||`Login poll failed: ${m.status}`);if(w.status!=="complete"||!w.apiKey)continue;let S=await U();S.activeProfile=n,S.profiles[n]={apiKey:w.apiKey,baseUrl:a,keyId:w.keyId,createdAt:new Date().toISOString()},await O(S),console.log(`Logged in as profile ${n}. Credentials saved to ${I()}.`);return}throw new Error("Login timed out. Run `nodus login` again.")}if(r==="auth"&&o==="status"){if(!k.apiKey)return t({authenticated:false,profile:k.profileName,source:k.source,baseUrl:k.baseUrl,configFile:I()});try{let n=await e.agents.list();return t({authenticated:!0,profile:k.profileName,source:k.source,baseUrl:k.baseUrl,configFile:I(),agentCount:n.length})}catch(n){if(n instanceof h&&n.status===401)return t({authenticated:false,profile:k.profileName,source:k.source,baseUrl:k.baseUrl,configFile:I(),error:n.message});throw n}}if(r==="logout"){let n=await U(),a=T(s("--profile")||process.env.NODUS_PROFILE||n.activeProfile),d=n.profiles[a];if(!d){console.log(`Profile ${a} is not logged in.`);return}let c=await fetch(`${d.baseUrl.replace(/\/$/,"")}/cli-auth/logout`,{method:"POST",headers:{authorization:`Bearer ${d.apiKey}`}});if(!c.ok&&c.status!==401){let g=await c.json().catch(()=>({}));throw new Error(g.error||`Logout failed: ${c.status}`)}delete n.profiles[a],n.activeProfile===a&&(n.activeProfile=Object.keys(n.profiles)[0]||"default"),await O(n),console.log(`Logged out profile ${a}.`);return}if(r==="capabilities"||r==="sdk")return t({sdkVersion:q,capabilities:j(e),features:L(),commands:W()});if(r==="agents"){if(o==="list")return t({agents:await e.agents.list()});if(o==="deploy"){let a=u("--provider"),d=u("--api-key"),c=s("--name"),g=s("--model"),m=s("--owner-tag"),w={...g?{modelId:g}:{},...m?{ownerTag:m}:{}};return t({agent:await e.agents.deploy({provider:a,apiKey:d,...c?{displayName:c}:{},...Object.keys(w).length?{metadata:w}:{}})})}let n=u("--agent");if(o==="quota")return t(await e.agents.quota(n,{refresh:l.includes("--refresh")}));if(o==="configure"){let a=s("--reasoning");if(a&&!["low","medium","high","xhigh","max"].includes(a))throw new Error("--reasoning must be low, medium, high, xhigh, or max.");let d={...l.includes("--default-model")?{modelId:null}:s("--model")?{modelId:s("--model")}:{},...l.includes("--default-reasoning")?{reasoningEffort:null}:a?{reasoningEffort:a}:{}};if(Object.keys(d).length===0)throw new Error("Provide --model, --default-model, --reasoning, or --default-reasoning.");return t(await e.agents.update(n,d))}if(o==="concurrency"){let a=Number(u("--max"));if(!Number.isInteger(a)||a<1||a>160)throw new Error("--max must be an integer from 1 to 160.");return t(await e.agents.setRunConcurrency(n,a))}}if(r==="runs"){if(o==="list")return t(await e.agentRuns.list({agentId:s("--agent"),status:s("--status"),limit:s("--limit")?Number(s("--limit")):void 0,cursor:s("--cursor"),metadata:v(f("--metadata"))}));if(o==="queue")return t(await e.agentRuns.queueStats());if(o==="create"&&s("--from-jsonl")){let a=s("--concurrency")?Number(s("--concurrency")):8,d=(await readFile(u("--from-jsonl"),"utf8")).split(`
|
|
10
|
+
`).map(c=>c.trim()).filter(Boolean);await C(d,a,async(c,g)=>{try{process.stdout.write(JSON.stringify({line:g+1,...await e.agentRuns.create(JSON.parse(c))})+`
|
|
11
|
+
`);}catch(m){process.stdout.write(JSON.stringify({line:g+1,error:m instanceof Error?m.message:String(m)})+`
|
|
12
|
+
`);}});return}if(o==="export"){let a=s("--concurrency")?Number(s("--concurrency")):8,d={agentId:s("--agent"),status:s("--status")},c=[],g=s("--cursor");do{let w=await e.agentRuns.list({...d,limit:100,cursor:g});c.push(...w.runs),g=w.nextCursor??void 0;}while(g);let m=await C(c,a,async w=>(await e.agentRuns.get(w.id)).run);for(let w of m)process.stdout.write(JSON.stringify(w)+`
|
|
13
|
+
`);return}let n=u("--run");if(o==="get")return t(await e.agentRuns.get(n));if(o==="events")return t(await e.agentRuns.events(n));if(o==="cancel")return t(await e.agentRuns.cancel(n));if(o==="artifact"){let a=await e.agentRuns.getArtifactDownloadUrl(n,u("--artifact")),d=s("--output");if(!d)return t(a);let c=await fetch(a.url);if(!c.ok)throw new Error(`Artifact download failed: ${c.status}`);return await writeFile(d,Buffer.from(await c.arrayBuffer())),t({ok:true,output:d})}}if(r==="session"){if(o==="list")return t(await e.sessions.list({status:s("--status"),limit:s("--limit")?Number(s("--limit")):void 0,cursor:s("--cursor")}));if(o==="create")return t(await e.sessions.create({agentId:u("--agent"),workspaceRevisionId:s("--workspace-revision"),desktopRuntimeId:s("--desktop-runtime"),...l.includes("--persistent")?{persistent:true}:{}}));let n=u("--session");if(o==="get")return t(await e.sessions.get(n));if(o==="events")return t(await e.sessions.events(n,{after:s("--after")?Number(s("--after")):void 0}));if(o==="telemetry")return t(await e.sessions.telemetry(n));if(o==="telemetry-history")return t(await e.sessions.telemetryHistory(n,{startMs:s("--start-ms")?Number(s("--start-ms")):void 0,endMs:s("--end-ms")?Number(s("--end-ms")):void 0,stepSec:s("--step-sec")?Number(s("--step-sec")):void 0}));if(o==="diff")return t(await e.sessions.diff(n,s("--known")));if(o==="ports")return t(await e.sessions.ports(n,s("--known")));if(o==="terminal-ticket")return t(await e.sessions.terminalTicket(n));if(o==="git"){let a=u("--action");if(a!=="commit"&&a!=="revert")throw new Error("--action must be commit or revert.");return t(await e.sessions.git(n,{action:a,message:s("--message"),path:s("--path")}))}if(o==="run"){let a=await A(f("--attach")),d=a.length?{attachments:a}:{};return t(l.includes("--wait")?await e.sessions.runAndWait(n,u("--instructions"),d):await e.sessions.run(n,u("--instructions"),d))}if(o==="exec")return t(await e.sessions.execWorkspace(n,u("--command"),s("--timeout")?Number(s("--timeout")):void 0));if(o==="pause")return t(await e.sessions.pause(n));if(o==="resume")return t(await e.sessions.resume(n));if(o==="activate")return t(await e.sessions.activate(n));if(o==="destroy")return t(await e.sessions.destroy(n))}if(r==="workspace"&&o==="create")return t(await e.workspaces.create({name:s("--name")??"Workspace"}));if(r==="workspace"&&o==="list")return t(await e.workspaces.list());if(r==="workspace"&&o==="get")return t(await e.workspaces.get(u("--workspace")));if(r==="workspace"&&o==="archive")return t(await e.workspaces.archive(u("--workspace")));if(r==="workspace"&&o==="revisions")return t(await e.workspaces.revisions(u("--workspace")));if(r==="workspace"&&o==="builder"){if(p==="start")return t(await e.workspaces.startBuilder(u("--workspace"),{baseRevisionId:s("--base-revision")}));if(p==="exec"){let n=l.includes("--")?l.slice(l.indexOf("--")+1).join(" "):"";if(!n)throw new Error("Missing command after --.");return t(await e.workspaces.exec(u("--build"),n))}if(p==="put"){let n=u("--file");return t(await e.workspaces.upload(u("--build"),s("--path")??y.basename(n),await readFile(n)))}if(p==="get"){let n=await e.workspaces.download(u("--build"),u("--path")),a=s("--output");if(a)return await writeFile(a,n),t({ok:true,output:a});process.stdout.write(n);return}if(p==="files")return t(await e.workspaces.listFiles(u("--build"),s("--path")));if(p==="publish")return t(await e.workspaces.publish(u("--build")));if(p==="destroy")return t(await e.workspaces.destroyBuilder(u("--build")));if(p==="status")return t(await e.workspaces.getBuild(u("--build")))}if(r==="workspace"&&(o==="build"||o==="dockerfile-build")){let n=u("--workspace"),a=y.resolve(s("--context")??"."),d=await mkdtemp(y.join(tmpdir(),"nodus-workspace-"));try{let c=y.join(d,"context.tar.gz");await $("tar",["--exclude=.git","-czf",c,"-C",a,"."]);let g=await e.workspaces.createContextUpload(n),m=await fetch(g.uploadUrl,{method:"PUT",headers:{"content-type":"application/gzip"},body:await readFile(c)});if(!m.ok)throw new Error(`Context upload failed: ${m.status}`);return t(await e.workspaces.buildFromDockerfile(n,{contextBlobKey:g.objectKey,dockerfilePath:s("--dockerfile")}))}finally{await rm(d,{recursive:true,force:true});}}if(r==="workspace"&&o==="builder-start")return t(await e.workspaces.startBuilder(u("--workspace"),{baseRevisionId:s("--base-revision")}));if(r==="workspace"&&o==="exec"){let n=l.includes("--")?l.slice(l.indexOf("--")+1).join(" "):"";if(!n)throw new Error("Missing command after --.");return t(await e.workspaces.exec(u("--build"),n))}if(r==="workspace"&&o==="publish")return t(await e.workspaces.publish(u("--build")));if(r==="workspace"&&o==="destroy")return t(await e.workspaces.destroyBuilder(u("--build")));if(r==="desktop"&&o==="list")return t(await e.desktopRuntimes.list({agentId:s("--agent")}));if(r==="desktop"&&o==="get")return t(await e.desktopRuntimes.get(u("--desktop")));if(r==="desktop"&&o==="rename")return t(await e.desktopRuntimes.rename(u("--desktop"),u("--name")));if(r==="desktop"&&o==="browse")return t(await e.desktopRuntimes.browse(u("--desktop"),{path:s("--path"),dirsOnly:!l.includes("--files")}));if(r==="desktop"&&o==="read")return t(await e.desktopRuntimes.read(u("--desktop"),u("--path")));if(r==="desktop"&&o==="ports")return t(await e.desktopRuntimes.ports(u("--desktop"),s("--known")));if(r==="desktop"&&o==="terminal-ticket")return t(await e.desktopRuntimes.terminalTicket(u("--desktop")));if(r==="desktop"&&o==="attach"){let n=u("--file");return t(await e.desktopRuntimes.attach(u("--desktop"),{name:s("--name")??y.basename(n),contentBase64:(await readFile(n)).toString("base64")}))}if(r==="desktop"&&o==="reveal"){let n=s("--app");if(n&&!["finder","editor","terminal"].includes(n))throw new Error("--app must be finder, editor, or terminal.");return t(await e.desktopRuntimes.reveal(u("--desktop"),{path:u("--path"),app:n}))}if(r==="desktop"&&o==="bind")return t(await e.desktopRuntimes.bind(u("--desktop"),l.includes("--none")?null:u("--agent")));if(r==="desktop"&&o==="disconnect")return t(await e.desktopRuntimes.disconnect(u("--desktop")));if(r==="desktop"&&(o==="connect"||o==="agent")){let{connect:n}=await import('./desktop-agent-J5A2KK2P.js');return n({baseUrl:process.env.NODUS_BASE_URL??"https://nodus-api-production-d291.up.railway.app",folder:s("--folder")??"~",name:s("--name")})}if(r==="run"){let n=s("--agent"),a=s("--provider");if(!n&&!a)throw new Error("run requires --agent or --provider");let d=await Promise.all(f("--attach").map(async D=>{let B=b(D),_=B.split(".").pop()??"";return {contentBase64:(await readFile(D)).toString("base64"),path:`input/${B}`,...i[_]?{contentType:i[_]}:{}}})),c=s("--prompt")||s("--instructions");if(!c)throw new Error("Provide --prompt or --instructions.");let g=s("--goal-file"),m=s("--goal");if(g&&m)throw new Error("Provide either --goal or --goal-file.");let w=g?await readFile(y.resolve(g),"utf8"):m,S=v(f("--metadata")),x=s("--output-schema"),N=x?JSON.parse(await readFile(y.resolve(x),"utf8")):void 0,H={...n?{agentId:n}:{provider:a,model:s("--model")},...d.length?{resources:d}:{},workspaceRevisionId:s("--workspace-revision"),desktopRuntimeId:s("--desktop-runtime"),prompt:c,...w?{mode:"goal",goal:w}:{},...l.includes("--persistent")?{persistent:true}:{},...S?{metadata:S}:{},...N?{outputSchema:N}:{}},M=await e.agentRuns.create(H);return t(l.includes("--wait")?{run:await e.agentRuns.wait(M.run.id)}:M)}throw new Error("Unknown command. Run `nodus help`.")}async function ue(l){return process.platform==="darwin"?$("open",[l]):process.platform==="win32"?$("cmd",["/c","start","",l]):$("xdg-open",[l])}ae().catch(l=>{l instanceof h?console.error(JSON.stringify({error:l.message,status:l.status,code:l.code,retryAt:l.retryAt},null,2)):console.error(l instanceof Error?l.message:String(l)),process.exitCode=1;});
|