@rayul/kordis 0.6.6 → 0.7.0

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
@@ -56,6 +56,25 @@ Pass `--json` on any command to get machine-readable output.
56
56
 
57
57
  Refresh and access tokens are stored in the OS keychain via `keytar` (with a file-based fallback at `~/.kordis/.tokens.json`, mode `0600`, when keytar is unavailable).
58
58
 
59
+ ## Privacy & environment variables
60
+
61
+ Kordis collects two opt-in signals, both off by default. Both can be set non-interactively for agents (`kordis setup --personalization on|off` and `kordis setup --error-telemetry on|off`) and both have an env-var escape hatch that overrides config.
62
+
63
+ | env var | purpose | default |
64
+ |---|---|---|
65
+ | `KORDIS_NO_CONTEXT=1` | suppress personalization header even if opted in | off |
66
+ | `KORDIS_NO_TELEMETRY=1` | suppress error telemetry POST even if opted in | off |
67
+ | `KORDIS_NO_KEYCHAIN=1` | force plaintext token storage even when keytar loads (escape hatch for macOS keychain-prompt fatigue) | off |
68
+ | `KORDIS_DEBUG=1` | surface auto-finalize stage outcomes to stderr (diagnostic) | off |
69
+
70
+ **What error telemetry carries** (0.7.0+, when `error_telemetry: 'on'`):
71
+ - error `kind` (4 enum values — auto-finalize Stage 1/2 failure, network unreachable, unhandled error)
72
+ - `cli_version`, `node_version`, `platform`
73
+ - `message_excerpt` — first 200 chars of `err.message` AFTER client-side sanitization (homedir → `~`; unix paths ≥2 segments → `<path>`; Windows paths → `<path>`; query strings → `?<query>`; JWTs → `<jwt>`; UUIDs → `<uuid>`; hex runs ≥32 → `<hex>`)
74
+ - optional `error_code` (when present on the Error) + `elapsed_ms` + ISO timestamp
75
+
76
+ **What it NEVER carries**: `argv`, env-var values, file paths (after sanitization), identity (`user_id`, `username`, `session_id`, refresh tokens), `X-Kordis-Context` content, full stack traces. The server route additionally ignores any `Authorization` header so identity can't leak in even by accident.
77
+
59
78
  ## Docs
60
79
 
61
80
  The agent guide that `setup` injects is also shipped as `assets/KORDIS.md` in this package.
@@ -155,3 +155,43 @@ User: "add a pre-commit hook that blocks committing API keys"
155
155
  - confirm with the user before pulling
156
156
  - kordis pull <full_slug>
157
157
  </example>
158
+
159
+ ## Telemetry & opt-in (0.7.0+)
160
+
161
+ Kordis can send anonymized error reports to help maintainers fix wild failures that the user might otherwise just give up on. Off by default.
162
+
163
+ When asked at `kordis setup`, the consent prompt explicitly states the payload contents. Agents that already received user consent in chat can record it non-interactively:
164
+
165
+ ```
166
+ kordis setup --error-telemetry on
167
+ kordis setup --error-telemetry off
168
+ ```
169
+
170
+ The same applies to the older personalization opt-in:
171
+
172
+ ```
173
+ kordis setup --personalization on
174
+ kordis setup --personalization off
175
+ ```
176
+
177
+ **Never assume consent on the user's behalf.** Ask in plain words, then record their answer with the flag.
178
+
179
+ **What error telemetry carries** (sent to `POST /api/telemetry/error`):
180
+
181
+ | field | value |
182
+ |---|---|
183
+ | `kind` | one of `auto_finalize_stage1_fail`, `auto_finalize_stage2_fail`, `network_unreachable`, `unhandled_error` |
184
+ | `cli_version`, `node_version`, `platform` | version + OS strings |
185
+ | `message_excerpt` | first 200 chars of `err.message` after sanitization (homedir → `~`; paths / JWTs / UUIDs / long hex runs stripped) |
186
+ | `error_code`, `elapsed_ms` | optional |
187
+ | `timestamp` | ISO 8601 |
188
+
189
+ **Never carries**: `argv`, env-var values, file paths (after sanitization), identity (`user_id` / `username` / `session_id` / refresh tokens), `X-Kordis-Context` content, full stack traces. The server's POST route ignores `Authorization` headers entirely.
190
+
191
+ **Escape hatches** (highest precedence wins):
192
+
193
+ 1. `KORDIS_NO_TELEMETRY=1` env var — disables for that invocation
194
+ 2. `kordis setup --error-telemetry off` — persists the off decision
195
+ 3. `kordis setup --error-telemetry on` — persists the on decision
196
+
197
+ If a user asks "what does Kordis collect about me" — quote the table above; don't paraphrase.
package/dist/index.js CHANGED
@@ -829,6 +829,69 @@ var init_recommend = __esm({
829
829
  }
830
830
  });
831
831
 
