@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.
@@ -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, or star/unstar a package. 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>".
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, & Stars
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 stars.
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 does NOT yet expose follow, listing comments, replies, editing or
133
- moderating comments, or notifications. If the user asks for any of those, tell
134
- them truthfully that the CLI verb is not implemented yet — do NOT fabricate a
135
- command. For now direct them to `https://kordis.store`.
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
- try {
997
- const init = {
998
- method: opts.method ?? "GET",
999
- headers,
1000
- cache: "no-store"
1001
- };
1002
- if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
1003
- res = await this.fetchImpl(url, init);
1004
- } catch (err) {
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}: ${err.message}`,
1007
- err
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
- throw new KordisApiError(res.status, parsed, requestId);
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 Command19 } from "commander";
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 packages (requires login)").option("--limit <n>", "max results per page", (v) => parseInt(v, 10), 20).option("--page <n>", "page number (1-based)", (v) => parseInt(v, 10), 1).option("--sort <mode>", "sort: latest|popular", "latest").action(async (opts) => {
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
- requireAuth(bundle);
4287
- const spin = spinner("Fetching your packages");
4288
- const res = await bundle.authedRequest(
4289
- (api) => api.listMyPackages({ page: opts.page, limit: opts.limit, sort })
4290
- );
4291
- spin.succeed();
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.packages.length === 0) {
4296
- printHuman(c.yellow("(no packages yet \u2014 use `kordis push` to publish your first)"));
4462
+ if (res.results.length === 0) {
4463
+ printHuman(c.yellow(`(no public packages for ${username})`));
4297
4464
  return;
4298
4465
  }
4299
- printHuman(
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(`v${p.current_version}`)} ${c.cyan(`[${p.type}]`)}`
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.page * res.limit < res.total) {
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 Command19("kordis").version(VERSION).description("Kordis \u2014 registry for agentic-coding packages").option("--json", "emit JSON output to stdout (default: human)", false).hook("preAction", (thisCommand) => {
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",