@tagma/sdk 0.1.8 → 0.1.9

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.
@@ -1,207 +1,207 @@
1
- import { existsSync } from 'node:fs';
2
- import { isAbsolute, relative, dirname, join } from 'node:path';
3
- import type {
4
- DriverPlugin, DriverCapabilities, DriverResultMeta,
5
- TaskConfig, TrackConfig, DriverContext, SpawnSpec, Permissions,
6
- } from '../types';
7
-
8
- // Claude Code CLI reference: https://code.claude.com/docs/en/cli-reference
9
-
10
- const MODEL_MAP: Record<string, string> = {
11
- high: 'opus', medium: 'sonnet', low: 'haiku',
12
- };
13
-
14
- function resolveModel(tier: string): string {
15
- return MODEL_MAP[tier] ?? 'sonnet';
16
- }
17
-
18
- function resolveTools(permissions: Permissions): string {
19
- const tools = ['Grep', 'Glob'];
20
- if (permissions.read) tools.push('Read');
21
- if (permissions.write) tools.push('Edit', 'Write');
22
- if (permissions.execute) tools.push('Bash');
23
- return tools.join(',');
24
- }
25
-
26
- // Maps our Permissions to Claude Code's --permission-mode. In print (-p) mode
27
- // Claude needs non-interactive permission handling:
28
- // - `bypassPermissions` skips all checks (required for reliable Bash automation
29
- // under `execute: true`, matches the "full trust" semantics of that tier).
30
- // - `dontAsk` auto-denies anything outside `--allowedTools`, which is exactly
31
- // what we want for read/write tiers: the allowedTools whitelist already
32
- // enumerates what Claude may do, and dontAsk makes violations fail fast
33
- // instead of hanging on a prompt no one can answer in headless mode.
34
- // See: https://code.claude.com/docs/en/permission-modes
35
- function resolvePermissionMode(permissions: Permissions): string {
36
- if (permissions.execute) return 'bypassPermissions';
37
- return 'dontAsk';
38
- }
39
-
40
- // Returns true if `sub` is inside `root` (or equal to it).
41
- function isInside(root: string, sub: string): boolean {
42
- const rel = relative(root, sub);
43
- return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
44
- }
45
-
46
- // Claude Code requires CLAUDE_CODE_GIT_BASH_PATH on Windows pointing to
47
- // Git Bash (bin\bash.exe under a Git for Windows install). See:
48
- // https://code.claude.com/docs/en/troubleshooting#windows-claude-code-on-windows-requires-git-bash
49
- // The path must use native Windows backslashes — forward slashes are rejected
50
- // by Claude Code's path validation.
51
- function resolveGitBashEnv(): Record<string, string> {
52
- if (process.platform !== 'win32') return {};
53
-
54
- // Respect user-provided value if it points to an actual file. If the user
55
- // set it to a non-existent path, fall through to discovery rather than
56
- // propagating the broken config.
57
- const existing = process.env.CLAUDE_CODE_GIT_BASH_PATH;
58
- if (existing && existsSync(existing)) return {};
59
-
60
- const discovered = discoverGitBash();
61
- return discovered ? { CLAUDE_CODE_GIT_BASH_PATH: discovered } : {};
62
- }
63
-
64
- function discoverGitBash(): string | null {
65
- // Strategy 1: find git.exe in PATH (equivalent to `where.exe git`) and
66
- // walk up looking for bin\bash.exe under a Git install root. Git for
67
- // Windows may expose multiple git.exe locations (cmd\git.exe,
68
- // mingw64\bin\git.exe, mingw64\libexec\git-core\git.exe), so we walk up
69
- // several levels rather than assuming a fixed depth.
70
- const gitExe = findExeInPath('git.exe');
71
- if (gitExe) {
72
- let dir = dirname(gitExe);
73
- for (let depth = 0; depth < 5; depth++) {
74
- const candidate = join(dir, 'bin', 'bash.exe');
75
- if (existsSync(candidate)) return candidate;
76
- const parent = dirname(dir);
77
- if (parent === dir) break;
78
- dir = parent;
79
- }
80
- }
81
-
82
- // Strategy 2: check common Git for Windows install locations.
83
- // Uses %ProgramFiles%/%LOCALAPPDATA%/%USERPROFILE% env vars so it works on
84
- // systems where those aren't mapped to C:\ (e.g. localized Windows).
85
- const programFiles = process.env['ProgramFiles'] ?? 'C:\\Program Files';
86
- const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
87
- const localAppData = process.env['LOCALAPPDATA'];
88
- const userProfile = process.env['USERPROFILE'];
89
-
90
- const candidates = [
91
- join(programFiles, 'Git', 'bin', 'bash.exe'),
92
- join(programFilesX86, 'Git', 'bin', 'bash.exe'),
93
- // Git for Windows user-level install
94
- localAppData && join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
95
- // Scoop
96
- userProfile && join(userProfile, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
97
- // Chocolatey default
98
- 'C:\\tools\\git\\bin\\bash.exe',
99
- ].filter((p): p is string => Boolean(p));
100
-
101
- for (const c of candidates) {
102
- if (existsSync(c)) return c;
103
- }
104
-
105
- // Strategy 3: scan PATH for any entry containing "git" (e.g. Git's
106
- // mingw64/bin or usr/bin already in PATH), walk up to find bash.exe.
107
- // Catches custom install locations.
108
- const pathEntries = (process.env.PATH ?? '').split(';');
109
- for (const entry of pathEntries) {
110
- if (!/git/i.test(entry)) continue;
111
- const normalized = entry.replace(/\//g, '\\').replace(/\\+$/, '');
112
- const parts = normalized.split('\\');
113
- for (let depth = 1; depth <= 4; depth++) {
114
- const root = parts.slice(0, parts.length - depth).join('\\');
115
- if (!root) continue;
116
- const candidate = root + '\\bin\\bash.exe';
117
- if (existsSync(candidate)) return candidate;
118
- }
119
- }
120
-
121
- return null;
122
- }
123
-
124
- function findExeInPath(exe: string): string | null {
125
- const pathDirs = (process.env.PATH ?? '').split(';');
126
- for (const dir of pathDirs) {
127
- if (!dir) continue;
128
- const full = join(dir, exe);
129
- if (existsSync(full)) return full;
130
- }
131
- return null;
132
- }
133
-
134
- export const ClaudeCodeDriver: DriverPlugin = {
135
- name: 'claude-code',
136
-
137
- capabilities: {
138
- sessionResume: true,
139
- systemPrompt: true,
140
- outputFormat: true,
141
- } satisfies DriverCapabilities,
142
-
143
- resolveModel,
144
- resolveTools,
145
-
146
- async buildCommand(
147
- task: TaskConfig, track: TrackConfig, ctx: DriverContext,
148
- ): Promise<SpawnSpec> {
149
- const permissions = task.permissions ?? track.permissions!;
150
- const model = resolveModel(task.model_tier ?? track.model_tier ?? 'medium');
151
- const tools = resolveTools(permissions);
152
- const permissionMode = resolvePermissionMode(permissions);
153
-
154
- const args: string[] = [
155
- 'claude',
156
- '-p', task.prompt!,
157
- '--model', model,
158
- '--allowedTools', tools,
159
- '--permission-mode', permissionMode,
160
- '--output-format', 'json',
161
- '--verbose',
162
- // Pin to project+local settings only; don't inherit arbitrary user-level
163
- // config (hooks, MCP servers, etc.) into pipeline automation.
164
- '--setting-sources', 'project,local',
165
- ];
166
-
167
- const profile = task.agent_profile ?? track.agent_profile;
168
- if (profile) {
169
- args.push('--append-system-prompt', profile);
170
- }
171
-
172
- // If the task runs in a subdirectory of the project, grant read/edit
173
- // access to the project root via --add-dir so Claude can still see
174
- // shared files (configs, types, etc.) outside task.cwd.
175
- const effectiveCwd = task.cwd ?? ctx.workDir;
176
- if (effectiveCwd !== ctx.workDir && isInside(ctx.workDir, effectiveCwd)) {
177
- args.push('--add-dir', ctx.workDir);
178
- }
179
-
180
- // Native session resume
181
- if (task.continue_from) {
182
- const sessionId = ctx.sessionMap.get(task.continue_from);
183
- if (sessionId) {
184
- args.push('--resume', sessionId);
185
- }
186
- }
187
-
188
- return { args, cwd: effectiveCwd, env: resolveGitBashEnv() };
189
- },
190
-
191
- parseResult(stdout: string): DriverResultMeta {
192
- try {
193
- const json = JSON.parse(stdout);
194
- // Extract canonical text: strip JSON envelope so downstream drivers
195
- // get the actual AI response, not metadata
196
- const normalizedOutput = json.result ?? json.text ?? json.content ?? stdout;
197
- return {
198
- sessionId: json.session_id,
199
- normalizedOutput: typeof normalizedOutput === 'string'
200
- ? normalizedOutput
201
- : JSON.stringify(normalizedOutput),
202
- };
203
- } catch {
204
- return { normalizedOutput: stdout };
205
- }
206
- },
207
- };
1
+ import { existsSync } from 'node:fs';
2
+ import { isAbsolute, relative, dirname, join } from 'node:path';
3
+ import type {
4
+ DriverPlugin, DriverCapabilities, DriverResultMeta,
5
+ TaskConfig, TrackConfig, DriverContext, SpawnSpec, Permissions,
6
+ } from '../types';
7
+
8
+ // Claude Code CLI reference: https://code.claude.com/docs/en/cli-reference
9
+
10
+ const MODEL_MAP: Record<string, string> = {
11
+ high: 'opus', medium: 'sonnet', low: 'haiku',
12
+ };
13
+
14
+ function resolveModel(tier: string): string {
15
+ return MODEL_MAP[tier] ?? 'sonnet';
16
+ }
17
+
18
+ function resolveTools(permissions: Permissions): string {
19
+ const tools = ['Grep', 'Glob'];
20
+ if (permissions.read) tools.push('Read');
21
+ if (permissions.write) tools.push('Edit', 'Write');
22
+ if (permissions.execute) tools.push('Bash');
23
+ return tools.join(',');
24
+ }
25
+
26
+ // Maps our Permissions to Claude Code's --permission-mode. In print (-p) mode
27
+ // Claude needs non-interactive permission handling:
28
+ // - `bypassPermissions` skips all checks (required for reliable Bash automation
29
+ // under `execute: true`, matches the "full trust" semantics of that tier).
30
+ // - `dontAsk` auto-denies anything outside `--allowedTools`, which is exactly
31
+ // what we want for read/write tiers: the allowedTools whitelist already
32
+ // enumerates what Claude may do, and dontAsk makes violations fail fast
33
+ // instead of hanging on a prompt no one can answer in headless mode.
34
+ // See: https://code.claude.com/docs/en/permission-modes
35
+ function resolvePermissionMode(permissions: Permissions): string {
36
+ if (permissions.execute) return 'bypassPermissions';
37
+ return 'dontAsk';
38
+ }
39
+
40
+ // Returns true if `sub` is inside `root` (or equal to it).
41
+ function isInside(root: string, sub: string): boolean {
42
+ const rel = relative(root, sub);
43
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
44
+ }
45
+
46
+ // Claude Code requires CLAUDE_CODE_GIT_BASH_PATH on Windows pointing to
47
+ // Git Bash (bin\bash.exe under a Git for Windows install). See:
48
+ // https://code.claude.com/docs/en/troubleshooting#windows-claude-code-on-windows-requires-git-bash
49
+ // The path must use native Windows backslashes — forward slashes are rejected
50
+ // by Claude Code's path validation.
51
+ function resolveGitBashEnv(): Record<string, string> {
52
+ if (process.platform !== 'win32') return {};
53
+
54
+ // Respect user-provided value if it points to an actual file. If the user
55
+ // set it to a non-existent path, fall through to discovery rather than
56
+ // propagating the broken config.
57
+ const existing = process.env.CLAUDE_CODE_GIT_BASH_PATH;
58
+ if (existing && existsSync(existing)) return {};
59
+
60
+ const discovered = discoverGitBash();
61
+ return discovered ? { CLAUDE_CODE_GIT_BASH_PATH: discovered } : {};
62
+ }
63
+
64
+ function discoverGitBash(): string | null {
65
+ // Strategy 1: find git.exe in PATH (equivalent to `where.exe git`) and
66
+ // walk up looking for bin\bash.exe under a Git install root. Git for
67
+ // Windows may expose multiple git.exe locations (cmd\git.exe,
68
+ // mingw64\bin\git.exe, mingw64\libexec\git-core\git.exe), so we walk up
69
+ // several levels rather than assuming a fixed depth.
70
+ const gitExe = findExeInPath('git.exe');
71
+ if (gitExe) {
72
+ let dir = dirname(gitExe);
73
+ for (let depth = 0; depth < 5; depth++) {
74
+ const candidate = join(dir, 'bin', 'bash.exe');
75
+ if (existsSync(candidate)) return candidate;
76
+ const parent = dirname(dir);
77
+ if (parent === dir) break;
78
+ dir = parent;
79
+ }
80
+ }
81
+
82
+ // Strategy 2: check common Git for Windows install locations.
83
+ // Uses %ProgramFiles%/%LOCALAPPDATA%/%USERPROFILE% env vars so it works on
84
+ // systems where those aren't mapped to C:\ (e.g. localized Windows).
85
+ const programFiles = process.env['ProgramFiles'] ?? 'C:\\Program Files';
86
+ const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
87
+ const localAppData = process.env['LOCALAPPDATA'];
88
+ const userProfile = process.env['USERPROFILE'];
89
+
90
+ const candidates = [
91
+ join(programFiles, 'Git', 'bin', 'bash.exe'),
92
+ join(programFilesX86, 'Git', 'bin', 'bash.exe'),
93
+ // Git for Windows user-level install
94
+ localAppData && join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
95
+ // Scoop
96
+ userProfile && join(userProfile, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
97
+ // Chocolatey default
98
+ 'C:\\tools\\git\\bin\\bash.exe',
99
+ ].filter((p): p is string => Boolean(p));
100
+
101
+ for (const c of candidates) {
102
+ if (existsSync(c)) return c;
103
+ }
104
+
105
+ // Strategy 3: scan PATH for any entry containing "git" (e.g. Git's
106
+ // mingw64/bin or usr/bin already in PATH), walk up to find bash.exe.
107
+ // Catches custom install locations.
108
+ const pathEntries = (process.env.PATH ?? '').split(';');
109
+ for (const entry of pathEntries) {
110
+ if (!/git/i.test(entry)) continue;
111
+ const normalized = entry.replace(/\//g, '\\').replace(/\\+$/, '');
112
+ const parts = normalized.split('\\');
113
+ for (let depth = 1; depth <= 4; depth++) {
114
+ const root = parts.slice(0, parts.length - depth).join('\\');
115
+ if (!root) continue;
116
+ const candidate = root + '\\bin\\bash.exe';
117
+ if (existsSync(candidate)) return candidate;
118
+ }
119
+ }
120
+
121
+ return null;
122
+ }
123
+
124
+ function findExeInPath(exe: string): string | null {
125
+ const pathDirs = (process.env.PATH ?? '').split(';');
126
+ for (const dir of pathDirs) {
127
+ if (!dir) continue;
128
+ const full = join(dir, exe);
129
+ if (existsSync(full)) return full;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ export const ClaudeCodeDriver: DriverPlugin = {
135
+ name: 'claude-code',
136
+
137
+ capabilities: {
138
+ sessionResume: true,
139
+ systemPrompt: true,
140
+ outputFormat: true,
141
+ } satisfies DriverCapabilities,
142
+
143
+ resolveModel,
144
+ resolveTools,
145
+
146
+ async buildCommand(
147
+ task: TaskConfig, track: TrackConfig, ctx: DriverContext,
148
+ ): Promise<SpawnSpec> {
149
+ const permissions = task.permissions ?? track.permissions!;
150
+ const model = resolveModel(task.model_tier ?? track.model_tier ?? 'medium');
151
+ const tools = resolveTools(permissions);
152
+ const permissionMode = resolvePermissionMode(permissions);
153
+
154
+ const args: string[] = [
155
+ 'claude',
156
+ '-p', task.prompt!,
157
+ '--model', model,
158
+ '--allowedTools', tools,
159
+ '--permission-mode', permissionMode,
160
+ '--output-format', 'json',
161
+ '--verbose',
162
+ // Pin to project+local settings only; don't inherit arbitrary user-level
163
+ // config (hooks, MCP servers, etc.) into pipeline automation.
164
+ '--setting-sources', 'project,local',
165
+ ];
166
+
167
+ const profile = task.agent_profile ?? track.agent_profile;
168
+ if (profile) {
169
+ args.push('--append-system-prompt', profile);
170
+ }
171
+
172
+ // If the task runs in a subdirectory of the project, grant read/edit
173
+ // access to the project root via --add-dir so Claude can still see
174
+ // shared files (configs, types, etc.) outside task.cwd.
175
+ const effectiveCwd = task.cwd ?? ctx.workDir;
176
+ if (effectiveCwd !== ctx.workDir && isInside(ctx.workDir, effectiveCwd)) {
177
+ args.push('--add-dir', ctx.workDir);
178
+ }
179
+
180
+ // Native session resume
181
+ if (task.continue_from) {
182
+ const sessionId = ctx.sessionMap.get(task.continue_from);
183
+ if (sessionId) {
184
+ args.push('--resume', sessionId);
185
+ }
186
+ }
187
+
188
+ return { args, cwd: effectiveCwd, env: resolveGitBashEnv() };
189
+ },
190
+
191
+ parseResult(stdout: string): DriverResultMeta {
192
+ try {
193
+ const json = JSON.parse(stdout);
194
+ // Extract canonical text: strip JSON envelope so downstream drivers
195
+ // get the actual AI response, not metadata
196
+ const normalizedOutput = json.result ?? json.text ?? json.content ?? stdout;
197
+ return {
198
+ sessionId: json.session_id,
199
+ normalizedOutput: typeof normalizedOutput === 'string'
200
+ ? normalizedOutput
201
+ : JSON.stringify(normalizedOutput),
202
+ };
203
+ } catch {
204
+ return { normalizedOutput: stdout };
205
+ }
206
+ },
207
+ };