832
+ // ../../packages/contracts/src/telemetry.ts
833
+ import { z as z12 } from "zod";
834
+ var TelemetryErrorKindSchema, TelemetryPlatformSchema, TelemetryErrorPayloadSchema, AdminTelemetrySummaryQuerySchema, TelemetryKindCountSchema, TelemetryDailyBucketSchema, TelemetryTopExcerptSchema, AdminTelemetrySummaryResponseSchema;
835
+ var init_telemetry = __esm({
836
+ "../../packages/contracts/src/telemetry.ts"() {
837
+ "use strict";
838
+ TelemetryErrorKindSchema = z12.enum([
839
+ /** Stage 1 of CLI auto-finalize (pollDeviceFlow) failed with a non-pending error. */
840
+ "auto_finalize_stage1_fail",
841
+ /** Stage 2 of CLI auto-finalize (me/keychain/saveConfig) failed — the 0.6.6 class. */
842
+ "auto_finalize_stage2_fail",
843
+ /** Top-level CLI handler caught KordisNetworkError. */
844
+ "network_unreachable",
845
+ /** Top-level CLI handler caught an unrecognised error class. */
846
+ "unhandled_error"
847
+ ]);
848
+ TelemetryPlatformSchema = z12.enum(["darwin", "linux", "win32", "other"]);
849
+ TelemetryErrorPayloadSchema = z12.object({
850
+ kind: TelemetryErrorKindSchema,
851
+ cli_version: z12.string().min(1).max(32),
852
+ node_version: z12.string().min(1).max(32),
853
+ platform: TelemetryPlatformSchema,
854
+ /** First 200 chars of err.message AFTER client-side sanitization. */
855
+ message_excerpt: z12.string().min(1).max(200),
856
+ /** err.code when present on the original error object. */
857
+ error_code: z12.string().min(1).max(64).optional(),
858
+ /** Optional timing — duration of the failing operation if the catch site knows it. */
859
+ elapsed_ms: z12.number().int().nonnegative().optional(),
860
+ /** ISO 8601 UTC. */
861
+ timestamp: z12.string().min(1).max(40)
862
+ });
863
+ AdminTelemetrySummaryQuerySchema = z12.object({
864
+ /** ISO 8601 cutoff; defaults to 7 days back. */
865
+ since: z12.string().datetime().optional()
866
+ });
867
+ TelemetryKindCountSchema = z12.object({
868
+ kind: TelemetryErrorKindSchema,
869
+ cli_version: z12.string(),
870
+ count: z12.number().int().nonnegative()
871
+ });
872
+ TelemetryDailyBucketSchema = z12.object({
873
+ /** YYYY-MM-DD UTC. */
874
+ date: z12.string().regex(/^\d{4}-\d{2}-\d{2}$/),
875
+ count: z12.number().int().nonnegative()
876
+ });
877
+ TelemetryTopExcerptSchema = z12.object({
878
+ kind: TelemetryErrorKindSchema,
879
+ message_excerpt: z12.string(),
880
+ count: z12.number().int().nonnegative()
881
+ });
882
+ AdminTelemetrySummaryResponseSchema = z12.object({
883
+ /** Cutoff applied to all aggregates. */
884
+ since: z12.string().datetime(),
885
+ /** Count grouped by (kind, cli_version), descending. */
886
+ counts: z12.array(TelemetryKindCountSchema),
887
+ /** Top 10 distinct message_excerpts per kind, for quick triage. */
888
+ top_excerpts: z12.array(TelemetryTopExcerptSchema),
889
+ /** Daily volume sparkline, oldest → newest. */
890
+ daily: z12.array(TelemetryDailyBucketSchema)
891
+ });
892
+ }
893
+ });
894
+
832
895
  // ../../packages/contracts/src/index.ts
833
896
  var init_src = __esm({
834
897
  "../../packages/contracts/src/index.ts"() {
@@ -844,6 +907,7 @@ var init_src = __esm({
844
907
  init_api();
845
908
  init_context();
846
909
  init_recommend();
910
+ init_telemetry();
847
911
  }
848
912
  });
849
913
 
@@ -1136,6 +1200,24 @@ var init_client = __esm({
1136
1200
  `/api/comments/${id}/replies`
1137
1201
  );
1138
1202
  }
1203
+ // ─── telemetry ─────────────────────────────────────────────────────────
1204
+ /**
1205
+ * Fire-and-forget anonymized error report (0.7.0). The CLI's
1206
+ * `apps/cli/src/lib/error-reporter.ts` wraps this with sanitization +
1207
+ * gating + timeout. The server route is anonymous-OK (no Authorization
1208
+ * header read) and rate-limited to 30/min per IP via its own bucket so
1209
+ * a CI runner burst can't lock the IP out of the actual API.
1210
+ *
1211
+ * Server returns `204 No Content` on success; the reporter ignores
1212
+ * any non-2xx (and even network errors — telemetry MUST NOT itself
1213
+ * become a source of user-visible failure).
1214
+ */
1215
+ recordTelemetryError(payload) {
1216
+ return this.request("/api/telemetry/error", {
1217
+ method: "POST",
1218
+ body: payload
1219
+ });
1220
+ }
1139
1221
  };
1140
1222
  }
1141
1223
  });
@@ -1186,7 +1268,8 @@ async function loadCliConfig() {
1186
1268
  apiUrl: apiUrlFromEnv ?? parsed.apiUrl ?? "https://api.kordis.store",
1187
1269
  ...parsed.username ? { username: parsed.username } : {},
1188
1270
  ...parsed.sessionId ? { sessionId: parsed.sessionId } : {},
1189
- personalization: parsed.personalization ?? "undecided"
1271
+ personalization: parsed.personalization ?? "undecided",
1272
+ error_telemetry: parsed.error_telemetry ?? "undecided"
1190
1273
  };
