@tagma/sdk 0.1.3 → 0.1.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 +139 -139
- package/package.json +4 -4
- package/src/adapters/stdin-approval.ts +117 -117
- package/src/adapters/websocket-approval.ts +144 -144
- package/src/completions/exit-code.ts +19 -19
- package/src/completions/file-exists.ts +39 -39
- package/src/completions/output-check.ts +57 -57
- package/src/dag.ts +137 -137
- package/src/drivers/claude-code.ts +207 -207
- package/src/engine.ts +637 -598
- package/src/hooks.ts +138 -138
- package/src/logger.ts +107 -100
- package/src/middlewares/static-context.ts +29 -29
- package/src/runner.ts +193 -193
- package/src/schema.ts +260 -260
- package/src/triggers/file.ts +94 -94
- package/src/triggers/manual.ts +61 -61
- package/src/utils.ts +147 -147
package/README.md
CHANGED
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
# @tagma/sdk
|
|
2
|
-
|
|
3
|
-
A local AI task orchestration SDK for [Bun](https://bun.sh). Define multi-track pipelines in YAML, run AI coding agents (Claude Code, Codex, OpenCode) and shell commands in parallel with dependency resolution, approval gates, and lifecycle hooks.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun add @tagma/sdk
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
**1. Define a pipeline** (`pipeline.yaml`)
|
|
14
|
-
|
|
15
|
-
```yaml
|
|
16
|
-
pipeline:
|
|
17
|
-
name: build-and-test
|
|
18
|
-
tracks:
|
|
19
|
-
- id: backend
|
|
20
|
-
name: Backend
|
|
21
|
-
driver: claude-code
|
|
22
|
-
permissions: { read: true, write: true, execute: false }
|
|
23
|
-
tasks:
|
|
24
|
-
- id: implement
|
|
25
|
-
name: Implement feature
|
|
26
|
-
prompt: "Add a /health endpoint to src/server.ts"
|
|
27
|
-
output: ./output/implement.txt
|
|
28
|
-
- id: test
|
|
29
|
-
name: Run tests
|
|
30
|
-
command: "bun test"
|
|
31
|
-
depends_on: [implement]
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**2. Run it programmatically**
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import {
|
|
38
|
-
bootstrapBuiltins,
|
|
39
|
-
loadPipeline,
|
|
40
|
-
runPipeline,
|
|
41
|
-
InMemoryApprovalGateway,
|
|
42
|
-
} from '@tagma/sdk';
|
|
43
|
-
|
|
44
|
-
// Register built-in drivers, triggers, completions
|
|
45
|
-
bootstrapBuiltins();
|
|
46
|
-
|
|
47
|
-
const yaml = await Bun.file('pipeline.yaml').text();
|
|
48
|
-
const config = await loadPipeline(yaml, process.cwd());
|
|
49
|
-
|
|
50
|
-
const result = await runPipeline(config, process.cwd());
|
|
51
|
-
console.log(result.success ? 'Done' : 'Failed');
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Features
|
|
55
|
-
|
|
56
|
-
- **Multi-track DAG execution** -- tasks run in parallel across tracks, respecting `depends_on` ordering
|
|
57
|
-
- **Driver plugins** -- built-in `claude-code` driver; install `@tagma/driver-codex` or `@tagma/driver-opencode` for other agents
|
|
58
|
-
- **Session handoff** -- `continue_from` passes context between tasks (session resume or text injection)
|
|
59
|
-
- **Approval gates** -- trigger-based approval with stdin and WebSocket adapters
|
|
60
|
-
- **Lifecycle hooks** -- `pipeline_start`, `task_start`, `task_success`, `task_failure`, `pipeline_complete`, `pipeline_error`
|
|
61
|
-
- **Middleware** -- enrich prompts before execution (e.g. inject static context)
|
|
62
|
-
- **Completion checks** -- validate task output with `exit_code`, `file_exists`, or `output_check` plugins
|
|
63
|
-
- **Template expansion** -- reusable task templates with parameterized `use` / `with`
|
|
64
|
-
|
|
65
|
-
## Pipeline YAML Reference
|
|
66
|
-
|
|
67
|
-
```yaml
|
|
68
|
-
pipeline:
|
|
69
|
-
name: my-pipeline
|
|
70
|
-
driver: claude-code # default driver for all tasks
|
|
71
|
-
timeout: 30m # pipeline-level timeout
|
|
72
|
-
plugins: # load external driver plugins
|
|
73
|
-
- "@tagma/driver-codex"
|
|
74
|
-
hooks:
|
|
75
|
-
pipeline_start: "echo starting"
|
|
76
|
-
task_failure: "notify-slack.sh"
|
|
77
|
-
tracks:
|
|
78
|
-
- id: track-1
|
|
79
|
-
name: Track One
|
|
80
|
-
model_tier: high # high | medium | low
|
|
81
|
-
permissions:
|
|
82
|
-
read: true
|
|
83
|
-
write: true
|
|
84
|
-
execute: false
|
|
85
|
-
on_failure: skip_downstream # skip_downstream | stop_all | ignore
|
|
86
|
-
tasks:
|
|
87
|
-
- id: task-a
|
|
88
|
-
name: Do something
|
|
89
|
-
prompt: "Your prompt here"
|
|
90
|
-
output: ./output/task-a.txt
|
|
91
|
-
timeout: 10m
|
|
92
|
-
- id: task-b
|
|
93
|
-
name: Follow up
|
|
94
|
-
prompt: "Continue the work"
|
|
95
|
-
continue_from: task-a
|
|
96
|
-
depends_on: [task-a]
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## API
|
|
100
|
-
|
|
101
|
-
### `bootstrapBuiltins()`
|
|
102
|
-
|
|
103
|
-
Registers all built-in plugins (claude-code driver, file/manual triggers, completion checks, static-context middleware).
|
|
104
|
-
|
|
105
|
-
### `loadPipeline(yaml: string, workDir: string): Promise<PipelineConfig>`
|
|
106
|
-
|
|
107
|
-
Parses YAML, resolves inheritance, expands templates, and validates the configuration.
|
|
108
|
-
|
|
109
|
-
### `runPipeline(config, workDir, options?): Promise<EngineResult>`
|
|
110
|
-
|
|
111
|
-
Executes the pipeline. Returns `{ success, summary, states }`.
|
|
112
|
-
|
|
113
|
-
Options:
|
|
114
|
-
- `approvalGateway` -- custom `ApprovalGateway` instance (defaults to `InMemoryApprovalGateway`)
|
|
115
|
-
|
|
116
|
-
### `loadPlugins(names: string[]): Promise<void>`
|
|
117
|
-
|
|
118
|
-
Dynamically loads and registers external plugin packages.
|
|
119
|
-
|
|
120
|
-
### `attachStdinApprovalAdapter(gateway): StdinApprovalAdapter`
|
|
121
|
-
|
|
122
|
-
Attaches an interactive stdin-based approval handler.
|
|
123
|
-
|
|
124
|
-
### `attachWebSocketApprovalAdapter(gateway, options?): WebSocketApprovalAdapter`
|
|
125
|
-
|
|
126
|
-
Starts a WebSocket server for remote approval decisions.
|
|
127
|
-
|
|
128
|
-
## Related Packages
|
|
129
|
-
|
|
130
|
-
| Package | Description |
|
|
131
|
-
|---|---|
|
|
132
|
-
| [@tagma/types](https://www.npmjs.com/package/@tagma/types) | Shared TypeScript types |
|
|
133
|
-
| [@tagma/driver-codex](https://www.npmjs.com/package/@tagma/driver-codex) | Codex CLI driver plugin |
|
|
134
|
-
| [@tagma/driver-opencode](https://www.npmjs.com/package/@tagma/driver-opencode) | OpenCode CLI driver plugin |
|
|
135
|
-
| [@tagma/cli](https://www.npmjs.com/package/@tagma/cli) | CLI runner |
|
|
136
|
-
|
|
137
|
-
## License
|
|
138
|
-
|
|
139
|
-
MIT
|
|
1
|
+
# @tagma/sdk
|
|
2
|
+
|
|
3
|
+
A local AI task orchestration SDK for [Bun](https://bun.sh). Define multi-track pipelines in YAML, run AI coding agents (Claude Code, Codex, OpenCode) and shell commands in parallel with dependency resolution, approval gates, and lifecycle hooks.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @tagma/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
**1. Define a pipeline** (`pipeline.yaml`)
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
pipeline:
|
|
17
|
+
name: build-and-test
|
|
18
|
+
tracks:
|
|
19
|
+
- id: backend
|
|
20
|
+
name: Backend
|
|
21
|
+
driver: claude-code
|
|
22
|
+
permissions: { read: true, write: true, execute: false }
|
|
23
|
+
tasks:
|
|
24
|
+
- id: implement
|
|
25
|
+
name: Implement feature
|
|
26
|
+
prompt: "Add a /health endpoint to src/server.ts"
|
|
27
|
+
output: ./output/implement.txt
|
|
28
|
+
- id: test
|
|
29
|
+
name: Run tests
|
|
30
|
+
command: "bun test"
|
|
31
|
+
depends_on: [implement]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**2. Run it programmatically**
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
bootstrapBuiltins,
|
|
39
|
+
loadPipeline,
|
|
40
|
+
runPipeline,
|
|
41
|
+
InMemoryApprovalGateway,
|
|
42
|
+
} from '@tagma/sdk';
|
|
43
|
+
|
|
44
|
+
// Register built-in drivers, triggers, completions
|
|
45
|
+
bootstrapBuiltins();
|
|
46
|
+
|
|
47
|
+
const yaml = await Bun.file('pipeline.yaml').text();
|
|
48
|
+
const config = await loadPipeline(yaml, process.cwd());
|
|
49
|
+
|
|
50
|
+
const result = await runPipeline(config, process.cwd());
|
|
51
|
+
console.log(result.success ? 'Done' : 'Failed');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- **Multi-track DAG execution** -- tasks run in parallel across tracks, respecting `depends_on` ordering
|
|
57
|
+
- **Driver plugins** -- built-in `claude-code` driver; install `@tagma/driver-codex` or `@tagma/driver-opencode` for other agents
|
|
58
|
+
- **Session handoff** -- `continue_from` passes context between tasks (session resume or text injection)
|
|
59
|
+
- **Approval gates** -- trigger-based approval with stdin and WebSocket adapters
|
|
60
|
+
- **Lifecycle hooks** -- `pipeline_start`, `task_start`, `task_success`, `task_failure`, `pipeline_complete`, `pipeline_error`
|
|
61
|
+
- **Middleware** -- enrich prompts before execution (e.g. inject static context)
|
|
62
|
+
- **Completion checks** -- validate task output with `exit_code`, `file_exists`, or `output_check` plugins
|
|
63
|
+
- **Template expansion** -- reusable task templates with parameterized `use` / `with`
|
|
64
|
+
|
|
65
|
+
## Pipeline YAML Reference
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
pipeline:
|
|
69
|
+
name: my-pipeline
|
|
70
|
+
driver: claude-code # default driver for all tasks
|
|
71
|
+
timeout: 30m # pipeline-level timeout
|
|
72
|
+
plugins: # load external driver plugins
|
|
73
|
+
- "@tagma/driver-codex"
|
|
74
|
+
hooks:
|
|
75
|
+
pipeline_start: "echo starting"
|
|
76
|
+
task_failure: "notify-slack.sh"
|
|
77
|
+
tracks:
|
|
78
|
+
- id: track-1
|
|
79
|
+
name: Track One
|
|
80
|
+
model_tier: high # high | medium | low
|
|
81
|
+
permissions:
|
|
82
|
+
read: true
|
|
83
|
+
write: true
|
|
84
|
+
execute: false
|
|
85
|
+
on_failure: skip_downstream # skip_downstream | stop_all | ignore
|
|
86
|
+
tasks:
|
|
87
|
+
- id: task-a
|
|
88
|
+
name: Do something
|
|
89
|
+
prompt: "Your prompt here"
|
|
90
|
+
output: ./output/task-a.txt
|
|
91
|
+
timeout: 10m
|
|
92
|
+
- id: task-b
|
|
93
|
+
name: Follow up
|
|
94
|
+
prompt: "Continue the work"
|
|
95
|
+
continue_from: task-a
|
|
96
|
+
depends_on: [task-a]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### `bootstrapBuiltins()`
|
|
102
|
+
|
|
103
|
+
Registers all built-in plugins (claude-code driver, file/manual triggers, completion checks, static-context middleware).
|
|
104
|
+
|
|
105
|
+
### `loadPipeline(yaml: string, workDir: string): Promise<PipelineConfig>`
|
|
106
|
+
|
|
107
|
+
Parses YAML, resolves inheritance, expands templates, and validates the configuration.
|
|
108
|
+
|
|
109
|
+
### `runPipeline(config, workDir, options?): Promise<EngineResult>`
|
|
110
|
+
|
|
111
|
+
Executes the pipeline. Returns `{ success, summary, states }`.
|
|
112
|
+
|
|
113
|
+
Options:
|
|
114
|
+
- `approvalGateway` -- custom `ApprovalGateway` instance (defaults to `InMemoryApprovalGateway`)
|
|
115
|
+
|
|
116
|
+
### `loadPlugins(names: string[]): Promise<void>`
|
|
117
|
+
|
|
118
|
+
Dynamically loads and registers external plugin packages.
|
|
119
|
+
|
|
120
|
+
### `attachStdinApprovalAdapter(gateway): StdinApprovalAdapter`
|
|
121
|
+
|
|
122
|
+
Attaches an interactive stdin-based approval handler.
|
|
123
|
+
|
|
124
|
+
### `attachWebSocketApprovalAdapter(gateway, options?): WebSocketApprovalAdapter`
|
|
125
|
+
|
|
126
|
+
Starts a WebSocket server for remote approval decisions.
|
|
127
|
+
|
|
128
|
+
## Related Packages
|
|
129
|
+
|
|
130
|
+
| Package | Description |
|
|
131
|
+
|---|---|
|
|
132
|
+
| [@tagma/types](https://www.npmjs.com/package/@tagma/types) | Shared TypeScript types |
|
|
133
|
+
| [@tagma/driver-codex](https://www.npmjs.com/package/@tagma/driver-codex) | Codex CLI driver plugin |
|
|
134
|
+
| [@tagma/driver-opencode](https://www.npmjs.com/package/@tagma/driver-opencode) | OpenCode CLI driver plugin |
|
|
135
|
+
| [@tagma/cli](https://www.npmjs.com/package/@tagma/cli) | CLI runner |
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tagma/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"plugins/*"
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"js-yaml": "^4.1.0",
|
|
22
22
|
"chokidar": "^4.0.0",
|
|
23
|
-
"@tagma/types": "
|
|
23
|
+
"@tagma/types": "workspace:*"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/js-yaml": "^4.0.9",
|
|
27
27
|
"bun-types": "latest",
|
|
28
28
|
"typescript": "^6.0.2",
|
|
29
|
-
"@tagma/driver-codex": "
|
|
30
|
-
"@tagma/driver-opencode": "
|
|
29
|
+
"@tagma/driver-codex": "workspace:*",
|
|
30
|
+
"@tagma/driver-opencode": "workspace:*"
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import * as readline from 'readline';
|
|
2
|
-
import type { ApprovalGateway, ApprovalRequest } from '../approval';
|
|
3
|
-
|
|
4
|
-
// ═══ CLI Stdin Adapter ═══
|
|
5
|
-
//
|
|
6
|
-
// Subscribes to the gateway's 'requested' events, prompts the user on stdout,
|
|
7
|
-
// reads a line from stdin, and calls gateway.resolve(). Handles at most one
|
|
8
|
-
// prompt at a time — additional requests queue up.
|
|
9
|
-
|
|
10
|
-
export interface StdinApprovalAdapter {
|
|
11
|
-
readonly detach: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function attachStdinApprovalAdapter(gateway: ApprovalGateway): StdinApprovalAdapter {
|
|
15
|
-
const queue: ApprovalRequest[] = [];
|
|
16
|
-
let processing = false;
|
|
17
|
-
let rl: readline.Interface | null = null;
|
|
18
|
-
|
|
19
|
-
function ensureReadline(): readline.Interface {
|
|
20
|
-
if (!rl) {
|
|
21
|
-
rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
22
|
-
}
|
|
23
|
-
return rl;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function readOneLine(): Promise<string> {
|
|
27
|
-
return new Promise((resolvePromise) => {
|
|
28
|
-
const reader = ensureReadline();
|
|
29
|
-
const handler = (line: string): void => {
|
|
30
|
-
reader.off('line', handler);
|
|
31
|
-
resolvePromise(line);
|
|
32
|
-
};
|
|
33
|
-
reader.on('line', handler);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function processNext(): Promise<void> {
|
|
38
|
-
if (processing) return;
|
|
39
|
-
processing = true;
|
|
40
|
-
try {
|
|
41
|
-
while (queue.length > 0) {
|
|
42
|
-
const req = queue.shift()!;
|
|
43
|
-
// If the request was already resolved by another path while queued, skip it.
|
|
44
|
-
if (!gateway.pending().some((p) => p.id === req.id)) continue;
|
|
45
|
-
|
|
46
|
-
const optionsStr = req.options.join(' / ');
|
|
47
|
-
process.stdout.write(
|
|
48
|
-
`\n[APPROVAL REQUIRED] ${req.message}\n` +
|
|
49
|
-
` id: ${req.id}\n` +
|
|
50
|
-
` task: ${req.taskId}${req.trackId ? ` (track: ${req.trackId})` : ''}\n` +
|
|
51
|
-
` options: ${optionsStr}\n` +
|
|
52
|
-
` > `,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const input = (await readOneLine()).trim().toLowerCase();
|
|
56
|
-
|
|
57
|
-
const approveAliases = new Set(['approve', 'yes', 'y', 'ok', 'true', '1']);
|
|
58
|
-
const rejectAliases = new Set(['reject', 'no', 'n', 'deny', 'false', '0']);
|
|
59
|
-
const matchedOption = req.options.find((o) => o.toLowerCase() === input);
|
|
60
|
-
|
|
61
|
-
if (matchedOption) {
|
|
62
|
-
const isReject = rejectAliases.has(matchedOption.toLowerCase());
|
|
63
|
-
gateway.resolve(req.id, {
|
|
64
|
-
outcome: isReject ? 'rejected' : 'approved',
|
|
65
|
-
choice: matchedOption,
|
|
66
|
-
actor: 'cli',
|
|
67
|
-
});
|
|
68
|
-
} else if (approveAliases.has(input)) {
|
|
69
|
-
gateway.resolve(req.id, { outcome: 'approved', choice: input, actor: 'cli' });
|
|
70
|
-
} else if (rejectAliases.has(input)) {
|
|
71
|
-
gateway.resolve(req.id, {
|
|
72
|
-
outcome: 'rejected',
|
|
73
|
-
choice: input,
|
|
74
|
-
actor: 'cli',
|
|
75
|
-
reason: 'user rejected via CLI',
|
|
76
|
-
});
|
|
77
|
-
} else {
|
|
78
|
-
process.stdout.write(` unrecognized input "${input}" — treating as rejection\n`);
|
|
79
|
-
gateway.resolve(req.id, {
|
|
80
|
-
outcome: 'rejected',
|
|
81
|
-
actor: 'cli',
|
|
82
|
-
reason: `unrecognized CLI input: ${input}`,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} finally {
|
|
87
|
-
processing = false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const unsubscribe = gateway.subscribe((event) => {
|
|
92
|
-
switch (event.type) {
|
|
93
|
-
case 'requested':
|
|
94
|
-
queue.push(event.request);
|
|
95
|
-
void processNext();
|
|
96
|
-
return;
|
|
97
|
-
case 'resolved':
|
|
98
|
-
case 'expired':
|
|
99
|
-
case 'aborted': {
|
|
100
|
-
// Drop from queue if it's still waiting its turn.
|
|
101
|
-
const idx = queue.findIndex((r) => r.id === event.request.id);
|
|
102
|
-
if (idx >= 0) queue.splice(idx, 1);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
detach: () => {
|
|
110
|
-
unsubscribe();
|
|
111
|
-
if (rl) {
|
|
112
|
-
rl.close();
|
|
113
|
-
rl = null;
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
import type { ApprovalGateway, ApprovalRequest } from '../approval';
|
|
3
|
+
|
|
4
|
+
// ═══ CLI Stdin Adapter ═══
|
|
5
|
+
//
|
|
6
|
+
// Subscribes to the gateway's 'requested' events, prompts the user on stdout,
|
|
7
|
+
// reads a line from stdin, and calls gateway.resolve(). Handles at most one
|
|
8
|
+
// prompt at a time — additional requests queue up.
|
|
9
|
+
|
|
10
|
+
export interface StdinApprovalAdapter {
|
|
11
|
+
readonly detach: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function attachStdinApprovalAdapter(gateway: ApprovalGateway): StdinApprovalAdapter {
|
|
15
|
+
const queue: ApprovalRequest[] = [];
|
|
16
|
+
let processing = false;
|
|
17
|
+
let rl: readline.Interface | null = null;
|
|
18
|
+
|
|
19
|
+
function ensureReadline(): readline.Interface {
|
|
20
|
+
if (!rl) {
|
|
21
|
+
rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
22
|
+
}
|
|
23
|
+
return rl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readOneLine(): Promise<string> {
|
|
27
|
+
return new Promise((resolvePromise) => {
|
|
28
|
+
const reader = ensureReadline();
|
|
29
|
+
const handler = (line: string): void => {
|
|
30
|
+
reader.off('line', handler);
|
|
31
|
+
resolvePromise(line);
|
|
32
|
+
};
|
|
33
|
+
reader.on('line', handler);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function processNext(): Promise<void> {
|
|
38
|
+
if (processing) return;
|
|
39
|
+
processing = true;
|
|
40
|
+
try {
|
|
41
|
+
while (queue.length > 0) {
|
|
42
|
+
const req = queue.shift()!;
|
|
43
|
+
// If the request was already resolved by another path while queued, skip it.
|
|
44
|
+
if (!gateway.pending().some((p) => p.id === req.id)) continue;
|
|
45
|
+
|
|
46
|
+
const optionsStr = req.options.join(' / ');
|
|
47
|
+
process.stdout.write(
|
|
48
|
+
`\n[APPROVAL REQUIRED] ${req.message}\n` +
|
|
49
|
+
` id: ${req.id}\n` +
|
|
50
|
+
` task: ${req.taskId}${req.trackId ? ` (track: ${req.trackId})` : ''}\n` +
|
|
51
|
+
` options: ${optionsStr}\n` +
|
|
52
|
+
` > `,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const input = (await readOneLine()).trim().toLowerCase();
|
|
56
|
+
|
|
57
|
+
const approveAliases = new Set(['approve', 'yes', 'y', 'ok', 'true', '1']);
|
|
58
|
+
const rejectAliases = new Set(['reject', 'no', 'n', 'deny', 'false', '0']);
|
|
59
|
+
const matchedOption = req.options.find((o) => o.toLowerCase() === input);
|
|
60
|
+
|
|
61
|
+
if (matchedOption) {
|
|
62
|
+
const isReject = rejectAliases.has(matchedOption.toLowerCase());
|
|
63
|
+
gateway.resolve(req.id, {
|
|
64
|
+
outcome: isReject ? 'rejected' : 'approved',
|
|
65
|
+
choice: matchedOption,
|
|
66
|
+
actor: 'cli',
|
|
67
|
+
});
|
|
68
|
+
} else if (approveAliases.has(input)) {
|
|
69
|
+
gateway.resolve(req.id, { outcome: 'approved', choice: input, actor: 'cli' });
|
|
70
|
+
} else if (rejectAliases.has(input)) {
|
|
71
|
+
gateway.resolve(req.id, {
|
|
72
|
+
outcome: 'rejected',
|
|
73
|
+
choice: input,
|
|
74
|
+
actor: 'cli',
|
|
75
|
+
reason: 'user rejected via CLI',
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
process.stdout.write(` unrecognized input "${input}" — treating as rejection\n`);
|
|
79
|
+
gateway.resolve(req.id, {
|
|
80
|
+
outcome: 'rejected',
|
|
81
|
+
actor: 'cli',
|
|
82
|
+
reason: `unrecognized CLI input: ${input}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
processing = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const unsubscribe = gateway.subscribe((event) => {
|
|
92
|
+
switch (event.type) {
|
|
93
|
+
case 'requested':
|
|
94
|
+
queue.push(event.request);
|
|
95
|
+
void processNext();
|
|
96
|
+
return;
|
|
97
|
+
case 'resolved':
|
|
98
|
+
case 'expired':
|
|
99
|
+
case 'aborted': {
|
|
100
|
+
// Drop from queue if it's still waiting its turn.
|
|
101
|
+
const idx = queue.findIndex((r) => r.id === event.request.id);
|
|
102
|
+
if (idx >= 0) queue.splice(idx, 1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
detach: () => {
|
|
110
|
+
unsubscribe();
|
|
111
|
+
if (rl) {
|
|
112
|
+
rl.close();
|
|
113
|
+
rl = null;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|