@nodii/dev 0.6.26 → 0.6.28

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/dist/lanes.js CHANGED
@@ -182,6 +182,18 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
182
182
  if (action === "heartbeat") {
183
183
  return { payload: await request("POST", `/api/v1/lanes/${encode(id)}/heartbeat`, { ...(flags.note !== undefined ? { note: flags.note } : {}), ...via }), human: `${ref}: heartbeat recorded.` };
184
184
  }
185
+ if (action === "subagent-quota") {
186
+ const raw = need(args[1], "lane subagent-quota needs <key> <n>");
187
+ const quota = Number(raw);
188
+ if (!Number.isInteger(quota) || quota < 0 || quota > 100)
189
+ throw new CliError("USAGE", "Subagent quota must be an integer from 0 to 100.", 2);
190
+ const payload = await request(
191
+ "PUT",
192
+ `/api/v1/lanes/${encode(id)}/subagent-quota`,
193
+ { quota, ...via },
194
+ );
195
+ return { payload, human: `${ref}: subagent quota set to ${quota}.` };
196
+ }
185
197
  if (action === "land") return { payload: await request("POST", `/api/v1/lanes/${encode(id)}/land`, via), human: `${ref}: recorded on main.` };
186
198
  if (action === "review") {
187
199
  const body = await reviewBody(flags, read);
@@ -11,7 +11,7 @@ export const CODEX_HOOK_EVENTS = [
11
11
  ];
12
12
 
13
13
  export const managedHooksDir = (home = homedir()) => join(home, ".config", "nodii-dev", "codex-managed-hooks");
14
- export const MANAGED_DISPATCH_MIN_VERSION = "0.6.26";
14
+ export const MANAGED_DISPATCH_MIN_VERSION = "0.6.27";
15
15
 
16
16
  /** Only a single nodii-dev invocation is allowed; shell composition is never accepted. */
17
17
  export function nodiiCommandArgs(command) {
@@ -110,33 +110,36 @@ function responseOf(stdout) {
110
110
 
111
111
  /** Execute the current generated hook set behind the stable managed dispatcher. */
112
112
  export async function dispatchManagedHook(event, payloadText, deps = {}) {
113
- if (!CODEX_HOOK_EVENTS.includes(event)) throw new CliError("USAGE", `Unknown managed Codex hook event: ${event}`, 2);
114
- const env = deps.env ?? process.env;
115
- if (!laneConfig(env)) return emptyResponse(event);
116
- const dir = deps.dir ?? env.NODII_DEV_MANAGED_HOOKS_DIR ?? managedHooksDir();
117
- let config;
118
- try { config = JSON.parse(await (deps.read ?? readFile)(join(dir, "hooks.json"), "utf8")); }
119
- catch (error) { throw new CliError("HARNESS_SETTINGS_INVALID", `Cannot read managed Codex hooks: ${error.message}`, 2); }
120
- validateManagedHooks(config.hooks ?? {});
121
- const payload = JSON.parse(payloadText);
122
- const target = matchTarget(event, payload);
123
- const handlers = (config.hooks?.[event] ?? []).filter((entry) => {
124
- try { return new RegExp(entry.matcher || ".*").test(target); } catch { return false; }
125
- }).flatMap((entry) => entry.hooks ?? []);
126
- const results = await Promise.all(handlers.map((handler) => (deps.run ?? runHandler)(nodiiCommandArgs(handler.command), payloadText, deps)));
127
- const messages = results.map((result) => responseOf(result.stdout)).filter(Boolean);
128
- const errors = results.filter((result) => result.status !== 0).map((result) => result.stderr.trim() || result.stdout.trim()).filter(Boolean);
129
- if (["PreToolUse", "PermissionRequest"].includes(event)) {
130
- const denied = messages.find((message) => message.hookSpecificOutput?.permissionDecision === "deny");
131
- if (denied) return `${JSON.stringify(denied)}\n`;
132
- if (errors.length) return `${JSON.stringify({ hookSpecificOutput: { hookEventName: event, permissionDecision: "deny", permissionDecisionReason: errors.join("\n") } })}\n`;
133
- const contexts = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
134
- return contexts.length ? `${JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: contexts.join("\n") } })}\n` : "";
113
+ try {
114
+ if (!CODEX_HOOK_EVENTS.includes(event)) return emptyResponse(event);
115
+ const env = deps.env ?? process.env;
116
+ if (!laneConfig(env)) return emptyResponse(event);
117
+ const dir = deps.dir ?? env.NODII_DEV_MANAGED_HOOKS_DIR ?? managedHooksDir();
118
+ const config = JSON.parse(await (deps.read ?? readFile)(join(dir, "hooks.json"), "utf8"));
119
+ validateManagedHooks(config.hooks ?? {});
120
+ const payload = JSON.parse(payloadText);
121
+ const target = matchTarget(event, payload);
122
+ const handlers = (config.hooks?.[event] ?? []).filter((entry) => {
123
+ try { return new RegExp(entry.matcher || ".*").test(target); } catch { return false; }
124
+ }).flatMap((entry) => entry.hooks ?? []);
125
+ const results = await Promise.all(handlers.map((handler) => (deps.run ?? runHandler)(nodiiCommandArgs(handler.command), payloadText, deps)));
126
+ // A failed handler is unavailable policy, not a denial. Ignore both its
127
+ // output and stderr; doctor is where hook health remains visible.
128
+ const successful = results.filter((result) => result.status === 0);
129
+ const messages = successful.map((result) => responseOf(result.stdout)).filter(Boolean);
130
+ if (["PreToolUse", "PermissionRequest"].includes(event)) {
131
+ const denied = messages.find((message) => message.hookSpecificOutput?.permissionDecision === "deny");
132
+ if (denied) return `${JSON.stringify(denied)}\n`;
133
+ const contexts = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
134
+ return contexts.length ? `${JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: contexts.join("\n") } })}\n` : "";
135
+ }
136
+ const structured = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
137
+ const plain = successful.map((result) => responseOf(result.stdout) ? "" : result.stdout.trim()).filter(Boolean);
138
+ if (["Stop", "SubagentStop"].includes(event)) return "{}\n";
139
+ return [...structured, ...plain].join("\n") + ([...structured, ...plain].length ? "\n" : "");
140
+ } catch {
141
+ return emptyResponse(event);
135
142
  }
136
- const structured = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
137
- const plain = results.map((result) => responseOf(result.stdout) ? "" : result.stdout.trim()).filter(Boolean);
138
- if (["Stop", "SubagentStop"].includes(event)) return "{}\n";
139
- return [...structured, ...plain, ...errors].join("\n") + ([...structured, ...plain, ...errors].length ? "\n" : "");
140
143
  }
141
144
 
142
145
  const versionParts = (value) => String(value ?? "").split(".").map((part) => Number.parseInt(part, 10));
package/dist/nodii-dev.js CHANGED
@@ -1,4 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { fail, main } from "./cli.js";
3
3
 
4
- main().catch(fail);
4
+ // Hooks run in the agent's environment, which may be incomplete or may not be
5
+ // a git worktree. An unavailable hook must never turn into a refused tool call.
6
+ // Intentional policy denials are handled inside the hook command before they
7
+ // reach this boundary.
8
+ main().catch((error) => {
9
+ const denial = String(error?.code ?? "").startsWith("SAFETY_") ||
10
+ ["FIGMA_FILE_KEY_MISSING", "FIGMA_LOCK_DENIED", "WRITE_OUT_OF_SCOPE"].includes(error?.code);
11
+ if (process.argv[2] === "hook" && !denial) {
12
+ process.exitCode = 0;
13
+ return;
14
+ }
15
+ fail(error);
16
+ });
package/dist/runs.js ADDED
@@ -0,0 +1,341 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { basename } from "node:path";
3
+ import { CliError, devRequest, encode } from "./core.js";
4
+
5
+ export const RUN_ID_ENV = "NODII_DEV_RUN_ID";
6
+ const DEFAULT_HEARTBEAT_MS = 60_000;
7
+ const DEFAULT_TAIL_BYTES = 8 * 1024;
8
+ const RUN_KINDS = new Set(["build", "test_run", "subagent", "deploy"]);
9
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
+ const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
11
+
12
+ export function redactRunText(value) {
13
+ return String(value)
14
+ .replace(/-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi, "[REDACTED PRIVATE KEY]")
15
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]")
16
+ .replace(/\b(?:github_pat_|gh[pousr]_|sk-)[A-Za-z0-9_-]{12,}\b/g, "[REDACTED]")
17
+ .replace(
18
+ /\b((?=[A-Za-z0-9_-]*(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key))[A-Za-z][A-Za-z0-9_-]*\s*[:=]\s*)[^\s,;]+/gi,
19
+ "$1[REDACTED]",
20
+ )
21
+ .replace(/([a-z][a-z0-9+.-]*:\/\/[^\s:/]+:)[^@\s]+@/gi, "$1[REDACTED]@");
22
+ }
23
+
24
+ function appendTail(current, chunk, maxBytes) {
25
+ const next = Buffer.concat([current, Buffer.from(chunk)]);
26
+ return next.length <= maxBytes ? next : next.subarray(next.length - maxBytes);
27
+ }
28
+
29
+ function writeSafe(write, value) {
30
+ try {
31
+ write(value);
32
+ return true;
33
+ } catch (error) {
34
+ if (error?.code !== "EPIPE") throw error;
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function currentRunId(runId = process.env[RUN_ID_ENV]) {
40
+ if (runId === undefined || runId === "") return null;
41
+ if (!UUID.test(runId))
42
+ throw new CliError(
43
+ "RUN_CONTEXT_INVALID",
44
+ `${RUN_ID_ENV} must be the UUID of this lane, subagent, or coordinator run.`,
45
+ 2,
46
+ );
47
+ return runId;
48
+ }
49
+
50
+ function evidenceText(evidence) {
51
+ return (evidence ?? [])
52
+ .map((item) => {
53
+ const head = ` ${redactRunText(item.kind)}: ${redactRunText(item.ref)}`;
54
+ if (!item.note) return head;
55
+ return `${head}\n${redactRunText(item.note)
56
+ .split("\n")
57
+ .map((line) => ` ${line}`)
58
+ .join("\n")}`;
59
+ })
60
+ .join("\n");
61
+ }
62
+
63
+ function resultText(result) {
64
+ const lines = [
65
+ `Result ${redactRunText(result.id)} · ${redactRunText(result.outcome)} · run ${redactRunText(result.sourceRunId)}`,
66
+ ];
67
+ if (result.failureReason)
68
+ lines.push(` reason: ${redactRunText(result.failureReason)}`);
69
+ if (result.handoff) lines.push(` handoff: ${redactRunText(result.handoff)}`);
70
+ const evidence = evidenceText(result.evidence);
71
+ if (evidence) lines.push(evidence);
72
+ return lines.join("\n");
73
+ }
74
+
75
+ export async function showWaitingResults({
76
+ runId = process.env[RUN_ID_ENV],
77
+ request = devRequest,
78
+ write = (value) => process.stderr.write(value),
79
+ acknowledge = true,
80
+ } = {}) {
81
+ const waiterRunId = currentRunId(runId);
82
+ if (!waiterRunId) return [];
83
+ const payload = await request(
84
+ "GET",
85
+ `/api/v1/runs/${encode(waiterRunId)}/results`,
86
+ );
87
+ const results = payload.data ?? [];
88
+ if (!results.length) return [];
89
+
90
+ const rendered = writeSafe(
91
+ write,
92
+ `${results.map(resultText).join("\n\n")}\n`,
93
+ );
94
+ if (!rendered || !acknowledge) return results;
95
+ for (const result of results) {
96
+ try {
97
+ await request(
98
+ "POST",
99
+ `/api/v1/results/${encode(result.id)}/acknowledge`,
100
+ { waiterRunId },
101
+ );
102
+ } catch (error) {
103
+ writeSafe(
104
+ write,
105
+ `Result ${redactRunText(result.id)} remains waiting: acknowledgement failed (${redactRunText(error?.code ?? error?.message ?? "error")}).\n`,
106
+ );
107
+ }
108
+ }
109
+ return results;
110
+ }
111
+
112
+ function commandLabel(command) {
113
+ return basename(command[0] ?? "command").slice(0, 240) || "command";
114
+ }
115
+
116
+ function runKind(value) {
117
+ const kind = value ?? "build";
118
+ if (!RUN_KINDS.has(kind))
119
+ throw new CliError(
120
+ "USAGE",
121
+ `run --kind must be one of ${[...RUN_KINDS].join(", ")}.`,
122
+ 2,
123
+ );
124
+ return kind;
125
+ }
126
+
127
+ function runEvidence(command, exitCode, signal, tail) {
128
+ const evidence = [
129
+ { kind: "command", ref: commandLabel(command) },
130
+ { kind: "exit_code", ref: String(exitCode ?? -1) },
131
+ {
132
+ kind: "log_tail",
133
+ ref: "combined stdout/stderr",
134
+ note: redactRunText(tail.toString("utf8")) || "(no output)",
135
+ },
136
+ ];
137
+ if (signal) evidence.push({ kind: "signal", ref: signal });
138
+ return evidence;
139
+ }
140
+
141
+ async function endRun(request, runId, body, attempts = 3) {
142
+ let error;
143
+ for (let attempt = 0; attempt < attempts; attempt++) {
144
+ try {
145
+ return await request("POST", `/api/v1/runs/${encode(runId)}/end`, body);
146
+ } catch (caught) {
147
+ error = caught;
148
+ if (attempt + 1 < attempts)
149
+ await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)));
150
+ }
151
+ }
152
+ throw error;
153
+ }
154
+
155
+ export async function superviseRun({
156
+ command,
157
+ waiterRunId = process.env[RUN_ID_ENV],
158
+ kind,
159
+ name,
160
+ expectedHeartbeatSeconds = 60,
161
+ heartbeatIntervalMs = DEFAULT_HEARTBEAT_MS,
162
+ tailBytes = DEFAULT_TAIL_BYTES,
163
+ request = devRequest,
164
+ spawn = nodeSpawn,
165
+ stdoutWrite = (value) => process.stdout.write(value),
166
+ stderrWrite = (value) => process.stderr.write(value),
167
+ } = {}) {
168
+ const waiter = currentRunId(waiterRunId);
169
+ if (!waiter)
170
+ throw new CliError(
171
+ "RUN_CONTEXT_REQUIRED",
172
+ `nodii-dev run needs ${RUN_ID_ENV}, supplied by the harness launcher for the current waiter run.`,
173
+ 2,
174
+ );
175
+ if (!Array.isArray(command) || !command.length || !command[0])
176
+ throw new CliError("USAGE", "run needs a command after `--`.", 2);
177
+ const selectedKind = runKind(kind);
178
+ const label = redactRunText(String(name ?? commandLabel(command)))
179
+ .trim()
180
+ .slice(0, 300);
181
+ if (!label) throw new CliError("USAGE", "run --name cannot be empty.", 2);
182
+ const registered = await request("POST", "/api/v1/runs", {
183
+ kind: selectedKind,
184
+ name: label,
185
+ externalRef: `pid:${process.pid}`,
186
+ waiterRunId: waiter,
187
+ expectedHeartbeatSeconds,
188
+ metadata: { executable: redactRunText(commandLabel(command)) },
189
+ });
190
+ const runId = registered.data?.id;
191
+ if (!runId) throw new CliError("RUN_REGISTER_INVALID", "Dev registered a run without an id.", 1);
192
+
193
+ let tail = Buffer.alloc(0);
194
+ let spawnError = null;
195
+ let heartbeatInFlight = false;
196
+ let child;
197
+ let timer;
198
+ const forwardSignals = new Map();
199
+ for (const signal of SIGNALS) {
200
+ const forward = () => {
201
+ if (child?.pid && child.exitCode === null && child.signalCode === null)
202
+ child.kill(signal);
203
+ };
204
+ forwardSignals.set(signal, forward);
205
+ process.on(signal, forward);
206
+ }
207
+
208
+ try {
209
+ try {
210
+ child = spawn(command[0], command.slice(1), {
211
+ shell: false,
212
+ stdio: ["inherit", "pipe", "pipe"],
213
+ });
214
+ } catch (error) {
215
+ const evidence = runEvidence(command, 1, null, tail);
216
+ await endRun(request, runId, {
217
+ status: "failed",
218
+ reason: `Command could not start: ${redactRunText(error?.code ?? error?.message ?? "error")}`,
219
+ evidence,
220
+ });
221
+ return {
222
+ runId,
223
+ exitCode: 1,
224
+ signal: null,
225
+ tail: "",
226
+ };
227
+ }
228
+ child.stdout?.on("data", (chunk) => {
229
+ tail = appendTail(tail, chunk, tailBytes);
230
+ writeSafe(stdoutWrite, chunk);
231
+ });
232
+ child.stderr?.on("data", (chunk) => {
233
+ tail = appendTail(tail, chunk, tailBytes);
234
+ writeSafe(stderrWrite, chunk);
235
+ });
236
+
237
+ const heartbeat = async () => {
238
+ if (heartbeatInFlight) return;
239
+ heartbeatInFlight = true;
240
+ try {
241
+ await request("POST", `/api/v1/runs/${encode(runId)}/heartbeat`, {
242
+ note: `command pid ${child.pid ?? "starting"}`,
243
+ });
244
+ } catch (error) {
245
+ writeSafe(
246
+ stderrWrite,
247
+ `nodii-dev run heartbeat failed (${redactRunText(error?.code ?? error?.message ?? "error")}); retrying.\n`,
248
+ );
249
+ } finally {
250
+ heartbeatInFlight = false;
251
+ }
252
+ };
253
+ timer = setInterval(heartbeat, heartbeatIntervalMs);
254
+ const outcome = await new Promise((resolve) => {
255
+ child.once("error", (error) => {
256
+ spawnError = error;
257
+ });
258
+ child.once("close", (exitCode, signal) => resolve({ exitCode, signal }));
259
+ });
260
+ clearInterval(timer);
261
+ timer = undefined;
262
+ while (heartbeatInFlight)
263
+ await new Promise((resolve) => setTimeout(resolve, 5));
264
+
265
+ const evidence = runEvidence(
266
+ command,
267
+ outcome.exitCode,
268
+ outcome.signal,
269
+ tail,
270
+ );
271
+ const succeeded = outcome.exitCode === 0 && !outcome.signal && !spawnError;
272
+ const reason = spawnError
273
+ ? `Command could not start: ${redactRunText(spawnError.code ?? spawnError.message)}`
274
+ : outcome.signal
275
+ ? `Command ended by signal ${outcome.signal}`
276
+ : `Command exited with code ${outcome.exitCode ?? -1}`;
277
+ await endRun(
278
+ request,
279
+ runId,
280
+ succeeded
281
+ ? { status: "done", evidence }
282
+ : { status: "failed", reason, evidence },
283
+ );
284
+ return {
285
+ runId,
286
+ exitCode: succeeded ? 0 : (outcome.exitCode ?? 1),
287
+ signal: outcome.signal,
288
+ tail: redactRunText(tail.toString("utf8")),
289
+ };
290
+ } finally {
291
+ if (timer) clearInterval(timer);
292
+ for (const [signal, forward] of forwardSignals)
293
+ process.off(signal, forward);
294
+ }
295
+ }
296
+
297
+ export async function launchRunSupervisor(
298
+ command,
299
+ {
300
+ entrypoint = process.argv[1],
301
+ spawn = nodeSpawn,
302
+ env = process.env,
303
+ kind,
304
+ name,
305
+ } = {},
306
+ ) {
307
+ if (!entrypoint)
308
+ throw new CliError("RUN_SUPERVISOR_UNAVAILABLE", "Cannot locate the nodii-dev entrypoint.", 1);
309
+ const args = [entrypoint, "__run-supervisor"];
310
+ if (kind) args.push("--kind", kind);
311
+ if (name) args.push("--name", name);
312
+ args.push("--", ...command);
313
+ const supervisor = spawn(process.execPath, args, {
314
+ detached: true,
315
+ shell: false,
316
+ stdio: "inherit",
317
+ env,
318
+ });
319
+ const forwardSignals = new Map();
320
+ for (const signal of SIGNALS) {
321
+ const forward = () => {
322
+ if (
323
+ supervisor.pid &&
324
+ supervisor.exitCode === null &&
325
+ supervisor.signalCode === null
326
+ )
327
+ supervisor.kill(signal);
328
+ };
329
+ forwardSignals.set(signal, forward);
330
+ process.on(signal, forward);
331
+ }
332
+ try {
333
+ return await new Promise((resolve, reject) => {
334
+ supervisor.once("error", reject);
335
+ supervisor.once("close", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
336
+ });
337
+ } finally {
338
+ for (const [signal, forward] of forwardSignals)
339
+ process.off(signal, forward);
340
+ }
341
+ }
package/dist/upload.js CHANGED
@@ -168,6 +168,7 @@ const uploadBody = (docs, cls, flags, decision) => ({
168
168
  ...(flags["meaning-unchanged"] && cls === "editorial" ? { meaningUnchangedReason: String(flags["meaning-unchanged"]) } : {}),
169
169
  })),
170
170
  ...(decision ? { decision } : {}),
171
+ ...(flags.pending ? { pending: true } : {}),
171
172
  });
