@rayul/kordis 0.6.7 → 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");
@@ -2107,9 +2254,9 @@ var FrontmatterParseError = class extends Error {
2107
2254
 
2108
2255
  // ../../packages/harness-schema/src/deps.ts
2109
2256
  init_src();
2110
- import { z as z12 } from "zod";
2257
+ import { z as z13 } from "zod";
2111
2258
  var StringDepRe = /^([a-z0-9](-?[a-z0-9])*\/[a-z0-9](-?[a-z0-9])*)(?:@(.+))?$/;
2112
- var ObjectDepSchema = z12.object({
2259
+ var ObjectDepSchema = z13.object({
2113
2260
  full_slug: FullSlugSchema,
2114
2261
  version_range: SemverRangeSchema.default("*")
2115
2262
  });
@@ -2145,50 +2292,50 @@ init_src();
2145
2292
 
2146
2293
  // ../../packages/harness-schema/src/types/skill.ts
2147
2294
  init_src();
2148
- import { z as z13 } from "zod";
2149
- var SkillFrontmatterSchema = z13.object({
2150
- name: z13.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
2151
- 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),
2152
2299
  version: SemverSchema.optional(),
2153
2300
  // Agent Skills optional metadata
2154
- license: z13.string().max(64).optional(),
2155
- homepage: z13.string().url().optional(),
2156
- model: z13.string().optional(),
2301
+ license: z14.string().max(64).optional(),
2302
+ homepage: z14.string().url().optional(),
2303
+ model: z14.string().optional(),
2157
2304
  // model override hint
2158
- "allowed-tools": z13.array(z13.string()).optional(),
2159
- "disable-model-invocation": z13.boolean().optional(),
2305
+ "allowed-tools": z14.array(z14.string()).optional(),
2306
+ "disable-model-invocation": z14.boolean().optional(),
2160
2307
  // Kordis-forwarded cross-type fields (kept minimal to not pollute the spec)
2161
- title: z13.string().min(1).max(200).optional(),
2162
- tags: z13.array(TagSchema).max(16).optional(),
2163
- 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()
2164
2311
  }).strict();
2165
2312
 
2166
2313
  // ../../packages/harness-schema/src/types/mcp-server.ts
2167
2314
  init_src();
2168
- import { z as z14 } from "zod";
2169
- var McpTransportSchema = z14.union([
2170
- z14.object({
2171
- type: z14.literal("stdio"),
2172
- command: z14.string().min(1),
2173
- args: z14.array(z14.string()).optional().default([]),
2174
- 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({})
2175
2322
  }),
2176
- z14.object({
2177
- type: z14.enum(["http", "sse"]),
2178
- url: z14.string().url(),
2179
- 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({})
2180
2327
  })
2181
2328
  ]);
2182
- var McpCapabilitiesSchema = z14.object({
2183
- tools: z14.array(z14.string()).optional().default([]),
2184
- resources: z14.array(z14.string()).optional().default([]),
2185
- 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([])
2186
2333
  });
2187
- var McpServerFrontmatterSchema = z14.object({
2188
- name: z14.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2189
- 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),
2190
2337
  version: SemverSchema,
2191
- mcp_spec_version: z14.string().default("2026-03"),
2338
+ mcp_spec_version: z15.string().default("2026-03"),
2192
2339
  transport: McpTransportSchema,
2193
2340
  capabilities: McpCapabilitiesSchema.optional().default({
2194
2341
  tools: [],
@@ -2196,35 +2343,35 @@ var McpServerFrontmatterSchema = z14.object({
2196
2343
  prompts: []
2197
2344
  }),
2198
2345
  /** Human-readable setup / auth instructions. Surfaced in `kordis info`. */
2199
- auth_notes: z14.string().optional(),
2346
+ auth_notes: z15.string().optional(),
2200
2347
  /** Required env vars with description + whether they must be secret. */
2201
- env_schema: z14.record(
2202
- z14.object({
2203
- description: z14.string(),
2204
- required: z14.boolean().default(true),
2205
- 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)
2206
2353
  })
2207
2354
  ).optional(),
2208
- homepage: z14.string().url().optional(),
2209
- repository: z14.string().url().optional(),
2210
- license: z14.string().max(64).optional(),
2211
- 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()
2212
2359
  }).strict();
2213
2360
 
2214
2361
  // ../../packages/harness-schema/src/types/project-rules.ts
2215
2362
  init_src();
2216
- import { z as z15 } from "zod";
2217
- var ProjectRulesFrontmatterSchema = z15.object({
2218
- title: z15.string().min(1).max(200),
2219
- 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),
2220
2367
  version: SemverSchema.optional(),
2221
- scope: z15.enum(["root", "nested", "globs"]).default("root"),
2222
- target_path: z15.string().optional(),
2223
- globs: z15.array(z15.string()).optional(),
2224
- 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([]),
2225
2372
  /** When true, installation is idempotent via `<!-- kordis:... -->` markers. */
2226
- mergeable: z15.boolean().default(true),
2227
- license: z15.string().max(64).optional()
2373
+ mergeable: z16.boolean().default(true),
2374
+ license: z16.string().max(64).optional()
2228
2375
  }).strict().refine(
2229
2376
  (v) => v.scope !== "nested" || !!v.target_path,
2230
2377
  { message: 'scope="nested" requires target_path' }
@@ -2235,33 +2382,33 @@ var ProjectRulesFrontmatterSchema = z15.object({
2235
2382
 
2236
2383
  // ../../packages/harness-schema/src/types/custom-agent.ts
2237
2384
  init_src();
2238
- import { z as z16 } from "zod";
2239
- var CustomAgentFrontmatterSchema = z16.object({
2240
- name: z16.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2241
- description: z16.string().min(1).max(2048),
2242
- 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(),
2243
2390
  version: SemverSchema.optional(),
2244
2391
  /** System prompt that defines the agent's persona + rules. Required. */
2245
- system_prompt: z16.string().min(1).max(5e4),
2392
+ system_prompt: z17.string().min(1).max(5e4),
2246
2393
  /** Optional model override — string passed verbatim to the host. */
2247
- model: z16.string().optional(),
2394
+ model: z17.string().optional(),
2248
2395
  /** Tools this agent can call. Host-specific names (no uniform namespace). */
2249
- allowed_tools: z16.array(z16.string()).optional().default([]),
2250
- 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([]),
2251
2398
  /** Fork/context behaviour hint (Claude: `context: fork` etc.). */
2252
- context: z16.enum(["inherit", "fork", "isolated"]).optional(),
2399
+ context: z17.enum(["inherit", "fork", "isolated"]).optional(),
2253
2400
  /** Known-good tool runtimes. Empty = untested everywhere. */
2254
- tool_compat: z16.array(ToolSlugSchema).optional().default([]),
2401
+ tool_compat: z17.array(ToolSlugSchema).optional().default([]),
2255
2402
  /** Pre-load skills when this agent activates. */
2256
- preload_skills: z16.array(z16.string()).optional().default([]),
2257
- license: z16.string().max(64).optional(),
2258
- 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()
2259
2406
  }).strict();
2260
2407
 
2261
2408
  // ../../packages/harness-schema/src/types/hook.ts
2262
2409
  init_src();
2263
- import { z as z17 } from "zod";
2264
- var HookEventSchema = z17.enum([
2410
+ import { z as z18 } from "zod";
2411
+ var HookEventSchema = z18.enum([
2265
2412
  "PreToolUse",
2266
2413
  "PostToolUse",
2267
2414
  "UserPromptSubmit",
@@ -2273,37 +2420,37 @@ var HookEventSchema = z17.enum([
2273
2420
  "SubagentStop",
2274
2421
  "Background"
2275
2422
  ]);
2276
- var HookActionSchema = z17.union([
2277
- z17.object({
2278
- type: z17.literal("command"),
2279
- command: z17.string().min(1),
2280
- timeout_ms: z17.number().int().positive().max(6e5).optional(),
2281
- 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)
2282
2429
  }),
2283
- z17.object({
2284
- type: z17.literal("http"),
2285
- url: z17.string().url(),
2286
- method: z17.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
2287
- headers: z17.record(z17.string()).optional().default({}),
2288
- timeout_ms: z17.number().int().positive().max(6e5).optional(),
2289
- 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)
2290
2437
  })
2291
2438
  ]);
2292
- var HookBindingSchema = z17.object({
2439
+ var HookBindingSchema = z18.object({
2293
2440
  event: HookEventSchema,
2294
- matcher: z17.string().optional(),
2441
+ matcher: z18.string().optional(),
2295
2442
  action: HookActionSchema
2296
2443
  });
2297
- var HookFrontmatterSchema = z17.object({
2298
- name: z17.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
2299
- description: z17.string().min(1).max(2048),
2300
- 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(),
2301
2448
  version: SemverSchema.optional(),
2302
2449
  /** One or more hook bindings. */
2303
- hooks: z17.array(HookBindingSchema).min(1),
2450
+ hooks: z18.array(HookBindingSchema).min(1),
2304
2451
  /** Declared to surface in search filters — always "claude-code" for now. */
2305
- tool_compat: z17.array(z17.enum(["claude-code"])).default(["claude-code"]),
2306
- 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()
2307
2454
  }).strict();
2308
2455
 
2309
2456
  // ../../packages/harness-schema/src/validator.ts
@@ -3333,11 +3480,11 @@ function getTranslator(type, format) {
3333
3480
  }
3334
3481
 
3335
3482
  // src/lockfile.ts
3336
- import { homedir as homedir6 } from "os";
3483
+ import { homedir as homedir7 } from "os";
3337
3484
  import { join as join8 } from "path";
3338
3485
  import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
3339
3486
  function kordisHome2() {
3340
- return process.env["KORDIS_HOME"] ?? join8(homedir6(), ".kordis");
3487
+ return process.env["KORDIS_HOME"] ?? join8(homedir7(), ".kordis");
3341
3488
  }
3342
3489
  function lockfilePath() {
3343
3490
  return join8(kordisHome2(), "installed.json");
@@ -3621,7 +3768,7 @@ function logoutCommand() {
3621
3768
 
3622
3769
  // src/commands/setup.ts
3623
3770
  import { readFile as readFile8, writeFile as writeFile7, mkdir as mkdir7, access as access3 } from "fs/promises";
3624
- import { homedir as homedir7 } from "os";
3771
+ import { homedir as homedir8 } from "os";
3625
3772
  import { join as join10, dirname as dirname3 } from "path";
3626
3773
  import { fileURLToPath as fileURLToPath2 } from "url";
3627
3774
  import { Command as Command9 } from "commander";
@@ -3730,8 +3877,11 @@ function setupCommand() {
3730
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(
3731
3878
  "--personalization <choice>",
3732
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)"
3733
3883
  ).action(async (opts) => {
3734
- const home = homedir7();
3884
+ const home = homedir8();
3735
3885
  const dryRun = opts.dryRun ?? false;
3736
3886
  if (opts.personalization !== void 0) {
3737
3887
  const v = opts.personalization.toLowerCase();
@@ -3754,6 +3904,27 @@ function setupCommand() {
3754
3904
  }
3755
3905
  }
3756
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
+ }
3757
3928
  let hubContent;
3758
3929
  let subSkillContents;
3759
3930
  try {
@@ -3859,6 +4030,30 @@ function setupCommand() {
3859
4030
  );
3860
4031
  }
3861
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
+ }
3862
4057
  }
3863
4058
  });
3864
4059
  }
@@ -4668,6 +4863,7 @@ function handleError(err) {
4668
4863
  printHuman(c.red(`\u2717 ${err.message}`));
4669
4864
  for (const line of hint.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
4670
4865
  }
4866
+ void reportError("network_unreachable", err);
4671
4867
  process.exit(err.exitCode());
4672
4868
  }
4673
4869
  if (err instanceof Error) {
@@ -4687,6 +4883,7 @@ function handleError(err) {
4687
4883
  for (const line of hintStr.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
4688
4884
  }
4689
4885
  }
4886
+ void reportError("unhandled_error", err);
4690
4887
  process.exit(1);
4691
4888
  }
4692
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.7",
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",