axconfig 3.6.2 → 3.6.4

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
@@ -8,7 +8,7 @@ Different AI coding agents each have their own config formats, permission syntax
8
8
 
9
9
  axconfig solves this by providing:
10
10
 
11
- 1. **Unified permission syntax** — One way to express permissions (`--allow read,bash:git *`) that works across all agents
11
+ 1. **Unified permission syntax** — One way to express permissions (`--allow "read,bash:git *"`) that works across all agents
12
12
  2. **Common config access** — Get and set configuration properties using a consistent API, regardless of agent
13
13
  3. **Translation to agent-specific formats** — Converts unified config into whatever format each agent expects
14
14
  4. **Capability validation** — Knows what each agent supports and warns/errors appropriately
@@ -18,6 +18,28 @@ It can be used:
18
18
  - **As a library** — integrate into your own tools and scripts
19
19
  - **As a CLI** — standalone tool for managing agent configs
20
20
 
21
+ ## Prerequisites
22
+
23
+ - Node.js 22.14+
24
+ - pnpm (install/build) or npx (run without install)
25
+ - jq (for `--format json` examples)
26
+ - Bash or Zsh for the examples; adjust for PowerShell/fish
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ # CLI (global)
32
+ npm install -g axconfig
33
+ # or
34
+ pnpm add -g axconfig
35
+
36
+ # Library usage (non-global)
37
+ npm install axconfig
38
+
39
+ # Run without install
40
+ npx -y axconfig --help
41
+ ```
42
+
21
43
  ## Core Functionality
22
44
 
23
45
  ### Permission Parsing
@@ -30,13 +52,20 @@ read, write, bash, glob, grep, webfetch
30
52
 
31
53
  # Bash command patterns
32
54
  bash:git *
33
- bash:npm run build*
55
+ bash:npm run build *
34
56
 
35
57
  # Path restrictions (agent-dependent)
36
58
  read:src/**
37
59
  write:.env
38
60
  ```
39
61
 
62
+ Quote permission strings so your shell does not expand globs or split on spaces:
63
+
64
+ ```bash
65
+ axconfig set --agent claude allow "read,glob,bash:git *"
66
+ axconfig set --agent claude deny "bash:rm *"
67
+ ```
68
+
40
69
  ### Config Translation
41
70
 
42
71
  Translates unified permissions to agent-specific formats:
@@ -117,6 +146,8 @@ reader.writeRaw(configDir, "customSetting", { foo: "bar" });
117
146
 
118
147
  ## CLI Commands
119
148
 
149
+ Output formats: `--format env` (default) or `--format json`. Examples use long flags (`--agent`, `--format`) for clarity.
150
+
120
151
  ### Create Config
121
152
 
122
153
  ```bash
@@ -125,13 +156,18 @@ axconfig create --agent claude --output /tmp/config \
125
156
  --allow "read,glob,bash:git *" \
126
157
  --deny "bash:rm *"
127
158
 
128
- # Export for shell usage
129
- eval $(axconfig create --agent claude --output /tmp/config --allow read)
159
+ # Export for shell usage (trusted input only)
160
+ tmpfile="$(mktemp "${TMPDIR:-/tmp}/axconfig.XXXXXX")"
161
+ axconfig create --agent claude --output /tmp/config --allow read > "$tmpfile"
162
+ . "$tmpfile"
163
+ rm -f "$tmpfile"
130
164
 
131
165
  # Export as JSON for parsing
132
166
  axconfig create --agent claude --output /tmp/cfg --allow read --format json | jq '.env'
133
167
  ```
134
168
 
169
+ Note: `axconfig create` outputs shell code (`export ...`) and values are not shell-escaped. Sourcing will execute `$(...)`/backticks if present, so only source trusted values; for untrusted input, prefer `--format json` and parse with `jq`.
170
+
135
171
  ### Get/Set Unified Settings
136
172
 
