Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions plugins/codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,22 @@ rules. The shared runner remains a plain shell entrypoint that the skill calls.

## Install

On Linux/macOS:
```bash
git clone <repo-url> SkillOpt-Sleep
cd SkillOpt-Sleep
bash plugins/codex/install.sh # installs the skill
export SKILLOPT_SLEEP_REPO="$(pwd)" # so the runner is found from anywhere
```

On Windows (PowerShell):
```powershell
git clone <repo-url> SkillOpt-Sleep
cd SkillOpt-Sleep
powershell -File plugins/codex/install.ps1
[System.Environment]::SetEnvironmentVariable("SKILLOPT_SLEEP_REPO", "$(pwd)", "User")
```

If a previous install created `~/.codex/prompts/sleep.md`, the installer moves
that deprecated prompt aside with a `.skillopt-legacy*.bak` suffix.

Expand Down
47 changes: 47 additions & 0 deletions plugins/codex/install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Install the SkillOpt-Sleep Codex integration as a user-level Codex skill on Windows.
# Idempotent; prints what it does.

$ErrorActionPreference = "Stop"

$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
$CodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE ".codex" }
$AgentsSkills = Join-Path $env:USERPROFILE ".agents\skills"
$LegacyPrompt = Join-Path $CodexHome "prompts\sleep.md"

Write-Output "[install] repo: $RepoRoot"

# 1) user-level skill
$SkillDir = Join-Path $AgentsSkills "skillopt-sleep"
if (-not (Test-Path $SkillDir)) {
New-Item -ItemType Directory -Path $SkillDir -Force | Out-Null
}
Copy-Item (Join-Path $RepoRoot "plugins\codex\skills\skillopt-sleep\SKILL.md") (Join-Path $SkillDir "SKILL.md") -Force
Write-Output "[install] skill -> $(Join-Path $SkillDir 'SKILL.md')"

# 2) retire the old custom prompt entrypoint from previous installs
if (Test-Path $LegacyPrompt) {
$Backup = "${LegacyPrompt}.skillopt-legacy.bak"
if (Test-Path $Backup) {
$DateStr = Get-Date -Format "yyyyMMddHHmmss"
$Backup = "${LegacyPrompt}.skillopt-legacy.${DateStr}.bak"
}
Move-Item $LegacyPrompt $Backup -Force
Write-Output "[install] legacy prompt -> $Backup"
}

# 3) record the repo location so the runner is found from anywhere
Write-Output "[install] add to your environment variables:"
Write-Output " [System.Environment]::SetEnvironmentVariable('SKILLOPT_SLEEP_REPO', '$RepoRoot', 'User')"
Write-Output " Or set it via System Properties."

# 4) optional: append an AGENTS.md hint (only if the user opts in)
Write-Output ""
Write-Output "[install] Optional — add this to ~/.codex/AGENTS.md so Codex always knows the tool:"
Write-Output ""
Write-Output " ## SkillOpt-Sleep"
Write-Output " Use the skillopt-sleep skill when I ask to run a sleep/dream/offline"
Write-Output " self-improvement cycle. The runner is:"
Write-Output " \`powershell -File `"$RepoRoot\plugins\run-sleep.ps1`" status --project `"\$(pwd)\`"\`."
Write-Output ""
Write-Output "Done. Try asking Codex:"
Write-Output " Use the skillopt-sleep skill to run status for this project."
12 changes: 12 additions & 0 deletions plugins/codex/skills/skillopt-sleep/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" run --project "$(pwd)" --source
bash "$SKILLOPT_SLEEP_REPO/plugins/run-sleep.sh" adopt --project "$(pwd)"
```

On Windows (CMD / PowerShell):
```cmd
:: CMD
set SKILLOPT_SLEEP_REPO=C:\path\to\SkillOpt-Sleep
"%SKILLOPT_SLEEP_REPO%\plugins\run-sleep.cmd" status --project "%CD%"
```
```powershell
# PowerShell
$env:SKILLOPT_SLEEP_REPO = "C:\path\to\SkillOpt-Sleep"
powershell -File "$env:SKILLOPT_SLEEP_REPO\plugins\run-sleep.ps1" status --project "$(pwd)"
```

Actions are `status`, `harvest`, `dry-run`, `run`, `adopt`, `schedule`, and `unschedule`.

- Default backend is `mock`, which is deterministic and spends no API budget.
Expand Down
67 changes: 67 additions & 0 deletions plugins/run-sleep.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@echo off
setlocal enabledelayedexpansion

:: Resolve REPO_ROOT
set "SCRIPT_DIR=%~dp0"
:: Strip trailing backslash
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"

