@rayul/kordis 0.6.1 → 0.6.3
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 +47 -26
- 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
|
@@ -1779,14 +1779,25 @@ function loginCommand() {
|
|
|
1779
1779
|
|
|
1780
1780
|
// src/commands/whoami.ts
|
|
1781
1781
|
import { Command as Command2 } from "commander";
|
|
1782
|
+
|
|
1783
|
+
// src/lib/require-auth.ts
|
|
1784
|
+
init_src2();
|
|
1785
|
+
function requireAuth(bundle) {
|
|
1786
|
+
if (bundle.username) return;
|
|
1787
|
+
throw new KordisApiError(401, {
|
|
1788
|
+
error: {
|
|
1789
|
+
code: "not_authenticated",
|
|
1790
|
+
message: "authentication required",
|
|
1791
|
+
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."
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// src/commands/whoami.ts
|
|
1782
1797
|
function whoamiCommand() {
|
|
1783
1798
|
return new Command2("whoami").description("show the currently authenticated user").action(async () => {
|
|
1784
1799
|
const bundle = await useClient();
|
|
1785
|
-
|
|
1786
|
-
printHuman(c.yellow("Not logged in. Run `kordis login`."));
|
|
1787
|
-
process.exitCode = 10;
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1800
|
+
requireAuth(bundle);
|
|
1790
1801
|
const me = await bundle.authedRequest((api) => api.me());
|
|
1791
1802
|
printDual({
|
|
1792
1803
|
json: me,
|
|
@@ -3631,9 +3642,33 @@ async function copyBundledSkill(filePath, content, dryRun) {
|
|
|
3631
3642
|
return existing === null ? "injected" : "updated";
|
|
3632
3643
|
}
|
|
3633
3644
|
function setupCommand() {
|
|
3634
|
-
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").
|
|
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").option(
|
|
3646
|
+
"--personalization <choice>",
|
|
3647
|
+
"record opt-in non-interactively: 'on' | 'off' (skips the Y/n prompt; safe for agents that just received user consent in chat)"
|
|
3648
|
+
).action(async (opts) => {
|
|
3635
3649
|
const home = homedir7();
|
|
3636
3650
|
const dryRun = opts.dryRun ?? false;
|
|
3651
|
+
if (opts.personalization !== void 0) {
|
|
3652
|
+
const v = opts.personalization.toLowerCase();
|
|
3653
|
+
if (v !== "on" && v !== "off") {
|
|
3654
|
+
throw new Error("--personalization must be 'on' or 'off'");
|
|
3655
|
+
}
|
|
3656
|
+
if (!dryRun) {
|
|
3657
|
+
const cfg = await loadCliConfig();
|
|
3658
|
+
await saveCliConfig({ ...cfg, personalization: v });
|
|
3659
|
+
}
|
|
3660
|
+
if (!opts.silent) {
|
|
3661
|
+
if (v === "on") {
|
|
3662
|
+
printHuman(
|
|
3663
|
+
c.green("\u2713 personalization on") + c.gray(" \u2014 disable any time: `KORDIS_NO_CONTEXT=1` env, `--no-context` flag, or `kordis setup --personalization off`")
|
|
3664
|
+
);
|
|
3665
|
+
} else {
|
|
3666
|
+
printHuman(
|
|
3667
|
+
c.gray("\u2192 personalization off \u2014 recommendations use popularity only.")
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3637
3672
|
let hubContent;
|
|
3638
3673
|
let subSkillContents;
|
|
3639
3674
|
try {
|
|
@@ -3763,11 +3798,7 @@ function commentCommand() {
|
|
|
3763
3798
|
throw new Error('full_slug must be "{username}/{slug}"');
|
|
3764
3799
|
}
|
|
3765
3800
|
const bundle = await useClient();
|
|
3766
|
-
|
|
3767
|
-
printHuman(c.yellow("Not logged in. Run `kordis login`."));
|
|
3768
|
-
process.exitCode = 10;
|
|
3769
|
-
return;
|
|
3770
|
-
}
|
|
3801
|
+
requireAuth(bundle);
|
|
3771
3802
|
const res = await bundle.authedRequest(
|
|
3772
3803
|
(api) => api.createComment(username, slug, {
|
|
3773
3804
|
kind: opts.kind,
|
|
@@ -3798,11 +3829,7 @@ function starCommand() {
|
|
|
3798
3829
|
throw new Error('full_slug must be "{username}/{slug}"');
|
|
3799
3830
|
}
|
|
3800
3831
|
const bundle = await useClient();
|
|
3801
|
-
|
|
3802
|
-
printHuman(c.yellow("Not logged in. Run `kordis login`."));
|
|
3803
|
-
process.exitCode = 10;
|
|
3804
|
-
return;
|
|
3805
|
-
}
|
|
3832
|
+
requireAuth(bundle);
|
|
3806
3833
|
const res = await bundle.authedRequest((api) => api.star(username, slug));
|
|
3807
3834
|
printDual({
|
|
3808
3835
|
json: res,
|
|
@@ -3820,11 +3847,7 @@ function unstarCommand() {
|
|
|
3820
3847
|
throw new Error('full_slug must be "{username}/{slug}"');
|
|
3821
3848
|
}
|
|
3822
3849
|
const bundle = await useClient();
|
|
3823
|
-
|
|
3824
|
-
printHuman(c.yellow("Not logged in. Run `kordis login`."));
|
|
3825
|
-
process.exitCode = 10;
|
|
3826
|
-
return;
|
|
3827
|
-
}
|
|
3850
|
+
requireAuth(bundle);
|
|
3828
3851
|
const res = await bundle.authedRequest(
|
|
3829
3852
|
(api) => api.unstar(username, slug)
|
|
3830
3853
|
);
|
|
@@ -3883,9 +3906,7 @@ function listCommand() {
|
|
|
3883
3906
|
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) => {
|
|
3884
3907
|
const sort = opts.sort === "popular" ? "popular" : "latest";
|
|
3885
3908
|
const bundle = await useClient();
|
|
3886
|
-
|
|
3887
|
-
throw new Error("Not logged in. Run `kordis login` first.");
|
|
3888
|
-
}
|
|
3909
|
+
requireAuth(bundle);
|
|
3889
3910
|
const spin = spinner("Fetching your packages");
|
|
3890
3911
|
const res = await bundle.authedRequest(
|
|
3891
3912
|
(api) => api.listMyPackages({ page: opts.page, limit: opts.limit, sort })
|
|
@@ -4290,7 +4311,7 @@ function recommendCommand() {
|
|
|
4290
4311
|
if (!enable) {
|
|
4291
4312
|
printHuman(
|
|
4292
4313
|
c.gray(
|
|
4293
|
-
"\u2192 recommendations will use popularity only. Re-enable later: `kordis setup
|
|
4314
|
+
"\u2192 recommendations will use popularity only. Re-enable later: `kordis setup --personalization on`."
|
|
4294
4315
|
)
|
|
4295
4316
|
);
|
|
4296
4317
|
}
|
|
@@ -4338,7 +4359,7 @@ function recommendCommand() {
|
|
|
4338
4359
|
if (!res.personalized) {
|
|
4339
4360
|
printHuman(
|
|
4340
4361
|
c.gray(
|
|
4341
|
-
"\u2192 enable personalization for tailored picks: `kordis setup` (
|
|
4362
|
+
"\u2192 enable personalization for tailored picks: `kordis setup --personalization on` (no Y/n prompt \u2014 agent-friendly; confirm with the user in chat first)"
|
|
4342
4363
|
)
|
|
4343
4364
|
);
|
|
4344
4365
|
}
|