xtrm-tools 2.1.17 → 2.1.21

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.
@@ -8,38 +8,27 @@
8
8
  // ── Shared workflow steps ────────────────────────────────────────────────────
9
9
 
10
10
  export const WORKFLOW_STEPS =
11
- ' 1. git checkout -b feature/<name> ← start here\n' +
12
- ' 2. bd create + bd update in_progress track your work\n' +
13
- ' 3. Edit files / write code\n' +
11
+ ' 1. git checkout -b feature/<name>\n' +
12
+ ' 2. bd create + bd update in_progress\n' +
13
+ ' 3. Edit / write code\n' +
14
14
  ' 4. bd close <id> && git add && git commit\n' +
15
15
  ' 5. git push -u origin feature/<name>\n' +
16
16
  ' 6. gh pr create --fill && gh pr merge --squash\n' +
17
- ' 7. git checkout master && git reset --hard origin/master\n';
17
+ ' 7. git checkout master && git reset --hard origin/master\n';;
18
18
 
19
19
  export const SESSION_CLOSE_PROTOCOL =
20
- ' 3. bd close <id1> <id2> ... close all in_progress issues\n' +
21
- ' 4. git add <files> && git commit -m "..." commit your changes\n' +
22
- ' 5. git push -u origin <feature-branch> push feature branch\n' +
23
- ' 6. gh pr create --fill create PR\n' +
24
- ' 7. gh pr merge --squash merge PR\n' +
25
- ' 8. git checkout master && git reset --hard origin/master\n';
20
+ ' bd close <id> commit push → gh pr create --fill → gh pr merge --squash\n';;
26
21
 
27
22
  export const COMMIT_NEXT_STEPS =
28
- ' 3. bd close <id1> <id2> ... ← you are here\n' +
29
- ' 4. git add <files> && git commit -m "..."\n' +
30
- ' 5. git push -u origin <feature-branch>\n' +
31
- ' 6. gh pr create --fill && gh pr merge --squash\n' +
32
- ' 7. git checkout master && git reset --hard origin/master\n';
23
+ ' bd close <id> && git add && git commit\n' +
24
+ ' git push -u origin <feature-branch>\n' +
25
+ ' gh pr create --fill && gh pr merge --squash\n';;
33
26
 
34
27
  // ── Edit gate messages ───────────────────────────────────────────────────────
35
28
 
