delegation-layer 0.1.2 → 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 +94 -149
- package/docs/assets/delegation-layer-hero.jpg +0 -0
- package/package.json +17 -2
- package/scripts/install-agent-integration.mjs +430 -10
package/README.md
CHANGED
|
@@ -1,129 +1,119 @@
|
|
|
1
1
|
# Delegation Layer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Provider availability is profile-specific; see the provider guide for native
|
|
35
|
-
platform prerequisites.
|
|
36
|
-
- Node.js 18 or newer is needed for the npm and pnpm installers; Bun is needed
|
|
37
|
-
for the Bun installer.
|
|
38
|
-
|
|
39
|
-
The provider executable must be available to the process that dispatches the
|
|
40
|
-
task. Delegation Layer runs the provider's version and help commands before
|
|
41
|
-
admission and accepts any reported version that is valid and advertises every
|
|
42
|
-
flag required by the adapter. It does not pin a provider release or predeclared
|
|
43
|
-
binary hash. A valid version here means nonempty trimmed UTF-8 output without control
|
|
44
|
-
characters; it is not required to match a semantic-version pattern. The
|
|
45
|
-
observed version is diagnostic evidence, while capability flags, executable
|
|
46
|
-
identity, and provider behavior decide compatibility. No web release lookup or
|
|
47
|
-
semver allowlist is used.
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="#install"><img src="docs/assets/delegation-layer-hero.jpg" alt="A lead AI model coordinating several specialist coding agents through Delegation Layer" width="1400"></a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Give your lead AI agent a reliable way to delegate bounded coding work to the
|
|
8
|
+
provider that fits the task.
|
|
9
|
+
|
|
10
|
+
Delegation Layer is a local CLI and Agent Skill for supervising one finite turn
|
|
11
|
+
of a coding agent, checking its capabilities at runtime, and returning a
|
|
12
|
+
validated result with durable evidence. It works with the provider CLIs you
|
|
13
|
+
already use instead of replacing them.
|
|
14
|
+
|
|
15
|
+
## Why use it?
|
|
16
|
+
|
|
17
|
+
- **One simple control layer** — your agent can discover providers and dispatch
|
|
18
|
+
work through one provider-neutral interface.
|
|
19
|
+
- **Bounded by design** — every task has an explicit workspace, permission mode,
|
|
20
|
+
and time budget.
|
|
21
|
+
- **Recoverable results** — a later `collect` call can read a finished task
|
|
22
|
+
without launching the provider again.
|
|
23
|
+
- **Runtime compatibility** — the selected provider's executable and advertised
|
|
24
|
+
capabilities are checked when the task is prepared.
|
|
25
|
+
- **Native authentication** — provider login stays with the provider CLI; task
|
|
26
|
+
state does not contain copied credentials.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
1. Install the Agent Skill so your harness knows how to use Delegation Layer.
|
|
31
|
+
2. The lead agent discovers the available provider capabilities.
|
|
32
|
+
3. Delegation Layer admits one bounded provider turn through the supervisor.
|
|
33
|
+
4. The lead agent collects the sealed result and evidence.
|
|
48
34
|
|
|
49
35
|
## Install
|
|
50
36
|
|
|
51
|
-
### Install the
|
|
37
|
+
### Install the Agent Skill
|
|
52
38
|
|
|
53
|
-
The
|
|
54
|
-
|
|
55
|
-
`$HOME/.local/bin`:
|
|
39
|
+
The standard Agent Skills installer works across popular coding agents. From
|
|
40
|
+
the project where you want the skill available, run:
|
|
56
41
|
|
|
57
42
|
```sh
|
|
58
|
-
|
|
43
|
+
npx --yes skills add hishamkaram/delegation-layer --skill agent-integration
|
|
59
44
|
```
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
The `--skill agent-integration` selection matters because this repository also
|
|
47
|
+
contains maintainer skills. The installer detects supported agents and lets you
|
|
48
|
+
choose the project or global scope. Add `--global` to install for all projects,
|
|
49
|
+
or `--agent codex` (and similar agent names) to choose a specific harness.
|
|
63
50
|
|
|
64
|
-
|
|
51
|
+
If you want the Delegation Layer guided installer instead, run:
|
|
65
52
|
|
|
66
53
|
```sh
|
|
67
|
-
|
|
54
|
+
npx --yes delegation-layer install
|
|
68
55
|
```
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
It provides an interactive scope and harness picker, with global installation
|
|
58
|
+
selected by default. For scripts or a choice without prompts:
|
|
72
59
|
|
|
73
60
|
```sh
|
|
74
|
-
npx --yes delegation-layer install
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
npx --yes delegation-layer install \
|
|
62
|
+
--scope project \
|
|
63
|
+
--harness universal \
|
|
64
|
+
--yes
|
|
77
65
|
```
|
|
78
66
|
|
|
79
|
-
|
|
67
|
+
The skill installer only installs guidance for your agent. It does not install
|
|
68
|
+
the CLI, change provider logins, or configure a supervisor.
|
|
69
|
+
|
|
70
|
+
### Install the CLI
|
|
71
|
+
|
|
72
|
+
Use the prebuilt installer that fits your machine:
|
|
80
73
|
|
|
81
74
|
```sh
|
|
82
|
-
|
|
83
|
-
go install github.com/hishamkaram/delegation-layer/cmd/delegate-run@latest
|
|
75
|
+
curl -fsSL https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/install.sh | sh
|
|
84
76
|
```
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
```sh
|
|
79
|
+
brew install hishamkaram/tap/delegation-layer
|
|
80
|
+
```
|
|
89
81
|
|
|
90
82
|
```sh
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
npx --yes delegation-layer install-cli
|
|
84
|
+
pnpm dlx delegation-layer install-cli
|
|
85
|
+
bunx --bun delegation-layer install-cli
|
|
93
86
|
```
|
|
94
87
|
|
|
95
|
-
|
|
88
|
+
The release installer verifies checksums and installs `delegate` and
|
|
89
|
+
`delegate-run` into `$HOME/.local/bin` by default. Set
|
|
90
|
+
`DELEGATION_LAYER_INSTALL_DIR` to use another directory.
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
install the current skill directly:
|
|
92
|
+
Check the installation:
|
|
99
93
|
|
|
100
94
|
```sh
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
delegate --help
|
|
96
|
+
delegate providers --json
|
|
103
97
|
```
|
|
104
98
|
|
|
105
|
-
|
|
106
|
-
pnpm, or Bun installer:
|
|
99
|
+
## What you need to run a task
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
Normal users do not need Go or Node after choosing an installation method.
|
|
102
|
+
Dispatching a task requires:
|
|
103
|
+
|
|
104
|
+
- a released Delegation Layer CLI;
|
|
105
|
+
- Pueue 4.0.4 with a private, running `pueued` daemon;
|
|
106
|
+
- a signed-in supported provider CLI such as `agy`, `codex`, `claude`, `pi`,
|
|
107
|
+
or `opencode`;
|
|
108
|
+
- Darwin or Linux on amd64 or arm64 for the supplied release builds.
|
|
116
109
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
and `DELEGATION_LAYER_VERSION` remain available when a deployment needs a
|
|
121
|
-
reproducible version.
|
|
110
|
+
Go is only needed for source builds and contributor work. Node.js 18 or newer
|
|
111
|
+
is needed for the npm-based installers; it is not a runtime requirement for the
|
|
112
|
+
released Go CLI. Provider authentication remains native to each provider.
|
|
122
113
|
|
|
123
|
-
##
|
|
114
|
+
## Try one bounded task
|
|
124
115
|
|
|
125
|
-
Create a brief and a workspace outside the state root
|
|
126
|
-
the Pueue configuration that controls the private daemon:
|
|
116
|
+
Create a brief and a workspace outside the private state root:
|
|
127
117
|
|
|
128
118
|
```sh
|
|
129
119
|
mkdir -p "$HOME/delegation-workspace"
|
|
@@ -139,80 +129,35 @@ delegate --pueue-config /absolute/path/to/pueue.yml dispatch \
|
|
|
139
129
|
```
|
|
140
130
|
|
|
141
131
|
The response contains a task ID and separate admission, liveness, and
|
|
142
|
-
publication fields. Observe it with
|
|
143
|
-
then read the validated result with `collect`:
|
|
132
|
+
publication fields. Observe and collect it with:
|
|
144
133
|
|
|
145
134
|
```sh
|
|
146
135
|
delegate status TASK_ID --json
|
|
147
136
|
delegate collect TASK_ID --watch 5s --json
|
|
148
137
|
```
|
|
149
138
|
|
|
150
|
-
Use
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
## Providers
|
|
155
|
-
|
|
156
|
-
| Profile | Modes | Options |
|
|
157
|
-
| --- | --- | --- |
|
|
158
|
-
| `antigravity:print` | `workspace-write` | `continuation`, `native-timeout` |
|
|
159
|
-
| `codex:exec` | `read-only`, `workspace-write` | `continuation` |
|
|
160
|
-
| `claude:print` | `read-only`, `workspace-write` | `continuation` |
|
|
161
|
-
| `pi:json` | `read-only` | `continuation`, `model`, `effort` |
|
|
162
|
-
| `opencode:run` | `read-only`, `workspace-write` | `continuation`, `model`, `effort` |
|
|
163
|
-
|
|
164
|
-
Run `delegate providers --json` for the compiled capability catalog. See
|
|
165
|
-
[providers](docs/providers.md) for launch behavior, policy boundaries, and
|
|
166
|
-
runtime compatibility checks.
|
|
167
|
-
Use `delegate capabilities --provider PROFILE --json` for one provider's
|
|
168
|
-
provider-neutral contract. Its catalog response is descriptive; dispatch is
|
|
169
|
-
where the supervised runtime probe establishes host compatibility.
|
|
170
|
-
|
|
171
|
-
When native logins are available, `make acceptance-native` runs the Pi and
|
|
172
|
-
OpenCode live gates through a private supervisor. A pass exits `0`; an
|
|
173
|
-
unavailable executable, supervisor, or login emits a sanitized `BLOCKED`
|
|
174
|
-
receipt and exits `2`, which the aggregate target treats as neutral. Real
|
|
175
|
-
behavior and evidence failures still fail the target.
|
|
176
|
-
|
|
177
|
-
The catalog uses public request option names. The public `effort` option maps
|
|
178
|
-
to Pi's native `--thinking` flag and OpenCode's native `--variant` flag.
|
|
179
|
-
|
|
180
|
-
## Safety model
|
|
181
|
-
|
|
182
|
-
- Every task has one immutable request and one provider turn.
|
|
183
|
-
- State, workspace, provider runtime, and temporary directories are canonical
|
|
184
|
-
and kept disjoint where the selected policy requires it.
|
|
185
|
-
- The supervisor owns process lifetime; the runner owns capture and sealing.
|
|
186
|
-
- Admission and launch re-check executable identity, effective policy, and
|
|
187
|
-
session identity.
|
|
188
|
-
- Briefs and control responses are bounded. Sealed payloads and raw streams are
|
|
189
|
-
described by validated paths, sizes, and SHA-256 digests.
|
|
190
|
-
- Provider authentication remains native to the provider CLI. Delegation Layer
|
|
191
|
-
does not copy credentials into task state or rewrite account settings.
|
|
192
|
-
|
|
193
|
-
Read [security](docs/security.md) for the full boundary and its operational
|
|
194
|
-
limitations.
|
|
139
|
+
Use `delegate providers --json` for the compiled provider catalog and
|
|
140
|
+
`delegate capabilities --provider PROFILE --json` for a provider-neutral
|
|
141
|
+
capability response. Dispatch still performs the host-specific runtime probe;
|
|
142
|
+
catalog discovery alone does not prove authentication or supervisor readiness.
|
|
195
143
|
|
|
196
144
|
## Documentation
|
|
197
145
|
|
|
198
|
-
- [Getting started](docs/getting-started.md) — install
|
|
199
|
-
first task.
|
|
146
|
+
- [Getting started](docs/getting-started.md) — install and run a first task.
|
|
200
147
|
- [CLI reference](docs/cli-reference.md) — commands, flags, responses, and
|
|
201
148
|
exit codes.
|
|
202
|
-
- [Integration API](docs/api.md) — JSON responses
|
|
203
|
-
idempotent client behavior.
|
|
204
|
-
- [Provider guide](docs/providers.md) — supported profiles and compatibility
|
|
149
|
+
- [Integration API](docs/api.md) — JSON responses and idempotent client
|
|
205
150
|
behavior.
|
|
206
|
-
- [
|
|
207
|
-
- [
|
|
208
|
-
- [
|
|
151
|
+
- [Provider guide](docs/providers.md) — profiles and runtime compatibility.
|
|
152
|
+
- [Architecture](docs/architecture.md) — lifecycle and ownership boundaries.
|
|
153
|
+
- [System components](diagrams/components.html) — interactive Archify view.
|
|
154
|
+
- [Task lifecycle](diagrams/lead-interface.html) — interactive lifecycle view.
|
|
209
155
|
- [Security](docs/security.md) — isolation, policy, and result integrity.
|
|
210
156
|
- [Troubleshooting](docs/troubleshooting.md) — common setup and runtime
|
|
211
157
|
failures.
|
|
212
|
-
- [
|
|
213
|
-
|
|
214
|
-
- [
|
|
215
|
-
JSON and evidence handling guidance for agents.
|
|
158
|
+
- [Agent integration skill](skills/agent-integration/SKILL.md) — the
|
|
159
|
+
provider-neutral instructions installed into an agent harness.
|
|
160
|
+
- [Contributing](CONTRIBUTING.md) — development and review guidance.
|
|
216
161
|
|
|
217
162
|
## License
|
|
218
163
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delegation-layer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Multi-provider AI agent delegation CLI and Agent Skill for running bounded coding tasks across provider CLIs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agents",
|
|
7
|
+
"agent-skills",
|
|
8
|
+
"ai-orchestration",
|
|
9
|
+
"coding-agents",
|
|
10
|
+
"multi-agent",
|
|
11
|
+
"llm-orchestration",
|
|
12
|
+
"model-routing",
|
|
13
|
+
"delegation",
|
|
14
|
+
"developer-tools",
|
|
15
|
+
"cli",
|
|
16
|
+
"automation",
|
|
17
|
+
"golang"
|
|
18
|
+
],
|
|
5
19
|
"type": "module",
|
|
6
20
|
"bin": {
|
|
7
21
|
"delegation-layer": "scripts/install-agent-integration.mjs"
|
|
8
22
|
},
|
|
9
23
|
"files": [
|
|
10
24
|
"LICENSE",
|
|
25
|
+
"docs/assets/delegation-layer-hero.jpg",
|
|
11
26
|
"install.sh",
|
|
12
27
|
"skills/agent-integration/SKILL.md",
|
|
13
28
|
"scripts/install-agent-integration.mjs",
|
|
@@ -1,23 +1,116 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { accessSync, constants } from "node:fs";
|
|
3
4
|
import { copyFile, lstat, mkdir, readFile } from "node:fs/promises";
|
|
4
|
-
import {
|
|
5
|
+
import { emitKeypressEvents } from "node:readline";
|
|
6
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
5
8
|
import { fileURLToPath } from "node:url";
|
|
6
9
|
|
|
7
10
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
11
|
const sourcePath = join(packageRoot, "skills", "agent-integration", "SKILL.md");
|
|
12
|
+
const harnesses = [
|
|
13
|
+
{
|
|
14
|
+
name: "universal",
|
|
15
|
+
label: "Universal Agent Skills",
|
|
16
|
+
projectPath: join(".agents", "skills"),
|
|
17
|
+
globalPath: join(".agents", "skills"),
|
|
18
|
+
projectMarkers: [".agents"],
|
|
19
|
+
globalMarkers: [".agents"],
|
|
20
|
+
commands: [],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "codex",
|
|
24
|
+
label: "Codex",
|
|
25
|
+
projectPath: join(".agents", "skills"),
|
|
26
|
+
globalPath: join(".agents", "skills"),
|
|
27
|
+
projectMarkers: [".agents", ".codex"],
|
|
28
|
+
globalMarkers: [".agents", ".codex"],
|
|
29
|
+
commands: ["codex"],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "claude",
|
|
33
|
+
label: "Claude Code",
|
|
34
|
+
projectPath: join(".claude", "skills"),
|
|
35
|
+
globalPath: join(".claude", "skills"),
|
|
36
|
+
projectMarkers: [".claude"],
|
|
37
|
+
globalMarkers: [".claude"],
|
|
38
|
+
commands: ["claude"],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "cursor",
|
|
42
|
+
label: "Cursor",
|
|
43
|
+
projectPath: join(".cursor", "skills"),
|
|
44
|
+
globalPath: join(".cursor", "skills"),
|
|
45
|
+
projectMarkers: [".cursor"],
|
|
46
|
+
globalMarkers: [".cursor"],
|
|
47
|
+
commands: ["cursor"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "gemini",
|
|
51
|
+
label: "Gemini CLI",
|
|
52
|
+
projectPath: join(".gemini", "skills"),
|
|
53
|
+
globalPath: join(".gemini", "skills"),
|
|
54
|
+
projectMarkers: [".gemini"],
|
|
55
|
+
globalMarkers: [".gemini"],
|
|
56
|
+
commands: ["gemini"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "opencode",
|
|
60
|
+
label: "OpenCode",
|
|
61
|
+
projectPath: join(".opencode", "skills"),
|
|
62
|
+
globalPath: join(".config", "opencode", "skills"),
|
|
63
|
+
projectMarkers: [".opencode"],
|
|
64
|
+
globalMarkers: [join(".config", "opencode")],
|
|
65
|
+
commands: ["opencode"],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "pi",
|
|
69
|
+
label: "Pi",
|
|
70
|
+
projectPath: join(".pi", "skills"),
|
|
71
|
+
globalPath: join(".pi", "agent", "skills"),
|
|
72
|
+
projectMarkers: [".pi"],
|
|
73
|
+
globalMarkers: [join(".pi", "agent")],
|
|
74
|
+
commands: ["pi"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "hermes",
|
|
78
|
+
label: "Hermes",
|
|
79
|
+
projectPath: join(".hermes", "skills"),
|
|
80
|
+
globalPath: join(".hermes", "skills"),
|
|
81
|
+
projectMarkers: [".hermes"],
|
|
82
|
+
globalMarkers: [".hermes"],
|
|
83
|
+
commands: ["hermes"],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const harnessByName = new Map(harnesses.map((harness) => [harness.name, harness]));
|
|
9
87
|
|
|
10
|
-
function usage(message) {
|
|
88
|
+
function usage(message, exitCode = 2) {
|
|
11
89
|
if (message) {
|
|
12
90
|
console.error(`Error: ${message}`);
|
|
13
91
|
}
|
|
14
92
|
console.error(
|
|
15
|
-
"Usage:\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
|
|
93
|
+
"Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
|
|
16
94
|
);
|
|
17
|
-
process.exitCode =
|
|
95
|
+
process.exitCode = exitCode;
|
|
18
96
|
}
|
|
19
97
|
|
|
20
|
-
function
|
|
98
|
+
function help() {
|
|
99
|
+
console.log(
|
|
100
|
+
"Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]\n\nThe install command detects supported agent harnesses and installs the skill for a project or user.\nGlobal scope is the default; use --scope project for a project-local install.\nUse --harness universal when no specific harness is detected.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function requiredValue(argumentsList, index, option) {
|
|
105
|
+
const value = argumentsList[index + 1];
|
|
106
|
+
if (!value || value.startsWith("--")) {
|
|
107
|
+
usage(`${option} requires a value`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseLegacyArguments(argumentsList) {
|
|
21
114
|
let target;
|
|
22
115
|
let force = false;
|
|
23
116
|
|
|
@@ -28,14 +121,15 @@ function parseArguments(argumentsList) {
|
|
|
28
121
|
continue;
|
|
29
122
|
}
|
|
30
123
|
if (argument === "--target") {
|
|
31
|
-
target = argumentsList
|
|
124
|
+
target = requiredValue(argumentsList, index, "--target");
|
|
125
|
+
if (!target) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
32
128
|
index += 1;
|
|
33
129
|
continue;
|
|
34
130
|
}
|
|
35
131
|
if (argument === "--help" || argument === "-h") {
|
|
36
|
-
|
|
37
|
-
"Usage:\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
|
|
38
|
-
);
|
|
132
|
+
help();
|
|
39
133
|
process.exit(0);
|
|
40
134
|
}
|
|
41
135
|
usage(`unknown argument ${argument}`);
|
|
@@ -53,6 +147,63 @@ function parseArguments(argumentsList) {
|
|
|
53
147
|
return { force, target: resolve(target) };
|
|
54
148
|
}
|
|
55
149
|
|
|
150
|
+
function parseInstallArguments(argumentsList) {
|
|
151
|
+
let force = false;
|
|
152
|
+
let harnessNames;
|
|
153
|
+
let scope;
|
|
154
|
+
let yes = false;
|
|
155
|
+
|
|
156
|
+
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
157
|
+
const argument = argumentsList[index];
|
|
158
|
+
if (argument === "--force") {
|
|
159
|
+
force = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (argument === "--yes" || argument === "-y") {
|
|
163
|
+
yes = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (argument === "--scope") {
|
|
167
|
+
scope = requiredValue(argumentsList, index, "--scope");
|
|
168
|
+
if (!scope) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
index += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (argument === "--harness") {
|
|
175
|
+
const value = requiredValue(argumentsList, index, "--harness");
|
|
176
|
+
if (!value) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
harnessNames = value.split(",").map((name) => name.trim().toLowerCase());
|
|
180
|
+
index += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (argument === "--help" || argument === "-h") {
|
|
184
|
+
help();
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
usage(`unknown argument ${argument}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (scope && scope !== "project" && scope !== "global") {
|
|
192
|
+
usage("--scope must be project or global");
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (harnessNames?.length === 0 || harnessNames?.some((name) => !name)) {
|
|
196
|
+
usage("--harness requires at least one harness name");
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
if (harnessNames?.some((name) => !harnessByName.has(name))) {
|
|
200
|
+
const unknown = harnessNames.find((name) => !harnessByName.has(name));
|
|
201
|
+
usage(`unknown harness ${unknown}; choose from ${harnesses.map((harness) => harness.name).join(", ")}`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return { force, harnessNames, scope, yes };
|
|
205
|
+
}
|
|
206
|
+
|
|
56
207
|
async function existingPath(path) {
|
|
57
208
|
try {
|
|
58
209
|
return await lstat(path);
|
|
@@ -64,6 +215,255 @@ async function existingPath(path) {
|
|
|
64
215
|
}
|
|
65
216
|
}
|
|
66
217
|
|
|
218
|
+
function executableOnPath(command) {
|
|
219
|
+
const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
220
|
+
const candidates = process.platform === "win32" ? [command, `${command}.exe`, `${command}.cmd`] : [command];
|
|
221
|
+
return pathEntries.some((pathEntry) =>
|
|
222
|
+
candidates.some((candidate) => {
|
|
223
|
+
try {
|
|
224
|
+
accessSync(join(pathEntry, candidate), constants.X_OK);
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function detectHarnesses({ cwd, home }) {
|
|
234
|
+
return Promise.all(
|
|
235
|
+
harnesses.map(async (harness) => {
|
|
236
|
+
const reasons = [];
|
|
237
|
+
for (const marker of harness.projectMarkers) {
|
|
238
|
+
const markerPath = resolve(cwd, marker);
|
|
239
|
+
if ((await existingPath(markerPath))?.isDirectory()) {
|
|
240
|
+
reasons.push(`project ${marker}`);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const marker of harness.globalMarkers) {
|
|
245
|
+
const markerPath = resolve(home, marker);
|
|
246
|
+
if ((await existingPath(markerPath))?.isDirectory()) {
|
|
247
|
+
reasons.push(`global ${marker}`);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const command of harness.commands) {
|
|
252
|
+
if (executableOnPath(command)) {
|
|
253
|
+
reasons.push(`${command} on PATH`);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { harness, reasons };
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function defaultHarnessNames(detections) {
|
|
263
|
+
const detected = detections
|
|
264
|
+
.filter(({ harness, reasons }) => harness.name !== "universal" && reasons.length > 0)
|
|
265
|
+
.map(({ harness }) => harness.name);
|
|
266
|
+
if (detected.length > 0) {
|
|
267
|
+
return detected;
|
|
268
|
+
}
|
|
269
|
+
return ["universal"];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatDetected(reasons) {
|
|
273
|
+
return reasons.length > 0 ? ` (detected: ${reasons.join(", ")})` : "";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function printDetections(detections) {
|
|
277
|
+
console.log("Detected harnesses:");
|
|
278
|
+
detections.forEach(({ harness, reasons }, index) => {
|
|
279
|
+
console.log(` ${index + 1}. ${harness.label}${formatDetected(reasons)}`);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderPicker({ error, index, multiple, options, selected, title }) {
|
|
284
|
+
const lines = [title, ""];
|
|
285
|
+
options.forEach((option, optionIndex) => {
|
|
286
|
+
const current = optionIndex === index;
|
|
287
|
+
const marker = multiple ? (selected.has(option.value) ? "[x]" : "[ ]") : "";
|
|
288
|
+
const cursor = current ? "❯" : " ";
|
|
289
|
+
const color = current ? "\u001b[36m" : "";
|
|
290
|
+
const reset = current ? "\u001b[0m" : "";
|
|
291
|
+
lines.push(`${color}${cursor} ${marker ? `${marker} ` : ""}${option.label}${reset}`);
|
|
292
|
+
if (option.description) {
|
|
293
|
+
lines.push(` ${option.description}`);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push(multiple ? "↑↓ move Space select Enter continue a all n none q cancel" : "↑↓ move Enter select q cancel");
|
|
298
|
+
if (error) {
|
|
299
|
+
lines.push(`\u001b[31m${error}\u001b[0m`);
|
|
300
|
+
}
|
|
301
|
+
process.stdout.write(`\u001b[2J\u001b[H${lines.join("\n")}\n`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function pickOptions({ initialValues = [], multiple = false, options, title }) {
|
|
305
|
+
const input = process.stdin;
|
|
306
|
+
const output = process.stdout;
|
|
307
|
+
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let index = 0;
|
|
312
|
+
const selected = new Set(initialValues);
|
|
313
|
+
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
let settled = false;
|
|
316
|
+
let error;
|
|
317
|
+
|
|
318
|
+
const cleanup = () => {
|
|
319
|
+
input.off("keypress", onKeypress);
|
|
320
|
+
try {
|
|
321
|
+
input.setRawMode(false);
|
|
322
|
+
} catch {
|
|
323
|
+
// The terminal may already be closed while handling cancellation.
|
|
324
|
+
}
|
|
325
|
+
input.pause();
|
|
326
|
+
output.write("\u001b[?25h\u001b[0m");
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const finish = (value) => {
|
|
330
|
+
if (settled) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
settled = true;
|
|
334
|
+
cleanup();
|
|
335
|
+
resolve(value);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const onKeypress = (character, key = {}) => {
|
|
339
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape" || key.name === "q") {
|
|
340
|
+
finish(null);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (key.name === "up" || key.name === "k") {
|
|
344
|
+
index = (index + options.length - 1) % options.length;
|
|
345
|
+
error = undefined;
|
|
346
|
+
renderPicker({ error, index, multiple, options, selected, title });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (key.name === "down" || key.name === "j") {
|
|
350
|
+
index = (index + 1) % options.length;
|
|
351
|
+
error = undefined;
|
|
352
|
+
renderPicker({ error, index, multiple, options, selected, title });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (multiple && (key.name === "space" || character === " ")) {
|
|
356
|
+
const value = options[index].value;
|
|
357
|
+
if (selected.has(value)) {
|
|
358
|
+
selected.delete(value);
|
|
359
|
+
} else {
|
|
360
|
+
selected.add(value);
|
|
361
|
+
}
|
|
362
|
+
error = undefined;
|
|
363
|
+
renderPicker({ error, index, multiple, options, selected, title });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (multiple && key.name === "a") {
|
|
367
|
+
options.forEach((option) => selected.add(option.value));
|
|
368
|
+
error = undefined;
|
|
369
|
+
renderPicker({ error, index, multiple, options, selected, title });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (multiple && key.name === "n") {
|
|
373
|
+
selected.clear();
|
|
374
|
+
error = undefined;
|
|
375
|
+
renderPicker({ error, index, multiple, options, selected, title });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (key.name === "return" || key.name === "enter") {
|
|
379
|
+
if (multiple && selected.size === 0) {
|
|
380
|
+
error = "Select at least one harness, or press q to cancel.";
|
|
381
|
+
renderPicker({ error, index, multiple, options, selected, title });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
finish(multiple ? options.filter((option) => selected.has(option.value)).map((option) => option.value) : options[index].value);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
emitKeypressEvents(input);
|
|
390
|
+
input.setRawMode(true);
|
|
391
|
+
input.resume();
|
|
392
|
+
input.on("keypress", onKeypress);
|
|
393
|
+
output.write("\u001b[?25l");
|
|
394
|
+
renderPicker({ error, index, multiple, options, selected, title });
|
|
395
|
+
} catch (pickerError) {
|
|
396
|
+
cleanup();
|
|
397
|
+
reject(pickerError);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
|
|
403
|
+
const cwd = process.cwd();
|
|
404
|
+
const home = resolve(process.env.DELEGATION_LAYER_HOME ?? homedir());
|
|
405
|
+
const detections = await detectHarnesses({ cwd, home });
|
|
406
|
+
const defaults = defaultHarnessNames(detections);
|
|
407
|
+
const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
408
|
+
const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
|
|
409
|
+
|
|
410
|
+
if (!harnessNames && !needsPrompt) {
|
|
411
|
+
printDetections(detections);
|
|
412
|
+
}
|
|
413
|
+
if (!canPrompt && !yes && !harnessNames && !scope) {
|
|
414
|
+
console.log("No interactive terminal detected; using global scope and detected harnesses.");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (needsPrompt) {
|
|
418
|
+
scope = await pickOptions({
|
|
419
|
+
initialValues: ["global"],
|
|
420
|
+
options: [
|
|
421
|
+
{
|
|
422
|
+
value: "global",
|
|
423
|
+
label: "Global",
|
|
424
|
+
description: `available to your agents from ${home}`,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
value: "project",
|
|
428
|
+
label: "Project",
|
|
429
|
+
description: `available to this project from ${cwd}`,
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
title: "Where should the agent skill be installed?",
|
|
433
|
+
});
|
|
434
|
+
if (!scope) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
harnessNames = await pickOptions({
|
|
438
|
+
initialValues: defaults,
|
|
439
|
+
multiple: true,
|
|
440
|
+
options: detections.map(({ harness, reasons }) => ({
|
|
441
|
+
value: harness.name,
|
|
442
|
+
label: `${harness.label}${formatDetected(reasons)}`,
|
|
443
|
+
description: targetFor(harness, scope, cwd, home),
|
|
444
|
+
})),
|
|
445
|
+
title: "Which harnesses should use the skill?",
|
|
446
|
+
});
|
|
447
|
+
if (!harnessNames) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
cwd,
|
|
454
|
+
force,
|
|
455
|
+
harnessNames: harnessNames ?? defaults,
|
|
456
|
+
home,
|
|
457
|
+
scope: scope ?? "global",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function targetFor(harness, scope, cwd, home) {
|
|
462
|
+
const root = scope === "global" ? home : cwd;
|
|
463
|
+
const relativePath = scope === "global" ? harness.globalPath : harness.projectPath;
|
|
464
|
+
return join(resolve(root, relativePath), "agent-integration");
|
|
465
|
+
}
|
|
466
|
+
|
|
67
467
|
async function install({ force, target }) {
|
|
68
468
|
const targetStat = await existingPath(target);
|
|
69
469
|
if (targetStat?.isSymbolicLink()) {
|
|
@@ -109,8 +509,28 @@ if (argumentsList[0] === "install-cli") {
|
|
|
109
509
|
console.error(`Error: ${error.message}`);
|
|
110
510
|
process.exitCode = 1;
|
|
111
511
|
}
|
|
512
|
+
} else if (argumentsList[0] === "install") {
|
|
513
|
+
const options = parseInstallArguments(argumentsList.slice(1));
|
|
514
|
+
if (options) {
|
|
515
|
+
try {
|
|
516
|
+
const selected = await chooseInstallOptions(options);
|
|
517
|
+
if (!selected) {
|
|
518
|
+
console.log("Installation cancelled.");
|
|
519
|
+
} else {
|
|
520
|
+
const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
|
|
521
|
+
const uniqueTargets = [...new Set(targets)];
|
|
522
|
+
console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
|
|
523
|
+
for (const target of uniqueTargets) {
|
|
524
|
+
await install({ force: selected.force, target });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.error(`Error: ${error.message}`);
|
|
529
|
+
process.exitCode = 1;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
112
532
|
} else {
|
|
113
|
-
const options =
|
|
533
|
+
const options = parseLegacyArguments(argumentsList);
|
|
114
534
|
if (options) {
|
|
115
535
|
try {
|
|
116
536
|
await install(options);
|