1191
1274
  } catch {
1192
1275
  return { apiUrl: apiUrlFromEnv ?? "https://api.kordis.store" };
@@ -1518,15 +1601,93 @@ async function getCachedContextHeader(cwd = process.cwd()) {
1518
1601
  return header;
1519
1602
  }
1520
1603
 
1604
+ // src/lib/error-reporter.ts
1605
+ init_src2();
1606
+ import { homedir as homedir4, platform } from "os";
1607
+
1608
+ // src/version.ts
1609
+ import { readFileSync } from "fs";
1610
+ import { fileURLToPath } from "url";
1611
+ import { dirname, join as join4 } from "path";
1612
+ var __dir = dirname(fileURLToPath(import.meta.url));
1613
+ var pkgPath = join4(__dir, "..", "package.json");
1614
+ var VERSION = (() => {
1615
+ try {
1616
+ const raw = readFileSync(pkgPath, "utf8");
1617
+ const parsed = JSON.parse(raw);
1618
+ return typeof parsed.version === "string" ? parsed.version : "0.0.0-unknown";
1619
+ } catch {
1620
+ return "0.0.0-unknown";
1621
+ }
1622
+ })();
1623
+
1624
+ // src/lib/error-reporter.ts
1625
+ var HOME = homedir4();
1626
+ var REPORT_TIMEOUT_MS = 3e3;
1627
+ function sanitizeMessage(raw) {
1628
+ let s = raw;
1629
+ if (HOME && HOME.length > 0) {
1630
+ s = s.split(HOME).join("~");
1631
+ }
1632
+ s = s.replace(/(\/[^\s'"]+){2,}/g, "<path>");
1633
+ s = s.replace(/[A-Za-z]:\\[^\s'"]+/g, "<path>");
1634
+ s = s.replace(/\?[^\s'"]+/g, "?<query>");
1635
+ s = s.replace(/eyJ[\w-]+\.[\w-]+\.[\w-]+/g, "<jwt>");
1636
+ s = s.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<uuid>");
1637
+ s = s.replace(/\b[0-9a-f]{32,}\b/gi, "<hex>");
1638
+ s = s.trim().slice(0, 200);
1639
+ return s.length === 0 ? "<empty>" : s;
1640
+ }
1641
+ function normalisePlatform() {
1642
+ const p = platform();
1643
+ if (p === "darwin" || p === "linux" || p === "win32") return p;
1644
+ return "other";
1645
+ }
1646
+ async function telemetryBaseUrl() {
1647
+ const override = process.env["KORDIS_TELEMETRY_URL"];
1648
+ if (override) return override;
1649
+ const cfg = await loadCliConfig();
1650
+ return cfg.apiUrl;
1651
+ }
1652
+ async function reportError(kind, err, extras = {}) {
1653
+ try {
1654
+ if (process.env["KORDIS_NO_TELEMETRY"] === "1") return;
1655
+ const cfg = await loadCliConfig();
1656
+ if (cfg.error_telemetry !== "on") return;
1657
+ const rawMessage = err instanceof Error ? err.message : String(err);
1658
+ const detectedCode = extras.error_code ?? (err.code !== void 0 ? String(err.code).slice(0, 64) : void 0);
1659
+ const payload = {
1660
+ kind,
1661
+ cli_version: VERSION,
1662
+ node_version: process.version,
1663
+ platform: normalisePlatform(),
1664
+ message_excerpt: sanitizeMessage(rawMessage),
1665
+ ...detectedCode ? { error_code: detectedCode } : {},
1666
+ ...extras.elapsed_ms !== void 0 ? { elapsed_ms: extras.elapsed_ms } : {},
1667
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1668
+ };
1669
+ const baseUrl = await telemetryBaseUrl();
1670
+ const client = new KordisClient({
1671
+ baseUrl,
1672
+ userAgent: `kordis-cli/${VERSION}/telemetry node/${process.version}`
1673
+ });
1674
+ await Promise.race([
1675
+ client.recordTelemetryError(payload),
1676
+ new Promise((resolve2) => setTimeout(resolve2, REPORT_TIMEOUT_MS))
1677
+ ]);
1678
+ } catch {
1679
+ }
1680
+ }
1681
+
1521
1682
  // src/lib/pending-login.ts
1522
1683
  import { readFile as readFile4, writeFile as writeFile3, unlink as unlink2, mkdir as mkdir3 } from "fs/promises";
1523
- import { homedir as homedir4 } from "os";
1524
- import { join as join4 } from "path";
1684
+ import { homedir as homedir5 } from "os";
1685
+ import { join as join5 } from "path";
1525
1686
  function dir() {
1526
- return process.env["KORDIS_HOME"] ?? join4(homedir4(), ".kordis");
1687
+ return process.env["KORDIS_HOME"] ?? join5(homedir5(), ".kordis");
1527
1688
  }
1528
1689
  function path() {
1529
- return join4(dir(), "pending-login.json");
1690
+ return join5(dir(), "pending-login.json");
1530
1691
  }
1531
1692
  async function readPending() {
1532
1693
  try {
@@ -1552,22 +1713,6 @@ function isExpired(file) {
1552
1713
  return Math.floor(Date.now() / 1e3) >= file.expires_at_epoch;
1553
1714
  }
1554
1715
 
1555
- // src/version.ts
1556
- import { readFileSync } from "fs";
1557
- import { fileURLToPath } from "url";
1558
- import { dirname, join as join5 } from "path";
1559
- var __dir = dirname(fileURLToPath(import.meta.url));
1560
- var pkgPath = join5(__dir, "..", "package.json");
1561
- var VERSION = (() => {
1562
- try {
1563
- const raw = readFileSync(pkgPath, "utf8");
1564
- const parsed = JSON.parse(raw);
1565
- return typeof parsed.version === "string" ? parsed.version : "0.0.0-unknown";
1566
- } catch {
1567
- return "0.0.0-unknown";
1568
- }
1569
- })();
1570
-
1571
1716
  // src/client.ts
1572
1717
  async function useClient() {
1573
1718
  let cfg = await loadCliConfig();
@@ -1599,6 +1744,7 @@ async function useClient() {
1599
1744
  process.stderr.write(`[kordis] auto-finalize: poll failed (code=${code ?? "none"}): ${msg}
1600
1745
  `);
1601
1746
  }
1747
+ void reportError("auto_finalize_stage1_fail", err);
1602
1748
  }
1603
1749
  }
1604
1750
  if (pollResult) {
@@ -1631,6 +1777,7 @@ async function useClient() {
1631
1777
  `[kordis] auto-finalize: server issued tokens but local persistence failed (${msg}). Run \`kordis login --emit-only\` again to retry.
1632
1778
  `
1633
1779
  );
1780
+ void reportError("auto_finalize_stage2_fail", err);
1634
1781
  }
1635
1782
  }
1636
1783
  }
@@ -1848,10 +1995,10 @@ import { Command as Command3 } from "commander";
1848
1995
 
1849
1996
  // src/lib/recommendation-cache.ts
1850
1997
  import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
1851
- import { homedir as homedir5 } from "os";
1998
+ import { homedir as homedir6 } from "os";
1852
1999
  import { join as join6 } from "path";
1853
2000
  function cacheDir() {
1854
- return process.env["KORDIS_HOME"] ?? join6(homedir5(), ".kordis");
2001
+ return process.env["KORDIS_HOME"] ?? join6(homedir6(), ".kordis");
1855
2002
  }
1856
2003
  function cachePath() {
1857
2004
  return join6(cacheDir(), "recommendations.json");
@@ -1919,14 +2066,47 @@ var SORT_MODES = [
1919
2066
  "rising"
1920
2067
  ];
1921
2068
  var TIME_WINDOWS = ["24h", "7d", "30d"];
2069
+ var TOOL_SLUGS = [
2070
+ "claude-code",
2071
+ "codex",
2072
+ "cursor",
2073
+ "antigravity",
2074
+ "windsurf",
2075
+ "continue",
2076
+ "copilot",
2077
+ "opencode",
2078
+ "gemini-cli"
2079
+ ];
2080
+ var TOOL_ALIASES = {
2081
+ claude: "claude-code",
2082
+ gemini: "gemini-cli"
2083
+ };
1922
2084
  function searchCommand() {
1923
- return new Command3("search").description("search the Kordis registry").argument("[query...]", "keywords to match against title/description").option("--type <type>", "filter by harness type (skill|mcp-server|project-rules|custom-agent|hook)").option("--tool <tool>", "filter by compatible tool (claude-code|codex|cursor|...)").option("--tier <tier>", "filter by portability tier (A|B|C|D)").option("--author <username>", "filter to a single author (server returns exact total when set)").option("--sort <mode>", `sort: ${SORT_MODES.join("|")}`).option("--trending", "list trending packages (overrides --sort)", false).option("--rising", "list rising packages by 7d acceleration (overrides --sort)", false).option("--window <w>", `time window for --trending: ${TIME_WINDOWS.join("|")}`, "24h").option("--limit <n>", "max results", (v) => parseInt(v, 10), 20).action(async (query, opts) => {
2085
+ return new Command3("search").description("search the Kordis registry").argument("[query...]", "keywords to match against title/description").option("--type <type>", "filter by harness type (skill|mcp-server|project-rules|custom-agent|hook)").option(
2086
+ "--tool <tool>",
2087
+ `filter by compatible tool \u2014 one of: ${TOOL_SLUGS.join("|")} (aliases: ${Object.entries(
2088
+ TOOL_ALIASES
2089
+ ).map(([alias, canonical]) => `${alias}\u2192${canonical}`).join(", ")})`
2090
+ ).option("--tier <tier>", "filter by portability tier (A|B|C|D)").option("--author <username>", "filter to a single author (server returns exact total when set)").option(
2091
+ "--cursor <token>",
2092
+ "pagination cursor \u2014 pass the next_cursor field from a prior search response to fetch the next page"
2093
+ ).option("--sort <mode>", `sort: ${SORT_MODES.join("|")}`).option("--trending", "list trending packages (overrides --sort)", false).option("--rising", "list rising packages by 7d acceleration (overrides --sort)", false).option("--window <w>", `time window for --trending: ${TIME_WINDOWS.join("|")}`, "24h").option("--limit <n>", "max results", (v) => parseInt(v, 10), 20).action(async (query, opts) => {
1924
2094
  if (opts.sort && !SORT_MODES.includes(opts.sort)) {
1925
2095
  throw new Error(`--sort must be one of ${SORT_MODES.join(", ")}`);
1926
2096
  }
1927
2097
  if (opts.window && !TIME_WINDOWS.includes(opts.window)) {
1928
2098
  throw new Error(`--window must be one of ${TIME_WINDOWS.join(", ")}`);
1929
2099
  }
2100
+ let resolvedTool;
2101
+ if (opts.tool) {
2102
+ const candidate = TOOL_ALIASES[opts.tool] ?? opts.tool;
2103
+ if (!TOOL_SLUGS.includes(candidate)) {
2104
+ throw new Error(
2105
+ `--tool must be one of ${TOOL_SLUGS.join(", ")} (you passed "${opts.tool}")`
2106
+ );
2107
+ }
2108
+ resolvedTool = candidate;
2109
+ }
1930
2110
  const bundle = await useClient();
1931
2111
  let res;
1932
2112
  if (opts.rising) {
@@ -1944,9 +2124,10 @@ function searchCommand() {
1944
2124
  res = await bundle.api.search({
1945
2125
  q: query.join(" ") || void 0,
1946
2126
  type: opts.type ? [opts.type] : void 0,
1947
- tool: opts.tool ? [opts.tool] : void 0,
2127
+ tool: resolvedTool ? [resolvedTool] : void 0,
1948
2128
  tier: opts.tier,
1949
2129
  ...opts.author ? { author: opts.author } : {},
2130
+ ...opts.cursor ? { cursor: opts.cursor } : {},
1950
2131
  sort: opts.sort ?? "relevance",
1951
2132
  limit: opts.limit
1952
2133
  });
@@ -2073,9 +2254,9 @@ var FrontmatterParseError = class extends Error {
2073
2254
 
2074
2255
  // ../../packages/harness-schema/src/deps.ts
2075
2256
  init_src();
2076
- import { z as z12 } from "zod";
2257
+ import { z as z13 } from "zod";
2077
2258
  var StringDepRe = /^([a-z0-9](-?[a-z0-9])*\/[a-z0-9](-?[a-z0-9])*)(?:@(.+))?$/;
2078
- var ObjectDepSchema = z12.object({
2259
+ var ObjectDepSchema = z13.object({
2079
2260
  full_slug: FullSlugSchema,
2080
2261
  version_range: SemverRangeSchema.default("*")
2081
2262
  });
@@ -2111,50 +2292,50 @@ init_src();
2111
2292
 
2112
2293
  // ../../packages/harness-schema/src/types/skill.ts
2113
2294
  init_src();
2114
- import { z as z13 } from "zod";
2115
- var SkillFrontmatterSchema = z13.object({
2116
- name: z13.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
2117
- description: z13.string().min(1).max(2048),
2295
+ import { z as z14 } from "zod";
2296
+ var SkillFrontmatterSchema = z14.object({
2297
+ name: z14.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
2298
+ description: z14.string().min(1).max(2048),
2118
2299
  version: SemverSchema.optional(),
2119
2300
  // Agent Skills optional metadata
2120
- license: z13.string().max(64).optional(),
2121
- homepage: z13.string().url().optional(),
2122
- model: z13.string().optional(),
2301
+ license: z14.string().max(64).optional(),
2302
+ homepage: z14.string().url().optional(),
2303
+ model: z14.string().optional(),
2123
2304
  // model override hint
2124
- "allowed-tools": z13.array(z13.string()).optional(),
2125
- "disable-model-invocation": z13.boolean().optional(),
2305
+ "allowed-tools": z14.array(z14.string()).optional(),
2306
+ "disable-model-invocation": z14.boolean().optional(),
2126
2307
  // Kordis-forwarded cross-type fields (kept minimal to not pollute the spec)
2127
- title: z13.string().min(1).max(200).optional(),
2128
- tags: z13.array(TagSchema).max(16).optional(),
2129
- tools: z13.array(ToolSlugSchema).optional()
2308
+ title: z14.string().min(1).max(200).optional(),
2309
+ tags: z14.array(TagSchema).max(16).optional(),
2310
+ tools: z14.array(ToolSlugSchema).optional()
2130
2311
  }).strict();
2131
2312
 
2132
2313
  // ../../packages/harness-schema/src/types/mcp-server.ts
2133
2314
  init_src();
2134
- import { z as z14 } from "zod";
2135
- var McpTransportSchema = z14.union([
2136
- z14.object({
2137
- type: z14.literal("stdio"),
2138
- command: z14.string().min(1),
2139
- args: z14.array(z14.string()).optional().default([]),
2140
- env: z14.record(z14.string()).optional().default({})
2315
+ import { z as z15 } from "zod";
2316
+ var McpTransportSchema = z15.union([
2317
+ z15.object({
2318
+ type: z15.literal("stdio"),
2319
+ command: z15.string().min(1),
2320
+ args: z15.array(z15.string()).optional().default([]),
2321
+ env: z15.record(z15.string()).optional().default({})
2141
2322
  }),
2142
- z14.object({
2143
- type: z14.enum(["http", "sse"]),
2144
- url: z14.string().url(),
2145
- headers: z14.record(z14.string()).optional().default({})
2323
+ z15.object({
2324
+ type: z15.enum(["http", "sse"]),
2325
+ url: z15.string().url(),
2326
+ headers: z15.record(z15.string()).optional().default({})
2146
2327
  })
2147
2328
  ]);
2148
- var McpCapabilitiesSchema = z14.object({
2149
- tools: z14.array(z14.string()).optional().default([]),
2150
- resources: z14.array(z14.string()).optional().default([]),
2151
- prompts: z14.array(z14.string()).optional().default([])
2329
+ var McpCapabilitiesSchema = z15.object({
2330
+ tools: z15.array(z15.string()).optional().default([]),
2331
+ resources: z15.array(z15.string()).optional().default([]),
2332
+ prompts: z15.array(z15.string()).optional().default([])
2152
2333
  });
2153
- var McpServerFrontmatterSchema = z14.object({
2154
- name: z14.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2155
- description: z14.string().min(1).max(2048),
2334
+ var McpServerFrontmatterSchema = z15.object({
2335
+ name: z15.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2336
+ description: z15.string().min(1).max(2048),
2156
2337
  version: SemverSchema,
2157
- mcp_spec_version: z14.string().default("2026-03"),
2338
+ mcp_spec_version: z15.string().default("2026-03"),
2158
2339
  transport: McpTransportSchema,
2159
2340
  capabilities: McpCapabilitiesSchema.optional().default({
2160
2341
  tools: [],
@@ -2162,35 +2343,35 @@ var McpServerFrontmatterSchema = z14.object({
2162
2343
  prompts: []
2163
2344
  }),
2164
2345
  /** Human-readable setup / auth instructions. Surfaced in `kordis info`. */
2165
- auth_notes: z14.string().optional(),
2346
+ auth_notes: z15.string().optional(),
2166
2347
  /** Required env vars with description + whether they must be secret. */
2167
- env_schema: z14.record(
2168
- z14.object({
2169
- description: z14.string(),
2170
- required: z14.boolean().default(true),
2171
- secret: z14.boolean().default(false)
2348
+ env_schema: z15.record(
2349
+ z15.object({
2350
+ description: z15.string(),
2351
+ required: z15.boolean().default(true),
2352
+ secret: z15.boolean().default(false)
2172
2353
  })
2173
2354
  ).optional(),
2174
- homepage: z14.string().url().optional(),
2175
- repository: z14.string().url().optional(),
2176
- license: z14.string().max(64).optional(),
2177
- title: z14.string().min(1).max(200).optional()
2355
+ homepage: z15.string().url().optional(),
2356
+ repository: z15.string().url().optional(),
2357
+ license: z15.string().max(64).optional(),
2358
+ title: z15.string().min(1).max(200).optional()
2178
2359
  }).strict();
2179
2360
 
2180
2361
  // ../../packages/harness-schema/src/types/project-rules.ts
2181
2362
  init_src();
2182
- import { z as z15 } from "zod";
2183
- var ProjectRulesFrontmatterSchema = z15.object({
2184
- title: z15.string().min(1).max(200),
2185
- description: z15.string().min(1).max(2048),
2363
+ import { z as z16 } from "zod";
2364
+ var ProjectRulesFrontmatterSchema = z16.object({
2365
+ title: z16.string().min(1).max(200),
2366
+ description: z16.string().min(1).max(2048),
2186
2367
  version: SemverSchema.optional(),
2187
- scope: z15.enum(["root", "nested", "globs"]).default("root"),
2188
- target_path: z15.string().optional(),
2189
- globs: z15.array(z15.string()).optional(),
2190
- target_tools: z15.array(ToolSlugSchema).optional().default([]),
2368
+ scope: z16.enum(["root", "nested", "globs"]).default("root"),
2369
+ target_path: z16.string().optional(),
2370
+ globs: z16.array(z16.string()).optional(),
2371
+ target_tools: z16.array(ToolSlugSchema).optional().default([]),
2191
2372
  /** When true, installation is idempotent via `<!-- kordis:... -->` markers. */
2192
- mergeable: z15.boolean().default(true),
2193
- license: z15.string().max(64).optional()
2373
+ mergeable: z16.boolean().default(true),
2374
+ license: z16.string().max(64).optional()
2194
2375
  }).strict().refine(
2195
2376
  (v) => v.scope !== "nested" || !!v.target_path,
2196
2377
  { message: 'scope="nested" requires target_path' }
@@ -2201,33 +2382,33 @@ var ProjectRulesFrontmatterSchema = z15.object({
2201
2382
 
2202
2383
  // ../../packages/harness-schema/src/types/custom-agent.ts
2203
2384
  init_src();
2204
- import { z as z16 } from "zod";
2205
- var CustomAgentFrontmatterSchema = z16.object({
2206
- name: z16.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2207
- description: z16.string().min(1).max(2048),
2208
- title: z16.string().min(1).max(200).optional(),
2385
+ import { z as z17 } from "zod";
2386
+ var CustomAgentFrontmatterSchema = z17.object({
2387
+ name: z17.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2388
+ description: z17.string().min(1).max(2048),
2389
+ title: z17.string().min(1).max(200).optional(),
2209
2390
  version: SemverSchema.optional(),
2210
2391
  /** System prompt that defines the agent's persona + rules. Required. */
2211
- system_prompt: z16.string().min(1).max(5e4),
2392
+ system_prompt: z17.string().min(1).max(5e4),
2212
2393
  /** Optional model override — string passed verbatim to the host. */
2213
- model: z16.string().optional(),
2394
+ model: z17.string().optional(),
2214
2395
  /** Tools this agent can call. Host-specific names (no uniform namespace). */
2215
- allowed_tools: z16.array(z16.string()).optional().default([]),
2216
- disallowed_tools: z16.array(z16.string()).optional().default([]),
2396
+ allowed_tools: z17.array(z17.string()).optional().default([]),
2397
+ disallowed_tools: z17.array(z17.string()).optional().default([]),
2217
2398
  /** Fork/context behaviour hint (Claude: `context: fork` etc.). */
2218
- context: z16.enum(["inherit", "fork", "isolated"]).optional(),
2399
+ context: z17.enum(["inherit", "fork", "isolated"]).optional(),
2219
2400
  /** Known-good tool runtimes. Empty = untested everywhere. */
2220
- tool_compat: z16.array(ToolSlugSchema).optional().default([]),
2401
+ tool_compat: z17.array(ToolSlugSchema).optional().default([]),
2221
2402
  /** Pre-load skills when this agent activates. */
2222
- preload_skills: z16.array(z16.string()).optional().default([]),
2223
- license: z16.string().max(64).optional(),
2224
- homepage: z16.string().url().optional()
2403
+ preload_skills: z17.array(z17.string()).optional().default([]),
2404
+ license: z17.string().max(64).optional(),
2405
+ homepage: z17.string().url().optional()
2225
2406
  }).strict();
2226
2407
 
2227
2408
  // ../../packages/harness-schema/src/types/hook.ts
2228
2409
  init_src();
2229
- import { z as z17 } from "zod";
2230
- var HookEventSchema = z17.enum([
2410
+ import { z as z18 } from "zod";
2411
+ var HookEventSchema = z18.enum([
2231
2412
  "PreToolUse",
2232
2413
  "PostToolUse",
2233
2414
  "UserPromptSubmit",
@@ -2239,37 +2420,37 @@ var HookEventSchema = z17.enum([
2239
2420
  "SubagentStop",
2240
2421
  "Background"
2241
2422
  ]);
2242
- var HookActionSchema = z17.union([
2243
- z17.object({
2244
- type: z17.literal("command"),
2245
- command: z17.string().min(1),
2246
- timeout_ms: z17.number().int().positive().max(6e5).optional(),
2247
- blocking: z17.boolean().default(false)
2423
+ var HookActionSchema = z18.union([
2424
+ z18.object({
2425
+ type: z18.literal("command"),
2426
+ command: z18.string().min(1),
2427
+ timeout_ms: z18.number().int().positive().max(6e5).optional(),
2428
+ blocking: z18.boolean().default(false)
2248
2429
  }),
2249
- z17.object({
2250
- type: z17.literal("http"),
2251
- url: z17.string().url(),
2252
- method: z17.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
2253
- headers: z17.record(z17.string()).optional().default({}),
2254
- timeout_ms: z17.number().int().positive().max(6e5).optional(),
2255
- blocking: z17.boolean().default(false)
2430
+ z18.object({
2431
+ type: z18.literal("http"),
2432
+ url: z18.string().url(),
2433
+ method: z18.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
2434
+ headers: z18.record(z18.string()).optional().default({}),
2435
+ timeout_ms: z18.number().int().positive().max(6e5).optional(),
2436
+ blocking: z18.boolean().default(false)
2256
2437
  })
2257
2438
  ]);
2258
- var HookBindingSchema = z17.object({
2439
+ var HookBindingSchema = z18.object({
2259
2440
  event: HookEventSchema,
2260
- matcher: z17.string().optional(),
2441
+ matcher: z18.string().optional(),
2261
2442
  action: HookActionSchema
2262
2443
  });
2263
- var HookFrontmatterSchema = z17.object({
2264
- name: z17.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2265
- description: z17.string().min(1).max(2048),
2266
- title: z17.string().min(1).max(200).optional(),
2444
+ var HookFrontmatterSchema = z18.object({
2445
+ name: z18.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2446
+ description: z18.string().min(1).max(2048),
2447
+ title: z18.string().min(1).max(200).optional(),
2267
2448
  version: SemverSchema.optional(),
2268
2449
  /** One or more hook bindings. */
2269
- hooks: z17.array(HookBindingSchema).min(1),
2450
+ hooks: z18.array(HookBindingSchema).min(1),
2270
2451
  /** Declared to surface in search filters — always "claude-code" for now. */
2271
- tool_compat: z17.array(z17.enum(["claude-code"])).default(["claude-code"]),
2272
- license: z17.string().max(64).optional()
2452
+ tool_compat: z18.array(z18.enum(["claude-code"])).default(["claude-code"]),
2453
+ license: z18.string().max(64).optional()
2273
2454
  }).strict();
2274
2455
 
2275
2456
  // ../../packages/harness-schema/src/validator.ts
@@ -3299,21 +3480,26 @@ function getTranslator(type, format) {
3299
3480
  }
3300
3481
 
3301
3482
  // src/lockfile.ts
3302
- import { homedir as homedir6 } from "os";
3483
+ import { homedir as homedir7 } from "os";
3303
3484
  import { join as join8 } from "path";
3304
3485
  import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
3305
- var LOCKFILE_PATH = join8(homedir6(), ".kordis", "installed.json");
3486
+ function kordisHome2() {
3487
+ return process.env["KORDIS_HOME"] ?? join8(homedir7(), ".kordis");
3488
+ }
3489
+ function lockfilePath() {
3490
+ return join8(kordisHome2(), "installed.json");
3491
+ }
3306
3492
  async function readLockfile() {
3307
3493
  try {
3308
- const raw = await readFile7(LOCKFILE_PATH, "utf8");
3494
+ const raw = await readFile7(lockfilePath(), "utf8");
3309
3495
  return JSON.parse(raw);
3310
3496
  } catch {
3311
3497
  return { packages: {} };
3312
3498
  }
3313
3499
  }
3314
3500
  async function writeLockfile(lf) {
3315
- await mkdir5(join8(homedir6(), ".kordis"), { recursive: true });
3316
- await writeFile5(LOCKFILE_PATH, JSON.stringify(lf, null, 2) + "\n", "utf8");
3501
+ await mkdir5(kordisHome2(), { recursive: true });
3502
+ await writeFile5(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", "utf8");
3317
3503
  }
3318
3504
  async function recordInstalled(fullSlug, entry) {
3319
3505
  const lf = await readLockfile();
@@ -3582,7 +3768,7 @@ function logoutCommand() {
3582
3768
 
3583
3769
  // src/commands/setup.ts
3584
3770
  import { readFile as readFile8, writeFile as writeFile7, mkdir as mkdir7, access as access3 } from "fs/promises";
3585
- import { homedir as homedir7 } from "os";
3771
+ import { homedir as homedir8 } from "os";
3586
3772
  import { join as join10, dirname as dirname3 } from "path";
3587
3773
  import { fileURLToPath as fileURLToPath2 } from "url";
3588
3774
  import { Command as Command9 } from "commander";
@@ -3691,8 +3877,11 @@ function setupCommand() {
3691
3877
  return new Command9("setup").description("inject kordis usage guide into global agent instruction files").option("--silent", "suppress errors \u2014 safe for postinstall").option("--dry-run", "detect installed tools without writing any files").option(
3692
3878
  "--personalization <choice>",
3693
3879
  "record opt-in non-interactively: 'on' | 'off' (skips the Y/n prompt; safe for agents that just received user consent in chat)"
3880
+ ).option(
3881
+ "--error-telemetry <choice>",
3882
+ "record opt-in non-interactively: 'on' | 'off' (skips the Y/n prompt; agent forwards consent received in chat)"
3694
3883
  ).action(async (opts) => {
3695
- const home = homedir7();
3884
+ const home = homedir8();
3696
3885
  const dryRun = opts.dryRun ?? false;
3697
3886
  if (opts.personalization !== void 0) {
3698
3887
  const v = opts.personalization.toLowerCase();
@@ -3715,6 +3904,27 @@ function setupCommand() {
3715
3904
  }
3716
3905
  }
3717
3906
  }
3907
+ if (opts.errorTelemetry !== void 0) {
3908
+ const v = opts.errorTelemetry.toLowerCase();
3909
+ if (v !== "on" && v !== "off") {
3910
+ throw new Error("--error-telemetry must be 'on' or 'off'");
3911
+ }
3912
+ if (!dryRun) {
3913
+ const cfg = await loadCliConfig();
3914
+ await saveCliConfig({ ...cfg, error_telemetry: v });
3915
+ }
3916
+ if (!opts.silent) {
3917
+ if (v === "on") {
3918
+ printHuman(
3919
+ c.green("\u2713 error telemetry on") + c.gray(" \u2014 anonymized: kind + cli/node version + platform + sanitized message excerpt (\u2264200 chars). Never carries identity, paths, or env values. Override any time: `KORDIS_NO_TELEMETRY=1` env or `kordis setup --error-telemetry off`")
3920
+ );
3921
+ } else {
3922
+ printHuman(
3923
+ c.gray("\u2192 error telemetry off \u2014 failures stay on your machine.")
3924
+ );
3925
+ }
3926
+ }
3927
+ }
3718
3928
  let hubContent;
3719
3929
  let subSkillContents;
3720
3930
  try {
@@ -3820,6 +4030,30 @@ function setupCommand() {
3820
4030
  );
3821
4031
  }
3822
4032
  }
4033
+ const cfg2 = await loadCliConfig();
4034
+ if (cfg2.error_telemetry === "undecided") {
4035
+ printHuman("");
4036
+ const { enable } = await prompts({
4037
+ type: "confirm",
4038
+ name: "enable",
4039
+ message: "Send anonymized error reports to help fix wild failures? Payload: kind + cli/node version + platform + sanitized message excerpt (\u2264200 chars). NEVER includes paths, env values, identity, query text, or the full stack. KORDIS_NO_TELEMETRY=1 disables any time.",
4040
+ initial: true
4041
+ });
4042
+ const newCfg = {
4043
+ ...cfg2,
4044
+ error_telemetry: enable ? "on" : "off"
4045
+ };
4046
+ await saveCliConfig(newCfg);
4047
+ if (enable) {
4048
+ printHuman(
4049
+ c.green("\u2713 error telemetry on") + c.gray(" \u2014 disable any time: `KORDIS_NO_TELEMETRY=1` env or `kordis setup --error-telemetry off`")
4050
+ );
4051
+ } else {
4052
+ printHuman(
4053
+ c.gray("\u2192 error telemetry off \u2014 failures stay on your machine.")
4054
+ );
4055
+ }
4056
+ }
3823
4057
  }
3824
4058
  });
3825
4059
  }
@@ -3833,6 +4067,8 @@ var KINDS = [
3833
4067
  "praise",
3834
4068
  "question"
3835
4069
  ];
4070
+ var LIST_SORTS = ["recent", "old"];
4071
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3836
4072
  function commentCommand() {
3837
4073
  return new Command10("comment").description("post a comment on a package").argument("<full_slug>", "package identifier {username}/{slug}").requiredOption("--body <text>", "comment body (1\u201310000 chars)").option("--kind <kind>", `top-level kind: ${KINDS.join("|")}`, "comment").option("--title <text>", "top-level post title (1\u2013140 chars)").action(
3838
4074
  async (rawSlug, opts) => {
@@ -3863,11 +4099,103 @@ function commentCommand() {
3863
4099
  if (res.permalink) {
3864
4100
  printHuman(c.gray(` view: ${res.permalink}`));
3865
4101
  }
4102
+ printHuman(c.gray(` delete: kordis uncomment ${res.id}`));
3866
4103
  }
3867
4104
  });
3868
4105
  }
3869
4106
  );
3870
4107
  }
4108
+ function commentsCommand() {
4109
+ return new Command10("comments").description("list comments on a package").argument("<full_slug>", "package identifier {username}/{slug}").option("--limit <n>", "max top-level comments (1\u201350)", (v) => parseInt(v, 10), 20).option("--cursor <token>", "pagination cursor returned by a prior call as next_cursor").option(
4110
+ "--kind <kind>",
4111
+ `filter top-level posts by kind: ${KINDS.join("|")}`
4112
+ ).option("--sort <mode>", `sort: ${LIST_SORTS.join("|")} (default: recent)`).action(
4113
+ async (rawSlug, opts) => {
4114
+ const [username, slug] = rawSlug.split("/");
4115
+ if (!username || !slug) {
4116
+ throw new Error('full_slug must be "{username}/{slug}"');
4117
+ }
4118
+ if (opts.kind && !KINDS.includes(opts.kind)) {
4119
+ throw new Error(`--kind must be one of ${KINDS.join(", ")}`);
4120
+ }
4121
+ if (opts.sort && !LIST_SORTS.includes(opts.sort)) {
4122
+ throw new Error(`--sort must be one of ${LIST_SORTS.join(", ")}`);
4123
+ }
4124
+ const bundle = await useClient();
4125
+ const res = await bundle.api.listComments(username, slug, {
4126
+ limit: opts.limit,
4127
+ ...opts.cursor ? { cursor: opts.cursor } : {},
4128
+ ...opts.kind ? { kind: opts.kind } : {},
4129
+ ...opts.sort ? { sort: opts.sort } : {}
4130
+ });
4131
+ printDual({
4132
+ json: res,
4133
+ human: () => {
4134
+ if (res.results.length === 0) {
4135
+ printHuman(c.yellow("(no comments)"));
4136
+ printHuman(
4137
+ c.gray(" \u2192 be the first: kordis comment " + rawSlug + ' --body "..."')
4138
+ );
4139
+ return;
4140
+ }
4141
+ printHuman(
4142
+ c.gray(
4143
+ `Showing ${res.results.length} of ${res.total} top-level comment${res.total === 1 ? "" : "s"}`
4144
+ )
4145
+ );
4146
+ printHuman("");
4147
+ for (const node of res.results) {
4148
+ renderCommentNode(node, 0);
4149
+ }
4150
+ if (res.next_cursor) {
4151
+ printHuman(
4152
+ c.gray(
4153
+ `(more available \u2014 kordis comments ${rawSlug} --cursor ${res.next_cursor})`
4154
+ )
4155
+ );
4156
+ }
4157
+ }
4158
+ });
4159
+ }
4160
+ );
4161
+ }
4162
+ function renderCommentNode(node, depth) {
4163
+ const indent = " ".repeat(depth);
4164
+ const tag = node.kind === "comment" ? "" : c.cyan(`[${node.kind}]`) + " ";
4165
+ const title = node.title ? c.bold(node.title) + " " : "";
4166
+ const author = `${node.author.username}${node.author.display_name ? ` (${node.author.display_name})` : ""}`;
4167
+ printHuman(`${indent}${tag}${title}${c.gray(`\u2014 ${author}`)}`);
4168
+ for (const line of node.body.split("\n")) {
4169
+ printHuman(`${indent} ${line}`);
4170
+ }
4171
+ const trail = [`id: ${node.id}`];
4172
+ if (node.reply_count > 0) trail.push(`${node.reply_count} repl${node.reply_count === 1 ? "y" : "ies"}`);
4173
+ if (node.is_edited) trail.push("edited");
4174
+ if (node.permalink) trail.push(`view: ${node.permalink}`);
4175
+ printHuman(c.gray(`${indent} ${trail.join(" \xB7 ")}`));
4176
+ printHuman("");
4177
+ for (const child of node.replies) {
4178
+ renderCommentNode(child, depth + 1);
4179
+ }
4180
+ }
4181
+ function uncommentCommand() {
4182
+ return new Command10("uncomment").description("delete one of your comments by id").argument("<id>", "comment id (uuid v4)").action(async (id) => {
4183
+ if (!UUID_RE.test(id)) {
4184
+ throw new Error(
4185
+ "comment id must be a uuid v4 \u2014 get it from `kordis comments <slug>` or the `id` field on `kordis comment`'s response"
4186
+ );
4187
+ }
4188
+ const bundle = await useClient();
4189
+ requireAuth(bundle);
4190
+ await bundle.authedRequest((api) => api.deleteComment(id));
4191
+ printDual({
4192
+ json: { status: "deleted", id },
4193
+ human: () => {
4194
+ printHuman(c.gray(`\u2717 deleted comment ${id}`));
4195
+ }
4196
+ });
4197
+ });
4198
+ }
3871
4199
 
3872
4200
  // src/commands/star.ts
3873
4201
  import { Command as Command11 } from "commander";
@@ -4465,6 +4793,8 @@ function buildProgram() {
4465
4793
  program.addCommand(readCommand());
4466
4794
  program.addCommand(infoCommand());
4467
4795
  program.addCommand(commentCommand());
4796
+ program.addCommand(commentsCommand());
4797
+ program.addCommand(uncommentCommand());
4468
4798
  program.addCommand(starCommand());
4469
4799
  program.addCommand(unstarCommand());
4470
4800
  program.addCommand(deleteCommand());
@@ -4533,6 +4863,7 @@ function handleError(err) {
4533
4863
  printHuman(c.red(`\u2717 ${err.message}`));
4534
4864
  for (const line of hint.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
4535
4865
  }
4866
+ void reportError("network_unreachable", err);
4536
4867
  process.exit(err.exitCode());
4537
4868
  }
4538
4869
  if (err instanceof Error) {
@@ -4552,6 +4883,7 @@ function handleError(err) {
4552
4883
  for (const line of hintStr.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
4553
4884
  }
4554
4885
  }
4886
+ void reportError("unhandled_error", err);
4555
4887
  process.exit(1);
4556
4888
  }
4557
4889
  if (isJsonMode()) printJson({ error: { code: "generic_failure", message: String(err) } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "Command-line client for the Kordis registry — discover, install, and publish agentic-coding packages.",
6
6
  "license": "MIT",