137
173
  ```bash
@@ -195,7 +231,7 @@ axconfig get --agent claude allow \
195
231
  | cut -d: -f2 | cut -d' ' -f1 | sort | uniq -c | sort -rn
196
232
 
197
233
  # Compare permissions between agents
198
- diff <(axconfig get -a claude allow) <(axconfig get -a gemini allow)
234
+ diff <(axconfig get --agent claude allow) <(axconfig get --agent gemini allow)
199
235
 
200
236
  # Check if a specific permission exists
201
237
  axconfig get --agent claude allow | grep -q 'bash:git' && echo "git allowed"
@@ -76,7 +76,7 @@ function readExistingSettings(settingsPath) {
76
76
  }
77
77
  catch (error) {
78
78
  const message = error instanceof Error ? error.message : String(error);
79
- throw new Error(`Failed to parse existing settings at ${settingsPath}: ${message}`);
79
+ throw new Error(`Failed to parse existing settings at ${settingsPath}: ${message}`, { cause: error });
80
80
  }
81
81
  }
82
82
  /**
@@ -130,7 +130,7 @@ function build(config, output) {
130
130
  }
131
131
  catch (error) {
132
132
  const message = error instanceof Error ? error.message : String(error);
133
- throw new Error(`Failed to parse existing config at ${configPath}: ${message}`);
133
+ throw new Error(`Failed to parse existing config at ${configPath}: ${message}`, { cause: error });
134
134
  }
135
135
  }
136
136
  // Infer sandbox mode
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Gemini TOML rule generation helpers.
3
+ */
4
+ /**
5
+ * Generate TOML rule for tool permissions.
6
+ *
7
+ * For allow rules that include run_shell_command, adds `allow_redirection = true`
8
+ * to permit heredocs, pipes, and redirects.
9
+ */
10
+ export declare function generateToolRule(toolNames: string[], decision: "allow" | "deny", priority: number): string;
11
+ /**
12
+ * Generate TOML rule for bash command patterns.
13
+ *
14
+ * For allow rules, includes `allow_redirection = true` to permit
15
+ * heredocs, pipes, and redirects. Gemini CLI normally downgrades
16
+ * ALLOW to ASK_USER (DENY in --yolo mode) when redirections detected.
17
+ */
18
+ export declare function generateBashRule(patterns: string[], decision: "allow" | "deny", priority: number): string;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Gemini TOML rule generation helpers.
3
+ */
4
+ /**
5
+ * Generate TOML rule for tool permissions.
6
+ *
7
+ * For allow rules that include run_shell_command, adds `allow_redirection = true`
8
+ * to permit heredocs, pipes, and redirects.
9
+ */
10
+ export function generateToolRule(toolNames, decision, priority) {
11
+ if (toolNames.length === 0)
12
+ return "";
13
+ const toolNameValue = toolNames.length === 1 ? `"${toolNames[0]}"` : JSON.stringify(toolNames);
14
+ // Allow redirections for shell commands (heredocs, pipes, >)
15
+ const includesShell = toolNames.includes("run_shell_command");
16
+ const allowRedirection = decision === "allow" && includesShell ? "\nallow_redirection = true" : "";
17
+ return `[[rule]]
18
+ toolName = ${toolNameValue}
19
+ decision = "${decision}"
20
+ priority = ${priority}${allowRedirection}`;
21
+ }
22
+ /**
23
+ * Generate TOML rule for bash command patterns.
24
+ *
25
+ * For allow rules, includes `allow_redirection = true` to permit
26
+ * heredocs, pipes, and redirects. Gemini CLI normally downgrades
27
+ * ALLOW to ASK_USER (DENY in --yolo mode) when redirections detected.
28
+ */
29
+ export function generateBashRule(patterns, decision, priority) {
30
+ if (patterns.length === 0)
31
+ return "";
32
+ const prefixValue = patterns.length === 1 ? `"${patterns[0]}"` : JSON.stringify(patterns);
33
+ // Allow redirections (heredocs, pipes, >) for allowed commands
34
+ const allowRedirection = decision === "allow" ? "\nallow_redirection = true" : "";
35
+ return `[[rule]]
36
+ toolName = "run_shell_command"
37
+ commandPrefix = ${prefixValue}
38
+ decision = "${decision}"
39
+ priority = ${priority}${allowRedirection}`;
40
+ }
@@ -6,3 +6,10 @@
6
6
  * Throws if file exists but contains invalid JSON to prevent data loss.
7
7
  */
8
8
  export declare function readExistingSettings(settingsPath: string): Record<string, unknown>;
9
+ /**
10
+ * Build settings with /tmp workspace added.
11
+ *
12
+ * Adds /tmp to workspaces array to allow temporary file writes.
13
+ * Safe for CI/CD environments (ephemeral containers).
14
+ */
15
+ export declare function buildSettingsWithTemporaryWorkspace(existingSettings: Record<string, unknown>): Record<string, unknown>;
@@ -16,6 +16,25 @@ export function readExistingSettings(settingsPath) {
16
16
  }
17
17
  catch (error) {
18
18
  const message = error instanceof Error ? error.message : String(error);
19
- throw new Error(`Failed to parse existing settings at ${settingsPath}: ${message}`);
19
+ throw new Error(`Failed to parse existing settings at ${settingsPath}: ${message}`, { cause: error });
20
20
  }
21
21
  }
22
+ /**
23
+ * Build settings with /tmp workspace added.
24
+ *
25
+ * Adds /tmp to workspaces array to allow temporary file writes.
26
+ * Safe for CI/CD environments (ephemeral containers).
27
+ */
28
+ export function buildSettingsWithTemporaryWorkspace(existingSettings) {
29
+ const existingWorkspaces = Array.isArray(existingSettings.workspaces)
30
+ ? existingSettings.workspaces
31
+ : [];
32
+ // Avoid duplicate /tmp entries
33
+ if (existingWorkspaces.includes("/tmp")) {
34
+ return existingSettings;
35
+ }
36
+ return {
37
+ ...existingSettings,
38
+ workspaces: [...existingWorkspaces, "/tmp"],
39
+ };
40
+ }
@@ -14,7 +14,8 @@ import { atomicWriteFileSync } from "../atomic-write.js";
14
14
  import { registerConfigBuilder } from "../builder.js";
15
15
  // Re-export reader
16
16
  export { geminiConfigReader } from "./gemini-reader.js";
17
- import { readExistingSettings } from "./gemini-settings.js";
17
+ import { generateBashRule, generateToolRule } from "./gemini-rules.js";
18
+ import { buildSettingsWithTemporaryWorkspace, readExistingSettings, } from "./gemini-settings.js";
18
19
  /** Gemini CLI tool name mapping */
19
20
  const TOOL_MAP = {
20
21
  read: "read_file",
@@ -31,31 +32,6 @@ const CAPABILITIES = {
31
32
  pathRestrictions: false, // Gemini doesn't support path patterns
32
33
  canDenyRead: true,
33
34
  };
34
- /**
35
- * Generate TOML rule for tool permissions.
36
- */
37
- function generateToolRule(toolNames, decision, priority) {
38
- if (toolNames.length === 0)
39
- return "";
40
- const toolNameValue = toolNames.length === 1 ? `"${toolNames[0]}"` : JSON.stringify(toolNames);
41
- return `[[rule]]
42
- toolName = ${toolNameValue}
43
- decision = "${decision}"
44
- priority = ${priority}`;
45
- }
46
- /**
47
- * Generate TOML rule for bash command patterns.
48
- */
49
- function generateBashRule(patterns, decision, priority) {
50
- if (patterns.length === 0)
51
- return "";
52
- const prefixValue = patterns.length === 1 ? `"${patterns[0]}"` : JSON.stringify(patterns);
53
- return `[[rule]]
54
- toolName = "run_shell_command"
55
- commandPrefix = ${prefixValue}
56
- decision = "${decision}"
57
- priority = ${priority}`;
58
- }
59
35
  /**
60
36
  * Build Gemini CLI configuration.
61
37
  *
@@ -162,7 +138,8 @@ function build(config, output) {
162
138
  // Write settings.json, preserving existing settings (e.g., model)
163
139
  const settingsPath = path.join(output, "settings.json");
164
140
  const existingSettings = readExistingSettings(settingsPath);
165
- atomicWriteFileSync(settingsPath, JSON.stringify(existingSettings, undefined, 2));
141
+ const settings = buildSettingsWithTemporaryWorkspace(existingSettings);
142
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, undefined, 2));
166
143
  return {
167
144
  ok: true,
168
145
  env: { GEMINI_DIR: output },
@@ -75,7 +75,7 @@ function build(config, output) {
75
75
  }
76
76
  catch (error) {
77
77
  const message = error instanceof Error ? error.message : String(error);
78
- throw new Error(`Failed to parse existing config at ${configPath}: ${message}`);
78
+ throw new Error(`Failed to parse existing config at ${configPath}: ${message}`, { cause: error });
79
79
  }
80
80
  }
81
81
  // Write config file, preserving existing settings
package/dist/cli.js CHANGED
@@ -32,8 +32,14 @@ const program = new Command()
32
32
  .helpCommand(false)
33
33
  .addHelpText("after", String.raw `
