@rayul/kordis 0.7.0 → 0.8.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/assets/kordis-search/SKILL.md +12 -1
- package/assets/kordis-social/SKILL.md +70 -7
- package/dist/index.js +366 -44
- package/package.json +1 -1
|
@@ -26,7 +26,8 @@ Keyword search; `--json` for parsing.
|
|
|
26
26
|
| `--trending` | list packages trending in `--window` (overrides `--sort`) |
|
|
27
27
|
| `--rising` | list rising packages by 7d acceleration (overrides `--sort`) |
|
|
28
28
|
| `--window <w>` | `24h` (default), `7d`, or `30d` — only honoured with `--trending` |
|
|
29
|
-
| `--limit <n>` | max results (default 20)
|
|
29
|
+
| `--limit <n>` | max results per page (default 20, max 100) |
|
|
30
|
+
| `--cursor <t>` | pagination — pass `next_cursor` from a prior response to fetch page 2+. 0.6.7+. |
|
|
30
31
|
|
|
31
32
|
### `kordis info <full_slug> --json`
|
|
32
33
|
Inspect metadata (title, description, type, tier, version, author, stars,
|
|
@@ -121,3 +122,13 @@ User: "how many packages has alice published?" / "alice가 publish한거 다 보
|
|
|
121
122
|
`total: null`); render "Showing N of M matches"
|
|
122
123
|
- do NOT post-process with jq for author filtering — use the flag
|
|
123
124
|
</example>
|
|
125
|
+
|
|
126
|
+
<example>
|
|
127
|
+
User: "show me page 2 of those results" / "다음 페이지 보여줘"
|
|
128
|
+
- response from page 1 carries `next_cursor` (opaque base64 token, or
|
|
129
|
+
`null` when no more pages)
|
|
130
|
+
- kordis search <q> --cursor <token> --json # 0.6.7+
|
|
131
|
+
- keep feeding `next_cursor` from each response until it comes back `null`
|
|
132
|
+
- DO NOT try `--offset` for cursor-paginated endpoints — CLI passes
|
|
133
|
+
`--cursor` only
|
|
134
|
+
</example>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kordis-social
|
|
3
|
-
description: Use whenever the user wants to log into Kordis, log out, check identity, post a typed comment on a Kordis package,
|
|
3
|
+
description: Use whenever the user wants to log into Kordis, log out, check identity, post a typed comment on a Kordis package, star/unstar a package, view a user's public profile, list their packages or stars, or follow/unfollow another user. Triggers include "log into Kordis", "kordis login", "who am I logged in as on Kordis", "log me out of Kordis", "leave a tip on <slug>", "report a bug on <slug>", "comment on <slug>", "star <slug>", "unstar <slug>", "favourite <slug>", "follow <username>", "unfollow <username>", "팔로우", "팔로워", "show me <username>'s profile", "프로필", "what has <username> starred", "who follows <username>", "who does <username> follow".
|
|
4
4
|
type: skill
|
|
5
5
|
allowed-tools: ["Bash(kordis:*)"]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Kordis: Identity, Comments, &
|
|
8
|
+
# Kordis: Identity, Comments, Stars, & Social
|
|
9
9
|
|
|
10
10
|
## Goal
|
|
11
11
|
Drive the user's login state and the social actions Kordis exposes — typed
|
|
12
|
-
comments and
|
|
12
|
+
comments, stars, public profiles, and follow/unfollow (0.8.0+).
|
|
13
13
|
|
|
14
14
|
## Identity commands
|
|
15
15
|
|
|
@@ -127,9 +127,72 @@ User: "unstar foo/bar"
|
|
|
127
127
|
- show updated count
|
|
128
128
|
</example>
|
|
129
129
|
|
|
130
|
+
## Profile & follow commands (0.8.0+)
|
|
131
|
+
|
|
132
|
+
### `kordis profile <username>`
|
|
133
|
+
Anonymous-OK. Shows display name, bio, follower count, package count,
|
|
134
|
+
total stars, total downloads, first/last published timestamps.
|
|
135
|
+
|
|
136
|
+
### `kordis list <username>` (extended)
|
|
137
|
+
With no argument → your own packages (auth required, offset pagination).
|
|
138
|
+
With a username → public packages of that user (anonymous-OK, cursor
|
|
139
|
+
pagination via `--cursor`).
|
|
140
|
+
|
|
141
|
+
### `kordis stars <username>`
|
|
142
|
+
Anonymous-OK. Lists packages this user has starred. Cursor-paginated.
|
|
143
|
+
|
|
144
|
+
### `kordis follow <username>` / `kordis unfollow <username>`
|
|
145
|
+
Auth required. Idempotent (re-following is a no-op, no double-count).
|
|
146
|
+
Self-follow is rejected with `usage_error`. Response carries the
|
|
147
|
+
post-mutation `follower_count` so you don't need a separate GET.
|
|
148
|
+
|
|
149
|
+
### `kordis followers <username>` / `kordis following <username>`
|
|
150
|
+
Anonymous-OK, cursor-paginated. Returns `PublicUser` rows.
|
|
151
|
+
|
|
152
|
+
<example>
|
|
153
|
+
User: "show me alice's profile"
|
|
154
|
+
- kordis profile alice --json
|
|
155
|
+
- render display_name, bio, package_count, follower_count, joined date
|
|
156
|
+
</example>
|
|
157
|
+
|
|
158
|
+
<example>
|
|
159
|
+
User: "follow alice"
|
|
160
|
+
- ALWAYS check the user is logged in first (`kordis whoami --json`); if
|
|
161
|
+
not authenticated, prompt them to run `kordis login --emit-only`
|
|
162
|
+
- kordis follow alice --json
|
|
163
|
+
- report the updated follower_count from the response
|
|
164
|
+
</example>
|
|
165
|
+
|
|
166
|
+
<example>
|
|
167
|
+
User: "what has bob starred?" / "bob이 별 누른거 보여줘"
|
|
168
|
+
- kordis stars bob --json
|
|
169
|
+
- render top results; offer `--cursor` for the next page if `next_cursor` is non-null
|
|
170
|
+
</example>
|
|
171
|
+
|
|
172
|
+
<example>
|
|
173
|
+
User: "who follows alice?"
|
|
174
|
+
- kordis followers alice --json
|
|
175
|
+
- list users; for each, display username + display_name + follower_count
|
|
176
|
+
</example>
|
|
177
|
+
|
|
178
|
+
<example>
|
|
179
|
+
User: "i don't want to follow alice anymore"
|
|
180
|
+
- ALWAYS confirm the user intent before destructive social actions (unfollow IS reversible — re-following is one command — but a courtesy confirm avoids surprises)
|
|
181
|
+
- kordis unfollow alice --json
|
|
182
|
+
- report the new follower_count
|
|
183
|
+
</example>
|
|
184
|
+
|
|
185
|
+
<example>
|
|
186
|
+
User: "kordis list popular"
|
|
187
|
+
- this is a sort-keyword collision — `popular` is NOT a username
|
|
188
|
+
- the CLI catches this client-side and points the user at `--sort popular`
|
|
189
|
+
- show the error message verbatim; don't try to "fix" it by guessing
|
|
190
|
+
</example>
|
|
191
|
+
|
|
130
192
|
## Limitations
|
|
131
193
|
|
|
132
|
-
The CLI
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
194
|
+
The CLI now exposes follow/unfollow + profile + public list/stars
|
|
195
|
+
(0.8.0+). Still NOT exposed: listing comments per user (use `kordis
|
|
196
|
+
comments <full_slug>` for per-package), per-user blocklists,
|
|
197
|
+
notifications, friend recommendations. For those direct the user to
|
|
198
|
+
`https://kordis.store` or wait for the next minor.
|
package/dist/index.js
CHANGED
|
@@ -486,7 +486,7 @@ var init_auth = __esm({
|
|
|
486
486
|
|
|
487
487
|
// ../../packages/contracts/src/api.ts
|
|
488
488
|
import { z as z9 } from "zod";
|
|
489
|
-
var SearchQuerySchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, AssetSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageResponseSchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema;
|
|
489
|
+
var SearchQuerySchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, AssetSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageResponseSchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema, ListUserPackagesResponseSchema, ListUserStarsResponseSchema, FollowResponseSchema, ListFollowersQuerySchema, ListFollowersResponseSchema;
|
|
490
490
|
var init_api = __esm({
|
|
491
491
|
"../../packages/contracts/src/api.ts"() {
|
|
492
492
|
"use strict";
|
|
@@ -709,6 +709,26 @@ var init_api = __esm({
|
|
|
709
709
|
limit: z9.coerce.number().int().min(1).max(100).default(20),
|
|
710
710
|
cursor: CursorSchema.optional()
|
|
711
711
|
});
|
|
712
|
+
ListUserPackagesResponseSchema = z9.object({
|
|
713
|
+
results: PackageSummarySchema.array(),
|
|
714
|
+
next_cursor: z9.string().nullable()
|
|
715
|
+
});
|
|
716
|
+
ListUserStarsResponseSchema = z9.object({
|
|
717
|
+
results: PackageSummarySchema.array(),
|
|
718
|
+
next_cursor: z9.string().nullable()
|
|
719
|
+
});
|
|
720
|
+
FollowResponseSchema = z9.object({
|
|
721
|
+
following: z9.boolean(),
|
|
722
|
+
follower_count: z9.number().int().nonnegative()
|
|
723
|
+
});
|
|
724
|
+
ListFollowersQuerySchema = z9.object({
|
|
725
|
+
limit: z9.coerce.number().int().min(1).max(100).default(50),
|
|
726
|
+
cursor: CursorSchema.optional()
|
|
727
|
+
});
|
|
728
|
+
ListFollowersResponseSchema = z9.object({
|
|
729
|
+
results: PublicUserSchema.array(),
|
|
730
|
+
next_cursor: z9.string().nullable()
|
|
731
|
+
});
|
|
712
732
|
}
|
|
713
733
|
});
|
|
714
734
|
|
|
@@ -912,6 +932,34 @@ var init_src = __esm({
|
|
|
912
932
|
});
|
|
913
933
|
|
|
914
934
|
// ../../packages/api-client/src/errors.ts
|
|
935
|
+
function httpStatusToCode(status) {
|
|
936
|
+
if (status === 401) return "not_authenticated";
|
|
937
|
+
if (status === 403) return "not_authorized";
|
|
938
|
+
if (status === 404) return "not_found";
|
|
939
|
+
if (status === 409) return "conflict";
|
|
940
|
+
if (status === 429) return "rate_limited";
|
|
941
|
+
if (status >= 500) return "internal_error";
|
|
942
|
+
if (status >= 400) return "usage_error";
|
|
943
|
+
return "generic_failure";
|
|
944
|
+
}
|
|
945
|
+
function normalizeErrorEnvelope(status, parsed, fallbackText) {
|
|
946
|
+
const isObject = (v) => typeof v === "object" && v !== null;
|
|
947
|
+
if (isObject(parsed) && isObject(parsed.error) && typeof parsed.error.code === "string" && typeof parsed.error.message === "string") {
|
|
948
|
+
return parsed;
|
|
949
|
+
}
|
|
950
|
+
let message;
|
|
951
|
+
if (isObject(parsed) && typeof parsed.message === "string") {
|
|
952
|
+
message = parsed.message;
|
|
953
|
+
} else if (fallbackText) {
|
|
954
|
+
message = fallbackText.slice(0, 500);
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
error: {
|
|
958
|
+
code: httpStatusToCode(status),
|
|
959
|
+
message: message ?? `HTTP ${status}`
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
915
963
|
var KordisApiError, KordisNetworkError;
|
|
916
964
|
var init_errors2 = __esm({
|
|
917
965
|
"../../packages/api-client/src/errors.ts"() {
|
|
@@ -993,26 +1041,42 @@ var init_client = __esm({
|
|
|
993
1041
|
};
|
|
994
1042
|
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
995
1043
|
let res;
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1044
|
+
const init = {
|
|
1045
|
+
method: opts.method ?? "GET",
|
|
1046
|
+
headers,
|
|
1047
|
+
cache: "no-store"
|
|
1048
|
+
};
|
|
1049
|
+
if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
|
|
1050
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
1051
|
+
const retryable = method === "GET";
|
|
1052
|
+
let lastErr;
|
|
1053
|
+
for (let attempt = 0; attempt < (retryable ? 2 : 1); attempt++) {
|
|
1054
|
+
try {
|
|
1055
|
+
res = await this.fetchImpl(url, init);
|
|
1056
|
+
lastErr = void 0;
|
|
1057
|
+
break;
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
lastErr = err;
|
|
1060
|
+
if (attempt === 0 && retryable) {
|
|
1061
|
+
await new Promise((resolve2) => setTimeout(resolve2, 150));
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (lastErr !== void 0) {
|
|
1005
1067
|
throw new KordisNetworkError(
|
|
1006
|
-
`cannot reach ${url.hostname}: ${
|
|
1007
|
-
|
|
1068
|
+
`cannot reach ${url.hostname}: ${lastErr.message}`,
|
|
1069
|
+
lastErr
|
|
1008
1070
|
);
|
|
1009
1071
|
}
|
|
1072
|
+
res = res;
|
|
1010
1073
|
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
1011
1074
|
if (res.status === 204) return void 0;
|
|
1012
1075
|
const text = await res.text();
|
|
1013
1076
|
const parsed = text ? JSON.parse(text) : void 0;
|
|
1014
1077
|
if (!res.ok) {
|
|
1015
|
-
|
|
1078
|
+
const envelope = normalizeErrorEnvelope(res.status, parsed, text);
|
|
1079
|
+
throw new KordisApiError(res.status, envelope, requestId);
|
|
1016
1080
|
}
|
|
1017
1081
|
return parsed;
|
|
1018
1082
|
}
|
|
@@ -1218,6 +1282,51 @@ var init_client = __esm({
|
|
|
1218
1282
|
body: payload
|
|
1219
1283
|
});
|
|
1220
1284
|
}
|
|
1285
|
+
// ─── social (0.8.0+) ───────────────────────────────────────────────────
|
|
1286
|
+
/** Public profile + aggregated stats. Anonymous-OK. */
|
|
1287
|
+
getUserProfile(username) {
|
|
1288
|
+
return this.request(`/api/users/${encodeURIComponent(username)}`);
|
|
1289
|
+
}
|
|
1290
|
+
/** Cursor-paginated public package list authored by `username`. */
|
|
1291
|
+
listUserPackages(username, query = {}) {
|
|
1292
|
+
return this.request(
|
|
1293
|
+
`/api/users/${encodeURIComponent(username)}/packages`,
|
|
1294
|
+
{ query }
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
/** Cursor-paginated packages that `username` has starred. */
|
|
1298
|
+
listUserStars(username, query = {}) {
|
|
1299
|
+
return this.request(
|
|
1300
|
+
`/api/users/${encodeURIComponent(username)}/stars`,
|
|
1301
|
+
{ query }
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
/** Auth-required. Idempotent (re-follow is a no-op). Returns post-mutation counter. */
|
|
1305
|
+
followUser(username) {
|
|
1306
|
+
return this.request(`/api/users/${encodeURIComponent(username)}/follow`, {
|
|
1307
|
+
method: "POST"
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
/** Auth-required. Idempotent (unfollow of non-edge is a no-op). */
|
|
1311
|
+
unfollowUser(username) {
|
|
1312
|
+
return this.request(`/api/users/${encodeURIComponent(username)}/follow`, {
|
|
1313
|
+
method: "DELETE"
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
/** Cursor-paginated followers of `username`. Anonymous-OK. */
|
|
1317
|
+
listUserFollowers(username, query = {}) {
|
|
1318
|
+
return this.request(
|
|
1319
|
+
`/api/users/${encodeURIComponent(username)}/followers`,
|
|
1320
|
+
{ query }
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
/** Cursor-paginated users that `username` follows. Anonymous-OK. */
|
|
1324
|
+
listUserFollowing(username, query = {}) {
|
|
1325
|
+
return this.request(
|
|
1326
|
+
`/api/users/${encodeURIComponent(username)}/following`,
|
|
1327
|
+
{ query }
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1221
1330
|
};
|
|
1222
1331
|
}
|
|
1223
1332
|
});
|
|
@@ -1227,7 +1336,8 @@ var src_exports = {};
|
|
|
1227
1336
|
__export(src_exports, {
|
|
1228
1337
|
KordisApiError: () => KordisApiError,
|
|
1229
1338
|
KordisClient: () => KordisClient,
|
|
1230
|
-
KordisNetworkError: () => KordisNetworkError
|
|
1339
|
+
KordisNetworkError: () => KordisNetworkError,
|
|
1340
|
+
normalizeErrorEnvelope: () => normalizeErrorEnvelope
|
|
1231
1341
|
});
|
|
1232
1342
|
var init_src2 = __esm({
|
|
1233
1343
|
"../../packages/api-client/src/index.ts"() {
|
|
@@ -1239,7 +1349,7 @@ var init_src2 = __esm({
|
|
|
1239
1349
|
|
|
1240
1350
|
// src/index.ts
|
|
1241
1351
|
init_src2();
|
|
1242
|
-
import { Command as
|
|
1352
|
+
import { Command as Command23 } from "commander";
|
|
1243
1353
|
|
|
1244
1354
|
// src/commands/login.ts
|
|
1245
1355
|
import { Command } from "commander";
|
|
@@ -1774,7 +1884,7 @@ async function useClient() {
|
|
|
1774
1884
|
username = null;
|
|
1775
1885
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1776
1886
|
process.stderr.write(
|
|
1777
|
-
`[kordis] auto-finalize: server issued tokens but local persistence failed (${msg}). Run \`kordis login --emit-only\` again to retry.
|
|
1887
|
+
`[kordis] auto-finalize: server issued tokens but local persistence failed (${msg}). Run \`kordis login --emit-only\` again to retry. If this keeps happening on macOS, the OS keychain prompt may be timing out \u2014 set \`KORDIS_NO_KEYCHAIN=1\` to fall back to a plaintext file at ~/.kordis/.tokens.json (mode 0600). Set \`KORDIS_DEBUG=1\` for full auto-finalize stage output.
|
|
1778
1888
|
`
|
|
1779
1889
|
);
|
|
1780
1890
|
void reportError("auto_finalize_stage2_fail", err);
|
|
@@ -1968,7 +2078,7 @@ function requireAuth(bundle) {
|
|
|
1968
2078
|
error: {
|
|
1969
2079
|
code: "not_authenticated",
|
|
1970
2080
|
message: "authentication required",
|
|
1971
|
-
hint: "Run `kordis login --emit-only` to start browser sign-in (exits immediately, no blocking poll), then re-run this command \u2014 the next CLI call auto-claims the token. For interactive shells, plain `kordis login` (10-min poll) also works."
|
|
2081
|
+
hint: "Run `kordis login --emit-only` to start browser sign-in (exits immediately, no blocking poll), then re-run this command \u2014 the next CLI call auto-claims the token. For interactive shells, plain `kordis login` (10-min poll) also works. If you DID login but auth keeps failing, run with `KORDIS_DEBUG=1` to surface auto-finalize stages (Round 12 F1 \u2014 diagnostic moment-of-need)."
|
|
1972
2082
|
}
|
|
1973
2083
|
});
|
|
1974
2084
|
}
|
|
@@ -4279,44 +4389,91 @@ function deleteCommand() {
|
|
|
4279
4389
|
|
|
4280
4390
|
// src/commands/list.ts
|
|
4281
4391
|
import { Command as Command13 } from "commander";
|
|
4392
|
+
|
|
4393
|
+
// src/lib/parse-username.ts
|
|
4394
|
+
init_src();
|
|
4395
|
+
var SORT_KEYWORDS = ["latest", "popular", "recent", "updated", "stars", "downloads"];
|
|
4396
|
+
function parseUsername(raw) {
|
|
4397
|
+
if (SORT_KEYWORDS.includes(raw.toLowerCase())) {
|
|
4398
|
+
throw new Error(
|
|
4399
|
+
`"${raw}" is a sort keyword \u2014 did you mean \`--sort ${raw}\`? Username arguments must follow the Kordis username shape (lowercase alphanumeric, hyphens allowed).`
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
4402
|
+
const parsed = UsernameSchema.safeParse(raw);
|
|
4403
|
+
if (!parsed.success) {
|
|
4404
|
+
throw new Error(
|
|
4405
|
+
`invalid username "${raw}" \u2014 must be 3-32 chars, lowercase alphanumeric with single hyphens (e.g. "alice", "my-team")`
|
|
4406
|
+
);
|
|
4407
|
+
}
|
|
4408
|
+
return parsed.data;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
// src/commands/list.ts
|
|
4282
4412
|
function listCommand() {
|
|
4283
|
-
return new Command13("list").description("list your own
|
|
4284
|
-
const sort = opts.sort === "popular" ? "popular" : "latest";
|
|
4413
|
+
return new Command13("list").description("list packages \u2014 your own (no arg) or a public user\u2019s (with <username>)").argument("[username]", "kordis username to list (omit for self)").option("--limit <n>", "max results per page", (v) => parseInt(v, 10), 20).option("--page <n>", "self-mode page number (1-based)", (v) => parseInt(v, 10), 1).option("--cursor <token>", "public-mode pagination cursor").option("--sort <mode>", "sort: latest|popular (self) or stars|downloads|recent|updated (public)", "latest").action(async (rawUsername, opts) => {
|
|
4285
4414
|
const bundle = await useClient();
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4415
|
+
if (rawUsername === void 0) {
|
|
4416
|
+
requireAuth(bundle);
|
|
4417
|
+
const sort = opts.sort === "popular" ? "popular" : "latest";
|
|
4418
|
+
const spin = spinner("Fetching your packages");
|
|
4419
|
+
const res2 = await bundle.authedRequest(
|
|
4420
|
+
(api) => api.listMyPackages({ page: opts.page, limit: opts.limit, sort })
|
|
4421
|
+
);
|
|
4422
|
+
spin.succeed();
|
|
4423
|
+
printDual({
|
|
4424
|
+
json: res2,
|
|
4425
|
+
human: () => {
|
|
4426
|
+
if (res2.packages.length === 0) {
|
|
4427
|
+
printHuman(c.yellow("(no packages yet \u2014 use `kordis push` to publish your first)"));
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
printHuman(
|
|
4431
|
+
c.gray(
|
|
4432
|
+
`Showing ${res2.packages.length} of ${res2.total} package${res2.total !== 1 ? "s" : ""} (page ${res2.page})`
|
|
4433
|
+
)
|
|
4434
|
+
);
|
|
4435
|
+
printHuman("");
|
|
4436
|
+
for (const p of res2.packages) {
|
|
4437
|
+
const desc = p.description.length > 60 ? p.description.slice(0, 57) + "..." : p.description;
|
|
4438
|
+
printHuman(
|
|
4439
|
+
`${c.bold(p.full_slug)} ${c.gray(`v${p.current_version}`)} ${c.cyan(`[${p.type}]`)}`
|
|
4440
|
+
);
|
|
4441
|
+
printHuman(` ${desc}`);
|
|
4442
|
+
printHuman(c.gray(` \u2605 ${p.stars} \u21E3 ${p.downloads} ${p.visibility}`));
|
|
4443
|
+
printHuman("");
|
|
4444
|
+
}
|
|
4445
|
+
if (res2.page * res2.limit < res2.total) {
|
|
4446
|
+
printHuman(c.gray(`(more available \u2014 pass --page ${res2.page + 1})`));
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
return;
|
|
4451
|
+
}
|
|
4452
|
+
const username = parseUsername(rawUsername);
|
|
4453
|
+
const publicSort = ["stars", "downloads", "recent", "updated"].includes(opts.sort) ? opts.sort : "stars";
|
|
4454
|
+
const res = await bundle.api.listUserPackages(username, {
|
|
4455
|
+
limit: opts.limit,
|
|
4456
|
+
sort: publicSort,
|
|
4457
|
+
...opts.cursor ? { cursor: opts.cursor } : {}
|
|
4458
|
+
});
|
|
4292
4459
|
printDual({
|
|
4293
4460
|
json: res,
|
|
4294
4461
|
human: () => {
|
|
4295
|
-
if (res.
|
|
4296
|
-
printHuman(c.yellow(
|
|
4462
|
+
if (res.results.length === 0) {
|
|
4463
|
+
printHuman(c.yellow(`(no public packages for ${username})`));
|
|
4297
4464
|
return;
|
|
4298
4465
|
}
|
|
4299
|
-
|
|
4300
|
-
c.gray(
|
|
4301
|
-
`Showing ${res.packages.length} of ${res.total} package${res.total !== 1 ? "s" : ""} (page ${res.page})`
|
|
4302
|
-
)
|
|
4303
|
-
);
|
|
4304
|
-
printHuman("");
|
|
4305
|
-
for (const p of res.packages) {
|
|
4466
|
+
for (const p of res.results) {
|
|
4306
4467
|
const desc = p.description.length > 60 ? p.description.slice(0, 57) + "..." : p.description;
|
|
4307
4468
|
printHuman(
|
|
4308
|
-
`${c.bold(p.full_slug)} ${c.gray(
|
|
4469
|
+
`${c.bold(p.full_slug)} ${c.gray("v" + p.current_version)} ${c.cyan("[" + p.type + "]")}`
|
|
4309
4470
|
);
|
|
4310
4471
|
printHuman(` ${desc}`);
|
|
4311
|
-
printHuman(
|
|
4312
|
-
c.gray(` \u2605 ${p.stars} \u21E3 ${p.downloads} ${p.visibility}`)
|
|
4313
|
-
);
|
|
4472
|
+
printHuman(c.gray(` \u2605 ${p.stars} \u21E3 ${p.downloads}`));
|
|
4314
4473
|
printHuman("");
|
|
4315
4474
|
}
|
|
4316
|
-
if (res.
|
|
4317
|
-
printHuman(
|
|
4318
|
-
c.gray(`(more available \u2014 pass --page ${res.page + 1})`)
|
|
4319
|
-
);
|
|
4475
|
+
if (res.next_cursor) {
|
|
4476
|
+
printHuman(c.gray(`(more available \u2014 kordis list ${username} --cursor ${res.next_cursor})`));
|
|
4320
4477
|
}
|
|
4321
4478
|
}
|
|
4322
4479
|
});
|
|
@@ -4778,12 +4935,171 @@ function renderItem(idx, it, verbose) {
|
|
|
4778
4935
|
printHuman("");
|
|
4779
4936
|
}
|
|
4780
4937
|
|
|
4938
|
+
// src/commands/profile.ts
|
|
4939
|
+
import { Command as Command19 } from "commander";
|
|
4940
|
+
function profileCommand() {
|
|
4941
|
+
return new Command19("profile").description("inspect a user\u2019s public profile").argument("<username>", "kordis username").action(async (raw) => {
|
|
4942
|
+
const username = parseUsername(raw);
|
|
4943
|
+
const bundle = await useClient();
|
|
4944
|
+
const profile = await bundle.api.getUserProfile(username);
|
|
4945
|
+
printDual({
|
|
4946
|
+
json: profile,
|
|
4947
|
+
human: () => {
|
|
4948
|
+
const verified = profile.is_verified ? c.cyan(" \u2713") : "";
|
|
4949
|
+
const displayLine = profile.display_name ? `${c.bold(profile.username)}${verified} ${c.gray("(" + profile.display_name + ")")}` : `${c.bold(profile.username)}${verified}`;
|
|
4950
|
+
printHuman(displayLine);
|
|
4951
|
+
if (profile.bio) printHuman(c.gray(profile.bio));
|
|
4952
|
+
printHuman("");
|
|
4953
|
+
printHuman(
|
|
4954
|
+
` ${c.bold(String(profile.stats.package_count))} packages \xB7 ${c.bold(String(profile.stats.total_stars))} \u2605 across all \xB7 ${c.bold(String(profile.stats.total_downloads))} downloads`
|
|
4955
|
+
);
|
|
4956
|
+
printHuman(
|
|
4957
|
+
` ${c.bold(String(profile.follower_count))} follower${profile.follower_count === 1 ? "" : "s"} \xB7 joined ${profile.created_at.split("T")[0]}`
|
|
4958
|
+
);
|
|
4959
|
+
if (profile.stats.last_published_at) {
|
|
4960
|
+
printHuman(
|
|
4961
|
+
c.gray(
|
|
4962
|
+
` last published: ${profile.stats.last_published_at.split("T")[0]}`
|
|
4963
|
+
)
|
|
4964
|
+
);
|
|
4965
|
+
}
|
|
4966
|
+
printHuman("");
|
|
4967
|
+
printHuman(c.gray(` \u2192 list packages: kordis list ${profile.username}`));
|
|
4968
|
+
printHuman(c.gray(` \u2192 list stars: kordis stars ${profile.username}`));
|
|
4969
|
+
printHuman(c.gray(` \u2192 follow: kordis follow ${profile.username}`));
|
|
4970
|
+
}
|
|
4971
|
+
});
|
|
4972
|
+
});
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4975
|
+
// src/commands/stars.ts
|
|
4976
|
+
import { Command as Command20 } from "commander";
|
|
4977
|
+
function starsCommand() {
|
|
4978
|
+
return new Command20("stars").description("list packages a user has starred").argument("<username>", "kordis username").option("--limit <n>", "max results per page (1-100, default 50)", (v) => parseInt(v, 10), 50).option("--cursor <token>", "pagination cursor returned by a prior response").action(async (raw, opts) => {
|
|
4979
|
+
const username = parseUsername(raw);
|
|
4980
|
+
const bundle = await useClient();
|
|
4981
|
+
const res = await bundle.api.listUserStars(username, {
|
|
4982
|
+
limit: opts.limit,
|
|
4983
|
+
...opts.cursor ? { cursor: opts.cursor } : {}
|
|
4984
|
+
});
|
|
4985
|
+
printDual({
|
|
4986
|
+
json: res,
|
|
4987
|
+
human: () => {
|
|
4988
|
+
if (res.results.length === 0) {
|
|
4989
|
+
printHuman(c.yellow(`(no stars yet for ${username})`));
|
|
4990
|
+
printHuman(c.gray(` \u2192 they may not have starred any public packages, or have set stars to private`));
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
for (const p of res.results) {
|
|
4994
|
+
printHuman(
|
|
4995
|
+
`${c.bold(p.full_slug)} ${c.gray("v" + p.current_version)} ${c.cyan("[" + p.type + "]")}`
|
|
4996
|
+
);
|
|
4997
|
+
printHuman(` ${p.description}`);
|
|
4998
|
+
printHuman(c.gray(` \u2605 ${p.stars} \xB7 \u21E3 ${p.downloads}`));
|
|
4999
|
+
printHuman("");
|
|
5000
|
+
}
|
|
5001
|
+
if (res.next_cursor) {
|
|
5002
|
+
printHuman(c.gray(`(more available \u2014 kordis stars ${username} --cursor ${res.next_cursor})`));
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
});
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
// src/commands/follow.ts
|
|
5010
|
+
import { Command as Command21 } from "commander";
|
|
5011
|
+
function followCommand() {
|
|
5012
|
+
return new Command21("follow").description("follow a kordis user").argument("<username>", "kordis username").action(async (raw) => {
|
|
5013
|
+
const username = parseUsername(raw);
|
|
5014
|
+
const bundle = await useClient();
|
|
5015
|
+
requireAuth(bundle);
|
|
5016
|
+
const res = await bundle.authedRequest((api) => api.followUser(username));
|
|
5017
|
+
printDual({
|
|
5018
|
+
json: res,
|
|
5019
|
+
human: () => {
|
|
5020
|
+
printHuman(c.green(`\u2713 following ${username}`));
|
|
5021
|
+
printHuman(c.gray(` total followers: ${res.follower_count}`));
|
|
5022
|
+
}
|
|
5023
|
+
});
|
|
5024
|
+
});
|
|
5025
|
+
}
|
|
5026
|
+
function unfollowCommand() {
|
|
5027
|
+
return new Command21("unfollow").description("stop following a kordis user").argument("<username>", "kordis username").action(async (raw) => {
|
|
5028
|
+
const username = parseUsername(raw);
|
|
5029
|
+
const bundle = await useClient();
|
|
5030
|
+
requireAuth(bundle);
|
|
5031
|
+
const res = await bundle.authedRequest((api) => api.unfollowUser(username));
|
|
5032
|
+
printDual({
|
|
5033
|
+
json: res,
|
|
5034
|
+
human: () => {
|
|
5035
|
+
printHuman(c.gray(`\u2717 unfollowed ${username}`));
|
|
5036
|
+
printHuman(c.gray(` total followers: ${res.follower_count}`));
|
|
5037
|
+
}
|
|
5038
|
+
});
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
// src/commands/followers.ts
|
|
5043
|
+
import { Command as Command22 } from "commander";
|
|
5044
|
+
function makeListing(name) {
|
|
5045
|
+
return new Command22(name).description(
|
|
5046
|
+
name === "followers" ? "list users following <username>" : "list users that <username> follows"
|
|
5047
|
+
).argument("<username>", "kordis username").option("--limit <n>", "max results per page (1-100, default 50)", (v) => parseInt(v, 10), 50).option("--cursor <token>", "pagination cursor returned by a prior response").action(async (raw, opts) => {
|
|
5048
|
+
const username = parseUsername(raw);
|
|
5049
|
+
const bundle = await useClient();
|
|
5050
|
+
const fn = name === "followers" ? bundle.api.listUserFollowers.bind(bundle.api) : bundle.api.listUserFollowing.bind(bundle.api);
|
|
5051
|
+
const res = await fn(username, {
|
|
5052
|
+
limit: opts.limit,
|
|
5053
|
+
...opts.cursor ? { cursor: opts.cursor } : {}
|
|
5054
|
+
});
|
|
5055
|
+
printDual({
|
|
5056
|
+
json: res,
|
|
5057
|
+
human: () => {
|
|
5058
|
+
if (res.results.length === 0) {
|
|
5059
|
+
const noun = name === "followers" ? "followers yet" : "one yet";
|
|
5060
|
+
printHuman(c.yellow(`(no ${noun} for ${username})`));
|
|
5061
|
+
return;
|
|
5062
|
+
}
|
|
5063
|
+
for (const u of res.results) {
|
|
5064
|
+
const verified = u.is_verified ? c.cyan(" \u2713") : "";
|
|
5065
|
+
const display = u.display_name ? ` ${c.gray("(" + u.display_name + ")")}` : "";
|
|
5066
|
+
printHuman(`${c.bold(u.username)}${verified}${display}`);
|
|
5067
|
+
printHuman(
|
|
5068
|
+
c.gray(
|
|
5069
|
+
` ${u.follower_count} follower${u.follower_count === 1 ? "" : "s"} \xB7 joined ${u.created_at.split("T")[0]}`
|
|
5070
|
+
)
|
|
5071
|
+
);
|
|
5072
|
+
printHuman("");
|
|
5073
|
+
}
|
|
5074
|
+
if (res.next_cursor) {
|
|
5075
|
+
printHuman(c.gray(`(more available \u2014 kordis ${name} ${username} --cursor ${res.next_cursor})`));
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
});
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
5081
|
+
function followersCommand() {
|
|
5082
|
+
return makeListing("followers");
|
|
5083
|
+
}
|
|
5084
|
+
function followingCommand() {
|
|
5085
|
+
return makeListing("following");
|
|
5086
|
+
}
|
|
5087
|
+
|
|
4781
5088
|
// src/index.ts
|
|
4782
5089
|
function buildProgram() {
|
|
4783
|
-
const program = new
|
|
5090
|
+
const program = new Command23("kordis").version(VERSION).description("Kordis \u2014 registry for agentic-coding packages").option("--json", "emit JSON output to stdout (default: human)", false).hook("preAction", (thisCommand) => {
|
|
4784
5091
|
const opts = thisCommand.optsWithGlobals();
|
|
4785
5092
|
if (opts.json) setJsonMode(true);
|
|
4786
|
-
})
|
|
5093
|
+
}).addHelpText("after", `
|
|
5094
|
+
Environment variables (see README.md "Privacy & environment variables"):
|
|
5095
|
+
KORDIS_API_URL=<url> override registry endpoint (default: https://api.kordis.store)
|
|
5096
|
+
KORDIS_HOME=<dir> override config + token fallback dir (default: ~/.kordis)
|
|
5097
|
+
KORDIS_NO_CONTEXT=1 suppress personalization header even if opted in
|
|
5098
|
+
KORDIS_NO_TELEMETRY=1 suppress anonymized error telemetry even if opted in
|
|
5099
|
+
KORDIS_NO_KEYCHAIN=1 force plaintext token storage at ~/.kordis/.tokens.json
|
|
5100
|
+
(escape hatch for macOS keychain-prompt fatigue)
|
|
5101
|
+
KORDIS_DEBUG=1 surface auto-finalize stage outcomes to stderr (diagnostic)
|
|
5102
|
+
`);
|
|
4787
5103
|
program.addCommand(loginCommand());
|
|
4788
5104
|
program.addCommand(logoutCommand());
|
|
4789
5105
|
program.addCommand(whoamiCommand());
|
|
@@ -4797,6 +5113,12 @@ function buildProgram() {
|
|
|
4797
5113
|
program.addCommand(uncommentCommand());
|
|
4798
5114
|
program.addCommand(starCommand());
|
|
4799
5115
|
program.addCommand(unstarCommand());
|
|
5116
|
+
program.addCommand(profileCommand());
|
|
5117
|
+
program.addCommand(starsCommand());
|
|
5118
|
+
program.addCommand(followCommand());
|
|
5119
|
+
program.addCommand(unfollowCommand());
|
|
5120
|
+
program.addCommand(followersCommand());
|
|
5121
|
+
program.addCommand(followingCommand());
|
|
4800
5122
|
program.addCommand(deleteCommand());
|
|
4801
5123
|
program.addCommand(listCommand());
|
|
4802
5124
|
program.addCommand(versionsCommand());
|