@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 +19 -0
- package/assets/kordis-using/SKILL.md +40 -0
- package/dist/index.js +455 -123
- package/package.json +1 -1
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
|
|
1524
|
-
import { join as
|
|
1684
|
+
import { homedir as homedir5 } from "os";
|
|
1685
|
+
import { join as join5 } from "path";
|
|
1525
1686
|
function dir() {
|
|
1526
|
-
return process.env["KORDIS_HOME"] ??
|
|
1687
|
+
return process.env["KORDIS_HOME"] ?? join5(homedir5(), ".kordis");
|
|
1527
1688
|
}
|
|
1528
1689
|
function path() {
|
|
1529
|
-
return
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
2115
|
-
var SkillFrontmatterSchema =
|
|
2116
|
-
name:
|
|
2117
|
-
description:
|
|
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:
|
|
2121
|
-
homepage:
|
|
2122
|
-
model:
|
|
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":
|
|
2125
|
-
"disable-model-invocation":
|
|
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:
|
|
2128
|
-
tags:
|
|
2129
|
-
tools:
|
|
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
|
|
2135
|
-
var McpTransportSchema =
|
|
2136
|
-
|
|
2137
|
-
type:
|
|
2138
|
-
command:
|
|
2139
|
-
args:
|
|
2140
|
-
env:
|
|
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
|
-
|
|
2143
|
-
type:
|
|
2144
|
-
url:
|
|
2145
|
-
headers:
|
|
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 =
|
|
2149
|
-
tools:
|
|
2150
|
-
resources:
|
|
2151
|
-
prompts:
|
|
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 =
|
|
2154
|
-
name:
|
|
2155
|
-
description:
|
|
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:
|
|
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:
|
|
2346
|
+
auth_notes: z15.string().optional(),
|
|
2166
2347
|
/** Required env vars with description + whether they must be secret. */
|
|
2167
|
-
env_schema:
|
|
2168
|
-
|
|
2169
|
-
description:
|
|
2170
|
-
required:
|
|
2171
|
-
secret:
|
|
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:
|
|
2175
|
-
repository:
|
|
2176
|
-
license:
|
|
2177
|
-
title:
|
|
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
|
|
2183
|
-
var ProjectRulesFrontmatterSchema =
|
|
2184
|
-
title:
|
|
2185
|
-
description:
|
|
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:
|
|
2188
|
-
target_path:
|
|
2189
|
-
globs:
|
|
2190
|
-
target_tools:
|
|
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:
|
|
2193
|
-
license:
|
|
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
|
|
2205
|
-
var CustomAgentFrontmatterSchema =
|
|
2206
|
-
name:
|
|
2207
|
-
description:
|
|
2208
|
-
title:
|
|
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:
|
|
2392
|
+
system_prompt: z17.string().min(1).max(5e4),
|
|
2212
2393
|
/** Optional model override — string passed verbatim to the host. */
|
|
2213
|
-
model:
|
|
2394
|
+
model: z17.string().optional(),
|
|
2214
2395
|
/** Tools this agent can call. Host-specific names (no uniform namespace). */
|
|
2215
|
-
allowed_tools:
|
|
2216
|
-
disallowed_tools:
|
|
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:
|
|
2399
|
+
context: z17.enum(["inherit", "fork", "isolated"]).optional(),
|
|
2219
2400
|
/** Known-good tool runtimes. Empty = untested everywhere. */
|
|
2220
|
-
tool_compat:
|
|
2401
|
+
tool_compat: z17.array(ToolSlugSchema).optional().default([]),
|
|
2221
2402
|
/** Pre-load skills when this agent activates. */
|
|
2222
|
-
preload_skills:
|
|
2223
|
-
license:
|
|
2224
|
-
homepage:
|
|
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
|
|
2230
|
-
var HookEventSchema =
|
|
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 =
|
|
2243
|
-
|
|
2244
|
-
type:
|
|
2245
|
-
command:
|
|
2246
|
-
timeout_ms:
|
|
2247
|
-
blocking:
|
|
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
|
-
|
|
2250
|
-
type:
|
|
2251
|
-
url:
|
|
2252
|
-
method:
|
|
2253
|
-
headers:
|
|
2254
|
-
timeout_ms:
|
|
2255
|
-
blocking:
|
|
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 =
|
|
2439
|
+
var HookBindingSchema = z18.object({
|
|
2259
2440
|
event: HookEventSchema,
|
|
2260
|
-
matcher:
|
|
2441
|
+
matcher: z18.string().optional(),
|
|
2261
2442
|
action: HookActionSchema
|
|
2262
2443
|
});
|
|
2263
|
-
var HookFrontmatterSchema =
|
|
2264
|
-
name:
|
|
2265
|
-
description:
|
|
2266
|
-
title:
|
|
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:
|
|
2450
|
+
hooks: z18.array(HookBindingSchema).min(1),
|
|
2270
2451
|
/** Declared to surface in search filters — always "claude-code" for now. */
|
|
2271
|
-
tool_compat:
|
|
2272
|
-
license:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
3316
|
-
await writeFile5(
|
|
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
|
|
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 =
|
|
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) } });
|