34
34
  Examples:
35
- # Create config and source it in current shell
36
- eval $(axconfig create --agent claude --output /tmp/cfg --allow read)
35
+ # Create config and source it in current shell (trusted input only; output is not shell-escaped)
36
+ tmpfile="$(mktemp "${"$"}{TMPDIR:-/tmp}/axconfig.XXXXXX")"
37
+ axconfig create --agent claude --output /tmp/cfg --allow read > "$tmpfile"
38
+ . "$tmpfile"
39
+ rm -f "$tmpfile"
40
+
41
+ # For untrusted input, use JSON output instead
42
+ axconfig create --agent claude --output /tmp/cfg --allow read --format json | jq '.env'
37
43
 
38
44
  # Get current settings
39
45
  axconfig get --agent claude allow
@@ -49,9 +49,9 @@ function setByPath(object, keyPath, value) {
49
49
  for (let index = 0; index < keys.length - 1; index++) {
50
50
  const key = keys[index];
51
51
  if (key === undefined)
52
- continue;
52
+ return;
53
53
  if (UNSAFE_KEYS.has(key))
54
- continue;
54
+ return;
55
55
  if (!(key in current) ||
56
56
  typeof current[key] !== "object" ||
57
57
  current[key] === null) {
@@ -22,7 +22,9 @@ function readJsonConfig(configPath) {
22
22
  }
23
23
  catch (error) {
24
24
  if (error instanceof SyntaxError) {
25
- throw new Error(`Invalid JSON in ${configPath}: ${error.message}`);
25
+ throw new Error(`Invalid JSON in ${configPath}: ${error.message}`, {
26
+ cause: error,
27
+ });
26
28
  }
27
29
  throw error;
28
30
  }
@@ -23,7 +23,9 @@ function readTomlConfig(configPath) {
23
23
  }
24
24
  catch (error) {
25
25
  if (error instanceof Error) {
26
- throw new Error(`Invalid TOML in ${configPath}: ${error.message}`);
26
+ throw new Error(`Invalid TOML in ${configPath}: ${error.message}`, {
27
+ cause: error,
28
+ });
27
29
  }
28
30
  throw error;
29
31
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axconfig",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "3.6.2",
5
+ "version": "3.6.4",
6
6
  "description": "Unified configuration management for AI coding agents - common API for permissions, settings, and config across Claude Code, Codex, Gemini CLI, GitHub Copilot CLI, and OpenCode",
7
7
  "repository": {
8
8
  "type": "git",
@@ -62,29 +62,29 @@
62
62
  "automation",
63
63
  "coding-assistant"
64
64
  ],
65
- "packageManager": "pnpm@10.28.0",
65
+ "packageManager": "pnpm@10.30.1",
66
66
  "engines": {
67
67
  "node": ">=22.14.0"
68
68
  },
69
69
  "dependencies": {
70
70
  "@commander-js/extra-typings": "^14.0.0",
71
71
  "@iarna/toml": "^2.2.5",
72
- "axshared": "^4.0.0",
73
- "commander": "^14.0.2"
72
+ "axshared": "^5.0.0",
73
+ "commander": "^14.0.3"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@total-typescript/ts-reset": "^0.6.1",
77
77
  "@types/iarna__toml": "^2.0.5",
78
- "@types/node": "^25.0.9",
79
- "@vitest/coverage-v8": "^4.0.17",
80
- "eslint": "^9.39.2",
81
- "eslint-config-axkit": "^1.1.0",
78
+ "@types/node": "^25.3.0",
79
+ "@vitest/coverage-v8": "^4.0.18",
80
+ "eslint": "^10.0.1",
81
+ "eslint-config-axkit": "^1.2.1",
82
82
  "fta-check": "^1.5.1",
83
83
  "fta-cli": "^3.0.0",
84
- "knip": "^5.82.0",
85
- "prettier": "3.8.0",
86
- "semantic-release": "^25.0.2",
84
+ "knip": "^5.85.0",
85
+ "prettier": "3.8.1",
86
+ "semantic-release": "^25.0.3",
87
87
  "typescript": "^5.9.3",
88
- "vitest": "^4.0.17"
88
+ "vitest": "^4.0.18"
89
89
  }
90
90
  }