agentradar-cli 0.3.7 → 0.3.8

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/channel.js CHANGED
@@ -71,7 +71,7 @@ export async function startChannel(runnerId, deps = {}) {
71
71
  });
72
72
  await noteCliUpdate(response.headers, logError);
73
73
  if (!response.ok) {
74
- const said = response.status === 426 ? await response.json().then((body) => body.error?.message).catch(() => null) : null;
74
+ const said = await response.json().then((body) => body.error?.message).catch(() => null);
75
75
  throw new Error(said ?? `AgentRadar refused the channel call (${response.status})`);
76
76
  }
77
77
  const text = await response.text();
package/dist/tool-run.js CHANGED
@@ -129,6 +129,23 @@ export function parseCallArgs(rest) {
129
129
  const pathOf = (tool) => typeof tool._meta?.cli?.path === "string" ? tool._meta.cli.path : undefined;
130
130
  const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
131
131
  const firstSentence = (text = "") => text.split(/(?<=[.!?])\s/)[0].trim();
132
+ // Vadym 14075: a hosted agent tried ~10 key sets for approvalDetails on request_egress and never found the real ones,
133
+ // so --help spells out a JSON argument's fields and these commands carry one complete example each.
134
+ // The approvals and browser skills quote this output word for word (tool-run.test.ts checks it).
135
+ const APPROVAL_EXAMPLES = {
136
+ request_egress: `--host supplier.example.com --reason 'The price list task needs this supplier website.' --approval-details '{"context":"The price list task needs prices from a supplier website.","reason":"No connector has these prices, and the website is the only source.","ifDeclined":"The price list stays unfinished.","decisionDetail":"Let my browser reach supplier.example.com."}'`,
137
+ request_spend_limit: `--monthly-limit-cents 5000 --reason 'The weekly reports need more AI time.' --approval-details '{"context":"I used most of my monthly AI limit by the 20th.","reason":"The weekly reports added this month need more AI time.","ifDeclined":"I pause the weekly reports until next month.","decisionDetail":"Raise my monthly AI limit to $50."}'`,
138
+ request_hire: `--title 'Support agent' --approval-details '{"context":"Customer emails wait two days for an answer.","reason":"Nobody on the team has time to answer them.","ifDeclined":"Emails keep waiting and some customers may leave.","decisionDetail":"Hire one support agent to answer customer emails."}'`,
139
+ request_permission: `--area projects --op write --approval-details '{"context":"I was asked to create a project for the spring launch.","reason":"My role can read projects but not create them.","ifDeclined":"Someone else has to create the project.","decisionDetail":"Let my role create and edit projects."}'`,
140
+ };
141
+ /** ` context (required, 1-600 characters): What happened: ...` for each field of a JSON argument. */
142
+ const fieldLines = (name, schema, root) => [`--${kebab(name)} is JSON with these fields:`,
143
+ ...Object.entries(schema.properties ?? {}).map(([field, own]) => {
144
+ // A field reusing another's rule arrives as {"$ref": "#/properties/approvalDetails/properties/context"} plus its own description.
145
+ const f = { ...own.$ref?.split("/").slice(1).reduce((at, key) => at?.[key], root), ...own };
146
+ const size = f.minLength !== undefined && f.maxLength !== undefined ? `, ${f.minLength}-${f.maxLength} characters` : "";
147
+ return ` ${field} (${schema.required?.includes(field) ? "required" : "optional"}${size})${f.description ? `: ${f.description}` : ""}`;
148
+ })];
132
149
  async function listedTools(runnerId, deps) {
133
150
  const listed = await awpRequest(runnerId, "tools/list", {}, deps);
134
151
  return listed ? listed.result?.tools ?? [] : null;
@@ -179,6 +196,8 @@ function leafHelp(tool) {
179
196
  "USAGE", ` agentradar ${path}${required.map((name) => ` --${kebab(name)} ${typeOf(properties[name] ?? {})}`).join("")}${optional.length ? " [flags]" : ""}`,
180
197
  ...(required.length ? ["REQUIRED", ...required.filter((name) => name in properties).map(flag)] : []),
181
198
  "FLAGS", ...optional.map(flag), " --dry-run print the exact call and send nothing --json raw JSON --runner <id> the runner (default $AR_CLI_RUNNER_ID)",
199
+ ...Object.entries(properties).flatMap(([name, schema]) => schema.properties ? fieldLines(name, schema, tool.inputSchema) : []),
200
+ ...(tool.name && APPROVAL_EXAMPLES[tool.name] ? [`Example: agentradar ${path} ${APPROVAL_EXAMPLES[tool.name]}`] : []),
182
201
  EXIT_CODES,
183
202
  ].join("\n");
184
203
  }
@@ -194,8 +213,10 @@ async function flagArguments(entry, flags, deps) {
194
213
  for (let [flag, value] of flags) {
195
214
  // --body-file page.md: the argument read from a file, so a multi-line page never sits in a quoted
196
215
  // shell argument (Claude Code prompts on a newline followed by # inside one).
216
+ // --content-base64-file page.html: a *Base64 argument gets the file's bytes as base64, any type (ANY-FILE-SEND).
197
217
  if (flag.endsWith("-file") && !(flag in schema) && !(camel(flag) in schema)) {
198
- const text = await readArgumentFile(value, deps.cwd ?? process.cwd(), deps.home ?? homedir()).catch((error) => error);
218
+ const binary = camel(flag.slice(0, -"-file".length)).endsWith("Base64");
219
+ const text = await readArgumentFile(value, deps.cwd ?? process.cwd(), deps.home ?? homedir(), binary).catch((error) => error);
199
220
  if (text instanceof Error) {
200
221
  logError(`--${flag}: ${text.message}`);
201
222
  return null;
@@ -222,7 +243,9 @@ async function flagArguments(entry, flags, deps) {
222
243
  }
223
244
  const typed = coerce(type, value);
224
245
  if (typed === undefined) {
225
- logError(`--${flag} must be a ${type}`);
246
+ logError(schema[key].properties
247
+ ? `--${flag} must be JSON with the fields ${(schema[key].required ?? Object.keys(schema[key].properties)).join(", ")}; \`agentradar ${pathOf(entry) ?? `call <runner-id> ${entry.name}`} --help\` shows each one.`
248
+ : `--${flag} must be a ${type}`);
226
249
  return null;
227
250
  }
228
251
  out[key] = typed;
@@ -255,21 +278,21 @@ function listFlagError(flag, itemType, schema) {
255
278
  }
256
279
  /**
257
280
  * `agentradar run` is always-allowed, so a file flag must not read and upload any file the agent names:
258
- * only a regular file inside the working folder (after symlinks), and at most 256 KB — and never one in
281
+ * only a regular file inside the working folder (after symlinks), and at most 256 KB (7 MB as base64) — and never one in
259
282
  * ~/.agentradar, which holds the runner's own key even when the working folder is $HOME.
260
283
  * ponytail: a hard link to the key is not caught (same inode, different path); making one is the agent's own act.
261
284
  */
262
- async function readArgumentFile(path, cwd, home) {
285
+ async function readArgumentFile(path, cwd, home, binary = false) {
263
286
  const real = await realpath(resolve(cwd, path)).catch(() => { throw new Error(`cannot read ${path}`); });
264
287
  const within = (dir) => { const inside = relative(dir, real); return inside !== ".." && !inside.startsWith(`..${sep}`) && !isAbsolute(inside); };
265
288
  const own = await realpath(join(home, ".agentradar")).catch(() => null);
266
289
  if (own && within(own))
267
290
  throw new Error(`${path} is refused: a file flag never reads AgentRadar's own folder (~/.agentradar), which holds this agent's key`);
268
291
  const info = await stat(real);
269
- if (!info.isFile() || !within(await realpath(cwd)) || info.size > 256 * 1024) {
270
- throw new Error(`${path} is refused: a file flag reads only a regular file inside the current folder, up to 256 KB`);
292
+ if (!info.isFile() || !within(await realpath(cwd)) || info.size > (binary ? 7 * 1024 * 1024 : 256 * 1024)) {
293
+ throw new Error(`${path} is refused: a file flag reads only a regular file inside the current folder, up to ${binary ? "7 MB" : "256 KB"}`);
271
294
  }
272
- return await readFile(real, "utf8");
295
+ return binary ? (await readFile(real)).toString("base64") : await readFile(real, "utf8");
273
296
  }
274
297
  function coerce(type, value) {
275
298
  if (type === "string")
@@ -339,7 +362,8 @@ export async function callAwpTool(runnerId, name, toolArguments, deps) {
339
362
  // A refusal is the server's sentence and belongs on stderr with a non-zero exit, or a script that
340
363
  // pipes stdout reads "the tool ran" from a body that says it was refused.
341
364
  if (body.result?.isError) {
342
- logError(said);
365
+ // The server's JSON, then its plain "AgentRadar refused <tool>: ... Now: ..." line when it sends one.
366
+ logError([said, ...(body.result.content ?? []).slice(1).map((c) => c.text).filter((t) => typeof t === "string")].join("\n"));
343
367
  return 1;
344
368
  }
345
369
  log(withCliUpdate(said));
@@ -415,15 +439,14 @@ async function postAwp(url, bearer, method, params, deps) {
415
439
  });
416
440
  await noteCliUpdate(response.headers, logError);
417
441
  if (!response.ok) {
418
- // 426: this CLI is below the server's minimum; its sentence says how to upgrade.
419
- const said = response.status === 426 ? await response.json().then((body) => body.error?.message).catch(() => null) : null;
420
- // 401: the server says only "unauthorized" for every cause, so a revoked key and an ended hire read the same here.
442
+ // 426 says how to upgrade; 401 says why the key was refused and how to re-join.
443
+ const said = await response.json().then((body) => body.error?.message).catch(() => null);
421
444
  logError(said ?? (response.status === 401 ? UNAUTHORIZED : `AgentRadar refused the tool call (${response.status}).`));
422
445
  return null;
423
446
  }
424
447
  const body = await response.json();
425
448
  if (body.error) {
426
- logError(typeof body.error.message === "string" ? body.error.message : "AgentRadar returned an error");
449
+ logError(typeof body.error.message === "string" ? body.error.message : `AgentRadar returned an error to ${method === "tools/call" ? String(params.name) : method}. Now: run \`agentradar upgrade\`, then try again.`);
427
450
  return null;
428
451
  }
429
452
  return body;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },