@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.
Files changed (52) hide show
  1. package/README.md +35 -1
  2. package/dist/cli/commands/index.d.ts +7 -0
  3. package/dist/cli/commands/index.js +59 -0
  4. package/dist/cli/commands/skills.d.ts +8 -0
  5. package/dist/cli/commands/skills.js +51 -0
  6. package/dist/cli/program.js +4 -0
  7. package/dist/config/schema.d.ts +199 -0
  8. package/dist/config/schema.js +55 -0
  9. package/dist/constants.d.ts +13 -0
  10. package/dist/constants.js +13 -0
  11. package/dist/indexing/chunker.d.ts +28 -0
  12. package/dist/indexing/chunker.js +65 -0
  13. package/dist/indexing/embedder.d.ts +98 -0
  14. package/dist/indexing/embedder.js +140 -0
  15. package/dist/indexing/index.d.ts +9 -0
  16. package/dist/indexing/index.js +9 -0
  17. package/dist/indexing/indexer.d.ts +45 -0
  18. package/dist/indexing/indexer.js +104 -0
  19. package/dist/indexing/retriever.d.ts +32 -0
  20. package/dist/indexing/retriever.js +53 -0
  21. package/dist/indexing/service.d.ts +49 -0
  22. package/dist/indexing/service.js +132 -0
  23. package/dist/indexing/store.d.ts +103 -0
  24. package/dist/indexing/store.js +279 -0
  25. package/dist/indexing/types.d.ts +71 -0
  26. package/dist/indexing/types.js +6 -0
  27. package/dist/indexing/util.d.ts +34 -0
  28. package/dist/indexing/util.js +97 -0
  29. package/dist/indexing/walker.d.ts +42 -0
  30. package/dist/indexing/walker.js +166 -0
  31. package/dist/skills/index.d.ts +4 -0
  32. package/dist/skills/index.js +4 -0
  33. package/dist/skills/loader.d.ts +42 -0
  34. package/dist/skills/loader.js +0 -0
  35. package/dist/skills/parser.d.ts +29 -0
  36. package/dist/skills/parser.js +90 -0
  37. package/dist/skills/service.d.ts +41 -0
  38. package/dist/skills/service.js +92 -0
  39. package/dist/skills/types.d.ts +94 -0
  40. package/dist/skills/types.js +21 -0
  41. package/dist/tools/index.d.ts +3 -0
  42. package/dist/tools/index.js +3 -0
  43. package/dist/tools/list-skills.d.ts +9 -0
  44. package/dist/tools/list-skills.js +34 -0
  45. package/dist/tools/load-skill.d.ts +21 -0
  46. package/dist/tools/load-skill.js +49 -0
  47. package/dist/tools/registry.js +6 -0
  48. package/dist/tools/search-codebase.d.ts +25 -0
  49. package/dist/tools/search-codebase.js +70 -0
  50. package/package.json +6 -2
  51. package/skills/git-commit/SKILL.md +60 -0
  52. 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
+ }
@@ -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.1.1",
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.