@rayul/kordis 0.14.1 → 0.15.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.md CHANGED
@@ -35,6 +35,15 @@ from reusable domain knowledge, structure, or templates, run
35
35
  `kordis read <full_slug>` before proceeding; otherwise continue ad hoc.
36
36
  NEVER fabricate a slug.
37
37
 
38
+ **Search in the user's own words** — NEVER translate the user's query
39
+ before searching, in any language. Pass their words through verbatim. The
40
+ server already translates the non-Latin words it knows AND keeps the
41
+ originals in the same query, so a query you pre-translated searches strictly
42
+ less: it can no longer reach titles and keywords written in that language.
43
+ The answer names what else was searched and which words it could not
44
+ translate — relay both, because a word it skipped may be the one that
45
+ mattered.
46
+
38
47
  **Judgment patterns** — `judgment-pattern` packages carry agentic _judgment_
39
48
  (when to switch modes, what to verify, what NOT to do), not installable files.
40
49
  Don't install them: `kordis read` one, or `kordis adapt <slug>` to get a brief
@@ -67,6 +67,21 @@ the right filters.
67
67
  ALWAYS with `--json`. **Success criteria**: CLI exits 0 and returns a results
68
68
  array.
69
69
 
70
+ **If the response carries `query_translation`, SAY SO in your answer.** It has
71
+ three parts and each changes how the user should read your results:
72
+ `translated` (what else was searched on their behalf — a slash means either
73
+ word matches), and `unmapped` (words the lexicon did not know, which therefore
74
+ reached the index only as typed). One sentence before the results is enough:
75
+ "searched your words as typed, and also as `commit message rule/convention`".
76
+
77
+ This is a rule, not a flourish. The user cannot see the field — only you can —
78
+ so an unreported translation makes a widened search look like a narrow one, and
79
+ an unreported `unmapped` makes a thin answer read as "the registry has nothing"
80
+ when it may mean "two of your three words were never looked up". Measured
81
+ 2026-09-07: agents that had this only as an example in the Examples section
82
+ relayed it 0 times out of 2 on the weaker tier while seeing the field every
83
+ time.
84
+
70
85
  ### 3. Triage the results
71
86
 
72
87
  - Prefer `review_status: passed`, higher `stars`, then `tier: A`.
@@ -109,6 +124,22 @@ User: "find me a Python skill"
109
124
  - ask which to read or pull
110
125
  </example>
111
126
 
127
+ <example>
128
+ User: "커밋 메시지 규칙 패키지 있어?"
129
+ - kordis search "커밋 메시지 규칙" --json — the user's words, VERBATIM
130
+ - do NOT search `"commit message rule"` instead. The registry being mostly
131
+ English is not a reason to translate first: the server already searches the
132
+ English terms AND the Korean originals in one query, so your translation
133
+ only subtracts — Korean titles and the Korean keywords on English packages
134
+ become unreachable
135
+ - the response carries `query_translation` {original, translated, unmapped}
136
+ - tell the user what else was searched ("commit message rule/convention" — a
137
+ slash means either word matches) and
138
+ which words the lexicon skipped (`unmapped`); a row may have matched only
139
+ the translation. If `unmapped` is non-empty and the answer is thin, say so
140
+ before concluding the registry has nothing — those words were never searched
141
+ </example>
142
+
112
143
  <example>
113
144
  User: "convert this .hwp file to text"
114
145
  - kordis search hwp --json
@@ -86,13 +86,41 @@ Writes files into the current project.
86
86
  | `--no-with-required` | skip transitive required deps (advanced; default installs) |
87
87
 
88
88
  IMPORTANT: `pull` **fails fast** when the target tool can't be inferred from
89
- the cwd (no `.claude/`, `.codex/`, `.cursor/`, `AGENTS.md`, `CLAUDE.md`,
90
- etc.). Pass `--tool <name>` explicitly when running in a project that has
91
- not been initialised for any specific tool yet. Don't rely on the old "drop
92
- a canonical SKILL.md at cwd" fallback — Claude Code never picked it up.
89
+ the cwd. Pass `--tool <name>` explicitly in a project that has not been
90
+ initialised for any tool yet. Don't rely on the old "drop a canonical
91
+ SKILL.md at cwd" fallback — Claude Code never picked it up.
92
+
93
+ Auto-detection is ordered by EVIDENCE, not by vendor. A project is matched
94
+ against, in order: `.claude/` → `.codex/` → `.gemini/` → `.agents/` →
95
+ `AGENTS.md` → `CLAUDE.md` → `GEMINI.md`, then the rules-file markers
96
+ (`.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md`).
97
+ A bare `.cursor/`, `.continue/`, `.windsurf/`, `.antigravity/` or
98
+ `.opencode/` directory no longer selects its own vendor — nothing has been
99
+ shown to read `<vendor>/skills/`. They still DETECT, and they resolve to
100
+ `agents-md`, so such a project gets its package somewhere with evidence behind
101
+ it rather than an error. An explicit `--tool` always wins over detection and
102
+ never changes behaviour.
103
+
104
+ `AGENTS.md` alone now resolves to `agents-md`, not to `codex`. AGENTS.md is a
105
+ cross-vendor standard (an Agentic AI Foundation project alongside MCP and
106
+ goose), so a project carrying one is not thereby a Codex project.
93
107
 
