@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.
- package/assets/kordis-recommend/SKILL.md +41 -9
- package/dist/index.js +72 -11
- package/package.json +1 -1
|
@@ -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
|
|
49
|
-
|
|
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
|
-
|
|
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 —
|
|
58
|
-
|
|
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`,
|
|
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
|
|
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").
|
|
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
|
|
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
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
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` (
|
|
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
|
}
|