hilos-agent 0.11.1 → 0.11.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 +111 -73
- package/bin/hilos-agent.mjs +87 -14
- package/package.json +6 -4
- package/src/acp-session.mjs +16 -1
- package/src/cli.mjs +34 -1
- package/src/codex-mcp-session.mjs +36 -0
- package/src/config.mjs +39 -5
- package/src/cursor-store.mjs +43 -8
- package/src/handler.mjs +279 -38
- package/src/iterate-claim-recovery.mjs +574 -0
- package/src/mcp.mjs +411 -34
- package/src/model-resolve.mjs +3 -1
- package/src/opencode-session.mjs +22 -2
- package/src/progress-emitter.mjs +1 -1
- package/src/queue.mjs +70 -3
- package/src/run.mjs +453 -76
- package/src/webmcp-bridge.mjs +77 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pablo Stanley
|
|
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,13 +1,16 @@
|
|
|
1
1
|
# hilos-agent
|
|
2
2
|
|
|
3
|
-
Run **your own** coding agent — Claude Code, Codex, Cursor, opencode,
|
|
3
|
+
Run **your own** coding agent — Claude Code, Codex, Cursor, opencode,
|
|
4
|
+
Antigravity, Hermes, or any command — as
|
|
4
5
|
an autonomous teammate inside a [hilos](https://hilos.sh) channel.
|
|
5
6
|
|
|
6
7
|
It connects to hilos over MCP, watches for `@mentions` of your agent in a
|
|
7
8
|
git-linked channel (including thread replies) and for **every message in a DM
|
|
8
9
|
with your agent — no tag needed there**, runs your coding agent in a
|
|
9
|
-
**local** checkout, and — by default — **opens a PR for review**.
|
|
10
|
-
your git
|
|
10
|
+
**local** checkout, and — by default — **opens a PR for review**. The checkout
|
|
11
|
+
and your coding-provider/git credentials stay on your machine. Changes go to the
|
|
12
|
+
configured Git remote; hilos receives bounded plans, progress, reports, and—when
|
|
13
|
+
the pre-push gate is enabled—the proposal diff.
|
|
11
14
|
|
|
12
15
|
```
|
|
13
16
|
hilos channel ──MCP/HTTPS──▶ hilos-agent (your laptop)
|
|
@@ -19,13 +22,13 @@ hilos channel ──MCP/HTTPS──▶ hilos-agent (your laptop)
|
|
|
19
22
|
Changes ▶ agent re-works with your note
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
Prefer **approve-before-push**? Set `"gate": true` — the agent
|
|
23
|
-
proposed diff as a card
|
|
24
|
-
|
|
25
|
+
Prefer **approve-before-push**? Set `"gate": true` — the agent posts a bounded
|
|
26
|
+
proposed diff to hilos as a card, then pushes the change to the Git remote only
|
|
27
|
+
after you Approve.
|
|
25
28
|
|
|
26
29
|
## Quick start
|
|
27
30
|
|
|
28
|
-
In hilos: open your agent's profile → **Connect** → **Run in channel**. Copy the
|
|
31
|
+
In hilos: open your agent's profile → **Connect agent** → **Run in channel**. Copy the
|
|
29
32
|
terminal command and run it from inside your repo's folder. It waits at a hidden
|
|
30
33
|
prompt; copy the private join code from Hilos and paste it there. The daemon
|
|
31
34
|
matches the repo by its git remote, so no config is needed, and the reusable
|
|
@@ -39,30 +42,42 @@ npx hilos-agent@latest --join-stdin # then paste the private join code when ask
|
|
|
39
42
|
Previously copied `--join <blob>` commands remain compatible. New commands use
|
|
40
43
|
stdin because the blob contains the agent token and should not live in argv.
|
|
41
44
|
|
|
42
|
-
Running from elsewhere, or want to map several repos explicitly?
|
|
45
|
+
Running from elsewhere, or want to map several repos explicitly? Save this as
|
|
46
|
+
`~/.hilos/agent.json` (or `./hilos-agent.json`). The file is strict JSON, so it
|
|
47
|
+
cannot contain comments:
|
|
43
48
|
|
|
44
|
-
```
|
|
45
|
-
// ~/.hilos/agent.json (or ./hilos-agent.json)
|
|
49
|
+
```json
|
|
46
50
|
{
|
|
47
51
|
"url": "https://hilos.sh/api/mcp",
|
|
48
52
|
"token": "mgo_…",
|
|
49
53
|
"repos": { "your-org/your-repo": "/Users/you/code/your-repo" },
|
|
50
|
-
"codingCmd": "claude -p --permission-mode acceptEdits",
|
|
51
|
-
"codingModel": "",
|
|
52
|
-
"chatCmd": "",
|
|
53
|
-
"webSearch": true,
|
|
54
|
+
"codingCmd": "claude -p --permission-mode acceptEdits",
|
|
55
|
+
"codingModel": "",
|
|
56
|
+
"chatCmd": "",
|
|
57
|
+
"webSearch": true,
|
|
54
58
|
"defaultBranch": "main",
|
|
55
|
-
"gate": false,
|
|
56
|
-
"heartbeatMs": 180000,
|
|
57
|
-
"chatTimeoutMs": 90000,
|
|
58
|
-
"longPollMs": 20000,
|
|
59
|
-
"catchupMs": 86400000
|
|
59
|
+
"gate": false,
|
|
60
|
+
"heartbeatMs": 180000,
|
|
61
|
+
"chatTimeoutMs": 90000,
|
|
62
|
+
"longPollMs": 20000,
|
|
63
|
+
"catchupMs": 86400000
|
|
60
64
|
}
|
|
61
65
|
```
|
|
62
66
|
|
|
67
|
+
`codingCmd` above is the safe default; it can also be `codex exec`,
|
|
68
|
+
`cursor-agent -p --output-format text --trust`, `opencode run`, `agy -p`, or a
|
|
69
|
+
custom command. `codingModel` accepts the Codex tiers `most-capable`, `balanced`,
|
|
70
|
+
or `fastest`; empty uses the tool's default. Empty `chatCmd` derives a fast chat
|
|
71
|
+
command from `codingCmd`. `webSearch: false` disables hilos's web-search
|
|
72
|
+
enablement, and `gate: true` switches to approve-before-push. `heartbeatMs: 0`
|
|
73
|
+
disables the heartbeat (minimum nonzero value 15 seconds); `longPollMs: 0` uses
|
|
74
|
+
plain five-second polling; `catchupMs: 0` restarts at now instead of replaying
|
|
75
|
+
the default 24-hour window.
|
|
76
|
+
|
|
63
77
|
**Staying responsive.** Every code task posts an **instant acknowledgement**
|
|
64
|
-
(under a second), and — if your server exposes `edit_message` and a
|
|
65
|
-
|
|
78
|
+
(under a second), and — if your server exposes `edit_message` and a fast chat
|
|
79
|
+
command can be resolved (explicit `chatCmd` or one derived from `codingCmd`) —
|
|
80
|
+
a quick **plan** ("On it — I'll do X, then open a PR") edits into it. On a
|
|
66
81
|
run longer than `heartbeatMs` (default 3 min; env `HILOS_HEARTBEAT_MS`, `0`
|
|
67
82
|
disables, clamped to ≥15s) the agent posts **one progress reply** in the thread
|
|
68
83
|
then edits it in place with elapsed time + the CLI's latest line — so the channel
|
|
@@ -98,7 +113,7 @@ The local daemon can give its coding and chat agents a narrow bridge to
|
|
|
98
113
|
third-party WebMCP sites. It is off until you name exact origins and exact read
|
|
99
114
|
tools in the machine's config:
|
|
100
115
|
|
|
101
|
-
```
|
|
116
|
+
```json
|
|
102
117
|
{
|
|
103
118
|
"webMcp": {
|
|
104
119
|
"origins": {
|
|
@@ -113,8 +128,11 @@ tools in the machine's config:
|
|
|
113
128
|
The site does not authorize itself: `readOnlyHint` is informational, while this
|
|
114
129
|
person-owned list decides what may run. Site descriptions are omitted, schema
|
|
115
130
|
prose is stripped, results are bounded and labeled untrusted, and cookies stay
|
|
116
|
-
inside a separate browser profile. The
|
|
117
|
-
Node.js
|
|
131
|
+
inside a separate browser profile. The optional browser bridge needs Node.js 24
|
|
132
|
+
or newer. On Node.js 20 or 22 WebMCP stays unavailable, while the rest of the
|
|
133
|
+
daemon keeps working regardless of whether that optional package was omitted by
|
|
134
|
+
the installer. Run `hilos-agent webmcp doctor` after upgrading Node.js and
|
|
135
|
+
reinstalling the package.
|
|
118
136
|
|
|
119
137
|
```sh
|
|
120
138
|
hilos-agent webmcp doctor
|
|
@@ -128,7 +146,9 @@ hilos-agent webmcp close
|
|
|
128
146
|
The daemon adds this capability and its citation rules to agent prompts only
|
|
129
147
|
when the config is valid. Unlisted tools — including writes — are refused with
|
|
130
148
|
`human_approval_required`; there is no approval flag the agent can set. WebMCP
|
|
131
|
-
can never approve or merge hilos work.
|
|
149
|
+
can never approve or merge hilos work. This wrapper is a policy boundary for
|
|
150
|
+
the bridge, not a sandbox around the coding CLI, which still has the machine
|
|
151
|
+
access its operator granted. See the complete contract in
|
|
132
152
|
[WebMCP in hilos](https://hilos.sh/docs/webmcp).
|
|
133
153
|
|
|
134
154
|
## How it works
|
|
@@ -147,8 +167,10 @@ can never approve or merge hilos work. See the complete contract in
|
|
|
147
167
|
it", "approved", or "go for it" after a request, in any language → a code run.
|
|
148
168
|
- **Repo resolution** — the channel's linked repo is mapped to a local path via
|
|
149
169
|
`repos`. No mapping → the agent says so and stops.
|
|
150
|
-
- **Run** — it
|
|
151
|
-
`codingCmd` with the task,
|
|
170
|
+
- **Run** — it temporarily stashes a dirty checkout, branches off
|
|
171
|
+
`defaultBranch`, runs `codingCmd` with the task, stages the result, and attempts
|
|
172
|
+
to restore the person's pre-existing work on every exit path. If `stash pop`
|
|
173
|
+
conflicts, the stash stays intact and the room gets the recovery command.
|
|
152
174
|
- **Continue the thread's PR** — when the mention lands in a thread hilos says is
|
|
153
175
|
about a pull request, the daemon works on *that* PR instead of opening a second
|
|
154
176
|
one: it fetches the PR's head branch, commits there, and the same PR updates.
|
|
@@ -187,8 +209,7 @@ to a plain **local folder** and the agent works in it **directly** — it edits
|
|
|
187
209
|
files in place (no branch, no commit, no PR), then posts a report of what changed.
|
|
188
210
|
It's your own machine, so this is the same trust as running the CLI yourself.
|
|
189
211
|
|
|
190
|
-
```
|
|
191
|
-
// hilos-agent.json
|
|
212
|
+
```json
|
|
192
213
|
{
|
|
193
214
|
"folders": { "<channelId>": "/Users/you/notes-site" }
|
|
194
215
|
}
|
|
@@ -222,7 +243,7 @@ It's your own machine, so this is the same trust as running the CLI yourself.
|
|
|
222
243
|
|
|
223
244
|
Add an optional per-channel deploy target next to `folders`:
|
|
224
245
|
|
|
225
|
-
```
|
|
246
|
+
```json
|
|
226
247
|
{
|
|
227
248
|
"folders": { "<channelId>": "/Users/you/notes-site" },
|
|
228
249
|
"deploy": { "<channelId>": { "provider": "vercel", "prod": false } }
|
|
@@ -277,30 +298,29 @@ touching the token.
|
|
|
277
298
|
|
|
278
299
|
## Permissions / autonomy
|
|
279
300
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"codingCmd": "claude -p --dangerously-skip-permissions"
|
|
301
|
+
There are two separate gates:
|
|
302
|
+
|
|
303
|
+
- **Room runtime permissions.** When the workspace has granted runtime
|
|
304
|
+
permissions, current daemons put Claude Code, Codex, Cursor, and OpenCode
|
|
305
|
+
behind the same server-authorized card flow. A tool ask pauses mechanically
|
|
306
|
+
and resumes only after an eligible channel member chooses **Allow once**, an
|
|
307
|
+
exact harness-suggested **Always** rule, or **Deny**. Cursor and OpenCode use
|
|
308
|
+
ACP; Claude Code uses its permission-prompt tool; Codex uses `codex
|
|
309
|
+
mcp-server` because `codex exec` has no approval transport. Once a relay is
|
|
310
|
+
active, authority loss, expiry, cancellation, and transport failure all fail
|
|
311
|
+
closed. A positively unavailable old-CLI transport is the compatibility
|
|
312
|
+
exception: the daemon warns the room, then starts the ungated fallback.
|
|
313
|
+
Explicit vendor bypass modes such as `opencode run --auto` or Claude Code's
|
|
314
|
+
`--dangerously-skip-permissions` deliberately opt out of the cards.
|
|
315
|
+
- **The CLI's own mode.** Without a workspace runtime grant, `codingCmd` remains
|
|
316
|
+
authoritative. The default Claude Code command uses
|
|
317
|
+
`--permission-mode acceptEdits`: it may edit without prompting, but a headless
|
|
318
|
+
command that needs additional approval can stall. For a fully independent
|
|
319
|
+
local agent, `--dangerously-skip-permissions` can run any command in the repo
|
|
320
|
+
you point it at, so use it only where that trust is acceptable.
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{ "codingCmd": "claude -p --dangerously-skip-permissions" }
|
|
304
324
|
```
|
|
305
325
|
|
|
306
326
|
- **Approve-before-push (`gate:true`), most cautious.** Independent of the two
|
|
@@ -309,10 +329,9 @@ safest first:
|
|
|
309
329
|
|
|
310
330
|
The default stays `acceptEdits`. Reach for `--dangerously-skip-permissions` when
|
|
311
331
|
you want a truly hands-off teammate, and keep `gate:true` if you'd rather review
|
|
312
|
-
before anything is pushed.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
vendor-neutral hilos substrate.
|
|
332
|
+
before anything is pushed. Runtime cards and the push gate are independent: one
|
|
333
|
+
governs tools during the run, the other governs when the resulting code leaves
|
|
334
|
+
the machine.
|
|
316
335
|
|
|
317
336
|
## Hooks — keep a raw Codex, Claude Code, or Cursor session in the room
|
|
318
337
|
|
|
@@ -361,8 +380,9 @@ npx hilos-agent@latest hooks print
|
|
|
361
380
|
live card. The normal one-line `--join` flow does not need that file for
|
|
362
381
|
replies: its token stays in daemon memory, and Hilos signs each session
|
|
363
382
|
binding so the daemon can verify it before resuming local code.
|
|
364
|
-
- Keep `hilos-agent` running to pick replies up. Hooks
|
|
365
|
-
|
|
383
|
+
- Keep `hilos-agent` running to pick replies up. Hooks only emit lifecycle
|
|
384
|
+
events; the daemon is the consumer that waits on the server's wake/long-poll
|
|
385
|
+
path and resumes the local session.
|
|
366
386
|
|
|
367
387
|
**What you get:**
|
|
368
388
|
- Team members see "Editing lib/x.ts" or "Running pnpm test" on the agent's live
|
|
@@ -418,8 +438,9 @@ An older server simply leaves it off.
|
|
|
418
438
|
### Run transcripts are opt-in
|
|
419
439
|
|
|
420
440
|
The room gets what a teammate needs to see: a plan, live progress, a report, a
|
|
421
|
-
PR. The coding tool's
|
|
422
|
-
|
|
441
|
+
PR. The coding tool's raw stream is **not** part of that by default. Current
|
|
442
|
+
servers do keep a bounded, redacted set of structured activity facts through
|
|
443
|
+
the runtime-neutral event protocol described above; that is not a transcript.
|
|
423
444
|
|
|
424
445
|
Set `"uploadTranscripts": true` and the daemon, after posting its report, sends
|
|
425
446
|
the tail of that run's stream to hilos, where it becomes the same transcript a
|
|
@@ -440,18 +461,35 @@ restart — and **deleting** the key counts as off, not as "leave it as it was".
|
|
|
440
461
|
|
|
441
462
|
## Flags
|
|
442
463
|
|
|
443
|
-
`--join <blob>` · `--channel <id>` · `--config <path>` ·
|
|
444
|
-
`--
|
|
464
|
+
`--join <blob>` · `--join-stdin` · `--channel <id>` · `--config <path>` ·
|
|
465
|
+
`--url <endpoint>` · `--token <token>` ·
|
|
466
|
+
`--coding-cmd <cmd>` · `--coding-model <tier>` · `--chat-cmd <cmd>` ·
|
|
467
|
+
`--web-search` · `--no-web-search` · `--once` · `--backfill` · `--gate` ·
|
|
468
|
+
`--no-gate` · `--no-reply-bridge` · `--version` · `--help`. Hook installation
|
|
469
|
+
also accepts `--global` or one of `--claude`, `--codex`, and `--cursor`.
|
|
470
|
+
|
|
471
|
+
`--token` remains only for compatibility with older scripts. Command-line
|
|
472
|
+
arguments can be visible to other processes on the machine, so use
|
|
473
|
+
`--join-stdin`, `HILOS_TOKEN`, or the private config file for credentials.
|
|
474
|
+
Likewise, prefer `--join-stdin` over the legacy `--join <blob>` form for a new
|
|
475
|
+
connection.
|
|
476
|
+
|
|
477
|
+
See `hilos-agent --help` for the `init`, `webmcp`, `web doctor`, and `hooks`
|
|
478
|
+
subcommands.
|
|
445
479
|
|
|
446
|
-
Env: `HILOS_TOKEN`, `HILOS_URL`, `HILOS_CHANNEL`, `CODING_CMD`,
|
|
447
|
-
`
|
|
480
|
+
Env: `HILOS_TOKEN`, `HILOS_URL`, `HILOS_CHANNEL`, `CODING_CMD`,
|
|
481
|
+
`HILOS_CODING_MODEL`, `HILOS_CHAT_CMD`, `HILOS_WEB_SEARCH=1|0`,
|
|
482
|
+
`HILOS_ACP=1|0`, `HILOS_HEARTBEAT_MS`, `HILOS_PROGRESS_MS`,
|
|
483
|
+
`HILOS_CHAT_TIMEOUT_MS`, `HILOS_REPLY_BRIDGE=1|0`, `HILOS_ONCE=1`,
|
|
484
|
+
`HILOS_BACKFILL=1`, and `HILOS_UPLOAD_TRANSCRIPTS=1|0`.
|
|
448
485
|
|
|
449
486
|
## Releasing
|
|
450
487
|
|
|
451
|
-
Bump `version` in `package.json`, merge, then tag that commit
|
|
452
|
-
`hilos-agent-v<version>` and push the tag. The `npm-publish` workflow
|
|
453
|
-
|
|
454
|
-
and
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
`NPM_TOKEN` repository secret
|
|
488
|
+
Bump `version` in `package.json`, merge, wait for main CI, then tag that commit
|
|
489
|
+
`hilos-agent-v<version>` and push the tag. The `npm-publish` workflow requires
|
|
490
|
+
that exact version, a commit contained in `main`, and a successful `verify`
|
|
491
|
+
check. It packs and clean-installs the artifact on Node.js 20, 22, and 24 before
|
|
492
|
+
publishing, then installs the registry copy and checks its version and help.
|
|
493
|
+
A manual workflow dispatch is always a dry run and cannot publish. The current
|
|
494
|
+
release path needs the `NPM_TOKEN` repository secret until npm trusted
|
|
495
|
+
publishing is configured (1094, 1211).
|
package/bin/hilos-agent.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// hilos-agent — run your coding agent as an autonomous teammate in a hilos
|
|
3
|
-
// channel. Picks up @mentions
|
|
4
|
-
//
|
|
3
|
+
// channel. Picks up @mentions and opens a PR by default; an optional pre-push
|
|
4
|
+
// gate waits for a person's approval. The checkout and credentials stay local;
|
|
5
|
+
// changes go to the configured Git remote and bounded run state goes to hilos.
|
|
5
6
|
//
|
|
6
7
|
// Usage:
|
|
7
8
|
// hilos-agent --join <blob> connect with a copy-paste link from hilos
|
|
@@ -35,43 +36,107 @@ function packageVersion() {
|
|
|
35
36
|
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function requiredOptionValue(argv, index, option) {
|
|
40
|
+
const value = argv[index + 1];
|
|
41
|
+
if (value === undefined || value.startsWith("-")) {
|
|
42
|
+
throw new Error(`Option ${option} requires a value.`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateCommand(cmd, positional, { skipShape = false } = {}) {
|
|
48
|
+
const commands = new Set(["run", "init", "hook", "hooks", "webmcp", "web", "help", "version"]);
|
|
49
|
+
if (!commands.has(cmd)) {
|
|
50
|
+
throw new Error(`Unknown command: ${cmd}. Try \`hilos-agent --help\`.`);
|
|
51
|
+
}
|
|
52
|
+
if (skipShape) return;
|
|
53
|
+
|
|
54
|
+
if (["run", "init", "hook", "help", "version"].includes(cmd) && positional.length > 1) {
|
|
55
|
+
throw new Error(`Unexpected argument for ${cmd}: ${positional[1]}. Try \`hilos-agent --help\`.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (cmd === "hooks") {
|
|
59
|
+
const subcommand = positional[1];
|
|
60
|
+
if (!subcommand) throw new Error("A hooks command is required: install or print.");
|
|
61
|
+
if (!new Set(["install", "print"]).has(subcommand)) {
|
|
62
|
+
throw new Error(`Unknown hooks command: ${subcommand}. Use install or print.`);
|
|
63
|
+
}
|
|
64
|
+
if (positional.length > 2) {
|
|
65
|
+
throw new Error(`Unexpected argument for hooks ${subcommand}: ${positional[2]}.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (cmd === "web") {
|
|
70
|
+
const subcommand = positional[1] || "doctor";
|
|
71
|
+
if (subcommand !== "doctor") {
|
|
72
|
+
throw new Error(`Unknown web command: ${subcommand}. Try \`hilos-agent web doctor\`.`);
|
|
73
|
+
}
|
|
74
|
+
if (positional.length > 2) {
|
|
75
|
+
throw new Error(`Unexpected argument for web doctor: ${positional[2]}.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cmd === "webmcp") {
|
|
80
|
+
const operation = positional[1] || "doctor";
|
|
81
|
+
const allowed = new Set(["doctor", "login", "open", "tools", "call", "close"]);
|
|
82
|
+
if (!allowed.has(operation)) {
|
|
83
|
+
throw new Error(`Unknown webmcp command: ${operation}. Use doctor, login, open, tools, call, or close.`);
|
|
84
|
+
}
|
|
85
|
+
const argumentCount = positional.length - 2;
|
|
86
|
+
const validCount = operation === "login" || operation === "open"
|
|
87
|
+
? argumentCount === 1
|
|
88
|
+
: operation === "call"
|
|
89
|
+
? argumentCount === 1 || argumentCount === 2
|
|
90
|
+
: argumentCount === 0;
|
|
91
|
+
if (!validCount) {
|
|
92
|
+
throw new Error(`Invalid arguments for webmcp ${operation}. Try \`hilos-agent --help\`.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
38
97
|
function parseArgs(argv) {
|
|
39
98
|
const flags = {};
|
|
40
99
|
const positional = [];
|
|
41
100
|
for (let i = 0; i < argv.length; i++) {
|
|
42
101
|
const a = argv[i];
|
|
43
|
-
if (a === "--join") flags.join = argv
|
|
102
|
+
if (a === "--join") flags.join = requiredOptionValue(argv, i++, a);
|
|
44
103
|
else if (a === "--join-stdin") flags.joinStdin = true;
|
|
45
|
-
else if (a === "--config") flags.config = argv
|
|
46
|
-
else if (a === "--channel") flags.channelId = argv
|
|
47
|
-
else if (a === "--url") flags.url = argv
|
|
48
|
-
else if (a === "--token") flags.token = argv
|
|
49
|
-
else if (a === "--coding-cmd") flags.codingCmd = argv
|
|
50
|
-
else if (a === "--coding-model") flags.codingModel = argv
|
|
51
|
-
else if (a === "--chat-cmd") flags.chatCmd = argv
|
|
104
|
+
else if (a === "--config") flags.config = requiredOptionValue(argv, i++, a);
|
|
105
|
+
else if (a === "--channel") flags.channelId = requiredOptionValue(argv, i++, a);
|
|
106
|
+
else if (a === "--url") flags.url = requiredOptionValue(argv, i++, a);
|
|
107
|
+
else if (a === "--token") flags.token = requiredOptionValue(argv, i++, a);
|
|
108
|
+
else if (a === "--coding-cmd") flags.codingCmd = requiredOptionValue(argv, i++, a);
|
|
109
|
+
else if (a === "--coding-model") flags.codingModel = requiredOptionValue(argv, i++, a);
|
|
110
|
+
else if (a === "--chat-cmd") flags.chatCmd = requiredOptionValue(argv, i++, a);
|
|
52
111
|
else if (a === "--no-web-search") flags.webSearch = false;
|
|
53
112
|
else if (a === "--web-search") flags.webSearch = true;
|
|
54
113
|
else if (a === "--once") flags.once = true;
|
|
55
114
|
else if (a === "--backfill") flags.backfill = true;
|
|
115
|
+
else if (a === "--gate") flags.gate = true;
|
|
56
116
|
else if (a === "--no-gate") flags.gate = false;
|
|
57
117
|
else if (a === "--no-reply-bridge") flags.replyBridge = false;
|
|
58
118
|
else if (a === "--global") flags.global = true;
|
|
59
119
|
else if (a === "--claude") flags.hookClient = "claude";
|
|
60
120
|
else if (a === "--codex") flags.hookClient = "codex";
|
|
61
121
|
else if (a === "--cursor") flags.hookClient = "cursor";
|
|
62
|
-
else if (a === "--vendor") flags.vendor = argv
|
|
122
|
+
else if (a === "--vendor") flags.vendor = requiredOptionValue(argv, i++, a);
|
|
63
123
|
else if (a === "--scope-managed") flags.scopeManaged = true;
|
|
124
|
+
// Marker embedded in self-contained hook commands. It is intentionally
|
|
125
|
+
// internal: hook.mjs uses it to recognize and replace managed installs.
|
|
64
126
|
else if (a === "--managed-runtime") flags.managedRuntime = true;
|
|
65
127
|
else if (a === "-h" || a === "--help") flags.help = true;
|
|
66
128
|
else if (a === "-v" || a === "--version") flags.version = true;
|
|
129
|
+
else if (a.startsWith("-")) throw new Error(`Unknown option: ${a}. Try \`hilos-agent --help\`.`);
|
|
67
130
|
else positional.push(a);
|
|
68
131
|
}
|
|
69
|
-
|
|
132
|
+
const cmd = positional[0] || "run";
|
|
133
|
+
validateCommand(cmd, positional, { skipShape: flags.help || flags.version });
|
|
134
|
+
return { cmd, flags, positional };
|
|
70
135
|
}
|
|
71
136
|
|
|
72
137
|
const HELP = `hilos-agent — your coding agent as a teammate in hilos
|
|
73
138
|
|
|
74
|
-
hilos-agent --join <blob>
|
|
139
|
+
hilos-agent --join <blob> legacy argv-compatible connect link
|
|
75
140
|
hilos-agent --join-stdin paste the private link at a no-echo prompt
|
|
76
141
|
hilos-agent init write a starter config to ~/.hilos/agent.json
|
|
77
142
|
hilos-agent webmcp doctor verify the local WebMCP browser bridge
|
|
@@ -82,16 +147,21 @@ const HELP = `hilos-agent — your coding agent as a teammate in hilos
|
|
|
82
147
|
hilos-agent webmcp close close the isolated browser session
|
|
83
148
|
hilos-agent web doctor report this CLI's native public-web capability
|
|
84
149
|
hilos-agent run the daemon (watch @mentions, propose diffs)
|
|
150
|
+
hilos-agent run same as above, explicit
|
|
85
151
|
hilos-agent hooks install stream this repo's Codex, Claude, and Cursor
|
|
86
152
|
sessions to hilos and continue replies in the same
|
|
87
153
|
local session. Installs all three hook formats;
|
|
88
154
|
use --codex, --claude, or --cursor to choose, and
|
|
89
155
|
--global for every repo. HILOS_HOOKS=off pauses
|
|
90
156
|
streaming; HILOS_REPLY_BRIDGE=off pauses pickup.
|
|
157
|
+
hilos-agent hooks print preview the hook configuration without writing
|
|
91
158
|
|
|
92
159
|
Options:
|
|
93
160
|
--channel <id> watch only one channel (per-channel override)
|
|
94
161
|
--config <path> use a specific config file
|
|
162
|
+
--url <endpoint> override the MCP endpoint (or use HILOS_URL/config)
|
|
163
|
+
--token <token> legacy token override; argv may be visible to other local
|
|
164
|
+
processes. Prefer --join-stdin or HILOS_TOKEN.
|
|
95
165
|
--coding-cmd <cmd> the coding agent to run — claude -p, codex exec,
|
|
96
166
|
cursor-agent -p --trust, opencode run, agy -p, hermes -z,
|
|
97
167
|
or any command
|
|
@@ -104,10 +174,12 @@ Options:
|
|
|
104
174
|
--chat-cmd <cmd> fast command for chat replies + the plan-ack (default:
|
|
105
175
|
derived from the coding command, so a Codex or Cursor
|
|
106
176
|
daemon chats with its own tool)
|
|
177
|
+
--web-search allow hilos to request native public web tools (default)
|
|
107
178
|
--no-web-search stop hilos from enabling/requesting native public web
|
|
108
179
|
--once one poll then exit (cron-friendly)
|
|
109
180
|
--backfill also act on mentions that predate startup
|
|
110
|
-
--
|
|
181
|
+
--gate wait for approval in hilos before pushing a branch
|
|
182
|
+
--no-gate open a PR directly without pre-push approval (default)
|
|
111
183
|
--no-reply-bridge don't resume local sessions from replies in bound threads
|
|
112
184
|
-v, --version print the installed version
|
|
113
185
|
-h, --help this help
|
|
@@ -158,6 +230,7 @@ async function main() {
|
|
|
158
230
|
const starter = { ...(joinPayload || {}) };
|
|
159
231
|
if (flags.codingCmd) starter.codingCmd = flags.codingCmd;
|
|
160
232
|
if (flags.codingModel) starter.codingModel = flags.codingModel;
|
|
233
|
+
if (flags.gate !== undefined) starter.gate = flags.gate;
|
|
161
234
|
const path = writeStarterConfig(joinPayload ? GLOBAL_CONFIG : flags.config, starter);
|
|
162
235
|
console.log(`Wrote ${path}.`);
|
|
163
236
|
console.log(joinPayload ? "Token + endpoint set from your link." : "Fill in token + repos, then run `hilos-agent`.");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.11.
|
|
4
|
-
"description": "Run your own coding agent (Claude Code
|
|
3
|
+
"version": "0.11.3",
|
|
4
|
+
"description": "Run your own coding agent (Claude Code, Codex, Cursor, OpenCode, Hermes, or any command) as a teammate in a hilos room. The checkout and credentials stay local; changes go to your configured Git remote as a PR for human review, and bounded progress and reports go to hilos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hilos-agent": "bin/hilos-agent.mjs"
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
"claude-code",
|
|
31
31
|
"codex",
|
|
32
32
|
"cursor",
|
|
33
|
+
"opencode",
|
|
34
|
+
"hermes-agent",
|
|
33
35
|
"coding-agent"
|
|
34
36
|
],
|
|
35
37
|
"license": "MIT",
|
|
36
|
-
"
|
|
37
|
-
"agent-browser": "^0.
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"agent-browser": "^0.36.0"
|
|
38
40
|
}
|
|
39
41
|
}
|
package/src/acp-session.mjs
CHANGED
|
@@ -191,6 +191,7 @@ export function createNdjsonParser() {
|
|
|
191
191
|
* requestPermission?: (request: object, context: object) => Promise<unknown>,
|
|
192
192
|
* getPermissionDecision?: (handle: unknown, context: object) => Promise<unknown>,
|
|
193
193
|
* mcpServers?: object[],
|
|
194
|
+
* beforeSpawn?: () => boolean | Promise<boolean>,
|
|
194
195
|
* spawnImpl?: (cmd: string, args: string[], options: object) => import("node:child_process").ChildProcess,
|
|
195
196
|
* setTimer?: typeof setTimeout,
|
|
196
197
|
* clearTimer?: typeof clearTimeout,
|
|
@@ -217,6 +218,7 @@ export async function runAcpSession({
|
|
|
217
218
|
/** Session to continue (0778); null starts a fresh one. */
|
|
218
219
|
resumeSessionId = null,
|
|
219
220
|
mcpServers = [],
|
|
221
|
+
beforeSpawn,
|
|
220
222
|
spawnImpl = spawn,
|
|
221
223
|
setTimer = setTimeout,
|
|
222
224
|
clearTimer = clearTimeout,
|
|
@@ -264,6 +266,7 @@ export async function runAcpSession({
|
|
|
264
266
|
let currentMessageId = null;
|
|
265
267
|
let messageBuffer = "";
|
|
266
268
|
const eventMapper = createAcpEventMapper();
|
|
269
|
+
let authorityLost = false;
|
|
267
270
|
|
|
268
271
|
const emitOutput = (text) => {
|
|
269
272
|
try {
|
|
@@ -411,6 +414,17 @@ export async function runAcpSession({
|
|
|
411
414
|
}
|
|
412
415
|
|
|
413
416
|
try {
|
|
417
|
+
if (typeof beforeSpawn === "function") {
|
|
418
|
+
try {
|
|
419
|
+
if ((await beforeSpawn()) === false) {
|
|
420
|
+
authorityLost = true;
|
|
421
|
+
throw new Error("execution authority lost before ACP spawn");
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
authorityLost = true;
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
414
428
|
child = spawnImpl(cmd, acpArgs, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
415
429
|
const spawned = new Promise((resolve, reject) => {
|
|
416
430
|
child.once("spawn", resolve);
|
|
@@ -508,13 +522,14 @@ export async function runAcpSession({
|
|
|
508
522
|
};
|
|
509
523
|
} catch (error) {
|
|
510
524
|
flushMessage();
|
|
511
|
-
const aborted = abortKind === "cancelled";
|
|
525
|
+
const aborted = abortKind === "cancelled" || authorityLost;
|
|
512
526
|
const timedOut = abortKind === "timeout";
|
|
513
527
|
return {
|
|
514
528
|
status: null,
|
|
515
529
|
stdout: stdout.trimEnd(),
|
|
516
530
|
stderr: stderr.trimEnd(),
|
|
517
531
|
...(aborted ? { aborted: true } : {}),
|
|
532
|
+
...(authorityLost ? { authorityLost: true } : {}),
|
|
518
533
|
...(sessionId ? { sessionId } : {}),
|
|
519
534
|
error:
|
|
520
535
|
error instanceof Error && !timedOut
|
package/src/cli.mjs
CHANGED
|
@@ -179,6 +179,9 @@ const MAX_CAPTURE_BYTES = 50 * 1024 * 1024;
|
|
|
179
179
|
* BEFORE the ungated compat retry is spawned when a CLI rejects the 0777
|
|
180
180
|
* permission flags (0785), so the caller can warn its room while the run can
|
|
181
181
|
* still be stopped. A throw here never fails the run.
|
|
182
|
+
* @property {() => (boolean | Promise<boolean>)} [beforeSpawn] - fail-closed
|
|
183
|
+
* authority check, awaited immediately before each child-process attempt.
|
|
184
|
+
* Returning false or throwing prevents that spawn and marks the run aborted.
|
|
182
185
|
*/
|
|
183
186
|
|
|
184
187
|
/**
|
|
@@ -191,7 +194,7 @@ const MAX_CAPTURE_BYTES = 50 * 1024 * 1024;
|
|
|
191
194
|
*
|
|
192
195
|
* @param {RunCliOptions} opts
|
|
193
196
|
*/
|
|
194
|
-
function runCliOnce(opts) {
|
|
197
|
+
async function runCliOnce(opts) {
|
|
195
198
|
const {
|
|
196
199
|
cmd,
|
|
197
200
|
args = [],
|
|
@@ -204,7 +207,37 @@ function runCliOnce(opts) {
|
|
|
204
207
|
signal,
|
|
205
208
|
onData,
|
|
206
209
|
env,
|
|
210
|
+
beforeSpawn,
|
|
207
211
|
} = opts || {};
|
|
212
|
+
// This guard belongs at the process boundary, after all potentially-slow
|
|
213
|
+
// caller setup and once per compatibility retry. Checking in runCli's caller
|
|
214
|
+
// leaves a race before the first child and lets its internal retries escape.
|
|
215
|
+
if (signal?.aborted) {
|
|
216
|
+
return { status: null, stdout: "", stderr: "", aborted: true, error: new Error("cancelled") };
|
|
217
|
+
}
|
|
218
|
+
if (typeof beforeSpawn === "function") {
|
|
219
|
+
try {
|
|
220
|
+
if ((await beforeSpawn()) === false) {
|
|
221
|
+
return {
|
|
222
|
+
status: null,
|
|
223
|
+
stdout: "",
|
|
224
|
+
stderr: "",
|
|
225
|
+
aborted: true,
|
|
226
|
+
authorityLost: true,
|
|
227
|
+
error: new Error("execution authority lost before spawn"),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return {
|
|
232
|
+
status: null,
|
|
233
|
+
stdout: "",
|
|
234
|
+
stderr: "",
|
|
235
|
+
aborted: true,
|
|
236
|
+
authorityLost: true,
|
|
237
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
208
241
|
return new Promise((resolve) => {
|
|
209
242
|
// Already cancelled before we even start.
|
|
210
243
|
if (signal?.aborted) {
|