@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.
- package/bin/locus.js +213 -13
- 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
|
-
|
|
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
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
8699
|
-
if (key
|
|
8700
|
-
|
|
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.
|
|
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.
|
|
39
|
+
"@locusai/sdk": "^0.26.1",
|
|
40
40
|
"@types/bun": "latest",
|
|
41
41
|
"typescript": "^5.8.3"
|
|
42
42
|
},
|