94
108
  Valid `--tool` values: `claude-code`, `codex`, `cursor`, `continue`,
95
- `windsurf`, `antigravity`, `copilot`.
109
+ `windsurf`, `antigravity`, `copilot`, `opencode`, `gemini-cli`, `agents-md`.
110
+
111
+ **PASS `--tool <your own tool>` WHEN YOU ARE THE ONE WHO WILL USE THE SKILL.**
112
+ Detection reads the project's markers, which answer "what convention does this
113
+ repo follow" — not "which agent is about to consume this". When the user asks
114
+ YOU to install a skill so that YOU behave differently from now on, the right
115
+ target is your own tool home, and you know that better than the directory
116
+ does. Say `--tool claude-code` (or `codex`, `gemini-cli`, …) and, for a habit
117
+ meant to outlive this one repo, `--target "$HOME"`.
118
+
119
+ Leave detection to decide when the project's convention is the point: setting a
120
+ repo up for whoever works in it next, or installing on behalf of a tool that is
121
+ not you. Measured 2026-09-07: an agent that skipped this pulled with detection,
122
+ read `--help`, `rm -rf`'d what it had just installed, and pulled again with
123
+ `--tool` — four steps to reach the target it wanted from the start.
96
124
 
97
125
  ### `kordis info <full_slug> --json`
98
126
 
@@ -107,8 +135,16 @@ user is about to execute foreign code or runtime config.
107
135
  LLM-facing recipe (Markdown body following the open Agent Skills spec).
108
136
 
109
137
  - DEFAULT: `read`.
110
- - `pull` writes to the auto-detected tool's skill path
111
- (e.g. `~/.claude/skills/<slug>/SKILL.md` for Claude Code).
138
+ - `pull` writes to the detected tool's skill path — `.claude/skills/<name>/SKILL.md`
139
+ for Claude Code, `.codex/skills/…` for Codex, `.gemini/skills/…` for Gemini CLI,
140
+ and `.agents/skills/<name>/SKILL.md` for `agents-md`.
141
+ - `.agents/skills/` is the shared one: measured 2026-09-06, both Codex 0.153.4
142
+ and Gemini CLI 0.56.0 load it, at project level and at `~/.agents/skills`.
143
+ So a package installed there is picked up by either without further setup.
144
+ Claude Code does not read it and keeps `.claude/skills/`.
145
+ - Gemini CLI only loads PROJECT skills in a folder the user has marked trusted.
146
+ If a freshly pulled skill does not show up, that trust prompt is the reason,
147
+ not the install.
112
148
 
113
149
  ### `project-rules`
114
150
 
@@ -36,6 +36,18 @@ to drop the filter or broaden the words, and the same sentence is on `hint`
36
36
  under `--json`. Take it. Guessing a third phrasing when the tool has already
37
37
  named the lever is how one lookup becomes five.
38
38
 
39
+ NEVER translate the user's words before searching. Pass them as typed, in
40
+ whatever script they arrive in. That the registry is mostly English is not a
41
+ reason to search in English first: the server translates the words a fixed
42
+ lexicon knows AND searches the originals in the same query, so a pre-translated
43
+ query searches strictly less — the Korean titles, and the Korean keywords
44
+ carried by English packages, stop being reachable. The answer carries a line
45
+ naming what else was searched and which words were skipped
46
+ (`query_translation` under `--json`: `original`, `translated`, `unmapped`).
47
+ Relay that line — a row may have matched only the translation, and a skipped
48
+ word may be the one that mattered. When `unmapped` is non-empty, never report
49
+ "the registry has nothing" without saying which words were never searched.
50
+
39
51
  ## `registry read` — somebody else's judgment, in your context
40
52
 
41
53
  ```
@@ -188,6 +188,8 @@ neither the command nor you should rewrite it uninvited.
188
188
  ```
189
189
  memgineering retire <ref> --reason "the date moved" # no longer current, still visible
190
190
  memgineering exclude path/to/note.md --reason "it has someone's phone number in it" # stop reading it
