@rayul/kordis 0.6.2 → 0.6.4

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.
@@ -45,17 +45,24 @@ re-ranking ONLY — never persisted (see CLAUDE.md "Telemetry / privacy").
45
45
 
46
46
  ## Personalization opt-in
47
47
 
48
- The user is asked ONCE — at first interactive `kordis setup` OR (if
49
- postinstall ran in `--silent` mode) at first `kordis recommend`. Answer
50
- persists in `~/.kordis/config.json` (`personalization: 'on' | 'off'`).
48
+ The user is asked ONCE. Three paths persist the answer to
49
+ `~/.kordis/config.json` as `personalization: 'on' | 'off'`:
51
50
 
52
- Override at runtime:
51
+ 1. **Interactive `kordis setup`** in a real TTY — Y/n prompt.
52
+ 2. **First `kordis recommend`** in a TTY (lazy fallback if postinstall
53
+ ran `--silent`).
54
+ 3. **`kordis setup --personalization on|off`** — non-interactive flag
55
+ (0.6.3+). The **agent-facing path**: works without a TTY, no prompt.
56
+ Use this when the user has confirmed consent in chat — see Step 2.5.
57
+
58
+ Override at runtime (does not change the persisted answer):
53
59
  - `KORDIS_NO_CONTEXT=1` env — no header for the entire shell session
54
60
  - `--no-context` flag — no header for this single call
55
61
 
56
62
  If the response field `personalized` is `false`, the server returned
57
- popularity-only results — surface this to the user so they know the
58
- recommendations aren't tailored.
63
+ popularity-only results — almost always because the user hasn't been
64
+ asked yet (agent contexts have no TTY, so paths 1 and 2 never fire).
65
+ Surface this AND ask the user before flipping the flag (Step 2.5).
59
66
 
60
67
  ## Menu envelope (one item)
61
68
 
@@ -92,8 +99,7 @@ explicitly asked you not to share their stack.)
92
99
 
93
100
  ### 2. Triage
94
101
  Filter mentally by `confidence`. If everything is `low` and `personalized`
95
- is `false`, tell the user personalization is off and they can enable it
96
- via `kordis setup` (re-asks the prompt on next interactive run).
102
+ is `false`, follow Step 2.5 to offer the user a one-click consent path.
97
103
 
98
104
  If the response carries `capped_by_author > 0`, ALSO surface a one-line
99
105
  note to the user — e.g. "N more candidates from the same publisher were
@@ -101,6 +107,30 @@ skipped by the diversity cap" — so they understand why the list is
101
107
  shorter than the requested `--limit`. Without this hint users frequently
102
108
  mistake a short menu for a sparse or broken registry.
103
109
 
