delegation-layer 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/README.md +87 -153
- package/docs/assets/delegation-layer-hero.jpg +0 -0
- package/package.json +19 -3
- package/scripts/install-agent-integration.mjs +311 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hesham Karm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,129 +1,110 @@
|
|
|
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
|
-
|
|
54
|
-
checksum, and installs both `delegate` and `delegate-run` in
|
|
55
|
-
`$HOME/.local/bin`:
|
|
39
|
+
From the project where you want the skill available, run:
|
|
56
40
|
|
|
57
41
|
```sh
|
|
58
|
-
|
|
42
|
+
npx --yes delegation-layer install
|
|
59
43
|
```
|
|
60
44
|
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
The installer detects supported Agent Skills harnesses, shows what it found,
|
|
46
|
+
and asks whether to install for the current project or your user account. It
|
|
47
|
+
offers the shared `.agents/skills` location when no specific harness is found.
|
|
63
48
|
|
|
64
|
-
|
|
49
|
+
For scripts or a choice without prompts:
|
|
65
50
|
|
|
66
51
|
```sh
|
|
67
|
-
|
|
52
|
+
npx --yes delegation-layer install \
|
|
53
|
+
--scope project \
|
|
54
|
+
--harness universal \
|
|
55
|
+
--yes
|
|
68
56
|
```
|
|
69
57
|
|
|
70
|
-
The
|
|
71
|
-
|
|
58
|
+
The skill installer only installs guidance for your agent. It does not install
|
|
59
|
+
the CLI, change provider logins, or configure a supervisor.
|
|
60
|
+
|
|
61
|
+
### Install the CLI
|
|
62
|
+
|
|
63
|
+
Use the prebuilt installer that fits your machine:
|
|
72
64
|
|
|
73
65
|
```sh
|
|
74
|
-
|
|
75
|
-
pnpm dlx delegation-layer install-cli
|
|
76
|
-
bunx --bun delegation-layer install-cli
|
|
66
|
+
curl -fsSL https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/install.sh | sh
|
|
77
67
|
```
|
|
78
68
|
|
|
79
|
-
|
|
69
|
+
```sh
|
|
70
|
+
brew install hishamkaram/tap/delegation-layer
|
|
71
|
+
```
|
|
80
72
|
|
|
81
73
|
```sh
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
npx --yes delegation-layer install-cli
|
|
75
|
+
pnpm dlx delegation-layer install-cli
|
|
76
|
+
bunx --bun delegation-layer install-cli
|
|
84
77
|
```
|
|
85
78
|
|
|
86
|
-
|
|
79
|
+
The release installer verifies checksums and installs `delegate` and
|
|
80
|
+
`delegate-run` into `$HOME/.local/bin` by default. Set
|
|
81
|
+
`DELEGATION_LAYER_INSTALL_DIR` to use another directory.
|
|
87
82
|
|
|
88
|
-
|
|
83
|
+
Check the installation:
|
|
89
84
|
|
|
90
85
|
```sh
|
|
91
86
|
delegate --help
|
|
92
87
|
delegate providers --json
|
|
93
88
|
```
|
|
94
89
|
|
|
95
|
-
|
|
90
|
+
## What you need to run a task
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
Normal users do not need Go or Node after choosing an installation method.
|
|
93
|
+
Dispatching a task requires:
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Any harness with a writable skill directory can use the dependency-free npm,
|
|
106
|
-
pnpm, or Bun installer:
|
|
107
|
-
|
|
108
|
-
```sh
|
|
109
|
-
npx --yes delegation-layer \
|
|
110
|
-
--target "$HOME/.hermes/skills/agent-integration"
|
|
111
|
-
pnpm dlx delegation-layer \
|
|
112
|
-
--target "$HOME/.hermes/skills/agent-integration"
|
|
113
|
-
bunx --bun delegation-layer \
|
|
114
|
-
--target "$HOME/.hermes/skills/agent-integration"
|
|
115
|
-
```
|
|
95
|
+
- a released Delegation Layer CLI;
|
|
96
|
+
- Pueue 4.0.4 with a private, running `pueued` daemon;
|
|
97
|
+
- a signed-in supported provider CLI such as `agy`, `codex`, `claude`, `pi`,
|
|
98
|
+
or `opencode`;
|
|
99
|
+
- Darwin or Linux on amd64 or arm64 for the supplied release builds.
|
|
116
100
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
and `DELEGATION_LAYER_VERSION` remain available when a deployment needs a
|
|
121
|
-
reproducible version.
|
|
101
|
+
Go is only needed for source builds and contributor work. Node.js 18 or newer
|
|
102
|
+
is needed for the npm-based installers; it is not a runtime requirement for the
|
|
103
|
+
released Go CLI. Provider authentication remains native to each provider.
|
|
122
104
|
|
|
123
|
-
##
|
|
105
|
+
## Try one bounded task
|
|
124
106
|
|
|
125
|
-
Create a brief and a workspace outside the state root
|
|
126
|
-
the Pueue configuration that controls the private daemon:
|
|
107
|
+
Create a brief and a workspace outside the private state root:
|
|
127
108
|
|
|
128
109
|
```sh
|
|
129
110
|
mkdir -p "$HOME/delegation-workspace"
|
|
@@ -139,83 +120,36 @@ delegate --pueue-config /absolute/path/to/pueue.yml dispatch \
|
|
|
139
120
|
```
|
|
140
121
|
|
|
141
122
|
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`:
|
|
123
|
+
publication fields. Observe and collect it with:
|
|
144
124
|
|
|
145
125
|
```sh
|
|
146
126
|
delegate status TASK_ID --json
|
|
147
127
|
delegate collect TASK_ID --watch 5s --json
|
|
148
128
|
```
|
|
149
129
|
|
|
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.
|
|
130
|
+
Use `delegate providers --json` for the compiled provider catalog and
|
|
131
|
+
`delegate capabilities --provider PROFILE --json` for a provider-neutral
|
|
132
|
+
capability response. Dispatch still performs the host-specific runtime probe;
|
|
133
|
+
catalog discovery alone does not prove authentication or supervisor readiness.
|
|
195
134
|
|
|
196
135
|
## Documentation
|
|
197
136
|
|
|
198
|
-
- [Getting started](docs/getting-started.md) — install
|
|
199
|
-
first task.
|
|
137
|
+
- [Getting started](docs/getting-started.md) — install and run a first task.
|
|
200
138
|
- [CLI reference](docs/cli-reference.md) — commands, flags, responses, and
|
|
201
139
|
exit codes.
|
|
202
|
-
- [Integration API](docs/api.md) — JSON responses
|
|
203
|
-
idempotent client behavior.
|
|
204
|
-
- [Provider guide](docs/providers.md) — supported profiles and compatibility
|
|
140
|
+
- [Integration API](docs/api.md) — JSON responses and idempotent client
|
|
205
141
|
behavior.
|
|
206
|
-
- [
|
|
207
|
-
- [
|
|
208
|
-
- [
|
|
142
|
+
- [Provider guide](docs/providers.md) — profiles and runtime compatibility.
|
|
143
|
+
- [Architecture](docs/architecture.md) — lifecycle and ownership boundaries.
|
|
144
|
+
- [System components](diagrams/components.html) — interactive Archify view.
|
|
145
|
+
- [Task lifecycle](diagrams/lead-interface.html) — interactive lifecycle view.
|
|
209
146
|
- [Security](docs/security.md) — isolation, policy, and result integrity.
|
|
210
147
|
- [Troubleshooting](docs/troubleshooting.md) — common setup and runtime
|
|
211
148
|
failures.
|
|
212
|
-
- [
|
|
213
|
-
|
|
214
|
-
- [
|
|
215
|
-
JSON and evidence handling guidance for agents.
|
|
149
|
+
- [Agent integration skill](skills/agent-integration/SKILL.md) — the
|
|
150
|
+
provider-neutral instructions installed into an agent harness.
|
|
151
|
+
- [Contributing](CONTRIBUTING.md) — development and review guidance.
|
|
216
152
|
|
|
217
153
|
## License
|
|
218
154
|
|
|
219
|
-
|
|
220
|
-
as proprietary and do not redistribute them without the project owner's
|
|
221
|
-
permission.
|
|
155
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delegation-layer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.3",
|
|
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": [
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"docs/assets/delegation-layer-hero.jpg",
|
|
10
26
|
"install.sh",
|
|
11
27
|
"skills/agent-integration/SKILL.md",
|
|
12
28
|
"scripts/install-agent-integration.mjs",
|
|
@@ -23,5 +39,5 @@
|
|
|
23
39
|
"bugs": {
|
|
24
40
|
"url": "https://github.com/hishamkaram/delegation-layer/issues"
|
|
25
41
|
},
|
|
26
|
-
"license": "
|
|
42
|
+
"license": "MIT"
|
|
27
43
|
}
|
|
@@ -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 { createInterface } from "node:readline/promises";
|
|
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.\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,140 @@ 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 parseHarnessSelection(value, detections) {
|
|
284
|
+
const tokens = value
|
|
285
|
+
.split(",")
|
|
286
|
+
.map((token) => token.trim().toLowerCase())
|
|
287
|
+
.filter(Boolean);
|
|
288
|
+
const names = tokens.map((token) => {
|
|
289
|
+
if (/^\d+$/.test(token)) {
|
|
290
|
+
const detection = detections[Number(token) - 1];
|
|
291
|
+
if (!detection) {
|
|
292
|
+
throw new Error(`harness choice ${token} is out of range`);
|
|
293
|
+
}
|
|
294
|
+
return detection.harness.name;
|
|
295
|
+
}
|
|
296
|
+
if (!harnessByName.has(token)) {
|
|
297
|
+
throw new Error(`unknown harness ${token}`);
|
|
298
|
+
}
|
|
299
|
+
return token;
|
|
300
|
+
});
|
|
301
|
+
return [...new Set(names)];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
|
|
305
|
+
const cwd = process.cwd();
|
|
306
|
+
const home = resolve(process.env.DELEGATION_LAYER_HOME ?? homedir());
|
|
307
|
+
const detections = await detectHarnesses({ cwd, home });
|
|
308
|
+
const defaults = defaultHarnessNames(detections);
|
|
309
|
+
const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
310
|
+
const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
|
|
311
|
+
|
|
312
|
+
if (!harnessNames) {
|
|
313
|
+
printDetections(detections);
|
|
314
|
+
}
|
|
315
|
+
if (!canPrompt && !yes && !harnessNames && !scope) {
|
|
316
|
+
console.log("No interactive terminal detected; using project scope and detected harnesses.");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (needsPrompt) {
|
|
320
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
321
|
+
try {
|
|
322
|
+
const selectedScope = (await readline.question("Install for [project/global] (project): ")).trim().toLowerCase();
|
|
323
|
+
scope = selectedScope || "project";
|
|
324
|
+
if (scope !== "project" && scope !== "global") {
|
|
325
|
+
throw new Error("scope must be project or global");
|
|
326
|
+
}
|
|
327
|
+
const defaultSelection = defaults
|
|
328
|
+
.map((name) => detections.findIndex(({ harness }) => harness.name === name) + 1)
|
|
329
|
+
.join(",");
|
|
330
|
+
const selection = await readline.question(`Choose harnesses [${defaultSelection}]: `);
|
|
331
|
+
harnessNames = selection.trim() ? parseHarnessSelection(selection, detections) : defaults;
|
|
332
|
+
} finally {
|
|
333
|
+
readline.close();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
cwd,
|
|
339
|
+
force,
|
|
340
|
+
harnessNames: harnessNames ?? defaults,
|
|
341
|
+
home,
|
|
342
|
+
scope: scope ?? "project",
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function targetFor(harness, scope, cwd, home) {
|
|
347
|
+
const root = scope === "global" ? home : cwd;
|
|
348
|
+
const relativePath = scope === "global" ? harness.globalPath : harness.projectPath;
|
|
349
|
+
return join(resolve(root, relativePath), "agent-integration");
|
|
350
|
+
}
|
|
351
|
+
|
|
67
352
|
async function install({ force, target }) {
|
|
68
353
|
const targetStat = await existingPath(target);
|
|
69
354
|
if (targetStat?.isSymbolicLink()) {
|
|
@@ -109,8 +394,24 @@ if (argumentsList[0] === "install-cli") {
|
|
|
109
394
|
console.error(`Error: ${error.message}`);
|
|
110
395
|
process.exitCode = 1;
|
|
111
396
|
}
|
|
397
|
+
} else if (argumentsList[0] === "install") {
|
|
398
|
+
const options = parseInstallArguments(argumentsList.slice(1));
|
|
399
|
+
if (options) {
|
|
400
|
+
try {
|
|
401
|
+
const selected = await chooseInstallOptions(options);
|
|
402
|
+
const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
|
|
403
|
+
const uniqueTargets = [...new Set(targets)];
|
|
404
|
+
console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
|
|
405
|
+
for (const target of uniqueTargets) {
|
|
406
|
+
await install({ force: selected.force, target });
|
|
407
|
+
}
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error(`Error: ${error.message}`);
|
|
410
|
+
process.exitCode = 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
112
413
|
} else {
|
|
113
|
-
const options =
|
|
414
|
+
const options = parseLegacyArguments(argumentsList);
|
|
114
415
|
if (options) {
|
|
115
416
|
try {
|
|
116
417
|
await install(options);
|