@jaggerxtrm/specialists 2.1.3 → 2.1.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 CHANGED
@@ -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
 
package/bin/install.js CHANGED
@@ -12,7 +12,7 @@ const SPECIALISTS_DIR = join(HOME, '.agents', 'specialists');
12
12
  const CLAUDE_DIR = join(HOME, '.claude');
13
13
  const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
14
14
  const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
15
- const HOOK_FILE = join(HOOKS_DIR, 'specialists-main-guard.sh');
15
+ 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
 
@@ -78,43 +78,56 @@ function registerMCP() {
78
78
 
79
79
  // ── Hook installation ─────────────────────────────────────────────────────────
80
80
 
81
- const HOOK_SCRIPT = `#!/usr/bin/env bash
82
- # specialists — Claude Code PreToolUse hook
83
- # Blocks writes and git commit/push on main/master branch.
84
- # Exit 0: allow | Exit 2: block (message shown to user)
85
- #
86
- # Installed by: npx --package=@jaggerxtrm/specialists install
87
-
88
- BRANCH=$(git branch --show-current 2>/dev/null)
89
-
90
- # Not in a git repo or not on a protected branch — allow
91
- if [ -z "$BRANCH" ] || { [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; }; then
92
- exit 0
93
- fi
94
-
95
- INPUT=$(cat)
96
- TOOL=$(echo "$INPUT" | jq -r '.tool_name' 2>/dev/null)
97
-
98
- BLOCK_MSG="⛔ Direct edits on '$BRANCH' are not allowed.
99
- Create a feature branch first: git checkout -b feature/<name>"
100
-
101
- case "$TOOL" in
102
- Edit|Write|MultiEdit|NotebookEdit)
103
- echo "$BLOCK_MSG" >&2
104
- exit 2
105
- ;;
106
- Bash)
107
- CMD=$(echo "$INPUT" | jq -r '.tool_input.command' 2>/dev/null)
108
- if echo "$CMD" | grep -qE '^git (commit|push)'; then
109
- echo "$BLOCK_MSG" >&2
110
- exit 2
111
- fi
112
- exit 0
113
- ;;
114
- *)
115
- exit 0
116
- ;;
117
- esac
81
+ const HOOK_SCRIPT = `#!/usr/bin/env node
82
+ // specialists — Claude Code PreToolUse hook
83
+ // Blocks writes and git commit/push on main/master branch.
84
+ // Exit 0: allow | Exit 2: block (message shown to user)
85
+ //
86
+ // Installed by: npx --package=@jaggerxtrm/specialists install
87
+
88
+ import { execSync } from 'node:child_process';
89
+ import { readFileSync } from 'node:fs';
90
+
91
+ let branch = '';
92
+ try {
93
+ branch = execSync('git branch --show-current', {
94
+ encoding: 'utf8',
95
+ stdio: ['pipe', 'pipe', 'pipe'],
96
+ }).trim();
97
+ } catch {}
98
+
99
+ if (!branch || (branch !== 'main' && branch !== 'master')) {
100
+ process.exit(0);
101
+ }
102
+
103
+ let input;
104
+ try {
105
+ input = JSON.parse(readFileSync(0, 'utf8'));
106
+ } catch {
107
+ process.exit(0);
108
+ }
109
+
110
+ const tool = input.tool_name ?? '';
111
+ const blockMsg =
112
+ \`⛔ Direct edits on '\${branch}' are not allowed.\\n\` +
113
+ \`Create a feature branch first: git checkout -b feature/<name>\`;
114
+
115
+ const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
116
+
117
+ if (WRITE_TOOLS.has(tool)) {
118
+ process.stderr.write(blockMsg + '\\n');
119
+ process.exit(2);
120
+ }
121
+
122
+ if (tool === 'Bash') {
123
+ const cmd = input.tool_input?.command ?? '';
124
+ if (/^git (commit|push)/.test(cmd)) {
125
+ process.stderr.write(blockMsg + '\\n');
126
+ process.exit(2);
127
+ }
128
+ }
129
+
130
+ process.exit(0);
118
131
  `;
119
132
 
120
133
  const HOOK_ENTRY = {
@@ -202,7 +215,7 @@ installHook();
202
215
  hookExisted
203
216
  ? ok('main-guard hook updated')
204
217
  : ok('main-guard hook installed → ~/.claude/hooks/specialists-main-guard.sh');
205
- info('Blocks Edit/Write/git commit/push on main or master branch');
218
+ info('Blocks Edit/Write/git commit/push on main or master branch (JS, no jq needed)');
206
219
 
207
220
  // 7. Health check
208
221
  section('Health check');
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: provider,
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
- for (const line of chunk.toString().split(`
24962
- `).filter(Boolean)) {
24963
- this._handleEvent(line);
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 variables = { prompt: options.prompt, ...options.variables };
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
- const preScripts = spec.specialist.skills?.scripts?.filter((s) => s.phase === "pre") ?? [];
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
- await session.executeBash(script.path);
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",
3
+ "version": "2.1.5",
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",