@locusai/cli 0.26.0 → 0.26.1

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.
Files changed (2) hide show
  1. package/bin/locus.js +213 -13
  2. package/package.json +2 -2
package/bin/locus.js CHANGED
@@ -4101,6 +4101,80 @@ ${bold2("Skill Information")}
4101
4101
  process.stderr.write(`
4102
4102
  `);
4103
4103
  }
4104
+ async function searchSkills(query, flags) {
4105
+ if (!query && !flags.tag) {
4106
+ process.stderr.write(`${red2("✗")} Please provide a search query.
4107
+ `);
4108
+ process.stderr.write(` Usage: ${bold2("locus skills search <query>")} or ${bold2("locus skills search --tag <tag>")}
4109
+ `);
4110
+ process.exit(1);
4111
+ }
4112
+ let registry;
4113
+ try {
4114
+ registry = await fetchRegistry();
4115
+ } catch (err) {
4116
+ process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
4117
+ `);
4118
+ process.stderr.write(` ${dim2(err.message)}
4119
+ `);
4120
+ process.exit(1);
4121
+ }
4122
+ const tagFilter = flags.tag?.toLowerCase();
4123
+ const queryLower = query?.toLowerCase() ?? "";
4124
+ const matches = registry.skills.filter((s) => {
4125
+ if (tagFilter) {
4126
+ return s.tags.some((t) => t.toLowerCase() === tagFilter);
4127
+ }
4128
+ const nameMatch = s.name.toLowerCase().includes(queryLower);
4129
+ const descMatch = s.description.toLowerCase().includes(queryLower);
4130
+ const tagMatch = s.tags.some((t) => t.toLowerCase().includes(queryLower));
4131
+ return nameMatch || descMatch || tagMatch;
4132
+ });
4133
+ if (matches.length === 0) {
4134
+ process.stderr.write(`
4135
+ ${yellow2("⚠")} No skills found matching "${bold2(tagFilter || query)}"
4136
+ `);
4137
+ process.stderr.write(` Run ${bold2("locus skills list")} to see all available skills.
4138
+
4139
+ `);
4140
+ return;
4141
+ }
4142
+ const cwd = process.cwd();
4143
+ const lockFile = readLockFile(cwd);
4144
+ process.stderr.write(`
4145
+ ${bold2("Search Results")} for "${cyan2(tagFilter || query)}"
4146
+
4147
+ `);
4148
+ const columns = [
4149
+ { key: "name", header: "Name", minWidth: 12, maxWidth: 24 },
4150
+ { key: "description", header: "Description", minWidth: 20, maxWidth: 44 },
4151
+ {
4152
+ key: "tags",
4153
+ header: "Tags",
4154
+ minWidth: 10,
4155
+ maxWidth: 30,
4156
+ format: (val) => dim2(val.join(", "))
4157
+ },
4158
+ {
4159
+ key: "status",
4160
+ header: "Status",
4161
+ minWidth: 8,
4162
+ maxWidth: 12
4163
+ }
4164
+ ];
4165
+ const rows = matches.map((s) => ({
4166
+ name: cyan2(s.name),
4167
+ description: s.description,
4168
+ tags: s.tags,
4169
+ status: s.name in lockFile.skills ? green("installed") : dim2("available")
4170
+ }));
4171
+ process.stderr.write(`${renderTable(columns, rows)}
4172
+
4173
+ `);
4174
+ process.stderr.write(` ${dim2(`${matches.length} skill(s) found.`)} Install with: ${bold2("locus skills install <name>")}
4175
+
4176
+ `);
4177
+ }
4104
4178
  function printSkillsHelp() {
4105
4179
  process.stderr.write(`
4106
4180
  ${bold2("Usage:")}
@@ -4109,6 +4183,7 @@ ${bold2("Usage:")}
4109
4183
  ${bold2("Subcommands:")}
4110
4184
  ${cyan2("list")} List available skills from the registry
4111
4185
  ${cyan2("list")} ${dim2("--installed")} List locally installed skills
4186
+ ${cyan2("search")} ${dim2("<query>")} Search skills by name, description, or tags
4112
4187
  ${cyan2("install")} ${dim2("<name>")} Install a skill from the registry
4113
4188
  ${cyan2("remove")} ${dim2("<name>")} Remove an installed skill (alias: ${cyan2("uninstall")})
4114
4189
  ${cyan2("update")} ${dim2("[name]")} Update installed skill(s) from registry
@@ -4117,6 +4192,8 @@ ${bold2("Subcommands:")}
4117
4192
  ${bold2("Examples:")}
4118
4193
  locus skills list ${dim2("# Browse available skills")}
4119
4194
  locus skills list --installed ${dim2("# Show installed skills")}
4195
+ locus skills search "code review" ${dim2("# Search by keyword")}
4196
+ locus skills search --tag testing ${dim2("# Search by tag")}
4120
4197
  locus skills install code-review ${dim2("# Install a skill")}
4121
4198
  locus skills remove code-review ${dim2("# Remove a skill")}
4122
4199
  locus skills update ${dim2("# Update all installed skills")}
@@ -4161,10 +4238,15 @@ async function skillsCommand(args, flags) {
4161
4238
  await infoSkill(skillName);
4162
4239
  break;
4163
4240
  }
4241
+ case "search": {
4242
+ const searchQuery = args.slice(1).join(" ");
4243
+ await searchSkills(searchQuery, flags);
4244
+ break;
4245
+ }
4164
4246
  default:
4165
4247
  process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
4166
4248
  `);
4167
- process.stderr.write(` Available: ${bold2("list")}, ${bold2("install")}, ${bold2("remove")} (${bold2("uninstall")}), ${bold2("update")}, ${bold2("info")}
4249
+ process.stderr.write(` Available: ${bold2("list")}, ${bold2("search")}, ${bold2("install")}, ${bold2("remove")} (${bold2("uninstall")}), ${bold2("update")}, ${bold2("info")}
4168
4250
  `);
4169
4251
  process.exit(1);
4170
4252
  }
@@ -8394,6 +8476,105 @@ var init_sprint = __esm(() => {
8394
8476
  init_terminal();
8395
8477
  });
8396
8478
 
8479
+ // src/skills/matcher.ts
8480
+ function matchRelevantSkills(issue, skills) {
8481
+ if (skills.length <= MAX_SKILLS)
8482
+ return skills;
8483
+ const titleTokens = tokenize(issue.title);
8484
+ const bodyTokens = tokenize(issue.body).slice(0, 200);
8485
+ const labelTokens = issue.labels.flatMap((l) => tokenize(l));
8486
+ const scored = skills.map((skill) => {
8487
+ let score = 0;
8488
+ const tagSet = new Set(skill.tags.map((t) => t.toLowerCase()));
8489
+ const descTokens = new Set(tokenize(skill.description));
8490
+ for (const lt of labelTokens) {
8491
+ if (tagSet.has(lt))
8492
+ score += 5;
8493
+ }
8494
+ for (const tw of titleTokens) {
8495
+ if (tagSet.has(tw))
8496
+ score += 3;
8497
+ }
8498
+ for (const bw of bodyTokens) {
8499
+ if (tagSet.has(bw))
8500
+ score += 2;
8501
+ }
8502
+ for (const tw of titleTokens) {
8503
+ if (descTokens.has(tw))
8504
+ score += 1;
8505
+ }
8506
+ for (const bw of bodyTokens) {
8507
+ if (descTokens.has(bw))
8508
+ score += 0.5;
8509
+ }
8510
+ return { skill, score };
8511
+ });
8512
+ scored.sort((a, b) => b.score - a.score);
8513
+ const relevant = scored.filter((s) => s.score >= RELEVANCE_THRESHOLD);
8514
+ if (relevant.length > 0) {
8515
+ return relevant.slice(0, MAX_SKILLS).map((s) => s.skill);
8516
+ }
8517
+ return scored.slice(0, FALLBACK_COUNT).map((s) => s.skill);
8518
+ }
8519
+ function tokenize(text) {
8520
+ if (!text)
8521
+ return [];
8522
+ return text.toLowerCase().replace(/[^a-z0-9\-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
8523
+ }
8524
+ var RELEVANCE_THRESHOLD = 2, MAX_SKILLS = 5, FALLBACK_COUNT = 3, STOP_WORDS;
8525
+ var init_matcher = __esm(() => {
8526
+ STOP_WORDS = new Set([
8527
+ "a",
8528
+ "an",
8529
+ "the",
8530
+ "is",
8531
+ "it",
8532
+ "to",
8533
+ "in",
8534
+ "on",
8535
+ "of",
8536
+ "for",
8537
+ "and",
8538
+ "or",
8539
+ "not",
8540
+ "with",
8541
+ "this",
8542
+ "that",
8543
+ "from",
8544
+ "by",
8545
+ "as",
8546
+ "at",
8547
+ "be",
8548
+ "we",
8549
+ "i",
8550
+ "you",
8551
+ "my",
8552
+ "our",
8553
+ "do",
8554
+ "if",
8555
+ "no",
8556
+ "so",
8557
+ "up",
8558
+ "can",
8559
+ "all",
8560
+ "but",
8561
+ "has",
8562
+ "had",
8563
+ "have",
8564
+ "will",
8565
+ "should",
8566
+ "would",
8567
+ "could",
8568
+ "need",
8569
+ "want",
8570
+ "also",
8571
+ "new",
8572
+ "use",
8573
+ "add",
8574
+ "make"
8575
+ ]);
8576
+ });
8577
+
8397
8578
  // src/core/prompt-builder.ts
8398
8579
  import { execSync as execSync9 } from "node:child_process";
8399
8580
  import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
@@ -8401,7 +8582,7 @@ import { join as join17 } from "node:path";
8401
8582
  function buildExecutionPrompt(ctx) {
8402
8583
  const sections = [];
8403
8584
  sections.push(buildSystemContext(ctx.projectRoot));
8404
- const skills = buildSkillsContext(ctx.projectRoot);
8585
+ const skills = buildSkillsContext(ctx.projectRoot, ctx.issue);
8405
8586
  if (skills)
8406
8587
  sections.push(skills);
8407
8588
  sections.push(buildTaskContext(ctx.issue, ctx.issueComments));
@@ -8419,7 +8600,7 @@ function buildExecutionPrompt(ctx) {
8419
8600
  function buildFeedbackPrompt(ctx) {
8420
8601
  const sections = [];
8421
8602
  sections.push(buildSystemContext(ctx.projectRoot));
8422
- const skills = buildSkillsContext(ctx.projectRoot);
8603
+ const skills = buildSkillsContext(ctx.projectRoot, ctx.issue);
8423
8604
  if (skills)
8424
8605
  sections.push(skills);
8425
8606
  sections.push(buildTaskContext(ctx.issue));
@@ -8652,7 +8833,7 @@ function buildFeedbackInstructions() {
8652
8833
  6. When done, summarize what you changed in response to each comment.
8653
8834
  </instructions>`;
8654
8835
  }
8655
- function buildSkillsContext(projectRoot) {
8836
+ function buildSkillsContext(projectRoot, issue) {
8656
8837
  const skillsDir = join17(projectRoot, CLAUDE_SKILLS_DIR);
8657
8838
  if (!existsSync17(skillsDir))
8658
8839
  return null;
@@ -8664,21 +8845,35 @@ function buildSkillsContext(projectRoot) {
8664
8845
  }
8665
8846
  if (dirs.length === 0)
8666
8847
  return null;
8667
- const entries = [];
8848
+ let allSkills = [];
8668
8849
  for (const dir of dirs) {
8669
8850
  const skillPath = join17(skillsDir, dir, "SKILL.md");
8670
8851
  const content = readFileSafe(skillPath);
8671
8852
  if (!content)
8672
8853
  continue;
8673
8854
  const fm = parseFrontmatter(content);
8674
- const name = fm.name || dir;
8675
- const description = fm.description || "";
8676
- entries.push(`- **${name}**: ${description}`);
8855
+ allSkills.push({
8856
+ dir,
8857
+ name: fm.name || dir,
8858
+ description: fm.description || "",
8859
+ tags: fm.tags ? fm.tags.split(",").map((t) => t.trim()) : []
8860
+ });
8677
8861
  }
8678
- if (entries.length === 0)
8862
+ if (allSkills.length === 0)
8679
8863
  return null;
8864
+ let selectedSkills = allSkills;
8865
+ if (issue) {
8866
+ const issueCtx = {
8867
+ title: issue.title,
8868
+ body: issue.body || "",
8869
+ labels: issue.labels
8870
+ };
8871
+ const relevant = matchRelevantSkills(issueCtx, allSkills);
8872
+ selectedSkills = allSkills.filter((s) => relevant.some((r) => r.name === s.name));
8873
+ }
8874
+ const entries = selectedSkills.map((s) => `- **${s.name}**: ${s.description}`);
8680
8875
  return `<installed-skills>
8681
- The following skills are installed in this project. If a skill is relevant to the current task, read its full instructions from \`.claude/skills/<name>/SKILL.md\` before starting work.
8876
+ The following skills are installed and relevant to this task. Read the full instructions from \`.claude/skills/<name>/SKILL.md\` before using a skill.
8682
8877
 
8683
8878
  ${entries.join(`
8684
8879
  `)}
@@ -8695,9 +8890,13 @@ function parseFrontmatter(content) {
8695
8890
  if (idx === -1)
8696
8891
  continue;
8697
8892
  const key = line.slice(0, idx).trim();
8698
- const val = line.slice(idx + 1).trim();
8699
- if (key && val)
8700
- result[key] = val;
8893
+ let val = line.slice(idx + 1).trim();
8894
+ if (!key || !val)
8895
+ continue;
8896
+ if (val.startsWith("[") && val.endsWith("]")) {
8897
+ val = val.slice(1, -1);
8898
+ }
8899
+ result[key] = val;
8701
8900
  }
8702
8901
  return result;
8703
8902
  }
@@ -8712,6 +8911,7 @@ function readFileSafe(path) {
8712
8911
  }
8713
8912
  var MEMORY_MAX_CHARS = 4000;
8714
8913
  var init_prompt_builder = __esm(() => {
8914
+ init_matcher();
8715
8915
  init_memory();
8716
8916
  });
8717
8917
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "dependencies": {},
38
38
  "devDependencies": {
39
- "@locusai/sdk": "^0.26.0",
39
+ "@locusai/sdk": "^0.26.1",
40
40
  "@types/bun": "latest",
41
41
  "typescript": "^5.8.3"
42
42
  },