mini-coder 0.0.4 → 0.0.5
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 +25 -4
- package/code-quality-issues.md +135 -0
- package/dist/mc.js +382 -81
- package/docs/custom-agents.md +70 -0
- package/docs/custom-commands.md +133 -0
- package/docs/skills.md +71 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,10 @@ I can also connect to **MCP servers** (like Exa for web search), giving you supe
|
|
|
53
53
|
- **Multi-provider** — set `OPENCODE_API_KEY` for Zen, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, or just run Ollama locally. I auto-discover whatever's available.
|
|
54
54
|
- **Session memory** — conversations are saved in a local SQLite database. Resume where you left off with `-c` or pick a specific session with `-r <id>`.
|
|
55
55
|
- **Shell integration** — prefix with `!` to run shell commands inline. Use `@` to reference files in your prompt (with Tab completion).
|
|
56
|
-
- **Slash commands** — `/model` to switch models, `/plan` for read-only thinking mode, `/ralph` for autonomous looping
|
|
56
|
+
- **Slash commands** — `/model` to switch models, `/plan` for read-only thinking mode, `/ralph` for autonomous looping, `/review` for a code review, `/undo` to roll back a turn, `/new` for a clean session, `/mcp` to manage MCP servers. See all with `/help`.
|
|
57
|
+
- **Custom commands** — drop a `.md` file in `.agents/commands/` and it becomes a `/command`. Supports argument placeholders (`$ARGUMENTS`, `$1`…`$9`) and shell interpolation (`` !`cmd` ``). Global commands live in `~/.agents/commands/`. Custom commands take precedence over built-ins. → [docs/custom-commands.md](docs/custom-commands.md)
|
|
58
|
+
- **Custom agents** — drop a `.md` file in `.agents/agents/` (or `~/.agents/agents/` globally) and reference it with `@agent-name` in your prompt. The agent runs in its own context window with a custom system prompt and optional model override. → [docs/custom-agents.md](docs/custom-agents.md)
|
|
59
|
+
- **Skills** — place a `SKILL.md` in `.agents/skills/<name>/` and inject it into any prompt with `@skill-name`. Skills are *never* auto-loaded — always explicit. → [docs/skills.md](docs/skills.md)
|
|
57
60
|
- **Post-tool hooks** — drop an executable at `.agents/hooks/post-<tool>` and I'll run it after every matching tool call.
|
|
58
61
|
- **Beautiful, minimal output** — diffs for edits, formatted trees for file searches, a live status bar with model, git branch, and token counts.
|
|
59
62
|
- **16 ANSI colors only** — my output inherits *your* terminal theme. Dark mode, light mode, Solarized, Gruvbox — I fit right in.
|
|
@@ -64,17 +67,35 @@ I can also connect to **MCP servers** (like Exa for web search), giving you supe
|
|
|
64
67
|
|
|
65
68
|
- **I eat my own dog food.** I was built *by* a mini-coder agent. It's agents all the way down. 🐢
|
|
66
69
|
- **I'm tiny but mighty.** The whole runtime is [Bun.js](https://bun.com) — fast startup, native TypeScript, and a built-in SQLite driver.
|
|
67
|
-
- **I respect existing conventions.** Hook scripts live in `.agents/hooks/`, context in `AGENTS.md` or `CLAUDE.md
|
|
70
|
+
- **I respect existing conventions.** Hook scripts live in `.agents/hooks/`, context in `AGENTS.md` or `CLAUDE.md`, commands in `.agents/commands/`, agents in `.agents/agents/`, skills in `.agents/skills/` — I follow the ecosystem instead of inventing new standards.
|
|
68
71
|
- **I spin while I think.** ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ (It's the little things.)
|
|
69
72
|
- **I can clone myself.** The `subagent` tool lets me spin up parallel instances of myself to tackle independent subtasks simultaneously. Divide and conquer! (Up to 3 levels deep.)
|
|
70
73
|
|
|
71
74
|
---
|
|
72
75
|
|
|
76
|
+
## 📁 The `.agents` folder
|
|
77
|
+
|
|
78
|
+
mini-coder follows the [`.agents` convention](https://github.com/agentsmd/agents) used across the AI coding tool ecosystem. Drop files in `.agents/` to extend behaviour for the current repo, or `~/.agents/` to apply them globally.
|
|
79
|
+
|
|
80
|
+
| Path | What it does |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `.agents/commands/*.md` | Custom slash commands (`/name`) |
|
|
83
|
+
| `.agents/agents/*.md` | Custom agents (`@name`) |
|
|
84
|
+
| `.agents/skills/<name>/SKILL.md` | Reusable skill instructions (`@name`) |
|
|
85
|
+
| `.agents/hooks/post-<tool>` | Scripts run after a tool call |
|
|
86
|
+
| `AGENTS.md` | Project context injected into every system prompt |
|
|
87
|
+
|
|
88
|
+
Local always overrides global. The same `~/.agents/` folder is shared with Claude Code, Opencode, and other compatible tools — skills and agents you write once work everywhere.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
|
|
73
93
|
## 🚀 Getting Started
|
|
74
94
|
|
|
75
95
|
```bash
|
|
76
|
-
# Install
|
|
77
|
-
bun
|
|
96
|
+
# Install from npm
|
|
97
|
+
bun add -g mini-coder
|
|
98
|
+
# or: npm install -g mini-coder
|
|
78
99
|
|
|
79
100
|
# Set your provider key (pick one — or run Ollama locally)
|
|
80
101
|
export OPENCODE_API_KEY=your-zen-key # recommended
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Code Quality Issues
|
|
2
|
+
|
|
3
|
+
## 1. 🔴 Duplicate Code — `parseFrontmatter` tripled across modules
|
|
4
|
+
|
|
5
|
+
`parseFrontmatter` (including its `Frontmatter` interface, `FM_RE` regex and YAML key-parsing loop) is **copy-pasted verbatim** in three files:
|
|
6
|
+
|
|
7
|
+
- `src/cli/agents.ts` (lines 20–45)
|
|
8
|
+
- `src/cli/custom-commands.ts` (lines 20–45)
|
|
9
|
+
- `src/cli/skills.ts` has a partial version `parseSkillMeta` that reads `name` and `description` from the same format.
|
|
10
|
+
|
|
11
|
+
The agents and custom-commands versions are byte-for-byte identical — same regex, same loop, same trim/strip-quotes, same `description`/`model` key handling. The only difference is the struct they populate. Fix: extract into a shared `src/cli/frontmatter.ts` utility and import it in all three.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. 🔴 Duplicate Code — `loadFromDir` pattern tripled
|
|
16
|
+
|
|
17
|
+
The `loadFromDir` function in `agents.ts` and `custom-commands.ts` is essentially the same skeleton: `existsSync` check → `readdirSync` → `.endsWith(".md")` filter → `readFileSync` → `parseFrontmatter` → `Map.set`. The `loadSkills` version differs only in that it looks for `SKILL.md` inside subdirectories instead of `.md` flat files, but the rest of the structure is the same. Same for the public `loadXxx(cwd)` function: three identical "merge global + local, local wins" patterns.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 3. 🔴 Duplicate Code — `homedir()`-based `cwdDisplay` computed in two places in `agent.ts`
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// buildSystemPrompt (line 98-100)
|
|
25
|
+
const cwdDisplay = cwd.startsWith(homedir())
|
|
26
|
+
? `~${cwd.slice(homedir().length)}`
|
|
27
|
+
: cwd;
|
|
28
|
+
|
|
29
|
+
// renderStatusBarForSession (line 646-648)
|
|
30
|
+
const cwdDisplay = cwd.startsWith(homedir())
|
|
31
|
+
? `~${cwd.slice(homedir().length)}`
|
|
32
|
+
: cwd;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Also duplicated in `session/manager.ts` (line 53–55) and `cli/output.ts` already has `HOME = homedir()` cached at the top. This pattern should be a small helper function, e.g. `tildePath(p: string): string`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 4. 🔴 Inlined `import()` calls — violates project rule
|
|
40
|
+
|
|
41
|
+
The rule says **"Do not inline `import` calls"**. There are 12 occurrences of dynamic inline type imports:
|
|
42
|
+
|
|
43
|
+
- `src/agent/agent.ts` lines 197, 224, 673 — `import("../tools/subagent.ts").SubagentOutput`, `SubagentToolEntry`, `CoreMessage`
|
|
44
|
+
- `src/agent/tools.ts` line 104 — `import("../tools/subagent.ts").SubagentOutput`
|
|
45
|
+
- `src/cli/commands.ts` line 44 — `import("../tools/subagent.ts").SubagentOutput`
|
|
46
|
+
- `src/cli/output.ts` lines 332, 342, 512, 579, 597 — `SubagentToolEntry`, `SubagentOutput`, `CoreMessage`
|
|
47
|
+
- `src/llm-api/types.ts` line 81 — `import("../llm-api/turn.ts").CoreMessage`
|
|
48
|
+
- `src/llm-api/turn.ts` line 32 — `import("ai").FlexibleSchema<unknown>`
|
|
49
|
+
|
|
50
|
+
All of these should be top-level `import type` statements.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 5. 🟡 Dead code — `userMessage` in `turn.ts` is never called
|
|
55
|
+
|
|
56
|
+
`src/llm-api/turn.ts` line 185 exports `userMessage(text: string): CoreMessage`. It is never imported or called anywhere in the codebase. It should be removed.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 6. 🟡 Dead code — `availableProviders` imported but never used
|
|
61
|
+
|
|
62
|
+
`src/cli/commands.ts` imports `availableProviders` from `providers.ts` (line 3) but it is never referenced anywhere in that file (only `fetchAvailableModels` is used). This is an unused import.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 7. 🟡 Dead code — `saveMessage` (singular) exported but never used
|
|
67
|
+
|
|
68
|
+
`src/session/db.ts` exports `saveMessage` (single-message variant, line 191). The entire codebase always calls `saveMessages` (plural). `saveMessage` has no callers and should be removed.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 8. 🟡 Dead code — `updateSessionTitle` and `deleteSession` exported but never called
|
|
73
|
+
|
|
74
|
+
`src/session/db.ts` exports `updateSessionTitle` (line 161) and `deleteSession` (line 185). Neither appears in any other file. They may be future API surface, but currently they are dead exports.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 9. 🟡 Dead code — most of `src/llm-api/types.ts` is orphaned
|
|
79
|
+
|
|
80
|
+
`ProviderConfig`, `MessageRole`, `TextContent`, `ToolCallContent`, `ToolResultContent`, `MessageContent`, and `Message` are all defined and exported in `types.ts` but **never imported anywhere**. The codebase uses `CoreMessage` from `turn.ts` directly for all message handling. The only things from `types.ts` that are actually used are `ToolDef` and the `TurnEvent` family. The unused types should be removed.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 10. 🟡 Unused import — `relative` in `session/manager.ts`
|
|
85
|
+
|
|
86
|
+
`src/session/manager.ts` line 2 imports `relative` from `"node:path"` but it is never used anywhere in the file.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 11. 🟡 Unused import — `PREFIX` in `session/manager.ts`
|
|
91
|
+
|
|
92
|
+
`src/session/manager.ts` line 4 imports `PREFIX` from `"../cli/output.ts"` but it is not used anywhere in the file (only `writeln` and `c` from `yoctocolors` are used in `printSessionList`).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 12. 🟡 Bug-prone — `zenGoogle` ignores its parameter and recreates provider on each call
|
|
97
|
+
|
|
98
|
+
`providers.ts` line 78: `zenGoogle(modelId: string)` takes `modelId` but only uses it to pass to `createGoogleGenerativeAI` (which doesn't use it — the model ID is passed to the returned function). Also unlike the other zen providers, `zenGoogle` doesn't memoize — it creates a new `createGoogleGenerativeAI` instance on every call. This is inconsistent and wasteful.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 13. 🟡 Minor — double `homedir()` call per `cwdDisplay` computation
|
|
103
|
+
|
|
104
|
+
In `buildSystemPrompt` and `renderStatusBarForSession` (agent.ts), `homedir()` is called twice inline:
|
|
105
|
+
```ts
|
|
106
|
+
cwd.startsWith(homedir()) ? `~${cwd.slice(homedir().length)}` : cwd
|
|
107
|
+
```
|
|
108
|
+
`homedir()` is cheap but its result is constant — it should be captured once (as `output.ts` already does with `const HOME = homedir()`).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 14. 🟢 Style — `eslint-disable` comments in `mcp/client.ts`
|
|
113
|
+
|
|
114
|
+
Lines 81 and 88 in `mcp/client.ts` contain `// eslint-disable-next-line @typescript-eslint/no-explicit-any` comments. The project uses Biome, not ESLint — these comments are dead noise and have no effect. They should be removed.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Summary
|
|
119
|
+
|
|
120
|
+
| # | Severity | File(s) | Issue |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| 1 | 🔴 | `agents.ts`, `custom-commands.ts`, `skills.ts` | `parseFrontmatter` duplicated 3× |
|
|
123
|
+
| 2 | 🔴 | same 3 files | `loadFromDir` + merge pattern duplicated 3× |
|
|
124
|
+
| 3 | 🔴 | `agent.ts` (×2), `manager.ts` | `cwdDisplay` tilde logic duplicated |
|
|
125
|
+
| 4 | 🔴 | 6 files (12 occurrences) | Inlined `import()` type calls — project rule violation |
|
|
126
|
+
| 5 | 🟡 | `turn.ts` | `userMessage` exported but never used |
|
|
127
|
+
| 6 | 🟡 | `commands.ts` | `availableProviders` imported but never used |
|
|
128
|
+
| 7 | 🟡 | `db.ts` | `saveMessage` (singular) exported but never called |
|
|
129
|
+
| 8 | 🟡 | `db.ts` | `updateSessionTitle`, `deleteSession` — dead exports |
|
|
130
|
+
| 9 | 🟡 | `types.ts` | `ProviderConfig`, `Message`, `MessageRole`, etc. — never imported |
|
|
131
|
+
| 10 | 🟡 | `manager.ts` | `relative` imported but not used |
|
|
132
|
+
| 11 | 🟡 | `manager.ts` | `PREFIX` imported but not used |
|
|
133
|
+
| 12 | 🟡 | `providers.ts` | `zenGoogle` doesn't memoize, ignores its parameter |
|
|
134
|
+
| 13 | 🟡 | `agent.ts` | `homedir()` called twice per expression |
|
|
135
|
+
| 14 | 🟢 | `mcp/client.ts` | Dead `eslint-disable` comments (project uses Biome) |
|
package/dist/mc.js
CHANGED
|
@@ -5,11 +5,81 @@
|
|
|
5
5
|
import * as c7 from "yoctocolors";
|
|
6
6
|
|
|
7
7
|
// src/agent/agent.ts
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import {
|
|
10
|
-
import { join as join11 } from "path";
|
|
8
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
9
|
+
import { join as join14 } from "path";
|
|
11
10
|
import * as c6 from "yoctocolors";
|
|
12
11
|
|
|
12
|
+
// src/cli/agents.ts
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { basename, join } from "path";
|
|
16
|
+
|
|
17
|
+
// src/cli/frontmatter.ts
|
|
18
|
+
var FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
19
|
+
function parseFrontmatter(raw) {
|
|
20
|
+
const m = raw.match(FM_RE);
|
|
21
|
+
if (!m)
|
|
22
|
+
return { meta: {}, body: raw };
|
|
23
|
+
const meta = {};
|
|
24
|
+
const yamlBlock = m[1] ?? "";
|
|
25
|
+
for (const line of yamlBlock.split(`
|
|
26
|
+
`)) {
|
|
27
|
+
const colon = line.indexOf(":");
|
|
28
|
+
if (colon === -1)
|
|
29
|
+
continue;
|
|
30
|
+
const key = line.slice(0, colon).trim();
|
|
31
|
+
const val = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
32
|
+
if (key === "name")
|
|
33
|
+
meta.name = val;
|
|
34
|
+
if (key === "description")
|
|
35
|
+
meta.description = val;
|
|
36
|
+
if (key === "model")
|
|
37
|
+
meta.model = val;
|
|
38
|
+
}
|
|
39
|
+
return { meta, body: (m[2] ?? "").trim() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/cli/agents.ts
|
|
43
|
+
function loadFromDir(dir, source) {
|
|
44
|
+
const agents = new Map;
|
|
45
|
+
if (!existsSync(dir))
|
|
46
|
+
return agents;
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = readdirSync(dir);
|
|
50
|
+
} catch {
|
|
51
|
+
return agents;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.endsWith(".md"))
|
|
55
|
+
continue;
|
|
56
|
+
const name = basename(entry, ".md");
|
|
57
|
+
const filePath = join(dir, entry);
|
|
58
|
+
let raw;
|
|
59
|
+
try {
|
|
60
|
+
raw = readFileSync(filePath, "utf-8");
|
|
61
|
+
} catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
65
|
+
agents.set(name, {
|
|
66
|
+
name,
|
|
67
|
+
description: meta.description ?? name,
|
|
68
|
+
...meta.model ? { model: meta.model } : {},
|
|
69
|
+
systemPrompt: body,
|
|
70
|
+
source
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return agents;
|
|
74
|
+
}
|
|
75
|
+
function loadAgents(cwd) {
|
|
76
|
+
const globalDir = join(homedir(), ".agents", "agents");
|
|
77
|
+
const localDir = join(cwd, ".agents", "agents");
|
|
78
|
+
const global = loadFromDir(globalDir, "global");
|
|
79
|
+
const local = loadFromDir(localDir, "local");
|
|
80
|
+
return new Map([...global, ...local]);
|
|
81
|
+
}
|
|
82
|
+
|
|
13
83
|
// src/cli/commands.ts
|
|
14
84
|
import * as c3 from "yoctocolors";
|
|
15
85
|
|
|
@@ -48,6 +118,7 @@ var ZEN_GOOGLE_MODELS = new Set([
|
|
|
48
118
|
]);
|
|
49
119
|
var _zenAnthropic = null;
|
|
50
120
|
var _zenOpenAI = null;
|
|
121
|
+
var _zenGoogle = null;
|
|
51
122
|
var _zenCompat = null;
|
|
52
123
|
function getZenApiKey() {
|
|
53
124
|
const key = process.env.OPENCODE_API_KEY;
|
|
@@ -73,11 +144,14 @@ function zenOpenAI() {
|
|
|
73
144
|
}
|
|
74
145
|
return _zenOpenAI;
|
|
75
146
|
}
|
|
76
|
-
function zenGoogle(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
147
|
+
function zenGoogle() {
|
|
148
|
+
if (!_zenGoogle) {
|
|
149
|
+
_zenGoogle = createGoogleGenerativeAI({
|
|
150
|
+
apiKey: getZenApiKey(),
|
|
151
|
+
baseURL: ZEN_BASE
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return _zenGoogle;
|
|
81
155
|
}
|
|
82
156
|
function zenCompat() {
|
|
83
157
|
if (!_zenCompat) {
|
|
@@ -153,7 +227,7 @@ function resolveModel(modelString) {
|
|
|
153
227
|
return zenOpenAI()(modelId);
|
|
154
228
|
}
|
|
155
229
|
if (ZEN_GOOGLE_MODELS.has(modelId)) {
|
|
156
|
-
return zenGoogle(
|
|
230
|
+
return zenGoogle()(modelId);
|
|
157
231
|
}
|
|
158
232
|
return zenCompat()(modelId);
|
|
159
233
|
}
|
|
@@ -235,17 +309,17 @@ async function fetchAvailableModels() {
|
|
|
235
309
|
|
|
236
310
|
// src/session/db.ts
|
|
237
311
|
import { Database } from "bun:sqlite";
|
|
238
|
-
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
239
|
-
import { homedir } from "os";
|
|
240
|
-
import { join } from "path";
|
|
312
|
+
import { existsSync as existsSync2, mkdirSync, unlinkSync } from "fs";
|
|
313
|
+
import { homedir as homedir2 } from "os";
|
|
314
|
+
import { join as join2 } from "path";
|
|
241
315
|
function getConfigDir() {
|
|
242
|
-
return
|
|
316
|
+
return join2(homedir2(), ".config", "mini-coder");
|
|
243
317
|
}
|
|
244
318
|
function getDbPath() {
|
|
245
319
|
const dir = getConfigDir();
|
|
246
|
-
if (!
|
|
320
|
+
if (!existsSync2(dir))
|
|
247
321
|
mkdirSync(dir, { recursive: true });
|
|
248
|
-
return
|
|
322
|
+
return join2(dir, "sessions.db");
|
|
249
323
|
}
|
|
250
324
|
var DB_VERSION = 3;
|
|
251
325
|
var SCHEMA = `
|
|
@@ -321,7 +395,7 @@ function getDb() {
|
|
|
321
395
|
db.close();
|
|
322
396
|
} catch {}
|
|
323
397
|
for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
324
|
-
if (
|
|
398
|
+
if (existsSync2(path))
|
|
325
399
|
unlinkSync(path);
|
|
326
400
|
}
|
|
327
401
|
db = new Database(dbPath, { create: true });
|
|
@@ -471,6 +545,88 @@ function generateSessionId() {
|
|
|
471
545
|
return `${ts}-${rand}`;
|
|
472
546
|
}
|
|
473
547
|
|
|
548
|
+
// src/cli/custom-commands.ts
|
|
549
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
550
|
+
import { homedir as homedir3 } from "os";
|
|
551
|
+
import { basename as basename2, join as join3 } from "path";
|
|
552
|
+
function loadFromDir2(dir, source) {
|
|
553
|
+
const commands = new Map;
|
|
554
|
+
if (!existsSync3(dir))
|
|
555
|
+
return commands;
|
|
556
|
+
let entries;
|
|
557
|
+
try {
|
|
558
|
+
entries = readdirSync2(dir);
|
|
559
|
+
} catch {
|
|
560
|
+
return commands;
|
|
561
|
+
}
|
|
562
|
+
for (const entry of entries) {
|
|
563
|
+
if (!entry.endsWith(".md"))
|
|
564
|
+
continue;
|
|
565
|
+
const name = basename2(entry, ".md");
|
|
566
|
+
const filePath = join3(dir, entry);
|
|
567
|
+
let raw;
|
|
568
|
+
try {
|
|
569
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
570
|
+
} catch {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
574
|
+
commands.set(name, {
|
|
575
|
+
name,
|
|
576
|
+
description: meta.description ?? name,
|
|
577
|
+
...meta.model ? { model: meta.model } : {},
|
|
578
|
+
template: body,
|
|
579
|
+
source
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return commands;
|
|
583
|
+
}
|
|
584
|
+
function loadCustomCommands(cwd) {
|
|
585
|
+
const globalDir = join3(homedir3(), ".agents", "commands");
|
|
586
|
+
const localDir = join3(cwd, ".agents", "commands");
|
|
587
|
+
const global = loadFromDir2(globalDir, "global");
|
|
588
|
+
const local = loadFromDir2(localDir, "local");
|
|
589
|
+
return new Map([...global, ...local]);
|
|
590
|
+
}
|
|
591
|
+
async function expandTemplate(template, args, cwd) {
|
|
592
|
+
const tokens = args.match(/("([^"]*)")|('([^']*)')|(\S+)/g)?.map((t) => t.replace(/^["']|["']$/g, "")) ?? [];
|
|
593
|
+
let result = template;
|
|
594
|
+
for (let i = 9;i >= 1; i--) {
|
|
595
|
+
result = result.replaceAll(`$${i}`, tokens[i - 1] ?? "");
|
|
596
|
+
}
|
|
597
|
+
result = result.replaceAll("$ARGUMENTS", args);
|
|
598
|
+
const SHELL_RE = /!`([^`]+)`/g;
|
|
599
|
+
const shellMatches = [...result.matchAll(SHELL_RE)];
|
|
600
|
+
for (const match of shellMatches) {
|
|
601
|
+
const cmd = match[1] ?? "";
|
|
602
|
+
let output = "";
|
|
603
|
+
try {
|
|
604
|
+
const signal = AbortSignal.timeout(1e4);
|
|
605
|
+
const proc = Bun.spawn(["bash", "-c", cmd], {
|
|
606
|
+
cwd,
|
|
607
|
+
stdout: "pipe",
|
|
608
|
+
stderr: "pipe"
|
|
609
|
+
});
|
|
610
|
+
await Promise.race([
|
|
611
|
+
proc.exited,
|
|
612
|
+
new Promise((_, reject) => signal.addEventListener("abort", () => {
|
|
613
|
+
proc.kill();
|
|
614
|
+
reject(new Error("timeout"));
|
|
615
|
+
}))
|
|
616
|
+
]);
|
|
617
|
+
const [stdout, stderr] = await Promise.all([
|
|
618
|
+
new Response(proc.stdout).text(),
|
|
619
|
+
new Response(proc.stderr).text()
|
|
620
|
+
]);
|
|
621
|
+
const exitCode = proc.exitCode ?? 0;
|
|
622
|
+
output = exitCode === 0 ? [stdout, stderr].filter(Boolean).join(`
|
|
623
|
+
`).trim() : stdout.trim();
|
|
624
|
+
} catch {}
|
|
625
|
+
result = result.replaceAll(match[0], output);
|
|
626
|
+
}
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
|
|
474
630
|
// src/cli/markdown.ts
|
|
475
631
|
import * as c from "yoctocolors";
|
|
476
632
|
function renderInline(text) {
|
|
@@ -584,10 +740,13 @@ function renderChunk(text, inFence) {
|
|
|
584
740
|
}
|
|
585
741
|
|
|
586
742
|
// src/cli/output.ts
|
|
587
|
-
import { homedir as
|
|
743
|
+
import { homedir as homedir4 } from "os";
|
|
588
744
|
import * as c2 from "yoctocolors";
|
|
589
|
-
var HOME =
|
|
590
|
-
var PACKAGE_VERSION = "0.0.
|
|
745
|
+
var HOME = homedir4();
|
|
746
|
+
var PACKAGE_VERSION = "0.0.4";
|
|
747
|
+
function tildePath(p) {
|
|
748
|
+
return p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p;
|
|
749
|
+
}
|
|
591
750
|
function restoreTerminal() {
|
|
592
751
|
try {
|
|
593
752
|
process.stderr.write("\x1B[?25h");
|
|
@@ -1209,6 +1368,48 @@ var PREFIX = {
|
|
|
1209
1368
|
success: G.ok
|
|
1210
1369
|
};
|
|
1211
1370
|
|
|
1371
|
+
// src/cli/skills.ts
|
|
1372
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs";
|
|
1373
|
+
import { homedir as homedir5 } from "os";
|
|
1374
|
+
import { join as join4 } from "path";
|
|
1375
|
+
function loadFromDir3(dir, source) {
|
|
1376
|
+
const skills = new Map;
|
|
1377
|
+
if (!existsSync4(dir))
|
|
1378
|
+
return skills;
|
|
1379
|
+
let entries;
|
|
1380
|
+
try {
|
|
1381
|
+
entries = readdirSync3(dir);
|
|
1382
|
+
} catch {
|
|
1383
|
+
return skills;
|
|
1384
|
+
}
|
|
1385
|
+
for (const entry of entries) {
|
|
1386
|
+
const skillFile = join4(dir, entry, "SKILL.md");
|
|
1387
|
+
try {
|
|
1388
|
+
if (!statSync(join4(dir, entry)).isDirectory())
|
|
1389
|
+
continue;
|
|
1390
|
+
if (!existsSync4(skillFile))
|
|
1391
|
+
continue;
|
|
1392
|
+
const content = readFileSync3(skillFile, "utf-8");
|
|
1393
|
+
const { meta } = parseFrontmatter(content);
|
|
1394
|
+
const name = meta.name ?? entry;
|
|
1395
|
+
skills.set(name, {
|
|
1396
|
+
name,
|
|
1397
|
+
description: meta.description ?? name,
|
|
1398
|
+
content,
|
|
1399
|
+
source
|
|
1400
|
+
});
|
|
1401
|
+
} catch {}
|
|
1402
|
+
}
|
|
1403
|
+
return skills;
|
|
1404
|
+
}
|
|
1405
|
+
function loadSkills(cwd) {
|
|
1406
|
+
const globalDir = join4(homedir5(), ".agents", "skills");
|
|
1407
|
+
const localDir = join4(cwd, ".agents", "skills");
|
|
1408
|
+
const global = loadFromDir3(globalDir, "global");
|
|
1409
|
+
const local = loadFromDir3(localDir, "local");
|
|
1410
|
+
return new Map([...global, ...local]);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1212
1413
|
// src/cli/commands.ts
|
|
1213
1414
|
async function handleModel(ctx, args) {
|
|
1214
1415
|
if (args) {
|
|
@@ -1426,7 +1627,35 @@ function handleNew(ctx) {
|
|
|
1426
1627
|
ctx.startNewSession();
|
|
1427
1628
|
writeln(`${PREFIX.success} ${c3.dim("new session started \u2014 context cleared")}`);
|
|
1428
1629
|
}
|
|
1429
|
-
function
|
|
1630
|
+
async function handleCustomCommand(cmd, args, ctx) {
|
|
1631
|
+
const prompt = await expandTemplate(cmd.template, args, ctx.cwd);
|
|
1632
|
+
const label = c3.cyan(cmd.name);
|
|
1633
|
+
const srcPath = cmd.source === "local" ? `.agents/commands/${cmd.name}.md` : `~/.agents/commands/${cmd.name}.md`;
|
|
1634
|
+
const src = c3.dim(`[${srcPath}]`);
|
|
1635
|
+
writeln(`${PREFIX.info} ${label} ${src}`);
|
|
1636
|
+
writeln();
|
|
1637
|
+
try {
|
|
1638
|
+
const output = await ctx.runSubagent(prompt, cmd.model);
|
|
1639
|
+
if (output.activity.length) {
|
|
1640
|
+
renderSubagentActivity(output.activity, " ", 1);
|
|
1641
|
+
writeln();
|
|
1642
|
+
}
|
|
1643
|
+
write(renderMarkdown(output.result));
|
|
1644
|
+
writeln();
|
|
1645
|
+
return {
|
|
1646
|
+
type: "inject-user-message",
|
|
1647
|
+
text: `/${cmd.name} output:
|
|
1648
|
+
|
|
1649
|
+
${output.result}
|
|
1650
|
+
|
|
1651
|
+
<system-message>Summarize the findings above to the user.</system-message>`
|
|
1652
|
+
};
|
|
1653
|
+
} catch (e) {
|
|
1654
|
+
writeln(`${PREFIX.error} /${cmd.name} failed: ${String(e)}`);
|
|
1655
|
+
return { type: "handled" };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function handleHelp(ctx, custom) {
|
|
1430
1659
|
writeln();
|
|
1431
1660
|
const cmds = [
|
|
1432
1661
|
["/model [id]", "list or switch models (fetches live list)"],
|
|
@@ -1447,7 +1676,35 @@ function handleHelp() {
|
|
|
1447
1676
|
for (const [cmd, desc] of cmds) {
|
|
1448
1677
|
writeln(` ${c3.cyan(cmd.padEnd(26))} ${c3.dim(desc)}`);
|
|
1449
1678
|
}
|
|
1679
|
+
if (custom.size > 0) {
|
|
1680
|
+
writeln();
|
|
1681
|
+
writeln(c3.dim(" custom commands:"));
|
|
1682
|
+
for (const cmd of custom.values()) {
|
|
1683
|
+
const tag = cmd.source === "local" ? c3.dim(" (local)") : c3.dim(" (global)");
|
|
1684
|
+
writeln(` ${c3.green(`/${cmd.name}`.padEnd(26))} ${c3.dim(cmd.description)}${tag}`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const agents = loadAgents(ctx.cwd);
|
|
1688
|
+
if (agents.size > 0) {
|
|
1689
|
+
writeln();
|
|
1690
|
+
writeln(c3.dim(" agents (~/.agents/agents/ or .agents/agents/):"));
|
|
1691
|
+
for (const agent of agents.values()) {
|
|
1692
|
+
const tag = agent.source === "local" ? c3.dim(" (local)") : c3.dim(" (global)");
|
|
1693
|
+
writeln(` ${c3.magenta(`@${agent.name}`.padEnd(26))} ${c3.dim(agent.description)}${tag}`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const skills = loadSkills(ctx.cwd);
|
|
1697
|
+
if (skills.size > 0) {
|
|
1698
|
+
writeln();
|
|
1699
|
+
writeln(c3.dim(" skills (~/.agents/skills/ or .agents/skills/):"));
|
|
1700
|
+
for (const skill of skills.values()) {
|
|
1701
|
+
const tag = skill.source === "local" ? c3.dim(" (local)") : c3.dim(" (global)");
|
|
1702
|
+
writeln(` ${c3.yellow(`@${skill.name}`.padEnd(26))} ${c3.dim(skill.description)}${tag}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1450
1705
|
writeln();
|
|
1706
|
+
writeln(` ${c3.green("@agent".padEnd(26))} ${c3.dim("run prompt through a custom agent (Tab to complete)")}`);
|
|
1707
|
+
writeln(` ${c3.green("@skill".padEnd(26))} ${c3.dim("inject skill instructions into prompt (Tab to complete)")}`);
|
|
1451
1708
|
writeln(` ${c3.green("@file".padEnd(26))} ${c3.dim("inject file contents into prompt (Tab to complete)")}`);
|
|
1452
1709
|
writeln(` ${c3.green("!cmd".padEnd(26))} ${c3.dim("run shell command, output added as context")}`);
|
|
1453
1710
|
writeln();
|
|
@@ -1455,6 +1712,11 @@ function handleHelp() {
|
|
|
1455
1712
|
writeln();
|
|
1456
1713
|
}
|
|
1457
1714
|
async function handleCommand(command, args, ctx) {
|
|
1715
|
+
const custom = loadCustomCommands(ctx.cwd);
|
|
1716
|
+
const customCmd = custom.get(command.toLowerCase());
|
|
1717
|
+
if (customCmd) {
|
|
1718
|
+
return await handleCustomCommand(customCmd, args, ctx);
|
|
1719
|
+
}
|
|
1458
1720
|
switch (command.toLowerCase()) {
|
|
1459
1721
|
case "model":
|
|
1460
1722
|
case "models":
|
|
@@ -1479,15 +1741,16 @@ async function handleCommand(command, args, ctx) {
|
|
|
1479
1741
|
return await handleReview(ctx, args);
|
|
1480
1742
|
case "help":
|
|
1481
1743
|
case "?":
|
|
1482
|
-
handleHelp();
|
|
1744
|
+
handleHelp(ctx, custom);
|
|
1483
1745
|
return { type: "handled" };
|
|
1484
1746
|
case "exit":
|
|
1485
1747
|
case "quit":
|
|
1486
1748
|
case "q":
|
|
1487
1749
|
return { type: "exit" };
|
|
1488
|
-
default:
|
|
1750
|
+
default: {
|
|
1489
1751
|
writeln(`${PREFIX.error} unknown: /${command} ${c3.dim("\u2014 /help for commands")}`);
|
|
1490
1752
|
return { type: "unknown", command };
|
|
1753
|
+
}
|
|
1491
1754
|
}
|
|
1492
1755
|
}
|
|
1493
1756
|
|
|
@@ -1527,7 +1790,7 @@ async function loadImageFile(filePath) {
|
|
|
1527
1790
|
}
|
|
1528
1791
|
|
|
1529
1792
|
// src/cli/input.ts
|
|
1530
|
-
import { join as
|
|
1793
|
+
import { join as join5, relative } from "path";
|
|
1531
1794
|
import * as c4 from "yoctocolors";
|
|
1532
1795
|
var ESC = "\x1B";
|
|
1533
1796
|
var CSI = `${ESC}[`;
|
|
@@ -1556,16 +1819,33 @@ var CTRL_K = "\v";
|
|
|
1556
1819
|
var CTRL_L = "\f";
|
|
1557
1820
|
var CTRL_R = "\x12";
|
|
1558
1821
|
var TAB = "\t";
|
|
1559
|
-
async function
|
|
1822
|
+
async function getAtCompletions(prefix, cwd) {
|
|
1560
1823
|
const query = prefix.startsWith("@") ? prefix.slice(1) : prefix;
|
|
1561
|
-
const glob = new Bun.Glob(`**/*${query}*`);
|
|
1562
1824
|
const results = [];
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
results.
|
|
1567
|
-
|
|
1825
|
+
const MAX = 10;
|
|
1826
|
+
const skills = loadSkills(cwd);
|
|
1827
|
+
for (const [name] of skills) {
|
|
1828
|
+
if (results.length >= MAX)
|
|
1829
|
+
break;
|
|
1830
|
+
if (name.includes(query))
|
|
1831
|
+
results.push(`@${name}`);
|
|
1832
|
+
}
|
|
1833
|
+
const agents = loadAgents(cwd);
|
|
1834
|
+
for (const [name] of agents) {
|
|
1835
|
+
if (results.length >= MAX)
|
|
1568
1836
|
break;
|
|
1837
|
+
if (name.includes(query))
|
|
1838
|
+
results.push(`@${name}`);
|
|
1839
|
+
}
|
|
1840
|
+
if (results.length < MAX) {
|
|
1841
|
+
const glob = new Bun.Glob(`**/*${query}*`);
|
|
1842
|
+
for await (const file of glob.scan({ cwd, onlyFiles: true })) {
|
|
1843
|
+
if (file.includes("node_modules") || file.includes(".git"))
|
|
1844
|
+
continue;
|
|
1845
|
+
results.push(`@${relative(cwd, join5(cwd, file))}`);
|
|
1846
|
+
if (results.length >= MAX)
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1569
1849
|
}
|
|
1570
1850
|
return results;
|
|
1571
1851
|
}
|
|
@@ -1584,7 +1864,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
|
|
|
1584
1864
|
}
|
|
1585
1865
|
}
|
|
1586
1866
|
if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
|
|
1587
|
-
const filePath = trimmed.startsWith("/") ? trimmed :
|
|
1867
|
+
const filePath = trimmed.startsWith("/") ? trimmed : join5(cwd, trimmed);
|
|
1588
1868
|
const attachment = await loadImageFile(filePath);
|
|
1589
1869
|
if (attachment) {
|
|
1590
1870
|
const name = filePath.split("/").pop() ?? trimmed;
|
|
@@ -1860,7 +2140,7 @@ async function readline(opts) {
|
|
|
1860
2140
|
const beforeCursor = buf.slice(0, cursor);
|
|
1861
2141
|
const atMatch = beforeCursor.match(/@(\S*)$/);
|
|
1862
2142
|
if (atMatch) {
|
|
1863
|
-
const completions = await
|
|
2143
|
+
const completions = await getAtCompletions(atMatch[0], cwd);
|
|
1864
2144
|
if (completions.length === 1 && completions[0]) {
|
|
1865
2145
|
const replacement = completions[0];
|
|
1866
2146
|
buf = buf.slice(0, cursor - (atMatch[0] ?? "").length) + replacement + buf.slice(cursor);
|
|
@@ -2104,8 +2384,8 @@ async function connectMcpServer(config) {
|
|
|
2104
2384
|
}
|
|
2105
2385
|
|
|
2106
2386
|
// src/tools/snapshot.ts
|
|
2107
|
-
import { readFileSync, unlinkSync as unlinkSync2 } from "fs";
|
|
2108
|
-
import { join as
|
|
2387
|
+
import { readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
2388
|
+
import { join as join6 } from "path";
|
|
2109
2389
|
async function gitBytes(args, cwd) {
|
|
2110
2390
|
try {
|
|
2111
2391
|
const proc = Bun.spawn(["git", ...args], {
|
|
@@ -2196,7 +2476,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
|
|
|
2196
2476
|
return false;
|
|
2197
2477
|
const files = [];
|
|
2198
2478
|
for (const entry of entries) {
|
|
2199
|
-
const absPath =
|
|
2479
|
+
const absPath = join6(repoRoot, entry.path);
|
|
2200
2480
|
if (!entry.existsOnDisk) {
|
|
2201
2481
|
const { bytes, code } = await gitBytes(["show", `HEAD:${entry.path}`], repoRoot);
|
|
2202
2482
|
if (code === 0) {
|
|
@@ -2210,7 +2490,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
|
|
|
2210
2490
|
}
|
|
2211
2491
|
if (entry.isNew) {
|
|
2212
2492
|
try {
|
|
2213
|
-
const content =
|
|
2493
|
+
const content = readFileSync4(absPath);
|
|
2214
2494
|
files.push({
|
|
2215
2495
|
path: entry.path,
|
|
2216
2496
|
content: new Uint8Array(content),
|
|
@@ -2220,7 +2500,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
|
|
|
2220
2500
|
continue;
|
|
2221
2501
|
}
|
|
2222
2502
|
try {
|
|
2223
|
-
const content =
|
|
2503
|
+
const content = readFileSync4(absPath);
|
|
2224
2504
|
files.push({
|
|
2225
2505
|
path: entry.path,
|
|
2226
2506
|
content: new Uint8Array(content),
|
|
@@ -2245,7 +2525,7 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
|
|
|
2245
2525
|
const root = repoRoot ?? cwd;
|
|
2246
2526
|
let anyFailed = false;
|
|
2247
2527
|
for (const file of files) {
|
|
2248
|
-
const absPath =
|
|
2528
|
+
const absPath = join6(root, file.path);
|
|
2249
2529
|
if (!file.existed) {
|
|
2250
2530
|
try {
|
|
2251
2531
|
if (await Bun.file(absPath).exists()) {
|
|
@@ -2274,7 +2554,6 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
|
|
|
2274
2554
|
}
|
|
2275
2555
|
|
|
2276
2556
|
// src/session/manager.ts
|
|
2277
|
-
import { homedir as homedir3 } from "os";
|
|
2278
2557
|
import * as c5 from "yoctocolors";
|
|
2279
2558
|
function newSession(model, cwd) {
|
|
2280
2559
|
const id = generateSessionId();
|
|
@@ -2301,7 +2580,7 @@ function printSessionList() {
|
|
|
2301
2580
|
${c5.bold("Recent sessions:")}`);
|
|
2302
2581
|
for (const s of sessions) {
|
|
2303
2582
|
const date = new Date(s.updated_at).toLocaleString();
|
|
2304
|
-
const cwd =
|
|
2583
|
+
const cwd = tildePath(s.cwd);
|
|
2305
2584
|
const title = s.title || c5.dim("(untitled)");
|
|
2306
2585
|
writeln(` ${c5.dim(s.id.padEnd(14))} ${title.padEnd(30)} ${c5.cyan(s.model.split("/").pop() ?? s.model).padEnd(20)} ${c5.dim(cwd)} ${c5.dim(date)}`);
|
|
2307
2586
|
}
|
|
@@ -2314,8 +2593,8 @@ function getMostRecentSession() {
|
|
|
2314
2593
|
}
|
|
2315
2594
|
|
|
2316
2595
|
// src/tools/create.ts
|
|
2317
|
-
import { existsSync as
|
|
2318
|
-
import { dirname, join as
|
|
2596
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
2597
|
+
import { dirname, join as join7, relative as relative2 } from "path";
|
|
2319
2598
|
import { z as z2 } from "zod";
|
|
2320
2599
|
|
|
2321
2600
|
// src/tools/diff.ts
|
|
@@ -2455,10 +2734,10 @@ var createTool = {
|
|
|
2455
2734
|
schema: CreateSchema,
|
|
2456
2735
|
execute: async (input) => {
|
|
2457
2736
|
const cwd = input.cwd ?? process.cwd();
|
|
2458
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
2737
|
+
const filePath = input.path.startsWith("/") ? input.path : join7(cwd, input.path);
|
|
2459
2738
|
const relPath = relative2(cwd, filePath);
|
|
2460
2739
|
const dir = dirname(filePath);
|
|
2461
|
-
if (!
|
|
2740
|
+
if (!existsSync5(dir))
|
|
2462
2741
|
mkdirSync2(dir, { recursive: true });
|
|
2463
2742
|
const file = Bun.file(filePath);
|
|
2464
2743
|
const created = !await file.exists();
|
|
@@ -2470,7 +2749,7 @@ var createTool = {
|
|
|
2470
2749
|
};
|
|
2471
2750
|
|
|
2472
2751
|
// src/tools/glob.ts
|
|
2473
|
-
import { join as
|
|
2752
|
+
import { join as join8, relative as relative3 } from "path";
|
|
2474
2753
|
import { z as z3 } from "zod";
|
|
2475
2754
|
var GlobSchema = z3.object({
|
|
2476
2755
|
pattern: z3.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
|
|
@@ -2502,7 +2781,7 @@ var globTool = {
|
|
|
2502
2781
|
if (ignored)
|
|
2503
2782
|
continue;
|
|
2504
2783
|
try {
|
|
2505
|
-
const fullPath =
|
|
2784
|
+
const fullPath = join8(cwd, file);
|
|
2506
2785
|
const stat = await Bun.file(fullPath).stat?.() ?? null;
|
|
2507
2786
|
matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
|
|
2508
2787
|
} catch {
|
|
@@ -2515,13 +2794,13 @@ var globTool = {
|
|
|
2515
2794
|
if (truncated)
|
|
2516
2795
|
matches.pop();
|
|
2517
2796
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
2518
|
-
const files = matches.map((m) => relative3(cwd,
|
|
2797
|
+
const files = matches.map((m) => relative3(cwd, join8(cwd, m.path)));
|
|
2519
2798
|
return { files, count: files.length, truncated };
|
|
2520
2799
|
}
|
|
2521
2800
|
};
|
|
2522
2801
|
|
|
2523
2802
|
// src/tools/grep.ts
|
|
2524
|
-
import { join as
|
|
2803
|
+
import { join as join9 } from "path";
|
|
2525
2804
|
import { z as z4 } from "zod";
|
|
2526
2805
|
|
|
2527
2806
|
// src/tools/hashline.ts
|
|
@@ -2607,7 +2886,7 @@ var grepTool = {
|
|
|
2607
2886
|
if (ignoreGlob.some((g) => g.match(relPath) || g.match(relPath.split("/")[0] ?? ""))) {
|
|
2608
2887
|
continue;
|
|
2609
2888
|
}
|
|
2610
|
-
const fullPath =
|
|
2889
|
+
const fullPath = join9(cwd, relPath);
|
|
2611
2890
|
let text;
|
|
2612
2891
|
try {
|
|
2613
2892
|
text = await Bun.file(fullPath).text();
|
|
@@ -2658,8 +2937,8 @@ var grepTool = {
|
|
|
2658
2937
|
|
|
2659
2938
|
// src/tools/hooks.ts
|
|
2660
2939
|
import { constants, accessSync } from "fs";
|
|
2661
|
-
import { homedir as
|
|
2662
|
-
import { join as
|
|
2940
|
+
import { homedir as homedir6 } from "os";
|
|
2941
|
+
import { join as join10 } from "path";
|
|
2663
2942
|
function isExecutable(filePath) {
|
|
2664
2943
|
try {
|
|
2665
2944
|
accessSync(filePath, constants.X_OK);
|
|
@@ -2671,8 +2950,8 @@ function isExecutable(filePath) {
|
|
|
2671
2950
|
function findHook(toolName, cwd) {
|
|
2672
2951
|
const scriptName = `post-${toolName}`;
|
|
2673
2952
|
const candidates = [
|
|
2674
|
-
|
|
2675
|
-
|
|
2953
|
+
join10(cwd, ".agents", "hooks", scriptName),
|
|
2954
|
+
join10(homedir6(), ".agents", "hooks", scriptName)
|
|
2676
2955
|
];
|
|
2677
2956
|
for (const p of candidates) {
|
|
2678
2957
|
if (isExecutable(p))
|
|
@@ -2761,7 +3040,7 @@ function hookEnvForRead(input, cwd) {
|
|
|
2761
3040
|
}
|
|
2762
3041
|
|
|
2763
3042
|
// src/tools/insert.ts
|
|
2764
|
-
import { join as
|
|
3043
|
+
import { join as join11, relative as relative4 } from "path";
|
|
2765
3044
|
import { z as z5 } from "zod";
|
|
2766
3045
|
var InsertSchema = z5.object({
|
|
2767
3046
|
path: z5.string().describe("File path to edit (absolute or relative to cwd)"),
|
|
@@ -2776,7 +3055,7 @@ var insertTool = {
|
|
|
2776
3055
|
schema: InsertSchema,
|
|
2777
3056
|
execute: async (input) => {
|
|
2778
3057
|
const cwd = input.cwd ?? process.cwd();
|
|
2779
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3058
|
+
const filePath = input.path.startsWith("/") ? input.path : join11(cwd, input.path);
|
|
2780
3059
|
const relPath = relative4(cwd, filePath);
|
|
2781
3060
|
const file = Bun.file(filePath);
|
|
2782
3061
|
if (!await file.exists()) {
|
|
@@ -2821,7 +3100,7 @@ function parseAnchor(value) {
|
|
|
2821
3100
|
}
|
|
2822
3101
|
|
|
2823
3102
|
// src/tools/read.ts
|
|
2824
|
-
import { join as
|
|
3103
|
+
import { join as join12, relative as relative5 } from "path";
|
|
2825
3104
|
import { z as z6 } from "zod";
|
|
2826
3105
|
var ReadSchema = z6.object({
|
|
2827
3106
|
path: z6.string().describe("File path to read (absolute or relative to cwd)"),
|
|
@@ -2836,7 +3115,7 @@ var readTool = {
|
|
|
2836
3115
|
schema: ReadSchema,
|
|
2837
3116
|
execute: async (input) => {
|
|
2838
3117
|
const cwd = input.cwd ?? process.cwd();
|
|
2839
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3118
|
+
const filePath = input.path.startsWith("/") ? input.path : join12(cwd, input.path);
|
|
2840
3119
|
const file = Bun.file(filePath);
|
|
2841
3120
|
const exists = await file.exists();
|
|
2842
3121
|
if (!exists) {
|
|
@@ -2869,7 +3148,7 @@ var readTool = {
|
|
|
2869
3148
|
};
|
|
2870
3149
|
|
|
2871
3150
|
// src/tools/replace.ts
|
|
2872
|
-
import { join as
|
|
3151
|
+
import { join as join13, relative as relative6 } from "path";
|
|
2873
3152
|
import { z as z7 } from "zod";
|
|
2874
3153
|
var ReplaceSchema = z7.object({
|
|
2875
3154
|
path: z7.string().describe("File path to edit (absolute or relative to cwd)"),
|
|
@@ -2884,7 +3163,7 @@ var replaceTool = {
|
|
|
2884
3163
|
schema: ReplaceSchema,
|
|
2885
3164
|
execute: async (input) => {
|
|
2886
3165
|
const cwd = input.cwd ?? process.cwd();
|
|
2887
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3166
|
+
const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
|
|
2888
3167
|
const relPath = relative6(cwd, filePath);
|
|
2889
3168
|
const file = Bun.file(filePath);
|
|
2890
3169
|
if (!await file.exists()) {
|
|
@@ -3025,15 +3304,19 @@ var shellTool = {
|
|
|
3025
3304
|
// src/tools/subagent.ts
|
|
3026
3305
|
import { z as z9 } from "zod";
|
|
3027
3306
|
var SubagentInput = z9.object({
|
|
3028
|
-
prompt: z9.string().describe("The task or question to give the subagent")
|
|
3307
|
+
prompt: z9.string().describe("The task or question to give the subagent"),
|
|
3308
|
+
agentName: z9.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
|
|
3029
3309
|
});
|
|
3030
|
-
function createSubagentTool(runSubagent) {
|
|
3310
|
+
function createSubagentTool(runSubagent, availableAgents) {
|
|
3311
|
+
const agentSection = availableAgents.size > 0 ? `
|
|
3312
|
+
|
|
3313
|
+
When the user's message contains @<agent-name>, delegate to that agent by setting agentName to the exact agent name. Available custom agents: ${[...availableAgents.entries()].map(([name, cfg]) => `"${name}" (${cfg.description})`).join(", ")}.` : "";
|
|
3031
3314
|
return {
|
|
3032
3315
|
name: "subagent",
|
|
3033
|
-
description:
|
|
3316
|
+
description: `Spawn a sub-agent to handle a focused subtask. Use this for parallel exploration, specialised analysis, or tasks that benefit from a fresh context window. The subagent has access to all the same tools.${agentSection}`,
|
|
3034
3317
|
schema: SubagentInput,
|
|
3035
3318
|
execute: async (input) => {
|
|
3036
|
-
return runSubagent(input.prompt);
|
|
3319
|
+
return runSubagent(input.prompt, input.agentName);
|
|
3037
3320
|
}
|
|
3038
3321
|
};
|
|
3039
3322
|
}
|
|
@@ -3090,12 +3373,12 @@ function buildToolSet(opts) {
|
|
|
3090
3373
|
withHooks(withCwdDefault(replaceTool, cwd), lookupHook, cwd, (result) => hookEnvForReplace(result, cwd), onHook),
|
|
3091
3374
|
withHooks(withCwdDefault(insertTool, cwd), lookupHook, cwd, (result) => hookEnvForInsert(result, cwd), onHook),
|
|
3092
3375
|
withHooks(withCwdDefault(shellTool, cwd), lookupHook, cwd, (result, input) => hookEnvForShell(result, input, cwd), onHook),
|
|
3093
|
-
createSubagentTool(async (prompt) => {
|
|
3376
|
+
createSubagentTool(async (prompt, agentName) => {
|
|
3094
3377
|
if (depth >= MAX_SUBAGENT_DEPTH) {
|
|
3095
3378
|
throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
|
|
3096
3379
|
}
|
|
3097
|
-
return opts.runSubagent(prompt, depth + 1);
|
|
3098
|
-
})
|
|
3380
|
+
return opts.runSubagent(prompt, depth + 1, agentName);
|
|
3381
|
+
}, opts.availableAgents)
|
|
3099
3382
|
];
|
|
3100
3383
|
}
|
|
3101
3384
|
function buildReadOnlyToolSet(opts) {
|
|
@@ -3126,14 +3409,14 @@ async function getGitBranch(cwd) {
|
|
|
3126
3409
|
}
|
|
3127
3410
|
function loadContextFile(cwd) {
|
|
3128
3411
|
const candidates = [
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3412
|
+
join14(cwd, "AGENTS.md"),
|
|
3413
|
+
join14(cwd, "CLAUDE.md"),
|
|
3414
|
+
join14(getConfigDir(), "AGENTS.md")
|
|
3132
3415
|
];
|
|
3133
3416
|
for (const p of candidates) {
|
|
3134
|
-
if (
|
|
3417
|
+
if (existsSync6(p)) {
|
|
3135
3418
|
try {
|
|
3136
|
-
return
|
|
3419
|
+
return readFileSync5(p, "utf-8");
|
|
3137
3420
|
} catch {}
|
|
3138
3421
|
}
|
|
3139
3422
|
}
|
|
@@ -3141,7 +3424,7 @@ function loadContextFile(cwd) {
|
|
|
3141
3424
|
}
|
|
3142
3425
|
function buildSystemPrompt(cwd) {
|
|
3143
3426
|
const contextFile = loadContextFile(cwd);
|
|
3144
|
-
const cwdDisplay =
|
|
3427
|
+
const cwdDisplay = tildePath(cwd);
|
|
3145
3428
|
const now = new Date().toLocaleString(undefined, { hour12: false });
|
|
3146
3429
|
let prompt = `You are mini-coder, a small and fast CLI coding agent.
|
|
3147
3430
|
You have access to tools to read files, search code, make edits, run shell commands, and spawn subagents.
|
|
@@ -3205,16 +3488,23 @@ async function runAgent(opts) {
|
|
|
3205
3488
|
}
|
|
3206
3489
|
let turnIndex = getMaxTurnIndex(session.id) + 1;
|
|
3207
3490
|
const coreHistory = [...session.messages];
|
|
3208
|
-
const runSubagent = async (prompt, depth = 0) => {
|
|
3491
|
+
const runSubagent = async (prompt, depth = 0, agentName, modelOverride) => {
|
|
3492
|
+
const allAgents = loadAgents(cwd);
|
|
3493
|
+
const agentConfig = agentName ? allAgents.get(agentName) : undefined;
|
|
3494
|
+
if (agentName && !agentConfig) {
|
|
3495
|
+
throw new Error(`Unknown agent "${agentName}". Available agents: ${[...allAgents.keys()].join(", ") || "(none)"}`);
|
|
3496
|
+
}
|
|
3497
|
+
const model = modelOverride ?? agentConfig?.model ?? currentModel;
|
|
3498
|
+
const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(cwd);
|
|
3209
3499
|
const subMessages = [{ role: "user", content: prompt }];
|
|
3210
3500
|
const subTools = buildToolSet({
|
|
3211
3501
|
cwd,
|
|
3212
3502
|
depth,
|
|
3213
3503
|
runSubagent,
|
|
3214
|
-
onHook: renderHook
|
|
3504
|
+
onHook: renderHook,
|
|
3505
|
+
availableAgents: allAgents
|
|
3215
3506
|
});
|
|
3216
|
-
const subLlm = resolveModel(
|
|
3217
|
-
const systemPrompt = buildSystemPrompt(cwd);
|
|
3507
|
+
const subLlm = resolveModel(model);
|
|
3218
3508
|
let result = "";
|
|
3219
3509
|
let inputTokens = 0;
|
|
3220
3510
|
let outputTokens = 0;
|
|
@@ -3254,11 +3544,13 @@ async function runAgent(opts) {
|
|
|
3254
3544
|
}
|
|
3255
3545
|
return { result, inputTokens, outputTokens, activity };
|
|
3256
3546
|
};
|
|
3547
|
+
const agents = loadAgents(cwd);
|
|
3257
3548
|
const tools = buildToolSet({
|
|
3258
3549
|
cwd,
|
|
3259
3550
|
depth: 0,
|
|
3260
3551
|
runSubagent,
|
|
3261
|
-
onHook: renderHook
|
|
3552
|
+
onHook: renderHook,
|
|
3553
|
+
availableAgents: agents
|
|
3262
3554
|
});
|
|
3263
3555
|
const mcpTools = [];
|
|
3264
3556
|
async function connectAndAddMcp(name) {
|
|
@@ -3310,7 +3602,7 @@ async function runAgent(opts) {
|
|
|
3310
3602
|
planMode = v;
|
|
3311
3603
|
},
|
|
3312
3604
|
cwd,
|
|
3313
|
-
runSubagent: (prompt) => runSubagent(prompt),
|
|
3605
|
+
runSubagent: (prompt, model) => runSubagent(prompt, 0, undefined, model),
|
|
3314
3606
|
undoLastTurn: async () => {
|
|
3315
3607
|
if (session.messages.length === 0)
|
|
3316
3608
|
return false;
|
|
@@ -3526,7 +3818,7 @@ ${out}
|
|
|
3526
3818
|
const branch = await getGitBranch(cwd);
|
|
3527
3819
|
const provider = currentModel.split("/")[0] ?? "";
|
|
3528
3820
|
const modelShort = currentModel.split("/").slice(1).join("/");
|
|
3529
|
-
const cwdDisplay =
|
|
3821
|
+
const cwdDisplay = tildePath(cwd);
|
|
3530
3822
|
renderStatusBar({
|
|
3531
3823
|
model: modelShort,
|
|
3532
3824
|
provider,
|
|
@@ -3567,11 +3859,20 @@ async function resolveFileRefs(text, cwd) {
|
|
|
3567
3859
|
let result = text;
|
|
3568
3860
|
const matches = [...text.matchAll(atPattern)];
|
|
3569
3861
|
const images = [];
|
|
3570
|
-
|
|
3862
|
+
const skills = loadSkills(cwd);
|
|
3863
|
+
for (const match of [...matches].reverse()) {
|
|
3571
3864
|
const ref = match[1];
|
|
3572
3865
|
if (!ref)
|
|
3573
3866
|
continue;
|
|
3574
|
-
const
|
|
3867
|
+
const skill = skills.get(ref);
|
|
3868
|
+
if (skill) {
|
|
3869
|
+
const replacement = `<skill name="${skill.name}">
|
|
3870
|
+
${skill.content}
|
|
3871
|
+
</skill>`;
|
|
3872
|
+
result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
|
|
3873
|
+
continue;
|
|
3874
|
+
}
|
|
3875
|
+
const filePath = ref.startsWith("/") ? ref : join14(cwd, ref);
|
|
3575
3876
|
if (isImageFilename(ref)) {
|
|
3576
3877
|
const attachment = await loadImageFile(filePath);
|
|
3577
3878
|
if (attachment) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Custom Agents
|
|
2
|
+
|
|
3
|
+
An agent is a subagent with a custom system prompt and optional model override.
|
|
4
|
+
Use `@agent-name` anywhere in your prompt to route the message through it.
|
|
5
|
+
|
|
6
|
+
## Where to put them
|
|
7
|
+
|
|
8
|
+
| Location | Scope |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `.agents/agents/*.md` | Current repo only |
|
|
11
|
+
| `~/.agents/agents/*.md` | All projects (global) |
|
|
12
|
+
|
|
13
|
+
Local agents override global ones with the same name.
|
|
14
|
+
|
|
15
|
+
## Create an agent
|
|
16
|
+
|
|
17
|
+
The filename becomes the agent name.
|
|
18
|
+
|
|
19
|
+
`~/.agents/agents/reviewer.md`:
|
|
20
|
+
|
|
21
|
+
```md
|
|
22
|
+
---
|
|
23
|
+
description: Strict code reviewer focused on bugs and structure
|
|
24
|
+
model: zen/claude-sonnet-4-6
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
You are a senior engineer doing a code review. Be direct and specific.
|
|
28
|
+
Cite file and line number for every finding. Flag bugs first, then
|
|
29
|
+
structure issues, then style — only if they violate project conventions.
|
|
30
|
+
No flattery. End with a one-line verdict.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then in the REPL:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
@reviewer review the auth module for race conditions
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The rest of the message (everything except the `@reviewer` token) becomes
|
|
40
|
+
the prompt. The agent runs in its own context window and returns its output
|
|
41
|
+
into the conversation.
|
|
42
|
+
|
|
43
|
+
## Frontmatter fields
|
|
44
|
+
|
|
45
|
+
| Field | Required | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `description` | No | Shown in `/help`. Defaults to filename. |
|
|
48
|
+
| `model` | No | Override the active model for this agent. |
|
|
49
|
+
|
|
50
|
+
The markdown body (after frontmatter) is the agent's system prompt.
|
|
51
|
+
|
|
52
|
+
## Combining with files
|
|
53
|
+
|
|
54
|
+
`@file` references are resolved before the agent fires:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
@reviewer @src/auth/session.ts check this file for issues
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Tab completion
|
|
61
|
+
|
|
62
|
+
Type `@` and press `Tab` to autocomplete agent names alongside files.
|
|
63
|
+
|
|
64
|
+
## Listing agents
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
/help
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Agents are listed in magenta, tagged `(local)` or `(global)`.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Custom Commands
|
|
2
|
+
|
|
3
|
+
Custom commands let you define reusable prompts that run as `/command` in the mini-coder REPL.
|
|
4
|
+
|
|
5
|
+
## Where to put them
|
|
6
|
+
|
|
7
|
+
| Location | Scope |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `.agents/commands/*.md` | Current repo only |
|
|
10
|
+
| `~/.agents/commands/*.md` | All projects (global) |
|
|
11
|
+
|
|
12
|
+
Local commands override global ones with the same name.
|
|
13
|
+
|
|
14
|
+
## Create a command
|
|
15
|
+
|
|
16
|
+
Create a markdown file. The filename becomes the command name.
|
|
17
|
+
|
|
18
|
+
`.agents/commands/standup.md`:
|
|
19
|
+
|
|
20
|
+
```md
|
|
21
|
+
---
|
|
22
|
+
description: Summarise what changed since yesterday
|
|
23
|
+
model: zen/claude-3-5-haiku
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Run `!`git log --oneline --since=yesterday`` and summarise the changes
|
|
27
|
+
as a short standup update. Group by theme, skip merge commits.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then in the REPL:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
/standup
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Frontmatter fields
|
|
37
|
+
|
|
38
|
+
| Field | Required | Description |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `description` | No | Shown in `/help`. Defaults to the command name. |
|
|
41
|
+
| `model` | No | Override the active model for this command. |
|
|
42
|
+
|
|
43
|
+
## Arguments
|
|
44
|
+
|
|
45
|
+
Use `$ARGUMENTS` for the full argument string, or `$1`, `$2`, … `$9` for individual tokens.
|
|
46
|
+
|
|
47
|
+
`.agents/commands/search.md`:
|
|
48
|
+
|
|
49
|
+
```md
|
|
50
|
+
---
|
|
51
|
+
description: Search the codebase for a topic
|
|
52
|
+
model: zen/claude-3-5-haiku
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
Search the codebase for: $ARGUMENTS
|
|
56
|
+
|
|
57
|
+
Use glob, grep, and read tools to explore thoroughly. Report all
|
|
58
|
+
relevant files, key code snippets with line numbers, and a short summary.
|
|
59
|
+
Be exhaustive but concise. No edits — read only.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
/search session management
|
|
64
|
+
/search error handling in providers
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Positional tokens:
|
|
68
|
+
|
|
69
|
+
```md
|
|
70
|
+
---
|
|
71
|
+
description: Create a new component
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
Create a React component named $1 in the $2 directory.
|
|
75
|
+
Use TypeScript, include prop types and a default export.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
/component Button src/ui
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Shell interpolation
|
|
83
|
+
|
|
84
|
+
Use `` !`cmd` `` to inject shell output into the prompt at expansion time.
|
|
85
|
+
Commands time out after 10 seconds.
|
|
86
|
+
|
|
87
|
+
```md
|
|
88
|
+
---
|
|
89
|
+
description: Review failing tests
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
The following tests are currently failing:
|
|
93
|
+
|
|
94
|
+
!`bun test 2>&1 | grep "fail\|✗" | head -20`
|
|
95
|
+
|
|
96
|
+
Investigate the failures and suggest fixes. Read the relevant source
|
|
97
|
+
files before drawing conclusions.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
/fix-tests
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Model override
|
|
105
|
+
|
|
106
|
+
Specify a model in frontmatter to use a faster or cheaper model for
|
|
107
|
+
lightweight tasks regardless of what the session is currently set to.
|
|
108
|
+
|
|
109
|
+
```md
|
|
110
|
+
---
|
|
111
|
+
description: Quick grep for a symbol
|
|
112
|
+
model: zen/claude-3-5-haiku
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
Find all usages of $ARGUMENTS across the codebase using grep and glob.
|
|
116
|
+
List each occurrence with file path and line number. No explanations needed.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Large models for deep analysis, small models for search and lookup.
|
|
120
|
+
|
|
121
|
+
## Precedence
|
|
122
|
+
|
|
123
|
+
Custom commands shadow built-ins. If you create `.agents/commands/review.md`
|
|
124
|
+
it will replace the built-in `/review` for that project.
|
|
125
|
+
|
|
126
|
+
## Listing commands
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
/help
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Custom commands are listed at the bottom under **custom commands**, tagged
|
|
133
|
+
with `(local)` or `(global)`.
|
package/docs/skills.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Skills
|
|
2
|
+
|
|
3
|
+
A skill is a reusable instruction file injected inline into your prompt.
|
|
4
|
+
Use `@skill-name` to load it — the content is inserted into the message
|
|
5
|
+
before it's sent to the LLM.
|
|
6
|
+
|
|
7
|
+
> **Skills are never auto-loaded.** They must be explicitly referenced
|
|
8
|
+
> with `@skill-name` in your prompt. Nothing is injected automatically.
|
|
9
|
+
|
|
10
|
+
## Where to put them
|
|
11
|
+
|
|
12
|
+
Each skill is a folder containing a `SKILL.md`:
|
|
13
|
+
|
|
14
|
+
| Location | Scope |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `.agents/skills/<name>/SKILL.md` | Current repo only |
|
|
17
|
+
| `~/.agents/skills/<name>/SKILL.md` | All projects (global) |
|
|
18
|
+
|
|
19
|
+
Local skills override global ones with the same name.
|
|
20
|
+
|
|
21
|
+
## Create a skill
|
|
22
|
+
|
|
23
|
+
The folder name becomes the skill name (unless overridden by `name:` in frontmatter).
|
|
24
|
+
|
|
25
|
+
`.agents/skills/conventional-commits/SKILL.md`:
|
|
26
|
+
|
|
27
|
+
```md
|
|
28
|
+
---
|
|
29
|
+
name: conventional-commits
|
|
30
|
+
description: Conventional commit message format rules
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# Conventional Commits
|
|
34
|
+
|
|
35
|
+
All commit messages must follow this format:
|
|
36
|
+
|
|
37
|
+
<type>(<scope>): <short summary>
|
|
38
|
+
|
|
39
|
+
Types: feat, fix, docs, refactor, test, chore
|
|
40
|
+
- Summary is lowercase, no period at the end
|
|
41
|
+
- Breaking changes: add `!` after type, e.g. `feat!:`
|
|
42
|
+
- Body is optional, wrapped at 72 chars
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then in the REPL:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
@conventional-commits write a commit message for my staged changes
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The skill content is wrapped in `<skill name="…">…</skill>` tags and
|
|
52
|
+
included in the message sent to the LLM.
|
|
53
|
+
|
|
54
|
+
## Frontmatter fields
|
|
55
|
+
|
|
56
|
+
| Field | Required | Description |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `name` | No | Skill name for `@` reference. Defaults to folder name. |
|
|
59
|
+
| `description` | No | Shown in `/help`. Defaults to name. |
|
|
60
|
+
|
|
61
|
+
## Tab completion
|
|
62
|
+
|
|
63
|
+
Type `@` and press `Tab` to autocomplete skill names alongside files.
|
|
64
|
+
|
|
65
|
+
## Listing skills
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
/help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Skills are listed in yellow, tagged `(local)` or `(global)`.
|