hilos-agent 0.11.10 → 0.11.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hilos-agent",
3
- "version": "0.11.10",
3
+ "version": "0.11.12",
4
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": {
package/src/hook.mjs CHANGED
@@ -26,6 +26,7 @@ import { join, dirname } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
27
  import { sanitizeText, webTarget } from "./agent-events.mjs";
28
28
  import { resolveConfig } from "./config.mjs";
29
+ import { normalizeSeatRepository } from "./seat-repository.mjs";
29
30
 
30
31
  export const HOOK_STATE_DIR = join(homedir(), ".hilos", "hook-state");
31
32
  export const CODEX_HOOK_SCOPE_FILE = join(homedir(), ".hilos", "codex-hook-scope.json");
@@ -376,13 +377,15 @@ const PERSONAL_CLIENTS = {
376
377
  };
377
378
 
378
379
  /**
379
- * Resolve the personal-seat config from the environment. `HILOS_MCP_TOKEN` is
380
- * the same `hpt_` token the plugin's MCP server entry uses, so installing the
381
- * plugin is the only gesture; `--personal` forces the lane on for a hook
382
- * installed by `hooks install --claude --personal`. PURE.
380
+ * Resolve the personal-seat config from the environment. Claude Code exports
381
+ * its saved plugin option to hooks; manual hooks and other clients still use
382
+ * HILOS_MCP_TOKEN. A nonempty saved option wins, including an invalid/revoked
383
+ * one: never silently attach as the fallback identity. PURE.
383
384
  */
384
385
  export function personalHookConfig(env = {}, { vendor = "unknown" } = {}) {
385
- const token = typeof env.HILOS_MCP_TOKEN === "string" ? env.HILOS_MCP_TOKEN.trim() : "";
386
+ const option = typeof env.CLAUDE_PLUGIN_OPTION_HILOS_MCP_TOKEN === "string"
387
+ ? env.CLAUDE_PLUGIN_OPTION_HILOS_MCP_TOKEN.trim() : "";
388
+ const token = option || (typeof env.HILOS_MCP_TOKEN === "string" ? env.HILOS_MCP_TOKEN.trim() : "");
386
389
  if (!token || !token.startsWith("hpt_")) return null;
387
390
  const client = PERSONAL_CLIENTS[vendor];
388
391
  if (!client) return null;
@@ -402,24 +405,25 @@ export function seatLabel(value) {
402
405
  }
403
406
 
404
407
  /**
405
- * The label for this session's seat: the repo from `git remote`, else the
406
- * folder name. `git` is asked once per session (SessionStart) with a short
407
- * timeout, and a failure just falls back — a hook never blocks the CLI.
408
+ * The canonical origin for this session. Git is asked once at attach with a
409
+ * short timeout. A missing or unsupported origin leaves the seat unbound.
408
410
  */
409
- export function readSeatLabel(cwd, { spawn = spawnSync } = {}) {
410
- const folder = seatLabel(cwd);
411
+ export function readSeatRepository(cwd, { spawn = spawnSync } = {}) {
411
412
  try {
412
413
  const out = spawn("git", ["-C", String(cwd || "."), "remote", "get-url", "origin"], {
413
414
  encoding: "utf8",
414
415
  timeout: 1500,
415
416
  });
416
- const remote = out?.status === 0 ? seatLabel(out.stdout) : "";
417
- return remote || folder;
417
+ return out?.status === 0 ? normalizeSeatRepository(out.stdout) : null;
418
418
  } catch {
419
- return folder;
419
+ return null;
420
420
  }
421
421
  }
422
422
 
423
+ export function readSeatLabel(cwd, options) {
424
+ return seatLabel(readSeatRepository(cwd, options)) || seatLabel(cwd);
425
+ }
426
+
423
427
  /** One bounded personal `tools/call`. Returns the parsed payload, or null. */
424
428
  export async function callPersonalTool({ url, token, client, name, args }) {
425
429
  const controller = new AbortController();
@@ -463,7 +467,8 @@ export async function runPersonalSeat({
463
467
  now = Date.now,
464
468
  note = "",
465
469
  call = callPersonalTool,
466
- label = readSeatLabel,
470
+ label,
471
+ repository = readSeatRepository,
467
472
  }) {
468
473
  if (!personal || !ev) return state;
469
474
  const seat = state.seat && typeof state.seat === "object" ? state.seat : {};
@@ -479,12 +484,14 @@ export async function runPersonalSeat({
479
484
  ev.event === "PostToolUse";
480
485
  if (!seat.id && !wantsSeat) return state;
481
486
  if (ev.event === "SessionStart" || !seat.id) {
487
+ const repositoryUrl = repository(ev.cwd);
482
488
  const attached = await call({
483
489
  ...base,
484
490
  name: "attach_session",
485
491
  args: {
486
492
  client: personal.client,
487
- label: label(ev.cwd),
493
+ label: label ? label(ev.cwd) : seatLabel(repositoryUrl) || seatLabel(ev.cwd),
494
+ ...(repositoryUrl ? { repositoryUrl } : {}),
488
495
  sessionKey: ev.sessionId,
489
496
  },
490
497
  });
@@ -945,7 +952,7 @@ export async function hookMain({ vendor = "unknown", scopeManaged = false, perso
945
952
  // `--personal` is the plugin's install: this session belongs to a person,
946
953
  // so it never streams into an agent's live card even when an agent
947
954
  // credential happens to sit in ~/.hilos/agent.json. The seat itself rides
948
- // HILOS_MCP_TOKEN, because a plugin cannot prompt for a token.
955
+ // the saved Claude plugin option, or HILOS_MCP_TOKEN for manual hooks.
949
956
  const cfg = personal ? {} : resolveConfig({});
950
957
  await runHook({ raw, cfg, vendor, personal: personalHookConfig(process.env, { vendor }) });
951
958
  } catch {
@@ -0,0 +1 @@
1
+ export function normalizeSeatRepository(value: unknown): string | null;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A GitHub origin as a credential-free HTTPS URL. Room links currently name
3
+ * GitHub repositories, so another host (even with the same owner/repo), local
4
+ * paths, SSH aliases, and malformed remotes cannot select a room.
5
+ */
6
+ export function normalizeSeatRepository(value) {
7
+ if (typeof value !== "string") return null;
8
+ const text = value.trim();
9
+ if (!text || /[\\\s]/.test(text)) return null;
10
+ let url;
11
+ try {
12
+ const scp = text.match(/^git@github\.com:([^?#]+)$/i);
13
+ url = new URL(scp ? `https://github.com/${scp[1]}` : text);
14
+ } catch {
15
+ return null;
16
+ }
17
+ if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol) ||
18
+ url.hostname.toLowerCase() !== "github.com" || url.port) return null;
19
+ const path = url.pathname.replace(/\/$/, "").replace(/\.git$/i, "");
20
+ if (!/^\/[a-z0-9-]+\/[a-z0-9_.-]+$/i.test(path)) return null;
21
+ const [, owner, repo] = path.split("/");
22
+ if (repo === "." || repo === "..") return null;
23
+ return `https://github.com/${owner.toLowerCase()}/${repo.toLowerCase()}`;
24
+ }