36
29
  export function editBlockMessage(sessionId) {
37
30
  return (
38
- '🚫 BEADS GATE: This session has no active claim — claim an issue before editing files.\n\n' +
39
- ' bd update <id> --status=in_progress\n' +
40
- ` bd kv set "claimed:${sessionId}" "<id>"\n\n` +
41
- 'Or create a new issue:\n' +
42
- ' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
31
+ '🚫 No active claim — claim an issue first.\n' +
43
32
  ' bd update <id> --status=in_progress\n' +
44
33
  ` bd kv set "claimed:${sessionId}" "<id>"\n`
45
34
  );
@@ -47,11 +36,9 @@ export function editBlockMessage(sessionId) {
47
36
 
48
37
  export function editBlockFallbackMessage() {
49
38
  return (
50
- '🚫 BEADS GATE: No active issue — create one before editing files.\n\n' +
51
- ' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
52
- ' bd update <id> --status=in_progress\n\n' +
53
- 'Full workflow (do this every session):\n' +
54
- WORKFLOW_STEPS
39
+ '🚫 No active issue — create one before editing.\n' +
40
+ ' bd create --title="<task>" --type=task --priority=2\n' +
41
+ ' bd update <id> --status=in_progress\n'
55
42
  );
56
43
  }
57
44
 
@@ -60,8 +47,8 @@ export function editBlockFallbackMessage() {
60
47
  export function commitBlockMessage(summary, claimed) {
61
48
  const issueSummary = summary ?? ` Claimed: ${claimed}`;
62
49
  return (
63
- '🚫 BEADS GATE: Close open issues before committing.\n\n' +
64
- `Open issues:\n${issueSummary}\n\n` +
50
+ '🚫 Close open issues before committing.\n\n' +
51
+ `${issueSummary}\n\n` +
65
52
  'Next steps:\n' + COMMIT_NEXT_STEPS
66
53
  );
67
54
  }
@@ -71,9 +58,9 @@ export function commitBlockMessage(summary, claimed) {
71
58
  export function stopBlockMessage(summary, claimed) {
72
59
  const issueSummary = summary ?? ` Claimed: ${claimed}`;
73
60
  return (
74
- '🚫 BEADS STOP GATE: Unresolved issues — complete the session close protocol.\n\n' +
75
- `Open issues:\n${issueSummary}\n\n` +
76
- 'Session close protocol:\n' + SESSION_CLOSE_PROTOCOL
61
+ '🚫 Unresolved issues — close before stopping.\n\n' +
62
+ `${issueSummary}\n\n` +
63
+ SESSION_CLOSE_PROTOCOL
77
64
  );
78
65
  }
79
66
 
@@ -81,12 +68,8 @@ export function stopBlockMessage(summary, claimed) {
81
68
 
82
69
  export function memoryPromptMessage() {
83
70
  return (
84
- '🧠 MEMORY GATE: Before ending the session, evaluate this session\'s work.\n\n' +
85
- 'For each issue you worked on and closed, ask:\n' +
86
- ' Is this a stable pattern, key decision, or solution I\'ll encounter again?\n\n' +
87
- ' YES → bd remember "<precise, durable insight>"\n' +
88
- ' NO → explicitly note "nothing worth persisting" and continue\n\n' +
89
- 'When done, signal completion and stop again:\n' +
90
- ' touch .beads/.memory-gate-done\n'
71
+ '🧠 Memory gate: for each closed issue, worth persisting?\n' +
72
+ ' YES bd remember "<insight>" NO note "nothing to persist"\n' +
73
+ ' touch .beads/.memory-gate-done when done.\n'
91
74
  );
92
75
  }
@@ -55,28 +55,16 @@ const WRITE_TOOLS = new Set([
55
55
  ]);
56
56
 
57
57
  if (WRITE_TOOLS.has(tool)) {
58
- deny(
59
- `⛔ You are on '${branch}' never edit files directly on master.\n\n` +
60
- 'Full workflow:\n' +
61
- ' 1. git checkout -b feature/<name> ← start here\n' +
62
- ' 2. bd create + bd update in_progress track your work\n' +
63
- ' 3. Edit files / write code\n' +
64
- ' 4. bd close <id> && git add && git commit\n' +
65
- ' 5. git push -u origin feature/<name>\n' +
66
- ' 6. gh pr create --fill && gh pr merge --squash\n' +
67
- ' 7. git checkout master && git reset --hard origin/master\n'
68
- );
58
+ deny(`⛔ On '${branch}' — checkout a feature branch first.\n`
59
+ + ' git checkout -b feature/<name>\n');
69
60
  }
70
61
 
71
62
  const WORKFLOW =
72
- 'Full workflow:\n' +
73
- ' 1. git checkout -b feature/<name> \u2190 start here\n' +
74
- ' 2. bd create + bd update in_progress track your work\n' +
75
- ' 3. Edit files / write code\n' +
76
- ' 4. bd close <id> && git add && git commit\n' +
77
- ' 5. git push -u origin feature/<name>\n' +
78
- ' 6. gh pr create --fill && gh pr merge --squash\n' +
79
- ' 7. git checkout master && git reset --hard origin/master\n';
63
+ ' 1. git checkout -b feature/<name>\n'
64
+ + ' 2. bd create + bd update in_progress\n'
65
+ + ' 3. bd close <id> && git add && git commit\n'
66
+ + ' 4. git push -u origin feature/<name>\n'
67
+ + ' 5. gh pr create --fill && gh pr merge --squash\n';
80
68
 
81
69
  if (tool === 'Bash') {
82
70
  const cmd = (input.tool_input?.command ?? '').trim().replace(/\s+/g, ' ');
@@ -90,17 +78,8 @@ if (tool === 'Bash') {
90
78
  // Must check BEFORE the gh allowlist pattern
91
79
  if (/^gh\s+pr\s+merge\b/.test(cmd)) {
92
80
  if (!/--squash\b/.test(cmd)) {
93
- deny(
94
- `\u26D4 Only squash merges are allowed — use 'gh pr merge --squash'\n\n` +
95
- 'Why squash?\n' +
96
- ' - Keeps history linear and easy to read\n' +
97
- ' - One commit per PR = easy to revert\n' +
98
- ' - Matches the workflow documented in AGENTS.md\n\n' +
99
- 'Correct usage:\n' +
100
- ' gh pr merge --squash\n\n' +
101
- 'If you really need a merge commit, use:\n' +
102
- ' MAIN_GUARD_ALLOW_BASH=1 gh pr merge --merge\n'
103
- );
81
+ deny('⛔ Squash only: gh pr merge --squash\n'
82
+ + ' (override: MAIN_GUARD_ALLOW_BASH=1 gh pr merge --merge)\n');
104
83
  }
105
84
  // --squash present — allow
106
85
  process.exit(0);
@@ -129,10 +108,8 @@ if (tool === 'Bash') {
129
108
 
130
109
  // Specific messages for common blocked operations
131
110
  if (/^git\s+commit\b/.test(cmd)) {
132
- deny(
133
- `\u26D4 Don't commit directly to '${branch}' \u2014 use a feature branch.\n\n` +
134
- WORKFLOW
135
- );
111
+ deny(`⛔ No commits on '${branch}' — use a feature branch.\n`
112
+ + ' git checkout -b feature/<name>\n');
136
113
  }
137
114
 
138
115
  if (/^git\s+push\b/.test(cmd)) {
@@ -141,36 +118,17 @@ if (tool === 'Bash') {
141
118
  const explicitProtected = protectedBranches.some(b => lastToken === b || lastToken.endsWith(`:${b}`));
142
119
  const impliedProtected = tokens.length <= 3 && protectedBranches.includes(branch);
143
120
  if (explicitProtected || impliedProtected) {
144
- deny(
145
- `\u26D4 Don't push directly to '${branch}' \u2014 use the PR workflow.\n\n` +
146
- 'Next steps:\n' +
147
- ' 5. git push -u origin <feature-branch> \u2190 push your branch\n' +
148
- ' 6. gh pr create --fill create PR\n' +
149
- ' gh pr merge --squash merge it\n' +
150
- ' 7. git checkout master sync master\n' +
151
- ' git reset --hard origin/master\n\n' +
152
- "If you're not on a feature branch yet:\n" +
153
- ' git checkout -b feature/<name> (then re-commit and push)\n'
154
- );
121
+ deny(`⛔ No direct push to '${branch}' — push a feature branch and open a PR.\n`
122
+ + ' git push -u origin <feature-branch> && gh pr create --fill\n');
155
123
  }
156
124
  // Pushing to a feature branch — allow
157
125
  process.exit(0);
158
126
  }
159
127
 
160
128
  // Default deny — block everything else on protected branches
161
- deny(
162
- `\u26D4 Bash is restricted on '${branch}' \u2014 use a feature branch for file writes and script execution.\n\n` +
163
- 'Allowed on protected branches:\n' +
164
- ' git status / log / diff / branch / fetch / pull / stash\n' +
165
- ' git checkout -b <name> (create feature branch \u2014 the exit path)\n' +
166
- ' git switch -c <name> (same)\n' +
167
- ' git worktree / config\n' +
168
- ' gh <any> (GitHub CLI)\n' +
169
- ' bd <any> (beads issue tracking)\n\n' +
170
- 'To run arbitrary commands:\n' +
171
- ' 1. git checkout -b feature/<name> \u2190 move to a feature branch, or\n' +
172
- ' 2. MAIN_GUARD_ALLOW_BASH=1 <command> (escape hatch, use sparingly)\n'
173
- );
129
+ deny(`⛔ Bash restricted on '${branch}'. Allowed: git status/log/diff/pull/stash, gh, bd.\n`
130
+ + ' Exit: git checkout -b feature/<name>\n'
131
+ + ' Override: MAIN_GUARD_ALLOW_BASH=1 <cmd>\n');
174
132
  }
175
133
 
176
134
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "2.1.17",
3
+ "version": "2.1.21",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,45 +0,0 @@
1
- /**
2
- * oh-pi Git Checkpoint Extension
3
- *
4
- * Auto-stash before each turn, notify on agent completion.
5
- * Combines git-checkpoint + notify + dirty-repo-guard.
6
- */
7
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
-
9
- function terminalNotify(title: string, body: string): void {
10
- if (process.env.KITTY_WINDOW_ID) {
11
- process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
12
- process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
13
- } else {
14
- process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
15
- }
16
- }
17
-
18
- export default function (pi: ExtensionAPI) {
19
- let turnCount = 0;
20
-
21
- // Warn on dirty repo at session start
22
- pi.on("session_start", async (_event, ctx) => {
23
- try {
24
- const { stdout } = await pi.exec("git", ["status", "--porcelain"]);
25
- if (stdout.trim() && ctx.hasUI) {
26
- const lines = stdout.trim().split("\n").length;
27
- ctx.ui.notify(`⚠️ Dirty repo: ${lines} uncommitted change(s)`, "warning");
28
- }
29
- } catch { /* not a git repo, ignore */ }
30
- });
31
-
32
- // Stash checkpoint before each turn
33
- pi.on("turn_start", async () => {
34
- turnCount++;
35
- try {
36
- await pi.exec("git", ["stash", "create", "-m", `oh-pi-turn-${turnCount}`]);
37
- } catch { /* not a git repo */ }
38
- });
39
-
40
- // Notify when agent is done
41
- pi.on("agent_end", async () => {
42
- terminalNotify("oh-pi", `Done after ${turnCount} turn(s). Ready for input.`);
43
- turnCount = 0;
44
- });
45
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * oh-pi Safe Guard Extension
3
- *
4
- * Combines destructive command confirmation + protected paths in one extension.
5
- */
6
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
-
8
- export const DANGEROUS_PATTERNS = [
9
- /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*-rf\b|.*--force\b)/,
10
- /\bsudo\s+rm\b/,
11
- /\b(DROP|TRUNCATE|DELETE\s+FROM)\b/i,
12
- /\bchmod\s+777\b/,
13
- /\bmkfs\b/,
14
- /\bdd\s+if=/,
15
- />\s*\/dev\/sd[a-z]/,
16
- ];
17
-
18
- export const PROTECTED_PATHS = [".env", ".git/", "node_modules/", ".pi/", "id_rsa", ".ssh/"];
19
-
20
- export default function (pi: ExtensionAPI) {
21
- pi.on("tool_call", async (event, ctx) => {
22
- // Check bash commands for dangerous patterns
23
- if (event.toolName === "bash") {
24
- const cmd = (event.input as { command?: string }).command ?? "";
25
- const match = DANGEROUS_PATTERNS.find((p) => p.test(cmd));
26
- if (match && ctx.hasUI) {
27
- const ok = await ctx.ui.confirm("⚠️ Dangerous Command", `Execute: ${cmd}?`);
28
- if (!ok) return { block: true, reason: "Blocked by user" };
29
- }
30
- }
31
-
32
- // Check write/edit for protected paths
33
- if (event.toolName === "write" || event.toolName === "edit") {
34
- const path = (event.input as { path?: string }).path ?? "";
35
- const hit = PROTECTED_PATHS.find((p) => path.includes(p));
36
- if (hit) {
37
- if (ctx.hasUI) {
38
- const ok = await ctx.ui.confirm("🛡️ Protected Path", `Allow write to ${path}?`);
39
- if (!ok) return { block: true, reason: `Protected path: ${hit}` };
40
- } else {
41
- return { block: true, reason: `Protected path: ${hit}` };
42
- }
43
- }
44
- }
45
- });
46
- }
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env python3
2
- import sys
3
- import os
4
-
5
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
6
- from agent_context import AgentContext
7
-
8
- EDIT_KEYWORDS = [
9
- 'fix', 'refactor', 'change', 'update', 'modify', 'edit',
10
- 'rename', 'move', 'delete', 'remove', 'rewrite', 'implement',
11
- 'add', 'replace', 'extract', 'migrate', 'upgrade',
12
- ]
13
-
14
- REMINDER = """*** GITNEXUS: Run impact analysis before editing any symbol ***
15
-
16
- Before modifying a function, class, or method:
17
- npx gitnexus impact <symbolName> --direction upstream --repo xtrm-tools
18
-
19
- Review d=1 items (WILL BREAK) before proceeding.
20
- Skip for docs, configs, and test-only changes.
21
- """
22
-
23
- try:
24
- ctx = AgentContext()
25
-
26
- if ctx.event == 'UserPromptSubmit':
27
- prompt_lower = ctx.prompt.lower()
28
- if any(kw in prompt_lower for kw in EDIT_KEYWORDS):
29
- ctx.allow(additional_context=REMINDER)
30
-
31
- ctx.fail_open()
32
-
33
- except Exception as e:
34
- print(f"Hook error: {e}", file=sys.stderr)
35
- sys.exit(0)
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Test AgentContext hook output formatting"""
3
- import json
4
- import sys
5
- from io import StringIO
6
-
7
- # Mock sys.stdin and sys.exit for testing
8
- class MockExit(Exception):
9
- pass
10
-
11
- def test_hook_output(event_name, method_name, *args, **kwargs):
12
- """Test a specific hook method and return its JSON output"""
13
- import agent_context
14
-
15
- # Prepare mock input
16
- mock_input = {
17
- "hook_event_name": event_name,
18
- "tool_name": "Read",
19
- "tool_input": {"file_path": "/test/file.txt"}
20
- }
21
-
22
- # Mock stdin
23
- original_stdin = sys.stdin
24
- original_exit = sys.exit
25
- sys.stdin = StringIO(json.dumps(mock_input))
26
-
27
- # Capture stdout
28
- output_buffer = StringIO()
29
- original_stdout = sys.stdout
30
- sys.stdout = output_buffer
31
-
32
- try:
33
- ctx = agent_context.AgentContext()
34
-
35
- # Call the method
36
- method = getattr(ctx, method_name)
37
- try:
38
- method(*args, **kwargs)
39
- except SystemExit:
40
- pass # Expected
41
-
42
- # Get output
43
- output = output_buffer.getvalue().strip()
44
- return json.loads(output) if output else {}
45
-
46
- finally:
47
- sys.stdin = original_stdin
48
- sys.stdout = original_stdout
49
- sys.exit = original_exit
50
-
51
- def main():
52
- print("Testing AgentContext hook output formats...\n")
53
-
54
- # Test 1: PreToolUse allow() with systemMessage
55
- print("Test 1: PreToolUse allow() with systemMessage")
56
- output = test_hook_output("PreToolUse", "allow", system_message="Test message")
57
- print(f"Output: {json.dumps(output, indent=2)}")
58
- assert "systemMessage" in output, "Should have systemMessage"
59
- assert output["hookSpecificOutput"]["hookEventName"] == "PreToolUse"
60
- assert output["hookSpecificOutput"]["permissionDecision"] == "allow"
61
- assert "decision" not in output, "Should NOT have top-level 'decision'"
62
- print("✓ PASS\n")
63
-
64
- # Test 2: PreToolUse allow() with additionalContext
65
- print("Test 2: PreToolUse allow() with additionalContext")
66
- output = test_hook_output("PreToolUse", "allow", additional_context="Extra context")
67
- print(f"Output: {json.dumps(output, indent=2)}")
68
- assert output["hookSpecificOutput"]["permissionDecision"] == "allow"
69
- assert output["hookSpecificOutput"]["additionalContext"] == "Extra context"
70
- print("✓ PASS\n")
71
-
72
- # Test 3: UserPromptSubmit allow() with systemMessage (no permissionDecision)
73
- print("Test 3: UserPromptSubmit allow() with systemMessage")
74
- output = test_hook_output("UserPromptSubmit", "allow", system_message="Reminder")
75
- print(f"Output: {json.dumps(output, indent=2)}")
76
- assert "systemMessage" in output
77
- assert "permissionDecision" not in output.get("hookSpecificOutput", {}), \
78
- "UserPromptSubmit should NOT have permissionDecision"
79
- print("✓ PASS\n")
80
-
81
- # Test 4: UserPromptSubmit allow() with additionalContext
82
- print("Test 4: UserPromptSubmit allow() with additionalContext")
83
- output = test_hook_output("UserPromptSubmit", "allow", additional_context="Context")
84
- print(f"Output: {json.dumps(output, indent=2)}")
85
- assert output["hookSpecificOutput"]["additionalContext"] == "Context"
86
- assert "permissionDecision" not in output["hookSpecificOutput"]
87
- print("✓ PASS\n")
88
-
89
- # Test 5: PreToolUse block()
90
- print("Test 5: PreToolUse block()")
91
- output = test_hook_output("PreToolUse", "block", "Dangerous operation")
92
- print(f"Output: {json.dumps(output, indent=2)}")
93
- assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
94
- assert output["hookSpecificOutput"]["permissionDecisionReason"] == "Dangerous operation"
95
- assert "decision" not in output, "Should NOT have top-level 'decision'"
96
- print("✓ PASS\n")
97
-
98
- # Test 6: UserPromptSubmit block() (should use continue: false)
99
- print("Test 6: UserPromptSubmit block()")
100
- output = test_hook_output("UserPromptSubmit", "block", "Blocked")
101
- print(f"Output: {json.dumps(output, indent=2)}")
102
- assert output.get("continue") == False, "Should have continue: false"
103
- assert output.get("stopReason") == "Blocked"
104
- assert "permissionDecision" not in output.get("hookSpecificOutput", {})
105
- print("✓ PASS\n")
106
-
107
- print("=" * 50)
108
- print("All tests passed! ✓")
109
- print("=" * 50)
110
-
111
- if __name__ == "__main__":
112
- main()