hivenet 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # hivenet
2
2
 
3
- The support channel for AI agents. When an agent (or human) hits friction in a product's
3
+ The support channel for AI agents. The same package provides both the `npx hivenet`
4
+ CLI and an embeddable library. When an agent (or human) hits friction in a product's
4
5
  CLI, docs, SDK, or MCP server, this submits a structured, context-rich report straight to
5
6
  the maintainers — harness + session auto-detected, threaded, offline-safe.
6
7
 
@@ -9,13 +10,28 @@ npx hivenet --to acme --category cli --subject "acme deploy" \
9
10
  "acme deploy --json prints a spinner to stdout, corrupting the JSON output"
10
11
  ```
11
12
 
13
+ ```sh
14
+ npm install hivenet
15
+ ```
16
+
17
+ ```ts
18
+ import { sendFeedback } from 'hivenet';
19
+
20
+ await sendFeedback({
21
+ feedback: 'acme deploy --json corrupts stdout',
22
+ to: 'acme',
23
+ category: 'cli',
24
+ subject: 'acme deploy --json',
25
+ });
26
+ ```
27
+
12
28
  - Never prompts, never blocks; queues locally when offline (`--flush` retries).
13
29
  - `DO_NOT_TRACK=1` strips all auto-detected context; feedback still delivers anonymously.
14
30
  - `--json` machine output · `--dry-run` shows the exact event before anything is sent.
15
31
  - Destinations: `--to <slug|domain>`, `--dsn`, `HIVENET_DSN`, or feedback.config.json.
16
32
 
17
- Vendors: claim a slug and get paste-ready snippets at
18
- https://hivenet.app — or self-host with
19
- [@hivenet/server](https://www.npmjs.com/package/@hivenet/server).
33
+ Vendors: claim a slug and get paste-ready snippets at https://hivenet.app.
34
+ The reference self-hosted ingest implementation remains available in the
35
+ [Hivenet repository](https://github.com/zvadaadam/hivenet/tree/master/packages/server).
20
36
 
21
37
  Docs: https://github.com/zvadaadam/hivenet
@@ -15161,7 +15161,14 @@ var FEEDBACK_CATEGORIES = [
15161
15161
  "ux",
15162
15162
  "other"
15163
15163
  ];
15164
- var EVENT_STATUSES = ["new", "seen", "in_progress", "resolved", "wont_fix", "spam"];
15164
+ var EVENT_STATUSES = [
15165
+ "new",
15166
+ "seen",
15167
+ "in_progress",
15168
+ "resolved",
15169
+ "wont_fix",
15170
+ "spam"
15171
+ ];
15165
15172
  var ISSUE_STATUSES = ["open", "in_progress", "resolved", "wont_fix", "spam"];
15166
15173
  var VENDOR_CATEGORY_PATTERN = /^x-[a-z0-9][a-z0-9-]{0,38}$/;
15167
15174
  var categorySchema = external_exports.union([
@@ -15212,7 +15219,10 @@ var feedbackContextSchema = external_exports.object({
15212
15219
  /** Only populated via vendor identity paths (signed grants / pass-through). */
15213
15220
  user: userContextSchema.optional()
15214
15221
  });
15215
- var metadataSchema = external_exports.record(external_exports.string().min(1).max(40), external_exports.union([external_exports.string().max(200), external_exports.number().finite(), external_exports.boolean()])).refine((value) => Object.keys(value).length <= 20, { message: "at most 20 metadata keys" });
15222
+ var metadataSchema = external_exports.record(
15223
+ external_exports.string().min(1).max(40),
15224
+ external_exports.union([external_exports.string().max(200), external_exports.number().finite(), external_exports.boolean()])
15225
+ ).refine((value) => Object.keys(value).length <= 20, { message: "at most 20 metadata keys" });
15216
15226
  var feedbackEventSchema = external_exports.object({
15217
15227
  v: external_exports.literal(PROTOCOL_VERSION),
15218
15228
  /** Tenant slug or domain this feedback is addressed to. Optional when the write key implies it. */
@@ -15286,6 +15296,10 @@ function validateIngestResponse(input) {
15286
15296
  return result.success ? { ok: true, data: result.data } : { ok: false, errors: formatIssues(result.error) };
15287
15297
  }
15288
15298
 
15299
+ // ../core/dist/index.js
15300
+ import { readFileSync } from "fs";
15301
+ import path2 from "path";
15302
+
15289
15303
  // ../../node_modules/.pnpm/agent-cli-detector@0.1.5/node_modules/agent-cli-detector/dist/index.js
15290
15304
  import { execFileSync } from "child_process";
15291
15305
  import path from "path";
@@ -15580,8 +15594,6 @@ function sessionIdProperties(agent, env) {
15580
15594
 
15581
15595
  // ../core/dist/index.js
15582
15596
  var import_ci_info = __toESM(require_ci_info(), 1);
15583
- import { readFileSync } from "fs";
15584
- import path2 from "path";
15585
15597
 
15586
15598
  // ../../node_modules/.pnpm/sandbox-cli-detector@0.2.0/node_modules/sandbox-cli-detector/dist/chunk-HZH3SATY.js
15587
15599
  var defaultSandboxes = [
@@ -15656,13 +15668,14 @@ import path32 from "path";
15656
15668
  import { appendFileSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
15657
15669
  import path4 from "path";
15658
15670
  import { randomBytes } from "crypto";
15671
+ import { createRequire } from "module";
15659
15672
  var DSN_ENV_VAR = "HIVENET_DSN";
15660
15673
  var REGISTRY_ENV_VAR = "HIVENET_REGISTRY";
15661
15674
  var HOME_ENV_VAR = "HIVENET_HOME";
15662
15675
  var NO_TELEMETRY_ENV_VAR = "HIVENET_NO_TELEMETRY";
15663
15676
  function feedbackHomeDir(env = process.env) {
15664
15677
  const override = env[HOME_ENV_VAR];
15665
- return override && override.trim() ? override : path3.join(homedir(), ".hivenet");
15678
+ return override?.trim() ? override : path3.join(homedir(), ".hivenet");
15666
15679
  }
15667
15680
  function isTelemetryDisabled(extraOptOutEnvVars = [], env = process.env) {
15668
15681
  const vars = ["DO_NOT_TRACK", NO_TELEMETRY_ENV_VAR, ...extraOptOutEnvVars];
@@ -16044,14 +16057,114 @@ async function flushOutbox(options = {}) {
16044
16057
  }
16045
16058
  return { delivered, remaining: failures.length };
16046
16059
  }
16060
+ async function resolveApiOrigin(options) {
16061
+ if (!options.to && !options.dsn) {
16062
+ return new URL(DEFAULT_REGISTRY_URL).origin;
16063
+ }
16064
+ const destination = await resolveDestination({
16065
+ ...options.to !== void 0 ? { to: options.to } : {},
16066
+ ...options.dsn !== void 0 ? { dsn: options.dsn } : {},
16067
+ ...options.env ? { env: options.env } : {},
16068
+ ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
16069
+ });
16070
+ return new URL(destination.endpoint).origin;
16071
+ }
16072
+ function ownerApi(options) {
16073
+ const origin = (options.apiOrigin ?? new URL(DEFAULT_REGISTRY_URL).origin).replace(/\/$/, "");
16074
+ const fetchImpl = options.fetchImpl ?? fetch;
16075
+ const timeoutMs = options.timeoutMs ?? 15e3;
16076
+ async function request(path5, init) {
16077
+ let response;
16078
+ try {
16079
+ response = await fetchImpl(`${origin}${path5}`, {
16080
+ ...init,
16081
+ signal: AbortSignal.timeout(timeoutMs),
16082
+ headers: {
16083
+ authorization: `Bearer ${options.secretKey}`,
16084
+ ...init?.body ? { "content-type": "application/json" } : {},
16085
+ ...init?.headers
16086
+ }
16087
+ });
16088
+ } catch (error51) {
16089
+ throw new FeedbackError(
16090
+ `Could not reach ${origin} (${error51 instanceof Error ? error51.message : String(error51)}).`
16091
+ );
16092
+ }
16093
+ let body;
16094
+ try {
16095
+ body = await response.json();
16096
+ } catch {
16097
+ body = void 0;
16098
+ }
16099
+ if (!response.ok) {
16100
+ const errors = body?.errors;
16101
+ throw new FeedbackError(
16102
+ Array.isArray(errors) ? errors.map(String).join("; ") : `Request failed (${response.status}).`
16103
+ );
16104
+ }
16105
+ return body;
16106
+ }
16107
+ return {
16108
+ async listFeedback(filters = {}) {
16109
+ const params = new URLSearchParams();
16110
+ if (filters.status) params.set("status", filters.status);
16111
+ if (filters.category) params.set("category", filters.category);
16112
+ if (filters.thread) params.set("thread", filters.thread);
16113
+ if (filters.since) params.set("since", filters.since);
16114
+ if (filters.limit) params.set("limit", String(filters.limit));
16115
+ const query = params.toString();
16116
+ return request(`/v1/events${query ? `?${query}` : ""}`);
16117
+ },
16118
+ async getFeedback(id) {
16119
+ return request(`/v1/events/${encodeURIComponent(id)}`);
16120
+ },
16121
+ async setFeedbackStatus(id, status, note) {
16122
+ return request(`/v1/events/${encodeURIComponent(id)}`, {
16123
+ method: "PATCH",
16124
+ body: JSON.stringify({ status, ...note !== void 0 ? { note } : {} })
16125
+ });
16126
+ },
16127
+ async listIssues(filters = {}) {
16128
+ const params = new URLSearchParams();
16129
+ if (filters.status) params.set("status", filters.status);
16130
+ if (filters.since) params.set("since", filters.since);
16131
+ if (filters.limit) params.set("limit", String(filters.limit));
16132
+ const query = params.toString();
16133
+ return request(`/v1/issues${query ? `?${query}` : ""}`);
16134
+ },
16135
+ async getIssue(id) {
16136
+ return request(`/v1/issues/${encodeURIComponent(id)}`);
16137
+ },
16138
+ async setIssueStatus(id, status, note) {
16139
+ return request(`/v1/issues/${encodeURIComponent(id)}`, {
16140
+ method: "PATCH",
16141
+ body: JSON.stringify({ status, ...note !== void 0 ? { note } : {} })
16142
+ });
16143
+ },
16144
+ /** Attach an external link (Slack message, Linear/GitHub issue, PR) to evt_… or iss_…. */
16145
+ async addLink(id, url2, label) {
16146
+ const path5 = id.startsWith("iss_") ? `/v1/issues/${id}` : `/v1/events/${id}`;
16147
+ return request(path5, {
16148
+ method: "PATCH",
16149
+ body: JSON.stringify({ addLink: { url: url2, ...label !== void 0 ? { label } : {} } })
16150
+ });
16151
+ }
16152
+ };
16153
+ }
16047
16154
  function mintIdempotencyKey() {
16048
16155
  return randomBytes(16).toString("hex");
16049
16156
  }
16050
16157
  var DEFAULT_TIMEOUT_MS = 15e3;
16051
16158
  var DEFAULT_CLIENT_NAME = "hivenet";
16052
- var DEFAULT_CLIENT_VERSION = "0.1.0";
16159
+ var DEFAULT_CLIENT_VERSION = (() => {
16160
+ try {
16161
+ return createRequire(import.meta.url)("../package.json").version ?? "0";
16162
+ } catch {
16163
+ return "0";
16164
+ }
16165
+ })();
16053
16166
  function generateThreadId() {
16054
- return randomBytes(6).toString("hex");
16167
+ return randomBytes(16).toString("hex");
16055
16168
  }
16056
16169
  function resolveThreadId(resume) {
16057
16170
  if (resume !== void 0 && THREAD_ID_PATTERN.test(resume)) {
@@ -16150,113 +16263,27 @@ async function sendFeedback(options) {
16150
16263
  resumed: thread.resumed
16151
16264
  };
16152
16265
  }
16153
- async function resolveApiOrigin(options) {
16154
- if (!options.to && !options.dsn) {
16155
- return new URL(DEFAULT_REGISTRY_URL).origin;
16156
- }
16157
- const destination = await resolveDestination({
16158
- ...options.to !== void 0 ? { to: options.to } : {},
16159
- ...options.dsn !== void 0 ? { dsn: options.dsn } : {},
16160
- ...options.env ? { env: options.env } : {},
16161
- ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
16162
- });
16163
- return new URL(destination.endpoint).origin;
16164
- }
16165
- function ownerApi(options) {
16166
- const origin = (options.apiOrigin ?? new URL(DEFAULT_REGISTRY_URL).origin).replace(/\/$/, "");
16167
- const fetchImpl = options.fetchImpl ?? fetch;
16168
- const timeoutMs = options.timeoutMs ?? 15e3;
16169
- async function request(path5, init) {
16170
- let response;
16171
- try {
16172
- response = await fetchImpl(`${origin}${path5}`, {
16173
- ...init,
16174
- signal: AbortSignal.timeout(timeoutMs),
16175
- headers: {
16176
- authorization: `Bearer ${options.secretKey}`,
16177
- ...init?.body ? { "content-type": "application/json" } : {},
16178
- ...init?.headers
16179
- }
16180
- });
16181
- } catch (error51) {
16182
- throw new FeedbackError(
16183
- `Could not reach ${origin} (${error51 instanceof Error ? error51.message : String(error51)}).`
16184
- );
16185
- }
16186
- let body;
16187
- try {
16188
- body = await response.json();
16189
- } catch {
16190
- body = void 0;
16191
- }
16192
- if (!response.ok) {
16193
- const errors = body?.errors;
16194
- throw new FeedbackError(
16195
- Array.isArray(errors) ? errors.map(String).join("; ") : `Request failed (${response.status}).`
16196
- );
16197
- }
16198
- return body;
16199
- }
16200
- return {
16201
- async listFeedback(filters = {}) {
16202
- const params = new URLSearchParams();
16203
- if (filters.status) params.set("status", filters.status);
16204
- if (filters.category) params.set("category", filters.category);
16205
- if (filters.thread) params.set("thread", filters.thread);
16206
- if (filters.since) params.set("since", filters.since);
16207
- if (filters.limit) params.set("limit", String(filters.limit));
16208
- const query = params.toString();
16209
- return request(`/v1/events${query ? `?${query}` : ""}`);
16210
- },
16211
- async getFeedback(id) {
16212
- return request(`/v1/events/${encodeURIComponent(id)}`);
16213
- },
16214
- async setFeedbackStatus(id, status, note) {
16215
- return request(`/v1/events/${encodeURIComponent(id)}`, {
16216
- method: "PATCH",
16217
- body: JSON.stringify({ status, ...note !== void 0 ? { note } : {} })
16218
- });
16219
- },
16220
- async listIssues(filters = {}) {
16221
- const params = new URLSearchParams();
16222
- if (filters.status) params.set("status", filters.status);
16223
- if (filters.since) params.set("since", filters.since);
16224
- if (filters.limit) params.set("limit", String(filters.limit));
16225
- const query = params.toString();
16226
- return request(`/v1/issues${query ? `?${query}` : ""}`);
16227
- },
16228
- async getIssue(id) {
16229
- return request(`/v1/issues/${encodeURIComponent(id)}`);
16230
- },
16231
- async setIssueStatus(id, status, note) {
16232
- return request(`/v1/issues/${encodeURIComponent(id)}`, {
16233
- method: "PATCH",
16234
- body: JSON.stringify({ status, ...note !== void 0 ? { note } : {} })
16235
- });
16236
- },
16237
- /** Attach an external link (Slack message, Linear/GitHub issue, PR) to evt_… or iss_…. */
16238
- async addLink(id, url2, label) {
16239
- const path5 = id.startsWith("iss_") ? `/v1/issues/${id}` : `/v1/events/${id}`;
16240
- return request(path5, {
16241
- method: "PATCH",
16242
- body: JSON.stringify({ addLink: { url: url2, ...label !== void 0 ? { label } : {} } })
16243
- });
16244
- }
16245
- };
16246
- }
16247
16266
 
16248
16267
  export {
16249
16268
  __toESM,
16269
+ PROTOCOL_VERSION,
16250
16270
  MAX_FEEDBACK_LENGTH,
16271
+ WELL_KNOWN_PATH,
16251
16272
  FEEDBACK_CATEGORIES,
16252
16273
  EVENT_STATUSES,
16253
16274
  ISSUE_STATUSES,
16254
16275
  sentimentSchema,
16255
16276
  authorSchema,
16277
+ validateFeedbackEvent,
16278
+ validateDescriptor,
16256
16279
  FeedbackError,
16280
+ parseDsn,
16281
+ registryUrl,
16282
+ postEvent,
16257
16283
  flushOutbox,
16258
- sendFeedback,
16259
16284
  resolveApiOrigin,
16260
16285
  ownerApi,
16286
+ generateThreadId,
16287
+ sendFeedback,
16261
16288
  require_arg
16262
16289
  };
package/dist/cli.js CHANGED
@@ -9,16 +9,21 @@ import {
9
9
  require_arg,
10
10
  sendFeedback,
11
11
  sentimentSchema
12
- } from "./chunk-K4DIOYVH.js";
12
+ } from "./chunk-WWPF6V2Y.js";
13
13
 
14
14
  // src/cli.ts
15
15
  var import_arg = __toESM(require_arg(), 1);
16
16
  var CLI_NAME = "hivenet";
17
- var CLI_VERSION = "0.4.0";
17
+ var CLI_VERSION = "0.4.2";
18
18
  async function main() {
19
19
  const subcommand = process.argv[2];
20
+ if (subcommand === "doctor") {
21
+ const { runDoctor } = await import("./doctor-CVWF7J3P.js");
22
+ await runDoctor(process.argv.slice(3), CLI_VERSION);
23
+ return;
24
+ }
20
25
  if (subcommand === "inbox" || subcommand === "triage" || subcommand === "issues" || subcommand === "issue") {
21
- const owner = await import("./owner-LAL5WKKW.js");
26
+ const owner = await import("./owner-N2MSYLV7.js");
22
27
  const run = {
23
28
  inbox: owner.runInbox,
24
29
  triage: owner.runTriage,
@@ -39,6 +44,11 @@ async function main() {
39
44
  "--sentiment": String,
40
45
  "--author": String,
41
46
  "--meta": [String],
47
+ "--task": String,
48
+ "--expected": String,
49
+ "--actual": String,
50
+ "--mistake": String,
51
+ "--attempts": Number,
42
52
  "--resume": String,
43
53
  "--json": Boolean,
44
54
  "--dry-run": Boolean,
@@ -74,12 +84,14 @@ Usage: npx ${CLI_NAME} --to <slug|domain> "<specific, actionable feedback>"`
74
84
  const sentiment = parseEnumFlag("--sentiment", args["--sentiment"], sentimentSchema);
