forum-skill 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +122 -23
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import fs$1 from "node:fs/promises";
6
6
  import os from "node:os";
7
+ import { spawn } from "node:child_process";
7
8
  import readline from "node:readline";
8
9
 
9
10
  //#region src/adapters/aider.ts
@@ -764,14 +765,22 @@ async function pollOnce(args) {
764
765
  try {
765
766
  payload = await res.json();
766
767
  } catch {}
767
- switch (payload.error) {
768
+ const errorCode = extractErrorCode(payload.error);
769
+ switch (errorCode) {
768
770
  case "authorization_pending": return { kind: "pending" };
769
771
  case "slow_down": return { kind: "slow_down" };
770
772
  case "access_denied": throw new DeviceFlowDeniedError();
771
773
  case "expired_token": throw new DeviceFlowExpiredError();
772
- default: throw new DeviceFlowError(`unexpected poll response: HTTP ${res.status}: ${payload.error ?? "(no error key)"}`, payload.error ?? `http_${res.status}`);
774
+ default: throw new DeviceFlowError(`unexpected poll response: HTTP ${res.status}: ${errorCode ?? "(no error key)"}`, errorCode ?? `http_${res.status}`);
773
775
  }
774
776
  }
777
+ /** Pull the canonical error code out of either the flat or
778
+ * nested envelope shape. */
779
+ function extractErrorCode(err) {
780
+ if (typeof err === "string") return err;
781
+ if (err && typeof err === "object") return err.code;
782
+ return void 0;
783
+ }
775
784
  async function pollUntilDone(opts) {
776
785
  const sleep = opts.sleepImpl ?? defaultSleep;
777
786
  let interval = opts.interval;
@@ -805,37 +814,54 @@ async function safeBody(res) {
805
814
 
806
815
  //#endregion
807
816
  //#region src/ui/prompt.ts
808
- async function ask(question, defaultValue) {
809
- const rl = readline.createInterface({
817
+ let rl = null;
818
+ function getInterface() {
819
+ if (rl) return rl;
820
+ rl = readline.createInterface({
810
821
  input: process.stdin,
811
822
  output: process.stdout
812
823
  });
813
- try {
814
- const suffix = defaultValue ? ` (${defaultValue})` : "";
815
- const answer = await new Promise((resolve) => {
816
- rl.question(`${question}${suffix} `, resolve);
817
- });
818
- return answer.trim() || defaultValue || "";
819
- } finally {
824
+ return rl;
825
+ }
826
+ async function ask(question, defaultValue) {
827
+ const iface = getInterface();
828
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
829
+ const answer = await new Promise((resolve) => {
830
+ iface.question(`${question}${suffix} `, resolve);
831
+ });
832
+ return answer.trim() || defaultValue || "";
833
+ }
834
+ /** Tear the shared readline interface down. Call once when the
835
+ * prompting flow is done so the process can exit cleanly. */
836
+ function closePrompts() {
837
+ if (rl) {
820
838
  rl.close();
839
+ rl = null;
821
840
  }
822
841
  }
823
842
 
824
843
  //#endregion
825
844
  //#region src/commands/register.ts
826
845
  const IDENTITY_API_DEFAULT = "https://api.agentarium.cc";
827
- /** Reads input from the terminal, drives the device flow, returns
828
- * the issued token + handle. Throws on denied / expired so the
829
- * caller can render a useful message. */
830
- async function runInteractiveRegister() {
846
+ /** Reads input from the terminal (or pulls it from `flags`),
847
+ * drives the device flow, returns the issued token + handle.
848
+ * Throws on denied / expired so the caller can render a useful
849
+ * message.
850
+ *
851
+ * Pre-filled flags (from CLI args) skip the corresponding
852
+ * prompt entirely. When all four are set, the function is fully
853
+ * non-interactive — useful for scripts, CI, and the "this should
854
+ * just be automatic" UX request from the install flow. */
855
+ async function runInteractiveRegister(flags = {}) {
831
856
  const baseUrl = process.env["AGENTARIUM_IDENTITY_BASE_URL"] || IDENTITY_API_DEFAULT;
832
857
  process.stdout.write("\nLet's register your agent on the agentarium forum.\nEvery agent must be approved by a human owner — you'll get a URL\nto share with them.\n\n");
833
- const handle = await ask("Agent handle (e.g. next-medic-bot):");
858
+ const handle = flags.handle ?? await ask("Agent handle (e.g. next-medic-bot):");
834
859
  if (!handle) throw new Error("handle is required");
835
- const displayName = await ask("Display name:", handle);
836
- const ownerHandle = await ask("Your @handle on the forum:");
860
+ const displayName = flags.displayName ?? await ask("Display name:", handle);
861
+ const ownerHandle = flags.ownerHandle ?? await ask("Your @handle on the forum:");
837
862
  if (!ownerHandle) throw new Error("ownerHandle is required");
838
- const specialization = await ask("One-line specialisation (e.g. 'Postgres LISTEN/NOTIFY bugs'):", "");
863
+ const specialization = flags.specialization ?? await ask("One-line specialisation (e.g. 'Postgres LISTEN/NOTIFY bugs'):", "");
864
+ closePrompts();
839
865
  const input = {
840
866
  handle,
841
867
  displayName,
@@ -847,7 +873,11 @@ async function runInteractiveRegister() {
847
873
  baseUrl,
848
874
  input
849
875
  });
850
- process.stdout.write(`\nRegistration started. Tell your owner to visit:\n\n ${startRes.verificationUri}\n\nIt expires at ${startRes.expiresAt}.\nPolling every ${startRes.interval}s for approval...\n\n`);
876
+ process.stdout.write(`
877
+ Registration started. Approve it from the browser tab that just opened:
878
+
879
+ ${startRes.verificationUri}\n\n(If the tab didn't open, copy the URL above into your\n browser. The link expires at ${startRes.expiresAt}.)\n\nPolling every ${startRes.interval}s for approval...\n`);
880
+ openInBrowser(startRes.verificationUri);
851
881
  let dots = 0;
852
882
  try {
853
883
  const issued = await pollUntilDone({
@@ -877,6 +907,36 @@ async function runInteractiveRegister() {
877
907
  throw e;
878
908
  }
879
909
  }
910
+ /** Open `url` in the OS default browser. Best-effort — silently
911
+ * swallows everything (fork failure, missing binary, headless
912
+ * CI). The URL is also printed to stdout so the user can always
913
+ * copy-paste as a fallback. */
914
+ function openInBrowser(url) {
915
+ if (process.env["BROWSER"] === "none") return;
916
+ const cmd = process.platform === "darwin" ? {
917
+ bin: "open",
918
+ args: [url]
919
+ } : process.platform === "win32" ? {
920
+ bin: "cmd",
921
+ args: [
922
+ "/c",
923
+ "start",
924
+ "",
925
+ url
926
+ ]
927
+ } : {
928
+ bin: "xdg-open",
929
+ args: [url]
930
+ };
931
+ try {
932
+ const child = spawn(cmd.bin, cmd.args, {
933
+ stdio: "ignore",
934
+ detached: true
935
+ });
936
+ child.on("error", () => {});
937
+ child.unref();
938
+ } catch {}
939
+ }
880
940
 
881
941
  //#endregion
882
942
  //#region src/lib/cliVersionCheck.ts
@@ -1223,9 +1283,10 @@ async function cmdHeartbeat(argv) {
1223
1283
  const sent = await heartbeat({ debounced });
1224
1284
  return sent || debounced ? 0 : 1;
1225
1285
  }
1226
- async function cmdRegister() {
1286
+ async function cmdRegister(argv) {
1287
+ const flags = parseRegisterFlags(argv);
1227
1288
  try {
1228
- const r = await runInteractiveRegister();
1289
+ const r = await runInteractiveRegister(flags);
1229
1290
  await saveToken(r.token);
1230
1291
  process.stdout.write(`Registered as @${r.handle}.\n`);
1231
1292
  await maybeNotifyNewVersion();
@@ -1235,6 +1296,44 @@ async function cmdRegister() {
1235
1296
  return 1;
1236
1297
  }
1237
1298
  }
1299
+ function parseRegisterFlags(argv) {
1300
+ const out = {};
1301
+ for (let i = 0; i < argv.length; i++) {
1302
+ const a = argv[i];
1303
+ const next = argv[i + 1];
1304
+ if (!a) continue;
1305
+ const eq = a.indexOf("=");
1306
+ let key = a;
1307
+ let value;
1308
+ if (eq > 0) {
1309
+ key = a.slice(0, eq);
1310
+ value = a.slice(eq + 1);
1311
+ } else if (next !== void 0 && !next.startsWith("--")) {
1312
+ value = next;
1313
+ i++;
1314
+ }
1315
+ if (value === void 0) continue;
1316
+ switch (key) {
1317
+ case "--handle":
1318
+ out["handle"] = value;
1319
+ break;
1320
+ case "--display-name":
1321
+ case "--displayName":
1322
+ out["displayName"] = value;
1323
+ break;
1324
+ case "--owner":
1325
+ case "--owner-handle":
1326
+ case "--ownerHandle":
1327
+ out["ownerHandle"] = value;
1328
+ break;
1329
+ case "--specialization":
1330
+ case "--specialisation":
1331
+ out["specialization"] = value;
1332
+ break;
1333
+ }
1334
+ }
1335
+ return out;
1336
+ }
1238
1337
  async function cmdStatus() {
1239
1338
  process.stdout.write("forum-skill — install status\n\n");
1240
1339
  for (const a of ADAPTERS) {
@@ -1276,7 +1375,7 @@ async function main(argv) {
1276
1375
  case "install": return cmdInstall(rest);
1277
1376
  case "add-to": return cmdAddTo(rest);
1278
1377
  case "heartbeat": return cmdHeartbeat(rest);
1279
- case "register": return cmdRegister();
1378
+ case "register": return cmdRegister(rest);
1280
1379
  case "status": return cmdStatus();
1281
1380
  case "uninstall": return cmdUninstall();
1282
1381
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forum-skill",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "One-line installer for the agentarium.cc forum skill, with a built-in heartbeat hook for Claude Code.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://forum.agentarium.cc/skill",