@jaggerxtrm/specialists 2.1.4 → 2.1.6
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 +15 -4
- package/bin/install.js +32 -15
- package/dist/index.js +49 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ Specialists are `.specialist.yaml` files that define an autonomous agent: its mo
|
|
|
19
19
|
| Scope | Location | Purpose |
|
|
20
20
|
|-------|----------|---------|
|
|
21
21
|
| **project** | `./specialists/` | Per-project specialists |
|
|
22
|
-
| **user** | `~/.agents/specialists/` |
|
|
23
|
-
| **system** | bundled with the package |
|
|
22
|
+
| **user** | `~/.agents/specialists/` | Built-in defaults (copied on install) + your own |
|
|
23
|
+
| **system** | bundled with the package | Fallback if user scope is empty |
|
|
24
24
|
|
|
25
25
|
When a specialist runs, the server spawns a `pi` subprocess with the right model, tools, and system prompt injected. Output streams back in real time via cursor-based polling.
|
|
26
26
|
|
|
@@ -93,7 +93,7 @@ npm install -g @jaggerxtrm/specialists
|
|
|
93
93
|
specialists install
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
Installs: **pi** (`@mariozechner/pi-coding-agent`), **beads** (`@beads/bd`), **dolt** (interactive sudo on Linux / brew on macOS), registers the `specialists` MCP at user scope, scaffolds `~/.agents/specialists
|
|
96
|
+
Installs: **pi** (`@mariozechner/pi-coding-agent`), **beads** (`@beads/bd`), **dolt** (interactive sudo on Linux / brew on macOS), registers the `specialists` MCP at user scope, scaffolds `~/.agents/specialists/`, and installs the `main-guard` PreToolUse hook (`~/.claude/hooks/main-guard.mjs`) to protect `main`/`master` branches from direct edits.
|
|
97
97
|
|
|
98
98
|
After running, **restart Claude Code** to load the MCP. Re-run `specialists install` at any time to update or repair the installation.
|
|
99
99
|
|
|
@@ -170,8 +170,19 @@ specialist:
|
|
|
170
170
|
|
|
171
171
|
communication:
|
|
172
172
|
publishes: [result]
|
|
173
|
+
|
|
174
|
+
# Optional: run scripts before/after the specialist
|
|
175
|
+
skills:
|
|
176
|
+
scripts:
|
|
177
|
+
- path: ./scripts/health-check.sh
|
|
178
|
+
phase: pre # runs before the task prompt
|
|
179
|
+
inject_output: true # output injected as $pre_script_output
|
|
180
|
+
- path: ./scripts/cleanup.sh
|
|
181
|
+
phase: post # runs after the specialist completes
|
|
173
182
|
```
|
|
174
183
|
|
|
184
|
+
Pre-script output is formatted as `<pre_flight_context>` XML and available in `task_template` via `$pre_script_output`. Scripts run locally via the host shell — not inside the pi agent. Failed scripts include their exit code so the specialist can reason about failures.
|
|
185
|
+
|
|
175
186
|
**Model IDs** use the full provider/model format: `anthropic/claude-sonnet-4-6`, `google-gemini-cli/gemini-3-flash-preview`, `anthropic/claude-haiku-4-5`.
|
|
176
187
|
|
|
177
188
|
---
|
|
@@ -187,7 +198,7 @@ bun test
|
|
|
187
198
|
```
|
|
188
199
|
|
|
189
200
|
- **Build**: `bun build src/index.ts --target=node --outfile=dist/index.js`
|
|
190
|
-
- **Test**: `bun --bun vitest run` (
|
|
201
|
+
- **Test**: `bun --bun vitest run` (68 unit tests)
|
|
191
202
|
- **Lint**: `tsc --noEmit`
|
|
192
203
|
|
|
193
204
|
See [CLAUDE.md](CLAUDE.md) for the full architecture guide and [ROADMAP.md](ROADMAP.md) for planned features.
|
package/bin/install.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Usage: npx --package=@jaggerxtrm/specialists install
|
|
4
4
|
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, chmodSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
@@ -16,6 +16,9 @@ const HOOK_FILE = join(HOOKS_DIR, 'specialists-main-guard.mjs');
|
|
|
16
16
|
const MCP_NAME = 'specialists';
|
|
17
17
|
const GITHUB_PKG = '@jaggerxtrm/specialists';
|
|
18
18
|
|
|
19
|
+
// Bundled specialists dir — resolved relative to this file (bin/../specialists/)
|
|
20
|
+
const BUNDLED_SPECIALISTS_DIR = new URL('../specialists', import.meta.url).pathname;
|
|
21
|
+
|
|
19
22
|
// ── ANSI helpers ──────────────────────────────────────────────────────────────
|
|
20
23
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
21
24
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
@@ -136,15 +139,13 @@ const HOOK_ENTRY = {
|
|
|
136
139
|
};
|
|
137
140
|
|
|
138
141
|
function installHook() {
|
|
139
|
-
// 1. Write hook script
|
|
140
142
|
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
141
143
|
writeFileSync(HOOK_FILE, HOOK_SCRIPT, 'utf8');
|
|
142
144
|
chmodSync(HOOK_FILE, 0o755);
|
|
143
145
|
|
|
144
|
-
// 2. Merge into ~/.claude/settings.json
|
|
145
146
|
let settings = {};
|
|
146
147
|
if (existsSync(SETTINGS_FILE)) {
|
|
147
|
-
try { settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8')); } catch {
|
|
148
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8')); } catch {}
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
if (!Array.isArray(settings.hooks?.PreToolUse)) {
|
|
@@ -152,7 +153,6 @@ function installHook() {
|
|
|
152
153
|
settings.hooks.PreToolUse = [];
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
// Idempotent: remove any previous specialists-main-guard entry, re-add
|
|
156
156
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse
|
|
157
157
|
.filter(e => !e.hooks?.some(h => h.command?.includes('specialists-main-guard')));
|
|
158
158
|
settings.hooks.PreToolUse.push(HOOK_ENTRY);
|
|
@@ -199,23 +199,39 @@ registered
|
|
|
199
199
|
? ok(`MCP '${MCP_NAME}' registered at user scope`)
|
|
200
200
|
: skip(`MCP '${MCP_NAME}' already registered`);
|
|
201
201
|
|
|
202
|
-
// 5. Scaffold
|
|
203
|
-
section('
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
202
|
+
// 5. Scaffold + copy built-in specialists
|
|
203
|
+
section('Specialists');
|
|
204
|
+
mkdirSync(SPECIALISTS_DIR, { recursive: true });
|
|
205
|
+
|
|
206
|
+
const yamlFiles = existsSync(BUNDLED_SPECIALISTS_DIR)
|
|
207
|
+
? readdirSync(BUNDLED_SPECIALISTS_DIR).filter(f => f.endsWith('.specialist.yaml'))
|
|
208
|
+
: [];
|
|
209
|
+
|
|
210
|
+
let installed = 0;
|
|
211
|
+
let skipped = 0;
|
|
212
|
+
for (const file of yamlFiles) {
|
|
213
|
+
const dest = join(SPECIALISTS_DIR, file);
|
|
214
|
+
if (existsSync(dest)) {
|
|
215
|
+
skipped++;
|
|
216
|
+
} else {
|
|
217
|
+
copyFileSync(join(BUNDLED_SPECIALISTS_DIR, file), dest);
|
|
218
|
+
installed++;
|
|
219
|
+
}
|
|
209
220
|
}
|
|
210
221
|
|
|
222
|
+
if (installed > 0) ok(`${installed} specialist(s) installed → ~/.agents/specialists/`);
|
|
223
|
+
if (skipped > 0) skip(`${skipped} specialist(s) already exist (user-modified, keeping)`);
|
|
224
|
+
if (installed === 0 && skipped === 0) skip('No built-in specialists found');
|
|
225
|
+
info('Edit any .specialist.yaml in ~/.agents/specialists/ to customise models, prompts, permissions');
|
|
226
|
+
|
|
211
227
|
// 6. Claude Code hooks
|
|
212
228
|
section('Claude Code hooks');
|
|
213
229
|
const hookExisted = existsSync(HOOK_FILE);
|
|
214
230
|
installHook();
|
|
215
231
|
hookExisted
|
|
216
232
|
? ok('main-guard hook updated')
|
|
217
|
-
: ok('main-guard hook installed → ~/.claude/hooks/specialists-main-guard.
|
|
218
|
-
info('Blocks Edit/Write/git commit/push on main or master branch
|
|
233
|
+
: ok('main-guard hook installed → ~/.claude/hooks/specialists-main-guard.mjs');
|
|
234
|
+
info('Blocks Edit/Write/git commit/push on main or master branch');
|
|
219
235
|
|
|
220
236
|
// 7. Health check
|
|
221
237
|
section('Health check');
|
|
@@ -231,4 +247,5 @@ console.log('\n' + bold(green(' Done!')));
|
|
|
231
247
|
console.log('\n' + bold(' Next steps:'));
|
|
232
248
|
console.log(` 1. ${bold('Configure pi:')} run ${yellow('pi')} then ${yellow('pi config')} to enable model providers`);
|
|
233
249
|
console.log(` 2. ${bold('Restart Claude Code')} to load the MCP and hooks`);
|
|
234
|
-
console.log(` 3. ${bold('
|
|
250
|
+
console.log(` 3. ${bold('Customise specialists:')} edit files in ${yellow('~/.agents/specialists/')}`);
|
|
251
|
+
console.log(` 4. ${bold('Update later:')} re-run this installer (existing specialists preserved)\n`);
|
package/dist/index.js
CHANGED
|
@@ -24916,15 +24916,15 @@ class PiAgentSession {
|
|
|
24916
24916
|
_doneReject;
|
|
24917
24917
|
_agentEndReceived = false;
|
|
24918
24918
|
_killed = false;
|
|
24919
|
+
_lineBuffer = "";
|
|
24919
24920
|
meta;
|
|
24920
24921
|
constructor(options, meta) {
|
|
24921
24922
|
this.options = options;
|
|
24922
24923
|
this.meta = meta;
|
|
24923
24924
|
}
|
|
24924
24925
|
static async create(options) {
|
|
24925
|
-
const provider = mapSpecialistBackend(options.model);
|
|
24926
24926
|
const meta = {
|
|
24927
|
-
backend:
|
|
24927
|
+
backend: options.model.includes("/") ? options.model.split("/")[0] : mapSpecialistBackend(options.model),
|
|
24928
24928
|
model: options.model,
|
|
24929
24929
|
sessionId: crypto.randomUUID(),
|
|
24930
24930
|
startedAt: new Date
|
|
@@ -24940,7 +24940,6 @@ class PiAgentSession {
|
|
|
24940
24940
|
"rpc",
|
|
24941
24941
|
...providerArgs,
|
|
24942
24942
|
"--no-session",
|
|
24943
|
-
"--print",
|
|
24944
24943
|
...extraArgs
|
|
24945
24944
|
];
|
|
24946
24945
|
const toolsFlag = mapPermissionToTools(this.options.permissionLevel);
|
|
@@ -24958,9 +24957,19 @@ class PiAgentSession {
|
|
|
24958
24957
|
});
|
|
24959
24958
|
this._donePromise = donePromise;
|
|
24960
24959
|
this.proc.stdout?.on("data", (chunk) => {
|
|
24961
|
-
|
|
24962
|
-
|
|
24963
|
-
|
|
24960
|
+
this._lineBuffer += chunk.toString();
|
|
24961
|
+
const lines = this._lineBuffer.split(`
|
|
24962
|
+
`);
|
|
24963
|
+
this._lineBuffer = lines.pop() ?? "";
|
|
24964
|
+
for (const line of lines) {
|
|
24965
|
+
if (line.trim())
|
|
24966
|
+
this._handleEvent(line);
|
|
24967
|
+
}
|
|
24968
|
+
});
|
|
24969
|
+
this.proc.stdout?.on("end", () => {
|
|
24970
|
+
if (this._lineBuffer.trim()) {
|
|
24971
|
+
this._handleEvent(this._lineBuffer);
|
|
24972
|
+
this._lineBuffer = "";
|
|
24964
24973
|
}
|
|
24965
24974
|
});
|
|
24966
24975
|
this.proc.on("close", (code) => {
|
|
@@ -25049,6 +25058,7 @@ class PiAgentSession {
|
|
|
25049
25058
|
const msg = JSON.stringify({ type: "prompt", message: task }) + `
|
|
25050
25059
|
`;
|
|
25051
25060
|
this.proc?.stdin?.write(msg);
|
|
25061
|
+
this.proc?.stdin?.end();
|
|
25052
25062
|
}
|
|
25053
25063
|
async waitForDone() {
|
|
25054
25064
|
return this._donePromise;
|
|
@@ -25056,26 +25066,6 @@ class PiAgentSession {
|
|
|
25056
25066
|
async getLastOutput() {
|
|
25057
25067
|
return this._lastOutput;
|
|
25058
25068
|
}
|
|
25059
|
-
async executeBash(command) {
|
|
25060
|
-
return new Promise((resolve) => {
|
|
25061
|
-
const id = crypto.randomUUID();
|
|
25062
|
-
const handler = (chunk) => {
|
|
25063
|
-
for (const line of chunk.toString().split(`
|
|
25064
|
-
`).filter(Boolean)) {
|
|
25065
|
-
try {
|
|
25066
|
-
const ev = JSON.parse(line);
|
|
25067
|
-
if (ev.id === id) {
|
|
25068
|
-
this.proc?.stdout?.off("data", handler);
|
|
25069
|
-
resolve(ev.output ?? ev.data?.output ?? "");
|
|
25070
|
-
}
|
|
25071
|
-
} catch {}
|
|
25072
|
-
}
|
|
25073
|
-
};
|
|
25074
|
-
this.proc?.stdout?.on("data", handler);
|
|
25075
|
-
this.proc?.stdin?.write(JSON.stringify({ type: "bash", command, id }) + `
|
|
25076
|
-
`);
|
|
25077
|
-
});
|
|
25078
|
-
}
|
|
25079
25069
|
kill() {
|
|
25080
25070
|
this._killed = true;
|
|
25081
25071
|
this.proc?.kill();
|
|
@@ -25145,6 +25135,32 @@ function shouldCreateBead(beadsIntegration, permissionRequired) {
|
|
|
25145
25135
|
}
|
|
25146
25136
|
|
|
25147
25137
|
// src/specialist/runner.ts
|
|
25138
|
+
import { execSync } from "node:child_process";
|
|
25139
|
+
import { basename } from "node:path";
|
|
25140
|
+
function runScript(scriptPath) {
|
|
25141
|
+
try {
|
|
25142
|
+
const output = execSync(scriptPath, { encoding: "utf8", timeout: 30000 });
|
|
25143
|
+
return { name: basename(scriptPath), output, exitCode: 0 };
|
|
25144
|
+
} catch (e) {
|
|
25145
|
+
return { name: basename(scriptPath), output: e.stdout ?? e.message ?? "", exitCode: e.status ?? 1 };
|
|
25146
|
+
}
|
|
25147
|
+
}
|
|
25148
|
+
function formatScriptOutput(results) {
|
|
25149
|
+
const withOutput = results.filter((r) => r.output.trim());
|
|
25150
|
+
if (withOutput.length === 0)
|
|
25151
|
+
return "";
|
|
25152
|
+
const blocks = withOutput.map((r) => {
|
|
25153
|
+
const status = r.exitCode === 0 ? "" : ` exit_code="${r.exitCode}"`;
|
|
25154
|
+
return `<script name="${r.name}"${status}>
|
|
25155
|
+
${r.output.trim()}
|
|
25156
|
+
</script>`;
|
|
25157
|
+
}).join(`
|
|
25158
|
+
`);
|
|
25159
|
+
return `<pre_flight_context>
|
|
25160
|
+
${blocks}
|
|
25161
|
+
</pre_flight_context>`;
|
|
25162
|
+
}
|
|
25163
|
+
|
|
25148
25164
|
class SpecialistRunner {
|
|
25149
25165
|
deps;
|
|
25150
25166
|
sessionFactory;
|
|
@@ -25168,7 +25184,10 @@ class SpecialistRunner {
|
|
|
25168
25184
|
circuit_breaker_state: circuitBreaker.getState(model),
|
|
25169
25185
|
scope: "project"
|
|
25170
25186
|
});
|
|
25171
|
-
const
|
|
25187
|
+
const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
|
|
25188
|
+
const preResults = preScripts.map((s) => runScript(s.path)).filter((_, i) => preScripts[i].inject_output);
|
|
25189
|
+
const preScriptOutput = formatScriptOutput(preResults);
|
|
25190
|
+
const variables = { prompt: options.prompt, pre_script_output: preScriptOutput, ...options.variables };
|
|
25172
25191
|
const renderedTask = renderTemplate(prompt.task_template, variables);
|
|
25173
25192
|
const promptHash = createHash("sha256").update(renderedTask).digest("hex").slice(0, 16);
|
|
25174
25193
|
await hooks.emit("post_render", invocationId, metadata.name, metadata.version, {
|
|
@@ -25233,22 +25252,12 @@ You have access via Bash:
|
|
|
25233
25252
|
});
|
|
25234
25253
|
await session.start();
|
|
25235
25254
|
onKillRegistered?.(session.kill.bind(session));
|
|
25236
|
-
|
|
25237
|
-
let preScriptOutput = "";
|
|
25238
|
-
for (const script of preScripts) {
|
|
25239
|
-
const out = await session.executeBash(script.path);
|
|
25240
|
-
if (script.inject_output)
|
|
25241
|
-
preScriptOutput += out + `
|
|
25242
|
-
`;
|
|
25243
|
-
}
|
|
25244
|
-
const finalTask = preScriptOutput ? renderTemplate(renderedTask, { pre_script_output: preScriptOutput.trim() }) : renderedTask;
|
|
25245
|
-
await session.prompt(finalTask);
|
|
25255
|
+
await session.prompt(renderedTask);
|
|
25246
25256
|
await session.waitForDone();
|
|
25247
25257
|
output = await session.getLastOutput();
|
|
25248
25258
|
const postScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "post") ?? [];
|
|
25249
|
-
for (const script of postScripts)
|
|
25250
|
-
|
|
25251
|
-
}
|
|
25259
|
+
for (const script of postScripts)
|
|
25260
|
+
runScript(script.path);
|
|
25252
25261
|
circuitBreaker.recordSuccess(model);
|
|
25253
25262
|
} catch (err) {
|
|
25254
25263
|
circuitBreaker.recordFailure(model);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaggerxtrm/specialists",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
4
|
"description": "OmniSpecialist — 7-tool MCP orchestration layer powered by the Specialist System. Discover and execute .specialist.yaml files across project/user/system scopes via pi.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|