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 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, Hermes, or any command — as
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**. Your code and
10
- your git/`gh` credentials never leave your machine hilos only relays messages.
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 then posts the
23
- proposed diff as a card and pushes only after you Approve (nothing leaves your
24
- machine until then).
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? Use a config:
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
- ```jsonc
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", // safe default; see Permissions / autonomy. or "codex exec", "cursor-agent -p --output-format text --trust", "opencode run", "agy -p", any command
51
- "codingModel": "", // Codex tier ("most-capable" | "balanced" | "fastest"), resolved against this account's own model list; "" = the tool's default
52
- "chatCmd": "", // FAST command for chat replies + the plan-ack. Empty = derived from codingCmd's tool (codex daemons chat with codex, etc.); set to override
53
- "webSearch": true, // native public-web tools are on by default; false stops Hilos enablement/instructions
54
+ "codingCmd": "claude -p --permission-mode acceptEdits",
55
+ "codingModel": "",
56
+ "chatCmd": "",
57
+ "webSearch": true,
54
58
  "defaultBranch": "main",
55
- "gate": false, // default: open a PR directly. true = approve-before-push
56
- "heartbeatMs": 180000, // long runs post one "still working…" thread reply this often (0 = off, min 15s)
57
- "chatTimeoutMs": 90000, // cap a chat reply / plan-ack so a stalled model can't go silent
58
- "longPollMs": 20000, // ask the server to HOLD the mention poll and answer the moment work lands (0 = plain 5s polling; older servers fall back automatically)
59
- "catchupMs": 86400000 // how far back a restart replays mentions from the persisted cursor (default 24h; 0 = restart at "now", the pre-0866 behavior)
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 `chatCmd` is
65
- set a quick **plan** ("On it I'll do X, then open a PR") edits into it. On a
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
- ```jsonc
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 bundled browser bridge currently needs
117
- Node.js 24 or newer; the rest of the daemon keeps its existing Node.js support.
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. See the complete contract in
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 branches off `defaultBranch` (refuses a dirty tree), runs
151
- `codingCmd` with the task, and stages the result.
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
- ```jsonc
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
- ```jsonc
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
- `codingCmd` decides how much the coding agent can do on its own. Three levels,
281
- safest first:
282
-
283
- - **`opencode run` (runtime-gated).** When the connected hilos server advertises
284
- runtime permissions, the daemon runs OpenCode through an authenticated
285
- loopback server and becomes its sole permission responder. A tool ask pauses
286
- mechanically, posts a card in the run thread, and resumes only after a channel
287
- member chooses **Allow once**, an exact harness-suggested **Always** rule, or
288
- **Deny**. Missing transport, expiry, and cancellation all reject the tool
289
- call. `opencode run --auto` deliberately bypasses these cards and keeps
290
- OpenCode's dangerous auto-approve behavior.
291
- - **`--permission-mode acceptEdits` (default).** The agent edits files without
292
- prompting, but in headless `claude -p` a step that needs bash — run the tests,
293
- install a dep has no interactive prompt to grant, so the task can **stall**.
294
- Good when the work is edit-only; frustrating for anything that needs to run
295
- commands.
296
- - **`--dangerously-skip-permissions` (recommended for independent agents).** Full
297
- autonomy: the agent can run the tests, install deps, and finish hands-off.
298
- Caution: it can run **any** command in the repo you point it at — only use it
299
- on a repo and machine where that's acceptable. This is the option to pick if
300
- you want the agent to actually work on its own.
301
-
302
- ```jsonc
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. OpenCode is the first harness with the runtime-card
313
- bridge; Claude Code, Codex, Cursor, and other adapters still follow their own
314
- CLI permission modes until their native approval hooks join the same
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 alone still stream live
365
- steps, but MCP has no server-push channel that can wake an idle local client.
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 own stream is **not** part of that by default — the live
422
- card is an eight-step window and nothing keeps it after the run.
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>` · `--coding-cmd <cmd>` ·
444
- `--chat-cmd <cmd>` · `--once` · `--backfill` · `--no-gate` · `--help`
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`, `HILOS_ONCE=1`,
447
- `HILOS_BACKFILL=1`, `HILOS_UPLOAD_TRANSCRIPTS=1|0`.
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 checks
453
- the tag against the tree, parses every source file on Node 20, 22, and 24,
454
- and publishes to npm (without provenance while the repository is private:
455
- npm refuses a provenance bundle from a private source repository, 1182).
456
- `workflow_dispatch` runs the same pipeline as a dry run. Needs the
457
- `NPM_TOKEN` repository secret (1094).
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).
@@ -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, proposes a diff, pushes only after a human
4
- // approves in hilos. Your code + credentials never leave your machine.
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[++i];
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[++i];
46
- else if (a === "--channel") flags.channelId = argv[++i];
47
- else if (a === "--url") flags.url = argv[++i];
48
- else if (a === "--token") flags.token = argv[++i];
49
- else if (a === "--coding-cmd") flags.codingCmd = argv[++i];
50
- else if (a === "--coding-model") flags.codingModel = argv[++i];
51
- else if (a === "--chat-cmd") flags.chatCmd = argv[++i];
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[++i];
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
- return { cmd: positional[0] || "run", flags, positional };
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> connect using a link copied from hilos
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
- --no-gate propose only; don't wait for approval / push
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.1",
4
- "description": "Run your own coding agent (Claude Code / Codex / Cursor) as an autonomous teammate in a hilos channel. Picks up @mentions in channels and threads, makes the change, and opens a PR for review your code and credentials never leave your machine. (Approve-before-push is available via gate:true.)",
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
- "dependencies": {
37
- "agent-browser": "^0.35.0"
38
+ "optionalDependencies": {
39
+ "agent-browser": "^0.36.0"
38
40
  }
39
41
  }
@@ -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) {