110
+ ### 2.5. Offer personalization consent (when `personalized: false`)
111
+
112
+ If the response is `personalized: false` and the user is engaged in chat
113
+ (i.e., you can ask them a question), surface this naturally:
114
+
115
+ > "추천 결과가 popularity-only로 왔어요 (개인화 OFF). 익명 신호
116
+ > (설치된 에이전트 툴, 현재 디렉토리의 언어/프레임워크 힌트)로
117
+ > 추천을 맞춤화할 수 있는데 켜드릴까요? 서버는 신호를 저장하지
118
+ > 않고 응답 re-ranking에만 사용해요."
119
+
120
+ ONLY after the user explicitly says yes, run:
121
+
122
+ ```
123
+ kordis setup --personalization on
124
+ ```
125
+
126
+ Then re-run `kordis recommend --limit 10 --json` to get the personalized
127
+ menu. **Never run `--personalization on` without the user's confirmation
128
+ in chat** — it persists a consent flag, which is the user's choice.
129
+
130
+ If the user says no (or doesn't reply), don't push it — record opt-out
131
+ optionally via `kordis setup --personalization off` so the question
132
+ isn't asked again.
133
+
104
134
  ### 3. Present
105
135
  Surface 3-5 picks max. For each, show: full_slug, one-line `why_recommended`,
106
136
  confidence wording per the table above, and the next-step
@@ -127,7 +157,9 @@ User: "any new tools I should try?"
127
157
  <example>
128
158
  User: "추천 좀 해봐"
129
159
  1. kordis recommend --limit 10 --json
130
- 2. If personalized is false, tell the user briefly + suggest `kordis setup`
160
+ 2. If `personalized: false`, ask in chat (Step 2.5): "개인화 켜드릴까요? ..."
161
+ - if user says yes → `kordis setup --personalization on`, then re-run
162
+ - if user says no → surface popularity-only picks as-is
131
163
  3. Surface 3-5 in Korean using the why_recommended verbatim (English is fine
132
164
  — re-render the user-facing prose in Korean as needed)
133
165
  </example>
package/dist/index.js CHANGED
@@ -224,13 +224,13 @@ var init_user = __esm({
224
224
  bio: z5.string().max(500).nullable(),
225
225
  avatar_url: UrlSchema.nullable(),
226
226
  is_verified: z5.boolean(),
227
- package_count: z5.number().int().nonnegative(),
228
227
  follower_count: z5.number().int().nonnegative(),
229
228
  created_at: IsoDateTimeSchema
230
229
  });
231
230
  SelfUserSchema = PublicUserSchema.extend({
232
231
  primary_provider: OAuthProviderSchema,
233
232
  role: UserRoleSchema,
233
+ package_count: z5.number().int().nonnegative(),
234
234
  following_count: z5.number().int().nonnegative(),
235
235
  last_active_at: IsoDateTimeSchema.nullable()
236
236
  });
@@ -386,7 +386,15 @@ var init_comment = __esm({
386
386
  is_edited: z7.boolean(),
387
387
  edited_at: IsoDateTimeSchema.nullable(),
388
388
  is_deleted: z7.boolean(),
389
- created_at: IsoDateTimeSchema
389
+ created_at: IsoDateTimeSchema,
390
+ /**
391
+ * Web permalink (`https://kordis.store/p/<full_slug>#comment-<id>`). Server
392
+ * populates whenever it has the package slug at serialization time —
393
+ * always on create + list paths; absent on update/status paths that don't
394
+ * carry the slug. Old API responses (pre-0.6.4) omit it; new clients use
395
+ * it directly when present (e.g., "✓ comment posted — view: <permalink>").
396
+ */
397
+ permalink: z7.string().url().optional()
390
398
  });
391
399
  CommentTreeSchema = CommentSchema.extend({
392
400
  replies: z7.lazy(() => z7.array(CommentTreeSchema))
@@ -1887,7 +1895,7 @@ var SORT_MODES = [
1887
1895
  ];
1888
1896
  var TIME_WINDOWS = ["24h", "7d", "30d"];
1889
1897
  function searchCommand() {
1890
- return new Command3("search").description("search the Kordis registry").argument("[query...]", "keywords to match against title/description").option("--type <type>", "filter by harness type (skill|mcp-server|project-rules|custom-agent|hook)").option("--tool <tool>", "filter by compatible tool (claude-code|codex|cursor|...)").option("--tier <tier>", "filter by portability tier (A|B|C|D)").option("--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) => {
1898
+ return new Command3("search").description("search the Kordis registry").argument("[query...]", "keywords to match against title/description").option("--type <type>", "filter by harness type (skill|mcp-server|project-rules|custom-agent|hook)").option("--tool <tool>", "filter by compatible tool (claude-code|codex|cursor|...)").option("--tier <tier>", "filter by portability tier (A|B|C|D)").option("--author <username>", "filter to a single author (server returns exact total when set)").option("--sort <mode>", `sort: ${SORT_MODES.join("|")}`).option("--trending", "list trending packages (overrides --sort)", false).option("--rising", "list rising packages by 7d acceleration (overrides --sort)", false).option("--window <w>", `time window for --trending: ${TIME_WINDOWS.join("|")}`, "24h").option("--limit <n>", "max results", (v) => parseInt(v, 10), 20).action(async (query, opts) => {
1891
1899
  if (opts.sort && !SORT_MODES.includes(opts.sort)) {
1892
1900
  throw new Error(`--sort must be one of ${SORT_MODES.join(", ")}`);
1893
1901
  }
@@ -1913,6 +1921,7 @@ function searchCommand() {
1913
1921
  type: opts.type ? [opts.type] : void 0,
1914
1922
  tool: opts.tool ? [opts.tool] : void 0,
1915
1923
  tier: opts.tier,
1924
+ ...opts.author ? { author: opts.author } : {},
1916
1925
  sort: opts.sort ?? "relevance",
1917
1926
  limit: opts.limit
1918
1927
  });
@@ -1944,6 +1953,14 @@ function searchCommand() {
1944
1953
  }
1945
1954
  return;
1946
1955
  }
1956
+ if (typeof res.total === "number") {
1957
+ printHuman(
1958
+ c.gray(
1959
+ `Showing ${res.results.length} of ${res.total} match${res.total === 1 ? "" : "es"}`
1960
+ )
1961
+ );
1962
+ printHuman("");
1963
+ }
1947
1964
  for (const p of res.results) {
1948
1965
  printHuman(
1949
1966
  `${c.bold(p.full_slug)} ${c.gray(`v${p.current_version}`)} ${c.cyan(`[${p.type}]`)}`
@@ -2557,6 +2574,10 @@ function pushCommand() {
2557
2574
  content = ensureTypeField(content, "skill");
2558
2575
  } else {
2559
2576
  content = await readFile6(abs, "utf8");
2577
+ const isSkillFilename = abs.endsWith("/SKILL.md") || abs.endsWith(".skill.md");
2578
+ if (isSkillFilename) {
2579
+ content = ensureTypeField(content, "skill");
2580
+ }
2560
2581
  }
2561
2582
  let nameFromFm = null;
2562
2583
  try {
@@ -3642,9 +3663,33 @@ async function copyBundledSkill(filePath, content, dryRun) {
3642
3663
  return existing === null ? "injected" : "updated";
3643
3664
  }
3644
3665
  function setupCommand() {
3645
- 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").action(async (opts) => {
3666
+ 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(
3667
+ "--personalization <choice>",
3668
+ "record opt-in non-interactively: 'on' | 'off' (skips the Y/n prompt; safe for agents that just received user consent in chat)"
3669
+ ).action(async (opts) => {
3646
3670
  const home = homedir7();
3647
3671
  const dryRun = opts.dryRun ?? false;
3672
+ if (opts.personalization !== void 0) {
3673
+ const v = opts.personalization.toLowerCase();
3674
+ if (v !== "on" && v !== "off") {
3675
+ throw new Error("--personalization must be 'on' or 'off'");
3676
+ }
3677
+ if (!dryRun) {
3678
+ const cfg = await loadCliConfig();
3679
+ await saveCliConfig({ ...cfg, personalization: v });
3680
+ }
3681
+ if (!opts.silent) {
3682
+ if (v === "on") {
3683
+ printHuman(
3684
+ c.green("\u2713 personalization on") + c.gray(" \u2014 disable any time: `KORDIS_NO_CONTEXT=1` env, `--no-context` flag, or `kordis setup --personalization off`")
3685
+ );
3686
+ } else {
3687
+ printHuman(
3688
+ c.gray("\u2192 personalization off \u2014 recommendations use popularity only.")
3689
+ );
3690
+ }
3691
+ }
3692
+ }
3648
3693
  let hubContent;
3649
3694
  let subSkillContents;
3650
3695
  try {
@@ -3790,6 +3835,9 @@ function commentCommand() {
3790
3835
  const meta = [`kind: ${res.kind}`];
3791
3836
  if (res.title) meta.push(`title: "${res.title}"`);
3792
3837
  printHuman(` ${meta.join(" \xB7 ")}`);
3838
+ if (res.permalink) {
3839
+ printHuman(c.gray(` view: ${res.permalink}`));
3840
+ }
3793
3841
  }
3794
3842
  });
3795
3843
  }
@@ -4287,7 +4335,7 @@ function recommendCommand() {
4287
4335
  if (!enable) {
4288
4336
  printHuman(
4289
4337
  c.gray(
4290
- "\u2192 recommendations will use popularity only. Re-enable later: `kordis setup` (re-asks the opt-in)."
4338
+ "\u2192 recommendations will use popularity only. Re-enable later: `kordis setup --personalization on`."
4291
4339
  )
4292
4340
  );
4293
4341
  }
@@ -4306,11 +4354,24 @@ function recommendCommand() {
4306
4354
  human: () => {
4307
4355
  if (res.items.length === 0) {
4308
4356
  printHuman(c.yellow("(no recommendations available right now)"));
4309
- printHuman(
4310
- c.gray(
4311
- " \u2192 registry may be sparse; try `kordis search --trending` for broader discovery"
4312
- )
4313
- );
4357
+ if (!res.personalized) {
4358
+ printHuman(
4359
+ c.gray(
4360
+ " \u2192 personalization is OFF, so the server ranks on popularity alone \u2014 likely empty pool."
4361
+ )
4362
+ );
4363
+ printHuman(
4364
+ c.gray(
4365
+ " \u2192 enable for stack-aware matches: `kordis setup --personalization on` (no Y/n prompt \u2014 agent-friendly; confirm with the user first)"
4366
+ )
4367
+ );
4368
+ } else {
4369
+ printHuman(
4370
+ c.gray(
4371
+ " \u2192 personalization is ON but no matches for your stack \u2014 registry may be sparse for this profile. Try `kordis search <topic>` or `kordis search --trending`."
4372
+ )
4373
+ );
4374
+ }
4314
4375
  return;
4315
4376
  }
4316
4377
  printHuman(
@@ -4335,7 +4396,7 @@ function recommendCommand() {
4335
4396
  if (!res.personalized) {
4336
4397
  printHuman(
4337
4398
  c.gray(
4338
- "\u2192 enable personalization for tailored picks: `kordis setup` (re-asks the opt-in)"
4399
+ "\u2192 enable personalization for tailored picks: `kordis setup --personalization on` (no Y/n prompt \u2014 agent-friendly; confirm with the user in chat first)"
4339
4400
  )
4340
4401
  );
4341
4402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",