191
+ memgineering retract --actor "mcp:ChatGPT" --since 2026-09-01 # SHOWS what one writer / window wrote
192
+ memgineering retract --actor "mcp:ChatGPT" --since 2026-09-01 --yes # retires all of it, one ledger row each
191
193
  ```
192
194
 
193
195
  Retiring keeps the note in recall, ranked last and labelled. Excluding takes it
package/dist/index.js CHANGED
@@ -36,7 +36,8 @@ var init_primitives = __esm({
36
36
  "continue",
37
37
  "copilot",
38
38
  "opencode",
39
- "gemini-cli"
39
+ "gemini-cli",
40
+ "agents-md"
40
41
  ]);
41
42
  TagSchema = z.string().min(1).max(32).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/);
42
43
  UrlSchema = z.string().url().startsWith("http");
@@ -502,12 +503,34 @@ var init_manifest = __esm({
502
503
  modes: ["read", "install"],
503
504
  default: "read"
504
505
  }),
505
- // Rules merge into a file the project already owns (.cursorrules,
506
- // CLAUDE.md) — the reason they need review, not just approval.
506
+ // Rules merge into a file the project already owns (CLAUDE.md, AGENTS.md,
507
+ // GEMINI.md, …) — the reason they need review, not just approval.
508
+ //
509
+ // Cursor is the exception, and it is declared per host rather than folded
510
+ // into the wildcard. Its current rules format is one `.mdc` file per rule
511
+ // under `.cursor/rules/`, so Kordis writes a whole file Cursor did not have
512
+ // instead of appending to a file the project owns. That is a `create`, and
513
+ // declaring it as a merge would be a false statement about what install
514
+ // does — `packages/translators/src/parity.test.ts` compares this table
515
+ // against what the translators actually emit, per host, and catches exactly
516
+ // that. Risk is unchanged: `deriveAggregates` still sees a merge among the
517
+ // effects, so project-rules stays `review-required`.
507
518
  "project-rules": (input) => build(input, {
508
519
  kind: "rules",
509
520
  profile: null,
510
- effects: [{ host: "*", phase: "install", mode: "filesystem-merge" }],
521
+ effects: [
522
+ { host: "claude-code", phase: "install", mode: "filesystem-merge" },
523
+ { host: "codex", phase: "install", mode: "filesystem-merge" },
524
+ { host: "agents-md", phase: "install", mode: "filesystem-merge" },
525
+ { host: "gemini-cli", phase: "install", mode: "filesystem-merge" },
526
+ { host: "windsurf", phase: "install", mode: "filesystem-merge" },
527
+ { host: "copilot", phase: "install", mode: "filesystem-merge" },
528
+ { host: "continue", phase: "install", mode: "filesystem-merge" },
529
+ { host: "antigravity", phase: "install", mode: "filesystem-merge" },
530
+ { host: "opencode", phase: "install", mode: "filesystem-merge" },
531
+ { host: "canonical", phase: "install", mode: "filesystem-merge" },
532
+ { host: "cursor", phase: "install", mode: "filesystem-create" }
533
+ ],
511
534
  modes: ["read", "install"],
512
535
  default: "read"
513
536
  }),
@@ -1035,7 +1058,7 @@ var init_auth = __esm({
1035
1058
 
1036
1059
  // ../../packages/contracts/src/api.ts
1037
1060
  import { z as z11 } from "zod";
1038
- var SearchQuerySchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, AssetSchema, LineageEdgeInputSchema, LineageRelativeSchema, LineageStatsSchema, LineageGraphResponseSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageRequestSchema, PullPackageResponseSchema, YankRequestSchema, YankResponseSchema, ResolveVersionQuerySchema, ResolveVersionResponseSchema, GetPackageQuerySchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema, ListUserPackagesResponseSchema, ListUserStarsResponseSchema, FollowResponseSchema, ListFollowersQuerySchema, ListFollowersResponseSchema;
1061
+ var SearchQuerySchema, SearchQueryTranslationSchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, AssetSchema, LineageEdgeInputSchema, LineageRelativeSchema, LineageStatsSchema, LineageGraphResponseSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageRequestSchema, PullPackageResponseSchema, YankRequestSchema, YankResponseSchema, ResolveVersionQuerySchema, ResolveVersionResponseSchema, GetPackageQuerySchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema, ListUserPackagesResponseSchema, ListUserStarsResponseSchema, FollowResponseSchema, ListFollowersQuerySchema, ListFollowersResponseSchema;
1039
1062
  var init_api = __esm({
1040
1063
  "../../packages/contracts/src/api.ts"() {
1041
1064
  "use strict";
@@ -1057,7 +1080,15 @@ var init_api = __esm({
1057
1080
  limit: z11.coerce.number().int().min(1).max(100).default(20),
1058
1081
  cursor: CursorSchema.optional()
1059
1082
  });
1060
- SearchResponseSchema = paginatedResponseSchema(PackageSummarySchema);
1083
+ SearchQueryTranslationSchema = z11.object({
1084
+ kind: z11.literal("lexicon"),
1085
+ original: z11.string(),
1086
+ translated: z11.string(),
1087
+ unmapped: z11.array(z11.string())
1088
+ });
1089
+ SearchResponseSchema = paginatedResponseSchema(PackageSummarySchema).extend({
1090
+ query_translation: SearchQueryTranslationSchema.optional()
1091
+ });
1061
1092
  TrendingQuerySchema = z11.object({
1062
1093
  window: TimeWindowSchema.default("24h"),
1063
1094
  type: HarnessTypeSchema.optional(),
@@ -1308,7 +1339,22 @@ var init_api = __esm({
1308
1339
  * Walk depth-0 `recommended` deps (transitive recommends NOT followed).
1309
1340
  * Default false — opt-in noise.
1310
1341
  */
1311
- with_recommends: z11.boolean().optional().default(false)
1342
+ with_recommends: z11.boolean().optional().default(false),
1343
+ /**
1344
+ * 2026-09-06 — which install target the CLI resolved for this pull
1345
+ * ('claude-code', 'agents-md', …). Recorded on `package_pull_events.tool`;
1346
+ * the server never derives it, because only the client knows which tool
1347
+ * home it is actually writing into.
1348
+ *
1349
+ * Why this ships without coordinating a CLI and a server release: this
1350
+ * schema is a NON-STRICT `z.object`, so an OLD server handed the new field
1351
+ * strips it rather than returning 400, and a NEW server handed no field
1352
+ * records NULL. Both directions degrade to "tool not recorded", which is
1353
+ * exactly what the nullable column already means — so CLI and server can
1354
+ * roll out in either order, which matters here because this repo has no CD
1355
+ * and the server is deployed by hand.
1356
+ */
1357
+ tool: ToolSlugSchema.optional()
1312
1358
  });
1313
1359
  PullPackageResponseSchema = z11.object({
1314
1360
  status: z11.enum(["installed", "already_installed", "skipped"]),
@@ -1451,8 +1497,12 @@ var init_context = __esm({
1451
1497
  * Which agentic tool configs exist locally. Presence only — no file
1452
1498
  * contents inspected. Used to boost packages whose `tools_compatible`
1453
1499
  * overlaps.
1500
+ *
1501
+ * The cap is derived from the enum rather than written as a literal: a
1502
+ * hand-written `.max(8)` silently started rejecting honest payloads the
1503
+ * moment a tenth member was added, and nothing in the type system says so.
1454
1504
  */
1455
- tools: z12.array(ToolSlugSchema).max(8).optional(),
1505
+ tools: z12.array(ToolSlugSchema).max(ToolSlugSchema.options.length).optional(),
1456
1506
  /**
1457
1507
  * Top-3 file extensions in CWD (excluding `.git`, `node_modules`,
1458
1508
  * `dist`, `.next`, build artifacts). Each entry is a short lowercase
@@ -1920,6 +1970,15 @@ var init_client = __esm({
1920
1970
  { query: { range } }
1921
1971
  );
1922
1972
  }
1973
+ /**
1974
+ * `opts` is the CONTRACT's request type, not a hand-written copy of it.
1975
+ * The copy that used to live here listed three fields and silently stopped
1976
+ * matching the schema the moment a fourth (`tool`, 2026-09-06) was added —
1977
+ * callers could not pass the new field without casting, and nothing failed
1978
+ * until someone tried. Taking `PullPackageRequestInput` means a field added
1979
+ * to the schema is transmissible the same day, and a field REMOVED from it
1980
+ * becomes a compile error here instead of a body the server ignores.
1981
+ */
1923
1982
  pull(username, slug, opts = {}) {
1924
1983
  return this.request(`/api/packages/${seg(username)}/${seg(slug)}/pull`, {
1925
1984
  method: "POST",
@@ -6521,7 +6580,14 @@ async function detectTools() {
6521
6580
  { tool: "claude-code", paths: [join4(homedir2(), ".claude", "skills")] },
6522
6581
  { tool: "codex", paths: [join4(homedir2(), ".codex"), join4(homedir2(), ".codex", "skills")] },
6523
6582
  { tool: "cursor", paths: [join4(homedir2(), ".cursor")] },
6524
- { tool: "gemini-cli", paths: [join4(homedir2(), ".gemini"), join4(homedir2(), ".gemini", "skills")] }
6583
+ { tool: "gemini-cli", paths: [join4(homedir2(), ".gemini"), join4(homedir2(), ".gemini", "skills")] },
6584
+ // `~/.agents/skills` is not one vendor's directory — measured 2026-09-06,
6585
+ // both codex-cli 0.153.4 and gemini-cli 0.56.0 load skills from it (Codex
6586
+ // lists it as a skill root; Gemini reaches it via `getUserAgentSkillsDir`).
6587
+ // Its presence therefore means "this machine follows the AGENTS.md
6588
+ // convention", which is exactly the personalization signal `agents-md`
6589
+ // packages should be ranked on.
6590
+ { tool: "agents-md", paths: [join4(homedir2(), ".agents", "skills")] }
6525
6591
  ];
6526
6592
  const found = [];
6527
6593
  for (const { tool, paths } of checks) {
@@ -7234,6 +7300,7 @@ async function maybeRenderReminder() {
7234
7300
  }
7235
7301
 
7236
7302
  // src/commands/search.ts
7303
+ init_src();
7237
7304
  var SORT_MODES = [
7238
7305
  "relevance",
7239
7306
  "stars",
@@ -7244,17 +7311,7 @@ var SORT_MODES = [
7244
7311
  "rising"
7245
7312
  ];
7246
7313
  var TIME_WINDOWS = ["24h", "7d", "30d"];
7247
- var TOOL_SLUGS = [
7248
- "claude-code",
7249
- "codex",
7250
- "cursor",
7251
- "antigravity",
7252
- "windsurf",
7253
- "continue",
7254
- "copilot",
7255
- "opencode",
7256
- "gemini-cli"
7257
- ];
7314
+ var TOOL_SLUGS = ToolSlugSchema.options;
7258
7315
  var TOOL_ALIASES = {
7259
7316
  claude: "claude-code",
7260
7317
  gemini: "gemini-cli"
@@ -7321,13 +7378,24 @@ function searchCommand() {
7321
7378
  });
7322
7379
  }
7323
7380
  const queryProvided = query.length > 0;
7324
- const emptyHint = opts.type ? `no packages of type "${opts.type}" match. Try without --type, or list everything: kordis search --type ${opts.type} --sort recent --limit 25` : queryProvided ? "narrow with `--type skill|mcp-server|project-rules|custom-agent|hook|judgment-pattern` or `--tag <topic>` (e.g. aws, fastapi), broaden the keywords, or browse `--sort trending`" : 'try keywords (e.g. `kordis search "topic"`), `--tag <topic>`, or `kordis search --trending`';
7381
+ const untranslatedWords = res.query_translation && res.query_translation.unmapped.length > 0 ? res.query_translation.unmapped.map(stripControl).join(", ") : "";
7382
+ const partialNote = untranslatedWords === "" ? "" : `${untranslatedWords} could not be translated and were searched only as typed, so this answer may be partial \u2014 tell the user which words those were before judging what the registry has. `;
7383
+ const baseEmptyHint = opts.type ? `no packages of type "${opts.type}" match. Try without --type, or list everything: kordis search --type ${opts.type} --sort recent --limit 25` : queryProvided ? "narrow with `--type skill|mcp-server|project-rules|custom-agent|hook|judgment-pattern` or `--tag <topic>` (e.g. aws, fastapi), broaden the keywords, or browse `--sort trending`" : 'try keywords (e.g. `kordis search "topic"`), `--tag <topic>`, or `kordis search --trending`';
7384
+ const emptyHintForJson = partialNote === "" ? baseEmptyHint : `${partialNote}Then ${baseEmptyHint}`;
7325
7385
  printDual({
7326
- json: res.results.length === 0 ? { ...res, hint: emptyHint } : res,
7386
+ json: res.results.length === 0 ? { ...res, hint: emptyHintForJson } : (
7387
+ // A non-empty answer to a partly-untranslated query is still a
7388
+ // partial one, and rows that look plausible are exactly when
7389
+ // that goes unsaid: the session this came from read ten
7390
+ // unrelated hits and reported the registry had nothing, never
7391
+ // mentioning that two of its three words were never looked up.
7392
+ partialNote === "" ? res : { ...res, hint: partialNote.trimEnd() }
7393
+ ),
7327
7394
  human: () => {
7328
7395
  if (res.results.length === 0) {
7329
7396
  printHuman(c.yellow("(no results)"));
7330
- printHuman(c.gray(` \u2192 ${emptyHint}`));
7397
+ printTranslation(res.query_translation);
7398
+ printHuman(c.gray(` \u2192 ${baseEmptyHint}`));
7331
7399
  return;
7332
7400
  }
7333
7401
  if (typeof res.total === "number") {
@@ -7338,6 +7406,7 @@ function searchCommand() {
7338
7406
  );
7339
7407
  printHuman("");
7340
7408
  }
7409
+ printTranslation(res.query_translation);
7341
7410
  for (const p of res.results) {
7342
7411
  printHuman(
7343
7412
  `${c.bold(p.full_slug)} ${c.gray(`v${p.current_version}`)} ${c.cyan(`[${p.type}]`)}`
@@ -7365,6 +7434,22 @@ function searchCommand() {
7365
7434
  });
7366
7435
  });
7367
7436
  }
7437
+ function printTranslation(fb) {
7438
+ if (!fb) return;
7439
+ printHuman(
7440
+ c.gray(
7441
+ `Korean words in "${stripControl(fb.original)}" were also searched as "${stripControl(fb.translated)}" (registry ko\u2192en lexicon)`
7442
+ )
7443
+ );
7444
+ if (fb.unmapped.length > 0) {
7445
+ printHuman(
7446
+ c.yellow(
7447
+ `\u26A0 NOT searched: ${fb.unmapped.map(stripControl).join(", ")} \u2014 no lexicon entry. Say so before reporting that nothing was found.`
7448
+ )
7449
+ );
7450
+ }
7451
+ printHuman("");
7452
+ }
7368
7453
 
7369
7454
  // src/commands/push.ts
7370
7455
  init_parser();
@@ -7617,13 +7702,11 @@ var ProjectRulesFrontmatterSchema = z18.object({
7617
7702
  /** When true, installation is idempotent via `<!-- kordis:... -->` markers. */
7618
7703
  mergeable: z18.boolean().default(true),
7619
7704
  license: z18.string().max(64).optional()
7620
- }).strict().refine(
7621
- (v) => v.scope !== "nested" || !!v.target_path,
7622
- { message: 'scope="nested" requires target_path' }
7623
- ).refine(
7624
- (v) => v.scope !== "globs" || v.globs && v.globs.length > 0,
7625
- { message: 'scope="globs" requires a non-empty globs list' }
7626
- );
7705
+ }).strict().refine((v) => v.scope !== "nested" || !!v.target_path, {
7706
+ message: 'scope="nested" requires target_path'
7707
+ }).refine((v) => v.scope !== "globs" || v.globs && v.globs.length > 0, {
7708
+ message: 'scope="globs" requires a non-empty globs list'
7709
+ });
7627
7710
 
7628
7711
  // ../../packages/harness-schema/src/types/custom-agent.ts
7629
7712
  init_src();
@@ -8652,9 +8735,10 @@ function buildSkillFile(input, outputPath) {
8652
8735
  function buildSkillDir(input, outputPath) {
8653
8736
  const result = buildSkillFile(input, outputPath);
8654
8737
  const skillMdPath = result.files[0].path;
8655
- const skillDir = skillMdPath.slice(0, skillMdPath.lastIndexOf("/"));
8738
+ const slash = skillMdPath.lastIndexOf("/");
8739
+ const skillDir = slash === -1 ? "" : skillMdPath.slice(0, slash);
8656
8740
  const assetFiles = (input.assets ?? []).map((asset) => ({
8657
- path: `${skillDir}/${asset.path}`,
8741
+ path: skillDir ? `${skillDir}/${asset.path}` : asset.path,
8658
8742
  content: asset.content,
8659
8743
  mergeable: false,
8660
8744
  format: "plain",
@@ -8686,6 +8770,7 @@ var skillToCopilot = (input) => buildSkillSingleFile(input, ".github/skills/{nam
8686
8770
  var skillToGeminiCli = (input) => buildSkillDir(input, ".gemini/skills/{name}/SKILL.md");
8687
8771
  var skillToOpenCode = (input) => buildSkillSingleFile(input, ".opencode/skills/{name}.md");
8688
8772
  var skillToAntigravity = (input) => buildSkillSingleFile(input, ".antigravity/skills/{name}.md");
8773
+ var skillToAgentsMd = (input) => buildSkillDir(input, ".agents/skills/{name}/SKILL.md");
8689
8774
  var skillCanonical = (input) => {
8690
8775
  const res = buildSkillDir(input, "SKILL.md");
8691
8776
  res.format = "agent-skills-canonical";
@@ -8702,15 +8787,39 @@ function fence(input) {
8702
8787
  }
8703
8788
  var TOOL_FILE_SPEC = {
8704
8789
  claude: { path: "CLAUDE.md", format: "markdown" },
8790
+ // Same file, two entries on purpose: `codex` is one vendor that reads
8791
+ // AGENTS.md, `agents-md` is the convention itself. Splitting them lets the
8792
+ // label stop lying without changing a byte of what gets written.
8705
8793
  codex: { path: "AGENTS.md", format: "markdown" },
8706
- gemini: { path: "GEMINI.md", format: "markdown" },
8707
- cursor: {
8708
- path: ".cursorrules",
8709
- format: "plain",
8710
- notes: [
8711
- "Cursor .cursorrules is a flat text file; fence markers are preserved as HTML comments."
8712
- ]
8713
- },
8794
+ "agents-md": { path: "AGENTS.md", format: "markdown" },
8795
+ // Gemini CLI's context file is GEMINI.md, and it is not an AGENTS.md reader:
8796
+ // the shipped 0.56.0 bundle has `DEFAULT_CONTEXT_FILENAME = "GEMINI.md"` and
8797
+ // zero occurrences of the string `AGENTS.md` anywhere in it (checked
8798
+ // 2026-09-06). A `.gemini/rules/project.md` spec used to sit here as well and
8799
+ // was never reachable — no such path exists in that bundle either.
8800
+ "gemini-cli": { path: "GEMINI.md", format: "markdown" },
8801
+ // `cursor` is deliberately absent from this table — `.cursor/rules/*.mdc`
8802
+ // is a per-package path with YAML frontmatter and a whole-file write, none
8803
+ // of which `ToolSpec`/`build()` can express. See `projectRulesToCursor`.
8804
+ //
8805
+ // `windsurf` is a LEGACY VENDOR NAME. Cognition rebranded Windsurf to Devin
8806
+ // Desktop in 2026-06 and retired Cascade on 2026-07-01, so this slug names a
8807
+ // product that no longer ships under that name. The slug stays because it is
8808
+ // a member of `ToolSlugSchema`, it is recorded in published packages'
8809
+ // `compatibility.hosts`, and people have scripted `--tool windsurf`.
8810
+ //
8811
+ // `.windsurfrules` is UNVERIFIED, and that is the honest status rather than
8812
+ // an oversight: no current Devin Desktop documentation has been read
8813
+ // first-hand for this path, and the Windsurf docs that once described it went
8814
+ // with the product name. It is kept because it is what Kordis has always
8815
+ // written here — not because anything confirmed it.
8816
+ //
8817
+ // Do NOT "fix" this by inventing `.devin/rules/`. There is no evidence for
8818
+ // that path either, and writing to a guessed path is the exact failure this
8819
+ // arc is correcting (compare the dead `.gemini/rules/project.md` above).
8820
+ // Establishing Devin Desktop's real rules location — and its skills
8821
+ // location, `skillToWindsurf` in `../skill/identity.ts` — is a separate
8822
+ // track that needs the product in hand.
8714
8823
  windsurf: { path: ".windsurfrules", format: "plain" },
8715
8824
  copilot: {
8716
8825
  path: ".github/copilot-instructions.md",
@@ -8728,10 +8837,20 @@ var TOOL_FILE_SPEC = {
8728
8837
  path: ".antigravity/rules/project.md",
8729
8838
  format: "markdown"
8730
8839
  },
8731
- "gemini-cli": {
8732
- path: ".gemini/rules/project.md",
8733
- format: "markdown"
8734
- },
8840
+ // `.opencode/rules/project.md` is DISPROVEN, not merely unverified: OpenCode
8841
+ // reads the project's `AGENTS.md` for rules (opencode.ai/docs/rules, checked
8842
+ // 2026-09-06; global copy at `~/.config/opencode/AGENTS.md`), and
8843
+ // `.opencode/` appears nowhere in that documentation. So every rules package
8844
+ // ever installed through this entry landed in a file OpenCode does not read.
8845
+ // Full evidence on `skillToOpenCode` (`../skill/identity.ts`).
8846
+ //
8847
+ // Detection no longer routes here: `TOOL_MARKERS`
8848
+ // (`apps/cli/src/commands/pull.ts`) maps an `AGENTS.md` project to
8849
+ // `agents-md`, and there is no `.opencode` marker, so an OpenCode project
8850
+ // now gets `AGENTS.md` written. This entry survives only for an explicit
8851
+ // `--tool opencode`, and only so that flag keeps working for anyone who
8852
+ // scripted it — the path it writes is still the wrong one.
8853
+ // @deprecated Use `--tool agents-md` (or plain detection) instead.
8735
8854
  opencode: { path: ".opencode/rules/project.md", format: "markdown" }
8736
8855
  };
8737
8856
  function build2(spec, input, formatName) {
@@ -8750,13 +8869,37 @@ function build2(spec, input, formatName) {
8750
8869
  }
8751
8870
  var projectRulesToClaude = (input) => build2(TOOL_FILE_SPEC["claude"], input, "claude-md");
8752
8871
  var projectRulesToCodex = (input) => build2(TOOL_FILE_SPEC["codex"], input, "agents-md");
8753
- var projectRulesToGemini = (input) => build2(TOOL_FILE_SPEC["gemini"], input, "gemini-md");
8754
- var projectRulesToCursor = (input) => build2(TOOL_FILE_SPEC["cursor"], input, "cursorrules");
8872
+ var projectRulesToAgentsMd = (input) => build2(TOOL_FILE_SPEC["agents-md"], input, "agents-md");
8873
+ var projectRulesToGeminiCli = (input) => build2(TOOL_FILE_SPEC["gemini-cli"], input, "gemini-md");
8874
+ var CURSOR_RULE_DIR = ".cursor/rules";
8875
+ var projectRulesToCursor = (input) => {
8876
+ const slug = input.fullSlug.split("/")[1] ?? input.fullSlug;
8877
+ const description = typeof input.frontmatter["description"] === "string" ? input.frontmatter["description"] : input.fullSlug;
8878
+ const frontmatter = `---
8879
+ description: ${JSON.stringify(description)}
8880
+ alwaysApply: true
8881
+ ---
8882
+
8883
+ `;
8884
+ return {
8885
+ files: [
8886
+ {
8887
+ path: `${CURSOR_RULE_DIR}/kordis-${slug}.mdc`,
8888
+ content: frontmatter + fence(input),
8889
+ mergeable: false,
8890
+ format: "markdown"
8891
+ }
8892
+ ],
8893
+ notes: [
8894
+ `Cursor rule installed at ${CURSOR_RULE_DIR}/kordis-${slug}.mdc with alwaysApply: true \u2014 it is in context for every request. Delete the file to stop that.`
8895
+ ],
8896
+ format: "cursor-mdc"
8897
+ };
8898
+ };
8755
8899
  var projectRulesToWindsurf = (input) => build2(TOOL_FILE_SPEC["windsurf"], input, "windsurfrules");
8756
8900
  var projectRulesToCopilot = (input) => build2(TOOL_FILE_SPEC["copilot"], input, "copilot-instructions-md");
8757
8901
  var projectRulesToContinue = (input) => build2(TOOL_FILE_SPEC["continue"], input, "continue-rules-md");
8758
8902
  var projectRulesToAntigravity = (input) => build2(TOOL_FILE_SPEC["antigravity"], input, "antigravity-rules-md");
8759
- var projectRulesToGeminiCli = (input) => build2(TOOL_FILE_SPEC["gemini-cli"], input, "gemini-cli-rules-md");
8760
8903
  var projectRulesToOpenCode = (input) => build2(TOOL_FILE_SPEC["opencode"], input, "opencode-rules-md");
8761
8904
  var projectRulesCanonical = (input) => ({
8762
8905
  files: [
@@ -9072,7 +9215,8 @@ var REGISTRY = {
9072
9215
  copilot: skillToCopilot,
9073
9216
  "gemini-cli": skillToGeminiCli,
9074
9217
  opencode: skillToOpenCode,
9075
- antigravity: skillToAntigravity
9218
+ antigravity: skillToAntigravity,
9219
+ "agents-md": skillToAgentsMd
9076
9220
  },
9077
9221
  "project-rules": {
9078
9222
  canonical: projectRulesCanonical,
@@ -9084,9 +9228,8 @@ var REGISTRY = {
9084
9228
  copilot: projectRulesToCopilot,
9085
9229
  "gemini-cli": projectRulesToGeminiCli,
9086
9230
  opencode: projectRulesToOpenCode,
9087
- antigravity: projectRulesToAntigravity
9088
- // `gemini` CLI vs codex's GEMINI.md — codex reads both; we map "gemini-cli"
9089
- // to its own dir and "codex" to AGENTS.md. GEMINI.md alias below.
9231
+ antigravity: projectRulesToAntigravity,
9232
+ "agents-md": projectRulesToAgentsMd
9090
9233
  },
9091
9234
  "mcp-server": {
9092
9235
  canonical: mcpCanonical,
@@ -9110,7 +9253,6 @@ var REGISTRY = {
9110
9253
  "claude-code": judgmentToClaude
9111
9254
  }
9112
9255
  };
9113
- REGISTRY["project-rules"]["gemini-cli"] = projectRulesToGemini;
9114
9256
  var TranslatorNotSupportedError = class extends Error {
9115
9257
  type;
9116
9258
  format;
@@ -9339,11 +9481,13 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
9339
9481
  throw e;
9340
9482
  }
9341
9483
  const bundle = await useClient();
9342
- const pullRes = await bundle.api.pull(username, slug, {
9484
+ const pullBody = {
9343
9485
  ...opts.version ? { version: opts.version } : {},
9344
9486
  ...opts.withRequired !== void 0 ? { with_required: opts.withRequired } : {},
9345
- ...opts.withRecommends !== void 0 ? { with_recommends: opts.withRecommends } : {}
9346
- });
9487
+ ...opts.withRecommends !== void 0 ? { with_recommends: opts.withRecommends } : {},
9488
+ tool
9489
+ };
9490
+ const pullRes = await bundle.api.pull(username, slug, pullBody);
9347
9491
  const contentRes = await bundle.api.getContent(username, slug, {
9348
9492
  format: "canonical",
9349
9493
  // Server resolved the range during pull; pass the concrete version
@@ -9463,9 +9607,12 @@ function pullCommand() {
9463
9607
  const installRoot = opts.target ?? userCwd;
9464
9608
  const detected = opts.tool ?? await detectTool(installRoot);
9465
9609
  if (!detected) {
9466
- throw new Error(
9467
- "Cannot detect target tool from the current directory.\n Pass --tool <name> explicitly. Valid: claude-code, codex, cursor,\n continue, windsurf, antigravity, copilot.\n Or run `kordis pull` inside a project that already has one of:\n .claude/, .codex/, .cursor/, AGENTS.md, CLAUDE.md, .cursorrules,\n .continue/, .windsurf/, .antigravity/, .github/copilot-instructions.md"
9610
+ const e = new Error(
9611
+ "Cannot detect target tool from the current directory."
9468
9612
  );
9613
+ e.hint = `Pass --tool <name> explicitly. Valid: ${ToolSlugSchema.options.join(", ")}.
9614
+ Or run \`kordis pull\` inside a project that already has one of: ${TOOL_MARKERS.map(([marker]) => marker).join(", ")}.`;
9615
+ throw e;
9469
9616
  }
9470
9617
  const rootLabel = pin ? `${parsed.fullSlug}@${pin}` : parsed.fullSlug;
9471
9618
  const spin = spinner(`Pulling ${rootLabel}`);
@@ -9560,22 +9707,33 @@ function pullCommand() {
9560
9707
  }
9561
9708
  );
9562
9709
  }
9710
+ var TOOL_MARKERS = [
9711
+ [".claude", "claude-code"],
9712
+ [".codex", "codex"],
9713
+ [".gemini", "gemini-cli"],
9714
+ // The shared skills dir. Both Codex and Gemini read it; neither owns it.
9715
+ [".agents", "agents-md"],
9716
+ // Was `codex` until 2026-09-06. AGENTS.md is a convention, not a vendor.
9717
+ ["AGENTS.md", "agents-md"],
9718
+ ["CLAUDE.md", "claude-code"],
9719
+ ["GEMINI.md", "gemini-cli"],
9720
+ // Project-rules markers only — about rules files, not skill routing.
9721
+ [".cursorrules", "cursor"],
9722
+ [".windsurfrules", "windsurf"],
9723
+ [".github/copilot-instructions.md", "copilot"],
9724
+ // Vendor directories with no evidence behind their skills path. Present =
9725
+ // "a project set up for an agentic tool", which is true and useful; it just
9726
+ // does not license writing to `<vendor>/skills/`. Last, so everything above
9727
+ // wins first.
9728
+ [".cursor", "agents-md"],
9729
+ [".continue", "agents-md"],
9730
+ [".windsurf", "agents-md"],
9731
+ [".antigravity", "agents-md"],
9732
+ [".opencode", "agents-md"]
9733
+ ];
9563
9734
  async function detectTool(root) {
9564
9735
  const { stat: stat3 } = await import("fs/promises");
9565
- const checks = [
9566
- [".claude", "claude-code"],
9567
- [".cursor", "cursor"],
9568
- [".continue", "continue"],
9569
- [".windsurf", "windsurf"],
9570
- [".antigravity", "antigravity"],
9571
- [".codex", "codex"],
9572
- ["AGENTS.md", "codex"],
9573
- ["CLAUDE.md", "claude-code"],
9574
- [".cursorrules", "cursor"],
9575
- [".windsurfrules", "windsurf"],
9576
- [".github/copilot-instructions.md", "copilot"]
9577
- ];
9578
- for (const [path2, tool] of checks) {
9736
+ for (const [path2, tool] of TOOL_MARKERS) {
9579
9737
  try {
9580
9738
  await stat3(join10(root, path2));
9581
9739
  return tool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.14.1",
3
+ "version": "0.15.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": "Apache-2.0",