@cruxy/cli 0.1.1 → 0.3.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/README.md +35 -1
- package/dist/cli/commands/index.d.ts +7 -0
- package/dist/cli/commands/index.js +59 -0
- package/dist/cli/commands/skills.d.ts +8 -0
- package/dist/cli/commands/skills.js +51 -0
- package/dist/cli/program.js +4 -0
- package/dist/config/schema.d.ts +199 -0
- package/dist/config/schema.js +55 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +13 -0
- package/dist/indexing/chunker.d.ts +28 -0
- package/dist/indexing/chunker.js +65 -0
- package/dist/indexing/embedder.d.ts +98 -0
- package/dist/indexing/embedder.js +140 -0
- package/dist/indexing/index.d.ts +9 -0
- package/dist/indexing/index.js +9 -0
- package/dist/indexing/indexer.d.ts +45 -0
- package/dist/indexing/indexer.js +104 -0
- package/dist/indexing/retriever.d.ts +32 -0
- package/dist/indexing/retriever.js +53 -0
- package/dist/indexing/service.d.ts +49 -0
- package/dist/indexing/service.js +132 -0
- package/dist/indexing/store.d.ts +103 -0
- package/dist/indexing/store.js +279 -0
- package/dist/indexing/types.d.ts +71 -0
- package/dist/indexing/types.js +6 -0
- package/dist/indexing/util.d.ts +34 -0
- package/dist/indexing/util.js +97 -0
- package/dist/indexing/walker.d.ts +42 -0
- package/dist/indexing/walker.js +166 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/loader.d.ts +42 -0
- package/dist/skills/loader.js +0 -0
- package/dist/skills/parser.d.ts +29 -0
- package/dist/skills/parser.js +90 -0
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +92 -0
- package/dist/skills/types.d.ts +94 -0
- package/dist/skills/types.js +21 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/list-skills.d.ts +9 -0
- package/dist/tools/list-skills.js +34 -0
- package/dist/tools/load-skill.d.ts +21 -0
- package/dist/tools/load-skill.js +49 -0
- package/dist/tools/registry.js +6 -0
- package/dist/tools/search-codebase.d.ts +25 -0
- package/dist/tools/search-codebase.js +70 -0
- package/package.json +6 -2
- package/skills/git-commit/SKILL.md +60 -0
- package/skills/using-skills/SKILL.md +62 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Tool } from "./types.js";
|
|
3
|
+
declare const parameters: z.ZodObject<{
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
name: string;
|
|
7
|
+
}, {
|
|
8
|
+
name: string;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Load a skill's full instructions on demand — the expensive half of
|
|
12
|
+
* progressive disclosure. Returns the markdown body plus the absolute asset root
|
|
13
|
+
* and the relative paths of any `scripts/` and `reference/` files, so the agent
|
|
14
|
+
* can read or run them through the existing read_file / run_command tools.
|
|
15
|
+
*
|
|
16
|
+
* Read-only — no approval. Loading a skill never executes anything: scripts are
|
|
17
|
+
* surfaced as paths only and run, if at all, through run_command (which applies
|
|
18
|
+
* the normal approval gate).
|
|
19
|
+
*/
|
|
20
|
+
export declare const loadSkillTool: Tool<typeof parameters>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getSkillService } from "../skills/index.js";
|
|
3
|
+
const parameters = z.object({
|
|
4
|
+
name: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe("The exact name of the skill to load (from list_skills)."),
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Load a skill's full instructions on demand — the expensive half of
|
|
11
|
+
* progressive disclosure. Returns the markdown body plus the absolute asset root
|
|
12
|
+
* and the relative paths of any `scripts/` and `reference/` files, so the agent
|
|
13
|
+
* can read or run them through the existing read_file / run_command tools.
|
|
14
|
+
*
|
|
15
|
+
* Read-only — no approval. Loading a skill never executes anything: scripts are
|
|
16
|
+
* surfaced as paths only and run, if at all, through run_command (which applies
|
|
17
|
+
* the normal approval gate).
|
|
18
|
+
*/
|
|
19
|
+
export const loadSkillTool = {
|
|
20
|
+
name: "load_skill",
|
|
21
|
+
description: "Load the full instructions for a skill by name (discover names with list_skills). Returns the skill's markdown body plus the paths of any bundled scripts/reference files. Read-only and requires no approval — it never runs anything; use run_command to execute a script the skill describes.",
|
|
22
|
+
parameters,
|
|
23
|
+
async execute(input, ctx) {
|
|
24
|
+
try {
|
|
25
|
+
const skill = await getSkillService(ctx.cwd, ctx.logger).load(input.name);
|
|
26
|
+
return { ok: true, output: formatSkill(skill) };
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return { ok: false, error: err.message };
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
/** Render a loaded skill: instructions, then an asset reference footer. */
|
|
34
|
+
function formatSkill(skill) {
|
|
35
|
+
const parts = [
|
|
36
|
+
`# Skill: ${skill.name} (${skill.source})`,
|
|
37
|
+
"",
|
|
38
|
+
skill.body.trim(),
|
|
39
|
+
];
|
|
40
|
+
const footer = [`Asset root: ${skill.assetRoot}`];
|
|
41
|
+
if (skill.scripts.length > 0) {
|
|
42
|
+
footer.push(`Scripts (run via run_command — not auto-executed): ${skill.scripts.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
if (skill.references.length > 0) {
|
|
45
|
+
footer.push(`References (read via read_file): ${skill.references.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
parts.push("", "---", footer.join("\n"));
|
|
48
|
+
return parts.join("\n");
|
|
49
|
+
}
|
package/dist/tools/registry.js
CHANGED
|
@@ -3,6 +3,9 @@ import { listFilesTool } from "./list-files.js";
|
|
|
3
3
|
import { gitStatusTool } from "./git-status.js";
|
|
4
4
|
import { readFileTool, writeFileTool, editFileTool, applyPatchTool, globTool, grepFilesTool, } from "./file/index.js";
|
|
5
5
|
import { runCommandTool } from "./shell/index.js";
|
|
6
|
+
import { searchCodebaseTool } from "./search-codebase.js";
|
|
7
|
+
import { listSkillsTool } from "./list-skills.js";
|
|
8
|
+
import { loadSkillTool } from "./load-skill.js";
|
|
6
9
|
/**
|
|
7
10
|
* In-memory catalogue of the tools available to the agent. Names are unique;
|
|
8
11
|
* `toToolSpecs()` projects the catalogue into the `@cruxy/sdk` wire format.
|
|
@@ -59,5 +62,8 @@ export function buildDefaultRegistry() {
|
|
|
59
62
|
registry.register(grepFilesTool);
|
|
60
63
|
registry.register(gitStatusTool);
|
|
61
64
|
registry.register(runCommandTool);
|
|
65
|
+
registry.register(searchCodebaseTool);
|
|
66
|
+
registry.register(listSkillsTool);
|
|
67
|
+
registry.register(loadSkillTool);
|
|
62
68
|
return registry;
|
|
63
69
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Tool } from "./types.js";
|
|
3
|
+
declare const parameters: z.ZodObject<{
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
k: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
pathGlob: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
query: string;
|
|
9
|
+
k?: number | undefined;
|
|
10
|
+
pathGlob?: string | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
query: string;
|
|
13
|
+
k?: number | undefined;
|
|
14
|
+
pathGlob?: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Semantic search over the project's local code index (C.17). Read-only — no
|
|
18
|
+
* approval — like read_file and grep_files. The index is built/refreshed lazily
|
|
19
|
+
* on first use; results are ranked by cosine similarity and token-budgeted.
|
|
20
|
+
*
|
|
21
|
+
* Complements `grep_files`: prefer this for conceptual "where / how does X work"
|
|
22
|
+
* questions, and grep for exact strings or symbols.
|
|
23
|
+
*/
|
|
24
|
+
export declare const searchCodebaseTool: Tool<typeof parameters>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getIndexService } from "../indexing/index.js";
|
|
3
|
+
/** Hard cap on `k`, mirroring the retriever. */
|
|
4
|
+
const MAX_K = 50;
|
|
5
|
+
const parameters = z.object({
|
|
6
|
+
query: z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.describe("A natural-language description of the code you're looking for (e.g. 'where are tool results fed back to the model', 'JWT verification'). Concepts work better than exact tokens."),
|
|
10
|
+
k: z
|
|
11
|
+
.number()
|
|
12
|
+
.int()
|
|
13
|
+
.positive()
|
|
14
|
+
.max(MAX_K)
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Number of results to return (default 8)."),
|
|
17
|
+
pathGlob: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Optional glob to restrict results by path, e.g. 'src/**/*.ts' or 'packages/cli/**'."),
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Semantic search over the project's local code index (C.17). Read-only — no
|
|
24
|
+
* approval — like read_file and grep_files. The index is built/refreshed lazily
|
|
25
|
+
* on first use; results are ranked by cosine similarity and token-budgeted.
|
|
26
|
+
*
|
|
27
|
+
* Complements `grep_files`: prefer this for conceptual "where / how does X work"
|
|
28
|
+
* questions, and grep for exact strings or symbols.
|
|
29
|
+
*/
|
|
30
|
+
export const searchCodebaseTool = {
|
|
31
|
+
name: "search_codebase",
|
|
32
|
+
description: "Semantically search the project's code index for the snippets most relevant to a natural-language query. Returns ranked matches as 'path:startLine-endLine (score)' with a code snippet. Read-only, no approval. Prefer this for conceptual 'where is / how does X work' questions; use grep_files for exact strings or symbol names.",
|
|
33
|
+
parameters,
|
|
34
|
+
async execute(input, ctx) {
|
|
35
|
+
if (!ctx.config.index.enabled) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: "codebase indexing is disabled (set index.enabled = true to use search_codebase)",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const service = await getIndexService(ctx.cwd, ctx.config, ctx.logger);
|
|
43
|
+
const hits = await service.search({
|
|
44
|
+
query: input.query,
|
|
45
|
+
k: input.k,
|
|
46
|
+
pathGlob: input.pathGlob,
|
|
47
|
+
});
|
|
48
|
+
if (hits.length === 0) {
|
|
49
|
+
return { ok: true, output: "(no matches in the codebase index)" };
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, output: formatHits(hits) };
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return { ok: false, error: err.message };
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
/** Render hits as a compact, model-readable block. */
|
|
59
|
+
function formatHits(hits) {
|
|
60
|
+
return hits
|
|
61
|
+
.map((hit) => {
|
|
62
|
+
const header = `${hit.path}:${hit.startLine}-${hit.endLine} (score ${hit.score.toFixed(3)})`;
|
|
63
|
+
const body = hit.snippet
|
|
64
|
+
.split("\n")
|
|
65
|
+
.map((line) => ` ${line}`)
|
|
66
|
+
.join("\n");
|
|
67
|
+
return `${header}\n${body}`;
|
|
68
|
+
})
|
|
69
|
+
.join("\n\n");
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxy/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "an agentic coding CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cruxy": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"skills"
|
|
11
12
|
],
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=20"
|
|
@@ -28,7 +29,9 @@
|
|
|
28
29
|
"directory": "packages/cli"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
32
|
+
"better-sqlite3": "^12.11.1",
|
|
31
33
|
"commander": "^12.1.0",
|
|
34
|
+
"fastembed": "^2.1.0",
|
|
32
35
|
"picocolors": "^1.1.1",
|
|
33
36
|
"tinyglobby": "^0.2.10",
|
|
34
37
|
"zod": "^3.23.8",
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
"@cruxy/sdk": "0.1.0"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
42
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
39
43
|
"@types/node": "^22.10.0",
|
|
40
44
|
"tsx": "^4.19.2",
|
|
41
45
|
"typescript": "^5.7.2",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-commit
|
|
3
|
+
description: Write a conventional commit message that passes this repo's commitlint. Use when committing, writing a commit message, or opening a PR in cruxy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing commit messages for cruxy
|
|
7
|
+
|
|
8
|
+
This repo enforces [Conventional Commits](https://www.conventionalcommits.org)
|
|
9
|
+
via commitlint (a husky `commit-msg` hook) plus changesets. Follow this to get a
|
|
10
|
+
message that passes on the first try.
|
|
11
|
+
|
|
12
|
+
## Format
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
<type>(<scope>): <subject>
|
|
16
|
+
|
|
17
|
+
<body>
|
|
18
|
+
|
|
19
|
+
<footer>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **type** — one of `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`,
|
|
23
|
+
`build`, `ci`, `style`, `revert`.
|
|
24
|
+
- **scope** _(optional, but if present must be in the allow-list)_ —
|
|
25
|
+
`cli`, `sdk`, `ui`, `api`, `auth`, `billing`, `web`, `dashboard`, `docs`,
|
|
26
|
+
`ide`, `infra`, `pricing`, `repo`, `ci`. The canonical list lives in
|
|
27
|
+
`commitlint.config.cjs` (`scope-enum`).
|
|
28
|
+
- **subject** — imperative mood, no trailing period.
|
|
29
|
+
|
|
30
|
+
## The rule people trip on
|
|
31
|
+
|
|
32
|
+
**The subject must NOT start with a capital letter.** commitlint's
|
|
33
|
+
`subject-case` rejects sentence-case / start-case / pascal-case / upper-case, so
|
|
34
|
+
a subject beginning with an uppercase letter fails the hook (and CI). A tag like
|
|
35
|
+
`C.18` cannot be the first token.
|
|
36
|
+
|
|
37
|
+
- ✅ `feat(cli): codebase indexing + search_codebase (C.18)`
|
|
38
|
+
- ❌ `feat(cli): C.18 codebase indexing` — capital `C` ⇒ sentence-case ⇒ rejected.
|
|
39
|
+
|
|
40
|
+
Put any phase/issue tag in a parenthetical at the end, or lowercase the lead.
|
|
41
|
+
|
|
42
|
+
## Body and footer
|
|
43
|
+
|
|
44
|
+
- Wrap the body at ~72 columns; explain _what_ and _why_, not _how_.
|
|
45
|
+
- A breaking change uses `!` after the type/scope (`feat(cli)!: …`) and/or a
|
|
46
|
+
`BREAKING CHANGE:` footer.
|
|
47
|
+
- Co-author trailers go in the footer, e.g.
|
|
48
|
+
`Co-Authored-By: Name <email>`.
|
|
49
|
+
|
|
50
|
+
## Don't forget the changeset
|
|
51
|
+
|
|
52
|
+
User-facing changes need a changeset: run `pnpm changeset` (or add a file under
|
|
53
|
+
`.changeset/`) describing the change and the semver bump for the affected
|
|
54
|
+
package (e.g. `@cruxy/cli` minor).
|
|
55
|
+
|
|
56
|
+
## Quick check
|
|
57
|
+
|
|
58
|
+
If a commit is rejected with `subject must not be sentence-case … [subject-case]`,
|
|
59
|
+
lowercase the first word of the subject (or move the capitalized tag into a
|
|
60
|
+
trailing parenthetical) and recommit.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: using-skills
|
|
3
|
+
description: How cruxy skills work and how to author a SKILL.md. Use when the user asks about skills or wants to create or edit one.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skills in cruxy
|
|
7
|
+
|
|
8
|
+
A **skill** is a directory containing a `SKILL.md` — YAML frontmatter plus a
|
|
9
|
+
markdown instructions body — that packages reusable, task-specific know-how for
|
|
10
|
+
the agent. Discover skills with the `list_skills` tool and pull a skill's full
|
|
11
|
+
instructions with `load_skill` when a task matches its description.
|
|
12
|
+
|
|
13
|
+
## Progressive disclosure
|
|
14
|
+
|
|
15
|
+
Only each skill's `name` and `description` are visible by default (a cheap
|
|
16
|
+
catalog). The full body is loaded **on demand** via `load_skill`, so a large
|
|
17
|
+
library of skills costs almost nothing until one is actually used. Write the
|
|
18
|
+
`description` so the agent can tell, from one line, _when_ to reach for the
|
|
19
|
+
skill.
|
|
20
|
+
|
|
21
|
+
## Authoring a SKILL.md
|
|
22
|
+
|
|
23
|
+
Create `skills/<name>/SKILL.md`:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
---
|
|
27
|
+
name: <name>
|
|
28
|
+
description: <one line — what it does and when to use it>
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# instructions
|
|
32
|
+
…the body the agent reads after load_skill…
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Rules (all enforced, fail-loud):
|
|
36
|
+
|
|
37
|
+
- `name` is kebab-case (`^[a-z0-9][a-z0-9-]*$`) and **must equal the directory
|
|
38
|
+
name**.
|
|
39
|
+
- `description` is required and non-empty.
|
|
40
|
+
- Frontmatter is strict: unknown keys, missing fields, or malformed YAML are
|
|
41
|
+
reported in `cruxy skills --status` and the skill is excluded from the
|
|
42
|
+
catalog.
|
|
43
|
+
|
|
44
|
+
## Sources and precedence
|
|
45
|
+
|
|
46
|
+
Skills are discovered from three layers; on a name collision the higher layer
|
|
47
|
+
wins:
|
|
48
|
+
|
|
49
|
+
1. **project** — `<cwd>/.cruxy/skills/`
|
|
50
|
+
2. **user** — `~/.cruxy/skills/`
|
|
51
|
+
3. **builtin** — shipped with the CLI
|
|
52
|
+
|
|
53
|
+
Run `cruxy skills` to see the resolved catalog, or `cruxy skills --status` to see
|
|
54
|
+
the source directories and any validation errors.
|
|
55
|
+
|
|
56
|
+
## Bundled assets
|
|
57
|
+
|
|
58
|
+
A skill may include `scripts/` and `reference/` subdirectories. `load_skill`
|
|
59
|
+
returns their paths, but **nothing is auto-executed** — the agent runs a script
|
|
60
|
+
through `run_command` (which goes through the normal approval gate) and reads a
|
|
61
|
+
reference through `read_file`. Skills are data, not a privileged execution path,
|
|
62
|
+
and may only reference files inside their own directory.
|