if exist "%SCRIPT_DIR%\..\skillopt_sleep" (
cd /d "%SCRIPT_DIR%\.."
set "REPO_ROOT=%CD%"
) else if not "%CLAUDE_PLUGIN_ROOT%"=="" if exist "%CLAUDE_PLUGIN_ROOT%\..\..\skillopt_sleep" (
cd /d "%CLAUDE_PLUGIN_ROOT%\..\.."
set "REPO_ROOT=%CD%"
) else if not "%SKILLOPT_SLEEP_REPO%"=="" if exist "%SKILLOPT_SLEEP_REPO%\skillopt_sleep" (
set "REPO_ROOT=%SKILLOPT_SLEEP_REPO%"
) else (
:: Search upward from current directory
set "d=%CD%"
set "REPO_ROOT="
:loop
if exist "!d!\skillopt_sleep" (
set "REPO_ROOT=!d!"
goto found
)
for %%I in ("!d!") do set "parent=%%~dpI"
:: Strip trailing backslash from parent if it's not root
if "!parent!"=="!d!" goto notfound
set "parent=!parent:~0,-1!"
if "!parent!"=="" goto notfound
set "d=!parent!"
goto loop
:notfound
echo [sleep] ERROR: could not locate the skillopt_sleep package. Set SKILLOPT_SLEEP_REPO to the repo root. >&2
exit /b 1
:found
)

:: Find python >= 3.10
set "PY="
if not "%SKILLOPT_SLEEP_PYTHON%"=="" (
set "PY=%SKILLOPT_SLEEP_PYTHON%"
) else (
for %%p in (python3.exe python.exe py.exe) do (
where %%p >nul 2>nul
if !errorlevel! equ 0 (
%%p -c "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)" >nul 2>nul
if !errorlevel! equ 0 (
set "PY=%%p"
goto py_found
)
)
)
)
:py_found

if "%PY%"=="" (
echo [sleep] ERROR: need Python >= 3.10 (found none). >&2
exit /b 1
)

cd /d "%REPO_ROOT%"
if "%~1"=="" (
"%PY%" -m skillopt_sleep status
) else (
"%PY%" -m skillopt_sleep %*
)
62 changes: 62 additions & 0 deletions plugins/run-sleep.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SkillOpt-Sleep shared runner for Windows PowerShell
# Resolves the repo root, picks Python >= 3.10, and runs the engine CLI.
#
# Usage: .\run-sleep.ps1 [status|run|dry-run|adopt|...] [args...]

$ErrorActionPreference = "Stop"

$ScriptDir = $PSScriptRoot

$RepoRoot = $null
if (Test-Path (Join-Path $ScriptDir "..\skillopt_sleep")) {
$RepoRoot = Resolve-Path (Join-Path $ScriptDir "..")
} elseif ($env:CLAUDE_PLUGIN_ROOT -and (Test-Path (Join-Path $env:CLAUDE_PLUGIN_ROOT "..\..\skillopt_sleep"))) {
$RepoRoot = Resolve-Path (Join-Path $env:CLAUDE_PLUGIN_ROOT "..\..")
} elseif ($env:SKILLOPT_SLEEP_REPO -and (Test-Path (Join-Path $env:SKILLOPT_SLEEP_REPO "skillopt_sleep"))) {
$RepoRoot = Resolve-Path $env:SKILLOPT_SLEEP_REPO
} else {
# search upward from current location
$d = Get-Item .
while ($d -and $d.FullName -ne $d.Root.FullName) {
if (Test-Path (Join-Path $d.FullName "skillopt_sleep")) {
$RepoRoot = $d.FullName
break
}
$d = Split-Path -Parent $d.FullName -ErrorAction SilentlyContinue | Get-Item -ErrorAction SilentlyContinue
}
}

if (-not $RepoRoot) {
Write-Error "[sleep] ERROR: could not locate the skillopt_sleep package. Set SKILLOPT_SLEEP_REPO to the repo root."
exit 1
}

$Py = ""
if ($env:SKILLOPT_SLEEP_PYTHON) {
$Py = $env:SKILLOPT_SLEEP_PYTHON
} else {
foreach ($cand in @("python3", "python", "py")) {
$cmd = Get-Command $cand -ErrorAction SilentlyContinue
if ($cmd) {
$ver = & $cand -c "import sys; print('%d%d' % sys.version_info[:2])" 2>$null
if ($ver -and [int]$ver -ge 310) {
$Py = $cand
break
}
}
}
}

if (-not $Py) {
Write-Error "[sleep] ERROR: need Python >= 3.10 (found none)."
exit 1
}

$argsList = @($args)
if ($argsList.Count -eq 0) {
$argsList = @("status")
}

Set-Location $RepoRoot
# Run using the call operator
& $Py -m skillopt_sleep $argsList
83 changes: 59 additions & 24 deletions skillopt_sleep/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,14 +711,27 @@ def resolve_codex_path(explicit: str = "") -> str:
env = os.environ.get("SKILLOPT_SLEEP_CODEX_PATH")
if env:
return env
candidates = [
os.path.expanduser("~/.nvm/versions/node/v22.22.3/bin/codex"),
]
# any nvm node version
nvm = os.path.expanduser("~/.nvm/versions/node")
if os.path.isdir(nvm):
for ver in sorted(os.listdir(nvm), reverse=True):
candidates.append(os.path.join(nvm, ver, "bin", "codex"))
import sys
candidates = []
if sys.platform == "win32":
appdata = os.environ.get("APPDATA")
if appdata:
candidates.append(os.path.join(appdata, "npm", "codex.cmd"))
userprofile = os.environ.get("USERPROFILE")
if userprofile:
candidates.append(os.path.join(userprofile, "AppData", "Roaming", "npm", "codex.cmd"))
nvm_home = os.environ.get("NVM_HOME")
if nvm_home:
candidates.append(os.path.join(nvm_home, "codex.cmd"))
else:
candidates = [
os.path.expanduser("~/.nvm/versions/node/v22.22.3/bin/codex"),
]
# any nvm node version
nvm = os.path.expanduser("~/.nvm/versions/node")
if os.path.isdir(nvm):
for ver in sorted(os.listdir(nvm), reverse=True):
candidates.append(os.path.join(nvm, ver, "bin", "codex"))
for c in candidates:
if not c or not os.path.exists(c):
continue
Expand Down Expand Up @@ -861,23 +874,45 @@ def attempt_with_tools(self, task, skill, memory, tools):
work = tempfile.mkdtemp(prefix="skillopt_sleep_codextools_")
calllog = os.path.join(work, "_tool_calls.log")
out_path = os.path.join(work, "_last.txt")
tool_names = tools or ["search"]
is_windows = os.name == "nt"
try:
for tname in (tools or ["search"]):
shim = os.path.join(work, tname)
with open(shim, "w") as f:
f.write(
"#!/usr/bin/env bash\n"
f'echo "{tname}" >> "{calllog}"\n'
'echo "(search results: 3 relevant notes found; use them to answer)"\n'
)
os.chmod(shim, os.stat(shim).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
tool_hint = (
"Shell tools are available in the working directory: "
+ ", ".join(f"./{t}" for t in (tools or ["search"]))
+ ". When the skill says to look something up or search before "
"answering, you MUST actually run the tool (e.g. `./search \"query\"`) "
"before giving your final answer."
)
for tname in tool_names:
if is_windows:
shim = os.path.join(work, f"{tname}.cmd")
with open(shim, "w") as f:
f.write(
"@echo off\n"
f'echo %~n0>>"{calllog}"\n'
"echo (search results: 3 relevant notes found; use them to answer)\n"
)
else:
shim = os.path.join(work, tname)
with open(shim, "w") as f:
f.write(
"#!/usr/bin/env bash\n"
f'echo "{tname}" >> "{calllog}"\n'
'echo "(search results: 3 relevant notes found; use them to answer)"\n'
)
os.chmod(shim, os.stat(shim).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
if is_windows:
tool_hint = (
"Shell tools are available in the working directory: "
+ ", ".join(f"{t}.cmd" for t in tool_names)
+ " (each callable as `" + tool_names[0] + "` or `.\\"
+ tool_names[0] + "`). When the skill says to look something "
"up or search before answering, you MUST actually run the "
"tool (e.g. `" + tool_names[0] + " \"query\"`) before giving "
"your final answer."
)
else:
tool_hint = (
"Shell tools are available in the working directory: "
+ ", ".join(f"./{t}" for t in tool_names)
+ ". When the skill says to look something up or search before "
"answering, you MUST actually run the tool (e.g. `./search \"query\"`) "
"before giving your final answer."
)
prompt = (
"Complete the task. Apply the skill and memory rules EXACTLY, "
"including any rule about searching before answering. Treat a "
Expand Down
2 changes: 2 additions & 0 deletions skillopt_sleep/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def _runner_cmd(project: str, backend: str, extra: str, python: str) -> str:
# use absolute python + -m so cron's minimal env still works
cmd = (f'{python} -m skillopt_sleep run --project "{project}" '
f'--scope invoked --backend {backend} {extra}'.rstrip())
if sys.platform == "win32":
return f'if not exist "{logdir}" mkdir "{logdir}" && cd /d "{_repo_root()}" && {cmd} >> "{log}" 2>&1'
return f'mkdir -p "{logdir}"; cd "{_repo_root()}" && {cmd} >> "{log}" 2>&1'


Expand Down
Loading