aihook 0.1.5__py3-none-any.whl

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.
aihook/SKILL.md ADDED
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: aihook
3
+ description: "Pause a running Python script and explore or manipulate its live namespace over HTTP from an AI coding agent. Use for debugging hard-to-reproduce runtime state, inspecting real (not mocked) objects, and trying fixes against the live process before editing source files. Includes a CLI (aihook) with lock-file-based session discovery."
4
+ version: "0.1.5"
5
+ ---
6
+
7
+
8
+
9
+ # aihook — Agentic REPL Skill
10
+
11
+ Pause a running Python script and explore / manipulate its live namespace
12
+ over HTTP. Designed to be driven by an AI coding agent.
13
+
14
+ ## When to use
15
+
16
+ - Debugging hard-to-reproduce runtime state.
17
+ - Exploring real (not mocked) objects: shapes, keys, attributes.
18
+ - Trying fixes against the live process before editing source files.
19
+
20
+ ## Install the hook in the host script
21
+
22
+ ```python
23
+ from aihook import agent_hook
24
+ agent_hook()
25
+ ```
26
+
27
+ Calling `agent_hook()` with no arguments uses the caller's globals and
28
+ locals (locals override globals on name collision). It starts an HTTP REPL
29
+ bound to `127.0.0.1` on a free port in `5001-5101` and blocks until
30
+ `exit()` is sent.
31
+
32
+ ## Canonical agent workflow
33
+
34
+ 1. Start the host script **in the background** with output redirected to a
35
+ log file:
36
+ ```bash
37
+ python path/to/host_script.py > aihook-host.log 2>&1 &
38
+ ```
39
+ **Do not run the host script in the foreground.** It will not return
40
+ until `exit()` is sent, which would block the agent's shell turn.
41
+ `aihook-host.log` is the recommended name for the log file. If you need
42
+ multiple log files append a number, e.g. `aihook-host1.log` etc.
43
+
44
+ 2.
45
+ - Interact with the paused script. The CLI waits up to 180s by default
46
+ for `./aihook-lock.yml` to appear, validates that the process is alive and
47
+ the port is responding, then resolves the port automatically:
48
+ ```bash
49
+ aihook 'complex_var["nested"]["value"]'
50
+ aihook -f snippet.py
51
+ ```
52
+ - Use `-f FILE` to reuse complex testing snippets after editing the host
53
+ code — rerun the same file after each change. `aihook-snippet.py` is the
54
+ recommended name. If you need multiple snippets append a number, e.g.
55
+ `aihook-snippet1.py` etc.
56
+
57
+ - If 180s is not enough (rare), override with `--wait`:
58
+ ```bash
59
+ aihook --wait 600 'x'
60
+ ```
61
+ - When the session is found, the CLI prints how long it waited. If the
62
+ timeout expires without finding a healthy session, it exits with a clear
63
+ error — check the host script's log file for crashes.
64
+
65
+ 4. End the session:
66
+ ```bash
67
+ aihook --exit
68
+ ```
69
+
70
+ **Iterative probe loop.** For anything beyond short snippets (1-5 lines),
71
+ keep a `snippet.py` next to the host script and rerun it after each edit:
72
+ ```
73
+ # edit snippet.py
74
+ aihook -f snippet.py
75
+ # observe, edit snippet.py again, repeat
76
+ ```
77
+ This is the primary workflow; the single-expression form is for quick
78
+ probes only.
79
+
80
+ ## CLI reference
81
+
82
+ - `aihook '<code>'` — send code to the active session.
83
+ - `aihook -f FILE` — send the contents of FILE as the command.
84
+ - `aihook -f -` — same, but read the file from stdin.
85
+ - `aihook -` — read code from stdin (also the default when stdin is piped).
86
+ - `aihook --exit` — send `exit()` to shut the session down.
87
+ - `aihook --status` — show whether a session is active, stale, or absent (exits 0 if healthy).
88
+ - `aihook --clean` — remove a stale lock file; refuses if the session is active.
89
+ - `aihook -p PORT` — target a specific port (skips lock-file discovery).
90
+ - `aihook --lockfile PATH` — use a custom lock-file path.
91
+ - `aihook --wait SECONDS` — how long to wait for a healthy session (default 180s). Increase
92
+ for unusually slow startup; if it times out, check the host script's log for crashes.
93
+
94
+ Exit code is non-zero if the remote code raised or wrote to stderr.
95
+
96
+ `-f FILE` is opened by the CLI process (agent side), so relative paths
97
+ resolve against the **agent's** current working directory, not the host
98
+ script's. Use an absolute path (e.g. `-f "$PWD/snippet.py"`) if you are
99
+ not certain the cwds match, or if your shell tool chains `cd` commands
100
+ unreliably.
101
+
102
+ ## Auto-print last expression
103
+
104
+ If the submitted command parses as a **single expression**, its `repr()` is
105
+ printed automatically (unless the value is `None`). You don't need to wrap
106
+ probes in `print(...)`.
107
+
108
+ ```
109
+ aihook 'complex_var["nested"]'
110
+ # -> {'value': 42, 'items': [1, 2, 3, 4]}
111
+ ```
112
+
113
+ **Multi-statement blocks do NOT auto-print.** Statements (`=`, `def`, `for`,
114
+ multi-line snippets) execute silently unless you add explicit `print()` calls.
115
+ A probe that returns nothing is often a missing `print()`, not an empty result.
116
+
117
+ ```python
118
+ # snippet.py — always use print() in multi-line snippets
119
+ for k, v in data.items():
120
+ print(k, v)
121
+ ```
122
+
123
+ ## Session discovery
124
+
125
+ On startup, the host writes `./aihook-lock.yml` in its current working
126
+ directory, containing `pid`, `port`, `cwd`, `start_time`, `script`. The
127
+ file is removed on clean shutdown. The banner also prints the source file and
128
+ line number where `agent_hook()` was called, which is useful when a script
129
+ has multiple hook points.
130
+
131
+ **Assumption: at most one aihook process per working directory.** If a
132
+ second host script is started in the same cwd while another is active, it
133
+ will refuse to start and point you to the existing lock file. Stale lock
134
+ files (pid no longer alive) are overwritten automatically.
135
+
136
+ Lock-file discovery does **not** walk up parent directories — `aihook`
137
+ only looks at `./aihook-lock.yml` in the invoking shell's exact cwd.
138
+ Run `aihook` from the same directory as the host script.
139
+
140
+ If the host process is killed uncleanly (e.g. `SIGKILL`, OOM), the lock
141
+ file may remain. A subsequent `agent_hook()` call in the same cwd will
142
+ detect this automatically (pid not alive) and overwrite it. To clean up
143
+ manually, just `rm ./aihook-lock.yml`.
144
+
145
+ ## CPython caveat: local-variable write-back
146
+
147
+ Rebinding a **local** variable of the calling function from inside the
148
+ REPL does *not* write back to that function's fast-locals. This is the
149
+ same limitation as `pdb`:
150
+
151
+ ```python
152
+ # In host_script.py inside my_function():
153
+ # x = 1
154
+ # agent_hook()
155
+ # print(x) # will still print 1, even if you did `x = 2` via aihook
156
+ ```
157
+
158
+ **Mutating mutable objects works as expected:**
159
+
160
+ ```python
161
+ complex_var["nested"]["items"].append(99) # visible in the host afterwards
162
+ ```
163
+
164
+ Prefer mutation for testing fixes. To change a simple local, mutate a
165
+ container, a module-level global, or an object attribute instead.
166
+
167
+ ## Environment variables
168
+
169
+ - `AIHOOK_PORT=NNNN` — force a specific port.
170
+ - `AIHOOK_PORT_RANGE=LO-HI` — override the default `5001-5101` range.
171
+
172
+ ## Agent-specific learnings library
173
+
174
+ For niche, agent-specific topics (e.g., specific Python package quirks, custom workflow tips) that are too detailed for this main skill file, refer to the `learnings` directory.
175
+
176
+ The `learnings` directory is managed by `aihook` and stored in your platform's user data directory (e.g., `~/.local/share/aihook/learnings` on Linux, `~/Library/Application Support/aihook/learnings` on macOS, `C:\Users\<User>\AppData\Local\aihook\learnings` on Windows). It is created when you run `aihook --bootstrap`.
177
+
178
+ The directory contains markdown files named `topic_<name>.md` (e.g., `topic_numpy.md`) covering independent topics relevant to multiple agent sessions. Default files (like `README.md`) are copied from the aihook package; user-added topic files are never overwritten.
179
+
180
+ Add your own topic files to this directory as your agents accumulate learnings.
aihook/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ from .release import __version__
2
+
3
+ # the package is imported during installation
4
+ # however installation happens in an isolated build environment
5
+ # where no dependencies are installed.
6
+
7
+ # this means: no importing the following modules will fail
8
+ # during installation. This is OK, but only during installation
9
+
10
+
11
+ try:
12
+ from . import core
13
+ from .core import agent_hook, AgenticREPL
14
+ except ImportError:
15
+ import os
16
+ import sys
17
+
18
+ # detect whether installation is running
19
+ cond1 = "PIP_BUILD_TRACKER" in os.environ # triggered by pip
20
+ cond2 = os.path.join("uv", "builds-v") in sys.executable
21
+ cond3 = "_PYPROJECT_HOOKS_BUILD_BACKEND" in os.environ # triggered by uv pip install
22
+ cond4 = "PEP517_BUILD_BACKEND" in os.environ # triggered during `python -m build`
23
+
24
+ if any((cond1, cond2, cond3, cond4)):
25
+ pass
26
+ else:
27
+ # raise the original exception
28
+ raise
aihook/cli.py ADDED
@@ -0,0 +1,456 @@
1
+ """
2
+ Command line interface for aihook.
3
+
4
+ Sends Python code to an active aihook session (started by ``agent_hook()``
5
+ in a host script) and prints the resulting stdout / stderr.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import os
11
+ import socket as _socket
12
+ import sys
13
+ import time
14
+ from urllib import request as urlrequest
15
+ from urllib.error import URLError
16
+
17
+ import platformdirs
18
+ from importlib.resources import files as _resource_files
19
+
20
+ from . import core
21
+
22
+
23
+ AIDER_DESK_SKILL_DIR = os.path.expanduser("~/.aider-desk/skills/aihook")
24
+ CLAUDE_CODE_COMMANDS_DIR = os.path.expanduser("~/.claude/commands")
25
+
26
+
27
+ DEFAULT_WAIT_SECONDS = 180.0
28
+
29
+
30
+ def _port_is_listening(port, timeout=1.0):
31
+ """Return True if 127.0.0.1:port is accepting connections."""
32
+ try:
33
+ s = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
34
+ s.settimeout(timeout)
35
+ s.connect(("127.0.0.1", int(port)))
36
+ s.close()
37
+ return True
38
+ except OSError:
39
+ return False
40
+
41
+
42
+ def _resolve_port(args):
43
+ """
44
+ Resolve the target port according to the documented priority:
45
+ 1. --port
46
+ 2. --lockfile PATH
47
+ 3. ./aihook-lock.yml
48
+ If no port is resolvable yet and a wait is configured, wait up to the
49
+ configured number of seconds for the lock file to appear (and its pid to
50
+ be alive and its port to be listening).
51
+ """
52
+ if args.port is not None:
53
+ return args.port
54
+
55
+ lockfile_path = args.lockfile or os.path.join(os.getcwd(), core.LOCKFILE_NAME)
56
+ wait_seconds = args.wait if args.wait is not None else DEFAULT_WAIT_SECONDS
57
+
58
+ start = time.monotonic()
59
+ deadline = start + max(0.0, wait_seconds)
60
+ first = True
61
+ while True:
62
+ if os.path.exists(lockfile_path):
63
+ try:
64
+ data = core.read_lockfile(lockfile_path)
65
+ except Exception as e:
66
+ if time.monotonic() >= deadline:
67
+ sys.stderr.write(f"aihook: could not parse lock file {lockfile_path}: {e}\n")
68
+ sys.exit(2)
69
+ time.sleep(0.1)
70
+ continue
71
+
72
+ pid = data.get("pid")
73
+ port = data.get("port")
74
+ if pid and port and core._pid_alive(pid):
75
+ if _port_is_listening(port):
76
+ elapsed = time.monotonic() - start
77
+ if elapsed >= 0.5:
78
+ sys.stderr.write(f"aihook: session found after {elapsed:.1f}s\n")
79
+ return int(port)
80
+ # PID alive but port not responding — server may have crashed
81
+ if time.monotonic() >= deadline:
82
+ sys.stderr.write(
83
+ f"aihook: pid {pid} is alive but port {port} is not responding.\n"
84
+ f"aihook: the server may have crashed. "
85
+ f"Run 'aihook --clean' to remove the lock file.\n"
86
+ )
87
+ sys.exit(2)
88
+ else:
89
+ # Stale lock file: wait a bit in case it is being rewritten.
90
+ if time.monotonic() >= deadline:
91
+ sys.stderr.write(f"aihook: lock file {lockfile_path} is stale (pid {pid} not alive).\n")
92
+ sys.exit(2)
93
+
94
+ if time.monotonic() >= deadline:
95
+ sys.stderr.write(
96
+ f"aihook: no active session found.\n"
97
+ f"aihook: expected lock file at {lockfile_path}.\n"
98
+ f"aihook: hint: start the host script (see SKILL.md) or use --wait.\n"
99
+ )
100
+ sys.exit(2)
101
+
102
+ if first:
103
+ first = False
104
+ time.sleep(0.1)
105
+
106
+
107
+ def _send(port, command, timeout=30.0):
108
+ """POST ``command`` to the /execute endpoint, request JSON response."""
109
+ url = f"http://127.0.0.1:{port}/execute?format=json"
110
+ data = command.encode("utf-8")
111
+ req = urlrequest.Request(url, data=data, method="POST", headers={"Content-Type": "text/plain"})
112
+ try:
113
+ with urlrequest.urlopen(req, timeout=timeout) as resp:
114
+ body = resp.read().decode("utf-8")
115
+ except URLError as e:
116
+ sys.stderr.write(f"aihook: cannot reach session on port {port}: {e}\n")
117
+ sys.exit(2)
118
+
119
+ try:
120
+ return json.loads(body)
121
+ except json.JSONDecodeError:
122
+ # Fallback: treat as plain text on stdout.
123
+ return {"stdout": body, "stderr": "", "result_repr": None, "exception": None}
124
+
125
+
126
+ def _status_cmd(lockfile_path):
127
+ """Report the status of the aihook session at lockfile_path."""
128
+ if not os.path.exists(lockfile_path):
129
+ print(f"aihook: no active session (no lock file at {lockfile_path})")
130
+ sys.exit(0)
131
+
132
+ try:
133
+ data = core.read_lockfile(lockfile_path)
134
+ except Exception as e:
135
+ sys.stderr.write(f"aihook: corrupt lock file at {lockfile_path}: {e}\n")
136
+ sys.exit(2)
137
+
138
+ pid = data.get("pid")
139
+ port = data.get("port")
140
+ started = data.get("start_time", "unknown")
141
+ tool = data.get("tool")
142
+
143
+ if tool and tool != "aihook":
144
+ sys.stderr.write(f"aihook: warning: lock file 'tool' field is {tool!r}, expected 'aihook'\n")
145
+
146
+ if core.lockfile_is_stale(lockfile_path):
147
+ sys.stderr.write(
148
+ f"aihook: stale lock file at {lockfile_path} (pid {pid} not alive)\n"
149
+ f"aihook: run 'aihook --clean' to remove it.\n"
150
+ )
151
+ sys.exit(1)
152
+
153
+ if port and _port_is_listening(port):
154
+ print(f"aihook: session active — pid={pid}, port={port}, started={started}")
155
+ sys.exit(0)
156
+ else:
157
+ sys.stderr.write(
158
+ f"aihook: pid {pid} is alive but port {port} is not responding.\n"
159
+ f"aihook: the server may have crashed. Run 'aihook --clean' to remove the lock file.\n"
160
+ )
161
+ sys.exit(1)
162
+
163
+
164
+ def _clean_cmd(lockfile_path):
165
+ """Remove a stale lock file. Refuse if the session is active."""
166
+ if not os.path.exists(lockfile_path):
167
+ print(f"aihook: nothing to clean (no lock file at {lockfile_path})")
168
+ sys.exit(0)
169
+
170
+ try:
171
+ data = core.read_lockfile(lockfile_path)
172
+ corrupt = False
173
+ except Exception:
174
+ data = {}
175
+ corrupt = True
176
+
177
+ if not corrupt and not core.lockfile_is_stale(lockfile_path):
178
+ pid = data.get("pid")
179
+ port = data.get("port")
180
+ sys.stderr.write(
181
+ f"aihook: refusing to remove lock file for active session "
182
+ f"(pid={pid}, port={port}).\n"
183
+ f"aihook: use 'aihook --exit' to stop the session first.\n"
184
+ )
185
+ sys.exit(1)
186
+
187
+ pid = data.get("pid")
188
+ core.remove_lockfile(lockfile_path)
189
+ if corrupt:
190
+ print(f"aihook: removed corrupt lock file {lockfile_path}")
191
+ else:
192
+ print(f"aihook: removed stale lock file {lockfile_path} (pid {pid} was not alive)")
193
+ sys.exit(0)
194
+
195
+
196
+ def _read_command(args):
197
+ """Determine the command source."""
198
+ if args.exit:
199
+ return "exit()"
200
+
201
+ if args.file:
202
+ if args.file == "-":
203
+ return sys.stdin.read()
204
+ with open(args.file, "r", encoding="utf-8") as f:
205
+ return f.read()
206
+
207
+ if args.cmd == "-" or (args.cmd is None and not sys.stdin.isatty()):
208
+ return sys.stdin.read()
209
+
210
+ if args.cmd is None:
211
+ return None
212
+
213
+ return args.cmd
214
+
215
+
216
+ def _build_parser():
217
+ parser = argparse.ArgumentParser(
218
+ prog="aihook",
219
+ description="Send Python code to a running aihook REPL session.",
220
+ add_help=False, # We'll add custom help
221
+ )
222
+ parser.add_argument(
223
+ "cmd",
224
+ nargs="?",
225
+ help="Python code to execute. Use '-' to read from stdin.",
226
+ )
227
+ parser.add_argument(
228
+ "-p",
229
+ "--port",
230
+ type=int,
231
+ default=None,
232
+ help="Target port. Overrides lock-file discovery.",
233
+ )
234
+ parser.add_argument(
235
+ "-f",
236
+ "--file",
237
+ default=None,
238
+ help="Read code from FILE and send it. Useful for reusing snippets.",
239
+ )
240
+ parser.add_argument(
241
+ "--exit",
242
+ action="store_true",
243
+ help="Send exit() to shut down the session.",
244
+ )
245
+ parser.add_argument(
246
+ "--wait",
247
+ type=float,
248
+ default=None,
249
+ help=f"Seconds to wait for a lock file to appear (default: {DEFAULT_WAIT_SECONDS}s).",
250
+ )
251
+ parser.add_argument(
252
+ "--lockfile",
253
+ default=None,
254
+ help="Path to the lock file (default: ./aihook-lock.yml).",
255
+ )
256
+ parser.add_argument(
257
+ "--status",
258
+ action="store_true",
259
+ help="Show status of the current aihook session and exit.",
260
+ )
261
+ parser.add_argument(
262
+ "--clean",
263
+ action="store_true",
264
+ help="Remove a stale lock file and exit. Refuses if a session is active.",
265
+ )
266
+ parser.add_argument(
267
+ "--bootstrap",
268
+ action="store_true",
269
+ help=(
270
+ f"Install SKILL.md for the target agent (see --agent) and create the learnings "
271
+ f"directory, then exit. Aider: {AIDER_DESK_SKILL_DIR}/SKILL.md. "
272
+ f"Claude: {CLAUDE_CODE_COMMANDS_DIR}/aihook.md."
273
+ ),
274
+ )
275
+ parser.add_argument(
276
+ "--agent",
277
+ default="aider",
278
+ choices=["aider", "claude", "all"],
279
+ help="Target agent for --bootstrap skill installation (default: aider).",
280
+ )
281
+ parser.add_argument(
282
+ "--allow-overwrite-SKILL.md",
283
+ dest="allow_overwrite_SKILL_md",
284
+ action="store_true",
285
+ help="With --bootstrap: overwrite an existing skill file at the destination.",
286
+ )
287
+ parser.add_argument(
288
+ "--version",
289
+ action="store_true",
290
+ help="Show the version and exit.",
291
+ )
292
+ parser.add_argument(
293
+ "-h",
294
+ "--help",
295
+ action="store_true",
296
+ help="Show this help message and exit.",
297
+ )
298
+ return parser
299
+
300
+
301
+ def _load_skill_content():
302
+ """Return the packaged SKILL.md text, or exit with an error."""
303
+ try:
304
+ source = _resource_files("aihook").joinpath("SKILL.md")
305
+ return source.read_text(encoding="utf-8")
306
+ except (FileNotFoundError, ModuleNotFoundError) as e:
307
+ sys.stderr.write(f"aihook: could not locate packaged SKILL.md: {e}\n")
308
+ sys.exit(1)
309
+
310
+
311
+ def _install_skill_aider(allow_overwrite):
312
+ dest_skill = os.path.join(AIDER_DESK_SKILL_DIR, "SKILL.md")
313
+ if os.path.exists(dest_skill) and not allow_overwrite:
314
+ sys.stderr.write(
315
+ f"aihook: {dest_skill} already exists.\n"
316
+ f"aihook: pass --allow-overwrite-SKILL.md to replace it.\n"
317
+ )
318
+ sys.exit(1)
319
+ skill_content = _load_skill_content()
320
+ os.makedirs(AIDER_DESK_SKILL_DIR, exist_ok=True)
321
+ with open(dest_skill, "w", encoding="utf-8") as f:
322
+ f.write(skill_content)
323
+ print(f"aihook: wrote {dest_skill}")
324
+
325
+
326
+ def _install_skill_claude(allow_overwrite):
327
+ dest_skill = os.path.join(CLAUDE_CODE_COMMANDS_DIR, "aihook.md")
328
+ if os.path.exists(dest_skill) and not allow_overwrite:
329
+ sys.stderr.write(
330
+ f"aihook: {dest_skill} already exists.\n"
331
+ f"aihook: pass --allow-overwrite-SKILL.md to replace it.\n"
332
+ )
333
+ sys.exit(1)
334
+ skill_content = _load_skill_content()
335
+ os.makedirs(CLAUDE_CODE_COMMANDS_DIR, exist_ok=True)
336
+ with open(dest_skill, "w", encoding="utf-8") as f:
337
+ f.write(skill_content)
338
+ print(f"aihook: wrote {dest_skill}")
339
+
340
+
341
+ def _setup_learnings():
342
+ try:
343
+ aihook_data_dir = platformdirs.user_data_dir("aihook")
344
+ except Exception as e:
345
+ sys.stderr.write(f"aihook: could not determine user data directory: {e}\n")
346
+ sys.exit(1)
347
+ learnings_dest = os.path.join(aihook_data_dir, "learnings")
348
+ os.makedirs(learnings_dest, exist_ok=True)
349
+ print(f"aihook: learnings directory at {learnings_dest}")
350
+
351
+ try:
352
+ pkg_learnings = _resource_files("aihook").joinpath("learnings")
353
+ for item in pkg_learnings.iterdir():
354
+ if item.is_file():
355
+ dest_file = os.path.join(learnings_dest, item.name)
356
+ if os.path.exists(dest_file):
357
+ sys.stderr.write(f"aihook: warning: {dest_file} already exists, skipping.\n")
358
+ continue
359
+ try:
360
+ content = item.read_text(encoding="utf-8")
361
+ except Exception as e:
362
+ sys.stderr.write(f"aihook: could not read packaged learnings file {item.name}: {e}\n")
363
+ continue
364
+ with open(dest_file, "w", encoding="utf-8") as f:
365
+ f.write(content)
366
+ print(f"aihook: wrote {dest_file}")
367
+ except (FileNotFoundError, ModuleNotFoundError) as e:
368
+ sys.stderr.write(f"aihook: no default learnings found in package: {e}\n")
369
+ except Exception as e:
370
+ sys.stderr.write(f"aihook: error processing learnings: {e}\n")
371
+
372
+
373
+ def _bootstrap(allow_overwrite_skillmd, agent):
374
+ if agent in ("aider", "all"):
375
+ _install_skill_aider(allow_overwrite_skillmd)
376
+ if agent in ("claude", "all"):
377
+ _install_skill_claude(allow_overwrite_skillmd)
378
+ _setup_learnings()
379
+ print("aihook: bootstrap complete.")
380
+ sys.exit(0)
381
+
382
+
383
+ def main(argv=None):
384
+ parser = _build_parser()
385
+ args = parser.parse_args(argv)
386
+
387
+ if args.help:
388
+ parser.print_help()
389
+ installed = []
390
+ aider_skill = os.path.join(AIDER_DESK_SKILL_DIR, "SKILL.md")
391
+ if os.path.exists(aider_skill):
392
+ installed.append(f" SKILL.md (aider): {aider_skill}")
393
+ claude_skill = os.path.join(CLAUDE_CODE_COMMANDS_DIR, "aihook.md")
394
+ if os.path.exists(claude_skill):
395
+ installed.append(f" aihook.md (claude): {claude_skill}")
396
+ try:
397
+ learnings_dir = os.path.join(platformdirs.user_data_dir("aihook"), "learnings")
398
+ except Exception:
399
+ learnings_dir = None
400
+ if learnings_dir and os.path.exists(learnings_dir):
401
+ installed.append(f" Learnings dir: {learnings_dir}")
402
+ if installed:
403
+ print("\nInstalled files/directories:")
404
+ for line in installed:
405
+ print(line)
406
+ sys.exit(0)
407
+
408
+ if args.version:
409
+ from .release import __version__
410
+
411
+ print(f"aihook {__version__}")
412
+ sys.exit(0)
413
+
414
+ if args.status:
415
+ lockfile_path = args.lockfile or os.path.join(os.getcwd(), core.LOCKFILE_NAME)
416
+ _status_cmd(lockfile_path)
417
+
418
+ if args.clean:
419
+ lockfile_path = args.lockfile or os.path.join(os.getcwd(), core.LOCKFILE_NAME)
420
+ _clean_cmd(lockfile_path)
421
+
422
+ if args.bootstrap:
423
+ _bootstrap(args.allow_overwrite_SKILL_md, args.agent)
424
+
425
+ if args.agent != "aider":
426
+ parser.error("--agent only makes sense with --bootstrap")
427
+ if args.allow_overwrite_SKILL_md:
428
+ parser.error("--allow-overwrite-SKILL.md only makes sense with --bootstrap")
429
+
430
+ command = _read_command(args)
431
+ if command is None:
432
+ parser.error("no command given (pass a positional arg, -f FILE, stdin, or --exit)")
433
+
434
+ port = _resolve_port(args)
435
+ result = _send(port, command)
436
+
437
+ stdout = result.get("stdout", "") or ""
438
+ stderr = result.get("stderr", "") or ""
439
+ exception = result.get("exception")
440
+
441
+ if stdout:
442
+ sys.stdout.write(stdout)
443
+ if not stdout.endswith("\n"):
444
+ sys.stdout.write("\n")
445
+ if stderr:
446
+ sys.stderr.write(stderr)
447
+ if not stderr.endswith("\n"):
448
+ sys.stderr.write("\n")
449
+
450
+ if exception or stderr:
451
+ sys.exit(1)
452
+ sys.exit(0)
453
+
454
+
455
+ if __name__ == "__main__":
456
+ main()