172
173
 
173
174
  export async function docsCommand(action, args, flags, { request = devRequest, log = console.error, cwd = process.cwd(), waitInterval } = {}) {
@@ -185,6 +186,12 @@ export async function docsCommand(action, args, flags, { request = devRequest, l
185
186
 
186
187
  if (action === "validate" || action === "upload") {
187
188
  const cls = changeClass(flags);
189
+ if (flags.pending !== undefined && flags.pending !== true)
190
+ throw new CliError("USAGE", "--pending is a switch and takes no value.", 2);
191
+ if (flags.pending && action !== "upload")
192
+ throw new CliError("USAGE", "--pending is valid only with docs upload.", 2);
193
+ if (flags.pending && cls !== "ruling")
194
+ throw new CliError("USAGE", "--pending requires --class ruling and --decision FILE.", 2);
188
195
  const decision = await readJsonFile(flags.decision, "decision");
189
196
  if (cls === "ruling" && !decision) throw new CliError("RULING_NEEDS_DECISION", "A ruling write needs its decision: --decision DECISION.json (title, context, options, ruling, consequences, provenance, amends).", 2);
190
197
  if (cls !== "ruling" && decision) throw new CliError("DECISION_NEEDS_RULING", "--decision goes with --class ruling only.", 2);
@@ -220,7 +227,9 @@ export async function docsCommand(action, args, flags, { request = devRequest, l
220
227
  payload: { data: result },
221
228
  human: [
222
229
  ...result.documents.map((d) => `${d.created ? "+" : "~"} ${d.code} ${d.version}\t${d.changeNote}`),
223
- ...(result.decision ? [`Locked ${result.decision.id}: ${result.decision.title}`] : []),
230
+ ...(result.decision
231
+ ? [`${result.decision.status === "pending" ? "Drafted" : "Locked"} ${result.decision.id}: ${result.decision.title}`]
232
+ : []),
224
233
  `Uploaded ${result.documents.length} document${result.documents.length === 1 ? "" : "s"} (${cls}).`,
225
234
  ].join("\n"),
226
235
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.26",
3
+ "version": "0.6.28",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {