devs-common 5.0.5__tar.gz → 6.0.0__tar.gz

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.
Files changed (60) hide show
  1. {devs_common-5.0.5 → devs_common-6.0.0}/PKG-INFO +1 -1
  2. devs_common-6.0.0/devs_common/agents.py +79 -0
  3. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/config.py +10 -0
  4. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/core/container.py +10 -41
  5. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/Dockerfile +37 -3
  6. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/devcontainer.json +1 -0
  7. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/extensions/devs-bridge-drop.vsix +0 -0
  8. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/post-start.sh +16 -0
  9. devs_common-6.0.0/devs_common/templates/scripts/setup-hermes.sh +24 -0
  10. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/sudo-scripts/setup-devs-env.sh +35 -18
  11. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common.egg-info/PKG-INFO +1 -1
  12. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common.egg-info/SOURCES.txt +2 -0
  13. {devs_common-5.0.5 → devs_common-6.0.0}/pyproject.toml +1 -1
  14. {devs_common-5.0.5 → devs_common-6.0.0}/LICENSE +0 -0
  15. {devs_common-5.0.5 → devs_common-6.0.0}/README.md +0 -0
  16. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/__init__.py +0 -0
  17. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/core/__init__.py +0 -0
  18. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/core/project.py +0 -0
  19. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/core/workspace.py +0 -0
  20. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/devs_config.py +0 -0
  21. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/exceptions.py +0 -0
  22. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/README-vscode-settings.md +0 -0
  23. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/extensions/README.md +0 -0
  24. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/extensions/install.sh +0 -0
  25. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/machine-settings.json +0 -0
  26. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/machine-settings.tmux.json +0 -0
  27. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/register-vscode-extensions.js +0 -0
  28. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/copy-ssh.sh +0 -0
  29. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/install-bridge-drop.sh +0 -0
  30. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
  31. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
  32. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/run-autoexec.sh +0 -0
  33. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
  34. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/setup-vscode-settings.sh +0 -0
  35. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/setup-workspace.sh +0 -0
  36. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/start-tailscale.sh +0 -0
  37. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/start-tunnel.sh +0 -0
  38. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/ts.sh +0 -0
  39. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/tunnel-status.sh +0 -0
  40. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
  41. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
  42. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/sudo-scripts/setup-magicdns.sh +0 -0
  43. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
  44. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
  45. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/sudo-scripts/start-tailscaled.sh +0 -0
  46. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/sudo-scripts/ts-cli.sh +0 -0
  47. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/templates/vscode-settings-reference.md +0 -0
  48. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/__init__.py +0 -0
  49. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/config_hash.py +0 -0
  50. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/console.py +0 -0
  51. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/devcontainer.py +0 -0
  52. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/devcontainer_template.py +0 -0
  53. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/docker_client.py +0 -0
  54. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/file_utils.py +0 -0
  55. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/git_utils.py +0 -0
  56. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common/utils/repo_cache.py +0 -0
  57. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common.egg-info/dependency_links.txt +0 -0
  58. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common.egg-info/requires.txt +0 -0
  59. {devs_common-5.0.5 → devs_common-6.0.0}/devs_common.egg-info/top_level.txt +0 -0
  60. {devs_common-5.0.5 → devs_common-6.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 5.0.5
3
+ Version: 6.0.0
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -0,0 +1,79 @@
1
+ """Registry of the AI coding agents devs can run inside a devcontainer.
2
+
3
+ Adding an agent:
4
+
5
+ 1. Install it in ``templates/Dockerfile`` (and add shell aliases next to the others).
6
+ 2. Add an ``AgentSpec`` to ``AGENTS`` below — this gives ``ContainerManager.exec_agent``.
7
+ 3. Register its ``devs <name>`` command with ``_add_agent_command`` in the CLI's ``cli.py``.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Dict
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class AgentSpec:
16
+ """How to run one coding agent non-interactively in a devs container.
17
+
18
+ Attributes:
19
+ name: Registry key and CLI subcommand (``devs <name> ...``).
20
+ display_name: Human-readable name used in status messages.
21
+ description: What gets executed, used in help text ("Execute <description> ...").
22
+ command: Shell command run from the container workspace after ``~/.zshrc`` is
23
+ sourced. The prompt is written to its stdin, so the command must read it
24
+ from there and exit when done.
25
+ """
26
+
27
+ name: str
28
+ display_name: str
29
+ description: str
30
+ command: str
31
+
32
+
33
+ AGENTS: Dict[str, AgentSpec] = {
34
+ spec.name: spec
35
+ for spec in (
36
+ AgentSpec(
37
+ name="claude",
38
+ display_name="Claude",
39
+ description="Claude CLI",
40
+ command="claude --dangerously-skip-permissions -p",
41
+ ),
42
+ AgentSpec(
43
+ name="codex",
44
+ display_name="Codex",
45
+ description="OpenAI Codex CLI",
46
+ # `--full-auto` was removed from the Codex CLI (passing it now aborts with
47
+ # "unexpected argument"). This is the flag Codex intends for externally
48
+ # sandboxed environments, which a devcontainer is — and it matches the
49
+ # `claude --dangerously-skip-permissions` invocation beside it.
50
+ command="codex --dangerously-bypass-approvals-and-sandbox",
51
+ ),
52
+ AgentSpec(
53
+ name="hermes",
54
+ display_name="Hermes",
55
+ description="Hermes Agent (Nous Research)",
56
+ # `--query-file -` reads the prompt from stdin; `--oneshot` answers and exits
57
+ # rather than seeding an interactive session. `--yolo` skips dangerous-command
58
+ # approvals, like the other agents. `hermes chat` ignores HERMES_INFERENCE_MODEL,
59
+ # so forward it as --model when set; otherwise the model baked into the image's
60
+ # Hermes config is used. Hermes picks OpenRouter from OPENROUTER_API_KEY.
61
+ command=(
62
+ "hermes --yolo chat --oneshot --query-file - "
63
+ "${HERMES_INFERENCE_MODEL:+--model=$HERMES_INFERENCE_MODEL}"
64
+ ),
65
+ ),
66
+ )
67
+ }
68
+
69
+
70
+ def get_agent(name: str) -> AgentSpec:
71
+ """Look up an agent by name.
72
+
73
+ Raises:
74
+ ValueError: If no agent with that name is registered.
75
+ """
76
+ try:
77
+ return AGENTS[name]
78
+ except KeyError:
79
+ raise ValueError(f"Unknown agent '{name}'. Available: {', '.join(AGENTS)}") from None
@@ -12,6 +12,7 @@ class BaseConfig(ABC):
12
12
  # Default directories shared across CLI and webhook
13
13
  CLAUDE_CONFIG_DIR = Path.home() / ".devs" / "claudeconfig"
14
14
  CODEX_CONFIG_DIR = Path.home() / ".devs" / "codexconfig"
15
+ HERMES_CONFIG_DIR = Path.home() / ".devs" / "hermesconfig"
15
16
 
16
17
  def __init__(self) -> None:
17
18
  """Initialize base configuration."""
@@ -25,6 +26,13 @@ class BaseConfig(ABC):
25
26
  codex_config_env = os.getenv("DEVS_CODEX_CONFIG_DIR")
26
27
  self.codex_config_dir = Path(codex_config_env) if codex_config_env else self.CODEX_CONFIG_DIR
27
28
 
29
+ hermes_config_env = os.getenv("DEVS_HERMES_CONFIG_DIR")
30
+ hermes_config_dir = Path(hermes_config_env) if hermes_config_env else self.HERMES_CONFIG_DIR
31
+ # Bypass __setattr__: pydantic subclasses reject attributes they don't declare as
32
+ # fields, and devs-webhook releases from before Hermes support don't declare this
33
+ # one. Their `devs-common>=` pin lets a newer devs-common be installed alongside.
34
+ object.__setattr__(self, "hermes_config_dir", hermes_config_dir)
35
+
28
36
  @property
29
37
  def workspaces_dir(self) -> Path:
30
38
  """Get workspaces directory."""
@@ -100,3 +108,5 @@ class BaseConfig(ABC):
100
108
  self.bridge_dir.mkdir(parents=True, exist_ok=True)
101
109
  self.claude_config_dir.mkdir(parents=True, exist_ok=True)
102
110
  self.codex_config_dir.mkdir(parents=True, exist_ok=True)
111
+ # Bind-mounted into containers, so it must exist before `devcontainer up`
112
+ self.hermes_config_dir.mkdir(parents=True, exist_ok=True)
@@ -5,9 +5,10 @@ import re
5
5
  import time
6
6
  from datetime import datetime, timezone
7
7
  from pathlib import Path
8
- from typing import Dict, List, Optional, Tuple
8
+ from typing import Dict, List, Optional, Tuple, Union
9
9
  import subprocess
10
10
 
11
+ from ..agents import AgentSpec, get_agent
11
12
  from ..config import BaseConfig
12
13
 
13
14
 
@@ -915,13 +916,14 @@ class ContainerManager:
915
916
  except (DockerError, subprocess.SubprocessError) as e:
916
917
  raise ContainerError(f"Failed to exec command in {dev_name}: {e}")
917
918
 
918
- def exec_claude(self, dev_name: str, workspace_dir: Path, prompt: str, debug: bool = False, stream: bool = True, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> tuple[bool, str, str, int]:
919
- """Execute Claude CLI in the container.
919
+ def exec_agent(self, agent: Union[str, AgentSpec], dev_name: str, workspace_dir: Path, prompt: str, debug: bool = False, stream: bool = True, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> tuple[bool, str, str, int]:
920
+ """Execute a coding agent (Claude, Codex, Hermes, ...) in the container.
920
921
 
921
922
  Args:
923
+ agent: Agent name from ``devs_common.agents.AGENTS``, or an ``AgentSpec``
922
924
  dev_name: Development environment name
923
925
  workspace_dir: Workspace directory path
924
- prompt: Prompt to send to Claude
926
+ prompt: Prompt to send to the agent (passed on stdin)
925
927
  debug: Show debug output for devcontainer operations
926
928
  stream: Stream output to console in real-time
927
929
  live: Whether the container is in live mode
@@ -931,47 +933,14 @@ class ContainerManager:
931
933
  Tuple of (success, stdout, stderr, exit_code)
932
934
 
933
935
  Raises:
934
- ContainerError: If Claude execution fails
936
+ ValueError: If the agent name is not registered
937
+ ContainerError: If agent execution fails
935
938
  """
936
- # Simply delegate to exec_command with the Claude command and prompt as stdin
939
+ spec = get_agent(agent) if isinstance(agent, str) else agent
937
940
  return self.exec_command(
938
941
  dev_name=dev_name,
939
942
  workspace_dir=workspace_dir,
940
- command="claude --dangerously-skip-permissions -p",
941
- stdin_input=prompt,
942
- debug=debug,
943
- stream=stream,
944
- live=live,
945
- extra_env=extra_env
946
- )
947
-
948
- def exec_codex(self, dev_name: str, workspace_dir: Path, prompt: str, debug: bool = False, stream: bool = True, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> tuple[bool, str, str, int]:
949
- """Execute OpenAI Codex CLI in the container.
950
-
951
- Args:
952
- dev_name: Development environment name
953
- workspace_dir: Workspace directory path
954
- prompt: Prompt to send to Codex
955
- debug: Show debug output for devcontainer operations
956
- stream: Stream output to console in real-time
957
- live: Whether the container is in live mode
958
- extra_env: Additional environment variables to pass to container
959
-
960
- Returns:
961
- Tuple of (success, stdout, stderr, exit_code)
962
-
963
- Raises:
964
- ContainerError: If Codex execution fails
965
- """
966
- # Simply delegate to exec_command with the Codex command and prompt as stdin
967
- return self.exec_command(
968
- dev_name=dev_name,
969
- workspace_dir=workspace_dir,
970
- # `--full-auto` was removed from the Codex CLI (passing it now aborts with
971
- # "unexpected argument"). This is the flag Codex intends for externally
972
- # sandboxed environments, which a devcontainer is — and it matches the
973
- # `claude --dangerously-skip-permissions` alias sitting beside it.
974
- command="codex --dangerously-bypass-approvals-and-sandbox",
943
+ command=spec.command,
975
944
  stdin_input=prompt,
976
945
  debug=debug,
977
946
  stream=stream,
@@ -36,6 +36,7 @@ RUN apt update && apt install -y less \
36
36
  nano \
37
37
  htop \
38
38
  tmux \
39
+ ripgrep \
39
40
  && apt clean \
40
41
  && rm -rf /var/lib/apt/lists/*
41
42
 
@@ -88,7 +89,7 @@ RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhisto
88
89
  ENV DEVCONTAINER=true
89
90
 
90
91
  # Create workspace and config directories and set permissions
91
- RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig \
92
+ RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig /home/node/hermesconfig \
92
93
  /home/node/.vscode-server/extensions \
93
94
  /home/node/.vscode-server/data/Machine && \
94
95
  echo '[]' > /home/node/.vscode-server/extensions/extensions.json && \
@@ -150,6 +151,37 @@ RUN curl -fsSL https://claude.ai/install.sh | bash
150
151
  ARG CODEX_CLI_VERSION=0.144.6
151
152
  RUN npm install -g "@openai/codex@${CODEX_CLI_VERSION}"
152
153
 
154
+ # Install Hermes Agent (Nous Research's open-source coding agent). Pinned to a release
155
+ # tag for the same reason as Codex above; bump HERMES_AGENT_VERSION to upgrade. Setup
156
+ # wizard, browser and computer-use extras are skipped — the agent is driven through
157
+ # OpenRouter: put OPENROUTER_API_KEY in ~/.devs/envs/<project>/.env (or DEVS.yml env_vars).
158
+ #
159
+ # The code goes in /home/node/.hermes-agent and the data dir (HERMES_HOME: sessions,
160
+ # memory, skills, config.yaml) in /home/node/hermesconfig, which devcontainer.json
161
+ # bind-mounts from ~/.devs/hermesconfig so it persists across rebuilds like claudeconfig
162
+ # and codexconfig. The installer's default puts the code *inside* HERMES_HOME, where the
163
+ # mount would hide it. Because config.yaml lives on the host, the default model can't be
164
+ # baked in here: scripts/setup-hermes.sh seeds HERMES_DEFAULT_MODEL into it at container
165
+ # start if no model is configured yet. HERMES_INFERENCE_MODEL overrides it per container.
166
+ ARG HERMES_AGENT_VERSION=v2026.9.14
167
+ ARG HERMES_DEFAULT_MODEL=z-ai/glm-5.3
168
+ ENV HERMES_HOME=/home/node/hermesconfig \
169
+ HERMES_DEFAULT_MODEL=${HERMES_DEFAULT_MODEL}
170
+ # Download first rather than `curl | bash`: /bin/sh has no pipefail, so a failed
171
+ # download (e.g. a GitHub 429) would otherwise run an empty script and "succeed".
172
+ # The installer also seeds a data dir (~85 MB: its own uv, skills, a sample config.yaml);
173
+ # the mount would hide it anyway, so point that at a scratch dir and delete it. Hermes
174
+ # re-syncs bundled skills into the mounted HERMES_HOME on first run.
175
+ RUN curl -fsSL --retry 5 --retry-delay 10 --retry-all-errors \
176
+ "https://raw.githubusercontent.com/NousResearch/hermes-agent/${HERMES_AGENT_VERSION}/scripts/install.sh" \
177
+ -o /tmp/hermes-install.sh && \
178
+ HERMES_HOME=/tmp/hermes-build-home bash /tmp/hermes-install.sh \
179
+ --branch "${HERMES_AGENT_VERSION}" --dir /home/node/.hermes-agent \
180
+ --hermes-home /tmp/hermes-build-home \
181
+ --non-interactive --skip-setup --skip-browser --skip-computer-use && \
182
+ HERMES_HOME=/tmp/hermes-build-home /home/node/.local/bin/hermes --version && \
183
+ rm -rf /tmp/hermes-install.sh /tmp/hermes-build-home
184
+
153
185
  # Install cloudflared for tunnel support (proxies all HTTP methods correctly)
154
186
  # Install VS Code CLI for tunnel support (needs root for /usr/local/bin)
155
187
  # Using the standalone CLI which supports the 'tunnel' command
@@ -180,14 +212,16 @@ RUN ARCH=$(dpkg --print-architecture) && \
180
212
  rm -rf /tmp/tailscale.tgz "/tmp/tailscale_${TAILSCALE_VERSION}_${ARCH}" && \
181
213
  chmod +x /usr/local/bin/tailscale /usr/local/bin/tailscaled
182
214
 
183
- # Set up environment variables and aliases for Claude and Codex
215
+ # Set up environment variables and aliases for Claude, Codex and Hermes
184
216
  RUN for shell_rc in /home/node/.zshrc /home/node/.bashrc; do \
185
217
  echo "export CLAUDE_CONFIG_DIR=\"/home/node/claudeconfig\"" >> "$shell_rc" && \
186
- echo "export CODEX_CONFIG_HOME=\"/home/node/codexconfig\"" >> "$shell_rc" && \
218
+ echo "export CODEX_HOME=\"/home/node/codexconfig\"" >> "$shell_rc" && \
187
219
  echo "export PATH=\"/home/node/.local/bin:\$PATH\"" >> "$shell_rc" && \
188
220
  echo "alias claude=\"claude --dangerously-skip-permissions\"" >> "$shell_rc" && \
189
221
  echo "alias codex=\"/usr/local/share/npm-global/bin/codex -c check_for_update_on_startup=false --dangerously-bypass-approvals-and-sandbox\"" >> "$shell_rc" && \
190
222
  echo "alias codex-normal=\"/usr/local/share/npm-global/bin/codex -c check_for_update_on_startup=false\"" >> "$shell_rc" && \
223
+ echo "alias hermes='/home/node/.local/bin/hermes --yolo \${HERMES_INFERENCE_MODEL:+--model=\$HERMES_INFERENCE_MODEL}'" >> "$shell_rc" && \
224
+ echo "alias hermes-normal='/home/node/.local/bin/hermes \${HERMES_INFERENCE_MODEL:+--model=\$HERMES_INFERENCE_MODEL}'" >> "$shell_rc" && \
191
225
  echo "" >> "$shell_rc" && \
192
226
  echo "# Auto-activate Python venv if it exists" >> "$shell_rc" && \
193
227
  echo "if [ -f /home/node/.devs-venv/workspace-venv/bin/activate ]; then" >> "$shell_rc" && \
@@ -37,6 +37,7 @@
37
37
  "source=claude-code-bashhistory,target=/commandhistory,type=volume",
38
38
  "source=${localEnv:HOME}/.devs/claudeconfig,target=/home/node/claudeconfig,type=bind",
39
39
  "source=${localEnv:HOME}/.devs/codexconfig,target=/home/node/codexconfig,type=bind",
40
+ "source=${localEnv:HOME}/.devs/hermesconfig,target=/home/node/hermesconfig,type=bind",
40
41
  "source=${localEnv:DEVS_ENV_MOUNT_PATH},target=/home/node/.devs-env,type=bind",
41
42
  "source=${localEnv:DEVS_BRIDGE_MOUNT_PATH},target=/home/node/bridge,type=bind",
42
43
  // Remove the extensions mount to get fresh state each time
@@ -16,16 +16,30 @@ if [ -f /home/node/.devs-env/.env ]; then
16
16
  set -a; . /home/node/.devs-env/.env; set +a
17
17
  fi
18
18
 
19
+ # Per-developer start hooks: ~/.devs/envs/<project>/autoexec-start/ on the host.
20
+ # The every-start counterpart of the create-time autoexec/ — for whatever a hook
21
+ # launched that a restart kills along with everything else that isn't PID 1. Same
22
+ # runner; no directory => no-op. Called last on either path below, so a hook that
23
+ # needs tailnet names finds MagicDNS already in place.
24
+ run_start_hooks() {
25
+ DEVS_AUTOEXEC_DIR=/home/node/.devs-env/autoexec-start /usr/local/bin/run-autoexec.sh \
26
+ || echo "⚠️ autoexec-start hook failed (continuing)."
27
+ }
28
+
19
29
  # VS Code machine settings (+ optional tmux overlay, off unless DEVS_TMUX=1). Not
20
30
  # Tailscale-gated — it configures the editor regardless of how you connect. Must come
21
31
  # before the TS_ENABLE gate below, which exits when Tailscale is off.
22
32
  /usr/local/bin/setup-vscode-settings.sh || true
23
33
 
34
+ # Hermes Agent's default model, seeded into its host-persisted config if none is set yet.
35
+ /usr/local/bin/setup-hermes.sh || true
36
+
24
37
  # Gate on the same master switch as the rest of the Tailscale wiring.
25
38
  case "${TS_ENABLE:-}" in
26
39
  1|true|yes) ;;
27
40
  *)
28
41
  echo "ℹ️ Tailscale not enabled (TS_ENABLE=${TS_ENABLE:-unset}) — skipping tailnet setup."
42
+ run_start_hooks
29
43
  exit 0 ;;
30
44
  esac
31
45
 
@@ -41,3 +55,5 @@ esac
41
55
  # container. Must run after the bring-up above (it no-ops unless tailscaled is up),
42
56
  # and on every start because Docker regenerates /etc/resolv.conf each time.
43
57
  sudo -n /usr/local/bin/setup-magicdns.sh || true
58
+
59
+ run_start_hooks
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+ # Seed Hermes Agent's default model into its persisted config (runs as the node user from
3
+ # post-start.sh on every container start).
4
+ #
5
+ # HERMES_HOME (/home/node/hermesconfig) is bind-mounted from ~/.devs/hermesconfig on the
6
+ # host, so config.yaml can't be baked into the image. Without a configured model Hermes
7
+ # falls back to its own pick, so write HERMES_DEFAULT_MODEL (a Dockerfile build arg) the
8
+ # first time — but never overwrite a model or provider the user has chosen since, e.g.
9
+ # with `hermes model`. HERMES_INFERENCE_MODEL still overrides per container at run time.
10
+ set -uo pipefail
11
+
12
+ HERMES=/home/node/.local/bin/hermes
13
+ [ -x "$HERMES" ] || exit 0
14
+ [ -n "${HERMES_DEFAULT_MODEL:-}" ] || exit 0
15
+
16
+ if [ -n "$("$HERMES" config get model.default 2>/dev/null)" ]; then
17
+ exit 0
18
+ fi
19
+
20
+ if [ -z "$("$HERMES" config get model.provider 2>/dev/null)" ]; then
21
+ "$HERMES" config set model.provider openrouter >/dev/null || exit 0
22
+ fi
23
+ "$HERMES" config set model.default "$HERMES_DEFAULT_MODEL" >/dev/null &&
24
+ echo "✅ Hermes default model set to $HERMES_DEFAULT_MODEL in $HERMES_HOME/config.yaml"
@@ -68,24 +68,6 @@ check_github_token_setup() {
68
68
  git config --global core.trustctime false
69
69
  echo "✅ Git core.trustctime set to false (prevents timezone-related rebase issues)"
70
70
 
71
- # Ensure it's available in all shell sessions for the node user
72
- if ! grep -q "source /home/node/.devs-env/.env" /home/node/.zshrc 2>/dev/null; then
73
- echo "# Load devs environment variables" >> /home/node/.zshrc
74
- echo "if [ -f /home/node/.devs-env/.env ]; then" >> /home/node/.zshrc
75
- echo " set -a" >> /home/node/.zshrc
76
- echo " source /home/node/.devs-env/.env" >> /home/node/.zshrc
77
- echo " set +a" >> /home/node/.zshrc
78
- echo "fi" >> /home/node/.zshrc
79
- fi
80
- if ! grep -q "source /home/node/.devs-env/.env" /home/node/.bashrc 2>/dev/null; then
81
- echo "# Load devs environment variables" >> /home/node/.bashrc
82
- echo "if [ -f /home/node/.devs-env/.env ]; then" >> /home/node/.bashrc
83
- echo " set -a" >> /home/node/.bashrc
84
- echo " source /home/node/.devs-env/.env" >> /home/node/.bashrc
85
- echo " set +a" >> /home/node/.bashrc
86
- echo "fi" >> /home/node/.bashrc
87
- fi
88
-
89
71
  # Test if gh CLI can authenticate
90
72
  if command -v gh >/dev/null 2>&1; then
91
73
  if gh auth status >/dev/null 2>&1; then
@@ -104,10 +86,45 @@ check_github_token_setup() {
104
86
  fi
105
87
  }
106
88
 
89
+ # Load the mounted env file in every shell for the node user. This used to happen only
90
+ # when GH_TOKEN was set, which left other keys in the file (e.g. OPENROUTER_API_KEY for
91
+ # Hermes) invisible to interactive shells and to `devs <agent>` execs.
92
+ setup_env_file_sourcing() {
93
+ local rc
94
+ for rc in /home/node/.zshrc /home/node/.bashrc; do
95
+ if ! grep -q "source /home/node/.devs-env/.env" "$rc" 2>/dev/null; then
96
+ {
97
+ echo "# Load devs environment variables"
98
+ echo "if [ -f /home/node/.devs-env/.env ]; then"
99
+ echo " set -a"
100
+ echo " source /home/node/.devs-env/.env"
101
+ echo " set +a"
102
+ echo "fi"
103
+ } >> "$rc"
104
+ fi
105
+ done
106
+ }
107
+
108
+ # Report whether Hermes Agent has an inference provider key
109
+ check_hermes_setup() {
110
+ if [ -n "${OPENROUTER_API_KEY:-}" ]; then
111
+ echo "✅ OpenRouter API key (OPENROUTER_API_KEY) is available - Hermes Agent is ready"
112
+ else
113
+ echo "ℹ️ No OpenRouter API key (OPENROUTER_API_KEY) configured"
114
+ echo " To use Hermes Agent, add OPENROUTER_API_KEY=your_key to"
115
+ echo " ~/.devs/envs/<project-name>/.env (or ~/.devs/envs/default/.env)"
116
+ fi
117
+ }
118
+
107
119
  # Check SSH access (configured during build)
108
120
  check_ssh_setup
109
121
 
110
122
  # Check GitHub token access (configured via mounted env files)
111
123
  check_github_token_setup
112
124
 
125
+ setup_env_file_sourcing
126
+
127
+ # check_github_token_setup sourced the env file, so OPENROUTER_API_KEY is visible here
128
+ check_hermes_setup
129
+
113
130
  echo "Devs environment setup complete!"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 5.0.5
3
+ Version: 6.0.0
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  devs_common/__init__.py
5
+ devs_common/agents.py
5
6
  devs_common/config.py
6
7
  devs_common/devs_config.py
7
8
  devs_common/exceptions.py
@@ -31,6 +32,7 @@ devs_common/templates/scripts/post-start.sh
31
32
  devs_common/templates/scripts/python-venv-integration.sh
32
33
  devs_common/templates/scripts/run-autoexec.sh
33
34
  devs_common/templates/scripts/screenshot-webapp.sh
35
+ devs_common/templates/scripts/setup-hermes.sh
34
36
  devs_common/templates/scripts/setup-vscode-settings.sh
35
37
  devs_common/templates/scripts/setup-workspace.sh
36
38
  devs_common/templates/scripts/start-tailscale.sh
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devs-common"
7
- version = "5.0.5"
7
+ version = "6.0.0"
8
8
  description = "Shared utilities for devs package ecosystem"
9
9
  authors = [
10
10
  {name = "Dan Lester"},
File without changes
File without changes
File without changes