75
85
  const author = parseEnumFlag("--author", args["--author"], authorSchema);
76
86
  const metadata = parseMetaFlags(args["--meta"]);
87
+ const evalReport = buildEvalReport(args);
77
88
  const options = {
78
89
  feedback,
79
90
  ...args["--to"] !== void 0 ? { to: args["--to"] } : {},
80
91
  ...args["--dsn"] !== void 0 ? { dsn: args["--dsn"] } : {},
81
- ...args["--category"] !== void 0 ? { category: args["--category"].toLowerCase() } : {},
92
+ ...args["--category"] !== void 0 ? { category: args["--category"].toLowerCase() } : evalReport ? { category: "eval" } : {},
82
93
  ...args["--subject"] !== void 0 ? { subject: args["--subject"] } : {},
94
+ ...evalReport ? { eval: evalReport } : {},
83
95
  ...sentiment !== void 0 ? { sentiment } : {},
84
96
  ...author !== void 0 ? { author } : {},
85
97
  ...metadata ? { metadata } : {},
@@ -102,7 +114,12 @@ Usage: npx ${CLI_NAME} --to <slug|domain> "<specific, actionable feedback>"`
102
114
  delivered: true,
103
115
  ...result.destination.to ? { to: result.destination.to } : {},
104
116
  ...result.response
105
- } : { delivered: false, queued: result.queued, error: result.error, thread: result.event.thread.id },
117
+ } : {
118
+ delivered: false,
119
+ queued: result.queued,
120
+ error: result.error,
121
+ thread: result.event.thread.id
122
+ },
106
123
  null,
107
124
  2
108
125
  )
@@ -148,7 +165,28 @@ async function buildDryRun(options) {
148
165
  });
149
166
  });
150
167
  const result = await sendFeedback({ ...options, fetchImpl: capture, outbox: false });
151
- return { destination: { endpoint: result.destination.endpoint, to: result.destination.to }, event: captured };
168
+ return {
169
+ destination: { endpoint: result.destination.endpoint, to: result.destination.to },
170
+ event: captured
171
+ };
172
+ }
173
+ function buildEvalReport(args) {
174
+ const task = args["--task"];
175
+ const details = {
176
+ ...args["--expected"] !== void 0 ? { expected: args["--expected"] } : {},
177
+ ...args["--actual"] !== void 0 ? { actual: args["--actual"] } : {},
178
+ ...args["--mistake"] !== void 0 ? { mistake: args["--mistake"] } : {},
179
+ ...args["--attempts"] !== void 0 ? { attempts: args["--attempts"] } : {}
180
+ };
181
+ if (task === void 0) {
182
+ if (Object.keys(details).length > 0) {
183
+ throw new FeedbackError(
184
+ '--expected/--actual/--mistake/--attempts describe a failed task; add --task "<what you were trying to do>".'
185
+ );
186
+ }
187
+ return void 0;
188
+ }
189
+ return { task, ...details };
152
190
  }
153
191
  function parseMetaFlags(pairs) {
154
192
  if (!pairs || pairs.length === 0) {
@@ -171,6 +209,7 @@ function printHelp() {
171
209
  console.log(`
172
210
  Usage
173
211
  $ npx ${CLI_NAME} --to <slug|domain> [options] "<feedback>"
212
+ $ npx ${CLI_NAME} doctor --to <slug|domain> (vendors: verify your integration end-to-end)
174
213
  $ npx ${CLI_NAME} inbox --help (owners: list + filter your project's feedback)
175
214
  $ npx ${CLI_NAME} triage --help (owners: set status/notes/links on an event)
176
215
  $ npx ${CLI_NAME} issues --help (owners: clustered issues, regression counts)
@@ -186,6 +225,11 @@ function printHelp() {
186
225
  --category, -c <cat> ${FEEDBACK_CATEGORIES.join(" | ")} | x-<vendor>
187
226
  --subject, -s <subject> Exact item the feedback is about (see table below)
188
227
  --meta <key=value> Vendor-defined enrichment, repeatable (e.g. --meta sdk_version=1.2.3)
228
+ --task <text> Failed task you are reporting (sets --category eval when omitted)
229
+ --expected <text> What should have happened (with --task)
230
+ --actual <text> What actually happened (with --task)
231
+ --mistake <text> The specific mistake made (with --task)
232
+ --attempts <n> How many attempts before giving up (with --task)
189
233
  --sentiment <s> positive | negative | neutral
190
234
  --author <a> agent | human | hybrid (auto-detected when omitted)
191
235
  --resume <threadId> Continue an earlier feedback thread
@@ -204,7 +248,7 @@ function printHelp() {
204
248
  cli Full command, e.g. acme deploy --json
205
249
  api Endpoint or SDK method
206
250
  model Model behavior involved
207
- eval Capability the failed task involves (include --category eval details)
251
+ eval Capability the failed task involves (add --task, --expected, --actual, \u2026)
208
252
  ux Flow or surface name
209
253
  other Concise topic, or leave empty
210
254
 
@@ -0,0 +1,210 @@
1
+ import {
2
+ FeedbackError,
3
+ PROTOCOL_VERSION,
4
+ WELL_KNOWN_PATH,
5
+ __toESM,
6
+ generateThreadId,
7
+ parseDsn,
8
+ postEvent,
9
+ registryUrl,
10
+ require_arg,
11
+ validateDescriptor,
12
+ validateFeedbackEvent
13
+ } from "./chunk-WWPF6V2Y.js";
14
+
15
+ // src/doctor.ts
16
+ var import_arg = __toESM(require_arg(), 1);
17
+ var FETCH_TIMEOUT_MS = 1e4;
18
+ async function runDoctor(argv, cliVersion) {
19
+ const args = (0, import_arg.default)(
20
+ {
21
+ "--help": Boolean,
22
+ "--to": String,
23
+ "--dsn": String,
24
+ "--smoke": Boolean,
25
+ "--json": Boolean,
26
+ "-h": "--help",
27
+ "-t": "--to"
28
+ },
29
+ { argv, permissive: false }
30
+ );
31
+ if (args["--help"] || !args["--to"] && !args["--dsn"]) {
32
+ printDoctorHelp();
33
+ return;
34
+ }
35
+ const checks = [];
36
+ let endpoint;
37
+ let writeKey;
38
+ let slug;
39
+ let descriptor;
40
+ if (args["--dsn"]) {
41
+ try {
42
+ const destination = parseDsn(args["--dsn"]);
43
+ endpoint = destination.endpoint;
44
+ writeKey = destination.writeKey;
45
+ slug = destination.to;
46
+ checks.push({ name: "dsn", ok: true, detail: `endpoint ${endpoint}` });
47
+ } catch (error) {
48
+ checks.push({ name: "dsn", ok: false, detail: messageOf(error) });
49
+ }
50
+ } else {
51
+ const to = args["--to"].trim();
52
+ const descriptorUrl = to.includes(".") ? `https://${to}${WELL_KNOWN_PATH}` : `${registryUrl()}/v1/registry/${to}`;
53
+ const result = await checkDescriptor(descriptorUrl);
54
+ checks.push(result.check);
55
+ descriptor = result.descriptor;
56
+ endpoint = descriptor?.endpoint;
57
+ writeKey = descriptor?.writeKey;
58
+ slug = descriptor?.slug ?? (to.includes(".") ? void 0 : to);
59
+ }
60
+ if (endpoint) {
61
+ checks.push(checkEndpointUrl(endpoint));
62
+ checks.push(
63
+ writeKey ? {
64
+ name: "write key",
65
+ ok: true,
66
+ detail: `${writeKey.slice(0, 12)}\u2026 (write-only, safe to publish)`
67
+ } : {
68
+ name: "write key",
69
+ ok: true,
70
+ detail: "none published \u2014 the ingest must accept keyless submissions for this project"
71
+ }
72
+ );
73
+ const categories = descriptor?.categories ? Object.keys(descriptor.categories) : [];
74
+ checks.push({
75
+ name: "categories",
76
+ ok: true,
77
+ detail: categories.length > 0 ? categories.join(", ") : "none published \u2014 agents will invent free-text subjects"
78
+ });
79
+ if (args["--smoke"]) {
80
+ checks.push(await sendSmokeEvent(endpoint, writeKey, slug, cliVersion));
81
+ }
82
+ }
83
+ const healthy = checks.every((check) => check.ok);
84
+ if (args["--json"]) {
85
+ console.log(JSON.stringify({ ok: healthy, checks }, null, 2));
86
+ } else {
87
+ for (const check of checks) {
88
+ console.log(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}`);
89
+ }
90
+ if (healthy && !args["--smoke"]) {
91
+ console.log(" Add --smoke to send one disposable event through the real pipe.");
92
+ }
93
+ }
94
+ if (!healthy) {
95
+ process.exitCode = 1;
96
+ }
97
+ }
98
+ async function checkDescriptor(url) {
99
+ const name = url.includes(WELL_KNOWN_PATH) ? "well-known descriptor" : "registry entry";
100
+ let response;
101
+ try {
102
+ response = await fetch(url, {
103
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
104
+ headers: { accept: "application/json" }
105
+ });
106
+ } catch (error) {
107
+ return { check: { name, ok: false, detail: `could not reach ${url} (${messageOf(error)})` } };
108
+ }
109
+ if (!response.ok) {
110
+ return { check: { name, ok: false, detail: `${url} responded ${response.status}` } };
111
+ }
112
+ const contentType = response.headers.get("content-type") ?? "unknown";
113
+ const body = await response.text();
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(body);
117
+ } catch {
118
+ const spaHint = /<[a-z!]/i.test(body.trimStart()) ? " \u2014 looks like an HTML page (an SPA catch-all route?); serve the JSON file at exactly this path" : "";
119
+ return {
120
+ check: { name, ok: false, detail: `${url} returned ${contentType}, not JSON${spaHint}` }
121
+ };
122
+ }
123
+ const validated = validateDescriptor(parsed);
124
+ if (!validated.ok) {
125
+ return {
126
+ check: {
127
+ name,
128
+ ok: false,
129
+ detail: `${url} is not a valid descriptor: ${validated.errors.join("; ")}`
130
+ }
131
+ };
132
+ }
133
+ return {
134
+ check: { name, ok: true, detail: `${url} (v${validated.data.v}, ${validated.data.name})` },
135
+ descriptor: validated.data
136
+ };
137
+ }
138
+ function checkEndpointUrl(endpoint) {
139
+ try {
140
+ const url = new URL(endpoint);
141
+ if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
142
+ return { name: "endpoint", ok: false, detail: `${endpoint} is not https` };
143
+ }
144
+ return { name: "endpoint", ok: true, detail: endpoint };
145
+ } catch {
146
+ return { name: "endpoint", ok: false, detail: `"${endpoint}" is not a URL` };
147
+ }
148
+ }
149
+ async function sendSmokeEvent(endpoint, writeKey, slug, cliVersion) {
150
+ const candidate = {
151
+ v: PROTOCOL_VERSION,
152
+ ...slug ? { to: slug } : {},
153
+ feedback: "hivenet doctor smoke event \u2014 verifying this integration; safe to mark as spam.",
154
+ category: "other",
155
+ subject: "hivenet doctor",
156
+ thread: { id: generateThreadId() },
157
+ client: { name: "hivenet-doctor", version: cliVersion },
158
+ consent: { telemetry: false }
159
+ };
160
+ const validated = validateFeedbackEvent(candidate);
161
+ if (!validated.ok) {
162
+ return { name: "smoke event", ok: false, detail: validated.errors.join("; ") };
163
+ }
164
+ const result = await postEvent({
165
+ endpoint,
166
+ ...writeKey ? { writeKey } : {},
167
+ event: validated.data,
168
+ timeoutMs: FETCH_TIMEOUT_MS
169
+ });
170
+ switch (result.kind) {
171
+ case "accepted":
172
+ return {
173
+ name: "smoke event",
174
+ ok: true,
175
+ detail: `delivered (${result.response.id})${result.response.guidance ? " \u2014 guidance configured \u2713" : ""}`
176
+ };
177
+ case "rejected":
178
+ return { name: "smoke event", ok: false, detail: `rejected: ${result.errors.join("; ")}` };
179
+ case "unreachable":
180
+ return { name: "smoke event", ok: false, detail: `endpoint unreachable: ${result.error}` };
181
+ }
182
+ }
183
+ function messageOf(error) {
184
+ if (error instanceof FeedbackError || error instanceof Error) {
185
+ return error.message;
186
+ }
187
+ return String(error);
188
+ }
189
+ function printDoctorHelp() {
190
+ console.log(`
191
+ Usage
192
+ $ npx hivenet doctor --to <slug|domain> [--smoke] [--json]
193
+ $ npx hivenet doctor --dsn <url> [--smoke] [--json]
194
+
195
+ Verify a hivenet integration end-to-end, the way an agent would resolve it:
196
+ registry entry or well-known descriptor (content type, schema), endpoint,
197
+ write key, and published categories. Ignores HIVENET_DSN and
198
+ feedback.config.json so a healthy local setup can't mask a broken public one.
199
+
200
+ Options
201
+ --to, -t <slug|domain> Registered slug, or a domain serving ${WELL_KNOWN_PATH}
202
+ --dsn <url> Explicit DSN: https://hv_pub_KEY@host/slug
203
+ --smoke Also send one disposable, clearly-labeled event
204
+ --json Machine-readable result
205
+ --help, -h This help
206
+ `);
207
+ }
208
+ export {
209
+ runDoctor
210
+ };