11// Helpers for figuring out whether agentlock is already wired into a
22// harness on disk, and which daemon URL it currently points at. Used by
33// detectors so the install picker can pre-check rows that are already
4- // installed and show "wired → http://..." next to them.
4+ // installed and show "wired -> http://..." next to them.
55
66import { existsSync , readFileSync } from "node:fs" ;
77import { join } from "node:path" ;
@@ -14,10 +14,6 @@ export interface AgentlockState {
1414
1515const NOT_INSTALLED : AgentlockState = { installed : false } ;
1616
17- // originOf returns scheme://host[:port] for a parseable URL, or the
18- // original string if URL parsing fails. Lets us collapse a per-hook
19- // URL like "http://127.0.0.1:7878/v1/hooks/claude-code/pre-tool-use"
20- // down to "http://127.0.0.1:7878" for the picker sub-line.
2117function originOf ( u : string ) : string {
2218 try {
2319 return new URL ( u ) . origin ;
@@ -26,10 +22,6 @@ function originOf(u: string): string {
2622 }
2723}
2824
29- // claudeAgentlockState reads a Claude Code settings.json and reports
30- // whether agentlock-tagged hook entries (the `_agentlock: true` marker
31- // applyClaudeCode writes) are present, plus the daemon URL if we can
32- // pull one out of any embedded HTTP hook.
3325export function claudeAgentlockState ( settingsPath : string ) : AgentlockState {
3426 if ( ! existsSync ( settingsPath ) ) return NOT_INSTALLED ;
3527 let raw : string ;
@@ -54,10 +46,6 @@ export function claudeAgentlockState(settingsPath: string): AgentlockState {
5446 if ( ! entry || typeof entry !== "object" ) continue ;
5547 const e = entry as Record < string , unknown > ;
5648 if ( e . _agentlock !== true ) continue ;
57- // Prefer the URL from the first nested HTTP hook so the picker can
58- // surface "wired → http://...:7878" without rendering all of them.
59- // Strip the per-hook path (`/v1/hooks/...`) so the picker only
60- // shows the daemon origin — that's the part the user is choosing.
6149 const inner = Array . isArray ( e . hooks ) ? ( e . hooks as unknown [ ] ) : [ ] ;
6250 for ( const h of inner ) {
6351 if ( h && typeof h === "object" ) {
@@ -73,12 +61,6 @@ export function claudeAgentlockState(settingsPath: string): AgentlockState {
7361 return NOT_INSTALLED ;
7462}
7563
76- // claudeDesktopAgentlockState reads a Claude Desktop config file
77- // (claude_desktop_config.json) and reports whether agentlock is wired in
78- // as an MCP server. Claude Desktop has no PreToolUse hook surface; the
79- // only install we can do is registering an MCP server entry under
80- // mcpServers. We mark our entry with `_agentlock: true` (an unknown key
81- // MCP ignores) so uninstall can find it without name-collision risk.
8264export function claudeDesktopAgentlockState ( configPath : string ) : AgentlockState {
8365 if ( ! existsSync ( configPath ) ) return NOT_INSTALLED ;
8466 let raw : string ;
@@ -101,8 +83,6 @@ export function claudeDesktopAgentlockState(configPath: string): AgentlockState
10183 if ( ! entry || typeof entry !== "object" ) continue ;
10284 const e = entry as Record < string , unknown > ;
10385 if ( e . _agentlock !== true ) continue ;
104- // Daemon URL lives in `env.AGENTLOCK_DAEMON_URL` — same convention as
105- // the Claude Code shim. No nested HTTP hook on this surface.
10686 const env = ( e . env as Record < string , unknown > | undefined ) ?? undefined ;
10787 const url = env && typeof env . AGENTLOCK_DAEMON_URL === "string"
10888 ? env . AGENTLOCK_DAEMON_URL
@@ -112,10 +92,52 @@ export function claudeDesktopAgentlockState(configPath: string): AgentlockState
11292 return NOT_INSTALLED ;
11393}
11494
115- // devStubAgentlockState reads `.agentlock-dev.json` from a non-claude
116- // harness's dev sandbox dir. The daemon's apply pipeline writes that
117- // marker for harnesses without a real installer yet. Presence + the
118- // stamped daemon_url are enough for the picker.
95+ export function codexAgentlockState ( hooksPath : string ) : AgentlockState {
96+ if ( ! existsSync ( hooksPath ) ) return NOT_INSTALLED ;
97+ let raw : string ;
98+ try {
99+ raw = readFileSync ( hooksPath , "utf8" ) ;
100+ } catch {
101+ return NOT_INSTALLED ;
102+ }
103+ let parsed : unknown ;
104+ try {
105+ parsed = JSON . parse ( raw ) ;
106+ } catch {
107+ return NOT_INSTALLED ;
108+ }
109+ const root =
110+ parsed && typeof parsed === "object" && ! Array . isArray ( parsed )
111+ ? ( parsed as Record < string , unknown > )
112+ : { } ;
113+ const hooks =
114+ root . hooks && typeof root . hooks === "object"
115+ ? ( root . hooks as Record < string , unknown > )
116+ : root ;
117+ for ( const list of Object . values ( hooks ) ) {
118+ if ( ! Array . isArray ( list ) ) continue ;
119+ for ( const entry of list ) {
120+ if ( ! entry || typeof entry !== "object" ) continue ;
121+ const e = entry as Record < string , unknown > ;
122+ if ( e . _agentlock !== true ) continue ;
123+ const inner = Array . isArray ( e . hooks ) ? ( e . hooks as unknown [ ] ) : [ ] ;
124+ for ( const h of inner ) {
125+ if ( ! h || typeof h !== "object" ) continue ;
126+ const hook = h as { env ?: unknown } ;
127+ const env = hook . env ;
128+ if ( env && typeof env === "object" ) {
129+ const url = ( env as { AGENTLOCK_DAEMON_URL ?: unknown } ) . AGENTLOCK_DAEMON_URL ;
130+ if ( typeof url === "string" && url . length > 0 ) {
131+ return { installed : true , daemonURL : originOf ( url ) } ;
132+ }
133+ }
134+ }
135+ return { installed : true } ;
136+ }
137+ }
138+ return NOT_INSTALLED ;
139+ }
140+
119141export function devStubAgentlockState ( harnessDir : string ) : AgentlockState {
120142 const marker = join ( harnessDir , ".agentlock-dev.json" ) ;
121143 if ( ! existsSync ( marker ) ) return NOT_INSTALLED ;
@@ -128,17 +150,10 @@ export function devStubAgentlockState(harnessDir: string): AgentlockState {
128150 const url = typeof parsed . daemon_url === "string" ? parsed . daemon_url : undefined ;
129151 return { installed : true , daemonURL : url } ;
130152 } catch {
131- // Marker exists but is unparseable — treat as installed without a
132- // URL so the picker still flags it. Better than hiding partial
133- // state from the user.
134153 return { installed : true } ;
135154 }
136155}
137156
138- // devStubStateForHarness mirrors the daemon's devStubDir layout: every
139- // non-claude harness writes its marker under `<home>/.<harnessId>/`.
140- // Detectors call this with their HarnessId to get the same answer the
141- // install picker needs without hand-rolling paths.
142157export function devStubStateForHarness ( harnessID : string ) : AgentlockState {
143158 return devStubAgentlockState ( join ( home ( ) , "." + harnessID ) ) ;
144159}
0 commit comments