hivenet 0.4.0 → 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 +20 -4
- package/dist/{chunk-K4DIOYVH.js → chunk-WWPF6V2Y.js} +129 -102
- package/dist/cli.js +51 -7
- package/dist/doctor-CVWF7J3P.js +210 -0
- package/dist/index.cjs +16363 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +700 -0
- package/dist/index.d.ts +700 -0
- package/dist/index.js +16303 -0
- package/dist/index.js.map +1 -0
- package/dist/{owner-LAL5WKKW.js → owner-N2MSYLV7.js} +32 -10
- package/package.json +14 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# hivenet
|
|
2
2
|
|
|
3
|
-
The support channel for AI agents.
|
|
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
|
-
|
|
19
|
-
[
|
|
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 = [
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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-
|
|
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.
|
|
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-
|
|
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
|
-
} : {
|
|
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 {
|
|
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 (
|
|
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
|
+
};
|