task-delegate 0.1.0

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.
@@ -0,0 +1,84 @@
1
+ export const backend = {
2
+ name: 'opencode',
3
+ status: 'stable',
4
+ binary: 'opencode',
5
+ defaultMode: 'safe-auto',
6
+ supportedModes: ['plan', 'manual', 'safe-auto']
7
+ };
8
+
9
+ export function opencodePermissionProfile() {
10
+ return {
11
+ read: {
12
+ '*': 'allow',
13
+ '*.env': 'deny',
14
+ '*.env.*': 'deny',
15
+ '*.pem': 'deny',
16
+ '*.key': 'deny',
17
+ 'id_rsa*': 'deny'
18
+ },
19
+ edit: {
20
+ '*': 'allow',
21
+ '*.env': 'deny',
22
+ '*.env.*': 'deny',
23
+ '*.pem': 'deny',
24
+ '*.key': 'deny'
25
+ },
26
+ glob: 'allow',
27
+ grep: 'allow',
28
+ bash: {
29
+ '*': 'ask',
30
+ 'git status*': 'allow',
31
+ 'git diff*': 'allow',
32
+ 'git log*': 'allow',
33
+ 'git show*': 'allow',
34
+ 'npm test*': 'allow',
35
+ 'npm run test*': 'allow',
36
+ 'npm run lint*': 'allow',
37
+ 'npm run build*': 'allow',
38
+ 'pnpm test*': 'allow',
39
+ 'pnpm run test*': 'allow',
40
+ 'pnpm run lint*': 'allow',
41
+ 'pnpm run build*': 'allow',
42
+ 'yarn test*': 'allow',
43
+ 'yarn lint*': 'allow',
44
+ 'yarn build*': 'allow',
45
+ 'git add*': 'deny',
46
+ 'git commit*': 'deny',
47
+ 'git push*': 'deny',
48
+ 'git reset*': 'deny',
49
+ 'git clean*': 'deny',
50
+ 'rm *': 'deny',
51
+ 'rm -rf*': 'deny',
52
+ 'curl *': 'ask',
53
+ 'wget *': 'ask'
54
+ },
55
+ webfetch: 'deny',
56
+ websearch: 'deny',
57
+ external_directory: 'deny',
58
+ doom_loop: 'ask'
59
+ };
60
+ }
61
+
62
+ export function buildInvocation({ prompt, mode, model }) {
63
+ const args = ['run', '--format', 'json', '--title', 'TaskDelegate delegated run'];
64
+
65
+ if (mode === 'safe-auto') args.push('--auto');
66
+ if (mode === 'plan') args.push('--agent', 'plan');
67
+ if (mode !== 'plan') args.push('--agent', 'build');
68
+ if (model) args.push('--model', model);
69
+
70
+ args.push(prompt);
71
+
72
+ return {
73
+ command: backend.binary,
74
+ args,
75
+ env: {
76
+ OPENCODE_PERMISSION: JSON.stringify(opencodePermissionProfile()),
77
+ OPENCODE_DISABLE_AUTOUPDATE: 'true',
78
+ OPENCODE_DISABLE_PRUNE: 'true'
79
+ },
80
+ warnings: mode === 'safe-auto'
81
+ ? ['OpenCode safe-auto uses --auto with OPENCODE_PERMISSION deny rules. Review is still required.']
82
+ : []
83
+ };
84
+ }
@@ -0,0 +1,30 @@
1
+ import { runProcess } from './utils.mjs';
2
+
3
+ export async function git(cwd, args) {
4
+ return runProcess('git', args, { cwd, timeoutMs: 60_000 });
5
+ }
6
+
7
+ export async function isGitRepo(cwd) {
8
+ const result = await git(cwd, ['rev-parse', '--is-inside-work-tree']);
9
+ return result.exitCode === 0 && result.stdout.trim() === 'true';
10
+ }
11
+
12
+ export async function statusPorcelain(cwd) {
13
+ const result = await git(cwd, ['status', '--porcelain']);
14
+ return result.exitCode === 0 ? result.stdout : '';
15
+ }
16
+
17
+ export async function dirty(cwd) {
18
+ return (await statusPorcelain(cwd)).trim().length > 0;
19
+ }
20
+
21
+ export async function diffStat(cwd) {
22
+ const result = await git(cwd, ['diff', '--stat']);
23
+ return result.exitCode === 0 ? result.stdout : '';
24
+ }
25
+
26
+ export async function changedFiles(cwd) {
27
+ const result = await git(cwd, ['diff', '--name-only']);
28
+ if (result.exitCode !== 0) return [];
29
+ return result.stdout.split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
30
+ }
@@ -0,0 +1,179 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ export function nowIso() {
7
+ return new Date().toISOString();
8
+ }
9
+
10
+ export function safeTimestamp() {
11
+ return nowIso().replace(/[:.]/g, '-');
12
+ }
13
+
14
+ export async function exists(filePath) {
15
+ try {
16
+ await access(filePath, constants.F_OK);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ export async function ensureDir(dirPath) {
24
+ await mkdir(dirPath, { recursive: true });
25
+ }
26
+
27
+ export async function readText(filePath) {
28
+ return readFile(filePath, 'utf8');
29
+ }
30
+
31
+ export async function writeText(filePath, content) {
32
+ await ensureDir(path.dirname(filePath));
33
+ await writeFile(filePath, content, 'utf8');
34
+ }
35
+
36
+ export function countLines(text) {
37
+ if (!text) return 0;
38
+ return text.split(/\r?\n/).length;
39
+ }
40
+
41
+ export function parseArgs(argv) {
42
+ const args = {
43
+ backend: 'opencode',
44
+ mode: undefined,
45
+ brief: undefined,
46
+ cd: process.cwd(),
47
+ out: undefined,
48
+ model: undefined,
49
+ timeoutMs: 30 * 60 * 1000,
50
+ maxBriefLines: 120,
51
+ allowDirty: false,
52
+ dryRun: false,
53
+ force: false
54
+ };
55
+
56
+ for (let i = 0; i < argv.length; i += 1) {
57
+ const token = argv[i];
58
+ const next = () => {
59
+ if (i + 1 >= argv.length) throw new Error(`Missing value for ${token}`);
60
+ i += 1;
61
+ return argv[i];
62
+ };
63
+
64
+ switch (token) {
65
+ case '--backend': args.backend = next(); break;
66
+ case '--mode': args.mode = next(); break;
67
+ case '--brief': args.brief = next(); break;
68
+ case '--cd': args.cd = next(); break;
69
+ case '--out': args.out = next(); break;
70
+ case '--model': args.model = next(); break;
71
+ case '--timeout-ms': args.timeoutMs = Number(next()); break;
72
+ case '--max-brief-lines': args.maxBriefLines = Number(next()); break;
73
+ case '--allow-dirty': args.allowDirty = true; break;
74
+ case '--dry-run': args.dryRun = true; break;
75
+ case '--force': args.force = true; break;
76
+ case '--help':
77
+ case '-h':
78
+ args.help = true;
79
+ break;
80
+ default:
81
+ throw new Error(`Unknown argument: ${token}`);
82
+ }
83
+ }
84
+
85
+ return args;
86
+ }
87
+
88
+ export function printHelp() {
89
+ console.log(`TaskDelegate CLI
90
+
91
+ Usage:
92
+ task-delegate run --brief brief.md [options]
93
+ task-delegate doctor [--json]
94
+ task-delegate list-backends [--json]
95
+ task-delegate init-brief [--out brief.md]
96
+ task-delegate install-skill [--dest .claude/skills]
97
+ task-delegate skill-path
98
+
99
+ Run options:
100
+ --backend <opencode|codex|claude> Backend adapter. Default: opencode
101
+ --mode <plan|manual|safe-auto> Execution mode. Default depends on backend
102
+ --brief <path> Required task brief path
103
+ --cd <path> Project directory. Default: current directory
104
+ --out <path> Output run directory
105
+ --model <name> Optional backend model name
106
+ --timeout-ms <number> Timeout. Default: 1800000
107
+ --max-brief-lines <number> Default: 120
108
+ --allow-dirty Allow starting with dirty git worktree
109
+ --dry-run Build prompt/result skeleton without launching backend
110
+ --force Allow experimental/risky mode combinations or overwrite files
111
+ --help Show this help
112
+
113
+ Examples:
114
+ npx task-delegate doctor
115
+ npx task-delegate init-brief --out brief.md
116
+ npx task-delegate run --backend opencode --mode safe-auto --brief brief.md --cd .
117
+ npx task-delegate run --backend codex --mode manual --brief brief.md --cd .
118
+ npx task-delegate install-skill --dest .claude/skills
119
+ `);
120
+ }
121
+
122
+ export function runProcess(command, args, options = {}) {
123
+ const {
124
+ cwd = process.cwd(),
125
+ env = process.env,
126
+ timeoutMs = 30 * 60 * 1000,
127
+ stdin = undefined
128
+ } = options;
129
+
130
+ return new Promise((resolve) => {
131
+ const child = spawn(command, args, {
132
+ cwd,
133
+ env,
134
+ stdio: ['pipe', 'pipe', 'pipe'],
135
+ shell: false
136
+ });
137
+
138
+ let stdout = '';
139
+ let stderr = '';
140
+ let timedOut = false;
141
+
142
+ const timer = setTimeout(() => {
143
+ timedOut = true;
144
+ child.kill('SIGTERM');
145
+ setTimeout(() => child.kill('SIGKILL'), 3000).unref();
146
+ }, timeoutMs);
147
+
148
+ child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
149
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
150
+ child.on('error', (error) => {
151
+ clearTimeout(timer);
152
+ resolve({ command, args, cwd, exitCode: 127, stdout, stderr: `${stderr}\n${error.message}`, timedOut });
153
+ });
154
+ child.on('close', (exitCode) => {
155
+ clearTimeout(timer);
156
+ resolve({ command, args, cwd, exitCode, stdout, stderr, timedOut });
157
+ });
158
+
159
+ if (stdin) child.stdin.write(stdin);
160
+ child.stdin.end();
161
+ });
162
+ }
163
+
164
+ export async function commandExists(command) {
165
+ const probe = process.platform === 'win32'
166
+ ? await runProcess('where', [command], { timeoutMs: 5000 })
167
+ : await runProcess('command', ['-v', command], { timeoutMs: 5000, env: process.env });
168
+
169
+ if (process.platform !== 'win32' && probe.exitCode === 127) {
170
+ const fallback = await runProcess('which', [command], { timeoutMs: 5000 });
171
+ return fallback.exitCode === 0;
172
+ }
173
+
174
+ return probe.exitCode === 0;
175
+ }
176
+
177
+ export function relativePath(fromDir, targetPath) {
178
+ return path.relative(fromDir, targetPath) || '.';
179
+ }